Kubernetes-云原生-DevOps-指南-全-
Kubernetes 云原生 DevOps 指南(全)
原文:
zh.annas-archive.org/md5/1bac09deb5de2aa098be99f84313e92b译者:飞龙
第二版前言
我很幸运早期接触了 Kubernetes(和容器)。在领导精品咨询公司的 DevOps 实践时,我看到容器为我们的许多客户带来巨大好处的潜力。这种兴奋和兴趣促使我加入了 Docker Inc.,在那里我第一手见证了新兴云原生世界中正在形成的一些令人难以置信的创新。
Kubernetes 开始流行起来,我转投到 Heptio(由 Google 项目创始人创立,后被 VMware 收购),专注于帮助客户和社区成功学习和部署它。因此,有时我会忘记许多人现在才刚开始探索和采用这些技术及其功能。
最近,我与客户合作,演示 Kubernetes 自动配置云负载均衡器、注册适当的 DNS 名称,并为他们的应用程序附加相关的 TLS 证书,使其可以公开访问。“太酷了!”这是他们对我们成功的反应,完美地捕捉到了我首次发现和学习 Kubernetes 时的感受。不过,就像几乎所有先进技术一样,它并非一帆风顺。
Kubernetes 经常受到的批评之一是它复杂,这在我看来至少似乎带有负面含义。我不同意,并更愿意将其描述为必要的复杂性。Kubernetes 有许多组件相互协作,构建一个能够在最高规模下提供弹性、效率和可扩展性的应用平台。它封装了共享知识,并节省了我们重新实现其提供的许多常见功能的时间和精力。
然而,作为新用户,选择从何处着手可能令人望而却步,尤其是在如此庞大的功能阵列中,更不用说广泛的云原生生态系统中存在的大量关联工具了。因此,我最喜欢《Cloud Native DevOps with Kubernetes》的一点是作者假定读者没有先前的知识。这确实是我在刚开始时希望有的一本书,在面对我面前广阔可能性时既敬畏又困惑!
一旦你翻开书页,你将首先了解 DevOps 和 Kubernetes 背后的历史和文化背景,然后介绍易于理解且与在实际场景中实施这些技术直接相关的实际示例。因此,虽然某些标题更像是参考书,但我建议您按顺序阅读本书。
贾斯汀和约翰在书中的叙述构建在前人基础之上,做得非常好。新概念以逐层方式展开,使你能够随着进展“探索”它们更深入。尽管如此,在完成本书后,它绝对可以作为关键日常概念的简明参考,你会发现自己一次又一次地求助于它们。
凭借实际示例和务实的业务建议的均衡混合,我经常推荐这本书作为"一站式"指南,以帮助架构师和工程师掌握他们在理解云原生环境和开始部署成功的 Kubernetes 应用平台过程中所需的知识。
《Cloud Native DevOps with Kubernetes》的第一版发布已经超过三年了,在云原生技术领域,这几乎是一个世纪。不仅现有技术和范式发生了演变(在某些情况下被废弃),而且还出现了新技术。在第一版的基础上,贾斯汀再次运用他丰富的实际经验,增强了原始指导,同时保持了建议的合理性、覆盖的广度和实际示例的精彩结合。
我的经验告诉我,并不存在所谓的“最佳实践”,每种情况都受到微妙约束的驱动,这可能会带来挑战。贾斯汀在与这些工具的日常实践中积累的经验在每一节中都闪耀出来,并将帮助你在选择要采纳的东西以及组织和/或用例的最佳实现方式时,应对不可避免的权衡和艰难决策。
如果你正在阅读这本书,我假设你在 DevOps 和 Kubernetes 旅程的开端。让我祝贺你迈出了第一步;你将迎来一段充满回报和激动人心的旅程!
约翰·哈里斯
Kong Inc.首席现场工程师
《Production Kubernetes》合著者(O’Reilly,2021 年)
西雅图,2022 年 2 月
第一版序言
欢迎来到使用 Kubernetes 进行云原生 DevOps。
Kubernetes 是一个真正的行业革命。只需简要查看云原生计算基金会的Landscape,其中包含了今天云原生世界中超过六百个项目的数据,就可以突显出 Kubernetes 这些日子的重要性。并非所有这些工具都是为 Kubernetes 开发的,甚至其中的一些工具也不能与 Kubernetes 一起使用,但它们都是巨大生态系统的一部分,其中 Kubernetes 是旗舰技术之一。
Kubernetes 改变了应用程序开发和运维的方式。它是当今 DevOps 世界的核心组件。Kubernetes 为开发人员带来了灵活性,为运维人员带来了自由。今天,你可以在任何主要的云服务提供商、裸金属本地环境以及本地开发者的机器上使用 Kubernetes。稳定性、灵活性、强大的 API、开放代码和开放的开发者社区是 Kubernetes 成为行业标准的几个原因,就像 Linux 在操作系统世界中是标准一样。
使用 Kubernetes 进行云原生 DevOps 是一本对于那些正在使用 Kubernetes 进行日常工作或刚刚开始 Kubernetes 之旅的人们来说非常好的手册。John 和 Justin 涵盖了部署、配置和操作 Kubernetes 的所有主要方面,以及在其上开发和运行应用程序的最佳实践。他们还对相关技术,包括 Prometheus、Helm 和持续部署,进行了全面的概述。对于 DevOps 世界的每个人来说,这是一本必读的书籍。
Kubernetes 不仅仅是又一个令人激动的工具;它是一个行业标准,也是下一代技术的基础,包括无服务器(OpenFaaS、Knative)和机器学习(Kubeflow)工具。整个 IT 行业因为云原生革命而改变,能够亲历这一切是非常激动人心的。
Ihor Dvoretskyi
云原生计算基金会开发者倡导者
2018 年 12 月
序言
在 IT 运维领域,DevOps 的关键原则已被广泛理解和采纳,但现在景观正在发生变化。一个名为 Kubernetes 的新应用平台迅速被全球各地各行各业的公司广泛采纳。随着越来越多的应用程序和业务从传统服务器迁移到 Kubernetes 环境,人们开始思考如何在这个新世界中实施 DevOps。
本书解释了在 Kubernetes 成为标准平台的云原生世界中,DevOps 的含义。它将帮助您从 Kubernetes 生态系统中选择最佳工具和框架。它还将呈现一种连贯的方式来使用这些工具和框架,提供在生产环境中实际运行的经过实战检验的解决方案。
我会学到什么?
你将了解 Kubernetes 是什么,它的起源是什么,以及它对软件开发和运维的未来意味着什么。你将学习容器的工作原理,如何构建和管理它们,以及如何设计云原生服务和基础设施。
你将理解自行构建和托管 Kubernetes 集群之间的权衡,以及使用托管服务的区别。你将了解到流行的 Kubernetes 安装工具如 kops 和 kubeadm 的能力、限制以及优缺点。你将详细了解来自亚马逊、谷歌和微软等公司的主要托管 Kubernetes 解决方案。
你将获得实际动手写和部署 Kubernetes 应用程序的经验,配置和操作 Kubernetes 集群,并使用 Helm 等工具自动化云基础设施和部署。您将学习 Kubernetes 对安全性、认证和权限的支持,包括基于角色的访问控制(RBAC)以及在生产环境中保护容器和 Kubernetes 的最佳实践。
你将学习如何使用 Kubernetes 设置持续集成和部署;了解 GitOps 的一些基本概念;如何进行数据备份和恢复;如何测试集群的符合性和可靠性;如何监控、追踪、记录和聚合指标;以及如何使您的 Kubernetes 基础设施具备可扩展性、弹性和成本效益。
为了演示我们所讨论的所有内容,我们将它们应用于一个非常简单的演示应用程序。您可以使用我们 Git 仓库中的代码跟随我们的所有示例。
这本书适合谁?
本书最直接适用于负责服务器、应用程序和服务的 IT 运维人员,以及负责构建新的云原生服务或将现有应用迁移到 Kubernetes 和云平台的开发人员。我们假设您对 Kubernetes 或容器没有任何先验知识 —— 不用担心,我们会逐步引导您。
即使是经验丰富的 Kubernetes 用户,在本书中仍然会找到许多有价值的内容:它涵盖了诸如 RBAC、持续部署、秘密管理和可观察性等高级主题。无论您的专业水平如何,我们希望您在这些页面中找到一些有用的东西。
这本书回答了哪些问题?
在计划和撰写本书时,我们与数百人讨论了云原生和 Kubernetes,包括行业领袖、专家和完全初学者。以下是他们表示希望本书解答的一些问题:
-
“我想知道为什么我应该投入时间学习这项技术。它将帮助我和我的团队解决什么问题?”
-
“Kubernetes 看起来很棒,但学习曲线相当陡峭。快速设置演示很容易,但是操作和故障排除似乎令人望而生畏。我们希望得到关于在现实世界中运行 Kubernetes 集群以及我们可能遇到的问题的坚实指导。”
-
“有主观建议会很有用。Kubernetes 生态系统有太多选项供初学团队选择。当存在多种完成同一任务的方法时,哪一种最好?我们如何选择?”
或许最重要的问题是:
- “我如何在不破坏公司的情况下使用 Kubernetes?”
在撰写本书时,我们牢记了这些问题,以及许多其他问题,并尽力回答了它们。我们的表现如何?继续阅读以了解答案。
本书中使用的约定
本书使用以下排版约定:
斜体
表示新术语、网址、电子邮件地址、文件名和文件扩展名。
等宽
用于程序清单,以及在段落内用于引用程序元素,例如变量或函数名称、数据库、数据类型、环境变量、语句和关键字。
等宽粗体
显示用户应按照字面输入的命令或其他文本。
等宽斜体
显示应由用户提供值或由上下文确定值替换的文本。
提示
这个元素表示一个提示或建议。
注
这个元素表示一般注释。
警告
这个元素表示警告或注意事项。
使用代码示例
附加材料(代码示例、练习等)可在https://github.com/cloudnativedevops/demo下载。
如果您有技术问题或在使用代码示例时遇到问题,请发送电子邮件至bookquestions@oreilly.com。
This book is here to help you get your job done. In general, if example code is offered with this book, you may use it in your programs and documentation. You do not need to contact us for permission unless you’re reproducing a significant portion of the code. For example, writing a program that uses several chunks of code from this book does not require permission. Selling or distributing examples from O’Reilly books does require permission. Answering a question by citing this book and quoting example code does not require permission. Incorporating a significant amount of example code from this book into your product’s documentation does require permission.
We appreciate, but do not require, attribution. An attribution usually includes the title, author, publisher, and ISBN. For example: “Cloud Native DevOps with Kubernetes by Justin Domingus and John Arundel (O’Reilly). Copyright 2022 John Arundel and Justin Domingus, 978-1-492-04076-7.”
If you feel your use of code examples falls outside fair use or the permission given above, feel free to contact us at permissions@oreilly.com.
O’Reilly Online Learning
Note
For more than 40 years, O’Reilly Media has provided technology and business training, knowledge, and insight to help companies succeed.
Our unique network of experts and innovators share their knowledge and expertise through books, articles, and our online learning platform. O’Reilly’s online learning platform gives you on-demand access to live training courses, in-depth learning paths, interactive coding environments, and a vast collection of text and video from O’Reilly and 200+ other publishers. For more information, visit http://oreilly.com.
How to Contact Us
Please address comments and questions concerning this book to the publisher:
-
O’Reilly Media, Inc.
-
1005 Gravenstein Highway North
-
Sebastopol, CA 95472
-
800-998-9938 (in the United States or Canada)
-
707-829-0515 (international or local)
-
707-829-0104 (fax)
We have a web page for this book, where we list errata, examples, and any additional information. You can access this page at http://bit.ly/cloud-nat-dev-ops.
Email bookquestions@oreilly.com to comment or ask technical questions about this book.
For news and information about our books and courses, visit http://oreilly.com.
Find us on Facebook: http://facebook.com/oreilly
Follow us on Twitter: http://twitter.com/oreillymedia
Watch us on YouTube: http://youtube.com/oreillymedia
Acknowledgments
我们衷心感谢许多人,在这本书的初稿阶段阅读并给予我们宝贵的反馈和建议,或以其他方式帮助我们,包括(但不限于)Abby Bangser,Adam J. McPartlan,Adrienne Domingus,Alexis Richardson,Aron Trauring,Camilla Montonen,Gabe Medrash,Gabriell Nascimento,Hannah Klemme,Hans Findel,Ian Crosby,Ian Shaw,Ihor Dvoretskyi,Ike Devolder,Jeremy Yates,Jérôme Petazzoni,Jessica Deen,John Harris,Jon Barber,Kitty Karate,Marco Lancini,Mark Ellens,Matt North,Michel Blanc,Mitchell Kent,Nicolas Steinmetz,Nigel Brown,Patrik Duditš,Paul van der Linden,Philippe Ensarguet,Pietro Mamberti,Richard Harper,Rick Highness,Sathyajith Bhat,Suresh Vishnoi,Thomas Liakos,Tim McGinnis,Toby Sullivan,Tom Hall,Vincent De Smet 和 Will Thames.
第一章:云中的革命
从未有过一个世界从何开始的时候,因为它像圆圈一样不断地旋转,而在圆圈上没有任何一个地方是它开始的地方。
艾伦·沃茨
正在发生一场革命。实际上,是三场革命。
第一场革命是云的诞生,我们将解释它是什么,以及为什么它如此重要。第二场是DevOps的黎明,您将了解它涉及的内容以及它如何改变运营。第三场革命是容器的广泛采用。这三波变革共同创建了一个新的软件世界:云原生世界。这个世界的操作系统被称为Kubernetes。
在本章中,我们将简要回顾这些革命的历史和意义,并探讨这些变化如何影响我们所有人部署和操作软件的方式。我们将概述云原生的含义,以及如果您从事软件开发、运维、部署、工程、网络或安全工作,您可以期待在这个新世界中看到的变化。
由于这些相互关联革命的影响,我们认为计算的未来在于基于云的、容器化的、分布式系统,由自动化动态管理,在 Kubernetes 平台上(或类似的平台)运行。开发和运行这些应用程序的艺术 —— 云原生 DevOps —— 将在本书的其余部分探讨。
如果您已经熟悉所有这些背景资料,只想开始享受 Kubernetes 的乐趣,请随意跳到第二章。如果没有,请舒服地坐下来,拿着您喜爱的饮料,我们开始吧。
云的诞生
起初(好吧,无论如何是在 1960 年代),计算机填满了遥远、大规模、空调机房中的机架,用户从未直接看到它们或与其直接交互。开发者远程提交任务到机器上,并等待结果。成百上千的用户共享同一套计算基础设施,每个人只需支付他们使用的处理器时间或资源的费用。
对每个公司或组织来说,购买和维护自己的计算硬件是不划算的,因此出现了一种商业模式,用户共享第三方拥有和运行的远程机器的计算能力。
如果这听起来像是现在,而不是上个世纪,那并非巧合。革命这个词意味着“循环运动”,而计算机在某种程度上回到了它的起点。虽然多年来计算机变得更加强大 —— 今天的苹果手表相当于图 1-1 中展示的主机计算机的三倍 —— 对计算资源的共享、按使用量付费的访问却是一个非常古老的概念。现在我们称之为云,以及从分时共享主机开始的革命已经走了一整圈。

图 1-1. 早期云计算机:IBM System/360 Model 91,位于 NASA 的 Goddard 太空飞行中心
购买时间
云的核心理念是这样的:不再购买计算机,而是购买计算。也就是说,不再将大量资本投入到容易扩展、机械故障和迅速过时的物理设备中,而是简单地购买他人计算机上的时间,并让他们负责扩展、维护和升级。在裸金属机器时代——如果你喜欢的话,可以称之为“铁器时代”——计算能力是资本支出。现在它是运营支出,这造成了巨大的不同。
云不仅仅是关于远程租用的计算能力。它还涉及分布式系统。您可以购买原始计算资源(例如 Google Compute 实例 或 AWS Lambda 函数)并使用它来运行自己的软件,但您也越来越多地租用云服务:基本上是使用他人的软件。例如,如果您使用 PagerDuty 监控系统并在某些情况下提醒您,您正在使用云服务(有时称为软件即服务或 SaaS)。这些 SaaS 服务的成功部分归功于云的这一最新革命。现在几乎任何人都可以创建新的应用程序或网站,在公共云提供商上托管它,并在找到一些成功时将其扩展到全球观众。
基础设施即服务
当您使用云基础架构来运行自己的服务时,您购买的是基础设施即服务(IaaS)。您不必消耗资本去购买它,也不必去建造它,也不必去升级它。它就像电力或水一样是一种商品。云计算是企业与其 IT 基础设施之间关系的革命。
外包硬件只是故事的一部分;云还允许您外包您没有编写的软件:操作系统、数据库、集群、复制、网络、监控、高可用性、队列和流处理,以及连接您的代码和 CPU 之间的各种软件层和配置。托管服务可以为您处理几乎所有这些无差别的重活(您将在第三章中了解到更多关于托管服务优势的内容)。
云革命还引发了使用它的人们的另一场革命:DevOps 运动。
DevOps 的黎明
在 DevOps 出现之前,开发和操作软件基本上是两个由不同人群执行的独立工作。开发人员编写软件,然后将其传递给运维人员,在生产环境中运行和维护软件(也就是说,为真实用户提供服务,而不仅仅是内部测试或功能开发目的的运行)。就像大型主机计算机需要建筑物内自己的一层一样,这种分离根源于上个世纪中期。软件开发是一个非常专业的工作,计算机操作也是如此,这两种角色之间的重叠很少。
这两个部门有着完全不同的目标和激励,经常彼此冲突。开发人员倾向于快速推出新功能,而运维团队则更关心在长期内使服务稳定可靠。在某些情况下,可能会制定安全政策,阻止软件开发人员甚至访问其应用程序在生产环境中的日志或指标。他们需要征得运维团队的许可才能调试应用程序并部署任何修复。无论原因如何,通常出现应用程序问题时都会责备运维团队。
随着云计算的普及,行业发生了变化。分布式系统复杂,互联网非常庞大。在处理这些分布式系统的技术细节时,如何从故障中恢复、处理超时、平滑升级版本等,与系统的设计、架构和实施密不可分。
此外,“系统”不再仅限于您的软件:它包括内部软件、云服务、网络资源、负载均衡器、监控、内容分发网络、防火墙、DNS 等。所有这些东西都密切相连,相互依赖。编写软件的人必须了解其与系统其余部分的关系,而操作系统的人必须了解软件的工作方式和故障情况。
改进反馈循环
DevOps 运动的起源在于试图让这两个群体合作,共享理解,共同承担系统可靠性和软件正确性的责任,并改进构建这些系统和团队的可伸缩性。
DevOps 旨在改进各团队在编写代码、构建应用程序、运行测试和部署更改时存在的反馈循环和交接点,以确保事物运行顺畅和高效。
什么是 DevOps?
DevOps偶尔会被用来定义,这个术语有时会引起争议,一些人认为它不过是软件开发中现有良好实践的现代标签,还有人拒绝在开发和运营之间需要更多协作的必要性。
对于 DevOps 实际是什么也存在广泛的误解:一个职位?一个团队?一种方法论?一种技能?有影响力的 DevOps 作家约翰·威利斯确定了四个关键支柱,他称之为文化、自动化、测量和共享(CAMS)。实践 DevOps 的组织具有鼓励协作、拒绝将知识隔离在团队之间以及寻找持续改进方法的文化。另一种分解方法是布莱恩·道森所说的 DevOps 三位一体:人与文化、过程与实践、工具与技术。
有些人认为云和容器意味着我们不再需要 DevOps——有时被称为NoOps。这种观点的核心是,由于所有 IT 运营都外包给云提供商或其他第三方服务,企业不再需要全职运营人员。
NoOps 的谬误基于对 DevOps 工作实际涉及的误解:
在 DevOps 中,大部分传统的 IT 运营工作发生在代码进入生产之前。每个发布都包括监控、日志记录和 A/B 测试。CI/CD 管道会自动运行单元测试、安全扫描器和策略检查。部署是自动化的。控制、任务和非功能性需求现在在发布之前实施,而不是在关键故障的紧张和后果中进行。
Jordan Bach (AppDynamics)
目前,您会发现很多关于 DevOps 工程师职称的招聘信息,以及这种角色在不同组织中预期的广泛范围。有时它看起来更像是传统的“系统管理员”角色,与软件工程师的互动很少。有时,这种角色会与开发人员并肩工作,构建和部署他们自己的应用程序。重要的是考虑 DevOps 对您意味着什么,以及您希望在组织中如何实现它。
关于 DevOps 最重要的一点是,它主要是一个组织和人的问题,而不是技术问题。这符合杰瑞·温伯格的咨询第二定律:
不管一开始看起来如何,它总是一个人的问题。
杰拉尔德·M·温伯格,《咨询的秘密》
而 DevOps 确实有效。研究经常表明,采纳 DevOps 原则的公司发布更好的软件速度更快,对失败和问题的反应更好更快,在市场上更具敏捷性,并显著提高产品质量:
DevOps 不是一时的风尚;相反,它是成功组织今天工业化交付质量软件的方式,也将成为未来和未来多年的新基线。
Brian Dawson,CloudBees
基础设施即代码
曾几何时,开发人员处理软件,而运维团队则处理硬件及其运行的操作系统。
现在硬件在云中,从某种意义上说,一切都是软件。DevOps 运动将软件开发技能引入运维:快速、敏捷、协作地构建复杂系统的工具和工作流程。这通常被称为基础设施即代码(IaC)。
与其物理安装和连接计算机和交换机,云基础设施可以通过软件自动配置。运维工程师们不再手动部署和升级硬件,而是成为编写自动化云服务的软件的人。
流量不仅仅是单向的。开发人员从运维团队那里学习如何预测分布式、基于云的系统中固有的故障和问题,如何减轻其后果,以及如何设计能够优雅降级和安全失败的软件。
共同学习
开发团队和运维团队都在学习如何共同工作。他们学习如何设计和构建系统,如何在生产环境中监控和获得反馈,并如何利用这些信息改进系统。更重要的是,他们学习如何为用户提供更好的体验,为资助他们的业务提供更好的价值。
云的大规模和 DevOps 运动中的协作、以代码为中心的性质,已将运维转变为一个软件问题。同时,它们也将软件转变为一个运维问题。所有这些都引发了以下问题:
-
如何在不同服务器架构和操作系统的大型、多样化网络上部署和升级软件?
-
如何在可靠且可重复的方式中部署到分布式环境中,使用大部分标准化的组件?
进入第三次革命:容器。
容器时代的到来
要部署一段软件,你不仅需要软件本身,还需要其依赖项。这包括库、解释器、子包、编译器、扩展等等。
你还需要它的配置:设置、特定于站点的详细信息、许可密钥、数据库密码——所有能使原始软件成为可用服务的东西。
技术的现状
早期解决这个问题的尝试包括使用配置管理系统,比如 Puppet 或 Ansible,这些系统由代码组成,用于安装、运行、配置和更新软件。
另一种解决方案是综合包,顾名思义,试图将应用程序所需的一切都塞进一个文件中。综合包包含软件、其配置、其依赖的软件组件、它们的配置、它们的依赖等等。(例如,Java 综合包将包含 Java 运行时以及应用程序的所有 Java 存档[JAR]文件。)
一些供应商甚至走得更远,将运行所需的整个计算机系统包括为虚拟机镜像(VM 镜像),但这些镜像体积庞大、难以操作、构建和维护耗时,下载和部署速度慢,并在性能和资源占用效率上极为低效。
从运营角度来看,不仅需要管理这些不同类型的软件包,还需要管理一整群的服务器来运行它们。
服务器需要进行配置、网络连接、部署、配置、保持最新的安全补丁、监控、管理等。
这一切需要大量的时间、技能和努力,只是为了提供一个运行软件的平台。难道没有更好的方法吗?
盒子内思考
为了解决这些问题,技术行业从运输行业借鉴了一个想法:集装箱。在 1950 年代,一名名叫马尔科姆·麦克莱恩的卡车司机提出,不必费力地从把货物单独从把它们带到港口的卡车拖车上卸下来,并装载到船上,可以直接将卡车本身——或者说卡车箱体——装上船。
一个卡车拖车本质上是一个有轮子的大金属箱子。如果你能把箱子——也就是集装箱——与用来运输它的轮子和底盘分开,那么你就有了一个非常容易提升、装载、堆叠和卸载的东西,可以直接装上船或另一辆卡车,就在航行终点的另一头。集装箱还使用标准尺寸,这使得整个航运行业,包括船只、火车和卡车,在移动它们时知道可以期待什么。(图 1-2)
McLean 的集装箱运输公司 Sea-Land 通过使用这个系统更便宜地运输货物而变得非常成功,而且集装箱迅速流行开来。今天,每年运输数亿个集装箱,携带价值数万亿美元的货物。

图 1-2. 标准化集装箱大大降低了批量货物运输成本(照片由Lucarelli拍摄,根据知识共享许可证使用)
把软件放入容器中
软件容器正是相同的理念:一个标准的打包和分发格式,通用且广泛,能够大大增加运载能力、降低成本、实现规模经济并便于处理。容器格式包含应用程序运行所需的一切,都融入到一个 镜像文件 中,可以由 容器运行时 执行。
这与虚拟机镜像有何不同?虚拟机镜像也包含应用程序运行所需的一切,但还包括更多其他内容。一个典型的 VM 镜像大约为 1 GiB。[¹] 另一方面,设计良好的容器镜像可能小上百倍。
因为虚拟机包含许多无关的程序、库和应用程序永远不会使用的内容,它的大部分空间都被浪费了。通过网络传输 VM 镜像比优化的容器慢得多。
更糟糕的是,虚拟机是 虚拟 的:底层物理 CPU 实际上实现了一个 仿真 CPU,虚拟机在其上运行。虚拟化层对 性能 有显著且负面的影响:在测试中,虚拟化的工作负载比等效的容器慢大约 30%。
相比之下,容器直接在实际 CPU 上运行,没有虚拟化开销,就像普通的二进制可执行文件一样。
并且由于容器只包含它们需要的文件,它们比 VM 镜像小得多。它们还使用可寻址的文件系统 层 的巧妙技术,这些层可以在容器之间共享和重用。
例如,如果你有两个容器,每个都是从相同的 Debian Linux 基础镜像派生的,那么基础镜像只需下载一次,每个容器可以简单地引用它。
容器运行时将组装所有必要的层,并且仅在本地缓存中不存在时下载层。这使得磁盘空间和网络带宽的使用非常高效。
插拔式应用程序
容器不仅是部署的单位和打包的单位;它还是 重用 的单位(相同的容器镜像可以作为许多不同服务的组成部分使用)、扩展 的单位和 资源分配 的单位(容器可以在满足其特定需求的任何地方运行,只要有足够的资源可用)。
开发者不再需要担心维护不同版本的软件以运行在不同的 Linux 发行版上,针对不同的库和语言版本等。容器唯一依赖的是操作系统内核(例如 Linux)。
只需提供一个容器镜像中的应用程序,它就可以在支持标准容器格式并且有兼容内核的任何平台上运行。
Kubernetes 开发者 Brendan Burns 和 David Oppenheimer 在他们的论文 “基于容器的分布式系统设计模式” 中这样描述:
通过密封,携带它们的依赖关系,并提供原子部署信号(“成功”/“失败”),[容器]在数据中心或云中部署软件方面显著改进了以往的技术水平。但容器有可能不仅仅是一个更好的部署工具——我们相信它们注定将成为面向对象软件系统中的对象的类比,从而促进分布式系统设计模式的发展。
进行容器乐队
运维团队也发现容器大大简化了他们的工作负载。他们不再需要维护各种类型、架构和操作系统的庞大机器群,他们只需运行一个容器编排器:一种软件,旨在将许多不同的机器集合成一个集群。容器编排器是一种统一的计算基础设施,对用户来说就像是一台非常强大的单一计算机,可以在上面运行容器。
“编排”和“调度”这两个术语常常被宽泛地作为同义词使用。严格来说,在这个背景下,“编排”意味着协调和安排不同的活动以达成共同的目标(就像管弦乐团中的音乐家们)。而“调度”意味着管理现有的资源,并将工作负载分配到最高效的地方运行。(不要与“预定作业”中的调度概念混淆,后者是在预设的时间执行。)
第三个重要活动是集群管理:将多个物理或虚拟服务器连接成一个统一、可靠、容错的、表面上无缝的群组。
“容器编排器”这个术语通常指的是一个单一的服务,负责调度、编排和集群管理。
容器化(使用容器作为标准的软件部署和运行方法)提供了明显的优势,而事实上的标准容器格式使各种规模经济成为可能。但是,广泛采用容器还面临一个问题:缺乏标准的容器编排系统。
只要市场上存在多种不同的调度和编排容器的工具,企业就不愿意押注于使用哪种技术。但这一切即将改变。
Kubernetes
Google 在其他人之前很长一段时间就已经开始大规模生产负载的容器化运行。几乎所有 Google 的服务都在容器中运行:Gmail、Google 搜索、Google 地图、Google 应用引擎等等。由于当时没有合适的容器编排系统,Google 被迫发明了一个。
从 Borg 到 Kubernetes
为了解决在数百万台服务器上全球范围内运行大量服务的问题,Google 开发了一个名为Borg的私有内部容器编排系统。
Borg 实质上是一个集中式管理系统,用于分配和调度容器在一组服务器上运行。虽然非常强大,但 Borg 紧密耦合于谷歌自有的内部专有技术,难以扩展且无法公开发布。
2014 年,谷歌成立了一个名为 Kubernetes 的开源项目(来自希腊语单词κυβερνήτης,意为“舵手,领航员”),旨在开发一个容器编排器,供所有人使用,基于从 Borg 及其后继项目Omega中汲取的经验教训。
Kubernetes 的崛起迅猛。虽然在 Kubernetes 之前存在其他容器编排系统,但没有一个像 Kubernetes 一样被广泛采用。随着一个真正自由开放源码的容器编排器的出现,容器和 Kubernetes 的采用率以惊人的速度增长。
Kubernetes 继续在流行中增长,并正在成为运行容器化应用程序的标准。根据Datadog发布的一份报告:
Kubernetes 已成为容器编排的事实标准。如今,一半的运行容器的组织使用 Kubernetes,无论是在自管理的集群中,还是通过云服务提供商的服务……自 2017 年以来,Kubernetes 的采用率已经翻了一番,并且稳步增长,没有任何放缓的迹象。
就像容器标准化了软件打包和部署的方式一样,Kubernetes 正在标准化运行这些容器的平台。
为什么选择 Kubernetes?
Kelsey Hightower,谷歌的一名高级开发者倡导者,Kubernetes Up & Running(O’Reilly)的合著者,以及 Kubernetes 社区的传奇人物,这样描述:
Kubernetes 实现了最优秀的系统管理员所能做的事情:自动化、故障转移、集中式日志记录、监控。它将我们在 DevOps 社区中学到的知识变成了默认设置。
Kelsey Hightower
云原生世界中,许多传统的系统管理员任务,如升级服务器、安装安全补丁、配置网络和运行备份,已不再成为问题。Kubernetes 可以自动化这些任务,使您的团队可以集中精力做核心工作。
一些功能,如负载均衡和自动扩展,已经集成到 Kubernetes 的核心中;其他功能则由插件、扩展和使用 Kubernetes API 的第三方工具提供。Kubernetes 生态系统庞大且不断增长。
Kubernetes 使部署变得简单
运维人员喜爱 Kubernetes 的原因在于这些功能,但对开发人员来说也有一些显著优势。Kubernetes 大大减少了部署所需的时间和精力。零停机部署很常见,因为 Kubernetes 默认执行滚动更新(启动带有新版本的容器,等待它们变为健康状态,然后关闭旧版本的容器)。
Kubernetes 还提供了帮助您实施持续部署实践的设施,例如金丝雀部署:逐步在每台服务器上发布更新以尽早发现问题(参见“金丝雀部署”)。另一个常见的做法是蓝绿部署:在并行中启动系统的新版本,并在完全运行后将流量切换到它(参见“蓝绿部署”)。
需求峰值将不再导致服务崩溃,因为 Kubernetes 支持自动扩展。例如,如果容器的 CPU 利用率达到某个水平,Kubernetes 可以持续添加新的容器副本,直到利用率降到阈值以下。需求下降时,Kubernetes 会再次缩减副本,释放集群容量以运行其他工作负载。
因为 Kubernetes 内置了冗余和故障转移,所以您的应用程序将更加可靠和弹性。一些托管服务甚至可以根据需求调整 Kubernetes 集群的大小,这样您在任何时刻都不会为超出需要的更大的集群付费(参见“自动扩展”)。这确实意味着您的应用程序需要设计成能够在动态环境中运行,但 Kubernetes 提供了标准方式来利用这种基础设施。
企业也会喜欢 Kubernetes,因为它降低了基础设施成本,并更好地利用给定资源。传统的服务器,甚至云服务器,大部分时间都处于空闲状态。在正常情况下,您需要处理需求峰值的多余容量基本上是浪费的。
Kubernetes 利用这些浪费的容量来运行工作负载,因此您可以更高效地利用机器,而且您还可以免费获得扩展、负载平衡和故障转移。
虽然一些功能,如自动扩展,在 Kubernetes 之前就已经存在,但它们总是与特定的云提供商或服务绑定在一起。Kubernetes 是供应商无关的:一旦您定义了使用的资源,您可以在任何 Kubernetes 集群上运行它们,无论底层的云提供商是什么。
这并不意味着 Kubernetes 会限制你到最低公分母。Kubernetes 将你的资源映射到适当的供应商特定功能上:例如,在谷歌云上的负载均衡 Kubernetes 服务将创建一个谷歌云负载均衡器;在亚马逊上,它将创建一个亚马逊网络服务(AWS)负载均衡器。Kubernetes 抽象掉云特定的细节,让你专注于定义应用程序的行为。
就像容器是定义软件的便携方式一样,Kubernetes 资源提供了软件应该如何运行的便携定义。
Kubernetes 会消失吗?
令人奇怪的是,尽管当前对 Kubernetes 的兴奋,我们在未来可能不会再多谈论它。许多曾经新颖革命的东西现在已经成为计算的一部分,我们并不真正去思考它们:微处理器、鼠标、互联网。
Kubernetes 也可能会逐渐淡出视线,成为基础设施的一部分。这很无聊,但也很好!一旦你学会了在 Kubernetes 上部署应用程序所需的知识,你就可以把时间集中在为应用程序添加功能上了。
管理 Kubernetes 的托管服务很可能会在 Kubernetes 自身的运行背后做更多的重活。2021 年,谷歌云平台(GCP)推出了名为 Autopilot 的新服务,用于其现有的 Kubernetes 服务,它负责集群升级、网络以及根据需求自动缩放虚拟机。其他云服务提供商也朝着这个方向发展,并提供基于 Kubernetes 的平台,开发人员只需关注应用程序的运行,而不用担心底层基础设施。
Kubernetes 不是万能药
所有未来的软件基础设施都会完全基于 Kubernetes 吗?可能不会。运行任何类型的工作负载是否非常简单明了?还不完全是。
例如,在分布式系统上运行数据库需要仔细考虑重新启动时发生的事情,以及如何确保数据保持一致。
在容器中编排软件涉及启动新的可互换实例,而无需它们之间的协调。但是数据库副本并非可互换;它们各自具有唯一的状态,并且部署数据库副本需要与其他节点协调,以确保诸如模式更改同时发生在所有地方。
尽管可以在 Kubernetes 中运行像数据库这样的有状态工作负载,并具有企业级可靠性,但这需要大量的时间和工程投入,也许并不适合你的公司(见“少运行软件”)。通常来说,使用托管数据库服务更加经济高效。
其次,有些事物实际上可能并不需要 Kubernetes,并且可以在有时被称为无服务器平台,更好地称为函数即服务(FaaS)平台上运行。
云函数
例如,AWS Lambda 是一个 FaaS 平台,允许你运行用 Go、Python、Java、Node.js、C# 和其他语言编写的代码,而你根本不需要编译或部署你的应用程序。亚马逊会为你完成所有这些工作。Google Cloud 的 Cloud Run 和 Functions 也提供类似的服务,微软也提供 Azure Functions。
因为你按毫秒执行时间计费,FaaS 模型非常适合仅在需要时运行的计算,而不是为了一个云服务器付费,不管你是否在使用它。
这些云函数在某些方面比容器更方便(尽管一些 FaaS 平台也可以运行容器)。但它们最适合短期的独立作业(例如,AWS Lambda 限制函数运行时间为 15 分钟),特别是那些与现有的云计算服务集成的作业,如 Azure Cognitive Services 或 Google Cloud Vision API。
这些类型的事件驱动平台通常被称为“无服务器”模型。从技术上讲,仍然涉及到服务器:只不过是别人的服务器。关键在于你不必为其提供和维护服务器;云服务提供商为你处理这一切。
并不是每种工作负载都适合在 FaaS 平台上运行,但它仍然有可能成为将来云原生应用的关键技术之一。
云函数并不仅限于像 Lambda Functions 或 Azure Functions 这样的公共 FaaS 平台:如果你已经拥有 Kubernetes 集群并希望在其上运行 FaaS 应用程序,像 OpenFaaS 和 Knative 这样的开源项目使这成为可能。
一些 Kubernetes 无服务器平台包含长期运行的容器和事件驱动的短期函数,这可能意味着将来这些计算类型之间的区别可能会变得模糊或完全消失。
云原生
云原生这个术语已经成为越来越流行的简化方式,用来讨论利用云、容器和编排的现代应用和服务,通常基于开源软件。
确实,Cloud Native Computing Foundation (CNCF) 成立于 2015 年,他们的话是为了“促进围绕微服务架构中的容器进行编排的高质量项目的社区”。
作为 Linux 基金会的一部分,CNCF 致力于汇集开发人员、最终用户和供应商,包括主要的公共云提供商。在 CNCF 伞下最著名的项目是 Kubernetes 本身,但基金会还孵化和推广云原生生态系统的其他关键组件:Prometheus、Envoy、Helm、Fluentd、gRPC 等等。
那么我们究竟指的是什么云原生?像大多数类似的事物一样,它对不同的人可能意味着不同的东西,但或许存在一些共同点。
首先,云并不一定意味着像 AWS 或 Azure 这样的公共云提供商。许多组织在同时使用一个或多个公共提供商为不同工作负载提供服务的同时,也在内部运行自己的“云”平台。术语云宽泛地意味着用于运行软件基础设施的服务器平台,可以采用多种形式。
那么是什么使得一个应用程序成为云原生?仅仅将现有的应用程序在云计算实例上运行并不会使其成为云原生。它也不仅仅是在容器中运行,或者使用云服务,如 Azure 的 Cosmos DB 或 Google 的 Pub/Sub,尽管这些可能是云原生应用程序的重要方面之一。
那么让我们来看看大多数人都能认同的云原生系统的一些特性:
自动化
如果应用程序要由机器而不是人类部署和管理,它们需要遵守共同的标准、格式和接口。Kubernetes 提供了这些标准接口,这意味着应用程序开发人员甚至不需要担心它们。
普遍和灵活
因为它们与诸如磁盘或任何特定计算节点的物理资源解耦,容器化的微服务可以轻松地从一个节点移动到另一个节点,甚至从一个集群移动到另一个集群。
具有弹性和可伸缩性
传统应用程序往往存在单点故障:如果其主进程崩溃、底层机器发生硬件故障或网络资源拥塞,应用程序将停止工作。由于云原生应用程序本质上是分布式的,通过冗余和优雅降级可以实现高可用性。
动态的
诸如 Kubernetes 之类的容器编排器可以安排容器以充分利用可用资源。它可以运行许多容器的副本以实现高可用性,并执行滚动更新以平稳升级服务,而无需停止流量。
可观察的
由于云原生应用程序的性质,它们更难以检查和调试。因此,分布式系统的一个关键要求是可观察性:监控、日志记录、追踪和度量都有助于工程师了解其系统正在做什么(以及它们做错了什么)。
分布式
云原生是一种利用云的分布式和去中心化特性来构建和运行应用程序的方法。它关注的是你的应用程序如何工作,而不是它在哪里运行。与将代码部署为单个实体(称为单体)不同,云原生应用程序往往由多个协作的分布式微服务组成。微服务只是一个自包含的执行单一任务的服务。如果将足够多的微服务放在一起,你就会得到一个应用程序。
不仅仅是关于微服务
然而,微服务也不是万能药。单体更容易理解,因为一切都在一个地方,你可以追踪不同部分之间的交互。但是单体应用在代码本身和维护它的开发团队方面都很难扩展。随着代码的增长,其各个部分之间的交互呈指数级增长,整个系统超出了单个大脑理解的能力。
一个设计良好的云原生应用程序由微服务组成,但决定这些微服务应该是什么,边界在哪里,以及不同服务如何交互是一个不容易解决的问题。良好的云原生服务设计包括如何明智地选择如何分离架构的不同部分。然而,即使是设计良好的云原生应用程序仍然是一个分布式系统,这使它本质上复杂,难以观察和推理,并且容易以令人惊讶的方式失败。
虽然云原生系统往往是分布式的,但仍然有可能在云中运行单体应用程序,使用容器,并从中获得相当大的商业价值。这可能是逐步将单体的部分迁移到现代化的微服务的过程中的一步,或者是在重新设计系统以完全符合云原生要求之前的一个权宜之计。
运维的未来
运维、基础设施工程和系统管理是高技能的工作。它们在云原生的未来是否面临风险?我们认为不会。
相反,这些技能只会变得更加重要。设计和推理分布式系统是困难的。网络和容器编排器是复杂的。每个开发云原生应用程序的团队都需要运维技能和知识。自动化使员工从无聊、重复、手动的工作中解脱出来,去处理计算机尚无法自行解决的更复杂、有趣和有趣的问题。
这并不意味着所有当前的运维工作都是保证的。系统管理员过去可以不需要编码技能,除了可能会编写一些简单的 shell 脚本。在云原生的世界中,这将不足以成功。
在软件定义的世界中,编写、理解和维护软件的能力变得至关重要。如果你不想学习新技能,行业会把你抛在后面——这一直是如此。
分布式 DevOps
运维专业知识不再集中在一个为其他团队提供服务的运维团队中,而是分布在许多团队中。
每个开发团队至少需要一个运维专家,负责团队提供的系统或服务的健康状况。他们也会是开发者,但他们还将是网络,Kubernetes,性能,弹性以及其他开发者提供其代码到云端的工具和系统的领域专家。
由于 DevOps 革命,大多数组织将不再容纳那些不能运维的开发者,或者不会开发的运维人员。这两个学科之间的区分已经过时,正在迅速消失。开发和运营软件只是同一事物的两个方面。
有些事情将保持集中化
DevOps 是否有限制?或者传统的中央 IT 和运营团队会完全消失,分解成一群流动的内部顾问,教练,教授和解决运维问题?
我们认为不会,或者至少不完全是这样。有些事情仍然受益于集中化。例如,每个应用程序或服务团队拥有自己检测和沟通生产事故的方式,或者自己的票务系统或部署工具,这是没有意义的。没有必要让每个人都重新发明自己的轮子。
开发者生产力工程
关键在于自助服务有其局限性,而 DevOps 的目标是加速开发团队,而不是通过不必要和多余的工作减慢它们的速度。
是的,传统运营的大部分工作可以并且应该下放给其他团队,主要是那些部署代码并响应与代码相关事件的团队。但要使这种情况发生,就需要一个强大的中央团队来构建和支持 DevOps 生态系统,其他所有团队都在这个生态系统中运作。
我们不称呼这个团队为团队运维,我们喜欢称之为开发者生产力工程。一些组织可能称这个角色为平台工程师,甚至是DevOps 工程师。关键是这些团队会做任何必要的事情,帮助其他软件工程团队更好,更快地完成他们的工作:操作基础设施,构建工具,解决问题。
尽管开发者生产力工程仍然是一种专业技能,工程师们本身可能会向组织外部移动,将专业知识带到需要的地方。
Lyft 工程师 Matt Klein 建议,尽管纯粹的 DevOps 模型对初创公司和小公司是有意义的,但随着组织的增长,基础设施和可靠性专家自然倾向于集中团队。但他说这个团队不能无限扩展:
当一个工程组织达到大约 75 人的规模时,几乎肯定会有一个中央基础设施团队开始构建产品团队所需的共同基础特性。但是在某一点上,中央基础设施团队不再能够继续建设和运营对业务成功至关重要的基础设施,同时还要维护帮助产品团队处理操作任务的支持负担。
在这一点上,并非每个开发人员都能成为基础设施专家,就像一个基础设施专家团队无法为日益增长的开发人员数量提供服务一样。对于较大的组织来说,虽然仍然需要一个中央基础设施团队,但在每个开发或产品团队中嵌入站点可靠性工程师(SREs)也是有道理的。他们作为顾问向每个团队提供他们的专业知识,同时在产品开发和基础设施运营之间构建桥梁。
你就是未来。
如果你正在阅读本书,这意味着你是云原生未来的一部分。在剩下的章节中,我们将涵盖作为开发人员或运维工程师在云基础设施、容器和 Kubernetes 上工作所需的所有知识和技能。
其中一些事物可能会很熟悉,而另一些则可能是新的,但我们希望当您完成本书时,您对自己能力获得和掌握云原生技能会更有信心。是的,有很多东西需要学习,但这并非您无法应对。您能做到!
现在继续阅读吧。
摘要
我们在本书中对这一领域的概况进行了相对快速的介绍,包括 DevOps 的历史、云计算以及使用容器和 Kubernetes 来运行云原生应用程序的新兴标准。我们希望这足以让您了解这个领域的一些挑战以及它们如何可能改变 IT 行业。
在我们亲自见识 Kubernetes 之前,让我们快速回顾一下主要观点:
-
云计算使您摆脱了管理自己硬件的费用和开销,使您能够构建具有弹性、灵活性、可伸缩性的分布式系统。
-
DevOps 是一种认识,现代软件开发不止于交付代码:它是关于缩小编写代码与使用代码之间反馈环路的差距。
-
DevOps 也将代码为中心的方法和良好的软件工程实践带入了基础设施和运维领域。
-
容器允许您以小型、标准化、自包含的单元部署和运行软件。通过连接容器化的微服务,这使得构建大型、多样化、分布式系统变得更加容易和便宜。
-
编排系统负责部署您的容器、调度、扩展、网络以及所有一个优秀系统管理员需要做的事情,但以自动化、可编程的方式进行。
-
Kubernetes 是事实上的标准容器编排系统,您可以立即在生产环境中使用它。这仍然是一个快速发展的项目,所有主要的云服务提供商都提供更多的托管服务,自动处理核心 Kubernetes 组件的底层操作。
-
“无服务器”事件驱动计算也越来越受到云原生应用的欢迎,通常使用容器作为运行时。现在已有工具可在 Kubernetes 集群上运行这些类型的函数。
-
云原生是一个有用的简写术语,用于描述基于云、容器化、分布式系统,由合作的微服务组成,通过自动化的基础设施即代码动态管理。
-
远非云原生革命所废除的运维和基础设施技能将变得比以往任何时候都更加重要。
-
将会消失的是软件工程师和运维工程师之间的鲜明界限。现在一切都只是软件,我们都是工程师。
^(1) Gibibyte(GiB)是国际电工委员会(IEC)定义的数据单位,等于 1,024 Mebibytes(MiB),而 Kibibyte(KiB)则定义为 1,024 字节。我们在本书中将使用 IEC 单位(GiB、MiB、KiB),以避免任何歧义。
第二章:Kubernetes 初步
你已经踏出了进入更大世界的第一步。
奥比-旺·克诺比,《星球大战:新希望》
理论够了,让我们开始使用 Kubernetes 和容器工作吧。在本章中,你将构建一个简单的容器化应用程序,并将其部署到运行在你的机器上的本地 Kubernetes 集群中。在这个过程中,你将会接触到一些非常重要的云原生技术和概念:Docker、Git、Go、容器注册表和kubectl工具。
提示
本章是互动的!在本书中,我们会要求你按照示例在自己的计算机上安装软件、输入命令并运行容器。我们认为这比仅仅通过文字解释来学习更为有效。你可以在GitHub上找到所有的示例。
运行你的第一个容器
正如我们在第一章中看到的,容器是云原生开发的关键概念之一。构建和运行容器最流行的工具是 Docker。还有其他工具用于运行容器,但我们稍后会详细介绍。
在本节中,我们将使用 Docker Desktop 工具构建一个简单的演示应用程序,在本地运行它,并将镜像推送到容器注册表。
如果你已经对容器非常熟悉,请直接跳到“你好,Kubernetes”,那里才是真正的乐趣所在。如果你想知道容器是什么,它们如何工作,并在开始学习 Kubernetes 之前对它们进行一些实际操作,可以继续阅读。
安装 Docker Desktop
Docker Desktop 是 Mac 和 Windows 的免费软件包。它带有一个完整的 Kubernetes 开发环境,你可以在笔记本电脑或台式机上测试你的应用程序。
现在让我们安装 Docker Desktop 并使用它来运行一个简单的容器化应用程序。如果你已经安装了 Docker,请跳过本节,直接进入“运行容器镜像”。
下载适合你的计算机版本的Docker Desktop Community Edition,然后按照你的平台上的说明安装 Docker 并启动它。
注意
目前 Docker Desktop 尚不支持 Linux,因此 Linux 用户需要安装Docker 引擎,然后安装Minikube(参见“Minikube”)。
一旦完成了这一步,你应该能够打开终端并运行以下命令:
`docker version`
... Version: 20.10.7 ...
根据你的平台,确保 Docker 正确安装和运行,你将看到类似示例输出的内容。
在 Linux 系统上,你可能需要运行sudo docker version。你可以使用sudo usermod -aG docker $USER && newgrp docker将你的帐户添加到 docker 组,然后你就不需要每次都使用sudo了。
Docker 是什么?
Docker 实际上是几个不同但相关的东西:一个容器镜像格式,一个管理容器生命周期的容器运行库,一个打包和运行容器的命令行工具,以及一个容器管理的 API。这里不需要关注细节,因为 Kubernetes 支持 Docker 容器作为其众多组件之一,尽管它是一个重要的组件。
运行一个容器镜像
到底什么是容器镜像?从技术细节上讲并不重要,但您可以将镜像看作是类似于 ZIP 文件。它是一个唯一 ID 的单一二进制文件,包含了运行容器所需的一切。
无论您是直接使用 Docker 运行容器,还是在 Kubernetes 集群上运行,您只需指定一个容器镜像 ID 或 URL,系统将负责查找、下载、解压和启动容器。
我们编写了一个小型演示应用程序,将在整本书中用它来说明我们所讲的内容。您可以下载并运行我们之前准备好的容器镜像来使用该应用程序。运行以下命令以试用:
`docker container run -p 9999:8888 --name hello cloudnatived/demo:hello`
让此命令保持运行状态,并将浏览器指向 http://localhost:9999/。
您应该会看到一个友好的消息:
Hello, 世界
每当您请求这个 URL,我们的演示应用程序都将准备好并等待着迎接您。
当您玩够了,请通过在终端中按下 Ctrl-C 来停止容器。
演示应用程序
它是如何工作的?让我们下载运行在此容器中的演示应用程序的源代码,并查看一下。
对于这部分,您需要安装 Git。^(1) 如果您不确定是否已安装 Git,请尝试以下命令:
`git version`
git version 2.32.0
如果您还没有安装 Git,请按照您平台的安装说明进行操作。
安装完 Git 后,运行此命令:
`git clone https://github.com/cloudnativedevops/demo.git`
Cloning into *`demo`*... ...
查看源代码
此 Git 仓库包含了我们将在整本书中使用的演示应用程序。为了更好地了解每个阶段的情况,该仓库在不同的子目录中包含了每个连续版本的应用程序。第一个版本简单命名为 hello。要查看源代码,请运行此命令:
`cd demo/hello`
`ls`
Dockerfile README.md go.mod main.go
在您喜爱的编辑器中打开 main.go 文件(我们推荐使用Visual Studio Code,它对 Go、Docker 和 Kubernetes 开发有很好的支持)。您将看到以下源代码:
package main
import (
"fmt"
"log"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, 世界")
}
func main() {
http.HandleFunc("/", handler)
fmt.Println("Running demo app. Press Ctrl+C to exit...")
log.Fatal(http.ListenAndServe(":8888", nil))
}
介绍 Go
我们的演示应用程序是用 Go 编程语言编写的。
Go 是一种现代编程语言(自 2009 年起由 Google 开发),注重简单性、安全性和可读性,并专为构建大规模并发应用程序(特别是网络服务)而设计。在这种语言中编程也非常有趣。^(2)
Kubernetes 本身就是用 Go 编写的,Docker、Terraform 和许多其他流行的开源项目也是如此。这使得 Go 成为开发云原生应用程序的一个不错选择。
演示应用程序的工作原理
正如您所见,演示应用程序相当简单,尽管它实现了一个 HTTP 服务器(Go 提供了一个强大的标准库)。它的核心是这个名为 handler 的函数:
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, 世界")
}
如其名称所示,它处理 HTTP 请求。请求作为参数传递给函数(尽管函数目前没有对其进行任何操作)。
HTTP 服务器还需要一种方法向客户端发送响应。http.ResponseWriter 对象使我们的函数能够向用户发送消息,以在其浏览器中显示,本例中只是字符串 Hello, 世界。
任何语言中的第一个示例程序传统上会打印 Hello, world。但是因为 Go 本地支持 Unicode(国际文本表示标准),示例 Go 程序通常打印 Hello, 世界,只是为了炫耀。如果您不会说中文,没关系:Go 会处理!
程序的其余部分负责将 handler 函数注册为处理 HTTP 请求的处理程序,并打印应用程序正在启动的消息,然后实际启动 HTTP 服务器以监听并提供在 8888 端口上提供服务。
这就是整个应用程序!它现在还没有做太多事情,但随着我们的继续,我们将为其添加功能。
构建容器
您知道容器镜像是一个包含容器运行所需一切的单个文件,但首先如何构建镜像呢?嗯,要做到这一点,您使用 docker image build 命令,它以特殊的文本文件(称为Dockerfile)作为输入。Dockerfile 精确地指定了需要放入容器镜像中的内容。
容器的一个关键优势是能够基于现有镜像构建新镜像。例如,您可以获取一个包含完整 Ubuntu 操作系统的容器镜像,向其添加一个文件,结果将是一个新的镜像。
通常,一个 Dockerfile 包含一些指令,用于获取一个起始镜像(所谓的基础镜像),以某种方式进行转换,并将结果保存为一个新的镜像。
理解 Dockerfile
让我们看看我们演示应用程序的 Dockerfile(它位于应用程序存储库的hello子目录中):
FROM golang:1.17-alpine AS build
WORKDIR /src/
COPY main.go go.* /src/
RUN CGO_ENABLED=0 go build -o /bin/demo
FROM scratch
COPY --from=build /bin/demo /bin/demo
ENTRYPOINT ["/bin/demo"]
目前不需要详细了解它是如何工作的确切细节,但它使用了一种称为多阶段构建的标准 Go 容器构建过程。第一阶段从官方的 golang 容器镜像开始,它只是一个安装了 Go 语言环境的操作系统(在本例中是 Alpine Linux)。它运行 go build 命令来编译我们之前看到的 main.go 文件。
这个操作的结果是一个名为demo的可执行二进制文件。第二阶段获取一个完全空的容器镜像(称为scratch镜像,即从零开始),并将demo二进制文件复制到其中。
极简容器镜像
为什么需要第二个构建阶段?嗯,Go 语言环境以及其余的 Alpine Linux,实际上只是用来 构建 程序的。运行程序只需要 demo 二进制文件,因此 Dockerfile 创建一个新的 scratch 容器来放置它。生成的镜像非常小(约 6 MiB)— 这就是可以在生产中部署的镜像。
如果没有第二阶段,你最终得到的容器镜像将会有大约 350 MiB 的大小,其中 98% 是不必要的,永远不会被执行。镜像越小,上传和下载速度就越快,启动速度也就越快。
最小化容器也有减少安全问题攻击面的好处。容器中程序越少,潜在的漏洞就越少。
因为 Go 是一种可以生成自包含可执行文件的编译语言,所以非常适合编写最小化的容器。相比之下,官方的 Ruby 容器镜像大小为 850 MB;比我们的 Alpine Go 镜像大约大 140 倍,而且这还不包括你添加的 Ruby 程序!另一个用于使用精简容器的好资源是 distroless images,它们只包含运行时依赖项,保持最终容器镜像的大小小。
运行 Docker 镜像构建
我们已经看到 Dockerfile 包含 docker image build 工具将我们的 Go 源代码转换为可执行容器的指令。让我们继续尝试一下。在 hello 目录中,运行以下命令:
`docker image build -t myhello .`
Sending build context to Docker daemon 4.096kB Step 1/7 : FROM golang:1.17-alpine AS build ... Successfully built eeb7d1c2e2b7 Successfully tagged myhello:latest
恭喜!你刚刚构建了你的第一个容器!你可以从输出中看到,Docker 在 Dockerfile 中逐步执行每个动作,最终生成一个可用的镜像。
给你的镜像命名
当你构建一个镜像时,默认情况下它只会得到一个十六进制的 ID,你可以稍后用它来引用(例如,运行它)。这些 ID 并不是特别容易记住或输入,因此 Docker 允许你使用 -t 开关为镜像指定一个易读的名称。在前面的例子中,你将镜像命名为 myhello,所以现在应该能够使用这个名称来运行镜像。
让我们看看它是否正常工作:
`docker container run -p 9999:8888 myhello`
现在你正在运行自己的演示应用程序副本,并且可以通过浏览到与之前相同的 URL (http://localhost:9999/) 来检查它。
你应该看到 Hello, 世界。当你运行完这个镜像后,按 Ctrl-C 停止 docker container run 命令。
端口转发
在容器中运行的程序与在同一台机器上运行的其他程序隔离,这意味着它们不能直接访问网络端口等资源。
演示应用程序侦听端口 8888 上的连接,但这是容器的私有端口 8888,并非您计算机上的端口。为了连接到容器的端口 8888,您需要将本地机器上的端口转发到容器的端口。它可以是(几乎)任何端口,包括 8888,但我们将使用 9999,以清楚表明哪个是您的端口,哪个是容器的端口。
要告诉 Docker 转发端口,您可以使用-p开关,就像在“运行容器镜像”中一样:
`docker container run -p HOST_PORT:CONTAINER_PORT ...`
一旦容器运行起来,本地计算机上对HOST_PORT的任何请求都会自动转发到容器中的CONTAINER_PORT,这就是您如何通过浏览器连接到应用程序的方式。
我们之前说过,您可以使用几乎任何端口,因为低于1024的任何端口号都被视为特权端口,这意味着为了使用这些端口,您的进程必须以特殊权限的用户(如root)运行。普通非管理员用户无法使用低于 1024 的端口,因此为了避免权限问题,我们将在示例中使用较高的端口号。
容器注册表
在“运行容器镜像”中,只需提供镜像名称,Docker 就会自动下载并运行镜像。
您可能会合理地想知道它是从哪里下载的。虽然您可以通过仅构建和运行本地镜像来完美使用 Docker,但如果能够从容器注册表推送和拉取镜像,则更为实用。注册表允许您存储镜像并使用唯一名称(如cloudnatived/demo:hello)检索它们。
docker container run命令的默认注册表是 Docker Hub,但您可以指定一个不同的注册表,或者设置自己的注册表。
现在,让我们继续使用 Docker Hub。虽然您可以从 Docker Hub 下载和使用任何公共容器镜像,但要推送您自己的镜像,您需要一个帐户(称为Docker ID)。请按照Docker Hub上的说明创建您的 Docker ID。
认证注册表
一旦您获得了您的 Docker ID,下一步就是使用您的 ID 和密码将本地 Docker 客户端与 Docker Hub 连接起来:
docker login
Login with your Docker ID to push and pull images from Docker Hub. If you don't
have a Docker ID, head over to https://hub.docker.com to create one.
Username: *YOUR_DOCKER_ID*
Password: *YOUR_DOCKER_PASSWORD*
Login Succeeded
给镜像命名并推送
为了能够将本地镜像推送到注册表,您需要使用以下格式命名它:_YOUR_DOCKER_ID_/myhello。
要创建此名称,您无需重新构建镜像;相反,请运行此命令:
docker image tag myhello *YOUR_DOCKER_ID*/myhello
这样做是为了在将镜像推送到注册表时,Docker 知道要将其存储在哪个帐户中。
请继续使用以下命令将镜像推送到 Docker Hub:
docker image push *YOUR_DOCKER_ID*/myhello
The push refers to repository [docker.io/*YOUR_DOCKER_ID*/myhello]
b2c591f16c33: Pushed
latest: digest:
sha256:7ac57776e2df70d62d7285124fbff039c9152d1bdfb36c75b5933057cefe4fc7
size: 528
运行您的镜像
恭喜!您的容器镜像现在可以在任何地方运行(至少是在有互联网访问的地方),使用以下命令:
docker container run -p 9999:8888 *YOUR_DOCKER_ID*/myhello
你好,Kubernetes
现在你已经构建并推送了第一个容器镜像到镜像仓库,你可以使用 docker container run 命令运行它,但那不是很有趣。让我们做一些更有冒险精神的事情,并在 Kubernetes 中运行它。
有很多方法可以获得 Kubernetes 集群,在第三章中我们将更详细地探讨其中的一些。如果你已经可以访问 Kubernetes 集群,那很棒,如果愿意,你可以在本章的其余示例中使用它。
如果没有,不要担心。Docker Desktop 包含 Kubernetes 支持(Linux 用户,请参阅 “Minikube”)。要启用它,打开 Docker Desktop 首选项,选择 Kubernetes 选项卡,然后选中启用。详细信息请参阅 Docker Desktop Kubernetes 文档。
安装和启动 Kubernetes 可能需要几分钟时间。一旦完成,你就可以运行演示应用程序了!
Linux 用户还需要安装 kubectl 工具,按照 Kubernetes 文档网站 上的说明操作。
运行演示应用
让我们首先运行你之前构建的演示镜像。打开终端并使用以下参数运行 kubectl 命令:
kubectl run demo --image=*YOUR_DOCKER_ID*/myhello --port=9999 --labels app=demo
pod/demo created
现在暂时不必担心这个命令的细节:它基本上是 Kubernetes 中 docker container run 命令的等价物,你之前在本章中用来运行演示镜像的命令。如果你还没有构建自己的镜像,你可以使用我们的:--image=cloudnatived/demo:hello。
请记住,为了使用浏览器连接到容器的端口 8888,你需要在本地机器上将端口 9999 转发到那里。在这里也需要使用 kubectl port-forward 命令:
`kubectl port-forward pod/demo 9999:8888`
Forwarding from 127.0.0.1:9999 -> 8888 Forwarding from [::1]:9999 -> 8888
让这个命令保持运行状态,并在新终端中继续进行操作。
使用浏览器连接到 http://localhost:9999/ 查看 Hello, 世界 消息。
可能需要几秒钟来启动容器并使应用程序可用。如果半分钟后还没有准备好,尝试这个命令:
`kubectl get pods --selector app=demo`
NAME READY STATUS RESTARTS AGE demo 1/1 Running 0 9m
当容器运行并使用浏览器连接时,你会在终端看到这条消息:
Handling connection for 9999
如果容器无法启动
如果 STATUS 没有显示为 Running,可能会有问题。例如,如果状态是 ErrImagePull 或 ImagePullBackoff,这意味着 Kubernetes 无法找到并下载你指定的镜像。你可能在镜像名称中有拼写错误;检查你的 kubectl run 命令。
如果状态是 ContainerCreating,那一切都很好;Kubernetes 仍在下载和启动镜像。稍等几秒钟,然后再次检查。
完成后,你会想要清理你的演示容器:
`kubectl delete pod demo`
pod "demo" deleted
我们将在接下来的章节中详细介绍更多 Kubernetes 的术语,但现在你可以将 Pod 理解为在 Kubernetes 中运行的容器,类似于你在电脑上运行 Docker 容器的方式。
Minikube
如果您不想使用或无法使用 Docker Desktop 中的 Kubernetes 支持,还有一个替代方案:备受喜爱的 Minikube。与 Docker Desktop 类似,Minikube 提供在您自己的机器上运行的单节点 Kubernetes 集群(实际上在虚拟机中,但这并不重要)。
要安装 Minikube,请按照官方Minikube “入门指南”中的说明操作。
总结
如果您像我们一样,对于关于 Kubernetes 如此伟大的冗长文章感到不耐烦,希望您喜欢在本章中掌握一些实际任务。如果您已经是经验丰富的 Docker 或 Kubernetes 用户,也许您会原谅这个复习课程。我们希望确保每个人都能在基本方式下构建和运行容器,并且在进入更高级内容之前,您可以拥有一个可以进行玩耍和实验的 Kubernetes 环境。
这是您应该从本章中了解到的内容:
-
所有源代码示例(以及更多)都可以在伴随本书的demo 代码库中找到。
-
Docker 工具允许您在本地构建容器,将其推送到或从 Docker Hub 等容器注册表中拉取,并在本地机器上运行容器映像。
-
一个容器映像完全由 Dockerfile 指定:这是一个包含有关如何构建容器的说明的文本文件。
-
Docker Desktop 允许您在 Mac 或 Windows 机器上运行一个小型(单节点)Kubernetes 集群。Minikube 是另一个选项,在 Linux 上也可以运行。
-
kubectl工具是与 Kubernetes 集群交互的主要方式。它可以用来在 Kubernetes 中创建资源,查看集群和 Pod 的状态,并应用 YAML 文件形式的 Kubernetes 配置。
^(1) 如果您对 Git 不熟悉,请阅读 Scott Chacon 和 Ben Straub 的优秀书籍Pro Git(Apress)。
^(2) 如果您对 Go 不熟悉,Jon Bodner 的学习 Go(O'Reilly)是一本非常宝贵的指南。
第三章:获得 Kubernetes
困惑是知识的开始。
卡里尔·贾伯兰
Kubernetes 是云原生世界的操作系统,为运行容器化工作负载提供可靠且可扩展的平台。但是,您应该如何运行 Kubernetes 呢?您应该自己托管它吗?在云实例上?在裸金属服务器上?还是应该使用托管的 Kubernetes 服务?或者使用基于 Kubernetes 的托管平台,并通过工作流工具、仪表板和 Web 界面进行扩展?
一个章节要回答这么多问题,确实有点多,但我们会尽力而为。
在这里需要注意的是,我们不会特别关注 Kubernetes 本身的技术细节,比如构建、调优和故障排除集群等。有许多优秀的资源可以帮助您解决这些问题,我们特别推荐 Brendan Burns 和 Craig Tracey 的书籍 管理 Kubernetes:在现实世界中操作 Kubernetes 集群(O'Reilly)。
相反,我们将专注于帮助您了解集群的基本架构,并提供您在决定如何运行 Kubernetes 时所需的信息。我们将概述托管服务的利弊,并探讨一些流行的供应商。
如果您想运行自己的 Kubernetes 集群,我们列出了一些最佳安装工具,可帮助您设置和管理集群。
集群架构
您知道 Kubernetes 将多个服务器连接成一个集群,但集群是什么,它是如何工作的呢?对于本书的目的来说,技术细节并不重要,但您应该了解 Kubernetes 的基本组件及其如何组合在一起,以便了解在构建或购买 Kubernetes 集群时的选择。
控制平面
集群的核心是控制平面,它负责运行 Kubernetes 所需的所有任务:调度容器、管理服务、处理 API 请求等(见 图 3-1)。

图 3-1. Kubernetes 集群如何工作
控制平面实际上由多个组件组成:
kube-apiserver
这是控制平面的前端服务器,处理 API 请求。
etcd
这是 Kubernetes 存储其所有信息的数据库:存在哪些节点,集群上存在哪些资源等。
kube-scheduler
这决定了新创建的 Pod 运行在哪里。
kube-controller-manager
这负责运行资源控制器,如部署。
cloud-controller-manager
这与云提供商交互(在基于云的集群中),管理负载均衡器和磁盘卷等资源。
生产集群中的控制平面组件通常在多台服务器上运行,以确保高可用性。
节点组件
运行用户工作负载的集群成员称为工作节点。
Kubernetes 集群中的每个工作节点都运行以下组件:
kubelet
这负责驱动容器运行时启动在节点上调度的工作负载,并监控其状态。
kube-proxy
这执行网络魔术,路由请求在不同节点上的 Pod 之间,以及 Pod 与互联网之间。
容器运行时
实际上启动和停止容器并处理它们的通信。历史上最流行的选项是 Docker,但 Kubernetes 也支持其他容器运行时,如containerd和CRI-O。
除了运行不同的容器化组件外,运行控制平面组件的节点与运行应用程序负载的工作节点之间没有固有的区别。通常情况下,运行控制平面组件的节点不会同时运行用户创建的工作负载,除非是非常小的集群(如 Docker Desktop 或 Minikube)。
高可用性
正确配置的 Kubernetes 集群具有多个控制平面节点,使其具备高可用性;也就是说,如果任何一个节点失败或关闭,或者其上的某个控制平面组件停止运行,集群仍将正常工作。一个高可用的控制平面还将处理控制平面节点正常工作但无法与其他节点通信的情况,这是由于网络故障引起的网络分区。
etcd数据库在多个节点上复制,并且可以在个别节点失败的情况下继续运行,只要仍然有超过一半原始数量的etcd副本的仲裁可用。
如果所有这些都配置正确,控制平面可以在单个节点重新启动或临时失败时继续运行。
控制平面故障
控制平面损坏并不一定意味着您的应用程序将停机,尽管这可能会导致奇怪和不稳定的行为。
例如,如果您停止集群中所有控制平面节点,那么工作节点上的 Pod 将继续运行——至少一段时间。但是,您将无法部署任何新的容器或更改任何 Kubernetes 资源,并且诸如 Deployments 的控制器将停止工作。
因此,控制平面的高可用性对于正常运行的集群至关重要。您需要有足够的控制平面节点可用,以便即使其中一个失败,集群也能保持仲裁;对于生产集群,可行的最小数量是三个(参见“最小的集群”)。
工作节点故障
相比之下,任何单个工作节点的故障实际上并不重要,只要应用程序配置为以多于一个副本运行。只要控制平面仍在工作,Kubernetes 将检测到故障并将节点的 Pod 重新调度到其他地方。
如果同时有大量节点故障,这可能意味着集群不再具备运行您所需的所有工作负载所需的足够资源。幸运的是,这种情况并不经常发生,即使发生了,Kubernetes 也会尽可能地保持您的 Pods 运行,同时替换缺失的节点。
不过,需要记住的是,工作节点越少,每个节点代表的集群容量比例就越大。你应该假设单节点故障随时可能发生,特别是在云环境下,同时两个故障也不是闻所未闻的事情。
一种罕见但完全可能发生的故障是失去整个云可用区。云供应商如 AWS 和 Google Cloud 在每个地区提供多个可用区,每个大致对应一个数据中心。因此,与其将所有工作节点放在同一区域,不如将它们分布在两个甚至三个区域更明智。
信任,但需验证
尽管高可用性应该使您的集群能够在丢失一些节点时继续运行,但实际测试这一点始终是明智的。在计划的维护窗口期间或在非高峰时段,尝试重启一个工作节点,看看会发生什么。(希望是什么也不会发生,或者对您的应用程序用户不可见的情况。)然后,如果可以的话,尝试重启一个控制平面节点,看看在节点宕机时是否能够继续运行kubectl命令。
对于更严格的测试,重启控制平面节点之一。(如 Amazon EKS、Azure AKS 或 Google Kubernetes Engine(GKE)等托管服务,在本章后面我们将进一步讨论)不允许您这样做。不过,一个生产级别的集群应该可以毫无问题地经受住这一点。
自托管 Kubernetes 的成本
对于考虑在 Kubernetes 中运行生产工作负载的任何人来说,最重要的决定是买还是建?您应该自己运行集群,还是付钱给其他人来运行?让我们看看一些选项。
最基本的选择是自托管的 Kubernetes。通过自托管,我们指的是您个人或您组织内的一个团队,在您拥有或控制的机器上安装和配置 Kubernetes,就像您可能对使用的任何其他软件(如 Redis、PostgreSQL 或 NGINX)所做的那样。
这种选择为您提供了最大的灵活性和控制权。您可以决定运行哪些版本的 Kubernetes,启用哪些选项和功能,何时以及是否升级集群等。但是,正如我们将在下一节中看到的那样,也存在一些显著的缺点。
比你想象的工作要多
自托管选项还需要最大的资源,包括人力、技能、工程时间、维护和故障排除。仅仅设置一个工作的 Kubernetes 集群非常简单,但这距离一个为生产准备好的集群还有很长一段路。您至少需要考虑以下几个问题:
-
控制平面是否具有高可用性?也就是说,如果任何节点宕机或变得无响应,您的集群是否仍然可以工作?您是否仍然可以部署或更新应用程序?您的运行中的应用程序是否在没有控制平面的情况下仍然具有容错能力?(参见“高可用性”。)
-
您的工作节点池是否高可用?也就是说,如果故障导致多个工作节点或甚至整个云可用性区域宕机,您的工作负载是否会停止运行?您的集群是否会继续工作?它是否能够自动提供新节点来修复自身,还是需要手动干预?
-
您的集群是否安全设置?其内部组件是否使用 TLS 加密和受信任的证书进行通信?用户和应用程序在集群操作中是否具有最低权限和权限?容器安全默认设置是否正确?节点是否不必要地访问控制平面组件?对底层的
etcd数据库的访问是否得到适当的控制和认证? -
您的集群中的所有服务是否安全?如果它们可以从互联网访问,它们是否经过适当的身份验证和授权?对集群 API 的访问是否严格限制?
-
您的集群是否符合规范?它是否符合 Cloud Native Computing Foundation 定义的 Kubernetes 集群标准?(详见“一致性检查”。)
-
您的集群节点是否完全由配置管理,而不是通过命令式 shell 脚本设置然后被遗忘?每个节点上的操作系统和内核是否已更新,并已应用安全补丁等。
-
您的集群中的数据是否有适当的备份,包括任何持久存储?您的恢复过程是什么?您多久测试一次还原?
-
一旦您有一个运行的集群,如何随时间维护它?如何提供新节点?如何推出现有节点的配置更改?如何推出 Kubernetes 更新?如何根据需求进行伸缩?如何执行策略?
这不仅仅是关于初始设置
现在请注意,您不仅需要在首次设置第一个集群时注意这些因素,而且需要在未来所有集群的所有时间内注意。当您对 Kubernetes 基础架构进行更改或升级时,您需要考虑对高可用性、安全性等的影响。
您需要设置监控系统,以确保集群节点和所有 Kubernetes 组件正常工作。您还需要一个警报系统,以便员工可以在白天或夜晚处理任何问题。
Kubernetes 仍在快速发展中,新功能和更新正在不断发布。您需要将您的集群与这些保持最新,并了解这些更改如何影响您的现有设置。您可能需要重新配置您的集群以充分利用最新的 Kubernetes 功能。
仅仅读几本书或文章、正确配置集群然后就了事是远远不够的。你需要定期测试和验证配置——比如随机关闭任意一个控制平面节点,并确保一切仍然正常运行。
自动弹性测试工具,如Netflix 的 Chaos Monkey,可以通过定期随机杀死节点、Pods 或网络连接来帮助实现这一点。根据您的云服务提供商的可靠性,您可能会发现 Chaos Monkey 是不必要的,因为定期的真实世界故障也将测试您的集群和运行在其上的服务的弹性(参见“混沌测试”)。
工具不能全面替代您的工作
有很多工具——大量的工具——可以帮助您设置和配置 Kubernetes 集群,并且它们中的许多都自称是更多或少的点-and-click、零工作量、即时解决方案。不幸的是,在我们看来,绝大多数这些工具只解决了简单的问题,而忽略了困难的问题。
另一方面,功能强大、灵活、企业级的商业工具往往非常昂贵,甚至可能不向公众开放,因为销售托管服务比销售通用的集群管理工具能够赚取更多的钱。
Kubernetes 之难
Kelsey Hightower 的 Kubernetes 之难 教程可能是熟悉 Kubernetes 集群所有底层组件的最佳方法之一。它展示了涉及的各个移动部件的复杂性,并且对于任何考虑自行运行 Kubernetes,即使是作为托管服务的人来说,这都是一种值得进行的练习,以便深入了解其底层运作方式。
Kubernetes 是困难的
尽管普遍认为设置和管理 Kubernetes 是简单的,事实上 Kubernetes 很难。考虑到它的功能,它设计得相当简单和良好,但它必须处理非常复杂的情况,这导致了复杂的软件。
毫无疑问,学习如何正确管理自己的集群以及从日常到月常的实际操作,都需要大量的时间和精力投入。我们不想阻止您使用 Kubernetes,但我们希望您清楚地了解到运行 Kubernetes 自身所涉及的成本和收益,以帮助您做出明智的决策。与使用托管服务相比,这将有助于您对自行托管的成本和收益有一个清晰的理解。
行政开销
如果您的组织规模较大,有足够资源为专门的 Kubernetes 集群运维团队提供支持,这可能不是一个大问题。但对于中小企业,甚至只有少数工程师的初创企业来说,自行管理 Kubernetes 集群的行政开销可能是难以承受的。
提示
考虑到预算有限和 IT 运营可用人员的数量,您希望将多少资源用于管理 Kubernetes 本身?这些资源是否最好用于支持您业务的工作负载?您是否可以通过自己的员工或使用托管服务更具成本效益地运营 Kubernetes?
从托管服务开始
您可能会有些惊讶,一个 Kubernetes 的书籍中,我们建议您不要运行 Kubernetes!至少,不要自己运行控制平面。基于我们在前面章节中阐述的原因,我们认为使用托管服务很可能比自行托管 Kubernetes 集群更具成本效益。除非您想要在 Kubernetes 上进行一些奇怪和实验性质的操作,而这些操作没有任何托管提供商支持,否则基本上没有理由选择自托管的路线。
提示
根据我们的经验,以及我们在写作本书时采访的许多人的经验,托管服务是运行 Kubernetes 的最佳方式,毋庸置疑。
如果您在考虑 Kubernetes 是否适合您,使用托管服务是一个很好的尝试方式。您可以在几分钟内获得一个完全工作、安全、高可用、生产级的集群,每天只需几美元。(大多数云服务提供商甚至提供免费的层级,让您在数周或数月内运行 Kubernetes 集群而无需支付任何费用。)即使您在试用期后决定更喜欢运行自己的 Kubernetes 集群,托管服务也会向您展示应该如何完成。
另一方面,如果您已经尝试过自行设置 Kubernetes,您会对托管服务如何简化流程感到高兴。您可能没有建造自己的房子;当别人可以更便宜、更快速地为您建造更好的结果时,为什么要自己建造集群呢?
在接下来的部分中,我们将概述一些最流行的托管 Kubernetes 服务,告诉您我们对它们的看法,并推荐我们的最爱。如果您仍不确定,本章的后半部分将探讨您可以使用的 Kubernetes 安装程序(参见“Kubernetes 安装程序”)。
在此我们应该声明,作者中没有任何人与任何云服务提供商或商业 Kubernetes 供应商有所关联。没有人支付我们来推荐他们的产品或服务。这里的观点是我们自己的观点,基于个人经验以及我们在撰写本书时与数百名 Kubernetes 用户交流的观点。
当然,在 Kubernetes 的世界中,事情进展迅速,托管服务市场尤其竞争激烈。预计这里描述的特性和服务会迅速变化。这里列出的列表并不完整,但我们已尽力包含我们认为最佳、最广泛使用或其他重要的服务。
托管 Kubernetes 服务
托管的 Kubernetes 服务几乎消除了您设置和运行 Kubernetes 集群的所有管理开销,特别是控制平面部分。实际上,托管服务意味着您支付给其他人(如 Microsoft、Amazon 或 Google)来为您运行集群。
谷歌 Kubernetes 引擎(GKE)
正如您期望的那样,作为 Kubernetes 的创始人,谷歌提供了一个完全托管的 Kubernetes 服务,与谷歌云平台(GCP)完全集成。您可以使用 GCP Web 控制台gcloud CLI或它们的Terraform 模块部署集群。几分钟之内,您的集群将准备就绪。
谷歌负责监视和替换失败的节点,并自动应用安全补丁。您可以设置集群在您选择的维护窗口自动升级到最新版本的 Kubernetes。
为了实现延长的高可用性,您可以创建多区域集群,将工作节点分布在多个故障区域(大致相当于各个数据中心)。即使整个故障区域受到故障的影响,您的工作负载也将继续运行。
集群自动缩放
GKE 还提供了一个集群自动缩放选项(参见“自动缩放”)。启用自动缩放后,如果有待处理的工作负载正在等待节点变为可用状态,系统将自动添加新节点以满足需求。
相反,如果有多余的容量,自动缩放器将会将 Pod 整合到较少的节点上,并删除未使用的节点。由于 GKE 的计费是基于工作节点的数量,这有助于您控制成本。
Autopilot
谷歌还为 GKE 提供了一个名为Autopilot的层级,将托管服务推进了一步。虽然大多数托管方案负责管理控制平面节点,但 Autopilot 还完全管理工作节点。您按照 Pod 请求的 CPU 和内存计费,实际的工作节点 VM 对您来说是抽象的。对于希望拥有 Kubernetes 灵活性但不太关心容器最终运行的基础服务器的团队来说,这是一个值得考虑的选项。
亚马逊弹性 Kubernetes 服务(EKS)
亚马逊长期以来也一直提供托管容器集群服务,但直到最近,唯一的选择是弹性容器服务(ECS),这是亚马逊在 EC2 虚拟机上运行容器的专有技术。虽然完全可用,ECS不如 Kubernetes 强大或灵活,显然连亚马逊自己也认为未来是 Kubernetes,因此推出了弹性 Kubernetes 服务(EKS)。
今天,亚马逊拥有最流行的公共云服务,大多数运行 Kubernetes 的云部署都在 AWS 上进行。如果您已经在 AWS 上建立了基础设施,并计划将应用程序迁移到 Kubernetes,则 EKS 是一个明智的选择。亚马逊负责管理控制平面节点,而您将容器部署到 EC2 实例的工作节点上。
如果您希望设置 使用 CloudWatch 进行集中日志记录 或 集群自动缩放,则需要在集群启动后进行配置。这使得其不如市场上其他托管服务提供的“电池包含”体验多,但根据您的环境和用例,您可能希望自定义或省略这些功能。
部署 EKS 集群有几种选择,包括使用 AWS 管理控制台,aws 命令行工具,名为 eksctl 的开源命令行工具,以及流行的 EKS Terraform 模块。每种工具都可以自动化创建所需的各种 IAM、VPC 和 EC2 资源,以建立一个功能正常的 Kubernetes 集群。eksctl 还可以处理设置额外的组件,如 CloudWatch 日志记录或在集群配置过程中安装各种附加组件,使其成为一个更为全面的开箱即用 Kubernetes 体验。
Azure Kubernetes Service (AKS)
Azure Kubernetes Service (AKS) 是 Microsoft 在 Azure 上托管 Kubernetes 集群的选项。AKS 传统上在其 GKE 或 EKS 竞争对手之前支持较新版本的 Kubernetes。您可以通过 Azure Web 界面或使用 Azure az 命令行工具创建集群,或使用他们的 Terraform AKS 模块。
与 GKE 和 EKS 一样,您可以选择交由管理控制平面节点,并且您的计费基于集群中的工作节点数量。AKS 还支持 集群自动缩放,根据使用情况调整工作节点数量。
IBM 云 Kubernetes 服务
作为一家庄重的公司,IBM 在托管 Kubernetes 服务领域不容忽视。IBM 云 Kubernetes 服务 相当简单直接,可以在 IBM Cloud 中设置一个纯净的 Kubernetes 集群。
您可以通过默认的 Kubernetes CLI 和提供的命令行工具或基本 GUI 访问和管理 IBM Cloud 集群。没有真正突出的功能能够将 IBM 的服务与其他主要云提供商的服务区分开来,但如果您已经在使用 IBM Cloud,这是一个合乎逻辑的选择。
DigitalOcean Kubernetes
DigitalOcean 以提供简单的云服务而闻名,并为开发者提供优秀的文档和教程。最近,他们开始提供名为 DigitalOcean Kubernetes 的托管 Kubernetes 服务。与 AKS 类似,他们不收取托管控制平面节点的运行费用,您只需为应用程序运行的工作节点付费。
Kubernetes 安装程序
如果托管或托管式集群不适合您,那么您将需要考虑某种级别的 Kubernetes 自助托管:即,在自己的机器上设置和运行 Kubernetes。
除了学习和演示目的外,几乎不可能完全从头部署和运行 Kubernetes。绝大多数人使用其中一个或多个可用的 Kubernetes 安装工具或服务来设置和管理他们的集群。
kops
kops 是一个用于自动化创建 Kubernetes 集群的命令行工具。它是 Kubernetes 项目的一部分,并且作为一个 AWS 特定工具已经存在很长时间,但现在也开始为包括 Google Cloud、DigitalOcean、Azure 和 OpenStack 在内的其他提供商添加 alpha 和 beta 支持。
kops 支持构建高可用性集群,使其适用于生产 Kubernetes 部署。它使用声明性配置,就像 Kubernetes 资源本身一样,并且不仅可以提供必要的云资源和设置集群,还可以扩展节点、调整大小、执行升级和其他有用的管理员任务。
就 Kubernetes 的整个世界而言,kops 正在快速发展,但它是一个相对成熟且复杂的工具,被广泛使用。如果您计划在 AWS 上运行自托管 Kubernetes,kops 是一个不错的选择。
Kubespray
Kubespray(以前称为 Kargo)是 Kubernetes 项目下的一个工具,用于轻松部署生产就绪的集群。它提供了许多选项,包括高可用性,并支持多个平台。
Kubespray 专注于在现有机器上安装 Kubernetes,特别是本地和裸金属服务器。然而,它也适用于包括私有云(运行在您自己服务器上的虚拟机)在内的任何云环境。它使用 Ansible Playbooks,因此如果您有使用 Ansible 进行服务器配置管理的经验,那么这将是一个值得探索的选择。
kubeadm
kubeadm 是 Kubernetes 发行版的一部分,旨在帮助您按照最佳实践安装和维护 Kubernetes 集群。kubeadm 不会为集群本身提供基础设施,因此适合在裸金属服务器或任何云实例上安装 Kubernetes。
本章提到的许多其他工具和服务在内部使用 kubeadm 来处理集群管理员操作,但是如果你愿意,你也可以直接使用它。
Rancher Kubernetes Engine (RKE)
RKE旨在成为一个简单快速的 Kubernetes 安装程序。它不会为你提供节点,并且你必须自己在节点上安装 Docker,然后才能使用 RKE 安装集群。RKE 支持 Kubernetes 控制平面的高可用性。
Puppet Kubernetes 模块
Puppet 是一个功能强大、成熟、复杂的通用配置管理工具,被广泛使用,并拥有一个庞大的开源模块生态系统。官方支持的Kubernetes 模块在现有节点上安装和配置 Kubernetes,包括对控制平面和etcd的高可用性支持。
购买或自建:我们的建议
这必然是一个快速浏览一些管理 Kubernetes 集群的选项,因为提供的服务范围广泛且多样化,而且一直在不断增长。然而,我们可以根据常识原则提出一些建议。其中之一是 减少运行的软件 的理念。
减少运行的软件
减少运行的软件理念有三个支柱,所有这些支柱都将帮助你操控时间并击败你的敌人。
- 选择标准技术
- 外包未区分的繁重工作
- 创造持久的竞争优势
Rich Archbold
虽然使用创新性的新技术很有趣和令人兴奋,但从商业角度来看并不总是明智的。使用众所周知的软件通常是一个好主意。它可能有效,可能得到很好的支持,并且你不会成为承担风险和处理不可避免的错误的那个人。
如果你运行容器化工作负载和云原生应用程序,Kubernetes 是最稳定的选择,最好的方式。鉴于此,你应该选择最成熟、稳定和广泛使用的 Kubernetes 工具和服务。
未区分的繁重工作 是亚马逊创造的一个术语,用来表示所有的辛勤工作和努力,比如安装和管理软件,维护基础架构等等。这项工作没有什么特别之处;对你来说和对其他任何公司来说都一样。这项工作会花费你的钱,而不是让你赚钱。
减少运行的软件理念认为你应该外包未区分的繁重工作,因为从长远来看这样会更便宜,并且它释放了可以用来专注于核心业务的资源。
如果可以的话,使用托管的 Kubernetes
考虑到减少软件运行的原则,我们建议您将 Kubernetes 集群操作外包给托管服务。安装、配置、维护、保护、升级和使您的 Kubernetes 集群可靠是重复且繁重的工作,因此对于几乎所有企业来说,自行操作并不合理。
云原生是通过不运行不区分您的内容来加速您的业务的实践。它不是一个云提供商,不是 Kubernetes,不是容器,不是技术。
但是供应商锁定呢?
如果您选择了特定供应商(例如 Google Cloud)的托管 Kubernetes 服务,那么会不会将您锁定到该供应商,从而减少未来的选择?未必。Kubernetes 是一个标准平台,因此您构建的任何应用程序和服务都可以在任何其他经过认证的 Kubernetes 提供商系统上运行,可能需要进行一些微小的调整。
托管 Kubernetes 是否会使您更容易陷入锁定,而不是自己运行 Kubernetes 集群?我们认为情况恰恰相反。自主托管 Kubernetes 涉及大量的机械操作和配置维护,所有这些都与特定云提供商的 API 密切相关。例如,在 AWS 上部署虚拟机来运行 Kubernetes,与在 Google Cloud 上执行相同操作需要完全不同的代码。某些 Kubernetes 设置助手(如本章提到的一些)支持多个云提供商,但也有很多不支持。
Kubernetes 的一部分目的是抽象出底层基础设施的技术细节,并为开发人员提供一个标准且熟悉的界面,无论是在 Azure 上运行还是在您自己的金属裸机服务器上运行,它都以相同的方式工作。
只要您设计应用程序和自动化以针对 Kubernetes 本身而不是直接针对云基础设施,您就可以尽可能地摆脱供应商锁定。每个基础设施提供商对于计算、网络和存储的定义都有所不同,但对于 Kubernetes 来说,这些只是 Kubernetes 清单的微小调整而已。大多数 Kubernetes 部署无论在何处运行,工作方式都是相同的。
金属裸机和本地部署
您可能会感到惊讶,云原生实际上并不需要完全“在云端”,即将您的基础设施外包给 Azure 或 AWS 等公共云提供商。
许多组织在裸机硬件上运行其基础设施的部分或全部,无论是在数据中心共享托管还是在本地部署。我们在本书中讨论的关于 Kubernetes 和容器的所有内容同样适用于内部基础设施以及云环境。
您可以在自己的硬件机器上运行 Kubernetes;如果预算有限,甚至可以在一堆树莓派上运行它(图 3-2)。一些企业运行着私有云,由本地硬件托管的虚拟机组成。

图 3-2. 预算有限下的 Kubernetes:一个树莓派集群(摄影:David Merrick)
多云 Kubernetes 集群
在一些组织中,开发人员将应用程序部署到包括公共云和私有本地环境在内的多种基础设施类型上。由于 Kubernetes 可以在任何地方运行,它已成为在多云或混合云情况下标准化体验的有用工具。有可用的工具可以更轻松地管理在这些环境中运行的不同 Kubernetes 集群的工作负载。
VMware Tanzu
VMware 传统上与制作管理虚拟机和相关 VM 基础设施的工具有关。他们现在还有用于 Kubernetes 环境的工具套件 Tanzu。具体而言,Tanzu Mission Control允许您集中管理多个 Kubernetes 集群,无论它们在何处运行。还有用于构建、部署和监控容器化工作负载的工具。
OpenShift
OpenShift不仅仅是一个托管的 Kubernetes 服务:它是一个完整的平台即服务(PaaS)产品,旨在管理整个软件开发生命周期,包括持续集成和构建工具、测试运行器、应用程序部署、监控和编排。
OpenShift 可以部署到裸金属服务器、虚拟机、私有云和公共云上,因此您可以创建一个跨越所有这些环境的单一 Kubernetes 集群。这使其成为非常大型组织或具有非常异构基础设施的组织的不错选择。
Anthos
类似于 VMware Tanzu,Google 的Anthos工具使得可以在多个云中心地管理运行在多个云环境中(如 GKE、AWS 和本地 Kubernetes 环境)的 Kubernetes 工作负载。它们还允许您将本地基础设施连接到 Google Cloud 提供的其他服务,如托管的容器注册表、构建管道工具和网络层。
大多数中小型团队可能不需要立即专注于多云基础设施。这些类型的环境带来了额外的复杂性和成本。我们建议从基础知识开始,并逐步构建。将您的应用程序置于容器中,并部署到托管的 Kubernetes 云服务中,已经在为未来灵活性做好了准备,即使将来可能需要在多个云中同时运行您的应用程序。
如果必须的话,请使用标准的 Kubernetes 自托管工具
如果您已经知道自己有特殊需求,意味着托管的 Kubernetes 提供不适合您,那么只有在这种情况下才应考虑自己运行 Kubernetes。
如果情况是这样,您应该选择最成熟、功能强大且广泛使用的工具。我们建议先使用 kubeadm,然后将其与 kops 或 Kubespray 进行比较,看看它们是否符合您的需求。
另一方面,如果您需要跨多个云或平台(包括裸机服务器)扩展集群,并希望保持选项开放,考虑研究 VMware Tanzu 或 Google Anthos。由于大部分 Kubernetes 的管理开销在于设置和维护控制平面,这是一个很好的折中方案。
无集群容器服务
如果您真的想要最小化运行容器工作负载的开销,还有一种完全托管的 Kubernetes 服务的更高级别。这些被称为无集群服务,例如 Azure Container Instances(ACI)、AWS Fargate 和 Google Cloud Run。虽然在幕后确实有一个集群,但您不能通过kubectl等工具访问它。相反,您只需指定要运行的容器镜像以及一些参数,如应用程序的 CPU 和内存需求,服务将处理其余部分。
这些类型的托管服务非常适合快速启动和运行容器,但您可能会遇到某些云提供商在网络或存储方面的限制,因此可能需要转向更强大和灵活的解决方案,例如完整的 Kubernetes 平台。
AWS Fargate
根据亚马逊的说法,“Fargate 就像 EC2,但不是虚拟机,而是容器。”与 ECS 不同,无需自己规划集群节点然后将它们连接到控制平面。您只需定义一个任务,这本质上是一组关于如何运行您的容器镜像的指令,并启动它。按照任务消耗的 CPU 和内存资源的秒计费。
可以说Fargate适合简单、自包含、长时间运行的计算任务或批处理作业(如数据处理),不需要太多定制或与其他服务集成。它也非常适合构建容器,这些容器往往生命周期短暂,并且在无法自我管理工作节点的开销合理情况下使用。
如果您已经在使用带有 EC2 工作节点的 ECS,转换到 Fargate 将免去您需要规划和管理这些节点的麻烦。
Azure 容器实例(ACI)
微软的 Azure 容器实例(ACI)服务与 Fargate 类似,但还提供与 Azure Kubernetes Service(AKS)的集成。例如,您可以配置 AKS 集群,在 ACI 内部生成临时额外的 Pod 来处理需求的峰值或突发。
同样地,您可以在 ACI 中以临时方式运行批处理作业,而无需在没有工作要做时保留空闲节点。Microsoft 将这种理念称为无服务器容器,但我们发现这个术语既令人困惑(无服务器通常指云函数或作为服务的函数[FaaS]),又不准确(还是有服务器存在;只是您无法访问它们)。
ACI 还与 Azure 事件网格集成,这是 Microsoft 的托管事件路由服务。使用事件网格,ACI 容器可以与云服务、云函数或在 AKS 中运行的 Kubernetes 应用程序进行通信。
您可以使用 Azure 函数创建、运行或传递数据给 ACI 容器。这样做的好处是,您可以从云函数运行任何工作负载,而不仅仅是使用官方支持的(祝福)语言,如 Python 或 JavaScript。
如果您能将工作负载容器化,就可以将其作为云函数运行,并使用相关工具。例如,Microsoft Power Automate 甚至允许非程序员以图形方式构建工作流程,连接容器、函数和事件。
Google Cloud Run
类似于 ACI 和 Fargate,Google Cloud Run 是另一个“容器即服务”的选择,将所有服务器基础设施都隐藏起来。您只需发布一个容器映像,并告诉 Cloud Run 在每个传入的 Web 请求或接收到的Pub/Sub消息上运行该容器,它们的托管消息总线服务。默认情况下,启动的容器在 5 分钟后超时,尽管您可以将其延长至 60 分钟。
摘要
Kubernetes 无处不在!我们在广阔的 Kubernetes 工具、服务和产品领域中的旅程必然简要,但希望您觉得它有用。
尽管我们尽力使产品和功能的覆盖率尽可能更新,但世界变化迅速,我们预计即使在您阅读本文时,很多事情也会发生变化。
然而,我们认为基本观点是站得住脚的:如果服务提供商能够更好地、更便宜地管理 Kubernetes 集群,那么自己管理可能并不值得。
在我们为迁移到 Kubernetes 的公司提供咨询的经验中,这通常是一个令人惊讶的想法,或者至少不是许多人想到的。我们经常发现,组织已经开始使用诸如 kops 之类的自托管集群工具迈出第一步,但并没有真正考虑使用像 GKE 这样的托管服务。这确实值得考虑。
需要记住的更多事项:
-
Kubernetes 集群由控制平面和工作节点组成,用于运行工作负载。
-
生产集群必须具有高可用性,这意味着控制平面节点的故障不会丢失数据或影响集群的操作。
-
从简单的演示集群到准备好处理关键生产工作负载的集群还有很长的路要走。高可用性、安全性和节点管理只是其中的一些问题。
-
管理自己的集群需要大量的时间、精力和专业知识投入。即便如此,仍有可能出错。
-
像 GKE、EKS、AKS 和其他类似的托管服务为你做了所有繁重的工作,成本比自行托管低得多。
-
如果你必须自行托管你的集群,kops 和 Kubespray 是成熟且广泛使用的工具,可以在 AWS 和 Google Cloud 上提供和管理生产级集群。
-
像 VMware Tanzu 和 Google Anthos 这样的工具使得在多个云和本地基础设施上集中管理运行的 Kubernetes 集群成为可能。
-
没有充分的业务理由,请不要自行托管你的集群。如果你选择自行托管,请不要低估初始设置和持续维护的工程时间成本。
第四章:使用 Kubernetes 对象工作
我不明白为什么人们害怕新想法。我害怕旧想法。
约翰·凯奇
在第二章中,您构建并部署了一个应用程序到 Kubernetes。在本章中,您将学习涉及该过程的基本 Kubernetes 对象:Pods、Deployments 和 Services。您还将了解如何使用基本的 Helm 工具来管理 Kubernetes 中的应用程序。
在“运行演示应用程序”示例中工作后,你应该在 Kubernetes 集群中运行一个容器镜像,但实际上是如何工作的呢?在幕后,kubectl run命令创建了一个称为 Deployment 的 Kubernetes 资源。那么它是什么?Deployment 如何实际运行您的容器镜像?
部署
回想一下您如何使用 Docker 运行演示应用程序。docker container run命令启动了容器,并且直到您使用 docker stop 命令停止它。
但是假设容器因其他原因退出:可能是程序崩溃,或者系统错误,或者您的机器磁盘空间不足,或者宇宙射线在错误的时刻击中了您的 CPU(不太可能,但确实会发生)。假设这是一个生产应用程序,这意味着现在有不愉快的用户,直到有人可以到终端并输入 docker container run 命令再次启动容器。
这是一种不令人满意的安排。您真正想要的是一种监督程序,它不断检查容器是否在运行,并且如果它停止,立即重新启动。在传统服务器上,您可以使用诸如 systemd、runit 或 supervisord 等工具来实现此目的;Docker 有类似的工具,而 Kubernetes 也有一个监督功能:Deployment。
监督和调度
对于 Kubernetes 需要监督的每个程序,它都会创建一个相应的部署对象,记录有关程序的一些信息:容器镜像的名称,您想要运行的副本数,以及启动容器所需的其他任何信息。
与部署资源一起工作的是一种称为控制器的 Kubernetes 组件。控制器基本上是一段代码,连续运行在循环中,监视它们负责的资源,确保它们存在且正常工作。如果某个特定的部署由于某种原因没有足够的副本运行,控制器将创建一些新的副本。(如果由于某种原因有太多副本,控制器将关闭多余的副本。无论哪种情况,控制器都会确保实际状态与期望状态匹配。)
实际上,一个 Deployment 并不直接管理副本:相反,它会自动创建一个称为 ReplicaSet 的关联对象来处理。我们稍后会在 “ReplicaSets” 中详细讨论 ReplicaSets,但由于通常只与 Deployments 交互,让我们先更熟悉它们。
重新启动容器
乍一看,Deployments 的行为方式可能有些令人惊讶。如果您的容器完成工作并退出,Deployment 将重新启动它。如果它崩溃,或者您用信号杀死它,或者用 kubectl 终止它,Deployment 将重新启动它。(这是您在概念上应该考虑的方式;实际情况略微复杂,我们会看到的。)
大多数 Kubernetes 应用程序设计为长时间运行和可靠性,所以这种行为是有道理的:容器可能因各种原因退出,在大多数情况下,人工操作员只需重新启动它们,这也是 Kubernetes 默认的操作方式。
可以为单个容器更改此策略:例如,从不重新启动它,或者仅在失败时重新启动它,而不是正常退出时(参见 “Restart Policies”)。然而,默认行为(始终重新启动)通常是您想要的。
Deployment 的任务是监视其关联的容器,并确保指定数量的容器始终运行。如果数量不足,它会启动更多。如果数量过多,它会终止一些。这比传统的监督程序类型更强大和灵活。
创建 Deployments
请在您的本地 Kubernetes 环境中使用我们的演示容器镜像创建一个 Deployment,这样我们就可以深入了解它们的工作原理:
`kubectl create deployment demo --image=cloudnatived/demo:hello`
deployment.apps/demo created
您可以通过运行以下命令查看当前 namespace 中所有活动的 Deployments(参见 “Using Namespaces”):
`kubectl get deployments`
NAME READY UP-TO-DATE AVAILABLE AGE demo 1/1 1 1 37s
要获取有关此特定 Deployment 的更详细信息,请运行以下命令:
`kubectl describe deployments/demo`
Name: demo Namespace: default ... Labels: app=demo Annotations: deployment.kubernetes.io/revision: 1 Selector: app=demo ...
正如您所见,这里有很多信息,大部分对现在来说并不重要。不过,让我们更仔细地看一下Pod Template部分:
Pod Template:
Labels: app=demo
Containers:
demo:
Image: cloudnatived/demo:hello
Port: <none>
Host Port: <none>
Environment: <none>
Mounts: <none>
Volumes: <none>
...
你知道 Deployment 包含了 Kubernetes 运行容器所需的信息,这就是它的作用。但是 Pod 模板是什么?实际上,在我们回答这个问题之前,什么是 Pod?
Pods
Pod 是代表一个或多个容器组的 Kubernetes 对象(pod 也是鲸群的名称,这与 Kubernetes 隐约的海洋比喻相符)。
为什么 Deployment 不直接管理单个容器?答案是有时一组容器需要一起调度,在同一个节点上运行,并在本地通信,可能共享存储。这就是 Kubernetes 开始超越直接在主机上运行容器(例如 Docker)的地方。它管理整个容器组合、它们的配置和存储等,跨越一个节点集群。
例如,博客应用程序可能有一个容器用于将内容与 Git 存储库同步,以及一个 NGINX Web 服务器容器用于向用户提供博客内容。由于它们共享数据,这两个容器需要在一个 Pod 中一起调度。但在实际操作中,许多 Pod 只有一个容器,就像本例一样。(详见 “Pod 中应该包含什么?”)
因此,Pod 规范(spec)包含一个 containers 列表,在我们的例子中只有一个容器,即 demo:
demo:
Image: cloudnatived/demo:hello
Image 规范是我们在 Docker Hub 上的演示 Docker 容器镜像,这是 Kubernetes 部署启动 Pod 并保持其运行所需的所有信息。
这是一个重要的观点。kubectl create deployment 命令实际上并没有直接创建 Pod。相反,它创建了一个部署,然后部署创建了一个副本集,再由副本集创建了 Pod。部署是你期望状态的声明:“应该有一个运行着 demo 容器的 Pod。”
副本集
部署并不直接管理 Pod。这是副本集对象的工作。
副本集负责一组相同的 Pod 或 副本。如果 Pod 的数量(与规范相比)太少(或太多),副本集控制器将启动(或停止)一些 Pod 以纠正情况。
部署反过来管理副本集,并控制更新时副本的行为——例如,通过部署新版本的应用程序来进行滚动更新(参见 “部署策略”)。当你更新部署时,会创建一个新的副本集来管理新的 Pod,更新完成后,旧的副本集及其 Pod 将被终止。
在 图 4-1 中,每个副本集(V1、V2、V3)代表应用程序的不同版本,并伴随其对应的 Pod。

图 4-1. 部署、副本集和 Pod
通常情况下,你不会直接与副本集进行交互,因为部署工作已经为你完成了这些工作——但了解它们是非常有用的。
维护期望状态
Kubernetes 控制器会持续检查每个资源指定的期望状态与集群的实际状态是否一致,并进行必要的调整以保持同步。这个过程称为 协调循环,因为它会无限循环,试图协调实际状态与期望状态。
例如,当你首次创建 demo 部署时,没有 demo Pod 在运行。因此 Kubernetes 会立即启动所需的 Pod。如果 Pod 停止,只要部署仍然存在,Kubernetes 就会再次启动它。
现在让我们通过手动删除 Pod 来验证一下。首先,检查 Pod 是否确实在运行:
`kubectl get pods --selector app=demo`
NAME READY STATUS RESTARTS AGE demo-794889fc8d-5ddsg 1/1 Running 0 33s
请注意,Pod 的名称对你来说将是唯一的。你也可以通过运行以下命令查看创建此 Pod 的副本集:
`kubectl get replicaset --selector app=demo`
NAME DESIRED CURRENT READY AGE demo-794889fc8d 1 1 1 64s
看看 ReplicaSet 如何具有与上述 demo Pod 名称的开头部分匹配的随机生成 ID?在这个示例中,demo-794889fc8d ReplicaSet 创建了一个名为demo-794889fc8d-5ddsg的 Pod。
现在,运行以下命令以删除 Pod:
`kubectl delete pods --selector app=demo`
pod "demo-794889fc8d-bdbcp" deleted
再次列出 Pods:
`kubectl get pods --selector app=demo`
NAME READY STATUS RESTARTS AGE demo-794889fc8d-qbcxm 1/1 Running 0 5s demo-794889fc8d-bdbcp 0/1 Terminating 0 1h
您可能会注意到原始 Pod 正在关闭(其状态为Terminating),但它已被一个新的 Pod 替换,该 Pod 只有五秒钟的历史。您还可以看到新的 Pod 具有相同的 ReplicaSet,demo-794889fc8d,但有一个新的唯一 Pod 名称demo-794889fc8d-qbcxm。这就是协调循环在工作。
通过创建的部署告诉 Kubernetes,demo Pod 应该始终运行一个副本。它听信于你的话,即使你自己删除了 Pod,Kubernetes 也会假设你可能犯了一个错误,并帮助你启动一个新的 Pod 来替换它。
当您完成对部署的实验后,请使用以下命令关闭并清理:
`kubectl delete all --selector app=demo`
pod "demo-794889fc8d-qbcxm" deleted deployment.apps "demo" deleted replicaset.apps "demo-794889fc8d" deleted
Kubernetes 调度程序
我们说过类似部署将创建 Pods和Kubernetes 将启动所需的 Pod,但并没有真正解释它是如何发生的。
Kubernetes 调度器是此过程的负责部分。当一个部署(通过其关联的 ReplicaSet)决定需要一个新的副本时,它会在 Kubernetes 数据库中创建一个 Pod 资源。同时,这个 Pod 被添加到一个队列中,这就像调度器的收件箱。
调度器的工作是监视其未调度 Pod 的队列,从中获取下一个 Pod,并找到一个节点来运行它。它将使用一些不同的标准,包括 Pod 的资源请求,来选择一个合适的节点,假设有一个可用的(我们将在第五章详细讨论此过程)。
一旦 Pod 已在节点上调度,运行在该节点上的 kubelet 将接管并负责实际启动其容器(参见“节点组件”)。
当你删除一个 Pod 在“维持期望状态”中时,ReplicaSet 发现并开始替换它。它知道一个demo Pod 应该在其节点上运行,如果找不到,则会启动一个。 (如果您完全关闭节点会发生什么?它的 Pods 将变为未调度状态,并重新进入调度程序的队列,以重新分配到其他节点。)
Stripe 工程师 Julia Evans 已经清楚地解释了Kubernetes 中调度工作的方式。
YAML 格式的资源清单
现在你知道如何在 Kubernetes 中运行应用程序了,就这样了吗?你完成了吗?还不完全。使用kubectl create命令创建一个部署是有用的,但有限制。假设你想要改变部署规范中的某些内容:比如镜像名称或版本。你可以删除现有的部署(使用kubectl delete)并创建一个新的,带有正确字段的部署。但我们来看看我们是否能做得更好。
因为 Kubernetes 本质上是一个声明式系统,持续地将实际状态与期望状态进行协调,你只需改变期望的状态——即 Deployment 规范——Kubernetes 就会完成其余的工作。你如何做到这一点呢?
资源即数据
所有 Kubernetes 资源,如部署或 Pod,在其内部数据库中都以记录的形式表示。协调循环会监视数据库中这些记录的任何更改,并采取适当的行动。事实上,kubectl create命令所做的就是在数据库中添加一个与部署对应的新记录,然后由 Kubernetes 完成其余的工作。
但是你不需要使用kubectl create来与 Kubernetes 交互。你还可以直接创建和编辑资源清单(资源期望状态的规范)。你可以(而且应该)将清单文件保存在版本控制系统中,而不是运行命令以进行即时更改,你可以修改你的清单文件,然后告诉 Kubernetes 读取更新的数据。
部署清单
Kubernetes 清单文件的通常格式是 YAML,尽管它也可以理解 JSON 格式。那么部署的 YAML 清单文件是什么样的呢?
看一下我们的演示应用示例(hello-k8s/k8s/deployment.yaml):
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo
labels:
app: demo
spec:
replicas: 1
selector:
matchLabels:
app: demo
template:
metadata:
labels:
app: demo
spec:
containers:
- name: demo
image: cloudnatived/demo:hello
ports:
- containerPort: 8888
乍一看,这看起来很复杂,但它主要是样板文件。唯一有趣的部分与你已经以各种形式看到的相同信息:容器镜像名称和端口。当你之前将这些信息提供给kubectl create时,它在幕后创建了这个 YAML 清单的等效项,并将其提交给 Kubernetes。
使用 kubectl apply
要充分利用 Kubernetes 作为声明式基础设施即代码系统的全部功能,请自行向集群提交 YAML 清单,使用kubectl apply命令。
试试我们的示例部署清单,在demo 仓库中的hello-k8s/k8s/deployment.yaml中。^(1)
在你克隆的演示存储库中运行以下命令:
`cd hello-k8s`
`kubectl apply -f k8s/deployment.yaml`
deployment.apps "demo" created
几秒钟后,demo Pod 应该已经在运行了。
`kubectl get pods --selector app=demo`
NAME READY STATUS RESTARTS AGE demo-c77cc8d6f-nc6fm 1/1 Running 0 13s
不过我们还没结束,因为为了使用 web 浏览器连接到demo Pod,我们将创建一个 Service,这是一个 Kubernetes 资源,允许你连接到部署的 Pod(稍后详细说明)。
首先,让我们探讨一下什么是 Service,以及为什么我们需要它。
服务资源
假设你想要与 Pod 进行网络连接(比如我们的示例应用程序)。你该如何做?你可以找到 Pod 的 IP 地址,并直接连接到该地址和应用程序的端口号。但是当 Pod 重新启动时,IP 地址可能会更改,因此你需要不断查找以确保它是最新的。
更糟糕的是,可能会有多个 Pod 的副本,每个副本都有不同的地址。需要联系 Pod 的每个其他应用程序都必须维护这些地址的列表,这听起来并不是一个好主意。
幸运的是,有一种更好的方法:Service 资源为你提供一个单一且不变的 IP 地址或 DNS 名称,将自动路由到任何匹配的 Pod。稍后在“Ingress”中,我们将讨论 Ingress 资源,它允许更高级的路由和使用 TLS 证书。
但现在,让我们更仔细地看一看 Kubernetes Service 是如何工作的。
你可以将 Service 理解为类似于 Web 代理或负载均衡器,将请求转发到一组后端 Pod(参见图 4-2)。但它并不限于 Web 端口:Service 可以将流量从任何端口转发到任何其他端口,详细信息在规范的 ports 部分描述。

图 4-2. Service 为一组 Pod 提供了持久的终结点
这是我们演示应用程序的 Service 的 YAML 清单:
apiVersion: v1
kind: Service
metadata:
name: demo
labels:
app: demo
spec:
ports:
- port: 8888
protocol: TCP
targetPort: 8888
selector:
app: demo
type: ClusterIP
你可以看到它在某些方面与我们之前展示的 Deployment 资源相似。但是,kind 是 Service,而不是 Deployment,而 spec 只包括 ports 列表,加上 selector 和 type。
如果你放大一点,你会看到 Service 将其端口 8888 转发到 Pod 的端口 8888:
...
ports:
- port: `8888`
protocol: TCP
targetPort: `8888`
selector 部分告诉 Service 如何将请求路由到特定的 Pods。请求将转发到任何匹配指定标签集的 Pods;在这种情况下,只有 app: demo(参见“标签”)。在我们的示例中,只有一个匹配的 Pod,但如果有多个 Pods,Service 将每个请求发送到随机选择的一个。(参见 2)
在这方面,Kubernetes Service 有点像传统的负载均衡器,事实上,Service 和 Ingress 都可以自动创建云负载均衡器(参见“Ingress”)。
目前,要记住的主要内容是,Deployment 管理应用程序的一组 Pod,而 Service 为这些 Pod 提供单一的请求入口点。
现在,可以应用清单,创建 Service:
`kubectl apply -f k8s/service.yaml`
service "demo" created
`kubectl port-forward service/demo 9999:8888`
Forwarding from 127.0.0.1:9999 -> 8888 Forwarding from [::1]:9999 -> 8888
与之前一样,kubectl port-forward 将把 demo Pod 连接到本地机器上的一个端口,这样你就可以用你的 Web 浏览器连接到 http://localhost:9999/。
一旦确认一切都正常工作,运行以下命令清理,然后继续下一节:
`kubectl delete -f k8s/`
deployment.apps "demo" deleted service "demo" deleted
提示
你可以像之前一样使用带有标签选择器的 kubectl delete 来删除匹配选择器的所有资源(参见 “标签”)。或者,你可以像这样使用 kubectl delete -f,用一个清单目录。所有清单文件描述的资源都将被删除。
使用 kubectl 查询集群
kubectl 工具是 Kubernetes 的瑞士军刀:它应用配置,创建、修改和销毁资源,并且还可以查询集群关于现有资源及其状态的信息。
我们已经看到如何使用 kubectl get 查询 Pod 和 Deployment。您还可以使用它查看集群中存在的节点。
如果你正在运行 minikube,它应该看起来像这样:
`kubectl get nodes`
NAME STATUS ROLES AGE VERSION minikube Ready control-plane,master 17d v1.21.2
如果要查看所有类型的资源,请使用 kubectl get all。(实际上,这并不会显示 literally all 的资源,只是最常见的类型,但目前我们不会对此挑剔。)
要查看单个 Pod(或任何其他资源)的详细信息,请使用 kubectl describe:
`kubectl describe pod/demo-dev-6c96484c48-69vss`
Name: demo-794889fc8d-7frgb Namespace: default Priority: 0 Node: minikube/192.168.49.2 Start Time: Mon, 02 Aug 2021 13:21:25 -0700 ... Containers:
demo: Container ID: docker://646aaf7c4baf6d... Image: cloudnatived/demo:hello ... Conditions:
Type Status Initialized True Ready True PodScheduled True ... Events:
Type Reason Age From Message ---- ------ ---- ---- ------- Normal Scheduled 1d default-scheduler Successfully assigned demo-dev... Normal Pulling 1d kubelet pulling image "cloudnatived/demo... ...
在示例输出中,您可以看到 kubectl 提供了有关容器本身的一些基本信息,包括其镜像标识符和状态,以及发生在容器中的事件的有序列表。(我们将在 第七章 中详细了解 kubectl 的强大功能。)
将资源提升到下一个级别
现在,您已经知道如何使用声明性 YAML 清单将应用程序部署到 Kubernetes 集群了。但是这些文件中存在大量重复:例如,您多次重复了名称 demo、标签选择器 app: demo 和端口 8888。
你不应该只需在 Kubernetes 清单中指定这些值一次,然后在所需的任何地方引用它们吗?
例如,如果能定义类似 container.name 和 container.port 的变量将会很棒,然后在 YAML 文件中需要它们的任何地方使用它们。然后,如果需要更改应用程序的名称或监听的端口号,您只需在一个地方进行更改,所有清单将自动更新。
幸运的是,有一个工具可以做到这一点,在本章的最后一节中,我们将展示它能做些什么。
Helm:一个 Kubernetes 包管理器
Kubernetes 的一个流行的包管理器称为 Helm,它的工作方式与我们在前一节中描述的完全相同。你可以使用 helm 命令行工具来安装和配置应用程序(无论是你自己的还是别人的),还可以创建称为 Helm charts 的包,这些包完全指定了运行应用程序所需的资源、其依赖关系和可配置的设置。
Helm 是 Cloud Native Computing Foundation 项目家族的一部分(参见 “Cloud Native”),这反映了其稳定性和广泛采用。
注意
需要注意的是,与像 APT 或 Yum 这类工具使用的二进制软件包不同,Helm 图表实际上并不包括容器映像本身。相反,它只包含有关映像位置的元数据,就像 Kubernetes 的 Deployment 一样。
当您安装图表时,Kubernetes 本身将从您指定的位置定位并下载二进制容器映像。事实上,Helm 图表实际上只是 Kubernetes YAML 清单的便捷包装。
安装 Helm
按照Helm 安装说明为您的操作系统进行操作。
要验证 Helm 是否已安装并正在工作,请运行:
`helm version`
version.BuildInfo{Version:"v3...GoVersion:"go1.16.5"}
一旦此命令成功,您就可以开始使用 Helm 了。
安装 Helm 图表
我们的演示应用程序的 Helm 图表会是什么样子?在 hello-helm3 目录中,您会看到一个 k8s 子目录,前一个示例中 (hello-k8s) 只包含用于部署应用程序的 Kubernetes 清单文件。现在它包含一个 Helm 图表,在 demo 目录中:
`ls k8s/demo`
Chart.yaml production-values.yaml staging-values.yaml templates values.yaml
我们将在“Helm 图表的内容是什么?”中看到所有这些文件的用途,但现在让我们使用 Helm 安装演示应用程序。首先,清理之前部署的资源:
`kubectl delete all --selector app=demo`
然后运行以下命令:
`helm upgrade --install demo ./k8s/demo`
NAME: demo LAST DEPLOYED: Mon Aug 2 13:37:21 2021 NAMESPACE: default STATUS: deployed REVISION: 1 TEST SUITE: None
如果您使用之前学到的 kubectl get deployment 和 kubectl get service 命令,您会看到 Helm 已创建了一个 Deployment 资源(启动一个 Pod)和一个 Service,就像之前的示例中一样。helm upgrade --install 命令还会创建一个 Kubernetes Secret,类型为 helm.sh/release.v1 以跟踪发布。
图表、仓库和发布
这些是您需要了解的三个最重要的 Helm 术语:
-
图表 是一个 Helm 包,包含在 Kubernetes 中运行应用程序所需的所有资源定义。
-
仓库 是可以收集和共享图表的地方。
-
发布 是在 Kubernetes 集群中运行的图表的特定实例。
Helm 资源与 Docker 容器有一些相似之处:
-
Helm 仓库 是存储和从客户端下载图表的服务器,类似于容器注册表存储和提供 Docker Hub 等容器映像。
-
当图表安装到集群中时,称为 Helm 发布,就像发布 Docker 映像作为运行中容器一样。
Helm 图表可以从仓库服务器下载并安装,也可以通过指向文件系统上包含 Helm YAML 文件的本地路径直接安装。
一个图表可以在同一集群中安装多次。例如,您可能会为不同的应用程序运行多个 Redis 图表副本,每个副本作为不同网站的后端。每个独立的 Helm 图表实例都是一个不同的发布。
您可能还希望在集群中集中安装一些所有应用程序都使用的内容,例如 Prometheus 用于集中监控,或者 NGINX Ingress Controller 用于处理传入的 Web 请求。
列出 Helm 发布
要随时检查您正在运行的发布版本,请运行 helm list:
`helm list`
NAME NAMESPACE REVISION UPDATED STATUS CHART demo default 1 ... deployed demo-1.0.1
要查看特定发布的确切状态,请运行 helm status,然后输入发布的名称。您将看到与首次部署发布时相同的信息。
本书后面,我们将向您展示如何为您的应用构建自己的 Helm 图表(请参阅 “Helm 图表的内容是什么?”)。现在,只需知道 Helm 是从公共图表中安装应用的便捷方式。
许多流行的应用程序托管在各种 Helm 仓库中,并由包提供者维护。您可以添加 Helm 仓库并安装其图表,您也可以为自己的应用程序托管和发布自己的 Helm 图表。
提示
您可以在 Artifact Hub 上看到许多流行 Helm 图表的示例,这是另一个 CNCF 项目。
总结
本书主要讲述如何使用 Kubernetes,而不是深入探讨 Kubernetes 的工作原理。我们的目标是向您展示 Kubernetes 能够做什么,并迅速带您达到可以在生产环境中运行真实工作负载的水平。然而,了解一些您将要使用的主要组件(如 Pods 和 Deployments)也是很有用的。在本章中,我们简要介绍了一些最重要的内容。我们还推荐 管理 Kubernetes、生产 Kubernetes 和 Kubernetes the Hard Way 仓库,供那些希望更加熟悉 Kubernetes 内部工作机制的人使用。
尽管像我们这样的极客对这项技术如此迷人,但我们也对完成工作感兴趣。因此,我们并没有详尽地覆盖 Kubernetes 提供的每一种资源,因为种类实在是太多了,而且其中许多您几乎肯定用不上(至少目前还用不上)。
我们认为您现在需要了解的关键点有:
-
Pod 是 Kubernetes 中的基本工作单元,指定了一个或多个通信容器的组合,它们一起被调度。
-
Deployment 是一个高级 Kubernetes 资源,以声明方式管理 Pod,进行部署、调度、更新并在必要时重新启动它们。
-
Service 是 Kubernetes 中负载均衡器或代理的等效物,通过单一、著名且持久的 IP 地址或 DNS 名称将流量路由到其匹配的 Pods。
-
Kubernetes 调度器会监视尚未在任何节点上运行的 Pod,找到合适的节点,并指示该节点上的 kubelet 运行该 Pod。
-
资源如部署(Deployments)在 Kubernetes 的内部数据库中以记录形式表示。在外部,这些资源可以以 YAML 格式的文本文件(称为清单)表示。清单是资源期望状态的声明。
-
kubectl是与 Kubernetes 交互的主要工具,允许您应用清单、查询资源、进行更改、删除资源以及执行许多其他任务。 -
Helm 是一个 Kubernetes 包管理器。它简化了配置和部署 Kubernetes 应用程序,允许您使用一组捆绑的清单和模板来生成参数化的 Kubernetes YAML 文件,而不是自己维护原始的 YAML 文件。
^(1) k8s,发音为kates,是Kubernetes的常见缩写,遵循将单词缩写为数字代号的极客模式:它们的第一个和最后一个字母,以及中间字母的数量(k-8-s)。也可以参见i18n(国际化)、a11y(可访问性)和o11y(可观察性)。
^(2) 这是默认的负载均衡算法;Kubernetes 1.10+版本还支持其他算法,如最小连接。更多信息请参阅Kubernetes 文档。
第五章:管理资源
对于那个对于他来说,有的从来不算多。
伊壁鸠鲁
在本章中,我们将看看如何充分利用您的集群:如何管理和优化资源使用,如何管理容器的生命周期,以及如何使用命名空间来分区集群。我们还将概述一些技术和最佳实践,以降低集群成本,同时尽可能地发挥其性能。
您将学习如何使用资源请求、限制和默认设置,以及如何通过垂直 Pod 自动缩放器优化它们;如何使用就绪探针、存活探针和 Pod 破坏预算来管理容器;如何优化云存储;以及何时如何使用可抢占或预留实例来控制成本。
理解资源
假设您有一个具有一定容量的 Kubernetes 集群,并且具有适当大小的节点数量。如何从中获得最大的性价比?也就是说,在确保有足够的余地应对需求峰值、节点故障和糟糕的部署的同时,如何为您的工作负载充分利用可用的集群资源?
要回答这个问题,把自己放在 Kubernetes 调度器的位置上,试着从它的角度看事物。调度器的工作是决定在哪个节点上运行特定的 Pod。是否有任何节点有足够的空闲资源来运行这个 Pod?
除非调度器知道 Pod 需要多少资源才能运行,否则无法回答这个问题。一个需要 1 GiB 内存的 Pod 无法被调度到只剩下一百 MiB 空闲内存的节点上。
同样,当一个贪婪的 Pod 占用了太多资源并使同一节点上的其他 Pod 饥饿时,调度器必须能够采取行动。但太多是多少?为了有效地调度 Pod,调度器必须了解每个 Pod 的最小和最大允许的资源需求。
这就是 Kubernetes 资源请求和限制发挥作用的地方。Kubernetes 理解如何管理两种资源:CPU 和内存。还有其他重要类型的资源,比如网络带宽、磁盘 I/O 操作(IOPS)和磁盘空间,这些可能在集群中引起争用,但 Kubernetes 还没有一种方法来描述 Pod 对这些资源的需求。
资源单位
Pod 的 CPU 使用量通常用 CPU 单位来表示,就像你预期的那样。在 Kubernetes 中,1 个 CPU 单位等同于一个 AWS 虚拟 CPU(vCPU)、一个 Google Cloud Core、一个 Azure vCore,或者支持超线程的裸金属处理器上的一个超线程。换句话说,Kubernetes 术语中的 1 CPU 意味着你认为的那样。
因为大多数 Pod 不需要整个 CPU,请求和限制通常用 millicpus(有时称为 millicores)表示。内存以字节为单位进行测量,或者更方便地,以 mebibytes(MiB)为单位。
资源请求
Kubernetes 的 资源请求 指定了 Pod 运行所需的最小资源量。例如,请求 100m(100 毫核)和 250Mi(250 MiB 内存)意味着 Pod 不能被调度到没有足够这些资源的节点上。如果没有足够容量的节点可用,Pod 将保持在 pending 状态,直到有足够的资源为止。
例如,如果所有集群节点都有两个 CPU 内核和 4 GiB 内存,那么请求 2.5 个 CPU 的容器将永远无法被调度,请求 5 GiB 内存的容器也是如此。
让我们看看资源请求应用到我们的演示应用会是什么样子:
spec:
containers:
- name: demo
image: cloudnatived/demo:hello
ports:
- containerPort: 8888
`resources``:`
`requests``:`
`memory``:` `"``10Mi``"`
`cpu``:` `"``100m``"`
资源限制
资源限制 指定了 Pod 允许使用的最大资源量。尝试使用超过其分配的 CPU 限制的 Pod 将被 限制,从而降低其性能。
尝试使用超过允许的内存限制的 Pod 将被终止。如果可以重新调度终止的 Pod,那么就会重新调度。实际上,这可能意味着 Pod 只是在同一节点上重新启动。
有些应用程序,如网络服务器,可以根据需求的增加而随时间消耗更多资源。指定资源限制是防止这些资源需求大的 Pod 使用超过集群容量的公平份额的好方法。
这里是一个在演示应用上设置资源限制的例子:
spec:
containers:
- name: demo
image: cloudnatived/demo:hello
ports:
- containerPort: 8888
`resources``:`
`limits``:`
`memory``:` `"``20Mi``"`
`cpu``:` `"``250m``"`
确定为特定应用设置什么样的限制是观察和判断的问题(见“优化 Pods”)。
Kubernetes 允许资源 过度提交;也就是说,节点上所有容器的资源限制总和可以超过该节点的总资源。这是一种赌博:调度程序打赌,大多数情况下,大多数容器不需要达到其资源限制。
如果这种赌博失败,并且总资源使用接近节点的最大容量时,Kubernetes 将开始更积极地终止容器。在资源压力条件下,已超过其请求但未达到限制的容器可能仍会被终止。
服务质量
根据 Pod 的请求和限制,Kubernetes 将其分类为以下几种服务质量(QoS)类之一:Guaranteed、Burstable 或 BestEffort。
当请求匹配限制时,一个 Pod 被分类为 Guaranteed 类,意味着控制平面认为它是最重要的 Pod 之一,将尽最大努力确保 Pod 只有在超过指定限制时才会被终止。对于高度关键的生产工作负载,您可能希望考虑设置限制和请求以匹配,以便优先调度这些容器。
突发型 Pod 的优先级低于保证型 Pod,如果节点上有空余容量,Kubernetes 将允许它们“突发”超出其请求直至其限制。如果 Pod 使用的资源超过了请求,如果控制平面需要为更高 QoS 类别的 Pod 腾出位置,则可能会终止 Pod。
如果 Pod 没有指定任何请求或限制,它被视为最佳努力,这是最低优先级。允许 Pod 使用节点上可用的任何 CPU 和内存,但当集群需要为更高的 QoS Pod 腾出空间时,它将首先被终止。
最佳实践
对于您的容器,始终指定资源请求和限制。这有助于 Kubernetes 适当地调度和管理您的 Pod。
管理容器生命周期
我们已经看到,当 Kubernetes 知道 Pod 的 CPU 和内存需求时,它可以最好地管理您的 Pod。但它还必须知道容器何时工作:即,当它正常运行并准备好处理请求时。
容器化应用程序经常会进入卡住状态,此时进程仍在运行,但没有提供任何请求服务。Kubernetes 需要一种方法来检测此情况,以便重新启动容器以解决问题。
存活探针
Kubernetes 允许您在容器规范的一部分指定一个存活探针:一个健康检查,用于确定容器是否存活(即,正在工作)。
对于 HTTP 服务器容器,存活探针规范通常看起来像这样:
livenessProbe:
httpGet:
path: /healthz
port: 8888
initialDelaySeconds: 3
periodSeconds: 3
failureThreshold: 2
httpGet 探针对您指定的 URI 和端口进行 HTTP 请求;在这种情况下是对端口 8888 上的 /healthz。
如果您的应用程序没有用于健康检查的特定端点,您可以使用 / 或应用程序的任何有效 URL。不过,通常的做法是专门创建一个 /healthz 端点以此为目的。(为什么是 z?只是为了确保它不会与类似 health 的现有路径发生冲突,比如可能是关于健康信息的页面)。
如果应用程序响应 HTTP 2xx 或 3xx 状态码,Kubernetes 将认为它是存活的。如果它响应其他任何内容,或者根本没有响应,则容器被视为死亡,并将重新启动。
探测延迟和频率
Kubernetes 应该多快开始检查您的存活探针?没有应用可以立即启动。如果 Kubernetes 在启动容器后立即尝试存活探针,很可能会失败,导致容器重新启动,这种循环将永远重复!
initialDelaySeconds 字段让您告诉 Kubernetes 在尝试第一个存活探针之前等待多长时间,避免这种死循环的情况。从 Kubernetes 1.20 版本开始,还有一个专门的 startupProbe 功能,用于配置探测器以确定应用程序何时已完成启动。有关更多详情,请参见 “启动探针”。
Kubernetes 不应该以每秒数千次的频率向您的应用程序发送healthz端点的请求,这不是一个好主意。您的健康检查端点应该快速且不会给应用程序增加明显的负载。您不希望因为您的应用程序因应对大量健康检查而繁忙而导致用户体验受损。periodSeconds字段指定应定期检查活动探针的频率;在此示例中,每三秒钟检查一次。
failureThreshold允许您设置探测器在 Kubernetes 将应用程序视为不健康之前可以失败多少次。默认值为三次,这允许应用程序出现一些小问题,但您可能需要根据调度程序在确定应用程序健康性方面做出决策时希望的侵略性来调整此值。
其他类型的探针
httpGet并不是唯一可用的探针类型;对于不使用 HTTP 的网络服务器,您可以使用tcpSocket:
livenessProbe:
tcpSocket:
port: 8888
如果到指定端口的 TCP 连接成功,则容器处于运行状态。
您还可以在容器内运行任意命令,使用exec探针:
livenessProbe:
exec:
command:
- cat
- /tmp/healthy
exec探针在容器内部运行指定的命令,并且如果命令成功(即以零状态退出),则探针成功。通常情况下,exec更适用作为就绪探针,我们将在下一节中看到它们的用法。
就绪探针
与活动探针相关但语义不同的是就绪探针。有时,应用程序需要向 Kubernetes 发出信号,表示它暂时无法处理请求;可能是因为它正在执行一些长时间的初始化过程,或者在等待某个子进程完成。就绪探针用于执行此功能。
如果您的应用程序在准备好服务之前不开始侦听 HTTP,则您的就绪探针可以与活动探针相同:
readinessProbe:
httpGet:
path: /healthz
port: 8888
initialDelaySeconds: 3
periodSeconds: 3
如果容器未能通过其就绪探针,则将其从匹配 Pod 的任何服务中移除。这就像从负载均衡池中移除失败节点一样:在就绪探针再次成功之前,不会向 Pod 发送任何流量。请注意,这与livenessProbe不同,因为失败的readinessProbe不会杀死并重新启动 Pod。
通常情况下,当 Pod 启动时,Kubernetes 会在容器进入运行状态后立即开始向其发送流量。然而,如果容器有就绪探针,则 Kubernetes 会等待探针成功之后再开始向其发送任何请求,以确保用户不会看到来自未就绪容器的错误。这对于零停机升级非常重要(有关详细信息,请参阅“部署策略”)。
一个未准备好的容器仍将显示为Running,但READY列将显示一个或多个未准备好的容器在 Pod 中:
`kubectl get pods`
NAME READY STATUS RESTARTS AGE readiness-test 0/1 Running 0 56s
注意
就绪性探针应该仅返回 HTTP 200 OK状态。虽然 Kubernetes 本身将 2xx 和 3xx 状态码都视为就绪,但云负载均衡器可能不会这样。如果您正在使用与云负载均衡器配对的 Ingress 资源(参见“Ingress”),并且您的就绪性探针返回 301 重定向,负载均衡器可能标记所有您的 Pod 为不健康。确保您的就绪性探针只返回 200 状态码。
启动探针
除了具有initialDelaySeconds的活跃性探针外,Kubernetes 还提供了另一种确定应用程序何时完成启动的方法。某些应用程序需要更长的初始化时间,或者您可能希望在应用程序中的一个特殊端点中仪表化检查启动状态,这与其他活跃性和就绪性检查不同。
当配置了startupProbe时,livenessProbe将等待它成功后才开始进行活跃性检查。如果它从未成功过,则 Kubernetes 将杀死并重启 Pod。
startupProbe的语法与活跃性和就绪性探测类似:
livenessProbe:
httpGet:
path: /healthz
port: 8888
failureThreshold: 2
startupProbe:
httpGet:
path: /healthz
port: 8888
failureThreshold: 10
在这个例子中,注意我们的livenessProbe将在failureThreshold达到两次失败后将 Pod 标记为不健康,但我们在startupProbe中为应用程序提供更多启动时间,例如failureThreshold: 10。这希望能防止 Pod 可能启动不够快速,否则livenessProbe可能会在其有机会运行之前放弃并重新启动它。
gRPC 探针
尽管许多应用程序和服务通过 HTTP 进行通信,但现在越来越流行使用Google 远程过程调用(gRPC)协议,特别是用于微服务。gRPC 是由 Google 开发并由 Cloud Native Computing Foundation 托管的高效、可移植的二进制网络协议。
httpGet探针在 gRPC 服务器上无法工作,虽然你可以使用tcpSocket探针,但这只能告诉你能否连接到套接字,而不能确认服务器本身是否工作。
gRPC 有一个标准的健康检查协议,大多数 gRPC 服务支持。要使用 Kubernetes 的活跃性探针来查询此健康检查,您可以使用grpc-health-probe工具。如果将该工具添加到您的容器中,您可以使用exec探针进行检查。
基于文件的就绪性探针
或者,您可以让应用程序在容器文件系统上创建一个名为/tmp/healthy之类的文件,并使用exec就绪性探针检查该文件是否存在。
这种就绪探针非常有用,因为如果您想临时将容器停止以调试问题,您可以附加到容器并删除/tmp/healthy文件。下一个就绪探针将失败,Kubernetes 将从任何匹配的服务中删除容器。(然而,更好的方法是调整容器的标签,使其不再匹配服务:参见“服务资源”。)
您现在可以随意检查和排除容器故障。完成后,您可以终止容器并部署修复版本,或者将探针文件放回原位,以便容器可以再次开始接收流量。
最佳实践
使用就绪探针和存活探针告知 Kubernetes 何时准备好处理请求,或者何时出现问题需要重新启动。还要考虑在更广泛的生态系统中您的应用程序如何运行以及失败时应采取的措施。如果您的探针相互连接并共享依赖关系,可能会导致级联失败场景。
minReadySeconds
默认情况下,容器或 Pod 在其就绪探针成功时被视为就绪。在某些情况下,您可能希望运行容器一段时间以确保其稳定。在部署期间,Kubernetes 会等到每个新 Pod 准备就绪后再启动下一个(参见“滚动更新”)。如果有一个有问题的容器立即崩溃,这将停止滚动更新,但如果它需要几秒钟才能崩溃,所有它的副本可能会在您发现问题之前完成部署。
为了避免这种情况,您可以在容器上设置minReadySeconds字段。容器或 Pod 在其就绪探针运行了minReadySeconds(默认为 0)之后才被认为是就绪的。
Pod 中断预算
有时候,尽管 Pod 仍然活着并且就绪(称为驱逐过程),Kubernetes 也需要停止它们。例如,在升级之前可能会排空它们所在的节点,需要将 Pod 迁移到另一个节点。
然而,这不一定会导致应用程序的停机时间,只要保持足够的副本运行即可。您可以使用PodDisruptionBudget资源来指定对于给定应用程序,您可以在任何给定时间内失去多少个 Pod。
例如,您可能会指定一次不超过应用程序 Pod 总数的 10%可以被中断。或者您可能希望指定,只要至少有三个副本始终运行,Kubernetes 可以驱逐任意数量的 Pod。
minAvailable
下面是一个 PodDisruptionBudget 的示例,使用minAvailable字段指定要保持运行的最小 Pod 数量:
apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
name: demo-pdb
spec:
`minAvailable``:` `3`
selector:
matchLabels:
app: demo
在这个示例中,minAvailable: 3指定了至少需要三个匹配标签app: demo的 Pod 始终运行。Kubernetes 可以驱逐尽可能多的demo Pod,只要始终保留至少三个。
maxUnavailable
相反,您可以使用 maxUnavailable 来限制 Kubernetes 可以驱逐的 Pod 的总数或百分比:
apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
name: demo-pdb
spec:
`maxUnavailable``:` `10%`
selector:
matchLabels:
app: demo
在这里,不允许一次驱逐超过 10% 的 demo Pod。不过,这仅适用于所谓的 自愿驱逐;也就是说,由 Kubernetes 启动的驱逐。例如,如果一个节点遭遇硬件故障或被删除,那么其上的 Pod 将被迫驱逐,即使这可能违反了中断预算。
由于 Kubernetes 倾向于在节点之间均匀分布 Pod,其他条件相同的情况下,考虑集群需要多少个节点时,这是值得注意的。如果您有三个节点,其中一个节点的故障可能导致您失去三分之一的所有 Pod,并且这可能不足以维持可接受的服务水平(参见 “高可用性”)。
最佳实践
为您的业务关键应用程序设置 PodDisruptionBudgets,以确保始终有足够的副本来维护服务,即使 Pod 被驱逐。
使用命名空间
另一个管理集群资源使用的非常有用的方法是使用 namespaces。Kubernetes 命名空间是将集群分区为不同子部分的一种方式,您可以根据需要来使用它们。
例如,您可能为应用程序的不同版本测试而拥有不同的命名空间,或者每个团队单独一个命名空间。正如 namespace 这个术语所暗示的那样,一个命名空间中的名称对另一个命名空间是不可见的。
这意味着您可以在 prod 命名空间中有一个名为 demo 的服务,而在 test 命名空间中有另一个名为 demo 的服务,它们之间不会有任何冲突。
要查看集群上存在的命名空间,请运行以下命令:
`kubectl get namespaces`
NAME STATUS AGE default Active 1y kube-public Active 1y kube-system Active 1y
您可以将命名空间视为计算机硬盘上的文件夹。虽然您可以将所有文件放在同一个文件夹中,但这会很不方便。查找特定文件会耗费时间,而且很难看出哪些文件与其他文件属于同一组。命名空间将相关资源分组在一起,并使其更容易处理。但与文件夹不同,命名空间不能嵌套。
使用命名空间
到目前为止,在使用 Kubernetes 时,我们总是使用 default namespace。如果在运行 kubectl 命令时未指定命名空间(例如 kubectl run),则命令将在默认命名空间上运行。如果您想知道 kube-system 命名空间是什么,请注意 Kubernetes 内部系统组件运行在这里,以使它们与您自己的应用程序分开。
相反,如果您使用 --namespace 标志(或简称 -n)指定了命名空间,那么您的命令将使用该命名空间。例如,要获取 prod 命名空间中 Pod 的列表,请运行:
`kubectl get pods --namespace prod`
我应该使用什么样的命名空间?
您完全可以自行决定如何将您的集群划分为命名空间。一个直观的想法是为每个应用程序或团队创建一个命名空间。例如,您可以创建一个demo命名空间来运行演示应用程序。您可以使用类似以下内容的 Kubernetes 命名空间资源创建命名空间:
apiVersion: v1
kind: Namespace
metadata:
name: `demo`
要应用此资源清单,请使用kubectl apply -f命令(有关此命令的更多信息,请参阅“YAML 格式的资源清单”)。您可以在演示应用程序仓库的hello-namespace目录中找到本节所有示例的 YAML 清单:
`cd demo/hello-namespace`
`ls k8s`
deployment.yaml limitrange.yaml namespace.yaml resourcequota.yaml service.yaml
您还可以进一步为应用程序在每个环境中运行创建命名空间,例如demo-prod、demo-staging、demo-test等。您可以使用命名空间作为一种临时的虚拟集群,并在完成后删除命名空间。但是要小心!删除命名空间会删除其中的所有资源。您真的不希望对错误的命名空间运行该命令。(请参阅“引入基于角色的访问控制(RBAC)”了解如何在单个命名空间上授予或拒绝用户权限。)
在当前版本的 Kubernetes 中,目前没有办法保护诸如命名空间之类的资源免受删除(尽管有关于此功能的提案正在讨论中)。因此,请不要删除命名空间,除非它们确实是临时的,并且您确定它们不包含任何生产资源。
最佳实践
为您的每个应用程序或基础架构的每个逻辑组件创建单独的命名空间。不要使用默认命名空间:这样做容易出错。
如果您需要阻止特定命名空间中的所有网络流量的进出,您可以使用Kubernetes 网络策略来强制执行此操作。
服务地址
尽管命名空间在逻辑上相互隔离,它们仍然可以与其他命名空间中的服务通信。您可能还记得从“服务资源”中,每个 Kubernetes 服务都有一个关联的 DNS 名称,您可以使用它来与之通信。连接到主机名demo将连接到其名称为demo的服务。这在不同命名空间之间是如何工作的?
服务 DNS 名称始终遵循此模式:
SERVICE.NAMESPACE.svc.cluster.local
.svc.cluster.local 部分是可选的,命名空间也是如此。但是,如果您想与prod命名空间中的demo服务通信,例如,您可以使用:
demo.prod
即使您有一打名为demo的不同服务,每个服务都在自己的命名空间中,您可以将命名空间添加到服务的 DNS 名称中,以明确指定您要使用的服务。
资源配额
除了限制单个容器的 CPU 和内存使用量外(您可以在“资源请求”中了解更多信息),您还可以(并且应该)限制给定命名空间的资源使用情况。做法是在命名空间中创建一个 ResourceQuota。
这里是一个 ResourceQuota 示例:
apiVersion: v1
kind: ResourceQuota
metadata:
name: demo-resourcequota
spec:
hard:
pods: "100"
将此清单应用于特定命名空间(例如 demo)会在该命名空间内设置一次性运行的一百个 Pod 的硬限制。(请注意,ResourceQuota 的 metadata.name 可以是您喜欢的任何内容。它影响的命名空间取决于您将清单应用于哪些命名空间。)
`cd demo/hello-namespace`
`kubectl create namespace demo`
namespace "demo" created `kubectl apply --namespace demo -f k8s/resourcequota.yaml`
resourcequota "demo-resourcequota" created
现在,Kubernetes 将阻止任何可能超出 demo 命名空间配额的 API 操作。例如,ResourceQuota 的示例将该命名空间的 Pod 限制为 100 个,因此如果已经有 100 个 Pod 在运行,并且尝试启动一个新的 Pod,您将看到如下错误消息:
Error from server (Forbidden): pods "demo" is forbidden: exceeded quota:
demo-resourcequota, requested: pods=1, used: pods=100, limited: pods=100
使用 ResourceQuotas 是阻止一个命名空间中的应用程序获取过多资源并使集群其他部分资源匮乏的好方法。
您还可以限制命名空间中 Pod 的总 CPU 和内存使用量。这对于在许多不同团队共享 Kubernetes 集群的大型组织中进行预算编制非常有用。团队可能需要设置他们将用于其命名空间的 CPU 数量,如果超出配额,他们将无法使用更多集群资源,直到增加 ResourceQuota。
Pod 限制对于防止配置错误或输入错误导致生成大量可能无限的 Pod 非常有用。很容易忘记从常规任务中清理某些对象,并且有一天发现您的集群中有成千上万个这样的对象。
最佳实践
在每个命名空间使用 ResourceQuotas 来强制限制可以在该命名空间运行的 Pod 数量。
要检查特定命名空间中是否激活了 ResourceQuota,请使用 kubectl describe resourcequotas 命令:
`kubectl describe resourcequota -n demo`
Name: demo-resourcequota Namespace: demo Resource Used Hard -------- ---- ---- pods 1 100
默认资源请求和限制
预先了解您的容器资源需求并不总是容易的。您可以使用 LimitRange 资源为命名空间中的所有容器设置默认请求和限制:
apiVersion: v1
kind: LimitRange
metadata:
name: demo-limitrange
spec:
limits:
- default:
cpu: "500m"
memory: "256Mi"
defaultRequest:
cpu: "200m"
memory: "128Mi"
type: Container
提示
与 ResourceQuotas 类似,LimitRange 的 metadata.name 可以是您想要的任何内容。例如,它不对应 Kubernetes 命名空间。只有在您将清单应用于特定命名空间时,LimitRange 或 ResourceQuota 才会生效。
任何在命名空间中不指定资源限制或请求的容器将从 LimitRange 继承默认值。例如,未指定 cpu 请求的容器将从 LimitRange 继承 200m 的值。类似地,未指定 memory 限制的容器将从 LimitRange 继承 256Mi 的值。
理论上,您可以在 LimitRange 中设置默认值,而不必为各个容器指定请求或限制。但这并不是好的做法:应该能够查看容器规范并了解其请求和限制,而不必知道是否存在 LimitRange。只使用 LimitRange 作为防范措施,以防容器所有者忘记指定请求和限制而产生问题。
最佳实践
在每个命名空间中使用 LimitRanges 设置容器的默认资源请求和限制,但不要依赖它们;把它们视为最后的手段。始终在容器规范本身中指定明确的请求和限制。
优化集群成本
在《集群大小和扩展》(ch06.html#sizing)中,我们概述了选择集群初始大小以及随着工作负载演变而进行扩展的一些考虑因素。但是,假设您的集群大小正确,并且具有足够的容量,那么如何以最具成本效益的方式运行它呢?
Kubecost
当多个应用程序和团队共享同一集群时,了解运行 Kubernetes 基础设施所涉及的总成本往往是困难的。
幸运的是,有一个名为 Kubecost 的工具可用于跟踪每个命名空间、标签甚至容器级别的成本。 Kubecost 目前免费提供单个集群版本,并提供支持更大环境的付费版本。
优化部署
你真的需要这么多副本吗?这似乎是一个显而易见的问题,但是集群中的每个 Pod 都会使用一些资源,这些资源因此无法提供给其他一些 Pod。
有时为了确保个别 Pods 失效时服务质量不会降低,或在滚动升级期间,会诱人地为所有内容运行大量副本。此外,副本越多,您的应用程序可以处理的流量就越多。
但是您应该明智地使用副本。您的集群只能运行有限数量的 Pods。将它们分配给真正需要最大可用性和性能的应用程序。
如果在升级期间某个 Deployment 短暂下线并不重要,那么它就不需要很多副本。令人惊讶的是,大量的应用程序和服务完全可以仅靠一个或两个副本运行得非常好。
检查每个 Deployment 配置的副本数量,并询问:
-
这项服务的性能和可用性的业务需求是什么?
-
我们能用更少的副本满足这些要求吗?
如果一个应用程序在处理需求时遇到困难,或者在升级 Deployment 时用户遇到太多错误,那么它需要更多副本。但在许多情况下,在性能下降开始显著之前,您可以显著减少 Deployment 的规模。
在《自动扩展》(ch06.html#autoscaling)的后续部分中,我们将介绍如何利用自动缩放在您知道使用率低时节省成本的方法。
最佳实践
对于满足性能和可用性需求的给定 Deployment,请使用最少数量的 Pods。逐渐减少副本数量,直到刚好满足您的服务级别目标为止。
优化 Pods
在本章的前面,我们强调了为容器设置正确的资源请求和限制的重要性,在“资源请求”部分。如果资源请求过小,你很快就会知道:Pods 开始失败。然而,如果它们过大,你第一次知道可能是在收到每月的云账单时。
你应该定期审查各种工作负载的资源请求和限制,并将它们与实际使用情况进行比较。
大多数托管的 Kubernetes 服务都提供一种仪表板,显示容器随时间的 CPU 和内存使用情况——我们将在“监控集群状态”中详细了解更多。
你也可以使用 Prometheus 和 Grafana 构建自己的仪表板和统计信息,我们将在第十五章中详细介绍这一点。
设置最佳的资源请求和限制有点像艺术,对每种工作负载的答案都会有所不同。有些容器可能大部分时间处于空闲状态,偶尔会因处理请求而使资源使用率急剧上升;而其他一些可能会持续忙碌,并逐渐使用更多内存直至达到限制。
一般情况下,你应该将容器的资源限制设置在它在正常运行中使用的最大值略高一点。例如,如果某个容器在几天内的内存使用从未超过 500 MiB,你可以将其内存限制设置为 600 MiB。
注意
到底应不应该为容器设置限制?一派观点认为,在生产环境中容器不应该有任何限制,或者限制应该设置得非常高,以至于容器永远不会超过它们。对于非常大和资源密集型的容器而言,这可能有些道理,但我们认为还是最好设置限制。没有限制的话,一个有内存泄漏或使用过多 CPU 的容器可能会吞噬节点上所有可用的资源,从而使其他容器陷入饥饿状态。
为了避免“资源吞噬者”的情况,将容器的限制设置为正常使用量的略高一点。这将确保只要容器正常工作,就不会被杀死,但如果出现问题,仍会最大限度地减少影响范围。
请求设置比限制更不那么关键,但也不应设置得过高(因为 Pod 将永远不会被调度),或者过低(因为超出请求的 Pods 会首先被驱逐)。
垂直 Pod 自动缩放器
Kubernetes 有一个名为垂直 Pod 自动缩放器的插件,可以帮助你确定资源请求的理想值。它会监视指定的部署,并根据实际使用情况自动调整其 Pod 的资源请求。它有一个 dry-run 模式,只会提出建议,而不会实际修改正在运行的 Pods,这可能非常有帮助。
优化节点
Kubernetes 可以处理各种节点大小,但某些节点的性能可能会优于其他节点。为了获得最佳的集群容量性价比,你需要观察你的节点在实际需求条件下的表现,特别是在特定的工作负载下。这将帮助你确定最具成本效益的实例类型。
值得记住的是,每个节点都必须安装操作系统,这会消耗磁盘、内存和 CPU 资源。Kubernetes 系统组件和容器运行时也是如此。节点越小,这些开销在其总资源中所占比例越大。
因此,较大的节点可以更具成本效益,因为它们的更多资源可用于你的工作负载。但需要注意的是,丢失一个单独的节点会对集群的可用容量产生更大的影响。
较小的节点还具有更高百分比的滞留资源:未使用的内存空间和 CPU 时间的片段,但任何现有的 Pod 都无法使用它们。
一个很好的经验法则是节点应该足够大,可以运行至少五个典型的 Pod,以保持被滞留资源的比例在 10%或更低。如果节点可以运行 10 个或更多的 Pod,则滞留资源将低于 5%。
Kubernetes 的默认限制是每个节点最多支持 110 个 Pod。尽管可以通过调整kubelet的--max-pods设置来增加此限制,但某些托管服务可能不支持此操作,建议除非有强烈理由,否则最好遵循 Kubernetes 的默认设置。
每个节点的 Pod 限制意味着你可能无法利用云服务提供商的最大实例大小。相反,考虑运行一些更多的较小节点,以获得更好的利用率。例如,与其运行 8 vCPUs 的 6 个节点,不如运行 4 vCPUs 的 12 个节点。
提示
查看每个节点的资源利用率百分比,可以使用你的云服务提供商的仪表板或kubectl top nodes命令。使用中央处理器(CPU)百分比越大,利用率就越高。如果你的集群中较大的节点利用率更高,你可能会建议移除一些较小的节点,并用较大的节点替换它们。
另一方面,如果较大的节点利用率低,你的集群可能过载,因此可以移除一些节点或将其变小,从而减少总成本。
最佳实践
较大的节点通常更具成本效益,因为系统开销消耗它们资源的比例较低。根据你的集群的实际使用情况来选择节点大小,目标是每个节点支持 10 到 100 个 Pod。
优化存储
一个经常被忽视的云成本是磁盘存储的成本。云服务提供商为其不同的实例大小提供不同数量的磁盘空间,并且大规模存储的价格也不同。
虽然通过 Kubernetes 的资源请求和限制可以实现相当高的 CPU 和内存利用率,但对于存储来说却并非如此,许多集群节点在磁盘空间方面明显过度配置。
不仅许多节点的存储空间超过了他们的需求,存储类别也可能是一个因素。大多数云提供商根据每秒 I/O 操作数(IOPS)或带宽分配不同类别的存储。
例如,使用持久磁盘卷的数据库通常需要非常高的 IOPS 评级,以便快速、高吞吐量地访问存储。这是昂贵的。通过为不需要那么多带宽的工作负载提供低 IOPS 存储,你可以节省云成本。另一方面,如果你的应用因为花费大量时间等待存储 I/O 而表现不佳,你可能需要提供更多的 IOPS 来处理这个问题。
你的云端或 Kubernetes 提供商控制台通常可以显示你的节点实际使用了多少 IOPS,你可以利用这些数据来帮助决定在哪里削减成本。
理想情况下,你应该能够为需要高带宽或大量存储的容器设置资源请求。然而,目前 Kubernetes 并不支持这一点,尽管未来可能会添加对 IOPS 请求的支持。
最佳实践
不要使用比你实际需要更多存储的实例类型。根据你实际使用的吞吐量和空间,提供尽可能小的、IOPS 最低的磁盘卷。
清理未使用的资源
随着你的 Kubernetes 集群的扩展,你会发现许多未使用或者遗失的资源潜伏在黑暗的角落里。随着时间的推移,如果这些遗失的资源不被清理掉,它们将开始占据你整体成本的重要部分。
在最高层面上,你可能会发现一些云实例并不属于任何一个集群;当一台机器不再使用时,很容易忘记终止它。
其他类型的云资源,如负载均衡器、公共 IP 和磁盘卷,即使未被使用也会花费你的资金。你应定期审查每种资源类型的使用情况,以找到并移除未使用的实例。
同样地,在你的 Kubernetes 集群中可能有一些 Deployments 和 Pods 实际上并没有被任何 Service 引用,因此无法接收流量。
即使未运行的容器镜像也会占据节点的磁盘空间。幸运的是,当节点开始磁盘空间不足时,Kubernetes 会自动清理未使用的镜像。^(1)
使用所有者元数据
减少未使用资源的一个有用方法是制定一个组织范围的政策,要求每个资源都必须附带有关其所有者的信息标签。你可以使用 Kubernetes 的注解来实现这一点(参见“标签和注解”)。
例如,你可以像这样为每个 Deployment 添加注解:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-brilliant-app
annotations:
example.com/owner: "Customer Apps Team"
...
所有者元数据应指定联系此资源的人员或团队。这无论如何都很有用,但对于识别被弃用或未使用的资源尤为方便。(请注意,最好使用您公司的域名作为自定义注释的前缀,例如example.com,以防止与其他可能具有相同名称的注释发生冲突。)
您可以定期查询集群中所有没有所有者注释的资源,并列出它们以供潜在终止。特别严格的政策可能会立即终止所有没有所有者的资源。但是,刚开始时不要太严格:开发者的好意至关重要,甚至比集群容量更为重要。
最佳实践
在所有资源上设置所有者注释,提供有关在资源出现问题时应联系的人员信息,或者如果看起来资源已被弃用且可能要终止。
查找未充分利用的资源
一些资源可能接收到非常低水平的流量,甚至没有。也许它们由于标签变更而与服务前端断开连接,或者可能是临时的或实验性的。
每个 Pod 应该公开其接收请求的数量作为指标(参见第十六章了解更多信息)。利用这些指标找出接收流量低或零的 Pods,并列出可能可以终止的资源。
您还可以在网页控制台上检查每个 Pod 的 CPU 和内存利用率,并找出集群中利用率最低的 Pods。那些什么也不做的 Pods 可能不是资源的好利用方式。
如果 Pods 具有所有者元数据,请联系它们的所有者,了解这些 Pods 是否真的需要(例如,它们可能是一个仍在开发中的应用程序)。
您可以使用另一个自定义的 Kubernetes 注释(例如 example.com/lowtraffic)来识别未收到请求但由于某种原因仍然需要的 Pods。
最佳实践
定期审查您的集群,找出未充分利用或被遗弃的资源并将其清理。所有者注释可以提供帮助。
清理已完成的作业
Kubernetes 作业(参见“作业”)是只运行一次并且不会重新启动的 Pods。然而,作业对象仍然存在于 Kubernetes 数据库中,一旦完成的作业数量达到一定量,这可能会影响 API 的性能。您可以通过 ttlSecondsAfterFinished 设置告诉 Kubernetes 在作业完成后自动删除它们:
apiVersion: batch/v1
kind: Job
metadata:
name: demo-job
spec:
ttlSecondsAfterFinished: 60
template:
spec:
containers:
...
在这个例子中,当您的作业完成时,将在 60 秒后自动删除。
检查备用容量
集群中应始终保留足够的备用容量以处理单个工作节点的故障。要检查这一点,请尝试排空您的最大节点(参见 “缩减”)。一旦所有 Pod 已从节点驱逐出去,请检查所有应用程序是否仍然以配置的副本数处于工作状态。如果不是这样,则需要向集群添加更多的容量。
如果在节点故障时没有足够的空间重新安排其工作负载,您的服务最多可能会受到降级,最坏情况下可能会不可用。
使用预留实例
一些云服务提供商根据机器的生命周期提供不同的实例类别。预留 实例在价格和灵活性之间进行权衡。
例如,AWS 的预留实例价格约为 按需 实例(默认类型)的一半。您可以为不同的时间段(一年、三年等)预留实例。AWS 的预留实例具有固定的大小,因此如果在三个月后发现需要更大的实例,您的预订将大部分被浪费。
Google Cloud 的预留实例等同于 承诺使用折扣,允许您预付一定数量的虚拟 CPU 和内存。这比 AWS 的预约更加灵活,因为您可以使用比预定资源更多的资源;您只需为未被预订的部分支付正常的按需价格。
当您知道未来的需求时,预留实例和承诺使用折扣可能是一个不错的选择。然而,对于您最终没有使用的预订,没有退款,并且您必须提前支付整个预订期限的费用。因此,您应该只选择预订实例的期限,期间您的需求不太可能发生显著变化。
然而,如果您能提前一两年进行规划,使用预留实例可能会带来可观的节省。
最佳实践
当您的需求未来一两年内不太可能改变时,请使用预留实例——但明智地选择您的预订,因为一旦进行了预订,它们就无法更改或退款。
使用抢先(竞价)实例
竞价 实例(AWS 的称呼)或谷歌术语中的 抢先 VM 提供了不保证可用性的服务,并且其生命周期通常有限。因此,它们代表了价格和可用性之间的权衡。
竞价实例价格便宜,但可能随时暂停或恢复,并可能完全终止。幸运的是,Kubernetes 设计为在失去单个集群节点时提供高可用性服务。
可变价格或可变抢占
因此,竞价实例可以是您集群的一种具有成本效益的选择。对于 AWS 竞价实例,每小时的定价根据需求变化。当某个特定区域和可用性区域内某种实例类型的需求高时,价格将上涨。
另一方面,Google Cloud 的抢占式 VM 是按固定费率计费,但抢占率会有所变化。Google 表示,平均而言,每周大约有 5-15% 的节点会被抢占。但抢占式 VM 的价格可能比按需实例便宜高达 80%,具体取决于实例类型。
抢占节点可以将您的成本减少一半
因此,使用抢占节点来减少 Kubernetes 集群的成本可以是一种非常有效的方法。虽然您可能需要运行更多的节点来确保您的工作负载能够抵御抢占,但有人说总体而言每个节点的成本可以降低 50%。
您可能还会发现使用抢占节点是将一些混乱工程引入您的集群的好方法(参见“混乱测试”)—前提是您的应用程序首先准备好接受混乱测试。
请记住,您始终应该有足够的非抢占节点来处理集群的最小工作负载。永远不要赌上您无法承受失去的东西。如果您有大量抢占节点,可能最好使用集群自动缩放,以确保任何抢占的节点尽快被替换(参见“自动缩放”)。
理论上,所有您的抢占节点可能会同时消失。因此,尽管可以节省成本,但建议将您的抢占节点数量限制在集群的三分之二以下,比如说。
最佳实践
通过使用抢占式或 spot 实例来降低成本,但不要超出您能承受的范围。也始终保留一些非抢占节点。
使用节点亲和性来控制调度
您可以使用 Kubernetes 节点亲和性 来确保无法容忍故障的 Pod 不会被调度到抢占节点上(参见“节点亲和性”)。
例如,Google Kubernetes Engine (GKE) 的抢占节点带有标签 cloud.google.com/gke-preemptible。要告诉 Kubernetes 永远不要在这些节点上调度 Pod,请将以下内容添加到 Pod 或 Deployment 规范中:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: cloud.google.com/gke-preemptible
operator: DoesNotExist
requiredDuringScheduling... 亲和性是强制性的:具有此亲和性的 Pod 将永远不会被调度到不匹配选择器表达式的节点上(称为硬亲和性)。
或者,您可能希望告诉 Kubernetes 您的一些次要 Pod,可以容忍偶尔的故障,应优先在抢占节点上调度。在这种情况下,您可以使用相反意义的软亲和性:
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- preference:
matchExpressions:
- key: cloud.google.com/gke-preemptible
operator: Exists
weight: 100
这实际上意味着“如果可以的话,请在可抢占节点上调度此 Pod;如果不行,那也无妨。”
最佳实践
如果您正在运行抢占节点,请使用 Kubernetes 节点亲和性确保关键工作负载不会被抢占。
保持工作负载的平衡
我们已经讨论了 Kubernetes 调度器所做的工作,确保工作负载尽可能平均地分布在许多节点上,并尝试将副本 Pod 放置在不同的节点上以实现高可用性。
总体而言,调度器表现出色,但还有一些边缘情况需要注意。
举个例子,假设你有两个节点,以及两个服务 A 和 B,每个服务都有两个副本。在一个平衡的集群中,每个节点上会有一个服务 A 的副本,以及一个服务 B 的副本(图 5-1)。如果一个节点发生故障,服务 A 和 B 仍然可用。

图 5-1. 服务 A 和 B 在可用节点上保持平衡
到目前为止,一切都很好。但假设节点 2 确实发生故障。调度器会注意到服务 A 和 B 都需要一个额外的副本,而现在只有一个节点可以为它们创建,因此它就这么做了。现在节点 1 上运行着两个服务 A 的副本和两个服务 B 的副本。
现在假设我们启动一个新节点来取代故障的节点 2。即使它可用,上面也不会有任何 Pod。调度器从不将正在运行的 Pod 从一个节点移动到另一个节点。
现在我们有一个不平衡的集群,所有的 Pod 都在节点 1 上,而节点 2 上没有任何 Pod(图 5-2)。

图 5-2. 节点 2 故障后,所有副本都迁移到节点 1
更糟糕的是,假设你对服务 A 部署了一个滚动更新(我们称新版本为服务 A)。调度器需要为服务 A 启动两个新的副本,等待它们启动完成,然后终止旧的副本。新的副本将在哪里启动?在新的节点 2 上,因为它是空闲的,而节点 1 已经运行了四个 Pod。因此,两个新的服务 A* 的副本在节点 2 上启动,而旧的副本从节点 1 上移除(图 5-3)。

图 5-3. 在服务 A* 上线后,集群仍然不平衡
现在你处于一个糟糕的情况中,因为服务 B 的两个副本都在同一个节点(节点 1),而服务 A* 的两个副本也在同一个节点(节点 2)。虽然你有两个节点,但没有高可用性。无论是节点 1 还是节点 2 的故障都会导致服务中断。
这个问题的关键在于,调度器永远不会将运行中的 Pod 从一个节点移动到另一个节点,除非出于某种原因需要重新启动。此外,调度器旨在将工作负载均匀分布在节点上的目标,有时会与维护个别服务的高可用性发生冲突。
解决这个问题的一种方法是使用名为Descheduler的工具。您可以定期运行此工具作为 Kubernetes 作业,它将尽力通过找到需要移动的 Pod 并杀死它们来重新平衡集群。
Descheduler 具有各种您可以配置的策略和政策。例如,一个策略寻找未充分利用的节点,并杀死其他节点上的 Pod 以强制它们重新调度到空闲节点上。
另一个策略寻找重复的 Pod,即在同一节点上运行两个或更多个相同 Pod 的副本,并将其驱逐。这解决了我们示例中出现的问题,即工作负载在名义上平衡,但实际上两个服务都不是高度可用的。
摘要
Kubernetes 在可靠、高效的方式下运行工作负载方面做得相当不错,几乎不需要手动干预。只要提供给调度器准确的容器资源需求估计,您基本上可以让 Kubernetes 自行处理。
您本来会花在解决运营问题上的时间现在可以更好地利用,比如开发应用程序。谢谢,Kubernetes!
了解 Kubernetes 如何管理资源是构建和正确运行集群的关键。最重要的要点是:
-
Kubernetes 根据请求和限制为容器分配 CPU 和内存资源。
-
容器的请求是其运行所需的最小资源量。其限制指定其允许使用的最大量。
-
最小化的容器镜像构建、推送、部署和启动速度更快。容器越小,潜在的安全漏洞就越少。
-
存活探针告诉 Kubernetes 容器是否正常工作。如果容器的存活探针失败,它将被杀死并重新启动。
-
就绪探针告诉 Kubernetes 容器已准备好并能够提供请求。如果就绪探针失败,容器将从引用它的任何服务中移除,使其与用户流量断开连接。
-
启动探针类似于存活探针,但仅用于确定应用程序是否已完成启动并准备好存活探针接管检查状态。
-
PodDisruptionBudgets 允许您限制一次停止的 Pod 数量,以保持应用程序的高可用性。
-
命名空间是逻辑上划分集群的一种方式。您可以为每个应用程序或一组相关应用程序创建一个命名空间。
-
要引用另一个命名空间中的服务,您可以使用类似于这样的 DNS 地址:
SERVICE.NAMESPACE。 -
ResourceQuotas 允许您为给定命名空间设置整体资源限制。
-
LimitRanges 指定命名空间中容器的默认资源请求和限制。
-
设置资源限制,使您的应用程序几乎达到但不超过正常使用中的限制。
-
不要分配比您所需更多的云存储,并且除非对您的应用程序性能至关重要,否则不要配置高带宽存储。
-
在所有资源上设置所有者注释,并定期扫描集群以查找无主资源。
-
查找并清理不再使用的资源(但请与它们的所有者确认)。
-
如果能够长期规划您的使用情况,预留实例可以为您节省费用。
-
预留实例可以立即为您节省费用,但要做好它们突然消失的准备。使用节点亲和性,将对故障敏感的 Pod 保持远离预留节点。
^(1) 您可以通过调整kubelet垃圾回收设置来自定义此行为。
第六章:操作集群
如果俄罗斯方块教会了我什么,那就是错误会积累起来,成就却会消失。
Andrew Clay Shafer
一旦您拥有了一个 Kubernetes 集群,如何确保它运行良好并且在需求增加时进行扩展,同时保持云成本最低?在本章中,我们将探讨运行 Kubernetes 集群以处理生产工作负载所涉及的问题,以及一些可以帮助您的工具。
正如我们在第三章中看到的,关于您的 Kubernetes 集群有许多重要事项需要考虑:可用性、认证、升级等等。如果您正在使用我们推荐的良好托管的 Kubernetes 服务,大部分这些问题应该已经为您解决了。
然而,您对集群实际做什么取决于您。在本章中,您将学习如何确定集群的大小和扩展,检查它的符合性,以及使用混沌猴测试基础设施的弹性。
集群大小和扩展
您的集群需要多大?对于自托管的 Kubernetes 集群和几乎所有托管服务来说,集群的持续成本直接取决于其节点的数量和大小。如果集群的容量过小,您的工作负载将无法正常运行,或在高负载下失败。如果容量过大,您将浪费金钱。
适当地确定集群的大小和扩展非常重要,所以让我们看看涉及到的一些决策。
容量规划
估算您需要的容量的一种方法是考虑运行相同应用程序所需的传统服务器数量。例如,如果您当前的架构在 10 个独立的云虚拟机实例上运行,您可能不需要超过 10 个类似大小的节点在您的 Kubernetes 集群中运行相同的工作负载,再加上另外 1 或 2 个用于冗余。事实上,您可能甚至不需要那么多,因为 Kubernetes 将均匀地在所有机器上平衡容器,因此可以实现比传统服务器更高的利用率。但是,要为优化容量调整您的集群可能需要一些时间和实际经验。
最小的集群
当您首次设置一个集群时,您可能会用它来玩耍、实验,并弄清楚如何运行您的应用程序。因此,在您确定需要多少容量之前,您可能不需要在大型集群上花费大量资金。
最小可能的 Kubernetes 集群是单节点。这将允许您尝试 Kubernetes 并运行开发中的小工作负载,就像我们在第二章中看到的那样。然而,单节点集群在节点硬件故障或 Kubernetes API 服务器或 kubelet(负责在每个节点上运行工作负载的代理守护进程)失败时没有恢复能力。
如果你正在使用像 GKE、EKS 或 AKS 这样的托管 Kubernetes 服务(参见“托管的 Kubernetes 服务”),那么你不需要担心控制平面节点的预配问题:这些已经为你完成了。另一方面,如果你正在构建自己的集群,你需要决定如何布置控制平面。
构建一个具有弹性的 Kubernetes 集群的最低控制平面节点数量为三个。一个不够弹性,两个可能在哪个是领导者上发生分歧,因此至少需要三个节点。
虽然你可以在这样小的 Kubernetes 集群上进行有用的工作,但并不推荐这样做。更好的做法是增加一些工作节点,以便你自己的工作负载不会与 Kubernetes 控制平面竞争资源。
只要你的集群控制平面高度可用,你可以只用一个工作节点,但至少两个节点是合理的最低要求,以保护节点故障并允许 Kubernetes 运行每个 Pod 的至少两个副本。节点数量越多越好,尤其是因为 Kubernetes 调度程序不能始终确保工作负载在所有可用节点上完全平衡(参见“保持你的工作负载平衡”)。
K3S
当涉及到轻量级集群时,像K3s这样的工具是值得一试的。它将所有的 Kubernetes 组件打包到一个单一的二进制文件中,非常适合在隔离或资源受限的环境中使用。
最佳实践
Kubernetes 集群至少需要三个节点来运行控制平面组件,以实现高可用性,对于处理更大的集群工作负载,可能需要更多节点。两个工作节点是确保你的工作负载对单个节点故障具有容错能力的最低要求,而三个工作节点则更好。
最大的集群
Kubernetes 集群的规模是否有限制?简短的回答是有,但你几乎肯定不必担心;Kubernetes 1.22 版本正式支持多达 5,000 个节点的集群。
因为集群需要节点之间的通信,可能的通信路径数量以及基础数据库的累积负载随集群大小呈指数增长。虽然 Kubernetes 在超过 5,000 个节点时可能仍然能够正常工作,但不能保证能够处理生产工作负载或响应足够快。
Kubernetes 文档建议,支持的集群配置最多不超过 5,000 个节点,总共不超过 150,000 个 Pods,总共不超过 300,000 个容器,并且每个节点最多不超过 100 个 Pods。值得注意的是,集群越大,对控制平面节点的负载就越大;如果你负责运行自己的控制平面,它们需要非常强大的机器来应对数千个节点的集群。
最佳实践
为了最大可靠性,请确保您的 Kubernetes 集群小于 5,000 个节点和 150,000 个 Pod(对大多数用户来说并非问题)。如果您需要更多资源,请运行多个集群。
联邦集群
如果您有极具挑战性的工作负载或需要在大规模运行,这些限制可能会成为您的实际问题。在这种情况下,您可以运行多个 Kubernetes 集群,并在必要时联邦它们,以便工作负载可以在集群之间复制。
联邦提供了在两个或多个集群之间保持同步运行相同工作负载的能力。如果您需要在不同的云提供商、为了弹性或在不同的地理位置使用 Kubernetes 集群,则此功能可能非常有用,以减少用户的延迟。即使单个集群失败,联邦集群组也可以继续运行。
您可以在 Kubernetes 文档中详细了解集群联邦。
对于大多数 Kubernetes 用户来说,联邦不是他们需要关注的事项,在实践中,大多数非常大规模的用户能够通过多个未联邦的每个几百到几千个节点的集群来处理他们的工作负载。通常,与大型集中式联邦集群相比,为团队或应用程序边界提供更容易管理的较小分隔集群是更好的选择。
最佳实践
如果您需要在多个集群之间复制工作负载,可能是为了地理冗余或延迟原因,请使用联邦。然而,大多数用户不需要将他们的集群联合起来。
我需要多个集群吗?
除非您在非常大规模上运作,如我们在前一节和“多云 Kubernetes 集群”中提到的,您可能不需要超过一个或两个集群:也许一个用于生产,一个用于暂存和测试。
为了便利和资源管理的简易性,您可以使用命名空间将您的集群划分为逻辑分区,我们在“使用命名空间”中详细介绍了这一点。除了少数情况外,管理多个集群通常不值得管理开销。
在某些特定情况下,例如安全性和法规合规性,您可能希望确保一个集群中的服务绝对与另一个集群中的服务隔离(例如处理受保护健康信息时,或由于法律原因无法从一个地理位置传输数据到另一个地理位置)。在这些情况下,您需要创建单独的集群。对于大多数 Kubernetes 用户来说,这不会成为问题。
最佳实践
除非您真正需要完全隔离一个工作负载或团队的另一个工作负载或团队,否则请使用单个生产和单个暂存集群。如果您只是想要将集群分区以便管理,请改用命名空间。
节点和实例
给定节点的容量越大,它可以执行的工作就越多,容量以可用的 CPU 核心数(虚拟或其他方式)、可用内存和较少的磁盘空间来表达。但例如,是否更好地运行 10 个非常大的节点,而不是 100 个较小的节点?
选择正确的节点大小
Kubernetes 集群的节点大小没有普遍适用的标准。答案取决于您的云或硬件提供商以及您特定的工作负载。
不同实例大小的每单位容量成本可能会影响您决定节点大小的方式。例如,一些云提供商可能会为较大的实例大小提供轻微折扣,因此如果您的工作负载非常计算密集,可能比在许多较小的节点上运行它们更便宜。
集群中所需的节点数量也会影响节点大小的选择。要获得 Kubernetes 提供的诸如 Pod 复制和高可用性等优势,您需要将工作分布在多个节点上。但如果节点有太多空闲容量,那将是浪费金钱。
如果您需要至少 10 个节点以实现高可用性,但每个节点只需要运行几个 Pod,则节点实例可以非常小。另一方面,如果您只需要两个节点,您可以使它们非常大,并且可能通过更有利的实例定价节省金钱。
最佳实践
使用提供商提供的最经济节点类型。通常,更大的节点会更便宜,但如果您只有少数节点,您可能希望添加一些较小的节点,以增加冗余性。
云实例类型
因为 Kubernetes 组件本身(例如 kubelet)使用了一定量的资源,并且您需要一些备用容量来执行有用的工作,所以您的云提供商提供的最小实例大小可能不适合 Kubernetes。
对于小型集群(总节点数约为五个以内),控制平面应至少具有两个 CPU 和两个 GiB 内存,较大的集群则需要更多的内存和 CPU 以供每个控制平面节点使用。
对于较大的集群,可能有几十个节点,为您配置两到三种不同的实例大小可能是有意义的。这意味着 Kubernetes 可以在大节点上安排需要大量内存的计算密集工作负载的 Pod,从而使较小的节点可以处理较小的 Pod(见“节点亲和性”)。这样做可以使 Kubernetes 调度程序在决定在哪里运行给定的 Pod 时具有最大的自由选择。
异构节点
并非所有节点都是相同的。您可能需要一些具有特殊属性的节点,比如图形处理单元(GPU)。GPU 是高性能的并行处理器,广泛用于计算密集型问题,与图形无关,比如机器学习或数据分析。
您可以使用 Kubernetes 中的资源限制功能(参见“资源限制”)指定某个 Pod 至少需要一个 GPU,例如。这将确保这些 Pod 仅在启用 GPU 的节点上运行,并优先于可以在任何节点上运行的 Pod。
大多数 Kubernetes 节点可能运行各种 Linux,适用于几乎所有应用程序。请记住,容器不是虚拟机,因此容器内部的进程直接在底层节点的操作系统内核上运行。例如,Windows 二进制文件无法在 Linux Kubernetes 节点上运行,因此如果需要运行 Windows 容器,必须为其提供 Windows 节点。
最佳实践
大多数容器都是为 Linux 构建的,因此您可能希望主要运行基于 Linux 的节点。您可能需要添加一两种特殊类型的节点来满足特定的需求,例如 GPU 或 Windows。
裸金属服务器
Kubernetes 最有用的一个特性之一是其能力,即连接各种大小、架构和能力不同的机器,提供一个统一的、逻辑的机器,可以在其上运行工作负载。虽然 Kubernetes 通常与云服务器相关联,但许多组织在数据中心拥有大量物理裸金属机器,这些机器可能潜在地被整合成 Kubernetes 集群中。
我们在第一章中看到,云技术将资本支出基础设施(作为资本支出购买机器)转变为运营支出基础设施(作为运营支出租用计算能力),这在财务上是合理的。但是,如果你的企业已经拥有大量裸金属服务器,你不需要立刻将它们写下:相反,考虑将它们加入到 Kubernetes 集群中(参见“裸金属和本地”)。
最佳实践
如果您有硬件服务器有空余容量,或者您尚未完全准备好完全迁移到云上,可以使用 Kubernetes 在现有机器上运行容器工作负载。
扩展集群
选择合理的集群初始大小,并选择合适的工作节点实例大小的正确组合,这就是结局了吗?几乎肯定不是:随着时间的推移,您可能需要根据需求变化或业务需求来扩展或缩小集群。
实例组
向 Kubernetes 集群添加节点非常容易。如果您运行的是自托管集群,可以使用 kops 等集群管理工具(参见“kops”)来执行此操作。kops 具有实例组的概念,这是一组特定实例类型的节点(例如,m5.large)。托管服务如 GKE 也具有相同的功能,称为节点池。Elastic Kubernetes Service (EKS) 工具 eksctl 将此概念称为nodegroup。
您可以通过更改组的最小和最大大小,或更改指定的实例类型,或同时更改两者来缩放实例组或节点池。
缩减
原则上,缩减 Kubernetes 集群也没有问题。你可以告诉 Kubernetes排空你想要移除的节点,这将逐渐关闭或将运行中的 Pods 移动到其他地方。
大多数集群管理工具将会自动为您执行节点排空,或者您可以使用kubectl drain命令自行执行。一旦成功排空节点并且在集群的其他部分有足够的空闲容量来重新调度被注定的 Pods,您可以终止它们。
为了避免过度减少给定服务的 Pod 副本数,您可以使用 PodDisruptionBudgets 指定可用 Pod 的最小数量,或者任何时候可以不可用的最大 Pod 数(参见“Pod Disruption Budgets”)。
如果排空节点会导致 Kubernetes 超出这些限制,排空操作将阻塞,直到您更改限制或在集群中释放更多资源。
排空允许 Pods 优雅地关闭,清理自身并保存必要的状态。对于大多数应用程序来说,这比简单关闭节点更可取,后者会立即终止 Pods。
最佳实践
当您不再需要节点时,请不要直接关闭它们。首先进行排空以确保它们的工作负载迁移到其他节点,并确保集群中仍有足够的剩余容量。
自动缩放
大多数云服务提供商支持自动缩放:根据某些度量标准或时间表自动增加或减少实例的数量。例如,AWS 自动缩放组(ASG)可以维护一定数量的最小和最大实例,以便如果一个实例失败,将启动另一个实例来替代它,或者如果运行的实例过多,则关闭一些实例。
或者,如果您的需求根据时间的不同波动,您可以安排组在指定的时间增长和收缩。您还可以根据需要动态配置扩展组的缩放:例如,如果平均 CPU 利用率在 15 分钟内超过 90%,则可以自动添加实例,直到 CPU 使用率低于阈值。需求再次降低时,可以缩小组来节省成本。
Kubernetes 有一个 Cluster Autoscaler 附加组件,像 kops 这样的集群管理工具可以利用它来实现云自动缩放,而像 AKS 这样的托管集群也提供自动缩放的功能。
然而,要正确设置自动缩放的参数可能需要一些时间和实验,并且对许多用户来说可能根本不需要。大多数 Kubernetes 集群从小规模开始,并逐渐通过在资源使用增长时逐个添加节点来单调增长。
对于大规模用户或需求高度变化的应用程序来说,集群自动缩放是一个非常有用的功能。
最佳实践
不要因为功能存在就启用集群自动缩放,除非你已经确定你需要它。除非你的需求或工作负载非常变化,否则你可能不需要它。首先通过手动缩放你的集群,并且在了解你的规模需求随时间变化的感觉时逐渐习惯于监控使用情况。
一致性检查
Kubernetes 不是 Kubernetes?Kubernetes 的灵活性意味着有许多不同的方法来设置 Kubernetes 集群,这可能会导致潜在问题。如果 Kubernetes 要成为一个通用平台,你应该能够将工作负载在任何 Kubernetes 集群上运行,并且它应该按照你期望的方式工作。这意味着相同的 API 调用和 Kubernetes 对象必须可用,它们必须具有相同的行为,它们必须按照广告的方式工作,等等。
幸运的是,Kubernetes 本身包含一个测试套件,用于验证给定 Kubernetes 集群是否 符合规范;也就是说,它满足了给定 Kubernetes 版本的一组核心要求。这些一致性测试对于 Kubernetes 管理员非常有用。
如果你的集群未通过这些测试,那么你的设置中存在问题需要解决。如果它通过了测试,知道它是符合规范的可以让你确信为 Kubernetes 设计的应用程序将与你的集群一起工作,并且你在集群上构建的东西也将在其他地方正常工作。
CNCF 认证
云原生计算基金会(CNCF)是 Kubernetes 项目和商标的官方所有者(见 “云原生”),并且为与 Kubernetes 相关的产品、工程师和供应商提供各种认证。
Certified Kubernetes
如果你使用托管或部分托管的 Kubernetes 服务,请检查它是否带有 Certified Kubernetes 标志和标志(见图 6-1)。这表示供应商和服务符合 CNCF 规定的 Certified Kubernetes 标准。

图 6-1. Certified Kubernetes 标志表示产品或服务经 CNCF 批准。
如果产品名称中含有 Kubernetes,它必须经过 CNCF 的认证。这意味着客户清楚地知道他们得到了什么,并且可以确信它与其他符合规范的 Kubernetes 服务可以互操作。供应商可以通过运行 Sonobuoy 一致性检查工具(见 “使用 Sonobuoy 进行一致性测试”)进行自我认证。
Certified Kubernetes 产品还必须跟踪 Kubernetes 的最新版本,至少每年提供更新。不仅托管服务可以带有 Certified Kubernetes 标志,分发版和安装工具也可以。
Certified Kubernetes 管理员(CKA)
要成为认证的 Kubernetes 管理员(CKA),您需要证明自己具备管理生产环境中的 Kubernetes 集群的关键技能,包括安装和配置、网络、维护、API 知识、安全性和故障排除。任何人都可以参加 CKA 考试,该考试在线进行,包括一系列具有挑战性的实际测试。有关如何培训和注册参加考试的更多信息,请访问CNCF 网站。
CKA 考试以其全面、具有挑战性的声誉而闻名。您可以确信,任何获得 CKA 认证的工程师确实了解 Kubernetes。如果您在 Kubernetes 上运行业务,请考虑将一些员工通过 CKA 项目,特别是直接负责管理集群的人员。
Kubernetes 认证服务提供商(KCSP)
供应商可以申请成为 Kubernetes 认证服务提供商(KCSP)程序的一部分。供应商必须是 CNCF 会员,提供企业支持(例如通过向客户现场提供现场工程师),积极参与 Kubernetes 社区,并雇用三名或更多 CKA 认证工程师才有资格。
最佳实践
查找 Certified Kubernetes 标记以确保产品符合 CNCF 标准。寻找 KCSP 认证供应商,如果您正在招聘 Kubernetes 管理员,请寻找 CKA 资格。
使用 Sonobuoy 进行符合性测试
如果您管理自己的集群,或者即使使用托管服务但想要确保它配置正确且更新到最新,您可以运行 Kubernetes 符合性测试来证明它。运行这些测试的标准工具是Sonobuoy。
Sonobuoy 使用 CLI 工具和您的kubectl认证来在集群内运行测试。安装完成后,您可以使用以下命令运行测试套件:
`sonobuoy run`
... INFO[0000] created object name=sonobuoy namespace= resource=namespaces
...
创建一个新的命名空间,启动 Sonobuoy pods,并开始运行测试。您可以使用kubectl查看:
`kubectl get pods -n sonobuoy`
NAME READY STATUS RESTARTS AGE sonobuoy 1/1 Running 0 14s ...
完整的测试套件可能需要一个小时或更长时间才能完成!您可以在sonobuoy run命令中添加--mode quick标志来运行单个测试以验证连接性。
一旦符合性测试完成,您可以使用retrieve命令查看结果,该命令将结果保存到本地文件中。然后,您可以使用results命令检查该输出:
`results=$(sonobuoy retrieve)`
`sonobuoy results $results`
Plugin: e2e Status: passed Total: 5771 Passed: 1 Failed: 0 ...
后来,在“集群安全扫描”中,我们将涵盖专注于扫描集群潜在安全问题的类似工具。与 Sonobuoy 一起使用时,这些工具可以更好地展示您的集群可能与当前 Kubernetes 合规性最佳实践不符的地方。
最佳实践
在首次设置集群后运行 Sonobuoy,以验证其是否符合标准并且一切正常。定期再次运行以确保没有符合性问题。
Kubernetes 审计日志记录
假设您在集群上发现了问题,比如一个您不认识的 Pod,您想知道它来自哪里。如何查明谁在集群上做了什么?Kubernetes 审计日志会告诉您。
启用审计日志记录后,所有对集群 API 的请求将被记录,包括时间戳,请求者(服务帐户)、请求详细信息(如查询的资源)以及响应内容。
审计事件可以发送到您的中央日志系统,您可以像处理其他日志数据一样对其进行过滤和警报(参见第十五章)。一个良好的托管服务,如 GKE,默认情况下将包括审计日志记录,但否则您可能需要自行配置集群以启用它。
混沌测试
我们在“信任,但要验证”中指出,验证高可用性的唯一真实方法是关闭一个或多个集群节点,看看会发生什么。同样适用于您的 Kubernetes Pod 和应用程序的高可用性。例如,您可以随机选择一个 Pod,终止它,然后检查 Kubernetes 是否重新启动它,并且您的错误率不受影响。
手动执行这项工作耗时且您可能会不自觉地保留那些您知道对应用至关重要的资源。为了进行公正的测试,必须自动化该过程。
这种对生产服务的自动化、随机干扰有时被称为混沌猴子测试,以 Netflix 开发的同名工具命名,用于测试其基础架构:
想象一只猴子进入数据中心,这些托管我们在线活动所有关键功能的服务器农场。猴子随机地拔掉电缆,摧毁设备...
IT 经理面临的挑战是设计他们负责的信息系统,使其能够在这些猴子到来时仍然正常工作,而谁也不知道它们何时到来以及它们将摧毁什么。
安东尼奥·加西亚·马丁内斯,《混沌猴子》
除了混沌猴子本身会随机终止云服务器外,Netflix 的猴子军团还包括其他混沌工程工具,如 Latency Monkey,引入通信延迟以模拟网络问题,Security Monkey,查找已知漏洞,以及 Chaos Gorilla,将整个 AWS 可用区关闭。
只有生产才是真正的生产
您也可以将混沌猴子的想法应用于 Kubernetes 应用程序。虽然您可以在预发布集群上运行混沌工程工具以避免干扰生产环境,但这只能告诉您有限的信息。要了解您的生产环境,您需要在生产中进行测试:
许多系统过于庞大、复杂和成本高昂,难以克隆。想象一下尝试为测试复制 Facebook(带有其多个全球分布的数据中心)。
用户流量的不可预测性使得模拟变得不可能;即使您可以完美重现昨天的流量,您仍无法预测明天的流量。只有生产环境才是真正的生产环境。
此外,需要注意的是,为了使您的混沌实验最有用,需要进行自动化和持续的。仅仅运行一次并决定您的系统从此可靠是不够的:
自动化混沌实验的整个目的是,您可以一遍又一遍地运行它们,以建立对系统的信任和信心。不仅要展现新的弱点,还要确保您已经克服了首次出现的弱点。
您可以使用几种工具来自动执行集群的混沌工程。以下是几个选项。
chaoskube
chaoskube 在您的集群中随机杀死 Pod。默认情况下,它以干预运行模式操作,显示它将要执行的操作,但实际上不终止任何内容。
您可以根据标签(参见 “标签”)、注释和命名空间配置 chaoskube,以包括或排除 Pod,并避免特定的时间段或日期(例如,不要在平安夜杀死任何内容)。默认情况下,它可能会杀死任何命名空间中的任何 Pod,包括 Kubernetes 系统 Pod,甚至 chaoskube 本身。
一旦您满意 chaoskube 的筛选配置,您可以禁用干预运行模式并让其运行。
chaoskube 安装和设置简单,是开始混沌工程的理想工具。
kube-monkey
kube-monkey 在预设时间运行(默认为工作日上午 8 点),并创建一个部署计划表,将在当天的其余时间段内(默认为上午 10 点到下午 4 点)进行目标部署。与其他一些工具不同,kube-monkey 是基于选择加入的方式工作:只有通过注释明确启用 kube-monkey 的 Pod 将被目标化。
这意味着在开发特定应用程序或服务过程中,您可以添加 kube-monkey 测试,并根据服务设置不同的频率和攻击级别。例如,以下 Pod 上的注释将设置平均故障间隔(MTBF)为两天:
kube-monkey/mtbf: *`2`*
kill-mode 注释允许您指定将在部署的 Pod 中杀死多少个,或者最大百分比。以下注释将杀死目标部署中最多 50% 的 Pod:
kube-monkey/kill-mode: "random-max-percent"
kube-monkey/kill-value: 50
PowerfulSeal
PowerfulSeal 是一个开源的 Kubernetes 混沌工程工具,可以以交互和自主两种模式工作。交互模式允许您探索集群并手动破坏以查看发生了什么。它可以终止节点、命名空间、部署和单个 Pod。
自动模式使用一组由您指定的策略:操作哪些资源,避免哪些资源,何时运行(例如,您可以配置仅在工作时间内,例如周一至周五运行),以及采取多大程度的侵略性(例如,杀死所有匹配的部署的一定百分比)。PowerfulSeal 的策略文件非常灵活,几乎可以设置任何可以想象到的混沌工程场景。
最佳实践
如果您的应用程序需要高可用性,请定期运行诸如 chaoskube 之类的混沌测试工具,以确保意外的节点或 Pod 故障不会引起问题。确保事先与负责操作集群和受测应用程序的人员进行清晰沟通。
概要
确定如何规模化和配置您的第一个 Kubernetes 集群可能非常困难。您可以做出许多选择,但实际获得生产经验之前,您不会真正知道您将需要什么。
我们不能为您做出这些决策,但希望我们至少给了您在做出决策时一些有用的思考方向:
-
在配置生产 Kubernetes 集群之前,请考虑需要多少节点以及节点的大小。
-
您至少需要三个控制平面节点(除非您使用托管服务),至少需要两个(理想情况下是三个)工作节点。当您只运行少量小工作负载时,这可能使 Kubernetes 集群显得有点昂贵,但不要忘记内置的弹性和扩展的优势。
-
Kubernetes 集群可以扩展到成千上万个节点和数十万个容器。
-
如果您需要超出此范围,请使用多个集群(有时也需要出于安全或合规性原因)。如果需要在集群之间复制工作负载,可以使用联合。
-
Kubernetes 不仅适用于云端,它也可以在裸金属服务器上运行。如果您有空置的服务器,为什么不使用呢?
-
您可以手动扩展和缩小集群,而不会遇到太多麻烦,而且您可能不必经常这样做。当工作负载增长和缩小时,自动缩放功能非常有用。
-
Kubernetes 供应商和产品有一个明确定义的标准:CNCF 认证的 Kubernetes 标志。如果您没有看到这个标志,请问原因。
-
混沌测试是一个随机关闭 Pod 并查看您的应用程序是否仍在运行的过程。这是有用的,但云端也有自己的混沌测试方式,而无需您要求。
第七章:Kubernetes 强大工具
我的机械师告诉我,“我修不好你的刹车,所以我把你的喇叭声音调得更大了。”
Steven Wright
人们经常问我们,“关于所有这些 Kubernetes 工具怎么办?我需要它们吗?如果需要,具体是哪些?它们都做什么?”
在本章中,我们将探索帮助你使用 Kubernetes 工作的工具和实用程序的一小部分。我们将向你展示一些与 kubectl 相关的高级技术,以及一些实用工具,如 jq、kubectx/kubens、kube-ps1、Click、kubed-sh、Stern 和 BusyBox。
精通 kubectl
我们已经在第二章遇到了 kubectl,由于它是与 Kubernetes 交互的主要工具,你可能已经对基础知识感到很舒服。现在让我们看看 kubectl 的一些更高级特性,包括一些对你来说可能是新的技巧和窍门。
Shell 别名
大多数 Kubernetes 用户为了简化操作,首先做的事情之一就是为 kubectl 命令创建一个 shell 别名。例如,在我们的 .bash_profile 或 .zshrc 文件中设置了以下别名:
alias k=*`kubectl`*
不必为每个命令都完整输入 kubectl,我们可以简单地使用 k:
`k get pods`
如果有一些你经常使用的 kubectl 命令,你可能也喜欢为它们创建别名。这里有一些可能的例子:
alias kg=*`kubectl` `get`*
alias kgdep=*`kubectl` `get` `deployment`*
alias ksys=*`kubectl` `--namespace``=``kube-system`*
alias kd=*`kubectl` `describe`*
谷歌工程师 Ahmet Alp Balkan 想出了一个逻辑的别名系统,并创建了一个脚本为你生成所有这些别名(目前大约有八百个别名)。
当然你不一定非得用它们;我们建议你从 k 开始,并为你经常使用的命令添加易记的别名。
使用短标志
像大多数命令行工具一样,kubectl 支持许多标志和开关的缩写形式。这可以节省很多输入时间。
例如,你可以将 --namespace 标志缩写为 -n(见“使用命名空间”):
`kubectl get pods -n kube-system`
使用 kubectl 常见的一种方式是使用 --selector 标志来操作符合一组标签的资源(见“标签”)。幸运的是,这可以缩短为 -l(labels):
`kubectl get pods -l "environment=staging"`
缩写资源类型
使用 kubectl 的常见方式是列出各种类型的资源,例如 Pods、Deployments、Services 和命名空间。通常的方法是使用 kubectl get 后跟,例如 deployments。
为了加快速度,kubectl 支持这些资源类型的简写形式:
`kubectl get po`
`kubectl get deploy`
`kubectl get svc`
`kubectl get ns`
其他有用的缩写包括 no 代表 nodes,cm 代表 configmaps,sa 代表 serviceaccounts,ds 代表 daemonsets,pv 代表 persistentvolumes。
自动完成 kubectl 命令
如果你使用 bash 或 zsh shell,你可以让它们自动完成 kubectl 命令。运行以下命令查看如何为你的 shell 启用自动完成:
`kubectl completion -h`
按照说明操作,你应该可以按 Tab 键来完成部分 kubectl 命令。现在试试吧:
`kubectl cl<TAB>`
命令应该完成到 kubectl cluster-info。
如果您只键入 kubectl 并按两次 Tab 键,您将看到所有可用的命令:
`kubectl <TAB><TAB>`
alpha attach cluster-info cordon describe ...
您可以使用相同的技术列出可以与当前命令一起使用的所有标志:
`kubectl get pods --<TAB><TAB>`
--all-namespaces --cluster= --label-columns= ...
实用地,kubectl 也会自动完成 Pod、Deployment、命名空间等名称:
`kubectl -n kube-system describe pod <TAB><TAB>`
event-exporter-v0.1.9-85bb4fd64d-2zjng kube-dns-autoscaler-79b4b844b9-2wglc fluentd-gcp-scaler-7c5db745fc-h7ntr ...
获取帮助
最好的命令行工具包括详尽的文档,kubectl 也不例外。您可以通过 kubectl -h 获得所有可用命令的完整概述:
`kubectl -h`
您可以进一步获取每个命令的详细文档,包括所有可用选项和一组示例,只需键入 kubectl COMMAND -h:
`kubectl get -h`
获取 Kubernetes 资源的帮助
除了自身文档,kubectl 还可以为 Kubernetes 对象(如 Deployments 或 Pods)提供帮助。kubectl explain 命令将显示指定类型资源的文档:
`kubectl explain pods`
您可以使用 kubectl explain RESOURCE.FIELD 获取资源特定字段的更多信息。实际上,您可以通过 explain 深入到任何您想要的程度:
`kubectl explain deploy.spec.template.spec.containers.livenessProbe.exec`
显示更详细的输出
您已经知道 kubectl get 将列出各种类型的资源,如 Pods:
`kubectl get pods`
NAME READY STATUS RESTARTS AGE demo-54f4458547-pqdxn 1/1 Running 6 5d
通过使用 -o wide 标志,您可以查看每个 Pod 运行在哪个节点上的额外信息:
`kubectl get pods -o wide`
NAME ... IP NODE demo-54f4458547-pqdxn ... 10.76.1.88 gke-k8s-cluster-1-n1-standard...
(为了节省空间,我们已经省略了您在没有 -o wide 的情况下看到的信息。)
根据资源类型的不同,-o wide 将显示不同的信息。例如,使用节点时:
`kubectl get nodes -o wide`
NAME STATUS ROLES AGE VERSION INTERNAL-IP... minikube Ready control-plane 40d v1.21.2 192.168.49.2...
使用 JSON 数据和 jq 工作
kubectl get 的默认输出格式是纯文本,但它也可以以 JSON 格式打印信息:
`kubectl get pods -n kube-system -o json`
{
"apiVersion": "v1", "items": [ { "apiVersion": "v1", "kind": "Pod", "metadata": { "creationTimestamp": "2021-07-16T03:07:53Z", ...
不出所料,这会产生大量输出(在我们的集群上大约五千行)。幸运的是,由于输出是广泛使用的 JSON 格式,您可以使用其他工具来过滤它,如非常有用的 jq。
如果您尚未安装 jq,可以按照您系统的惯例方式(macOS 上 brew install jq,Debian/Ubuntu 上 apt install jq 等)进行安装。
一旦安装了 jq,您可以使用它来查询和过滤 kubectl output:
`kubectl get pods -n kube-system -o json | jq '.items[].metadata.name'`
"coredns-558bd4d5db-2m5tv" "etcd-minikube" "kube-apiserver-minikube" "kube-controller-manager-minikube" ...
jq 是一个非常强大的工具,用于查询和转换 JSON 数据。
例如,按每个节点上运行的 Pod 数列出最繁忙的节点:
`kubectl get pods -o json --all-namespaces | jq '.items | ` `group_by(.spec.nodeName) | map({"nodeName": .[0].spec.nodeName,` `"count": length}) | sort_by(.count) | reverse'`
有一个方便的 在线 playground,您可以在其中粘贴 JSON 数据并尝试不同的 jq 查询,以获得您想要的确切结果。
如果您没有 jq,kubectl 也支持 JSONPath 查询。JSONPath 是一种 JSON 查询语言,虽然不如 jq 强大,但用于快速的一行命令很有用:
kubectl get pods -o=jsonpath=*{.items[0].metadata.name}*
demo-66ddf956b9-pnknx
观察对象
当您等待一堆 Pod 启动时,每隔几秒钟就需要不停地输入 kubectl get pods... 来查看是否有任何进展,这可能很烦人。
kubectl 提供了 --watch 标志(简称 -w),以免您不得不这样做。例如:
`kubectl get pods --watch`
NAME READY STATUS RESTARTS AGE demo-95444875c-z9xv4 0/1 ContainerCreating 0 1s ... [time passes] ... demo-95444875c-z9xv4 0/1 Completed 0 2s demo-95444875c-z9xv4 1/1 Running 0 2s
每当匹配的 Pod 的状态发生变化时,你将在终端上看到更新。(查看 “使用 kubespy 监视 Kubernetes 资源” 以了解一种优雅的观察资源的方法。)
描述对象
想要获取有关 Kubernetes 对象的详细信息,你可以使用 kubectl describe 命令:
`kubectl describe pods demo-d94cffc44-gvgzm`
Events 部分特别适用于排查工作不正常的容器问题,因为它记录了容器生命周期的每个阶段,以及发生的任何错误。
使用资源
到目前为止,你主要使用 kubectl 进行查询或列出事物,以及使用 kubectl apply 应用声明性 YAML 文件。然而,kubectl 也有一整套 命令式 命令:直接创建或修改资源的操作。
命令式的 kubectl 命令
我们在 “运行演示应用” 中展示了一个例子,使用 kubectl run 命令,该命令隐式创建一个 Pod 来运行指定的容器。
你也可以使用 kubectl create 明确地创建大多数资源:
`kubectl create namespace my-new-namespace`
namespace "my-new-namespace" created
类似地,kubectl delete 将删除一个资源:
`kubectl delete namespace my-new-namespace`
namespace "my-new-namespace" deleted
kubectl edit 命令赋予你查看和修改任何资源的权力:
`kubectl edit deployments my-deployment`
这将使用默认编辑器打开一个 YAML 清单文件,代表指定的资源。
这是查看任何资源配置的详细方法,但你也可以在编辑器中进行任何喜欢的更改。当你保存文件并退出编辑器时,kubectl 将更新资源,就像你对资源的清单文件运行了 kubectl apply 一样。
如果引入了任何错误,比如无效的 YAML,kubectl 将告诉你,并重新打开文件让你修复问题。
不适用命令式命令的时机
在本书中,我们强调了使用 声明性 基础设施即代码的重要性。因此,我们不建议使用命令式的 kubectl 命令并不足为奇。
虽然它们对于快速测试或尝试想法可能非常有用,但是命令式命令的主要问题在于没有一个单一的 真实来源。无法知道谁在集群上什么时候运行了哪些命令,以及效果如何。一旦运行任何命令式命令,集群的状态就会与存储在版本控制中的清单文件不同步。
下次有人应用 YAML 清单时,无论你以命令方式做了哪些更改,它们都将被覆盖和丢失。这可能导致意外的结果,对关键服务可能造成不利影响:
当服务负载突然增加时,Alice 作为值班人员。 Alice 使用
kubectl scale命令将副本数从 5 增加到 10。 几天后,Bob 在版本控制中编辑 YAML 清单以使用新的容器镜像,但他没有注意到文件中的副本数目前是 5,而不是生产中的 10。 Bob 继续进行部署,这使得副本数量减少了一半,导致立即的过载或故障。Kelsey Hightower 等人,《Kubernetes 上手指南》
Alice 在她进行命令式更改后忘记更新版本控制中的文件,但这很容易做到,尤其是在事故的压力下(参见 “On-Call Should Not Be Hell”)。 现实生活并不总是遵循最佳实践。
同样,在重新应用清单文件之前,Bob 应该使用 kubectl diff 检查差异(参见 “Diffing Resources”)以查看将发生的更改。 但如果你没有预料到会有不同的东西,很容易忽视它。 也许 Bob 没有读过这本书。
避免这种问题的最佳方法是始终通过编辑和应用版本控制下的资源文件进行更改。 我们稍后会在 “GitOps” 中详细介绍。
最佳实践
不要在生产集群上使用 kubectl 命令式命令,如 create 或 edit。 相反,始终使用带有 kubectl apply(或 Helm charts)的版本控制的 YAML 清单管理资源。
生成资源清单
即使我们不建议使用命令式模式的 kubectl 对集群进行更改,但当从头开始创建 Kubernetes YAML 文件时,命令式命令可以节省大量时间。
而不是在空文件中键入大量样板代码,您可以使用 kubectl 为您生成 YAML 清单,为您提供一个起点:
`kubectl create deployment demo --image=cloudnatived/demo:hello`
`--dry-run=client -o yaml`
`>deployment.yaml`
--dry-run=client 标志告诉 kubectl 不实际创建资源,而只是打印出它将要创建的内容。 -o yaml 标志以 YAML 格式给出资源清单。 > 字符将命令的输出写入文件,然后您可以使用该文件进行任何编辑,最后应用它以在集群中创建资源。
导出资源
除了帮助您创建新的资源清单外,kubectl 还可以为已经存在于集群中的资源生成清单文件。 例如,也许您使用命令式命令 (kubectl create) 创建了一个 Deployment,编辑并调整它以获得正确的设置,现在您想要编写一个声明性 YAML 清单以添加到版本控制中。
要执行此操作,请使用 kubectl get 命令的 -o 标志:
`kubectl create deployment newdemo --image=cloudnatived/demo:hello`
deployment.apps/newdemo created `kubectl get deployments newdemo -o yaml >deployment.yaml`
此输出将包含一些额外信息,例如可以删除的 status 部分(在保存到其他清单文件之前),更新并应用于 kubectl apply -f。
如果您到目前为止一直在使用命令式 kubectl 命令来管理您的集群,并且希望切换到我们在本书中推荐的声明式风格,这是一个很好的方法。使用 kubectl 带有 -o 标志导出集群中的所有资源到清单文件中,如示例所示,您就可以做好准备了。
资源差异比较
在使用 kubectl apply 应用 Kubernetes 清单之前,查看集群上确切会发生什么变化非常有用。kubectl diff 命令将为您完成这项工作:
`kubectl diff -f deployment.yaml`
- replicas: 10 + replicas: 5
您可以使用此 diff 输出来检查您所做的更改是否会产生您预期的效果。此外,它还会在实际资源的状态与 YAML 清单不同步时发出警告,这可能是因为自上次应用它以来有人通过命令方式编辑了它。
最佳实践
使用 kubectl diff 在将任何更新应用到生产集群之前检查将会发生的变化。
与容器一起工作
大多数 Kubernetes 集群中的操作发生在容器内部,因此当出现问题时,很难看到发生了什么。以下是使用 kubectl 与运行中容器一起工作的几种有用方法。
查看容器的日志
当您尝试使一个容器正常工作但其行为不符合预期时,其中一个最有用的信息源是容器的日志。在 Kubernetes 中,日志被认为是容器写入标准输出和标准错误流的内容;如果您在终端中运行程序,这些内容就是您在终端中看到的打印输出。
在生产应用程序中,特别是分布式应用程序,您需要能够聚合来自多个服务的日志,将它们存储在持久性数据库中,并进行查询和图形化显示。这是一个重要的话题,我们将在第十五章中详细讨论。
尽管如此,检查特定容器的日志消息仍然是一个非常有用的故障排除技术,您可以直接使用 kubectl logs 命令,后跟 Pod 的名称:
`kubectl logs -n kube-system --tail=20 kube-dns-autoscaler-69c5cbdcdd-94h7f`
autoscaler.go:49] Scaling Namespace: kube-system, Target: deployment/kube-dns autoscaler_server.go:133] ConfigMap not found: configmaps "kube-dns-autoscaler" k8sclient.go:117] Created ConfigMap kube-dns-autoscaler in namespace kube-system plugin.go:50] Set control mode to linear linear_controller.go:59] ConfigMap version change (old: new: 526) - rebuilding
大多数长时间运行的容器会生成大量的日志输出,因此通常您会希望仅限于最近的几行,可以使用 --tail 标志,就像这个例子一样。(这些容器日志会显示时间戳,但在这里我们已经对其进行了修剪以适应页面上的消息。)
若要在容器运行时观看其输出并将日志实时流式传输到您的终端,请使用 --follow 标志(简写为 -f):
`kubectl logs --namespace kube-system --tail=10 --follow etcd-docker-for-desktop`
etcdserver: starting server... [version: 3.1.12, cluster version: 3.1] embed: ClientTLS: cert = /var/lib/localkube/certs/etcd/server.crt, key = ... ...
只要使用 kubectl logs 命令并带有 --follow 标志持续运行,您将继续看到来自 Pod 的任何新输出。
查看 Kubernetes API 服务器的日志尤为有用;例如,如果您遇到 RBAC 权限错误(参见“引入基于角色的访问控制(RBAC)”),它们将显示在这里。如果您可以访问控制平面节点,则可以在 kube-system 命名空间中找到 kube-apiserver Pod,并使用 kubectl logs 查看其输出。
如果您正在使用托管服务,控制平面节点对您不可见,请查阅提供商的文档,了解如何查找控制平面日志。
小贴士
当 Pod 中有多个容器时,您可以使用 --container 标志(简写为 -c)指定您想查看日志的容器:
`kubectl logs -n kube-system metrics-server`
`-c metrics-server-nanny`
...
对于更复杂的日志监控,您可能需要使用类似 Stern 这样的专用工具(参见 “Stern”)。
附加到容器
当查看容器的日志不足以解决问题时,您可能需要将本地终端连接到容器中。这样可以直接查看容器的输出。要执行此操作,请使用 kubectl attach:
`kubectl attach demo-54f4458547-fcx2n`
Defaulting container name to demo. Use *`kubectl describe pod/demo-54f4458547-fcx2n`* to see all of the containers in this pod. If you don't see a command prompt, try pressing enter.
使用 kubespy 观察 Kubernetes 资源
当您部署 Kubernetes 清单的更改时,通常会有一段焦虑的等待期,以查看接下来会发生什么。
在部署应用程序时,通常需要在幕后执行许多操作,例如 Kubernetes 创建资源、启动 Pods 等等。
因为这种情况是 自动完成 的,就像工程师们常说的那样,很难分辨发生了什么。kubectl get 和 kubectl describe 可以为您提供单个资源的快照,但我们真正想要的是一种能实时查看 Kubernetes 资源状态变化的方法。
这里介绍了 kubespy,这是来自 Pulumi 项目的一个很棒的工具。^(1) kubespy 能够监视集群中的单个资源,并展示它随时间的变化情况。
例如,如果将 kubespy 指向一个 Service 资源,它会显示 Service 创建时的情况,当其分配了 IP 地址时,以及连接其终端节点时的情况,等等。
转发容器端口
我们之前在 “运行演示应用” 中使用过 kubectl port-forward,将 Kubernetes Service 转发到本地机器上的某个端口。但是,您也可以用它来转发容器的端口,如果您想直接连接到特定的 Pod。只需指定 Pod 名称以及本地和远程端口即可:
`kubectl port-forward demo-54f4458547-vm88z 9999:8888`
Forwarding from 127.0.0.1:9999 -> 8888 Forwarding from [::1]:9999 -> 8888
现在您本地机器上的 9999 端口将会被转发到容器上的 8888 端口,并且您可以用浏览器连接它,例如。
在容器上执行命令
容器的隔离性在您需要运行可靠、安全工作负载时非常有用。但当某些问题出现时,你却看不到问题所在,这可能会有点不便。
当您在本地机器上运行程序时,如果它表现不正常,您可以利用命令行的强大功能来进行故障排除:您可以用 ps 查看运行中的进程,用 ls 和 cat 列出和显示文件,甚至可以用文本编辑器直接编辑文件。
在容器出现故障时,很多时候需要在容器中运行一个 shell,以便进行此类交互式调试。
使用 kubectl exec 命令,您可以在任何容器中运行指定的命令,包括 shell:
`kubectl run alpine --image alpine --command -- sleep 999`
pod/alpine created
`kubectl get pods`
NAME READY STATUS RESTARTS AGE alpine 1/1 Running 0 39s
`kubectl exec -it alpine -- /bin/sh`
/ # `ps`
PID USER TIME COMMAND
1 root 0:00 sleep 999 7 root 0:00 /bin/sh 11 root 0:00 ps
如果 Pod 中有多个容器,kubectl exec 默认会在第一个容器中运行命令。或者,你可以使用 -c 标志指定容器:
`kubectl exec -it -c container2 POD_NAME -- /bin/sh`
(如果容器没有 shell,请参阅 “将 BusyBox 添加到您的容器”。)
用于故障排除的运行容器
除了在现有容器上运行命令之外,有时还可以在集群中运行像 wget 或 nslookup 这样的命令,以查看你的应用程序的结果。你已经学会了如何使用 kubectl run 在集群中运行容器,但这里有一些用于调试目的的有用的一次性容器命令示例。
首先,让我们运行一个演示应用实例进行测试:
`kubectl run demo --image cloudnatived/demo:hello --expose --port 8888`
service/demo created pod/demo created
demo 服务应已分配一个 IP 地址和一个名为 demo 的 DNS 名称,可从集群内部访问。让我们通过在容器内运行 nslookup 命令来检查一下:
`kubectl run nslookup --image=busybox:1.28 --rm -it --restart=Never \` `--command -- nslookup demo`
Server: 10.79.240.10 Address 1: 10.79.240.10 kube-dns.kube-system.svc.cluster.local
Name: demo Address 1: 10.79.242.119 demo.default.svc.cluster.local
好消息是:DNS 名称有效,因此我们应该能够使用 wget 进行 HTTP 请求并查看结果:
`kubectl run wget --image=busybox:1.28 --rm -it --restart=Never \` `--command -- wget -qO- http://demo:8888`
Hello, 世界 pod "wget" deleted
你可以看到这些 kubectl run 命令使用了一组常见的标志:
kubectl run NAME --image=IMAGE `--rm -it --restart=Never --command -- ...`
这些命令做什么?
--rm
这告诉 Kubernetes 删除此命令创建的附加容器的资源,以免占用节点的本地存储空间。
-it
这将以交互方式(i)在终端(t)中运行容器,以便你在自己的终端中看到容器的输出,并且如果需要,可以向其发送按键。
--restart=Never
这告诉 Kubernetes 在容器退出时跳过其通常的重新启动行为。由于我们只想运行容器一次,我们可以禁用默认的重启策略。
--command --
这指定要运行的命令,而不是容器的默认入口点。-- 后面的所有内容将作为命令行传递给容器,包括参数。
使用 BusyBox 命令
尽管你可以运行任何可用的容器,但 busybox 镜像特别有用,因为它包含了最常用的 Unix 命令,如 cat、echo、find、grep 和 kill。你可以在它们的网站上查看完整的 BusyBox 命令 列表。
BusyBox 还包括一个轻量级的类似 bash 的 shell,称为 ash,与标准的 /bin/sh shell 脚本兼容。因此,要在集群中获取交互式 shell,你可以运行:
`kubectl run busybox --image=busybox:1.28 --rm -it --restart=Never /bin/sh`
因为从 BusyBox 镜像运行命令的模式总是相同的,你甚至可以为其制作一个 shell 别名(参见 “Shell 别名”):
alias bb=*kubectl run busybox --image=busybox:1.28 --rm -it --restart=Never
--command --*
bb nslookup demo
...
bb wget -qO- http://demo:8888
...
bb sh
If you don't see a command prompt, try pressing enter.
/ #
将 BusyBox 添加到您的容器
如果你的容器已经有一个 shell(例如,如果它是从 Linux 基础镜像构建的,比如 alpine),那么你可以通过运行
kubectl exec -it *POD_NAME*-- /bin/sh
但是,如果容器中没有 /bin/sh 怎么办?例如,如果您正在使用如 “理解 Dockerfile” 中描述的极简、scratch 镜像。
使您的容器易于调试的最简单方法,同时保持镜像非常小,就是在构建时将 busybox 可执行文件复制到其中。它仅有 1 MiB,这对于拥有一个可用的 shell 和一组 Unix 实用程序来说是一个小代价。
在前面讨论多阶段构建时,您学到可以使用 Dockerfile 的 COPY --from 命令将文件从先前构建的容器复制到新容器中。这个命令的一个较少为人知的特性是,您还可以从任何公共镜像复制文件,而不仅限于本地构建的镜像。
以下 Dockerfile 显示了如何在演示镜像中执行此操作:
FROM golang:1.17-alpine AS build
WORKDIR /src/
COPY main.go go.* /src/
RUN CGO_ENABLED=0 go build -o /bin/demo
FROM scratch
COPY --from=build /bin/demo /bin/demo
`COPY` `--from``=``busybox:1.28` `/bin/busybox` `/bin/busybox`
ENTRYPOINT ["/bin/demo"]
在这里,--from=busybox:1.28 引用了公共 BusyBox 库镜像。^(2) 您可以从任何喜欢的镜像中复制文件(例如 alpine)。
现在,您仍然拥有一个非常小的容器,但您也可以通过运行以下命令在其上获取一个 shell:
kubectl exec -it *POD_NAME* -- /bin/busybox sh
而不是直接执行 /bin/sh,您可以执行 /bin/busybox,然后是您想要的命令名称;在这种情况下,是 sh。
在容器上安装程序
如果您需要一些不包含在 BusyBox 中或者在公共容器镜像中不可用的程序,您可以运行 Linux 镜像,比如 alpine 或 ubuntu,然后在其上安装您需要的任何内容:
`kubectl run alpine --image alpine --rm -it --restart=Never /bin/sh`
If you don't see a command prompt, try pressing enter. / # `apk --update add emacs`
请记住,这些临时调试容器可能需要以 root 用户身份运行,以便安装新包,而且一旦容器退出,容器内的任何更改都会丢失。这个过程仅适用于一次性故障排除会话,而不是您希望在集群上持续运行的应用程序。
上下文与命名空间
到目前为止,在本书中我们一直在使用单个 Kubernetes 集群,并且您运行的所有 kubectl 命令自然应用于该集群。
那么,当您拥有多个集群时会发生什么呢?例如,也许您在本地测试机器上有一个 Kubernetes 集群,在云中有一个生产集群,也许还有一个用于暂存和开发的远程集群。kubectl 如何知道您指的是哪一个?
要解决这个问题,kubectl 有上下文。上下文是集群、用户和命名空间的组合(参见 “使用命名空间”)。
当您运行 kubectl 命令时,它们始终在当前上下文中执行。让我们看一个例子:
`kubectl config get-contexts`
CURRENT NAME CLUSTER AUTHINFO NAMESPACE
gke gke_test_us-w gke_test_us myapp * docker-for-desktop docker-for-d docker-for-d
这些是 kubectl 当前知道的上下文。每个上下文都有一个名称,并指向一个特定的集群,一个用于认证到该集群的用户名,以及集群内的一个命名空间。正如您可能期望的那样,docker-for-desktop 上下文指的是我的本地 Kubernetes 集群。
当前上下文在第一列中用 * 显示(在示例中为 docker-for-desktop)。如果现在运行 kubectl 命令,它将在 Docker Desktop 集群中的默认命名空间中执行(因为 NAMESPACE 列为空,表示上下文指向默认命名空间):
kubectl cluster-info
Kubernetes control plane is running at *https://192.168.49.2:8443*
CoreDNS is running at *https://192.168.49.2:8443/api/v1/namespaces/*...
To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
您可以使用 kubectl config use-context 命令切换到另一个上下文:
`kubectl config use-context gke`
Switched to context "gke".
可以将上下文想象为书签:它们可以让你轻松切换到特定的集群和特定的命名空间。要创建新的上下文,请使用 kubectl config set-context:
`kubectl config set-context myapp --cluster=gke --namespace=myapp`
Context "myapp" created.
现在每当切换到 myapp 上下文时,当前上下文将是 Docker Desktop 集群上的 myapp 命名空间。
如果忘记了当前上下文是什么,可以使用 kubectl config current-context 命令查看:
`kubectl config current-context`
myapp
kubeconfig 文件
这些不同 Kubernetes 集群的上下文默认存储在一个文件(或多个文件)中,通常位于您的机器上的 ~/.kube/config。如果希望使用不同的路径,可以自定义 KUBECONFIG 环境变量,具体见管理 kubeconfig 文件的 文档。
kubectx 和 kubens
如果你像我们一样以键盘为生,可能不喜欢多敲击键盘。为了快速切换 kubectl 上下文,可以使用 kubectx 和 kubens 工具。在 GitHub 上查看 说明 安装 kubectx 和 kubens。
现在可以使用 kubectx 命令切换上下文:
`kubectx docker-for-desktop`
Switched to context "docker-for-desktop".
kubectx 的一个很好的功能是 kubectx - 可以切换到先前的上下文,因此可以快速在两个上下文之间切换:
`kubectx -`
Switched to context "gke". `kubectx -`
Switched to context "docker-for-desktop".
只需kubectx命令,将显示所有已存储的上下文,并突出显示当前上下文。
切换命名空间比切换上下文更常见,因此 kubens 工具非常适合这种情况:
`kubens`
`default`
kube-public kube-system
`kubens kube-system`
Context "docker-for-desktop" modified. Active namespace is "kube-system".
`kubens -`
Context "docker-for-desktop" modified. Active namespace is "default".
您也可以使用 krew 包管理工具来安装它们,它会打开一个整个生态系统的有用插件,使得与 Kubernetes 工作更轻松。
小贴士
kubectx和kubens工具各有所长,是 Kubernetes 工具箱中非常实用的补充。使用krew可以轻松安装类似的辅助工具。
kube-ps1
如果使用 bash 或 zsh shell,还有一个小 实用工具,它将当前的 Kubernetes 上下文添加到您的提示符中。
安装了 kube-ps1 后,你将不会忘记当前所在的上下文:
`source "/usr/local/opt/kube-ps1/share/kube-ps1.sh"`
`PS1="[$(kube_ps1)]$ "`
[(⎈ |docker-for-desktop:default)] `kubectx cloudnativedevops`
Switched to context "cloudnativedevops". (⎈ |cloudnativedevops:myapp)
Kubernetes Shell 和工具
在普通 shell 中使用 kubectl 对于大多数需要在 Kubernetes 集群中执行的任务来说已经足够,但也有其他选择。
kube-shell
如果 kubectl 自动完成功能对你来说还不够,还有 kube-shell,这是一个 kubectl 的包装器,为每个命令提供可能的完成选项的弹出菜单(见 图 7-1)。

图 7-1. kube-shell 是一个交互式的 Kubernetes 客户端。
Click
Click 提供了更复杂的 Kubernetes 终端体验。
Click 就像是一个交互式的 kubectl 版本,它记住了你正在处理的当前对象。例如,在 kubectl 中想要查找和描述一个 Pod,通常需要先列出所有匹配的 Pods,然后复制并粘贴感兴趣的 Pod 的唯一名称到一个新命令中。
相比之下,使用 Click,你可以通过键入其编号(例如,1 表示第一个项目)来选择列表中的任何资源。这样选择后,当前的资源就是所选的资源,默认情况下,下一个 Click 命令将操作该资源。为了更轻松地找到你想要的对象,Click 支持通过正则表达式进行搜索。
kubed-sh
虽然 kube-shell 和 Click 提供了一些了解 Kubernetes 的本地 shell 功能,kubed-sh(发音为 kube-dash)是一个更有趣的想法:一种在集群内部运行的 shell。
kubed-sh 将拉取并运行必要的容器来执行 JavaScript、Ruby 或 Python 程序在你的当前集群上。例如,你可以在本地机器上创建一个 Ruby 脚本,并使用 kubed-sh 将该脚本作为 Kubernetes 部署执行。
Stern
虽然 kubectl logs 是一个有用的命令(参见 “查看容器日志”),但它并不像它本应该那样方便。例如,在使用它之前,你首先需要找出你想要查看日志的 Pod 和容器的唯一名称,并在命令行中指定这些信息,通常意味着至少要复制并粘贴一次。
另外,如果你使用 -f 来跟踪特定容器的日志,当容器重新启动时,你的日志流将停止。你需要找出容器的新名称,然后再次运行 kubectl logs 来跟踪它。而且你一次只能跟踪一个 Pod 的日志。
一个更复杂的日志流工具可以让你通过匹配它们的名称或标签的正则表达式来指定一组 Pods,并且即使个别容器被重新启动,它也能持续流式传输日志。
幸运的是,这正是 Stern 工具所做的。Stern 尾随匹配正则表达式的所有 Pods 的日志(例如 demo.*)。如果 Pod 中有多个容器,Stern 将显示每个容器的日志消息,以其名称为前缀。
--since 标志允许你限制输出到最近的消息(例如在上述示例中为最近 10 分钟内的消息)。
与使用正则表达式匹配特定 Pod 名称不同,你可以使用任何 Kubernetes 标签选择器表达式,就像使用 kubectl 一样。结合 --all-namespaces 标志使用,这非常适合查看多个容器的日志。
Kubernetes IDEs
随着您对 Kubernetes 的使用越来越多,您将迅速发现需要有效地查找、查看和编辑涉及的所有 YAML 清单的方法。您将在不同的命名空间和集群之间跳转。您喜爱的文本编辑器和kubectl可能是您所需的一切,但也有一些很棒的工具值得尝试,这些工具为与 Kubernetes 交互添加了漂亮的功能和可视化效果。
Lens
Lens是一个 GUI 应用程序,像高级可视化器和 IDE 一样专为与 Kubernetes 集群交互而制作。您可以将其用作仪表板资源管理器,查看集群内部的运行情况。Lens 使得在不同上下文和命名空间之间快速切换变得容易,并包括一个集成的终端,您可以用于kubectl。
VS Code Kubernetes 扩展
VS Code是一个流行的文本编辑器,拥有庞大的扩展生态系统,可以为几乎所有内容提供扩展。就像Lens,你可以使用Kubernetes 扩展作为集群的仪表板工具。它还包括与 Azure 的特殊集成,用于创建 AKS 集群和直接部署应用程序。
构建您自己的 Kubernetes 工具
结合像jq和标准的 Unix 工具集(cut,grep,xargs等)这样的查询工具,kubectl可以用于对 Kubernetes 资源进行一些相当复杂的脚本编写。正如我们在本章中看到的那样,还有许多第三方工具可用于自动化脚本的一部分。
然而,这种方法有其局限性。为交互式调试和探索编写巧妙的单行命令和临时 shell 脚本是可以的,但它们可能难以理解和维护。
对于自动化生产工作流程的真实系统程序,我们强烈建议您使用真实的系统编程语言。Go 是一个合乎逻辑的选择,因为它足够好用于 Kubernetes 的作者们——当然,Kubernetes 还包含了用于 Go 程序的功能齐全的客户端库。
因为client-go库为您提供了对 Kubernetes API 的完全访问权限,您可以像kubectl一样做任何事情,甚至更多。以下片段展示了如何列出集群中的所有 Pods,例如:
...
podList, err := clientset.CoreV1().Pods("").List(metav1.ListOptions{})
if err != nil {
log.Fatal(err)
}
fmt.Println("There are", len(podList.Items), "pods in the cluster:")
for _, i := range podList.Items {
fmt.Println(i.ObjectMeta.Name)
}
...
您还可以创建或删除 Pods、Deployments 或任何其他资源。您甚至可以实现自己的自定义资源类型。
如果您需要一个在 Kubernetes 中缺失的功能,可以使用客户端库自行实现。
其他编程语言,如 Python,Java 和 dotnet,也有官方的Kubernetes 客户端库,您可以以同样的方式使用。还有社区维护的其他编程语言的库。
总结
Kubernetes 工具种类繁多,每周都会有新工具发布。再看到另一个貌似离不开的工具时,你可能会感到有些厌倦。
事实上,大多数这些工具你都不需要。Kubernetes 本身通过 kubectl 就能完成你大部分需求。其余的只是为了好玩和方便而已。
没有人知道所有事情,但每个人都知道一些事情。在撰写本章时,我们整合了许多经验丰富的 Kubernetes 工程师的技巧和窍门,来自书籍、博客文章和文档,以及我们自己的一两个小发现。我们向每个人展示过,无论多么专家,至少都学到了一些有用的东西。这让我们感到高兴。
值得花点时间来熟悉 kubectl 并探索其可能性;它是你使用最多的最重要的 Kubernetes 工具。
以下是几个最重要的要点需要知道:
-
kubectl包含了关于自身的完整详尽文档,可以使用kubectl -h获取,以及关于每个 Kubernetes 资源、字段或特性的文档,使用kubectl explain。 -
例如在脚本中需要对
kubectl输出进行复杂的过滤和转换时,可以选择 JSON 格式,使用-o json。有了 JSON 数据,你可以使用像jq这样的强大工具来查询。 -
kubectl --dry-run=client选项结合-o YAML可以使用命令式命令生成 Kubernetes 清单文件。例如,对于新应用程序的清单文件创建,这可以大大节省时间。 -
你也可以使用
-o标志将现有资源转换为 YAML 清单,使用kubectl get。 -
kubectl diff将告诉你如果应用清单会发生什么变化,而不会实际更改它。 -
你可以使用
kubectl logs查看任何容器的输出和错误消息,使用--follow标志连续流式传输它们,或者使用 Stern 进行更复杂的多 Pod 日志尾随。 -
当需要调试问题容器时,可以使用
kubectl attach来附加到容器上,或者使用kubectl exec -it ... -- /bin/sh在容器上获取 shell。 -
你可以使用
kubectl run运行任何公共容器镜像来帮助解决问题,包括多才多艺的 BusyBox 工具,其中包含了所有你喜欢的 Unix 命令。 -
Kubernetes 上下文就像书签,标记着你在特定集群和命名空间中的位置。你可以使用
kubectx和kubens工具方便地在上下文和命名空间之间切换。 -
Click 是一个强大的 Kubernetes shell,提供了所有
kubectl的功能,但增加了状态:它记住了从一个命令到下一个命令选择的当前对象,因此你不必每次都指定它。 -
Lens 是一个出色的独立应用程序,可以用来管理 Kubernetes 集群。VS Code 也有一个 Kubernetes 扩展,让你可以在代码旁边与你的集群交互。
-
Kubernetes 设计为可以通过代码自动化和控制。当你需要超出
kubectl提供的功能时,Kubernetes 的client-go库可以让你通过 Go 代码完全控制集群的每个方面。
^(1) Pulumi 是一个云原生基础设施即代码框架。
^(2) BusyBox 图像版本在 1.28 之后在 Kubernetes 中存在 DNS 查询问题。
第八章:运行容器
如果您有一个棘手的问题无法回答,可以先处理一个简单问题无法回答的问题。
Max Tegmark
在之前的章节中,我们主要关注了 Kubernetes 的操作方面:如何获取您的集群,如何维护它们以及如何管理您的集群资源。现在让我们转向 Kubernetes 中最基本的对象:容器。我们将深入探讨容器在技术层面上的工作原理,它们与 Pods 的关系以及如何将容器镜像部署到 Kubernetes。
本章中,我们还将涵盖容器安全的重要主题,以及如何利用 Kubernetes 中的安全功能按照最佳实践安全地部署应用程序。最后,我们将讨论如何在 Pod 上挂载磁盘卷,使容器能够共享和持久化数据。
容器和 Pods
我们已经在第二章介绍了 Pods,并讨论了部署如何使用 ReplicaSets 来维护一组副本 Pods,但我们并没有详细研究过 Pods 本身。Pod 是 Kubernetes 中的调度单位。一个 Pod 对象表示一个容器或一组容器,Kubernetes 中的所有运行都通过 Pod 来实现:
一个 Pod 代表在同一执行环境中运行的应用程序容器和卷的集合。Pods 而不是容器是 Kubernetes 集群中最小的可部署工件。这意味着 Pod 中的所有容器始终部署在同一台机器上。
Kelsey Hightower 等,《Kubernetes 权威指南》
到目前为止,在本书中,“Pod”和“容器”这两个术语基本可以互换使用:演示应用程序 Pod 中只有一个容器。然而,在更复杂的应用程序中,Pod 很可能包含两个或更多个容器。因此,让我们看看这是如何工作的,以及何时以及为什么您可能希望将容器组合在 Pods 中。
什么是容器?
在询问为什么要在 Pod 中拥有多个容器之前,让我们花点时间重新审视容器到底是什么。
您从“容器的到来”了解到,容器是一个标准化的软件包,其中包含软件及其依赖项、配置、数据等:即使是运行所需的一切。然而,它究竟是如何工作的呢?
在 Linux 和大多数其他操作系统中,所有在机器上运行的东西都是通过进程来实现的。进程代表运行应用程序的二进制代码和内存状态,如 Chrome、top或 Visual Studio Code。所有进程存在于同一个全局命名空间:它们可以相互看到和交互,它们共享 CPU、内存和文件系统等资源池。(Linux 命名空间有点像 Kubernetes 命名空间,尽管从技术上讲并非完全相同。)
从操作系统的角度来看,容器表示存在于自己命名空间中的隔离进程(或一组进程)。容器内部的进程看不到外部进程,反之亦然。容器无法访问属于另一个容器的资源,或者容器外部的进程。容器边界就像是一个环形栅栏,阻止进程胡作非为并占用彼此的资源。
就容器内部进程而言,它好像在自己的机器上运行,并且完全访问所有资源,且没有其他进程在运行。如果在容器内运行几个命令,您就可以看到这一点:
`kubectl run busybox --image busybox:1.28 --rm -it --restart=Never /bin/sh`
If you don't see a command prompt, try pressing enter. / # `ps ax`
PID USER TIME COMMAND
1 root 0:00 /bin/sh 8 root 0:00 ps ax
/ # `hostname`
busybox
通常,ps ax 命令将列出机器上运行的所有进程,通常有很多进程(在典型的 Linux 服务器上可能有几百个)。但在这里只显示了两个进程:/bin/sh 和 ps ax。因此,容器内可见的进程只有实际在容器中运行的进程。
类似地,hostname 命令通常会显示主机名称,但在这里返回的是 busybox:实际上,这是容器的名称。因此,对于 busybox 容器来说,它看起来好像在一个名为 busybox 的机器上运行,并且它拥有整个机器的控制权。这对于同一台机器上运行的每个容器都是成立的。
小贴士
一个有趣的练习是自己创建一个容器,而不依赖像 Docker 这样的容器运行时。Liz Rice 在“What is a Container, Really?”的精彩演讲中展示了如何在 Go 程序中从头开始做到这一点。
Kubernetes 中的容器运行时
正如我们在第三章中提到的,Docker 并不是运行容器的唯一方式。事实上,2020 年 12 月,维护者们宣布 Kubernetes 中的 Docker 运行时将被弃用,并用使用容器运行时接口(CRI)的替代方案取而代之。这并不意味着将来 Kubernetes 中将不再支持 Docker 容器,也不意味着 Docker 将不再是与 Kubernetes 外的容器交互的有用工具。只要容器符合开放容器倡议(OCI)定义的标准,它就应该能在 Kubernetes 中运行,而使用 Docker 构建的容器确实符合这些标准。您可以在 Kubernetes 博客上的“Dockershim Removal FAQ”中详细了解此变更及其影响。
容器中应该放什么内容?
技术上没有理由限制您在一个容器中运行多少进程:您可以在同一个容器中运行完整的 Linux 发行版,包括多个正在运行的应用程序、网络服务等等。这也是为什么有时容器被称为轻量级虚拟机。但这并不是使用容器的最佳方式,因为这样您无法获得资源隔离的好处。
如果进程不需要彼此知道,则它们无需在同一个容器中运行。一个容器的一个良好的经验法则是它应该只做一件事。例如,我们的演示应用程序容器监听一个网络端口,并向连接到它的任何人发送字符串Hello, 世界。这是一个简单的、自包含的服务:它不依赖于任何其他程序或服务,反过来也没有程序依赖它。这是一个非常适合拥有自己容器的理想候选者。
容器还有一个入口点:在容器启动时运行的命令。通常情况下,这会导致创建一个单独的进程来运行命令,尽管某些应用程序经常启动几个子进程作为辅助程序或工作程序。如果要在容器中启动多个独立的进程,您需要编写一个包装脚本作为入口点,然后启动您想要的进程。
提示
每个容器应只运行一个主要进程。如果在一个容器中运行大量不相关的进程,则没有充分利用容器的强大功能,您应考虑将应用程序拆分为多个相互通信的容器。
什么应该放在一个 Pod 中?
现在您知道容器是什么,就能明白将它们组合在 Pod 中的好处所在。Pod 代表一组需要相互通信和共享数据的容器;它们需要一起调度,需要一起启动和停止,并且需要运行在同一物理机器上。
典型例子是将数据存储在本地缓存中的应用程序,比如Memcached。您需要运行两个进程:您的应用程序和处理存储和检索数据的memcached服务器进程。虽然您可以将这两个进程运行在一个单独的容器内,但这是不必要的——它们只需要通过网络套接字进行通信。最好将它们拆分成两个独立的容器,每个容器只需关心构建和运行自己的进程。
您可以使用公共的 Memcached 容器镜像,可从 Docker Hub 获取,它已经设置为可以作为 Pod 的一部分与另一个容器一起工作。
因此,您创建一个 Pod,其中包含两个容器:Memcached 和您的应用程序。应用程序可以通过网络连接与 Memcached 通信,因为这两个容器位于同一个 Pod 中,所以连接始终是本地的:这两个容器始终运行在同一节点上。
同样地,想象一个 web 应用程序,其中包含一个 web 服务器容器,比如 NGINX,以及一个生成 HTML 网页、文件和图片等的博客应用程序。博客容器将数据写入磁盘,由于 Pod 中的容器可以共享磁盘卷,数据也可以供 NGINX 容器通过 HTTP 服务。你可以在 Kubernetes 文档网站 上找到这样一个例子。
总的来说,在设计 Pod 时应该问自己的正确问题是,“如果这些容器落在不同的机器上,它们能否正常工作?” 如果答案是“否”,那么 Pod 是容器的正确分组方式。如果答案是“是”,那么多个 Pod 可能是正确的解决方案。
Kelsey Hightower 等人,《Kubernetes 上手指南》
Pod 中的容器应该共同工作以执行一个任务。如果你只需要一个容器来完成这个任务,那很好:使用一个容器。如果你需要两个或三个,那也可以。如果你有更多,你可能需要考虑这些容器是否可以分成单独的 Pod。
容器清单
我们已经概述了容器的内容、容器中应包含的内容,以及何时应将容器组合在 Pod 中。那么,我们如何在 Kubernetes 中实际运行容器呢?
当你创建你的第一个 Deployment 时,在“部署清单”中,它包含一个 template.spec 部分,指定要运行的容器(在那个例子中只有一个容器):
spec:
containers:
`-` `name``:` `demo`
`image``:` `cloudnatived/demo:hello`
`ports``:`
`-` `containerPort``:` `8888`
下面是具有两个容器部署的 template.spec 部分的示例:
spec:
containers:
`-` `name``:` `container1`
`image``:` `example/container1`
`-` `name``:` `container2`
`image``:` `example/container2`
每个容器规范中唯一需要的字段是 name 和 image:一个容器必须有一个名称,这样其他资源可以引用它,并且你必须告诉 Kubernetes 在容器中运行什么镜像。
镜像标识符
到目前为止,你在本书中已经使用了一些不同的容器镜像标识符;例如,cloudnatived/demo:hello、alpine 和 busybox:1.28。
实际上,镜像标识符有四个不同的部分:注册表主机名、仓库命名空间、镜像仓库 和 标签。除了镜像名称外,其他都是可选的。一个使用所有这些部分的镜像标识符看起来像这样:
docker.io/cloudnatived/demo:hello
-
在这个例子中,注册表主机名是
docker.io;实际上,这是 Docker 镜像的默认设置,所以我们不需要指定它。然而,如果你的镜像存储在另一个注册表中,你需要给出其主机名。例如,Google Container Registry 的镜像以gcr.io为前缀。 -
仓库命名空间是
cloudnatived:这是我们(你好!)。如果不指定仓库命名空间,则使用默认命名空间(称为library)。这是一组 官方镜像,由 Docker, Inc. 批准和维护。流行的官方镜像包括操作系统基础镜像(alpine、ubuntu、debian、centos)、语言环境(golang、python、ruby、php、java)以及广泛使用的软件(mongo、mysql、nginx、redis)。 -
镜像仓库是
demo,它标识了注册表和命名空间内的特定容器镜像(另见 “Container Digests”)。 -
标签是
hello。标签标识了同一镜像的不同版本。
你可以根据需要给容器打上任何标签:一些常见的选择包括:
-
语义化版本标签,比如
v1.3.0。这通常指的是应用程序的版本。 -
Git SHA 标签,比如
5ba6bfd...。这标识了构建容器所用源代码库中特定的提交(参见 “Git SHA Tags”)。 -
它所代表的环境,如
staging或production。
你可以给一个给定的镜像添加任意多个标签。
最新标签
如果在拉取镜像时未指定标签,默认的 Docker 镜像标签是 latest。例如,当你运行一个未指定标签的 alpine 镜像时,你将得到 alpine:latest。
latest 标签是在没有指定标签的情况下构建或推送镜像时添加的默认标签。它并不一定标识最近的镜像,只是最近未显式标记的镜像。这使得 latest 作为标识符 相当无用。
这就是为什么在部署生产容器到 Kubernetes 时,始终使用特定标签非常重要。当你只运行一个临时容器进行故障排除或实验时,例如 alpine 容器,可以省略标签并获取最新镜像。但对于真正的应用程序,你需要确保如果明天部署 Pod,你将得到与今天部署时相同的容器镜像:
在生产环境中部署容器时应避免使用
latest标签,因为难以追踪镜像的版本,也更难以正确回滚。
容器摘要
正如我们所见,latest 标签并不总是如你所想象的那样,即使是语义化版本或者 Git SHA 标签也不能唯一永久地标识特定的容器镜像。如果维护者决定推送一个具有相同标签的不同镜像,在下次部署时,你将获得那个更新的镜像。技术上来说,一个标签是 不确定的。
有时希望具有确定性部署:换句话说,保证部署始终引用你指定的确切容器镜像。你可以通过容器的摘要来实现这一点:镜像内容的加密哈希,不可变地标识该镜像。
镜像可以有多个标签,但只有一个摘要。这意味着如果你的容器清单指定了镜像摘要,你可以保证确定性部署。带有摘要的镜像标识看起来像这样:
cloudnatived/demo@sha256:aeae1e551a6cbd60bcfd56c3b4ffec732c45b8012b7cb758c6c4a34...
基础镜像标签
在 Dockerfile 中引用基础镜像时,如果未指定标签,将会获取latest,就像在运行容器时一样。如果有一天你的构建停止工作,并发现你正在使用的latest镜像现在指向了引入某些破坏性变更的不同版本的镜像,这可能会让人困惑。
出于这个原因,你可能希望在 Dockerfile 中的FROM行中使用更具体的基础镜像标签。但你应该使用哪个标签?或者应该使用确切的摘要吗?这在很大程度上取决于你的开发情况和偏好。
让我们以Docker Hub 上的官方 Python 镜像为例。你可以选择使用python:3、python:3.9、python:3.9.7或许多其他版本标签的变体,还可以选择不同的基础操作系统,如 Windows、Alpine 和 Debian。
使用较为通用的标签,比如python:3,的优势在于,每次构建新镜像时,你会自动获取任何更新和安全补丁,以及 Python 3 的最新小版本。缺点是,有时这些更新可能会导致问题,比如系统包重命名或移除。你的应用在 Python 3.9 上可能运行良好,但如果构建新镜像后没有意识到基础镜像python:3实际上从 3.9 版本移动到了 3.10 版本,可能会因为新的 3.10 版本引入的变更而开始失败。
如果你使用更具体的标签,比如python:3.9.7,你的基础镜像不太可能意外更改。然而,你需要注意并手动更新你的 Dockerfile,以便在有重要的安全和错误修复时拉取这些更新。你可能更喜欢这种开发风格,因为你可以更好地控制你的构建过程,但定期检查基础镜像的更新是很重要的,以免滞后,因为它们将缺少维护者推送的安全修复。
你使用的镜像标签很大程度上取决于你的团队偏好、发布节奏和开发风格。你应该权衡你选择的标签系统的利弊,并定期检查,以确保你有一个可以提供合理可靠的构建并按可持续的速度进行常规更新的流程。
端口
在我们的演示应用程序中,您已经看到了ports字段在“服务资源”中的使用。它指定应用程序将监听的网络端口号,并且可以与服务匹配,用于将请求路由到容器。
资源请求和限制
我们已经在第五章详细介绍了容器的资源请求和限制,所以在这里简要回顾一下就够了。
每个容器可以作为其规范的一部分提供以下一个或多个:
-
resources.requests.cpu -
resources.requests.memory -
resources.limits.cpu -
resources.limits.memory
尽管在单个容器上指定了请求和限制,但我们通常根据 Pod 的总资源请求和限制来讨论。Pod 的资源请求是该 Pod 中所有容器的资源请求之和,等等。
镜像拉取策略
在容器可以在节点上运行之前,必须从适当的容器注册表中拉取或下载镜像。容器上的imagePullPolicy字段控制 Kubernetes 完成此操作的频率。它可以取三个值之一:Always、IfNotPresent或Never:
-
Always将在每次容器启动时拉取镜像。假设您指定了一个标签 —— 这是您应该做的(参见“最新标签”)——那么这可能是不必要的,并且可能会浪费时间和带宽。 -
IfNotPresent是默认值,适合大多数情况。如果镜像尚未在节点上存在,将下载它。之后,除非更改镜像规范,否则每次容器启动时都会使用保存的镜像,并且 Kubernetes 不会尝试重新下载它。 -
Never根本不会更新镜像。使用此策略时,Kubernetes 永远不会从注册表中获取镜像:如果节点上已经存在,将使用它,但如果不存在,则容器将无法启动。您不太可能希望这样做。
如果遇到奇怪的问题(例如,推送新容器镜像后 Pod 没有更新),请检查您的镜像拉取策略。
环境变量
环境变量是在运行时向容器传递信息的常见但有限的方式。常见是因为所有 Linux 可执行文件都可以访问环境变量,即使是在容器出现之前编写的程序也可以使用它们的环境进行配置。有限是因为环境变量只能是字符串值:没有数组,没有键值对,总之没有结构化数据。进程环境的总大小也被限制为 32 KiB,因此不能在环境中传递大数据文件。
要设置环境变量,请在容器的env字段中列出它:
containers:
- name: demo
image: cloudnatived/demo:hello
env:
- name: GREETING
value: "Hello from the environment"
如果容器镜像本身指定了环境变量(例如在 Dockerfile 中设置),那么 Kubernetes 的env设置将覆盖它们。这对于修改容器的默认配置非常有用。
小贴士
向容器传递配置数据的更灵活的方法是使用 Kubernetes 的 ConfigMap 或 Secret 对象:有关详细信息,请参阅第十章。
容器安全性
您可能已经注意到在 “什么是容器?” 中,当我们用 ps ax 命令查看容器中的进程列表时,所有进程都以 root 用户身份运行。在 Linux 和其他类 Unix 操作系统中,root 是超级用户,拥有读取任何数据、修改任何文件以及执行系统上任何操作的权限。
在完整的 Linux 系统上,一些进程需要以 root 身份运行(例如管理所有其他进程的 init 进程),但在容器中通常情况并非如此。
确实,当不需要时以 root 用户身份运行进程是一个坏主意。这违反了 最小权限原则。这一原则指出,程序只能访问它实际需要执行其工作的信息和资源。
程序会有 bug ——这是任何写过程序的人都明白的事实。一些 bug 允许恶意用户劫持程序去执行不应该做的事情,比如读取秘密数据或执行任意代码。为了减少这种风险,以最小可能的权限运行容器非常重要。
这始于不允许它们以 root 身份运行,而是分配给它们一个普通用户:一个没有特殊特权的用户,例如读取其他用户文件的特权。
就像你不应该在服务器上以 root 运行任何东西一样,在服务器上的容器中也不应该以 root 运行任何东西。运行在其他地方创建的二进制文件需要极大的信任,同样适用于容器中的二进制文件。
攻击者也可以利用容器运行时的 bug “逃离” 容器,并在主机上获取与容器中相同的权限。
以非 root 用户身份运行容器
下面是一个容器规范的示例,告诉 Kubernetes 以特定用户身份运行容器:
containers:
- name: demo
image: cloudnatived/demo:hello
securityContext:
`runAsUser``:` `1000`
runAsUser 的值是 UID(数字用户标识符)。在许多 Linux 系统上,UID 1000 被分配给系统上创建的第一个非 root 用户,因此通常选择 1000 或更高的值作为容器 UID 是安全的。无论容器中是否存在具有该 UID 的 Unix 用户,甚至容器中是否存在操作系统,这种方式都同样有效,即使是在空白容器中也是如此。
如果指定了 runAsUser UID,它会覆盖容器镜像中配置的任何用户。如果没有指定 runAsUser,但容器指定了一个用户,Kubernetes 将以该用户身份运行。如果在清单或镜像中都没有指定用户,容器将以 root 用户运行(正如我们所见,这是一个坏主意)。
为了最大的安全性,您应该为每个容器选择不同的 UID。这样,如果某个容器被某种方式妥协,或者意外覆盖数据,它只有访问自己数据的权限,而不是其他容器的权限。
另一方面,如果你希望两个或更多的容器能够通过挂载卷等方式访问相同的数据,你应该为它们分配相同的 UID。
阻止 Root 容器
为了防止这种情况发生,Kubernetes 允许您阻止容器以 root 用户身份运行。
使用runAsNonRoot: true设置将实现这一点:
containers:
- name: demo
image: cloudnatived/demo:hello
securityContext:
`runAsNonRoot``:` `true`
当 Kubernetes 运行该容器时,它将检查容器是否希望以 root 用户身份运行。如果是这样,它将拒绝启动。这将保护您免受忘记在容器中设置非 root 用户或运行配置为以 root 用户身份运行的第三方容器的影响。
如果发生这种情况,您将看到 Pod 状态显示为CreateContainerConfigError,当您使用kubectl describe查看 Pod 时,您将看到此错误:
Error: container has runAsNonRoot and image will run as root
最佳实践
使用runAsNonRoot: true设置以非 root 用户身份运行容器,并阻止 root 容器的运行。
设置只读文件系统
另一个有用的安全上下文设置是readOnlyRootFilesystem,它将阻止容器向自己的文件系统写入。例如,可以想象一个容器利用 Docker 或 Kubernetes 中的错误,将其文件系统的写入影响到主机节点上的文件。如果其文件系统是只读的,这种情况就不会发生;容器将收到 I/O 错误:
containers:
- name: demo
image: cloudnatived/demo:hello
securityContext:
`readOnlyRootFilesystem``:` `true`
许多容器不需要向自己的文件系统写入任何内容,因此这个设置不会对它们产生干扰。始终设置readOnlyRootFilesystem是良好的实践,除非容器确实需要写入文件。
禁用特权升级
通常,Linux 二进制文件以执行它们的用户拥有的权限运行。但是也有例外情况:使用setuid机制的二进制文件可以临时获取拥有该二进制文件的用户(通常为root)的权限。
这在容器中是一个潜在的问题,因为即使容器以常规用户(例如 UID 1000)身份运行,如果它包含一个setuid二进制文件,该二进制文件默认可以通过设置获得 root 权限。
要防止这种情况发生,请将容器安全策略中的allowPrivilegeEscalation字段设置为false:
containers:
- name: demo
image: cloudnatived/demo:hello
securityContext:
`allowPrivilegeEscalation``:` `false`
现代 Linux 程序不需要setuid;它们可以使用称为 capabilities 的更灵活和细粒度的权限机制来实现相同的功能。
能力
传统上,Unix 程序具有两个特权级别:normal 和 superuser。普通程序的权限不超过运行它们的用户,而超级用户程序可以执行任何操作,绕过所有内核安全检查。
Linux 的能力机制通过定义程序可以执行的各种具体操作来改进此问题:加载内核模块、执行直接网络 I/O 操作、访问系统设备等等。需要特定权限的程序可以被授予,但不能超出其它权限。
例如,监听端口 80 的 Web 服务器通常需要以 root 用户身份运行才能实现此功能;低于 1024 的端口号被视为特权 系统 端口。相反,程序可以被授予 NET_BIND_SERVICE 能力,允许其绑定到任何端口,但不给予其它特殊权限。
Docker 容器的默认能力集相当宽松。这是一个权衡安全性与可用性的实用决策:默认情况下不给容器设置任何能力会要求操作者在许多容器上设置能力才能运行。
另一方面,最小权限原则表明容器不应具有其不需要的任何能力。Kubernetes 安全上下文允许您从默认设置中删除任何能力,并根据需要添加能力,就像这个例子展示的那样:
containers:
- name: demo
image: cloudnatived/demo:hello
securityContext:
`capabilities``:`
`drop``:` `[``"``CHOWN``"``,` `"``NET_RAW``"``,` `"``SETPCAP``"``]`
`add``:` `[``"``NET_ADMIN``"``]`
容器将删除 CHOWN、NET_RAW 和 SETPCAP 能力,并添加 NET_ADMIN 能力。
Docker 文档列出了默认情况下容器设置的所有能力,并可以根据需要添加。
为了最大化安全性,您应该为每个容器放弃所有能力,并仅在需要时添加特定的能力:
containers:
- name: demo
image: cloudnatived/demo:hello
securityContext:
`capabilities``:`
`drop``:` `[``"``all``"``]`
`add``:` `[``"``NET_BIND_SERVICE``"``]`
容量机制在容器内部的进程能够做的事情上设置了硬限制,即使它们以 root 用户身份运行。一旦在容器级别放弃了某项能力,即使是以最高权限运行的恶意进程也无法重新获得该能力。
Pod 安全上下文
我们已经在单个容器级别上覆盖了安全上下文设置,但您也可以在 Pod 级别上设置其中的一些:
apiVersion: v1
kind: Pod
...
spec:
`securityContext``:`
`runAsUser``:` `1000`
`runAsNonRoot``:` `false`
`allowPrivilegeEscalation``:` `false`
这些设置将适用于 Pod 中的所有容器,除非容器在其自己的安全上下文中覆盖了特定设置。
Pod 服务账户
Pod 使用命名空间的默认服务账户权限运行,除非您另有指定(参见“应用程序和部署”)。如果出于某些原因需要授予额外的权限(例如查看其他命名空间中的 Pod),请为应用程序创建专用服务账户,将其绑定到所需的角色,并配置 Pod 使用新的服务账户。
要实现这一点,请在 Pod 规范的 serviceAccountName 字段中设置服务账户的名称:
apiVersion: v1
kind: Pod
...
spec:
`serviceAccountName``:` `deploy-tool`
卷
正如您可能记得的那样,每个容器都有自己的文件系统,只能由该容器访问,并且是 临时的:当容器重新启动时,不属于容器镜像的任何数据都将丢失。
通常情况下,这是可以的;例如,演示应用程序是一个无状态服务器,因此不需要持久存储。它也不需要与任何其他容器共享文件。
然而,更复杂的应用可能需要既能与同一 Pod 中的其他容器共享数据,又能在重启后保持数据持久性。Kubernetes Volume 对象可以提供这两者。
您可以将许多不同类型的 Volume 附加到 Pod 上。无论基础存储介质如何,挂载到 Pod 上的 Volume 对所有 Pod 中的容器都是可访问的。需要通过共享文件进行通信的容器可以使用各种类型的 Volume。我们将在以下部分讨论一些更重要的类型。
emptyDir Volumes
最简单的 Volume 类型是 emptyDir。这是一种临时存储,初始为空——因此得名——并将其数据存储在节点上(可以是内存中或节点的磁盘上)。它只在 Pod 在该节点上运行时存在。
当您想要为容器提供一些额外的存储空间,但数据永久保存或在容器移动到另一个节点时移动并不关键时,emptyDir 是有用的。一些示例包括缓存下载文件或生成内容,或者用于数据处理作业的临时工作空间。
同样地,如果您只想在 Pod 中的容器之间共享文件,但不需要长时间保留数据,那么 emptyDir Volume 是理想的选择。
这是一个创建 emptyDir Volume 并将其挂载到容器上的 Pod 示例:
apiVersion: v1
kind: Pod
...
spec:
volumes:
- name: cache-volume
emptyDir: {}
containers:
- name: demo
image: cloudnatived/demo:hello
volumeMounts:
- mountPath: /cache
name: cache-volume
首先,在 Pod 规范的 volumes 部分,我们创建一个名为 cache-volume 的 emptyDir Volume:
volumes:
- `name``:` `cache-volume`
`emptyDir``:` `{``}`
现在 cache-volume Volume 可供 Pod 中的任何容器挂载和使用。为此,我们在 demo 容器的 volumeMounts 部分列出它:
name: demo
image: cloudnatived/demo:hello
volumeMounts:
- `mountPath``:` `/cache`
`name``:` `cache-volume`
容器不必采取任何特殊操作即可使用新存储:任何写入路径 /cache 的内容将写入 Volume,并对挂载同一 Volume 的其他容器可见。所有挂载该 Volume 的容器都可以对其读写。
提示
在写入共享 Volume 时要小心。Kubernetes 不强制执行磁盘写入的任何锁定。如果两个容器同时尝试写入同一个文件,可能会导致数据损坏。为了避免这种情况,可以实现自己的写锁定机制,或使用支持锁定的 Volume 类型,如 nfs 或 glusterfs。
持久化 Volumes
尽管临时的 emptyDir Volume 适合缓存和临时文件共享,但某些应用需要存储持久数据;例如,任何类型的数据库。总的来说,我们不建议您试图在 Kubernetes 中运行数据库。通常情况下,最好使用云服务:例如,大多数云提供商都有管理解决方案,如 MySQL 和 PostgreSQL 的关系型数据库,以及键值(NoSQL)存储。
正如我们在“Kubernetes 不是万能药”中看到的,Kubernetes 最擅长管理无状态应用,这意味着没有持久化数据。存储持久化数据会显著复杂化应用程序的 Kubernetes 配置。您需要确保您的持久化存储可靠、高性能、安全且备份。
如果您需要在 Kubernetes 中使用持久卷,那么持久卷资源(PersistentVolume)正是您要找的。我们不会在这里详细介绍它,因为具体细节通常特定于您的云提供商;您可以在 Kubernetes 的文档中了解更多关于持久卷的信息。
在 Kubernetes 中最灵活使用持久卷的方式是创建一个 PersistentVolumeClaim 对象。这代表了对特定类型和大小持久卷的请求;例如,一个 10 GiB 大小的高速读写存储卷。
然后,Pod 可以将此持久卷索取作为一个卷,容器可以挂载和使用它:
volumes:
- name: data-volume
persistentVolumeClaim:
claimName: data-pvc
您可以在集群中创建一组持久卷(PersistentVolumes),供 Pod 按此方式索取。或者,您可以设置动态配置:当像这样的持久卷索取时,将自动配置并连接适当大小的存储到 Pod。
我们将在“有状态副本集”中更详细地讨论这个问题。
重启策略(Restart Policies)
正如我们在“运行容器进行故障排除”中看到的,当 Pod 退出时,Kubernetes 会始终重新启动它。因此,默认的重启策略是Always,但您可以将其更改为OnFailure(仅在容器以非零状态退出时重启)或Never:
apiVersion: v1
kind: Pod
...
spec:
`restartPolicy``:` `OnFailure`
如果您希望运行一个 Pod 直到完成并退出,而不是重新启动,您可以使用 Job 资源来实现这一点(参见“作业”)。
镜像拉取密钥(Image Pull Secrets)
如果容器注册表中没有指定的镜像,Kubernetes 将从该注册表下载。但是,如果您使用的是私有注册表呢?您如何提供给 Kubernetes 认证到注册表的凭据?
Pod 上的imagePullSecrets字段允许您进行配置。首先,您需要将注册表凭据存储在一个 Secret 对象中(有关详细信息,请参阅“Kubernetes Secrets”)。现在,您可以告诉 Kubernetes 在拉取 Pod 中的任何容器时使用此 Secret。例如,如果您的 Secret 命名为registry-creds:
apiVersion: v1
kind: Pod
...
spec:
`imagePullSecrets``:`
`-` `name``:` `registry-creds`
注册表凭据数据的确切格式在 Kubernetes 的文档中有描述。
您还可以将imagePullSecrets附加到服务账户上(参见“Pod 服务账户”)。使用此服务账户创建的任何 Pod 都将自动具有附加的注册表凭据。
初始容器(Init Containers)
如果你发现自己需要在运行主应用程序之前运行容器的情况下,可以使用一个初始化容器。
初始化容器在 Pod 规范中定义,工作方式基本与常规容器相同,但它们不使用存活探针或就绪探针。相反,初始化容器必须在 Pod 中的其他容器启动之前成功运行并退出:
apiVersion: v1
kind: Pod
...
spec:
containers:
- name: demo
image: cloudnatived/demo:hello
initContainers:
- name: init-demo
image: busybox
command: [*`sh`*, *`-c`*, *`echo` `Hello` `from` `initContainer`*]
这些可以在启动应用程序之前进行预检查,或者运行任何必要的引导脚本来准备你的应用程序。一个常见的初始化容器用例是从外部 Secret 存储中获取 Secrets,并在启动前将它们挂载到你的应用程序中。只需确保你的初始化容器是幂等的,并且如果决定使用它们,则可以安全地重试。
总结
要理解 Kubernetes,首先需要理解容器。在本章中,我们概述了容器的基本概念,它们如何在 Pod 中协同工作,以及可用于控制容器在 Kubernetes 中运行方式的选项。
基本要求:
-
Linux 容器在内核级别是一组隔离的进程集合,具有环形资源。从容器内部看,它看起来就像容器拥有一个专属的 Linux 机器。
-
容器不是虚拟机。每个容器应该运行一个主要进程。
-
一个 Pod 通常包含一个运行主要应用程序的容器,以及可选的辅助容器来支持它。
-
容器镜像规格可以包括注册表主机名、仓库命名空间、镜像仓库和标签;例如,
docker.io/cloudnatived/demo:hello。只有镜像名称是必需的。 -
为了可重现的部署,始终为容器镜像指定一个标签。否则,你将得到任意的
latest版本。 -
容器中的程序不应该以 root 用户身份运行。相反,分配给它们一个普通用户。
-
你可以在容器上设置
runAsNonRoot: true字段,以阻止任何想要作为root运行的容器。 -
容器的其他有用的安全设置包括
readOnlyRootFilesystem: true和allowPrivilegeEscalation: false。 -
Linux 权限提供了一种细粒度的权限控制机制,但是容器的默认权限可能过于宽松。你可以通过取消所有容器的权限,然后根据需要授予特定的权限来锁定你的 Pods。
-
相同 Pod 中的容器可以通过读取和写入挂载的 Volume 来共享数据。最简单的 Volume 类型是
emptyDir,它在开始时是空的,并且只在 Pod 运行时保留其内容。 -
另一方面,持久卷会根据需要保留其内容。Pods 可以使用持久卷声明动态配置新的持久卷。
-
初始化容器在 Pod 中启动应用程序之前进行初始设置非常有用。
第九章:管理 Pods
没有大问题,只有很多小问题。
亨利·福特
在前一章中,我们详细介绍了容器,并解释了在 Kubernetes 中,容器如何组合在一起形成 Pods。Pods 还有一些其他有趣的方面,我们将在本章中介绍,包括标签、使用节点亲和性指导 Pod 调度、使用污点和容忍来阻止 Pods 在特定节点上运行、使用 Pod 亲和性将 Pods 保持在一起或分开、以及使用诸如 DaemonSets 和 StatefulSets 等 Pod 控制器编排应用程序。我们还将介绍一些高级网络功能,包括 Ingress 控制器和服务网格工具。
标签
你知道 Pod(以及其他 Kubernetes 资源)可以附加标签,并且这些标签在连接相关资源(例如,将请求从 Service 发送到适当的后端)中起着重要作用。让我们在本节中更详细地了解标签和选择器。
什么是标签?
标签是附加到对象(例如 Pods)的键/值对。标签旨在用于指定对象的标识属性,这些属性对用户来说是有意义且相关的,但并不直接向核心系统提供语义。
Kubernetes 文档
换句话说,标签存在是为了为资源打上对我们有意义的信息,但它们对 Kubernetes 来说没有特别的意义。例如,通常会为 Pod 打上它们所属的应用程序的标签:
apiVersion: v1
kind: Pod
metadata:
labels:
`app``:` `demo`
现在,单独看这个标签没有任何效果。它作为文档仍然很有用:某人可以查看这个 Pod 并查看它运行的应用程序。但是标签的真正威力是在我们与选择器一起使用它时才体现出来。
选择器
选择器是匹配标签(或一组标签)的表达式。这是一种通过它们的标签指定一组资源的方法。例如,Service 资源有一个选择器,用于标识它将发送请求到的 Pods。还记得我们在“Service 资源”中的演示 Service 吗?
apiVersion: v1
kind: Service
...
spec:
...
`selector``:`
`app``:` `demo`
这是一个非常简单的选择器,匹配所有具有app标签且其值为demo的资源。如果一个资源根本没有app标签,它将不会匹配这个选择器。如果它有app标签,但其值不是demo,它也不会匹配选择器。只有符合条件的资源(在本例中为 Pod)才会匹配,所有这些资源都将被此 Service 选中。
标签不仅仅用于连接 Service 和 Pod;当使用kubectl get命令查询集群时,你可以直接使用它们,使用--selector标志:
`kubectl get pods --all-namespaces --selector app=demo`
NAMESPACE NAME READY STATUS RESTARTS AGE demo demo-5cb7d6bfdd-9dckm 1/1 Running 0 20s
你可能还记得从“使用简短标志”中学到的,--selector可以缩写为-l(用于标签)。
如果你想查看 Pod 上定义的标签,可以使用kubectl get命令的--show-labels标志:
`kubectl get pods --show-labels`
NAME ... LABELS demo-5cb7d6bfdd-9dckm ... app=demo,environment=development
更高级的选择器
大多数情况下,像app: demo这样的简单选择器(称为等值选择器)就足够了。你可以结合不同的标签来创建更具体的选择器:
`kubectl get pods -l app=demo,environment=production`
这将仅返回具有app: demo和environment: production标签的 Pods。这在 YAML 中等效于(例如在服务中):
selector:
`app``:` `demo`
`environment``:` `production`
像这样的等值选择器是 Service 中唯一可用的一种,但对于与kubectl进行交互式查询或更复杂的资源(如 Deployments),还有其他选项。
其中一个是选择不相等的标签:
`kubectl get pods -l app!=demo`
这将返回所有具有与demo不同值的app标签的 Pods,或者根本没有app标签的 Pods。
你还可以请求处于集合中的标签值:
kubectl get pods -l `` `*environment in (staging, production)*` ``
在 YAML 中的等效方式如下:
selector:
`matchExpressions``:`
`-` `{``key``:` `environment``,` `operator``:` `In``,` `values``:` `[``staging``,` `production``]``}`
你还可以请求不在给定集合中的标签值:
kubectl get pods -l `` `*environment notin (production)*` ``
这在 YAML 中的等效方式如下:
selector:
`matchExpressions``:`
`-` `{``key``:` `environment``,` `operator``:` `NotIn``,` `values``:` `[``production``]``}`
你可以在“使用节点亲和性控制调度”中看到另一个使用matchExpressions的例子。
标签的其他用途
我们已经看到如何使用app标签将 Pods 与 Services 链接起来(实际上,你可以使用任何标签,但app很常见)。但标签还有哪些其他用途呢?
在我们的演示应用程序的 Helm 图表中(参见“Helm 图表的内部结构是什么?”),我们设置了一个environment标签,例如可以是staging或production。如果你在同一个集群中运行 staging 和 production Pods(参见“我需要多个集群吗?”),你可能希望使用这样的标签来区分这两个环境。例如,你的服务选择器可能是:
selector:
app: demo
environment: production
如果没有额外的environment选择器,该服务将匹配具有app: demo的所有 Pods,包括可能不想要的 staging Pods。
根据你的应用程序,你可能希望使用标签以多种不同的方式对资源进行分组。以下是一些例子:
metadata:
labels:
app: demo
tier: frontend
environment: production
version: v1.12.0
role: primary
这允许你沿着这些不同的维度查询集群,了解正在发生的情况。
你也可以将标签用作金丝雀部署的一种方式(参见“金丝雀部署”)。如果你想向仅有少量 Pods 中部署新版本的应用程序,你可以使用像track: stable和track: canary这样的标签来进行两个独立的部署。
如果你的服务选择器只匹配app标签,它将发送流量到所有匹配该选择器的 Pods,包括stable和canary。你可以逐步增加canary Pods 的副本数量,以逐渐增加canary Pods 的比例。一旦所有运行中的 Pods 都在 canary 轨道上,你可以将它们重新标记为stable,然后开始下一个版本的过程。
标签和注释
你可能想知道标签和注释之间的区别是什么。它们都是提供有关资源元数据的键值对集合。
不同之处在于标签标识资源。它们用于选择相关资源组,例如服务的选择器中的标签。另一方面,注释用于非标识信息,供 Kubernetes 外的工具或服务使用。例如,在“Helm 钩子”中,有一个使用注释来控制 Helm 工作流程的示例。
因为标签通常用于对 Kubernetes 的内部查询是性能关键的,所以对有效标签有一些相当严格的限制。例如,标签名称限制为 63 个字符,尽管可以通过 DNS 子域的可选 253 字符前缀来扩展,用斜杠字符与标签分隔。标签只能以字母或数字(字母或数字)开头,并且只能包含字母数字字符以及破折号、下划线和点号。标签值也有类似的限制。
实际上,我们怀疑您会因为标签字符用尽而遇到问题,因为常见使用的大多数标签只是一个单词(例如,app)。
节点亲和力
我们在“使用节点亲和力控制调度”中简要提到了节点亲和力,与可抢占节点有关。在那一节中,您学习了如何使用节点亲和力优先在特定节点上调度 Pod(或者不调度)。现在让我们更详细地看一下节点亲和力。
在大多数情况下,您不需要节点亲和力。Kubernetes 在将 Pod 调度到正确节点方面非常聪明。如果所有节点都同样适合运行特定 Pod,则无需担心此问题。
然而,也有例外情况(例如上一个示例中的可抢占节点)。如果重新启动 Pod 成本高昂,您可能希望尽可能避免将其调度到可抢占节点上;可抢占节点可能在没有警告的情况下从集群中消失。您可以使用节点亲和力表达这种偏好。
Kubernetes 中有两种类型的亲和力:硬和软,分别称为:
-
requiredDuringSchedulingIgnoredDuringExecution(硬) -
preferredDuringSchedulingIgnoredDuringExecution(软)
记住,required表示硬亲和力(规则必须满足才能调度此 Pod),而preferred表示软亲和力(最好满足规则,但不是必须的)可能会有所帮助。
提示
硬亲和力和软亲和力类型的长名称表明这些规则适用于调度期间,但不适用于执行期间。也就是说,一旦将 Pod 调度到满足亲和性的特定节点上,它将保留在那里。如果在 Pod 运行时发生变化导致规则不再满足,Kubernetes 不会移动 Pod。
硬亲和力
亲和性通过描述您希望 Pod 在其上运行的节点类型来表达。可能有关于您希望 Kubernetes 为 Pod 选择节点的几条规则。每个规则都使用nodeSelectorTerms字段表示。这里是一个例子:
apiVersion: v1
kind: Pod
...
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: "topology.kubernetes.io/zone"
operator: In
values: ["us-central1-a"]
只有在us-central1-a区域的节点才会匹配此规则,因此总体效果是确保此 Pod 仅在该区域中调度。
软亲和性
软亲和性的表达方式基本相同,只是每个规则被分配一个从 1 到 100 的数值权重,以确定其对结果的影响。这里是一个例子:
preferredDuringSchedulingIgnoredDuringExecution:
- `weight``:` `10`
preference:
matchExpressions:
- key: "topology.kubernetes.io/zone"
operator: In
values: ["us-central1-a"]
- `weight``:` `100`
preference:
matchExpressions:
- key: "topology.kubernetes.io/zone"
operator: In
values: ["us-central1-b"]
因为这是一个“preferred…”规则,它是软亲和性:Kubernetes 可以将 Pod 调度到任何节点,但它会优先选择符合这些规则的节点。
您可以看到这两条规则具有不同的weight值。第一条规则的权重是 10,但第二条规则的权重是 100。如果有符合两条规则的节点,Kubernetes 会给予第二条规则匹配的节点 10 倍的优先级(位于可用性区us-central1-b)。
权重是表达您偏好的相对重要性的一种有用方式。
Pod 亲和性和反亲和性
我们已经看到您可以使用节点亲和性来引导调度程序向或远离在某些类型节点上运行 Pod。但是是否可以根据节点上已经运行的其他 Pod 来影响调度决策呢?
有时,一些 Pod 对在同一个节点上一起工作更好;例如,像 Redis 这样的 Web 服务器和内容缓存。如果您可以向 Pod 规范添加信息,告诉调度程序它希望与匹配特定标签集的 Pod 一起放置,那将非常有用。
相反,有时您希望 Pod 避免彼此。在“保持工作负载平衡”中,我们看到如果 Pod 副本最终在同一节点上结束,而不是分布在整个集群中可能会出现的问题。您能告诉调度程序避免在已经运行同一 Pod 的另一个副本的节点上调度 Pod 吗?
这正是您可以通过 Pod 亲和性做的事情。像节点亲和性一样,Pod 亲和性被表达为一组规则:要么是硬要求,要么是带有一组权重的软偏好。
保持 Pod 在一起
首先考虑第一个案例:将 Pod 一起调度。假设您有一个标记为app: server的 Pod,这是您的 Web 服务器,另一个标记为app: cache的 Pod,这是您的内容缓存。即使它们在不同的节点上,它们仍然可以一起工作,但如果它们在同一个节点上会更好,因为它们可以在不经过网络的情况下进行通信。您如何要求调度程序将它们放在一起?
下面是所需的 Pod 亲和性的示例,作为server Pod 规范的一部分表达。如果将其添加到cache规范或两个 Pod 都添加,效果将完全相同:
apiVersion: v1
kind: Pod
metadata:
name: server
labels:
app: server
...
spec:
affinity:
`podAffinity``:`
`requiredDuringSchedulingIgnoredDuringExecution``:`
`labelSelector``:`
`-` `matchExpressions``:`
`-` `key``:` `app`
`operator``:` `In`
`values``:` `[``"``cache``"``]`
`topologyKey``:` `kubernetes.io/hostname`
这种亲和性的整体效果是确保如果可能的话,server Pod 将安排在同时运行有带有 cache 标签的 Pod 的节点上。如果没有这样的节点,或者没有匹配的节点具有足够的空闲资源来运行 Pod,则无法运行。
在现实生活中,这可能不是您想要的行为。如果两个 Pod 绝对必须共存,请将它们的容器放在同一个 Pod 中。如果它们只是最好共存,请使用软 Pod 亲和性(preferredDuringSchedulingIgnoredDuringExecution)。
保持 Pod 分开
现在让我们看看反亲和性的情况:保持某些 Pod 分开。与其使用 podAffinity,我们使用 podAntiAffinity:
apiVersion: v1
kind: Pod
metadata:
name: server
labels:
app: server
...
spec:
affinity:
`podAntiAffinity``:`
`requiredDuringSchedulingIgnoredDuringExecution``:`
`labelSelector``:`
`-` `matchExpressions``:`
`-` `key``:` `app`
`operator``:` `In`
`values``:` `[``"``server``"``]`
`topologyKey``:` `kubernetes.io/hostname`
这与之前的例子非常相似,只是它是 podAntiAffinity,因此表达了相反的意义,匹配表达式也不同。这次,表达式是:“app 标签必须具有 server 的值。”
这种亲和性的效果是确保 Pod 不会被安排在符合此规则的任何节点上。换句话说,不能将带有 app: server 标签的 Pod 安排在已运行 app: server Pod 的节点上。这将在集群中平均分布 server Pod,但可能会以所需副本数量为代价。
软反亲和性
然而,我们通常更关心是否有足够的副本可用,而不是尽可能公平地分配它们。硬规则并不是我们真正想要的。让我们稍微修改一下,使其成为软反亲和性:
affinity:
`podAntiAffinity``:`
`preferredDuringSchedulingIgnoredDuringExecution``:`
`-` `weight``:` `1`
`podAffinityTerm``:`
`labelSelector``:`
`-` `matchExpressions``:`
`-` `key``:` `app`
`operator``:` `In`
`values``:` `[``"``server``"``]`
`topologyKey``:` `kubernetes.io/hostname`
请注意,现在规则是 preferred...,而不是 required...,使其成为软反亲和性。如果可以满足规则,将会满足,但如果不能,Kubernetes 仍将安排 Pod。
因为这是一种偏好,我们指定一个 weight 值,就像我们为软节点亲和性所做的一样。如果有多个亲和性规则需要考虑,Kubernetes 将根据您为每条规则分配的权重进行优先级排序。
何时使用 Pod 亲和性
就像节点亲和性一样,您应将 Pod 亲和性视为特殊情况的精细调整增强。调度程序已经很擅长放置 Pod,以获得集群的最佳性能和可用性。Pod 亲和性限制了调度程序的自由度,以换取一个应用程序来换取另一个应用程序。
污点和容忍性
在“节点亲和性”中,您了解到 Pod 的一个属性可以将其引导向(或远离)一组节点。相反,污点 允许节点根据节点的某些属性排斥一组 Pod。
例如,您可以使用污点来保留特定节点:仅用于特定类型的 Pod 的节点。如果节点上存在某些问题,例如低内存或缺乏网络连接,Kubernetes 也会为您创建污点。
要向特定节点添加污点,请使用 kubectl taint 命令:
`kubectl taint nodes docker-for-desktop dedicated=true:NoSchedule`
这会在 docker-for-desktop 节点上添加一个名为 dedicated=true 的污点,并设置效果为 NoSchedule:除非具有匹配的 容忍,否则现在无法在此处安排任何 Pod。
要查看特定节点上配置的污点,请使用 kubectl describe node...。
要从节点上删除一个污点,请重复执行 kubectl taint 命令,但在污点名称后加上一个减号:
`kubectl taint nodes docker-for-desktop dedicated:NoSchedule-`
容忍是描述 Pod 的属性,用于描述它们与之兼容的污点。例如,要使 Pod 能够容忍 dedicated=true 污点,请将此添加到 Pod 的规范中:
apiVersion: v1
kind: Pod
...
spec:
`tolerations``:`
`-` `key``:` `"``dedicated``"`
`operator``:` `"``Equal``"`
`value``:` `"``true``"`
`effect``:` `"``NoSchedule``"`
这实际上是在说:“此 Pod 允许在具有效果为 NoSchedule 的 dedicated=true 污点的节点上运行。”由于容忍 匹配 了污点,因此可以安排该 Pod。任何没有这种容忍的 Pod 将不被允许在带有污点的节点上运行。
当由于有污点的节点而无法运行 Pod 时,它将保持 Pending 状态,并且在 Pod 描述中会看到类似以下的消息:
Warning FailedScheduling 4s (x10 over 2m) default-scheduler 0/1 nodes are
available: 1 node(s) had taints that the pod didn't tolerate.
污点和容忍的其他用途包括标记具有专用硬件(如 GPU)的节点,并允许某些 Pod 容忍某些节点问题。
例如,如果一个节点掉线,Kubernetes 会自动添加污点 node.kubernetes.io/unreachable。通常情况下,这会导致其 kubelet 从节点上驱逐所有 Pod。然而,您可能希望保持某些 Pod 运行,希望网络能在合理时间内恢复。为此,您可以为这些 Pod 添加与 unreachable 污点匹配的容忍。
您可以在 Kubernetes 文档 中了解有关污点和容忍的更多信息。
Pod 控制器
在本章中我们已经讨论了许多关于 Pod 的内容,这是有道理的:所有 Kubernetes 应用程序都在一个 Pod 中运行。但您可能会想,为什么我们还需要其他类型的对象呢?仅仅为一个应用程序创建一个 Pod 并运行它就足够了吗?
这实际上是直接使用 docker container run 运行容器时得到的结果,正如我们在 “运行容器映像” 中所做的那样。它有效,但非常有限:
-
如果容器由于某些原因退出,您必须手动重新启动它。
-
如果手动运行它们,那么您的容器只有一个副本,而且无法在多个副本之间进行负载均衡。
-
如果要高可用的副本,您必须决定在哪些节点上运行它们,并注意保持集群的平衡。
-
当您更新容器时,必须小心依次停止每个运行中的映像,拉取新映像并重新启动它。
这正是 Kubernetes 通过控制器来为您处理的工作。在“ReplicaSets”中,我们介绍了 ReplicaSet 控制器,它管理特定 Pod 的一组副本。它持续工作以确保始终存在指定数量的副本,如果副本不足,则启动新副本;如果副本过多,则停止部分副本。
您现在还熟悉了 Deployment,正如我们在“Deployments”中看到的,它管理 ReplicaSets 来控制应用程序更新的推出。当您更新 Deployment 时,例如使用新的容器规范,它会创建一个新的 ReplicaSet 来启动新的 Pod,并最终关闭管理旧 Pod 的 ReplicaSet。
对于大多数简单应用程序来说,Deployment 就足够了。但在本节中,我们将简要介绍一些其他有用的 Pod 控制器。
DaemonSets
假设您希望将所有应用程序的日志发送到集中式日志服务器,如 Elasticsearch-Logstash-Kibana(ELK)堆栈或诸如 Datadog 这样的 SaaS 监控产品(见“Datadog”)。有几种方法可以做到这一点。
每个应用都可以包含连接到日志服务、认证、写日志等代码,但这样会导致大量重复的代码,效率低下。
或者,您可以在每个 Pod 中运行一个额外的容器作为日志代理(这称为sidecar模式)。这意味着应用程序不需要内置如何与日志服务通信的知识,但这也意味着您可能会在节点上运行几个相同的日志代理副本。
由于它只是管理与日志服务的连接并将日志消息传递给它,因此您实际上只需要在每个节点上有一个日志代理副本。这是一个如此常见的需求,以至于 Kubernetes 为此提供了一个特殊的控制器对象:DaemonSet。
提示
术语守护进程传统上指运行在服务器上处理诸如日志记录之类的长时间后台进程,因此类比,Kubernetes 的 DaemonSets 在集群中的每个节点上运行一个守护进程容器。
一个 DaemonSet 的清单,正如您可能期望的那样,看起来非常像 Deployment 的清单:
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: fluentd-elasticsearch
...
spec:
...
template:
...
spec:
containers:
- name: fluentd-elasticsearch
...
当需要在集群的每个节点上运行一个 Pod 的副本时,请使用 DaemonSet。如果您运行的是一个应用程序,维护指定数量的副本比确切地在哪个节点上运行 Pod 更重要,请改用 Deployment。
StatefulSets
像 Deployment 或 DaemonSet 一样,StatefulSet 是一种 Pod 控制器。StatefulSet 添加的功能是能够按特定顺序启动和停止 Pod。
例如,使用 Deployment 时,所有 Pod 都以随机顺序启动和停止。对于无状态服务来说,这是可以接受的,因为每个副本都是相同的,执行相同的工作。
然而,有时您需要按特定编号顺序启动 Pod,并能够通过其编号进行识别。例如,分布式应用程序(如 Redis、MongoDB 或 Cassandra)会创建自己的集群,并需要能够通过可预测的名称识别集群领导者。
对于这种情况,StatefulSet 是理想选择。例如,如果您创建一个名为 redis 的 StatefulSet,第一个启动的 Pod 将被命名为 redis-0,Kubernetes 将等待该 Pod 准备就绪后再启动下一个,即 redis-1。
根据应用程序的不同,您可以利用这一特性以可靠的方式对 Pod 进行集群化。例如,每个 Pod 可以运行一个启动脚本,检查它是否正在 redis-0 上运行。如果是,则它将成为 Redis 集群的领导者。如果不是,则将尝试通过联系 redis-0 作为副本加入集群。
在 Kubernetes 中,每个 StatefulSet 的副本必须在下一个开始之前运行并准备就绪,类似地,在终止 StatefulSet 时,将按相反顺序关闭副本,并等待每个 Pod 完成后再继续。
除了这些特殊属性外,StatefulSet 看起来与普通的部署(Deployment)非常相似:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis
spec:
selector:
matchLabels:
app: redis
serviceName: "redis"
replicas: 3
template:
...
要能够通过可预测的 DNS 名称(如 redis-1)访问每个 Pod,还需要创建一个 clusterIP 类型为 None 的服务(称为 无头服务)。
使用非无头服务(nonheadless Service),您会得到一个单一的 DNS 条目(比如 redis),它负载均衡到所有后端 Pod。使用无头服务时,您仍然会得到单一的服务 DNS 名称,但同时还会为每个编号的 Pod(比如 redis-0、redis-1、redis-2等)得到单独的 DNS 条目。
需要加入 Redis 集群的 Pod 可以特定地联系 redis-0,但只需要负载均衡 Redis 服务的应用程序可以使用 redis DNS 名称与随机选择的 Redis Pod 进行通信。
StatefulSets 还可以管理其 Pod 的磁盘存储,使用 VolumeClaimTemplate 对象自动创建 PersistentVolumeClaim(参见“持久卷”)。
作业
Kubernetes 中另一个有用的 Pod 控制器类型是作业(Job)。与部署(Deployment)运行指定数量的 Pod 并持续重启不同,作业仅运行指定次数的 Pod。完成后,作业被认为已完成。
例如,批处理任务或队列工作 Pod 通常启动,完成工作,然后退出。这是由作业管理的理想候选者。
控制作业执行的两个字段是 completions 和 parallelism。首先,completions 确定指定 Pod 需要成功运行的次数,然后作业才被认为已完成。默认值为 1,意味着 Pod 将运行一次。
parallelism 字段指定一次应该运行多少个 Pod。同样,默认值为 1,意味着一次只会运行一个 Pod。
例如,假设您想运行一个队列工作 Job,其目的是从队列中消耗工作项。您可以将 parallelism 设置为 10,并且不设置 completions。这将启动 10 个 Pod,每个 Pod 将继续从队列中消耗工作,直到没有更多的工作可做,然后退出,此时 Job 将完成:
apiVersion: batch/v1
kind: Job
metadata:
name: queue-worker
spec:
completions: 10
template:
metadata:
name: queue-worker
spec:
containers:
...
另外,如果您想运行一个单独的一次性任务,您可以将 completions 和 parallelism 都设置为 1。这将启动一个 Pod 的副本,并等待其成功完成。如果它崩溃、失败或以任何非成功的方式退出,Job 将重新启动它,就像 Deployment 一样。只有成功的退出才算作所需的 completions 数量。
如何启动一个 Job?您可以通过使用 kubectl 或 Helm 应用 Job 清单来手动执行它。另外,Job 可能会被自动触发;例如您的持续部署流水线(参见第十四章)。
当您的 Job 完成时,如果您希望 Kubernetes 在完成后自动清理,您可以使用 ttlSecondsAfterFinished 设置。一旦作业退出后指定的秒数过去,它将自动被删除。您还可以将 ttlSecondsAfterFinished 设置为 0,这意味着您的 Job 将在完成后立即被删除。
当您需要定期运行 Job,例如在一天的特定时间或给定间隔内,Kubernetes 还提供了 CronJob 对象。
CronJobs
在 Unix 环境中,定时作业由 cron 守护进程运行(其名称来自希腊语单词 χρόνος,意思是“时间”)。因此,它们被称为 CronJobs,而 Kubernetes 的 CronJob 对象正是做同样的事情。
CronJob 的样子是这样的:
apiVersion: batch/v1
kind: CronJob
metadata:
name: demo-cron
spec:
schedule: "*/1 * * * *"
jobTemplate:
spec:
containers:
...
在 CronJob 清单中要查看的两个重要字段是 spec.schedule 和 spec.jobTemplate。schedule 字段指定作业将使用与 Unix cron 实用程序相同的格式运行的时间表。
jobTemplate 指定要运行的 Job 的模板,与普通的 Job 清单完全相同(参见“Jobs”)。
水平 Pod 自动伸缩器
请记住,Deployment 控制器维护指定数量的 Pod 副本。如果一个副本失败,将启动另一个副本以取代它,以实现目标副本数。
在 Deployment 清单中设置了所需的副本数,并且我们已经看到,您可以根据流量情况调整此值以增加 Pod 的数量,或者在 Pod 无人使用时减少以缩减 Deployment。
但是,如果 Kubernetes 可以自动调整副本数量,以响应增加的需求呢?这正是 水平 Pod 自动伸缩器 的作用。(水平 扩展指调整服务的副本数量,与 垂直 扩展形成对比,后者根据 CPU 或内存大小调整单个副本的大小。)
水平 Pod 自动缩放器(HPA)会监视指定的 Deployment,不断监视给定的度量标准,以确定是否需要增加或减少副本数量。
最常见的自动缩放指标之一是 CPU 利用率。记住来自“资源请求”的内容,Pod 可以请求一定数量的 CPU 资源;例如,500 毫核。随着 Pod 的运行,其 CPU 使用率会波动,这意味着在任何给定时刻,Pod 实际上使用了其原始 CPU 请求的一部分百分比。
您可以根据此值自动缩放 Deployment:例如,您可以创建一个 HPA,该 HPA 的目标是 Pod 的 80% CPU 利用率。如果 Deployment 中所有 Pod 的平均 CPU 使用率仅为其请求量的 70%,HPA 将通过减少目标副本数来进行缩减。如果 Pod 的工作负载不太重,我们就不需要那么多 Pod,HPA 可以将它们缩减。
另一方面,如果平均 CPU 利用率为 90%,这超过了 80% 的目标,因此我们需要增加更多的副本,直到平均 CPU 使用率降下来。HPA 将修改 Deployment 以增加目标副本数。
每当 HPA 确定需要进行缩放操作时,它将根据实际度量值与目标的比率调整副本数。如果 Deployment 与目标 CPU 利用率非常接近,HPA 只会添加或删除少量副本;但如果它明显偏离,HPA 将以更大的数量进行调整。
HPA 使用另一个名为 Metrics Server 的流行 Kubernetes 项目来获取其自动缩放决策所需的数据。您可以按照 metrics-server 仓库 中的说明进行安装。
这是基于 CPU 利用率的 HPA 示例:
apiVersion: autoscaling/v2beta2
kind: HorizontalPodAutoscaler
metadata:
name: demo-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: demo
minReplicas: 1
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 80
这里的有趣字段包括:
-
spec.scaleTargetRef指定要缩放的 Deployment -
spec.minReplicas和spec.maxReplicas指定缩放的限制 -
spec.metrics确定用于缩放的度量标准
尽管 CPU 利用率是最常见的缩放指标,但您可以使用 Kubernetes 可用的任何度量标准,包括内置的系统度量,如 CPU 和内存使用情况,以及应用程序特定的服务度量,您可以从应用程序定义并导出(参见第十六章)。例如,您可以根据应用程序的错误率或每秒传入请求的数量进行缩放。
您可以在 Kubernetes 文档 中详细了解自动缩放器和自定义度量标准。
根据已知计划进行自动缩放
HorizontalPodAutoscaler与CronJob结合使用时,在应用程序的流量模式基于一天中的时间可预测的情况下非常有用。例如,如果您确切地知道每天早上 8 点之前需要 20 个 Pod 处于运行状态以处理一大波请求,那么您可以创建一个CronJob,在这个时间之前使用内部服务帐户运行kubectl命令进行扩展(参见“Pod Service Accounts”):
apiVersion: batch/v1
kind: CronJob
...
args:
- "kubectl patch hpa service-hpa --patch *`{``\"``spec``\"``:{``\"``minReplicas``\"``:20}}`*"
...
然后,您可以在业务结束时创建类似的CronJob来缩减minReplicas。当与如“Autoscaling”中讨论的集群自动缩放结合使用时,您可以使用这个技巧来节省总计算成本。
对于您的用例来说,使用普通的 HPA 而不是 cron 可能运行良好,但请记住,扩展新节点和 Pods 并不会立即发生。在您已经知道将需要一定容量来处理即将到来的负载高峰的情况下,添加一个CronJob可以确保您在高峰开始时所有资源都已经运行。
操作员和自定义资源定义(CRDs)
我们在“StatefulSets”中看到,虽然标准的 Kubernetes 对象如 Deployment 和 Service 对于简单的无状态应用程序很好,但它们也有它们的限制。一些应用程序需要多个协作的 Pods,这些 Pods 必须按特定顺序初始化(例如,复制的数据库或集群服务)。
对于需要更复杂管理或复杂资源类型的应用程序,Kubernetes 允许您创建自己的新类型的对象。这些被称为自定义资源定义(CRDs)。例如,Velero 备份工具创建并使用它称为Configs和Backups的新自定义 Kubernetes 对象(参见“Velero”)。
Kubernetes 被设计为可扩展的,您可以自由地定义和创建任何类型的对象,使用 CRD 机制。一些 CRD 只是存在来存储数据,就像 Velero 的BackupStorageLocation对象一样。但是您可以更进一步,创建像 Deployment 或 StatefulSet 一样作为 Pod 控制器的对象。
例如,如果您想要创建一个控制器对象,在 Kubernetes 中设置复制的高可用性 MySQL 数据库集群,您会如何去做呢?
第一步是为您的自定义控制器对象创建一个 CRD。为了使其起作用,您需要编写一个与 Kubernetes API 通信的程序。正如我们在“Building Your Own Kubernetes Tools”中看到的那样,这很容易做到。这样的程序被称为操作员(可能是因为它自动执行人类操作员可能执行的动作)。
您可以在OperatorHub.io 站点看到社区构建和维护的大量 Operator 示例。这是一个包含数百个 Operator 的仓库,您可以在您的集群上安装它们,或者只需浏览它们的代码以获取构建您自己 Operator 的想法。
Ingress
虽然服务(见“服务资源”)用于在集群内部路由内部流量(例如,从一个微服务到另一个微服务),Ingress 用于将外部流量路由到您的集群,并将其路由到适当的微服务(见图 9-1)。您可以将 Ingress 的概念看作是一个负载均衡器,与 Service 协调工作,根据其标签选择器将来自外部客户端的请求发送到正确的 Pod。所有这些都是通过一个 Ingress 控制器来实现的,我们稍后会详细介绍。现在,让我们看看典型的 Ingress 资源是如何在集群外部暴露您的应用程序的。

图 9-1. Ingress 资源
这是一个通用 Ingress 资源的清单:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: demo-ingress
spec:
rules:
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: demo-service
port:
number: 8888
此示例中的这个 Ingress 查看名为demo-service的 Service,然后该 Service 使用选择器标签和就绪状态(见“就绪探针”)来确定适合接收请求的 Pod。不符合demo-service中定义的选择器标签的 Pod 以及具有失败的“就绪”状态的任何 Pod 将不会接收任何请求。除了基本的请求路由外,Ingress 还可以处理更高级的任务,例如管理 SSL 证书、速率限制以及与负载均衡器常见相关的其他功能。这些功能的具体实现由 Ingress 控制器处理。
Ingress 控制器
Ingress 控制器负责管理集群中的 Ingress 资源。根据您运行集群的位置以及您需要的功能,您使用的控制器可能会有所不同。
通常,选择要使用的 Ingress 控制器,并配置控制器的行为,是通过 Ingress 清单中的注释来完成的。例如,要在 EKS 集群中使用面向公众的 AWS 应用负载均衡器,您可以添加类似以下的注释:
...
kind: Ingress
metadata:
name: demo-aws-ingress
annotations:
kubernetes.io/ingress.class: alb
alb.ingress.kubernetes.io/scheme: internet-facing
...
每个 Ingress 控制器都有其自己的一组注释,用于配置该控制器提供的各种功能。
在 GCP 中托管的 GKE 集群可以使用Google Cloud 的负载均衡器控制器(GLBC)来处理 Ingress 资源。AWS 有一个类似的产品,我们之前提到过叫做AWS 负载均衡器控制器,而 Azure 也有其自己的应用程序网关 Ingress 控制器(AGIC)。如果您使用其中一个主要的公共云提供商,并且有需要将应用程序暴露在集群外部的情况,我们建议首先探索使用您的云提供商维护的特定 Ingress 控制器。
您还可以选择在集群内安装和使用不同的 Ingress 控制器,甚至如果需要,可以运行多个控制器。有许多不同的 Ingress 控制器选项,其中一些较受欢迎的包括:
NGINX 长久以来一直是一款流行的负载均衡工具,甚至在 Kubernetes 出现之前也是如此。nginx-ingress项目由 Kubernetes 社区维护。
这个控制器由 NGINX 公司本身支持。这个项目与前面段落提到的 Kubernetes 社区项目有一些区别。
Contour 实际上在底层使用另一个工具叫做Envoy来代理客户端和 Pod 之间的请求。
这是一个轻量级的代理工具,还可以自动管理您的 Ingress 的 TLS 证书。
Kong 托管了Kong 插件中心,其中的插件与他们的 Ingress 控制器集成,用于配置 OAuth 认证、LetsEncrypt 证书、IP 限制、指标和其他负载均衡器的有用功能。
多年来,HAProxy 一直是另一款流行的负载均衡工具,他们也为 Kubernetes 提供了自己的 Ingress 控制器以及一个 Helm 图表,用于在您的集群中安装它。
Ingress 规则
Ingress 还可以用于将流量转发到不同的后端服务,具体取决于您指定的某些规则。其中一个常见用法是根据请求 URL 将请求路由到不同的位置,称为fanout:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: fanout-ingress
spec:
rules:
- http:
paths:
- path: /hello
backend:
serviceName: hello
servicePort: 80
- path: /goodbye
backend:
serviceName: goodbye
servicePort: 80
使用 Ingress 终止 TLS
大多数 Ingress 控制器还可以使用传输层安全性(TLS)(前身为安全套接层[SSL]协议)来保护连接。这通常使用一个 Kubernetes Secret(我们将在“Kubernetes Secrets”中介绍)来存储证书和密钥的内容,并在 Ingress 清单的tls部分进行配置:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: demo-ingress
spec:
tls:
- secretName: demo-tls-secret
backend:
serviceName: demo-service
servicePort: 80
...
---
apiVersion: v1
kind: Secret
type: kubernetes.io/tls
metadata:
name: demo-tls-secret
data:
tls.crt: LS0tLS1CRUdJTiBDRV...LS0tCg==
tls.key: LS0tLS1CRUdJTiBSU0...LS0tCg==
使用 Cert-Manager 自动化 LetsEncrypt 证书
如果您想要使用流行的Let's Encrypt颁发机构(或其他 ACME 证书提供者)自动请求和更新 TLS 证书,您可以使用cert-manager。
如果在您的集群中运行cert-manager,它将自动检测没有证书的 TLS 入口,并从指定的提供者请求证书。它还可以在证书接近到期时自动续订这些证书。
服务网格
Kubernetes 的 Ingress 和 Services 可能是从客户端到您的应用程序路由请求所需的全部内容,这取决于您组织的复杂性。但是,有一个越来越大的兴趣集中在一个称为服务网格的新概念上。服务网格负责管理更复杂的网络操作,例如限流和加密微服务之间的网络流量。服务网格工具还可以为流经网络的请求添加度量和日志记录,跟踪请求花费的时间长短,或跟踪请求从何处开始以及沿途通过各种微服务的路径。某些服务网格工具可以处理失败请求的自动重试,并具有根据需要拒绝或阻止入站或出站请求的能力。
有几种实现服务网格的选项,我们将在这里列出。我们预计服务网格工具的领域将继续在未来几年迅速发展,并将成为任何云原生基础架构堆栈的核心部分。如果您刚开始部署几个应用程序,那么您可能只需使用 Kubernetes 提供的标准服务和入口资源即可开始。但是,如果您发现自己需要深入研究服务网格的这些更高级功能,这些都是一些值得探索的好选择。
Istio
Istio 是与提供服务网格相关的最早的工具之一。它作为许多托管 Kubernetes 集群的可选附加组件提供,包括GKE。如果您想自己安装 Istio,请参阅Istio 安装文档获取更多信息。Istio: Up and Running(O'Reilly 出版社)是学习有关 Istio 及与服务网格通常相关的更广泛概念的好书。
Linkerd
Linkerd提供了许多关键的服务网格功能,但与 Istio 相比,它的占用空间更小,复杂性更低。它可用于设置服务之间的互相 TLS,收集请求速率和延迟的度量,蓝绿部署以及请求重试。
Consul Connect
在 Kubernetes 广为人知和使用之前,HashiCorp 提供了一个名为 Consul 的流行工具,专注于服务发现。它处理应用程序的健康检查和在分布式计算环境中自动路由请求到正确位置。这部分功能现在在 Kubernetes 中已经本地化处理,但 Consul 现在扩展到包括一个名为 Consul Connect 的新工具,具有服务网格功能。如果您运行在 Kubernetes 之外的混合环境中的应用程序,或者已经熟悉使用 Consul,则可以考虑探索 Consul Connect 作为您的服务网格的价值。
NGINX 服务网格
除了 Ingress 控制器外,NGINX 还为 Kubernetes 提供了一个完整的 服务网格 产品。它还使用 sidecar 模式,其中一个 NGINX 容器与您的应用程序并行运行,并处理网络流量的路由。该服务网格容器提供 mTLS 加密、流量分割能力,并跟踪网络性能指标以进行可观察性分析。
摘要
最终,在 Kubernetes 中一切都围绕着运行 Pods 进行。这些 Pods 可以使用 Kubernetes 控制器和对象进行配置和管理,无论是长期运行的进程还是短期作业和 CronJobs。更复杂的设置可能使用自定义资源定义和运算符。将网络请求路由到 Pods 包括使用 Services 和 Ingress 控制器。
需要记住的基本思想:
-
Labels 是标识资源的键值对,可以与选择器一起使用以匹配指定组的资源。
-
Node affinities 吸引或排斥 Pods 到或远离具有指定属性的节点。例如,您可以指定 Pod 只能运行在指定可用区的节点上。
-
虽然硬节点亲和性可以阻止 Pod 运行,但软节点亲和性更像是对调度器的建议。您可以将多个具有不同权重的软亲和性组合起来。
-
Pod 亲和性表达了希望将 Pods 调度到与其他 Pods 相同节点上的偏好。例如,受益于在同一节点上运行的 Pods 可以使用 Pod 亲和性来表达这一点。
-
Pod anti-affinities 反对其他 Pods 而不是吸引它们。例如,对相同 Pod 副本的反亲和性可以帮助在集群中均匀地分布您的副本。
-
Taints 是一种标记节点特定信息的方式,通常涉及节点问题或故障。默认情况下,Pods 不会被调度到有 taint 的节点上。
-
Tolerations 允许一个 Pod 被调度到具有特定 taint 的节点上。您可以使用这种机制仅在专用节点上运行某些 Pods。
-
DaemonSets 允许您在每个节点上调度一个 Pod 的副本(例如日志代理)。
-
StatefulSets 按特定编号顺序启动和停止 Pod 副本,允许您通过可预测的 DNS 名称来访问每个副本。这对于集群应用程序(如数据库)非常理想。
-
作业(Jobs)在完成之前运行一个 Pod(或指定次数)。类似地,CronJobs 定期在指定的时间运行一个 Pod。
-
水平 Pod 自动缩放器(HPA)监视一组 Pod,试图优化给定的指标(如 CPU 利用率)。它们增加或减少所需的副本数量以达到指定的目标。
-
自定义资源定义(CRD)允许您创建自己的自定义 Kubernetes 对象,以存储您希望的任何数据。运算符是 Kubernetes 客户端程序,可以为您的特定应用程序实现编排行为。OperatorHub.io 是搜索社区构建的运算符的绝佳资源。
-
Ingress 资源根据一组规则将请求路由到不同的服务,例如匹配请求 URL 的部分。它们还可以为您的应用程序终止 TLS 连接。
-
Istio、Linkerd 和 Consul Connect 是高级服务网格工具,为微服务环境提供网络功能,如加密、QoS、指标、日志记录以及更复杂的路由策略。
第十章:配置和 Secrets
如果你想保守一个秘密,你必须也要从自己那里隐藏它。
George Orwell,1984
能够将 Kubernetes 应用程序的逻辑与其配置分离开来非常有用:即可能在应用程序生命周期内发生变化的任何值或设置。配置值通常包括诸如特定于环境的设置、第三方服务的 DNS 地址和身份验证凭据等内容。
虽然您可以直接将这些值放入您的代码中,但这并不是一个非常灵活的方法。首先,更改配置值将需要完全重建和重新部署应用程序。最好是将这些值从代码中分离出来,并从文件或环境变量中读取它们。
Kubernetes 提供了几种不同的方法来帮助您管理配置。一种方法是通过 Pod 规范中的环境变量向应用程序传递值(参见 “环境变量”)。另一种方法是直接将配置数据存储在 Kubernetes 中,使用 ConfigMap 和 Secret 对象。
在本章中,我们将详细探讨 ConfigMaps 和 Secrets,并查看一些管理应用程序中配置和密钥的实用技术,使用演示应用程序作为示例。
ConfigMaps
ConfigMap 是 Kubernetes 中存储配置数据的主要对象。您可以将其视为一个命名的键值对集合,用于存储配置数据。一旦创建了 ConfigMap,您可以通过在 Pod 中创建文件或将其注入到 Pod 的环境中来为应用程序提供数据。
在本节中,我们将探讨将数据输入到 ConfigMap 中的几种不同方法,然后探索将这些数据提取并馈送到您的 Kubernetes 应用程序中的各种方法。
创建 ConfigMaps
假设您想在 Pod 的文件系统中创建一个名为 config.yaml 的 YAML 配置文件,并具有以下内容:
autoSaveInterval: 60
batchSize: 128
protocols:
- http
- https
有了这些值集合,您如何将它们转换为可应用于 Kubernetes 的 ConfigMap 资源?
一种方式是将数据指定为 ConfigMap 清单中的 YAML 字面值。这是 ConfigMap 对象清单的样子:
apiVersion: v1
kind: ConfigMap
metadata:
name: demo-config
data:
config.yaml: |
autoSaveInterval: 60
batchSize: 128
protocols:
- http
- https
您可以通过从头开始编写清单来创建 ConfigMap,并将 config.yaml 中的值添加到 data 部分,就像我们在这个例子中所做的那样。
不过,更简单的方法是让 kubectl 为您做一些工作。您可以直接从 YAML 文件创建 ConfigMap 如下所示:
`kubectl create configmap demo-config --from-file=config.yaml`
configmap "demo-config" created
要导出与此 ConfigMap 对应的清单文件,请运行:
`kubectl get configmap/demo-config -o yaml ` `>demo-config.yaml`
这将写入集群的 ConfigMap 资源的 YAML 清单表示到文件 demo-config.yaml,但它将包含额外的信息,如 status 部分,您可能希望在再次应用之前将其删除(参见 “导出资源”)。
从 ConfigMaps 设置环境变量
现在我们已经在 ConfigMap 对象中拥有了所需的配置数据,那么如何将这些数据传输到容器中呢?让我们看一个使用我们演示应用程序的完整示例。您将在演示存储库的hello-config-env目录中找到代码。
这是我们在之前章节中使用的相同演示应用程序,它监听 HTTP 请求并以问候语回复(参见“查看源代码”)。
不过,这次我们不会将字符串Hello硬编码到应用程序中,而是希望将问候语设置为可配置。因此,稍微修改了handler函数以从环境变量GREETING中读取该值:
func handler(w http.ResponseWriter, r *http.Request) {
`greeting` `:=` `os``.``Getenv``(``"GREETING"``)`
fmt.Fprintf(w, "%s, 世界\n", greeting)
}
不用担心 Go 代码的具体细节;这只是一个演示。可以肯定的是,如果在程序运行时存在GREETING环境变量,它将在响应请求时使用该值。无论您使用何种语言编写应用程序,都可以使用它来读取环境变量。
现在,让我们创建 ConfigMap 对象以保存问候值。您将在演示存储库的hello-config-env目录中找到 ConfigMap 清单文件,以及修改后的 Go 应用程序。
看起来像这样:
apiVersion: v1
kind: ConfigMap
metadata:
name: demo-config
data:
greeting: Hola
为了使这些数据在容器的环境中可见,我们需要稍微修改部署。以下是演示部署的相关部分:
spec:
containers:
- name: demo
`image``:` `cloudnatived/demo:hello-config-env`
ports:
- containerPort: 8888
`env``:`
`-` `name``:` `GREETING`
`valueFrom``:`
`configMapKeyRef``:`
`name``:` `demo-config`
`key``:` `greeting`
注意我们使用了不同的容器镜像标签,与之前的示例不同(参见“镜像标识符”)。:hello-config-env标签让我们获取修改后的演示应用程序版本,该版本读取GREETING变量:cloudnatived/demo:hello-config-env。
其次感兴趣的是env部分。从“环境变量”中记得,您可以通过添加name/value对来创建具有字面值的环境变量。
这里仍然有name,但是我们使用了valueFrom而不是value。这告诉 Kubernetes,它不应该使用变量的字面值,而是应该去其他地方找到该值。
configMapKeyRef告诉它引用特定 ConfigMap 中的特定键。要查看的 ConfigMap 的名称是demo-config,我们要查找的键是greeting。我们使用 ConfigMap 清单创建了这些数据,所以现在应该可以将其读取到容器的环境中。
如果 ConfigMap 不存在,部署将无法运行(其 Pod 将显示CreateContainerConfigError状态)。
这就是使更新后的应用程序工作所需的一切,因此继续将清单部署到您的 Kubernetes 集群。从演示存储库目录中运行以下命令:
`kubectl apply -f hello-config-env/k8s/`
configmap/demo-config created deployment.apps/demo created
与之前一样,要在 Web 浏览器中查看应用程序,您需要将本地端口转发到 Pod 的端口 8888:
`kubectl port-forward deploy/demo 9999:8888`
Forwarding from 127.0.0.1:9999 -> 8888 Forwarding from [::1]:9999 -> 8888
(这次我们没有费心创建一个 Service;尽管在实际的生产应用程序中您会使用 Service,但在这个示例中,我们只是直接使用 kubectl 将本地端口转发到 demo Deployment。)
如果您将您的网络浏览器指向 http://localhost:9999/,如果一切正常,您应该能看到:
Hola, 世界
从 ConfigMap 设置整个环境
虽然您可以从单个 ConfigMap 键设置一两个环境变量,就像我们在前面的示例中看到的那样,但对于大量变量来说,这可能会变得乏味。
幸运的是,有一种简单的方法可以从 ConfigMap 中获取所有键,并将它们转换为环境变量,使用 envFrom:
spec:
containers:
- name: demo
image: cloudnatived/demo:hello-config-env
ports:
- containerPort: 8888
`envFrom``:`
`-` `configMapRef``:`
`name``:` `demo-config`
现在,demo-config ConfigMap 中的每个设置都将成为容器环境中的变量。因为在我们的示例 ConfigMap 中键称为 greeting,所以环境变量也将被命名为 greeting(小写)。如果您在使用 envFrom 时希望使环境变量名大写,请在 ConfigMap 中进行更改。
您也可以像在我们之前的示例中那样,在清单文件中直接放置文字值或使用 ConfigMapKeyRef,正常方式设置容器的其他环境变量。Kubernetes 允许您同时使用 env、envFrom 或两者来设置环境变量。
如果在 env 中设置的变量与在 envFrom 中引用的 ConfigMap 中设置的变量名称相同,则将优先使用 env 中指定的值。例如,如果您在 env 和 envFrom 中引用的 ConfigMap 中同时设置了变量 GREETING,则 env 中指定的值将覆盖 ConfigMap 中的值。
在命令参数中使用环境变量
虽然将配置数据放入容器环境非常有用,但有时您需要将其作为容器入口点的命令行参数来提供。
您可以通过从 ConfigMap 中获取环境变量,如前面的示例那样,但使用特殊的 Kubernetes 语法 $(VARIABLE) 在命令行参数中引用它们。
在演示库的 hello-config-args 目录中,您可以在 deployment.yaml 文件中找到这个示例:
spec:
containers:
- name: demo
image: cloudnatived/demo:hello-config-args
`args``:`
`-` `"``-greeting``"`
`-` `"``$(GREETING)``"`
ports:
- containerPort: 8888
env:
- name: GREETING
valueFrom:
configMapKeyRef:
name: demo-config
key: greeting
在这里,我们为容器规范添加了一个 args 字段,这将把我们的自定义参数传递给容器的默认入口点(/bin/demo)。
Kubernetes 会将清单中形式为 $(VARIABLE) 的任何内容替换为环境变量 VARIABLE 的值。由于我们已创建了 GREETING 变量并从 ConfigMap 设置了其值,因此可以在容器的命令行中使用它。
当您应用这些清单时,GREETING 的值将以这种方式传递给演示应用程序:
`kubectl apply -f hello-config-args/k8s/`
configmap/demo-config created deployment.apps/demo created
您应该在您的网络浏览器中看到效果:
Salut, 世界
从 ConfigMap 创建配置文件
我们已经看到了几种不同的方法将 Kubernetes ConfigMap 中的数据传递到应用程序中:通过环境和通过容器命令行。然而,更复杂的应用程序通常希望从磁盘上的文件中读取它们的配置。
幸运的是,Kubernetes 提供了一种直接从 ConfigMap 创建这些文件的方法。首先,让我们更改我们的 ConfigMap,使其不再是单个键,而是存储一个完整的 YAML 文件(这个文件恰好只包含一个键,但如果您愿意,它可以是一百个):
apiVersion: v1
kind: ConfigMap
metadata:
name: demo-config
data:
config: |
greeting: Buongiorno
与上一个示例中设置 greeting 键不同,我们正在创建一个名为 config 的新键,并将其分配给一个数据 块(YAML 中的竖线符号 | 表示接下来是一个原始数据块)。这就是数据的内容:
greeting: Buongiorno
它碰巧是有效的 YAML,但不要因此而困惑;它可以是 JSON、TOML、纯文本或任何其他格式。无论是什么,Kubernetes 最终都会将整个数据块按原样写入容器中的文件。
现在我们已经存储了必要的数据,让我们将其部署到 Kubernetes。在 demo 仓库的 hello-config-file 目录中,您会找到包含 Deployment 模板的内容:
spec:
containers:
- name: demo
image: cloudnatived/demo:hello-config-file
ports:
- containerPort: 8888
`volumeMounts``:`
`-` `mountPath``:` `/config/`
`name``:` `demo-config-volume`
`readOnly``:` `true`
`volumes``:`
`-` `name``:` `demo-config-volume`
`configMap``:`
`name``:` `demo-config`
`items``:`
`-` `key``:` `config`
`path``:` `demo.yaml`
查看 volumes 部分,您可以看到我们从现有的 demo-config ConfigMap 创建了一个名为 demo-config-volume 的 Volume。
在容器的 volumeMounts 部分,我们将此 Volume 挂载在 mountPath: /config/ 上,选择 config 键,并将其写入路径 demo.yaml。其结果将是,Kubernetes 将在容器中创建一个文件 /config/demo.yaml,其中包含 YAML 格式中的 demo-config 数据:
greeting: Buongiorno
演示应用程序将在启动时从此文件读取其配置。像以前一样,使用以下命令应用清单:
`kubectl apply -f hello-config-file/k8s/`
configmap/demo-config created deployment.apps/demo created
您应该在您的 Web 浏览器中看到结果:
Buongiorno, 世界
如果您想查看集群中 ConfigMap 数据的内容,请运行以下命令:
`kubectl describe configmap/demo-config`
Name: demo-config Namespace: default Labels: <none>
Data ==== config: greeting: Buongiorno
Events: <none>
如果您更新 ConfigMap 并更改其值,则相应的文件(在我们的示例中为 /config/demo.yaml)将自动更新。某些应用程序可能会自动检测到其配置文件已更改并重新读取它;其他则可能不会。
一种选择是重新部署应用程序以获取更改(参见“更新 Pods 的配置更改”),但如果应用程序有触发实时重新加载的方式,例如 Unix 信号(例如 SIGHUP)或在容器中运行命令,这可能并不必要。
更新 Pods 的配置更改
假设您在集群中运行一个 Deployment,并且您想更改其 ConfigMap 中的一些值。如果您正在使用 Helm chart(参见“Helm:Kubernetes 包管理器”),有一个巧妙的技巧可以使其自动检测配置更改并重新加载您的 Pods。在您的 Deployment 规范中添加此注释:
`checksum/config``:` `{``{` `include` `(print` `$.Template.BasePath` `"/configmap.yaml")` `.`
`|` `sha256sum` `}``}`
因为 Deployment 模板现在包含配置设置的哈希值,如果这些设置更改,哈希也会更改。当您运行 helm upgrade 时,Helm 将检测到 Deployment 规范已更改,并重新启动所有 Pods。
Kubernetes Secrets
我们已经看到 Kubernetes ConfigMap 对象提供了一种灵活的方式来存储和访问集群中的配置数据。然而,大多数应用程序都有一些涉及密码或 API 密钥等机密和敏感的配置数据。虽然我们可以使用 ConfigMaps 来存储这些数据,但这并不是一个理想的解决方案。
相反,Kubernetes 提供了一种特殊的对象类型来存储秘密数据:Secret。让我们看一个如何在演示应用程序中使用它的示例。
首先,这是 Secret 的 Kubernetes 清单(参见 hello-secret-env/k8s/secret.yaml):
apiVersion: v1
kind: Secret
metadata:
name: demo-secret
stringData:
magicWord: xyzzy
在本例中,秘密键是 magicWord,秘密值是单词 xyzzy(在计算中非常有用)。与 ConfigMap 一样,你可以在 Secret 中放入多个键和值。这里为了简单起见,我们只使用一个键值对。
使用 Secrets 作为环境变量
就像 ConfigMaps 一样,Secrets 可以通过将它们放入环境变量或者将它们作为文件挂载到容器的文件系统中,从而对容器可见。在这个例子中,我们将设置一个环境变量,其值为 Secret 的值:
spec:
containers:
- name: demo
image: cloudnatived/demo:hello-secret-env
ports:
- containerPort: 8888
env:
- name: MAGIC_WORD
valueFrom:
`secretKeyRef``:`
`name``:` `demo-secret`
`key``:` `magicWord`
我们设置了环境变量 MAGIC_WORD,方式与使用 ConfigMap 时完全相同,只是现在是一个 secretKeyRef 而不是 configMapKeyRef(参见 “从 ConfigMaps 设置环境变量”)。
在演示库目录中运行以下命令来应用这些清单:
`kubectl apply -f hello-secret-env/k8s/`
deployment.apps/demo created secret/demo-secret created
与之前一样,将本地端口转发到 Deployment,以便在 Web 浏览器中查看结果:
`kubectl port-forward deploy/demo 9999:8888`
Forwarding from 127.0.0.1:9999 -> 8888 Forwarding from [::1]:9999 -> 8888
浏览到 http://localhost:9999/,你应该能看到:
魔法词是 "xyzzy"
写入 Secrets 到文件
在本例中,我们将把 Secret 作为文件挂载到容器中。你可以在演示库的 hello-secret-file 文件夹中找到这个示例的代码。
为了在容器中将 Secret 挂载为文件,我们使用了如下的 Deployment:
spec:
containers:
- name: demo
image: cloudnatived/demo:hello-secret-file
ports:
- containerPort: 8888
`volumeMounts``:`
`-` `name``:` `demo-secret-volume`
`mountPath``:` `"``/secrets/``"`
`readOnly``:` `true`
`volumes``:`
`-` `name``:` `demo-secret-volume`
`secret``:`
`secretName``:` `demo-secret`
就像我们在 “从 ConfigMaps 创建配置文件” 中所做的一样,我们创建了一个 Volume(本例中为 demo-secret-volume),并在 spec 的 volumeMounts 部分将其挂载到容器上。mountPath 是 /secrets,Kubernetes 将在该目录下为 Secret 中定义的每个键值对创建一个文件。
在示例 Secret 中,我们只定义了一个名为 magicWord 的键值对,因此此清单将在容器中创建只读文件 /secrets/magicWord,文件的内容将是秘密数据。
如果你像前面的示例一样应用这个清单,你应该能看到相同的结果:
魔法词是 "xyzzy"
读取 Secrets
在前面的部分中,我们能够使用 kubectl describe 查看 ConfigMap 中的数据。我们能否对 Secret 也做同样的操作呢?
`kubectl describe secret/demo-secret`
Name: demo-secret Namespace: default Labels: <none> Annotations: Type: Opaque
Data ==== magicWord: 5 bytes
请注意,这次不显示实际数据。Kubernetes 的 Secrets 是 Opaque 类型,这意味着它们不会显示在 kubectl describe 的输出中,也不会出现在日志消息或终端中。这可以防止秘密数据意外暴露。
您可以通过使用 kubectl get 命令以 YAML 输出格式查看秘密数据的模糊版本:
`kubectl get secret/demo-secret -o yaml`
apiVersion: v1 data:
`magicWord: eHl6enk=`
kind: Secret metadata: ... type: Opaque
base64
那个 eHl6enk= 是什么?它看起来与我们原始的秘密数据不太相似。事实上,这是 Secret 的 base64 表示。Base64 是一种将任意二进制数据编码为字符字符串的方案。
因为秘密数据可能是不可打印的二进制数据(例如,传输层安全 [TLS] 加密密钥),所以 Kubernetes Secrets 总是以 base64 格式存储。
文本 eHl6enk= 是我们秘密词 xyzzy 的 base64 编码版本。您可以在终端使用 base64 --decode 命令验证这一点:
`echo "eHl6enk=" | base64 --decode`
xyzzy
尽管 Kubernetes 可以防止您意外地将秘密数据打印到终端或日志文件中,但如果您有权限读取特定命名空间中的 Secrets,则可以获取以 base64 格式编码的数据,然后解码它。
如果您需要对一些文本进行 base64 编码(例如,将其添加到 Secret 中),请使用带有 -n 标志的 base64 工具以避免包含换行符:
`echo -n xyzzy | base64`
eHl6enk=
访问 Secrets
谁可以读取或编辑 Secrets?这由 Kubernetes 访问控制机制 RBAC 控制,我们将在 “介绍基于角色的访问控制(RBAC)” 中详细讨论。
静态加密
那么,如果有人能够访问存储所有 Kubernetes 信息的 etcd 数据库呢?即使没有 API 权限读取 Secret 对象,他们也能访问秘密数据吗?
从 Kubernetes 版本 1.7 开始,支持 静态加密。这意味着存储在 etcd 数据库中的秘密数据实际上以加密形式存储在磁盘上,即使可以直接访问数据库的人也无法阅读。只有 Kubernetes API 服务器有解密此数据的密钥。在正确配置的集群中,应启用静态加密。
保持 Secrets 和 ConfigMaps
从版本 1.21 开始,Kubernetes 支持 不可变的 Secrets 和 不可变的 ConfigMaps。在 Secret 或 ConfigMap 的清单中添加 immutable: true 将阻止其被修改。更改不可变的 Secret 或 ConfigMap 的唯一方法是删除并重新创建一个新的。
有时您会有一些 Kubernetes 资源,您永远不希望它们从集群中删除。如果您使用 Helm,则可以使用 Helm 特定的注释来防止资源被删除:
kind: Secret
metadata:
annotations:
"helm.sh/resource-policy": keep
保密管理策略
在上一节的示例中,一旦我们将秘密数据存储在集群中,就能保护它免受未授权访问。但秘密数据在我们的清单文件中以明文形式表示。
您永远不应该在提交到源代码控制的文件中公开此类机密数据。那么,在应用于 Kubernetes 集群之前,如何安全地管理和存储机密数据呢?
无论您选择何种工具或策略来管理应用程序中的机密信息,您都需要至少回答以下问题:
-
您应该将机密信息存储在何处,以确保其高可用性?
-
如何使机密信息对运行中的应用程序可用?
-
当您更换或更改机密信息时,运行中的应用程序需要执行哪些操作?
在本节中,我们将介绍一些流行的机密管理策略,并分析它们如何处理这些问题。
在版本控制中加密机密信息
处理机密信息的第一种,也许是最简单的选项是将机密信息直接存储在版本控制存储库中,与源代码一起,但是以加密形式存储。存储在源代码存储库中的机密信息绝不能以明文保存。相反,它们以一种只能在部署时或启动时使用某个受信任密钥解密的形式加密。然后,应用程序可以像处理任何其他配置数据一样读取和使用解密后的机密信息。
在版本控制中加密机密信息,您可以像处理应用程序代码更改一样审查和跟踪机密信息的更改。只要您的版本控制存储库具有高可用性,您的机密信息也将具有高可用性。
要更改或轮换机密信息,只需在源代码的本地副本中解密它们,更新它们,重新加密并将更改提交到版本控制。
尽管这种策略实施简单,除了密钥和加密/解密工具外没有依赖性,但也存在一些潜在的缺点。如果多个应用程序使用相同的机密信息,则所有应用程序都需要其源代码中的副本。这意味着更换机密信息会更费功夫,因为您必须确保找到并更改了所有实例。
在意外提交明文机密信息到版本控制也存在严重风险。错误是难免的,即使是私有版本控制存储库,任何这样提交的机密信息都应视为已泄露,应尽快进行更换。在源代码控制中处理带有加密机密信息的合并冲突也可能会有些棘手。
尽管如此,对于小团队或非关键机密信息而言,该策略可能是一个很好的起点。它相对不需要太多干预和易于设置,同时仍足够灵活,可以处理多个应用程序和不同类型的机密数据。在本章的最后一节中,我们将概述一些您可以使用的加密/解密工具选项,但首先,让我们简要描述其他机密管理策略。
使用专用机密管理工具
虽然在源代码中加密机密信息是一个相对简单的入门方法,但您可能希望评估使用专用的机密管理工具,例如 HashiCorp 的 Vault 或 Square 的 Keywhiz。您还可以考虑使用托管的云服务,例如 AWS Secrets Manager,Azure 的 Key Vault,或者 Google 的 Secret Manager。这些工具以高可用的方式安全地存储所有应用程序机密,并且还可以控制哪些用户和服务账户具有添加、删除、更改或查看机密的权限。
在机密管理系统中,所有操作都经过审计和可审查,这样更容易分析安全漏洞并证明合规性。一些工具还提供定期自动轮换机密的能力,这不仅在任何情况下都是个好主意,而且在许多企业安全策略中也是必需的。开发人员可以拥有自己的个人凭据,只有对他们负责的应用程序的读取或写入机密的权限。
应用程序如何从机密管理工具获取数据?一种常见的方法是使用一个具有对机密保险库只读访问权限的服务账户,以便每个应用程序只能读取其所需的机密。通常会使用一个初始化容器(参见“初始化容器”)首先拉取并解密机密,然后通过卷挂载到 Pod 中。
虽然集中式机密管理系统是目前最强大和灵活的选择,但它也会给您的基础架构增加相当多的复杂性,特别是如果您决定自行托管工具。使用托管解决方案将使您摆脱运行此基础架构的负担,但会增加您的云账单成本。您还需要为应用程序实施一些中间件或流程来安全地使用机密。虽然应用程序可以直接访问特定的机密保险库,但与直接在其前面添加一个获取机密并在应用程序启动时将其放入环境或配置文件中的层相比,这可能更昂贵和耗时。
使用 Sops 加密机密信息
现在让我们来看看一个流行的加密工具,您可以使用它在源代码中安全存储您的机密信息。Sops(secrets operations 的缩写),来自 Mozilla 项目,是一个与 YAML、JSON 或二进制文件兼容的加密/解密工具,支持多种加密后端,包括 age,Azure Key Vault,AWS 的密钥管理服务 (KMS),以及 Google 的 Cloud KMS。访问 Sops 项目主页 获取安装和使用说明。
Sops 不是加密整个文件,而是仅加密键-值对中的个别密钥值。例如,如果你的明文文件包含:
password: foo
当你用 Sops 加密时,生成的文件将如下所示:
password: `ENC[AES256_GCM,data:p673w==,iv:YY=,aad:UQ=,tag:A=]`
这样可以轻松地查看代码,而无需解密值就能理解使用的哪个密钥。
使用 Sops 加密文件
让我们试试 Sops 来加密一个文件。正如我们提到的,Sops 实际上不处理加密本身;它将其委托给一个不同的后端工具。在本例中,我们将使用一个名为age的工具与 Sops 一起加密一个包含秘密的文件。最终结果将是一个可以安全提交到版本控制的文件。
我们不会详细讨论age加密的工作原理,但请知道,它像 SSH 和 TLS 一样,是一种公钥加密系统。它不是用单一密钥加密数据,而是使用一对密钥:一个公钥,一个私钥。你可以安全地与他人分享你的公钥,但绝不能泄露你的私钥。
现在让我们生成你的密钥对。首先,安装 age,如果你还没有安装。
一旦安装完成,请运行以下命令生成一个新的密钥对:
`age-keygen -o key.txt`
Public key: age1fea...
一旦你的密钥成功生成,请记下Public key。它将是唯一的,并且标识刚刚创建的密钥。key.txt文件还包含你的私钥,因此应该安全地存储,永远不要提交到源代码控制中。
现在让我们使用 Sops 和age来加密一个文件。如果你还没有在你的机器上安装 Sops,请先安装它。
首先让我们创建一个测试秘密的 YAML 文件来加密:
`echo "password: secret123" > test.yaml`
`cat test.yaml`
password: secret123
现在,使用 Sops 进行加密。将你的密钥指纹传递给--age开关和上面的Public key,就像这样:
`sops --age age1fea... --encrypt --in-place test.yaml`
`cat test.yaml`
password: ENC[AES256_GCM,data:6U6tZpn/TCTG,iv:yfO6... ... sops:
... age: - recipient: age1fea...
成功了!test.yaml文件已经安全加密,password的值被加密并且只能用你的私钥解密。你还会注意到,Sops 在文件底部添加了一些元数据,以便将来如何解密它。
Sops 的一个很好的特性是,因为只有password的值被加密,文件的 YAML 格式保持不变,你仍然可以查看密钥的名称。
为了确保我们能够恢复加密数据,并检查它是否与我们输入的匹配,请运行:
`SOPS_AGE_KEY_FILE=$(pwd)/key.txt sops --decrypt test.yaml`
password: secret123
命令中的SOPS_AGE_KEY_FILE部分指向你最初与age生成的密钥文件的位置。你可以考虑将该文件存储在 Sops 期望的默认位置,即你的$HOME/sops/目录。
在部署应用程序时,可以使用 Sops 解密模式来生成应用程序使用的明文密钥——但记住在之后删除明文文件,并且永远不要提交它们到版本控制!
当作为集中式 CI/CD 流水线的一部分使用 Sops 时,您的部署服务器基础架构还需要一个age密钥,并且必须是一个受信任的接收者,以便解密文件。
现在您知道如何使用 Sops,在您的源代码中可以加密任何敏感数据,无论是应用程序配置文件、Kubernetes YAML 资源还是其他任何内容。接下来,我们将向您展示如何在 Helm chart 中以这种方式使用 Sops。您不仅可以在使用 Helm 部署应用程序时解密秘密,还可以根据部署环境使用不同的秘密集:例如,staging与production(参见“使用 Sops 管理 Helm Chart 中的秘密”)。
还值得一提的是,如果您需要在 Helm chart 中管理加密的秘密,可以使用名为helm-secrets的插件来完成。当您运行helm upgrade...或helm install...时,helm-secrets将解密部署所需的秘密。有关helm-secrets的更多信息,包括安装和使用说明,请参阅GitHub 仓库。
使用 KMS 后端
如果您在云中使用 Amazon KMS 或 Google Cloud KMS 进行密钥管理,也可以将它们与 Sops 一起使用。在我们的age示例中,使用 KMS 密钥的方式完全相同,但文件中的元数据将不同。文件底部的sops:部分可能看起来像这样:
sops:
kms:
- created_at: 1441570389.775376
enc: CiC....Pm1Hm
arn: arn:aws:kms:us-east-1:656532957310:key/920aff2e...
就像使用age一样,文件中嵌入了密钥 ID(arn:aws:kms...),以便 Sops 知道如何稍后解密它。
Sealed Secrets
另一个在源代码控制中存储加密密钥的好选择是 Bitnami 团队维护的开源工具Sealed Secrets。与 Sops 不同的是,在这种情况下,加密密钥实际上是在您的 Kubernetes 集群内生成、安装和存储的,使部署和解密过程变得简单直接。
安装了 Sealed Secrets 后,您可以使用kubeseal客户端工具来加密 Kubernetes Secret。这将生成一个新的SealedSecret,然后可以安全地提交到您的源代码仓库中,就像使用 Sops 加密的 YAML 文件一样。在应用到集群时,Sealed Secret 工具将从 Kubernetes 内部解密SealedSecret对象,并安全传递给您的应用程序 Pod。
摘要
有关 Kubernetes 相关的配置和密钥是人们最常询问我们的主题之一。我们很高兴能够为此奉献一章,并概述一些将您的应用程序连接到所需设置和数据的方法。
我们学到的最重要的事情:
-
将配置数据与应用程序代码分离,并使用 Kubernetes ConfigMaps 和 Secrets 进行部署。这样,每次更改密码时,您无需重新部署应用程序。
-
你可以通过直接在你的 Kubernetes 清单文件中编写数据,或使用
kubectl将现有的 YAML 文件转换为 ConfigMap 规范,将数据输入到 ConfigMaps 中。 -
一旦数据在 ConfigMap 中,你可以将它插入到容器的环境中,或者插入到其入口点的命令行参数中。或者,你可以将数据写入挂载在容器上的文件中。
-
秘密的工作方式与 ConfigMaps 类似,不同之处在于数据在静态时加密,并在
kubectl输出中混淆。 -
管理秘密的一个简单方法是将它们直接存储在源代码仓库中,但使用 Sops 或其他基于文本的加密工具对其进行加密。
-
不要忽视秘密管理,特别是在初始阶段。从团队理解的东西开始,提供一个安全的管理团队秘密的流程。
-
像 Vault 这样的专用秘密管理工具,或者托管的云 KMS 工具,增加了堆栈的成本和复杂性,但提供了更好的审计和灵活性,用于安全地保护你的秘密。
-
Sops 是一种加密工具,适用于像 YAML 和 JSON 这样的键值文件。它可以从本地密钥环或云密钥管理服务(如 Amazon KMS 和 Google Cloud KMS)获取其加密密钥。
-
Sealed Secrets 使得在源代码控制中存储加密的秘密并从 Kubernetes 集群内安全传递它们到你的应用程序变得容易。
第十一章:安全性、备份和集群健康
如果您认为技术可以解决您的安全问题,那么您不了解这些问题,也不了解技术。
Bruce Schneier,《应用密码学》
在本章中,我们将探讨 Kubernetes 中的安全性和访问控制机制,包括基于角色的访问控制(RBAC),概述一些漏洞扫描工具和服务,并解释如何备份您的 Kubernetes 数据和状态(甚至更重要的是如何恢复)。我们还将介绍一些有用的获取集群信息的方法。
访问控制和权限
小型科技公司往往从只有少数几名员工开始,每个人都对每个系统有管理员访问权限。
随着组织的增长,最终会变得明显,不是每个人都拥有管理员权限都不是一个好主意:某人很容易犯错并改变不该改变的东西。同样适用于 Kubernetes。
通过集群管理访问
保护您的 Kubernetes 集群的最简单和最有效的方法之一是限制谁可以访问它。通常有两组人需要访问 Kubernetes 集群:集群操作员 和 应用开发者,他们通常在工作职能中需要不同的权限和特权。
此外,您可能会有多个部署环境,如生产环境和暂存环境。这些独立的环境将根据您的组织需求需要不同的策略。生产环境可能仅限于某些个人,而暂存环境可能对更广泛的工程师开放。
正如我们在“我是否需要多个集群?”中看到的,通常为生产和暂存或测试单独设置集群是个好主意。如果某人在暂存环境中意外部署了导致集群节点崩溃的东西,它不会影响生产环境。
如果一个团队不应该访问另一个团队的软件和部署流程,那么每个团队可以拥有自己专用的集群,甚至不需要在其他团队的集群上拥有凭证。
这无疑是最安全的方法,但额外的集群伴随着一些权衡。每个集群都需要打补丁和监控,而许多小集群的效率往往不如大集群高。
引入基于角色的访问控制(RBAC)
您还可以通过控制谁可以在集群内执行特定操作来管理访问,使用 Kubernetes 的基于角色的访问控制(RBAC)系统。
RBAC 的设计目的是为特定用户(或与自动化系统关联的服务账户)授予特定权限。例如,如果需要,您可以授予特定用户在集群中列出所有 Pod 的能力。
关于 RBAC 的第一点和最重要的事情是,它应该被启用。RBAC 在 Kubernetes 1.6 中作为设置集群的选项引入。但是,您的集群是否实际启用了此选项取决于您的云提供商或 Kubernetes 安装程序。
如果您正在运行自托管集群,请尝试使用以下命令查看集群是否已启用 RBAC:
`kubectl describe pod -n kube-system -l component=kube-apiserver`
Name: kube-apiserver-docker-for-desktop Namespace: kube-system ... Containers:
kube-apiserver: ... Command: kube-apiserver ... --authorization-mode=Node,RBAC
如果--authorization-mode不包含RBAC,则您的集群未启用 RBAC。请查阅安装程序的文档,了解如何重新构建已启用 RBAC 的集群。
没有 RBAC,任何具有访问集群权限的人都有权执行任何操作,包括运行任意代码或删除工作负载。这可能不是您想要的。
理解角色
因此,假设您已启用 RBAC,它是如何工作的?了解最重要的概念是用户、角色和角色绑定。
每次连接到 Kubernetes 集群时,您都会作为特定用户进行。您如何对集群进行身份验证取决于您的提供者;例如,在 GKE 中,您使用gcloud工具获取访问特定集群的访问令牌。在 EKS 中,您使用 AWS IAM 凭据。集群中还有服务账户;例如,每个命名空间都有一个默认的服务账户。用户和服务账户都可以拥有不同的权限集。
这些由 Kubernetes 的角色管理。角色描述了特定的权限集。Kubernetes 包含一些预定义的角色供您使用。例如,cluster-admin角色适用于超级用户,允许访问和更改集群中的任何资源。相比之下,view角色可以列出和检查给定命名空间中的大多数对象,但不能修改它们。
您可以在命名空间级别(使用Role对象)或整个集群范围内(使用 ClusterRole 对象)定义角色。以下是授予任何命名空间中 Secrets 读取权限的 ClusterRole 清单示例:
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: secret-reader
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "watch", "list"]
将角色绑定到用户
如何将用户与角色关联?您可以使用角色绑定来实现。就像角色一样,您可以创建一个适用于特定命名空间的RoleBinding对象,或者适用于集群级别的 ClusterRoleBinding。
以下是 RoleBinding 清单示例,它在demo命名空间中仅为daisy用户分配edit角色:
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: daisy-edit
namespace: demo
subjects:
- kind: User
name: daisy
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: edit
apiGroup: rbac.authorization.k8s.io
在 Kubernetes 中,权限是累加的;用户起初没有权限,您可以使用 Roles 和 RoleBindings 为其添加权限。您不能从已经拥有权限的人那里减去权限。
提示
您可以在 Kubernetes 的文档中详细了解 RBAC 的详情,以及可用的角色和权限。
我需要哪些角色?
那么在您的集群中应该设置哪些角色和绑定?预定义的角色cluster-admin、edit和view可能已经涵盖了大多数需求。要查看特定角色具有哪些权限,请使用kubectl describe命令:
`kubectl` `describe` `clusterrole/edit`
Name: edit
Labels: kubernetes.io/bootstrapping=rbac-defaults
Annotations: rbac.authorization.kubernetes.io/autoupdate=true
PolicyRule:
Resources ... Verbs
--------- ... -----
bindings ... [get list watch]
configmaps ... [create delete deletecollection get list patch update watch]
endpoints ... [create delete deletecollection get list patch update watch]
...
您可以为组织内的特定人员或工作角色(例如开发者角色)或个别团队(例如 QA 或安全团队)创建角色。
保护对 cluster-admin 的访问
谁有权限访问 cluster-admin 角色要非常小心。这是集群超级用户,相当于 Unix 系统上的 root 用户。他们可以对任何东西进行任何操作。永远不要将此角色授予不是集群运营商的用户,尤其是不要将此角色授予可能暴露在互联网上的应用程序服务账号,比如 Kubernetes 仪表盘(参见 “Kubernetes 仪表盘”)。
警告
不要通过不必要地授予 cluster-admin 来解决问题。在诸如 Stack Overflow 等网站上可能会找到一些错误的建议。面对 Kubernetes 权限错误时,一个常见的应对方式是将 cluster-admin 角色授予应用程序。不要这样做。是的,它可以让错误消失,但这是以绕过所有安全检查并可能打开集群给攻击者的风险为代价的。相反,应该授予应用程序最少所需的权限角色。
应用程序和部署
在 Kubernetes 中运行的应用通常不需要任何特殊的 RBAC 权限。除非另有指定,所有的 Pod 都会作为其命名空间中的 default 服务账号运行,该账号没有任何关联角色。
如果您的应用程序因某种原因需要访问 Kubernetes API(例如需要列出 Pod 的监控工具),请为该应用程序创建一个专用的服务账号,使用 RoleBinding 将其关联到必要的角色(例如 view),并限制它只能访问特定的命名空间。
那么,部署应用程序到集群需要哪些权限呢?最安全的方式是只允许连续部署工具来部署应用程序(参见 第十四章)。它可以使用专用的服务账号,在特定命名空间内具有创建和删除 Pod 的权限。
edit 角色非常适合此用途。具有 edit 角色的用户可以在命名空间中创建和销毁资源,但不能创建新的角色或向其他用户授予权限。
如果没有自动化的部署工具,并且开发人员必须直接部署到集群中,他们还需要在适当的命名空间中拥有编辑权限。基于应用程序的基础,逐个应用程序授予这些权限;不要在整个集群范围内授予任何人编辑权限。那些不需要部署应用程序的人默认只能具备 view 角色。
理想情况下,应该设置一个集中化的 CI/CD 部署流程,这样开发人员就不需要直接在 Kubernetes 上进行部署。我们将在 第十四章 和 “GitOps” 中更详细地介绍这一点。
最佳实践
确保 RBAC 在所有集群中启用。只将cluster-admin权限授予实际需要完全控制集群的用户。如果您的应用程序需要访问集群资源,请为其创建服务账户,并将其绑定到只在需要的命名空间中拥有所需权限的角色。
RBAC 故障排除
如果您正在运行一个不支持 RBAC 或者您仍在调试自己应用程序所需权限的旧第三方应用程序,可能会遇到 RBAC 权限错误。这些错误看起来如何?
如果一个应用程序请求执行它没有权限做的 API 请求(比如列出节点),它将会从 API 服务器看到Forbidden错误响应(HTTP 状态码 403):
Error from server (Forbidden): nodes.metrics.k8s.io is forbidden: User
"demo" cannot list nodes.metrics.k8s.io at the cluster scope.
如果应用程序没有记录这些信息,或者您不确定哪个应用程序失败了,可以检查 API 服务器的日志(详见“Viewing a Container’s Logs”了解更多信息)。它将记录包含RBAC DENY字符串及错误描述的消息:
`kubectl logs -n kube-system -l component=kube-apiserver | grep "RBAC DENY"`
RBAC DENY: user "demo" cannot "list" resource "nodes" cluster-wide
(在 GKE 集群或任何不提供对控制平面访问的托管 Kubernetes 服务上,您将无法执行此操作:请参阅您的 Kubernetes 提供商的文档,了解如何访问 API 服务器日志。)
kubectl还包括一个有用的命令来测试权限,称为auth can-i。这允许您使用当前角色尝试 Kubernetes 操作,或者测试其他人的权限以查看他们的角色是否允许特定命令:
[source, console, subs="quotes"]
*kubectl auth can-i list secrets*
yes
*kubectl auth can-i create deployments --as test-user*
no
RBAC 以复杂而闻名,但其实并不是。只需授予用户所需的最低权限,保持cluster-admin安全,一切都会好起来的。
集群安全扫描
为了检查潜在的已知安全问题,有工具可以扫描您的集群,并通知您检测到的任何问题。
Gatekeeper/OPA
2021 年 2 月,CNCF 毕业了 Open Policy Agent (OPA) 项目,这意味着它符合被包括在其他官方采纳的 CNCF 项目旁边的标准和成熟度。OPA 是一个 策略引擎 工具,允许您为您的任何云原生工具(包括 Kubernetes)定义安全策略。
Gatekeeper是一个相关项目,它将 OPA 引擎作为本地资源在 Kubernetes 中运行。
例如,使用 Gatekeeper,您可以添加以下约束来阻止在特定命名空间中运行使用latest标签的容器(详见“The latest Tag”了解更多信息):
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sDisallowedTags
metadata:
name: container-image-must-not-have-latest-tag
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
namespaces:
- "my-namespace"
parameters:
tags: ["latest"]
在 gatekeeper-library repo 中维护了许多其他的针对 Kubernetes 的特定策略。其他示例包括确保所有 Pod 具有已知的一组标签,或强制容器来自可信任的注册表源列表。你还可以考虑使用 OPA 来审计和保护你的非 Kubernetes 云资源。
kube-bench
kube-bench 是一个用于按照由网络安全中心(CIS)制定的一组基准审计你的 Kubernetes 集群的工具。实际上,它验证你的集群是否按照安全最佳实践进行了设置。虽然你可能不需要这样做,但你可以配置 kube-bench 运行的测试,甚至添加自己的测试,以 YAML 文档的形式指定。
与 “使用 Sonobuoy 进行一致性测试” 类似,kube-bench 在你将 Job 部署到 Kubernetes 并检查结果时运行。根据 存储库文档 下载适合你集群的适当的 Job YAML 文件,并将其安装到你的集群中:
`kubectl apply -f job.yaml`
job.batch/kube-bench created
`kubectl logs job/kube-bench`
... == Summary total == 63 checks PASS 13 checks FAIL 46 checks WARN 0 checks INFO ...
然后,你可以阅读日志,了解 kube-bench 发现的任何警告或失败的具体信息。
Kubescape
这个领域的另一个工具称为 Kubescape,它检查你的集群是否根据最近在 CIS 基准 中定义的标准安全。它会告诉你关于以 root 权限运行的容器、任何可能不安全的公开端口、检查你的认证和审计日志设置是否安全配置等内容,这些项目都在 CIS 检查清单中定义。
容器安全扫描
如果你在集群中运行第三方软件,检查其安全问题和恶意软件是明智之举。但即使是你自己的容器也可能包含你不知道的软件,也需要进行检查。
Clair
Clair 是 CoreOS 项目生产的开源容器扫描器。它静态分析容器镜像,即在实际运行之前,看看它们是否包含已知不安全的软件或版本。
你可以手动运行 Clair 来检查特定镜像是否存在问题,或将其集成到你的持续交付流程中,在部署之前测试所有镜像(参见 第十四章)。
另外,Clair 可以连接到你的容器注册表,扫描推送到其中的任何镜像并报告问题。
值得一提的是,你不应该自动信任基础镜像,比如alpine。Clair 预装了许多流行基础镜像的安全检查,如果你使用的镜像存在已知漏洞,它会立即告诉你。
Aqua
Aqua 容器安全平台 是一种全方位的商业容器安全解决方案,允许组织扫描容器以检测漏洞、恶意软件和可疑活动,同时提供策略执行和合规性监管。
顺理成章地,Aqua 的平台与您的容器注册表、CI/CD 管道以及包括 Kubernetes 在内的多个编排系统集成。
Aqua 还提供 Trivy,这是一个可以添加到您的容器镜像中的免费工具,用于扫描安装的软件包,以检测来自 Aqua 安全平台使用的同一数据库中已知的漏洞。
如果您想让 Trivy 扫描特定的 Docker 镜像,请安装 CLI 工具并运行:
`trivy image [YOUR_CONTAINER_IMAGE_NAME]`
您还可以使用它来扫描 Dockerfile、Terraform 文件甚至您的 Kubernetes 清单中的安全问题和错误配置:
`trivy config [YOUR_CODE_DIR]`
另一个方便的来自 Aqua 的开源工具是 kube-hunter,旨在发现 Kubernetes 集群本身的安全问题。如果您将其作为容器在集群外的机器上运行,就像攻击者可能会做的那样,它将检查各种问题:证书中的公开电子邮件地址、不安全的仪表板、开放的端口和端点等。
Anchore Engine
Anchore Engine 是一个开源工具,用于扫描容器镜像,不仅用于已知的漏洞,还可以识别容器中存在的一切内容的物料清单,包括库、配置文件和文件权限。您可以使用此工具根据用户定义的策略验证容器:例如,您可以阻止包含安全凭据或应用程序源代码的任何镜像。
Synk
Docker 与 Synk 合作,将漏洞扫描直接添加到 docker CLI 工具中。您可以使用 docker scan 命令扫描任何镜像:
`docker scan golang:1.17-alpine`
Testing golang:1.17-alpine...
Package manager: apk Project name: docker-image|golang Docker image: golang:1.17-alpine Platform: linux/amd64 Base image: golang:1.17.1-alpine3.14
✓ Tested 15 dependencies for known vulnerabilities, no vulnerable paths found.
Synk 将告诉您在镜像中发现的已知安全问题和弃用问题:
`docker scan golang:1.14-alpine`
... Tested 15 dependencies for known vulnerabilities, found 10 vulnerabilities.
Base Image Vulnerabilities Severity golang:1.14-alpine 10 2 critical, 5 high, 2 medium, 1 low
Recommendations for base image upgrade:
Minor upgrades Base Image Vulnerabilities Severity golang:1.17.0-alpine 0 0 critical, 0 high, 0 medium, 0 low
这可以是将一些基本安全扫描快速轻松地添加到您的 Docker 工作流程以及您的 CI/CD 管道中的一种方法。
最佳实践
不要从不受信任的源运行容器,或者当您不确定其中包含什么内容时。运行像 Clair 或 Synk 这样的扫描工具来检查所有容器,特别是您自己构建的那些容器,确保基础镜像或依赖项中没有已知的漏洞。
备份
您可能想知道在云原生架构中是否仍然需要备份。毕竟,Kubernetes 本身是可靠的,可以处理一次丢失多个节点的情况,而不会丢失状态,甚至不会显著降低应用程序性能。
此外,Kubernetes 是一种声明性的基础架构即代码系统。所有 Kubernetes 资源都由存储在可靠数据库(etcd)中的数据描述。如果某些 Pod 被意外删除,它们的监控 Deployment 将从数据库中保存的规范重新创建它们。
我需要备份 Kubernetes 吗?
那么,您是否仍然需要备份?是的。例如,存储在持久卷上的数据容易出现故障(参见“持久卷”)。虽然您的云供应商可能会提供名义上高可用的卷(例如跨两个不同可用性区域复制数据),但这并非备份。
让我们重申这一点,因为这并不明显:
警告
复制并非备份。尽管复制可以保护您免受底层存储卷的故障影响,但它无法防止您在 Web 控制台误点击而意外删除卷,例如。
复制也不能防止配置错误的应用程序覆盖其数据,或者操作员在运行具有错误环境变量的命令时意外删除生产数据库而不是开发数据库。(这种情况发生过,可能比任何人愿意承认的频繁^(1))
备份 etcd
正如我们在“高可用性”中看到的,Kubernetes 将其所有状态存储在 etcd 数据库中,因此任何此处的故障或数据丢失都可能是灾难性的。这是为什么我们建议您使用保证 etcd 和控制平面可用性的托管服务的一个非常好的理由(参见“如果可以,请使用托管 Kubernetes”)。
如果您自己运行控制平面节点,则需要负责管理 etcd 集群和复制。即使定期进行数据快照,仍然需要一定时间来检索和验证快照,重建集群并恢复数据。在此期间,您的集群可能会不可用或严重降级。
这就是为什么非常重要,您要将您的 Kubernetes 清单和 Helm 图表存储在源代码控制中,并实施高效的部署流程,以便在出现 etcd 问题时能够快速恢复和运行您的集群。我们将在第十四章中进一步介绍此内容。
最佳实践
使用托管或即插即用服务提供商运行带有 etcd 集群和备份的控制平面节点。如果您自己运行它们,请务必确保您知道自己在做什么。弹性的 etcd 管理是一项专业工作,如果做错,后果可能很严重。
备份资源状态
除了 etcd 故障外,还有一个问题是保存您的各个资源的状态。例如,如果您误删了错误的 Deployment,您将如何重新创建它?
在本书中,我们强调了 基础设施即代码 范式的价值,并建议您始终通过应用存储在版本控制中的 YAML 清单或 Helm 图表来声明式管理您的 Kubernetes 资源。
理论上,为了重新创建集群工作负载的完整状态,您应该能够检出相关版本控制存储库,并应用其中的所有资源。理论上。
备份集群状态
实际上,并非您在版本控制中拥有的所有内容都在您的集群中运行。某些应用程序可能已经停止服务,或者被更新版本替换。有些可能尚未准备好部署。
我们在整本书中建议您避免直接更改资源,而是应用来自更新的清单文件的更改(参见 “不要使用命令式命令”)。但人们并不总是遵循好的建议。
在任何情况下,很可能在应用程序的初始部署和测试过程中,工程师可能会动态调整设置,如副本数和节点亲和性,并且只有在确定了正确值后才将它们存储在版本控制中。
假设你的集群完全关闭,或者所有资源都被删除(希望这种情况不太可能,但这是一个有用的思想实验)。你能多快重新创建它?
即使您有一个设计优良且最新的集群自动化系统,可以重新部署所有内容到一个新的集群,您如何 知道 这个集群的状态与丢失的状态匹配?
一种帮助确保这一点的方法是对正在运行的集群进行快照,以便以后在出现问题时进行参考。
大型和小型灾难
很少可能会整个丢失整个集群:成千上万的 Kubernetes 贡献者已经努力确保这种情况不会发生。
更有可能的情况是你(或者你最新的团队成员)可能会意外删除一个命名空间,无意中关闭一个 Deployment,或者在 kubectl delete 命令中错误地指定了一组标签,导致删除的东西比预期的更多。
不管原因是什么,灾难确实会发生,所以让我们看看一个可以帮助您避免它们的备份工具。
Velero
Velero(前身为 Ark)是一个可以备份和恢复集群状态及持久数据的免费开源工具。
Velero 在您的集群中运行,并连接到您选择的云存储服务(例如,Amazon S3 或 Azure Storage)。
前往 velero.io 网站 获取在您的平台上设置 Velero 的说明。
配置 Velero
在使用 Velero 之前,您需要在 Kubernetes 集群中创建一个 BackupStorageLocation 对象,告诉它备份存储位置(例如,AWS S3 云存储桶)。这里有一个配置 Velero 将备份到 demo-backup 存储桶的示例:
apiVersion: velero.io/v1
kind: BackupStorageLocation
metadata:
name: default
namespace: velero
spec:
provider: aws
objectStorage:
bucket: demo-backup
config:
region: us-east-1
您必须至少拥有一个名为 default 的存储位置,尽管您可以添加其他任何您喜欢的名称。
Velero 还可以备份您的持久卷的内容。要告诉它存储它们的位置,您需要创建一个 VolumeSnapshotLocation 对象:
apiVersion: velero.io/v1
kind: VolumeSnapshotLocation
metadata:
name: aws-default
namespace: velero
spec:
provider: aws
config:
region: us-east-1
创建一个 Velero 备份
使用velero backup命令创建备份时,Velero 服务器会查询 Kubernetes API 以检索与您提供的选择器匹配的资源(默认情况下,它备份所有资源)。您可以备份一组命名空间或整个集群:
`velero backup create demo-backup --include-namespaces demo`
然后,它将根据您配置的 BackupStorageLocation 将所有这些资源导出到云存储桶中的一个命名文件中。还将备份到您配置的 VolumeSnapshotLocation 中的持久卷的元数据和内容。
或者,您可以备份集群中除了指定命名空间(例如,kube-system)以外的所有内容。您还可以安排自动备份:例如,您可以让 Velero 每晚或每小时备份您的集群。
每个 Velero 备份都是完整的,而不是增量备份。因此,要恢复备份,您只需要最近的备份文件。
恢复数据
使用velero backup get命令可以列出您的可用备份。要查看特定备份中的内容,请使用velero backup download:
`velero backup download demo-backup`
Backup demo-backup has been successfully downloaded to $PWD/demo-backup-data.tar.gz
下载的文件是一个* tar.gz *归档文件,您可以使用标准工具解压并检查。如果您只想要特定资源的清单,例如,您可以从备份文件中提取它,并使用kubectl apply -f单独恢复它。
要恢复整个备份,velero restore命令将启动该过程,并且 Velero 将重新创建指定快照中描述的所有资源和卷,跳过任何已经存在的内容。
如果资源存在,但与备份中的资源不同,Velero 会警告您,但不会覆盖现有资源。因此,例如,如果您想要将运行中的部署状态重置为最新快照中的状态,请首先删除运行中的部署,然后使用 Velero 恢复。
或者,如果您正在恢复一个命名空间的备份,可以首先删除该命名空间,然后恢复备份。
恢复过程和测试
您应该编写详细的逐步过程描述如何从备份中恢复数据,并确保所有员工都知道在哪里找到此文档。当灾难发生时,通常是在不方便的时间,关键人员不可用,每个人都处于恐慌状态,您的程序应该如此清晰和精确,以至于可以由不熟悉 Velero 甚至 Kubernetes 的人员执行。
每个月,通过让不同团队成员执行恢复过程来运行恢复测试,对一个临时集群。这可以验证您的备份有效,并且恢复过程正确,确保每个人都熟悉如何操作。
安排 Velero 备份
所有备份都应自动化,Velero 也不例外。您可以使用velero schedule create命令安排定期备份:
`velero schedule create demo-schedule --schedule="0 1 * * *" --include-namespaces` `demo`
Schedule "demo-schedule" created successfully.
schedule 参数指定何时运行备份,使用 Unix cron 格式(参见 “CronJobs”)。例如,0 1 * * * 每天 01:00 运行备份。
要查看你计划中的备份,请使用 velero schedule get。
Velero 的其他用途
尽管 Velero 在灾难恢复方面非常有用,但你也可以使用它来迁移资源和数据从一个集群到另一个。
定期进行 Velero 备份还可以帮助你了解你的 Kubernetes 使用情况随时间的变化,比如将当前状态与一个月前、六个月前和一年前的状态进行比较。
快照也可以是审计信息的有用来源:例如,在给定日期或时间运行的集群中有什么在运行,以及集群状态如何以及何时发生了变化。
最佳实践
定期使用 Velero 备份你的集群状态和持久数据:至少每天一次。至少每月运行一次恢复测试。
监控集群状态
监控云原生应用是一个重要的主题,正如我们将在 第十五章 中看到的那样,包括可观察性、指标、日志、跟踪和传统的封闭箱监控。
然而,在本章中,我们只关心监控 Kubernetes 集群本身:集群的健康状况、单个节点的状态以及集群的利用率和其工作负载的进度。
kubectl
我们在 第二章 中介绍了无价的 kubectl 命令,但我们还没有探索完它的所有可能性。除了作为 Kubernetes 资源的通用管理工具外,kubectl 还可以报告有关集群组件状态的有用信息。
控制平面状态
kubectl get componentstatuses 命令(或简称 kubectl get cs)提供了控制平面组件的健康信息——调度程序、控制器管理器和 etcd:
`kubectl get componentstatuses`
NAME STATUS MESSAGE ERROR controller-manager Healthy ok scheduler Healthy ok etcd-0 Healthy {"health": "true"}
如果任何控制平面组件出现严重问题,这很快就会显现出来,但仍然有必要能够检查和报告它们,作为集群的一种顶级健康指标。
如果你的任何控制平面组件状态不为 Healthy,则需要修复。对于托管 Kubernetes 服务,这种情况不应该发生,但对于自托管集群,你将需要自行处理。
节点状态
另一个有用的命令是 kubectl get nodes,它将列出集群中的所有节点,并报告它们的状态和 Kubernetes 版本:
`kubectl get nodes`
NAME STATUS ROLES AGE VERSION docker-desktop Ready control-plane,master 24d v1.21.4
由于 Docker Desktop 集群只有一个节点,所以这个输出并不特别具有信息量;让我们看一下来自一个小型 GKE 集群的输出,以获得更现实的数据:
`kubectl get nodes`
NAME STATUS ROLES AGE VERSION gke-k8s-cluster-1-n1-standard-2-pool--8l6n Ready <none> 9d v1.20.2-gke.1 gke-k8s-cluster-1-n1-standard-2-pool--dwtv Ready <none> 19d v1.20.2-gke.1 gke-k8s-cluster-1-n1-standard-2-pool--67ch Ready <none> 20d v1.20.2-gke.1 ...
请注意,在 Docker Desktop 的 get nodes 输出中,节点的 角色 显示为 control-plane。理所当然的是,因为只有一个节点,那必定是控制平面和唯一的工作节点。
在托管 Kubernetes 服务中,通常无法直接访问控制平面节点。因此,kubectl get nodes 仅列出工作节点。
如果任何节点显示 NotReady 状态,则存在问题。重新启动节点可能会修复问题,但如果无效,则可能需要进一步调试——或者您可以删除它,然后创建一个新节点。
若要详细排查不良节点问题,您可以使用 kubectl describe node 命令获取更多信息:
`kubectl describe nodes/gke-k8s-cluster-1-n1-standard-2-pool--8l6n`
这将显示节点的内存和 CPU 容量,以及当前由 Pod 使用的资源。
工作负载
您可能还记得来自 “使用 kubectl 查询集群” 的内容,您也可以使用 kubectl 列出集群中所有 Pod(或任何资源)。在该示例中,您仅列出了默认命名空间中的 Pod,但 --all-namespaces 标志(或简写为 -A)将允许您查看整个集群中的所有 Pod:
`kubectl get pods --all-namespaces`
NAMESPACE NAME READY STATUS RESTARTS AGE cert-manager cert-manager-cert-manager-55 1/1 Running 1 10d pa-test permissions-auditor-15281892 0/1 CrashLoopBackOff 1720 6d metrics-api metrics-api-779758f445 3/3 Running 5 20d ...
这可以为您提供集群中正在运行的内容以及任何 Pod 级别的问题的概述。例如,在示例中,如果像 permissions-auditor Pod 这样的 Pod 不处于 Running 状态,则可能需要进一步调查。
READY 列显示 Pod 中实际运行的容器数与配置数的比较。例如,metrics-api Pod 显示 3/3:3 个容器中的 3 个正在运行,因此一切正常。
另一方面,permissions-auditor 显示 0/1 容器就绪:0 个容器正在运行,但需要 1 个。状态在 STATUS 列中显示原因为 CrashLoopBackOff,容器无法正常启动。
当容器崩溃时,Kubernetes 将尝试以递增的时间间隔重新启动它,从 10 秒开始,并每次加倍,最多达到 5 分钟。这种策略称为指数回退,因此会显示 CrashLoopBackOff 状态消息。
CPU 和内存利用率
您可以通过 kubectl top 命令查看集群的另一个有用视图。对于节点,它将显示每个节点的 CPU 和内存容量,以及当前使用量:
`kubectl top nodes`
NAME CPU(cores) CPU% MEMORY(bytes) MEMORY% gke-k8s-cluster-1-n1-...8l6n 151m 7% 2783Mi 49% gke-k8s-cluster-1-n1-...dwtv 155m 8% 3449Mi 61% gke-k8s-cluster-1-n1-...67ch 580m 30% 3172Mi 56% ...
对于 Pod,它将显示每个指定 Pod 正在使用的 CPU 和内存:
`kubectl top pods -n kube-system`
NAME CPU(cores) MEMORY(bytes) event-exporter-v0.1.9-85bb4fd64d-2zjng 0m 27Mi fluentd-gcp-scaler-7c5db745fc-h7ntr 10m 27Mi fluentd-gcp-v3.0.0-5m627 11m 171Mi ...
用于广泛查看集群和 Pod 健康状况的另一个有用工具是使用 “Lens”。
云提供商控制台
如果您正在使用云提供商提供的托管 Kubernetes 服务,那么您将可以访问一个基于 web 的控制台,该控制台可以显示有关您的集群、节点和工作负载的有用信息。
您还可以列出节点、服务和集群的配置详细信息。这与使用 kubectl 工具获取的信息基本相同,但云控制台还允许您执行管理任务:创建集群、升级节点以及日常管理集群所需的一切。
Kubernetes 仪表板
Kubernetes 仪表盘是一个基于 Web 的用户界面,用于 Kubernetes 集群(图 11-1)。如果您运行自己的 Kubernetes 集群,而不是使用托管服务,则可以运行 Kubernetes 仪表盘,以获得与托管服务控制台基本相同的信息。

图 11-1. Kubernetes 仪表盘显示关于您的集群的有用信息
如您所料,仪表盘允许您查看集群、节点和工作负载的状态,方式与kubectl工具类似,但具有图形界面。您还可以使用仪表盘创建和销毁资源。
由于仪表盘公开了关于您的集群和工作负载的大量信息,因此正确保护它非常重要,并且绝不能将其暴露在公共互联网上。仪表盘允许您查看 ConfigMaps 和 Secrets 的内容,其中可能包含凭据和加密密钥,因此您需要像对待这些秘密本身一样严格控制对仪表盘的访问。
2018 年,安全公司 RedLock 发现了数百个Kubernetes 仪表盘控制台可以通过互联网访问而没有任何密码保护,其中包括一个由特斯拉公司所有。他们从中提取了云安全凭证,并使用这些凭证访问了更多的敏感信息。
最佳实践
如果您不必运行 Kubernetes 仪表盘(例如,如果您已经有由 GKE 等托管服务提供的 Kubernetes 控制台),则不要运行它。如果确实需要运行,确保它具有最低权限,并且永远不要将其暴露在互联网上。而是通过kubectl proxy访问它。
Weave Scope
Weave Scope是一个出色的可视化和监控工具,适用于您的集群,实时显示节点、容器和进程的地图。您还可以查看指标和元数据,甚至可以使用 Scope 启动或停止容器。
kube-ops-view
kube-ops-view为您提供集群中正在发生的情况的可视化:节点信息、每个节点上的 CPU 和内存利用率、每个节点正在运行的 Pod 数量以及这些 Pod 的状态。这是获取集群概览及其运行情况的好方法。
node-problem-detector
node-problem-detector是一个 Kubernetes 附加组件,可以检测并报告几种节点级问题:硬件问题(如 CPU 或内存错误)、文件系统损坏以及卡住的容器运行时。
当前,node-problem-detector 通过向 Kubernetes API 发送事件来报告问题,并配有一个 Go 客户端库,您可以用它来与自己的工具集成。
尽管 Kubernetes 目前对来自节点问题检测器的事件不采取任何行动,但未来可能会进一步集成,使调度器能够避免在有问题的节点上运行 Pod,例如。
进一步阅读
Kubernetes 的安全和集群管理是一个复杂和专业的主题,我们在这里只是浅尝辄止。这确实值得一本专门的书…… 而且已经有了。安全专家 Liz Rice 和 Michael Hausenblas 合著了优秀的Kubernetes Security(O’Reilly),涵盖了安全集群设置、容器安全、密钥管理等内容。
另一个很好的资源是Production Kubernetes(O’Reilly),由 Josh Rosso、Rich Lander、Alex Brand 和 John Harris 合著。我们强烈推荐这两本书。
总结
安全不是一个产品或一个最终目标,而是一个需要知识、思考和关注的持续过程。容器安全也不例外,确保安全的机制可以供你使用。如果你已经阅读并理解了本章的信息,你就知道如何在 Kubernetes 中安全配置你的容器了——但我们确信你明白,这只是安全流程的开始,而不是结束。
主要事项
-
RBAC 在 Kubernetes 中为你提供了细粒度的权限管理。确保启用,并使用 RBAC 角色仅授予特定用户和应用所需的最低权限。
-
容器并非神奇免于安全和恶意软件问题。使用扫描工具来检查你在生产环境中运行的任何容器。
-
使用 Kubernetes 并不意味着你不需要备份。使用 Velero 来备份你的数据和集群状态。它对于在集群之间移动内容也很方便。
-
kubectl是一个强大的工具,可用于检查和报告集群及其工作负载的各个方面。熟悉kubectl。你们将会一起花费很多时间。 -
使用你的 Kubernetes 提供商的 Web 控制台和
kube-ops-view查看图形化概览。如果使用 Kubernetes Dashboard,请像保护云凭证和加密密钥一样保护它的安全性。
^(1) 参见 David Cassel 在 2017 年 6 月 10 日发表的文章“初级开发人员误删生产数据库”,文章发表在 thenewstack.io blog 上。
第十二章:部署 Kubernetes 应用程序
我躺在背上,惊讶地感受到自己是如何平静和专注的,绑在四百五十万磅的炸丨药上。
罗恩·加兰,宇航员
在本章中,我们将讨论如何将您的清单文件转换为运行中的应用程序。我们将学习如何为您的应用程序构建 Helm 图表,并查看一些替代清单管理工具:Tanka、kustomize、Kapitan 和 kompose。
使用 Helm 构建清单
我们在 第二章 中看到如何使用从 YAML 清单创建的 Kubernetes 资源部署和管理应用程序。您完全可以使用这种方式管理所有 Kubernetes 应用程序的原始 YAML 文件,但这并不理想。维护这些文件不仅困难,而且存在分发问题。
假设您想要让其他人在他们自己的集群中运行您的应用程序。您可以向他们分发清单文件,但他们必然需要根据自己的环境定制一些设置。
为了做到这一点,他们将不得不复制 Kubernetes 配置的自己的副本,找到定义各种设置的位置(可能在几个地方重复),并对其进行编辑。
随着时间的推移,他们将需要维护文件的自己的副本,并在您提供更新后,他们将不得不手动拉取并与其本地更改进行协调。
最终,这开始变得痛苦起来。我们希望能够将原始清单文件与您或应用程序任何用户可能需要调整的特定设置和变量分离开来。理想情况下,我们可以将这些内容以标准格式提供给任何人下载并安装到 Kubernetes 集群中。
一旦我们拥有了这些,那么每个应用程序都可以暴露出不仅是配置值,还包括它对其他应用程序或服务的任何依赖关系。一个智能的包管理工具可以通过单个命令安装和运行应用程序及其所有依赖项。
在 “Helm:一个 Kubernetes 包管理器” 中,我们介绍了 Helm 工具,并展示了如何使用它安装公共图表。现在让我们更详细地看看 Helm 图表,以及如何创建我们自己的图表。
Helm 图表的内部构成是什么?
在演示资料库中,打开 hello-helm3/k8s 目录,看看我们的 Helm 图表里面有什么。
每个 Helm 图表都有一个标准结构。首先,图表包含在一个与图表同名的目录中(在这种情况下是 demo):
demo
├── Chart.yaml
├── production-values.yaml
├── staging-values.yaml
├── templates
│ ├── deployment.yaml
│ └── service.yaml
└── values.yaml
Chart.yaml 文件
接下来,它包含一个名为 Chart.yaml 的文件,其中指定了图表的名称和版本:
name: demo
sources:
- https://github.com/cloudnativedevops/demo
version: 1.0.1
Chart.yaml 中有许多可选字段供您提供,包括指向项目源代码的链接,如此处所示,但唯一必需的信息是名称和版本。
values.yaml 文件
还有一个名为 values.yaml 的文件,其中包含图表作者公开的可修改用户设置:
environment: development
container:
name: demo
port: 8888
image: cloudnatived/demo
tag: hello
replicas: 1
这看起来有点像 Kubernetes YAML 清单,但有一个重要的区别。values.yaml 文件是完全自由格式的 YAML,没有预定义的模式:由您选择定义什么变量,它们的名称和值。
您的 Helm chart 中完全可以没有任何变量,但如果有的话,您可以将它们放在 values.yaml 中,然后在图表的其他地方引用它们。
暂时忽略 production-values.yaml 和 staging-values.yaml 文件;我们稍后会解释它们的用途。
Helm 模板
那么这些变量被引用在哪里?如果你看 templates 子目录,你会看到几个看起来很熟悉的文件:
`ls k8s/demo/templates`
deployment.yaml service.yaml
这些与前面示例中的部署和服务清单文件完全相同,只是现在它们是模板:不再直接引用诸如容器名称之类的东西,而是包含一个 Helm 将从 values.yaml 中实际值替换的占位符。
下面是模板部署的样子:
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Values.container.name }}-{{ .Values.environment }}
spec:
replicas: {{ .Values.replicas }}
selector:
matchLabels:
app: {{ .Values.container.name }}
template:
metadata:
labels:
app: {{ .Values.container.name }}
environment: {{ .Values.environment }}
spec:
containers:
- name: {{ .Values.container.name }}
image: {{ .Values.container.image }}:{{ .Values.container.tag }}
ports:
- containerPort: {{ .Values.container.port }}
env:
- name: ENVIRONMENT
value: {{ .Values.environment }}
提示
花括号表示 Helm 应该替换变量值的位置,但它们实际上是 Go 模板语法 的一部分。
(没错,Go 到处都是。Kubernetes 和 Helm 本身都是用 Go 编写的,所以 Helm charts 使用 Go 模板并不奇怪。)
插值变量
此模板中引用了多个变量:
...
metadata:
name: `{``{` `.Values.container.name` `}``}``-{{` `.Values.environment` `}}`
这整段文字,包括花括号,都将被插值(即替换)为从 values.yaml 中取得的 container.name 和 environment 的值。生成的结果将看起来像这样:
...
metadata:
name: `demo-development`
这非常强大,因为诸如 container.name 这样的值在模板中被多次引用。当然,它也在 Service 模板中被引用:
apiVersion: v1
kind: Service
metadata:
name: `{``{` `.Values.container.name` `}``}`-service-{{ .Values.environment }}
labels:
app: `{``{` `.Values.container.name` `}``}`
spec:
ports:
- port: {{ .Values.container.port }}
protocol: TCP
targetPort: {{ .Values.container.port }}
selector:
app: `{``{` `.Values.container.name` `}``}`
type: ClusterIP
例如,您可以看到 .Values.container.name 被引用了多少次。即使在这样一个简单的图表中,您需要多次重复相同的信息片段。使用 Helm 变量可以消除这种重复。例如,要更改容器名称,您只需编辑 values.yaml 并重新安装图表,更改将在所有模板中传播。
Go 模板格式非常强大,你可以用它来做远不止简单的变量替换:它支持循环、表达式、条件语句,甚至调用函数。Helm charts 可以利用这些特性从输入值生成相当复杂的配置,不像我们示例中的简单替换。
您可以在 Helm 文档中阅读更多有关如何编写 Helm 模板的信息。
引用模板中的值
您可以在 Helm 中使用 quote 函数自动引用模板中的值:
name: {{.Values.MyName | quote }}
只有字符串值应该用引号括起来 —— 不要对数值(比如端口号)使用 quote 函数。
指定依赖关系
如果你的图表依赖于其他图表怎么办?例如,如果你的应用程序使用 Redis,那么你的应用 Helm 图表可能需要指定redis图表作为一个依赖项。
你可以使用Chart.yaml中的dependencies部分来实现这一点:
dependencies:
- name: redis
version: ~15.4.1
repository: https://charts.bitnami.com/bitnami
现在运行helm dependency update命令,Helm 将下载那些图表,准备与你自己的应用程序一起安装。这些依赖项可以是本地图表,也可以是托管在公共 Helm 存储库中的图表。
你也可以覆盖任何作为依赖项引入的图表的默认值(通常称为子图表)。有关更多详情,请参阅Helm 文档上的子图表值。
版本中的~符号告诉 Helm,你愿意自动升级到图表的更新版本,直到下一个次要版本发布。例如,~15.4.1表示 Helm 将安装最新的15.4.x版本,在15.5.x之前停止。Helm,像许多其他包管理器一样,还使用.lock 文件,因此更新后,你应该看到生成一个Chart.lock文件,记录了当时使用的确切版本。
部署 Helm 图表
现在让我们看看如何实际使用 Helm 图表来部署应用程序。Helm 最有价值的功能之一是能够指定、更改、更新和覆盖配置设置。在本节中,我们将看到它的工作原理。如果你想跟着做示例,这个例子在cloudnativedevops GitHub 演示库的目录中。
设置变量
我们已经看到,Helm 图表的作者可以将所有用户可修改的设置放在values.yaml中,以及这些设置的默认值。那么,一个图表的用户如何改变或覆盖这些设置以适应他们的本地站点或环境呢?helm install和helm upgrade命令允许你在命令行上指定额外的值文件,这些文件将覆盖values.yaml中的任何默认值。让我们看一个例子。
创建环境变量
假设你想在一个分段环境中部署应用程序的一个版本。对于我们的示例目的,实际上这意味着什么并不重要,但假设应用程序根据名为ENVIRONMENT的环境变量的值知道它是在分段环境还是在生产环境,并相应地改变其行为。那个环境变量是如何创建的?
再次查看deployment.yaml模板,这个环境变量是通过以下代码提供给容器的:
...
env:
- name: ENVIRONMENT
value: {{ .Values.environment }}
environment的值来自values.yaml,这是你所期望的:
environment: development
...
因此,使用默认值安装图表将导致容器的ENVIRONMENT变量包含development。假设你想将其更改为staging。你可以像我们看到的那样编辑values.yaml文件,但更好的方法是创建一个额外的 YAML 文件,仅包含该变量的一个值:
environment: staging
您会在 k8s/demo/staging-values.yaml 文件中找到这个值,该文件不是 Helm 图表的一部分——我们只是提供它来节省一点打字时间。
在 Helm 发布中指定值
要在helm upgrade命令中指定额外的值文件,请使用--values标志,如下所示:
`helm upgrade --install demo-staging` `--values=./k8s/demo/staging-values.yaml ./k8s/demo`
注意
请注意,在这些示例中,我们使用helm upgrade --install而不是helm install。这样做可以让您无论 Helm 发布是否已安装,都可以使用相同的命令。如果您更喜欢首先使用helm install,然后在后续部署时使用helm upgrade,那也可以。
这将创建一个新的发布,名称为demo-staging,运行容器的ENVIRONMENT变量将设置为staging而不是development。我们指定的额外值文件中列出的变量(使用--values标志)与默认值文件(values.yaml)中的变量组合在一起。在这种情况下,只有一个变量(environment),staging-values.yaml中的值将覆盖默认值文件中的值。
你还可以直接在命令行上使用--set标志指定数值。这对于诸如更改镜像标签版本或测试快速更改等情况非常有用。通常,你应该创建一个单独的 YAML 文件,包含特定环境所需的任何值覆盖,例如示例中的 staging-values.yaml 文件,并将该文件跟踪到源代码控制存储库中,然后使用--values标志应用它。
虽然您自然会希望以这种方式设置配置值来安装自己的 Helm 图表,但您也可以对公共图表进行操作。要查看图表提供的可设置值列表,请运行带有图表名称的helm get values:
`helm inspect values ./k8s/demo`
使用 Helm 更新应用程序
你已经学会了如何使用默认值和自定义值文件安装 Helm 图表,但是如何更改某些正在运行的应用程序的值呢?
helm upgrade命令将为您执行此操作。假设您希望更改演示应用程序的副本数(Kubernetes 应运行的 Pod 的副本数)。默认情况下为 1,您可以从 values.yaml 文件中看到:
replicas: 1
您已经知道如何使用自定义值文件覆盖此设置,因此编辑 staging-values.yaml 文件以添加适当的设置:
environment: staging
`replicas``:` `2`
运行与之前相同的命令,将您的更改应用于现有的demo-staging部署,而不是创建一个新的部署:
`helm upgrade --install demo-staging` `--values=./k8s/demo/staging-values.yaml ./k8s/demo`
Release "demo-staging" has been upgraded. Happy Helming!
您可以随意运行helm upgrade多次以更新正在运行的部署,Helm 将乐意为您效劳。
回滚到之前的版本
如果你决定不喜欢刚刚部署的版本,或者出现了问题,使用helm rollback命令回滚到之前的版本非常容易,只需指定之前发布的编号(如helm history输出中所示)即可:
`helm rollback demo-staging 1`
Rollback was a success! Happy Helming!
实际上,回滚不一定要回到先前的版本;假设你回滚到版本 1,然后决定要向前回滚到版本 2。如果运行helm rollback demo-staging 2,这正是会发生的事情。
使用 helm 进行自动回滚
你可以让 Helm 自动回滚一个不成功的部署。如果在helm upgrade命令中加入--atomic标志,Helm 将等待你的部署成功。如果进入FAILED状态,它将自动回滚到上一个成功的发布。你应该确保在此情况下设置了失败部署的警报,以便你能够调试发生了什么,否则你可能不会注意到你的部署实际上并没有成功!
创建 Helm Chart 库
到目前为止,我们已经使用 Helm 从本地目录安装了图表。你不需要拥有自己的图表库来使用 Helm,因为将应用程序的 Helm Chart 存储在应用程序自己的库中是很常见的。
但是,如果你确实想要维护自己的 Helm chart 库,这非常简单。图表需要通过 HTTP 访问,并且有多种方式可以实现这一点:将它们放在云存储桶中、托管自己的ChartMuseum 服务器,使用 Artifactory,使用 GitHub Pages,或者如果有现成的 Web 服务器,则可以使用现有的 Web 服务器。
一旦所有图表都集中在一个单独的目录下,运行helm repo index命令来创建包含库元数据的index.yaml文件。
你的图表库已经准备就绪!详细了解管理图表库的更多细节,请参阅 Helm 的文档。
要从你的库中安装图表,首先需要将库添加到 Helm 的列表中:
`helm repo add myrepo http://myrepo.example.com`
`helm install myrepo/myapp`
使用 Sops 管理 Helm Chart 中的秘密
我们在“Kubernetes Secrets”中看到了如何在 Kubernetes 中存储秘密数据,以及如何通过环境变量或挂载文件将其传递给应用程序。如果需要管理超过一两个秘密,可能会更容易创建一个包含所有秘密的单个文件,而不是每个文件都包含一个秘密。如果你正在使用 Helm 部署你的应用程序,你可以将该文件作为值文件,并使用 Sops 进行加密(参见“使用 Sops 加密秘密”)。
我们在演示库的hello-sops目录中为你构建了一个示例:
`cd hello-sops`
`tree`
. ├── k8s │ └── demo │ ├── Chart.yaml │ ├── production-secrets.yaml │ ├── production-values.yaml │ ├── staging-secrets.yaml │ ├── staging-values.yaml │ ├── templates │ │ ├── deployment.yaml │ │ └── secrets.yaml │ └── values.yaml └── temp.yaml
3 directories, 9 files
这是与我们早期示例类似的 Helm Chart 布局(参见“Helm Chart 的内部是什么?”)。在这里,我们定义了一个Deployment和一个Secret。但在这个例子中,我们添加了一个变化,使得管理不同环境中的多个秘密变得更加容易。
让我们来看看我们的应用程序将需要的秘密:
`cat k8s/demo/production-secrets.yaml`
secret_one: ENC[AES256_GCM,data:ekH3xIdCFiS4j1I2ja8=,iv:C95KilXL...1g==,type:str] secret_two: ENC[AES256_GCM,data:0Xcmm1cdv3TbfM3mIkA=,iv:PQOcI9vX...XQ==,type:str] ...
在这里,我们使用 Sops 加密了多个应用程序使用的秘密的值。
现在看一下 Kubernetes 的secrets.yaml文件:
`cat k8s/demo/templates/secrets.yaml`
apiVersion: v1 kind: Secret metadata:
name: {{ .Values.container.name }}-secrets type: Opaque data:
{{ $environment := .Values.environment }} app_secrets.yaml: {{ .Files.Get (nospace (cat $environment "-secrets.yaml")) | b64enc }}
在最后两行中,我们在 Helm 图表中添加了一些 Go 模板,以便根据 values.yaml 文件中设置的 environment 从 production-secrets.yaml 或 staging-secrets.yaml 文件中读取秘密。
最终结果将是一个名为 demo-secrets 的单个 Kubernetes Secret,其中包含在任一秘密文件中定义的所有键值对。此 Secret 将作为一个名为 secrets.yaml 的单个文件挂载到 Deployment 中供应用程序使用。
我们还在最后一行末尾添加了 ...| b64enc。这是使用 Helm 的 Go 模板的另一个方便的快捷方式,自动将秘密数据从明文转换为 base64,这是 Kubernetes 默认期望秘密的格式(参见 base64)。
我们需要首先使用 Sops 临时解密文件,然后将更改应用到 Kubernetes 集群。这是一个命令管道,用于部署演示应用程序的暂存版本及其暂存秘密:
sops -d k8s/demo/staging-secrets.yaml > temp-staging-secrets.yaml && \
helm upgrade --install staging-demo --values staging-values.yaml \
--values temp-staging-secrets.yaml ./k8s/demo && rm temp-staging-secrets.yaml
这是它的工作原理:
-
Sops 解密 staging-secrets 文件,并将解密后的输出写入 temp-staging-secrets。
-
Helm 使用来自 staging-values 和 temp-staging-secrets 的数值来安装
demo图表。 -
temp-staging-secrets 文件被删除。
因为所有这些操作都在一步中完成,所以我们不会留下包含明文秘密的文件,供错误的人发现。这与 helm-secrets 插件的工作方式非常相似,因此如果您喜欢这种管理秘密的工作流程,那么值得查看这个项目。
使用 Helmfile 管理多个图表
当我们在 “Helm: Kubernetes 包管理器” 中介绍 Helm 时,我们向您展示了如何将演示应用程序 Helm 图表部署到 Kubernetes 集群。尽管 Helm 非常有用,但它一次只能操作一个图表。您如何知道应该在集群中运行哪些应用程序,以及您在安装它们时应用的自定义设置?
有一个很棒的工具叫做 Helmfile,它可以帮助你完成这些操作。就像 Helm 通过模板和变量使您能够部署单个应用程序一样,Helmfile 使您能够使用单个命令部署应该安装在您的集群上的所有内容。
Helmfile 的内容是什么?
在 demo 仓库中有一个使用 Helmfile 的示例。在 hello-helmfile 文件夹中,您会找到 helmfile.yaml:
repositories:
- name: prometheus-community
url: https://prometheus-community.github.io/helm-charts
releases:
- name: demo
namespace: demo
chart: ../hello-helm3/k8s/demo
values:
- "../hello-helm3/k8s/demo/production-values.yaml"
- name: kube-state-metrics
namespace: kube-state-metrics
chart: prometheus-community/kube-state-metrics
- name: prometheus
namespace: prometheus
chart: prometheus-community/prometheus
set:
- name: rbac.create
value: true
repositories 部分定义了我们将要引用的 Helm 图表仓库。在这种情况下,唯一的仓库是 prometheus-community,官方的 Prometheus Helm 仓库。如果您正在使用自己的 Helm 图表仓库(参见 “创建 Helm 图表仓库”),请在这里添加。
接下来,我们定义了一组 releases:我们希望部署到集群的应用程序。每个发布指定了以下一些元数据:
-
name用于部署的 Helm 图表名称 -
namespace用于部署 -
chart是图表本身的 URL 或路径 -
values给出了与部署一起使用的values.yaml文件的路径 -
set在值文件中设置任何额外的值
我们在这里定义了三个发布版本:演示应用程序,加上 Prometheus(请参阅“Prometheus”)和kube-state-metrics(请参阅“Kubernetes Metrics”)。
图表元数据
请注意,我们已经指定了相对路径到demo图表和值文件:
- name: demo
namespace: demo
chart: ../hello-helm3/k8s/demo
values:
- "../hello-helm3/k8s/demo/production-values.yaml"
因此,您的图表不需要在图表存储库中以便 Helmfile 管理它们; 例如,您可以将它们全部保存在同一个源代码存储库中。
对于prometheus图表,我们已指定了prometheus-community/prometheus。 由于这不是文件系统路径,Helmfile 知道要在仓库的 URL 上查找图表,我们在repositories部分中定义了该 URL:
- name: prometheus-community
url: https://prometheus-community.github.io/helm-charts/
所有图表都在各自的values.yaml文件中设置了各种默认值。 在 Helmfile 的set:部分,您可以指定在安装应用程序时想要覆盖的任何值。
在这个例子中,对于prometheus发布,我们想确保rbac.create的值设置为true:
- name: prometheus
namespace: prometheus
chart: prometheus-community/prometheus
set:
- name: rbac.create
value: true
应用 Helmfile
helmfile.yaml然后以声明方式指定了集群中应该运行的所有内容(或者至少是其中的一个子集),就像 Kubernetes 清单一样。 当您应用这个声明性清单时,Helmfile 将使集群符合您的规范。
要做到这一点,请运行:
`helmfile sync`
Adding repo prometheus- community https://prometheus-community.github.io/helm-charts "prometheus-community" has been added to your repositories
Building dependency release=demo, chart=../hello-helm3/k8s/demo Affected releases are:
demo (../hello-helm3/k8s/demo) UPDATED kube-state-metrics (prometheus-community/kube-state-metrics) UPDATED prometheus (prometheus-community/prometheus) UPDATED prometheus-node- exporter (prometheus-community/prometheus-node-exporter) UPDATED ...
就好像您依次为您定义的每个 Helm 图表运行了helm install/helm upgrade一样。
例如,您可能希望作为连续部署管道的一部分自动运行helm sync(请参阅第十四章)。 而不是手动运行helm install以将新应用程序添加到集群中,您可以只需编辑您的 Helmfile,将其检入源代码控制,并等待自动化推出您的更改。 在“GitOps”中,我们还将介绍使用集中工具管理多个 helm 发布的另一种方法。
提示
使用单一的真相源。 不要混合手动使用 Helm 部署单个图表,并使用 Helmfile 或 GitOps 工具在集群中声明性地管理所有图表。 如果您应用了 Helmfile,然后还使用 Helm 在带外部署或修改应用程序,您将不再拥有集群的单一真相源。 这肯定会导致问题,因此选择一种管理部署的流程,并在使用方式上保持一致。
如果 Helmfile 不完全符合您的口味,Helmsman是一个类似的工具,做的事情或多或少相同。
与任何新工具一样,我们建议仔细阅读文档,比较各种选项,尝试它们,然后决定哪个适合您。
高级清单管理工具
虽然 Helm 是一个很棒的工具,并且被广泛使用,但它确实有一些局限性。编写和编辑 Helm 模板并不是一件很有趣的事情。Kubernetes 的 YAML 文件复杂、冗长且重复。因此,Helm 模板也如此。
正在开发几种新工具来解决这些问题,并使得处理 Kubernetes 清单变得更加容易:要么通过比 YAML 更强大的语言描述,例如 Jsonnet,要么将 YAML 文件分组到基本模式并使用覆盖文件进行定制。
kustomize
kustomize 与 Helm 并列,可能是另一个最受欢迎的清单管理工具。实际上,从 Kubernetes 版本 1.14 开始,kustomize 已经包含在 kubectl CLI 工具中。您还可以按照他们的 说明 单独使用 kustomize 通过安装二进制文件。与模板化不同,kustomize 使用普通的 YAML 文件和覆盖来允许替换和重用。您从 base YAML 清单开始,并使用 overlays 来修补不同环境或配置的清单。kustomize 将从基本文件加上覆盖生成最终的清单。
我们在 cloudnativedevops GitHub 演示库 的 hello-kustomize 目录中有一个工作示例:
`cd hello-kustomize/demo/base`
`kubectl kustomize ./`
apiVersion: v1 kind: Service metadata:
labels: app: demo org: acmeCorporation name: demo-service ... --- apiVersion: apps/v1 kind: Deployment metadata:
labels: app: demo org: acmeCorporation name: demo spec:
replicas: 1 ...
在这个例子中,kustomize 读取我们的 kustomization.yaml 文件,在部署(Deployment)和服务(Service)的 app 和 org 上设置了 commonLabels,并显示了更新清单的最终渲染输出。与 Helm 的不同之处在于,这里的原始 deployment.yaml 和 service.yaml 文件是完全有效且可用的 YAML,但我们仍然可以通过 kustomize 添加一些灵活性,例如在我们的清单中使用多个公共标签。
patchesStrategicMerge kustomize 设置允许我们覆盖字段,例如按环境更改副本计数:
`kubectl kustomize ../overlays/production`
... apiVersion: apps/v1 kind: Deployment metadata:
labels: app: demo org: acmeCorporation name: prod-demo spec:
replicas: 3 ...
再次强调,您应该查看渲染后的部署(Deployment)和服务(Service)清单的最终结果,但请注意我们如何基于 overlays/production 目录中的 replicas.yaml 文件,将生产环境的 replicas 字段更改为 3。
我们在这里应用的另一个有用修改是 namePrefix 设置。我们的新部署现在命名为 name: prod-demo,而不是在 base 目录中定义的默认 name: demo,我们的服务被重命名为 prod-demo-service。Kustomize 还配有其他类似的辅助工具,用于在不同情况下对清单进行调整,同时保持原始的 YAML 清单处于可用且有效的状态。
要应用您的 kustomize 定制清单,您可以使用 kubectl apply -k(而不是 kubectl apply -f),kubectl 将使用 kustomize 读取 YAML 文件,渲染任何覆盖或修改,然后将最终结果应用到集群中。
如果不喜欢模板化 YAML 文件,那么 kustomize 值得一试。您可能会遇到社区项目,这些项目提供使用它们的 Helm 图表或 kustomize 安装其应用程序的选项,因此熟悉它的工作方式是值得的。
Tanka
有时候,仅使用声明性 YAML 是不够的,特别是对于需要能够使用计算和逻辑的大型和复杂部署。例如,您可能希望根据集群的大小动态设置副本数。为此,您需要一个真正的编程语言。
Tanka 允许您使用名为 Jsonnet 的语言编写 Kubernetes 清单,Jsonnet 是 JSON 的扩展版本(它是等同于 YAML 的声明性数据格式,Kubernetes 也可以理解 JSON 格式的清单)。Jsonnet 为 JSON 添加了重要的功能——变量、循环、算术、条件语句、错误处理等等。
Kapitan
Kapitan 是另一个清单管理工具,专注于在多个应用程序甚至集群之间共享配置值。它还可以用于其他类型的工具,如 terraform 代码、Dockerfiles 和 Jinja2 模板文档。Kapitan 具有配置值的分层数据库(称为 inventory),允许您通过插入不同的值来重用清单模式,具体取决于环境或应用程序,并且可以使用 Jsonnet 或基于 Python 的 Kadet 后端引擎生成它们:
local kube = import "lib/kube.libjsonnet";
local kap = import "lib/kapitan.libjsonnet";
local inventory = kap.inventory();
local p = inventory.parameters;
{
"00_namespace": kube.Namespace(p.namespace),
"10_serviceaccount": kube.ServiceAccount("default")
}
kompose
如果您一直在 Docker 容器中运行生产服务,但尚未使用 Kubernetes,则可能熟悉 Docker Compose。
Compose 允许您定义和部署一组共同工作的容器:例如,一个 web 服务器,一个后端应用程序以及像 Redis 这样的数据库。一个单独的 docker-compose.yml 文件可以用来定义这些容器如何相互通信。
kompose 是一个将 docker-compose.yml 文件转换为 Kubernetes 清单的工具,帮助您从 Docker Compose 迁移到 Kubernetes,而无需从头编写自己的 Kubernetes 清单或 Helm 图表。
Ansible
您可能已经熟悉 Ansible,这是一款流行的基础设施自动化工具。它不是专门针对 Kubernetes 的,但它可以使用扩展模块管理许多不同类型的资源,类似于 Puppet(参见“Puppet Kubernetes Module”)。
除了安装和配置 Kubernetes 集群外,Ansible 还可以直接管理 Kubernetes 资源,如部署和服务,使用 k8s 模块。
像 Helm 一样,Ansible 可以使用其标准模板语言(Jinja)模板化 Kubernetes 清单,并且它具有更复杂的变量查找概念,使用分层系统。例如,您可以为一组应用程序或部署环境(如 staging)设置公共值。
如果你的组织已经在使用 Ansible,那么评估一下是否应该也将其用于管理 Kubernetes 资源是非常值得的。如果你的基础设施仅基于 Kubernetes,那么 Ansible 可能会比你需要的功能更多,但对于混合基础设施来说,使用一个工具来管理所有内容会非常有帮助:
kube_resource_configmaps:
my-resource-env: "{{ lookup('template', template_dir +
'/my-resource-env.j2') }}"
kube_resource_manifest_files: "{{ lookup('fileglob', template_dir +
'/*manifest.yml') }}"
- hosts: "{{ application }}-{{ env }}-runner"
roles:
- kube-resource
kubeval
与本节讨论的其他工具不同,kubeval 不是用于生成或模板化 Kubernetes 配置文件的工具,而是用于验证它们的有效性。
每个 Kubernetes 版本都有一个不同的 YAML 或 JSON 配置文件的模式,能够自动检查你的配置文件是否符合模式是非常重要的。例如,kubeval 将检查你是否为特定对象指定了所有必需的字段,以及这些值是否为正确的类型。
当应用配置文件时,kubectl 也会验证它们,并在尝试应用无效配置文件时给出错误。但事先验证它们也非常有用。kubeval 不需要访问集群,也可以验证针对任何 Kubernetes 版本的配置文件。
将 kubeval 添加到你的持续部署流水线中是个好主意,这样可以在对配置文件进行更改时自动验证它们。你也可以使用 kubeval 测试,例如在实际升级之前,你的配置文件是否需要任何调整以适用于最新版本的 Kubernetes。
总结
尽管你可以仅使用原始的 YAML 配置文件将应用部署到 Kubernetes,但这样做并不方便。Helm 是一个强大的工具,可以帮助你解决这个问题,前提是你要了解如何充分利用它。
未来将有很多不同的工具可以使 Kubernetes 配置文件的管理变得更加简单。了解使用 Helm 的基础知识非常重要,因为许多流行的工具都以 Helm Charts 的形式提供给你,用于在你的集群中安装:
-
Chart 是 Helm 的一个包规范,包括关于包的元数据,一些用于配置它的配置值,以及引用这些值的模板 Kubernetes 对象。
-
安装一个 Chart 会创建一个 Helm 发布(release)。每次你安装一个 Chart 的实例时,都会创建一个新的发布。当你使用不同的配置值更新一个发布时,Helm 会增加发布的修订版本号。
-
要根据自己的需求定制一个 Helm Chart,可以创建一个自定义的 values 文件,仅覆盖你关心的设置,并将其添加到
helm install或helm upgrade命令行中。 -
你可以使用一个变量(例如
environment)来选择不同的值或密钥集,具体取决于部署环境:预备环境、生产环境等。 -
使用 Helmfile,你可以声明性地指定一组 Helm Charts 和要应用到你的集群的值,并通过一个命令安装或更新它们全部。
-
Helm 可与 Sops 一起用于处理图表中的秘密配置。它还可以使用函数自动将您的秘密进行 base64 编码,这是 Kubernetes 所期望的。
-
Helm 不是管理 Kubernetes 清单的唯一工具。Kustomize 是另一个强大的工具,甚至内置在
kubectl中。Kustomize 采用与 Helm 不同的方法,而不是插入变量,而是使用 YAML 覆盖来调整清单。 -
Tanka 和 Kapitan 是使用 Jsonnet 的替代清单管理工具。
-
使用
kubeval快速测试和验证清单的有效语法和常见错误。
第十三章:开发工作流程
冲浪是一个令人惊奇的概念。你用一根小棍子挑战大自然,说着 我要骑你!,但很多时候大自然会回答 不,你做不到!,把你摔到水底。
乔琳·布拉洛克
在本章中,我们将扩展讨论第十二章中的内容,关注整个应用程序生命周期,从本地开发到在 Kubernetes 集群中部署更新,包括数据库迁移这个棘手的主题。我们将介绍一些工具,帮助你开发、测试和部署应用程序,包括 Skaffold 和 Telepresence。我们还将介绍 Knative 和 OpenFaaS,这两个选项可在你的集群上运行“无服务器”架构。我们还将查看更复杂的部署,并使用 Helm 钩子来顺序应用程序的部署。
开发工具
在 第十二章 中,我们看了一些工具,帮助你编写、构建和部署你的 Kubernetes 资源清单。这在一定程度上很好,但是当你开发运行在 Kubernetes 中的应用程序时,通常希望能够即时尝试并查看变化,而不必经历完整的构建-推送-部署-更新循环。
Skaffold
Skaffold 是来自 Google 的开源工具,旨在提供快速的本地开发工作流程。它在你本地开发时自动重建你的容器,并将这些更改部署到本地或远程集群。
在你的仓库中的 skaffold.yaml 文件中定义你想要的工作流程,并运行 skaffold 命令行工具启动流水线。当你在本地目录中的文件上进行更改时,Skaffold 将唤醒,使用更改构建一个新的容器,然后自动为你部署它,省去了往返到容器的时间。
我们在 演示仓库 中有一个 Skaffold 示例来展示它的工作原理。按照你操作系统的 Skaffold 安装说明,将你的 kubectl 指向你的本地开发 Kubernetes 集群,并切换到 hello-skaffold 目录查看其工作方式:
`cd hello-skaffold`
`skaffold dev`
Listing files to watch...
- skaffold-demo Generating tags...
- skaffold-demo -> skaffold-demo:e50c9e7-dirty Checking cache...
- skaffold-demo: Found Locally Tags used in deployment:
- skaffold-demo -> skaffold-demo:39f0eb63b0ced173... Starting deploy... Helm release demo not installed. Installing... NAME: demo NAMESPACE: demo STATUS: deployed REVISION: 1 TEST SUITE: None Waiting for deployments to stabilize...
- demo:deployment/demo is ready. Deployments stabilized in 1.097 second Press Ctrl+C to exit Watching for changes... ...
Skaffold 自动使用 Helm 构建和部署演示应用程序,现在正在监视代码或图表的更改。它使用随机生成的 SHA 作为临时占位符容器标签,这是在进行开发时的情况。如果你 curl localhost:8888,你应该会看到 Hello, skaffold!,因为这是在本示例的 main.go 中设置的。保持 Skaffold 运行状态,并更改 main.go 文件以说一些其他内容,比如 Hola, skaffold!。只要保存更改,Skaffold 将自动重新构建和重新部署应用程序的更新副本:
...
Watching for changes...
Tags used in deployment:
- skaffold-demo -> skaffold-demo:39f0eb63b0ced173...
Starting deploy...
Release "demo" has been upgraded. Happy Helming!
NAME: demo
NAMESPACE: demo
STATUS: deployed
REVISION: 2
TEST SUITE: None
Waiting for deployments to stabilize...
- demo:deployment/demo is ready.
Deployments stabilized in 1.066 second
Watching for changes...
现在,如果您curl localhost:8888,您将看到您修改后的代码正在运行。Skaffold 构建了演示容器的新副本,并使用 Helm 发布了一个新版本到您的集群。继续在运行 Skaffold 时进行更改,它将继续自动更新应用程序。当您玩够了,可以使用Ctrl+C停止 Skaffold,并自动卸载演示应用程序。
Cleaning up...
release "demo" uninstalled
您可以使用 Skaffold 进行其他类型的构建,例如 Bazel,或使用您自己的自定义构建脚本。它支持使用Google Cloud Build作为远程镜像构建工具,而不是使用您的本地计算机。您还可以将测试套件整合到工作流程中,并在构建和部署过程中自动运行测试。
Telepresence
Telepresence与 Skaffold 采取了稍微不同的方法。您不需要本地 Kubernetes 集群;Telepresence Pod 在您的实际集群中作为应用程序的占位符运行。然后拦截和路由应用程序 Pod 的流量,将其转发到在您本地计算机上运行的容器。
这使开发者能够使其本地计算机参与远程集群。随着他们对应用程序代码的更改,这些更改将在实时集群中反映出来,而无需部署新的容器。如果您的应用程序构建时间特别长,这可能是一个值得尝试的吸引人工具。
开发应用程序时,常见的抱怨之一是需要构建一个新的容器来包含新的代码更改,如果重新构建速度慢的话,这可能会让人沮丧。Telepresence 旨在减轻这种沮丧,并加快本地开发过程,同时仍然使用容器和 Kubernetes,这样本地开发和生产部署之间的运行环境不需要完全不同。
Waypoint
HashiCorp,开发了像 Vault、Terraform 和 Consul 这样流行工具的开发者,已经构建了一个新工具,专注于简化端到端的应用开发体验。Waypoint引入了一个声明性的waypoint.hcl文件,允许您定义应用的构建、部署、配置和发布步骤,无论其使用的运行时或平台如何。使用像 Waypoint 这样的标准包装器意味着您的 CI/CD 工具和流程可以标准化,即使您的应用程序使用不同的构建和部署步骤。开发者可以检出一个应用程序仓库,运行waypoint build—而不必考虑这个特定的应用程序是否使用 Dockerfile 或类似CloudNative Buildpack,并期望得到用于运行应用程序的正确构件。同样,waypoint deploy抽象了这个特定应用程序使用 Kubernetes、Helm 或者它可能作为 AWS Lambda 函数运行的细节。为开发者和 CI/CD 工具提供标准的 CLI 体验可以极大地简化不同类型应用程序的构建和部署流程。
Knative
虽然我们看到的其他工具侧重于加速本地开发循环,Knative则更为雄心勃勃。它旨在提供一种标准机制,用于将各种工作负载部署到 Kubernetes:不仅限于容器化应用程序,还包括无服务器风格的函数。
Knative 与 Kubernetes 和 Istio 集成(参见“Istio”)以提供完整的应用/函数部署平台,包括设置构建流程、自动化部署以及事件机制,标准化应用程序使用消息传递和队列系统的方式(例如 Pub/Sub、Kafka 或 RabbitMQ)。
OpenFaaS
OpenFaas是另一个项目,可以在 Kubernetes 上运行无服务器函数。它包括用于构建和部署函数到集群的 CLI 工具,并能触发 HTTP 事件、定时任务、MQTT、Kafka 和 RabbitMQ 消息以及其他流行的事件驱动系统和消息队列。这些无服务器架构与微服务思维方式结合得很好,并且可以更有效地利用服务器资源。
来自 OpenFaaS 开发团队的一个有趣的旁支项目被称为arkade,它类似于一个应用商店或市场,用于安装流行的云原生工具(例如 OpenFaaS 本身)的统一工具。通过一个arkade命令,您可以在您的 Kubernetes 集群上安装 OpenFaaS 并开始部署您的无服务器工作负载。
Crossplane
Crossplane 是一个旨在将 Kubernetes 的标准化引入非 Kubernetes 云资源的工具。许多应用程序需要在 Kubernetes 之外生活的基础设施组件,例如 AWS S3 存储桶、RDS 数据库实例或 Azure Cosmos DB 实例。通常,这意味着开发人员最终需要使用像Terraform这样的工具先部署这些外部依赖,然后再切换到使用 Dockerfile、Kubernetes 清单和 Helm 图表部署应用程序。
Crossplane 使用 CRD(参见“Operators and Custom Resource Definitions (CRDs)”)来定义那些生活在 Kubernetes 之外的基础设施类型,使它们就像本地的 Kubernetes 对象一样。它使用云供应商的 API 实际创建和部署这些云资源,就像您使用 Terraform CLI 或您的云提供商的 CLI 和 SDK 工具一样。想象一下,您可以像这样声明一个您的应用程序需要运行的 PostgreSQL RDS 实例:
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: xpostgresqlinstances.aws.database.example.org
labels:
provider: aws
vpc: default
spec:
writeConnectionSecretsToNamespace: crossplane-system
compositeTypeRef:
apiVersion: database.example.org/v1alpha1
kind: XPostgreSQLInstance
resources:
- name: rdsinstance
base:
apiVersion: database.aws.crossplane.io/v1beta1
kind: RDSInstance
spec:
forProvider:
region: us-east-1
dbInstanceClass: db.t2.small
masterUsername: postgres-admin
engine: postgres
engineVersion: "12"
skipFinalSnapshotBeforeDeletion: true
publiclyAccessible: false
writeConnectionSecretToRef:
namespace: crossplane-system
...
现在,您可以将应用程序的 RDS 实例清单与其他 Kubernetes 清单一起打包并一起部署。然后,Kubernetes 协调循环将确保此 RDS 实例被配置,就像为您的应用程序 Pods 做的那样。另一个好处是,当基础架构与应用程序一起管理和部署时,您可以轻松地在它们之间传递配置。例如,Crossplane CRD 可以在创建数据库时自动为密码创建一个 Kubernetes Secret,并将其传递给您的应用程序以供连接使用。我们预计随着 Kubernetes 的广泛采用,这种扩展 Kubernetes 管理其他类型基础设施的方式将变得流行起来。
部署策略
如果您手动升级运行中的应用程序(没有使用 Kubernetes),一种方法是只需关闭应用程序,安装新版本,然后重新启动。但这意味着会有停机时间。
如果您的应用程序在多个服务器上分布有多个实例,一个更好的方式是依次升级每个实例,这样就不会中断服务:所谓的零停机部署。
并非所有应用都需要零停机时间。例如,消费消息队列的内部服务是幂等的,因此它们可以一次性进行升级。这意味着升级速度更快,但对于用户界面的应用程序来说,我们通常更关心避免停机时间,而不是追求快速升级。
在 Kubernetes 中,您可以选择最合适的策略。RollingUpdate是逐个 Pod 进行的零停机时间选项,而Recreate是快速一次性升级所有 Pod 的选项。此外,还有一些字段可以调整,以获得您应用程序所需的确切行为。
在 Kubernetes 中,应用程序的部署策略在部署清单中定义。默认为RollingUpdate,如果不指定策略,就会使用这个。要将策略更改为Recreate,请像这样设置:
apiVersion: apps/v1
kind: Deployment
spec:
replicas: 1
strategy:
type: Recreate
现在让我们更仔细地看看这些部署策略,看看它们是如何工作的。
滚动更新
在滚动更新中,Pod 会逐个升级,直到所有副本都被新版本替换。
例如,假设我们运行一个具有三个副本的应用程序,每个副本都运行v1。开发人员使用kubectl apply...或helm upgrade...命令启动了升级到v2的过程。会发生什么?
首先,三个v1 Pod 中的一个将被终止。Kubernetes 将标记它为未就绪,并停止向其发送流量。将会启动一个新的v2 Pod 来替代它。与此同时,剩余的v1 Pod 继续接收传入的请求。在等待第一个v2 Pod 变为就绪时,我们只剩下两个 Pod,但仍在为用户提供服务。
当v2 Pod 准备就绪时,Kubernetes 将开始向其发送用户流量,同时还会发送到其他两个v1 Pod。现在我们又回到了三个 Pod 的完整组合。
这个过程会一直进行,逐个 Pod 地,直到所有的v1 Pod 都被替换成了v2 Pod。尽管有时候会出现比平时更少的 Pod 用于处理流量,但整个应用实际上从未真正下线。这就是零停机部署。
注意
在滚动更新期间,旧版本和新版本的应用将同时处于服务状态。虽然通常这不会成为问题,但你可能需要采取措施来确保安全;例如,如果你的更改涉及数据库迁移(见“使用 Helm 处理迁移”),普通的滚动更新可能就不可行了。
如果你的 Pod 有时候在就绪状态后短时间内可能会崩溃或失败,可以使用minReadySeconds字段让滚动更新等待每个 Pod 稳定(参见minReadySeconds)。
重新创建
在Recreate模式下,所有运行中的副本会同时被终止,然后重新创建新的副本。
对于不直接处理请求的应用程序来说,这是可以接受的。Recreate的一个优点是它避免了同时运行两个不同版本的应用程序的情况(参见“滚动更新”)。
maxSurge 和 maxUnavailable
随着滚动更新的进行,有时候会有比名义上的replicas值更多或更少的 Pod 在运行。两个重要的设置管理这种行为:maxSurge和maxUnavailable:
-
maxSurge设置了额外 Pod 的最大数量。例如,如果你有 10 个副本并将maxSurge设置为 30%,那么任何时候运行的 Pod 数量不会超过 13 个。 -
maxUnavailable设置了不可用 Pod 的最大数量。对于名义上有 10 个副本并且maxUnavailable设置为 20%的 Kubernetes,不会让可用 Pod 的数量少于 8 个。
这些可以设置为整数或百分比:
apiVersion: apps/v1
kind: Deployment
spec:
replicas: 10
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 20%
maxUnavailable: 3
通常,对于这两个设置,默认值25%是可以接受的,您可能不需要调整它们,但根据负载要求,您可能需要对其进行微调,以确保应用程序在升级期间能够保持可接受的容量。在非常大的规模上,您可能会发现在部署期间以75%的可用性运行不足以处理您的传入流量,因此您需要稍微减少maxUnavailable。
maxSurge的值越大,您的发布速度越快,但这会增加您集群资源的负载。较大的maxUnavailable值也加快了发布速度,但会减少应用程序的容量。
另一方面,较小的maxSurge和maxUnavailable值减少了对您集群和用户的影响,但可能会使您的发布过程时间更长。只有您可以决定对您的应用程序来说,正确的权衡是什么。
蓝/绿部署
在蓝/绿部署中,与逐个杀死和替换 Pods 不同,会创建一个全新的 Deployment,并启动一个新的运行v2的 Pods 堆栈,放置在现有的v1 Deployment 旁边。
其中一个优点是您无需同时处理旧版本和新版本的应用程序处理请求。另一方面,您的集群需要足够大,以运行所需应用程序副本的两倍,这可能很昂贵,并且大部分时间都会有很多未使用的容量(除非根据需要调整集群的规模)。
我们在“服务资源”中了解到,Kubernetes 使用标签来决定哪些 Pods 应该从服务接收流量。实施蓝/绿部署的一种方法是在旧 Pods 和新 Pods 上设置不同的标签(见“标签”)。
通过对示例应用程序的服务定义进行小调整,您可以仅将流量发送到标记为deployment: blue的 Pods:
apiVersion: v1
kind: Service
metadata:
name: demo
spec:
ports:
- port: 8080
protocol: TCP
targetPort: 8080
selector:
app: demo
`deployment``:` `blue`
type: ClusterIP
在部署新版本时,您可以将其标记为deployment: green。即使完全运行,它也不会接收任何流量,因为 Service 仅将流量发送到blue Pods。您可以对其进行测试,并确保它准备就绪,然后再进行切换。
要切换到新的Deployment,编辑Service以更改选择器为deployment: green。现在新的green Pods 将开始接收流量,一旦所有旧的blue Pods 处于空闲状态,您可以关闭它们。
彩虹部署
在一些罕见情况下,特别是当 Pods 具有非常长寿的连接(例如 websockets)时,仅仅使用蓝色和绿色可能不够。您可能需要同时维护三个或更多版本的应用程序。
有时这被称为彩虹部署。每次部署更新时,您都会获得一组新的颜色 Pods 集合。随着连接最终从您最老的 Pods 集合中排干,它们可以关闭。
Brandon Dimcheff 在详细描述了一个 彩虹部署示例。
金丝雀部署
蓝/绿(或彩虹)部署的优点在于,如果你决定不喜欢新版本或者它的行为不正确,你可以简单地切换回仍在运行的旧版本。然而,这种方法很昂贵,因为你需要集群能力同时运行两个版本,这意味着你可能需要运行更多的节点。
避免这个问题的一种替代方法是金丝雀部署。像煤矿中的金丝雀一样,一小部分新的 Pods 暴露在生产环境中,观察它们的反应。如果它们能够生存下来,升级过程就可以继续进行。如果出现问题,影响范围则严格限制。
就像蓝/绿部署一样,你可以使用标签来做到这一点(参见 “标签”)。在 Kubernetes 文档中有一个详细的金丝雀部署示例,可以参考 文档。
更复杂的方法是使用服务网格,例如 Istio、Linkerd 或我们在 “服务网格” 中提到的其他工具之一,它们允许你随机路由变量比例的流量到一个或多个服务版本。这也使得像 A/B 测试之类的操作变得更加容易。
使用 Helm 处理迁移
无状态应用容易部署和升级,但涉及到数据库时情况可能更复杂。通常情况下,对数据库模式的更改需要在新应用版本运行之前运行迁移任务。例如,在 Rails 应用中,你需要在启动新的应用 Pods 之前运行 rails db:migrate。
在 Kubernetes 中,你可以使用 Job 资源来执行此操作(参见 “Jobs”)。另一个选择是使用 initContainer(参见 “Init Containers”)。你也可以在部署过程中使用 kubectl 命令来编写脚本,或者如果你使用 Helm,那么你可以使用一个名为 hooks 的内置功能。
Helm 钩子
Helm 钩子允许你控制部署过程中发生事情的顺序。它们还允许你在升级过程中遇到问题时中止操作。
这里是一个使用 Helm 部署的 Rails 应用程序数据库迁移 Job 示例:
apiVersion: batch/v1
kind: Job
metadata:
name: {{ .Values.appName }}-db-migrate
annotations:
"helm.sh/hook": pre-upgrade
"helm.sh/hook-delete-policy": hook-succeeded
spec:
activeDeadlineSeconds: 60
template:
name: {{ .Values.appName }}-db-migrate
spec:
restartPolicy: Never
containers:
- name: {{ .Values.appName }}-migration-job
image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
command:
- rails
- db:migrate
helm.sh/hook 属性在 annotations 部分中定义:
annotations:
"helm.sh/hook": pre-upgrade
"helm.sh/hook-delete-policy": hook-succeeded
pre-upgrade 设置告诉 Helm 在执行升级之前应用此 Job 清单。该 Job 将运行标准的 Rails 迁移命令。
"helm.sh/hook-delete-policy": hook-succeeded 告诉 Helm 如果任务成功完成(即退出状态为 0),则删除该 Job。
处理失败的钩子
如果 Job 返回非零退出代码,这表明出现错误,迁移过程未能成功完成。Helm 将保留 Job 处于失败状态,以便你可以调试出错原因。
如果发生这种情况,发布过程将停止,应用程序将不会升级。运行 kubectl get pods 将显示失败的 Pod,允许您检查日志并查看发生了什么。
问题解决后,您可以删除失败的作业(kubectl delete job <job-name>),然后再次尝试升级。另一个自动清理失败作业的选项是添加我们在 “清理已完成的作业” 中介绍的 ttlSecondsAfterFinished 字段。
其他钩子
钩子除了 pre-upgrade 之外还有其他阶段。您可以在发布的以下任何阶段使用钩子:
-
pre-install在渲染模板后,但在创建任何资源之前执行。 -
post-install在所有资源加载后执行。 -
pre-delete在删除请求前执行,而不是删除任何资源。 -
post-delete在删除请求后,释放的所有资源都已删除。 -
pre-upgrade在渲染模板后,但在加载任何资源之前执行(例如,在kubectl apply操作之前)。 -
post-upgrade在所有资源升级后执行。 -
pre-rollback在回滚请求后,但在回滚任何资源之前执行渲染模板。 -
post-rollback在回滚请求后,所有资源已被修改时执行。
钩子链
Helm 钩子还具有将它们按特定顺序链在一起的能力,使用 helm.sh/hook-weight 属性。这些钩子将按从低到高的顺序运行,因此具有 hook-weight 为 0 的作业将在具有 hook-weight 为 1 的作业之前运行:
apiVersion: batch/v1
kind: Job
metadata:
name: {{ .Values.appName }}-stage-0
annotations:
"helm.sh/hook": pre-upgrade
"helm.sh/hook-delete-policy": hook-succeeded
"helm.sh/hook-weight": 0
您可以在 Helm 的 文档 中找到关于钩子的所有信息。
另一种实现更复杂部署情况的方法是使用 Kubernetes 的 Operators。这使您能够构建适合您用例的自定义任务集,包括作为部署应用程序新版本一部分需要发生的任何特殊逻辑或自定义步骤。一个常见的使用 Operator 的例子是升级数据库,通常需要作为升级到新版本的一部分进行额外的维护或准备任务。
总结
如果您必须构建、推送和部署容器映像以测试每个小的代码更改,那么开发 Kubernetes 应用程序可能会很繁琐。像 Skaffold 和 Telepresence 这样的工具可以加快这个循环,加快开发速度。
特别是,与传统服务器相比,使用 Kubernetes 更容易将更改推广到生产环境,前提是您理解基本概念以及如何自定义它们以适应您的应用程序:
-
Kubernetes 中默认的
RollingUpdate部署策略每次升级少量 Pod,等待每个替换 Pod 就绪后再关闭旧的 Pod。 -
滚动更新避免了停机时间,但代价是使部署时间变长。这也意味着在部署期间您的应用的新旧版本将同时运行。
-
您可以调整
maxSurge和maxUnavailable字段来优化滚动更新。取决于您使用的 Kubernetes API 版本,预设值可能适合也可能不适合您的情况。 -
Recreate策略会直接清除所有旧的 Pod 并一次性启动新的 Pod。这很快,但会导致停机时间,因此不适合面向用户的应用程序。 -
在蓝/绿部署中,所有新的 Pod 都会启动并准备就绪,但不接收任何用户流量。然后在一次性切换所有流量到新的 Pod 之前,将所有流量转移到新的 Pod 上,然后淘汰旧的 Pod。
-
彩虹部署类似于蓝/绿部署,但同时有多个版本在服务中。
-
您可以通过调整 Pod 的标签并更改前端 Service 上的选择器来在 Kubernetes 中实现蓝/绿和彩虹部署,以将流量引导到适当的 Pod 集合。服务网格工具还增加了将流量分配到应用的不同运行版本的能力。
-
Helm 钩子提供了一种在部署的特定阶段应用某些 Kubernetes 资源(通常是作业)的方法,例如运行数据库迁移。钩子可以定义部署期间应用资源的顺序,并在某些操作失败时导致部署停止。
-
Knative 和 OpenFaaS 允许您在集群上运行“无服务器”函数,使得在可以运行 Kubernetes 的任何地方(几乎任何地方!)部署事件驱动架构变得更加容易。
第十四章:Kubernetes 中的持续部署
道不远,无不为,无不作。
老子
在本章中,我们将探讨一个关键的 DevOps 原则——持续集成 和 持续部署 (CI/CD),并看看如何在云原生、基于 Kubernetes 的环境中实现这一点。我们概述了一些设置持续部署流水线以与 Kubernetes 配合工作的选项,并展示了一个完整的示例,使用了 Google 的 Cloud Build。我们还将涵盖 GitOps 的概念,并演示如何使用名为 Flux 的 GitOps 工具自动部署到 Kubernetes。
什么是持续部署?
持续部署 (CD) 是将成功构建自动部署到生产环境。与测试套件一样,部署也应该是集中管理和自动化的。开发人员可以通过点击按钮、合并合并请求或推送 Git 发布标签来部署新版本。
CD 通常与持续集成 (CI) 相关联:开发人员的更改会自动集成和测试到主线分支。这个想法是,如果你在一个分支上进行的更改会在合并到主线时破坏构建,持续集成会立即让你知道这一点,而不是等到你完成分支并做最终合并时才发现。持续集成和部署的组合通常被称为CI/CD。
持续部署的机制通常被称为流水线:一系列自动化操作,通过一系列测试和验收阶段,将代码从开发人员的工作站传送到生产环境。
容器化应用程序的典型流水线可能如下所示:
-
开发人员将其代码更改推送到存储库。
-
构建系统会自动构建当前版本的代码并运行测试。
-
如果所有测试通过,容器图像将被发布到中央容器注册表。
-
新构建的容器会自动部署到预备环境。
-
预备环境会经历一些自动化和/或手动接受测试。
-
验证过的容器映像将被部署到生产环境。
一个关键点是通过您的各个环境测试和部署的工件不是源代码,而是容器。在源代码和运行的二进制文件之间存在许多错误产生的方式,而测试容器而不是代码可以帮助捕捉到其中很多错误。
CD 的最大好处是在生产中没有意外;除非确切的二进制图像已在预备环境成功测试过,否则不会部署任何东西。
您可以在“使用 Cloud Build 的 CI/CD 流水线”中看到此类 CD 流水线的详细示例。
我应该使用哪个 CD 工具?
和往常一样,问题并不是可用工具的短缺,而是选择的范围之广。有几个专门为云原生应用程序设计的新的 CI/CD 工具,而长期存在的传统构建工具如 Jenkins 也现在有插件允许它们与 Kubernetes 和容器一起工作。
因此,如果您已经在进行 CI/CD,您可能不需要切换到全新的系统。如果您正在将现有应用程序迁移到 Kubernetes,您可能只需对现有的流水线进行少量调整即可。
在下一节中,我们将简要介绍一些受欢迎的托管和自托管 CI/CD 工具选项。我们当然不可能覆盖所有工具,但这里有一个快速列表,应该能帮助您开始搜索。
托管 CI/CD 工具
如果您正在寻找一个开箱即用的解决方案用于您的 CI/CD 流水线,而您不需要维护底层基础设施,那么您应该考虑使用托管服务。主要的云提供商都提供了与其生态系统良好集成的 CI/CD 工具,因此首先探索一下已经包含在您云账户中的工具是值得的。
Azure 流水线
Microsoft 的 Azure DevOps 服务(以前称为 Visual Studio Team Services)包括一个持续交付管道工具,称为 Azure 流水线,类似于 Google Cloud Build。
Google Cloud Build
如果您在 Google Cloud Platform 上运行您的基础设施,那么您应该看看 Cloud Build。它作为各种构建步骤运行容器,并且流水线的配置 YAML 存储在您的代码仓库中。
您可以配置 Cloud Build 来监视您的 Git 仓库。当触发预设条件时,比如推送到特定分支或标签时,Cloud Build 将运行您指定的流水线,比如构建新的容器镜像,运行您的测试套件,发布镜像,甚至部署新版本到 Kubernetes。
要查看 Cloud Build 中完整的工作示例,请参阅 “使用 Cloud Build 的 CI/CD 流水线”。
Codefresh
Codefresh 是一个用于测试和部署应用程序到 Kubernetes 的托管服务。一个有趣的特性是能够为每个功能分支部署临时的 staging 环境。
使用容器,Codefresh 可以按需构建、测试和部署环境,然后您可以配置如何将您的容器部署到集群中的各种环境中。
GitHub Actions
GitHub Actions 已经整合到流行的托管 Git 仓库站点中。通过 GitHub 仓库共享 Actions,可以非常容易地混合和匹配并在不同应用程序之间共享构建工具。Azure 发布了一个用于部署到 Kubernetes 集群的 热门 GitHub Action。
GitHub 还提供了在您自己的服务器上本地运行 GitHub Action runners 的选项,以保持构建在您的网络内部进行。
GitLab CI
GitLab 是一个流行的替代品,用于托管 Git 仓库,与 GitHub 类似。您可以使用他们的托管服务,或在自己的基础设施上运行 GitLab。它自带强大的内置 CI/CD 工具,GitLab CI,可用于测试和部署代码。如果您已经在使用 GitLab,那么使用 GitLab CI 来实现您的持续部署流水线是有意义的。
自托管的 CI/CD 工具
如果您更倾向于拥有更多基础设施的管控权以用于您的流水线,那么还有几个很好的选择可供您在任何地方运行。其中一些工具在 Kubernetes 出现之前就存在,一些则是专门为基于 Kubernetes 的 CI/CD 流水线开发的。
Jenkins
Jenkins 是一个非常广泛采纳的 CI/CD 工具,存在多年。它有几乎可以想到的所有工作流程插件,包括 Docker、kubectl 和 Helm。还有一个专门用于在 Kubernetes 中运行 Jenkins 的新项目,称为 JenkinsX。
Drone
Drone 是一个使用容器构建的工具。它简单轻量,流水线由单个 YAML 文件定义。由于每个构建步骤都是运行容器,这意味着您可以在 Drone 上运行可以在容器中运行的任何东西。
Tekton
Tekton 引入了一个有趣的概念,即 CI/CD 组件实际上由 Kubernetes CRD 组成。因此,您可以使用原生 Kubernetes 资源构建您的构建、测试和部署步骤,并像管理 Kubernetes 集群中的其他内容一样管理流水线。
Concourse
Concourse 是一个用 Go 编写的开源 CD 工具。它也采用了声明式流水线的方法,类似于 Drone 和 Cloud Build,使用 YAML 文件来定义和执行构建步骤。Concourse 提供了一个官方的 Helm chart,可以在 Kubernetes 上部署它,从而轻松快速地启动一个容器化的流水线。
Spinnaker
Spinnaker 非常强大和灵活,但乍一看可能有点令人生畏。最初由 Netflix 开发,特别擅长大规模和复杂的部署,例如蓝/绿部署(参见“蓝/绿部署”)。有一本关于 Spinnaker 的免费电子书,名为 Continuous Delivery with Spinnaker(O’Reilly),可以帮助您了解 Spinnaker 是否符合您的需求。
Argo
Argo CD是一个类似于 Flux 的 GitOps 工具(参见“GitOps”),它通过将运行在 Kubernetes 中的内容与存储在中央 Git 仓库中的清单进行同步,自动化部署。Argo 不像使用kubectl或helm“推送”变更,而是持续从 Git 仓库中“拉取”变更,并在集群内应用它们。Argo 还提供了一个流行的流水线工具,用于运行各种流水线工作流,不仅仅是用于 CI/CD。
Keel
Keel并不是一个完整的端到端 CI/CD 工具,它仅关注于当发布新的容器镜像到容器注册表时部署它们。它可以配置响应 webhook、发送和接收 Slack 消息,并在部署到新环境之前等待批准。如果你已经有一个适合你的 CI 流程,但只需要自动化 CD 部分的方法,那么 Keel 可能值得评估。
使用 Cloud Build 的 CI/CD 流水线
现在你已经了解了 CI/CD 的一般原则,并学习了一些工具选项,让我们来看一个完整的端到端演示流水线的例子。
我们的想法并不是你一定要像我们这里使用完全相同的工具和配置;相反,我们希望你能够了解如何将所有东西组合起来,并可以调整这个示例的某些部分以适应你自己的环境。
在这个例子中,我们将使用 GitHub、Google Kubernetes Engine (GKE)集群和 Google Cloud Build,但我们不依赖于这些产品的任何特定功能。你可以使用你喜欢的任何工具复制这种类型的流水线。
如果你想使用自己的 GCP 账户来完成这个示例,请记住它会使用一些可计费资源。你会希望在测试完云资源后删除和清理它们,以确保不会意外收费。
如果你宁愿在本地尝试一个 CI/CD 示例而不使用任何 Google Cloud 资源,请跳到“GitOps”。
设置 Google Cloud 和 GKE
如果你是第一次注册新的 Google Cloud 账户,你将有资格获得一些免费的积分,这些积分可以让你在几个月内运行一个 Kubernetes 集群和其他云资源,而不会被收费。但是,在尝试任何云服务时,你一定要监控你的使用情况,以确保不会产生任何意外的费用。你可以在Google Cloud 平台网站了解更多关于免费套餐的信息并创建一个账户。
一旦您注册并登录到您自己的 Google Cloud 项目中,请按照 这些说明 创建 GKE 集群。对于此示例来说,Autopilot 集群将是合适的选择,并选择一个靠近您位置的区域。您还需要在新项目中启用 Cloud Build 和 Artifact Registry API,因为我们将与 GKE 一起使用这些服务。
接下来,我们将引导您完成以下准备创建流水线的步骤:
-
Fork demo 仓库到您自己的个人 GitHub 帐户中。
-
在 Artifact Registry 中创建一个容器仓库。
-
验证 Cloud Build 以使用 Artifact Registry 和 GKE。
-
为在推送到任何 Git 分支时构建和测试创建 Cloud Build 触发器。
-
创建基于 Git 标签部署到 GKE 的触发器。
Fork Demo Repository
使用您的 GitHub 帐户,在 GitHub 界面上 fork demo repo。如果您对 repo fork 不熟悉,您可以在 GitHub docs 中了解更多信息。
创建 Artifact Registry 容器仓库
GCP 提供了一个名为 Artifact Registry 的私有 artifact 仓库工具,可存储 Docker 容器、Python 包、npm 包和其他类型的 artifact。我们将使用它来托管我们将构建的 demo 容器映像。
在 Google Cloud web 控制台 的 Artifact Registry 页面创建名为 demo 的新 Docker 仓库,按照 这些说明。在创建 GKE 集群的同一 Google Cloud 区域中创建。
您还需要授权 Cloud Build 服务帐户具有权限更改您的 Kubernetes Engine 集群。在 GCP 的 IAM 部分,按照 GCP 文档中的说明 为 Cloud Build 服务帐户授予 Kubernetes Engine Developer 和 Artifact Registry Repository Administrator IAM 角色。
配置 Cloud Build
现在让我们看看构建流水线中的步骤。在许多现代 CI/CD 平台中,流水线的每个步骤都是运行一个容器。使用 YAML 文件在您的 Git repo 中定义构建步骤。每个步骤使用容器意味着您可以轻松地打包、版本化和共享不同流水线之间的通用工具和脚本。
在 demo 仓库内,有一个名为 hello-cloudbuild-v2 的目录。在该目录中,您会找到定义我们 Cloud Build 流水线的 cloudbuild.yaml 文件。
注意
注意在 hello-cloudbuild-v2 目录名中的 -v2。这里有一些第二版书籍的更改,与第一版不匹配。为了避免破坏第一版中使用的任何示例,我们在此示例中使用了完全不同的目录。
让我们依次查看此文件中的每个构建步骤。
构建测试容器
这是第一步:
- id: build-test-image
dir: hello-cloudbuild-v2
name: gcr.io/cloud-builders/docker
entrypoint: bash
args:
- -c
- |
docker image build --target build --tag demo:test .
与所有 Cloud Build 步骤一样,这由一组 YAML 键值对组成:
-
id为构建步骤提供了一个人性化标签。 -
dir指定要在 Git 仓库中工作的子目录。 -
name标识要为此步骤运行的容器。 -
entrypoint指定在容器中运行的命令,如果不是默认值。 -
args提供了传递给入口点命令的必要参数。(我们在这里使用bash -c |的小技巧,将我们的args保持在一行上,这样更容易阅读。)
第一步的目的是构建一个我们可以用来运行应用程序测试的容器。由于我们使用了多阶段构建(参见“理解 Dockerfile”),我们现在只想构建第一个阶段。因此,我们使用--target build参数,告诉 Docker 只构建 Dockerfile 中FROM golang:1.17-alpine AS build下的部分,并在继续下一步之前停止。
这意味着生成的容器仍然安装有 Go,以及标记为...AS build的步骤中使用的任何包或文件,这意味着我们可以使用此镜像来运行测试套件。通常情况下,您需要在容器中安装运行测试所需的包,但不希望这些包出现在最终的生产镜像中。在这里,我们实际上正在构建一个一次性容器,仅用于运行测试套件,之后将其丢弃。
运行测试
这是我们cloudbuild.yaml中的下一步:
- id: run-tests
dir: hello-cloudbuild-v2
name: gcr.io/cloud-builders/docker
entrypoint: bash
args:
- -c
- |
docker container run demo:test go test
由于我们将我们的一次性容器标记为demo:test,在 Cloud Build 中此临时镜像将在本次构建的其余过程中仍然可用。此步骤将对该容器运行go test命令。如果任何测试失败,此步骤将失败,并且构建将退出。否则,它将继续进行到下一步。
构建应用程序容器
在这里,我们再次运行docker build,但没有使用--target标志,因此我们运行整个多阶段构建,最终得到最终的应用程序容器:
- id: build-app
dir: hello-cloudbuild-v2
name: gcr.io/cloud-builders/docker
args:
- docker
- build
- --tag
- ${_REGION}-docker.pkg.dev/$PROJECT_ID/demo/demo:$COMMIT_SHA
- .
替换变量
为了使 Cloud Build 管道文件可重用和灵活,我们使用变量,或者 Cloud Build 称之为替换。任何以$开头的内容在管道运行时都将被替换。例如,$PROJECT_ID将插入为 Google Cloud 项目,在特定构建运行时,$COMMIT_SHA是触发此构建的特定 Git 提交 SHA。在 Cloud Build 中,用户定义的替换必须以下划线字符(_)开头,并且只能使用大写字母和数字。我们将在创建构建触发器时使用${_REGION}替换变量。
Git SHA 标签
您可能会问为什么我们在容器映像标签中使用了$COMMIT_SHA。在 Git 中,每个提交都有一个唯一标识符,称为 SHA(以生成它的安全哈希算法命名)。SHA 是一串长的十六进制数字,如5ba6bfd64a31eb4013ccaba27d95cddd15d50ba3。
如果您使用此 SHA 标记您的镜像,它将提供一个链接到生成它的确切 Git 提交的链接,这也是容器中代码的完整快照。使用带有源 Git SHA 标记的构建工件的好处在于,您可以同时构建和测试许多特性分支,而不会出现任何冲突。
验证 Kubernetes 清单
在管道的这一步骤中,我们已经构建了一个通过测试的新容器,并且准备部署。但在我们部署之前,我们还希望快速检查一下我们的 Kubernetes 清单是否有效。在构建的最后一步中,我们将运行helm template生成我们的 Helm 图表的渲染版本,然后将其传输到kubeval工具(参见kubeval)以检查是否存在任何问题:
- id: kubeval
dir: hello-cloudbuild-v2
name: cloudnatived/helm-cloudbuilder
entrypoint: bash
args:
- -c
- |
helm template ./k8s/demo/ | kubeval
注意
请注意,我们在这里使用自己的 Helm 容器镜像(cloudnatived/helm-cloudbuilder),其中包含 helm 和 kubeval,但您也可以创建和使用自己的“构建镜像”,其中包含您使用的任何其他构建或测试工具。只需记住,保持构建镜像的小巧和精简很重要(参见“最小化容器镜像”)。当您每天运行数十甚至数百个构建时,大容器的拉取时间会累积增加。
发布镜像
假设管道中的每个步骤都成功完成,Cloud Build 然后可以将生成的容器镜像发布到您之前创建的 Artifact Registry 存储库中。要指定要发布的构建中的哪些镜像,请在 Cloud Build 文件的images下列出它们:
images:
- ${_REGION}-docker.pkg.dev/$PROJECT_ID/demo/demo:$COMMIT_SHA
创建第一个构建触发器
现在您已经了解了管道的工作原理,请在 Google Cloud 中创建实际执行管道的构建触发器,基于我们指定的条件。Cloud Build 触发器指定要监视的 Git 存储库,激活的条件(例如推送到特定分支或标签),以及要执行的管道文件。
现在继续创建新的 Cloud Build 触发器。登录到您的 Google Cloud 项目,并浏览到Cloud Build 触发器页面。
单击“添加触发器”按钮以创建新的构建触发器,并选择 GitHub 作为源代码库。系统将要求您授权 Google Cloud 访问您的 GitHub 存储库。选择YOUR_GITHUB_USERNAME/demo,Google Cloud 将链接到您 fork 的演示存储库的副本。您可以随意命名触发器。
在分支部分下,选择.*以匹配任何分支。
在配置部分,选择云构建配置文件,并将位置设置为hello-cloudbuild-v2/cloudbuild.yaml,这是演示存储库中文件所在的位置。
最后,我们需要创建一些替换变量,以便我们可以为不同的构建重复使用相同的cloudbuild.yaml文件。
对于此示例,您需要向触发器添加以下替换变量:
_REGION应该是您部署 Artifact Registry 和 GKE 集群的 GCP 区域,如us-central1或southamerica-east1。
当您完成时,点击创建触发器按钮。现在,您可以测试触发器并查看发生了什么!
测试触发器
继续对您分叉的演示库进行更改。编辑 main.go 和 main_test.go,用 Hola 替换 Hello 或您喜欢的任何内容,并保存这两个文件(我们将在下面的示例中使用 sed)。如果您安装了 Golang,您还可以在本地运行测试,以确保测试套件仍然通过。准备好后,提交并推送更改到您分叉的 Git 存储库:
cd hello-cloudbuild-v2
sed -i *s/Hello/Hola/g* main.go
sed -i *s/Hello/Hola/g* main_test.go
go test
PASS
ok github.com/cloudnativedevops/demo/hello-cloudbuild 0.011s
git commit -am "Update greeting"
git push
...
如果您查看 Cloud Build Web UI,您会看到项目中最近构建的列表。您应该看到列表顶部有一个当前刚刚推送的当前更改的构建。它可能仍在运行中,也可能已经完成。
希望您会看到一个绿色勾 indicating that all steps passed。如果没有,请检查构建中的日志输出,看看是什么失败了。
假设它通过了,一个容器应该已经发布到您的私有 Google Artifact Registry,并标记有您更改的 Git 提交 SHA。
从 CI/CD 管道部署
现在您可以通过 Git 推送触发构建、运行测试并发布最终容器到注册表,您已准备好将该容器部署到 Kubernetes。
对于此示例,我们将假设有两个环境,一个用于 production,一个用于 staging,并将它们部署到单独的命名空间:staging-demo 和 production-demo。两者将在同一个 GKE 集群中运行(尽管您可能希望为您的真实应用程序使用单独的集群)。
为了简化事务,我们将使用 Git 标签 production 和 staging 触发对每个环境的部署。您可能有自己的版本管理过程,如使用语义化版本(SemVer)、发布标签或在更新主分支或主干分支时自动部署到暂存环境。请随意根据您自己的情况调整这些示例。
我们将配置 Cloud Build 在将 staging Git 标签推送到仓库时部署到暂存,将 production 标签推送时部署到生产。这需要一个使用不同 Cloud Build YAML 文件 cloudbuild-deploy.yaml 的新管道。让我们看看我们部署管道中的步骤:
获取 Kubernetes 集群的凭据
要使用 Cloud Build 部署到 Kubernetes,构建将需要一个有效的 KUBECONFIG,我们可以使用 kubectl 获取它:
- id: get-kube-config
dir: hello-cloudbuild-v2
name: gcr.io/cloud-builders/kubectl
env:
- CLOUDSDK_CORE_PROJECT=$PROJECT_ID
- CLOUDSDK_COMPUTE_REGION=${_REGION}
- CLOUDSDK_CONTAINER_CLUSTER=${_CLOUDSDK_CONTAINER_CLUSTER}
- KUBECONFIG=/workspace/.kube/config
args:
- cluster-info
部署到集群
一旦构建经过身份验证,它可以运行 Helm 实际在集群中升级(或安装)应用程序:
- id: deploy
dir: hello-cloudbuild-v2
name: cloudnatived/helm-cloudbuilder
env:
- KUBECONFIG=/workspace/.kube/config
args:
- helm
- upgrade
- --create-namespace
- --install
- ${TAG_NAME}-demo
- --namespace=${TAG_NAME}-demo
- --values
- k8s/demo/${TAG_NAME}-values.yaml
- --set
- container.image=${_REGION}-docker.pkg.dev/$PROJECT_ID/demo/demo
- --set
- container.tag=${COMMIT_SHA}
- ./k8s/demo
我们向 helm upgrade 命令传递了一些额外的标志:
namespace
应部署应用程序的命名空间
values
用于此环境的 Helm 值文件
set container.image
设置部署容器的名称
set container.tag
使用具体标签(源 Git SHA)部署图像
创建一个部署触发器
现在让我们为我们想象中的staging环境创建一个新的 Cloud Build 触发器。
在 Cloud Build Web UI 中创建一个新的触发器,就像您在“创建第一个构建触发器”中所做的那样。仓库将保持不变,但这次配置它在推送tag而不是分支时触发,并设置标签名称与staging匹配。
此次构建将不再使用cloudbuild.yaml文件,而是使用*hello-cloudbuild-v2/cloudbuild-deploy.yaml*。
在替代变量部分,我们将设置一些特定于部署构建的值:
-
_REGION将与您在第一个触发器中使用的相同。它应该与您创建 GKE 集群和 Artifact Registry 仓库的 GCP 可用区域匹配。 -
_CLOUDSDK_CONTAINER_CLUSTER是您的 GKE 集群的名称。
在这里使用这些变量意味着,即使这些环境位于不同的集群或不同的 GCP 项目中,我们也可以使用相同的 YAML 文件来部署分别用于临时和生产环境。
一旦为staging标签创建了触发器,请继续通过将staging标签推送到仓库来尝试它:
`git tag -f staging`
`git push -f origin refs/tags/staging`
Total 0 (delta 0), reused 0 (delta 0) To github.com:domingusj/demo.git
* [new tag] staging -> staging
与以往一样,您可以在云构建 UI 中查看构建进度。如果一切顺利,Cloud Build 应能够成功验证到您的 GKE 集群,并将应用程序的临时版本部署到staging-demo命名空间。您可以通过检查GKE 仪表板(或使用helm status)来验证此操作。
最后,按照相同的步骤创建一个单独的 Cloud Build 触发器,以便在推送到production标签时部署到生产环境。如果一切顺利,您将在新的production-demo命名空间中运行该应用程序的另一份副本。在此示例中,我们将两个环境都部署到同一个 GKE 集群,但对于真实应用程序,您可能希望将它们分开。
调整示例流水线
当您完成尝试演示流水线时,您将希望删除为测试创建的任何 GCP 资源,包括 GKE 集群、demoArtifact Registry 仓库和您的 Cloud Build 触发器。
我们希望此示例演示了 CI/CD 流水线的关键概念。如果您使用 Cloud Build,则可以将这些示例用作设置自己流水线的起点。如果您使用其他工具,希望您能轻松地调整我们在此展示的模式,以适应您自己的环境。自动化应用程序的构建、测试和部署步骤将极大地提高所有参与者创建和部署软件的体验。
GitOps
正如我们在“基础设施即代码”章节中提到的,作为行业向 DevOps 转变的一部分,管理基础设施需要通过代码和源代码控制来进行。 “GitOps” 是一个较新的术语,根据不同的理解,其含义可能有所不同。但在高层次上,GitOps 使用源代码控制(Git 是较流行的源代码控制工具之一)来以自动化方式跟踪和管理基础设施。想象一下 Kubernetes 协调循环,但应用于更广泛的范围,即通过推送变更到 Git 存储库来配置和部署任何和所有基础设施。一些现有的 CI/CD 工具已经重新定位自己为“GitOps”工具,我们预计这个概念将在未来几年内在软件行业中快速发展和演变。
在本节中,我们将使用一个名为 Flux 的工具,通过将更改推送到 GitHub 存储库,自动将演示应用程序部署到本地 Kubernetes 集群。
Flux
Weaveworks(eksctl的创建者)可能是首先创造GitOps 术语的人。他们还构建了一个名为Flux的较受欢迎的 GitOps 工具。它可以通过轮询 Git 存储库、监视任何更改并自动应用 Flux Pods 内部运行的更改来自动将更改部署到 Kubernetes 集群。让我们尝试一个示例,看看它如何工作。
设置 Flux
flux 是与 Flux 进行交互的 CLI 工具,也可用于在 Kubernetes 中安装 Flux 组件。请按照您操作系统的安装说明设置,并将您的 kubectl 指向测试 Kubernetes 集群。
您需要创建一个 GitHub 个人访问令牌,以便 Flux 可以安全地与 GitHub 进行通信。您可以在 GitHub 个人资料的设置页面的开发者设置部分生成一个。它将需要 repo 权限,并且您可以决定是否希望它定期自动过期,或者设置为永不过期。对于本示例,任何一种方式都可以,但在真实的生产系统中,您应始终具有定期轮换凭据的流程。
请按照GitHub 说明生成个人访问令牌,并将其导出到您的环境,同时附上您的用户名,然后使用 flux check 命令检查 Flux 是否准备好安装:
export GITHUB_TOKEN=*YOUR_PERSONAL_ACCESS_TOKEN*
export GITHUB_USER=*YOUR_GITHUB_USERNAME*
flux check --pre
checking prerequisites
Kubernetes 1.21.4 >=1.19.0-0
prerequisites checks passed
...
安装 Flux
假设检查通过,您现在可以安装 Flux 了!作为过程的一部分,它将使用您的个人访问令牌自动在您的帐户中创建一个新的 GitHub 存储库,然后将该存储库用于管理您的集群。
`flux bootstrap github \`
`--owner=$GITHUB_USER \`
`--repository=flux-demo \`
`--branch=main \`
`--path=./clusters/demo-cluster \`
`--personal`
connecting to github.com ... cloned repository ... all components are healthy
此外,您应该看到 Flux 成功连接到您的仓库。您可以浏览到您的新仓库,并看到在这个仓库内,Flux 创建了一个名为clusters/demo-cluster/flux-system的目录,其中包含所有在新flux-system命名空间中运行的 Flux Kubernetes 清单。
使用 Flux 创建新部署
现在让我们使用 Flux 自动部署一个新的命名空间和部署到您的集群中。按照适当的 GitOps 方式,我们只需将更改推送到 Git 仓库即可完成此操作。您需要克隆 Flux 创建的新仓库,这意味着您需要设置 GitHub 凭据以推送新的提交。如果您尚未完成此操作,可以按照GitHub 提供的这些说明进行操作。克隆仓库后,在flux-system旁边为我们的新flux-demo部署创建一个新目录:
`git clone git@github.com:_$GITHUB_USER_/flux-demo.git`
`cd flux-demo`
`mkdir clusters/demo-cluster/flux-demo`
`cd clusters/demo-cluster/flux-demo`
接下来,我们将使用kubectl和--dry-run标志生成一个新命名空间和名为flux-demo的部署所需的 YAML。保存这些到新的清单文件后,我们将其提交并推送到仓库:
`kubectl create namespace flux-demo -o yaml --dry-run=client > namespace.yaml`
`kubectl create deployment flux-demo -n flux-demo \`
`-o yaml --dry-run=client --image=cloudnatived/demo:hello > deployment.yaml`
`git add namespace.yaml deployment.yaml`
`git commit -m "Create flux-demo Deployment"`
`git push`
由于 Flux 定期轮询 Git 仓库并监视任何更改,当它检测到新文件时,它将自动创建和部署您的新flux-demo清单:
`kubectl get pods -n flux-demo`
NAME READY STATUS RESTARTS AGE flux-demo-c77cc8d6f-zfzql 1/1 Running 0 39s
除了普通的 Kubernetes 清单外,Flux 还可以管理 Helm 发布和使用 kustomize 的清单。您还可以配置 Flux 轮询您的容器注册表,并自动部署新的镜像。然后,它将向仓库进行新的 Git 提交,跟踪它部署的镜像版本,使您的 Git 仓库与实际在集群中运行的内容保持同步。
此外,就像 Kubernetes 的协调循环一样,Flux 持续监视其管理的资源是否有任何手动更改。它将尝试保持集群与 Git 仓库中的内容同步。这样,任何使用 Flux 管理的手动更改应该会自动回滚,使您更加信任 Git 仓库作为集群中应该运行的最终来源或真实数据源。
这是 GitOps 的主要目标之一:您应该能够使用在 Git 中跟踪的代码自动管理 Kubernetes。在使用 Flux 时,将更改推送到此仓库是您唯一应该对集群进行更改的方式。使用 Git 来管理您的基础设施意味着您将在提交历史中有所有更改的记录,同时所有更改也可以作为团队合并请求流程的一部分进行同行评审。
概述
为您的应用程序设置持续部署流水线可以让您以一致、可靠和快速的方式部署软件。理想情况下,开发人员应该能够将代码推送到源代码控制库,所有构建、测试和部署阶段都在一个集中的流水线中自动完成。
由于有许多 CI/CD 软件和技术选择,我们无法给出适用于所有人的单一配方。相反,我们的目标是向您展示 CD 为何有益,并在您组织内实施时提供一些重要的思考要点:
-
在构建新管道时,决定使用哪些 CI/CD 工具是一个重要的过程。我们在本书中提到的所有工具都可能被整合到几乎任何现有环境中。
-
Jenkins、GitHub Actions、GitLab、Drone、Cloud Build 和 Spinnaker 只是一些与 Kubernetes 配合良好的热门 CI/CD 工具。
-
使用代码定义构建管道步骤使您能够与应用代码一起跟踪和修改这些步骤。
-
容器使开发人员能够将构建产物推广至各种环境,例如测试、预发布,最终到生产环境,理想情况下无需重新构建新的容器。
-
我们使用 Cloud Build 的示例管道应该很容易适应其他工具和应用程序类型。无论使用的工具或软件类型如何,CI/CD 管道的总体构建、测试和部署步骤基本相同。
-
GitOps 是谈论 CI/CD 管道时使用的较新术语。其主要理念是部署和基础设施变更应使用在源代码控制(Git)中跟踪的代码进行管理。
-
Flux 和 Argo 是流行的 GitOps 工具,可以在您将代码更改推送到 Git 存储库时自动应用更改到您的集群。
第十五章:可观测性和监控
在船上,没有什么是完全正确的。
威廉·兰格维奇,《违禁之海》
在本章中,我们将探讨云原生应用程序的可观测性和监控问题。什么是可观测性?它与监控有何关系?在 Kubernetes 中如何进行监控、日志记录、度量和跟踪?
什么是可观测性?
可观测性可能对您来说并不是一个熟悉的术语,尽管它作为表达传统监控之外更广阔世界的方式越来越流行。在我们看到可观测性如何扩展监控之前,让我们先解决监控这个问题。
什么是监控?
你的网站现在能正常工作吗?去检查一下;我们会等你。了解所有应用程序和服务是否按预期工作的最基本方法是自己查看它们。但是在 DevOps 上下文中谈论监控时,我们大多数时候指的是自动化监控。
自动化监控是以某种编程方式定期检查网站或服务的可用性或行为,并通常以自动化方式警告人类工程师是否存在问题。但问题的定义是什么?
封闭箱监控
让我们以静态网站为例。如果根本无法工作,它将不会响应,或者您会在浏览器中看到错误消息。因此,此站点的最简单监控检查是获取主页并检查 HTTP 状态码(200 表示成功请求)。您可以使用命令行 HTTP 客户端(如httpie或curl)执行此操作。如果客户端的退出状态为非零,则表示获取网站时出现问题。
但假设 Web 服务器配置出现问题,尽管服务器工作并响应 HTTP 200 OK 状态,但实际上却提供空白页面(或某种默认或欢迎页面,或者可能是错误的网站)。我们简单的监控检查将无法检测到任何问题,因为 HTTP 请求成功;然而,网站对用户而言并未按预期工作。
更复杂的监控检查可能会寻找页面上的特定文本,例如组织的名称。这将捕捉配置错误但工作正常的 Web 服务器问题。
超越静态页面
您可以想象更复杂的网站可能需要更复杂的监控。例如,如果网站具有用户登录功能,则监控检查可能还会尝试使用已知的测试用户帐户登录,并在登录失败时发出警报。或者如果网站具有搜索功能,则检查可能会填写文本字段以某些搜索文本,模拟点击搜索按钮,并验证结果中是否包含某些预期的文本。
对于简单的网站,对于问题“它是否工作?”的肯定或否定答案可能已经足够。对于云原生应用程序来说,这些应用程序往往是更复杂的分布式系统,问题可能会变成多个问题:
-
我的应用程序在全球各地都可用吗?还是只在某些地区?
-
大多数用户加载需要多长时间?
-
关于可能拥有较慢下载速度的用户怎么办?
-
我网站的所有功能是否按预期工作?
-
特定功能的工作速度慢或完全不起作用,影响了多少用户?
-
如果它依赖于第三方服务,当外部服务出现故障或不可用时,我的应用程序会发生什么?
-
当我的云提供商遇到故障时会发生什么?
开始清楚地看到,在监控云原生分布式系统的世界中,并没有很多事情是清晰的。
闭盒监控的局限性
但无论这些检查变得多么复杂,它们都属于同一类监控:闭盒监控。正如其名称所示,闭盒检查只观察系统的外部行为,而不尝试观察其内部发生的事情。
直到几年前,像 Nagios、Icinga、Zabbix、Sensu 和 Check_MK 等流行工具执行的闭盒监控基本上是技术的最前沿。确实,任何类型的系统自动监控相比没有监控都是一大进步。但是闭盒检查存在一些局限性:
-
他们只能检测可预测的故障(例如,网站无响应)。
-
他们只检查系统外部暴露的部分的行为。
-
他们是被动和反应式的;他们只在问题发生后告诉你。
-
他们可以回答“发生了什么问题?”的问题,但无法回答更重要的“为什么?”的问题。
要回答“为什么?”的问题,我们需要超越传统的监控。
这种up/down测试还有另外一个问题;up究竟意味着什么?
“Up”是什么意思?
在运维中,我们习惯于通过uptime来衡量应用程序的弹性和可用性,通常以百分比表示。例如,99%的可用性意味着在相关时间段内最多不可用 1%。99.9%的可用性,称为三个 9,相当于每年大约九小时的停机时间,对于平均网络应用来说是一个不错的指标。四个 9(99.99%)意味着每年不到一个小时的停机时间,而五个 9(99.999%)大约是五分钟。
因此,更多的“nines”越好,你可能会这样想。但是,用这种方式看待问题会忽略一个重要的观点:
如果用户不满意,百分比无关紧要。
如果用户不满意,百分比无关紧要。
俗话说,被测量的东西会被最大化。因此,你最好非常小心你测量的内容。如果你的服务对用户不起作用,无论内部指标如何,都无关紧要:服务已停止。服务可能使用户不满意的方式有很多,即使它名义上是up。
举个明显的例子,如果您的网站加载需要 10 秒钟?加载后可能运行正常,但如果响应太慢,它可能就等同于完全不可用了。用户只会转而去其他地方。
传统的封闭盒监控可能会尝试通过定义比如说五秒的加载时间为上行,超过这个时间就被视为下行并生成警报来解决这个问题。但是如果用户体验到各种不同的加载时间,从 2 秒到 10 秒呢?使用这样的硬阈值,对于一些用户来说服务可能被认为是下行,但对于其他用户来说是上行。如果加载时间对于北美的用户来说很好,但对于欧洲或亚洲的用户来说不可用呢?
云原生应用程序从来都不是“上行”
尽管您可以继续精细化更复杂的规则和阈值,以使我们能够对服务状态给出上行/下行的答复,但事实是这个问题已经彻底有缺陷了。像云原生应用这样的分布式系统永远不会是“上行”;它们存在于一个持续部分降级服务的状态中。
这是一类称为灰色失败.^(1)的问题的示例。灰色失败从单一视角或单一观察特别难以检测到。
因此,尽管封闭盒监控可能是开始您可观察性之旅的好地方,但重要的是要意识到您不应该止步于此。让我们看看是否能做得更好。
日志记录
大多数应用程序都会产生某种形式的日志。日志是一系列记录,通常带有某种时间戳,指示记录的编写时间和顺序。例如,Web 服务器会在其日志中记录每个请求,包括诸如:
-
请求的 URI
-
客户端的 IP 地址
-
响应的 HTTP 状态
如果应用程序遇到错误,通常会记录此事实,以及可能或可能不有助于运营商找出问题原因的一些信息。
经常,来自各种应用和服务的日志将被汇总到一个中央数据库(例如 Elasticsearch),在那里可以查询和绘制图表以帮助解决问题。像 Logstash 和 Kibana 这样的工具,或者像 Splunk 和 Loggly 这样的托管服务,旨在帮助您收集和分析大量的日志数据。
日志的限制
日志可能很有用,但它们也有其局限性。关于是否记录或不记录的决定是在编写应用程序时由程序员做出的。因此,就像封闭盒检查一样,日志只能回答事先可以预测的问题或检测问题。
从日志中提取信息也可能很困难,因为每个应用程序都以不同的格式编写日志,并且运营商通常需要为每种类型的日志记录编写定制的解析器,将其转换为可用的数值或事件数据。
因为日志必须记录足够的信息来诊断任何可能的问题,它们通常具有较低的信噪比。如果记录所有内容,则在尝试查找所需的一个错误消息时,浏览数百页日志非常困难和耗时。如果仅记录偶发错误,则很难知道正常看起来是什么样子。
使用标准格式记录日志可以极大地提高其实用性。在类似 JSON 的已知结构中记录事件将使在尝试理解日志内容时能够更轻松地筛选噪音。
日志难以扩展
日志在处理流量方面也扩展不佳。如果每个用户请求都生成一个必须发送到聚合器的日志行,则可能会使用大量网络带宽(从而无法为用户提供服务),并且您的日志聚合器可能会成为瓶颈。
许多托管日志提供商还根据生成的日志量收费,这是可以理解的但不幸的:它在财务上激励您记录较少的信息,并减少用户并降低服务流量!
自托管的日志解决方案也是如此:存储的数据越多,您需要支付的硬件、存储和网络资源就越多,而仅仅保持日志聚合工作也需要更多的工程时间。
在 Kubernetes 中,日志记录是否有用?
我们稍微谈了一下容器如何生成日志以及如何在 Kubernetes 中直接检查它们,在“查看容器日志”中。这对于单个容器是一种有用的调试技术。
如果您确实使用日志记录,应该使用某种结构化数据,比如 JSON,这样可以自动解析(参见“可观测性管道”),而不是简单的文本记录。
集中日志聚合服务(如Loki)对于 Kubernetes 应用程序非常有用,但这并非解决问题的全部。尽管集中日志有一些商业用例(例如审计和安全要求,或客户分析),日志不能提供我们实现真正可观察性所需的所有信息。
因此,我们需要超越日志,寻找更强大的解决方案。
引入度量指标
更复杂的收集服务信息的方式是使用度量指标。顾名思义,度量指标是某些事物的数值度量。根据应用程序的不同,相关的度量指标可能包括:
-
当前正在处理的请求数量
-
每分钟(或每秒、每小时)处理的请求数量
-
在处理请求时遇到的错误数量
-
服务请求平均处理时间(或峰值时间、第 99 百分位数)
收集基础架构以及应用程序的度量指标也非常有用:
-
单个进程或容器的 CPU 使用率
-
节点和服务器的磁盘 I/O 活动
-
机器、集群或负载均衡器的入站和出站网络流量
指标帮助回答“为什么?”的问题
指标开启了一种新的监控维度,不仅仅是简单地查看某些东西是否工作或不工作。就像你车上的速度计或温度计上的温度刻度一样,它们为你提供了正在发生的数值信息。与日志不同,指标可以轻松地以各种有用的方式处理:绘制图表、进行统计或在预定义的阈值上发出警报。例如,你的监控系统可能会在应用程序的错误率在特定时间段内超过 10%时向你发出警报。
指标还可以帮助回答关于问题“为什么?”的问题。例如,假设用户从您的应用程序中体验到长时间的响应时间(高延迟)。您检查了您的指标,发现延迟指标的峰值与特定机器或组件的CPU 使用指标的类似峰值相一致。这立即为您提供了一个问题开始寻找问题的线索。组件可能被卡住,或者重复尝试某些失败的操作,或者其主机节点可能存在硬件问题。
指标有助于预测问题
此外,指标可以是预测性的:当事情出错时,通常不会一下子就发生。在问题对您或您的用户可察觉之前,某些指标的增加可能表明麻烦即将来临。
例如,服务器的磁盘使用率指标可能随着时间的推移而逐渐增加,最终达到磁盘实际上用完空间并开始失败的点。如果在它进入失败状态之前就对该指标发出警报,您可以完全防止失败的发生。
一些系统甚至使用机器学习技术分析指标,检测异常,并推断原因。这在复杂的分布式系统中尤其有用,但对于大多数目的来说,仅仅有一种收集、绘图和警报指标的方法就已经足够了。
指标监视应用程序的内部情况
通过闭盒检查,运营商必须猜测应用程序或服务的内部实现,并预测可能发生的故障以及这将对外部行为产生什么影响。相比之下,指标允许应用程序开发人员根据他们对系统实际运行方式(以及其失败方式)的了解,导出关键信息的隐藏方面:
停止逆向工程应用程序,从内部开始监控。
工具如 Prometheus、StatsD 和 Graphite,或者像 Datadog、New Relic 和 Dynatrace 这样的托管服务,被广泛用于收集和管理指标数据。
我们将在第十六章详细讨论 Kubernetes 的度量标准,包括应重点关注的类型以及如何处理它们。现在,让我们通过观察跟踪来完成我们对可观测性的调查。
跟踪
在监控工具箱中,另一个有用的技术是跟踪。在分布式系统中尤为重要。虽然度量和日志可以告诉您系统每个单独组件的运行情况,但跟踪则是跟随单个用户请求的整个生命周期。
假设您正试图弄清楚为什么一些用户的请求延迟非常高。您检查了系统每个组件的度量标准:负载均衡器、入口、Web 服务器、应用服务器、数据库、消息总线等等;一切看起来都正常。那么问题出在哪里呢?
当您跟踪一个(希望是代表性的)请求,从用户连接打开到关闭,您将了解该请求在系统中每个阶段的整体延迟情况。
例如,您可能会发现在管道的每个阶段处理请求所花费的时间都是正常的,除了数据库跳跃,其时间是正常时间的一百倍。尽管数据库运行良好,其度量显示没有问题,但出于某种原因,应用服务器必须等待数据库请求完成的时间非常长。
最终,您发现问题是应用服务器与数据库服务器之间的某个特定网络链接上出现了严重的数据包丢失。如果没有分布式跟踪提供的请求视角,很难发现这类问题。
一些流行的分布式跟踪工具包括 Zipkin,Jaeger 和 Lightstep。工程师 Masroor Hasan 写了一篇有用的博文,“使用 Jaeger 在 Kubernetes 上进行分布式跟踪基础设施”,描述了如何在 Kubernetes 中使用 Jaeger 进行分布式跟踪。^(2)
OpenTracing 框架(CNCF 的一部分)旨在为分布式跟踪提供一套标准的 API 和库。
Pixie 是另一个有趣的项目,为 Kubernetes 应用程序添加了跟踪和应用程序性能分析功能,还提供了丰富的查询语言,让您可以探索集群内部发生的情况。
可观测性
因为监控这个术语对不同的人有不同的意义——从简单的闭盒检查到指标、日志和跟踪的组合,现在使用可观察性作为一个涵盖所有这些技术的总称越来越普遍。系统的可观察性衡量了它的仪表化程度,以及你能够多容易地了解其内部情况。有些人说可观察性是监控的超集,另一些人则认为可观察性反映了与传统监控完全不同的思维方式。
区分这些术语最有用的方法或许是说监控告诉你系统是否工作,而可观察性则促使你问为什么它不工作,并考虑它的表现如何。
可观察性是关于理解
更普遍地说,可观察性是关于理解:理解你的系统在做什么以及它是如何做到的。例如,如果你推出一个设计上能提高特定功能性能 10%的代码更改,可观察性可以告诉你它是否起作用了。如果性能只提高了一点点,或者更糟的是稍微下降了,你需要重新审视代码。
另一方面,如果性能提高了 20%,这种变化超出了你的预期,也许你需要考虑为什么你的预测不足。可观察性帮助你建立和完善系统不同部分相互作用的心理模型。
可观察性也涉及数据。我们需要知道生成什么数据、收集什么数据、如何聚合数据(如果适用)、关注哪些结果,以及如何查询和展示它们。
软件是不透明的
在传统监控中,我们拥有大量关于机器的数据:CPU 负载、磁盘活动、网络数据包等等。但是从这些数据反推我们的软件在做什么是很困难的。要做到这一点,我们需要为软件本身添加仪表:
软件默认是不透明的;它必须生成数据,以便让人类了解它在做什么。可观察的系统使人类能够回答“它是否正常工作?”这个问题,并且如果答案是否定的,还能够诊断影响范围并确定出现了什么问题。
建立可观察性文化
更普遍地说,可观察性是关于文化的。它是 DevOps 哲学的关键原则,即在开发代码和在生产环境中运行代码之间闭环。可观察性是关闭这一环路的主要工具。开发人员和运维人员需要密切合作,为可观察性仪表化服务,并找出消费和处理所提供信息的最佳方法。
可观察性管道
从实际角度来看,可观察性是如何工作的?通常会有多个数据源(日志、指标等)以一种相当自发的方式连接到不同的数据存储中。
例如,您的日志可能会发送到 ELK 服务器,而指标可能会发送到三四个不同的托管服务,传统的监控检查报告可能会发送到另一个服务。这并不理想。
一方面,难以扩展。数据源和存储越多,相互连接越多,连接上的流量就越多。把工程时间投入到使所有这些不同类型的连接稳定可靠并不合理。
此外,系统与特定解决方案或提供者的紧密集成,使得更改它们或尝试替代方案更加困难。
解决这个问题的越来越流行的方法是可观察性管道:
使用可观察性管道,我们将数据源与目的地解耦,并提供缓冲区。这使得可观察性数据易于消费。我们不再需要弄清楚应该从容器、虚拟机和基础设施发送什么数据,将其发送到哪里以及如何发送。相反,所有数据都发送到管道中,管道负责过滤并将其发送到正确的位置。这还使我们在添加或删除数据接收器方面具有更大的灵活性,并为数据生产者和消费者之间提供了缓冲区。
Tyler Treat
可观察性管道带来了很大的优势。现在,添加新的数据源只是将其连接到您的管道。类似地,新的可视化或警报服务只是管道的另一个消费者。
由于管道会缓冲数据,因此不会丢失任何数据。如果流量突然激增并导致指标数据过载,管道会缓冲它,而不是丢弃样本。
使用可观察性管道需要标准的指标格式(参见“Prometheus”)以及理想情况下,应用程序使用 JSON 或其他合理的序列化数据格式进行结构化日志记录。不要再发出原始文本日志,然后稍后用脆弱的正则表达式进行解析,而是从一开始就使用结构化数据。
Kubernetes 监控
现在我们对封闭盒监控及其与总体上的可观察性的关系有了更多理解,让我们看看它如何适用于 Kubernetes 应用程序。
外部封闭盒检查
正如我们所见,封闭盒监控只能告诉您应用程序已经宕机。但这仍然是非常有用的信息。云原生应用程序可能存在各种问题,但仍然可以接受某些请求。工程师可以致力于解决内部问题,如慢查询和增加的错误率,而用户并不真正意识到存在问题。
然而,更严重的问题类别导致全面的停机:应用程序对大多数用户不可用或不起作用。这对用户来说是不好的,依赖于应用程序,这也可能对您的业务不利。为了检测停机情况,您的监控需要以与用户相同的方式使用服务。
监控模拟用户行为
例如,如果是 HTTP 服务,则监控系统需要向其发出 HTTP 请求,而不仅仅是 TCP 连接。如果服务只返回静态文本,监控可以检查文本是否与某些预期字符串匹配。通常情况比这复杂一点,但您的检查也可以更加健壮。
在停机情况下,一个简单的文本匹配很可能足以告诉您应用程序已停止。但是仅在您的基础设施内(例如在 Kubernetes 中)进行这些封闭框检查是不够的。停机可能由用户与基础设施外部边缘之间的各种问题和故障导致,包括:
-
错误的 DNS 记录
-
网络分区
-
数据包丢失
-
配置错误的路由器
-
缺失或错误的防火墙规则
-
云提供商停机
在所有这些情况下,您的内部指标和监控可能根本没有显示任何问题。因此,您的首要可观察性任务应该是从您自己的基础设施外部的某一点监控服务的可用性。有许多第三方服务可以为您执行此类监控,包括 Uptime Robot、Pingdom 和 Wormly。
不要建立自己的监控基础设施
大多数这些服务都有免费层或相当廉价的订阅,并且无论你为它们付费多少,都应将其视为必要的运营费用。不要试图建立自己的外部监控基础设施;这是不值得的。一年的 Uptime Robot 专业订阅费用可能还不足以支付你工程师一小时的工时费用。
在外部监控提供商中查找以下关键特性:
-
HTTP/HTTPS 检查
-
检测您的 TLS 证书是否无效或已过期
-
关键词匹配(当关键词缺失时发出警报或当其存在时)
-
通过 API 自动创建或更新检查
-
通过电子邮件、短信、Webhook 或其他直接机制发送警报
在本书中,我们倡导基础设施即代码的理念,因此应该可以使用代码自动化您的外部监控检查。例如,Uptime Robot 提供了一个简单的 REST API 用于创建新检查,并且您可以使用类似uptimerobot的客户端库或命令行工具进行自动化。
使用哪种外部监控服务并不重要,只要使用一个。但不要止步于此。在下一节中,我们将看看如何监控 Kubernetes 集群内应用程序的健康状态。
内部健康检查
云原生应用程序以复杂、难以预测和难以检测的方式失败。应用程序必须被设计为在面对意外故障时具有弹性,并且能够在封闭盒监控面前优雅地退化。但具有讽刺意味的是,它们越具有弹性,检测这些故障就越难以通过封闭盒监控来实现。
要解决这个问题,应用程序可以并且应该执行自己的健康检查。特定功能或服务的开发者最了解它所需的“健康”状况,并且他们可以编写代码来检查这一点,并以一种可以从容器外部监控结果的方式公开这些结果(例如 HTTP 终端点)。
用户是否满意?
正如我们在 “存活性探针” 中看到的,Kubernetes 为应用程序提供了一个简单的机制来宣传它们的存活性或就绪性,所以这是一个很好的起点。通常,Kubernetes 的存活性或就绪性探针非常简单;应用程序总是对任何请求回应“OK”。如果没有响应,Kubernetes 将认为其已宕机或未准备好。
然而,正如许多程序员从痛苦的经验中知道的那样,仅仅因为程序运行,并不一定意味着它能正常工作。一个更复杂的就绪性探针应该问:“这个应用程序为了完成工作需要什么?”
例如,如果需要与数据库交互,它可以检查是否有有效且响应迅速的数据库连接。如果依赖其他服务,它可以检查这些服务的可用性。(如果频繁运行健康检查,则不应执行会影响真实用户请求服务的昂贵操作。)
注意,我们仍然对准备探测给出二进制的是/否响应。只是这是一个更为明智的回答。我们试图回答的问题是:“用户是否满意?”尽可能准确地回答这个问题。
服务和熔断器
正如您所知,如果容器的“存活性”检查失败,Kubernetes 将自动以指数回退的循环重新启动它。但在容器没有问题的情况下,其某个依赖关系失败时,这并不真正有帮助。另一方面,失败的“就绪性”检查的语义是:“我没问题,但当前无法为用户请求提供服务。”
在这种情况下,容器将被从它是后端的任何服务中移除,并且 Kubernetes 将停止向其发送请求,直到它再次就绪。这是处理失败依赖关系的更好方法。
假设你有一条由 10 个微服务组成的链条,每个微服务都依赖于下一个微服务的某个关键部分工作。链条中最后一个服务失败了。倒数第二个服务会检测到这一点,并开始失败其准备探测。Kubernetes 会将其断开连接,而下一个服务检测到这一点,依此类推。最终,前端服务将失败,并(希望)会触发封闭盒监控警报。
一旦基础服务的问题得到解决,或者可能通过自动重新启动来治愈,链中的所有其他服务将自动依次恢复就绪状态,而无需重新启动或丢失任何状态。这是所谓的断路器模式的一个例子。当应用检测到下游故障时,通过就绪检查将自己从服务中移出,以防止向其发送更多请求,直到问题解决为止。
优雅降级
尽管断路器对尽快展现问题很有用,但您应该设计您的服务,避免在一个或多个组件服务不可用时使整个系统失败。相反,尝试使您的服务优雅降级:即使它们不能完成它们应该做的一切,也许它们仍然可以做一些事情。
在分布式系统中,我们必须假设服务、组件和连接将会以神秘和间歇性的方式经常失败。一个具有弹性的系统可以在不完全失败的情况下处理这一点。
概要
关于监控和可观测性的主题有很多要说的。我们希望本章为您提供了一些关于传统监控技术的有用信息,它们能做什么,不能做什么,以及在云原生环境中需要如何适应。
可观测性 的概念使我们了解到比传统日志文件和封闭盒检查更广阔的视角。指标在这个视角中占据重要位置,在下一章也是最后一章中,我们将深入探讨 Kubernetes 中指标的世界。
在翻页之前,你可能想起这些关键点:
-
封闭盒监控检查观察系统的外部行为,以检测可预测的故障。
-
分布式系统暴露了传统监控的局限性,因为它们不处于上或下状态:它们存在于部分降级服务的不断状态。换句话说,在船上没有什么是完全正确的。
-
日志对事后故障排除很有用,但它们的扩展成本高昂。Loki 和 ELK 是流行的集中式日志记录工具。
-
指标开启了一个新的维度,超越了简单的工作/不工作状态,并且为您的系统的数百或数千个方面提供了持续的数值时间序列数据。Prometheus 是聚合应用指标的一个流行选项。
-
指标可以帮助您回答“为什么?”的问题,以及在导致停机之前识别问题趋势。
-
跟踪通过个别请求的整个生命周期精确记录事件的时间,帮助您调试性能问题。Jaeger、Zipkin 和 Pixie 是提供应用程序跟踪的一些工具示例。
-
可观测性是传统监控、日志记录、指标和跟踪的结合,以及您可以了解系统的所有其他方式。
-
可观测性还代表了一个基于事实和反馈的工程团队文化的转变。
-
依然很重要的是确保你的面向用户的服务处于运行状态,通过外部的闭合盒检查,但不要试图自己构建:使用像 Uptime Robot 这样的第三方监控服务。
-
如果用户不满意,99.999% 的可用性也毫无意义。
^(1) “云规模系统的致命弱点:灰失败”,作者 Adrian Colyer,2017 年 6 月。
^(2) “在 Kubernetes 上使用 Jaeger 的分布式跟踪基础设施”,作者 Masroor Hasan,2018 年 9 月。
第十六章:Kubernetes 中的度量标准
可能你对某一主题的了解非常深入,以至于你变得完全无知。
弗兰克·赫伯特,《沙丘星际:教会堂》
在本章中,我们将深入探讨我们在第十五章中引入的度量标准概念,并详细介绍适用于 Kubernetes 的细节:有哪些类型的度量标准,哪些对于云原生服务很重要,如何选择要关注的度量标准,如何分析度量标准数据以获取可操作信息,以及如何将原始度量标准数据转换为有用的仪表板和警报?最后,我们将概述一些度量工具和平台的选项。
究竟什么是度量标准?
正如我们在“引入度量标准”中看到的那样,度量标准是特定事物的数值度量。传统服务器世界中一个熟悉的例子是特定机器的内存使用情况。如果当前只有 10%的物理内存分配给用户进程,那么机器有多余的容量。但如果内存使用率达到 90%,那么机器可能相当忙碌。
因此,度量标准可以为我们提供的一种宝贵信息是特定时刻正在发生的事情的快照。但我们可以做得更多。内存使用情况会随着工作负载的启动和停止而不断上下波动,但有时我们感兴趣的是随时间的内存使用量的变化。
时间序列数据
如果你定期采样内存使用情况,可以创建该数据的时间序列。图 16-1 展示了谷歌 Kubernetes 引擎节点内存使用时间序列数据的图表,跨越了一周的时间。这比一些瞬时值更能清晰地展示发生了什么。

图 16-1. GKE 节点内存使用的时间序列图
对于云原生可观察性目的而言,大多数我们感兴趣的度量标准都以时间序列形式表示,并且它们都是数值。例如,与日常日志数据不同,度量标准是可以进行数学和统计处理的值。
计数器与量规
它们是什么样的数字?虽然某些数量可以由整数表示(例如一台机器中的物理 CPU 数量),但大多数需要有小数部分,为了避免处理两种不同类型的数字,度量标准几乎总是表示为浮点十进制值。
鉴于此,度量标准值主要有两种类型:计数器和量规。计数器只能增加(或重置为零);它们适用于测量诸如请求服务的次数和接收到的错误数等事物。另一方面,量规可以上下变动;它们适用于连续变化的数量,例如内存使用量,或者用于表达其他数量的比率。
对于一些问题,答案只能是是或否:例如特定端点是否响应 HTTP 连接。在这种情况下,适当的度量标准将是一个量规,其取值范围有限:可能是 0 和 1。
例如,一个端点的 HTTP 检查可以被命名为http.can_connect,当端点响应时,其值可能为 1,否则为 0。
度量能告诉我们什么?
度量有什么用处?嗯,正如我们在本章前面看到的那样,度量可以告诉你什么时候出了问题。例如,如果你的错误率突然上升(或者对支持页面的请求突然激增),这可能表明存在问题。你可以根据阈值为某些度量自动生成警报。
但是度量也可以告诉你事物的运行情况,例如你的应用程序当前支持多少个同时用户。这些数字的长期趋势对操作决策和业务智能都是有用的。
选择好的度量标准
起初,你可能会认为“如果度量好,那么越多的度量肯定会更好!”但事实并非如此。你不能监控所有东西。例如,Google Cloud 的运维套件捕获了关于你的云资源的数百种内置度量数据,包括:
instance/network/sent_packets_count
每个计算实例发送的网络数据包数量
storage/object_count
每个存储桶中对象的总数
container/cpu/utilization
一个容器当前使用的 CPU 分配的百分比
列表继续(而且还会继续)。即使你能同时显示所有这些度量的图表,那需要一个像房子那么大的显示屏,你也不可能从中获取有用的信息。为了做到这一点,我们需要专注于我们关心的度量子集。
当你观察自己的应用程序时,你应该关注什么?只有你自己能回答这个问题,但我们有一些可能有帮助的建议。在本节的其余部分,我们将概述一些面向不同受众并设计以满足不同需求的可观测性常见度量模式。
值得一提的是,这是一次完美的 DevOps 协作机会,你应该在开发初期开始思考和讨论你将在开发开始时需要哪些度量标准,而不是在最后(参见“共同学习”)。
服务:RED 模式
大多数使用 Kubernetes 的人都在运行某种类型的 Web 服务:用户发出请求,应用程序发送响应。用户可以是程序或其他服务;在基于微服务的分布式系统中,每个服务向其他服务或中央 API 网关服务器发出请求,并使用结果返回信息给更多服务。无论哪种方式,这都是一个请求驱动的系统。
对于一个请求驱动的系统,了解什么是有用的?
-
显而易见的是请求的数量。
-
另一个是各种方式失败的请求数量;也就是说,错误的数量。
-
第三个有用的指标是每个请求的持续时间。这为您提供了一个了解服务表现如何以及用户可能感到多么不满意的概念。
Requests-Errors-Duration(RED)模式是一个经典的可观察性工具,可以追溯到在线服务的早期阶段。Google 的Site Reliability Engineering书籍讨论了四个黄金信号,基本上是请求、错误、持续时间和饱和度(我们稍后会谈论饱和度)。
引入了RED首字母缩写的工程师 Tom Wilkie 在一篇博客文章中阐述了该模式背后的原理:
为什么你应该为每个服务测量相同的指标?每个服务不是都很特别吗?从监控的角度来看,将每个服务都视为相同,能够提高运营团队的可扩展性。通过使每个服务看起来、感觉起来和品尝起来都相同,这减少了响应事件的认知负荷。顺便说一句,如果你对所有服务都采取相同的对待方式,许多重复性任务就可以自动化。
Tom Wilkie
那么我们究竟如何测量这些数字呢?由于总请求数量只会增加,查看请求速率更为有用:例如每秒请求数量。这为我们提供了一个有意义的概念,即系统在给定时间间隔内处理了多少流量。
因为错误率与请求率相关,所以测量错误作为请求的百分比是个好主意。因此,例如,典型的服务仪表板可能显示:
-
每秒接收的请求数
-
返回错误的请求百分比
-
请求的持续时间(也称为延迟)
资源:USE 模式
你已经看到RED模式为你提供了关于你的服务性能和用户体验的有用信息。你可以将其视为一种自上而下查看可观察性数据的方法。
另一方面,由 Netflix 性能工程师 Brendan Gregg 开发的USE 模式是一种自下而上的方法,旨在帮助分析性能问题并找出瓶颈。USE 代表利用率(Utilization)、饱和度(Saturation)和错误(Errors)。
与服务不同,我们在 USE 中关注的是资源:更低级别的基础设施服务器组件,如 CPU 和磁盘,或网络接口和链路。这些任何一个都可能成为系统性能的瓶颈,而 USE 指标将帮助我们找出是哪个:
利用率
资源忙于服务请求的平均时间,或者当前正在使用的资源容量。例如,一个已经使用 90%的磁盘将具有 90%的利用率。
饱和度
资源过载的程度,或等待此资源可用的请求队列长度。例如,如果有 10 个进程在等待在 CPU 上运行,那么它的饱和度值为 10。
错误
某资源上操作失败的次数。例如,具有一些坏扇区的磁盘可能有 25 次读取失败的错误计数。
对系统中关键资源进行这些数据测量是发现瓶颈和可能即将发生的问题的好方法。低利用率、无饱和度和无错误的资源可能是正常的。任何偏离这一情况的都值得关注。例如,如果您的某个网络链路饱和,或者出现大量错误,可能会影响整体性能问题:
USE 方法是一种简单的策略,您可以用来对系统健康进行全面检查,识别常见的瓶颈和错误。它可以早期部署,快速识别问题区域,然后可以根据需要深入研究其他方法。
USE 方法的优势在于其速度和可见性:通过考虑所有资源,您不太可能忽视任何问题。然而,它只能发现某些类型的问题——瓶颈和错误——应将其视为更大工具箱中的一个工具。
Brendan Gregg
业务指标
我们已经研究了应用和服务指标(“服务:RED 模式”),这些指标可能对开发人员最感兴趣,以及基础设施指标(“资源:USE 模式”),这些对运维和平台工程师有帮助。但是业务方面呢?可观测性是否能帮助管理人员和高管了解业务绩效,并为业务决策提供有用的输入?哪些指标可以为此做出贡献呢?
大多数企业已经跟踪他们关心的关键绩效指标(KPI),如销售收入、利润率和客户获取成本。这些指标通常来自财务部门,不需要开发人员和基础设施人员的支持。
此外,应用和服务还能生成其他有用的业务指标。例如,像软件即服务(SaaS)产品这样的订阅业务需要了解其订阅者的数据:
-
漏斗分析(有多少人访问了登陆页面、有多少点击了注册页面、有多少完成了交易等)
-
注册和取消的速率(流失率)
-
每位客户的收入(用于计算月度重复收入、每位客户的平均收入和客户的生命周期价值)
-
帮助和支持页面的效果(例如,“此页面是否解决了您的问题?”的回答是“是”的百分比)
-
系统状态公告页面的流量(在出现故障或服务降级时通常会激增)
大部分信息通常更容易通过从您的应用生成实时指标数据来收集,而不是通过处理日志和查询数据库来分析。当您为生成指标的应用程序添加工具时,不要忽视对业务重要的信息。
在业务和客户参与专家需要的可观察性信息以及技术专家需要的信息之间,并没有必然的清晰界限。实际上,它们之间存在很多重叠。最好在早期阶段与所有利益相关者讨论指标,并就需要收集的数据、收集频率、聚合方式等达成一致。
尽管如此,这两个(或更多)群体对您正在收集的可观察性数据有不同的问题需要解决,因此每个群体都需要其数据的独特视图。您可以使用常见的数据湖为每个不同的群体创建仪表板(参见“使用仪表板绘制指标”)和报告。
Kubernetes 指标
我们已经从一般术语上讨论了可观察性和指标,并查看了不同类型的数据及其分析方法。那么所有这些如何适用于 Kubernetes 呢?对 Kubernetes 集群来说,值得跟踪的指标是什么,它们能帮助我们做出什么样的决策?
在最低层次上,一个名为cAdvisor的工具监控每个集群节点上运行的容器的资源使用情况和性能统计信息,例如每个容器使用的 CPU、内存和磁盘空间。
Kubernetes 本身通过查询 kubelet 获取并使用这些cAdvisor数据,用于关于调度、自动伸缩等方面的决策。但是,您也可以将这些数据导出到第三方指标服务,在那里您可以绘制图表并对其进行警报。例如,跟踪每个容器使用的 CPU 和内存将非常有用。
您还可以使用一个名为kube-state-metrics的工具来监视 Kubernetes 本身。它监听 Kubernetes API 并报告关于逻辑对象(如节点、Pod 和部署)的信息。这些数据对于集群可观察性也非常有用。例如,如果为某个部署配置了副本,但由于某些原因(可能是集群容量不足),当前无法调度,您可能希望了解这一情况。
通常情况下,问题不在于缺少指标数据,而在于决定关注、跟踪和可视化哪些关键指标。以下是一些建议。
集群健康指标
要在顶层监控您集群的健康状态和性能,您至少应该关注以下内容:
-
节点数量
-
节点健康状态
-
每个节点和整体的 Pod 数量
-
每个节点的资源使用/分配情况,以及总体情况
这些概览指标将帮助您了解您的集群的性能如何,是否有足够的容量,其使用情况随时间的变化如何,以及是否需要扩展或减少集群。
如果您正在使用像 GKE 这样的托管 Kubernetes 服务,不健康节点将会自动检测并自动修复(前提是为您的集群和节点池启用了自动修复)。然而,了解是否出现异常故障仍然很有用,这可能表明存在潜在问题。
部署指标
对于所有您的部署,了解以下信息是很值得的:
-
部署数量
-
每个部署的配置副本数量
-
每个部署的不可用副本数量
如果您在 Kubernetes 中启用了某些自动缩放选项,能够随时间跟踪这些信息尤为有用(参见“自动缩放”)。特别是关于不可用副本的数据将帮助您警觉容量问题。
容器指标
在容器级别,了解以下信息最为有用:
-
每个节点和总体上的容器/Pods 数量
-
每个容器的资源使用与其请求/限制的比较(参见“资源请求”)
-
容器的活跃性/准备性
-
容器/Pod 的重启次数
-
每个容器的网络收发流量和错误
因为 Kubernetes 会自动重新启动失败或超出资源限制的容器,您需要知道这种情况发生的频率。过多的重新启动可能表明特定容器存在问题。如果某个容器经常超出其资源限制,这可能是程序错误的迹象,或者可能只是需要增加资源限制,如提供更多内存。
应用程序指标
无论您的应用程序使用哪种语言或软件平台,可能都有一个库或工具可供您导出自定义指标。这些主要用于开发人员和运维团队,以便了解应用程序的活动情况、频率及其持续时间。这些是性能问题或可用性问题的关键指标。
选择要捕获和导出的应用程序指标取决于您的应用程序具体的功能。但是有一些常见模式。例如,如果您的服务从队列中消费消息、处理它们,并根据消息采取某些操作,您可能希望报告以下指标:
-
接收到的消息数量
-
成功处理的消息数量
-
等待处理的消息数量
-
无效或错误消息的数量
-
处理每条消息的时间
-
生成的成功操作数量
-
失败操作的数量
同样地,如果您的应用程序主要是请求驱动的,可以使用 RED 模式(参见“服务:RED 模式”):
-
收到的请求
-
返回的错误
-
每个请求处理的时间
在开发的早期阶段,很难知道哪些指标会对您有用。如果不确定,请记录一切。对于大多数应用程序来说,输出指标相对便宜,对于时间序列数据库来说也很容易存储;您可能会因为当时看似不重要的指标数据而在很长一段时间后发现一个意想不到的生产问题。
如果它移动,就对其进行图形化。即使它不动,也要将其图形化,因为它未来可能会移动。
劳里·丹尼斯(彭博社)
如果您要求您的应用程序生成业务指标(请参阅“业务指标”),您也可以将这些计算并导出为自定义指标。
另一个可能对业务有用的事情是查看您的应用程序如何根据您与客户的任何服务级别目标(SLO)或服务级别协议(SLA)以及供应商服务如何根据 SLO 执行的表现。您可以创建一个自定义指标来显示目标请求持续时间(例如,200 毫秒),并创建一个仪表板,将其叠加在实际当前性能之上。
运行时指标
在运行时级别,大多数指标库还会报告有关程序正在执行的有用数据,例如:
-
进程/线程/goroutine 数量
-
堆和栈的使用情况
-
非堆内存使用情况
-
网络 I/O 缓冲池
-
垃圾收集器运行和暂停持续时间(适用于垃圾收集语言)
-
正在使用的文件描述符/网络套接字
这种信息对于诊断性能不佳甚至崩溃非常有价值。例如,长时间运行的应用程序通常会逐渐使用更多内存,直到由于超出 Kubernetes 资源限制而被终止并重新启动。应用程序运行时指标可能会帮助您准确找出内存消耗情况,特别是与应用程序正在执行的操作相关的自定义指标结合使用时。
现在您对值得捕获的指标数据有了一些了解,接下来我们将看看如何使用这些数据:换句话说,如何分析它。
分析指标
数据并非理解的同义词。为了从我们捕获的原始数据中获取有用的信息,我们需要对其进行聚合、处理和分析,这意味着对其进行统计分析。统计学可能是一门棘手的生意,特别是在抽象层面上,因此让我们通过一个具体的例子来阐明这一讨论:请求持续时间。
在“服务:RED 模式”中,我们提到您应该跟踪服务请求的持续时间指标,但我们没有明确说明如何做到这一点。我们究竟是什么意思持续时间?通常,我们对用户等待获取某些请求响应的时间感兴趣。
例如,在网站中,我们可以将持续时间定义为用户连接到服务器并服务器首次开始发送响应数据之间的时间。(用户的总等待时间实际上比这更长,因为建立连接需要一些时间,读取响应数据并在浏览器中呈现也需要时间。尽管如此,我们通常无法访问这些数据,所以我们只捕捉我们能够的。)
每个请求的持续时间都不同,那么我们如何将数百甚至数千个请求的数据聚合为一个单一的数字呢?
简单平均值有什么问题?
显而易见的答案是取平均值。但仔细观察后,平均意味着并不一定简单。统计学中一个古老的笑话是,平均人的腿略少于两条。换句话说,大多数人的腿比平均值多。这怎么可能?
大多数人有两条腿,但有些人只有一条或没有,这会降低整体平均值。(可能有些人有超过两条腿,但更多的人有少于两条腿。)简单的平均值并不能为我们提供关于人群中腿的分布或大多数人腿拥有体验的有用信息。
平均值也有多种类型。你可能知道,普通的平均概念指的是均值。一组值的均值是所有值的总和除以值的数量。例如,三个人的组合的平均年龄是他们年龄总和除以 3。
另一方面,中位数指的是将集合分成两个相等的部分的值,一个部分包含大于中位数的值,另一个部分包含较小的值。例如,在任何一群人中,一半人的身高高于中位身高,根据定义,另一半则更矮。
均值、中位数和异常值
采用直接平均值(均值)作为请求持续时间的方法有什么问题?其中一个重要问题是均值很容易被异常值所偏离:一两个极端值可以大幅扭曲平均值。
因此,中位数比均值受异常值影响小,是平均度量指标更有帮助的一种方式。如果某项服务的中位数延迟为一秒,那么有一半的用户经历的延迟小于一秒,另一半则更多。
图 16-2 展示了平均值可能会误导的情况。这四组数据的均值相同,但在图形上看起来截然不同(统计学家将此例称为安斯库姆四重奏)。顺便说一句,这也是演示绘制数据的重要性,而不仅仅查看原始数字的好方法。

图 16-2. 这四个数据集的均值(平均值)相同 (图像,由 Schutz,CC BY-SA 3.0 提供)。
发现百分位数
当我们讨论观察请求驱动系统的指标时,我们通常更感兴趣的是知道用户体验的最坏延迟,而不是平均值。毕竟,对于那些可能经历 10 秒或更长延迟的小群体来说,中位数延迟 1 秒并没有什么安慰之处。
获得这些信息的方法是将数据分解为百分位数。第 90 百分位数延迟(通常称为P90)是高于 90%用户体验到的值。换句话说,10%的用户将会经历高于 P90 值的延迟。
用这种语言表达,中位数就是第 50 百分位数,或者 P50。在可观察性中经常测量的其他百分位数是 P95 和 P99,分别是 95th 和 99th 百分位数。
应用百分位数到指标数据
Travis CI 的 Igor Wiedler 提供了一个不错的demonstration,从一个 10 分钟内向生产服务的 135,000 次请求数据集开始(图 16-3)。正如您所见,这些数据杂乱而波动,并且从原始状态很难得出任何有用的结论。

图 16-3. 135,000 次请求的原始延迟数据,以毫秒为单位
现在让我们看看如果我们将这些数据在 10 秒间隔内进行平均化会发生什么(图 16-4)。这看起来很不错:所有数据点都在 50 毫秒以下。因此看起来大多数用户的延迟都在 50 毫秒以下。但这真的是这样吗?

图 16-4. 相同数据的平均(均值)延迟,每 10 秒一个间隔
现在让我们绘制 P99 延迟。这是观察到的最大延迟,如果我们舍弃最高 1%的样本。看起来完全不同(图 16-5)。现在我们看到一个崎岖的模式,大多数值聚集在 0 到 500 毫秒之间,有几个请求尖峰接近 1,000 毫秒。

图 16-5. 相同数据的 P99(99th 百分位数)延迟
我们通常希望知道最坏的情况
由于我们会特别注意到慢的网络请求,所以 P99 数据很可能给我们提供用户体验延迟的更真实的图像。例如,考虑一个每天有 100 万页面浏览量的高流量网站。如果 P99 延迟为 10 秒,则有 10,000 个页面视图需要超过 10 秒。这是很多不满意的用户。
但情况变得更糟:在分布式系统中,每个页面视图可能需要完成数十甚至数百个内部请求。如果每个内部服务的 P99 延迟为 10 秒,并且每个页面视图进行 10 个内部请求,那么慢页面视图的数量每天将增加到 100,000 个。现在大约有 10%的用户感到不满意,这是一个大问题。
超越百分位数
许多指标服务实施的百分位数延迟的一个问题是,请求往往在本地进行采样,然后在中心进行统计聚合。因此,您最终得到的 P99 延迟是每个代理报告的 P99 延迟的平均值,可能涵盖数百个代理。
嗯,百分位数已经是一个平均值了,试图平均这些平均值是一个众所周知的统计陷阱。结果未必与实际平均值相同。
根据我们选择如何聚合数据,最终的 P99 延迟数字可能会相差多达 10 倍。这对于得出有意义的结果并不吉利。除非您的指标服务接收每个原始事件并生成真正的平均值,否则这个数字将是不可靠的。
工程师 Yan Cui 建议更好的方法是监控出现了什么问题,而不是正常情况:
作为监控应用程序性能的主要指标,我们可以使用什么来替代百分位数呢?当应用程序性能开始下降时,我们可以接收警报吗?
如果您回顾您的 SLO 或 SLA,您可能有类似于“99% 的请求应在 1 秒或更短时间内完成”的要求。换句话说,允许超过 1%的请求花费超过 1 秒的时间完成。
那么,如果我们监控超过阈值的请求百分比呢?当我们的 SLA 违反时,我们可以在预定义的时间窗口内触发警报,当该百分比大于 1%时。
Yan Cui
如果每个代理提交一个总请求数量的指标和超过阈值的请求数量,我们可以有用地对这些数据进行平均处理,以生成超过 SLO 的请求百分比,并对其进行警报。
使用仪表板图形化指标
到目前为止,在本章中,我们已经了解了为什么指标很有用,应该记录哪些指标以及一些有用的统计技术来批量分析它们。这都很好,但我们实际上要做什么来利用所有这些指标呢?
答案很简单:我们将对它们进行图表化处理,将它们分组到仪表板中,并可能对其进行警报。我们将在下一节讨论警报,但现在让我们看看一些图表化和仪表板技术和工具。
对所有服务使用标准布局
当您拥有多个服务时,始终以相同的方式布局您的仪表板是有意义的。响应现场页的人员可以一眼看到受影响服务的仪表板,并立即知道如何解释它,而不必熟悉该特定服务。
在 Weaveworks 博客文章 中,Tom Wilkie 建议以下标准格式(见 图 16-6):
-
每个服务一行
-
左侧是请求和错误率,错误率是请求的百分比
-
右侧是延迟

图 16-6. Weaveworks 建议的服务仪表板布局
你不必使用这个精确的布局;重要的是每个仪表板都采用相同的布局,并且每个人都熟悉它。你应该定期审查你的关键仪表板(至少每周一次),查看上周的数据,这样每个人都知道正常的样子。
请求、错误、持续时间 仪表板对服务非常有效(见 “服务:RED 模式”)。对于资源,如集群节点、磁盘和网络,通常最有用的信息是利用率、饱和度、错误(见 “资源:USE 模式”)。
构建一个主要仪表板的信息辐射器
如果你有一百个服务,你就有一百个仪表板,但你可能不经常看它们。仍然很重要将这些信息提供出来(例如帮助发现哪个服务出现故障),但在这个规模上,你需要一个更一般的概览。
为了做到这一点,请制作一个主要的仪表板,显示所有服务的请求、错误和持续时间,汇总显示。不要使用任何复杂的东西,如堆叠面积图表;坚持使用简单的总请求数、总错误百分比和总延迟的折线图。这些比复杂图表更容易解释,也更准确。
理想情况下,您将使用一个信息辐射器(也称为墙板或大型可见图表)。这是一个大屏幕,显示着关键的可观察性数据,对相关团队或办公室的每个人都可见。或者,对于分布式团队,也许这是监控网站的主页,每个人在首次登录时都会看到。信息辐射器的目的是:
-
一目了然地显示当前系统状态
-
为了明确传达团队认为重要的度量标准
-
让人熟悉正常的样子
这个辐射屏幕上应该包括什么?只有关键信息。关键,既指真正重要,也指生命体征:告诉你关于系统生命的信息。
您将在医院床边看到的生命体征监视器是一个很好的例子。它们显示人类的关键指标:心率、血压、氧饱和度、体温和呼吸频率。还有许多其他指标可以用于追踪患者,它们在医学上有重要的用途,但在主要仪表板级别,这些是关键的指标。任何严重的医学问题都会在这些指标中的一个或多个中显示出来;其他一切都是诊断的问题。
同样,您的信息辐射器应显示业务或服务的生命体征。如果有数字,可能不应超过四到五个数字。如果有图表,可能不应超过四到五个图表。
人们往往会试图把太多信息塞进仪表板中,使其看起来复杂和技术性。这不是目标。目标是专注于少数几个关键点,并使它们在整个房间内易于看到(参见图 16-7)。
会出故障的仪表板
除了主要信息辐射器和单个服务和资源的仪表板之外,您可能还希望创建特定指标的仪表板,这些指标告诉您关于系统的重要信息。您可能已经能够根据系统架构考虑到其中一些事项。但另一个有用的信息来源是会出故障的东西。

图 16-7。由Grafana Dash Gen生成的信息辐射器示例
每当发生事故或停机时,都要寻找一项或多项指标,这些指标本应在事先警告您此问题。例如,如果生产停机是由于服务器磁盘空间耗尽引起的,那么服务器磁盘空间图可能会事先警告您可用空间趋向下降并进入停机领域。
我们在这里讨论的不是在几分钟甚至几个小时内发生的问题;这些通常会被自动警报捕捉到(参见“关于指标的警报”)。相反,我们关注的是在几天或几周内逐渐接近的缓慢移动的冰山。这些是危险,如果您不注意并采取避免行动,它们将在最糟糕的时刻击沉您的系统。
事故发生后,总是要问:“如果我们事先知道这个问题会有什么预警?” 如果答案是你已经拥有但没有注意到的一些数据,那么就采取行动突出显示这些数据。仪表板是实现这一目标的一种可能方式。
虽然警报可以告诉您某个值已超过预设阈值,但您可能并不总能事先知道危险级别。图形让您可以可视化该值在长时间内的行为,并帮助您在实际影响系统之前检测到问题趋势。
关于指标的警报
也许你会感到惊讶,我们在大部分章节中都在讨论可观察性和监控,却没有提到警报。对于一些人来说,警报就是监控的全部。我们认为这种理念需要改变,原因有几点。
警报存在哪些问题?
警报表示从稳定的工作状态中出现了一些意外偏离。好吧,分布式系统没有那种状态!
正如我们所提到的,大规模分布式系统从来不完全处于正常运行状态;它们几乎总是处于部分降级服务状态(参见“云原生应用永远不“正常””)。它们有如此多的指标,如果每次某个指标超出正常限制就发出警报,你每天都会发送数百页没有任何好处:
人们过度对自己发出警报,因为他们的可观察性有问题,他们不信任他们的工具能够可靠地调试和诊断问题。因此,他们会收到数十甚至数百个警报,他们通过模式匹配来寻找问题根本原因的线索。他们在盲目飞行。在我们所有人都朝着混乱的未来快速前进的情况下,你实际上必须有纪律地减少更多的分页警报,而不是增加。请求速率、延迟、错误率、饱和度。
对于一些不幸的人来说,值班警报是生活的一部分。这不仅仅因为人性的明显原因是一件坏事。警报疲劳在医学上是一个众所周知的问题,医护人员可能会因为不断的警报而迅速变得麻木,导致在真正出现问题时更容易忽略严重情况。
要使监控系统有用,它必须具有非常高的信号与噪声比。虚假警报不仅令人恼火,而且危险:它们降低了对系统的信任,并使人们认为可以安全地忽略警报。
过多、持续不断和无关紧要的警报是三里岛核事故的一个主要因素,即使单个警报设计良好,操作员同时收到过多警报也可能会感到不堪重负。
警报应该有一个非常简单的含义:现在需要由人来采取行动。
如果不需要采取任何行动,就不需要警报。如果行动需要在某个时候进行,但现在不需要,那么可以将警报降级为较低优先级的通知,比如电子邮件或聊天消息。如果可以由自动化系统执行操作,则应该自动化:不要唤醒宝贵的人类。
值班不应该成为地狱
虽然作为 DevOps 哲学的关键部分,对自己的服务负责任是一个好主意,但同样重要的是,值班应该尽可能少地带来痛苦。
警报页面应该是一种罕见和特殊的情况。当它们确实发生时,应该有一个既定而有效的处理程序,尽量减少响应者的压力。
没有人应该全天候值班。如果情况如此,可以增加轮换人数。你不需要成为专家就可以值班:你的主要任务是对问题进行分类,决定是否需要采取行动,并将其升级给合适的人员。
尽管值班的负担应该公平分配,但人们的个人情况有所不同。如果你有家庭或工作外的其他承诺,可能不那么容易参加值班。需要仔细和敏感的管理来安排值班,以确保对每个人都公平。
如果工作涉及到值班,那么在雇佣该人时应当明确告知。对于值班轮班的频率和情况的期望应当写入他们的合同。雇佣某人从事严格的九点到五点工作,然后又决定让他们在晚上和周末值班,这是不公平的。
值班应当得到适当的补偿,可以是现金、调休或其他有意义的福利。无论您是否真正收到任何警报,当您值班时,在某种程度上您都在工作。
每个人在值班时的时间应该有严格限制。拥有更多空闲时间或精力的人可能愿意自愿帮助减少同事的压力,这很好,但不要让任何人承担过多的责任。
认识到当您让人们值班时,您正在花费人力资本。要明智地使用它。
紧急、重要和可行动的警报
如果警报如此可怕,为什么我们还要讨论它们呢?好吧,您仍然需要警报。事情会出错、爆炸、崩溃和停顿——通常发生在最不方便的时候。
可观察性是很棒的,但如果您没有寻找问题,那么您就找不到问题。仪表板很好用,但您不会付钱让某人整天盯着仪表板看。要检测当前发生的停机或问题,并引起人类注意,基于阈值的自动警报是最佳选择。
例如,您可能希望系统在某个服务的错误率在某段时间内超过 10%时向您发出警报,比如五分钟。当某个服务的 P99 延迟超过某个固定值,比如 1000 毫秒时,您可能会生成一个警报。
一般来说,如果一个问题对业务有实际或潜在的影响,并且需要立即采取行动,那么它有可能成为紧急警报通知的候选。
不要对每一个指标都发出警报。在数百甚至数千个指标中,您应该只有少数几个可以生成警报的指标。即使它们生成了警报,也不一定意味着您需要给某人发出页面。
页面应该仅限于紧急、重要和可行动的警报:
-
重要但不紧急的警报可以在正常工作时间内处理。只有不能等到早晨的事情才需要页面。
-
紧急但不重要的警报并不值得唤醒某人。例如,一个很少使用且不影响客户的内部服务的失败。
-
如果没有立即可以采取的行动来修复它,那么对此发出页面毫无意义。
对于其他情况,您可以发送异步通知:电子邮件、聊天消息、支持票据、项目问题等等。如果您的系统正常工作,这些通知将及时被查看和处理。不需要通过响亮的警报声在半夜唤醒某人,使其皮质醇水平飙升。
跟踪您的警报、非工作时间页面和唤醒
您的人员对您的基础设施同样至关重要,事实上,更为重要。因此,监控您的人员情况与监控您的服务情况一样是有意义的。
在给定周发送的警报数量是系统整体健康和稳定性的良好指标。紧急页面的数量,特别是在非工作时间、周末和正常睡眠时间发送的页面数量,是您团队整体健康和士气的良好指标。
您应该为紧急页面设置预算,特别是在非工作时间,应该非常低。每周每个值班工程师收到一到两个非工作时间页面可能是限制。如果您经常超过这个限制,您需要修复警报、修复系统或增加更多工程师。
至少每周审核所有紧急页面,并修复或消除任何虚假警报或不必要的警报。如果您不认真对待这一点,人们也不会认真对待您的警报。如果您经常因不必要的警报打断人们的睡眠和私人生活,他们会开始寻找更好的工作。
指标工具和服务
现在让我们具体讨论一些问题。您应该使用哪些工具或服务来收集、分析和传达指标?在“不要建立自己的监控基础设施”中,我们指出面对普通问题时,应该使用普通解决方案。这是否意味着您一定要使用像 Datadog 或 New Relic 这样的第三方托管指标服务呢?
这里的答案并不那么明确。尽管这些服务提供了许多强大的功能,但在大规模使用时可能会很昂贵。是否运行您自己的指标服务器的决定在很大程度上取决于您的情况,包括您管理的应用数量以及您正在收集的数据量。如果您决定建立自己的指标基础设施,有一个优秀的免费开源产品可供选择。
Prometheus
在云原生世界中,事实上的标准指标解决方案是Prometheus。它被广泛使用,特别是在 Kubernetes 上,几乎所有东西都可以以某种方式与 Prometheus 互操作,因此在考虑指标监控选项时,这是您应该首先考虑的东西。
Prometheus 是一个基于时间序列指标数据的开源系统监控和警报工具包。Prometheus 的核心是一个收集和存储指标的服务器。它还有各种其他可选组件,如一个警报工具(Alertmanager),以及针对诸如 Go 之类的编程语言的客户端库,您可以用它来为您的应用程序添加仪表。
这听起来可能有些复杂,但实际操作中非常简单。您可以使用社区 Helm 图表在您的 Kubernetes 集群中一条命令安装 Prometheus。它将自动从集群中收集指标,还会从您指定的任何应用程序中收集数据,使用一个称为抓取的过程。
Prometheus 通过在预设端口向您的应用程序进行 HTTP 连接并下载可用的指标数据来抓取指标。然后它将数据存储在其数据库中,您可以随时查询、绘制或进行警报。
提示
Prometheus 收集指标的方法称为拉取监控。在这种方案中,监控服务器会联系应用程序并请求指标数据。相反的方法称为推送,被一些其他监控工具如 StatsD 所使用,其工作方式正好相反:应用程序联系监控服务器并发送指标数据。Prometheus 还通过其Pushgateway组件支持推送模型。
就像 Kubernetes 本身一样,Prometheus 也受到 Google 自家基础设施的启发。它是在 SoundCloud 开发的,但是它吸收了来自一个名为 Borgmon 的工具的许多想法。正如其名所示,Borgmon 旨在监控 Google 的 Borg 容器编排系统(见“从 Borg 到 Kubernetes”)。
Kubernetes 直接建立在 Google 十年的自家集群调度系统 Borg 的经验基础上。Prometheus 与 Google 的联系则要松散得多,但它从 Borgmon 中汲取了大量灵感,这是 Google 在大约同一时期提出的内部监控系统。用一个非常粗糙的比较来说,你可以说 Kubernetes 是面向凡人的 Borg,而 Prometheus 是面向凡人的 Borgmon。它们都是“第二代系统”,试图在保留优点的同时避免祖先们的错误和死胡同。
Björn Rabenstein(SoundCloud)
您可以在其网站上了解更多关于 Prometheus 的信息,包括如何在您的环境中安装和配置它的说明。
虽然 Prometheus 本身专注于收集和存储指标的工作,但也有其他高质量的开源选项用于绘图、仪表板和警报。Grafana是一个强大而能干的时序数据绘图引擎(见图 16-8)。
Prometheus 项目包括一个名为Alertmanager的工具,它与 Prometheus 配合良好,但也可以独立运行。Alertmanager 的工作是接收来自多个来源(包括 Prometheus 服务器)的警报,并对其进行处理(见“基于指标进行警报”)。
处理警报的第一步是去重。然后,Alertmanager 可以将其检测到的警报分组为相关的警报;例如,主要的网络中断可能会导致数百个单独的警报,但 Alertmanager 可以将所有这些警报组合成单个消息,以便响应人员不会被页面所淹没。
最后,Alertmanager 将处理过的警报路由到适当的通知服务,例如 PagerDuty、Slack 或电子邮件。
令人方便的是,Prometheus 指标格式得到了广泛的工具和服务支持,这种事实上的标准现在已成为 OpenMetrics 的基础,OpenMetrics 是一个云原生计算基金会项目,旨在生产度量数据的中立标准格式。许多受欢迎的托管监控工具,如 Amazon CloudWatch、Operations Suite、Datadog 和 New Relic,都可以导入和理解 Prometheus 数据。

图 16-8. 显示 Prometheus 数据的 Grafana 仪表板
Google Operations Suite
Operations Suite 之前被称为 Stackdriver,虽然现在是 Google 的一部分,但并不仅限于 Google Cloud:它也与 AWS 兼容。Cloud Monitoring 组件可以收集、绘制和警报来自各种来源的指标和日志数据。它会自动发现和监视您的云资源,包括虚拟机、数据库和 Kubernetes 集群。Operations Suite 将所有这些数据汇集到一个中央网络控制台,您可以在其中创建自定义仪表板和警报。
Operations Suite 理解如何从诸如 PostgreSQL、NGINX、Cassandra 和 Elasticsearch 等流行软件工具中获取运营指标。如果您想包含自己应用程序的自定义指标,可以使用 Operations Suite 的客户端库导出您想要的任何数据。它还提供了运行托管的 Prometheus 实例的能力,使您可以继续使用现有的 Prometheus 导出器和 Grafana 仪表板。
如果您使用 Google Cloud,Operations Suite 对所有与 GCP 相关的指标免费;对于自定义指标或来自其他云平台的指标,您需要按每月监控数据的兆字节支付费用。
AWS CloudWatch
亚马逊自己的云监控产品 CloudWatch 具有与 Operations Suite 类似的功能集。它与所有 AWS 服务集成,您可以使用 CloudWatch SDK 或命令行工具导出自定义指标。
CloudWatch 提供了免费套餐,允许您以五分钟的间隔收集基本指标(例如 VM 的 CPU 利用率),一定数量的仪表板和警报等等。除此之外,您需要按指标、仪表板或警报每月支付费用,还可以按实例支付高分辨率指标(每分钟间隔)的费用。
CloudWatch 是基础但有效的。如果您的主要云基础设施是 AWS,CloudWatch 是开始使用指标的良好选择,并且对于小型部署可能是您所需要的全部。
Azure Monitor
Azure Monitor 是微软的等同于 GCP 的 Operations Suite 或 AWS 的 CloudWatch 的产品。它收集来自所有 Azure 资源(包括 Kubernetes 集群)的日志和指标数据,并允许您对其进行可视化和警报。它还提供基于 Prometheus 的指标抓取器,这样您就不需要在应用程序中使用其他工具进行仪表化,如果您已经配置了 Prometheus 的话。
Datadog
与像 Operations Suite 和 CloudWatch 这样的云提供商内置工具相比,Datadog 是一个非常复杂和强大的监控和分析平台。它为超过 250 个平台和服务提供集成,包括所有主要提供商的云服务以及流行的软件,如 Jenkins、NGINX、Consul、PostgreSQL 和 MySQL。
Datadog 还提供应用程序性能监控(APM)组件,以及一个日志聚合产品,旨在帮助您监视和分析应用程序的性能。无论您使用 Go、Java、Ruby 还是任何其他软件平台,Datadog 都可以从您的软件中收集指标、日志和跟踪数据,并为您解答以下问题:
-
我的服务的特定个体用户的用户体验如何?
-
在特定端点上,哪些 10 个客户端看到了最慢的响应?
-
我的各种分布式服务中哪些正在导致请求的总体延迟?
除了通常的仪表化(见图 16-9)和警报功能(可通过 Datadog API 和客户端库自动化,包括 Terraform),Datadog 还提供由机器学习驱动的异常检测功能,它还支持从您的应用程序收集 Prometheus 指标。

图 16-9. Datadog 仪表板
New Relic
New Relic 是一个非常成熟和广泛使用的指标平台,专注于应用程序性能监控(APM)。它的主要优势在于诊断应用程序和分布式系统内部的性能问题和瓶颈(见图 16-10)。然而,它也提供基础设施指标和监控、警报、软件分析等所有您所期望的功能。
如果您正在寻找高级企业指标平台,您可能会看向 New Relic(稍微更加关注应用程序)或 Datadog(稍微更加关注基础设施)。两者都提供良好的基础设施即代码支持;例如,您可以使用官方 Terraform 提供程序为 New Relic 和 Datadog 创建监控仪表板和警报。

图 16-10. New Relic APM 仪表板
摘要
许多工程师喜欢的一句话是“量一次,砍两次”。在云原生世界中,如果没有适当的指标和可观测性数据,很难知道发生了什么。另一方面,一旦打开了指标的闸门,信息过载可能和信息不足一样无用。
关键是首先收集正确的数据,以正确的方式处理它,用它来回答正确的问题,在正确的方式下可视化它,并在正确的时间用它向正确的人员发出关于正确事项的警报。
如果你在这一章中忘记了其他所有内容,请记住这一点:
-
专注于每个服务的关键指标:请求量,错误率和持续时间(RED)。对于每个资源:利用率,饱和度和错误(USE)。
-
为你的应用程序安装仪表,以公开自定义指标,既用于内部可观测性,也用于业务 KPI。
-
有用的 Kubernetes 指标包括,在集群级别,节点数量,每个节点的 Pod 数量,以及节点资源的使用情况。
-
在部署级别,跟踪部署和副本,特别是不可用的副本,这可能表明存在容量问题。
-
在容器级别,跟踪每个容器的资源使用情况,活跃/准备状态,重启次数,网络流量和网络错误。
-
为每个服务构建一个仪表板,使用标准布局和主要信息显示器,报告整个系统的重要信息。
-
如果你基于指标发出警报,警报应该是紧急的,重要的,并且可操作的。警报噪音会造成疲劳并损害士气。
-
跟踪和审查团队接收的紧急页面数量,特别是在周末和休息日的唤醒。
-
云原生世界中的事实标准指标解决方案是 Prometheus,并且几乎所有内容都使用 Prometheus 数据格式。
-
流行的第三方托管指标服务包括 Google Operations Suite,Amazon CloudWatch,Datadog 和 New Relic。
^(1) 辛普森悖论的维基百科条目 提供了更多信息。
第十九章:后记
没有什么比引入新事物的领导更难的了,更危险的了,也更不确定其成功的了。
尼科洛·马基雅维利
好了,这真是一段旅程。我们希望这本书为您的 Kubernetes 之旅提供了良好的路线图,并让您对未来在 Kubernetes 上迈出下一步充满信心。
他们说,专家只是手册中比你多一页的人。如果您正在阅读本书,那么您可能是您组织中当前的 Kubernetes 专家,至少目前是如此。我们希望您将继续发现这本书作为经常参考的资源,但它也只是一个起点。
下一步去哪里
您可能会发现以下资源对您有用,无论是更多了解 Kubernetes 和云原生,还是跟上最新新闻和发展:
官方 Kubernetes Slack 组织。这是一个提问和与其他用户交流的好地方。
一个公共论坛,讨论所有关于 Kubernetes 的事务。
由谷歌制作的每周播客。每集通常约 20 分钟,涵盖周报新闻,并通常包括与涉及 Kubernetes 的某人的访谈。
https://github.com/vmware-tanzu/tgik
TGIK8s 是一个每周的直播视频流,最初由乔·贝达发起。通常的格式包括大约一个小时的实时演示 Kubernetes 生态系统中的某些内容。所有之前的视频都已存档,并可随需观看。
https://www.cncf.io/kubeweekly
KubeWeekly 是一个每周发布的电子邮件新闻简报,内容包括关于 Kubernetes 的实用技巧、公告和文章。
这里还有一些其他电子邮件新闻简报可供订阅,涵盖软件开发、安全、DevOps 以及一切与云原生相关的主题:
第二版注记
过去几年对 Kubernetes 的一个普遍观察是它变得“乏味”,而我们认为这是一件非常好的事情。“乏味”在这个背景下也意味着“可靠”和“可预测”。Kubernetes 及其周围的云原生生态系统不断成熟和稳定。这使得软件组织能够更好地规划其未来的基础设施投资,并雇佣那些有使用这些标准工具经验的工程师。
记住,Kubernetes 只是一个工具。它确实是一个令人兴奋的工具,但真正的激动点应该集中在我们可以用 Kubernetes 去做些什么上。我们的目标是构建在世界上产生积极影响并帮助解决人们问题的优秀事物。
欢迎加入
我们从正确中学不到任何东西。
伊丽莎白·比贝斯科
在你的 Kubernetes 学习旅程中,你的首要任务应该是尽可能广泛地传播你的专业知识,并向他人学习尽可能多的东西。我们没有人知道所有事情,但每个人都知道一些事情。一起,我们也许能够解决问题。
不要害怕尝试新事物。创建你自己的演示应用程序,或者借用我们的应用程序,尝试在其中进行可能在生产环境中需要的操作。如果你做的每件事情都完美无缺,那么你的尝试还不够。真正的学习来自于失败,来自于尝试弄清楚问题并加以修复。你失败得越多,学到的就越多。
至于我们对 Kubernetes 的了解,大部分都是因为我们失败了很多次。我们希望你也一样。玩得开心!


浙公网安备 33010602011771号