<?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; Ceph</title>
	<atom:link href="http://tonybai.com/tag/ceph/feed/" rel="self" type="application/rss+xml" />
	<link>https://tonybai.com</link>
	<description>一个程序员的心路历程</description>
	<lastBuildDate>Fri, 17 Apr 2026 00:21:29 +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>MinIO 开源版突发“安乐死”：维护模式开启，社区愤怒，你的数据还安全吗？</title>
		<link>https://tonybai.com/2025/12/04/minio-enter-maintenance-mode/</link>
		<comments>https://tonybai.com/2025/12/04/minio-enter-maintenance-mode/#comments</comments>
		<pubDate>Thu, 04 Dec 2025 09:40:44 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[AGPL]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[AIStor]]></category>
		<category><![CDATA[Ceph]]></category>
		<category><![CDATA[CLA]]></category>
		<category><![CDATA[CloudVendors]]></category>
		<category><![CDATA[Garage]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[infrastructure]]></category>
		<category><![CDATA[MaintenanceMode]]></category>
		<category><![CDATA[minio]]></category>
		<category><![CDATA[ObjectStorage]]></category>
		<category><![CDATA[Opensource]]></category>
		<category><![CDATA[redis]]></category>
		<category><![CDATA[Rust]]></category>
		<category><![CDATA[RustFS]]></category>
		<category><![CDATA[S3]]></category>
		<category><![CDATA[SeaweedFS]]></category>
		<category><![CDATA[SSPL]]></category>
		<category><![CDATA[Valkey]]></category>
		<category><![CDATA[VersityGateway]]></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=5476</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/12/04/minio-enter-maintenance-mode 大家好，我是Tony Bai。 “这个项目目前处于维护状态，不接受新的更改。” 近日，GitHub 上拥有近 60k Star、Go 语言生态中最著名的开源对象存储项目——MinIO，悄然修改了其 README。这一行看似平淡的声明，标志着 MinIO 开源版实际上已经被宣判了“死刑”。 曾经，MinIO 是自建 S3 兼容存储的首选，是开源界的宠儿。如今，它转身拥抱了企业级市场和 AI 浪潮，留下了一脸错愕的社区用户和无数依赖它的开源项目。这究竟是一场无奈的求生，还是一次蓄谋已久的“收割”？ 突如其来的“维护模式” MinIO 官方在没有任何预警的情况下，将其开源仓库置于“维护模式”。这意味着： 功能冻结：不再接受任何新功能或改进。 社区关门：不再接受 Pull Request，现有的 Issue 和 PR 也不会被积极审查。 安全补丁随缘：关键的安全修复“可能”会根据具体情况进行评估，不再有保证。 官方建议很明确：“对于企业支持和积极维护的版本，请参阅MinIO AIStor。”，而AIStor则是MinIO的企业版对象存储产品。 这一举动在 Hacker News 上引发了轩然大波。用户感到被背叛，一位评论者愤怒地写道：“太恶心了。构建一个产品，通过开源获得动力，等你做完了就完全抛弃它。我为曾经推广这个项目感到羞耻。” 为何“背叛”？—— 商业化的必然与 AI 的诱惑 MinIO 的转向并非无迹可寻。从更换为更严格的 AGPL 协议，到此次事实上的闭源，其背后的逻辑清晰而冷酷： 开源无法变现的困境 MinIO 作为一个高性能、单二进制文件的存储服务，太容易“被集成”了。云厂商、集成商可以轻松地将其打包进自己的产品中获利，而 MinIO 公司却难以从中分一杯羹。AGPL 协议虽然意在限制云厂商的“白嫖”，但也未能从根本上解决其商业化难题。 AI 浪潮的巨大诱惑 MinIO [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/minio-enter-maintenance-mode-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/12/04/minio-enter-maintenance-mode">本文永久链接</a> &#8211; https://tonybai.com/2025/12/04/minio-enter-maintenance-mode</p>
<p>大家好，我是Tony Bai。</p>
<p>“这个项目目前处于维护状态，不接受新的更改。”</p>
<p>近日，GitHub 上拥有近 60k Star、Go 语言生态中最著名的开源对象存储项目——MinIO，<a href="https://github.com/minio/minio/commit/27742d469462e1561c776f88ca7a1f26816d69e2">悄然修改了其 README</a>。这一行看似平淡的声明，标志着 MinIO 开源版实际上已经被宣判了“死刑”。</p>
<p>曾经，MinIO 是自建 S3 兼容存储的首选，是开源界的宠儿。如今，它转身拥抱了企业级市场和 AI 浪潮，留下了一脸错愕的社区用户和无数依赖它的开源项目。这究竟是一场无奈的求生，还是一次蓄谋已久的“收割”？</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/paid/google-adk-in-action-qr.png" alt="" /></p>
<h2>突如其来的“维护模式”</h2>
<p><img src="https://tonybai.com/wp-content/uploads/2025/minio-enter-maintenance-mode-2.png" alt="" /></p>
<p>MinIO 官方在没有任何预警的情况下，将其开源仓库置于“维护模式”。这意味着：</p>
<ul>
<li><strong>功能冻结</strong>：不再接受任何新功能或改进。</li>
<li><strong>社区关门</strong>：不再接受 Pull Request，现有的 Issue 和 PR 也不会被积极审查。</li>
<li><strong>安全补丁随缘</strong>：关键的安全修复“可能”会根据具体情况进行评估，不再有保证。</li>
</ul>
<p>官方建议很明确：<strong>“对于企业支持和积极维护的版本，请参阅<a href="https://github.com/minio/aistor">MinIO AIStor</a>。”</strong>，而AIStor则是MinIO的企业版对象存储产品。</p>
<p>这一举动<a href="https://news.ycombinator.com/item?id=46136023">在 Hacker News 上引发了轩然大波</a>。用户感到被背叛，一位评论者愤怒地写道：“太恶心了。构建一个产品，通过开源获得动力，等你做完了就完全抛弃它。我为曾经推广这个项目感到羞耻。”</p>
<h2>为何“背叛”？—— 商业化的必然与 AI 的诱惑</h2>
<p>MinIO 的转向并非无迹可寻。从更换为更严格的 AGPL 协议，到此次事实上的闭源，其背后的逻辑清晰而冷酷：</p>
<h3>开源无法变现的困境</h3>
<p>MinIO 作为一个高性能、单二进制文件的存储服务，太容易“被集成”了。云厂商、集成商可以轻松地将其打包进自己的产品中获利，而 MinIO 公司却难以从中分一杯羹。AGPL 协议虽然意在限制云厂商的“白嫖”，但也未能从根本上解决其商业化难题。</p>
<h3>AI 浪潮的巨大诱惑</h3>
<p>MinIO 的新产品名为 <strong>AIStor</strong>。这不仅仅是一个改名，更是一次战略转型。在 AI 时代，数据存储是基础设施的核心。MinIO 试图通过重新包装，将自己定位为 AI 基础设施的关键组件，从而向更有付费能力的企业客户（尤其是 AI 公司）靠拢。</p>
<p>正如一位 HN 用户指出的：“他们在上一轮融资中估值 10 亿美元，要想成功退出，必须有深口袋的买家（如 Nvidia, Dell 等）。现在的开源版本只会拖累他们的财报。”</p>
<h2>社区的反击与法律迷局</h2>
<p>MinIO 的做法也引发了法律层面的争议。</p>
<ul>
<li><strong>贡献者的权利</strong>：MinIO 曾要求贡献者签署 CLA（贡献者许可协议）。这意味着 MinIO 公司拥有代码的版权，他们确实有权改变许可证或停止开源。</li>
<li><strong>AGPL 的约束</strong>：但对于那些没有签署 CLA 的早期贡献者，或者包含在代码库中的第三方 AGPL 代码，MinIO 是否有权单方面“私有化”？这是一个复杂的法律问题。</li>
</ul>
<p>更有趣的是，MinIO 过去曾因 AGPL 许可问题积极“维权”，甚至公开指责其他公司违反协议。如今，它自己却试图摆脱开源的束缚，这种双重标准让社区感到讽刺。</p>
<h2>历史的镜像 —— Redis 与 Valkey 的启示</h2>
<p>MinIO 的剧变，让人不由得想起了 2024 年初震动开源界的另一场“地震”——<strong>Redis 修改开源协议事件</strong>。</p>
<p>当时，Redis Inc. 宣布不再遵循开源定义，转而采用限制性更强的 SSPL 协议。这一举动激怒了整个社区和云厂商，Linux 基金会迅速集结了 AWS、Google、Oracle 等巨头，基于 Redis 旧版本 fork 出了 <strong>Valkey</strong>。如今，Valkey 已经展现出取代 Redis 的蓬勃生命力。</p>
<p><strong>MinIO 与 Redis 的异同：</strong></p>
<ul>
<li><strong>相同点</strong>：两者都面临“云厂商困境”。AWS 直接拿 Redis 做 ElastiCache，拿 MinIO 做兼容 S3 的服务，却无需向原厂付费。原厂为了生存，不得不通过协议（AGPL/SSPL）或停止维护来“筑墙”。</li>
<li><strong>不同点</strong>：Redis 选择了<strong>“掀桌子”</strong>（改协议），引发了激烈的对抗和即时的 Fork（Valkey）；而 MinIO 选择了<strong>“冷处理”</strong>（维护模式），这更像是一种温水煮青蛙式的告别。</li>
</ul>
<p><strong>MinIO 会迎来它的“Valkey 时刻”吗？</strong></p>
<p>目前来看，难。对象存储的复杂度和维护成本远高于内存缓存，且市场上已经存在成熟的替代品（如 SeaweedFS, Ceph, Garage）。MinIO 社区或许不会像 Redis 那样迅速集结出一个统一的 Fork，而是会走向<strong>分裂和迁徙</strong>。</p>
<p>对于开发者而言，Redis 和 MinIO 的连续“暴雷”是一个明确的信号：<strong>在基础设施选型时，除了关注技术指标，更要评估其背后的治理模式。由单一商业公司绝对控制的“开源”项目，始终悬着一把达摩克利斯之剑。</strong></p>
<h2>自救指南 —— 寻找 MinIO 的替代品</h2>
<p>对于现有的 MinIO 用户来说，现在是时候寻找备胎了。社区推荐了几个值得关注的替代方案：</p>
<h3>SeaweedFS (Go)</h3>
<ul>
<li><strong>特点</strong>：基于 Haystack 论文实现，擅长处理海量小文件，自带 File 和 S3 接口。</li>
<li><strong>适用场景</strong>：需要高性能小文件存储的场景。</li>
<li><strong>评价</strong>：功能丰富，甚至有点“过度”，但性能强悍。</li>
</ul>
<h3>Ceph (C++)</h3>
<ul>
<li><strong>特点</strong>：存储界的瑞士军刀，功能极其强大，但也极其复杂。</li>
<li><strong>适用场景</strong>：大规模、生产级、需要块存储和文件存储的场景。</li>
<li><strong>评价</strong>：如果你有运维团队，Ceph 是永远不会错的选择。</li>
</ul>
<h3><a href="https://github.com/versity/versitygw/">Versity Gateway (Go)</a></h3>
<ul>
<li><strong>特点</strong>：基于文件的 S3 网关，可以在开发测试环境作为 MinIO 的直接替代品，后端直接对接文件系统。</li>
</ul>
<h3>RustFS (Rust)</h3>
<ul>
<li><strong>特点</strong>：野心勃勃的新晋选手，试图在性能和易用性上直接对标甚至超越 MinIO。</li>
<li><strong>适用场景</strong>：极客尝鲜、非生产环境的测试与评估。</li>
<li><strong>评价</strong>：社区评价两极分化。一方面，它展现了强大的潜力；另一方面，用户反馈其目前<strong>稳定性欠佳</strong>，且项目要求签署 CLA（贡献者许可协议），这让不少刚被 MinIO 伤过心的开发者担心它未来会重演“养肥再杀”的剧本。<strong>“潜力巨大，但需谨慎观望。”</strong></li>
</ul>
<h3><a href="https://github.com/deuxfleurs-org/garage">Garage</a> (Rust)</h3>
<ul>
<li><strong>特点</strong>：轻量级、自包含、专注于在异构硬件和地理分布的网络上运行。</li>
<li><strong>适用场景</strong>：自托管、家庭实验室、中小规模集群。</li>
<li><strong>评价</strong>：“非常稳固，简单可靠，没有风险投资背景。”</li>
</ul>
<h2>小结：开源的尽头是商业，还是背叛？</h2>
<p>MinIO 的故事，是开源软件商业化困境的又一个注脚。它提醒我们：</p>
<ul>
<li><strong>没有免费的午餐</strong>：由 VC 支持的开源项目，最终都要面临盈利的压力。当增长遇到瓶颈，社区往往是被牺牲的第一个对象。</li>
<li><strong>选择开源项目需谨慎</strong>：除了代码质量，项目的治理结构、CLA 协议、背后的商业模式，都是选型时必须考虑的风险因素。</li>
</ul>
<p>MinIO 虽已“离去”，但开源精神不死。也许下一个更好的 MinIO，正在某个 GitHub 的角落里悄然生长。</p>
<p>资料链接：https://news.ycombinator.com/item?id=46136023</p>
<hr />
<p>还在为“复制粘贴喂AI”而烦恼？我的新专栏 <strong>《<a href="http://gk.link/a/12EPd">AI原生开发工作流实战</a>》</strong> 将带你：</p>
<ul>
<li>告别低效，重塑开发范式</li>
<li>驾驭AI Agent(Claude Code)，实现工作流自动化</li>
<li>从“AI使用者”进化为规范驱动开发的“工作流指挥家”</li>
</ul>
<p>扫描下方二维码，开启你的AI原生开发之旅。</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/ai-native-dev-workflow-qr.png" alt="" /></p>
<hr />
<p>你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？</p>
<ul>
<li>想写出更地道、更健壮的Go代码，却总在细节上踩坑？</li>
<li>渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？</li>
<li>想打造生产级的Go服务，却在工程化实践中屡屡受挫？</li>
</ul>
<p>继《<a href="http://gk.link/a/10AVZ">Go语言第一课</a>》后，我的《<a href="http://gk.link/a/12yGY">Go语言进阶课</a>》终于在极客时间与大家见面了！</p>
<p>我的全新极客时间专栏 《<a href="http://gk.link/a/12yGY">Tony Bai·Go语言进阶课</a>》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。</p>
<p>目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /></p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/12/04/minio-enter-maintenance-mode/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>TB一周萃选[第9期]</title>
		<link>https://tonybai.com/2018/02/11/9th-issue-of-the-tech-weekly-carefully-chosen-by-tonybai/</link>
		<comments>https://tonybai.com/2018/02/11/9th-issue-of-the-tech-weekly-carefully-chosen-by-tonybai/#comments</comments>
		<pubDate>Sun, 11 Feb 2018 05:41:08 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[artificial-intelligence]]></category>
		<category><![CDATA[browser]]></category>
		<category><![CDATA[Ceph]]></category>
		<category><![CDATA[circuitbreaker]]></category>
		<category><![CDATA[cloud-native]]></category>
		<category><![CDATA[CNCF]]></category>
		<category><![CDATA[Codereview]]></category>
		<category><![CDATA[conduit]]></category>
		<category><![CDATA[Context]]></category>
		<category><![CDATA[deep-learning]]></category>
		<category><![CDATA[EBS]]></category>
		<category><![CDATA[egress]]></category>
		<category><![CDATA[GCP]]></category>
		<category><![CDATA[GeoffreyHinton]]></category>
		<category><![CDATA[gerrit]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Go-1.10-Release-Party]]></category>
		<category><![CDATA[Go1.10]]></category>
		<category><![CDATA[Go1.6]]></category>
		<category><![CDATA[Go1.7]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Google]]></category>
		<category><![CDATA[GoogleCloudPlatform]]></category>
		<category><![CDATA[HTTP2]]></category>
		<category><![CDATA[ingress]]></category>
		<category><![CDATA[istio]]></category>
		<category><![CDATA[k8s]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[metrics]]></category>
		<category><![CDATA[middleware]]></category>
		<category><![CDATA[NFS]]></category>
		<category><![CDATA[Package]]></category>
		<category><![CDATA[PR]]></category>
		<category><![CDATA[Redhat]]></category>
		<category><![CDATA[rook]]></category>
		<category><![CDATA[S3]]></category>
		<category><![CDATA[ServiceMesh]]></category>
		<category><![CDATA[SSE]]></category>
		<category><![CDATA[tracing]]></category>
		<category><![CDATA[websocket]]></category>
		<category><![CDATA[中华民族]]></category>
		<category><![CDATA[云原生]]></category>
		<category><![CDATA[云存储]]></category>
		<category><![CDATA[亲情]]></category>
		<category><![CDATA[人工智能]]></category>
		<category><![CDATA[传统]]></category>
		<category><![CDATA[充电]]></category>
		<category><![CDATA[分布式存储]]></category>
		<category><![CDATA[华夏]]></category>
		<category><![CDATA[反向传播]]></category>
		<category><![CDATA[回家]]></category>
		<category><![CDATA[安全]]></category>
		<category><![CDATA[密码管理]]></category>
		<category><![CDATA[度量]]></category>
		<category><![CDATA[微服务]]></category>
		<category><![CDATA[性能优化]]></category>
		<category><![CDATA[春节]]></category>
		<category><![CDATA[春运]]></category>
		<category><![CDATA[服务治理]]></category>
		<category><![CDATA[浏览器]]></category>
		<category><![CDATA[深度学习]]></category>
		<category><![CDATA[胶囊理论]]></category>
		<category><![CDATA[节日]]></category>
		<category><![CDATA[过年]]></category>

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

		<guid isPermaLink="false">http://tonybai.com/?p=2477</guid>
		<description><![CDATA[关于基于Harbor的高可用私有镜像仓库，在我的博客里曾不止一次提到，在源创会2017沈阳站上，我还专门以此题目和大家做了分享。事后，很多人通过微博私信、个人公众号或博客评论问我是否可以在Kubernetes集群上安装高可用的Harbor仓库，今天我就用这篇文章来回答大家这个问题。 一、Kubernetes上的高可用Harbor方案 首先，我可以肯定给出一个回答：Harbor支持在Kubernetes部署。只不过Harbor官方的默认安装并非是高可用的，而是“单点式”的。在《基于Harbor的高可用企业级私有容器镜像仓库部署实践》一文中，我曾谈到了一种在裸机或VM上的、基于Cephfs共享存储的高可用Harbor方案。在Kubernetes上部署，其高可用的思路也是类似的，可见下面这幅示意图： 围绕这幅示意图，简单说明一下我们的方案： 通过在Kubernetes上启动Harbor内部各组件的多个副本的方式实现Harbor服务的计算高可用； 通过挂载CephFS共享存储的方式实现镜像数据高可用； Harbor使用的配置数据和关系数据放在外部(External)数据库集群中，保证数据高可用和实时一致性； 通过外部Redis集群实现UI组件的session共享。 方案确定后，接下来我们就开始部署。 二、环境准备 在Harbor官方的对Kubernetes支持的说明中，提到当前的Harbor on kubernetes相关脚本和配置在Kubernetes v1.6.5和Harbor v1.2.0上验证测试通过了，因此在我们的实验环境中，Kubernetes至少要准备v1.6.5及以后版本。下面是我的环境的一些信息： Kubernetes使用v1.7.3版本： # kubelet --version Kubernetes v1.7.3 Docker使用17.03.2版本： # docker version Client: Version: 17.03.2-ce API version: 1.27 Go version: go1.7.5 Git commit: f5ec1e2 Built: Tue Jun 27 03:35:14 2017 OS/Arch: linux/amd64 Server: Version: 17.03.2-ce API version: 1.27 (minimum version 1.12) Go [...]]]></description>
			<content:encoded><![CDATA[<p>关于<a href="http://tonybai.com/2017/06/09/setup-a-high-availability-private-registry-based-on-harbor-and-cephfs/">基于Harbor的高可用私有镜像仓库</a>，在我的博客里<a href="http://tonybai.com/2017/06/15/fix-auth-fail-when-login-harbor-registry/">曾不止一次提到</a>，在<a href="http://tonybai.com/2017/10/24/go-evolution-for-ten-years-an-interview-by-osc/">源创会2017沈阳站</a>上，我还专门<a href="http://tonybai.com/2017/10/23/the-speech-script-practice-on-deploying-a-ha-harbor-cluster-for-osc-shenyang-2017/">以此题目和大家做了分享</a>。事后，很多人通过<a href="https://weibo.com/bigwhite20xx">微博私信</a>、<a href="https://mp.weixin.qq.com/mp/qrcode?scene=10000004&amp;size=102&amp;__biz=MzIyNzM0MDk0Mg==&amp;mid=2247483828&amp;idx=1&amp;sn=d8bcc352a0ad2fdb5e02f3a2c40c4b2b&amp;send_time=">个人公众号</a>或博客评论问我是否可以在<a href="http://tonybai.com/tag/kubernetes">Kubernetes集群</a>上安装高可用的<a href="https://github.com/vmware/harbor">Harbor</a>仓库，今天我就用这篇文章来回答大家这个问题。</p>
<h2>一、Kubernetes上的高可用Harbor方案</h2>
<p>首先，我可以肯定给出一个回答：Harbor支持在Kubernetes部署。只不过Harbor官方的默认安装并非是高可用的，而是“单点式”的。在<a href="http://tonybai.com/2017/10/23/the-speech-script-practice-on-deploying-a-ha-harbor-cluster-for-osc-shenyang-2017/">《基于Harbor的高可用企业级私有容器镜像仓库部署实践》</a>一文中，我曾谈到了一种在裸机或VM上的、基于<a href="http://tonybai.com/2017/05/08/mount-cephfs-acrossing-nodes-in-kubernetes-cluster/">Cephfs</a>共享存储的高可用Harbor方案。在Kubernetes上部署，其高可用的思路也是类似的，可见下面这幅示意图：</p>
<p><img src="http://tonybai.com/wp-content/uploads/harbor-on-k8s-arch.png" alt="img{512x368}" /></p>
<p>围绕这幅示意图，简单说明一下我们的方案：</p>
<ul>
<li>通过在Kubernetes上启动Harbor内部各组件的多个副本的方式实现Harbor服务的计算高可用；</li>
<li>通过挂载CephFS共享存储的方式实现镜像数据高可用；</li>
<li>Harbor使用的配置数据和关系数据放在外部(External)数据库集群中，保证数据高可用和实时一致性；</li>
<li>通过外部<a href="https://redis.io/">Redis</a>集群实现UI组件的session共享。</li>
</ul>
<p>方案确定后，接下来我们就开始部署。</p>
<h2>二、环境准备</h2>
<p>在Harbor官方的<a href="https://github.com/vmware/harbor/blob/master/docs/kubernetes_deployment.md">对Kubernetes支持的说明</a>中，提到当前的Harbor on kubernetes相关脚本和配置在Kubernetes v1.6.5和Harbor v1.2.0上验证测试通过了，因此在我们的实验环境中，<a href="http://tonybai.com/tag/kubernetes">Kubernetes</a>至少要准备v1.6.5及以后版本。下面是我的环境的一些信息：</p>
<pre><code>Kubernetes使用v1.7.3版本：

# kubelet --version
Kubernetes v1.7.3

Docker使用17.03.2版本：

# docker version
Client:
 Version:      17.03.2-ce
 API version:  1.27
 Go version:   go1.7.5
 Git commit:   f5ec1e2
 Built:        Tue Jun 27 03:35:14 2017
 OS/Arch:      linux/amd64

Server:
 Version:      17.03.2-ce
 API version:  1.27 (minimum version 1.12)
 Go version:   go1.7.5
 Git commit:   f5ec1e2
 Built:        Tue Jun 27 03:35:14 2017
 OS/Arch:      linux/amd64
 Experimental: false

</code></pre>
<p>关于Harbor的相关脚本，我们直接用master branch中的，而不是v1.2.0这个release版本中的。<strong>切记</strong>！否则你会发现v1.2.0版本源码中的相关kubernetes支持脚本根本就没法工作，甚至缺少adminserver组件的相关脚本。不过Harbor相关组件的image版本，我们使用的还是<strong>v1.2.0</strong>的：</p>
<pre><code>Harbor源码的版本：

commit 82d842d77c01657589d67af0ea2d0c66b1f96014
Merge pull request #3741 from wy65701436/add-tc-concourse   on Dec 4, 2017

Harbor各组件的image的版本：

REPOSITORY                      TAG                 IMAGE ID
vmware/harbor-jobservice      v1.2.0          1fb18427db11
vmware/harbor-ui              v1.2.0          b7069ac3bd4b
vmware/harbor-adminserver     v1.2.0          a18331f0c1ae
vmware/registry               2.6.2-photon    c38af846a0da
vmware/nginx-photon           1.11.13         2971c92cc1ae
</code></pre>
<p>除此之外，高可用Harbor使用外部的DB cluster和redis cluster，DB cluster我们采用MySQL，对于MySQL cluster，可以使用<a href="http://galeracluster.com/products/">mysql galera cluster</a>或MySQL5.7以上版本自带的Group Replication (MGR) 集群。</p>
<h2>三、探索harbor on k8s部署脚本和配置</h2>
<p>我们在本地创建harbor-install-on-k8s目录，并将Harbor最新源码下载到该目录下：</p>
<pre><code># mkdir harbor-install-on-k8s
# cd harbor-install-on-k8s
# wget -c https://github.com/vmware/harbor/archive/master.zip
# unzip master.zip
# cd harbor-master
# ls -F
AUTHORS  CHANGELOG.md  contrib/  CONTRIBUTING.md  docs/
LICENSE  make/  Makefile  NOTICE  partners.md  README.md
ROADMAP.md  src/  tests/  tools/  VERSION

</code></pre>
<p>将Harbor部署到k8s上的脚本就在make/kubernetes目录下：</p>
<pre><code># cd harbor-master/make
# tree kubernetes
kubernetes
├── adminserver
│   ├── adminserver.rc.yaml
│   └── adminserver.svc.yaml
├── jobservice
│   ├── jobservice.rc.yaml
│   └── jobservice.svc.yaml
├── k8s-prepare
├── mysql
│   ├── mysql.rc.yaml
│   └── mysql.svc.yaml
├── nginx
│   ├── nginx.rc.yaml
│   └── nginx.svc.yaml
├── pv
│   ├── log.pvc.yaml
│   ├── log.pv.yaml
│   ├── registry.pvc.yaml
│   ├── registry.pv.yaml
│   ├── storage.pvc.yaml
│   └── storage.pv.yaml
├── registry
│   ├── registry.rc.yaml
│   └── registry.svc.yaml
├── templates
│   ├── adminserver.cm.yaml
│   ├── jobservice.cm.yaml
│   ├── mysql.cm.yaml
│   ├── nginx.cm.yaml
│   ├── registry.cm.yaml
│   └── ui.cm.yaml
└── ui
    ├── ui.rc.yaml
    └── ui.svc.yaml

8 directories, 25 files

</code></pre>
<ul>
<li>k8s-prepare脚本：根据templates下的模板文件以及harbor.cfg中的配置生成各个组件，比如registry等的最终configmap配置文件。它的作用类似于用docker-compose工具部署Harbor时的prepare脚本；</li>
<li>templates目录：templates目录下放置各个组件的配置模板文件（configmap文件模板），将作为k8s-prepare的输入；</li>
<li>pv目录：Harbor组件所使用的存储插件的配置，默认情况下使用hostpath，对于高可用Harbor而言，我们这里将使用cephfs；</li>
<li>其他组件目录，比如：registry：这些目录中存放这各个组件的service yaml和rc yaml，用于在Kubernetes cluster启动各个组件时使用。</li>
</ul>
<p>下面我用一个示意图来形象地描述一下配置的生成过程以及各个文件在后续Harbor组件启动中的作用：</p>
<p><img src="http://tonybai.com/wp-content/uploads/harbor-on-k8s-config-generating-and-components-launch-flow.png" alt="img{512x368}" /></p>
<p>由于使用external mysql db，Harbor自带的mysql组件我们不会使用，对应的pv目录下的storage.pv.yaml和storage.pvc.yaml我们也不会去关注和使用。</p>
<h2>四、部署步骤</h2>
<h3>1、配置和创建挂载Cephfs的pv和pvc</h3>
<p>我们先在共享分布式存储CephFS上为Harbor的存储需求创建目录：apps/harbor-k8s，并在harbor-k8s下创建两个子目录：log和registry，分别满足jobservice和registry的存储需求：</p>
<pre><code># cd /mnt   // CephFS的根目录挂载到了/mnt下面
# mkdir -p apps/harbor-k8s/log
# mkdir -p apps/harbor-k8s/registry
# tree apps/harbor-k8s
apps/harbor-k8s
├── log
└── registry
</code></pre>
<p>关于CephFS的挂载等具体操作步骤，可以参见我的<a href="http://tonybai.com/2017/05/08/mount-cephfs-acrossing-nodes-in-kubernetes-cluster/">《Kubernetes集群跨节点挂载CephFS》</a>一文。</p>
<p>接下来，创建用于k8s pv挂载cephfs的ceph-secret，我们编写一个ceph-secret.yaml文件：</p>
<pre><code>//ceph-secret.yaml
apiVersion: v1
data:
  key: {base64 encoding of the ceph admin.secret}
kind: Secret
metadata:
  name: ceph-secret
type: Opaque
</code></pre>
<p>创建ceph-secret：</p>
<pre><code># kubectl create -f ceph-secret.yaml
secret "ceph-secret" created
</code></pre>
<p>最后，我们来修改pv、pvc文件并创建对应的pv和pvc资源，要修改的文件包括pv/log.xxx和pv/registry.xxx，我们的目的就是用cephfs替代原先的hostPath：</p>
<pre><code>//log.pv.yaml

apiVersion: v1
kind: PersistentVolume
metadata:
  name: log-pv
  labels:
    type: log
spec:
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteMany
  cephfs:
    monitors:
      - {ceph-mon-node-ip}:6789
    path: /apps/harbor-k8s/log
    user: admin
    secretRef:
      name: ceph-secret
    readOnly: false
  persistentVolumeReclaimPolicy: Retain

//log.pvc.yaml

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: log-pvc
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 1Gi
  selector:
    matchLabels:
      type: log

// registry.pv.yaml

apiVersion: v1
kind: PersistentVolume
metadata:
  name: registry-pv
  labels:
    type: registry
spec:
  capacity:
    storage: 5Gi
  accessModes:
    - ReadWriteMany
  cephfs:
    monitors:
      - 10.47.217.91:6789
    path: /apps/harbor-k8s/registry
    user: admin
    secretRef:
      name: ceph-secret
    readOnly: false
  persistentVolumeReclaimPolicy: Retain

//registry.pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: registry-pvc
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 5Gi
  selector:
    matchLabels:
      type: registry
</code></pre>
<p>创建pv和pvc：</p>
<pre><code># kubectl create -f log.pv.yaml
persistentvolume "log-pv" created
# kubectl create -f log.pvc.yaml
persistentvolumeclaim "log-pvc" created
# kubectl create -f registry.pv.yaml
persistentvolume "registry-pv" created
# kubectl create -f registry.pvc.yaml
persistentvolumeclaim "registry-pvc" created
# kubectl get pvc
NAME           STATUS    VOLUME        CAPACITY   ACCESSMODES   STORAGECLASS   AGE
log-pvc        Bound     log-pv        1Gi        RWX                          31s
registry-pvc   Bound     registry-pv   5Gi        RWX                          2s
# kubectl get pv
NAME          CAPACITY   ACCESSMODES   RECLAIMPOLICY   STATUS    CLAIM                  STORAGECLASS   REASON    AGE
log-pv        1Gi        RWX           Retain          Bound     default/log-pvc                                 36s
registry-pv   5Gi        RWX           Retain          Bound     default/registry-pvc                            6s
</code></pre>
<h3>2、创建和初始化Harbor用的数据库</h3>
<p>我们需要在External DB中创建Harbor访问数据库所用的user(harbork8s/harbork8s)以及所使用的数据库(registry_k8s)：</p>
<pre><code>mysql&gt; create user harbork8s identified  by 'harbork8s';
Query OK, 0 rows affected (0.03 sec)

mysql&gt; GRANT ALL PRIVILEGES ON *.* TO 'harbork8s'@'%' IDENTIFIED BY 'harbork8s' WITH GRANT OPTION;
Query OK, 0 rows affected, 1 warning (0.00 sec)

# mysql&gt; create database registry_k8s;
Query OK, 1 row affected (0.00 sec)

mysql&gt; grant all on registry_k8s.* to 'harbork8s' identified by 'harbork8s';
Query OK, 0 rows affected, 1 warning (0.00 sec)

</code></pre>
<p>由于目前Harbor还不支持自动init数据库，因此我们需要为新建的registry_k8s数据库做初始化，具体的方案就是先使用docker-compose工具在本地启动一个harbor，通过mysqldump将harbor-db container中的数据表dump出来，再导入到external db中的registry_k8s中，具体操作步骤如下：</p>
<pre><code># wget -c http://harbor.orientsoft.cn/harbor-1.2.0/harbor-offline-installer-v1.2.0.tgz
# tar zxvf harbor-offline-installer-v1.2.0.tgz

进入harbor目录，修改harbor.cfg中的hostname:

hostname = hub.tonybai.com:31777

# ./prepare
# docker-compose up -d

找到harbor_db的container id: 77fde71390e7，进入容器，并将数据库registry dump出来：

# docker exec -i -t  77fde71390e7 bash
# mysqldump -u root -pxxx --databases registry &gt; registry.dump

离开容器，将容器内导出的registry.dump copy到本地：
# docker cp 77fde71390e7:/tmp/registry.dump ./

修改registry.dump为registry_k8s.dump，修改其内容中的registry为registry_k8s，然后导入到external db：

# mysqldump -h external_db_ip -P 3306 -u harbork8s -pharbork8s
mysql&gt; source ./registry_k8s.dump;

</code></pre>
<h3>3、配置make/harbor.cfg</h3>
<p>harbor.cfg是整个配置生成的重要输入，我们在k8s-prepare执行之前，先要根据我们的需要和环境对harbor.cfg进行配置：</p>
<pre><code>// make/harbor.cfg
hostname = hub.tonybai.com:31777
db_password = harbork8s
db_host = {external_db_ip}
db_user = harbork8s
</code></pre>
<h3>4、对templates目录下的configmap配置模板(*.cm.yaml)进行配置调整</h3>
<ul>
<li>templates/adminserver.cm.yaml:</li>
</ul>
<pre><code>MYSQL_HOST: {external_db_ip}
MYSQL_USR: harbork8s
MYSQL_DATABASE: registry_k8s
RESET: "true"
</code></pre>
<p>注：adminserver.cm.yaml没有使用harbor.cfg中的有关数据库的配置项，而是需要单独再配置一遍，这块估计将来会fix掉这个问题。</p>
<ul>
<li>templates/registry.cm.yaml:</li>
</ul>
<pre><code>rootcertbundle: /etc/registry/root.crt
</code></pre>
<ul>
<li>templates/ui.cm.yaml:</li>
</ul>
<p>ui组件需要添加session共享。ui组件读取_REDIS_URL环境变量：</p>
<pre><code>//vmware/harbor/src/ui/main.go
... ..
    redisURL := os.Getenv("_REDIS_URL")
    if len(redisURL) &gt; 0 {
        beego.BConfig.WebConfig.Session.SessionProvider = "redis"
        beego.BConfig.WebConfig.Session.SessionProviderConfig = redisURL
    }
... ...

而redisURL的格式在beego的源码中有说明：

// beego/session/redis/sess_redis.go

// SessionInit init redis session
// savepath like redis server addr,pool size,password,dbnum
// e.g. 127.0.0.1:6379,100,astaxie,0
func (rp *Provider) SessionInit(maxlifetime int64, savePath string) error {...}
</code></pre>
<p>因此，我们在templates/ui.cm.yaml中添加一行：</p>
<pre><code>_REDIS_URL: {redis_ip}:6379,100,{redis_password},11
</code></pre>
<p>jobservice.cm.yaml和nginx.cm.yaml无需改变。</p>
<h3>5、对各组件目录下的xxx.rc.yaml和xxx.svc.yaml配置模板进行配置调整</h3>
<ul>
<li>adminserver/adminserver.rc.yaml</li>
</ul>
<pre><code>replicas: 3
</code></pre>
<ul>
<li>adminserver/adminserver.svc.yaml</li>
</ul>
<p>不变。</p>
<ul>
<li>jobservice/jobservice.rc.yaml、jobservice/jobservice.svc.yaml</li>
</ul>
<p>不变。</p>
<ul>
<li>nginx/nginx.rc.yaml</li>
</ul>
<pre><code>replicas: 3
</code></pre>
<ul>
<li>nginx/nginx.svc.yaml</li>
</ul>
<pre><code>apiVersion: v1
kind: Service
metadata:
  name: nginx
spec:
  type: NodePort
  ports:
    - name: http
      port: 80
      nodePort: 31777
      protocol: TCP
  selector:
    name: nginx-apps
</code></pre>
<ul>
<li>registry/registry.rc.yaml</li>
</ul>
<pre><code>replicas: 3
mountPath: /etc/registry
</code></pre>
<p><strong>这里有一个严重的<a href="https://github.com/vmware/harbor/issues/3637">bug</a></strong>，即registry.rc.yaml中configmap的默认mount路径：/etc/docker/registry与registry的docker image中的registry配置文件的路径/etc/registry不一致，这将导致我们精心配置的registry的configmap根本没有发挥作用，数据依然在memory中，而不是在我们配置的Cephfs中。这样一旦registry container退出，仓库的image数据就会丢失。同时也无法实现数据的高可用。因此，我们将mountPath都改为与registry image的一致，即：/etc/registry目录。</p>
<ul>
<li>registry/registry.svc.yaml</li>
</ul>
<p>不变。</p>
<ul>
<li>ui/ui.rc.yaml</li>
</ul>
<pre><code>replicas: 3
</code></pre>
<ul>
<li>ui/ui.svc.yaml</li>
</ul>
<pre><code>- name: _REDIS_URL
             valueFrom:
               configMapKeyRef:
                 name: harbor-ui-config
                 key: _REDIS_URL
</code></pre>
<h3>6、执行k8s-prepare</h3>
<p>执行k8s-prepare，生成各个组件的configmap文件：</p>
<pre><code># ./k8s-prepare
# git status
 ... ...

    adminserver/adminserver.cm.yaml
    jobservice/jobservice.cm.yaml
    mysql/mysql.cm.yaml
    nginx/nginx.cm.yaml
    registry/registry.cm.yaml
    ui/ui.cm.yaml
</code></pre>
<h3>7、启动Harbor组件</h3>
<ul>
<li>创建configmap</li>
</ul>
<pre><code># kubectl apply -f jobservice/jobservice.cm.yaml
configmap "harbor-jobservice-config" created
# kubectl apply -f nginx/nginx.cm.yaml
configmap "harbor-nginx-config" created
# kubectl apply -f registry/registry.cm.yaml
configmap "harbor-registry-config" created
# kubectl apply -f ui/ui.cm.yaml
configmap "harbor-ui-config" created
# kubectl apply -f adminserver/adminserver.cm.yaml
configmap "harbor-adminserver-config" created

# kubectl get cm
NAME                        DATA      AGE
harbor-adminserver-config   42        14s
harbor-jobservice-config    8         16s
harbor-nginx-config         3         16s
harbor-registry-config      2         15s
harbor-ui-config            9         15s
</code></pre>
<ul>
<li>创建harbor各组件对应的k8s service</li>
</ul>
<pre><code># kubectl apply -f jobservice/jobservice.svc.yaml
service "jobservice" created
# kubectl apply -f nginx/nginx.svc.yaml
service "nginx" created
# kubectl apply -f registry/registry.svc.yaml
service "registry" created
# kubectl apply -f ui/ui.svc.yaml
service "ui" created
# kubectl apply -f adminserver/adminserver.svc.yaml
service "adminserver" created

# kubectl get svc
NAME               CLUSTER-IP      EXTERNAL-IP   PORT(S)
adminserver        10.103.7.8      &lt;none&gt;        80/TCP
jobservice         10.104.14.178   &lt;none&gt;        80/TCP
nginx              10.103.46.129   &lt;nodes&gt;       80:31777/TCP
registry           10.101.185.42   &lt;none&gt;        5000/TCP,5001/TCP
ui                 10.96.29.187    &lt;none&gt;        80/TCP
</code></pre>
<ul>
<li>创建rc，启动各个组件pods</li>
</ul>
<pre><code># kubectl apply -f registry/registry.rc.yaml
replicationcontroller "registry-rc" created
# kubectl apply -f jobservice/jobservice.rc.yaml
replicationcontroller "jobservice-rc" created
# kubectl apply -f ui/ui.rc.yaml
replicationcontroller "ui-rc" created
# kubectl apply -f nginx/nginx.rc.yaml
replicationcontroller "nginx-rc" created
# kubectl apply -f adminserver/adminserver.rc.yaml
replicationcontroller "adminserver-rc" created

#kubectl get pods
NAMESPACE     NAME                  READY     STATUS    RESTARTS   AGE
default       adminserver-rc-9pc78  1/1       Running   0          3m
default       adminserver-rc-pfqtv  1/1       Running   0          3m
default       adminserver-rc-w55sx  1/1       Running   0          3m
default       jobservice-rc-d18zk   1/1       Running   1          3m
default       nginx-rc-3t5km        1/1       Running   0          3m
default       nginx-rc-6wwtz        1/1       Running   0          3m
default       nginx-rc-dq64p        1/1       Running   0          3m
default       registry-rc-6w3b7     1/1       Running   0          3m
default       registry-rc-dfdld     1/1       Running   0          3m
default       registry-rc-t6fnx     1/1       Running   0          3m
default       ui-rc-0kwrz           1/1       Running   1          3m
default       ui-rc-kzs8d           1/1       Running   1          3m
default       ui-rc-vph6d           1/1       Running   1          3m

</code></pre>
<h2>五、验证与Troubleshooting</h2>
<h3>1、docker cli访问</h3>
<p>由于harbor默认使用了http访问，因此在docker login前先要将我们的仓库地址加到/etc/docker/daemon.json的insecure-registries中：</p>
<pre><code>///etc/docker/daemon.json
{
  "insecure-registries": ["hub.tonybai.com:31777"]
}
</code></pre>
<p>systemctl daemon-reload and restart后，我们就可以通过docker login登录新建的仓库了(初始密码：Harbor12345)：</p>
<pre><code> docker login hub.tonybai.com:31777
Username (admin): admin
Password:
Login Succeeded

</code></pre>
<h3>2、docker push &amp; pull</h3>
<p>我们测试上传一个busybox image：</p>
<pre><code># docker pull busybox
Using default tag: latest
latest: Pulling from library/busybox
0ffadd58f2a6: Pull complete
Digest: sha256:bbc3a03235220b170ba48a157dd097dd1379299370e1ed99ce976df0355d24f0
Status: Downloaded newer image for busybox:latest
# docker tag busybox:latest hub.tonybai.com:31777/library/busybox:latest
# docker push hub.tonybai.com:31777/library/busybox:latest
The push refers to a repository [hub.tonybai.com:31777/library/busybox]
0271b8eebde3: Preparing
0271b8eebde3: Pushing [==================================================&gt;] 1.338 MB
0271b8eebde3: Pushed
latest: digest: sha256:179cf024c8a22f1621ea012bfc84b0df7e393cb80bf3638ac80e30d23e69147f size: 527
</code></pre>
<p>下载刚刚上传的busybox:</p>
<pre><code># docker pull hub.tonybai.com:31777/library/busybox:latest
latest: Pulling from library/busybox
414e5515492a: Pull complete
Digest: sha256:179cf024c8a22f1621ea012bfc84b0df7e393cb80bf3638ac80e30d23e69147f
Status: Downloaded newer image for hub.tonybai.com:31777/library/busybox:latest
</code></pre>
<h3>3、访问Harbor UI</h3>
<p>在浏览器中打开http://hub.tonybai.com:31777，用admin/Harbor12345登录，如果看到下面页面，说明安装部署成功了：</p>
<p><img src="http://tonybai.com/wp-content/uploads/harbor-on-k8s-ui.png" alt="img{512x368}" /></p>
<h2>六、参考资料</h2>
<ul>
<li><a href="https://github.com/vmware/harbor/blob/master/docs/kubernetes_deployment.md">Integration with Kubernetes</a></li>
<li><a href="http://tonybai.com/2017/06/09/setup-a-high-availability-private-registry-based-on-harbor-and-cephfs/">基于Harbor和CephFS搭建高可用Private Registry</a></li>
</ul>
<hr />
<p>微博：<a href="http://weibo.com/bigwhite20xx">@tonybai_cn</a><br />
微信公众号：iamtonybai<br />
github.com: https://github.com/bigwhite</p>
<p>微信赞赏：<br />
<img src="http://tonybai.com/wp-content/uploads/wechat-zanshang-code-512x512.jpg" alt="img{512x368}" /></p>
<p style='text-align:left'>&copy; 2017, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2017/12/08/deploy-high-availability-harbor-on-kubernetes-cluster/feed/</wfw:commentRss>
		<slash:comments>17</slash:comments>
		</item>
		<item>
		<title>源创会2017沈阳站讲稿：基于Harbor的高可用企业级私有容器镜像仓库部署实践</title>
		<link>https://tonybai.com/2017/10/23/the-speech-script-practice-on-deploying-a-ha-harbor-cluster-for-osc-shenyang-2017/</link>
		<comments>https://tonybai.com/2017/10/23/the-speech-script-practice-on-deploying-a-ha-harbor-cluster-for-osc-shenyang-2017/#comments</comments>
		<pubDate>Mon, 23 Oct 2017 08:28:35 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[AD]]></category>
		<category><![CDATA[beego]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[CD]]></category>
		<category><![CDATA[Ceph]]></category>
		<category><![CDATA[CephFS]]></category>
		<category><![CDATA[cgroups]]></category>
		<category><![CDATA[CI]]></category>
		<category><![CDATA[daemon.json]]></category>
		<category><![CDATA[distribution]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[docker-compose]]></category>
		<category><![CDATA[docker-registry]]></category>
		<category><![CDATA[dotCloud]]></category>
		<category><![CDATA[DX]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[GroupReplication]]></category>
		<category><![CDATA[harbor]]></category>
		<category><![CDATA[High-Available]]></category>
		<category><![CDATA[image]]></category>
		<category><![CDATA[Kernel]]></category>
		<category><![CDATA[KVM]]></category>
		<category><![CDATA[LDAP]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[linux-container]]></category>
		<category><![CDATA[loadbalance]]></category>
		<category><![CDATA[LXC]]></category>
		<category><![CDATA[MGR]]></category>
		<category><![CDATA[mount]]></category>
		<category><![CDATA[MVC]]></category>
		<category><![CDATA[MySQL]]></category>
		<category><![CDATA[namespaces]]></category>
		<category><![CDATA[osc]]></category>
		<category><![CDATA[pipeline]]></category>
		<category><![CDATA[portus]]></category>
		<category><![CDATA[redis]]></category>
		<category><![CDATA[registry]]></category>
		<category><![CDATA[Scaling]]></category>
		<category><![CDATA[SUSE]]></category>
		<category><![CDATA[unionfs]]></category>
		<category><![CDATA[vm]]></category>
		<category><![CDATA[vmware]]></category>
		<category><![CDATA[Xen]]></category>
		<category><![CDATA[伸缩]]></category>
		<category><![CDATA[容器]]></category>
		<category><![CDATA[开发流水线]]></category>
		<category><![CDATA[开源中国]]></category>
		<category><![CDATA[持续交付]]></category>
		<category><![CDATA[持续集成]]></category>
		<category><![CDATA[虚拟机]]></category>
		<category><![CDATA[镜像仓库]]></category>
		<category><![CDATA[高可用]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=2427</guid>
		<description><![CDATA[上周六开源中国的源创会在沈阳举办了一次技术活动，很荣幸以本地讲师的身份和大家交流了一个topic: “基于Harbor的高可用企业级私有容器镜像仓库部署实践”。之所以选择这个topic，是因为这是我们团队的项目实践心得。很多企业和组织在深入使用Docker之后，都会有类似的高可用私有容器仓库搭建的需求，于是我就把我们摸索的实践和填坑过程拿出来，用30分钟与大家分享一下。另外这算是一个入门级的分享，并未深入过多原理。以下就是本次分享的内容讲稿整理。如有不妥或不正确的地方，欢迎交流指正。 大家下午好，欢迎各位来到源创会沈阳站。在这里我也代表沈阳的IT人欢迎源创会来到沈阳，希望能有更多的像源创会这样的组织到沈阳举办技术活动。非常高兴能有这个机会在源创会这个平台上做分享， 今天和大家一起探讨的题目是：“基于Harbor的高可用企业级私有容器镜像仓库部署实践”。题目有些长，简单来说就是如何搭建一个好用的镜像仓库。 首先做个简单的自我介绍。我叫白明，东软(注：源创会这次活动的会场在东软沈阳园区)是我的主场，在这里工作很多年，目前就职东软云科技；Gopher一枚，近两年主要使用Go语言开发；技术译者，曾参与翻译过《七周七语言》一书；并且参与过智慧城市架构系列丛书的编著工作；GopherChina大会讲师，这里顺便说一下GopherChina大会，它是目前中国地区规模最大、水平最高的Go语言技术大会，一般每年4月份在北京或上海举行。希望有志于Go语言开发的开发者积极参与；Blogger，写博10多年，依旧笔耕不倦；目前主要从事Docker&#38;kubernetes的研究和实践。 当今，IT技术发展飞快。五年前， IT从业者口中谈论最多的技术是Virtual Machine，即虚拟化技术，人们经常争论的是到底是vmware的技术好,还是原生kvm技术稳定，又或是xen的技术完美。转眼间五年过去了，大家口中经常讨论的技术词汇发生了变化，越来越多的技术人在谈论Docker，谈论容器。 Docker是什么？ Docker这门技术非常热，但我们要透过现象看其本质： Docker技术并不是新技术，而是将已有技术进行了更好的整合和包装。 内核容器技术以一种完整形态最早出现在Sun公司的Solaris操作系统上，Solaris是当时最先进的服务器操作系统。2005年Solaris发布Solaris Container技术，从此开启了内核容器之门。 IT技术发展的趋势就是这样：商业有的，开源也要有。三年后，即2008年，以Google公司开发人员为主导的Linux Container，LXC功能在被merge到Linux内核。LXC是一种内核级虚拟化技术，主要基于namespaces和cgroup技术，实现共享一个os kernel前提下的进程资源隔离，为进程提供独立的虚拟执行环境，这样的一个虚拟的执行环境就是一个容器。本质上说，LXC容器与现在的Docker所提供容器是一样的。但是，当时LXC处于早期阶段，开发人员可能更为关注LXC的技术实现，而对开发体验方面有所忽略，导致LXC技术使用门槛较高，普通应用开发者学习、理解和使用它的心智负担较高，因此应用并不广泛。 这一情况一直持续到2013年，当时美国一家名不见经传的公司dotCloud发布了一款平台工具Docker，对外宣称可以实现：“build,ship and run any app and anywhere”。Docker实质上也是基于namespaces和cgroup技术的，Docker的创新之处在于其基于union fs技术定义了一套应用打包规范，真正将应用及其运行的所有依赖都封装到一个特定格式的文件中，这种文件就被称为image，即镜像文件。同时，Docker还提供了一套抽象层次更高的工具集，这套工具对dev十分友好，具有良好的开发体验(Developer eXperience)，开发者无需关心namespace, cgroups之类底层技术，即可很easy的启动一个承载着其应用的容器： Docker run ubuntu echo hello 因此， 从2013发布以来，Docker项目就像坐上了火箭，发展迅猛，目前已经是github上最火爆的开源项目之一。这里还要提一点就是：Docker项目是使用go语言开发的，Docker项目的成功，也或多或少得益于Go优异的开发效率和执行效率。 Docker技术的出现究竟给我们带来了哪些好处呢，个人觉得至少有以下三点： 交付标准化：Docker使得应用程序和依赖的运行环境真正绑定结合为一体，得之即用。这让开发人员、测试和运维实现了围绕同一交付物，保持开发交付上下文同步的能力，即“test what you write, ship what you test”； 执行高效化：应用的启动速度从原先虚拟机的分钟级缩短到容器的秒级甚至ms级，使得应用可以支持快速scaling伸缩； 资源集约化：与vm不同的是，Container共享一个内核，这使得一个container的资源消耗仅为进程级别或进程组级别。同时，容器的镜像也因为如此，其size可以实现的很小，最小可能不足1k，平均几十M。与vm动辄几百兆的庞大身段相比，具有较大优势。 有了image文件后，自然而言我们就有了对image进行存取和管理的需求，即我们需要一个镜像仓库，于是Docker推出了Docker registry这个项目。Docker Registry就是Docker image的仓库，用来存储、管理和分发image的；Docker registry由Docker公司实现，项目名为distribution，其实现了Docker Registr 2.0协议，与早前的Registry 1.x协议版本相比，Distribution采用Go语言替换了Python，在安全性和性能方面都有了大幅提升；Docker官方运行着一个世界最大的公共镜像仓库：hub.docker.com，最常用的image都在hub上，比如反向代理nginx、redis、ubuntu等。鉴于国内访问hub网速不佳，多使用国内容器服务厂商提供的加速器。Docker官方还将Registry本身打入到了一个image中，方便开发人员快速以容器形式启动一个Registry： docker run -d [...]]]></description>
			<content:encoded><![CDATA[<p>上周六<a href="http://www.oschina.net/">开源中国</a>的<a href="https://www.oschina.net/event/ych">源创会</a>在沈阳举办了一次技术活动，很荣幸以本地讲师的身份和大家交流了一个topic: “基于<a href="https://github.com/vmware/harbor">Harbor</a>的高可用企业级私有容器镜像仓库部署实践”。之所以选择这个topic，是因为这是我们团队的项目实践心得。很多企业和组织在深入使用Docker之后，都会有类似的高可用私有容器仓库搭建的需求，于是我就把我们摸索的实践和填坑过程拿出来，用30分钟与大家分享一下。另外这算是一个入门级的分享，并未深入过多原理。以下就是本次分享的内容讲稿整理。如有不妥或不正确的地方，欢迎交流指正。</p>
<p><img src="http://tonybai.com/wp-content/uploads/osc-shenyang-2017-1.jpg" alt="img{512x368}" /></p>
<p>大家下午好，欢迎各位来到源创会沈阳站。在这里我也代表沈阳的IT人欢迎源创会来到沈阳，希望能有更多的像源创会这样的组织到沈阳举办技术活动。非常高兴能有这个机会在源创会这个平台上做分享， 今天和大家一起探讨的题目是：“<a href="https://github.com/bigwhite/talks/tree/master/osc/2017">基于Harbor的高可用企业级私有容器镜像仓库部署实践</a>”。题目有些长，简单来说就是如何搭建一个好用的镜像仓库。</p>
<p><img src="http://tonybai.com/wp-content/uploads/osc-shenyang-2017-2.jpg" alt="img{512x368}" /></p>
<p>首先做个简单的自我介绍。我叫白明，东软(注：源创会这次活动的会场在东软沈阳园区)是我的主场，在这里工作很多年，目前就职东软云科技；<a href="https://golang.org/">Gopher</a>一枚，近两年主要使用Go语言开发；技术译者，曾参与翻译过《<a href="https://book.douban.com/subject/10555435/">七周七语言</a>》一书；并且参与过智慧城市架构系列丛书的编著工作；<a href="http://tonybai.com/2017/04/18/my-experience-of-gopherchina-2017-as-a-speaker/">GopherChina大会讲师</a>，这里顺便说一下<a href="http://gopherchina.org/">GopherChina大会</a>，它是目前中国地区规模最大、水平最高的Go语言技术大会，一般每年4月份在北京或上海举行。希望有志于<a href="http://tonybai.com/tag/go">Go语言</a>开发的开发者积极参与；Blogger，写博10多年，依旧笔耕不倦；目前主要从事<a href="http://tonybai.com/tag/docker">Docker</a>&amp;<a href="http://tonybai.com/tag/kubernetes">kubernetes</a>的研究和实践。</p>
<p>当今，IT技术发展飞快。五年前， IT从业者口中谈论最多的技术是<a href="https://en.wikipedia.org/wiki/Virtual_machine">Virtual Machine</a>，即虚拟化技术，人们经常争论的是到底是vmware的技术好,还是原生<a href="https://en.wikipedia.org/wiki/Kernel-based_Virtual_Machine">kvm</a>技术稳定，又或是<a href="https://en.wikipedia.org/wiki/Xen">xen</a>的技术完美。转眼间五年过去了，大家口中经常讨论的技术词汇发生了变化，越来越多的技术人在谈论<a href="https://en.wikipedia.org/wiki/Docker_%28software%29">Docker</a>，谈论容器。</p>
<p>Docker是什么？ Docker这门技术非常热，但我们要透过现象看其本质：</p>
<blockquote>
<p><strong>Docker技术并不是新技术，而是将已有技术进行了更好的整合和包装</strong>。</p>
</blockquote>
<p>内核容器技术以一种完整形态最早出现在<a href="https://en.wikipedia.org/wiki/Sun_Microsystems">Sun公司</a>的<a href="http://en.wikipedia.org/wiki/Solaris_(operating_system)">Solaris操作系统</a>上，Solaris是当时最先进的服务器操作系统。2005年Solaris发布<a href="https://en.wikipedia.org/wiki/Solaris_Containers">Solaris Container</a>技术，从此开启了内核容器之门。</p>
<p>IT技术发展的趋势就是这样：商业有的，开源也要有。三年后，即2008年，以Google公司开发人员为主导的<a href="https://en.wikipedia.org/wiki/LXC">Linux Container，LXC功能</a>在被merge到<a href="https://en.wikipedia.org/wiki/Linux_kernel">Linux内核</a>。LXC是一种内核级虚拟化技术，主要基于<a href="https://en.wikipedia.org/wiki/Linux_namespaces">namespaces</a>和<a href="https://en.wikipedia.org/wiki/Cgroups">cgroup</a>技术，实现共享一个os kernel前提下的进程资源隔离，为进程提供独立的虚拟执行环境，这样的一个虚拟的执行环境就是一个容器。本质上说，LXC容器与现在的Docker所提供容器是一样的。但是，当时LXC处于早期阶段，开发人员可能更为关注LXC的技术实现，而对开发体验方面有所忽略，导致LXC技术使用门槛较高，普通应用开发者学习、理解和使用它的心智负担较高，因此应用并不广泛。</p>
<p>这一情况一直持续到2013年，当时美国一家名不见经传的公司<a href="https://en.wikipedia.org/wiki/DotCloud">dotCloud</a>发布了一款平台工具Docker，对外宣称可以实现：“build,ship and run any app and anywhere”。Docker实质上也是基于namespaces和cgroup技术的，Docker的创新之处在于其基于<a href="https://en.wikipedia.org/wiki/Category:Union_file_systems">union fs技术</a>定义了一套应用打包规范，真正将应用及其运行的所有依赖都封装到一个特定格式的文件中，这种文件就被称为image，即镜像文件。同时，Docker还提供了一套抽象层次更高的工具集，这套工具对dev十分友好，具有良好的开发体验(Developer eXperience)，开发者无需关心namespace, cgroups之类底层技术，即可很easy的启动一个承载着其应用的容器：</p>
<pre><code>Docker run ubuntu echo hello
</code></pre>
<p>因此， 从2013发布以来，Docker项目就像坐上了火箭，发展迅猛，目前已经是github上最火爆的开源项目之一。这里还要提一点就是：Docker项目是使用go语言开发的，Docker项目的成功，也或多或少得益于Go优异的开发效率和执行效率。</p>
<p>Docker技术的出现究竟给我们带来了哪些好处呢，个人觉得至少有以下三点：</p>
<ul>
<li>交付标准化：Docker使得应用程序和依赖的运行环境真正绑定结合为一体，得之即用。这让开发人员、测试和运维实现了围绕同一交付物，保持开发交付上下文同步的能力，即“test what you write, ship what you test”；</li>
<li>执行高效化：应用的启动速度从原先虚拟机的分钟级缩短到容器的秒级甚至ms级，使得应用可以支持快速scaling伸缩；</li>
<li>资源集约化：与vm不同的是，Container共享一个内核，这使得一个container的资源消耗仅为进程级别或进程组级别。同时，容器的镜像也因为如此，其size可以实现的很小，最小可能不足1k，平均几十M。与vm动辄几百兆的庞大身段相比，具有较大优势。</li>
</ul>
<p>有了image文件后，自然而言我们就有了对image进行存取和管理的需求，即我们需要一个镜像仓库，于是Docker推出了Docker registry这个项目。Docker Registry就是Docker image的仓库，用来存储、管理和分发image的；Docker registry由Docker公司实现，项目名为<a href="https://github.com/docker/distribution">distribution</a>，其实现了Docker Registr 2.0协议，与早前的Registry 1.x协议版本相比，Distribution采用Go语言替换了Python，在安全性和性能方面都有了大幅提升；Docker官方运行着一个世界最大的公共镜像仓库：hub.docker.com，最常用的image都在hub上，比如反向代理nginx、redis、ubuntu等。鉴于国内访问hub网速不佳，多使用国内容器服务厂商提供的加速器。Docker官方还将Registry本身打入到了一个image中，方便开发人员快速以容器形式启动一个Registry：</p>
<pre><code>docker run -d -p 5000:5000 --restart=always --name registry registry:2
</code></pre>
<p>不过，这样启动的Registry更多仅仅是一个Demo级别或满足个体开发者自身需要的，离满足企业内部开发流程或生产需求还差了许多。</p>
<p>既然Docker官方运行着免费的镜像仓库，那我们还需要自己搭建吗？实际情况是，对Docker的使用越深入，对私有仓库的需求可能就越迫切。我们先来看一组Docker 2016官方的调查数据，看看Docker都应用在哪些场合。 从Docker 2016官方调查来看，Docker 更多用于dev、<a href="https://en.wikipedia.org/wiki/Continuous_integration">ci</a>和<a href="https://en.wikipedia.org/wiki/DevOps">DevOps</a>等环节，这三个场合下的应用占据了半壁江山。而相比于公共仓库，私有镜像仓库能更好的满足开发人员在这些场合对镜像仓库的需求。理由至少有四点：</p>
<ul>
<li>
<p>便于集成到内部CI/Cd<br />
以我司内部为例，由于公司内部办公需要使用正向代理访问外部网络，要想将Public Registry集成到你的内部CI中，技术上就会有很多坎儿，整个搭建过程可能是非常痛苦的；</p>
</li>
<li>
<p>对镜像可以更全面掌控<br />
一般来说，外部Public Registry提供的管理功能相对单一，往往无法满足企业内部的开发和交付需求；</p>
</li>
<li>
<p>内部网络，网络传输性能更好<br />
内部开发运维流水线很多环节是有一定的时间敏感性的，比如：一次CI如果因为network问题导致image pull总是timeout，会让dev非常闹心，甚至影响整体的开发和交付效率。</p>
</li>
<li>
<p>出于安全考虑<br />
总是有企业不想将自己开发的软件或数据放到公网上，因此在企业内部选择搭建一个private registry更会让这些企业得到满足；另外企业对仓库的身份验证可能还有LDAP支持的需求，这是外部registry无法满足的。</p>
</li>
</ul>
<p>一旦企业决定搭建自己的private仓库，那么就得做一个private仓库的技术选型。商业版不在我们讨论范围内，我们从开源软件中挑选。不过开源的可选的不多，Docker 官方的Registry更聚焦通用功能，没有针对企业客户需求定制，开源领域我们大致有两个主要候选者：<a href="https://github.com/SUSE/">SUSE</a>的<a href="https://github.com/SUSE/Portus">Portus</a>和Vmware的<a href="https://github.com/vmware/harbor">Harbor</a>。针对开源项目的技术选型，我个人的挑选原则最简单的就是看社区生态，落实到具体的指标上包括：</p>
<ul>
<li>项目关注度（即star数量）</li>
<li>社区对issue的反馈数量和积极性</li>
<li>项目维护者对issue fix的积极程度以及是否有远大的roadmap</li>
</ul>
<p>对比后，我发现在这三个指标上，目前Harbor都暂时领先portus一段距离，于是我们选择Harbor。</p>
<p>Harbor是VMware中国团队开源的企业级镜像仓库项目，聚焦镜像仓库的企业级需求，这里从其官网摘录一些特性，大家一起来看一下：</p>
<p>– 支持基于角色的访问控制RBAC;<br />
– 支持镜像复制策略(PUSH);<br />
– 支持无用镜像数据的自动回收和删除; – 支持LDAP/AD认证;<br />
– Web UI;<br />
– 提供审计日志功能;<br />
– 提供RESTful API,便于扩展;<br />
– 支持中文&amp;部署Easy。</p>
<p>不过，Harbor默认安装的是单实例仓库，并非是<a href="https://en.wikipedia.org/wiki/High_availability">高可用的</a>。对于接纳和使用Docker的企业来说，镜像仓库已经企业内部开发、交付和运维流水线的核心，一旦仓库停掉，流水线将被迫暂停，对开发交付的效率会产生重要影响；对于一些中大型企业组织，单实例的仓库性能也无法满足需求，为此高可用的Harbor势在必行。在设计Harbor HA方案之前，我们简单了解一下Harbor组成架构。</p>
<p>一个Harbor实例就是一组由<a href="https://github.com/docker/compose">docker-compose</a>工具启动的容器服务，主要包括四个主要组件：</p>
<ul>
<li>
<p>proxy<br />
实质就是一个反向代理<a href="http://tonybai.com/tag/nginx">nginx</a>，负责流量路由分担到ui和registry上；</p>
</li>
<li>
<p>registry<br />
这里的registry就是原生的docker官方的registry镜像仓库，Harbor在内部内置了一个仓库，所有仓库的核心功能均是由registry完成的；</p>
</li>
<li>
<p>core service<br />
包含了ui、token和webhook服务；</p>
</li>
<li>
<p>job service<br />
主要用于镜像复制供。</p>
</li>
</ul>
<p>同时，每个Harbor实例还启动了一个MySQL数据库容器，用于保存自身的配置和镜像管理相关的关系数据。</p>
<p>高可用系统一般考虑三方面：计算高可用、存储高可用和网络高可用。在这里我们不考虑网络高可用。基于Harbor的高可用仓库方案，这里列出两个。</p>
<p><img src="http://tonybai.com/wp-content/uploads/harbor-ha-solutions.png" alt="img{512x368}" /></p>
<p>两个方案的共同点是计算高可用，都是通过lb实现的多主热运行，保证无单点；存储高可用则各有各的方案。一个使用了分布式共享存储，数据可靠性由共享存储provider提供；另外一个则需要harbor自身逻辑参与，通过镜像相互复制的方式保持数据的多副本。</p>
<p>两种方案各有优缺点，就看哪种更适合你的组织以及你手里的资源是否能满足方案的搭建要求。</p>
<p>方案1是Harbor开发团队推荐的标准方案，由于基于分布式共享存储，因此其scaling非常好；同样，由于多Harbor实例共享存储，因此可以保持数据是实时一致的。方案1的不足也是很明显的，第一：门槛高，需要具备共享存储provider；第二搭建难度要高于第二个基于镜像复制的方案。</p>
<p>方案2的优点就是首次搭建简单。不足也很多：scaling差，甚至是不能，一旦有三个或三个以上节点，可能就会出现“环形复制”；镜像复制需要时间，因此存在多节点上数据周期性不一致的情况；Harbor的镜像复制规则以Project为单位配置，因此一旦新增Project，需要在每个节点上手工维护复制规则，非常繁琐。因此，我们选择方案1。</p>
<p>我们来看一下方案1的细节： 这是一幅示意图。</p>
<ul>
<li>每个安放harbor实例的node都mount cephfs。ceph是目前最流行的分布式共享存储方案之一；</li>
<li>每个node上的harbor实例（包含组件：ui、registry等）都volume mount node上的cephfs mount路径；</li>
<li>通过Load Balance将request流量负载到各个harbor实例上；</li>
<li>使用外部MySQL cluster替代每个Harbor实例内部自维护的那个MySQL容器；对于MySQL cluster，可以使用<a href="http://galeracluster.com/products/">mysql galera cluster</a>或MySQL5.7以上版本自带的Group Replication (MGR) 集群。</li>
<li>通过外部Redis实现访问Harbor ui的session共享，这个功能是Harbor UI底层MVC框架-<a href="https://github.com/astaxie/beego">beego</a>提供的。</li>
</ul>
<p>接下来，我们就来看具体的部署步骤和细节。</p>
<p>环境和先决条件：</p>
<ul>
<li>三台VM(Ubuntu 16.04及以上版本)；</li>
<li><a href="http://tonybai.com/2016/11/07/integrate-kubernetes-with-ceph-rbd/">CephFS</a>、MySQL、Redis已就绪；</li>
<li>Harbor v1.1.0及以上版本；</li>
<li>一个域名：hub.tonybai.com:8070。我们通过该域名和服务端口访问Harbor，我们可以通过dns解析多ip轮询实现最简单的Load balance，虽然不完美。</li>
</ul>
<h3>第一步：挂载cephfs</h3>
<p>每个安装Harbor instance的节点都要mount cephfs的相关路径，步骤包括：</p>
<pre><code>#安装cephfs内核驱动
apt install ceph-fs-common

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

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

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

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

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

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

		<guid isPermaLink="false">http://tonybai.com/?p=2333</guid>
		<description><![CDATA[我们有给客户搭建私有容器仓库的需求。开源的私有容器registry可供选择的不多，除了docker官方的distribution之外，比较知名的是VMware China出品的Harbor，我们选择了harbor。 harbor在docker distribution的基础上增加了一些安全、访问控制、管理的功能以满足企业对于镜像仓库的需求。harbor以docker-compose的规范形式组织各个组件，并通过docker-compose工具进行启停。 不过，harbor默认的安装配置是针对single node的，要想做得可靠性高一些，我们需要自己探索一些可行的方案。本文将结合harbor和CephFS搭建一个满足企业高可用性需求的private registry。 一、实验环境 这里用两台阿里云ECS作为harbor的工作节点： node1: 10.47.217.91 node2: 10.28.61.30 两台主机运行的都是Ubuntu 16.04.1 LTS (GNU/Linux 4.4.0-58-generic x86_64)，使用root用户。 docker版本与docker-compose的版本如下： # docker version Client: Version: 1.12.5 API version: 1.24 Go version: go1.6.4 Git commit: 7392c3b Built: Fri Dec 16 02:42:17 2016 OS/Arch: linux/amd64 Server: Version: 1.12.5 API version: 1.24 Go version: go1.6.4 Git commit: 7392c3b [...]]]></description>
			<content:encoded><![CDATA[<p>我们有给客户搭建私有容器仓库的需求。开源的私有容器registry可供选择的不多，除了docker官方的<a href="https://github.com/docker/distribution">distribution</a>之外，比较知名的是VMware China出品的<a href="https://github.com/vmware/harbor">Harbor</a>，我们选择了harbor。</p>
<p>harbor在<a href="https://github.com/docker/distribution">docker distribution</a>的基础上增加了一些安全、访问控制、管理的功能以满足企业对于镜像仓库的需求。harbor以<a href="https://github.com/docker/compose">docker-compose</a>的规范形式组织各个组件，并通过docker-compose工具进行启停。</p>
<p>不过，harbor默认的安装配置是针对single node的，要想做得可靠性高一些，我们需要自己探索一些可行的方案。本文将结合harbor和<a href="http://tonybai.com/tag/cephfs">CephFS</a>搭建一个满足企业高可用性需求的private registry。</p>
<h2>一、实验环境</h2>
<p>这里用两台阿里云ECS作为harbor的工作节点：</p>
<pre><code>node1:  10.47.217.91
node2:  10.28.61.30

</code></pre>
<p>两台主机运行的都是<a href="http://tonybai.com/tag/ubuntu">Ubuntu</a> 16.04.1 LTS (GNU/Linux 4.4.0-58-generic x86_64)，使用root用户。</p>
<p><a href="http://tonybai.com/tag/docker">docker版本</a>与docker-compose的版本如下：</p>
<pre><code># docker version
Client:
 Version:      1.12.5
 API version:  1.24
 Go version:   go1.6.4
 Git commit:   7392c3b
 Built:        Fri Dec 16 02:42:17 2016
 OS/Arch:      linux/amd64

Server:
 Version:      1.12.5
 API version:  1.24
 Go version:   go1.6.4
 Git commit:   7392c3b
 Built:        Fri Dec 16 02:42:17 2016
 OS/Arch:      linux/amd64

# docker-compose -v
docker-compose version 1.12.0, build b31ff33
</code></pre>
<p><a href="http://tonybai.com/tag/ceph">ceph</a>版本如下：</p>
<pre><code># ceph -v
ceph version 10.2.7
</code></pre>
<p>ceph的安装和配置可参考<a href="http://tonybai.com/2017/05/08/mount-cephfs-acrossing-nodes-in-kubernetes-cluster/">这里</a>。</p>
<h2>二、方案思路</h2>
<p>首先，从部署上说，我们需要的Private Registry是独立于<a href="http://tonybai.com/2016/12/30/install-kubernetes-on-ubuntu-with-kubeadm/">k8s cluster</a>存在的，即在k8s cluster外部，其存储和管理的镜像供k8s cluster 组件以及运行于k8s cluster上的应用使用。</p>
<p>其次，企业对registry有高可用需求，但我们也要有折中，我们的目标并不是理想的完全高可用，那样投入成本可能有些高。一般企业环境下更注重数据安全。因此首要保证harbor的数据安全，这样即便harbor实例宕掉，保证数据依然不会丢失即可。并且生产环境下registry的使用很难称得上高频，对镜像仓库的性能要求也没那么高。这种情况下，harbor的高可用至少有两种方案：</p>
<ul>
<li>多harbor实例共享后端存储</li>
<li>多harbor实例相互数据同步（通过配置两个harbor相互复制镜像数据）</li>
</ul>
<p>harbor原生支持双实例的镜像数据同步。不过这里我们采用第一种方案：即多harbor实例共享后端存储，因为我们有现成的cephfs供harbor使用。理想的方案示意图如下：</p>
<p><img src="http://tonybai.com/wp-content/uploads/harbor-ha-ideal.png" alt="img{512x368}" /></p>
<ul>
<li>每个安放harbor实例的node都mount cephfs；</li>
<li>每个node上的harbor实例（包含组件：ui、db、registry等）都volume mount node上的cephfs mount路径；</li>
<li>通过Load Balance将request流量负载到各个harbor实例上。</li>
</ul>
<p>但这样做可行么？如果这么做，Harbor实例里的mysql container就会“抱怨”：</p>
<pre><code>May 17 22:45:45 172.19.0.1 mysql[12110]: 2017-05-17 14:45:45 1 [ERROR] InnoDB: Unable to lock ./ibdata1, error: 11
May 17 22:45:45 172.19.0.1 mysql[12110]: 2017-05-17 14:45:45 1 [Note] InnoDB: Check that you do not already have another mysqld process using the same InnoDB data or log files.
</code></pre>
<p>MySQL多个实例无法共享一份mysql数据文件。</p>
<p>那么，我们会考虑将harbor连接的mysql放到外面来，使用external database；同时考虑到session共享，我们还需要增加一个存储session信息的redis cluster，这样一来，方案示意图变更如下：</p>
<p><img src="http://tonybai.com/wp-content/uploads/harbor-ha-we-use.png" alt="img{512x368}" /></p>
<p>图中的mysql、redis你即可以用cluster，也可以用单点，还是看你的需求和投入。如果你具备现成的mysql cluster和redis cluster，那么直接用就好了。但是如果你没有，并且你还不想投入这么多(尤其是搞mysql cluster)，那么用单点就好了。考虑到数据安全，可以将单点mysql的数据存储在cephfs上，如果你已经有了现成的cephfs。</p>
<h2>三、在一个node上安装Harbor</h2>
<h3>1、初装步骤</h3>
<p>以一个node上的Harbor安装为例，harbor提供了详细的<a href="https://github.com/vmware/harbor/blob/master/docs/installation_guide.md">安装步骤文档</a>，我们按照步骤逐步进行即可(这里我使用的是1.1.0版本，截至目前为止的最新稳定版本为1.1.1版本)：</p>
<pre><code>~/harbor-install# wget -c https://github.com/vmware/harbor/releases/download/v1.1.0/harbor-offline-installer-v1.1.0.tgz

~/harbor-install# tar zxvf harbor-offline-installer-v1.1.0.tgz

~/harbor-install/harbor# ls -F
common/  docker-compose.notary.yml  docker-compose.yml  harbor.cfg  harbor.v1.1.0.tar.gz  install.sh*  LICENSE  NOTICE  prepare*

~/harbor-install/harbor./install.sh

[Step 0]: checking installation environment ...

Note: docker version: 1.12.5
Note: docker-compose version: 1.12.0
[Step 1]: loading Harbor images ...
... ...
[Step 2]: preparing environment ...
Generated and saved secret to file: /data/secretkey
Generated configuration file: ./common/config/nginx/nginx.conf
Generated configuration file: ./common/config/adminserver/env
Generated configuration file: ./common/config/ui/env
Generated configuration file: ./common/config/registry/config.yml
Generated configuration file: ./common/config/db/env
Generated configuration file: ./common/config/jobservice/env
Generated configuration file: ./common/config/jobservice/app.conf
Generated configuration file: ./common/config/ui/app.conf
Generated certificate, key file: ./common/config/ui/private_key.pem, cert file: ./common/config/registry/root.crt
The configuration files are ready, please use docker-compose to start the service.

[Step 3]: checking existing instance of Harbor ...
[Step 4]: starting Harbor ...

Creating network "harbor_harbor" with the default driver
Creating harbor-log
Creating harbor-db
Creating registry
Creating harbor-adminserver
Creating harbor-ui
Creating nginx
Creating harbor-jobservice

ERROR: for proxy  Cannot start service proxy: driver failed programming external connectivity on endpoint nginx (fdeb3e538d5f8d714ea5c79a9f3f127f05f7ba5d519e09c4c30ef81f40b2fe77): Error starting userland proxy: listen tcp 0.0.0.0:80: bind: address already in use
</code></pre>
<p>harbor实例默认的监听端口是80，但一般node上的80口都会被占用，因此我们需要修改一个端口号。注意：此时harbor仅启动成功了一些container而已，尚无法正常工作。</p>
<h3>2、修改harbor proxy组件的listen端口</h3>
<p>harbor的proxy组件就是一个nginx，通过nginx这个反向代理，将不同的服务请求分发到内部其他组件中去。nginx默认监听node的80端口，我们用8060端口替代80端口需要进行两处配置修改：</p>
<pre><code>1、harbor.cfg

hostname = node_public_ip:8060

2、docker-compose.yml

proxy:
    image: vmware/nginx:1.11.5-patched
    container_name: nginx
    restart: always
    volumes:
      - ./common/config/nginx:/etc/nginx:z
    networks:
      - harbor
    ports:
      - 8060:80   &lt;--- 修改端口映射
      - 443:443
      - 4443:4443
</code></pre>
<p>由于我们修改了harbor.cfg文件，我们需要重新prepare一下，执行下面命令：</p>
<pre><code># docker-compose down -v
Stopping harbor-jobservice ... done
Stopping nginx ... done
Stopping harbor-ui ... done
Stopping harbor-db ... done
Stopping registry ... done
Stopping harbor-adminserver ... done
Stopping harbor-log ... done
Removing harbor-jobservice ... done
Removing nginx ... done
Removing harbor-ui ... done
Removing harbor-db ... done
Removing registry ... done
Removing harbor-adminserver ... done
Removing harbor-log ... done
Removing network harbor_harbor

# ./prepare
Clearing the configuration file: ./common/config/nginx/nginx.conf
Clearing the configuration file: ./common/config/ui/env
Clearing the configuration file: ./common/config/ui/app.conf
Clearing the configuration file: ./common/config/ui/private_key.pem
Clearing the configuration file: ./common/config/adminserver/env
Clearing the configuration file: ./common/config/jobservice/env
Clearing the configuration file: ./common/config/jobservice/app.conf
Clearing the configuration file: ./common/config/db/env
Clearing the configuration file: ./common/config/registry/config.yml
Clearing the configuration file: ./common/config/registry/root.crt
loaded secret from file: /mnt/cephfs/harbor/data/secretkey
Generated configuration file: ./common/config/nginx/nginx.conf
Generated configuration file: ./common/config/adminserver/env
Generated configuration file: ./common/config/ui/env
Generated configuration file: ./common/config/registry/config.yml
Generated configuration file: ./common/config/db/env
Generated configuration file: ./common/config/jobservice/env
Generated configuration file: ./common/config/jobservice/app.conf
Generated configuration file: ./common/config/ui/app.conf
Generated certificate, key file: ./common/config/ui/private_key.pem, cert file: ./common/config/registry/root.crt
The configuration files are ready, please use docker-compose to start the service.

# docker-compose up -d

Creating network "harbor_harbor" with the default driver
Creating harbor-log
Creating harbor-adminserver
Creating registry
Creating harbor-db
Creating harbor-ui
Creating harbor-jobservice
Creating nginx
</code></pre>
<p>我们可以通过docker-compose ps命令查看harbor组件的状态：</p>
<pre><code># docker-compose ps
       Name                     Command               State                                 Ports
--------------------------------------------------------------------------------------------------------------------------------
harbor-adminserver   /harbor/harbor_adminserver       Up
harbor-db            docker-entrypoint.sh mysqld      Up      3306/tcp
harbor-jobservice    /harbor/harbor_jobservice        Up
harbor-log           /bin/sh -c crond &amp;&amp; rm -f  ...   Up      127.0.0.1:1514-&gt;514/tcp
harbor-ui            /harbor/harbor_ui                Up
nginx                nginx -g daemon off;             Up      0.0.0.0:443-&gt;443/tcp, 0.0.0.0:4443-&gt;4443/tcp, 0.0.0.0:8060-&gt;80/tcp
registry             /entrypoint.sh serve /etc/ ...   Up      5000/tcp
</code></pre>
<p>如果安全组将8060端口打开，通过访问:http://node_public_ip:8060，你将看到如下harbor的web页面：</p>
<p><img src="http://tonybai.com/wp-content/uploads/harbor-homepage.png" alt="img{512x368}" /></p>
<p>我们可以通过harbor内置的默认用户名和密码admin/Harbor12345登录harbor ui。当然，我们更重要的是通过cmdline访问harbor，push和pull image。如果这时你直接尝试docker login harbor_url，你可能会得到如下错误日志：</p>
<pre><code># docker login -u admin -p Harbor12345 node_public_ip:8060
Error response from daemon: Get https://node_public_ip:8060/v1/users/: http: server gave HTTP response to HTTPS client
</code></pre>
<p>这是因为docker默认采用https访问registry，因此我们需要在docker engine的配置中，添加&#8211;insecure-registry option。关于ubuntu 16.04下docker配置的问题，请参考<a href="http://tonybai.com/2016/12/27/when-docker-meets-systemd/">这里</a>：</p>
<pre><code>DOCKER_OPTS="--dns 8.8.8.8 --dns 8.8.4.4 --registry-mirror=https://xxxxx.mirror.aliyuncs.com --insecure-registry=node_public_ip:8060"
</code></pre>
<p>重启docker engine后尝试再次登录harbor：</p>
<pre><code>docker login -u admin -p Harbor12345 node_public_ip:8060
Login Succeeded
</code></pre>
<p>一旦docker client login ok，我们就可以通过docker client对harbor中的相关repository进行操作了。</p>
<h2>四、挂载路径修改</h2>
<p>默认情况下，harbor将数据volume挂载到主机的/data路径下面。但由于我们采用ceph共享存储保证数据的高可用，需要修改harbor组件内容器的挂载路径，将其mount到共享存储挂载node上的路径：/mnt/cephfs/harbor/data/。对比两个路径，可以看出前缀由”/”变为了”/mnt/cephfs/harbor/”，我们需要修改docker-compose.yml和harbor.cfg两个文件。</p>
<p>由于docker-compose.yml文件较长，这里将原始文件改名为docker-compose.yml.orig，并将其与修改后的docker-compose.yml做对比：</p>
<pre><code># diff  docker-compose.yml.orig docker-compose.yml
8c8
&lt;       - /var/log/harbor/:/var/log/docker/:z
---
&gt;       - /mnt/cephfs/harbor/log/:/var/log/docker/:z
20c20
&lt;       - /data/registry:/storage:z
---
&gt;       - /mnt/cephfs/harbor/data/registry:/storage:z
40c40
&lt;       - /data/database:/var/lib/mysql:z
---
&gt;       - /mnt/cephfs/harbor/data/database:/var/lib/mysql:z
59,61c59,61
&lt;       - /data/config/:/etc/adminserver/config/:z
&lt;       - /data/secretkey:/etc/adminserver/key:z
&lt;       - /data/:/data/:z
---
&gt;       - /mnt/cephfs/harbor/data/config/:/etc/adminserver/config/:z
&gt;       - /mnt/cephfs/harbor/data/secretkey:/etc/adminserver/key:z
&gt;       - /mnt/cephfs/harbor/data/:/data/:z
80,81c80,81
&lt;       - /data/secretkey:/etc/ui/key:z
&lt;       - /data/ca_download/:/etc/ui/ca/:z
---
&gt;       - /mnt/cephfs/harbor/data/secretkey:/etc/ui/key:z
&gt;       - /mnt/cephfs/harbor/data/ca_download/:/etc/ui/ca/:z
100c100
&lt;       - /data/job_logs:/var/log/jobs:z
---
&gt;       - /mnt/cephfs/harbor/data/job_logs:/var/log/jobs:z
102c102
&lt;       - /data/secretkey:/etc/jobservice/key:z
---
&gt;       - /mnt/cephfs/harbor/data/secretkey:/etc/jobservice/key:z

</code></pre>
<p>harbor.cfg文件需要修改的地方不多：</p>
<pre><code>// harbor.cfg

#The path of cert and key files for nginx, they are applied only the protocol is set to https
ssl_cert = /mnt/cephfs/harbor/data/cert/server.crt
ssl_cert_key = /mnt/cephfs/harbor/data/cert/server.key

#The path of secretkey storage
secretkey_path = /mnt/cephfs/harbor/data
</code></pre>
<p>配置修改完毕后，执行如下命令：</p>
<pre><code># docker-compose down -v
# prepare
# docker-compose up -d
</code></pre>
<p>新的harbor实例就启动起来了。注意：这一步我们用cephfs替换了本地存储，主要的存储变动针对log、database和registry三个输出数据的组件。你也许会感受到cephfs给harbor ui页面加载带来的影响，实感要比之前的加载慢一些。</p>
<h2>五、使用外部数据库(external database)</h2>
<p>前面提到了挂载ceph后，多个node上harbor实例中的db组件将出现竞争问题，导致只有一个node上的harbor db组件可以工作。因此，我们要使用外部数据库(或db集群)来解决这个问题。但是harbor官方针对如何配置使用外部DB很是“讳莫如深”，我们只能自己探索。</p>
<p>假设我们已经有了一个external database，并且建立了harbor这个user，并做了相应的授权。由于harbor习惯了独享database，在测试环境下可以考虑</p>
<pre><code>GRANT ALL ON *.* TO 'harbor'@'%';
</code></pre>
<h3>1、迁移数据</h3>
<p>如果此时镜像库中已经有了数据，我们需要做一些迁移工作。</p>
<p>attach到harbor db组件的container中，将registry这张表dump到registry.dump文件中：</p>
<pre><code>#docker exec -i -t  6e1e4b576315  bash

在db container中：
# mysqldump -u root -p --databases registry &gt; registry.dump

回到node，将dump文件从container中copy出来：

#docker cp 6e1e4b576315:/root/registry.dump ./

再mysql login到external Database，将registry.dump文件导入：

# mysql -h external_db_ip -P 3306 -u harbor -p
# mysql&gt; source ./registry.dump;

</code></pre>
<h3>2、修改harbor配置，使得ui、jobservice组件连接external db</h3>
<p>根据当前<a href="https://github.com/vmware/harbor/wiki/Architecture-Overview-of-Harbor">harbor architecture</a>图所示：</p>
<p><img src="http://tonybai.com/wp-content/uploads/harbor-arch.png" alt="img{512x368}" /></p>
<p>与database“有染”的组件包括ui和jobservice，如何通过配置修改来让这两个组件放弃老db，访问新的external db呢？这要从挖掘配置开始。harbor的组件配置都在common/config下：</p>
<pre><code>~/harbor-install/harbor# tree -L 3 common
common
├── config
│   ├── adminserver
│   │   └── env
│   ├── db
│   │   └── env
│   ├── jobservice
│   │   ├── app.conf
│   │   └── env
│   ├── nginx
│   │   └── nginx.conf
│   ├── registry
│   │   ├── config.yml
│   │   └── root.crt
│   └── ui
│       ├── app.conf
│       ├── env
│       └── private_key.pem
└── templates
 ... ...
</code></pre>
<p>在修改config之前，我们先docker-compose down掉harbor。接下来，我们看到ui和jobservice下都有env文件，这里想必就是可以注入新db的相关访问信息的地方，我们来试试！</p>
<pre><code>// common/config/ui/env
LOG_LEVEL=debug
CONFIG_PATH=/etc/ui/app.conf
UI_SECRET=$ui_secret
JOBSERVICE_SECRET=$jobservice_secret
GODEBUG=netdns=cgo
MYSQL_HOST=new_db_ip
MYSQL_PORT=3306
MYSQL_USR=harbor
MYSQL_PWD=harbor_password

// common/config/jobservice/env
LOG_LEVEL=debug
CONFIG_PATH=/etc/jobservice/app.conf
UI_SECRET=$ui_secret
JOBSERVICE_SECRET=$jobservice_secret
GODEBUG=netdns=cgo
MYSQL_HOST=new_db_ip
MYSQL_PORT=3306
MYSQL_USR=harbor
MYSQL_PWD=harbor_password
</code></pre>
<p>同时，由于不再需要harbor_db组件，<em>因此切记：要将其从docker-compose.yml中剔除！</em>。docker-compose up -d重新创建harbor各组件容器并启动！Harbor的日志可以在挂载的ceph路径： /mnt/cephfs/harbor/log下查找到：</p>
<pre><code>/mnt/cephfs/harbor/log# tree 2017-06-09
2017-06-09
├── adminserver.log
├── anacron.log
├── CROND.log
├── jobservice.log
├── mysql.log
├── proxy.log
├── registry.log
├── run-parts.log
└── ui.log
</code></pre>
<p>我们以ui.log为例，我们发现harbor启动后，ui.log输出如下错误日志(jobservice.log也是相同)：</p>
<pre><code>Jun  9 11:00:17 172.19.0.1 ui[16039]: 2017-06-09T03:00:17Z [INFO] initializing database: type-MySQL host-mysql port-3306 user-root database-registry
Jun  9 11:00:18 172.19.0.1 ui[16039]: 2017-06-09T03:00:18Z [ERROR] [utils.go:94]: failed to connect to tcp://mysql:3306, retry after 2 seconds :dial tcp: lookup mysql: no such host
</code></pre>
<p>我们明明注入了新的db env，为何ui还是要访问“tcp://mysql:3306”呢？我们docker inspect一下ui的container，看看env是否包含我们添加的那些：</p>
<pre><code># docker inspect e91ab20e1dcb
... ...
            "Env": [
                "DATABASE_TYPE=mysql",
                "MYSQL_HOST=database_ip",
                "MYSQL_PORT=3306",
                "MYSQL_PWD=harbor_password",
                "MYSQL_USR=harbor",
                "MYSQL_DATABASE=registry",
            ],
.... ...
</code></pre>
<p>env已经注入，那么为何ui、jobservice无法连接到external database呢？要想搞清楚这点，我们只能去<em>“啃代码”</em>了。还好harbor代码并非很难啃。我们发现基于beego实现的ui、jobservice两个组件并未直接通过os.Getenv去获取这些env变量，而是调用了adminserver组件的服务。adminserver在初始化时，在RESET环境变量为true的情况下，读取了common/config/adminserver/env下的所有环境变量。</p>
<p>搞清楚原理后，我们知道了要修改的是common/config/adminserver/env，而不是common/config/ui/env和common/config/jobservice/env。我们将后两个文件还原。修改common/config/adminserver/env文件：</p>
<pre><code>//common/config/adminserver/env
... ...
MYSQL_HOST=new_db_ip
MYSQL_PORT=3306
MYSQL_USR=harbor
MYSQL_PWD=harbor_password
... ...
RESET=true    &lt;--- 改为true，非常关键

</code></pre>
<p>重新up harbor服务后，我们发现ui, jobservice与新database的连接成功了！打开harbor web页面，登录进去，我们看到了之前已经添加的用户、项目和镜像文件。</p>
<h3>3、一劳永逸</h3>
<p>如果你重新执行prepare，那么上面对config目录下的配置修改将被重新覆盖。如果要一劳永逸，那么需要修改的是common/templates下面的同位置同名配置文件。</p>
<h2>六、安装其他节点上的harbor实例</h2>
<p>前面，我们只搭建了一个节点，为的是验证方案的可行性。要实现高可用，我们还需要在其他节点上安装harbor实例。由于多个节点上harbor实例共同挂载ceph的同一目录，因此考虑到log的分离，在部署其他节点上的harbor时，最好对docker-compose.yml下log组件的volumes映射路径进行调整，以在多个节点间做隔离，便于日志查看，比如：</p>
<pre><code>volumes:
      - /mnt/cephfs/harbor/log1/:/var/log/docker/:z
</code></pre>
<p>除此之外，各个节点上的harbor配置与上述配置完全一致。</p>
<h2>七、共享session设置</h2>
<p>到harbor的请求被负载均衡分发到多个node上的harbor实例上，这样就有了session共享的需求。Harbor对此已经给予了支持。在ui组件的代码中，我们发现ui在初始化时使用Getenv获取”_REDIS_URL”这个环境变量的值，因此我们只需要将_REDIS_URL这个环境变量配置到各个节点harbor ui组件的env文件中即可：</p>
<pre><code>// common/config/adminserver/env

LOG_LEVEL=debug
CONFIG_PATH=/etc/ui/app.conf
UI_SECRET=LuAwkKUtYjF4l0mQ
JOBSERVICE_SECRET=SmsO1kVo4SrmgOIp
GODEBUG=netdns=cgo
_REDIS_URL=redis_ip:6379,100,redis_password,0
</code></pre>
<p>重新up harbor后，session共享生效。</p>
<p>不过光有一个外部redis存储共享session还不够，请求在多个harbor实例中的registry组件中进行鉴权需要harbor各个实例share相同的<a href="https://github.com/vmware/harbor/blob/master/docs/customize_token_service.md">key和certificate</a>。好在，我们的多harbor实例通过ceph共享存储，key和cert本就是共享的，都存放在目录：/mnt/cephfs/harbor/data/cert/的下边，因此也就不需要在各个harbor实例间同步key和cert了。</p>
<h2>八、更换为域名访问</h2>
<p>我们有通过域名访问docker registry的需求，那么直接通过域名访问harbor ui和registry是否可行呢？这要看harbor  nginx的配置:</p>
<pre><code># docker ps |grep nginx
fa92765e8871        vmware/nginx:1.11.5-patched   "nginx -g 'daemon off"   3 hours ago
Up 3 hours          0.0.0.0:443-&gt;443/tcp, 0.0.0.0:4443-&gt;4443/tcp, 0.0.0.0:8060-&gt;80/tcp               nginx

# docker exec fa92765e8871 cat /etc/nginx/nginx.conf

... ...
http {
   server {
    listen 80;
   ... ...

}

</code></pre>
<p>nginx在http server block并未对域名或ip进行匹配，因此直接将域名A地址设置为反向代理的地址或直接解析为Harbor暴露的公网ip地址都是可以正常访问harbor服务的，当然也包括image push和pull服务。</p>
<blockquote>
<p>注意：如果使用域名访问harbor服务，那么就将harbor.cfg中的hostname赋值为你的”域名+端口”，并重新prepare。否则你可能会发现通过harbor域名上传的image无法pull，因为其pull的地址为由ip组成的地址，以docker push hub.tonybai.com:8989/myrepo/foo:latest为例，push成功后，docker pull hub.tonybai.com:8989/myrepo/foo:latest可能提示你找不到该image，因为harbor中该imag<br />
  e的地址可能是my_ip_address:8989/myrepo/foo:latest。</p>
</blockquote>
<h2>九、统一registry的证书和token service的私钥</h2>
<p>这是在本篇文章发表之后发现的问题，针对该问题，我专门写了一篇文章：《<a href="http://tonybai.com/2017/06/15/fix-auth-fail-when-login-harbor-registry/">解决登录Harbor Registry时鉴权失败的问题</a>》,请移步这篇文章，完成HA Harbor的搭建。</p>
<h2>十、参考资料</h2>
<ul>
<li><a href="https://github.com/vmware/harbor/blob/master/docs/installation_guide.md">Installation &amp; Configuration Guide</a></li>
<li><a href="http://dockone.io/article/1640">用Harbor实现容器镜像仓库的管理和运维</a></li>
</ul>
<hr />
<p>微博：<a href="http://weibo.com/bigwhite20xx">@tonybai_cn</a><br />
微信公众号：iamtonybai<br />
github.com: https://github.com/bigwhite</p>
<p style='text-align:left'>&copy; 2017, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2017/06/09/setup-a-high-availability-private-registry-based-on-harbor-and-cephfs/feed/</wfw:commentRss>
		<slash:comments>20</slash:comments>
		</item>
		<item>
		<title>Kubernetes集群node主机名修改导致的异常</title>
		<link>https://tonybai.com/2017/05/09/exception-caused-by-kubernetes-node-hostname-change/</link>
		<comments>https://tonybai.com/2017/05/09/exception-caused-by-kubernetes-node-hostname-change/#comments</comments>
		<pubDate>Tue, 09 May 2017 02:37:23 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Ceph]]></category>
		<category><![CDATA[ceph-deploy]]></category>
		<category><![CDATA[CephFS]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[hostname]]></category>
		<category><![CDATA[k8s]]></category>
		<category><![CDATA[kubelet]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[Master]]></category>
		<category><![CDATA[minion]]></category>
		<category><![CDATA[集群]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=2309</guid>
		<description><![CDATA[除了在生产环境使用的Kubernetes 1.3.7集群之外，我这里还有一套1.5.1的Kubernetes测试环境，这个测试环境一来用于验证各种技术方案，二来也是为了跟踪Kubernetes的最新进展。本篇要记录的一个异常就是发生在该测试Kubernetes集群中的。 一、缘起 前两天我在Kubernetes测试环境搭建一套Ceph，为了便于ceph-deploy的安装，我通过hostnamectl命令将阿里云默认提供的复杂又冗长的主机名改为短小且更有意义的主机名： iZ25beglnhtZ -&#62; yypdmaster iz2ze39jeyizepdxhwqci6z -&#62; yypdnode 以yypdmaster为例，修改过程如下： # hostnamectl --static set-hostname yypdmaster # hostnamectl status Static hostname: yypdmaster Transient hostname: iZ25beglnhtZ Icon name: computer-vm Chassis: vm Machine ID: 91aa4b8f2556de49e743dc2f53e8a5c4 Boot ID: 5d0e642ebafa460086388da4177e488e Virtualization: kvm Operating System: Ubuntu 16.04.1 LTS Kernel: Linux 4.4.0-58-generic Architecture: x86-64 # cat /etc/hostname yypdmaster hostnamectl并未修改/etc/hosts，我手动在/etc/hosts中将yypdmaster对应的ip配置上： xx.xx.xx.xx yypdmaster [...]]]></description>
			<content:encoded><![CDATA[<p>除了在生产环境使用的<a href="http://tonybai.com/2016/10/18/learn-how-to-install-kubernetes-on-ubuntu/">Kubernetes 1.3.7集群</a>之外，我这里还有<a href="http://tonybai.com/2016/12/30/install-kubernetes-on-ubuntu-with-kubeadm/">一套1.5.1的Kubernetes测试环境</a>，这个测试环境一来用于验证各种技术方案，二来也是为了跟踪Kubernetes的最新进展。本篇要记录的一个异常就是发生在该测试Kubernetes集群中的。</p>
<h3>一、缘起</h3>
<p>前两天我在Kubernetes测试环境搭建一套<a href="http://tonybai.com/2016/11/07/integrate-kubernetes-with-ceph-rbd/">Ceph</a>，为了便于ceph-deploy的安装，我通过hostnamectl命令将阿里云默认提供的复杂又冗长的主机名改为短小且更有意义的主机名：</p>
<pre><code>iZ25beglnhtZ -&gt; yypdmaster
iz2ze39jeyizepdxhwqci6z -&gt; yypdnode

以yypdmaster为例，修改过程如下：

# hostnamectl --static set-hostname yypdmaster
# hostnamectl status
Static hostname: yypdmaster
Transient hostname: iZ25beglnhtZ
         Icon name: computer-vm
           Chassis: vm
        Machine ID: 91aa4b8f2556de49e743dc2f53e8a5c4
           Boot ID: 5d0e642ebafa460086388da4177e488e
    Virtualization: kvm
  Operating System: Ubuntu 16.04.1 LTS
            Kernel: Linux 4.4.0-58-generic
      Architecture: x86-64

# cat /etc/hostname
yypdmaster

hostnamectl并未修改/etc/hosts，我手动在/etc/hosts中将yypdmaster对应的ip配置上：

xx.xx.xx.xx yypdmaster
</code></pre>
<p>重新登录后，我们看到主机名状态：Transient hostname不见了，只剩下了静态主机名：</p>
<pre><code># hostnamectl status
   Static hostname: yypdmaster
         Icon name: computer-vm
           Chassis: vm
        Machine ID: 91aa4b8f2556de49e743dc2f53e8a5c4
           Boot ID: 5d0e642ebafa460086388da4177e488e
    Virtualization: kvm
  Operating System: Ubuntu 16.04.1 LTS
            Kernel: Linux 4.4.0-58-generic
      Architecture: x86-64

</code></pre>
<p>另外一台主机也是如此修改。主机名修改后，整个k8s集群工作一切正常，因此我最初以为hostname的修改对k8s cluster的运行没有影响。</p>
<h3>二、集群”Crash”</h3>
<p>昨天在做<a href="http://tonybai.com/2017/05/08/mount-cephfs-acrossing-nodes-in-kubernetes-cluster/">跨节点挂载Cephfs</a>测试时，发现在yypdmaster上kubectl exec另外一个node上的pod不好用，提示：连接10250端口超时！而且从错误日志来看，yypdmaster上的k8s组件居然通过yypdnode的外网ip去访问yypdnode上的10250端口，也就是yypdnode上kubelet监听的端口。由于aliyun的安全组规则限制，这个端口是不允许外网访问的，因此timeout错误是合理的。但为什么之前集群都是好好的？突然间出现这个问题呢？为什么不用内网的ip地址访问呢？</p>
<p>我尝试重启了yypdnode上的kubelet服务。不过似乎没什么效果！正当我疑惑时，我发现集群似乎”Crash”了，下面是当时查看集群的pod情况的输出：</p>
<pre><code># kubectl get pod --all-namespaces -o wide

NAMESPACE                    NAME                                    READY     STATUS             RESTARTS   AGE       IP             NODE
default                      ceph-pod2                               1/1       Unknown            0          26m       172.30.192.4   iz2ze39jeyizepdxhwqci6z
default                      ceph-pod2-with-secret                   1/1       Unknown            0          38m       172.30.192.2   iz2ze39jeyizepdxhwqci6z
default                      ceph-pod2-with-secret-on-master         1/1       Unknown            0          34m       172.30.0.51    iz25beglnhtz
default                      nginx-kit-3630450072-2c0jk              0/2       Pending            0          12m       &lt;none&gt;
default                      nginx-kit-3630450072-3n50m              2/2       Unknown            20         35d       172.30.0.44    iz25beglnhtz
default                      nginx-kit-3630450072-90v4q              0/2       Pending            0          12m       &lt;none&gt;
default                      nginx-kit-3630450072-j8qrk              2/2       Unknown            20         72d       172.30.0.47    iz25beglnhtz
kube-system                  dummy-2088944543-9382n                  1/1       Running            0          12m       xx.xx.xx.xx   yypdmaster
kube-system                  dummy-2088944543-93f4c                  1/1       Unknown            16         130d      xx.xx.xx.xx   iz25beglnhtz
kube-system                  elasticsearch-logging-v1-dhl35          1/1       Running            0          12m       172.30.192.6   yypdnode
kube-system                  elasticsearch-logging-v1-s3sbj          1/1       Unknown            9          35d       172.30.0.45    iz25beglnhtz
kube-system                  elasticsearch-logging-v1-t8wg0          1/1       Unknown            29         68d       172.30.0.43    iz25beglnhtz
kube-system                  elasticsearch-logging-v1-zdp19          1/1       Running            0          12m       172.30.0.3     yypdmaster
kube-system                  etcd-iz25beglnhtz                       1/1       Unknown            17         130d      xx.xx.xx.xx   iz25beglnhtz
kube-system                  etcd-yypdmaster                         1/1       Running            17         17m       xx.xx.xx.xx   yypdmaster
kube-system                  fluentd-es-v1.22-ggvv4                  1/1       NodeLost           24         68d       172.30.0.46    iz25beglnhtz
kube-system                  fluentd-es-v1.22-rj871                  1/1       Running            0          17m       172.30.0.1     yypdmaster
kube-system                  fluentd-es-v1.22-xn77x                  1/1       NodeLost           0          6d        172.30.192.0   iz2ze39jeyizepdxhwqci6z
kube-system                  fluentd-es-v1.22-z82rz                  1/1       Running            0          18m       172.30.192.5   yypdnode
kube-system                  kibana-logging-3746979809-dplzv         1/1       Running            0          12m       172.30.0.4     yypdmaster
kube-system                  kibana-logging-3746979809-lq9m3         1/1       Unknown            9          35d       172.30.0.49    iz25beglnhtz
kube-system                  kube-apiserver-iz25beglnhtz             1/1       Unknown            19         104d      xx.xx.xx.xx   iz25beglnhtz
kube-system                  kube-apiserver-yypdmaster               1/1       Running            19         17m       xx.xx.xx.xx   yypdmaster
kube-system                  kube-controller-manager-iz25beglnhtz    1/1       Unknown            21         130d      xx.xx.xx.xx   iz25beglnhtz
kube-system                  kube-controller-manager-yypdmaster      1/1       Running            21         17m       xx.xx.xx.xx   yypdmaster
kube-system                  kube-discovery-1769846148-wh1z4         1/1       Unknown            12         73d       xx.xx.xx.xx   iz25beglnhtz
kube-system                  kube-discovery-1769846148-z2v87         0/1       Pending            0          12m       &lt;none&gt;
kube-system                  kube-dns-2924299975-206tg               4/4       Unknown            129        130d      172.30.0.48    iz25beglnhtz
kube-system                  kube-dns-2924299975-g1kks               4/4       Running            0          12m       172.30.0.5     yypdmaster
kube-system                  kube-proxy-3z29k                        1/1       Running            0          18m       yy.yy.yy.yy    yypdnode
kube-system                  kube-proxy-kfzxv                        1/1       Running            0          17m       xx.xx.xx.xx   yypdmaster
kube-system                  kube-proxy-n2xmf                        1/1       NodeLost           16         130d      xx.xx.xx.xx   iz25beglnhtz

</code></pre>
<p>观察这个输出，我们看到几点异常：</p>
<ul>
<li>不常见的Pod状态：Unknown、NodeLost</li>
<li>Node一列居然出现了四个Node: yypdmaster、yypdnode、 iz25beglnhtz和  iz2ze39jeyizepdxhwqci6z</li>
</ul>
<p>等了一会儿，这种状态依然不见好转。我于是重启了master上的kubelet、重启了两个节点上的docker engine，不过启动后问题依旧！</p>
<p>查看Running状态的Pod情况：</p>
<pre><code># kubectl get pod --all-namespaces -o wide|grep Running
kube-system                  dummy-2088944543-9382n                  1/1       Running            0          18m       xx.xx.xx.xx   yypdmaster
kube-system                  elasticsearch-logging-v1-dhl35          1/1       Running            0          18m       172.30.192.6   yypdnode
kube-system                  elasticsearch-logging-v1-zdp19          1/1       Running            0          18m       172.30.0.3     yypdmaster
kube-system                  etcd-yypdmaster                         1/1       Running            17         23m       xx.xx.xx.xx   yypdmaster
kube-system                  fluentd-es-v1.22-rj871                  1/1       Running            0          23m       172.30.0.1     yypdmaster
kube-system                  fluentd-es-v1.22-z82rz                  1/1       Running            0          24m       172.30.192.5   yypdnode
kube-system                  kibana-logging-3746979809-dplzv         1/1       Running            0          18m       172.30.0.4     yypdmaster
kube-system                  kube-apiserver-yypdmaster               1/1       Running            19         23m       xx.xx.xx.xx   yypdmaster
kube-system                  kube-controller-manager-yypdmaster      1/1       Running            21         23m       xx.xx.xx.xx   yypdmaster
kube-system                  kube-dns-2924299975-g1kks               4/4       Running            0          18m       172.30.0.5     yypdmaster
kube-system                  kube-proxy-3z29k                        1/1       Running            0          24m       yy.yy.yy.yy    yypdnode
kube-system                  kube-proxy-kfzxv                        1/1       Running            0          23m       xx.xx.xx.xx   yypdmaster
kube-system                  kube-scheduler-yypdmaster               1/1       Running            22         23m       xx.xx.xx.xx   yypdmaster
kube-system                  kubernetes-dashboard-3109525988-cj74d   1/1       Running            0          18m       172.30.0.6     yypdmaster
mioss-namespace-s0fcvegcmw   console-sm7cg2-101699315-f3g55          1/1       Running            0          18m       172.30.0.7     yypdmaster
</code></pre>
<p>似乎Kubernetes集群并未真正”Crash”，但从Node列来看，正常的pod归属的node不是yypdmaster就是yypdnode， iz25beglnhtz和  iz2ze39jeyize</p>
<p style='text-align:left'>&copy; 2017, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2017/05/09/exception-caused-by-kubernetes-node-hostname-change/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Kubernetes集群跨节点挂载CephFS</title>
		<link>https://tonybai.com/2017/05/08/mount-cephfs-acrossing-nodes-in-kubernetes-cluster/</link>
		<comments>https://tonybai.com/2017/05/08/mount-cephfs-acrossing-nodes-in-kubernetes-cluster/#comments</comments>
		<pubDate>Mon, 08 May 2017 09:51:46 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[CehpRBD]]></category>
		<category><![CDATA[Ceph]]></category>
		<category><![CDATA[CephFS]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[k8s]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[persistent-volume]]></category>
		<category><![CDATA[persistent-volume-claim]]></category>
		<category><![CDATA[pod]]></category>
		<category><![CDATA[PV]]></category>
		<category><![CDATA[PVC]]></category>
		<category><![CDATA[rbd]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=2303</guid>
		<description><![CDATA[在Kubernetes集群中运行有状态服务或应用总是不那么容易的。比如，之前我在项目中使用了CephRBD，虽然遇到过几次问题，但总体算是运行良好。但最近发现CephRBD无法满足跨节点挂载的需求，我只好另辟蹊径。由于CephFS和CephRBD师出同门，它自然成为了这次我首要考察的目标。这里将跨节点挂载CephFS的考察过程记录一下，一是备忘，二则也可以为其他有相似需求的朋友提供些资料。 一、CephRBD的问题 这里先提一嘴CephRBD的问题。最近项目中有这样的需求：让集群中的Pod共享外部分布式存储，即多个Pod共同挂载一份存储，实现存储共享，这样可大大简化系统设计和复杂性。之前CephRBD都是挂载到一个Pod中运行的，CephRBD是否支持多Pod同时挂载呢？官方文档中给出了否定的答案: 基于CephRBD的Persistent Volume仅支持两种accessmode： ReadWriteOnce和ReadOnlyMany，不支持ReadWriteMany。这样对于有读写需求的Pod来说，一个CephRBD pv仅能被一个node挂载一次。 我们来验证一下这个“不幸的”事实。 我们首先创建一个测试用的image：foo1。这里我利用了项目里写的CephRBD API服务，也可通过ceph命令手工创建： # curl -v -H "Content-type: application/json" -X POST -d '{"kind": "Images","apiVersion": "v1", "metadata": {"name": "foo1", "capacity": 512} ' http://192.168.3.22:8080/api/v1/pools/rbd/images ... ... { "errcode": 0, "errmsg": "ok" } # curl http://192.168.3.22:8080/api/v1/pools/rbd/images { "Kind": "ImagesList", "APIVersion": "v1", "Items": [ { "name": "foo1" } ] } 利用下面文件创建pv和pvc： //ceph-pv.yaml [...]]]></description>
			<content:encoded><![CDATA[<p>在<a href="http://tonybai.com/tag/kubernetes">Kubernetes</a>集群中运行有状态服务或应用总是不那么容易的。比如，之前我在项目中使用了<a href="http://tonybai.com/2016/11/07/integrate-kubernetes-with-ceph-rbd/">CephRBD</a>，虽然<a href="http://tonybai.com/2017/02/17/temp-fix-for-pod-unable-mount-cephrbd-volume/">遇到过几次问题</a>，但总体算是运行良好。但最近发现CephRBD无法满足跨节点挂载的需求，我只好另辟蹊径。由于CephFS和CephRBD师出同门，它自然成为了这次我首要考察的目标。这里将跨节点挂载CephFS的考察过程记录一下，一是备忘，二则也可以为其他有相似需求的朋友提供些资料。</p>
<h2>一、CephRBD的问题</h2>
<p>这里先提一嘴CephRBD的问题。最近项目中有这样的需求：让集群中的Pod共享外部分布式存储，即多个Pod共同挂载一份存储，实现存储共享，这样可大大简化系统设计和复杂性。之前CephRBD都是挂载到一个Pod中运行的，CephRBD是否支持多Pod同时挂载呢？<a href="https://kubernetes.io/docs/concepts/storage/persistent-volumes">官方文档</a>中给出了否定的答案: 基于CephRBD的Persistent Volume仅支持两种accessmode：<br />
ReadWriteOnce和ReadOnlyMany，不支持ReadWriteMany。这样对于有读写需求的Pod来说，一个CephRBD pv仅能被一个node挂载一次。</p>
<p>我们来验证一下这个“不幸的”事实。</p>
<p>我们首先创建一个测试用的image：foo1。这里我利用了项目里写的<a href="http://tonybai.com/2016/11/21/kuberize-ceph-rbd-api-service/">CephRBD API</a>服务，也可通过ceph命令手工创建：</p>
<pre><code># curl -v  -H "Content-type: application/json" -X POST -d '{"kind": "Images","apiVersion": "v1", "metadata": {"name": "foo1", "capacity": 512} ' http://192.168.3.22:8080/api/v1/pools/rbd/images
... ...
{
  "errcode": 0,
  "errmsg": "ok"
}

# curl http://192.168.3.22:8080/api/v1/pools/rbd/images
{
  "Kind": "ImagesList",
  "APIVersion": "v1",
  "Items": [
    {
      "name": "foo1"
    }
  ]
}

</code></pre>
<p>利用下面文件创建pv和pvc：</p>
<pre><code>//ceph-pv.yaml

apiVersion: v1
kind: PersistentVolume
metadata:
  name: foo-pv
spec:
  capacity:
    storage: 512Mi
  accessModes:
    - ReadWriteMany
  rbd:
    monitors:
      - ceph_monitor_ip:port
    pool: rbd
    image: foo1
    user: admin
    secretRef:
      name: ceph-secret
    fsType: ext4
    readOnly: false
  persistentVolumeReclaimPolicy: Recycle

//ceph-pvc.yaml

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: foo-claim
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 512Mi
</code></pre>
<p>创建后：</p>
<pre><code># kubectl get pv
[NAME                CAPACITY   ACCESSMODES   RECLAIMPOLICY   STATUS    CLAIM                        REASON    AGE
foo-pv              512Mi      RWO           Recycle         Bound     default/foo-claim                      20h

# kubectl get pvc
NAME                 STATUS    VOLUME              CAPACITY   ACCESSMODES   AGE
foo-claim            Bound     foo-pv              512Mi      RWO           20h
</code></pre>
<p>创建挂载上述image的Pod：</p>
<pre><code>// ceph-pod2.yaml

apiVersion: v1
kind: Pod
metadata:
  name: ceph-pod2
spec:
  containers:
  - name: ceph-ubuntu2
    image: ubuntu:14.04
    command: ["tail", "-f", "/var/log/bootstrap.log"]
    volumeMounts:
    - name: ceph-vol2
      mountPath: /mnt/cephrbd/data
      readOnly: false
  volumes:
  - name: ceph-vol2
    persistentVolumeClaim:
      claimName: foo-claim

</code></pre>
<p>创建成功后，我们可以查看挂载目录的数据：</p>
<pre><code># kubectl exec ceph-pod2 ls /mnt/cephrbd/data
1.txt
lost+found
</code></pre>
<p>我们在同一个kubernetes node上再启动一个pod（可以把上面的ceph-pod2.yaml的pod name改为ceph-pod3），挂载同样的pv：</p>
<pre><code>NAMESPACE                    NAME                                    READY     STATUS    RESTARTS   AGE       IP             NODE
default                      ceph-pod2                               1/1       Running   0          3m        172.16.57.9    xx.xx.xx.xx
default                      ceph-pod3                               1/1       Running   0          0s        172.16.57.10    xx.xx.xx.xx
</code></pre>
<pre><code># kubectl exec ceph-pod3 ls /mnt/cephrbd/data
1.txt
lost+found
</code></pre>
<p>我们通过ceph-pod2写一个文件，在ceph-pod3中将其读出：</p>
<pre><code># kubectl exec ceph-pod2 -- bash -c "for i in {1..10}; do sleep 1; echo 'pod2: Hello, World'&gt;&gt; /mnt/cephrbd/data/foo.txt ; done "
root@node1:~/k8stest/k8s-cephrbd/footest# kubectl exec ceph-pod3 cat /mnt/cephrbd/data/foo.txt
pod2: Hello, World
pod2: Hello, World
pod2: Hello, World
pod2: Hello, World
pod2: Hello, World
pod2: Hello, World
pod2: Hello, World
pod2: Hello, World
pod2: Hello, World
pod2: Hello, World

</code></pre>
<p>到目前为止，在一个node上多个Pod是可以以ReadWrite模式挂载同一个CephRBD的。</p>
<p>我们在另外一个节点启动一个试图挂载该pv的Pod，该Pod启动后一直处于pending状态，通过kubectl describe查看其详细信息，可以看到：</p>
<pre><code>Events:
  FirstSeen    LastSeen    Count    From            SubobjectPath    Type        Reason        Message
  ---------    --------    -----    ----            -------------    --------    ------        -------
.. ...
  2m        37s        2    {kubelet yy.yy.yy.yy}            Warning        FailedMount    Unable to mount volumes for pod "ceph-pod2-master_default(a45f62aa-2bc3-11e7-9baa-00163e1625a9)": timeout expired waiting for volumes to attach/mount for pod "ceph-pod2-master"/"default". list of unattached/unmounted volumes=[ceph-vol2]
  2m        37s        2    {kubelet yy.yy.yy.yy}            Warning        FailedSync    Error syncing pod, skipping: timeout expired waiting for volumes to attach/mount for pod "ceph-pod2-master"/"default". list of unattached/unmounted volumes=[ceph-vol2]
</code></pre>
<p>查看kubelet.log中的错误日志：</p>
<pre><code>I0428 11:39:15.737729    1241 reconciler.go:294] MountVolume operation started for volume "kubernetes.io/rbd/a45f62aa-2bc3-11e7-9baa-00163e1625a9-foo-pv" (spec.Name: "foo-pv") to pod "a45f62aa-2bc3-11e7-9baa-00163e1625a9" (UID: "a45f62aa-2bc3-11e7-9baa-00163e1625a9").
I0428 11:39:15.939183    1241 operation_executor.go:768] MountVolume.SetUp succeeded for volume "kubernetes.io/secret/923700ff-12c2-11e7-9baa-00163e1625a9-default-token-40z0x" (spec.Name: "default-token-40z0x") pod "923700ff-12c2-11e7-9baa-00163e1625a9" (UID: "923700ff-12c2-11e7-9baa-00163e1625a9").
E0428 11:39:17.039656    1241 disk_manager.go:56] failed to attach disk
E0428 11:39:17.039722    1241 rbd.go:228] rbd: failed to setup mount /var/lib/kubelet/pods/a45f62aa-2bc3-11e7-9baa-00163e1625a9/volumes/kubernetes.io~rbd/foo-pv rbd: image foo1 is locked by other nodes
E0428 11:39:17.039857    1241 nestedpendingoperations.go:254] Operation for "\"kubernetes.io/rbd/a45f62aa-2bc3-11e7-9baa-00163e1625a9-foo-pv\" (\"a45f62aa-2bc3-11e7-9baa-00163e1625a9\")" failed. No retries permitted until 2017-04-28 11:41:17.039803969 +0800 CST (durationBeforeRetry 2m0s). Error: MountVolume.SetUp failed for volume "kubernetes.io/rbd/a45f62aa-2bc3-11e7-9baa-00163e1625a9-foo-pv" (spec.Name: "foo-pv") pod "a45f62aa-2bc3-11e7-9baa-00163e1625a9" (UID: "a45f62aa-2bc3-11e7-9baa-00163e1625a9") with: rbd: image foo1 is locked by other nodes

</code></pre>
<p>可以看到“rbd: image foo1 is locked by other nodes”的日志。我们用试验证明了目前CephRBD仅能被k8s中的一个node挂载的事实。</p>
<h2>二、Ceph集群安装mds以支持CephFS</h2>
<p>这次我在两个Ubuntu 16.04的vm上新部署了一套Ceph，过程与之前<a href="http://tonybai.com/2016/11/07/integrate-kubernetes-with-ceph-rbd/">第一次部署Ceph</a>时大同小异，这里就不赘述了。要让Ceph支持CephFS，我们需要安装mds组件，有了前面的基础，通过ceph-deploy工具安装mds十分简单：</p>
<pre><code># ceph-deploy mds create yypdmaster yypdnode
[ceph_deploy.conf][DEBUG ] found configuration file at: /root/.cephdeploy.conf
[ceph_deploy.cli][INFO  ] Invoked (1.5.37): /usr/bin/ceph-deploy mds create yypdmaster yypdnode
[ceph_deploy.cli][INFO  ] ceph-deploy options:
[ceph_deploy.cli][INFO  ]  username                      : None
[ceph_deploy.cli][INFO  ]  verbose                       : False
[ceph_deploy.cli][INFO  ]  overwrite_conf                : False
[ceph_deploy.cli][INFO  ]  subcommand                    : create
[ceph_deploy.cli][INFO  ]  quiet                         : False
[ceph_deploy.cli][INFO  ]  cd_conf                       : &lt;ceph_deploy.conf.cephdeploy.Conf instance at 0x7f60fb5e71b8&gt;
[ceph_deploy.cli][INFO  ]  cluster                       : ceph
[ceph_deploy.cli][INFO  ]  func                          : &lt;function mds at 0x7f60fba4e140&gt;
[ceph_deploy.cli][INFO  ]  ceph_conf                     : None
[ceph_deploy.cli][INFO  ]  mds                           : [('yypdmaster', 'yypdmaster'), ('yypdnode', 'yypdnode')]
[ceph_deploy.cli][INFO  ]  default_release               : False
[ceph_deploy.mds][DEBUG ] Deploying mds, cluster ceph hosts yypdmaster:yypdmaster yypdnode:yypdnode
[yypdmaster][DEBUG ] connected to host: yypdmaster
[yypdmaster][DEBUG ] detect platform information from remote host
[yypdmaster][DEBUG ] detect machine type
[ceph_deploy.mds][INFO  ] Distro info: Ubuntu 16.04 xenial
[ceph_deploy.mds][DEBUG ] remote host will use systemd
[ceph_deploy.mds][DEBUG ] deploying mds bootstrap to yypdmaster
[yypdmaster][DEBUG ] write cluster configuration to /etc/ceph/{cluster}.conf
[yypdmaster][DEBUG ] create path if it doesn't exist
[yypdmaster][INFO  ] Running command: ceph --cluster ceph --name client.bootstrap-mds --keyring /var/lib/ceph/bootstrap-mds/ceph.keyring auth get-or-create mds.yypdmaster osd allow rwx mds allow mon allow profile mds -o /var/lib/ceph/mds/ceph-yypdmaster/keyring
[yypdmaster][INFO  ] Running command: systemctl enable ceph-mds@yypdmaster
[yypdmaster][WARNIN] Created symlink from /etc/systemd/system/ceph-mds.target.wants/ceph-mds@yypdmaster.service to /lib/systemd/system/ceph-mds@.service.
[yypdmaster][INFO  ] Running command: systemctl start ceph-mds@yypdmaster
[yypdmaster][INFO  ] Running command: systemctl enable ceph.target
[yypdnode][DEBUG ] connected to host: yypdnode
[yypdnode][DEBUG ] detect platform information from remote host
[yypdnode][DEBUG ] detect machine type
[ceph_deploy.mds][INFO  ] Distro info: Ubuntu 16.04 xenial
[ceph_deploy.mds][DEBUG ] remote host will use systemd
[ceph_deploy.mds][DEBUG ] deploying mds bootstrap to yypdnode
[yypdnode][DEBUG ] write cluster configuration to /etc/ceph/{cluster}.conf
[yypdnode][DEBUG ] create path if it doesn't exist
[yypdnode][INFO  ] Running command: ceph --cluster ceph --name client.bootstrap-mds --keyring /var/lib/ceph/bootstrap-mds/ceph.keyring auth get-or-create mds.yypdnode osd allow rwx mds allow mon allow profile mds -o /var/lib/ceph/mds/ceph-yypdnode/keyring
[yypdnode][INFO  ] Running command: systemctl enable ceph-mds@yypdnode
[yypdnode][WARNIN] Created symlink from /etc/systemd/system/ceph-mds.target.wants/ceph-mds@yypdnode.service to /lib/systemd/system/ceph-mds@.service.
[yypdnode][INFO  ] Running command: systemctl start ceph-mds@yypdnode
[yypdnode][INFO  ] Running command: systemctl enable ceph.target
</code></pre>
<p>非常顺利。安装后，可以在任意一个node上看到mds在运行：</p>
<pre><code># ps -ef|grep ceph
ceph      7967     1  0 17:23 ?        00:00:00 /usr/bin/ceph-osd -f --cluster ceph --id 1 --setuser ceph --setgroup ceph
ceph     15674     1  0 17:32 ?        00:00:00 /usr/bin/ceph-mon -f --cluster ceph --id yypdnode --setuser ceph --setgroup ceph
ceph     18019     1  0 17:35 ?        00:00:00 /usr/bin/ceph-mds -f --cluster ceph --id yypdnode --setuser ceph --setgroup ceph
</code></pre>
<p>mds是存储cephfs的元信息的，我的ceph是10.2.7版本：</p>
<pre><code># ceph -v
ceph version 10.2.7 (50e863e0f4bc8f4b9e31156de690d765af245185)
</code></pre>
<p>虽然支持多 active mds并行运行，但官方文档建议保持一个active mds，其他mds作为standby(见下面ceph集群信息中的fsmap部分)：</p>
<pre><code># ceph -s
    cluster ffac3489-d678-4caf-ada2-3dd0743158b6
    ... ...
      fsmap e6: 1/1/1 up {0=yypdnode=up:active}, 1 up:standby
     osdmap e19: 2 osds: 2 up, 2 in
            flags sortbitwise,require_jewel_osds
      pgmap v192498: 576 pgs, 5 pools, 126 MB data, 238 objects
            44365 MB used, 31881 MB / 80374 MB avail
                 576 active+clean
</code></pre>
<h2>三、创建fs并测试挂载</h2>
<p>我们在ceph上创建一个fs：</p>
<pre><code># ceph osd pool create cephfs_data 128
pool 'cephfs_data' created

# ceph osd pool create cephfs_metadata 128
pool 'cephfs_metadata' created

# ceph fs new test_fs cephfs_metadata cephfs_data
new fs with metadata pool 2 and data pool 1

# ceph fs ls
name: test_fs, metadata pool: cephfs_metadata, data pools: [cephfs_data ]
</code></pre>
<p>不过，ceph当前正式版功能中仅支持一个fs，对多个fs的支持仅存在于实验feature中：</p>
<pre><code># ceph osd pool create cephfs1_data 128
# ceph osd pool create cephfs1_metadata 128
# ceph fs new test_fs1 cephfs1_metadata cephfs1_data
Error EINVAL: Creation of multiple filesystems is disabled.  To enable this experimental feature, use 'ceph fs flag set enable_multiple true'
</code></pre>
<p>在物理机上挂载cephfs可以使用mount命令、mount.ceph(apt-get install ceph-fs-common)或ceph-fuse(apt-get install ceph-fuse)，我们先用mount命令挂载：</p>
<pre><code>我们将上面创建的cephfs挂载到主机的/mnt下：

#mount -t ceph ceph_mon_host:6789:/ /mnt -o name=admin,secretfile=admin.secret

# cat admin.secret //ceph.client.admin.keyring中的key
AQDITghZD+c/DhAArOiWWQqyMAkMJbWmHaxjgQ==
</code></pre>
<p>查看cephfs信息：</p>
<pre><code># df -h
ceph_mon_host:6789:/   79G   45G   35G  57% /mnt
</code></pre>
<p>可以看出：cephfs将两个物理节点上的磁盘全部空间作为了自己的空间。</p>
<p>通过ceph-fuse挂载，还可以限制对挂载路径的访问权限，我们来创建用户foo，让其仅仅拥有对/ceph-volume1-test路径具有只读访问权限：</p>
<pre><code># ceph auth get-or-create client.foo mon 'allow *' mds 'allow r path=/ceph-volume1-test' osd 'allow *'
# ceph-fuse -n client.foo -m 10.47.217.91:6789 /mnt -r /ceph-volume1-test
ceph-fuse[10565]: starting ceph client2017-05-03 16:07:25.958903 7f1a14fbff00 -1 init, newargv = 0x557e350defc0 newargc=11
ceph-fuse[10565]: starting fuse
</code></pre>
<p>查看挂载路径，并尝试创建文件：</p>
<pre><code># cd /mnt
root@yypdnode:/mnt# ls
1.txt
root@yypdnode:/mnt# touch 2.txt
touch: cannot touch '2.txt': Permission denied
</code></pre>
<p>由于foo用户只拥有对 /ceph-volume1-test的只读权限，因此创建文件失败了！</p>
<h2>四、Kubernetes跨节点挂载CephFS</h2>
<p>在K8s中，至少可以通过两种方式挂载CephFS，一种是通过Pod直接挂载；另外一种则是通过pv和pvc挂载。我们分别来看。</p>
<h3>1、Pod直接挂载CephFS</h3>
<pre><code>//ceph-pod2-with-secret.yaml
apiVersion: v1
kind: Pod
metadata:
  name: ceph-pod2-with-secret
spec:
  containers:
  - name: ceph-ubuntu2
    image: ubuntu:14.04
    command: ["tail", "-f", "/var/log/bootstrap.log"]
    volumeMounts:
    - name: ceph-vol2
      mountPath: /mnt/cephfs/data
      readOnly: false
  volumes:
  - name: ceph-vol2
    cephfs:
      monitors:
      - ceph_mon_host:6789
      user: admin
      secretFile: "/etc/ceph/admin.secret"
      readOnly: false
</code></pre>
<p>注意：保证每个节点上都存在/etc/ceph/admin.secret文件。</p>
<p>查看Pod挂载的内容：</p>
<pre><code># docker ps|grep pod
bc96431408c7        ubuntu:14.04                                                  "tail -f /var/log/boo"   About a minute ago   Up About a minute                                                                        k8s_ceph-ubuntu2.66c44128_ceph-pod2-with-secret_default_3d8a05f8-33c3-11e7-bcd9-6640d35a0e90_fc483b8a
bcc65ab82069        gcr.io/google_containers/pause-amd64:3.0                      "/pause"                 About a minute ago   Up About a minute                                                                        k8s_POD.d8dbe16c_ceph-pod2-with-secret_default_3d8a05f8-33c3-11e7-bcd9-6640d35a0e90_02381204

root@yypdnode:~# docker exec bc96431408c7 ls /mnt/cephfs/data
1.txt
apps
ceph-volume1-test
test1.txt

</code></pre>
<p>我们再在另外一个node上启动挂载同一个cephfs的Pod，看是否可以跨节点挂载：</p>
<pre><code># kubectl get pods

NAMESPACE                    NAME                                    READY     STATUS    RESTARTS   AGE       IP             NODE
default                      ceph-pod2-with-secret                   1/1       Running   0          3m        172.30.192.2   iz2ze39jeyizepdxhwqci6z
default                      ceph-pod2-with-secret-on-master         1/1       Running   0          3s        172.30.0.51    iz25beglnhtz
... ...

# kubectl exec ceph-pod2-with-secret-on-master ls /mnt/cephfs/data
1.txt
apps
ceph-volume1-test
test1.txt

</code></pre>
<p>可以看到不同节点可以挂载同一CephFS。我们在一个pod中操作一下挂载的cephfs：</p>
<pre><code># kubectl exec ceph-pod2-with-secret-on-master -- bash -c "for i in {1..10}; do sleep 1; echo 'pod2-with-secret-on-master: Hello, World'&gt;&gt; /mnt/cephfs/data/foo.txt ; done "
root@yypdmaster:~/k8stest/cephfstest/footest# kubectl exec ceph-pod2-with-secret-on-master cat /mnt/cephfs/data/foo.txt
pod2-with-secret-on-master: Hello, World
pod2-with-secret-on-master: Hello, World
pod2-with-secret-on-master: Hello, World
pod2-with-secret-on-master: Hello, World
pod2-with-secret-on-master: Hello, World
pod2-with-secret-on-master: Hello, World
pod2-with-secret-on-master: Hello, World
pod2-with-secret-on-master: Hello, World
pod2-with-secret-on-master: Hello, World
pod2-with-secret-on-master: Hello, World
</code></pre>
<h3>2、通过PV和PVC挂载CephFS</h3>
<p>挂载cephfs的pv和pvc在写法方面与上面挂载rbd的类似：</p>
<pre><code>//ceph-pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: foo-pv
spec:
  capacity:
    storage: 512Mi
  accessModes:
    - ReadWriteMany
  cephfs:
    monitors:
      - ceph_mon_host:6789
    path: /
    user: admin
    secretRef:
      name: ceph-secret
    readOnly: false
  persistentVolumeReclaimPolicy: Recycle

//ceph-pvc.yaml

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: foo-claim
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 512Mi
</code></pre>
<p>使用pvc的pod:</p>
<pre><code>//ceph-pod2.yaml
apiVersion: v1
kind: Pod
metadata:
  name: ceph-pod2
spec:
  containers:
  - name: ceph-ubuntu2
    image: ubuntu:14.04
    command: ["tail", "-f", "/var/log/bootstrap.log"]
    volumeMounts:
    - name: ceph-vol2
      mountPath: /mnt/cephfs/data
      readOnly: false
  volumes:
  - name: ceph-vol2
    persistentVolumeClaim:
      claimName: foo-claim
</code></pre>
<p>创建pv、pvc：</p>
<pre><code># kubectl create -f ceph-pv.yaml
persistentvolume "foo-pv" created
# kubectl create -f ceph-pvc.yaml
persistentvolumeclaim "foo-claim" created

# kubectl get pvc
NAME        STATUS    VOLUME    CAPACITY   ACCESSMODES   AGE
foo-claim   Bound     foo-pv    512Mi      RWX           4s
# kubectl get pv
NAME      CAPACITY   ACCESSMODES   RECLAIMPOLICY   STATUS    CLAIM               REASON    AGE
foo-pv    512Mi      RWX           Recycle         Bound     default/foo-claim             24s
</code></pre>
<p>启动pod，通过exec命令查看挂载情况：</p>
<pre><code># docker ps|grep pod
a6895ec0274f        ubuntu:14.04                                                  "tail -f /var/log/boo"   About a minute ago   Up About a minute                                                                        k8s_ceph-ubuntu2.66c44128_ceph-pod2_default_4e4fc8d4-33c6-11e7-bcd9-6640d35a0e90_1b37ed76
52b6811a6584        gcr.io/google_containers/pause-amd64:3.0                      "/pause"                 About a minute ago   Up About a minute                                                                        k8s_POD.d8dbe16c_ceph-pod2_default_4e4fc8d4-33c6-11e7-bcd9-6640d35a0e90_27e5f988
55b96edbf4bf        ubuntu:14.04                                                  "tail -f /var/log/boo"   14 minutes ago       Up 14 minutes                                                                            k8s_ceph-ubuntu2.66c44128_ceph-pod2-with-secret_default_9d383b0c-33c4-11e7-bcd9-6640d35a0e90_1656e5e0
f8b699bc0459        gcr.io/google_containers/pause-amd64:3.0                      "/pause"                 14 minutes ago       Up 14 minutes                                                                            k8s_POD.d8dbe16c_ceph-pod2-with-secret_default_9d383b0c-33c4-11e7-bcd9-6640d35a0e90_effdfae7
root@yypdnode:~# docker exec a6895ec0274f ls /mnt/cephfs/data
1.txt
apps
ceph-volume1-test
foo.txt
test1.txt

# docker exec a6895ec0274f cat /mnt/cephfs/data/foo.txt
pod2-with-secret-on-master: Hello, World
pod2-with-secret-on-master: Hello, World
pod2-with-secret-on-master: Hello, World
pod2-with-secret-on-master: Hello, World
pod2-with-secret-on-master: Hello, World
pod2-with-secret-on-master: Hello, World
pod2-with-secret-on-master: Hello, World
pod2-with-secret-on-master: Hello, World
pod2-with-secret-on-master: Hello, World
pod2-with-secret-on-master: Hello, World

</code></pre>
<h2>五、pv的状态</h2>
<p>如果你不删除pvc，一切都安然无事：</p>
<pre><code># kubectl get pv
NAME                CAPACITY   ACCESSMODES   RECLAIMPOLICY   STATUS    CLAIM                        REASON    AGE
foo-pv              512Mi      RWX           Recycle         Bound     default/foo-claim                      1h

# kubectl get pvc
NAME                 STATUS    VOLUME              CAPACITY   ACCESSMODES   AGE
foo-claim            Bound     foo-pv              512Mi      RWX           1h
</code></pre>
<p>但是如果删除pvc，pv的状态将变成failed：</p>
<pre><code>删除pvc：

# kubectl get pv
NAME                CAPACITY   ACCESSMODES   RECLAIMPOLICY   STATUS    CLAIM                        REASON    AGE
foo-pv              512Mi      RWX           Recycle         Failed    default/foo-claim                      2h

# kubectl describe pv/foo-pv
Name:        foo-pv
Labels:        &lt;none&gt;
Status:        Failed
Claim:        default/foo-claim
Reclaim Policy:    Recycle
Access Modes:    RWX
Capacity:    512Mi
Message:    No recycler plugin found for the volume!
Source:
    Type:        RBD (a Rados Block Device mount on the host that shares a pod's lifetime)
    CephMonitors:    [xx.xx.xx.xx:6789]
    RBDImage:        foo1
    FSType:        ext4
    RBDPool:        rbd
    RadosUser:        admin
    Keyring:        /etc/ceph/keyring
    SecretRef:        &amp;{ceph-secret}
    ReadOnly:        false
Events:
  FirstSeen    LastSeen    Count    From                SubobjectPath    Type        Reason            Message
  ---------    --------    -----    ----                -------------    --------    ------            -------
  29s        29s        1    {persistentvolume-controller }            Warning        VolumeFailedRecycle    No recycler plugin found for the volume!

</code></pre>
<p>我们在pv中指定的persistentVolumeReclaimPolicy是Recycle，但无论是cephrbd还是cephfs都不没有对应的recycler plugin，导致pv的status变成了failed，只能手工删除重建。</p>
<p style='text-align:left'>&copy; 2017, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2017/05/08/mount-cephfs-acrossing-nodes-in-kubernetes-cluster/feed/</wfw:commentRss>
		<slash:comments>25</slash:comments>
		</item>
		<item>
		<title>使用Fluentd和ElasticSearch Stack实现Kubernetes的集群Logging</title>
		<link>https://tonybai.com/2017/03/03/implement-kubernetes-cluster-level-logging-with-fluentd-and-elasticsearch-stack/</link>
		<comments>https://tonybai.com/2017/03/03/implement-kubernetes-cluster-level-logging-with-fluentd-and-elasticsearch-stack/#comments</comments>
		<pubDate>Fri, 03 Mar 2017 12:17:38 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[borg]]></category>
		<category><![CDATA[Ceph]]></category>
		<category><![CDATA[cephrbd]]></category>
		<category><![CDATA[container]]></category>
		<category><![CDATA[DaemonSet]]></category>
		<category><![CDATA[dashboard]]></category>
		<category><![CDATA[deployment]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[EFK]]></category>
		<category><![CDATA[ElasticSearch]]></category>
		<category><![CDATA[ELK]]></category>
		<category><![CDATA[Fluentd]]></category>
		<category><![CDATA[gem]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[heapster]]></category>
		<category><![CDATA[k8s]]></category>
		<category><![CDATA[Kibana]]></category>
		<category><![CDATA[kube-up.sh]]></category>
		<category><![CDATA[kubeadm]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[Logstash]]></category>
		<category><![CDATA[Ruby]]></category>
		<category><![CDATA[serviceaccount]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=2200</guid>
		<description><![CDATA[在本篇文章中，我们继续来说Kubernetes。 经过一段时间的探索，我们先后完成了Kubernetes集群搭建，DNS、Dashboard、Heapster等插件安装，集群安全配置，搭建作为Persistent Volume的CephRBD，以及服务更新等探索和实现工作。现在Kubernetes集群层面的Logging需求逐渐浮上水面了。 随着一些小应用在我们的Kubernetes集群上的部署上线，集群的运行迈上了正轨。但问题随之而来，那就是如何查找和诊断集群自身的问题以及运行于Pod中应用的问题。日志，没错！我们也只能依赖Kubernetes组件以及Pod中应用输出的日志。不过目前我们仅能通过kubectl logs命令或Kubernetes Dashboard来查看Log。在没有cluster level logging的情况下，我们需要分别查看各个Pod的日志，操作繁琐，过程低效。我们迫切地需要为Kubernetes集群搭建一套集群级别的集中日志收集和分析设施。 对于任何基础设施或后端服务系统，日志都是极其重要的。对于受Google内部容器管理系统Borg启发而催生出的Kubernetes项目来说，自然少不了对Logging的支持。在“Logging Overview“中，官方概要介绍了Kubernetes上的几个层次的Logging方案，并给出Cluster-level logging的参考架构： Kubernetes还给出了参考实现： &#8211; Logging Backend：Elastic Search stack(包括：Kibana) &#8211; Logging-agent：fluentd ElasticSearch stack实现的cluster level logging的一个优势在于其对Kubernetes集群中的Pod没有侵入性，Pod无需做任何配合性改动。同时EFK/ELK方案在业内也是相对成熟稳定的。 在本文中，我将为我们的Kubernetes 1.3.7集群安装ElasticSearch、Fluentd和Kibana。由于1.3.7版本略有些old，EFK能否在其上面run起来，我也是心中未知。能否像《生化危机：终章》那样有一个完美的结局，我们还需要一步一步“打怪升级”慢慢看。 一、Kubernetes 1.3.7集群的 “漏网之鱼” Kubernetes 1.3.7集群是通过kube-up.sh搭建并初始化的。按照K8s官方文档有关elasticsearch logging的介绍，在kubernetes/cluster/ubuntu/config-default.sh中，我也发现了下面几个配置项： // kubernetes/cluster/ubuntu/config-default.sh # Optional: Enable node logging. ENABLE_NODE_LOGGING=false LOGGING_DESTINATION=${LOGGING_DESTINATION:-elasticsearch} # Optional: When set to true, Elasticsearch and Kibana will be setup as part of [...]]]></description>
			<content:encoded><![CDATA[<p>在本篇文章中，我们继续来说<a href="http://tonybai.com/tag/kubernetes">Kubernetes</a>。</p>
<p>经过一段时间的探索，我们先后完成了<a href="http://tonybai.com/2016/10/18/learn-how-to-install-kubernetes-on-ubuntu/">Kubernetes集群搭建</a>，<a href="http://tonybai.com/2016/10/23/install-dns-addon-for-k8s/">DNS</a>、<a href="http://tonybai.com/2017/01/19/install-dashboard-addon-for-k8s/">Dashboard</a>、<a href="http://tonybai.com/2017/01/20/integrate-heapster-for-kubernetes-dashboard/">Heapster</a>等插件安装，<a href="http://tonybai.com/2016/11/25/the-security-settings-for-kubernetes-cluster/">集群安全配置</a>，搭建<a href="http://tonybai.com/2016/11/07/integrate-kubernetes-with-ceph-rbd/">作为Persistent Volume的CephRBD</a>，以及<a href="http://tonybai.com/2017/02/09/rolling-update-for-services-in-kubernetes-cluster">服务更新</a>等<a href="http://tonybai.com/2017/01/24/explore-kubernetes-cluster-installed-by-kubeadm">探索</a>和实现工作。现在Kubernetes<a href="https://kubernetes.io/docs/user-guide/logging/overview/">集群层面的Logging</a>需求逐渐浮上水面了。</p>
<p>随着一些小应用在我们的Kubernetes集群上的部署上线，集群的运行迈上了正轨。但问题随之而来，那就是如何查找和诊断集群自身的问题以及运行于Pod中应用的问题。日志，没错！我们也只能依赖Kubernetes组件以及Pod中应用输出的日志。不过目前我们仅能通过kubectl logs命令或<a href="http://tonybai.com/2017/01/19/install-dashboard-addon-for-k8s/">Kubernetes Dashboard</a>来查看Log。在没有cluster level logging的情况下，我们需要分别查看各个Pod的日志，操作繁琐，过程低效。我们迫切地需要为Kubernetes集群搭建一套集群级别的集中日志收集和分析设施。</p>
<p>对于任何基础设施或后端服务系统，日志都是极其重要的。对于受Google内部容器管理系统Borg启发而催生出的Kubernetes项目来说，自然少不了对Logging的支持。在“<a href="https://kubernetes.io/docs/user-guide/logging/overview/">Logging Overview</a>“中，官方概要介绍了Kubernetes上的几个层次的Logging方案，并给出Cluster-level logging的参考架构：</p>
<p><img src="http://tonybai.com/wp-content/uploads/k8s-cluster-level-logging-architectures.png" alt="img{512x368}" /></p>
<p>Kubernetes还给出了参考实现：<br />
    &#8211; Logging Backend：<a href="https://www.elastic.co/">Elastic Search</a> stack(包括：<a href="https://www.elastic.co/products/kibana">Kibana</a>)<br />
    &#8211; Logging-agent：<a href="http://www.fluentd.org/">fluentd</a></p>
<p>ElasticSearch stack实现的cluster level logging的一个优势在于其对Kubernetes集群中的Pod没有侵入性，Pod无需做任何配合性改动。同时EFK/ELK方案在业内也是相对成熟稳定的。</p>
<p>在本文中，我将为我们的Kubernetes 1.3.7集群安装ElasticSearch、Fluentd和Kibana。由于1.3.7版本略有些old，EFK能否在其上面run起来，我也是心中未知。能否像《<a href="https://movie.douban.com/subject/20471852/">生化危机：终章</a>》那样有一个完美的结局，我们还需要一步一步“打怪升级”慢慢看。</p>
<h3>一、Kubernetes 1.3.7集群的 “漏网之鱼”</h3>
<p>Kubernetes 1.3.7集群是通过kube-up.sh搭建并初始化的。按照<a href="https://kubernetes.io/docs/user-guide/logging/elasticsearch/">K8s官方文档</a>有关elasticsearch logging的介绍，在kubernetes/cluster/ubuntu/config-default.sh中，我也发现了下面几个配置项：</p>
<pre><code>// kubernetes/cluster/ubuntu/config-default.sh
# Optional: Enable node logging.
ENABLE_NODE_LOGGING=false
LOGGING_DESTINATION=${LOGGING_DESTINATION:-elasticsearch}

# Optional: When set to true, Elasticsearch and Kibana will be setup as part of the cluster bring up.
ENABLE_CLUSTER_LOGGING=false
ELASTICSEARCH_LOGGING_REPLICAS=${ELASTICSEARCH_LOGGING_REPLICAS:-1}
</code></pre>
<p>显然，当初如果搭建集群伊始时要是知道这些配置的意义，可能那个时候就会将elastic logging集成到集群中了。现在为时已晚，集群上已经跑了很多应用，无法重新通过kube-up.sh中断集群运行并安装elastic logging了。我只能手工进行安装了！</p>
<h3>二、镜像准备</h3>
<p>1.3.7源码中kubernetes/cluster/addons/fluentd-elasticsearch下的manifest已经比较old了，我们直接使用kubernetes最新源码中的<a href="https://github.com/kubernetes/kubernetes/tree/master/cluster/addons/fluentd-elasticsearch">manifest文件</a>：</p>
<pre><code>k8s.io/kubernetes/cluster/addons/fluentd-elasticsearch$ ls *.yaml
es-controller.yaml  es-service.yaml  fluentd-es-ds.yaml  kibana-controller.yaml  kibana-service.yaml
</code></pre>
<p>分析这些yaml，我们需要三个镜像：</p>
<pre><code> gcr.io/google_containers/fluentd-elasticsearch:1.22
 gcr.io/google_containers/elasticsearch:v2.4.1-1
 gcr.io/google_containers/kibana:v4.6.1-1
</code></pre>
<p>显然镜像都在墙外。由于生产环境下的Docker引擎并没有配置加速器代理，因此我们需要手工下载一下这三个镜像。我采用的方法是通过另外一台配置了加速器的机器上的<a href="http://tonybai.com/tag/docker">Docker引擎</a>将三个image下载，并重新打tag，上传到我在hub.docker.com上的账号下，以elasticsearch:v2.4.1-1为例：</p>
<pre><code># docker pull  gcr.io/google_containers/elasticsearch:v2.4.1-1
# docker tag gcr.io/google_containers/elasticsearch:v2.4.1-1 bigwhite/elasticsearch:v2.4.1-1
# docker push bigwhite/elasticsearch:v2.4.1-1
</code></pre>
<p>下面是我们在后续安装过程中真正要使用到的镜像：</p>
<pre><code>bigwhite/fluentd-elasticsearch:1.22
bigwhite/elasticsearch:v2.4.1-1
bigwhite/kibana:v4.6.1-1
</code></pre>
<h3>三、启动fluentd</h3>
<p>fluentd是以<a href="https://kubernetes.io/docs/admin/daemons/">DaemonSet</a>的形式跑在K8s集群上的，这样k8s可以保证每个k8s cluster node上都会启动一个fluentd(注意：将image改为上述镜像地址，如果你配置了加速器，那自然就不必了)。</p>
<pre><code># kubectl create -f fluentd-es-ds.yaml --record
daemonset "fluentd-es-v1.22" created
</code></pre>
<p>查看daemonset中的Pod的启动情况，我们发现：</p>
<pre><code>kube-system                  fluentd-es-v1.22-as3s5                  0/1       CrashLoopBackOff   2          43s       172.16.99.6    10.47.136.60
kube-system                  fluentd-es-v1.22-qz193                  0/1       CrashLoopBackOff   2          43s       172.16.57.7    10.46.181.146
</code></pre>
<p>fluentd Pod启动失败，fluentd的日志可以通过/var/log/fluentd.log查看：</p>
<pre><code># tail -100f /var/log/fluentd.log

2017-03-02 02:27:01 +0000 [info]: reading config file path="/etc/td-agent/td-agent.conf"
2017-03-02 02:27:01 +0000 [info]: starting fluentd-0.12.31
2017-03-02 02:27:01 +0000 [info]: gem 'fluent-mixin-config-placeholders' version '0.4.0'
2017-03-02 02:27:01 +0000 [info]: gem 'fluent-mixin-plaintextformatter' version '0.2.6'
2017-03-02 02:27:01 +0000 [info]: gem 'fluent-plugin-docker_metadata_filter' version '0.1.3'
2017-03-02 02:27:01 +0000 [info]: gem 'fluent-plugin-elasticsearch' version '1.5.0'
2017-03-02 02:27:01 +0000 [info]: gem 'fluent-plugin-kafka' version '0.4.1'
2017-03-02 02:27:01 +0000 [info]: gem 'fluent-plugin-kubernetes_metadata_filter' version '0.24.0'
2017-03-02 02:27:01 +0000 [info]: gem 'fluent-plugin-mongo' version '0.7.16'
2017-03-02 02:27:01 +0000 [info]: gem 'fluent-plugin-rewrite-tag-filter' version '1.5.5'
2017-03-02 02:27:01 +0000 [info]: gem 'fluent-plugin-s3' version '0.8.0'
2017-03-02 02:27:01 +0000 [info]: gem 'fluent-plugin-scribe' version '0.10.14'
2017-03-02 02:27:01 +0000 [info]: gem 'fluent-plugin-td' version '0.10.29'
2017-03-02 02:27:01 +0000 [info]: gem 'fluent-plugin-td-monitoring' version '0.2.2'
2017-03-02 02:27:01 +0000 [info]: gem 'fluent-plugin-webhdfs' version '0.4.2'
2017-03-02 02:27:01 +0000 [info]: gem 'fluentd' version '0.12.31'
2017-03-02 02:27:01 +0000 [info]: adding match pattern="fluent.**" type="null"
2017-03-02 02:27:01 +0000 [info]: adding filter pattern="kubernetes.**" type="kubernetes_metadata"
2017-03-02 02:27:02 +0000 [error]: config error file="/etc/td-agent/td-agent.conf" error="Invalid Kubernetes API v1 endpoint https://192.168.3.1:443/api: 401 Unauthorized"
2017-03-02 02:27:02 +0000 [info]: process finished code=256
2017-03-02 02:27:02 +0000 [warn]: process died within 1 second. exit.
</code></pre>
<p>从上述日志中的error来看：fluentd访问apiserver secure port(443)出错了：Unauthorized!  通过分析 cluster/addons/fluentd-elasticsearch/fluentd-es-image/build.sh和td-agent.conf，我们发现是fluentd image中的<a href="https://github.com/fabric8io/fluent-plugin-kubernetes_metadata_filter">fluent-plugin-kubernetes_metadata_filter</a>要去访问API Server以获取一些kubernetes的metadata信息。不过未做任何特殊配置的fluent-plugin-kubernetes_metadata_filter，我猜测它使用的是kubernetes为Pod传入的环境变量：KUBERNETES_SERVICE_HOST和KUBERNETES_SERVICE_PORT来得到API Server的访问信息的。但API Server在secure port上是开启了<a href="http://tonybai.com/2016/11/25/the-security-settings-for-kubernetes-cluster/">安全身份验证机制</a>的，fluentd直接访问必然是失败的。</p>
<p>我们找到了fluent-plugin-kubernetes_metadata_filter项目在github.com上的<a href="https://github.com/fabric8io/fluent-plugin-kubernetes_metadata_filter">主页</a>，在这个页面上我们看到了fluent-plugin-kubernetes_metadata_filter支持的其他配置，包括：ca_file、client_cert、client_key等，显然这些字眼非常眼熟。我们需要修改一下fluentd image中td-agent.conf的配置，为fluent-plugin-kubernetes_metadata_filter增加一些配置项，比如：</p>
<pre><code>// td-agent.conf
... ...
&lt;filter kubernetes.**&gt;
  type kubernetes_metadata
  ca_file /srv/kubernetes/ca.crt
  client_cert /srv/kubernetes/kubecfg.crt
  client_key /srv/kubernetes/kubecfg.key
&lt;/filter&gt;
... ...
</code></pre>
<p>这里我不想重新制作image，那么怎么办呢？Kubernetes提供了<a href="https://kubernetes.io/docs/user-guide/configmap/">ConfigMap</a>这一强大的武器，我们可以将新版td-agent.conf制作成kubernetes的configmap资源，并挂载到fluentd pod的相应位置以替换image中默认的td-agent.conf。</p>
<p>需要注意两点：<br />
 * 在基于td-agent.conf创建configmap资源之前，需要将td-agent.conf中的注释行都删掉，否则生成的configmap的内容可能不正确；<br />
 * fluentd pod将创建在kube-system下，因此configmap资源也需要创建在kube-system namespace下面，否则kubectl create无法找到对应的configmap。</p>
<pre><code># kubectl create configmap td-agent-config --from-file=./td-agent.conf -n kube-system
configmap "td-agent-config" created

# kubectl get configmaps -n kube-system
NAME              DATA      AGE
td-agent-config   1         9s

# kubectl get configmaps td-agent-config -o yaml
apiVersion: v1
data:
  td-agent.conf: |
    &lt;match fluent.**&gt;
      type null
    &lt;/match&gt;

    &lt;source&gt;
      type tail
      path /var/log/containers/*.log
      pos_file /var/log/es-containers.log.pos
      time_format %Y-%m-%dT%H:%M:%S.%NZ
      tag kubernetes.*
      format json
      read_from_head true
    &lt;/source&gt;
... ...
</code></pre>
<p>fluentd-es-ds.yaml也要随之做一些改动，主要是增加两个mount: 一个是mount 上面的configmap td-agent-config，另外一个就是mount hostpath：/srv/kubernetes以获取到相关client端的数字证书：</p>
<pre><code>  spec:
      containers:
      - name: fluentd-es
        image: bigwhite/fluentd-elasticsearch:1.22
        command:
          - '/bin/sh'
          - '-c'
          - '/usr/sbin/td-agent 2&gt;&amp;1 &gt;&gt; /var/log/fluentd.log'
        resources:
          limits:
            memory: 200Mi
          #requests:
            #cpu: 100m
            #memory: 200Mi
        volumeMounts:
        - name: varlog
          mountPath: /var/log
        - name: varlibdockercontainers
          mountPath: /var/lib/docker/containers
          readOnly: true
        - name: td-agent-config
          mountPath: /etc/td-agent
        - name: tls-files
          mountPath: /srv/kubernetes
      terminationGracePeriodSeconds: 30
      volumes:
      - name: varlog
        hostPath:
          path: /var/log
      - name: varlibdockercontainers
        hostPath:
          path: /var/lib/docker/containers
      - name: td-agent-config
        configMap:
          name: td-agent-config
      - name: tls-files
        hostPath:
          path: /srv/kubernetes
</code></pre>
<p>接下来，我们重新创建fluentd ds，步骤不赘述。这回我们的创建成功了：</p>
<pre><code>kube-system                  fluentd-es-v1.22-adsrx                  1/1       Running    0          1s        172.16.99.6    10.47.136.60
kube-system                  fluentd-es-v1.22-rpme3                  1/1       Running    0          1s        172.16.57.7    10.46.181.146
</code></pre>
<p>但通过查看/var/log/fluentd.log，我们依然能看到“问题”：</p>
<pre><code>2017-03-02 03:57:58 +0000 [warn]: temporarily failed to flush the buffer. next_retry=2017-03-02 03:57:59 +0000 error_class="Fluent::ElasticsearchOutput::ConnectionFailure" error="Can not reach Elasticsearch cluster ({:host=&gt;\"elasticsearch-logging\", :port=&gt;9200, :scheme=&gt;\"http\"})!" plugin_id="object:3fd99fa857d8"
  2017-03-02 03:57:58 +0000 [warn]: suppressed same stacktrace
2017-03-02 03:58:00 +0000 [warn]: temporarily failed to flush the buffer. next_retry=2017-03-02 03:58:03 +0000 error_class="Fluent::ElasticsearchOutput::ConnectionFailure" error="Can not reach Elasticsearch cluster ({:host=&gt;\"elasticsearch-logging\", :port=&gt;9200, :scheme=&gt;\"http\"})!" plugin_id="object:3fd99fa857d8"
2017-03-02 03:58:00 +0000 [info]: process finished code=9
2017-03-02 03:58:00 +0000 [error]: fluentd main process died unexpectedly. restarting.
</code></pre>
<p>由于ElasticSearch logging还未创建，这是连不上elasticsearch所致。</p>
<h3>四、启动elasticsearch</h3>
<p>启动elasticsearch：</p>
<pre><code># kubectl create -f es-controller.yaml
replicationcontroller "elasticsearch-logging-v1" created

# kubectl create -f es-service.yaml
service "elasticsearch-logging" created

get pods：

kube-system                  elasticsearch-logging-v1-3bzt6          1/1       Running    0          7s        172.16.57.8    10.46.181.146
kube-system                  elasticsearch-logging-v1-nvbe1          1/1       Running    0          7s        172.16.99.10   10.47.136.60
</code></pre>
<p>elastic search logging启动成功后，上述fluentd的fail日志就没有了！</p>
<p>不过elastic search真的运行ok了么？我们查看一下elasticsearch相关Pod日志：</p>
<pre><code># kubectl logs -f elasticsearch-logging-v1-3bzt6 -n kube-system
F0302 03:59:41.036697       8 elasticsearch_logging_discovery.go:60] kube-system namespace doesn't exist: the server has asked for the client to provide credentials (get namespaces kube-system)
goroutine 1 [running]:
k8s.io/kubernetes/vendor/github.com/golang/glog.stacks(0x19a8100, 0xc400000000, 0xc2, 0x186)
... ...
main.main()
    elasticsearch_logging_discovery.go:60 +0xb53

[2017-03-02 03:59:42,587][INFO ][node                     ] [elasticsearch-logging-v1-3bzt6] version[2.4.1], pid[16], build[c67dc32/2016-09-27T18:57:55Z]
[2017-03-02 03:59:42,588][INFO ][node                     ] [elasticsearch-logging-v1-3bzt6] initializing ...
[2017-03-02 03:59:44,396][INFO ][plugins                  ] [elasticsearch-logging-v1-3bzt6] modules [reindex, lang-expression, lang-groovy], plugins [], sites []
... ...
[2017-03-02 03:59:44,441][INFO ][env                      ] [elasticsearch-logging-v1-3bzt6] heap size [1007.3mb], compressed ordinary object pointers [true]
[2017-03-02 03:59:48,355][INFO ][node                     ] [elasticsearch-logging-v1-3bzt6] initialized
[2017-03-02 03:59:48,355][INFO ][node                     ] [elasticsearch-logging-v1-3bzt6] starting ...
[2017-03-02 03:59:48,507][INFO ][transport                ] [elasticsearch-logging-v1-3bzt6] publish_address {172.16.57.8:9300}, bound_addresses {[::]:9300}
[2017-03-02 03:59:48,547][INFO ][discovery                ] [elasticsearch-logging-v1-3bzt6] kubernetes-logging/7_f_M2TKRZWOw4NhBc4EqA
[2017-03-02 04:00:18,552][WARN ][discovery                ] [elasticsearch-logging-v1-3bzt6] waited for 30s and no initial state was set by the discovery
[2017-03-02 04:00:18,562][INFO ][http                     ] [elasticsearch-logging-v1-3bzt6] publish_address {172.16.57.8:9200}, bound_addresses {[::]:9200}
[2017-03-02 04:00:18,562][INFO ][node                     ] [elasticsearch-logging-v1-3bzt6] started
[2017-03-02 04:01:15,754][WARN ][discovery.zen.ping.unicast] [elasticsearch-logging-v1-3bzt6] failed to send ping to [{#zen_unicast_1#}{127.0.0.1}{127.0.0.1:9300}]
SendRequestTransportException[[][127.0.0.1:9300][internal:discovery/zen/unicast]]; nested: NodeNotConnectedException[[][127.0.0.1:9300] Node not connected];
... ...
Caused by: NodeNotConnectedException[[][127.0.0.1:9300] Node not connected]
    at org.elasticsearch.transport.netty.NettyTransport.nodeChannel(NettyTransport.java:1141)
    at org.elasticsearch.transport.netty.NettyTransport.sendRequest(NettyTransport.java:830)
    at org.elasticsearch.transport.TransportService.sendRequest(TransportService.java:329)
    ... 12 more
</code></pre>
<p>总结了一下，日志中有两个错误：<br />
- 无法访问到API Server，这个似乎和fluentd最初的问题一样；<br />
- elasticsearch两个节点间互ping失败。</p>
<p>要想找到这两个问题的原因，还得回到源头，去分析elastic search image的组成。</p>
<p>通过cluster/addons/fluentd-elasticsearch/es-image/run.sh文件内容：</p>
<pre><code>/elasticsearch_logging_discovery &gt;&gt; /elasticsearch/config/elasticsearch.yml

chown -R elasticsearch:elasticsearch /data

/bin/su -c /elasticsearch/bin/elasticsearch elasticsearch
</code></pre>
<p>我们了解到image中，其实包含了两个程序，一个为/elasticsearch_logging_discovery，该程序执行后生成一个配置文件： /elasticsearch/config/elasticsearch.yml。该配置文件后续被另外一个程序：/elasticsearch/bin/elasticsearch使用。</p>
<p>我们查看一下已经运行的docker中的elasticsearch.yml文件内容：</p>
<pre><code># docker exec 3cad31f6eb08 cat /elasticsearch/config/elasticsearch.yml
cluster.name: kubernetes-logging

node.name: ${NODE_NAME}
node.master: ${NODE_MASTER}
node.data: ${NODE_DATA}

transport.tcp.port: ${TRANSPORT_PORT}
http.port: ${HTTP_PORT}

path.data: /data

network.host: 0.0.0.0

discovery.zen.minimum_master_nodes: ${MINIMUM_MASTER_NODES}
discovery.zen.ping.multicast.enabled: false
</code></pre>
<p>这个结果中缺少了一项：</p>
<pre><code>discovery.zen.ping.unicast.hosts: ["172.30.0.11", "172.30.192.15"]
</code></pre>
<p>这也是导致第二个问题的原因。综上，elasticsearch logging的错误其实都是由于/elasticsearch_logging_discovery无法访问API Server导致 /elasticsearch/config/elasticsearch.yml没有被正确生成造成的，我们就来解决这个问题。</p>
<p>我查看了一下/elasticsearch_logging_discovery的<a href="https://github.com/kubernetes/kubernetes/blob/master/cluster/addons/fluentd-elasticsearch/es-image/elasticsearch_logging_discovery.go">源码</a>，elasticsearch_logging_discovery是一个典型通过<a href="https://github.com/kubernetes/client-go">client-go</a>通过service account访问API Server的程序，很显然这就是我在《<a href="http://tonybai.com/2017/03/03/access-api-server-from-a-pod-through-serviceaccount">在Kubernetes Pod中使用Service Account访问API Server</a>》一文中提到的那个问题：默认的service account不好用。</p>
<p>解决方法：在kube-system namespace下创建一个新的service account资源，并在es-controller.yaml中显式使用该新创建的service account。</p>
<p>创建一个新的serviceaccount在kube-system namespace下：</p>
<pre><code>//serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: k8s-efk

# kubectl create -f serviceaccount.yaml -n kube-system
serviceaccount "k8s-efk" created

# kubectl get serviceaccount -n kube-system
NAME      SECRETS   AGE
default   1         139d
k8s-efk   1         17s
</code></pre>
<p>在es-controller.yaml中，使用service account “k8s-efk”：</p>
<pre><code>//es-controller.yaml
... ...
spec:
  replicas: 2
  selector:
    k8s-app: elasticsearch-logging
    version: v1
  template:
    metadata:
      labels:
        k8s-app: elasticsearch-logging
        version: v1
        kubernetes.io/cluster-service: "true"
    spec:
      serviceAccount: k8s-efk
      containers:
... ...
</code></pre>
<p>重新创建elasticsearch logging service后，我们再来查看elasticsearch-logging pod的日志：</p>
<pre><code># kubectl logs -f elasticsearch-logging-v1-dklui -n kube-system
[2017-03-02 08:26:46,500][INFO ][node                     ] [elasticsearch-logging-v1-dklui] version[2.4.1], pid[14], build[c67dc32/2016-09-27T18:57:55Z]
[2017-03-02 08:26:46,504][INFO ][node                     ] [elasticsearch-logging-v1-dklui] initializing ...
[2017-03-02 08:26:47,984][INFO ][plugins                  ] [elasticsearch-logging-v1-dklui] modules [reindex, lang-expression, lang-groovy], plugins [], sites []
[2017-03-02 08:26:48,073][INFO ][env                      ] [elasticsearch-logging-v1-dklui] using [1] data paths, mounts [[/data (/dev/vda1)]], net usable_space [16.9gb], net total_space [39.2gb], spins? [possibly], types [ext4]
[2017-03-02 08:26:48,073][INFO ][env                      ] [elasticsearch-logging-v1-dklui] heap size [1007.3mb], compressed ordinary object pointers [true]
[2017-03-02 08:26:53,241][INFO ][node                     ] [elasticsearch-logging-v1-dklui] initialized
[2017-03-02 08:26:53,241][INFO ][node                     ] [elasticsearch-logging-v1-dklui] starting ...
[2017-03-02 08:26:53,593][INFO ][transport                ] [elasticsearch-logging-v1-dklui] publish_address {172.16.57.8:9300}, bound_addresses {[::]:9300}
[2017-03-02 08:26:53,651][INFO ][discovery                ] [elasticsearch-logging-v1-dklui] kubernetes-logging/Ky_OuYqMRkm_918aHRtuLg
[2017-03-02 08:26:56,736][INFO ][cluster.service          ] [elasticsearch-logging-v1-dklui] new_master {elasticsearch-logging-v1-dklui}{Ky_OuYqMRkm_918aHRtuLg}{172.16.57.8}{172.16.57.8:9300}{master=true}, added {{elasticsearch-logging-v1-vjxm3}{cbzgrfZATyWkHfQYHZhs7Q}{172.16.99.10}{172.16.99.10:9300}{master=true},}, reason: zen-disco-join(elected_as_master, [1] joins received)
[2017-03-02 08:26:56,955][INFO ][http                     ] [elasticsearch-logging-v1-dklui] publish_address {172.16.57.8:9200}, bound_addresses {[::]:9200}
[2017-03-02 08:26:56,956][INFO ][node                     ] [elasticsearch-logging-v1-dklui] started
[2017-03-02 08:26:57,157][INFO ][gateway                  ] [elasticsearch-logging-v1-dklui] recovered [0] indices into cluster_state
[2017-03-02 08:27:05,378][INFO ][cluster.metadata         ] [elasticsearch-logging-v1-dklui] [logstash-2017.03.02] creating index, cause [auto(bulk api)], templates [], shards [5]/[1], mappings []
[2017-03-02 08:27:06,360][INFO ][cluster.metadata         ] [elasticsearch-logging-v1-dklui] [logstash-2017.03.01] creating index, cause [auto(bulk api)], templates [], shards [5]/[1], mappings []
[2017-03-02 08:27:07,163][INFO ][cluster.routing.allocation] [elasticsearch-logging-v1-dklui] Cluster health status changed from [RED] to [YELLOW] (reason: [shards started [[logstash-2017.03.01][3], [logstash-2017.03.01][3]] ...]).
[2017-03-02 08:27:07,354][INFO ][cluster.metadata         ] [elasticsearch-logging-v1-dklui] [logstash-2017.03.02] create_mapping [fluentd]
[2017-03-02 08:27:07,988][INFO ][cluster.metadata         ] [elasticsearch-logging-v1-dklui] [logstash-2017.03.01] create_mapping [fluentd]
[2017-03-02 08:27:09,578][INFO ][cluster.routing.allocation] [elasticsearch-logging-v1-dklui] Cluster health status changed from [YELLOW] to [GREEN] (reason: [shards started [[logstash-2017.03.02][4]] ...]).

</code></pre>
<p>elasticsearch logging启动运行ok！</p>
<h3>五、启动kibana</h3>
<p>有了elasticsearch logging的“前车之鉴”，这次我们也把上面新创建的serviceaccount：k8s-efk显式赋值给kibana-controller.yaml:</p>
<pre><code>//kibana-controller.yaml
... ...
spec:
      serviceAccount: k8s-efk
      containers:
      - name: kibana-logging
        image: bigwhite/kibana:v4.6.1-1
        resources:
          # keep request = limit to keep this container in guaranteed class
          limits:
            cpu: 100m
          #requests:
          #  cpu: 100m
        env:
          - name: "ELASTICSEARCH_URL"
            value: "http://elasticsearch-logging:9200"
          - name: "KIBANA_BASE_URL"
            value: "/api/v1/proxy/namespaces/kube-system/services/kibana-logging"
        ports:
        - containerPort: 5601
          name: ui
          protocol: TCP
... ...
</code></pre>
<p>启动kibana，并观察pod日志：</p>
<pre><code># kubectl create -f kibana-controller.yaml
# kubectl create -f kibana-service.yaml
# kubectl logs -f kibana-logging-3604961973-jby53 -n kube-system
ELASTICSEARCH_URL=http://elasticsearch-logging:9200
server.basePath: /api/v1/proxy/namespaces/kube-system/services/kibana-logging
{"type":"log","@timestamp":"2017-03-02T08:30:15Z","tags":["info","optimize"],"pid":6,"message":"Optimizing and caching bundles for kibana and statusPage. This may take a few minutes"}
</code></pre>
<p>kibana缓存着实需要一段时间，请耐心等待！可能是几分钟。之后你将会看到如下日志：</p>
<pre><code># kubectl logs -f kibana-logging-3604961973-jby53 -n kube-system
ELASTICSEARCH_URL=http://elasticsearch-logging:9200
server.basePath: /api/v1/proxy/namespaces/kube-system/services/kibana-logging
{"type":"log","@timestamp":"2017-03-02T08:30:15Z","tags":["info","optimize"],"pid":6,"message":"Optimizing and caching bundles for kibana and statusPage. This may take a few minutes"}
{"type":"log","@timestamp":"2017-03-02T08:40:04Z","tags":["info","optimize"],"pid":6,"message":"Optimization of bundles for kibana and statusPage complete in 588.60 seconds"}
{"type":"log","@timestamp":"2017-03-02T08:40:04Z","tags":["status","plugin:kibana@1.0.0","info"],"pid":6,"state":"green","message":"Status changed from uninitialized to green - Ready","prevState":"uninitialized","prevMsg":"uninitialized"}
{"type":"log","@timestamp":"2017-03-02T08:40:05Z","tags":["status","plugin:elasticsearch@1.0.0","info"],"pid":6,"state":"yellow","message":"Status changed from uninitialized to yellow - Waiting for Elasticsearch","prevState":"uninitialized","prevMsg":"uninitialized"}
{"type":"log","@timestamp":"2017-03-02T08:40:05Z","tags":["status","plugin:kbn_vislib_vis_types@1.0.0","info"],"pid":6,"state":"green","message":"Status changed from uninitialized to green - Ready","prevState":"uninitialized","prevMsg":"uninitialized"}
{"type":"log","@timestamp":"2017-03-02T08:40:05Z","tags":["status","plugin:markdown_vis@1.0.0","info"],"pid":6,"state":"green","message":"Status changed from uninitialized to green - Ready","prevState":"uninitialized","prevMsg":"uninitialized"}
{"type":"log","@timestamp":"2017-03-02T08:40:05Z","tags":["status","plugin:metric_vis@1.0.0","info"],"pid":6,"state":"green","message":"Status changed from uninitialized to green - Ready","prevState":"uninitialized","prevMsg":"uninitialized"}
{"type":"log","@timestamp":"2017-03-02T08:40:06Z","tags":["status","plugin:spyModes@1.0.0","info"],"pid":6,"state":"green","message":"Status changed from uninitialized to green - Ready","prevState":"uninitialized","prevMsg":"uninitialized"}
{"type":"log","@timestamp":"2017-03-02T08:40:06Z","tags":["status","plugin:statusPage@1.0.0","info"],"pid":6,"state":"green","message":"Status changed from uninitialized to green - Ready","prevState":"uninitialized","prevMsg":"uninitialized"}
{"type":"log","@timestamp":"2017-03-02T08:40:06Z","tags":["status","plugin:table_vis@1.0.0","info"],"pid":6,"state":"green","message":"Status changed from uninitialized to green - Ready","prevState":"uninitialized","prevMsg":"uninitialized"}
{"type":"log","@timestamp":"2017-03-02T08:40:06Z","tags":["listening","info"],"pid":6,"message":"Server running at http://0.0.0.0:5601"}
{"type":"log","@timestamp":"2017-03-02T08:40:11Z","tags":["status","plugin:elasticsearch@1.0.0","info"],"pid":6,"state":"yellow","message":"Status changed from yellow to yellow - No existing Kibana index found","prevState":"yellow","prevMsg":"Waiting for Elasticsearch"}
{"type":"log","@timestamp":"2017-03-02T08:40:14Z","tags":["status","plugin:elasticsearch@1.0.0","info"],"pid":6,"state":"green","message":"Status changed from yellow to green - Kibana index ready","prevState":"yellow","prevMsg":"No existing Kibana index found"}
</code></pre>
<p>接下来，通过浏览器访问下面地址就可以访问kibana的web页面了，注意：Kinaba的web页面加载也需要一段时间。</p>
<pre><code>https://{API Server external IP}:{API Server secure port}/api/v1/proxy/namespaces/kube-system/services/kibana-logging/app/kibana#/settings/indices/
</code></pre>
<p>下面是创建一个index（相当于mysql中的一个database）页面：</p>
<p><img src="http://tonybai.com/wp-content/uploads/k8s-kibana-index-create.png" alt="img{512x368}" /></p>
<p>取消“Index contains time-based events”，然后点击“Create”即可创建一个Index。</p>
<p>点击页面上的”Setting” -> “Status”，可以查看当前elasticsearch logging的整体状态，如果一切ok，你将会看到下图这样的页面：</p>
<p><img src="http://tonybai.com/wp-content/uploads/k8s-kibana-status-ok.png" alt="img{512x368}" /></p>
<p>创建Index后，可以在Discover下看到ElasticSearch logging中汇聚的日志：</p>
<p><img src="http://tonybai.com/wp-content/uploads/k8s-kibana-discover.png" alt="img{512x368}" /></p>
<h3>六、小结</h3>
<p>以上就是在Kubernetes 1.3.7集群上安装Fluentd和ElasticSearch stack，实现kubernetes cluster level logging的过程。在<a href="http://tonybai.com/2016/12/30/install-kubernetes-on-ubuntu-with-kubeadm">使用kubeadm安装的Kubernetes 1.5.1环境</a>下安装这些，则基本不会遇到上述这些问题。</p>
<p>另外ElasticSearch logging默认挂载的volume是emptyDir，实验用可以。但要部署在生产环境，必须换成Persistent Volume，比如：<a href="http://tonybai.com/2016/11/07/integrate-kubernetes-with-ceph-rbd">CephRBD</a>。</p>
<p style='text-align:left'>&copy; 2017, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2017/03/03/implement-kubernetes-cluster-level-logging-with-fluentd-and-elasticsearch-stack/feed/</wfw:commentRss>
		<slash:comments>18</slash:comments>
		</item>
		<item>
		<title>Kubernetes Pod无法挂载ceph RBD存储卷的临时解决方法</title>
		<link>https://tonybai.com/2017/02/17/temp-fix-for-pod-unable-mount-cephrbd-volume/</link>
		<comments>https://tonybai.com/2017/02/17/temp-fix-for-pod-unable-mount-cephrbd-volume/#comments</comments>
		<pubDate>Fri, 17 Feb 2017 09:27:58 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Ceph]]></category>
		<category><![CDATA[cephrbd]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[image]]></category>
		<category><![CDATA[k8s]]></category>
		<category><![CDATA[kubectl]]></category>
		<category><![CDATA[kubelet]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[PetSet]]></category>
		<category><![CDATA[pod]]></category>
		<category><![CDATA[PV]]></category>
		<category><![CDATA[PVC]]></category>
		<category><![CDATA[StatefulSet]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[volume]]></category>
		<category><![CDATA[存储卷]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=2181</guid>
		<description><![CDATA[所有涉及到存储的地方都是极易出现“坑”的地方，Kubernetes也不例外。 一、问题起因 问题始于昨天升级一个stateful service的操作。该service下的Pod挂载了使用ceph RBD提供的一个Persistent Volume。该Pod是用普通deployment部署的，并没有使用处于alpha状态的PetSet。改动仅仅是image的版本发生了变化。我执行的操作如下： # kubectl apply -f index-api.yaml 操作是成功的。但命令执行后，再次查看index-api这个Pod的状态，该Pod的状态长期处于：“ContainerCreating”，显然Pod没能重启成功。 进一步通过describe pod 检视events，发现如下Warning: events: FirstSeen LastSeen Count From SubobjectPath Type Reason Message --------- -------- ----- ---- ------------- -------- ------ ------- 2m 2m 1 {default-scheduler } Normal Scheduled Successfully assigned index-api-3362878852-9tm9j to 10.46.181.146 11s 11s 1 {kubelet 10.46.181.146} Warning FailedMount Unable to mount volumes [...]]]></description>
			<content:encoded><![CDATA[<p>所有涉及到存储的地方都是极易出现“坑”的地方，<a href="http://tonybai.com/tag/kubernetes">Kubernetes</a>也不例外。</p>
<h3>一、问题起因</h3>
<p>问题始于昨天升级一个stateful service的操作。该service下的Pod挂载了<a href="http://tonybai.com/2016/11/07/integrate-kubernetes-with-ceph-rbd/">使用ceph RBD提供的一个Persistent Volume</a>。该Pod是用普通<a href="http://tonybai.com/2017/02/09/rolling-update-for-services-in-kubernetes-cluster/">deployment</a>部署的，并没有使用处于alpha状态的<a href="https://kubernetes.io/docs/user-guide/petset/">PetSet</a>。改动仅仅是image的版本发生了变化。我执行的操作如下：</p>
<pre><code># kubectl apply -f index-api.yaml
</code></pre>
<p>操作是成功的。但命令执行后，再次查看index-api这个Pod的状态，该Pod的状态长期处于：“ContainerCreating”，显然Pod没能重启成功。</p>
<p>进一步通过describe pod 检视events，发现如下Warning:</p>
<pre><code>events:
  FirstSeen    LastSeen    Count    From            SubobjectPath    Type        Reason        Message
  ---------    --------    -----    ----            -------------    --------    ------        -------
  2m        2m        1    {default-scheduler }            Normal        Scheduled    Successfully assigned index-api-3362878852-9tm9j to 10.46.181.146
  11s        11s        1    {kubelet 10.46.181.146}            Warning        FailedMount    Unable to mount volumes for pod "index-api-3362878852-9tm9j_default(ad89c829-f40b-11e6-ad11-00163e1625a9)": timeout expired waiting for volumes to attach/mount for pod "index-api-3362878852-9tm9j"/"default". list of unattached/unmounted volumes=[index-api-pv]
  11s        11s        1    {kubelet 10.46.181.146}            Warning        FailedSync    Error syncing pod, skipping: timeout expired waiting for volumes to attach/mount for pod "index-api-3362878852-9tm9j"/"default". list of unattached/unmounted volumes=[index-api-pv]
</code></pre>
<p>index-api这个Pod尝试挂载index-api-pv这个pv超时，并失败。</p>
<h3>二、问题探索和临时解决</h3>
<p>首先查看问题pod所在Node(10.46.181.146)上的<a href="http://tonybai.com/2016/10/18/learn-how-to-install-kubernetes-on-ubuntu/">kubelet日志</a>，kubelet负责与本地的docker engine以及其他本地服务交互：</p>
<pre><code>... ...
I0216 13:59:27.380007    1159 reconciler.go:294] MountVolume operation started for volume "kubernetes.io/rbd/7e6c415a-f40c-11e6-ad11-00163e1625a9-index-api-pv" (spec.Name: "index-api-pv") to pod "7e6c415a-f40c-11e6-ad11-00163e1625a9" (UID: "7e6c415a-f40c-11e6-ad11-00163e1625a9").
E0216 13:59:27.393946    1159 disk_manager.go:56] failed to attach disk
E0216 13:59:27.394013    1159 rbd.go:228] rbd: failed to setup mount /var/lib/kubelet/pods/7e6c415a-f40c-11e6-ad11-00163e1625a9/volumes/kubernetes.io~rbd/index-api-pv rbd: image index-api-image is locked by other nodes
E0216 13:59:27.394121    1159 nestedpendingoperations.go:254] Operation for "\"kubernetes.io/rbd/7e6c415a-f40c-11e6-ad11-00163e1625a9-index-api-pv\" (\"7e6c415a-f40c-11e6-ad11-00163e1625a9\")" failed. No retries permitted until 2017-02-16 14:01:27.394076217 +0800 CST (durationBeforeRetry 2m0s). Error: MountVolume.SetUp failed for volume "kubernetes.io/rbd/7e6c415a-f40c-11e6-ad11-00163e1625a9-index-api-pv" (spec.Name: "index-api-pv") pod "7e6c415a-f40c-11e6-ad11-00163e1625a9" (UID: "7e6c415a-f40c-11e6-ad11-00163e1625a9") with: rbd: image index-api-image is locked by other nodes
E0216 13:59:32.695919    1159 kubelet.go:1958] Unable to mount volumes for pod "index-api-3362878852-pzxm8_default(7e6c415a-f40c-11e6-ad11-00163e1625a9)": timeout expired waiting for volumes to attach/mount for pod "index-api-3362878852-pzxm8"/"default". list of unattached/unmounted volumes=[index-api-pv]; skipping pod
E0216 13:59:32.696223    1159 pod_workers.go:183] Error syncing pod 7e6c415a-f40c-11e6-ad11-00163e1625a9, skipping: timeout expired waiting for volumes to attach/mount for pod "index-api-3362878852-pzxm8"/"default". list of unattached/unmounted volumes=[index-api-pv]
... ...

</code></pre>
<p>通过kubelet的日志我们可以看出调度到10.46.181.146这个Node上的index-api pod之所以无法挂载<a href="http://tonybai.com/2016/11/21/kuberize-ceph-rbd-api-service/">ceph RBD</a> volume，是因为index-api-image已经被其他node锁住。</p>
<p>我的这个小集群一共就只有两个Node(10.46.181.146和10.47.136.60)，那锁住index-api-image的就是10.47.136.60这个node了。我们查看一下平台上pv和pvc的状态：</p>
<pre><code># kubectl get pv
NAME           CAPACITY   ACCESSMODES   RECLAIMPOLICY   STATUS    CLAIM                   REASON    AGE
ceph-pv        1Gi        RWO           Recycle         Bound     default/ceph-claim                101d
index-api-pv   2Gi        RWO           Recycle         Bound     default/index-api-pvc             49d

# kubectl get pvc
NAME            STATUS    VOLUME         CAPACITY   ACCESSMODES   AGE
ceph-claim      Bound     ceph-pv        1Gi        RWO           101d
index-api-pvc   Bound     index-api-pv   2Gi        RWO           49d

</code></pre>
<p>index-api-pv和index-api-pvc的状态都是正常的，从这里看不出lock的情况。无奈我只能从<a href="http://tonybai.com/2016/11/09/operate-ceph-rbd-images-with-go-ceph/">ceph</a>这个层面去查问题了！</p>
<p>index-api-image在mioss pool下面，我们利用ceph的rbd cli工具查看一下其状态：</p>
<pre><code># rbd ls mioss
index-api-image

# rbd info mioss/index-api-image
rbd image 'index-api-image':
    size 2048 MB in 512 objects
    order 22 (4096 kB objects)
    block_name_prefix: rb.0.5e36.1befd79f
    format: 1

# rbd disk-usage mioss/index-api-image
warning: fast-diff map is not enabled for index-api-image. operation may be slow.
NAME            PROVISIONED USED
index-api-image       2048M 168M
</code></pre>
<p>index-api-image状态ok。</p>
<p>如果你在执行rbd时，出现下面错误：</p>
<pre><code># rbd
rbd: error while loading shared libraries: /usr/lib/x86_64-linux-gnu/libicudata.so.52: invalid ELF header
</code></pre>
<p>可以通过重装libicu52这个包(这里演示的是基于ubuntu 14.04 amd64的版本)来解决：</p>
<pre><code># wget -c http://security.ubuntu.com/ubuntu/pool/main/i/icu/libicu52_52.1-3ubuntu0.4_amd64.deb
# dpkg -i ./libicu52_52.1-3ubuntu0.4_amd64.deb
</code></pre>
<p>回归正题！</p>
<p>经查manual发现，rbd提供了lock相关子命令可以查看image的lock list：</p>
<pre><code># rbd lock list  mioss/index-api-image
There is 1 exclusive lock on this image.
Locker       ID                       Address
client.24128 kubelet_lock_magic_node1 10.47.136.60:0/1864102866
</code></pre>
<p>真凶找到！我们看到位于10.47.136.60 node上有一个locker将该image锁住。我尝试重启10.47.136.60上的kubelet，发现重启后，lock依旧。</p>
<p>怎么取消这个锁呢？rbd不光提供了lock list命令，还提供了lock remove命令：</p>
<pre><code>lock remove (lock rm)       Release a lock on an image

usage:
      lock remove image-spec lock-id locker
              Release a lock on an image. The lock id and locker are as output by lock ls.
</code></pre>
<p>开始解锁：</p>
<pre><code># rbd lock remove  mioss/index-api-image   kubelet_lock_magic_node1 client.24128
</code></pre>
<p>解锁成功后，delete掉那个处于ContainerCreating的Pod，然后index-api pod就启动成功了：</p>
<pre><code>NAMESPACE                    NAME                                    READY     STATUS    RESTARTS   AGE       IP             NODE            LABELS
default                      index-api-3362878852-m6k0j              1/1       Running   0          10s       172.16.57.7    10.46.181.146   app=index-api,pod-template-hash=3362878852
</code></pre>
<h3>三、问题简要分析</h3>
<p>从问题现象来看，起因是由于index-api pod被从10.47.136.60这个node调度到 10.46.181.146这个node上而导致的。但是为什么image的lock没有释放的确怪异，因为我的index-api是捕捉pod退回信号，支持优雅退出的：</p>
<pre><code># kubectl delete -f index-api-deployment.yaml
deployment "index-api" deleted

2017/02/16 08:41:27 1 Received SIGTERM.
2017/02/16 08:41:27 1 [::]:30080 Listener closed.
2017/02/16 08:41:27 1 Waiting for connections to finish...
2017/02/16 08:41:27 [C] [asm_amd64.s:2086] ListenAndServe:  accept tcp [::]:30080: use of closed network connection 1
2017/02/16 08:41:27 [I] [engine.go:109] engine[mioss1(online)]: mioss1-29583fe44a637eabe4f865bc59bde44fa307e38e exit!
2017/02/16 08:41:27 [I] [engine.go:109] engine[wx81f621e486239f6b(online)]: wx81f621e486239f6b-58b5643015a5f337931aaa4a5f4db1b35ac784bb exit!
2017/02/16 08:41:27 [I] [engine.go:109] engine[wxa4d49c280cefd38c(online)]: wxa4d49c280cefd38c-f38959408617862ed69dab9ad04403cee9564353 exit!
2017/02/16 08:41:27 [D] [enginemgr.go:310] Search Engines exit ok
</code></pre>
<p>因此，初步猜测：这里很可能是kubernetes在监视和处理pod退出时，对于存储插件的状态处理存在一些bug，至于具体什么问题，还不得而知。</p>
<h3>四、小结</h3>
<p>对于像index-api service这样的stateful服务来说，使用普通deployment显然不能满足要求。Kubernetes在[1.3.0, 1.5.0)版本区间提供了处于alpha状态的<a href="https://kubernetes.io/docs/user-guide/petset/">PetSet</a> controller，在<a href="http://tonybai.com/2016/12/30/install-kubernetes-on-ubuntu-with-kubeadm/">1.5.0版本</a>后，PetSet被改名为<a href="https://kubernetes.io/docs/concepts/abstractions/controllers/statefulsets/">StatefulSet</a>。与普通Pod不同，PetSet下面的每个Pet都有严格的身份属性，并根据身份属性绑定一定资源，并且不会像普通Pod那样被Kubernetes随意调度到任意Node上。</p>
<p>像index-api-service<a href="http://tonybai.com/2016/12/06/an-intro-to-wukong-fulltext-search-engine/">索引服务</a>这样的一个实例绑定一个cephRBD pv的应用，特别适合使用PetSet或StatefulSet，不过我这里尚未测试用上PetSet后是否还会出现无法挂载rbd卷的问题。</p>
<hr />
<p>微博：<a href="http://weibo.com/bigwhite20xx">@tonybai_cn</a><br />
微信公众号：iamtonybai<br />
github.com账号: https://github.com/bigwhite</p>
<p style='text-align:left'>&copy; 2017, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2017/02/17/temp-fix-for-pod-unable-mount-cephrbd-volume/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>使用Kubeadm安装Kubernetes-Part2</title>
		<link>https://tonybai.com/2016/12/30/install-kubernetes-on-ubuntu-with-kubeadm-2/</link>
		<comments>https://tonybai.com/2016/12/30/install-kubernetes-on-ubuntu-with-kubeadm-2/#comments</comments>
		<pubDate>Fri, 30 Dec 2016 08:48:26 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[aliyun]]></category>
		<category><![CDATA[Apt-get]]></category>
		<category><![CDATA[BGP]]></category>
		<category><![CDATA[brctl]]></category>
		<category><![CDATA[calico]]></category>
		<category><![CDATA[centos]]></category>
		<category><![CDATA[Ceph]]></category>
		<category><![CDATA[cephrbd]]></category>
		<category><![CDATA[cluster]]></category>
		<category><![CDATA[cni]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[ECS]]></category>
		<category><![CDATA[flannel]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[HA]]></category>
		<category><![CDATA[ipset]]></category>
		<category><![CDATA[k8s]]></category>
		<category><![CDATA[kube-apiserver]]></category>
		<category><![CDATA[kubeadm]]></category>
		<category><![CDATA[kubectl]]></category>
		<category><![CDATA[kubelet]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[Master]]></category>
		<category><![CDATA[minion]]></category>
		<category><![CDATA[nginx]]></category>
		<category><![CDATA[nodeport]]></category>
		<category><![CDATA[overlaynetwork]]></category>
		<category><![CDATA[panic]]></category>
		<category><![CDATA[registry]]></category>
		<category><![CDATA[route]]></category>
		<category><![CDATA[swarm]]></category>
		<category><![CDATA[swarmkit]]></category>
		<category><![CDATA[systemd]]></category>
		<category><![CDATA[troubleshooting]]></category>
		<category><![CDATA[tunnel]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[UDP]]></category>
		<category><![CDATA[vip]]></category>
		<category><![CDATA[VXLAN]]></category>
		<category><![CDATA[weave]]></category>
		<category><![CDATA[内核]]></category>
		<category><![CDATA[服务编排]]></category>
		<category><![CDATA[集群]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=2169</guid>
		<description><![CDATA[此文为《使用Kubeadm安装Kubernetes》的第二部分。文章第一部分在这里可以看到。 五、weave network for pod 经过上面那么多次尝试，结果是令人扫兴的。Weave network似乎是最后一颗救命稻草了。有了前面的铺垫，这里就不详细列出各种命令的输出细节了。Weave network也有专门的官方文档用于指导如何与kubernetes集群集成，我们主要也是参考它。 1、安装weave network add-on 在kubeadm reset后，我们重新初始化了集群。接下来我们安装weave network add-on： # kubectl apply -f https://git.io/weave-kube daemonset "weave-net" created 前面无论是Flannel还是calico，在安装pod network add-on时至少都还是顺利的。不过在Weave network这次，我们遭遇“当头棒喝”:(: # kubectl get pod --all-namespaces -o wide NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE kube-system dummy-2088944543-4kxtk 1/1 Running 0 42m 10.47.217.91 iz25beglnhtz kube-system etcd-iz25beglnhtz 1/1 Running 0 [...]]]></description>
			<content:encoded><![CDATA[<p>此文为《使用Kubeadm安装Kubernetes》的第二部分。文章第一部分在<a href="http://tonybai.com/2016/12/30/install-kubernetes-on-ubuntu-with-kubeadm/">这里</a>可以看到。</p>
<h3>五、weave network for pod</h3>
<p>经过上面那么多次尝试，结果是令人扫兴的。Weave network似乎是最后一颗救命稻草了。有了前面的铺垫，这里就不详细列出各种命令的输出细节了。Weave network也有<a href="https://www.weave.works/docs/net/latest/kube-addon/">专门的官方文档</a>用于指导如何与kubernetes集群集成，我们主要也是参考它。</p>
<h4>1、安装weave network add-on</h4>
<p>在kubeadm reset后，我们重新初始化了集群。接下来我们安装weave network add-on：</p>
<pre><code># kubectl apply -f https://git.io/weave-kube
daemonset "weave-net" created
</code></pre>
<p>前面无论是Flannel还是calico，在安装pod network add-on时至少都还是顺利的。不过在Weave network这次，我们遭遇“当头棒喝”:(:</p>
<pre><code># kubectl get pod --all-namespaces -o wide
NAMESPACE     NAME                                   READY     STATUS              RESTARTS   AGE       IP             NODE
kube-system   dummy-2088944543-4kxtk                 1/1       Running             0          42m       10.47.217.91   iz25beglnhtz
kube-system   etcd-iz25beglnhtz                      1/1       Running             0          42m       10.47.217.91   iz25beglnhtz
kube-system   kube-apiserver-iz25beglnhtz            1/1       Running             0          42m       10.47.217.91   iz25beglnhtz
kube-system   kube-controller-manager-iz25beglnhtz   1/1       Running             0          42m       10.47.217.91   iz25beglnhtz
kube-system   kube-discovery-1769846148-pzv8p        1/1       Running             0          42m       10.47.217.91   iz25beglnhtz
kube-system   kube-dns-2924299975-09dcb              0/4       ContainerCreating   0          42m       &lt;none&gt;         iz25beglnhtz
kube-system   kube-proxy-z465f                       1/1       Running             0          42m       10.47.217.91   iz25beglnhtz
kube-system   kube-scheduler-iz25beglnhtz            1/1       Running             0          42m       10.47.217.91   iz25beglnhtz
kube-system   weave-net-3wk9h                        0/2       CrashLoopBackOff    16         17m       10.47.217.91   iz25beglnhtz
</code></pre>
<p>安装后，weave-net pod提示:CrashLoopBackOff。追踪其Container log，得到如下错误信息：</p>
<pre><code># docker logs cde899efa0af
time="2016-12-28T08:25:29Z" level=info msg="Starting Weaveworks NPC 1.8.2"
time="2016-12-28T08:25:29Z" level=info msg="Serving /metrics on :6781"
Wed Dec 28 08:25:29 2016 &lt;5&gt; ulogd.c:843 building new pluginstance stack: 'log1:NFLOG,base1:BASE,pcap1:PCAP'
time="2016-12-28T08:25:29Z" level=fatal msg="ipset [destroy] failed: ipset v6.29: Set cannot be destroyed: it is in use by a kernel component\n: exit status 1"
</code></pre>
<h4>2、解决ipset destroy错误</h4>
<p>从上述的错误日志来看，似乎某些内核组件占用了一些IP资源，没有释放。ipset(administration tool for IP sets)这个工具以前从来没有接触过。在node上利用apt-get install 一个ipset工具，手工执行以下命令：</p>
<pre><code># ipset destroy
ipset v6.29: Set cannot be destroyed: it is in use by a kernel component

</code></pre>
<p>这个错误输出与container中的error log一模一样。试着用ipset看看哪些ip资源没有释放，这一招让我们看到了蛛丝马迹：</p>
<p>在minion node上执行：</p>
<pre><code># ipset list
Name: felix-calico-hosts-4
Type: hash:ip
Revision: 4
Header: family inet hashsize 1024 maxelem 1048576
Size in memory: 224
References: 1
Members:
123.56.200.187
59.110.67.15

Name: felix-all-ipam-pools
Type: hash:net
Revision: 6
Header: family inet hashsize 1024 maxelem 1048576
Size in memory: 448
References: 1
Members:
192.168.0.0/16

Name: felix-masq-ipam-pools
Type: hash:net
Revision: 6
Header: family inet hashsize 1024 maxelem 1048576
Size in memory: 448
References: 1
Members:
192.168.0.0/16
</code></pre>
<p>我们看到了calico字样。原来是calico的“残留势力”在作祟啊。进一步我们发现calico创建的一个network device依旧存在于两个Node上：</p>
<pre><code>47: tunl0@NONE: &lt;NOARP,UP,LOWER_UP&gt; mtu 1440 qdisc noqueue state UNKNOWN group default qlen 1
    link/ipip 0.0.0.0 brd 0.0.0.0
    inet 192.168.91.0/32 scope global tunl0
       valid_lft forever preferred_lft forever
</code></pre>
<p>我们试图删除它，但最终都以失败告终：</p>
<pre><code># ip tunnel show
tunl0: ip/ip  remote any  local any  ttl inherit  nopmtudisc

 #ip tunnel del tunl0
delete tunnel "tunl0" failed: Operation not permitted

</code></pre>
<p>无奈只能把它down掉：</p>
<pre><code>#ip -f inet addr delete 192.168.91.0/32  dev tunl0

47: tunl0@NONE: &lt;NOARP,UP,LOWER_UP&gt; mtu 1440 qdisc noqueue state UNKNOWN group default qlen 1
    link/ipip 0.0.0.0 brd 0.0.0.0

# ifconfig tunl0 down

47: tunl0@NONE: &lt;NOARP&gt; mtu 1440 qdisc noqueue state DOWN group default qlen 1
    link/ipip 0.0.0.0 brd 0.0.0.0
</code></pre>
<p>但依旧无法删除它。我们通过ipset del命令将上面ipset占用的ip entry逐个删除掉（比如ipset del felix-calico-hosts-4  123.56.200.187）。但即便全部清空，ipset destroy依然失败。</p>
<p>无奈之下，决定重启一下两个Node试试。重启后，calico创建的这个tunnel居然消失了。</p>
<h4>3、再遇路由冲突错误</h4>
<p>重启ECS实例后，我们重新从头来创建cluster。不过在执行“kubectl apply -f https://git.io/weave-kube” 后我们发现weave-net pod依旧没有起来，这次的错误是“路有冲突”：</p>
<pre><code>#docker logs 80383071f721
Network 10.32.0.0/12 overlaps with existing route 10.0.0.0/8 on host.
</code></pre>
<p>查看当前路由表：</p>
<pre><code>netstat -rn
Kernel IP routing table
Destination     Gateway         Genmask         Flags   MSS Window  irtt Iface
0.0.0.0         123.56.203.247  0.0.0.0         UG        0 0          0 eth1
10.0.0.0        10.47.223.247   255.0.0.0       UG        0 0          0 eth0
10.47.216.0     0.0.0.0         255.255.248.0   U         0 0          0 eth0
100.64.0.0      10.47.223.247   255.192.0.0     UG        0 0          0 eth0
123.56.200.0    0.0.0.0         255.255.252.0   U         0 0          0 eth1
172.16.0.0      10.47.223.247   255.240.0.0     UG        0 0          0 eth0
192.168.0.0     0.0.0.0         255.255.240.0   U         0 0          0 docker0
</code></pre>
<p>的确weave-net默认要使用的 10.32.0.0/12与 10.0.0.0/8 存在交集。对此，weave net官方是给出<a href=":https://www.weave.works/documentation/net-latest-using-weave/net-latest-configuring-weave/">解决方案</a>了的。</p>
<p>我们先将https://git.io/weave-kube对应的yaml文件下载到本地：weave-daemonset.yaml。修改该文件，为container增加IPALLOC_RANGE环境变量：</p>
<pre><code>containers:
        - name: weave
          env:
            - name: IPALLOC_RANGE
              value: 172.30.0.0/16

</code></pre>
<p>更新weave net pod：</p>
<pre><code># kubectl delete -f weave-daemonset.yaml
daemonset "weave-net" deleted

# kubectl apply -f weave-daemonset.yaml
daemonset "weave-net" created
</code></pre>
<p>不过依然存在路有冲突。原来路由表里已经存在了一条这样的路由：</p>
<pre><code>172.16.0.0      10.28.63.247    255.240.0.0     UG    0      0        0 eth0
</code></pre>
<p>这条路由应该没有什么用，也许是之前折腾时被某个network addon加进去的。于是用route命令将其删除：</p>
<pre><code># route del -net 172.16.0.0 netmask 255.240.0.0 gw 10.28.63.247
</code></pre>
<p>再次更新weave net pod并查看cluster status：</p>
<pre><code># kubectl delete -f weave-daemonset.yaml
daemonset "weave-net" deleted

# kubectl apply -f weave-daemonset.yaml
daemonset "weave-net" created

# kubectl get pods --all-namespaces -o wide
NAMESPACE     NAME                                   READY     STATUS    RESTARTS   AGE       IP             NODE
kube-system   dummy-2088944543-93f4c                 1/1       Running   0          21m       10.47.217.91   iz25beglnhtz
kube-system   etcd-iz25beglnhtz                      1/1       Running   0          21m       10.47.217.91   iz25beglnhtz
kube-system   kube-apiserver-iz25beglnhtz            1/1       Running   0          20m       10.47.217.91   iz25beglnhtz
kube-system   kube-controller-manager-iz25beglnhtz   1/1       Running   0          21m       10.47.217.91   iz25beglnhtz
kube-system   kube-discovery-1769846148-wbc7h        1/1       Running   0          21m       10.47.217.91   iz25beglnhtz
kube-system   kube-dns-2924299975-206tg              4/4       Running   0          21m       172.30.0.2     iz25beglnhtz
kube-system   kube-proxy-n2xmf                       1/1       Running   0          21m       10.47.217.91   iz25beglnhtz
kube-system   kube-scheduler-iz25beglnhtz            1/1       Running   0          20m       10.47.217.91   iz25beglnhtz
kube-system   weave-net-h38k5                        2/2       Running   0          18s       10.47.217.91   iz25beglnhtz

</code></pre>
<p>这回weave-net pod running了。taint master node并且minion node join后cluster依旧是ok的：</p>
<pre><code># kubectl get pods --all-namespaces -o wide
NAMESPACE     NAME                                   READY     STATUS    RESTARTS   AGE       IP             NODE
kube-system   dummy-2088944543-93f4c                 1/1       Running   0          23m       10.47.217.91   iz25beglnhtz
kube-system   etcd-iz25beglnhtz                      1/1       Running   0          23m       10.47.217.91   iz25beglnhtz
kube-system   kube-apiserver-iz25beglnhtz            1/1       Running   0          22m       10.47.217.91   iz25beglnhtz
kube-system   kube-controller-manager-iz25beglnhtz   1/1       Running   0          23m       10.47.217.91   iz25beglnhtz
kube-system   kube-discovery-1769846148-wbc7h        1/1       Running   0          23m       10.47.217.91   iz25beglnhtz
kube-system   kube-dns-2924299975-206tg              4/4       Running   0          23m       172.30.0.2     iz25beglnhtz
kube-system   kube-proxy-377zh                       1/1       Running   0          8s        10.28.61.30    iz2ze39jeyizepdxhwqci6z
kube-system   kube-proxy-n2xmf                       1/1       Running   0          23m       10.47.217.91   iz25beglnhtz
kube-system   kube-scheduler-iz25beglnhtz            1/1       Running   0          22m       10.47.217.91   iz25beglnhtz
kube-system   weave-net-9tf1d                        2/2       Running   0          8s        10.28.61.30    iz2ze39jeyizepdxhwqci6z
kube-system   weave-net-h38k5                        2/2       Running   0          2m        10.47.217.91   iz25beglnhtz
</code></pre>
<h4>4、测试weave net跨节点pod连通性</h4>
<p>这回我们依旧启动my-nginx service，在任意一个节点curl localhost:30062，我们发现被调度到minion node上的my-nginx container也收到了request并成功回复response：</p>
<pre><code>172.30.0.1 - - [30/Dec/2016:03:14:47 +0000] "GET / HTTP/1.1" 200 612 "-" "curl/7.47.0" "-"
</code></pre>
<p>Weave net初步测试ok！</p>
<h3>六、小结</h3>
<p>虽然过程坎坷，但最终在Weave net的帮助下，我们还是初步调通了一个使用kubeadm安装的kubernetes cluster。后来我发现，在K8s官方博客中有一篇名为《<a href="http://blog.kubernetes.io/2016/09/how-we-made-kubernetes-easy-to-install.html">Kubernetes: How we made Kubernetes insanely easy to install</a>》的文章，其使用的pod network add-on也是weave network。</p>
<p>这是一个试验环境。后续我们还是要进一步探究如何用上Flannel的。同时，Kubernetes 1.5带来的<a href="http://blog.kubernetes.io/2016/12/five-days-of-kubernetes-1.5.html">诸多新特性</a>，比如：Master HA等还需要进一步试验证明。</p>
<p>为了满足我们的production环境要求，之前实践的<a href="http://tonybai.com/2016/11/07/integrate-kubernetes-with-ceph-rbd">Ceph RBD为K8s提供存储卷</a>、<a href="http://tonybai.com/2016/11/16/how-to-pull-images-from-private-registry-on-kubernetes-cluster">k8s从private registry拉取image</a>、<a href="http://tonybai.com/2016/11/25/the-security-settings-for-kubernetes-cluster">k8s集群的安全配置</a>等还要在新集群上进一步试验，直到满足我们的要求。</p>
<p style='text-align:left'>&copy; 2016 &#8211; 2017, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2016/12/30/install-kubernetes-on-ubuntu-with-kubeadm-2/feed/</wfw:commentRss>
		<slash:comments>4</slash:comments>
		</item>
	</channel>
</rss>
