<?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; Namespace</title>
	<atom:link href="http://tonybai.com/tag/namespace/feed/" rel="self" type="application/rss+xml" />
	<link>https://tonybai.com</link>
	<description>一个程序员的心路历程</description>
	<lastBuildDate>Wed, 15 Apr 2026 23:35:12 +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>如果《疯狂动物城》是一个分布式系统，那它一定是用 Go 写的</title>
		<link>https://tonybai.com/2025/12/06/zootopia-distributed-system-written-in-go/</link>
		<comments>https://tonybai.com/2025/12/06/zootopia-distributed-system-written-in-go/#comments</comments>
		<pubDate>Sat, 06 Dec 2025 14:05:50 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[影音坊]]></category>
		<category><![CDATA[技术志]]></category>
		<category><![CDATA[BlockingI/O]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[Cgroup]]></category>
		<category><![CDATA[cloudnative]]></category>
		<category><![CDATA[distributedsystem]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[GMP]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Gopher]]></category>
		<category><![CDATA[goroutine]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[Microservices]]></category>
		<category><![CDATA[Namespace]]></category>
		<category><![CDATA[QPS]]></category>
		<category><![CDATA[Zootopia]]></category>
		<category><![CDATA[Zootopia2]]></category>
		<category><![CDATA[分布式系统]]></category>
		<category><![CDATA[协程]]></category>
		<category><![CDATA[容器化]]></category>
		<category><![CDATA[微服务]]></category>
		<category><![CDATA[架构师]]></category>
		<category><![CDATA[环境隔离]]></category>
		<category><![CDATA[疯狂动物城]]></category>
		<category><![CDATA[疯狂动物城2]]></category>
		<category><![CDATA[调度模型]]></category>
		<category><![CDATA[高并发]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=5489</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/12/06/zootopia-distributed-system-written-in-go 大家好，我是Tony Bai。 文章开始前，先给各位道个歉，今天的标题确实有点“党”。 毕竟，非要说一个满是毛茸茸动物的动画片是用 Go 语言写的，这脑洞开得确实有点大。 但请原谅一个老程序员的“职业病”。 为了迎接本周末《疯狂动物城2》的观影家庭活动，上个周末，我特意腾出时间，陪家里5岁的二娃重温了第一部经典。原本我是想好好享受亲子时光的，可看着看着，作为写了十几年代码的程序员，我的关注点却莫名其妙地“跑偏”了。 当看到那座容纳了冰川、沙漠、雨林，拥有千万级“居民并发量”的超级城市运转得如此丝滑时，我脑子里的画面变了：这越看越像一个设计精良的云原生分布式系统；而那个身手敏捷的兔子警官，怎么看都像一只跑在服务器里的 Gopher…… 于是，我忍不住这股“胡思乱想”的冲动，决定一本正经地胡说八道一番。 如果你也好奇，当一个架构师戴着“代码滤镜”看电影时，到底看到了什么？不妨继续听我聊聊 在我眼里，如果要把这座“动物城”搬到服务器上，它的底层架构，一定是用 Go 语言写的。 为什么这么说？因为陪娃看电影的过程中，我仿佛看到了 Go 语言设计哲学的完美具象化。 那个巨大的“空调墙”与容器化 电影最震撼的一幕，莫过于朱迪坐火车进城。 火车穿过烈日炎炎的撒哈拉广场（Sahara Square），下一秒就钻进了冰天雪地的冰川镇（Tundratown）。 女儿指着屏幕好奇地问我：“爸爸，为什么那边那么热，这边这么冷，它们在一起不会化掉吗？” 我指着那道巨大的分隔墙说：“因为有那堵墙呀，它把热气和冷气隔开了。” 在那一刻，我脑子里闪过的其实是 Docker 和 Kubernetes。 在传统的系统里，不同环境的应用混在一起很容易“打架”（环境冲突）。而在动物城里，为了让北极熊（需要低温库）和骆驼（需要高温环境）在同一台“物理机”上共存，设计师构建了最极致的环境隔离。 这不正是 Go 语言统治的云原生世界吗？ Go 语言构建了 Docker，构建了 Kubernetes。正是这些基础设施，像那道巨大的空调墙一样，通过 Namespace（命名空间）和 Cgroup（资源限制），让成千上万个习性迥异的“服务”互不干扰，在此消彼长的流量洪峰中，不仅没“化掉”，还活得很好。 树懒“闪电”与高并发的噩梦 重温经典，依然被树懒“闪电”查车牌那段笑出内伤。 女儿笑得在沙发上捧腹：“爸爸，他太慢了！朱迪急死了！” 我跟着笑，但心里却是一阵恶寒——这简直是每一个后端工程师的噩梦：主线程阻塞（Blocking I/O）。 试想一下，如果动物城的市政大厅系统是单线程的，一只树懒卡在窗口办业务，后面排队的一万只动物全得等着。整个城市的吞吐量（QPS）瞬间归零，系统直接宕机。 但动物城（Zootopia）作为一个千万人口的超大系统，依然运转良好，说明它底层一定解决了这个问题。 如果是用 Go 写的，这就很好解释了。 Go 的设计哲学里，最核心的就是“高并发”。面对慢吞吞的“树懒式”任务（比如网络等待、文件读取），Go 不会傻等。它会派出一个轻量级的 goroutine（协程）去盯着树懒，主线程立马转头去处理下一只豹子或兔子的请求。 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/zootopia-distributed-system-written-in-go-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/12/06/zootopia-distributed-system-written-in-go">本文永久链接</a> &#8211; https://tonybai.com/2025/12/06/zootopia-distributed-system-written-in-go</p>
<p>大家好，我是Tony Bai。</p>
<p><strong>文章开始前，先给各位道个歉，今天的标题确实有点“党”。</strong></p>
<p>毕竟，非要说一个满是毛茸茸动物的动画片是用 Go 语言写的，这脑洞开得确实有点大。</p>
<p>但请原谅一个老程序员的“职业病”。</p>
<p>为了迎接本周末《疯狂动物城2》的观影家庭活动，上个周末，我特意腾出时间，陪家里5岁的二娃重温了第一部经典。原本我是想好好享受亲子时光的，可看着看着，作为写了十几年代码的程序员，我的关注点却莫名其妙地“跑偏”了。</p>
<p>当看到那座容纳了冰川、沙漠、雨林，拥有千万级“居民并发量”的超级城市运转得如此丝滑时，我脑子里的画面变了：这越看越像一个设计精良的<strong>云原生分布式系统</strong>；而那个身手敏捷的兔子警官，怎么看都像一只跑在服务器里的 <strong>Gopher</strong>……</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/zootopia-distributed-system-written-in-go-2.jpg" alt="" /></p>
<p>于是，我忍不住这股“胡思乱想”的冲动，决定一本正经地胡说八道一番。</p>
<p><strong>如果你也好奇，当一个架构师戴着“代码滤镜”看电影时，到底看到了什么？不妨继续听我聊聊</strong></p>
<p>在我眼里，如果要把这座“动物城”搬到服务器上，它的底层架构，一定是用 <strong>Go 语言</strong>写的。</p>
<p>为什么这么说？因为陪娃看电影的过程中，我仿佛看到了 Go 语言设计哲学的完美具象化。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/paid/google-adk-in-action-qr.png" alt="" /></p>
<h2>那个巨大的“空调墙”与容器化</h2>
<p>电影最震撼的一幕，莫过于朱迪坐火车进城。</p>
<p>火车穿过烈日炎炎的撒哈拉广场（Sahara Square），下一秒就钻进了冰天雪地的冰川镇（Tundratown）。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/zootopia-distributed-system-written-in-go-3.jpg" alt="" /></p>
<p>女儿指着屏幕好奇地问我：“爸爸，为什么那边那么热，这边这么冷，它们在一起不会化掉吗？”</p>
<p>我指着那道巨大的分隔墙说：“因为有那堵墙呀，它把热气和冷气隔开了。”</p>
<p>在那一刻，我脑子里闪过的其实是 <strong>Docker 和 Kubernetes</strong>。</p>
<p>在传统的系统里，不同环境的应用混在一起很容易“打架”（环境冲突）。而在动物城里，为了让北极熊（需要低温库）和骆驼（需要高温环境）在同一台“物理机”上共存，设计师构建了最极致的<strong>环境隔离</strong>。</p>
<p>这不正是 Go 语言统治的云原生世界吗？</p>
<p>Go 语言构建了 Docker，构建了 Kubernetes。正是这些基础设施，像那道巨大的空调墙一样，通过 Namespace（命名空间）和 Cgroup（资源限制），让成千上万个习性迥异的“服务”互不干扰，在此消彼长的流量洪峰中，不仅没“化掉”，还活得很好。</p>
<h2>树懒“闪电”与高并发的噩梦</h2>
<p>重温经典，依然被树懒“闪电”查车牌那段笑出内伤。</p>
<p>女儿笑得在沙发上捧腹：“爸爸，他太慢了！朱迪急死了！”</p>
<p>我跟着笑，但心里却是一阵恶寒——这简直是每一个后端工程师的噩梦：<strong>主线程阻塞（Blocking I/O）</strong>。</p>
<p>试想一下，如果动物城的市政大厅系统是单线程的，一只树懒卡在窗口办业务，后面排队的一万只动物全得等着。整个城市的吞吐量（QPS）瞬间归零，系统直接宕机。</p>
<p>但动物城（Zootopia）作为一个千万人口的超大系统，依然运转良好，说明它底层一定解决了这个问题。</p>
<p>如果是用 <strong>Go</strong> 写的，这就很好解释了。</p>
<p>Go 的设计哲学里，最核心的就是<strong>“高并发”</strong>。面对慢吞吞的“树懒式”任务（比如网络等待、文件读取），Go 不会傻等。它会派出一个轻量级的 goroutine（协程）去盯着树懒，主线程立马转头去处理下一只豹子或兔子的请求。</p>
<p>在这个庞大的系统里，也许有成千上万只“树懒”在慢动作，但整个城市依然像朱迪一样反应灵敏、健步如飞。这就是 Go 语言 GMP 调度模型的魔力。</p>
<h2>朱迪警官：小身材，大能量</h2>
<p>最后，说说我们的主角，兔子朱迪。</p>
<p>在满是大象、犀牛、北极熊的警局里，朱迪显得太小了。她没有庞大的身躯，起初也不被看好，被安排去贴罚单。</p>
<p>这像极了 Go 语言刚诞生时的处境。相比于 Java（大象）的厚重、C++（犀牛）的复杂，Go 显得语法简单、标准库精简，甚至生成的二进制文件都很小，一度被认为是“玩具语言”。</p>
<p>但朱迪凭什么破了大案？</p>
<p><strong>靠的是灵活性、执行力和低资源消耗。</strong></p>
<p>她能钻进犀牛进不去的狭窄管道（相对低内存的占用），她能在他人的视野盲区快速穿梭（极速启动）。</p>
<p>在构建现代微服务架构时，我们越来越不喜欢笨重的“单体应用”，而倾向于像朱迪这样<strong>小而美、独立部署、逻辑清晰</strong>的服务。</p>
<p>Go 语言就是代码世界里的“朱迪”。它剔除了所有花哨的语法糖，强制你写出清晰（甚至有点死板）的代码，但正是这种克制和高效，让它成为了支撑起整个动物城（云原生生态）最坚实的骨架。</p>
<h2>写在最后</h2>
<p>电影结束了，女儿意犹未尽，还在模仿朱迪的动作。</p>
<p>她问我：“爸爸，下周我们去看《疯狂动物城》第二部，朱迪会不会变得更厉害？”</p>
<p>我说：“肯定会啊，因为她一直在努力让这个城市变得更好。”</p>
<p>作为程序员，我们写下的每一行代码，何尝不是在构建一个虚拟的“动物城”？我们选择 Go，选择各种架构，不过是为了让这个系统更包容、更稳定，让里面的“居民”生活得更好。</p>
<p><strong>这周末，我将带娃直击《疯狂动物城2》。</strong> 听说这一次，动物城面临了前所未有的复杂危机。</p>
<p>届时，我会继续为大家带来<strong>“程序员眼中的《疯狂动物城2》”</strong>，看看在新的挑战下，我们的“系统架构”又该如何进化？</p>
<p>敬请期待！</p>
<hr />
<p><strong>互动话题：</strong></p>
<p>在重温经典电影时，你有没有因为“职业病”而产生过什么奇怪的联想？欢迎在评论区分享你的脑洞！</p>
<hr />
<p>还在为“复制粘贴喂AI”而烦恼？我的新专栏 <strong>《<a href="http://gk.link/a/12EPd">AI原生开发工作流实战</a>》</strong> 将带你：</p>
<ul>
<li>告别低效，重塑开发范式</li>
<li>驾驭AI Agent(Claude Code)，实现工作流自动化</li>
<li>从“AI使用者”进化为规范驱动开发的“工作流指挥家”</li>
</ul>
<p>扫描下方二维码，开启你的AI原生开发之旅。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/ai-native-dev-workflow-qr.png" alt="" /></p>
<hr />
<p>你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？</p>
<ul>
<li>想写出更地道、更健壮的Go代码，却总在细节上踩坑？</li>
<li>渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？</li>
<li>想打造生产级的Go服务，却在工程化实践中屡屡受挫？</li>
</ul>
<p>继《<a href="http://gk.link/a/10AVZ">Go语言第一课</a>》后，我的《<a href="http://gk.link/a/12yGY">Go语言进阶课</a>》终于在极客时间与大家见面了！</p>
<p>我的全新极客时间专栏 《<a href="http://gk.link/a/12yGY">Tony Bai·Go语言进阶课</a>》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。</p>
<p>目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /></p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/12/06/zootopia-distributed-system-written-in-go/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go项目设计的“七宗罪”？警惕那些流行的“反模式”</title>
		<link>https://tonybai.com/2025/04/21/go-project-design-antipatterns/</link>
		<comments>https://tonybai.com/2025/04/21/go-project-design-antipatterns/#comments</comments>
		<pubDate>Sun, 20 Apr 2025 23:20:07 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[anti-pattern]]></category>
		<category><![CDATA[Bug]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[cmd]]></category>
		<category><![CDATA[common]]></category>
		<category><![CDATA[DAG]]></category>
		<category><![CDATA[DesignPattern]]></category>
		<category><![CDATA[DRY]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[GoProverbs]]></category>
		<category><![CDATA[handler]]></category>
		<category><![CDATA[helpers]]></category>
		<category><![CDATA[import]]></category>
		<category><![CDATA[Interface]]></category>
		<category><![CDATA[internal]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[linter]]></category>
		<category><![CDATA[Mock]]></category>
		<category><![CDATA[model]]></category>
		<category><![CDATA[Namespace]]></category>
		<category><![CDATA[Package]]></category>
		<category><![CDATA[pkg]]></category>
		<category><![CDATA[Service]]></category>
		<category><![CDATA[shared]]></category>
		<category><![CDATA[Testing]]></category>
		<category><![CDATA[util]]></category>
		<category><![CDATA[utils]]></category>
		<category><![CDATA[依赖包]]></category>
		<category><![CDATA[函数]]></category>
		<category><![CDATA[包]]></category>
		<category><![CDATA[反模式]]></category>
		<category><![CDATA[循环导入]]></category>
		<category><![CDATA[抽象]]></category>
		<category><![CDATA[接口]]></category>
		<category><![CDATA[接口隔离]]></category>
		<category><![CDATA[最佳实践]]></category>
		<category><![CDATA[标准]]></category>
		<category><![CDATA[测试]]></category>
		<category><![CDATA[简洁]]></category>
		<category><![CDATA[耦合]]></category>
		<category><![CDATA[表驱动测试]]></category>
		<category><![CDATA[规范]]></category>
		<category><![CDATA[设计模式]]></category>
		<category><![CDATA[过度设计]]></category>
		<category><![CDATA[项目布局]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4596</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/04/21/go-project-design-antipatterns 大家好，我是Tony Bai。 在软件开发这个行当里，“最佳实践”、“设计模式”、“标准规范”这些词汇总是自带光环。它们总是承诺会带来更好的代码质量、可维护性和扩展性。然而，当这些“圣经”般的原则被生搬硬套到Go语言的语境下时，有时非但不能带来预期的好处，反而可能把我们引入“歧途”，滋生出一些看似“专业”实则有害的“反模式”。 最近我也拜读了几篇国外开发者关于Go项目布局和设计哲学的文章，结合我自己这些年的实践和观察，我愈发觉得，Go社区中确实存在一些需要警惕的、流行的设计“反模式”。这些“反模式”很多人都或多或少的使用过，包括曾经的我自己。 在这篇文章中，我就总结一下我眼中的Go项目设计“七宗罪”，希望能帮助大家在实践中保持清醒，做出更符合Go精神的决策。 第一宗罪：为了结构而结构——过度分层与分组 表现： 项目伊始，不假思索地创建pkg/、internal/、cmd/、util/、model/、handler/、service/ 等层层嵌套的目录，美其名曰“组织清晰”、“符合标准”。 危害： * 违背简洁： Go 的核心哲学是简洁。不必要的目录层级增加了认知负担和导航成本。 * 过早抽象/耦合： 在需求尚不明确时就划分 service、handler 等，可能导致错误的抽象边界和不必要的耦合。 * pkg/ 的迷思： pkg/ 是一个过时的、缺乏语义的约定，Go官方在Go 1.4时将Go项目中的pkg层次去掉了，Go官方的module布局指南中也使用了更多有意义的名字代替了pkg。 * internal/ 的滥用： 它是 Go 工具链的一个特性，用于保护内部实现不被外部导入。但如果你的项目根本不作为库被外部依赖，或者需要保护的代码很少，强制使用 internal/ 只会徒增复杂性。 * cmd/ 的误用： 除非你的仓库包含多个独立的可执行文件，否则将单一的main.go放入cmd/毫无必要。 解药： 保持扁平！从根目录开始，根据实际的功能或领域需要创建有意义的包。让结构随着项目的增长有机演化，而不是一开始就套用模板。 注：笔者当年也是pkg的“忠实粉丝”，新创建一个项目，无论规模大小，总喜欢先将pkg目录预创建出来。现在是时候根据项目的演进和规模的增长来判断是否需要”pkg”这个有点像“namespace”的目录了，即当你有多个希望公开的库时，是否用pkg/作为一个顶层分组，这个是要基于项目的实际情况进行判断的。 第二宗罪：无效的“美化运动”——无价值的重构与移动 表现： 为了让代码看起来“更干净”、“更符合某种设计模式”或“消除Linter警告”，在没有明确收益（修复 Bug、增加功能、提升性能、解决安全问题）的情况下，大规模地移动代码、修改变量名、调整文件结构。 危害： * 浪费时间精力： 投入大量时间做无意义的表面文章。 * 引入风险： 任何修改都有引入新 Bug [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/go-project-design-antipatterns-1.jpg" alt="" /></p>
<p><a href="https://tonybai.com/2025/04/21/go-project-design-antipatterns">本文永久链接</a> &#8211; https://tonybai.com/2025/04/21/go-project-design-antipatterns</p>
<p>大家好，我是Tony Bai。</p>
<p>在软件开发这个行当里，“最佳实践”、“设计模式”、“标准规范”这些词汇总是自带光环。它们总是承诺会带来更好的代码质量、可维护性和扩展性。然而，当这些“圣经”般的原则被生搬硬套到Go语言的语境下时，有时非但不能带来预期的好处，反而可能把我们引入“歧途”，滋生出一些看似“专业”实则有害的“反模式”。</p>
<p>最近我也拜读了几篇国外开发者关于Go项目布局和设计哲学的文章，结合我自己这些年的实践和观察，我愈发觉得，Go社区中确实存在一些需要警惕的、流行的设计“反模式”。这些“反模式”很多人都或多或少的使用过，包括曾经的我自己。</p>
<p>在这篇文章中，我就总结一下我眼中的Go项目设计“七宗罪”，希望能帮助大家在实践中保持清醒，做出更符合Go精神的决策。</p>
<h2>第一宗罪：为了结构而结构——过度分层与分组</h2>
<p><img src="https://tonybai.com/wp-content/uploads/2025/go-project-design-antipatterns-2.jpg" alt="" /></p>
<p><strong>表现：</strong> 项目伊始，不假思索地创建pkg/、internal/、cmd/、util/、model/、handler/、service/ 等层层嵌套的目录，美其名曰“组织清晰”、“符合标准”。</p>
<p><strong>危害：</strong><br />
*   <strong>违背简洁：</strong> Go 的核心哲学是简洁。不必要的目录层级增加了认知负担和导航成本。<br />
*   <strong>过早抽象/耦合：</strong> 在需求尚不明确时就划分 service、handler 等，可能导致错误的抽象边界和不必要的耦合。<br />
*   <strong>pkg/ 的迷思：</strong> pkg/ 是一个过时的、缺乏语义的约定，Go官方在<a href="https://tonybai.com/2014/11/04/some-changes-in-go-1-4">Go 1.4</a>时将Go项目中的pkg层次去掉了，<a href="https://go.dev/doc/modules/layout">Go官方的module布局指南</a>中也使用了更多有意义的名字代替了pkg。<br />
*   <strong>internal/ 的滥用：</strong> 它是 Go 工具链的一个特性，用于保护内部实现不被外部导入。但如果你的项目根本不作为库被外部依赖，或者需要保护的代码很少，强制使用 internal/ 只会徒增复杂性。<br />
*   <strong>cmd/ 的误用：</strong> 除非你的仓库包含多个独立的可执行文件，否则将单一的main.go放入cmd/毫无必要。</p>
<p><strong>解药：</strong> 保持扁平！从根目录开始，根据<strong>实际的功能或领域</strong>需要创建<strong>有意义的包</strong>。让结构随着项目的增长<strong>有机演化</strong>，而不是一开始就套用模板。</p>
<blockquote>
<p>注：笔者当年也是pkg的“忠实粉丝”，新创建一个项目，无论规模大小，总喜欢先将pkg目录预创建出来。现在是时候根据项目的演进和规模的增长来判断是否需要”pkg”这个有点像“namespace”的目录了，即当你有多个希望公开的库时，是否用pkg/作为一个顶层分组，这个是要基于项目的实际情况进行判断的。</p>
</blockquote>
<h2>第二宗罪：无效的“美化运动”——无价值的重构与移动</h2>
<p><img src="https://tonybai.com/wp-content/uploads/2025/go-project-design-antipatterns-3.jpg" alt="" /></p>
<p><strong>表现：</strong> 为了让代码看起来“更干净”、“更符合某种设计模式”或“消除Linter警告”，在没有明确收益（修复 Bug、增加功能、提升性能、解决安全问题）的情况下，大规模地移动代码、修改变量名、调整文件结构。</p>
<p><strong>危害：</strong><br />
*   <strong>浪费时间精力：</strong> 投入大量时间做无意义的表面文章。<br />
*   <strong>引入风险：</strong> 任何修改都有引入新 Bug 的风险，没有价值的修改更是得不偿失。<br />
*   <strong>增加 Code Review 负担：</strong> 团队成员需要花费时间理解这些非功能性的变更。<br />
*   <strong>违背价值驱动：</strong> 软件工程的核心是交付价值，而不是追求代码的“艺术感”。</p>
<p><strong>解药：</strong> 坚持<strong>价值驱动</strong>的变更！在做任何结构或代码调整前，严格拷问自己：这个改动解决了什么<strong>真实的、当前存在</strong>的问题？它的收益是否能明确衡量并大于风险？</p>
<h2>第三宗罪：接口的“原罪”——过早、过度的抽象</h2>
<p><img src="https://tonybai.com/wp-content/uploads/2025/go-project-design-antipatterns-4.jpg" alt="" /></p>
<p><strong>表现：</strong><br />
*   在只有一个具体实现的情况下，就为其定义接口。<br />
*   定义庞大、臃肿的接口，包含过多方法。<br />
*   为了“可测试性”而无脑地给所有东西加上接口。</p>
<p><strong>危害：</strong><br />
*   <strong>不必要的抽象：</strong> 接口是为了解耦和多态。在不需要这些时引入接口，只会增加代码量和理解成本。<br />
*   <strong>弱化抽象能力：</strong> “接口越大，抽象越弱”（来自<a href="https://go-proverbs.github.io/">Go谚语</a>）。大接口难以实现和维护，它变得模糊，难以理解哪些方法是真正必要的，也失去了其作为“契约”的精准性。<br />
*   <strong>阻碍演化：</strong> 过早定义接口可能锁定不成熟的设计，后续修改成本更高。<br />
*   <strong>测试的借口：</strong> Go拥有强大的测试工具（如<a href="https://tonybai.com/2024/01/01/go-testing-by-example">表驱动测试</a>），很多时候并不需要接口来实现可测试性。为测试而引入的接口可能扭曲生产代码的设计。</p>
<p><strong>解药：</strong><br />
*   <strong>拥抱具体：</strong> 先写具体实现。<br />
*   <strong>发现接口，而非设计接口：</strong> 只有当你<strong>确实需要</strong>多种实现（包括<a href="https://tonybai.com/2023/04/20/provide-fake-object-for-external-collaborators">测试中的Mock</a>，但要谨慎对待），或者需要<strong>打破循环依赖</strong>时，才考虑提取接口。<br />
*   <strong>保持接口小巧、正交：</strong> 遵循接口隔离原则。</p>
<h2>第四宗罪：“大杂烩”的诱惑——utils/common/shared 黑洞</h2>
<p><img src="https://tonybai.com/wp-content/uploads/2025/go-project-design-antipatterns-5.jpg" alt="" /></p>
<p><strong>表现：</strong> 创建一个名为 utils、common、shared 或 helpers 的包，把各种看似“通用”的函数、类型塞进去。</p>
<p><strong>危害：</strong><br />
*   <strong>职责不清：</strong> 这些包缺乏明确的领域或功能归属，成为代码的“垃圾抽屉”。<br />
*   <strong>依赖洼地：</strong> 随着项目增长，这些包往往会依赖越来越多的其他包，同时也被越来越多的包依赖，极易引发循环依赖或成为构建瓶颈。<br />
*   <strong>降低内聚性：</strong> 本应属于特定领域的功能被剥离出来，破坏了原有包的内聚性。</p>
<p><strong>解药：</strong><br />
*   <strong>就近原则：</strong> 如果一个“工具函数”只被一个包使用，就把它放在那个包里（可以是私有的）。<br />
*   <strong>功能归类：</strong> 如果一个“工具函数”被多个包使用，思考它真正属于哪个<strong>功能领域</strong>，为其创建一个<strong>有意义的</strong>新包（例如 applog 而不是 logutil）。<br />
*   <strong>思考依赖方向：</strong> 真正通用的基础库（如自定义的 string 处理、时间处理）应该处于依赖关系图的底层，不应依赖上层业务逻辑。</p>
<blockquote>
<p>注：坦白说，其他几项“罪过”或许还只是部分开发者的“偶发行为”，但这“第四宗罪”——随手创建 utils 或 common 包——恐怕是我们绝大多数人都曾犯过，甚至习以为常的“通病”。笔者也是如此:)。</p>
</blockquote>
<h2>第五宗罪：对 DRY 的“迷信”——为了“不重复”而引入不当依赖</h2>
<p><img src="https://tonybai.com/wp-content/uploads/2025/go-project-design-antipatterns-6.jpg" alt="" /></p>
<p><strong>表现：</strong> 为了避免几行相似代码的重复，强行提取公共函数或类型，并为此引入新的包依赖，有时甚至导致复杂的依赖关系或循环依赖。</p>
<p><strong>危害：</strong><br />
*   <strong>错误的抽象：</strong> 有时看似重复的代码，在不同的上下文中可能有细微的差别或独立演化的需求。强行合并可能导致错误的抽象。<br />
*   <strong>不必要的耦合：</strong> 为了共享几行代码而引入整个包的依赖，增加了耦合度，可能比少量重复代码的维护成本更高。<br />
*   <strong>违背 Go 谚语：</strong> “A little copying is better than a little dependency.”（一点复制代码胜过一点点依赖）。Go 社区鼓励在权衡后接受适度的代码重复，以换取更低的耦合度和更高的独立性。</p>
<p><strong>解药：</strong><br />
*   <strong>批判性看待重复：</strong> 看到重复代码时，先思考它们是否真的是“同一件事”？它们的演化趋势是否一致？<br />
*   <strong>权衡成本：</strong> 引入依赖的成本（耦合、潜在冲突、维护负担）是否真的低于复制代码的成本？<br />
*   <strong>优先考虑简单：</strong> 在不确定时，保持简单，适度复制代码通常更安全。</p>
<blockquote>
<p>注：这种事儿，恐怕咱们自己或者团队里都遇到过不少：就为了用里面那一两个小函数，咔嚓一下，引入了一个庞大无比的依赖库。</p>
</blockquote>
<h2>第六宗罪：盲目崇拜与跟风——“伪标准”与“最佳实践”的陷阱</h2>
<p><img src="https://tonybai.com/wp-content/uploads/2025/go-project-design-antipatterns-7.jpg" alt="" /></p>
<p><strong>表现：</strong><br />
*   不加批判地复制某个“明星项目”或所谓的“Go 标准项目布局”（如已被社区诟病的<a href="https://github.com/golang-standards/project-layout">golang-standards/project-layout</a>）。<br />
*   将其他语言（如 Java, C#）的复杂模式生搬硬套到 Go 项目中。<br />
*   将任何 Linter 规则或所谓的“最佳实践”奉为圭臬，不考虑具体场景。</p>
<p><strong>危害：</strong><br />
*   <strong>脱离实际：</strong> 别人的“最佳实践”是基于他们的特定问题和上下文演化而来的，未必适合你的项目。<br />
*   <strong>扼杀思考：</strong> 放弃了基于自己项目需求进行独立思考和决策的机会。<br />
*   <strong>违背Go文化：</strong> Go 推崇实用主义和具体问题具体分析，而非僵化的教条。</p>
<p><strong>解药：</strong><br />
*   <strong>保持独立思考：</strong> 理解每个模式或实践要解决的<strong>原始问题</strong>是什么，它是否在你的项目中真实存在？<br />
*   <strong>以我为主，兼收并蓄：</strong> 学习和借鉴，但最终决策要基于你自己的项目需求、团队情况和对 Go 语言的理解。<br />
*   <strong>质疑“最佳”：</strong> 没有万能的“最佳实践”，只有在特定上下文中的“较好实践”。</p>
<blockquote>
<p>注：确实，很多Go初学者（甚至一些老手，包括我自己）都曾长期困惑甚至“抱怨”：官方为何不给出一个项目布局的指导呢？这个呼声持续多年后，Go官方终于在2023年发布了一份<a href="https://tonybai.com/2023/10/05/the-official-guide-of-organizing-go-project/">官方布局指南</a>。这份指南无疑是我们理解官方思路、开始设计Go项目布局的一个重要起点。</p>
</blockquote>
<h2>第七宗罪：与“引力”对抗——忽视 Go 的依赖约束</h2>
<p><img src="https://tonybai.com/wp-content/uploads/2025/go-project-design-antipatterns-8.jpg" alt="" /></p>
<p><strong>表现：</strong><br />
*   设计出隐含循环依赖的架构（例如，某些复杂的 ORM 模式，或者 Service 层与 Repository 层相互调用具体类型）。<br />
*   当遇到 import cycle not allowed 错误时，不从根本上调整结构，而是通过滥用接口、全局变量或 init() 函数等“技巧”来绕过编译错误。</p>
<p><strong>危害：</strong><br />
*   <strong>与语言对抗：</strong> Go禁止循环依赖是其核心设计之一，旨在强制形成清晰的、可管理的依赖关系图 (DAG)。试图绕过它，本质上是在与语言的设计哲学对抗。<br />
*   <strong>隐藏的复杂性：</strong> 用“技巧”解决循环依赖，只是将问题扫到地毯下，使得真实的依赖关系变得模糊不清，增加了维护难度。<br />
*   <strong>错失优化机会：</strong> 循环依赖往往是代码职责不清、耦合过度的信号。解决循环依赖的过程，本身就是一次优化架构、厘清职责的好机会。</p>
<p><strong>解药：</strong><br />
*   <strong>拥抱 DAG：</strong> 理解并尊重 Go 的依赖规则，将其视为架构设计的“向导”。<br />
*   <strong>分析依赖：</strong> 当出现循环依赖时，深入分析其根源，理解是哪个环节的职责划分或耦合出了问题。<br />
*   <strong>结构性解决：</strong> 优先使用移动代码、提取新包（向上或向下）等结构性方法来打破循环。接口解耦是可用手段，但不应是首选或唯一手段。</p>
<h2>小结：回归常识，拥抱简洁</h2>
<p>Go语言的设计哲学是务实和简洁。许多所谓的“最佳实践”和“复杂模式”，在Go的世界里可能水土不服。识别并避免上述这些“反模式”，需要我们：</p>
<ul>
<li><strong>保持批判性思维：</strong> 不盲从，不跟风，时刻追问“为什么”。</li>
<li><strong>坚持价值驱动：</strong> 让每一个设计决策都服务于解决真实问题。</li>
<li><strong>深刻理解Go：</strong> 尊重其核心约束（如无循环依赖），发挥其优势（如简洁性）。</li>
<li><strong>拥抱演化：</strong> 从简单开始，让架构随着需求的明确而有机生长。</li>
</ul>
<p>希望这篇“七宗罪”的总结能给大家带来一些警示和启发。<strong>你是否也曾在项目中遇到过这些“反模式”？你认为还有哪些Go设计中需要警惕的“坑”？欢迎在评论区分享你的看法和经验！</strong></p>
<p>也别忘了点个【赞】和【在看】，让更多Gopher看到这篇“反模式”的总结！</p>
<hr />
<p>避开这些设计“反模式”是迈向Go高手的关键一步。如果你渴望更深层次地理解Go语言精髓，与顶尖Gopher交流切磋，并紧跟Go+AI前沿动态…</p>
<p>那么，我的 <strong>「Go &amp; AI 精进营」知识星球</strong> 正是你需要的！在这里，你可以沉浸式学习【Go原理/进阶/避坑】等独家深度专栏，随时向我提问获<br />
得解析，并与高活跃社区成员碰撞思想火花。</p>
<p><strong>扫码加入，开启你的Go深度学习与精进之旅！</strong></p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-and-ai-tribe-zsxq-small-card.jpg" alt="img{512x368}" /></p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/04/21/go-project-design-antipatterns/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>gRPC客户端的那些事儿</title>
		<link>https://tonybai.com/2021/09/17/those-things-about-grpc-client/</link>
		<comments>https://tonybai.com/2021/09/17/those-things-about-grpc-client/#comments</comments>
		<pubDate>Fri, 17 Sep 2021 14:31:24 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[builder]]></category>
		<category><![CDATA[Client]]></category>
		<category><![CDATA[CNCF]]></category>
		<category><![CDATA[consul]]></category>
		<category><![CDATA[container]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[etcd]]></category>
		<category><![CDATA[ghz]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go.mod]]></category>
		<category><![CDATA[go1.16]]></category>
		<category><![CDATA[go1.17]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[goreman]]></category>
		<category><![CDATA[gRPC]]></category>
		<category><![CDATA[hey]]></category>
		<category><![CDATA[http]]></category>
		<category><![CDATA[json]]></category>
		<category><![CDATA[lb]]></category>
		<category><![CDATA[loadbalancer]]></category>
		<category><![CDATA[Mutex]]></category>
		<category><![CDATA[nacos]]></category>
		<category><![CDATA[Namespace]]></category>
		<category><![CDATA[Naming]]></category>
		<category><![CDATA[Procfile]]></category>
		<category><![CDATA[protobuf]]></category>
		<category><![CDATA[protoc]]></category>
		<category><![CDATA[replace]]></category>
		<category><![CDATA[resolver]]></category>
		<category><![CDATA[RESTAPI]]></category>
		<category><![CDATA[RPC]]></category>
		<category><![CDATA[scheme]]></category>
		<category><![CDATA[Service]]></category>
		<category><![CDATA[service-discovery]]></category>
		<category><![CDATA[service-register]]></category>
		<category><![CDATA[stream]]></category>
		<category><![CDATA[weight]]></category>
		<category><![CDATA[客户端]]></category>
		<category><![CDATA[容器]]></category>
		<category><![CDATA[服务]]></category>
		<category><![CDATA[服务发现]]></category>
		<category><![CDATA[服务注册]]></category>
		<category><![CDATA[权重]]></category>
		<category><![CDATA[流式RPC]]></category>
		<category><![CDATA[设计模式]]></category>
		<category><![CDATA[通信模式]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3293</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2021/09/17/those-things-about-grpc-client 在云原生与微服务主导架构模式的时代，内部服务间交互所采用的通信协议选型无非就是两类：HTTP API(RESTful API)和RPC。在如今的硬件配置与网络条件下，现代RPC实现的性能一般都是好于HTTP API的。我们以json over http与gRPC(insecure)作比较，分别使用ghz和hey压测gRPC和json over http的实现，gRPC的性能（Requests/sec: 59924.34）要比http api性能(Requests/sec: 49969.9234)高出20%。实测gPRC使用的protobuf的编解码性能更是最快的json编解码的2-3倍，是Go标准库json包编解码性能的10倍以上(具体数据见本文附录)。 对于性能敏感并且内部通信协议较少变动的系统来说，内部服务使用RPC可能是多数人的选择。而gRPC虽然不是性能最好的RPC实现，但作为有谷歌大厂背书且是CNCF唯一的RPC项目，gRPC自然得到了开发人员最广泛的关注与使用。 本文也来说说gRPC，不过我们更多关注一下gRPC的客户端，我们来看看使用gRPC客户端时都会考虑的那些事情（本文所有代码基于gRPC v1.40.0版本，Go 1.17版本)。 1. 默认的gRPC的客户端 gRPC支持四种通信模式，它们是（以下四张图截自《gRPC: Up and Running》一书）： 简单RPC(Simple RPC)：最简单的，也是最常用的gRPC通信模式，简单来说就是一请求一应答 服务端流RPC(Server-streaming RPC)：一请求，多应答 客户端流RPC(Client-streaming RPC)：多请求，一应答 双向流RPC(Bidirectional-Streaming RPC)：多请求，多应答 我们以最常用的Simple RPC(也称Unary RPC)为例来看一下如何实现一个gRPC版的helloworld。 我们无需自己从头来编写helloworld.proto并生成相应的gRPC代码，gRPC官方提供了一个helloworld的例子，我们仅需对其略微改造一下即可。 helloworld例子的IDL文件helloworld.proto如下： // https://github.com/grpc/grpc-go/tree/master/examples/helloworld/helloworld/helloworld.proto syntax = "proto3"; option go_package = "google.golang.org/grpc/examples/helloworld/helloworld"; option java_multiple_files = true; option java_package = "io.grpc.examples.helloworld"; option [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/those-things-about-grpc-client-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2021/09/17/those-things-about-grpc-client">本文永久链接</a> &#8211; https://tonybai.com/2021/09/17/those-things-about-grpc-client</p>
<p>在云原生与微服务主导架构模式的时代，内部服务间交互所采用的通信协议选型无非就是两类：HTTP API(RESTful API)和RPC。在如今的硬件配置与网络条件下，现代RPC实现的性能一般都是好于HTTP API的。我们以json over http与<a href="https://grpc.io">gRPC</a>(insecure)作比较，分别使用<a href="https://github.com/bojand/ghz">ghz</a>和<a href="https://github.com/rakyll/hey">hey</a>压测gRPC和json over http的实现，gRPC的性能（Requests/sec: 59924.34）要比http api性能(Requests/sec: 49969.9234)高出20%。实测gPRC使用的protobuf的编解码性能更是最快的json编解码的2-3倍，是Go标准库json包编解码性能的10倍以上(具体数据见本文附录)。</p>
<p>对于性能敏感并且内部通信协议较少变动的系统来说，内部服务使用RPC可能是多数人的选择。而gRPC虽然不是性能最好的RPC实现，但作为有谷歌大厂背书且是<a href="https://www.cncf.io/projects/">CNCF唯一的RPC项目</a>，gRPC自然得到了开发人员最广泛的关注与使用。</p>
<p>本文也来说说gRPC，不过我们更多关注一下gRPC的客户端，我们来看看使用gRPC客户端时都会考虑的那些事情（本文所有代码基于gRPC v1.40.0版本，<a href="https://mp.weixin.qq.com/s/y_pC6GYeZnKuHG8ycNy6rg">Go 1.17版本</a>)。</p>
<h3>1. 默认的gRPC的客户端</h3>
<p>gRPC支持四种通信模式，它们是（以下四张图截自<a href="https://book.douban.com/subject/34796013/">《gRPC: Up and Running》</a>一书）：</p>
<ul>
<li>简单RPC(Simple RPC)：最简单的，也是最常用的gRPC通信模式，简单来说就是<strong>一请求一应答</strong></li>
</ul>
<p><img src="https://tonybai.com/wp-content/uploads/those-things-about-grpc-client-2.png" alt="" /></p>
<ul>
<li>服务端流RPC(Server-streaming RPC)：<strong>一请求，多应答</strong></li>
</ul>
<p><img src="https://tonybai.com/wp-content/uploads/those-things-about-grpc-client-3.png" alt="" /></p>
<ul>
<li>客户端流RPC(Client-streaming RPC)：<strong>多请求，一应答</strong></li>
</ul>
<p><img src="https://tonybai.com/wp-content/uploads/those-things-about-grpc-client-4.png" alt="" /></p>
<ul>
<li>双向流RPC(Bidirectional-Streaming RPC)：<strong>多请求，多应答</strong></li>
</ul>
<p><img src="https://tonybai.com/wp-content/uploads/those-things-about-grpc-client-5.png" alt="" /></p>
<p>我们以最常用的Simple RPC(也称Unary RPC)为例来看一下如何实现一个gRPC版的helloworld。</p>
<p>我们无需自己从头来编写helloworld.proto并生成相应的gRPC代码，<a href="https://github.com/grpc/grpc-go/tree/master/examples/helloworld">gRPC官方提供了一个helloworld的例子</a>，我们仅需对其略微改造一下即可。</p>
<p>helloworld例子的IDL文件helloworld.proto如下：</p>
<pre><code>// https://github.com/grpc/grpc-go/tree/master/examples/helloworld/helloworld/helloworld.proto

syntax = "proto3";

option go_package = "google.golang.org/grpc/examples/helloworld/helloworld";
option java_multiple_files = true;
option java_package = "io.grpc.examples.helloworld";
option java_outer_classname = "HelloWorldProto";

package helloworld;

// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}
</code></pre>
<p>对.proto文件的规范讲解大家可以参考<a href="https://grpc.io/docs/what-is-grpc/core-concepts/">grpc官方文档</a>，这里不赘述。显然上面这个IDL是极致简单的。这里定义了一个service：Greeter，它仅包含一个方法SayHello，并且这个方法的参数与返回值都是一个仅包含一个string字段的结构体。</p>
<p>我们无需手工执行protoc命令来基于该.proto文件生成对应的Greeter service的实现以及HelloRequest、HelloReply的protobuf编解码实现，因为gRPC在example下已经放置了生成后的Go源文件，我们直接引用即可。这里要注意，最新的<a href="https://github.com/grpc/grpc-go">grpc-go项目仓库</a>采用了多module的管理模式，examples作为一个独立的go module而存在，因此我们需要将其单独作为一个module导入到其使用者的项目中。以gRPC客户端greeter_client为例，它的go.mod要这样来写：</p>
<pre><code>// https://github.com/bigwhite/experiments/tree/master/grpc-client/demo1/greeter_client/go.mod
module github.com/bigwhite/grpc-client/demo1

go 1.17

require (
    google.golang.org/grpc v1.40.0
    google.golang.org/grpc/examples v1.40.0
)

require (
    github.com/golang/protobuf v1.4.3 // indirect
    golang.org/x/net v0.0.0-20201021035429-f5854403a974 // indirect
    golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f // indirect
    golang.org/x/text v0.3.3 // indirect
    google.golang.org/genproto v0.0.0-20200806141610-86f49bd18e98 // indirect
    google.golang.org/protobuf v1.25.0 // indirect
)

replace google.golang.org/grpc v1.40.0 =&gt; /Users/tonybai/Go/src/github.com/grpc/grpc-go

replace google.golang.org/grpc/examples v1.40.0 =&gt; /Users/tonybai/Go/src/github.com/grpc/grpc-go/examples
</code></pre>
<blockquote>
<p>注：grpc-go项目的标签(tag)似乎打的有问题，由于没有打grpc/examples/v1.40.0标签，go命令在grpc-go的v1.40.0标签中找不到examples，因此上面的go.mod中使用了一个replace trick(example module的v1.40.0版本是假的哦)，将examples module指向本地的代码。</p>
</blockquote>
<p>gRPC通信的两端我们也稍作改造。原greeter_client仅发送一个请求便退出，这里我们将其改为每隔2s发送请求（便于后续观察），如下面代码所示：</p>
<pre><code>// https://github.com/bigwhite/experiments/tree/master/grpc-client/demo1/greeter_client/main.go
... ...
func main() {
    // Set up a connection to the server.
    ctx, cf1 := context.WithTimeout(context.Background(), time.Second*3)
    defer cf1()
    conn, err := grpc.DialContext(ctx, address, grpc.WithInsecure(), grpc.WithBlock())
    if err != nil {
        log.Fatalf("did not connect: %v", err)
    }
    defer conn.Close()
    c := pb.NewGreeterClient(conn)

    // Contact the server and print out its response.
    name := defaultName
    if len(os.Args) &gt; 1 {
        name = os.Args[1]
    }

    for i := 0; ; i++ {
        ctx, _ := context.WithTimeout(context.Background(), time.Second)
        r, err := c.SayHello(ctx, &amp;pb.HelloRequest{Name: fmt.Sprintf("%s-%d", name, i+1)})
        if err != nil {
            log.Fatalf("could not greet: %v", err)
        }
        log.Printf("Greeting: %s", r.GetMessage())
        time.Sleep(2 * time.Second)
    }
}
</code></pre>
<p>greeter_server加了一个命令行选项-port并支持<a href="https://www.imooc.com/read/87/article/2473">gRPC server的优雅退出</a>：</p>
<pre><code>// https://github.com/bigwhite/experiments/tree/master/grpc-client/demo1/greeter_server/main.go
... ...

var port int

func init() {
    flag.IntVar(&amp;port, "port", 50051, "listen port")
}

func main() {
    flag.Parse()
    lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", port))
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    s := grpc.NewServer()
    pb.RegisterGreeterServer(s, &amp;server{})

    go func() {
        if err := s.Serve(lis); err != nil {
            log.Fatalf("failed to serve: %v", err)
        }
    }()

    var c = make(chan os.Signal)
    signal.Notify(c, os.Interrupt, os.Kill)
    &lt;-c
    s.Stop()
    fmt.Println("exit")
}
</code></pre>
<p>搞定go.mod以及对client和server进行改造ok后，我们就可以来构建和运行greeter_client和greeter_server了：</p>
<pre><code>编译和启动server：

$cd grpc-client/demo1/greeter_server
$make
$./demo1-server -port 50051
2021/09/11 12:10:33 Received: world-1
2021/09/11 12:10:35 Received: world-2
2021/09/11 12:10:37 Received: world-3
... ...

编译和启动client：
$cd grpc-client/demo1/greeter_client
$make
$./demo1-client
2021/09/11 12:10:33 Greeting: Hello world-1
2021/09/11 12:10:35 Greeting: Hello world-2
2021/09/11 12:10:37 Greeting: Hello world-3
... ...
</code></pre>
<p>我们看到：greeter_client和greeter_server启动后可以正常的通信！我们重点看一下greeter_client。</p>
<p>greeter_client在Dial服务端时传给DialContext的target参数是一个静态的服务地址：</p>
<pre><code>const (
      address     = "localhost:50051"
)
</code></pre>
<p>这个形式的target经过google.golang.org/grpc/internal/grpcutil.ParseTarget的解析后返回一个值为nil的resolver.Target。于是gRPC采用默认的scheme：”passthrough”(github.com/grpc/grpc-go/resolver/resolver.go)，默认的”passthrough” scheme下，gRPC将使用内置的passthrough resolver(google.golang.org/grpc/internal/resolver/passthrough)。默认的这个passthrough resolver是如何设置要连接的service地址的呢？下面是passthrough resolver的代码摘录：</p>
<pre><code>// github.com/grpc/grpc-go/internal/resolver/passthrough/passthrough.go

func (r *passthroughResolver) start() {
    r.cc.UpdateState(resolver.State{Addresses: []resolver.Address{{Addr: r.target.Endpoint}}})
}
</code></pre>
<p>我们看到它将target.Endpoint，即localhost:50051直接传给了ClientConnection(上面代码的r.cc)，后者将向这个地址建立tcp连接。这正应了该resolver的名字：<strong>passthrough</strong>。</p>
<p>上面greeter_client连接的仅仅是service的一个实例(instance)，如果我们同时启动了该service的三个实例，比如使用<a href="https://github.com/mattn/goreman">goreman</a>通过加载脚本文件来启动多个service实例：</p>
<pre><code>// https://github.com/bigwhite/experiments/tree/master/grpc-client/demo1/greeter_server/Procfile

# Use goreman to run `go get github.com/mattn/goreman`
demo1-server1: ./demo1-server -port 50051
demo1-server2: ./demo1-server -port 50052
demo1-server3: ./demo1-server -port 50053

同时启动多实例：

$goreman start
15:22:12 demo1-server3 | Starting demo1-server3 on port 5200
15:22:12 demo1-server2 | Starting demo1-server2 on port 5100
15:22:12 demo1-server1 | Starting demo1-server1 on port 5000
</code></pre>
<p>那么我们应该如何告诉greeter_client去连接这三个实例呢？是否可以将address改为下面这样就可以了呢：</p>
<pre><code>const (
    address     = "localhost:50051,localhost:50052,localhost:50053"
    defaultName = "world"
)
</code></pre>
<p>我们来改改试试，修改后重新编译greeter_client，启动greeter_client，我们看到下面结果：</p>
<pre><code>$./demo1-client
2021/09/11 15:26:32 did not connect: context deadline exceeded
</code></pre>
<p>greeter_client连接server超时！也就是说像上面这样简单的传入多个实例的地址是不行的！那问题来了！我们该怎么让greeter_client去连接一个service的多个实例呢？我们继续向下看。</p>
<h3>2. 连接一个Service的多个实例(instance)</h3>
<p>grpc.Dial/grpc.DialContext的参数target可不仅仅是service实例的服务地址这么简单，<strong>它的实参(argument)形式决定了gRPC client将采用哪一个resolver来确定service实例的地址集合</strong>。</p>
<p>下面我们以一个返回service实例地址静态集合(即service的实例数量固定且服务地址固定)的StaticResolver为例，来看如何让gRPC client连接一个Service的多个实例。</p>
<h4>1) StaticResolver</h4>
<p>我们首先来设计一下传给grpc.DialContext的target形式。<a href="https://github.com/grpc/grpc/blob/master/doc/naming.md">关于gRPC naming resolution，gRPC有专门文档说明</a>。在这里，我们也创建一个新的scheme：static，多个service instance的服务地址通过逗号分隔的字符串传入，如下面代码：</p>
<pre><code>// https://github.com/bigwhite/experiments/tree/master/grpc-client/demo2/greeter_client/main.go

const (
      address = "static:///localhost:50051,localhost:50052,localhost:50053"
)
</code></pre>
<p>当address被作为target的实参传入grpc.DialContext后，它会被grpcutil.ParseTarget解析为一个resolver.Target结构体，该结构体包含三个字段：</p>
<pre><code>// github.com/grpc/grpc-go/resolver/resolver.go
type Target struct {
    Scheme    string
    Authority string
    Endpoint  string
}
</code></pre>
<p>其中Scheme为”static”，Authority为空，Endpoint为”localhost:50051,localhost:50052,localhost:50053&#8243;。</p>
<p>接下来，gRPC会根据Target.Scheme的值到resolver包中的builder map中查找是否有对应的Resolver Builder实例。到目前为止gRPC内置的的resolver Builder都无法匹配该Scheme值。是时候自定义一个StaticResolver的Builder了！</p>
<p>grpc的resolve包定义了一个Builder实例需要实现的接口：</p>
<pre><code>// github.com/grpc/grpc-go/resolver/resolver.go 

// Builder creates a resolver that will be used to watch name resolution updates.
type Builder interface {
    // Build creates a new resolver for the given target.
    //
    // gRPC dial calls Build synchronously, and fails if the returned error is
    // not nil.
    Build(target Target, cc ClientConn, opts BuildOptions) (Resolver, error)
    // Scheme returns the scheme supported by this resolver.
    // Scheme is defined at https://github.com/grpc/grpc/blob/master/doc/naming.md.
    Scheme() string
}
</code></pre>
<p>Scheme方法返回这个Builder对应的scheme，而Build方法则是真正用于构建Resolver实例的方法，我们来看一下StaticBuilder的实现：</p>
<pre><code>// https://github.com/bigwhite/experiments/tree/master/grpc-client/demo2/greeter_client/builder.go

func init() {
    resolver.Register(&amp;StaticBuilder{}) //在init函数中将StaticBuilder实例注册到resolver包的Resolver map中
}

type StaticBuilder struct{}

func (sb *StaticBuilder) Build(target resolver.Target, cc resolver.ClientConn,
    opts resolver.BuildOptions) (resolver.Resolver, error) {

    // 解析target.Endpoint (例如：localhost:50051,localhost:50052,localhost:50053)
    endpoints := strings.Split(target.Endpoint, ",")

    r := &amp;StaticResolver{
        endpoints: endpoints,
        cc:        cc,
    }
    r.ResolveNow(resolver.ResolveNowOptions{})
    return r, nil
}

func (sb *StaticBuilder) Scheme() string {
    return "static" // 返回StaticBuilder对应的scheme字符串
}
</code></pre>
<p>在这个StaticBuilder实现中，init函数在包初始化是就将一个StaticBuilder实例注册到resolver包的Resolver map中。这样gRPC在Dial时就能通过target中的scheme找到该builder。Build方法是StaticBuilder的关键，在这个方法中，它首先解析传入的target.Endpoint，得到三个service instance的服务地址并存到新创建的StaticResolver实例中，并调用StaticResolver实例的ResolveNow方法确定即将连接的service instance集合。</p>
<p>和Builder一样，grpc的resolver包也定义了每个resolver需要实现的Resolver接口：</p>
<pre><code>// github.com/grpc/grpc-go/resolver/resolver.go 

// Resolver watches for the updates on the specified target.
// Updates include address updates and service config updates.
type Resolver interface {
    // ResolveNow will be called by gRPC to try to resolve the target name
    // again. It's just a hint, resolver can ignore this if it's not necessary.
    //
    // It could be called multiple times concurrently.
    ResolveNow(ResolveNowOptions)
    // Close closes the resolver.
    Close()
}
</code></pre>
<p>从这个接口注释我们也能看出，Resolver的实现负责监视(watch)服务测的地址与配置变化，并将变化更新给grpc的ClientConn。我们来看看我们的StaticResolver的实现：</p>
<pre><code>// https://github.com/bigwhite/experiments/tree/master/grpc-client/demo2/greeter_client/resolver.go

type StaticResolver struct {
    endpoints []string
    cc        resolver.ClientConn
    sync.Mutex
}

func (r *StaticResolver) ResolveNow(opts resolver.ResolveNowOptions) {
    r.Lock()
    r.doResolve()
    r.Unlock()
}

func (r *StaticResolver) Close() {
}

func (r *StaticResolver) doResolve() {
    var addrs []resolver.Address
    for i, addr := range r.endpoints {
        addrs = append(addrs, resolver.Address{
            Addr:       addr,
            ServerName: fmt.Sprintf("instance-%d", i+1),
        })
    }

    newState := resolver.State{
        Addresses: addrs,
    }

    r.cc.UpdateState(newState)
}
</code></pre>
<blockquote>
<p>注：resolver.Resolver接口的注释要求ResolveNow方法是要支持并发安全的，所以这里我们通过sync.Mutex来实现同步。</p>
</blockquote>
<p>由于服务侧的服务地址数量与信息都是不变的，因此这里并没有watch和update的过程，而只是在实现了ResolveNow(并在Builder中的Build方法中调用），在ResolveNow中将service instance的地址集合更新给ClientConnection(r.cc)。</p>
<p>接下来我们来编译与运行一下demo2的client与server：</p>
<pre><code>$cd grpc-client/demo2/greeter_server
$make
$goreman start
22:58:21 demo2-server1 | Starting demo2-server1 on port 5000
22:58:21 demo2-server2 | Starting demo2-server2 on port 5100
22:58:21 demo2-server3 | Starting demo2-server3 on port 5200

$cd grpc-client/demo2/greeter_client
$make
$./demo2-client
</code></pre>
<p>执行一段时间后，你会在server端的日志中发现一个问题，如下日志所示：</p>
<pre><code>22:57:16 demo2-server1 | 2021/09/11 22:57:16 Received: world-1
22:57:18 demo2-server1 | 2021/09/11 22:57:18 Received: world-2
22:57:20 demo2-server1 | 2021/09/11 22:57:20 Received: world-3
22:57:22 demo2-server1 | 2021/09/11 22:57:22 Received: world-4
22:57:24 demo2-server1 | 2021/09/11 22:57:24 Received: world-5
22:57:26 demo2-server1 | 2021/09/11 22:57:26 Received: world-6
22:57:28 demo2-server1 | 2021/09/11 22:57:28 Received: world-7
22:57:30 demo2-server1 | 2021/09/11 22:57:30 Received: world-8
22:57:32 demo2-server1 | 2021/09/11 22:57:32 Received: world-9
</code></pre>
<p>我们的Service instance集合中明明有三个地址，为何只有server1收到了rpc请求，其他两个server都处于空闲状态呢？这是客户端的负载均衡策略在作祟！默认情况下，grpc会为客户端选择内置的“pick_first”负载均衡策略，即在service instance集合中选择第一个intance进行请求。在这个例子中，在pick_first策略的作用下，grpc总是会选择demo2-server1发起rpc请求。</p>
<p>如果要将请求发到各个server上，我们可以将负载均衡策略改为另外一个内置的策略：round_robin，就像下面代码这样：</p>
<pre><code>// https://github.com/bigwhite/experiments/tree/master/grpc-client/demo2/greeter_client/main.go

conn, err := grpc.DialContext(ctx, address, grpc.WithInsecure(), grpc.WithBlock(), grpc.WithBalancerName("round_robin"))
</code></pre>
<p>重新编译运行greeter_client后，在server测我们就可以看到rpc请求被轮询地发到了每个server instance上了。</p>
<h4>2) Resolver原理</h4>
<p>我们再来用一幅图来梳理一下Builder以及Resolver的工作原理：</p>
<p><img src="https://tonybai.com/wp-content/uploads/those-things-about-grpc-client-6.png" alt="" /></p>
<p>图中的SchemeResolver泛指实现了某一特定scheme的resolver。如图所示，service instance集合resolve过程的步骤大致如下：</p>
<ul>
<li>
<ol>
<li>SchemeBuilder将自身实例注册到resolver包的map中；</li>
</ol>
</li>
<li>
<ol>
<li>grpc.Dial/DialContext时使用特定形式的target参数</li>
</ol>
</li>
<li>
<ol>
<li>对target解析后，根据target.Scheme到resolver包的map中查找Scheme对应的Buider；</li>
</ol>
</li>
<li>
<ol>
<li>调用Buider的Build方法</li>
</ol>
</li>
<li>
<ol>
<li>Build方法构建出SchemeResolver实例；</li>
</ol>
</li>
<li>
<ol>
<li>后续由SchemeResolver实例监视service instance变更状态并在有变更的时候更新ClientConnection。</li>
</ol>
</li>
</ul>
<h4>3) NacosResolver</h4>
<p>在生产环境中，考虑到服务的高可用、可伸缩等，我们很少使用固定地址、固定数量的服务实例集合，更多是通过服务注册和发现机制自动实现服务实例集合的更新。这里我们再来实现一个基于<a href="https://nacos.io/zh-cn/">nacos</a>的NacosResolver，实现服务实例变更时grpc Client的自动调整(注：nacos的本地单节点安装方案见文本附录)，让示例具实战意义^_^。</p>
<p>由于有了上面关于Resolver原理的描述，这里简化了一些描述。</p>
<p>首先和StaticResolver一样，我们也来设计一下target的形式。nacos有namespace, group的概念，因此我们将target设计为如下形式：</p>
<pre><code>nacos://[authority]/host:port/namespace/group/serviceName
</code></pre>
<p>具体到我们的greeter_client中，其address为：</p>
<pre><code>// https://github.com/bigwhite/experiments/tree/master/grpc-client/demo3/greeter_client/main.go

const (
      address = "nacos:///localhost:8848/public/group-a/demo3-service" //no authority
)
</code></pre>
<p>接下来我们来看NacosBuilder：</p>
<pre><code>// https://github.com/bigwhite/experiments/tree/master/grpc-client/demo3/greeter_client/builder.go

func (nb *NacosBuilder) Build(target resolver.Target,
    cc resolver.ClientConn,
    opts resolver.BuildOptions) (resolver.Resolver, error) {

    // use info in target to access naming service
    // parse the target.endpoint
    // target.Endpoint - localhost:8848/public/DEFAULT_GROUP/serviceName, the addr of naming service :nacos endpoint
    sl := strings.Split(target.Endpoint, "/")
    nacosAddr := sl[0]
    namespace := sl[1]
    group := sl[2]
    serviceName := sl[3]
    sl1 := strings.Split(nacosAddr, ":")
    host := sl1[0]
    port := sl1[1]
    namingClient, err := initNamingClient(host, port, namespace, group)
    if err != nil {
        return nil, err
    }

    r := &amp;NacosResolver{
        namingClient: namingClient,
        cc:           cc,
        namespace:    namespace,
        group:        group,
        serviceName:  serviceName,
    }

    // initialize the cc's states
    r.ResolveNow(resolver.ResolveNowOptions{})

    // subscribe and watch
    r.watch()
    return r, nil
}

func (nb *NacosBuilder) Scheme() string {
    return "nacos"
}
</code></pre>
<p>NacosBuilder的Build方法流程也StaticBuilder并无二致，首先我们也是解析传入的target的Endpoint，即”localhost:8848/public/group-a/demo3-service”，并将解析后的各段信息存入新创建的NacosResolver实例中备用。NacosResolver还需要一个信息，那就是与nacos的连接，这里用initNamingClient创建一个nacos client端实例(调用<a href="https://github.com/nacos-group/nacos-sdk-go">nacos提供的go sdk</a>)。</p>
<p>接下来我们调用NacosResolver的ResolveNow获取一次nacos上demo3-service的服务实例列表并初始化ClientConn，最后我们调用NacosResolver的watch方法来订阅并监视demo3-service的实例变化。下面是NacosResolver的部分实现：</p>
<pre><code>// https://github.com/bigwhite/experiments/tree/master/grpc-client/demo3/greeter_client/resolver.go

func (r *NacosResolver) doResolve(opts resolver.ResolveNowOptions) {
    instances, err := r.namingClient.SelectAllInstances(vo.SelectAllInstancesParam{
        ServiceName: r.serviceName,
        GroupName:   r.group,
    })
    if err != nil {
        fmt.Println(err)
        return
    }

    if len(instances) == 0 {
        fmt.Printf("service %s has zero instance\n", r.serviceName)
        return
    }

    // update cc.States
    var addrs []resolver.Address
    for i, inst := range instances {
        if (!inst.Enable) || (inst.Weight == 0) {
            continue
        }

        addrs = append(addrs, resolver.Address{
            Addr:       fmt.Sprintf("%s:%d", inst.Ip, inst.Port),
            ServerName: fmt.Sprintf("instance-%d", i+1),
        })
    }

    if len(addrs) == 0 {
        fmt.Printf("service %s has zero valid instance\n", r.serviceName)
    }

    newState := resolver.State{
        Addresses: addrs,
    }

    r.Lock()
    r.cc.UpdateState(newState)
    r.Unlock()
}

func (r *NacosResolver) ResolveNow(opts resolver.ResolveNowOptions) {
    r.doResolve(opts)
}

func (r *NacosResolver) Close() {
    r.namingClient.Unsubscribe(&amp;vo.SubscribeParam{
        ServiceName: r.serviceName,
        GroupName:   r.group,
    })
}

func (r *NacosResolver) watch() {
    r.namingClient.Subscribe(&amp;vo.SubscribeParam{
        ServiceName: r.serviceName,
        GroupName:   r.group,
        SubscribeCallback: func(services []model.SubscribeService, err error) {
            fmt.Printf("subcallback: %#v\n", services)
            r.doResolve(resolver.ResolveNowOptions{})
        },
    })
}
</code></pre>
<p>这里的一个重要实现是ResolveNow和watch都调用的doResolve方法，该方法通过nacos-go sdk中的SelectAllInstances获取demo-service3的所有实例，并将得到的enabled(=true)和权重(weight)不为0的合法实例集合更新给ClientConn(r.cc.UpdateState)。</p>
<p>在NacosResolver的watch方法中，我们通过nacos-go sdk中的Subscribe方法订阅demo3-service并提供了一个回调函数。这样每当demo3-service的实例发生变化时，该回调会被调用。在该回调中我们可以基于传回的最新的service实例集合（services []model.SubscribeService）来更新ClientConn，但在这里我们复用了doResolve方法，即又去nacos获取一次demo-service3的实例。</p>
<p>编译运行demo3下greeter_server：</p>
<pre><code>$cd grpc-client/demo3/greeter_server
$make
$goreman start
06:06:02 demo3-server3 | Starting demo3-server3 on port 5200
06:06:02 demo3-server1 | Starting demo3-server1 on port 5000
06:06:02 demo3-server2 | Starting demo3-server2 on port 5100
06:06:02 demo3-server3 | 2021-09-12T06:06:02.913+0800   INFO    nacos_client/nacos_client.go:87 logDir:&lt;/tmp/nacos/log/50053&gt;   cacheDir:&lt;/tmp/nacos/cache/50053&gt;
06:06:02 demo3-server2 | 2021-09-12T06:06:02.913+0800   INFO    nacos_client/nacos_client.go:87 logDir:&lt;/tmp/nacos/log/50052&gt;   cacheDir:&lt;/tmp/nacos/cache/50052&gt;
06:06:02 demo3-server1 | 2021-09-12T06:06:02.913+0800   INFO    nacos_client/nacos_client.go:87 logDir:&lt;/tmp/nacos/log/50051&gt;   cacheDir:&lt;/tmp/nacos/cache/50051&gt;
</code></pre>
<p>运行greeter_server后，我们在nacos dashboard上会看到demo-service3的所有实例信息：</p>
<p><img src="https://tonybai.com/wp-content/uploads/those-things-about-grpc-client-7.png" alt="" /><br />
<img src="https://tonybai.com/wp-content/uploads/those-things-about-grpc-client-8.png" alt="" /></p>
<p>编译运行demo3下greeter_client：</p>
<pre><code>$cd grpc-client/demo3/greeter_client
$make
$./demo3-client
2021-09-12T06:08:25.551+0800    INFO    nacos_client/nacos_client.go:87 logDir:&lt;/Users/tonybai/go/src/github.com/bigwhite/experiments/grpc-client/demo3/greeter_client/log&gt;   cacheDir:&lt;/Users/tonybai/go/src/github.com/bigwhite/experiments/grpc-client/demo3/greeter_client/cache&gt;
2021/09/12 06:08:25 Greeting: Hello world-1
2021/09/12 06:08:27 Greeting: Hello world-2
2021/09/12 06:08:29 Greeting: Hello world-3
2021/09/12 06:08:31 Greeting: Hello world-4
2021/09/12 06:08:33 Greeting: Hello world-5
2021/09/12 06:08:35 Greeting: Hello world-6
... ...
</code></pre>
<p>由于采用了round robin负载策略，greeter_server侧每个server(权重都为1)都会平等的收到rpc请求：</p>
<pre><code>06:06:36 demo3-server1 | 2021/09/12 06:06:36 Received: world-1
06:06:38 demo3-server3 | 2021/09/12 06:06:38 Received: world-2
06:06:40 demo3-server2 | 2021/09/12 06:06:40 Received: world-3
06:06:42 demo3-server1 | 2021/09/12 06:06:42 Received: world-4
06:06:44 demo3-server3 | 2021/09/12 06:06:44 Received: world-5
06:06:46 demo3-server2 | 2021/09/12 06:06:46 Received: world-6
... ...
</code></pre>
<p>这时我们可以通过nacos dashboard调整demo3-service的实例权重或下线某个实例，比如下线service instance-2(端口50052)，之后我们会看到greeter_client回调函数执行，之后greeter_server侧将只有实例1和实例3收到rpc请求。重新上线service instance-2后，一切会恢复正常。</p>
<h3>3. 自定义客户端balancer</h3>
<p>现实中服务端的实例所部署的主机(虚拟机/容器)算力可能不同，如果所有实例都使用相同权重1，那么肯定是不科学且存在算力浪费。但grpc-go内置的balancer实现有限，不能满足我们需求，我们就需要自定义一个可以满足我们需求的balancer了。</p>
<p>这里我们以自定义一个Weighted Round Robin(wrr) Balancer为例，看看自定义balancer的步骤（我们参考grpc-go中内置<a href="https://github.com/grpc/grpc-go/tree/master/balancer/roundrobin">round_robin的实现</a>）。</p>
<p>和resolver包相似，balancer也是通过一个Builder(创建模式)来实例化的，并且balancer的Balancer接口与resolver.Balancer差不多：</p>
<pre><code>// github.com/grpc/grpc-go/balancer/balancer.go 

// Builder creates a balancer.
type Builder interface {
    // Build creates a new balancer with the ClientConn.
    Build(cc ClientConn, opts BuildOptions) Balancer
    // Name returns the name of balancers built by this builder.
    // It will be used to pick balancers (for example in service config).
    Name() string
}
</code></pre>
<p>通过Builder.Build方法我们构建一个Balancer接口的实现，Balancer接口定义如下：</p>
<pre><code>// github.com/grpc/grpc-go/balancer/balancer.go 

type Balancer interface {
    // UpdateClientConnState is called by gRPC when the state of the ClientConn
    // changes.  If the error returned is ErrBadResolverState, the ClientConn
    // will begin calling ResolveNow on the active name resolver with
    // exponential backoff until a subsequent call to UpdateClientConnState
    // returns a nil error.  Any other errors are currently ignored.
    UpdateClientConnState(ClientConnState) error
    // ResolverError is called by gRPC when the name resolver reports an error.
    ResolverError(error)
    // UpdateSubConnState is called by gRPC when the state of a SubConn
    // changes.
    UpdateSubConnState(SubConn, SubConnState)
    // Close closes the balancer. The balancer is not required to call
    // ClientConn.RemoveSubConn for its existing SubConns.
    Close()
}
</code></pre>
<p>可以看到，Balancer要比Resolver要复杂很多。gRPC的核心开发者们也看到了这一点，于是他们提供了一个可简化自定义Balancer创建的包：google.golang.org/grpc/balancer/base。gRPC内置的round_robin Balancer也是基于base包实现的。</p>
<p>base包提供了NewBalancerBuilder可以快速返回一个balancer.Builder的实现：</p>
<pre><code>// github.com/grpc/grpc-go/balancer/base/base.go 

// NewBalancerBuilder returns a base balancer builder configured by the provided config.
func NewBalancerBuilder(name string, pb PickerBuilder, config Config) balancer.Builder {
    return &amp;baseBuilder{
        name:          name,
        pickerBuilder: pb,
        config:        config,
    }
}
</code></pre>
<p>我们看到，这个函数接收一个参数：pb，它的类型是PikcerBuilder，这个接口类型则比较简单：</p>
<pre><code>// github.com/grpc/grpc-go/balancer/base/base.go 

// PickerBuilder creates balancer.Picker.
type PickerBuilder interface {
    // Build returns a picker that will be used by gRPC to pick a SubConn.
    Build(info PickerBuildInfo) balancer.Picker
}
</code></pre>
<p>我们仅需要提供一个PickerBuilder的实现以及一个balancer.Picker的实现即可，而Picker则是仅有一个方法的接口类型：</p>
<pre><code>// github.com/grpc/grpc-go/balancer/balancer.go 

type Picker interface {
    Pick(info PickInfo) (PickResult, error)
}
</code></pre>
<p>嵌套的有些多，我们用下面这幅图来直观看一下balancer的创建和使用流程：</p>
<p><img src="https://tonybai.com/wp-content/uploads/those-things-about-grpc-client-9.png" alt="" /></p>
<p>再简述一下大致流程：</p>
<ul>
<li>首先要注册一个名为”my_weighted_round_robin”的balancer Builder:wrrBuilder，该Builder由base包的NewBalancerBuilder构建；</li>
<li>base包的NewBalancerBuilder函数需要传入一个PickerBuilder实现，于是我们需要自定义一个返回Picker接口实现的PickerBuilder。</li>
<li>grpc.Dial调用时传入一个WithBalancerName(“my_weighted_round_robin”)，grpc通过balancer Name从已注册的balancer builder中选出我们实现的wrrBuilder，并调用wrrBuilder创建Picker：wrrPicker。</li>
<li>在grpc实施rpc调用SayHello时，wrrPicker的Pick方法会被调用，选出一个Connection，并在该connection上发送rpc请求。</li>
</ul>
<p>由于用到的权重值，我们的resolver实现需要做一些变动，主要是在doResolve方法时将service instance的权重(weight)通过Attribute设置到ClientConnection中：</p>
<pre><code>// https://github.com/bigwhite/experiments/tree/master/grpc-client/demo4/greeter_client/resolver.go

func (r *NacosResolver) doResolve(opts resolver.ResolveNowOptions) {
    instances, err := r.namingClient.SelectAllInstances(vo.SelectAllInstancesParam{
        ServiceName: r.serviceName,
        GroupName:   r.group,
    })
    if err != nil {
        fmt.Println(err)
        return
    }

    if len(instances) == 0 {
        fmt.Printf("service %s has zero instance\n", r.serviceName)
        return
    }

    // update cc.States
    var addrs []resolver.Address
    for i, inst := range instances {
        if (!inst.Enable) || (inst.Weight == 0) {
            continue
        }

        addr := resolver.Address{
            Addr:       fmt.Sprintf("%s:%d", inst.Ip, inst.Port),
            ServerName: fmt.Sprintf("instance-%d", i+1),
        }
        addr.Attributes = addr.Attributes.WithValues("weight", int(inst.Weight)) //考虑权重并纳入cc的状态中
        addrs = append(addrs, addr)
    }

    if len(addrs) == 0 {
        fmt.Printf("service %s has zero valid instance\n", r.serviceName)
    }

    newState := resolver.State{
        Addresses: addrs,
    }

    r.Lock()
    r.cc.UpdateState(newState)
    r.Unlock()
}
</code></pre>
<p>接下来我们重点看看greeter_client中wrrPickerBuilder与wrrPicker的实现：</p>
<pre><code>// https://github.com/bigwhite/experiments/tree/master/grpc-client/demo4/greeter_client/balancer.go

type wrrPickerBuilder struct{}

func (*wrrPickerBuilder) Build(info base.PickerBuildInfo) balancer.Picker {
    if len(info.ReadySCs) == 0 {
        return base.NewErrPicker(balancer.ErrNoSubConnAvailable)
    }

    var scs []balancer.SubConn
    // 提取已经就绪的connection的权重信息，作为Picker实例的输入
    for subConn, addr := range info.ReadySCs {
        weight := addr.Address.Attributes.Value("weight").(int)
        if weight &lt;= 0 {
            weight = 1
        }
        for i := 0; i &lt; weight; i++ {
            scs = append(scs, subConn)
        }
    }

    return &amp;wrrPicker{
        subConns: scs,
        // Start at a random index, as the same RR balancer rebuilds a new
        // picker when SubConn states change, and we don't want to apply excess
        // load to the first server in the list.
        next: rand.Intn(len(scs)),
    }
}

type wrrPicker struct {
    // subConns is the snapshot of the roundrobin balancer when this picker was
    // created. The slice is immutable. Each Get() will do a round robin
    // selection from it and return the selected SubConn.
    subConns []balancer.SubConn

    mu   sync.Mutex
    next int
}

// 选出一个Connection
func (p *wrrPicker) Pick(info balancer.PickInfo) (balancer.PickResult, error) {
    p.mu.Lock()
    sc := p.subConns[p.next]
    p.next = (p.next + 1) % len(p.subConns)
    p.mu.Unlock()
    return balancer.PickResult{SubConn: sc}, nil
}
</code></pre>
<p>这是一个简单的Weighted Round Robin实现，加权算法十分简单，如果一个conn的权重为n，那么就在加权结果集中加入n个conn，这样在后续Pick时不需要考虑加权的问题，只需向普通Round Robin那样逐个Pick出来即可。</p>
<p>运行demo4 greeter_server后，我们在nacos将instance-1的权重改为5，我们后续就会看到如下输出：</p>
<pre><code>$goreman start
09:20:18 demo4-server3 | Starting demo4-server3 on port 5200
09:20:18 demo4-server2 | Starting demo4-server2 on port 5100
09:20:18 demo4-server1 | Starting demo4-server1 on port 5000
09:20:18 demo4-server2 | 2021-09-12T09:20:18.633+0800   INFO    nacos_client/nacos_client.go:87 logDir:&lt;/tmp/nacos/log/50052&gt;   cacheDir:&lt;/tmp/nacos/cache/50052&gt;
09:20:18 demo4-server1 | 2021-09-12T09:20:18.633+0800   INFO    nacos_client/nacos_client.go:87 logDir:&lt;/tmp/nacos/log/50051&gt;   cacheDir:&lt;/tmp/nacos/cache/50051&gt;
09:20:18 demo4-server3 | 2021-09-12T09:20:18.633+0800   INFO    nacos_client/nacos_client.go:87 logDir:&lt;/tmp/nacos/log/50053&gt;   cacheDir:&lt;/tmp/nacos/cache/50053&gt;
09:20:23 demo4-server2 | 2021/09/12 09:20:23 Received: world-1
09:20:25 demo4-server3 | 2021/09/12 09:20:25 Received: world-2
09:20:27 demo4-server1 | 2021/09/12 09:20:27 Received: world-3
09:20:29 demo4-server2 | 2021/09/12 09:20:29 Received: world-4
09:20:31 demo4-server3 | 2021/09/12 09:20:31 Received: world-5
09:20:33 demo4-server1 | 2021/09/12 09:20:33 Received: world-6
09:20:35 demo4-server2 | 2021/09/12 09:20:35 Received: world-7
09:20:37 demo4-server3 | 2021/09/12 09:20:37 Received: world-8
09:20:39 demo4-server1 | 2021/09/12 09:20:39 Received: world-9
09:20:41 demo4-server2 | 2021/09/12 09:20:41 Received: world-10
09:20:43 demo4-server1 | 2021/09/12 09:20:43 Received: world-11
09:20:45 demo4-server2 | 2021/09/12 09:20:45 Received: world-12
09:20:47 demo4-server3 | 2021/09/12 09:20:47 Received: world-13
//这里将权重改为5后
09:20:49 demo4-server1 | 2021/09/12 09:20:49 Received: world-14
09:20:51 demo4-server1 | 2021/09/12 09:20:51 Received: world-15
09:20:53 demo4-server1 | 2021/09/12 09:20:53 Received: world-16
09:20:55 demo4-server1 | 2021/09/12 09:20:55 Received: world-17
09:20:57 demo4-server1 | 2021/09/12 09:20:57 Received: world-18
09:20:59 demo4-server2 | 2021/09/12 09:20:59 Received: world-19
09:21:01 demo4-server3 | 2021/09/12 09:21:01 Received: world-20
09:21:03 demo4-server1 | 2021/09/12 09:21:03 Received: world-21
</code></pre>
<p>注意：每次nacos的service instance发生变化后，balancer都会重新build一个新Picker实例，后续会使用新Picker实例在其Connection集合中Pick出一个conn。</p>
<h3>4. 小结</h3>
<p>在本文中我们了解了gRPC的四种通信模式。我们重点关注了在最常用的simple RPC(unary RPC)模式下gRPC Client侧需要考虑的事情，包括：</p>
<ul>
<li>如何实现一个helloworld的一对一的通信</li>
<li>如何实现一个自定义的Resolver以实现一个client到一个静态服务实例集合的通信</li>
<li>如何实现一个自定义的Resolver以实现一个client到一个动态服务实例集合的通信</li>
<li>如何自定义客户端Balancer</li>
</ul>
<p>本文代码仅做示例使用，并未考虑太多异常处理。</p>
<p>本文涉及的所有代码可以从<a href="https://github.com/bigwhite/experiments/tree/master/grpc-client">这里下载</a>：https://github.com/bigwhite/experiments/tree/master/grpc-client</p>
<h3>5. 参考资料</h3>
<ul>
<li>gRPC Name Resolution &#8211; https://github.com/grpc/grpc/blob/master/doc/naming.md</li>
<li>Load Balancing in gRPC &#8211; https://github.com/grpc/grpc/blob/master/doc/load-balancing.md</li>
<li>基于 gRPC的服务发现与负载均衡（基础篇）- https://pandaychen.github.io/2019/07/11/GRPC-SERVICE-DISCOVERY/</li>
<li>比较 gRPC服务和HTTP API &#8211; https://docs.microsoft.com/zh-cn/aspnet/core/grpc/comparison</li>
</ul>
<h3>6. 附录</h3>
<h4>1) json vs. protobuf编解码性能基准测试结果</h4>
<p>测试源码位于<a href="https://github.com/bigwhite/experiments/tree/master/grpc-client/grpc-vs-httpjson/codec">这里</a>：https://github.com/bigwhite/experiments/tree/master/grpc-client/grpc-vs-httpjson/codec</p>
<p>我们使用了Go标准库json编解码、字节开源的<a href="https://github.com/bytedance/sonic">sonic json编解码包</a>以及<a href="https://tonybai.com/2020/03/16/build-high-performance-object-storage-with-minio-part1-prototype/">minio</a>开源的<a href="https://github.com/minio/simdjson-go">simdjson-go</a>高性能json解析库与protobuf作对比的结果如下：</p>
<pre><code>$go test -bench .
goos: darwin
goarch: amd64
pkg: github.com/bigwhite/codec
cpu: Intel(R) Core(TM) i5-8257U CPU @ 1.40GHz
BenchmarkSimdJsonUnmarshal-8           43304         28177 ns/op      113209 B/op         19 allocs/op
BenchmarkJsonUnmarshal-8              153214          7187 ns/op        1024 B/op          6 allocs/op
BenchmarkJsonMarshal-8                601590          2057 ns/op        2688 B/op          2 allocs/op
BenchmarkSonicJsonUnmarshal-8        1394211           861.1 ns/op      2342 B/op          2 allocs/op
BenchmarkSonicJsonMarshal-8          1592898           765.2 ns/op      2239 B/op          4 allocs/op
BenchmarkProtobufUnmarshal-8         3823441           317.0 ns/op      1208 B/op          3 allocs/op
BenchmarkProtobufMarshal-8           4461583           274.8 ns/op      1152 B/op          1 allocs/op
PASS
ok      github.com/bigwhite/codec   10.901s
</code></pre>
<p>benchmark测试结果印证了protobuf的编解码性能要远高于json编解码。但是在benchmark结果中，一个结果让我很意外，那就是号称高性能的simdjson-go的数据难看到离谱。谁知道为什么吗？simd指令没生效？字节开源的sonic的确性能很好，与pb也就2-3倍的差距，没有数量级的差距。</p>
<h4>2) gRPC(insecure) vs. json over http</h4>
<p>测试源码位于<a href="https://github.com/bigwhite/experiments/tree/master/grpc-client/grpc-vs-httpjson/protocol">这里</a>：https://github.com/bigwhite/experiments/tree/master/grpc-client/grpc-vs-httpjson/protocol</p>
<p>使用ghz对gRPC实现的server进行压测结果如下：</p>
<pre><code>$ghz --insecure -n 100000 -c 500 --proto publish.proto --call proto.PublishService.Publish -D data.json localhost:10000

Summary:
  Count:    100000
  Total:    1.67 s
  Slowest:    48.49 ms
  Fastest:    0.13 ms
  Average:    6.34 ms
  Requests/sec:    59924.34

Response time histogram:
  0.133  [1]     |
  4.968  [40143] |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎
  9.803  [47335] |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎
  14.639 [11306] |∎∎∎∎∎∎∎∎∎∎
  19.474 [510]   |
  24.309 [84]    |
  29.144 [89]    |
  33.980 [29]    |
  38.815 [3]     |
  43.650 [8]     |
  48.485 [492]   |

Latency distribution:
  10 % in 3.07 ms
  25 % in 4.12 ms
  50 % in 5.49 ms
  75 % in 7.94 ms
  90 % in 10.24 ms
  95 % in 11.28 ms
  99 % in 15.52 ms

Status code distribution:
  [OK]   100000 responses
</code></pre>
<p>使用hey对使用<a href="https://tonybai.com/2021/04/25/server-side-performance-nethttp-vs-fasthttp">fasthttp</a>与sonic实现的http server进行压测结果如下：</p>
<pre><code>$hey -n 100000 -c 500  -m POST -D ./data.json http://127.0.0.1:10001/

Summary:
  Total:    2.0012 secs
  Slowest:    0.1028 secs
  Fastest:    0.0001 secs
  Average:    0.0038 secs
  Requests/sec:    49969.9234

Response time histogram:
  0.000 [1]     |
  0.010 [96287] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
  0.021 [2639]  |■
  0.031 [261]   |
  0.041 [136]   |
  0.051 [146]   |
  0.062 [128]   |
  0.072 [43]    |
  0.082 [24]    |
  0.093 [10]    |
  0.103 [4]     |

Latency distribution:
  10% in 0.0013 secs
  25% in 0.0020 secs
  50% in 0.0031 secs
  75% in 0.0040 secs
  90% in 0.0062 secs
  95% in 0.0089 secs
  99% in 0.0179 secs

Details (average, fastest, slowest):
  DNS+dialup:    0.0000 secs, 0.0001 secs, 0.1028 secs
  DNS-lookup:    0.0000 secs, 0.0000 secs, 0.0000 secs
  req write:    0.0000 secs, 0.0000 secs, 0.0202 secs
  resp wait:    0.0031 secs, 0.0000 secs, 0.0972 secs
  resp read:    0.0005 secs, 0.0000 secs, 0.0575 secs

Status code distribution:
  [200]    99679 responses
</code></pre>
<p>我们看到：gRPC的性能（Requests/sec: 59924.34）要比http api性能(Requests/sec: 49969.9234)高出20%。</p>
<h4>3) nacos docker安装</h4>
<p>单机容器版nacos安装步骤如下：</p>
<pre><code>$git clone https://github.com/nacos-group/nacos-docker.git
$cd nacos-docker
$docker-compose -f example/standalone-derby.yaml up
</code></pre>
<p>nacos相关容器启动成功后，可以打开浏览器访问http://localhost:8848/nacos，打开nacos仪表盘登录页面，输入nacos/nacos即可进入nacos web操作界面。</p>
<hr />
<p><a href="https://mp.weixin.qq.com/s/jUqAL7hf2GmMun64BJufEA">“Gopher部落”知识星球</a>正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！部落目前虽小，但持续力很强。在2021年上半年，部落将策划两个专题系列分享，并且是部落独享哦：</p>
<ul>
<li>Go技术书籍的书摘和读书体会系列</li>
<li>Go与eBPF系列</li>
</ul>
<p>欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/202103/gopher-tribe-zsxq-card.png" alt="" /></p>
<p>Go技术专栏“<a href="https://www.imooc.com/read/87">改善Go语⾔编程质量的50个有效实践</a>”正在慕课网火热热销中！本专栏主要满足广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订<br />
阅！</p>
<p><img src="https://tonybai.com/wp-content/uploads/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>我的网课“<a href="https://coding.imooc.com/class/284.html">Kubernetes实战：高可用集群搭建、配置、运维与应用</a>”在慕课网热卖中，欢迎小伙伴们订阅学习！</p>
<p><img src="https://tonybai.com/wp-content/uploads/imooc-k8s-practice-with-qr.jpg" alt="img{512x368}" /></p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博：https://weibo.com/bigwhite20xx</li>
<li>微信公众号：iamtonybai</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>“Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544</li>
</ul>
<p>微信赞赏：<br />
<img src="https://tonybai.com/wp-content/uploads/wechat-zanshang-code-512x512.jpg" alt="img{512x368}" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2021, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2021/09/17/those-things-about-grpc-client/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>提高您的kubectl生产力（第三部分）：集群上下文切换、使用别名减少输入和插件扩展</title>
		<link>https://tonybai.com/2019/08/31/kubectl-productivity-part3/</link>
		<comments>https://tonybai.com/2019/08/31/kubectl-productivity-part3/#comments</comments>
		<pubDate>Sat, 31 Aug 2019 05:01:43 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[alias]]></category>
		<category><![CDATA[apiserver]]></category>
		<category><![CDATA[apt]]></category>
		<category><![CDATA[AWS]]></category>
		<category><![CDATA[Azure]]></category>
		<category><![CDATA[Bash]]></category>
		<category><![CDATA[bashrc]]></category>
		<category><![CDATA[bash_profile]]></category>
		<category><![CDATA[brew]]></category>
		<category><![CDATA[complete-alias]]></category>
		<category><![CDATA[Context]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[etcd]]></category>
		<category><![CDATA[fzf]]></category>
		<category><![CDATA[GCP]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[http]]></category>
		<category><![CDATA[jsonpath]]></category>
		<category><![CDATA[k8s]]></category>
		<category><![CDATA[krew]]></category>
		<category><![CDATA[kube-controller-manager]]></category>
		<category><![CDATA[kube-scheduler]]></category>
		<category><![CDATA[kubeconfig]]></category>
		<category><![CDATA[kubectl]]></category>
		<category><![CDATA[kubectl-aliases]]></category>
		<category><![CDATA[kubectl-ctx]]></category>
		<category><![CDATA[kubectl-ns]]></category>
		<category><![CDATA[kubectx]]></category>
		<category><![CDATA[kubelet]]></category>
		<category><![CDATA[kubens]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[macos]]></category>
		<category><![CDATA[Namespace]]></category>
		<category><![CDATA[plugin]]></category>
		<category><![CDATA[pod]]></category>
		<category><![CDATA[ReplicaSet]]></category>
		<category><![CDATA[RESTAPI]]></category>
		<category><![CDATA[RESTFUL]]></category>
		<category><![CDATA[Shell]]></category>
		<category><![CDATA[xpath]]></category>
		<category><![CDATA[yaml]]></category>
		<category><![CDATA[yum]]></category>
		<category><![CDATA[zsh]]></category>
		<category><![CDATA[上下文]]></category>
		<category><![CDATA[命名空间]]></category>
		<category><![CDATA[插件]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=2757</guid>
		<description><![CDATA[本文翻译自《Boosting your kubectl productivity》。 第一部分：什么是kubectl？ 第二部分：命令完成、资源规范快速查看和自定义列输出格式什么是kubectl？ 4. 轻松切换集群和名称空间 当kubectl必须向Kubernetes API发出请求时，它会读取系统上所谓的kubeconfig文件，以获取它需要访问的所有连接参数并向API服务器发出请求。 默认的kubeconfig文件是~/.kube/config。此文件通常由某个命令自动创建或更新（例如，aws eks update-kubeconfig或者gcloud container clusters get-credentials，如果您使用托管Kubernetes服务）。 使用多个集群时，您的kubeconfig文件中配置了多个集群的连接参数。这意味着，您需要一种方法来告诉kubectl 您希望它连接到哪个集群。 在集群中，您可以设置多个名称空间（名称空间是物理集群中的一种“虚拟”集群）。Kubectl也会从kubeconfig文件确定用于请求的命名空间。因此，您需要一种方法来告诉kubectl 您希望它使用哪个命名空间。 本节将介绍kubectl切换集群上下文的原理以及它是如何轻松完成的。 请注意，您还可以在KUBECONFIG环境变量中列出多个kubeconfig文件。在这种情况下，所有这些文件将在执行时合并为单个有效配置。您还可以使用&#8211;kubeconfig指定kubectl命令的选项以覆盖默认的kubeconfig文件。请参阅官方文档。 Kubeconfig文件 让我们看看kubeconfig文件实际包含的内容： 如您所见，kubeconfig文件由一组上下文组成。上下文包含以下三个元素： 集群(cluster)：集群的API服务器的URL 用户(user)：集群的特定用户的身份验证凭据 命名空间(namespace)：连接到集群时使用的命名空间 实际上，人们经常在他们的kubeconfig文件中为每个集群的配置一个上下文。但是，你也可以为每个集群配置多个上下文，其用户或命名空间不同。但这似乎不太常见，因此通常在集群和上下文之间存在一对一的映射。 在任何给定时间，其中一个上下文被设置为当前上下文（通过kubeconfig文件中的专用字段）： 当kubectl读取kubeconfig文件时，它总是使用当前上下文中的信息。因此，在上面的例子中，kubectl将连接到Hare集群。 因此，要切换到另一个集群，您只需更改kubeconfig文件中的当前上下文： 在上面的示例中，kubectl现在将连接到Fox集群。 要切换到同一集群中的另一个命名空间，您可以更改当前上下文的命名空间元素的值： 在上面的示例中，kubectl现在将使用Fox群集中的Prod命名空间（而不是之前设置的Test命名空间）。 请注意，kubectl还提供了&#8211;cluster，&#8211;user和&#8211;namespace，以及&#8211;context允许您覆盖单个元素和当前上下文本身的选项，无论kubeconfig文件中设置了什么。见kubectl options。 理论上，您可以通过手动编辑kubeconfig文件来执行这些更改。但当然这很乏味。以下部分介绍了允许您自动执行这些更改的各种工具。 使用kubectx kubectx是一种非常流行的用于在集群和命名空间之间切换的工具。 此工具提供允许您分别更改当前上下文和命名空间的命令kubectx和kubens命令。 如上所述，如果每个集群只有一个上下文，则更改当前上下文意味着更改集群。 在这里，您可以看到这两个命令： 在表象之下，这些命令只是编辑kubeconfig文件，如上一节中所述。 要安装kubectx，只需按照GitHub页面上的说明操作即可。 kubectx和kubens都通过完成交办提供命令完成(command completion)。这允许您自动完成上下文名称和名称空间，这样您就不必完全键入它们。您也可以在GitHub页面上找到设置完成的说明。 kubectx的另一个有用功能是交互模式。这与fzf工具结合使用，您必须单独安装（事实上，安装fzf，将自动启用kubectx交互模式）。交互模式允许您通过交互式模糊搜索界面（由fzf提供）选择目标上下文或命名空间。 使用shell别名 实际上，您并不需要单独的工具来更改当前上下文和命名空间，因为kubectl也提供了执行此操作的命令。特别是，该kubectl config命令提供了用于编辑kubeconfig文件的子命令。这里是其中的一些： kubectl config get-contexts：列出所有上下文 kubectl [...]]]></description>
			<content:encoded><![CDATA[<p>本文翻译自<a href="https://learnk8s.io/blog/kubectl-productivity/">《Boosting your kubectl productivity》</a>。</p>
<p>第一部分：<a href="https://tonybai.com/2019/08/29/kubectl-productivity-part1/">什么是kubectl？</a><br />
第二部分：<a href="https://tonybai.com/2019/08/30/kubectl-productivity-part2/">命令完成、资源规范快速查看和自定义列输出格式什么是kubectl？</a></p>
<h2>4. 轻松切换集群和名称空间</h2>
<p>当kubectl必须向<a href="https://tonybai.com/tag/k8s">Kubernetes</a> API发出请求时，它会读取系统上所谓的kubeconfig文件，以获取它需要访问的所有连接参数并向API服务器发出请求。</p>
<blockquote>
<p>默认的kubeconfig文件是~/.kube/config。此文件通常由某个命令自动创建或更新（例如，aws eks update-kubeconfig或者gcloud container clusters get-credentials，如果您使用托管Kubernetes服务）。</p>
</blockquote>
<p>使用多个集群时，您的kubeconfig文件中配置了多个集群的连接参数。这意味着，您需要一种方法来告诉kubectl 您希望它连接到哪个集群。</p>
<p>在集群中，您可以设置多个<a href="https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/">名称空间</a>（名称空间是物理集群中的一种“虚拟”集群）。Kubectl也会从kubeconfig文件确定用于请求的命名空间。因此，您需要一种方法来告诉kubectl 您希望它使用哪个命名空间。</p>
<p>本节将介绍kubectl切换集群上下文的原理以及它是如何轻松完成的。</p>
<blockquote>
<p>请注意，您还可以在<a href="https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/#the-kubeconfig-environment-variable">KUBECONFIG环境变量</a>中列出多个kubeconfig文件。在这种情况下，所有这些文件将在执行时合并为单个有效配置。您还可以使用&#8211;kubeconfig指定kubectl命令的选项以覆盖默认的kubeconfig文件。请参阅<a href="https://kubernetes.io/docs/tasks/access-application-cluster/configure-access-multiple-clusters/">官方文档</a>。</p>
</blockquote>
<h3>Kubeconfig文件</h3>
<p>让我们看看kubeconfig文件实际包含的内容：</p>
<p><img src="https://tonybai.com/wp-content/uploads/kubectl/kubectl-productivity-part3-1.png" alt="img{512x368}" /></p>
<p>如您所见，kubeconfig文件由一组上下文组成。上下文包含以下三个元素：</p>
<ul>
<li>集群(cluster)：集群的API服务器的URL</li>
<li>用户(user)：集群的特定用户的身份验证凭据</li>
<li>命名空间(namespace)：连接到集群时使用的命名空间</li>
</ul>
<blockquote>
<p>实际上，人们经常在他们的kubeconfig文件中为每个集群的配置一个上下文。但是，你也可以为每个集群配置多个上下文，其用户或命名空间不同。但这似乎不太常见，因此通常在集群和上下文之间存在一对一的映射。</p>
</blockquote>
<p>在任何给定时间，其中一个上下文被设置为当前上下文（通过kubeconfig文件中的专用字段）：</p>
<p><img src="https://tonybai.com/wp-content/uploads/kubectl/kubectl-productivity-part3-2.png" alt="img{512x368}" /></p>
<p>当kubectl读取kubeconfig文件时，它总是使用当前上下文中的信息。因此，在上面的例子中，kubectl将连接到Hare集群。</p>
<p>因此，要切换到另一个集群，您只需更改kubeconfig文件中的当前上下文：</p>
<p><img src="https://tonybai.com/wp-content/uploads/kubectl/kubectl-productivity-part3-3.png" alt="img{512x368}" /></p>
<p>在上面的示例中，kubectl现在将连接到Fox集群。</p>
<p>要切换到同一集群中的另一个命名空间，您可以更改当前上下文的命名空间元素的值：</p>
<p><img src="https://tonybai.com/wp-content/uploads/kubectl/kubectl-productivity-part3-4.png" alt="img{512x368}" /></p>
<p>在上面的示例中，kubectl现在将使用Fox群集中的Prod命名空间（而不是之前设置的Test命名空间）。</p>
<blockquote>
<p>请注意，kubectl还提供了&#8211;cluster，&#8211;user和&#8211;namespace，以及&#8211;context允许您覆盖单个元素和当前上下文本身的选项，无论kubeconfig文件中设置了什么。见kubectl options。</p>
</blockquote>
<p>理论上，您可以通过手动编辑kubeconfig文件来执行这些更改。但当然这很乏味。以下部分介绍了允许您自动执行这些更改的各种工具。</p>
<h3>使用kubectx</h3>
<p><a href="https://github.com/ahmetb/kubectx/">kubectx</a>是一种非常流行的用于在集群和命名空间之间切换的工具。</p>
<p>此工具提供允许您分别更改当前上下文和命名空间的命令kubectx和kubens命令。</p>
<blockquote>
<p>如上所述，如果每个集群只有一个上下文，则更改当前上下文意味着更改集群。</p>
</blockquote>
<p>在这里，您可以看到这两个命令：</p>
<p><img src="https://tonybai.com/wp-content/uploads/kubectl/kubectl-productivity-part3-5.gif" alt="img{512x368}" /></p>
<blockquote>
<p>在表象之下，这些命令只是编辑kubeconfig文件，如上一节中所述。</p>
</blockquote>
<p>要安装kubectx，只需按照<a href="https://github.com/ahmetb/kubectx/#installation">GitHub页面上的说明操作即可</a>。</p>
<p>kubectx和kubens都通过完成交办提供命令完成(command completion)。这允许您自动完成上下文名称和名称空间，这样您就不必完全键入它们。您也可以在<a href="https://github.com/ahmetb/kubectx/#installation">GitHub页面</a>上找到设置完成的说明。</p>
<p>kubectx的另一个有用功能是<a href="https://github.com/ahmetb/kubectx/#interactive-mode">交互模式</a>。这与<a href="https://github.com/junegunn/fzf">fzf</a>工具结合使用，您必须单独安装（事实上，安装fzf，将自动启用kubectx交互模式）。交互模式允许您通过交互式模糊搜索界面（由fzf提供）选择目标上下文或命名空间。</p>
<h3>使用shell别名</h3>
<p>实际上，您并不需要单独的工具来更改当前上下文和命名空间，因为kubectl也提供了执行此操作的命令。特别是，该kubectl config命令提供了用于编辑kubeconfig文件的子命令。这里是其中的一些：</p>
<ul>
<li>kubectl config get-contexts：列出所有上下文</li>
<li>kubectl config current-context：获取当前上下文</li>
<li>kubectl config use-context：更改当前上下文</li>
<li>kubectl config set-context：更改上下文的元素</li>
</ul>
<p>但是，直接使用这些命令并不是很方便，因为它们很难输入。但是你可以做的是将它们包装成可以更容易执行的shell别名。</p>
<p>我基于这些命令创建了一组别名，这些命令提供了与kubectx类似的功能。在这里你可以看到他们的行动：</p>
<p><img src="https://tonybai.com/wp-content/uploads/kubectl/kubectl-productivity-part3-6.gif" alt="img{512x368}" /></p>
<blockquote>
<p>请注意，别名使用fzf来提供交互式模糊搜索界面（如kubectx的交互模式）。这意味着，您需要安装fzf才能使用这些别名。</p>
</blockquote>
<p>以下是别名的定义：</p>
<pre><code># Get current context
alias krc='kubectl config current-context'
# List all contexts
alias klc='kubectl config get-contexts -o name | sed "s/^/  /;\|^  $(krc)$|s/ /*/"'
# Change current context
alias kcc='kubectl config use-context "$(klc | fzf -e | sed "s/^..//")"'

# Get current namespace
alias krn='kubectl config get-contexts --no-headers "$(krc)" | awk "{print \$5}" | sed "s/^$/default/"'
# List all namespaces
alias kln='kubectl get -o name ns | sed "s|^.*/|  |;\|^  $(krn)$|s/ /*/"'
# Change current namespace
alias kcn='kubectl config set-context --current --namespace "$(kln | fzf -e | sed "s/^..//")"'
</code></pre>
<p>要安装这些别名，你只需要在上面定义添加到您的~/.bashrc或~/.zshrc文件，并重新加载你的shell(source ~/.bashrc or source ~/.zshrc)！</p>
<h3>使用插件</h3>
<p>Kubectl允许安装可以像本机命令一样调用的插件。例如，您可以安装名为kubectl-foo的插件，然后将其调用为kubectl foo。</p>
<blockquote>
<p>Kubectl插件将在本文的后续部分中详细介绍。</p>
</blockquote>
<p>能够像这样更改当前上下文和命名空间不是很好吗？例如，运行kubectl ctx以更改上下文，kubectl ns更改名称空间？</p>
<p>我创建了两个允许这样做的插件：</p>
<ul>
<li><a href="https://github.com/weibeld/kubectl-ctx">kubectl-CTX</a></li>
<li><a href="https://github.com/weibeld/kubectl-ns">kubectl-NS</a></li>
</ul>
<p>在内部，插件构建在上一节的别名之上。</p>
<p>在这里你可以看到插件的实际效果：</p>
<p><img src="https://tonybai.com/wp-content/uploads/kubectl/kubectl-productivity-part3-7.gif" alt="img{512x368}" /></p>
<blockquote>
<p>请注意，插件使用fzf来提供交互式模糊搜索界面。这意味着，您需要安装fzf才能使用这些插件。</p>
</blockquote>
<p>要安装插件，你只需要将名为的shell脚本<a href="https://raw.githubusercontent.com/weibeld/kubectl-ctx/master/kubectl-ctx">kubectl-ctx</a>和<a href="https://raw.githubusercontent.com/weibeld/kubectl-ns/master/kubectl-ns">kubectl-ns</a>的脚本下载以到PATH下的任何目录中，并使他们具备可执行权限（例如，使用chmod +x）。紧接着，你就应该能够使用kubectl ctx和kubectl ns！</p>
<h2>5. 使用自动生成的别名减少输入</h2>
<p>Shell别名通常是减少手工输入的好方法。该<a href="https://github.com/ahmetb/kubectl-aliases">kubectl-aliases</a>项目就是以这个想法为核心，并提供800多个kubectl命令别名。</p>
<p>您可能想知道如何记住800个别名？实际上，您不需要记住它们，因为它们都是根据一个简单的方案生成的，下面将显示一些示例别名：</p>
<p><img src="https://tonybai.com/wp-content/uploads/kubectl/kubectl-productivity-part3-8.png" alt="img{512x368}" /></p>
<p>如您所见，别名由<strong>组件(component)</strong>组成，每个组件代表kubectl命令的特定元素。每个别名可以有一个用于基本命令，操作和资源的组件，以及用于选项的多个组件，您只需根据上述方案从左到右“填充”这些组件。</p>
<blockquote>
<p>请注意，目前完全详细的方案在<a href="https://github.com/ahmetb/kubectl-aliases#syntax-explanation">GitHub页面</a>上。在那里，您还可以找到别名的<a href="https://github.com/ahmetb/kubectl-aliases/blob/master/.kubectl_aliases">完整列表</a>。</p>
</blockquote>
<p>例如，别名kgpooyamlall代表命令kubectl get pods -o yaml &#8211;all-namespaces：</p>
<p><img src="https://tonybai.com/wp-content/uploads/kubectl/kubectl-productivity-part3-9.png" alt="img{512x368}" /></p>
<p>请注意，大多数选项组件的相对顺序无关紧要。所以，kgpooyamlall相当于kgpoalloyaml。</p>
<p>您不需要将所有组件用于别名。例如k，kg，klo，ksys，或者kgpo是有效的别名也。此外，您可以在命令行中将别名与其他单词组合使用。</p>
<p>例如，您可以k proxy用于运行kubectl proxy：</p>
<p><img src="https://tonybai.com/wp-content/uploads/kubectl/kubectl-productivity-part3-10.png" alt="img{512x368}" /></p>
<p>或者您可以kg roles用于运行kubectl get roles（目前不存在Roles资源的别名组件）：</p>
<p><img src="https://tonybai.com/wp-content/uploads/kubectl/kubectl-productivity-part3-11.png" alt="img{512x368}" /></p>
<p>要获取特定Pod，您可以使用kgpo my-pod以运行kubectl get pod my-pod：</p>
<p><img src="https://tonybai.com/wp-content/uploads/kubectl/kubectl-productivity-part3-12.png" alt="img{512x368}" /></p>
<p>请注意，某些别名甚至需要在命令行上的进一步参数。例如，kgpol别名代表kubectl get pods -l。该-l选项需要一个参数（标签规范）。所以，你必须使用这个别名，例如，像这样:</p>
<p><img src="https://tonybai.com/wp-content/uploads/kubectl/kubectl-productivity-part3-13.png" alt="img{512x368}" /></p>
<blockquote>
<p>出于这个原因，你可以使用a，f以及l只在一个别名的结尾部分。</p>
</blockquote>
<p>一般来说，一旦你掌握了这个方案，就可以直观地从你想要执行的命令中推断出别名，并节省大量的输入！</p>
<h3>安装</h3>
<p>要安装kubectl-别名，你只需要下载<a href="https://raw.githubusercontent.com/ahmetb/kubectl-aliases/master/.kubectl_aliases">.kubectl-aliases</a>GitHub文件，并在你的~/.bashrc或~/.zshrc文件生效它：</p>
<pre><code>source ~/.kubectl_aliases
</code></pre>
<p>重新加载shell后，您应该能够使用所有800个kubectl别名！</p>
<h3>命令完成</h3>
<p>如您所见，您经常在命令行上向别名添加更多单词。例如：</p>
<pre><code>$kgpooyaml test-pod-d4b77b989
</code></pre>
<p>如果你使用kubectl命令完成，那么你可能习惯于自动完成资源名称之类的事情。但是当你使用别名时，你还可以这样做吗？</p>
<p>这是一个重要的问题，因为如果它不起作用，那将消除这些别名的一些好处！</p>
<p>答案取决于您使用的shell。</p>
<p>对于Zsh，完成对于别名是开箱即用的。</p>
<p>不幸的是，对于Bash，默认情况下，对于别名，完成功能不起作用。好消息是它可以通过一些额外的步骤来完成。下一节将介绍如何执行此操作。</p>
<h3>在Bash中启用别名的完成</h3>
<p>Bash的问题在于它尝试在别名上尝试完成（每当你按Tab键），而不是在别名命令（如Zsh）上。由于您没有所有800个别名的完成脚本，因此不起作用。</p>
<p><a href="https://github.com/cykerway/complete-alias">complete-alias</a>项目提供了解决这个问题的通用解决方案。它使用别名的完成机制，在内部将别名扩展到别名命令，并返回扩展命令的完成建议。这意味着，它使别名的完成行为与别名命令完全相同。</p>
<p>在下文中，我将首先解释如何安装complete-alias，然后如何配置它以启用所有kubectl别名的完成。</p>
<h4>安装complete-alias</h4>
<p>首先，complete-alias依赖于<a href="https://github.com/scop/bash-completion">bash-completion</a>。因此，您需要确保在安装complete-alias之前安装了bash-completion。早先已经为Linux和macOS提供了相关说明。</p>
<blockquote>
<p>对于macOS用户的重要注意事项：与kubectl完成脚本一样，complete-alias不适用于Bash 3.2，这是macOS上Bash的默认版本。特别是，complete-alias依赖于bash-completion v2（brew install bash-completion@2），它至少需要Bash 4.1。这意味着，要在macOS上使用complete-alias，您需要安装较新版本的Bash。</p>
</blockquote>
<p>要安装complete-alias，您只需bash_completion.sh从GitHub存储库下载脚本，并将其在您的~/.bashrc文件中source：</p>
<pre><code>source ~/bash_completion.sh
</code></pre>
<p>重新加载shell后，应正确安装complete-alias。</p>
<h4>启用kubectl别名的完成</h4>
<p>从技术上讲，complete-alias提供了_complete_aliasshell函数。此函数检查别名并返回别名命令的完成建议。</p>
<p>要将其与特定别名挂钩，您必须使用completeBash内置来设置别名_complete_alias的完成功能。</p>
<p>举个例子，我们k来看一下代表kubectl命令的别名。要设置_complete_alias此别名的完成功能，您必须执行以下命令：</p>
<pre><code>$complete -F _complete_alias k
</code></pre>
<p>这样做的结果是，无论何时在k别名上自动完成，_complete_alias都会调用该函数，该函数检查别名并返回kubectl命令的完成建议。</p>
<p>作为另一个例子，让我们采用kg代表的别名kubectl get：</p>
<pre><code>$complete -F _complete_alias kg
</code></pre>
<p>同样，这样做的结果是，当您自动完成时kg，您将获得与之相同的完成建议kubectl get。</p>
<blockquote>
<p>请注意，可以以这种方式对系统上的任何别名使用complete-alias。</p>
</blockquote>
<p>因此，要启用所有 kubectl别名的完成，您只需为每个别名运行上述命令。以下代码片段完全相同（假设您安装了kubectl-aliases ~/.kubectl-aliases）：</p>
<pre><code>for _a in $(sed '/^alias /!d;s/^alias //;s/=.*$//' ~/.kubectl_aliases); do
  complete -F _complete_alias "$_a"
done
</code></pre>
<p>只需将此片段添加到您的~/.bashrc文件中，重新加载您的shell，现在您应该可以使用所有800 kubectl别名的完成！</p>
<h2>6. 使用插件扩展kubectl</h2>
<p>从<a href="https://github.com/kubernetes/kubernetes/blob/master/CHANGELOG-1.12.md#sig-cli-1">版本1.12</a>开始，kubectl包含一个<a href="https://kubernetes.io/docs/tasks/extend-kubectl/kubectl-plugins/">插件机制</a>，允许您使用自定义命令扩展kubectl。</p>
<p>以下是kubectl插件的示例，可以调用为kubectl hello：</p>
<pre><code>$ kubectl hello
Hello, I'm a kubectl plugin!
</code></pre>
<blockquote>
<p>kubectl插件机制将严格遵循Git插件机制的设计。</p>
</blockquote>
<p>本节将向您展示如何安装插件，您可以在哪里找到现有的插件，以及如何创建自己的插件。</p>
<h4>安装插件</h4>
<p>Kubectl插件作为简单的可执行文件分发，其名称的形式为kubectl-x。前缀kubectl-是必需的，接下来是允许调用插件的新kubectl子命令。</p>
<p>例如，上面显示的hello插件将作为名为的文件分发kubectl-hello。</p>
<p>要<a href="https://kubernetes.io/docs/tasks/extend-kubectl/kubectl-plugins/#installing-kubectl-plugins">安装插件</a>，您只需将kubectl-x文件复制到您的任何目录中PATH并使其可执行（例如，使用chmod +x）。之后，您可以立即调用该插件kubectl x。</p>
<p>您可以使用以下命令列出系统上当前安装的所有插件：</p>
<pre><code>$kubectl plugin list
</code></pre>
<p>如果您有多个具有相同名称的插件，或者存在不可执行的插件文件，则此命令还会显示警告。</p>
<h3>使用krew查找和安装插件</h3>
<p>Kubectl插件可以像软件包一样共享和重用。但是在哪里可以找到其他人共享的插件？</p>
<p>该<a href="https://github.com/GoogleContainerTools/krew">krew项目</a>旨在提供一个统一的解决方案，共享，查找，安装和管理kubectl插件。该项目将自己称为“kubectl插件的包管理器”（名称krew是brew的提示）。</p>
<p>Krew 以kubectl<a href="https://github.com/GoogleContainerTools/krew-index">插件索引</a>为中心，您可以从中选择和安装。</p>
<pre><code>$ kubectl krew search | less
$ kubectl krew search view
$ kubectl krew info view-utilization
$ kubectl krew install view-utilization
$ kubectl krew list
</code></pre>
<p>如您所见，krew本身是一个kubectl插件。这意味着，安装krew本质上就像安装任何其他kubectl插件一样。您可以在<a href="https://github.com/GoogleContainerTools/krew/#installation">GitHub页面</a>上找到krew的详细安装说明。</p>
<p>最重要的krew命令如下：</p>
<pre><code># Search the krew index (with an optional search query)
$ kubectl krew search [&lt;query&gt;]
# Display information about a plugin
$ kubectl krew info &lt;plugin&gt;
# Install a plugin
$ kubectl krew install &lt;plugin&gt;
# Upgrade all plugins to the newest versions
$ kubectl krew upgrade
# List all plugins that have been installed with krew
$ kubectl krew list
# Uninstall a plugin
$ kubectl krew remove &lt;plugin&gt;
</code></pre>
<p>请注意，使用krew安装插件并不妨碍以传统方式安装插件。即使你使用krew，你仍然可以通过其他方式安装你在其他地方找到的插件（或自己创建）。</p>
<blockquote>
<p>请注意，该kubectl krew list命令仅列出已使用krew安装的插件，而该kubectl plugin list命令列出了所有插件，即使用krew安装的插件和以其他方式安装的插件。</p>
</blockquote>
<h3>在其他地方寻找插件</h3>
<p>Krew仍然是一个年轻的项目，目前<a href="https://github.com/GoogleContainerTools/krew-index/">krew索引</a>中只有大约30个插件。如果你在那里找不到你需要的东西，你可以在其他地方寻找插件，例如，在GitHub上。</p>
<p>我建议查看<a href="https://github.com/topics/kubectl-plugins">kubectl-plugins GitHub主题</a>。你会发现有几十个可用的插件值得一看。</p>
<h3>创建自己的插件</h3>
<p>当然，您可以<a href="https://kubernetes.io/docs/tasks/extend-kubectl/kubectl-plugins/#writing-kubectl-plugins">创建自己的kubectl插件</a>，这很容易实现。</p>
<p>您只需创建一个可执行文件，执行您想要的操作，为其命名kubectl-x，然后按上述方法安装它。</p>
<p>可执行文件可以是任何类型，Bash脚本，编译的Go程序，Python脚本，它确实无关紧要。唯一的要求是它可以由操作系统直接执行。</p>
<p>我们现在创建一个示例插件。在上部分中，您使用kubectl命令列出每个pod的容器镜像。您可以轻松地将此命令转换为可以调用的插件，比如说kubectl img。</p>
<p>为此，只需创建一个名为kubectl-img以下内容的文件：</p>
<pre><code>#!/bin/bash
kubectl get pods -o custom-columns='NAME:metadata.name,IMAGES:spec.containers[*].image'

</code></pre>
<p>现在使文件可执行，chmod +x kubectl-img并将其移动到您的任何PATH中的目录。之后，您可以立即开始使用该插件kubectl img！</p>
<blockquote>
<p>如上所述，kubectl插件可以用任何编程语言或脚本语言编写。如果使用shell脚本，则可以从插件轻松调用kubectl。但是，您可以使用实际编程语言编写更复杂的插件，例如，使用<a href="https://kubernetes.io/docs/reference/using-api/client-libraries/">Kubernetes客户端库</a>。如果使用<a href="https://tonybai.com/tag/go">Go</a>，您还可以使用<a href="https://github.com/kubernetes/cli-runtime">cli-runtime库</a>，它专门用于编写kubectl插件。</p>
</blockquote>
<h3>分享你的插件</h3>
<p>如果您认为其中一个插件可能对其他人有用，请随时在GitHub上分享。确保将其添加到<a href="https://github.com/topics/kubectl-plugins">kubectl-plugins主题</a>中，以便其他人可以找到它。</p>
<p>您还可以请求将您的插件添加到<a href="https://github.com/GoogleContainerTools/krew-index/">krew索引</a>中。您可以在<a href="https://github.com/GoogleContainerTools/krew/blob/master/docs/DEVELOPER_GUIDE.md">krew GitHub存储库</a>中找到有关如何执行此操作的说明。</p>
<h3>命令完成</h3>
<p>目前，插件机制遗憾的是还不支持命令完成。这意味着您需要完全键入插件名称以及插件的任何参数。</p>
<p>但是，在kubectl GitHub存储库中有一个处于open状态的<a href="https://github.com/kubernetes/kubectl/issues/585">功能请求issue</a>。因此，此功能有可能在将来的某个时间得到实现。</p>
<p>以上就是有关kubectl高效使用的所有内容了！</p>
<hr />
<p>我的网课“<a href="https://coding.imooc.com/class/284.html">Kubernetes实战：高可用集群搭建、配置、运维与应用</a>”在慕课网上线了，感谢小伙伴们学习支持！</p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/<br />
smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。</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>我的联系方式：</p>
<p>微博：https://weibo.com/bigwhite20xx<br />
微信公众号：iamtonybai<br />
博客：tonybai.com<br />
github: https://github.com/bigwhite</p>
<p>微信赞赏：<br />
<img src="https://tonybai.com/wp-content/uploads/wechat-zanshang-code-512x512.jpg" alt="img{512x368}" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2019, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2019/08/31/kubectl-productivity-part3/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>官宣：慕课网课程“Kubernetes实战：高可用集群搭建、配置、运维与应用”上线了</title>
		<link>https://tonybai.com/2018/10/17/imooc-course-kubernetes-practice-go-online/</link>
		<comments>https://tonybai.com/2018/10/17/imooc-course-kubernetes-practice-go-online/#comments</comments>
		<pubDate>Wed, 17 Oct 2018 10:28:04 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[aliyun]]></category>
		<category><![CDATA[cloud-native]]></category>
		<category><![CDATA[CNCF]]></category>
		<category><![CDATA[cni]]></category>
		<category><![CDATA[container]]></category>
		<category><![CDATA[controller]]></category>
		<category><![CDATA[dashboard]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[EFK]]></category>
		<category><![CDATA[elastic]]></category>
		<category><![CDATA[ElasticSearch]]></category>
		<category><![CDATA[ELK]]></category>
		<category><![CDATA[event]]></category>
		<category><![CDATA[harbor]]></category>
		<category><![CDATA[heapster]]></category>
		<category><![CDATA[High-Available]]></category>
		<category><![CDATA[image]]></category>
		<category><![CDATA[imooc]]></category>
		<category><![CDATA[ingress]]></category>
		<category><![CDATA[istio]]></category>
		<category><![CDATA[k8s]]></category>
		<category><![CDATA[kubeadm]]></category>
		<category><![CDATA[kubectl]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[label]]></category>
		<category><![CDATA[logging]]></category>
		<category><![CDATA[Master]]></category>
		<category><![CDATA[microservice]]></category>
		<category><![CDATA[Namespace]]></category>
		<category><![CDATA[network]]></category>
		<category><![CDATA[pod]]></category>
		<category><![CDATA[PV]]></category>
		<category><![CDATA[PVC]]></category>
		<category><![CDATA[registry]]></category>
		<category><![CDATA[Service]]></category>
		<category><![CDATA[ServiceMesh]]></category>
		<category><![CDATA[storage]]></category>
		<category><![CDATA[StorageClass]]></category>
		<category><![CDATA[troubleshooting]]></category>
		<category><![CDATA[volume]]></category>
		<category><![CDATA[weave]]></category>
		<category><![CDATA[worker]]></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=2644</guid>
		<description><![CDATA[距离我的第一门网课《Kubernetes基础：开启云原生之门》上线已经过去5个多月了，我的实战课《Kubernetes实战：高可用集群搭建、配置、运维与应用》终于在9月27日正式上线了。 一. 课程介绍 《Kubernetes实战：高可用集群搭建、配置、运维与应用》的课程内容与最初课程设计时规划的内容大纲没有太多出入，基本就是根据我最初的想法拟定的内容，这也基本是我这两年学习k8s、积累的k8s实践的路线。整个课程基于kubernetes 1.10.2版本(docker 17.03.2ce)。课程内容大致分为七个部分（与课程主页的课程目录结构稍有差异，但课程内容是一致的）： 第一章 搭建你的第一个Kubernetes集群 本章介绍了一个使用kubeadm引导的Kubernetes集群的搭建和基本配置方法。 1-1: 导学 1-2: 安装准备 1-3: 初始化集群master节点 1-4: 向集群加入worker节点 1-5: 安装dashboard和heapster 1-6: 验证集群安装结果 第二章 Kubernetes集群探索 本章对kubeadm初始化集群的原理进行了讲解，并对已经建立的k8s集群中的各个组件进行详细介绍，包括功用、原理和配置等 2-1: kubeadm init流程揭秘 2-2: kubeadm join流程揭秘 2-3: kubernetes核心组件详解 2-4: kubectl详解 第三章 Kubernetes网络、安全与存储 本章讲解k8s集群的三个难点：网络、安全与存储的概念和运行原理。 3-1：kubernetes集群网络 3-1-1: kubernetes集群的“三个网络” 3-1-2: kubernetes网络的设计要求 3-1-3: kubernetes网络实现 3-1-4: pod网络实现原理 3-1-5: pod网络方案对比 3-1-6: service网络实现原理 3-2: kubernetes集群安全 3-2-1: kube-apiserver安全模型 3-2-2: [...]]]></description>
			<content:encoded><![CDATA[<p>距离我的第一门网课<a href="https://www.imooc.com/learn/978">《Kubernetes基础：开启云原生之门》</a>上线已经过去5个多月了，我的实战课<a href="https://coding.imooc.com/class/chapter/284.html">《Kubernetes实战：高可用集群搭建、配置、运维与应用》</a>终于在9月27日正式上线了。</p>
<p><img src="https://tonybai.com/wp-content/uploads/k8s-practice-frontpage.png" alt="img{512x368}" /></p>
<h3>一. 课程介绍</h3>
<p><img src="https://tonybai.com/wp-content/uploads/k8s-practice-content-1.png" alt="img{512x368}" /></p>
<p><img src="https://tonybai.com/wp-content/uploads/k8s-practice-content-2.png" alt="img{512x368}" /></p>
<p><a href="https://coding.imooc.com/class/chapter/284.html">《Kubernetes实战：高可用集群搭建、配置、运维与应用》</a>的课程内容与最初课程设计时规划的内容大纲没有太多出入，基本就是根据我最初的想法拟定的内容，<strong>这也基本是我这两年学习k8s、积累的k8s实践的路线</strong>。整个课程基于kubernetes 1.10.2版本(<a href="https://tonybai.com/tag/docker">docker</a> 17.03.2ce)。课程内容大致分为七个部分（与课程主页的课程目录结构稍有差异，但课程内容是一致的）：</p>
<p>第一章 搭建你的第一个<a href="https://tonybai.com/tag/kubernetes">Kubernetes集群</a></p>
<p>本章介绍了一个使用<a href="https://tonybai.com/2016/12/30/install-kubernetes-on-ubuntu-with-kubeadm/">kubeadm</a>引导的Kubernetes集群的搭建和基本配置方法。</p>
<ul>
<li>1-1: 导学</li>
<li>1-2: 安装准备</li>
<li>1-3: 初始化集群master节点</li>
<li>1-4: 向集群加入worker节点</li>
<li>1-5: <a href="https://tonybai.com/2017/09/26/some-notes-about-deploying-kubernetes-dashboard-1-7-0/">安装dashboard和heapster</a></li>
<li>1-6: 验证集群安装结果</li>
</ul>
<p>第二章 <a href="https://tonybai.com/2017/01/24/explore-kubernetes-cluster-installed-by-kubeadm/">Kubernetes集群探索</a></p>
<p>本章对kubeadm初始化集群的原理进行了讲解，并对已经建立的k8s集群中的各个组件进行详细介绍，包括功用、原理和配置等</p>
<ul>
<li>2-1: kubeadm init流程揭秘</li>
<li>2-2: kubeadm join流程揭秘</li>
<li>2-3: kubernetes核心组件详解</li>
<li>2-4: <a href="https://tonybai.com/2018/06/14/the-authentication-and-authorization-of-kubectl-when-accessing-k8s-cluster/">kubectl</a>详解</li>
</ul>
<p>第三章 Kubernetes网络、安全与存储<br />
本章讲解k8s集群的三个难点：网络、安全与存储的概念和运行原理。</p>
<p>3-1：kubernetes<a href="https://tonybai.com/2017/01/17/understanding-flannel-network-for-kubernetes/">集群网络</a></p>
<ul>
<li>3-1-1: kubernetes集群的“三个网络”</li>
<li>3-1-2: kubernetes网络的设计要求</li>
<li>3-1-3: kubernetes网络实现</li>
<li>3-1-4: pod网络实现原理</li>
<li>3-1-5: pod网络方案对比</li>
<li>3-1-6: service网络实现原理</li>
</ul>
<p>3-2: <a href="https://tonybai.com/2018/06/14/the-authentication-and-authorization-of-kubectl-when-accessing-k8s-cluster/">kubernetes集群安全</a></p>
<ul>
<li>3-2-1: kube-apiserver安全模型</li>
<li>3-2-2: 传输安全</li>
<li>3-2-3: <a href="https://tonybai.com/2016/11/25/the-security-settings-for-kubernetes-cluster/">身份验证</a></li>
<li>3-2-4: 授权</li>
<li>3-2-5: 准入控制</li>
</ul>
<p>3-3 kubernets集群存储</p>
<ul>
<li>3-3-1: Volume</li>
<li>3-3-2: <a href="https://tonybai.com/2016/11/07/integrate-kubernetes-with-ceph-rbd/">PV和PVC</a></li>
<li>3-3-3: StorageClass和动态PV供给</li>
<li>3-3-4: Kubernetes存储模型</li>
</ul>
<p>第四章 <a href="https://tonybai.com/2017/05/15/setup-a-ha-kubernetes-cluster-based-on-kubeadm-part1/">高可用Kubernetes集群</a>搭建方案<br />
本章介绍了什么是高可用k8s集群，并给出了一个可行的高可用Kubernetes集群的搭建方案。</p>
<ul>
<li>4-1: 什么是高可用Kubernetes集群</li>
<li>4-2: 高可用Kubernetes集群方案</li>
</ul>
<p>第五章 Kubernetes集群常见运维操作</p>
<p>本章讲解了Kubernetes集群的基本运维操作，包括node管理、service、pod管理、日志查看等。并讲解了面对k8s集群问题时如何做troubleshooting。</p>
<ul>
<li>5-1: 管理Node与Label</li>
<li>5-2: 管理Namespace、Service和Pod</li>
<li>5-3: <a href="https://tonybai.com/2017/10/16/out-of-node-resource-handling-in-kubernetes-cluster/">计算资源管理</a></li>
<li>5-4: 查看事件和容器日志</li>
<li>5-5: 常用TroubleShooting方法</li>
</ul>
<p>第六章 Kubernetes支撑<a href="https://www.cncf.io/">云原生应用</a>开发案例<br />
本章讲解了Kubernetes集群的应用：支撑云原生应用开发。并通过实际操作讲解了镜像仓库、集中日志以及云应用治理框架的搭建和使用。</p>
<ul>
<li>6-1: Kubernetes与云原生应用</li>
<li>6-2: <a href="https://tonybai.com/2017/12/08/deploy-high-availability-harbor-on-kubernetes-cluster/">高可用私有镜像仓库搭建</a></li>
<li>6-3: <a href="https://tonybai.com/2018/06/13/setup-efk-on-kubernetes-1-10-3-in-the-hard-way/">基于ElasticSearch Stack搭建集群Logging设施</a></li>
<li>6-4: <a href="https://tonybai.com/2018/01/03/an-intro-of-microservices-governance-by-istio/">基于istio service mesh实现服务治理</a></li>
</ul>
<p>第七章 课程回顾与总结</p>
<h3>二. 做网课目的与课程思路</h3>
<p>当初接下慕课商务的这门课主要是出于两个目的：</p>
<ul>
<li>通过这门课程对自己的k8s学习和实践做一个阶段性的系统总结</li>
<li>尝试一下网课这个“新鲜”事物</li>
</ul>
<p>现在看来，当初这两个“目的”都实现了。但是录制网课的确是件很“辛苦”的事情，不知道多少的夜晚和周末都留给了“网课资料编写和录制”。尤其是Kubernetes这个主题，讲起来“顾虑”很多：</p>
<ul>
<li>
<p>和编程语言课不同，Kubernetes平台是个复杂的平台，外延生态很庞大。k8s概念多，如果不把概念和原理交待清楚、讲透彻，直接就上手操作，那样学习后，对k8s的理解仍然不会很深刻，很多问题仍然无法自己去解决，尤其是中高级阶段。 这就导致很多小伙伴认为课程概念讲解“有些多”；</p>
</li>
<li>
<p>生产环境中k8s集群有大有小，使用目的也是大不相同，安装方式也是有很多种(官方就列了10多种)，所在的网络环境以及使用的pod网络插件也是区别很大，遇到的问题更是千差万别，这里在准备 课程时也是思来想去，无法覆盖所有生产环境的所有情况。最后决定使用kubeadm搭建一个4节点的集群(使用weave network plugin)，可能能更好的满足初学者的需求，学员们更容易获取搭建这样一个 k8s环境所需的资源。而关于课程中实际操作部分重点集中在前面的k8s搭建、集群探索以及后面的k8s对云应用支撑的环节。所以如果小伙伴们的环境与课程不同，可以在课程后提问，我会尽量第一时间、细致的回答各位的问题。</p>
</li>
<li>
<p>关于时长，我在课程里尽量做到没有”废话“。现在的网课多根据“时长”定价（虽然不赞同，但是目前也没有一个更好的量化课程质量的方法）：比如10个小时以上可能就会定到399元，但是不足10小时，可能就在199元这个价位。<strong>于是我努力地将课程做到了“199”这个价位上了</strong>。对于真正想学习k8s的小伙伴们，这也许是一个“好消息”:)。</p>
</li>
</ul>
<h3>三. 课程小结</h3>
<p>Kubernetes还在快速不断地演进！我个人觉得学完本门课程也仅仅是“Kubernetes实践之路”的一个开始而已！应用上云的趋势已经不可逆转，对于云应用开发人员来说，<strong>了解和学习Kubernetes就像当年单机时代开发人员要去了解PC操作系统一样重要</strong>！希望本门课程能给更多的开发者带去帮助！</p>
<p>下面是课程的自制海报，欢迎转发:)</p>
<p><img src="https://tonybai.com/wp-content/uploads/k8s-practice-with-qr-and-text.png" alt="img{512x368}" /></p>
<hr />
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/<br />
smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。</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>我的联系方式：</p>
<p>微博：https://weibo.com/bigwhite20xx<br />
微信公众号：iamtonybai<br />
博客：tonybai.com<br />
github: https://github.com/bigwhite</p>
<p>微信赞赏：<br />
<img src="https://tonybai.com/wp-content/uploads/wechat-zanshang-code-512x512.jpg" alt="img{512x368}" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2018 &#8211; 2020, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2018/10/17/imooc-course-kubernetes-practice-go-online/feed/</wfw:commentRss>
		<slash:comments>6</slash:comments>
		</item>
		<item>
		<title>在Kubernetes Pod中使用Service Account访问API Server</title>
		<link>https://tonybai.com/2017/03/03/access-api-server-from-a-pod-through-serviceaccount/</link>
		<comments>https://tonybai.com/2017/03/03/access-api-server-from-a-pod-through-serviceaccount/#comments</comments>
		<pubDate>Fri, 03 Mar 2017 01:08:29 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Admission-Control]]></category>
		<category><![CDATA[Authentication]]></category>
		<category><![CDATA[base64]]></category>
		<category><![CDATA[cluster]]></category>
		<category><![CDATA[container]]></category>
		<category><![CDATA[deployment]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[k8s]]></category>
		<category><![CDATA[kube-apiserver]]></category>
		<category><![CDATA[kube-up.sh]]></category>
		<category><![CDATA[kubeadm]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[Master]]></category>
		<category><![CDATA[minion]]></category>
		<category><![CDATA[Namespace]]></category>
		<category><![CDATA[pod]]></category>
		<category><![CDATA[serviceaccount]]></category>
		<category><![CDATA[token]]></category>
		<category><![CDATA[useraccount]]></category>
		<category><![CDATA[容器]]></category>
		<category><![CDATA[数字证书]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=2194</guid>
		<description><![CDATA[Kubernetes API Server是整个Kubernetes集群的核心，我们不仅有从集群外部访问API Server的需求，有时，我们还需要从Pod的内部访问API Server。 然而，在生产环境中，Kubernetes API Server都是“设防”的。在《Kubernetes集群的安全配置》一文中，我提到过：Kubernetes通过client cert、static token、basic auth等方法对客户端请求进行身份验证。对于运行于Pod中的Process而言，有些时候这些方法是适合的，但有些时候，像client cert、static token或basic auth这些信息是不便于暴露给Pod中的Process的。并且通过这些方法通过API Server验证后的请求是具有全部授权的，可以任意操作Kubernetes cluster，这显然是不能满足安全要求的。为此，Kubernetes更推荐大家使用service account这种方案的。本文就带大家详细说说如何通过service account从一个Pod中访问API Server的。 零、试验环境 本文的试验环境是Kubernetes 1.3.7 cluster，双节点，master承载负荷。cluster通过kube-up.sh搭建的，具体的搭建方法见《一篇文章带你了解Kubernetes安装》。 一、什么是service account？ 什么是service account? 顾名思义，相对于user account（比如：kubectl访问APIServer时用的就是user account），service account就是Pod中的Process用于访问Kubernetes API的account，它为Pod中的Process提供了一种身份标识。相比于user account的全局性权限，service account更适合一些轻量级的task，更聚焦于授权给某些特定Pod中的Process所使用。 service account作为一种resource存在于Kubernetes cluster中，我们可以通过kubectl获取当前cluster中的service acount列表： # kubectl get serviceaccount --all-namespaces NAMESPACE NAME SECRETS AGE default default 1 140d kube-system default 1 140d [...]]]></description>
			<content:encoded><![CDATA[<p><a href="https://kubernetes.io/docs/admin/kube-apiserver/">Kubernetes API Server</a>是整个<a href="http://tonybai.com/2016/10/18/learn-how-to-install-kubernetes-on-ubuntu/">Kubernetes集群</a>的核心，我们不仅有从集群外部访问API Server的需求，有时，我们还需要从Pod的内部访问API Server。</p>
<p>然而，在生产环境中，Kubernetes API Server都是“设防”的。在《<a href="http://tonybai.com/2016/11/25/the-security-settings-for-kubernetes-cluster/">Kubernetes集群的安全配置</a>》一文中，我提到过：Kubernetes通过client cert、static token、basic auth等方法对客户端请求进行<a href="https://kubernetes.io/docs/admin/authentication/#authentication-strategies">身份验证</a>。对于运行于Pod中的Process而言，有些时候这些方法是适合的，但有些时候，像client cert、static token或basic auth这些信息是不便于暴露给Pod中的Process的。并且通过这些方法通过API Server验证后的请求是具有全部授权的，可以任意操作<a href="http://tonybai.com/2017/01/24/explore-kubernetes-cluster-installed-by-kubeadm/">Kubernetes cluster</a>，这显然是不能满足安全要求的。为此，Kubernetes更推荐大家使用<a href="https://kubernetes.io/docs/user-guide/service-accounts/">service account</a>这种方案的。本文就带大家详细说说如何通过service account从一个Pod中访问API Server的。</p>
<h3>零、试验环境</h3>
<p>本文的试验环境是Kubernetes 1.3.7 cluster，双节点，master承载负荷。cluster通过<a href="https://kubernetes.io/docs/getting-started-guides/ubuntu/manual/">kube-up.sh</a>搭建的，具体的搭建方法见《<a href="http://tonybai.com/2016/10/18/learn-how-to-install-kubernetes-on-ubuntu/">一篇文章带你了解Kubernetes安装</a>》。</p>
<h3>一、什么是service account？</h3>
<p>什么是service account? 顾名思义，相对于user account（比如：kubectl访问APIServer时用的就是user account），service account就是Pod中的Process用于访问Kubernetes API的account，它为Pod中的Process提供了一种身份标识。相比于user account的全局性权限，service account更适合一些轻量级的task，更聚焦于授权给某些特定Pod中的Process所使用。</p>
<p>service account作为一种resource存在于Kubernetes cluster中，我们可以通过kubectl获取当前cluster中的service acount列表：</p>
<pre><code># kubectl get serviceaccount --all-namespaces
NAMESPACE                    NAME           SECRETS   AGE
default                      default        1         140d
kube-system                  default        1         140d
</code></pre>
<p>我们查看一下kube-system namespace下名为”default”的service account的详细信息：</p>
<pre><code># kubectl describe serviceaccount/default -n kube-system
Name:        default
Namespace:    kube-system
Labels:        &lt;none&gt;

Image pull secrets:    &lt;none&gt;

Mountable secrets:     default-token-hpni0

Tokens:                default-token-hpni0
</code></pre>
<p>我们看到service account并不复杂，只是关联了一个secret资源作为token，该token也叫service-account-token，该token才是真正在API Server验证(authentication)环节起作用的：</p>
<pre><code># kubectl get secret  -n kube-system
NAME                  TYPE                                  DATA      AGE
default-token-hpni0   kubernetes.io/service-account-token   3         140d

# kubectl get secret default-token-hpni0 -o yaml -n kube-system
apiVersion: v1
data:
  ca.crt: {base64 encoding of ca.crt data}
  namespace: a3ViZS1zeXN0ZW0=
  token: {base64 encoding of bearer token}

kind: Secret
metadata:
  annotations:
    kubernetes.io/service-account.name: default
    kubernetes.io/service-account.uid: 90ded7ff-9120-11e6-a0a6-00163e1625a9
  creationTimestamp: 2016-10-13T08:39:33Z
  name: default-token-hpni0
  namespace: kube-system
  resourceVersion: "2864"
  selfLink: /api/v1/namespaces/kube-system/secrets/default-token-hpni0
  uid: 90e71909-9120-11e6-a0a6-00163e1625a9
type: kubernetes.io/service-account-token

</code></pre>
<p>我们看到这个类型为service-account-token的secret资源包含的数据有三部分：ca.crt、namespace和token。</p>
<ul>
<li>
<p>ca.crt<br />
这个是API Server的<a href="http://tonybai.com/2015/04/30/go-and-https/">CA公钥证书</a>，用于Pod中的Process对API Server的服务端数字证书进行校验时使用的；</p>
</li>
<li>
<p>namespace<br />
这个就是Secret所在namespace的值的base64编码：# echo -n “kube-system”|base64  => “a3ViZS1zeXN0ZW0=”</p>
</li>
<li>
<p>token</p>
</li>
</ul>
<p>这是一段用API Server私钥签发(sign)的bearer tokens的base64编码，在API Server authenticating环节，它将派上用场。</p>
<h3>二、API Server的service account authentication(身份验证)</h3>
<p>前面说过，service account为Pod中的Process提供了一种身份标识，在Kubernetes的身份校验(authenticating)环节，以某个service account提供身份的Pod的用户名为：</p>
<pre><code>system:serviceaccount:(NAMESPACE):(SERVICEACCOUNT)
</code></pre>
<p>以上面那个kube-system namespace下的“default” service account为例，使用它的Pod的username全称为：</p>
<pre><code>system:serviceaccount:kube-system:default
</code></pre>
<p>有了username，那么credentials呢？就是上面提到的service-account-token中的token。在《<a href="http://tonybai.com/2016/11/25/the-security-settings-for-kubernetes-cluster/">Kubernetes集群的安全配置</a>》一文中我们谈到过，API Server的authenticating环节支持多种身份校验方式：client cert、bearer token、static password auth等，这些方式中有一种方式通过authenticating（Kubernetes API Server会逐个方式尝试），那么身份校验就会通过。一旦API Server发现client发起的request使用的是service account token的方式，API Server就会自动采用signed bearer token方式进行身份校验。而request就会使用携带的service account token参与验证。该token是API Server在创建service account时用API server启动参数：&#8211;service-account-key-file的值签署(sign)生成的。如果&#8211;service-account-key-file未传入任何值，那么将默认使用&#8211;tls-private-key-file的值，即API Server的私钥（server.key）。</p>
<p>通过authenticating后，API Server将根据Pod username所在的group：system:serviceaccounts和system:serviceaccounts:(NAMESPACE)的权限对其进行<a href="https://kubernetes.io/docs/admin/authorization/">authority</a> 和<a href="https://kubernetes.io/docs/admin/admission-controllers/">admission control</a>两个环节的处理。在这两个环节中，cluster管理员可以对service account的权限进行细化设置。</p>
<h3>三、默认的service account</h3>
<p>Kubernetes会为每个cluster中的namespace自动创建一个默认的service account资源，并命名为”default”：</p>
<pre><code># kubectl get serviceaccount --all-namespaces
NAMESPACE                    NAME           SECRETS   AGE
default                      default        1         140d
kube-system                  default        1         140d
</code></pre>
<p>如果Pod中没有显式指定spec.serviceAccount字段值，那么Kubernetes会将该namespace下的”default” service account自动mount到在这个namespace中创建的Pod里。我们以namespace “default”为例，我们查看一下其中的一个Pod的信息：</p>
<pre><code># kubectl describe pod/index-api-2822468404-4oofr
Name:        index-api-2822468404-4oofr
Namespace:    default
... ...

Containers:
  index-api:
   ... ...
    Volume Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-40z0x (ro)
    Environment Variables:    &lt;none&gt;
... ...
Volumes:
... ...
  default-token-40z0x:
    Type:    Secret (a volume populated by a Secret)
    SecretName:    default-token-40z0x

QoS Class:    BestEffort
Tolerations:    &lt;none&gt;
No events.
</code></pre>
<p>可以看到，kubernetes将default namespace中的service account “default”的service account token挂载(mount)到了Pod中容器的/var/run/secrets/kubernetes.io/serviceaccount路径下。</p>
<p>深入容器内部，查看mount的serviceaccount路径下的结构：</p>
<pre><code># docker exec 3d11ee06e0f8 ls  /var/run/secrets/kubernetes.io/serviceaccount
ca.crt
namespace
token
</code></pre>
<p>这三个文件与上面提到的service account的token中的数据是一一对应的。</p>
<h3>四、default service account doesn&#8217;t work</h3>
<p>上面提到过，每个Pod都会被自动挂载一个其所在namespace的default service account，该service account用于该Pod中的Process访问API Server时使用。Pod中的Process该怎么用这个service account呢？Kubernetes官方提供了一个<a href="https://github.com/kubernetes/client-go">client-go</a>项目可以为你演示如何使用service account访问API Server。这里我们就基于client-go项目中的examples/in-cluster/main.go来测试一下是否能成功访问API Server。</p>
<p>先下载client-go源码：</p>
<pre><code># go get k8s.io/client-go

# ls -F
CHANGELOG.md  dynamic/   Godeps/     INSTALL.md   LICENSE   OWNERS  plugin/    rest/     third_party/  transport/  vendor/
discovery/    examples/  informers/  kubernetes/  listers/  pkg/    README.md  testing/  tools/        util/
</code></pre>
<p>我们改造一下examples/in-cluster/main.go，考虑到panic会导致不便于观察Pod日志，我们将panic改为输出到“标准输出”，并且不return，让Pod周期性的输出相关日志，即便fail：</p>
<pre><code>// k8s.io/client-go/examples/in-cluster/main.go
... ...
func main() {
    // creates the in-cluster config
    config, err := rest.InClusterConfig()
    if err != nil {
        fmt.Println(err)
    }
    // creates the clientset
    clientset, err := kubernetes.NewForConfig(config)
    if err != nil {
        fmt.Println(err)
    }
    for {
        pods, err := clientset.CoreV1().Pods("").List(metav1.ListOptions{})
        if err != nil {
            fmt.Println(err)
        } else {
            fmt.Printf("There are %d pods in the cluster\n", len(pods.Items))
        }
        time.Sleep(10 * time.Second)
    }
}

</code></pre>
<p>基于该main.go的go build默认输出，创建一个简单的Dockerfile：</p>
<pre><code>From ubuntu:14.04
MAINTAINER Tony Bai &lt;bigwhite.cn@gmail.com&gt;

COPY main /root/main
RUN chmod +x /root/main
WORKDIR /root
ENTRYPOINT ["/root/main"]

</code></pre>
<p>构建一个测试用docker image：</p>
<pre><code># docker build -t k8s/example1:latest .
... ...

# docker images|grep k8s
k8s/example1                                                  latest              ceb3efdb2f91        14 hours ago        264.4 MB

</code></pre>
<p>创建一份deployment manifest：</p>
<pre><code>//main.yaml

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: k8s-example1
spec:
  replicas: 1
  template:
    metadata:
      labels:
        run: k8s-example1
    spec:
      containers:
      - name: k8s-example1
        image: k8s/example1:latest
        imagePullPolicy: IfNotPresent
</code></pre>
<p>我们来创建该deployment（kubectl create -f main.yaml -n kube-system），观察Pod中的main程序能否成功访问到API Server：</p>
<pre><code># kubectl logs k8s-example1-1569038391-jfxhx
the server has asked for the client to provide credentials (get pods)
the server has asked for the client to provide credentials (get pods)

API Server log(/var/log/upstart/kube-apiserver.log):

E0302 15:45:40.944496   12902 handlers.go:54] Unable to authenticate the request due to an error: crypto/rsa: verification error
E0302 15:45:50.946598   12902 handlers.go:54] Unable to authenticate the request due to an error: crypto/rsa: verification error
E0302 15:46:00.948398   12902 handlers.go:54] Unable to authenticate the request due to an error: crypto/rsa: verification error
</code></pre>
<p>出错了！kube-system namespace下的”default” service account似乎不好用啊！（注意：这是在kubernetes 1.3.7环境）。</p>
<h3>五、创建一个新的自用的service account</h3>
<p>在kubernetes github issues中，有好多issue是关于”default” service account不好用的问题，给出的解决方法似乎都是创建一个新的service account。</p>
<p>service account的创建非常简单，我们创建一个serviceaccount.yaml：</p>
<pre><code>//serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: k8s-example1
</code></pre>
<p>创建该service account：</p>
<pre><code># kubectl create -f serviceaccount.yaml
serviceaccount "k8s-example1" created

# kubectl get serviceaccount
NAME           SECRETS   AGE
default        1         139d
k8s-example1   1         12s
</code></pre>
<p>修改main.yaml，让Pod显示使用这个新的service account：</p>
<pre><code>//main.yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: k8s-example1
spec:
  replicas: 1
  template:
    metadata:
      labels:
        run: k8s-example1
    spec:
      serviceAccount: k8s-example1
      containers:
      - name: k8s-example1
        image: k8s/example1:latest
        imagePullPolicy: IfNotPresent
</code></pre>
<p>好了，我们重新创建该deployment，查看Pod日志：</p>
<pre><code># kubectl logs k8s-example1-456041623-rqj87
There are 14 pods in the cluster
There are 14 pods in the cluster
... ...
</code></pre>
<p>我们看到main程序使用新的service account成功通过了API Server的身份验证环节，并获得了cluster的相关信息。</p>
<h3>六、尾声</h3>
<p>在我的另外一个<a href="http://tonybai.com/2016/12/30/install-kubernetes-on-ubuntu-with-kubeadm/">使用kubeadm安装的k8s 1.5.1环境</a>中，我重复做了上面这个简单测试，不同的是这次我直接使用了default service account。在<a href="http://tonybai.com/2017/01/24/explore-kubernetes-cluster-installed-by-kubeadm/">k8s 1.5.1</a>下，pod的执行结果是ok的，也就是说通过default serviceaccount，我们的client-go in-cluster example程序可以顺利通过API Server的身份验证，获取到相关的Pods元信息。</p>
<h3>七、参考资料</h3>
<ul>
<li><a href="https://kubernetes.io/docs/admin/authentication">Kubernetes authentication</a></li>
<li><a href="https://kubernetes.io/docs/user-guide/service-accounts/">Service Accounts</a></li>
<li><a href="https://kubernetes.io/docs/user-guide/accessing-the-cluster/#accessing-the-api-from-a-pod">Accessing the cluster</a></li>
<li><a href="https://kubernetes.io/docs/admin/service-accounts-admin/">Service Accounts Admin</a></li>
</ul>
<hr />
<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/03/03/access-api-server-from-a-pod-through-serviceaccount/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>理解Docker容器网络之Linux Network Namespace</title>
		<link>https://tonybai.com/2017/01/11/understanding-linux-network-namespace-for-docker-network/</link>
		<comments>https://tonybai.com/2017/01/11/understanding-linux-network-namespace-for-docker-network/#comments</comments>
		<pubDate>Wed, 11 Jan 2017 14:30:55 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[brctl]]></category>
		<category><![CDATA[bridge]]></category>
		<category><![CDATA[cni]]></category>
		<category><![CDATA[CNM]]></category>
		<category><![CDATA[curl]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[docker-proxy]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[http]]></category>
		<category><![CDATA[iproute2]]></category>
		<category><![CDATA[iptables]]></category>
		<category><![CDATA[k8s]]></category>
		<category><![CDATA[Kernel]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[Namespace]]></category>
		<category><![CDATA[nginx]]></category>
		<category><![CDATA[ping]]></category>
		<category><![CDATA[traceroute]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[veth]]></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=2118</guid>
		<description><![CDATA[由于2016年年中调换工作的原因，对容器网络的研究中断过一段时间。随着当前项目对Kubernetes应用的深入，我感觉之前对于容器网络的粗浅理解已经不够了，容器网络成了摆在前面的“一道坎”。继续深入理解K8s网络、容器网络已经势在必行。而这篇文章就算是一个重新开始，也是对之前浅表理解的一个补充。 我还是先从Docker容器网络入手，虽然Docker与Kubernetes采用了不同的网络模型：K8s是Container Network Interface, CNI模型，而Docker则采用的是Container Network Model, CNM模型。而要了解Docker容器网络，理解Linux Network Namespace是不可或缺的。在本文中我们将尝试理解Linux Network Namespace及相关Linux内核网络设备的概念，并手工模拟Docker容器网络模型的部分实现，包括单机容器网络中的容器与主机连通、容器间连通以及端口映射等。 一、Docker的CNM网络模型 Docker通过libnetwork实现了CNM网络模型。libnetwork设计doc中对CNM模型的简单诠释如下： CNM模型有三个组件： Sandbox(沙盒)：每个沙盒包含一个容器网络栈(network stack)的配置，配置包括：容器的网口、路由表和DNS设置等。 Endpoint(端点)：通过Endpoint，沙盒可以被加入到一个Network里。 Network(网络)：一组能相互直接通信的Endpoints。 光看这些，我们还很难将之与现实中的Docker容器联系起来，毕竟是抽象的模型不对应到实体，总有种漂浮的赶脚。文档中又给出了CNM模型在Linux上的参考实现技术，比如：沙盒的实现可以是一个Linux Network Namespace；Endpoint可以是一对VETH；Network则可以用Linux Bridge或Vxlan实现。 这些实现技术反倒是比较接地气。之前我们在使用Docker容器时，了解过Docker是用linux network namespace实现的容器网络隔离的。使用docker时，在物理主机或虚拟机上会有一个docker0的linux bridge，brctl show时能看到 docker0上“插上了”好多veth网络设备： # ip link show ... ... 3: docker0: &#60;BROADCAST,MULTICAST,UP,LOWER_UP&#62; mtu 1500 qdisc noqueue state UP mode DEFAULT group default link/ether 02:42:30:11:98:ef brd ff:ff:ff:ff:ff:ff 19: veth4559467@if18: &#60;BROADCAST,MULTICAST,UP,LOWER_UP&#62; [...]]]></description>
			<content:encoded><![CDATA[<p>由于2016年年中<a href="http://tonybai.com/2017/01/03/2016-summary/">调换工作</a>的原因，对容器网络的研究中断过一段时间。随着当前项目对<a href="http://tonybai.com/tag/kubernetes">Kubernetes</a>应用的深入，我感觉之前对于<a href="http://tonybai.com/2016/01/15/understanding-container-networking-on-single-host/">容器网络的粗浅理解</a>已经不够了，容器网络成了摆在前面的“一道坎”。继续深入理解K8s网络、容器网络已经势在必行。而这篇文章就算是一个重新开始，也是对之前浅表理解的一个补充。</p>
<p>我还是先从<a href="https://www.docker.com/">Docker</a>容器网络入手，虽然Docker与Kubernetes采用了不同的网络模型：K8s是<a href="https://github.com/containernetworking/cni">Container Network Interface, CNI</a>模型，而Docker则采用的是<a href="https://github.com/docker/libnetwork/blob/master/docs/design.md">Container Network Model, CNM</a>模型。而要了解Docker容器网络，理解Linux Network Namespace是不可或缺的。在本文中我们将尝试理解Linux Network Namespace及相关Linux内核网络设备的概念，并手工模拟Docker容器网络模型的部分实现，包括<a href="http://tonybai.com/2016/01/15/understanding-container-networking-on-single-host/">单机容器网络</a>中的容器与主机连通、容器间连通以及<a href="http://tonybai.com/2016/01/18/understanding-binding-docker-container-ports-to-host/">端口映射</a>等。</p>
<h3>一、Docker的CNM网络模型</h3>
<p>Docker通过<a href="https://github.com/docker/libnetwork">libnetwork</a>实现了CNM网络模型。libnetwork设计doc中对CNM模型的简单诠释如下：</p>
<p><img src="http://tonybai.com/wp-content/uploads/docker-cnm-model.jpg" alt="img{512x368}" /></p>
<p>CNM模型有三个组件：</p>
<ul>
<li>Sandbox(沙盒)：每个沙盒包含一个容器网络栈(network stack)的配置，配置包括：容器的网口、路由表和DNS设置等。</li>
<li>Endpoint(端点)：通过Endpoint，沙盒可以被加入到一个Network里。</li>
<li>Network(网络)：一组能相互直接通信的Endpoints。</li>
</ul>
<p>光看这些，我们还很难将之与现实中的Docker容器联系起来，毕竟是抽象的模型不对应到实体，总有种漂浮的赶脚。文档中又给出了CNM模型在Linux上的参考实现技术，比如：沙盒的实现可以是一个<a href="https://en.wikipedia.org/wiki/Linux_namespaces#Network_.28net.29">Linux Network Namespace</a>；Endpoint可以是一对<a href="https://openvz.org/Virtual_Ethernet_device">VETH</a>；Network则可以用<a href="https://wiki.linuxfoundation.org/networking/bridge">Linux Bridge</a>或<a href="https://en.wikipedia.org/wiki/Virtual_Extensible_LAN">Vxlan</a>实现。</p>
<p>这些实现技术反倒是比较接地气。之前我们在使用Docker容器时，了解过Docker是用linux network namespace实现的容器网络隔离的。使用docker时，在物理主机或虚拟机上会有一个docker0的linux bridge，brctl show时能看到 docker0上“插上了”好多veth网络设备：</p>
<pre><code># ip link show
... ...
3: docker0: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc noqueue state UP mode DEFAULT group default
    link/ether 02:42:30:11:98:ef brd ff:ff:ff:ff:ff:ff
19: veth4559467@if18: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc noqueue master docker0 state UP mode DEFAULT group default
    link/ether a6:14:99:52:78:35 brd ff:ff:ff:ff:ff:ff link-netnsid 3
... ...

$ brctl show
bridge name    bridge id        STP enabled    interfaces
... ...
docker0        8000.0242301198ef    no        veth4559467
</code></pre>
<p>模型与现实终于有点接驳了！下面我们将进一步深入对这些术语概念的理解。</p>
<h3>二、Linux Bridge、VETH和Network Namespace</h3>
<p><a href="https://wiki.linuxfoundation.org/networking/bridge">Linux Bridge</a>，即Linux网桥设备，是Linux提供的一种虚拟网络设备之一。其工作方式非常类似于物理的网络交换机设备。Linux Bridge可以工作在二层，也可以工作在三层，默认工作在二层。工作在二层时，可以在同一网络的不同主机间转发以太网报文；一旦你给一个Linux Bridge分配了IP地址，也就开启了该Bridge的三层工作模式。在Linux下，你可以用<a href="https://wiki.linuxfoundation.org/networking/iproute2">iproute2</a>工具包或brctl命令对Linux bridge进行管理。</p>
<p>VETH(Virtual Ethernet )是Linux提供的另外一种特殊的网络设备，中文称为虚拟网卡接口。它总是成对出现，要创建就创建一个pair。一个Pair中的veth就像一个网络线缆的两个端点，数据从一个端点进入，必然从另外一个端点流出。每个veth都可以被赋予IP地址，并参与三层网络路由过程。</p>
<p>关于Linux Bridge和VETH的具体工作原理，可以参考IBM developerWorks上的这篇文章《<a href="http://www.ibm.com/developerworks/cn/linux/1310_xiawc_networkdevice/">Linux 上的基础网络设备详解</a>》。</p>
<p>Network namespace，网络名字空间，允许你在Linux创建相互隔离的网络视图，每个网络名字空间都有独立的网络配置，比如：网络设备、路由表等。新建的网络名字空间与主机默认网络名字空间之间是隔离的。我们平时默认操作的是主机的默认网络名字空间。</p>
<p>概念总是抽象的，接下来我们将在一个模拟Docker容器网络的例子中看到这些Linux网络概念和网络设备到底是起到什么作用的以及是如何操作的。</p>
<h3>三、用Network namespace模拟Docker容器网络</h3>
<p>为了进一步了解network namespace、bridge和veth在docker容器网络中的角色和作用，我们来做一个demo：用network namespace模拟Docker容器网络，实际上Docker容器网络在linux上也是基于network namespace实现的，我们只是将其“自动化”的创建过程做成了“分解动作”，便于大家理解。</p>
<h4>1、环境</h4>
<p>我们在一台物理机上进行这个Demo实验。物理机安装了Ubuntu 16.04.1，内核版本：4.4.0-57-generic。Docker容器版本：</p>
<pre><code>Client:
 Version:      1.12.1
 API version:  1.24
 Go version:   go1.6.3
 Git commit:   23cf638
 Built:        Thu Aug 18 05:33:38 2016
 OS/Arch:      linux/amd64

Server:
 Version:      1.12.1
 API version:  1.24
 Go version:   go1.6.3
 Git commit:   23cf638
 Built:        Thu Aug 18 05:33:38 2016
 OS/Arch:      linux/amd64
</code></pre>
<p>另外，环境中需安装了<a href="https://wiki.linuxfoundation.org/networking/iproute2">iproute2</a>和brctl工具。</p>
<h4>2、拓扑</h4>
<p>我们来模拟一个拥有两个容器的容器桥接网络：</p>
<p><img src="http://tonybai.com/wp-content/uploads/linux-network-namespaces-1.png" alt="img{512x368}" /></p>
<p>对应的用手工搭建的模拟版本拓扑如下(由于在同一台主机，模拟版本采用172.16.0.0/16网段)：</p>
<p><img src="http://tonybai.com/wp-content/uploads/linux-network-namespaces-2.png" alt="img{512x368}" /></p>
<h4>3、创建步骤</h4>
<h5>a) 创建Container_ns1和Container_ns2 network namespace</h5>
<p>默认情况下，我们在Host上看到的都是default network namespace的视图。为了模拟容器网络，我们新建两个network namespace：</p>
<pre><code>sudo ip netns add Container_ns1
sudo ip netns add Container_ns2

$ sudo ip netns list
Container_ns2
Container_ns1
</code></pre>
<p>创建的ns也可以在/var/run/netns路径下看到：</p>
<pre><code>$ sudo ls /var/run/netns
Container_ns1  Container_ns2
</code></pre>
<p>我们探索一下新创建的ns的网络空间(通过ip netns exec命令可以在特定ns的内部执行相关程序，这个exec命令是至关重要的，后续还会发挥更大作用)：</p>
<pre><code>$ sudo ip netns exec Container_ns1 ip a
1: lo: &lt;LOOPBACK&gt; mtu 65536 qdisc noop state DOWN group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

$ sudo ip netns exec Container_ns2 ip a
1: lo: &lt;LOOPBACK&gt; mtu 65536 qdisc noop state DOWN group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

$ sudo ip netns exec Container_ns2 ip route

</code></pre>
<p>可以看到，新建的ns的网络设备只有一个loopback口，并且路由表为空。</p>
<h5>b) 创建MyDocker0 bridge</h5>
<p>我们在default network namespace下创建MyDocker0 linux bridge：</p>
<pre><code>$ sudo brctl addbr MyDocker0

$ brctl show
bridge name    bridge id        STP enabled    interfaces
MyDocker0        8000.000000000000    no
</code></pre>
<p>给MyDocker0分配ip地址并生效该设备，开启三层，为后续充当Gateway做准备：</p>
<pre><code>$ sudo ip addr add 172.16.1.254/16 dev MyDocker0
$ sudo ip link set dev MyDocker0 up
</code></pre>
<p>启用后，我们发现default network namespace的路由配置中增加了一条路由：</p>
<pre><code>$ route -n
内核 IP 路由表
目标            网关            子网掩码        标志  跃点   引用  使用 接口
0.0.0.0         10.11.36.1      0.0.0.0         UG    100    0        0 eno1
... ...
172.16.0.0      0.0.0.0         255.255.0.0     U     0      0        0 MyDocker0
... ...
</code></pre>
<h5>c) 创建VETH，连接两对network namespaces</h5>
<p>到目前为止，default ns与Container_ns1、Container_ns2之间还没有任何瓜葛。接下来就是见证奇迹的时刻了。我们通过veth pair建立起多个ns之间的联系：</p>
<p>创建连接default ns与Container_ns1之间的veth pair &#8211; veth1和veth1p：</p>
<pre><code>$sudo ip link add veth1 type veth peer name veth1p

$sudo ip -d link show
... ...
21: veth1p@veth1: &lt;BROADCAST,MULTICAST,M-DOWN&gt; mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 66:6d:e7:75:3f:43 brd ff:ff:ff:ff:ff:ff promiscuity 0
    veth addrgenmode eui64
22: veth1@veth1p: &lt;BROADCAST,MULTICAST,M-DOWN&gt; mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 56:cd:bb:f2:10:3f brd ff:ff:ff:ff:ff:ff promiscuity 0
    veth addrgenmode eui64
... ...
</code></pre>
<p>将veth1“插到”MyDocker0这个bridge上：</p>
<pre><code>$ sudo brctl addif MyDocker0 veth1
$ sudo ip link set veth1 up
$ brctl show
bridge name    bridge id        STP enabled    interfaces
MyDocker0        8000.56cdbbf2103f    no        veth1
</code></pre>
<p>将veth1p“放入”Container_ns1中：</p>
<pre><code>$ sudo ip link set veth1p netns Container_ns1

$ sudo ip netns exec Container_ns1 ip a
1: lo: &lt;LOOPBACK&gt; mtu 65536 qdisc noop state DOWN group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
21: veth1p@if22: &lt;BROADCAST,MULTICAST&gt; mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 66:6d:e7:75:3f:43 brd ff:ff:ff:ff:ff:ff link-netnsid 0
</code></pre>
<p>这时，你在default ns中将看不到veth1p这个虚拟网络设备了。按照上面拓扑，位于Container_ns1中的veth应该更名为eth0：</p>
<pre><code>$ sudo ip netns exec Container_ns1 ip link set veth1p name eth0
$ sudo ip netns exec Container_ns1 ip a
1: lo: &lt;LOOPBACK&gt; mtu 65536 qdisc noop state DOWN group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
21: eth0@if22: &lt;BROADCAST,MULTICAST&gt; mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 66:6d:e7:75:3f:43 brd ff:ff:ff:ff:ff:ff link-netnsid 0
</code></pre>
<p>将Container_ns1中的eth0生效并配置IP地址：</p>
<pre><code>$ sudo ip netns exec Container_ns1 ip link set eth0 up
$ sudo ip netns exec Container_ns1 ip addr add 172.16.1.1/16 dev eth0
</code></pre>
<p>赋予IP地址后，自动生成一条直连路由：</p>
<pre><code>sudo ip netns exec Container_ns1 ip route
172.16.0.0/16 dev eth0  proto kernel  scope link  src 172.16.1.1
</code></pre>
<p>现在在Container_ns1下可以ping通MyDocker0了，但由于没有其他路由，包括默认路由，ping其他地址还是不通的（比如：docker0的地址：172.17.0.1）：</p>
<pre><code>$ sudo ip netns exec Container_ns1 ping -c 3 172.16.1.254
PING 172.16.1.254 (172.16.1.254) 56(84) bytes of data.
64 bytes from 172.16.1.254: icmp_seq=1 ttl=64 time=0.074 ms
64 bytes from 172.16.1.254: icmp_seq=2 ttl=64 time=0.064 ms
64 bytes from 172.16.1.254: icmp_seq=3 ttl=64 time=0.068 ms

--- 172.16.1.254 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1998ms
rtt min/avg/max/mdev = 0.064/0.068/0.074/0.010 ms

$ sudo ip netns exec Container_ns1 ping -c 3 172.17.0.1
connect: Network is unreachable

</code></pre>
<p>我们再给Container_ns1添加一条默认路由，让其能ping通物理主机上的其他网络设备或其他ns空间中的网络设备地址：</p>
<pre><code>$ sudo ip netns exec Container_ns1 ip route add default via 172.16.1.254
$ sudo ip netns exec Container_ns1 ip route
default via 172.16.1.254 dev eth0
172.16.0.0/16 dev eth0  proto kernel  scope link  src 172.16.1.1

$ sudo ip netns exec Container_ns1 ping -c 3 172.17.0.1
PING 172.17.0.1 (172.17.0.1) 56(84) bytes of data.
64 bytes from 172.17.0.1: icmp_seq=1 ttl=64 time=0.068 ms
64 bytes from 172.17.0.1: icmp_seq=2 ttl=64 time=0.076 ms
64 bytes from 172.17.0.1: icmp_seq=3 ttl=64 time=0.069 ms

--- 172.17.0.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1999ms
rtt min/avg/max/mdev = 0.068/0.071/0.076/0.003 ms

</code></pre>
<p>不过这时候，如果想在Container_ns1中ping通物理主机之外的地址，比如:google.com，那还是不通的。为什么呢？因为ping的icmp的包的源地址没有做snat（docker是通过设置<a href="https://www.netfilter.org/">iptables</a>规则实现的），导致出去的以172.16.1.1为源地址的包“有去无回”了^0^。</p>
<p>接下来，我们按照上述步骤，再创建连接default ns与Container_ns2之间的veth pair &#8211; veth2和veth2p，由于步骤相同，这里就不列出那么多信息了，只列出关键操作：</p>
<pre><code>$ sudo ip link add veth2 type veth peer name veth2p
$ sudo brctl addif MyDocker0 veth2
$ sudo ip link set veth2 up
$ sudo ip link set veth2p netns Container_ns2
$ sudo ip netns exec Container_ns2 ip link set veth2p name eth0
$ sudo ip netns exec Container_ns2 ip link set eth0 up
$ sudo ip netns exec Container_ns2 ip addr add 172.16.1.2/16 dev eth0
$ sudo ip netns exec Container_ns2 ip route add default via 172.16.1.254
</code></pre>
<p>至此，模拟创建告一段落！两个ns之间以及它们与default ns之间连通了！</p>
<pre><code>$ sudo ip netns exec Container_ns2 ping -c 3 172.16.1.1
PING 172.16.1.1 (172.16.1.1) 56(84) bytes of data.
64 bytes from 172.16.1.1: icmp_seq=1 ttl=64 time=0.101 ms
64 bytes from 172.16.1.1: icmp_seq=2 ttl=64 time=0.083 ms
64 bytes from 172.16.1.1: icmp_seq=3 ttl=64 time=0.087 ms

--- 172.16.1.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1998ms
rtt min/avg/max/mdev = 0.083/0.090/0.101/0.010 ms

$ sudo ip netns exec Container_ns1 ping -c 3 172.16.1.2
PING 172.16.1.2 (172.16.1.2) 56(84) bytes of data.
64 bytes from 172.16.1.2: icmp_seq=1 ttl=64 time=0.053 ms
64 bytes from 172.16.1.2: icmp_seq=2 ttl=64 time=0.092 ms
64 bytes from 172.16.1.2: icmp_seq=3 ttl=64 time=0.089 ms

--- 172.16.1.2 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1999ms
rtt min/avg/max/mdev = 0.053/0.078/0.092/0.017 ms
</code></pre>
<p>当然此时两个ns之间连通，主要还是通过直连网络，实质上是MyDocker0在二层起到的作用。以在Container_ns1中ping Container_ns2的eth0地址为例：</p>
<p>Container_ns1此时的路由表：</p>
<pre><code>$ sudo ip netns exec Container_ns1 ip route
default via 172.16.1.254 dev eth0
172.16.0.0/16 dev eth0  proto kernel  scope link  src 172.16.1.1
</code></pre>
<p>ping 172.16.1.2执行后，根据路由表，将首先匹配到直连网络（第二条），即无需gateway转发便可以直接将数据包送达。arp查询后（要么从arp cache中找到，要么在MyDocker0这个二层交换机中泛洪查询）获得172.16.1.2的mac地址。ip包的目的ip填写172.16.1.2，二层数据帧封包将目的mac填写为刚刚查到的mac地址，通过eth0(172.16.1.1)发送出去。eth0实际上是一个veth pair，另外一端“插”在MyDocker0这个交换机上，因此这一过程就是一个标准的二层交换机的数据报文交换过程, MyDocker0相当于从交换机上的一个端口收到以太帧数据，并将数据从另外一个端口发出去。ping应答包亦如此。</p>
<p>而如果是在Container_ns1中ping某个docker container的地址，比如172.17.0.2。当ping执行后，根据Container_ns1下的路由表，没有匹配到直连网络，只能通过default路由将数据包发给Gateway: 172.16.1.254。虽然都是MyDocker0接收数据，但这次更类似于“数据被直接发到 Bridge 上，而不是Bridge从一个端口接收(这块儿与我之前的文章中的理解稍有差异)”。二层的目的mac地址填写的是gateway 172.16.1.254自己的mac地址（Bridge的mac地址），此时的MyDocker0更像是一块普通网卡的角色，工作在三层。MyDocker0收到数据包后，发现并非是发给自己的ip包，通过主机路由表找到直连链路路由，MyDocker0将数据包Forward到docker0上（封装的二层数据包的目的MAC地址为docker0的mac地址）。此时的docker0也是一种“网卡”的角色，由于目的ip依然不是docker0自身，因此docker0也会继续这一转发流程。通过traceroute可以印证这一过程：</p>
<pre><code>$ sudo ip netns exec Container_ns1  traceroute 172.17.0.2
traceroute to 172.17.0.2 (172.17.0.2), 30 hops max, 60 byte packets
 1  172.16.1.254 (172.16.1.254)  0.082 ms  0.023 ms  0.019 ms
 2  172.17.0.2 (172.17.0.2)  0.054 ms  0.034 ms  0.029 ms

$ sudo ip netns exec Container_ns1  ping -c 3 172.17.0.2
PING 172.17.0.2 (172.17.0.2) 56(84) bytes of data.
64 bytes from 172.17.0.2: icmp_seq=1 ttl=63 time=0.084 ms
64 bytes from 172.17.0.2: icmp_seq=2 ttl=63 time=0.101 ms
64 bytes from 172.17.0.2: icmp_seq=3 ttl=63 time=0.098 ms

--- 172.17.0.2 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1998ms
rtt min/avg/max/mdev = 0.084/0.094/0.101/0.010 ms
</code></pre>
<p>现在，你应该大致了解docker engine在创建单机容器网络时都在背后做了哪些手脚了吧（当然，这里只是简单模拟，docker实际做的要比这复杂许多）。</p>
<h3>四、基于userland proxy的容器端口映射的模拟</h3>
<p><a href="http://tonybai.com/2016/01/18/understanding-binding-docker-container-ports-to-host/">端口映射</a>让位于容器中的service可以将服务范围扩展到主机之外，比如：一个运行于container中的<a href="http://tonybai.com/2016/11/22/deploy-nginx-service-for-the-services-in-kubernetes-cluster/">nginx</a>可以通过宿主机的9091端口对外提供http server服务：</p>
<pre><code>$ sudo docker run -d -p 9091:80 nginx:latest
8eef60e3d7b48140c20b11424ee8931be25bc47b5233aa42550efabd5730ac2f

$ curl 10.11.36.15:9091
&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
&lt;title&gt;Welcome to nginx!&lt;/title&gt;
&lt;style&gt;
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;h1&gt;Welcome to nginx!&lt;/h1&gt;
&lt;p&gt;If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;Thank you for using nginx.&lt;/em&gt;&lt;/p&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>容器的端口映射实际是通过docker engine的docker proxy功能实现的。默认情况下，docker engine(截至docker 1.12.1版本)采用userland proxy(&#8211;userland-proxy=true)为每个expose端口的容器启动一个proxy实例来做端口流量转发：</p>
<pre><code>$ ps -ef|grep docker-proxy
root     26246  6228  0 16:18 ?        00:00:00 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 9091 -container-ip 172.17.0.2 -container-port 80
</code></pre>
<p>docker-proxy实际上就是在default ns和container ns之间转发流量而已。我们完全可以模拟这一过程。</p>
<p>我们创建一个fileserver demo：</p>
<pre><code>//testfileserver.go
package main

import "net/http"

func main() {
    http.ListenAndServe(":8080", http.FileServer(http.Dir(".")))
}
</code></pre>
<p>我们在Container_ns1下启动这个Fileserver service:</p>
<pre><code>$ sudo ip netns exec Container_ns1 ./testfileserver

$ sudo ip netns exec Container_ns1 lsof -i tcp:8080
COMMAND    PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
testfiles 3605 root    3u  IPv4 297022      0t0  TCP *:http-alt (LISTEN)
</code></pre>
<p>可以看到在Container_ns1下面，8080已经被testfileserver监听，不过在default ns下，8080端口依旧是avaiable的。</p>
<p>接下来，我们在default ns下创建一个简易的proxy：</p>
<pre><code>//proxy.go
... ...

var (
    host          string
    port          string
    container     string
    containerport string
)

func main() {
    flag.StringVar(&amp;host, "host", "0.0.0.0", "host addr")
    flag.StringVar(&amp;port, "port", "", "host port")
    flag.StringVar(&amp;container, "container", "", "container addr")
    flag.StringVar(&amp;containerport, "containerport", "8080", "container port")

    flag.Parse()

    fmt.Printf("%s\n%s\n%s\n%s", host, port, container, containerport)

    ln, err := net.Listen("tcp", host+":"+port)
    if err != nil {
        // handle error
        log.Println("listen error:", err)
        return
    }
    log.Println("listen ok")

    for {
        conn, err := ln.Accept()
        if err != nil {
            // handle error
            log.Println("accept error:", err)
            continue
        }
        log.Println("accept conn", conn)
        go handleConnection(conn)
    }
}

func handleConnection(conn net.Conn) {
    cli, err := net.Dial("tcp", container+":"+containerport)
    if err != nil {
        log.Println("dial error:", err)
        return
    }
    log.Println("dial ", container+":"+containerport, " ok")

    go io.Copy(conn, cli)
    _, err = io.Copy(cli, conn)
    fmt.Println("communication over: error:", err)
}

</code></pre>
<p>在default ns下执行：</p>
<pre><code>./proxy -host 0.0.0.0 -port 9090 -container 172.16.1.1 -containerport 8080
0.0.0.0
9090
172.16.1.1
80802017/01/11 17:26:10 listen ok
</code></pre>
<p>我们http get一下宿主机的9090端口：</p>
<pre><code>$curl 10.11.36.15:9090
&lt;pre&gt;
&lt;a href="proxy"&gt;proxy&lt;/a&gt;
&lt;a href="proxy.go"&gt;proxy.go&lt;/a&gt;
&lt;a href="testfileserver"&gt;testfileserver&lt;/a&gt;
&lt;a href="testfileserver.go"&gt;testfileserver.go&lt;/a&gt;
&lt;/pre&gt;

</code></pre>
<p>成功获得file list！</p>
<p>proxy的输出日志：</p>
<pre><code>2017/01/11 17:26:16 accept conn &amp;{{0xc4200560e0}}
2017/01/11 17:26:16 dial  172.16.1.1:8080  ok
communication over: error:&lt;nil&gt;
</code></pre>
<p>由于每个做端口映射的Container都要启动至少一个docker proxy与之配合，一旦运行的container增多，那么docker proxy对资源的消耗将是大大的。因此docker engine在docker 1.6之后（好像是这个版本）提供了基于iptables的端口映射机制，无需再启动docker proxy process了。我们只需修改一下docker engine的启动配置即可：</p>
<p>在使用systemd init system的系统中如果为docker engine配置&#8211;userland-proxy=false，可以参考《<a href="http://tonybai.com/2016/12/27/when-docker-meets-systemd">当Docker遇到systemd</a>》这篇文章。</p>
<p>由于这个与network namespace关系不大，后续单独理解^0^。</p>
<h3>六、参考资料</h3>
<p>1、《<a href="https://book.douban.com/subject/26929989/">Docker networking cookbook</a>》<br />
2、《<a href="https://book.douban.com/subject/26631435/">Docker cookbook</a>》</p>
<p style='text-align:left'>&copy; 2017, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2017/01/11/understanding-linux-network-namespace-for-docker-network/feed/</wfw:commentRss>
		<slash:comments>7</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>也谈C语言标识符的NAMESPACE</title>
		<link>https://tonybai.com/2008/05/15/also-talk-about-namespace-in-c/</link>
		<comments>https://tonybai.com/2008/05/15/also-talk-about-namespace-in-c/#comments</comments>
		<pubDate>Thu, 15 May 2008 14:02:56 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[GCC]]></category>
		<category><![CDATA[Grammar]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Namespace]]></category>
		<category><![CDATA[Opensource]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[内存对齐]]></category>
		<category><![CDATA[博客]]></category>
		<category><![CDATA[命名空间]]></category>
		<category><![CDATA[程序员]]></category>
		<category><![CDATA[语法]]></category>

		<guid isPermaLink="false">http://tonybai.com/2008/05/15/%e4%b9%9f%e8%b0%88c%e8%af%ad%e8%a8%80%e6%a0%87%e8%af%86%e7%ac%a6%e7%9a%84namespace/</guid>
		<description><![CDATA[P.J Plauger的&#34;The Standard C Library&#34;一书的Chapter0的章后练习中有这样的一道题：编写一个包含如下一行语句的正确的程序：<br />
x:&#160;&#160;&#160;&#160;&#160; ((struct x*)x)-&#62;x=x(5); <br />
并描述这行语句中x的5种截然不同的use，这里其实涉及到这么一个知识或者说概念：C语言的命名空间(namespace)，在&#34;C语言参考手册&#34;中还被称作: overloading class。]]></description>
			<content:encoded><![CDATA[<p>P.J Plauger的&quot;The Standard C Library&quot;一书的Chapter0的章后练习中有这样的一道题：编写一个包含如下一行语句的正确的程序：<br />x:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ((struct x*)x)-&gt;x=x(5); <br />并描述这行语句中x的5种截然不同的use，这里其实涉及到这么一个知识或者说概念：C语言的命名空间(namespace)，在&quot;C语言参考手册&quot;中还被称作: overloading class。</p>
<p>这里namespace，并非C++中的那个keyword &quot;namespace&quot;，这里的namespace更多是编译器为了识别不同范围下的标识符而进行的划分，而不是提供给应用程序员的类似c++中的那个namespace facility。再次注意：C的namespace不是一个关键字。</p>
<p>简单分析一下这行语句：x:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ((struct x*)x)-&gt;x=x(5); <br />这里有5个x，第一印象：这样的语句能编译过去么？那既然P.J Plauger提出了这样的问题，那么自然有solution。<br />从左到右顺序：<br />第一个x &#8212; 毋庸置疑，这是一个标号(label) ；<br />第二个x &#8212; 这里的x显然是一个struct tag(结构体标志)；<br />第三个x &#8212; 这里的x 无法确定其具体身份，可能是一指针类型，也可能就是一个整型；<br />第四个x &#8212; x前面有-&gt;，显然这个x是某结构体的一个成员变量；<br />第五个x &#8212; x(5)让人&quot;浮想联翩&quot;，第一印象是函数调用，细致一想还可能是一个宏哦(你肯定会说不可能，呵呵，别着急，慢慢来)</p>
<p>到底如何增加一些语法元素能让这一行能顺利通过编译，并执行后得到合理结果呢？我们不妨先来温习一下C标准中对C的&quot;命名空间&quot;的诠释。 </p>
<p>在&quot;C语言参考手册&quot;中有如此说明，标准C将其Namespace分成了五种，分别是：<br />1) 预处理器宏名<br />2) 语句标号<br />3) 结构、枚举、联合结构的标志<br />4) 成员名<br />5) 其他名称 包括变量名、函数名、typedef名称和枚举常量</p>
<p>有了以上的说明，我们有了第一种方案：<br />上面说了，语句x:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ((struct x*)x)-&gt;x=x(5)中有三个x都是可以确定的，不确定的是第三个x和最后一个x。我们先考虑让最后一个x为一个函数。</p>
<p>考虑到最后一个名称空间的说明，一旦最后一个x为函数的话，第三个x就不能为变量名、typedef名称和枚举常量了。如果x是对象宏(不带参数的宏)，显然也不合理；那么我们先将x实现为函数看看：<br />struct x { //for the 2nd x<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; int x;&nbsp; //for the 4th x<br />};</p>
<p>int x(int a) { //for the 3rd and 5th x<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return a;<br />}</p>
<p>int main() {<br />x:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ((struct x*)x)-&gt;x=x(5); <br />}<br />这个在gcc(sunos or mingw on windows下)下编译能顺利通过。但是执行一下编译出的程序，会出现致命错误。初略分析一下也不奇怪。函数x的地址是在代码段，那块内存区域是只读且受保护的，尝试强制赋值显然os是不允许的。</p>
<p>第一种方案虽然能通过编译，但是执行结果不合理。我们来做第二种尝试：试着将最后一个x实现为一个函数宏(带参数的宏)。<br />struct x { //for the 2nd x<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; int x;&nbsp; //for the 4th x<br />};</p>
<p>struct x ax;</p>
<p>#define x(a)&nbsp; (a);</p>
<p>int main() {<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; int x = (int)(&#038;ax);<br />x:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ((struct x*)x)-&gt;x=x(5); &nbsp;<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; printf(&quot;%d\n&quot;, ((struct x*)x)-&gt;x); //output: 5<br />}<br />这回，我们得到了正确的且合理的solution了。在P.J Plauger的&quot;The Standard C Library&quot;一书中还有一张关于C语言命名空间的图，记起来更形象。</p>
<p style='text-align:left'>&copy; 2008, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2008/05/15/also-talk-about-namespace-in-c/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
	</channel>
</rss>
