原文:
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 中展示的主机计算机的三倍 —— 对计算资源的共享、按使用量付费的访问却是一个非常古老的概念。现在我们称之为云,以及从分时共享主机开始的革命已经走了一整圈。
https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cldntv-dop-k8s/img/cnd2_0101.png
图 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 通过使用这个系统更便宜地运输货物而变得非常成功,而且集装箱迅速流行开来。今天,每年运输数亿个集装箱,携带价值数万亿美元的货物。
https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cldntv-dop-k8s/img/cnd2_0102.png
图 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)。
https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cldntv-dop-k8s/img/cndk_0301.png
图 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)。一些企业运行着私有云,由本地硬件托管的虚拟机组成。
https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cldntv-dop-k8s/img/cnd2_0302.png
图 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。
https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cldntv-dop-k8s/img/cndk_0401.png
图 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
部分描述。
https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cldntv-dop-k8s/img/cndk_0402.png
图 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 仍然可用。
https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cldntv-dop-k8s/img/cndk_0501.png
图 5-1. 服务 A 和 B 在可用节点上保持平衡
到目前为止,一切都很好。但假设节点 2 确实发生故障。调度器会注意到服务 A 和 B 都需要一个额外的副本,而现在只有一个节点可以为它们创建,因此它就这么做了。现在节点 1 上运行着两个服务 A 的副本和两个服务 B 的副本。
现在假设我们启动一个新节点来取代故障的节点 2。即使它可用,上面也不会有任何 Pod。调度器从不将正在运行的 Pod 从一个节点移动到另一个节点。
现在我们有一个不平衡的集群,所有的 Pod 都在节点 1 上,而节点 2 上没有任何 Pod(图 5-2)。
https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cldntv-dop-k8s/img/cndk_0502.png
图 5-2. 节点 2 故障后,所有副本都迁移到节点 1
更糟糕的是,假设你对服务 A 部署了一个滚动更新(我们称新版本为服务 A*)。调度器需要为服务 A* 启动两个新的副本,等待它们启动完成,然后终止旧的副本。新的副本将在哪里启动?在新的节点 2 上,因为它是空闲的,而节点 1 已经运行了四个 Pod。因此,两个新的服务 A* 的副本在节点 2 上启动,而旧的副本从节点 1 上移除(图 5-3)。
https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cldntv-dop-k8s/img/cndk_0503.png
图 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
垃圾回收设置来自定义此行为。