<?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; consul</title>
	<atom:link href="http://tonybai.com/tag/consul/feed/" rel="self" type="application/rss+xml" />
	<link>https://tonybai.com</link>
	<description>一个程序员的心路历程</description>
	<lastBuildDate>Wed, 08 Apr 2026 00:17:11 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.2.1</generator>
		<item>
		<title>不止是云原生：为什么 Go 的热度在持续上升？来自社区的真实声音</title>
		<link>https://tonybai.com/2025/07/23/go-surge-in-popularity/</link>
		<comments>https://tonybai.com/2025/07/23/go-surge-in-popularity/#comments</comments>
		<pubDate>Tue, 22 Jul 2025 23:05:49 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[consul]]></category>
		<category><![CDATA[Copilot]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[etcd]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[helm]]></category>
		<category><![CDATA[hypervisor]]></category>
		<category><![CDATA[IaC]]></category>
		<category><![CDATA[infra]]></category>
		<category><![CDATA[k8s]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[podman]]></category>
		<category><![CDATA[reddit]]></category>
		<category><![CDATA[terraform]]></category>
		<category><![CDATA[基础设施]]></category>
		<category><![CDATA[工程体验]]></category>
		<category><![CDATA[性能]]></category>
		<category><![CDATA[泛型]]></category>
		<category><![CDATA[简单]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4934</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/07/23/go-surge-in-popularity 大家好，我是Tony Bai。 最近，在国外的 Go 语言社区（Reddit r/golang），有用户提出了一个我们许多人可能都想过的问题：“是只有我一个人觉得，还是 Go 近年来的人气确实在上升？” 这个问题迅速引爆了社区，收到了近百条来自全球一线开发者的回复。答案是响亮而一致的：不，不是你一个人。 Go 的崛起，早已超越了其在云原生领域的舒适区，正以一种不可阻挡的势头，渗透到软件工程的各个角落。 这篇文章，不谈空泛的理论，也不做单纯的布道。我想带你一起，潜入这场热烈的社区讨论，去倾听那些最真实、最鲜活的声音，看看开发者们自己，是如何解释 Go 成功的秘诀。 第一支柱：Go，新一代的“基础设施语言” 在所有的讨论中，一个观点被反复提及，并获得了最高的赞誉： “我称 Go 为‘基础设施语言’（the language of infrastructure）。” 这个定义精准地抓住了 Go 的灵魂。当我们审视当今软件世界的基石时，会发现一个惊人的事实：那些支撑着我们数字世界的骨架，几乎都是用 Go 构建的。社区用户随手就列出了一份星光熠熠的名单： Docker &#38; Kubernetes Podman &#38; Helm Etcd、Consul &#38; Terraform ……等等等等 这些工具定义了容器化、编排和基础设施即代码（IaC）的现代范式。而一个更具冲击力的例子来自一位正在构建 Hypervisor 平台的开发者，他分享道： “我们的核心分布式系统是用纯 Go 编写的，总共只用了 4 个 外部依赖。其余的一切，都来自 Go 的标准库和 FreeBSD。是的，你没看错，我没有打错字。” 仅凭标准库就能构建如此复杂的底层系统，这强有力地证明了 Go 语言的强大、自足与工程上的优越性。它不是玩具，而是真正能用来打造重型装备的工业级工具。 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/go-surge-in-popularity-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/07/23/go-surge-in-popularity">本文永久链接</a> &#8211; https://tonybai.com/2025/07/23/go-surge-in-popularity</p>
<p>大家好，我是Tony Bai。</p>
<p>最近，在国外的 Go 语言社区（Reddit r/golang），有用户提出了一个我们许多人可能都想过的问题：“是只有我一个人觉得，还是 Go 近年来的人气确实在上升？”</p>
<p>这个问题迅速引爆了社区，收到了近百条来自全球一线开发者的回复。答案是响亮而一致的：<strong>不，不是你一个人。</strong> Go 的崛起，早已超越了其在云原生领域的舒适区，正以一种不可阻挡的势头，渗透到软件工程的各个角落。</p>
<p>这篇文章，不谈空泛的理论，也不做单纯的布道。我想带你一起，潜入这场热烈的社区讨论，去倾听那些最真实、最鲜活的声音，看看开发者们自己，是如何解释 Go 成功的秘诀。</p>
<h2>第一支柱：Go，新一代的“基础设施语言”</h2>
<p>在所有的讨论中，一个观点被反复提及，并获得了最高的赞誉：</p>
<blockquote>
<p><strong>“我称 Go 为‘基础设施语言’（the language of infrastructure）。”</strong></p>
</blockquote>
<p>这个定义精准地抓住了 Go 的灵魂。当我们审视当今软件世界的基石时，会发现一个惊人的事实：那些支撑着我们数字世界的骨架，几乎都是用 Go 构建的。社区用户随手就列出了一份星光熠熠的名单：</p>
<ul>
<li>Docker &amp; Kubernetes</li>
<li>Podman &amp; Helm</li>
<li>Etcd、Consul &amp; Terraform</li>
<li>……等等等等</li>
</ul>
<p>这些工具定义了容器化、编排和基础设施即代码（IaC）的现代范式。而一个更具冲击力的例子来自一位正在构建 Hypervisor 平台的开发者，他分享道：</p>
<blockquote>
<p>“我们的核心分布式系统是用纯 Go 编写的，总共只用了 <strong>4 个</strong> 外部依赖。其余的一切，都来自 Go 的标准库和 FreeBSD。是的，你没看错，我没有打错字。”</p>
</blockquote>
<p>仅凭标准库就能构建如此复杂的底层系统，这强有力地证明了 Go 语言的强大、自足与工程上的优越性。它不是玩具，而是真正能用来打造重型装备的工业级工具。</p>
<h2>第二支柱：简单的“宿命”——生产力的终极来源</h2>
<p>一个极具洞察力的观点在社区中引发了共鸣：</p>
<blockquote>
<p><strong>“Go 的简单性，注定了它会随着时间的推移而越来越受欢迎。”</strong></p>
</blockquote>
<p>这是一个奇妙的悖论。许多开发者初识 Go 时，可能会抱怨它“缺少功能”（比如早年关于泛型的激烈争论）。然而，随着项目的深入，大家逐渐意识到，<strong>简单性，恰恰是 Go 最强大的武器。</strong></p>
<p>因为它带来了：</p>
<ul>
<li><strong>极高的可维护性</strong>：没有复杂的继承链，没有隐晦的语法糖，代码直截了当，易于理解和修改。</li>
<li><strong>惊人的生产力</strong>：当你不再需要为语言的复杂特性而烦恼时，你就能更专注于解决业务问题本身。</li>
<li><strong>极低的上手门槛</strong>：正如一位用户所说，“Go 很容易教给新员工”。在一个需要团队协作的工程世界里，这一点至关重要。</li>
</ul>
<p>另一位开发者补充道：“我讨厌在晦涩的语言废话上浪费时间。我只需要交付高质量、可长期维护的生产级代码。Go 提供了最核心的骨架，这正是我所需要的。”</p>
<h2>第三支柱：出色的性能与工程体验的完美平衡</h2>
<p>如果说简单是 Go 的哲学，那么在性能与体验之间找到那个“甜点”（Sweet Spot），就是它在工程实践中取胜的关键。</p>
<p>社区对此有一个生动的总结：“<strong>我们用 Go 得到了 C 语言 95% 的好处，同时摆脱了它的那些麻烦。</strong>” 评论区里一句饱含情感的“<strong>NO CMAKE!</strong>”足以让无数系统程序员会心一笑。</p>
<p>同时，Go 语言“缓慢改进”（slowly improving）的策略也被认为是优点。对于生产环境而言，这意味着更少的破坏性变更和更稳定的生态系统。</p>
<p>在与另一门备受推崇的系统语言 Rust 的对比中，社区的看法也相当务实：“我们用 Rust 来做更接近底层硬件（close to the metal）的工作，用 Go 来做更高层次的事情。” 两者各有所长，Go 在应用层和中间件层提供了无与伦比的开发效率。</p>
<h2>一个现代化的加分项：与 AI 工具的奇妙协同</h2>
<p>在 AI 赋能开发的今天，Go 的简单性再次展现出意想不到的优势。社区里关于 Go 与 AI Code Assistants（如 Copilot）的讨论，揭示了一个新的增长点。</p>
<ul>
<li><strong>一方面，AI 更“喜欢”Go。</strong> 因为 Go 语言相对年轻，其在网络上的训练数据中，“历史垃圾代码”（比如陈旧的 WordPress/PHP 样例）较少。其简洁、统一的语法也让 AI 更容易学习和生成高质量的代码。</li>
<li><strong>另一方面，开发者更喜欢用 AI 写 Go。</strong> 正如一位用户所说：“因为 Go 代码易于阅读和理解，AI 提出的建议可以在几秒钟内被接受或拒绝。”</li>
</ul>
<p>这种奇妙的协同效应，恰恰体现了 AI 辅助开发的最佳实践：AI 作为一个强大的初稿生成器，而 Go 的简洁性则极大地降低了人类进行代码审查和最终决策的认知负荷。</p>
<h2>小结：一个引人深思的提醒</h2>
<p>在这场热烈的讨论中，那位构建 Hypervisor 的资深开发者，在给一位求学的学生提供职业建议时，留下了一段发人深省的话：</p>
<blockquote>
<p>“我能给你的最大建议是，亲身去经历用你自己的大脑、用你自己的手指去构建一切的痛苦……<strong>不要用 AI，它会在你最需要拓展大脑的时候腐蚀你的大脑。</strong> 深入研究未知问题和构想解决方案的能力，将使你无可替代。”</p>
</blockquote>
<p>这番话并非是要我们全盘否定 AI，而是一个善意的提醒。</p>
<p>Go 的成功，归根结底是其设计哲学——简单、实用、高效——的成功。它让工程师能将精力聚焦于创造性的核心工作上。而 AI，作为这个时代最强大的工具，我们应该如何使用它，才能放大而非削弱我们作为人类工程师的核心价值？</p>
<p>这或许是 Go热度上升后，带给我们的另一个值得深思的问题。</p>
<p>资料链接：https://www.reddit.com/r/golang/comments/1m41dz9/is_it_just_me_or_has_golang_been_surging_in/</p>
<hr />
<p>你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？</p>
<ul>
<li>想写出更地道、更健壮的Go代码，却总在细节上踩坑？</li>
<li>渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？</li>
<li>想打造生产级的Go服务，却在工程化实践中屡屡受挫？</li>
</ul>
<p>继《<a href="http://gk.link/a/10AVZ">Go语言第一课</a>》后，我的《<a href="http://gk.link/a/12yGY">Go语言进阶课</a>》终于在极客时间与大家见面了！</p>
<p>我的全新极客时间专栏 《<a href="http://gk.link/a/12yGY">Tony Bai·Go语言进阶课</a>》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。</p>
<p>目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /></p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/07/23/go-surge-in-popularity/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>HashiCorp创始人Mitchell Hashimoto 的 Agentic Engineering 实战心法</title>
		<link>https://tonybai.com/2025/07/20/mitchell-hashimoto-agentic-engineering/</link>
		<comments>https://tonybai.com/2025/07/20/mitchell-hashimoto-agentic-engineering/#comments</comments>
		<pubDate>Sun, 20 Jul 2025 11:32:29 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Agent]]></category>
		<category><![CDATA[Agentic]]></category>
		<category><![CDATA[AgenticEngineering]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[consul]]></category>
		<category><![CDATA[Ghostty]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[hashicorp]]></category>
		<category><![CDATA[MitchellHashimoto]]></category>
		<category><![CDATA[terraform]]></category>
		<category><![CDATA[vagrant]]></category>
		<category><![CDATA[Zed]]></category>
		<category><![CDATA[Zig]]></category>
		<category><![CDATA[智能体]]></category>
		<category><![CDATA[架构]]></category>
		<category><![CDATA[注释维护]]></category>
		<category><![CDATA[重构]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4922</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/07/20/mitchell-hashimoto-agentic-engineering 大家好，我是Tony Bai。 在云计算Infra和云原生工程领域，Mitchell Hashimoto 是一个如雷贯耳的名字。作为 HashiCorp 的创始人，他一手打造了 Terraform、Vagrant、Consul 等一系列定义了现代 DevOps 和基础设施即代码（IaC）的工具。如今，这位大师级程序员正在开发他的新项目——一个用小众语言 Zig 编写的高性能终端模拟器 Ghostty。 最令人关注的是，在开发这样一个严肃、底层的系统软件时，Mitchell 正深度使用 AI Agent 来辅助编程。这并非简单的 Web 应用开发，而是对 AI 赋能开发在“硬核”场景下的终极考验。 最近，我有幸读到一篇对 Mitchell 的深度访谈，其中详细阐述了他的 Agentic Engineering 实战心法。这些经验并非空谈理论，而是充满了可以直接应用的、来自一线的真知灼见。今天，我想把这些宝贵的“干货”分享给你。 核心哲学：“我是架构师，AI 是初级工程师” 当被问及如何使用 AI 时，Mitchell 提出的核心理念，足以给当下狂热的“AI 全自动编程”思潮泼上一盆冷水： “我感觉自己更像是软件项目的架构师。我仍然会构思代码的结构、应用的数据流、状态的存放位置等。我将这些指导信息提供给 AI 工具……我发现这能带来最大的成功。” 他从不直接向 AI 抛出一个模糊的问题，比如“修复这个 Bug”。相反，他会在脑中构思好解决方案的“形状”（Shape），然后将 AI 视为一个初级工程师来分配任务。 他用了一个绝妙的比喻：给 AI 派任务，就像带一个初级工程师，你需要提供清晰的范围和明确的“护栏”（guardrails），就像给保龄球道装上保险杠，确保球能击中目标。 这种“人机协作”的模式，并非对 AI 的不信任，而是一种深刻的工程智慧：将开发者的精力从“如何实现”的繁琐细节中解放出来，聚焦于“应该怎样实现”的顶层设计。 AI [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/mitchell-hashimoto-agentic-engineering-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/07/20/mitchell-hashimoto-agentic-engineering">本文永久链接</a> &#8211; https://tonybai.com/2025/07/20/mitchell-hashimoto-agentic-engineering</p>
<p>大家好，我是Tony Bai。</p>
<p>在云计算Infra和云原生工程领域，Mitchell Hashimoto 是一个如雷贯耳的名字。作为 HashiCorp 的创始人，他一手打造了 Terraform、Vagrant、Consul 等一系列定义了现代 DevOps 和基础设施即代码（IaC）的工具。如今，这位大师级程序员正在开发他的新项目——一个用小众语言 Zig 编写的高性能终端模拟器 Ghostty。</p>
<p>最令人关注的是，在开发这样一个严肃、底层的系统软件时，Mitchell 正深度使用 AI Agent 来辅助编程。这并非简单的 Web 应用开发，而是对 AI 赋能开发在“硬核”场景下的终极考验。</p>
<p>最近，我有幸读到一篇<a href="https://www.youtube.com/watch?v=XyQ4ZTS5dGw">对 Mitchell 的深度访谈</a>，其中详细阐述了他的 Agentic Engineering 实战心法。这些经验并非空谈理论，而是充满了可以直接应用的、来自一线的真知灼见。今天，我想把这些宝贵的“干货”分享给你。</p>
<h2>核心哲学：“我是架构师，AI 是初级工程师”</h2>
<p>当被问及如何使用 AI 时，Mitchell 提出的核心理念，足以给当下狂热的“AI 全自动编程”思潮泼上一盆冷水：</p>
<blockquote>
<p>“我感觉自己更像是软件项目的架构师。我仍然会构思代码的结构、应用的数据流、状态的存放位置等。我将这些指导信息提供给 AI 工具……我发现这能带来最大的成功。”</p>
</blockquote>
<p>他从不直接向 AI 抛出一个模糊的问题，比如“修复这个 Bug”。相反，他会在脑中构思好解决方案的“形状”（Shape），然后将 AI 视为一个<strong>初级工程师</strong>来分配任务。</p>
<p>他用了一个绝妙的比喻：<strong>给 AI 派任务，就像带一个初级工程师，你需要提供清晰的范围和明确的“护栏”（guardrails），就像给保龄球道装上保险杠，确保球能击中目标。</strong></p>
<p>这种“人机协作”的模式，并非对 AI 的不信任，而是一种深刻的工程智慧：将开发者的精力从“如何实现”的繁琐细节中解放出来，聚焦于“应该怎样实现”的顶层设计。</p>
<h2>AI 的“甜点”与“禁区”：知其长，避其短</h2>
<p>要成为 AI 的“架构师”，首先要清晰地认知 AI 这个“初级工程师”的能力边界。Mitchell 在访谈中分享了他眼中 AI 的“甜点区”与“禁区”。</p>
<h3>AI 的“甜点”（可以大胆授权）</h3>
<ol>
<li>
<p><strong>代码重构</strong>：提炼函数、重命名、调整代码结构等机械性工作。Mitchell 的评价是：“我几乎不用给任何修改意见，它总是做得很完美。”</p>
</li>
<li>
<p><strong>UI 复刻</strong>：这是一个杀手级应用。他曾直接给 AI 一张 Zed 编辑器命令面板的截图，让它用 Swift UI 复刻出来。Ghostty 的这个功能，其视图部分 90% 以上都是 AI 直接从截图生成的。</p>
</li>
<li>
<p><strong>注释维护（一个反直觉的惊喜）</strong>：在传统观念里，“好的代码应自解释，无需过多注释”。但 Mitchell 的做法恰恰相反，他推崇<strong>重度注释</strong>：“我做每件事都做两遍：一次用代码，一次用英语。如果注释和代码不匹配，那说明有一方是错的。” 在 AI 时代，这种看似“冗余”的习惯发挥出了惊人的价值：</p>
<ul>
<li><strong>提供上下文</strong>：丰富的注释是 AI Agent 理解代码意图的最佳养料。</li>
<li><strong>成为“校验和”</strong>：AI 能通过对比代码和注释的不一致，发现潜在的 bug 或过时的文档。</li>
<li><strong>跨文件洞察</strong>：最令人惊叹的是，AI 能在一个文件的修改后，发现另一个完全不相干的文件里，有一行相关的注释变得不准确了——这是人类代码审查时极易忽略的盲点。</li>
</ul>
<p>在 Mitchell 的工作流中，注释不再仅仅是文档，它升级成为了<strong>人与 AI 高效协作的“接口协议”</strong>。</p>
</li>
</ol>
<h3>AI 的“禁区”（需要人工接管）</h3>
<ol>
<li><strong>高层架构设计</strong>：AI 无法进行有远见的顶层设计。</li>
<li><strong>复杂的、定制化的高性能数据结构</strong>：AI 不理解性能约束。Mitchell 举了 Ghostty 的例子，为了极致的性能和缓存亲和性，他们设计了基于虚拟内存页和 16 位偏移指针的复杂数据结构。“没有任何一个 LLM 能理解这里面发生了什么”。</li>
<li><strong>小众语言（如 Zig）的熟练编写</strong>：由于训练数据不足，AI 编写 Zig 代码时举步维艰。他的变通方法是：让 AI 用它擅长的语言（如 C 或 Rust）生成逻辑，然后自己手动移植到 Zig。</li>
</ol>
<h2>Mitchell 的实战工作流：一套大师级的“组合拳”</h2>
<p>除了哲学思想，Mitchell 还分享了一系列具体、可操作的战术，堪称一套大师级的“组合拳”。</p>
<ul>
<li>
<p><strong>并行竞赛</strong>：为同一个任务，在多个代码库副本上（ghosty, ghosty2, ghosty3&#8230;）同时运行不同的 AI 模型（Claude, Gemini 等），然后选择做得最好的那个。他开玩笑说：“你可以让它们‘战斗至死’，这是对机器才能做的事。”</p>
</li>
<li>
<p><strong>“Jiu-Jitsu 快照”</strong>：他使用 Jiu-Jitsu（一个现代化的 Git 替代品）的版本快照功能。当 AI 走错路时，他会直接回滚到上一个状态，然后给出新的、更精确的指令，而不是让 AI “撤销”或“重试”，这样更干净、更可控。</p>
</li>
<li>
<p><strong>人机并行工作</strong>：在 AI “思考”时，他从不干等。他会利用这段时间去做更需要人类智慧的工作，比如对上一个版本进行 QA 测试，或者观看 WWDC 视频学习新技术。这实现了人机效率的最大化。</p>
</li>
<li>
<p><strong>“复制-粘贴式”重构法</strong>：这是一个他坚持了十多年的习惯，在 AI 时代变得尤为强大。重构时，他会先<strong>复制</strong>旧的实现，在新副本上进行修改，让新旧两版代码在项目中<strong>并存</strong>，直到新的版本完全就绪。这样做能为 AI 提供极其清晰的“before”和“after”上下文，让 AI 更准确地理解重构的意图和模式。</p>
</li>
</ul>
<h2>结论：重新定义“高效”，而非放弃思考</h2>
<p>听完 Mitchell 的分享，我最大的感触是：<strong>Agentic Engineering 不是为了“偷懒”，而是为了重新定义“高效”。</strong></p>
<p>它将开发者从繁琐、重复的劳动中解放出来，让我们能将宝贵的精力聚焦于架构设计、性能调优、代码审查这些真正体现工程师价值的创造性工作上。它不是要替代我们，而是要成为放大我们能力的杠杆。</p>
<p>最后，我想用 Mitchell 的一句话来结尾，以此回应那些对 AI 效果感到失望的人：</p>
<blockquote>
<p><strong>“你用过什么新工具是让你立刻就变快的吗？”</strong></p>
</blockquote>
<p>无论是学习一门新语言，还是切换到一个新的版本控制系统，我们总要经历一段学习和适应的阵痛期。AI 也不例外。</p>
<p>我们需要学习的，是如何成为一名优秀的“架构师”，去引导和驾驭我们手下这位不知疲倦、潜力无限的“初级工程师”。这，或许就是 AI 时代对我们所有开发者提出的新要求。</p>
<p>原视频链接：https://www.youtube.com/watch?v=XyQ4ZTS5dGw</p>
<hr />
<p>你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？</p>
<ul>
<li>想写出更地道、更健壮的Go代码，却总在细节上踩坑？</li>
<li>渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？</li>
<li>想打造生产级的Go服务，却在工程化实践中屡屡受挫？</li>
</ul>
<p>继《<a href="http://gk.link/a/10AVZ">Go语言第一课</a>》后，我的《<a href="http://gk.link/a/12yGY">Go语言进阶课</a>》终于在极客时间与大家见面了！</p>
<p>我的全新极客时间专栏 《<a href="http://gk.link/a/12yGY">Tony Bai·Go语言进阶课</a>》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。</p>
<p>目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /></p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/07/20/mitchell-hashimoto-agentic-engineering/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>都2024年了，当初那个“Go，互联网时代的C语言”的预言成真了吗？</title>
		<link>https://tonybai.com/2024/08/17/go-the-c-language-of-the-internet-era-come-true/</link>
		<comments>https://tonybai.com/2024/08/17/go-the-c-language-of-the-internet-era-come-true/#comments</comments>
		<pubDate>Fri, 16 Aug 2024 22:17:02 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[caddy]]></category>
		<category><![CDATA[calico]]></category>
		<category><![CDATA[cilium]]></category>
		<category><![CDATA[cloudnative]]></category>
		<category><![CDATA[cockroachdb]]></category>
		<category><![CDATA[consul]]></category>
		<category><![CDATA[container]]></category>
		<category><![CDATA[containerd]]></category>
		<category><![CDATA[CoreDNS]]></category>
		<category><![CDATA[cortex]]></category>
		<category><![CDATA[dagger]]></category>
		<category><![CDATA[Dgraph]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[dragonfly]]></category>
		<category><![CDATA[etcd]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[gohugo]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[goplus]]></category>
		<category><![CDATA[harbor]]></category>
		<category><![CDATA[InfluxDB]]></category>
		<category><![CDATA[istio]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[juicefs]]></category>
		<category><![CDATA[junodb]]></category>
		<category><![CDATA[k8s]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[linkerd2]]></category>
		<category><![CDATA[longhorn]]></category>
		<category><![CDATA[mattermost]]></category>
		<category><![CDATA[Milvus]]></category>
		<category><![CDATA[minio]]></category>
		<category><![CDATA[nats]]></category>
		<category><![CDATA[nsq]]></category>
		<category><![CDATA[ollama]]></category>
		<category><![CDATA[opentelemetry]]></category>
		<category><![CDATA[opentofu]]></category>
		<category><![CDATA[Otel]]></category>
		<category><![CDATA[podman]]></category>
		<category><![CDATA[prometheus]]></category>
		<category><![CDATA[rook]]></category>
		<category><![CDATA[terraform]]></category>
		<category><![CDATA[TIOBE]]></category>
		<category><![CDATA[traefik]]></category>
		<category><![CDATA[VictoriaMetrics]]></category>
		<category><![CDATA[vitess]]></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=4248</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2024/08/17/go-the-c-language-of-the-internet-era-come-true 本文最初发表于我个人的微信公众号(iamtonybai)，但鉴于图片消息的篇幅受限(&#60;=1000字)，一些内容没能如愿展开，这里在博客上重新发布一下，也顺道丰富一下文章的内容。 2012年，七牛云创始人、goplus语言之父许式伟在一次演讲中给出一个大胆的预言：“Go，互联网时代的C语言”。 十余年过去了，我们不禁要问：当初的那个预言是否已经成真？ 在讨论这个预言之前，我们先来看在同一份演讲稿中，老许给出的另外三个预判： 它们是： Java语言份额继续下滑，最终被C和Go语言超越； C语言将长居编程榜第二的位置，有望在Go取代Java前重获第一的宝座； Go语言最终会取代Java位居编程榜榜首。 编程语言排行榜有很多，我们就以名气最大的TIOBE刚刚发布的2024年8月排行榜为例，看看这些预判是否成真。 很遗憾，一个也没命中。 在这份最新榜单中，C位列第三、Java位列第四，Go位列第九，相对于前两个月的第七还下降了两位。不过不得不说，老许对C语言的预判还是相对准确的。 那这是否意味着老许最初的那个预言也Miss了呢？个人觉得：并没有。因为这要看从哪个角度来审视。 传统观点认为，C语言被视为系统编程语言的杰出代表，因其卓越的底层操作能力和极致性能而广受推崇。它允许开发者直接与硬件交互，提供了高效的资源管理和快速的执行速度。如果从这样的视角去看待那则预言，那显然Go与“互联网时代C语言”这个评价和地位是不相称的。虽然Go最初的定位也是一门系统编程语言。 但如果我们跳出以“低级操作和性能”为中心的比较框架，而是从不同时代软件技术栈的层次与构建来看，Go与C语言的地位又极其的相似。 在互联网时代到来之前，C语言已经是整个软件技术栈的基石：从操作系统内核、设备驱动程序、中间件到应用程序，C语言凭借卓越的性能、无以伦比的生态，在技术栈的各个层次都有着广泛且核心的应用。 当时针指向云原生时代时，Go语言在云原生技术栈的构建中，发挥了与C语言相似的作用： 云原生“操作系统”：Kubernetes； 云原生“驱动程序”：容器运行时（docker、containerd、podman）、网络插件(Calico、cilium、CoreDNS等)、存储插件（Rook、longhorn等）； 云原生“中间件”：数据库(CockroachDB、Vitess、InfluxDB(2.x)、VictoriaMetrics、Dgraph、milvus等)、消息队列(NATS、nsq等)、服务网格(Istio、linkerd2)、API网关/代理(Traefik、emissary等)、镜像仓库/加速器(harbor、Dragonfly)、key-value存储(Etcd、consul、junodb)、安全相关(falco、OPA、vault)、可观测组件(OpenTelemetry、Prometheus、Thanos、Cortex等)、基础设施管理(terraform、dagger)、分布式存储(minio、SeaweedFS、juicefs)、AI大模型运维(ollama)。 应用层：Caddy、gohugo、mattermost等。 我们用一张示意图来横向对比一下： 听我讲到这里，你是不是觉得老许的那个预言好像命中了呢！ 当然，从狭义的角度来看，Go与C还有一些地方是很像的，比如：语法简单、跨平台可移植性好等。并且两者还“沾亲带故”：Unix之父Ken Thompson当年和Dennis Ritchie一起发明了C语言，又和Rob Pike等一起设计了Go语言！ 最后，回顾许式伟2012年的预言，我们不得不惊叹于其洞察力。Go语言确实在很大程度上成为了”互联网时代的C语言”，但不是通过传统的性能优势，而是通过重新构建了云原生技术栈，从这个角度看，Go语言也不失为云原生时代的”系统语言” —— 它不仅能够优雅地处理分布式系统的复杂性，它还使得构建和维护大规模、高可靠性的分布式系统变得更为简单，是云原生时代的思维方式和解决方案的集大成者，某种程度上还可以说定义了云原生时代的软件开发范式。 Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！ 著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。 Gopher Daily(Gopher每日新闻) &#8211; https://gopherdaily.tonybai.com 我的联系方式： 微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 &#8211; https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/go-the-c-language-of-the-internet-era-come-true-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2024/08/17/go-the-c-language-of-the-internet-era-come-true">本文永久链接</a> &#8211; https://tonybai.com/2024/08/17/go-the-c-language-of-the-internet-era-come-true</p>
<p><a href="https://mp.weixin.qq.com/s/GTXSNoPTmJ-mprMKAY8esw">本文最初发表于我个人的微信公众号(iamtonybai)</a>，但鉴于图片消息的篇幅受限(&lt;=1000字)，一些内容没能如愿展开，这里在博客上重新发布一下，也顺道丰富一下文章的内容。</p>
<hr />
<p>2012年，<a href="https://www.qiniu.com/">七牛云</a>创始人、<a href="https://github.com/goplus/gop">goplus语言</a>之父<a href="https://github.com/xushiwei">许式伟</a>在一次演讲中给出一个大胆的预言：“<strong>Go，互联网时代的C语言</strong>”。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-the-c-language-of-the-internet-era-come-true-2.png" alt="" /></p>
<p>十余年过去了，我们不禁要问：当初的那个预言是否已经成真？</p>
<p>在讨论这个预言之前，我们先来看在同一份演讲稿中，老许给出的另外三个预判：</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-the-c-language-of-the-internet-era-come-true-3.png" alt="" /></p>
<p>它们是：</p>
<ul>
<li>Java语言份额继续下滑，最终被C和Go语言超越；</li>
<li>C语言将长居编程榜第二的位置，有望在Go取代Java前重获第一的宝座；</li>
<li>Go语言最终会取代Java位居编程榜榜首。</li>
</ul>
<p>编程语言排行榜有很多，我们就以名气最大的<a href="https://www.tiobe.com/tiobe-index/">TIOBE</a>刚刚发布的2024年8月排行榜为例，看看这些预判是否成真。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-the-c-language-of-the-internet-era-come-true-4.png" alt="" /></p>
<p>很遗憾，<strong>一个也没命中</strong>。</p>
<p>在这份最新榜单中，C位列第三、Java位列第四，Go位列第九，<a href="https://mp.weixin.qq.com/s?__biz=MzIyNzM0MDk0Mg==&amp;mid=2247497403&amp;idx=1&amp;sn=03bc972e38163e1539da765249d46586&amp;chksm=e860115adf17984cfe47f9680d8c0fb6370987ad45415ff2d38233d05fe6b315210ce6ada385#rd">相对于前两个月的第七</a>还下降了两位。不过不得不说，老许对C语言的预判还是相对准确的。</p>
<p>那这是否意味着老许最初的那个预言也Miss了呢？个人觉得：<strong>并没有</strong>。因为这要看从哪个角度来审视。</p>
<p>传统观点认为，C语言被视为系统编程语言的杰出代表，因其卓越的底层操作能力和极致性能而广受推崇。它允许开发者直接与硬件交互，提供了高效的资源管理和快速的执行速度。如果从这样的视角去看待那则预言，那显然Go与“互联网时代C语言”这个评价和地位是不相称的。虽然<a href="https://go.dev/talks/2012/splash.article">Go最初的定位也是一门系统编程语言</a>。</p>
<p>但如果我们跳出以“低级操作和性能”为中心的比较框架，而是<strong>从不同时代软件技术栈的层次与构建来看，Go与C语言的地位又极其的相似</strong>。</p>
<p>在互联网时代到来之前，C语言已经是整个软件技术栈的基石：从操作系统内核、设备驱动程序、中间件到应用程序，C语言凭借卓越的性能、无以伦比的生态，在技术栈的各个层次都有着广泛且核心的应用。</p>
<p>当时针指向云原生时代时，<strong>Go语言在云原生技术栈的构建中，发挥了与C语言相似的作用</strong>：</p>
<ul>
<li>云原生“操作系统”：<a href="https://mp.weixin.qq.com/s/paOduv0t1CtBCUoUBfJ7rQ">Kubernetes</a>；</li>
<li>云原生“驱动程序”：容器运行时（<a href="https://tonybai.com/tag/docker">docker</a>、<a href="https://github.com/containerd/containerd">containerd</a>、<a href="https://github.com/containers/podman">podman</a>）、网络插件(<a href="https://github.com/projectcalico/calico">Calico</a>、<a href="https://github.com/cilium/cilium">cilium</a>、<a href="https://github.com/coredns/coredns">CoreDNS</a>等)、存储插件（<a href="https://github.com/rook/rook">Rook</a>、<a href="https://github.com/longhorn/longhorn?tab=readme-ov-file">longhorn</a>等）；</li>
<li>云原生“中间件”：数据库(<a href="https://github.com/cockroachdb/cockroach">CockroachDB</a>、<a href="https://github.com/vitessio/vitess">Vitess</a>、<a href="https://github.com/influxdata/influxdb/tree/main-2.x">InfluxDB(2.x)</a>、<a href="https://github.com/VictoriaMetrics/VictoriaMetrics">VictoriaMetrics</a>、<a href="https://github.com/dgraph-io/dgraph">Dgraph</a>、<a href="https://github.com/milvus-io/milvus">milvus</a>等)、消息队列(<a href="https://github.com/nats-io/nats-server">NATS</a>、<a href="https://github.com/nsqio/nsq">nsq</a>等)、服务网格(<a href="https://tonybai.com/2018/01/03/an-intro-of-microservices-governance-by-istio">Istio</a>、<a href="https://github.com/linkerd/linkerd2">linkerd2</a>)、API网关/代理(<a href="https://github.com/traefik/traefik">Traefik</a>、<a href="https://github.com/emissary-ingress/emissary">emissary</a>等)、镜像仓库/加速器(<a href="https://tonybai.com/2017/10/23/the-speech-script-practice-on-deploying-a-ha-harbor-cluster-for-osc-shenyang-2017/">harbor</a>、<a href="https://github.com/dragonflyoss/Dragonfly2">Dragonfly</a>)、key-value存储(<a href="https://github.com/etcd-io/etcd">Etcd</a>、<a href="https://github.com/hashicorp/consul">consul</a>、<a href="https://github.com/paypal/junodb">junodb</a>)、安全相关(<a href="https://github.com/falcosecurity/falco">falco</a>、<a href="https://github.com/open-policy-agent/opa">OPA</a>、<a href="https://github.com/hashicorp/vault">vault</a>)、可观测组件(<a href="https://github.com/open-telemetry/community">OpenTelemetry</a>、<a href="https://github.com/prometheus/prometheus">Prometheus</a>、<a href="https://github.com/thanos-io/thanos">Thanos</a>、<a href="https://github.com/cortexproject/cortex">Cortex</a>等)、基础设施管理(<a href="https://github.com/hashicorp/terraform">terraform</a>、<a href="https://github.com/dagger/dagger">dagger</a>)、分布式存储(<a href="https://github.com/minio/">minio</a>、<a href="https://github.com/seaweedfs/seaweedfs">SeaweedFS</a>、<a href="https://github.com/juicedata/juicefs">juicefs</a>)、AI大模型运维(<a href="https://github.com/ollama/ollama">ollama</a>)。</li>
<li>应用层：<a href="https://github.com/caddyserver/caddy">Caddy</a>、<a href="https://github.com/gohugoio/hugo">gohugo</a>、<a href="https://github.com/mattermost/mattermost">mattermost</a>等。</li>
</ul>
<p>我们用一张示意图来横向对比一下：</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-the-c-language-of-the-internet-era-come-true-5.png" alt="" /></p>
<p>听我讲到这里，你是不是觉得老许的那个预言好像命中了呢！</p>
<p>当然，从狭义的角度来看，Go与C还有一些地方是很像的，比如：语法简单、跨平台可移植性好等。并且两者还“沾亲带故”：Unix之父Ken Thompson当年和Dennis Ritchie一起发明了C语言，又和Rob Pike等一起设计了Go语言！</p>
<p>最后，回顾许式伟2012年的预言，我们不得不惊叹于其洞察力。Go语言确实在很大程度上成为了”互联网时代的C语言”，但不是通过传统的性能优势，而是通过<strong>重新构建了云原生技术栈</strong>，从这个角度看，Go语言也不失为云原生时代的”系统语言” —— 它不仅能够优雅地处理分布式系统的复杂性，它还使得构建和维护大规模、高可靠性的分布式系统变得更为简单，是云原生时代的思维方式和解决方案的集大成者，某种程度上还可以说定义了云原生时代的软件开发范式。</p>
<hr />
<p><a href="https://public.zsxq.com/groups/51284458844544">Gopher部落知识星球</a>在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻) &#8211; https://gopherdaily.tonybai.com</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>Gopher Daily归档 &#8211; https://github.com/bigwhite/gopherdaily</li>
<li>Gopher Daily Feed订阅 &#8211; https://gopherdaily.tonybai.com/feed</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2024, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2024/08/17/go-the-c-language-of-the-internet-era-come-true/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>通过实例理解Web应用的机密管理</title>
		<link>https://tonybai.com/2023/11/08/understand-go-web-secret-management-by-example/</link>
		<comments>https://tonybai.com/2023/11/08/understand-go-web-secret-management-by-example/#comments</comments>
		<pubDate>Tue, 07 Nov 2023 21:58:30 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[ACL]]></category>
		<category><![CDATA[AES]]></category>
		<category><![CDATA[API]]></category>
		<category><![CDATA[AWS]]></category>
		<category><![CDATA[Azure]]></category>
		<category><![CDATA[Barrier]]></category>
		<category><![CDATA[CI]]></category>
		<category><![CDATA[consul]]></category>
		<category><![CDATA[credentials]]></category>
		<category><![CDATA[CRUD]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[etcd]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[HA]]></category>
		<category><![CDATA[hashicorp]]></category>
		<category><![CDATA[HSM]]></category>
		<category><![CDATA[http]]></category>
		<category><![CDATA[https]]></category>
		<category><![CDATA[jwt]]></category>
		<category><![CDATA[k8s]]></category>
		<category><![CDATA[KeePass]]></category>
		<category><![CDATA[key]]></category>
		<category><![CDATA[KeyWhiz]]></category>
		<category><![CDATA[KMS]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[nomad]]></category>
		<category><![CDATA[OTP]]></category>
		<category><![CDATA[rotate]]></category>
		<category><![CDATA[RSA]]></category>
		<category><![CDATA[SDK]]></category>
		<category><![CDATA[sealed]]></category>
		<category><![CDATA[secret]]></category>
		<category><![CDATA[secret-management]]></category>
		<category><![CDATA[SSH]]></category>
		<category><![CDATA[terraform]]></category>
		<category><![CDATA[TLS]]></category>
		<category><![CDATA[token]]></category>
		<category><![CDATA[unseal]]></category>
		<category><![CDATA[vault]]></category>
		<category><![CDATA[Web]]></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=4037</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2023/11/08/understand-go-web-secret-management-by-example 如果你是一个Web应用系统的开发人员，你的日常大概率是“乐此不疲”地做着CRUD的活儿，很少接触到安全方面的内容。如果这时有人和你提到“机密(信息)管理(secret management)”，你大概率会说：那是啥？和我有关系吗？ 你只是大多应用系统开发人员中的一个典型代表。现阶段，很多开发人员，尤其是业务应用开发人员在工作中较少甚至没有接触过专门的机密管理系统，在系统设计时也较少考虑到机密管理方面的要求，精力仍主要集中在保证系统功能的正确性、性能等方面。这种对安全的重视程度不够，不了解机密管理的现象较为普遍，下面是一些常见的表现： 明文存储密码、密钥等敏感数据 很多项目依然直接将用户密码、数据库连接密码、第三方服务密钥等信息明文写在代码或配置文件中，存在被攻击者直接获取的风险。 硬编码密钥与密码 重复地在代码中多次硬编码密码、密钥等机密信息，导致不能统一变更及管理。 使用弱密码、未定期更换 使用常见的弱密码，或使用默认或长期不变更的密码，很容易被猜测或破解。 不同环境复用同一密钥 开发、测试、生产环境复用同一密钥，一旦泄露将影响所有环境。 明文传输密码 HTTP传输中明文传递密码，导致可被嗅探截获。 日志中输出明文密码 调试日志中直接输出数据库密码等敏感信息，可能被利用。 缺乏访问控制和审计机制 密钥等资源无访问控制，且操作不被审计，难以追踪。 这些现象的普遍存在表明当前对于机密管理的重要性认知还有待提高，尤其是在当前互联网/移动互联网安全形势日益严峻的情况下，开发人员在系统开发的每个环节都应该意识到机密管理的重要性，并将机密管理纳入开发流程的各个阶段，这可以帮助大家构建出更可靠、安全的系统。 在这篇文章中，我就和大家一起来了解一下什么是机密管理，日常进行Web应用开发过程中该如何集成机密管理来保证机密信息在存储、传输、使用过程中的安全，最后，通过实例的方式来剖析Web应用是如何对一些典型的机密信息进行机密管理的。 1. 认识机密管理 在IT领域，机密管理是一种网络安全最佳实践，用于持续地、自动化地管理和保护数字身份验证凭证(如密码、密钥、API令牌等机密信息)，确保这些机密信息只能被经过授权的实体在严格的访问控制下使用。 机密管理拥有一套自己的核心管理措施，包括： 从代码、配置文件和其他未经受保护的区域中删除明文机密信息，将机密信息与代码/配置隔离存储; 执行最小特权(Least Privilege)原则，即设计访问控制时，用户和程序只会被授予执行其任务所需的最小/最低权限； 执行严格的访问控制(尤其是要对所有非人类凭证的访问请求进行验证)，并对所有访问进行跟踪和全面审计； 定期对机密信息(secrets)和凭证(credentials)进行轮转(rotate)； 自动管理机密信息的全生命周期，例如存储、分发、轮转等，并应用一致的访问策略； &#8230; &#8230; 机密管理涉及要管理的机密信息的类型包括(但不限于)： 用户密码或自动生成的密码 API和其他应用程序的密钥(Key)/凭证（包括容器内的密钥/凭证） SSH密钥 数据库和其他system-to-system的密码 用于安全通信、传输和接收数据的私人证书（TLS、SSL 等） RSA和其他一次性密码设备 综合上面信息，我们看到机密管理不仅有一套严格的管理措施，而且要管理的机密信息的类型也是很多，并且随着软件系统复杂性的增加，云原生应用兴起，需要管理的机密类型和数量激增，不仅包括传统的密码和密钥，还有云平台的访问证书、微服务间的通信令牌等；管理难度也会大大提高。远程访问和云部署使得传统的边界安全防护变得困难。机密信息传输和存储的渠道更多，风险也上升。高速迭代的软件交付流程和自动化部署，也要求机密管理能同步地快速响应和自动化，机密管理面临着越来越大的挑战。面对这些挑战，业界迫切需要引入自动化、智能化和专业化的机密管理系统来应对。 2. 机密管理系统 机密管理系统是一套专业的用于集中存储、管理、控制和监控机密信息的安全解决方案。机密管理系统的发展经历了一个从分散到集中、从静态到动态、从本地到云端、从加密到访问控制、从人工操作到DevOps自动集成的发展历程，这个历程大致可分为如下几个阶段： 文件加密阶段 早期开发人员通过对文档和配置文件进行加密来保护机密信息，代表技术是PGP等加密软件。但很显然，这种方式操作不便，不支持访问控制等高级功能。 自建解决方案阶段 企业开始自研一些机密管理解决方案(包括基于一些像KeePass这样的开源项目)，但功能有限，更多是局限于满足企业自己的需求，很少支持跨平台和集中管理等功能。 开源机密管理项目 随着云计算时代的到来，开源社区推出了支持云和容器的自动化机密管理项目，例如：Vault、Keywhiz等，这些项目的一些公同的功能特性包括：轻量化实现、支持访问控制、提供机密信息版本控制、提供审计功能、提供API便于应用集成、支持与 CI/CD 工具集成、支持Docker、Kubernetes等容器平台等。这一时期的开源机密管理系统大大简化了机密管理流程，为随后的云原生机密管理平台的发展奠定了基础。 注：Keywhiz目前2023年9月宣布不再开发，建议使用Hashicorp [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/understand-go-web-secret-management-by-example-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2023/11/08/understand-go-web-secret-management-by-example">本文永久链接</a> &#8211; https://tonybai.com/2023/11/08/understand-go-web-secret-management-by-example</p>
<p>如果你是一个Web应用系统的开发人员，你的日常大概率是“乐此不疲”地做着<a href="https://www.codecademy.com/article/what-is-crud">CRUD</a>的活儿，很少接触到安全方面的内容。如果这时有人和你提到“机密(信息)管理(secret management)”，你大概率会说：那是啥？和我有关系吗？</p>
<p>你只是大多应用系统开发人员中的一个<strong>典型代表</strong>。现阶段，很多开发人员，尤其是业务应用开发人员在工作中较少甚至没有接触过专门的机密管理系统，在系统设计时也较少考虑到机密管理方面的要求，精力仍主要集中在保证系统功能的正确性、性能等方面。这种对安全的重视程度不够，不了解机密管理的现象较为普遍，下面是一些常见的表现：</p>
<ul>
<li>明文存储密码、密钥等敏感数据</li>
</ul>
<p>很多项目依然直接将用户密码、数据库连接密码、第三方服务密钥等信息明文写在代码或配置文件中，存在被攻击者直接获取的风险。</p>
<ul>
<li>硬编码密钥与密码</li>
</ul>
<p>重复地在代码中多次硬编码密码、密钥等机密信息，导致不能统一变更及管理。</p>
<ul>
<li>使用弱密码、未定期更换</li>
</ul>
<p>使用常见的弱密码，或使用默认或长期不变更的密码，很容易被猜测或破解。</p>
<ul>
<li>不同环境复用同一密钥</li>
</ul>
<p>开发、测试、生产环境复用同一密钥，一旦泄露将影响所有环境。</p>
<ul>
<li>明文传输密码</li>
</ul>
<p>HTTP传输中明文传递密码，导致可被嗅探截获。</p>
<ul>
<li>日志中输出明文密码</li>
</ul>
<p>调试日志中直接输出数据库密码等敏感信息，可能被利用。</p>
<ul>
<li>缺乏访问控制和审计机制</li>
</ul>
<p>密钥等资源无访问控制，且操作不被审计，难以追踪。</p>
<p>这些现象的普遍存在表明当前对于机密管理的重要性认知还有待提高，尤其是在当前互联网/移动互联网安全形势日益严峻的情况下，开发人员在系统开发的每个环节都应该意识到机密管理的重要性，并将机密管理纳入开发流程的各个阶段，这可以帮助大家构建出更可靠、安全的系统。</p>
<p>在这篇文章中，我就和大家一起来了解一下什么是机密管理，日常进行Web应用开发过程中该如何集成机密管理来保证机密信息在存储、传输、使用过程中的安全，最后，通过实例的方式来剖析Web应用是如何对一些典型的机密信息进行机密管理的。</p>
<h2>1. 认识机密管理</h2>
<p>在IT领域，机密管理是一种网络安全最佳实践，用于持续地、自动化地管理和保护数字身份验证凭证(如密码、密钥、API令牌等机密信息)，确保这些机密信息只能被经过授权的实体在严格的访问控制下使用。</p>
<p>机密管理拥有一套自己的核心管理措施，包括：</p>
<ul>
<li>从代码、配置文件和其他未经受保护的区域中删除明文机密信息，将机密信息与代码/配置隔离存储;</li>
<li>执行最小特权(Least Privilege)原则，即设计访问控制时，用户和程序只会被授予执行其任务所需的最小/最低权限；</li>
<li>执行严格的访问控制(尤其是要对所有非人类凭证的访问请求进行验证)，并对所有访问进行跟踪和全面审计；</li>
<li>定期对机密信息(secrets)和凭证(credentials)进行轮转(rotate)；</li>
<li>自动管理机密信息的全生命周期，例如存储、分发、轮转等，并应用一致的访问策略；</li>
<li>&#8230; &#8230;</li>
</ul>
<p>机密管理涉及要管理的机密信息的类型包括(但不限于)：</p>
<ul>
<li>用户密码或自动生成的密码</li>
<li>API和其他应用程序的密钥(Key)/凭证（包括容器内的密钥/凭证）</li>
<li>SSH密钥</li>
<li>数据库和其他system-to-system的密码</li>
<li>用于安全通信、传输和接收数据的私人证书（TLS、SSL 等）</li>
<li>RSA和其他一次性密码设备</li>
</ul>
<p>综合上面信息，我们看到机密管理不仅有一套严格的管理措施，而且要管理的机密信息的类型也是很多，并且随着软件系统复杂性的增加，云原生应用兴起，需要管理的机密类型和数量激增，不仅包括传统的密码和密钥，还有云平台的访问证书、微服务间的通信令牌等；管理难度也会大大提高。远程访问和云部署使得传统的边界安全防护变得困难。机密信息传输和存储的渠道更多，风险也上升。高速迭代的软件交付流程和自动化部署，也要求机密管理能同步地快速响应和自动化，机密管理面临着越来越大的挑战。面对这些挑战，业界迫切需要引入自动化、智能化和专业化的<strong>机密管理系统</strong>来应对。</p>
<h2>2. 机密管理系统</h2>
<p>机密管理系统是一套专业的用于集中存储、管理、控制和监控机密信息的安全解决方案。机密管理系统的发展经历了一个从分散到集中、从静态到动态、从本地到云端、从加密到访问控制、从人工操作到DevOps自动集成的发展历程，这个历程大致可分为如下几个阶段：</p>
<ul>
<li>文件加密阶段</li>
</ul>
<p>早期开发人员通过对文档和配置文件进行加密来保护机密信息，代表技术是PGP等加密软件。但很显然，这种方式操作不便，不支持访问控制等高级功能。</p>
<ul>
<li>自建解决方案阶段</li>
</ul>
<p>企业开始自研一些机密管理解决方案(包括基于一些像<a href="https://keepass.info">KeePass</a>这样的开源项目)，但功能有限，更多是局限于满足企业自己的需求，很少支持跨平台和集中管理等功能。</p>
<ul>
<li>开源机密管理项目</li>
</ul>
<p>随着云计算时代的到来，开源社区推出了支持云和容器的自动化机密管理项目，例如：<a href="https://github.com/hashicorp/vault/">Vault</a>、<a href="https://github.com/square/keywhiz">Keywhiz</a>等，这些项目的一些公同的功能特性包括：轻量化实现、支持访问控制、提供机密信息版本控制、提供审计功能、提供API便于应用集成、支持与 CI/CD 工具集成、支持Docker、Kubernetes等容器平台等。这一时期的开源机密管理系统大大简化了机密管理流程，为随后的云原生机密管理平台的发展奠定了基础。</p>
<blockquote>
<p>注：Keywhiz目前2023年9月宣布不再开发，建议使用Hashicorp Vault。</p>
</blockquote>
<ul>
<li>云原生机密管理平台</li>
</ul>
<p>在开源机密管理项目的基础之上，这些开源项目背后的开发商以及一些专业的公有云提供商开始面向云原生应用和DevOps，以SaaS形式提供专业的机密管理服务和全面的机密管理解决方案，如<a href="https://azure.microsoft.com/en-us/products/key-vault/">Azure Key Vault</a>、<a href="https://cloud.google.com/secret-manager">Google Secret Manager</a>、<a href="https://aws.amazon.com/secrets-manager/">AWS Secrets Manager</a>、HashiCorp Vault等。</p>
<p>我们看到：专业的机密系统发展到今天的水平，其过程不是一蹴而就的。正是基于历史经验的积累和总结，现代机密管理平台才演化出了面向云原生架构、支持DevOps、细粒度访问控制、机密信息的动态化以及生命周期的自动化管理等先进功能特性。</p>
<p>在上面的优秀的云原生机密管理系统中，HashiCorp Vault是唯一开源且可以私有化部署在企业内部的。<a href="https://www.hashicorp.com/blog/vault-announcement">HashiCorp公司于2015年发布并开源了Vault</a>，经过多年发展，Vault已经发展成为一款功能强大的企业级机密管理系统，并被广泛视为云原生领域的首选解决方案。</p>
<p>对于普通Web应用开发者而言，<strong>既要有机密管理的意识，又要有机密管理的实现手段</strong>。HashiCorp Vault的设计目标之一就是将机密管理下沉到平台层面，让应用开发者能够专注于应用程序的开发而无需过多关注机密的管理和保护。</p>
<p>作为Web应用开发者，基于Vault实现Web应用的机密管理是一条非常可行的机密管理方案。通过与Vault的集成，Web应用开发者可以利用Vault提供的丰富功能来处理各种机密管理需求和场景。开发者只需要学习如何使用Vault的API或客户端库与Vault进行交互，就能轻松地访问和管理机密数据，实现机密信息(如数据库凭据、API 密钥等)获取、动态机密信息生成、访问控制、审计和监控等机密管理功能，并且可以减少机密管理的开发和维护的复杂性。</p>
<p>接下来，我就和大家一起简要的了解一下Hashicorp的Vault。</p>
<h2>3. 认识Vault</h2>
<h3>3.1 Vault的架构</h3>
<p>如果对Hashicorp这家公司很熟悉，你肯定知道Hashicorp大部分产品(和开源项目)都是由Go开发的，包括<a href="https://tonybai.com/2018/09/10/setup-service-discovery-and-load-balance-based-on-consul/">consul</a>、<a href="https://tonybai.com/2019/03/30/cluster-management-and-microservice-deployment-and-scheduled-by-nomad/">nomad</a>、<a href="https://github.com/hashicorp/terraform">terraform</a>以及<a href="https://github.com/hashicorp/vagrant">vagrant</a>(<a href="https://discuss.hashicorp.com/t/status-of-vagrant-go/42562">vagrant的新版本将切换到go实现</a>)等。</p>
<p>Vault这款优秀的机密管理软件系统继承了Hashicorp的开发基因，也是由Go语言开发的。从2015年至今，Vault已经演化为一个功能强大，但相对也比较复杂的系统，下面是<a href="https://developer.hashicorp.com/vault/docs/internals/architecture">Hashicorp官方架构文档</a>中的一个关于Vault的high level的结构示意图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/understand-go-web-secret-management-by-example-2.png" alt="" /></p>
<p>从整体架构设计思路来看，vault支持：</p>
<ul>
<li>高可用性</li>
</ul>
<p>Vault的架构设计允许部署多个Vault服务器以实现高可用性和容错性，在<a href="https://developer.hashicorp.com/vault/docs/internals/high-availability">高可用集群部署模式</a>下，多个vault服务器共享存储后端，并且每个vault服务器可能是两个状态：active和standby。任意时刻集群都只有一个实例处于active状态，所有standby实例都处于热备用状态(hot standby)。只有处于active状态的服务器会处理所有请求；standby服务器会将所有请求重定向到活动Vault服务器，这点与consul的设计是一致的。如果active服务器被sealed、发生故障或失去网络连接，则standby Vault服务器中的一个将成为active实例。</p>
<p>这里有人可能会问：如果只有一个active实例，那么在访问量增大的时候，active实例便会成为热点或性能瓶颈！没错，这是vault开源版本的约束。这个约束在vault的企业付费版中被取消，在付费版中，standby服务器可以接收只读请求，所有只读请求会均衡分担到各个standby实例上，如果standby实例收到写请求，它会将写请求转发给active实例处理。</p>
<ul>
<li>封存和解封</li>
</ul>
<p>说高可用性时，我们提到了vault服务器实例的sealed(封存)状态。启动Vault服务器时，它会处于sealed状态。在这种状态下，Vault仅知道访问物理存储的位置和方式，但不知道如何解密存储中数据。在unseal(解封)之前，该vault服务器几乎无法做任何操作。在对处于sealed状态的Vault实例进行任何操作之前，必须对其进行解封(unseal)。</p>
<p>解封操作需要提供<strong>解封密钥(unseal keys)</strong>。有人注意到了，我用了unseal keys，而不是unseal key，因为解封密钥是由一种名为<a href="https://en.wikipedia.org/wiki/Shamir%27s_Secret_Sharing">Shamir&#8217;s Secret Sharing</a>的算法分解保存和汇集生成的。Shamir&#8217;s Secret Sharing（Shamir的机密分享算法）是一种密码学算法，用于将机密数据(在本文中指的就是“unseal key”)分割成多个部分，称为shares。这些share可以被分发给不同的人，如下图所示：</p>
<p><img src="https://tonybai.com/wp-content/uploads/understand-go-web-secret-management-by-example-3.png" alt="" /></p>
<p>而只有当足够数量的share被汇集时，才能恢复出原始的机密数据(unseal key)，并用恢复出的机密数据进行下一步操作(如下图所示，下图来自Hashicorp官方文档)：</p>
<p><img src="https://tonybai.com/wp-content/uploads/understand-go-web-secret-management-by-example-4.png" alt="" /></p>
<p>在这幅图中，当汇集一定个数的unseal keys&#8217;share后，vault就能够重构解封密钥(“unseal key”)，然后用它来解密得到根密钥(root key，也称为master key)，根密钥再被用来解密得到加密密钥(Encryption key)用于保护所有vault的数据，即这个Encryption key就是后续参与机密数据加解密的密钥。</p>
<blockquote>
<p>注：实际生产部署时，究竟要如何对Vault Server进行unseal，<a href="https://developer.hashicorp.com/vault/tutorials/recommended-patterns/pattern-unseal">HashiCorp提供了一些unseal pattern</a>供大家参考。</p>
</blockquote>
<ul>
<li>加密层</li>
</ul>
<p>前面架构图中左侧南北横贯多层的部分是Vault的加密层，被称为<strong>barrier</strong>，负责对Vault数据进行加密和解密，确保数据在存储和传输过程中的机密性和完整性。Vault服务器启动时，会将数据写入存储后端。由于存储后端位于barrier之外，被视为不可信的(与零信任网络理念一致)，因此Vault会在将数据发送到存储后端之前对其进行加密。这种机制确保了即便恶意攻击者试获取了对存储后端的访问权限，其拿到的数据仍然保持加密状态。</p>
<ul>
<li>认证和授权</li>
</ul>
<p>如下图(来自Hashicorp官方文档)，当客户端首次连接到Vault时，需要进行身份验证。Vault提供可配置的认证方法，并在身份验证机制上提供灵活性。操作员可以使用用户名/密码等机制进行身份验证，而应用程序可以使用公钥/私钥或令牌进行身份验证。 Core（核心）负责管理请求的流程，包括流经哪个身份验证方法来确定请求是否有效，并得到关联策略的列表，执行访问控制规则（ACLs），确保审计日志记录，并将请求路由到相应的机密引擎进行处理。</p>
<p><img src="https://tonybai.com/wp-content/uploads/understand-go-web-secret-management-by-example-5.png" alt="" /></p>
<ul>
<li>策略管理</li>
</ul>
<p>策略是一组命名的访问控制规则。Vault内置了一些策略，如”root”策略，允许对所有资源的访问。用户可以创建任意数量的命名策略，并对路径进行细粒度的控制。除非通过策略明确授权，否则不允许进行操作。</p>
<ul>
<li>机密引擎</li>
</ul>
<p>Vault使用机密引擎来生成和管理动态机密数据，如临时凭据、API密钥等。机密引擎的类型可以是静态的，如数据库凭据，也可以是动态的，如 AWS IAM凭据。机密引擎根据配置的规则和策略生成和提供机密数据。</p>
<ul>
<li>审计和日志记录</li>
</ul>
<p>Vault记录请求和响应的审计日志，并有Audit Broker(审计代理)将其分发到配置的审计设备(audit device)。审计日志用于监控和审计对Vault的访问和操作。</p>
<ul>
<li>Expiration Manager（租期管理）</li>
</ul>
<p>Vault由Expiration Mgr管理令牌和机密数据的过期，自动回收已过期的客户端令牌和机密数据。</p>
<ul>
<li>Token Store（令牌存储）</li>
</ul>
<p>Token Store生成和管理客户端令牌，用于进行后续的请求操作。令牌类似于网站登录时发送的 cookie，用于验证客户端的身份和授权。</p>
<p>以上是Vault的主要架构设计思路和各部分的功能范围。Vault的架构保证了安全性、高可用性和可扩展性，使用户能够安全地管理和保护机密信息。</p>
<h3>3.2 Vault的安全模型</h3>
<p>Vault是做机密信息管理的，其自身安全模型是否完善直接关系到应用系统的安全。Vault官方也十分重视这点，在<a href="https://developer.hashicorp.com/vault/docs/internals/security">官方文档中也对其安全模型做了说明</a>，这里梳理一下。</p>
<p>Vault的安全模型旨在提供数据的机密性、完整性、可用性、可追溯性和认证性。以下是Vault安全模型的几个设计要点：</p>
<ul>
<li>通信安全</li>
</ul>
<p>Vault要求客户端与服务器之间的通信通过TLS建立安全通道，以确保通信的机密性和完整性。此外，Vault服务器之间的集群通信也使用相互认证的TLS，以保护数据在传输过程中的机密性和完整性。</p>
<ul>
<li>身份验证和授权</li>
</ul>
<p>前面说架构时提及过：所有客户端请求必须经过适当的身份验证和授权。当客户端首次进行身份验证时，Vault使用认证方法验证客户端的身份，并返回与其关联的ACL策略列表。每个请求都需要提供有效的客户端令牌，Vault根据令牌验证其有效性，并生成基于关联策略的访问控制列表（ACL）。</p>
<ul>
<li>数据安全</li>
</ul>
<p>Vault对于存储在后端的数据，以及在传输过程中的数据，都要求保证安全。Vault使用256位的高级加密标准（AES）密码和96位随机数作为加密密钥，对离开Vault的所有数据进行加密。同时，在解密过程中验证Galios Counter Mode（GCM）的认证标签，以检测任何篡改。</p>
<ul>
<li>内部威胁保护</li>
</ul>
<p>Vault关注内部攻击威胁，即已经获得某种程度Vault访问权限的攻击者企图获取未经授权的机密信息。Vault在客户端进行身份验证时，使用事先配置的关联策略列表来生成客户端令牌，并使用严格的默认拒绝策略来进行访问控制。每个策略指定对Vault中路径的访问级别，最终的访问权限由所有关联策略中最高级别的权限决定。</p>
<ul>
<li>密钥管理</li>
</ul>
<p>Vault使用Shamir&#8217;s Secret Sharing技术来实现密钥的管理和保护unseal key，本质上也是对Root key和Encryption key的保护。只有在提供足够数量的share时，才能恢复unseal密钥，这样可以避免对单个持有者的绝对信任，同时也不需要存储完整的加密密钥。</p>
<p>但需要注意的是，Vault的安全模型并不涵盖所有可能的威胁和攻击，例如对存储后端的完全控制、存储后端中存在的秘密信息的泄露、运行中的Vault实例内存分析等。此外，Vault还依赖于外部系统或服务的安全性，如果这些外部系统存在漏洞或受到攻击，可能会导致Vault中数据的机密性或完整性受到威胁。</p>
<p>说了这么多Vault，Vault究竟长什么样？应该如何用呢？接下来我们简单介绍一下Vault的安装和使用，也是为后续的实例部分做个铺垫。</p>
<h3>3.3 Vault的安装</h3>
<p><a href="https://developer.hashicorp.com/vault/docs/install">Vault支持多种形式的安装部署</a>，包括基于预编译好的二进制文件(precompiled binary)、基于容器或包管理器等，你甚至可以自己基于源码编译。</p>
<p>我这里使用的是Precompiled binary方式，将Vault直接部署在我的开发环境下，一台MacBook Pro上。</p>
<p>Precompiled binary下载后就是一个可执行文件，把它放到特定路径下，并在PATH环境变量中将这个路径加入进来，环境变量生效后，你就可以在任意路径下使用vault命令了。</p>
<p>下面的命令打印了下载的vault的版本：</p>
<pre><code>$vault -v
Vault v1.15.1 (b94e275f25ccd9011146d14c00ea9e49fd5032dc), built 2023-10-20T19:16:11Z
</code></pre>
<p>通过-h命令行参数，可以查看vault的命令帮助信息：</p>
<pre><code>$vault -h
Usage: vault &lt;command&gt; [args]

Common commands:
    read        Read data and retrieves secrets
    write       Write data, configuration, and secrets
    delete      Delete secrets and configuration
    list        List data or secrets
    login       Authenticate locally
    agent       Start a Vault agent
    server      Start a Vault server
    status      Print seal and HA status
    unwrap      Unwrap a wrapped secret

Other commands:
    audit                Interact with audit devices
    auth                 Interact with auth methods
    debug                Runs the debug command
    events
    kv                   Interact with Vault's Key-Value storage
    lease                Interact with leases
    monitor              Stream log messages from a Vault server
    namespace            Interact with namespaces
    operator             Perform operator-specific tasks
    patch                Patch data, configuration, and secrets
    path-help            Retrieve API help for paths
    pki                  Interact with Vault's PKI Secrets Engine
    plugin               Interact with Vault plugins and catalog
    policy               Interact with policies
    print                Prints runtime configurations
    proxy                Start a Vault Proxy
    secrets              Interact with secrets engines
    ssh                  Initiate an SSH session
    token                Interact with tokens
    transform            Interact with Vault's Transform Secrets Engine
    transit              Interact with Vault's Transit Secrets Engine
    version-history      Prints the version history of the target Vault server
</code></pre>
<blockquote>
<p>注：Vault继承了Hashicorp产品的一贯风格，即将所有功能放到一个程序中，各个功能通过subcommand的形式提供，比如vault server、vault agent、vault proxy等。如果你了解consul，你会发现consul就是这样的。</p>
</blockquote>
<h3>3.4 Vault的启动(dev模式)</h3>
<p>生产环境的Vault部署、配置、启动以及unseal过程还是蛮复杂的，<a href="https://developer.hashicorp.com/vault/tutorials/day-one-consul/multi-cluster-architecture">HashiCorp给了一些参考集群架构</a>，这些可以交给运维同学去琢磨。</p>
<p>对于开发人员而言，日常将应用与Vault集成实现机密管理的时候，只需在本机或远程开发机上启动dev模式的Vault实例即可，这里我们也基于dev模式来启动一个单实例的Vault：</p>
<pre><code>$vault server -dev
==&gt; Vault server configuration:

Administrative Namespace:
             Api Address: http://127.0.0.1:8200
                     Cgo: disabled
         Cluster Address: https://127.0.0.1:8201
   Environment Variables: Apple_PubSub_Socket_Render, CLASSPATH, CLISH_PATH, ETCDCTL_API, GITEA_WORK_DIR, GODEBUG, GONOPROXY, GONOSUMDB, GOPATH, GOPRIVATE, GOPROXY, GOROOT, GOSUMDB, HOME, HOMEBREW_BOTTLE_DOMAIN, LANG, LC_CTYPE, LESS, LOGNAME, LSCOLORS, MML_HOME, NVM_BIN, NVM_CD_FLAGS, NVM_DIR, OLDPWD, OPENCV_PATH, PAGER, PATH, PWD, PYTHONPATH, RUSTUP_DIST_SERVER, RUSTUP_UPDATE_ROOT, SHELL, SHLVL, SSH_AUTH_SOCK, TERM, TERM_PROGRAM, TERM_PROGRAM_VERSION, TERM_SESSION_ID, TMPDIR, USER, XPC_FLAGS, XPC_SERVICE_NAME, ZSH, _
              Go Version: go1.21.3
              Listener 1: tcp (addr: "127.0.0.1:8200", cluster address: "127.0.0.1:8201", max_request_duration: "1m30s", max_request_size: "33554432", tls: "disabled")
               Log Level:
                   Mlock: supported: false, enabled: false
           Recovery Mode: false
                 Storage: inmem
                 Version: Vault v1.15.1, built 2023-10-20T19:16:11Z
             Version Sha: b94e275f25ccd9011146d14c00ea9e49fd5032dc

==&gt; Vault server started! Log data will stream in below:

2023-11-06T10:25:37.723+0800 [INFO]  proxy environment: http_proxy="" https_proxy="" no_proxy=""
2023-11-06T10:25:37.727+0800 [INFO]  incrementing seal generation: generation=1
2023-11-06T10:25:37.727+0800 [WARN]  no `api_addr` value specified in config or in VAULT_API_ADDR; falling back to detection if possible, but this value should be manually set
2023-11-06T10:25:37.733+0800 [INFO]  core: Initializing version history cache for core
2023-11-06T10:25:37.734+0800 [INFO]  events: Starting event system
2023-11-06T10:25:37.736+0800 [INFO]  core: security barrier not initialized
2023-11-06T10:25:37.737+0800 [INFO]  core: security barrier initialized: stored=1 shares=1 threshold=1
2023-11-06T10:25:37.744+0800 [INFO]  core: post-unseal setup starting
2023-11-06T10:25:37.758+0800 [INFO]  core: loaded wrapping token key
2023-11-06T10:25:37.758+0800 [INFO]  core: successfully setup plugin runtime catalog
2023-11-06T10:25:37.758+0800 [INFO]  core: successfully setup plugin catalog: plugin-directory=""
2023-11-06T10:25:37.760+0800 [INFO]  core: no mounts; adding default mount table
2023-11-06T10:25:37.765+0800 [INFO]  core: successfully mounted: type=cubbyhole version="v1.15.1+builtin.vault" path=cubbyhole/ namespace="ID: root. Path: "
2023-11-06T10:25:37.774+0800 [INFO]  core: successfully mounted: type=system version="v1.15.1+builtin.vault" path=sys/ namespace="ID: root. Path: "
2023-11-06T10:25:37.777+0800 [INFO]  core: successfully mounted: type=identity version="v1.15.1+builtin.vault" path=identity/ namespace="ID: root. Path: "
2023-11-06T10:25:37.783+0800 [INFO]  core: successfully mounted: type=token version="v1.15.1+builtin.vault" path=token/ namespace="ID: root. Path: "
2023-11-06T10:25:37.785+0800 [INFO]  rollback: Starting the rollback manager with 256 workers
2023-11-06T10:25:37.787+0800 [INFO]  rollback: starting rollback manager
2023-11-06T10:25:37.789+0800 [INFO]  core: restoring leases
2023-11-06T10:25:37.791+0800 [INFO]  identity: entities restored
2023-11-06T10:25:37.791+0800 [INFO]  identity: groups restored
2023-11-06T10:25:37.791+0800 [INFO]  expiration: lease restore complete
2023-11-06T10:25:37.793+0800 [INFO]  core: Recorded vault version: vault version=1.15.1 upgrade time="2023-11-06 02:25:37.793171 +0000 UTC" build date=2023-10-20T19:16:11Z
2023-11-06T22:25:38.367+0800 [INFO]  core: post-unseal setup complete
2023-11-06T22:25:38.368+0800 [INFO]  core: root token generated
2023-11-06T22:25:38.368+0800 [INFO]  core: pre-seal teardown starting
2023-11-06T22:25:38.369+0800 [INFO]  rollback: stopping rollback manager
2023-11-06T22:25:38.369+0800 [INFO]  core: pre-seal teardown complete
2023-11-06T22:25:38.370+0800 [INFO]  core.cluster-listener.tcp: starting listener: listener_address=127.0.0.1:8201
2023-11-06T22:25:38.370+0800 [INFO]  core.cluster-listener: serving cluster requests: cluster_listen_address=127.0.0.1:8201
2023-11-06T22:25:38.371+0800 [INFO]  core: post-unseal setup starting
2023-11-06T22:25:38.371+0800 [INFO]  core: loaded wrapping token key
2023-11-06T22:25:38.371+0800 [INFO]  core: successfully setup plugin runtime catalog
2023-11-06T22:25:38.371+0800 [INFO]  core: successfully setup plugin catalog: plugin-directory=""
2023-11-06T22:25:38.372+0800 [INFO]  core: successfully mounted: type=system version="v1.15.1+builtin.vault" path=sys/ namespace="ID: root. Path: "
2023-11-06T22:25:38.372+0800 [INFO]  core: successfully mounted: type=identity version="v1.15.1+builtin.vault" path=identity/ namespace="ID: root. Path: "
2023-11-06T22:25:38.372+0800 [INFO]  core: successfully mounted: type=cubbyhole version="v1.15.1+builtin.vault" path=cubbyhole/ namespace="ID: root. Path: "
2023-11-06T22:25:38.373+0800 [INFO]  core: successfully mounted: type=token version="v1.15.1+builtin.vault" path=token/ namespace="ID: root. Path: "
2023-11-06T22:25:38.373+0800 [INFO]  rollback: Starting the rollback manager with 256 workers
2023-11-06T22:25:38.373+0800 [INFO]  rollback: starting rollback manager
2023-11-06T22:25:38.374+0800 [INFO]  core: restoring leases
2023-11-06T22:25:38.374+0800 [INFO]  expiration: lease restore complete
2023-11-06T22:25:38.374+0800 [INFO]  identity: entities restored
2023-11-06T22:25:38.374+0800 [INFO]  identity: groups restored
2023-11-06T22:25:38.374+0800 [INFO]  core: post-unseal setup complete
2023-11-06T22:25:38.374+0800 [INFO]  core: vault is unsealed
2023-11-06T22:25:38.386+0800 [INFO]  core: successful mount: namespace="" path=secret/ type=kv version=""
WARNING! dev mode is enabled! In this mode, Vault runs entirely in-memory
and starts unsealed with a single unseal key. The root token is already
authenticated to the CLI, so you can immediately begin using Vault.

You may need to set the following environment variables:

    $ export VAULT_ADDR='http://127.0.0.1:8200'

The unseal key and root token are displayed below in case you want to
seal/unseal the Vault or re-authenticate.

Unseal Key: KiF1ohtchsOjr4IvzHY38/OAPOqS1/rARczTFG6Ull8=
Root Token: hvs.9QOJsa7zlwHO8ieW15CXXoOp

Development mode should NOT be used in production installations!
</code></pre>
<p>我们看到dev模式下，Vault server是自动unseal的，并打印出了Unseal Key和Root Token，而且显式地告诉你：所有机密数据都是存储在内存中的，<strong>不要将这个模式用于生产环境</strong>。</p>
<p>前面说过，vault程序继承了Hashicorp产品的基因，它既可以用来启动server，其自身也是一个命令行程序，我们可以用vault命令查看启动的server的状态：</p>
<pre><code>$vault status
Error checking seal status: Get "https://127.0.0.1:8200/v1/sys/seal-status": http: server gave HTTP response to HTTPS client
</code></pre>
<p>我们看到：获取vault server状态的命令执行失败，因为我们并没有开启vault server的https端口，仅使用了http端口。我们设置一下环境变量后，再执行status命令：</p>
<pre><code>$export VAULT_ADDR='http://127.0.0.1:8200' // 设置vault server addr为http非安全方式
$vault status
Key             Value
---             -----
Seal Type       shamir
Initialized     true
Sealed          false
Total Shares    1
Threshold       1
Version         1.15.1
Build Date      2023-10-20T19:16:11Z
Storage Type    inmem
Cluster Name    vault-cluster-23f54192
Cluster ID      a86c14e2-b88c-5391-e8b4-0b1b9e9a9aaf
HA Enabled      false
</code></pre>
<p>接下来，我们试着向Vault写入一个机密信息。Vault支持多种secret engine，比如：<a href="https://developer.hashicorp.com/vault/tutorials/secrets-management/static-secrets">Key/Value secrets engine</a>、<a href="https://developer.hashicorp.com/vault/tutorials/secrets-management/versioned-kv">Versioned Key/value secrets engine(k/v引擎的v2版本)</a>、<a href="https://developer.hashicorp.com/vault/tutorials/secrets-management/openldap">LDAP secrets engine</a>、<a href="https://developer.hashicorp.com/vault/tutorials/secrets-management/azure-secrets">Azure secrets engine</a>等，其中K/V引擎以及带版本的K/V引擎是最常用的。</p>
<blockquote>
<p>注：Vault还支持开发者<a href="https://developer.hashicorp.com/vault/tutorials/custom-secrets-engine">自定义secret engine</a>。</p>
</blockquote>
<p>我们尝试使用kv子命令向vault中写入一个key/value，放到secret路径下(在dev模式下，secret路径下自动开启v2版本引擎)，key为hello，值为foo=world：</p>
<pre><code>$vault kv put -mount=secret hello foo=world
Error making API request.

URL: GET http://127.0.0.1:8200/v1/sys/internal/ui/mounts/secret
Code: 403. Errors:

* permission denied

</code></pre>
<p>我们看到命令执行失败，提示没有权限。vault server要求每个访问请求都必须带上token，我们可以使用vault server启动时打印的root token，可以使用环境变量的方式将token注入：</p>
<pre><code>export VAULT_TOKEN="hvs.9QOJsa7zlwHO8ieW15CXXoOp"
</code></pre>
<p>也可以执行下面命令并输入root token完成登录：</p>
<pre><code>$vault login
Token (will be hidden):
Success! You are now authenticated. The token information displayed below
is already stored in the token helper. You do NOT need to run "vault login"
again. Future Vault requests will automatically use this token.

Key                  Value
---                  -----
token                hvs.9QOJsa7zlwHO8ieW15CXXoOp
token_accessor       170OHOscEZjfl8fSa8aVpNkZ
token_duration       ∞
token_renewable      false
token_policies       ["root"]
identity_policies    []
policies             ["root"]
</code></pre>
<p>之后，root token就被放置在“~/.vault-token”中了：</p>
<pre><code>$cat ~/.vault-token
hvs.9QOJsa7zlwHO8ieW15CXXoOp
</code></pre>
<blockquote>
<p>注：我们通常不会使用root token，而是会<a href="https://developer.hashicorp.com/vault/tutorials/secrets-management/vault-nomad-secrets">利用vault token命令生成新token</a>作为vault cli访问vault server的token。</p>
</blockquote>
<p>现在我们重新执行一下kv put命令：</p>
<pre><code>$vault kv put -mount=secret hello foo=world
== Secret Path ==
secret/data/hello

======= Metadata =======
Key                Value
---                -----
created_time       2023-11-06T03:01:25.968883Z
custom_metadata    &lt;nil&gt;
deletion_time      n/a
destroyed          false
version            2
</code></pre>
<p>kv创建成功，路径secret/data/hello(注：vault会默认在mount的路径secret下创建data路径)。vault server在将value值存储在backend storage(这里是memory)前，会用Encryption Key对内容进行加密。如果你多执行几次这个命令，你会发现输出信息中的version的数值会递增，这个数值表示设置的值的版本。</p>
<p>我们可以用kv get获取刚才写入的kv值，vault会将数据从backend storage中读取出来并解密：</p>
<pre><code>$vault kv get -mount=secret hello
== Secret Path ==
secret/data/hello

======= Metadata =======
Key                Value
---                -----
created_time       2023-11-06T03:01:25.968883Z
custom_metadata    &lt;nil&gt;
deletion_time      n/a
destroyed          false
version            2

=== Data ===
Key    Value
---    -----
foo    world
</code></pre>
<p>我们还可以通过delete删除刚刚建立的kv值(为后面的基本场景示例做铺垫)：</p>
<pre><code>$vault kv delete secret/foo
Success! Data deleted (if it existed) at: secret/data/foo
$vault kv get secret/foo
No value found at secret/data/foo
</code></pre>
<p>到这里我们看到，一旦vault安装完毕后，基本使用场景还是蛮简单的，但也仅限于基本使用场景^_^。下面我们再来看看如何通过代码来实现这些基本功能场景。</p>
<h3>3.5 使用client SDK与Vault交互</h3>
<p><a href="https://developer.hashicorp.com/vault/api-docs/libraries">Vault支持各种主流语言的client SDK</a>，其中Vault官方维护了三个：Go、Ruby和C&#35;，其他语言的SDK则是由社区维护。</p>
<p>我们用Go Client SDK来编写一个设置kv和获取kv值的小程序，如下面代码所示：</p>
<pre><code>// secret-management-examples/basic/main.go

package main

import (
    "context"
    "fmt"

    "github.com/hashicorp/vault/api"
)

func main() {
    // 创建一个新的Vault客户端
    client, err := api.NewClient(api.DefaultConfig())
    if err != nil {
        fmt.Println("无法创建Vault客户端:", err)
        return
    }

    // 设置Vault服务器的地址
    client.SetAddress("http://localhost:8200/")

    // 设置Vault的访问令牌（如果需要认证）
    client.SetToken("hvs.9QOJsa7zlwHO8ieW15CXXoOp")

    // 设置要写入的机密信息
    secretData := map[string]interface{}{
        "foo": "bar",
    }

    kv2 := client.KVv2("secret") // mount "secret"

    // 写入机密信息到Vault的secret/data/{key}路径下
    key := "hello"
    _, err = kv2.Put(context.Background(), key, secretData)
    if err != nil {
        fmt.Println("无法写入机密信息:", err)
        return
    }

    // 读取Vault的secret/data/{key}路径下的机密信息
    secret, err := kv2.Get(context.Background(), key)
    if err != nil {
        fmt.Println("无法读取机密信息:", err)
        return
    }

    // 打印读取到的值
    fmt.Println("读取到的值:", secret.Data)
}
</code></pre>
<p>我们看到：默认创建的api.Client操作的都是v1版本的数据，这里通过KVv2方法将其转换为可以操作v2版本数据的client，之后put和get就可以如预期正常工作了！</p>
<p>下面是其运行结果：</p>
<pre><code>$go run main.go
读取到的值: map[foo:bar]
</code></pre>
<p>有了基础场景做铺垫，接下来我们就进入实例环节，看看应用是如何基于Vault应对一些常见的机密管理场景的。</p>
<h2>4. 常见的机密管理场景</h2>
<p>Vault支持对多种机密信息的管理，包括应用访问外部服务或资源所需的用户名/密码、API密钥或访问令牌(token)，应用程序的配置中的机密配置信息，比如数据库连接字符串、加密密钥等，以及私钥、证书等加密相关的机密信息等。这里我们就分别来看看应用与Vault集成并获取这些机密信息的场景，不过在这之前，我们首先需要先来了解一下应用本身与Vault是如何集成的。</p>
<h3>4.1 应用通过Vault身份认证和授权的方法</h3>
<p>在3.5小节的基本场景示例中，我们的client使用了<a href="https://developer.hashicorp.com/vault/docs/auth/token">一个长期有效的token</a>通过了Vault的身份认证和授权环节，拥有了操作Vault数据的权限。</p>
<p>token auth方法也是dev模式下Vault server实例支持的唯一auth method，我们可以通过auth list命令查看vault server当前支持的auth方法集合：</p>
<pre><code>$vault auth list
Path      Type     Accessor               Description                Version
----      ----     --------               -----------                -------
token/    token    auth_token_6f9cc41c    token based credentials    n/a
</code></pre>
<p>不过，基于token来实现app与Vault的集成并非Vault官方推荐的在生产环境使用的<a href="https://developer.hashicorp.com/vault/docs/auth">auth方式</a>，理由也很明显：这种方式涉及手动创建一个长期有效的令牌，这有悖于最佳实践，并存在安全风险。</p>
<p>除了Token auth method，Vault还支持<a href="https://developer.hashicorp.com/vault/docs/auth/approle">AppRole</a>、<a href="https://developer.hashicorp.com/vault/docs/auth/jwt">JWT/OIDC</a>、<a href="https://developer.hashicorp.com/vault/docs/auth/cert">TLS证书</a>以及<a href="https://developer.hashicorp.com/vault/docs/auth/userpass">User/Password</a>等<a href="https://developer.hashicorp.com/vault/docs/auth">多种auth method</a>，这些auth method的共同之处在于通过身份认证后，Vault可自动创建<strong>短期令牌</strong>供客户端使用，无需定期手动生成新令牌，短期令牌可以减少令牌泄露的风险，因为短期令牌在一定时间后会自动失效，并需要重新进行身份认证。</p>
<p>简单起见，我这里就用User/Password method作为实例演示一下应用通过Vault的身份认证和授权。</p>
<p>我们先来开启(enable)基于User/Password的auth method：</p>
<pre><code>$vault auth enable userpass
Success! Enabled userpass auth method at: userpass/
</code></pre>
<p>该命令默认将会启用auth/userpass路径，之后通过auth list查看，就能在list中看到新增的userpass auth method了：</p>
<pre><code>$vault auth list
Path         Type        Accessor                  Description                Version
----         ----        --------                  -----------                -------
token/       token       auth_token_6f9cc41c       token based credentials    n/a
userpass/    userpass    auth_userpass_b5b6e974    n/a                        n/a

</code></pre>
<p>接下来，我们在vault服务实例中建立一个新的user：</p>
<pre><code>$vault write auth/userpass/users/tonybai password=ilovegolang
Success! Data written to: auth/userpass/users/tonybai

$vault read auth/userpass/users/tonybai
Key                        Value
---                        -----
token_bound_cidrs          []
token_explicit_max_ttl     0s
token_max_ttl              0s
token_no_default_policy    false
token_num_uses             0
token_period               0s
token_policies             [default]
token_ttl                  0s
token_type                 default
</code></pre>
<p>下面是示例代码：</p>
<pre><code>// secret-management-examples/auth_user_password/main.go

package main

import (
    "context"
    "fmt"

    "github.com/hashicorp/vault/api"
    auth "github.com/hashicorp/vault/api/auth/userpass"
)

func main() {
    user := "tonybai"
    pass := "ilovegolang"

    // 创建Vault API客户端
    client, err := api.NewClient(api.DefaultConfig())
    if err != nil {
        fmt.Printf("无法创建Vault客户端: %v\n", err)
        return
    }
    // 设置 Vault 地址
    client.SetAddress("http://localhost:8200")

    // client登录vault服务器获取临时访问令牌
    userpassAuth, err := auth.NewUserpassAuth(user, &amp;auth.Password{FromString: pass})
    if err != nil {
        fmt.Errorf("无法初始化userpass auth method: %w", err)
        return
    }

    secret, err := client.Auth().Login(context.Background(), userpassAuth)
    if err != nil {
        fmt.Errorf("登录Vault失败: %w", err)
        return
    }
    if secret == nil {
        fmt.Printf("登录后没有secret信息返回: %v\n", err)
        return
    }
    fmt.Printf("登录Vault成功\n")

    token := secret.Auth.ClientToken

    // 设置临时访问令牌
    client.SetToken(token)

    kv2 := client.KVv2("secret") // mount "secret"
    // 读取Vault的secret/data/{key}路径下的机密信息
    data, err := kv2.Get(context.Background(), "hello")
    if err != nil {
        fmt.Println("无法读取机密信息:", err)
        return
    }

    // 打印读取到的值
    fmt.Println("读取到的值:", data.Data)
}
</code></pre>
<p>如果你在Vault的GO SDK中没有找到对user/password auth method的直接支持，你也可以参考<a href="https://developer.hashicorp.com/vault/api-docs/auth/userpass">user/password auth method的API文档</a>自行实现登录Vault并读取特定机密信息，代码如下(与上面代码功能是等价的)：</p>
<pre><code>// secret-management-examples/auth_user_password_self_impl/main.go

func clientAuth(vaultAddr, user, pass string) (*api.Secret, error) {
    payload := fmt.Sprintf(`{"password": "%s"}`, pass)

    req, err := http.NewRequest("POST", vaultAddr+"/v1/auth/userpass/login/"+user, strings.NewReader(payload))
    if err != nil {
        return nil, err
    }

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return nil, err
    }

    if resp.StatusCode != http.StatusOK {
        return nil, errors.New(string(body))
    }

    return api.ParseSecret(bytes.NewReader(body))
}

func main() {
    vaultAddr := "http://localhost:8200"
    user := "tonybai"
    pass := "ilovegolang"

    // client登录vault服务器获取临时访问令牌
    secret, err := clientAuth(vaultAddr, user, pass)
    if err != nil {
        fmt.Printf("登录Vault失败: %v\n", err)
        return
    }
    fmt.Printf("登录Vault成功\n")

    // 创建Vault API客户端
    client, err := api.NewClient(api.DefaultConfig())
    if err != nil {
        fmt.Printf("无法创建Vault客户端: %v\n", err)
        return
    }

    // 设置 Vault 地址
    client.SetAddress("http://localhost:8200")
    token := secret.Auth.ClientToken

    // 设置临时访问令牌
    client.SetToken(token)

    kv2 := client.KVv2("secret") // mount "secret"
    // 读取Vault的secret/data/{key}路径下的机密信息
    data, err := kv2.Get(context.Background(), "hello")
    if err != nil {
        fmt.Println("无法读取机密信息:", err)
        return
    }

    // 打印读取到的值
    fmt.Println("读取到的值:", data.Data)
}
</code></pre>
<p>我们运行一下上述两个示例代码之一：</p>
<pre><code>$go run main.go
登录Vault成功
无法读取机密信息: error encountered while reading secret at secret/data/hello: Error making API request.

URL: GET http://localhost:8200/v1/secret/data/hello
Code: 403. Errors:

* 1 error occurred:
    * permission denied

</code></pre>
<p>通过错误信息来看，“tonybai”这个user没有权限读取secret/data/hello下的机密信息！那么怎么给这个用户加上secret/data/hello的读取权限呢？Vault通过policy来管理权限，如果某个user具有某个policy的绑定，那么该user就拥有该policy设定的权限，这有点像RBAC的思路，只是没有引入role的概念! 我们先来添加一个拥有secret/data/hello读权限的policy：</p>
<pre><code>$vault policy write my-policy -&lt;&lt;EOF
# Allow "read" permission on "secret/data/*" secrets
path "secret/data/*" {
  capabilities = ["read"]
}
EOF
Success! Uploaded policy: my-policy
</code></pre>
<p>接下来重写user的属性数据，将my-policy赋给”tonybai”这个user：</p>
<pre><code>$vault write auth/userpass/users/tonybai password=ilovegolang token_policies=my-policy
Success! Data written to: auth/userpass/users/tonybai

$vault read auth/userpass/users/tonybai
Key                        Value
---                        -----
token_bound_cidrs          []
token_explicit_max_ttl     0s
token_max_ttl              0s
token_no_default_policy    false
token_num_uses             0
token_period               0s
token_policies             [my-policy]
token_ttl                  0s
token_type                 default
</code></pre>
<p>完成上述设置后，我们再来运行一下基于user/password auth method的程序：</p>
<pre><code>$go run main.go
登录Vault成功
读取到的值: map[foo:bar]
</code></pre>
<p>这次程序成功登录Vault并成功读取了secret/data/hello下面的机密数据。</p>
<p>这里我们除了设置了token_policies，其他属性都保持了默认值，这样我们拿到的临时token其实并不“临时”，我们可以一直使用。下面我们通过设置token_ttl来指定每个临时token的最大有效时间：</p>
<pre><code>$vault write auth/userpass/users/tonybai password=ilovegolang token_policies=my-policy token_ttl=5s
Success! Data written to: auth/userpass/users/tonybai

$vault read auth/userpass/users/tonybai
Key                        Value
---                        -----
token_bound_cidrs          []
token_explicit_max_ttl     0s
token_max_ttl              0s
token_no_default_policy    false
token_num_uses             0
token_period               0s
token_policies             [my-policy]
token_ttl                  5s
token_type                 default
</code></pre>
<p>我们改写一下程序，让程序每隔1秒用临时token获取一下机密信息并输出：</p>
<pre><code>// secret-management-examples/auth_user_password_renewal/main.go (临时版本)

    for {
        // 每个一秒读取一次Vault的secret/data/{key}路径下的机密信息
        data, err := kv2.Get(context.Background(), "hello")
        if err != nil {
            fmt.Println("无法读取机密信息:", err)
            return
        }

        // 打印读取到的值
        log.Println("读取到的值:", data.Data)
        time.Sleep(time.Second)
    }
</code></pre>
<p>我们运行这个程序将得到如下结果：</p>
<pre><code>$go run main.go
登录Vault成功
2023/11/06 05:24:17 读取到的值: map[foo:bar]
2023/11/06 05:24:18 读取到的值: map[foo:bar]
2023/11/06 05:24:19 读取到的值: map[foo:bar]
2023/11/06 05:24:20 读取到的值: map[foo:bar]
2023/11/06 05:24:21 读取到的值: map[foo:bar]
无法读取机密信息: error encountered while reading secret at secret/data/hello: Error making API request.

URL: GET http://localhost:8200/v1/secret/data/hello
Code: 403. Errors:

* permission denied
</code></pre>
<p>我们看到如果token过期，而我们的程序又没有<a href="https://developer.hashicorp.com/vault/docs/concepts/lease#lease-durations-and-renewal">对token进行续期(renewal)</a>，程序后续对Vault中机密数据的访问将以”permission denied”的失败而告终。下面我们就来为程序加上token续期，Vault SDK提供了LifetimeWatcher来辅助token续期工作，下面就是利用LifetimeWatcher进行token续期的示例：</p>
<pre><code>// secret-management-examples/auth_user_password_renewal/main.go

package main

import (
    "context"
    "fmt"
    "log"
    "time"

    "github.com/hashicorp/vault/api"
    auth "github.com/hashicorp/vault/api/auth/userpass"
)

func main() {
    user := "tonybai"
    pass := "ilovegolang"

    // 创建Vault API客户端
    client, err := api.NewClient(api.DefaultConfig())
    if err != nil {
        fmt.Printf("无法创建Vault客户端: %v\n", err)
        return
    }
    // 设置 Vault 地址
    client.SetAddress("http://localhost:8200")

    // client登录vault服务器获取临时访问令牌
    userpassAuth, err := auth.NewUserpassAuth(user, &amp;auth.Password{FromString: pass})
    if err != nil {
        fmt.Errorf("无法初始化userpass auth method: %w", err)
        return
    }

    secret, err := client.Auth().Login(context.Background(), userpassAuth)
    if err != nil {
        fmt.Errorf("登录Vault失败: %w", err)
        return
    }
    if secret == nil {
        fmt.Printf("登录后没有secret信息返回: %v\n", err)
        return
    }
    fmt.Printf("登录Vault成功\n")

    token := secret.Auth.ClientToken

    // 设置临时访问令牌
    client.SetToken(token)

    // 设置renewel watcher
    watcher, err := client.NewLifetimeWatcher(&amp;api.LifetimeWatcherInput{
        Secret: secret,
    })
    go watcher.Start()
    defer watcher.Stop()

    kv2 := client.KVv2("secret") // mount "secret"
    ticker := time.NewTicker(time.Second)

    for {
        select {
        case err := &lt;-watcher.DoneCh():
            if err != nil {
                log.Printf("Failed to renew token: %v. Re-attempting login.", err)
                return
            }

            // This occurs once the token has reached max TTL.
            log.Printf("Token can no longer be renewed. Re-attempting login.")
            return

        case renewal := &lt;-watcher.RenewCh():
            // Renewal is now over
            log.Printf("Successfully renewed: %#v", renewal)

        case &lt;-ticker.C:
            // 每个一秒读取一次Vault的secret/data/{key}路径下的机密信息
            data, err := kv2.Get(context.Background(), "hello")
            if err != nil {
                fmt.Println("无法读取机密信息:", err)
                continue
            }
            // 打印读取到的值
            log.Println("读取到的值:", data.Data)
        }
    }
}

</code></pre>
<p>运行上述示例(此时token_ttl为5s)：</p>
<pre><code>$go run main.go
登录Vault成功
2023/11/06 05:17:42 Successfully renewed: &amp;api.RenewOutput{RenewedAt:time.Date(2023, time.November, 7, 14, 17, 42, 233750000, time.UTC), Secret:(*api.Secret)(0xc000114a80)}
2023/11/06 05:17:43 读取到的值: map[foo:bar]
2023/11/06 05:17:44 读取到的值: map[foo:bar]
2023/11/06 05:17:45 读取到的值: map[foo:bar]
2023/11/06 05:17:45 Successfully renewed: &amp;api.RenewOutput{RenewedAt:time.Date(2023, time.November, 7, 14, 17, 45, 841374000, time.UTC), Secret:(*api.Secret)(0xc0002827e0)}
2023/11/06 05:17:46 读取到的值: map[foo:bar]
2023/11/06 05:17:47 读取到的值: map[foo:bar]
2023/11/06 05:17:48 读取到的值: map[foo:bar]
2023/11/06 05:17:49 读取到的值: map[foo:bar]
2023/11/06 05:17:49 Successfully renewed: &amp;api.RenewOutput{RenewedAt:time.Date(2023, time.November, 7, 14, 17, 49, 443211000, time.UTC), Secret:(*api.Secret)(0xc0002831a0)}
2023/11/06 05:17:50 读取到的值: map[foo:bar]
2023/11/06 05:17:51 读取到的值: map[foo:bar]
2023/11/06 05:17:52 读取到的值: map[foo:bar]
2023/11/06 05:17:53 Successfully renewed: &amp;api.RenewOutput{RenewedAt:time.Date(2023, time.November, 7, 14, 17, 53, 46880000, time.UTC), Secret:(*api.Secret)(0xc000115a40)}
2023/11/06 05:17:53 读取到的值: map[foo:bar]
2023/11/06 05:17:54 读取到的值: map[foo:bar]
... ...
</code></pre>
<p>我们看到，在token过期之前，LifetimeWatcher帮助Client完成了续期请求。LifetimeWatcher运行在一个单独的goroutine中，通过channel与main goroutine通信。Vault默认token_max_ttl的值为32天，即便你没有设置其值，当token续期到32天时，就无法再renew了，此时watcher.DoneCh会返回事件，这是让你重新login的信号，示例中只给出了注释，并未重新login，大家注意一下。出于安全考虑，可以将token_max_ttl设置为一个合理的值，使其起到应有的安全作用。</p>
<p>通过这个示例我们看到，只要通过Vault的身份认证和授权，我们就能安全地存储和使用机密信息了。那么如何保证应用在与Vault进行身份认证和授权时所使用的凭据的安全呢？比如上面程序里所需的user和password。这个感觉又回到“先有鸡还是先有蛋”的问题了！实际在生产环境，我们可以依赖IaaS层或公有云的安全措施来保证，比如通过环境变量在运行时注入user和password；再比如利用公有云提供的KMS(key management system)或HSM(Hardware Security Module)服务来保证user和password安全。</p>
<h3>4.2  静态secret</h3>
<p>将静态secret作为机密信息保存和管理，是Vault非常常见的应用。secret可以存在很长时间不变，或可能很少改变。Vault可以使用它的加密屏障(barrier)存储这些secret，应用程序运行时可以向Vault请求读取这些secret来使用。</p>
<p>Vault的versioned secrets engine支持你以安全的方式存储和管理secret，同时还提供secret的版本控制能力。你可以使用不同版本的secret进行应用程序升级或回滚，也可以在需要时轻松地恢复旧版本secret。引擎还可以记录secret每个版本的修改人和修改时间。</p>
<p>关于静态secret的管理和使用，可以参见3.5中的基本场景，这里就不赘述了。</p>
<h3>4.3 动态secret</h3>
<p>有静态、长有效期的静态secret，就会有对应的动态secret。和静态secret相比，动态secret安全性高，每个动态secret的有效期都较短，并且一旦泄露可以马上撤销，同时动态secret也便于轮换，定期自动过期无需中断业务。</p>
<p>Vault提供了对多种针对不同系统的动态secret管理能力，包括数据库访问凭据、Active Directory账号, SSH keys和PKI certificates ，Vault针对不同系统提供了不同的secret engine。</p>
<p>Vault官方举了一个有关<a href="https://developer.hashicorp.com/vault/tutorials/db-credentials/database-secrets">使用Database Secrets Engine实现数据库动态secret</a>的示例，</p>
<p>鉴于篇幅，这里也不细说了。</p>
<h3>4.4 其他场景</h3>
<p>根据<a href="https://developer.hashicorp.com/vault/docs/use-cases">Vault官方文档对Vault应用场景的描述</a>，除了静态和动态secret类机密信息，Vault可以处理以下类型的机密信息：</p>
<ul>
<li>数据加密类(Data encryption)机密信息 </li>
</ul>
<p>Vault支持将数据加密服务外包给Vault，应用只需关注数据的加密与解密，Vault负责核心密钥和加密管理。Vault还支持对数据进行传输加密与存储加密。</p>
<ul>
<li>身份识别类(Identity-Based access)机密信息</li>
</ul>
<p>Vault支持从不同身份验证系统整合用户身份，实现统一的ACL系统，管理对系统和应用的访问。</p>
<ul>
<li>加密密钥类(Key management)机密信息</li>
</ul>
<p>Vault支持对云提供商密钥的生命周期管理，例如管理AWS KMS或GCP云密钥。</p>
<p>鉴于篇幅和实验环境有限，这里就针对每种情况做详细示例说明了，大家可以根据自己的需求，针对具体的某个场景做专题性的研究。</p>
<h2>5. 小结</h2>
<p>本文首先介绍了机密管理的概念，阐述了在现代Web应用开发中，为何需要重视机密管理。</p>
<p>接着，文中概述了专用于实现机密管理的机密管理系统的发展历程，以及从功能上逐步演化出的云原生机密管理系统的特征。</p>
<p>文章以业内知名的开源机密管理系统HashiCorp Vault为例，全面系统地介绍了它的架构设计、安全模型、使用方法，并详细阐释了应用程序如何通过与Vault API/SDK的集成，实现对各类机密信息的安全存储、动态生成、访问控制、审计等功能。</p>
<p>最后，文章用代码实例详细演示了基于Vault的几个典型机密管理场景，如不同类型机密信息的读写操作，以及不同认证方式的集成等。</p>
<p>这是个”每个人都应该重视安全的时代”，安全需要每个环节的参与，一处薄弱，就会导致“处处薄弱”。我相信本文的内容能有助于让大家对机密管理的概念、重要性及具体实现方法有更深入的理解。</p>
<p>本文涉及的代码可以在<a href="https://github.com/bigwhite/experiments/tree/master/secret-management-examples">这里</a>下载。</p>
<blockquote>
<p>注：Vault项目还提供了Vault Agent和Vault Proxy，旨在为应用提供更可扩展、更简单的方式来集成Vault，消除应用程序采用Vault的初期障碍。Vault Agent可以获取secrets并将它们提供给应用程序，Vault Proxy可以在Vault和应用程序之间充当代理，可选地简化认证过程并缓存请求。有兴趣的童鞋可以参考<a href="https://developer.hashicorp.com/vault/docs/agent-and-proxy">Vault Agent和Proxy的官方文档</a>。</p>
</blockquote>
<h2>6. 参考资料</h2>
<ul>
<li><a href="https://arxiv.org/abs/2109.09905">Comparative Analysis of Cryptographic Key Management Systems</a> &#8211; https://arxiv.org/abs/2109.09905</li>
<li><a href="https://arxiv.org/abs/2208.11280">What are the Practices for Secret Management in Software Artifacts?</a> &#8211; https://arxiv.org/abs/2208.11280</li>
<li><a href="https://en.wikipedia.org/wiki/Shamir%27s_secret_sharing">Shamir&#8217;s secret sharing</a> &#8211; https://en.wikipedia.org/wiki/Shamir%27s_secret_sharing</li>
<li><a href="https://www.hashicorp.com/blog/vault-announcement">HashiCorp Vault</a> &#8211; https://www.hashicorp.com/blog/vault-announcement</li>
<li><a href="https://developer.hashicorp.com/vault/docs/internals/architecture">Vault Architecture</a> &#8211; https://developer.hashicorp.com/vault/docs/internals/architecture</li>
<li><a href="https://github.com/external-secrets/external-secrets">External Secrets</a> &#8211; https://github.com/external-secrets/external-secrets</li>
<li><a href="https://www.hashicorp.com/resources/5-best-practices-for-secrets-management">5 best practices for secrets management</a> &#8211; https://www.hashicorp.com/resources/5-best-practices-for-secrets-management</li>
<li><a href="https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html">Secrets Management Cheat Sheet</a> &#8211; https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html</li>
<li><a href="https://www.imperva.com/learn/data-security/secret-management/">Secret Management</a> &#8211; https://www.imperva.com/learn/data-security/secret-management/</li>
<li><a href="https://learn.microsoft.com/en-us/azure/key-vault/secrets/secrets-best-practices">Best practices for secrets management in Key Vault</a> &#8211; https://learn.microsoft.com/en-us/azure/key-vault/secrets/secrets-best-practices</li>
<li><a href="https://www.beyondtrust.com/resources/glossary/secrets-management">Glossary: Secrets Management</a> &#8211; https://www.beyondtrust.com/resources/glossary/secrets-management</li>
</ul>
<hr />
<p><a href="https://public.zsxq.com/groups/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻) &#8211; https://gopherdaily.tonybai.com</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>Gopher Daily归档 &#8211; https://github.com/bigwhite/gopherdaily</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2023, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2023/11/08/understand-go-web-secret-management-by-example/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>Go语言很无聊&#8230;其实它妙不可言！[译]</title>
		<link>https://tonybai.com/2021/01/07/go-is-boring/</link>
		<comments>https://tonybai.com/2021/01/07/go-is-boring/#comments</comments>
		<pubDate>Thu, 07 Jan 2021 09:06:35 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[benchmark]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[Channel]]></category>
		<category><![CDATA[consul]]></category>
		<category><![CDATA[CPU]]></category>
		<category><![CDATA[CSP]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[etcd]]></category>
		<category><![CDATA[exception]]></category>
		<category><![CDATA[GC]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[GopherCon]]></category>
		<category><![CDATA[goroutine]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[LLVM]]></category>
		<category><![CDATA[overloading]]></category>
		<category><![CDATA[override]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[RAM]]></category>
		<category><![CDATA[Rust]]></category>
		<category><![CDATA[select]]></category>
		<category><![CDATA[Swift]]></category>
		<category><![CDATA[traefik]]></category>
		<category><![CDATA[vault]]></category>
		<category><![CDATA[垃圾回收]]></category>
		<category><![CDATA[并发]]></category>
		<category><![CDATA[泛型]]></category>
		<category><![CDATA[虚拟机]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3069</guid>
		<description><![CDATA[无聊是一种很奇妙的状态，它可以稀释掉人类的一切情感。- 《古董局中局》马伯庸 在GopherCon 2020技术大会上(线上虚拟大会)，Jon Bodner为全球gopher们做了主题为“Go Is Boring”的精彩演讲(关注公众号iamtonybai，发送gophercon2020即可得到GopherCon 2020技术大会幻灯片资料)。 其实早在2020年6月，Jon Bodner就发表过类似主题的文章《Go is Boring&#8230;And That’s Fantastic!》。其副标题为：深入探究世界为何依赖简单，可靠且易于理解的技术。本文将在这篇文章的基础上，结合演讲内容做综合翻译与整理，为大家呈现Jon Bodner这个资深程序员对Go语言哲学的理解。 1. 大多编程语言都在堆砌新功能特性 我从事专业软件工程师已有将近23年的时间，而我编写程序的时间也已有38年了。在这个过程中，我使用过很多编程语言。我喜欢编程语言，并且了解它们的新功能特性以及与之前的语言相比所进行的改动。 如果看一下过去十年的编程语言，您会发现很多变化。C++，Java，Python和JavaScript增加了许多新功能，而一些新编程语言，诸如Rust和Swift等自诞生以来也发生了显著的变化。这一切都非常令人兴奋，但同时也会让你产生一种感觉：有时候，您永远无法跟上这些语言的所有想法。 图：C到C++，再到更复杂的C++ 图：Java到Java2，再到更复杂的Java3？ JavaScript、Python、Rust、Swift、&#8230; &#8230; 2. Go没有这么多功能特性 接下来轮到Go了！考量Go的最好方法是思考它没有的功能特性： Go没有虚拟机或基于LLVM的编译器； Go没有异常(exception)； Go没有用户定义的实现继承； Go不支持重载函数、方法或运算符； Go没有不变量； Go没有枚举； Go没有泛型； 自2012年Go 1.0发布以来，Go并未添加任何主要功能特性。 Go令人兴奋的一件事是通过goroutine，channel和select原生支持并发。但是，它基于CSP的思想，即Communicating Sequential Processes, 要知道，这可是一个早在1978年就被提出的思想。 这听起来不像是21世纪的编程语言，对吗？ 然而，根据Stack Overflow的说法，Go是第三名程序员最想要学习的语言，而且（也许并非巧合）也是第三名最高薪的语言。硅谷的每个创业公司都在使用Go来构建其基础架构。Go语言编写了Docker，Kubernetes，etcd，Terraform，Vault，Consul，Traefik和许多其他前沿项目。那么问题来了？为什么每个人都对这种无聊的语言感兴趣呢？ 3. 为什么每个人都对这种无聊的语言感兴趣呢？ 在回答这个问题之前，让我们先退一步。 这是希腊Argolis的Arkadiko桥，它是世界上最古老的桥梁，至今已有3000多年的历史。令人惊讶的是，它仍在使用中。 现在，我们为什么要关心一座古老的桥呢？这是因为软件开发有一个普遍的、但软件工程师们却不喜欢过多谈论的真理： 我们真的不擅长编写软件。 我指的不仅仅是办公室里的那个人，你的经理在紧要关头派他去减少bug的数量。我指的是每个人&#8211;我，你，还有你能想到的所有著名的开发者。 但那些设计和建造桥梁的人，他们很擅长建桥。桥梁能按时、按预算建成，并能持续服务几十、几百、甚至几千年。造桥，如果你仔细想想，还真有点厉害。而桥梁是这样一种常见的现象，它们也是非常无聊的。当一座桥正常工作的时候，没有人惊奇，而当软件正常工作的时候，大家都有点惊奇。 不幸的是，这个世界非常依赖软件。它对软件的依赖甚至可能比对桥梁的依赖更甚。所以，我们必须以比造桥更快的速度更好地编写软件。 4. 这些年我们对编写软件的了解 在过去的60年中，我们在编写程序方面已经学到了一些东西，其中有很多普遍的共识： 早发现问题比晚发现问题要好。 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/go-is-boring/go-is-boring-0.png" alt="img{512x368}" /></p>
<blockquote>
<p>无聊是一种很奇妙的状态，它可以稀释掉人类的一切情感。- 《古董局中局》马伯庸</p>
</blockquote>
<p>在<a href="https://www.gophercon.com">GopherCon 2020技术大会上</a>(线上虚拟大会)，Jon Bodner为全球gopher们做了主题为“Go Is Boring”的精彩演讲(关注公众号<strong>iamtonybai</strong>，发送<strong>gophercon2020</strong>即可得到GopherCon 2020技术大会幻灯片资料)。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-is-boring/go-is-boring-1.png" alt="img{512x368}" /></p>
<p>其实早在2020年6月，Jon Bodner就发表过类似主题的文章<a href="https://www.capitalone.com/tech/software-engineering/go-is-boring/">《Go is Boring&#8230;And That’s Fantastic!》</a>。其副标题为：<strong>深入探究世界为何依赖简单，可靠且易于理解的技术</strong>。本文将在这篇文章的基础上，结合演讲内容做综合翻译与整理，为大家呈现Jon Bodner这个资深程序员对Go语言哲学的理解。</p>
<h3>1. 大多编程语言都在堆砌新功能特性</h3>
<p>我从事专业软件工程师已有将近23年的时间，而我编写程序的时间也已有38年了。在这个过程中，我使用过很多编程语言。我喜欢编程语言，并且了解它们的新功能特性以及与之前的语言相比所进行的改动。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-is-boring/go-is-boring-2.png" alt="img{512x368}" /></p>
<p>如果看一下过去十年的编程语言，您会发现很多变化。C++，Java，Python和JavaScript增加了许多新功能，而一些新编程语言，诸如Rust和Swift等自诞生以来也发生了显著的变化。这一切都非常令人兴奋，但同时也会让你产生一种感觉：有时候，您永远无法跟上这些语言的所有想法。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-is-boring/go-is-boring-3.png" alt="img{512x368}" /><br />
<center>图：C到C++，再到更复杂的C++</center></p>
<p><img src="https://tonybai.com/wp-content/uploads/go-is-boring/go-is-boring-4.png" alt="img{512x368}" /><br />
<center>图：Java到Java2，再到更复杂的Java3？</center></p>
<p>JavaScript、Python、Rust、Swift、&#8230; &#8230;</p>
<h3>2. Go没有这么多功能特性</h3>
<p>接下来轮到Go了！考量Go的最好方法是思考它没有的功能特性：</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-is-boring/go-is-boring-5.jpg" alt="img{512x368}" /></p>
<ul>
<li>Go没有虚拟机或基于LLVM的编译器；</li>
<li>Go没有异常(exception)；</li>
<li>Go没有用户定义的实现继承；</li>
<li>Go不支持重载函数、方法或运算符；</li>
<li>Go没有不变量；</li>
<li>Go没有枚举；</li>
<li>Go没有<a href="https://mp.weixin.qq.com/s/SMT40557JgQ9FjUkswznlA">泛型</a>；</li>
<li>自2012年Go 1.0发布以来，Go并未添加任何主要功能特性。</li>
</ul>
<p>Go令人兴奋的一件事是通过goroutine，channel和select原生支持并发。但是，它基于CSP的思想，即<a href="https://www.cs.cmu.edu/~crary/819-f09/Hoare78.pdf">Communicating Sequential Processes</a>, 要知道，这可是一个早在1978年就被提出的思想。</p>
<p>这听起来不像是21世纪的编程语言，对吗？</p>
<p>然而，根据Stack Overflow的说法，<a href="https://insights.stackoverflow.com/survey/2019/#most-loved-dreaded-and-wanted">Go是第三名程序员最想要学习的语言</a>，而且（也许并非巧合）也是<a href="https://insights.stackoverflow.com/survey/2019/#technology-_-what-languages-are-associated-with-the-highest-salaries-worldwide">第三名最高薪的语言</a>。硅谷的每个创业公司都在使用Go来构建其基础架构。Go语言编写了Docker，Kubernetes，etcd，Terraform，Vault，Consul，Traefik和许多其他前沿项目。那么问题来了？为什么每个人都对这种无聊的语言感兴趣呢？</p>
<h3>3. 为什么每个人都对这种无聊的语言感兴趣呢？</h3>
<p>在回答这个问题之前，让我们先退一步。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-is-boring/go-is-boring-6.jpeg" alt="img{512x368}" /></p>
<p>这是<a href="https://www.atlasobscura.com/places/arkadiko-bridge">希腊Argolis的Arkadiko桥</a>，它是世界上最古老的桥梁，至今已有3000多年的历史。令人惊讶的是，它仍在使用中。</p>
<p>现在，我们为什么要关心一座古老的桥呢？这是因为软件开发有一个普遍的、但软件工程师们却不喜欢过多谈论的真理：</p>
<p><strong>我们真的不擅长编写软件</strong>。</p>
<p>我指的不仅仅是办公室里的那个人，你的经理在紧要关头派他去减少bug的数量。我指的是每个人&#8211;我，你，还有你能想到的所有著名的开发者。</p>
<p>但那些设计和建造桥梁的人，他们很擅长建桥。桥梁能按时、按预算建成，并能持续服务几十、几百、甚至几千年。造桥，如果你仔细想想，还真有点厉害。而桥梁是这样一种常见的现象，它们也是非常无聊的。当一座桥正常工作的时候，没有人惊奇，而当软件正常工作的时候，大家都有点惊奇。</p>
<p>不幸的是，这个世界非常依赖软件。它对软件的依赖甚至可能比对桥梁的依赖更甚。所以，我们必须以比造桥更快的速度更好地编写软件。</p>
<h3>4. 这些年我们对编写软件的了解</h3>
<p>在过去的60年中，我们在编写程序方面已经学到了一些东西，其中有很多普遍的共识：</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-is-boring/go-is-boring-7.jpg" alt="img{512x368}" /></p>
<ul>
<li>早发现问题比晚发现问题要好。</li>
<li>人们在管理程序的内存方面很糟糕。</li>
<li>代码评审有助于发现bug。</li>
<li>在任何一个超过一个人的项目中，沟通成本占主导地位。</li>
</ul>
<h3>5. 硬件也不能拯救我们</h3>
<p>我们可以把这几件我们知道的事情和另一个已经确定下来的事实结合起来：电脑的速度不再快了。至少不像以前那样了。<a href="http://www.gotw.ca/publications/concurrency-ddj.htm">在20世纪80年代和90年代，CPU每1-2年就会快一倍</a>。但现在情况变了。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-is-boring/go-is-boring-8.png" alt="img{512x368}" /></p>
<p>当你看单核性能时，2019年最快的酷睿i9的速度不到2011年最快的酷睿i7的两倍。我们没有变得更快，而是给CPU增加了更多的核心。当你看多核性能时，它更好一些，略微快了2倍多。</p>
<p>限制我们的不仅仅是CPU性能。Forrest Smith写了一篇关于RAM和RAM访问模式对性能影响的<a href="https://www.forrestthewoods.com/blog/memory-bandwidth-napkin-math/">精彩博文</a>。其要点如下：</p>
<ul>
<li>RAM比CPU要慢得多，而且差距并没有得到改善，尽管CPU的速度并没有变快多少。</li>
</ul>
<p><img src="https://tonybai.com/wp-content/uploads/go-is-boring/go-is-boring-9.png" alt="img{512x368}" /></p>
<ul>
<li>RAM可能是随机访问，但如果你真的这样使用，它的速度很慢。在现代英特尔CPU上，如果数据是顺序的，你可以每秒从RAM中读取40千兆字节左右。如果你进行随机读取，每秒不到半GB。</li>
</ul>
<p><img src="https://tonybai.com/wp-content/uploads/go-is-boring/go-is-boring-10.png" alt="img{512x368}" /></p>
<ul>
<li>有很多指针的代码特别慢。引用Forrest的话。“按顺序将指针后面的值相加的速度低于1GB/秒。随机访问，两次错过缓存，运行速度只有0.1 GB/s。指针追逐的速度要慢10到20倍”。</li>
</ul>
<p><img src="https://tonybai.com/wp-content/uploads/go-is-boring/go-is-boring-11.png" alt="img{512x368}" /></p>
<h3>6. 无聊带来新的惊喜，我们再来看看Go</h3>
<p>鉴于我们知道的这些关于如何构建软件的几个宝贵的东西和我们现有的硬件状况，我们再来重新审视一下Go语言。</p>
<h4>1) Go和软件</h4>
<ul>
<li>尽早发现问题</li>
</ul>
<p>Go语言可能缺乏功能特性，但它却有一套很棒的工具。Go的编译器速度很快，这种快速的编译速度被Go团队认为是一个特点。它可以让你快速查看你的代码是否能编译，如果不能，它可以让你看到问题所在。测试被内置在标准库中，以鼓励开发者测试他们的代码并发现问题。<a href="https://www.imooc.com/read/87/article/2439">基准测试(benchmark)</a>、<a href="https://www.imooc.com/read/87/article/2440">剖析(profiling)</a>和竞态检查(-race)也是开箱即用的。很少有语言能提供这些工具，它们能让你更容易快速地发现问题。</p>
<ul>
<li>内存管理</li>
</ul>
<p>众所周知，Go有一个垃圾收集器。你不用担心跟踪内存，这是一件很奇妙的事情。在编译语言中，垃圾回收是很罕见的。Rust的borrow checker是获得高性能和内存管理的一个迷人的方法，但它实际上把开发者变成了垃圾收集器，这可能很难正确使用；如果你犯了错误，忘记将一些引用声明为弱引用，Swift的ARC仍然会泄漏内存。现在，Go的GC的性能不如这些半自动系统，有些情况下，你需要额外的速度，但在大多数情况下，它肯定是足够的。</p>
<ul>
<li>代码评审</li>
</ul>
<p>如果代码评审做得好，代码评审就很重要。为了进行有效的代码评审，你需要确保评审人员专注于正确的事情。低质量的代码评审会把时间花在格式化等事情上。Go在这里提供了很大帮助，因为在评审Go代码时没有有关代码格式的争论，因为所有的Go代码都是按照go fmt的标准代码格式进行格式化。</p>
<p>而代码评审是一个双向的过程。如果你想评审的效果好，你需要确保其他人能够理解你的代码。Go程序应该是简单的，使用一些很好理解的结构，这些结构自语言发布以来就没有改变过。因为没有异常(exception)，没有面向方面的编程(AOP)，没有继承和方法重写(override)，也没有重载(overloading)，所以很清楚什么代码在调用什么，在哪里返回值。如果你在Go中减少包级变量的使用，那就很容易看到数据到底是如何被修改的。由于Go的变化很小，你可以避免<a href="http://antipatterns.com/lavaflow.htm">熔岩流反模式</a>，你可以根据代码中使用的语法特性被引入到Go中的时间点来判断它到底有多老。</p>
<ul>
<li>沟通成本</li>
</ul>
<p>Go是如何帮助解决这个问题的呢？我们已经讨论过<a href="https://www.imooc.com/read/87/article/2321">Go的简单性</a>、稳定性和标准格式化如何让你更容易地传达你的代码正在做什么。虽然这只是其中的一部分，但还有其他的东西。Go的隐式接口帮助团队编写解耦的代码。它们由调用代码定义，以准确描述需要什么功能，这就明确了你的代码在做什么。</p>
<h4>2) Go和硬件</h4>
<p>让Go成为编译语言的决定得到了回报。当CPU每天都在变快时，在虚拟机(译注：这里所谓的虚拟机是指动态语言的解释器或像jvm之类的字节码运行程序)中运行的解释语言似乎是个好主意。如果你的程序不够快，只要再等一年就可以了。但现在这已经行不通了。编译成原生代码比最新的虚拟机技巧少了很多乐趣，但它能带来很大的性能优势。</p>
<p>让我们用<a href="https://benchmarksgame-team.pages.debian.net/benchmarksgame/index.html">The Benchmark Game</a>的微基准来比较Go与一些在虚拟机中运行的语言的性能。首先我们来看看Python和Ruby与Go的比较。任何小于100%的百分比都意味着比Go快，大于100%意味着慢：</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-is-boring/go-is-boring-12.png" alt="img{512x368}" /></p>
<p>这里有很多红色(意味着比Go慢的测试)。有一个基准测试是Python更快 (奇怪的是，它不仅是Go的两倍，而且在这个测试中比其他所有语言都快)，而Ruby则没有一个测试比Go快。除了那一个情况，这两种语言产生的代码都比Go慢了17%到60多倍。</p>
<p>现在让我们再看看Java和JavaScript与Go的比较：</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-is-boring/go-is-boring-13.png" alt="img{512x368}" /></p>
<p>这两门语言与Go的性能更为接近。JavaScript在一个基准上比Go快，在其他基准上比Go慢，但JavaScript最坏的情况是比Go慢了三倍左右。</p>
<p>Java和Go的性能相当接近。Java在四种情况下比Go快，在两种情况下差不多，在四种情况下比Go慢。Go做的最差的情况是比Java慢三倍左右，Go做的最好的情况是比Java快50%左右。</p>
<p>我们看到的是，唯一能跟上Go的虚拟机是Java的。<a href="https://www.oracle.com/technetwork/java/whitepaper-135217.html">Hotspot</a>是令人惊异的技术，但你需要一个世界上最好的工程软件，才能与一个优先考虑编译速度而非优化的编译器达到平衡，这说明了一些问题。而且你要为这种惊人的技术付出代价。Java应用程序的内存使用量要比Go应用程序大出很多很多倍。</p>
<p>还有第二个优势。垃圾收集器管理的垃圾都是不使用的指针。与隐藏指针的语言不同，Go给了你控制权。它让你避开指针，并以一种允许快速访问RAM的方式布局你的数据结构。这也让Go可以使用更简单的垃圾收集器，因为Go程序只是简单地制造更少的垃圾。枯燥无味的工作就是少了。</p>
<p>而我们都知道，CPU正在用更多的内核来弥补速度提升的不足。所以，使用一种能够利用这一点的语言是很好的。这就是Go内置并发支持的目的。有了对并发的语言级支持和一个在多个线程中调度goroutine的运行时库，意味着当你有多个CPU核时，这些线程可以被自然地映射到这些核上。</p>
<h3>7. 我不想要我没有得到的那些功能特性</h3>
<p>我们已经看到，Go专注于我们所知道的使创建软件更容易、更适合现代计算机的内存和CPU架构的功能和工具。但是其他语言有而Go没有的功能特性呢？也许Go的开发者错过了，那些Go没有的特性能帮助开发者写出了更少错误、更容易维护的代码？好吧，研究人员的研究结果告诉我们，事实并非如此。</p>
<p>2017年一篇名为<a href="https://web.cs.ucdavis.edu/~filkov/papers/lang_github.pdf">《Github中编程语言与代码质量的大规模研究》</a>的论文，该论文研究了17种语言的729个项目、8000万行代码、2.9万名作者、150万次提交，并试图回答这个问题：编程语言对软件质量的影响是什么？他们的答案是，差别不大。</p>
<blockquote>
<p>“值得注意的是，这些因语言设计而产生的微弱影响，绝大多数是由项目规模、团队规模和提交规模等过程因素主导的。”</p>
</blockquote>
<p>另一组研究人员对这些数据进行了第二次研究，并在2019年做了一项名为<a href="https://arxiv.org/pdf/1901.10220.pdf">“关于编程语言对代码质量的影响”</a>的论文。他们的发现更令人惊讶：</p>
<blockquote>
<p>“根据手头的数据，不仅无法建立编程语言和代码质量之间的因果关系，甚至它们之间的相关性也被证明是值得怀疑的。”</p>
</blockquote>
<p>如果编程语言的选择并不重要，那为什么要选择Go？这些研究表明的是，<strong>流程很重要。工具、测试、性能和长期维护的便利性比时髦的功能特性更重要</strong>。如果使用得当，Go内置的工具支持更好的流程，同时提供久经考验的功能特性。</p>
<p>这并不是说新功能不好。在过去的几个世纪和几千年里，桥梁建设技术当然在不断进步。但是，你想成为第一个走过一座用全新的理念和未经测试的技术建造的桥梁吗？你会想等一下，让人们测试一下再采用。</p>
<p>软件也是如此。如果我们要建立像桥梁一样可靠的软件基础架构，我们就需要使用像物理基础架构一样经过充分测试和理解的软件技术。这就是为什么Go主要使用20世纪70年代设计的功能特性，我们知道它们是有效的。</p>
<p>Go很无聊&#8230;.其实它妙不可言。让我们都来用它来构建明天的精彩应用吧。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-is-boring/go-is-boring-14.png" alt="img{512x368}" /></p>
<hr />
<p>“Gopher部落”知识星球开球了！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！星球首开，福利自然是少不了的！2020年年底之前，8.8折(很吉利吧^_^)加入星球，下方图片扫起来吧！</p>
<p><img src="http://image.tonybai.com/img/202011/gopher-tribe-zsxq.png" alt="" /></p>
<p>Go技术专栏“<a href="https://www.imooc.com/read/87">改善Go语⾔编程质量的50个有效实践</a>”正在慕课网火热热销中！本专栏主要满足广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订阅！</p>
<p><img src="http://image.tonybai.com/img/202011/go-column-pgo-with-qr-and-text.png" alt="" /></p>
<p>我的网课“<a href="https://coding.imooc.com/class/284.html">Kubernetes实战：高可用集群搭建、配置、运维与应用</a>”在慕课网热卖中，欢迎小伙伴们订阅学习！</p>
<p><img src="https://tonybai.com/wp-content/uploads/k8s-practice-with-qr-and-text.png" 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/01/07/go-is-boring/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>HashiCorp联合创始人：Go是成功且无悔的选择</title>
		<link>https://tonybai.com/2020/11/13/go-is-a-successful-and-zero-regret-choice-for-us-by-hashicorp-founder/</link>
		<comments>https://tonybai.com/2020/11/13/go-is-a-successful-and-zero-regret-choice-for-us-by-hashicorp-founder/#comments</comments>
		<pubDate>Thu, 12 Nov 2020 23:03:57 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[consul]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[gobuild]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[HackerNews]]></category>
		<category><![CDATA[hashicorp]]></category>
		<category><![CDATA[nomad]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[Ruby]]></category>
		<category><![CDATA[terraform]]></category>
		<category><![CDATA[vagrant]]></category>
		<category><![CDATA[vault]]></category>
		<category><![CDATA[云原生]]></category>
		<category><![CDATA[跨平台编译]]></category>
		<category><![CDATA[错误处理]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=2986</guid>
		<description><![CDATA[提到HashiCorp这个公司，可能很多人都没听说过。但提到vagrant、consul、nomad、terraform或者vault，那么你一定对这些工具或其中之一有所耳闻。这些工具都是HashiCorp这家公司的开源项目。 今年年初，HashiCorp在中国IT圈着实“火”了一把！当时HashiCorp宣布旗下软件禁止在中国区销售，这让很多重度依赖欧美主导的开源软件的国内大厂、小厂、传统IT公司以及IT化做的比较好的大型国企“惊出一身冷汗”。但事后证实只是HashiCorp旗下的企业软件禁止在中国区销售，开源版本不受影响。并且企业版软件禁售的原因是因为其下产品Vault的加密方式不符合中国当地法律要求，为了遵循当地法律，所以禁止销售。 好了，书归正传！除了早期开源项目是使用python、ruby等动态语言开发的，HashiCorp公司的后期主流产品均基于Go语言开发。在近期Hacker News的一则“Go语言已有十多年的历史了，你怎么看？”的帖子里，HashiCorp公司的联合创始人Mitchell Hashimoto分享了HashiCorp公司选择Go的考虑以及Go语言给公司带来的益处，并称“Go是HashiCorp成功且无悔的选择”。 下面就是Mitchell Hashimoto印证其观点的阐述： 凭据：我大约在9年前开始使用Go，从那时起，我已经建立了一个拥有1000多名员工的公司，拥有约250名全职Go开发工程师。我们维护着数十个全部用Go编写的开源项目和库（Terraform，Vault等）。我们交付的商业产品已经被很多财富500强公司所使用。例如，Vault每年为我们知道的一家公司提供数万亿secret的服务。 提到Go，我能输出很多页的内容，但在这里我将尝试聚焦于其中的一部分。无论如何，Go都不是完美的语言或社区，但我喜欢它。 注意：人们在阅读反馈时通常会带有“但X语言也可以&#8230;”或某些类似的说法，我不希望出现这种情况。除非我专门使用示例，否则我的反馈意见不针对任何其他语言。另一种语言可能会为您解决所有这些相同的问题！我只是在分享Go对于我们在这些方面的出色表现。 对初学者和新员工的友善 Go是一种非常简单的语言。从公司成立到现在，我们可以雇用从未使用过Go的人员，告诉他们几个学习Go的资源（例如Tour of Go），他们就可以在一周内向生产级项目提交代码了，不可思议！ 使用Go你很难做出任何不明显的事情。这样做的代价通常是冗长或重复。但是我认为这带来的好处是值得的。我知道很多人不同意这一点，但是我个人更喜欢重复“if err！= nil”一千次，而不是引入需要重新学习的新错误控制流程。 我喜欢告诉新人（初级或非初级）：从上至下读源文件，这就是程序执行的路线。在大多数情况下，这总是对的。 作为围绕Go建立快速成长的团队/公司的人，这是必不可少的。 灵活(flexible) 理论上，任何“通用语言”都可以编写任何软件。但是，我敢肯定，我们都会同意：使用某些语言编写某些软件更加容易，这是一件好事。 但是，我对Go的灵活程度感到震惊，而又不会感到被强迫。我们已经使用Go编写了桌面CLI，Web API，分布式系统，安全软件，网络软件，基础设施软件，记帐软件(accounting)，机器人(bot)等。 而且，其中大多数软件每年的下载量达到数百万，并且已成功投入生产。 作为先前有着Ruby背景的我，很高兴已经实现的这些。但在做这些事情时也需要进行重大权衡。使用Go实现这些是可能的，但是您必须真正了解要进行的权衡。在Go中，当然要进行一些折衷，但是这些折衷是最小的，所以Go才起作用。 作为个人贡献者和公司管理者，这种灵活性在组建公司方面非常出色。 跨平台编译和静态链接的二进制文件 自9年前我采用Go的第一天起，Go就鼓励并简化了静态二进制编译和简单的跨平台编译。今天，您基本上只需要根据目标平台设置一下环境变量，然后运行go build，它通常就可以正常工作。 您仍然必须了解各类平台的各种陷阱（API可用性，文件路径，子进程/信号行为等），但仅就编译方面来说，Go让事情变得如此容易。 我以前在大规模软件开发方面的经验是在Ruby中使用Vagrant，而使其能跨平台工作是一个持续不断的巨大挑战。我需花几个月的时间让安装程序帮助跨平台设置正确的运行时环境。 从ARM系统和其他非标准体系结构（甚至是企业级Power &#8230;）的日益普及的角度来看，Go的这一特性非常重要。 文化 通常，Go社区的文化在哲学上与我对软件的看法非常一致。我将其粗略地描述为务实的和审慎的。 我认为这种文化正是为什么许多人不喜欢Go（或似乎“讨厌” Go的原因，我认为这是对用于编程计算机的语言的一种疯狂的情感反应，但我离题了）。 Go社区不会“赶时髦”，不会抛开谨慎和务实而去紧急实现某种“最新技术”。有人认为，从某种定义上来说这是Go的劣势，但我认为这是一个巨大的好处。在我看来，Go核心团队这种三思后而行的行为方式与我十分契合（我不是核心团队的成员，因此这纯粹是一种看法）。我喜欢这种文化。 乐趣 归根结底，这门语言对我来说很有趣。我喜欢用它编写程序。这也很重要。 结论 我很幸运，Go在我建立公司的行业中脱颖而出。当我们开始使用它时，肯定不是那样（它是1.0之前的版本，Docker之前的版本，K8S之前的版本，多数基础设施软件仍在使用Ruby）。 我觉得该语言非常高效（例如，我们在大约6周内就实现了Vault 0.1版本），它已经证明了它可以适应大规模的需求，并且运行稳定（在美国主要的一个证券交易市场中，Vault处于每笔交易的热门路径上，并且服务从未停过），我们已经能够围绕它建立一个大公司和活跃的开源社区。 把我们成功无悔的选择传递下去吧！ 我的Go技术专栏：“改善Go语⾔编程质量的50个有效实践”上线了，欢迎大家订阅学习！ 我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！ 我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/ smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。 2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/go-11th-years-old-2.jpeg" alt="img{512x368}" /></p>
<p>提到<a href="https://www.hashicorp.com">HashiCorp</a>这个公司，可能很多人都没听说过。但提到vagrant、<a href="https://tonybai.com/2018/09/10/setup-service-discovery-and-load-balance-based-on-consul">consul</a>、<a href="https://tonybai.com/2019/03/30/cluster-management-and-microservice-deployment-and-scheduled-by-nomad/">nomad</a>、terraform或者vault，那么你一定对这些工具或其中之一有所耳闻。这些工具都是HashiCorp这家公司的开源项目。</p>
<p>今年年初，HashiCorp在中国IT圈着实“火”了一把！当时HashiCorp宣布旗下软件禁止在中国区销售，这让很多重度依赖欧美主导的开源软件的国内大厂、小厂、传统IT公司以及IT化做的比较好的大型国企“惊出一身冷汗”。但事后证实只是HashiCorp旗下的企业软件禁止在中国区销售，开源版本不受影响。并且企业版软件禁售的原因是因为其下产品Vault的加密方式不符合中国当地法律要求，为了遵循当地法律，所以禁止销售。</p>
<p>好了，书归正传！除了早期开源项目是使用python、ruby等动态语言开发的，HashiCorp公司的后期主流产品均基于Go语言开发。在近期Hacker News的一则<a href="https://news.ycombinator.com/item?id=24887521">“Go语言已有十多年的历史了，你怎么看？”</a>的帖子里，HashiCorp公司的联合创始人<a href="https://github.com/mitchellh">Mitchell Hashimoto</a>分享了HashiCorp公司选择Go的考虑以及Go语言给公司带来的益处，并称<strong>“Go是HashiCorp成功且无悔的选择”</strong>。</p>
<p>下面就是Mitchell Hashimoto印证其观点的阐述：</p>
<p>凭据：我大约在9年前<a href="https://tonybai.com/tag/go">开始使用Go</a>，从那时起，我已经建立了一个拥有1000多名员工的公司，拥有约250名全职Go开发工程师。我们维护着数十个全部用Go编写的开源项目和库（Terraform，Vault等）。我们交付的商业产品已经被很多财富500强公司所使用。例如，Vault每年为我们知道的一家公司提供数万亿secret的服务。</p>
<p>提到Go，我能输出很多页的内容，但在这里我将尝试聚焦于其中的一部分。无论如何，<strong>Go都不是完美的语言或社区，但我喜欢它</strong>。</p>
<blockquote>
<p>注意：人们在阅读反馈时通常会带有“但X语言也可以&#8230;”或某些类似的说法，我不希望出现这种情况。除非我专门使用示例，否则我的反馈意见不针对任何其他语言。另一种语言可能会为您解决所有这些相同的问题！我只是在分享Go对于我们在这些方面的出色表现。</p>
</blockquote>
<h3>对初学者和新员工的友善</h3>
<p>Go是一种非常简单的语言。从公司成立到现在，我们可以雇用从未使用过Go的人员，告诉他们几个学习Go的资源（例如<a href="https://tour.golang.org/welcome/1">Tour of Go</a>），他们就可以在一周内向生产级项目提交代码了，不可思议！</p>
<p>使用Go你很难做出任何不明显的事情。这样做的代价通常是冗长或重复。但是我认为这带来的好处是值得的。我知道很多人不同意这一点，但是我<strong>个人更喜欢重复“if err！= nil”一千次</strong>，而不是引入需要重新学习的新错误控制流程。</p>
<p>我喜欢告诉新人（初级或非初级）：从上至下读源文件，这就是程序执行的路线。在大多数情况下，这总是对的。</p>
<p>作为围绕Go建立快速成长的团队/公司的人，这是必不可少的。</p>
<h3>灵活(flexible)</h3>
<p>理论上，任何“通用语言”都可以编写任何软件。但是，我敢肯定，我们都会同意：使用某些语言编写某些软件更加容易，这是一件好事。</p>
<p>但是，我对Go的灵活程度感到震惊，而又不会感到被强迫。我们已经使用Go编写了桌面CLI，Web API，分布式系统，安全软件，网络软件，基础设施软件，记帐软件(accounting)，机器人(bot)等。</p>
<p>而且，其中大多数软件每年的下载量达到数百万，并且已成功投入生产。</p>
<p>作为先前有着Ruby背景的我，很高兴已经实现的这些。但在做这些事情时也需要进行重大权衡。使用Go实现这些是可能的，但是您必须真正了解要进行的权衡。在Go中，当然要进行一些折衷，但是这些折衷是最小的，所以Go才起作用。</p>
<p>作为个人贡献者和公司管理者，这种灵活性在组建公司方面非常出色。</p>
<h3>跨平台编译和静态链接的二进制文件</h3>
<p>自9年前我采用Go的第一天起，Go就鼓励并简化了静态二进制编译和简单的跨平台编译。今天，您基本上只需要根据目标平台设置一下环境变量，然后运行<code>go build</code>，它通常就可以正常工作。</p>
<p>您仍然必须了解各类平台的各种陷阱（API可用性，文件路径，子进程/信号行为等），但仅就编译方面来说，Go让事情变得如此容易。</p>
<p>我以前在大规模软件开发方面的经验是在Ruby中使用Vagrant，而使其能跨平台工作是一个持续不断的巨大挑战。我需花几个月的时间让安装程序帮助跨平台设置正确的运行时环境。</p>
<p>从ARM系统和其他非标准体系结构（甚至是企业级Power &#8230;）的日益普及的角度来看，Go的这一特性非常重要。</p>
<h3>文化</h3>
<p>通常，Go社区的文化在哲学上与我对软件的看法非常一致。我将其粗略地描述为<strong>务实的和审慎的</strong>。</p>
<p>我认为这种文化正是为什么许多人不喜欢Go（或似乎“讨厌” Go的原因，我认为这是对用于编程计算机的语言的一种疯狂的情感反应，但我离题了）。</p>
<p>Go社区不会“赶时髦”，不会抛开谨慎和务实而去紧急实现某种“最新技术”。有人认为，从某种定义上来说这是Go的劣势，但我认为这是一个巨大的好处。在我看来，Go核心团队这种三思后而行的行为方式与我十分契合（我不是核心团队的成员，因此这纯粹是一种看法）。我喜欢这种文化。</p>
<h3>乐趣</h3>
<p>归根结底，这门语言对我来说很有趣。我喜欢用它编写程序。这也很重要。</p>
<h3>结论</h3>
<p>我很幸运，Go在我建立公司的行业中脱颖而出。当我们开始使用它时，肯定不是那样（它是1.0之前的版本，Docker之前的版本，K8S之前的版本，多数基础设施软件仍在使用Ruby）。</p>
<p>我觉得该语言非常高效（例如，我们在大约6周内就实现了Vault 0.1版本），它已经证明了它可以适应大规模的需求，并且运行稳定（在美国主要的一个证券交易市场中，Vault处于每笔交易的热门路径上，并且服务从未停过），我们已经能够围绕它建立一个大公司和活跃的开源社区。</p>
<p><strong>把我们成功无悔的选择传递下去吧！</strong></p>
<hr />
<p>我的Go技术专栏：“<a href="https://www.imooc.com/read/87">改善Go语⾔编程质量的50个有效实践</a>”上线了，欢迎大家订阅学习！</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-column-pgo-with-qr-and-text.png" 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/k8s-practice-with-qr-and-text.png" alt="img{512x368}" /></p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/<br />
smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。</p>
<p>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>我的联系方式：</p>
<ul>
<li>微博：https://weibo.com/bigwhite20xx</li>
<li>微信公众号：iamtonybai</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>Gopher Daily(Go每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</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; 2020, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2020/11/13/go-is-a-successful-and-zero-regret-choice-for-us-by-hashicorp-founder/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>使用nomad在weave网络中部署工作负载</title>
		<link>https://tonybai.com/2019/04/20/deploy-workload-in-weave-network-using-nomad/</link>
		<comments>https://tonybai.com/2019/04/20/deploy-workload-in-weave-network-using-nomad/#comments</comments>
		<pubDate>Sat, 20 Apr 2019 04:15:56 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[CIDR]]></category>
		<category><![CDATA[cluster]]></category>
		<category><![CDATA[consul]]></category>
		<category><![CDATA[container]]></category>
		<category><![CDATA[DNS]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[driver]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[hashicorp]]></category>
		<category><![CDATA[image]]></category>
		<category><![CDATA[iproute]]></category>
		<category><![CDATA[job]]></category>
		<category><![CDATA[k8s]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[NAT]]></category>
		<category><![CDATA[nomad]]></category>
		<category><![CDATA[openssh-server]]></category>
		<category><![CDATA[overlay]]></category>
		<category><![CDATA[overlaynetwork]]></category>
		<category><![CDATA[pod]]></category>
		<category><![CDATA[proxy]]></category>
		<category><![CDATA[route]]></category>
		<category><![CDATA[Service]]></category>
		<category><![CDATA[subnet]]></category>
		<category><![CDATA[task]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[virtualbox]]></category>
		<category><![CDATA[vm]]></category>
		<category><![CDATA[weave]]></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=2707</guid>
		<description><![CDATA[当初Kubernetes网络的设计目标是使得开发者使用pod时在网络这一层面可以像使用传统物理主机或虚拟机一样。具体的基本要求如下： 所有pod间均应可以在无需NAT的情况下直接通信； 所有集群节点与所有集群的Pod之间均应可以在无需NAT的情况下直接通信； 容器自身的地址和其他pod看到的它的地址是同一个地址； 按照这样的要求，集群中的每个pod都在一个平坦的、共享网络命名空间中，并且每个Pod都拥有一个IP，通信时无需端口映射。 用户也需要额外考虑如何建立Pod之间的连接，也不需要考虑将容器端口映射到主机端口等问题。基于这些要求而实现的k8s pod网络模型，将具有向后兼容的特性，可以使得Pod从某些角度上可以被看成是一个传统的物理主机或vm来对待。 在《使用nomad实现集群管理和微服务部署调度》一文中，我们看到nomad部署调度的driver为docker的服务实例都是通过主机和容器间的端口映射来对外提供服务的。服务实例多的时候，大量服务端口出现在眼前，我们很难用端口判断这是什么服务。并且通过映射端口暴露服务有局限，对于那些需要映射到主机固定端口的服务来说，很可能存在与其他服务的端口冲突而导致部署失败。除此之外，这种端口映射的方式还缺少隔离的作用，所有实例暴露的端口在同一个全局网络空间。 nomad是否可以像k8s一样将服务实例部署到overlay网络中从而实现每个服务实例所在container可以被看成一个独立的vm；并且我们还可以通过划分overlay的网段来隔离，实现某种意义上的“多租户”呢？在本篇文章中，我们来试验一下上述想法是否可行。 一、搭建试验环境 我们这次在一个VirtualBox搭建的三节点环境中进行验证。如果小伙伴对这段很熟悉，或者有现成的环境可用，那么可以跳过这一小节。另外这节不是重点，我不会对这个过程用过多文字做解释。 1. 创建虚机，组建网络 我们在一台ubuntu 18.04 desktop版本主机上搭建环境，所使用的软件版本信息如下： VirtualBox: 5.2.18 Guest OS: Ubuntu 16.04.6 LTS (GNU/Linux 4.4.0-142-generic x86_64) 组件环境的虚拟机和网络拓扑示意图如下： 如上图所示：三个vm 通过连入host-only网络(vboxnet0)实现内网通；通过连入NAT网络（NatNetwork）实现外网通。（怪异：在windows上的virtualbox实际上通过natnetwork即可实现全通的，无需host-only network，但是在ubuntu下居然不行）。 每个vm中网络配置如下： # cat /etc/network/interfaces # This file describes the network interfaces available on your system # and how to activate them. For more information, see [...]]]></description>
			<content:encoded><![CDATA[<p>当初<a href="https://tonybai.com/tag/kubernetes">Kubernetes</a>网络的设计目标是<strong>使得开发者使用pod时在网络这一层面可以像使用传统物理主机或虚拟机一样</strong>。具体的基本要求如下：</p>
<ul>
<li>所有pod间均应可以在无需NAT的情况下直接通信；</li>
<li>所有集群节点与所有集群的Pod之间均应可以在无需NAT的情况下直接通信；</li>
<li>容器自身的地址和其他pod看到的它的地址是同一个地址；</li>
</ul>
<p>按照这样的要求，集群中的每个pod都在一个平坦的、共享网络命名空间中，并且每个Pod都拥有一个IP，通信时无需端口映射。 用户也需要额外考虑如何建立Pod之间的连接，也不需要考虑将容器端口映射到主机端口等问题。基于这些要求而实现的k8s pod网络模型，将具有向后兼容的特性，可以使得Pod从某些角度上可以被看成是一个传统的物理主机或vm来对待。</p>
<p>在<a href="https://tonybai.com/2019/03/30/cluster-management-and-microservice-deployment-and-scheduled-by-nomad/">《使用nomad实现集群管理和微服务部署调度》</a>一文中，我们看到nomad部署调度的driver为docker的服务实例都是通过主机和容器间的端口映射来对外提供服务的。服务实例多的时候，大量服务端口出现在眼前，我们很难用端口判断这是什么服务。并且通过映射端口暴露服务有局限，对于那些需要映射到主机固定端口的服务来说，很可能存在与其他服务的端口冲突而导致部署失败。除此之外，这种端口映射的方式还缺少隔离的作用，所有实例暴露的端口在同一个全局网络空间。</p>
<p>nomad是否可以像k8s一样将服务实例部署到overlay网络中从而实现每个服务实例所在container可以被看成一个独立的vm；并且我们还可以通过划分overlay的网段来隔离，实现某种意义上的“多租户”呢？在本篇文章中，我们来试验一下上述想法是否可行。</p>
<h2>一、搭建试验环境</h2>
<p>我们这次在一个<a href="https://www.virtualbox.org/">VirtualBox</a>搭建的三节点环境中进行验证。<strong>如果小伙伴对这段很熟悉，或者有现成的环境可用，那么可以跳过这一小节</strong>。另外这节不是重点，我不会对这个过程用过多文字做解释。</p>
<h3>1. 创建虚机，组建网络</h3>
<p>我们在一台<a href="https://tonybai.com/tag/ubuntu">ubuntu</a> 18.04 desktop版本主机上搭建环境，所使用的软件版本信息如下：</p>
<ul>
<li>
<p>VirtualBox: 5.2.18</p>
</li>
<li>
<p>Guest OS: Ubuntu 16.04.6 LTS (GNU/Linux 4.4.0-142-generic x86_64)</p>
</li>
</ul>
<p>组件环境的虚拟机和网络拓扑示意图如下：</p>
<p><img src="https://tonybai.com/wp-content/uploads/virtualbox-3-vm-environment-for-nomad.png" alt="img{512x368}" /></p>
<p>如上图所示：三个vm 通过连入host-only网络(vboxnet0)实现内网通；通过连入NAT网络（NatNetwork）实现外网通。（怪异：在windows上的virtualbox实际上通过natnetwork即可实现全通的，无需host-only network，但是在ubuntu下居然不行）。</p>
<p>每个vm中网络配置如下：</p>
<pre><code># cat /etc/network/interfaces

# This file describes the network interfaces available on your system
# and how to activate them. For more information, see interfaces(5).

source /etc/network/interfaces.d/*

# The loopback network interface
auto lo
iface lo inet loopback

# The primary network interface
auto enp0s3
iface enp0s3 inet dhcp

auto enp0s8
iface enp0s8 inet dhcp

</code></pre>
<p>保存后，执行/etc/init.d/networking restart生效。</p>
<p>另外每个vm上安装了openssh-server(apt install openssh-server)并设置root可登陆。三个vm的主机名分为为u1、u2和u3（可通过hostnamectl &#8211;static set-hostname u1设置。并在/etc/hosts中添加主机名和内网IP的对应关系）。</p>
<p>每台主机上安装了docker引擎(通过apt install docker.io安装），docker版本信息如下：</p>
<pre><code># docker version
Client:
 Version:           18.09.2
 API version:       1.39
 Go version:        go1.10.4
 Git commit:        6247962
 Built:             Tue Feb 26 23:56:24 2019
 OS/Arch:           linux/amd64
 Experimental:      false

Server:
 Engine:
  Version:          18.09.2
  API version:      1.39 (minimum version 1.12)
  Go version:       go1.10.4
  Git commit:       6247962
  Built:            Tue Feb 12 22:47:29 2019
  OS/Arch:          linux/amd64
  Experimental:     false

</code></pre>
<h2>二、使用weave创建跨节点的overlay network</h2>
<p>我们选择<a href="https://www.weave.works/">weave</a>作为overlay network的实现。</p>
<h3>1. 安装weave</h3>
<p>我们在每个vm节点上安装目前最新版本的weave，以一个节点为例：</p>
<pre><code># curl -L git.io/weave -o /usr/local/bin/weave
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:--  0:00:01 --:--:--     0
  0     0    0     0    0     0      0      0 --:--:--  0:00:02 --:--:--     0
100   595    0   595    0     0     62      0 --:--:--  0:00:09 --:--:--   137
100 52227  100 52227    0     0   4106      0  0:00:12  0:00:12 --:--:-- 21187

# chmod a+x /usr/local/bin/weave

# weave version
weave script 2.5.1

... ...

</code></pre>
<p>通过weave setup预先将weave相关的容器Image下载到各个节点，为后面的weave launch所使用。</p>
<pre><code># weave setup

2.5.1: Pulling from weaveworks/weave
... ...
c458f7a37ca6: Pull complete
Digest: sha256:a170dd93fa7e678cc37919ffd65601d1015da6c3f10878534ac237381ea0db19
Status: Downloaded newer image for weaveworks/weave:2.5.1
2.5.1: Pulling from weaveworks/weaveexec
... ...
c11f30d06b58: Pull complete
Digest: sha256:ad53aaabf648548ec26cceac3ab49394778322e1623f0d184a2b74ad06338087
Status: Downloaded newer image for weaveworks/weaveexec:2.5.1
latest: Pulling from weaveworks/weavedb
9b0681f946a1: Pull complete
Digest: sha256:c280cf4e7208f4ca0d2514539e0f476dd12db70beacdc368793b7736de023d8d
Status: Downloaded newer image for weaveworks/weavedb:latest

</code></pre>
<h3>2. 启动跨多节点(peer) weave network</h3>
<p>weave的一个优点是建立跨节点overlay network时并不需要一个外部的存储(比如etcd），位于多个节点上的weave进程会自动同步相关信息。而且weave支持动态向weave overlay network中添加节点。</p>
<p>我们来初始化这个由三个vm节点构成的weave overlay network：</p>
<pre><code>root@u1:~# weave launch --no-dns 192.168.56.4 192.168.56.5
78f459a4a8acc07d46c1f86a15a519b91978c809876452b9d9c1294e760394a9

root@u2:~# weave launch --no-dns 192.168.56.3 192.168.56.5
1f379e50f3917e05bd133589f75594d7b2da20a680bb1e5e7172e37a18abe3ff

root@u3:~# weave launch --no-dns 192.168.56.3 192.168.56.4
aa600bfad8db8711e2cbc5f8e127022460ca3738226dd7aa33bb5b9b049f8cee

</code></pre>
<p>执行完上面命令后，在任意一个vm节点上执行下面命令，查看节点weave之间的连接状态：</p>
<pre><code>root@u1:~# weave status connections
&lt;- 192.168.56.4:54715    established fastdp 8e:d8:ad:a8:32:eb(u2) mtu=1376
&lt;- 192.168.56.5:51504    established fastdp f6:58:43:5c:68:d7(u3) mtu=1376

</code></pre>
<p>我们看到u1节点已经和u2、u3节点成功建立了连接，weave的工作模式是fastdp(fast data path)，mtu为默认的1376（<a href="https://tonybai.com/2019/04/18/benchmark-result-of-k8s-network-plugin-cni/">适当调节weave mtu可以提升weave overlay network的网络性能</a>）。<br />
我们也可以通过weave status命令查看一下weave网络的整体状态：</p>
<pre><code># weave status

        Version: 2.5.1 (up to date; next check at 2019/04/18 12:35:41)

        Service: router
       Protocol: weave 1..2
           Name: f6:58:43:5c:68:d7(u3)
     Encryption: disabled
  PeerDiscovery: enabled
        Targets: 3
    Connections: 3 (2 established, 1 failed)
          Peers: 3 (with 6 established connections)
 TrustedSubnets: none

        Service: ipam
         Status: ready
          Range: 10.32.0.0/12
  DefaultSubnet: 10.32.0.0/12

        Service: dns
         Domain: weave.local.
       Upstream: 10.0.3.3
            TTL: 1
        Entries: 0

        Service: proxy
        Address: unix:///var/run/weave/weave.sock

        Service: plugin (legacy)
     DriverName: weave

</code></pre>
<h3>3. 在weave overlay network中创建container并测试overlay网内container的互通性</h3>
<p>我们通过为docker指定net driver为weave的方式让docker在weave overlay network中创建container：</p>
<pre><code>root@u1:~# docker run -ti --net=weave busybox /bin/sh

root@u2:~# docker run -ti --net=weave busybox /bin/sh

root@u3:~# docker run -ti --net=weave busybox /bin/sh

</code></pre>
<p>我们在u1上启动的容器内去ping位于其他两个vm上启动的新容器：</p>
<pre><code>/ # ping -c 3 10.32.0.1
PING 10.32.0.1 (10.32.0.1): 56 data bytes
64 bytes from 10.32.0.1: seq=0 ttl=64 time=1.540 ms
64 bytes from 10.32.0.1: seq=1 ttl=64 time=1.548 ms
64 bytes from 10.32.0.1: seq=2 ttl=64 time=1.434 ms

--- 10.32.0.1 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 1.434/1.507/1.548 ms

/ # ping -c 3 10.46.0.0
PING 10.46.0.0 (10.46.0.0): 56 data bytes
64 bytes from 10.46.0.0: seq=0 ttl=64 time=5.118 ms
64 bytes from 10.46.0.0: seq=1 ttl=64 time=1.608 ms
64 bytes from 10.46.0.0: seq=2 ttl=64 time=1.837 ms

--- 10.46.0.0 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 1.608/2.854/5.118 ms

</code></pre>
<p>我们看到位于weave overlay network中的三个容器是连通的。</p>
<h3>4. 测试host到weave overlay网络中容器的连通性</h3>
<p>考虑到后续host上的<a href="https://tonybai.com/2018/09/10/setup-service-discovery-and-load-balance-based-on-consul/">consul</a>会对部署在weave overlay network中的container中的服务做health check，因此需要在host上能连通位于overlay network中的container。</p>
<p>我们来测试一下：</p>
<pre><code>root@u1:~# docker run -ti --net=weave busybox /bin/sh

/ # ip a
1: lo: &lt;LOOPBACK,UP,LOWER_UP&gt; mtu 65536 qdisc noqueue qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
29: ethwe0@if30: &lt;BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN&gt; mtu 1376 qdisc noqueue
    link/ether aa:8f:45:8f:5f:d6 brd ff:ff:ff:ff:ff:ff
    inet 10.40.0.0/12 brd 10.47.255.255 scope global ethwe0
       valid_lft forever preferred_lft forever
31: eth0@if32: &lt;BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN&gt; mtu 1500 qdisc noqueue
    link/ether 02:42:ac:12:00:02 brd ff:ff:ff:ff:ff:ff
    inet 172.18.0.2/16 brd 172.18.255.255 scope global eth0
       valid_lft forever preferred_lft forever

root@u1:~# ping 10.40.0.0
PING 10.40.0.0 (10.40.0.0) 56(84) bytes of data.

^C
--- 10.40.0.0 ping statistics ---
4 packets transmitted, 0 received, 100% packet loss, time 3024ms

</code></pre>
<p>从测试结果来看，在host无法ping通位于weave network上的container。这个问题实则也显而易见，因为当前host上的路由表中没有以weave网络range: 10.32.0.0/12为目的地址的路由，并且weave网络设备也并未启用ip地址：</p>
<pre><code>root@u1:~# ip route
default via 10.0.3.2 dev enp0s8
10.0.3.0/24 dev enp0s8  proto kernel  scope link  src 10.0.3.15
172.17.0.0/16 dev docker0  proto kernel  scope link  src 172.17.0.1
172.18.0.0/16 dev docker_gwbridge  proto kernel  scope link  src 172.18.0.1
192.168.56.0/24 dev enp0s3  proto kernel  scope link  src 192.168.56.3

</code></pre>
<p>关于这个问题，weave官方给出了<a href="https://www.weave.works/docs/net/latest/tasks/manage/host-network-integration/">答案</a>：我们可以通过weave expose命令自动为主机上的weave设备分配ip地址，添加到10.32.0.0/12的路由。</p>
<pre><code>root@u1:~# weave expose
10.40.0.1

root@u1:~# ip a

.... ...

7: weave: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1376 qdisc noqueue state UP group default qlen 1000
    link/ether b2:97:b5:7b:0f:a9 brd ff:ff:ff:ff:ff:ff
    inet 10.40.0.1/12 brd 10.47.255.255 scope global weave
       valid_lft forever preferred_lft forever
    inet6 fe80::b097:b5ff:fe7b:fa9/64 scope link
       valid_lft forever preferred_lft forever

.... ...

root@u1:~# ip route
default via 10.0.3.2 dev enp0s8
10.0.3.0/24 dev enp0s8  proto kernel  scope link  src 10.0.3.15
10.32.0.0/12 dev weave  proto kernel  scope link  src 10.40.0.1
172.17.0.0/16 dev docker0  proto kernel  scope link  src 172.17.0.1
172.18.0.0/16 dev docker_gwbridge  proto kernel  scope link  src 172.18.0.1
192.168.56.0/24 dev enp0s3  proto kernel  scope link  src 192.168.56.3

</code></pre>
<p>我们看到在u1节点上执行完expose之后，weave设备拥有了自己的ip地址，并且主机路由表中也增加了10.32.0.0/12网络的路由。我们再来测试一下u1上主机到container是否通了：</p>
<pre><code>root@u1:~# ping 10.40.0.0
PING 10.40.0.0 (10.40.0.0) 56(84) bytes of data.
64 bytes from 10.40.0.0: icmp_seq=1 ttl=64 time=4.42 ms

64 bytes from 10.40.0.0: icmp_seq=2 ttl=64 time=1.04 ms
64 bytes from 10.40.0.0: icmp_seq=3 ttl=64 time=1.21 ms
^C
--- 10.40.0.0 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 1.048/2.228/4.425/1.554 ms

</code></pre>
<p>网络已经打通。我们继续在u2、u3两个节点上执行weave expose，这样三台主机都可以通过网络reach到位于任何一台主机上的、weave network中的container。</p>
<p>而从container到host，原本就可以访问，以u1上的container为例：</p>
<pre><code>/ # ping 192.168.56.3
PING 192.168.56.3 (192.168.56.3): 56 data bytes
64 bytes from 192.168.56.3: seq=0 ttl=64 time=0.345 ms
^C
--- 192.168.56.3 ping statistics ---
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min/avg/max = 0.345/0.345/0.345 ms

/ # ping 192.168.56.4
PING 192.168.56.4 (192.168.56.4): 56 data bytes
64 bytes from 192.168.56.4: seq=0 ttl=63 time=1.277 ms
^C
--- 192.168.56.4 ping statistics ---
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min/avg/max = 1.277/1.277/1.277 ms

</code></pre>
<h2>三、安装consul和nomad集群</h2>
<p>在<a href="https://tonybai.com/2019/03/30/cluster-management-and-microservice-deployment-and-scheduled-by-nomad/">《使用nomad实现集群管理和微服务部署调度》</a>一文中，我们已经详细说过consul和nomad的安装配置过程，这里仅列出步骤，不再详细说明。已经有环境的朋友可以略过该步骤！</p>
<h3>1. 安装consul</h3>
<p>在每个节点上执行下面步骤安装：</p>
<pre><code># wget -c https://releases.hashicorp.com/consul/1.4.4/consul_1.4.4_linux_amd64.zip
# unzip consul_1.4.4_linux_amd64.zip
# mv consul /usr/local/bin

# mkdir -p ~/consul-install/consul-data

</code></pre>
<p>启动consul集群：</p>
<pre><code>u1:

# nohup consul agent -server -ui -dns-port=53 -bootstrap-expect=3 -data-dir=/root/consul-install/consul-data -node=consul-1 -client=0.0.0.0 -bind=192.168.56.3 -datacenter=dc1 &gt; consul-1.log &amp; 2&gt;&amp;1

u2:

# nohup consul agent -server -ui -dns-port=53 -bootstrap-expect=3 -data-dir=/root/consul-install/consul-data -node=consul-2 -client=0.0.0.0 -bind=192.168.56.4 -datacenter=dc1 -join 192.168.56.3 &gt; consul-2.log &amp; 2&gt;&amp;1

u3:

nohup consul agent -server -ui -dns-port=53  -bootstrap-expect=3 -data-dir=/root/consul-install/consul-data -node=consul-3 -client=0.0.0.0 -bind=192.168.56.5 -datacenter=dc1 -join 192.168.56.3 &gt; consul-3.log &amp; 2&gt;&amp;1
</code></pre>
<p>查看启动状态：</p>
<pre><code>#  consul operator raft list-peers
Node      ID                                    Address            State     Voter  RaftProtocol
consul-1  db838e7c-2b02-949b-763b-a6646ee51981  192.168.56.3:8300  leader    true   3
consul-2  33c81139-5054-7e76-f320-7d28d7528cc8  192.168.56.4:8300  follower  true   3
consul-3  4eda7d24-3fe2-45f5-f4ad-b95fa39f13c1  192.168.56.5:8300  follower  true   3

</code></pre>
<p>如果输出类似上面的日志，则说明consul集群启动成功！</p>
<p>接下来为了利用consul内嵌的DNS server，我们修改一下各个node的DNS配置 /etc/resolvconf/resolv.conf.d/base：</p>
<pre><code>//  /etc/resolvconf/resolv.conf.d/base

nameserver 192.168.56.3
nameserver 192.168.56.4

options timeout:2 attempts:3 rotate single-request-reopen

# /etc/init.d/resolvconf restart

[ ok ] Restarting resolvconf (via systemctl): resolvconf.service.
</code></pre>
<h3>2. 安装nomad并启动nomad集群</h3>
<p>下面是在每个node上安装nomad的步骤：</p>
<pre><code># wget -c https://releases.hashicorp.com/nomad/0.8.7/nomad_0.8.7_linux_amd64.zip

# mkdir nomad-install

# unzip nomad_0.8.7_linux_amd64.zip

# mv nomad /usr/local/bin

# nomad version
Nomad v0.8.7 (21a2d93eecf018ad2209a5eab6aae6c359267933+CHANGES)

</code></pre>
<p>在每个node上创建agent.hcl文件，放到nomad-install下面：</p>
<pre><code>// agent.hcl

data_dir = "/root/nomad-install/nomad.d"

bind_addr = "192.168.56.3" //node 内网ip，这里以u1 host为例

server {
  enabled = true
  bootstrap_expect = 3
}

client {
  enabled = true
}

</code></pre>
<p>启动集群(基于consul)：</p>
<pre><code>u1:

# nohup nomad agent -config=/root/nomad-install/agent.hcl  &gt; nomad-1.log &amp; 2&gt;&amp;1

u2:

# nohup nomad agent -config=/root/nomad-install/agent.hcl  &gt; nomad-2.log &amp; 2&gt;&amp;1

u3:

# nohup nomad agent -config=/root/nomad-install/agent.hcl  &gt; nomad-3.log &amp; 2&gt;&amp;1

</code></pre>
<p>查看nomad集群状态：</p>
<pre><code># nomad server members -address="http://192.168.56.3:4646"
Name       Address       Port  Status  Leader  Protocol  Build  Datacenter  Region
u1.global  192.168.56.3  4648  alive   false   2         0.8.7  dc1         global
u2.global  192.168.56.4  4648  alive   true    2         0.8.7  dc1         global
u3.global  192.168.56.5  4648  alive   false   2         0.8.7  dc1         global

# nomad operator raft list-peers -address="http://192.168.56.3:4646"
Node       ID                 Address            State     Voter  RaftProtocol
u3.global  192.168.56.5:4647  192.168.56.5:4647  follower  true   2
u2.global  192.168.56.4:4647  192.168.56.4:4647  leader    true   2
u1.global  192.168.56.3:4647  192.168.56.3:4647  follower  true   2

</code></pre>
<p>nomad集群启动成功！</p>
<h2>四. nomad实现在weave overlay network中的job部署</h2>
<h3>1. 创建位于weave overlay network中的nomad task service实例</h3>
<p>我们定义如下nomad job的配置文件：</p>
<pre><code>//httpbackend.nomad

job "httpbackend" {
  datacenters = ["dc1"]
  type = "service"

  group "httpbackend" {
    count = 3

    task "httpbackend" {
      driver = "docker"
      config {
        image = "bigwhite/httpbackendservice:v1.0.0"
        dns_servers =  ["192.168.56.3", "192.168.56.4", "192.168.56.5"]
        network_mode = "weave"
        logging {
          type = "json-file"
        }
      }

      resources {
        network {
          mbits = 10
        }
      }

      service {
        name = "httpbackend"
      }
    }
  }
}

</code></pre>
<p>与之前文章中job的配置文件不同的是，该job配置在task的config中增加了：</p>
<ul>
<li>
<p>dns_servers：由于docker 18.09在-net=weave下，container没有继承host的/etc/resolv.conf文件，我们为了能在container中通过服务的domain查询到其真实ip地址，我们在docker的执行参数中加入dns_servers，我们将u1,u2,u3都作为dns server提供了。</p>
</li>
<li>
<p>network_node：我们希望nomad调度负载、创建docker容器时将docker container创建在weave network中，因此我们在network_node中传入”weave”，这就相当于在执行docker时执行：docker run &#8230; &#8211;net=weave &#8230; &#8230;</p>
</li>
</ul>
<p>我们来创建一下该job：</p>
<pre><code># nomad job run -address=http://192.168.56.3:4646 httpbackend.nomad

==&gt; Monitoring evaluation "806eaecf"
    Evaluation triggered by job "httpbackend"
    Allocation "6e06be74" created: node "11212ed9", group "httpbackend"
    Allocation "e7ed8569" created: node "aa5a06fe", group "httpbackend"
    Allocation "fd6c6a05" created: node "fe7a7e9c", group "httpbackend"
    Evaluation status changed: "pending" -&gt; "complete"
==&gt; Evaluation "806eaecf" finished with status "complete"

# nomad job status -address=http://192.168.56.3:4646  httpbackend
ID            = httpbackend
Name          = httpbackend
Submit Date   = 2019-04-19T13:18:21+08:00
Type          = service
Priority      = 50
Datacenters   = dc1
Status        = running
Periodic      = false
Parameterized = false

Summary
Task Group   Queued  Starting  Running  Failed  Complete  Lost
httpbackend  0       0         3        0       0         0

Allocations
ID        Node ID   Task Group   Version  Desired  Status   Created  Modified
6e06be74  11212ed9  httpbackend  0        run      running  54s ago  7s ago
e7ed8569  aa5a06fe  httpbackend  0        run      running  54s ago  6s ago
fd6c6a05  fe7a7e9c  httpbackend  0        run      running  54s ago  12s ago

</code></pre>
<p>我们查看一下u1节点上的httpbackend负载的状态和ip：</p>
<pre><code>root@u1:~/nomad-install/jobs# docker ps
CONTAINER ID        IMAGE                    COMMAND                  CREATED             STATUS              PORTS               NAMES
2e2229cf8f64        c196c122feea             "/root/httpbackendse…"   49 seconds ago      Up 48 seconds                           httpbackend-e7ed8569-fdde-537b-91b3-84583d1ea238
912ac43350f7        weaveworks/weave:2.5.1   "/home/weave/weaver …"   22 hours ago        Up 22 hours                             weave

root@u1:~/nomad-install/jobs# docker exec 2e2229cf8f64 ip a
... ...
49: ethwe0@if50: &lt;BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN&gt; mtu 1376 qdisc noqueue
    link/ether a2:f1:ef:d7:89:ee brd ff:ff:ff:ff:ff:ff
    inet 10.40.0.0/12 brd 10.47.255.255 scope global ethwe0
       valid_lft forever preferred_lft forever
.... ...

</code></pre>
<p>我们看到新创建的container的ip为10.40.0.0，是weave network subnet range中的一个地址。</p>
<p>我们访问一下该服务：</p>
<pre><code># curl http://10.40.0.0:8081
this is httpbackendservice, version: v1.0.0

</code></pre>
<p>我们看到了预期返回的结果。通过consul的域名访问也同样ok：</p>
<pre><code># curl httpbackend.service.dc1.consul:8081
this is httpbackendservice, version: v1.0.0

</code></pre>
<p>我们从一个位于weave network中的container中去访问httpbackend服务，依然会得到正确的应答结果：</p>
<pre><code># docker run -ti --net=weave --dns=192.168.56.3 --dns=8.8.8.8 ubuntu /bin/bash

root@3fe76a39b66f:/# curl httpbackend.service.dc1.consul:8081
this is httpbackendservice, version: v1.0.0

</code></pre>
<h2>五、 应用隔离</h2>
<p>有些时候我们需要将部署的应用之间做隔离，让彼此无法互相访问。weave overlay network是支持这样做的，我们一起来看一下。</p>
<h3>1.重建weave网络</h3>
<p>我们首先需要重新创建weave网络，使之能支持划分不同subnet。</p>
<p>先在每个node上执行下面命令，将原有的weave网络清理干净：</p>
<pre><code># weave reset

</code></pre>
<p>执行后，发现weave网络设备、weave相关容器、路由表中有关weave的路由都不见了。</p>
<p>我们重新建立三节点的weave网络，在这个10.32.0.0/16的大网中，我们划分若干subnet，默认的subnet为10.32.0.0/24。</p>
<pre><code>u1:

# weave launch --no-dns --ipalloc-range 10.32.0.0/16 --ipalloc-default-subnet 10.32.0.0/24 192.168.56.4 192.168.56.5

# weave expose

u2:

# weave launch --no-dns --ipalloc-range 10.32.0.0/16 --ipalloc-default-subnet 10.32.0.0/24 192.168.56.3 192.168.56.5

# weave expose

u3:

# weave launch --no-dns --ipalloc-range 10.32.0.0/16 --ipalloc-default-subnet 10.32.0.0/24 192.168.56.3 192.168.56.4

# weave expose

</code></pre>
<p>接下来我们在不同的subnet下分别建立两个container：</p>
<p>首先在u1上，在default subnet下建立两个container a1和a2：</p>
<pre><code>#docker run -ti --net=weave --dns=192.168.56.3 --dns=8.8.8.8 --name a1 busybox /bin/sh

#docker run -ti --net=weave --dns=192.168.56.3 --dns=8.8.8.8 --name a2 busybox /bin/sh

</code></pre>
<p>再在u2上在subnet 10.32.1.0/24下建立两个container：b1和b2</p>
<pre><code>u2上：

# docker run -ti --net=weave --dns=192.168.56.3 --dns=8.8.8.8 -e WEAVE_CIDR=net:10.32.1.0/24 --name b1 busybox /bin/sh

# docker run -ti --net=weave --dns=192.168.56.3 --dns=8.8.8.8 -e WEAVE_CIDR=net:10.32.1.0/24 --name b2 busybox /bin/sh

</code></pre>
<p>我们经过测试发现：a1与a2、a1与b1都是可以ping通的，这与我们的预期a1与b1、b2不通不符。我们发现b1(10.32.0.2)、b2(10.32.0.3)两个容器的ip地址居然依然在default subnet内，似乎通过环境变量WEAVE_CIDR传递的subnet信息没有生效。<br />
在weave的一个<a href="https://github.com/weaveworks/weave/issues/2420">issue</a>中，有开发者提到：WEAVE_CIDR仅用于weave proxy模式，在weave作为plugin模式工作时，docker不会将该环境变量信息传递给weave。也就是说即便上面在u2上创建b1、b2时设置了环境变量WEAVE_CIDR，weave插件也无法得到该信息，于是依旧在默认subnet范围为b1、b2分配了ip。</p>
<h3>2. 让docker使用weave proxy模式</h3>
<p>weave proxy是位于docker client与docker engine(docker daemon)之间的代理服务：</p>
<pre><code>docker client --&gt; weave proxy ---&gt; docker engine/daemon

</code></pre>
<p>默认情况下，/var/run/docker.sock是docker client和docker engine之间的通信“媒介”，Docker daemon默认监听的Unix域套接字(Unix domain socket)：/var/run/docker.sock，docker client以及容器中的进程可以通过它与Docker daemon进行通信。</p>
<p>我们可通过docker -H xxx.sock或通过设置 DOCKER_HOST环境变量的方式让docker client与传入的unix socket通信。这样我们就可以将weave proxy的套接字unix:///var/run/weave/weave.sock（通过weave env查看到）传给docker client了。我们来测试一下：</p>
<pre><code>u1:

# docker -H unix:///var/run/weave/weave.sock run -ti --dns=192.168.56.3 --dns=8.8.8.8 --name a1 busybox /bin/sh

# docker -H unix:///var/run/weave/weave.sock run -ti --dns=192.168.56.3 --dns=8.8.8.8 --name a2 busybox /bin/sh

u2:

# docker -H unix:///var/run/weave/weave.sock  run -ti --dns=192.168.56.3 --dns=8.8.8.8 -e WEAVE_CIDR=net:10.32.1.0/24 --name b1 busybox /bin/sh

#docker -H unix:///var/run/weave/weave.sock run -ti --dns=192.168.56.3 --dns=8.8.8.8 -e WEAVE_CIDR=net:10.32.1.0/24 --name b2 busybox /bin/sh

</code></pre>
<p>四个container启动后，我们发现b1、b2的ip地址都在WEAVE_CIDR指定的空间内，a1、a2间互通；b1、b2间互通，但a1、a2与b1、b2间是不通的。这样就与预期相符了。</p>
<h3>3. nomad与weave proxy模式集成实现应用工作负载的隔离</h3>
<p>接下来，我们来看看如何将nomad和weave的proxy模式集成在一起，实现工作负载分配在不同subnet。</p>
<p>这里我们就无法仅仅通过在job配置文件中传入参数的方式来实现了，我们需要修改一下agent.hcl并重启nomad集群。以u1节点上的agent.hcl为例，我们需要改为下面这样：</p>
<pre><code>data_dir = "/root/nomad-install/nomad.d"

bind_addr = "192.168.56.5"

server {
  enabled = true
  bootstrap_expect = 3
}

client {
  enabled = true
  "options":{
     "docker.endpoint":"unix://var/run/weave/weave.sock"
  }
}

</code></pre>
<p>我们在client配置block中增加一个options，设置了docker.endpoint为weave proxy监听的weave.sock。重启集群：</p>
<pre><code>u1:

# nohup nomad agent -config=/root/nomad-install/agent.hcl  &gt; nomad-1.log &amp; 2&gt;&amp;1

u2:

# nohup nomad agent -config=/root/nomad-install/agent.hcl  &gt; nomad-2.log &amp; 2&gt;&amp;1

u3:

# nohup nomad agent -config=/root/nomad-install/agent.hcl  &gt; nomad-3.log &amp; 2&gt;&amp;1

</code></pre>
<p>接下来，我们重建一个httpbackend-another-subnet.nomad，内容如下：</p>
<pre><code>//httpbackend-another-subnet.nomad

job "httpbackend" {
  datacenters = ["dc1"]
  type = "service"

  group "httpbackend" {
    count = 3

    task "httpbackend" {
      driver = "docker"
      config {
        image = "bigwhite/httpbackendservice:v1.0.0"
        dns_servers =  ["192.168.56.3", "192.168.56.4", "192.168.56.5"]
        logging {
          type = "json-file"
        }
      }

      env {
        WEAVE_CIDR="net:10.32.1.0/24"
      }

      resources {
        network {
          mbits = 10
        }
      }

      service {
        name = "httpbackend"
      }
    }
  }
}

</code></pre>
<p>我们去掉了network_mode = “weave”，增加了一个env：WEAVE_CIDR=”net:10.32.1.0/24&#8243;。run这个job：</p>
<pre><code># nomad job run -address=http://192.168.56.3:4646 httpbackend-another-subnet.nomad
==&gt; Monitoring evaluation "e94bdd00"
    Evaluation triggered by job "httpbackend"
    Allocation "3f5032b5" created: node "11212ed9", group "httpbackend"
    Allocation "40d75ae8" created: node "aa5a06fe", group "httpbackend"
    Allocation "627fe1e7" created: node "fe7a7e9c", group "httpbackend"
    Evaluation status changed: "pending" -&gt; "complete"
==&gt; Evaluation "e94bdd00" finished with status "complete"

# docker ps
CONTAINER ID        IMAGE                    COMMAND                  CREATED             STATUS              PORTS               NAMES
700bbea7c89e        c196c122feea             "/w/w /root/httpback…"   17 seconds ago      Up 16 seconds                           httpbackend-40d75ae8-fe75-c560-b87b-c1272db4850c
8b7e29522b8b        weaveworks/weave:2.5.1   "/home/weave/weaver …"   10 hours ago        Up 10 hours                             weave
root@u1:~/nomad-install/jobs# docker exec 700bbea7c89e ip a
1: lo: &lt;LOOPBACK,UP,LOWER_UP&gt; mtu 65536 qdisc noqueue qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
142: eth0@if143: &lt;BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN&gt; mtu 1500 qdisc noqueue
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever
144: ethwe@if145: &lt;BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN&gt; mtu 1376 qdisc noqueue
    link/ether f2:55:9d:26:72:56 brd ff:ff:ff:ff:ff:ff
    inet 10.32.1.192/24 brd 10.32.1.255 scope global ethwe
       valid_lft forever preferred_lft forever

</code></pre>
<p>我们看到新创建的httpbackend container的ip已经分配到10.32.1.0/24 subnet下面了。这种方式使得我们可以任意安排我们的job放入哪个subnet。</p>
<h3>4. 遗留问题</h3>
<p>我们通过consul go api试图从consul中获取service: httpbackend的ip信息，我们得到了如下的输出：</p>
<pre><code>#  ./services
10.0.3.15 : 0
10.0.3.15 : 0
10.0.3.15 : 0
[]

</code></pre>
<p>如果在httpbackend的service配置中使用如下配置：</p>
<pre><code> service {
        name = "httpbackend"
        address_mode = "driver"
      }

</code></pre>
<p>那么，我们得到的是下面结果：</p>
<pre><code># ./services
172.17.0.3 : 0
172.17.0.2 : 0
172.17.0.2 : 0
[]

</code></pre>
<p>也就是说nomad在consul中记录的container的advertise ip不是我们想要的weave subnet网段的ip信息，这样就会导致我们通过consul的DNS服务或者通过consul api获取的服务ip信息有误，导致无法通过这两种方式访问到服务实例。在nomad的最新版v0.9.0中该问题依然存在。</p>
<p>综上，“隔离”的目的得到了部分满足，期待后续nomad的改进。</p>
<h2>六、参考资料</h2>
<ul>
<li>
<p>https://www.weave.works/docs/net/latest/install/installing-weave/</p>
</li>
<li>
<p>https://www.weave.works/docs/net/latest/install/using-weave/#peer-connections</p>
</li>
<li>
<p>https://www.weave.works/docs/net/latest/install/plugin/plugin/#launching</p>
</li>
<li>
<p>https://www.weave.works/docs/net/latest/tasks/manage/host-network-integration/</p>
</li>
<li>
<p>https://docs.docker.com/v17.09/engine/userguide/networking/configure-dns/</p>
</li>
<li>
<p>https://www.nomadproject.io/docs/drivers/docker.html#client-requirements</p>
</li>
<li>
<p>https://www.weave.works/docs/net/latest/tasks/manage/application-isolation/</p>
</li>
<li>
<p>https://www.weave.works/docs/net/latest/tasks/weave-docker-api/weave-docker-api/</p>
</li>
<li>
<p>https://www.nomadproject.io/docs/drivers/docker.html</p>
</li>
<li>
<p>https://www.nomadproject.io/docs/configuration/client.html</p>
</li>
<li>
<p>https://www.nomadproject.io/docs/job-specification/service.html#using-driver-address-mode</p>
</li>
<li>
<p>https://success.docker.com/article/networking</p>
</li>
</ul>
<p>本文涉及到的配置文件和源码，参见<a href="https://github.com/bigwhite/experiments/tree/master/nomad-demo/part3">这里</a>。</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/04/20/deploy-workload-in-weave-network-using-nomad/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>使用nomad实现集群管理和微服务部署调度</title>
		<link>https://tonybai.com/2019/03/30/cluster-management-and-microservice-deployment-and-scheduled-by-nomad/</link>
		<comments>https://tonybai.com/2019/03/30/cluster-management-and-microservice-deployment-and-scheduled-by-nomad/#comments</comments>
		<pubDate>Sat, 30 Mar 2019 11:21:09 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[cloud-native]]></category>
		<category><![CDATA[consul]]></category>
		<category><![CDATA[curl]]></category>
		<category><![CDATA[DaemonSet]]></category>
		<category><![CDATA[deployment]]></category>
		<category><![CDATA[dig]]></category>
		<category><![CDATA[DNS]]></category>
		<category><![CDATA[eBay]]></category>
		<category><![CDATA[fabio]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[group]]></category>
		<category><![CDATA[hashicorp]]></category>
		<category><![CDATA[hcl]]></category>
		<category><![CDATA[http]]></category>
		<category><![CDATA[https]]></category>
		<category><![CDATA[imooc]]></category>
		<category><![CDATA[istio]]></category>
		<category><![CDATA[job]]></category>
		<category><![CDATA[k8s]]></category>
		<category><![CDATA[kubeadm]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[loadbalance]]></category>
		<category><![CDATA[nomad]]></category>
		<category><![CDATA[pod]]></category>
		<category><![CDATA[Scheduler]]></category>
		<category><![CDATA[Service]]></category>
		<category><![CDATA[ServiceMesh]]></category>
		<category><![CDATA[sni]]></category>
		<category><![CDATA[SSL]]></category>
		<category><![CDATA[task]]></category>
		<category><![CDATA[TLS]]></category>
		<category><![CDATA[Trace]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[vault]]></category>
		<category><![CDATA[weave]]></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=2689</guid>
		<description><![CDATA[在“云原生”、“容器化”、“微服务”、“服务网格”等概念大行其道的今天，一提到集群管理、容器工作负载调度，人们首先想到的是Kubernetes。 Kubernetes经过多年的发展，目前已经成为了云原生计算平台的事实标准，得到了诸如谷歌、微软、红帽、亚马逊、IBM、阿里等大厂的大力支持，各大云计算提供商也都提供了专属Kubernetes集群服务。开发人员可以一键在这些大厂的云上创建k8s集群。对于那些不愿被cloud provider绑定的组织或开发人员，Kubernetes也提供了诸如Kubeadm这样的k8s集群引导工具，帮助大家在裸金属机器上搭建自己的k8s集群，当然这样做的门槛较高（如果您想学习自己搭建和管理k8s集群，可以参考我在慕课网上发布的实战课《高可用集群搭建、配置、运维与应用》）。 Kubernetes的学习曲线是公认的较高，尤其是对于应用开发人员。再加上Kubernetes发展很快，越来越多的概念和功能加入到k8s技术栈，这让人们不得不考虑建立和维护这样一套集群所要付出的成本。人们也在考虑是否所有场景都需要部署一个k8s集群，是否有轻量级的且能满足自身需求的集群管理和微服务部署调度方案呢？外国朋友Matthias Endler就在其文章《也许你不需要Kubernetes》中给出一个轻量级的集群管理方案 &#8211; 使用hashicorp开源的nomad工具。 这让我想起了去年写的《基于consul实现微服务的服务发现和负载均衡》一文。文中虽然实现了基于consul的服务注册、发现以及负载均衡，但是缺少一个环节：那就是整个集群管理以及工作负载部署调度自动化的缺乏。nomad应该恰好可以补足这一短板，并且它足够轻量。本文我们就来探索和实践一下使用nomad实现集群管理和微服务部署调度。 一. 安装nomad集群 nomad是Hashicorp公司出品的集群管理和工作负荷调度器，支持多种驱动形式的工作负载调度，包括Docker容器、虚拟机、原生可执行程序等，并支持跨数据中心调度。Nomad不负责服务发现或密钥管理等 ，它将这些功能分别留给了HashiCorp的Consul和Vault。HashiCorp的创始人认为，这会使得Nomad更为轻量级，调度性能更高。 nomad使用Go语言实现，因此其本身仅仅是一个可执行的二进制文件。和Hashicorp其他工具产品(诸如：consul等)类似，nomad一个可执行文件既可以以server模式运行，亦可以client模式运行，甚至可以启动一个实例，既是server，也是client。 下面是nomad集群的架构图(来自hashicorp官方）: 一个nomad集群至少要包含一个server，作为集群的控制平面；一个或多个client则用于承载工作负荷。通常生产环境nomad集群的控制平面至少要有5个及以上的server才能在高可用上有一定保证。 建立一个nomad集群有多种方法，包括手工建立、基于consul自动建立和基于云自动建立。考虑到后续涉及微服务的注册发现，这里我们采用基于consul自动建立nomad集群的方法，下面是部署示意图： 我这里的试验环境仅有三台hosts，因此这三台host既承载consul集群，也承载nomad集群（包括server和client），即nomad的控制平面和工作负荷由这三台host一并承担了。 1. consul集群启动 在之前的《基于consul实现微服务的服务发现和负载均衡》一文中，我对consul集群的建立做过详细地说明，因此这里只列出步骤，不详细解释了。注意：这次consul的版本升级到了consul v1.4.4了。 在每个node上分别下载consul 1.4.4： # wget -c https://releases.hashicorp.com/consul/1.4.4/consul_1.4.4_linux_amd64.zip # unzip consul_1.4.4_linux_amd64.zip # cp consul /usr/local/bin # consul -v Consul v1.4.4 Protocol 2 spoken by default, understands 2 to 3 (agent will automatically use protocol &#62;2 when [...]]]></description>
			<content:encoded><![CDATA[<p>在<a href="https://www.cncf.io/">“云原生”</a>、<a href="https://tonybai.com/tag/docker">“容器化”</a>、<a href="https://en.wikipedia.org/wiki/Microservices">“微服务”</a>、<a href="https://tonybai.com/2018/01/03/an-intro-of-microservices-governance-by-istio/">“服务网格”</a>等概念大行其道的今天，一提到集群管理、容器工作负载调度，人们首先想到的是<a href="https://tonybai.com/tag/kubernetes">Kubernetes</a>。</p>
<p><a href="https://kubernetes.io/">Kubernetes</a>经过多年的发展，目前已经成为了云原生计算平台的事实标准，得到了诸如谷歌、微软、红帽、亚马逊、IBM、阿里等大厂的大力支持，各大云计算提供商也都提供了专属Kubernetes集群服务。开发人员可以<strong>一键</strong>在这些大厂的云上<a href="https://tonybai.com/2017/05/15/setup-a-ha-kubernetes-cluster-based-on-kubeadm-part1/">创建k8s集群</a>。对于那些不愿被cloud provider绑定的组织或开发人员，Kubernetes也提供了诸如<a href="https://tonybai.com/2017/05/15/setup-a-ha-kubernetes-cluster-based-on-kubeadm-part1/">Kubeadm</a>这样的k8s集群引导工具，帮助大家在裸金属机器上<a href="https://tonybai.com/2018/10/17/imooc-course-kubernetes-practice-go-online/">搭建自己的k8s集群</a>，当然这样做的门槛较高（如果您想学习自己搭建和管理k8s集群，可以参考我在<a href="https://www.imooc.com/">慕课网</a>上发布的实战课<a href="https://coding.imooc.com/class/284.html">《高可用集群搭建、配置、运维与应用》</a>）。</p>
<p>Kubernetes的学习曲线是公认的较高，尤其是对于应用开发人员。再加上Kubernetes发展很快，越来越多的概念和功能加入到k8s技术栈，这让人们不得不考虑建立和维护这样一套集群所要付出的成本。人们也在考虑是否所有场景都需要部署一个k8s集群，是否有轻量级的且能满足自身需求的集群管理和微服务部署调度方案呢？外国朋友Matthias Endler就在其文章<a href="https://matthias-endler.de/2019/maybe-you-dont-need-kubernetes/">《也许你不需要Kubernetes》</a>中给出一个轻量级的集群管理方案 &#8211; 使用<a href="https://www.hashicorp.com/">hashicorp</a>开源的<a href="https://github.com/hashicorp/nomad">nomad工具</a>。</p>
<p>这让我想起了去年写的<a href="https://tonybai.com/2018/09/10/setup-service-discovery-and-load-balance-based-on-consul/">《基于consul实现微服务的服务发现和负载均衡》</a>一文。文中虽然实现了基于<a href="https://tonybai.com/2015/07/06/implement-distributed-services-registery-and-discovery-by-consul/">consul</a>的服务注册、发现以及负载均衡，但是缺少一个环节：那就是整个集群管理以及工作负载部署调度自动化的缺乏。nomad应该恰好可以补足这一短板，并且它足够轻量。本文我们就来探索和实践一下使用<a href="https://github.com/hashicorp/nomad">nomad</a>实现集群管理和微服务部署调度。</p>
<h2>一. 安装nomad集群</h2>
<p>nomad是Hashicorp公司出品的集群管理和工作负荷调度器，支持多种驱动形式的工作负载调度，包括<a href="https://tonybai.com/tag/docker">Docker容器</a>、虚拟机、原生可执行程序等，并支持跨数据中心调度。Nomad不负责服务发现或密钥管理等 ，它将这些功能分别留给了HashiCorp的<a href="https://tonybai.com/tag/consul">Consul</a>和<a href="https://github.com/hashicorp/vault">Vault</a>。HashiCorp的创始人认为，这会使得Nomad更为轻量级，调度性能更高。</p>
<p>nomad使用<a href="https://tonybai.com/tag/golang">Go语言</a>实现，因此其本身仅仅是一个可执行的二进制文件。和Hashicorp其他工具产品(诸如：consul等)类似，nomad一个可执行文件既可以以server模式运行，亦可以client模式运行，甚至可以启动一个实例，既是server，也是client。</p>
<p>下面是nomad集群的架构图(来自hashicorp官方）:</p>
<p><img src="https://tonybai.com/wp-content/uploads/nomad-arch.png" alt="img{512x368}" /></p>
<p>一个nomad集群至少要包含一个server，作为集群的控制平面；一个或多个client则用于承载工作负荷。通常生产环境nomad集群的控制平面至少要有5个及以上的server才能在高可用上有一定保证。</p>
<p>建立一个nomad集群有多种方法，包括手工建立、基于consul自动建立和基于云自动建立。考虑到后续涉及微服务的注册发现，这里我们采用基于consul自动建立nomad集群的方法，下面是部署示意图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/nomad-cluster-bootstrap-from-consul-cluster-on-ubuntu-hosts.png" alt="img{512x368}" /></p>
<p>我这里的试验环境仅有三台hosts，因此这三台host既承载consul集群，也承载nomad集群（包括server和client），即nomad的控制平面和工作负荷由这三台host一并承担了。</p>
<h3>1. consul集群启动</h3>
<p>在之前的<a href="https://tonybai.com/2018/09/10/setup-service-discovery-and-load-balance-based-on-consul/">《基于consul实现微服务的服务发现和负载均衡》</a>一文中，我对consul集群的建立做过详细地说明，因此这里只列出步骤，不详细解释了。注意：这次consul的版本升级到了consul v1.4.4了。</p>
<p>在每个node上分别下载consul 1.4.4：</p>
<pre><code># wget -c https://releases.hashicorp.com/consul/1.4.4/consul_1.4.4_linux_amd64.zip
# unzip consul_1.4.4_linux_amd64.zip

# cp consul /usr/local/bin

# consul -v

Consul v1.4.4
Protocol 2 spoken by default, understands 2 to 3 (agent will automatically use protocol &gt;2 when speaking to compatible agents)

</code></pre>
<p>启动consul集群：(每个node上创建~/.bin/consul-install目录，并进入该目录下执行)</p>
<pre><code>dxnode1:

# nohup consul agent -server -ui -dns-port=53 -bootstrap-expect=3 -data-dir=~/.bin/consul-install/consul-data -node=consul-1 -client=0.0.0.0 -bind=172.16.66.102 -datacenter=dc1 &gt; consul-1.log &amp; 2&gt;&amp;1

dxnode2:

# nohup consul agent -server -ui -dns-port=53  -bootstrap-expect=3 -data-dir=/root/consul-install/consul-data -node=consul-2 -client=0.0.0.0 -bind=172.16.66.103 -datacenter=dc1 -join 172.16.66.102 &gt; consul-2.log &amp; 2&gt;&amp;1

dxnode3:

nohup consul agent -server -ui -dns-port=53  -bootstrap-expect=3 -data-dir=/root/consul-install/consul-data -node=consul-3 -client=0.0.0.0 -bind=172.16.66.104 -datacenter=dc1 -join 172.16.66.102 &gt; consul-3.log &amp; 2&gt;&amp;1

</code></pre>
<p>consul集群启动结果查看如下：</p>
<pre><code># consul members
Node      Address             Status  Type    Build  Protocol  DC   Segment
consul-1  172.16.66.102:8301  alive   server  1.4.4  2         dc1  &lt;all&gt;
consul-2  172.16.66.103:8301  alive   server  1.4.4  2         dc1  &lt;all&gt;
consul-3  172.16.66.104:8301  alive   server  1.4.4  2         dc1  &lt;all&gt;

# consul operator raft list-peers
Node      ID                                    Address             State     Voter  RaftProtocol
consul-3  d048e55b-5f6a-34a4-784c-e6607db0e89e  172.16.66.104:8300  leader    true   3
consul-1  160a7a20-f177-d2f5-0765-e6d1a9a1a9a4  172.16.66.102:8300  follower  true   3
consul-2  6795cd2c-fad5-9d4f-2531-13b0a65e0893  172.16.66.103:8300  follower  true   3

</code></pre>
<h3>2. DNS设置（可选）</h3>
<p>如果采用基于consul DNS的方式进行服务发现，那么在每个<strong>nomad client node</strong>上设置DNS则很必要。否则如果要是基于consul service catalog的API去查找service，则可忽略这个步骤。设置步骤如下：</p>
<p>在每个node上，创建和编辑/etc/resolvconf/resolv.conf.d/base，填入如下内容：</p>
<pre><code>nameserver {consul-1-ip}
nameserver {consul-2-ip}

</code></pre>
<p>然后重启resolvconf服务:</p>
<pre><code>#  /etc/init.d/resolvconf restart
[ ok ] Restarting resolvconf (via systemctl): resolvconf.service.

</code></pre>
<p>新的resolv.conf将变成：</p>
<pre><code># cat /etc/resolv.conf
# Dynamic resolv.conf(5) file for glibc resolver(3) generated by resolvconf(8)
#     DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN
nameserver {consul-1-ip}
nameserver {consul-2-ip}
nameserver 100.100.2.136
nameserver 100.100.2.138
options timeout:2 attempts:3 rotate single-request-reopen

</code></pre>
<p>这样无论是在host上，还是在新启动的container里就都可以访问到xx.xx.consul域名的服务了：</p>
<pre><code># ping -c 3 consul.service.dc1.consul
PING consul.service.dc1.consul (172.16.66.103) 56(84) bytes of data.
64 bytes from 172.16.66.103: icmp_seq=1 ttl=64 time=0.227 ms
64 bytes from 172.16.66.103: icmp_seq=2 ttl=64 time=0.158 ms
^C
--- consul.service.dc1.consul ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 999ms
rtt min/avg/max/mdev = 0.158/0.192/0.227/0.037 ms

# docker run busybox ping -c 3 consul.service.dc1.consul

PING consul.service.dc1.consul (172.16.66.104): 56 data bytes
64 bytes from 172.16.66.104: seq=0 ttl=64 time=0.067 ms
64 bytes from 172.16.66.104: seq=1 ttl=64 time=0.061 ms
64 bytes from 172.16.66.104: seq=2 ttl=64 time=0.076 ms

--- consul.service.dc1.consul ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.061/0.068/0.076 ms

</code></pre>
<h3>3. 基于consul集群引导启动nomad集群</h3>
<p>按照之前的拓扑图，我们需先在每个node上分别下载nomad：</p>
<pre><code># wget -c https://releases.hashicorp.com/nomad/0.8.7/nomad_0.8.7_linux_amd64.zip

# unzip nomad_0.8.7_linux_amd64.zip.zip

# cp ./nomad /usr/local/bin

# nomad -v

Nomad v0.8.7 (21a2d93eecf018ad2209a5eab6aae6c359267933+CHANGES)

</code></pre>
<p>我们已经建立了consul集群，因为我们将采用<a href="https://www.nomadproject.io/guides/operations/cluster/automatic.html">基于consul集群引导启动nomad集群</a>这一创建nomad集群的最Easy方式。同时，我们每个node上既要运行nomad server，也要nomad client，于是我们在nomad的配置文件中，对server和client都设置为”enabled = true”。下面是nomad启动的配置文件，每个node上的nomad均将该配置文件作为为输入：</p>
<pre><code>// agent.hcl

data_dir = "/root/.bin/nomad-install/nomad.d"

server {
  enabled = true
  bootstrap_expect = 3
}

client {
  enabled = true
}

</code></pre>
<p>下面是在各个节点上启动nomad的操作步骤：</p>
<pre><code>dxnode1:

# nohup nomad agent -config=/root/.bin/nomad-install/agent.hcl  &gt; nomad-1.log &amp; 2&gt;&amp;1

dxnode2:

# nohup nomad agent -config=/root/.bin/nomad-install/agent.hcl  &gt; nomad-2.log &amp; 2&gt;&amp;1

dxnode3:

# nohup nomad agent -config=/root/.bin/nomad-install/agent.hcl  &gt; nomad-3.log &amp; 2&gt;&amp;1

</code></pre>
<p>查看nomad集群的启动结果：</p>
<pre><code>#  nomad server members
Name            Address        Port  Status  Leader  Protocol  Build  Datacenter  Region
dxnode1.global  172.16.66.102  4648  alive   true    2         0.8.7  dc1         global
dxnode2.global  172.16.66.103  4648  alive   false   2         0.8.7  dc1         global
dxnode3.global  172.16.66.104  4648  alive   false   2         0.8.7  dc1         global

# nomad operator raft list-peers

Node            ID                  Address             State     Voter  RaftProtocol
dxnode1.global  172.16.66.102:4647  172.16.66.102:4647  leader    true   2
dxnode2.global  172.16.66.103:4647  172.16.66.103:4647  follower  true   2
dxnode3.global  172.16.66.104:4647  172.16.66.104:4647  follower  true   2

# nomad node-status
ID        DC   Name     Class   Drain  Eligibility  Status
7acdd7bc  dc1  dxnode1  &lt;none&gt;  false  eligible     ready
c281658a  dc1  dxnode3  &lt;none&gt;  false  eligible     ready
9e3ef19f  dc1  dxnode2  &lt;none&gt;  false  eligible     ready

</code></pre>
<p>以上这些命令的结果都显示nomad集群工作正常！</p>
<p>nomad还提供一个ui界面（http://nomad-node-ip:4646/ui），可以让运维人员以可视化的方式直观看到当前nomad集群的状态，包括server、clients、工作负载(job)的情况：</p>
<p><img src="https://tonybai.com/wp-content/uploads/nomad-ui.png" alt="img{512x368}" /></p>
<p>nomad ui首页</p>
<p><img src="https://tonybai.com/wp-content/uploads/nomad-ui-servers.png" alt="img{512x368}" /></p>
<p>nomad server列表和状态</p>
<p><img src="https://tonybai.com/wp-content/uploads/nomad-ui-clients.png" alt="img{512x368}" /></p>
<p>nomad client列表和状态</p>
<h2>二. 部署工作负载</h2>
<p>引导启动成功nomad集群后，我们接下来就要向集群中添加“工作负载”了。</p>
<p>在<a href="https://coding.imooc.com/class/284.html">Kubernetes</a>中，我们可以通过创建deployment、pod等向集群添加工作负载；在nomad中我们也可以通过类似的声明式的方法向nomad集群添加工作负载。不过nomad相对简单许多，它<strong>仅提供了一种</strong>名为job的抽象，并给出了<a href="https://www.nomadproject.io/docs/job-specification/index.html">job的specification</a>。nomad集群所有关于工作负载的操作均通过job描述文件和nomad job相关子命令完成。下面是通过job部署工作负载的流程示意图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/nomad-job-workflow.png" alt="img{512x368}" /></p>
<p>从图中可以看到，我们需要做的仅仅是将编写好的job文件提交给nomad即可。</p>
<p>Job spec定义了：job -> group -> task的层次关系。每个job文件只有一个job，但是一个job可能有多个group，每个group可能有多个task。group包含一组要放在同一个集群中调度的task。一个Nomad task是由其驱动程序（driver）在Nomad client节点上执行的命令、服务、应用程序或其他工作负载。task可以是短时间的批处理作业（batch）或长时间运行的服务(service)，例如web应用程序、数据库服务器或API。</p>
<p>Tasks是在用<a href="https://github.com/hashicorp/hcl">HCL语法</a>的声明性job规范中定义的。Job文件提交给Nomad服务端，服务端决定在何处以及如何将job文件中定义的task分配给客户端节点。另一种概念化的理解是:job规范表示工作负载的期望状态，Nomad服务端创建并维护其实际状态。</p>
<p>通过job，开发人员还可以为工作负载定义约束和资源。约束（constraint）通过内核类型和版本等属性限制了工作负载在节点上的位置。资源（resources）需求包括运行task所需的内存、网络、CPU等。</p>
<p>有三种类型的job：system、service和batch，它们决定Nomad将用于此job中task的调度器。service 调度器被设计用来调度永远不会宕机的长寿命服务。batch作业对短期性能波动的敏感性要小得多，寿命也很短，几分钟到几天就可以完成。system调度器用于注册应该在满足作业约束的所有nomad client上运行的作业。当某个client加入到nomad集群或转换到就绪状态时也会调用它。</p>
<p>Nomad允许job作者为自动重新启动失败和无响应的任务指定策略，并自动将失败的任务重新调度到其他节点，从而使任务工作负载具有弹性。</p>
<p>如果对应到k8s中的概念，group更像是某种controller，而task更类似于pod，是被真实调度的实体。Job spec对应某个k8s api object的spec，具体体现在某个yaml文件中。</p>
<p>下面我们就来真实地在nomad集群中创建一个工作负载。我们使用之前在<a href="https://tonybai.com/2018/09/10/setup-service-discovery-and-load-balance-based-on-consul/">《基于consul实现微服务的服务发现和负载均衡》</a>一文中使用过的那几个demo image，这里我们先使用<a href="https://hub.docker.com/r/bigwhite/httpbackendservice">httpbackendservice镜像</a>来创建一个job。</p>
<p>下面是httpbackend的job文件：</p>
<pre><code>// httpbackend-1.nomad

job "httpbackend" {
  datacenters = ["dc1"]
  type = "service"

  group "httpbackend" {
    count = 2

    task "httpbackend" {
      driver = "docker"
      config {
        image = "bigwhite/httpbackendservice:v1.0.0"
        port_map {
          http = 8081
        }
        logging {
          type = "json-file"
        }
      }

      resources {
        network {
          mbits = 10
          port "http" {}
        }
      }

      service {
        name = "httpbackend"
        port = "http"
      }
    }
  }
}

</code></pre>
<p>这个文件基本都是自解释的，重点提几个地方：</p>
<ul>
<li>
<p>job type: service ： 说明该job创建和调度的是一个service类型的工作负载；</p>
</li>
<li>
<p>count = 2 ： 类似于k8s的replicas字段，期望在nomad集群中运行2个httpbackend服务实例，nomad来保证始终处于期望状态。</p>
</li>
<li>
<p>关于port：port_map指定了task中容器的监听端口。network中的port “http” {}没有指定静态IP，因此将采用动态主机端口。service中的port则指明使用”http”这个tag的动态主机端口。这和k8s中service中port使用名称匹配的方式映射到具体pod中的port的方法类似。</p>
</li>
</ul>
<p>我们使用nomad job子命令来创建该工作负载。正式创建之前，我们可以先通过nomad job plan来dry-run一下，一是看job文件格式是否ok；二来检查一下nomad集群是否有空余资源创建和调度新的工作负载：</p>
<pre><code># nomad job plan httpbackend-1.nomad
+/- Job: "httpbackend"
+/- Stop: "true" =&gt; "false"
    Task Group: "httpbackend" (2 create)
      Task: "httpbackend"

Scheduler dry-run:
- All tasks successfully allocated.

Job Modify Index: 4248
To submit the job with version verification run:

nomad job run -check-index 4248 httpbackend-1.nomad

When running the job with the check-index flag, the job will only be run if the
server side version matches the job modify index returned. If the index has
changed, another user has modified the job and the plan's results are
potentially invalid.

</code></pre>
<p>如果plan的输出结果没有问题，则可以用nomad job run正式创建和调度job：</p>
<pre><code># nomad job run httpbackend-1.nomad
==&gt; Monitoring evaluation "40c63529"
    Evaluation triggered by job "httpbackend"
    Allocation "6b0b83de" created: node "9e3ef19f", group "httpbackend"
    Allocation "d0710b85" created: node "7acdd7bc", group "httpbackend"
    Evaluation status changed: "pending" -&gt; "complete"
==&gt; Evaluation "40c63529" finished with status "complete"

</code></pre>
<p>接下来，我们可以使用nomad job status命令查看job的创建情况以及某个job的详细状态信息：</p>
<pre><code># nomad job status
ID                  Type     Priority  Status   Submit Date
httpbackend         service  50        running  2019-03-30T04:58:09+08:00

# nomad job status httpbackend
ID            = httpbackend
Name          = httpbackend
Submit Date   = 2019-03-30T04:58:09+08:00
Type          = service
Priority      = 50
Datacenters   = dc1
Status        = running
Periodic      = false
Parameterized = false

Summary
Task Group   Queued  Starting  Running  Failed  Complete  Lost
httpbackend  0       0         2        0       0         0

Allocations
ID        Node ID   Task Group   Version  Desired  Status    Created    Modified
6b0b83de  9e3ef19f  httpbackend  11       run      running   8m ago     7m50s ago
d0710b85  7acdd7bc  httpbackend  11       run      running   8m ago     7m39s ago

</code></pre>
<p>前面说过，nomad只是集群管理和负载调度，服务发现它是不管的，并且服务发现的问题早已经被consul解决掉了。所以httpbackend创建后，要想使用该服务，我们还得走consul提供的路线：</p>
<p>DNS方式(前面已经做过铺垫了)：</p>
<pre><code># dig SRV httpbackend.service.dc1.consul

; &lt;&lt;&gt;&gt; DiG 9.10.3-P4-Ubuntu &lt;&lt;&gt;&gt; SRV httpbackend.service.dc1.consul
;; global options: +cmd
;; Got answer:
;; -&gt;&gt;HEADER&lt;&lt;- opcode: QUERY, status: NOERROR, id: 7742
;; flags: qr aa rd; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 5
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;httpbackend.service.dc1.consul.    IN    SRV

;; ANSWER SECTION:
httpbackend.service.dc1.consul.    0 IN    SRV    1 1 23578 consul-1.node.dc1.consul.
httpbackend.service.dc1.consul.    0 IN    SRV    1 1 22819 consul-2.node.dc1.consul.

;; ADDITIONAL SECTION:
consul-1.node.dc1.consul. 0    IN    A    172.16.66.102
consul-1.node.dc1.consul. 0    IN    TXT    "consul-network-segment="
consul-2.node.dc1.consul. 0    IN    A    172.16.66.103
consul-2.node.dc1.consul. 0    IN    TXT    "consul-network-segment="

;; Query time: 471 msec
;; SERVER: 172.16.66.102#53(172.16.66.102)
;; WHEN: Sat Mar 30 05:07:54 CST 2019
;; MSG SIZE  rcvd: 251

# curl http://172.16.66.102:23578
this is httpbackendservice, version: v1.0.0

# curl http://172.16.66.103:22819
this is httpbackendservice, version: v1.0.0

</code></pre>
<p>或http api方式(可通过<a href="https://godoc.org/github.com/hashicorp/consul/api">官方API</a>查询服务)：</p>
<pre><code># curl http://127.0.0.1:8500/v1/health/service/httpbackend

[
    {
        "Node": {"ID":"160a7a20-f177-d2f5-0765-e6d1a9a1a9a4","Node":"consul-1","Address":"172.16.66.102","Datacenter":"dc1","TaggedAddresses":{"lan":"172.16.66.102","wan":"172.16.66.102"},"Meta":{"consul-network-segment":""},"CreateIndex":7,"ModifyIndex":10},
        "Service": {"ID":"_nomad-task-5uxc3b7hjzivbklslt4yj5bpsfagibrb","Service":"httpbackend","Tags":[],"Address":"172.16.66.102","Meta":null,"Port":23578,"Weights":{"Passing":1,"Warning":1},"EnableTagOverride":false,"ProxyDestination":"","Proxy":{},"Connect":{},"CreateIndex":30727,"ModifyIndex":30727},
        "Checks": [{"Node":"consul-1","CheckID":"serfHealth","Name":"Serf Health Status","Status":"passing","Notes":"","Output":"Agent alive and reachable","ServiceID":"","ServiceName":"","ServiceTags":[],"Definition":{},"CreateIndex":7,"ModifyIndex":7}]
    },
    {
        "Node": {"ID":"6795cd2c-fad5-9d4f-2531-13b0a65e0893","Node":"consul-2","Address":"172.16.66.103","Datacenter":"dc1","TaggedAddresses":{"lan":"172.16.66.103","wan":"172.16.66.103"},"Meta":{"consul-network-segment":""},"CreateIndex":5,"ModifyIndex":5},
        "Service": {"ID":"_nomad-task-hvqnbklzqr6q5mpspqcqbnhxdil4su4d","Service":"httpbackend","Tags":[],"Address":"172.16.66.103","Meta":null,"Port":22819,"Weights":{"Passing":1,"Warning":1},"EnableTagOverride":false,"ProxyDestination":"","Proxy":{},"Connect":{},"CreateIndex":30725,"ModifyIndex":30725},
        "Checks": [{"Node":"consul-2","CheckID":"serfHealth","Name":"Serf Health Status","Status":"passing","Notes":"","Output":"Agent alive and reachable","ServiceID":"","ServiceName":"","ServiceTags":[],"Definition":{},"CreateIndex":8,"ModifyIndex":8}]
    }
]

</code></pre>
<h2>三. 将服务暴露到外部以及负载均衡</h2>
<p>集群内部的东西向流量可以通过consul的服务发现来实现，南北向流量则需要我们将部分服务暴露到外部才能实现流量导入。在<a href="https://tonybai.com/2018/09/10/setup-service-discovery-and-load-balance-based-on-consul/">《基于consul实现微服务的服务发现和负载均衡》</a>一文中，我们是通过<a href="https://tonybai.com/tag/nginx">nginx</a>实现服务暴露和负载均衡的，但是需要<a href="https://github.com/hashicorp/consul-template">consul-template</a>的协助，并且自己需要实现一个nginx的配置模板，门槛较高也比较复杂。</p>
<p>nomad的官方文档推荐了<a href="https://github.com/fabiolb/fabio">fabio</a>这个反向代理和负载均衡工具。fabio最初由位于荷兰的“<a href="https://www.ebayclassifiedsgroup.com/">eBay Classifieds Group</a>”开发，它为荷兰（marktplaats.nl），澳大利亚（gumtree.com.au）和意大利（www.kijiji.it）的一些最大网站提供支持。自2015年9月以来，它为这些站点提供23000个请求/秒的处理能力(性能应对一般中等流量是没有太大问题的)，没有发现重大问题。</p>
<p>与consul-template+nginx的组合不同，fabio无需开发人员做任何二次开发，也不需要自定义模板，它直接从consul读取service list并生成相关路由。至于哪些服务要暴露在外部，路由形式是怎样的，是需要在服务启动时为服务设置特定的tag，fabio定义了一套灵活的路由匹配描述方法。</p>
<p>下面我们就来部署fabio，并将上面的httpbackend暴露到外部。</p>
<h3>1. 部署fabio</h3>
<p>fabio也是nomad集群的一个工作负载，因此我们可以像普通job那样部署fabio。我们先来使用nomad官方文档中给出fabio.nomad：</p>
<pre><code>//fabio.nomad

job "fabio" {
  datacenters = ["dc1"]
  type = "system"

  group "fabio" {
    task "fabio" {
      driver = "docker"
      config {
        image = "fabiolb/fabio"
        network_mode = "host"
        logging {
          type = "json-file"
        }
      }

      resources {
        cpu    = 200
        memory = 128
        network {
          mbits = 20
          port "lb" {
            static = 9999
          }
          port "ui" {
            static = 9998
          }
        }
      }
    }
  }
}

</code></pre>
<p>这里有几点值得注意：</p>
<ol>
<li>
<p>fabio job的类型是”system”，也就是说该job会被部署到job可以匹配到（通过设定的约束条件）的所有nomad client上，且每个client上仅部署一个实例，有些类似于k8s的daemonset控制下的pod；</p>
</li>
<li>
<p>network_mode = “host” 告诉fabio的驱动docker：fabio容器使用host网络，即与主机同网络namespace；</p>
</li>
<li>
<p>static = 9999和static = 9998，说明fabio在每个nomad client上监听固定的静态端口而不是使用动态端口。这也要求了每个nomad client上不允许存在与fabio端口冲突的应用启动。</p>
</li>
</ol>
<p>我们来plan和run一下这个fabio job：</p>
<pre><code># nomad job plan fabio.nomad

+ Job: "fabio"
+ Task Group: "fabio" (3 create)
  + Task: "fabio" (forces create)

Scheduler dry-run:
- All tasks successfully allocated.

Job Modify Index: 0
To submit the job with version verification run:

nomad job run -check-index 0 fabio.nomad

When running the job with the check-index flag, the job will only be run if the
server side version matches the job modify index returned. If the index has
changed, another user has modified the job and the plan's results are
potentially invalid.

# nomad job run fabio.nomad
==&gt; Monitoring evaluation "97bfc16d"
    Evaluation triggered by job "fabio"
    Allocation "1b77dcfa" created: node "c281658a", group "fabio"
    Allocation "da35a778" created: node "7acdd7bc", group "fabio"
    Allocation "fc915ab7" created: node "9e3ef19f", group "fabio"
    Evaluation status changed: "pending" -&gt; "complete"
==&gt; Evaluation "97bfc16d" finished with status "complete"

</code></pre>
<p>查看一下fabio job的运行状态：</p>
<pre><code># nomad job status fabio

ID            = fabio
Name          = fabio
Submit Date   = 2019-03-27T14:30:29+08:00
Type          = system
Priority      = 50
Datacenters   = dc1
Status        = running
Periodic      = false
Parameterized = false

Summary
Task Group  Queued  Starting  Running  Failed  Complete  Lost
fabio       0       0         3        0       0         0

Allocations
ID        Node ID   Task Group  Version  Desired  Status   Created    Modified
1b77dcfa  c281658a  fabio       0        run      running  1m11s ago  58s ago
da35a778  7acdd7bc  fabio       0        run      running  1m11s ago  54s ago
fc915ab7  9e3ef19f  fabio       0        run      running  1m11s ago  58s ago

</code></pre>
<p>通过9998端口，可以查看fabio的ui页面，这个页面主要展示的是fabio生成的路由信息：</p>
<p><img src="https://tonybai.com/wp-content/uploads/fabio-ui-routes-table-1.png" alt="img{512x368}" /></p>
<p>由于尚未暴露任何服务，因此fabio的路由表为空。</p>
<p>fabio的流量入口为9999端口，不过由于没有配置路由和upstream service，因此如果此时向9999端口发送http请求，将会得到404的应答。</p>
<h3>2. 暴露HTTP服务到外部</h3>
<p>接下来，我们就将上面创建的httpbackend服务通过fabiolb暴露到外部，使得特定条件下通过fabiolb进入集群内部的流量可以被准确路由到集群中的httpbackend实例上面。</p>
<p>下面是fabio将nomad集群内部服务暴露在外部的原理图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/fabiolb-mechanism.png" alt="img{512x368}" /></p>
<p>我们看到原理图中最为关键的一点就是service tag，该信息由nomad在创建job时写入到consul集群；fabio监听consul集群service信息变更，读取有新变动的job，解析job的service tag，生成路由规则。fabio关注所有带有”urlprefix-”前缀的service tag。</p>
<p>fabio启动时监听的9999端口，默认是http接入。我们修改一下之前的httpbackend.nomad，为该job中的service增加tag字段：</p>
<pre><code>// httpbackend.nomad

... ...

     service {
        name = "httpbackend"
        tags = ["urlprefix-mysite.com:9999/"]
        port = "http"
        check {
          name     = "alive"
          type     = "http"
          path     = "/"
          interval = "10s"
          timeout  = "2s"
        }
      }

</code></pre>
<p>对于上面httpbackend.nomad中service块的变更，主要有两点：</p>
<p>1) 增加tag：匹配的路由信息为：“mysite.com:9999/”</p>
<p>2) 增加check块：如果没有check设置，该路由信息将不会在fabio中生效</p>
<p>更新一下httpbackend:</p>
<pre><code># nomad job run httpbackend-2.nomad
==&gt; Monitoring evaluation "c83af3d3"
    Evaluation triggered by job "httpbackend"
    Allocation "6b0b83de" modified: node "9e3ef19f", group "httpbackend"
    Allocation "d0710b85" modified: node "7acdd7bc", group "httpbackend"
    Evaluation status changed: "pending" -&gt; "complete"
==&gt; Evaluation "c83af3d3" finished with status "complete"

</code></pre>
<p>查看fabio的route表，可以看到增加了两条新路由信息：</p>
<p><img src="https://tonybai.com/wp-content/uploads/httpbackend-routes.png" alt="img{512x368}" /></p>
<p>我们通过fabio来访问一下httpbackend服务：</p>
<pre><code># curl http://mysite.com:9999/      --- 注意：事先已经在/etc/hosts中添加了 mysite.com的地址为127.0.0.1
this is httpbackendservice, version: v1.0.0

</code></pre>
<p>我们看到httpbackend service已经被成功暴露到lb的外部了。</p>
<h2>四. 暴露HTTPS、TCP服务到外部</h2>
<h3>1. 定制fabio</h3>
<p>我们的目标是将https、tcp服务暴露到lb的外部，nomad官方文档中给出的fabio.nomad将不再适用，我们需要让fabio监听多个端口，每个端口有着不同的用途。同时，我们通过给fabio传入适当的命令行参数来帮助我们查看fabio的详细access日志信息，并让fabio支持<a href="https://github.com/fabiolb/fabio/wiki/Features#request-tracing">TRACE机制</a>。</p>
<p>fabio.nomad调整如下：</p>
<pre><code>job "fabio" {
  datacenters = ["dc1"]
  type = "system"

  group "fabio" {
    task "fabio" {
      driver = "docker"
      config {
        image = "fabiolb/fabio"
        network_mode = "host"
        logging {
          type = "json-file"
        }
        args = [
          "-proxy.addr=:9999;proto=http,:9997;proto=tcp,:9996;proto=tcp+sni",
          "-log.level=TRACE",
          "-log.access.target=stdout"
        ]
      }

      resources {
        cpu    = 200
        memory = 128
        network {
          mbits = 20
        }
      }
    }
  }
}

</code></pre>
<p>我们让fabio监听三个端口：</p>
<ul>
<li>
<p>9999: http端口</p>
</li>
<li>
<p>9997: tcp端口</p>
</li>
<li>
<p>9996: tcp+sni端口</p>
</li>
</ul>
<p>后续会针对这三个端口暴露的不同服务做细致说明。</p>
<p>我们将fabio的日志级别调低为TRACE级别，以便能查看到fabio日志中输出的trace信息，帮助我们进行路由匹配的诊断。</p>
<p>重新nomad job run fabio.nomad后，我们来看看TRACE的效果：</p>
<pre><code>//访问后端服务，在http header中添加"Trace: abc"：

# curl -H 'Trace: abc' 'http://mysite.com:9999/'
this is httpbackendservice, version: v1.0.0

//查看fabio的访问日志：

2019/03/30 08:13:15 [TRACE] abc Tracing mysite.com:9999/
2019/03/30 08:13:15 [TRACE] abc Matching hosts: [mysite.com:9999]
2019/03/30 08:13:15 [TRACE] abc Match mysite.com:9999/
2019/03/30 08:13:15 [TRACE] abc Routing to service httpbackend on http://172.16.66.102:23578/
127.0.0.1 - - [30/Mar/2019:08:13:15 +0000] "GET / HTTP/1.1" 200 44

</code></pre>
<p>我们可以清晰的看到fabio收到请求后，匹配到一条路由：”mysite.com:9999/”，然后将http请求转发到 172.16.66.102:23578这个httpbackend服务实例上去了。</p>
<h3>2. https服务</h3>
<p>接下来，我们考虑将一个https服务暴露在lb外部。</p>
<p>一种方案是fabiolb做ssl termination，然后再在与upstream https服务建立的ssl连接上传递数据。这种两段式https通信是比较消耗资源的，fabio要对数据进行两次加解密。</p>
<p>另外一种方案是fabiolb将收到的请求透传给后面的upsteam https服务，由client与upsteam https服务直接建立“安全数据通道”，这个方案我们在后续会提到。</p>
<p>第三种方案，那就是对外依旧暴露http，但是fabiolb与upsteam之间通过https通信。我们先来看一下这种“间接暴露https”的方案。</p>
<pre><code>// httpsbackend-upstreamhttps.nomad

job "httpsbackend" {
  datacenters = ["dc1"]
  type = "service"

  group "httpsbackend" {
    count = 2
    restart {
      attempts = 2
      interval = "30m"
      delay = "15s"
      mode = "fail"
    }

    task "httpsbackend" {
      driver = "docker"
      config {
        image = "bigwhite/httpsbackendservice:v1.0.0"
        port_map {
          https = 7777
        }
        logging {
          type = "json-file"
        }
      }

      resources {
        network {
          mbits = 10
          port "https" {}
        }
      }

      service {
        name = "httpsbackend"
        tags = ["urlprefix-mysite-https.com:9999/ proto=https tlsskipverify=true"]
        port = "https"
        check {
          name     = "alive"
          type     = "tcp"
          path     = "/"
          interval = "10s"
          timeout  = "2s"
        }
      }
    }
  }
}

</code></pre>
<p>我们将创建名为httpsbackend的job，job中Task对应的tag为：”urlprefix-mysite-https.com:9999/ proto=https tlsskipverify=true”。解释为：路由mysite-https.com:9999/，上游upstream服务为https服务，fabio不验证upstream服务的公钥数字证书。</p>
<p>我们创建该job：</p>
<pre><code># nomad job run httpsbackend-upstreamhttps.nomad
==&gt; Monitoring evaluation "ba7af6d4"
    Evaluation triggered by job "httpsbackend"
    Allocation "3127aac8" created: node "7acdd7bc", group "httpsbackend"
    Allocation "b5f1b7a7" created: node "9e3ef19f", group "httpsbackend"
    Evaluation status changed: "pending" -&gt; "complete"
==&gt; Evaluation "ba7af6d4" finished with status "complete"

</code></pre>
<p>我们来通过fabiolb访问一下httpsbackend这个服务：</p>
<pre><code># curl -H "Trace: abc"  http://mysite-https.com:9999/
this is httpsbackendservice, version: v1.0.0

// fabiolb 日志

2019/03/30 09:35:48 [TRACE] abc Tracing mysite-https.com:9999/
2019/03/30 09:35:48 [TRACE] abc Matching hosts: [mysite-https.com:9999]
2019/03/30 09:35:48 [TRACE] abc Match mysite-https.com:9999/
2019/03/30 09:35:48 [TRACE] abc Routing to service httpsbackend on https://172.16.66.103:29248
127.0.0.1 - - [30/Mar/2019:09:35:48 +0000] "GET / HTTP/1.1" 200 45

</code></pre>
<h3>3. 基于tcp代理暴露https服务</h3>
<p>上面的方案虽然将https暴露在外面，但是client到fabio这个环节的数据传输不是在安全通道中。上面提到的方案2：fabiolb将收到的请求透传给后面的upsteam https服务，由client与upsteam https服务直接建立“安全数据通道”似乎更佳。fabiolb支持tcp端口的反向代理，我们基于tcp代理来暴露https服务到外部。</p>
<p>我们建立httpsbackend-tcp.nomad文件，考虑篇幅有限，我们仅列出差异化的部分：</p>
<pre><code>job "httpsbackend-tcp" {

 ... ...

    service {
        name = "httpsbackend-tcp"
        tags = ["urlprefix-:9997 proto=tcp"]
        port = "https"
        check {
          name     = "alive"
          type     = "tcp"
          path     = "/"
          interval = "10s"
          timeout  = "2s"
        }
      }

... ...

}

</code></pre>
<p>从httpsbackend-tcp.nomad文件，我们看到我们在9997这个tcp端口上暴露服务，tag为：“urlprefix-:9997 proto=tcp”，即凡是到达9997端口的流量，无论应用协议类型是什么，都转发到httpsbackend-tcp上，且通过tcp协议转发。</p>
<p>我们创建并测试一下该方案：</p>
<pre><code># nomad job run httpsbackend-tcp.nomad

# curl -k https://localhost:9997   //由于使用的是自签名证书，所有告诉curl不校验server端公钥数字证书
this is httpsbackendservice, version: v1.0.0

</code></pre>
<h3>4. 多个https服务共享一个fabio端口</h3>
<p>上面的基于tcp代理暴露https服务的方案还有一个问题，那就是每个https服务都要独占一个fabio listen的端口。那是否可以实现多个https服务使用一个fabio端口，并通过host name route呢？fabio支持tcp+sni的route策略。</p>
<p>SNI, 全称Server Name Indication，即服务器名称指示。它是一个扩展的TLS计算机联网协议。该协议允许在握手过程开始时通过客户端告诉它正在连接的服务器的主机名称。这允许服务器在相同的IP地址和TCP端口号上呈现多个证书，也就是允许在相同的IP地址上提供多个安全HTTPS网站（或其他任何基于TLS的服务），而不需要所有这些站点使用相同的证书。</p>
<p>接下来，我们就来看一下如何在fabio中让多个后端https服务共享一个Fabio服务端口(9996)。我们建立两个job：httpsbackend-sni-1和httpsbackend-sni-2。</p>
<pre><code>//httpsbackend-tcp-sni-1.nomad

job "httpsbackend-sni-1" {

... ...

    service {
        name = "httpsbackend-sni-1"
        tags = ["urlprefix-mysite-sni-1.com/ proto=tcp+sni"]
        port = "https"
        check {
          name     = "alive"
          type     = "tcp"
          path     = "/"
          interval = "10s"
          timeout  = "2s"
        }
      }

.... ...

}

//httpsbackend-tcp-sni-2.nomad

job "httpsbackend-sni-2" {

... ...

   task "httpsbackend-sni-2" {
      driver = "docker"
      config {
        image = "bigwhite/httpsbackendservice:v1.0.1"
        port_map {
          https = 7777
        }
        logging {
          type = "json-file"
        }
    }

    service {
        name = "httpsbackend-sni-2"
        tags = ["urlprefix-mysite-sni-2.com/ proto=tcp+sni"]
        port = "https"
        check {
          name     = "alive"
          type     = "tcp"
          path     = "/"
          interval = "10s"
          timeout  = "2s"
        }
      }

.... ...

}

</code></pre>
<p>我们看到与之前的server tag不同的是：这里proto=tcp+sni，即告诉fabio建立sni路由。httpsbackend-sni-2 task与httpsbackend-sni-1不同之处在于其使用image为bigwhite/httpsbackendservice:v1.0.1，为的是能通过https的应答结果，将这两个服务区分开来。</p>
<p>除此之外，我们还看到tag中并不包含端口号了，而是直接采用host name作为路由匹配标识。</p>
<p>创建这两个job：</p>
<pre><code># nomad job run httpsbackend-tcp-sni-1.nomad
==&gt; Monitoring evaluation "af170d98"
    Evaluation triggered by job "httpsbackend-sni-1"
    Allocation "8ea1cc8d" modified: node "7acdd7bc", group "httpsbackend-sni-1"
    Allocation "e16cdc73" modified: node "9e3ef19f", group "httpsbackend-sni-1"
    Evaluation status changed: "pending" -&gt; "complete"
==&gt; Evaluation "af170d98" finished with status "complete"

# nomad job run httpsbackend-tcp-sni-2.nomad
==&gt; Monitoring evaluation "a77d3799"
    Evaluation triggered by job "httpsbackend-sni-2"
    Allocation "32df450c" modified: node "c281658a", group "httpsbackend-sni-2"
    Allocation "e1bf4871" modified: node "7acdd7bc", group "httpsbackend-sni-2"
    Evaluation status changed: "pending" -&gt; "complete"
==&gt; Evaluation "a77d3799" finished with status "complete"

</code></pre>
<p>我们来分别访问这两个服务：</p>
<pre><code># curl -k https://mysite-sni-1.com:9996/
this is httpsbackendservice, version: v1.0.0

# curl -k https://mysite-sni-2.com:9996/
this is httpsbackendservice, version: v1.0.1

</code></pre>
<p>从返回的结果我们看到，通过9996，我们成功暴露出两个不同的https服务。</p>
<h2>五. 小结</h2>
<p>到这里，我们实现了我们的既定目标：</p>
<ol>
<li>
<p>使用nomad实现了工作负载的创建和调度；</p>
</li>
<li>
<p>东西向流量通过consul机制实现；</p>
</li>
<li>
<p>通过fabio实现了http、https(through tcp)、多https(though tcp+sni)的服务暴露和负载均衡。</p>
</li>
</ol>
<p>后续我们将进一步探索基于nomad实现负载的多种场景的升降级操作(滚动、金丝雀、蓝绿部署)、对非host网络的支持（比如weave network)等。</p>
<p>本文涉及到的源码文件在<a href="https://github.com/bigwhite/experiments/tree/master/nomad-demo/part1/jobs">这里</a>可以下载。</p>
<h2>六. 参考资料</h2>
<ol>
<li><a href="https://www.hashicorp.com/blog/resilient-infrastructure-with-nomad-scheduling">使用Nomad构建弹性基础设施：nomad调度</a></li>
<li><a href="https://www.hashicorp.com/blog/resilient-infrastructure-with-nomad-restarting-tasks">使用Nomad构建弹性基础设施：重启任务</a></li>
<li><a href="https://www.hashicorp.com/blog/building-resilient-infrastructure-with-nomad-job-lifecycle">使用Nomad构建弹性基础设施:   job生命周期</a></li>
<li><a href="https://www.hashicorp.com/blog/resilient-infrastructure-with-nomad-fault-tolerance-outage-recovery">使用Nomad构建弹性基础设施：容错和自我修复</a></li>
<li><a href="https://fabiolb.net/ref/">fabio参考指南</a></li>
</ol>
<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/03/30/cluster-management-and-microservice-deployment-and-scheduled-by-nomad/feed/</wfw:commentRss>
		<slash:comments>4</slash:comments>
		</item>
		<item>
		<title>基于consul实现微服务的服务发现和负载均衡</title>
		<link>https://tonybai.com/2018/09/10/setup-service-discovery-and-load-balance-based-on-consul/</link>
		<comments>https://tonybai.com/2018/09/10/setup-service-discovery-and-load-balance-based-on-consul/#comments</comments>
		<pubDate>Mon, 10 Sep 2018 13:22:55 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[busybox]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[C/S]]></category>
		<category><![CDATA[consul]]></category>
		<category><![CDATA[consul-template]]></category>
		<category><![CDATA[container]]></category>
		<category><![CDATA[Delphi]]></category>
		<category><![CDATA[DNS]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[ECS]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[hasicorp]]></category>
		<category><![CDATA[http]]></category>
		<category><![CDATA[https]]></category>
		<category><![CDATA[json]]></category>
		<category><![CDATA[k8s]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[loadbalance]]></category>
		<category><![CDATA[microservice]]></category>
		<category><![CDATA[nameserver]]></category>
		<category><![CDATA[nginx]]></category>
		<category><![CDATA[nginx.conf]]></category>
		<category><![CDATA[raft]]></category>
		<category><![CDATA[registrator]]></category>
		<category><![CDATA[Service]]></category>
		<category><![CDATA[service-discovery]]></category>
		<category><![CDATA[ServiceMesh]]></category>
		<category><![CDATA[SSL]]></category>
		<category><![CDATA[systemctl]]></category>
		<category><![CDATA[systemd]]></category>
		<category><![CDATA[TCP]]></category>
		<category><![CDATA[template]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[VB]]></category>
		<category><![CDATA[互联网医院]]></category>
		<category><![CDATA[医疗IT]]></category>
		<category><![CDATA[容器]]></category>
		<category><![CDATA[微服务]]></category>
		<category><![CDATA[服务网格]]></category>
		<category><![CDATA[架构]]></category>
		<category><![CDATA[负载均衡]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=2630</guid>
		<description><![CDATA[一. 背景 随着2018年年初国务院办公厅联合多个部委共同发布了《国务院办公厅关于促进“互联网+医疗健康”发展的意见(国办发〔2018〕26号)》，国内医疗IT领域又迎来了一波互联网医院建设的高潮。不过互联网医院多基于实体医院建设，虽说挂了一个“互联网”的名号，但互联网医院系统也多与传统的院内系统，比如：HIS、LIS、PACS、EMR等共享院内的IT基础设施。 如果你略微了解过国内医院院内IT系统的现状，你就知道目前的多数医院的IT系统相比于互联网行业、电信等行业来说是相对“落伍”的，这种落伍不仅体现在IT基础设施的专业性和数量上，更体现在对新概念、新技术、新设计理念等应用上。虽然国内医院IT系统在技术层面呈现出“多样性”的特征，但整体上偏陈旧和保守 &#8211; - 你可以在全国范围内找到10-15年前的各种主流语言(VB、delphi、c#等实现的IT系统，并且系统架构多为两层C/S结构的。 近几年“互联网+医疗”的兴起的确在一些方面提升了医院的服务效率和水平，但这些互联网医疗系统多部署于院外，并主要集中在“做入口”。它们并不算是医院的核心系统：即没有这些互联网系统，医院的业务也是照常进行的(患者可以在传统的窗口办理所有院内业务，就是效率低罢了)。因此，虽然这些互联网医疗系统采用了先进的互联网系统设计理念和技术，但并没有真正提升院内系统的技术水平，它们也只能与院内那些“陈旧”的、难于扩展的系统做对接。 不过互联网医院与这些系统有所不同，虽然它依然“可有可无”，但它却是部署在院内IT基础设施上的系统，同时也受到了院内IT基础设施条件的限制。在我们即将上线的一个针对医院集团的互联网医院版本中，我们就遇到了“被限制”的问题。我们本想上线的Kubernetes集群因为院方提供的硬件“不足”而无法实施，只能“降级”为手工打造的基于consul的微服务服务发现和负载均衡平台，初步满足我们的系统需要。而从k8s到consul的实践过程，总是让我有一种从工业时代回到的农业时代或是“消费降级”的赶脚^_^。 本文就来说说基于当前较新版本的consul实现微服务的服务发现和负载均衡的过程。 二. 实验环境 这里有三台阿里云的ECS，即用作部署consul集群，也用来承载工作负载的节点（这点与真实生产环境还是蛮像的，医院也仅能提供类似的这点儿可怜的设备）： consul-1: 192.168.0.129 consul-2: 192.168.0.130 consul-3: 192.168.0.131 操作系统：Ubuntu server 16.04.4 LTS 内核版本：4.4.0-117-generic 实验环境安装有： Docker 17.03.3-ce consul v1.1.0 consul-template 0.19.5 nginx 1.10.3 registrator master版本 Go 1.11版本 实验所用的样例程序镜像： httpfrontservice httpbackendservice tcpfrontservice 三. 目标及方案原理 本次实验的最基础、最朴素的两个目标： 所有业务应用均基于容器运行 某业务服务容器启动后，会被自动注册服务，同时其他服务可以自动发现该服务并调用，并且到达这个服务的请求会负载均衡到服务的多个实例。 这里选择了与编程语言技术栈无关的、可搭建微服务的服务发现和负载均衡的Hashicorp的consul。关于consul是什么以及其基本原理和应用，可以参见我多年前写的这篇有关consul的文章。 但是光有consul还不够，我们还需要结合consul-template、gliderlab的registrator以及nginx共同来实现上述目标，原理示意图如下： 原理说明： 对于每个biz node上启动的容器，位于每个node上的Registrator实例会监听到该节点上容器的创建和停止的event，并将容器的信息以consul service的形式写入consul或从consul删除。 位于每个nginx node上的consul-template实例会watch consul集群，监听到consul [...]]]></description>
			<content:encoded><![CDATA[<h2>一. 背景</h2>
<p>随着2018年年初国务院办公厅联合多个部委共同发布了<a href="http://www.gov.cn/zhengce/content/2018-04/28/content_5286645.htm">《国务院办公厅关于促进“互联网+医疗健康”发展的意见(国办发〔2018〕26号)》</a>，国内医疗IT领域又迎来了一波互联网医院建设的高潮。不过互联网医院多基于实体医院建设，虽说挂了一个“互联网”的名号，但互联网医院系统也多与传统的院内系统，比如：<a href="https://en.wikipedia.org/wiki/Hospital_information_system">HIS</a>、<a href="https://en.wikipedia.org/wiki/Laboratory_information_system">LIS</a>、<a href="https://en.wikipedia.org/wiki/Picture_archiving_and_communication_system">PACS</a>、<a href="https://en.wikipedia.org/wiki/Electronic_health_record">EMR</a>等共享院内的IT基础设施。</p>
<p>如果你略微了解过国内医院院内IT系统的现状，你就知道目前的多数医院的IT系统相比于互联网行业、电信等行业来说是相对“落伍”的，这种落伍不仅体现在IT基础设施的专业性和数量上，更体现在对新概念、新技术、新设计理念等应用上。虽然国内医院IT系统在技术层面呈现出“多样性”的特征，但整体上偏陈旧和保守 &#8211; - 你可以在全国范围内找到10-15年前的各种主流语言(<a href="https://en.wikipedia.org/wiki/Visual_Basic">VB</a>、<a href="https://en.wikibooks.org/wiki/Delphi_Programming">delphi</a>、<a href="https://en.wikibooks.org/wiki/Delphi_Programming">c#</a>等实现的IT系统，并且系统架构多为两层C/S结构的。</p>
<p>近几年<strong>“互联网+医疗”</strong>的兴起的确在一些方面提升了医院的服务效率和水平，但这些互联网医疗系统多部署于院外，并主要集中在<strong>“做入口”</strong>。它们并不算是医院的核心系统：即没有这些互联网系统，医院的业务也是照常进行的(患者可以在传统的窗口办理所有院内业务，就是效率低罢了)。因此，虽然这些互联网医疗系统采用了先进的互联网系统设计理念和技术，但并没有真正提升院内系统的技术水平，它们也只能与院内那些“陈旧”的、难于扩展的系统做对接。</p>
<p>不过互联网医院与这些系统有所不同，虽然它依然“可有可无”，但它却是部署在院内IT基础设施上的系统，同时也受到了院内IT基础设施条件的<strong>限制</strong>。在我们即将上线的一个针对医院集团的互联网医院版本中，我们就遇到了“被限制”的问题。我们本想上线的<a href="https://tonybai.com/tag/kubernetes">Kubernetes集群</a>因为院方提供的硬件“不足”而无法实施，只能“降级”为手工打造的基于<a href="https://tonybai.com/2015/07/06/implement-distributed-services-registery-and-discovery-by-consul/">consul</a>的微服务服务发现和负载均衡平台，初步满足我们的系统需要。而从<a href="https://kubernetes.io/">k8s</a>到<a href="https://www.consul.io/">consul</a>的实践过程，总是让我有一种从工业时代回到的农业时代或是“消费降级”的赶脚^_^。</p>
<p>本文就来说说基于当前较新版本的<a href="https://tonybai.com/tag/consul">consul</a>实现微服务的服务发现和负载均衡的过程。</p>
<h2>二. 实验环境</h2>
<p>这里有三台阿里云的ECS，即用作部署consul集群，也用来承载工作负载的节点（这点与真实生产环境还是蛮像的，医院也仅能提供类似的这点儿可怜的设备）：</p>
<ul>
<li>consul-1: 192.168.0.129</li>
<li>consul-2: 192.168.0.130</li>
<li>consul-3: 192.168.0.131</li>
</ul>
<p>操作系统：<a href="https://tonybai.com/tag/ubuntu">Ubuntu</a> server 16.04.4 LTS<br />
内核版本：4.4.0-117-generic</p>
<p>实验环境安装有：</p>
<ul>
<li><a href="https://docs.docker.com/install/linux/docker-ce/ubuntu/">Docker 17.03.3-ce</a></li>
<li><a href="https://releases.hashicorp.com/consul/1.1.0/">consul v1.1.0</a></li>
<li><a href="https://releases.hashicorp.com/consul-template/0.19.5/">consul-template 0.19.5</a></li>
<li><a href="http://nginx.org/en/download.html">nginx 1.10.3</a></li>
<li><a href="https://hub.docker.com/r/gliderlabs/registrator/tags/">registrator master版本</a></li>
<li><a href="https://blog.golang.org/go1.11">Go 1.11版本</a></li>
</ul>
<p>实验所用的样例程序镜像：</p>
<ul>
<li><a href="https://hub.docker.com/r/bigwhite/httpfrontservice/">httpfrontservice</a></li>
<li><a href="https://hub.docker.com/r/bigwhite/httpbackendservice/">httpbackendservice</a></li>
<li><a href="https://hub.docker.com/r/bigwhite/tcpfrontservice/">tcpfrontservice</a></li>
</ul>
<h2>三. 目标及方案原理</h2>
<p>本次实验的最基础、最朴素的两个目标：</p>
<ul>
<li>所有业务应用均基于容器运行</li>
<li>某业务服务容器启动后，会被自动注册服务，同时其他服务可以自动发现该服务并调用，并且到达这个服务的请求会负载均衡到服务的多个实例。</li>
</ul>
<p>这里选择了与编程语言技术栈无关的、可搭建微服务的服务发现和负载均衡的<a href="https://www.hashicorp.com/">Hashicorp</a>的<a href="https://github.com/hashicorp/consul">consul</a>。关于consul是什么以及其基本原理和应用，可以参见我多年前写的<a href="https://tonybai.com/2015/07/06/implement-distributed-services-registery-and-discovery-by-consul/">这篇有关consul的文章</a>。</p>
<p>但是光有consul还不够，我们还需要结合consul-template、gliderlab的registrator以及nginx共同来实现上述目标，原理示意图如下：</p>
<p><img src="https://tonybai.com/wp-content/uploads/setup-service-discovery-and-load-balance-based-on-consul-1.png" alt="img{512x368}" /></p>
<p>原理说明：</p>
<ul>
<li>对于每个biz node上启动的容器，位于每个node上的Registrator实例会监听到该节点上容器的创建和停止的event，并将容器的信息以<a href="https://www.consul.io/docs/agent/services.html">consul service</a>的形式写入consul或从consul删除。</li>
<li>位于每个nginx node上的consul-template实例会watch consul集群，监听到consul service的相关event，并将需要expose到external的service信息获取，按照事先定义好的nginx conf template重新生成nginx.conf并reload本节点的nginx，使得nginx的新配置生效。</li>
<li>对于内部服务来说（不通过nginx暴露到外部)，在被registrator写入consul的同时，也完成了在consul DNS的注册，其他服务可以通过特定域名的方式获取该内部服务的IP列表（A地址)和其他信息，比如端口(SRV)，并进而实现与这些内部服务的通信。</li>
</ul>
<p>参考该原理，落地到我们实验环境的部署示意图如下：</p>
<p><img src="https://tonybai.com/wp-content/uploads/setup-service-discovery-and-load-balance-based-on-consul-2.png" alt="img{512x368}" /></p>
<h2>四. 步骤</h2>
<p>下面说说详细的实验步骤。</p>
<h3>1. 安装consul集群</h3>
<p>首先我们先来安装consul集群。consul既支持二进制程序直接部署，也支持Docker容器化部署。如果consul集群单独部署在几个专用节点上，那么consul可以使用二种方式的任何一种。但是如果consul所在节点还承载工作负载，考虑consul作为整个分布式平台的核心，降低它与docker engine引擎的耦合（docker engine可能会因各种情况经常restart），还是建议以二进制程序形式直接部署在物理机或vm上。这里的实验环境资源有限，我们采用的是以二进制程序形式直接部署的方式。</p>
<p>consul最新版本是1.2.2（截至发稿时），consul 1.2.x版本与consul 1.1.x版本最大的不同在于<a href="https://www.hashicorp.com/blog/consul-1-2-service-mesh">consul 1.2.x支持service mesh</a>了，这对于consul来说可是<strong>革新性</strong>的变化，因此这里担心其初期的稳定性，因此我们选择consul 1.1.0版本。</p>
<p>我们下载<a href="https://releases.hashicorp.com/consul/1.1.0/consul_1.1.0_linux_amd64.zip">consul 1.1.0安装包</a>后，将其解压到/usr/local/bin下。</p>
<p>在$HOME下建立consul-install目录，并在其下面存放consul集群的运行目录consul-data。在consul-install目录下，执行命令启动<strong>节点consul-1</strong>上的consul：</p>
<pre><code>consul-1 node:

# nohup consul agent -server -ui -dns-port=53 -bootstrap-expect=3 -data-dir=/root/consul-install/consul-data -node=consul-1 -client=0.0.0.0 -bind=192.168.0.129 -datacenter=dc1 &gt; consul-1.log &amp; 2&gt;&amp;1

# tail -100f consul-1.log
bootstrap_expect &gt; 0: expecting 3 servers
==&gt; Starting Consul agent...
==&gt; Consul agent running!
           Version: 'v1.1.0'
           Node ID: 'd23b9495-4caa-9ef2-a1d5-7f20aa39fd15'
         Node name: 'consul-1'
        Datacenter: 'dc1' (Segment: '&lt;all&gt;')
            Server: true (Bootstrap: false)
       Client Addr: [0.0.0.0] (HTTP: 8500, HTTPS: -1, DNS: 53)
      Cluster Addr: 192.168.0.129 (LAN: 8301, WAN: 8302)
           Encrypt: Gossip: false, TLS-Outgoing: false, TLS-Incoming: false

==&gt; Log data will now stream in as it occurs:

    2018/09/10 10:21:09 [INFO] raft: Initial configuration (index=0): []
    2018/09/10 10:21:09 [INFO] raft: Node at 192.168.0.129:8300 [Follower] entering Follower state (Leader: "")
    2018/09/10 10:21:09 [INFO] serf: EventMemberJoin: consul-1.dc1 192.168.0.129
    2018/09/10 10:21:09 [INFO] serf: EventMemberJoin: consul-1 192.168.0.129
    2018/09/10 10:21:09 [INFO] consul: Adding LAN server consul-1 (Addr: tcp/192.168.0.129:8300) (DC: dc1)
    2018/09/10 10:21:09 [INFO] consul: Handled member-join event for server "consul-1.dc1" in area "wan"
    2018/09/10 10:21:09 [INFO] agent: Started DNS server 0.0.0.0:53 (tcp)
    2018/09/10 10:21:09 [INFO] agent: Started DNS server 0.0.0.0:53 (udp)
    2018/09/10 10:21:09 [INFO] agent: Started HTTP server on [::]:8500 (tcp)
    2018/09/10 10:21:09 [INFO] agent: started state syncer
==&gt; Newer Consul version available: 1.2.2 (currently running: 1.1.0)
    2018/09/10 10:21:15 [WARN] raft: no known peers, aborting election
    2018/09/10 10:21:17 [ERR] agent: failed to sync remote state: No cluster leader

</code></pre>
<p>我们的三个节点的consul都以server角色启动（consul agent -server）,consul集群初始有三个node( -bootstrap-expect=3)，均位于dc1 datacenter(-datacenter=dc1)，服务bind地址为192.168.0.129(-bind=192.168.0.129 )，允许任意client连接（ -client=0.0.0.0）。我们启动了consul ui(-ui)，便于以图形化的方式查看consul集群的状态。我们设置了consul DNS服务的端口号为53（-dns-port=53），这个后续会起到重要作用，这里先埋下小伏笔。</p>
<p>这里我们使用nohup+&amp;符号的方式将consul运行于后台。生产环境建议使用<a href="https://tonybai.com/2016/12/27/when-docker-meets-systemd/">systemd</a>这样的init系统对consul的启停和配置更新进行管理。</p>
<p>从consul-1的输出日志来看，单节点并没有选出leader。我们需要继续在consul-2和consul-3两个节点上也重复consul-1上的操作，启动consul：</p>
<pre><code>consul-2 node:

#nohup consul agent -server -ui -dns-port=53  -bootstrap-expect=3 -data-dir=/root/consul-install/consul-data -node=consul-2 -client=0.0.0.0 -bind=192.168.0.130 -datacenter=dc1 -join 192.168.0.129 &gt; consul-2.log &amp; 2&gt;&amp;1

consul-3 node:

# nohup consul agent -server -ui -dns-port=53  -bootstrap-expect=3 -data-dir=/root/consul-install/consul-data -node=consul-3 -client=0.0.0.0 -bind=192.168.0.131 -datacenter=dc1 -join 192.168.0.129 &gt; consul-3.log &amp; 2&gt;&amp;1

</code></pre>
<p>启动后，我们查看到consul-3.log中的日志:</p>
<pre><code>    2018/09/10 10:24:01 [INFO] consul: New leader elected: consul-3
    2018/09/10 10:24:01 [WARN] raft: AppendEntries to {Voter a215865f-dba7-5caa-cfb3-6850316199a3 192.168.0.130:8300} rejected, sending older logs (next: 1)
    2018/09/10 10:24:01 [INFO] raft: pipelining replication to peer {Voter a215865f-dba7-5caa-cfb3-6850316199a3 192.168.0.130:8300}
    2018/09/10 10:24:01 [WARN] raft: AppendEntries to {Voter d23b9495-4caa-9ef2-a1d5-7f20aa39fd15 192.168.0.129:8300} rejected, sending older logs (next: 1)
    2018/09/10 10:24:01 [INFO] raft: pipelining replication to peer {Voter d23b9495-4caa-9ef2-a1d5-7f20aa39fd15 192.168.0.129:8300}
    2018/09/10 10:24:01 [INFO] consul: member 'consul-1' joined, marking health alive
    2018/09/10 10:24:01 [INFO] consul: member 'consul-2' joined, marking health alive
    2018/09/10 10:24:01 [INFO] consul: member 'consul-3' joined, marking health alive
    2018/09/10 10:24:01 [INFO] agent: Synced node info
==&gt; Newer Consul version available: 1.2.2 (currently running: 1.1.0)

</code></pre>
<p>consul-3 node上的consul被选为初始leader了。我们可以通过consul提供的子命令查看集群状态：</p>
<pre><code>#  consul operator raft list-peers
Node      ID                                    Address             State     Voter  RaftProtocol
consul-3  0020b7aa-486a-5b44-b5fd-be000a380a89  192.168.0.131:8300  leader  true   3
consul-1  d23b9495-4caa-9ef2-a1d5-7f20aa39fd15  192.168.0.129:8300  follower  true   3
consul-2  a215865f-dba7-5caa-cfb3-6850316199a3  192.168.0.130:8300  follower    true   3
</code></pre>
<p>我们还可以通过consul ui以图形化方式查看集群状态和集群内存储的各种配置信息：</p>
<p><img src="https://tonybai.com/wp-content/uploads/setup-service-discovery-and-load-balance-based-on-consul-3.png" alt="img{512x368}" /></p>
<p>至此，consul集群就搭建ok了。</p>
<h3>2. 安装Nginx、consul-template和Registrator</h3>
<p>根据前面的“部署示意图”，我们在consul-1和consul-2上安装nginx、consul-template和Registrator，在consul-3上安装Registrator。</p>
<p><strong>a) Nginx的安装</strong></p>
<p>我们使用ubuntu 16.04.4默认源中的nginx版本:1.10.3，通过apt-get install nginx安装nginx，这个无须赘述了。</p>
<p><strong>b) consul-template的安装</strong></p>
<p><a href="https://github.com/hashicorp/consul-template">consul-template</a>是一个将consul集群中存储的信息转换为文件形式的工具。常用的场景是监听consul集群中数据的变化，并结合模板将数据持久化到某个文件中，再执行某一关联的action。比如我们这里通过consul-template监听consul集群中service信息的变化，并将service信息数据与nginx的配置模板结合，生成nginx可用的nginx.conf配置文件，并驱动nginx重新reload配置文件，使得nginx的配置更新生效。因此一般来说，哪里部署有nginx，我们就应该有一个配对的consul-template部署。</p>
<p>在我们的实验环境中consul-1和consul-2两个节点部署了nginx，因此我们需要在consul-1和consul-2两个节点上部署consul-template。我们直接安装comsul-template的二进制程序（我们使用0.19.5版本），下载安装包并解压后，将consul-template放入/usr/local/bin目录下：</p>
<pre><code># wget -c https://releases.hashicorp.com/consul-template/0.19.5/consul-template_0.19.5_linux_amd64.zip

# unzip consul-template_0.19.5_linux_amd64.zip
# mv consul-tempate /usr/local/bin
# consul-template -v
consul-template v0.19.5 (57b6c71)
</code></pre>
<p>这里先不启动consul-template，后续在注册不同服务的场景中，我们再启动consul-template。</p>
<p><strong>c) Registrator的安装</strong></p>
<p><a href="https://github.com/gliderlabs/registrator">Registrator</a>是另外一种工具，它监听Docker引擎上发生的容器创建和停止事件，并将启动的容器信息以consul service的形式存储在consul集群中。因此，Registrator和node上的docker engine对应，有docker engine部署的节点上都应该安装有对应的Registator。因此我们要在实验环境的三个节点上都部署Registrator。</p>
<p>Registrator官方推荐的就是以Docker容器方式运行，但这里我并不使用lastest版本，而是用master版本，因为只有最新的master版本<a href="https://github.com/gliderlabs/registrator/issues/633">才支持service meta数据的写入</a>，而当前的latest版本是<a href="https://github.com/gliderlabs/registrator/releases/tag/v7">v7版本</a>，年头较长，并不支持service meta数据写入。</p>
<p>在所有实验环境节点上执行：</p>
<pre><code> # docker run --restart=always -d \
    --name=registrator \
    --net=host \
    --volume=/var/run/docker.sock:/tmp/docker.sock \
    gliderlabs/registrator:master\
      consul://localhost:8500
</code></pre>
<p>我们看到registrator将node节点上的/var/run/docker.sock映射到容器内部的/tmp/docker.sock上，通过这种方式registrator可以监听到node上docker引擎上的事件变化。registrator的另外一个参数：consul://localhost:8500则是Registrator要写入信息的consul地址（当然Registrator不仅仅支持consul，还支持etcd、zookeeper等），这里传入的是本node上consul server的地址和服务端口。</p>
<p>Registrator的启动日志如下：</p>
<pre><code># docker logs -f registrator
2018/09/10 05:56:39 Starting registrator v7 ...
2018/09/10 05:56:39 Using consul adapter: consul://localhost:8500
2018/09/10 05:56:39 Connecting to backend (0/0)
2018/09/10 05:56:39 consul: current leader  192.168.0.130:8300
2018/09/10 05:56:39 Listening for Docker events ...
2018/09/10 05:56:39 Syncing services on 1 containers
2018/09/10 05:56:39 ignored: 6ef6ae966ee5 no published ports

</code></pre>
<p>在所有节点都启动完Registrator后，我们来先查看一下当前consul集群中service的catelog以及每个catelog下的service的详细信息：</p>
<pre><code>// consul-1:

# curl  http://localhost:8500/v1/catalog/services
{"consul":[]}

</code></pre>
<p>目前只有consul自己内置的consul service catelog，我们查看一下consul这个catelog service的详细信息：</p>
<pre><code>// consul-1:

# curl  localhost:8500/v1/catalog/service/consul|jq
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  1189  100  1189    0     0   180k      0 --:--:-- --:--:-- --:--:--  193k
[
  {
    "ID": "d23b9495-4caa-9ef2-a1d5-7f20aa39fd15",
    "Node": "consul-1",
    "Address": "192.168.0.129",
    "Datacenter": "dc1",
    "TaggedAddresses": {
      "lan": "192.168.0.129",
      "wan": "192.168.0.129"
    },
    "NodeMeta": {
      "consul-network-segment": ""
    },
    "ServiceID": "consul",
    "ServiceName": "consul",
    "ServiceTags": [],
    "ServiceAddress": "",
    "ServiceMeta": {},
    "ServicePort": 8300,
    "ServiceEnableTagOverride": false,
    "CreateIndex": 5,
    "ModifyIndex": 5
  },
  {
    "ID": "a215865f-dba7-5caa-cfb3-6850316199a3",
    "Node": "consul-2",
    "Address": "192.168.0.130",
    "Datacenter": "dc1",
    "TaggedAddresses": {
      "lan": "192.168.0.130",
      "wan": "192.168.0.130"
    },
    "NodeMeta": {
      "consul-network-segment": ""
    },
    "ServiceID": "consul",
    "ServiceName": "consul",
    "ServiceTags": [],
    "ServiceAddress": "",
    "ServiceMeta": {},
    "ServicePort": 8300,
    "ServiceEnableTagOverride": false,
    "CreateIndex": 6,
    "ModifyIndex": 6
  },
  {
    "ID": "0020b7aa-486a-5b44-b5fd-be000a380a89",
    "Node": "consul-3",
    "Address": "192.168.0.131",
    "Datacenter": "dc1",
    "TaggedAddresses": {
      "lan": "192.168.0.131",
      "wan": "192.168.0.131"
    },
    "NodeMeta": {
      "consul-network-segment": ""
    },
    "ServiceID": "consul",
    "ServiceName": "consul",
    "ServiceTags": [],
    "ServiceAddress": "",
    "ServiceMeta": {},
    "ServicePort": 8300,
    "ServiceEnableTagOverride": false,
    "CreateIndex": 7,
    "ModifyIndex": 7
  }
]

</code></pre>
<h3>3. 内部http服务的注册和发现</h3>
<p>对于微服务而言，有暴露到外面的，也有仅运行在内部，被内部服务调用的。我们先来看看内部服务，这里以一个http服务为例。</p>
<p>对于暴露到外部的微服务而言，可以通过域名、路径、端口等来发现。但是对于内部服务，我们怎么发现呢？<a href="https://tonybai.com/tag/k8s">k8s</a>中我们可以通过k8s集群的DNS插件进行自动域名解析实现，每个pod中container的DNS server指向的就是k8s dns server。这样service之间可以通过使用固定规则的<strong>域名</strong>(比如：your_svc.default.svc.cluster.local)来访问到另外一个service(仅需配置一个service name)，再通过service实现该服务请求负载均衡到service关联的后端endpoint(pod container)上。consul集群也可以做到这点，并<strong>使用consul提供的DNS服务</strong>来实现内部服务的发现。</p>
<p>我们需要对三个节点的DNS配置进行update，将consul DNS server加入到主机DNS resolver(这也是之前在启动consul时将consul DNS的默认监听端口从8600改为53的原因)，步骤如下：</p>
<ul>
<li>编辑/etc/resolvconf/resolv.conf.d/base，加入一行：</li>
</ul>
<pre><code>nameserver 127.0.0.1
</code></pre>
<ul>
<li>重启resolveconf服务</li>
</ul>
<pre><code> /etc/init.d/resolvconf restart
</code></pre>
<p>再查看/etc/resolve.conf文件：</p>
<pre><code># cat /etc/resolv.conf
# Dynamic resolv.conf(5) file for glibc resolver(3) generated by resolvconf(8)
#     DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN
nameserver 100.100.2.136
nameserver 100.100.2.138
nameserver 127.0.0.1
options timeout:2 attempts:3 rotate single-request-reopen
</code></pre>
<p>我们发现127.0.0.1这个DNS server地址已经被加入到/etc/resolv.conf中了（切记：不要直接手工修改/etc/resolve.conf）。</p>
<p>好了！有了consul DNS，我们就可以发现consul中的服务了。consul给其集群内部的service一个默认的域名：your_svc.service.{data-center}.consul. 之前我们查看了cluster中只有一个consul catelog service，我们就来访问一下该consul service：</p>
<pre><code># ping -c 3 consul.service.dc1.consul
PING consul.service.dc1.consul (192.168.0.129) 56(84) bytes of data.
64 bytes from iZbp15tvx7it019hvy750tZ (192.168.0.129): icmp_seq=1 ttl=64 time=0.029 ms
64 bytes from iZbp15tvx7it019hvy750tZ (192.168.0.129): icmp_seq=2 ttl=64 time=0.025 ms
64 bytes from iZbp15tvx7it019hvy750tZ (192.168.0.129): icmp_seq=3 ttl=64 time=0.031 ms

# ping -c 3 consul.service.dc1.consul
PING consul.service.dc1.consul (192.168.0.130) 56(84) bytes of data.
64 bytes from 192.168.0.130: icmp_seq=1 ttl=64 time=0.186 ms
64 bytes from 192.168.0.130: icmp_seq=2 ttl=64 time=0.136 ms
64 bytes from 192.168.0.130: icmp_seq=3 ttl=64 time=0.195 ms

# ping -c 3 consul.service.dc1.consul
PING consul.service.dc1.consul (192.168.0.131) 56(84) bytes of data.
64 bytes from 192.168.0.131: icmp_seq=1 ttl=64 time=0.149 ms
64 bytes from 192.168.0.131: icmp_seq=2 ttl=64 time=0.184 ms
64 bytes from 192.168.0.131: icmp_seq=3 ttl=64 time=0.179 ms
</code></pre>
<p>我们看到consul服务有三个实例，因此DNS轮询在不同ping命令执行时返回了不同的地址。</p>
<p>现在在主机层面上，我们可以发现consul中的service了。如果我们的服务调用者跑在docker container中，我们还能找到consul服务么？</p>
<pre><code># docker run busybox ping consul.service.dc1.consul
ping: bad address 'consul.service.dc1.consul'

</code></pre>
<p>事实告诉我们：不行！</p>
<p>那么我们如何让运行于docker container中的服务调用者也能发现consul中的service呢？我们需要给docker引擎指定DNS：</p>
<p>在/etc/docker/daemon.json中添加下面配置:</p>
<pre><code>{
    "dns": ["node_ip", "8.8.8.8"] //node_ip： consul_1为192.168.0.129、consul_2为192.168.0.130、consul_3为192.168.0.131
}
</code></pre>
<p>重启docker引擎后，再尝试在容器内发现consul服务：</p>
<pre><code># docker run busybox ping consul.service.dc1.consul
PING consul.service.dc1.consul (192.168.0.131): 56 data bytes
64 bytes from 192.168.0.131: seq=0 ttl=63 time=0.268 ms
64 bytes from 192.168.0.131: seq=1 ttl=63 time=0.245 ms
64 bytes from 192.168.0.131: seq=2 ttl=63 time=0.235 ms

</code></pre>
<p>这次就ok了！</p>
<p>接下来我们在三个节点上以容器方式启动我们的一个内部http服务demo httpbackend：</p>
<pre><code># docker run --restart=always -d  -l "SERVICE_NAME=httpbackend" -p 8081:8081 bigwhite/httpbackendservice:v1.0.0
</code></pre>
<p>我们查看一下consul集群内的httpbackend service信息：</p>
<pre><code># curl  localhost:8500/v1/catalog/service/httpbackend|jq
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  1374  100  1374    0     0   519k      0 --:--:-- --:--:-- --:--:--  670k
[
  {
    "ID": "d23b9495-4caa-9ef2-a1d5-7f20aa39fd15",
    "Node": "consul-1",
    "Address": "192.168.0.129",
   ...
  },
  {
    "ID": "a215865f-dba7-5caa-cfb3-6850316199a3",
    "Node": "consul-2",
    "Address": "192.168.0.130",
   ...
  },
  {
    "ID": "0020b7aa-486a-5b44-b5fd-be000a380a89",
    "Node": "consul-3",
    "Address": "192.168.0.131",
   ...
  }
]

</code></pre>
<p>再访问一下该服务：</p>
<pre><code># curl httpbackend.service.dc1.consul:8081
this is httpbackendservice, version: v1.0.0
</code></pre>
<p>内部服务发现成功！</p>
<h3>4. 暴露外部http服务</h3>
<p>说完了内部服务，我们再来说说那些要暴露到外部的服务，这个环节就轮到consul-template登场了！在我们的实验中，consul-template读取consul中service信息，并结合模板生成nginx配置文件。我们基于默认安装的/etc/nginx/nginx.conf文件内容来编写我们的模板。我们先实验暴露http服务到外面。下面是模板样例：</p>
<pre><code>//nginx.conf.template

.... ...

http {
        ... ...
        ##
        # Virtual Host Configs
        ##

        include /etc/nginx/conf.d/*.conf;
        include /etc/nginx/sites-enabled/*;

        #
        # http server config
        #

        {{range services -}}
        {{$name := .Name}}
        {{$service := service .Name}}
        {{- if in .Tags "http" -}}
        upstream {{$name}} {
          zone upstream-{{$name}} 64k;
          {{range $service}}
          server {{.Address}}:{{.Port}} max_fails=3 fail_timeout=60 weight=1;
          {{end}}
        }{{end}}
        {{end}}

        {{- range services -}} {{$name := .Name}}
        {{- if in .Tags "http" -}}
        server {
          listen 80;
          server_name {{$name}}.tonybai.com;

          location / {
            proxy_pass http://{{$name}};
          }
        }
        {{end}}
        {{end}}

}

</code></pre>
<p>consul-template使用的模板采用的是<a href="https://golang.org/pkg/html/template/">go template</a>的语法。我们看到在http block中，我们要为consul中的每个要expose到外部的catelog service定义一个server block(对应的域名为your_svc.tonybai.com)和一个upstream block。</p>
<p>对上面的模板做简单的解析，弄明白三点，模板基本就全明白了：</p>
<ul>
<li>{{- range services -}}： 标准的{{ range pipeline }}模板语法，<strong>services</strong>这个pipeline的调用相当于： curl  localhost:8500/v1/catalog/services，即获取catelog services列表。这个列表中的每项仅有Name和Tags两个字段可用。</li>
<li>{{- if in .Tags “http” -}}：判断语句，即如果Tags字段中有http这个tag，那么则暴露该catelog service。</li>
<li>{{range $service}}： 也是标准的{{ range pipeline }}模板语法，$service这个pipeline调用相当于curl  localhost:8500/v1/catalog/service/xxxx，即获取某个service xxx的详细信息，包括Address、Port、Tag、Meta等。</li>
</ul>
<p>接下来，我们在consul-1和consul-2上启动consul-template：</p>
<pre><code>consul-1:
# nohup  consul-template -template "/root/consul-install/templates/nginx.conf.template:/etc/nginx/nginx.conf:nginx -s reload" &gt; consul-template.log &amp; 2&gt;&amp;1

consul-2:
# nohup  consul-template -template "/root/consul-install/templates/nginx.conf.template:/etc/nginx/nginx.conf:nginx -s reload" &gt; consul-template.log &amp; 2&gt;&amp;1

</code></pre>
<p>查看/etc/nginx/nginx.conf，你会发现http server config下面并没有生成任何配置，因为consul集群中还没有满足Tag条件的service（包含tag “http”)。现在我们就来在三个node上创建httpfront services。</p>
<pre><code># docker run --restart=always -d -l "SERVICE_NAME=httpfront" -l "SERVICE_TAGS=http" -P bigwhite/httpfrontservice:v1.0.0

</code></pre>
<p>查看生成的nginx.conf:</p>
<pre><code>upstream httpfront {
      zone upstream-httpfront 64k;

          server 192.168.0.129:32769 max_fails=3 fail_timeout=60 weight=1;

          server 192.168.0.130:32768 max_fails=3 fail_timeout=60 weight=1;

          server 192.168.0.131:32768 max_fails=3 fail_timeout=60 weight=1;

    }

    server {
      listen 80;
          server_name httpfront.tonybai.com;

      location / {
        proxy_pass http://httpfront;
      }
    }

</code></pre>
<p>测试一下httpfront.tonybai.com(可通过修改/etc/hosts)，httpfront service会调用内部服务httpbackend(通过httpbackend.service.dc1.consul:8081访问)：</p>
<pre><code># curl httpfront.tonybai.com
this is httpfrontservice, version: v1.0.0, calling backendservice ok, its resp: [this is httpbackendservice, version: v1.0.0
]
</code></pre>
<p>可以在各个节点上查看httpfront的日志：(通过docker logs)，你会发现到httpfront.tonybai.com的请求被均衡到了各个节点上的httpfront service上了：</p>
<pre><code>{GET / HTTP/1.0 1 0 map[Connection:[close] User-Agent:[curl/7.47.0] Accept:[*/*]] {} &lt;nil&gt; 0 [] true httpfront map[] map[] &lt;nil&gt; map[] 192.168.0.129:35184 / &lt;nil&gt; &lt;nil&gt; &lt;nil&gt; 0xc0000524c0}
calling backendservice...
{200 OK 200 HTTP/1.1 1 1 map[Date:[Mon, 10 Sep 2018 08:23:33 GMT] Content-Length:[44] Content-Type:[text/plain; charset=utf-8]] 0xc0000808c0 44 [] false false map[] 0xc000132600 &lt;nil&gt;}
this is httpbackendservice, version: v1.0.0
</code></pre>
<h3>5. 暴露外部tcp服务</h3>
<p>我们的微服务可不仅仅有http服务的，还有直接暴露tcp socket服务的。nginx对tcp的支持是通过stream block支持的。在stream block中，我们来为每个要暴露在外面的tcp service生成server block和upstream block，这部分模板内容如下：</p>
<pre><code>stream {
   {{- range services -}}
   {{$name := .Name}}
   {{$service := service .Name}}
     {{- if in .Tags "tcp" -}}
  upstream {{$name}} {
    least_conn;
    {{- range $service}}
    server {{.Address}}:{{.Port}} max_fails=3 fail_timeout=30s weight=5;
    {{ end }}
  }
     {{end}}
  {{end}}

   {{- range services -}}
   {{$name := .Name}}
   {{$nameAndPort := $name | split "-"}}
    {{- if in .Tags "tcp" -}}
  server {
      listen {{ index $nameAndPort 1 }};
      proxy_pass {{$name}};
  }
    {{end}}
   {{end}}
}

</code></pre>
<p>和之前的http服务模板相比，这里的Tag过滤词换为了<strong>“tcp”</strong>，并且由于端口具有排他性，这里用”名字-端口”串来作为service的name以及upstream block的标识。用一个例子来演示会更加清晰。由于修改了nginx模板，在演示demo前，需要重启一下各个consul-template。</p>
<p>然后我们在各个节点上启动tcpfront service（注意服务名为tcpfront-9999，9999是tcpfrontservice expose到外部的端口）：</p>
<pre><code># docker run -d --restart=always -l "SERVICE_TAGS=tcp" -l "SERVICE_NAME=tcpfront-9999" -P bigwhite/tcpfrontservice:v1.0.0
</code></pre>
<p>启动后，我们查看一下生成的nginx.conf:</p>
<pre><code>stream {

   upstream tcpfront-9999 {
    least_conn;
    server 192.168.0.129:32770 max_fails=3 fail_timeout=30s weight=5;

    server 192.168.0.130:32769 max_fails=3 fail_timeout=30s weight=5;

    server 192.168.0.131:32769 max_fails=3 fail_timeout=30s weight=5;

  }

   server {
      listen 9999;
      proxy_pass tcpfront-9999;
  }

}
</code></pre>
<p>nginx对外的9999端口对应到集群内的tcpfront服务！这个tcpfront是一个echo服务，我们来测试一下：</p>
<pre><code># telnet localhost 9999
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
hello
[v1.0.0]2018-09-10 08:56:15.791728641 +0000 UTC m=+531.620462772 [hello
]
tonybai
[v1.0.0]2018-09-10 08:56:17.658482957 +0000 UTC m=+533.487217127 [tonybai
]

</code></pre>
<p>基于暴露tcp服务，我们还可以实现将全透传的https服务暴露到外部。所谓全透传的https服务，即ssl证书配置在服务自身，而不是nginx上面。其实现方式与暴露tcp服务相似，这里就不举例了。</p>
<h2>五. 小结</h2>
<p>以上基于consul+consul-template+registrator+nginx实现了一个基本的微服务服务发现和负载均衡框架，但要应用到生产环境还需一些进一步的考量。</p>
<p>关于服务治理的一些功能，consul 1.2.x版本已经加入了<a href="https://blog.buoyant.io/2017/04/25/whats-a-service-mesh-and-why-do-i-need-one/">service mesh</a>的support，后续在成熟后可以考虑upgrade consul cluster。</p>
<p>consul-template在v0.19.5中还不支持servicemeta的，但<a href="https://github.com/hashicorp/consul-template/blob/master/dependency/health_service.go#L45">在master版本中已经支持</a>，后续利用新版本的consul-template可以实现功能更为丰富的模板，比如实现<a href="https://en.wikipedia.org/wiki/A/B_testing">灰度发布</a>等。</p>
<hr />
<p><a href="https://tonybai.com/">51短信平台</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, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2018/09/10/setup-service-discovery-and-load-balance-based-on-consul/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
