<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Tony Bai &#187; Redhat</title>
	<atom:link href="http://tonybai.com/tag/redhat/feed/" rel="self" type="application/rss+xml" />
	<link>https://tonybai.com</link>
	<description>一个程序员的心路历程</description>
	<lastBuildDate>Wed, 15 Apr 2026 23:34:11 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.2.1</generator>
		<item>
		<title>代码之外的必修课：顶级技术文档风格指南如何提升你的工程效率</title>
		<link>https://tonybai.com/2025/07/14/writing-style-guide/</link>
		<comments>https://tonybai.com/2025/07/14/writing-style-guide/#comments</comments>
		<pubDate>Mon, 14 Jul 2025 12:30:40 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Bash]]></category>
		<category><![CDATA[eslint]]></category>
		<category><![CDATA[Git]]></category>
		<category><![CDATA[gofmt]]></category>
		<category><![CDATA[PR]]></category>
		<category><![CDATA[README.md]]></category>
		<category><![CDATA[Redhat]]></category>
		<category><![CDATA[UI]]></category>
		<category><![CDATA[写作]]></category>
		<category><![CDATA[开源]]></category>
		<category><![CDATA[技术债]]></category>
		<category><![CDATA[文档]]></category>
		<category><![CDATA[沟通]]></category>
		<category><![CDATA[社区]]></category>
		<category><![CDATA[软技能]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4904</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/07/14/writing-style-guide 大家好，我是Tony Bai。 作为一名开发者、架构师或运维专家，我们大部分时间都在与代码、系统和架构打交道。然而，我们同样在持续不断地进行另一种形式的“编码”——沟通编码。无论是撰写一个清晰的 README.md，提交一份详尽的 Pull Request 描述，编写项目内部的技术文档，还是在社区中回答一个问题，我们都在扮演着“技术作者”的角色。 代码的质量决定了软件能否运行，而沟通的质量则决定了项目能否高效协作、知识能否有效传承、社区能否健康发展。一份糟糕的文档，如同晦涩难懂的“面条代码”，会极大地消耗团队的精力和热情。 最近，Redhat公司发布了《Red Hat Technical Writing Style Guide》7.1版本。这份指南不仅仅是一系列规则的集合，它更像是一部由顶级开源软件公司沉淀下来的、关于如何通过清晰沟通来提升工程效率的哲学。 在这篇文章中，我将提炼其中的一些精髓，探讨那些能直接提升您和团队工程能力的写作原则，供大家参考。 写作的“第一性原理”：清晰、精确、用户至上 技术文档的首要目标是传递信息，任何模糊、冗长或模棱两可的表达都是工程效率的天敌。指南强调了几个核心原则： 1. 拥抱主动语态，指令明确无误 主动语态让指令更直接、更有力。在指导性文档中，这能显著降低读者的认知负荷。 不推荐 (被动语态) 推荐 (主动语态) Linuxconf can be started by typing &#8230; Type &#8230; to start Linuxconf. 新的配置可以被应用通过重启服务。 重启服务以应用新的配置。 对开发者的价值：当用户（或未来的你）阅读操作手册时，清晰的指令意味着更低的出错率和更快的解决问题速度。 2. 杜绝冗余，尊重读者的时间 避免使用不必要的填充词，让每一句话都言之有物。 冗余 精炼 Perform the installation of the product. Install the [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/writing-style-guide-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/07/14/writing-style-guide">本文永久链接</a> &#8211; https://tonybai.com/2025/07/14/writing-style-guide</p>
<p>大家好，我是Tony Bai。</p>
<p>作为一名开发者、架构师或运维专家，我们大部分时间都在与代码、系统和架构打交道。然而，我们同样在持续不断地进行另一种形式的“编码”——<strong>沟通编码</strong>。无论是撰写一个清晰的 README.md，提交一份详尽的 Pull Request 描述，编写项目内部的技术文档，还是在社区中回答一个问题，我们都在扮演着“技术作者”的角色。</p>
<p>代码的质量决定了软件能否运行，而<strong>沟通的质量则决定了项目能否高效协作、知识能否有效传承、社区能否健康发展</strong>。一份糟糕的文档，如同晦涩难懂的“面条代码”，会极大地消耗团队的精力和热情。</p>
<p>最近，Redhat公司发布了《<a href="https://www.stylepedia.net/style/">Red Hat Technical Writing Style Guide</a>》7.1版本。这份指南不仅仅是一系列规则的集合，它更像是一部由顶级开源软件公司沉淀下来的、关于<strong>如何通过清晰沟通来提升工程效率</strong>的哲学。</p>
<p>在这篇文章中，我将提炼其中的一些精髓，探讨那些能直接提升您和团队工程能力的写作原则，供大家参考。</p>
<h2>写作的“第一性原理”：清晰、精确、用户至上</h2>
<p>技术文档的首要目标是传递信息，任何模糊、冗长或模棱两可的表达都是工程效率的天敌。指南强调了几个核心原则：</p>
<h3>1. 拥抱主动语态，指令明确无误</h3>
<p>主动语态让指令更直接、更有力。在指导性文档中，这能显著降低读者的认知负荷。</p>
<table>
<thead>
<tr>
<th align="left">不推荐 (被动语态)</th>
<th align="left"><strong>推荐 (主动语态)</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">Linuxconf can be started by typing &#8230;</td>
<td align="left">Type &#8230; to start Linuxconf.</td>
</tr>
<tr>
<td align="left">新的配置<strong>可以被应用</strong>通过重启服务。</td>
<td align="left"><strong>重启服务</strong>以应用新的配置。</td>
</tr>
</tbody>
</table>
<p><strong>对开发者的价值</strong>：当用户（或未来的你）阅读操作手册时，清晰的指令意味着更低的出错率和更快的解决问题速度。</p>
<h3>2. 杜绝冗余，尊重读者的时间</h3>
<p>避免使用不必要的填充词，让每一句话都言之有物。</p>
<table>
<thead>
<tr>
<th align="left">冗余</th>
<th align="left"><strong>精炼</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">Perform the installation of the product.</td>
<td align="left">Install the product.</td>
</tr>
<tr>
<td align="left">This problem is located on the /dev/sda1 partition.</td>
<td align="left">This problem is on the /dev/sda1 partition.</td>
</tr>
</tbody>
</table>
<h3>3. 避免歧义：This 指的是什么？</h3>
<p>在技术文档中，代词（如 this, that, it）是歧义的重灾区，尤其对于翻译和非母语阅读者。指南建议明确指出代词所指代的的名词。</p>
<pre><code>- A site can use these to self-assign a private routable IP address space.
+ A site can use these unique local addresses to self-assign a private routable IP address space.

- This causes SSH to lose the recorded identities.
+ This action causes SSH to lose the recorded identities.
</code></pre>
<p><strong>对开发者的价值</strong>：在复杂的配置说明或问题排查指南中，消除代词歧义可以防止因误解而导致的配置错误。</p>
<h2>为全球化社区而写：包容性与可翻译性</h2>
<p>开源项目和现代技术团队本质上是全球化的。我们的文档需要被不同文化背景的人阅读和翻译。</p>
<h3>1. 使用包容性语言</h3>
<p>这是现代技术社区的基石。避免使用可能带有偏见或冒犯性的术语，有助于建立一个更健康、更多元化的社区环境。</p>
<ul>
<li><strong>master/slave</strong> -> 推荐使用 primary/replica, controller/worker, leader/follower 等。</li>
<li><strong>whitelist/blacklist</strong> -> 推荐使用 allowlist/denylist 或 blocklist。</li>
<li><strong>性别代词</strong> -> 避免使用 he/she，推荐使用中性的 they（可指代单数）或直接使用第二人称 you。</li>
</ul>
<h3>2. 为翻译而设计</h3>
<p>糟糕的措辞会给机器翻译和人工翻译带来灾难。一些简单的规则可以极大地提升文档的可翻译性：</p>
<ul>
<li><strong>避免使用俚语和行话</strong>：eat your own dogfood (使用自己的产品), boil the ocean (范围过大) 等表达在其他文化中可能完全无法理解。</li>
<li><strong>慎用 may 和 should</strong>：may 可能表示“可能性”或“许可”，should 可能表示“建议”或“期望”。使用 can (可以), might (可能), must (必须) 会更精确。</li>
<li><strong>避免名词堆叠</strong>：Standard system log management configuration 这种连续名词的组合，在翻译时极易出错。可以调整为 Standard configuration of system log management。</li>
</ul>
<h2>工程师的文字“代码规范”：一致性与标准化</h2>
<p>如同 eslint 或 gofmt 为代码提供规范一样，风格指南为我们的文字提供了“格式化”标准。这能确保整个项目文档风格统一，易于阅读和维护。</p>
<h3>1. 统一命令语法文档</h3>
<p>在展示命令行示例时，保持一致的格式至关重要。</p>
<pre><code class="bash"># 一个清晰的命令语法示例
$ git clone [username@]hostname:/repository_filename [directory]
</code></pre>
<pre><code>- 使用 $ 表示普通用户，# 表示 root 用户。
- 使用 [] 表示可选参数。
- 使用斜体或描述性词语（如 _filename_）表示 需替换的值。
- 在需要省略输出时，使用 ...output omitted... 标记，而不是随意删减。
</code></pre>
<h3>2. 精确描述 UI 元素</h3>
<p>当描述用户界面时，精确和简洁是关键。</p>
<ul>
<li><strong>直接了当</strong>：不说 Click the Save button，而说 Click <strong>Save</strong>。</li>
<li><strong>名称匹配</strong>：文档中的 UI 元素名称（如按钮、菜单项）应与界面上显示的<strong>完全一致</strong>（包括大小写）。</li>
<li><strong>导航路径</strong>：使用 -> 或 →清晰地表示导航路径，例如：Go to Monitoring → Metrics。</li>
</ul>
<h3>3. 避免产品名称的所有格</h3>
<p>一个看似微小但能提升专业度的细节：</p>
<ul>
<li><strong>不推荐</strong>: Red Hat OpenShift&#8217;s Logging operator creates&#8230;</li>
<li><strong>推荐</strong>: The Red Hat OpenShift Logging operator creates&#8230;</li>
</ul>
<h2>总结与展望：将沟通视为工程技艺</h2>
<p>《红帽风格指南》带给我们的最大启示是：<strong>清晰、精确、专业的书面沟通不是一种“软技能”，而是工程技艺（Craftsmanship）不可或缺的一部分</strong>。它与编写高质量代码、设计健壮架构同等重要。</p>
<p>下一次，当你准备提交一个 Pull Request、更新一份 README，或撰写一篇技术博客时，不妨尝试运用其中的一两个原则：</p>
<ul>
<li>将一个被动语态的句子改为主动语态。</li>
<li>检查是否有模糊的代词 it 或 this 可以被替换。</li>
<li>思考一下你使用的术语是否足够包容和全球通用。</li>
</ul>
<p>投资于沟通，就是投资于整个团队的效率和项目的未来。正如一份优雅的代码令人赏心悦悦目，一份清晰的文档同样能带来极致的工程之美。</p>
<hr />
<p>你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？</p>
<ul>
<li>想写出更地道、更健壮的Go代码，却总在细节上踩坑？</li>
<li>渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？</li>
<li>想打造生产级的Go服务，却在工程化实践中屡屡受挫？</li>
</ul>
<p>继《<a href="http://gk.link/a/10AVZ">Go语言第一课</a>》后，我的《<a href="http://gk.link/a/12yGY">Go语言进阶课</a>》终于在极客时间与大家见面了！</p>
<p>我的全新极客时间专栏 《<a href="http://gk.link/a/12yGY">Tony Bai·Go语言进阶课</a>》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。</p>
<p>目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /></p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/07/14/writing-style-guide/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>使用Go开发Kubernetes Operator：基本结构</title>
		<link>https://tonybai.com/2022/08/15/developing-kubernetes-operators-in-go-part1/</link>
		<comments>https://tonybai.com/2022/08/15/developing-kubernetes-operators-in-go-part1/#comments</comments>
		<pubDate>Mon, 15 Aug 2022 14:47:40 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[API]]></category>
		<category><![CDATA[builder]]></category>
		<category><![CDATA[cluster]]></category>
		<category><![CDATA[container]]></category>
		<category><![CDATA[controller]]></category>
		<category><![CDATA[coreos]]></category>
		<category><![CDATA[CR]]></category>
		<category><![CDATA[CRD]]></category>
		<category><![CDATA[CustomResourceDefinition]]></category>
		<category><![CDATA[DaemonSet]]></category>
		<category><![CDATA[deployment]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[etcd]]></category>
		<category><![CDATA[framework]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[image]]></category>
		<category><![CDATA[k8s]]></category>
		<category><![CDATA[kubebuilder]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[Make]]></category>
		<category><![CDATA[Makefile]]></category>
		<category><![CDATA[nginx]]></category>
		<category><![CDATA[operator]]></category>
		<category><![CDATA[operator-framework]]></category>
		<category><![CDATA[operator-sdk]]></category>
		<category><![CDATA[pod]]></category>
		<category><![CDATA[prometheus]]></category>
		<category><![CDATA[reconcile]]></category>
		<category><![CDATA[reconciliation]]></category>
		<category><![CDATA[Redhat]]></category>
		<category><![CDATA[replicas]]></category>
		<category><![CDATA[ReplicaSet]]></category>
		<category><![CDATA[resource]]></category>
		<category><![CDATA[role]]></category>
		<category><![CDATA[role-binding]]></category>
		<category><![CDATA[scale]]></category>
		<category><![CDATA[SDK]]></category>
		<category><![CDATA[Service]]></category>
		<category><![CDATA[service-account]]></category>
		<category><![CDATA[spec]]></category>
		<category><![CDATA[TPR]]></category>
		<category><![CDATA[webserver]]></category>
		<category><![CDATA[伸缩]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3638</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2022/08/15/developing-kubernetes-operators-in-go-part1 注：文章首图基于《Kubernetes Operators Explained》修改 几年前，我还称Kubernetes为服务编排和容器调度领域的事实标准，如今K8s已经是这个领域的“霸主”，地位无可撼动。不过，虽然Kubernetes发展演化到今天已经变得非常复杂，但是Kubernetes最初的数据模型、应用模式与扩展方式却依然有效。并且像Operator这样的应用模式和扩展方式日益受到开发者与运维者的欢迎。 我们的平台内部存在有状态(stateful)的后端服务，对有状态的服务的部署和运维是k8s operator的拿手好戏，是时候来研究一下operator了。 一. Operator的优点 kubernetes operator的概念最初来自CoreOS &#8211; 一家被红帽(redhat)收购的容器技术公司。 CoreOS在引入Operator概念的同时，也给出了Operator的第一批参考实现：etcd operator和prometheus operator。 注：etcd于2013年由CoreOS以开源形式发布；prometheus作为首款面向云原生服务的时序数据存储与监控系统，由SoundCloud公司于2012年以开源的形式发布。 下面是CoreOS对Operator这一概念的诠释：Operator在软件中代表了人类的运维操作知识，通过它可以可靠地管理一个应用程序。 图：CoreOS对operator的诠释(截图来自CoreOS官方博客归档) Operator出现的初衷就是用来解放运维人员的，如今Operator也越来越受到云原生运维开发人员的青睐。 那么operator好处究竟在哪里呢？下面示意图对使用Operator和不使用Operator进行了对比： 通过这张图，即便对operator不甚了解，你也能大致感受到operator的优点吧。 我们看到在使用operator的情况下，对有状态应用的伸缩操作(这里以伸缩操作为例，也可以是其他诸如版本升级等对于有状态应用来说的“复杂”操作)，运维人员仅需一个简单的命令即可，运维人员也无需知道k8s内部对有状态应用的伸缩操作的原理是什么。 在没有使用operator的情况下，运维人员需要对有状态应用的伸缩的操作步骤有深刻的认知，并按顺序逐个执行一个命令序列中的命令并检查命令响应，遇到失败的情况时还需要进行重试，直到伸缩成功。 我们看到operator就好比一个内置于k8s中的经验丰富运维人员，时刻监控目标对象的状态，把复杂性留给自己，给运维人员一个简洁的交互接口，同时operator也能降低运维人员因个人原因导致的操作失误的概率。 不过，operator虽好，但开发门槛却不低。开发门槛至少体现在如下几个方面： 对operator概念的理解是基于对k8s的理解的基础之上的，而k8s自从2014年开源以来，变的日益复杂，理解起来需要一定时间投入； 从头手撸operator很verbose，几乎无人这么做，大多数开发者都会去学习相应的开发框架与工具，比如：kubebuilder、operator framework sdk等； operator的能力也有高低之分，operator framework就提出了一个包含五个等级的operator能力模型(CAPABILITY MODEL)，见下图。使用Go开发高能力等级的operator需要对client-go这个kubernetes官方go client库中的API有深入的了解。 图：operator能力模型(截图来自operator framework官网) 当然在这些门槛当中，对operator概念的理解既是基础也是前提，而理解operator的前提又是对kubernetes的诸多概念要有深入理解，尤其是resource、resource type、API、controller以及它们之间的关系。接下来我们就来快速介绍一下这些概念。 二. Kubernetes resource、resource type、API和controller介绍 Kubernetes发展到今天，其本质已经显现： Kubernetes就是一个“数据库”(数据实际持久存储在etcd中)； 其API就是“sql语句”； API设计采用基于resource的Restful风格, resource type是API的端点(endpoint)； 每一类resource(即Resource Type)是一张“表”，Resource Type的spec对应“表结构”信息(schema)； 每张“表”里的一行记录就是一个resource，即该表对应的Resource Type的一个实例(instance)； [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/developing-kubernetes-operators-in-go-part1-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2022/08/15/developing-kubernetes-operators-in-go-part1">本文永久链接</a> &#8211; https://tonybai.com/2022/08/15/developing-kubernetes-operators-in-go-part1</p>
<blockquote>
<p>注：文章首图基于《Kubernetes Operators Explained》修改</p>
</blockquote>
<p><a href="https://tonybai.com/2018/10/17/imooc-course-kubernetes-practice-go-online/">几年前，我还称Kubernetes为服务编排和容器调度领域的事实标准</a>，如今K8s已经是这个领域的“霸主”，地位无可撼动。不过，虽然Kubernetes发展演化到今天已经变得非常复杂，但是Kubernetes最初的数据模型、应用模式与扩展方式却依然有效。并且像<a href="https://kubernetes.io/docs/concepts/extend-kubernetes/operator/">Operator这样的应用模式和扩展方式</a>日益受到开发者与运维者的欢迎。</p>
<p>我们的平台内部存在有状态(stateful)的后端服务，对有状态的服务的部署和运维是k8s operator的<strong>拿手好戏</strong>，是时候来研究一下operator了。</p>
<h3>一. Operator的优点</h3>
<p><a href="https://web.archive.org/web/20170129131616/https://coreos.com/blog/introducing-operators.html">kubernetes operator的概念最初来自CoreOS</a> &#8211; 一家被红帽(redhat)收购的容器技术公司。</p>
<p>CoreOS在引入Operator概念的同时，也给出了Operator的第一批参考实现：<a href="https://web.archive.org/web/20170224100544/https://coreos.com/blog/introducing-the-etcd-operator.html">etcd operator</a>和<a href="https://web.archive.org/web/20170224101137/https://coreos.com/blog/the-prometheus-operator.html">prometheus operator</a>。</p>
<blockquote>
<p>注：<a href="https://etcd.io">etcd</a>于2013年由CoreOS以开源形式发布；<a href="https://prometheus.io">prometheus</a>作为首款面向云原生服务的时序数据存储与监控系统，由SoundCloud公司于2012年以开源的形式发布。</p>
</blockquote>
<p>下面是CoreOS对Operator这一概念的诠释：<strong>Operator在软件中代表了人类的运维操作知识，通过它可以可靠地管理一个应用程序</strong>。</p>
<p><img src="https://tonybai.com/wp-content/uploads/developing-kubernetes-operators-in-go-part1-4.png" alt="" /><br />
<center>图：CoreOS对operator的诠释(截图来自CoreOS官方博客归档)</center></p>
<p>Operator出现的初衷就是用来解放运维人员的，如今Operator也越来越受到云原生运维开发人员的青睐。</p>
<p>那么operator好处究竟在哪里呢？下面示意图对使用Operator和不使用Operator进行了对比：</p>
<p><img src="https://tonybai.com/wp-content/uploads/developing-kubernetes-operators-in-go-part1-2.png" alt="" /></p>
<p>通过这张图，即便对operator不甚了解，你也能大致感受到operator的优点吧。</p>
<p>我们看到在使用operator的情况下，对有状态应用的伸缩操作(这里以伸缩操作为例，也可以是其他诸如版本升级等对于有状态应用来说的“复杂”操作)，运维人员仅需一个简单的命令即可，运维人员也无需知道k8s内部对有状态应用的伸缩操作的原理是什么。</p>
<p>在没有使用operator的情况下，运维人员需要对有状态应用的伸缩的操作步骤有深刻的认知，并按顺序逐个执行一个命令序列中的命令并检查命令响应，遇到失败的情况时还需要进行重试，直到伸缩成功。</p>
<p>我们看到operator就好比一个内置于k8s中的经验丰富运维人员，时刻监控目标对象的状态，把复杂性留给自己，给运维人员一个简洁的交互接口，同时operator也能降低运维人员因个人原因导致的操作失误的概率。</p>
<p>不过，operator虽好，但开发门槛却不低。开发门槛至少体现在如下几个方面：</p>
<ul>
<li>对operator概念的理解是基于对k8s的理解的基础之上的，而k8s自从2014年开源以来，变的日益复杂，理解起来需要一定时间投入；</li>
<li>从头手撸operator很verbose，几乎无人这么做，大多数开发者都会去学习相应的开发框架与工具，比如：<a href="https://github.com/kubernetes-sigs/kubebuilder">kubebuilder</a>、<a href="https://sdk.operatorframework.io">operator framework sdk</a>等；</li>
<li>operator的能力也有高低之分，operator framework就提出了一个包含<strong>五个等级的operator能力模型(CAPABILITY MODEL)</strong>，见下图。使用Go开发高能力等级的operator需要对<a href="https://github.com/kubernetes/client-go">client-go</a>这个kubernetes官方go client库中的API有深入的了解。</li>
</ul>
<p><img src="https://tonybai.com/wp-content/uploads/developing-kubernetes-operators-in-go-part1-3.png" alt="" /><br />
<center>图：operator能力模型(截图来自operator framework官网)</center></p>
<p>当然在这些门槛当中，对operator概念的理解既是基础也是前提，而理解operator的前提又是对kubernetes的诸多概念要有深入理解，尤其是resource、resource type、API、controller以及它们之间的关系。接下来我们就来快速介绍一下这些概念。</p>
<h3>二. Kubernetes resource、resource type、API和controller介绍</h3>
<p>Kubernetes发展到今天，其本质已经显现：</p>
<ul>
<li>Kubernetes就是一个“数据库”(数据实际持久存储在etcd中)；</li>
<li>其API就是“sql语句”；</li>
<li>API设计采用基于resource的Restful风格, resource type是API的端点(endpoint)；</li>
<li>每一类resource(即Resource Type)是一张“表”，Resource Type的spec对应“表结构”信息(schema)；</li>
<li>每张“表”里的一行记录就是一个resource，即该表对应的Resource Type的一个实例(instance)；</li>
<li>Kubernetes这个“数据库”内置了很多“表”，比如Pod、Deployment、DaemonSet、ReplicaSet等；</li>
</ul>
<p>下面是一个Kubernetes API与resource关系的示意图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/developing-kubernetes-operators-in-go-part1-5.png" alt="" /></p>
<p>我们看到resource type有两类，一类的namespace相关的(namespace-scoped)，我们通过下面形式的API操作这类resource type的实例：</p>
<pre><code>VERB /apis/GROUP/VERSION/namespaces/NAMESPACE/RESOURCETYPE - 操作某特定namespace下面的resouce type中的resource实例集合
VERB /apis/GROUP/VERSION/namespaces/NAMESPACE/RESOURCETYPE/NAME - 操作某特定namespace下面的resource type中的某个具体的resource实例
</code></pre>
<p>另外一类则是namespace无关，即cluster范围(cluster-scoped)的，我们通过下面形式的API对这类resource type的实例进行操作：</p>
<pre><code>VERB /apis/GROUP/VERSION/RESOURCETYPE - 操作resouce type中的resource实例集合
VERB /apis/GROUP/VERSION/RESOURCETYPE/NAME - 操作resource type中的某个具体的resource实例
</code></pre>
<p>我们知道Kubernetes并非真的只是一个“数据库”，它是服务编排和容器调度的平台标准，它的基本调度单元是Pod(也是一个resource type)，即一组容器的集合。那么Pod又是如何被创建、更新和删除的呢？这就离不开控制器(controller)了。<strong>每一类resource type都有自己对应的控制器(controller)</strong>。以pod这个resource type为例，它的controller为ReplicasSet的实例。</p>
<p>控制器的运行逻辑如下图所示：</p>
<p><img src="https://tonybai.com/wp-content/uploads/developing-kubernetes-operators-in-go-part1-6.png" alt="" /><br />
<center>图：控制器运行逻辑(引自《Kubernetes Operators Explained》一文)</center></p>
<p>控制器一旦启动，将尝试获得resource的当前状态(current state)，并与存储在k8s中的resource的期望状态（desired state，即spec)做比对，如果不一致，controller就会调用相应API进行调整，尽力使得current state与期望状态达成一致。这个达成一致的过程被称为<strong>协调(reconciliation)</strong>，协调过程的伪代码逻辑如下：</p>
<pre><code>for {
    desired := getDesiredState()
    current := getCurrentState()
    makeChanges(desired, current)
}
</code></pre>
<blockquote>
<p>注：k8s中有一个object的概念？那么object是什么呢？它类似于Java Object基类或Ruby中的Object超类。不仅resource type的实例resource是一个(is-a)object，resource type本身也是一个object，它是kubernetes concept的实例。</p>
</blockquote>
<p>有了上面对k8s这些概念的初步理解，我们下面就来理解一下Operator究竟是什么！</p>
<h3>三. Operator模式 = 操作对象(CRD) + 控制逻辑(controller)</h3>
<p>如果让运维人员直面这些内置的resource type(如deployment、pod等)，也就是前面“使用operator vs. 不使用operator”对比图中的第二种情况, 运维人员面临的情况将会很复杂，且操作易错。</p>
<p>那么如果不直面内置的resource type，那么我们如何自定义resource type呢, Kubernetes提供了Custom Resource Definition，CRD(在coreos刚提出operator概念的时候，crd的前身是Third Party Resource, TPR)可以用于自定义resource type。</p>
<p>根据前面我们对resource type理解，定义CRD相当于建立新“表”(resource type)，一旦CRD建立，k8s会为我们自动生成对应CRD的API endpoint，我们就可以通过yaml或API来操作这个“表”。我们可以向“表”中“插入”数据，即基于CRD创建Custom Resource(CR)，这就好比我们创建Deployment实例，向Deployment“表”中插入数据一样。</p>
<p>和原生内置的resource type一样，光有存储对象状态的CR还不够，原生resource type有对应controller负责协调(reconciliation)实例的创建、伸缩与删除，CR也需要这样的“协调者”，即我们也需要定义一个controller来负责监听CR状态并管理CR创建、伸缩、删除以及保持期望状态(spec)与当前状态(current state)的一致。这个controller不再是面向原生Resource type的实例，而是<strong>面向CRD的实例CR的controller</strong>。</p>
<p>有了自定义的操作对象类型(CRD)，有了面向操作对象类型实例的controller，我们将其打包为一个概念：“Operator模式”，operator模式中的controller也被称为operator，它是在集群中对CR进行维护操作的主体。</p>
<h3>四. 使用kubebuilder开发webserver operator</h3>
<blockquote>
<p>假设：此时你的本地开发环境已经具备访问实验用k8s环境的一切配置，通过kubectl工具可以任意操作k8s。</p>
</blockquote>
<p><strong>再深入浅出的概念讲解都不如一次实战对理解概念更有帮助</strong>，下面我们就来开发一个简单的Operator。</p>
<p>前面提过operator开发非常verbose，因此社区提供了开发工具和框架来帮助开发人员简化开发过程，目前主流的包括operator framework sdk和kubebuilder，前者是redhat开源并维护的一套工具，支持使用go、ansible、helm进行operator开发(其中只有go可以开发到能力级别5的operator，其他两种则不行)；而kubebuilder则是kubernetes官方的一个sig(特别兴趣小组)维护的operator开发工具。目前基于operator framework sdk和go进行operator开发时，operator sdk底层使用的也是kubebuilder，所以这里我们就直接使用kubebuilder来开发operator。</p>
<p>按照operator能力模型，我们这个operator差不多处于2级这个层次，我们定义一个Webserver的resource type，它代表的是一个基于nginx的webserver集群，我们的operator支持创建webserver示例(一个nginx集群)，支持nginx集群伸缩，支持集群中nginx的版本升级。</p>
<p>下面我们就用kubebuilder来实现这个operator！</p>
<h4>1. 安装kubebuilder</h4>
<p>这里我们采用源码构建方式安装，步骤如下：</p>
<pre><code>$git clone git@github.com:kubernetes-sigs/kubebuilder.git
$cd kubebuilder
$make
$cd bin
$./kubebuilder version
Version: main.version{KubeBuilderVersion:"v3.5.0-101-g5c949c2e",
KubernetesVendor:"unknown",
GitCommit:"5c949c2e50ca8eec80d64878b88e1b2ee30bf0bc",
BuildDate:"2022-08-06T09:12:50Z", GoOs:"linux", GoArch:"amd64"}
</code></pre>
<p>然后将bin/kubebuilder拷贝到你的PATH环境变量中的某个路径下即可。</p>
<h4>2. 创建webserver-operator工程</h4>
<p>接下来，我们就可以使用kubebuilder创建webserver-operator工程了：</p>
<pre><code>$mkdir webserver-operator
$cd webserver-operator
$kubebuilder init  --repo github.com/bigwhite/webserver-operator --project-name webserver-operator

Writing kustomize manifests for you to edit...
Writing scaffold for you to edit...
Get controller runtime:
$ go get sigs.k8s.io/controller-runtime@v0.12.2
go: downloading k8s.io/client-go v0.24.2
go: downloading k8s.io/component-base v0.24.2
Update dependencies:
$ go mod tidy
Next: define a resource with:
kubebuilder create api
</code></pre>
<blockquote>
<p>注：&#8211;repo指定go.mod中的module root path，你可以定义你自己的module root path。</p>
</blockquote>
<h4>3. 创建API，生成初始CRD</h4>
<p>Operator包括CRD和controller，这里我们就来建立自己的CRD，即自定义的resource type，也就是API的endpoint，我们使用下面kubebuilder create命令来完成这个步骤：</p>
<pre><code>$kubebuilder create api --version v1 --kind WebServer
Create Resource [y/n]
y
Create Controller [y/n]
y
Writing kustomize manifests for you to edit...
Writing scaffold for you to edit...
api/v1/webserver_types.go
controllers/webserver_controller.go
Update dependencies:
$ go mod tidy
Running make:
$ make generate
mkdir -p /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin
test -s /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen || GOBIN=/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.9.2
/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
Next: implement your new API and generate the manifests (e.g. CRDs,CRs) with:
$ make manifests
</code></pre>
<p>之后，我们执行make manifests来生成最终CRD对应的yaml文件：</p>
<pre><code>$make manifests
/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
</code></pre>
<p>此刻，整个工程的目录文件布局如下：</p>
<pre><code>$tree -F .
.
├── api/
│   └── v1/
│       ├── groupversion_info.go
│       ├── webserver_types.go
│       └── zz_generated.deepcopy.go
├── bin/
│   └── controller-gen*
├── config/
│   ├── crd/
│   │   ├── bases/
│   │   │   └── my.domain_webservers.yaml
│   │   ├── kustomization.yaml
│   │   ├── kustomizeconfig.yaml
│   │   └── patches/
│   │       ├── cainjection_in_webservers.yaml
│   │       └── webhook_in_webservers.yaml
│   ├── default/
│   │   ├── kustomization.yaml
│   │   ├── manager_auth_proxy_patch.yaml
│   │   └── manager_config_patch.yaml
│   ├── manager/
│   │   ├── controller_manager_config.yaml
│   │   ├── kustomization.yaml
│   │   └── manager.yaml
│   ├── prometheus/
│   │   ├── kustomization.yaml
│   │   └── monitor.yaml
│   ├── rbac/
│   │   ├── auth_proxy_client_clusterrole.yaml
│   │   ├── auth_proxy_role_binding.yaml
│   │   ├── auth_proxy_role.yaml
│   │   ├── auth_proxy_service.yaml
│   │   ├── kustomization.yaml
│   │   ├── leader_election_role_binding.yaml
│   │   ├── leader_election_role.yaml
│   │   ├── role_binding.yaml
│   │   ├── role.yaml
│   │   ├── service_account.yaml
│   │   ├── webserver_editor_role.yaml
│   │   └── webserver_viewer_role.yaml
│   └── samples/
│       └── _v1_webserver.yaml
├── controllers/
│   ├── suite_test.go
│   └── webserver_controller.go
├── Dockerfile
├── go.mod
├── go.sum
├── hack/
│   └── boilerplate.go.txt
├── main.go
├── Makefile
├── PROJECT
└── README.md

14 directories, 40 files
</code></pre>
<h4>4. webserver-operator的基本结构</h4>
<p>忽略我们此次不关心的诸如leader election、auth_proxy等，我将这个operator例子的主要部分整理到下面这张图中：</p>
<p><img src="https://tonybai.com/wp-content/uploads/developing-kubernetes-operators-in-go-part1-7.png" alt="" /></p>
<p>图中的各个部分就是使用kubebuilder生成的<strong>operator的基本结构</strong>。</p>
<p>webserver operator主要由CRD和controller组成：</p>
<ul>
<li>CRD</li>
</ul>
<p>图中的左下角的框框就是上面生成的CRD yaml文件：config/crd/bases/my.domain_webservers.yaml。CRD与api/v1/webserver_types.go密切相关。我们在api/v1/webserver_types.go中为CRD定义spec相关字段，之后make manifests命令可以解析webserver_types.go中的变化并更新CRD的yaml文件。</p>
<ul>
<li>controller</li>
</ul>
<p>从图的右侧部分可以看出，controller自身就是作为一个deployment部署在k8s集群中运行的，它监视CRD的实例CR的运行状态，并在Reconcile方法中检查预期状态与当前状态是否一致，如果不一致，则执行相关操作。</p>
<ul>
<li>其它</li>
</ul>
<p>图中左上角是有关controller的权限的设置，controller通过serviceaccount访问k8s API server，通过role.yaml和role_binding.yaml设置controller的角色和权限。</p>
<h4>5. 为CRD spec添加字段(field)</h4>
<p>为了实现Webserver operator的功能目标，我们需要为CRD spec添加一些状态字段。前面说过，CRD与api中的webserver_types.go文件是同步的，我们只需修改webserver_types.go文件即可。我们在WebServerSpec结构体中增加Replicas和Image两个字段，它们分别用于表示webserver实例的副本数量以及使用的容器镜像：</p>
<pre><code>// api/v1/webserver_types.go

// WebServerSpec defines the desired state of WebServer
type WebServerSpec struct {
    // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
    // Important: Run "make" to regenerate code after modifying this file

    // The number of replicas that the webserver should have
    Replicas int `json:"replicas,omitempty"`

    // The container image of the webserver
    Image string `json:"image,omitempty"`

    // Foo is an example field of WebServer. Edit webserver_types.go to remove/update
    Foo string `json:"foo,omitempty"`
}
</code></pre>
<p>保存修改后，<strong>执行make manifests</strong>重新生成config/crd/bases/my.domain_webservers.yaml</p>
<pre><code>$cat my.domain_webservers.yaml
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    controller-gen.kubebuilder.io/version: v0.9.2
  creationTimestamp: null
  name: webservers.my.domain
spec:
  group: my.domain
  names:
    kind: WebServer
    listKind: WebServerList
    plural: webservers
    singular: webserver
  scope: Namespaced
  versions:
  - name: v1
    schema:
      openAPIV3Schema:
        description: WebServer is the Schema for the webservers API
        properties:
          apiVersion:
            description: 'APIVersion defines the versioned schema of this representation
              of an object. Servers should convert recognized schemas to the latest
              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
            type: string
          kind:
            description: 'Kind is a string value representing the REST resource this
              object represents. Servers may infer this from the endpoint the client
              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
            type: string
          metadata:
            type: object
          spec:
            description: WebServerSpec defines the desired state of WebServer
            properties:
              foo:
                description: Foo is an example field of WebServer. Edit webserver_types.go
                  to remove/update
                type: string
              image:
                description: The container image of the webserver
                type: string
              replicas:
                description: The number of replicas that the webserver should have
                type: integer
            type: object
          status:
            description: WebServerStatus defines the observed state of WebServer
            type: object
        type: object
    served: true
    storage: true
    subresources:
      status: {}
</code></pre>
<p>一旦定义完CRD，我们就可以将其安装到k8s中：</p>
<pre><code>$make install
/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
test -s /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/kustomize || { curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash -s -- 3.8.7 /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin; }
{Version:kustomize/v3.8.7 GitCommit:ad092cc7a91c07fdf63a2e4b7f13fa588a39af4f BuildDate:2020-11-11T23:14:14Z GoOs:linux GoArch:amd64}
kustomize installed to /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/kustomize
/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/kustomize build config/crd | kubectl apply -f -
customresourcedefinition.apiextensions.k8s.io/webservers.my.domain created
</code></pre>
<p>检查安装情况：</p>
<pre><code>$kubectl get crd|grep webservers
webservers.my.domain                                             2022-08-06T21:55:45Z
</code></pre>
<h4>6. 修改role.yaml</h4>
<p>在开始controller开发之前，我们先来为controller后续的运行“铺平道路”，即设置好相应权限。</p>
<p>我们在controller中会为CRD实例创建对应deployment和service，这样就要求controller有操作deployments和services的权限，这样就需要我们修改role.yaml，增加service account:  controller-manager 操作deployments和services的权限：</p>
<pre><code>// config/rbac/role.yaml
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  creationTimestamp: null
  name: manager-role
rules:
- apiGroups:
  - my.domain
  resources:
  - webservers
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - my.domain
  resources:
  - webservers/finalizers
  verbs:
  - update
- apiGroups:
  - my.domain
  resources:
  - webservers/status
  verbs:
  - get
  - patch
  - update
- apiGroups:
  - apps
  resources:
  - deployments
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - apps
  - ""
  resources:
  - services
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
</code></pre>
<p>修改后的role.yaml先放在这里，后续与controller一并部署到k8s上。</p>
<h4>7. 实现controller的Reconcile(协调)逻辑</h4>
<p>kubebuilder为我们搭好了controller的代码架子，我们只需要在controllers/webserver_controller.go中实现WebServerReconciler的Reconcile方法即可。下面是Reconcile的一个简易流程图，结合这幅图理解代码就容易的多了：</p>
<p><img src="https://tonybai.com/wp-content/uploads/developing-kubernetes-operators-in-go-part1-8.png" alt="" /></p>
<p>下面是对应的Reconcile方法的代码：</p>
<pre><code>// controllers/webserver_controller.go

func (r *WebServerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    log := r.Log.WithValues("Webserver", req.NamespacedName)

    instance := &amp;mydomainv1.WebServer{}
    err := r.Get(ctx, req.NamespacedName, instance)
    if err != nil {
        if errors.IsNotFound(err) {
            // Request object not found, could have been deleted after reconcile request.
            // Return and don't requeue
            log.Info("Webserver resource not found. Ignoring since object must be deleted")
            return ctrl.Result{}, nil
        }

        // Error reading the object - requeue the request.
        log.Error(err, "Failed to get Webserver")
        return ctrl.Result{RequeueAfter: time.Second * 5}, err
    }

    // Check if the webserver deployment already exists, if not, create a new one
    found := &amp;appsv1.Deployment{}
    err = r.Get(ctx, types.NamespacedName{Name: instance.Name, Namespace: instance.Namespace}, found)
    if err != nil &amp;&amp; errors.IsNotFound(err) {
        // Define a new deployment
        dep := r.deploymentForWebserver(instance)
        log.Info("Creating a new Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
        err = r.Create(ctx, dep)
        if err != nil {
            log.Error(err, "Failed to create new Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
            return ctrl.Result{RequeueAfter: time.Second * 5}, err
        }
        // Deployment created successfully - return and requeue
        return ctrl.Result{Requeue: true}, nil
    } else if err != nil {
        log.Error(err, "Failed to get Deployment")
        return ctrl.Result{RequeueAfter: time.Second * 5}, err
    }

    // Ensure the deployment replicas and image are the same as the spec
    var replicas int32 = int32(instance.Spec.Replicas)
    image := instance.Spec.Image

    var needUpd bool
    if *found.Spec.Replicas != replicas {
        log.Info("Deployment spec.replicas change", "from", *found.Spec.Replicas, "to", replicas)
        found.Spec.Replicas = &amp;replicas
        needUpd = true
    }

    if (*found).Spec.Template.Spec.Containers[0].Image != image {
        log.Info("Deployment spec.template.spec.container[0].image change", "from", (*found).Spec.Template.Spec.Containers[0].Image, "to", image)
        found.Spec.Template.Spec.Containers[0].Image = image
        needUpd = true
    }

    if needUpd {
        err = r.Update(ctx, found)
        if err != nil {
            log.Error(err, "Failed to update Deployment", "Deployment.Namespace", found.Namespace, "Deployment.Name", found.Name)
            return ctrl.Result{RequeueAfter: time.Second * 5}, err
        }
        // Spec updated - return and requeue
        return ctrl.Result{Requeue: true}, nil
    }

    // Check if the webserver service already exists, if not, create a new one
    foundService := &amp;corev1.Service{}
    err = r.Get(ctx, types.NamespacedName{Name: instance.Name + "-service", Namespace: instance.Namespace}, foundService)
    if err != nil &amp;&amp; errors.IsNotFound(err) {
        // Define a new service
        srv := r.serviceForWebserver(instance)
        log.Info("Creating a new Service", "Service.Namespace", srv.Namespace, "Service.Name", srv.Name)
        err = r.Create(ctx, srv)
        if err != nil {
            log.Error(err, "Failed to create new Servie", "Service.Namespace", srv.Namespace, "Service.Name", srv.Name)
            return ctrl.Result{RequeueAfter: time.Second * 5}, err
        }
        // Service created successfully - return and requeue
        return ctrl.Result{Requeue: true}, nil
    } else if err != nil {
        log.Error(err, "Failed to get Service")
        return ctrl.Result{RequeueAfter: time.Second * 5}, err
    }

    // Tbd: Ensure the service state is the same as the spec, your homework

    // reconcile webserver operator in again 10 seconds
    return ctrl.Result{RequeueAfter: time.Second * 10}, nil
}
</code></pre>
<p>这里大家可能发现了：<strong>原来CRD的controller最终还是将CR翻译为k8s原生Resource，比如service、deployment等。CR的状态变化(比如这里的replicas、image等)最终都转换成了deployment等原生resource的update操作</strong>，这就是operator的精髓！理解到这一层，operator对大家来说就不再是什么密不可及的概念了。</p>
<p>有些朋友可能也会发现，上面流程图中似乎没有考虑CR实例被删除时对deployment、service的操作，的确如此。不过对于一个7&#215;24小时运行于后台的服务来说，我们更多关注的是其变更、伸缩、升级等操作，删除是优先级最低的需求。</p>
<h4>8. 构建controller image</h4>
<p>controller代码写完后，我们就来构建controller的image。通过前文我们知道，这个controller其实就是运行在k8s中的一个deployment下的pod。我们需要构建其image并通过deployment部署到k8s中。</p>
<p>kubebuilder创建的operator工程中包含了Makefile，通过make docker-build即可构建controller image。docker-build使用golang builder image来构建controller源码，不过如果不对Dockerfile稍作修改，你很难编译过去，因为默认GOPROXY在国内无法访问。这里最简单的改造方式是使用vendor构建，下面是改造后的Dockerfile：</p>
<pre><code># Build the manager binary
FROM golang:1.18 as builder

ENV GOPROXY https://goproxy.cn
WORKDIR /workspace
# Copy the Go Modules manifests
COPY go.mod go.mod
COPY go.sum go.sum
COPY vendor/ vendor/
# cache deps before building and copying source so that we don't need to re-download as much
# and so that source changes don't invalidate our downloaded layer
#RUN go mod download

# Copy the go source
COPY main.go main.go
COPY api/ api/
COPY controllers/ controllers/

# Build
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -mod=vendor -a -o manager main.go

# Use distroless as minimal base image to package the manager binary
# Refer to https://github.com/GoogleContainerTools/distroless for more details
#FROM gcr.io/distroless/static:nonroot
FROM katanomi/distroless-static:nonroot
WORKDIR /
COPY --from=builder /workspace/manager .
USER 65532:65532

ENTRYPOINT ["/manager"]
</code></pre>
<p>下面是构建的步骤：</p>
<pre><code>$go mod vendor
$make docker-build

test -s /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen || GOBIN=/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.9.2
/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
go fmt ./...
go vet ./...
KUBEBUILDER_ASSETS="/home/tonybai/.local/share/kubebuilder-envtest/k8s/1.24.2-linux-amd64" go test ./... -coverprofile cover.out
?       github.com/bigwhite/webserver-operator    [no test files]
?       github.com/bigwhite/webserver-operator/api/v1    [no test files]
ok      github.com/bigwhite/webserver-operator/controllers    4.530s    coverage: 0.0% of statements
docker build -t bigwhite/webserver-controller:latest .
Sending build context to Docker daemon  47.51MB
Step 1/15 : FROM golang:1.18 as builder
 ---&gt; 2d952adaec1e
Step 2/15 : ENV GOPROXY https://goproxy.cn
 ---&gt; Using cache
 ---&gt; db2b06a078e3
Step 3/15 : WORKDIR /workspace
 ---&gt; Using cache
 ---&gt; cc3c613c19c6
Step 4/15 : COPY go.mod go.mod
 ---&gt; Using cache
 ---&gt; 5fa5c0d89350
Step 5/15 : COPY go.sum go.sum
 ---&gt; Using cache
 ---&gt; 71669cd0fe8e
Step 6/15 : COPY vendor/ vendor/
 ---&gt; Using cache
 ---&gt; 502b280a0e67
Step 7/15 : COPY main.go main.go
 ---&gt; Using cache
 ---&gt; 0c59a69091bb
Step 8/15 : COPY api/ api/
 ---&gt; Using cache
 ---&gt; 2b81131c681f
Step 9/15 : COPY controllers/ controllers/
 ---&gt; Using cache
 ---&gt; e3fd48c88ccb
Step 10/15 : RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -mod=vendor -a -o manager main.go
 ---&gt; Using cache
 ---&gt; 548ac10321a2
Step 11/15 : FROM katanomi/distroless-static:nonroot
 ---&gt; 421f180b71d8
Step 12/15 : WORKDIR /
 ---&gt; Running in ea7cb03027c0
Removing intermediate container ea7cb03027c0
 ---&gt; 9d3c0ea19c3b
Step 13/15 : COPY --from=builder /workspace/manager .
 ---&gt; a4387fe33ab7
Step 14/15 : USER 65532:65532
 ---&gt; Running in 739a32d251b6
Removing intermediate container 739a32d251b6
 ---&gt; 52ae8742f9c5
Step 15/15 : ENTRYPOINT ["/manager"]
 ---&gt; Running in 897893b0c9df
Removing intermediate container 897893b0c9df
 ---&gt; e375cc2adb08
Successfully built e375cc2adb08
Successfully tagged bigwhite/webserver-controller:latest
</code></pre>
<blockquote>
<p>注：执行make命令之前，先将Makefile中的IMG变量初值改为IMG ?= bigwhite/webserver-controller:latest</p>
</blockquote>
<p>构建成功后，执行make docker-push将image推送到镜像仓库中(这里使用了docker公司提供的公共仓库)。</p>
<h4>9. 部署controller</h4>
<p>之前我们已经通过make install将CRD安装到k8s中了，接下来再把controller部署到k8s上，我们的operator就算部署完毕了。执行make deploy即可实现部署：</p>
<pre><code>$make deploy
test -s /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen || GOBIN=/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.9.2
/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
test -s /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/kustomize || { curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash -s -- 3.8.7 /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin; }
cd config/manager &amp;&amp; /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/kustomize edit set image controller=bigwhite/webserver-controller:latest
/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/kustomize build config/default | kubectl apply -f -
namespace/webserver-operator-system created
customresourcedefinition.apiextensions.k8s.io/webservers.my.domain unchanged
serviceaccount/webserver-operator-controller-manager created
role.rbac.authorization.k8s.io/webserver-operator-leader-election-role created
clusterrole.rbac.authorization.k8s.io/webserver-operator-manager-role created
clusterrole.rbac.authorization.k8s.io/webserver-operator-metrics-reader created
clusterrole.rbac.authorization.k8s.io/webserver-operator-proxy-role created
rolebinding.rbac.authorization.k8s.io/webserver-operator-leader-election-rolebinding created
clusterrolebinding.rbac.authorization.k8s.io/webserver-operator-manager-rolebinding created
clusterrolebinding.rbac.authorization.k8s.io/webserver-operator-proxy-rolebinding created
configmap/webserver-operator-manager-config created
service/webserver-operator-controller-manager-metrics-service created
deployment.apps/webserver-operator-controller-manager created
</code></pre>
<p>我们看到deploy不仅会安装controller、serviceaccount、role、rolebinding，它还会创建namespace，也会将crd安装一遍。也就是说deploy是一个完整的operator安装命令。</p>
<blockquote>
<p>注：使用make undeploy可以完整卸载operator相关resource。</p>
</blockquote>
<p>我们用kubectl logs查看一下controller的运行日志：</p>
<pre><code>$kubectl logs -f deployment.apps/webserver-operator-controller-manager -n webserver-operator-system
1.6600280818476188e+09    INFO    controller-runtime.metrics    Metrics server is starting to listen    {"addr": "127.0.0.1:8080"}
1.6600280818478029e+09    INFO    setup    starting manager
1.6600280818480284e+09    INFO    Starting server    {"path": "/metrics", "kind": "metrics", "addr": "127.0.0.1:8080"}
1.660028081848097e+09    INFO    Starting server    {"kind": "health probe", "addr": "[::]:8081"}
I0809 06:54:41.848093       1 leaderelection.go:248] attempting to acquire leader lease webserver-operator-system/63e5a746.my.domain...
I0809 06:54:57.072336       1 leaderelection.go:258] successfully acquired lease webserver-operator-system/63e5a746.my.domain
1.6600280970724037e+09    DEBUG    events    Normal    {"object": {"kind":"Lease","namespace":"webserver-operator-system","name":"63e5a746.my.domain","uid":"e05aaeb5-4a3a-4272-b036-80d61f0b6788","apiVersion":"coordination.k8s.io/v1","resourceVersion":"5238800"}, "reason": "LeaderElection", "message": "webserver-operator-controller-manager-6f45bc88f7-ptxlc_0e960015-9fbe-466d-a6b1-ff31af63a797 became leader"}
1.6600280970724993e+09    INFO    Starting EventSource    {"controller": "webserver", "controllerGroup": "my.domain", "controllerKind": "WebServer", "source": "kind source: *v1.WebServer"}
1.6600280970725305e+09    INFO    Starting Controller    {"controller": "webserver", "controllerGroup": "my.domain", "controllerKind": "WebServer"}
1.660028097173026e+09    INFO    Starting workers    {"controller": "webserver", "controllerGroup": "my.domain", "controllerKind": "WebServer", "worker count": 1}
</code></pre>
<p>可以看到，controller已经成功启动，正在等待一个WebServer CR的相关事件(比如创建)！下面我们就来创建一个WebServer CR!</p>
<h4>10. 创建WebServer CR</h4>
<p>webserver-operator项目中有一个CR sample，位于config/samples下面，我们对其进行改造，添加我们在spec中加入的字段：</p>
<pre><code>// config/samples/_v1_webserver.yaml 

apiVersion: my.domain/v1
kind: WebServer
metadata:
  name: webserver-sample
spec:
  # TODO(user): Add fields here
  image: nginx:1.23.1
  replicas: 3
</code></pre>
<p>我们通过kubectl创建该WebServer CR：</p>
<pre><code>$cd config/samples
$kubectl apply -f _v1_webserver.yaml
webserver.my.domain/webserver-sample created
</code></pre>
<p>观察controller的日志：</p>
<pre><code>1.6602084232243123e+09  INFO    controllers.WebServer   Creating a new Deployment   {"Webserver": "default/webserver-sample", "Deployment.Namespace": "default", "Deployment.Name": "webserver-sample"}
1.6602084233446114e+09  INFO    controllers.WebServer   Creating a new Service  {"Webserver": "default/webserver-sample", "Service.Namespace": "default", "Service.Name": "webserver-sample-service"}
</code></pre>
<p>我们看到当CR被创建后，controller监听到相关事件，创建了对应的Deployment和service，我们查看一下为CR创建的Deployment、三个Pod以及service：</p>
<pre><code>$kubectl get service
NAME                       TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
kubernetes                 ClusterIP   172.26.0.1     &lt;none&gt;        443/TCP        22d
webserver-sample-service   NodePort    172.26.173.0   &lt;none&gt;        80:30010/TCP   2m58s

$kubectl get deployment
NAME               READY   UP-TO-DATE   AVAILABLE   AGE
webserver-sample   3/3     3            3           4m44s

$kubectl get pods
NAME                               READY   STATUS    RESTARTS   AGE
webserver-sample-bc698b9fb-8gq2h   1/1     Running   0          4m52s
webserver-sample-bc698b9fb-vk6gw   1/1     Running   0          4m52s
webserver-sample-bc698b9fb-xgrgb   1/1     Running   0          4m52s
</code></pre>
<p>我们访问一下该服务：</p>
<pre><code>$curl http://192.168.10.182:30010
&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
&lt;title&gt;Welcome to nginx!&lt;/title&gt;
&lt;style&gt;
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;h1&gt;Welcome to nginx!&lt;/h1&gt;
&lt;p&gt;If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.&lt;/p&gt;

&lt;p&gt;For online documentation and support please refer to
&lt;a href="http://nginx.org/"&gt;nginx.org&lt;/a&gt;.&lt;br/&gt;
Commercial support is available at
&lt;a href="http://nginx.com/"&gt;nginx.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Thank you for using nginx.&lt;/em&gt;&lt;/p&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>服务如预期返回响应！</p>
<h4>11. 伸缩、变更版本和Service自愈</h4>
<p>接下来我们来对CR做一些常见的运维操作。</p>
<ul>
<li>副本数由3变为4</li>
</ul>
<p>我们将CR的replicas由3改为4，对容器实例做一次扩展操作：</p>
<pre><code>// config/samples/_v1_webserver.yaml 

apiVersion: my.domain/v1
kind: WebServer
metadata:
  name: webserver-sample
spec:
  # TODO(user): Add fields here
  image: nginx:1.23.1
  replicas: 4
</code></pre>
<p>然后通过kubectl apply使之生效：</p>
<pre><code>$kubectl apply -f _v1_webserver.yaml
webserver.my.domain/webserver-sample configured
</code></pre>
<p>上述命令执行后，我们观察到operator的controller日志如下：</p>
<pre><code>1.660208962767797e+09   INFO    controllers.WebServer   Deployment spec.replicas change {"Webserver": "default/webserver-sample", "from": 3, "to": 4}
</code></pre>
<p>稍后，查看pod数量：</p>
<pre><code>$kubectl get pods
NAME                               READY   STATUS    RESTARTS   AGE
webserver-sample-bc698b9fb-8gq2h   1/1     Running   0          9m41s
webserver-sample-bc698b9fb-v9gvg   1/1     Running   0          42s
webserver-sample-bc698b9fb-vk6gw   1/1     Running   0          9m41s
webserver-sample-bc698b9fb-xgrgb   1/1     Running   0          9m41s
</code></pre>
<p>webserver pod副本数量成功从3扩为4。</p>
<ul>
<li>变更webserver image版本</li>
</ul>
<p>我们将CR的image的版本从nginx:1.23.1改为nginx:1.23.0，然后执行kubectl apply使之生效。</p>
<p>我们查看controller的响应日志如下：</p>
<pre><code>1.6602090494113188e+09  INFO    controllers.WebServer   Deployment spec.template.spec.container[0].image change {"Webserver": "default/webserver-sample", "from": "nginx:1.23.1", "to": "nginx:1.23.0"}
</code></pre>
<p>controller会更新deployment，导致所辖pod进行滚动升级：</p>
<pre><code>$kubectl get pods
NAME                               READY   STATUS              RESTARTS   AGE
webserver-sample-bc698b9fb-8gq2h   1/1     Running             0          10m
webserver-sample-bc698b9fb-vk6gw   1/1     Running             0          10m
webserver-sample-bc698b9fb-xgrgb   1/1     Running             0          10m
webserver-sample-ffcf549ff-g6whk   0/1     ContainerCreating   0          12s
webserver-sample-ffcf549ff-ngjz6   0/1     ContainerCreating   0          12s
</code></pre>
<p>耐心等一小会儿，最终的pod列表为：</p>
<pre><code>$kubectl get pods
NAME                               READY   STATUS    RESTARTS   AGE
webserver-sample-ffcf549ff-g6whk   1/1     Running   0          6m22s
webserver-sample-ffcf549ff-m6z24   1/1     Running   0          3m12s
webserver-sample-ffcf549ff-ngjz6   1/1     Running   0          6m22s
webserver-sample-ffcf549ff-t7gvc   1/1     Running   0          4m16s
</code></pre>
<ul>
<li>service自愈：恢复被无删除的Service</li>
</ul>
<p>我们来一次“误操作”，将webserver-sample-service删除，看看controller能否帮助service自愈：</p>
<pre><code>$kubectl delete service/webserver-sample-service
service "webserver-sample-service" deleted
</code></pre>
<p>查看controller日志：</p>
<pre><code>1.6602096994710526e+09  INFO    controllers.WebServer   Creating a new Service  {"Webserver": "default/webserver-sample", "Service.Namespace": "default", "Service.Name": "webserver-sample-service"}
</code></pre>
<p>我们看到controller检测到了service被删除的状态，并重建了一个新service！</p>
<p>访问新建的service：</p>
<pre><code>$curl http://192.168.10.182:30010
&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
&lt;title&gt;Welcome to nginx!&lt;/title&gt;
&lt;style&gt;
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;h1&gt;Welcome to nginx!&lt;/h1&gt;
&lt;p&gt;If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.&lt;/p&gt;

&lt;p&gt;For online documentation and support please refer to
&lt;a href="http://nginx.org/"&gt;nginx.org&lt;/a&gt;.&lt;br/&gt;
Commercial support is available at
&lt;a href="http://nginx.com/"&gt;nginx.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Thank you for using nginx.&lt;/em&gt;&lt;/p&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>可以看到service在controller的帮助下完成了自愈！</p>
<h3>五. 小结</h3>
<p>本文对Kubernetes Operator的概念以及优点做了初步的介绍，并基于kubebuilder这个工具开发了一个具有2级能力的operator。当然这个operator离完善还有很远的距离，其主要目的还是帮助大家理解operator的概念以及实现套路。</p>
<p>相信你阅读完本文后，对operator，尤其是其基本结构会有一个较为清晰的了解，并具备开发简单operator的能力！</p>
<p>文中涉及的源码可以在<a href="https://github.com/bigwhite/experiments/tree/master/webserver-operator">这里</a>下载 &#8211; https://github.com/bigwhite/experiments/tree/master/webserver-operator。</p>
<h3>六. 参考资料</h3>
<ul>
<li>kubernetes operator 101, Part 1: Overview and key features &#8211; https://developers.redhat.com/articles/2021/06/11/kubernetes-operators-101-part-1-overview-and-key-features</li>
<li>Kubernetes Operators 101, Part 2: How operators work &#8211; https://developers.redhat.com/articles/2021/06/22/kubernetes-operators-101-part-2-how-operators-work</li>
<li>Operator SDK: Build Kubernetes Operators &#8211; https://developers.redhat.com/blog/2020/04/28/operator-sdk-build-kubernetes-operators-and-deploy-them-on-openshift</li>
<li>kubernetes doc: Custom Resources &#8211; https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/</li>
<li>kubernetes doc: Operator pattern &#8211; https://kubernetes.io/docs/concepts/extend-kubernetes/operator/</li>
<li>kubernetes doc: API concepts &#8211; https://kubernetes.io/docs/reference/using-api/api-concepts/</li>
<li>Introducing Operators: Putting Operational Knowledge into Software 第一篇有关operator的文章 by coreos &#8211; https://web.archive.org/web/20170129131616/https://coreos.com/blog/introducing-operators.html</li>
<li>CNCF Operator白皮书v1.0 &#8211; https://github.com/cncf/tag-app-delivery/blob/main/operator-whitepaper/v1/Operator-WhitePaper_v1-0.md</li>
<li>Best practices for building Kubernetes Operators and stateful apps &#8211; https://cloud.google.com/blog/products/containers-kubernetes/best-practices-for-building-kubernetes-operators-and-stateful-apps</li>
<li>A deep dive into Kubernetes controllers &#8211;  https://docs.bitnami.com/tutorials/a-deep-dive-into-kubernetes-controllers</li>
<li>Kubernetes Operators Explained &#8211; https://blog.container-solutions.com/kubernetes-operators-explained</li>
<li>书籍《Kubernetes Operator》 &#8211; https://book.douban.com/subject/34796009/</li>
<li>书籍《Programming Kubernetes》 &#8211; https://book.douban.com/subject/35498478/</li>
<li>Operator SDK Reaches v1.0 &#8211; https://cloud.redhat.com/blog/operator-sdk-reaches-v1.0</li>
<li>What is the difference between kubebuilder and operator-sdk &#8211; https://github.com/operator-framework/operator-sdk/issues/1758</li>
<li>Kubernetes Operators in Depth &#8211; https://www.infoq.com/articles/kubernetes-operators-in-depth/</li>
<li>Get started using Kubernetes Operators &#8211; https://developer.ibm.com/learningpaths/kubernetes-operators/ </li>
<li>Use Kubernetes operators to extend Kubernetes’ functionality &#8211; https://developer.ibm.com/learningpaths/kubernetes-operators/operators-extend-kubernetes/</li>
<li>memcached operator &#8211; https://github.com/operator-framework/operator-sdk-samples/tree/master/go/memcached-operator</li>
</ul>
<hr />
<p><a href="https://wx.zsxq.com/dweb2/index/group/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博：https://weibo.com/bigwhite20xx</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2022, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2022/08/15/developing-kubernetes-operators-in-go-part1/feed/</wfw:commentRss>
		<slash:comments>12</slash:comments>
		</item>
		<item>
		<title>TB一周萃选[第10期]</title>
		<link>https://tonybai.com/2018/03/03/10th-issue-of-the-tech-weekly-carefully-chosen-by-tonybai/</link>
		<comments>https://tonybai.com/2018/03/03/10th-issue-of-the-tech-weekly-carefully-chosen-by-tonybai/#comments</comments>
		<pubDate>Sat, 03 Mar 2018 06:43:11 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[API]]></category>
		<category><![CDATA[bitcoin]]></category>
		<category><![CDATA[blockchain]]></category>
		<category><![CDATA[cargo]]></category>
		<category><![CDATA[cloudflare]]></category>
		<category><![CDATA[CNCF]]></category>
		<category><![CDATA[dep]]></category>
		<category><![CDATA[DevOps]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[ethereum]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go.mod]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Google]]></category>
		<category><![CDATA[GOPATH]]></category>
		<category><![CDATA[gopheracademy]]></category>
		<category><![CDATA[GopherCon]]></category>
		<category><![CDATA[gRPC]]></category>
		<category><![CDATA[import]]></category>
		<category><![CDATA[k8s]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[macos]]></category>
		<category><![CDATA[microservice]]></category>
		<category><![CDATA[minimal-version-selection]]></category>
		<category><![CDATA[Module]]></category>
		<category><![CDATA[mvs]]></category>
		<category><![CDATA[MySQL]]></category>
		<category><![CDATA[npm]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[Redhat]]></category>
		<category><![CDATA[RPC]]></category>
		<category><![CDATA[RussCox]]></category>
		<category><![CDATA[semantic-import-versioning]]></category>
		<category><![CDATA[semver]]></category>
		<category><![CDATA[slack]]></category>
		<category><![CDATA[smartcontract]]></category>
		<category><![CDATA[TB一周萃选]]></category>
		<category><![CDATA[vendor]]></category>
		<category><![CDATA[vgo]]></category>
		<category><![CDATA[vitess]]></category>
		<category><![CDATA[vscode]]></category>
		<category><![CDATA[youtube]]></category>
		<category><![CDATA[云原生]]></category>
		<category><![CDATA[以太坊]]></category>
		<category><![CDATA[元宵节]]></category>
		<category><![CDATA[包依赖]]></category>
		<category><![CDATA[区块链]]></category>
		<category><![CDATA[宜家]]></category>
		<category><![CDATA[容器]]></category>
		<category><![CDATA[微服务]]></category>
		<category><![CDATA[春节]]></category>
		<category><![CDATA[智能合约]]></category>
		<category><![CDATA[机器学习]]></category>
		<category><![CDATA[比特币]]></category>
		<category><![CDATA[过年]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=2566</guid>
		<description><![CDATA[本文是首发于个人微信公众号的文章“TB一周萃选[第10期]”的归档。 这个世界上最危险的毒药，就是成就感。而解药就是每晚都想一想，明天如何做得更好。 &#8211; 英格瓦坎普拉德，宜家创始人 2018年元宵节已过，这个传统意义上的年就算真的过完了，我们的那颗有些闲散、有些懈怠的心需要收一收，是时候为2018年的“事业”做些规划，从2018的起跑线上起跑出去了。就连现在的孩子，在开学第一课时都要对自己的寒假生活做生动的回顾并且对新学期给予展望了。 春节假期匆忙且短暂，不过在这段时间里还是有很多值得关注的文章、资料、书籍以及项目的。 一、一周文章精粹 1. Go官方提出新的包依赖管理工具：vgo 就在上周，Go社区里发生了一件“大事”：Go大神Russ Cox一周内连发了七篇文章，并宣布Go很可能在下一个版本：Go 1.11中加入可选的、“实验性”的新模型： vgo(versioned Go)，以试图解决长期以来Go被广泛诟病的包依赖管理问题。 Russ Cox在设计vgo时参考了当今比较流行的cargo、npm等工具，也从之前Go官方实验dep中吸取了足够的实验结论，另辟蹊径，提出了很多很有创新的观点和方法，在社区里引起了广泛的关注和讨论。 vgo的一些主要设计考量如下： 接受语义版本(semver)规则 使用semantic import versioning规则替代原有的import rule 引入module概念（go.mod) 使用minimal version selection(最小版本选择)，而不是业界事实标准的maximal version selected（最新版本选择）的方案； 去除vendor机制 去除GOPATH Russ Cox还提供了一个vgo的初步实现，供广大Gopher体验。 vgo的公开意味着Go team已经将包依赖管理问题列为高优先级待解决的问题，vgo虽然只是原型，其设计思路也可能不会全部进入到最终的解决方法中，但这毕竟迈出了坚实的一步。 文章链接：Go &#38; Verisioning 2. Go官方2017用户调查结果 本周Go官方在Blog上公布了2017用户调查结果，几个结论值得大家关注： 越来越多用户在工作中正式使用Go (67%) Web开发、系统编程、Devops、网络编程依旧是Go使用的主要领域，但在移动端、桌面端GUI编程的比例下滑明显 在API/RPC服务领域的使用占据榜首，CLI、WebService(返回html)排名2、3 包依赖管理以及缺少泛型依然是Gopher最希望Go team解决的两个问题 Linux、MacOS依然是Gopher主力开发平台 vscode在Go编辑器市场份额升至No.1 最喜欢的关键字：go、defer、func、select和interface排名top5 文章链接：“Go 2017用户调查结果” 3. 容器术语介绍入门 著名开源公司Redhat近两年拥抱容器的态度十分坚决，近期来收购了coreos。近期Redhat在官博上发表了一篇文章，对容器领域的相关术语概念做了详尽的介绍，强烈推荐。 文章链接：“容器术语介绍入门” [...]]]></description>
			<content:encoded><![CDATA[<p>本文是首发于<a href="https://mp.weixin.qq.com/mp/qrcode?scene=10000005&amp;size=102&amp;__biz=MzIyNzM0MDk0Mg==&amp;mid=2247483848&amp;idx=1&amp;sn=a3cd9182a2b2d3716623cc2c43d59f37&amp;send_time=">个人微信公众号</a>的文章<strong>“TB一周萃选[第10期]”</strong>的归档。</p>
<p><img src="https://tonybai.com/wp-content/uploads/weekly-issues/10th-issue/cover.jpg" alt="img{512x368}" /></p>
<blockquote>
<p>这个世界上最危险的毒药，就是成就感。而解药就是每晚都想一想，明天如何做得更好。 &#8211; 英格瓦坎普拉德，宜家创始人</p>
</blockquote>
<p>2018年元宵节已过，这个传统意义上的年就算真的过完了，我们的那颗有些闲散、有些懈怠的心需要收一收，是时候为2018年的“事业”做些规划，从2018的起跑线上起跑出去了。就连现在的孩子，在开学第一课时都要对自己的寒假生活做生动的回顾并且对新学期给予展望了。</p>
<p><img src="https://tonybai.com/wp-content/uploads/weekly-issues/10th-issue/2018-kaixue.jpg" alt="img{512x368}" /></p>
<p>春节假期匆忙且短暂，不过在这段时间里还是有很多值得关注的文章、资料、书籍以及项目的。</p>
<h2>一、一周文章精粹</h2>
<h3>1. Go官方提出新的包依赖管理工具：vgo</h3>
<p>就在上周，Go社区里发生了一件“大事”：Go大神Russ Cox一周内连发了<a href="https://research.swtch.com/vgo">七篇文章</a>，并宣布Go很可能在下一个版本：Go 1.11中加入可选的、“实验性”的新模型： vgo(versioned Go)，以试图解决长期以来Go被广泛诟病的包依赖管理问题。</p>
<p>Russ Cox在设计vgo时参考了当今比较流行的cargo、npm等工具，也从之前Go官方实验<a href="https://tonybai.com/tag/dep">dep</a>中吸取了足够的实验结论，另辟蹊径，提出了很多很有创新的观点和方法，在社区里引起了广泛的关注和讨论。</p>
<p>vgo的一些主要设计考量如下：</p>
<ul>
<li>接受语义版本(semver)规则</li>
<li>使用semantic import versioning规则替代原有的import rule</li>
<li>引入module概念（go.mod)</li>
<li>使用minimal version selection(最小版本选择)，而不是业界事实标准的maximal version selected（最新版本选择）的方案；</li>
<li>去除vendor机制</li>
<li>去除GOPATH</li>
</ul>
<p>Russ Cox还提供了一个<a href="https://github.com/golang/vgo">vgo的初步实现</a>，供广大Gopher体验。</p>
<p>vgo的公开意味着Go team已经将包依赖管理问题列为高优先级待解决的问题，vgo虽然只是原型，其设计思路也可能不会全部进入到最终的解决方法中，但这毕竟迈出了坚实的一步。</p>
<p>文章链接：<a href="https://research.swtch.com/vgo">Go &amp; Verisioning</a></p>
<h3>2. Go官方2017用户调查结果</h3>
<p>本周Go官方在Blog上公布了2017用户调查结果，几个结论值得大家关注：</p>
<ul>
<li>越来越多用户在工作中正式使用Go (67%)</li>
<li>Web开发、系统编程、Devops、网络编程依旧是Go使用的主要领域，但在移动端、桌面端GUI编程的比例下滑明显</li>
<li>在API/RPC服务领域的使用占据榜首，CLI、WebService(返回html)排名2、3</li>
<li>包依赖管理以及缺少泛型依然是Gopher最希望Go team解决的两个问题</li>
<li>Linux、MacOS依然是Gopher主力开发平台</li>
<li><a href="https://tonybai.com/tag/vscode">vscode</a>在Go编辑器市场份额升至No.1</li>
<li>最喜欢的关键字：go、defer、func、select和interface排名top5</li>
</ul>
<p>文章链接：<a href="https://blog.golang.org/survey2017-results">“Go 2017用户调查结果”</a></p>
<h3>3. 容器术语介绍入门</h3>
<p>著名开源公司Redhat近两年拥抱容器的态度十分坚决，近期来收购了coreos。近期Redhat在官博上发表了一篇文章，对容器领域的相关术语概念做了详尽的介绍，强烈推荐。</p>
<p>文章链接：<a href="https://developers.redhat.com/blog/2018/02/22/container-terminology-practical-introduction/">“容器术语介绍入门”</a></p>
<h3>4. Go语言实现的微服务系列</h3>
<p>Go语言已经被证明了是当前应用云化、面向微服务的服务端编程的头部语言之一。关于Go与Microservice的文章也有不少。Ewan Valentine的Go语言实现微服务系列（10篇）就是这类文章中难得的全面、细致讲述Go如何实现微服务应用的文章资料。在这一系列文章中，作者谈到的了mongodb, grpc, docker, Google Cloud, Kubernetes, NATS, CircleCI, Terraform、go-micro框架等诸多在编写、部署、运维微服务过程中所能用到的框架、协议、工具等。.</p>
<p>文章链接：<a href="https://ewanvalentine.io/microservices-in-golang-part-1/">microservice in golang series</a></p>
<h3>5. Brian Ketelsen专访：Go取得快速增长的原因</h3>
<p><a href="https://github.com/bketelsen">Brian Ketelsen</a>是知名Gopher，GopherCon大会、GopherAcademy的联合发起人、《Go in action》一书的联合作者。在Microsoft对其的一篇专访中，Brian Ketelsen谈了对Go语言这些年取得快速成长的看法。</p>
<p>文章链接：<a href="https://open.microsoft.com/2018/02/21/go-lang-brian-ketelsen-explains-fast-growth/">Brian Ketelsen专访：Go取得快速增长的原因</a></p>
<h3>6. 在Linux上使用Go作为脚本语言</h3>
<p>Cloudflare公司的很多产品采用的是Go技术栈，公司内部支撑系统亦是。Go的简单特质以及Go tools的使用模式让Go十分适合在Linux系统上被当做“脚本语言”使用（结合shebang行），它的强类型特性又是真正的脚本语言所不具备的。cloudflare的这篇文章讲解了该公司使用go作为脚本语言在Linux上的实践方法，值得借鉴。</p>
<p>文章链接：<a href="https://blog.cloudflare.com/using-go-as-a-scripting-language-in-linux/">《在Linux使用Go作为脚本语言》</a></p>
<h2>二、一周资料分享</h2>
<h3>1. Google机器学习速成教程</h3>
<p><img src="https://tonybai.com/wp-content/uploads/weekly-issues/10th-issue/google-machine-learning-crash-course.png" alt="img{512x368}" /></p>
<p>Google公司本周正式推出面向普通开发者、机器学习爱好者的机器学习速成教程资料。粗略浏览了一遍，感觉该教程是目前传统程序员向机器学习、AI领域转型的最优秀资料之一。教程提供了教程中实验的全部资料和实验环境，并给出了前提条件中给出了预备知识的学习教程，包括数学知识、Python编程等。更为可贵的是该教程提供完整的中文版，国内程序员学习起来曲线也降低了不少。唯一不便的可能就是需要<strong>科学上网</strong>才能打开教程。</p>
<p>资料分享链接：<a href="https://developers.google.com/machine-learning/crash-course/">“Google机器学习速成教程”</a></p>
<h2>三、一周项目推荐</h2>
<h3>1. vitess</h3>
<p><img src="https://tonybai.com/wp-content/uploads/weekly-issues/10th-issue/vitess-architecture.png" alt="img{512x368}" /></p>
<p>之所以推荐vitess这个项目，是因为它在不久前成为了<a href="https://www.cncf.io">CNCF基金会</a>的<a href="https://www.cncf.io/blog/2018/02/05/cncf-host-vitess/">第16个孵化级别项目</a>，并且是cncf第二个存储项目。Vitess最初是作为YouTube的一个内部解决方案来处理大量存储的扩展，它是一个数据库编排系统，通过广义分片来对MySQL进行水平缩放。通过封装分片路由逻辑，Vitess允许应用程序代码和数据库查询对于将数据分布到多个分片上保持不变。借助Vitess，组织甚至可以根据需求的增长来分割和合并碎片，原子切割步骤只需要几秒钟。</p>
<p>同时该项目还是<a href="https://tonybai.com/tag/go">Go语言</a>的早期“尝鲜者”：在2011年就开始使用Go语言开发了。随着vitess用户的增多（包括slack、flipkart等），vitess似乎又进入一个黄金开发的阶段，将较为成熟的、业界广为使用的数据库分片技术继续延续和优化下去，并且vitess与容器、<a href="https://vitess.io/getting-started/">kubernetes的结合使用</a>也日益成熟，为云原生应用在k8s上提供一个可扩展的存储层。</p>
<p>项目链接：<a href="https://vitess.io/">“vitess”</a></p>
<h2>四、一周图书推荐</h2>
<h3>1.《Master Ethereum》</h3>
<p><img src="https://tonybai.com/wp-content/uploads/weekly-issues/10th-issue/book-master-ethereum.jpg" alt="img{512x368}" /></p>
<p>随着2017年<a href="https://www.bitcoin.com/">比特币</a>市场的异常繁荣，2018的<a href="https://en.wikipedia.org/wiki/Blockchain">区块链技术</a>有迎来爆发的趋势。作为第二代区块链技术代表的<a href="https://en.wikipedia.org/wiki/Ethereum">以太坊</a>(Ethereum)，它试图实现一个总体上完全无需信任基础的<a href="https://en.wikipedia.org/wiki/Smart_contract">智能合约</a>平台和庞大的生态圈，受到了区块链业界最为广泛的关注，有关以太坊的技术书籍亦是如此。</p>
<p>《Master Ethereum》，中文名可译为“精通以太坊”，这是一本尚未完成的书，但在编写的过程中就受到了广泛的关注。除了是因为大家对以太坊技术关注之外，该书在github的开源也是其吸引眼球的重要原因。该书的两位作者是bitcoin专家，本书的目标是为开发者提供有关以太坊概念、使用、智能合约(smart contract)、经典以太坊网络、以太坊标准等全面的内容。</p>
<p>图书链接：<a href="https://github.com/ethereumbook/ethereumbook">《Master Ethereum》</a></p>
<hr />
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>我的联系方式：</p>
<p>微博：https://weibo.com/bigwhite20xx<br />
微信公众号：iamtonybai<br />
博客：tonybai.com<br />
github: https://github.com/bigwhite</p>
<p>微信赞赏：<br />
<img src="https://tonybai.com/wp-content/uploads/wechat-zanshang-code-512x512.jpg" alt="img{512x368}" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2018, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2018/03/03/10th-issue-of-the-tech-weekly-carefully-chosen-by-tonybai/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>TB一周萃选[第9期]</title>
		<link>https://tonybai.com/2018/02/11/9th-issue-of-the-tech-weekly-carefully-chosen-by-tonybai/</link>
		<comments>https://tonybai.com/2018/02/11/9th-issue-of-the-tech-weekly-carefully-chosen-by-tonybai/#comments</comments>
		<pubDate>Sun, 11 Feb 2018 05:41:08 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[artificial-intelligence]]></category>
		<category><![CDATA[browser]]></category>
		<category><![CDATA[Ceph]]></category>
		<category><![CDATA[circuitbreaker]]></category>
		<category><![CDATA[cloud-native]]></category>
		<category><![CDATA[CNCF]]></category>
		<category><![CDATA[Codereview]]></category>
		<category><![CDATA[conduit]]></category>
		<category><![CDATA[Context]]></category>
		<category><![CDATA[deep-learning]]></category>
		<category><![CDATA[EBS]]></category>
		<category><![CDATA[egress]]></category>
		<category><![CDATA[GCP]]></category>
		<category><![CDATA[GeoffreyHinton]]></category>
		<category><![CDATA[gerrit]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Go-1.10-Release-Party]]></category>
		<category><![CDATA[Go1.10]]></category>
		<category><![CDATA[Go1.6]]></category>
		<category><![CDATA[Go1.7]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Google]]></category>
		<category><![CDATA[GoogleCloudPlatform]]></category>
		<category><![CDATA[HTTP2]]></category>
		<category><![CDATA[ingress]]></category>
		<category><![CDATA[istio]]></category>
		<category><![CDATA[k8s]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[metrics]]></category>
		<category><![CDATA[middleware]]></category>
		<category><![CDATA[NFS]]></category>
		<category><![CDATA[Package]]></category>
		<category><![CDATA[PR]]></category>
		<category><![CDATA[Redhat]]></category>
		<category><![CDATA[rook]]></category>
		<category><![CDATA[S3]]></category>
		<category><![CDATA[ServiceMesh]]></category>
		<category><![CDATA[SSE]]></category>
		<category><![CDATA[tracing]]></category>
		<category><![CDATA[websocket]]></category>
		<category><![CDATA[中华民族]]></category>
		<category><![CDATA[云原生]]></category>
		<category><![CDATA[云存储]]></category>
		<category><![CDATA[亲情]]></category>
		<category><![CDATA[人工智能]]></category>
		<category><![CDATA[传统]]></category>
		<category><![CDATA[充电]]></category>
		<category><![CDATA[分布式存储]]></category>
		<category><![CDATA[华夏]]></category>
		<category><![CDATA[反向传播]]></category>
		<category><![CDATA[回家]]></category>
		<category><![CDATA[安全]]></category>
		<category><![CDATA[密码管理]]></category>
		<category><![CDATA[度量]]></category>
		<category><![CDATA[微服务]]></category>
		<category><![CDATA[性能优化]]></category>
		<category><![CDATA[春节]]></category>
		<category><![CDATA[春运]]></category>
		<category><![CDATA[服务治理]]></category>
		<category><![CDATA[浏览器]]></category>
		<category><![CDATA[深度学习]]></category>
		<category><![CDATA[胶囊理论]]></category>
		<category><![CDATA[节日]]></category>
		<category><![CDATA[过年]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=2552</guid>
		<description><![CDATA[本文是首发于个人微信公众号的文章“TB一周萃选[第9期]”的归档。 亲情犹如一江剪不断的春水，流动的是游子心中永远的思念；亲情犹如一丘数不尽的细沙，沉淀的是长年堆积的牵挂；亲情犹如夜空中那颗北斗，指引的是那迷路的羔羊回家的方向。忙碌了一年，该回家了，给心放个假，带上媳妇带上你的娃，回家看看那年迈的爸妈，出发！ &#8212; 改编自网络 此时此刻，很多人刚刚踏上了春节回家的旅途，有些人更是已经叩开了家的大门。每逢中国传统佳节-春节，令世界瞩目并为之瞠目结舌的中国式人口大迁移就会发生一次：几亿人熬夜刷票并不辞辛劳地携着夫/妻儿女，经由多种交通工具，跨越高山大河，不远千百里，战胜种种“囧况”，只为一个目的：在春节前回到那个充满熟悉味道的家乡。 这种在一个文明延续5000多年未中断的民族中发生的全民行为让西方社会感到十分不解，甚至指责这是对资源的一种浪费；并且也有国内的人发出类似不和谐的声音。但是它依然在发生着，每年都在发生，形式有些许变化，但剧情大体雷同。 曾经有国内外学者对中国特有的春节大迁徙的原因进行研究和分析，并给出了各种专业化的理由。但在我看来，对现代人来说，回家过年，是一种心灵的相互充电! 而且是充电7天，“通话”一整年。 对于一年到头在外奔波劳碌的人们来说，只有回家，才能真实地触摸到自己的“根”，才能切切实实地体会这种归属感，才能在一定程度上纾解那些在工作的城市中涵盖不了的人生寄托。在这种归属感中，哪怕只是获得片刻的身心安宁，也是一种极为重要的精神能量的充电；而对于守候在家乡的父母或者孩童儿，你的回家，让他们将近一年的期盼终于有了一个圆满的结果，这同样为下一个365天的期盼周期提供了强大的动力和希望。 如果非要给这种行为找个理由，那我要说这就是由一个体内延绵数千年的中华民族血脉的中国人的基因所决定的。 一、一周文章精粹 1. Go 1.10发布Party 自从Go 1.6开始，每逢偶数版本（一般在每年2、3月发布），Gopher社区都会举办庆祝Release的全球Party。在中国农历春节到来之际，也恰逢Go最新版本Go 1.10即将发布之时，Go wiki发布了Go 1.10 Release Party的Schedule和相关资料。截至目前，已经有15个Party已经list到页面上，活动从2月15号一直延续到3月份。 Go 1.10发布Party官网页面 Go 1.10 Release Note Draft The State of Go 1.10 2. 避免或减少对Go context Value的使用 context包最初诞生于Google公司内部，并在Google内部项目大量使用。context在golang/x中孵化了多年，并得到了很多开源项目的使用，尤其是一些使用了”middleware”模式的项目中，于是在Go 1.7发布时，context包正式加入Go标准库。context加入后，可谓既带来魔力，亦带来了争议，甚至有人将其视为具有“病毒”属性，一旦使用，便可轻易传染到项目中代码的各个角落。 Go开发者、培训师Jon Calhoun也在个人网站上撰写了一篇文章，来告诫大家Go context value的一些缺陷，建议大家避免或减少对Go context Value的使用，并给出自己的替代方案。其主要理由是：context.WithValue和Context.Value的使用让我们失去了编译器对类型安全性的检查。 文章链接：“Pitfalls of context values and how to avoid or mitigate [...]]]></description>
			<content:encoded><![CDATA[<p>本文是首发于<a href="https://mp.weixin.qq.com/mp/qrcode?scene=10000005&amp;size=102&amp;__biz=MzIyNzM0MDk0Mg==&amp;mid=2247483848&amp;idx=1&amp;sn=a3cd9182a2b2d3716623cc2c43d59f37&amp;send_time=">个人微信公众号</a>的文章<strong>“TB一周萃选[第9期]”</strong>的归档。</p>
<p><img src="http://tonybai.com/wp-content/uploads/weekly-issues/9th-issue/go-home-during-spring-festival.jpg" alt="img{512x368}" /></p>
<blockquote>
<p>亲情犹如一江剪不断的春水，流动的是游子心中永远的思念；亲情犹如一丘数不尽的细沙，沉淀的是长年堆积的牵挂；亲情犹如夜空中那颗北斗，指引的是那迷路的羔羊回家的方向。忙碌了一年，该回家了，给心放个假，带上媳妇带上你的娃，回家看看那年迈的爸妈，出发！  &#8212;  改编自网络</p>
</blockquote>
<p>此时此刻，很多人刚刚踏上了春节回家的旅途，有些人更是已经叩开了家的大门。每逢中国传统佳节-<a href="https://en.wikipedia.org/wiki/Chinese_New_Year">春节</a>，令世界瞩目并为之瞠目结舌的中国式人口大迁移就会发生一次：几亿人熬夜刷票并不辞辛劳地携着夫/妻儿女，经由多种交通工具，跨越高山大河，不远千百里，战胜种种“囧况”，只为一个目的：在春节前回到那个充满熟悉味道的家乡。</p>
<p>这种在一个文明延续5000多年未中断的民族中发生的<strong>全民行为</strong>让西方社会感到十分不解，甚至指责这是对资源的一种浪费；并且也有国内的人发出类似不和谐的声音。但是<strong>它依然在发生着，每年都在发生</strong>，形式有些许变化，但剧情大体雷同。</p>
<p>曾经有国内外学者对中国特有的春节大迁徙的原因进行研究和分析，并给出了各种专业化的理由。但在我看来，对现代人来说，回家过年，是一种<strong>心灵的相互充电</strong>!  而且是<strong>充电7天，“通话”一整年</strong>。</p>
<p>对于一年到头在外奔波劳碌的人们来说，只有回家，才能真实地触摸到自己的“根”，才能切切实实地体会这种归属感，才能在一定程度上纾解那些在工作的城市中涵盖不了的人生寄托。在这种归属感中，哪怕只是获得片刻的身心安宁，也是一种极为重要的精神能量的充电；而对于守候在家乡的父母或者孩童儿，你的回家，让他们将近一年的期盼终于有了一个圆满的结果，这同样为下一个365天的期盼周期提供了强大的动力和希望。</p>
<p>如果非要给这种行为找个理由，那我要说这就是由一个体内延绵数千年的中华民族血脉的中国人的基因所决定的。</p>
<p><img src="http://tonybai.com/wp-content/uploads/weekly-issues/9th-issue/chinese-dragon.jpg" alt="img{512x368}" /></p>
<h2>一、一周文章精粹</h2>
<h3>1. Go 1.10发布Party</h3>
<p>自从<a href="http://tonybai.com/2016/02/21/some-changes-in-go-1-6/">Go 1.6</a>开始，每逢偶数版本（一般在每年2、3月发布），Gopher社区都会举办庆祝Release的<a href="https://twitter.com/hashtag/goreleaseparty">全球Party</a>。在中国农历春节到来之际，也恰逢Go最新版本Go 1.10即将发布之时，Go wiki发布了<a href="https://github.com/golang/go/wiki/Go-1.10-Release-Party">Go 1.10 Release Party</a>的Schedule和相关资料。截至目前，已经有15个Party已经list到页面上，活动从2月15号一直延续到3月份。</p>
<ul>
<li><a href="https://github.com/golang/go/wiki/Go-1.10-Release-Party">Go 1.10发布Party官网页面</a></li>
<li><a href="https://tip.golang.org/doc/go1.10">Go 1.10 Release Note Draft</a></li>
<li><a href="https://speakerdeck.com/campoy/the-state-of-go-1-dot-10">The State of Go 1.10</a></li>
</ul>
<h3>2. 避免或减少对Go context Value的使用</h3>
<p>context包最初诞生于Google公司内部，并在Google内部项目大量使用。context在golang/x中孵化了多年，并得到了很多开源项目的使用，尤其是一些使用了”middleware”模式的项目中，于是在<a href="http://tonybai.com/2016/06/21/some-changes-in-go-1-7/">Go 1.7发布</a>时，context包正式加入Go标准库。context加入后，可谓既带来魔力，亦带来了争议，甚至有人将其视为<a href="https://faiface.github.io/post/context-should-go-away-go2/">具有“病毒”属性</a>，一旦使用，便可轻易传染到项目中代码的各个角落。</p>
<p>Go开发者、培训师Jon Calhoun也在个人网站上撰写了一篇文章，来告诫大家Go context value的一些缺陷，建议大家避免或减少对Go context Value的使用，并给出自己的替代方案。其主要理由是：context.WithValue和Context.Value的使用让我们失去了编译器对类型安全性的检查。</p>
<p>文章链接：<a href="https://www.calhoun.io/pitfalls-of-context-values-and-how-to-avoid-or-mitigate-them">“Pitfalls of context values and how to avoid or mitigate them in Go”</a></p>
<h3>3. 来自Google Cloud Platform的12条有关用户账号、授权和密码管理的最佳实践</h3>
<p>对于许多开发者来说，账户管理是一个黑暗的角落，没有得到足够的重视。来自Google Cloud Platform的解决方案专家Ian Maddox给我们带来了12条有关此方面的最佳实践，包括：区分用户标识与用户账号、允许用户更改用户名、用户ID大小写敏感、两步验证等。</p>
<p><img src="http://tonybai.com/wp-content/uploads/weekly-issues/9th-issue/user-account-and-password.jpg" alt="img{512x368}" /></p>
<p>文章链接：<a href="https://cloudplatform.googleblog.com/2018/01/12-best-practices-for-user-account.html">“12 best practices for user account, authorization and password management”</a></p>
<h3>4. AI界网红-深度学习之父Geoffrey Hinton的传奇学术生涯</h3>
<p>这几年最火爆的人工智能技术就是深度学习，可以说当下的主流人工智能就是深度学习，而深度学习的理论基石就是反向传播。和当代物理学类似，最新的计算机应用实际上也是在消化几十年前就已经建立的理论，这不：反向传播就是Geoffrey Hinton与同事David Rumelhart、Ronald Williams在1986年发布的成果，Geoffrey Hinton也因此被誉为深度学习之父。Geoffrey Hinton花了30年在AI前沿的研究，在今天终于开花结果。不过这位现在AI奠基人并没有就此停歇，去年他还提出了“<a href="https://arxiv.org/abs/1710.09829">胶囊理论</a>”，不过要彻底理解他的理论，不知道AI应用界还要花多久。下面这篇文章是“多伦多生活”上发表的一篇有关Geoffrey Hinton的传奇学术生涯的新闻稿，我们可以通过它一瞥AI超级明星的学术人生。</p>
<p><img src="http://tonybai.com/wp-content/uploads/weekly-issues/9th-issue/the-father-of-deeplearning-geoffrey-hinton.jpg" alt="img{512x368}" /><br />
图：Geoffrey Hinton</p>
<p>文章链接：<a href="https://torontolife.com/tech/ai-superstars-google-facebook-apple-studied-guy/">“深度学习之父Geoffrey Hinton的传奇学术生涯”</a></p>
<h3>5. Go项目在github上接受PR了</h3>
<p>go语言自身的开发一直是在google内部的平台上，github上的golang项目仅仅是其一个mirror。在这之前，golang项目在github上是拒绝pr的，contributor必须注册google的开发账号才能为go语言本身做贡献，这种门槛显然有些高。近期Go项目作出了对社区更为友好的举动：<a href="https://go-review.googlesource.com/c/go/+/92995">允许在github上直接提交PR</a>。不过代码的review依旧是在google原平台上，github上提交的pr将被GerritBot自动同步到Go team的Gerrit上进行code review。不过这已经是一个不错的开端了。估计会吸引更多开发者为Go做contribution。</p>
<p>文章链接：<br />
  * <a href="https://go-review.googlesource.com/c/go/+/92995">“doc: remove Pull Request note in README.md”</a><br />
  * <a href="https://github.com/golang/go/wiki/GerritBot">“pr流程”</a></p>
<h2>二、一周资料分享</h2>
<h3>1. istio微服务教程 by Redhat</h3>
<p>下一代微服务平台日益火爆，比如：<a href="http://tonybai.com/2018/01/03/an-intro-of-microservices-governance-by-istio/">istio</a>、<a href="https://conduit.io/">conduit</a>等。近期Redhat开源了一套istio微服务教程，主要是for java microservice，但感觉对其他语言开发的微服务也适用。教程使用的是<a href="https://istio.io/">istio</a>最新发布的<a href="https://github.com/istio/istio/releases/tag/0.5.0">0.5.0版本</a>，底层使用的是redhat自身的oc平台(openshift)，但替换成<a href="http://tonybai.com/tag/kubernetes">kubernetes</a>应该很容易。教程包含的内容还是很全面的，针对包括metrics、tracing、routerule管理、fault injection、retry&amp;timeout、mirroring traffic、access control、rate limiting、circuit breaker、egress等常见的微服务框架治理机制都提供了demo实例。</p>
<p>资料分享链接：<a href="https://github.com/redhat-developer-demos/istio-tutorial">Istio Tutorial for Java Microservices</a></p>
<h2>三、一周项目推荐</h2>
<h3>1. rook：致力于让存储服务成为云原生平台上的“头等”服务</h3>
<p>2018年1月30日，云原生<a href="https://www.cncf.io/">cncf组织</a>下又增加了一位新成员:<a href="https://rook.io/">rook项目</a>，由于刚入行，其与linkerd、coredns同样处于Inception级别。rook是什么？它解决了哪些问题呢？</p>
<p>如今在Kubernetes上部署的应用在使用存储服务时，多使用k8s集群外提供的外部存储服务。在公有云上，使用较多的是诸如<a href="https://aws.amazon.com/cn/ebs/">EBS</a>、<a href="https://aws.amazon.com/s3/">S3</a>等；在定制云/私有云中，使用的则是NFS、<a href="http://tonybai.com/tag/ceph">Ceph</a>或更为传统的存储解决方案，如下图所示：</p>
<p><img src="http://tonybai.com/wp-content/uploads/weekly-issues/9th-issue/before-using-rook.png" alt="img{512x368}" /><br />
图：使用rook前</p>
<p>Rook存在的意义就是将存储服务移入集群内部，让那些依赖存储服务的应用可以无缝地使用这些服务，这样一来，整个云原生集群环境就可以脱离厂商依赖（比如对amazon、google cloud platform的依赖），实现整体的可移植了，无论是公有云还是私有云。</p>
<p><img src="http://tonybai.com/wp-content/uploads/weekly-issues/9th-issue/after-using-rook.png" alt="img{512x368}" /><br />
图：使用rook后</p>
<p>可以说，Rook<strong>让存储服务成为云原生平台上的“头等”服务</strong>，与其他应用服务一样。</p>
<p>那Rook究竟是什么呢？Rook不是一个像ceph那样的分布式共享存储系统。rook的考虑是：与其花费几年甚至十几年实现一个成熟的、久经考验的分布式存储系统，到不如帮助现有的已经十分成熟的、久经沙场的存储系统更方便的被云原生环境中的应用所使用，比如：<a href="https://ceph.com/">ceph</a>。于是rook通过将那些专有存储服务管理员的日常操作自动化：包括引导启动、配置、伸缩、升级、迁移、灾难恢复、监控、资源管理，将存储服务包装为云原生应用，无缝运行在云原生环境上，目前主要是在Kubernetes上。</p>
<p><img src="http://tonybai.com/wp-content/uploads/weekly-issues/9th-issue/rook-architecture.png" alt="img{512x368}" /><br />
图：rook架构</p>
<p>Rook的出现，迅速得到了来自Redhat、ceph开发者的支持，社区也在日益壮大。目前其最新版本为v0.6.2，按计划在2018年中旬发布第一个production-ready的正式版。</p>
<p>项目地址：<a href="https://github.com/rook/rook">Rook</a></p>
<h2>四、一周图书推荐</h2>
<h3>1.《High Performance Browser Networking》</h3>
<p><img src="http://tonybai.com/wp-content/uploads/weekly-issues/9th-issue/book-high-performance-browser-networking.jpg" alt="img{512x368}" /></p>
<p>Ilya Grigorik是Google性能优化工程师，他在2013出版的这本<a href="https://book.douban.com/subject/21866396/">《High Performance Browser Networking》</a>堪称当代Web性能调优的圣经。该书以调优为核心，从网络基础(101)讲起，然后深入探讨了无线和移动网络的工作机制。最后，揭示了HTTP 协议的底层细节，同时详细介绍了HTTP 2.0、 XHR、SSE、WebSocket、WebRTC 和DataChannel 等现代浏览器新增的具有革命性的新能力。该书无论是对前端开发，还是后端网络服务开发设计人员都是大有裨益的。</p>
<p>更重要的是该书当时所讲述的诸多浏览器协议技术，比如：HTTP2.0、WebSocket、SSE在如今已经成为标准，并广泛应用于生产实践中。</p>
<p>图书链接：<br />
   英文版：<a href="https://book.douban.com/subject/21866396/">《High Performance Browser Networking》</a><br />
   中文版：<a href="https://book.douban.com/subject/25856314/">《Web性能权威指南》</a><br />
   免费版：<a href="https://hpbn.co/">《High Performance Browser Networking》</a></p>
<hr />
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>我的联系方式：</p>
<p>微博：http://weibo.com/bigwhite20xx<br />
微信公众号：iamtonybai<br />
博客：tonybai.com<br />
github: https://github.com/bigwhite</p>
<p>微信赞赏：<br />
<img src="http://tonybai.com/wp-content/uploads/wechat-zanshang-code-512x512.jpg" alt="img{512x368}" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作</p>
<p style='text-align:left'>&copy; 2018, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2018/02/11/9th-issue-of-the-tech-weekly-carefully-chosen-by-tonybai/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>使用core-vagrant方式安装CoreOS</title>
		<link>https://tonybai.com/2015/07/20/install-coreos-by-coreos-vagrant/</link>
		<comments>https://tonybai.com/2015/07/20/install-coreos-by-coreos-vagrant/#comments</comments>
		<pubDate>Mon, 20 Jul 2015 11:34:24 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Amazon]]></category>
		<category><![CDATA[amazonec2]]></category>
		<category><![CDATA[atomic]]></category>
		<category><![CDATA[Azure]]></category>
		<category><![CDATA[cluster]]></category>
		<category><![CDATA[coreos]]></category>
		<category><![CDATA[Debian]]></category>
		<category><![CDATA[DigitalOcean]]></category>
		<category><![CDATA[distributedsystem]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[etcd]]></category>
		<category><![CDATA[etcd2]]></category>
		<category><![CDATA[Fedora]]></category>
		<category><![CDATA[GCE]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Google]]></category>
		<category><![CDATA[iso]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[Microsoft]]></category>
		<category><![CDATA[PXE]]></category>
		<category><![CDATA[Redhat]]></category>
		<category><![CDATA[snappy]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[vagrant]]></category>
		<category><![CDATA[virtualbox]]></category>
		<category><![CDATA[分布式系统]]></category>
		<category><![CDATA[然并卵]]></category>
		<category><![CDATA[集群]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=1768</guid>
		<description><![CDATA[CoreOS是一种专门为运行类docker容器而生的linux发行版。与其他通用linux发行版（ubuntu、debian、redhat)相 比，它具有体型最小，消耗最小，支持滚动更新等特点。除此之外CoreOS内置的分布式系统服务组件也给开发者和运维者组建分布式集群、部署分布式服务应 用带来了极大便利。 CoreOS与知名容器Docker脚前脚后诞生，到目前为止已经较为成熟，国外主流云平台提供商如Amazon EC2、Google Compute Engine、Microsoft Azure、Digtial Ocean等均提供了CoreOS image，通过这些服务，你可以一键建立一个CoreOS实例，这似乎也是CoreOS官方推荐的主流install方式（最Easy）。 CoreOS当然支持其他方式的安装，比如支持虚拟机安装(vagrant+virtualbox)、PXE(preboot execute environment)安装以及iso install to 物理disk方式。如果仅仅是做一些实验，虚拟机安装是最简单也是最安全的方式。不过由于CoreOS的官方下载站在大陆无法直接访问（大陆程序员们好悲 催啊），因此这一最简单的虚拟机安装CoreOS的过程也就不那么简单了。 通过core-vagrant安装的直接结果是CoreOS被安装到一个VirtualBox虚拟机中，之后我们利用Vagrant命令来进行 CoreOS虚拟机的启停。CoreOS以及Vagrant都在持续演进，尤其是CoreOS目前在active dev中，版本号变化很快，这也是CoreOS滚动升级的必然结果。因此在安装操作演示前，我们有必要明确一下这个安装过程使用的软件版本： &#160;&#160;&#160; 物理机OS: &#160;&#160;&#160; &#160;&#160;&#160; Ubuntu 12.04 3.8.0-42-generic x86_64 &#160;&#160;&#160; VirtualBox: &#160;&#160;&#160; &#160;&#160;&#160; Oracle VM VirtualBox Manager 4.2.10 &#160;&#160;&#160; Vagrant: &#160;&#160;&#160; &#160;&#160;&#160; Vagrant 1.7.3 &#160;&#160;&#160; CoreOS: &#160;&#160;&#160; &#160;&#160;&#160; stable 717.3.0 &#160;&#160;&#160; coreos-vagrant source: &#160;&#160;&#160; &#160;&#160;&#160; commit [...]]]></description>
			<content:encoded><![CDATA[<p><a href="http://coreos.com">CoreOS</a>是一种专门为运行类<a href="http://tonybai.com/tag/docker">docker</a>容器而生的linux发行版。与其他通用linux发行版（<a href="http://tonybai.com/tag/ubuntu">ubuntu</a>、<a href="http://www.debian.org">debian</a>、<a href="http://www.redhat.com">redhat</a>)相 比，它具有体型最小，消耗最小，支持滚动更新等特点。除此之外CoreOS内置的分布式系统服务组件也给开发者和运维者组建分布式集群、部署分布式服务应 用带来了极大便利。</p>
<p>CoreOS与知名容器<a href="http://docker.com">Docker</a>脚前脚后诞生，到目前为止已经较为成熟，国外主流云平台提供商如<a href="http://aws.amazon.com/ec2">Amazon EC2</a>、<a href="https://cloud.google.com/compute">Google Compute Engine</a>、<a href="http://azure.microsoft.com">Microsoft Azure</a>、<a href="https://www.digitalocean.com/?refcode=bff6eed92687">Digtial Ocean</a>等均提供了CoreOS image，通过这些服务，你可以一键建立一个CoreOS实例，这似乎也是CoreOS官方推荐的主流install方式（最Easy）。</p>
<p>CoreOS当然支持其他方式的安装，比如支持虚拟机安装(<a href="https://www.vagrantup.com">vagrant</a>+<a href="https://virtualbox.org">virtualbox</a>)、PXE(preboot execute environment)安装以及iso install to 物理disk方式。如果仅仅是做一些实验，虚拟机安装是最简单也是最安全的方式。不过由于CoreOS的官方下载站在大陆无法直接访问（大陆程序员们好悲 催啊），因此这一最简单的虚拟机安装CoreOS的过程也就不那么简单了。</p>
<p>通过core-vagrant安装的直接结果是CoreOS被安装到一个VirtualBox虚拟机中，之后我们利用Vagrant命令来进行 CoreOS虚拟机的启停。CoreOS以及Vagrant都在持续演进，尤其是CoreOS目前在active dev中，版本号变化很快，这也是CoreOS滚动升级的必然结果。因此在安装操作演示前，我们有必要明确一下这个安装过程使用的软件版本：</p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; 物理机OS:<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; Ubuntu 12.04 3.8.0-42-generic x86_64<br />
	&nbsp;&nbsp;&nbsp; VirtualBox:<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; Oracle VM VirtualBox Manager 4.2.10<br />
	&nbsp;&nbsp;&nbsp; Vagrant:<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; Vagrant 1.7.3</font><br />
	<font face="Courier New">&nbsp;&nbsp;&nbsp; CoreOS:<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; stable 717.3.0</font><br />
	<font face="Courier New">&nbsp;&nbsp;&nbsp; coreos-vagrant source:<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; commit b9ed7e2182ff08b72419ab3e89f4a5652bc75082</font></p>
<p><b>一、原理</b></p>
<p>如果没有Wall，CoreOS的coreos-vagrant安装将非常简单：</p>
<p><font face="Courier New">1、git clone https://github.com/coreos/coreos-vagrant<br />
	2、编辑配置文件<br />
	3、vagrant up<br />
	4、vagrant ssh</font></p>
<p>但是现在有了Wall，步骤3：vagrant up会报错：无法连接到http://stable.release.core-os.net/amd64-usr/717.3.0/xx这个url，导致安装失败。</p>
<p>我大致分析了一下vagrant up的执行过程：</p>
<p><font face="Courier New">1、设置配置默认值</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; $num_instances = 1<br />
	&nbsp;&nbsp;&nbsp; $instance_name_prefix = &quot;core&quot;<br />
	&nbsp;&nbsp;&nbsp; $update_channel = &quot;alpha&quot;<br />
	&nbsp;&nbsp;&nbsp; $image_version = &quot;current&quot;<br />
	&nbsp;&nbsp;&nbsp; $enable_serial_logging = false<br />
	&nbsp;&nbsp;&nbsp; $share_home = false<br />
	&nbsp;&nbsp;&nbsp; $vm_gui = false<br />
	&nbsp;&nbsp;&nbsp; $vm_memory = 1024<br />
	&nbsp;&nbsp;&nbsp; $vm_cpus = 1<br />
	&nbsp;&nbsp;&nbsp; $shared_folders = {}<br />
	&nbsp;&nbsp;&nbsp; $forwarded_ports = {}</font></p>
<p>2、判断是否存在config.rb这个配置，如果有，则加载。<br />
	3、设置config.vm.url，并获取对应的json文件：</p>
<p><font face="Courier New">{<br />
	&nbsp; &quot;name&quot;: &quot;coreos-stable&quot;,<br />
	&nbsp; &quot;description&quot;: &quot;CoreOS stable&quot;,<br />
	&nbsp; &quot;versions&quot;: [{<br />
	&nbsp;&nbsp;&nbsp; &quot;version&quot;: &quot;717.3.0&quot;,<br />
	&nbsp;&nbsp;&nbsp; &quot;providers&quot;: [{<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &quot;name&quot;: &quot;virtualbox&quot;,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &quot;url&quot;: &quot;http://stable.release.core-os.net/amd64-usr/717.3.0/coreos_production_vagrant.box&quot;,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &quot;checksum_type&quot;: &quot;sha256&quot;,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &quot;checksum&quot;: &quot;99dcd74c7cae8b1d90f108f8819f92b17bfbd34f4f141325bd0400fe4def55b6&quot;<br />
	&nbsp;&nbsp;&nbsp; }]<br />
	&nbsp; }]<br />
	}</font></p>
<p>4、根据config.vm.provider（是virtualbox还是vmvare等）来决定采用哪种虚拟机创建逻辑。</p>
<p>这里我们看到，整个过程只需要从core-os.net下载两个文件：<font face="Courier New">coreos_production_vagrant.box</font>和<font face="Courier New">coreos_production_vagrant.json</font>。如果我们提前将这两个文件下载到本地，并放在一个临时的http server下，修改Vagrantfile和coreos_production_vagrant.json这两个文件，就应该可以通过coreos-vagrant安装了。</p>
<p><b>二、coreos-vagrant安装single instance CoreOS</b></p>
<p>好了，根据上述原理，我们首先要下载<font face="Courier New">coreos_production_vagrant.box</font>和<font face="Courier New">coreos_production_vagrant.json这两个文件，根据我们的channel和版本选择，两个文件的下载地址分别为：</font></p>
<p><font face="Courier New">&nbsp;http://stable.release.core-os.net/amd64-usr/717.3.0/coreos_production_vagrant.box</font><br />
	<font face="Courier New">&nbsp;http://stable.release.core-os.net/amd64-usr/717.3.0/</font><font face="Courier New"><font face="Courier New">coreos_production_vagrant.json</font></font></p>
<p>接下来就是不管你用什么梯子，只要把这两个文件下载到本地，并放到一个目录下就好了。</p>
<p>我们需要修改一下<font face="Courier New"><font face="Courier New">coreos_production_vagrant.json，将其中的url改为：<br />
	&nbsp;&nbsp;&nbsp;<br />
	&nbsp;&nbsp;&nbsp; &quot;url&quot;: &quot;http://localhost:8080/coreos_production_vagrant.box&quot;</font></font></p>
<p>我们要将这两个文件放到一个local file server中，后续供core-vagrant访问。最简单的方法就是使用<font face="Courier New">:</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; python -m SimpleHTTPServer 8080</font></p>
<p>当然使用Go实现一个简单的http file server也是非常简单的：</p>
<p><font face="Courier New">//fileserver.go<br />
	package main</font></p>
<p><font face="Courier New">import &quot;net/http&quot;<br />
	import &quot;log&quot;</font></p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp; log.Fatal(http.ListenAndServe(&quot;:8080&quot;, http.FileServer(http.Dir(&quot;./&quot;))))<br />
	}</font></p>
<p>接下来我们就可以按照正常步骤，下载coreos-vagrant并up了：</p>
<p><font face="Courier New">$git clone https://github.com/coreos/coreos-vagrant</font></p>
<p>修改Vagrantfile：</p>
<p><font face="Courier New">$ diff Vagrantfile Vagrantfile.bak<br />
	14,15c14,15<br />
	&lt; $update_channel = &quot;stable&quot;<br />
	&lt; $image_version = &quot;717.3.0&quot;<br />
	&#8212;<br />
	&gt; $update_channel = &quot;alpha&quot;<br />
	&gt; $image_version = &quot;current&quot;<br />
	55c55<br />
	&lt;&nbsp;&nbsp; config.vm.box_url = &quot;http://localhost:8080/coreos_production_vagrant.json&quot;<br />
	&#8212;<br />
	&gt;&nbsp;&nbsp; config.vm.box_url = &quot;http://%s.release.core-os.net/amd64-usr/%s/coreos_production_vagrant.json&quot; % [$update_channel, $image_version]</font></p>
<p>将user-data.sample改名为user-data，并编辑user-data，在etcd2下面增加一行：</p>
<p>&nbsp;&nbsp;&nbsp; &nbsp; <font face="Courier New">etcd2:<br />
	&nbsp;&nbsp;&nbsp; name: core-01</font></p>
<p>将units:下面对于etcd2的注释去掉，以enable etcd2服务。（将etcd服务注释掉）</p>
<p>万事俱备，只需vagrant up。</p>
<p><font face="Courier New">$ vagrant up<br />
	Bringing machine &#39;core-01&#39; up with &#39;virtualbox&#39; provider&#8230;<br />
	==&gt; core-01: Box &#39;coreos-stable&#39; could not be found. Attempting to find and install&#8230;<br />
	&nbsp;&nbsp;&nbsp; core-01: <b>Box Provider: virtualbox</b><br />
	&nbsp;&nbsp;&nbsp; core-01: <b>Box Version: 717.3.0</b><br />
	==&gt; core-01: <b>Loading metadata for box &#39;http://localhost:8080/coreos_production_vagrant.json&#39;</b><br />
	&nbsp;&nbsp;&nbsp; core-01: URL: http://localhost:8080/coreos_production_vagrant.json<br />
	==&gt; core-01: Adding box &#39;coreos-stable&#39; (v717.3.0) for provider: virtualbox<br />
	&nbsp;&nbsp;&nbsp; core-01: <b>Downloading: http://localhost:8080/coreos_production_vagrant.box</b><br />
	&nbsp;&nbsp;&nbsp; core-01: Calculating and comparing box checksum&#8230;<br />
	==&gt; core-01: Successfully added box &#39;coreos-stable&#39; (v717.3.0) for &#39;virtualbox&#39;!<br />
	==&gt; core-01: Importing base box &#39;coreos-stable&#39;&#8230;<br />
	==&gt; core-01: Matching MAC address for NAT networking&#8230;<br />
	==&gt; core-01: Checking if box &#39;coreos-stable&#39; is up to date&#8230;<br />
	==&gt; core-01: Setting the name of the VM: coreos-vagrant_core-01_1437121834188_89503<br />
	==&gt; core-01: Clearing any previously set network interfaces&#8230;<br />
	==&gt; core-01: Preparing network interfaces based on configuration&#8230;<br />
	&nbsp;&nbsp;&nbsp; core-01: Adapter 1: nat<br />
	&nbsp;&nbsp;&nbsp; core-01: Adapter 2: hostonly<br />
	==&gt; core-01: Forwarding ports&#8230;<br />
	&nbsp;&nbsp;&nbsp; core-01: 22 =&gt; 2222 (adapter 1)<br />
	==&gt; core-01: Running &#39;pre-boot&#39; VM customizations&#8230;<br />
	==&gt; core-01: Booting VM&#8230;<br />
	==&gt; core-01: Waiting for machine to boot. This may take a few minutes&#8230;<br />
	&nbsp;&nbsp;&nbsp; core-01: SSH address: 127.0.0.1:2222<br />
	&nbsp;&nbsp;&nbsp; core-01: SSH username: core<br />
	&nbsp;&nbsp;&nbsp; core-01: SSH auth method: private key<br />
	&nbsp;&nbsp;&nbsp; core-01: Warning: Connection timeout. Retrying&#8230;<br />
	==&gt; core-01: Machine booted and ready!<br />
	==&gt; core-01: Setting hostname&#8230;<br />
	==&gt; core-01: Configuring and enabling network interfaces&#8230;<br />
	==&gt; core-01: Running provisioner: file&#8230;<br />
	==&gt; core-01: Running provisioner: shell&#8230;<br />
	&nbsp;&nbsp;&nbsp; core-01: Running: inline script</font></p>
<p><font face="Courier New">登入你的coreos实例：<br />
	$ vagrant ssh<br />
	CoreOS stable (717.3.0)<br />
	core@core-01 ~ $ </font></p>
<p>在vagrant up时，你可能会遇到如下两个错误：</p>
<p>错误1：</p>
<p><font face="Courier New">Progress state: VBOX_E_FILE_ERROR<br />
	VBoxManage: error: Could not open the medium storage unit &#39;/home1/tonybai/.vagrant.d/boxes/coreos-stable/717.3.0/virtualbox/coreos_production_vagrant_image.vmdk&#39;.<br />
	VBoxManage: error: VMDK: inconsistent references to grain directory in &#39;/home1/tonybai/.vagrant.d/boxes/coreos-stable/717.3.0/virtualbox/coreos_production_vagrant_image.vmdk&#39;&nbsp; (VERR_VD_VMDK_INVALID_HEADER).</font></p>
<p>这个问题的原因很可能是你的Virtualbox版本不对，比如版本太低，与<font face="Courier New"><b>coreos_production_vagrant.box</b>格式不兼容<b>。可</b></font>尝试安装一下高版本virtualbox来解决。</p>
<p>错误2：</p>
<p><font face="Courier New">core-01: SSH address: 127.0.0.1:2222<br />
	core-01: SSH username: core<br />
	core-01: SSH auth method: private key<br />
	core-01: Warning: Connection timeout. Retrying&#8230;<br />
	core-01: Warning: Connection timeout. Retrying&#8230;<br />
	core-01: Warning: Connection timeout. Retrying&#8230;</font></p>
<p>coreos虚拟机创建后，似乎一直无法连接上。在coreos的github issue中，有人遇到了这个问题，目前给出的原因是因为cpu的支持虚拟化技术的vt开关没有打开，需要在bios中将其开启。这主要在安装64bit box时才会发生。</p>
<p>到这里，我们已经完成了一个single instance coreos虚拟机的安装。vagrant halt可以帮助你将启动的coreos虚拟机停下来。</p>
<p><font face="Courier New">$ vagrant halt<br />
	==&gt; core-01: Attempting graceful shutdown of VM&#8230;</font></p>
<p><b>三、&nbsp; CoreOS</b><font face="Courier New"><b> cluster</b></font></p>
<p>上面虽然成功的安装了coreos，然并卵。在实际应用中，CoreOS多以Cluster形式呈现，也就是说我们要启动多个CoreOS实例。</p>
<p>使用vagrant启动多个coreos实例很简单，只需将配置中的$num_instances从1改为n。</p>
<p>这里我们启用config.rb这个配置文件(将<font face="Courier New">config.rb.sample改名为config.rb</font>)，并将其中的$num_instances修改为3：</p>
<p><font face="Courier New"># Size of the CoreOS cluster created by Vagrant<br />
	$num_instances=3</font></p>
<p>该配置文件中的数据会覆盖Vagrantfile中的默认配置。</p>
<p>三个instance中的etcd2要想组成集群还需要一个配置修改，那就是在etcd.io上申请一个token：</p>
<p><font face="Courier New">$curl https://discovery.etcd.io/new</p>
<p>https://discovery.etcd.io/fe81755687323aae273dc5f111eb059a</font></p>
<p>将这个token配置到user-data中的etcd2下：</p>
<p><font face="Courier New">&nbsp; etcd2:</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; #generate a new token for each unique cluster from https://discovery.etcd.io/new<br />
	&nbsp;&nbsp;&nbsp; #discovery: https://discovery.etcd.io/&lt;token&gt;<br />
	&nbsp;&nbsp;&nbsp; discovery: https://discovery.etcd.io/fe81755687323aae273dc5f111eb059a</font></p>
<p>我们再来up看看：</p>
<p><font face="Courier New">$ vagrant up<br />
	Bringing machine &#39;core-01&#39; up with &#39;virtualbox&#39; provider&#8230;<br />
	Bringing machine &#39;core-02&#39; up with &#39;virtualbox&#39; provider&#8230;<br />
	Bringing machine &#39;core-03&#39; up with &#39;virtualbox&#39; provider&#8230;<br />
	==&gt; core-01: Checking if box &#39;coreos-stable&#39; is up to date&#8230;<br />
	==&gt; core-01: VirtualBox VM is already running.<br />
	==&gt; core-02: Importing base box &#39;coreos-stable&#39;&#8230;<br />
	==&gt; core-02: Matching MAC address for NAT networking&#8230;<br />
	==&gt; core-02: Checking if box &#39;coreos-stable&#39; is up to date&#8230;<br />
	==&gt; core-02: Setting the name of the VM: coreos-vagrant_core-02_1437388468647_96550<br />
	==&gt; core-02: Fixed port collision for 22 =&gt; 2222. Now on port 2200.<br />
	==&gt; core-02: Clearing any previously set network interfaces&#8230;<br />
	==&gt; core-02: Preparing network interfaces based on configuration&#8230;<br />
	&nbsp;&nbsp;&nbsp; core-02: Adapter 1: nat<br />
	&nbsp;&nbsp;&nbsp; core-02: Adapter 2: hostonly<br />
	==&gt; core-02: Forwarding ports&#8230;<br />
	&nbsp;&nbsp;&nbsp; core-02: 22 =&gt; 2200 (adapter 1)<br />
	==&gt; core-02: Running &#39;pre-boot&#39; VM customizations&#8230;<br />
	==&gt; core-02: Booting VM&#8230;<br />
	==&gt; core-02: Waiting for machine to boot. This may take a few minutes&#8230;<br />
	&nbsp;&nbsp;&nbsp; core-02: SSH address: 127.0.0.1:2200<br />
	&nbsp;&nbsp;&nbsp; core-02: SSH username: core<br />
	&nbsp;&nbsp;&nbsp; core-02: SSH auth method: private key<br />
	&nbsp;&nbsp;&nbsp; core-02: Warning: Connection timeout. Retrying&#8230;<br />
	==&gt; core-02: Machine booted and ready!<br />
	==&gt; core-02: Setting hostname&#8230;<br />
	==&gt; core-02: Configuring and enabling network interfaces&#8230;<br />
	==&gt; core-02: Running provisioner: file&#8230;<br />
	==&gt; core-02: Running provisioner: shell&#8230;<br />
	&nbsp;&nbsp;&nbsp; core-02: Running: inline script<br />
	==&gt; core-03: Importing base box &#39;coreos-stable&#39;&#8230;<br />
	==&gt; core-03: Matching MAC address for NAT networking&#8230;<br />
	==&gt; core-03: Checking if box &#39;coreos-stable&#39; is up to date&#8230;<br />
	==&gt; core-03: Setting the name of the VM: coreos-vagrant_core-03_1437388512743_68112<br />
	==&gt; core-03: Fixed port collision for 22 =&gt; 2222. Now on port 2201.<br />
	==&gt; core-03: Clearing any previously set network interfaces&#8230;<br />
	==&gt; core-03: Preparing network interfaces based on configuration&#8230;<br />
	&nbsp;&nbsp;&nbsp; core-03: Adapter 1: nat<br />
	&nbsp;&nbsp;&nbsp; core-03: Adapter 2: hostonly<br />
	==&gt; core-03: Forwarding ports&#8230;<br />
	&nbsp;&nbsp;&nbsp; core-03: 22 =&gt; 2201 (adapter 1)<br />
	==&gt; core-03: Running &#39;pre-boot&#39; VM customizations&#8230;<br />
	==&gt; core-03: Booting VM&#8230;<br />
	==&gt; core-03: Waiting for machine to boot. This may take a few minutes&#8230;<br />
	&nbsp;&nbsp;&nbsp; core-03: SSH address: 127.0.0.1:2201<br />
	&nbsp;&nbsp;&nbsp; core-03: SSH username: core<br />
	&nbsp;&nbsp;&nbsp; core-03: SSH auth method: private key<br />
	&nbsp;&nbsp;&nbsp; core-03: Warning: Connection timeout. Retrying&#8230;<br />
	==&gt; core-03: Machine booted and ready!<br />
	==&gt; core-03: Setting hostname&#8230;<br />
	==&gt; core-03: Configuring and enabling network interfaces&#8230;<br />
	==&gt; core-03: Running provisioner: file&#8230;<br />
	==&gt; core-03: Running provisioner: shell&#8230;<br />
	&nbsp;&nbsp;&nbsp; core-03: Running: inline script</font></p>
<p><font face="Courier New">$vagrant ssh core-02<br />
	CoreOS stable (717.3.0)<br />
	core@core-02 ~ $</font></p>
<p>可以看到Vagrant启动了三个coreos instance。关闭这些instance，同样用halt：</p>
<p><font face="Courier New">$ vagrant halt<br />
	==&gt; core-03: Attempting graceful shutdown of VM&#8230;<br />
	==&gt; core-02: Attempting graceful shutdown of VM&#8230;<br />
	==&gt; core-01: Attempting graceful shutdown of VM&#8230;</font></p>
<p><b>四、小结</b></p>
<p>以上仅仅是CoreOS最基本的入门，虽然现在安装ok了，但CoreOS的各种服务组件的功用、配置；如何与Docker配合形成分布式服务系统；如何用<a href="http://github.com/GoogleCloudPlatform/kubernetes">Google Kubernetes</a>管理容器集群等还需更进一步深入学习，这个后续会慢慢道来。</p>
<p style='text-align:left'>&copy; 2015, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2015/07/20/install-coreos-by-coreos-vagrant/feed/</wfw:commentRss>
		<slash:comments>4</slash:comments>
		</item>
		<item>
		<title>探讨Docker容器中修改系统变量的方法</title>
		<link>https://tonybai.com/2014/10/14/discussion-on-the-approach-to-modify-system-variables-in-docker/</link>
		<comments>https://tonybai.com/2014/10/14/discussion-on-the-approach-to-modify-system-variables-in-docker/#comments</comments>
		<pubDate>Tue, 14 Oct 2014 13:56:12 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[centos]]></category>
		<category><![CDATA[container]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[init]]></category>
		<category><![CDATA[Kernel]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[phusion]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[Redhat]]></category>
		<category><![CDATA[SharedMemory]]></category>
		<category><![CDATA[shmget]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[共享内存]]></category>
		<category><![CDATA[内核]]></category>
		<category><![CDATA[博客]]></category>
		<category><![CDATA[容器]]></category>
		<category><![CDATA[工作]]></category>
		<category><![CDATA[开源]]></category>
		<category><![CDATA[思考]]></category>
		<category><![CDATA[感悟]]></category>
		<category><![CDATA[程序员]]></category>
		<category><![CDATA[虚拟化]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=1563</guid>
		<description><![CDATA[探讨完Docker对共享内存状态持久化的支持状况后，我将遗留产品build到一个pre-production image中，测试启动是否OK。很显然，我过于乐观了，Docker之路并不平坦。我收到了shmget报出的EINVAL错误码，提示参数非法。 shmget的manual对EINVAL错误码的说明如下： EINVAL： A&#160; new&#160; segment&#160; was&#160; to&#160; be&#160; created&#160; and size &#60; SHMMIN or size &#62; SHMMAX, or no new segment was to be created, a segment with given key existed, but size is greater than the size of that segment. 显然我们要创建的shared memory的size很可能大于SHMMAX这个系统变量了。那么一个从base image创建出的容器中的系统变量到底是什么值呢？我们来查看一下，我们基于&#34;centos:centos6&#34;启动一个Docker容器，并检查其中的 系统变量值设置： $ sudo docker run -it &#34;centos:centos6&#34; /bin/bash bash-4.1# [...]]]></description>
			<content:encoded><![CDATA[<p>探讨完<a href="http://tonybai.com/2014/10/12/discussion-on-shared-mem-support-in-docker/">Docker对共享内存状态持久化的支持状况</a>后，我将遗留产品build到一个pre-production image中，测试启动是否OK。很显然，我过于乐观了，<a href="http://www.docker.com">Docker</a>之路并不平坦。我收到了shmget报出的EINVAL错误码，提示参数非法。 shmget的manual对EINVAL错误码的说明如下：</p>
<p><font face="Courier New">EINVAL：<br />
	A&nbsp; new&nbsp; segment&nbsp; was&nbsp; to&nbsp; be&nbsp; created&nbsp; and size &lt; SHMMIN or size &gt; SHMMAX, or no new segment was to be created, a segment with given key existed, but size is greater than the size of that segment.</font></p>
<p>显然我们要创建的shared memory的size很可能大于SHMMAX这个系统变量了。那么一个从base image创建出的容器中的系统变量到底是什么值呢？我们来查看一下，我们基于&quot;centos:centos6&quot;启动一个Docker容器，并检查其中的 系统变量值设置：</p>
<p><font face="Courier New">$ sudo docker run -it &quot;centos:centos6&quot; /bin/bash<br />
	bash-4.1# cat /proc/sys/kernel/shmmax<br />
	33554432<br />
	bash-4.1# sysctl -a|grep shmmax<br />
	kernel.shmmax = 33554432</font></p>
<p>可以看出默认情况下，当前容器中root账号看到的shmmax值我<font face="Courier New">33554432</font>， 我的程序要创建的shm size的确要大于这个值，报出EINVAL错误也就无可厚非了。我尝试按照物理机上的方法临时修改一下该值：</p>
<p><font face="Courier New">bash-4.1# echo 68719476736 &gt; /proc/sys/kernel/shmmax<br />
	bash: /proc/sys/kernel/shmmax: Read-only file system</font></p>
<p><font face="Courier New">/proc/sys/kernel/shmmax居然是只读的，无法修改。</font></p>
<p><font face="Courier New">我又尝试修改/etc/sysctl.conf这个持久化系统变量的地方，但打开/etc/sysctl.conf文件，我发现我又错了，这 个文件中shmmax的值如下：</font></p>
<p><font face="Courier New"># Controls the maximum shared segment size, in bytes<br />
	kernel.shmmax = 68719476736</font></p>
<p><font face="Courier New"><font face="Courier New">/etc/sysctl.conf文件 中的系统变量shmmax的值是68719476736，而系统当前的实际值则是33554432，难道是/etc /sysctl.conf中的值没有生效，于是我手工重新加载一次该文件：</font></font></p>
<p><font face="Courier New"><font face="Courier New">-bash-4.1# sysctl -p<br />
	error: &quot;Read-only file system&quot; setting key &quot;net.ipv4.ip_forward&quot;<br />
	error: &quot;Read-only file system&quot; setting key &quot;net.ipv4.conf.default.rp_filter&quot;<br />
	error: &quot;Read-only file system&quot; setting key &quot;net.ipv4.conf.default.accept_source_route&quot;<br />
	error: &quot;Read-only file system&quot; setting key &quot;kernel.sysrq&quot;<br />
	error: &quot;Read-only file system&quot; setting key &quot;kernel.core_uses_pid&quot;<br />
	error: &quot;net.ipv4.tcp_syncookies&quot; is an unknown key<br />
	error: &quot;net.bridge.bridge-nf-call-ip6tables&quot; is an unknown key<br />
	error: &quot;net.bridge.bridge-nf-call-iptables&quot; is an unknown key<br />
	error: &quot;net.bridge.bridge-nf-call-arptables&quot; is an unknown key<br />
	error: &quot;Read-only file system&quot; setting key &quot;kernel.msgmnb&quot;<br />
	error: &quot;Read-only file system&quot; setting key &quot;kernel.msgmax&quot;<br />
	error: &quot;Read-only file system&quot; setting key &quot;kernel.shmmax&quot;<br />
	error: &quot;Read-only file system&quot; setting key &quot;kernel.shmall&quot;</font></font></p>
<p><font face="Courier New"><font face="Courier New">我得到了和之前类似的错误结果：只读文件系统，无法修改。于是乎两个问题萦绕在我的面前：<br />
	1、为什么容器内当前系统变量值与sysctl.conf中的不一致？<br />
	2、为什么无法修改当前系统变量值?</font></font></p>
<p><font face="Courier New"><font face="Courier New">在翻阅了Stackoverflow, github docker issues后，我得到了的答案如下：</font></font></p>
<p><font face="Courier New"><font face="Courier New">1、Docker的base image做的很精简，甚至都没有init进程，原本在OS启动时执行生效系统变量的过程(sysctl -p)也给省略了，导致这些系统变量依旧保留着kernel默认值。以CentOs为例，在linux kernel boot后，init都会执行/etc/rc.d/rc.sysinit，后者会加载/etc/sysctl.conf中的系统变量值。下面是 CentOs5.6中的rc.sysinit代码摘录：</font></font></p>
<p><font face="Courier New"><font face="Courier New">&#8230; &#8230;<br />
	# Configure kernel parameters<br />
	update_boot_stage RCkernelparam<br />
	sysctl -e -p /etc/sysctl.conf &gt;/dev/null 2&gt;&amp;1<br />
	&#8230; &#8230;</font></font></p>
<p><font face="Courier New"><font face="Courier New">2、Docker容器中的系统变量在non-priviledged模式下目前(我使用的时docker 1.2.0版本)就无法修改，</font></font>这 和resolv.conf、hosts等文件映射到宿主机对应的文件有不同。</p>
<p><font face="Courier New">$ mount -l<br />
	&#8230;. &#8230;.<br />
	/dev/mapper/ubuntu&#8211;Server&#8211;14&#8211;vg-root on /etc/resolv.conf type ext4 (rw,relatime,errors=remount-ro,data=ordered)<br />
	/dev/mapper/ubuntu&#8211;Server&#8211;14&#8211;vg-root on /etc/hostname type ext4 (rw,relatime,errors=remount-ro,data=ordered)<br />
	/dev/mapper/ubuntu&#8211;Server&#8211;14&#8211;vg-root on /etc/hosts type ext4 (rw,relatime,errors=remount-ro,data=ordered)<br />
	&#8230; &#8230;</font></p>
<p>那么我们该如何修改系统变量值来满足遗留产品的需求呢？</p>
<p><b>一、使用&#8211;privileged选项</b></p>
<p>我们使用&#8211;privileged这个特权选项来启动一个基于centos:centos6的新容器，看看是否能对shmmax这样的系统变量值 进行修改：</p>
<p><font face="Courier New">$ sudo docker run -it &#8211;privileged&nbsp; &quot;centos:centos6&quot; /bin/bash<br />
	bash-4.1# cat /proc/sys/kernel/shmmax<br />
	33554432<br />
	bash-4.1# echo 68719476736 &gt; /proc/sys/kernel/shmmax<br />
	bash-4.1# cat /proc/sys/kernel/shmmax<br />
	68719476736</font><br />
	<font face="Courier New">bash-4.1# sysctl -p<br />
	net.ipv4.ip_forward = 0<br />
	net.ipv4.conf.default.rp_filter = 1<br />
	net.ipv4.conf.default.accept_source_route = 0<br />
	kernel.sysrq = 0<br />
	kernel.core_uses_pid = 1<br />
	&#8230; &#8230;<br />
	kernel.msgmnb = 65536<br />
	kernel.msgmax = 65536<br />
	kernel.shmmax = 68719476736<br />
	kernel.shmall = 4294967296</font></p>
<p>可以看出，通过&#8211;privileged选项，容器获得了额外的特权，并且可以对系统变量的值进行修改了。不过这样的修改是不能保存在容器里的， 我们stop 容器，再重启该容器就能看出来：</p>
<p><font face="Courier New">$ sudo docker start 3e22d65a7845<br />
	$ sudo docker attach 3e22d65a7845<br />
	bash-4.1# cat /proc/sys/kernel/shmmax<br />
	33554432</font></p>
<p>shmmax的值在容器重启后又变回了原先的那个默认值。不过重启后的容器依旧具有privileged的特权，我们还可以重新手工执行命令对系 统变量进行修改：</p>
<p><font face="Courier New">bash-4.1# echo 68719476736 &gt; /proc/sys/kernel/shmmax<br />
	bash-4.1# cat /proc/sys/kernel/shmmax<br />
	68719476736</font></p>
<p>但即便这样，也无法满足我们的需求，我们总不能每次都在容器中手工执行系统变量值修改的操作吧。privileged选项的能力能否带到 image中呢？答案是目前还不能，我们无法在build image时通过privileged选项修改系统变量值。</p>
<p>这样一来，我们能做的只有把产品启动与系统变量值修改放在一个脚本中了，并将该脚本作为docker 容器的cmd命令来执行，比如我们构建一个Dockerfile：</p>
<p><font face="Courier New">FROM centos:centos6<br />
	MAINTAINER Tony Bai <a class="moz-txt-link-rfc2396E" href="mailto:bigwhite.cn@gmail.com">&lt;bigwhite.cn@gmail.com&gt;</a><br />
	RUN yum install python-setuptools -y<br />
	RUN easy_install supervisor<br />
	RUN mkdir -p /var/log/supervisor<br />
	COPY ./supervisord.conf /etc/supervisord.conf<br />
	COPY ./start.sh /bin/start.sh<br />
	RUN chmod +x /bin/start.sh<br />
	CMD ["/bin/start.sh]</font></p>
<p><font face="Courier New">//start.sh<br />
	sysctl -p<br />
	/usr/bin/supervisord</font></p>
<p>这样，start.sh在supervisord启动前将系统变量值重新加载，而supervisord后续启动的程序就可以看到这些新系统变量 的值了。不过别忘了利用这个image启动容器时要加上&#8211;priviledged选项，否则容器启动就会失败。</p>
<p><b>二、使用phusion/baseimage</b></p>
<p>前面说过/etc/sysctl.conf中的值没有生效是因为docker官方提供的centos:centos6把init进程的初始化过程给精 简掉了。<a href="https://registry.hub.docker.com/u/phusion/baseimage/">phusion/baseimage</a>是目前docker registery上仅次于ubuntu和centos两个之后的base image，其提供了/sbin/my_init这个init进程，用于在container充当init进程的角色。那么my_init是否可以用于执行sysctl -p呢？我们试验一下：</p>
<p>我们先pull这个base image下来：<font face="Courier New">sudo docker pull phusion/baseimage。pull成功后，我们先基于&ldquo;phusion/baseimage&rdquo;启动一个容器做一些explore工作：</font></p>
<p><font face="Courier New">$ sudo docker run -i -t &quot;phusion/baseimage&quot;<br />
	*** Running /etc/my_init.d/00_regen_ssh_host_keys.sh&#8230;<br />
	No SSH host key available. Generating one&#8230;<br />
	Creating SSH2 RSA key; this may take some time &#8230;<br />
	Creating SSH2 DSA key; this may take some time &#8230;<br />
	Creating SSH2 ECDSA key; this may take some time &#8230;<br />
	Creating SSH2 ED25519 key; this may take some time &#8230;<br />
	invoke-rc.d: policy-rc.d denied execution of restart.<br />
	*** Running /etc/rc.local&#8230;<br />
	*** Booting runit daemon&#8230;<br />
	*** Runit started as PID 100</font></p>
<p><font face="Courier New">通过nsenter进去，查看一下/sbin/my_init的源码，我们发现这是一个python脚本，不过从头到尾浏览一遍，没有发现sysctl加载/etc/sysctl.conf系统变量的操作。</font></p>
<p><font face="Courier New">不过，phusion文档中说my_init可以在初始化过程中执行/etc/my_init.d下的脚本。那是不是我们将一个执行sysctl -p的脚本放入/etc/my_init.d下就可以实现我们的目的了呢？试试。</font></p>
<p><font face="Courier New">我们编写一个脚本：load_sys_varibles.sh</font></p>
<p><font face="Courier New">#!/bin/sh<br />
	sysctl -p &gt; init.txt</font></p>
<p><font face="Courier New">下面是制作image的Dockerfile:</font></p>
<p><font face="Courier New">FROM phusion/baseimage:latest<br />
	MAINTAINER Tony Bai &lt;bigwhite.cn@gmail.com&gt;<br />
	RUN echo &quot;kernel.shmmax = 68719476736&quot; &gt;&gt; /etc/sysctl.conf<br />
	RUN mkdir -p /etc/my_init.d<br />
	ADD load_sys_varibles.sh /etc/my_init.d/load_sys_varibles.sh<br />
	RUN chmod +x /etc/my_init.d/load_sys_varibles.sh<br />
	CMD ["/sbin/my_init"]</font></p>
<p><font face="Courier New">phusion/baseimage是基于ubuntu的OS，其sysctl.conf默认情况下没啥内容，所以我们在Dockerfile中向这个文件写入我们需要的系统变量值。构建image并启动容器：</font></p>
<p><font face="Courier New">$ sudo docker build -t &quot;myphusion:v1&quot; ./<br />
	Sending build context to Docker daemon 13.12 MB<br />
	Sending build context to Docker daemon<br />
	Step 0 : FROM phusion/baseimage:latest<br />
	&nbsp;&#8212;&gt; cf39b476aeec<br />
	Step 1 : MAINTAINER Tony Bai &lt;bigwhite.cn@gmail.com&gt;<br />
	&nbsp;&#8212;&gt; Using cache<br />
	&nbsp;&#8212;&gt; d0e9b51a3e4f<br />
	Step 2 : RUN echo &quot;kernel.shmmax = 68719476736&quot; &gt;&gt; /etc/sysctl.conf<br />
	&nbsp;&#8212;&gt; Using cache<br />
	&nbsp;&#8212;&gt; 2c800687cc83<br />
	Step 3 : RUN mkdir -p /etc/my_init.d<br />
	&nbsp;&#8212;&gt; Using cache<br />
	&nbsp;&#8212;&gt; fe366eea5eb4<br />
	Step 4 : ADD load_sys_varibles.sh /etc/my_init.d/load_sys_varibles.sh<br />
	&nbsp;&#8212;&gt; a641bb595fb9<br />
	Removing intermediate container c381b9f001c2<br />
	Step 5 : RUN chmod +x /etc/my_init.d/load_sys_varibles.sh<br />
	&nbsp;&#8212;&gt; Running in 764866552f25<br />
	&nbsp;&#8212;&gt; eae3d7f1eac5<br />
	Removing intermediate container 764866552f25<br />
	Step 6 : CMD ["/sbin/my_init"]<br />
	&nbsp;&#8212;&gt; Running in 9ab8d0b717a7<br />
	&nbsp;&#8212;&gt; 8be4e7b6b174<br />
	Removing intermediate container 9ab8d0b717a7<br />
	Successfully built 8be4e7b6b174</font></p>
<p><font face="Courier New">$ sudo docker run -it &quot;myphusion:v1&quot;<br />
	*** Running /etc/my_init.d/00_regen_ssh_host_keys.sh&#8230;<br />
	No SSH host key available. Generating one&#8230;<br />
	Creating SSH2 RSA key; this may take some time &#8230;<br />
	Creating SSH2 DSA key; this may take some time &#8230;<br />
	Creating SSH2 ECDSA key; this may take some time &#8230;<br />
	Creating SSH2 ED25519 key; this may take some time &#8230;<br />
	invoke-rc.d: policy-rc.d denied execution of restart.<br />
	*** Running /etc/my_init.d/load_sys_varibles.sh&#8230;<br />
	sysctl: setting key &quot;kernel.shmmax&quot;: Read-only file system<br />
	*** /etc/my_init.d/load_sys_varibles.sh failed with status 255</font></p>
<p><font face="Courier New">*** Killing all processes&#8230;</font></p>
<p><font face="Courier New">唉，还是老问题！即便是在my_init中执行，依旧无法逾越Read-only file system，查看Phusion/baseimage的Dockerfile才知道，它也是From ubuntu:14.04的，根不变，上层再怎么折腾也没用。</font></p>
<p><font face="Courier New">换一种容器run方法吧，加上&#8211;privileged：</font></p>
<p><font face="Courier New">$ sudo docker run -it &#8211;privileged&nbsp; &quot;myphusion:v1&quot;<br />
	*** Running /etc/my_init.d/00_regen_ssh_host_keys.sh&#8230;<br />
	No SSH host key available. Generating one&#8230;<br />
	Creating SSH2 RSA key; this may take some time &#8230;<br />
	Creating SSH2 DSA key; this may take some time &#8230;<br />
	Creating SSH2 ECDSA key; this may take some time &#8230;<br />
	Creating SSH2 ED25519 key; this may take some time &#8230;<br />
	invoke-rc.d: policy-rc.d denied execution of restart.<br />
	*** Running /etc/my_init.d/load_sys_varibles.sh&#8230;<br />
	*** Running /etc/rc.local&#8230;<br />
	*** Booting runit daemon&#8230;<br />
	*** Runit started as PID 102</font></p>
<p><font face="Courier New">这回灵光了。enter到容器里看看设置的值是否生效了：</font></p>
<p><font face="Courier New">root@9e399f46372a:~#cat /proc/sys/kernel/shmmax<br />
	68719476736</font></p>
<p><font face="Courier New">结果如预期。这样来看phusion/baseimage算是为sysctl -p加载系统变量值提供了一个便利，但依旧无法脱离&#8211;privileged，且依旧无法在image中持久化这个设置。</font></p>
<p><font face="Courier New">在Docker github的issue中有人提出建议在Dockerfile中加入类似RUNP这样的带有特权的指令语法，但不知何时才能在Docker中加入这一功能。</font></p>
<p><font face="Courier New">总而言之，基于目前docker官网提供的base image，我们很难找到特别理想的修改系统变量值的方法，除非自己制作base image，这个还没尝试过，待后续继续研究。</font></p>
<p style='text-align:left'>&copy; 2014, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2014/10/14/discussion-on-the-approach-to-modify-system-variables-in-docker/feed/</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
		<item>
		<title>docker容器内服务程序的优雅退出</title>
		<link>https://tonybai.com/2014/10/09/gracefully-shutdown-app-running-in-docker/</link>
		<comments>https://tonybai.com/2014/10/09/gracefully-shutdown-app-running-in-docker/#comments</comments>
		<pubDate>Thu, 09 Oct 2014 13:58:49 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[BestPractice]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[centos]]></category>
		<category><![CDATA[container]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[Dockerfile]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[image]]></category>
		<category><![CDATA[Kernel]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Namespace]]></category>
		<category><![CDATA[nsenter]]></category>
		<category><![CDATA[Opensource]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[Redhat]]></category>
		<category><![CDATA[Signal]]></category>
		<category><![CDATA[supervisor]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[vm]]></category>
		<category><![CDATA[yum]]></category>
		<category><![CDATA[信号]]></category>
		<category><![CDATA[内核]]></category>
		<category><![CDATA[博客]]></category>
		<category><![CDATA[命令行]]></category>
		<category><![CDATA[容器]]></category>
		<category><![CDATA[工作]]></category>
		<category><![CDATA[思考]]></category>
		<category><![CDATA[感悟]]></category>
		<category><![CDATA[映像]]></category>
		<category><![CDATA[最佳实践]]></category>
		<category><![CDATA[程序员]]></category>
		<category><![CDATA[虚拟化]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=1555</guid>
		<description><![CDATA[近期在试验如何将我们的产品部署到docker容器中去，这其中涉及到一个技术环节，那就是如何让docker容器退出时其内部运行的服务程序也 可以优雅的退出。所谓优雅退出，指的就是程序在退出前有清理资源（比如关闭文件描述符、关闭socket），保存必要中间状态，持久化内存数据 （比如将内存中的数据flush到文件中）的机会。docker作为目前最火的轻量级虚拟化技术，其在后台服务领域的应用是极其广泛的，其设计者 在程序优雅退出方面是有考虑的。下面我们由简单到复杂逐一考量一下。 一、优雅退出的原理 对于服务程序而言，一般都是以daemon形式运行在后台的。通知这些服务程序退出需要使用到系统的signal机制。一般服务程序都会监听某个 特定的退出signal，比如SIGINT、SIGTERM等（通过kill -l命令你可以查看到几十种signal）。当我们使用kill + 进程号时，系统会默认发送一个SIGTERM给相应的进程。该进程通过signal handler响应这一信号，并在这个handler中完成相应的&#8220;优雅退出&#8221;操作。 与&#8220;优雅退出&#8221;对立的是&#8220;暴力退出&#8221;，也就是我们常说的使用kill -9，也就是kill -s SIGKILL + 进程号，这个行为不会给目标进程任何时间空隙，而是直接将进程杀死，无论进程当前在做何种操作。这种操作常常导致&#8220;不一致&#8221;状态的出现。SIGKILL这 个信号比较特殊，进程无法有效监听该信号，无法有效针对该信号设置handler，无法改变其信号的默认处理行为。 二、测试用&#8220;服务程序&#8221; 为了测试docker容器对优雅退出的支持，我们编写如下&#8220;服务程序&#8221;用于放在docker容器中运行： //dockerapp1.go package main import &#34;fmt&#34; import &#34;time&#34; import &#34;os&#34; import &#34;os/signal&#34; import &#34;syscall&#34; type signalHandler func(s os.Signal, arg interface{}) type signalSet struct { &#160;&#160;&#160;&#160;&#160;&#160;&#160; m map[os.Signal]signalHandler } func signalSetNew() *signalSet { &#160;&#160;&#160;&#160;&#160;&#160;&#160; ss := new(signalSet) [...]]]></description>
			<content:encoded><![CDATA[<p><span style="line-height: 1.6em;">近期在试验如何将我们的产品部署到</span><a href="http://docker.com" style="line-height: 1.6em;">docker容器</a><span style="line-height: 1.6em;">中去，这其中涉及到一个技术环节，那就是如何让docker容器退出时其内部运行的服务程序也 可以优雅的退出。所谓优雅退出，指的就是程序在退出前有清理资源（比如关闭文件描述符、关闭socket），保存必要中间状态，持久化内存数据 （比如将内存中的数据flush到文件中）的机会。docker作为目前最火的轻量级虚拟化技术，其在后台服务领域的应用是极其广泛的，其设计者 在程序优雅退出方面是有考虑的。下面我们由简单到复杂逐一考量一下。</span></p>
<p><b>一、优雅退出的原理</b></p>
<p>对于服务程序而言，一般都是以daemon形式运行在后台的。通知这些服务程序退出需要使用到系统的signal机制。一般服务程序都会监听某个 特定的退出signal，比如SIGINT、SIGTERM等（通过kill -l命令你可以查看到几十种signal）。当我们使用kill + 进程号时，系统会默认发送一个SIGTERM给相应的进程。该进程通过signal handler响应这一信号，并在这个handler中完成相应的&ldquo;优雅退出&rdquo;操作。</p>
<p>与&ldquo;优雅退出&rdquo;对立的是&ldquo;暴力退出&rdquo;，也就是我们常说的使用kill -9，也就是kill -s SIGKILL + 进程号，这个行为不会给目标进程任何时间空隙，而是直接将进程杀死，无论进程当前在做何种操作。这种操作常常导致&ldquo;不一致&rdquo;状态的出现。SIGKILL这 个信号比较特殊，进程无法有效监听该信号，无法有效针对该信号设置handler，无法改变其信号的默认处理行为。</p>
<p><b>二、</b><b>测试用&ldquo;服务程序&rdquo;</b></p>
<p>为了测试docker容器对优雅退出的支持，我们编写如下&ldquo;服务程序&rdquo;用于放在docker容器中运行：</p>
<p><font face="Courier New">//dockerapp1.go</font></p>
<p><font face="Courier New">package main</font></p>
<p><font face="Courier New">import &quot;fmt&quot;<br />
	import &quot;time&quot;<br />
	import &quot;os&quot;<br />
	import &quot;os/signal&quot;<br />
	import &quot;syscall&quot;</font></p>
<p><font face="Courier New">type signalHandler func(s os.Signal, arg interface{})</font></p>
<p><font face="Courier New">type signalSet struct {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; m map[os.Signal]signalHandler<br />
	}</font></p>
<p><font face="Courier New">func signalSetNew() *signalSet {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ss := new(signalSet)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ss.m = make(map[os.Signal]signalHandler)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return ss<br />
	}</font></p>
<p><font face="Courier New">func (set *signalSet) register(s os.Signal, handler signalHandler) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if _, found := set.m[s]; !found {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; set.m[s] = handler<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	}</font></p>
<p><font face="Courier New">func (set *signalSet) handle(sig os.Signal, arg interface{}) (err error) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if _, found := set.m[sig]; found {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; set.m[sig](sig, arg)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return nil<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; } else {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return fmt.Errorf(&quot;No handler available for signal %v&quot;, sig)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; panic(&quot;won&#39;t reach here&quot;)<br />
	}</font></p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; go sysSignalHandleDemo()<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; time.Sleep(time.Hour) // make the main goroutine wait!<br />
	}</font></p>
<p><font face="Courier New">func sysSignalHandleDemo() {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ss := signalSetNew()<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; handler := func(s os.Signal, arg interface{}) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;handle signal: %v\n&quot;, s)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if s == syscall.SIGTERM {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;signal termiate received, app exit normally\n&quot;)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; os.Exit(0)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ss.register(syscall.SIGINT, handler)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ss.register(syscall.SIGUSR1, handler)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ss.register(syscall.SIGUSR2, handler)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ss.register(syscall.SIGTERM, handler)</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; for {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; c := make(chan os.Signal)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; var sigs []os.Signal<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; for sig := range ss.m {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; sigs = append(sigs, sig)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; signal.Notify(c)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; sig := &lt;-c</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; err := ss.handle(sig, nil)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;unknown signal received: %v, app exit unexpectedly\n&quot;, sig)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; os.Exit(1)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	}</font></p>
<p>关于<a href="http://tonybai.com/tag/golang">Go语言</a>对系统Signal的处理，可以参考《<a href="http://tonybai.com/2012/09/21/signal-handling-in-go/">Go中的系统Signal处理</a>》一文。</p>
<p><b>三、制作测试用docker image</b></p>
<p>在《 <a href="http://tonybai.com/2014/09/26/install-docker-on-ubuntu-server-1404/">Ubuntu Server 14.04安装docker</a>》一文中，我们完成了在ubuntu 14.04上安装docker的步骤。要制作测试用docker image，我们首先需要pull一个base image。我们以CentOS6.5为例：</p>
<p>在Ubuntu 14.04上执行：<br />
	&nbsp;&nbsp;&nbsp; <font face="Courier New">sudo&nbsp; docker pull centos:centos6</font></p>
<p>docker会自动从<a href="https://registry.hub.docker.com">官方仓库</a>下载一个制作好的docker image。下载成功后，我们可以run一下试试，像这样：</p>
<p><font face="Courier New">$&gt; sudo docker run -t -i centos:centos6 /bin/bash</font></p>
<p>我们查看一下CentOS6的小版本：<br />
	<font face="Courier New">$&gt; cat /etc/centos-release<br />
	CentOS release 6.5 (Final)</font></p>
<p>这是一个极其精简的CentOS，各种工具均未安装：<br />
	<font face="Courier New">bash-4.1# telnet<br />
	bash: telnet: command not found<br />
	bash-4.1# ssh<br />
	bash: ssh: command not found<br />
	bash-4.1# ftp<br />
	bash: ftp: command not found<br />
	bash-4.1# echo $PATH<br />
	/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin</font></p>
<p>如果你要安装一些必要的工具，可以直接使用yum install，默认的base image已经将yum配置好了，可以直接使用。如果通过公司代理访问外部网络，别忘了先export http_proxy。另外docker直接使用宿主机的/etc/resolv.conf作为容器的DNS，我们也无需额外设置DNS。</p>
<p>接下来，我们就制作我们的第一个测试用image。安装官方推荐的Best Practice，我们使用Dockerfile来bulid一个测试用image。步骤如下：</p>
<p>- 建立~/ImagesFactory目录<br />
	- 将构建好的dockerapp1拷贝到~/ImagesFactory目录下<br />
	- 进入~/ImagesFactory目录，创建Dockerfile文件，Dockerfile内容如下：</p>
<p><font face="Courier New">FROM centos:centos6<br />
	MAINTAINER Tony Bai <a class="moz-txt-link-rfc2396E" href="mailto:bigwhite.cn@gmail.com">&lt;bigwhite.cn@gmail.com&gt;</a><br />
	COPY ./dockerapp1 /bin<br />
	CMD /bin/dockerapp1</font></p>
<p>- 执行docker build，结果如下：</p>
<p><font face="Courier New">$ sudo docker build -t=&quot;test:v1&quot; ./<br />
	Sending build context to Docker daemon 7.496 MB<br />
	Sending build context to Docker daemon<br />
	Step 0 : FROM centos:centos6<br />
	&nbsp;&#8212;&gt; 68edf809afe7<br />
	Step 1 : MAINTAINER Tony Bai <a class="moz-txt-link-rfc2396E" href="mailto:bigwhite.cn@gmail.com">&lt;bigwhite.cn@gmail.com&gt;</a><br />
	&nbsp;&#8212;&gt; Using cache<br />
	&nbsp;&#8212;&gt; c617b456934a<br />
	Step 2 : COPY ./dockerapp1 /bin<br />
	2014/10/09 16:05:25 lchown /var/lib/docker/aufs/mnt/fb0e864d3f07ca17ef8b6b69f034728e1f1158fd3f9c83fa48243054b2f26958/bin/dockerapp1: not a directory</font></p>
<p>居然build失败，提示什么not a directory。于是各种Search，终于发现问题所在，原来是&ldquo;<font face="Courier New">COPY ./dockerapp1 /bin</font>&rdquo;这条命令错了，少了个&ldquo;/&rdquo;，将&quot; /bin&quot;改为&ldquo;/bin/&rdquo;就OK了，Docker真是奇怪啊，这块明显应该做得更兼容些。新的Dockerfile如下：</p>
<p><font face="Courier New">FROM centos:centos6<br />
	MAINTAINER Tony Bai <a class="moz-txt-link-rfc2396E" href="mailto:bigwhite.cn@gmail.com">&lt;bigwhite.cn@gmail.com&gt;</a><br />
	COPY ./dockerapp1 /bin/<br />
	CMD /bin/dockerapp1</font></p>
<p>构建结果如下：</p>
<p><font face="Courier New">$ sudo docker build -t=&quot;test:v1&quot; ./<br />
	Sending build context to Docker daemon 7.496 MB<br />
	Sending build context to Docker daemon<br />
	Step 0 : FROM centos:centos6<br />
	&nbsp;&#8212;&gt; 68edf809afe7<br />
	Step 1 : MAINTAINER Tony Bai <a class="moz-txt-link-rfc2396E" href="mailto:bigwhite.cn@gmail.com">&lt;bigwhite.cn@gmail.com&gt;</a><br />
	&nbsp;&#8212;&gt; Using cache<br />
	&nbsp;&#8212;&gt; c617b456934a<br />
	Step 2 : COPY ./dockerapp1 /bin/<br />
	&nbsp;&#8212;&gt; 20c3783c42ab<br />
	Removing intermediate container cab639ab4321<br />
	Step 3 : CMD /bin/dockerapp1<br />
	&nbsp;&#8212;&gt; Running in 31875d3c37f9<br />
	&nbsp;&#8212;&gt; 21a720a808a7<br />
	Removing intermediate container 31875d3c37f9<br />
	Successfully built 21a720a808a7</font></p>
<p><font face="Courier New">$ sudo docker images<br />
	REPOSITORY&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; TAG&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; IMAGE ID&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; CREATED&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; VIRTUAL SIZE<br />
	test&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; v1&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 21a720a808a7&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 59 seconds ago&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 214.6 MB</font></p>
<p><b>四、第一个测试容器</b></p>
<p>我们基于image &quot;test:v1&quot;启动一个测试容器：</p>
<p><font face="Courier New">$ sudo docker run -d &quot;test:v1&quot;<br />
	daf3ae88fec23a31cde9f6b9a3f40057953c87b56cca982143616f738a84dcba</font></p>
<p><font face="Courier New">$ sudo docker ps<br />
	CONTAINER ID&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; IMAGE&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; COMMAND&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; CREATED&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; STATUS&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; PORTS&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; NAMES<br />
	daf3ae88fec2&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; test:v1&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &quot;/bin/sh -c /bin/doc&nbsp;&nbsp; 17 seconds ago&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Up 16 seconds&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; condescending_sammet&nbsp;&nbsp; </font></p>
<p>通过docker run命令，我们基于image&quot;test:v1&quot;启动了一个容器。通过docker ps命令可以看到容器成功启动，容器id：<font face="Courier New">daf3ae88fec2，别名为：</font><font face="Courier New">condescending_sammet。</font></p>
<p><font face="Courier New">根据Dockerfile我们知道，容器启动后将执行&quot;/bin/dockerapp1&quot;这个程序，dockerapp1退出，容器即退出。 run命令的&quot;-d&quot;选项表示容器将以daemon的形式运行，我们在前台无法看到容器的输出。那么我们怎么查看容器的输出呢？我们可以通过 docker logs + 容器id的方式查看容器内应用的标准输出或标准错误。我们也可以进入容器来查看。</font></p>
<p><font face="Courier New">进入容器有多种方法，比如用sudo docker attach </font><font face="Courier New"><font face="Courier New">daf3ae88fec2</font>。attach后，就好比将daemon方式运行的容器 拿到了前台，你可以Ctrl + C一下，可以看到如下dockerapp1的输出:</font></p>
<p><font face="Courier New">^Chandle signal: interrupt</font></p>
<p><font face="Courier New">另外一种方式是利用nsenter工具进入我们容器的namespace空间。ubuntu 14.04下可以通过如下方式安装该工具：</font></p>
<p><font face="Courier New">$ wget <a class="moz-txt-link-freetext" href="https://www.kernel.org/pub/linux/utils/util-linux/v2.24/util-linux-2.24.tar.gz">https://www.kernel.org/pub/linux/utils/util-linux/v2.24/util-linux-2.24.tar.gz</a>; tar xzvf util-linux-2.24.tar.gz<br />
	$ cd util-linux-2.24<br />
	$ ./configure &#8211;without-ncurses &amp;&amp; make nsenter<br />
	$ sudo cp nsenter /usr/local/bin</font></p>
<p>安装后，我们通过如下方式即可进入上面的容器：</p>
<p><font face="Courier New">$ echo $(sudo docker inspect &#8211;format &quot;{{ .State.Pid }}&quot; daf3ae88fec2)<br />
	5494<br />
	$ sudo nsenter &#8211;target 5494 &#8211;mount &#8211;uts &#8211;ipc &#8211;net &#8211;pid<br />
	-bash-4.1# ps -ef<br />
	UID&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; PID&nbsp; PPID&nbsp; C STIME TTY&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; TIME CMD<br />
	root&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 1&nbsp;&nbsp;&nbsp;&nbsp; 0&nbsp; 0 09:20 ?&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 00:00:00 /bin/dockerapp1<br />
	root&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 16&nbsp;&nbsp;&nbsp;&nbsp; 0&nbsp; 0 09:32 ?&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 00:00:00 -bash<br />
	root&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 27&nbsp;&nbsp;&nbsp; 16&nbsp; 0 09:32 ?&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 00:00:00 ps -ef<br />
	-bash-4.1# </font></p>
<p>进入容器后通过ps命令可以看到正在运行的dockerapp1程序。在容器内，我们可以通过kill来测试dockerapp1的运行情况：</p>
<p><font face="Courier New">-bash-4.1# kill -s SIGINT 1</font></p>
<p>通过前面的attach窗口，我们可以看到dockerapp1输出:</p>
<p><font face="Courier New">handle signal: interrupt</font></p>
<p>如果你发送SIGTERM信号，那么dockerapp1将终止运行，容器也就停止了。</p>
<p><font face="Courier New">-bash-4.1# kill 1</font></p>
<p>attach窗口显示：</p>
<p><font face="Courier New">signal termiate received, app exit normally</font></p>
<p>我们可以看到容器启动后默认执行的时Dockerfile中的CMD命令，如果Dockerfile中有多行CMD命令，Docker在启动容器 时只会执行最后一条CMD命令。如果在docker run中指定了命令，docker则会执行命令行中的命令而不会执行dockerapp1，比如：</p>
<p><font face="Courier New">$ sudo docker run -t -i &quot;test:v1&quot; /bin/bash<br />
	bash-4.1# </font></p>
<p>这里我们看到直接执行的时bash，dockerapp1并未执行。</p>
<p><b>五、docker stop的行为</b></p>
<p>我们先来看看docker stop的manual：</p>
<p><font face="Courier New">$ sudo docker stop &#8211;help<br />
	Usage: docker stop [OPTIONS] CONTAINER [CONTAINER...]<br />
	Stop a running container by sending SIGTERM and then SIGKILL after a grace period<br />
	&nbsp; -t, &#8211;time=10&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Number of seconds to wait for the container to stop before killing it. Default is 10 seconds.</font></p>
<p>可以看出当我们执行docker stop时，docker会首先向容器内的当前主程序发送一个SIGTERM信号，用于容器内程序的退出。如果容器在收到SIGTERM后没有马上退出， 那么stop命令会在等待一段时间（默认是10s）后，再向容器发送SIGKILL信号，将容器杀死，变为退出状态。</p>
<p>我们来验证一下docker stop的行为。启动刚才那个容器：</p>
<p><font face="Courier New">$ sudo docker start daf3ae88fec2<br />
	daf3ae88fec2</font></p>
<p><font face="Courier New">attach到容器daf3ae88fec2<br />
	$ sudo docker attach daf3ae88fec2</font></p>
<p>新打开一个窗口，执行docker stop命令：<br />
	<font face="Courier New">$ sudo docker stop daf3ae88fec2<br />
	daf3ae88fec2</font></p>
<p>可以看到attach窗口输出：<br />
	<font face="Courier New">handle signal: terminated<br />
	signal termiate received, app exit normally</font></p>
<p>通过docker ps查看，发现容器已经退出。</p>
<p>也许通过上面的例子还不能直观的展示stop命令的<b>两阶段行为</b>，因为dockerapp1收到SIGTERM后直接就退出 了，stop命令无需等待容器慢慢退出，也无需发送SIGKILL。我们改造一下dockerapp1这个程序。</p>
<p>我们复制一下dockerapp1.go为dockerapp2.go，编辑dockerapp2.go，将handler中对SIGTERM的 处理注释掉，其他不变：</p>
<p><font face="Courier New">handler := func(s os.Signal, arg interface{}) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;handle signal: %v\n&quot;, s)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; /*<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if s == syscall.SIGTERM {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;signal termiate received, app exit normally\n&quot;)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; os.Exit(0)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; */<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</font></p>
<p>我们使用dockerapp2来构建一个新image：test:v2，将Dockerfile中得dockerapp1换成 dockerapp2即可。</p>
<p><font face="Courier New">$ sudo docker build -t=&quot;test:v2&quot; ./<br />
	Sending build context to Docker daemon 9.369 MB<br />
	Sending build context to Docker daemon<br />
	Step 0 : FROM centos:centos6<br />
	&nbsp;&#8212;&gt; 68edf809afe7<br />
	Step 1 : MAINTAINER Tony Bai <a class="moz-txt-link-rfc2396E" href="mailto:bigwhite.cn@gmail.com">&lt;bigwhite.cn@gmail.com&gt;</a><br />
	&nbsp;&#8212;&gt; Using cache<br />
	&nbsp;&#8212;&gt; c617b456934a<br />
	Step 2 : COPY ./dockerapp2 /bin/<br />
	&nbsp;&#8212;&gt; 27cd613a9bd7<br />
	Removing intermediate container 07c760b6223b<br />
	Step 3 : CMD /bin/dockerapp2<br />
	&nbsp;&#8212;&gt; Running in 1aac086452a7<br />
	&nbsp;&#8212;&gt; 82eb876fefd2<br />
	Removing intermediate container 1aac086452a7<br />
	Successfully built 82eb876fefd2</font></p>
<p>利用image &quot;test:v2&quot;创建一个容器来测试stop。</p>
<p><font face="Courier New">$ sudo docker run -d &quot;test:v2&quot;<br />
	29f3ec1af3c355458cbbd802a5e8a53da28e9f51a56ce822c7bba2a772edceac</font></p>
<p><font face="Courier New">$ sudo docker ps<br />
	CONTAINER ID&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; IMAGE&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; COMMAND&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; CREATED&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; STATUS&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; PORTS&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; NAMES<br />
	29f3ec1af3c3&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; test:v2&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &quot;/bin/sh -c /bin/doc&nbsp;&nbsp; 7 seconds ago&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Up 6 seconds&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; romantic_feynman&nbsp;</font>&nbsp;&nbsp;</p>
<p>Attach到这个容器并观察，在另外一个窗口stop该container。我们在attach窗口只看到如下输出：</p>
<p><font face="Courier New">handle signal: terminated</font></p>
<p>stop命令的执行没有立即返回，而是等待容器退出。等待10s后，容器退出，stop命令执行结束。从这个例子我们可以明显看出stop的两阶 段行为。</p>
<p>如果我们以<font face="Courier New">sudo docker run -i -t &quot;test:v1&quot; /bin/bash</font>形式启动容器，那stop命令会将SIGTERM发送给bash这个程序，即使你通过nsenter进入容 器，启动了dockerapp1，dockerapp1也不会收到SIGTERM，dockerapp1会随着容器的退出而被强行终止，就像被 kill -9了一样。</p>
<p><b>六、多进程容器服务</b>程序</p>
<p>上面无论是dockerapp1还是dockerapp2，都是一个单进程服务程序。如果我们在容器内执行一个多进程程序，我们该如何优雅退出 呢？我们先来编写一个多进程的服务程序dockerapp3：</p>
<p>在dockerapp1.go的基础上对main和sysSignalHandleDemo进行修改形成dockerapp3.go，修改后这两 个函数的代码如下：</p>
<p><font face="Courier New">//dockerapp3.go<br />
	&#8230; &#8230;</font></p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; go sysSignalHandleDemo()</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; pid, _, err := syscall.RawSyscall(syscall.SYS_FORK, 0, 0, 0)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if err != 0 {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;err fork process, err: %v\n&quot;, err)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if pid == 0 {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;i am in child process, pid = %v\n&quot;, syscall.Getpid())<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; time.Sleep(time.Hour) // make the child process wait<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;i am parent process, pid = %v\n&quot;, syscall.Getpid())<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;fork ok, childpid = %v\n&quot;, pid)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; time.Sleep(time.Hour) // make the main goroutine wait!<br />
	}</font></p>
<p><font face="Courier New">func sysSignalHandleDemo() {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ss := signalSetNew()<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; handler := func(s os.Signal, arg interface{}) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;%v: handle signal: %v\n&quot;, syscall.Getpid(), s)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if s == syscall.SIGTERM {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;%v: signal termiate received, app exit normally\n&quot;, syscall.Getpid())<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; os.Exit(0)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ss.register(syscall.SIGINT, handler)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ss.register(syscall.SIGUSR1, handler)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ss.register(syscall.SIGUSR2, handler)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ss.register(syscall.SIGTERM, handler)</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; for {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; c := make(chan os.Signal)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; var sigs []os.Signal<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; for sig := range ss.m {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; sigs = append(sigs, sig)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; signal.Notify(c)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; sig := &lt;-c</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; err := ss.handle(sig, nil)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;%v: unknown signal received: %v, app exit unexpectedly\n&quot;, syscall.Getpid(), sig)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; os.Exit(1)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	}</font></p>
<p>dockerapp3利用fork创建了一个子进程，这样dockerapp3实际上是两个进程在运行，各自有自己的signal监听 goroutine，goroutine的处理逻辑是相同的。注意：由于Windows和Mac OS X不具备fork语义，因此在这两个平台上运行dockerapp3不会得到预期结果。</p>
<p>利用dockerapp3，我们创建image &quot;test:v3&quot;:</p>
<p><font face="Courier New">$ sudo docker build -t=&quot;test:v3&quot; ./<br />
	[sudo] password for tonybai:<br />
	Sending build context to Docker daemon 11.24 MB<br />
	Sending build context to Docker daemon<br />
	Step 0 : FROM centos:centos6<br />
	&nbsp;&#8212;&gt; 68edf809afe7<br />
	Step 1 : MAINTAINER Tony Bai <a class="moz-txt-link-rfc2396E" href="mailto:bigwhite.cn@gmail.com">&lt;bigwhite.cn@gmail.com&gt;</a><br />
	&nbsp;&#8212;&gt; Using cache<br />
	&nbsp;&#8212;&gt; c617b456934a<br />
	Step 2 : COPY ./dockerapp3 /bin/<br />
	&nbsp;&#8212;&gt; 6ccf97065853<br />
	Removing intermediate container 6d85fe241939<br />
	Step 3 : CMD /bin/dockerapp3<br />
	&nbsp;&#8212;&gt; Running in 75d76380992a<br />
	&nbsp;&#8212;&gt; c9e7bf361ed7<br />
	Removing intermediate container 75d76380992a<br />
	Successfully built c9e7bf361ed7</font></p>
<p>启动基于test:v3 image的容器：</p>
<p><font face="Courier New">$ sudo docker run -d &quot;test:v3&quot;<br />
	781cecb4b3628cb33e1b104ea57e506ad5cb4a44243256ebd1192af86834bae6<br />
	$ sudo docker ps<br />
	CONTAINER ID&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; IMAGE&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; COMMAND&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; CREATED&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; STATUS&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; PORTS&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; NAMES<br />
	781cecb4b362&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; test:v3&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &quot;/bin/sh -c /bin/doc&nbsp;&nbsp; 5 seconds ago&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Up 4 seconds&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; insane_bohr&nbsp;&nbsp;&nbsp;</font>&nbsp;&nbsp;&nbsp;</p>
<p>通过docker logs查看dockerapp3的输出：</p>
<p><font face="Courier New">$ sudo docker logs 781cecb4b362<br />
	i am parent process, pid = 1<br />
	fork ok, childpid = 13<br />
	i am in child process, pid = 13</font></p>
<p>可以看出主进程pid为1，子进程pid为13。我们通过stop停止该容器：</p>
<p><font face="Courier New">$ sudo docker stop 781cecb4b362<br />
	781cecb4b362</font></p>
<p>再次通过docker logs查看：</p>
<p><font face="Courier New">$ sudo docker logs 781cecb4b362<br />
	i am parent process, pid = 1<br />
	fork ok, childpid = 13<br />
	i am in child process, pid = 13<br />
	1: handle signal: terminated<br />
	1: signal termiate received, app exit normally</font></p>
<p>我们可以看到主进程收到了stop发来的SIGTERM并退出，主进程的退出导致容器退出，导致子进程13也无法生存，并且没有优雅退出。而在非 容器状态下，子进程是可以被init进程接管的。</p>
<p>因此对于docker容器内运行的多进程程序，stop命令只会将SIGTERM发送给容器主进程，要想让其他进程也能优雅退出，需要在主进程与 其他进程间建立一种通信机制。在主进程退出前，等待其他子进程退出。待所有其他进程退出后，主进程再退出，容器停止。这样才能保证服务程序的优雅 退出。</p>
<p><b>七、容器内启动多个服务程序</b></p>
<p>虽说docker <a href="https://docs.docker.com/articles/dockerfile_best-practices/">best practice</a>建议一个container内只放置一个服务程序，但对已有的一些遗留系统，在架构没有做出重构之前，很可能会有在一个 container中部署两个以上服务程序的情况和需求。而docker Dockerfile只允许执行一个CMD，这种情况下，我们就需要借助类似supervisor这样的进程监控管理程序来启动和管理container 内的多个程序了。</p>
<p>下面我们来自制作一个基于centos:centos6的安装了supervisord以及两个服务程序的image。我们将dockerapp1拷贝一份，并将拷贝命名为dockerapp1-brother。下面是我们的Dockerfile：</p>
<p><font face="Courier New">FROM centos:centos6<br />
	MAINTAINER Tony Bai &lt;bigwhite.cn@gmail.com&gt;<br />
	RUN yum install python-setuptools -y<br />
	RUN easy_install supervisor<br />
	RUN mkdir -p /var/log/supervisor<br />
	COPY ./supervisord.conf /etc/supervisord.conf<br />
	COPY ./dockerapp1 /bin/<br />
	COPY ./dockerapp1-brother /bin/<br />
	CMD ["/usr/bin/supervisord"]</font></p>
<p>supervisord的配置文件supervisord.conf内容如下：</p>
<p><font face="Courier New">; supervisor config file</font></p>
<p><font face="Courier New">[unix_http_server]<br />
	file=/var/run/supervisor.sock&nbsp;&nbsp; ; (the path to the socket file)<br />
	chmod=0700&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ; sockef file mode (default 0700)</font></p>
<p><font face="Courier New">[supervisord]<br />
	logfile=/var/log/supervisor/supervisord.log ; (main log file;default $CWD/supervisord.log)<br />
	pidfile=/var/run/supervisord.pid ; (supervisord pidfile;default supervisord.pid)<br />
	childlogdir=/var/log/supervisor&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ; (&#39;AUTO&#39; child log dir, default $TEMP)</font></p>
<p><font face="Courier New">[rpcinterface:supervisor]<br />
	supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface</font></p>
<p><font face="Courier New">[supervisorctl]<br />
	serverurl=unix:///var/run/supervisor.sock ; use a unix:// URL&nbsp; for a unix socket</font></p>
<p><font face="Courier New">[supervisord]<br />
	nodaemon=false</font></p>
<p><font face="Courier New">[program:dockerapp1]<br />
	command=/bin/dockerapp1<br />
	stdout_logfile=/tmp/dockerapp1.log<br />
	stopsignal=TERM<br />
	stopwaitsecs=10</font></p>
<p><font face="Courier New">[program:dockerapp1-brother]<br />
	command=/bin/dockerapp1-brother<br />
	stdout_logfile=/tmp/dockerapp1-brother.log<br />
	stopsignal=QUIT<br />
	stopwaitsecs=10</font></p>
<p>开始build镜像：<br />
	&nbsp;&nbsp;&nbsp; <font face="Courier New">$&gt; sudo docker build -t=&quot;test:supervisor-v1&quot; ./<br />
	&nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	&nbsp;&nbsp;&nbsp; Successfully built d006b9ad10eb</font></p>
<p>基于该镜像，启动一个容器：<br />
	<font face="Courier New">$&gt; sudo docker run -d &quot;test:supervisor-v1&quot;<br />
	05ded2b898c90059d4c9b5c6ccc8603b6848ae767360c42bd9b36ff87fb4b9df</font></p>
<p>执行ps命令查看镜像id：<br />
	<font face="Courier New">$ sudo docker ps<br />
	CONTAINER ID&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; IMAGE&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; COMMAND&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; CREATED&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; STATUS&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; PORTS&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; NAMES</font></p>
<p>怎么回事？Container没有启动起来？</p>
<p><font face="Courier New">$ sudo docker ps -a<br />
	CONTAINER ID&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; IMAGE&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; COMMAND&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; CREATED&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; STATUS&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; PORTS&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; NAMES<br />
	05ded2b898c9&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; test:supervisor-v1&nbsp;&nbsp;&nbsp; &quot;/usr/bin/supervisor&nbsp;&nbsp; 22 seconds ago&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Exited (0) 21 seconds ago&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; hungry_engelbart</font></p>
<p>通过ps -a查看，container启动是成功了，但是成功退出了。于是尝试查看一下log：</p>
<p><font face="Courier New">sudo docker logs 05ded2b898c9<br />
	/usr/lib/python2.6/site-packages/supervisor-3.1.2-py2.6.egg/supervisor/options.py:296: UserWarning: Supervisord is running as root and it is searching for its configuration file in default locations (including its current working directory); you probably want to specify a &quot;-c&quot; argument specifying an absolute path to a configuration file for improved security.<br />
	&nbsp; &#39;Supervisord is running as root and it is searching &#39;</font></p>
<p>似乎是supervisord转为daemon程序，容器主进程退出了，容器随之终止了。</p>
<p>看来容器内的supervisord不能以daemon形式运行，应该以前台形式run。修改一下supervisord.conf中得配置：</p>
<p>将<br />
	<font face="Courier New">[supervisord]<br />
	nodaemon=false</font></p>
<p>改为</p>
<p><font face="Courier New">[supervisord]<br />
	nodaemon=true</font></p>
<p>重新制作镜像:</p>
<p><font face="Courier New">$ sudo docker build -t=&quot;test:supervisor-v2&quot; ./<br />
	Sending build context to Docker daemon 13.12 MB<br />
	Sending build context to Docker daemon<br />
	Step 0 : FROM centos:centos6<br />
	&nbsp;&#8212;&gt; 68edf809afe7<br />
	Step 1 : MAINTAINER Tony Bai &lt;bigwhite.cn@gmail.com&gt;<br />
	&nbsp;&#8212;&gt; Using cache<br />
	&nbsp;&#8212;&gt; c617b456934a<br />
	Step 2 : RUN yum install python-setuptools -y<br />
	&nbsp;&#8212;&gt; Using cache<br />
	&nbsp;&#8212;&gt; e09c66a1ea8c<br />
	Step 3 : RUN easy_install supervisor<br />
	&nbsp;&#8212;&gt; Using cache<br />
	&nbsp;&#8212;&gt; 9c8797e8c27e<br />
	Step 4 : RUN mkdir -p /var/log/supervisor<br />
	&nbsp;&#8212;&gt; Using cache<br />
	&nbsp;&#8212;&gt; 9bfc67f8517d<br />
	Step 5 : COPY ./supervisord.conf /etc/supervisord.conf<br />
	&nbsp;&#8212;&gt; 8c514f998363<br />
	Removing intermediate container 4a185856e6ed<br />
	Step 6 : COPY ./dockerapp1 /bin/<br />
	&nbsp;&#8212;&gt; 0317bd4914d3<br />
	Removing intermediate container ac5738380854<br />
	Step 7 : COPY ./dockerapp1-brother /bin/<br />
	&nbsp;&#8212;&gt; d89711888bdf<br />
	Removing intermediate container eadc9444e716<br />
	Step 8 : CMD ["/usr/bin/supervisord"]<br />
	&nbsp;&#8212;&gt; Running in aaa042ac3914<br />
	&nbsp;&#8212;&gt; 9655256bbfed<br />
	Removing intermediate container aaa042ac3914<br />
	Successfully built 9655256bbfed</font></p>
<p>有了前面的铺垫，这次build image瞬间完成。启动容器，查看容器启动状态，查看容器内supervisord的运行日志如下：</p>
<p><font face="Courier New">$ sudo docker run -d &quot;test:supervisor-v2&quot;<br />
	61916f1c82338b28ced101b6bde119e4afb7c7fa349b4332ed51a43a4586b1b9</font></p>
<p><font face="Courier New">$ sudo docker ps<br />
	CONTAINER ID&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; IMAGE&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; COMMAND&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; CREATED&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; STATUS&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; PORTS&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; NAMES<br />
	61916f1c8233&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; test:supervisor-v2&nbsp;&nbsp; &quot;/usr/bin/supervisor&nbsp;&nbsp; 16 seconds ago&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Up 16 seconds&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; prickly_einstein</font></p>
<p><font face="Courier New">$ sudo docker logs 8eb3e9892e66</font></p>
<p><font face="Courier New">/usr/lib/python2.6/site-packages/supervisor-3.1.2-py2.6.egg/supervisor/options.py:296: UserWarning: Supervisord is running as root and it is searching for its configuration file in default locations (including its current working directory); you probably want to specify a &quot;-c&quot; argument specifying an absolute path to a configuration file for improved security.<br />
	&nbsp; &#39;Supervisord is running as root and it is searching &#39;<br />
	2014-10-09 14:36:02,334 CRIT Supervisor running as root (no user in config file)<br />
	2014-10-09 14:36:02,349 INFO RPC interface &#39;supervisor&#39; initialized<br />
	2014-10-09 14:36:02,349 CRIT Server &#39;unix_http_server&#39; running without any HTTP authentication checking<br />
	2014-10-09 14:36:02,349 INFO supervisord started with pid 1<br />
	2014-10-09 14:36:03,354 INFO spawned: &#39;dockerapp1&#39; with pid 14<br />
	2014-10-09 14:36:03,363 INFO spawned: &#39;dockerapp1-brother&#39; with pid 15<br />
	2014-10-09 14:36:04,368 INFO success: dockerapp1 entered RUNNING state, process has stayed up for &gt; than 1 seconds (startsecs)<br />
	2014-10-09 14:36:04,369 INFO success: dockerapp1-brother entered RUNNING state, process has stayed up for &gt; than 1 seconds (startsecs)</font></p>
<p>可以看到supervisord已经将dockerapp1和dockerapp1-brother启动起来了。</p>
<p>现在我们尝试停止容器，我们预期是supervisord在退出前通知dockerapp1和dockerapp1-brother先退出，我们可以通过 查看容器内的/tmp/dockerapp1.log和/tmp/dockerapp1-brother.log来确认supervisord是否做了通 知。</p>
<p><font face="Courier New">$ sudo docker stop 61916f1c8233<br />
	61916f1c8233</font></p>
<p><font face="Courier New">$ sudo docker logs 61916f1c8233<br />
	&#8230; &#8230;<br />
	2014-10-09 14:37:52,253 WARN received SIGTERM indicating exit request<br />
	2014-10-09 14:37:52,254 INFO waiting for dockerapp1, dockerapp1-brother to die<br />
	2014-10-09 14:37:52,254 INFO stopped: dockerapp1-brother (exit status 0)<br />
	2014-10-09 14:37:52,256 INFO stopped: dockerapp1 (exit status 0)</font></p>
<p>通过容器的log，我们看出supervisord是等待两个程序退出后才退出的，不过我们还是要看看两个程序的输出日志以最终确认。重新启动容器，通过nsenter进入到容器中。</p>
<p><font face="Courier New">-bash-4.1# vi /tmp/dockerapp1.log</font></p>
<p><font face="Courier New">handle signal: terminated<br />
	signal termiate received, app exit normally</font></p>
<p><font face="Courier New">-bash-4.1# vi /tmp/dockerapp1-brother.log</font></p>
<p><font face="Courier New">handle signal: terminated<br />
	signal termiate received, app exit normally</font></p>
<p>两个程序的标准输出日志证实了我们的预期。</p>
<p>BTW，在物理机上测试supervisord以daemon形式运行，当kill掉supervisord时，supervisord是不会通知其监控 和管理的程序退出的。只有在以non-daemon形式运行时，supervisord才会在退出前先通知下面的程序退出。如果在一段时间内下面程序没有 退出，supervisord在退出前会kill -9强制杀死这些程序的进程。</p>
<p>最后要说的时，在验证一些想法时，没有必要build image，我们可以直接将本地文件copy到容器中，下面是一个例子，我们将dockerapp1和dockerapp1-brother拷贝到镜像中：<br />
	<font face="Courier New">$ sudo docker ps<br />
	CONTAINER ID&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; IMAGE&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; COMMAND&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; CREATED&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; STATUS&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; PORTS&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; NAMES<br />
	4d8982bfccc7&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; centos:centos6&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &quot;/bin/bash&quot;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 26 minutes ago&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Up 26 minutes&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; sharp_thompson&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br />
	$ sudo docker inspect -f &#39;{{.Id}}&#39; 4d8982bfccc7<br />
	4d8982bfccc79dea762b41f8a6f669bda1ec73c8881b6ca76e7a7917c62972c4<br />
	$ sudo cp dockerapp1&nbsp; /var/lib/docker/aufs/mnt/4d8982bfccc79dea762b41f8a6f669bda1ec73c8881b6ca76e7a7917c62972c4/bin/dockerapp1<br />
	$ sudo cp dockerapp1-brother&nbsp; /var/lib/docker/aufs/mnt/4d8982bfccc79dea762b41f8a6f669bda1ec73c8881b6ca76e7a7917c62972c4/bin/dockerapp1-brother</font></p>
<p style='text-align:left'>&copy; 2014, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2014/10/09/gracefully-shutdown-app-running-in-docker/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>libiconv库链接问题一则</title>
		<link>https://tonybai.com/2013/04/25/a-libiconv-linkage-problem/</link>
		<comments>https://tonybai.com/2013/04/25/a-libiconv-linkage-problem/#comments</comments>
		<pubDate>Thu, 25 Apr 2013 10:04:34 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[GCC]]></category>
		<category><![CDATA[Glibc]]></category>
		<category><![CDATA[GNU]]></category>
		<category><![CDATA[iconv]]></category>
		<category><![CDATA[ld]]></category>
		<category><![CDATA[libc]]></category>
		<category><![CDATA[libiconv]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[Redhat]]></category>
		<category><![CDATA[Solaris]]></category>
		<category><![CDATA[Unix]]></category>
		<category><![CDATA[博客]]></category>
		<category><![CDATA[工作]]></category>
		<category><![CDATA[开源]]></category>
		<category><![CDATA[程序员]]></category>
		<category><![CDATA[编译器]]></category>
		<category><![CDATA[链接]]></category>
		<category><![CDATA[链接器]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=1258</guid>
		<description><![CDATA[与在Solaris系统上不同，Linux的libc库中包含了libiconv库中函数的定义，因此在Linux上使用libiconv库相关函数，编译时是不需要显式-liconv的。但最近我的一位同事在某redhat enterprise server 5.6机器上编译程序时却遇到了找不到iconv库函数符号的链接问题，到底是怎样一回事呢？这里分享一下问题查找过程。 一、现场重现 这里借用一下这位同事的测试程序以及那台机器，重现一下问题过程： /*test.c */ &#8230; #include &#60;iconv.h&#62; int main(void) { &#160;&#160;&#160; int r; &#160;&#160;&#160; char *sin, *sout; &#160;&#160;&#160; size_t lenin, lenout; &#160;&#160;&#160; char *src = &#34;你好!&#34;; &#160;&#160;&#160; char dst[256] = {0}; &#160;&#160;&#160; iconv_t c_pt;&#160;&#160; &#160;&#160;&#160; sin = src; &#160;&#160;&#160; lenin = strlen(src)+1; &#160;&#160;&#160; sout = dst; &#160;&#160;&#160; lenout = 256; &#160;&#160;&#160; [...]]]></description>
			<content:encoded><![CDATA[<p>与在<a href="http://tonybai.com/2009/11/05/a-64bit-compiling-problem-on-x86-solaris/">Solaris</a>系统上不同，<a href="http://tonybai.com/2012/12/04/upgrade-ubuntu-to-1204-lts/">Linux</a>的libc库中包含了<a href="http://tonybai.com/2009/10/31/internal-code-transform-by-iconv/">libiconv</a>库中函数的定义，因此在Linux上使用libiconv库相关函数，编译时是不需要显式-liconv的。但最近我的一位同事在某redhat enterprise server 5.6机器上编译程序时却遇到了找不到iconv库函数符号的<a href="http://tonybai.com/2007/12/08/those-things-about-symbol-linkage/">链接问题</a>，到底是怎样一回事呢？这里分享一下问题查找过程。</p>
<p><b>一、现场重现</b></p>
<p>这里借用一下这位同事的测试程序以及那台机器，重现一下问题过程：<br />
	/*test.c */</p>
<p>&#8230;<br />
	<font face="Courier New">#include &lt;iconv.h&gt;</font></p>
<p><font face="Courier New">int main(void)<br />
	{<br />
	&nbsp;&nbsp;&nbsp; int r;<br />
	&nbsp;&nbsp;&nbsp; char *sin, *sout;<br />
	&nbsp;&nbsp;&nbsp; size_t lenin, lenout;<br />
	&nbsp;&nbsp;&nbsp; char *src = &quot;你好!&quot;;<br />
	&nbsp;&nbsp;&nbsp; char dst[256] = {0};<br />
	&nbsp;&nbsp;&nbsp; iconv_t c_pt;&nbsp;&nbsp;</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; sin = src;<br />
	&nbsp;&nbsp;&nbsp; lenin = strlen(src)+1;</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; sout = dst;<br />
	&nbsp;&nbsp;&nbsp; lenout = 256;</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; if ((c_pt = iconv_open(&quot;UTF-8&quot;, &quot;GB2312&quot;)) == (iconv_t)(-1)){<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; printf(&quot;iconv_open error!. errno[%d].\n&quot;, errno);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return -1;<br />
	&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; if ((r = iconv(c_pt, (char **)&amp;sin, &amp;lenin, &amp;sout, &amp;lenout)) != 0){<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; printf(&quot;iconv error!. errno[%d].\n&quot;, r);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return -1;<br />
	&nbsp;&nbsp;&nbsp; }&nbsp;&nbsp;</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; iconv_close(c_pt);</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; printf(&quot;SRC[%s], DST[%s].\n&quot;, src, dst);</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; return 0;<br />
	}</font></p>
<p>根据之前的经验，我们按如下命令编译该程序：</p>
<p><font face="Courier New">$&gt; gcc -g -o test test.c</font></p>
<p><font face="Courier New">/tmp/ccyQ5blC.o: In function `main&#39;:<br />
	/home/tonybai/tmp/test.c:28: undefined reference to `libiconv_open&#39;<br />
	/home/tonybai/tmp/test.c:33: undefined reference to `libiconv&#39;<br />
	/home/tonybai/tmp/test.c:38: undefined reference to `libiconv_close&#39;</font></p>
<p>咦，这是咋搞的呢？怎么找不到iconv库的符号！！！显式加上iconv的链接指示再试试。</p>
<p><font face="Courier New">$&gt; gcc -g -o test test.c -liconv</font></p>
<p>这回编译OK了。的确如那位同事所说出现了怪异的情况。</p>
<p><b>二、现场取证</b></p>
<p>惯性思维让我<b>首先</b>提出疑问：难道是这台机器上的<a href="http://www.gnu.org/s/libc/">libc</a>版本有差异，检查一下<a href="http://tonybai.com/2009/04/11/glibc-strlen-source-analysis/">libc</a>中是否定义了iconv相关符号。</p>
<p><font face="Courier New">$ nm /lib64/libc.so.6 |grep iconv<br />
	000000397141e040 T iconv<br />
	000000397141e1e0 T iconv_close<br />
	000000397141ddc0 T iconv_open</font></p>
<p>iconv的函数都定义了呀！怎么会链接不到？</p>
<p>我们<b>再来</b>看看已经编译成功的那个test到底连接到哪个iconv库了。</p>
<p><font face="Courier New">$ ldd test<br />
	&nbsp;&nbsp;&nbsp; linux-vdso.so.1 =&gt;&nbsp; (0x00007fff77d6b000)<br />
	&nbsp;&nbsp;&nbsp; libiconv.so.2 =&gt; /usr/local/lib/libiconv.so.2 (0x00002abbeb09e000)<br />
	&nbsp;&nbsp;&nbsp; libc.so.6 =&gt; /lib64/libc.so.6 (0&#215;0000003971400000)<br />
	&nbsp;&nbsp;&nbsp; /lib64/ld-linux-x86-64.so.2 (0&#215;0000003971000000)</font></p>
<p>哦，系统里居然在/usr/local/lib下面单独安装了一份libiconv。gcc显然是链接到这里的libiconv了，但gcc怎么会链接到这里了呢？</p>
<p><b>三、</b><b>大侦探的分析^_^</b></p>
<p><a href="http://tonybai.com/2006/03/14/explain-gcc-warning-options-by-examples/">Gcc</a>到底做了什么呢？我们看看其verbose的输出结果。</p>
<p><font face="Courier New">$ gcc -g -o test test.c -liconv -v<br />
	使用内建 specs。<br />
	目标：x86_64-redhat-linux<br />
	配置为：../configure &#8211;prefix=/usr &#8211;mandir=/usr/share/man &#8211;infodir=/usr/share/info &#8211;enable-shared &#8211;enable-threads=posix &#8211;enable-&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; checking=release &#8211;with-system-zlib &#8211;enable-__cxa_atexit &#8211;disable-libunwind-exceptions &#8211;enable-libgcj-multifile &#8211;enable-languages=c,c++,&nbsp;&nbsp; objc,obj-c++,java,fortran,ada &#8211;enable-java-awt=gtk &#8211;disable-dssi &#8211;disable-plugin &#8211;with-java-home=/usr/lib/jvm/java-1.4.2-gcj-1.4.2.0/jre &#8211;with-cpu=generic &#8211;host=x86_64-redhat-linux<br />
	线程模型：posix<br />
	gcc 版本 4.1.2 20080704 (Red Hat 4.1.2-50)<br />
	&nbsp;/usr/libexec/gcc/x86_64-redhat-linux/4.1.2/cc1 -quiet -v test.c -quiet -dumpbase test.c -mtune=generic -auxbase test -g -version -o /tmp/&nbsp;&nbsp;&nbsp;&nbsp; ccypZm0v.s<br />
	忽略不存在的目录&ldquo;/usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../x86_64-redhat-linux/include&rdquo;<br />
	#include &quot;&#8230;&quot; 搜索从这里开始：<br />
	#include &lt;&#8230;&gt; 搜索从这里开始：<br />
	&nbsp;/usr/local/include<br />
	&nbsp;/usr/lib/gcc/x86_64-redhat-linux/4.1.2/include<br />
	&nbsp;/usr/include<br />
	搜索列表结束。<br />
	GNU C 版本 4.1.2 20080704 (Red Hat 4.1.2-50) (x86_64-redhat-linux)<br />
	&nbsp;&nbsp;&nbsp; 由 GNU C 版本 4.1.2 20080704 (Red Hat 4.1.2-50) 编译。<br />
	GGC 准则：&#8211;param ggc-min-expand=100 &#8211;param ggc-min-heapsize=131072<br />
	Compiler executable checksum: ef754737661c9c384f73674bd4e06594<br />
	&nbsp;as -V -Qy -o /tmp/ccaqvDgX.o /tmp/ccypZm0v.s<br />
	GNU assembler version 2.17.50.0.6-14.el5 (x86_64-redhat-linux) using BFD version 2.17.50.0.6-14.el5 20061020<br />
	&nbsp;/usr/libexec/gcc/x86_64-redhat-linux/4.1.2/collect2 &#8211;eh-frame-hdr -m elf_x86_64 &#8211;hash-style=gnu -dynamic-linker /lib64/ld-linux-x86-64.so.&nbsp; 2 -o test /usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../lib64/crt1.o /usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../lib64/crti.o /usr/&nbsp;&nbsp; lib/gcc/x86_64-redhat-linux/4.1.2/crtbegin.o -L/usr/lib/gcc/x86_64-redhat-linux/4.1.2 -L/usr/lib/gcc/x86_64-redhat-linux/4.1.2 -L/usr/lib/gcc/ x86_64-redhat-linux/4.1.2/../../../../lib64 -L/lib/../lib64<br />
	-L/usr/lib/../lib64 /tmp/ccaqvDgX.o -liconv -lgcc &#8211;as-needed -lgcc_s &#8211;no-as-needed -lc -lgcc &#8211;as-needed -lgcc_s &#8211;no-as-needed /usr/lib/gcc/x86_64-redhat-linux/4.1.2/crtend.o /usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../lib64/crtn.o</font></p>
<p>从这个结果来看，gcc在search iconv.h这个头文件时，首先找到的是/usr/local/include/iconv.h，而不是/usr/include/iconv.h。这两个文件有啥不同么？</p>
<p>在/usr/local/include/iconv.h中，我找到如下代码：</p>
<p><font face="Courier New">&#8230;</font><br />
	<font face="Courier New">#ifndef LIBICONV_PLUG<br />
	#define iconv_open libiconv_open<br />
	#endif<br />
	extern iconv_t iconv_open (const char* tocode, const char* fromcode);<br />
	&#8230;</font></p>
<p>libiconv_open vs iconv_open，卧槽！！！再对比一下前面编译时输出的错误信息：</p>
<p><font face="Courier New">/tmp/ccyQ5blC.o: In function `main&#39;:<br />
	/home/tonybai/tmp/test.c:28: undefined reference to `libiconv_open&#39;<br />
	/home/tonybai/tmp/test.c:33: undefined reference to `libiconv&#39;<br />
	/home/tonybai/tmp/test.c:38: undefined reference to `libiconv_close&#39;</font></p>
<p>大侦探醒悟了！大侦探带你还原一下真实情况。</p>
<p>我们在执行<font face="Courier New">gcc -g -o test test.c</font>时， 根据gcc -v中include search dir的顺序，gcc首先search到的是/usr/local/include/iconv.h，而这里iconv_open等函数被预编译器替换成 了libiconv_open等加上了lib前缀的函数，而这些函数符号显然在libc中是无法找到的，libc中只有不带lib前缀的 iconv_open等函数的定义。大侦探也是一时眼拙了，没有细致查看gcc的编译错误信息中的内容，这就是问题所在！</p>
<p>而<font face="Courier New">gcc -g -o test test.c -liconv</font>为何可以顺利编译通过呢？gcc是如何找到/usr/local/lib下的libiconv的呢？大侦探再次为大家还原一下真相。</p>
<p>我们在执行<font face="Courier New">gcc -g -o test test.c -liconv</font>时，gcc同 样首先search到的是/usr/local/include/iconv.h，然后编译test.c源码，ok；接下来启动ld程序进行链接；ld找 到了libiconv，ld是怎么找到iconv的呢，libiconv在/usr/local/lib下，ld显然是到这个目录下search了。我们 通过执行下面命令可以知晓ld的默认搜索路径：</p>
<p><font face="Courier New">$&gt; ld -verbose|grep SEARCH<br />
	SEARCH_DIR(&quot;/usr/x86_64-redhat-linux/lib64&quot;); SEARCH_DIR(&quot;/usr/local/lib64&quot;); SEARCH_DIR(&quot;/lib64&quot;); SEARCH_DIR(&quot;/usr/lib64&quot;); SEARCH_DIR(&quot;/usr/x86_64-redhat-linux/lib&quot;); SEARCH_DIR(&quot;/usr/lib64&quot;); SEARCH_DIR(&quot;/usr/local/lib&quot;); SEARCH_DIR(&quot;/lib&quot;); SEARCH_DIR(&quot;/usr/lib&quot;);</font></p>
<p>ld的默认search路径中有/usr/local/lib(我之前一直是以为/usr/local/lib不是gcc/ld的默认搜索路径的)，因此找到libiconv就不足为奇了。</p>
<p><b>四、问题解决</b></p>
<p>我们不想显式的加上-liconv，那如何解决这个问题呢？我们是否可以强制gcc先找到/usr/include/iconv.h呢？我们先来做个试验。</p>
<p><font face="Courier New">$ gcc -g -o test test.c -liconv -I ~/include -v<br />
	&#8230;<br />
	忽略不存在的目录&ldquo;/usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../x86_64-redhat-linux/include&rdquo;<br />
	#include &quot;&#8230;&quot; 搜索从这里开始：<br />
	#include &lt;&#8230;&gt; 搜索从这里开始：<br />
	&nbsp;<b>/home/</b><b>tonybai</b><b>/include</b><br />
	&nbsp;/usr/local/include<br />
	&nbsp;/usr/lib/gcc/x86_64-redhat-linux/4.1.2/include<br />
	&nbsp;/usr/include<br />
	搜索列表结束。</font></p>
<p><font face="Courier New">&#8230;</font></p>
<p>试验结果似乎让我们觉得可行，我们通过-I指定的路径被放在了第一的位置进行search。我们来尝试一下强制gcc先search /usr/include。</p>
<p><font face="Courier New">$ gcc -g -o test test.c -I ~/include -v<br />
	&#8230;<br />
	忽略不存在的目录&ldquo;/usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../x86_64-redhat-linux/include&rdquo;<br />
	忽略重复的目录&ldquo;/usr/include&rdquo;<br />
	&nbsp; 因为它是一个重复了系统目录的非系统目录<br />
	#include &quot;&#8230;&quot; 搜索从这里开始：<br />
	#include &lt;&#8230;&gt; 搜索从这里开始：<br />
	&nbsp;/usr/local/include<br />
	&nbsp;/usr/lib/gcc/x86_64-redhat-linux/4.1.2/include<br />
	&nbsp;/usr/include<br />
	搜索列表结束。<br />
	&#8230;</font></p>
<p>糟糕！/usr/include被忽略了！还是从/usr/local/include开始，方案失败。</p>
<p>似乎剩下的唯一方案就是将/usr/local/lib下的那份libiconv卸载掉！那就这么做吧^_^！</p>
<p style='text-align:left'>&copy; 2013, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2013/04/25/a-libiconv-linkage-problem/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>Go中的系统Signal处理</title>
		<link>https://tonybai.com/2012/09/21/signal-handling-in-go/</link>
		<comments>https://tonybai.com/2012/09/21/signal-handling-in-go/#comments</comments>
		<pubDate>Fri, 21 Sep 2012 08:56:41 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[Daemon]]></category>
		<category><![CDATA[Debian]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Google]]></category>
		<category><![CDATA[Interface]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[map]]></category>
		<category><![CDATA[Opensource]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[Redhat]]></category>
		<category><![CDATA[Signal]]></category>
		<category><![CDATA[Slice]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[Unix]]></category>
		<category><![CDATA[信号]]></category>
		<category><![CDATA[切片]]></category>
		<category><![CDATA[博客]]></category>
		<category><![CDATA[开源]]></category>
		<category><![CDATA[接口]]></category>
		<category><![CDATA[程序员]]></category>
		<category><![CDATA[结构体]]></category>
		<category><![CDATA[编程，语言]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=1046</guid>
		<description><![CDATA[我们在生产环境下运行的系统要求优雅退出，即程序接收退出通知后，会有机会先执行一段清理代码，将收尾工作做完后再真正退出。我们采用系统Signal来 通知系统退出，即kill pragram-pid。我们在程序中针对一些系统信号设置了处理函数，当收到信号后，会执行相关清理程序或通知各个子进程做自清理。kill -9强制杀掉程序是不能被接受的，那样会导致某些处理过程被强制中断，留下无法恢复的现场，导致消息被破坏，影响下次系统启动运行。 最近用Golang实现的一个代理程序也需要优雅退出，因此我尝试了解了一下Golang中对系统Signal的处理方式，这里和大家分享。Golang 的系统信号处理主要涉及os包、os.signal包以及syscall包。其中最主要的函数是signal包中的Notify函数： func Notify(c chan&#60;- os.Signal, sig &#8230;os.Signal) 该函数会将进程收到的系统Signal转发给channel c。转发哪些信号由该函数的可变参数决定，如果你没有传入sig参数，那么Notify会将系统收到的所有信号转发给c。如果你像下面这样调用Notify： signal.Notify(c, syscall.SIGINT, syscall.SIGUSR1, syscall.SIGUSR2) 则Go只会关注你传入的Signal类型，其他Signal将会按照默认方式处理，大多都是进程退出。因此你需要在Notify中传入你要关注和处理的Signal类型，也就是拦截它们，提供自定义处理函数来改变它们的行为。 下面是一个较为完整的例子： //signal.go package main import &#34;fmt&#34; import &#34;time&#34; import &#34;os&#34; import &#34;os/signal&#34; import &#34;syscall&#34; type signalHandler func(s os.Signal, arg interface{}) type signalSet struct { &#160;&#160;&#160; m map[os.Signal]signalHandler } func signalSetNew()(*signalSet){ &#160;&#160;&#160; ss := new(signalSet) &#160;&#160;&#160; ss.m [...]]]></description>
			<content:encoded><![CDATA[<p>我们在生产环境下运行的系统要求优雅退出，即程序接收退出通知后，会有机会先执行一段清理代码，将收尾工作做完后再真正退出。我们采用系统Signal来 通知系统退出，即kill pragram-pid。我们在程序中针对一些系统信号设置了处理函数，当收到信号后，会执行相关清理程序或通知各个子进程做自清理。kill -9强制杀掉程序是不能被接受的，那样会导致某些处理过程被强制中断，留下无法恢复的现场，导致消息被破坏，影响下次系统启动运行。</p>
<p>	最近用<a href="http://golang.org">Golang</a>实现的一个代理程序也需要优雅退出，因此我尝试了解了一下<a href="http://tonybai.com/2012/08/17/hello-go/">Golang</a>中对系统Signal的处理方式，这里和大家分享。Golang 的系统信号处理主要涉及os包、os.signal包以及syscall包。其中最主要的函数是signal包中的Notify函数：</p>
<p>	<font face="Courier New">func Notify(c chan&lt;- os.Signal, sig &#8230;os.Signal)</font></p>
<p>	该函数会将进程收到的系统Signal转发给channel c。转发哪些信号由该函数的可变参数决定，如果你没有传入sig参数，那么Notify会将系统收到的所有信号转发给c。如果你像下面这样调用Notify：</p>
<p>	<font face="Courier New">signal.Notify(c, syscall.SIGINT, syscall.SIGUSR1, syscall.SIGUSR2)</font></p>
<p>	则Go只会关注你传入的Signal类型，其他Signal将会按照默认方式处理，大多都是进程退出。因此你需要在Notify中传入你要关注和处理的Signal类型，也就是拦截它们，提供自定义处理函数来改变它们的行为。</p>
<p>	下面是一个较为完整的例子：</p>
<p>	<font face="Courier New">//signal.go</font></p>
<p>	<font face="Courier New">package main</p>
<p>	import &quot;fmt&quot;<br />
	import &quot;time&quot;<br />
	import &quot;os&quot;<br />
	import &quot;os/signal&quot;<br />
	import &quot;syscall&quot;</p>
<p>	type signalHandler func(s os.Signal, arg interface{})</p>
<p>	type signalSet struct {<br />
	&nbsp;&nbsp;&nbsp; m map[os.Signal]signalHandler<br />
	}</p>
<p>	func signalSetNew()(*signalSet){<br />
	&nbsp;&nbsp;&nbsp; ss := new(signalSet)<br />
	&nbsp;&nbsp;&nbsp; ss.m = make(map[os.Signal]signalHandler)<br />
	&nbsp;&nbsp;&nbsp; return ss<br />
	}</p>
<p>	func (set *signalSet) register(s os.Signal, handler signalHandler) {<br />
	&nbsp;&nbsp;&nbsp; if _, found := set.m[s]; !found {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; set.m[s] =&nbsp; handler<br />
	&nbsp;&nbsp;&nbsp; }<br />
	}</p>
<p>	func (set *signalSet) handle(sig os.Signal, arg interface{})(err error) {<br />
	&nbsp;&nbsp;&nbsp; if _, found := set.m[sig]; found {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; set.m[sig](sig, arg)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return nil<br />
	&nbsp;&nbsp;&nbsp; } else {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return fmt.Errorf(&quot;No handler available for signal %v&quot;, sig)<br />
	&nbsp;&nbsp;&nbsp; }</p>
<p>	&nbsp;&nbsp;&nbsp; panic(&quot;won&#39;t reach here&quot;)<br />
	}</p>
<p>	func main() {<br />
	&nbsp;&nbsp;&nbsp; go sysSignalHandleDemo()<br />
	&nbsp;&nbsp;&nbsp; time.Sleep(time.Hour) // make the main goroutine wait!<br />
	}</p>
<p>	func sysSignalHandleDemo() {<br />
	&nbsp;&nbsp;&nbsp; ss := signalSetNew()<br />
	&nbsp;&nbsp;&nbsp; handler := func(s os.Signal, arg interface{}) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;handle signal: %v\n&quot;, s)<br />
	&nbsp;&nbsp;&nbsp; }</p>
<p>	&nbsp;&nbsp;&nbsp; ss.register(syscall.SIGINT, handler)<br />
	&nbsp;&nbsp;&nbsp; ss.register(syscall.SIGUSR1, handler)<br />
	&nbsp;&nbsp;&nbsp; ss.register(syscall.SIGUSR2, handler)</p>
<p>	&nbsp;&nbsp;&nbsp; for {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; c := make(chan os.Signal)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; var sigs []os.Signal<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; for sig := range ss.m {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; sigs = append(sigs, sig)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; signal.Notify(c)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; sig := &lt;-c</p>
<p>	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; err := ss.handle(sig, nil)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (err != nil) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;unknown signal received: %v\n&quot;, sig)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; os.Exit(1)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp; }<br />
	}</font></p>
<p>	上例中Notify函数只有一个参数，没有传入要关注的sig，因此程序会将收到的所有类型Signal都转发到channel c中。build该源文件并执行程序：</p>
<p>	<span style="font-family:courier new,courier,monospace;">$&gt; go build signal.go<br />
	$&gt; signal</span></p>
<p>	在另外一个窗口下执行如下命令：<br />
	<span style="font-family:courier new,courier,monospace;">$&gt; ps -ef|grep signal<br />
	tonybai&nbsp; 25271&nbsp; 1087&nbsp; 0 16:27 pts/1&nbsp;&nbsp;&nbsp; 00:00:00 signal<br />
	$&gt; kill -n 2 25271<br />
	$&gt; kill -n 12 25271<br />
	$&gt; kill 25271</span></p>
<p>	我们在第一个窗口会看到如下输出：<br />
	<span style="font-family:courier new,courier,monospace;">$&gt; signal<br />
	handle signal: interrupt<br />
	handle signal: user defined signal 2<br />
	unknown signal received: terminated</span></p>
<p>	在sysSignalHandleDemo中我们也可以为Notify传入我们所关注的Signal集合：</p>
<p>	<font face="Courier New">signal.Notify(c, sigs&#8230;)</font></p>
<p>	这样只有在该集合中的信号我们才能捕获，收到未在集合中的信号时，程序多直接退出。上面只是一个Demo，只是说明了我们可以捕捉到我们所关注的信号，并未体现程序如何优雅退出，不同程序的退出方式不同，这里没有通用方法，就不细说了，你的程序需要你专门的设计。</p>
<p>	另外我们生产环境下的程序多是以Daemon守护进程的形式运行的。我们用C实现的程序多参考&ldquo;<a href="http://book.douban.com/subject/1788421/">Unix高级编程</a>&rdquo;中的方法将程序转为Daemon Process，但在Go中目前尚提供相关方式，网上有一些实现，但据说都不理想。更多的Go开发者建议不要在代码中实现Daemon转换，建议直接利用 第三方工具。比如在<a href="http://ubuntu.com">Ubuntu</a>下我们可以使用start-stop-daemon这个小程序轻松将你的程序转换为Daemon：</p>
<p>	$&gt; start-stop-daemon &#8211;start &#8211;pidfile ./signal.pid &#8211;startas /home/tonybai/test/go/signal &#8211;background -m<br />
	$&gt; start-stop-daemon &#8211;stop &#8211;pidfile ./signal.pid &#8211;startas /home/tonybai/test/go/signal</p>
<p>	这里注意：只有加上-m选项，pidfile才能成功创建。</p>
<p>	start-stop-daemon在<a href="http://www.debian.org">Debian</a>系的Linux发行版中都是默认自带的。但在<a href="http://www.redhat.com">Redhat</a>系Linux发行版中却没有该工具，我们可以自行安装：</p>
<p>	<font face="Courier New">wget -c http://developer.axis.com/download/distribution/apps-sys-utils-start-stop-daemon-IR1_9_18-2.tar.gz<br />
	tar -xzf apps-sys-utils-start-stop-daemon-IR1_9_18-2.tar.gz<br />
	cd apps/sys-utils/start-stop-daemon-IR1_9_18-2<br />
	gcc start-stop-daemon.c -o start-stop-daemon</font></p>
<p>	切换到root下<br />
	<font face="Courier New">cp start-stop-daemon /sbin/<br />
	chmod +x /sbin/start-stop-daemon</font></p>
<p>	另外Go 1.0.2提供的二进制安装包直接在Redhat 5.6(Linux tonybai 2.6.18-238.el5 #1 SMP Sun Dec 19 14:22:44 EST 2010 x86_64 x86_64 x86_64 GNU/Linux)下面运行出错，提示无法找到GLIBC 2.7版本。目前解决这一问题的方法似乎只有从源码编译安装。进入到$GOROOT/src下，执行./all.bash即可。重现编译链接后的go可执 行程序则运行一切正常。</p>
<p style='text-align:left'>&copy; 2012, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2012/09/21/signal-handling-in-go/feed/</wfw:commentRss>
		<slash:comments>8</slash:comments>
		</item>
		<item>
		<title>如何加入Linux内核开发社区(7)</title>
		<link>https://tonybai.com/2012/04/09/how-to-participate-linux-community-section-7/</link>
		<comments>https://tonybai.com/2012/04/09/how-to-participate-linux-community-section-7/#comments</comments>
		<pubDate>Mon, 09 Apr 2012 05:25:34 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[Community]]></category>
		<category><![CDATA[Developer]]></category>
		<category><![CDATA[GNU]]></category>
		<category><![CDATA[Kernel]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Linux基金会]]></category>
		<category><![CDATA[Opensource]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[Redhat]]></category>
		<category><![CDATA[translation]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[内核]]></category>
		<category><![CDATA[博客]]></category>
		<category><![CDATA[开发者]]></category>
		<category><![CDATA[开源]]></category>
		<category><![CDATA[社区]]></category>
		<category><![CDATA[翻译]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=847</guid>
		<description><![CDATA[本文翻译自The Linux Foundation的《How to Participate in the Linux Community》(基于2012-03-21最新版本)，原作者为Jonathan Corbet(corbet@lwn.net)。 下面是该文章第七章、第八章以及第九章节的中译文。 &#160; 7、高级主题 &#160; 但愿此时此刻，你已经理解了内核开发过程是如何进行的。但仍然还有很多东西要学习！这一节将涵盖几个主题，这些主题对于那些致力于成为Linux 内核开发过程中固定一员的开发者来说是很有帮助的。 &#160; 7.1、使用Git管理补丁 &#160; 早在2002年，内核就开始使用分布式版本管理工具了，当时Linus首先使用的是一款名为BitKeeper的专有(proprietary) 应用。虽然BitKeeper是有争议的，但它所代表的软件版本管理方法几乎是没有任何争议的。分布式版本控制使得内核开发项目的开发效率获得了加速地提升。如今，有很多种可以替代BitKeeper的工具。不管结果如何，内核项目已经决定了将git作为其版本管理工具的选择。 &#160; 使用git管理补丁可以使开发者的工作更加轻松，特别是当补丁的数量越来越多的情况下。Git也有其不完善的地方并且可能产生某种危险；它是一个年轻而强大的工具，目前其开发者仍然在对其进行改进。本文不会尝试教授读者们如何使用git；其自带的长文档提供了足够的资料。相反，这里着重关注git是如何融入到内核开发过程中去的。那些期望快速学会使用git的开发者可以在下面网址中找到更多信息： &#160; http://git-scm.com/ &#160; http://www.kernel.org/pub/software/scm/git/docs/user-manual.html &#160; 并且可以在互联网上找到各种不同的教程。 &#160; 第一件事就是阅读上述站点所提供的内容，在尝试使用git制作补丁之前充分理解git的工作原理。一个使用git的开发者应该能够从内核主线库获得代码拷贝、查看修改历史记录、向代码树提交改变以及使用分支等。对git重写历史的工具(例如rebase)的理解也是很有用的。Git尤其自己的术语与概念；一个git的新用户应该知道引用(refs)、远程分支(remote branches)、暂存区(index，译注：现在更多称之为stage)、快进合并(fast-forward merge)、推(push)和拉(pull)以及detached heads等。一开始这些可能会让人感到有些望而生畏，但通过一点点学习这些概念掌握起来也不是那么难。 &#160; 使用git生成通过email提交的补丁是一种用来加快git学习速度的非常好的练习。 &#160; 如果你准备创建一个供其他人查看的git源码树，你自然会需要一个服务器，其他人可以从该服务器上拉(pull)代码。如果你拥有一个可以访问互联网的系统，使用git-daemon搭建这个服务器将会相对简单一些。否则，一些出现在互联网上的免费的公共托管站点(例如，Github)可供使用。已被社区认可的开发者可以从kernel.org获得一个帐户，但这些可是来之不易的；更多内容请参见http://kernel.org/faq/ 。 &#160; 正常的git工作流程涉及到许多分支使用。每行代码都可能被分离到一个独立的&#34;主题分支&#34;中并且独立维护。在Git中使用分支的代价非常小，我们没有理由不自由使用它们。并且，无论如何你都不应该在一个你想要其他人从中拉取(pull)代码的分支上进行开发。对公众开放的分支应该谨慎创建；只有当开发分支上的代码完成并具备发布条件时再将代码合并到补丁中，不要在完成之前就合并。 &#160; Git提供了一些功能强大的工具，它们可以让你重写开发历史。一个令人为难的补丁(可能是破坏了bisection的补丁，又或是有其他明显bug的补丁)可能在适当地方被修复或整体从开发历史中消失。一个补丁序列可以被重写，重写后就好似今天主线上最新的修改似的，即便你已经在这个补丁序列上工作几个月了。改变可以透明地从一个分支转移到另一个分支，等等。明智地的使用git所提供的能力对代码库历史进行修订可以有助于创建出问题更少的整洁的补丁集合。 &#160; 然而，除了着迷于创建一个完美的项目历史之外，过度地使用git提供的能力可能会导致其他问题。重写历史将重写历史所对应的改变，将一个测试过(希望是)的内核树转化为一个未测试过的内核。但是，除此之外，如果没有有关项目历史的共享视图，开发者间的合作将不会那么容易；如果你重写了一段代码历史，并且其他开发者已经将这段代码拉入其个人代码库，你会让这些开发者的工作变得更为困难。因此，这里可以应用一条简单的经验法则：已经被导出到其他库中的历史记录此后一般应被视作不可改变的。 &#160; 这样，一旦你向你的公共代码库服务器推送了一组变更，这些变更就不应该被重写了。如果你尝试推送无法进行快进合并(例如，那些没有共享同一变更历史的改变)的变更，Git会试图强制执行这条规则。对这种检查进行重写是可能的，并且有时重写一个导出源码树可能是必须的。在linux-next中通过在树间移动变更集(changesets)来避免冲突就是一个例子。不过这种行为应该是不常发生的。这也是开发工作要在私有分支上(必要时可以进行历史重写)完成并只是在其处于开发后期时才移到公共分支的原因之一。 &#160; 随着主线版本(或即将到来的其他基于一组变更的源码树)的推进，人们总愿意合并那些树以保持走在开发的最前沿。对于一个私有分支来说，换基(rebasing)可以作为一种跟上另外一棵源码树开发进度的简单方法，但一旦源码树已经对外发布，换基这种方法就不再适合。一旦如此，就必须进行全量合并(full&#160;merge)。偶尔的合并很有意义，但过于频繁的合并可能会导致修订历史不必要得混乱。针对这种情况的建议是不要频繁地合并，通常只在特定发布点(例如，一个主线的-rc版本发布时)进行合并操作。如果你对特定的变更感到紧张不安，那么你可以一直在私有分支上进行测试合并。git的&#34;rerere&#34;工具在这种情况下十分有用；它会记住合并时的冲突是如何被解决的，这样你就无需再做一遍这个工作了。 &#160; 关于类似git这样的工具的一个最大的抱怨是：补丁从一棵树到另一棵树的大量的迁移使得许多欠考虑的变更很容易通过评审雷达的盲区而进入内核主线。当内核开发者看到这种事情发生时都会十分不满；搭建一棵包含了未评审或离题补丁的源码树很可能会对以后你的源码树被内核主线合并的资格产生影响。这里引述Linus的一段话： &#160; &#160; &#160;你可以给我发送补丁，不过对我来说是从你那里拉出一个git补丁。我需要知道你十分清楚你自己正在做什么，并且我需要有能力在无需手工逐个检查每个变更的情况下信任你所做的这些工作。(http://lwn.net/Articles/224135/). &#160; [...]]]></description>
			<content:encoded><![CDATA[<p>本文翻译自The Linux Foundation的《<a href="http://www.linuxfoundation.org/content/how-participate-linux-community-0">How to Participate in the Linux Community</a>》(基于2012-03-21最新版本)，原作者为Jonathan Corbet(corbet@lwn.net)。 下面是该文章第七章、第八章以及第九章节的中译文。</p>
<div>&nbsp;</div>
<div><strong>7、高级主题</strong></div>
<div>&nbsp;</div>
<div>但愿此时此刻，你已经理解了内核开发过程是如何进行的。但仍然还有很多东西要学习！这一节将涵盖几个主题，这些主题对于那些致力于成为Linux 内核开发过程中固定一员的开发者来说是很有帮助的。</div>
<div>&nbsp;</div>
<div><strong>7.1、使用Git管理补丁</strong></div>
<div>&nbsp;</div>
<div>早在2002年，内核就开始使用分布式版本管理工具了，当时Linus首先使用的是一款名为BitKeeper的专有(proprietary) 应用。虽然BitKeeper是有争议的，但它所代表的软件版本管理方法几乎是没有任何争议的。分布式版本控制使得内核开发项目的开发效率获得了加速地提升。如今，有很多种可以替代BitKeeper的工具。不管结果如何，内核项目已经决定了将git作为其版本管理工具的选择。</div>
<div>&nbsp;</div>
<div>使用git管理补丁可以使开发者的工作更加轻松，特别是当补丁的数量越来越多的情况下。Git也有其不完善的地方并且可能产生某种危险；它是一个年轻而强大的工具，目前其开发者仍然在对其进行改进。本文不会尝试教授读者们如何使用git；其自带的长文档提供了足够的资料。相反，这里着重关注git是如何融入到内核开发过程中去的。那些期望快速学会使用git的开发者可以在下面网址中找到更多信息：</div>
<div>&nbsp;</div>
<div>http://git-scm.com/</div>
<div>&nbsp;</div>
<div>http://www.kernel.org/pub/software/scm/git/docs/user-manual.html</div>
<div>&nbsp;</div>
<div>并且可以在互联网上找到各种不同的教程。</div>
<div>&nbsp;</div>
<div>第一件事就是阅读上述站点所提供的内容，在尝试使用git制作补丁之前充分理解git的工作原理。一个使用git的开发者应该能够从内核主线库获得代码拷贝、查看修改历史记录、向代码树提交改变以及使用分支等。对git重写历史的工具(例如rebase)的理解也是很有用的。Git尤其自己的术语与概念；一个git的新用户应该知道引用(refs)、远程分支(remote branches)、暂存区(index，译注：现在更多称之为stage)、快进合并(fast-forward merge)、推(push)和拉(pull)以及detached heads等。一开始这些可能会让人感到有些望而生畏，但通过一点点学习这些概念掌握起来也不是那么难。</div>
<div>&nbsp;</div>
<div>使用git生成通过email提交的补丁是一种用来加快git学习速度的非常好的练习。</div>
<div>&nbsp;</div>
<div>如果你准备创建一个供其他人查看的git源码树，你自然会需要一个服务器，其他人可以从该服务器上拉(pull)代码。如果你拥有一个可以访问互联网的系统，使用git-daemon搭建这个服务器将会相对简单一些。否则，一些出现在互联网上的免费的公共托管站点(例如，Github)可供使用。已被社区认可的开发者可以从kernel.org获得一个帐户，但这些可是来之不易的；更多内容请参见http://kernel.org/faq/ 。</div>
<div>&nbsp;</div>
<div>正常的git工作流程涉及到许多分支使用。每行代码都可能被分离到一个独立的&quot;主题分支&quot;中并且独立维护。在Git中使用分支的代价非常小，我们没有理由不自由使用它们。并且，无论如何你都不应该在一个你想要其他人从中拉取(pull)代码的分支上进行开发。对公众开放的分支应该谨慎创建；只有当开发分支上的代码完成并具备发布条件时再将代码合并到补丁中，不要在完成之前就合并。</div>
<div>&nbsp;</div>
<div>Git提供了一些功能强大的工具，它们可以让你重写开发历史。一个令人为难的补丁(可能是破坏了bisection的补丁，又或是有其他明显bug的补丁)可能在适当地方被修复或整体从开发历史中消失。一个补丁序列可以被重写，重写后就好似今天主线上最新的修改似的，即便你已经在这个补丁序列上工作几个月了。改变可以透明地从一个分支转移到另一个分支，等等。明智地的使用git所提供的能力对代码库历史进行修订可以有助于创建出问题更少的整洁的补丁集合。</div>
<div>&nbsp;</div>
<div>然而，除了着迷于创建一个完美的项目历史之外，过度地使用git提供的能力可能会导致其他问题。重写历史将重写历史所对应的改变，将一个测试过(希望是)的内核树转化为一个未测试过的内核。但是，除此之外，如果没有有关项目历史的共享视图，开发者间的合作将不会那么容易；如果你重写了一段代码历史，并且其他开发者已经将这段代码拉入其个人代码库，你会让这些开发者的工作变得更为困难。因此，这里可以应用一条简单的经验法则：已经被导出到其他库中的历史记录此后一般应被视作不可改变的。</div>
<div>&nbsp;</div>
<div>这样，一旦你向你的公共代码库服务器推送了一组变更，这些变更就不应该被重写了。如果你尝试推送无法进行快进合并(例如，那些没有共享同一变更历史的改变)的变更，Git会试图强制执行这条规则。对这种检查进行重写是可能的，并且有时重写一个导出源码树可能是必须的。在linux-next中通过在树间移动变更集(changesets)来避免冲突就是一个例子。不过这种行为应该是不常发生的。这也是开发工作要在私有分支上(必要时可以进行历史重写)完成并只是在其处于开发后期时才移到公共分支的原因之一。</div>
<div>&nbsp;</div>
<div>随着主线版本(或即将到来的其他基于一组变更的源码树)的推进，人们总愿意合并那些树以保持走在开发的最前沿。对于一个私有分支来说，换基(rebasing)可以作为一种跟上另外一棵源码树开发进度的简单方法，但一旦源码树已经对外发布，换基这种方法就不再适合。一旦如此，就必须进行全量合并(full&nbsp;merge)。偶尔的合并很有意义，但过于频繁的合并可能会导致修订历史不必要得混乱。针对这种情况的建议是不要频繁地合并，通常只在特定发布点(例如，一个主线的-rc版本发布时)进行合并操作。如果你对特定的变更感到紧张不安，那么你可以一直在私有分支上进行测试合并。git的&quot;rerere&quot;工具在这种情况下十分有用；它会记住合并时的冲突是如何被解决的，这样你就无需再做一遍这个工作了。</div>
<div>&nbsp;</div>
<div>关于类似git这样的工具的一个最大的抱怨是：补丁从一棵树到另一棵树的大量的迁移使得许多欠考虑的变更很容易通过评审雷达的盲区而进入内核主线。当内核开发者看到这种事情发生时都会十分不满；搭建一棵包含了未评审或离题补丁的源码树很可能会对以后你的源码树被内核主线合并的资格产生影响。这里引述Linus的一段话：</div>
<div>&nbsp;</div>
<div><em>&nbsp; &nbsp;你可以给我发送补丁，不过对我来说是从你那里拉出一个git补丁。我需要知道你十分清楚你自己正在做什么，并且我需要有能力在无需手工逐个检查每个变更的情况下信任你所做的这些工作。(http://lwn.net/Articles/224135/).</em></div>
<div>&nbsp;</div>
<div>为了避免这类情况，请确保一个特定分支里面的所有补丁都紧扣相关主题；一个&quot;驱动程序修复&quot;分支不应该对核心内存管理代码进行修改。并且，更为重要的是，不要使用git树绕过评审过程。不时地将源码树的概要发到相关的邮件列表中，并且当时机合适时，请求将你的源码树中的变更包含到linux-next中。</div>
<div>&nbsp;</div>
<div>如果当其他人开始向你的源码树发送补丁时，不要忘记评审这些补丁代码。同时，也要保证你维护着正确的作者身份信息；在这方面git的&quot;am&quot;工具做得最好，不过对于那些通过第三方转发给你的补丁，你需要为补丁增加一个&quot;From:&quot;行。</div>
<div>&nbsp;</div>
<div>当提出&quot;拉出&quot;请求时，请确保提供了所有相关信息：你的源码树的位置，从哪个分支拉出，以及此次拉出将导致哪些改变。在这方面，git的&quot;request-pull&quot;命令很有帮助；这个命令会将请求按照其他开发者所期望的那样进行格式化，并且还会执行检查以确保你记得已经将那些改变提交到公共代码树服务器上了。</div>
<div>&nbsp;</div>
<div><strong>7.2、评审补丁</strong></div>
<div>&nbsp;</div>
<div>很多读者肯定会反对将本章标题命名为&quot;高级主题&quot;，因为即便是刚入门的内核开发者也应当评审补丁。的确，没有比审查其他人发布的代码更好的方式去学习在内核环境下如何编程了。此外，评审者永远供不应求；通过审查代码，你可以对整个开发过程作出重要的贡献。</div>
<div>&nbsp;</div>
<div>评审代码可能是一件令人胆怯的事情，特别是对于内核开发新手们，他们对于那些经验丰富的开发者所公开提出的代码质疑很可能会感到紧张不安。不过，即使是经验最为丰富的开发者所编写到的代码也可能有改进的余地。也许对评审者(所有评审者)最好的建议是：用询问而不是批评来表达评审意见。问&quot;在这条路径上这个锁是如何被释放的？&quot;总是会比&quot;这里的锁用错了&quot;收到更好的效果。</div>
<div>&nbsp;</div>
<div>不同的开发者会从不同的角度去评审代码。一些人主要关注代码风格以及是否代码行伴有结尾空白。其他人会主要关注这个补丁所实现的改变对与内核整体来说是好事还是坏事。然而，还有其他一些人将检查有问题的锁、过度使用栈、潜在的安全问题、在其他地方发现重复代码、是否有充足的文档、对内核性能的不利影响、用户空间ABI变化等。如果能够促使更好的代码进入内核，那么所有类型的评审都是受欢迎的并且是值得花时间做的。</div>
<div>&nbsp;</div>
<div><strong>8、更多信息</strong></div>
<div>&nbsp;</div>
<div>Linux内核开发以及相关主题的信息来源有很多。这里面首当其冲的应该是可以在发布的内核源码包中找到的Documentation目录。顶层的HOWTO文件是一个重要的起点；SubmittingPatches和SubmittingDrivers同样是所有内核开发者都应该阅读的重要文档。许多内核内部API都使用kerneldoc机制进行了文档化；&quot;make htmldocs&quot;或&quot;make pdfdocs&quot;可用于生成HTML或PDF格式(但很多Linux发行版中包含的TeX版本运行时遇到内部限制，因此也无法正确地处理这里的文档)的内核文档。</div>
<div>&nbsp;</div>
<div>各种讨论内核开发细节的网络站点。作者这里将http://lwn.net作为一个内核开发信息来源推荐给大家；许多关于特定内核主题的信息都可以通过LWN内核索引找到：</div>
<div>&nbsp;</div>
<div>http://lwn.net/Kernel/Index/</div>
<div>&nbsp;</div>
<div>除此之外，一个对内核开发者有价值的资源是：</div>
<div>&nbsp;</div>
<div>http://kernelnewbies.org/</div>
<div>&nbsp;</div>
<div>有关linux-next源码树的资料汇集在：</div>
<div>&nbsp;</div>
<div>http://linux.f-seidel.de/linux-next/pmwiki/</div>
<div>&nbsp;</div>
<div>当然，大家不应该忘记http://kernel.org，这里可是内核发布版本信息的最终位置。</div>
<div>&nbsp;</div>
<div>下面是一些关于内核开发的书籍：</div>
<div>&nbsp; * Linux Device Drivers(译注：其中译版为《Linux设备驱动程序》), 3rd Edition (Jonathan Corbet, Alessandro Rubini, and Greg Kroah-Hartman). 在线版本在http://lwn.net/Kernel/LDD3/。</div>
<div>&nbsp;</div>
<div>&nbsp; * Linux Kernel Development (Robert Love)(译注：其中译版为《Linux内核设计与实现》)。</div>
<div>&nbsp;</div>
<div>&nbsp; * Understanding the Linux Kernel (Danial Bovet and Marco Cesati)(译注：其中译版为《深入理解Linux内核》)。</div>
<div>&nbsp;</div>
<div>但所有这些书籍都有一个共同的不足：在它们上架时往往有些过时，并且它们上架已经有一段时间了。不过，在这些书中我们仍然可以找到很多有价值的资料。</div>
<div>&nbsp;</div>
<div>Git的文档可以在下面网址上找到：</div>
<div>&nbsp;</div>
<div>http://www.kernel.org/pub/software/scm/git/docs/</div>
<div>&nbsp;</div>
<div>http://www.kernel.org/pub/software/scm/git/docs/user-manual.html</div>
<div>&nbsp;</div>
<div><strong>9、结论</strong></div>
<div>&nbsp;</div>
<div>恭喜每一个读完这篇冗长文档的人。希望本文可以为你对Linux内核的开发过程以及如何加入此过程的理解提供有用的帮助。</div>
<div>&nbsp;</div>
<div>最后，最重要的是参与。任何开源软件项目只不过是其所有贡献者所做事情的总和。Linux内核项目进展如此迅速，质量如此之好，都是因为有数量可观的开发者的帮助，他们的工作都是为了创建一个更好的内核。Linux内核就是一个由成千上万人为了一个共同的目标而一起奋斗而完成的一个最好的例子。</div>
<div>&nbsp;</div>
<div>虽然内核项目总是能受益于一个更为庞大的开发者基础，但那里也总是有更多的工作要去做。但同样重要的是，在Linux生态系统中的其他大多数参与者也能从对内核的贡献中受益。让代码进入主线是更高代码质量、更低的维护和发行成本、对内核开发方向的更高层次的影响以及更多其他事情的关键。这是一个所有参与者共赢的局面。发动你的编辑器并加入我们吧；你会受到热烈欢迎。</div>
<div>&nbsp;</div>
<div><em>(全文翻译结束)</em></div>
<p style='text-align:left'>&copy; 2012, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2012/04/09/how-to-participate-linux-community-section-7/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
