<?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; Blogger</title>
	<atom:link href="http://tonybai.com/tag/blogger/feed/" rel="self" type="application/rss+xml" />
	<link>https://tonybai.com</link>
	<description>一个程序员的心路历程</description>
	<lastBuildDate>Mon, 06 Apr 2026 00:29:15 +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>使用issue2md将Github issue转换为Markdown</title>
		<link>https://tonybai.com/2024/12/23/convert-github-issue-to-markdown-with-issue2md/</link>
		<comments>https://tonybai.com/2024/12/23/convert-github-issue-to-markdown-with-issue2md/#comments</comments>
		<pubDate>Mon, 23 Dec 2024 15:02:42 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[Amazon]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[Claude]]></category>
		<category><![CDATA[cli]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go1.24]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Go测试]]></category>
		<category><![CDATA[Go语言第一课]]></category>
		<category><![CDATA[hub]]></category>
		<category><![CDATA[issue]]></category>
		<category><![CDATA[issue2md]]></category>
		<category><![CDATA[issue2mdweb]]></category>
		<category><![CDATA[json]]></category>
		<category><![CDATA[markitdown]]></category>
		<category><![CDATA[omit]]></category>
		<category><![CDATA[omitzero]]></category>
		<category><![CDATA[sonnet]]></category>
		<category><![CDATA[systemd]]></category>
		<category><![CDATA[vercel]]></category>
		<category><![CDATA[Wechat]]></category>
		<category><![CDATA[加餐]]></category>
		<category><![CDATA[命令行]]></category>
		<category><![CDATA[极客时间]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4447</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2024/12/23/convert-github-issue-to-markdown-with-issue2md 到2024年底，不论你是否承认，AI时代都已经到来！近两个月，三大顶级商业AI模型巨头：Claude Sonnet 3.5、Google Gemini 2.0 Flash Experimental以及ChatGPT o3你方唱罢我登场，好不热闹！ 作为走在AI应用前沿的程序员，利用AI辅助自己提高学习和工作实践的效率都是必不可少的。在使用AI的过程中，我们经常需要向其提供一些文档资料，对于文字资料，AI更偏爱TXT、Markdown、PDF等格式的文件。部署在Vercel上的MarkdownDown支持输入网页URL并将其转换为Markdown，而微软开源的MarkItdown则能将多种格式(pdf、ppt、word、html、zip等)转换为Markdown。这些工具在实践中帮助我们实现对AI的快速“投喂”。 然而，一些资料，如GitHub Issues，尚不能通过上述工具方便地转换为干净的、无额外干扰内容的Markdown或其他适合投喂给AI的格式。受到MarkdownDown的启发，我思考是否可以将GitHub Issues转换为Markdown，最终促成了issue2md这个想法。该工具旨在简化GitHub Issues与Markdown之间的转换过程，使得开发者可以更高效地利用AI理解Github issue中的内容，包括用户讨论中的一些观点和想法。 三个月前，我利用AI完成了issue2md这个小工具，我自己甚至没有写下一行代码。我仅仅对其提出一个小小的要求，那就是不要依赖任何第三方包，仅可以依赖Go标准库。在这三个月中，该工具给了我很大的帮助，将由它生成的Github Issue对应的Markdown文档投喂给AI后，可以让我快速理解Github issue的要点，尤其是那些历经几年讨论，积累了数百条comment的issue！ 这里我将issue2md放到github上供大家下载使用，也希望能给大家带去相同的帮助。 下面简单介绍一下issue2md的用法。 issue2md项目有两个工具，或者说两种使用模式，一种是命令行模式，使用issue2md这个命令行工具。另外一种则是Web模式，使用issue2mdweb这个工具。 如果你喜欢命令行模式，那么你只需要使用下面命令安装issue2md即可： $go install github.com/bigwhite/issue2md/cmd/issue2md@latest issue2md cli程序的使用方法非常简单： Usage: issue2md issue-url [markdown-file] Arguments: issue-url The URL of the GitHub issue to convert. markdown-file (optional) The output markdown file. 它的第一个参数是github issue的URL。以Go 1.24版本json包增加对omitzero的支持的issue为例，它的url是https://github.com/golang/go/issues/45669，我们原封不动的将其作为issue2md的第一个参数执行： $issue2md https://github.com/golang/go/issues/45669 Issue [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/convert-github-issue-to-markdown-with-issue2md-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2024/12/23/convert-github-issue-to-markdown-with-issue2md">本文永久链接</a> &#8211; https://tonybai.com/2024/12/23/convert-github-issue-to-markdown-with-issue2md</p>
<p>到2024年底，不论你是否承认，<a href="https://tonybai.com/2024/10/14/programming-in-ai-era">AI时代都已经到来</a>！近两个月，三大顶级商业AI模型巨头：<a href="https://claude.ai/chats">Claude Sonnet 3.5</a>、<a href="https://aistudio.google.com/">Google Gemini 2.0 Flash Experimental</a>以及<a href="https://openai.com/12-days/">ChatGPT o3</a>你方唱罢我登场，好不热闹！</p>
<p>作为走在AI应用前沿的程序员，利用AI辅助自己提高学习和工作实践的效率都是必不可少的。在使用AI的过程中，我们经常需要向其提供一些文档资料，对于文字资料，AI更偏爱TXT、Markdown、PDF等格式的文件。<a href="https://markdowndown.vercel.app/">部署在Vercel上的MarkdownDown</a>支持输入网页URL并将其转换为Markdown，而<a href="https://github.com/microsoft/markitdown">微软开源的MarkItdown</a>则能将多种格式(pdf、ppt、word、html、zip等)转换为Markdown。这些工具在实践中帮助我们实现对AI的快速“投喂”。</p>
<p>然而，一些资料，如GitHub Issues，尚不能通过上述工具方便地转换为干净的、无额外干扰内容的Markdown或其他适合投喂给AI的格式。受到MarkdownDown的启发，我思考是否可以将GitHub Issues转换为Markdown，最终促成了issue2md这个想法。该工具旨在简化GitHub Issues与Markdown之间的转换过程，使得开发者可以更高效地利用AI理解Github issue中的内容，包括用户讨论中的一些观点和想法。</p>
<p>三个月前，我利用AI完成了issue2md这个小工具，我自己甚至没有写下一行代码。我仅仅对其提出一个小小的要求，那就是<strong>不要依赖任何第三方包，仅可以依赖Go标准库</strong>。在这三个月中，该工具给了我很大的帮助，将由它生成的Github Issue对应的Markdown文档投喂给AI后，可以让我快速理解Github issue的要点，尤其是那些历经几年讨论，积累了数百条comment的issue！</p>
<p>这里我将<a href="https://github.com/bigwhite/issue2md">issue2md放到github上</a>供大家下载使用，也希望能给大家带去相同的帮助。</p>
<p>下面简单介绍一下issue2md的用法。</p>
<p>issue2md项目有两个工具，或者说两种使用模式，一种是命令行模式，使用issue2md这个命令行工具。另外一种则是Web模式，使用issue2mdweb这个工具。</p>
<p>如果你喜欢命令行模式，那么你只需要使用下面命令安装issue2md即可：</p>
<pre><code>$go install github.com/bigwhite/issue2md/cmd/issue2md@latest
</code></pre>
<p>issue2md cli程序的使用方法非常简单：</p>
<pre><code>Usage: issue2md issue-url [markdown-file]
Arguments:
  issue-url      The URL of the GitHub issue to convert.
  markdown-file  (optional) The output markdown file.
</code></pre>
<p>它的第一个参数是github issue的URL。以<a href="https://tonybai.com/2024/12/17/go-1-24-foresight-part2/">Go 1.24版本</a>json包<a href="https://tonybai.com/2024/09/12/solve-the-empty-value-dilemma-in-json-encoding-with-omitzero/">增加对omitzero的支持</a>的issue为例，它的url是https://github.com/golang/go/issues/45669，我们原封不动的将其作为issue2md的第一个参数执行：</p>
<pre><code>$issue2md https://github.com/golang/go/issues/45669
Issue and comments saved as Markdown in file golang_go_issue_45669.md
</code></pre>
<p>issue2md cli默认会生成一个命名格式如下的文件：</p>
<pre><code>{owner}_{repo}_issue_number.md
</code></pre>
<p>其内容使用markdown编辑器打开并渲染后将呈现如下的效果：</p>
<p><img src="https://tonybai.com/wp-content/uploads/convert-github-issue-to-markdown-with-issue2md-2.png" alt="" /></p>
<p>当然你也可以通过传入第二个命令行参数，作为最终生成的markdown的文件名！</p>
<p>如果你不喜欢命令行模式，你可以<strong>使用issue2mdweb提供的Web模式</strong>。最简单的启动一个issue2mdweb服务的方法就是利用我发布到Docker hub上的issue2md的公共镜像，你可以像下面这样在本地或你的私有云里运行一个issue2mdweb服务：</p>
<pre><code>$docker run -d -p 8080:8080 bigwhite/issue2mdweb
</code></pre>
<p>然后用你的浏览器打开http://{host}:8080这个地址，你将看到如下的页面：</p>
<p><img src="https://tonybai.com/wp-content/uploads/convert-github-issue-to-markdown-with-issue2md-3.png" alt="" /></p>
<p>在中间的文本框中输入你要转换的Github issue地址，比如前面的https://github.com/golang/go/issues/45669，点击“Convert”，你的浏览器就会自动将转换后的Markdown文件下载到你的本地，文件命名和issue2md cli的默认命名格式一致！</p>
<p>如果你不想使用Docker运行，你可以自行下载issue2md代码并编译，也可以使用scripts中的命令将issue2mdweb安装为一个Systemd unit服务！</p>
<p>这里要注意的是，issue2md使用了Go标准口实现了对Github API的访问且没有使用任何账号信息，它仅适合将Public仓库的issue转换为Markdown，并且由于Github对API调用的限速，你在使用issue2md时不能过于频繁！此外，你若发现issue2md的bug或者你有什么新的想法，欢迎<a href="https://github.com/bigwhite/issue2md/issues">在issue2md仓库中提出你宝贵的issue</a>。</p>
<p>最后打个“广告”，根据极客时间的专栏推广计划，我在春节前会为“<a href="http://gk.link/a/10AVZ">Go语言第一课</a>”专栏<a href="https://time.geekbang.org/column/article/835197">续写五篇文章</a>，其中的第一篇“<a href="https://time.geekbang.org/column/article/835208">Go测试的5个使用建议</a>”已经上线。</p>
<p>无论你是“Go语言第一课”的学员，还是首次听说这门专栏的小伙伴，我都欢迎你阅读这些文章，希望这些专栏文章能你带去新的收获！也欢迎你将阅读后的感受在评论区分享出来！</p>
<hr />
<p><a href="https://public.zsxq.com/groups/51284458844544">Gopher部落知识星球</a>在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！</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>著名云主机服务厂商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://gopherdaily.tonybai.com</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>Gopher Daily归档 &#8211; https://github.com/bigwhite/gopherdaily</li>
<li>Gopher Daily Feed订阅 &#8211; https://gopherdaily.tonybai.com/feed</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; 2024, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2024/12/23/convert-github-issue-to-markdown-with-issue2md/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>又当爸爸了！</title>
		<link>https://tonybai.com/2020/07/29/my-second-daughter-was-born/</link>
		<comments>https://tonybai.com/2020/07/29/my-second-daughter-was-born/#comments</comments>
		<pubDate>Wed, 29 Jul 2020 06:01:34 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[生活簿]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></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">https://tonybai.com/?p=2932</guid>
		<description><![CDATA[2020年7月23日早6点46分，随着我家二宝(小名：七月)的呱呱坠地，我又当爸爸了! 图：二宝出生后的第一张照片 距离我家大宝(果果)的出生已经十年了。在这十年间，果果已经出落成一个聪明可爱、灵通剔透、漂亮温柔的大姑娘了，妥妥的是妈妈的小棉袄，爸爸的小情人:)，姥姥的小粘包，爷爷奶奶的乖孙女。 图：大宝果果是大姑娘了 但每每当果果提到其同班同学多数都有姐妹或兄弟陪伴上学、上才艺课的时候，我和我老婆的心里就会一动：究竟该不该给果果生一个亲弟弟/妹妹呢？ 2019下半年，我们决定为果果生个弟弟或妹妹。我们计划尝试半年，如果不行，我们年龄也大了，也许真的就不会再要了。结果上天十分眷顾我们，老婆在10月末怀上了。 大宝的愿望是我们要二宝的最直接和最主要的原因，我们也觉得两个宝宝在人生路上能相互陪伴总是更好的。其次，大宝出生时，我们还年轻，体验不充分，这次想再来一遍(也不知道哪来的这份勇气^_^)；再次，老人那边还有精力，还可以帮忙照顾孩子，我们在忙事业的同时，也不会有太多的后顾之忧；最后，两个宝宝也让家庭结构更合理。 写到这里，我也感觉上面的理由写得有些“冠冕堂皇”！想要就要，哪还需要这么多“借口”^_^。 生二宝唯一的担心就是已经是“高龄”的老婆。和十年前年轻的她相比，这次在孕期、生产和产后的风险都要高出许多。因此，在整个十月怀胎以及生产的过程中，我都更为紧张，但能做的也只有全程守护在老婆身边：制定营养计划、每天接送、全程陪检等。老婆本人倒是没有这方面担心，鉴于大宝生产前后的顺利，她坚信这次二宝也会同样顺利。 整个孕期也正如老婆坚信的那样，一切都很顺利，除了老婆患上了妊娠期糖尿病。老婆的一个性格特点就是认准一件事后，就能坚定不移地、自律地执行下去。由于妊娠期糖尿病对饮食的要求，老婆整整几个月都远离美味的“糖分”，保证了二宝在肚肚里的健康发育。同时，为了能够像一胎那样顺产，老婆坚持每天都要走上1w步，风雨无阻，天气不好，就在楼梯间里爬楼梯或在顶楼露台来回踱步。这份坚毅让老婆在38周的彩超检查中收获了期望的结论：医生看完彩超结果后给了我老婆十分肯定的诊断意见：你一定可以自己生！ 老婆，你真的很伟大！ 老婆在7月22日早晨见红了。按照一般经验，见红后24-48小时二宝就会出生了。7月23日凌晨3点老婆有了宫缩反应，虽然还不规律，但保险起见，我和老婆还是决定带上行李赶往医院。3:30到达医院急诊，产科的急诊大夫给开了一些检查和检验，结果出来后，就安排老婆去了(盛京医院滑翔园区)第五产科住院处办理住院。住院医生给老婆做了内检，说老婆今天很快就能生。早上5:00多，老婆进入待产室。随着规律性宫缩的到来，老婆十分痛苦。6点20分，医生决定让老妈进分娩室，我就在外面焦急等待。 老婆肚子里的二宝仿佛知道体谅她的妈妈，十分配合妈妈生产，让产程大大缩短，大大减少了老婆生产过程中承受的疼痛的时长。 在老婆进入分娩室后仅30分钟，站在分娩室外的我就听到了我家二宝第一声响亮的婴儿啼哭声。那时那刻，我和大宝出生时一样，流下了兴奋而又心疼老婆的眼泪。 母女平安！我的一颗高悬的心终于放下了，我再次当爸爸了！ 微信赞赏： &#169; 2020, bigwhite. 版权所有.]]></description>
			<content:encoded><![CDATA[<p>2020年7月23日早6点46分，随着我家<a href="https://daughter2.tonybai.com">二宝(小名：七月)</a>的呱呱坠地，<strong>我又当爸爸了</strong>!</p>
<p><img src="https://tonybai.com/wp-content/uploads/my-second-daughter/ally-was-born.jpeg" alt="img{512x368}" /><br />
<center>图：二宝出生后的第一张照片</center></p>
<p>距离我家<a href="https://tonybai.com/2010/05/11/now-i-am-a-father/">大宝(果果)的出生</a>已经十年了。在这十年间，<a href="https://daughter.tonybai.com">果果</a>已经出落成一个聪明可爱、灵通剔透、漂亮温柔的大姑娘了，妥妥的是<strong>妈妈的小棉袄，爸爸的小情人:)，姥姥的小粘包，爷爷奶奶的乖孙女</strong>。</p>
<p><img src="https://tonybai.com/wp-content/uploads/guoguo-ten-years-old/guoguo-10-years-old-1.jpg" alt="img{512x368}" /><br />
<center>图：大宝果果是大姑娘了</center></p>
<p>但每每当果果提到其同班同学多数都有姐妹或兄弟陪伴上学、上才艺课的时候，我和我老婆的心里就会一动：<strong>究竟该不该给果果生一个亲弟弟/妹妹呢？</strong> 2019下半年，我们决定为果果生个弟弟或妹妹。我们计划尝试半年，如果不行，我们年龄也大了，也许真的就不会再要了。结果<strong>上天十分眷顾我们，老婆在10月末怀上了</strong>。</p>
<p>大宝的愿望是我们要二宝的最直接和最主要的原因，我们也觉得两个宝宝在人生路上能相互陪伴总是更好的。其次，大宝出生时，我们还年轻，体验不充分，这次想再来一遍(<strong>也不知道哪来的这份勇气^_^</strong>)；再次，老人那边还有精力，还可以帮忙照顾孩子，我们在忙事业的同时，也不会有太多的后顾之忧；最后，两个宝宝也让家庭结构更合理。</p>
<blockquote>
<p>写到这里，我也感觉上面的理由写得有些“冠冕堂皇”！想要就要，哪还需要这么多“借口”^_^。</p>
</blockquote>
<p>生二宝唯一的担心就是已经是“高龄”的老婆。和十年前年轻的她相比，这次在孕期、生产和产后的风险都要高出许多。因此，在整个十月怀胎以及生产的过程中，我都更为紧张，但能做的也只有全程守护在老婆身边：制定营养计划、每天接送、全程陪检等。老婆本人倒是没有这方面担心，鉴于大宝生产前后的顺利，她坚信这次二宝也会同样顺利。</p>
<p>整个孕期也正如老婆坚信的那样，一切都很顺利，除了老婆患上了妊娠期糖尿病。老婆的一个性格特点就是认准一件事后，就能坚定不移地、自律地执行下去。由于妊娠期糖尿病对饮食的要求，老婆整整几个月都远离美味的“糖分”，保证了二宝在肚肚里的健康发育。同时，为了能够像一胎那样顺产，老婆坚持每天都要走上1w步，风雨无阻，天气不好，就在楼梯间里爬楼梯或在顶楼露台来回踱步。这份坚毅让老婆在38周的彩超检查中收获了期望的结论：医生看完彩超结果后给了我老婆十分肯定的诊断意见：<strong>你一定可以自己生</strong>！</p>
<p><strong>老婆，你真的很伟大！</strong></p>
<p>老婆在7月22日早晨见红了。按照一般经验，见红后24-48小时二宝就会出生了。7月23日凌晨3点老婆有了宫缩反应，虽然还不规律，但保险起见，我和老婆还是决定带上行李赶往医院。3:30到达医院急诊，产科的急诊大夫给开了一些检查和检验，结果出来后，就安排老婆去了(盛京医院滑翔园区)第五产科住院处办理住院。住院医生给老婆做了内检，说老婆今天很快就能生。早上5:00多，老婆进入待产室。随着规律性宫缩的到来，老婆十分痛苦。6点20分，医生决定让老妈进分娩室，我就在外面焦急等待。</p>
<p>老婆肚子里的二宝仿佛知道体谅她的妈妈，十分配合妈妈生产，让产程大大缩短，大大减少了老婆生产过程中承受的疼痛的时长。 在老婆进入分娩室后仅30分钟，站在分娩室外的我就听到了我家二宝第一声响亮的婴儿啼哭声。那时那刻，<strong>我和大宝出生时一样，流下了兴奋而又心疼老婆的眼泪</strong>。</p>
<p>母女平安！我的一颗高悬的心终于放下了，我再次当爸爸了！</p>
<hr />
<p>微信赞赏：<br />
<img src="https://tonybai.com/wp-content/uploads/wechat-zanshang-code-512x512.jpg" alt="img{512x368}" /></p>
<p style='text-align:left'>&copy; 2020, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2020/07/29/my-second-daughter-was-born/feed/</wfw:commentRss>
		<slash:comments>10</slash:comments>
		</item>
		<item>
		<title>果果十周岁了！</title>
		<link>https://tonybai.com/2020/05/03/guoguo-ten-years-old/</link>
		<comments>https://tonybai.com/2020/05/03/guoguo-ten-years-old/#comments</comments>
		<pubDate>Sun, 03 May 2020 05:24:50 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[生活簿]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></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">https://tonybai.com/?p=2937</guid>
		<description><![CDATA[好久没有在我的博客上写关于果果的事情了，因为很多关于果果成长的经历都记录在她自己的博客中了。但今天是她十周岁的生日，是个值得纪念的日子。闺女成长的十年，也是我学习为人父的十年。作为父亲，我发自内心地想说点啥，是回顾，也是感受，亦有些寄语^_^。 图：果果成长的十年 出生 老婆在2009年7月怀上了果果。那时我们刚刚新婚不久，二人世界还没过够^&#95;^，小家伙的突然到来还让我们有些“手足无措”。为此，我们还认真地讨论了两天，最终老婆拍板：我要生下这个孩子，于是果果保住了^&#95;^。如今，每当果果提及此事，都会“发狠”地盯上我几眼，我也只能呵呵呵呵地应对^_^。 老婆怀胎中段，我一直在福建出差，虽然有岳母陪在老婆身边，但年底那两个月，老婆心情十分差。直到那一年大年三十的下午2点，我才在桃仙机场下的飞机，匆匆赶回家。大街上连车的打不到，多亏还有公交系统。进入家门，心里满满的都是对老婆的愧疚。记忆中老婆似乎并没有说啥，只说了一句“吃饭吧”，我顿感心里热乎乎的。 果果似乎很享受在妈妈子宫中待着，预产期都过了几天了，她还没有反应，直到2010年5月3日凌晨1点多，规律性的宫缩“来袭”。我们匆忙赶到医院，早上6点半多，老婆进入产室，9点多，我在产室外听到了果果呱呱坠地后的第一声啼哭。 图：刚出生的果果 果果出生后，恰逢徐峥的作品《人在囧途》上映，影片中徐峥扮演角色的孩子叫果果，我们觉得这个小名不错，于是便给我们宝宝起名为果果。 第一次照顾这么小的孩子也着实让我们这些大人手忙脚乱一段时间。出月子后，生活逐渐恢复平稳。果果每天除了吃奶就是睡觉，也算是比较省心了。稍大一些后，果果似乎并不太愿意睡觉了，每次喂完奶都得放到小车上在厅里推过来推过去哄她睡觉，后来果果姥姥买的一个能发出大恐龙吼声的玩具也加入到促进果果睡眠的行列中^&#95;^。 第一次走路(0岁) 果果一直母乳喂养，身体也很壮实。抬头、翻身、学会爬行和其他孩子相比都略有提前。最让我印象深刻是她第一次学会独立行走。记得那是2010年农历春节前，我们家当时的供暖非常好，室温在28度以上。果果在家只穿一套内衣裤，因此行动和玩耍起来非常方便。之前果果就可以扶着床沿踱步了并且她似乎也很喜欢站起来的感觉。那天她自己在卧室的地板上玩耍，我在门口偷偷观察她。她玩了一会儿就开始扶着床站起来，看我站在门口，她居然放开了扶着床沿的双手，向我摇摇晃晃地走了过来。从床边到卧室门口大约有不到2米的距离，我也兴奋地张开双臂，引导她向我走来。她边走边兴奋地笑，似乎也在惊讶于自己能独立行走了。在她扑到我的怀中的那一刻，我才意识到我见证了果果人生第一次独立行走，老婆听到这个消息也是兴奋不已。自从果果学会了走路，此后便一发不可收拾^&#95;^。 打针不哭(1岁) 可能是因为母乳喂养，果果在一岁的这一年中没有患过任何感冒发烧的病症。但当母体带给她的免疫力逐渐失去作用后，果果便和其他小朋友一样，会得感冒，也会发烧。记得果果第一次感冒(刚过一岁生日没多久)就烧的特别厉害。由于我们也是第一次遇到这种情况，特别担心，于是就带她去医院检查。虽然我们期望医生开立口服药，但医生最终还是开了点滴。在护士站门口，果果看到其他孩子扎针时都哇哇的哭，心里也胆怯了。果不其然，当针头穿透她的皮肤进入血管的那一刻，果果也像其他孩子一样，哇哇大哭起来。 有了这次“痛苦”的扎针经历后，我们也对她进行了心理疏导，教她要学会坚强，从她的眼神看得出来，她似乎听懂了。在2011年的秋冬换季，果果又患感冒发烧。同样去了医院，同样开了点滴，但在护士站扎针的时候，果果居然很坚强的忍住了，没有哇哇的哭泣，这让看惯了孩子痛哭的护士也是惊奇不已。看着泪水在眼圈里打转但没有哭出来的果果，我们心里更是心疼她了。 送去幼儿园学说话(2岁) 果果在11个月的时候就会喊妈妈了，但直到2岁半她能吐出的字依然只有“妈妈”，偶尔也有“爸爸”。现在看来，果果说话晚是因为我们给她的语言刺激太少了。果果不愿意睡觉，一旦睡着了，老人生怕声音大吵醒她，于是就命令我们不许出声。久而久之，果果在潜意思中得到的声音刺激、语言刺激照比其他小朋友就要少很多。为此，我们还带着果果去看了生长发育门诊、做过筛查，结果都显示果果没有任何生理性的疾病。 我们需要找到一个让果果接受更多语言刺激的方法，最终我们决定在果果2岁零5个月的时候送她去幼儿园“学说话”。做过家长的都知道，送孩子去幼儿园的过程是“痛苦”的，孩子哭闹，家长心疼，但这个过程是必须要经历的。付出了就有收获。在果果上幼儿园后的一个月，果果的“话匣子”就彻底打开了^&#95;^。 更像女娃了，但怕大海(3岁) 出生时，果果头发稀少，为了让果果长出更好的头发，我们每隔一段时间就把她的头发剃的很短(几近光头)。在2岁之内，果果更像一个“男娃”。直到3岁以后，我们开始给她留头发了。小家伙似乎也知道留头发后自己更好看了，姥姥每次给她梳头扎辫她都很喜欢。留着还有些短的头发让她更像女娃了： 图：更像女娃的果果 下半年，我们把果果转到了更大的幼儿园，并且果果每天上幼儿园都不再费劲了。她在幼儿园也学到了许多知识、技能和礼节。 3岁的果果的身体显得比同龄的女宝高大一圈，我们也开始带着她到处出行游玩。劳动节黄金周我们第一次带孩子去海边。那天的风浪比较大，浪花拍击礁石的声音震耳欲聋，果果显得很害怕。我们抱着她向海边靠近，但她却一直在挣扎并大喊：“离开、离开、走、走”。当我自己独自向大海靠近时，她也大喊：“爸爸，你回来，你回来”。见此情景，我们都哈哈大笑起来。 后来我们去了一处比较海浪比较舒缓的地带，没有了海浪拍打的巨大轰隆声，果果镇定了许多。也开始站在沙滩上和其他小朋友一起挖起沙子了。 独立爬山(4岁) 4岁的果果不仅个头高，而且壮实了。我们在权衡了之后，决定在假期带她去爬山，并且我已经做好了背她上山的准备。那次我们爬的是千山。千山在整个省内的爬山困难指数排行榜上也是名列前茅的。不过小家伙似乎很喜欢爬山，在登山栈道上显得十分兴奋，我们也给她做心里建设，希望她自己爬到顶峰。虽然在中段她也曾打过退堂鼓，但最终小家伙还是凭借自己的双腿和毅力爬上了山顶，我和老婆也都是非常惊讶。下山过程中，小家伙也是一路欢喜，并没用我们费心。只是由于累了，在回程的车上，小家伙呼呼的睡了一道。 正因为此次爬山的经历，果果爱上了爬山。后续选择旅游景点，她总是先挑那些有山可爬的地方，比如：2019年的陕西的华山、骊山等。 和妈妈一起去普吉岛(5岁) 孩子小的时候，出行很麻烦，而且孩子能收获的东西有限。5岁是一个很好的节点，她基本能自立了，而且感知和吸收外界信息的能力已经很强了。 5岁这一年是果果第一次和妈妈出国旅游，此次出行的目的是泰国普吉岛，和她们一起去的还有老婆的同事，这些同事也能帮助老婆照顾照顾果果，顺道还能锻炼一下果果的交际能力。这也是果果第一次乘飞机出行，在机场她十分兴奋。她们的航班在首尔中转，从首尔飞到普吉需要5-6个小时，这下让果果过了一把飞行瘾，她尤其喜欢飞机起降过程中的那种感觉，以至于以后每次出行，她都嚷嚷着要买多次经停或中转的航班^&#95;^。 更难得可贵的是，这次的旅游经历深深印在果果的记忆中，至今每当翻看那时的照片时，她还能头头是道的给我们讲当时发生的故事。有些事情，我老婆都已经记不得了。 上小学了(6岁) 转眼间，果果来到了6周岁，已经到了上学的年龄了。9月份，果果正式成为一名珠江街第五小学一年级的“小豆包”。和第一次上幼儿园不同，这次果果适应的很快，也没有哭鼻子的情况。反倒是回来和我们说她班级有小朋友一直哭，她还很疑惑这些小朋友为什么要哭^&#95;^。 上学后，更多的教育责任“甩”给了班主任老师，我们平时更多是帮忙批改批改作业，督促读读书，带着上上才艺班。果果的古筝是各门才艺课中学的最好的，果果也有了那么一些古典的气质： 图：有一丝古典气质的果果 这一年我还给果果开了博客。有些东西，光靠脑子是记不住的，写下来，留给多年后的自己和孩子慢慢回味。在果果能自己维护这个博客之前，我就先替她维护了。 叛逆与独立(7~9岁) 进入到7岁以后，果果受到的教育多了，读的书多了，渐渐了有了自己的主见和小脾气，再也不是那个将父母话“奉为圭臬”的小女娃了。如果就某事“辩论”，她姥姥已经完全不是对手了。也只有我和老婆偶尔还能“恩威并举”的降住她:(。 果果喜欢读故事书。她最喜欢读郑渊洁的童话，按照她的说法，市面上郑渊洁的书她基本都读完了，有些书，她已经读过不止一遍了。受郑渊洁风格的影响，她喜欢写幻想类的作文，喜欢天马行空，因此在细节描写上就差了一些。 她还喜欢“米小圈上学记”，每天晚上都是在天猫精灵播放的米小圈上学记中入睡的。 她喜欢宜家买来的老虎和小狗玩偶，一个起名为花果，一个起名为木果，每天一左一右的陪她入睡。 天猫精灵是她每天不可或缺的“伙伴”。早上听新闻早报，天气预报；晚上听故事，听历史，听音乐；偶尔还和天猫精灵玩玩互动猜谜游戏。真不愧为互联网和智能时代的原著民。 8岁的果果，其古筝考级已经通过了10级，这还是在她不是很勤奋练琴的情况下取得的。 这个阶段的果果也十分贪玩，喜欢去游乐场，玩老爸都不敢玩的惊险刺激的项目(只能由她妈妈陪着)。 9岁时，她偶尔和妈妈看了一集“家有儿女”，从那时起就“沉迷”于该剧：只要拿起iPad就必然打开“家有儿女”视频。她看电视剧和她看书的特点一样，如果某一集是她喜欢的，她会反复看上好多遍，丝毫没有不耐烦的迹象。有些集的台词她都能背下来，并粘着我和我老婆要给我们讲。还别说，讲的还头头是道的^&#95;^。 亭亭玉立的大姑娘(10岁) 由于新冠疫情的影响，果果的10岁生日在家里过的，我们也没法带她出去“玩耍”一番。10岁的她已经是一个亭亭玉立的大姑娘了。她的个头快赶上她妈妈了，大长腿，身材是“青出于蓝而胜于蓝”。 图：十岁的果果 图：亭亭玉立的果果 这次10周岁的生日除了生日蛋糕，还有一个更为特殊的礼物，那就是妈妈肚里的二宝，这也是果果一直想要的弟弟/妹妹。自从知道妈妈怀了二宝之后，果果变得更加懂事了。每天晚上对着妈妈的肚子给二宝讲故事，晚上睡觉前也会对着妈妈肚子猛“亲”几口^&#95;^。 为了留下美好回忆，我们还特地在果果十周岁生日的时候在影楼留下了一家四口的合影： 图：十岁果果生日时一家四口合影 小结 果果成长的十年给我的最大感受就是：快！一晃间，果果都10岁了，二宝也即将出生。我和老婆也即将步入中年。这里做的这个阶段性的回顾，以期若干年后当记忆模糊时还能通过这篇文章回忆起当年果果小时候的点点滴滴。 这里也希望果果在未来的人生道路中能继续一帆风顺，身体健健康康，每天快快乐乐。 希望果果和即将出生的二宝一起姐妹情深，相濡以沫，共同走好人生之路。 [...]]]></description>
			<content:encoded><![CDATA[<p>好久没有在我的博客上写关于<a href="https://daughter.tonybai.com">果果</a>的事情了，因为很多关于果果成长的经历都记录在<a href="https://daughter.tonybai.com">她自己的博客</a>中了。但今天是她<a href="https://daughter.tonybai.com/2020/05/03/i-am-10-years-old/">十周岁的生日</a>，是个值得纪念的日子。闺女成长的十年，也是我学习为人父的十年。作为父亲，我发自内心地想说点啥，是回顾，也是感受，亦有些寄语^_^。</p>
<p><img src="https://tonybai.com/wp-content/uploads/guoguo-ten-years-old/guoguo-10-years-old-2.jpg" alt="img{512x368}" /><br />
<center>图：果果成长的十年</center></p>
<h3>出生</h3>
<p>老婆在2009年7月怀上了果果。那时我们刚刚新婚不久，二人世界还没过够^&#95;^，小家伙的突然到来还让我们有些“手足无措”。为此，我们还认真地讨论了两天，最终老婆拍板：<strong>我要生下这个孩子</strong>，于是果果保住了^&#95;^。如今，每当果果提及此事，都会“发狠”地盯上我几眼，我也只能呵呵呵呵地应对^_^。</p>
<p>老婆怀胎中段，我一直在福建出差，虽然有岳母陪在老婆身边，但年底那两个月，老婆心情十分差。直到那一年大年三十的下午2点，我才在桃仙机场下的飞机，匆匆赶回家。大街上连车的打不到，多亏还有公交系统。进入家门，心里满满的都是对老婆的愧疚。记忆中老婆似乎并没有说啥，只说了一句“吃饭吧”，我顿感心里热乎乎的。</p>
<p>果果似乎很享受在妈妈子宫中待着，预产期都过了几天了，她还没有反应，直到2010年5月3日凌晨1点多，规律性的宫缩“来袭”。我们匆忙赶到医院，早上6点半多，老婆进入产室，9点多，我在产室外听到了果果呱呱坠地后的第一声啼哭。</p>
<p><img src="https://tonybai.com/wp-content/uploads/guoguo-ten-years-old/guoguo-was-born.JPG" alt="img{512x368}" /><br />
<center>图：刚出生的果果</center></p>
<p>果果出生后，恰逢徐峥的作品《人在囧途》上映，影片中徐峥扮演角色的孩子叫果果，我们觉得这个小名不错，于是便给我们宝宝起名为<strong>果果</strong>。</p>
<p>第一次照顾这么小的孩子也着实让我们这些大人手忙脚乱一段时间。出月子后，生活逐渐恢复平稳。果果每天除了吃奶就是睡觉，也算是比较省心了。稍大一些后，果果似乎并不太愿意睡觉了，每次喂完奶都得放到小车上在厅里推过来推过去哄她睡觉，后来果果姥姥买的一个能发出大恐龙吼声的玩具也加入到促进果果睡眠的行列中^&#95;^。</p>
<h3>第一次走路(0岁)</h3>
<p>果果一直母乳喂养，身体也很壮实。抬头、翻身、学会爬行和其他孩子相比都略有提前。最让我印象深刻是她第一次学会独立行走。记得那是2010年农历春节前，我们家当时的供暖非常好，室温在28度以上。果果在家只穿一套内衣裤，因此行动和玩耍起来非常方便。之前果果就可以扶着床沿踱步了并且她似乎也很喜欢站起来的感觉。那天她自己在卧室的地板上玩耍，我在门口偷偷观察她。她玩了一会儿就开始扶着床站起来，看我站在门口，她居然放开了扶着床沿的双手，向我摇摇晃晃地走了过来。从床边到卧室门口大约有不到2米的距离，我也兴奋地张开双臂，引导她向我走来。她边走边兴奋地笑，似乎也在惊讶于自己能独立行走了。在她扑到我的怀中的那一刻，我才意识到我见证了果果人生第一次独立行走，老婆听到这个消息也是兴奋不已。自从果果学会了走路，此后便一发不可收拾^&#95;^。</p>
<h3>打针不哭(1岁)</h3>
<p>可能是因为母乳喂养，果果在一岁的这一年中没有患过任何感冒发烧的病症。但当母体带给她的免疫力逐渐失去作用后，果果便和其他小朋友一样，会得感冒，也会发烧。记得果果第一次感冒(刚过一岁生日没多久)就烧的特别厉害。由于我们也是第一次遇到这种情况，特别担心，于是就带她去医院检查。虽然我们期望医生开立口服药，但医生最终还是开了点滴。在护士站门口，果果看到其他孩子扎针时都哇哇的哭，心里也胆怯了。果不其然，当针头穿透她的皮肤进入血管的那一刻，果果也像其他孩子一样，哇哇大哭起来。</p>
<p>有了这次“痛苦”的扎针经历后，我们也对她进行了心理疏导，教她要学会坚强，从她的眼神看得出来，她似乎听懂了。在2011年的秋冬换季，果果又患感冒发烧。同样去了医院，同样开了点滴，但在护士站扎针的时候，果果居然很坚强的忍住了，没有哇哇的哭泣，这让看惯了孩子痛哭的护士也是惊奇不已。看着泪水在眼圈里打转但没有哭出来的果果，我们心里更是心疼她了。</p>
<h3>送去幼儿园学说话(2岁)</h3>
<p>果果在11个月的时候就会喊妈妈了，但直到2岁半她能吐出的字依然只有“妈妈”，偶尔也有“爸爸”。现在看来，果果说话晚是因为我们给她的语言刺激太少了。果果不愿意睡觉，一旦睡着了，老人生怕声音大吵醒她，于是就命令我们不许出声。久而久之，果果在潜意思中得到的声音刺激、语言刺激照比其他小朋友就要少很多。为此，我们还带着果果去看了生长发育门诊、做过筛查，结果都显示果果没有任何生理性的疾病。</p>
<p>我们需要找到一个让果果接受更多语言刺激的方法，最终我们决定在果果2岁零5个月的时候送她去幼儿园“学说话”。做过家长的都知道，送孩子去幼儿园的过程是“痛苦”的，孩子哭闹，家长心疼，但这个过程是必须要经历的。付出了就有收获。在果果上幼儿园后的一个月，果果的“话匣子”就彻底打开了^&#95;^。</p>
<h3>更像女娃了，但怕大海(3岁)</h3>
<p>出生时，果果头发稀少，为了让果果长出更好的头发，我们每隔一段时间就把她的头发剃的很短(几近光头)。在2岁之内，果果更像一个“男娃”。直到3岁以后，我们开始给她留头发了。小家伙似乎也知道留头发后自己更好看了，姥姥每次给她梳头扎辫她都很喜欢。留着还有些短的头发让她更像女娃了：</p>
<p><img src="https://tonybai.com/wp-content/uploads/guoguo-ten-years-old/guoguo-3-years-old.jpg" alt="img{512x368}" /><br />
<center>图：更像女娃的果果</center></p>
<p>下半年，我们把果果转到了更大的幼儿园，并且果果每天上幼儿园都不再费劲了。她在幼儿园也学到了许多知识、技能和礼节。</p>
<p>3岁的果果的身体显得比同龄的女宝高大一圈，我们也开始带着她到处出行游玩。劳动节黄金周我们第一次带孩子去海边。那天的风浪比较大，浪花拍击礁石的声音震耳欲聋，果果显得很害怕。我们抱着她向海边靠近，但她却一直在挣扎并大喊：“离开、离开、走、走”。当我自己独自向大海靠近时，她也大喊：“爸爸，你回来，你回来”。见此情景，我们都哈哈大笑起来。</p>
<p>后来我们去了一处比较海浪比较舒缓的地带，没有了海浪拍打的巨大轰隆声，果果镇定了许多。也开始站在沙滩上和其他小朋友一起挖起沙子了。</p>
<h3>独立爬山(4岁)</h3>
<p>4岁的果果不仅个头高，而且壮实了。我们在权衡了之后，决定在假期带她去爬山，并且我已经做好了背她上山的准备。那次我们爬的是千山。千山在整个省内的爬山困难指数排行榜上也是名列前茅的。不过小家伙似乎很喜欢爬山，在登山栈道上显得十分兴奋，我们也给她做心里建设，希望她自己爬到顶峰。虽然在中段她也曾打过退堂鼓，但最终小家伙还是凭借自己的双腿和毅力爬上了山顶，我和老婆也都是非常惊讶。下山过程中，小家伙也是一路欢喜，并没用我们费心。只是由于累了，在回程的车上，小家伙呼呼的睡了一道。</p>
<p>正因为此次爬山的经历，果果爱上了爬山。后续选择旅游景点，她总是先挑那些有山可爬的地方，比如：2019年的<a href="https://daughter.tonybai.com/2019/08/17/xian-chongqing-tour-2nd-day-2019/">陕西的华山</a>、<a href="https://daughter.tonybai.com/2019/08/17/xian-chongqing-tour-2nd-day-2019/">骊山</a>等。</p>
<h3>和妈妈一起去普吉岛(5岁)</h3>
<p>孩子小的时候，出行很麻烦，而且孩子能收获的东西有限。5岁是一个很好的节点，她基本能自立了，而且感知和吸收外界信息的能力已经很强了。</p>
<p>5岁这一年是果果第一次和妈妈出国旅游，此次出行的目的是泰国普吉岛，和她们一起去的还有老婆的同事，这些同事也能帮助老婆照顾照顾果果，顺道还能锻炼一下果果的交际能力。这也是果果第一次乘飞机出行，在机场她十分兴奋。她们的航班在首尔中转，从首尔飞到普吉需要5-6个小时，这下让果果过了一把飞行瘾，她尤其喜欢飞机起降过程中的那种感觉，以至于以后每次出行，她都嚷嚷着要买多次经停或中转的航班^&#95;^。</p>
<p>更难得可贵的是，这次的旅游经历深深印在果果的记忆中，至今每当翻看那时的照片时，她还能头头是道的给我们讲当时发生的故事。有些事情，我老婆都已经记不得了。</p>
<h3>上小学了(6岁)</h3>
<p>转眼间，果果来到了6周岁，已经到了上学的年龄了。9月份，果果正式成为一名珠江街第五小学一年级的“小豆包”。和第一次上幼儿园不同，这次果果适应的很快，也没有哭鼻子的情况。反倒是回来和我们说她班级有小朋友一直哭，她还很疑惑这些小朋友为什么要哭^&#95;^。</p>
<p>上学后，更多的教育责任“甩”给了班主任老师，我们平时更多是帮忙批改批改作业，督促读读书，带着上上才艺班。果果的古筝是各门才艺课中学的最好的，果果也有了那么一些古典的气质：</p>
<p><img src="https://tonybai.com/wp-content/uploads/guoguo-ten-years-old/guoguo-gavatar.jpg" alt="img{512x368}" /><br />
<center>图：有一丝古典气质的果果</center></p>
<p>这一年我还给果果开了<a href="https://daughter.tonybai.com">博客</a>。有些东西，光靠脑子是记不住的，写下来，留给多年后的自己和孩子慢慢回味。在果果能自己维护这个博客之前，我就先替她维护了。</p>
<h3>叛逆与独立(7~9岁)</h3>
<p>进入到7岁以后，果果受到的教育多了，读的书多了，渐渐了有了自己的主见和小脾气，再也不是那个将父母话“奉为圭臬”的小女娃了。如果就某事“辩论”，她姥姥已经完全不是对手了。也只有我和老婆偶尔还能“恩威并举”的降住她:(。</p>
<p>果果喜欢读故事书。她最喜欢读郑渊洁的童话，按照她的说法，市面上郑渊洁的书她基本都读完了，有些书，她已经读过不止一遍了。受郑渊洁风格的影响，她喜欢写幻想类的作文，喜欢天马行空，因此在细节描写上就差了一些。</p>
<p>她还喜欢“米小圈上学记”，每天晚上都是在天猫精灵播放的米小圈上学记中入睡的。</p>
<p>她喜欢宜家买来的老虎和小狗玩偶，一个起名为花果，一个起名为木果，每天一左一右的陪她入睡。</p>
<p>天猫精灵是她每天不可或缺的“伙伴”。早上听新闻早报，天气预报；晚上听故事，听历史，听音乐；偶尔还和天猫精灵玩玩互动猜谜游戏。真不愧为互联网和智能时代的原著民。</p>
<p>8岁的果果，其古筝考级已经通过了10级，这还是在她不是很勤奋练琴的情况下取得的。</p>
<p>这个阶段的果果也十分贪玩，喜欢去游乐场，玩老爸都不敢玩的惊险刺激的项目(只能由她妈妈陪着)。</p>
<p>9岁时，她偶尔和妈妈看了一集“家有儿女”，从那时起就“沉迷”于该剧：只要拿起iPad就必然打开“家有儿女”视频。她看电视剧和她看书的特点一样，如果某一集是她喜欢的，她会反复看上好多遍，丝毫没有不耐烦的迹象。有些集的台词她都能背下来，并粘着我和我老婆要给我们讲。还别说，讲的还头头是道的^&#95;^。</p>
<h3>亭亭玉立的大姑娘(10岁)</h3>
<p>由于新冠疫情的影响，果果的10岁生日在家里过的，我们也没法带她出去“玩耍”一番。10岁的她已经是一个亭亭玉立的大姑娘了。她的个头快赶上她妈妈了，大长腿，身材是“青出于蓝而胜于蓝”。</p>
<p><img src="https://tonybai.com/wp-content/uploads/guoguo-ten-years-old/guoguo-10-years-old-1.jpg" alt="img{512x368}" /><br />
<center>图：十岁的果果</center></p>
<p><img src="https://tonybai.com/wp-content/uploads/guoguo-ten-years-old/guoguo-10-years-old-4.jpg" alt="img{512x368}" /><br />
<center>图：亭亭玉立的果果</center></p>
<p>这次10周岁的生日除了生日蛋糕，还有一个更为特殊的礼物，那就是妈妈肚里的二宝，这也是果果一直想要的弟弟/妹妹。自从知道妈妈怀了二宝之后，果果变得更加懂事了。每天晚上对着妈妈的肚子给二宝讲故事，晚上睡觉前也会对着妈妈肚子猛“亲”几口^&#95;^。</p>
<p>为了留下美好回忆，我们还特地在果果十周岁生日的时候在影楼留下了一家四口的合影：</p>
<p><img src="https://tonybai.com/wp-content/uploads/guoguo-ten-years-old/guoguo-10-years-old-3.jpg" alt="img{512x368}" /><br />
<center>图：十岁果果生日时一家四口合影</center></p>
<h3>小结</h3>
<p>果果成长的十年给我的最大感受就是：<strong>快</strong>！一晃间，果果都10岁了，二宝也即将出生。我和老婆也即将步入中年。这里做的这个阶段性的回顾，以期若干年后当记忆模糊时还能通过这篇文章回忆起当年果果小时候的点点滴滴。</p>
<p>这里也希望果果在未来的人生道路中能继续一帆风顺，身体健健康康，每天快快乐乐。</p>
<p>希望果果和即将出生的二宝一起姐妹情深，相濡以沫，共同走好人生之路。</p>
<p>最后，<strong>人生在于经历，而不在于得失</strong>。</p>
<hr />
<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 style='text-align:left'>&copy; 2020, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2020/05/03/guoguo-ten-years-old/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>源创会2017沈阳站讲稿：基于Harbor的高可用企业级私有容器镜像仓库部署实践</title>
		<link>https://tonybai.com/2017/10/23/the-speech-script-practice-on-deploying-a-ha-harbor-cluster-for-osc-shenyang-2017/</link>
		<comments>https://tonybai.com/2017/10/23/the-speech-script-practice-on-deploying-a-ha-harbor-cluster-for-osc-shenyang-2017/#comments</comments>
		<pubDate>Mon, 23 Oct 2017 08:28:35 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[AD]]></category>
		<category><![CDATA[beego]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[CD]]></category>
		<category><![CDATA[Ceph]]></category>
		<category><![CDATA[CephFS]]></category>
		<category><![CDATA[cgroups]]></category>
		<category><![CDATA[CI]]></category>
		<category><![CDATA[daemon.json]]></category>
		<category><![CDATA[distribution]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[docker-compose]]></category>
		<category><![CDATA[docker-registry]]></category>
		<category><![CDATA[dotCloud]]></category>
		<category><![CDATA[DX]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[GroupReplication]]></category>
		<category><![CDATA[harbor]]></category>
		<category><![CDATA[High-Available]]></category>
		<category><![CDATA[image]]></category>
		<category><![CDATA[Kernel]]></category>
		<category><![CDATA[KVM]]></category>
		<category><![CDATA[LDAP]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[linux-container]]></category>
		<category><![CDATA[loadbalance]]></category>
		<category><![CDATA[LXC]]></category>
		<category><![CDATA[MGR]]></category>
		<category><![CDATA[mount]]></category>
		<category><![CDATA[MVC]]></category>
		<category><![CDATA[MySQL]]></category>
		<category><![CDATA[namespaces]]></category>
		<category><![CDATA[osc]]></category>
		<category><![CDATA[pipeline]]></category>
		<category><![CDATA[portus]]></category>
		<category><![CDATA[redis]]></category>
		<category><![CDATA[registry]]></category>
		<category><![CDATA[Scaling]]></category>
		<category><![CDATA[SUSE]]></category>
		<category><![CDATA[unionfs]]></category>
		<category><![CDATA[vm]]></category>
		<category><![CDATA[vmware]]></category>
		<category><![CDATA[Xen]]></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=2427</guid>
		<description><![CDATA[上周六开源中国的源创会在沈阳举办了一次技术活动，很荣幸以本地讲师的身份和大家交流了一个topic: “基于Harbor的高可用企业级私有容器镜像仓库部署实践”。之所以选择这个topic，是因为这是我们团队的项目实践心得。很多企业和组织在深入使用Docker之后，都会有类似的高可用私有容器仓库搭建的需求，于是我就把我们摸索的实践和填坑过程拿出来，用30分钟与大家分享一下。另外这算是一个入门级的分享，并未深入过多原理。以下就是本次分享的内容讲稿整理。如有不妥或不正确的地方，欢迎交流指正。 大家下午好，欢迎各位来到源创会沈阳站。在这里我也代表沈阳的IT人欢迎源创会来到沈阳，希望能有更多的像源创会这样的组织到沈阳举办技术活动。非常高兴能有这个机会在源创会这个平台上做分享， 今天和大家一起探讨的题目是：“基于Harbor的高可用企业级私有容器镜像仓库部署实践”。题目有些长，简单来说就是如何搭建一个好用的镜像仓库。 首先做个简单的自我介绍。我叫白明，东软(注：源创会这次活动的会场在东软沈阳园区)是我的主场，在这里工作很多年，目前就职东软云科技；Gopher一枚，近两年主要使用Go语言开发；技术译者，曾参与翻译过《七周七语言》一书；并且参与过智慧城市架构系列丛书的编著工作；GopherChina大会讲师，这里顺便说一下GopherChina大会，它是目前中国地区规模最大、水平最高的Go语言技术大会，一般每年4月份在北京或上海举行。希望有志于Go语言开发的开发者积极参与；Blogger，写博10多年，依旧笔耕不倦；目前主要从事Docker&#38;kubernetes的研究和实践。 当今，IT技术发展飞快。五年前， IT从业者口中谈论最多的技术是Virtual Machine，即虚拟化技术，人们经常争论的是到底是vmware的技术好,还是原生kvm技术稳定，又或是xen的技术完美。转眼间五年过去了，大家口中经常讨论的技术词汇发生了变化，越来越多的技术人在谈论Docker，谈论容器。 Docker是什么？ Docker这门技术非常热，但我们要透过现象看其本质： Docker技术并不是新技术，而是将已有技术进行了更好的整合和包装。 内核容器技术以一种完整形态最早出现在Sun公司的Solaris操作系统上，Solaris是当时最先进的服务器操作系统。2005年Solaris发布Solaris Container技术，从此开启了内核容器之门。 IT技术发展的趋势就是这样：商业有的，开源也要有。三年后，即2008年，以Google公司开发人员为主导的Linux Container，LXC功能在被merge到Linux内核。LXC是一种内核级虚拟化技术，主要基于namespaces和cgroup技术，实现共享一个os kernel前提下的进程资源隔离，为进程提供独立的虚拟执行环境，这样的一个虚拟的执行环境就是一个容器。本质上说，LXC容器与现在的Docker所提供容器是一样的。但是，当时LXC处于早期阶段，开发人员可能更为关注LXC的技术实现，而对开发体验方面有所忽略，导致LXC技术使用门槛较高，普通应用开发者学习、理解和使用它的心智负担较高，因此应用并不广泛。 这一情况一直持续到2013年，当时美国一家名不见经传的公司dotCloud发布了一款平台工具Docker，对外宣称可以实现：“build,ship and run any app and anywhere”。Docker实质上也是基于namespaces和cgroup技术的，Docker的创新之处在于其基于union fs技术定义了一套应用打包规范，真正将应用及其运行的所有依赖都封装到一个特定格式的文件中，这种文件就被称为image，即镜像文件。同时，Docker还提供了一套抽象层次更高的工具集，这套工具对dev十分友好，具有良好的开发体验(Developer eXperience)，开发者无需关心namespace, cgroups之类底层技术，即可很easy的启动一个承载着其应用的容器： Docker run ubuntu echo hello 因此， 从2013发布以来，Docker项目就像坐上了火箭，发展迅猛，目前已经是github上最火爆的开源项目之一。这里还要提一点就是：Docker项目是使用go语言开发的，Docker项目的成功，也或多或少得益于Go优异的开发效率和执行效率。 Docker技术的出现究竟给我们带来了哪些好处呢，个人觉得至少有以下三点： 交付标准化：Docker使得应用程序和依赖的运行环境真正绑定结合为一体，得之即用。这让开发人员、测试和运维实现了围绕同一交付物，保持开发交付上下文同步的能力，即“test what you write, ship what you test”； 执行高效化：应用的启动速度从原先虚拟机的分钟级缩短到容器的秒级甚至ms级，使得应用可以支持快速scaling伸缩； 资源集约化：与vm不同的是，Container共享一个内核，这使得一个container的资源消耗仅为进程级别或进程组级别。同时，容器的镜像也因为如此，其size可以实现的很小，最小可能不足1k，平均几十M。与vm动辄几百兆的庞大身段相比，具有较大优势。 有了image文件后，自然而言我们就有了对image进行存取和管理的需求，即我们需要一个镜像仓库，于是Docker推出了Docker registry这个项目。Docker Registry就是Docker image的仓库，用来存储、管理和分发image的；Docker registry由Docker公司实现，项目名为distribution，其实现了Docker Registr 2.0协议，与早前的Registry 1.x协议版本相比，Distribution采用Go语言替换了Python，在安全性和性能方面都有了大幅提升；Docker官方运行着一个世界最大的公共镜像仓库：hub.docker.com，最常用的image都在hub上，比如反向代理nginx、redis、ubuntu等。鉴于国内访问hub网速不佳，多使用国内容器服务厂商提供的加速器。Docker官方还将Registry本身打入到了一个image中，方便开发人员快速以容器形式启动一个Registry： docker run -d [...]]]></description>
			<content:encoded><![CDATA[<p>上周六<a href="http://www.oschina.net/">开源中国</a>的<a href="https://www.oschina.net/event/ych">源创会</a>在沈阳举办了一次技术活动，很荣幸以本地讲师的身份和大家交流了一个topic: “基于<a href="https://github.com/vmware/harbor">Harbor</a>的高可用企业级私有容器镜像仓库部署实践”。之所以选择这个topic，是因为这是我们团队的项目实践心得。很多企业和组织在深入使用Docker之后，都会有类似的高可用私有容器仓库搭建的需求，于是我就把我们摸索的实践和填坑过程拿出来，用30分钟与大家分享一下。另外这算是一个入门级的分享，并未深入过多原理。以下就是本次分享的内容讲稿整理。如有不妥或不正确的地方，欢迎交流指正。</p>
<p><img src="http://tonybai.com/wp-content/uploads/osc-shenyang-2017-1.jpg" alt="img{512x368}" /></p>
<p>大家下午好，欢迎各位来到源创会沈阳站。在这里我也代表沈阳的IT人欢迎源创会来到沈阳，希望能有更多的像源创会这样的组织到沈阳举办技术活动。非常高兴能有这个机会在源创会这个平台上做分享， 今天和大家一起探讨的题目是：“<a href="https://github.com/bigwhite/talks/tree/master/osc/2017">基于Harbor的高可用企业级私有容器镜像仓库部署实践</a>”。题目有些长，简单来说就是如何搭建一个好用的镜像仓库。</p>
<p><img src="http://tonybai.com/wp-content/uploads/osc-shenyang-2017-2.jpg" alt="img{512x368}" /></p>
<p>首先做个简单的自我介绍。我叫白明，东软(注：源创会这次活动的会场在东软沈阳园区)是我的主场，在这里工作很多年，目前就职东软云科技；<a href="https://golang.org/">Gopher</a>一枚，近两年主要使用Go语言开发；技术译者，曾参与翻译过《<a href="https://book.douban.com/subject/10555435/">七周七语言</a>》一书；并且参与过智慧城市架构系列丛书的编著工作；<a href="http://tonybai.com/2017/04/18/my-experience-of-gopherchina-2017-as-a-speaker/">GopherChina大会讲师</a>，这里顺便说一下<a href="http://gopherchina.org/">GopherChina大会</a>，它是目前中国地区规模最大、水平最高的Go语言技术大会，一般每年4月份在北京或上海举行。希望有志于<a href="http://tonybai.com/tag/go">Go语言</a>开发的开发者积极参与；Blogger，写博10多年，依旧笔耕不倦；目前主要从事<a href="http://tonybai.com/tag/docker">Docker</a>&amp;<a href="http://tonybai.com/tag/kubernetes">kubernetes</a>的研究和实践。</p>
<p>当今，IT技术发展飞快。五年前， IT从业者口中谈论最多的技术是<a href="https://en.wikipedia.org/wiki/Virtual_machine">Virtual Machine</a>，即虚拟化技术，人们经常争论的是到底是vmware的技术好,还是原生<a href="https://en.wikipedia.org/wiki/Kernel-based_Virtual_Machine">kvm</a>技术稳定，又或是<a href="https://en.wikipedia.org/wiki/Xen">xen</a>的技术完美。转眼间五年过去了，大家口中经常讨论的技术词汇发生了变化，越来越多的技术人在谈论<a href="https://en.wikipedia.org/wiki/Docker_%28software%29">Docker</a>，谈论容器。</p>
<p>Docker是什么？ Docker这门技术非常热，但我们要透过现象看其本质：</p>
<blockquote>
<p><strong>Docker技术并不是新技术，而是将已有技术进行了更好的整合和包装</strong>。</p>
</blockquote>
<p>内核容器技术以一种完整形态最早出现在<a href="https://en.wikipedia.org/wiki/Sun_Microsystems">Sun公司</a>的<a href="http://en.wikipedia.org/wiki/Solaris_(operating_system)">Solaris操作系统</a>上，Solaris是当时最先进的服务器操作系统。2005年Solaris发布<a href="https://en.wikipedia.org/wiki/Solaris_Containers">Solaris Container</a>技术，从此开启了内核容器之门。</p>
<p>IT技术发展的趋势就是这样：商业有的，开源也要有。三年后，即2008年，以Google公司开发人员为主导的<a href="https://en.wikipedia.org/wiki/LXC">Linux Container，LXC功能</a>在被merge到<a href="https://en.wikipedia.org/wiki/Linux_kernel">Linux内核</a>。LXC是一种内核级虚拟化技术，主要基于<a href="https://en.wikipedia.org/wiki/Linux_namespaces">namespaces</a>和<a href="https://en.wikipedia.org/wiki/Cgroups">cgroup</a>技术，实现共享一个os kernel前提下的进程资源隔离，为进程提供独立的虚拟执行环境，这样的一个虚拟的执行环境就是一个容器。本质上说，LXC容器与现在的Docker所提供容器是一样的。但是，当时LXC处于早期阶段，开发人员可能更为关注LXC的技术实现，而对开发体验方面有所忽略，导致LXC技术使用门槛较高，普通应用开发者学习、理解和使用它的心智负担较高，因此应用并不广泛。</p>
<p>这一情况一直持续到2013年，当时美国一家名不见经传的公司<a href="https://en.wikipedia.org/wiki/DotCloud">dotCloud</a>发布了一款平台工具Docker，对外宣称可以实现：“build,ship and run any app and anywhere”。Docker实质上也是基于namespaces和cgroup技术的，Docker的创新之处在于其基于<a href="https://en.wikipedia.org/wiki/Category:Union_file_systems">union fs技术</a>定义了一套应用打包规范，真正将应用及其运行的所有依赖都封装到一个特定格式的文件中，这种文件就被称为image，即镜像文件。同时，Docker还提供了一套抽象层次更高的工具集，这套工具对dev十分友好，具有良好的开发体验(Developer eXperience)，开发者无需关心namespace, cgroups之类底层技术，即可很easy的启动一个承载着其应用的容器：</p>
<pre><code>Docker run ubuntu echo hello
</code></pre>
<p>因此， 从2013发布以来，Docker项目就像坐上了火箭，发展迅猛，目前已经是github上最火爆的开源项目之一。这里还要提一点就是：Docker项目是使用go语言开发的，Docker项目的成功，也或多或少得益于Go优异的开发效率和执行效率。</p>
<p>Docker技术的出现究竟给我们带来了哪些好处呢，个人觉得至少有以下三点：</p>
<ul>
<li>交付标准化：Docker使得应用程序和依赖的运行环境真正绑定结合为一体，得之即用。这让开发人员、测试和运维实现了围绕同一交付物，保持开发交付上下文同步的能力，即“test what you write, ship what you test”；</li>
<li>执行高效化：应用的启动速度从原先虚拟机的分钟级缩短到容器的秒级甚至ms级，使得应用可以支持快速scaling伸缩；</li>
<li>资源集约化：与vm不同的是，Container共享一个内核，这使得一个container的资源消耗仅为进程级别或进程组级别。同时，容器的镜像也因为如此，其size可以实现的很小，最小可能不足1k，平均几十M。与vm动辄几百兆的庞大身段相比，具有较大优势。</li>
</ul>
<p>有了image文件后，自然而言我们就有了对image进行存取和管理的需求，即我们需要一个镜像仓库，于是Docker推出了Docker registry这个项目。Docker Registry就是Docker image的仓库，用来存储、管理和分发image的；Docker registry由Docker公司实现，项目名为<a href="https://github.com/docker/distribution">distribution</a>，其实现了Docker Registr 2.0协议，与早前的Registry 1.x协议版本相比，Distribution采用Go语言替换了Python，在安全性和性能方面都有了大幅提升；Docker官方运行着一个世界最大的公共镜像仓库：hub.docker.com，最常用的image都在hub上，比如反向代理nginx、redis、ubuntu等。鉴于国内访问hub网速不佳，多使用国内容器服务厂商提供的加速器。Docker官方还将Registry本身打入到了一个image中，方便开发人员快速以容器形式启动一个Registry：</p>
<pre><code>docker run -d -p 5000:5000 --restart=always --name registry registry:2
</code></pre>
<p>不过，这样启动的Registry更多仅仅是一个Demo级别或满足个体开发者自身需要的，离满足企业内部开发流程或生产需求还差了许多。</p>
<p>既然Docker官方运行着免费的镜像仓库，那我们还需要自己搭建吗？实际情况是，对Docker的使用越深入，对私有仓库的需求可能就越迫切。我们先来看一组Docker 2016官方的调查数据，看看Docker都应用在哪些场合。 从Docker 2016官方调查来看，Docker 更多用于dev、<a href="https://en.wikipedia.org/wiki/Continuous_integration">ci</a>和<a href="https://en.wikipedia.org/wiki/DevOps">DevOps</a>等环节，这三个场合下的应用占据了半壁江山。而相比于公共仓库，私有镜像仓库能更好的满足开发人员在这些场合对镜像仓库的需求。理由至少有四点：</p>
<ul>
<li>
<p>便于集成到内部CI/Cd<br />
以我司内部为例，由于公司内部办公需要使用正向代理访问外部网络，要想将Public Registry集成到你的内部CI中，技术上就会有很多坎儿，整个搭建过程可能是非常痛苦的；</p>
</li>
<li>
<p>对镜像可以更全面掌控<br />
一般来说，外部Public Registry提供的管理功能相对单一，往往无法满足企业内部的开发和交付需求；</p>
</li>
<li>
<p>内部网络，网络传输性能更好<br />
内部开发运维流水线很多环节是有一定的时间敏感性的，比如：一次CI如果因为network问题导致image pull总是timeout，会让dev非常闹心，甚至影响整体的开发和交付效率。</p>
</li>
<li>
<p>出于安全考虑<br />
总是有企业不想将自己开发的软件或数据放到公网上，因此在企业内部选择搭建一个private registry更会让这些企业得到满足；另外企业对仓库的身份验证可能还有LDAP支持的需求，这是外部registry无法满足的。</p>
</li>
</ul>
<p>一旦企业决定搭建自己的private仓库，那么就得做一个private仓库的技术选型。商业版不在我们讨论范围内，我们从开源软件中挑选。不过开源的可选的不多，Docker 官方的Registry更聚焦通用功能，没有针对企业客户需求定制，开源领域我们大致有两个主要候选者：<a href="https://github.com/SUSE/">SUSE</a>的<a href="https://github.com/SUSE/Portus">Portus</a>和Vmware的<a href="https://github.com/vmware/harbor">Harbor</a>。针对开源项目的技术选型，我个人的挑选原则最简单的就是看社区生态，落实到具体的指标上包括：</p>
<ul>
<li>项目关注度（即star数量）</li>
<li>社区对issue的反馈数量和积极性</li>
<li>项目维护者对issue fix的积极程度以及是否有远大的roadmap</li>
</ul>
<p>对比后，我发现在这三个指标上，目前Harbor都暂时领先portus一段距离，于是我们选择Harbor。</p>
<p>Harbor是VMware中国团队开源的企业级镜像仓库项目，聚焦镜像仓库的企业级需求，这里从其官网摘录一些特性，大家一起来看一下：</p>
<p>– 支持基于角色的访问控制RBAC;<br />
– 支持镜像复制策略(PUSH);<br />
– 支持无用镜像数据的自动回收和删除; – 支持LDAP/AD认证;<br />
– Web UI;<br />
– 提供审计日志功能;<br />
– 提供RESTful API,便于扩展;<br />
– 支持中文&amp;部署Easy。</p>
<p>不过，Harbor默认安装的是单实例仓库，并非是<a href="https://en.wikipedia.org/wiki/High_availability">高可用的</a>。对于接纳和使用Docker的企业来说，镜像仓库已经企业内部开发、交付和运维流水线的核心，一旦仓库停掉，流水线将被迫暂停，对开发交付的效率会产生重要影响；对于一些中大型企业组织，单实例的仓库性能也无法满足需求，为此高可用的Harbor势在必行。在设计Harbor HA方案之前，我们简单了解一下Harbor组成架构。</p>
<p>一个Harbor实例就是一组由<a href="https://github.com/docker/compose">docker-compose</a>工具启动的容器服务，主要包括四个主要组件：</p>
<ul>
<li>
<p>proxy<br />
实质就是一个反向代理<a href="http://tonybai.com/tag/nginx">nginx</a>，负责流量路由分担到ui和registry上；</p>
</li>
<li>
<p>registry<br />
这里的registry就是原生的docker官方的registry镜像仓库，Harbor在内部内置了一个仓库，所有仓库的核心功能均是由registry完成的；</p>
</li>
<li>
<p>core service<br />
包含了ui、token和webhook服务；</p>
</li>
<li>
<p>job service<br />
主要用于镜像复制供。</p>
</li>
</ul>
<p>同时，每个Harbor实例还启动了一个MySQL数据库容器，用于保存自身的配置和镜像管理相关的关系数据。</p>
<p>高可用系统一般考虑三方面：计算高可用、存储高可用和网络高可用。在这里我们不考虑网络高可用。基于Harbor的高可用仓库方案，这里列出两个。</p>
<p><img src="http://tonybai.com/wp-content/uploads/harbor-ha-solutions.png" alt="img{512x368}" /></p>
<p>两个方案的共同点是计算高可用，都是通过lb实现的多主热运行，保证无单点；存储高可用则各有各的方案。一个使用了分布式共享存储，数据可靠性由共享存储provider提供；另外一个则需要harbor自身逻辑参与，通过镜像相互复制的方式保持数据的多副本。</p>
<p>两种方案各有优缺点，就看哪种更适合你的组织以及你手里的资源是否能满足方案的搭建要求。</p>
<p>方案1是Harbor开发团队推荐的标准方案，由于基于分布式共享存储，因此其scaling非常好；同样，由于多Harbor实例共享存储，因此可以保持数据是实时一致的。方案1的不足也是很明显的，第一：门槛高，需要具备共享存储provider；第二搭建难度要高于第二个基于镜像复制的方案。</p>
<p>方案2的优点就是首次搭建简单。不足也很多：scaling差，甚至是不能，一旦有三个或三个以上节点，可能就会出现“环形复制”；镜像复制需要时间，因此存在多节点上数据周期性不一致的情况；Harbor的镜像复制规则以Project为单位配置，因此一旦新增Project，需要在每个节点上手工维护复制规则，非常繁琐。因此，我们选择方案1。</p>
<p>我们来看一下方案1的细节： 这是一幅示意图。</p>
<ul>
<li>每个安放harbor实例的node都mount cephfs。ceph是目前最流行的分布式共享存储方案之一；</li>
<li>每个node上的harbor实例（包含组件：ui、registry等）都volume mount node上的cephfs mount路径；</li>
<li>通过Load Balance将request流量负载到各个harbor实例上；</li>
<li>使用外部MySQL cluster替代每个Harbor实例内部自维护的那个MySQL容器；对于MySQL cluster，可以使用<a href="http://galeracluster.com/products/">mysql galera cluster</a>或MySQL5.7以上版本自带的Group Replication (MGR) 集群。</li>
<li>通过外部Redis实现访问Harbor ui的session共享，这个功能是Harbor UI底层MVC框架-<a href="https://github.com/astaxie/beego">beego</a>提供的。</li>
</ul>
<p>接下来，我们就来看具体的部署步骤和细节。</p>
<p>环境和先决条件：</p>
<ul>
<li>三台VM(Ubuntu 16.04及以上版本)；</li>
<li><a href="http://tonybai.com/2016/11/07/integrate-kubernetes-with-ceph-rbd/">CephFS</a>、MySQL、Redis已就绪；</li>
<li>Harbor v1.1.0及以上版本；</li>
<li>一个域名：hub.tonybai.com:8070。我们通过该域名和服务端口访问Harbor，我们可以通过dns解析多ip轮询实现最简单的Load balance，虽然不完美。</li>
</ul>
<h3>第一步：挂载cephfs</h3>
<p>每个安装Harbor instance的节点都要mount cephfs的相关路径，步骤包括：</p>
<pre><code>#安装cephfs内核驱动
apt install ceph-fs-common

# 修改/etc/fstab，添加挂载指令，保证节点重启依旧可以自动挂载cephfs
xx.xx.xx.xx:6789:/apps/harbor /mnt/cephfs/harbor ceph name=harbor,secretfile=/etc/ceph/a dmin.secret,noatime,_netdev 0 2
</code></pre>
<p>这里涉及一个密钥文件admin.secret，这个secret文件可以在ceph集群机器上使用ceph auth tool生成。</p>
<p><img src="http://tonybai.com/wp-content/uploads/harbor-prepare-process.png" alt="img{512x368}" /></p>
<p>前面提到过每个Harbor实例都是一组容器服务，这组容器启动所需的配置文件是在Harbor正式启动前由prepare脚本生成的，Prepare脚本生成过程的输入包括：harbor.cfg、docker-compose.yml和common/templates下的配置模板文件。这也是部署高可用Harbor的核心步骤，我们逐一来看。</p>
<h3>第二步：修改harbor.cfg</h3>
<p>我们使用域名访问Harbor，因此我们需要修改hostname配置项。注意如果要用域名访问，这里一定填写域名，否则如果这里使用的是Harbor node的IP，那么在后续会存在client端和server端仓库地址不一致的情况；</p>
<p>custom_crt=false 关闭 crt生成功能。注意：三个node关闭其中两个，留一个生成一套数字证书和私钥。</p>
<h3>第三步：修改docker-compose.yml</h3>
<p>docker-compose.yml是docker-compose工具标准配置文件，用于配置docker-compose即将启动的容器服务。针对该配置文件，我们主要做三点修改：</p>
<ul>
<li>修改volumes路径<br />
由/data/xxx 改为：/mnt/cephfs/harbor/data/xxx</li>
<li>由于使用外部Mysql，因此需要删除mysql service以及其他 service对mysql service的依赖 (depends_on)</li>
<li>修改对proxy外服务端口 ports:  8070:80</li>
</ul>
<h3>第四步：配置访问external mysql和redis</h3>
<p>external mysql的配置在common/templates/adminserver/env中，我们用external Mysql的访问方式覆盖下面四项配置：</p>
<pre><code>MYSQL_HOST=harbor_host
MYSQL_PORT=3306
MYSQL_USR=harbor
MYSQL_PWD=harbor_password

</code></pre>
<p>还有一个关键配置，那就是将RESET由false改为true。<a href="http://tonybai.com/2017/06/09/setup-a-high-availability-private-registry-based-on-harbor-and-cephfs/">只有改为true，adminserver启动时，才能读取更新后的配置</a>：</p>
<pre><code>RESET=true
</code></pre>
<p>Redis连接的配置在common/templates/ui/env中，我们需要新增一行：</p>
<pre><code>_REDIS_URL=redis_ip:6379,100,password,0
</code></pre>
<h3>第五步：prepare并启动harbor</h3>
<p>执行prepare脚本生成harbor各容器服务的配置；在每个Harbor node上通过下面命令启动harbor实例：</p>
<pre><code>docker-compose up -d

</code></pre>
<p>启动后，可以通过docker-compose ps命令查看harbor实例中各容器的启动状态。如果启动顺利，都是”Up”状态，那么我们可以在浏览器里输入：http://hub.tonybai.com:8070，不出意外的话，我们就可以看到Harbor ui的登录页面了。</p>
<p>至此，我们的高可用Harbor cluster搭建过程就告一段落了。</p>
<h3>Troubleshooting</h3>
<p>不过，对Harbor的认知还未结束，我们在后续使用Harbor的过程中遇到了一些问题，这里举两个例子。</p>
<h4>问题1： docker login hub.tonybai.com:8070 failed</h4>
<p>现象日志：</p>
<pre><code>Error response from daemon: Get https://hub.tonybai.com:8070/v1/users/: http: server gave HTTP response to HTTPS client
</code></pre>
<p>通过错误日志分析应该是docker daemon与镜像仓库所用协议不一致导致。docker engine默认采用https协议访问仓库，但之前我们搭建的Harbor采用的是http协议提供服务，两者不一致。</p>
<p>解决方法有两种，这里列出第一种：让docker引擎通过http方式访问harbor仓库：</p>
<pre><code>在/etc/docker/daemon.json中添加insecure-registry：

{
    "insecure-registries": ["hub.tonybai.com:8070"]
}

重启docker service生效
</code></pre>
<p>第二种方法就是让Harbor支持https，需要为harbor的proxy配置私钥和证书，位置：harbor.cfg中</p>
<pre><code>#The path of cert and key files for nginx, they are applied only the protocol is set to https
ssl_cert = /data/cert/server.crt
ssl_cert_key = /data/cert/server.key
</code></pre>
<p>这里就不细说了。</p>
<h4>问题2：docker login hub.tonybai.com:8070 有时成功，有时failed</h4>
<p>现象日志:</p>
<pre><code>第一次登录成功：
# docker login -u user -p passwd http://hub.tonybai.com:8070 Login Succeeded

第二次登录失败：
# docker login -u user -p passwd http://hub.tonybai.com:8070
Error response from daemon: login attempt to http://hub.tonybai.com:8070/v2/ failed with status: 401 Unauthorized
</code></pre>
<p>这个问题的原因在于对docker registry v2协议登录过程理解不够透彻。docker registry v2是一个两阶段登录的过程：</p>
<ul>
<li>首先：docker client会到registry去尝试登录，registry发现request中没有携带token，则返回失败应答401，并告诉客户端到哪里去获取token；</li>
<li>客户端收到应答后，获取应答中携带的token service地址，然后到harbor的core services中的token service那里获取token（使用user, password进行校验）。一旦token service校验ok，则会使用private_key.pem生成一个token；</li>
<li>客户端拿到token后，再次到registry那里去登录，这次registry用root.crt去校验客户端携带的token，校验通过，则login成功。</li>
</ul>
<p>由于我们是一个harbor cluster，如果docker client访问的token service和registry是在一个harbor实例中的，那么login就会ok；否则docker client就会用harbor node1上token service生成的token到harbor node2上的registry去登录，由于harbor node2上root.crt与harbor node1上private_key.pem并非一对，因此<a href="http://tonybai.com/2017/06/15/fix-auth-fail-when-login-harbor-registry/">登录失败</a>。</p>
<p>解决方法：将所有节点上使用同一套root.crt和private_key.pem。即将一个harbor node（harbor.cfg中custom_crt=true的那个）上的 common/config/ui/private_key.pem和 common/config/registry/root.crt复制到其他harbor node;然后重建各harbor实例中的容器。</p>
<p>至此，我们的高可用Harbor仓库部署完了。针对上面的配置过程，我还做了几个录屏文件，由于时间关系，这里不能播放了，大家可以在下面这个连接下载并自行播放收看。</p>
<pre><code>Harbor install 录屏: https://pan.baidu.com/s/1o8JYKEe
</code></pre>
<p>谢谢大家！</p>
<h2>讲稿slide可以在<a href="https://github.com/bigwhite/talks/tree/master/osc/2017">这里</a>获取到。</h2>
<p>微博：<a href="http://weibo.com/bigwhite20xx">@tonybai_cn</a><br />
微信公众号：iamtonybai<br />
github.com: https://github.com/bigwhite</p>
<p style='text-align:left'>&copy; 2017, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2017/10/23/the-speech-script-practice-on-deploying-a-ha-harbor-cluster-for-osc-shenyang-2017/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>将Blog迁移到DigitalOcean的VPS上</title>
		<link>https://tonybai.com/2014/11/28/migrate-blog-to-digitalocean-vps/</link>
		<comments>https://tonybai.com/2014/11/28/migrate-blog-to-digitalocean-vps/#comments</comments>
		<pubDate>Fri, 28 Nov 2014 14:58:00 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[生活簿]]></category>
		<category><![CDATA[Apache]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogbus]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[DigitalOcean]]></category>
		<category><![CDATA[directadmin]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[MySQL]]></category>
		<category><![CDATA[PromoCode]]></category>
		<category><![CDATA[snapshot]]></category>
		<category><![CDATA[virtualhost]]></category>
		<category><![CDATA[vps]]></category>
		<category><![CDATA[Wordpress]]></category>
		<category><![CDATA[博客]]></category>
		<category><![CDATA[黑色星期五]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=1637</guid>
		<description><![CDATA[自从2012年初将Blog从Blogbus搬出来放到同事代理的虚拟主机上后，Blog运行一直很稳定，我也算 是比较满意。但同事的主机代理生意这两年来每况愈下，这促使他在前些时候做出了在今年年末放弃这门生意的决定，于是我又不得不为Blog另找落脚儿地了。 这次不想再单纯的买Wordpress虚拟主机了，一来功能有限，二来国外的入门级VPS价格已经与虚拟主机价格逐渐缩小，尤其是像 DigitalOcean这样的后起之秀，5$/mon的入门级配置VPS基本可以满足我的应用。于是DigitalOcean VPS就成为了我的购买目标。DigitalOcean这两年推广力度大，其Promo code的优惠有时可达20$以上，去年黑色星期五当天就给出了50$的优惠码。于是我期望着今天（2014黑色星期五）DigitalOcean的 50$优惠码能再现江湖。 但事与愿违，当时间走入美国当地时间星期五后，网上哪些所谓50$的Promo code依旧无法正常使用。无奈只能退而求次，使用&#34;SHIPITFAST10&#34;这个10$的优惠码，对于入门级VPS来说，10$也够试用两个月的了。 Digital Ocean VPS的注册和购买流程非常简单，按照官方提示一步一步做即可。这里要注意的是如果选择信用卡支付，务必一次填对信用卡信息，否则account就会短暂 无法使用，你需要fill out一个Form，提交给客服人工验证才能解除对你account的封锁。 接下来就是稍详细的说明Wordpress blog迁移到Digital Ocean VPS的步骤了，希望能对大家有所帮助。 一、备份WordPress Blog 网上关于迁移WordPress的方法有许多方案，之前在测试将WordPress迁移到Docker容器中时，我采用的是数据表导出导入+WordPress程序覆盖的方式，这次我依旧采用此方法。 现有的Blog用的是DirectAdmin的后台管理面板，支持全站备份，备份后的文件为：backup-Nov-27-2014-1.tar.gz。这个压缩包中有两个重要的组件（解压后你就可以看到）： &#160;&#160;&#160; &#8211; backup/tonybai_db.sql &#160;&#160;&#160; &#8211; domains/tonybai.com/public_html/ &#160;&#160;&#160; 我们要迁移的就是这两个组件。第一个.sql文件就是我们导出的数据库表，需要导入到新主机中的新库中。而第二个则是Wordpress安装后的文件集合，用于直接覆盖目标主机上对应的Wordpress文件包的。 二、创建Digital Ocean VPS Droplet 在填写完信用卡，利用优惠码充值账户成功后，就可以创建Droplet了。Droplet是DO的术语，理解成一个VPS实例即可。Droplet的创建 体验不错，DO已经准备好了各种VPS常用的应用组合以及OS供选择。我选择了5$/mon的Ubuntu 14.04 x64 + WordPress的组合，机房选择San Francisco 1。确认后，DO会开始创建Droplet操作，不到1分钟，Droplet就创建完毕了。如果不用ssh key，则VPS的root密码会发到你的注册邮箱中。有了root和密码，我们就可以通过&#34;ssh root@YOUR_VPS_IP&#34;访问你的VPS了。 首次后台登陆VPS，VPS会强制你修改root登陆密码。 三、初始安装WordPress 现在我们的VPS上已经安装好了WordPress运行所需要的所有软件了，包括apache2、mysql等。修改/etc/hosts，将自己的域名tonybai.com映射为VPS IP。 访问tonybai.com，WordPress的自安装程序启动，按照提示一步一步即可安装好Wordpress，这里带的Wordpress是4.0.0版本（注意：我们后续是要覆盖掉这个 WordPress的）。 安装好后，再访问tonybai.com就可以看到默认安装后的一篇example blog了。 现在我们进入tonybai.com/wp-admin页面，Apache弹出一个登陆框，在DO官方文档提到过，/wp-admin初始情况使用了 apache的.htaccess credential保护机制了，我们需要输入用户名密码才能进入wp-admin页面。这个用户名密码就在/root/WORDPRESS里。 [...]]]></description>
			<content:encoded><![CDATA[<p>自从2012年初将Blog<a href="http://tonybai.com/2012/02/29/a-new-departure- of-my-blog-move-from-blogbus-to-wordpress/">从Blogbus搬出来</a>放到同事代理的虚拟主机上后，Blog运行一直很稳定，我也算 是比较满意。但同事的主机代理生意这两年来每况愈下，这促使他在前些时候做出了在今年年末放弃这门生意的决定，于是我又不得不为Blog另找落脚儿地了。</p>
<p>这次不想再单纯的买Wordpress虚拟主机了，一来功能有限，二来国外的入门级VPS价格已经与虚拟主机价格逐渐缩小，尤其是像 <a href="http://www.digitalocean.com">DigitalOcean</a>这样的后起之秀，5$/mon的入门级配置VPS基本可以满足我的应用。于是DigitalOcean VPS就成为了我的购买目标。DigitalOcean这两年推广力度大，其Promo code的优惠有时可达20$以上，去年黑色星期五当天就给出了50$的优惠码。于是我期望着今天（2014黑色星期五）DigitalOcean的 50$优惠码能再现江湖。</p>
<p>但事与愿违，当时间走入美国当地时间星期五后，网上哪些所谓50$的Promo code依旧无法正常使用。无奈只能退而求次，使用&quot;SHIPITFAST10&quot;这个10$的优惠码，对于入门级VPS来说，10$也够试用两个月的了。</p>
<p>Digital Ocean VPS的注册和购买流程非常简单，按照官方提示一步一步做即可。这里要注意的是如果选择信用卡支付，务必一次填对信用卡信息，否则account就会短暂 无法使用，你需要fill out一个Form，提交给客服人工验证才能解除对你account的封锁。</p>
<p>接下来就是稍详细的说明Wordpress blog迁移到Digital Ocean VPS的步骤了，希望能对大家有所帮助。</p>
<p><b>一、备份WordPress Blog</b></p>
<p>网上关于迁移WordPress的方法有许多方案，之前在测试<a href="http://tonybai.com/2014/11/01/migrate-wordpress-into-docker-container/">将WordPress迁移到Docker容器</a>中时，我采用的是数据表导出导入+WordPress程序覆盖的方式，这次我依旧采用此方法。</p>
<p>现有的Blog用的是<a href="http://www.directadmin.com">DirectAdmin</a>的后台管理面板，支持全站备份，备份后的文件为：backup-Nov-27-2014-1.tar.gz。这个压缩包中有两个重要的组件（解压后你就可以看到）：</p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; &#8211; backup/tonybai_db.sql<br />
	&nbsp;&nbsp;&nbsp; &#8211; domains/tonybai.com/public_html/</font><br />
	&nbsp;&nbsp;&nbsp;<br />
	我们要迁移的就是这两个组件。第一个.sql文件就是我们导出的数据库表，需要导入到新主机中的新库中。而第二个则是Wordpress安装后的文件集合，用于直接覆盖目标主机上对应的Wordpress文件包的。</p>
<p><b>二、创建Digital Ocean VPS Droplet</b></p>
<p>在填写完信用卡，利用优惠码充值账户成功后，就可以创建Droplet了。Droplet是DO的术语，理解成一个VPS实例即可。Droplet的创建 体验不错，DO已经准备好了各种VPS常用的应用组合以及OS供选择。我选择了5$/mon的Ubuntu 14.04 x64 + WordPress的组合，机房选择San Francisco 1。确认后，DO会开始创建Droplet操作，不到1分钟，Droplet就创建完毕了。如果不用ssh key，则VPS的root密码会发到你的注册邮箱中。有了root和密码，我们就可以通过&quot;<font face="Courier New">ssh root@YOUR_VPS_IP</font>&quot;访问你的VPS了。</p>
<p>首次后台登陆VPS，VPS会强制你修改root登陆密码。</p>
<p><b>三、初始安装WordPress</b></p>
<p>现在我们的VPS上已经安装好了WordPress运行所需要的所有软件了，包括apache2、mysql等。修改/etc/hosts，将自己的域名tonybai.com映射为VPS IP。</p>
<p>访问tonybai.com，WordPress的自安装程序启动，按照<a href="https://www.digitalocean.com /community/tutorials/one-click-install-wordpress-on-ubuntu-14-04-with- digitalocean">提示</a>一步一步即可安装好Wordpress，这里带的Wordpress是4.0.0版本（注意：我们后续是要覆盖掉这个 WordPress的）。</p>
<p>安装好后，再访问tonybai.com就可以看到默认安装后的一篇example blog了。</p>
<p>现在我们进入tonybai.com/wp-admin页面，Apache弹出一个登陆框，在DO官方文档提到过，/wp-admin初始情况使用了 apache的.htaccess credential保护机制了，我们需要输入用户名密码才能进入wp-admin页面。这个用户名密码就在/root/WORDPRESS里。</p>
<p><b>四、导表</b></p>
<p>接下来，我们先将<font face="Courier New">backup/tonybai_db.sql导入mysql数据库。</font></p>
<p><font face="Courier New">mysql的数据库访问密码在/root/.my.cnf中，用户名是root。</font></p>
<p><font face="Courier New">管理mysql我们更多使用phpmyadmin工具，于是通过apt-get install phpmyadmin -y安装一个。</font></p>
<p><font face="Courier New">为了通过Web页面访问到phpmyadmin，我们还需执行以下两个步骤：</font></p>
<p><font face="Courier New">&nbsp;在/etc/apache2/apache2.conf尾部添加一行：<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; Include /etc/phpmyadmin/apache.conf</font></p>
<p><font face="Courier New">&nbsp;重启apache2：service apache2 restart</font></p>
<p>之后通过tonybai.com/phpmyadmin访问phpmyadmin工具。登录时使用mysql的root和密码即可。</p>
<p>进入phpmyadmin后，我们可以看到前面的Wordpress安装过程在mysql中建立了名为wordpress的数据库以及名为 wordpress的数据库用户。但我之前的blog使用的数据库用户和数据库并非wordpress，而是tonybai_user和tonybaidb，于是我们需要自己创建 tonybaidb数据库以及tonybai_user这个数据库账号。</p>
<p>创建tonybaidb时，注意使用utf8_general_ci字符集。</p>
<p>创建tonybai_user数据库账户时，注意其权限仅局限于localhost发起的访问以及tonybaidb这个数据库，其密码设置为原blog wp-config.php中的数据库密码。</p>
<p>由于phpmyadmin导入的文件不能超过2M，因此我们只能通过后台导表：</p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; mysql -u root -p<br />
	&nbsp;&nbsp;&nbsp; mysql&gt; use tonybai_db<br />
	&nbsp; &nbsp; database changed<br />
	&nbsp;&nbsp;&nbsp; mysql&gt; source ./tonybai_db.sql</font></p>
<p><b>五、替换Wordpress安装文件</b></p>
<p>默认下wordpress安装到了/var/www下。我们需要将<font face="Courier New">domains/tonybai.com/public_html替换掉/var/www目录：</font></p>
<p><font face="Courier New">cd /var<br />
	mv www www.bak</font></p>
<p><font face="Courier New">将domain/tonybai.com/public_html cp到/var/下，改名为www</font></p>
<p><font face="Courier New">chown -R www-data www<br />
	chgrp -R www-data www</font></p>
<p>剩下的就是访问tonybai.com即可。</p>
<p>是不是熟悉的页面和风格又展现在你眼前了！</p>
<p><b>六、创建SnapShot</b></p>
<p>DO提供两种备份方式Snapshot和Backups，其中Snapshot目前还是免费的，但backup服务是要付费的。Snapshot创建的前提是先stop这个Droplet。建议导入blog、访问正常后，马上建立一个Droplet的Snapshot。</p>
<p><b>七、其它</b></p>
<p>由于是入门型VPS，其内存仅有512M，并且默认情况下Ubuntu 14.04 VPS没有创建Swap，考虑到VPS的高可用性，我们还是需要自己动手创建一些swap空间，以供不时之需，创建步骤很简单，执行下面命令即可：</p>
<p><font face="Courier New">fallocate -l 512M /swapfile<br />
	mkswap /swapfile<br />
	swapon /swapfile</font></p>
<p><font face="Courier New">swapon -s</font>&nbsp; 查看一下当前swap，可以看到：<br />
	<font face="Courier New">Filename&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;&nbsp;&nbsp;&nbsp;&nbsp; Type&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Size&nbsp;&nbsp;&nbsp; Used&nbsp;&nbsp;&nbsp; Priority<br />
	/swapfile&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;&nbsp;&nbsp;&nbsp; file&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 524284&nbsp; 0&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; -1</font></p>
<p>另外调试过程中发现访问tonybai.com/feed出现如下错误：<br />
	<font face="Courier New">Forbidden：<br />
	&nbsp;&nbsp;&nbsp; You don&#39;t have permission to access /feed/ on this server.</font></p>
<p>	Google、Baidu许久才发现真正问题所在：我的旧Blog目录下有一个feed子目录，把这个目录删除即可。</p>
<p style='text-align:left'>&copy; 2014, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2014/11/28/migrate-blog-to-digitalocean-vps/feed/</wfw:commentRss>
		<slash:comments>7</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/12/discussion-on-shared-mem-support-in-docker/</link>
		<comments>https://tonybai.com/2014/10/12/discussion-on-shared-mem-support-in-docker/#comments</comments>
		<pubDate>Sat, 11 Oct 2014 21:23:34 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[IPC]]></category>
		<category><![CDATA[Kernel]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[mmap]]></category>
		<category><![CDATA[POSIX]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[SharedMemory]]></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=1560</guid>
		<description><![CDATA[我们的遗留系统广泛使用了性能最佳的IPC方式 &#8211; 共享内存，而且用到了两种共享内存的实现方式：System V共享内存(shmget、shmat、shmdt)以及Mmap映射Regular File。System V共享内存支持一定程度上的内存数据持久化，即当程序创建共享内存对象后，如果不显式删除或物理主机重启，该IPC对象会一直保留，其中的数据也不会丢 失；mmap映射Regular File的方式支持内存数据持久化到文件中，即便物理主机重启，这部分数据依旧不会丢失，除非显式删除文件。这两个共享内存机制，尤其是其持久化的特性是 我们的系统所依赖的。但是在Docker容器中，这两种共享内存机制依旧能被很好的支持吗？我们通过试验来分析一下。 一、System V共享内存 一个启动的Docker容器就是一个拥有了自己的内核名字空间的进程，其pid、net、ipc、mnt、uts、user等均与其他进程隔离，对于运行于该容器内的程序而言，它仿佛会觉得它独占了一台&#8220;主机&#8221;。对于这类&#8220;主机&#8221;，我们首先来测试一下其中的system v共享内存是否依旧能像物理主机上一样，在程序退出后依旧能保持持久化？在容器退出后能保持么？ 我们先来写两个测试程序，一个用于创建system v共享内存，并写入一些数据，另外一个程序则映射该共享内存并尝试读出内存中的数据。由于Golang目前仍未提供对System V共享内存的高级封装接口，通过syscall包的Syscall调用又太繁琐，因此我们直接使用C代码与Go代码结合的方式实现这两个测试程序。之前写 过一篇名为《Go与C语言互操作》的博文，看不懂下面代码的朋友，可以先阅读一下这篇文章。 //systemv_shm_wr.go package main //#include &#60;sys/types.h&#62; //#include &#60;sys/ipc.h&#62; //#include &#60;sys/shm.h&#62; //#include &#60;stdio.h&#62; // //#define SHMSZ&#160;&#160;&#160;&#160; 27 // //int shm_wr() { //&#160;&#160;&#160; char c; //&#160;&#160;&#160; int shmid; //&#160;&#160;&#160; key_t key; //&#160;&#160;&#160; char *shm, *s; // //&#160;&#160;&#160; key = 5678; // [...]]]></description>
			<content:encoded><![CDATA[<p>我们的遗留系统广泛使用了性能最佳的IPC方式 &#8211; <a href="http://en.wikipedia.org/wiki/Shared_memory">共享内存</a>，而且用到了两种共享内存的实现方式：System V共享内存(shmget、shmat、shmdt)以及Mmap映射Regular File。System V共享内存支持一定程度上的内存数据持久化，即当程序创建共享内存对象后，如果不显式删除或物理主机重启，该IPC对象会一直保留，其中的数据也不会丢 失；mmap映射Regular File的方式支持内存数据持久化到文件中，即便物理主机重启，这部分数据依旧不会丢失，除非显式删除文件。这两个共享内存机制，尤其是其持久化的特性是 我们的系统所依赖的。但是在<a href="http://docker.com">Docker</a>容器中，这两种共享内存机制依旧能被很好的支持吗？我们通过试验来分析一下。</p>
<p><b>一、System V共享内存</b></p>
<p>一个启动的Docker容器就是一个拥有了自己的内核名字空间的进程，其<b>pid</b><b>、net</b><b>、ipc</b><b>、mnt</b><b>、uts</b><b>、user</b>等均与其他进程隔离，对于运行于该容器内的程序而言，它仿佛会觉得它独占了一台&ldquo;主机&rdquo;。对于这类&ldquo;主机&rdquo;，我们首先来测试一下其中的system v共享内存是否依旧能像物理主机上一样，在程序退出后依旧能保持持久化？在容器退出后能保持么？</p>
<p>我们先来写两个测试程序，一个用于创建system v共享内存，并写入一些数据，另外一个程序则映射该共享内存并尝试读出内存中的数据。由于Golang目前仍未提供对System V共享内存的高级封装接口，通过syscall包的Syscall调用又太繁琐，因此我们直接使用C代码与Go代码结合的方式实现这两个测试程序。之前写 过一篇名为《<a href="http://tonybai.com/2012/09/26/interoperability-between-go-and-c/">Go与C语言互操作</a>》的博文，看不懂下面代码的朋友，可以先阅读一下这篇文章。</p>
<p><font face="Courier New">//systemv_shm_wr.go<br />
	package main</font></p>
<p><font face="Courier New">//#include &lt;sys/types.h&gt;<br />
	//#include &lt;sys/ipc.h&gt;<br />
	//#include &lt;sys/shm.h&gt;<br />
	//#include &lt;stdio.h&gt;<br />
	//<br />
	//#define SHMSZ&nbsp;&nbsp;&nbsp;&nbsp; 27<br />
	//<br />
	//int shm_wr() {<br />
	//&nbsp;&nbsp;&nbsp; char c;<br />
	//&nbsp;&nbsp;&nbsp; int shmid;<br />
	//&nbsp;&nbsp;&nbsp; key_t key;<br />
	//&nbsp;&nbsp;&nbsp; char *shm, *s;<br />
	//<br />
	//&nbsp;&nbsp;&nbsp; key = 5678;<br />
	//<br />
	//&nbsp;&nbsp;&nbsp; if ((shmid = shmget(key, SHMSZ, IPC_CREAT | 0666)) &lt; 0) {<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return -1;<br />
	//&nbsp;&nbsp;&nbsp; }<br />
	//<br />
	//&nbsp;&nbsp;&nbsp; if ((shm = shmat(shmid, NULL, 0)) == (char *) -1) {<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return -2;<br />
	//&nbsp;&nbsp;&nbsp; }<br />
	//<br />
	//&nbsp;&nbsp;&nbsp; s = shm;<br />
	//&nbsp;&nbsp;&nbsp; for (c = &#39;a&#39;; c &lt;= &#39;z&#39;; c++)<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; *s++ = c;<br />
	//&nbsp;&nbsp;&nbsp; s = NULL;<br />
	//<br />
	//&nbsp;&nbsp;&nbsp; return 0;<br />
	//}<br />
	import &quot;C&quot;</font></p>
<p><font face="Courier New">import &quot;fmt&quot;</font></p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; i := C.shm_wr()<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if i != 0 {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Println(&quot;SystemV Share Memory Create and Write Error:&quot;, i)<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; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Println(&quot;SystemV Share Memory Create and Write Ok&quot;)<br />
	}</font></p>
<p><font face="Courier New">//systemv_shm_rd.go</font></p>
<p><font face="Courier New">package main</font></p>
<p><font face="Courier New">//#include &lt;sys/types.h&gt;<br />
	//#include &lt;sys/ipc.h&gt;<br />
	//#include &lt;sys/shm.h&gt;<br />
	//#include &lt;stdio.h&gt;<br />
	//<br />
	//#define SHMSZ&nbsp;&nbsp;&nbsp;&nbsp; 27<br />
	//<br />
	//int shm_rd() {<br />
	//&nbsp;&nbsp;&nbsp; char c;<br />
	//&nbsp;&nbsp;&nbsp; int shmid;<br />
	//&nbsp;&nbsp;&nbsp; key_t key;<br />
	//&nbsp;&nbsp;&nbsp; char *shm, *s;<br />
	//<br />
	//&nbsp;&nbsp;&nbsp; key = 5678;<br />
	//<br />
	//&nbsp;&nbsp;&nbsp; if ((shmid = shmget(key, SHMSZ, 0666)) &lt; 0) {<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return -1;<br />
	//&nbsp;&nbsp;&nbsp; }<br />
	//<br />
	//&nbsp;&nbsp;&nbsp; if ((shm = shmat(shmid, NULL, 0)) == (char *) -1) {<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return -2;<br />
	//&nbsp;&nbsp;&nbsp; }<br />
	//<br />
	//&nbsp;&nbsp;&nbsp; s = shm;<br />
	//<br />
	//&nbsp;&nbsp;&nbsp; int i = 0;<br />
	//&nbsp;&nbsp;&nbsp; for (i = 0; i &lt; SHMSZ-1; i++)<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; printf(&quot;%c &quot;, *(s+i));<br />
	//&nbsp;&nbsp;&nbsp; printf(&quot;\n&quot;);<br />
	//&nbsp;&nbsp;&nbsp; s = NULL;<br />
	//<br />
	//&nbsp;&nbsp;&nbsp; return 0;<br />
	//}<br />
	import &quot;C&quot;</font></p>
<p><font face="Courier New">import &quot;fmt&quot;</font></p>
<p><font face="Courier New">import &quot;fmt&quot;</font></p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; i := C.shm_rd()<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if i != 0 {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Println(&quot;SystemV Share Memory Create and Read Error:&quot;, i)<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; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Println(&quot;SystemV Share Memory Create and Read Ok&quot;)<br />
	}</font></p>
<p>我们通过go build构建上面两个程序，得到两个测试用可执行程序：systemv_shm_wr和systemv_shm_rd。下面我们来构建我们的测试用docker image，Dockerfile内容如下：</p>
<p><font face="Courier New">FROM centos:centos6<br />
	MAINTAINER Tony Bai &lt;bigwhite.cn@gmail.com&gt;<br />
	COPY ./systemv_shm_wr /bin/<br />
	COPY ./systemv_shm_rd /bin/</font></p>
<p>构建Docker image：&ldquo;shmemtest:v1&rdquo;：</p>
<p><font face="Courier New">$ sudo docker build -t=&quot;shmemtest:v1&quot; ./<br />
	Sending build context to Docker daemon 16.81 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 : COPY ./systemv_shm_wr /bin/<br />
	&nbsp;&#8212;&gt; ea59fb767573<br />
	Removing intermediate container 4ce91720897b<br />
	Step 3 : COPY ./systemv_shm_rd /bin/<br />
	&nbsp;&#8212;&gt; 1ceb207b1009<br />
	Removing intermediate container 7ace7ad53a3f<br />
	Successfully built 1ceb207b1009</font></p>
<p>启动一个基于该image的容器：<br />
	<font face="Courier New">$ sudo docker run -it &quot;shmemtest:v1&quot; /bin/bash</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; 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 />
	0a2f37bee6eb&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; shmemtest:v1&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &quot;/bin/bash&quot;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 28 seconds ago&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Up 28 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; elegant_hawking</font></p>
<p>进入容器，先后执行systemv_shm_wr和systemv_shm_rd，我们得到如下结果：</p>
<p><font face="Courier New">bash-4.1# systemv_shm_wr<br />
	SystemV Share Memory Create and Write Ok<br />
	bash-4.1# systemv_shm_rd<br />
	a b c d e f g h i j k l m n o p q r s t u v w x y z<br />
	SystemV Share Memory Create and Read Ok</font></p>
<p>在容器运行过程中，SystemV共享内存对象是可以持久化的。systemv_shm_wr退出后，数据依旧得以保留。我们接下来尝试一下重启container后是否还能读出数据：</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; 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 />
	0a2f37bee6eb&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; shmemtest:v1&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &quot;/bin/bash&quot;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 8 minutes ago&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Up 8 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;&nbsp; elegant_hawking&nbsp;&nbsp;&nbsp;&nbsp;<br />
	$ sudo docker stop 0a2f37bee6eb<br />
	0a2f37bee6eb<br />
	$ sudo docker start 0a2f37bee6eb<br />
	0a2f37bee6eb<br />
	$ sudo docker attach 0a2f37bee6eb<br />
	bash-4.1# systemv_shm_rd<br />
	SystemV Share Memory Create and Read Error: -1</font></p>
<p>程序返回-1，显然在shmget时就出错了，系统已经没有了key为&quot;5678&quot;的这个共享内存IPC对象了。也就是说当容器stop时，就好比我们的物理主机关机，docker将该容器对应的共享内存IPC对象删除了。</p>
<p>从原理上分析，似乎我们也能得出此结论：毕竟Docker container是通过kernel namespace隔离的，容器中的进程在IPC资源申请时需要加入namespace信息。打个比方，如果我们启动容器的进程pid(物理主机视角)是 1234，那么这容器内进程申请的共享内存IPC资源（比如key=5678）的标识应该类似于&ldquo;1234:5678&rdquo;这样的形式。重启容器 后，Docker Daemon无法给该容器分配与上次启动相同的pid，因此pid发生了变化，之前容器中的&quot;1234:5678&quot;保留下来也是毫无意义的，还无端占用系 统资源。因此，System V IPC在Docker容器中的运用与物理机有不同，这方面要小心，目前似乎没有很好的方法，也许以后Docker会加入全局IPC，这个我们只能等待。</p>
<p><b>二、Mmap映射共享内存</b></p>
<p>接下来我们探讨mmap共享内存在容器中的支持情况。mmap常见的有两类共享内存映射方式，一种映射到/dev/zero，另外一种则是映射到 Regular Fiile。前者在程序退出后数据自动释放，后者则保留在映射的文件中。后者对我们更有意义，这次测试的也是后者。</p>
<p>同样，我们也先来编写两个测试程序。</p>
<p><font face="Courier New">//mmap_shm_wr.go</font><br />
	<font face="Courier New">package main</font></p>
<p><font face="Courier New">//#include &lt;stdio.h&gt;<br />
	//#include &lt;sys/types.h&gt;<br />
	//#include &lt;sys/mman.h&gt;<br />
	//#include &lt;fcntl.h&gt;<br />
	//<br />
	//#define SHMSZ&nbsp;&nbsp;&nbsp;&nbsp; 27<br />
	//<br />
	//int shm_wr()<br />
	//{<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; char c;<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; char *shm = NULL;<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; char *s = NULL;<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; int fd;<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if ((fd = open(&quot;./shm.txt&quot;, O_RDWR|O_CREAT, S_IRUSR|S_IWUSR)) == -1)&nbsp; {<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return -1;<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	//<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; lseek(fd, 500, SEEK_CUR);<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; write(fd, &quot;\0&quot;, 1);<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; lseek(fd, 0, SEEK_SET);<br />
	//<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; shm = (char*)mmap(shm, SHMSZ, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (!shm) {<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return -2;<br />
	//<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	//<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; close(fd);<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; s = shm;<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; for (c = &#39;a&#39;; c &lt;= &#39;z&#39;; c++) {<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; *(s+(int)(c &#8211; &#39;a&#39;)) = c;<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return 0;<br />
	//}<br />
	import &quot;C&quot;</font></p>
<p><font face="Courier New">import &quot;fmt&quot;</font></p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; i := C.shm_wr()<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if i != 0 {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Println(&quot;Mmap Share Memory Create and Write Error:&quot;, i)<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; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Println(&quot;Mmap Share Memory Create and Write Ok&quot;)<br />
	}</font></p>
<p><font face="Courier New">//mmap_shm_rd.go</font><br />
	<font face="Courier New">package main</font></p>
<p><font face="Courier New">//#include &lt;stdio.h&gt;<br />
	//#include &lt;sys/types.h&gt;<br />
	//#include &lt;sys/mman.h&gt;<br />
	//#include &lt;fcntl.h&gt;<br />
	//<br />
	//#define SHMSZ&nbsp;&nbsp;&nbsp;&nbsp; 27<br />
	//<br />
	//int shm_rd()<br />
	//{<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; char c;<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; char *shm = NULL;<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; char *s = NULL;<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; int fd;<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if ((fd = open(&quot;./shm.txt&quot;, O_RDONLY)) == -1)&nbsp; {<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return -1;<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	//<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; shm = (char*)mmap(shm, SHMSZ, PROT_READ, MAP_SHARED, fd, 0);<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (!shm) {<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return -2;<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	//<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; close(fd);<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; s = shm;<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; int i = 0;<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; for (i = 0; i &lt; SHMSZ &#8211; 1; i++) {<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; printf(&quot;%c &quot;, *(s + i));<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; printf(&quot;\n&quot;);<br />
	//<br />
	//&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return 0;<br />
	//}<br />
	import &quot;C&quot;</font></p>
<p><font face="Courier New">import &quot;fmt&quot;</font></p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; i := C.shm_rd()<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if i != 0 {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Println(&quot;Mmap Share Memory Read Error:&quot;, i)<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; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Println(&quot;Mmap Share Memory Read Ok&quot;)<br />
	}</font></p>
<p>我们通过go build构建上面两个程序，得到两个测试用可执行程序：mmap_shm_wr和mmap_shm_rd。下面我们来构建我们的测试用docker image，Dockerfile内容如下：</p>
<p><font face="Courier New">FROM centos:centos6<br />
	MAINTAINER Tony Bai &lt;bigwhite.cn@gmail.com&gt;<br />
	COPY ./mmap_shm_wr /bin/<br />
	COPY ./mmap_shm_rd /bin/</font></p>
<p>构建Docker image：&ldquo;shmemtest:v2&rdquo;：</p>
<p><font face="Courier New">$ sudo docker build -t=&quot;shmemtest:v2&quot; ./<br />
	Sending build context to Docker daemon 16.81 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 : COPY ./mmap_shm_wr /bin/<br />
	&nbsp;&#8212;&gt; Using cache<br />
	&nbsp;&#8212;&gt; 01e2f6bc7606<br />
	Step 3 : COPY ./mmap_shm_rd /bin/<br />
	&nbsp;&#8212;&gt; 0de95503c851<br />
	Removing intermediate container 0c472e92809f<br />
	Successfully built 0de95503c851</font></p>
<p>启动一个基于该image的容器：<br />
	<font face="Courier New">$ sudo docker run -it &quot;shmemtest:v2&quot; /bin/bash</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; 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 />
	1182f9eca367&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; shmemtest:v2&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &quot;/bin/bash&quot;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 11 seconds ago&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Up 11 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; distracted_elion</font></p>
<p>进入容器，先后执行mmap_shm_wr和mmap_shm_rd，我们得到如下结果：</p>
<p><font face="Courier New">bash-4.1# mmap_shm_wr<br />
	Mmap Share Memory Create and Write Ok<br />
	bash-4.1# mmap_shm_rd<br />
	a b c d e f g h i j k l m n o p q r s t u v w x y z<br />
	Mmap Share Memory Read Ok</font></p>
<p>我们接下来尝试一下重启container后是否还能读出数据：</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; CREATED&nbsp;&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 />
	1182f9eca367&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; shmemtest:v2&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &quot;/bin/bash&quot;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; About a minute ago&nbsp;&nbsp; Up About a minute&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; distracted_elion&nbsp;&nbsp;&nbsp;<br />
	$ sudo docker stop 1182f9eca367<br />
	1182f9eca367<br />
	$ sudo docker start 1182f9eca367<br />
	1182f9eca367<br />
	$ sudo docker attach 1182f9eca36</font><font face="Courier New">7</font></p>
<p><font face="Courier New">bash-4.1# mmap_shm_rd<br />
	a b c d e f g h i j k l m n o p q r s t u v w x y z<br />
	Mmap Share Memory Read Ok</font></p>
<p>通过执行结果可以看出，通过mmap映射文件方式，共享内存的数据即便在容器重启后依旧可以得到保留。从原理上看，shm.txt是容器内 的一个文件，该文件存储在容器的可写文件系统layer中，从物理主机上看，其位置在/var/lib/docker/aufs/mnt /container_full_id/下，即便容器重启，该文件也不会被删除，而是作为容器文件系统的一部分：</p>
<p><font face="Courier New">$ sudo docker inspect -f &#39;{{.Id}}&#39; 1182f9eca367<br />
	1182f9eca36756219537f9a1c7cd1b62c6439930cc54bc69e87915c5dc8f7b97<br />
	$ sudo ls /var/lib/docker/aufs/mnt/1182f9eca36756219537f9a1c7cd1b62c6439930cc54bc69e87915c5dc8f7b97<br />
	bin&nbsp; dev&nbsp; etc&nbsp; home&nbsp; lib&nbsp; lib64&nbsp; lost+found&nbsp; media&nbsp; mnt&nbsp; opt&nbsp; proc&nbsp; root&nbsp; sbin&nbsp;&nbsp;&nbsp; selinux&nbsp;<big><b> shm.txt&nbsp;</b></big> srv&nbsp; sys&nbsp; tmp&nbsp; usr&nbsp; var</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/12/discussion-on-shared-mem-support-in-docker/feed/</wfw:commentRss>
		<slash:comments>2</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>Golang Channel用法简编</title>
		<link>https://tonybai.com/2014/09/29/a-channel-compendium-for-golang/</link>
		<comments>https://tonybai.com/2014/09/29/a-channel-compendium-for-golang/#comments</comments>
		<pubDate>Mon, 29 Sep 2014 09:02:43 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Actor]]></category>
		<category><![CDATA[BestPractice]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[Channel]]></category>
		<category><![CDATA[Concurrent]]></category>
		<category><![CDATA[container]]></category>
		<category><![CDATA[CSP]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Google]]></category>
		<category><![CDATA[GopherCon]]></category>
		<category><![CDATA[LXC]]></category>
		<category><![CDATA[Opensource]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[TIOBE]]></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=1551</guid>
		<description><![CDATA[在进入正式内容前，我这里先顺便转发一则消息，那就是Golang 1.3.2已经正式发布了。国内的golangtc已经镜像了golang.org的安装包下载页面，国内go程序员与爱好者们可以到&#34;Golang中 国&#34;，即golangtc.com去下载go 1.3.2版本。 Go这门语言也许你还不甚了解，甚至是完全不知道，这也有情可原，毕竟Go在TIOBE编程语言排行榜上位列30开外。但近期使用Golang 实现的一杀手级应用 Docker你却不该不知道。docker目前火得是一塌糊涂啊。你去国内外各大技术站点用眼轻瞥一下，如 果没有涉及到&#8220;docker&#8221;字样新闻的站点建 议你以后就不要再去访问了^_^。Docker是啥、怎么用以及基础实践可以参加国内一位仁兄的经验之作：《 Docker &#8211; 从入门到实践》。 据我了解，目前国内试水Go语言开发后台系统的大公司与初创公司日益增多，比如七牛、京东、小米，盛大，金山，东软，搜狗等，在这里我们可以看到一些公司的Go语言应用列表，并且目前这个列表似乎依旧在丰富中。国内Go语言的推广与布道也再稳步推进中，不过目前来看多以Go入 门与基础为主题，Go idioms、tips或Best Practice的Share并不多见，想必国内的先行者、布道师们还在韬光养晦，积攒经验，等到时机来临再厚积薄发。另外国内似乎还没有一个针对Go的 布道平台，比如Golang技术大会之类的的平台。 在国外，虽然Go也刚刚起步，但在Golang share的广度和深度方面显然更进一步。Go的国际会议目前还不多，除了Golang老东家Google在自己的各种大会上留给Golang展示自己的 机会外，由 Gopher Academy 发起的GopherCon 会议也于今年第一次举行，并放出诸多高质量资料，在这里可以下载。欧洲的Go语言大会.dotgo也即将开幕，估计后续这两个大会将撑起Golang技术分享 的旗帜。 言归正传，这里要写的东西并非原创，自己的Go仅仅算是入门级别，工程经验、Best Practice等还谈不上有多少，因此这里主要是针对GopherCon2014上的&#8220;舶来品&#8221;的学习心得。来自CloudFlare的工程师John Graham-Cumming谈了关于 Channel的实践经验，这里针对其分享的内容，记录一些学习体会和理解，并结合一些外延知识，也可以算是一种学习笔记吧，仅供参考。 一、Golang并发基础理论 Golang在并发设计方面参考了C.A.R Hoare的CSP，即Communicating Sequential Processes并发模型理论。但就像John Graham-Cumming所说的那样，多数Golang程序员或爱好者仅仅停留在&#8220;知道&#8221;这一层次，理解CSP理论的并不多，毕竟多数程序员是搞工程 的。不过要想系统学习CSP的人可以从这里下载到CSP论文的最新版本。 维基百科中概要罗列了CSP模型与另外一种并发模型Actor模型的区别： Actor模型广义上讲与CSP模型很相似。但两种模型就提供的原语而言，又有一些根本上的不同之处： &#160;&#160;&#160; &#8211; CSP模型处理过程是匿名的，而Actor模型中的Actor则具有身份标识。 &#160;&#160;&#160; &#8211; CSP模型的消息传递在收发消息进程间包含了一个交会点，即发送方只能在接收方准备好接收消息时才能发送消息。相反，actor模型中的消息传递是异步 的，即消息的发送和接收无需在同一时间进行，发送方可以在接收方准备好接收消息前将消息发送出去。这两种方案可以认为是彼此对偶的。在某种意义下，基于交 会点的系统可以通过构造带缓冲的通信的方式来模拟异步消息系统。而异步系统可以通过构造带消息/应答协议的方式来同步发送方和接收方来模拟交会点似的通信 方式。 &#160;&#160;&#160; &#8211; CSP使用显式的Channel用于消息传递，而Actor模型则将消息发送给命名的目的Actor。这两种方法可以被认为是对偶的。某种意义下，进程可 以从一个实际上拥有身份标识的channel接收消息，而通过将actors构造成类Channel的行为模式也可以打破actors之间的名字耦合。 二、Go Channel基本操作语法 Go Channel的基本操作语法如下： [...]]]></description>
			<content:encoded><![CDATA[<p>在进入正式内容前，我这里先顺便转发一则消息，那就是Golang 1.3.2已经正式发布了。国内的<a href="http://golangtc.com">golangtc</a>已经镜像了golang.org的安装包下载页面，国内go程序员与爱好者们可以到&quot;Golang中 国&quot;，即golangtc.com去下载go 1.3.2版本。</p>
<p><a href="http://golang.org">Go</a>这门语言也许你还不甚了解，甚至是完全不知道，这也有情可原，毕竟Go在<a href="http://www.tiobe.com">TIOBE</a>编程语言排行榜上位列30开外。但近期使用Golang 实现的一杀手级应用<a href="http://docker.com"> <b>Docker</b></a>你却不该不知道。docker目前火得是一塌糊涂啊。你去国内外各大技术站点用眼轻瞥一下，如 果没有涉及到&ldquo;docker&rdquo;字样新闻的站点建 议你以后就不要再去访问了^_^。Docker是啥、怎么用以及基础实践可以参加国内一位仁兄的经验之作：《 <a href="https://www.gitbook.io/book/yeasy/docker_practice">Docker &#8211; 从入门到实践</a>》。</p>
<p>据我了解，目前国内试水Go语言开发后台系统的大公司与初创公司日益增多，比如七牛、京东、小米，盛大，金山，东软，搜狗等，在<a href="https://github.com/qiniu/go/issues/15#issuecomment-55568731">这里</a>我们可以看到一些公司的Go语言应用列表，并且目前这个列表似乎依旧在丰富中。国内Go语言的推广与布道也再稳步推进中，不过目前来看多以Go入 门与基础为主题，Go idioms、tips或Best Practice的Share并不多见，想必国内的先行者、布道师们还在韬光养晦，积攒经验，等到时机来临再厚积薄发。另外国内似乎还没有一个针对Go的 布道平台，比如Golang技术大会之类的的平台。</p>
<p>在国外，虽然Go也刚刚起步，但在Golang share的广度和深度方面显然更进一步。Go的国际会议目前还不多，除了Golang老东家Google在自己的各种大会上留给Golang展示自己的 机会外，由 <span style="font-family: Ubuntu, Tahoma, sans-serif; font-size: 14px; line-height: 20px;"><a href="http://gopheracademy.com/">Gopher Academy</a> 发起的</span><a href="http://www.gophercon.com">GopherCon</a> 会议也于今年第一次举行，并放出诸多高质量资料，在<a href="https://github.com/gophercon/2014-talks">这里</a>可以下载。欧洲的Go语言大会<a href="http://www.dotgo.eu/">.dotgo</a>也即将开幕，估计后续这两个大会将撑起Golang技术分享 的旗帜。</p>
<p>言归正传，这里要写的东西并非原创，自己的Go仅仅算是入门级别，工程经验、Best Practice等还谈不上有多少，因此这里主要是针对GopherCon2014上的&ldquo;舶来品&rdquo;的学习心得。来自CloudFlare的工程师John Graham-Cumming谈了关于 Channel的实践经验，这里针对其分享的内容，记录一些学习体会和理解，并结合一些外延知识，也可以算是一种学习笔记吧，仅供参考。</p>
<p><b>一、Golang并发基础理论</b></p>
<p>Golang在并发设计方面参考了C.A.R Hoare的CSP，即Communicating Sequential Processes并发模型理论。但就像John Graham-Cumming所说的那样，多数Golang程序员或爱好者仅仅停留在&ldquo;知道&rdquo;这一层次，理解CSP理论的并不多，毕竟多数程序员是搞工程 的。不过要想系统学习CSP的人可以从<a href="http://www.usingcsp.com">这里</a>下载到CSP论文的最新版本。</p>
<p><a href="http://en.wikipedia.org/wiki/Communicating_sequential_processes">维基百科</a>中概要罗列了CSP模型与另外一种并发模型Actor模型的区别：</p>
<p>Actor模型广义上讲与CSP模型很相似。但两种模型就提供的原语而言，又有一些根本上的不同之处：<br />
	&nbsp;&nbsp;&nbsp; &#8211; CSP模型处理过程是匿名的，而Actor模型中的Actor则具有身份标识。<br />
	&nbsp;&nbsp;&nbsp; &#8211; CSP模型的消息传递在收发消息进程间包含了一个交会点，即发送方只能在接收方准备好接收消息时才能发送消息。相反，actor模型中的消息传递是异步 的，即消息的发送和接收无需在同一时间进行，发送方可以在接收方准备好接收消息前将消息发送出去。这两种方案可以认为是彼此对偶的。在某种意义下，基于交 会点的系统可以通过构造带缓冲的通信的方式来模拟异步消息系统。而异步系统可以通过构造带消息/应答协议的方式来同步发送方和接收方来模拟交会点似的通信 方式。<br />
	&nbsp;&nbsp;&nbsp; &#8211; CSP使用显式的Channel用于消息传递，而Actor模型则将消息发送给命名的目的Actor。这两种方法可以被认为是对偶的。某种意义下，进程可 以从一个实际上拥有身份标识的channel接收消息，而通过将actors构造成类Channel的行为模式也可以打破actors之间的名字耦合。</p>
<p><b>二、Go Channel基本操作语法</b></p>
<p>Go Channel的基本操作语法如下：</p>
<p><font face="Courier New">c := make(chan bool) //创建一个无缓冲的bool型Channel <br />
	c &lt;- x&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; //向一个Channel发送一个值<br />
	&lt;- c&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; //从一个Channel中接收一个值<br />
	x = &lt;- c&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; //从Channel c接收一个值并将其存储到x中<br />
	x, ok = &lt;- c&nbsp; //从Channel接收一个值，如果channel关闭了或没有数据，那么ok将被置为false</font></p>
<p><i>不带缓冲的Channel</i>兼具通信和同步两种特性，颇受青睐。</p>
<p><b>三、Channel用作信号(Signal)的场景</b></p>
<p>1、等待一个事件(Event)</p>
<p><font face="Courier New">等待一个事件，有时候通过close一个Channel就足够了。例如：</font></p>
<p><font face="Courier New">//testwaitevent1.go<br />
	package main</font></p>
<p><font face="Courier New">import &quot;fmt&quot;</font></p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Println(&quot;Begin doing something!&quot;)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; c := make(chan bool)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; go func() {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Println(&quot;Doing something&#8230;&quot;)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <i>close(c)</i><br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }()<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <i>&lt;-c</i><br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Println(&quot;Done!&quot;)<br />
	}</font></p>
<p>这里main goroutine通过&quot;<font face="Courier New">&lt;-c</font>&quot;来等待sub goroutine中的&ldquo;完成事件&rdquo;，sub goroutine通过close channel促发这一事件。当然也可以通过向Channel写入一个bool值的方式来作为事件通知。main goroutine在channel c上没有任何数据可读的情况下会阻塞等待。</p>
<p>关于输出结果：</p>
<p>根据《<a href="http://golang.org/ref/mem">Go memory model</a>》中关于close channel与recv from channel的order的定义：<span style="color: rgb(34, 34, 34); font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-style: italic; line-height: normal;"><font face="Courier New">The closing of a channel happens before a receive that returns a zero value because the channel is closed.</font></span></p>
<p>我们可以很容易判断出上面程序的输出结果：</p>
<p><font face="Courier New">Begin doing something!<br />
	Doing something&#8230;<br />
	Done!</font></p>
<p>如果将<font face="Courier New">close(c)</font>换成<font face="Courier New">c&lt;-true</font>，则根据《Go memory model》中的定义：<font face="Courier New"><span style="color: rgb(34, 34, 34); font-size: 16px; font-style: italic; line-height: normal;">A receive from an unbuffered channel happens before the send on that channel completes.</span></font><br />
	&quot;<font face="Courier New">&lt;-c</font>&quot;要先于&quot;<font face="Courier New">c&lt;-true</font>&quot;完成，但也不影响日志的输出顺序，输出结果仍为上面三行。</p>
<p>2、协同多个Goroutines</p>
<p>同上，close channel还可以用于协同多个Goroutines，比如下面这个例子，我们创建了100个Worker Goroutine，这些Goroutine在被创建出来后都阻塞在&quot;<font face="Courier New">&lt;-start&quot;</font>上，直到我们在main goroutine中给出<u>开工</u>的信号：&quot;<font face="Courier New">close(start)&quot;</font>，这些goroutines才开始真正的并发运行起来。</p>
<p><font face="Courier New">//testwaitevent2.go<br />
	package main</font></p>
<p><font face="Courier New">import &quot;fmt&quot;</font></p>
<p><font face="Courier New">func worker(start chan bool, index int) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &lt;-start<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Println(&quot;This is Worker:&quot;, index)<br />
	}</font></p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; start := make(chan bool)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; for i := 1; i &lt;= 100; i++ {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; go worker(start, i)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; close(start)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; select {} //deadlock we expected<br />
	}</font></p>
<p>3、Select</p>
<p>【select的基本操作】<br />
	select是Go语言特有的操作，使用select我们可以同时在多个channel上进行发送/接收操作。下面是select的基本操作。</p>
<p><font face="Courier New">select {<br />
	case x := &lt;- somechan:<br />
	&nbsp;&nbsp;&nbsp; // &#8230; 使用x进行一些操作</font></p>
<p><font face="Courier New">case y, ok := &lt;- someOtherchan:<br />
	&nbsp;&nbsp;&nbsp; // &#8230; 使用y进行一些操作，<br />
	&nbsp;&nbsp;&nbsp; // </font><font face="Courier New"><font face="Courier New">检查ok值判断someOtherchan是否已经关闭</font></font></p>
<p><font face="Courier New">case outputChan &lt;- z:<br />
	&nbsp;&nbsp;&nbsp; // &#8230; z值被成功发送到Channel上时</font></p>
<p><font face="Courier New">default:<br />
	&nbsp;&nbsp;&nbsp; // &#8230; 上面case均无法通信时，执行此分支<br />
	}</font></p>
<p>【惯用法：for/select】</p>
<p>我们在使用select时很少只是对其进行一次evaluation，我们常常将其与for {}结合在一起使用，并选择适当时机从for{}中退出。</p>
<p><font face="Courier New">for {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; select {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; case x := &lt;- somechan:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // &#8230; 使用x进行一些操作</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; case y, ok := &lt;- someOtherchan:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // &#8230; 使用y进行一些操作，<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // 检查ok值判断someOtherchan是否已经关闭</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; case outputChan &lt;- z:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // &#8230; z值被成功发送到Channel上时</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; default:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // &#8230; 上面case均无法通信时，执行此分支<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	} </font></p>
<p>【终结workers】</p>
<p>下面是一个常见的终结sub worker goroutines的方法，每个worker goroutine通过select监视一个die channel来及时获取main goroutine的退出通知。</p>
<p><font face="Courier New">//testterminateworker1.go</font><br />
	<font face="Courier New">package main</font></p>
<p><font face="Courier New">import (<br />
	&nbsp;&nbsp;&nbsp; &quot;fmt&quot;<br />
	&nbsp;&nbsp;&nbsp; &quot;time&quot;<br />
	)</font></p>
<p><font face="Courier New">func worker(die chan bool, index int) {<br />
	&nbsp;&nbsp;&nbsp; fmt.Println(&quot;Begin: This is Worker:&quot;, index)<br />
	&nbsp;&nbsp;&nbsp; for {<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; select {<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; //case xx：<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; //做事的分支<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; case &lt;-die:<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; fmt.Println(&quot;Done: This is Worker:&quot;, index)<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; return<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp; }<br />
	}</font></p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp; die := make(chan bool)</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; for i := 1; i &lt;= 100; i++ {<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; go worker(die, i)<br />
	&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; time.Sleep(time.Second * 5)<br />
	&nbsp;&nbsp;&nbsp; close(die)<br />
	&nbsp;&nbsp;&nbsp; select {} </font><font face="Courier New"><font face="Courier New">//deadlock we expected</font><br />
	}</font></p>
<p>【终结验证】</p>
<p>有时候终结一个worker后，main goroutine想确认worker routine是否真正退出了，可采用下面这种方法：</p>
<p><font face="Courier New">//testterminateworker2.go<br />
	package main</font></p>
<p><font face="Courier New">import (<br />
	&nbsp;&nbsp;&nbsp; &quot;fmt&quot;<br />
	&nbsp;&nbsp;&nbsp; //&quot;time&quot;<br />
	)</font></p>
<p><font face="Courier New">func worker(die chan bool) {<br />
	&nbsp;&nbsp;&nbsp; fmt.Println(&quot;Begin: This is Worker&quot;)<br />
	&nbsp;&nbsp;&nbsp; for {<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; select {<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; //case xx：<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; //做事的分支<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; case &lt;-die:<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; fmt.Println(&quot;Done: This is Worker&quot;)<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; die &lt;- true<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; return<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp; }<br />
	}</font></p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp; die := make(chan bool)</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; go worker(die)</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; die &lt;- true<br />
	&nbsp;&nbsp;&nbsp; &lt;-die<br />
	&nbsp;&nbsp;&nbsp; fmt.Println(&quot;Worker goroutine has been terminated&quot;)<br />
	}</font></p>
<p>【关闭的Channel永远不会阻塞】</p>
<p>下面演示在一个已经关闭了的channel上读写的结果：</p>
<p><font face="Courier New">//testoperateonclosedchannel.go<br />
	package main</font></p>
<p><font face="Courier New">import &quot;fmt&quot;</font></p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; cb := make(chan bool)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; close(cb)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; x := &lt;-cb<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;%#v\n&quot;, x)</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; x, ok := &lt;-cb<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;%#v %#v\n&quot;, x, ok)</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ci := make(chan int)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; close(ci)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; y := &lt;-ci<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;%#v\n&quot;, y)</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; cb &lt;- true<br />
	}</font></p>
<p><font face="Courier New">$go run </font><font face="Courier New"><font face="Courier New">testoperateonclosedchannel.go</font><br />
	false<br />
	false false<br />
	0<br />
	panic: runtime error: send on closed channel</font></p>
<p>可以看到在一个已经close的unbuffered channel上执行读操作，回返回channel对应类型的零值，比如bool型channel返回false，int型channel返回0。但向close的channel写则会触发panic。不过无论读写都不会导致阻塞。</p>
<p>【关闭带缓存的channel】</p>
<p>将unbuffered channel换成buffered channel会怎样？我们看下面例子：</p>
<p><font face="Courier New">//testclosedbufferedchannel.go<br />
	package main</font></p>
<p><font face="Courier New">import &quot;fmt&quot;</font></p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; c := make(chan int, 3)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; c &lt;- 15<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; c &lt;- 34<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; c &lt;- 65<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; close(c)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;%d\n&quot;, &lt;-c)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;%d\n&quot;, &lt;-c)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;%d\n&quot;, &lt;-c)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;%d\n&quot;, &lt;-c)</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; c &lt;- 1<br />
	}</font></p>
<p><font face="Courier New">$go run testclosedbufferedchannel.go<br />
	15<br />
	34<br />
	65<br />
	0<br />
	panic: runtime error: send on closed channel</font></p>
<p>可以看出带缓冲的channel略有不同。尽管已经close了，但我们依旧可以从中读出关闭前写入的3个值。第四次读取时，则会返回该channel类型的零值。向这类channel写入操作也会触发panic。</p>
<p>【range】</p>
<p>Golang中的range常常和channel并肩作战，它被用来从channel中读取所有值。下面是一个简单的实例：</p>
<p><font face="Courier New">//testrange.go<br />
	package main</font></p>
<p><font face="Courier New">import &quot;fmt&quot;</font></p>
<p><font face="Courier New">func generator(strings chan string) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; strings &lt;- &quot;Five hour&#39;s New York jet lag&quot;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; strings &lt;- &quot;and Cayce Pollard wakes in Camden Town&quot;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; strings &lt;- &quot;to the dire and ever-decreasing circles&quot;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; strings &lt;- &quot;of disrupted circadian rhythm.&quot;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; close(strings)<br />
	}</font></p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; strings := make(chan string)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; go generator(strings)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; for s := range strings {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;%s\n&quot;, s)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;\n&quot;)<br />
	}</font></p>
<p><b>四、隐藏状态</b></p>
<p>下面通过一个例子来演示一下channel如何用来隐藏状态：</p>
<p>1、例子：唯一的ID服务</p>
<p><font face="Courier New">//testuniqueid.go<br />
	package main</font></p>
<p><font face="Courier New">import &quot;fmt&quot;</font></p>
<p><font face="Courier New">func newUniqueIDService() &lt;-chan string {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; id := make(chan string)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; go func() {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; var counter int64 = 0<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; for {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; id &lt;- fmt.Sprintf(&quot;%x&quot;, counter)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; counter += 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 />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return id<br />
	}<br />
	func main() {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; id := newUniqueIDService()<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; for i := 0; i &lt; 10; i++ {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Println(&lt;-id)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	}</font></p>
<p><font face="Courier New">$ go run testuniqueid.go<br />
	0<br />
	1<br />
	2<br />
	3<br />
	4<br />
	5<br />
	6<br />
	7<br />
	8<br />
	9</font></p>
<p>newUniqueIDService通过一个channel与main goroutine关联，main goroutine无需知道uniqueid实现的细节以及当前状态，只需通过channel获得最新id即可。</p>
<p><b>五、默认情况</b></p>
<p>我想这里John Graham-Cumming主要是想告诉我们select的default分支的实践用法。</p>
<p>1、select&nbsp; for non-blocking receive</p>
<p><font face="Courier New">idle:= make(chan []byte, 5) //用一个带缓冲的channel构造一个简单的队列</font></p>
<p><font face="Courier New">select {<br />
	case b = &lt;-idle:  //尝试从idle队列中读取<br />
	&nbsp;&nbsp;&nbsp; &#8230;<br />
	default:&nbsp; //队列空，分配一个新的buffer<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; makes += 1<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; b = make([]byte, size)<br />
	}</font></p>
<p>2、select for non-blocking send</p>
<p><font face="Courier New">idle:= make(chan []byte, 5) </font><font face="Courier New"><font face="Courier New">//用一个带缓冲的channel构造一个简单的队列</font></font></p>
<p><font face="Courier New">select {<br />
	case idle &lt;- b: //尝试向队列中插入一个buffer<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; //&#8230;<br />
	default: //队列满？</font></p>
<p><font face="Courier New">}</font></p>
<p><b>六、Nil Channels</b></p>
<p>1、nil channels阻塞</p>
<p>对一个没有初始化的channel进行读写操作都将发生阻塞，例子如下：</p>
<p><font face="Courier New">package main</font></p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; var c chan int<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &lt;-c<br />
	}</font></p>
<p><font face="Courier New">$go run testnilchannel.go<br />
	fatal error: all goroutines are asleep &#8211; deadlock!</font></p>
<p><font face="Courier New">package main</font></p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; var c chan int<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; c &lt;- 1<br />
	}</font></p>
<p><font face="Courier New">$go run testnilchannel.go<br />
	fatal error: all goroutines are asleep &#8211; deadlock!</font></p>
<p>2、nil channel在select中很有用</p>
<p>看下面这个例子：</p>
<p><font face="Courier New">//testnilchannel_bad.go<br />
	package main</font></p>
<p><font face="Courier New">import &quot;fmt&quot;<br />
	import &quot;time&quot;</font></p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; var c1, c2 chan int = make(chan int), make(chan int)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; go func() {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; time.Sleep(time.Second * 5)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; c1 &lt;- 5<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; close(c1)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }()</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; go func() {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; time.Sleep(time.Second * 7)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; c2 &lt;- 7<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; close(c2)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }()</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; select {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; case x := &lt;-c1:<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.Println(x)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; case x := &lt;-c2:<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.Println(x)<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 />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Println(&quot;over&quot;)<br />
	}</font></p>
<p>我们原本期望程序交替输出5和7两个数字，但实际的输出结果却是：</p>
<p><font face="Courier New">5<br />
	0<br />
	0<br />
	0<br />
	&#8230; &#8230; 0死循环</font></p>
<p>再仔细分析代码，原来select每次按case顺序evaluate：<br />
	&nbsp;&nbsp;&nbsp; &#8211; 前5s，select一直阻塞；<br />
	&nbsp;&nbsp;&nbsp; &#8211; 第5s，c1返回一个5后被close了，&ldquo;case x := &lt;-c1&rdquo;这个分支返回，select输出5，并重新select<br />
	&nbsp;&nbsp;&nbsp; &#8211; 下一轮select又从&ldquo;case x := &lt;-c1&rdquo;这个分支开始evaluate，由于c1被close，按照前面的知识，close的channel不会阻塞，我们会读出这个 channel对应类型的零值，这里就是0；select再次输出0；这时即便c2有值返回，程序也不会走到c2这个分支<br />
	&nbsp;&nbsp;&nbsp; &#8211; 依次类推，程序无限循环的输出0</p>
<p>我们利用nil channel来改进这个程序，以实现我们的意图，代码如下：</p>
<p><font face="Courier New">//testnilchannel.go<br />
	package main</font></p>
<p><font face="Courier New">import &quot;fmt&quot;<br />
	import &quot;time&quot;</font></p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; var c1, c2 chan int = make(chan int), make(chan int)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; go func() {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; time.Sleep(time.Second * 5)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; c1 &lt;- 5<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; close(c1)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }()</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; go func() {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; time.Sleep(time.Second * 7)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; c2 &lt;- 7<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; close(c2)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }()</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; select {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; case x, ok := &lt;-c1:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if !ok {<br />
	&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;&nbsp;&nbsp;&nbsp;&nbsp; c1 = 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; } else {<br />
	&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;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Println(x)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&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; case x, ok := &lt;-c2:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if !ok {<br />
	&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;&nbsp;&nbsp;&nbsp;&nbsp; c2 = 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; } else {<br />
	&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;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Println(x)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&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;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if c1 == nil &amp;&amp; c2 == 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; break<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 />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Println(&quot;over&quot;)<br />
	}</font></p>
<p><font face="Courier New">$go run testnilchannel.go<br />
	5<br />
	7<br />
	over</font></p>
<p>可以看出：通过将已经关闭的channel置为nil，下次select将会阻塞在该channel上，使得select继续下面的分支evaluation。</p>
<p><b>七、Timers</b></p>
<p>1、超时机制Timeout</p>
<p>带超时机制的select是常规的tip，下面是示例代码，实现30s的超时select：</p>
<p><font face="Courier New">func worker(start chan bool) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; timeout := time.After(30 * time.Second)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; for {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; select {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // &#8230; do some stuff<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; case &lt;- timeout:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return<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>2、心跳HeartBeart</p>
<p>与timeout实现类似，下面是一个简单的心跳select实现：</p>
<p><font face="Courier New">func worker(start chan bool) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; heartbeat := time.Tick(30 * time.Second)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; for {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; select {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // &#8230; do some stuff<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; case &lt;- heartbeat:<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; //&#8230; do heartbeat stuff<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 style='text-align:left'>&copy; 2014, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2014/09/29/a-channel-compendium-for-golang/feed/</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
		<item>
		<title>Ubuntu Server 14.04安装docker</title>
		<link>https://tonybai.com/2014/09/26/install-docker-on-ubuntu-server-1404/</link>
		<comments>https://tonybai.com/2014/09/26/install-docker-on-ubuntu-server-1404/#comments</comments>
		<pubDate>Fri, 26 Sep 2014 07:09:52 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[container]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[GCC]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Kernel]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Opensource]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[SSH]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[virtualbox]]></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=1547</guid>
		<description><![CDATA[近期在研究docker这一轻量级容器引擎，研究docker对日常开发测试工作以及产品部署运维工作能带来哪些便利。前些时候刚刚将工作环境从 Ubuntu搬到了Mac Air上，对Mac OS X的一切均不甚熟悉，给docker研究带来了不便，于是打算在VirtualBox中安装一Ubuntu Server作为docker之承载平台。这里记录一下安装配置过程，主要为了备忘，如果能给其他人带来帮助，我会甚感欣慰。 docker官方对ubuntu的支持是蛮好的。docker对Linux内核版本有要求，要&#62;=3.8，Ubuntu Server目前最新版本14.04.1恰符合这一要求，其kernel version = 3.13.0-32。 一、VirtualBox安装Ubuntu Server 14.04.1 VirtualBox安装Ubuntu OS做过了不止一遍，即便是换成最新的14.04.1 Server版，差别也没有太多，无非是按照安装提示，逐步Next。这里给Ubuntu Server 14.04分配了1G Memory, 32G动态硬盘空间。 【配置源】 &#160; 默认情况下，/etc/apt/sources.list中只有一组源：cn.archive.ubuntu.com/ubuntu。这个国外源的下载速度显然无法满足我的要求，于是我把我常用的sohu源加入sources.list中，并且放在前面： &#160; deb http://mirrors.sohu.com/ubuntu/ trusty main restricted &#160; deb http://mirrors.sohu.com/ubuntu/ trusty-security main restricted &#160; deb http://mirrors.sohu.com/ubuntu/ trusty-updates main restricted &#160; deb http://mirrors.sohu.com/ubuntu/ trusty-proposed main restricted &#160; deb http://mirrors.sohu.com/ubuntu/ trusty-backports main restricted &#160; [...]]]></description>
			<content:encoded><![CDATA[<p>近期在研究<a href="http://docker.com">docker</a>这一轻量级容器引擎，研究docker对日常开发测试工作以及产品部署运维工作能带来哪些便利。前些时候刚刚将工作环境从 <a href="http://tonybai.com/tag/ubuntu">Ubuntu</a>搬到了Mac Air上，对Mac OS X的一切均不甚熟悉，给docker研究带来了不便，于是打算在<a href="http://virtualbox.org">VirtualBox</a>中安装一Ubuntu Server作为docker之承载平台。这里记录一下安装配置过程，主要为了备忘，如果能给其他人带来帮助，我会甚感欣慰。</p>
<p>docker官方对<a href="http://ubuntu.com">ubuntu</a>的支持是蛮好的。docker对Linux内核版本有要求，要&gt;=3.8，Ubuntu Server目前最新版本14.04.1恰符合这一要求，其kernel version = 3.13.0-32。</p>
<p><strong>一、VirtualBox安装Ubuntu Server 14.04.1</strong></p>
<p>VirtualBox安装Ubuntu OS做过了不止一遍，即便是换成最新的14.04.1 Server版，差别也没有太多，无非是按照安装提示，逐步Next。这里给Ubuntu Server 14.04分配了1G Memory, 32G动态硬盘空间。</p>
<p>【配置源】</p>
<p>&nbsp; 默认情况下，/etc/apt/sources.list中只有一组源：cn.archive.ubuntu.com/ubuntu。这个国外源的下载速度显然无法满足我的要求，于是我把我常用的sohu源加入sources.list中，并且放在前面：</p>
<p><font face="Courier New">&nbsp; deb http://mirrors.sohu.com/ubuntu/ trusty main restricted<br />
	&nbsp; deb http://mirrors.sohu.com/ubuntu/ trusty-security main restricted<br />
	&nbsp; deb http://mirrors.sohu.com/ubuntu/ trusty-updates main restricted<br />
	&nbsp; deb http://mirrors.sohu.com/ubuntu/ trusty-proposed main restricted<br />
	&nbsp; deb http://mirrors.sohu.com/ubuntu/ trusty-backports main restricted</font></p>
<p><font face="Courier New">&nbsp; deb-src http://mirros.sohu.com/ubuntu/ trusty main restricted<br />
	&nbsp; deb-src http://mirrors.sohu.com/ubuntu/ trusty-security main restricted<br />
	&nbsp; deb-src http://mirrors.sohu.com/ubuntu/ trusty-updates main restricted<br />
	&nbsp; deb-src http://mirrors.sohu.com/ubuntu/ trusty-proposed main restricted<br />
	&nbsp; deb-src http://mirrors.sohu.com/ubuntu/ trusty-backports main restricted</font></p>
<p>&nbsp; 公司采用代理访问外网，于是还得在/etc/apt/apt.conf中加上代理的设置，否则无法更新源，也就无法安装第三方软件：</p>
<p><font face="Courier New">&nbsp; Acquire::http::Proxy &quot;http://username:passwd@proxyhost:proxyport&quot;;</font></p>
<p>&nbsp;【乱码处理】</p>
<p>&nbsp; 由于安装时候选择了中国区域（locale zh_CN.UTF-8），因此在VirtualBox的窗口中直接执行命令的提示信息可能是乱码。对于Server，我们一般是不会直接通过其主机显示 器登录使用的，都是通过终端访问，但在未安装和开启ssh服务和未配置端口转发前，我们只能先凑合这个窗口了。可先将/etc/default /locale中的LANGUAGE由&quot;zh_CN:zh&quot;改为&quot;en_US:en&quot;， logout后重新登录就可以看到非乱码的英文提示信息了。</p>
<p>【安装VirtualBox增强组件】</p>
<p>&nbsp; Ubuntu Server默认是不安装图形桌面的，只有一个命令行窗口，连鼠标都无法使用。因此增强组件安装的意义没有桌面系统那么强烈。我能想到的只有&ldquo;共享目录&rdquo;这一个功能有些用处。</p>
<p>&nbsp; 安装方法也不难，按下面步骤逐步操作即可：</p>
<p><font face="Courier New">&nbsp; sudo apt-get install build-essential linux-headers-$(uname -r) dkms gcc g++<br />
	&nbsp; sudo mnt /dev/cdrom /mnt<br />
	&nbsp; cd /mnt<br />
	&nbsp; sudo bash ./VBoxLinuxAdditions.run</font></p>
<p>&nbsp; 如果结果都是&quot;done&quot;，重启后就ok了。</p>
<p>【安装ssh服务】</p>
<p>&nbsp;&nbsp;&nbsp; ssh服务由openssh-server提供：<br />
	&nbsp;&nbsp;&nbsp; <font face="Courier New">sudo apt-get openssh-server</font><br />
	&nbsp;&nbsp;&nbsp;<br />
	&nbsp;&nbsp; 安装成功后，ssh server服务就会自动启动起来。</p>
<p>&nbsp;&nbsp; 不过我们还是需要修改一些配置，比如允许Root登录：打开/etc/ssh/sshd_config，将PermitRootLogin后面的内容改为yes。<br />
	&nbsp;&nbsp;&nbsp;<br />
	【设置端口转发】</p>
<p>&nbsp; 前面说过，对于Server，我们更多是在其他主机上通过ssh或telnet远程访问该Server并执行各种操作。由于这里是VirtualBox安 装的虚拟机，其他主机无法看到这台Server，我们需要设置端口转发将外部访问的数据转发给这个内部虚拟Server。</p>
<p>&nbsp; 我们通过VirtualBox软件提供的图形界面即可完成这个操作：<br />
	&nbsp;&nbsp;&nbsp; 1、&ldquo;设置&rdquo;这个虚拟机<br />
	&nbsp;&nbsp;&nbsp; 2、在&ldquo;网络&rdquo;标签中，点击&ldquo;端口转发&rdquo;按钮，进入端口转发规则添加窗口。<br />
	&nbsp;&nbsp;&nbsp; 3、添加一条规则：<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 名称：ssh-rules<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 协议：TCP<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 主机IP、子系统IP可以为空。<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 主机端口：2222<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 子系统端口：22<br />
	&nbsp;&nbsp; 4、配置结束</p>
<p>&nbsp;&nbsp;&nbsp; 配置结束后，我们在宿主机上netstat -an|grep 2222，可以看到VirtualBox增加了该端口2222的监听。</p>
<p>&nbsp; 现在我们就可以在其他机器上通过ssh -l tonybai 宿主机ip -p 2222的方式登录到我们新安装的这台虚拟Server了。</p>
<p>&nbsp;&nbsp;<br />
	<b>二、安装docker</b></p>
<p>docker目前的最新版本号是1.2.0，但14.04源中的docker还是正式稳定版1.0之前的版本，显然这是无法满足我的要求的。我们只能另外添加docker源来安装最新版docker。</p>
<p>&nbsp; 【安装docker】</p>
<p>&nbsp;&nbsp;&nbsp; 我们在/etc/apt/sources.list中加入下面这个源：<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <font face="Courier New">deb <a class="moz-txt-link-freetext" href="http://mirror.yandex.ru/mirrors/docker/">http://mirror.yandex.ru/mirrors/docker/</a> docker main</font><br />
	&nbsp;&nbsp;<br />
	&nbsp;&nbsp;&nbsp; 执行apt-get update。</p>
<p>&nbsp;&nbsp;&nbsp; sudo apt-get install lxc-docker</p>
<p><font face="Courier New">正在读取软件包列表&#8230; 完成<br />
	正在分析软件包的依赖关系树&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br />
	正在读取状态信息&#8230; 完成&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br />
	将会安装下列额外的软件包：<br />
	&nbsp; aufs-tools cgroup-lite git git-man liberror-perl lxc-docker-1.2.0<br />
	建议安装的软件包：<br />
	&nbsp; git-daemon-run git-daemon-sysvinit git-doc git-el git-email git-gui gitk<br />
	&nbsp; gitweb git-arch git-bzr git-cvs git-mediawiki git-svn<br />
	下列新软件包将被安装：<br />
	&nbsp; aufs-tools cgroup-lite git git-man liberror-perl lxc-docker lxc-docker-1.2.0<br />
	升级了 0 个软件包，新安装了 7 个软件包，要卸载 0 个软件包，有 59 个软件包未被升级。<br />
	需要下载 7,477 kB 的软件包。<br />
	解压缩后会消耗掉 35.4 MB 的额外空间。<br />
	您希望继续执行吗？ [Y/n] y</font></p>
<p>&nbsp; 这个源里的docker居然是最新版。于是安装之。安装后，我们执行docker version来确认一下安装是否成功。</p>
<p><font face="Courier New">&nbsp; tonybai@ubuntu-Server-14:~$ docker version<br />
	Client version: 1.2.0<br />
	Client API version: 1.14<br />
	Go version (client): go1.3.1<br />
	Git commit (client): fa7b24f<br />
	OS/Arch (client): linux/amd64<br />
	2014/09/26 13:56:53 Get http:///var/run/docker.sock/v1.14/version: dial unix /var/run/docker.sock: permission denied</font></p>
<p>&nbsp; 【为docker设置http代理】</p>
<p>&nbsp;&nbsp;&nbsp; 在公司内使用代理才能访问到外网，于是我们也需要为docker命令设置代理以使其顺利执行命令。</p>
<p>&nbsp;&nbsp;&nbsp; 我们安装的docker实际上分为两部分，docker命令行和docker daemon。两者是C/S结构，docker命令行将用户的请求转发给docker daemon，后者会真正与外部通信完成各种操作。</p>
<p>&nbsp;&nbsp;&nbsp; 于是我们可以这样为docker daemon设置http_proxy:<br />
	<font face="Courier New">&nbsp;&nbsp;&nbsp; sudo service docker stop<br />
	&nbsp;&nbsp;&nbsp; sudo http_proxy=&#39;<a class="moz-txt-link-freetext" href="http://baim:xxx@proxy.neusoft.com:8080">http://user:passwd@proxyhost:port</a>&#39; docker -d &amp;</font></p>
<p>&nbsp;&nbsp;&nbsp; 这样设置启动后，我们可以通过下面命令测试设置是否ok：</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<font face="Courier New"> sudo docker search ubuntu</font></p>
<p>&nbsp;&nbsp;&nbsp; 如果你看到下面信息，说明设置成功了：</p>
<p>&nbsp;<font face="Courier New">&nbsp;&nbsp; tonybai@ubuntu-Server-14:~$ sudo docker search ubuntu<br />
	[info] GET /v1.14/images/search?term=ubuntu<br />
	[b36518a9] +job search(ubuntu)<br />
	[b36518a9] -job search(ubuntu) = OK (0)<br />
	NAME&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;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; DESCRIPTION&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;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; STARS&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;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; OFFICIAL&nbsp;&nbsp; AUTOMATED<br />
	ubuntu&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;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Official Ubuntu base image&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 709&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;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; [OK]&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br />
	dockerfile/ubuntu&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;&nbsp;&nbsp;&nbsp;&nbsp; Trusted automated Ubuntu (http://www.ubunt&#8230;&nbsp;&nbsp; 24&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;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; [OK]<br />
	crashsystems/gitlab-docker&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; A trusted, regularly updated build of GitL&#8230;&nbsp;&nbsp; 20&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;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; [OK]<br />
	ubuntu-upstart&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;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Upstart is an event-based replacement for &#8230;&nbsp;&nbsp; 13&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;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; [OK]&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </font><br />
	&#8230; &#8230;.</p>
<p>&nbsp;</p>
<p style='text-align:left'>&copy; 2014, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2014/09/26/install-docker-on-ubuntu-server-1404/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
