精通-Kubernetes-第四版-全-
精通 Kubernetes 第四版(全)
原文:
annas-archive.org/md5/ce6833f2a032d04dc931d1b1413cdf7a译者:飞龙
序言
Kubernetes 是一个开源系统,用于自动化容器化应用程序的部署、扩展和管理。如果你运行的不仅仅是几个容器,或者希望自动化管理你的容器,那么你就需要 Kubernetes。本书专注于引导你掌握 Kubernetes 集群的高级管理。
本书从解释 Kubernetes 架构的基础开始,详细介绍 Kubernetes 的设计。你将发现如何在 Kubernetes 上运行复杂的有状态微服务,包括横向扩展、滚动更新、资源配额和持久化存储后端等高级功能。通过真实的用例,你将探索网络配置的选项,并了解如何设置、操作、保护和排除 Kubernetes 集群的故障。最后,你将学习高级主题,如自定义资源、API 聚合、服务网格和无服务器计算。所有内容都与 Kubernetes 1.26 版本兼容。读完本书,你将掌握从中级到高级所需的所有知识。
本书的读者对象
本书面向那些具有中级 Kubernetes 知识的系统管理员和开发人员,并希望掌握高级功能的读者。你还应该具备基本的网络知识。本书是一本高级书籍,提供了一条通往掌握 Kubernetes 的道路。
本书内容
第一章,理解 Kubernetes 架构,在这一章中,我们将一起构建充分利用 Kubernetes 的必要基础。我们将首先理解什么是 Kubernetes,什么不是 Kubernetes,以及容器编排到底意味着什么。然后我们将介绍一些重要的 Kubernetes 概念,这些概念将构成我们在整本书中使用的术语。
第二章,创建 Kubernetes 集群,在这一章中,我们将卷起袖子,使用 minikube、KinD 和 k3d 构建一些 Kubernetes 集群。我们将讨论并评估其他工具,如 kubeadm 和 Kubespray。我们还将探索本地、云端和裸金属等不同的部署环境。
第三章,高可用性与可靠性,在这一章中,我们将深入探讨高可用集群的话题。这是一个复杂的话题。Kubernetes 项目和社区尚未就实现高可用性 nirvana(完美状态)达成一致的解决方案。
高可用 Kubernetes 集群涉及多个方面,如确保控制平面能够在故障发生时继续运行,保护 etcd 中的集群状态,保护系统数据,以及快速恢复容量和/或性能。不同的系统会有不同的可靠性和可用性要求。
第四章,确保 Kubernetes 的安全性,在本章中,我们将探讨安全性这一重要话题。Kubernetes 集群是由多个相互作用的组件层组成的复杂系统。在运行关键应用程序时,不同层次的隔离和分区非常重要。为了确保系统安全并确保对资源、能力和数据的适当访问,我们必须首先理解 Kubernetes 作为一个通用编排平台面临的独特挑战,特别是它运行的是未知的工作负载。然后,我们可以利用各种安全性、隔离和访问控制机制,确保集群、运行其上的应用程序和数据的安全性。我们将讨论各种最佳实践以及何时使用每种机制。
第五章,在实践中使用 Kubernetes 资源,在本章中,我们将设计一个虚构的大规模平台,挑战 Kubernetes 的能力和可扩展性。Hue 平台的目标是创建一个无所不知、无所不能的数字助手。Hue 是你的数字延伸,Hue 将帮助你做任何事情,找到任何东西,并且在许多情况下,会代替你做很多事情。显然,它需要存储大量信息,集成许多外部服务,响应通知和事件,并且在与你互动时表现得非常智能。
第六章,管理存储,在本章中,我们将探讨 Kubernetes 如何管理存储。存储与计算有很大的不同,但从高层次来看,它们都是资源。作为一个通用平台,Kubernetes 采取通过编程模型和存储提供商插件将存储抽象化的方式。
第七章,在 Kubernetes 上运行有状态应用程序,在本章中,我们将学习如何在 Kubernetes 上运行有状态应用程序。Kubernetes 通过根据复杂的需求和配置(如命名空间、限制和配额)自动启动和重启集群节点上的 pod,减轻了我们的工作负担。但当 pod 运行需要存储意识的软件,如数据库和队列时,迁移 pod 可能会导致系统崩溃。
第八章,部署和更新应用程序,在本章中,我们将探讨 Kubernetes 提供的自动化 pod 可扩展性,它如何影响滚动更新,以及如何与配额进行交互。我们将涉及一个重要的话题——资源配置,以及如何选择和管理集群的大小。最后,我们将介绍 Kubernetes 团队如何提高 Kubernetes 的性能,以及他们如何使用 Kubemark 工具测试 Kubernetes 的极限。
第九章,打包应用程序,在这一章中,我们将深入研究 Helm,Kubernetes 的包管理器。每个平台,尤其是那些成功且非平凡的平台,都必须有一个良好的打包系统。Helm 是由 Deis 开发的(2017 年 4 月 4 日被微软收购),并后来直接贡献给 Kubernetes 项目。它于 2018 年成为 CNCF 项目。我们将从理解 Helm 的动机、架构和组件开始。
第十章,探索 Kubernetes 网络,在这一章中,我们将研究网络这一重要话题。Kubernetes 作为一个编排平台,管理运行在不同机器(物理或虚拟)上的容器/Pod,并要求有一个明确的网络模型。
第十一章,在多个集群上运行 Kubernetes,在这一章中,我们将把它提升到一个新的层次,即在多个云和集群上运行 Kubernetes。Kubernetes 集群是一个紧密结合的单元,所有组件都在相对接近的地方运行,并通过快速网络连接(通常是物理数据中心或云服务提供商的可用区)。这对许多用例来说很有用,但也有一些重要的用例,系统需要超越单一集群进行扩展。
第十二章,Kubernetes 上的无服务器计算,在这一章中,我们将探索云中无服务器计算的迷人世界。术语“无服务器”受到了很多关注,但它是一个用来描述两种不同范式的误称。真正的无服务器应用程序在用户的浏览器或移动应用中运行,仅与外部服务交互。而我们在 Kubernetes 上构建的无服务器系统则不同。
第十三章,监控 Kubernetes 集群,在这一章中,我们将讨论如何确保系统正常运行并执行得当,以及在系统出现问题时如何响应。在第三章,高可用性和可靠性中,我们讨论了相关的主题。本章的重点是了解系统中发生的事情,以及可以使用哪些实践和工具。
第十四章,利用服务网格,在这一章中,我们将学习服务网格如何使您能够将监控和可观察性等跨切关注点从应用代码中外部化。服务网格是您在 Kubernetes 上设计、演进和操作分布式系统方式的真正范式转变。我喜欢将它看作是云原生分布式系统的面向方面编程。
第十五章,扩展 Kubernetes,在这一章中,我们将深入探讨 Kubernetes 的内部结构。我们将从 Kubernetes API 开始,学习如何通过直接访问 API、Go 客户端和自动化 kubectl 来进行编程式地与 Kubernetes 交互。接下来,我们将了解如何通过自定义资源扩展 Kubernetes API。最后一部分将重点讨论 Kubernetes 支持的各种插件。Kubernetes 的许多操作方面是模块化的,且设计为可扩展的。我们将检查 API 聚合层以及几种类型的插件,例如自定义调度器、授权、准入控制、自定义度量和存储卷。最后,我们将探讨如何扩展 kubectl 并添加自定义命令。
第十六章,Kubernetes 的治理,在这一章中,我们将了解 Kubernetes 在大型企业组织中的日益重要的角色,以及什么是治理,治理在 Kubernetes 中的应用。我们将探讨策略引擎,回顾一些流行的策略引擎,然后深入了解 Kyverno。
第十七章,在生产环境中运行 Kubernetes,在这一章中,我们将把注意力集中在 Kubernetes 在生产环境中的整体管理上。重点将放在如何在云中运行多个托管的 Kubernetes 集群。运行 Kubernetes 的生产环境有很多不同的方面,我们将主要关注计算方面,这是最基础和最重要的部分。
第十八章,Kubernetes 的未来,在这一章中,我们将从多个角度探讨 Kubernetes 的未来。我们将首先从 Kubernetes 自诞生以来的势头出发,涵盖社区、生态系统和思想领导力等维度。剧透:Kubernetes 凭借压倒性的优势赢得了容器编排大战。随着 Kubernetes 的成长和成熟,竞争的焦点不再是击败对手,而是与其自身的复杂性作斗争。可用性、工具链和教育将在容器编排这一仍然新兴、快速发展的、且尚未被充分理解的领域中发挥重要作用。接下来,我们将探讨一些非常有趣的模式和趋势。
最大化利用本书
为了跟随每一章的示例,你需要在机器上安装一个最新版本的 Docker 和 Kubernetes,理想情况下是 Kubernetes 1.18。如果你的操作系统是 Windows 10 专业版,你可以启用虚拟化模式;否则,你需要安装 VirtualBox 并使用 Linux 客户端操作系统。如果你使用 macOS,那么一切就绪。
下载示例代码文件
本书的代码包托管在 GitHub 上,网址为github.com/PacktPublishing/Mastering-Kubernetes-4th-Edition。我们还有其他代码包,来自我们丰富的书籍和视频目录,网址为github.com/PacktPublishing/。快来查看吧!
下载彩色图片
我们还提供了一个 PDF 文件,里面包含了本书使用的截图/图表的彩色图片。你可以在这里下载: packt.link/gXMql。
本书中使用的约定
本书中使用了多种文本约定。
CodeInText:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名。例如:“如果你选择了 HyperKit 而不是 VirtualBox,你需要在启动集群时添加标志--vm-driver=hyperkit。”
一段代码如下所示:
apiVersion: "etcd.database.coreos.com/v1beta2"
kind: "EtcdCluster"
metadata:
name: "example-etcd-cluster"
spec:
size: 3
version: "3.2.13"
当我们希望引起你注意某个代码块的特定部分时,相关的行或项目会被高亮显示:
apiVersion: "etcd.database.coreos.com/v1beta2"
kind: "EtcdCluster"
metadata:
**name:****"example-etcd-cluster"**
spec:
size: 3
version: "3.2.13"
任何命令行输入或输出都如下所示:
$ k get pods
NAME READY STATUS RESTARTS AGE
echo-855975f9c-r6kj8 1/1 Running 0 2m11s
粗体:表示一个新术语、一个重要词或在屏幕上看到的词。例如,菜单或对话框中的词会像这样出现在文本中。例如:“从管理面板中选择系统信息。”
警告或重要注意事项如下所示。
提示和技巧如下所示。
联系我们
我们始终欢迎读者的反馈。
一般反馈:发送电子邮件至 feedback@packtpub.com 并在邮件主题中注明书名。如果你对本书的任何内容有疑问,请通过 questions@packtpub.com 与我们联系。
勘误:虽然我们已尽力确保内容的准确性,但错误难免。如果你在本书中发现错误,请帮助我们报告。请访问 www.packtpub.com/submit-errata,点击提交勘误,并填写表单。
盗版:如果你在网上发现任何非法版本的我们的作品,感谢你提供该地址或网站名称。请通过 copyright@packtpub.com 联系我们,并附上相关资料的链接。
如果你有兴趣成为作者:如果你在某个领域拥有专长并且有兴趣写书或参与书籍编写,请访问 authors.packtpub.com。
分享你的想法
阅读完《掌握 Kubernetes 第四版》后,我们非常想听听你的想法!请 点击这里直接前往 Amazon 评价页面并分享你的反馈。
你的评论对我们和技术社区来说非常重要,它将帮助我们确保提供高质量的内容。
免费下载本书的 PDF 副本
感谢你购买本书!
你喜欢在路上阅读但又无法随身携带纸质书籍吗?你的电子书购买是否与你选择的设备不兼容?
别担心,现在每本 Packt 书籍都会免费提供该书的无 DRM PDF 版本。
随时随地,在任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制并粘贴代码到您的应用程序中。
福利不仅仅是这些,您还可以获得独家折扣、新闻简报,并每天在邮箱中收到精彩的免费内容
按照这些简单步骤获取福利:
- 扫描二维码或访问以下链接

packt.link/free-ebook/9781804611395
-
提交您的购买凭证
-
就这样!我们会将您的免费 PDF 及其他福利直接发送到您的邮箱
第一章:理解 Kubernetes 架构
用一句话来说,Kubernetes 是一个平台,用于编排容器化应用程序的部署、扩展和管理。你可能已经读过 Kubernetes 的相关内容,甚至可能在某个副项目或工作中使用过它。但要真正理解 Kubernetes 是什么,如何有效使用它,以及最佳实践是什么,远远不止这些。
Kubernetes 是一个庞大的开源项目和生态系统,包含了大量的代码和功能。Kubernetes 起源于 Google,但加入了 云原生计算基金会 (CNCF),现在成为了基于容器的应用程序领域的事实标准。根据 2021 年 CNCF 调查,96% 的组织在使用或评估 Kubernetes。
在本章中,我们将打下充分利用 Kubernetes 潜力所需的基础。我们将从理解什么是 Kubernetes,什么不是 Kubernetes,以及容器编排究竟意味着什么开始。然后,我们将介绍 Kubernetes 的重要概念,这些概念将构成我们贯穿全书的词汇。接下来,我们将深入探讨 Kubernetes 的架构,看看它是如何为用户提供各种功能的。之后,我们将讨论 Kubernetes 如何以通用的方式支持多种容器运行时。
我们将讨论的主题包括:
-
什么是 Kubernetes?
-
Kubernetes 不是
-
理解容器编排
-
Kubernetes 概念
-
深入研究 Kubernetes 架构
-
Kubernetes 容器运行时
在本章结束时,你将对容器编排、Kubernetes 解决的问题、Kubernetes 设计和架构的基本理念,以及它支持的不同运行时引擎有一个扎实的理解。
什么是 Kubernetes?
Kubernetes 是一个涵盖了大量服务和功能的庞大平台,且这些服务和功能还在不断增长。其核心功能是调度容器中的工作负载到你的基础设施中,但它不仅仅停留在这一点。以下是 Kubernetes 带来的其他一些功能:
-
提供身份验证和授权
-
调试应用程序
-
访问和获取日志
-
滚动更新
-
使用集群自动扩展
-
使用水平 Pod 自动扩展器
-
复制应用程序实例
-
检查应用程序健康状况和就绪性
-
监控资源
-
负载均衡
-
命名和服务发现
-
分发秘密
-
挂载存储系统
我们将在本书中详细介绍所有这些功能。此时,只需吸收并欣赏 Kubernetes 为你的系统带来的巨大价值。
Kubernetes 拥有广泛的功能,但同样重要的是要理解 Kubernetes 明确不提供的内容。
Kubernetes 不是
Kubernetes 不是 平台即服务 (PaaS)。它不强制规定很多重要方面,这些方面由你或在 Kubernetes 之上构建的其他系统(例如 OpenShift 和 Tanzu)来处理。例如:
-
Kubernetes 不要求特定的应用类型或框架
-
Kubernetes 不要求特定的编程语言
-
Kubernetes 不提供数据库或消息队列
-
Kubernetes 不区分应用和服务
-
Kubernetes 没有点击部署的服务市场
-
Kubernetes 不提供内建的函数即服务解决方案
-
Kubernetes 不强制要求日志、监控和告警系统
-
Kubernetes 不提供 CI/CD 管道
理解容器编排
Kubernetes 的主要责任是容器编排。那意味着确保所有执行各种工作负载的容器都能在物理或虚拟机上被调度运行。容器必须根据部署环境和集群配置的约束进行高效打包。此外,Kubernetes 还必须时刻监控所有运行的容器,并替换死掉的、无响应的或其他不健康的容器。Kubernetes 提供了许多更多的功能,你将在接下来的章节中学习到。在这一节中,重点是容器及其编排。
物理机器、虚拟机器和容器
一切从硬件开始并以硬件结束。为了运行工作负载,你需要一些真实的硬件资源。这包括实际的物理机器,具备一定计算能力(CPU 或核心)、内存以及一些本地持久存储(旋转磁盘或 SSD)。此外,你还需要一些共享的持久存储,并通过网络将所有这些机器连接起来,使它们能够互相发现并通信。此时,你可以在物理机器上运行多个虚拟机,或者停留在裸金属层级(不使用虚拟机)。Kubernetes 可以部署在裸金属集群(真实硬件)上,也可以部署在虚拟机集群上。Kubernetes 反过来可以直接在裸金属或虚拟机上编排它所管理的容器。理论上,Kubernetes 集群可以由裸金属和虚拟机的混合组成,但这并不常见。还有许多更为深奥的配置,具有不同级别的封装,例如运行在另一个 Kubernetes 集群的命名空间中的虚拟 Kubernetes 集群。
容器的优势
容器代表了大型复杂软件系统开发和运维的真正范式转变。与传统模型相比,以下是容器的一些优点:
-
敏捷应用创建和部署
-
持续开发、集成和部署
-
开发与运维的职责分离
-
开发、测试、预生产和生产环境的一致性
-
云和操作系统的分发可移植性
-
以应用为中心的管理
-
资源隔离
-
资源利用率
云中的容器
微服务是现代大规模系统的主流架构。其核心思想是将系统拆分成小的服务,每个服务有明确的责任,管理自己的数据,并通过明确的 API 与其他微服务进行通信。
容器是打包微服务的理想选择,因为它们不仅提供了微服务的隔离,而且非常轻量化,在部署多个微服务时,你不会像虚拟机那样增加过多的开销。这使得容器非常适合云部署,因为为每个微服务分配一台完整的虚拟机会导致成本过高。
如今,所有主要的云服务提供商,如 Amazon AWS、谷歌的 GCE 和微软的 Azure,都提供容器托管服务。许多其他公司也加入了 Kubernetes 的阵营,提供托管的 Kubernetes 服务,包括 IBM IKS、阿里云、DigitalOcean DKS、Oracle OKS、OVH 托管 Kubernetes 和 Rackspace KaaS。
谷歌的 GKE 始终基于 Kubernetes。AWS 的弹性 Kubernetes 服务(EKS)是在原有的 AWS ECS 编排解决方案基础上增加的。微软 Azure 的容器服务曾经基于 Apache Mesos,但后来转向 Kubernetes,推出了Azure Kubernetes 服务(AKS)。你始终可以在所有云平台上部署 Kubernetes,但它并未与其他服务深度集成。然而,在 2017 年底,所有云服务提供商都宣布了对 Kubernetes 的直接支持。微软推出了 AKS,AWS 发布了 EKS。此外,还有各种其他公司提供托管的 Kubernetes 服务,如 IBM、Oracle、Digital Ocean、阿里巴巴、腾讯和华为。
牲畜与宠物
在过去,当系统还很小的时候,每台服务器都有一个名字。开发人员和用户清楚地知道每台机器上运行的软件是什么。我记得,在我工作的许多公司中,我们曾经花费几天时间讨论如何为我们的服务器命名。例如,作曲家和希腊神话中的人物是常见的命名主题。那时一切都非常温馨。你像对待心爱的宠物一样对待你的服务器。当一台服务器“死掉”时,那可是个大危机。每个人都急忙去找另一台服务器,搞清楚死掉的服务器上运行了什么,如何让它在新服务器上重新工作。如果服务器存储了一些重要数据,那么希望你有最新的备份,也许你还能够恢复它。
显然,这种方法是无法扩展的。当你拥有数十或数百台服务器时,你必须开始像对待牲畜一样对待它们。你考虑的是整体,而不是个体。你可能仍然有一些“宠物”,比如你的 CI/CD 机器(尽管托管的 CI/CD 解决方案变得越来越普遍),但你的 Web 服务器和后端服务就只是牲畜。
Kubernetes 将“养牛”模式发挥到极致,并且完全负责将容器分配到特定的机器上。大多数时候,你无需与单独的机器(节点)交互。这种方式最适合无状态工作负载。对于有状态的应用,情况稍有不同,但 Kubernetes 提供了一种名为 StatefulSet 的解决方案,我们稍后将讨论。
在本节中,我们介绍了容器编排的概念,并讨论了主机(物理或虚拟)与容器之间的关系,以及在云中运行容器的好处。最后,我们讨论了“养牛”与“宠物”的区别。在接下来的章节中,我们将进一步了解 Kubernetes 的世界,并学习它的概念和术语。
Kubernetes 概念
在本节中,我们将简要介绍许多重要的 Kubernetes 概念,并为你提供一些背景信息,解释它们为何需要以及它们如何相互作用。目标是让你熟悉这些术语和概念。稍后,我们将看到这些概念如何被组织到 API 组和资源类别中,以实现卓越的功能。你可以将这些概念视为构建块。一些概念,例如节点和控制平面,作为一组 Kubernetes 组件来实现。这些组件处于不同的抽象层次,我们将在专门的章节 Kubernetes 组件 中详细讨论它们。
这是 Kubernetes 架构图:

图 1.1:Kubernetes 架构
节点
节点是一个单独的主机。它可以是物理机或虚拟机。它的工作是运行 pods。每个 Kubernetes 节点都运行多个 Kubernetes 组件,如 kubelet、容器运行时和 kube-proxy。节点由 Kubernetes 控制平面管理。节点是 Kubernetes 的“工蜂”,肩负着所有繁重的工作。过去它们被称为 minions。如果你阅读一些旧文档或文章,不要感到困惑,minions 就是节点。
集群
集群是由一组主机(节点)组成,它们提供计算、内存、存储和网络资源。Kubernetes 使用这些资源来运行构成系统的各种工作负载。请注意,整个系统可能由多个集群组成。稍后我们将详细讨论这一多集群系统的高级用例。
控制平面
Kubernetes 的控制平面包括多个组件,如 API 服务器、调度器、控制器管理器和可选的云控制器管理器。控制平面负责集群的全局状态、Pod 的集群级调度和事件处理。通常,所有控制平面组件都设置在同一台主机上,尽管这并非必需。在考虑高可用性场景或非常大的集群时,您可能希望具备控制平面的冗余性。我们将在第三章“高可用性和可靠性”中详细讨论高可用性集群。
Pod
Pod 是 Kubernetes 中的工作单位。每个 Pod 包含一个或多个容器(可以将其视为一个容器容器)。Pod 被调度为一个原子单位(所有容器运行在同一台机器上)。Pod 中的所有容器共享相同的 IP 地址和端口空间;它们可以使用 localhost 或标准的进程间通信进行通信。此外,Pod 中的所有容器可以访问托管 Pod 的节点上的共享本地存储。容器默认不会访问本地存储或任何其他存储,必须显式地将存储卷挂载到 Pod 内的每个容器中。
Pods 是 Kubernetes 的一个重要特性。通过像 supervisord 这样的主进程可以在单个容器内运行多个应用程序,但这种做法通常不被推荐,原因如下:
-
透明性:使得 Pod 内的容器对基础设施可见,使得基础设施能够为这些容器提供服务,如进程管理和资源监控。这为用户提供了许多便利。
-
解耦软件依赖关系:各个容器可以独立进行版本控制、重建和重新部署。Kubernetes 可能将来甚至支持对单个容器的实时更新。
-
易用性:用户无需运行自己的进程管理器,不必担心信号和退出码的传播等问题。
-
效率:由于基础设施承担了更多责任,容器可以更加轻量化。
Pods 提供了管理紧密相关的容器组的出色解决方案,这些容器相互依赖并需要在同一主机上协作以完成其目的。重要的是要记住,Pod 被视为临时的、可丢弃的实体,可以随意丢弃和替换。每个 Pod 都有一个唯一 ID(UID),因此如果有必要,仍然可以区分它们。
Label
标签是键值对,用于通过选择器将一组对象组合在一起,通常是 pods。这对于其他多个概念非常重要,例如副本集、部署和服务,这些概念处理动态对象组并需要识别组的成员。对象与标签之间存在一个 NxN 的关系。每个对象可以有多个标签,每个标签也可以应用于不同的对象。
标签在设计上有一定的限制。每个对象上的标签必须具有唯一的键。标签键必须符合严格的语法要求。请注意,标签专门用于标识对象,而不是用来附加任意的元数据到对象上。注解就是用于这个目的(见注解部分)。
标签选择器
标签选择器用于根据标签选择对象。基于相等性的选择器指定键名和值。对于值的相等或不等,有两个运算符:=(或==)和!=。例如:
role = webserver
这将选择所有具有该标签键和值的对象。
标签选择器可以有多个用逗号分隔的要求。例如:
role = webserver, application != foo
基于集合的选择器扩展了功能,允许基于多个值进行选择:
role in (webserver, backend)
注解
注解允许你将任意的元数据与 Kubernetes 对象关联。Kubernetes 只会存储注解并使它们的元数据可用。注解键的语法与标签键有类似的要求。
根据我的经验,对于复杂的系统,你总是需要这样的元数据,而 Kubernetes 能够识别这种需求并且开箱即用,提供这种元数据,这样你就不必自己创建单独的元数据存储和映射对象。
服务
服务用于将某些功能暴露给用户或其他服务。它们通常包含一组 pods,通常通过标签来标识。你可以拥有提供对外部资源访问的服务,或者直接在虚拟 IP 层控制的 pods。原生的 Kubernetes 服务通过便捷的端点暴露。注意,服务在第 3 层(TCP/UDP)上运行。Kubernetes 1.2 添加了 Ingress 对象,它提供对 HTTP 对象的访问——稍后我们会详细讲解。服务通过两种机制之一进行发布或发现:DNS 或环境变量。服务可以通过 Kubernetes 在集群内进行负载均衡。但开发人员可以选择自己管理负载均衡,尤其是对于使用外部资源或需要特别处理的服务。
IP 地址、虚拟 IP 地址和端口空间涉及许多复杂的细节。我们将在第十章《探索 Kubernetes 网络》中深入讨论它们。
卷
Pod 使用的本地存储是临时性的,在大多数情况下随着 Pod 一起消失。有时这正是你需要的,如果目标仅仅是让节点中的容器之间交换数据,但有时也很重要数据能存活超过 Pod,或者需要在 Pod 之间共享数据。卷的概念正是为此而生。卷的本质是一个包含某些数据的目录,它被挂载到容器中。
有多种卷类型。最初,Kubernetes 直接支持多种卷类型,但现代扩展 Kubernetes 卷类型的方法是通过 容器存储接口(CSI),我们将在第六章,管理存储中详细讨论。大多数原本内置的卷类型将被淘汰(或正在淘汰中),并由通过 CSI 提供的外部插件取代。
副本控制器与副本集
副本控制器和副本集都管理通过标签选择器标识的一组 Pod,并确保始终有指定数量的 Pod 处于运行状态。它们之间的主要区别在于,副本控制器通过名称匹配来测试成员资格,而副本集可以使用基于集合的选择。副本集是首选,因为它是副本控制器的超集。我预计副本控制器在某个时候会被弃用。Kubernetes 保证无论何时你在副本控制器或副本集中指定了要运行的 Pod 数量,它都能保持一致。如果由于宿主节点或 Pod 本身的问题,数量下降,Kubernetes 会启动新的实例。请注意,如果你手动启动了 Pod 并超过了指定数量,副本集控制器会终止一些多余的 Pod。
副本控制器曾经是许多工作流的核心,例如滚动更新和运行一次性任务。随着 Kubernetes 的发展,它引入了许多这些工作流的直接支持,提供了专门的对象,如 Deployment、Job、CronJob 和 DaemonSet。稍后我们会详细介绍它们。
StatefulSet
Pod 会不断地创建和销毁,如果你关心它们的数据,可以使用持久化存储。这很好。但有时你希望 Kubernetes 管理分布式数据存储,如 Cassandra 或 CockroachDB。这些集群存储会将数据分布到唯一标识的节点上。你不能通过普通的 Pod 和服务来建模这一点。这时就引入了 StatefulSet。如果你还记得之前我们提到过“宠物与牲畜”的比喻,以及为什么“牲畜”是更好的选择。那么,StatefulSet 就位于两者之间。StatefulSet 确保(类似于 ReplicaSet)任何时候都有给定数量的实例并且它们具有唯一的身份。StatefulSet 的成员具有以下特点:
-
稳定的主机名,可在 DNS 中使用
-
一个有序索引
-
稳定存储与有序索引和主机名相关联
-
成员按顺序被优雅地创建和终止
StatefulSet 可以帮助进行节点发现,并且能够安全地添加或删除成员。
Secret
Secrets 是包含敏感信息的小对象,例如凭证和令牌。默认情况下,它们以明文形式存储在 etcd 中,通过 Kubernetes API 服务器访问,并可以作为文件挂载到需要访问它们的 Pod 中(使用专用的 secret 卷,这些卷依赖于常规数据卷)。同一个 secret 可以挂载到多个 Pod 中。Kubernetes 本身为其组件创建 secrets,你也可以创建自己的 secrets。另一种方法是将 secrets 作为环境变量使用。请注意,Pod 中的 secrets 始终存储在内存中(挂载的 secret 的情况下是 tmpfs),以提高安全性。最佳实践是启用静态加密以及使用 RBAC 进行访问控制。我们将在后续章节详细讨论这一点。
名称
Kubernetes 中的每个对象都有一个 UID 和一个名称。名称用于在 API 调用中引用对象。名称应最多包含 253 个字符,并且只能使用小写字母数字字符、连字符 (-) 和点 (.)。如果删除一个对象,可以使用相同的名称创建另一个对象,但 UID 必须在整个集群生命周期内唯一。UID 是由 Kubernetes 生成的,因此你无需担心它。
命名空间
命名空间是一种隔离形式,允许你将资源进行分组并应用策略。它也是名称的作用域。相同类型的对象在一个命名空间内必须有唯一的名称。默认情况下,一个命名空间中的 Pod 可以访问其他命名空间中的 Pod 和服务。
请注意,也有一些集群范围的对象,如节点对象和持久卷,它们不属于任何命名空间。Kubernetes 可以将来自不同命名空间的 Pod 调度到同一节点上运行。同样,来自不同命名空间的 Pod 也可以使用相同的持久存储。
在多租户场景中,当需要完全隔离命名空间时,通过适当的网络策略和资源配额可以在一定程度上实现这一目标,以确保正确的访问控制和物理集群资源的分配。然而,通常来说,命名空间被认为是一种较弱的隔离形式,针对严格的多租户场景,虚拟集群等解决方案更加适用,后者将在第四章,Kubernetes 安全性中讨论。
我们已经涵盖了 Kubernetes 的大多数主要概念,还有一些我简单提到过。在接下来的章节中,我们将继续深入探讨 Kubernetes 架构,了解其设计动机、内部机制和实现,甚至看看源代码。
深入了解 Kubernetes 架构
Kubernetes 有非常宏大的目标。它旨在管理和简化跨多个环境和云服务提供商的分布式系统的编排、部署和管理。它提供了许多功能和服务,这些功能和服务应该能够在所有这些多样化的环境和使用案例中工作,同时在发展中保持足够简单,让普通人也能使用。这是一个艰巨的任务。Kubernetes 通过遵循清晰明确的高层设计和深思熟虑的架构来实现这一点,这种架构促进了可扩展性和插件化。
Kubernetes 最初有许多硬编码或环境感知的组件,但现在的趋势是将它们重构为插件,并保持核心小巧、通用和抽象。
在本节中,我们将像剥洋葱一样逐步剖析 Kubernetes,从各种分布式系统设计模式及其如何得到 Kubernetes 支持开始,然后讲解 Kubernetes 的表面部分,也就是它的 API 集,接着看看构成 Kubernetes 的实际组件。最后,我们将快速浏览源代码树,以便更好地了解 Kubernetes 本身的结构。
在本节结束时,你将对 Kubernetes 的架构和实现有一个扎实的理解,并明白为什么做出某些设计决策。
分布式系统设计模式
所有成功的(正常工作的)分布式系统都是相似的,借用托尔斯泰在《安娜·卡列尼娜》中的话说。也就是说,为了正常运作,所有设计良好的分布式系统必须遵循一些最佳实践和原则。Kubernetes 不仅仅想成为一个管理系统,它还希望支持并促进这些最佳实践,并为开发者和管理员提供高层服务。让我们来看看一些被称为设计模式的内容。我们将从单节点模式开始,比如 sidecar、ambassador 和 adapter,然后再讨论多节点模式。
Sidecar 模式
Sidecar 模式是指在 pod 中与主应用容器共同部署另一个容器。应用容器不会知道 sidecar 容器的存在,只会继续执行自己的工作。一个很好的例子是中央日志代理。主容器可以将日志输出到 stdout,但 sidecar 容器会将所有日志发送到中央日志服务,在那里与整个系统的日志汇总在一起。使用 sidecar 容器而不是将中央日志功能添加到主应用容器中的好处是巨大的。首先,应用程序不再承担中央日志的负担,这可能是一件麻烦事。如果你想升级或更改中央日志策略,或者切换到全新的提供商,你只需更新 sidecar 容器并重新部署即可。你的应用容器不会改变,因此你不会不小心破坏它们。Istio 服务网格使用 sidecar 模式将代理注入到每个 pod 中。
Ambassador 模式
使者模式是将远程服务表现得像本地服务,并可能执行一些策略的模式。使者模式的一个很好的例子是,如果你有一个 Redis 集群,其中一个主节点用于写入,多个副本用于读取。一个本地使者容器可以充当代理,将 Redis 显示给主应用容器,通过 localhost 提供服务。主应用容器只需连接到 localhost:6379(Redis 的默认端口),但它连接到运行在同一 pod 中的使者容器,后者过滤请求,并将写请求发送到真正的 Redis 主节点,将读取请求随机发送到一个读取副本。就像 sidecar 模式一样,主应用根本不知道发生了什么。这在与真实的本地 Redis 集群进行测试时非常有帮助。此外,如果 Redis 集群的配置发生变化,只需要修改使者,主应用仍然保持不知情。
适配器模式
适配器模式的目的是将主应用容器的输出标准化。考虑一个逐步推出的服务:它可能会生成与先前版本不兼容的报告格式。其他使用该输出的服务和应用尚未升级。一个适配器容器可以与新的应用容器一起部署在同一个 pod 中,并将它们的输出调整为符合旧版本,直到所有消费者都升级完成。适配器容器与主应用容器共享文件系统,因此它可以监视本地文件系统,每当新应用写入内容时,它会立即进行适配。
多节点模式
之前描述的单节点模式都是通过在单个节点上调度 pod 直接由 Kubernetes 支持的。多节点模式涉及在多个节点上调度 pod。像领导者选举、工作队列和散点收集这样的多节点模式不被 Kubernetes 直接支持,但通过将 pod 组合成具有标准接口的方式来实现它们是 Kubernetes 中可行的方法。
电平触发的基础设施与协调
Kubernetes 的核心是控制循环。它不断监控自身并纠正问题。基于电平触发的基础设施意味着 Kubernetes 有一个期望状态,并不断朝着该状态努力。例如,如果一个副本集的期望状态是 3 个副本,而它降到 2 个副本,Kubernetes(作为 Kubernetes 的 ReplicaSet 控制器部分)会注意到并努力恢复到 3 个副本。与此不同的边缘触发方法是基于事件的。如果副本数量从 2 个减少到 3 个,则创建一个新的副本。这种方法非常脆弱,并且有许多边界情况,特别是在分布式系统中,像副本进出这样的事件可能同时发生。
在深入了解 Kubernetes 架构后,让我们研究 Kubernetes API。
Kubernetes API
如果你想了解系统的功能以及它能提供什么,你必须非常关注其 API。API 提供了你作为用户可以使用系统的全面视图。Kubernetes 通过 API 组为不同的目的和受众暴露了多组 REST API。一些 API 主要供工具使用,而一些则可以直接供开发者使用。API 的一个重要方面是它们在不断发展。Kubernetes 开发者通过尽量扩展(添加新对象和新字段到现有对象)来保持可管理性,同时避免重命名或删除现有对象和字段。此外,所有 API 端点都有版本标识,并且通常还带有 alpha 或 beta 标注。例如:
/api/v1
/api/v2alpha1
你可以通过 kubectl CLI、客户端库或直接通过 REST API 调用访问 API。我们将在第四章,保护 Kubernetes中探讨详细的认证和授权机制。如果你拥有适当的权限,你可以列出、查看、创建、更新和删除各种 Kubernetes 对象。现在,让我们简单浏览一下 API 的表面区域。
探索 API 的最佳方式是通过 API 组。一些 API 组默认启用,其他组可以通过标志启用/禁用。例如,要禁用autoscaling/v1组并启用autoscaling/v2beta2组,可以在运行 API 服务器时设置--runtime-config标志,方法如下:
--runtime-config=autoscaling/v1=false,autoscaling/v2beta2=true
请注意,云中的托管 Kubernetes 集群不允许你为 API 服务器指定标志(因为它们由云服务提供商管理)。
资源类别
除了 API 组,另一种有用的 API 分类方式是按功能划分。Kubernetes API 非常庞大,将其拆分成不同的类别可以帮助你在探索过程中更加得心应手。Kubernetes 定义了以下资源类别:
-
Workloads:用于在集群上管理和运行容器的对象。
-
发现与负载均衡:用于将工作负载暴露给外界,作为外部可访问的、负载均衡的服务的对象。
-
配置和存储:用于初始化和配置应用程序的对象,以及持久化容器外部数据的对象。
-
Cluster:定义集群自身配置的对象;这些对象通常仅由集群操作员使用。
-
元数据:用于配置集群内其他资源行为的对象,例如用于扩展工作负载的HorizontalPodAutoscaler。
在以下小节中,我们将列出属于每个组的资源及其对应的 API 组。这里不会指定版本,因为 API 会快速从 alpha 到 beta 到GA(通用可用性),从 V1 到 V2 等等。
工作负载资源类别
工作负载类别包含以下资源及其对应的 API 组:
-
容器:核心
-
CronJob:批处理
-
ControllerRevision:应用程序
-
DaemonSet:应用程序
-
Deployment:应用程序
-
HorizontalPodAutoscaler: autoscaling
-
Job: batch
-
Pod: core
-
PodTemplate: core
-
PriorityClass: scheduling.k8s.io
-
ReplicaSet: apps
-
ReplicationController: core
-
StatefulSet: apps
控制器在 Pod 内创建容器。Pod 执行容器,并提供必要的依赖项,例如共享或持久化存储卷,以及注入到容器中的配置或秘密数据。
这是其中一个最常见操作的详细描述,获取所有命名空间中所有 Pods 的列表作为 REST API:
GET /api/v1/pods
它接受各种查询参数(均为可选):
-
fieldSelector: 指定一个选择器,通过其字段来缩小返回对象的范围。默认行为是包括所有对象。 -
labelSelector: 定义一个选择器,通过对象的标签来筛选返回的对象。默认情况下,所有对象都会被包含。 -
limit/continue:limit参数指定在列表调用中返回的最大响应数量。如果有更多的项目可用,服务器会在列表元数据中设置continue字段。这个值可以与初始查询一起使用来获取下一组结果。 -
pretty: 设置为'true'时,输出格式为人类可读的方式。 -
resourceVersion: 设置请求可提供的资源版本的约束。如果未指定,默认值为未设置。 -
resourceVersionMatch: 确定在列表调用中如何应用resourceVersion约束。如果未指定,默认值为未设置。 -
timeoutSeconds: 为列表/监视调用指定超时持续时间。无论是否有活动或非活动,这都限制了调用的持续时间。 -
watch: 启用对描述资源的变化进行监控,并返回一个连续的通知流,通知内容包括新增、更新和删除。必须指定resourceVersion参数。
发现与负载均衡
集群中的工作负载默认仅在集群内可访问。要使其可在外部访问,需要使用LoadBalancer或NodePort服务。然而,对于开发目的,内部可访问的工作负载可以通过 API 服务器使用“kubectl proxy”命令进行访问:
-
Endpoints:core -
EndpointSlice:discovery.k8s.io/v1 -
Ingress:networking.k8s.io -
IngressClass:networking.k8s.io -
Service:core
配置与存储
动态配置和无需重新部署的秘密管理是 Kubernetes 的基石,也是运行复杂分布式应用程序在 Kubernetes 集群上的关键。秘密和配置不会嵌入到容器镜像中,而是存储在 Kubernetes 的状态存储中(通常是 etcd)。Kubernetes 还提供了许多抽象层来管理任意存储。以下是一些主要资源:
-
ConfigMap:core -
CSIDriver:storage.k8s.io -
CSINode:storage.k8s.io -
CSIStorageCapacity:storage.k8s.io -
Secret:core -
PersistentVolumeClaim:core -
StorageClass:storage.k8s.io -
Volume:core -
VolumeAttachment:storage.k8s.io
元数据
元数据资源通常作为它们所配置资源的子资源出现。例如,限制范围是在命名空间级别定义的,并且可以指定:
-
命名空间内 pod 或容器的计算资源使用范围(最小值和最大值)。
-
命名空间内每个
PersistentVolumeClaim的存储请求范围(最小值和最大值)。 -
命名空间内特定资源的资源请求与限制之间的比例。
-
命名空间内计算资源的默认请求/限制,这些会在运行时自动注入到容器中。
大多数情况下,你不会直接与这些对象交互。元数据资源非常多。你可以在此处找到完整的列表:kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#-strong-metadata-apis-strong-。
集群
集群类别中的资源是为集群操作员设计的,而非开发人员。这个类别也有很多资源。以下是一些最重要的资源:
-
Namespace:core -
Node:core -
PersistentVolume:core -
ResourceQuota:core -
Role:rbac.authorization.k8s.io -
RoleBinding:rbac.authorization.k8s.io -
ClusterRole:rbac.authorization.k8s.io -
ClusterRoleBinding:rbac.authorization.k8s.io -
NetworkPolicy:networking.k8s.io
现在我们了解了 Kubernetes 如何通过 API 组和资源类别组织并公开其能力,接下来我们来看它是如何管理物理基础设施并与集群状态保持同步的。
Kubernetes 组件
一个 Kubernetes 集群有多个控制平面组件用于控制集群,还有运行在每个工作节点上的节点组件。让我们了解这些组件及其如何协同工作。
控制平面组件
控制平面组件可以全部运行在一个节点上,但在高度可用的设置或非常大的集群中,它们可能会分布在多个节点上。
API 服务器
Kubernetes API 服务器暴露了 Kubernetes 的 REST API。由于它是无状态的,并且将所有数据存储在 etcd 集群中(或者像 k3s 这样的 Kubernetes 发行版中的其他数据存储),它可以轻松进行水平扩展。API 服务器是 Kubernetes 控制平面的体现。
etcd
etcd 是一个高度可靠的分布式数据存储。Kubernetes 使用它来存储整个集群的状态。在小型、短暂的集群中,etcd 的单个实例可以与所有其他控制平面组件一起运行在同一个节点上。但对于更大的集群,通常会有一个 3 节点甚至 5 节点的 etcd 集群,以实现冗余和高可用性。
Kube 控制器管理器
Kube 控制器管理器是多个管理器集合的二进制文件。它包含副本集控制器、Pod 控制器、服务控制器、端点控制器等。所有这些管理器通过 API 监视集群状态,它们的工作是将集群引导到期望的状态。
云控制器管理器
在云环境中运行时,Kubernetes 允许云服务提供商集成他们的平台,以便管理节点、路由、服务和存储卷。云提供商的代码与 Kubernetes 代码交互。它替代了部分 Kube 控制器管理器的功能。使用云控制器管理器运行 Kubernetes 时,必须将 Kube 控制器管理器的标志 --cloud-provider 设置为 external。这将禁用云控制器管理器接管的控制循环。
云控制器管理器是在 Kubernetes 1.6 中引入的,已经被多个云服务提供商使用,例如:
-
GCP
-
AWS
-
Azure
-
百度云
-
Digital Ocean
-
Oracle
-
Linode
好的,让我们来看一些代码。具体的代码并不重要,目的是让你了解 Kubernetes 代码的样子。Kubernetes 是用 Go 实现的。为了帮助你理解代码,简要说明 Go 的用法:方法名在前,后面跟着方法的参数(括号内)。每个参数是一个由名称和类型组成的对。最后,指定返回值。Go 允许多个返回类型。返回错误对象和实际结果是很常见的。如果一切正常,错误对象将为 nil。
这里是 cloudprovider 包的主要接口:
package cloudprovider
import (
"context"
"errors"
"fmt"
"strings"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/informers"
clientset "k8s.io/client-go/kubernetes"
restclient "k8s.io/client-go/rest"
)
// Interface is an abstract, pluggable interface for cloud providers.
type Interface interface {
Initialize(clientBuilder ControllerClientBuilder, stop <-chan struct{})
LoadBalancer() (LoadBalancer, bool)
Instances() (Instances, bool)
InstancesV2() (InstancesV2, bool)
Zones() (Zones, bool)
Clusters() (Clusters, bool)
Routes() (Routes, bool)
ProviderName() string
HasClusterID() bool
}
大多数方法返回其他接口及其自己的方法。例如,这里是 LoadBalancer 接口:
type LoadBalancer interface {
GetLoadBalancer(ctx context.Context, clusterName string, service *v1.Service) (status *v1.LoadBalancerStatus, exists bool, err error)
GetLoadBalancerName(ctx context.Context, clusterName string, service *v1.Service) string
EnsureLoadBalancer(ctx context.Context, clusterName string, service *v1.Service, nodes []*v1.Node) (*v1.LoadBalancerStatus, error)
UpdateLoadBalancer(ctx context.Context, clusterName string, service *v1.Service, nodes []*v1.Node) error
EnsureLoadBalancerDeleted(ctx context.Context, clusterName string, service *v1.Service) error
}
Kube 调度器
kube-scheduler 负责将 Pod 调度到节点上。这是一项非常复杂的任务,因为它需要考虑多个相互作用的因素,例如:
-
资源需求
-
服务需求
-
硬件/软件政策约束
-
节点亲和性与反亲和性规范
-
Pod 亲和性与反亲和性规范
-
污点与容忍
-
本地存储需求
-
数据本地性
-
截止时间
如果你需要一些默认 Kube 调度器未覆盖的特殊调度逻辑,你可以用自己的自定义调度器替换它。你还可以让自定义调度器与默认调度器并行运行,只调度 Pod 的子集。
DNS
从 Kubernetes 1.3 开始,DNS 服务成为标准 Kubernetes 集群的一部分。它作为常规 Pod 调度。每个服务(除去无头服务)都会分配一个 DNS 名称。Pod 也可以分配 DNS 名称。这对于自动发现非常有用。
我们已经涵盖了所有的控制平面组件。接下来,让我们看看每个节点上运行的 Kubernetes 组件。
节点组件
集群中的节点需要一些组件与 API 服务器交互,接收要执行的工作负载,并向 API 服务器更新它们的状态。
代理
kube-proxy 在每个节点上进行低级的网络维护。它在本地反映 Kubernetes 服务,并可以进行 TCP 和 UDP 转发。它通过环境变量或 DNS 查找集群 IP。
kubelet
kubelet 是节点上 Kubernetes 的代表。它负责与 API 服务器通信,并管理正在运行的 Pod。它包括以下内容:
-
接收 Pod 的规格
-
从 API 服务器下载 Pod 密钥
-
挂载卷
-
运行 Pod 的容器(通过配置的容器运行时)
-
报告节点和每个 Pod 的状态
-
运行容器的存活、就绪和启动探针
在这一节中,我们深入探讨了 Kubernetes 的内部机制,从一个非常高层次的视角探索其架构,支持的设计模式,直到它的 API 以及用于控制和管理集群的组件。在下一节中,我们将快速浏览 Kubernetes 支持的各种运行时。
Kubernetes 容器运行时
Kubernetes 最初只支持 Docker 作为容器运行时引擎,但现在已经不再是这种情况。Kubernetes 现在支持任何实现了 CRI 接口的运行时。
在这一节中,你将更深入地了解 CRI,并了解一些实现它的运行时引擎。在本节结束时,你将能够做出充分的信息决策,选择适合你用例的容器运行时,并了解在什么情况下你可能会切换或甚至在同一系统中结合多个运行时。
容器运行时接口(CRI)
CRI 是一个 gRPC API,包含容器运行时与节点上的 kubelet 集成所需的规范/要求和库。在 Kubernetes 1.7 中,Kubernetes 内部的 Docker 集成被替换为基于 CRI 的集成。这是一个重大变化,开启了多个实现的可能性,可以利用容器领域的进展。kubelet 不需要直接与多个运行时接口交互。相反,它可以与任何符合 CRI 标准的容器运行时进行交互。以下图示说明了流程:

图 1.2:kubelet 和 cri
有两个 gRPC 服务接口ImageService和RuntimeService,CRI 容器运行时(或 shim)必须实现。ImageService负责管理镜像。以下是 gRPC/protobuf 接口(这不是 Go 语言):
service ImageService {
rpc ListImages(ListImagesRequest) returns (ListImagesResponse) {}
rpc ImageStatus(ImageStatusRequest) returns (ImageStatusResponse) {}
rpc PullImage(PullImageRequest) returns (PullImageResponse) {}
rpc RemoveImage(RemoveImageRequest) returns (RemoveImageResponse) {}
rpc ImageFsInfo(ImageFsInfoRequest) returns (ImageFsInfoResponse) {}
}
RuntimeService负责管理 Pod 和容器。以下是 gRPC/protobuf 接口:
service RuntimeService {
rpc Version(VersionRequest) returns (VersionResponse) {}
rpc RunPodSandbox(RunPodSandboxRequest) returns (RunPodSandboxResponse) {}
rpc StopPodSandbox(StopPodSandboxRequest) returns (StopPodSandboxResponse) {}
rpc RemovePodSandbox(RemovePodSandboxRequest) returns (RemovePodSandboxResponse) {}
rpc PodSandboxStatus(PodSandboxStatusRequest) returns (PodSandboxStatusResponse) {}
rpc ListPodSandbox(ListPodSandboxRequest) returns (ListPodSandboxResponse) {}
rpc CreateContainer(CreateContainerRequest) returns (CreateContainerResponse) {}
rpc StartContainer(StartContainerRequest) returns (StartContainerResponse) {}
rpc StopContainer(StopContainerRequest) returns (StopContainerResponse) {}
rpc RemoveContainer(RemoveContainerRequest) returns (RemoveContainerResponse) {}
rpc ListContainers(ListContainersRequest) returns (ListContainersResponse) {}
rpc ContainerStatus(ContainerStatusRequest) returns (ContainerStatusResponse) {}
rpc UpdateContainerResources(UpdateContainerResourcesRequest) returns (UpdateContainerResourcesResponse) {}
rpc ExecSync(ExecSyncRequest) returns (ExecSyncResponse) {}
rpc Exec(ExecRequest) returns (ExecResponse) {}
rpc Attach(AttachRequest) returns (AttachResponse) {}
rpc PortForward(PortForwardRequest) returns (PortForwardResponse) {}
rpc ContainerStats(ContainerStatsRequest) returns (ContainerStatsResponse) {}
rpc ListContainerStats(ListContainerStatsRequest) returns (ListContainerStatsResponse) {}
rpc UpdateRuntimeConfig(UpdateRuntimeConfigRequest) returns (UpdateRuntimeConfigResponse) {}
rpc Status(StatusRequest) returns (StatusResponse) {}
}
作为参数和返回类型使用的数据类型被称为消息,并作为 API 的一部分进行定义。以下是其中之一:
message CreateContainerRequest {
string pod_sandbox_id = 1;
ContainerConfig config = 2;
PodSandboxConfig sandbox_config = 3;
}
正如你所看到的,消息可以嵌套在彼此内部。CreateContainerRequest 消息有一个字符串字段和另外两个字段,这两个字段本身也是消息:ContainerConfig 和 PodSandboxConfig。
要了解更多关于 gRPC 和 CRI 的信息,请查看以下资源:
kubernetes.io/docs/concepts/architecture/cri/
现在你已经在代码层面上了解了 Kubernetes 所认为的运行时引擎,我们来简要看一下各个独立的运行时引擎。
Docker
Docker 曾是容器领域的“巨无霸”。Kubernetes 最初设计时仅用于管理 Docker 容器。多运行时功能首次在 Kubernetes 1.3 中引入,CRI 则是在 Kubernetes 1.5 中引入。直到那时,Kubernetes 只能管理 Docker 容器。即使在 CRI 引入后,Kubernetes 源代码中仍保留了 DockerShim,直到 Kubernetes 1.24 才被移除。从那时起,Docker 不再享有任何特别待遇。
如果你在读这本书,我假设你对 Docker 及其所提供的功能非常熟悉。Docker 在全球范围内享有极高的普及度和增长,但也受到许多批评。批评者通常提到以下几个问题:
-
安全性
-
设置多容器应用(尤其是网络)的难度
-
开发、监控和日志记录
-
Docker 容器运行单个命令的限制
-
过快发布未完成的功能
Docker 知道这些批评,并已解决其中的一些问题。特别是,Docker 投资于其 Docker Swarm 产品。Docker Swarm 是一个 Docker 原生的编排解决方案,竞争对手是 Kubernetes。它比 Kubernetes 更易于使用,但功能和成熟度不如 Kubernetes 强大。
从 2016 年 4 月发布的 Docker 1.11 开始,Docker 改变了它运行容器的方式。现在的运行时使用 containerd 和 runC 来运行 开放容器倡议(OCI)镜像:

图 1.3:Docker 和 OCI
从 Docker 1.12 开始,Swarm 模式原生包含在 Docker 守护进程中,这让一些人感到不满,因为它导致了膨胀和范围蔓延。因此,更多的人转向了其他容器运行时。
2021 年 9 月,Docker 对大型组织要求订阅 Docker Desktop。这一做法并不受欢迎,正如你所料,许多组织纷纷寻找替代方案。Docker Desktop 是 Docker 的客户端分发和 UI,它不影响容器运行时。但它进一步损害了 Docker 在社区中的声誉和善意。
containerd
containerd 自 2019 年以来已成为 CNCF 毕业项目。现在它是 Kubernetes 容器的主流选择。所有主要云服务商都支持它,并且从 Kubernetes 1.24 开始,它是默认的容器运行时。
此外,Docker 容器运行时也是构建在 containerd 之上的。
CRI-O
CRI-O 是一个 CNCF 孵化项目。它旨在提供 Kubernetes 与符合 OCI 标准的容器运行时(如 Docker)之间的集成路径。CRI-O 提供以下功能:
-
支持多种镜像格式,包括现有的 Docker 镜像格式
-
支持多种方式下载镜像,包括信任和镜像验证
-
容器镜像管理(管理镜像层、覆盖文件系统等)
-
容器进程生命周期管理
-
满足 CRI 要求的监控和日志记录
-
按 CRI 要求的资源隔离
它目前支持 runC 和 Kata 容器,但任何符合 OCI 标准的容器运行时都可以插入并与 Kubernetes 集成。
轻量级虚拟机
Kubernetes 在同一节点上运行来自不同应用程序的容器,分享相同的操作系统。这使得以非常高效的方式运行大量容器成为可能。然而,容器隔离是一个严重的安全问题,曾发生多起特权升级案例,这促使人们对另一种方法产生了浓厚兴趣。轻量级虚拟机提供强大的虚拟机级别隔离,但不像标准虚拟机那样沉重,这使得它们可以作为 Kubernetes 上的容器运行时运行。一些知名的项目包括:
-
AWS Firecracker
-
Google gVisor
-
Kata Containers
-
Singularity
-
SmartOS
在本节中,我们介绍了 Kubernetes 支持的各种运行时引擎,以及向标准化、融合和将运行时支持从核心 Kubernetes 外部化的趋势。
总结
在本章中,我们涉及了很多内容。你了解了 Kubernetes 的组织、设计和架构。Kubernetes 是一个用于运行微服务应用的容器编排平台。Kubernetes 集群有一个控制平面和工作节点。容器在 Pods 中运行。每个 Pod 运行在一台物理或虚拟机器上。Kubernetes 直接支持许多概念,如服务、标签和持久存储。你可以在 Kubernetes 上实现各种分布式系统设计模式。容器运行时只需要实现 CRI。支持 Docker、containerd、CRI-O 等。
在第二章,创建 Kubernetes 集群中,我们将探索创建 Kubernetes 集群的各种方式,讨论何时使用不同的选项,并构建一个本地的多节点集群。
加入我们的 Discord 社区!
与其他用户、云专家、作者以及志同道合的专业人士一起阅读本书。
提出问题,提供解决方案给其他读者,通过“问我任何问题”环节与作者交流等等。
扫描二维码或访问链接立即加入社区。

第二章:创建 Kubernetes 集群
在上一章中,我们了解了 Kubernetes 的基本概念,它的设计、支持的概念、架构以及支持的各种容器运行时。
从零开始创建 Kubernetes 集群是一项复杂的任务。需要选择多种选项和工具,考虑的因素也很多。在本章中,我们将动手构建一些 Kubernetes 集群,使用 Minikube、KinD 和 k3d。我们还将讨论并评估其他工具,如 Kubeadm 和 Kubespray。我们还会研究本地、云端和裸金属等部署环境。我们将涵盖的主题如下:
-
为你的第一个集群做准备
-
使用 Minikube 创建一个单节点集群
-
使用 KinD 创建一个多节点集群
-
使用 k3d 创建一个多节点集群
-
在云端创建集群
-
从零开始创建裸金属集群
-
审视其他创建 Kubernetes 集群的选项
在本章结束时,你将对创建 Kubernetes 集群的各种选项有一个全面的理解,并掌握支持 Kubernetes 集群创建的最佳工具,同时你还将构建多个集群,包括单节点和多节点集群。
为你的第一个集群做准备
在我们开始创建集群之前,我们应该安装一些工具,如 Docker 客户端和 kubectl。如今,最方便的在 Mac 和 Windows 上安装 Docker 和 kubectl 的方法是通过 Rancher Desktop。如果你已经安装了这些工具,可以跳过本节。
安装 Rancher Desktop
Rancher Desktop 是一个跨平台的桌面应用程序,让你可以在本地计算机上运行 Docker。它将安装额外的工具,如:
-
Helm
-
Kubectl
-
Nerdctl
-
Moby(开源 Docker)
-
Docker Compose
macOS 上的安装
在 macOS 上安装 Rancher Desktop 最简便的方法是通过 Homebrew:
brew install --cask rancher
Windows 上的安装
在 Windows 上安装 Rancher Desktop 最简便的方法是通过 Chocolatey:
choco install rancher-desktop
其他安装方法
有关其他安装 Docker Desktop 的方法,请按照此处的说明进行操作:
docs.rancherdesktop.io/getting-started/installation/
让我们验证一下docker是否已正确安装。输入以下命令,并确保没有错误(如果你安装了与我不同的版本,输出不必完全相同):
$ docker version
Client:
Version: 20.10.9
API version: 1.41
Go version: go1.16.8
Git commit: c2ea9bc
Built: Thu Nov 18 21:17:06 2021
OS/Arch: darwin/arm64
Context: rancher-desktop
Experimental: true
Server:
Engine:
Version: 20.10.14
API version: 1.41 (minimum version 1.12)
Go version: go1.17.9
Git commit: 87a90dc786bda134c9eb02adbae2c6a7342fb7f6
Built: Fri Apr 15 00:05:05 2022
OS/Arch: linux/arm64
Experimental: false
containerd:
Version: v1.5.11
GitCommit: 3df54a852345ae127d1fa3092b95168e4a88e2f8
runc:
Version: 1.0.2
GitCommit: 52b36a2dd837e8462de8e01458bf02cf9eea47dd
docker-init:
Version: 0.19.0
GitCommit:
同时,让我们验证一下 kubectl 是否已正确安装:
$ kubectl version
Client Version: version.Info{Major:"1", Minor:"23", GitVersion:"v1.23.4", GitCommit:"e6c093d87ea4cbb530a7b2ae91e54c0842d8308a", GitTreeState:"clean", BuildDate:"2022-02-16T12:38:05Z", GoVersion:"go1.17.7", Compiler:"gc", Platform:"darwin/amd64"}
Server Version: version.Info{Major:"1", Minor:"23", GitVersion:"v1.23.6+k3s1", GitCommit:"418c3fa858b69b12b9cefbcff0526f666a6236b9", GitTreeState:"clean", BuildDate:"2022-04-28T22:16:58Z", GoVersion:"go1.17.5", Compiler:"gc", Platform:"linux/arm64"}
如果没有激活的 Kubernetes 服务器在运行,Server部分可能会为空。当你看到这个输出时,可以放心地确认 kubectl 已经准备好使用。
认识 kubectl
在我们开始创建集群之前,先来聊聊 kubectl。它是官方的 Kubernetes CLI,能够通过其 API 与 Kubernetes 集群的 API 服务器进行交互。默认情况下,它使用 ~/.kube/config 文件进行配置,这是一个 YAML 文件,包含元数据、连接信息以及一个或多个集群的身份验证令牌或证书。Kubectl 提供了查看配置和在多个集群之间切换的命令。如果配置文件包含多个集群,你也可以通过设置 KUBECONFIG 环境变量或传递 --kubeconfig 命令行标志,指定 kubectl 使用不同的配置文件。
下面的代码使用 kubectl 命令检查当前活动集群的 kube-system 命名空间中的 pods:
$ kubectl get pods -n kube-system
NAME READY STATUS RESTARTS AGE
svclb-traefik-fv84n 2/2 Running 6 (7d20h ago) 8d
local-path-provisioner-84bb864455-s2xmp 1/1 Running 20 (7d20h ago) 27d
metrics-server-ff9dbcb6c-lsffr 0/1 Running 88 (10h ago) 27d
coredns-d76bd69b-mc6cn 1/1 Running 11 (22h ago) 8d
traefik-df4ff85d6-2fskv 1/1 Running 7 (3d ago) 8d
Kubectl 很棒,但它并不是唯一的选择。我们来看看一些替代工具。
Kubectl 替代品 – K9S、KUI 和 Lens
Kubectl 是一个直截了当的命令行工具。它非常强大,但对于一些人来说,它的输出可能难以视觉解析,或者记不住所有的标志和选项。社区开发了许多可以替代(或者更像是补充)kubectl 的工具。在我看来,最好的几个是 K9S、KUI 和 Lens。
K9S
K9S 是一个基于终端的 UI,用于管理 Kubernetes 集群。它拥有许多快捷键和聚合视图,这些视图通常需要执行多个 kubectl 命令才能完成。
下面是 K9S 窗口的样子:

图 2.1:K9S 窗口
在这里查看:k9scli.io
KUI
KUI 是一个为 CLI(命令行界面)添加图形界面的框架。这是一个非常有趣的概念。KUI 当然专注于 Kubernetes。它让你运行 Kubectl 命令,并将结果以图形方式呈现。KUI 还收集了大量相关信息,并通过标签和详细面板以简洁的方式呈现,便于更深入探索。
KUI 基于 Electron,但它非常快速。
下面是 KUI 窗口的样子:

图 2.2:KUI 窗口
在这里查看:kui.tools
Lens
Lens 是一个非常精致的应用程序。它同样提供了集群的图形化视图,并允许你在 UI 中执行大量操作,必要时还可以切换到终端界面。我尤其欣赏 Lens 提供的轻松操作多个集群的能力。
下面是 Lens 窗口的样子:

图 2.3:Lens 窗口
在这里查看:k8slens.dev
所有这些工具都是本地运行的。我强烈建议你先尝试使用 kubectl,然后再试试这些工具。也许其中某一个正适合你的使用方式。
在本节中,我们介绍了 Rancher Desktop 的安装,介绍了 kubectl,并查看了一些替代方案。现在我们准备创建我们的第一个 Kubernetes 集群。
使用 Minikube 创建单节点集群
在本节中,我们将使用 Minikube 创建一个本地单节点集群。本地集群对于开发人员非常有用,尤其是那些希望在提交更改之前,在自己的机器上进行快速的编辑-测试-部署-调试循环的开发人员。本地集群对于 DevOps 和操作员也很有用,尤其是那些希望在不担心破坏共享环境或在云中创建昂贵资源并忘记清理的情况下,进行本地 Kubernetes 测试的人员。虽然 Kubernetes 通常在生产环境中部署在 Linux 上,但许多开发人员使用的是 Windows PC 或 Mac。因此,如果您希望在 Linux 上安装 Minikube,差异不大。

图 2.4:minikube
Minikube 简介
Minikube 是最成熟的本地 Kubernetes 集群。它运行最新的稳定 Kubernetes 版本,支持 Windows、macOS 和 Linux。Minikube 提供了许多高级选项和功能:
-
LoadBalancer 服务类型 - 通过 minikube tunnel
-
NodePort 服务类型 - 通过 minikube service
-
多个集群
-
文件系统挂载
-
GPU 支持 - 用于机器学习
-
RBAC
-
持久化卷
-
Ingress
-
仪表盘 - 通过 minikube dashboard
-
自定义容器运行时 - 通过
start --container-runtime标志 -
通过命令行标志配置 API 服务器和 kubelet 选项
-
插件
安装 Minikube
终极指南在这里:minikube.sigs.k8s.io/docs/start/
但是,为了节省您的时间,以下是写作时的最新安装说明。
在 Windows 上安装 Minikube
在 Windows 上,我喜欢通过 Chocolatey 包管理器安装软件。如果您还没有安装它,可以在这里获取:chocolatey.org/
如果您不想使用 Chocolatey,请查看上面的终极指南,获取其他方法。
安装 Chocolatey 后,安装过程相当简单:
PS C:\Windows\system32> choco install minikube -y
Chocolatey v0.12.1
Installing the following packages:
minikube
By installing, you accept licenses for the packages.
Progress: Downloading Minikube 1.25.2... 100%
kubernetes-cli v1.24.0 [Approved]
kubernetes-cli package files install completed. Performing other installation steps.
Extracting 64-bit C:\ProgramData\chocolatey\lib\kubernetes-cli\tools\kubernetes-client-windows-amd64.tar.gz to C:\ProgramData\chocolatey\lib\kubernetes-cli\tools...
C:\ProgramData\chocolatey\lib\kubernetes-cli\tools
Extracting 64-bit C:\ProgramData\chocolatey\lib\kubernetes-cli\tools\kubernetes-client-windows-amd64.tar to C:\ProgramData\chocolatey\lib\kubernetes-cli\tools...
C:\ProgramData\chocolatey\lib\kubernetes-cli\tools
ShimGen has successfully created a shim for kubectl-convert.exe
ShimGen has successfully created a shim for kubectl.exe
The install of kubernetes-cli was successful.
Software installed to 'C:\ProgramData\chocolatey\lib\kubernetes-cli\tools'
Minikube v1.25.2 [Approved]
minikube package files install completed. Performing other installation steps.
ShimGen has successfully created a shim for minikube.exe
The install of minikube was successful.
Software installed to 'C:\ProgramData\chocolatey\lib\Minikube'
Chocolatey installed 2/2 packages.
See the log for details (C:\ProgramData\chocolatey\logs\chocolatey.log).
在 Windows 上,您可以在不同的命令行环境中工作。最常见的环境是 PowerShell 和 WSL(Windows Subsystem for Linux)。这两种环境都可以使用。某些操作可能需要以管理员模式运行。
至于控制台窗口,近年来我推荐使用官方的 Windows Terminal。您可以通过一条命令安装它:
choco install microsoft-windows-terminal --pre
如果您更喜欢使用其他控制台窗口,如 ConEMU 或 Cmdr,也是完全可以的。
我会使用快捷方式让操作更简单。如果您希望跟着操作并将别名复制到您的配置文件中,您可以在 PowerShell 和 WSL 中使用以下内容。
对于 PowerShell,请将以下内容添加到您的 $profile:
function k { kubectl.exe $args } function mk { minikube.exe $args }
对于 WSL,请将以下内容添加到 .bashrc:
alias k='kubectl.exe'
alias mk=minikube.exe'
让我们验证 Minikube 是否正确安装:
$ mk version
minikube version: v1.25.2
commit: 362d5fdc0a3dbee389b3d3f1034e8023e72bd3a7
让我们使用 mk start 创建一个集群:
$ mk start
 minikube v1.25.2 on Microsoft Windows 10 Pro 10.0.19044 Build 19044
 Automatically selected the docker driver. Other choices: hyperv, ssh
 Starting control plane node minikube in cluster minikube
 Pulling base image ...
 Downloading Kubernetes v1.23.3 preload ...
> preloaded-images-k8s-v17-v1...: 505.68 MiB / 505.68 MiB 100.00% 3.58 MiB
> gcr.io/k8s-minikube/kicbase: 379.06 MiB / 379.06 MiB 100.00% 2.61 MiB p/
 Creating docker container (CPUs=2, Memory=8100MB) ...
 docker "minikube" container is missing, will recreate.
 Creating docker container (CPUs=2, Memory=8100MB) ...
 Downloading VM boot image ...
> minikube-v1.25.2.iso.sha256: 65 B / 65 B [-------------] 100.00% ? p/s 0s
> minikube-v1.25.2.iso: 237.06 MiB / 237.06 MiB 100.00% 12.51 MiB p/s 19s
 Creating hyperv VM (CPUs=2, Memory=6000MB, Disk=20000MB) ...
 This VM is having trouble accessing https://k8s.gcr.io
 To pull new external images, you may need to configure a proxy: https://minikube.sigs.k8s.io/docs/reference/networki
ng/proxy/
 Preparing Kubernetes v1.23.3 on Docker 20.10.12 ...
▪ kubelet.housekeeping-interval=5m
▪ Generating certificates and keys ...
▪ Booting up control plane ...
▪ Configuring RBAC rules ...
 Verifying Kubernetes components...
▪ Using image gcr.io/k8s-minikube/storage-provisioner:v5
 Enabled addons: storage-provisioner, default-storageclass
 Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default
如你所见,即使是默认设置的过程也相当复杂,需要多次重试(自动进行)。你可以通过多种命令行标志来自定义集群创建过程。输入mk start -h查看可用的选项。
让我们检查集群的状态:
$ mk status
minikube
type: Control Plane
host: Running
kubelet: Running
apiserver: Running
kubeconfig: Configured
一切顺利!
现在让我们停止集群,然后稍后重新启动它:
$ mk stop
 Stopping node "minikube" ...
 Powering off "minikube" via SSH ...
 1 node stopped.
使用time命令重启并测量所需时间:
$ time mk start
 minikube v1.25.2 on Microsoft Windows 10 Pro 10.0.19044 Build 19044
 Using the hyperv driver based on existing profile
 Starting control plane node minikube in cluster minikube
 Restarting existing hyperv VM for "minikube" ...
 This VM is having trouble accessing https://k8s.gcr.io
 To pull new external images, you may need to configure a proxy: https://minikube.sigs.k8s.io/docs/reference/networki
ng/proxy/
 Preparing Kubernetes v1.23.3 on Docker 20.10.12 ...
▪ kubelet.housekeeping-interval=5m
 Verifying Kubernetes components...
▪ Using image gcr.io/k8s-minikube/storage-provisioner:v5
 Enabled addons: storage-provisioner, default-storageclass
 Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default
real 1m8.666s
user 0m0.004s
sys 0m0.000s
这花了稍微超过一分钟。
让我们回顾一下 Minikube 为你背后做了哪些工作。当你从零开始创建集群时,你将需要做很多这些操作。
-
启动了一个 Hyper-V 虚拟机
-
为本地机器和虚拟机创建了证书
-
下载了镜像
-
设置本地机器与虚拟机之间的网络连接
-
在虚拟机上运行本地 Kubernetes 集群
-
配置了集群
-
启动了所有 Kubernetes 控制平面组件
-
配置了 kubelet
-
启用了附加组件(用于存储)
-
配置了 kubectl 与集群通信
在 macOS 上安装 Minikube
在 Mac 上,我建议使用 Homebrew 安装 minikube:
$ brew install minikube
Running `brew update --preinstall`...
==> Auto-updated Homebrew!
Updated 2 taps (homebrew/core and homebrew/cask).
==> Updated Formulae
Updated 39 formulae.
==> New Casks
contour hdfview rancher-desktop | kube-system
==> Updated Casks
Updated 17 casks.
==> Downloading https://ghcr.io/v2/homebrew/core/kubernetes-cli/manifests/1.24.0
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/kubernetes-cli/blobs/sha256:e57f8f7ea19d22748d1bcae5cd02b91e71816147712e6dcd
==> Downloading from https://pkg-containers.githubusercontent.com/ghcr1/blobs/sha256:e57f8f7ea19d22748d1bcae5cd02b91e71816147
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/minikube/manifests/1.25.2
Already downloaded: /Users/gigi.sayfan/Library/Caches/Homebrew/downloads/fa0034afe1330adad087a8b3dc9ac4917982d248b08a4df4cbc52ce01d5eabff--minikube-1.25.2.bottle_manifest.json
==> Downloading https://ghcr.io/v2/homebrew/core/minikube/blobs/sha256:6dee5f22e08636346258f4a6daa646e9102e384ceb63f33981745d
Already downloaded: /Users/gigi.sayfan/Library/Caches/Homebrew/downloads/ceeab562206fd08fd3b6523a85b246d48d804b2cd678d76cbae4968d97b5df1f--minikube--1.25.2.arm64_monterey.bottle.tar.gz
==> Installing dependencies for minikube: kubernetes-cli
==> Installing minikube dependency: kubernetes-cli
==> Pouring kubernetes-cli--1.24.0.arm64_monterey.bottle.tar.gz
 /opt/homebrew/Cellar/kubernetes-cli/1.24.0: 228 files, 55.3MB
==> Installing minikube
==> Pouring minikube--1.25.2.arm64_monterey.bottle.tar.gz
==> Caveats
zsh completions have been installed to:
/opt/homebrew/share/zsh/site-functions
==> Summary
 /opt/homebrew/Cellar/minikube/1.25.2: 9 files, 70.3MB
==> Running `brew cleanup minikube`...
Disable this behaviour by setting HOMEBREW_NO_INSTALL_CLEANUP.
Hide these hints with HOMEBREW_NO_ENV_HINTS (see `man brew`).
==> Caveats
==> minikube
zsh completions have been installed to:
/opt/homebrew/share/zsh/site-functions
你可以将别名添加到你的.bashrc文件中(类似于 Windows 上的 WSL 别名):
alias k='kubectl'
alias mk='$(brew --prefix)/bin/minikube'
现在你可以使用k和mk,这样可以减少输入。
输入mk version来验证 Minikube 是否正确安装并正常运行:
$ mk version
minikube version: v1.25.2
commit: 362d5fdc0a3dbee389b3d3f1034e8023e72bd3a7
输入k version来验证 kubectl 是否正确安装并正常运行:
$ k version
I0522 15:41:13.663004 68055 versioner.go:58] invalid configuration: no configuration has been provided
Client Version: version.Info{Major:"1", Minor:"23", GitVersion:"v1.23.4", GitCommit:"e6c093d87ea4cbb530a7b2ae91e54c0842d8308a", GitTreeState:"clean", BuildDate:"2022-02-16T12:38:05Z", GoVersion:"go1.17.7", Compiler:"gc", Platform:"darwin/amd64"}
The connection to the server localhost:8080 was refused - did you specify the right host or port?
请注意客户端版本是 1.23。不要担心错误信息。没有集群在运行,所以 kubectl 无法连接任何东西。这是预期中的情况。当我们创建集群时,错误信息将消失。
你可以通过不带参数地输入命令,探索 Minikube 和 kubectl 的可用命令和标志。
在 macOS 上创建集群,只需运行mk start。
故障排除 Minikube 安装
如果在过程中出现问题,请尝试根据错误信息进行操作。你可以添加--alsologtostderr标志,将详细的错误信息输出到控制台。Minikube 执行的所有操作都井井有条地组织在~/.minikube目录下。这里是目录结构:
$ tree ~/.minikube\ -L 2
C:\Users\the_g\.minikube\
|-- addons
|-- ca.crt
|-- ca.key
|-- ca.pem
|-- cache
| |-- iso
| |-- kic
| `-- preloaded-tarball
|-- cert.pem
|-- certs
| |-- ca-key.pem
| |-- ca.pem
| |-- cert.pem
| `-- key.pem
|-- config
|-- files
|-- key.pem
|-- logs
| |-- audit.json
| `-- lastStart.txt
|-- machine_client.lock
|-- machines
| |-- minikube
| |-- server-key.pem
| `-- server.pem
|-- profiles
| `-- minikube
|-- proxy-client-ca.crt
`-- proxy-client-ca.key
13 directories, 16 files
如果你没有 tree 工具,可以安装它。
在 Windows 上:$ choco install -y tree
在 Mac 上:brew install tree
查看集群
既然我们已经有了一个正在运行的集群,让我们来看一下集群内部。
首先,让我们ssh进入虚拟机:
$ mk ssh
_ _
_ _ ( ) ( )
___ ___ (_) ___ (_)| |/') _ _ | |_ __
/' _ ` _ `\| |/' _ `\| || , < ( ) ( )| '_`\ /'__`\
| ( ) ( ) || || ( ) || || |\`\ | (_) || |_) )( ___/
(_) (_) (_)(_)(_) (_)(_)(_) (_)`\___/'(_,__/'`\____)
$ uname -a
Linux minikube 4.19.202 #1 SMP Tue Feb 8 19:13:02 UTC 2022 x86_64 GNU/Linux
$
很好!它能正常工作。那些奇怪的符号是“minikube”的 ASCII 艺术。现在,让我们开始使用 kubectl,因为它是 Kubernetes 的瑞士军刀,将对所有集群都非常有用。
通过ctrl+D或输入以下命令断开与虚拟机的连接:
$ logout
在我们的旅程中,我们将涵盖许多 kubectl 命令。首先,使用cluster-info检查集群状态:
$ k cluster-info
Kubernetes control plane is running at https://172.26.246.89:8443
CoreDNS is running at https://172.26.246.89:8443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
你可以看到控制平面正在正常运行。要查看集群中所有对象的更详细的 JSON 格式视图,输入:k cluster-info dump。输出可能会让人觉得有些让人望而生畏,我们可以使用更具体的命令来探索集群。
让我们使用get nodes检查集群中的节点:
$ k get nodes
NAME STATUS ROLES AGE VERSION
minikube Ready control-plane,master 62m v1.23.3
所以,我们有一个名为 minikube 的节点。要获取更多关于它的信息,输入:
k describe node minikube
输出非常详细;我会让你自己试一下。
在我们开始让集群工作之前,先检查一下 Minikube 默认安装的附加组件:
mk addons list
|-----------------------------|----------|--------------|--------------------------------|
| ADDON NAME | PROFILE | STATUS | MAINTAINER |
|-----------------------------|----------|--------------|--------------------------------|
| ambassador | minikube | disabled | third-party (ambassador) |
| auto-pause | minikube | disabled | google |
| csi-hostpath-driver | minikube | disabled | kubernetes |
| dashboard | minikube | disabled | kubernetes |
| default-storageclass | minikube | enabled  | kubernetes |
| efk | minikube | disabled | third-party (elastic) |
| freshpod | minikube | disabled | google |
| gcp-auth | minikube | disabled | google |
| gvisor | minikube | disabled | google |
| helm-tiller | minikube | disabled | third-party (helm) |
| ingress | minikube | disabled | unknown (third-party) |
| ingress-dns | minikube | disabled | google |
| istio | minikube | disabled | third-party (istio) |
| istio-provisioner | minikube | disabled | third-party (istio) |
| kong | minikube | disabled | third-party (Kong HQ) |
| kubevirt | minikube | disabled | third-party (kubevirt) |
| logviewer | minikube | disabled | unknown (third-party) |
| metallb | minikube | disabled | third-party (metallb) |
| metrics-server | minikube | disabled | kubernetes |
| nvidia-driver-installer | minikube | disabled | google |
| nvidia-gpu-device-plugin | minikube | disabled | third-party (nvidia) |
| olm | minikube | disabled | third-party (operator |
| | | | framework) |
| pod-security-policy | minikube | disabled | unknown (third-party) |
| portainer | minikube | disabled | portainer.io |
| registry | minikube | disabled | google |
| registry-aliases | minikube | disabled | unknown (third-party) |
| registry-creds | minikube | disabled | third-party (upmc enterprises) |
| storage-provisioner | minikube | enabled  | google |
| storage-provisioner-gluster | minikube | disabled | unknown (third-party) |
| volumesnapshots | minikube | disabled | kubernetes |
|-----------------------------|----------|--------------|--------------------------------|
正如你所看到的,Minikube 配载了许多附加组件,但默认仅启用了几个存储相关的附加组件。
开始工作
在开始之前,如果你正在运行 VPN,拉取镜像时可能需要关闭它。
我们已经有了一个干净的空集群(当然并非完全空,因为 DNS 服务和仪表板作为 Pod 运行在 kube-system 命名空间中)。现在是时候部署一些 Pod 了:
$ k create deployment echo --image=k8s.gcr.io/e2e-test-images/echoserver:2.5
deployment.apps/echo created
让我们检查一下已经创建的 Pod。-w 标志表示监视。每当状态变化时,都会显示新的一行:
$ k get po -w
NAME READY STATUS RESTARTS AGE
echo-7fd7648898-6hh48 0/1 ContainerCreating 0 5s
echo-7fd7648898-6hh48 1/1 Running 0 6s
要将我们的 Pod 暴露为服务,请输入以下命令:
$ k expose deployment echo --type=NodePort --port=8080
service/echo exposed
将服务暴露为 NodePort 类型意味着它通过某个端口暴露给主机。但这不是我们在 Pod 上运行的 8080 端口。端口会在集群中进行映射。要访问该服务,我们需要集群 IP 和暴露的端口:
$ mk ip
172.26.246.89
$ k get service echo -o jsonpath='{.spec.ports[0].nodePort}'
32649
现在我们可以访问回显服务,它返回了大量信息:
n$ curl http://172.26.246.89:32649/hi
Hostname: echo-7fd7648898-6hh48
Pod Information:
-no pod information available-
Server values:
server_version=nginx: 1.14.2 - lua: 10015
Request Information:
client_address=172.17.0.1
method=GET
real path=/hi
query=
request_version=1.1
request_scheme=http
request_uri=http://172.26.246.89:8080/hi
Request Headers:
accept=*/*
host=172.26.246.89:32649
user-agent=curl/7.79.1
Request Body:
-no body in request-
恭喜!你刚刚创建了一个本地 Kubernetes 集群,部署了一个服务,并将其暴露给全世界。
使用仪表板检查集群
Kubernetes 有一个非常好用的 Web 界面,当然它是作为服务部署在一个 Pod 中的。仪表板设计精良,提供了集群的高层次概览,并能深入查看单个资源、查看日志、编辑资源文件等。当你想手动查看集群,并且没有像 KUI 或 Lens 这样的本地工具时,它是完美的利器。Minikube 将它作为附加组件提供。
让我们启用它:
$ mk addons enable dashboard
▪ Using image kubernetesui/dashboard:v2.3.1
▪ Using image kubernetesui/metrics-scraper:v1.0.7
 Some dashboard features require the metrics-server addon. To enable all features please run:
minikube addons enable metrics-server
 The 'dashboard' addon is enabled
要启动它,输入:
$ mk dashboard
 Verifying dashboard health ...
 Launching proxy ...
 Verifying proxy health ...
 Opening http://127.0.0.1:63200/api/v1/namespaces/kubernetes-dashboard/services/http:kubernetes-dashboard:/proxy/ in your default browser...
Minikube 会打开一个浏览器窗口,显示仪表板 UI。
这是工作负载视图,显示了部署(Deployments)、副本集(Replica Sets)和Pod。

图 2.5:工作负载仪表板
它还可以显示守护进程集、状态集和任务,但我们在这个集群中没有这些。
要删除我们创建的集群,请输入:
$ mk delete
 Deleting "minikube" in docker ...
 Deleting container "minikube" ...
 Removing /Users/gigi.sayfan/.minikube/machines/minikube ...
 Removed all traces of the "minikube" cluster.
在本节中,我们在 Windows 上创建了一个本地单节点 Kubernetes 集群,使用 kubectl 探索了一下,部署了一个服务,并尝试了网页 UI。在下一节中,我们将转向多节点集群。
使用 KinD 创建多节点集群
在本节中,我们将使用 KinD 创建一个多节点集群。我们还将重复在 Minikube 上部署的回显服务器,并观察其中的差异。剧透警告——一切都会更快且更容易!
KinD 简介
KinD 代表 Kubernetes in Docker。它是一个用于创建临时集群(没有持久化存储)的工具。它最初是为运行 Kubernetes 兼容性测试而构建的。它支持 Kubernetes 1.11+。在后台,它使用 kubeadm 来启动 Docker 容器作为集群中的节点。KinD 是一个库和命令行工具的结合体。你可以在代码中使用该库进行测试或其他用途。KinD 可以创建具有多个控制平面节点的高可用集群。最后,KinD 是一个符合 CNCF 标准的 Kubernetes 安装工具。如果它被用于 Kubernetes 本身的兼容性测试,它必须符合这个标准。
KinD 启动非常迅速,但它也有一些限制:
-
没有持久化存储
-
目前尚不支持其他运行时,仅支持 Docker
让我们安装 KinD 并开始吧。
安装 KinD
必须安装 Docker,因为 KinD 实际上是作为 Docker 容器运行的。如果你安装了 Go,可以通过以下方式安装 KinD CLI:
go install sigs.k8s.io/kind@v0.14.0
否则,在 macOS 上输入:
brew install kind
在 Windows 上,输入:
choco install kind
处理 Docker 上下文
您的系统上可能有多个 Docker 引擎,而 Docker 上下文决定了使用哪个。您可能会遇到如下错误:
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
在这种情况下,检查你的 Docker 上下文:
$ docker context ls
NAME DESCRIPTION DOCKER ENDPOINT KUBERNETES ENDPOINT ORCHESTRATOR
colima colima unix:///Users/gigi.sayfan/.colima/docker.sock
default * Current DOCKER_HOST based configuration unix:///var/run/docker.sock https://127.0.0.1:6443 (default) swarm
rancher-desktop Rancher Desktop moby context unix:///Users/gigi.sayfan/.rd/docker.sock https://127.0.0.1:6443 (default)
用 * 标记的上下文是当前上下文。如果你使用 Rancher Desktop,则应将上下文设置为 rancher-desktop:
$ docker context use rancher-desktop
使用 KinD 创建集群
创建集群非常简单。
$ kind create cluster
Creating cluster "kind" ...
 Ensuring node image (kindest/node:v1.23.4) 
 Preparing nodes 
 Writing configuration 
 Starting control-plane 
 Installing CNI 
 Installing StorageClass 
Set kubectl context to "kind-kind"
You can now use your cluster with:
kubectl cluster-info --context kind-kind
Thanks for using kind! 
创建一个单节点集群需要不到 30 秒。
现在,我们可以使用 kubectl 访问集群:
$ k config current-context
kind-kind
$ k cluster-info
Kubernetes control plane is running at https://127.0.0.1:51561
CoreDNS is running at https://127.0.0.1:51561/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
默认情况下,KinD 会将其 kube 上下文添加到默认的 ~/.kube/config 文件中。当创建大量临时集群时,有时最好将 KinD 上下文存储在单独的文件中,避免让 ~/.kube/config 文件变得混乱。这可以通过传递 --kubeconfig 标志并指定文件路径轻松完成。
因此,KinD 默认创建一个单节点集群:
$ k get no
NAME STATUS ROLES AGE VERSION
kind-control-plane Ready control-plane,master 4m v1.23.4
让我们删除它并创建一个多节点集群:
$ kind delete cluster
Deleting cluster "kind" ...
要创建一个多节点集群,我们需要提供一个包含节点规格的配置文件。以下是一个配置文件,它将创建一个名为 multi-node-cluster 的集群,包含一个控制平面节点和两个工作节点:
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
name: multi-node-cluster
nodes:
- role: control-plane
- role: worker
- role: worker
让我们将配置文件保存为 kind-multi-node-config.yaml 并创建集群,将 kubeconfig 存储在自己的文件 $TMPDIR/kind-multi-node-config 中:
$ kind create cluster --config kind-multi-node-config.yaml --kubeconfig $TMPDIR/kind-multi-node-config
Creating cluster "multi-node-cluster" ...
 Ensuring node image (kindest/node:v1.23.4) 
 Preparing nodes 
 Writing configuration 
 Starting control-plane 
 Installing CNI 
 Installing StorageClass 
 Joining worker nodes 
Set kubectl context to "kind-multi-node-cluster"
You can now use your cluster with:
kubectl cluster-info --context kind-multi-node-cluster --kubeconfig /var/folders/qv/7l781jhs6j19gw3b89f4fcz40000gq/T//kind-multi-node-config
Have a nice day! 
对了,成功了!我们在不到一分钟的时间内得到了一个本地的 3 节点集群:
$ k get nodes --kubeconfig $TMPDIR/kind-multi-node-config
NAME STATUS ROLES AGE VERSION
multi-node-cluster-control-plane Ready control-plane,master 2m17s v1.23.4
multi-node-cluster-worker Ready <none> 100s v1.23.4
multi-node-cluster-worker2 Ready <none> 100s v1.23.4
KinD 还很贴心(看到了吗)地允许我们创建具有多个控制平面节点的 HA(高可用)集群以实现冗余。如果你想要一个具有三个控制平面节点和两个工作节点的高可用集群,你的集群配置文件会非常类似:
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
name: ha-multi-node-cluster
nodes:
- role: control-plane
- role: control-plane
- role: control-plane
- role: worker
- role: worker
让我们将配置文件保存为 kind-ha-multi-node-config.yaml 并创建一个新的 HA 集群:
$ kind create cluster --config kind-ha-multi-node-config.yaml --kubeconfig $TMPDIR/kind-ha-multi-node-config
Creating cluster "ha-multi-node-cluster" ...
 Ensuring node image (kindest/node:v1.23.4) 
 Preparing nodes 
 Configuring the external load balancer 
 Writing configuration 
 Starting control-plane 
 Installing CNI 
 Installing StorageClass 
 Joining more control-plane nodes 
 Joining worker nodes 
Set kubectl context to "kind-ha-multi-node-cluster"
You can now use your cluster with:
kubectl cluster-info --context kind-ha-multi-node-cluster --kubeconfig /var/folders/qv/7l781jhs6j19gw3b89f4fcz40000gq/T//kind-ha-multi-node-config
Not sure what to do next?  Check out https://kind.sigs.k8s.io/docs/user/quick-start/
嗯……这里有些新变化。现在,KinD 创建了一个外部负载均衡器,并且在加入工作节点之前,会先加入更多的控制平面节点。负载均衡器对于在所有控制平面节点之间分发请求是必要的。
请注意,使用 kubectl 时,外部负载均衡器不会显示为节点:
$ k get nodes --kubeconfig $TMPDIR/kind-ha-multi-node-config
NAME STATUS ROLES AGE VERSION
ha-multi-node-cluster-control-plane Ready control-plane,master 3m31s v1.23.4
ha-multi-node-cluster-control-plane2 Ready control-plane,master 3m19s v1.23.4
ha-multi-node-cluster-control-plane3 Ready control-plane,master 2m22s v1.23.4
ha-multi-node-cluster-worker Ready <none> 2m4s v1.23.4
ha-multi-node-cluster-worker2 Ready <none> 2m5s v1.23.4
但是,KinD 有自己的 get nodes 命令,在这里你可以看到负载均衡器:
$ kind get nodes --name ha-multi-node-cluster
ha-multi-node-cluster-control-plane2
ha-multi-node-cluster-external-load-balancer
ha-multi-node-cluster-control-plane
ha-multi-node-cluster-control-plane3
ha-multi-node-cluster-worker
ha-multi-node-cluster-worker2
我们的 KinD 集群已启动并运行;让我们开始使用它。
使用 KinD 进行工作
让我们在 KinD 集群上部署我们的回声服务。它的启动方式相同:
$ k create deployment echo --image=g1g1/echo-server:0.1 --kubeconfig $TMPDIR/kind-ha-multi-node-config
deployment.apps/echo created
$ k expose deployment echo --type=NodePort --port=7070 --kubeconfig $TMPDIR/kind-ha-multi-node-config
service/echo exposed
检查我们的服务时,我们可以看到回声服务处于最前面:
$ k get svc echo --kubeconfig $TMPDIR/kind-ha-multi-node-config
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
echo NodePort 10.96.52.33 <none> 7070:31953/TCP 10s
但是,服务没有外部 IP。使用 minikube 时,我们可以通过 $(minikube ip) 获取 minikube 节点本身的 IP,并可以结合节点端口来访问服务。但在 KinD 集群中没有这个选项。让我们看看如何使用代理来访问回声服务。
通过代理本地访问 Kubernetes 服务
我们将在本书后续部分详细讨论网络、服务以及如何将它们暴露到集群外部。
在这里,我们将只展示如何完成操作,并且暂时保持悬念。首先,我们需要运行 kubectl proxy 命令来暴露 API 服务器、Pod 和服务到本地主机:
$ k proxy --kubeconfig $TMPDIR/kind-ha-multi-node-config &
[1] 32479
Starting to serve on 127.0.0.1:8001
然后,我们可以通过一个专门设计的代理 URL 来访问回声服务,URL 中包含暴露的端口(8080),而不是节点端口:
$ http http://localhost:8001/api/v1/namespaces/default/services/echo:7070/proxy/yeah-it-works
HTTP/1.1 200 OK
Audit-Id: 294cf10b-0d60-467d-8a51-4414834fc173
Cache-Control: no-cache, private
Content-Length: 13
Content-Type: text/plain; charset=utf-8
Date: Mon, 23 May 2022 21:54:01 GMT
yeah-it-works
我在上面的命令中使用了 httpie。你也可以使用 curl。要安装 httpie,请按照这里的说明操作:httpie.org/doc#installation。
我们将在第十章、探索 Kubernetes 网络中深入探讨发生了什么。目前,只需演示如何通过 kubectl proxy 访问我们的 KinD 服务就足够了。
让我们看看我最喜欢的本地集群解决方案——k3d。
使用 k3d 创建多节点集群
在这一节中,我们将使用 Rancher 的 k3d 创建一个多节点集群。我们不会重复回声服务的部署,因为它与 KinD 集群相同,包括通过代理访问它。剧透警告——使用 k3d 创建集群比 KinD 更快且更具用户友好性!
k3s 和 k3d 快速介绍
Rancher 创建了 k3s,这是一个轻量级的 Kubernetes 发行版。Rancher 表示 k3s 比 k8s 少了 5(如果有任何意义的话)。基本的想法是移除大多数人不需要的功能和能力,例如:
-
非默认功能
-
传统功能
-
Alpha 功能
-
内建存储驱动程序
-
内建云提供商
K3s 完全去除了 Docker,改用了 containerd。如果你依赖 Docker,仍然可以将它恢复。另一个重大变化是,k3s 将其状态存储在 SQLite 数据库中,而不是 etcd。对于网络和 DNS,k3s 使用 Flannel 和 CoreDNS。
K3s 还添加了一个简化的安装程序,负责 SSL 和证书的配置。
最终结果令人惊讶——一个单一的二进制文件(小于 40MB),只需要 512MB 的内存。
与 Minikube 和 KinD 不同,k3s 实际上是为生产环境设计的。其主要用途是边缘计算、物联网和持续集成系统。它已经针对 ARM 设备进行了优化。
好的,那是 k3s,但 k3d 又是什么?K3d 将 k3s 的所有优点打包在 Docker 中(类似于 KinD),并添加了一个友好的 CLI 来管理它。
让我们安装 k3d,亲自体验一下。
安装 k3d
在 macOS 上安装 k3d 就像这样简单:
brew install k3d
在 Windows 上,操作非常简单:
choco install -y k3d
在 Windows 上,可以选择将此别名添加到你的 WSL .bashrc 文件中:
alias k3d='k3d.exe'
让我们看看当前的情况:
$ k3d version
k3d version v5.4.1
k3s version v1.22.7-k3s1 (default)
如你所见,k3d 会报告其版本,显示一切正常。现在,我们可以使用 k3d 创建集群。
使用 k3d 创建集群
准备好震撼了吗?使用 k3d 创建单节点集群只需不到 20 秒!
$ time k3d cluster create
INFO[0000] Prep: Network
INFO[0000] Created network 'k3d-k3s-default'
INFO[0000] Created image volume k3d-k3s-default-images
INFO[0000] Starting new tools node...
INFO[0000] Starting Node 'k3d-k3s-default-tools'
INFO[0001] Creating node 'k3d-k3s-default-server-0'
INFO[0001] Creating LoadBalancer 'k3d-k3s-default-serverlb'
INFO[0002] Using the k3d-tools node to gather environment information
INFO[0002] HostIP: using network gateway 172.19.0.1 address
INFO[0002] Starting cluster 'k3s-default'
INFO[0002] Starting servers...
INFO[0002] Starting Node 'k3d-k3s-default-server-0'
INFO[0008] All agents already running.
INFO[0008] Starting helpers...
INFO[0008] Starting Node 'k3d-k3s-default-serverlb'
INFO[0015] Injecting records for hostAliases (incl. host.k3d.internal) and for 2 network members into CoreDNS configmap...
INFO[0017] Cluster 'k3s-default' created successfully!
INFO[0018] You can now use it like this:
kubectl cluster-info
real 0m18.154s
user 0m0.005s
sys 0m0.000s
没有负载均衡器的情况下,启动时间少于 8 秒!
那么,如何处理多节点集群呢?我们看到 KinD 的速度要慢得多,尤其是在创建具有多个控制平面节点和外部负载均衡器的高可用集群时。
让我们先删除单节点集群:
$ k3d cluster delete
INFO[0000] Deleting cluster 'k3s-default'
INFO[0000] Deleting cluster network 'k3d-k3s-default'
INFO[0000] Deleting 2 attached volumes...
WARN[0000] Failed to delete volume 'k3d-k3s-default-images' of cluster 'k3s-default': failed to find volume 'k3d-k3s-default-images': Error: No such volume: k3d-k3s-default-images -> Try to delete it manually
INFO[0000] Removing cluster details from default kubeconfig...
INFO[0000] Removing standalone kubeconfig file (if there is one)...
INFO[0000] Successfully deleted cluster k3s-default!
现在,让我们创建一个包含 3 个工作节点的集群。大约需要 30 秒多一点:
$ time k3d cluster create --agents 3
INFO[0000] Prep: Network
INFO[0000] Created network 'k3d-k3s-default'
INFO[0000] Created image volume k3d-k3s-default-images
INFO[0000] Starting new tools node...
INFO[0000] Starting Node 'k3d-k3s-default-tools'
INFO[0001] Creating node 'k3d-k3s-default-server-0'
INFO[0001] Creating node 'k3d-k3s-default-agent-0'
INFO[0002] Creating node 'k3d-k3s-default-agent-1'
INFO[0002] Creating node 'k3d-k3s-default-agent-2'
INFO[0002] Creating LoadBalancer 'k3d-k3s-default-serverlb'
INFO[0002] Using the k3d-tools node to gather environment information
INFO[0002] HostIP: using network gateway 172.22.0.1 address
INFO[0002] Starting cluster 'k3s-default'
INFO[0002] Starting servers...
INFO[0002] Starting Node 'k3d-k3s-default-server-0'
INFO[0008] Starting agents...
INFO[0008] Starting Node 'k3d-k3s-default-agent-0'
INFO[0008] Starting Node 'k3d-k3s-default-agent-2'
INFO[0008] Starting Node 'k3d-k3s-default-agent-1'
INFO[0018] Starting helpers...
INFO[0019] Starting Node 'k3d-k3s-default-serverlb'
INFO[0029] Injecting records for hostAliases (incl. host.k3d.internal) and for 5 network members into CoreDNS configmap...
INFO[0032] Cluster 'k3s-default' created successfully!
INFO[0032] You can now use it like this:
kubectl cluster-info
real 0m32.512s
user 0m0.005s
sys 0m0.000s
让我们验证集群是否按预期工作:
$ k cluster-info
Kubernetes control plane is running at https://0.0.0.0:60490
CoreDNS is running at https://0.0.0.0:60490/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
Metrics-server is running at https://0.0.0.0:60490/api/v1/namespaces/kube-system/services/https:metrics-server:https/proxy
To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
这里是各个节点。请注意,只有一个控制平面节点,名为k3d-k3s-default-server-0:
$ k get nodes
NAME STATUS ROLES AGE VERSION
k3d-k3s-default-server-0 Ready control-plane,master 5m33s v1.22.7+k3s1
k3d-k3s-default-agent-0 Ready <none> 5m30s v1.22.7+k3s1
k3d-k3s-default-agent-2 Ready <none> 5m30s v1.22.7+k3s1
k3d-k3s-default-agent-1 Ready <none> 5m29s v1.22.7+k3s1
你可以使用 k3d CLI 停止和启动集群,创建多个集群,列出现有集群。以下是所有命令,欢迎进一步探索:
$ k3d
Usage:
k3d [flags]
k3d [command]
Available Commands:
cluster Manage cluster(s)
completion Generate completion scripts for [bash, zsh, fish, powershell | psh]
config Work with config file(s)
help Help about any command
image Handle container images.
kubeconfig Manage kubeconfig(s)
node Manage node(s)
registry Manage registry/registries
version Show k3d and default k3s version
Flags:
-h, --help help for k3d
--timestamps Enable Log timestamps
--trace Enable super verbose output (trace logging)
--verbose Enable verbose output (debug logging)
--version Show k3d and default k3s version
Use "k3d [command] --help" for more information about a command.
你可以自行重复部署、暴露和访问回显服务的步骤。它的工作方式与 KinD 完全相同。
好的,我们已经使用 minikube、KinD 和 k3d 创建了集群。现在,让我们对它们进行比较,这样你就可以决定哪个最适合你。
比较 Minikube、KinD 和 k3d
Minikube 是一个官方的本地 Kubernetes 版本,功能非常成熟且全面。不过,它需要虚拟机,安装和启动都比较慢。有时,它的网络会出现问题,唯一的解决方法是删除集群并重新启动。此外,minikube 仅支持单节点。我建议只有在 Minikube 提供 KinD 或 k3d 不支持的某些功能时才使用它。更多信息请查看 minikube.sigs.k8s.io/。
KinD 比 Minikube 快得多,并且用于 Kubernetes 一致性测试,因此它本质上是一个符合标准的 Kubernetes 发行版。它是唯一提供多控制平面节点高可用集群的本地集群解决方案。它还被设计为可以作为库使用,但我觉得这并没有太大吸引力,因为通过代码自动化 CLI 非常容易。KinD 的主要缺点是它是临时性的。我建议如果你在为 Kubernetes 本身做贡献并想要进行相关测试时使用 KinD。更多信息请查看 kind.sigs.k8s.io/。
对我来说,K3d 是明显的赢家。速度非常快,支持多个集群以及每个集群中的多个工作节点。轻松停止和启动集群而不会丢失状态。查看 k3d.io/。
荣誉提名 – Rancher Desktop Kubernetes 集群
我使用 Rancher Desktop 作为我的 Docker 引擎提供商,但它还内置了一个 Kubernetes 集群。你无法自定义它,不能有多个集群,甚至同一个集群中也不能有多个节点。但如果你只需要一个本地的单节点 Kubernetes 集群来进行试验,那么rancher-desktop集群就适合你。
要使用此集群,请输入:
$ kubectl config use-context rancher-desktop
Switched to context "rancher-desktop".
你可以决定为其节点分配多少资源,这在尝试在其上部署大量工作负载时非常重要,因为你只有一个节点。

图 2.6:Rancher Desktop – Kubernetes 设置
在本节中,我们介绍了如何使用 Minikube、KinD 和 K3d 在本地创建 Kubernetes 集群。在下一节中,我们将探讨如何在云端创建集群。
在云中创建集群(GCP、AWS、Azure 和 Digital Ocean)
本地创建集群很有趣。它在开发过程中和尝试本地排查问题时也非常重要。但是,最终,Kubernetes 是为云原生应用程序(在云中运行的应用程序)设计的。Kubernetes 不希望知道单个云环境,因为这不具备可扩展性。因此,Kubernetes 有一个云提供商接口的概念。每个云提供商都可以实现此接口,然后托管 Kubernetes。
云提供商接口
云提供商接口是一个由 Go 数据类型和接口组成的集合。它定义在一个名为cloud.go的文件中,可以在以下位置找到:github.com/kubernetes/cloud-provider/blob/master/cloud.go。
这是主要界面:
type Interface interface {
Initialize(clientBuilder ControllerClientBuilder, stop <-chan struct{})
LoadBalancer() (LoadBalancer, bool)
Instances() (Instances, bool)
InstancesV2() (InstancesV2, bool)
Zones() (Zones, bool)
Clusters() (Clusters, bool)
Routes() (Routes, bool)
ProviderName() string
HasClusterID() bool
}
这非常清晰。Kubernetes 是基于实例、区域、集群和路由来运作的,还需要访问负载均衡器和提供商名称。主要接口主要是一个网关。上面Interface接口的大多数方法返回的是其他接口。
例如,Clusters()方法返回Cluster接口,使用起来非常简单:
type Clusters interface {
ListClusters(ctx context.Context) ([]string, error)
Master(ctx context.Context, clusterName string) (string, error)
}
ListClusters() 方法返回集群名称。Master() 方法返回集群控制平面的 IP 地址或 DNS 名称。
其他接口也没有更复杂。整个文件有 313 行(截至编写时),包括大量注释。关键点是,如果你的云环境利用了这些基本概念,实现 Kubernetes 提供商并不太复杂。
在云中创建 Kubernetes 集群
在我们查看云服务提供商及其对托管和非托管 Kubernetes 的支持之前,让我们先考虑一下你应该如何创建和维护集群。如果你决定使用一个云服务提供商,并且对使用他们的工具感到满意,那么你就准备好了。所有云服务提供商都允许你通过 Web UI、CLI 或 API 来创建和配置 Kubernetes 集群。然而,如果你更倾向于采用更通用的方法,并希望利用 GitOps 来管理集群,那么你应该研究如 Terraform 和 Pulumi 等基础设施即代码的解决方案。
如果你倾向于在云端部署非托管的 Kubernetes 集群,那么 kOps 是一个强有力的候选方案。请参见:kops.sigs.k8s.io。
在稍后的第十七章,在生产环境中运行 Kubernetes,我们将详细讨论多集群配置和管理的话题。这个领域有许多技术、开源项目和商业产品。
现在,让我们来看一下各种云服务提供商。
GCP
谷歌云平台(GCP)原生支持 Kubernetes。所谓的 Google Kubernetes Engine(GKE)是一个基于 Kubernetes 的容器管理解决方案。你无需在 GCP 上安装 Kubernetes,可以使用 Google Cloud API 来创建和配置 Kubernetes 集群。Kubernetes 是 GCP 的内建部分,这意味着它将始终与 GCP 深度集成,并经过充分测试,你不必担心底层平台的变化会破坏云服务提供商的接口。
如果你更倾向于自行管理 Kubernetes,那么你可以直接在 GCP 实例上部署它(或使用 kOps 的 GCP alpha 支持),但我一般不建议这么做,因为 GKE 为你做了很多工作,并且它与 GCP 的计算、网络和核心服务深度集成。
总的来说,如果你计划基于 Kubernetes 构建系统,并且没有在其他云平台上已有现有代码,那么 GCP 是一个可靠的选择。它在成熟度、精致度和与 GCP 服务的深度集成方面处于领先地位,并且通常是第一个更新到 Kubernetes 新版本的云平台。
我在 GKE 上花了很多时间,管理了几十个集群,升级它们并部署工作负载。GKE 毫无疑问是一个生产级的 Kubernetes 解决方案。
GKE 自动驾驶
GKE 还有一个名为 Autopilot 的项目,它为你管理工作节点和节点池,让你可以专注于部署和配置工作负载。
请参见:cloud.google.com/kubernetes-engine/docs/concepts/autopilot-overview。
AWS
AWS 有自己的容器管理服务,称为 ECS,它不是基于 Kubernetes 的。它也有一个托管的 Kubernetes 服务,称为 EKS。你可以在 AWS EC2 实例上自行运行 Kubernetes。我们先来谈谈如何自建 Kubernetes,然后再讨论 EKS。
在 EC2 上运行 Kubernetes
AWS 从一开始就是一个支持的云服务提供商。有大量的文档说明如何进行设置。尽管您可以自己配置一些 EC2 实例并使用 kubeadm 创建集群,但我建议使用之前提到的 kOps(Kubernetes 操作)项目。kOps 最初只支持 AWS,通常被认为是最经受考验且功能最丰富的工具,用于在 AWS 上自我配置 Kubernetes 集群(不使用 EKS)。
它支持以下功能:
-
自动化的 Kubernetes 集群 CRUD 操作(适用于 AWS)
-
高可用 Kubernetes 集群
-
使用状态同步模型进行干运行和自动幂等性
-
对 kubectl 插件的自定义支持
-
kOps 可以生成 Terraform 配置
-
基于在目录树中定义的简单元模型
-
简单的命令行语法
-
社区支持
要创建一个集群,您需要进行一些 IAM 和 DNS 配置,设置一个 S3 桶来存储集群配置,然后运行一个命令:
kops create cluster \
--name=${NAME} \
--cloud=aws \
--zones=us-west-2a \
--discovery-store=s3://prefix-example-com-oidc-store/${NAME}/discovery
完整的说明在这里:kops.sigs.k8s.io/getting_started/aws/。
2017 年底,AWS 加入了 CNCF,并发布了关于 Kubernetes 的两项重要公告:其基于 Kubernetes 的容器编排解决方案(EKS)和按需容器解决方案(Fargate)。
Amazon EKS
Amazon 弹性 Kubernetes 服务 (EKS) 是一个完全托管且高可用的 Kubernetes 解决方案。它有三个控制平面节点,分别运行在三个 AZ(可用区)。EKS 还会处理升级和补丁管理。EKS 的一个优点是它运行的是标准 Kubernetes。这意味着您可以使用社区开发的所有标准插件和工具。它还为与其他云服务提供商和/或您自己的本地 Kubernetes 集群进行便捷的集群联合打开了大门。
EKS 提供与 AWS 基础设施的深度集成,例如与 Kubernetes 基于角色的访问控制 (RBAC) 集成的 IAM 身份验证。
如果您希望直接从自己的 Amazon VPC 访问 Kubernetes 主节点,还可以使用 PrivateLink。通过 PrivateLink,您的 Kubernetes 控制平面和 Amazon EKS 服务终端将作为具有私有 IP 地址的弹性网络接口显示在您的 Amazon VPC 中。
另一个关键部分是一个特殊的 CNI 插件,允许您的 Kubernetes 组件使用 AWS 网络进行相互通信。
EKS 不断改进,亚马逊展示了其致力于保持更新和不断优化的承诺。如果您是 AWS 用户并开始使用 Kubernetes,我建议从 EKS 开始,而不是自行构建集群。
eksctl 工具是一个出色的 CLI,用于创建和管理 EKS 集群以及节点组,以便进行测试和开发。我使用 eksctl 成功创建、删除并向多个 AWS 上的 Kubernetes 集群添加节点。查看 eksctl.io/。
Fargate
Fargate 让你可以直接运行容器,无需担心硬件配置。它通过牺牲一些控制权,消除了大量的操作复杂性。在使用 Fargate 时,你将应用程序打包成容器,指定 CPU 和内存需求,定义网络和 IAM 策略,然后即可开始。Fargate 可以在 ECS 和 EKS 上运行。它是无服务器阵营中一个非常有趣的成员,尽管它不像 GKE 的 Autopilot 那样专门为 Kubernetes 定制。
Azure
Azure 曾经有一个基于 Mesos 的 DC/OS 或 Docker Swarm 的容器管理服务来管理你的容器。但你当然也可以使用 Kubernetes。你还可以自己配置集群(例如,使用 Azure 的所需状态配置),然后通过 kubeadm 创建 Kubernetes 集群。kOps 对 Azure 提供了 alpha 级支持,Kubespray 项目也是一个不错的选择。
然而,在 2017 年下半年,Azure 也加入了 Kubernetes 队列,并推出了 AKS(Azure Kubernetes 服务)。它与 Amazon EKS 类似,尽管其实现略微领先。
AKS 提供 Web UI、CLI 和 REST API 来管理你的 Kubernetes 集群。一旦 AKS 集群配置完成,你可以直接使用 kubectl 及任何其他 Kubernetes 工具。
以下是使用 AKS 的一些好处:
-
自动 Kubernetes 版本升级和补丁
-
简单的集群扩展
-
自愈的托管控制平面(主控节点)
-
成本节省 – 仅为运行代理池节点付费
AKS 还提供与 Azure 容器实例(ACI)的集成,类似于 AWS Fargate 和 GKE AutoPilot。这意味着不仅是你的 Kubernetes 集群的控制平面是托管的,连同工作节点也是。
Digital Ocean
Digital Ocean 并不是像 GCP、AWS、Azure 那样的大型云服务提供商,但它确实提供了托管的 Kubernetes 解决方案,并且在全球(美国、加拿大、欧洲、亚洲)都有数据中心。与其他选择相比,它也便宜得多,而成本通常是选择云服务提供商时的决定性因素。使用 Digital Ocean 时,控制平面不收费。除了更低的价格,Digital Ocean 还以简单性著称。
DOKS(数字海洋 Kubernetes 服务)为你提供了一个托管的 Kubernetes 控制平面(可以实现高可用性)并与 Digital Ocean 的 Droplets(用于节点和节点池)、负载均衡器和块存储卷进行集成。这满足了所有基本需求。你的集群当然符合 CNCF 标准。
Digital Ocean 将负责控制平面以及工作节点上的系统升级、安全补丁和已安装的包。
其他云服务提供商
GCP、AWS 和 Azure 是行业领先者,但也有很多其他公司提供托管的 Kubernetes 服务。一般来说,如果你已经与这些云服务提供商有显著的业务联系或集成,我推荐使用它们。
从前,在中国
如果你在中国运营并面临特殊的限制和约束,可能应该使用中国的云平台。主要有三家大平台:阿里巴巴、腾讯和华为。
中国的阿里巴巴云是云平台领域的后起之秀。它模仿了 AWS,尽管它的英文文档还有待提高。阿里巴巴云通过其 ACK(阿里云容器服务 for Kubernetes)支持 Kubernetes,并允许你:
-
运行你自己的专用 Kubernetes 集群(你必须创建 3 个主节点并对其进行升级和维护)
-
使用托管的 Kubernetes 集群(你只需要负责工作节点)
-
使用通过 ECI(弹性容器实例)提供的无服务器 Kubernetes 集群,类似于 Fargate 和 ACI。
ACK 是 CNCF 认证的 Kubernetes 发行版。如果你需要在中国部署云原生应用,ACK 看起来是一个可靠的选择。
查看 www.alibabacloud.com/product/kubernetes。
腾讯是另一家大型中国公司,拥有自己的云平台并支持 Kubernetes。TKE(腾讯 Kubernetes 引擎)似乎不如 ACK 成熟。查看 intl.cloud.tencent.com/products/tke。
最后,华为云平台提供 CCE(云容器引擎),这是一个基于 Kubernetes 构建的服务。它支持虚拟机、裸金属和 GPU 加速实例。查看 www.huaweicloud.com/intl/en-us/product/cce.html。
IBM Kubernetes 服务
IBM 正在大力投资 Kubernetes。2018 年底,它收购了 Red Hat。Red Hat 当然是 Kubernetes 世界的主要参与者,构建了基于 Kubernetes 的 OpenShift 平台并为 Kubernetes 做出了 RBAC 的贡献。IBM 拥有自己的云平台,并提供了一个托管的 Kubernetes 集群。你可以通过 $200 的信用额度免费试用,也有免费套餐。
IBM 也参与了 Istio 和 Knative 的开发,因此你可以期待 IKS 与这些技术的深度集成。
IKS 提供与众多 IBM 服务的集成。
查看 www.ibm.com/cloud/kubernetes-service。
Oracle 容器服务
Oracle 也有云平台,并且当然也提供托管的 Kubernetes 服务,具备高可用性、裸金属实例和多可用区支持。
OKE 支持 ARM 和 GPU 实例,并提供一些控制平面选项。
查看 www.oracle.com/cloud/cloud-native/container-engine-kubernetes/。
在这一部分中,我们讨论了云服务商接口,并查看了在不同云服务商上创建 Kubernetes 集群的推荐方式。这个场景仍然很年轻,工具也在迅速发展。我相信很快就会发生融合。Kubeadm 已经成熟,并且成为了许多其他工具在云内外启动和创建 Kubernetes 集群的基础。现在让我们考虑一下,创建裸金属集群需要什么,其中你还得自行配置硬件、低层次的网络和存储。
从零开始创建裸金属集群
在前一部分中,我们讨论了如何在云服务商上运行 Kubernetes。这是 Kubernetes 的主要部署方式。但在一些场景下,运行 Kubernetes 在裸金属上也有很强的应用需求,比如在边缘计算上使用 Kubernetes。我们这里不重点讨论托管与本地部署的区别,这是另一个维度。如果你已经在本地管理大量服务器,你就处在最佳位置来做出决策。
裸金属的使用场景
裸金属集群确实难以处理,特别是当你自己管理它们时。有些公司提供裸金属 Kubernetes 集群的商业支持,比如 Platform 9,但这些服务尚不成熟。一个稳健的开源选择是 Kubespray,它可以在裸金属、AWS、GCE、Azure 和 OpenStack 上部署工业级的 Kubernetes 集群。
这里有一些适合裸金属的使用场景:
-
价格:如果你已经管理了大规模的裸金属集群,在你的物理基础设施上运行 Kubernetes 集群可能会便宜得多
-
低网络延迟:如果你必须确保节点之间的低延迟,那么虚拟机的开销可能会过大
-
合规要求:如果你必须遵守某些法规,可能就不能使用云服务商
-
你想完全控制硬件:云服务提供商给了你许多选项,但你可能有特殊的需求
什么时候应该考虑创建裸金属集群?
从零开始创建集群的复杂性是显著的。一个 Kubernetes 集群并不是一个简单的东西。网上有大量关于如何设置裸金属集群的文档,但随着整个生态系统的发展,很多这些指南很快就会过时。
如果你有足够的操作能力去排查堆栈各层次的问题,那么你应该考虑走这条路。大部分问题可能与网络相关,但文件系统和存储驱动也可能会带来麻烦,还可能遇到组件之间的兼容性问题或版本不匹配,比如 Kubernetes 本身、Docker(或者其他运行时,如果你使用它们)、镜像、操作系统、操作系统内核以及你使用的各种插件和工具。如果你选择在裸金属上使用虚拟机,那么你还会增加一层复杂性。
理解这个过程
有很多事情需要做。以下是你需要解决的一些问题:
-
实现你自己的云服务商接口或绕过它
-
选择网络模型及其实现方式(CNI 插件、直接编译)
-
是否使用网络策略
-
选择系统组件的镜像
-
安全模型和 SSL 证书
-
管理员凭证
-
组件模板,如 API 服务器、复制控制器和调度器
-
集群服务:DNS、日志、监控和 GUI
我推荐 Kubernetes 网站上的以下指南,以深入了解使用 kubeadm 从零开始创建高可用集群的过程:
kubernetes.io/docs/setup/production-environment/tools/kubeadm/high-availability/
使用 Cluster API 管理裸金属集群
Cluster API(也叫 CAPI)是一个 Kubernetes 子项目,用于大规模管理 Kubernetes 集群。它使用 kubeadm 进行配置。它可以在任何环境中使用提供程序来配置和管理 Kubernetes 集群。在工作中,我们使用它来管理云中的多个集群。但它也有多个提供程序支持裸金属集群:
-
MAAS
-
Equinix metal
-
Metal3
-
Cidero
使用虚拟私有云基础设施
如果您的使用场景属于裸金属用例,但您没有必要的技术人员或不愿意处理裸金属的基础设施挑战,您可以选择使用 OpenStack 等私有云。如果您希望在抽象层次上更进一步,那么 Mirantis 提供了一个建立在 OpenStack 和 Kubernetes 之上的云平台。
让我们回顾一下在裸金属上构建 Kubernetes 集群的一些工具。这些工具中的一些也支持 OpenStack。
使用 Kubespray 构建自己的集群
Kubespray 是一个用于部署生产就绪的高可用性 Kubernetes 集群的项目。它使用 Ansible 并可以在大量目标上部署 Kubernetes,例如:
-
AWS
-
GCE
-
Azure
-
OpenStack
-
vSphere
-
Equinix metal
-
Oracle Cloud Infrastructure(实验性)
它也用于在普通裸金属机器上部署 Kubernetes 集群。
它高度可定制,支持多种操作系统节点、多个 CNI 插件用于网络连接,以及多个容器运行时。
如果您想在本地测试,它也可以部署到多节点的 Vagrant 设置中。如果您是 Ansible 的粉丝,Kubespray 可能是一个不错的选择。
请参见 kubespray.io。
使用 Rancher RKE 构建集群
Rancher Kubernetes Engine(RKE)是一个友好的 Kubernetes 安装器,可以在裸金属和虚拟化服务器上安装 Kubernetes。RKE 旨在解决安装 Kubernetes 的复杂性。它是开源的,并且有很好的文档。您可以在这里查看:rancher.com/docs/rke/v0.1.x/en/。
在裸金属或虚拟机上运行托管 Kubernetes
云服务商不希望仅限于他们自己的云平台。它们都提供了多云和混合云解决方案,让你可以在多个云平台上管理 Kubernetes 集群,并且可以在任何地方使用它们的托管控制平面。
GKE Anthos
Anthos 是一个全面的托管平台,便于应用程序的部署,涵盖了传统和云原生环境。它使你能够构建和管理全球范围的应用程序队列,同时确保它们之间的操作一致性。
EKS Anywhere
Amazon EKS Anywhere 提供了一种全新的 Amazon EKS 部署选项,允许你在自己的基础设施上建立和管理 Kubernetes 集群,并获得 AWS 支持。它使你可以在自己的本地基础设施上运行 Amazon EKS Anywhere,利用 VMware vSphere 以及裸金属环境。
AKS Arc
Azure Arc 包含一系列技术,将 Azure 的安全性和云原生服务扩展到混合云和多云环境。它使你能够在不同位置保护和管理基础设施和应用程序,同时提供熟悉的工具和服务,加速云原生应用的开发。这些应用可以在任何 Kubernetes 平台上部署。
在本节中,我们介绍了创建裸金属 Kubernetes 集群的方法,它可以让你完全控制,但同时也非常复杂,需要大量的精力和知识。幸运的是,已经有多个工具、项目和框架可以为你提供帮助。
总结
在本章中,我们进行了实际的集群创建操作。我们使用了 Minikube、KinD 和 k3d 等工具创建了单节点和多节点集群。然后,我们了解了在云服务提供商上创建 Kubernetes 集群的各种选项。最后,我们探讨了在裸金属上创建 Kubernetes 集群的复杂性。当前的局势非常动态,基础组件变化迅速,工具也在不断改进,每种环境都有不同的选项。Kubeadm 现在是大多数安装选项的基石,这对保持一致性和整合工作非常有帮助。虽然在自己的机器上搭建 Kubernetes 集群仍然不是完全简单的事情,但通过一些努力和细致的关注,你可以迅速完成。
我强烈建议考虑将 Cluster API 作为任何环境中集群配置和管理的首选解决方案——无论是托管的、私有云、虚拟机,还是裸金属环境。我们将在第十七章《在生产环境中运行 Kubernetes》中深入讨论 Cluster API。
在下一章中,我们将探讨可扩展性和高可用性这两个重要话题。一旦你的集群启动并运行,你需要确保它能保持稳定,即使请求量增加。这需要持续关注,并建立从故障中恢复的能力,同时调整流量变化。
加入我们的 Discord 群!
与其他用户、云计算专家、作者以及志同道合的专业人士一起阅读本书。
提问、为其他读者提供解决方案、通过“问我任何问题”环节与作者互动,还有更多内容。
扫描二维码或访问链接立即加入社区。

第三章:高可用性与可靠性
在第二章,创建 Kubernetes 集群中,我们学习了如何在不同环境中创建 Kubernetes 集群,尝试了不同的工具,并创建了几个集群。创建 Kubernetes 集群只是故事的开始。一旦集群启动并运行,你需要确保它保持正常运作。
在本章中,我们将深入探讨高可用集群的话题。这是一个复杂的话题。Kubernetes 项目和社区还没有确定一种真正的方式来实现高可用性。高可用 Kubernetes 集群有许多方面,例如确保控制平面在故障面前能够持续运行、保护 etcd 中的集群状态、保护系统数据以及快速恢复容量和/或性能。不同的系统将有不同的可靠性和可用性要求。如何设计和实现一个高度可用的 Kubernetes 集群将取决于这些要求。
本章将探讨以下主要主题:
-
高可用性概念
-
高可用性最佳实践
-
高可用性、可扩展性和容量规划
-
大型集群的性能、成本和设计权衡
-
选择和管理集群容量
-
推动 Kubernetes 的极限
-
在大规模上测试 Kubernetes
在本章结束时,你将理解与高可用性相关的各种概念,并熟悉 Kubernetes 的高可用性最佳实践以及何时使用它们。你将能够使用不同的策略和技术升级实时集群,并能够根据性能、成本和可用性之间的权衡,选择多种可能的解决方案。
高可用性概念
在这一部分,我们将通过探索可靠且高度可用的系统的概念和构建模块,开始我们的高可用性之旅。百万(万亿?)美元的问题是,我们如何从不可靠的组件中构建可靠且高度可用的系统?组件会失败,这一点你可以放心。硬件会故障,网络会中断,配置会出错,软件会有 bug,人们会犯错。接受这一点后,我们需要设计一个即使在组件失败时也能保持可靠和高度可用的系统。我们的思路是,从冗余开始,检测组件故障,并快速替换坏掉的组件。
冗余
冗余是硬件和软件层面上可靠且高度可用系统的基础。如果一个关键组件发生故障,并且你希望系统继续运行,你必须准备另一个相同的组件。Kubernetes 本身通过复制控制器和副本集来管理你的无状态 Pod。但你的 etcd 集群状态和控制平面组件本身需要冗余,以便在某些组件故障时继续运行。实际操作中,这意味着需要在 3 个或更多节点上运行 etcd 和 API 服务器。
此外,如果你的系统的有状态组件还没有通过冗余的持久化存储进行备份(例如,在云平台上),你需要添加冗余以防止数据丢失。
热插拔
热插拔是指在不中断系统的情况下,快速更换一个失败的组件,并尽量减少(理想情况下为零)对用户的影响。如果该组件是无状态的(或者它的状态存储在单独的冗余存储中),那么热插拔一个新组件来替换它很容易,只需将所有客户端重定向到新组件即可。但如果它存储了本地状态,包括内存中的状态,那么热插拔就不是一件简单的事。主要有两种选择:
-
放弃正在进行的事务(客户端将重试)
-
保持热备副本同步(主动-主动)
第一个解决方案要简单得多。大多数系统足够具备弹性来应对故障。客户端可以重试失败的请求,热插拔的组件将为其提供服务。
第二个解决方案更复杂且更脆弱,并且会带来性能开销,因为每个交互都必须复制到两个副本(并且需要确认)。它可能是系统某些关键部分所必需的。
领导者选举
领导者选举是分布式系统中常见的模式。你通常有多个相同的组件,它们协作并分担负载,但会选举出一个组件作为领导者,某些操作会通过领导者进行序列化。你可以把带有领导者选举的分布式系统看作是冗余和热插拔的结合体。所有组件都是冗余的,当当前领导者失败或不可用时,会选举出一个新的领导者并进行热插拔。
智能负载均衡
负载均衡是将工作负载分配到多个副本上,以处理传入的请求。这对于在负载较重时通过调整副本数量进行扩展和收缩非常有用。当一些副本失败时,负载均衡器会停止向失败或无法访问的组件发送请求。Kubernetes 会配置新的副本,恢复容量,并更新负载均衡器。Kubernetes 提供了出色的设施来支持这一点,通过服务、端点、副本集、标签和入口控制器。
幂等性
许多类型的故障可能是暂时性的。这在网络问题或超时过于严格时最为常见。未响应健康检查的组件将被视为不可达,另一个组件将取而代之。原本为假定失败的组件安排的工作可能会发送到另一个组件。但是,原始组件可能仍在工作并完成相同的任务。最终的结果是相同的工作可能会执行两次。要避免这种情况非常困难。为了支持精确一次语义,你需要在开销、性能、延迟和复杂性方面付出很大的代价。因此,大多数系统选择支持至少一次语义,这意味着相同的工作可以多次执行,而不会破坏系统的数据完整性。这个特性称为幂等性。幂等系统即使执行操作多次,也能保持其状态。
自愈
当动态系统中的组件发生故障时,通常希望系统能够自我修复。Kubernetes 的副本集就是自愈系统的典型例子。但是,故障可能远远超出 pod 的范围。自愈从自动检测问题开始,接着进行自动修复。配额和限制帮助创建检查与平衡,以确保自动化自愈不会因程序错误或诸如 DDoS 攻击等特殊情况而失控。自愈系统通过重试失败的操作来很好地处理暂时性故障,并且只有在没有其他选项时才会升级故障。一些自愈系统具有回退路径,包括在无法获取最新内容时提供缓存内容。自愈系统尽力优雅降级,并在核心问题修复之前继续工作。
在本节中,我们讨论了构建可靠和高度可用系统所涉及的各种概念。在下一节中,我们将应用这些概念,并演示在 Kubernetes 集群上部署系统的最佳实践。
高可用性最佳实践
构建可靠且高度可用的分布式系统是一项复杂的工作。在本节中,我们将检查一些最佳实践,这些最佳实践使基于 Kubernetes 的系统能够在面对各种故障类别时可靠运行并保持可用性。我们还将深入探讨如何构建你自己的高可用集群。然而,由于高可用集群的复杂性以及影响因素的众多,我们这里只提供指导,而不是逐步的构建高可用集群的教程。
请注意,你应该仅在非常特殊的情况下自己构建高度可用的 Kubernetes 集群。有多种强大的工具(通常是建立在 kubeadm 基础上)提供经过实战验证的方法,在控制平面级别创建高度可用的 Kubernetes 集群。你应该充分利用这些工具所投入的所有工作和努力。特别是,云服务提供商提供的托管 Kubernetes 集群具有高度可用性。
创建高度可用的集群
为了创建一个高度可用的 Kubernetes 集群,控制平面组件必须具备冗余性。这意味着 etcd 必须作为集群部署(通常跨三个或五个节点),并且 Kubernetes API 服务器必须具有冗余。辅助的集群管理服务,如可观察性堆栈存储,也应当冗余部署。以下图示描绘了一个典型的可靠且高度可用的 Kubernetes 集群,其采用堆叠的 etcd 拓扑结构。这里有多个负载均衡的控制平面节点,每个节点都包含所有控制平面组件以及一个 etcd 组件:

图 3.1:高度可用的集群配置
这并不是配置高度可用集群的唯一方法。例如,你可能更倾向于部署一个独立的 etcd 集群,以优化机器的工作负载,或者如果你需要比其他控制平面节点更高的冗余性来保护你的 etcd 集群。
以下图示展示了一个 Kubernetes 集群,其中 etcd 被部署为外部集群:

图 3.2:etcd 用作外部集群
自托管的 Kubernetes 集群,其中控制平面组件作为 pods 和 stateful sets 部署在集群中,是一种很好的方法,通过将 Kubernetes 应用到 Kubernetes 来简化控制平面组件的可靠性、灾难恢复和自我修复。这意味着一些管理 Kubernetes 的组件本身也由 Kubernetes 来管理。例如,如果某个 Kubernetes API 服务器节点发生故障,其他 API 服务器 pods 会察觉到并启动一个新的 API 服务器。
提高节点的可靠性
节点可能会失败,或者某些组件可能会失败,但许多故障是暂时的。基本的保证是确保运行时引擎(Docker 守护进程、Containerd 或任何 CRI 实现)和 kubelet 在故障发生时能够自动重启。
如果你运行的是 CoreOS、基于现代 Debian 的操作系统(包括 Ubuntu >= 16.04),或者任何其他使用 systemd 作为初始化机制的操作系统,那么部署 Docker 和 kubelet 作为自启动守护进程是非常简单的:
systemctl enable docker
systemctl enable kubelet
对于其他操作系统,Kubernetes 项目选择了monit进程监视器作为高可用性示例,但你可以使用任何你喜欢的进程监视器。主要要求是确保这两个关键组件在发生故障时会自动重启,无需外部干预。
查看此链接:monit-docs.web.cern.ch/base/kubernetes/。
保护你的集群状态
Kubernetes 集群状态通常存储在 etcd 中(某些 Kubernetes 实现,如 k3s,使用像 SQLite 这样的替代存储引擎)。etcd 集群设计时考虑了超高的可靠性,并且分布在多个节点上。为了确保 Kubernetes 集群的可靠性和高可用性,利用这些功能是至关重要的,确保有多个副本存储集群状态,以防某个副本丢失或不可达。
集群化 etcd
你的 etcd 集群应该至少有三个节点。如果需要更高的可靠性和冗余,可以选择五个、七个或任何其他奇数节点。节点数必须是奇数,以便在发生网络分裂时能有明确的多数节点。
为了创建一个集群,etcd 节点应该能够互相发现。实现这一点有多种方法,比如:
-
静态
-
etcd 发现
-
DNS 发现
CoreOS 的 etcd-operator 项目曾是部署 etcd 集群的首选解决方案。不幸的是,该项目已被归档,目前不再积极开发。Kubeadm 使用静态方法来配置 Kubernetes 的 etcd 集群,因此如果你使用任何基于 kubeadm 的工具,你就可以顺利完成。如果你想部署一个高可用的 etcd 集群,建议你遵循官方文档:github.com/etcd-io/etcd/blob/release-3.4/Documentation/op-guide/clustering.md。
保护你的数据
保护集群状态和配置固然重要,但更重要的是保护你自己的数据。如果集群状态 somehow 受损,你总是可以从头重建集群(尽管在重建期间集群不可用)。但如果你自己的数据受损或丢失,你就会陷入深深的麻烦。相同的规则依然适用,冗余是王道。虽然 Kubernetes 集群状态是高度动态的,但你的数据可能不那么动态。例如,很多历史数据通常很重要,可以备份并恢复。实时数据可能会丢失,但整个系统可以恢复到较早的快照,并且仅遭受暂时性损坏。
你应该考虑使用 Velero 作为备份整个集群(包括你的数据)的解决方案。Heptio(现在是 VMWare 的一部分)开发了 Velero,它是开源的,并且对于关键系统来说可能是救命稻草。
在这里查看:velero.io/。
运行冗余的 API 服务器
API 服务器是无状态的,所有必要的数据都实时从 etcd 集群中获取。这意味着你可以轻松运行多个 API 服务器,而无需在它们之间进行协调。一旦你运行了多个 API 服务器,可以在它们前面放置一个负载均衡器,使客户端对其透明。
使用 Kubernetes 运行领导选举
一些控制平面组件,例如调度器和控制器管理器,不能同时有多个实例处于活动状态。这将导致混乱,因为多个调度器会试图将同一个 Pod 调度到多个节点,或者将多个调度请求调度到同一个节点。可以运行多个调度器,并配置它们管理不同的 Pods。实现高可扩展 Kubernetes 集群的正确方法是让这些组件在领导选举模式下运行。这意味着多个实例同时运行,但只有一个处于活动状态,如果它失败,另一个将被选举为领导并接替其位置。
Kubernetes 通过 –leader-elect 标志支持此模式(默认值为 True)。调度器和控制器管理器可以通过将它们各自的清单复制到 /etc/kubernetes/manifests 目录中,作为 Pods 部署。
下面是一个来自调度器清单的代码片段,展示了如何使用该标志:
command:
- /bin/sh
- -c
- /usr/local/bin/kube-scheduler --master=127.0.0.1:8080 --v=2 --leader-elect=true 1>>/var/log/kube-scheduler.log
2>&1
下面是一个来自控制器管理器清单的代码片段,展示了如何使用该标志:
- command:
- /bin/sh
- -c
- /usr/local/bin/kube-controller-manager --master=127.0.0.1:8080 --cluster-name=e2e-test-bburns
--cluster-cidr=10.245.0.0/16 --allocate-node-cidrs=true --cloud-provider=gce --service-account-private-key-file=/srv/kubernetes/server.key
--v=2 --leader-elect=true 1>>/var/log/kube-controller-manager.log 2>&1
image: gcr.io/google\_containers/kube-controller-manager:fda24638d51a48baa13c35337fcd4793
还有几个标志可用来控制领导选举。它们都有合理的默认值:
--leader-elect-lease-duration duration Default: 15s
--leader-elect-renew-deadline duration Default: 10s
--leader-elect-resource-lock endpoints Default: "endpoints" ("configmaps" is the other option)
--leader-elect-retry-period duration Default: 2s
请注意,Kubernetes 不可能像其他 Pods 那样自动重启这些组件,因为它们恰恰是负责重启失败 Pods 的 Kubernetes 组件,因此它们无法在失败时自行重启。必须有一个已准备好的替代实例在运行。
使你的暂存环境具备高可用性
高可用性并不是一个简单的配置。如果你花费时间去设置高可用性,那就意味着你的系统有高可用性的业务需求。因此,在将其部署到生产环境之前,你需要测试你的可靠性和高可用性集群(除非你是 Netflix,那里是在生产环境中测试的)。另外,集群中的任何更改在理论上可能会破坏高可用性,而不会影响其他集群功能。关键点是,就像其他任何事情一样,如果你不测试它,就假设它无法正常工作。
我们已经确认你需要测试可靠性和高可用性。最好的方法是创建一个尽可能接近生产环境的暂存环境。这可能会变得很昂贵。这里有几种方法来管理成本:
-
临时高可用性暂存环境:只为高可用性测试的期间创建一个大型高可用性集群。
-
压缩时间:提前创建有趣的事件流和场景,输入这些数据,并快速模拟各种情况。
-
将 HA 测试与性能和压力测试结合起来:在你的性能和压力测试结束时,对系统进行超负荷测试,看看可靠性和高可用性配置如何处理负载。
同时,实践混沌工程并故意在不同层次上引发故障也非常重要,以验证系统是否能够处理这些故障模式。
测试高可用性
测试高可用性需要规划和对系统的深入理解。每个测试的目标是揭示系统设计和/或实现中的缺陷,并提供足够的覆盖范围,以便如果测试通过,你可以确信系统按照预期行为运行。
在可靠性、自愈和高可用性的领域,这意味着你需要找到方法来破坏系统并观察它如何自我修复。
这需要几个部分,如下所示:
-
可能故障的全面清单(包括合理的组合情况)
-
对于每一种可能的故障,应该清楚地知道系统应该如何响应。
-
诱发故障的方法
-
一种观察系统反应的方法。
没有哪一部分是微不足道的。根据我的经验,最好的方法是逐步进行,尝试提出相对较少的通用故障类别和通用响应,而不是列出一个详尽的、不断变化的低级故障清单。
例如,一个通用的故障类别是节点无响应。通用的响应可以是重启节点,诱发故障的方法可以是停止节点的虚拟机(如果它是虚拟机),观察应当是,在节点停机期间,系统仍然能够通过标准验收测试正常运行;节点最终恢复,系统恢复正常。你可能还想测试其他很多事情,比如问题是否被记录,是否有相关的警报发送给了正确的人,是否更新了各种统计信息和报告。
但是,要小心过度泛化。在通用的无响应节点故障模式下,一个关键的组成部分是检测节点是否无响应。如果你的检测方法存在缺陷,那么系统将无法做出正确反应。请使用健康检查和就绪检查等最佳实践。
请注意,有时一个故障无法通过单一响应来解决。例如,在我们节点无响应的案例中,如果是硬件故障,那么重启不会有所帮助。在这种情况下,第二线响应就会发挥作用,可能会通过提供新的节点来替代故障的节点。在这种情况下,你不能过于泛化,可能需要为节点上特定类型的 Pods/角色(例如 etcd、master、worker、数据库和监控)创建测试。
如果你有高质量的需求,请准备花费比生产环境更多的时间来设置适当的测试环境和测试。
最后一个重要的观点是尽量保持非侵入性。这意味着理想情况下,您的生产系统不应具有允许关闭其部分或使其配置为在测试中以降低容量运行的测试功能。原因是这会增加系统的攻击面,并且可能会因配置错误而意外触发。理想情况下,您可以在不修改将部署在生产中的代码或配置的情况下控制测试环境。使用 Kubernetes,通常可以轻松地注入具有自定义测试功能的 Pod 和容器,这些功能可以与演示环境中的系统组件交互,但永远不会部署到生产环境中。
Chaos Mesh CNCF 孵化项目是一个很好的起点:chaos-mesh.org。
在这一部分,我们看了如何确保一个可靠且高可用的集群,包括 etcd、API 服务器、调度器和控制器管理器。我们考虑了保护集群本身和您的数据的最佳实践,并特别关注了启动环境和测试的问题。
高可用性、可扩展性和容量规划
高可用系统还必须具备可扩展性。大多数复杂的分布式系统的负载可能会根据一天中的时间、工作日与周末、季节性影响、营销活动以及许多其他因素而显著变化。成功的系统会随着时间推移拥有更多用户并积累更多数据。这意味着集群的物理资源 - 主要是节点和存储 - 也必须随着时间增长。如果您的集群配置不足,它将无法满足所有需求,并且由于请求超时或排队等待而无法提供服务。
这就是容量规划的领域。一种简单的方法是过度配置您的集群。预测需求并确保您有足够的缓冲区以处理活动高峰。但是,这种方法存在几个不足之处:
-
对于高度动态和复杂的分布式系统,即使是大致预测需求也很困难。
-
过度配置是昂贵的。您会在很少或从不使用的资源上花费大量资金。
-
您必须定期重新执行整个过程,因为系统的平均和峰值负载随时间而变化。
-
您必须为使用特定资源的多组工作负载执行整个过程(例如使用高内存节点的工作负载和需要 GPU 的工作负载)。
更好的方法是使用基于意图的容量规划,其中使用高级抽象,系统会相应地调整自己。在 Kubernetes 的环境中,有水平 Pod 自动伸缩器(HPA),它可以根据工作负载请求的需要增加或减少所需的 Pod 数量。但是,这仅适用于更改分配给不同工作负载的资源比例。当整个集群(或节点池)接近饱和时,您只需增加更多资源。这就是集群自动缩放器发挥作用的地方。它是一个 Kubernetes 项目,在 Kubernetes 1.8 版本中推出。它在云环境中特别有效,可以通过编程 API 提供额外的资源。
当集群自动缩放器(CAS)确定无法为 Pod 安排节点(处于挂起状态)时,它会为集群提供一个新节点。如果确定集群的节点多于处理负载所需的节点,它还可以从集群中删除节点(缩减)。CAS 默认每 30 秒检查一次挂起的 Pod。仅在低使用率持续 10 分钟后才会删除节点,以避免频繁变动。
CAS 根据 CPU 和内存使用情况做出缩减决策。如果所有运行在节点上的 Pod 的 CPU 和内存请求总和小于节点可分配资源的 50%(默认可配置),则会考虑删除该节点。所有 Pod(除了 DaemonSet Pod)必须是可移动的(某些 Pod 由于调度约束或本地存储等因素无法移动),并且节点不能禁用缩减。
以下是需要考虑的一些问题:
-
即使总 CPU 或内存利用率较低,由于诸如亲和性、反亲和性、污点、容忍度、Pod 优先级、每节点最大 Pod 数量、每节点最大持久卷数量和 Pod 中断预算等控制机制,集群可能仍需要更多节点。
-
除了触发节点扩展或缩减的内置延迟外,从云服务提供商预配新节点还会增加几分钟的额外延迟。
-
有些节点(例如带有本地存储的节点)默认情况下无法删除(需要特殊标注)。
-
HPA 和 CAS 之间的交互可能会有些微妙。
安装集群自动缩放器
请注意,您无法在本地测试 CAS。您必须在支持的云服务提供商之一上运行 Kubernetes 集群:
-
AWS
-
BaiduCloud
-
Brightbox
-
CherryServers
-
CloudStack
-
HuaweiCloud
-
外部 gRPC
-
Hetzner
-
Equinix Metal
-
IonosCloud
-
OVHcloud
-
Linode
-
OracleCloud
-
ClusterAPI
-
BizflyCloud
-
Vultr
-
TencentCloud
我已成功在 GKE、EKS 和 AKS 上安装了它。在云环境中使用 CAS 的两个原因如下:
-
您自行安装了非托管 Kubernetes,并希望从 CAS 中受益。
-
您正在使用托管的 Kubernetes,但希望修改一些设置(例如更高的 CPU 利用率阈值)。在这种情况下,您需要禁用云服务提供商的 CAS 以避免冲突。
让我们看一下在 AWS 上安装 CAS 的清单。有几种方法可以做到这一点。我选择了多 ASG(自动扩缩组选项),这是最适合生产的。它支持具有不同配置的多个节点组。该文件包含安装集群自动扩缩器所需的所有 Kubernetes 资源。它涉及创建一个服务账户,并授予它各种 RBAC 权限,因为它需要监控整个集群的节点使用情况,并能够对其进行操作。最后,还有一个 Deployment,实际部署集群自动扩缩器镜像,并附带一个命令行,指定应维护的节点范围(最小和最大数量),在 EKS 的情况下,还需要节点组。最大数量非常重要,以防攻击或错误导致集群自动扩缩器不断添加节点,从而失控并造成巨大的账单。完整文件请见:github.com/kubernetes/autoscaler/blob/master/cluster-autoscaler/cloudprovider/aws/examples/cluster-autoscaler-multi-asg.yaml。
下面是 Deployment 的 pod 模板片段:
spec:
priorityClassName: system-cluster-critical
securityContext:
runAsNonRoot: true
runAsUser: 65534
fsGroup: 65534
serviceAccountName: cluster-autoscaler
containers:
- image: k8s.gcr.io/autoscaling/cluster-autoscaler:v1.22.2
name: cluster-autoscaler
resources:
limits:
cpu: 100m
memory: 600Mi
requests:
cpu: 100m
memory: 600Mi
command:
- ./cluster-autoscaler
- --v=4
- --stderrthreshold=info
- --cloud-provider=aws
- --skip-nodes-with-local-storage=false
- --expander=least-waste
- --nodes=1:10:k8s-worker-asg-1
- --nodes=1:3:k8s-worker-asg-2
volumeMounts:
- name: ssl-certs
mountPath: /etc/ssl/certs/ca-certificates.crt #/etc/ssl/certs/ca-bundle.crt for Amazon Linux Worker Nodes
readOnly: true
imagePullPolicy: "Always"
volumes:
- name: ssl-certs
hostPath:
path: "/etc/ssl/certs/ca-bundle.crt"
HPA 和 CAS 的结合提供了一个真正弹性的集群,其中 HPA 确保服务使用适当数量的 pods 来处理每个服务的负载,而 CAS 确保节点的数量与集群的整体负载匹配。
考虑垂直 pod 自动缩放器
垂直 pod 自动缩放器是另一个作用于 pods 的自动缩放器。它的作用是根据实际使用情况调整 pods 的 CPU 和内存请求与限制。它通过 CRD 为每个工作负载配置,并具有三个组件:
-
推荐器 - 监控 CPU 和内存使用情况,并为 CPU 和内存请求提供新值的推荐
-
更新器 - 终止那些其 CPU 和内存请求不符合推荐值的受管 pods
-
Admission control webhook - 根据推荐值设置新建或重建 pods 的 CPU 和内存请求
VPA 可以仅在推荐模式下运行,也可以主动调整 pods 的大小。当 VPA 决定调整 pod 大小时,它会驱逐该 pod。重新调度 pod 时,它会根据最新的推荐修改请求和限制。
这是一个定义在推荐模式下的 VPA 自定义资源的示例,针对名为 awesome-deployment 的 Deployment:
apiVersion: autoscaling.k8s.io/v1beta2
kind: VerticalPodAutoscaler
metadata:
name: awesome-deployment
spec:
targetRef:
apiVersion: "apps/v1"
kind: Deployment
name: awesome-deployment
updatePolicy:
updateMode: "Off"
以下是使用 VPA 时的一些主要要求和限制:
-
需要 metrics server
-
不能将内存设置为低于 250Mi
-
无法更新正在运行的 Pod(因此,更新器会杀死 Pod 以便重新启动它们并使用正确的请求)
-
无法驱逐未由控制器管理的 Pod
-
不建议将 VPA 与 HPA 一起运行
基于自定义指标的自动扩展
HPA 默认基于 CPU 和内存指标进行操作。但它可以配置为基于任意自定义指标运行,例如队列深度(例如 AWS SQS 队列)或线程数,这些可能由于并发性成为瓶颈,即使仍有可用的 CPU 和内存。Keda 项目(keda.sh/)为自定义指标提供了强大的解决方案,而不是从零开始。它们使用基于事件的自动扩展作为一种通用方法。
本节讨论了自动扩展性与高可用性之间的相互作用,并探讨了扩展 Kubernetes 集群和这些集群上运行的应用程序的不同方法。
大型集群的性能、成本和设计权衡
在上一节中,我们探讨了为容量规划和自动扩展集群及工作负载提供资源的不同方法。在这一节中,我们将考虑具有不同可靠性和高可用性属性的大型集群的各种选项和配置。当你设计集群时,你需要了解你的选项,并根据组织的需求明智地选择。
我们将讨论的主题包括各种可用性要求,从最佳努力到零停机的“圣杯”。最后,我们将落脚于实际的站点可靠性工程方法。对于每个可用性类别,我们将从性能和成本的角度考虑它的含义。
可用性要求
不同的系统对可靠性和可用性有非常不同的要求。此外,不同的子系统也有非常不同的要求。例如,计费系统通常优先级较高,因为如果计费系统宕机,就无法赚取收入。但即使在计费系统内部,如果偶尔无法进行费用争议处理,从业务角度来看,可能也是可以接受的。
最佳努力
最佳努力意味着,反直觉地,它根本没有任何保证。如果它有效,那就太好了!如果不起作用——那就算了,你能做什么呢?这种可靠性和可用性水平可能适用于经常变动的内部组件,而使其稳健的努力是不值得的。只要调用不可靠服务的服务或客户端能够处理偶尔的错误或宕机,那么一切都很好。这对于以 beta 版本发布到外部的服务也可能是合适的。
最佳努力模式对开发人员来说是很有利的。开发人员可以快速行动并打破常规。他们不必担心后果,也不需要经过严格的测试和批准过程。最佳努力服务的性能可能优于更稳健的服务,因为最佳努力服务通常可以跳过一些昂贵的步骤,例如验证请求、持久化中间结果和数据复制。但另一方面,更稳健的服务通常会经过大量优化,且其支持硬件已针对工作负载进行了精细调校。最佳努力服务的成本通常较低,因为它们不需要采用冗余,除非运营商忽视了基本的容量规划,导致过度配置。
在 Kubernetes 的背景下,关键问题是集群提供的所有服务是否都是最佳努力模式。如果是这样,那么集群本身就不需要高度可用。您可能只需要一个主节点和一个 etcd 实例,甚至可能不需要部署监控解决方案。这通常仅适用于本地开发集群。即使是多个开发人员共享的开发集群,也应该具有合理的可靠性和稳健性,否则每当集群意外宕机时,所有开发人员都会无所事事。
维护窗口
在有维护窗口的系统中,特定时间段被专门用于执行各种维护活动,如应用安全补丁、升级软件、修剪日志文件和数据库清理。有了维护窗口,系统(或子系统)会暂时不可用。通常会计划在非工作时间进行,并且用户通常会提前收到通知。维护窗口的好处在于,您不必担心维护操作会如何与进入系统的实时请求发生冲突。它可以大大简化操作。系统管理员和运营人员喜欢维护窗口,就像开发人员喜欢最佳努力系统一样。
当然,缺点是系统在维护期间会宕机。它可能仅适用于用户活动在特定时间内的系统(例如美国办公时间或仅限工作日)。
使用 Kubernetes,您可以通过将所有传入请求通过负载均衡器重定向到一个网页(或 JSON 响应)来通知用户维护窗口,从而实现维护窗口。
但在大多数情况下,Kubernetes 的灵活性应该允许您进行在线维护。在极端情况下,例如升级 Kubernetes 版本,或者从 etcd v2 切换到 etcd v3,您可能希望依赖维护窗口。蓝绿部署是另一种替代方案。但集群越大,蓝绿部署的替代方案就越庞大,因为您必须复制整个生产集群,这既昂贵又可能导致配额不足等问题。
快速恢复
快速恢复是高可用集群的另一个重要方面。总会有某个时刻出现问题。你的不可用时钟开始运行。你能多快恢复正常?平均恢复时间(MTTR)是一个重要的度量指标,用来跟踪并确保你的系统能够妥善应对灾难。
有时候,这并不是由你决定的。例如,如果你的云服务提供商发生了故障(而你没有实现联合集群,正如我们在第十一章《在多个集群上运行 Kubernetes》中将讨论的那样),那么你只能坐等他们解决问题。但最可能的罪魁祸首是最近的部署问题。当然,也存在与时间或日历相关的问题。你还记得 2012 年 2 月 29 日导致微软 Azure 崩溃的闰年 bug 吗?
快速恢复的典型代表当然是蓝绿部署——如果在发现问题时,你仍然保留着前一个版本的运行。但这通常适用于在部署过程中或刚部署后发生的问题。如果一个潜伏的 bug 在部署后几个小时才被发现,那么你可能已经拆掉了蓝色部署,而无法恢复。
另一方面,滚动更新意味着,如果问题早期被发现,那么大部分的 pods 仍会运行之前的版本。
与数据相关的问题可能需要很长时间才能恢复,即使你的备份是最新的,而且恢复程序确实有效(一定要定期测试这一点)。
像 Velero 这样的工具可以帮助你在某些场景中通过创建集群的快照备份来恢复系统,以防出现问题而你不知道如何修复时。
零停机
最后,我们来到了零停机系统。没有任何系统能够真正实现零停机。所有系统都会失败,所有软件系统也一定会失败。系统的可靠性通常通过“九个数”来衡量。请参阅en.wikipedia.org/wiki/High_availability#%22Nines%22。
有时故障严重到足以使系统或其中一些服务停机。将零停机视为一种最佳努力的分布式系统设计。你通过提供大量冗余和机制来应对预期的故障,避免系统停机,这样设计就是为了零停机。和往常一样,请记住,即使有零停机的商业需求,也并不意味着每个组件都必须实现零停机。可靠的(在合理范围内)系统可以由高度不可靠的组件构建而成。
零停机的计划如下:
-
每个层级的冗余:这是一个必要的条件。你不能在设计中有单点故障,因为当它失败时,整个系统会崩溃。
-
自动热交换故障组件:冗余系统的有效性取决于冗余组件在原始组件故障后能否迅速投入使用。一些组件可以分担负载(例如,无状态的 Web 服务器),因此无需显式操作。其他情况下,如 Kubernetes 调度器和控制器管理器,你需要进行领导者选举,以确保集群能够持续运行。
-
大量的指标、监控和警报来早期发现问题:即使经过仔细设计,你也可能漏掉一些东西,或者某些隐性假设可能会使你的设计失效。通常,这些微妙的问题会悄悄出现,经过足够关注后,你可能会在它们变成系统崩溃之前发现它们。例如,假设有一个机制可以在磁盘空间超过 90% 时清理旧的日志文件,但由于某种原因,它没有工作。如果你设置了一个磁盘空间超过 95% 时的警报,那么你就能及时发现并防止系统故障。
-
部署到生产环境前的彻底测试:全面的测试已被证明是提高质量的可靠方法。为复杂的大型 Kubernetes 集群和庞大的分布式系统进行全面测试是一项艰巨的工作,但你必须进行。你应该测试什么?所有的东西。没错。为了零停机,你需要同时测试应用程序和基础设施。你通过的 100% 单元测试是一个不错的起点,但它们不能让你完全放心地知道,当你在生产 Kubernetes 集群上部署应用时,它仍然会按预期运行。当然,最好的测试是在你生产集群上,进行蓝绿部署或在相同集群上进行的测试。若无法提供一个完全相同的集群,可以考虑建立一个尽可能忠实于生产环境的预发布环境。以下是你应该运行的测试列表。每个测试都应当全面,因为如果你漏掉了某些测试,可能会导致系统故障:
-
单元测试
-
验收测试
-
性能测试
-
压力测试
-
回滚测试
-
数据恢复测试
-
渗透测试
-
-
保留原始数据:对于许多系统来说,数据是最关键的资产。如果你保留原始数据,就可以在数据损坏或后期处理数据丢失的情况下恢复。这对于实现零停机没有直接帮助,因为重新处理原始数据可能需要一些时间,但它有助于实现零数据丢失,这通常更为重要。这个方法的缺点是原始数据通常比处理后的数据要大得多。一个好的选择是将原始数据存储在比处理数据更便宜的存储介质上。
-
将感知的正常运行时间作为最后的手段:好的,系统的某个部分可能已经宕机。你仍然可能能够维持某种程度的服务。在许多情况下,你可能能访问稍微过时的数据,或者允许用户访问系统的其他部分。这不是一个理想的用户体验,但从技术角度来看,系统仍然是可用的。
听起来疯狂吗?很好。零停机的大规模系统确实很难(实际上是不可能的)。微软、谷歌、亚马逊、Facebook 和其他大公司有成千上万的工程师(合计)专门从事基础设施、运营,并确保系统正常运行,这背后是有原因的。
网站可靠性工程
SRE 是一种在现实世界中操作可靠分布式系统的方法。SRE 接纳故障,并与服务水平指标(SLIs)、服务水平目标(SLOs)和服务水平协议(SLAs)配合使用。每个服务都有目标,比如 95% 的请求延迟低于 50 毫秒。如果某个服务未能达到其目标,那么团队会集中精力修复问题,然后再回到新功能和能力的开发工作中。
SRE 的魅力在于你可以调整成本和性能的“旋钮”。如果你想在可靠性上投入更多,那么要准备好为此支付更多的资源和开发时间。
性能和数据一致性
当你开发或运营分布式系统时,CAP 定理应该始终在你脑海中。CAP 代表一致性、可用性和分区容错:
-
一致性意味着每次读取都能得到最新的写入结果或错误
-
可用性意味着每个请求都会收到一个非错误响应(但响应可能是陈旧的)
-
分区容错意味着即使节点之间的任意数量的消息被丢失或延迟,系统仍然能够继续运行
该定理表明,你最多只能在三者中选择两个。由于任何分布式系统都可能遭遇网络分区,因此在实际操作中,你可以选择 CP 或 AP。CP 意味着为了保持一致性,系统在发生网络分区时将无法提供服务。AP 意味着系统将始终保持可用,但可能并不一致。例如,来自不同分区的读取可能返回不同的结果,因为某个分区没有收到写入操作。
本节我们重点讨论了高可用系统,即 AP。为了实现高可用性,我们必须牺牲一致性。但这并不意味着我们的系统会出现损坏或任意的数据。关键概念是最终一致性。我们的系统可能会稍微滞后,提供一些过时的数据,但最终你会得到预期的结果。
当你开始从最终一致性的角度思考时,它为潜在的显著性能提升打开了大门。例如,如果某个重要的值频繁更新(假设每秒一次),但是你只每分钟发送一次其值,那么你将网络流量减少了 60 倍,并且你平均只有 30 秒的延迟。这是非常显著的,这意味着你可以在相同的资源下,让你的系统处理更多的用户或请求——最多能应付 60 倍的负载。
正如我们之前讨论的,冗余是高可用系统的关键。然而,冗余和成本之间存在张力。在下一节中,我们将讨论如何选择和管理集群容量。
选择和管理集群容量
通过 Kubernetes 的水平 pod 自动扩展、DaemonSets、StatefulSets 和配额,我们可以扩展和控制我们的 pods、存储和其他对象。然而,最终我们受到 Kubernetes 集群中可用的物理(虚拟)资源的限制。如果所有节点都在 100% 的容量下运行,你需要为集群添加更多的节点。没有捷径可走,Kubernetes 将无法扩展。另一方面,如果你有非常动态的工作负载,Kubernetes 可以缩小你的 pods,但如果你没有相应地缩小节点,你仍然会为多余的容量付费。在云中,你可以按需停止和启动实例。将它与集群自动扩展器结合使用,可以自动解决计算容量问题。这是理论上的解决方案,但在实践中,总是会有一些细微差别。
选择节点类型
最简单的解决方案是选择单一节点类型,该节点类型具有已知的 CPU、内存和本地存储量。但这通常不是最有效且最具成本效益的解决方案。它使得容量规划变得简单,因为唯一的问题是需要多少个节点。每当你添加一个节点时,你就会为集群添加已知数量的 CPU 和内存,但大多数 Kubernetes 集群及其内部组件处理的是不同的工作负载。我们可能有一个流处理管道,其中许多 pods 接收一些数据并在一个地方进行处理。
这种工作负载是 CPU 密集型的,可能需要大量内存,也可能不需要。一些组件,如分布式内存缓存,可能需要大量内存,但 CPU 占用非常少。其他组件,如 Cassandra 集群,需要每个节点附加多个 SSD 硬盘。机器学习工作负载可能会受益于 GPU。此外,云服务提供商提供了抢占实例——这些节点价格更低,但如果其他客户愿意支付正常价格,它们可能会被抢走。
在大规模部署中,成本开始累积,你应该尝试将工作负载与节点配置对齐,这意味着使用多个具有不同节点(实例)类型的节点池。
对于每种类型的节点,您应分配适当的标签,并确保 Kubernetes 将设计在该节点类型上运行的 pods 调度到该节点。
选择你的存储解决方案
存储在扩展集群时是一个重要因素。有三种可扩展存储解决方案:
-
自己动手
-
使用你的云平台存储解决方案
-
使用集群外的解决方案
当你自己动手时,你会在你的 Kubernetes 集群中安装某种类型的存储解决方案。它的好处是灵活性和完全控制,但你必须自己管理和扩展它。
当你使用云平台存储解决方案时,你可以获得很多现成的功能,但你会失去控制权,通常会支付更多费用,并且根据服务的不同,你可能会被锁定在那个提供商那里。
当你使用集群外的解决方案时,数据传输的性能和成本可能会大大增加。如果你需要与现有系统集成,通常会使用这个选项。
当然,大型集群可能会有来自所有类别的多个数据存储。这是你必须做出的最关键的决策之一,而且你的存储需求可能会随着时间的推移而发生变化和演变。
权衡成本和响应时间
如果钱不是问题,你可以直接为你的集群进行过度配置。每个节点都会拥有最好的硬件配置,你会拥有比处理工作负载所需更多的节点,并且有大量可用存储。但猜猜看?钱永远是个问题!
当你刚开始时,集群的流量不大,你可能会通过过度配置来应对。你可能只运行五个节点,即使大部分时间两个节点就足够了。然而,将一切都乘以 1,000,财务部门的人就会来问你,为什么你有成千上万台闲置的机器和几 PB 的空闲存储。
好吧。所以,你仔细地衡量并优化,每个资源的利用率都达到了 99.99999%。恭喜你,你刚刚创建了一个无法处理任何额外负载或单个节点故障的系统,否则请求就会丢失或响应会延迟。
你需要找到一个折衷方案。了解你的工作负载的典型波动,并考虑拥有过剩容量与减少响应时间或处理能力的成本/收益比。
有时,如果你有严格的可用性和可靠性要求,你可以在系统中建立冗余,然后通过设计过度配置。例如,你希望能够在没有停机时间和没有明显影响的情况下热插拔一个故障的组件。也许你甚至不能失去一个交易。在这种情况下,你会为所有关键组件准备一个实时备份,这额外的容量可以用来在没有特殊操作的情况下缓解临时波动。
有效使用多节点配置
有效的容量规划要求你理解系统的使用模式和每个组件可以承受的负载。这可能包括系统内部生成的大量数据流。当你通过指标对典型工作负载有了深入了解后,可以查看工作流以及哪些组件处理哪些部分的负载。然后你可以计算 Pod 的数量及其资源需求。根据我的经验,有些工作负载相对固定,有些工作负载是可以预测变化的(如办公时间与非办公时间的区别),而有些则是完全不可预测的疯狂工作负载。你必须根据每种工作负载进行规划,并设计几种节点配置的方案,以便调度适应特定工作负载的 Pod。
受益于弹性云资源
大多数云服务提供商允许你自动扩展实例,这与 Kubernetes 的水平 Pod 自动扩展是完美的互补。如果你使用云存储,它也会在你什么都不做的情况下神奇地增长。然而,你需要注意一些潜在的问题。
自动扩展实例
所有大型云服务提供商都已提供实例自动扩展功能。虽然有些细节差异,但基于 CPU 利用率的扩展总是可用的,有时也提供自定义指标。有时,还会提供负载均衡。正如你所看到的,这里与 Kubernetes 有一些重叠。
如果你的云服务提供商没有提供足够的自动扩展控制,并且不支持集群自动扩展器,那么相对容易实现自定义扩展,你可以监控集群的资源使用情况,并调用云 API 来添加或移除实例。你可以从 Kubernetes 中提取指标。下面是一个示意图,展示了如何基于 CPU 负载监控来添加两个新实例:

图 3.3:基于 CPU 的自动扩展
留意你的云配额
在与云服务提供商合作时,最令人烦恼的事情之一就是配额。我曾与四个不同的云服务提供商(AWS、GCP、Azure 和阿里云)合作过,且在某些时刻都曾被配额所困扰。配额存在的目的是让云服务提供商进行自己的容量规划(同时保护你免于无意中启动 100 万个你无法支付的实例),但从你的角度来看,它是一个可能绊倒你的因素。试想,你设置了一个像魔法一样运作的完美自动扩展系统,结果当节点数达到 100 时,系统突然无法扩展。你很快发现自己被限制为 100 个节点,于是你提交了支持请求以增加配额。然而,配额请求必须由人工审批,这可能需要一两天时间。在此期间,你的系统无法处理负载。
小心管理区域
云平台按照区域和可用区进行组织。在像 GCP 和 Azure 这样的云服务提供商中,区域之间的成本差异可以高达 20%。在 AWS 上,可能会更为极端(30%-70%)。一些服务和机器配置仅在某些区域提供。
云配额也在区域级别进行管理。区域内的数据传输的性能和成本要比跨区域的低得多(通常是免费的)。在规划集群时,您应仔细考虑您的地理分布策略。如果您需要在多个区域运行工作负载,您可能需要就冗余性、可用性、性能和成本做出一些艰难的决策。
考虑容器原生解决方案
容器原生解决方案是指云服务提供商提供了一种直接将容器部署到其基础设施中的方式。您无需先配置实例,然后安装容器运行时(如 Docker 守护进程),再部署容器。相反,您只需提供容器,平台负责寻找运行容器的机器。您与容器运行的实际机器完全隔离。
所有主要的云服务提供商现在都提供完全抽象化的实例解决方案:
-
AWS Fargate
-
Azure Container Instances
-
Google Cloud Run
这些解决方案并非专门针对 Kubernetes,但它们可以与 Kubernetes 协同工作。云服务提供商已经提供了托管的 Kubernetes 控制平面,如 Google 的Google Kubernetes Engine(GKE)、Microsoft 的Azure Kubernetes Service(AKS)和 Amazon Web Services 的Elastic Kubernetes Service(EKS)。但是,数据平面(即节点)仍然由集群管理员管理。容器原生解决方案使云服务提供商可以代为管理这些工作。Google Run for GKE、AKS 与 ACI 以及 AWS EKS 与 Fargate 可以管理控制平面和数据平面。
例如,在 AKS 中,您可以配置虚拟节点。虚拟节点并不是由实际的虚拟机支持的。相反,它利用 ACI 在必要时部署容器。只有当集群需要超出常规节点的容量时,您才需要为此付费。与需要配置实际虚拟机支持的节点的集群自动扩展器相比,虚拟节点的扩展速度更快。
以下图示说明了这一突发到 ACI 的方式:

图 3.4:AKS 和 ACI
在本节中,我们探讨了影响您关于集群容量决策的各种因素,以及云服务提供商为您完成繁重工作的解决方案。在接下来的章节中,我们将看看单一 Kubernetes 集群的负载极限。
推动 Kubernetes 的极限
在本节中,我们将看到 Kubernetes 团队如何将 Kubernetes 推向极限。这些数字非常具有参考价值,但一些工具和技术,如 Kubemark,极为巧妙,你甚至可以用它们来测试你的集群。Kubernetes 设计支持具有以下特性的集群:
-
每个节点最多支持 110 个 Pods
-
最多 5,000 个节点
-
每个节点最多支持 150,000 个 Pods
-
最多 300,000 个总容器
这些数字只是指导性建议,并不是硬性限制。承载专门工作负载的集群,其部署和运行时模式可能支持不同数量的新 Pods 的进出。
在 CERN,OpenStack 团队实现了每秒 200 万个请求:superuser.openstack.org/articles/scaling-magnum-and-kubernetes-2-million-requests-per-second。
Mirantis 在其扩展实验室中进行了一次性能和扩展性测试,他们在 500 台物理服务器上部署了 5,000 个 Kubernetes 节点(在虚拟机中)。
OpenAI 将其机器学习 Kubernetes 集群扩展到 Azure 上的 2,500 个节点,并从中获得了一些宝贵的经验教训,例如注意日志代理的查询负载并将事件存储在单独的 etcd 集群中:openai.com/research/scaling-kubernetes-to-2500-nodes。
这里还有许多有趣的使用案例:www.cncf.io/projects/case-studies。
本节结束时,你将欣赏到为了在大规模上改进 Kubernetes 所付出的努力和创造力,你将了解单个 Kubernetes 集群能够达到的极限以及预期的性能,并且你将深入了解一些工具和技术,帮助你评估自己 Kubernetes 集群的性能。
提升 Kubernetes 性能和可扩展性
Kubernetes 团队在 Kubernetes 1.6 中非常重视性能和可扩展性。当 Kubernetes 1.2 发布时,它支持 Kubernetes 服务级目标内最多 1,000 个节点的集群。Kubernetes 1.3 将这一数字增加到 2,000 个节点,而 Kubernetes 1.6 将其提升到惊人的 5,000 个节点每个集群。5,000 个节点可以支持非常大的规模,尤其是当你使用大型节点时。但是,当你使用大型节点时,也需要注意每个节点的 Pods 数量指南。需要注意的是,云服务商仍然推荐每个集群最多支持 1,000 个节点。
我们稍后会讨论这些数字,但首先,让我们深入了解 Kubernetes 是如何实现这些令人印象深刻的改进的。
API 服务器中的读取缓存
Kubernetes 将系统的状态保存在 etcd 中,etcd 非常可靠,尽管速度并不是特别快(尽管 etcd 3 在特定方面提供了巨大的改进,特别是为了支持更大的 Kubernetes 集群)。Kubernetes 的各个组件基于这些状态的快照进行操作,而不依赖于实时更新。这个事实使得可以通过牺牲一些延迟来换取吞吐量。所有的快照以前是通过 etcd 观察进行更新的。现在,API 服务器有一个内存中的读取缓存,用于更新状态快照。这个内存读取缓存是通过 etcd 观察更新的。这些方案显著减少了 etcd 的负载,并提高了 API 服务器的整体吞吐量。
Pod 生命周期事件生成器
增加集群中的节点数量是水平扩展的关键,但 Pod 密度也至关重要。Pod 密度是 kubelet 在一个节点上可以高效管理的 Pod 数量。如果 Pod 密度较低,那么你就无法在一个节点上运行太多 Pod。这意味着你可能无法从更强大的节点(每个节点的 CPU 和内存更多)中获益,因为 kubelet 无法管理更多的 Pod。另一种选择是迫使开发者妥协设计,创建粒度较大的 Pod,每个 Pod 执行更多的工作。理想情况下,Kubernetes 不应强迫你在 Pod 粒度上做出妥协。Kubernetes 团队对此有着深刻的理解,并投入了大量工作来提高 Pod 密度。
在 Kubernetes 1.1 中,官方(经过测试和宣传的)每个节点支持的 Pod 数量为 30 个。实际上,我在 Kubernetes 1.1 上运行了每个节点 40 个 Pod,但我为此付出了过多的 kubelet 开销,这些开销抢占了 worker pod 的 CPU。在 Kubernetes 1.2 中,这个数字跃升至每个节点 100 个 Pod。以前,kubelet 会在每个 Pod 的自己的 goroutine 中持续轮询容器运行时。这给容器运行时带来了很大的压力,尤其是在性能峰值时,容器运行时会出现可靠性问题,特别是 CPU 利用率方面。解决方案是 Pod 生命周期事件生成器(PLEG)。PLEG 的工作方式是,它列出所有 Pod 和容器的状态,并将其与先前的状态进行比较。这是针对所有 Pod 和容器进行一次性操作。然后,通过比较当前状态与先前的状态,PLEG 能够知道哪些 Pod 需要再次同步,并仅调用这些 Pod。这个变化导致 kubelet 和容器运行时的 CPU 使用率降低了四倍。它还减少了轮询周期,从而提高了响应能力。
以下图表展示了在 Kubernetes 1.1 和 Kubernetes 1.2 下,120 个 Pod 的 CPU 利用率对比。你可以非常清晰地看到 4 倍的差异:

图 3.5:Kube 1.1 和 Kube 1.2 下 120 个 Pod 的 CPU 利用率
使用协议缓冲区序列化 API 对象
API 服务器提供了一个 REST API。REST API 通常使用 JSON 作为其序列化格式,Kubernetes API 服务器也不例外。然而,JSON 序列化意味着需要将 JSON 序列化和反序列化为本地数据结构。这是一个开销较大的操作。在大规模的 Kubernetes 集群中,很多组件需要频繁地查询或更新 API 服务器。所有这些 JSON 解析和组装的成本迅速累积。在 Kubernetes 1.3 中,Kubernetes 团队增加了一种高效的协议缓冲区序列化格式。JSON 格式依然存在,但 Kubernetes 各组件之间的所有内部通信都使用协议缓冲区序列化格式。
etcd3
Kubernetes 在 1.6 版本中将 etcd2 替换为 etcd3。这是一个重大变化。由于 etcd2 的限制,尤其是在 watch 实现方面,Kubernetes 扩展到 5000 个节点变得不可行。Kubernetes 的可扩展性需求推动了 etcd3 的许多改进,因为 CoreOS 使用 Kubernetes 作为衡量标准。这里讨论了一些重要的改进内容。
使用 gRPC 代替 REST
etcd2 提供了一个 REST API,而 etcd3 提供了一个 gRPC API(并通过 gRPC 网关提供 REST API)。gRPC 底层的 HTTP/2 协议可以使用单个 TCP 连接处理多个请求和响应流。
使用租约(Leases)代替 TTL
etcd2 使用生存时间(TTL)作为每个键的过期机制,而 etcd3 使用带 TTL 的租约(Leases),多个键可以共享同一个租约。这大大减少了保持连接的流量。
Watch 实现
etcd3 的 watch 实现利用了 gRPC 双向流,并保持一个单一的 TCP 连接来发送多个事件,从而将内存占用减少了至少一个数量级。
状态存储
使用 etcd3 后,Kubernetes 开始将所有状态存储为协议缓冲区,这消除了大量浪费的 JSON 序列化开销。
其他优化
Kubernetes 团队还进行了许多其他优化,例如:
-
优化调度器(这使得调度吞吐量提高了 5 到 10 倍)
-
将所有控制器切换到使用共享信息器的新推荐设计,从而减少了控制器管理器的资源消耗
-
优化 API 服务器中的个别操作(转换、深拷贝和补丁)
-
减少 API 服务器中的内存分配(这对 API 调用的延迟有显著影响)
测量 Kubernetes 的性能和可扩展性
为了提高性能和可伸缩性,您需要清楚地了解您想要改进的内容以及如何测量这些改进。您还必须确保在追求性能和可伸缩性的过程中不违反基本的属性和保证。我喜欢性能改进的原因在于它们通常可以免费为您提供可伸缩性改进。例如,如果一个 pod 需要一个节点的 50% CPU 来完成其工作,而您通过提高性能使得该 pod 可以使用 33% CPU 完成相同的工作,那么您突然可以在该节点上运行三个 pod 而不是两个,从而将您的集群总体的可伸缩性提高了 50%(或者将成本降低了 33%)。
Kubernetes SLOs
Kubernetes 具有服务级别目标(SLOs)。在尝试提高性能和可伸缩性时,必须遵守这些保证。Kubernetes 对 API 调用的响应时间有一个秒级(99 百分位数)的 SLO。实际上,大多数情况下,它实现了比这个响应时间快一个数量级的更快响应时间。
测量 API 响应性
API 具有许多不同的端点。没有简单的 API 响应性数字。每个调用必须单独测量。此外,由于系统的复杂性和分布式特性,更不用说网络问题,结果可能会有很大的波动性。一种可靠的方法是将 API 测量分成单独的端点,然后随时间运行大量测试,并查看百分位数(这是标准做法)。
同样重要的是要使用足够的硬件来管理大量对象。在此测试中,Kubernetes 团队使用了一个带有 32 核心和 120 GB 内存的虚拟机作为主节点。
下图描述了 Kubernetes 1.3 版本中各个重要 API 调用延迟的 50th、90th 和 99th 百分位数。您可以看到,90th 百分位数非常低,低于 20 毫秒。甚至对于DELETE pods 操作,99th 百分位数也低于 125 毫秒,对于所有其他操作也低于 100 毫秒:

图 3.6: API 调用延迟
另一类 API 调用是LIST操作。这些调用更为昂贵,因为它们需要在大型集群中收集大量信息,组成响应,并发送可能很大的响应。这就是性能改进(例如内存读取缓存和协议缓冲区序列化)的亮点所在。响应时间理所当然地比单个 API 调用要长,但仍远低于一秒(1,000 毫秒)的 SLO:

图 3.7: LIST API 调用延迟
测量端到端 pod 启动时间
大规模动态集群中最重要的性能特征之一是端到端的 Pod 启动时间。Kubernetes 一直在创建、销毁和重新调度 Pod。可以说,Kubernetes 的主要功能就是调度 Pod。在下图中,你可以看到,Pod 启动时间比 API 调用的波动性要小。这是合理的,因为很多工作是独立于集群大小的,例如启动一个新的运行时实例。在 Kubernetes 1.2 版本下的 1000 节点集群中,启动一个 Pod 的 99 分位端到端时间不到 3 秒。使用 Kubernetes 1.3 时,启动一个 Pod 的 99 分位端到端时间略高于 2.5 秒。
值得注意的是,尽管时间非常接近,但在 Kubernetes 1.3 中,2000 节点集群的表现稍微优于 1000 节点集群:

图 3.8:Pod 启动延迟 1

图 3.9:Pod 启动延迟 2
在这一部分中,我们研究了 Kubernetes 的极限以及大规模节点集群的性能表现。在下一部分,我们将探讨 Kubernetes 开发者如何在大规模下测试 Kubernetes 的一些创新方法。
在大规模下测试 Kubernetes
拥有成千上万节点的集群非常昂贵。即使是像 Kubernetes 这样得到 Google 和其他行业巨头支持的项目,仍然需要找到合理的方式进行测试,而不至于花费过高。
Kubernetes 团队每个版本至少会在一个真实的集群上进行全面测试,以收集真实世界的性能和可扩展性数据。然而,仍然需要一种轻量级且成本较低的方式来实验潜在的改进并检测回归问题。这就是 Kubemark 的作用。
介绍 Kubemark 工具
Kubemark 是一个 Kubernetes 集群,运行的是模拟节点,称为空洞节点,用于在大规模(空洞)集群上运行轻量级基准测试。一些在真实节点上可用的 Kubernetes 组件,如 kubelet,被一个空洞 kubelet 替代。空洞 kubelet 模拟了真实 kubelet 的许多功能。空洞 kubelet 并不实际启动任何容器,也不挂载任何卷。但从 Kubernetes 的角度来看——存储在 etcd 中的状态——这些对象是存在的,你可以查询 API 服务器。空洞 kubelet 实际上是一个真实的 kubelet,它注入了一个模拟的 Docker 客户端,后者不执行任何操作。
另一个重要的空洞组件是空洞代理,它模拟了 kube-proxy 组件。它再次使用真实的 kube-proxy 代码,但采用了一个不做任何操作的模拟 proxier 接口,避免了操作 iptables。
设置 Kubemark 集群
Kubemark 集群利用了 Kubernetes 的强大功能。要设置一个 Kubemark 集群,请执行以下步骤:
-
创建一个常规的 Kubernetes 集群,我们可以在其中运行 N 个空洞节点。
-
创建一个专用虚拟机来启动 Kubemark 集群的所有主组件。
-
在基础 Kubernetes 集群上调度 N 个空节点 Pod。这些空节点被配置为与运行在专用虚拟机上的 Kubemark API 服务器进行通信。
-
通过在基础集群上调度附加 Pod,并配置它们与 Kubemark API 服务器通信来创建附加 Pod。
完整的指南请点击这里:
github.com/kubernetes/community/blob/master/contributors/devel/sig-scalability/kubemark-guide.md
将 Kubemark 集群与真实世界的集群进行比较
Kubemark 集群的性能与真实集群的性能大致相同。在 Pod 启动的端到端延迟方面,差异可以忽略不计。而在 API 响应性方面,差异较大,但通常小于两倍。然而,趋势完全一致:真实集群上的改进/回归会在 Kubemark 中显示为相似的百分比下降/上升。
总结
在本章中,我们探讨了可靠且高度可用的大规模 Kubernetes 集群。这可以说是 Kubernetes 的最佳应用场景。虽然能够编排一个运行少量容器的小型集群是有用的,但它并非必要;然而在大规模集群中,你必须有一个可以信任的编排解决方案,以便它能够随着系统扩展并提供相应的工具和最佳实践。
现在你已经扎实掌握了分布式系统中可靠性和高可用性的概念。你深入探讨了运行可靠且高可用的 Kubernetes 集群的最佳实践。你也了解了 Kubernetes 集群扩展和性能衡量的复杂问题。现在,你能够做出明智的设计决策,选择合适的可靠性和可用性水平,同时考虑它们的性能和成本。
在下一章,我们将讨论 Kubernetes 中的一个重要话题——安全性。我们还将讨论保障 Kubernetes 安全性面临的挑战以及相关风险。我们将全面了解命名空间、服务账户、准入控制、身份验证、授权和加密等内容。
加入我们的 Discord 群组!
与其他用户、云计算专家、作者以及志同道合的专业人士一起阅读本书。
提问、为其他读者提供解决方案、通过“问我任何事情”环节与作者交流等等。
扫描二维码或访问链接,立即加入社区。

第四章:保护 Kubernetes
在第三章,高可用性与可靠性中,我们探讨了可靠且高可用的 Kubernetes 集群、基本概念、最佳实践,以及与可扩展性、性能和成本相关的许多设计权衡。
在本章中,我们将探讨安全这个重要话题。Kubernetes 集群是由多个交互组件组成的复杂系统。不同层次的隔离和分区在运行关键应用程序时非常重要。为了确保系统的安全并确保对资源、功能和数据的适当访问,我们必须首先了解 Kubernetes 作为一个通用编排平台,面临的独特挑战,它运行的是未知的工作负载。然后,我们可以利用各种安全性、隔离和访问控制机制,确保集群、运行其上的应用程序以及数据的安全。我们将讨论各种最佳实践,并在适当的时候使用每种机制。
本章将探讨以下主要主题:
-
理解 Kubernetes 的安全挑战
-
加固 Kubernetes
-
运行多租户集群
到本章结束时,你将对 Kubernetes 的安全挑战有一个良好的理解。你将获得如何加固 Kubernetes 以防范各种潜在攻击的实用知识,建立纵深防御,并能够安全地运行一个多租户集群,同时为不同用户提供完全的隔离和对其集群部分的完全控制。
理解 Kubernetes 的安全挑战
Kubernetes 是一个非常灵活的系统,以通用方式管理非常底层的资源。Kubernetes 本身可以部署在多种操作系统、硬件或虚拟机解决方案上,无论是本地部署还是云环境。Kubernetes 通过与运行时交互的明确定义的运行时接口运行工作负载,但无需理解它们是如何实现的。Kubernetes 代表或服务于其不了解的应用程序,操作着关键资源,如网络、DNS 和资源分配。
这意味着 Kubernetes 面临着一个艰巨的任务:在提供良好的安全机制和能力的同时,确保应用程序开发人员和集群管理员可以使用这些机制,同时保护自己、开发人员和管理员免受常见错误的影响。
在本节中,我们将讨论 Kubernetes 集群中几个层次或组件的安全挑战:节点、网络、镜像、Pods 和容器。纵深防御是一个重要的安全概念,要求系统在每个层级都能够自我保护,既能缓解渗透其他层的攻击,又能限制破坏的范围和程度。认识到每个层次的挑战是实现纵深防御的第一步。
这通常被描述为云原生安全的 4 个 C:

图 4.1:云原生安全的四个关键 C
然而,四个 C 模型是安全性的一种粗粒度方法。另一种方法是基于不同维度的安全挑战建立威胁模型,例如:
-
节点挑战
-
网络挑战
-
图像挑战
-
部署和配置挑战
-
Pod 和容器的挑战
-
组织、文化和流程挑战
让我们检查这些挑战。
节点挑战
节点是运行时引擎的主机。如果攻击者访问节点,这是一个严重威胁。他们至少可以控制主机本身和运行在其上的所有工作负载。但情况可能会更糟。
节点上运行一个与 API 服务器通信的 kubelet。一个高级的攻击者可以用修改过的 kubelet 替换它,并通过正常通信与 Kubernetes API 服务器通信,而运行他们自己的工作负载而不是计划的工作负载,收集关于整个集群的信息,并通过发送恶意消息干扰 API 服务器和集群的其余部分。攻击者将访问共享资源和可能允许其更深入渗透的秘密。节点入侵非常严重,因为可能造成的损害和事后检测的困难性。
节点也可以在物理级别上受到损害。这在裸机上更为相关,您可以查看哪些硬件分配给了 Kubernetes 集群。
另一个攻击向量是资源耗尽。想象一下,您的节点成为一个与您的 Kubernetes 集群无关的机器人网络的一部分,只运行自己的工作负载,如加密货币挖矿,耗尽 CPU 和内存。这里的危险在于,您的集群将窒息并耗尽运行工作负载所需的资源,或者您的基础架构可能会自动扩展并分配更多资源。
另一个问题是在自动化部署之外安装调试和故障排除工具或修改配置。这些通常是未经测试的,如果留下并保持活跃状态,至少会导致性能下降,但也可能导致更严重的问题。至少,它会增加攻击面。
在关注安全性时,这是一个数字游戏。您需要了解系统的攻击面和脆弱性所在。让我们列出一些可能的节点挑战:
-
攻击者控制主机
-
攻击者替换 kubelet
-
攻击者控制运行主控组件的节点(例如 API 服务器、调度器或控制器管理器)
-
攻击者获得对节点的物理访问
-
攻击者耗尽与 Kubernetes 集群无关的资源
-
安装调试和故障排除工具或配置更改会造成自我损害
缓解节点挑战需要多层防御,例如控制物理访问、阻止权限升级以及通过控制节点上安装的操作系统和软件来减少攻击面。
网络挑战
任何一个非简单的 Kubernetes 集群至少跨越一个网络。与网络相关的挑战很多。你需要非常细致地了解你的系统组件是如何连接的。哪些组件应该互相通信?它们使用哪些网络协议?使用哪些端口?它们交换哪些数据?你的集群如何与外界连接?
曝露端口和能力或服务的过程复杂:
-
容器到主机
-
内部网络中的主机对主机
-
主机对世界
使用覆盖网络(将在第十章《探索 Kubernetes 网络》中详细讨论)有助于增强防御深度,即使攻击者获得了对容器的访问权限,他们也被沙箱隔离,无法逃逸到底层网络的基础设施。
发现组件也是一个巨大的挑战。这里有几个选项,例如 DNS、专用发现服务和负载均衡器。每个选项都有一组优缺点,需要仔细的规划和洞察才能根据你的情况正确配置。确保两个容器能够互相发现并交换信息并非易事。
你需要决定哪些资源和端点应该对外部可访问。然后,你需要想出一种合适的方式来验证用户、服务身份,并授权它们操作资源。你可能还需要控制内部服务之间的访问。
敏感数据必须在进出集群的过程中进行加密,有时在静态存储时也需要加密。这意味着密钥管理和安全密钥交换,这是解决安全问题中最困难的问题之一。
如果你的集群与其他 Kubernetes 集群或非 Kubernetes 进程共享网络基础设施,那么你必须小心隔离和分离。
解决方案包括网络策略、防火墙规则和软件定义网络(SDN)。这套方案通常需要定制化。对于本地部署和裸机集群来说,这尤其具有挑战性。以下是你将面临的一些网络挑战:
-
制定连接计划
-
选择组件、协议和端口
-
确定动态发现
-
公共访问与私有访问
-
身份验证和授权(包括内部服务之间)
-
设计防火墙规则
-
决定网络策略
-
密钥管理和交换
-
加密通信
在网络层面上,容器、用户和服务之间互相发现和通信的便利性与通过网络或对网络本身的攻击进行访问控制之间始终存在着一种紧张关系。
许多这些挑战并非 Kubernetes 特有。然而,Kubernetes 作为一个管理关键基础设施并处理低层次网络的通用平台,使得我们必须考虑动态和灵活的解决方案,这些方案可以将系统特定的需求整合进 Kubernetes 中。这些解决方案通常涉及监控,并根据命名空间和 Pod 标签自动注入防火墙规则或应用网络策略。
镜像挑战
Kubernetes 运行符合其某个运行时引擎标准的容器。它并不知道这些容器在做什么(除了收集度量数据)。你可以通过配额对容器设置某些限制。你还可以通过网络策略限制它们对网络其他部分的访问。但最终,容器确实需要访问主机资源、网络中的其他主机、分布式存储和外部服务。镜像决定了容器的行为。臭名昭著的软件供应链问题正是这些容器镜像创建方式的核心。镜像存在两类问题:
-
恶意镜像
-
脆弱的镜像
恶意镜像是指那些包含攻击者设计的代码或配置,目的是造成伤害、收集信息,或者仅仅是利用你的基础设施为其目的服务(例如,进行加密货币挖矿)。恶意代码可能会被注入到你的镜像准备流程中,包括你使用的任何镜像仓库。或者,你可能会安装那些已经被攻击并且现在包含恶意代码的第三方镜像。
脆弱的镜像是你设计的镜像(或你安装的第三方镜像),它恰巧包含了一些漏洞,允许攻击者控制正在运行的容器或造成其他伤害,包括稍后注入他们自己的代码。
很难判断哪种情况更糟。极端情况下,它们是等价的,因为它们都允许完全控制容器。现有的其他防御措施(记得深度防御吗?)以及你对容器设置的限制将决定它能造成多大的损害。最大限度地减少不良镜像的危害非常具有挑战性。快速发展的公司使用微服务可能每天都会生成许多镜像。验证镜像也不是一项简单的任务。此外,一些容器需要广泛的权限来完成其合法工作。如果这样的容器被攻破,它可以造成极大的损害。
包含操作系统的基础镜像可能会在发现新的漏洞时变得脆弱。此外,如果你依赖于他人准备的基础镜像(这非常常见),那么恶意代码可能会进入这些你无法控制的基础镜像,而你却会完全信任它们。
当发现第三方依赖项中的漏洞时,理想情况下已经有了修复版本,你应该尽快进行修补。
我们可以总结出开发者可能面临的镜像挑战如下:
-
Kubernetes 无法了解容器的行为
-
Kubernetes 必须为指定的功能提供对敏感资源的访问
-
很难保护镜像准备和交付管道(包括镜像仓库)
-
新镜像的开发和部署速度与对变更的仔细审查相冲突
-
包含操作系统或其他常见依赖项的基础镜像容易过时并变得脆弱
-
基础镜像通常不在你的控制之下,可能更容易被注入恶意代码
将静态镜像分析器,如 CoreOS Clair 或 Anchore Engine,集成到你的 CI/CD 流水线中可以帮助很多。此外,通过限制容器仅访问执行其工作所需的资源来最小化爆炸范围,可以在容器被攻破时减少对系统的影响。你还必须对已知漏洞的修复保持谨慎。
配置和部署挑战
Kubernetes 集群是远程管理的。各种清单和策略决定了集群在每个时刻的状态。如果攻击者获得了对集群的管理权限,他们可以造成严重破坏,例如收集信息、注入恶意镜像、削弱安全性以及篡改日志。像往常一样,漏洞和错误同样具有危害;如果忽视了重要的安全措施,你将使集群暴露于攻击中。现在很常见的是,拥有集群管理权限的员工远程工作,无论是在家还是在咖啡店,他们带着笔记本电脑,其中你只需要执行一个 kubectl 命令,就能打开大门。
让我们重申一下挑战:
-
Kubernetes 是远程管理的
-
拥有远程管理权限的攻击者可以完全控制集群
-
配置和部署通常比代码更难测试
-
远程或离线员工面临长时间暴露的风险,使得攻击者能够获得他们的笔记本电脑或手机的管理访问权限
有一些最佳实践可以最小化这个风险,例如通过跳板机的间接层,开发者从外部连接到集群中的专用机器,严格控制与内部服务的安全交互,要求 VPN 连接(它对所有通信进行身份验证和加密),并使用多因素认证和一次性密码来防止简单的密码破解攻击。
Pod 和容器挑战
在 Kubernetes 中,pod 是工作单元,包含一个或多个容器。pod 是一个组合和部署结构。但通常,在同一个 pod 中部署的容器会通过直接机制进行交互。这些容器共享相同的 localhost 网络,并且通常共享来自主机的挂载卷。在同一个 pod 中容器之间的这种简单集成可能会导致将主机的部分内容暴露给所有容器。这可能会使一个坏容器(无论是恶意的还是仅仅是脆弱的)为 pod 中其他容器的升级攻击打开道路,最终接管节点本身和整个集群。控制平面插件通常与控制平面组件共同部署,这样的风险尤其明显,特别是因为它们中的许多是实验性的。对于在每个节点上运行 pod 的 DaemonSets 同样适用。Sidecar 容器的做法,指的是与应用程序容器一起部署在 pod 中的附加容器,尤其在服务网格中非常流行。这增加了风险,因为 sidecar 容器通常在你的控制之外,如果被攻破,可能会为你的基础设施提供访问权限。
多容器 pod 的挑战包括以下几个方面:
-
同一 pod 中的容器共享 localhost 网络
-
同一 pod 中的容器有时共享主机文件系统中的挂载卷
-
坏容器可能会对 pod 中的其他容器造成危害
-
如果与访问关键节点资源的其他容器共存,坏容器更容易攻击节点
-
与主控组件共同部署的实验性插件可能不够安全
-
服务网格引入了可能成为攻击向量的 sidecar 容器
仔细考虑在同一 pod 中运行的容器之间的交互。你应该意识到,坏容器可能会把破坏同 pod 中的其他容器作为首要攻击目标。这意味着你应该能够检测到被注入到 pod 中的恶意容器(例如通过恶意的准入控制 webhook 或被攻破的 CI/CD 管道)。你还应该应用最小权限原则,并将恶意容器能够造成的损害降到最低。
组织、文化和流程方面的挑战
安全性通常与生产力相对立。这是一个正常的权衡,不必担心。传统上,当开发和运维分开时,这种冲突是在组织层面管理的。开发人员推动更多的生产力,并将安全要求视为做生意的成本。运维控制生产环境,并负责访问和安全流程。DevOps 运动打破了开发和运维之间的壁垒。现在,开发速度通常占据了最前沿的位置。诸如持续部署、每天多次自动部署的概念在大多数组织中都是闻所未闻的。Kubernetes 就是为这种新的云原生应用场景设计的。但它是基于 Google 的经验开发的。
Google 有大量的时间和技术专家来开发适当的流程和工具,以平衡快速部署与安全性。对于小型组织来说,这种平衡可能非常具有挑战性,过于专注于生产力可能会削弱安全性。
采用 Kubernetes 的组织面临的挑战如下:
-
控制 Kubernetes 操作的开发人员可能不那么关注安全性
-
开发速度可能被认为比安全性更重要
-
持续部署可能使得在问题到达生产环境之前难以发现某些安全问题
-
小型组织可能没有足够的知识和专业能力来在 Kubernetes 集群中正确管理安全性
这里没有简单的答案。你应该在安全性和敏捷性之间找到合适的平衡。我建议有一个专门的安全团队(或至少一个专注于安全的人)参与所有的规划会议,并倡导安全。重要的是从一开始就将安全性融入到系统中。
在这一部分,我们回顾了在构建安全的 Kubernetes 集群时你所面临的许多挑战。这些挑战大多数并非 Kubernetes 特有,但使用 Kubernetes 意味着系统的很大一部分是通用的,并且无法知道系统正在做什么。
在尝试锁定系统时,这可能会带来问题。这些挑战分布在不同的层级:
-
Node 挑战
-
网络挑战
-
镜像挑战
-
配置和部署挑战
-
Pod 和容器挑战
-
组织和流程挑战
在下一节中,我们将查看 Kubernetes 提供的设施,解决其中的一些挑战。许多挑战需要在更大的系统范围内找到解决方案。重要的是要意识到,仅仅利用 Kubernetes 所有的安全功能还不够。
加固 Kubernetes
前一节列出了部署和维护 Kubernetes 集群时开发人员和管理员面临的各种安全挑战。在本节中,我们将重点介绍 Kubernetes 提供的设计方面、机制和功能,以应对一些挑战。通过明智地使用服务账户、网络策略、身份验证、授权、准入控制、AppArmor 和机密等能力,你可以达到相当不错的安全状态。
记住,Kubernetes 集群是一个更大系统的一部分,包含其他软件系统、人员和流程。Kubernetes 不能解决所有问题。你应该始终牢记一般的安全原则,如深度防御、按需知情和最小权限原则。
此外,记录下你认为在发生攻击时可能有用的所有信息,并设置警报以便在系统偏离其状态时进行早期检测。可能只是一个 bug,也可能是一次攻击。无论哪种情况,你都应该知道并做出响应。
理解 Kubernetes 中的服务账户
Kubernetes 有常规用户账户,这些账户在集群外部管理,供人类通过 kubectl 命令连接到集群使用,同时也有服务账户。
常规用户账户是全局的,可以访问集群中的多个命名空间。服务账户仅限于一个命名空间。这一点很重要。它确保了命名空间的隔离,因为每当 API 服务器收到来自 Pod 的请求时,其凭证仅适用于其所在的命名空间。
Kubernetes 代表 Pod 管理服务账户。每当 Kubernetes 实例化一个 Pod 时,它会分配一个服务账户,除非服务账户或 Pod 明确通过设置automountServiceAccountToken为False来选择退出。服务账户在与 API 服务器交互时标识所有 Pod 进程。每个服务账户都有一组凭证,挂载在一个机密卷中。每个命名空间都有一个默认的服务账户,称为 default。当你创建一个 Pod 时,它会自动分配默认的服务账户,除非你指定一个不同的服务账户。
如果你希望不同的 Pod 拥有不同的身份和权限,可以创建额外的服务账户。然后,你可以将不同的服务账户绑定到不同的角色。
创建一个名为 custom-service-account.yaml 的文件,内容如下:
apiVersion: v1
kind: ServiceAccount
metadata:
name: custom-service-account
现在输入以下命令:
$ kubectl create -f custom-service-account.yaml
serviceaccount/custom-service-account created
这是与默认服务账户一起列出的服务账户:
$ kubectl get serviceaccounts
NAME SECRETS AGE
custom-service-account 1 6s
default 1 2m28s
请注意,为你的新服务账户自动创建了一个机密:
$ kubectl get secret
NAME TYPE DATA AGE
custom-service-account-token-vbrbm kubernetes.io/service-account-token 3 62s
default-token-m4nfk kubernetes.io/service-account-token 3 3m24s
要获取更多细节,请输入以下命令:
$ kubectl get serviceAccounts/custom-service-account -o yaml
apiVersion: v1
kind: ServiceAccount
metadata:
creationTimestamp: "2022-06-19T18:38:22Z"
name: custom-service-account
namespace: default
resourceVersion: "784"
uid: f70f70cf-5b42-4a46-a2ff-b07792bf1220
secrets:
- name: custom-service-account-token-vbrbm
你可以通过输入以下命令查看包含ca.crt文件和令牌的机密信息:
$ kubectl get secret custom-service-account-token-vbrbm -o yaml
Kubernetes 如何管理服务账户?
API 服务器有一个专用组件称为服务账户准入控制器。它负责在创建 pod 时检查 API 服务器是否具有自定义服务账户,如果有的话,检查该自定义服务账户是否存在。如果未指定服务账户,则分配默认服务账户。
它还确保 pod 具有ImagePullSecrets,这在需要从远程镜像注册表拉取镜像时是必需的。如果 pod 规范没有任何 secrets,它将使用服务账户的ImagePullSecrets。
最后,它添加了一个包含 API 访问令牌的volume和一个volumeSource,挂载到/var/run/secrets/kubernetes.io/serviceaccount。
每当创建服务账户时,另一个名为令牌控制器的组件会创建并添加 API 令牌到密钥中。令牌控制器还监视 secrets,并在将 secrets 添加到或从服务账户中移除时添加或移除令牌。
服务账户控制器确保每个命名空间存在默认服务账户。
访问 API 服务器
访问 API 服务器需要一系列步骤,包括认证、授权和准入控制。在每个阶段,请求可能会被拒绝。每个阶段由多个插件串联在一起。
以下图表说明了这一过程:

图 4.2:访问 API 服务器
用户认证
当您首次创建集群时,会为您创建一些密钥和证书,用于对集群进行身份验证。这些凭据通常存储在文件~/.kube/config中,该文件可能包含多个集群的凭据。您也可以拥有多个配置文件,并通过设置KUBECONFIG环境变量或向 kubectl 传递--kubeconfig标志来控制使用哪个文件。kubectl 使用这些凭据通过 TLS(加密的 HTTPS 连接)与 API 服务器进行双向身份验证。让我们创建一个新的 KinD 集群,并通过设置KUBECONFIG环境变量将其凭据存储在专用配置文件中:
$ export KUBECONFIG=~/.kube/kind-config
$ kind create cluster
Creating cluster "kind" ...
 Ensuring node image (kindest/node:v1.23.4) 
 Preparing nodes 
 Writing configuration 
 Starting control-plane 
 Installing CNI 
 Installing StorageClass 
Set kubectl context to "kind-kind"
You can now use your cluster with:
kubectl cluster-info --context kind-kind
您可以使用以下命令查看您的配置:
$ kubectl config view
apiVersion: v1
clusters:
- cluster:
certificate-authority-data: DATA+OMITTED
server: https://127.0.0.1:61022
name: kind-kind
contexts:
- context:
cluster: kind-kind
user: kind-kind
name: kind-kind
current-context: kind-kind
kind: Config
preferences: {}
users:
- name: kind-kind
user:
client-certificate-data: REDACTED
client-key-data: REDACTED
这是 KinD 集群的配置。其他类型的集群可能看起来不同。
请注意,如果多个用户需要访问集群,则创建者应以安全的方式向其他用户提供必要的客户端证书和密钥。
这只是与 Kubernetes API 服务器本身建立基本信任。你还没有完成身份验证。各种身份验证模块可能会查看请求,并检查是否有附加的客户端证书、密码、承载令牌和 JWT 令牌(用于服务帐户)。大多数请求需要已验证的用户(无论是常规用户还是服务帐户),尽管也有一些匿名请求。如果请求在所有身份验证器中无法进行身份验证,它将被拒绝,并返回 401 HTTP 状态码(未授权,这个名称可能有点误导)。
集群管理员通过向 API 服务器提供各种命令行参数来确定使用哪些身份验证策略:
-
--client-ca-file=(用于从文件中指定的 x509 客户端证书) -
--token-auth-file=(用于从文件中指定的承载令牌) -
--basic-auth-file=(用于文件中指定的用户名/密码对) -
--enable-bootstrap-token-auth(用于 kubeadm 使用的引导令牌)
服务帐户使用自动加载的身份验证插件。管理员可以提供两个可选标志:
-
--service-account-key-file=(如果未指定,API 服务器将使用其 TLS 私钥作为 PEM 编码的密钥来签名承载令牌。) -
--service-account-lookup(启用时,如果令牌被删除,API 将会撤销这些令牌。)
还有其他几种方法,例如 OpenID Connect、webhooks、Keystone(OpenStack 身份服务)和身份验证代理。主要特点是身份验证阶段是可扩展的,可以支持任何身份验证机制。
各种身份验证插件会检查请求,并根据提供的凭证关联以下属性:
-
用户名(用户友好的名称)
-
UID(一个唯一标识符,比用户名更稳定)
-
组(用户所属的组名称集合)
-
额外字段(这些字段将字符串键映射到字符串值)
在 Kubernetes 1.11 中,kubectl 增加了使用凭证插件的功能,可以从提供者(如组织的 LDAP 服务器)获取不透明令牌。kubectl 会将这些凭证发送到 API 服务器,API 服务器通常使用 webhook 令牌验证器来验证凭证并接受请求。
身份验证器根本不知道特定用户被允许做什么。它们只是将一组凭证映射到一组身份。身份验证器按未指定的顺序运行;第一个接受传入凭证的身份验证器将与传入请求关联身份,并且身份验证被视为成功。如果所有身份验证器都拒绝凭证,则身份验证失败。
有趣的是,Kubernetes 完全不知道谁是它的常规用户。etcd 中没有用户列表。任何提供由与集群关联的证书颁发机构(CA)签名的有效证书的用户都会获得身份验证。
冒充
用户可以冒充不同的用户(前提是拥有适当的授权)。例如,管理员可能希望以具有较少权限的其他用户身份进行故障排除。这需要向 API 请求传递冒充头部。头部信息如下:
-
Impersonate-User: 指定要代表其执行操作的用户名。
-
Impersonate-Group: 指定要代表其执行操作的组名。可以通过多次指定此选项来提供多个组。此选项是可选的,但需要设置 Impersonate-User。
-
Impersonate-Extra-(extra name): 一个动态头部,用于将附加字段与用户关联。此选项是可选的,但需要设置 Impersonate-User。
使用 kubectl 时,可以传递 --as 和 --as-group 参数。
要冒充服务帐户,请输入以下内容:
kubectl --as system:serviceaccount:<namespace>:<service account name>
授权请求
用户通过身份验证后,授权开始。Kubernetes 具有通用的授权语义。一组授权模块会接收请求,其中包括已验证的用户名和请求的操作动词(如 list、get、watch、create 等)。与身份验证不同,所有授权插件都会处理每个请求。如果任何一个授权插件拒绝该请求,或者没有插件发表意见,则该请求会被拒绝,并返回 403 HTTP 状态码(禁止访问)。只有当至少一个插件接受请求且没有其他插件拒绝时,请求才会继续。
集群管理员通过指定 --authorization-mode 命令行标志来确定使用哪些授权插件,该标志是一个以逗号分隔的插件名称列表。
支持以下模式:
-
--authorization-mode=AlwaysDeny拒绝所有请求。如果不需要授权,可以使用此选项。 -
--authorization-mode=AlwaysAllow允许所有请求。如果不需要授权,可以使用此选项。这在测试期间非常有用。 -
--authorization-mode=ABAC允许基于简单的本地文件、用户配置的授权策略。ABAC 代表基于属性的访问控制。 -
--authorization-mode=RBAC是一种基于角色的机制,授权策略由 Kubernetes API 存储并驱动。RBAC 代表基于角色的访问控制。 -
--authorization-mode=Node是一种特殊模式,旨在授权 kubelet 发出的 API 请求。 -
--authorization-mode=Webhook允许通过远程服务使用 REST 来驱动授权。
你可以通过实现以下简单的 Go 接口,添加自定义授权插件:
type Authorizer interface {
Authorize(ctx context.Context, a Attributes) (authorized Decision, reason string, err error)
}
Attributes 输入参数也是一个接口,提供做出授权决策所需的所有信息:
type Attributes interface {
GetUser() user.Info
GetVerb() string
IsReadOnly() bool
GetNamespace() string
GetResource() string
GetSubresource() string
GetName() string
GetAPIGroup() string
GetAPIVersion() string
IsResourceRequest() bool
GetPath() string
}
你可以在 github.com/kubernetes/apiserver/blob/master/pkg/authorization/authorizer/interfaces.go 找到源代码。
使用 kubectl can-i 命令,你可以检查你可以执行的操作,甚至可以假扮其他用户:
$ kubectl auth can-i create deployments
Yes
$ kubectl auth can-i create deployments --as jack
no
kubectl 支持插件。稍后我们将在第十五章中深入讨论插件,扩展 Kubernetes。同时,我先提一下我最喜欢的插件之一是 rolesum。这个插件为你提供了用户或服务账户所有权限的总结。以下是一个例子:
$ kubectl rolesum job-controller -n kube-system
ServiceAccount: kube-system/job-controller
Secrets:
• */job-controller-token-tp72d
Policies:
• [CRB] */system:controller:job-controller  [CR] */system:controller:job-controller
Resource Name Exclude Verbs G L W C U P D DC
events.[,events.k8s.io] [*] [-] [-] 
jobs.batch [*] [-] [-] 
jobs.batch/finalizers [*] [-] [-] 
jobs.batch/status [*] [-] [-] 
pods [*] [-] [-] 
在此查看:github.com/Ladicle/kubectl-rolesum。
使用准入控制插件
好的。请求已通过身份验证和授权,但在执行之前还有一步。请求必须通过一系列准入控制插件的检查。与授权器类似,如果任何一个准入控制器拒绝了请求,则请求会被拒绝。准入控制器是一个很棒的概念。其核心思想是,可能存在一些全局集群问题,可以作为拒绝请求的依据。如果没有准入控制器,所有授权器都必须意识到这些问题并拒绝请求。但有了准入控制器,这些逻辑可以集中执行。此外,准入控制器还可以修改请求。准入控制器可以在验证模式或变更模式下运行。通常,集群管理员通过提供一个名为 admission-control 的命令行参数来决定运行哪些准入控制插件。其值是一个以逗号分隔且有序的插件列表。以下是 Kubernetes >= 1.9 推荐的插件列表(顺序很重要):
--admission-control=NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel,DefaultStorageClass,MutatingAdmissionWebhook,ValidatingAdmissionWebhook,ResourceQuota,DefaultTolerationSeconds
让我们来看一下可用的插件(不断有新的插件加入):
-
DefaultStorageClass:为创建未指定存储类的PersistentVolumeClaim请求添加默认存储类。 -
DefaultTolerationSeconds:设置 Pods 对于污点的默认容忍时间(如果尚未设置):notready:NoExecute和notreachable:NoExecute。 -
EventRateLimit:限制事件泛滥对 API 服务器的影响。 -
ExtendedResourceToleration:将节点上带有特殊资源(如 GPU 和 现场可编程门阵列 (FPGAs))的污点与请求这些资源的 Pods 上的容忍相结合。最终结果是,带有额外资源的节点将专门用于具有正确容忍的 Pods。 -
ImagePolicyWebhook:这个复杂的插件连接到一个外部后端,根据镜像决定是否应该拒绝请求。 -
LimitPodHardAntiAffinity:在requiredDuringSchedulingRequiredDuringExecution字段中,任何指定非kubernetes.io/hostname的 AntiAffinity 拓扑键的 Pod 将被拒绝。 -
LimitRanger:拒绝违反资源限制的请求。 -
MutatingAdmissionWebhook:调用已注册的变更 Webhook,这些 Webhook 可以修改其目标对象。请注意,由于其他变更 Webhook 的潜在更改,无法保证更改会生效。 -
NamespaceAutoProvision:如果请求中的命名空间不存在,则会创建该命名空间。 -
NamespaceLifecycle:拒绝在正在终止或不存在的命名空间中创建对象的请求。 -
ResourceQuota:拒绝违反命名空间资源配额的请求。 -
ServiceAccount:服务账户的自动化。 -
ValidatingAdmissionWebhook:准入控制器调用与请求匹配的验证 Webhook。匹配的 Webhook 会并行调用,如果任何一个 Webhook 拒绝请求,则整个请求失败。
正如你所看到的,准入控制插件有非常多样的功能。它们支持命名空间级的策略,并从资源管理和安全角度强制执行请求的有效性。这使得授权插件能够专注于有效的操作。ImagePolicyWebHook是验证镜像的门户,这是一个大挑战。MutatingAdmissionWebhook和ValidatingAdmissionWebhook是动态准入控制的门户,你可以在不将其编译到 Kubernetes 中的情况下部署自己的准入控制器。动态准入控制适用于资源的语义验证等任务(所有 Pod 是否都有标准的标签集合?)。我们将在第十六章《Kubernetes 治理》中深入讨论动态准入控制,因为它是 Kubernetes 中策略管理治理的基础。
通过认证、授权和准入等不同阶段的责任划分来验证传入请求,每个阶段都有自己的插件,这使得复杂的过程更容易理解、使用和扩展。
变更型准入控制器提供了大量的灵活性,并能够自动执行某些策略,而不会给用户带来负担(例如,如果命名空间不存在,则自动创建命名空间)。
安全保障 Pod
Pod 安全是一个重要问题,因为 Kubernetes 调度 Pod 并让它们运行。为了保护 Pod 和容器,有几种独立的机制。通过这些机制的配合支持深度防御,即使攻击者(或错误)绕过了某个机制,也会被另一个机制阻止。
使用私有镜像仓库
这种方法让你对集群的信心大增,确保它只拉取你之前审核过的镜像,同时也能更好地管理升级。考虑到软件供应链攻击的增加,这是一种重要的防御措施。你可以在每个节点上配置HOME/.docker/config.json。但是,在许多云服务提供商的环境下,你无法这么做,因为节点是自动为你提供的。
ImagePullSecrets
这种方法推荐用于云服务提供商上的集群。其理念是,由 Pod 提供注册表凭证,因此无论 Pod 调度在哪个节点上运行都不重要。这解决了节点级别的.dockercfg问题。
首先,您需要为凭证创建一个 secret 对象:
$ kubectl create secret docker-registry the-registry-secret \
--docker-server=<docker registry server> \
--docker-username=<username> \
--docker-password=<password> \
--docker-email=<email>
secret 'docker-registry-secret' created.
如果需要,您可以为多个注册中心(或相同注册中心的多个用户)创建 secrets。kubelet 将合并所有 ImagePullSecrets。
但是,由于 Pod 只能访问自己命名空间中的 secrets,因此必须在每个希望运行 Pod 的命名空间中创建一个 secret。一旦定义了 secret,就可以将其添加到 Pod 规格中,并在集群上运行一些 Pod。Pod 将使用 secret 中的凭证从目标镜像注册中心拉取镜像:
apiVersion: v1
kind: Pod
metadata:
name: cool-pod
namespace: the-namespace
spec:
containers:
- name: cool-container
image: cool/app:v1
imagePullSecrets:
- name: the-registry-secret
为 Pod 和容器指定安全上下文
Kubernetes 允许在 Pod 级别设置安全上下文,并在容器级别设置额外的安全上下文。Pod 安全上下文是一组操作系统级的安全设置,例如 UID、GID、能力和 SELinux 角色。Pod 安全上下文还可以将其安全设置(特别是 fsGroup 和 seLinuxOptions)应用于卷。
这是一个 Pod 安全上下文的示例:
apiVersion: v1
kind: Pod
metadata:
name: some-pod
spec:
securityContext:
fsGroup: 1234
supplementalGroups: [5678]
seLinuxOptions:
level: 's0:c123,c456'
containers:
...
有关 Pod 安全上下文字段的完整列表,请查看 kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#podsecuritycontext-v1-core。
容器安全上下文应用于每个容器,并添加容器特定的设置。容器安全上下文中的某些字段与 Pod 安全上下文中的字段重叠。如果容器安全上下文指定了这些字段,它们将覆盖 Pod 安全上下文中的值。容器上下文设置不能应用于卷,卷即使仅挂载到特定容器中,也仍然保持在 Pod 级别。
这是一个包含容器安全上下文的 Pod:
apiVersion: v1
kind: Pod
metadata:
name: some-pod
spec:
containers:
- name: some-container
...
securityContext:
privileged: true
seLinuxOptions:
level: 's0:c123,c456'
有关容器安全上下文字段的完整列表,请查看 kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#securitycontext-v1-core。
Pod 安全标准
Kubernetes 定义了适用于不同安全需求的安全配置文件,并汇总了推荐的设置。特权配置文件提供所有权限,并且不幸的是,它是默认配置。基础配置文件是一个最小的安全配置文件,只是防止特权提升。受限配置文件遵循强化最佳实践。
在此处查看更多信息:kubernetes.io/docs/concepts/security/pod-security-standards/。
使用 AppArmor 保护您的集群
AppArmor 是一个 Linux 内核安全模块。通过 AppArmor,您可以将容器中运行的进程限制在一组有限的资源中,例如网络访问、Linux 能力和文件权限。您可以通过配置文件来配置 AppArmor。
AppArmor 要求
AppArmor 支持在 Kubernetes 1.4 中作为 Beta 功能加入。它并非在所有操作系统中都可用,因此你必须选择一个支持的操作系统发行版才能使用它。Ubuntu 和 SUSE Linux 支持 AppArmor,并默认启用它。其他发行版则是可选支持。
要检查 AppArmor 是否启用,请连接到节点(例如通过ssh)并输入以下命令:
$ cat /sys/module/apparmor/parameters/enabled
Y
如果结果是Y,则表示已启用。如果文件不存在或结果不是Y,则表示未启用。
该配置文件必须加载到内核中。检查以下文件:
/sys/kernel/security/apparmor/profiles
Kubernetes 没有提供内建机制来将配置文件加载到节点中。通常你需要一个具有节点级别权限的 DaemonSet 来将必要的 AppArmor 配置文件加载到节点中。
有关将 AppArmor 配置文件加载到节点中的更多详细信息,请查看以下链接:kubernetes.io/docs/tutorials/security/apparmor/#setting-up-nodes-with-profiles。
使用 AppArmor 保护 Pod
由于 AppArmor 仍处于 Beta 阶段,因此你需要将元数据作为注释而非正式字段来指定。当它脱离 Beta 阶段后,这一点将会改变。
要将配置文件应用到容器,请添加以下注释:
container.apparmor.security.beta.kubernetes.io/<container name>: <profile reference>
配置文件的引用可以是default配置文件、runtime/default或主机/本地主机上的配置文件。
这是一个示例配置文件,防止写入文件:
> \#include \<tunables/global\>
>
> profile k8s-apparmor-example-deny-write flags=(attach\\\_disconnected)
> {
>
> \#include \<abstractions/base\>
>
> file,
>
> \# Deny all file writes.
>
> deny /\\\*\\\* w,
>
> }
AppArmor 不是 Kubernetes 资源,因此其格式并不是你熟悉的 YAML 或 JSON 格式。
要验证配置文件是否正确附加,请检查进程 1 的属性:
kubectl exec <pod-name> cat /proc/1/attr/current
默认情况下,Pod 可以在集群中的任何节点上调度。这意味着配置文件应加载到每个节点中。这是 DaemonSet 的经典用例。
编写 AppArmor 配置文件
手动编写 AppArmor 配置文件并不简单。有一些工具可以提供帮助:aa-genprof和aa-logprof可以为你生成配置文件,并通过在投诉模式下运行应用程序来协助微调。工具会跟踪应用程序的活动和 AppArmor 警告,并创建相应的配置文件。这种方法有效,但感觉有些笨重。
我最喜欢的工具是 bane,它通过基于 TOML 语法的简化配置语言生成 AppArmor 配置文件。bane 配置文件非常易读,且易于理解。以下是一个示例 bane 配置文件:
# name of the profile, we will auto prefix with `docker-`
# so the final profile name will be `docker-nginx-sample`
Name = "nginx-sample"
[Filesystem]
# read only paths for the container
ReadOnlyPaths = [
"/bin/**",
"/boot/**",
"/dev/**",
"/etc/**",
"/home/**",
"/lib/**",
"/lib64/**",
"/media/**",
"/mnt/**",
"/opt/**",
"/proc/**",
"/root/**",
"/sbin/**",
"/srv/**",
"/tmp/**",
"/sys/**",
"/usr/**",
]
# paths where you want to log on write
LogOnWritePaths = [
"/**"
]
# paths where you can write
WritablePaths = [
"/var/run/nginx.pid"
]
# allowed executable files for the container
AllowExec = [
"/usr/sbin/nginx"
]
# denied executable files
DenyExec = [
"/bin/dash",
"/bin/sh",
"/usr/bin/top"
]
# allowed capabilities
[Capabilities]
Allow = [
"chown",
"dac_override",
"setuid",
"setgid",
"net_bind_service"
]
[Network]
# if you don't need to ping in a container, you can probably
# set Raw to false and deny network raw
Raw = false
Packet = false
Protocols = [
"tcp",
"udp",
"icmp"
]
生成的 AppArmor 配置文件相当复杂(冗长且复杂)。
你可以在这里找到有关 bane 的更多信息:github.com/genuinetools/bane。
Pod 安全准入
Pod 安全准入控制是一个负责管理 Pod 安全标准的准入控制器(kubernetes.io/docs/concepts/security/pod-security-standards/)。Pod 安全限制应用于命名空间级别。目标命名空间中的所有 Pod 都将根据相同的安全配置文件进行检查(特权、基线或限制)。
请注意,Pod 安全准入控制不会设置相关的安全上下文。它仅验证 Pod 是否符合目标策略。
有三种模式:
-
enforce:策略违反将导致 Pod 被拒绝。 -
audit:策略违反将导致在审计日志中记录的事件中添加审计注释,但 Pod 仍将被允许。 -
warn:策略违反将触发用户警告,但 Pod 仍将被允许。
要在命名空间中启用 Pod 安全准入控制,你只需为目标命名空间添加一个标签:
$ MODE=warn # One of enforce, audit, or warn
$ LEVEL=baseline # One of privileged, baseline, or restricted
$ kubectl label namespace/ns-1 pod-security.kubernetes.io/${MODE}: ${LEVEL}
namespace/ns-1 created
管理网络策略
节点、Pod 和容器的安全性至关重要,但这还不够。网络分段对于设计安全的 Kubernetes 集群至关重要,它不仅允许多租户的支持,还能最小化安全漏洞的影响。深度防御要求你将系统中不需要相互通信的部分进行隔离,同时仔细管理网络流量的方向、协议和端口。
网络策略允许你对集群进行精细化控制和适当的网络分段。从核心上讲,网络策略是一组应用于通过标签选择的一组命名空间和 Pod 的防火墙规则。这非常灵活,因为标签可以定义虚拟网络段,并且可以在 Kubernetes 资源级别进行管理。
这相比于使用传统方法(如 IP 地址范围和子网掩码)来对网络进行分段是一项巨大的改进,后者常常导致 IP 地址用完或仅仅为了以防万一而分配过多的 IP 地址。
然而,如果你使用服务网格,你可能不需要使用网络策略,因为服务网格可以执行相同的角色。更多关于服务网格的内容,请参见第十四章,使用服务网格。
选择一个支持的网络解决方案
一些网络后端(网络插件)不支持网络策略。例如,流行的 Flannel 无法用来应用策略。这一点至关重要。即使你的网络插件不支持网络策略,你仍然可以定义网络策略。只是这些策略不会产生任何效果,从而给你一种错误的安全感。以下是支持网络策略(包括入站和出站)的一些网络插件列表:
-
Calico
-
WeaveNet
-
Canal
-
Cillium
-
Kube-Router
-
Romana
-
Contiv
如果你在托管 Kubernetes 服务上运行你的集群,那么选择已经为你做出了,尽管在某些托管 Kubernetes 服务上你也可以安装自定义 CNI 插件。
我们将在第十章,探索 Kubernetes 网络 中深入探讨网络插件的细节。在这里,我们重点关注网络策略。
定义网络策略
你可以使用标准的 YAML 清单来定义网络策略。支持的协议包括 TCP、UDP 和 SCTP(自 Kubernetes 1.20 起)。
这里是一个示例策略:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: the-network-policy
namespace: default
spec:
podSelector:
matchLabels:
role: db
ingress:
- from:
- namespaceSelector:
matchLabels:
project: cool-project
- podSelector:
matchLabels:
role: frontend
ports:
- protocol: TCP
port: 6379
spec 部分包含两个重要部分,podSelector 和 ingress。podSelector 决定该网络策略适用于哪些 pods,ingress 决定哪些命名空间和 pods 可以访问这些 pods,以及它们可以使用哪些协议和端口。
在前面的示例网络策略中,pod 选择器指定了网络策略的目标是所有标记为 role: db 的 pods。ingress 部分有一个 from 子部分,包含命名空间选择器和 pod 选择器。集群中所有标记为 project: cool-project 的命名空间,以及这些命名空间中所有标记为 role: frontend 的 pods,都可以访问标记为 role: db 的目标 pods。ports 部分定义了一个协议和端口的配对列表,进一步限制了允许使用的协议和端口。在这种情况下,协议是 tcp,端口是 6379(标准的 Redis 端口)。如果你想针对一系列端口,可以使用 endPort,如下所示:
ports:
- protocol: TCP
port: 6379
endPort: 7000
请注意,网络策略是集群范围的,因此集群中多个命名空间的 pods 可以访问目标命名空间。当前命名空间始终包括在内,即使它没有 project:cool 标签,标记为 role:frontend 的 pods 仍然可以访问。
重要的是要认识到,网络策略是以白名单方式操作的。默认情况下,所有访问都是禁止的,网络策略可以为匹配标签的某些 pods 打开特定的协议和端口。然而,网络策略的白名单特性仅适用于至少为一个网络策略所选中的 pods。如果一个 pod 没有被选中,它将允许所有访问。始终确保你的所有 pods 都被网络策略覆盖。
白名单特性带来的另一个影响是,如果存在多个网络策略,则所有规则的统一效果将一起生效。如果一个策略允许访问端口 1234,而另一个策略允许访问端口 5678,那么一个 pod 可以通过 1234 或 5678 访问。
为了负责任地使用网络策略,可以考虑从 deny-all 网络策略开始:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-all
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
然后,开始添加网络策略,以明确允许某些 pods 访问。请注意,你必须为每个命名空间应用 deny-all 策略:
$ k create -n ${NAMESPACE} -f deny-all-network-policy.yaml
限制出口流量到外部网络
Kubernetes 1.8 增加了出口网络策略的支持,因此你也可以控制出站流量。以下是一个示例,防止访问外部 IP 1.2.3.4。order: 999 确保在其他策略之前应用该策略:
apiVersion: networking.k8s.io/ v1
kind: NetworkPolicy
metadata:
name: default-deny-egress
spec:
order: 999
egress:
- action: deny
destination:
net: 1.2.3.4
source: {}
跨命名空间策略
如果你将集群分为多个命名空间,当 Pod 需要跨命名空间通信时,这有时会派上用场。你可以在网络策略中指定 ingress.namespaceSelector 字段,以启用来自多个命名空间的访问。例如,如果你有生产和预发布命名空间,并且定期将生产数据的快照填充到预发布环境中,这会非常有用。
网络策略的成本
网络策略并非免费的。你的 CNI 插件可能会在集群中的每个节点上安装额外的组件。这些组件使用宝贵的资源,并且可能会导致你的 Pod 因容量不足而被驱逐。例如,Calico CNI 插件会在kube-system命名空间中安装多个部署:
$ k get deploy -n kube-system -o name | grep calico
deployment.apps/calico-node-vertical-autoscaler
deployment.apps/calico-typha
deployment.apps/calico-typha-horizontal-autoscaler
deployment.apps/calico-typha-vertical-autoscaler
它还会配置一个 DaemonSet,在每个节点上运行一个 Pod:
$ k get ds -n kube-system -o name | grep calico-node
daemonset.apps/calico-node
使用机密
在安全系统中,机密至关重要。它们可以是凭证,如用户名和密码、访问令牌、API 密钥、证书或加密密钥。机密通常很小。如果你有大量的数据需要保护,你应该加密它,并将加密/解密密钥作为机密保存。
在 Kubernetes 中存储机密
Kubernetes 默认会将机密以明文存储在 etcd 中。这意味着应当限制直接访问 etcd,并加以谨慎保护。从 Kubernetes 1.7 开始,你现在可以在静态存储时加密你的机密(当它们被 etcd 存储时)。
机密是在命名空间级别进行管理的。Pod 可以通过机密卷作为文件或作为环境变量挂载机密。从安全角度来看,这意味着任何可以在命名空间中创建 Pod 的用户或服务,都可以访问该命名空间中管理的任何机密。如果你想限制对某个机密的访问,可以将其放在一个只有有限用户或服务可以访问的命名空间中。
当机密挂载到容器中时,它从不写入磁盘。它存储在 tmpfs 中。当 kubelet 与 API 服务器通信时,通常使用 TLS,因此机密在传输过程中受到保护。
Kubernetes 的机密限制为 1 MB。
配置静态加密
启动 API 服务器时,需要传递此参数:--encryption-provider-config。
这是一个示例加密配置:
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- identity: {}
- aesgcm:
keys:
- name: key1
secret: c2VjcmV0IGlzIHNlY3VyZQ==
- name: key2
secret: dGhpcyBpcyBwYXNzd29yZA==
- aescbc:
keys:
- name: key1
secret: c2VjcmV0IGlzIHNlY3VyZQ==
- name: key2
secret: dGhpcyBpcyBwYXNzd29yZA==
- secretbox:
keys:
- name: key1
secret: YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoxMjM0NTY=
创建机密
在尝试创建需要机密的 Pod 之前,必须先创建机密。机密必须存在,否则 Pod 创建将失败。
你可以使用以下命令创建机密:kubectl create secret。
在这里,我创建了一个名为hush-hush的通用机密,其中包含两个键,一个用户名和一个密码:
$ k create secret generic hush-hush \
--from-literal=username=tobias \
--from-literal=password=cutoffs
secret/hush-hush created
生成的机密是不可见的:
$ k describe secrets/hush-hush
Name: hush-hush
Namespace: default
Labels: <none>
Annotations: <none>
Type: Opaque
Data
====
password: 7 bytes
username: 6 bytes
你可以使用 --from-file 而不是 --from-literal 从文件创建机密,如果你将机密值编码为 base64,还可以手动创建机密。
机密中的键名必须遵循 DNS 子域的规则(没有前导点)。
解码机密
要获取机密的内容,可以使用 kubectl get secret:
$ k get secrets/hush-hush -o yaml
apiVersion: v1
data:
password: Y3V0b2Zmcw==
username: dG9iaWFz
kind: Secret
metadata:
creationTimestamp: "2022-06-20T19:49:56Z"
name: hush-hush
namespace: default
resourceVersion: "51831"
uid: 93e8d6d1-4c7f-4868-b146-32d1eb02b0a6
type: Opaque
这些值是 base64 编码的。你需要自行解码:
$ k get secrets/hush-hush -o jsonpath='{.data.password}' | base64 --decode
cutoffs
在容器中使用秘密
容器可以通过挂载来自 Pod 的卷以文件的形式访问秘密。另一种方法是将秘密作为环境变量访问。最后,如果容器(假设其服务帐户有权限)可以直接访问 Kubernetes API 或使用 kubectl get secret。
要使用作为卷挂载的秘密,Pod 清单应声明该卷,并且应在容器的规范中挂载它:
apiVersion: v1
kind: Pod
metadata:
name: pod-with-secret
spec:
containers:
- name: container-with-secret
image: g1g1/py-kube:0.3
command: ["/bin/bash", "-c", "while true ; do sleep 10 ; done"]
volumeMounts:
- name: secret-volume
mountPath: "/mnt/hush-hush"
readOnly: true
volumes:
- name: secret-volume
secret:
secretName: hush-hush
卷名(secret-volume)将 Pod 卷绑定到容器中的挂载点。多个容器可以挂载相同的卷。当此 Pod 正在运行时,用户名和密码作为文件保存在 /etc/hush-hush 目录下:
$ k create -f pod-with-secret.yaml
pod/pod-with-secret created
$ k exec pod-with-secret -- cat /mnt/hush-hush/username
tobias
$ k exec pod-with-secret -- cat /mnt/hush-hush/password
cutoffs
使用 Vault 管理秘密
Kubernetes secrets 是存储和管理 Kubernetes 上敏感数据的良好基础。然而,将加密数据存储在 etcd 中只是工业级秘密管理解决方案的冰山一角。这就是 Vault 发挥作用的地方。
Vault 是一个基于身份的源秘密管理系统,由 HashiCorp 自 2015 年以来开发。Vault 被认为是业内最好的,并且几乎没有什么非专有的竞争对手。它提供了一个 HTTP API、CLI 和 UI 来管理你的秘密。Vault 是一个成熟且经过实战检验的解决方案,已被大量企业组织和较小的公司所使用。Vault 拥有良好的安全性和威胁模型,覆盖了大量的内容。从实际操作角度看,只要你能确保 Vault 部署的物理安全性,Vault 就能保持你的秘密安全,并使其易于管理和审计。
在 Kubernetes 上运行 Vault 时,还需要采取一些其他重要措施,以确保 Vault 安全模型保持完整,例如:
-
多租户集群的注意事项(单一 Vault 将被所有租户共享)
-
端到端 TLS(在某些情况下 Kubernetes 可能跳过 TLS)
-
关闭进程核心转储以避免泄露 Vault 加密密钥
-
确保启用 mlock 以避免将内存交换到磁盘并泄露 Vault 加密密钥
-
容器主管和 Pod 应以非 root 用户身份运行
你可以在这里找到关于 Vault 的大量信息:www.vaultproject.io/。
Vault 的部署和配置非常简单。如果你想尝试,跟着这个教程来做:learn.hashicorp.com/tutorials/vault/kubernetes-minikube-raft。
运行多租户集群
在本节中,我们将简要地看一下使用单个集群托管多个用户或多个用户社区的系统的选项(这也被称为多租户)。其理念是,这些用户是完全隔离的,甚至可能不知道他们与其他用户共享同一个集群。
每个用户社区将拥有自己的资源,并且它们之间不会有任何通信(除了可能通过公共端点)。Kubernetes 命名空间的概念是这一理念的最终体现。但它们并不提供绝对的隔离。另一种解决方案是使用虚拟集群,其中每个命名空间对用户来说看起来像是一个完全独立的集群。
查看www.vcluster.com/了解有关虚拟集群的更多详细信息。
多租户集群的案例
为什么要为多个隔离的用户或部署运行一个集群?直接为每个用户创建一个专用集群不是更简单吗?主要有两个原因:成本和操作复杂性。如果你有许多相对较小的部署,并且想为每个部署创建一个专用集群,那么你将为每个集群配置一个独立的控制平面节点,可能还会有一个三节点的 etcd 集群。成本会逐渐增加。操作复杂性也非常重要。管理数十个、数百个甚至数千个独立集群绝非易事。每次升级和每个补丁都需要应用到每个集群。操作可能会失败,而你还得管理一个集群舰队,其中一些集群可能处于与其他集群略有不同的状态。跨所有集群的元操作可能更为困难。你需要汇总并编写工具来执行操作并收集所有集群的数据。
让我们看看一些多租户隔离社区或部署的使用案例和需求:
-
一个为软件即服务提供平台或服务的提供商
-
管理独立的测试、预发布和生产环境
-
将责任委派给社区/部署管理员
-
强制对每个社区施行资源配额和限制
-
用户只会看到他们社区中的资源
使用命名空间实现安全的多租户
Kubernetes 命名空间是安全的多租户集群的良好起点。这并不令人惊讶,因为命名空间的设计目标之一就是如此。
除了内置的kube-system和 default 命名空间外,你可以轻松创建新的命名空间。以下是一个 YAML 文件,将创建一个名为custom-namespace的新命名空间。它只有一个名为 name 的元数据项。没有比这更简单的了:
apiVersion: v1
kind: Namespace
metadata:
name: custom-namespace
让我们创建命名空间:
$ k create -f custom-namespace.yaml
namespace/custom-namespace created
$ k get ns
NAME STATUS AGE
custom-namespace Active 5s
default Active 24h
kube-node-lease Active 24h
kube-public Active 24h
kube-system Active 24h
我们可以看到默认命名空间、我们的新custom-namespace,以及其他几个以kube-为前缀的系统命名空间。
状态字段可以是Active或Terminating。当你删除一个命名空间时,它将进入Terminating状态。当命名空间处于该状态时,你将无法在该命名空间中创建新资源。这简化了命名空间资源的清理,并确保命名空间被真正删除。如果没有这一点,复制控制器在现有 Pod 被删除时可能会创建新的 Pod。
有时,命名空间可能会在终止时挂起。我写了一个名为 k8s-namespace-deleter 的小 Go 工具来删除顽固的命名空间。
在这里查看: github.com/the-gigi/k8s-namespace-deleter
它也可以作为 kubectl 插件使用。
要操作一个命名空间,你需要在 kubectl 命令中添加 --namespace(或简写为 -n)参数。以下是在 custom-namespace 命名空间中以交互模式运行 Pod 的方法:
$ k run trouble -it -n custom-namespace --image=g1g1/py-kube:0.3 bash
If you don't see a command prompt, try pressing enter.
root@trouble:/#
在 custom-namespace 中列出 Pod 只返回我们刚启动的那个 Pod:
$ k get po -n custom-namespace
NAME READY STATUS RESTARTS AGE
trouble 1/1 Running 1 (15s ago) 57s
避免命名空间的陷阱
命名空间很好,但有时会带来一些摩擦。当你仅使用默认命名空间时,可以直接省略命名空间。使用多个命名空间时,你必须为每个资源指定命名空间。这可能增加一定的负担,但并不会带来危险。
然而,如果一些用户(例如集群管理员)可以访问多个命名空间,那么你就有可能不小心修改或查询错误的命名空间。避免这种情况的最佳方法是严格封闭命名空间,并要求每个命名空间使用不同的用户和凭证,就像你在本地机器或远程机器上进行大多数操作时应使用普通用户账户,而仅在必要时通过 sudo 使用 root 权限。
此外,你应当使用一些工具来帮助明确你正在操作的命名空间(例如,在命令行中工作时使用 Shell 提示符,或者在 Web 界面中显著显示命名空间)。最受欢迎的工具之一是 kubens(与 kubectx 一起提供),可在 github.com/ahmetb/kubectx 下载。
确保能够操作专用命名空间的用户无法访问默认命名空间。否则,每当他们忘记指定命名空间时,就会默默地操作默认命名空间。
使用虚拟集群来实现强多租户
命名空间是可行的,但对于强多租户来说并不完全够用。命名空间隔离显然只适用于命名空间资源。但 Kubernetes 有许多集群级资源(特别是 CRD)。租户将共享这些资源。此外,控制平面的版本、安全性和审计也将被共享。
一个简单的解决方案是不使用多租户。每个租户都使用一个单独的集群。但如果租户数量很多且较小,这样做就不高效了。
Loft.sh 的 vcluster 项目 (www.vcluster.com) 采用了一种创新的方法,在物理 Kubernetes 集群上托管多个虚拟集群,这些虚拟集群对用户而言表现得像常规的 Kubernetes 集群,彼此之间以及与宿主集群完全隔离。这样可以充分利用多租户的所有好处,而无需担心命名空间级别隔离的缺点。
这是 vcluster 的架构:

图 4.3:vcluster 架构
让我们创建几个虚拟集群。首先安装 vcluster CLI:www.vcluster.com/docs/getting-started/setup。
确保它已正确安装:
$ vcluster version
vcluster version 0.10.1
现在,我们可以创建一些虚拟集群。你可以使用 vcluster CLI、Helm 或 kubectl 创建虚拟集群。我们选择 vcluster CLI。
$ vcluster create tenant-1
info Creating namespace vcluster-tenant-1
info Detected local kubernetes cluster kind. Will deploy vcluster with a NodePort
info Create vcluster tenant-1...
done √ Successfully created virtual cluster tenant-1 in namespace vcluster-tenant-1
info Waiting for vcluster to come up...
warn vcluster is waiting, because vcluster pod tenant-1-0 has status: ContainerCreating
info Starting proxy container...
done √ Switched active kube context to vcluster_tenant-1_vcluster-tenant-1_kind-kind
- Use `vcluster disconnect` to return to your previous kube context
- Use `kubectl get namespaces` to access the vcluster
让我们创建另一个虚拟集群:
$ vcluster create tenant-2
? You are creating a vcluster inside another vcluster, is this desired?
[Use arrows to move, enter to select, type to filter]
> No, switch back to context kind-kind
Yes
哎呀。在创建tenant-1虚拟集群后,Kubernetes 上下文切换到了这个集群。当我尝试创建tenant-2时,vcluster CLI 足够聪明地提醒了我。让我们再试一次:
$ k config use-context kind-kind
Switched to context "kind-kind".
$ vcluster create tenant-2
info Creating namespace vcluster-tenant-2
info Detected local kubernetes cluster kind. Will deploy vcluster with a NodePort
info Create vcluster tenant-2...
done √ Successfully created virtual cluster tenant-2 in namespace vcluster-tenant-2
info Waiting for vcluster to come up...
info Stopping docker proxy...
info Starting proxy container...
done √ Switched active kube context to vcluster_tenant-2_vcluster-tenant-2_kind-kind
- Use `vcluster disconnect` to return to your previous kube context
- Use `kubectl get namespaces` to access the vcluster
让我们检查一下我们的集群:
$ k config get-contexts -o name
kind-kind
vcluster_tenant-1_vcluster-tenant-1_kind-kind
vcluster_tenant-2_vcluster-tenant-2_kind-kind
是的,我们的两个虚拟集群已经可以使用。让我们看看主机kind集群中的命名空间:
$ k get ns --context kind-kind
NAME STATUS AGE
custom-namespace Active 3h6m
default Active 27h
kube-node-lease Active 27h
kube-public Active 27h
kube-system Active 27h
local-path-storage Active 27h
vcluster-tenant-1 Active 15m
vcluster-tenant-2 Active 3m48s
我们可以看到虚拟集群的两个新命名空间。让我们看看vcluster-tenant-1命名空间中运行了什么:
$ k get all -n vcluster-tenant-1 --context kind-kind
NAME READY STATUS RESTARTS AGE
pod/coredns-5df468b6b7-rj4nr-x-kube-system-x-tenant-1 1/1 Running 0 16m
pod/tenant-1-0 2/2 Running 0 16m
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kube-dns-x-kube-system-x-tenant-1 ClusterIP 10.96.200.106 <none> 53/UDP,53/TCP,9153/TCP 16m
service/tenant-1 NodePort 10.96.107.216 <none> 443:32746/TCP 16m
service/tenant-1-headless ClusterIP None <none> 443/TCP 16m
service/tenant-1-node-kind-control-plane ClusterIP 10.96.235.53 <none> 10250/TCP 16m
NAME READY AGE
statefulset.apps/tenant-1 1/1 16m
现在,让我们看看虚拟集群中有哪些命名空间:
$ k get ns --context vcluster_tenant-1_vcluster-tenant-1_kind-kind
NAME STATUS AGE
kube-system Active 17m
default Active 17m
kube-public Active 17m
kube-node-lease Active 17m
只是 k3s 集群的默认命名空间(vcluster 基于 k3s)。让我们创建一个新的命名空间并验证它是否仅出现在虚拟集群中:
$ k create ns new-ns --context vcluster_tenant-1_vcluster-tenant-1_kind-kind
namespace/new-ns created
$ k get ns new-ns --context vcluster_tenant-1_vcluster-tenant-1_kind-kind
NAME STATUS AGE
new-ns Active 19s
$ k get ns new-ns --context vcluster_tenant-2_vcluster-tenant-2_kind-kind
Error from server (NotFound): namespaces "new-ns" not found
$ k get ns new-ns --context kind-kind
Error from server (NotFound): namespaces "new-ns" not found
新的命名空间只在它创建的虚拟集群中可见,正如预期的那样。
在本节中,我们介绍了多租户集群,它们的用途,以及不同的租户隔离方法,如命名空间和虚拟集群。
总结
在本章中,我们讨论了开发人员和管理员在构建系统和部署应用程序时面临的众多安全挑战。我们还探讨了许多安全特性和灵活的基于插件的安全模型,它提供了多种限制、控制和管理容器、Pod 和节点的方法。Kubernetes 已经提供了针对大多数安全挑战的多种解决方案,随着 AppArmor 和各种插件从 alpha/beta 版本过渡到正式发布,这些解决方案将会变得更好。最后,我们考虑了如何使用命名空间和虚拟集群来支持在同一个 Kubernetes 集群中进行多租户社区或部署。
在下一章,我们将详细研究许多 Kubernetes 资源和概念,以及如何有效地使用和组合它们。Kubernetes 对象模型建立在少数通用概念(如资源、清单和元数据)坚实的基础之上。这使得 Kubernetes 具备了一个可扩展而又出奇一致的对象模型,能够为开发者和管理员提供一系列多样化的能力。
加入我们的 Discord!
与其他用户、云计算专家、作者以及志同道合的专业人士一起阅读本书。
提问、为其他读者提供解决方案、通过“问我任何问题”环节与作者交流等等。
扫描二维码或访问链接立即加入社区。

第五章:在实践中使用 Kubernetes 资源
在本章中,我们将设计一个虚构的大规模平台,挑战 Kubernetes 的能力和可扩展性。Hue 平台旨在创造一个全知全能的数字助手。Hue 是你的数字延伸。Hue 会帮助你做任何事情,找到任何东西,并且在许多情况下,会代表你做很多事情。显然,它需要存储大量信息,集成许多外部服务,响应通知和事件,并在与你互动时非常智能。
本章将借此机会让我们更好地了解 kubectl 及相关工具,深入探讨我们之前见过的熟悉资源(如 Pods),以及一些新资源(如 Jobs)。我们将探讨高级调度和资源管理。
本章将涉及以下主题:
-
设计 Hue 平台
-
使用 Kubernetes 构建 Hue 平台
-
分离内部和外部服务
-
高级调度
-
使用命名空间限制访问
-
使用 Kustomization 构建层次化集群结构
-
启动任务
-
混合非集群组件
-
使用 Kubernetes 管理 Hue 平台
-
使用 Kubernetes 发展 Hue 平台
为了明确,这只是一个设计练习!我们并不打算真正构建 Hue 平台。这个练习的目的是展示 Kubernetes 在大规模系统中,处理多个活动部分时所能提供的广泛功能。
在本章结束时,你将清楚地看到 Kubernetes 的强大,并了解它如何作为构建复杂系统的基础。
设计 Hue 平台
在本节中,我们将为令人惊叹的 Hue 平台奠定基础,并定义其范围。Hue 不是“大哥”;Hue 是“小弟”!Hue 会做你允许它做的任何事情。Hue 能做很多事,这可能会让一些人感到担忧,但你可以选择让 Hue 帮助你多少。准备好迎接一场疯狂的旅程吧!
定义 Hue 的范围
Hue 将管理你的数字身份。它会比你更了解你自己。以下是 Hue 可以管理和帮助你的部分服务列表:
-
搜索和内容聚合
-
医疗 – 电子健康记录,DNA 测序
-
智能家居
-
财务 – 银行业务,储蓄,退休,投资
-
办公室
-
社交
-
旅行
-
健康
-
家庭
让我们来看看 Hue 平台的一些功能,例如智能提醒和通知、安全、身份和隐私。
智能提醒和通知
让我们想象一下各种可能性。Hue 会了解你,也了解你的朋友,以及所有领域中其他用户的集合。Hue 会实时更新其模型。它不会被过时的数据所困扰。它会代表你行动,呈现相关信息,并不断学习你的偏好。它可以推荐你可能喜欢的新节目或书籍,根据你和你家人或朋友的日程安排预订餐厅,甚至控制你家的自动化系统。
安全性、身份和隐私
Hue 是你的在线代理。有人窃取你的 Hue 身份,甚至只是窃听你与 Hue 的互动,将带来灾难性后果。潜在用户可能甚至不愿意将他们的身份交给 Hue 组织。让我们设计一个无信任系统,使用户随时拥有切断 Hue 的权力。以下是一些想法:
-
通过专用设备实现强身份验证,采用多因素授权,包括多个生物识别因素
-
定期更换凭证
-
快速暂停服务并验证所有外部服务的身份(将要求每个服务提供商提供原始身份凭证)
-
Hue 后端将通过短生命周期令牌与所有外部服务进行交互
-
将 Hue 架构设计为松耦合微服务的集合,并确保强大的隔离性
-
GDPR 合规性
-
端到端加密
-
避免拥有关键数据(让外部服务提供商来管理)
Hue 的架构需要支持巨大的变化和灵活性。它还需要非常具有扩展性,因为现有功能和外部服务会不断升级,新的功能和外部服务也会不断集成到平台中。这种规模要求使用微服务架构,每个功能或服务都与其他服务完全独立,除非通过标准和/或可发现的 API 定义了良好的接口。
Hue 组件
在开始我们的微服务之旅之前,让我们回顾一下我们需要为 Hue 构建的组件类型。
用户档案
用户档案是一个主要组件,包含许多子组件。它代表用户的本质、他们的偏好、他们在各个领域的历史记录,以及 Hue 所知道的所有信息。你能从 Hue 获取的收益在很大程度上取决于档案的丰富程度。但档案管理的信息越多,若数据(或部分数据)被泄露,将带来越大的损害。
管理用户档案的一个重要部分是 Hue 为用户提供的报告和洞察。Hue 将采用先进的机器学习技术,更好地了解用户及其与其他用户和外部服务提供商的互动。
用户图谱
用户图组件建模了跨多个领域用户之间的交互网络。每个用户都参与多个网络:如 Facebook、Instagram 和 Twitter 等社交网络;专业网络;兴趣爱好网络;以及志愿者社区。这些网络中的一些是临时性的,Hue 将能够结构化它们以造福用户。即使不暴露私密信息,Hue 也能利用其对用户连接的丰富资料来改善交互。
身份
如前所述,身份管理至关重要,因此需要一个单独的组件。用户可能希望管理多个相互排斥的个人档案,并拥有不同的身份。例如,用户可能不愿将自己的健康档案与社交档案混合,担心无意间将个人健康信息暴露给朋友。虽然 Hue 可以为你找到有用的连接,但你可能更倾向于在更多隐私和更高能力之间做出权衡。
授权器
授权器是一个关键组件,用户明确授权 Hue 执行某些操作或代表其收集各种数据。这涉及到访问物理设备、外部服务账户和操作权限等方面。
外部服务
Hue 是外部服务的聚合器。它并不是用来替代你的银行、健康服务提供者或社交网络的。它将保留大量关于你活动的元数据,但内容仍将保留在外部服务中。每个外部服务都需要一个专门的组件来与外部服务 API 和政策进行交互。当没有可用 API 时,Hue 会通过自动化浏览器或本地应用程序来模拟用户行为。
通用传感器
Hue 的一个重要价值 proposition 是代表用户执行操作。为了有效执行此操作,Hue 需要了解各种事件。例如,如果 Hue 为你预订了假期,但它检测到有更便宜的航班,它可以自动更改你的航班或要求你确认。有无数的事件需要感知。为了控制感知范围,需要一个通用传感器。通用传感器将是可扩展的,但它暴露出一个通用接口,其他 Hue 组件即使在添加更多传感器时,也可以统一使用该接口。
通用执行器
这是通用传感器的对应组件。Hue 需要代表你执行某些操作;例如,预订航班或预约医生。为了做到这一点,Hue 需要一个通用执行器,该执行器可以扩展以支持特定功能,但能以统一的方式与其他组件(如身份管理器和授权器)进行交互。
用户学习者
这是 Hue 的大脑。它将持续监控你所有授权的互动,并更新其对你和你网络中其他用户的模型。这将使 Hue 随着时间的推移变得越来越有用,能够预测你的需求和兴趣,提供更好的选择,在合适的时间展示更相关的信息,避免令人烦恼和过于强势。
Hue 微服务
每个组件的复杂性都非常庞大。一些组件,比如外部服务、通用传感器和通用执行器,将需要跨越数百、数千甚至更多的外部服务,这些服务会不断变化,超出 Hue 的控制范围。即使是用户学习系统,也需要在多个领域和范畴中学习用户的偏好。微服务通过允许 Hue 渐进式发展并在不因其自身复杂性而崩溃的情况下,增长出更多独立的功能来解决这一需求。每个微服务通过标准接口与通用的 Hue 基础设施服务进行交互,并且可以通过明确定义且版本化的接口与少数其他服务进行交互。每个微服务的表面面积是可管理的,微服务之间的编排遵循标准的最佳实践。
插件
插件是扩展 Hue 的关键,而无需大量界面。关于插件的一个特点是,你通常需要跨越多个抽象层的插件链。例如,如果你想为 Hue 添加 YouTube 集成功能,那么你可以收集大量特定于 YouTube 的信息——你的频道、收藏的视频、推荐内容以及你观看过的视频。为了向用户展示这些信息并允许他们进行操作,你需要跨多个组件的插件,并最终出现在用户界面中。智能设计将通过汇总推荐、选择和延迟通知等行为类别,来帮助许多服务进行聚合。
插件的一个重要优点是它们可以由任何人开发。最初,Hue 开发团队将需要开发这些插件,但随着 Hue 的流行,外部服务将希望与 Hue 集成并构建 Hue 插件以启用他们的服务。当然,这将导致一个完整的插件注册、审批和管理生态系统。
数据存储
Hue 将需要几种类型的数据存储,每种类型还需要多个实例,以管理其数据和元数据:
-
关系型数据库
-
图形数据库
-
时序数据库
-
内存缓存
-
Blob 存储
由于 Hue 的规模,每个数据库都必须进行集群化、可扩展性和分布式处理。此外,Hue 还将使用边缘设备上的本地存储。
无状态微服务
微服务应该大多是无状态的。这将允许特定实例快速启动和终止,并根据需要在基础设施中迁移。状态将由存储管理,微服务通过短期访问令牌访问这些状态。Hue 将在适当的时候将频繁访问的数据存储在易于加速的缓存中。
无服务器函数
每个用户的 Hue 功能的大部分将涉及与外部服务或其他 Hue 服务的相对较短的交互。对于这些活动,可能无需运行一个完整的持久性微服务,也不需要扩展和管理它。更合适的解决方案可能是使用更轻量的无服务器函数。
事件驱动交互
所有这些微服务需要相互通信。用户将请求 Hue 代表他们执行任务。外部服务将通知 Hue 各种事件。队列与无状态微服务的结合提供了完美的解决方案。
每个微服务的多个实例将监听不同的队列,并在相关事件或请求从队列中弹出时作出响应。特定事件也可能触发无服务器函数。这种安排非常健壮且易于扩展。每个组件都可以是冗余的并且具有高可用性。虽然每个组件都有可能发生故障,但系统本身非常容错。
队列也可以用于异步的 RPC 或请求-响应风格的交互,其中调用实例提供一个私有队列名,响应将被发布到该私有队列。
也就是说,有时候通过一个明确界定的接口进行直接的服务对服务交互(或无服务器函数对服务交互)更为合适,并能简化架构。
规划工作流
Hue 经常需要支持工作流。一个典型的工作流会处理一个高层次的任务,比如预约看牙医。它会提取用户的牙医信息和日程安排,匹配用户的日程,选择多个选项中的一个,可能需要与用户确认,完成预约并设置提醒。我们可以将工作流分为完全自动化工作流和涉及人类的人工工作流。还有一些工作流涉及花费金钱,可能需要额外的审批层级。
自动化工作流
自动化工作流无需人工干预。Hue 具有从头到尾执行所有步骤的完全权限。用户分配给 Hue 的自主权越大,Hue 的效果就越好。用户将能够查看并审计所有的工作流,无论是过去的还是当前的。
人工工作流
人类工作流需要与人类互动。最常见的是用户需要从多个选项中做出选择或批准某个操作。但它也可能涉及其他服务中的人。例如,要与牙医预约,Hue 可能需要从秘书那里获取可用时间列表。在未来,Hue 将能够处理与人类的对话,并可能自动化一些这些工作流。
预算感知工作流
一些工作流,如支付账单或购买礼物,涉及花费金钱。虽然理论上可以给 Hue 授权访问用户的银行账户,但大多数用户可能会更愿意为不同的工作流设置预算,或者仅将支出作为一个人类批准的活动。用户可能会授予 Hue 访问一个专用账户或一组账户的权限,并根据提醒和报告,按需为 Hue 分配更多或更少的资金。
到目前为止,我们已经覆盖了很多内容,查看了构成 Hue 平台及其设计的不同组件。现在是时候看看 Kubernetes 如何帮助构建像 Hue 这样的平台了。
使用 Kubernetes 构建 Hue 平台
在本节中,我们将查看各种 Kubernetes 资源以及它们如何帮助我们构建 Hue。首先,我们将更好地了解多功能的 kubectl,然后我们将了解如何在 Kubernetes 中运行长时间运行的进程,如何内部和外部暴露服务,如何使用命名空间限制访问,如何启动临时作业,以及如何混合非集群组件。显然,Hue 是一个庞大的项目,因此我们将在本地集群上演示这些想法,而不实际构建一个真正的 Hue Kubernetes 集群。将其视为一种思维实验。如果你希望探索如何在 Kubernetes 上构建一个真正的微服务分布式系统,可以查看 Hands-On Microservices with Kubernetes:www.packtpub.com/product/hands-on-microservices-with-kubernetes/9781789805468。
高效使用 kubectl
kubectl 是你的瑞士军刀。它几乎可以完成集群中的任何任务。在后台,kubectl 通过 API 连接到你的集群。它读取你的 ~/.kube/config 文件(默认情况下,这可以通过 KUBECONFIG 环境变量或 --kubeconfig 命令行参数来覆盖),该文件包含连接到你的集群或多个集群所需的信息。命令被分为多个类别:
-
通用命令:以通用方式处理资源:
create、get、delete、run、apply、patch、replace等 -
集群管理命令:处理节点和整个集群的任务:
cluster-info、certificate、drain等 -
故障排除命令:
describe、logs、attach、exec等 -
部署命令:处理部署和扩展的任务:
rollout、scale、auto-scale等 -
设置命令: 处理标签和注释:
label、annotate等 -
杂项命令:
help、config和version -
自定义命令: 将 kustomize.io 的功能集成到 kubectl 中
-
配置命令: 处理上下文,切换集群和命名空间,设置当前上下文和命名空间,等等
你可以使用 Kubernetes 的 config view 命令查看配置。
这是我本地 KinD 集群的配置:
$ k config view
apiVersion: v1
clusters:
- cluster:
certificate-authority-data: DATA+OMITTED
server: https://127.0.0.1:50615
name: kind-kind
contexts:
- context:
cluster: kind-kind
user: kind-kind
name: kind-kind
current-context: kind-kind
kind: Config
preferences: {}
users:
- name: kind-kind
user:
client-certificate-data: REDACTED
client-key-data: REDACTED
你的 kubeconfig 文件可能与上面的代码示例相似,也可能不同,只要它指向一个正在运行的 Kubernetes 集群,你就可以跟随学习。让我们深入了解 kubectl 清单文件。
理解 kubectl 清单文件
许多 kubectl 操作,例如 create,需要一个复杂的层级结构(因为 API 要求此结构)。kubectl 使用 YAML 或 JSON 清单文件。YAML 更简洁且易于人类阅读,所以我们大多使用 YAML。下面是一个用于创建 pod 的 YAML 清单文件:
apiVersion: v1
kind: Pod
metadata:
name: ""
labels:
name: ""
namespace: ""
annotations: []
generateName: ""
spec:
...
让我们来看看清单中的各个字段。
apiVersion
非常重要的 Kubernetes API 持续发展,可以通过不同版本的 API 支持同一资源的不同版本。
kind
kind 告诉 Kubernetes 它正在处理的资源类型;在这种情况下是 Pod。这是必需的。
metadata
metadata 包含了描述 pod 及其操作位置的大量信息:
-
name: 在命名空间中唯一标识 pod -
labels: 可以应用多个标签 -
namespace: pod 所属的命名空间 -
annotations: 可查询的注释列表
spec
spec 是一个 pod 模板,包含启动 pod 所需的所有信息。它可能相当复杂,因此我们将分多个部分来探讨:
spec:
containers: [
...
],
"restartPolicy": "",
"volumes": []
容器规格
pod spec 的 containers 部分是一个容器规格列表。每个容器规格具有以下结构:
name: "",
image: "",
command: [""],
args: [""],
env:
- name: "",
value: ""
imagePullPolicy: "",
ports:
- containerPort": 0,
name: "",
protocol: ""
resources:
requests:
cpu: ""
memory: ""
limits:
cpu: ""
memory: ""
每个容器都有一个 image,一个命令,如果指定的话,将替代 Docker 镜像命令。它还可以有参数和环境变量。当然,还有镜像拉取策略、端口和资源限制。我们在前面的章节中已经介绍过这些内容。
如果你想深入探索 pod 资源或其他 Kubernetes 资源,那么以下命令将非常有用:kubectl explain。
它可以探索资源以及特定的子资源和字段。
尝试以下命令:
kubectl explain pod
kubectl explain pod.spec
在 pods 中部署长时间运行的微服务
长时间运行的微服务应该运行在 pods 中,并且是无状态的。让我们看看如何为 Hue 的一个微服务——Hue 学习器——创建 pods,Hue 学习器负责学习用户在不同领域的偏好。稍后,我们将提高抽象级别,使用部署(deployment)。
创建 pods
让我们从一个普通的 Pod 配置文件开始,来创建一个 Hue 学习器内部服务。该服务不需要公开暴露,它将监听一个队列以接收通知,并将其见解存储在持久存储中。
我们需要一个简单的容器来在 Pod 中运行。以下是可能是最简单的 Docker 文件,它将模拟 Hue 学习器:
FROM busybox
CMD ash -c "echo 'Started...'; while true ; do sleep 10 ; done"
它使用 busybox 基础镜像,打印 Started... 到标准输出,然后进入无限循环,这在所有情况下都是长期运行的。
我已经构建了两个 Docker 镜像,标签为 g1g1/hue-learn:0.3 和 g1g1/hue-learn:0.4,并将它们推送到 Docker Hub 注册表(g1g1 是我的用户名):
$ docker build . -t g1g1/hue-learn:0.3
$ docker build . -t g1g1/hue-learn:0.4
$ docker push g1g1/hue-learn:0.3
$ docker push g1g1/hue-learn:0.4
现在这些镜像可以拉取到 Hue 的 Pod 中的容器里。
我们将在这里使用 YAML,因为它更加简洁和易读。以下是模板和元数据标签:
apiVersion: v1
kind: Pod
metadata:
name: hue-learner
labels:
app: hue
service: learner
runtime-environment: production
tier: internal-service
接下来是重要的 containers 规格,它为每个容器定义了必需的名称和镜像:
spec:
containers:
- name: hue-learner
image: g1g1/hue-learn:0.3
resources 部分告诉 Kubernetes 容器的资源需求,从而实现更高效、更紧凑的调度和分配。在这里,容器请求 200 毫 CPU 单位(0.2 核)和 256 MiB(2 的 28 次方字节):
resources:
requests:
cpu: 200m
memory: 256Mi
environment 部分允许集群管理员提供将可用于容器的环境变量。在这里,它告诉容器通过 DNS 查找队列和存储。在测试环境中,可能使用不同的发现方法:
env:
- name: DISCOVER_QUEUE
value: dns
- name: DISCOVER_STORE
value: dns
给 Pod 添加标签
明智地标记 Pod 对灵活操作至关重要。它让你可以实时演进集群,将微服务组织成可以统一操作的组,并且可以快速深入观察不同的子集。
例如,我们的 Hue 学习器 Pod 具有以下标签(以及其他一些标签):
-
runtime-environment : production -
tier : internal-service
runtime-environment 标签允许对属于特定环境的所有 Pod 执行全局操作。tier 标签可以用于查询属于特定层级的所有 Pod。这些仅仅是示例,你的想象力才是限制。
下面是如何使用 get pods 命令列出标签的方法:
$ k get po -n kube-system --show-labels
NAME READY STATUS RESTARTS AGE LABELS
coredns-64897985d-gzrm4 1/1 Running 0 2d2h k8s-app=kube-dns,pod-template-hash=64897985d
coredns-64897985d-m8nm9 1/1 Running 0 2d2h k8s-app=kube-dns,pod-template-hash=64897985d
etcd-kind-control-plane 1/1 Running 0 2d2h component=etcd,tier=control-plane
kindnet-wx7kl 1/1 Running 0 2d2h app=kindnet,controller-revision-hash=9d779cb4d,k8s-app=kindnet,pod-template-generation=1,tier=node
kube-apiserver-kind-control-plane 1/1 Running 0 2d2h component=kube-apiserver,tier=control-plane
kube-controller-manager-kind-control-plane 1/1 Running 0 2d2h component=kube-controller-manager,tier=control-plane
kube-proxy-bgcrq 1/1 Running 0 2d2h controller-revision-hash=664d4bb79f,k8s-app=kube-proxy,pod-template-generation=1
kube-scheduler-kind-control-plane 1/1 Running 0 2d2h component=kube-scheduler,tier=control-plane
现在,如果你想过滤并仅列出 kube-dns Pod,可以输入以下命令:
$ k get po -n kube-system -l k8s-app=kube-dns
NAME READY STATUS RESTARTS AGE
coredns-64897985d-gzrm4 1/1 Running 0 2d2h
coredns-64897985d-m8nm9 1/1 Running 0 2d2h
使用部署部署长期运行的进程
在大规模系统中,Pod 不应只是被创建后任其自由。如果 Pod 因为某种原因意外死亡,你希望有另一个 Pod 来替代它,以维持整体容量。你可以自己创建副本控制器或副本集,但这样会留下错误的隐患,并且有部分失败的可能性。以声明的方式指定你希望启动多少个副本,这样显得更加合理。这正是 Kubernetes 部署的用途。
让我们通过 Kubernetes 部署资源来部署三个实例的 Hue 学习器微服务:
apiVersion: apps/v1
kind: Deployment
metadata:
name: hue-learn
labels:
app: hue
spec:
replicas: 3
selector:
matchLabels:
app: hue
template:
metadata:
labels:
app: hue
spec:
containers:
- name: hue-learner
image: g1g1/hue-learn:0.3
resources:
requests:
cpu: 200m
memory: 256Mi
env:
- name: DISCOVER_QUEUE
value: dns
- name: DISCOVER_STORE
value: dns
Pod 规格与之前的 pod 配置文件中的 spec 部分完全相同。
让我们创建这个部署并检查其状态:
$ k create -f hue-learn-deployment.yaml
deployment.apps/hue-learn created
$ k get deployment hue-learn
NAME READY UP-TO-DATE AVAILABLE AGE
hue-learn 3/3 3 3 25s
$ k get pods -l app=hue
NAME READY STATUS RESTARTS AGE
hue-learn-67d4649b58-qhc88 1/1 Running 0 45s
hue-learn-67d4649b58-qpm2q 1/1 Running 0 45s
hue-learn-67d4649b58-tzzq7 1/1 Running 0 45s
你可以使用 kubectl describe 命令获取有关部署的更多信息:
$ k describe deployment hue-learn
Name: hue-learn
Namespace: default
CreationTimestamp: Tue, 21 Jun 2022 21:11:50 -0700
Labels: app=hue
Annotations: deployment.kubernetes.io/revision: 1
Selector: app=hue
Replicas: 3 desired | 3 updated | 3 total | 3 available | 0 unavailable
StrategyType: RollingUpdate
MinReadySeconds: 0
RollingUpdateStrategy: 25% max unavailable, 25% max surge
Pod Template:
Labels: app=hue
Containers:
hue-learner:
Image: g1g1/hue-learn:0.3
Port: <none>
Host Port: <none>
Requests:
cpu: 200m
memory: 256Mi
Environment:
DISCOVER_QUEUE: dns
DISCOVER_STORE: dns
Mounts: <none>
Volumes: <none>
Conditions:
Type Status Reason
---- ------ ------
Available True MinimumReplicasAvailable
Progressing True NewReplicaSetAvailable
OldReplicaSets: <none>
NewReplicaSet: hue-learn-67d4649b58 (3/3 replicas created)
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal ScalingReplicaSet 106s deployment-controller Scaled up replica set hue-learn-67d4649b58 to 3
更新一个部署
Hue 平台是一个庞大且不断发展的系统。你需要不断地进行升级。部署可以进行更新,以便以一种无痛的方式推出更新。你只需要更改 pod 模板,从而触发一个完全由 Kubernetes 管理的滚动更新。目前,所有 pods 都在运行 0.3 版本:
$ k get pods -o jsonpath='{.items[*].spec.containers[0].image}' -l app=hue | xargs printf "%s\n"
g1g1/hue-learn:0.3
g1g1/hue-learn:0.3
g1g1/hue-learn:0.3
让我们更新部署,升级到 0.4 版本。在部署文件中修改镜像版本,不要修改标签,否则会导致错误。将其保存为 hue-learn-deployment-0.4.yaml。然后我们可以使用 kubectl apply 命令来升级版本,并验证 pods 是否现在运行 0.4 版本:
$ k apply -f hue-learn-deployment-0.4.yaml
Warning: resource deployments/hue-learn is missing the kubectl.kubernetes.io/last-applied-configuration annotation which is required by kubectl apply. kubectl apply should only be used on resources created declaratively by either kubectl create --save-config or kubectl apply. The missing annotation will be patched automatically.
deployment.apps/hue-learn configured
$ k get pods -o jsonpath='{.items[*].spec.containers[0].image}' -l app=hue | xargs printf "%s\n"
g1g1/hue-learn:0.4
g1g1/hue-learn:0.4
g1g1/hue-learn:0.4
请注意,新的 pods 会被创建,而原来的 0.3 版本的 pods 会以滚动更新的方式终止。
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
hue-learn-67d4649b58-fgt7m 1/1 Terminating 0 99s
hue-learn-67d4649b58-klhz5 1/1 Terminating 0 100s
hue-learn-67d4649b58-lgpl9 1/1 Terminating 0 101s
hue-learn-68d74fd4b7-bxxnm 1/1 Running 0 4s
hue-learn-68d74fd4b7-fh55c 1/1 Running 0 3s
hue-learn-68d74fd4b7-rnsj4 1/1 Running 0 2s
我们已经讲解了 kubectl 清单文件的结构,以及如何应用这些文件来部署和更新集群中的工作负载。接下来,我们将看看这些工作负载如何通过内部服务发现并相互调用,以及如何通过外部暴露的服务从集群外部进行访问。
分离内部服务和外部服务
内部服务是只允许集群中其他服务或作业(或登录并运行临时工具的管理员)直接访问的服务。也有一些工作负载完全不被访问。这些工作负载可能会监听某些事件并执行其功能,而不暴露任何 API。
但有些服务需要对用户或外部程序进行暴露。让我们看一个虚拟的 Hue 服务,它管理用户的提醒列表。它实际上做的事情并不多——只返回一个固定的提醒列表——但我们将用它来说明如何暴露服务。我已经将 hue-reminders 镜像推送到 Docker Hub:
docker push g1g1/hue-reminders:3.0
部署一个内部服务
这是该部署文件,它与 hue-learner 部署非常相似,除了我去掉了注释、env 和 resources 部分,保留了一个或两个标签以节省空间,并且添加了一个 ports 部分到容器中。这是至关重要的,因为一个服务必须通过某个端口进行暴露,其他服务才能访问它:
apiVersion: apps/v1
kind: Deployment
metadata:
name: hue-reminders
spec:
replicas: 2
selector:
matchLabels:
app: hue
service: reminders
template:
metadata:
name: hue-reminders
labels:
app: hue
service: reminders
spec:
containers:
- name: hue-reminders
image: g1g1/hue-reminders:3.0
ports:
- containerPort: 8080
当我们运行这个部署时,会有两个 hue-reminders pods 被添加到集群中:
$ k create -f hue-reminders-deployment.yaml
deployment.apps/hue-reminders created
$ k get pods
NAME READY STATUS RESTARTS AGE
hue-learn-68d74fd4b7-bxxnm 1/1 Running 0 12h
hue-learn-68d74fd4b7-fh55c 1/1 Running 0 12h
hue-learn-68d74fd4b7-rnsj4 1/1 Running 0 12h
hue-reminders-9bdcd7489-4jqhc 1/1 Running 0 11s
hue-reminders-9bdcd7489-bxh59 1/1 Running 0 11s
好的,Pods 已经在运行。从理论上讲,其他服务可以查找或通过它们的内部 IP 地址进行配置,并直接访问它们,因为它们都在同一个网络地址空间中。但这样并不具有可扩展性。每次提醒的 pod 终止并被新的 pod 替换,或者当我们只是增加 pod 的数量时,所有访问这些 pods 的服务都必须了解这些变更。Kubernetes 服务通过提供一个稳定的单一访问点来解决这个问题,所有共享一组选择器标签的 pods 都可以通过它来访问。这里是服务定义:
apiVersion: v1
kind: Service
metadata:
name: hue-reminders
labels:
app: hue
service: reminders
spec:
ports:
- port: 8080
targetPort: 80
protocol: TCP
selector:
app: hue
service: reminders
服务有一个 selector,通过匹配标签来确定支持的 Pods。它还暴露一个端口,其他服务将通过该端口访问它。这个端口不需要和容器的端口相同。你可以定义一个 targetPort。
protocol 字段可以是以下之一:TCP、UDP,或者(从 Kubernetes 1.12 开始)SCTP。
创建 hue-reminders 服务
让我们创建服务并进行探索:
$ k create -f hue-reminders-service.yaml
service/hue-reminders created
$ k describe svc hue-reminders
Name: hue-reminders
Namespace: default
Labels: app=hue
service=reminders
Annotations: <none>
Selector: app=hue,service=reminders
Type: ClusterIP
IP Family Policy: SingleStack
IP Families: IPv4
IP: 10.96.152.254
IPs: 10.96.152.254
Port: <unset> 8080/TCP
TargetPort: 8080/TCP
Endpoints: 10.244.0.32:8080,10.244.0.33:8080
Session Affinity: None
Events: <none>
服务已经启动并运行。其他 Pods 可以通过环境变量或 DNS 找到它。所有服务的环境变量在 Pod 创建时设置。这意味着如果你在创建服务时一个 Pod 已经在运行,你必须杀掉它并让 Kubernetes 使用新的服务环境变量重新创建它。
例如,Pod hue-learn-68d74fd4b7-bxxnm 在创建 hue-reminders 服务之前就已经创建,因此它没有 HUE_REMINDERS_SERVICE 的环境变量。打印该 Pod 的环境变量显示该环境变量不存在:
$ k exec hue-learn-68d74fd4b7-bxxnm -- printenv | grep HUE_REMINDERS_SERVICE
让我们杀掉该 Pod,当一个新的 Pod 替代它时,我们再试一次:
$ k delete po hue-learn-68d74fd4b7-bxxnm
pod "hue-learn-68d74fd4b7-bxxnm" deleted
让我们再次检查 hue-learn Pods:
$ k get pods | grep hue-learn
hue-learn-68d74fd4b7-fh55c 1/1 Running 0 13h
hue-learn-68d74fd4b7-rnsj4 1/1 Running 0 13h
hue-learn-68d74fd4b7-rw4qr 1/1 Running 0 2m
很好,我们有一个新的 Pod —— hue-learn-68d74fd4b7-rw4qr。让我们看看它是否有 HUE_REMINDERS_SERVICE 服务的环境变量:
$ k exec hue-learn-68d74fd4b7-rw4qr -- printenv | grep HUE_REMINDERS_SERVICE
HUE_REMINDERS_SERVICE_PORT=8080
HUE_REMINDERS_SERVICE_HOST=10.96.152.254
是的,它有!不过使用 DNS 要简单得多。Kubernetes 会为每个服务分配一个内部 DNS 名称。服务的 DNS 名称是:
<service name>.<namespace>.svc.cluster.local
$ kubectl exec hue-learn-68d74fd4b7-rw4qr -- nslookup hue-reminders.default.svc.cluster.local
Server: 10.96.0.10
Address: 10.96.0.10:53
Name: hue-reminders.default.svc.cluster.local
Address: 10.96.152.254
现在,集群中的每个 Pod 都可以通过其服务端点和端口 8080 访问 hue-reminders 服务:
$ kubectl exec hue-learn-68d74fd4b7-fh55c -- wget -q -O - hue-reminders.default.svc.cluster.local:8080
Dentist appointment at 3pm
Dinner at 7pm
是的,目前 hue-reminders 总是返回相同的两条提醒:
Dentist appointment at 3pm
Dinner at 7pm
这里只是为了演示目的。如果 hue-reminders 是一个真实的系统,它会返回实时和动态的提醒。
现在我们已经了解了内部服务及其访问方式,接下来我们来看一下外部服务。
暴露服务到外部
服务可以在集群内部访问。如果你想将其暴露给外界,Kubernetes 提供了几种方法:
-
配置
NodePort以便直接访问 -
如果你在云环境中运行,配置一个云负载均衡器
-
如果你在裸金属上运行,配置你自己的负载均衡器
在配置外部访问服务之前,你应该确保它是安全的。我们在第四章《Kubernetes 安全性》中已经讲解了这方面的原则。Kubernetes 文档中有一个很好的示例,涵盖了所有细节:github.com/kubernetes/examples/blob/master/staging/https-nginx/README.md。
这是暴露到外部通过 NodePort 的 hue-reminders 服务的 spec 部分:
spec:
type: NodePort
ports:
- port: 8080
targetPort: 8080
protocol: TCP
name: http
- port: 443
protocol: TCP
name: https
selector:
app: hue-reminders
通过 NodePort 暴露服务的主要缺点是端口号在所有服务之间共享。你必须在整个集群中进行全局协调,以避免冲突。对于大规模集群和有大量开发者部署服务的情况,这并不简单。
但你可能有其他原因不希望直接暴露 Kubernetes 服务,例如安全性问题和缺乏抽象,可能更倾向于在服务前使用 Ingress 资源。
Ingress
Ingress 是一个 Kubernetes 配置对象,允许你将服务暴露到外部世界,并处理许多细节。它可以做以下事情:
-
为你的服务提供一个对外可见的 URL
-
负载均衡流量
-
终止 SSL
-
提供基于名称的虚拟主机
要使用 Ingress,你必须在集群中运行一个 Ingress 控制器。Ingress 是在 Kubernetes 1.1 中引入的,并在 Kubernetes 1.19 中稳定。Ingress 控制器的一个当前限制是它并不适合大规模使用。因此,它目前不适合 Hue 平台。我们将在 第十章,探索 Kubernetes 网络 中更详细地讨论 Ingress 控制器。
下面是一个 Ingress 资源的样子:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: minimal-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx-example
rules:
- http:
paths:
- path: /testpath
pathType: Prefix
backend:
service:
name: test
port:
number: 80
注意这个注解,它提示这是一个与 Nginx Ingress 控制器配合使用的 Ingress 对象。还有许多其他 Ingress 控制器,它们通常使用注解来编码它们需要的信息,这些信息是 Ingress 对象及其规则本身无法捕获的。
其他 Ingress 控制器包括:
-
Traefik
-
Gloo
-
Contour
-
AWS ALB Ingress 控制器
-
HAProxy Ingress
-
Voyager
可以创建 IngressClass 资源并在 Ingress 资源中指定它。如果未指定,则使用默认的 IngressClass。
在本节中,我们查看了 Hue 平台的不同组件如何通过服务发现并相互通信,以及如何将面向公众的服务暴露给外部世界。在下一节中,我们将探讨如何在 Kubernetes 上高效且具成本效益地调度 Hue 工作负载。
高级调度
Kubernetes 最强大的特点之一就是其强大而灵活的调度器。简而言之,调度器的工作就是选择节点来运行新创建的 Pod。理论上,调度器甚至可以将现有的 Pod 在节点之间移动,但实际上它目前并不这样做,而是将这一功能交给其他组件。
默认情况下,调度器遵循几个指导原则,包括:
-
将来自同一副本集或有状态集的 Pod 分布到不同节点
-
将 Pod 调度到具有足够资源以满足 Pod 请求的节点上
-
平衡节点的整体资源利用率
这是一个相当不错的默认行为,但有时你可能希望对特定的 Pod 放置进行更好的控制。Kubernetes 1.6 引入了几个高级调度选项,使你可以精细地控制哪些 Pod 在哪些节点上调度,哪些 Pod 不调度,以及哪些 Pod 要一起调度或分开调度。
让我们在 Hue 的背景下回顾这些机制。
首先,让我们创建一个包含两个工作节点的 k3d 集群:
$ k3d cluster create --agents 2
...
INFO[0026] Cluster 'k3s-default' created successfully!
$ k get no
NAME STATUS ROLES AGE VERSION
k3d-k3s-default-agent-0 Ready <none> 22s v1.23.6+k3s1
k3d-k3s-default-agent-1 Ready <none> 22s v1.23.6+k3s1
k3d-k3s-default-server-0 Ready control-plane,master 31s v1.23.6+k3s1
让我们看看 Pod 可以如何调度到节点的各种方式,以及每种方法适用的场景。
节点选择器
节点选择器非常简单。Pod 可以在其规格中指定它希望调度到的节点。例如,trouble-shooter Pod 有一个nodeSelector,它指定了worker-2节点的kubernetes.io/hostname标签:
apiVersion: v1
kind: Pod
metadata:
name: trouble-shooter
labels:
role: trouble-shooter
spec:
nodeSelector:
kubernetes.io/hostname: k3d-k3s-default-agent-1
containers:
- name: trouble-shooter
image: g1g1/py-kube:0.3
command: ["bash"]
args: ["-c", "echo started...; while true ; do sleep 1 ; done"]
创建这个 Pod 时,它确实被调度到了k3d-k3s-default-agent-1节点:
$ k apply -f trouble-shooter.yaml
pod/trouble-shooter created
$ k get po trouble-shooter -o jsonpath='{.spec.nodeName}'
k3d-k3s-default-agent-1
污点与容忍度
你可以对一个节点添加污点,防止 Pods 被调度到该节点。例如,如果你不希望 Pods 被调度到你的控制平面节点,可以使用这种方法。容忍度允许 Pods 声明它们可以“忍受”特定的节点污点,之后这些 Pods 可以调度到被污染的节点。一个节点可以有多个污点,一个 Pod 可以有多个容忍度。污点是一个三元组:键(key)、值(value)、效果(effect)。键和值用于识别污点,效果包括以下几种:
-
NoSchedule(除非 Pods 忍受污点,否则不会调度到该节点) -
PreferNoSchedule(NoSchedule的软版本;调度器将尽量避免调度那些不忍受污点的 Pods) -
NoExecute(不会调度新的 Pod,但也会驱逐那些不忍受污点的现有 Pod)
让我们在我们的 k3d 集群上部署hue-learn和hue-reminders:
$ k apply -f hue-learn-deployment.yaml
deployment.apps/hue-learn created
$ k apply -f hue-reminders-deployment.yaml
deployment.apps/hue-reminders created
当前,在控制平面节点(k3d-k3s-default-server-0)上运行着一个hue-learn Pod:
$ k get po -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
trouble-shooter 1/1 Running 0 2m20s 10.42.2.4 k3d-k3s-default-agent-1 <none> <none>
hue-learn-67d4649b58-tklxf 1/1 Running 0 18s 10.42.1.8 k3d-k3s-default-server-0 <none> <none>
hue-learn-67d4649b58-wk55w 1/1 Running 0 18s 10.42.0.3 k3d-k3s-default-agent-0 <none> <none>
hue-learn-67d4649b58-jkwwg 1/1 Running 0 18s 10.42.2.5 k3d-k3s-default-agent-1 <none> <none>
hue-reminders-9bdcd7489-2j65p 1/1 Running 0 6s 10.42.2.6 k3d-k3s-default-agent-1 <none> <none>
hue-reminders-9bdcd7489-wntpx 1/1 Running 0 6s 10.42.0.4 k3d-k3s-default-agent-0 <none> <none>
让我们给控制平面节点加上污点:
$ k taint nodes k3d-k3s-default-server-0 control-plane=true:NoExecute
node/k3d-k3s-default-server-0 tainted
现在我们可以查看污点(taint):
$ k get nodes k3d-k3s-default-server-0 -o jsonpath='{.spec.taints[0]}'
map[effect:NoExecute key:control-plane value:true]
是的,成功了!现在没有 Pods 被调度到主节点上。k3d-k3s-default-server-0上的hue-learn Pod 被驱逐,并且一个新的 Pod(hue-learn-67d4649b58-bl8cn)现在在k3d-k3s-default-agent-0上运行:
$ k get po -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
trouble-shooter 1/1 Running 0 33m 10.42.2.4 k3d-k3s-default-agent-1 <none> <none>
hue-learn-67d4649b58-wk55w 1/1 Running 0 31m 10.42.0.3 k3d-k3s-default-agent-0 <none> <none>
hue-learn-67d4649b58-jkwwg 1/1 Running 0 31m 10.42.2.5 k3d-k3s-default-agent-1 <none> <none>
hue-reminders-9bdcd7489-2j65p 1/1 Running 0 30m 10.42.2.6 k3d-k3s-default-agent-1 <none> <none>
hue-reminders-9bdcd7489-wntpx 1/1 Running 0 30m 10.42.0.4 k3d-k3s-default-agent-0 <none> <none>
hue-learn-67d4649b58-bl8cn 1/1 Running 0 2m53s 10.42.0.5 k3d-k3s-default-agent-0 <none> <none>
若要允许 Pod 忍受污点,可以在其规格中添加一个容忍度(toleration),例如:
tolerations:
- key: "control-plane"
operator: "Equal"
value: "true"
effect: "NoSchedule"
节点亲和性与反亲和性
节点亲和性(node affinity)是比nodeSelector更复杂的一种形式。它有三个主要优点:
-
丰富的选择标准(
nodeSelector只是标签精确匹配的AND运算) -
规则可以是软性(soft)
-
你可以使用
NotIn和DoesNotExist等运算符实现反亲和性(anti-affinity)
请注意,如果你同时指定了nodeSelector和nodeAffinity,那么该 Pod 只会调度到满足两个要求的节点上。
例如,如果我们将以下部分添加到我们的trouble-shooter Pod,它将无法在任何节点上运行,因为它与nodeSelector发生冲突:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: NotIn
values:
- k3d-k3s-default-agent-1
Pod 亲和性与反亲和性
Pod 亲和性和反亲和性提供了另一种管理工作负载运行位置的方式。到目前为止,我们讨论的所有方法——节点选择器、污点/容忍、节点亲和性/反亲和性——都是关于将 pod 分配给节点的。但 pod 亲和性是关于不同 pod 之间的关系。pod 亲和性还涉及几个其他概念:命名空间(因为 pod 是有命名空间的)、拓扑区域(节点、机架、云提供商区域、云提供商区域)、权重(用于首选调度)。一个简单的例子是,如果你希望hue-reminders总是与trouble-shooter pod 一起调度。让我们看看如何在hue-reminders部署的 pod 模板规格中定义它:
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: role
operator: In
values:
- trouble-shooter
topologyKey: topology.kubernetes.io/zone # for clusters on cloud providers
拓扑键是一个节点标签,Kubernetes 会将其视为在调度时相同。在云提供商中,当工作负载应该彼此靠近时,建议使用topology.kubernetes.io/zone。在云环境中,区域相当于数据中心。
然后,在重新部署hue-reminders之后,所有的hue-reminders pod 都被调度到k3d-k3s-default-agent-1上,紧邻trouble-shooter pod:
$ k apply -f hue-reminders-deployment-with-pod-affinity.yaml
deployment.apps/hue-reminders configured
$ k get po -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
trouble-shooter 1/1 Running 0 117m 10.42.2.4 k3d-k3s-default-agent-1 <none> <none>
hue-learn-67d4649b58-wk55w 1/1 Running 0 115m 10.42.0.3 k3d-k3s-default-agent-0 <none> <none>
hue-learn-67d4649b58-jkwwg 1/1 Running 0 115m 10.42.2.5 k3d-k3s-default-agent-1 <none> <none>
hue-learn-67d4649b58-bl8cn 1/1 Running 0 87m 10.42.0.5 k3d-k3s-default-agent-0 <none> <none>
hue-reminders-544d96785b-pd62t 0/1 Pending 0 50s 10.42.2.4 k3d-k3s-default-agent-1 <none> <none>
hue-reminders-544d96785b-wpmjj 0/1 Pending 0 50s 10.42.2.4 k3d-k3s-default-agent-1 <none> <none>
Pod 拓扑分布约束
节点亲和性/反亲和性和 pod 亲和性/反亲和性有时过于严格。你可能希望分散你的 pod——如果同一部署的某些 pod 最终部署在同一节点上也是可以的。pod 拓扑分布约束为你提供了这种灵活性。你可以指定最大偏差,即你能接受的最优分布偏差,同时也可以指定当约束无法满足时的行为(DoNotSchedule或ScheduleAnyway)。
这是我们的hue-reminders部署,带有 pod 拓扑分布约束:
apiVersion: apps/v1
kind: Deployment
metadata:
name: hue-reminders
spec:
replicas: 3
selector:
matchLabels:
app: hue
service: reminders
template:
metadata:
name: hue-reminders
labels:
app: hue
service: reminders
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: node.kubernetes.io/instance-type
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: hue
service: hue-reminders
containers:
- name: hue-reminders
image: g1g1/hue-reminders:3.0
ports:
- containerPort: 80
我们可以看到,在应用清单之后,三个 pod 被分布在两个代理节点上(服务器节点如你所记得,有一个污点):
$ k apply -f hue-reminders-deployment-with-spread-contraitns.yaml
deployment.apps/hue-reminders created
$ k get po -o wide -l app=hue,service=reminders
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
hue-reminders-6664fccb8f-8bvf6 1/1 Running 0 4m40s 10.42.0.11 k3d-k3s-default-agent-0 <none> <none>
hue-reminders-6664fccb8f-8qrbl 1/1 Running 0 3m59s 10.42.0.12 k3d-k3s-default-agent-0 <none> <none>
hue-reminders-6664fccb8f-b5pbp 1/1 Running 0 56s 10.42.2.14 k3d-k3s-default-agent-1 <none> <none>
反调度器
Kubernetes 在根据复杂的放置规则将 pod 调度到节点方面表现出色。但是,一旦 pod 被调度,Kubernetes 就不会在原始条件发生变化时将其移动到另一个节点。以下是一些可以从工作负载迁移中受益的用例:
-
某些节点出现了资源利用不足或过度利用的情况。
-
当节点上的污点或标签被修改时,初始调度决策不再有效,导致 pod/node 亲和性要求不再满足。
-
某些节点发生了故障,导致它们的 pod 迁移到其他节点。
-
向集群中引入了额外的节点。
这时,反调度器就发挥作用了。反调度器并不是 Kubernetes 的原生功能。你需要安装它并定义策略,决定哪些正在运行的 pod 可能被驱逐。它可以作为Job、CronJob或Deployment运行。反调度器会定期检查当前 pod 的分布情况,并会驱逐违反某些策略的 pod。这些 pod 会被重新调度,标准的 Kubernetes 调度器会根据当前的条件重新安排它们。
在这里查看:github.com/kubernetes-sigs/descheduler。
在这一部分中,我们看到了 Kubernetes 提供的高级调度机制,以及像 descheduler 这样的项目,如何帮助 Hue 在可用的基础设施上以最优方式调度其工作负载。在下一部分中,我们将研究如何将 Hue 的工作负载划分到命名空间中,以便更好地管理对不同资源的访问。
使用命名空间来限制访问
Hue 项目进展顺利,我们有几百个微服务和大约 100 名开发人员以及 DevOps 工程师在参与其中。相关微服务的组逐渐形成,你会发现这些组相当自治,它们完全忽略了其他组。此外,像健康和财务等敏感区域,你希望更有效地控制访问。于是,命名空间应运而生。
让我们创建一个新的服务hue-finance,并将其放入一个名为restricted的新命名空间中。
这是新restricted命名空间的 YAML 文件:
kind: Namespace
apiVersion: v1
metadata:
name: restricted
labels:
name: restricted
我们可以像往常一样创建它:
$ kubectl create -f restricted-namespace.yaml
namespace "restricted" created
一旦命名空间创建完成,我们可以为该命名空间配置一个上下文:
$ k config set-context k3d-k3s-restricted --cluster k3d-k3s-default --namespace=restricted --user restricted@k3d-k3s-default
Context "restricted" created.
$ k config use-context restricted
Switched to context "restricted".
让我们检查一下我们的集群配置:
apiVersion: v1
clusters:
- cluster:
certificate-authority-data: DATA+OMITTED
server: https://0.0.0.0:53829
name: k3d-k3s-default
contexts:
- context:
cluster: k3d-k3s-default
user: admin@k3d-k3s-default
name: k3d-k3s-default
- context:
cluster: ""
namespace: restricted
user: restricted@k3d-k3s-default
name: restricted
current-context: restricted
kind: Config
preferences: {}
users:
- name: admin@k3d-k3s-default
user:
client-certificate-data: REDACTED
client-key-data: REDACTED
正如你所看到的,现在有两个上下文,当前的上下文是restricted。如果需要,我们甚至可以创建专用用户,并为他们分配允许在受限命名空间中操作的凭证。根据环境的不同,这可能很简单也可能很复杂,并且可能涉及通过 Kubernetes 证书颁发机构创建证书。云服务提供商提供与其身份和访问管理(IAM)系统的集成。
为了继续进行,我将使用admin@k3d-k3s-default用户的凭证,并直接在集群的kubeconfig文件中创建一个名为restricted@k3d-k3s-default的用户:
users:
- name: restricted@k3d-k3s-default
user:
client-certificate-data: REDACTED
client-key-data: REDACTED
现在,在这个空的命名空间中,我们可以创建我们的 hue-finance 服务,它将与默认命名空间中的其他服务分开:
$ k create -f hue-finance-deployment.yaml
deployment.apps/hue-finance created
$ k get pods
NAME READY STATUS RESTARTS AGE
hue-finance-84c445f684-vh8qv 1/1 Running 0 7s
hue-finance-84c445f684-fjkxs 1/1 Running 0 7s
hue-finance-84c445f684-sppkq 1/1 Running 0 7s
你不需要切换上下文。你还可以使用--namespace=<namespace>和--all-namespaces命令行选项,但当长时间在同一个非默认命名空间中操作时,将上下文设置为该命名空间会更加方便。
使用 Kustomization 进行分层集群结构
这不是打字错误。Kubectl 最近集成了 Kustomize 的功能(kustomize.io/)。它是一种无需模板即可配置 Kubernetes 的方式。关于 Kustomize 功能如何集成到 kubectl 本身中曾有过很多争议,因为还有其他选择,是否应该让 kubectl 具有这么强的偏好也曾是一个开放的问题。不过,这些都已经过去了。最终结论是,kubectl apply -k 解锁了大量的配置选项。让我们理解它帮助我们解决了什么问题,并利用它来帮助我们管理 Hue。
理解 Kustomize 的基础
Kustomize 是响应像 Helm 这样的模板驱动方法而创建的,用于配置和定制 Kubernetes 集群。它围绕声明式应用管理的原则设计。它接受一个有效的 Kubernetes YAML 清单(基础配置),并通过覆盖额外的 YAML 补丁(覆盖层)来专门化或扩展它。覆盖层依赖于它们的基础配置。所有文件都是有效的 YAML 文件。没有占位符。
kustomization.yaml 文件控制该过程。任何包含 kustomization.yaml 文件的目录都称为根目录。例如:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: staging
commonLabels:
environment: staging
bases:
- ../base
patchesStrategicMerge:
- hue-learn-patch.yaml
resources:
- namespace.yaml
Kustomize 在 GitOps 环境中表现良好,其中不同的 Kustomizations 存储在 Git 仓库中,并且对基础配置、覆盖层或 kustomization.yaml 文件的更改会触发部署。
Kustomize 的最佳使用场景之一是将你的系统组织成多个命名空间,如 staging 和 production。让我们重构 Hue 平台部署清单。
配置目录结构
首先,我们需要一个基础目录,其中包含所有清单的共性。然后我们将拥有一个 overlays 目录,该目录包含 staging 和 production 子目录:
$ tree
.
├── base
│ ├── hue-learn.yaml
│ └── kustomization.yaml
├── overlays
│ ├── production
│ │ ├── kustomization.yaml
│ │ └── namespace.yaml
│ └── staging
│ ├── hue-learn-patch.yaml
│ ├── kustomization.yaml
│ └── namespace.yaml
基础目录中的 hue-learn.yaml 文件只是一个示例。那里可能有许多文件。让我们快速查看它:
apiVersion: v1
kind: Pod
metadata:
name: hue-learner
labels:
tier: internal-service
spec:
containers:
- name: hue-learner
image: g1g1/hue-learn:0.3
resources:
requests:
cpu: 200m
memory: 256Mi
env:
- name: DISCOVER_QUEUE
value: dns
- name: DISCOVER_STORE
value: dns
它与我们之前创建的清单非常相似,但没有 app: hue 标签。这个标签并不是必需的,因为标签是由 kustomization.yaml 文件提供的,作为所有列出资源的通用标签:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
commonLabels:
app: hue
resources:
- hue-learn.yaml
应用 Kustomizations
我们可以通过在基础目录上运行 kubectl kustomize 命令来查看结果。你可以看到,通用标签 app: hue 被添加了:
$ k kustomize base
apiVersion: v1
kind: Pod
metadata:
labels:
app: hue
tier: internal-service
name: hue-learner
spec:
containers:
- env:
- name: DISCOVER_QUEUE
value: dns
- name: DISCOVER_STORE
value: dns
image: g1g1/hue-learn:0.3
name: hue-learner
resources:
requests:
cpu: 200m
memory: 256Mi
为了实际部署 Kustomization,我们可以运行 kubectl -k apply。但是,基础配置不应该单独部署。让我们深入到 overlays/staging 目录并查看它。
namespace.yaml 文件仅创建 staging 命名空间。正如我们即将看到的,它将受益于所有的 Kustomizations:
apiVersion: v1
kind: Namespace
metadata:
name: staging
kustomization.yaml 文件添加了通用标签 environment: staging。它依赖于基础目录,并将 namespace.yaml 文件添加到资源列表中(该列表已包括基础目录中的 hue-learn.yaml):
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: staging
commonLabels:
environment: staging
bases:
- ../../base
patchesStrategicMerge:
- hue-learn-patch.yaml
resources:
- namespace.yaml
但这还不是全部。Kustomizations 最有趣的部分是补丁。
补丁
补丁添加或替换清单的部分内容。它们从不删除现有的资源或资源的部分内容。hue-learn-patch.yaml 将镜像从 g1g1/hue-learn:0.3 更新为 g1g1/hue-learn:0.4:
apiVersion: v1
kind: Pod
metadata:
name: hue-learner
spec:
containers:
- name: hue-learner
image: g1g1/hue-learn:0.4
这是一个战略合并。Kustomize 支持另一种补丁类型,称为 JsonPatches6902。它基于 RFC 6902 (tools.ietf.org/html/rfc6902)。通常,它比战略合并更简洁。我们可以使用 YAML 语法为 JSON 6902 补丁编写。这里是使用 JsonPatches6902 语法将镜像版本更改为 0.4 的相同补丁:
- op: replace
path: /spec/containers/0/image
value: g1g1/hue-learn:0.4
对整个 staging 命名空间进行 Kustomize 操作
这是在overlays/staging目录上运行 Kustomize 时生成的内容:
$ k kustomize overlays/staging
apiVersion: v1
kind: Namespace
metadata:
labels:
environment: staging
name: staging
---
apiVersion: v1
kind: Pod
metadata:
labels:
app: hue
environment: staging
tier: internal-service
name: hue-learner
namespace: staging
spec:
containers:
- env:
- name: DISCOVER_QUEUE
value: dns
- name: DISCOVER_STORE
value: dns
image: g1g1/hue-learn:0.4
name: hue-learner
resources:
requests:
cpu: 200m
memory: 256Mi
请注意,命名空间没有继承基础中的app: hue标签,而只继承了其本地 Kustomization 文件中的environment: staging标签。另一方面,hue-learn pod 则得到了所有标签以及命名空间指定。
现在是时候将其部署到集群中了:
$ k apply -k overlays/staging
namespace/staging created
pod/hue-learner created
现在,我们可以查看在新创建的staging命名空间中的 pod:
$ k get po -n staging
NAME READY STATUS RESTARTS AGE
hue-learner 1/1 Running 0 21s
让我们检查 overlay 是否生效,并且镜像版本确实是 0.4:
$ k get po hue-learner -n staging -o jsonpath='{.spec.containers[0].image}'
g1g1/hue-learn:0.4
在本节中,我们介绍了 Kustomize 选项所提供的强大结构化和可重用性。这对像 Hue 平台这样的大规模系统非常重要,因为许多工作负载可以从统一的结构和一致的基础中受益。在下一节中,我们将介绍如何启动短期作业。
启动作业
Hue 已经发展起来,并且部署了许多长时间运行的微服务进程,但它也有许多任务会运行、完成某些目标然后退出。Kubernetes 通过Job资源支持这种功能。Kubernetes 作业管理一个或多个 pod,并确保它们在成功或失败之前一直运行。如果作业管理的 pod 之一失败或被删除,那么作业会启动一个新的 pod,直到成功为止。
也有许多适用于 Kubernetes 的无服务器或函数即服务解决方案,但它们是建立在原生 Kubernetes 之上的。我们将在第十二章中深入讨论无服务器计算,Kubernetes 上的无服务器计算。
这是一个运行 Python 进程计算 5 的阶乘(提示:结果是 120)的作业:
apiVersion: batch/v1
kind: Job
metadata:
name: factorial5
spec:
template:
metadata:
name: factorial5
spec:
containers:
- name: factorial5
image: g1g1/py-kube:0.3
command: ["python",
"-c",
"import math; print(math.factorial(5))"]
restartPolicy: Never
请注意,restartPolicy必须是Never或OnFailure。默认值Always无效,因为作业在成功完成后不会重启。
让我们启动作业并检查其状态:
$ k create -f factorial-job.yaml
job.batch/factorial5 created
$ k get jobs
NAME COMPLETIONS DURATION AGE
factorial5 1/1 4s 27s
完成任务的 pod 显示为Completed状态。请注意,作业 pod 有一个名为job-name的标签,标签值为作业的名称,因此很容易过滤出作业 pod:
$ k get po -l job-name=factorial5
NAME READY STATUS RESTARTS AGE
factorial5-dddzz 0/1 Completed 0 114s
让我们查看日志中的输出:
$ k logs factorial5-dddzz
120
按顺序启动作业对于某些用例是可以的,但通常运行并行作业会更有用。此外,完成后清理作业并定期运行作业也很重要。让我们看看这是怎么做到的。
并行运行作业
你也可以运行带有并行度的作业。在 spec 中有两个字段,分别是completions和parallelism。默认情况下,completions 设置为 1。如果你想要多个成功的完成次数,可以增加这个值。并行度决定了要启动多少个 pod。作业不会启动超过成功完成所需的 pod,即使并行度数大于该值。
让我们运行另一个作业,它只会休眠 20 秒,直到完成三次成功。我们将使用并行因子为六,但只会启动三个 pod:
apiVersion: batch/v1
kind: Job
metadata:
name: sleep20
spec:
completions: 3
parallelism: 6
template:
metadata:
name: sleep20
spec:
containers:
- name: sleep20
image: g1g1/py-kube:0.3
command: ["python",
"-c",
"import time; print('started...');
time.sleep(20); print('done.')"]
restartPolicy: Never
让我们运行作业并等待所有 pod 完成:
$ k create -f parallel-job.yaml
job.batch/sleep20 created
现在我们可以看到,所有三个 pods 都已完成,并且这些 pods 现在没有准备好,因为它们已经完成了工作:
$ k get pods -l job-name=sleep20
NAME READY STATUS RESTARTS AGE
sleep20-fqgst 0/1 Completed 0 4m5s
sleep20-2dv8h 0/1 Completed 0 4m5s
sleep20-kvn28 0/1 Completed 0 4m5s
已完成的 pods 不会占用节点上的资源,因此其他 pods 可以在该节点上被调度。
清理已完成的作业
当一个作业完成时,它会保留 – 其 pods 也会保留。这是设计使然,目的是让你查看日志或连接到 pods 进行探查。但通常情况下,当一个作业成功完成后,它就不再需要了。清理已完成的作业及其 pods 是你的责任。
最简单的方法是直接删除 job 对象,这样也会删除所有的 pods:
$ kubectl get jobs
NAME COMPLETIONS DURATION AGE
factorial5 1/1 2s 6h59m
sleep20 3/3 3m7s 5h54m
$ kubectl delete job factorial5
job.batch "factorial5" deleted
$ kubectl delete job sleep20
job.batch "sleep20" deleted
调度 cron 作业
Kubernetes cron 作业是按指定时间运行的作业,可以是一次性运行或重复运行。它们的行为与在 /etc/crontab 文件中指定的普通 Unix cron 作业一样。
CronJob 资源在 Kubernetes 1.21 版本中变得稳定。这里是每分钟启动一个 cron 作业的配置,用来提醒你做伸展运动。在调度中,你可以将 ‘*' 替换为 ‘?':
apiVersion: batch/v1
kind: CronJob
metadata:
name: cron-demo
spec:
schedule: "*/1 * * * *"
jobTemplate:
spec:
template:
metadata:
labels:
cronjob-name: cron-demo
spec:
containers:
- name: cron-demo
image: g1g1/py-kube:0.3
args:
- python
- -c
- from datetime import datetime; print(f'[{datetime.now()}] CronJob demo here...remember to stretch')
restartPolicy: OnFailure
在 pod 的 spec 中,在作业模板下,我添加了标签 cronjob-name: cron-demo。原因是,cron 作业及其 pods 会由 Kubernetes 分配一个随机前缀的名称。这个标签允许你轻松发现某个特定 cron 作业的所有 pods。Pods 还会有 job-name 标签,因为一个 cron 作业会为每次执行创建一个 job 对象。然而,作业名称本身带有随机前缀,因此它不能帮助我们发现 pods。
让我们运行 cron 作业并在一分钟后观察结果:
$ k get cj
NAME SCHEDULE SUSPEND ACTIVE LAST SCHEDULE AGE
cron-demo */1 * * * * False 0 <none> 16s
$ k get job
NAME COMPLETIONS DURATION AGE
cron-demo-27600079 1/1 3s 2m45s
cron-demo-27600080 1/1 3s 105s
cron-demo-27600081 1/1 3s 45s
$ k get pods
NAME READY STATUS RESTARTS AGE
cron-demo-27600080-dmcmq 0/1 Completed 0 2m6s
cron-demo-27600081-gjsvd 0/1 Completed 0 66s
cron-demo-27600082-sgjlh 0/1 Completed 0 6s
如你所见,每分钟 cron 作业都会创建一个新的作业,并且每个作业的 pod 都会被标记上不同的名称。每个作业的 pod 都被标记上其作业名称,并且还会标记上 cron 作业名称 —— cronjob-demo —— 以便于汇总所有来自该 cron 作业的 pods。
像往常一样,你可以使用 logs 命令查看已完成作业的 pod 输出:
$ k logs cron-demo-27600082-sgjlh
[2022-06-23 17:22:00.971343] CronJob demo here...remember to stretch
当你删除一个 cron 作业时,它会停止调度新的作业,并删除所有现有的 job 对象以及它创建的所有 pods。
你可以使用指定的标签(在这个例子中是 name=cron-demo)来定位 cron 作业启动的所有 job 对象:
$ k delete job -l name=cron-demo
job.batch "cron-demo-27600083" deleted
job.batch "cron-demo-27600084" deleted
job.batch "cron-demo-27600085" deleted
你也可以暂停一个 cron 作业,这样它就不会创建更多作业,但不会删除已完成的作业和 pods。你还可以通过在 spec 中设置历史限制来管理作业的保留数量:.spec.successfulJobsHistoryLimit 和 .spec.failedJobsHistoryLimit。
在本节中,我们介绍了启动作业和控制作业的关键主题。这是 Hue 平台的一个关键方面,它需要响应实时事件并通过启动作业以及定期执行短期任务来处理它们。
混合非集群组件
Kubernetes 集群中的大多数实时系统组件将与集群外的组件通信。那些组件可以是通过某些 API 访问的完全外部的第三方服务,也可以是运行在同一局域网中的内部服务,由于各种原因,它们不属于 Kubernetes 集群的一部分。
这里有两类:集群内网络和集群外网络。
集群外网络组件
这些组件无法直接访问集群。它们只能通过 API、外部可见的 URL 和暴露的服务访问集群。这些组件被视为任何外部用户。通常,集群组件会使用外部服务,这不会造成安全问题。例如,在以前的公司里,我们有一个 Kubernetes 集群,它将异常报告给名为 Sentry 的第三方服务(sentry.io/welcome/)。这是 Kubernetes 集群到第三方服务的单向通信。Kubernetes 集群拥有访问 Sentry 的凭据,这就是这单向通信的全部。
集群内网络组件
这些是运行在网络内部但不由 Kubernetes 管理的组件。运行这些组件有多种原因。它们可能是尚未“容器化”的遗留应用程序,或者是一些不易在 Kubernetes 内部运行的分布式数据存储。将这些组件运行在网络内部的原因是为了性能,并且与外界隔离,以便这些组件和 Pod 之间的流量更加安全。作为同一网络的一部分可以确保低延迟,并且减少需要为通信打开网络的需求,这不仅方便,而且可以提高安全性。
使用 Kubernetes 管理 Hue 平台
在本节中,我们将探讨 Kubernetes 如何帮助运营像 Hue 这样的巨大平台。Kubernetes 本身提供了很多能力来编排 Pod 并管理配额和限制,检测和恢复某些类型的通用故障(硬件故障、进程崩溃和无法访问的服务)。但是,在像 Hue 这样的复杂系统中,Pod 和服务可能已经启动并运行,但处于无效状态,或者正在等待其他依赖项才能执行其职能。这很棘手,因为如果一个服务或 Pod 尚未准备好,但已经在接收请求,那么你需要以某种方式进行管理:失败(责任转移给调用者)、重试(重试多少次?多长时间?多频繁?),以及稍后排队(谁来管理这个队列?)。
如果整个系统能够了解不同组件的就绪状态,或者仅当组件真正就绪时才对外可见,这通常会更好。Kubernetes 不了解 Hue,但它提供了几种机制,如活性探针、就绪探针、启动探针和初始化容器,以支持针对应用程序的集群管理。
使用活性探针确保容器存活
kubelet 负责监控你的容器。如果容器进程崩溃,kubelet 会根据重启策略处理它。但在许多情况下,这还不够。你的进程可能不会崩溃,而是陷入死循环或死锁中。重启策略可能不够精细。通过存活探针,你可以决定容器何时被视为存活的。如果存活探针失败,Kubernetes 会重启你的容器。这里是一个 Hue 音乐服务的 pod 模板。它有一个 livenessProbe 部分,使用 httpGet 探针。HTTP 探针需要一个方案(http 或 https,默认是 http)、一个主机(默认是 PodIp)、一个路径和一个端口。如果 HTTP 状态码在 200 到 399 之间,则认为探针成功。你的容器可能需要一些时间来初始化,因此你可以指定 initialDelayInSeconds。在此期间,kubelet 不会进行存活检查:
apiVersion: v1
kind: Pod
metadata:
labels:
app: music
service: music
name: hue-music
spec:
containers:
- image: g1g1/hue-music
livenessProbe:
httpGet:
path: /pulse
port: 8888
httpHeaders:
- name: X-Custom-Header
value: ItsAlive
initialDelaySeconds: 30
timeoutSeconds: 1
name: hue-music
如果任何容器的存活探针失败,那么 pod 的重启策略将生效。确保你的重启策略不是 Never,因为那样会使探针失效。
还有三种其他类型的存活探针:
-
TcpSocket– 只是检查端口是否打开 -
Exec– 运行一个返回 0 表示成功的命令 -
gRPC– 遵循 gRPC 健康检查协议 (github.com/grpc/grpc/blob/master/doc/health-checking.md)
使用就绪探针来管理依赖关系
就绪探针用于不同的目的。你的容器可能已经启动并运行,并通过了存活探针,但它可能依赖于当前不可用的其他服务。例如,hue-music 可能依赖于访问包含你听歌历史的数据服务。如果没有访问权限,它将无法履行职责。在这种情况下,其他服务或外部客户端不应向 hue-music 服务发送请求,但无需重启它。就绪探针解决了这一用例。当容器的就绪探针失败时,该容器的 pod 会从它注册的任何服务端点中移除。这确保了不会向无法处理请求的服务发送请求。请注意,你还可以使用就绪探针暂时移除过度预订的 pods,直到它们清空一些内部队列。
这里是一个示例就绪探针。我在这里使用 exec 探针来执行一个自定义命令。如果命令退出时返回非零的退出码,容器将被销毁:
readinessProbe:
exec:
command:
- /usr/local/bin/checker
- --full-check
- --data-service=hue-multimedia-service
initialDelaySeconds: 60
timeoutSeconds: 5
在同一个容器上同时使用就绪探针和存活探针是可以的,因为它们有不同的目的。
使用启动探针
一些应用程序(主要是遗留应用程序)可能具有较长的初始化周期。在这种情况下,活跃性探针可能会失败,并导致容器在完成初始化之前重新启动。这就是启动探针发挥作用的地方。如果配置了启动探针,则会跳过活跃性和就绪性检查,直到启动完成。此时,启动探针不再被调用,正常的活跃性和就绪性探针接管。
例如,在以下配置片段中,启动探针每 10 秒检查 5 分钟,以确定容器是否已启动(使用与活跃探针相同的检查)。如果启动探针失败 30 次(300 秒 = 5 分钟),则容器将被重新启动,并获得额外的 5 分钟来尝试初始化自身。但是,如果在 5 分钟内通过了启动探针的检查,则活跃探针生效,任何活跃检查失败将导致重新启动:
ports:
- name: liveness-port
containerPort: 8080
hostPort: 8080
livenessProbe:
httpGet:
path: /healthz
port: liveness-port
failureThreshold: 1
periodSeconds: 10
startupProbe:
httpGet:
path: /healthz
port: liveness-port
failureThreshold: 30
periodSeconds: 10
使用初始化容器有序地启动 Pod
活跃性、就绪性和启动探针非常重要。它们认识到,在启动过程中,容器可能尚未就绪,但不应视为失败。为了适应这一点,有一个initialDelayInSeconds设置,容器在此期间不会被视为失败。但是,如果这个初始延迟可能非常长怎么办?也许在大多数情况下,容器几秒钟后即可准备好并且可以处理请求,但由于初始延迟设置为 5 分钟以防万一,我们在容器空闲时浪费了大量时间。如果容器是高流量服务的一部分,那么每次升级后许多实例都可能在空闲 5 分钟后坐等,几乎使服务不可用。
初始化容器解决了这个问题。一个 Pod 可以有一组初始化容器在其他容器启动之前运行完成。初始化容器可以处理所有非确定性初始化,并让带有其就绪探针的应用容器具有最小的延迟。
初始化容器特别适用于 Pod 级别的初始化任务,例如等待卷准备就绪。初始化容器和启动探针之间有些重叠,选择取决于具体的用例。
初始化容器在 Kubernetes 1.6 中退出了 beta 版。您可以在 Pod 规范中指定它们作为initContainers字段,这与containers字段非常相似。以下是一个示例:
kind: Pod
metadata:
name: hue-fitness
spec:
containers:
- name: hue-fitness
image: busybox
initContainers:
- name: install
image: busybox
Pod 的就绪性和就绪性门
Pod 就绪性是在 Kubernetes 1.11 中引入的,并在 Kubernetes 1.14 中变得稳定。尽管就绪探针允许你在容器级别判断其是否准备好处理请求,但支持将流量传递到 pod 的整体基础设施可能尚未准备好。例如,服务、网络策略和负载均衡器可能需要一些额外的时间。这可能会成为一个问题,尤其是在滚动部署期间,Kubernetes 可能在新 pod 真的准备好之前就终止旧 pod,这将导致服务容量下降,甚至在极端情况下引发服务中断(所有旧 pod 被终止且没有新的 pod 完全准备好)。
这就是 pod 就绪性门控所解决的问题。其思路是将 pod 就绪性的概念扩展到检查额外的条件,而不仅仅是确保所有容器都已就绪。这是通过在 PodSpec 中添加一个新的字段 readinessGates 来实现的。你可以指定一组必须满足的条件,才能将 pod 视为已准备好。在以下示例中,由于 www.example.com/feature-1 条件的 status: False,该 pod 还未就绪:
Kind: Pod
...
spec:
readinessGates:
- conditionType: "www.example.com/feature-1"
status:
conditions:
- type: Ready # this is a builtin PodCondition
status: "False"
lastProbeTime: null
lastTransitionTime: 2023-01-01T00:00:00Z
- type: "www.example.com/feature-1" # an extra PodCondition
status: "False"
lastProbeTime: null
lastTransitionTime: 2023-01-01T00:00:00Z
containerStatuses:
- containerID: docker://abcd...
ready: true
...
与 DaemonSet pod 共享
DaemonSet pod 是自动部署的 pod,每个节点(或指定的节点子集)上部署一个。它们通常用于监视节点并确保它们处于正常工作状态。这是一个非常重要的功能,我们将在第十三章,Kubernetes 集群监控中详细讨论。但它们也可以用于更多其他功能。默认的 Kubernetes 调度器的特点是,它根据资源可用性和请求来调度 pod。如果你有很多不需要大量资源的 pod,那么这些 pod 很可能会被调度到同一个节点上。
假设有一个 pod 执行一个小任务,然后每秒将所有活动的总结发送到一个远程服务。现在,假设平均来说,50 个这样的 pod 会被调度到同一个节点上。这意味着每秒钟,50 个 pod 会发起 50 次网络请求,每次请求的数据量非常小。如何将其减少 50 倍,仅发送一个网络请求呢?通过使用 DaemonSet pod,其他 50 个 pod 可以与它通信,而不是直接与远程服务进行通信。DaemonSet pod 将从这 50 个 pod 收集所有数据,并每秒将其汇总报告到远程服务。当然,这要求远程服务的 API 支持汇总报告。好的一点是,这些 pod 本身无需修改;它们只需配置为与 DaemonSet pod 在 localhost 上通信,而不是直接与远程服务通信。DaemonSet pod 充当了一个聚合代理。它还可以实现重试和其他类似功能。
这个配置文件的有趣之处在于,hostNetwork、hostPID 和 hostIPC 选项被设置为 true。这使得 pods 可以高效地与代理进行通信,利用它们运行在同一个物理主机上的事实:
apiVersion: apps/v1
kind-fission | fission
kind: DaemonSet
metadata:
name: hue-collect-proxy kind-fission | fission
labels:
tier: stats
app: hue-collect-proxy
spec:
selector:
matchLabels:
tier: stats
app: hue-collect-proxy
template:
metadata:
labels:
tier: stats
app: hue-collect-proxy
spec:
hostPID: true
hostIPC: true
hostNetwork: true
containers:
- name: hue-collect-proxy
image: busybox
在本节中,我们探讨了如何在 Kubernetes 上管理 Hue 平台,并确保 Hue 组件在准备好时能够可靠地部署和访问,利用诸如初始化容器、就绪门和 DaemonSets 等功能。在接下来的章节中,我们将讨论 Hue 平台未来可能的发展方向。
用 Kubernetes 发展 Hue 平台
在本节中,我们将讨论扩展 Hue 平台并服务于其他市场和社区的其他方式。问题总是,哪些 Kubernetes 功能和能力可以帮助我们应对新的挑战或需求?
这是一个假设性章节,旨在从宏观角度思考,使用 Hue 作为一个复杂系统的例子。
在企业中使用 Hue
企业通常无法在云端运行,可能是出于安全性和合规性原因,或是由于性能原因,因为系统必须与不适合迁移到云端的数据和遗留系统进行协作。无论如何,企业版的 Hue 必须支持本地集群和/或裸金属集群。
虽然 Kubernetes 最常在云端部署,并且拥有专门的云服务提供商接口,但它并不依赖于云环境,可以在任何地方部署。它确实需要更多的专业知识,但那些已经在自己数据中心运行系统的企业组织,可能拥有这样的专业知识或能培养出来。
用 Hue 推动科学进步
Hue 在集成来自多个来源的信息方面非常出色,对科学界来说将是一个巨大的福音。试想一下,Hue 如何帮助不同学科的科学家之间进行多学科的协作。
一个科学社区的网络可能需要在多个地理位置分布的集群之间进行部署。这时,便引入了多集群 Kubernetes。Kubernetes 考虑到了这一使用场景并不断发展其支持。我们将在第十一章,《在多个集群上运行 Kubernetes》中详细讨论这一话题。
用 Hue 教育未来的孩子
Hue 可以用于教育,并为在线教育系统提供多种服务。然而,隐私问题可能会阻止将 Hue 部署为一个单一的集中式系统来服务于孩子们。一个可能的方案是设置一个集群,并为不同的学校配置命名空间。另一个部署选项是每个学校或县拥有自己的 Hue Kubernetes 集群。在第二种情况下,Hue 在教育领域的应用必须非常易于操作,以适应那些没有太多技术专长的学校。Kubernetes 可以通过提供自愈和自动扩展的功能和能力,帮助 Hue 尽可能接近零管理。
总结
在本章中,我们设计并规划了 Hue 平台的开发、部署和管理——这是一个基于微服务架构构建的虚拟全知全能系统。我们使用 Kubernetes 作为底层编排平台,当然,并深入探讨了它的许多概念和资源。特别地,我们专注于为长期运行的服务部署 pods,而不是为短期任务或定时任务部署 jobs,探索了内部服务与外部服务的区别,还使用了命名空间来划分 Kubernetes 集群。我们研究了 Kubernetes 的各种工作负载调度机制。接着,我们探讨了如何通过存活探针、就绪探针、启动探针、初始化容器和守护进程集来管理像 Hue 这样的庞大系统。
现在你应该能够熟练地设计由微服务组成的 Web 规模系统,并理解如何在 Kubernetes 集群中部署和管理它们。
在下一章,我们将探讨一个至关重要的领域——存储。数据是王者,但它通常是系统中最不灵活的部分。Kubernetes 提供了一个存储模型,并提供了多种与各种存储解决方案集成的选项。
第六章:存储管理
在本章中,我们将探讨 Kubernetes 如何管理存储。存储与计算有很大的不同,但从高层次来看,它们都是资源。作为一个通用平台,Kubernetes 采取了将存储抽象为编程模型和一组存储提供商插件的方式。首先,我们将详细了解概念存储模型,以及如何将存储提供给集群中的容器。接着,我们将介绍一些常见的云平台存储提供商,如亚马逊网络服务(AWS)、谷歌计算引擎(GCE)和 Azure。然后,我们将介绍一个著名的开源存储提供商,Red Hat 的 GlusterFS,它提供了一个分布式文件系统。我们还将探讨另一个解决方案——Ceph,它通过 Rook 操作符将数据管理到容器中,成为 Kubernetes 集群的一部分。我们还将看到 Kubernetes 如何支持现有企业存储解决方案的集成。最后,我们将深入探讨容器存储接口(CSI)及其所带来的所有高级功能。
本章将涵盖以下主要内容:
-
持久卷演示
-
演示持久卷存储的端到端实现
-
公有云存储卷类型 – GCE、AWS 和 Azure
-
Kubernetes 中的 GlusterFS 和 Ceph 卷
-
将企业存储集成到 Kubernetes 中
-
容器存储接口
在本章结束时,你将对 Kubernetes 中存储的表示方式、每种部署环境中的各种存储选项(本地测试、公有云和企业存储)有一个清晰的理解,并且能够根据你的使用场景选择最佳的存储选项。
你应该在 minikube 或其他支持存储的集群上尝试本章中的代码示例。KinD 集群在节点标签化方面存在一些问题,而节点标签化是某些存储解决方案所必需的。
持久卷演示
在本节中,我们将了解 Kubernetes 存储的概念模型,并了解如何将持久存储映射到容器中,以便它们能够读取和写入数据。让我们先从理解存储问题开始。
容器和 Pod 是短暂的。容器写入其文件系统的任何内容都会在容器停止时被清除。容器还可以挂载主机节点的目录,并读取或写入它们。这些数据在容器重启时会继续存在,但节点本身并不是永生的。此外,如果 Pod 本身被驱逐并调度到不同的节点,Pod 中的容器将无法访问旧节点主机的文件系统。
还有其他问题,例如容器死亡时挂载的主机目录的所有权问题。试想一下,一堆容器将重要数据写入它们主机上的各种数据目录,然后消失,留下所有数据分布在节点上,无法直接知道哪个容器写了哪个数据。你可以尝试记录这些信息,但你会把它记录在哪里呢?很明显,对于一个大规模系统,你需要一个从任何节点都能访问的持久存储来可靠地管理数据。
理解卷
基本的 Kubernetes 存储抽象是卷。容器挂载绑定到其 Pod 的卷,并像访问本地文件系统一样访问存储,无论它在哪里。这并不新鲜,而且非常好,因为作为一个开发者,在编写需要访问数据的应用程序时,你不必担心数据存储在哪里以及如何存储。Kubernetes 支持许多类型的卷,它们具有各自独特的特点。让我们回顾一些主要的卷类型。
使用 emptyDir 进行 Pod 间通信
在同一 Pod 内使用共享卷在容器之间共享数据非常简单。容器 1 和容器 2 只需挂载相同的卷,并通过读取和写入该共享空间进行通信。最基本的卷是emptyDir。emptyDir卷是主机上的一个空目录。请注意,它是非持久性的,因为当 Pod 被驱逐或删除时,内容会被清除。如果容器崩溃,Pod 会保留下来,重启的容器可以访问卷中的数据。另一个非常有趣的选项是使用 RAM 磁盘,通过将介质指定为Memory。现在,您的容器通过共享内存进行通信,这样的速度更快,但当然更容易丢失。如果节点被重启,emptyDir的卷内容会丢失。
这是一个 Pod 配置文件,包含两个挂载相同卷的容器,名为shared-volume。容器在不同的路径上挂载它,但当hue-global-listener容器将文件写入/notifications时,hue-job-scheduler会在/incoming下看到该文件:
apiVersion: v1
kind: Pod
metadata:
name: hue-scheduler
spec:
containers:
- image: g1g1/hue-global-listener:1.0
name: hue-global-listener
volumeMounts:
- mountPath: /notifications
name: shared-volume
- image: g1g1/hue-job-scheduler:1.0
name: hue-job-scheduler
volumeMounts:
- mountPath: /incoming
name: shared-volume
volumes:
- name: shared-volume
emptyDir: {}
要使用共享内存选项,我们只需在emptyDir部分添加medium: Memory:
volumes:
- name: shared-volume
emptyDir:
medium: Memory
请注意,基于内存的emptyDir会计入容器的内存限制。
为了验证是否有效,让我们创建 Pod,然后使用一个容器写入文件,并使用另一个容器读取它:
$ k create -f hue-scheduler.yaml
pod/hue-scheduler created
请注意,Pod 有两个容器:
$ k get pod hue-scheduler -o json | jq .spec.containers
[
{
"image": "g1g1/hue-global-listener:1.0",
"name": "hue-global-listener",
"volumeMounts": [
{
"mountPath": "/notifications",
"name": "shared-volume"
},
...
]
...
},
{
"image": "g1g1/hue-job-scheduler:1.0",
"name": "hue-job-scheduler",
"volumeMounts": [
{
"mountPath": "/incoming",
"name": "shared-volume"
},
...
]
...
}
]
现在,我们可以在hue-global-listener容器的/notifications目录中创建文件,并在hue-job-scheduler容器的/incoming目录中列出它:
$ kubectl exec -it hue-scheduler -c hue-global-listener -- touch /notifications/1.txt
$ kubectl exec -it hue-scheduler -c hue-job-scheduler -- ls /incoming
1.txt
正如你所看到的,我们可以在一个容器的文件系统中看到另一个容器创建的文件;因此,容器可以通过共享的文件系统进行通信。
使用 HostPath 进行节点间通信
有时,你希望 Pod 能够访问一些主机信息(例如,Docker 守护进程),或者你希望同一节点上的 Pod 能够相互通信。如果 Pod 知道它们位于同一主机上,这会很有用。由于 Kubernetes 根据可用资源调度 Pod,Pod 通常不知道它们与其他 Pod 共享同一节点。有几种情况,Pod 可以依赖其他 Pod 与它们一起调度到同一节点:
-
在单节点集群中,所有 Pod 显然共享同一个节点
-
DaemonSetPod 总是与任何匹配其选择器的 Pod 共享一个节点 -
具有所需 Pod 亲和性的 Pod 总是一起调度
例如,在第五章,实践中使用 Kubernetes 资源中,我们讨论了一个DaemonSet Pod,它充当聚合代理,连接到其他 Pod。实现这种行为的另一种方式是,Pod 只需将其数据写入一个挂载的卷,该卷绑定到主机目录,DaemonSet Pod 可以直接读取并进行操作。
HostPath卷是一个挂载到 Pod 中的主机文件或目录。在你决定使用HostPath卷之前,确保你理解其后果:
-
这是一种安全风险,因为访问主机文件系统可能暴露敏感数据(例如,kubelet 密钥)
-
如果 Pod 是数据驱动的,并且它们主机上的文件不同,那么具有相同配置的 Pod 的行为可能会有所不同
-
它可能违反基于资源的调度,因为 Kubernetes 无法监控
HostPath资源 -
访问主机目录的容器必须具有一个安全上下文,且
privileged设置为true,或者在主机端,你需要更改权限以允许写入 -
在同一节点上协调多个 Pod 的磁盘使用是很困难的。
-
你很容易用完磁盘空间
这是一个配置文件,将/coupons目录挂载到hue-coupon-hunter容器中,该目录映射到主机的/etc/hue/data/coupons目录:
apiVersion: v1
kind: Pod
metadata:
name: hue-coupon-hunter
spec:
containers:
- image: the_g1g1/hue-coupon-hunter
name: hue-coupon-hunter
volumeMounts:
- mountPath: /coupons
name: coupons-volume
volumes:
- name: coupons-volume
host-path:
path: /etc/hue/data/coupons
由于 Pod 没有特权安全上下文,它将无法写入主机目录。让我们通过添加安全上下文来更改容器规格,从而启用它:
- image: the_g1g1/hue-coupon-hunter
name: hue-coupon-hunter
volumeMounts:
- mountPath: /coupons
name: coupons-volume
securityContext:
privileged: true
在下图中,你可以看到每个容器都有自己的本地存储区域,其他容器或 Pod 无法访问,而主机的/data目录作为卷挂载到容器 1 和容器 2 中:

图 6.1:容器本地存储
使用本地卷进行持久化节点存储
本地卷类似于HostPath,但它们在 Pod 重启和节点重启后依然存在。从这个意义上讲,它们被视为持久卷。它们是在 Kubernetes 1.7 中引入的。从 Kubernetes 1.14 开始,它们被认为是稳定的。本地卷的目的是支持有状态集,其中特定的 Pod 需要调度到包含特定存储卷的节点上。当地卷有节点亲和性注解,可以简化 Pod 与它们需要访问的存储之间的绑定。
我们需要为使用本地卷定义一个存储类。我们将在本章稍后详细讨论存储类。简而言之,存储类使用配置器为 Pod 分配存储。让我们在一个名为local-storage-class.yaml的文件中定义该存储类并创建它:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: local-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
$ k create -f local-storage-class.yaml
storageclass.storage.k8s.io/local-storage created
现在,我们可以使用存储类创建一个持久化存储卷,即使使用它的 Pod 被终止后,存储卷仍会保持:
apiVersion: v1
kind: PersistentVolume
metadata:
name: local-pv
labels:
release: stable
capacity: 10Gi
spec:
capacity:
storage: 10Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Delete
storageClassName: local-storage
local:
path: /mnt/disks/disk-1
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- k3d-k3s-default-agent-1
提供持久化存储卷
虽然emptyDir卷可以被容器挂载并使用,但它们不是持久的,不需要特殊配置,因为它们使用节点上现有的存储。HostPath卷在原节点上持久存在,但如果 Pod 在不同节点上重新启动,它将无法访问先前节点上的HostPath卷。本地卷是实际的持久化存储卷,使用管理员预先配置的存储或通过存储类的动态配置来提供存储。它们在节点上持久存在,并且可以在 Pod 重新启动、重新调度甚至节点重新启动时存活。一些持久化存储卷使用提前由管理员配置的外部存储(而非物理附加到节点的磁盘)。在云环境中,配置可能非常简化,但仍然是必需的,作为 Kubernetes 集群管理员,你必须至少确保存储配额充足,并且认真监控配额的使用情况。
记住,持久化存储卷是 Kubernetes 集群正在使用的资源,类似于节点。因此,它们不由 Kubernetes API 服务器管理。
你可以静态或动态配置资源。
静态配置持久化存储卷
静态配置非常简单。集群管理员预先创建由某些存储介质支持的持久化存储卷,这些存储卷可以被容器声明。
动态配置持久化存储卷
当持久化存储卷声明与任何静态配置的持久化存储卷不匹配时,可能会发生动态配置。如果声明指定了存储类,并且管理员已为该存储类配置了动态配置,则可以实时配置持久化存储卷。稍后我们将在讨论持久化存储卷声明和存储类时看到示例。
外部配置持久化存储卷
Kubernetes 最初包含了大量用于存储配置的“内置”代码,作为 Kubernetes 主代码库的一部分。随着 CSI 的引入,存储提供者开始从 Kubernetes 核心迁移到卷插件(即“外部”)。外部存储提供者的工作方式与内置动态存储提供者相同,但可以独立部署和更新。大多数内置存储提供者已迁移到外部。查看这个项目,获取编写外部存储提供者的库和指南:github.com/kubernetes-sigs/sig-storage-lib-external-provisioner。
创建持久卷
以下是一个 NFS 持久卷的配置文件:
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-777
spec:
capacity:
storage: 10Gi
volumeMode: Filesystem
accessModes:
- ReadWriteMany
- ReadOnlyMany
persistentVolumeReclaimPolicy: Recycle
storageClassName: slow
mountOptions:
- hard
- nfsvers=4.2
nfs:
path: /tmp
server: nfs-server.default.svc.cluster.local
持久卷有一个规范和元数据,可能包括存储类名称。我们在这里重点关注规范部分。它包含六个部分:容量、卷模式、访问模式、回收策略、存储类和卷类型(示例中为 nfs)。
容量
每个卷有指定的存储量。存储声明可以由至少具有该存储量的持久卷满足。在这个示例中,持久卷的容量为 10 吉比字节(单个吉比字节是 2 的 30 次方字节)。
capacity:
storage: 10Gi
在分配静态持久卷时,理解存储请求模式非常重要。例如,如果你配置了 20 个 100 GiB 容量的持久卷,并且某个容器声明了一个 150 GiB 的持久卷,则即使集群中总体容量足够,该声明也不会得到满足。
卷模式
可选的卷模式是在 Kubernetes 1.9 中作为 Alpha 功能添加的(在 Kubernetes 1.13 中移至 Beta),用于静态配置。它允许你指定是否需要文件系统(Filesystem)或原始存储(Block)。如果不指定卷模式,则默认使用 Filesystem,就像 1.9 之前的版本一样。
访问模式
有三种访问模式:
-
ReadOnlyMany:可以由多个节点以只读方式挂载 -
ReadWriteOnce:只能由单个节点以读写方式挂载 -
ReadWriteMany:可以由多个节点以读写方式挂载
存储被挂载到节点上,因此即使是ReadWriteOnce,同一节点上的多个容器也可以挂载该卷并写入数据。如果这导致问题,需要通过其他机制来处理(例如,仅在你知道每个节点只有一个的 DaemonSet pod 中声明该卷)。
不同的存储提供商支持这些模式的某些子集。当你配置持久卷时,可以指定它将支持哪些模式。例如,NFS 支持所有模式,但在这个示例中,仅启用了以下这些模式:
accessModes:
- ReadWriteMany
- ReadOnlyMany
回收策略
回收策略决定了当持久卷声明被删除时会发生什么。共有三种不同的策略:
-
Retain– 卷需要手动回收 -
Delete– 内容、卷和后端存储将被删除 -
Recycle– 仅删除内容(rm -rf /volume/*)
Retain 和 Delete 策略意味着持久卷将不再可用于未来的声明。Recycle 策略允许卷再次被声明。
目前,NFS 和 HostPath 支持回收策略,而 AWS EBS、GCE PD、Azure 磁盘和 Cinder 卷支持删除策略。请注意,动态供应的卷始终会被删除。
存储类
你可以通过规格中的可选字段 storageClassName 来指定存储类。如果你指定了,那么只有指定相同存储类的持久卷声明才能与持久卷绑定。如果没有指定存储类,则只能绑定那些未指定存储类的 PV 声明。
storageClassName: slow
卷类型
卷类型在规格中通过名称指定。规格中没有 volumeType 部分。在上面的例子中,nfs 是卷类型:
nfs:
path: /tmp
server: 172.17.0.8
每种卷类型可能有自己的一组参数。在这种情况下,它是路径和服务器。
我们稍后将介绍各种卷类型。
挂载选项
一些持久卷类型有额外的挂载选项,你可以指定。挂载选项不会进行验证。如果提供了无效的挂载选项,卷配置将失败。例如,NFS 支持额外的挂载选项:
mountOptions:
- hard
- nfsvers=4.1
现在我们已经看过了如何配置单个持久卷,让我们来看看投影卷,它提供了更多灵活性和存储抽象。
投影卷
投影卷允许你将多个持久卷挂载到同一目录中。你当然需要小心命名冲突。
以下卷类型支持投影卷:
-
ConfigMap -
Secret -
SownwardAPI -
ServiceAccountToken
ConfigMap and a Secret into the same directory:
apiVersion: v1
kind: Pod
metadata:
name: projected-volumes-demo
spec:
containers:
- name: projected-volumes-demo
image: busybox:1.28
volumeMounts:
- name: projected-volumes-demo
mountPath: "/projected-volume"
readOnly: true
volumes:
- name: projected-volumes-demo
projected:
sources:
- secret:
name: the-user
items:
- key: username
path: the-group/the-user
- configMap:
name: the-config-map
items:
- key: config
path: the-group/the-config-map
投影卷的参数与常规卷非常相似。例外情况是:
-
为了与
ConfigMap命名保持一致,secretName字段已更新为name,用于存储秘密。 -
defaultMode只能在投影级别设置,不能单独为每个卷源指定(但你可以为每个投影显式指定模式)。
让我们来看一种特殊的投影卷——serviceAccountToken 例外。
serviceAccountToken 投影卷
Kubernetes pods 可以使用与 pod 关联的服务账户的权限访问 Kubernetes API 服务器。serviceAccountToken 投影卷从安全角度为你提供更多粒度和控制。该令牌可以有过期时间和特定的受众。
更多详细信息请参见:kubernetes.io/docs/concepts/storage/projected-volumes/#serviceaccounttoken。
创建本地卷
本地卷是静态持久磁盘,分配到特定的节点上。它们类似于HostPath卷,但 Kubernetes 知道本地卷属于哪个节点,并且会将绑定到该本地卷的 Pod 调度到该节点上。这意味着 Pod 不会被驱逐,也不会调度到另一个数据不可用的节点上。
让我们创建一个本地卷。首先,我们需要创建一个支持目录。对于 KinD 和 k3d 集群,你可以通过 Docker 访问节点:
$ docker exec -it k3d-k3s-default-agent-1 mkdir -p /mnt/disks/disk-1
$ docker exec -it k3d-k3s-default-agent-1 ls -la /mnt/disks
total 12
drwxr-xr-x 3 0 0 4096 Jun 29 21:40 .
drwxr-xr-x 3 0 0 4096 Jun 29 21:40 ..
drwxr-xr-x 2 0 0 4096 Jun 29 21:40 disk-1
对于 minikube,你需要使用minikube ssh。
现在,我们可以创建一个由/mnt/disks/disk1目录支持的本地卷:
apiVersion: v1
kind: PersistentVolume
metadata:
name: local-pv
labels:
release: stable
capacity: 10Gi
spec:
capacity:
storage: 10Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Delete
storageClassName: local-storage
local:
path: /mnt/disks/disk-1
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- k3d-k3s-default-agent-1
这里是create命令:
$ k create -f local-volume.yaml
persistentvolume/local-pv created
$ k get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
local-pv 10Gi RWO Delete Bound default/local-storage-claim local-storage 6m44s
创建持久卷声明
当容器需要访问持久存储时,它们会发出声明(或者更确切地说,开发人员和集群管理员协调所需的存储资源来发出声明)。这里是一个匹配前面部分的持久卷的声明示例——创建本地卷:
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: local-storage-claim
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 8Gi
storageClassName: local-storage
selector:
matchLabels:
release: "stable"
matchExpressions:
- {key: capacity, operator: In, values: [8Gi, 10Gi]}
让我们先创建声明,然后再解释各个部分的作用:
$ k create -f local-persistent-volume-claim.yaml
persistentvolumeclaim/local-storage-claim created
$ k get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
local-storage-claim WaitForFirstConsumer local-pv 10Gi RWO local-storage 21m
名称local-storage-claim在稍后将会在挂载声明到容器时变得重要。
规格中的访问模式是ReadWriteOnce,这意味着如果声明被满足,则不能满足其他任何具有ReadWriteOnce访问模式的声明,但可以满足ReadOnlyMany的声明。
资源部分请求 8 GiB。这可以由我们容量为 10 Gi 的持久卷满足。但这有些浪费,因为 2 Gi 会被定义为未使用。
存储类名称是local-storage。如前所述,它必须与持久卷的存储类名称匹配。然而,对于 PVC,空的类名("")和没有类名完全不同。前者(空类名)匹配没有存储类名称的持久卷。后者(没有类名)仅在启用了DefaultStorageClass入站插件并使用默认存储类时,才会绑定到持久卷。
选择器部分允许你进一步筛选可用的卷。例如,这里卷必须匹配标签release:stable,并且还必须具有capacity:8Gi或capacity:10Gi标签。假设我们有一些其他卷,它们的容量分别为 20 Gi 和 50 Gi。我们不希望当只需要 8 Gi 时去声明一个 50 Gi 的卷。
Kubernetes 始终会尝试匹配能够满足声明的最小卷,但如果没有 8 Gi 或 10 Gi 的卷,标签将防止分配 20 Gi 或 50 Gi 的卷,并转而使用动态配置。
重要的是要意识到声明不会通过名称提到卷。你不能声明一个特定的卷。匹配是由 Kubernetes 基于存储类、容量和标签来完成的。
最后,持久卷声明属于一个命名空间。将持久卷绑定到声明是独占的。这意味着持久卷将绑定到一个命名空间。即使访问模式是ReadOnlyMany或ReadWriteMany,所有挂载该持久卷声明的 Pods 必须来自该声明的命名空间。
将声明挂载为卷
好的,我们已经配置了一个卷并声明了它。现在是时候在容器中使用已声明的存储了。这实际上很简单。首先,持久卷声明必须作为 Pod 中的卷使用,然后 Pod 中的容器可以像任何其他卷一样挂载它。以下是一个 Pod 清单,指定了我们之前创建的持久卷声明(绑定到我们提供的本地持久卷):
kind: Pod
apiVersion: v1
metadata:
name: the-pod
spec:
containers:
- name: the-container
image: g1g1/py-kube:0.3
volumeMounts:
- mountPath: "/mnt/data"
name: persistent-volume
volumes:
- name: persistent-volume
persistentVolumeClaim:
claimName: local-storage-claim
关键在于persistentVolumeClaim部分中的卷。声明名称(此处为local-storage-claim)在当前命名空间内唯一标识特定声明,并将其作为卷(此处命名为persistent-volume)提供。然后,容器可以通过名称引用它并将其挂载到"/mnt/data"。
在我们创建 Pod 之前,重要的是要注意持久卷声明实际上尚未声明任何存储,也未与我们的本地卷绑定。声明处于挂起状态,直到某个容器尝试使用该声明挂载卷:
$ k get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
local-storage-claim Pending local-storage 6m14s
现在,在创建 Pod 时,声明将会被绑定:
$ k create -f pod-with-local-claim.yaml
pod/the-pod created
$ k get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
local-storage-claim Bound local-pv 100Gi RWO local-storage 20m
原始块卷
Kubernetes 1.9 将此功能作为 Alpha 特性加入。Kubernetes 1.13 将其移至 Beta。从 Kubernetes 1.18 开始,它已成为 GA。
原始块卷提供对底层存储的直接访问,绕过了文件系统抽象。这对于需要高性能存储的应用程序(如数据库)非常有用,或者在需要一致的 I/O 性能和低延迟时尤为重要。以下存储提供者支持原始块卷:
-
AWSElasticBlockStore
-
AzureDisk
-
FC (光纤通道)
-
GCE 持久磁盘
-
iSCSI
-
本地卷
-
OpenStack Cinder
-
RBD(Ceph 块设备)
-
VsphereVolume
此外,许多 CSI 存储提供商也提供原始块卷。完整的列表请查看:kubernetes-csi.github.io/docs/drivers.html。
以下是如何使用 FireChannel 提供程序定义原始块卷:
apiVersion: v1
kind: PersistentVolume
metadata:
name: block-pv
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteOnce
volumeMode: Block
persistentVolumeReclaimPolicy: Retain
fc:
targetWWNs: ["50060e801049cfd1"]
lun: 0
readOnly: false
匹配的持久卷声明 (PVC) 也必须指定volumeMode: Block。以下是其格式:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: block-pvc
spec:
accessModes:
- ReadWriteOnce
volumeMode: Block
resources:
requests:
storage: 10Gi
Pods 将原始块卷作为/dev下的设备使用,而不是作为挂载的文件系统。容器可以访问这些设备并进行读写。实际上,这意味着对块存储的 I/O 请求会直接传递到底层的块存储,而不是经过文件系统驱动程序。这理论上更快,但实际上,如果你的应用程序依赖于文件系统缓冲,可能会导致性能下降。
这是一个包含容器的 Pod,它将block-pvc与名为/dev/xdva的原始块存储绑定:
apiVersion: v1
kind: Pod
metadata:
name: pod-with-block-volume
spec:
containers:
- name: fc-container
image: fedora:26
command: ["/bin/sh", "-c"]
args: ["tail -f /dev/null"]
volumeDevices:
- name: data
devicePath: /dev/xvda
volumes:
- name: data
persistentVolumeClaim:
claimName: block-pvc
CSI 临时卷
我们将在本章稍后部分的 容器存储接口 章节详细介绍容器存储接口(CSI)。CSI 临时卷由节点上的本地存储提供支持。这些卷的生命周期与 pod 的生命周期相关联。此外,它们只能由该 pod 的容器挂载,这对于直接将秘密和证书填充到 pod 中而无需经过 Kubernetes 秘密对象非常有用。
下面是一个带有 CSI 临时卷的 pod 示例:
kind: Pod
apiVersion: v1
metadata:
name: the-pod
spec:
containers:
- name: the-container
image: g1g1/py-kube:0.3
volumeMounts:
- mountPath: "/data"
name: the-volume
command: [ "sleep", "1000000" ]
volumes:
- name: the-volume
csi:
driver: inline.storage.kubernetes.io
volumeAttributes:
key: value
自 Kubernetes 1.25 版本以来,CSI 临时卷已经 GA(一般可用)。然而,并不是所有的 CSI 驱动程序都支持它们。如往常一样,请检查列表:kubernetes-csi.github.io/docs/drivers.html。
通用临时卷
通用临时卷是另一种与 pod 生命周期相关联的卷类型。当 pod 被删除时,通用临时卷也会被删除。
这种卷类型实际上会创建一个完整的持久卷声明。这提供了几个功能:
-
卷的存储可以是本地的,也可以是网络附加的。
-
卷可以选择配置为固定大小。
-
根据驱动程序和指定的参数,卷可能包含初始数据。
-
如果驱动程序支持,通常的操作如快照、克隆、调整大小和存储容量跟踪可以对这些卷执行。
下面是一个带有通用临时卷的 pod 示例:
kind: Pod
apiVersion: v1
metadata:
name: the-pod
spec:
containers:
- name: the-container
image: g1g1/py-kube:0.3
volumeMounts:
- mountPath: "/data"
name: the-volume
command: [ "sleep", "1000000" ]
volumes:
- name: the-volume
ephemeral:
volumeClaimTemplate:
metadata:
labels:
type: generic-ephemeral-volume
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: generic-storage
resources:
requests:
storage: 1Gi
请注意,从安全角度来看,具有创建 pod 权限但没有 PVC 权限的用户,现在可以通过通用临时卷创建 PVC。为了防止这种情况,可以使用准入控制。
存储类
我们已经遇到过存储类了。那么,它们到底是什么?存储类允许管理员配置一个带有自定义持久存储的集群(只要有适当的插件来支持它)。存储类在元数据中有一个名称(必须在声明的 storageClassName 文件中指定),以及一个提供者、回收策略和参数。
我们之前为本地存储声明了一个存储类。下面是一个使用 AWS EBS 作为提供者的示例存储类(因此它仅适用于 AWS):
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: standard
provisioner: kubernetes.io/aws-ebs
parameters:
type: gp2
reclaimPolicy: Retain
allowVolumeExpansion: true
mountOptions:
- debug
volumeBindingMode: Immediate
你可以为相同的提供者创建多个存储类,使用不同的参数。每个提供者都有自己的参数。
当前支持的提供者有:
-
AWSElasticBlockStore
-
AzureFile
-
AzureDisk
-
CephFS
-
Cinder
-
FC
-
FlexVolume
-
Flocker
-
GCE 持久磁盘
-
GlusterFS
-
iSCSI
-
Quobyte
-
NFS
-
RBD
-
VsphereVolume
-
PortworxVolume
-
ScaleIO
-
StorageOS
-
本地
这个列表不包括其他卷类型的提供者,例如 configMap 或 secret,这些类型不依赖于典型的网络存储。这些卷类型不需要存储类。智能地利用卷类型是设计和管理集群的关键部分。
默认存储类
集群管理员还可以分配默认存储类。当分配了默认存储类并且启用了 DefaultStorageClass 入场插件时,未指定存储类的声明将使用默认存储类动态配置。如果没有定义默认存储类或未启用入场插件,则未指定存储类的声明只能匹配没有存储类的卷。
我们已经涵盖了很多内容,并探讨了多种配置存储和以不同方式使用存储的选项。现在,我们将把所有内容整合起来,展示从头到尾的整个过程。
演示持久卷存储的端到端过程
为了说明所有概念,让我们做一个小型演示,创建一个 HostPath 卷,声明它,挂载它,并让容器写入其中。我们将使用 k3d 来完成这部分。
首先,我们通过使用 dir 存储类创建一个 hostPath 卷。将以下内容保存为 dir-persistent-volume.yaml:
kind: PersistentVolume
apiVersion: v1
metadata:
name: dir-pv
spec:
storageClassName: dir
capacity:
storage: 1Gi
accessModes:
- ReadWriteMany
hostPath:
path: "/tmp/data"
然后,我们来创建它:
$ k create -f dir-persistent-volume.yaml
persistentvolume/dir-pv created
要查看可用的卷,可以使用资源类型 persistentvolumes,简写为 pv:
$ k get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
dir-pv 1Gi RWX Retain Available dir 22s
容量为请求的 1 GiB。回收策略是 Retain,因为主机路径卷会被保留(不会销毁)。状态为 Available,因为该卷尚未被声明。访问模式指定为 RWX,即 ReadWriteMany。所有访问模式都有一个简写版本:
-
RWO–ReadWriteOnce -
ROX–ReadOnlyMany -
RWX–ReadWriteMany
我们有一个持久卷。我们来创建一个声明。将以下内容保存为 dir-persistent-volume-claim.yaml:
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: dir-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
然后,运行以下命令:
$ k create -f dir-persistent-volume-claim.yaml
persistentvolumeclaim/dir-pvc created
我们来检查一下声明和卷:
$ k get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
dir-pvc Bound dir-pv 1Gi RWX dir 106s
$ k get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
dir-pv 1Gi RWX Retain Bound default/dir-pvc dir 4m25s
如你所见,声明和卷已经绑定在一起并互相引用。绑定之所以有效,是因为卷和声明使用了相同的存储类。但是,如果它们不匹配会怎样呢?我们来删除持久卷声明中的存储类,看看会发生什么。将以下持久卷声明保存为 some-persistent-volume-claim.yaml:
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: some-pvc
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Gi
然后,创建它:
$ k create -f some-persistent-volume-claim.yaml
persistentvolumeclaim/some-pvc created
好的,已经创建了。我们来看一下:
$ k get pvc some-pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
some-pvc Pending local-path 3m29s
非常有趣。some-pvc 声明与我们从未指定的 local-path 存储类关联,但它仍然是待处理状态。我们来了解一下原因。
这是 local-path 存储类:
$ k get storageclass local-path -o yaml
kind: StorageClass
metadata:
annotations:
objectset.rio.cattle.io/applied: H4sIAAAAAAAA/4yRT+vUMBCGv4rMua1bu1tKwIO u7EUEQdDzNJlux6aZkkwry7LfXbIqrIffn2PyZN7hfXIFXPg7xcQSwEBSiXimaupSxfJ2q6GAiYMDA9 /+oKPHlKCAmRQdKoK5AoYgisoSUj5K/5OsJtIqslQWVT3lNM4xUDzJ5VegWJ63CQxMTXogW128+czBvf/gnIQXIwLOBAa8WPTl30qvGkoL2jw5rT2V6ZKUZij+SbG5eZVRDKR0F8SpdDTg6rW8YzCgcSW4FeCxJ/+sjxHTCAbqrhmag20Pw9DbZtfu210z7JuhPnQ719m2w3cOe7fPof81W1DHfLlE2Th/IEUwEDHYkWJe8PCs gJgL8PxVPNsLGPhEnjRr2cSvM33k4Dicv4jLC34g60niiWPSo4S0zhTh9jsAAP//ytgh5S0CAAA
objectset.rio.cattle.io/id: ""
objectset.rio.cattle.io/owner-gvk: k3s.cattle.io/v1, Kind=Addon
objectset.rio.cattle.io/owner-name: local-storage
objectset.rio.cattle.io/owner-namespace: kube-system
storageclass.kubernetes.io/is-default-class: "true"
creationTimestamp: "2022-06-22T18:16:56Z"
labels:
objectset.rio.cattle.io/hash: 183f35c65ffbc3064603f43f1580d8c68a2dabd4
name: local-path
resourceVersion: "290"
uid: b51cf456-f87e-48ac-9062-4652bf8f683e
provisioner: rancher.io/local-path
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
这是一个附带 k3d(k3s)的存储类。
注意注释:storageclass.kubernetes.io/is-default-class: "true"。它告诉 Kubernetes 这是默认的存储类。由于我们的 PVC 没有指定存储类名称,因此它与默认存储类关联。但是,为什么声明仍然是待处理状态?原因是 volumeBindingMode 为 WaitForFirstConsumer。这意味着只有当容器尝试通过声明挂载卷时,声明的卷才会动态配置。
回到我们的 dir-pvc。最后一步是创建一个具有两个容器的 Pod,并将声明作为卷分配给它们两个。将以下内容保存到 shell-pod.yaml:
kind: Pod
apiVersion: v1
metadata:
name: just-a-shell
labels:
name: just-a-shell
spec:
containers:
- name: a-shell
image: g1g1/py-kube:0.3
command: ["sleep", "10000"]
volumeMounts:
- mountPath: "/data"
name: pv
- name: another-shell
image: g1g1/py-kube:0.3
command: ["sleep", "10000"]
volumeMounts:
- mountPath: "/another-data"
name: pv
volumes:
- name: pv
persistentVolumeClaim:
claimName: dir-pvc
这个 Pod 有两个容器,使用 g1g1/py-kube:0.3 镜像,两个容器都只是长时间休眠。目的是让容器保持运行状态,这样我们可以稍后连接它们并检查它们的文件系统。该 Pod 挂载了我们的持久卷声明,卷的名称为 pv。请注意,卷的规范仅在 Pod 层面定义一次,多个容器可以将其挂载到不同的目录中。
让我们创建 Pod 并验证两个容器是否都在运行:
$ k create -f shell-pod.yaml
pod/just-a-shell created
$ k get po just-a-shell -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
just-a-shell 2/2 Running 0 74m 10.42.2.104 k3d-k3s-default-agent-1 <none> <none>
接下来,连接到节点(k3d-k3s-default-agent-1)。这是主机,其 /tmp/data 是 Pod 的卷,并挂载为 /data 和 /another-data 进入每个运行中的容器:
$ docker exec -it k3d-k3s-default-agent-1 sh
/ #
接下来,我们在主机的 /tmp/data 目录下创建一个文件。该文件应该通过挂载卷在两个容器之间都能看到:
/ # echo "yeah, it works" > /tmp/data/cool.txt
让我们从外部验证文件 cool.txt 是否确实可用:
$ docker exec -it k3d-k3s-default-agent-1 cat /tmp/data/cool.txt
yeah, it works
接下来,让我们验证文件在容器中是否可用(在它们映射的目录中):
$ k exec -it just-a-shell -c a-shell -- cat /data/cool.txt
yeah, it works
$ k exec -it just-a-shell -c another-shell -- cat /another-data/cool.txt
yeah, it works
我们甚至可以在其中一个容器中创建一个新文件 yo.txt,并看到它在另一个容器或节点本身上也可用:
$ k exec -it just-a-shell -c another-shell – bash –c "echo yo > /another-data/yo.txt"
yo /another-data/yo.txt
$ k exec -it just-a-shell -c a-shell cat /data/yo.txt
yo
$ k exec -it just-a-shell -c another-shell cat /another-data/yo.txt
yo
是的,一切如预期般工作,两个容器共享相同的存储。
公有云存储卷类型 – GCE、AWS 和 Azure
在本节中,我们将介绍一些领先公有云平台中常见的卷类型。在大规模管理存储是一个复杂的任务,最终涉及到物理资源,类似于节点。如果您选择在公有云平台上运行 Kubernetes 集群,您可以让云提供商处理所有这些挑战,专注于您的系统。但理解每种卷类型的不同选项、约束和限制是非常重要的。
我们将要讲解的许多卷类型以前是由内置插件(Kubernetes 核心部分)处理的,但现在已经迁移到外部 CSI 插件。
CSI 迁移功能允许将具有相应外部 CSI 插件的内置插件指向外部插件,以作为过渡措施。
我们稍后会讲解 CSI 本身。
AWS 弹性块存储(EBS)
AWS 提供了 弹性块存储(EBS)作为 EC2 实例的持久存储。AWS Kubernetes 集群可以使用 AWS EBS 作为持久存储,但有以下限制:
-
Pod 必须在 AWS EC2 实例上运行作为节点
-
Pod 只能访问在其可用区内配置的 EBS 卷
-
一个 EBS 卷可以挂载到单个 EC2 实例上
这些是严重的限制。单一可用区的限制虽然对性能有好处,但却剥夺了在大规模或跨地理分布系统中共享存储的能力,除非进行自定义复制和同步。单个 EBS 卷只能绑定一个 EC2 实例的限制意味着,即使在同一个可用区内,pod 也不能共享存储(即使是只读),除非确保它们运行在同一个节点上。
这是一个内置插件的示例,它也有 CSI 驱动并支持 CSIMigration。这意味着,如果安装了 AWS EBS 的 CSI 驱动(ebs.csi.aws.com),则内置插件会将所有插件操作重定向到外部插件。
也可以通过将 InTreePluginAWSUnregister 功能开关设置为 true(默认为 false)来禁用加载内置的 awsElasticBlockStore 存储插件。
查看所有功能开关:kubernetes.io/docs/reference/command-line-tools-reference/feature-gates/。
让我们看看如何定义一个 AWS EBS 持久卷(静态配置):
apiVersion: v1
kind: PersistentVolume
metadata:
name: test-pv
spec:
accessModes:
- ReadWriteOnce
capacity:
storage: 5Gi
csi:
driver: ebs.csi.aws.com
volumeHandle: {EBS volume ID}
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: topology.ebs.csi.aws.com/zone
operator: In
values:
- {availability zone}
然后你需要定义一个 PVC:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: ebs-claim
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
最后,pod 可以挂载 PVC:
apiVersion: v1
kind: Pod
metadata:
name: some-pod
spec:
containers:
- image: some-container
name: some-container
volumeMounts:
- name: persistent-storage
mountPath: /data
volumes:
- name: persistent-storage
persistentVolumeClaim:
claimName: ebs-claim
AWS 弹性文件系统(EFS)
AWS 提供了一项名为 Elastic File System (EFS) 的服务。这实际上是一个托管的 NFS 服务。它使用 NFS 4.1 协议,并且相较于 EBS 具有许多优点:
-
多个 EC2 实例可以在多个可用区之间访问相同的文件(但必须在同一区域内)
-
容量会根据实际使用情况自动扩展和缩减
-
你只需为你使用的部分付费
-
你可以通过 VPN 将本地服务器连接到 EFS
-
EFS 运行在 SSD 硬盘上,这些硬盘会自动在可用区之间进行复制
也就是说,EFS 比 EBS 更为广泛,即使考虑到自动复制到多个可用区(假设你充分利用了 EBS 卷)。推荐的使用 EFS 的方式是通过它专用的 CSI 驱动:github.com/kubernetes-sigs/aws-efs-csi-driver。
这是静态配置的示例。首先,定义持久卷:
apiVersion: v1
kind: PersistentVolume
metadata:
name: efs-pv
spec:
capacity:
storage: 1Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
csi:
driver: efs.csi.aws.com
volumeHandle: <Filesystem Id>
你可以使用 AWS CLI 查找 Filesystem Id:
aws efs describe-file-systems --query "FileSystems[*].FileSystemId"
然后定义一个 PVC:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: efs-claim
spec:
accessModes:
- ReadWriteOnce
storageClassName: ""
resources:
requests:
storage: 1Gi
这里是一个使用 EFS 的 pod:
piVersion: v1
kind: Pod
metadata:
name: efs-app
spec:
containers:
- name: app
image: centos
command: ["/bin/sh"]
args: ["-c", "while true; do echo $(date -u) >> /data/out.txt; sleep 5; done"]
volumeMounts:
- name: persistent-storage
mountPath: /data
volumes:
- name: persistent-storage
persistentVolumeClaim:
claimName: efs-claim
你还可以通过定义一个合适的存储类来进行动态配置,而不是创建静态卷:
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: efs-sc
provisioner: efs.csi.aws.com
parameters:
provisioningMode: efs-ap
fileSystemId: <Filesystem Id>
directoryPerms: "700"
gidRangeStart: "1000" # optional
gidRangeEnd: "2000" # optional
basePath: "/dynamic_provisioning" # optional
PVC 类似,但现在使用了存储类名称:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: efs-claim
spec:
accessModes:
- ReadWriteMany
storageClassName: efs-sc
resources:
requests:
storage: 5Gi
pod 像之前一样使用 PVC:
apiVersion: v1
kind: Pod
metadata:
name: efs-app
spec:
containers:
- name: app
image: centos
command: ["/bin/sh"]
args: ["-c", "while true; do echo $(date -u) >> /data/out; sleep 5; done"]
volumeMounts:
- name: persistent-storage
mountPath: /data
volumes:
- name: persistent-storage
persistentVolumeClaim:
claimName: efs-claim
GCE 持久化磁盘
gcePersistentDisk 卷类型与 awsElasticBlockStore 非常相似。你必须提前配置磁盘。它只能被同一项目和可用区内的 GCE 实例使用。但同一个磁盘可以在多个实例上作为只读使用。这意味着它支持 ReadWriteOnce 和 ReadOnlyMany。你可以使用 GCE 持久化磁盘在同一可用区内的多个 pod 之间以只读方式共享数据。
它还具有一个名为 pd.csi.storage.gke.io 的 CSI 驱动程序,并支持 CSIMigration。
如果使用 ReadWriteOnce 模式的持久磁盘所在的 Pod 由复制控制器、复制集或部署控制,副本数量必须为 0 或 1。尝试扩展到超过 1 将因显而易见的原因而失败。
下面是使用 CSI 驱动程序的 GCE 持久磁盘存储类:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: csi-gce-pd
provisioner: pd.csi.storage.gke.io
parameters:
labels: key1=value1,key2=value2
volumeBindingMode: WaitForFirstConsumer
下面是 PVC 示例:
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: gce-pd-pvc
spec:
accessModes:
- ReadWriteOnce
storageClassName: csi-gce-pd
resources:
requests:
storage: 200Gi
Pod 可以使用它进行动态供给:
apiVersion: v1
kind: Pod
metadata:
name: some-pod
spec:
containers:
- image: some-image
name: some-container
volumeMounts:
- mountPath: /pd
name: some-volume
volumes:
- name: some-volume
persistentVolumeClaim:
claimName: gce-pd-pvc
readOnly: false
GCE 持久磁盘自 Kubernetes 1.10(Beta 版本)起支持区域磁盘选项。区域持久磁盘在两个区域之间自动同步。以下是区域持久磁盘的存储类示例:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: csi-gce-pd
provisioner: pd.csi.storage.gke.io
parameters:
type: pd-standard
replication-type: regional-pd
volumeBindingMode: WaitForFirstConsumer
Google Cloud Filestore
Google Cloud Filestore 是 GCP 的托管 NFS 文件服务。Kubernetes 没有内置的插件来支持它,也没有通用的 CSI 驱动程序。
然而,GKE 上使用了一个 CSI 驱动程序,如果你愿意尝试,即使你自己在 GCP 上安装 Kubernetes 并希望将 Google Cloud Storage 作为存储选项,也可以尝试使用它。
请参见:github.com/kubernetes-sigs/gcp-filestore-csi-driver。
Azure 数据磁盘
Azure 数据磁盘是存储在 Azure 存储中的虚拟硬盘。它的功能与 AWS EBS 或 GCE 持久磁盘类似。
它还有一个名为 disk.csi.azure.com 的 CSI 驱动程序,并支持 CSIMigration。请参见:github.com/kubernetes-sigs/azuredisk-csi-driver。
下面是定义 Azure 磁盘持久化卷的示例:
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-azuredisk
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: managed-csi
csi:
driver: disk.csi.azure.com
readOnly: false
volumeHandle: /subscriptions/{sub-id}/resourcegroups/{group-name}/providers/microsoft.compute/disks/{disk-id}
volumeAttributes:
fsType: ext4
除了必需的 diskName 和 diskURI 参数外,它还具有一些可选参数:
-
kind:磁盘存储配置的可用选项有Shared(允许每个存储帐户多个磁盘)、Dedicated(每个存储帐户提供一个独立的 Blob 磁盘)或Managed(提供 Azure 管理的数据磁盘)。默认值是Shared。 -
cachingMode:磁盘缓存模式。必须是None、ReadOnly或ReadWrite之一。默认值是None。 -
fsType:设置挂载的文件系统类型。默认值是ext4。 -
readOnly:文件系统是否作为readOnly使用。默认值是false。
Azure 数据磁盘的最大容量为 32 GiB。每个 Azure 虚拟机可以有最多 32 个数据磁盘。更大的虚拟机规格可以连接更多的磁盘。你可以将 Azure 数据磁盘附加到单个 Azure 虚拟机上。
通常,你应该创建一个 PVC 并在 Pod(或 Pod 控制器)中使用它。
Azure 文件
除了数据磁盘外,Azure 还提供了一个类似于 AWS EFS 的共享文件系统。不过,Azure 文件存储使用 SMB/CIFS 协议(它支持 SMB 2.1 和 SMB 3.0)。它基于 Azure 存储平台,并具备与 Azure Blob、Table 或 Queue 存储相同的可用性、耐久性、可扩展性和地理冗余能力。
为了使用 Azure 文件存储,您需要在每个客户端虚拟机上安装cifs-utils包。您还需要创建一个机密,这是一个必需的参数:
apiVersion: v1
kind: Secret
metadata:
name: azure-file-secret
type: Opaque
data:
azurestorageaccountname: <base64 encoded account name>
azurestorageaccountkey: <base64 encoded account key>
这是一个使用 Azure 文件存储的 Pod:
apiVersion: v1
kind: Pod
metadata:
name: some-pod
spec:
containers:
- image: some-container
name: some-container
volumeMounts:
- name: some-volume
mountPath: /azure
volumes:
- name: some-volume
azureFile:
secretName: azure-file-secret
shareName: azure-share
readOnly: false
Azure 文件存储支持在同一区域内共享,并连接本地客户端。
这部分内容涵盖了公共云存储卷类型。接下来,我们来看一些您可以在集群中自己安装的分布式存储卷。
Kubernetes 中的 GlusterFS 和 Ceph 卷
GlusterFS 和 Ceph 是两种分布式持久存储系统。GlusterFS 本质上是一个网络文件系统,而 Ceph 本质上是一个对象存储。两者都暴露块、对象和文件系统接口。两者在底层都使用 xfs 文件系统来存储数据和元数据作为 xattr 属性。您可能希望在 Kubernetes 集群中使用 GlusterFS 或 Ceph 作为持久卷的几个原因包括:
-
您运行的是本地存储,云存储不可用
-
您可能有大量数据和应用程序需要访问 GlusterFS 或 Ceph 中的数据
-
您具备管理 GlusterFS 或 Ceph 的操作经验
-
您运行在云端,但云平台持久存储的限制使其不可行
让我们更详细地了解一下 GlusterFS。
使用 GlusterFS
GlusterFS 故意保持简单,将底层目录暴露出来,交由客户端(或中间件)处理高可用性、复制和分发。GlusterFS 将数据组织成逻辑卷,这些逻辑卷涵盖了包含砖块的多个节点(机器),砖块用于存储文件。文件根据 DHT(分布式哈希表)分配到砖块。如果文件被重命名,或者 GlusterFS 集群被扩展或重新平衡,文件可能会在砖块之间移动。以下图展示了 GlusterFS 的构建块:

图 6.2:GlusterFS 构建块
要将 GlusterFS 集群用作 Kubernetes 的持久存储(假设您有一个运行中的 GlusterFS 集群),您需要按照几个步骤进行操作。特别是,GlusterFS 节点由插件作为 Kubernetes 服务管理。
创建端点
这是您可以通过 kubectl create 创建的端点资源示例,作为普通的 Kubernetes 资源:
kind: Endpoints
apiVersion: v1
metadata:
name: glusterfs-cluster
subsets:
- addresses:
- ip: 10.240.106.152
ports:
- port: 1
- addresses:
- ip: 10.240.79.157
ports:
- port: 1
添加一个 GlusterFS Kubernetes 服务
为了使端点保持持久性,您可以使用一个没有选择器的 Kubernetes 服务,表示这些端点是手动管理的:
kind: Service
apiVersion: v1
metadata:
name: glusterfs-cluster
spec:
ports:
- port: 1
创建 Pods
最后,在 Pod 规格的volumes部分,提供以下信息:
volumes:
- name: glusterfsvol
glusterfs:
endpoints: glusterfs-cluster
path: kube_vol
readOnly: true
容器随后可以按名称挂载glusterfsvol。
端点告诉 GlusterFS 卷插件如何找到 GlusterFS 集群的存储节点。
曾有尝试为 GlusterFS 创建 CSI 驱动程序,但该项目已被放弃:github.com/gluster/gluster-csi-driver。
在介绍完 GlusterFS 后,我们来看看 CephFS。
使用 Ceph
Ceph 的对象存储可以通过多种接口访问。与 GlusterFS 不同,Ceph 会自动完成很多工作。它自行进行分布式存储、复制和自我修复。下图展示了 RADOS——底层对象存储——可以通过多种方式访问。

图 6.3:访问 RADOS
Kubernetes 通过 Rados Block Device(RBD)接口支持 Ceph。
使用 RBD 连接到 Ceph
必须在 Kubernetes 集群的每个节点上安装 ceph-common。一旦你的 Ceph 集群启动并运行,你需要在 Pod 配置文件中提供 Ceph RBD 卷插件所需的一些信息:
-
monitors: Ceph 监视器。 -
pool: RADOS 池的名称。如果未提供,则使用默认的 RBD 池。 -
image: RBD 创建的镜像名称。 -
user: RADOS 用户名。如果未提供,则使用默认的管理员。 -
keyring: 密钥环文件的路径。如果未提供,则使用默认路径/etc/ceph/keyring。 -
secretName: 认证秘密的名称。如果提供了,secretName将覆盖keyring。注意:请参见下面关于如何创建秘密的段落。 -
fsType: 设备上格式化的文件系统类型(如ext4、xfs等)。 -
readOnly: 是否将文件系统用作readOnly。
如果使用 Ceph 认证秘密,则需要创建一个秘密对象:
apiVersion: v1
kind: Secret
metadata:
name: ceph-secret
type: "kubernetes.io/rbd"
data:
key: QVFCMTZWMVZvRjVtRXhBQTVrQ1FzN2JCajhWVUxSdzI2Qzg0SEE9PQ==
秘密类型为 kubernetes.io/rbd。
这是一个示例 Pod,它通过 RBD 使用内置提供程序与 Ceph 进行连接,并使用了一个秘密:
apiVersion: v1
kind: Pod
metadata:
name: rbd2
spec:
containers:
- image: kubernetes/pause
name: rbd-rw
volumeMounts:
- name: rbdpd
mountPath: /mnt/rbd
volumes:
- name: rbdpd
rbd:
monitors:
- '10.16.154.78:6789'
- '10.16.154.82:6789'
- '10.16.154.83:6789'
pool: kube
image: foo
fsType: ext4
readOnly: true
user: admin
secretRef:
name: ceph-secret
Ceph RBD 支持 ReadWriteOnce 和 ReadOnlyMany 访问模式。但如今,最好通过 Rook 来使用 Ceph。
Rook
Rook 是一个开源的云原生存储编排器。目前它是一个已毕业的 CNCF 项目。以前它提供了一个一致的体验,支持多种存储解决方案,如 Ceph、edgeFS、Cassandra、Minio、NFS、CockroachDB 和 YugabyteDB。但最终,它聚焦于只支持 Ceph。以下是 Rook 提供的功能:
-
自动化部署
-
启动
-
配置
-
配置存储
-
扩展
-
升级
-
迁移
-
调度
-
生命周期管理
-
资源管理
-
监控
-
灾难恢复
Rook 利用现代 Kubernetes 最佳实践,如 CRD 和操作符。
这是 Rook 架构:

图 6.4:Rook 架构
一旦安装了 Rook 操作符,你可以使用 Rook CRD 创建 Ceph 集群,例如:github.com/rook/rook/blob/release-1.10/deploy/examples/cluster.yaml。
这是一个简化版本(没有注释):
apiVersion: ceph.rook.io/v1
kind: CephCluster
metadata:
name: rook-ceph
namespace: rook-ceph # namespace:cluster
spec:
cephVersion:
image: quay.io/ceph/ceph:v17.2.5
allowUnsupported: false
dataDirHostPath: /var/lib/rook
skipUpgradeChecks: false
continueUpgradeAfterChecksEvenIfNotHealthy: false
waitTimeoutForHealthyOSDInMinutes: 10
mon:
count: 3
allowMultiplePerNode: false
mgr:
count: 2
allowMultiplePerNode: false
modules:
- name: pg_autoscaler
enabled: true
dashboard:
enabled: true
ssl: true
monitoring:
enabled: false
network:
connections:
encryption:
enabled: false
compression:
enabled: false
crashCollector:
disable: false
logCollector:
enabled: true
periodicity: daily # one of: hourly, daily, weekly, monthly
maxLogSize: 500M # SUFFIX may be 'M' or 'G'. Must be at least 1M.
cleanupPolicy:
confirmation: ""
sanitizeDisks:
method: quick
dataSource: zero
iteration: 1
allowUninstallWithVolumes: false
annotations:
labels:
resources:
removeOSDsIfOutAndSafeToRemove: false
priorityClassNames:
mon: system-node-critical
osd: system-node-critical
mgr: system-cluster-critical
storage: # cluster level storage configuration and selection
useAllNodes: true
useAllDevices: true
config:
onlyApplyOSDPlacement: false
disruptionManagement:
managePodBudgets: true
osdMaintenanceTimeout: 30
pgHealthCheckTimeout: 0
manageMachineDisruptionBudgets: false
machineDisruptionBudgetNamespace: openshift-machine-api
healthCheck:
daemonHealth:
mon:
disabled: false
interval: 45s
osd:
disabled: false
interval: 60s
status:
disabled: false
interval: 60s
livenessProbe:
mon:
disabled: false
mgr:
disabled: false
osd:
disabled: false
startupProbe:
mon:
disabled: false
mgr:
disabled: false
osd:
disabled: false
这是 CephFS 的存储类:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: rook-ceph-retain-bucket
provisioner: rook-ceph.ceph.rook.io/bucket # driver:namespace:cluster
# set the reclaim policy to retain the bucket when its OBC is deleted
reclaimPolicy: Retain
parameters:
objectStoreName: my-store # port 80 assumed
objectStoreNamespace: rook-ceph # namespace:cluster
完整代码可以在这里找到:github.com/rook/rook/blob/release-1.10/deploy/examples/storageclass-bucket-retain.yaml。
现在我们已经介绍了使用 GlusterFS、Ceph 和 Rook 的分布式存储,让我们来看看企业级存储选项。
将企业存储集成到 Kubernetes 中
如果你有一个通过 iSCSI 接口暴露的现有存储区域网络(SAN),Kubernetes 为你提供了一个卷插件。它遵循与我们之前看到的其他共享持久存储插件相同的模型。它支持以下功能:
-
连接到一个门户
-
直接挂载设备或通过
multipathd -
格式化和分区任何新设备
-
通过 CHAP 进行认证
你必须配置 iSCSI 发起程序,但无需提供任何发起程序信息。你只需提供以下内容:
-
iSCSI 目标的 IP 地址和端口(如果不是默认的
3260) -
目标的IQN(iSCSI 合格名称)——通常是反转的域名
-
LUN(逻辑单元号)
-
文件系统类型
-
Readonly布尔标志
iSCSI 插件支持ReadWriteOnce和ReadonlyMany。请注意,此时你不能对设备进行分区。下面是一个带有 iSCSI 卷规格的示例 Pod:
---
apiVersion: v1
kind: Pod
metadata:
name: iscsipd
spec:
containers:
- name: iscsipd-rw
image: kubernetes/pause
volumeMounts:
- mountPath: "/mnt/iscsipd"
name: iscsipd-rw
volumes:
- name: iscsipd-rw
iscsi:
targetPortal: 10.0.2.15:3260
portals: ['10.0.2.16:3260', '10.0.2.17:3260']
iqn: iqn.2001-04.com.example:storage.kube.sys1.xyz
lun: 0
fsType: ext4
readOnly: true
其他存储提供商
Kubernetes 的存储领域不断创新。许多公司将其产品适配到 Kubernetes,一些公司和组织则构建了专门的 Kubernetes 存储解决方案。以下是一些更受欢迎和成熟的解决方案:
-
OpenEBS
-
Longhorn
-
Portworx
容器存储接口
容器存储接口(CSI)是容器编排器与存储提供商之间交互的标准接口。它由 Kubernetes、Docker、Mesos 和 Cloud Foundry 开发。其目的是让存储提供商只实现一个 CSI 驱动程序,而所有容器编排器只需要支持 CSI。这相当于存储的 CNI。
在 Kubernetes 1.9 中添加了一个 CSI 卷插件作为 Alpha 功能,并自 Kubernetes 1.13 起正式提供。以前的 FlexVolume 方法(你可能遇到过)现在已经被弃用。
下面是一个展示 CSI 如何在 Kubernetes 内工作的示意图:

图 6.5:CSI 架构
将所有内建插件迁移到外部 CSI 驱动程序的工作正在顺利进行。更多详情请参见kubernetes-csi.github.io。
高级存储功能
这些功能仅对 CSI 驱动程序可用。它们代表了一种统一的存储模型的优势,允许通过统一的接口在所有存储提供商中添加可选的高级功能。
卷快照
卷快照从 Kubernetes 1.20 开始正式提供。它们就是字面意思——在某个时间点对卷的快照。你可以从快照创建卷,并在之后恢复。值得注意的是,与快照相关的 API 对象是 CRD,而不是 Kubernetes 核心 API 的一部分。这些对象包括:
-
VolumeSnapshotClass -
VolumeSnapshotContents -
VolumeSnapshot
卷快照使用一个external-prosnapshotter侧车容器,该容器由 Kubernetes 团队开发。它监视快照 CRD 的创建,并与快照控制器交互,后者可以调用实现快照支持的 CSI 驱动程序的CreateSnapshot和DeleteSnapshot操作。
以下是声明卷快照的方式:
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
name: new-snapshot-test
spec:
volumeSnapshotClassName: csi-hostpath-snapclass
source:
persistentVolumeClaimName: pvc-test
你还可以从快照中提供卷。
这是一个绑定到快照的持久化卷声明:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: restore-pvc
spec:
storageClassName: csi-hostpath-sc
dataSource:
name: new-snapshot-test
kind: VolumeSnapshot
apiGroup: snapshot.storage.k8s.io
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
详情请参见github.com/kubernetes-csi/external-snapshotter#design。
CSI 卷克隆
卷克隆从 Kubernetes 1.18 版本起已进入 GA。卷克隆是新创建的卷,内容来自现有卷。一旦卷克隆完成,原始卷和克隆卷之间没有关联。它们的内容将随着时间的推移而分歧。你可以通过手动创建快照并从该快照创建新卷来执行克隆。但卷克隆更加简化和高效。
它仅适用于动态供应,并且克隆使用源卷的存储类。你可以通过指定一个现有的持久化卷声明作为新持久化卷声明的数据源来启动卷克隆。这将触发动态供应新卷,并克隆源声明的卷:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: clone-of-pvc-1
namespace: myns
spec:
accessModes:
- ReadWriteOnce
storageClassName: cloning
resources:
requests:
storage: 5Gi
dataSource:
kind: PersistentVolumeClaim
name: pvc-1
详情请参见kubernetes.io/docs/concepts/storage/volume-pvc-datasource/。
存储容量跟踪
存储容量跟踪(Kubernetes 1.24 版本起 GA)允许调度器将需要存储的 Pod 调度到能够提供该存储的节点。这需要支持存储容量跟踪的 CSI 驱动程序。
CSI 驱动程序将为每个存储类创建一个CSIStorageCapacity对象,并确定哪些节点可以访问该存储。此外,CSIDriverSpec字段StorageCapacity必须设置为 true。
当 Pod 在WaitForFirstConsumer模式下指定存储类名称,并且 CSI 驱动程序将StorageCapacity设置为 true 时,Kubernetes 调度器将考虑与存储类关联的 CSIStorageCapacity 对象,并将 Pod 调度到有足够存储的节点。
了解更多信息,请参见:kubernetes.io/docs/concepts/storage/storage-capacity。
卷健康监控
卷健康监控是存储 API 的最近添加功能,自 Kubernetes 1.21 起处于 Alpha 阶段。它涉及两个组件:
-
外部健康监控器
-
kubelet
支持卷健康监控的 CSI 驱动程序将更新 PVC,报告与关联存储卷的异常情况相关的事件。外部健康监控器还会监视节点故障,并将报告与这些节点绑定的 PVC 的事件。
在启用了 CSI 驱动程序的节点端卷健康监控的情况下,任何检测到的异常情况都会导致为每个使用具有相关问题的 PVC 的 pod 报告事件。
还有一个与卷健康相关的新指标:kubelet_volume_stats_health_status_abnormal。
它有两个标签:namespace 和 persistentvolumeclaim。其值为 0 或 1。
更多细节请见这里:kubernetes.io/docs/concepts/storage/volume-health-monitoring/。
CSI 是一个令人兴奋的计划,通过外部化存储驱动程序简化了 Kubernetes 代码库本身。它简化了存储解决方案的工作,使其能够开发树外驱动程序,并为 Kubernetes 存储系统增添了许多先进功能。
总结
在本章中,我们深入探讨了 Kubernetes 中的存储。我们研究了基于卷、声明和存储类的通用概念模型,以及卷插件的实现。Kubernetes 最终将所有存储系统映射为容器中挂载的文件系统或原始块存储设备。这种简单的模型使管理员能够配置并连接任何存储系统,从本地主机目录到基于云的共享存储,再到企业级存储系统。存储提供者从树内驱动程序转变为基于 CSI 的树外驱动程序,这对存储生态系统来说是个好兆头。你现在应该清楚了解 Kubernetes 中存储的建模和实现方式,并能够在你的 Kubernetes 集群中做出智能的存储实现选择。
在第七章,使用 Kubernetes 运行有状态应用程序中,我们将看到 Kubernetes 如何提升抽象级别,并在存储之上,帮助开发、部署和运行有状态应用程序,使用像有状态集这样的概念。
加入我们在 Discord 的社区!
与其他用户、云专家、作者和志同道合的专业人士一起阅读本书。
提出问题,向其他读者提供解决方案,通过问我任何问题的环节与作者聊天,还有更多内容。
扫描二维码或访问链接立即加入社区。

第七章:使用 Kubernetes 运行有状态应用
在本章中,我们将学习如何在 Kubernetes 上运行有状态应用。Kubernetes 自动处理许多工作,根据复杂的需求和配置(如命名空间、限制和配额),自动启动和重启集群节点上的 Pod。但是,当 Pods 运行存储感知型软件时,如数据库和队列,移动 Pod 可能会导致系统出现故障。
首先,我们将探讨有状态 Pod 的本质,以及为什么它们在 Kubernetes 中的管理更为复杂。我们将看一下几种管理复杂性的方式,比如共享环境变量和 DNS 记录。在某些情况下,冗余的内存状态、DaemonSet 或持久存储声明可以解决问题。Kubernetes 提供的主要有状态 Pod 解决方案是 StatefulSet(之前称为 PetSet)资源,它允许我们管理具有稳定属性的按索引排列的 Pod 集合。最后,我们将深入探讨在 Kubernetes 上运行 Cassandra 集群的完整示例。
本章将涵盖以下主要内容:
-
Kubernetes 中有状态与无状态应用
-
在 Kubernetes 中运行 Cassandra 集群
在本章结束时,你将理解 Kubernetes 中状态管理的挑战,深入了解在 Kubernetes 上运行 Cassandra 作为数据存储的具体示例,并能够确定适合你工作负载的状态管理策略。
Kubernetes 中有状态与无状态应用
无状态的 Kubernetes 应用是指该应用不在 Kubernetes 集群中管理其状态。所有状态存储在内存中或集群外部,集群中的容器以某种方式访问它。另一方面,有状态的 Kubernetes 应用有一个持久状态,并在集群中管理。在本节中,我们将学习为什么状态管理对分布式系统设计至关重要,以及在 Kubernetes 集群中管理状态的好处。
理解分布式数据密集型应用的本质
让我们从基础开始。分布式应用是运行在多台机器上的一组进程,这些进程处理输入,操作数据,暴露 API,并可能有其他副作用。每个进程都是其程序、运行时环境以及输入输出的组合。
你在学校写的程序接收命令行参数作为输入;也许它们读取一个文件或访问数据库,然后将结果写入屏幕、文件或数据库。一些程序将状态保存在内存中,并能够通过网络处理请求。简单的程序运行在单台机器上,并且可以将所有状态保存在内存中或从文件中读取。它们的运行时环境是操作系统。如果程序崩溃,用户必须手动重启它们。它们与机器绑定。
分布式应用程序是另一种情况。单台机器不足以快速处理所有数据或响应所有请求。单台机器无法容纳所有数据。需要处理的数据量太大,以至于无法以经济的方式下载到每台处理机器中。机器可能会发生故障,需要更换。所有处理机器都需要进行升级。用户可能分布在全球各地。
考虑到所有这些问题,很明显传统方法行不通。限制因素变成了数据。用户/客户端只能接收到汇总或处理过的数据。所有大规模的数据处理必须在离数据本身很近的地方完成,因为数据传输过慢且成本过高。相反,大部分处理代码必须在数据所在的数据中心和网络环境中运行。
为什么要在 Kubernetes 中管理状态?
在 Kubernetes 内部管理状态而不是在单独的集群中管理的主要原因是,Kubernetes 已经提供了监控、扩展、分配、安全和运营存储集群所需的许多基础设施。运行一个平行的存储集群将导致大量重复的工作。
为什么要在 Kubernetes 外部管理状态?
我们不能排除其他选择。在某些情况下,将状态管理在一个独立的非 Kubernetes 集群中可能更好,只要它共享相同的内部网络(数据的接近性胜过一切)。
一些有效的理由如下:
-
您已经有了一个独立的存储集群,并且不想轻易改变现状
-
您的存储集群被其他非 Kubernetes 应用程序使用
-
Kubernetes 对您的存储集群的支持不稳定或不够成熟
-
您可能希望逐步接近 Kubernetes 中的有状态应用程序,从独立的存储集群开始,随后与 Kubernetes 更紧密地集成
共享环境变量与 DNS 记录进行发现
Kubernetes 为跨集群的全局发现提供了多种机制。如果您的存储集群不是由 Kubernetes 管理的,您仍然需要告诉 Kubernetes 的 pod 如何找到它并访问它。
有两种常见方法:
-
DNS
-
环境变量
在某些情况下,您可能希望同时使用这两种方法,因为环境变量可以覆盖 DNS。
通过 DNS 访问外部数据存储
DNS 方法简单明了。假设您的外部存储集群是负载均衡的并且可以提供稳定的端点,那么 pod 只需直接访问该端点并连接到外部集群。
通过环境变量访问外部数据存储
另一种简单的方法是使用环境变量将连接信息传递给外部存储集群。Kubernetes 提供了 ConfigMap 资源,作为将配置与容器镜像分开的方式。配置是一组 key-value 对。配置可以通过两种方式暴露。一种方式是作为环境变量,另一种是作为挂载到容器中的配置文件。对于像密码这样的敏感连接信息,你可能更倾向于使用秘密管理工具(secrets)。
创建 ConfigMap
以下文件是一个 ConfigMap,它保存了地址列表:
apiVersion: v1
kind: ConfigMap
metadata:
name: db-config
data:
db-ip-addresses: 1.2.3.4,5.6.7.8
将其保存为 db-config-map.yaml 并运行:
$ k create -f db-config-map.yaml
configmap/db-config created
data 部分包含所有的 key-value 对,在这个例子中,只有一个键值对,键名为 db-ip-addresses。当在 Pod 中使用 ConfigMap 时,这一部分将变得很重要。你可以查看内容确认它是否正确:
$ k get configmap db-config -o yaml
apiVersion: v1
data:
db-ip-addresses: 1.2.3.4,5.6.7.8
kind: ConfigMap
metadata:
creationTimestamp: "2022-07-17T17:39:05Z"
name: db-config
namespace: default
resourceVersion: "504571"
uid: 11e49df0-ed1e-4bee-9fd7-bf38bb2aa38a
还有其他方法可以创建 ConfigMap。你可以直接使用 --from-value 或 --from-file 命令行参数来创建。
作为环境变量使用 ConfigMap
当你创建一个 Pod 时,可以指定一个 ConfigMap 并以多种方式使用其值。以下是如何将配置映射作为环境变量使用:
apiVersion: v1
kind: Pod
metadata:
name: some-pod
spec:
containers:
- name: some-container
image: busybox
command: ["/bin/sh", "-c", "env"]
env:
- name: DB_IP_ADDRESSES
valueFrom:
configMapKeyRef:
name: db-config
key: db-ip-addresses
restartPolicy: Never
这个 Pod 运行 busybox 最小化容器,并执行 env bash 命令,随后立即退出。来自 db-configmap 的 db-ip-addresses 键映射到 DB_IP_ADDRESSES 环境变量,并在日志中反映出来:
$ k create -f pod-with-db.yaml
pod/some-pod created
$ k logs some-pod | grep DB_IP
DB_IP_ADDRESSES=1.2.3.4,5.6.7.8
使用冗余的内存状态
在某些情况下,你可能希望将临时状态保存在内存中。分布式缓存是一个常见的例子。时间敏感的信息也是如此。对于这些用例,通常不需要持久存储,通过服务访问的多个 Pods 可能是一个合适的解决方案。
我们可以使用标准的 Kubernetes 技术,例如标签,来识别属于分布式缓存的 Pods,存储相同状态的冗余副本,并通过服务暴露它们。如果某个 Pod 失效,Kubernetes 会创建一个新的 Pod,直到它赶上进度,其他 Pod 将继续提供该状态。我们甚至可以使用 Pod 的反亲和性特性,确保维护相同状态冗余副本的 Pods 不会被调度到同一节点。
当然,你也可以使用像 Memcached 或 Redis 这样的工具。
使用 DaemonSet 实现冗余持久存储
一些有状态应用程序,如分布式数据库或队列,管理其状态冗余并自动同步节点(稍后我们会深入了解 Cassandra)。在这些情况下,确保 Pods 被调度到不同的节点上非常重要。同样,确保 Pods 被调度到具有特定硬件配置的节点上,或者专门分配给有状态应用程序,也是非常重要的。DaemonSet 特性非常适合这种用例。我们可以为一组节点打标签,确保有状态的 Pods 被逐一调度到选定的节点组上。
应用持久卷声明
如果有状态应用可以有效利用共享的持久存储,那么在每个 pod 中使用持久卷声明是最佳选择,正如我们在第六章《存储管理》中展示的那样。有状态应用将看到一个看起来像本地文件系统的挂载卷。
使用 StatefulSet
StatefulSets 专门设计用于支持分布式有状态应用程序,其中成员的身份非常重要,并且如果 pod 被重启,它必须在集合中保持其身份。它提供了有序的部署和扩展。与常规 pod 不同,StatefulSet的 pod 与持久存储相关联。
何时使用 StatefulSet
StatefulSets非常适合需要以下任何功能的应用程序:
-
一致且独特的网络标识符
-
持久且持久的存储
-
有条不紊且有组织的部署与扩展
-
系统化和有序的删除与终止
StatefulSet 的组件
有几个元素需要正确配置才能使StatefulSet正常工作:
-
一个负责管理
StatefulSetpod 网络身份的无头服务 -
StatefulSet本身及其副本数 -
节点上的本地存储或由管理员动态或手动配置的持久存储
这里是一个名为nginx的无头服务示例,将用于StatefulSet:
apiVersion: v1
kind: Service
metadata:
name: nginx
labels:
app: nginx
spec:
selector:
app: nginx
ports:
- port: 80
name: web
clusterIP: None
现在,StatefulSet清单文件将引用该服务:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: web
spec:
serviceName: "nginx"
replicas: 3
template:
metadata:
labels:
app: nginx
下一部分是 pod 模板,其中包括一个名为www的挂载卷:
spec:
terminationGracePeriodSeconds: 10
containers:
- name: nginx
image: gcr.io/google_containers/nginx-slim:0.8
ports:
- containerPort: 80
name: web
volumeMounts:
- name: www
mountPath: /usr/share/nginx/html
最后但同样重要的是,volumeClaimTemplates使用名为www的声明来匹配挂载卷。该声明请求 1 Gib 的存储并具有ReadWriteOnce访问权限:
volumeClaimTemplates:
- metadata:
name: www
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 1Gib
使用 StatefulSets
让我们创建nginx无头服务和statefulset:
k apply -f nginx-headless-service.yaml
service/nginx created
$ k apply -f nginx-stateful-set.yaml
statefulset.apps/nginx created
我们可以使用kubectl get all命令查看所有已创建的资源:
$ k get all
NAME READY STATUS RESTARTS AGE
pod/nginx-0 1/1 Running 0 107s
pod/nginx-1 1/1 Running 0 104s
pod/nginx-2 1/1 Running 0 102s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/nginx ClusterIP None <none> 80/TCP 2m5s
NAME READY AGE
statefulset.apps/nginx 3/3 107s
如预期所示,我们有一个包含三个副本的statefulset和无头服务。没有预设的是ReplicaSet,你会在创建 Deployment 时看到它。StatefulSets 直接管理它们的 pods。
请注意,kubectl get all实际上并不显示所有资源。StatefulSet 还会为每个 pod 创建一个由持久卷支持的持久卷声明。它们是:
$ k get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
www-nginx-0 Bound pvc-40ac1c62-bba0-4e3c-9177-eda7402755b3 10Mi RWO standard 1m37s
www-nginx-1 Bound pvc-94022a60-e4cb-4495-825d-eb744088266f 10Mi RWO standard 1m43s
www-nginx-2 Bound pvc-8c60523f-a3e8-4ae3-a91f-6aaa53b02848 10Mi RWO standard 1m52h
$ k get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pvc-40ac1c62-bba0-4e3c-9177-eda7402755b3 10Mi RWO Delete Bound default/www-nginx-0 standard 1m59s
pvc-8c60523f-a3e8-4ae3-a91f-6aaa53b02848 10Mi RWO Delete Bound default/www-nginx-2 standard 2m2s
pvc-94022a60-e4cb-4495-825d-eb744088266f 10Mi RWO Delete Bound default/www-nginx-1 standard 2m1s
如果我们删除一个 pod,StatefulSet 会创建一个新 pod 并将其绑定到相应的持久卷声明。pod nginx-1 被绑定到www-nginx-1 pvc:
$ k get po nginx-1 -o yaml | yq '.spec.volumes[0]'
name: www
persistentVolumeClaim:
claimName: www-nginx-1
让我们删除nginx-1 pod 并检查所有剩余的 pod:
$ k delete po nginx-1
pod "nginx-1" deleted
$ k get po
NAME READY STATUS RESTARTS AGE
nginx-0 1/1 Running 0 12m
nginx-1 1/1 Running 0 14s
nginx-2 1/1 Running 0 12m
如你所见,StatefulSet 立即用一个新的nginx-1 pod(14 秒前创建)替换了它。新 pod 被绑定到相同的持久卷声明:
$ k get po nginx-1 -o yaml | yq '.spec.volumes[0]'
name: www
persistentVolumeClaim:
claimName: www-nginx-1
当旧的nginx-1 pod 被删除时,持久卷声明及其背后的持久卷并没有被删除,你可以通过它们的创建时间看出这一点:
$ k get pvc www-nginx-1
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
www-nginx-1 Bound pvc-94022a60-e4cb-4495-825d-eb744088266f 10Mi RWO standard 143s
$ k get pv pvc-94022a60-e4cb-4495-825d-eb744088266f
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pvc-94022a60-e4cb-4495-825d-eb744088266f 10Mi RWO Delete Bound default/www-nginx-1 standard 2m1s
这意味着即使 Pods 来来去去,StatefulSet 的状态也会被保持。每个通过其索引标识的 Pod 始终绑定到某个特定的状态分片,并由相应的持久卷声明备份。
到此为止,我们已经理解了 StatefulSets 的概念以及如何使用它们。让我们深入研究如何实现一个工业级数据存储,并看看它如何作为 StatefulSet 部署在 Kubernetes 中。
在 Kubernetes 中运行 Cassandra 集群
在本节中,我们将详细探讨如何配置一个非常大的 Cassandra 集群在 Kubernetes 集群中运行。我将分析并为一些有趣的部分提供背景。如果你想进一步探索完整的示例,可以在这里访问:
kubernetes.io/docs/tutorials/stateful-application/cassandra
这里的目标是了解在 Kubernetes 上运行一个真实世界的有状态工作负载需要什么,以及 StatefulSets 如何提供帮助。如果你没有理解每个细节也不用担心。
首先,我们将了解一些关于 Cassandra 的知识及其独特性,然后按照一步一步的流程,使用我们在前一部分中讨论的几种技术和策略将其运行起来。
Cassandra 简介
Cassandra 是一个分布式列式数据存储系统。它从一开始就为大数据而设计。Cassandra 具有快速、强大(没有单点故障)、高可用和线性可扩展的特点。它还支持多数据中心。它通过专注于特定功能并精心设计支持的特性,以及同样重要的是不支持的特性,来实现这一切。
在前一家公司,我运行了一个 Kubernetes 集群,使用 Cassandra 作为传感器数据的主要数据存储(大约 100 TB)。Cassandra 根据 分布式哈希表(DHT)算法将数据分配到一组节点(节点环)中。
集群节点通过 gossip 协议相互通信,并快速了解集群的整体状态(哪些节点加入、哪些节点离开或不可用)。Cassandra 会不断压缩数据并平衡集群。数据通常会被多次复制以保证冗余、稳健性和高可用性。
从开发者的角度来看,Cassandra 非常适合时间序列数据,并提供一个灵活的模型,允许在每个查询中指定一致性级别。它还是幂等的(这是分布式数据库中非常重要的特性),这意味着允许重复插入或更新操作。
下面是一个图示,展示了 Cassandra 集群的组织方式,客户端如何访问任意节点,以及请求将如何自动转发到具有请求数据的节点:

图 7.1:请求与 Cassandra 集群的交互
Cassandra Docker 镜像
在 Kubernetes 上部署 Cassandra 与在独立 Cassandra 集群中部署不同,需要使用特定的 Docker 镜像。这是一个重要的步骤,因为这意味着我们可以使用 Kubernetes 来跟踪我们的 Cassandra pods。镜像的 Dockerfile 可以在这里找到:github.com/kubernetes/examples/blob/master/cassandra/image/Dockerfile。
下面是构建 Cassandra 镜像的 Dockerfile。基础镜像是为容器使用设计的 Debian 版本(见 github.com/kubernetes/kubernetes/tree/master/build/debian-base)。
Cassandra Dockerfile 定义了一些构建参数,这些参数在镜像构建时必须设置,创建了一些标签,定义了许多环境变量,将所有文件添加到容器内的根目录,运行 build.sh 脚本,声明 Cassandra 数据卷(数据存储位置),暴露了一些端口,最后使用 dumb-init 执行 run.sh 脚本:
FROM k8s.gcr.io/debian-base-amd64:0.3
ARG BUILD_DATE
ARG VCS_REF
ARG CASSANDRA_VERSION
ARG DEV_CONTAINER
LABEL \
org.label-schema.build-date=$BUILD_DATE \
org.label-schema.docker.dockerfile="/Dockerfile" \
org.label-schema.license="Apache License 2.0" \
org.label-schema.name="k8s-for-greeks/docker-cassandra-k8s" \
org.label-schema.url="https://github.com/k8s-for-greeks/" \
org.label-schema.vcs-ref=$VCS_REF \
org.label-schema.vcs-type="Git" \
org.label-schema.vcs-url="https://github.com/k8s-for-greeks/docker-cassandra-k8s"
ENV CASSANDRA_HOME=/usr/local/apache-cassandra-${CASSANDRA_VERSION} \
CASSANDRA_CONF=/etc/cassandra \
CASSANDRA_DATA=/cassandra_data \
CASSANDRA_LOGS=/var/log/cassandra \
JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64 \
PATH=${PATH}:/usr/lib/jvm/java-8-openjdk-amd64/bin:/usr/local/apache-cassandra-${CASSANDRA_VERSION}/bin
ADD files /
RUN clean-install bash \
&& /build.sh \
&& rm /build.sh
VOLUME ["/$CASSANDRA_DATA"]
# 7000: intra-node communication
# 7001: TLS intra-node communication
# 7199: JMX
# 9042: CQL
# 9160: thrift service
EXPOSE 7000 7001 7199 9042 9160
CMD ["/usr/bin/dumb-init", "/bin/bash", "/run.sh"]
以下是 Dockerfile 使用的所有文件:
build.sh
cassandra-seed.h
cassandra.yaml
jvm.options
kubernetes-cassandra.jar
logback.xml
ready-probe.sh
run.sh
我们不会覆盖所有内容,而是专注于几个有趣的脚本:build.sh 和 run.sh 脚本。
探索 build.sh 脚本
Cassandra 是一个 Java 程序。构建脚本安装 Java 运行时环境和一些必要的库与工具。然后设置一些稍后将使用的变量,如 CASSANDRA_PATH。
它从 Apache 组织下载正确版本的 Cassandra(Cassandra 是一个 Apache 开源项目),创建了 /cassandra_data/data 目录以存储 Cassandra 的 SSTables 和 /etc/cassandra 配置目录,复制文件到配置目录,添加 Cassandra 用户,设置就绪探针,安装 Python,将 Cassandra JAR 文件和种子共享库移动到目标位置,然后清理在此过程中生成的所有中间文件:
...
clean-install \
openjdk-8-jre-headless \
libjemalloc1 \
localepurge \
dumb-init \
wget
CASSANDRA_PATH="cassandra/${CASSANDRA_VERSION}/apache-cassandra-${CASSANDRA_VERSION}-bin.tar.gz"
CASSANDRA_DOWNLOAD="http://www.apache.org/dyn/closer.cgi?path=/${CASSANDRA_PATH}&as_json=1"
CASSANDRA_MIRROR=`wget -q -O - ${CASSANDRA_DOWNLOAD} | grep -oP "(?<=\"preferred\": \")[^\"]+"`
echo "Downloading Apache Cassandra from $CASSANDRA_MIRROR$CASSANDRA_PATH..."
wget -q -O - $CASSANDRA_MIRROR$CASSANDRA_PATH \
| tar -xzf - -C /usr/local
mkdir -p /cassandra_data/data
mkdir -p /etc/cassandra
mv /logback.xml /cassandra.yaml /jvm.options /etc/cassandra/
mv /usr/local/apache-cassandra-${CASSANDRA_VERSION}/conf/cassandra-env.sh /etc/cassandra/
adduser --disabled-password --no-create-home --gecos '' --disabled-login cassandra
chmod +x /ready-probe.sh
chown cassandra: /ready-probe.sh
DEV_IMAGE=${DEV_CONTAINER:-}
if [ ! -z "$DEV_IMAGE" ]; then
clean-install python;
else
rm -rf $CASSANDRA_HOME/pylib;
fi
mv /kubernetes-cassandra.jar /usr/local/apache-cassandra-${CASSANDRA_VERSION}/lib
mv /cassandra-seed.so /etc/cassandra/
mv /cassandra-seed.h /usr/local/lib/include
apt-get -y purge localepurge
apt-get -y autoremove
apt-get clean
rm -rf <many files>
探索 run.sh 脚本
run.sh 脚本需要一些 shell 技能和对 Cassandra 的了解才能理解,但这值得付出努力。首先,设置一些本地变量用于 Cassandra 配置文件 /etc/cassandra/cassandra.yaml。CASSANDRA_CFG 变量将在脚本的其余部分使用:
set -e
CASSANDRA_CONF_DIR=/etc/cassandra
CASSANDRA_CFG=$CASSANDRA_CONF_DIR/cassandra.yaml
如果没有指定 CASSANDRA_SEEDS,则设置 HOSTNAME,稍后 StatefulSet 将使用该变量:
# we are doing StatefulSet or just setting our seeds
if [ -z "$CASSANDRA_SEEDS" ]; then
HOSTNAME=$(hostname -f)
CASSANDRA_SEEDS=$(hostname -f)
fi
然后是一个包含默认值的环境变量长列表。语法 ${VAR_NAME:-} 使用环境变量 VAR_NAME,如果已定义,或者使用默认值。
一个类似的语法 ${VAR_NAME:=} 做了同样的事情,但如果环境变量未定义,还会给环境变量赋予默认值。这是一个微妙但重要的区别。
这里使用了两种变体:
# The following vars relate to their counter parts in $CASSANDRA_CFG
# for instance rpc_address
CASSANDRA_RPC_ADDRESS="${CASSANDRA_RPC_ADDRESS:-0.0.0.0}"
CASSANDRA_NUM_TOKENS="${CASSANDRA_NUM_TOKENS:-32}"
CASSANDRA_CLUSTER_NAME="${CASSANDRA_CLUSTER_NAME:='Test Cluster'}"
CASSANDRA_LISTEN_ADDRESS=${POD_IP:-$HOSTNAME}
CASSANDRA_BROADCAST_ADDRESS=${POD_IP:-$HOSTNAME}
CASSANDRA_BROADCAST_RPC_ADDRESS=${POD_IP:-$HOSTNAME}
CASSANDRA_DISK_OPTIMIZATION_STRATEGY="${CASSANDRA_DISK_OPTIMIZATION_STRATEGY:-ssd}"
CASSANDRA_MIGRATION_WAIT="${CASSANDRA_MIGRATION_WAIT:-1}"
CASSANDRA_ENDPOINT_SNITCH="${CASSANDRA_ENDPOINT_SNITCH:-SimpleSnitch}"
CASSANDRA_DC="${CASSANDRA_DC}"
CASSANDRA_RACK="${CASSANDRA_RACK}"
CASSANDRA_RING_DELAY="${CASSANDRA_RING_DELAY:-30000}"
CASSANDRA_AUTO_BOOTSTRAP="${CASSANDRA_AUTO_BOOTSTRAP:-true}"
CASSANDRA_SEEDS="${CASSANDRA_SEEDS:false}"
CASSANDRA_SEED_PROVIDER="${CASSANDRA_SEED_PROVIDER:-org.apache.cassandra.locator.SimpleSeedProvider}"
CASSANDRA_AUTO_BOOTSTRAP="${CASSANDRA_AUTO_BOOTSTRAP:false}"
顺便说一句,我为 Kubernetes 做出了贡献,通过提交 PR 修复了一个小错误。请参见 github.com/kubernetes/examples/pull/348。
下一部分配置了监控 JMX 并控制垃圾回收输出:
# Turn off JMX auth
CASSANDRA_OPEN_JMX="${CASSANDRA_OPEN_JMX:-false}"
# send GC to STDOUT
CASSANDRA_GC_STDOUT="${CASSANDRA_GC_STDOUT:-false}"
接下来是一个部分,所有变量都会在屏幕上打印出来。我们跳过其中的大部分:
echo Starting Cassandra on ${CASSANDRA_LISTEN_ADDRESS}
echo CASSANDRA_CONF_DIR ${CASSANDRA_CONF_DIR}
echo CASSANDRA_CFG ${CASSANDRA_CFG}
echo CASSANDRA_AUTO_BOOTSTRAP ${CASSANDRA_AUTO_BOOTSTRAP}
...
下一部分非常重要。默认情况下,Cassandra 使用一个简单的 Snitch,它不了解机架和数据中心。当集群跨多个数据中心和机架时,这种配置并不理想。
Cassandra 支持机架和数据中心的感知,并可以优化冗余和高可用性,同时合理限制跨数据中心的通信:
# if DC and RACK are set, use GossipingPropertyFileSnitch
if [[ $CASSANDRA_DC && $CASSANDRA_RACK ]]; then
echo "dc=$CASSANDRA_DC" > $CASSANDRA_CONF_DIR/cassandra-rackdc.properties
echo "rack=$CASSANDRA_RACK" >> $CASSANDRA_CONF_DIR/cassandra-rackdc.properties
CASSANDRA_ENDPOINT_SNITCH="GossipingPropertyFileSnitch"
fi
内存管理也很重要,您可以控制最大堆内存大小,确保 Cassandra 不会开始抖动并将数据交换到磁盘:
if [ -n "$CASSANDRA_MAX_HEAP" ]; then
sed -ri "s/^(#)?-Xmx[0-9]+.*/-Xmx$CASSANDRA_MAX_HEAP/" "$CASSANDRA_CONF_DIR/jvm.options"
sed -ri "s/^(#)?-Xms[0-9]+.*/-Xms$CASSANDRA_MAX_HEAP/" "$CASSANDRA_CONF_DIR/jvm.options"
fi
if [ -n "$CASSANDRA_REPLACE_NODE" ]; then
echo "-Dcassandra.replace_address=$CASSANDRA_REPLACE_NODE/" >> "$CASSANDRA_CONF_DIR/jvm.options"
fi
机架和数据中心信息存储在一个简单的 Java propertiesfile 中:
for rackdc in dc rack; do
var="CASSANDRA_${rackdc^^}"
val="${!var}"
if [ "$val" ]; then
sed -ri 's/^('"$rackdc"'=).*/\1 '"$val"'/' "$CASSANDRA_CONF_DIR/cassandra-rackdc.properties"
fi
done
下一部分会遍历之前定义的所有变量,找到对应的 Cassandra.yaml 配置文件中的键,并覆盖它们。这确保了每个配置文件在启动 Cassandra 之前会动态定制:
for yaml in \
broadcast_address \
broadcast_rpc_address \
cluster_name \
disk_optimization_strategy \
endpoint_snitch \
listen_address \
num_tokens \
rpc_address \
start_rpc \
key_cache_size_in_mb \
concurrent_reads \
concurrent_writes \
memtable_cleanup_threshold \
memtable_allocation_type \
memtable_flush_writers \
concurrent_compactors \
compaction_throughput_mb_per_sec \
counter_cache_size_in_mb \
internode_compression \
endpoint_snitch \
gc_warn_threshold_in_ms \
listen_interface \
rpc_interface \
; do
var="CASSANDRA_${yaml^^}"
val="${!var}"
if [ "$val" ]; then
sed -ri 's/^(# )?('"$yaml"':).*/\2 '"$val"'/' "$CASSANDRA_CFG"
fi
done
echo "auto_bootstrap: ${CASSANDRA_AUTO_BOOTSTRAP}" >> $CASSANDRA_CFG
下一部分主要是设置种子或种子提供者,这取决于部署解决方案(是否使用 StatefulSet)。对于第一个 pod 启动为其自身种子有一个小技巧:
# set the seed to itself. This is only for the first pod, otherwise
# it will be able to get seeds from the seed provider
if [[ $CASSANDRA_SEEDS == 'false' ]]; then
sed -ri 's/- seeds:.*/- seeds: "'"$POD_IP"'"/' $CASSANDRA_CFG
else # if we have seeds set them. Probably StatefulSet
sed -ri 's/- seeds:.*/- seeds: "'"$CASSANDRA_SEEDS"'"/' $CASSANDRA_CFG
fi
sed -ri 's/- class_name: SEED_PROVIDER/- class_name: '"$CASSANDRA_SEED_PROVIDER"'/' $CASSANDRA_CFG
以下部分设置了远程管理和 JMX 监控的各种选项。在复杂的分布式系统中,拥有合适的管理工具至关重要。Cassandra 对广泛使用的JMX标准有深度支持:
# send gc to stdout
if [[ $CASSANDRA_GC_STDOUT == 'true' ]]; then
sed -ri 's/ -Xloggc:\/var\/log\/cassandra\/gc\.log//' $CASSANDRA_CONF_DIR/cassandra-env.sh
fi
# enable RMI and JMX to work on one port
echo "JVM_OPTS=\"\$JVM_OPTS -Djava.rmi.server.hostname=$POD_IP\"" >> $CASSANDRA_CONF_DIR/cassandra-env.sh
# getting WARNING messages with Migration Service
echo "-Dcassandra.migration_task_wait_in_seconds=${CASSANDRA_MIGRATION_WAIT}" >> $CASSANDRA_CONF_DIR/jvm.options
echo "-Dcassandra.ring_delay_ms=${CASSANDRA_RING_DELAY}" >> $CASSANDRA_CONF_DIR/jvm.options
if [[ $CASSANDRA_OPEN_JMX == 'true' ]]; then
export LOCAL_JMX=no
sed -ri 's/ -Dcom\.sun\.management\.jmxremote\.authenticate=true/ -Dcom\.sun\.management\.jmxremote\.authenticate=false/' $CASSANDRA_CONF_DIR/cassandra-env.sh
sed -ri 's/ -Dcom\.sun\.management\.jmxremote\.password\.file=\/etc\/cassandra\/jmxremote\.password//' $CASSANDRA_CONF_DIR/cassandra-env.sh
fi
最后,它保护数据目录,只有 cassandra 用户可以访问,CLASSPATH 设置为 Cassandra 的 JAR 文件,并且以 cassandra 用户的身份在前台启动 Cassandra(而非作为守护进程):
chmod 700 "${CASSANDRA_DATA}"
chown -c -R cassandra "${CASSANDRA_DATA}" "${CASSANDRA_CONF_DIR}"
export CLASSPATH=/kubernetes-cassandra.jar
su cassandra -c "$CASSANDRA_HOME/bin/cassandra -f"
连接 Kubernetes 和 Cassandra
连接 Kubernetes 和 Cassandra 需要一些工作,因为 Cassandra 设计为非常自给自足,但我们希望它能在适当的时机接入 Kubernetes,提供诸如自动重启失败节点、监控、分配 Cassandra pod 以及提供 Cassandra pod 与其他 pod 并排显示的统一视图等功能。
Cassandra 是一个复杂的系统,具有许多控制选项。它随附一个 Cassandra.yaml 配置文件,您可以通过环境变量覆盖所有选项。
深入了解 Cassandra 配置文件
有两个特别相关的设置:种子提供者和 snitch。种子提供者负责发布集群中节点的 IP 地址列表(种子)。每个启动的节点都会连接到这些种子(通常至少有三个),如果成功连接其中一个,它们会立即交换集群中所有节点的信息。随着节点之间的相互传播,这些信息会不断更新。
在Cassandra.yaml中配置的默认种子提供者只是一个静态 IP 地址列表,在此情况下,仅为回环接口:
# any class that implements the SeedProvider interface and has a
# constructor that takes a Map<String, String> of parameters will do.
seed_provider:
# Addresses of hosts that are deemed contact points.
# Cassandra nodes use this list of hosts to find each other and learn
# the topology of the ring. You must change this if you are running
# multiple nodes!
#- class_name: io.k8s.cassandra.KubernetesSeedProvider
- class_name: SEED_PROVIDER
parameters:
# seeds is actually a comma-delimited list of addresses.
# Ex: "<ip1>,<ip2>,<ip3>"
- seeds: "127.0.0.1"
另一个重要的设置是 snitch。它有两个作用:
-
Cassandra 利用 snitch 来深入了解网络拓扑,从而有效地路由请求。
-
Cassandra 利用这些知识来有策略地在集群中分配副本,降低相关故障的风险。为此,Cassandra 将机器组织到数据中心和机架中,确保副本不会集中在同一个机架上,即使这不一定与物理位置相关。
Cassandra 预装了几种 snitch 类,但没有一个是 Kubernetes 感知的。默认是SimpleSnitch,但可以被重写:
# You can use a custom Snitch by setting this to the full class
# name of the snitch, which will be assumed to be on your classpath.
endpoint_snitch: SimpleSnitch
其他的 snitch 有:
-
GossipingPropertyFileSnitch -
PropertyFileSnitch -
Ec2Snitch -
Ec2MultiRegionSnitch -
RackInferringSnitch
自定义种子提供者
当在 Kubernetes 中将 Cassandra 节点作为 Pod 运行时,Kubernetes 可能会移动 Pod,包括种子节点。为此,Cassandra 种子提供者需要与 Kubernetes API 服务器进行交互。
KubernetesSeedProvider (a Java class that implements the Cassandra SeedProvider API):
public class KubernetesSeedProvider implements SeedProvider {
...
/**
* Call Kubernetes API to collect a list of seed providers
*
* @return list of seed providers
*/
public List<InetAddress> getSeeds() {
GoInterface go = (GoInterface) Native.loadLibrary("cassandra-seed.so", GoInterface.class);
String service = getEnvOrDefault("CASSANDRA_SERVICE", "cassandra");
String namespace = getEnvOrDefault("POD_NAMESPACE", "default");
String initialSeeds = getEnvOrDefault("CASSANDRA_SEEDS", "");
if ("".equals(initialSeeds)) {
initialSeeds = getEnvOrDefault("POD_IP", "");
}
String seedSizeVar = getEnvOrDefault("CASSANDRA_SERVICE_NUM_SEEDS", "8");
Integer seedSize = Integer.valueOf(seedSizeVar);
String data = go.GetEndpoints(namespace, service, initialSeeds);
ObjectMapper mapper = new ObjectMapper();
try {
Endpoints endpoints = mapper.readValue(data, Endpoints.class);
logger.info("cassandra seeds: {}", endpoints.ips.toString());
return Collections.unmodifiableList(endpoints.ips);
} catch (IOException e) {
// This should not happen
logger.error("unexpected error building cassandra seeds: {}" , e.getMessage());
return Collections.emptyList();
}
}
创建 Cassandra 无头服务
无头服务的作用是允许 Kubernetes 集群中的客户端通过标准 Kubernetes 服务连接到 Cassandra 集群,而不必跟踪节点的网络身份或在所有节点前面设置专用负载均衡器。Kubernetes 通过其服务原生提供了这一切。
这是Service清单:
apiVersion: v1
kind: Service
metadata:
labels:
app: cassandra
name: cassandra
spec:
clusterIP: None
ports:
- port: 9042
selector:
app: Cassandra
app: cassandra标签将所有参与服务的 Pod 进行分组。Kubernetes 会创建端点记录,DNS 会返回用于发现的记录。clusterIP为None,这意味着该服务是无头的,Kubernetes 不会进行任何负载均衡或代理。这一点很重要,因为 Cassandra 节点会直接进行通信。
9042端口被 Cassandra 用于提供 CQL 请求。这些请求可以是查询、插入/更新(在 Cassandra 中总是 upsert)或删除。
使用 StatefulSet 创建 Cassandra 集群
声明一个StatefulSet并非易事,它可以说是最复杂的 Kubernetes 资源之一。它包含许多组成部分:标准元数据、StatefulSet 规范、Pod 模板(通常本身也相当复杂)和卷声明模板。
解剖 StatefulSet YAML 文件
让我们有条理地回顾一下这个声明了三节点 Cassandra 集群的 StatefulSet YAML 文件示例。
这是基本的元数据。请注意,apiVersion 字符串为 apps/v1(StatefulSet 在 Kubernetes 1.9 中正式发布):
apiVersion: "apps/v1"
kind: StatefulSet
metadata:
name: cassandra
labels:
app: cassandra
StatefulSet 规格定义了无头服务名称、标签选择器(app: cassandra)、StatefulSet 中的 Pod 数量以及 Pod 模板(稍后解释)。replicas 字段指定了 StatefulSet 中的 Pod 数量:
spec:
serviceName: cassandra
replicas: 3
selector:
matchLabels:
app: cassandra
template:
...
Pod 的“replicas”这个术语是一个不太恰当的选择,因为这些 Pod 不是彼此的副本。它们共享相同的 Pod 模板,但每个 Pod 都有一个独特的身份,并且通常负责不同的状态子集。对于 Cassandra 来说,情况更为混淆,因为它使用同样的术语“replicas”来指代那些冗余复制某些状态子集的节点组(但它们并不完全相同,因为每个节点还可以管理额外的状态)。
我在 Kubernetes 项目中开了一个 GitHub 问题,建议将“replicas”一词更改为“members”:
github.com/kubernetes/kubernetes.github.io/issues/2103
Pod 模板包含了基于自定义 Cassandra 镜像的单一容器。它还将终止宽限期设置为 30 分钟。这意味着当 Kubernetes 需要终止 Pod 时,它会向容器发送 SIGTERM 信号,通知它们应退出,并给予它们优雅退出的机会。任何在宽限期后仍在运行的容器将通过 SIGKILL 被强制终止。
这是带有 app: cassandra 标签的 Pod 模板:
template:
metadata:
labels:
app: cassandra
spec:
terminationGracePeriodSeconds: 1800
containers:
...
containers 部分包含多个重要部分。它以名称和我们之前查看的镜像开始:
containers:
- name: cassandra
image: gcr.io/google-samples/cassandra:v14
imagePullPolicy: Always
然后,它定义了 Cassandra 节点所需的多个容器端口,用于外部和内部通信:
ports:
- containerPort: 7000
name: intra-node
- containerPort: 7001
name: tls-intra-node
- containerPort: 7199
name: jmx
- containerPort: 9042
name: cql
resources 部分指定了容器所需的 CPU 和内存。这一点至关重要,因为存储管理层绝不应该因为 CPU 或内存的原因成为性能瓶颈。请注意,它遵循了请求和限制一致的最佳实践,以确保资源在分配后始终可用:
resources:
limits:
cpu: "500m"
memory: 1Gi
requests:
cpu: "500m"
memory: 1Gi
Cassandra 需要访问进程间通信(IPC),容器通过安全上下文的能力来请求这一访问权限:
securityContext:
capabilities:
add:
- IPC_LOCK
lifecycle 部分运行 Cassandra 的 nodetool drain 命令,以确保当容器需要关闭时,节点上的数据会转移到 Cassandra 集群中的其他节点。这也是为什么需要 30 分钟宽限期的原因。节点排空涉及大量数据转移:
lifecycle:
preStop:
exec:
command:
- /bin/sh
- -c
- nodetool drain
env 部分指定了容器内可用的环境变量。以下是必要变量的部分列表。CASSANDRA_SEEDS 变量设置为无头服务,这样 Cassandra 节点就可以在启动时与种子节点通信并发现整个集群。请注意,在此配置中,我们没有使用特殊的 Kubernetes 种子提供程序。POD_IP 很有趣,因为它利用了向下 API,通过字段引用 status.podIP 来填充其值:
env:
- name: MAX_HEAP_SIZE
value: 512M
- name: HEAP_NEWSIZE
value: 100M
- name: CASSANDRA_SEEDS
value: "cassandra-0.cassandra.default.svc.cluster.local"
- name: CASSANDRA_CLUSTER_NAME
value: "K8Demo"
- name: CASSANDRA_DC
value: "DC1-K8Demo"
- name: CASSANDRA_RACK
value: "Rack1-K8Demo"
- name: CASSANDRA_SEED_PROVIDER
value: io.k8s.cassandra.KubernetesSeedProvider
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
就绪探针确保在节点实际准备好处理请求之前,不会将请求发送到该节点。ready-probe.sh 脚本使用 Cassandra 的 nodetool status 命令:
readinessProbe:
exec:
command:
- /bin/bash
- -c
- /ready-probe.sh
initialDelaySeconds: 15
timeoutSeconds: 5
容器配置的最后一部分是卷挂载,必须与持久化卷声明匹配:
volumeMounts:
- name: cassandra-data
mountPath: /var/lib/cassandra
这就是容器配置的全部内容。最后一部分是卷声明模板。在这种情况下,使用的是动态供应。强烈建议为 Cassandra 存储使用 SSD 驱动,特别是其日志。此示例中请求的存储为 1 GiB。我通过实验发现,单个 Cassandra 节点的理想存储大小是 1 到 2 TB。原因是 Cassandra 在后台会做很多数据重排、压缩和重新平衡。如果一个节点离开集群或一个新节点加入集群,你必须等到数据被正确重新平衡后,才能正确地重新分配离开节点的数据,或者新节点会填充数据。
请注意,Cassandra 需要大量的磁盘空间来进行所有这些数据重排。建议保持 50% 的磁盘空间空闲。当你考虑到还需要复制(通常是 3 倍)时,所需的存储空间可能是数据大小的 6 倍。如果你敢于冒险,可能只使用 2 倍的复制,并且保持 30% 空闲空间,具体取决于你的使用情况。但即便是在单节点上,也不要让空闲磁盘空间低于 10%。我通过实际经验了解到,Cassandra 会卡住,无法压缩和重新平衡这种节点,除非采取极端措施。
在这种情况下,必须定义存储类 fast。通常对于 Cassandra,你需要一个特殊的存储类,而不能使用 Kubernetes 集群默认的存储类。
访问模式当然是 ReadWriteOnce:
volumeClaimTemplates:
- metadata:
name: cassandra-data
spec:
storageClassName: fast
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 1Gi
部署 StatefulSet 时,Kubernetes 按照索引号顺序创建 pod。在扩展或缩减时,也会按照顺序进行。对于 Cassandra 来说,这并不重要,因为它可以处理节点按任意顺序加入或离开集群。当一个 Cassandra pod 被销毁(不优雅地销毁时),持久化卷仍然存在。如果稍后创建具有相同索引的 pod,原始持久化卷将会被挂载到它上面。这种特定 pod 和其存储之间的稳定连接,使得 Cassandra 能够正确管理状态。
总结
在本章中,我们讨论了有状态应用程序以及如何将它们与 Kubernetes 集成。我们发现有状态应用程序非常复杂,并考虑了几种发现机制,如 DNS 和环境变量。我们还讨论了几种状态管理解决方案,如内存冗余存储、本地存储和持久化存储。本章的重点围绕着如何通过 StatefulSet 在 Kubernetes 集群中部署 Cassandra 集群。我们深入探讨了低级细节,以便更好地理解将像 Cassandra 这样复杂的第三方分布式系统集成到 Kubernetes 中究竟需要什么。到此为止,您应该对有状态应用程序以及如何在基于 Kubernetes 的系统中应用它们有了透彻的理解。您已经掌握了多种方法,适用于不同的使用场景,也许您还学到了一些关于 Cassandra 的知识。
在下一章中,我们将继续我们的旅程,探索一个重要话题——可扩展性,特别是自动扩展性,以及如何在集群动态增长时进行部署和实时升级更新。这些问题非常复杂,尤其是当集群中运行着有状态应用程序时。
第八章:部署与更新应用
在本章中,我们将探讨 Kubernetes 提供的自动化 Pod 可扩展性,了解它如何影响滚动更新,以及如何与配额互动。我们将涉及重要的资源配置话题,讨论如何选择和管理集群的规模。最后,我们将讨论 CI/CD 管道和基础设施配置。以下是我们将要覆盖的主要内容:
-
实时集群更新
-
水平 Pod 自动扩展
-
执行带自动扩展的滚动更新
-
使用配额和限制处理稀缺资源
-
持续集成与部署
-
使用 Terraform、Pulumi、自定义操作符和 Crossplane 配置基础设施
到本章结束时,您将具备规划大规模集群的能力,能够经济地配置集群,并在性能、成本和可用性之间做出明智的取舍决策。您还将了解如何设置水平 Pod 自动扩展,并智能地使用资源配额,让 Kubernetes 自动处理流量波动,同时安全地将软件部署到您的集群中。
实时集群更新
运行 Kubernetes 集群时,最复杂和最具风险的任务之一就是实时升级。当系统的不同部分使用不同版本时,它们之间的交互往往很难预测,但在许多情况下这是必需的。对于大型集群和众多用户来说,无法承受因维护而导致的停机时间。应对复杂性的最佳方法是分而治之。微服务架构在这方面非常有帮助。你永远不会升级整个系统,而是不断升级几组相关的微服务。如果 API 发生了变化,那么就需要同时升级它们的客户端。一个合理设计的升级会至少在所有客户端都升级之前保持向后兼容,然后在多个版本发布后弃用旧的 API。
在本节中,我们将讨论如何通过各种策略更新集群,如滚动更新、蓝绿部署和金丝雀部署。我们还将讨论何时适合引入破坏性升级与向后兼容的升级。接着,我们将进入架构和数据迁移这一关键话题。
滚动更新
滚动更新是指逐步将组件从当前版本更新到下一个版本。这意味着您的集群将同时运行当前和新版本的组件。这里有两种情况需要考虑:
-
新组件是向后兼容的
-
新组件不是向后兼容的
如果新组件是向后兼容的,那么升级应该非常容易。在 Kubernetes 的早期版本中,您必须非常小心地管理滚动更新,使用标签并逐步更改旧版本和新版本的副本数量(尽管 kubectl rolling-update 是复制控制器的便捷快捷方式)。但是,Kubernetes 1.2 中引入的 Deployment 资源使这一过程变得更加简单,并且支持副本集。它内置了以下功能:
-
运行在服务器端(即使您的机器断开连接,它也会继续运行)
-
版本控制
-
多个并发的滚动更新
-
更新部署
-
聚合所有 Pod 的状态
-
回滚
-
金丝雀部署
-
多种升级策略(滚动升级是默认的)
这是一个部署清单示例,用于部署三个 Nginx Pod:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.7.9
ports:
- containerPort: 80
资源类型是 Deployment,它的名字是 nginx-deployment,您可以稍后用它来引用这个部署(例如,用于更新或回滚)。最重要的部分当然是 spec,它包含了一个 Pod 模板。副本数决定了集群中将有多少个 Pod,模板规范中包含了每个容器的配置。在此例中,只有一个容器。
要启动滚动更新,请创建部署资源并检查它是否成功推出:
$ k create -f nginx-deployment.yaml
deployment.apps/nginx-deployment created
$ k rollout status deployment/nginx-deployment
deployment "nginx-deployment" successfully rolled out
Deployments have an update strategy, which defaults to rollingUpdate:
$ k get deployment nginx-deployment -o yaml | grep strategy -A 4
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
type: RollingUpdate
下图展示了滚动更新的工作原理:

图 8.1:Kubernetes 滚动更新
复杂的部署
当你只想升级一个 Pod 时,Deployment 资源非常好用,但你可能经常需要升级多个 Pod,而这些 Pod 有时会有版本间的依赖关系。在这种情况下,您有时必须放弃滚动更新或引入一个临时的兼容层。
例如,假设服务 A 依赖于服务 B。服务 B 现在有一个破坏性变更。服务 A 的 v1 Pod 无法与服务 B 的 v2 Pod 互操作。从可靠性和变更管理的角度来看,让服务 B 的 v2 Pod 支持旧的和新的 API 也是不理想的。在这种情况下,解决方案可能是引入一个适配器服务,它实现了服务 B 的 v1 API。这个服务将位于 A 和 B 之间,并在版本之间翻译请求和响应。
这增加了部署过程的复杂性,并需要几个步骤,但好处是 A 和 B 服务本身是简单的。您可以跨不兼容的版本进行滚动更新,并且一旦每个人都升级到 v2(所有 A Pod 和所有 B Pod),所有的间接操作都可以消失。
但是,滚动更新并不总是解决方案。
蓝绿部署
滚动更新对可用性非常有利,但有时管理正确的滚动更新可能被认为过于复杂,或者增加了大量工作量,从而推迟了更重要的项目。在这些情况下,蓝绿部署提供了一个很好的替代方案。使用蓝绿发布,您准备一个完整的生产环境副本,其中包含新版本。现在您有两个副本,旧版(蓝色)和新版(绿色)。哪个是蓝色,哪个是绿色并不重要。重要的是您有两个完全独立的生产环境。目前,蓝色是活动状态并处理所有请求。您可以在绿色上运行所有测试。一旦满意,您就可以切换,绿色变为活动状态。如果出现问题,回滚同样简单;只需从绿色切换回蓝色。
下图说明了如何使用两个部署、两个标签和一个单一服务的标签选择器从蓝色部署切换到绿色部署的蓝绿部署的工作方式:

图 8.2:蓝绿部署
我在之前的讨论中完全忽略了存储和内存状态。此即时切换假定蓝色和绿色仅由无状态组件组成,并共享通用持久化层。
如果存储发生变化或外部客户端可访问的 API 发生重大变化,则需要采取额外步骤。例如,如果蓝色和绿色各自拥有自己的存储,则可能需要将所有传入请求发送到蓝色和绿色,并且在切换之前,绿色可能需要从蓝色那里摄取历史数据以保持同步。
金丝雀部署
蓝绿部署非常酷。然而,有时需要更加细致的方法。假设您负责一个具有许多用户的大型分布式系统。开发人员计划部署其服务的新版本。他们在测试和暂存环境中测试了服务的新版本。但是,生产环境太复杂,无法一对一复制用于测试目的。这意味着服务在生产环境可能出现异常的风险。这就是金丝雀部署发挥作用的地方。
基本思想是在生产中以有限的容量测试服务。这样,如果新版本出现问题,只会影响到您的一小部分用户或请求。这可以在 Kubernetes 的 Pod 级别非常容易地实现。如果一个服务由 10 个 Pod 支持,并且您将新版本部署到一个 Pod 中,则服务负载均衡器只会将 10% 的请求路由到金丝雀 Pod,而其余 90% 的请求仍由当前版本服务。
下图说明了这种方法:

图 8.3:金丝雀部署
使用服务网格将流量路由到金丝雀部署的方式更为复杂。我们将在第十四章,利用服务网格中进一步探讨这个问题。
我们已经讨论了执行实时集群更新的不同方法。现在让我们来解决管理数据契约变更的难题。
管理数据契约的变更
数据契约描述了数据的组织方式。它是结构化元数据的总称。最常见的例子是关系型数据库的架构。其他例子包括网络有效载荷、文件格式,甚至字符串参数或响应的内容。如果你有一个配置文件,那么这个配置文件既有文件格式(JSON、YAML、TOML、XML、INI 或自定义格式),也有一些描述有效层级、键、值和数据类型的内部结构。有时数据契约是显式的,有时是隐式的。无论是哪种方式,你都需要小心管理它,否则,当读取、解析或验证的代码遇到结构不熟悉的数据时,会导致运行时错误。
数据迁移
数据迁移是一项大工程。如今,许多系统管理着以太字节、拍字节甚至更多为单位的惊人数据量。可收集和管理的数据量将在可预见的未来继续增加。数据收集的速度超过了硬件创新的速度。关键点是,如果你有大量数据,并且需要迁移它,这可能需要一段时间。在之前的公司中,我负责一个项目,将近 100TB 的数据从一个遗留系统的 Cassandra 集群迁移到另一个 Cassandra 集群。
第二个 Cassandra 集群有不同的架构,并且由一个 24/7 运行的 Kubernetes 集群进行访问。这个项目非常复杂,因此当出现紧急问题时,它经常被推迟。遗留系统在原定时间表之后很长一段时间仍然与下一代系统并行运行。
有许多机制用于拆分数据并将其发送到两个集群,但随后我们遇到了新系统的可扩展性问题,在继续之前必须解决这些问题。历史数据很重要,但它不需要与最近的热数据以相同的服务级别进行访问。因此,我们开始了另一个项目,将历史数据迁移到更便宜的存储。这意味着,当然,客户端库或前端服务必须知道如何查询这两个存储并合并结果。当你处理大量数据时,不能把任何事情都视为理所当然。你会在工具、基础设施、第三方依赖和流程中遇到可扩展性问题。大规模不仅仅是数量的变化;它通常也是质的变化。不要指望它会顺利进行。它远不只是将一些文件从 A 复制到 B。
弃用 API
API 废弃有两种类型:内部和外部。内部 API 是由完全由你和你的团队或组织控制的组件使用的 API。你可以确保所有 API 用户将在短时间内升级到新的 API。外部 API 是由你直接控制范围之外的用户或服务使用的 API。
有一些灰色地带的情况,比如你在一个庞大的组织中工作(比如 Google),甚至内部 API 可能需要像外部 API 一样处理。如果你幸运的话,所有的外部 API 都由自我更新的应用程序或你控制的 Web 界面使用。在这种情况下,API 几乎是隐藏的,你甚至不需要发布它。
如果你的 API 有很多用户(或几个非常重要的用户),你应该非常谨慎地考虑是否废弃 API。废弃 API 意味着你强迫用户改变他们的应用程序以与新 API 兼容,或者继续使用旧版本。
有几种方法可以缓解这些痛点:
-
不要废弃。扩展现有的 API 或保持先前的 API 活跃。虽然这增加了测试负担,但有时是相当简单的。
-
向你的目标用户提供所有相关编程语言的客户端库。这始终是一个好做法。它允许你对底层 API 进行许多更改,而不会打扰用户(只要你保持编程语言接口的稳定)。
-
如果你必须废弃 API,解释原因,给用户足够的时间进行升级,并提供尽可能多的支持(例如,带有示例的升级指南)。你的用户会感激你的。
我们介绍了不同的工作负载部署和升级方式,并讨论了如何管理数据迁移和废弃 API。现在让我们看一下 Kubernetes 的另一个基础功能——水平 Pod 自动扩缩容——它可以让我们的工作负载高效地处理不同数量的请求,并动态调整用于处理这些请求的 Pod 数量。
水平 Pod 自动扩缩容
Kubernetes 可以监视你的 Pods,并在 CPU 使用率、内存或其他指标超过阈值时对其进行扩缩容。自动扩缩容资源指定了详细信息(CPU 百分比和检查频率),相应的自动扩缩容控制器会在需要时调整副本数量。
以下图示说明了不同角色及其关系:

图 8.4:水平 Pod 自动扩缩容
如你所见,水平 Pod 自动扩缩容器并不会直接创建或销毁 Pods。它调整的是 Deployment 或 StatefulSet 资源中的副本数量,相应的控制器负责实际的 Pods 创建和销毁。这非常聪明,因为你无需处理自动扩缩容与控制器正常运行之间的冲突,控制器并未察觉自动扩缩容的操作。
自动扩缩器自动完成了我们之前必须手动执行的操作。如果没有自动扩缩器,假设我们有一个副本数为 3 的部署,但基于平均 CPU 利用率,我们实际需要 4 个副本,那么我们必须手动更新部署将副本数从 3 更新为 4,并继续监控所有 Pod 的 CPU 利用率。然而,自动扩缩器将为我们完成这些工作。
创建水平 Pod 自动扩缩器
要声明一个水平 Pod 自动扩缩器,我们需要一个工作负载资源(Deployment 或 StatefulSet),以及一个 HorizontalPodAutoscaler 资源。以下是一个简单的部署配置,旨在保持 3 个 Nginx Pod:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
replicas: 3
selector:
matchLabels:
run: nginx
template:
metadata:
labels:
run: nginx
spec:
containers:
- name: nginx
image: nginx
resources:
requests:
cpu: 400m
ports:
- containerPort: 80
$ k apply -f nginx-deployment.yaml
deployment.apps/nginx created
请注意,为了参与自动扩缩,容器必须请求特定数量的 CPU。
水平 Pod 自动扩缩器在 scaleTargetRef 中引用 Nginx 部署:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: nginx
spec:
maxReplicas: 4
minReplicas: 2
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: nginx
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 90
$ k apply -f nginx-hpa.yaml
horizontalpodautoscaler.autoscaling/nginx created
minReplicas 和 maxReplicas 指定了扩缩容的范围。这样做是为了避免因为某些问题而导致的资源浪费情况。想象一下,如果由于某个 bug,所有 Pod 无论实际负载如何,都立即使用 100% 的 CPU。那么没有 maxReplicas 限制的情况下,Kubernetes 会不断创建更多的 Pod,直到所有集群资源被耗尽。如果我们在一个具有虚拟机自动扩展的云环境中运行,可能会导致巨大的成本支出。另一方面,如果没有 minReplicas 且活动暂停,那么所有的 Pod 可能会被终止,当新的请求到来时,又需要重新创建并调度 Pod,这可能需要几分钟,尤其是如果需要为新的节点提供资源,并且 Pod 准备时间较长,这些时间会累计。如果有周期性的开关活动,这一过程可能会重复多次。保持最小副本数运行可以平滑这种现象。在上述示例中,minReplicas 设置为 2,maxReplicas 设置为 4。Kubernetes 将确保始终运行 2 到 4 个 Nginx 实例。
目标 CPU 利用率百分比是个较长的术语。我们将其缩写为TCUP。你指定一个数字,比如 80%,但是 Kubernetes 在跨越阈值时不会立即开始进行扩缩容。这可能导致不断的震荡,尤其是当平均负载徘徊在 TCUP 附近时。Kubernetes 会在添加副本和移除副本之间频繁切换,这通常不是期望的行为。为了解决这个问题,你可以为扩展或缩减操作指定一个延迟。
kube-controller-manager 有两个标志位来支持这一功能:
-
--horizontal-pod-autoscaler-downscale-delay:该选项需要一个持续时间值,用于确定在当前缩减操作完成后,自动扩缩容器等待多长时间才开始执行下一个缩减操作。默认持续时间为 5 分钟(5m0s)。 -
--horizontal-pod-autoscaler-upscale-delay:此选项需要一个持续时间值,用于确定在当前扩展操作完成后,自动扩展器等待多长时间才能启动另一次扩展操作。默认情况下,持续时间设置为 3 分钟(3m0s)。
让我们检查 HPA:
$ k get hpa
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
nginx Deployment/nginx <unknown>/90% 2 4 3 70s
如你所见,目标是未知的。HPA(水平 Pod 自动扩展器)需要一个度量服务器来测量 CPU 百分比。安装度量服务器的最简单方法之一是使用 Helm。我们在第二章,创建 Kubernetes 集群中已经安装了 Helm。以下是将 Kubernetes 度量服务器安装到监控命名空间的命令:
$ helm repo add metrics-server https://kubernetes-sigs.github.io/metrics-server/
"metrics-server" has been added to your repositories
$ helm upgrade --install metrics-server metrics-server/metrics-server \
--namespace monitoring \
--create-namespace
Release "metrics-server" does not exist. Installing it now.
NAME: metrics-server
LAST DEPLOYED: Sat Jul 30 23:16:09 2022
NAMESPACE: monitoring
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
***********************************************************************
* Metrics Server *
***********************************************************************
Chart version: 3.8.2
App version: 0.6.1
Image tag: k8s.gcr.io/metrics-server/metrics-server:v0.6.1
***********************************************************************
不幸的是,由于证书问题,metrics-server无法在 KinD 集群中直接运行。
你可以通过以下命令轻松修复这个问题:
$ k patch -n monitoring deployment metrics-server --type=json \
-p '[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--kubelet-insecure-tls"}]'
我们可能需要等待度量服务器准备好。一种好的方法是使用kubectl wait:
kubectl wait deployment metrics-server -n monitoring --for=condition=Available
deployment.apps/metrics-server condition met
既然kubectl已经返回,我们也可以利用kubectl top命令,它可以显示节点和 Pod 的度量信息:
$ k top no
NAME CPU(cores) CPU% MEMORY(bytes) MEMORY%
kind-control-plane 213m 5% 15Mi 0%
$ k top po
NAME CPU(cores) MEMORY(bytes)
nginx-64f97b4d86-gqmjj 0m 3Mi
nginx-64f97b4d86-sj8cz 0m 3Mi
nginx-64f97b4d86-xc99j 0m 3Mi
在重新部署 Nginx 和 HPA 后,你可以看到利用率,以及副本数为 3,处于 2-4 的范围内:
$ k get hpa
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
nginx Deployment/nginx 0%/90% 2 4 3 26s
由于 CPU 利用率低于目标利用率,经过几分钟后,HPA 将把 Nginx 扩展到最小的 2 个副本:
$ k get hpa
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
nginx Deployment/nginx 0%/90% 2 4 2 6m57s
自定义度量
CPU 利用率是一个重要的度量指标,用来判断如果 Pod 受到过多请求时,是否需要扩展,或者它们大部分时间处于空闲状态,是否可以缩减。但 CPU 并不是唯一的度量,有时甚至不是最好的度量。内存可能是限制因素,甚至可以使用更专业的度量,例如并发线程的数量、Pod 内部磁盘队列的深度、请求的平均延迟或平均服务超时数。
水平 Pod 自定义度量作为 alpha 扩展在 1.2 版本中添加。在 1.6 版本中它们升级为 beta 状态,在 1.23 版本中变为稳定状态。现在,你可以基于多个自定义度量来自动扩展 Pod。
自动扩展器将评估所有度量,并根据所需的最大副本数进行自动扩展,因此所有度量的要求都会被满足。
使用水平 Pod 自动扩展器和自定义度量时,在启动集群时需要进行一些配置。首先,你需要启用 API 聚合层。然后,你需要注册资源度量 API 和自定义度量 API。这并不简单。这就是 Keda 的作用。
Keda
Keda代表Kubernetes 事件驱动的自动扩展。这是一个令人印象深刻的项目,它将你实施水平 Pod 自定义度量所需的一切打包在一起。通常,你可能希望扩展 Deployments、StatefulSets 或 Jobs,但 Keda 也可以扩展 CRD,只要它们具有/scale子资源。Keda 作为一个操作器进行部署,监控多个自定义资源:
-
scaledobjects.keda.sh -
scaledjobs.keda.sh -
triggerauthentications.keda.sh -
clustertriggerauthentications.keda.sh
Keda 还具有一个指标服务器,支持大量事件源和扩展器,并能从所有这些源收集指标来通知扩展过程。事件源包括所有流行的数据库、消息队列、云数据存储和各种监控 API。例如,如果你依赖 Prometheus 获取指标,你可以使用 Keda 根据推送到 Prometheus 的任何指标或指标组合来扩展工作负载。
下图展示了 Keda 的架构:

图 8.5:Keda 架构
查看 keda.sh 获取更多细节。
使用 kubectl 进行自动扩展
kubectl 可以使用标准的 create 命令创建一个自动扩展资源,接受配置文件。但 kubectl 也有一个特殊命令 autoscale,它让你可以在一个命令中轻松设置自动扩展器,而无需特殊的配置文件。
首先,让我们启动一个确保有三个副本的简单 Pod 的部署,并且这个 Pod 只运行一个无限循环的 bash 脚本:
apiVersion: apps/v1
kind: Deployment
metadata:
name: bash-loop
spec:
replicas: 3
selector:
matchLabels:
name: bash-loop
template:
metadata:
labels:
name: bash-loop
spec:
containers:
- name: bash-loop
image: g1g1/py-kube:0.3
resources:
requests:
cpu: 100m
command: ["/bin/bash", "-c", "while true; do sleep 10; done"]
$ k apply -f bash-loop-deployment.yaml
deployment.apps/bash-loop created
这是结果中的部署:
$ k get deployment
NAME READY UP-TO-DATE AVAILABLE AGE
bash-loop 3/3 3 3 35s
你可以看到期望数量和当前数量都是 3,意味着正在运行三个 Pod。让我们确认一下:
$ k get pods
NAME READY STATUS RESTARTS AGE
bash-loop-8496f889f8-9khjs 1/1 Running 0 106s
bash-loop-8496f889f8-frhb7 1/1 Running 0 105s
bash-loop-8496f889f8-hcd2d 1/1 Running 0 105s
现在,让我们创建一个自动扩展器。为了增加趣味性,我们将最小副本数设置为 4,最大副本数设置为 6:
$ k autoscale deployment bash-loop --min=4 --max=6 --cpu-percent=50
horizontalpodautoscaler.autoscaling/bash-loop autoscaled
这里是结果中的水平 Pod 自动扩展器(你可以使用 hpa)。它显示了引用的部署、目标和当前的 CPU 百分比,以及最小/最大 Pod 数量。名称与引用的部署 bash-loop 匹配:
$ k get hpa
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
bash-loop Deployment/bash-loop 2%/50% 4 6 4 36s
最初,部署设置为有三个副本,但自动扩展器的最小副本数为四个。对部署有什么影响?现在期望的副本数是四个。如果平均 CPU 利用率超过 50%,它将增加到五个甚至六个,但永远不会少于四个:
$ k get deployment
NAME READY UP-TO-DATE AVAILABLE AGE
bash-loop 4/4 4 4 4m11s
当我们删除水平 Pod 自动扩展器时,部署会保留最后期望的副本数(在本例中为 4)。没有人记得该部署最初是用三个副本创建的:
$ k delete hpa bash-loop
horizontalpodautoscaler.autoscaling "bash-loop" deleted
如你所见,部署并没有重置,仍然保持四个 Pod,即使自动扩展器已经消失:
$ k get deployment
NAME READY UP-TO-DATE AVAILABLE AGE
bash-loop 4/4 4 4 5m17s
这是有道理的,因为水平 Pod 自动扩展器修改了部署的规格,使其拥有 4 个副本:
$ k get deploy bash-loop -o jsonpath='{.spec.replicas}'
4
我们试试别的。如果我们创建一个新的水平 Pod 自动扩展器,范围为 2 到 6,且 CPU 目标仍然为 50%,会发生什么?
$ k autoscale deployment bash-loop --min=2 --max=6 --cpu-percent=50
horizontalpodautoscaler.autoscaling/bash-loop autoscaled
好吧,部署仍然保持着四个副本,这在范围之内:
$ k get deployment
NAME READY UP-TO-DATE AVAILABLE AGE
bash-loop 4/4 4 4 8m18s
然而,实际的 CPU 利用率仅为 2%。该部署最终会缩减到两个副本,但由于水平 Pod 自动扩展器不会立即缩减,我们需要等待几分钟(默认 5 分钟):
$ k get deployment
NAME READY UP-TO-DATE AVAILABLE AGE
bash-loop 2/2 2 2 28m
让我们检查一下水平 Pod 自动扩展器本身:
$ k get hpa
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
bash-loop Deployment/bash-loop 2%/50% 2 6 2 21m
现在,你已经理解了水平 Pod 自动扩展的基本概念,让我们来看一下如何使用自动扩展执行滚动更新。
执行带有自动扩展的滚动更新
滚动更新是大规模集群中管理工作负载的基石。当你对由 HPA 控制的部署进行滚动更新时,部署将创建一个新的副本集,并开始增加副本数,同时减少旧副本集的副本数。同时,HPA 可能会改变部署的总副本数。这不是问题,所有内容最终会协调一致。
这里是我们在 第五章,实践中使用 Kubernetes 资源,中用于部署 hue-reminders 服务的部署配置文件:
apiVersion: apps/v1
kind: Deployment
metadata:
name: hue-reminders
spec:
replicas: 2
selector:
matchLabels:
app: hue
service: reminders
template:
metadata:
name: hue-reminders
labels:
app: hue
service: reminders
spec:
containers:
- name: hue-reminders
image: g1g1/hue-reminders:2.2
resources:
requests:
cpu: 100m
ports:
- containerPort: 80
$ k apply -f hue-reminders-deployment.yaml
deployment.apps/hue-reminders created
为了支持自动扩缩并确保我们始终保持 10 到 15 个实例在运行,我们可以创建一个自动扩缩器配置文件:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: hue-reminders
spec:
maxReplicas: 15
minReplicas: 10
targetCPUUtilizationPercentage: 90
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: hue-reminders
或者,我们可以使用 kubectl autoscale 命令:
$ k autoscale deployment hue-reminders --min=10 --max=15 --cpu-percent=90
horizontalpodautoscaler.autoscaling/hue-reminders autoscaled
让我们执行从版本 2.2 到 3.0 的滚动更新:
$ k set image deployment/hue-reminders hue-reminders=g1g1/hue-reminders:3.0
我们可以使用 rollout status 来检查状态:
$ k rollout status deployment hue-reminders
Waiting for deployment "hue-reminders" rollout to finish: 9 out of 10 new replicas have been updated...
Waiting for deployment "hue-reminders" rollout to finish: 9 out of 10 new replicas have been updated...
Waiting for deployment "hue-reminders" rollout to finish: 9 out of 10 new replicas have been updated...
Waiting for deployment "hue-reminders" rollout to finish: 9 out of 10 new replicas have been updated...
Waiting for deployment "hue-reminders" rollout to finish: 3 old replicas are pending termination...
Waiting for deployment "hue-reminders" rollout to finish: 3 old replicas are pending termination...
Waiting for deployment "hue-reminders" rollout to finish: 2 old replicas are pending termination...
Waiting for deployment "hue-reminders" rollout to finish: 2 old replicas are pending termination...
Waiting for deployment "hue-reminders" rollout to finish: 1 old replicas are pending termination...
Waiting for deployment "hue-reminders" rollout to finish: 1 old replicas are pending termination...
Waiting for deployment "hue-reminders" rollout to finish: 1 old replicas are pending termination...
Waiting for deployment "hue-reminders" rollout to finish: 8 of 10 updated replicas are available...
Waiting for deployment "hue-reminders" rollout to finish: 9 of 10 updated replicas are available...
deployment "hue-reminders" successfully rolled out
最后,我们回顾部署的历史:
$ k rollout history deployment hue-reminders
deployment.apps/hue-reminders
REVISION CHANGE-CAUSE
3 kubectl1.23.4 set image deployment/hue-reminders hue-reminders=g1g1/hue-reminders:3.0 --record=true
4 kubectl1.23.4 set image deployment/hue-reminders hue-reminders=g1g1/hue-reminders:3.0 --record=true
自动扩缩是基于资源使用情况和阈值来工作的。在接下来的部分中,我们将探讨 Kubernetes 如何让我们使用请求和限制来控制和管理每个工作负载的资源。
通过限制和配额处理稀缺资源
由于水平 Pod 自动扩缩器会动态创建 Pods,我们需要考虑如何管理资源。调度容易失控,资源使用不当是一个真实的担忧。影响因素有很多,这些因素可能会相互作用,产生微妙的影响:
-
整体集群容量
-
每个节点的资源粒度
-
每个命名空间的工作负载划分
-
守护进程集
-
有状态集
-
亲和性、反亲和性、污点和容忍
首先,让我们理解核心问题。Kubernetes 调度器在调度 Pods 时必须考虑所有这些因素。如果存在冲突或大量重叠的需求,那么 Kubernetes 可能会遇到无法找到空间调度新 Pods 的问题。例如,一个极端而简单的场景是,一个守护进程集在每个节点上运行一个需要 50% 可用内存的 Pod。现在,Kubernetes 就无法调度任何其他需要超过 50% 内存的 Pod,因为守护进程集的 Pod 拥有优先权。即使你新增了节点,守护进程集也会立即占用一半的内存。
有状态集与守护进程集类似,都需要新的节点来扩展。向有状态集添加新成员的触发因素是数据的增长,但其影响是占用了 Kubernetes 用于调度其他工作负载的资源池。在多租户环境下,噪音邻居问题可能会出现在配置或资源分配的过程中。你可能会在命名空间内精心规划不同 Pods 及其资源需求的准确比例,但你与来自其他命名空间的邻居共享实际节点,而这些邻居你可能完全无法察觉。
大多数这些问题可以通过谨慎使用命名空间资源配额和对跨多种资源类型(如 CPU、内存和存储)的集群容量进行仔细管理来缓解。此外,如果您控制节点供应,可以通过对它们进行污点处理来为您的工作负载划分专用节点。
但在大多数情况下,更强大和动态的方法是利用集群自动扩展器,在需要时增加集群的容量(直到配额用尽)。
启用资源配额
大多数 Kubernetes 发行版都原生支持 ResourceQuota。API 服务器的 --admission-control 标志必须将 ResourceQuota 作为其参数之一。您还必须创建一个 ResourceQuota 对象来执行它。请注意,每个命名空间最多只能有一个 ResourceQuota 对象,以防止潜在的冲突。这是 Kubernetes 强制执行的。
资源配额类型
我们可以管理和控制不同类型的配额。类别包括计算、存储和对象。
计算资源配额
计算资源包括 CPU 和内存。对于每一个,您可以指定一个限制或请求一定数量。以下是与计算相关的字段列表。请注意,requests.cpu 可以简单地指定为 cpu,requests.memory 可以简单地指定为 memory:
-
limits.cpu:考虑到所有非终端状态的 Pod,总 CPU 限制不得超过此值。 -
limits.memory:考虑到所有非终端状态的 Pod,组合内存限制不得超过此值。 -
requests.cpu:考虑到所有非终端状态的 Pod,总 CPU 请求不应超过此值。 -
requests.memory:考虑到所有非终端状态的 Pod,组合内存请求不应超过此值。 -
hugepages-:考虑到所有非终端状态的 Pod,特定大小的巨页请求的最大允许数量不得超过此值。
自 Kubernetes 1.10 开始,您还可以为诸如 GPU 资源之类的扩展资源指定配额。以下是一个例子:
requests.nvidia.com/gpu: 10
存储资源配额
存储资源配额类型稍微复杂一些。您可以限制每个命名空间的两个实体:存储量和持久卷索赔的数量。但是,除了全局设置总存储或持久卷索赔总数的配额之外,您还可以按存储类进行设置。存储类资源配额的表示法有点冗长,但它完成了工作:
-
requests.storage:所有持久卷索赔中请求的存储总量。 -
persistentvolumeclaims:命名空间中允许的持久卷索赔的最大数量 -
.storageclass.storage.k8s.io/requests.storage:与storage-class-name关联的所有持久卷索赔的请求存储总量。 -
.storageclass.storage.k8s.io/persistentvolumeclaims:命名空间中与storage-class-name相关联的最大持久卷声明数量
Kubernetes 1.8 还增加了对临时存储配额的 alpha 支持:
-
requests.ephemeral-storage:命名空间中所有 Pod 请求的临时存储总量 -
limits.ephemeral-storage:命名空间中所有 Pod 的临时存储限制总量
存储配置的一个问题是磁盘容量并不是唯一的因素,磁盘 I/O 也是一个重要资源。例如,考虑一个不断更新同一个小文件的 Pod。它不会使用大量的容量,但会执行很多 I/O 操作。
对象计数配额
Kubernetes 还有另一类资源配额,那就是 API 对象。我的猜测是,目标是保护 Kubernetes API 服务器,避免它管理过多的对象。记住,Kubernetes 在后台做了大量工作。它经常需要查询多个对象来进行身份验证、授权,并确保操作不会违反可能存在的任何政策。一个简单的例子是基于复制控制器的 Pod 调度。假设你有一百万个副本集对象,也许你只有三个 Pod,而大多数副本集的副本数为零。尽管如此,Kubernetes 会花费大量时间验证这些副本集确实没有副本,并且不需要终止任何 Pod。虽然这是一个极端的例子,但这一概念适用。过多的 API 对象意味着 Kubernetes 需要花费大量工作来管理。
此外,客户端使用发现缓存(如 kubectl 本身)也是一个问题。请参见此问题:github.com/kubernetes/kubectl/issues/1126。
从 Kubernetes 1.9 起,你可以限制任何命名空间资源的数量(在此之前,可限制的对象范围有些不稳定)。语法很有意思,count/<resource type>.<group>。通常在 YAML 文件和 kubectl 中,你首先按组标识对象,如 <group>/<resource type>。
下面是一些你可能想要限制的对象(请注意,部署可以针对两个独立的 API 组进行限制):
-
count/configmaps -
count/deployments.apps -
count/deployments.extensions -
count/persistentvolumeclaims -
count/replicasets.apps -
count/replicationcontrollers -
count/secrets -
count/services -
count/statefulsets.apps -
count/jobs.batch -
count/cronjobs.batch
从 Kubernetes 1.5 起,你还可以限制自定义资源的数量。请注意,虽然自定义资源定义是集群范围的,但这允许你限制每个命名空间中实际的自定义资源数量。例如:
count/awesome.custom.resource
最显著的遗漏是命名空间。没有限制命名空间的数量。由于所有限制都是按命名空间计算的,你可以通过创建过多命名空间轻松压垮 Kubernetes,每个命名空间只有少量 API 对象。但是,创建命名空间的能力应该仅保留给集群管理员,他们不需要资源配额来约束自己。
配额范围
一些资源(如 Pods)可能处于不同的状态,因此为这些不同的状态设置不同的配额是很有用的。例如,如果有许多 Pods 正在终止(这在滚动更新期间经常发生),那么即使总数超过配额,创建更多 Pods 也是可以的。这可以通过仅对非终止 Pods 应用 Pod 对象计数配额来实现。以下是现有的范围:
-
Terminating:选择activeDeadlineSeconds值大于或等于0的 Pods。 -
NotTerminating:选择未指定activeDeadlineSeconds(即为空值)的 Pods。 -
BestEffort:选择具有最佳努力服务质量的 Pods,意味着这些 Pods 未指定资源请求和限制。 -
NotBestEffort:选择不具有最佳努力服务质量的 Pods,表示这些 Pods 已指定资源请求和限制。 -
PriorityClass:选择定义优先级类的 Pods。 -
CrossNamespacePodAffinity:选择具有跨命名空间亲和性或反亲和性调度条件的 Pods。
虽然 BestEffort 范围仅适用于 Pods,但 Terminating、NotTerminating 和 NotBestEffort 范围也适用于 CPU 和内存。这很有趣,因为资源配额限制可能会阻止 Pod 终止。以下是支持的对象:
-
CPU
-
内存
-
limits.cpu -
limits.memory -
requests.cpu -
requests.memory -
Pods
资源配额和优先级类
Kubernetes 1.9 引入了优先级类,作为在资源紧张时优先调度 Pods 的一种方式。在 Kubernetes 1.14 中,优先级类变得稳定。然而,从 Kubernetes 1.12 开始,资源配额支持按优先级类设置单独的资源配额(处于 Beta 阶段)。这意味着通过优先级类,你可以在命名空间内非常精细地调整资源配额。
欲了解更多详情,请查看 kubernetes.io/docs/concepts/policy/resource-quotas/#resource-quota-per-priorityclass。
请求和限制
在资源配额的背景下,请求和限制的含义是要求容器明确指定目标属性。这样,Kubernetes 就可以管理总配额,因为它知道每个容器分配了哪些资源范围。
配额管理
这部分是理论内容。现在是时候动手操作了。我们首先创建一个命名空间:
$ k create namespace ns
namespace/ns created
使用命名空间特定的上下文
当使用非默认命名空间时,我更喜欢设置当前上下文的命名空间,这样我就不必在每个命令中都输入 --namespace=ns:
$ k config set-context --current --namespace ns
Context "kind-kind" modified.
创建配额
这是计算配额:
apiVersion: v1
kind: ResourceQuota
metadata:
name: compute-quota
spec:
hard:
pods: 2
requests.cpu: 1
requests.memory: 200Mi
limits.cpu: 2
limits.memory: 2Gi
我们通过输入以下命令来创建它:
$ k apply -f compute-quota.yaml
resourcequota/compute-quota created
这里是计数配额:
apiVersion: v1
kind: ResourceQuota
metadata:
name: object-counts-quota
spec:
hard:
count/configmaps: 10
count/persistentvolumeclaims: 4
count/jobs.batch: 20
count/secrets: 3
我们通过输入以下命令来创建它:
$ k apply -f object-count-quota.yaml
resourcequota/object-counts-quota created
我们可以观察所有的配额:
$ k get quota
NAME AGE REQUEST LIMIT
compute-quota 32s pods: 0/2, requests.cpu: 0/1, requests.memory: 0/200Mi limits.cpu: 0/2, limits.memory: 0/2Gi
object-counts-quota 13s count/configmaps: 1/10, count/jobs.batch: 0/20, count/persistentvolumeclaims: 0/4, count/secrets: 1/3
我们可以通过 kubectl describe 以更具可视化效果的方式深入查看这两个资源配额的所有信息:
$ k describe quota compute-quota
Name: compute-quota
Namespace: ns
Resource Used Hard
-------- ---- ----
limits.cpu 0 2
limits.memory 0 2Gi
pods 0 2
requests.cpu 0 1
requests.memory 0 200Mi
$ k describe quota object-counts-quota
Name: object-counts-quota
Namespace: ns
Resource Used Hard
-------- ---- ----
count/configmaps 1 10
count/jobs.batch 0 20
count/persistentvolumeclaims 0 4
count/secrets 1 3
如你所见,它完全反映了规格,并且它是在 ns 命名空间中定义的。
这个视图可以让我们迅速了解集群中重要资源的全局资源使用情况,而无需深入到太多单独的对象中。
让我们在命名空间中添加一个 Nginx 服务器:
$ k create -f nginx-deployment.yaml
deployment.apps/nginx created
让我们检查一下 Pods:
$ k get po
No resources found in ns namespace.
哎呀。没有找到资源。但是,在创建部署时没有报错。让我们检查一下部署:
$ k describe deployment nginx
Name: nginx
Namespace: ns
CreationTimestamp: Sun, 31 Jul 2022 13:49:24 -0700
Labels: <none>
Annotations: deployment.kubernetes.io/revision: 1 kind-kind | ns
Selector: run=nginx
Replicas: 3 desired | 0 updated | 0 total | 0 available | 3 unavailable
StrategyType: RollingUpdate
MinReadySeconds: 0
RollingUpdateStrategy: 25% max unavailable, 25% max surge
Pod Template:
Labels: run=nginx
Containers:
nginx:
Image: nginx
Port: 80/TCP
Host Port: 0/TCP
Requests:
cpu: 400m
Environment: <none>
Mounts: <none>
Volumes: <none>
Conditions:
Type Status Reason
---- ------ ------
Progressing True NewReplicaSetCreated
Available False MinimumReplicasUnavailable
ReplicaFailure True FailedCreate
OldReplicaSets: <none>
NewReplicaSet: nginx-64f97b4d86 (0/3 replicas created)
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal ScalingReplicaSet 65s deployment-controller Scaled up replica set nginx-64f97b4d86 to 3
在 Conditions 部分可以看到,ReplicaFailure 状态为 True,原因是 FailedCreate。你可以看到部署创建了一个新的副本集,名为 nginx-64f97b4d86,但它无法创建预期的 Pod。我们仍然不知道为什么。
让我们查看副本集。我使用 JSON 输出格式(-o json),并将其通过管道传递给 jq,以便获得更好的布局,这比 kubectl 本地支持的 jsonpath 输出格式要好得多:
$ k get rs nginx-64f97b4d86 -o json | jq .status.conditions
[
{
"lastTransitionTime": "2022-07-31T20:49:24Z",
"message": "pods \"nginx-64f97b4d86-ks7d6\" is forbidden: failed quota: compute-quota: must specify limits.cpu,limits.memory,requests.memory",
"reason": "FailedCreate",
"status": "True",
"type": "ReplicaFailure"
}
]
信息非常明确。由于命名空间中有计算配额,每个容器都必须指定其 CPU、内存请求和限制。配额控制器必须考虑每个容器的计算资源使用情况,以确保总的命名空间配额得到遵守。
好的,我们理解了问题,但如何解决呢?我们可以为每种我们想使用的 Pod 类型创建一个专门的部署对象,并小心地设置 CPU 和内存请求和限制。
例如,我们可以定义带有资源的 Nginx 部署。由于资源配额指定了一个硬性限制为 2 个 Pods,让我们将副本数量从 3 个减少到 2 个:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
replicas: 2
selector:
matchLabels:
run: nginx
template:
metadata:
labels:
run: nginx
spec:
containers:
- name: nginx
image: nginx
resources:
requests:
cpu: 400m
memory: 60Mi
limits:
cpu: 400m
memory: 60Mi
ports:
- containerPort: 80
让我们创建它并检查 Pods:
$ k apply -f nginx-deployment-with-resources.yaml
deployment.apps/nginx created
$ k get po
NAME READY STATUS RESTARTS AGE
nginx-5d68f45c5f-6h9w9 1/1 Running 0 21s
nginx-5d68f45c5f-b8htm 1/1 Running 0 21s
是的,成功了!但是,为每个 Pod 类型指定限制和资源可能非常繁琐。有没有更简单或更好的方法?
使用限制范围设置默认计算配额
更好的方法是指定默认计算限制。输入限制范围。这是一个设置容器默认值的配置文件:
apiVersion: v1
kind: LimitRange
metadata:
name: limits
spec:
limits:
- default:
cpu: 400m
memory: 50Mi
defaultRequest:
cpu: 400m
memory: 50Mi
type: Container
让我们创建它并观察默认的限制:
$ k apply -f limits.yaml
limitrange/limits created
$ k describe limits
Name: limits
Namespace: ns
Type Resource Min Max Default Request Default Limit Max Limit/Request Ratio
---- -------- --- --- --------------- ------------- -----------------------
Container cpu - - 400m 400m -
Container memory - - 50Mi 50Mi -
为了测试它,让我们删除当前的 Nginx 部署并重新部署原始的 Nginx,保持显式的限制:
$ k delete deployment nginx
deployment.apps "nginx" deleted
$ k apply -f nginx-deployment.yaml
deployment.apps/nginx created
$ k get deployment
NAME READY UP-TO-DATE AVAILABLE AGE
nginx 2/3 2 2 16s
如您所见,只有 3 个 Pod 中的 2 个处于就绪状态。发生了什么?默认的限制起作用了,但如果您记得的话,计算配额对于命名空间有 2 个 Pod 的硬性限制。无法通过 RangeLimit 对象覆盖它,因此部署只能够创建两个 Nginx Pod。这正是基于当前配置所期望的结果。如果部署确实需要 3 个 Pod,那么应该更新命名空间的计算配额以允许 3 个 Pod。
这部分内容结束了我们关于使用请求、限制和配额进行资源管理的讨论。下一节将探讨如何在 Kubernetes 上自动化大规模部署和配置多个工作负载。
持续集成和持续部署
Kubernetes 是运行基于微服务的应用程序的绝佳平台。但归根结底,它只是一个实现细节。用户,通常包括大多数开发人员,可能并不知道系统是部署在 Kubernetes 上的。但 Kubernetes 可以改变游戏规则,使之前过于复杂的事情变得可能。
在本节中,我们将探讨 CI/CD 流水线以及 Kubernetes 带来的优势。在本节结束时,您将能够设计利用 Kubernetes 特性(如轻松扩展和开发与生产一致性)来提高日常开发和部署的生产力和健壮性的 CI/CD 流水线。
什么是 CI/CD 流水线?
CI/CD 流水线是一组工具和步骤,旨在处理由开发人员或运维人员提交的一组变更,这些变更可能涉及系统的代码、数据或配置,并对其进行测试,然后将其部署到生产环境(以及其他可能的环境)。一些流水线是完全自动化的,另一些则是半自动化的,需要人工检查。在大型组织中,通常会将变更自动部署到测试和预发布环境中,而生产环境的发布则需要人工干预和批准。以下图示展示了一个典型的遵循这种实践的 CI/CD 流水线:

图 8.6:CI/CD 流水线
可能值得一提的是,开发人员可以完全与生产基础设施隔离。他们的界面只是一个 Git 工作流,其中一个很好的例子是 Deis Workflow(Kubernetes 上的 PaaS,类似于 Heroku)。
为 Kubernetes 设计 CI/CD 流水线
当您的部署目标是 Kubernetes 集群时,您应该重新考虑一些传统的做法。首先,打包方式不同。您需要为容器创建镜像。通过使用智能标签,回滚代码变更变得非常简单和即时。这让您非常有信心,如果某个错误的变更 somehow 穿过了测试网,您能够立即回滚到先前的版本。但在这方面您需要小心。模式变更和数据迁移无法在没有协调的情况下自动回滚。
Kubernetes 的另一个独特能力是开发人员可以在本地运行整个集群。 当你设计你的集群时需要一些工作,但由于组成系统的微服务运行在容器中,并且这些容器通过 API 进行交互,这是可能且实际可行的。 像往常一样,如果你的系统非常依赖数据驱动,你将需要适应这一点,并提供数据快照和开发人员可以使用的合成数据。 另外,如果你的服务访问外部系统或云提供商服务,那么完全本地集群可能不是理想的选择。
你的 CI/CD 流水线应该允许集群管理员快速调整配额和限制,以适应扩展和业务增长。 此外,你应该能够轻松地将大多数工作负载部署到不同的环境中。 例如,如果你的预发布环境与生产环境不同,这会减少在预发布环境中运行良好的更改对生产环境造成损害的信心。 通过确保所有环境更改都通过 CI/CD,可以保持不同环境的同步。
有许多商业的 CI/CD 解决方案支持 Kubernetes,但也有几个 Kubernetes 本地解决方案,例如 Tekton、Argo CD、Flux CD 和 Jenkins X。
Kubernetes 本地的 CI/CD 解决方案在你的集群内运行,使用 Kubernetes CRD 指定,并使用容器执行步骤。 通过使用 Kubernetes 本地的 CI/CD 解决方案,你可以享受 Kubernetes 管理和轻松扩展 CI/CD 流水线的好处,这通常是一个非常重要的任务。
为你的应用程序提供基础设施
CI/CD 流水线用于在 Kubernetes 上部署工作负载。 但是,这些服务通常要求你对基础设施(如云资源,数据库,甚至 Kubernetes 集群本身)进行操作。 有不同的方法来提供这些基础设施。 让我们审查一些常见的解决方案。
云提供商的 API 和工具
如果你完全致力于单一云提供商,并且没有使用多个云提供商或将基于云的集群与本地集群混合的意图,你可能更喜欢使用你的云提供商的 API 工具(例如 AWS CloudFormation)。 这种方法有几个好处:
-
与你的云提供商基础设施的深度集成
-
从你的云提供商获得最佳支持
-
没有间接层
然而,这意味着你对系统的视图将被分割。 一些信息将通过 Kubernetes 可用并存储在 etcd 中。 其他信息将存储并可通过你的云提供商访问。
缺乏 Kubernetes 本地基础设施视图意味着在本地运行集群可能具有挑战性,并且整合其他云提供商或本地环境将肯定需要大量工作。
Terraform
Terraform (terraform.io) 由 HashiCorp 开发,是一个基础设施即代码(IaC)工具。它是现有的领导者。你可以使用 Terraform 的 HCL 语言定义基础设施,并可以通过模块组织基础设施配置。最初它专注于 AWS,但随着时间的推移,它成为了一个通用工具,能够在任何云平台上以及通过提供者插件为其他类型的基础设施进行配置。
查看 Terraform 注册表中所有可用的提供者:registry.terraform.io/browse/providers。
由于 Terraform 是声明式地定义基础设施的,因此它自然支持 GitOps 生命周期,在这个生命周期中,基础设施的变更必须提交到代码控制中,并且可以进行审查,历史记录也会被保存。
你通常通过其 CLI 与 Terraform 进行交互。你可以运行 terraform plan 命令查看 Terraform 将进行哪些变更,如果你对结果满意,就可以通过 terraform apply 命令应用这些变更。
下图展示了 Terraform 的工作流:

图 8.7:Terraform 工作流
我已经广泛使用 Terraform 在 AWS、GCP 和 Azure 上为大规模系统配置基础设施。它确实能完成工作,但它也存在一些问题:
-
它的托管状态可能与现实世界的基础设施不同步
-
它的设计和语言使得在大规模环境中使用时变得困难
-
它无法自动检测并解决基础设施的外部变更
Pulumi
Pulumi 是一个更现代的 IaC 工具。从概念上讲,它与 Terraform 类似,但你可以使用多种编程语言来定义基础设施,而不是使用自定义的 DSL。这为你提供了一个完整的语言生态系统,如 TypeScript、Python 或 Go,包括测试和打包功能来管理你的基础设施。
Pulumi 还自豪地拥有动态提供者,这些提供者会在同一天更新,以支持云提供商资源。它还可以包装 Terraform 提供者,从而实现对你基础设施需求的完全覆盖。
Pulumi 的编程模型基于堆栈、资源和输入/输出的概念:

图 8.8:Pulumi 编程模型
这里是一个使用 Pulumi 在 Python 中配置 EC2 实例的简单示例:
import pulumi
import pulumi_aws as aws
group = aws.ec2.SecurityGroup('web-sg',
description='Enable HTTP access',
ingress=[
{ 'protocol': 'tcp', 'from_port': 80, 'to_port': 80, 'cidr_blocks': ['0.0.0.0/0'] }
])
server = aws.ec2.Instance('web-server',
ami='ami-6869aa05',
instance_type='t2.micro',
vpc_security_group_ids=[group.name] # reference the security group resource above
)
pulumi.export('public_ip', server.public_ip)
pulumi.export('public_dns', server.public_dns)
自定义操作符
Terraform 和 Pulumi 都支持 Kubernetes 并可以配置集群,但它们不是云原生的。它们也不支持动态一致性,这与 Kubernetes 模型的理念相悖。这意味着,如果有人删除或修改了由 Terraform 或 Pulumi 配置的某些基础设施,直到下次运行 Terraform/Pulumi 时才会被检测到。
编写自定义 Kubernetes 操作员让你拥有完全控制权。你可以暴露目标基础设施的配置表面,并且可以强制执行规则和默认配置。例如,在我当前的公司,我们曾经通过 Terraform 管理大量的 Cloudflare DNS 域。这导致了显著的性能问题,因为 Terraform 尝试通过向 Cloudflare 发出 API 请求来刷新所有这些域,任何基础设施的变化(即使与 Cloudflare 无关)。我们决定编写一个自定义 Kubernetes 操作员来管理这些域。该操作员定义了几个 CRD 来表示区域、域和记录,并通过 Cloudflare 的 API 进行交互。
除了完全控制和性能优势外,操作员还会自动协调任何外部变化,避免不小心的手动修改。
使用 Crossplane
自定义操作员非常强大,但编写和维护操作员需要大量的工作。Crossplane (crossplane.io) 将自己定位为你的基础设施控制平面。实际上,这意味着你通过 CRD 配置所有内容(提供程序、证书、资源和组合资源)。像数据库连接信息这样的基础设施凭证会写入 Kubernetes 秘密,这些凭证稍后可以被工作负载使用。Crossplane 操作员监视所有定义基础设施的自定义资源,并与基础设施提供商进行协调。
这里是定义 AWS RDS PostgresSQL 实例的一个示例:
apiVersion: database.example.org/v1alpha1
kind: PostgreSQLInstance
metadata:
name: the-db
namespace: data
spec:
parameters:
storageGB: 20
compositionSelector:
matchLabels:
provider: aws
vpc: default
writeConnectionSecretToRef:
name: db-conn
Crossplane 通过其自己的 CLI 扩展 kubectl,提供支持构建、推送和安装包的功能。
在本节中,我们介绍了 CI/CD 管道背后的概念以及在 Kubernetes 上配置基础设施的不同方法。
总结
在本章中,我们讨论了与部署和更新应用程序、扩展 Kubernetes 集群、管理资源、CI/CD 管道和配置基础设施相关的多个主题。我们讨论了实时集群更新、不同的部署模型、水平 Pod 自动扩展如何自动管理运行的 Pod 数量、如何在自动扩展的背景下正确且安全地执行滚动更新,以及如何通过资源配额处理稀缺资源。接着,我们讨论了 CI/CD 管道以及如何使用 Terraform、Pulumi、自定义操作员和 Crossplane 等工具在 Kubernetes 上配置基础设施。
到此为止,你已经很好地理解了 Kubernetes 集群在面对动态和增长的工作负载时的各种影响因素。你有多个工具可以选择,用于规划和设计自己的发布和扩展策略。
在下一章中,我们将学习如何将应用程序打包以便部署到 Kubernetes 上。我们将讨论 Helm、Kustomize 以及其他解决方案。
加入我们的 Discord!
与其他用户、云专家、作者以及志同道合的专业人士一起阅读本书。
提问、为其他读者提供解决方案、通过“问我任何问题”环节与作者交流,以及更多。
扫描二维码或访问链接立即加入社区。

第九章:打包应用程序
本章我们将探讨 Helm,这个流行的 Kubernetes 包管理工具。每一个成功且非平凡的平台都必须拥有一个良好的打包系统。Helm 是由 Deis 开发的(Deis 于 2017 年 4 月被微软收购),后来直接贡献给 Kubernetes 项目。它在 2018 年成为 CNCF 项目。我们将从理解 Helm 的动机、架构和组件开始。然后,我们将动手实践,看看如何在 Kubernetes 中使用 Helm 和其图表。这包括查找、安装、定制、删除和管理图表。最后,我们将讨论如何创建自己的图表以及如何处理版本控制、依赖关系和模板化。
我们将讨论的主题如下:
-
理解 Helm
-
使用 Helm
-
创建你自己的图表
-
Helm 替代品
理解 Helm
Kubernetes 提供了许多方法来在运行时组织和协调容器,但它缺乏将一组镜像高层次组织在一起的方式。这就是 Helm 的作用所在。在本节中,我们将讨论 Helm 的动机、架构和组件。我们将讨论 Helm 3。你可能仍然会在某些地方看到 Helm 2,但它的生命周期已经在 2020 年底结束。
如你所知,Kubernetes 在希腊语中意为舵手或导航员。Helm 项目非常重视这个航海主题,正如项目名称所暗示的那样。Helm 的主要概念是图表。就像航海图详细描述一个海域或沿海区域一样,Helm 图表详细描述了一个应用程序的所有部分。
Helm 的设计目的是执行以下操作:
-
从零开始构建图表
-
将图表打包成归档文件(
.tgz) -
与包含图表的仓库互动
-
在现有的 Kubernetes 集群中部署和移除图表
-
处理已安装图表的生命周期
Helm 的动机
Helm 支持几个重要的使用案例:
-
管理复杂性
-
简单升级
-
简单共享
-
安全回滚
图表可以定义最复杂的应用程序,提供一致的应用安装,并充当中央权限源。就地升级和自定义钩子允许轻松更新。共享图表非常简单,这些图表可以版本控制并托管在公共或私有服务器上。当你需要回滚最近的升级时,Helm 提供一个简单的命令来回滚基础设施的一整套更改。
Helm 3 架构
Helm 3 架构完全依赖于客户端工具,并将其状态作为 Kubernetes 秘密保存。Helm 3 包含几个组件:发布秘密、客户端和库。
客户端是命令行接口,通常 CI/CD 管道用于打包和安装应用程序。客户端利用 Helm 库执行请求的操作,且每个已部署应用程序的状态都存储在发布秘密中。
让我们回顾一下组件。
Helm 发布秘密
Helm 将其发布存储为目标命名空间中的 Kubernetes 密钥。这意味着你可以有多个同名的发布,只要它们存储在不同的命名空间中。以下是一个发布密钥的示例:
$ kubectl describe secret sh.helm.release.v1.prometheus.v1 -n monitoring
Name: sh.helm.release.v1.prometheus.v1
Namespace: monitoring
Labels: modifiedAt=1659855458
name=prometheus
owner=helm
status=deployed
version=1
Annotations: <none>
Type: helm.sh/release.v1
Data
====
release: 51716 bytes
数据被双重 Base64 编码并且经过 GZIP 压缩。
Helm 客户端
你需要在你的机器上安装 Helm 客户端。Helm 执行以下任务:
-
促进本地 chart 开发
-
管理仓库
-
监督发布
-
与 Helm 库进行交互:
-
部署新发布
-
升级现有发布
-
删除现有发布
-
Helm 库
Helm 库是 Helm 核心组件,负责执行所有繁重的任务。Helm 库与 Kubernetes API 服务器通信,并提供以下功能:
-
结合 Helm charts、模板和值文件来构建发布
-
将发布安装到 Kubernetes 中
-
创建发布对象
-
升级和卸载 charts
Helm 2 与 Helm 3 的区别
Helm 2 非常出色,并在 Kubernetes 生态系统中发挥了重要作用。但是,它的服务器端组件 Tiller 受到了很多批评。Helm 2 在 RBAC 成为官方访问控制方法之前设计和实现。在可用性方面,Tiller 默认安装时具有非常开放的权限集。要为生产环境锁定它并不容易,尤其是在多租户集群中。
Helm 团队听取了批评意见,提出了 Helm 3 设计。Helm 3 取代了集群内的 Tiller 组件,使用 Kubernetes API 服务器本身,通过 CRD 来管理发布的状态。最重要的是,Helm 3 是一个仅客户端程序。它仍然可以管理发布并执行与 Helm 2 相同的任务,但不需要安装服务器端组件。
这种方法更符合 Kubernetes 原生设计,且不那么复杂,安全性问题也得到了消除。Helm 用户只能根据其 kube 配置执行 Helm 允许的操作。
使用 Helm
Helm 是一个功能强大的包管理系统,它让你执行所有必要的步骤来管理集群中已安装的应用程序。让我们卷起袖子,开始吧。我们将一起安装 Helm 2 和 Helm 3,但在所有实际操作和演示中我们将使用 Helm 3。
安装 Helm
安装 Helm 涉及安装客户端和服务器。Helm 是用 Go 实现的。Helm 2 可作为客户端或服务器使用。如前所述,Helm 3 是一个仅客户端程序。
安装 Helm 客户端
你必须正确配置 Kubectl 才能与 Kubernetes 集群进行通信,因为 Helm 客户端使用 Kubectl 配置来与 Kubernetes API 服务器通信。
Helm 在这里提供所有平台的二进制发布:github.com/helm/helm/releases。
对于 Windows,Chocolatey(chocolatey.org)包管理器是最好的选择(通常是最新的):
choco install kubernetes-helm
对于 macOS 和 Linux,你可以通过脚本安装客户端:
$ curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3
$ chmod 700 get_helm.sh
$ ./get_helm.sh
在 macOS 上,你也可以使用 Homebrew(brew.sh):
brew install helm
$ helm version
version.BuildInfo{Version:"v3.9.2", GitCommit:"1addefbfe665c350f4daf868a9adc5600cc064fd", GitTreeState:"clean", GoVersion:"go1.18.4"}
查找 charts
要使用 Helm 安装有用的应用程序和软件,你需要先找到它们的 charts。Helm 是为了与多个 charts 仓库一起使用而设计的。Helm 2 默认配置为搜索 stable 仓库,但你可以添加其他仓库。Helm 3 没有默认仓库,但你可以搜索 Helm Hub(artifacthub.io)或特定的仓库。Helm Hub 于 2018 年 12 月发布,旨在简化发现 charts 和仓库的过程,这些仓库是托管在 stable 或 incubator 仓库之外的。
这就是helm search命令的作用。Helm 可以在 Helm Hub 中搜索特定的仓库。
目前,hub 中包含 9,053 个 charts:
$ helm search hub | wc -l
9053
我们可以在 hub 中搜索特定的关键词,如mariadb。这里是前 10 个 charts(总共有 38 个):
$ helm search hub mariadb --max-col-width 60 | head -n 10
URL CHART VERSION APP VERSION DESCRIPTION
https://artifacthub.io/packages/helm/cloudnativeapp/mariadb 6.1.0 10.3.15 Fast, reliable, scalable, and easy to use open-source rel...
https://artifacthub.io/packages/helm/riftbit/mariadb 9.6.0 10.5.12 Fast, reliable, scalable, and easy to use open-source rel...
https://artifacthub.io/packages/helm/bitnami/mariadb 11.1.6 10.6.8 MariaDB is an open source, community-developed SQL databa...
https://artifacthub.io/packages/helm/bitnami-aks/mariadb 11.1.5 10.6.8 MariaDB is an open source, community-developed SQL databa...
https://artifacthub.io/packages/helm/camptocamp3/mariadb 1.0.0 Fast, reliable, scalable, and easy to use open-source rel...
https://artifacthub.io/packages/helm/openinfradev/mariadb 0.1.1 OpenStack-Helm MariaDB
https://artifacthub.io/packages/helm/sitepilot/mariadb 1.0.3 10.6 MariaDB chart for the Sitepilot platform.
https://artifacthub.io/packages/helm/groundhog2k/mariadb 0.5.0 10.8.3 A Helm chart for MariaDB on Kubernetes
https://artifacthub.io/packages/helm/nicholaswilde/mariadb 1.0.6 110.4.21 The open source relational database
如你所见,有几个 charts 与关键词mariadb匹配。你可以进一步调查这些 charts,找到最适合你使用场景的。
添加仓库
默认情况下,Helm 3 没有设置任何仓库,因此你只能搜索 hub。过去,由 CNCF 托管的stable仓库是寻找 charts 的一个不错选择。但由于 CNCF 不想继续为其托管付费,现在该仓库只包含大量已弃用的 charts。
相反,你可以选择从 hub 安装 charts,或者进行一些研究并添加单独的仓库。例如,针对 Prometheus,有prometheus-community Helm 仓库。让我们添加它:
$ helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
"prometheus-community" has been added to your repositories
现在,我们可以搜索prometheus仓库:
$ helm search repo prometheus
NAME CHART VERSION APP VERSION DESCRIPTION test | default
prometheus-community/kube-prometheus-stack 39.4.1 0.58.0 kube-prometheus-stack collects Kubernetes manif...
prometheus-community/prometheus 15.12.0 2.36.2 Prometheus is a monitoring system and time seri...
prometheus-community/prometheus-adapter 3.3.1 v0.9.1 A Helm chart for k8s prometheus adapter
prometheus-community/prometheus-blackbox-exporter 6.0.0 0.20.0 Prometheus Blackbox Exporter
prometheus-community/prometheus-cloudwatch-expo... 0.19.2 0.14.3 A Helm chart for prometheus cloudwatch-exporter
prometheus-community/prometheus-conntrack-stats... 0.2.1 v0.3.0 A Helm chart for conntrack-stats-exporter
prometheus-community/prometheus-consul-exporter 0.5.0 0.4.0 A Helm chart for the Prometheus Consul Exporter
prometheus-community/prometheus-couchdb-exporter 0.2.0 1.0 A Helm chart to export the metrics from couchdb...
prometheus-community/prometheus-druid-exporter 0.11.0 v0.8.0 Druid exporter to monitor druid metrics with Pr...
prometheus-community/prometheus-elasticsearch-e... 4.14.0 1.5.0 Elasticsearch stats exporter for Prometheus
prometheus-community/prometheus-json-exporter 0.2.3 v0.3.0 Install prometheus-json-exporter
prometheus-community/prometheus-kafka-exporter 1.6.0 v1.4.2 A Helm chart to export the metrics from Kafka i...
prometheus-community/prometheus-mongodb-exporter 3.1.0 0.31.0 A Prometheus exporter for MongoDB metrics
prometheus-community/prometheus-mysql-exporter 1.9.0 v0.14.0 A Helm chart for prometheus mysql exporter with...
prometheus-community/prometheus-nats-exporter 2.9.3 0.9.3 A Helm chart for prometheus-nats-exporter
prometheus-community/prometheus-node-exporter 3.3.1 1.3.1 A Helm chart for prometheus node-exporter
prometheus-community/prometheus-operator 9.3.2 0.38.1 DEPRECATED - This chart will be renamed. See ht...
prometheus-community/prometheus-pingdom-exporter 2.4.1 20190610-1 A Helm chart for Prometheus Pingdom Exporter
prometheus-community/prometheus-postgres-exporter 3.1.0 0.10.1 A Helm chart for prometheus postgres-exporter
prometheus-community/prometheus-pushgateway 1.18.2 1.4.2 A Helm chart for prometheus pushgateway
prometheus-community/prometheus-rabbitmq-exporter 1.3.0 v0.29.0 Rabbitmq metrics exporter for prometheus
prometheus-community/prometheus-redis-exporter 5.0.0 1.43.0 Prometheus exporter for Redis metrics
prometheus-community/prometheus-snmp-exporter 1.1.0 0.19.0 Prometheus SNMP Exporter
prometheus-community/prometheus-stackdriver-exp... 4.0.0 0.12.0 Stackdriver exporter for Prometheus
prometheus-community/prometheus-statsd-exporter 0.5.0 0.22.7 A Helm chart for prometheus stats-exporter
prometheus-community/prometheus-to-sd 0.4.0 0.5.2 Scrape metrics stored in prometheus format and ...
prometheus-community/alertmanager 0.19.0 v0.23.0 The Alertmanager handles alerts sent by client ...
prometheus-community/kube-state-metrics 4.15.0 2.5.0 Install kube-state-metrics to generate and expo...
prometheus-community/prom-label-proxy 0.1.0 v0.5.0 A proxy that enforces a given label in a given ...
这里有相当多的 charts。如果你想获取关于某个特定 chart 的更多信息,我们可以使用show命令(你也可以使用inspectalias命令)。让我们来看一下prometheus-community/prometheus:
$ helm show chart prometheus-community/prometheus
apiVersion: v2
appVersion: 2.36.2
dependencies:
- condition: kubeStateMetrics.enabled
name: kube-state-metrics
repository: https://prometheus-community.github.io/helm-charts
version: 4.13.*
description: Prometheus is a monitoring system and time series database.
home: https://prometheus.io/
icon: https://raw.githubusercontent.com/prometheus/prometheus.github.io/master/assets/prometheus_logo-cb55bb5c346.png
maintainers:
- email: gianrubio@gmail.com
name: gianrubio
- email: zanhsieh@gmail.com
name: zanhsieh
- email: miroslav.hadzhiev@gmail.com
name: Xtigyro
- email: naseem@transit.app
name: naseemkullah
name: prometheus
sources:
- https://github.com/prometheus/alertmanager
- https://github.com/prometheus/prometheus
- https://github.com/prometheus/pushgateway
- https://github.com/prometheus/node_exporter
- https://github.com/kubernetes/kube-state-metrics
type: application
version: 15.12.0
你还可以要求 Helm 显示README文件、values,或者与 chart 相关的所有信息。有时这些信息可能会让人感到不知所措。
安装包
好的,你找到了理想的包。现在,你可能想把它安装到你的 Kubernetes 集群中。当你安装一个包时,Helm 会创建一个发布,你可以用它来跟踪安装进度。我们使用helm install命令在监控命名空间中安装prometheus,并指示 Helm 为我们创建命名空间:
$ helm install prometheus prometheus-community/prometheus -n monitoring --create-namespace
我们来看一下输出内容。输出的第一部分列出了我们提供的发布名称prometheus,它被部署的时间、命名空间和修订版本:
NAME: prometheus
LAST DEPLOYED: Sat Aug 6 23:54:50 2022
NAMESPACE: monitoring
STATUS: deployed
REVISION: 1
TEST SUITE: None
接下来的部分是自定义说明,这些内容可能会比较冗长。这里有关于如何连接到 Prometheus 服务器、告警管理器和Pushgateway的很多有用信息:
NOTES:
The Prometheus server can be accessed via port 80 on the following DNS name from within your cluster:
prometheus-server.default.svc.cluster.local
Get the Prometheus server URL by running these commands in the same shell: test | default
export POD_NAME=$(kubectl get pods --namespace default -l "app=prometheus,component=server" -o jsonpath="{.items[0].metadata.name}")
kubectl --namespace default port-forward $POD_NAME 9090
The Prometheus alertmanager can be accessed via port 80 on the following DNS name from within your cluster:
prometheus-alertmanager.default.svc.cluster.local
Get the Alertmanager URL by running these commands in the same shell:
export POD_NAME=$(kubectl get pods --namespace default -l "app=prometheus,component=alertmanager" -o jsonpath="{.items[0].metadata.name}")
kubectl --namespace default port-forward $POD_NAME 9093
#################################################################################
###### WARNING: Pod Security Policy has been moved to a global property. #####
###### use .Values.podSecurityPolicy.enabled with pod-based #####
###### annotations #####
###### (e.g. .Values.nodeExporter.podSecurityPolicy.annotations) #####
#################################################################################
The Prometheus PushGateway can be accessed via port 9091 on the following DNS name from within your cluster:
prometheus-pushgateway.default.svc.cluster.local
Get the PushGateway URL by running these commands in the same shell:
export POD_NAME=$(kubectl get pods --namespace default -l "app=prometheus,component=pushgateway" -o jsonpath="{.items[0].metadata.name}")
kubectl --namespace default port-forward $POD_NAME 9091
For more information on running Prometheus, visit:
https://prometheus.io/
检查安装状态
Helm 不会等待安装完成,因为这可能需要一段时间。helm status命令显示发布的最新信息,格式与初始helm install命令的输出相同。
如果你只关心状态,而不需要所有额外的信息,可以直接使用grep查找STATUS行:
$ helm status -n monitoring prometheus | grep STATUS
STATUS: deployed
让我们列出monitoring命名空间下的所有 Helm 发布,并验证prometheus是否在列出之中:
$ helm list -n monitoring
NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION
prometheus monitoring 1 2022-08-06 23:57:34.124225 -0700 PDT deployed prometheus-15.12.0 2.36.2
如你所记得,Helm 将发布信息存储在一个密钥中:
$ kubectl describe secret sh.helm.release.v1.prometheus.v1 -n monitoring
Name: sh.helm.release.v1.prometheus.v1
Namespace: monitoring
Labels: modifiedAt=1659855458
name=prometheus
owner=helm
status=deployed
version=1
Annotations: <none>
Type: helm.sh/release.v1
Data
====
release: 51716 bytes
如果你想找到所有命名空间中的所有 Helm 发布,可以使用:
$ helm list -A
如果你想深入了解,可以列出所有带有owner=helm标签的密钥:
$ kubectl get secret -A -l owner=helm
要从一个密钥中实际提取发布数据,你需要绕过一些障碍,因为它被 Base64 编码了两次(为什么?)并且经过了 GZIP 压缩。最终结果是 JSON 格式:
kubectl get secret sh.helm.release.v1.prometheus.v1 -n monitoring -o jsonpath='{.data.release}' | base64 --decode | base64 --decode | gunzip > prometheus.v1.json
你可能还对仅提取清单感兴趣,可以使用以下命令:
kubectl get secret sh.helm.release.v1.prometheus.v1 -n monitoring -o jsonpath='{.data.release}' | base64 --decode | base64 --decode | gunzip | jq .manifest -r
自定义图表
作为用户,你很可能希望自定义或配置你安装的图表。Helm 完全支持通过配置文件进行自定义。要了解可能的自定义选项,你可以再次使用helm show命令,但这次重点关注values。对于像 Prometheus 这样复杂的项目,values文件可能非常庞大:
$ helm show values prometheus-community/prometheus | wc -l
1901
以下是部分输出:
$ helm show values prometheus-community/prometheus | head -n 20
rbac:
create: true
podSecurityPolicy:
enabled: false
imagePullSecrets:
# - name: "image-pull-secret"
## Define serviceAccount names for components. Defaults to component's fully qualified name.
##
serviceAccounts:
alertmanager:
create: true
name:
annotations: {}
nodeExporter:
create: true
name:
annotations: {}
被注释掉的行通常包含默认值,比如imagePullSecrets的名称:
imagePullSecrets:
# - name: "image-pull-secret"
如果你想自定义 Prometheus 安装的任何部分,可以将值保存到一个文件中,做出任何修改,然后使用自定义值文件安装 Prometheus:
$ helm install prometheus prometheus-community/prometheus --create-namespace -n monitoring -f custom-values.yaml
你也可以在命令行上使用--set设置单个值。如果-f和--set都尝试设置相同的值,那么--set优先。你可以使用逗号分隔的列表来指定多个值:--set a=1,b=2。嵌套的值可以通过--set outer.inner=value来设置。
额外的安装选项
helm install命令可以与多种来源一起工作:
-
图表仓库(如展示所示)
-
本地图表归档(
helm install foo-0.1.1.tgz) -
一个提取的图表目录(
helm install path/to/foo) -
完整的 URL(
helm install https://example.com/charts/foo-1.2.3.tgz)
升级和回滚发布
你可能想要将已安装的包升级到最新的版本。Helm 提供了upgrade命令,它智能地操作,只更新发生变化的部分。例如,让我们检查一下当前prometheus安装的值:
$ helm get values prometheus -n monitoring
USER-SUPPLIED VALUES:
null
到目前为止,我们还没有提供任何用户值。作为默认安装的一部分,prometheus安装了一个告警管理器组件:
$ k get deploy prometheus-alertmanager -n monitoring
NAME READY UP-TO-DATE AVAILABLE AGE
prometheus-alertmanager 1/1 1 1 19h
让我们通过升级并传入一个新值来禁用告警管理器:
$ helm upgrade --set alertmanager.enabled=false \
prometheus prometheus-community/prometheus \
-n monitoring
Release "prometheus" has been upgraded. Happy Helming!
NAME: prometheus
LAST DEPLOYED: Sun Aug 7 19:55:52 2022
NAMESPACE: monitoring
STATUS: deployed
REVISION: 2
TEST SUITE: None
NOTES:
...
升级成功完成。我们可以看到输出中不再提到如何获取告警管理器的 URL。让我们验证一下告警管理器的部署是否被移除:
$ k get deployment -n monitoring
NAME READY UP-TO-DATE AVAILABLE AGE
prometheus-kube-state-metrics 1/1 1 1 20h
prometheus-pushgateway 1/1 1 1 20h
prometheus-server 1/1 1 1 20h
现在,如果我们检查自定义的值,我们可以看到我们的修改:
$ helm get values prometheus -n monitoring
USER-SUPPLIED VALUES:
alertmanager:
enabled: false
假设我们决定警报其实挺重要的,实际上我们希望拥有 Prometheus 警报管理器。没问题,我们可以回滚到最初的安装版本。helm history命令会展示所有我们可以回滚的可用修订版:
$ helm history prometheus -n monitoring
REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION
1 Sat Aug 6 23:57:34 2022 superseded prometheus-15.12.0 2.36.2 Install complete
2 Sun Aug 7 19:55:52 2022 deployed prometheus-15.12.0 2.36.2 Upgrade complete
让我们回滚到修订版 1:
$ helm rollback prometheus 1 -n monitoring
Rollback was a success! Happy Helming!
$ helm history prometheus -n monitoring
REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION
REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION
1 Sat Aug 6 23:57:34 2022 superseded prometheus-15.12.0 2.36.2 Install complete
2 Sun Aug 7 19:55:52 2022 superseded prometheus-15.12.0 2.36.2 Upgrade complete
3 Sun Aug 7 20:02:30 2022 deployed prometheus-15.12.0 2.36.2 Rollback to 1
如你所见,回滚实际上创建了一个新的修订号 3。修订版 2 仍然存在,以防我们想要回到它。
让我们验证一下我们的更改是否已经回滚:
$ k get deployment -n monitoring
NAME READY UP-TO-DATE AVAILABLE AGE
prometheus-alertmanager 1/1 1 1 152m
prometheus-kube-state-metrics 1/1 1 1 22h
prometheus-pushgateway 1/1 1 1 22h
prometheus-server 1/1 1 1 22h
是的,警报管理器已经恢复。
删除发布
当然,你也可以使用helm uninstall命令卸载发布。
首先,让我们检查发布列表。我们只有一个prometheus发布在监控命名空间中:
$ helm list -n monitoring
NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION
prometheus monitoring 3 2022-08-07 20:02:30.270229 -0700 PDT deployed prometheus-15.12.0 2.36.2
现在,让我们卸载它。你可以使用以下任一等效命令:
-
uninstall -
un -
delete -
del
在这里我们使用了uninstall命令:
$ helm uninstall prometheus -n monitoring
release "prometheus" uninstalled
到此为止,没有更多的发布了:
$ helm list -n monitoring
NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION
Helm 也可以跟踪未安装的发布。如果你在卸载时提供了--keep-history,那么你可以通过添加--all或--uninstalled标志到helm list命令来查看未安装的发布。
请注意,监控命名空间仍然存在,尽管它是 Helm 在安装 Prometheus 时创建的,但现在它是空的:
$ k get all -n monitoring
No resources found in monitoring namespace.
使用仓库
Helm 将图表存储在简单的 HTTP 服务器上的仓库中。任何标准的 HTTP 服务器都可以托管 Helm 仓库。在云中,Helm 团队验证了 AWS S3 和 Google Cloud Storage 都可以作为启用 Web 的 Helm 仓库。你甚至可以将 Helm 仓库存储在 GitHub Pages 上。
请注意,Helm 不提供上传图表到远程仓库的工具,因为这需要远程服务器理解 Helm、知道如何放置图表以及如何更新index.yaml文件。
请注意,Helm 最近添加了实验性支持,将 Helm 图表存储在 OCI 注册表中。有关更多详情,请查看helm.sh/docs/topics/registries/。
在客户端,helm repo命令可以让你列出、添加、删除、索引和更新:
$ helm repo
This command consists of multiple subcommands to interact with chart repositories.
It can be used to add, remove, list, and index chart repositories.
Usage:
helm repo [command]
Available Commands:
add add a chart repository
index generate an index file given a directory containing packaged charts
list list chart repositories
remove remove one or more chart repositories
update update information of available charts locally from chart repositories
我们之前已经使用过helm repo add和helm repo list命令。让我们看看如何创建自己的图表并管理它们。
使用 Helm 管理图表
Helm 提供了多个命令来管理图表。
它可以为你创建一个新的图表:
$ helm create cool-chart
Creating cool-chart
Helm 会在cool-chart下创建以下文件和目录:
$ tree cool-chart
cool-chart
├── Chart.yaml
├── charts
├── templates
│ ├── _helpers.tpl
│ ├── deployment.yaml
│ ├── hpa.yaml
│ ├── ingress.yaml
│ ├── NOTES.txt
│ ├── service.yaml
│ ├── serviceaccount.yaml
│ └── tests
│ └── test-connection.yaml
一旦你编辑了你的图表,你可以将其打包成一个tar.gz归档文件:
$ helm package cool-chart
Successfully packaged chart and saved it to: cool-chart-0.1.0.tgz
Helm 会创建一个名为cool-chart-0.1.0.tgz的归档文件,并将其存储在本地目录中。
你还可以使用helm lint来帮助你找到图表格式或信息中的问题:
$ helm lint cool-chart
==> Linting cool-chart
[INFO] Chart.yaml: icon is recommended
1 chart(s) linted, 0 chart(s) failed
利用启动包
helm create 命令提供了一个可选的 --starter 标志,允许你指定一个启动图表。启动图表是位于 $XDG_DATA_HOME/helm/starters 中的常规图表。作为图表开发者,你可以创建专门用作新图表模板的图表。在开发这类图表时,请牢记以下注意事项:
-
启动图表中的 YAML 内容将被生成器覆盖。
-
用户通常会修改启动图表的内容,因此提供清晰的文档以解释用户如何进行修改至关重要。
当前,没有内置机制用于安装启动图表。将图表添加到 $XDG_DATA_HOME/helm/starters 的唯一方法是通过手动复制。如果你创建了启动包图表,请确保图表的文档中明确提到这一要求。
创建你自己的图表
图表表示一组定义 Kubernetes 资源的文件。它可以是一个简单的 Memcached pod 部署,也可以是一个复杂的 Web 应用堆栈配置,包括 HTTP 服务器、数据库、缓存、队列等。
为了组织图表,其文件在特定的目录结构中进行存储。然后,这些文件可以打包成带版本的归档文件,便于部署和管理。关键文件是 Chart.yaml。
Chart.yaml 文件
Chart.yaml 文件是 Helm 图表的主要文件。它需要包含名称和版本字段:
-
apiVersion:图表的 API 版本。 -
name:图表的名称,应该与目录名称匹配。 -
version:图表的版本,使用SemVer2 格式。
此外,Chart.yaml 文件中可以包含几个可选字段:
-
kubeVersion:以SemVer格式指定的兼容 Kubernetes 版本范围。 -
description:项目的简短描述,一句话概括。 -
keywords:与项目相关的关键字列表。 -
home:项目主页的 URL。 -
sources:项目源代码的 URL 列表。 -
dependencies:图表的依赖项列表,包括名称、版本、仓库、条件、标签和别名。 -
maintainers:图表维护者的列表,包括姓名、电子邮件和网址。 -
icon:可用作图标的 SVG 或 PNG 图片的 URL。 -
appVersion:图表中包含的应用程序的版本。它不需要遵循SemVer。 -
deprecated:布尔值,指示图表是否已弃用。 -
annotations:提供额外信息的附加键值对。
图表版本控制
Chart.yaml 文件中的 version 字段对各种 Helm 工具有着至关重要的作用。在创建包时,它会被 helm package 命令使用,因为该命令会根据 Chart.yaml 中指定的版本构建包的名称。确保包名称中的版本号与 Chart.yaml 文件中的版本号一致非常重要。偏离这一预期可能会导致错误,因为系统会假设这些版本号的一致性。因此,必须保持 Chart.yaml 文件中的版本字段与生成的包名称之间的一致性,以避免出现问题。
appVersion 字段
可选的 appVersion 字段与 version 字段无关。它不被 Helm 使用,而是作为元数据或文档,供希望了解自己正在部署什么的用户参考。Helm 会忽略它。
弃用图表
有时,您可能希望弃用一个图表。您可以通过将 Chart.yaml 中的可选 deprecated 字段设置为 true 来标记该图表为弃用。仅弃用图表的最新版本就足够了。您以后可以重新使用图表名称,并发布一个不再弃用的较新版本。弃用图表的工作流程通常包括以下步骤:
-
更新 Chart.yaml 文件:修改图表的
Chart.yaml文件,标明该图表已弃用。可以通过添加deprecated字段并将其设置为true来实现。此外,通常做法是更新图表的版本号,以指示已发布包含弃用信息的新版本。 -
发布新版本:将更新后的图表与弃用信息一起打包并发布到图表仓库。这可以确保用户在尝试安装或升级图表时了解弃用信息。
-
沟通弃用信息:与用户沟通弃用信息并提供替代选项或推荐的迁移路径非常重要。可以通过文档、发布说明或其他渠道来实现,确保用户了解弃用信息,并能相应地进行计划。
-
从源代码仓库中移除图表:一旦弃用的图表已发布并通知用户,建议从源代码仓库中移除该图表,例如 Git 仓库,以避免混淆,并确保用户被引导到图表仓库中的最新版本。
通过遵循这些步骤,您可以有效地弃用一个图表,并为用户提供一个清晰的流程,以便他们过渡到更新的版本或替代方案。
图表元数据文件
图表可以包含多个元数据文件,例如 README.md、LICENSE 和 NOTES.txt,这些文件提供有关图表的重要信息。格式化为 Markdown 的 README.md 文件尤为重要,应该包含以下详细信息:
-
应用程序或服务描述:提供图表所代表的应用程序或服务的清晰简明描述。这个描述应帮助用户理解图表的目的和功能。
-
先决条件和要求:指定在使用图表之前需要满足的任何先决条件或要求。这可能包括特定版本的 Kubernetes、所需的依赖项或必须满足的其他条件。
-
YAML 选项和默认值:记录用户可以在图表的 YAML 文件中配置的可用选项。描述每个选项、其目的、接受的值或格式,以及默认值。这些信息使用户能够根据自己的需求定制图表。
-
安装和配置说明:提供有关如何安装和配置图表的清晰说明。这可能涉及指定命令行选项或 Helm 命令来部署图表,以及配置过程中的任何其他步骤或注意事项。
-
附加信息:包括任何其他相关信息,以帮助用户在安装或配置图表时。可能包括最佳实践、故障排除提示或已知的限制。
通过在README.md文件中包含这些细节,图表用户可以轻松理解图表的目的、要求,以及如何有效地安装和配置图表以适应他们的特定用例。
如果图表包含模板或 NOTES.txt 文件,则该文件将在安装后以及查看发布状态或升级时显示并打印。备注应简洁,以避免冗余,并指向 README.md 文件以获取详细说明。通常将使用说明和后续步骤放在 NOTES.txt 中。请记住,该文件作为模板进行评估。备注将在您运行 helm install 和 helm status 时显示在屏幕上。
管理图表依赖项
在 Helm 中,一个图表可能依赖于其他图表。这些依赖项通过在 Chart.yaml 文件的 dependencies 字段中显式列出,或直接复制到 charts/ 子目录中来表达。这提供了一种充分利用并重用他人知识和工作的好方法。Helm 中的依赖项可以是图表归档(例如 foo-1.2.3.tgz)或解压后的图表目录。然而,需要注意的是,依赖项的名称不应以下划线(_)或点(.)开头,因为这些文件会被图表加载器忽略。因此,建议避免以这些字符开头命名依赖项,以确保它们被 Helm 正确识别并加载。
让我们将来自 prometheus-community 仓库的 kube-state-metrics 添加为我们 cool-chart 的依赖项到 Chart.yaml 文件中:
dependencies:
- name: kube-state-metrics
version: "4.13.*"
repository: https://prometheus-community.github.io/helm-charts
condition: kubeStateMetrics.enabled
name 字段表示您希望安装的图表的名称。它应与仓库中定义的图表名称匹配。
version字段指定你想要安装的图表的具体版本。它有助于确保你获得所需版本的图表。
repository字段包含图表仓库的完整 URL,图表将从该仓库获取。它指向存储图表及其版本的位置,并且可以访问。
condition字段将在后续章节中讨论。
如果仓库尚未添加,请使用helm repo将其添加到本地。
一旦定义了依赖项,你可以运行helm dependency update命令。Helm 会将所有指定的图表下载到charts子目录中:
$ helm dep up cool-chart
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "prometheus-community" chart repository
Update Complete. Happy Helming!
Saving 1 charts
Downloading kube-state-metrics from repo https://prometheus-community.github.io/helm-charts
Deleting outdated charts
Helm 将依赖图表存储为charts/目录中的档案:
$ ls cool-chart/charts
kube-state-metrics-4.13.0.tgz
在Chart.yaml依赖字段中管理图表及其依赖项(而不是仅将图表复制到charts/子目录中)是一种最佳实践。它明确记录了依赖项,促进了团队间的共享,并支持自动化流水线。
使用依赖字段的附加子字段
requirements.yaml文件中的每个requirements条目可能包括可选字段,如tags和condition。
这些字段可用于动态控制图表的加载(如果未指定,则会加载所有图表)。如果存在tags或condition字段,Helm 将对其进行评估,并确定目标图表是否应加载。
-
图表依赖中的
condition字段包含一个或多个以逗号分隔的 YAML 路径。这些路径引用了顶级父图表值文件中的值。如果路径存在并且评估为布尔值,它将决定是否启用或禁用该图表。如果提供了多个路径,则仅评估第一个有效的路径。如果没有路径存在,则条件不起作用,图表将始终被加载。 -
tags字段允许你将标签与图表关联。它是一个 YAML 列表,你可以指定一个或多个标签。在顶级父图表的值文件中,你可以通过指定标签和相应的布尔值来启用或禁用所有具有特定标签的图表。这提供了一种方便的方式来基于关联标签管理和控制图表。
这是一个示例dependencies字段和一个values.yaml,它充分利用了条件和标签来启用或禁用依赖项的安装。dependencies字段根据全局启用字段的值和特定子图的启用字段定义了两个安装其依赖项的条件:
dependencies:
- name: subchart1
repository: http://localhost:10191
version: 0.1.0
condition: subchart1.enabled, global.subchart1.enabled
tags:
- front-end
- subchart1
- name: subchart2
repository: http://localhost:10191
version: 0.1.0
condition: subchart2.enabled,global.subchart2.enabled
tags:
- back-end
- subchart2
values.yaml文件为一些condition变量分配了值。subchart2标签没有值,因此它会自动启用:
# parentchart/values.yaml
subchart1:
enabled: true
tags:
front-end: false
back-end: true
你也可以在安装图表时从命令行设置tags和condition值,这些值会优先于values.yaml文件:
$ helm install --set subchart2.enabled=false
标签和条件的解析如下:
-
在值中设置的条件会覆盖标签。每个图表中第一个存在的
condition路径会生效,其他条件将被忽略。 -
如果与图表关联的任何标签在顶级父项的值中设置为
true,该图表就被认为是启用的。 -
tags和condition值必须在值文件的顶层设置。 -
当前不支持在全局配置中嵌套标签表或标签。这意味着标签应该直接位于顶层父项的值下,而不是嵌套在其他结构中。
使用模板和值
任何非平凡的应用都需要配置并根据特定的用例进行调整。Helm 图表是使用 Go 模板语言填充占位符的模板。Helm 支持来自 Sprig 库的附加函数,该库包含许多有用的助手函数以及其他几种专用函数。模板文件存储在图表的templates/子目录中。Helm 会使用模板引擎渲染此目录中的所有文件并应用提供的值文件。
编写模板文件
模板文件仅仅是遵循 Go 模板语言规则的文本文件。它们可以生成 Kubernetes 配置文件以及任何其他文件。以下是 Prometheus 服务器的service.yaml模板文件,来自prometheus-community库:
{{- if and .Values.server.enabled .Values.server.service.enabled -}}
apiVersion: v1
kind: Service
metadata:
{{- if .Values.server.service.annotations }}
annotations:
{{ toYaml .Values.server.service.annotations | indent 4 }}
{{- end }}
labels:
{{- include "prometheus.server.labels" . | nindent 4 }}
{{- if .Values.server.service.labels }}
{{ toYaml .Values.server.service.labels | indent 4 }}
{{- end }}
name: {{ template "prometheus.server.fullname" . }}
{{ include "prometheus.namespace" . | indent 2 }}
spec:
{{- if .Values.server.service.clusterIP }}
clusterIP: {{ .Values.server.service.clusterIP }}
{{- end }}
{{- if .Values.server.service.externalIPs }}
externalIPs:
{{ toYaml .Values.server.service.externalIPs | indent 4 }}
{{- end }}
{{- if .Values.server.service.loadBalancerIP }}
loadBalancerIP: {{ .Values.server.service.loadBalancerIP }}
{{- end }}
{{- if .Values.server.service.loadBalancerSourceRanges }}
loadBalancerSourceRanges:
{{- range $cidr := .Values.server.service.loadBalancerSourceRanges }}
- {{ $cidr }}
{{- end }}
{{- end }}
ports:
- name: http
port: {{ .Values.server.service.servicePort }}
protocol: TCP
targetPort: 9090
{{- if .Values.server.service.nodePort }}
nodePort: {{ .Values.server.service.nodePort }}
{{- end }}
{{- if .Values.server.service.gRPC.enabled }}
- name: grpc
port: {{ .Values.server.service.gRPC.servicePort }}
protocol: TCP
targetPort: 10901
{{- if .Values.server.service.gRPC.nodePort }}
nodePort: {{ .Values.server.service.gRPC.nodePort }}
{{- end }}
{{- end }}
selector:
{{- if and .Values.server.statefulSet.enabled .Values.server.service.statefulsetReplica.enabled }}
statefulset.kubernetes.io/pod-name: {{ template "prometheus.server.fullname" . }}-{{ .Values.server.service.statefulsetReplica.replica }}
{{- else -}}
{{- include "prometheus.server.matchLabels" . | nindent 4 }}
{{- if .Values.server.service.sessionAffinity }}
sessionAffinity: {{ .Values.server.service.sessionAffinity }}
{{- end }}
{{- end }}
type: "{{ .Values.server.service.type }}"
{{- end -}}
它可以在这里找到:github.com/prometheus-community/helm-charts/blob/main/charts/prometheus/templates/service.yaml。
如果它看起来有些混乱,不必担心。基本的思路是,你有一个简单的文本文件,其中包含一些占位符值,这些值可以通过不同的方式在后续进行填充,还有一些条件、函数和管道可以应用于这些值。
使用管道和函数
Helm 允许在模板文件中使用丰富且复杂的语法,借助内建的 Go 模板函数、Sprig 函数和管道。这里是一个模板示例,它利用了这些功能。它使用了repeat、quote和upper函数处理food和drink键,并通过管道将多个函数链在一起:
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
greeting: "Hello World"
drink: {{ .Values.favorite.drink | repeat 3 | quote }}
food: {{ .Values.favorite.food | upper }}
让我们添加一个values.yaml文件:
favorite:
drink: coffee
food: pizza
测试和故障排除你的图表
现在,我们可以使用helm template命令查看结果:
$ helm template food food-chart
---
# Source: food-chart/templates/config-map.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: food-configmap
data:
greeting: "Hello World"
drink: "coffeecoffeecoffee"
food: PIZZA
如你所见,我们的模板化工作成功了。饮料coffee被重复了三次并加上了引号,食物pizza被转为大写PIZZA(未加引号)。
另一种调试的好方法是使用install命令并加上--dry-run标志。这将提供更多的额外信息:
$ helm install food food-chart --dry-run -n monitoring
NAME: food
LAST DEPLOYED: Mon Aug 8 00:24:03 2022
NAMESPACE: monitoring
STATUS: pending-install
REVISION: 1
TEST SUITE: None
HOOKS:
MANIFEST:
---
# Source: food-chart/templates/config-map.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: food-configmap
data:
greeting: "Hello World"
drink: "coffeecoffeecoffee"
food: PIZZA
你还可以在命令行中覆盖这些值:
$ helm template food food-chart --set favorite.drink=water
---
# Source: food-chart/templates/config-map.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: food-configmap
data:
greeting: "Hello World"
drink: "waterwaterwater"
food: PIZZA
最终的测试,当然,是将你的图表安装到你的集群中。你无需将图表上传到图表仓库进行测试;只需在本地运行helm install命令即可:
$ helm install food food-chart -n monitoring
NAME: food
LAST DEPLOYED: Mon Aug 8 00:25:53 2022
NAMESPACE: monitoring
STATUS: deployed
REVISION: 1
TEST SUITE: None
现在,已经有一个名为food的 Helm 发布:
$ helm list -n monitoring
NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION
food monitoring 1 2022-08-08 00:25:53.587342 -0700 PDT deployed food-chart-0.1.0 1.16.0
最重要的是,food-configmap 配置映射已使用正确的数据创建:
$ k get cm food-configmap -o yaml -n monitoring
apiVersion: v1
data:
drink: coffeecoffeecoffee
food: PIZZA
greeting: Hello World
kind: ConfigMap
metadata:
annotations:
meta.helm.sh/release-name: food
meta.helm.sh/release-namespace: monitoring
creationTimestamp: "2022-08-08T07:25:54Z"
labels:
app.kubernetes.io/managed-by: Helm
name: food-configmap
namespace: monitoring
resourceVersion: "4247163"
uid: ada4957d-bd6d-4c2e-8b2c-1499ca74a3c3
嵌入内置对象
Helm 提供了一些内置对象,你可以在模板中使用。在上面的 Prometheus 图表模板中,Release.Name、Release.Service、Chart.Name 和 Chart.Version 是 Helm 预定义值的例子。其他对象包括:
-
Values -
Chart -
Template -
Files -
Capabilities
Values 对象包含在 values 文件或命令行中定义的所有值。Chart 对象是 Chart.yaml 的内容。Template 对象包含有关当前模板的信息。Files 和 Capabilities 是类似映射的对象,通过各种函数访问非专门文件和有关 Kubernetes 集群的一般信息。
请注意,Chart.yaml 中的未知字段会被模板引擎忽略,不能用来向模板传递任意结构化数据。
从文件中传递值
以下是 Prometheus 服务器默认 values 文件的一部分。该文件中的值用于填充多个模板。它们表示默认值,你可以通过复制该文件并修改以适应你的需求来覆盖这些默认值。注意文件中的有用注释,它们解释了每个值的目的和各种选项:
server:
## Prometheus server container name
##
enabled: true
## Use a ClusterRole (and ClusterRoleBinding)
## - If set to false - we define a RoleBinding in the defined namespaces ONLY
##
## NB: because we need a Role with nonResourceURL's ("/metrics") - you must get someone with Cluster-admin privileges to define this role for you, before running with this setting enabled.
## This makes prometheus work - for users who do not have ClusterAdmin privs, but wants prometheus to operate on their own namespaces, instead of clusterwide.
##
## You MUST also set namespaces to the ones you have access to and want monitored by Prometheus.
##
# useExistingClusterRoleName: nameofclusterrole
## namespaces to monitor (instead of monitoring all - clusterwide). Needed if you want to run without Cluster-admin privileges.
# namespaces:
# - yournamespace
name: server
# sidecarContainers - add more containers to prometheus server
# Key/Value where Key is the sidecar `- name: <Key>`
# Example:
# sidecarContainers:
# webserver:
# image: nginx
sidecarContainers: {}
这就是深入了解如何使用 Helm 创建自定义图表的内容。实际上,Helm 在 Kubernetes 应用程序的打包和部署中被广泛使用。然而,Helm 并不是唯一的选择。还有一些不错的替代方案,可能会更符合你的需求。在接下来的部分中,我们将回顾一些最具前景的 Helm 替代方案。
Helm 替代方案
Helm 已经经过战斗测试,并且在 Kubernetes 世界中非常常见,但它也有一些缺点和批评,尤其是在你自己开发图表时。很多批评集中在 Helm 2 及其服务器端组件 Tiller 上。然而,Helm 3 并不是万能的。在大规模的环境中,当你开发自己的图表和包含大量条件逻辑的复杂模板时,再加上庞大的 values 文件,管理起来会变得非常具有挑战性。
如果你感到困难,可能想要探索一些替代方案。需要注意的是,这些项目大多数都专注于部署方面。Helm 的依赖管理仍然是其优势之一。让我们看看一些有趣的项目,你或许可以考虑使用它们。
Kustomize
Kustomize 是一种替代 YAML 模板化的工具,它通过在原始 YAML 文件之上使用覆盖层的概念来实现。它在 Kubernetes 1.14 中被添加到 kubectl。
查看 github.com/kubernetes-sigs/kustomize。
Cue
Cue 是一个非常有趣的项目。它的数据验证语言和推理受到了逻辑编程的强烈启发。它不是一种通用编程语言,而是专注于数据验证、数据模板化、配置、查询和代码生成,但也包含一些脚本功能。Cue 的主要概念是类型和数据的统一。这使得 Cue 拥有强大的表达能力,并且不再需要像枚举(enums)和泛型(generics)这样的构造。
请参阅 cuelang.org。
这里有一个关于用 Cue 替代 Helm 的具体讨论:github.com/cue-lang/cue/discussions/1159。
kapp-controller
kapp-controller 为 Kubernetes 提供了持续交付和包管理功能。
它的声明式 API 和分层方法使你能够有效地构建、部署和管理应用程序。使用 Kapp-controller,你可以将软件打包成可分发的包,并帮助用户无缝地在 Kubernetes 集群上发现、配置和安装这些包。
请参阅 carvel.dev/kapp-controller/。
这就是我们对 Helm 替代方案的简要回顾。
总结
在本章中,我们介绍了 Helm,这是一款流行的 Kubernetes 包管理器。Helm 赋予 Kubernetes 管理由多个相互依赖的 Kubernetes 资源组成的复杂软件的能力。它的作用类似于操作系统的包管理器。Helm 组织包,允许你搜索图表(charts),安装和升级图表,并与协作者共享图表。你还可以开发自己的图表并将它们存储在仓库中。Helm 3 是一个仅限客户端的解决方案,使用 Kubernetes secrets 来管理集群中发布的状态。我们还介绍了一些 Helm 的替代方案。
到此为止,你应该已经理解了 Helm 在 Kubernetes 生态系统和社区中的重要角色。你应该能够高效使用 Helm,甚至开发并共享自己的图表。
在下一章,我们将讨论 Kubernetes 如何在较低层次上进行网络配置。
第十章:探索 Kubernetes 网络
在本章中,我们将探讨网络的重要话题。Kubernetes 作为一个编排平台,管理运行在不同机器(物理机或虚拟机)上的容器/pod,并要求一个明确的网络模型。我们将讨论以下主题:
-
理解 Kubernetes 网络模型
-
Kubernetes 网络插件
-
Kubernetes 与 eBPF
-
Kubernetes 网络解决方案
-
有效使用网络策略
-
负载均衡选项
到本章结束时,您将理解 Kubernetes 对网络的处理方法,并熟悉诸如标准接口、网络实现和负载均衡等方面的解决方案。您甚至可以在愿意的情况下编写您自己的 容器网络接口 (CNI) 插件。
理解 Kubernetes 网络模型
Kubernetes 网络模型基于一个扁平的地址空间。集群中的所有 pod 可以直接互相访问。每个 pod 都有自己的 IP 地址,且无需配置任何 网络地址转换 (NAT) 。此外,同一个 pod 中的容器共享 pod 的 IP 地址,并可以通过 localhost 互相通信。这个模型非常具有指导性,但一旦设置好,它能大大简化开发者和管理员的工作。它特别有助于将传统网络应用迁移到 Kubernetes。一个 pod 代表传统的节点,每个容器代表传统的进程。
我们将覆盖以下内容:
-
容器内通信
-
Pod 到服务的通信
-
外部访问
-
查找与发现
-
Kubernetes 中的 DNS
容器内通信(容器到容器)
一个运行中的 pod 总是调度在一个(物理或虚拟)节点上。这意味着所有容器都运行在同一个节点上,并可以通过多种方式互相通信,比如通过本地文件系统、任何 IPC 机制,或使用 localhost 和常见端口。不同 pod 之间不存在端口冲突的风险,因为每个 pod 都有自己的 IP 地址,而当 pod 中的容器使用 localhost 时,仅适用于 pod 的 IP 地址。因此,如果 pod 1 中的容器 1 连接到端口 1234,而容器 2 在 pod 1 上监听该端口,它不会与运行在同一节点上并在端口 1234 上监听的 pod 2 中的另一个容器冲突。唯一的警告是,如果您将端口暴露给主机,则应注意 pod 到节点的亲和性。这可以通过多种机制来处理,例如 Daemonsets 和 pod 反亲和性。
容器间通信(pod 到 pod)
Kubernetes 中的 Pods 被分配了一个网络可见的 IP 地址(与节点私有地址不同)。Pods 可以直接进行通信,无需 NAT、隧道、代理或任何其他遮蔽层的帮助。可以使用知名端口号实现无需配置的通信方案。Pod 的内部 IP 地址与其外部 IP 地址相同,外部 IP 地址是其他 Pods 看到的地址(仅限集群网络内;不向外界暴露)。这意味着像域名系统(DNS)这样的标准命名和发现机制可以开箱即用。
Pod 与服务的通信
Pods 可以通过其 IP 地址和知名端口直接相互通信,但这要求 Pods 知道彼此的 IP 地址。在 Kubernetes 集群中,Pods 可能会不断被销毁和创建,也可能会有多个副本,每个副本都有自己的 IP 地址。Kubernetes 服务资源提供了一层间接性,非常有用,因为即使响应请求的实际 Pods 集合发生变化,服务仍然是稳定的。此外,由于每个节点上的 kube-proxy 负责将流量重定向到正确的 Pod,因此你还可以获得自动的、高可用的负载均衡:

图 10.1:使用服务进行的内部负载均衡外部访问
最终,某些容器需要可以从外部访问。Pod 的 IP 地址对外部不可见。服务是合适的载体,但外部访问通常需要两次重定向。例如,云提供商的负载均衡器并不理解 Kubernetes,因此它们不能直接将流量导向运行可以处理请求的 Pod 的节点。相反,公共负载均衡器会将流量导向集群中的任何节点,而该节点上的 kube-proxy 会将流量重定向到适当的 Pod(如果当前节点没有运行所需的 Pod)。
以下图示展示了外部负载均衡器如何将流量发送到任意节点,kube-proxy 在需要时负责进一步的路由:

图 10.2:外部负载均衡器将流量发送到任意节点,并由 kube-proxy 处理
查找与发现
为了使 Pods 和容器能够相互通信,它们需要能够找到对方。容器可以通过多种方式定位其他容器或宣布自己的存在,接下来的子章节将讨论这些方式。每种方法都有其优缺点。
自我注册
我们已经提到过自注册几次了。让我们准确了解一下它的含义。当一个容器运行时,它知道自己的 pod 的 IP 地址。每个希望被其他集群容器访问的容器,都可以连接到某个注册服务并注册自己的 IP 地址和端口。其他容器可以查询注册服务,获取所有已注册容器的 IP 地址和端口,并与之连接。当一个容器被销毁(优雅地)时,它将注销自己。如果一个容器异常终止,则需要建立某种机制来检测这种情况。例如,注册服务可以定期对所有已注册容器进行 ping 检测,或者要求容器定期向注册服务发送保持活动消息。
自注册的好处在于,一旦通用注册服务到位(无需针对不同目的进行定制),就不需要担心跟踪容器的情况。另一个巨大的好处是,容器可以采用复杂的策略,根据本地条件决定是否暂时注销自己;例如,如果容器忙碌并且此时不希望接收任何请求。这种智能和去中心化的动态负载均衡,如果没有注册服务,全局实现起来可能非常困难。缺点是,注册服务是另一个容器需要了解的非标准组件,以便定位其他容器。
服务与端点
Kubernetes 服务可以视为标准注册服务。属于某个服务的 Pods 会根据其标签自动注册。其他 Pods 可以查找端点以找到所有服务 Pods,或直接利用该服务,向其发送消息,该消息将路由到其中一个后端 Pods。尽管如此,大多数时候,Pods 只会将消息发送给服务本身,服务会将其转发到其中一个支持的 Pods。动态成员管理可以通过结合使用部署的副本数、健康检查、就绪检查以及水平 Pod 自动扩展来实现。
使用队列实现松耦合连接
如果容器能够相互通信,而无需知道它们的 IP 地址、端口,甚至是服务 IP 地址或网络名称会怎么样?如果大部分通信都可以是异步且解耦的呢?在许多情况下,系统可以由松耦合的组件组成,这些组件不仅不知道其他组件的身份,甚至不知道其他组件的存在。队列促进了这种松耦合的系统。组件(容器)监听队列中的消息,响应消息,执行它们的任务,并将消息发布到队列中,如进度消息、完成状态和错误。队列有很多好处:
-
只需通过添加更多监听队列的容器,便可轻松增加处理能力,无需协调
-
基于队列深度,容易跟踪整体负载
-
通过为消息和/或队列主题进行版本控制,容易使多个版本的组件并行运行
-
通过让多个消费者以不同模式处理请求,轻松实现负载均衡和冗余
-
动态添加或移除其他类型的监听器非常容易
队列的缺点如下:
-
你需要确保队列提供适当的持久性和高可用性,以避免它成为单点故障(SPOF)
-
容器需要使用异步队列 API(可以抽象出来)
-
实现请求-响应机制需要在响应队列上进行相对繁琐的监听
总的来说,队列是大规模系统的优秀机制,可以在大型 Kubernetes 集群中使用,以简化协调工作。
与数据存储的松耦合连接
另一种松耦合的方法是使用数据存储(例如 Redis)来存储消息,然后其他容器可以读取这些消息。虽然这种方法可行,但这并不是数据存储的设计目标,结果往往显得笨重、脆弱,并且性能不佳。数据存储是为数据存储和访问优化的,而不是为通信优化的。话虽如此,数据存储可以与队列结合使用,某个组件将一些数据存储在数据存储中,然后发送一条消息到队列,告知数据已准备好处理。多个组件监听该消息,并且都开始并行处理数据。
Kubernetes Ingress
Kubernetes 提供了一个 ingress 资源和控制器,旨在将 Kubernetes 服务暴露到外部世界。当然,你也可以自己实现,但定义 ingress 时涉及的许多任务在大多数应用程序中都是常见的,尤其是对于某种类型的 ingress,比如 Web 应用程序、CDN 或 DDoS 防护器。你也可以编写自己的 ingress 对象。
ingress 对象通常用于智能负载均衡和 TLS 终结。你可以利用内建的 ingress 控制器,而无需配置和部署你自己的 Nginx 服务器。如果你需要复习相关内容,可以查看第五章,在实践中使用 Kubernetes 资源,在其中我们通过示例讨论了 ingress 资源。
Kubernetes 中的 DNS
DNS 是网络中的一项基础技术。可以通过 IP 网络访问的主机都有 IP 地址。DNS 是一个分层且去中心化的命名系统,它在 IP 地址之上提供了一层间接性。这对于多个使用场景非常重要,例如:
-
负载均衡
-
动态替换具有不同 IP 地址的主机
-
为知名访问点提供人性化名称
DNS 是一个庞大的话题,完整的讨论超出了本书的范围。为了让你有个概念,关于 DNS 有数十种不同的 RFC 标准:en.wikipedia.org/wiki/Domain_Name_System#Standards。
在 Kubernetes 中,主要的可寻址资源是 pod 和服务。每个 pod 和服务在集群内都有一个唯一的内部(私有)IP 地址。kubelet 使用 resolve.conf 文件配置 pod,将它们指向内部 DNS 服务器。下面是配置文件的样子:
$ k run -it --image g1g1/py-kube:0.3 -- bash
If you don't see a command prompt, try pressing enter.
root@bash:/#
root@bash:/# cat /etc/resolv.conf
search default.svc.cluster.local svc.cluster.local cluster.local
nameserver 10.96.0.10
options ndots:5
名称服务器的 IP 地址 10.96.0.10 是 kube-dns 服务的地址:
$ k get svc -n kube-system
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kube-dns ClusterIP 10.96.0.10 <none> 53/UDP,53/TCP,9153/TCP 19m
默认情况下,pod 的主机名就是其元数据名称。如果你希望 pod 在集群内拥有完全合格的域名(FQDN),可以创建一个无头服务,并显式设置主机名以及服务名称的子域名。下面是如何为两个名为 py-kube1 和 py-kube2 的 pod 设置 DNS,它们的主机名分别为 trouble1 和 trouble2,并且有一个名为 maker 的子域,该子域与无头服务相匹配:
apiVersion: v1
kind: Service
metadata:
name: maker
spec:
selector:
app: py-kube
clusterIP: None # headless service
---
apiVersion: v1
kind: Pod
metadata:
name: py-kube1
labels:
app: py-kube
spec:
hostname: trouble
subdomain: maker
containers:
- image: g1g1/py-kube:0.3
command:
- sleep
- "9999"
name: trouble
---
apiVersion: v1
kind: Pod
metadata:
name: py-kube2
labels:
app: py-kube
spec:
hostname: trouble2
subdomain: maker
containers:
- image: g1g1/py-kube:0.3
command:
- sleep
- "9999"
name: trouble
让我们创建 pod 和服务:
$ k apply -f pod-with-dns.yaml
service/maker created
pod/py-kube1 created
pod/py-kube2 created
现在,我们可以检查 pod 内的主机名和 DNS 解析情况。首先,我们将连接到 py-kube2,并验证其主机名是 trouble2,且 完全合格的域名 (FQDN) 为 trouble2.maker.default.svc.cluster.local。
然后,我们可以解析 trouble 和 trouble2 的 FQDN:
$ k exec -it py-kube2 -- bash
root@trouble2:/# hostname
trouble2
root@trouble2:/# hostname --fqdn
trouble2.maker.default.svc.cluster.local
root@trouble2:/# dig +short trouble.maker.default.svc.cluster.local
10.244.0.10
root@trouble2:/# dig +short trouble2.maker.default.svc.cluster.local
10.244.0.9
为了闭环,让我们确认 IP 地址 10.244.0.10 和 10.244.0.9 实际上属于 py-kube1 和 py-kube2 pod:
$ k get po -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
py-kube1 1/1 Running 0 10m 10.244.0.10 kind-control-plane <none> <none>
py-kube2 1/1 Running 0 18m 10.244.0.9 kind-control-plane <none> <none>
你可以应用更多的配置选项和 DNS 策略。请参见 kubernetes.io/docs/concepts/services-networking/dns-pod-service。
CoreDNS
之前我们提到,kubelet 使用 resolve.conf 文件来配置 pod,将它们指向内部 DNS 服务器,那么这个内部 DNS 服务器到底藏在哪里呢?你可以在 kube-system 命名空间找到它。该服务名为 kube-dns:
$ k describe svc -n kube-system kube-dns
Name: kube-dns
Namespace: kube-system
Labels: k8s-app=kube-dns
kubernetes.io/cluster-service=true
kubernetes.io/name=CoreDNS
Annotations: prometheus.io/port: 9153
prometheus.io/scrape: true
Selector: k8s-app=kube-dns
Type: ClusterIP
IP Family Policy: SingleStack
IP Families: IPv4
IP: 10.96.0.10
IPs: 10.96.0.10
Port: dns 53/UDP
TargetPort: 53/UDP
Endpoints: 10.244.0.2:53,10.244.0.3:53
Port: dns-tcp 53/TCP
TargetPort: 53/TCP
Endpoints: 10.244.0.2:53,10.244.0.3:53
Port: metrics 9153/TCP
TargetPort: 9153/TCP
Endpoints: 10.244.0.2:9153,10.244.0.3:9153
Session Affinity: None
Events: <none>
请注意选择器:k8s-app=kube-dns。让我们找到支撑这个服务的 pod:
$ k get po -n kube-system -l k8s-app=kube-dns
NAME READY STATUS RESTARTS AGE
coredns-64897985d-n4x5b 1/1 Running 0 97m
coredns-64897985d-nqtwk 1/1 Running 0 97m
该服务被称为 kube-dns,但是 pod 有一个 coredns 的前缀。很有意思。让我们检查一下部署使用的镜像:
$ k get deploy coredns -n kube-system -o jsonpath='{.spec.template.spec.containers[0]}' | jq .image
"k8s.gcr.io/coredns/coredns:v1.8.6"
这种不匹配的原因是,最初默认的 Kubernetes DNS 服务器被称为 kube-dns。后来,由于其简化的架构和更好的性能,CoreDNS 替代它成为主流 DNS 服务器。
我们已经涵盖了关于 Kubernetes 网络模型及其组件的许多信息。在下一节中,我们将介绍实现该模型的 Kubernetes 网络插件,以及 CNI 和 Kubenet 等标准接口。
Kubernetes 网络插件
Kubernetes 从一开始就有一个网络插件系统,因为网络非常多样化,不同的人可能希望以不同的方式实现它。Kubernetes 足够灵活,支持任何场景。主要的网络插件是 CNI,我们将深入讨论它。但 Kubernetes 也有一个更简单的网络插件,叫做 Kubenet。在深入细节之前,让我们先了解一下 Linux 网络的基础(这只是冰山一角)。这很重要,因为 Kubernetes 网络是建立在标准的 Linux 网络之上的,理解这一基础知识是理解 Kubernetes 网络工作原理的关键。
基本的 Linux 网络
Linux 默认情况下具有一个共享的网络空间。物理网络接口都可以在这个命名空间中访问。但是,物理命名空间可以被划分为多个逻辑命名空间,这对于容器网络非常相关。
IP 地址和端口
网络实体通过其 IP 地址来标识。服务器可以在多个端口上监听传入连接。客户端可以连接(TCP)或发送/接收数据(UDP)到同一网络中的服务器。
网络命名空间
命名空间将一组网络设备进行分组,使它们可以访问同一命名空间中的其他服务器,但不能访问其他服务器,即使它们在物理上位于同一网络中。通过桥接、交换机、网关和路由可以将网络或网络段连接起来。
子网、子网掩码和 CIDR
网络段的精细划分在设计和维护网络时非常有用。将网络划分为具有共同前缀的小子网是常见做法。这些子网可以通过位掩码来定义,位掩码表示子网的大小(它可以容纳多少主机)。例如,子网掩码 255.255.255.0 意味着前 3 个八位字节用于路由,并且只有 256(实际上是 254)个主机可用。无类域间路由 (CIDR) 符号通常用于此目的,因为它更加简洁,能够编码更多信息,并且允许将来自多个传统类别(A、B、C、D、E)的主机结合在一起。例如,172.27.15.0/24 表示前 24 位(3 个八位字节)用于路由。
虚拟以太网设备
虚拟以太网 (veth) 设备代表物理网络设备。当你创建一个与物理设备连接的 veth 时,你可以将该 veth(以及扩展的物理设备)分配到一个命名空间中,在这个命名空间内,其他命名空间的设备无法直接访问它,即使它们在物理上处于同一局域网内。
桥接
桥接将多个网络段连接成一个聚合网络,从而使所有节点都可以互相通信。桥接是在 OSI 网络模型的第二层(数据链路层)完成的。
路由
路由连接不同的网络,通常是基于路由表,路由表指示网络设备如何将数据包转发到目标地址。路由通过各种网络设备进行,如路由器、网关、交换机、防火墙,包括常规的 Linux 主机。
最大传输单元
最大传输单元(MTU)决定了数据包的最大大小。例如,在以太网网络中,MTU 是 1500 字节。MTU 越大,负载与头部的比例越好,这是有利的。但缺点是,最小延迟会减少,因为必须等待整个数据包到达,而且如果发生失败,必须重新传输整个大数据包。
Pod 网络
下面是一个描述 pod、主机和通过 veth0 与全球互联网之间网络关系的图示:

图 10.3:Pod 网络
Kubenet
回到 Kubernetes。Kubenet 是一个网络插件,功能非常基础:它建立一个名为 cbr0 的 Linux 桥接,并为每个 pod 创建一个 veth 接口。云服务提供商通常使用它来配置节点间通信的路由规则,或在单节点环境中使用。veth 对连接每个 pod 到主机节点,使用主机 IP 地址范围中的一个 IP 地址。
要求
Kubenet 插件有以下要求:
-
节点必须分配一个子网,用于为其 pod 分配 IP 地址
-
标准 CNI 桥接、
lo和 host-local 插件必须安装版本 0.2.0 或更高版本 -
kubelet 必须使用
--network-plugin=kubenet标志启动 -
kubelet 必须使用
--non-masquerade-cidr=<clusterCidr>标志启动 -
kubelet 必须使用
--pod-cidr启动,或 kube-controller-manager 必须使用--allocate-node-cidrs=true --cluster-cidr=<cidr>启动
设置 MTU
MTU 对网络性能至关重要。Kubernetes 网络插件如 Kubenet 会尽最大努力推测最佳 MTU,但有时它们需要帮助。如果现有的网络接口(例如,docker0 桥接)设置了较小的 MTU,则 Kubenet 会复用该设置。另一个例子是 IPsec,它由于 IPsec 封装的额外开销需要降低 MTU,但 Kubenet 网络插件并未考虑这一点。解决方法是避免依赖自动计算 MTU,而是通过 --network-plugin-mtu 命令行选项直接告知 kubelet 应使用哪种 MTU 来为网络插件指定值。该选项已提供给所有网络插件,但目前只有 Kubenet 网络插件会考虑这一命令行选项。
Kubenet 网络插件主要是为了向后兼容。CNI 是所有现代网络解决方案提供商实现的主要网络接口,用于与 Kubernetes 集成。我们来看看它的具体内容。
CNI
CNI 是一个规范以及一套库,用于编写网络插件,以便在 Linux 容器中配置网络接口。该规范实际上源自 rkt 网络提案。如今,CNI 已成为一个成熟的行业标准,甚至超出了 Kubernetes 的范畴。一些使用 CNI 的组织包括:
-
Kubernetes
-
OpenShift
-
Mesos
-
Kurma
-
Cloud Foundry
-
Nuage
-
IBM
-
AWS EKS 和 ECS
-
Lyft
CNI 团队维护一些核心插件,但也有许多第三方插件为 CNI 的成功做出了贡献。以下是一个非详尽的列表:
-
Project Calico:Kubernetes 的第 3 层虚拟网络
-
Weave:一个虚拟网络,用于连接多个主机上的 Docker 容器
-
Contiv 网络:基于策略的网络
-
Cilium:ePBF 用于容器
-
Flannel:Kubernetes 的第 3 层网络架构
-
Infoblox:企业级 IP 地址管理
-
Silk:Cloud Foundry 的 CNI 插件
-
OVN-kubernetes:基于 OVS 和开放虚拟网络(OVN)的 CNI 插件
-
DANM:诺基亚为 Kubernetes 上的电信工作负载提供的解决方案
CNI 插件为任意网络解决方案提供标准的网络接口。
容器运行时
CNI 为应用容器定义了插件规范,但插件必须插入到提供某些服务的容器运行时中。在 CNI 的上下文中,应用容器是一个网络可寻址实体(具有自己的 IP 地址)。对于 Docker,每个容器都有自己的 IP 地址。对于 Kubernetes,每个 pod 都有自己的 IP 地址,pod 被视为 CNI 容器,pod 内的容器对 CNI 不可见。
容器运行时的任务是配置网络,然后执行一个或多个 CNI 插件,将网络配置以 JSON 格式传递给它们。
以下图示展示了容器运行时如何使用 CNI 插件接口与多个 CNI 插件进行通信:

图 10.4:容器运行时与 CNI
CNI 插件
CNI 插件的任务是将网络接口添加到容器的网络命名空间,并通过 veth 对将容器与主机桥接。然后,它应通过 IP 地址管理(IPAM)插件分配一个 IP 地址,并设置路由。
容器运行时(任何符合 CRI 的运行时)作为可执行文件调用 CNI 插件。插件需要支持以下操作:
-
将容器添加到网络
-
从网络中移除容器
-
报告版本
插件使用简单的命令行接口、标准输入/输出和环境变量。网络配置以 JSON 格式通过标准输入传递给插件。其他参数则定义为环境变量:
-
CNI_COMMAND:指定所需的操作,例如ADD、DEL或VERSION。 -
CNI_CONTAINERID:表示容器的 ID。 -
CNI_NETNS:指向网络命名空间文件的路径。 -
CNI_IFNAME:指定要设置的接口名称。CNI 插件应使用此名称,或者返回一个错误。 -
CNI_ARGS:包含用户在调用时传递的额外参数。它由以分号分隔的字母数字键值对组成,如FOO=BAR;ABC=123。 -
CNI_PATH:表示用于查找 CNI 插件可执行文件的路径列表。路径之间由操作系统特定的分隔符分隔,例如 Linux 上是 ":",Windows 上是 ";"。
如果命令成功,插件将返回一个零退出码,并且生成的接口(在 ADD 命令的情况下)将以 JSON 格式流式传输到标准输出。这个低技术的接口非常聪明,因为它不需要任何特定的编程语言、组件技术或二进制 API。CNI 插件编写者也可以使用他们喜欢的编程语言。
调用 CNI 插件 ADD 命令的结果如下所示:
{
"cniVersion": "0.3.0",
"interfaces": [ (this key omitted by IPAM plugins)
{
"name": "<name>",
"mac": "<MAC address>", (required if L2 addresses are meaningful)
"sandbox": "<netns path or hypervisor identifier>" (required for container/hypervisor interfaces, empty/omitted for host interfaces)
}
],
"ip": [
{
"version": "<4-or-6>",
"address": "<ip-and-prefix-in-CIDR>",
"gateway": "<ip-address-of-the-gateway>", (optional)
"interface": <numeric index into 'interfaces' list>
},
...
],
"routes": [ (optional)
{
"dst": "<ip-and-prefix-in-cidr>",
"gw": "<ip-of-next-hop>" (optional)
},
...
]
"dns": {
"nameservers": <list-of-nameservers> (optional)
"domain": <name-of-local-domain> (optional)
"search": <list-of-additional-search-domains> (optional)
"options": <list-of-options> (optional)
}
}
输入的网络配置包含很多信息:cniVersion、name、type、args(可选)、ipMasq(可选)、ipam 和 dns。ipam 和 dns 参数是包含自己指定键的字典。以下是一个网络配置示例:
{
"cniVersion": "0.3.0",
"name": "dbnet",
"type": "bridge",
// type (plugin) specific
"bridge": "cni0",
"ipam": {
"type": "host-local",
// ipam specific
"subnet": "10.1.0.0/16",
"gateway": "10.1.0.1"
},
"dns": {
"nameservers": ["10.1.0.1"]
}
}
请注意,可以添加额外的插件特定元素。在这种情况下,bridge: cni0 元素是一个特定桥接插件理解的自定义元素。
CNI 规范还支持网络配置列表,可以按顺序调用多个 CNI 插件。
这就结束了对 Kubernetes 网络插件的概念讨论,这些插件建立在基础的 Linux 网络之上,允许多个网络解决方案提供商与 Kubernetes 平滑集成。
本章稍后我们将深入探讨 CNI 插件的完整实现。首先,让我们谈谈 Kubernetes 网络世界中最令人兴奋的前景之一 —— 扩展 Berkeley 数据包过滤器 (eBPF)。
Kubernetes 和 eBPF
如你所知,Kubernetes 是一个非常多功能且灵活的平台。Kubernetes 的开发者凭借其智慧,避免了做出许多可能后来把自己困住的假设和决策。例如,Kubernetes 网络仅在 IP 和 DNS 层面上运作。没有网络或子网的概念。这些都留给了通过非常狭窄和通用的接口(如 CNI)与 Kubernetes 集成的网络解决方案。
这为大量创新开辟了道路,因为 Kubernetes 不限制实现者的选择。
进入 eBPF。它是一种技术,可以在不妥协系统安全性或要求你对内核本身甚至内核模块做出更改的情况下,在 Linux 内核中安全地运行沙箱程序。这些程序响应事件执行。这对于软件定义网络、可观察性和安全性来说是一个重大突破。Brendan Gregg 称其为 Linux 的超能力。
原始的 BPF 技术只能附加到套接字上进行数据包过滤(因此得名 Berkeley Packet Filter)。使用 eBPF,您可以附加到其他对象,如:
-
Kprobes
-
跟踪点
-
网络调度器或 qdiscs 用于分类或操作
-
XDP
传统的 Kubernetes 路由由 kube-proxy 完成。它是一个在每个节点上运行的用户空间进程,负责设置 iptable 规则,并进行 UDP、TCP 和 STCP 转发以及负载均衡(基于 Kubernetes 服务)。在大规模集群中,kube-proxy 成为一个负担。iptable 规则是顺序处理的,频繁的用户空间到内核空间的切换也带来了不必要的开销。完全可以通过一个基于 eBPF 的方法来替代 kube-proxy,该方法能更高效地完成相同的功能。我们将在下一节讨论其中一种解决方案——Cilium。
这里是 eBPF 的概述:

图 10.5:eBPF 概述
欲了解更多详情,请查看 ebpf.io。
Kubernetes 网络解决方案
网络是一个广泛的话题。有很多种方式来设置网络,连接设备、Pod 和容器。Kubernetes 并不会对其做出固定的意见。Kubernetes 规定的高级网络模型是 Pod 的扁平地址空间。在这个空间内,可以实现许多有效的解决方案,适应不同环境的多种能力和策略。在本节中,我们将探讨一些可用的解决方案,并理解它们如何映射到 Kubernetes 网络模型中。
在裸机集群上进行桥接
最基本的环境是一个裸机集群,只有一个 L2 物理网络。你可以通过 Linux 桥接设备将容器连接到物理网络。这个过程相当复杂,且需要熟悉一些低级的 Linux 网络命令,如 brctl、ipaddr、iproute、iplink 和 nsenter。如果你计划实现这个方法,这份指南可以作为一个好的起点(请查找 With Linux Bridge devices 部分):blog.oddbit.com/2014/08/11/four-ways-to-connect-a-docker/。
Calico 项目
Calico 是一个多功能的虚拟网络和网络安全解决方案,适用于容器。Calico 可以与所有主要的容器编排框架和运行时集成:
-
Kubernetes (CNI 插件)
-
Mesos (CNI 插件)
-
Docker (libnetwork 插件)
-
OpenStack (Neutron 插件)
Calico 也可以在本地部署或公共云上部署,并提供完整的功能集。Calico 的网络策略执行可以针对每个工作负载进行定制,确保流量精确控制,数据包始终从源头流向经过审查的目标。Calico 可以自动将编排平台的网络策略概念映射到其自身的网络策略中。Kubernetes 的网络策略参考实现就是 Calico。Calico 可以与 Flannel 一起部署,利用 Flannel 的网络层和 Calico 的网络策略功能。
Weave Net
Weave Net 以易用性和零配置为核心。它在底层使用 VXLAN 封装,并在每个节点上运行微型 DNS。作为开发者,你在更高的抽象层次上操作。你为容器命名,Weave Net 让你连接到它们并使用标准端口提供服务。这有助于将现有应用迁移到容器化应用和微服务中。Weave Net 提供了一个 CNI 插件,能够与 Kubernetes(和 Mesos)进行接口集成。在 Kubernetes 1.4 及更高版本中,你可以通过运行一个命令来将 Weave Net 与 Kubernetes 集成,该命令会部署一个 Daemonset:
kubectl apply -f https://github.com/weaveworks/weave/releases/download/v2.8.1/weave-daemonset-k8s.yaml
每个节点上的 Weave Net Pod 会负责将你创建的任何新 Pod 连接到 Weave 网络。Weave Net 支持网络策略 API,并提供一个完整且易于设置的解决方案。
Cilium
Cilium 是一个 CNCF 孵化项目,专注于基于 eBPF 的网络、安全和可观察性(通过其 Hubble 项目)。
让我们来看一下 Cilium 提供的功能。
高效的 IP 分配与路由
Cilium 允许一个覆盖多个集群的扁平 Layer 3 网络,连接所有应用容器。主机范围的分配器可以在不与其他主机协调的情况下分配 IP 地址。Cilium 支持多种网络模型:
-
覆盖:该模型使用基于封装的虚拟网络,跨所有主机进行扩展。它支持如 VXLAN 和 Geneve 等封装格式,以及 Linux 支持的其他格式。覆盖模式适用于几乎所有网络基础设施,只要主机具有 IP 连通性即可。它提供了一个灵活且可扩展的解决方案。
-
原生路由:在此模型中,Kubernetes 利用 Linux 主机的常规路由表。网络基础设施必须能够路由应用容器使用的 IP 地址。原生路由模式被认为是更先进的,且需要了解底层网络基础设施。它与原生 IPv6 网络、云网络路由器或使用自定义路由守护进程时表现良好。
基于身份的服务间通信
Cilium 提供了一种安全管理功能,将相同安全策略的应用容器分配到安全身份。然后,这个身份与这些应用容器生成的所有网络数据包相关联。通过这种方式,Cilium 使接收节点能够验证身份。安全身份的管理通过一个键值存储来处理,这使得在 Cilium 网络解决方案中能够高效且安全地管理身份。
负载均衡
Cilium 为应用容器与外部服务之间的流量提供分布式负载均衡,作为 kube-proxy 的替代方案。该负载均衡功能通过在 eBPF 中使用高效的哈希表实现,相较于传统的 iptables 方法,提供了一种可扩展的方案。使用 Cilium,你可以实现高性能的负载均衡,同时确保网络资源的高效利用。
在东西向负载均衡方面,Cilium 在 Linux 内核的套接字层内直接进行高效的服务到后端转换,表现突出。这种方法消除了每个数据包的 NAT 操作,降低了开销并提升了性能。
对于南北向负载均衡,Cilium 的 eBPF 实现经过高度优化,以获得最佳性能。它可以与 XDP(eXpress Data Path)无缝集成,并支持 Direct Server Return(DSR)和 Maglev 一致性哈希等高级负载均衡技术。这使得负载均衡操作可以高效地从源主机卸载,从而进一步提升性能和可扩展性。
带宽管理
Cilium 通过基于 Earliest Departure Time(EDT)的速率限制和 eBPF 实现了带宽管理,用于出口流量。这显著降低了应用程序的传输尾延迟。
可观察性
Cilium 提供了全面的事件监控,拥有丰富的元数据。除了捕捉丢包的源 IP 地址和目标 IP 地址外,它还提供了发送者和接收者的详细标签信息。这些元数据增强了可见性和故障排除能力。此外,Cilium 通过 Prometheus 导出度量数据,方便监控和分析网络性能。
为了进一步增强可观察性,Hubble 可观察性平台提供了额外的功能,如服务依赖图、操作监控、警报功能以及对应用程序和安全性的全面可见性。通过利用流日志,Hubble 使管理员能够深入了解网络中服务的行为和交互。
Cilium 是一个庞大的项目,涵盖面广泛。在这里,我们只是略微触及了表面。更多细节请见 cilium.io。
有许多优秀的网络解决方案。那么,哪种网络解决方案最适合你呢?如果你在云中运行,我建议使用云服务提供商的原生 CNI 插件。如果你是独立运行,Calico 是一个可靠的选择。如果你敢于冒险并需要对网络进行深度优化,考虑使用 Cilium。
在下一部分,我们将介绍网络策略,帮助你掌控集群中的流量。
有效使用网络策略
Kubernetes 网络策略用于管理网络流量到选定的 Pods 和命名空间。在部署和编排了数百个微服务的 Kubernetes 环境中,管理 Pods 之间的网络连接是至关重要的。需要理解的是,网络策略并非主要的安全机制。如果攻击者能够访问内部网络,他们可能会创建符合网络策略的 Pods,并与其他 Pods 自由通信。在上一节中,我们探讨了不同的 Kubernetes 网络解决方案,并重点介绍了容器网络接口(CNI)。在这一节中,我们将重点讨论网络策略,尽管网络解决方案与网络策略的实现有着密切的关系。
理解 Kubernetes 网络策略设计
网络策略定义了 Kubernetes 集群中 Pods 和其他网络端点之间的通信规则。它使用标签选择特定的 Pods,并应用白名单规则来控制流量对选定 Pods 的访问。这些规则通过基于定义的标准允许额外的流量,从而补充了命名空间级别定义的隔离策略。通过配置网络策略,管理员可以精细化并限制 Pods 之间的通信,增强集群内的安全性和网络隔离。
网络策略与 CNI 插件
网络策略与 CNI 插件之间存在复杂的关系。一些 CNI 插件实现了网络连接和网络策略,而其他插件则只实现其中一个方面,但它们可以与实现另一个方面的 CNI 插件协作(例如,Calico 和 Flannel)。
配置网络策略
网络策略通过NetworkPolicy资源进行配置。您可以定义入站和/或出站策略。以下是一个示例网络策略,指定了入站和出站规则:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: test-network-policy
namespace: awesome-project
spec:
podSelector:
matchLabels:
role: db
policyTypes:
- Ingress
- Egress
ingress:
- from:
- namespaceSelector:
matchLabels:
project: awesome-project
- podSelector:
matchLabels:
role: frontend
ports:
- protocol: TCP
port: 6379
egress:
- to:
- ipBlock:
cidr: 10.0.0.0/24
ports:
- protocol: TCP
port: 7777
实施网络策略
虽然网络策略 API 本身是通用的,并且是 Kubernetes API 的一部分,但其实现与网络解决方案紧密耦合。这意味着在每个节点上,都会有一个特殊的代理或网关(Cilium 通过 eBPF 在内核中实现)来执行以下操作:
-
拦截所有进入节点的流量
-
验证是否符合网络策略
-
转发或拒绝每个请求
Kubernetes 提供了通过 API 定义和存储网络策略的功能。网络策略的强制执行则交由网络解决方案或与特定网络解决方案紧密集成的专用网络策略解决方案来完成。
Calico 就是这种方法的一个很好的例子。Calico 有自己的网络解决方案和网络策略解决方案,它们协同工作。在这两种情况下,两者之间有紧密的集成。下图展示了 Kubernetes 策略控制器如何管理网络策略,以及节点上的代理如何执行这些策略:

图 10.6:Kubernetes 网络策略管理
本节我们介绍了各种网络解决方案以及网络策略,并简要讨论了负载均衡。然而,负载均衡是一个广泛的主题,下一节将深入探讨它。
负载均衡选项
负载均衡是动态系统(如 Kubernetes 集群)中的关键能力。节点、虚拟机和 Pod 会不断变化,但客户端通常无法跟踪哪些单个实体可以处理它们的请求。即使它们能做到这一点,也需要复杂的操作来管理集群的动态映射,频繁刷新,并处理断开连接、无响应或仅仅是慢速的节点。这种所谓的客户端负载均衡仅适用于特定情况。服务器端负载均衡是一种经过战斗验证且广泛理解的机制,它增加了一层间接性,将集群内部的混乱隐藏在集群外的客户端或消费者面前。可以选择外部或内部负载均衡器,也可以混合使用两者。混合方法有其特定的优缺点,比如性能与灵活性之间的权衡。我们将介绍以下选项:
-
外部负载均衡器
-
服务负载均衡器
-
Ingress
-
HA Proxy
-
MetalLB
-
Traefik
-
Kubernetes 网关 API
外部负载均衡器
外部负载均衡器是运行在 Kubernetes 集群外部的负载均衡器。必须有一个外部负载均衡器提供商,Kubernetes 可以与之交互,以便为外部负载均衡器配置健康检查、防火墙规则,并获取负载均衡器的外部 IP 地址。
下图展示了负载均衡器(在云中)、Kubernetes API 服务器和集群节点之间的连接。外部负载均衡器拥有关于哪些 Pods 运行在哪些节点上的最新信息,它可以将外部服务流量引导到正确的 Pods:

图 10.7:负载均衡器、Kubernetes API 服务器和集群节点之间的连接
配置外部负载均衡器
外部负载均衡器通过服务配置文件或直接通过 kubectl 进行配置。我们使用 LoadBalancer 类型的服务,而不是使用 ClusterIP 类型的服务,后者直接将 Kubernetes 节点暴露为负载均衡器。这依赖于外部负载均衡器提供商在集群中正确安装和配置。
通过清单文件
这是一个服务清单文件示例,完成了这一目标:
apiVersion: v1
kind: Service
metadata:
name: api-gateway
spec:
type: LoadBalancer
ports:
- port: 80
targetPort: 5000
selector:
svc: api-gateway
app: delinkcious
通过 kubectl
你也可以通过直接使用 kubectl 命令实现相同的结果:
$ kubectl expose deployment api-gateway --port=80 --target-port=5000 --name=api-gateway --type=LoadBalancer
是否使用服务配置文件或kubectl命令,通常取决于你如何设置其余的基础设施并部署系统。清单文件更加声明式,更适合生产环境使用,因为你需要一种具有版本控制、可审计和可重复的方式来管理基础设施。通常,这会成为基于 GitOps 的 CI/CD 管道的一部分。
查找负载均衡器 IP 地址
负载均衡器将有两个相关的 IP 地址。内部 IP 地址可以在集群内部用于访问服务。集群外的客户端将使用外部 IP 地址。为外部 IP 地址创建 DNS 条目是一种好习惯。如果你想使用 TLS/SSL,特别重要,因为它需要稳定的主机名。要获取这两个地址,可以使用kubectl describe service命令。IP字段表示内部 IP 地址,LoadBalancer Ingress字段表示外部 IP 地址:
$ kubectl describe services example-service
Name: example-service
Selector: app=example
Type: LoadBalancer
IP: 10.67.252.103
LoadBalancer Ingress: 123.45.678.9
Port: <unnamed> 80/TCP
NodePort: <unnamed> 32445/TCP
Endpoints: 10.64.0.4:80,10.64.1.5:80,10.64.2.4:80
Session Affinity: None
No events.
保留客户端 IP 地址
有时,服务可能需要知道客户端的源 IP 地址。直到 Kubernetes 1.5 版本,这些信息是不可用的。在 Kubernetes 1.7 中,API 增加了保留原始客户端 IP 的功能。
指定原始客户端 IP 地址的保留
你需要配置service规格中的以下两个字段:
-
service.spec.externalTrafficPolicy:此字段决定服务是否应该将外部流量路由到节点本地端点或集群范围内的端点(默认值)。Cluster选项不会显示客户端源 IP,可能会增加跳转到另一个节点的情况,但能够很好地分配负载。Local选项保留客户端源 IP,并且只要服务类型是LoadBalancer或NodePort,就不会增加额外的跳转。它的缺点是可能不会很好地平衡负载。 -
service.spec.healthCheckNodePort:此字段是可选的。如果使用此字段,服务健康检查将使用该端口号。默认值为分配的节点端口。对于externalTrafficPolicy设置为Local的LoadBalancer类型服务,该字段会产生影响。
以下是一个示例:
apiVersion: v1
kind: Service
metadata:
name: api-gateway
spec:
type: LoadBalancer
externalTrafficPolicy: Local
ports:
- port: 80
targetPort: 5000
selector:
svc: api-gateway
app: delinkcious
理解外部负载均衡
外部负载均衡器在节点级别操作;虽然它们将流量引导到特定的 Pod,但负载分配是在节点级别完成的。这意味着,如果你的服务有四个 Pod,其中三个在节点 A 上,最后一个在节点 B 上,那么外部负载均衡器很可能会将负载均匀地分配到节点 A 和节点 B。
这将使位于节点 A 上的 3 个 Pod 处理一半的负载(每个 Pod 为 1/6),而节点 B 上的单个 Pod 将独自处理另一半负载。未来可能会增加权重来解决这个问题。你可以通过使用 Pod 反亲和性或拓扑分布约束来避免 Pod 在节点间分布不均的问题。
服务负载均衡器
服务负载均衡旨在在 Kubernetes 集群内部转发流量,而非外部负载均衡。这是通过使用 clusterIP 类型的服务来实现的。也可以通过使用 NodePort 类型的服务,直接通过预分配的端口暴露服务负载均衡器,并将其作为外部负载均衡器,但这需要在整个集群中管理所有 Node 端口,以避免冲突,并且可能不适用于生产环境。像 SSL 终止和 HTTP 缓存等期望的功能将不会直接可用。
以下图示展示了服务负载均衡器(黄色云朵)如何将流量路由到它管理的后端 Pod(当然是通过标签):

图 10.8:服务负载均衡器将流量路由到后端 Pod
Ingress
Kubernetes 中的 Ingress 本质上是一组规则,允许传入的 HTTP/S 流量到达集群服务。此外,某些 ingress 控制器还支持以下功能:
-
连接算法
-
请求限制
-
URL 重写和重定向
-
TCP/UDP 负载均衡
-
SSL 终止
-
访问控制与授权
Ingress 是通过 Ingress 资源进行指定,并由 ingress 控制器服务。在 Kubernetes 1.1 版本中,Ingress 资源一直处于测试阶段,直到 Kubernetes 1.19 版本才正式发布。下面是一个 ingress 资源示例,它管理进入两个服务的流量。规则将外部可见的 http://foo.bar.com/foo 映射到 s1 服务,http://foo.bar.com/bar 映射到 s2 服务:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: test
spec:
ingressClassName: cool-ingress
rules:
- host: foo.bar.com
http:
paths:
- path: /foo
backend:
service:
name: s1
port: 80
- path: /bar
backend:
service:
name: s2
port: 80
ingressClassname 指定一个 IngressClass 资源,其中包含有关 ingress 的额外信息。如果省略此项,则必须定义一个默认的 ingress 类。
这是它的样子:
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
labels:
app.kubernetes.io/component: controller
name: cool-ingress
annotations:
ingressclass.kubernetes.io/is-default-class: "true"
spec:
controller: k8s.io/ingress-nginx
Ingress 控制器通常需要在 Ingress 资源中添加注释,以自定义其行为。
以下图示演示了 Ingress 的工作原理:

图 10.9:Ingress 演示
目前在 Kubernetes 官方仓库中有两个官方的 ingress 控制器。一个是仅适用于 GCE 的 L7 ingress 控制器,另一个是更通用的 Nginx ingress 控制器,它允许你通过 ConfigMap 配置 Nginx Web 服务器。Nginx ingress 控制器非常复杂,并带来了许多通过 ingress 资源直接无法实现的功能。它使用 Endpoints API 直接将流量转发到 Pod。它支持 Minikube、GCE、AWS、Azure 和裸机集群。欲了解更多详细信息,请访问 github.com/kubernetes/ingress-nginx。
然而,还有许多其他 ingress 控制器,可能更适合你的使用场景,例如:
-
Ambassador
-
Traefik
-
Contour
-
Gloo
欲了解更多 ingress 控制器,请参阅 kubernetes.io/docs/concepts/services-networking/ingress-controllers/。
HAProxy
我们讨论了使用云服务提供商的外部负载均衡器,使用服务类型 LoadBalancer,以及使用集群内部的服务负载均衡器 ClusterIP。如果我们想要一个自定义的外部负载均衡器,可以创建一个自定义的外部负载均衡器提供程序,并使用 LoadBalancer 或者使用第三种服务类型 NodePort。高可用性 (HA) Proxy 是一个成熟且经过实践验证的负载均衡解决方案。它被认为是在本地集群中实现外部负载均衡的最佳选择之一。这可以通过多种方式实现:
-
使用
NodePort并仔细管理端口分配 -
实现一个自定义的负载均衡器提供程序接口
-
在集群内运行
HAProxy,作为前端服务器的唯一目标(无论是否进行负载均衡)
你可以使用所有这些方法与 HAProxy 配合使用。无论如何,仍然建议使用 Ingress 对象。service-loadbalancer 项目是一个社区项目,它在 HAProxy 上实现了负载均衡解决方案。你可以在这里找到它:github.com/kubernetes/contrib/tree/master/service-loadbalancer。让我们更详细地了解如何使用 HAProxy。
使用 NodePort
每个服务将从预定义范围内分配一个专用端口。这个端口通常是较高的范围,如 30,000 以上,以避免与其他使用不常见端口的应用程序冲突。在这种情况下,HAProxy 将运行在集群外部,并且会为每个服务配置正确的端口。然后,它可以将任何流量转发到任何节点和 Kubernetes 内部服务,负载均衡器将其路由到适当的 Pod(双重负载均衡)。当然,这是次优的,因为它引入了额外的跳跃。规避这种情况的方法是查询 Endpoints API,并动态管理每个服务的后端 Pod 列表,直接将流量转发到 Pod。
使用 HAProxy 的自定义负载均衡器提供程序
这种方法稍微复杂一些,但它的好处是能更好地与 Kubernetes 集成,并且可以让从本地部署到云端的迁移更加容易。
在 Kubernetes 集群内运行 HAProxy
在这种方法中,我们使用集群内的内部 HAProxy 负载均衡器。可能会有多个节点运行 HAProxy,它们将共享相同的配置,将传入请求映射并在后端服务器之间进行负载均衡(下图中的 Apache 服务器):

图 10.10:多个节点运行 HAProxy 来处理传入请求,并对后端服务器进行负载均衡
HAProxy 还开发了自己的 Ingress 控制器,它支持 Kubernetes。这无疑是将 HAProxy 用于 Kubernetes 集群的最简化方式。使用 HAProxy Ingress 控制器时,你可以获得以下一些功能:
-
与
HAProxy负载均衡器的简化集成 -
SSL 终止
-
速率限制
-
IP 白名单
-
多种负载均衡算法:轮询、最少连接数、URL 哈希和随机
-
一个展示 pod 健康状况、当前请求率、响应时间等信息的仪表盘。
-
流量过载保护
MetalLB
MetalLB 还为裸机集群提供负载均衡解决方案。它高度可配置,支持 L2 和 BGP 等多种模式。我甚至成功地为 minikube 配置了它。欲了解更多详情,请访问 metallb.universe.tf。
Traefik
Traefik 是一个现代的 HTTP 反向代理和负载均衡器,旨在支持微服务。它可以与许多后端系统一起工作,包括 Kubernetes,自动动态地管理其配置。这与传统的负载均衡器相比,是一次颠覆性的变化。它具有令人印象深刻的功能列表:
-
它非常快速
-
单一 Go 可执行文件
-
小巧的官方 Docker 镜像:该解决方案提供一个轻量级的官方 Docker 镜像,确保高效的资源利用。
-
Rest API:它提供一个 RESTful API,方便与该解决方案进行集成和交互。
-
配置的热重载:可以动态应用配置更改,无需重启进程,从而确保无缝更新。
-
电路断路器和重试:该解决方案包含电路断路器和重试机制,以处理网络故障并确保稳定的通信。
-
轮询和重新平衡负载均衡器:它支持如轮询和重新平衡等负载均衡算法,以便将流量分配到多个实例。
-
指标支持:该解决方案提供多种指标收集选项,包括 REST、Prometheus、Datadog、statsd 和 InfluxDB。
-
干净的 AngularJS Web UI:它提供一个用户友好的 Web UI,由 AngularJS 驱动,方便配置和监控。
-
Websocket、HTTP/2 和 GRPC 支持:该解决方案能够处理 Websocket、HTTP/2 和 GRPC 协议,实现高效的通信。
-
访问日志:它提供 JSON 和常见日志格式(CLF)的访问日志,便于监控和故障排除。
-
Let’s Encrypt 支持:该解决方案与 Let’s Encrypt 无缝集成,实现自动 HTTPS 证书生成和续期。
-
通过集群模式实现高可用性:它通过运行在集群模式下支持高可用性,确保冗余性和容错性。
总体而言,该解决方案提供了一套全面的功能,用于以可扩展和可靠的方式部署和管理应用程序。
参见 traefik.io/traefik/ 了解更多关于 Traefik 的信息。
Kubernetes 网关 API
Kubernetes 网关 API 是一组建模 Kubernetes 服务网络的资源。你可以把它看作是 Ingress API 的进化版。虽然没有计划移除 Ingress API,但由于其局限性,无法通过改进来解决,因此诞生了 Gateway API 项目。
与 Ingress API 由单个Ingress资源和可选的IngressClass组成不同,网关 API 更为细化,将流量管理和路由的定义分解为不同的资源。网关 API 定义了以下资源:
-
GatewayClass -
Gateway -
HTTPRoute -
TLSRoute -
TCPRoute -
UDPRoute
网关 API 资源
GatewayClass的作用是定义可以被多个类似网关使用的公共配置和行为。
网关的作用是定义一个端点和一组路由,流量可以通过这些路由进入集群并被路由到后端服务。最终,网关会配置一个底层的负载均衡器或代理。
路由的作用是将与路由匹配的特定请求映射到特定的后端服务。
以下图示展示了网关 API 的资源和组织结构:

图 10.11:网关 API 资源
将路由附加到网关
网关和路由可以通过不同的方式进行关联:
-
一对一:一个网关可能拥有一个来自单一拥有者的路由,并且该路由未与其他网关关联。
-
一对多:一个网关可能拥有多个来自不同拥有者的路由。
-
多对多:一个路由可能与多个网关关联(每个网关可能有附加的路由)
网关 API 在实际中的应用
让我们通过一个简单的示例来看一下网关 API 如何将所有组件组合在一起。这里是一个网关资源:
apiVersion: gateway.networking.k8s.io/v1beta1
kind: Gateway
metadata:
name: cool-gateway
namespace: ns1
spec:
gatewayClassName: cool-gateway-class
listeners:
- name: cool-service
port: 80
protocol: HTTP
allowedRoutes:
kinds:
- kind: HTTPRoute
namespaces:
from: Selector
selector:
matchLabels:
# This label is added automatically as of K8s 1.22
# to all namespaces
kubernetes.io/metadata.name: ns2
请注意,网关定义在命名空间ns1中,但仅允许定义在命名空间ns2中的 HTTP 路由。让我们看一下一个附加到该网关的路由:
apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
name: cool-route
namespace: ns2
spec:
parentRefs:
- kind: Gateway
name: cool-gateway
namespace: ns1
rules:
- backendRefs:
- name: cool-service
port: 8080
路由cool-route在命名空间ns2中正确定义;它是一个 HTTP 路由,因此可以匹配。为了闭合循环,该路由定义了指向命名空间ns1中的cool-gateway网关的父引用。
参见gateway-api.sigs.k8s.io以了解更多关于网关 API 的信息。
Kubernetes 上的负载均衡是一个令人兴奋的领域。它为南北向和东西向的负载均衡提供了多种选择。现在我们已经详细讨论了负载均衡,让我们深入研究 CNI 插件及其实现方式。
编写自己的 CNI 插件
在本节中,我们将讨论编写自定义 CNI 插件所需要的内容。首先,我们将查看最简单的插件——回环插件。然后,我们将检查实现 CNI 插件的大部分模板代码的插件框架。
最后,我们将回顾桥接插件的实现。在我们深入探讨之前,这里是对 CNI 插件的简要回顾:
-
CNI 插件是一个可执行文件
-
它负责将新容器连接到网络,分配唯一的 IP 地址给 CNI 容器,并处理路由。
-
容器是一个网络命名空间(在 Kubernetes 中,Pod 是 CNI 容器)
-
网络定义作为 JSON 文件进行管理,但通过标准输入流式传输到插件(插件不会读取文件)。
-
辅助信息可以通过环境变量提供
首先看一下 Loopback 插件
Loopback 插件仅添加回环接口。它非常简单,甚至不需要任何网络配置信息。大多数 CNI 插件都是用 Golang 实现的,loopback CNI 插件也不例外。完整的源代码可以在这里找到:github.com/containernetworking/plugins/blob/master/plugins/main/loopback。
GitHub 上有来自容器网络项目的多个包,这些包提供了实现 CNI 插件所需的许多构建模块,还有用于添加接口、移除接口、设置 IP 地址和设置路由的 netlink 包。首先让我们来看一下 loopback.go 文件的导入部分:
package main
import (
"encoding/json"
"errors"
"fmt"
"net"
"github.com/vishvananda/netlink"
"github.com/containernetworking/cni/pkg/skel"
"github.com/containernetworking/cni/pkg/types"
current "github.com/containernetworking/cni/pkg/types/100"
"github.com/containernetworking/cni/pkg/version"
"github.com/containernetworking/plugins/pkg/ns"
bv "github.com/containernetworking/plugins/pkg/utils/buildversion"
)
然后,插件实现了两个命令,cmdAdd 和 cmdDel,当容器被添加到或从网络中移除时会调用这两个命令。这里是 add 命令,它完成了所有繁重的工作:
func cmdAdd(args *skel.CmdArgs) error {
conf, err := parseNetConf(args.StdinData)
if err != nil {
return err
}
var v4Addr, v6Addr *net.IPNet
args.IfName = "lo" // ignore config, this only works for loopback
err = ns.WithNetNSPath(args.Netns, func(_ ns.NetNS) error {
link, err := netlink.LinkByName(args.IfName)
if err != nil {
return err // not tested
}
err = netlink.LinkSetUp(link)
if err != nil {
return err // not tested
}
v4Addrs, err := netlink.AddrList(link, netlink.FAMILY_V4)
if err != nil {
return err // not tested
}
if len(v4Addrs) != 0 {
v4Addr = v4Addrs[0].IPNet
// sanity check that this is a loopback address
for _, addr := range v4Addrs {
if !addr.IP.IsLoopback() {
return fmt.Errorf("loopback interface found with non-loopback address %q", addr.IP)
}
}
}
v6Addrs, err := netlink.AddrList(link, netlink.FAMILY_V6)
if err != nil {
return err // not tested
}
if len(v6Addrs) != 0 {
v6Addr = v6Addrs[0].IPNet
// sanity check that this is a loopback address
for _, addr := range v6Addrs {
if !addr.IP.IsLoopback() {
return fmt.Errorf("loopback interface found with non-loopback address %q", addr.IP)
}
}
}
return nil
})
if err != nil {
return err // not tested
}
var result types.Result
if conf.PrevResult != nil {
// If loopback has previous result which passes from previous CNI plugin,
// loopback should pass it transparently
result = conf.PrevResult
} else {
r := ¤t.Result{
CNIVersion: conf.CNIVersion,
Interfaces: []*current.Interface{
¤t.Interface{
Name: args.IfName,
Mac: "00:00:00:00:00:00",
Sandbox: args.Netns,
},
},
}
if v4Addr != nil {
r.IPs = append(r.IPs, ¤t.IPConfig{
Interface: current.Int(0),
Address: *v4Addr,
})
}
if v6Addr != nil {
r.IPs = append(r.IPs, ¤t.IPConfig{
Interface: current.Int(0),
Address: *v6Addr,
})
}
result = r
}
return types.PrintResult(result, conf.CNIVersion)
}
该函数的核心是在容器的网络命名空间中设置接口名称为 lo(回环接口),并将链接添加到容器的网络命名空间。它支持 IPv4 和 IPv6。
del 命令执行相反的操作,且非常简单:
func cmdDel(args *skel.CmdArgs) error {
if args.Netns == "" {
return nil
}
args.IfName = "lo" // ignore config, this only works for loopback
err := ns.WithNetNSPath(args.Netns, func(ns.NetNS) error {
link, err := netlink.LinkByName(args.IfName)
if err != nil {
return err // not tested
}
err = netlink.LinkSetDown(link)
if err != nil {
return err // not tested
}
return nil
})
if err != nil {
// if NetNs is passed down by the Cloud Orchestration Engine, or if it called multiple times
// so don't return an error if the device is already removed.
// https://github.com/kubernetes/kubernetes/issues/43014#issuecomment-287164444
_, ok := err.(ns.NSPathNotExistErr)
if ok {
return nil
}
return err
}
return nil
}
main 函数简单地调用了 skel 包的 PluginMain() 函数,并传入了命令函数。skel 包会负责运行 CNI 插件可执行文件,并在合适的时机调用 cmdAdd 和 delCmd 函数:
func main() {
skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("loopback"))
}
基于 CNI 插件骨架构建
让我们探索一下 skel 包,看看它在背后做了什么。PluginMain() 入口点负责调用 PluginMainWithError(),捕获错误,将错误打印到标准输出,并退出:
func PluginMain(cmdAdd, cmdCheck, cmdDel func(_ *CmdArgs) error, versionInfo version.PluginInfo, about string) {
if e := PluginMainWithError(cmdAdd, cmdCheck, cmdDel, versionInfo, about); e != nil {
if err := e.Print(); err != nil {
log.Print("Error writing error JSON to stdout: ", err)
}
os.Exit(1)
}
}
PluginErrorWithMain() 函数实例化一个调度器,使用所有的 I/O 流和环境配置它,并调用它的内部 pluginMain() 方法:
func PluginMainWithError(cmdAdd, cmdCheck, cmdDel func(_ *CmdArgs) error, versionInfo version.PluginInfo, about string) *types.Error {
return (&dispatcher{
Getenv: os.Getenv,
Stdin: os.Stdin,
Stdout: os.Stdout,
Stderr: os.Stderr,
}).pluginMain(cmdAdd, cmdCheck, cmdDel, versionInfo, about)
}
这里,终于是骨架的主要逻辑。它从环境中获取 cmd 参数(包括来自标准输入的配置),检测哪个 cmd 被调用,并调用相应的插件函数(cmdAdd 或 cmdDel)。它还可以返回版本信息:
func (t *dispatcher) pluginMain(cmdAdd, cmdCheck, cmdDel func(_ *CmdArgs) error, versionInfo version.PluginInfo, about string) *types.Error {
cmd, cmdArgs, err := t.getCmdArgsFromEnv()
if err != nil {
// Print the about string to stderr when no command is set
if err.Code == types.ErrInvalidEnvironmentVariables && t.Getenv("CNI_COMMAND") == "" && about != "" {
_, _ = fmt.Fprintln(t.Stderr, about)
_, _ = fmt.Fprintf(t.Stderr, "CNI protocol versions supported: %s\n", strings.Join(versionInfo.SupportedVersions(), ", "))
return nil
}
return err
}
if cmd != "VERSION" {
if err = validateConfig(cmdArgs.StdinData); err != nil {
return err
}
if err = utils.ValidateContainerID(cmdArgs.ContainerID); err != nil {
return err
}
if err = utils.ValidateInterfaceName(cmdArgs.IfName); err != nil {
return err
}
}
switch cmd {
case "ADD":
err = t.checkVersionAndCall(cmdArgs, versionInfo, cmdAdd)
case "CHECK":
configVersion, err := t.ConfVersionDecoder.Decode(cmdArgs.StdinData)
if err != nil {
return types.NewError(types.ErrDecodingFailure, err.Error(), "")
}
if gtet, err := version.GreaterThanOrEqualTo(configVersion, "0.4.0"); err != nil {
return types.NewError(types.ErrDecodingFailure, err.Error(), "")
} else if !gtet {
return types.NewError(types.ErrIncompatibleCNIVersion, "config version does not allow CHECK", "")
}
for _, pluginVersion := range versionInfo.SupportedVersions() {
gtet, err := version.GreaterThanOrEqualTo(pluginVersion, configVersion)
if err != nil {
return types.NewError(types.ErrDecodingFailure, err.Error(), "")
} else if gtet {
if err := t.checkVersionAndCall(cmdArgs, versionInfo, cmdCheck); err != nil {
return err
}
return nil
}
}
return types.NewError(types.ErrIncompatibleCNIVersion, "plugin version does not allow CHECK", "")
case "DEL":
err = t.checkVersionAndCall(cmdArgs, versionInfo, cmdDel)
case "VERSION":
if err := versionInfo.Encode(t.Stdout); err != nil {
return types.NewError(types.ErrIOFailure, err.Error(), "")
}
default:
return types.NewError(types.ErrInvalidEnvironmentVariables, fmt.Sprintf("unknown CNI_COMMAND: %v", cmd), "")
}
return err
}
Loopback 插件是最简单的 CNI 插件之一。让我们来看看桥接插件。
审查桥接插件
桥接插件更为强大。让我们来看一下它实现中的一些关键部分。完整的源代码可以在这里找到:github.com/containernetworking/plugins/tree/main/plugins/main/bridge。
插件在 bridge.go 文件中定义了一个网络配置结构体,包含以下字段:
type NetConf struct {
types.NetConf
BrName string `json:"bridge"`
IsGW bool `json:"isGateway"`
IsDefaultGW bool `json:"isDefaultGateway"`
ForceAddress bool `json:"forceAddress"`
IPMasq bool `json:"ipMasq"`
MTU int `json:"mtu"`
HairpinMode bool `json:"hairpinMode"`
PromiscMode bool `json:"promiscMode"`
Vlan int `json:"vlan"`
MacSpoofChk bool `json:"macspoofchk,omitempty"`
EnableDad bool `json:"enabledad,omitempty"`
Args struct {
Cni BridgeArgs `json:"cni,omitempty"`
} `json:"args,omitempty"`
RuntimeConfig struct {
Mac string `json:"mac,omitempty"`
} `json:"runtimeConfig,omitempty"`
mac string
}
由于篇幅限制,我们不会详细讲解每个参数的作用及其与其他参数的交互。目标是理解流程,并为实现自己的 CNI 插件提供一个起点。配置通过 loadNetConf() 函数从 JSON 加载。该函数在 cmdAdd() 和 cmdDel() 函数开始时被调用:
n, cniVersion, err := loadNetConf(args.StdinData, args.Args)
这是 cmdAdd() 的核心部分,它使用来自网络配置的信息,设置桥接并配置 veth:
br, brInterface, err := setupBridge(n)
if err != nil {
return err
}
netns, err := ns.GetNS(args.Netns)
if err != nil {
return fmt.Errorf("failed to open netns %q: %v", args.Netns, err)
}
defer netns.Close()
hostInterface, containerInterface, err := setupVeth(netns, br, args.IfName, n.MTU, n.HairpinMode, n.Vlan)
if err != nil {
return err
}
接下来,函数处理 L3 模式及其多个案例:
// Assume L2 interface only
result := ¤t.Result{
CNIVersion: current.ImplementedSpecVersion,
Interfaces: []*current.Interface{
brInterface,
hostInterface,
containerInterface,
},
}
if n.MacSpoofChk {
...
}
if isLayer3 {
// run the IPAM plugin and get back the config to apply
r, err := ipam.ExecAdd(n.IPAM.Type, args.StdinData)
if err != nil {
return err
}
// release IP in case of failure
defer func() {
if !success {
ipam.ExecDel(n.IPAM.Type, args.StdinData)
}
}()
// Convert whatever the IPAM result was into the current Result type
ipamResult, err := current.NewResultFromResult(r)
if err != nil {
return err
}
result.IPs = ipamResult.IPs
result.Routes = ipamResult.Routes
result.DNS = ipamResult.DNS
if len(result.IPs) == 0 {
return errors.New("IPAM plugin returned missing IP config")
}
// Gather gateway information for each IP family
gwsV4, gwsV6, err := calcGateways(result, n)
if err != nil {
return err
}
// Configure the container hardware address and IP address(es)
if err := netns.Do(func(_ ns.NetNS) error {
...
}
// check bridge port state
retries := []int{0, 50, 500, 1000, 1000}
for idx, sleep := range retries {
...
}
if n.IsGW {
...
}
if n.IPMasq {
...
}
} else {
...
}
最后,它更新了可能已更改的 MAC 地址并返回结果:
// Refetch the bridge since its MAC address may change when the first
// veth is added or after its IP address is set
br, err = bridgeByName(n.BrName)
if err != nil {
return err
}
brInterface.Mac = br.Attrs().HardwareAddr.String()
// Return an error requested by testcases, if any
if debugPostIPAMError != nil {
return debugPostIPAMError
}
// Use incoming DNS settings if provided, otherwise use the
// settings that were already configued by the IPAM plugin
if dnsConfSet(n.DNS) {
result.DNS = n.DNS
}
success = true
return types.PrintResult(result, cniVersion)
这只是完整实现的一部分。还有路由设置和硬件 IP 分配。如果你计划编写自己的 CNI 插件,我鼓励你查阅完整的源代码,它相当庞大,以便全面了解:github.com/containernetworking/plugins/tree/main/plugins/main/bridge。
让我们总结一下我们所学到的内容。
总结
在这一章中,我们涵盖了广泛的内容。网络是一个非常广泛的主题,因为涉及硬件、软件、操作环境和用户技能的组合非常多。制定一个全面的网络解决方案既稳健又安全、性能良好且易于维护,是一项非常复杂的工作。对于 Kubernetes 集群,云服务提供商通常解决这些问题。但如果你运行的是本地集群或需要定制化解决方案,你有很多选择可以挑选。Kubernetes 是一个非常灵活的平台,设计上便于扩展。特别是网络部分是高度可插拔的。
我们讨论的主要主题包括 Kubernetes 网络模型(一个平坦的地址空间,pod 可以相互访问)、查找和发现如何工作、Kubernetes 网络插件、不同抽象层次上的各种网络解决方案(许多有趣的变种)、如何有效使用网络策略来控制集群内部的流量、Ingress 和 Gateway API、负载均衡解决方案的广泛范围,最后,我们还探讨了如何通过分析一个现实世界的实现来编写 CNI 插件。
在这一点上,你可能感到有些不知所措,特别是如果你不是某个领域的专家。然而,你应该已经对 Kubernetes 网络的内部机制有了扎实的理解,了解实现一个完整解决方案所需的所有互联环节,并能够根据适合你的系统和技能水平的权衡来设计自己的解决方案。
在 第十一章,在多个集群上运行 Kubernetes,我们将进一步扩展,探讨如何通过联邦在多个集群上运行 Kubernetes。这是 Kubernetes 在地理分布式部署和最终可扩展性方面的重要组成部分。联邦的 Kubernetes 集群可以突破本地限制,但也带来了许多挑战。
第十一章:在多个集群上运行 Kubernetes
在这一章中,我们将进一步探讨在多个云平台和多个集群上运行 Kubernetes 和部署工作负载的选项。由于单个 Kubernetes 集群有其限制,一旦超过这些限制,你就必须运行多个集群。一个典型的 Kubernetes 集群是一个紧密连接的单元,所有组件都在相对靠近的地方运行,并通过快速网络(通常是物理数据中心或云提供商的可用区)连接。这适用于许多用例,但也有一些重要的用例,系统需要超越单个集群的规模,或者集群需要跨多个可用区扩展。
这是 Kubernetes 当前非常活跃的一个领域。在本书的前一版中,这一章介绍了 Kubernetes 联邦和 Gardener。自那时以来,Kubernetes 联邦项目已被弃用。目前有许多项目提供不同类型的多集群解决方案,如直接管理、虚拟 Kubelet 解决方案,以及非常独特的 gardener.cloud 项目。
我们将涵盖的主题包括:
-
扩展集群与多个集群的对比
-
Kubernetes 集群联邦的历史
-
Cluster API
-
Karmada
-
Clusternet
-
Clusterpedia
-
开放集群管理
-
虚拟 Kubelet 解决方案
-
Gardener 项目简介
扩展 Kubernetes 集群与多集群 Kubernetes 的对比
运行多个 Kubernetes 集群的原因有几个:
-
你希望在集群运行所在的地理区域出现问题时有冗余
-
你需要更多的节点或 pod,而单个 Kubernetes 集群无法支持
-
你希望因安全原因在不同集群之间隔离工作负载
由于第一个原因,可以使用扩展集群;而出于其他原因,你必须运行多个集群。
理解扩展的 Kubernetes 集群
扩展集群(也叫广域集群)是一个单一的 Kubernetes 集群,其中控制平面节点和工作节点分布在多个地理可用区或区域。云提供商为 HA 管理的 Kubernetes 集群提供这种模式。
扩展集群的优点
扩展集群模型有几个优点:
-
你的集群,通过适当的冗余,能够防止数据中心故障作为 SPOF(单点故障)
-
操作单个 Kubernetes 集群的简便性是一个巨大的优势(日志记录、度量和升级)
-
当你运行自己的非托管扩展集群时,可以透明地将其扩展到其他位置(本地、边缘或其他云提供商)
扩展集群的缺点
然而,扩展模型也有其缺点:
-
你不能超过单个 Kubernetes 集群的限制
-
由于跨区网络连接导致的性能下降
-
在云端跨区网络的费用可能相当可观
-
集群升级是一个全有或全无的事务
简而言之,拥有拉伸集群的选项是好事,但如果一些缺点是无法接受的,则要准备切换到多集群模型。
理解多集群 Kubernetes
多集群 Kubernetes 意味着提供多个 Kubernetes 集群。基于前述的各种原因,大规模系统通常无法部署在单个集群上。这意味着你需要提供多个 Kubernetes 集群,然后弄清如何在所有这些集群上部署你的工作负载,并处理各种使用案例,比如一些集群不可用或性能下降的情况。这带来了更多的自由度。
多集群 Kubernetes 的优点
这里是多集群模型的一些好处:
-
系统可以任意扩展 —— 集群数量没有固有的限制
-
在 RBAC 级别为敏感工作负载提供集群级别的隔离
-
在不产生过多成本的情况下(只要大部分流量仍然保持在同一云服务提供商的区域内),利用多个云服务提供商
-
升级和执行增量操作,即使是针对整个集群的操作
多集群 Kubernetes 的缺点
然而,多集群模型也存在一些非常不平凡的缺点:
-
部署和管理一系列集群的非常高复杂度
-
需要弄清如何连接所有的集群
-
需要弄清如何存储并提供对所有集群中数据的访问
-
在设计多集群部署时,有很多方式会让你自食其果
-
需要努力在所有集群中提供集中观测能力
目前有一些问题的解决方案,但目前还没有一个清晰的赢家可以轻松配置以适应你组织的多集群结构的需求。相反,你需要根据组织的具体问题来适应和解决问题。
Kubernetes 中集群联合的历史
在书的早期版本中,我们讨论了 Kubernetes 集群联合作为管理多个 Kubernetes 集群的单一概念集群的解决方案。不幸的是,自 2019 年以来,该项目一直处于停滞状态,并且 Kubernetes 多集群特别兴趣小组(SIG)正在考虑将其存档。在我们描述更现代化的方法之前,让我们先了解一些历史背景。谈论一个像 Kubernetes 这样的项目的历史,甚至在 2014 年之前根本不存在,这是很有趣的,但是其发展速度和大量贡献者的参与加速了 Kubernetes 的进化。这对于 Kubernetes 联合尤为重要。
2015 年 3 月,Kubernetes 集群联合提案的第一个版本被发布。当时它被亲切地称为“Ubernetes”。基本想法是重用现有的 Kubernetes API 来管理多个集群。这个提案现在被称为 Federation V1,经过了几轮修订和实施,但始终未达到广泛使用,且主仓库已被退休:github.com/kubernetes-retired/federation。
SIG 多集群工作组意识到,多集群问题比最初认为的要复杂得多。解决这一特定问题有多种方法,而且没有一种适用于所有的解决方案。集群联合的新方向是使用专门的 API 来进行联合。为此,创建并实施了一个新项目和一组工具——Kubernetes Federation V2:github.com/kubernetes-sigs/kubefed。
不幸的是,这个项目也没有取得成功,关于多集群 SIG 的共识是,由于该项目未再维护,因此需要将其归档。
查看 2022 年 8 月 9 日的会议记录:tinyurl.com/sig-multicluster-notes。
目前有许多项目正在快速推进,试图解决多集群问题,它们都在不同层面上进行操作。让我们来看看其中一些突出项目。这里的目标仅仅是介绍这些项目以及它们的独特之处。深入探讨每个项目超出了本章的范围。不过,我们将在第十七章《在生产环境中运行 Kubernetes》中深入探讨其中一个项目——Cluster API。
Cluster API
Cluster API(也叫 CAPI)是 Cluster Lifecycle SIG 的一个项目。其目标是简化多个 Kubernetes 集群的配置、升级和操作。它支持基于 kubeadm 的集群以及通过专用提供者管理的集群。它有一个很酷的标志,灵感来自著名的“从头到尾全是乌龟”的故事。这个想法是,Cluster API 使用 Kubernetes 来管理 Kubernetes 集群。

图 11.1:Cluster API 标志
Cluster API 架构
Cluster API 具有非常清晰和可扩展的架构。其主要组件包括:
-
管理集群
-
工作集群
-
启动提供者
-
基础设施提供者
-
控制平面
-
自定义资源

图 11.2:Cluster API 架构
让我们理解这些组件中的每一个的作用以及它们如何相互作用。
管理集群
管理集群是一个负责管理其他 Kubernetes 集群(工作集群)的 Kubernetes 集群。它运行 Cluster API 控制平面和提供者,并托管表示其他集群的 Cluster API 自定义资源。
clusterctl CLI 可以用来操作管理集群。clusterctl CLI 是一个命令行工具,提供了大量的命令和选项。如果你想通过其 CLI 来试验 Cluster API,请访问cluster-api.sigs.k8s.io/clusterctl/overview.html。
工作集群
工作集群只是一个普通的 Kubernetes 集群。这些集群是开发人员用来部署其工作负载的。工作集群无需意识到它们是由 Cluster API 管理的。
启动提供者
当 CAPI 创建一个新的 Kubernetes 集群时,在创建工作集群的控制平面和最终的工作节点之前,它需要证书。这是启动提供者的工作。它确保满足所有要求,并最终将工作节点加入控制平面。
基础设施提供者
基础设施提供者是一个可插拔组件,使 CAPI 能够在不同的基础设施环境中工作,例如云提供商或裸金属基础设施提供商。基础设施提供者实现了 CAPI 定义的一组接口,以提供对计算和网络资源的访问。
请查看当前提供者列表:cluster-api.sigs.k8s.io/reference/providers.html。
控制平面
Kubernetes 集群的控制平面由 API 服务器、etcd 状态存储、调度器以及运行控制循环以调和集群中资源的控制器组成。工作集群的控制平面可以通过不同的方式来提供。CAPI 支持以下模式:
-
基于机器的——控制平面组件作为静态 Pod 部署在专用机器上
-
基于 Pod 的——控制平面组件通过
Deployments和StatefulSet部署,API 服务器作为Service暴露 -
外部——控制平面由外部提供者(通常是云提供商)提供和管理
自定义资源
自定义资源代表由 CAPI 管理的 Kubernetes 集群和机器以及其他附加资源。自定义资源数量众多,其中一些仍被视为实验性。主要的 CRD 有:
-
Cluster -
ControlPlane(代表控制平面机器) -
MachineSet(代表工作机器) -
MachineDeployment -
Machine -
MachineHealthCheck
这些通用资源中的一些引用了基础设施提供者提供的相应资源。
以下图示展示了代表集群和机器集的控制平面资源之间的关系:

图 11.3:Cluster API 控制平面资源
CAPI 还具有一组额外的实验性资源,代表一个受管的云提供者环境:
-
MachinePool -
ClusterResourceSet -
ClusterClass
查看 github.com/kubernetes-sigs/cluster-api 获取更多细节。
Karmada
Karmada 是一个 CNCF 沙盒项目,专注于跨多个 Kubernetes 集群部署和运行工作负载。它的特点是你无需修改应用程序配置。虽然 CAPI 专注于集群生命周期管理,但 Karmada 在你已经拥有一组 Kubernetes 集群,并希望在所有集群中部署工作负载时发挥作用。从概念上讲,Karmada 是对已废弃的 Kubernetes 联邦项目的现代化改进。
它可以与 Kubernetes 在云端、本地和边缘环境中一起工作。
查看 github.com/karmada-io/karmada。
让我们来看看 Karmada 的架构。
Karmada 架构
Karmada 深受 Kubernetes 的启发。它提供了一个多集群控制平面,包含与 Kubernetes 控制平面类似的组件:
-
Karmada API 服务器
-
Karmada 控制器管理器
-
Karmada 调度器
如果你理解 Kubernetes 的工作原理,那么理解 Karmada 如何将其 1:1 扩展到多个集群就非常简单。
下图展示了 Karmada 的架构:

图 11.4:Karmada 架构
Karmada 概念
Karmada 以多个作为 Kubernetes CRD 实现的概念为中心。你使用这些概念定义和更新你的应用程序和服务,Karmada 会确保你的工作负载在多集群系统中正确部署和运行。
让我们来看看这些概念。
ResourceTemplate
资源模板看起来就像常规的 Kubernetes 资源,例如 Deployment 或 StatefulSet,但它并不会实际部署到 Karmada 控制平面。它仅作为一个蓝图,最终将被部署到成员集群。
PropagationPolicy
传播策略决定了资源模板应该部署到哪里。这里有一个简单的传播策略,它将 nginx 部署到两个集群,分别叫做 member1 和 member2:
apiVersion: policy.karmada.io/v1alpha1
kind: PropagationPolicy
metadata:
name: cool-policy
spec:
resourceSelectors:
- apiVersion: apps/v1
kind: Deployment
name: nginx
placement:
clusterAffinity:
clusterNames:
- member1
- member2
OverridePolicy
PropagationPolicy 跨多个集群操作,但有时会有例外。OverridePolicy 允许你应用细粒度规则,以覆盖现有的传播策略。规则有几种类型:
-
ImageOverrider:专门用于覆盖工作负载的镜像 -
CommandOverrider:专门用于覆盖工作负载的命令 -
ArgsOverrider:专门用于覆盖工作负载的参数 -
PlaintextOverrider:一个通用工具,用于覆盖任何类型的资源
额外功能
Karmada 还有更多功能,例如:
-
多集群重新调度
-
重新调度
-
多集群故障切换
-
多集群服务发现
查看 Karmada 文档了解更多细节:karmada.io/docs/。
Clusternet
Clusternet 是一个有趣的项目。它围绕管理多个 Kubernetes 集群的理念展开,目标是像“访问互联网”一样管理集群(因此取名为“Clusternet”)。它支持云端、本地、边缘和混合集群。Clusternet 的核心功能包括:
-
Kubernetes 多集群管理与治理
-
应用协调
-
通过 kubectl 插件提供的 CLI
-
通过对 Kubernetes Client-Go 库的包装进行编程访问
Clusternet 架构
Clusternet 架构类似于 Karmada,但更为简化。存在一个父集群,它运行 Clusternet hub 和 Clusternet scheduler。在每个子集群上,运行一个 Clusternet agent。下图展示了各个组件之间的结构和交互:

图 11.5:Clusternet 架构
Clusternet hub
Hub 有多个角色。它负责批准集群注册请求,并为所有子集群创建命名空间、服务账户和 RBAC 资源。它还充当一个聚合的 API 服务器,维护与子集群上代理的 WebSocket 连接。Hub 还提供类似 Kubernetes 的 API,将请求代理到每个子集群。最后但同样重要的是,Hub 协调从单一资源集群部署应用程序及其依赖项到多个集群。
Clusternet scheduler
Clusternet scheduler 是负责确保资源(在 Clusternet 术语中称为 feeds)根据名为 SchedulingStrategy 的策略在所有子集群间部署和均衡的组件。
Clusternet agent
Clusternet agent 在每个子集群上运行,并与 Hub 通信。子集群上的 agent 相当于节点上的 kubelet。它有多个角色。该 agent 将其子集群注册到父集群。该 agent 向 Hub 提供心跳信息,包含许多内容,如 Kubernetes 版本、运行平台、健康状况、就绪状态和工作负载的存活状态。该 agent 还设置与父集群 Hub 的 WebSocket 连接,以便通过单一 TCP 连接实现全双工通信通道。
多集群部署
Clusternet 将多集群部署建模为订阅和 feeds。它提供一个 Subscription 自定义资源,可以根据不同的标准将一组资源(称为 feeds)部署到多个集群(称为订阅者)。以下是一个 Subscription 示例,它将一个 Namespace、一个 Service 和一个 Deployment 部署到多个集群,并带有 clusters.clusternet.io/cluster-id 标签:
# examples/dynamic-dividing-scheduling/subscription.yaml
apiVersion: apps.clusternet.io/v1alpha1
kind: Subscription
metadata:
name: dynamic-dividing-scheduling-demo
namespace: default
spec:
subscribers: # filter out a set of desired clusters
- clusterAffinity:
matchExpressions:
- key: clusters.clusternet.io/cluster-id
operator: Exists
schedulingStrategy: Dividing
dividingScheduling:
type: Dynamic
dynamicDividing:
strategy: Spread # currently we only support Spread dividing strategy
feeds: # defines all the resources to be deployed with
- apiVersion: v1
kind: Namespace
name: qux
- apiVersion: v1
kind: Service
name: my-nginx-svc
namespace: qux
- apiVersion: apps/v1 # with a total of 6 replicas
kind: Deployment
name: my-nginx
namespace: qux
更多详情请参见 clusternet.io。
Clusterpedia
Clusterpedia 是一个 CNCF 沙箱项目。它的中心隐喻是 Kubernetes 集群的维基百科。它具有多集群搜索、过滤、字段选择和排序等多种能力。这是不同寻常的,因为它是一个只读项目。它不提供帮助管理集群或部署工作负载。它专注于观察您的集群。
Clusterpedia 架构
架构类似于其他多集群项目。有一个控制平面元素,运行 Clusterpedia API 服务器和 ClusterSynchro 管理器组件。对于每个观察到的集群,都有一个专用组件称为集群同步器,将集群的状态同步到 Clusterpedia 的存储层中。架构的一个最有趣的方面是 Clusterpedia 聚合 API 服务器,它使所有集群看起来像一个巨大的逻辑集群。请注意,Clusterpedia API 服务器和 ClusterSynchro 管理器松散耦合,不直接互动。它们只是从共享的存储层读取和写入。

图 11.6:Clusterpedia 架构
让我们看看每个组件,并理解它们的目的是什么。
Clusterpedia API 服务器
Clusterpedia API 服务器是一个聚合 API 服务器。这意味着它会向 Kubernetes API 服务器注册自己,并通过自定义端点扩展标准的 Kubernetes API 服务器。当请求发送到 Kubernetes API 服务器时,它会将其转发到 Clusterpedia API 服务器,后者访问存储层来满足这些请求。Kubernetes API 服务器充当 Clusterpedia 处理的请求的转发层。
这是 Kubernetes 的一个高级方面。我们将在 第十五章 扩展 Kubernetes 中讨论 API 服务器聚合。
ClusterSynchro 管理器
Clusterpedia 观察多个集群以提供其搜索、过滤和聚合功能。一种实现方式是,每当有请求进来时,Clusterpedia 将查询所有观察到的集群,收集结果并返回。这种方法非常问题,因为某些集群可能响应缓慢,并且类似的请求需要返回相同的信息,这是浪费和昂贵的。相反,ClusterSynchro 管理器会集体将每个观察到的集群的状态同步到 Clusterpedia 存储中,Clusterpedia API 服务器可以快速响应。
存储层
存储层是一个抽象层,用于存储所有观察到的集群的状态。它提供了一个可以由不同存储组件实现的统一接口。Clusterpedia API 服务器和 ClusterSynchro 管理器与存储层接口进行交互,从不直接互相通信。
存储组件
存储组件是实际的数据存储,执行存储层接口并存储观察到的集群状态。Clusterpedia 旨在支持不同的存储组件,以便为用户提供灵活性。目前,支持的存储组件包括 MySQL、Postgres 和 Redis。
导入集群
要将集群导入到 Clusterpedia,你需要定义一个 PediaCluster 自定义资源。这非常简单:
apiVersion: cluster.clusterpedia.io/v1alpha2
kind: PediaCluster
metadata:
name: cluster-example
spec:
apiserver: "https://10.30.43.43:6443"
kubeconfig:
caData:
tokenData:
certData:
keyData:
syncResources: []
你需要提供访问集群的凭证,然后 Clusterpedia 会接管并同步其状态。
高级多集群搜索
这是 Clusterpedia 的亮点。你可以通过 API 或 kubectl 访问 Clusterpedia 集群。通过 URL 访问时,它看起来像是你访问了聚合的 API 服务器端点:
kubectl get --raw="/apis/clusterpedia.io/v1beta1/resources/apis/apps/v1/deployments?clusters=cluster-1,cluster-2"
你可以将目标集群指定为查询参数(在此案例中,cluster-1 和 cluster-2)。
通过 kubectl 访问时,你指定目标集群为标签(在此案例中,"search.clusterpedia.io/clusters in (cluster-1,cluster-2)"):
kubectl --cluster clusterpedia get deployments -l "search.clusterpedia.io/clusters in (cluster-1,cluster-2)"
还有其他用于命名空间和资源名称的搜索标签和查询:
-
search.clusterpedia.io/namespaces(查询参数是namespaces) -
search.clusterpedia.io/names(查询参数是names)
还有一个实验性的模糊搜索标签 internalstorage.clusterpedia.io/fuzzy-name 用于资源名称,但没有查询参数。这很有用,因为资源通常会生成带有随机后缀的名称。
你还可以按创建时间进行搜索:
-
search.clusterpedia.io/before(查询参数是before) -
search.clusterpedia.io/since(查询参数是since)
其他功能包括按资源标签或字段选择器进行过滤,以及使用 OrderBy 和 Paging 来组织结果。
资源集合
另一个重要概念是资源集合。标准的 Kubernetes API 提供了一个简单的 REST API,你可以一次列出或获取一种资源。然而,用户通常希望同时获取多种类型的资源。例如,带有特定标签的 Deployment、Service 和 HorizontalPodAutoscaler。这需要通过标准的 Kubernetes API 进行多次调用,即使这些资源都在一个集群中。
Clusterpedia 定义了一个 CollectionResource,将以下类别的资源组合在一起:
-
any(所有资源) -
workloads(Deployments、StatefulSets和DaemonSets) -
kuberesources(除工作负载外的所有资源)
你可以通过传递 API 组和资源类型,在一次 API 调用中搜索任意组合的资源:
kubectl get --raw "/apis/clusterpedia.io/v1beta1/collectionresources/any?onlyMetadata=true&groups=apps&resources=batch/jobs,batch/cronjobs"
详情请见 github.com/clusterpedia-io/clusterpedia。
开放集群管理
开放集群管理 (OCM) 是一个 CNCF 沙箱项目,旨在进行多集群管理,以及多集群调度和工作负载放置。它的特点是紧密遵循许多 Kubernetes 概念,通过插件实现可扩展性,并与其他开源项目(如)进行强集成:
-
Submariner
-
Clusternet(我们之前介绍过的)
-
KubeVela
OCM 的范围涵盖集群生命周期、应用生命周期和治理。
让我们来看一下 OCM 的架构。
OCM 架构
OCM 的架构遵循中心和辐射模型。它有一个中心集群,即 OCM 控制平面,用于管理多个其他集群(辐射集群)。
控制平面的中心集群运行两个控制器:注册控制器和放置控制器。此外,控制平面还运行多个管理插件,它们是 OCM 可扩展性的基础。在每个托管集群上,都有一个所谓的 Klusterlet,它包含一个注册代理和一个工作代理,与中心集群上的注册控制器和放置控制器进行交互。然后,还有插件代理与中心集群上的插件进行交互。
以下图示展示了 OCM 各个组件如何进行通信:

图 11.7:OCM 架构
让我们来看一下 OCM 的不同方面。
OCM 集群生命周期
集群注册是 OCM 安全多集群故事中的重要部分。OCM 以其安全的双重确认握手注册为傲。由于中心-辐射式集群可能有不同的管理员,这种模型为每一方提供了对不希望的请求的保护。每一方都可以随时终止关系。
以下图示展示了注册过程(CSR 表示证书签名请求):

图 11.8:OCM 注册过程
OCM 应用生命周期
OCM 应用生命周期支持在多个集群之间创建、更新和删除资源。
主要的构建块是 ManifestWork 自定义资源,它可以定义多个资源。下面是一个仅包含单个 Deployment 的示例:
apiVersion: work.open-cluster-management.io/v1
kind: ManifestWork
metadata:
namespace: <target managed cluster>
name: awesome-workload
spec:
workload:
manifests:
- apiVersion: apps/v1
kind: Deployment
metadata:
name: hello
namespace: default
spec:
selector:
matchLabels:
app: hello
template:
metadata:
labels:
app: hello
spec:
containers:
- name: hello
image: quay.io/asmacdo/busybox
command:
["sh", "-c", 'echo "Hello, Kubernetes!" && sleep 3600']
ManifestWork 在中心集群上创建,并根据命名空间映射部署到目标集群。每个目标集群在中心集群中都有一个表示它的命名空间。运行在目标集群上的工作代理将监控它们命名空间中所有来自中心集群的 ManifestWork 资源,并同步更改。
OCM 治理、风险与合规性
OCM 提供了一种基于策略、策略模板和策略控制器的治理模型。这些策略可以绑定到特定的集群集合,从而实现精细的控制。
这是一个示例策略,要求存在一个名为 Prod 的命名空间:
apiVersion: policy.open-cluster-management.io/v1
kind: Policy
metadata:
name: policy-namespace
namespace: policies
annotations:
policy.open-cluster-management.io/standards: NIST SP 800-53
policy.open-cluster-management.io/categories: CM Configuration Management
policy.open-cluster-management.io/controls: CM-2 Baseline Configuration
spec:
remediationAction: enforce
disabled: false
policy-templates:
- objectDefinition:
apiVersion: policy.open-cluster-management.io/v1
kind: ConfigurationPolicy
metadata:
name: policy-namespace-example
spec:
remediationAction: inform
severity: low
object-templates:
- complianceType: MustHave
objectDefinition:
kind: Namespace # must have namespace 'prod'
apiVersion: v1
metadata:
name: prod
查看 open-cluster-management.io/ 获取更多详情。
虚拟 Kubelet
虚拟 Kubelet 是一个令人着迷的项目。它伪装成一个 kubelet,将 Kubernetes 连接到其他 API,例如 AWS Fargate 或 Azure ACI。虚拟 Kubelet 对 Kubernetes 集群看起来就像一个节点,但它背后的计算资源是抽象的。虚拟 Kubelet 对 Kubernetes 集群来说看起来就像另一个节点:

图 11.9:虚拟 Kubelet,对 Kubernetes 集群看起来像一个常规节点
虚拟 Kubelet 的特性包括:
-
创建、更新和删除 pod
-
访问容器日志和指标
-
获取 pod、pods 和 pod 状态
-
管理容量
-
访问节点地址、节点容量和节点守护进程端点
-
选择操作系统
-
支持你自己的虚拟网络
详情请见 github.com/virtual-kubelet/virtual-kubelet。
这个概念也可以用来连接多个 Kubernetes 集群,许多项目采用了这种方式。让我们简要了解一些使用虚拟 Kubelet 进行多集群管理的项目,例如 tensile-kube、Admiralty 和 Liqo。
Tensile-kube
Tensile-kube 是 Virtual Kubelet 组织在 GitHub 上的一个子项目。
Tensile-kube 提供以下功能:
-
集群资源的自动发现
-
异步通知 pod 修改
-
完全访问 pod 日志和 kubectl exec
-
pod 的全局调度
-
使用 descheduler 重新调度 pod
-
PV/PVC
-
服务
Tensile-kube 使用上层集群(包含虚拟 Kubelet 的集群)和下层集群(作为虚拟节点在上层集群中暴露的集群)这一术语。
这是 tensile-kube 架构:

图 11.10:Tensile-kube 架构
详情请见 github.com/virtual-kubelet/tensile-kube。
Admiralty
Admiralty 是一个由商业公司支持的开源项目。Admiralty 采用了虚拟 Kubelet 概念,并构建了一个复杂的多集群调度和编排解决方案。目标集群作为源集群中的虚拟节点来表示。它有一个相当复杂的架构,涉及三层调度。每当在代理上创建 pod 时,会在源集群上创建 pods,在每个目标集群上创建候选 pod,最终选定一个候选 pod 作为委托 pod,后者是一个真实的 pod,实际运行其容器。所有这一切都由基于 Kubernetes 调度框架构建的自定义多集群调度器支持。要在 Admiralty 上调度工作负载,你需要为任何 pod 模板添加注解 multicluster.admiralty.io/elect="",然后 Admiralty 将接管后续操作。
这是一个展示不同组件之间相互作用的图示:

图 11.11:Admiralty 架构
Admiralty 提供以下功能:
-
高可用
-
实时灾难恢复
-
动态CDN(内容分发网络)
-
多集群工作流
-
支持边缘计算、物联网(IoT)和 5G
-
治理
-
集群升级
-
集群作为牲畜的抽象
-
全球资源联合
-
云爆发与套利
参见admiralty.io了解更多详情。
Liqo
Liqo 是一个基于液态计算概念的开源项目。让你的任务和数据漂浮并找到最佳运行位置。它的范围非常广泛,不仅针对在多个集群中运行 Pod 的计算方面,还提供了网络架构和存储架构。这些连接集群并跨集群管理数据的方面,通常比单纯运行工作负载更难解决。
在 Liqo 的术语中,管理集群称为本地集群,目标集群称为外部集群。本地集群中的虚拟节点称为“大”节点,它们代表外部集群。
Liqo 利用 IP 地址映射实现了跨所有可能存在内部 IP 冲突的外部集群的扁平 IP 地址空间。
Liqo 过滤并批处理来自外部集群的事件,以减轻本地集群的压力。
下面是 Liqo 架构的图示:

图 11.12:Liqo 架构
参见liqo.io了解更多详情。
接下来,让我们深入了解 Gardener 项目,它采用了不同的方法。
介绍 Gardener 项目
Gardener 项目是由 SAP 开发的开源项目。它使你能够高效、经济地管理成千上万个(没错,是成千上万个!)Kubernetes 集群。Gardener 解决了一个非常复杂的问题,而且解决方案优雅但并不简单。Gardener 是唯一一个同时处理集群生命周期和应用生命周期的项目。
在本节中,我们将涵盖 Gardener 的术语及其概念模型,深入探讨其架构,并了解其可扩展性功能。Gardener 的主要主题是使用 Kubernetes 来管理 Kubernetes 集群。理解 Gardener 的一个好方法是把它看作是 Kubernetes 控制平面即服务。
参见gardener.cloud了解更多详情。
理解 Gardener 的术语
如你所料,Gardener 项目采用了植物学术语来描述其世界。园区是一个 Kubernetes 集群,负责管理种子集群。种子是一个 Kubernetes 集群,负责管理一组 shoot 集群。shoot 集群是运行实际工作负载的 Kubernetes 集群。
Gardener 背后的酷点子是,shoot 集群仅包含工作节点。所有 shoot 集群的控制平面作为 Kubernetes Pods 和服务运行在 seed 集群中。
以下图描述了 Gardener 的结构及其各个组件之间的关系:

图 11.13:Gardener 项目结构
不要慌张!在所有这些复杂性背后,是一个清晰明了的概念模型。
理解 Gardener 的概念模型
Gardener 的架构图可能会让人感到不知所措。让我们慢慢解开它,揭示其背后的基本原则。Gardener 真正拥抱了 Kubernetes 的精神,将管理大量 Kubernetes 集群的复杂性交给 Kubernetes 自身。Gardener 的核心是一个聚合的 API 服务器,通过各种控制器管理一组自定义资源。它充分利用了 Kubernetes 的可扩展性,这种做法在 Kubernetes 社区中很常见。定义一组自定义资源,让 Kubernetes 来为你管理它们。Gardener 的创新之处在于,它将这一方法推向极致,甚至抽象化了 Kubernetes 基础设施的某些部分。
在“正常”的 Kubernetes 集群中,控制平面与工作节点运行在同一个集群中。通常,在大型集群中,Kubernetes API 服务器和 etcd 等控制平面组件会运行在专用节点上,不与工作节点混合。Gardener 从多个集群的角度进行思考,它将所有 shoot 集群的控制平面集中管理,并且有一个种子集群来管理它们。因此,shoot 集群的 Kubernetes 控制平面作为常规的 Kubernetes Deployments 在种子集群中进行管理,这样 Kubernetes 会自动提供复制、监控、自愈和滚动更新等功能。
所以,Kubernetes shoot 集群的控制平面类似于一个 Deployment。而种子集群则映射到一个 Kubernetes 节点,它管理多个 shoot 集群。建议每个云提供商使用一个种子集群。Gardener 的开发人员实际上在为种子集群开发一个 gardenlet 控制器,它类似于节点上的 kubelet。
如果种子集群像 Kubernetes 节点,那么管理这些种子集群的 Garden 集群就像是管理其工作节点的 Kubernetes 集群。
通过大力推进 Kubernetes 模型,Gardener 项目利用了 Kubernetes 的优势,达到了构建一个从零开始非常难实现的鲁棒性和性能。
让我们深入探讨架构。
深入了解 Gardener 架构
Gardener 会在种子集群中为每个 shoot 集群创建一个 Kubernetes 命名空间。它将 shoot 集群的证书作为 Kubernetes 密钥存储在种子集群中。
管理集群状态
每个集群的 etcd 数据存储作为一个带有单个副本的 StatefulSet 部署。此外,事件会存储在一个独立的 etcd 实例中。etcd 数据会定期快照,并存储在远程存储中,以备份和恢复使用。这使得当集群丧失控制平面时(例如,整个种子集群变得无法访问时),能够非常快速地恢复。请注意,当种子集群出现故障时,shoot 集群仍会照常运行。
管理控制平面
如前所述,射出集群 X 的控制平面运行在一个独立的种子集群中,而工作节点运行在射出集群中。这意味着射出集群中的 Pods 可以使用内部 DNS 进行相互定位,但与运行在种子集群中的 Kubernetes API 服务器的通信必须通过外部 DNS 来完成。这意味着 Kubernetes API 服务器作为 LoadBalancer 类型的 Service 运行。
准备基础设施
在创建新的射出集群时,提供必要的基础设施非常重要。Gardener 使用 Terraform 来完成这项任务。根据射出集群的规格,动态生成一个 Terraform 脚本,并将其作为 ConfigMap 存储在种子集群中。为了简化这一过程,一个专用组件(Terraformer)作为一个作业运行,执行所有的资源配置,然后将状态写入一个单独的 ConfigMap 中。
使用机器控制器管理器
为了以与提供商无关的方式配置节点,Gardener 提供了多个自定义资源,例如 MachineDeployment、MachineClass、MachineSet 和 Machine。它们与 Kubernetes 集群生命周期组协作,以统一其抽象,因为它们之间有很多重叠。此外,Gardener 还利用了集群自动扩缩器,以减轻节点池扩展和收缩的复杂性。
跨集群网络
种子集群和射出集群可以运行在不同的云提供商上。射出集群中的工作节点通常部署在私有网络中。由于控制平面需要与工作节点(主要是 kubelet)紧密交互,Gardener 创建了一个 VPN 以便直接通信。
监控集群
可观测性是运营复杂分布式系统的重要组成部分。Gardener 提供了丰富的监控功能,内置了顶级的开源项目,如部署在花园集群中的中央 Prometheus 服务器,收集所有种子集群的信息。此外,每个射出集群都会在种子集群中获得一个独立的 Prometheus 实例。为了收集指标,Gardener 为每个集群部署了两个 kube-state-metrics 实例(一个用于种子集群中的控制平面,一个用于射出集群中的工作节点)。节点导出器也被部署,用于提供有关节点的额外信息。Prometheus AlertManager 用于在出现问题时通知操作员。Grafana 用于显示包含系统状态相关数据的仪表板。
gardenctl CLI
你可以仅使用 kubectl 来管理 Gardener,但在探索不同集群时,你需要频繁切换配置文件和上下文。Gardener 提供了 gardenctl 命令行工具,它提供了更高层次的抽象,并且可以同时操作多个集群。以下是一个示例:
$ gardenctl ls shoots
projects:
- project: team-a
shoots:
- dev-eu1
- prod-eu1
$ gardenctl target shoot prod-eu1
[prod-eu1]
$ gardenctl show prometheus
NAME READY STATUS RESTARTS AGE IP NODE
prometheus-0 3/3 Running 0 106d 10.241.241.42 ip-10-240-7-72.eu-central-1.compute.internal
URL: https://user:password@p.prod-eu1.team-a.seed.aws-eu1.example.com
Gardener 的一个突出特点是其可扩展性。它有着广泛的适用范围,支持许多环境。让我们来看一下它是如何在设计中实现可扩展性的。
扩展 Gardener
Gardener 支持以下环境:
-
阿里云
-
AWS
-
Azure
-
Equinix Metal
-
GCP
-
OpenStack
-
vSphere
它像 Kubernetes 一样,从主 Gardener 仓库中的大量特定于提供商的支持开始。随着时间的推移,它借鉴了 Kubernetes 的做法,将云提供商外部化,并将这些提供商迁移到独立的 Gardener 扩展中。可以使用 CloudProfile CRD 来指定提供商,例如:
apiVersion: core.gardener.cloud/v1beta1
kind: CloudProfile
metadata:
name: aws
spec:
type: aws
kubernetes:
versions:
- version: 1.24.3
- version: 1.23.8
expirationDate: "2022-10-31T23:59:59Z"
machineImages:
- name: coreos
versions:
- version: 2135.6.0
machineTypes:
- name: m5.large
cpu: "2"
gpu: "0"
memory: 8Gi
usable: true
volumeTypes:
- name: gp2
class: standard
usable: true
- name: io1
class: premium
usable: true
regions:
- name: eu-central-1
zones:
- name: eu-central-1a
- name: eu-central-1b
- name: eu-central-1c
providerConfig:
apiVersion: aws.provider.extensions.gardener.cloud/v1alpha1
kind: CloudProfileConfig
machineImages:
- name: coreos
versions:
- version: 2135.6.0
regions:
- name: eu-central-1
ami: ami-034fd8c3f4026eb39
# architecture: amd64 # optional
然后,一个 shoot 集群将选择一个提供商,并使用必要的信息进行配置:
apiVersion: gardener.cloud/v1alpha1
kind: Shoot
metadata:
name: johndoe-aws
namespace: garden-dev
spec:
cloudProfileName: aws
secretBindingName: core-aws
cloud:
type: aws
region: eu-west-1
providerConfig:
apiVersion: aws.cloud.gardener.cloud/v1alpha1
kind: InfrastructureConfig
networks:
vpc: # specify either 'id' or 'cidr'
# id: vpc-123456
cidr: 10.250.0.0/16
internal:
- 10.250.112.0/22
public:
- 10.250.96.0/22
workers:
- 10.250.0.0/19
zones:
- eu-west-1a
workerPools:
- name: pool-01
# Taints, labels, and annotations are not yet implemented. This requires interaction with the machine-controller-manager, see
# https://github.com/gardener/machine-controller-manager/issues/174\. It is only mentioned here as future proposal.
# taints:
# - key: foo
# value: bar
# effect: PreferNoSchedule
# labels:
# - key: bar
# value: baz
# annotations:
# - key: foo
# value: hugo
machineType: m4.large
volume: # optional, not needed in every environment, may only be specified if the referenced CloudProfile contains the volumeTypes field
type: gp2
size: 20Gi
providerConfig:
apiVersion: aws.cloud.gardener.cloud/v1alpha1
kind: WorkerPoolConfig
machineImage:
name: coreos
ami: ami-d0dcef3
zones:
- eu-west-1a
minimum: 2
maximum: 2
maxSurge: 1
maxUnavailable: 0
kubernetes:
version: 1.11.0
...
dns:
provider: aws-route53
domain: johndoe-aws.garden-dev.example.com
maintenance:
timeWindow:
begin: 220000+0100
end: 230000+0100
autoUpdate:
kubernetesVersion: true
backup:
schedule: "*/5 * * * *"
maximum: 7
addons:
kube2iam:
enabled: false
kubernetes-dashboard:
enabled: true
cluster-autoscaler:
enabled: true
nginx-ingress:
enabled: true
loadBalancerSourceRanges: []
kube-lego:
enabled: true
email: john.doe@example.com
但是,Gardener 的可扩展性目标远不止于仅仅做到与提供商无关。搭建 Kubernetes 集群的整个过程涉及许多步骤。Gardener 项目的目标是让运维人员通过定义自定义资源和 Webhook,来定制每一个步骤。以下是与每个步骤相关的 CRD、变更/验证准入控制器和 Webhook 的总体流程图:

图 11.14:CRD 变更和验证准入控制器的流程图
以下是构成 Gardener 可扩展性空间的 CRD 类别:
-
用于 DNS 管理的提供商,如 Route53 和 CloudDNS
-
用于 Blob 存储的提供商,包括 S3、GCS 和 ABS
-
基础设施提供商,如 AWS、GCP 和 Azure
-
支持 CoreOS Container Linux、Ubuntu 和 FlatCar Linux 等各种操作系统
-
网络插件,如 Calico、Flannel 和 Cilium
-
可选扩展,例如 Let’s Encrypt 的证书服务
我们已经深入探讨了 Gardener,这也标志着本章的结束。
总结
在本章中,我们探讨了多集群管理这一激动人心的领域。有许多项目从不同的角度来解决这个问题。Cluster API 项目在解决多个集群生命周期管理的子问题上有着很大的动力。许多其他项目则着眼于资源管理和应用生命周期。这些项目可以分为两类:一类是通过管理集群和被管理集群来显式管理多个集群,另一类是利用虚拟 Kubelet,将整个集群作为虚拟节点呈现在主集群中。
Gardener 项目有一种非常有趣的方式和架构。它从不同的角度解决了多个集群的问题,并专注于集群的大规模管理。它是唯一同时处理集群生命周期和应用生命周期的项目。
在这一点上,你应该已经清楚地了解了当前多集群管理的状况以及不同项目所提供的功能。你可以决定是觉得现在还为时过早,还是准备勇敢尝试。
在下一章,我们将探索 Kubernetes 上激动人心的无服务器计算世界。无服务器可以有两种不同的含义:你不需要为长时间运行的工作负载管理服务器,同时,还可以将函数作为服务运行。Kubernetes 提供了这两种形式的无服务器计算,它们都非常有用。
第十二章:Kubernetes 上的无服务器计算
本章我们将探索云中无服务器计算的迷人世界。术语“无服务器”正在获得大量关注,但它其实是一个误称。一个真正的无服务器应用程序在用户的浏览器或移动应用中运行,并且只与外部服务进行交互。然而,我们在 Kubernetes 上构建的无服务器系统类型是不同的。我们将详细解释在 Kubernetes 中“无服务器”的含义,以及它如何与其他无服务器解决方案相关联。我们将介绍无服务器云解决方案,介绍 Knative——Kubernetes 上的函数即服务基础设施——并深入探讨 Kubernetes 的函数即服务(FaaS)框架。
本章将涵盖以下主要内容:
-
理解无服务器计算
-
云中的无服务器 Kubernetes
-
Knative
-
Kubernetes FaaS 框架
让我们从澄清无服务器计算的概念开始。
理解无服务器计算
好的,先说清楚一点。服务器依然存在。术语“无服务器”意味着你无需自行配置、管理和维护服务器。公共云平台通过消除处理物理硬件、数据中心和网络的需求,真正带来了范式的转变。但即使在云端,创建机器镜像、配置实例、对其进行配置、升级和修补操作系统、定义网络策略、管理证书和访问控制等工作,仍然需要大量的技术和精力。借助无服务器计算,大部分繁琐但重要的工作得以消除。无服务器的吸引力有多方面:
-
与资源配置相关的一整类问题被消除了
-
容量规划不再是问题
-
你只为实际使用的部分付费
你会失去一些控制权,因为你必须接受云服务提供商做出的选择,但你可以在系统的关键部分进行大量的自定义。当然,如果你需要完全的控制权,你仍然可以通过显式地配置虚拟机(VM)并直接部署工作负载来管理自己的基础设施。
归根结底,无服务器方法不仅仅是炒作,它确实带来了实际的好处。让我们来探讨无服务器的两种形式。
在“无服务器”基础设施上运行长时间运行的服务
长时间运行的服务是基于微服务的分布式系统的核心。这些服务必须始终在线,等待服务请求,并且可以根据请求量进行扩展或收缩。在传统云中,你必须配置足够的容量以应对流量波动和变化,这通常导致过度配置,或者当请求等待不足配置的服务时,会增加处理延迟。
无服务器服务通过零开发者努力和相对较少的操作员努力解决了这个问题。其思想是,您只需标记您的服务在无服务器基础设施上运行,并配置一些参数,例如期望的 CPU、内存和扩展限制。该服务对其他服务和客户端的表现就像您自己在基础设施上部署的传统服务一样。
属于此类别的服务具有以下特征:
-
始终运行(它们永远不会缩减到零)
-
暴露多个端点(例如 HTTP 和 gRPC)
-
需要您自己实现请求处理和路由
-
可以监听事件,而不是仅仅暴露端点,或者在此基础上再暴露端点
-
服务实例可以维持内存缓存、长期连接和会话
-
在 Kubernetes 中,微服务直接由服务资源表示
现在,让我们来看一下 FaaS。
在“无服务器”基础设施上运行函数作为服务
即使在最大的分布式系统中,也不是每个工作负载都能处理每秒多个请求。总是有一些任务需要响应相对不频繁的事件,无论是按计划执行还是临时触发。虽然可以让一个长时间运行的服务就这么静静地等待,偶尔处理一个请求,但这是浪费资源。你可以尝试将这类任务挂接到其他长期运行的服务上,但这会导致非常不希望出现的耦合,违背了微服务的理念。
一种更好的方法是将这些任务单独处理,并提供不同的抽象和工具来解决它们,这就是 FaaS。
FaaS 是一种计算模型,在这种模型中,中央权威机构(例如云服务提供商或 Kubernetes)为用户提供了一种运行代码(本质上是函数)的方法,而无需担心代码运行的位置。
Kubernetes 有Job和CronJob对象的概念。它们解决了 FaaS 解决方案所处理的一些问题,但并不完全。
与传统服务相比,FaaS 解决方案通常更简单,开发人员可能只需要编写函数的代码;FaaS 解决方案会处理剩下的部分:
-
构建和打包
-
作为端点暴露
-
基于事件的触发器
-
自动化的资源配置和扩展
-
监控并提供日志和指标
以下是 FaaS 解决方案的一些特性:
-
按需运行(可以扩展到零)
-
它暴露一个单一端点(通常是 HTTP)
-
它可以通过事件触发或获得自动端点
-
它通常对资源使用和最大运行时间有严格的限制
-
有时,它可能会有冷启动(即,从零扩展时)
FaaS 确实是一种无服务器计算形式,因为用户无需为运行代码而配置服务器,但它用于运行短期函数。还有另一种形式的无服务器计算,用于运行长期服务。
云中的无服务器 Kubernetes
现在所有主要的云服务提供商都支持 Kubernetes 的无服务器长时间运行服务。微软 Azure 是第一个提供此功能的云服务商。Kubernetes 通过 kubelet 与节点交互。无服务器基础设施的基本思想是,不是预配置实际的节点(物理机或虚拟机),而是以某种方式创建一个虚拟节点。不同的云服务提供商使用不同的解决方案来实现这一目标。
不要忘记集群自动扩展器
在深入了解特定云服务提供商的解决方案之前,务必先查看集群自动扩展器的 Kubernetes 原生选项。集群自动扩展器会自动调整集群中的节点,并且没有其他一些解决方案的局限性。由于它只是自动化地向集群中添加和移除常规节点,因此所有 Kubernetes 调度和控制机制都可以开箱即用。没有使用任何特定于提供商的特殊功能。
但你可能有充分的理由更倾向于选择一个更集成的解决方案。例如,AWS Fargate 运行在 Firecracker 内(见 github.com/firecracker-microvm/firecracker),它是一个具有强大安全边界的轻量级虚拟机(顺便提一下,Lambda 函数也运行在 Firecracker 上)。同样,Google Cloud Run 运行在 gVisor 上。Azure 提供了几种不同的托管解决方案,如专用虚拟机、Kubernetes 和 Arc。
Azure AKS 和 Azure 容器实例
Azure 长期支持 Azure 容器实例(ACI)。ACI 并非 Kubernetes 专用。它允许你在 Azure 上的受管环境中按需运行容器。它在某些方面与 Kubernetes 相似,但仅限于 Azure。它甚至有一个类似于 Pod 的容器组的概念。容器组中的所有容器将被调度到同一主机上运行。

图 12.1:ACI 架构
与 Kubernetes/AKS 的集成被建模为从 AKS 到 ACI 的突发扩展。这里的指导原则是,对于已知的工作负载,你应该预配置自己的节点,但如果出现流量高峰,额外的负载将动态地扩展到 ACI。这种方法被认为更具经济性,因为在 ACI 上运行的成本高于预配置自己的节点。AKS 使用我们在上一章中探讨过的虚拟 kubelet CNCF 项目,将你的 Kubernetes 集群与 ACI 的无限容量进行集成。其工作原理是通过向你的集群中添加一个由 ACI 支持的虚拟节点,该节点在 Kubernetes 端表现为一个具有无限资源的单一节点。

图 12.2:AKS 中的虚拟节点架构
让我们看看 AWS 如何通过 EKS 和 Fargate 实现这一点。
AWS EKS 和 Fargate
AWS 于 2018 年发布了 Fargate(aws.amazon.com/fargate),它类似于 Azure ACI,允许你在受管环境中运行容器。最初,你可以在 EC2 或 ECS(AWS 的专有容器编排服务)上使用 Fargate。在 2019 年的 AWS 重大会议 re:Invent 上,Fargate 也开始在 EKS 上普遍可用。这意味着你现在拥有一个真正的无服务器 Kubernetes 解决方案。EKS 负责控制平面,Fargate 为你管理工作节点。

图 12.3:EKS 和 Fargate 架构
EKS 和 Fargate 模拟了 Kubernetes 集群与 Fargate 之间的交互方式,这与 AKS 和 ACI 的方式有所不同。虽然在 AKS 中,一个无限的虚拟节点代表了 ACI 的所有容量,但在 EKS 中,每个 Pod 都会获得自己的虚拟节点。但这些节点当然不是实际的节点。Fargate 拥有自己的控制平面和数据平面,支持 EC2、ECS 以及 EKS。EKS-Fargate 的集成是通过一组自定义的 Kubernetes 控制器完成的,这些控制器会监控需要部署到特定命名空间或具有特定标签的 Pod,并将这些 Pod 转发给 Fargate 安排调度。下图展示了从 EKS 到 Fargate 的工作流。

图 12.4:EKS 到 Fargate 工作流
在使用 Fargate 时,有几个限制需要注意:
-
每个 Pod 最多可配置 16 vCPU 和 120 GB 内存
-
20 GiB 容器镜像层存储
-
不支持需要持久卷或文件系统的有状态工作负载
-
不支持
Daemonsets、特权 Pod 或使用HostNetwork或HostPort的 Pod -
你可以使用应用程序负载均衡器或网络负载均衡器
如果这些限制对你来说过于严格,你可以尝试一种更直接的方法,利用虚拟 kubelet 项目将 Fargate 集成到你的集群中。
那么,Google——Kubernetes 的父亲呢?
Google Cloud Run
这可能会让人吃惊,但 Google 其实是无服务器 Kubernetes 的后起之秀。Cloud Run 是 Google 的无服务器服务。它基于 Knative,接下来我们将深入探讨它。基本的前提是,Cloud Run 有两种版本。普通的 Cloud Run 类似于 ACI 和 Fargate,它让你在 Google 完全托管的环境中运行容器。Cloud Run for Anthos 支持 GKE,并且在本地环境中可以让你在 GKE 集群中运行容器化工作负载。
Cloud Run for Anthos 目前是唯一允许你在自定义机器类型(包括 GPU)上运行容器的无服务器平台。Anthos Cloud Run 服务参与 Istio 服务网格,并提供流畅的 Kubernetes 原生体验。更多详情请参见 cloud.google.com/anthos/service-mesh。
请注意,虽然托管的 Cloud Run 使用 gVisor 隔离,Anthos Cloud Run 使用的是标准的 Kubernetes(基于容器)隔离。
下图展示了两种模型以及访问方法和部署选项的层次结构:

图 12.5:Cloud Run 模型
是时候深入了解 Knative 了。
Knative
Kubernetes 本身并不内置对 FaaS 的支持。因此,许多解决方案是由社区和生态系统开发的。Knative 的目标是提供多个 FaaS 解决方案可以利用的构建模块,而无需重新发明轮子。
但这还不是全部!Knative 还提供了一个独特的功能,可以将长时间运行的服务缩放到零。这是一个大新闻。有许多用例中,你可能更倾向于使用一个可以处理大量快速连续请求的长时间运行服务。在这些情况下,每个请求启动一个新的函数实例并不是最佳做法。但当没有流量时,将服务缩放到零实例,不支付任何费用,并为可能需要更多资源的其他服务腾出更多容量,是非常棒的。Knative 还支持其他重要的用例,如基于百分比的负载均衡、基于指标的负载均衡、蓝绿部署、金丝雀部署和高级路由。它甚至可以选择性地自动处理 TLS 证书以及 HTTP 监控。最后,Knative 支持 HTTP 和 gRPC。
当前有两个 Knative 组件:Knative Serving 和 Knative Eventing。曾经还有一个 Knative Build 组件,但它被拆分出去,成为 Tekton (github.com/tektoncd/pipeline) 的基础——一个 Kubernetes 原生的 CD 项目。
让我们从 Knative Serving 开始。
Knative Serving
Knative Serving 的领域是运行版本化的服务并在 Kubernetes 上路由流量到这些服务。这超出了标准 Kubernetes 服务的范围。Knative Serving 定义了几个 CRD 来建模其领域:Service、Route、Configuration 和 Revision。Service 管理一个 Route 和一个 Configuration。一个 Configuration 可以有多个修订版本。
Route 可以将服务流量路由到特定的修订版本。以下是一个图示,展示了不同对象之间的关系:

图 12.6:Knative Serving CRDs
让我们尝试在本地环境中使用 Knative Serving。
安装快速入门环境
Knative 提供了一个简便的开发环境。让我们安装 kn CLI 和快速入门插件。按照此处的说明进行操作:knative.dev/docs/getting-started/quickstart-install。
现在,我们可以使用 KinD 运行插件,它将提供一个新的 KinD 集群并安装多个组件,如 Knative-service、Kourier 网络层和 Knative 事件处理。
$ kn quickstart kind
Running Knative Quickstart using Kind
 Checking dependencies...
Kind version is: 0.16.0
 Creating Kind cluster...
Creating cluster "knative" ...
 Ensuring node image (kindest/node:v1.24.3) 
 Preparing nodes 
 Writing configuration 
 Starting control-plane 
 Installing CNI 
 Installing StorageClass 
 Waiting ≤ 2m0s for control-plane = Ready  kind-knative | default
• Ready after 19s 
Set kubectl context to "kind-knative"
You can now use your cluster with:
kubectl cluster-info --context kind-knative
Have a nice day! 
 Installing Knative Serving v1.6.0 ...
CRDs installed...
Core installed...
Finished installing Knative Serving
 Installing Kourier networking layer v1.6.0 ...
Kourier installed...
Ingress patched...
Finished installing Kourier Networking layer
 Configuring Kourier for Kind...
Kourier service installed...
Domain DNS set up...
Finished configuring Kourier
 Installing Knative Eventing v1.6.0 ...
CRDs installed...
Core installed...
In-memory channel installed...
Mt-channel broker installed...
Example broker installed...
Finished installing Knative Eventing
 Knative install took: 2m22s
 Now have some fun with Serverless and Event Driven Apps!
让我们安装示例 hello 服务:
$ kn service create hello \
--image gcr.io/knative-samples/helloworld-go \
--port 8080 \
--env TARGET=World
Creating service 'hello' in namespace 'default':
0.080s The Route is still working to reflect the latest desired specification.
0.115s ...
0.127s Configuration "hello" is waiting for a Revision to become ready.
21.229s ...
21.290s Ingress has not yet been reconciled.
21.471s Waiting for load balancer to be ready
21.665s Ready to serve.
Service 'hello' created to latest revision 'hello-00001' is available at URL:
http://hello.default.127.0.0.1.sslip.io
我们可以使用 httpie 调用服务,并获得 Hello, World! 的响应:
$ http --body http://hello.default.127.0.0.1.sslip.io
Hello World!
让我们看一下 Service 对象。
Knative Service 对象
Knative Service 将 Kubernetes 的 Deployment 和 Service 合并为一个对象。这是有道理的,因为除了无头服务的特殊情况 (kubernetes.io/docs/concepts/services-networking/service/#headless-services),每个服务背后总会有一个部署。
Knative Service 会自动管理其工作负载的整个生命周期。它负责创建路由、配置和每次服务更新时的新版本。这非常方便,因为用户只需要处理 Service 对象。
这是 helloworld-go Knative 服务的元数据:
$ k get ksvc hello -o json | jq .metadata
{
"annotations": {
"serving.knative.dev/creator": "kubernetes-admin",
"serving.knative.dev/lastModifier": "kubernetes-admin"
},
"creationTimestamp": "2022-09-25T21:11:21Z",
"generation": 1,
"name": "hello",
"namespace": "default",
"resourceVersion": "19380",
"uid": "03b5c668-3934-4260-bdba-13357a48501e"
}
这是规格说明:
$ k get ksvc hello -o json | jq .spec
{
"template": {
"metadata": {
"annotations": {
"client.knative.dev/updateTimestamp": "2022-09-25T21:11:21Z",
"client.knative.dev/user-image": "gcr.io/knative-samples/helloworld-go"
},
"creationTimestamp": null
},
"spec": {
"containerConcurrency": 0,
"containers": [
{
"env": [
{
"name": "TARGET",
"value": "World"
}
],
"image": "gcr.io/knative-samples/helloworld-go",
"name": "user-container",
"ports": [
{
"containerPort": 8080,
"protocol": "TCP"
}
],
"readinessProbe": {
"successThreshold": 1,
"tcpSocket": {
"port": 0
}
},
"resources": {}
}
],
"enableServiceLinks": false,
"timeoutSeconds": 300
}
},
"traffic": [
{
"latestRevision": true,
"percent": 100
}
]
}
请注意规格中的 traffic 部分,它将 100% 的请求引导到最新版本。这决定了 Route CRD。
创建新版本
让我们创建一个新的 hello 服务版本,并设置 TARGET 环境变量为 Knative:
$ kn service update hello --env TARGET=Knative
Updating Service 'hello' in namespace 'default':
0.097s The Configuration is still working to reflect the latest desired specification.
3.000s Traffic is not yet migrated to the latest revision.
3.041s Ingress has not yet been reconciled.
3.155s Waiting for load balancer to be ready
3.415s Ready to serve.
现在,我们有两个版本:
$ k get revisions
NAME CONFIG NAME K8S SERVICE NAME GENERATION READY REASON ACTUAL REPLICAS DESIRED REPLICAS
hello-00001 hello 1 True 0 0
hello-00002 hello 2 True 1 1
hello-00002 版本是当前活动版本。让我们确认一下:
$ http --body http://hello.default.127.0.0.1.sslip.io
Hello Knative!
Knative Route 对象
Knative Route 对象允许你将一定比例的传入请求引导到特定版本。默认情况下是 100% 的流量指向最新版本,但你可以进行更改。这允许更高级的部署场景,比如蓝绿部署以及金丝雀部署。
这是将 100% 流量引导到最新版本的 hello 路由:
apiVersion: serving.knative.dev/v1
kind: Route
metadata:
annotations:
serving.knative.dev/creator: kubernetes-admin
serving.knative.dev/lastModifier: kubernetes-admin
labels:
serving.knative.dev/service: hello
name: hello
namespace: default
spec:
traffic:
- configurationName: hello
latestRevision: true
percent: 100
让我们将 50% 的流量引导到之前的版本:
$ kn service update hello \
--traffic hello-00001=50 \
--traffic @latest=50
Updating Service 'hello' in namespace 'default':
0.078s The Route is still working to reflect the latest desired specification.
0.124s Ingress has not yet been reconciled.
0.192s Waiting for load balancer to be ready
0.399s Ready to serve.
Service 'hello' with latest revision 'hello-00002' (unchanged) is available at URL:
http://hello.default.127.0.0.1.sslip.io
现在,如果我们反复调用该服务,会看到来自两个版本的混合响应:
$ while true; do http --body http://hello.default.127.0.0.1.sslip.io; done
Hello World!
Hello World!
Hello World!
Hello Knative!
Hello Knative!
Hello Knative!
Hello Knative!
Hello World!
Hello Knative!
Hello World!
让我们使用 neat kubectl 插件查看路由 (github.com/itaysk/kubectl-neat):
$ k get route hello -o yaml | k neat
apiVersion: serving.knative.dev/v1
kind: Route
metadata:
annotations:
serving.knative.dev/creator: kubernetes-admin
serving.knative.dev/lastModifier: kubernetes-admin
labels:
serving.knative.dev/service: hello
name: hello
namespace: default
spec:
traffic:
- configurationName: hello
latestRevision: true
percent: 50
- latestRevision: false
percent: 50
revisionName: hello-00001
Knative 配置对象
Configuration CRD 包含服务的最新版本和版本数量。例如,如果我们将服务更新到版本 2:
apiVersion: serving.knative.dev/v1 # Current version of Knative
kind: Service
metadata:
name: helloworld-go # The name of the app
namespace: default # The namespace the app will use
spec:
template:
spec:
containers:
- image: gcr.io/knative-samples/helloworld-go # The URL to the image of the app
env:
- name: TARGET # The environment variable printed out by the sample app
value: "Yeah, it still works - version 2 !!!"
Knative 还会生成一个配置对象,该对象现在指向 hello-00002 版本:
$ k get configuration hello -o yaml
apiVersion: serving.knative.dev/v1
kind: Configuration
metadata:
annotations:
serving.knative.dev/creator: kubernetes-admin
serving.knative.dev/lastModifier: kubernetes-admin
serving.knative.dev/routes: hello
creationTimestamp: "2022-09-25T21:11:21Z"
generation: 2
labels:
serving.knative.dev/service: hello
serving.knative.dev/serviceUID: 03b5c668-3934-4260-bdba-13357a48501e
name: hello
namespace: default
ownerReferences:
- apiVersion: serving.knative.dev/v1
blockOwnerDeletion: true
controller: true
kind: Service
name: hello
uid: 03b5c668-3934-4260-bdba-13357a48501e
resourceVersion: "22625"
uid: fabfcb7c-e3bc-454e-a887-9f84057943f7
spec:
template:
metadata:
annotations: kind-knative | default
client.knative.dev/updateTimestamp: "2022-09-25T21:21:00Z"
client.knative.dev/user-image: gcr.io/knative-samples/helloworld-go
creationTimestamp: null
spec:
containerConcurrency: 0
containers:
- env:
- name: TARGET
value: Knative
image: gcr.io/knative-samples/helloworld-go@sha256:5ea96ba4b872685ff4ddb5cd8d1a97ec18c18fae79ee8df0d29f446c5efe5f50
name: user-container
ports:
- containerPort: 8080
protocol: TCP
readinessProbe:
successThreshold: 1
tcpSocket:
port: 0
resources: {}
enableServiceLinks: false
timeoutSeconds: 300
status:
conditions:
- lastTransitionTime: "2022-09-25T21:21:03Z"
status: "True"
type: Ready
latestCreatedRevisionName: hello-00002
latestReadyRevisionName: hello-00002
observedGeneration: 2
总结一下,Knative serving 为 Kubernetes 提供了更好的部署和网络支持,适用于长时间运行的服务和功能。接下来,让我们看看 Knative 事件驱动带来了什么。
Knative 事件驱动
Kubernetes 或其他系统上的传统服务暴露 API 端点,消费者可以通过这些端点(通常是 HTTP)发送请求进行处理。请求-响应模式非常有用,因此它非常流行。然而,这并不是调用服务或功能的唯一模式。大多数分布式系统都有某种形式的松耦合交互,其中事件会被发布。通常,当事件发生时,需要调用一些代码。
在 Knative 之前,你需要自己构建这个能力,或者使用一些第三方库将事件与代码绑定。Knative 事件处理旨在提供一种标准的方式来完成这项任务。它兼容 CNCF 的 CloudEvents 规范(github.com/cloudevents/spec)。
熟悉 Knative 事件处理的术语
在深入了解架构之前,让我们先定义一些我们稍后会用到的术语和概念。
事件消费者
有两种类型的事件消费者:可寻址的和可调用的。可寻址的消费者可以通过其 status.address.url 字段通过 HTTP 接收事件。Kubernetes 的 Service 对象没有这样的字段,但它也被视为一个特殊类型的可寻址消费者。
可调用消费者通过 HTTP 接收事件,并且它们可以在响应中返回另一个事件,该事件会像外部事件一样被消费。可调用消费者提供了一种有效的方式来转换事件。
事件源
事件源是事件的发起者。Knative 支持许多常见的事件源,你也可以编写自己的自定义事件源。以下是一些支持的事件源:
-
AWS SQS
-
Apache Camel
-
Apache CouchDB
-
Apache Kafka
-
Bitbucket
-
ContainerSource
-
Cron 作业
-
GCP PubSub
-
GitHub
-
GitLab
-
Google Cloud Scheduler
-
Kubernetes(Kubernetes 事件)
查看完整的事件源列表:knative.dev/docs/eventing/sources。
经纪人和触发器
经纪人调解由特定属性标识的事件,并通过触发器将这些事件与消费者匹配。触发器包括事件属性的过滤器和一个可寻址的消费者。当事件到达经纪人时,它会将事件转发给那些触发器过滤器与事件属性匹配的消费者。以下图示说明了这一工作流程:

图 12.7:经纪人、触发器和服务的工作流程
事件类型和事件注册
事件可以有一个类型,这个类型被建模为 EventType CRD。事件注册存储所有的事件类型。触发器可以使用事件类型作为其筛选标准之一。
渠道和订阅
渠道是一个可选的持久化层。不同的事件类型可以被路由到不同的渠道,并使用不同的存储后端。一些渠道可能将事件存储在内存中,而其他渠道则可能通过 NATS 流式传输、Kafka 或类似技术将事件持久化到磁盘。订阅者(消费者)最终会接收并处理这些事件。
现在我们已经了解了 Knative 事件处理的各个部分,接下来让我们理解它的架构。
Knative 事件处理的架构
当前架构支持两种事件传递模式:
-
简单传递
-
扩展式传递
简单传递就是 1:1 源 -> 消费者。消费者可以是核心 Kubernetes 服务或 Knative 服务。如果消费者无法访问,源负责处理无法传递事件的情况。源可以重试、记录错误或采取其他适当的行动。
下图展示了这一简单概念:

图 12.8:简单传递
分发传递支持任意复杂的处理,其中多个消费者订阅同一个通道上的事件。一旦事件被通道接收,源就不再负责该事件。这允许更动态的消费者订阅,因为源甚至不知道消费者是谁。从本质上讲,生产者和消费者之间是松耦合的。
下图展示了使用通道时可能出现的复杂处理和订阅模式:

图 12.9:分发传递
到目前为止,你应该对 Knative 的范围以及它如何为 Kubernetes 建立一个稳固的无服务器基础有了相当好的理解。接下来,让我们稍微玩一下 Knative,看看它的感觉如何。
检查 Knative 的零扩缩选项
Knative 默认配置为零扩缩,宽限期为 30 秒。这意味着,在 30 秒的非活动期(没有请求进入)后,所有 pod 将被终止,直到有新的请求进入。为了验证这一点,我们可以等待 30 秒并检查默认命名空间中的 pod:
$ kubectl get po
No resources found in default namespace.
然后,我们可以调用服务,稍等片刻后,我们就能得到响应:
$ http --body http://hello.default.127.0.0.1.sslip.io
Hello World!
让我们通过使用-w标志来观察 pod 何时消失:
$ k get po -w
NAME READY STATUS RESTARTS AGE
hello-00001-deployment-7c4b6cc4df-4j7bf 2/2 Running 0 46s
hello-00001-deployment-7c4b6cc4df-4j7bf 2/2 Terminating 0 98s
hello-00001-deployment-7c4b6cc4df-4j7bf 1/2 Terminating 0 2m
hello-00001-deployment-7c4b6cc4df-4j7bf 0/2 Terminating 0 2m9s
hello-00001-deployment-7c4b6cc4df-4j7bf 0/2 Terminating 0 2m9s
hello-00001-deployment-7c4b6cc4df-4j7bf 0/2 Terminating 0 2m9s
现在我们已经和 Knative 玩得开心了,可以继续讨论 Kubernetes 上的 FaaS 解决方案。
Kubernetes 函数即服务框架
让我们正视一个关键问题——FaaS。Kubernetes Job 和 CronJob 非常出色,集群自动扩缩容和云提供商管理基础设施也很棒。Knative 通过其零扩缩和流量路由功能非常酷。但是,实际的 FaaS 呢?别担心,Kubernetes 这里有许多选择——甚至可能有太多选择。Kubernetes 有许多 FaaS 框架:
-
Fission
-
Kubeless
-
OpenFaaS
-
OpenWhisk
-
Riff(基于 Knative 构建)
-
Nuclio
-
BlueNimble
-
Fn
-
Rainbond
这些框架中,有些获得了大量关注,而有些则没有。我在上一版书中讨论的两个最显著的框架,Kubeless 和 Riff,已经被归档(Riff 自称已完成)。
我们将研究一些仍然活跃的更流行的选项,特别是我们将重点关注 OpenFaaS 和 Fission。
OpenFaaS
OpenFaaS (www.openfaas.com) 是最成熟、最受欢迎且最活跃的 FaaS 项目之一。它于 2016 年创建,写作时在 GitHub 上已有超过 30,000 个 stars。OpenFaaS 拥有社区版以及授权的 Pro 和 Enterprise 版本。许多生产功能(如高级自动扩缩容和零扩缩容)在社区版中不可用。OpenFaaS 还包含两个附加组件——Prometheus(用于指标)和 NATS(异步队列)。让我们看看 OpenFaaS 如何在 Kubernetes 上提供 FaaS 解决方案。
交付流水线
OpenFaaS 提供了一个完整的生态系统和交付机制,来打包和运行你的函数在 Kubernetes 上。它也可以在虚拟机上使用 fasstd 运行,但这本书是关于 Kubernetes 的。典型的工作流如下所示:

图 12.10:典型的 OpenFaaS 工作流
faas-cli 允许你构建、推送并部署你的函数作为 Docker/OCI 镜像。当你构建函数时,可以使用各种模板,也可以添加你自己的模板。这些步骤可以集成到任何 CI/CD 流水线中。
OpenFaaS 特性
OpenFaaS 通过网关暴露其功能。你可以通过 REST API、CLI 或基于 Web 的 UI 与网关进行交互。网关暴露了不同的端点。
OpenFaaS 的主要特性包括:
-
函数管理
-
函数调用和触发器
-
自动扩缩容
-
指标
-
基于 Web 的 UI
函数管理
你通过创建或构建镜像、推送镜像并部署它们来管理函数。faas-cli 可以帮助你完成这些任务。我们将在本章后面看到一个示例。
函数调用和触发器
OpenFaaS 函数可以作为 HTTP 端点被调用,或者通过各种触发器进行调用,如 NATS 事件、其他事件系统,甚至直接通过 CLI 调用。
指标
OpenFaaS 暴露了一个 prometheus/metrics 端点,可用于抓取指标。某些指标仅在 Pro 版本中可用。完整的指标列表请参见:docs.openfaas.com/architecture/metrics/。
自动扩缩容
OpenFaaS 的一个显著特点是,它根据各种指标进行扩缩容(包括 Pro 版本中的零扩缩容)。它不使用 Kubernetes 水平 Pod 自动扩缩容器 (HPA),而是支持多种扩缩容模式,如 rps、容量和 CPU(与 Kubernetes HPA 相同)。你可以通过像 Keda (keda.sh) 这样的项目实现类似的功能,但那样你得自己构建,而 OpenFaaS 已经为你提供了现成的功能。
基于 Web 的 UI
OpenFaaS 提供一个简单的基于 Web 的 UI,位于 API 网关的 /ui 端点。
下面是其架构图:

图 12.11:OpenFaaS 基于 Web 的 UI
OpenFaaS 架构
OpenFaaS 具有多个组件,这些组件相互作用,以可扩展和 Kubernetes 原生的方式提供所有功能。
主要组件包括:
-
OpenFaaS API 网关
-
FaaS 提供商
-
Prometheus 和告警管理器
-
OpenFaaS 操作员
-
API 网关
你的函数作为 CRD 存储。OpenFaaS 操作员会监视这些函数。以下图示说明了各个组件及其关系。

图 12.12:OpenFaaS 架构
让我们玩一下 OpenFaaS,了解从用户角度如何操作。
带着 OpenFaaS 去体验
让我们安装 OpenFaaS 和 fass-cli CLI。我们将使用推荐的 arkade 包管理器(github.com/alexellis/arkade),这是 OpenFaaS 创始人开发的。所以,我们先安装 arkade。Arkade 可以安装 Kubernetes 应用程序和各种命令行工具。
在 Mac 上,你可以使用 homebrew:
$ brew install arkade
在 Windows 上,你需要安装 Git Bash(git-scm.com/downloads),然后在 Git Bash 提示符下:
$ curl -sLS https://get.arkade.dev | sh
让我们验证 arkade 是否可用:
$ ark
_ _
__ _ _ __| | ____ _ __| | ___
/ _` | '__| |/ / _` |/ _` |/ _ \
| (_| | | | < (_| | (_| | __/
\__,_|_| |_|\_\__,_|\__,_|\___|
Open Source Marketplace For Developer Tools
Usage:
arkade [flags]
arkade [command]
Available Commands:
chart Chart utilities
completion Output shell completion for the given shell (bash or zsh)
get The get command downloads a tool
help Help about any command
info Find info about a Kubernetes app
install Install Kubernetes apps from helm charts or YAML files
system System apps
uninstall Uninstall apps installed with arkade
update Print update instructions
version Print the version
Flags:
-h, --help help for arkade
Use "arkade [command] --help" for more information about a command.
如果你不想使用 arkade,还有其他选项可以安装 OpenFaaS。参见 docs.openfaas.com/deployment/kubernetes/。
接下来,让我们在 Kubernetes 集群上安装 OpenFaaS:
$ ark install openfaas
Using Kubeconfig: /Users/gigi.sayfan/.kube/config
Client: arm64, Darwin
2022/10/01 11:29:14 User dir established as: /Users/gigi.sayfan/.arkade/
Downloading: https://get.helm.sh/helm-v3.9.3-darwin-amd64.tar.gz
/var/folders/qv/7l781jhs6j19gw3b89f4fcz40000gq/T/helm-v3.9.3-darwin-amd64.tar.gz written.
2022/10/01 11:29:17 Extracted: /var/folders/qv/7l781jhs6j19gw3b89f4fcz40000gq/T/helm
2022/10/01 11:29:17 Copying /var/folders/qv/7l781jhs6j19gw3b89f4fcz40000gq/T/helm to /Users/gigi.sayfan/.arkade/bin/helm
Downloaded to: /Users/gigi.sayfan/.arkade/bin/helm helm
"openfaas" has been added to your repositories
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "openfaas" chart repository
Update Complete. Happy Helming!
VALUES values-arm64.yaml
Command: /Users/gigi.sayfan/.arkade/bin/helm [upgrade --install openfaas openfaas/openfaas --namespace openfaas --values /var/folders/qv/7l781jhs6j19gw3b89f4fcz40000gq/T/charts/openfaas/values-arm64.yaml --set gateway.directFunctions=false --set openfaasImagePullPolicy=IfNotPresent --set gateway.replicas=1 --set queueWorker.replicas=1 --set dashboard.publicURL=http://127.0.0.1:8080 --set queueWorker.maxInflight=1 --set autoscaler.enabled=false --set basic_auth=true --set faasnetes.imagePullPolicy=Always --set basicAuthPlugin.replicas=1 --set clusterRole=false --set operator.create=false --set ingressOperator.create=false --set dashboard.enabled=false --set serviceType=NodePort]
Release "openfaas" does not exist. Installing it now.
NAME: openfaas
LAST DEPLOYED: Sat Oct 1 11:29:28 2022
NAMESPACE: openfaas
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
To verify that openfaas has started, run:
kubectl -n openfaas get deployments -l "release=openfaas, app=openfaas"
=======================================================================
= OpenFaaS has been installed. =
=======================================================================
# Get the faascli
kind-openfass | default
curl -SLsf https://cli.openfaas.com | sudo sh
# Forward the gateway to your machine
kubectl rollout status -n openfaas deploy/gateway
kubectl port-forward -n openfaas svc/gateway 8080:8080 &
# If basic auth is enabled, you can now log into your gateway:
PASSWORD=$(kubectl get secret -n openfaas basic-auth -o jsonpath="{.data.basic-auth-password}" | base64 --decode; echo)
echo -n $PASSWORD | faas-cli login --username admin --password-stdin
faas-cli store deploy figlet
faas-cli list
# For Raspberry Pi
faas-cli store list \
--platform armhf
faas-cli store deploy figlet \
--platform armhf
# Find out more at:
# https://github.com/openfaas/faas
 arkade needs your support: https://github.com/sponsors/alexellis
OpenFaaS 创建了两个命名空间:openfaas 用于它自己,openfass-fn 用于你的函数。在 openfass 命名空间中有多个部署:
$ k get deploy -n openfaas
NAME READY UP-TO-DATE AVAILABLE AGE
alertmanager 1/1 1 1 6m2s
basic-auth-plugin 1/1 1 1 6m2s
gateway 1/1 1 1 6m2s
nats 1/1 1 1 6m2s
prometheus 1/1 1 1 6m2s
queue-worker 1/1 1 1 6m2s
openfass-fn 命名空间目前为空。
好的,接下来我们安装 OpenFaaS CLI:
$ brew install faas-cli
==> Downloading https://ghcr.io/v2/homebrew/core/faas-cli/manifests/0.14.8
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/faas-cli/blobs/sha256:cf9460398c45ea401ac688e77a8884cbceaf255064a1d583f8113b6c2bd68450
==> Downloading from https://pkg-containers.githubusercontent.com/ghcr1/blobs/sha256:cf9460398c45ea401ac688e77a8884cbceaf255064a1d583f8113b6c2bd68450?se=2022-10-01T18%3A50%3A00Z&sig=V%
######################################################################## 100.0%
==> Pouring faas-cli--0.14.8.arm64_monterey.bottle.tar.gz
==> Caveats
zsh completions have been installed to:
/opt/homebrew/share/zsh/site-functions
==> Summary
 /opt/homebrew/Cellar/faas-cli/0.14.8: 9 files, 8.4MB
==> Running `brew cleanup faas-cli`...
Disable this behaviour by setting HOMEBREW_NO_INSTALL_CLEANUP.
Hide these hints with HOMEBREW_NO_ENV_HINTS (see `man brew`).
首先,我们需要端口转发网关服务,以便 faas-cli 可以访问我们的集群:
$ kubectl port-forward -n openfaas svc/gateway 8080:8080 &
[3] 76489
$ Forwarding from 127.0.0.1:8080 -> 8080
Forwarding from [::1]:8080 -> 8080
下一步是从名为 basic-auth 的秘密中获取管理员密码,并用它作为管理员用户登录:
$ PASSWORD=$(kubectl get secret -n openfaas basic-auth -o jsonpath="{.data.basic-auth-password}" | base64 --decode; echo)
echo -n $PASSWORD | faas-cli login --username admin --password-stdin
现在,我们已经准备好在集群上部署和运行函数了。让我们看一下商店中可用的函数模板:
$ faas-cli template store list
NAME SOURCE DESCRIPTION
csharp openfaas Classic C# template
dockerfile openfaas Classic Dockerfile template
go openfaas Classic Golang template
java11 openfaas Java 11 template
java11-vert-x openfaas Java 11 Vert.x template
node17 openfaas HTTP-based Node 17 template
node16 openfaas HTTP-based Node 16 template
node14 openfaas HTTP-based Node 14 template
node12 openfaas HTTP-based Node 12 template
node openfaas Classic NodeJS 8 template
php7 openfaas Classic PHP 7 template
php8 openfaas Classic PHP 8 template
python openfaas Classic Python 2.7 template
python3 openfaas Classic Python 3.6 template
python3-dlrs intel Deep Learning Reference Stack v0.4 for ML workloads
ruby openfaas Classic Ruby 2.5 template
ruby-http openfaas Ruby 2.4 HTTP template
python27-flask openfaas Python 2.7 Flask template
python3-flask openfaas Python 3.7 Flask template
python3-flask-debian openfaas Python 3.7 Flask template based on Debian
python3-http openfaas Python 3.7 with Flask and HTTP
python3-http-debian openfaas Python 3.7 with Flask and HTTP based on Debian
golang-http openfaas Golang HTTP template
golang-middleware openfaas Golang Middleware template
python3-debian openfaas Python 3 Debian template
powershell-template openfaas-incubator Powershell Core Ubuntu:16.04 template
powershell-http-template openfaas-incubator Powershell Core HTTP Ubuntu:16.04 template
rust booyaa Rust template
crystal tpei Crystal template
csharp-httprequest distantcam C# HTTP template
csharp-kestrel burtonr C# Kestrel HTTP template
vertx-native pmlopes Eclipse Vert.x native image template
swift affix Swift 4.2 Template
lua53 affix Lua 5.3 Template
vala affix Vala Template
vala-http affix Non-Forking Vala Template
quarkus-native pmlopes Quarkus.io native image template
perl-alpine tmiklas Perl language template based on Alpine image
crystal-http koffeinfrei Crystal HTTP template
rust-http openfaas-incubator Rust HTTP template
bash-streaming openfaas-incubator Bash Streaming template
cobol devries COBOL Template
你知道吗!如果你感兴趣的话,还有一个 COBOL 模板。为了我们的目的,我们将使用 Golang。Golang 有多个模板,我们将使用 golang-http 模板。我们需要第一次拉取这个模板:
$ faas-cli template store pull golang-http
Fetch templates from repository: https://github.com/openfaas/golang-http-template at
2022/10/02 14:48:38 Attempting to expand templates from https://github.com/openfaas/golang-http-template
2022/10/02 14:48:39 Fetched 2 template(s) : [golang-http golang-middleware] from https://github.com/openfaas/golang-http-template
这个模板包含了大量的样板代码,处理了最终生成一个可以在 Kubernetes 上运行的容器所需的所有流程。
$ ls -la template/golang-http
total 64
drwxr-xr-x 11 gigi.sayfan staff 352 Oct 2 14:48 .
drwxr-xr-x 4 gigi.sayfan staff 128 Oct 2 14:52 ..
-rw-r--r-- 1 gigi.sayfan staff 52 Oct 2 14:48 .dockerignore
-rw-r--r-- 1 gigi.sayfan staff 9 Oct 2 14:48 .gitignore
-rw-r--r-- 1 gigi.sayfan staff 1738 Oct 2 14:48 Dockerfile
drwxr-xr-x 4 gigi.sayfan staff 128 Oct 2 14:48 function
-rw-r--r-- 1 gigi.sayfan staff 110 Oct 2 14:48 go.mod
-rw-r--r-- 1 gigi.sayfan staff 257 Oct 2 14:48 go.sum
-rw-r--r-- 1 gigi.sayfan staff 32 Oct 2 14:48 go.work
-rw-r--r-- 1 gigi.sayfan staff 3017 Oct 2 14:48 main.go
-rw-r--r-- 1 gigi.sayfan staff 465 Oct 2 14:48 template.yml
让我们创建我们的函数:
$ faas-cli new --prefix docker.io/g1g1 --lang golang-http openfaas-go
Folder: openfaas-go created.
kind-openfaas | openfaas
___ _____ ____
/ _ \ _ __ ___ _ __ | ___|_ _ __ _/ ___|
| | | | '_ \ / _ \ '_ \| |_ / _` |/ _` \___ \
| |_| | |_) | __/ | | | _| (_| | (_| |___) |
\___/| .__/ \___|_| |_|_| \__,_|\__,_|____/
|_|
Function created in folder: openfaas-go
Stack file written: openfaas-go.yml
Notes:
You have created a new function which uses Go 1.18 and Alpine
Linux as its base image.
To disable the go module, for private vendor code, please use
"--build-arg GO111MODULE=off" with faas-cli build or configure this
via your stack.yml file.
See more: https://docs.openfaas.com/cli/templates/
For the template's repo and more examples:
https://github.com/openfaas/golang-http-template
这个命令生成了 3 个文件:
-
openfaas-go.yml -
openfaas-go/go.mod -
openfaas-go/handler.go
让我们检查这些文件。
openfaas-go.yml 是我们的函数清单:
$ cat openfaas-go.yml
version: 1.0
provider:
name: openfaas
gateway: http://127.0.0.1:8080
functions:
openfaas-go:
lang: golang-http
handler: ./openfaas-go
image: docker.io/g1g1/openfaas-go:latest
注意,镜像有我的 Docker 注册表用户账户的前缀,以防我想推送镜像。一个清单文件中可以定义多个函数。
go.mod 文件非常基础:
$ cat openfaas-go/go.mod
module handler/function
go 1.18
handler.go 文件是我们编写代码的地方:
$ cat openfaas-go/handler.go
package function
import (
"fmt"
"net/http"
handler "github.com/openfaas/templates-sdk/go-http"
)
// Handle a function invocation
func Handle(req handler.Request) (handler.Response, error) {
var err error
message := fmt.Sprintf("Body: %s", string(req.Body))
return handler.Response{
Body: []byte(message),
StatusCode: http.StatusOK,
}, err
}
默认实现类似一个 HTTP 回显,响应只是返回请求的主体。
让我们开始构建。默认输出非常冗长,显示了大量来自 Docker 的输出,所以我将使用 --quiet 标志:
$ faas-cli build -f openfaas-go.yml --quiet
[0] > Building openfaas-go.
Clearing temporary build folder: ./build/openfaas-go/
Preparing: ./openfaas-go/ build/openfaas-go/function
Building: docker.io/g1g1/openfaas-go:latest with golang-http template. Please wait..
Image: docker.io/g1g1/openfaas-go:latest built.
[0] < Building openfaas-go done in 0.93s.
[0] Worker done.
Total build time: 0.93s
结果是一个 Docker 镜像:
$ docker images | grep openfaas
g1g1/openfaas-go latest 215e95884a9b 3 minutes ago 18.3MB
如果你有账户,我们可以将这个镜像推送到 Docker 注册表(或其他注册表):
$ faas-cli push -f openfaas-go.yml
[0] > Pushing openfaas-go [docker.io/g1g1/openfaas-go:latest].
The push refers to repository [docker.io/g1g1/openfaas-go]
668bbc37657f: Pushed
185851557ef2: Pushed
1d14a6a345f2: Pushed
5f70bf18a086: Pushed
ecf2d64591ca: Pushed
f6b0a98cfe18: Pushed
5d3e392a13a0: Mounted from library/golang
latest: digest: sha256:cb2b3051e2cac7c10ce78a844e331a5c55e9a2296c5c3ba9e0e8ee0523ceba84 size: 1780
[0] < Pushing openfaas-go [docker.io/g1g1/openfaas-go:latest] done.
Docker 镜像现在已在 Docker Hub 上可用。

图 12.13:Docker 镜像可在 Docker Hub 上获取
最后一步是将镜像部署到集群中:
$ faas-cli deploy -f openfaas-go.yml
Deploying: openfaas-go.
Handling connection for 8080
Deployed. 202 Accepted.
URL: http://127.0.0.1:8080/function/openfaas-go
让我们使用不同的请求体多次调用我们的函数,以验证响应是否准确:
$ http POST http://127.0.0.1:8080/function/openfaas-go body='yeah, it works!' -b
Handling connection for 8080
Body: {
"body": "yeah, it works!"
}
$ http POST http://127.0.0.1:8080/function/openfaas-go body='awesome!' -b
Handling connection for 8080
Body: {
"body": "awesome!"
}
太棒了,真的有效!
我们可以通过list命令查看我们的函数和一些统计信息,例如调用次数和副本数量:
$ faas-cli list
Handling connection for 8080
Function Invocations Replicas
openfaas-go 6 1
总结来说,OpenFaaS 为 Kubernetes 上的函数即服务提供了一个成熟且全面的解决方案。它仍然要求你构建 Docker 镜像、推送并部署到集群中,这些步骤需要使用其 CLI 单独执行。将这些步骤集成到 CI/CD 流水线或简单的脚本中相对简单。
Fission
Fission (fission.io) 是一个成熟且文档齐全的框架。它将 FaaS 世界建模为环境、函数和触发器。环境用于构建和运行特定语言的函数代码。每个语言环境包含一个 HTTP 服务器,并且通常具有一个动态加载器(适用于动态语言)。函数是表示无服务器函数的对象,触发器则是函数在集群中部署后如何被调用的方式。触发器有四种类型:
-
HTTP 触发器:通过 HTTP 端点调用函数。
-
定时器触发器:在特定时间调用函数。
-
消息队列触发器:当从消息队列中提取事件时调用函数(支持 Kafka、NATS 和 Azure 队列)。
-
Kubernetes watch 触发器:响应集群中 Kubernetes 事件来调用函数。
很有趣的是,消息队列触发器不仅仅是“触发后忘记”。它们支持可选的响应和错误队列。以下是展示流程的图示:

图 12.14:Fission mq 触发器
Fission 为其 100 毫秒的冷启动速度感到自豪。它通过保持一个“温热”的容器池并配有一个小型动态加载器来实现这一点。当第一次调用函数时,已经有一个正在运行的容器准备就绪,代码会发送到该容器中执行。从某种意义上说,Fission 通过从未冷启动来“作弊”。关键点是,Fission 不会扩展到零,但对于首次调用来说非常快速。
Fission 执行器
Fission 支持两种执行器类型——NewDeploy 和 PoolManager。NewDeploy 执行器与 OpenFaaS 非常相似,为每个函数创建一个 Deployment、Service 和 HPA。以下是使用 NewDeploy 执行器时的函数调用示例:

图 12.15:Fission 函数调用
PoolManager 执行器管理每个环境的通用 pod 池。当调用特定环境的函数时,PoolManager 执行器会在可用的通用池中运行该函数。
NewDeploy 执行器允许对运行特定函数所需的资源进行精细控制,并且它还可以扩展到零。其代价是每个函数需要更高的冷启动时间来为每个函数创建新 Pod。需要注意的是,Pod 会保留,因此,如果同一个函数在上次调用后不久再次被调用,就不必再次支付冷启动费用。
PoolManager 执行器保持常驻通用 Pod,因此调用函数的速度较快,但当没有新函数需要调用时,池中的 Pod 会闲置不动。此外,函数可以控制它们可用的资源。
根据功能的使用模式,你可以为不同的功能使用不同的执行器。

图 12.16:Fission 执行器
Fission 工作流
Fission 还有另一个特色——Fission 工作流。这是一个基于 Fission 的独立项目,允许你构建由多个 Fission 函数组成的复杂工作流。目前,由于 Fission 核心团队的时间限制,该项目处于维护模式。
请查看项目页面以获取更多详情:github.com/fission/fission-workflows。
这是一个描述 Fission 工作流架构的示意图:

图 12.17:Fission 工作流
你可以在 YAML 文件中定义工作流,指定任务(通常是 Fission 函数)、输入、输出、条件和延迟。例如:
apiVersion: 1
description: Send a message to a slack channel when the temperature exceeds a certain threshold
output: CreateResult
# Input: 'San Fransisco, CA'
tasks:
# Fetch weather for input
FetchWeather:
run: wunderground-conditions
inputs:
default:
apiKey: <API_KEY>
state: "{$.Invocation.Inputs.default.substring($.Invocation.Inputs.default.indexOf(',') + 1).trim()}"
city: "{$.Invocation.Inputs.default.substring(0, $.Invocation.Inputs.default.indexOf(',')).trim()}"
ToCelsius:
run: tempconv
inputs:
default:
temperature: "{$.Tasks.FetchWeather.Output.current_observation.temp_f}"
format: F
target: C
requires:
- FetchWeather
# Send a slack message if the temperature threshold has been exceeded
CheckTemperatureThreshold:
run: if
inputs:
if: "{$.Tasks.ToCelsius.Output.temperature > 25}"
then:
run: slack-post-message
inputs:
default:
message: "{'It is ' + $.Tasks.ToCelsius.Output.temperature + 'C in ' + $.Invocation.Inputs.default + ' :fire:'}"
path: <HOOK_URL>
requires:
- ToCelsius
# Besides the potential Slack message, compose the response of this workflow {location, celsius, fahrenheit}
CreateResult:
run: compose
inputs:
celsius: "{$.Tasks.ToCelsius.Output.temperature}"
fahrenheit: "{$.Tasks.FetchWeather.Output.current_observation.temp_f}"
location: "{$.Invocation.Inputs.default}"
sentSlackMsg: "{$.Tasks.CheckTemperatureThreshold.Output}"
requires:
- ToCelsius
- CheckTemperatureThreshold
让我们尝试一下 Fission:
使用 Fission 进行实验
首先,让我们通过 Helm 安装它:
$ k create ns fission
$ k create -k "github.com/fission/fission/crds/v1?ref=v1.17.0"
$ helm repo add fission-charts https://fission.github.io/fission-charts/
$ helm repo update
$ helm install --version v1.17.0 --namespace fission fission \
--set serviceType=NodePort,routerServiceType=NodePort \
fission-charts/fission-all
这里是它创建的所有 CRD:
$ k get crd -o name | grep fission
customresourcedefinition.apiextensions.k8s.io/canaryconfigs.fission.io
customresourcedefinition.apiextensions.k8s.io/environments.fission.io
customresourcedefinition.apiextensions.k8s.io/functions.fission.io
customresourcedefinition.apiextensions.k8s.io/httptriggers.fission.io
customresourcedefinition.apiextensions.k8s.io/kuberneteswatchtriggers.fission.io
customresourcedefinition.apiextensions.k8s.io/messagequeuetriggers.fission.io
customresourcedefinition.apiextensions.k8s.io/packages.fission.io
customresourcedefinition.apiextensions.k8s.io/timetriggers.fission.io
Fission CLI 也会派上用场:
对于 Mac:
$ curl -Lo fission https://github.com/fission/fission/releases/download/v1.17.0/fission-v1.17.0-darwin-amd64 && chmod +x fission && sudo mv fission /usr/local/bin/
对于 Linux 或 Windows 上的 WSL:
$ curl -Lo fission https://github.com/fission/fission/releases/download/v1.17.0/fission-v1.17.0-linux-amd64 && chmod +x fission && sudo mv fission /usr/local/bin/
我们需要创建一个环境,才能构建我们的函数。我们选择 Python 环境:
$ fission environment create --name python --image fission/python-env
poolsize setting default to 3
environment 'python' created
在 Python 环境准备好后,我们可以创建一个无服务器函数。首先,将此代码保存为 yeah.py:
def main():
return 'Yeah, it works!!!'
然后,我们创建名为“yeah”的 Fission 函数:
$ fission function create --name yeah --env python --code yeah.py
Package 'yeah-b9d5d944-9c6e-4e67-81fb-96e047625b74' created
function 'yeah' created
我们可以通过 Fission CLI 测试该函数:
$ fission function test --name yeah
Yeah, it works!!!
真正的重点是通过 HTTP 端点调用它。我们需要为此创建一个路由:
$ fission route create --method GET --url /yeah --function yeah --name yeah
trigger 'yeah' created
在设置路由后,我们仍然需要通过端口转发将服务 Pod 暴露到本地环境:
$ k -n fission port-forward $(k -n fission get pod -l svc=router -o name) 8888:8888 &
$ export FISSION_ROUTER=127.0.0.1:8888
在完成所有前置步骤后,让我们通过 httpie 测试我们的函数:
$ http http://${FISSION_ROUTER}/yeah -b
Handling connection for 8888
Yeah, it works!!!
你可以跳过端口转发,直接使用 Fission CLI 进行测试:
$ fission function test yeah --name yeah
Yeah, it works!!!
Fission 在功能上类似于 OpenFaaS,但它感觉更加简洁,使用起来更容易。两者都是不错的解决方案,选择哪个取决于你的个人偏好。
总结
在本章中,我们讨论了无服务器计算这一热门话题。我们解释了无服务器的两层含义——既是消除了管理服务器的需求,也包括将功能作为服务进行部署和运行。我们深入探讨了云中的无服务器基础设施,特别是在 Kubernetes 环境中的应用。我们将 Kubernetes 原生的集群自动扩展器与其他云服务提供商的解决方案进行了对比,比如 AWS EKS+Fargate、Azure AKS+ACI 和 Google Cloud Run。接着,我们转向了令人兴奋且充满前景的 Knative 项目,重点介绍了其零扩展能力和高级部署选项。然后,我们进入了 Kubernetes 上 FaaS 的精彩世界。
我们讨论了各种可用的解决方案,并对其进行了详细的分析,包括对两种最具代表性且经过实践检验的解决方案的实际操作实验:OpenFaaS 和 Fission。结论是,这两种无服务器计算方式在操作和成本管理方面都带来了真正的好处。未来,观察这些技术在云计算和 Kubernetes 环境中的发展与整合将非常令人兴奋。
在下一章,我们的重点将放在监控与可观察性上。像大型 Kubernetes 集群这样的复杂系统,涵盖了各种不同的工作负载、持续交付管道以及配置变更,必须具备出色的监控系统,以便保持系统的正常运转。Kubernetes 提供了一些非常棒的选项,我们应该加以利用。
第十三章:监控 Kubernetes 集群
在上一章中,我们探讨了无服务器计算及其在 Kubernetes 上的表现。这个领域充满了创新,跟踪其发展既非常有用,又令人着迷。
在本章中,我们将讨论如何确保你的系统正常运行并且表现良好,以及当它们出现问题时该如何响应。在第三章,高可用性和可靠性中,我们讨论了相关话题。这里的重点是了解你的系统发生了什么,并且可以使用哪些实践和工具。
监控有很多方面,比如日志记录、度量、分布式追踪、错误报告和警报。像自动扩展和自我修复等实践依赖于监控来检测是否需要扩展或修复。
本章我们将讨论的内容包括:
-
理解可观察性
-
使用 Kubernetes 记录日志
-
使用 Kubernetes 记录度量
-
使用 Jaeger 进行分布式追踪
-
排查问题
Kubernetes 社区认识到监控的重要性,并投入了大量精力确保 Kubernetes 拥有一个完善的监控方案。云原生计算基金会(CNCF)是云原生基础设施项目的事实上的管理机构。到目前为止,它已经毕业了二十个项目。Kubernetes 是第一个毕业的项目,在早期毕业的项目中,另外三个超过两年毕业的项目专注于监控:Prometheus、Fluentd 和 Jaeger。这意味着监控和可观察性是大规模基于 Kubernetes 的系统的基础。在深入探讨 Kubernetes 监控以及具体项目和工具之前,我们应该更好地理解监控的含义。一个好的思考监控的框架是:你的系统有多可观察。
理解可观察性
可观察性是一个大词。它在实践中意味着什么?有不同的定义,并且关于监控和可观察性之间的相似性与差异存在广泛的争论。我认为可观察性是系统的一个属性,它定义了我们现在以及历史上能知道系统的状态和行为。特别是,我们关注的是系统及其组件的健康状况。监控是我们用来提高系统可观察性的工具、过程和技术的集合。
我们需要收集、记录和聚合不同方面的信息,以便更好地了解系统的运行情况。这些方面包括日志、度量、分布式追踪和错误。监控或可观察性数据是多维的,跨越多个层次。仅仅收集数据并不能带来太大帮助。我们需要能够查询、可视化这些数据,并在出现问题时向其他系统发出警报。让我们回顾一下可观察性的各个组件。
日志记录
日志是一个关键的监控工具。每个自尊的长期运行的软件都必须有日志。日志捕捉带时间戳的事件。它们对许多应用程序至关重要,如商业智能、安全性、合规性、审计、调试和故障排除。重要的是要理解,复杂的分布式系统会为不同组件生成不同的日志,从日志中提取洞察不是一件简单的事。
日志有几个关键属性:格式、存储和聚合。
日志格式
日志可能有多种格式。纯文本格式非常常见且人类可读,但需要大量工作来解析和与其他日志合并。结构化日志更适合大规模系统,因为它们可以进行大规模处理。二进制日志适用于生成大量日志的系统,因为它们更节省空间,但需要自定义工具和处理才能提取其信息。
日志存储
日志可以存储在内存中、文件系统上、数据库中、云存储中、发送到远程日志服务,或这些方式的任何组合。在云原生环境中,软件运行在容器中,因此需要特别关注日志存储位置以及在必要时如何提取它们。
当容器可能随时消失时,持久性等问题就会浮现。在 Kubernetes 中,容器的标准输出和标准错误流会自动记录并保持可用,即使 pod 终止。但是,像日志空间不足和日志轮换等问题总是值得关注的。
日志聚合
最佳实践是将本地日志发送到一个集中式日志服务,这个服务设计用来处理各种日志格式,根据需要持久化它们,并以可查询和可推理的方式聚合多种日志。
指标
指标衡量系统随时间变化的某些方面。指标是数值值的时间序列(通常是浮动点数)。每个指标都有一个名称,通常还会有一组标签,方便后续的切片和分解。例如,节点的 CPU 利用率或服务的错误率就是指标。
指标比日志更经济。它们每个时间段需要固定的存储空间,不会像日志那样随着流量的变化而波动。
此外,由于指标本质上是数值型的,因此无需解析或转换。指标可以通过统计方法轻松组合和分析,并作为事件和警报的触发器。
不同层次的许多指标(如节点、容器、进程、网络和磁盘)通常会由操作系统、云服务提供商或 Kubernetes 自动为你收集。
但你也可以创建自定义指标,映射到系统的高层次关注点,并可以与应用程序级别的策略一起配置。
分布式追踪
现代分布式系统通常采用基于微服务的架构,其中传入的请求在多个微服务之间传递,等待队列,并触发无服务器函数。当你尝试分析错误、故障、数据完整性问题或性能问题时,能够追踪请求的路径是至关重要的。这就是分布式跟踪的作用。
分布式跟踪是由多个 span 和引用组成的集合。你可以将跟踪看作一个有向无环图(DAG),它表示一个请求在分布式系统各个组件中的传递过程。每个 span 记录请求在特定组件中花费的时间,而引用则是连接一个 span 与下一个 span 的图边。
这里有一个例子:

图 13.1:一个样本分布式跟踪的路径
分布式跟踪在理解复杂分布式系统中是不可或缺的。
应用程序错误报告
错误和异常报告有时是作为日志的一部分进行的。你肯定需要记录错误,查看日志在出现问题时是一项历史悠久的传统。然而,捕获错误信息的层级超出了日志记录。当应用程序发生错误时,捕获错误信息、错误在代码中的位置以及堆栈跟踪非常有用。这是非常标准的,大多数编程语言都可以提供这些信息,尽管堆栈跟踪通常是多行的,不太适合基于行的日志。一个有用的额外信息是捕获堆栈跟踪每一层的本地状态。当问题发生在一个核心位置时,本地状态(比如某些列表中的条目数量和大小)可以帮助识别根本原因。
像 Sentry 或 Rollbar 这样的中央错误报告服务提供了超出日志记录的错误特定价值,比如丰富的错误信息、上下文和用户信息。
仪表盘和可视化
好的,你已经成功地收集了日志,定义了指标,跟踪了请求,并报告了丰富的错误信息。现在,你想弄清楚你的系统或其部分正在做什么。基准是什么?流量在一天、一周以及节假日之间是如何波动的?当系统承受压力时,哪些部分最脆弱?
在一个涉及数百个服务和数据存储并与外部系统集成的复杂系统中,你不能仅仅依赖原始日志文件、指标和跟踪。
你需要能够整合大量信息,构建系统健康仪表盘,可视化基础设施,并创建业务级别的报告和图表。
如果你使用的是云平台,你可能会自动获得其中的一部分(尤其是基础设施方面的信息)。但你应该预期需要在可视化和仪表盘方面做一些深入的工作。
警报
仪表盘非常适合那些想要从全局角度了解系统并能够深入分析其行为的人。告警则是检测异常情况并触发某些操作。理想情况下,你的系统应该是自愈的,能够从大多数情况中自动恢复。但至少,你应该报告异常,以便人类可以随时查看发生了什么,并决定是否需要进一步的操作。
告警可以与电子邮件、聊天室和值班系统集成。它通常与指标关联,当特定条件满足时,告警就会触发。
现在,我们已经概述了监控复杂系统中涉及的不同元素,让我们看看如何在 Kubernetes 中实现这些操作。
使用 Kubernetes 进行日志记录
我们需要仔细考虑在 Kubernetes 中的日志策略。有几种类型的日志与监控相关。我们的工作负载当然运行在容器中,我们关心这些日志,但我们也关心 Kubernetes 组件的日志,如 API 服务器、kubelet 和容器运行时的日志。
此外,跨多个节点和容器追踪日志是不可行的。最佳实践是使用中央日志记录(也称为日志聚合)。这里有几个选项,我们很快会探讨。
容器日志
Kubernetes 会存储每个容器的标准输出和标准错误。通过 kubectl logs 命令可以访问这些日志。
这是一个打印当前日期和时间的 Pod 清单,每 10 秒钟打印一次:
apiVersion: v1
kind: Pod
metadata:
name: now
spec:
containers:
- name: now
image: g1g1/py-kube:0.3
command: ["/bin/bash", "-c", "while true; do sleep 10; date; done"]
我们可以将其保存到一个名为 now-pod.yaml 的文件中并创建它:
$ k apply -f now-pod.yaml
pod/now created
要查看日志,我们使用 kubectl logs 命令:
$ kubectl logs now
Sat Jan 4 00:32:38 UTC 2020
Sat Jan 4 00:32:48 UTC 2020
Sat Jan 4 00:32:58 UTC 2020
Sat Jan 4 00:33:08 UTC 2020
Sat Jan 4 00:33:18 UTC 2020
关于容器日志的几点说明。kubectl logs 命令需要指定 Pod 名称。如果该 Pod 有多个容器,你还需要指定容器名称:
$ k logs <pod name> -c <container name>
如果一个部署或副本集创建了多个相同 Pod 的副本,你可以通过使用共享标签,单次查询所有 Pod 的日志:
k logs -l <label>
如果某个容器因某种原因崩溃了,你可以使用 kubectl logs -p 命令查看崩溃容器的日志。
Kubernetes 组件日志
如果你在像 GKE、EKS 或 AKS 这样的托管环境中运行 Kubernetes,你将无法直接访问 Kubernetes 组件日志,但这是预期的。你不需要负责 Kubernetes 控制平面。然而,控制平面组件(如 API 服务器和集群自动扩展器)以及节点组件(如 kubelet 和容器运行时)的日志,对于故障排除可能非常重要。云服务提供商通常提供专有的方法来访问这些日志。
如果你自己运行 Kubernetes 控制平面,以下是标准控制平面组件及其日志位置:
-
API 服务器:
/var/log/kube-apiserver.log -
调度器:
/var/log/kube-scheduler.log -
控制器管理器:
/var/log/kube-controller-manager.log
工作节点组件及其日志位置如下:
-
Kubelet:
/var/log/kubelet.log -
Kube proxy:
/var/log/kube-proxy.log
请注意,在基于 systemd 的系统中,你需要使用 journalctl 来查看工作节点的日志。
集中式日志
阅读容器日志对于在单个 Pod 中进行快速且简单的故障排除是可以的。但要诊断和调试系统级别的问题,我们需要集中式日志(即日志聚合)。所有来自我们容器的日志都应该发送到一个中央存储库,并通过过滤器和查询进行切片和切割。
在决定你的中央日志方法时,有几个重要的决策:
-
如何收集日志
-
日志存储的位置
-
如何处理敏感日志信息
我们将在接下来的章节中回答这些问题。
选择日志收集策略
日志通常是通过一个运行在接近生成日志的进程旁边的代理收集的,并确保将其发送到中央日志服务。
让我们来看一下常见的方法。
直接将日志记录到远程日志服务
在这种方法中,没有日志代理。每个应用程序容器的责任是将日志发送到远程日志服务。这通常通过客户端库完成。这是一种高接触式方法,应用程序需要知道日志目标,并配置适当的凭证。

图 13.2:直接日志记录
如果你想要更改日志收集策略,将需要对每个应用程序进行修改(至少升级到新版本的库)。
节点代理
节点代理方法在你控制工作节点时最为合适,并且你希望将日志聚合的任务从应用程序中抽象出来。每个应用程序容器可以简单地写入标准输出和标准错误,运行在每个节点上的代理将拦截日志并将其发送到远程日志服务。
通常,你会将节点代理作为 DaemonSet 部署,因此,随着节点的添加或移除,日志代理将始终存在,无需额外操作。

图 13.3:使用节点代理进行日志记录
Sidecar 容器
当你无法控制集群节点,或者使用某些无服务器计算基础设施来部署容器但又不想使用直接日志记录方法时,sidecar 容器是最好的选择。如果你无法控制节点且无法安装代理,那么节点代理方法就不适用了,但你可以附加一个 sidecar 容器,它将收集日志并将其发送到中央日志服务。它的效率不如节点代理方法,因为每个容器都需要有自己的日志 sidecar 容器,但可以在部署阶段完成,而无需修改代码和应用程序知识。

图 13.4:使用 sidecar 容器进行日志记录
现在我们已经讨论了日志收集的主题,让我们考虑一下如何集中存储和管理这些日志。
集群级别的集中日志
如果您的整个系统运行在一个 Kubernetes 集群中,那么集群级别的日志记录可能是一个很好的选择。您可以在集群中安装一个中央日志服务,如 Grafana Loki、ElasticSearch 或 Graylog,享受一个统一的日志聚合体验,而无需将日志数据发送到其他地方。
远程中央日志记录
有些情况下,由于各种原因,集群内的中央日志记录无法满足需求:
-
日志用于审计目的;可能需要将日志记录到一个单独且受控的位置(例如,在 AWS 上,通常将日志记录到一个单独的账户)。
-
您的系统运行在多个集群上,每个集群的日志记录并不真正是中央化的。
-
您运行在云服务提供商上,并且更喜欢将日志记录到云平台的日志服务(例如,GCP 上的 StackDriver 或 AWS 上的 CloudWatch)。
-
您已经在使用像 SumoLogic 或 Splunk 这样的远程中央日志服务,并且希望继续使用它们。
-
您只是希望避免收集和存储日志数据的麻烦。
-
集群范围的问题可能会影响您的日志收集、存储或访问,并妨碍您排查故障。
将日志记录到远程中央位置可以通过所有方法完成:直接日志记录、节点代理日志记录或 Sidecar 日志记录。在所有情况下,都必须提供远程日志服务的端点和凭证,日志记录是针对该端点进行的。在大多数情况下,这将通过客户端库完成,应用程序无需了解其细节。至于系统级日志记录,常见的方法是通过专用的日志代理收集所有必要的日志,并将它们转发到远程日志服务。
处理敏感日志信息
好的,我们可以收集日志并将它们发送到中央日志服务。如果中央日志服务是远程的,您可能需要选择性地记录某些信息。
例如,个人身份信息(PII)和受保护的健康信息(PHI)是两类您可能不应记录的信息,除非确保访问日志的权限得到妥善控制。
通常会删除或屏蔽如用户名和电子邮件等个人身份信息(PII)日志记录。
使用 Fluentd 进行日志收集
Fluentd (www.fluentd.org)是一个开源的 CNCF 毕业项目。它被认为是 Kubernetes 中最好的选择,并且几乎可以与任何您想要的日志后端集成。如果您自己搭建集中式日志解决方案,我推荐使用 Fluentd。Fluentd 作为节点代理运行。以下图示展示了 Fluentd 如何作为 DaemonSet 在 Kubernetes 集群中部署:

图 13.5:在 Kubernetes 集群中作为 DaemonSet 部署 Fluentd
最流行的 DIY 集中式日志解决方案之一是 ELK,其中 E 代表 ElasticSearch,L 代表 LogStash,K 代表 Kibana。在 Kubernetes 上,EFK(Fluentd 替代 LogStash)非常常见。
Fluentd 具有基于插件的架构,因此不要觉得只限于 EFK。Fluentd 不需要很多资源,但如果你真的需要一个高性能的解决方案,Fluentbit(fluentbit.io/)是一个纯粹的转发器,只需要不到 450 KB 的内存。
我们已经覆盖了很多关于日志的内容。接下来,我们来看看可观测性故事的下一个部分,即指标。
使用 Kubernetes 收集指标
Kubernetes 有一个指标 API,它支持开箱即用的节点和 Pod 指标。你还可以定义自定义指标。
一个指标包含一个时间戳、一个使用字段和收集该指标的时间范围(许多指标是按时间段积累的)。以下是节点指标的 API 定义:
type NodeMetrics struct {
metav1.TypeMeta
metav1.ObjectMeta
Timestamp metav1.Time
Window metav1.Duration
Usage corev1.ResourceList
}
// NodeMetricsList is a list of NodeMetrics.
type NodeMetricsList struct {
metav1.TypeMeta
// Standard list metadata.
// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
metav1.ListMeta
// List of node metrics.
Items []NodeMetrics
}
使用字段类型是 ResourceList,但它实际上是一个资源名称到数量的映射:
// ResourceList is a set of (resource name, quantity) pairs.
type ResourceList map[ResourceName]resource.Quantity
Quantity 表示一个定点数。它支持 JSON 和 YAML 中的轻松序列化/反序列化,并提供如 String() 和 Int64() 等访问方法:
type Quantity struct {
// i is the quantity in int64 scaled form, if d.Dec == nil
i int64Amount
// d is the quantity in inf.Dec form if d.Dec != nil
d infDecAmount
// s is the generated value of this quantity to avoid recalculation
s string
// Change Format at will. See the comment for Canonicalize for more details.
Format
}
使用指标服务器进行监控
Kubernetes 指标服务器实现了 Kubernetes 指标 API。
你可以通过 Helm 部署它:
$ helm repo add metrics-server https://kubernetes-sigs.github.io/metrics-server/
$ helm upgrade --install metrics-server metrics-server/metrics-server
Release "metrics-server" does not exist. Installing it now.
NAME: metrics-server
LAST DEPLOYED: Sun Oct 9 14:11:54 2022
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
***********************************************************************
* Metrics Server *
***********************************************************************
Chart version: 3.8.2
App version: 0.6.1
Image tag: k8s.gcr.io/metrics-server/metrics-server:v0.6.1
***********************************************************************
在 minikube 上,你可以通过启用它作为插件来使用:
$ minikube addons enable metrics-server
▪ Using image k8s.gcr.io/metrics-server/metrics-server:v0.4.2
 The 'metrics-server' addon is enabled
请注意,在撰写本文时,minikube 上的指标服务器存在一个问题,该问题已在 Kubernetes 1.27 中修复(参见 github.com/kubernetes/minikube/issues/13969)。
我们将使用一个 kind 集群来部署 metrics-server。
等待几分钟,让指标服务器收集一些数据后,你可以使用以下命令查询节点指标:
$ k get --raw "/apis/metrics.k8s.io/v1beta1/nodes" | jq .
{
"kind": "NodeMetricsList",
"apiVersion": "metrics.k8s.io/v1beta1",
"metadata": {},
"items": [
{
"metadata": {
"name": "kind-control-plane",
"creationTimestamp": "2022-10-09T21:24:12Z",
"labels": {
"beta.kubernetes.io/arch": "arm64",
"beta.kubernetes.io/os": "linux",
"kubernetes.io/arch": "arm64",
"kubernetes.io/hostname": "kind-control-plane",
"kubernetes.io/os": "linux",
"node-role.kubernetes.io/control-plane": "",
"node.kubernetes.io/exclude-from-external-load-balancers": ""
}
},
"timestamp": "2022-10-09T21:24:05Z",
"window": "20.022s",
"usage": {
"cpu": "115537281n",
"memory": "47344Ki"
}
}
]
}
此外,kubectl top 命令从指标服务器获取信息:
$ k top nodes
NAME CPU(cores) CPU% MEMORY(bytes) MEMORY%
kind-control-plane 125m 3% 46Mi 1%
我们还可以获取 Pod 的指标:
$ k top pods -A
NAMESPACE NAME CPU(cores) MEMORY(bytes)
default metrics-server-554f79c654-hw2c7 4m 18Mi
kube-system coredns-565d847f94-t8knf 2m 12Mi
kube-system coredns-565d847f94-wdqzx 2m 14Mi
kube-system etcd-kind-control-plane 24m 28Mi
kube-system kindnet-fvfs7 1m 7Mi
kube-system kube-apiserver-kind-control-plane 43m 339Mi
kube-system kube-controller-manager-kind-control-plane 18m 48Mi
kube-system kube-proxy-svdc6 1m 11Mi
kube-system kube-scheduler-kind-control-plane 4m 21Mi
local-path-storage local-path-provisioner-684f458cdd-24w88 2m 6Mi
指标服务器也是 Kubernetes 仪表盘中的性能信息来源。
Prometheus 的崛起
Prometheus(prometheus.io/)是另一个成熟的 CNCF 开源项目。它专注于指标收集和告警管理。它具有一个简单但强大的数据模型,用于管理时间序列数据,并提供一个复杂的查询语言。它被认为是 Kubernetes 领域的最佳选择。Prometheus 允许你定义在固定间隔触发的记录规则,并从目标收集数据。此外,你可以定义告警规则来评估某个条件,并在满足条件时触发告警。
与其他监控解决方案相比,它具有几个独特的特点:
-
收集系统是通过 HTTP 拉取的。没有人需要将指标推送到 Prometheus(但通过网关支持推送)。
-
一个多维数据模型(每个指标都是一个命名的时间序列,并且每个数据点附有一组键/值对)。
-
PromQL:一种强大而灵活的查询语言,用于切割和分析你的指标。
-
Prometheus 服务器节点是独立的,不依赖于共享存储。
-
目标发现可以是动态的,也可以通过静态配置进行。
-
内建时间序列存储,但如果需要,也支持其他后端。
-
内建告警管理器,并支持定义告警规则。
下图展示了整个系统:

图 13.6:Prometheus 架构
安装 Prometheus
Prometheus 是一个复杂的系统,正如你所看到的。安装它的最佳方式是使用 Prometheus 操作员(github.com/prometheus-operator/)。kube-prometheus(github.com/prometheus-operator/kube-prometheus)子项目安装了操作员本身,以及许多附加组件,并以稳健的方式配置它们。
第一步是克隆 Git 仓库:
$ git clone https://github.com/prometheus-operator/kube-prometheus.git
Cloning into 'kube-prometheus'...
remote: Enumerating objects: 17062, done.
remote: Counting objects: 100% (185/185), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 17062 (delta 135), reused 155 (delta 116), pack-reused 16877
Receiving objects: 100% (17062/17062), 8.76 MiB | 11.63 MiB/s, done.
Resolving deltas: 100% (11135/11135), done.
接下来,设置清单安装了几个 CRD,并创建了一个名为 monitoring 的命名空间:
$ kubectl create -f manifests/setup
customresourcedefinition.apiextensions.k8s.io/alertmanagerconfigs.monitoring.coreos.com created
customresourcedefinition.apiextensions.k8s.io/alertmanagers.monitoring.coreos.com created
customresourcedefinition.apiextensions.k8s.io/podmonitors.monitoring.coreos.com created
customresourcedefinition.apiextensions.k8s.io/probes.monitoring.coreos.com created
customresourcedefinition.apiextensions.k8s.io/prometheuses.monitoring.coreos.com created
customresourcedefinition.apiextensions.k8s.io/prometheusrules.monitoring.coreos.com created
customresourcedefinition.apiextensions.k8s.io/servicemonitors.monitoring.coreos.com created
customresourcedefinition.apiextensions.k8s.io/thanosrulers.monitoring.coreos.com created
namespace/monitoring created
现在,我们可以安装这些清单文件:
$ kubectl create -f manifests
...
输出太长无法显示,但让我们来看一下实际上安装了什么。结果发现,它安装了多个部署、StatefulSet、一个 DaemonSet 和许多服务:
$ k get deployments -n monitoring
NAME READY UP-TO-DATE AVAILABLE AGE
blackbox-exporter 1/1 1 1 3m38s
grafana 1/1 1 1 3m37s
kube-state-metrics 1/1 1 1 3m37s
prometheus-adapter 2/2 2 2 3m37s
prometheus-operator 1/1 1 1 3m37s
$ k get statefulsets -n monitoring
NAME READY AGE
alertmanager-main 3/3 2m57s
prometheus-k8s 2/2 2m57s
$ k get daemonsets -n monitoring
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
node-exporter 1 1 1 1 1 kubernetes.io/os=linux 4m4s
$ k get services -n monitoring
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
alertmanager-main ClusterIP 10.96.231.0 <none> 9093/TCP,8080/TCP 4m25s
alertmanager-operated ClusterIP None <none> 9093/TCP,9094/TCP,9094/UDP 3m35s
blackbox-exporter ClusterIP 10.96.239.94 <none> 9115/TCP,19115/TCP 4m25s
grafana ClusterIP 10.96.80.116 <none> 3000/TCP 4m24s
kube-state-metrics ClusterIP None <none> 8443/TCP,9443/TCP 4m24s
node-exporter ClusterIP None <none> 9100/TCP 4m24s
prometheus-adapter ClusterIP 10.96.139.149 <none> 443/TCP 4m24s
prometheus-k8s ClusterIP 10.96.51.85 <none> 9090/TCP,8080/TCP 4m24s
prometheus-operated ClusterIP None <none> 9090/TCP 3m35s
prometheus-operator ClusterIP None <none> 8443/TCP 4m24s
这是一个高可用性配置。如你所见,Prometheus 本身作为 StatefulSet 部署,并且有两个副本,而告警管理器则作为 StatefulSet 部署,并有三个副本。
部署包括 blackbox-exporter、用于可视化指标的 Grafana、收集 Kubernetes 特定指标的 kube-state-metrics、Prometheus 适配器(一个与标准 Kubernetes 指标服务器兼容的替代方案),最后是 Prometheus 操作员。
与 Prometheus 交互
Prometheus 有一个基本的 Web 用户界面,你可以用它来探索其指标。让我们进行端口转发到 localhost:
$ k port-forward -n monitoring statefulset/prometheus-k8s 9090
Forwarding from 127.0.0.1:9090 -> 9090
Forwarding from [::1]:9090 -> 9090
然后,你可以浏览到 http://localhost:9090,在这里你可以选择不同的指标,并查看原始数据或图表:

图 13.7:Prometheus 用户界面
Prometheus 记录了大量的指标(在我当前的配置中是 9090)。在 Kubernetes 中,最相关的指标是 kube-state-metrics 和节点导出器暴露的指标。
集成 kube-state-metrics
Prometheus 操作员已经安装了 kube-state-metrics。它是一个监听 Kubernetes 事件的服务,并通过 /metrics HTTP 端点以 Prometheus 预期的格式暴露这些事件。因此,它是一个 Prometheus 导出器。
这与 Kubernetes 指标服务器非常不同,Kubernetes 指标服务器是 Kubernetes 为节点和 Pod 提供指标的标准方式,并且允许你暴露自定义的指标。Kubernetes 指标服务器是一个服务,它定期查询 Kubernetes 获取数据并将其存储在内存中。它通过 Kubernetes 指标 API 暴露其数据。Prometheus 适配器将 Kubernetes 指标服务器的信息进行适配,并以 Prometheus 格式暴露。
kube-state-metrics 暴露的指标非常广泛。以下是这些指标组的列表,单单这一组就已经非常庞大了。每一组对应一个 Kubernetes API 对象,并包含多个指标:
-
CertificateSigningRequest指标 -
ConfigMap指标 -
CronJob指标 -
DaemonSet指标 -
Deployment指标 -
Endpoint指标 -
HorizontalPodAutoscaler指标 -
Ingress指标 -
Job指标 -
LimitRange指标 -
MutatingWebhookConfiguration指标 -
Namespace指标 -
NetworkPolicy指标 -
Node指标 -
PersistentVolume指标 -
PersistentVolumeClaim指标 -
PodDisruptionBudget指标 -
Pod指标 -
ReplicaSet指标 -
ReplicationController指标 -
ResourceQuota指标 -
Secret指标 -
Service指标 -
StatefulSet指标 -
StorageClass指标 -
ValidatingWebhookConfiguration指标 -
VerticalPodAutoscaler指标 -
VolumeAttachment指标
例如,以下是 Kubernetes 服务收集的指标:
-
kube_service_info -
kube_service_labels -
kube_service_created -
kube_service_spec_type
使用节点导出器
kube-state-metrics 从 Kubernetes API 服务器收集节点信息,但这些信息相当有限。Prometheus 自带的节点导出器可以收集大量的节点底层信息。请记住,Prometheus 可能是 Kubernetes 上的事实标准指标平台,但它并非 Kubernetes 专用。对于使用 Prometheus 的其他系统,节点导出器非常重要。在 Kubernetes 上,如果你管理自己的节点,这些信息也非常宝贵。
这里是节点导出器暴露的一小部分指标:

图 13.8:节点导出器指标
集成自定义指标
内建指标、节点指标和 Kubernetes 指标很好,但通常情况下,最有趣的指标是特定领域的,需要作为自定义指标来捕获。有两种方式可以做到:
-
编写自己的导出器并告诉 Prometheus 去抓取它
-
使用 Push 网关,将指标推送到 Prometheus
在我的书《Kubernetes 微服务实战》(www.packtpub.com/product/hands-on-microservices-with-kubernetes/9781789805468)中,我提供了一个完整的示例,展示如何从 Go 服务实现自己的导出器。
如果你已经有了基于推送的指标收集器,并且只想让 Prometheus 记录这些指标,那么 Push 网关更为合适。它提供了一个从其他指标收集系统到 Prometheus 的便捷迁移路径。
使用 Alertmanager 进行告警
收集指标很好,但当问题发生时(或者理想情况下,在问题发生之前),你希望收到通知。在 Prometheus 中,这是 Alertmanager 的任务。你可以定义基于表达式的指标规则,当这些表达式成立时,它们会触发告警。
警报可以担任多种角色。它们可以由负责缓解特定问题的控制器自动处理,也可以在凌晨 3 点叫醒可怜的值班工程师,还可以触发电子邮件或群聊消息,或者以上述任意组合。
Alertmanager 允许您将类似的警报分组为单个通知,抑制正在触发的其他警报的通知,并消除警报。在大规模系统出现问题时,所有这些功能都非常有用。利益相关者了解情况,并且在排除故障和寻找根本原因时,不需要重复警报或同一警报的多个变体不断触发。
Prometheus operator 的一个很酷的功能是它在自定义资源定义 (CRD) 中管理所有内容。这包括所有规则,包括警报规则:
$ k get prometheusrules -n monitoring
NAME AGE
alertmanager-main-rules 11h
grafana-rules 11h
kube-prometheus-rules 11h
kube-state-metrics-rules 11h
kubernetes-monitoring-rules 11h
node-exporter-rules 11h
prometheus-k8s-prometheus-rules 11h
prometheus-operator-rules 11h
这里是 NodeFilesystemAlmostOutOfSpace 警报,检查节点上文件系统的可用磁盘空间是否低于 30 分钟的阈值。如果注意到,有两个几乎相同的警报。当可用空间低于 5%时,将触发警告警报。但是,如果空间低于 3%,则触发严重警报。请注意 runbook_url 字段,它指向一个页面,详细说明了警报的更多信息以及如何解决问题:
$ k get prometheusrules node-exporter-rules -n monitoring -o yaml | grep NodeFilesystemAlmostOutOfSpace -A 14
- alert: NodeFilesystemAlmostOutOfSpace
annotations:
description: Filesystem on {{ $labels.device }} at {{ $labels.instance }}
has only {{ printf "%.2f" $value }}% available space left.
runbook_url: https://runbooks.prometheus-operator.dev/runbooks/node/nodefilesystemalmostoutofspace
expr: |
(
node_filesystem_avail_bytes{job="node-exporter",fstype!=""} / node_filesystem_size_bytes{job="node-exporter",fstype!=""} * 100 < 5
and
node_filesystem_readonly{job="node-exporter",fstype!=""} == 0
)
for: 30m
labels:
severity: warning
- alert: NodeFilesystemAlmostOutOfSpace
annotations:
description: Filesystem on {{ $labels.device }} at {{ $labels.instance }}
has only {{ printf "%.2f" $value }}% available space left.
runbook_url: https://runbooks.prometheus-operator.dev/runbooks/node/nodefilesystemalmostoutofspace
summary: Filesystem has less than 3% space left.
expr: |
(
node_filesystem_avail_bytes{job="node-exporter",fstype!=""} / node_filesystem_size_bytes{job="node-exporter",fstype!=""} * 100 < 3
and
node_filesystem_readonly{job="node-exporter",fstype!=""} == 0
)
for: 30m
labels:
severity: critical
警报非常重要,但有些情况下,您希望可视化系统的整体状态或深入了解特定方面。这就是可视化发挥作用的地方。
使用 Grafana 可视化您的指标
您已经看过 Prometheus 表达式浏览器,它可以将您的指标显示为图形或表格形式。但我们可以做得更好。Grafana (grafana.com) 是一个专注于呈现令人惊叹的美观指标可视化的开源监控系统。它本身不存储指标,但可以与多个数据源一起工作,其中包括 Prometheus。Grafana 也具有警报功能。在使用 Prometheus 时,您可能更倾向于依赖它的 Alertmanager。
Prometheus operator 安装 Grafana 并配置了大量有用的 Kubernetes 仪表板。看看这个 Kubernetes 容量的漂亮仪表板:

图 13.9:Grafana 仪表板
要访问 Grafana,请输入以下命令:
$ k port-forward -n monitoring deploy/grafana 3000
Forwarding from 127.0.0.1:3000 -> 3000
Forwarding from [::1]:3000 -> 3000
然后您可以浏览 http://localhost:3000 并在 Grafana 中玩得开心。Grafana 需要用户名和密码。默认凭据为 admin 作为用户名和 admin 作为密码。
在通过 kube-prometheus 部署 Grafana 时,这里有一些默认仪表板配置:

图 13.10:默认 Grafana 仪表板
如你所见,列表相当广泛,但如果你愿意,你可以自定义仪表盘。你可以使用 Grafana 创建许多精美的可视化图表。我鼓励你深入探索。Grafana 仪表盘存储为配置映射。如果你想添加自定义仪表盘,只需添加一个包含仪表盘规范的配置映射。会有一个专门的 sidecar 容器监视新配置映射的添加,并确保添加你的自定义仪表盘。
你也可以通过 Grafana UI 添加仪表盘。
考虑 Loki
如果你喜欢 Prometheus 和 Grafana,并且还没有决定使用集中式日志解决方案(或者你对当前的日志解决方案不满意),那么你应该考虑使用 Grafana Loki (grafana.com/oss/loki/)。Loki 是一个开源的日志聚合项目,灵感来自 Prometheus。与大多数日志聚合系统不同,它不是对日志内容进行索引,而是对应用于日志的一组标签进行索引。这使得它非常高效。它仍然是相对较新的项目(始于 2018 年),因此在决定是否采用之前,你应该评估它是否符合你的需求。有一点是确定的:Loki 有着出色的 Grafana 支持。
相较于像 EFK 这样的解决方案,Loki 在使用 Prometheus 作为度量平台时有几个优势。特别是,你用于标记度量的标签集同样适用于标记日志。而且,Grafana 作为统一的可视化平台,用于展示日志和度量数据,这一点非常有用。
我们已经花费了大量时间讨论 Kubernetes 上的度量。现在让我们谈谈分布式追踪和 Jaeger 项目。
使用 Kubernetes 进行分布式追踪
在一个基于微服务的系统中,每个请求可能会在多个微服务之间传递,彼此调用、等待队列,并触发无服务器函数。要调试和排除这类系统的故障,你需要能够追踪请求并沿着它们的路径跟踪。
分布式追踪提供了几个功能,帮助开发者和运维人员理解他们的分布式系统:
-
分布式事务监控
-
性能和延迟追踪
-
根本原因分析
-
服务依赖关系分析
-
分布式上下文传播
分布式追踪通常需要应用程序和服务参与到端点的仪表化中。由于微服务世界是多语言的,可能会使用多种编程语言。因此,使用一个支持多种编程语言的共享分布式追踪规范和框架是很有意义的。由此出现了 OpenTelemetry。
什么是 OpenTelemetry?
OpenTelemetry(opentelemetry.io)是一个 API 规范及一套用于仪表化、收集和导出日志、指标和追踪的框架和库,涵盖不同语言。它诞生于 2019 年 5 月,当时 OpenCensus 和 OpenTracing 项目合并。它也是一个孵化中的 CNCF 项目。OpenTelemetry 得到了多个产品的支持,并成为事实上的标准。它可以从各种开源和商业来源收集数据。查看完整列表:github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/receiver。
通过使用符合 OpenTelemetry 标准的产品,您不会被锁定,并且您将使用一个可能对您的开发人员来说很熟悉的 API。
针对几乎所有主流编程语言都有仪表化库:
-
C++
-
.NET
-
Erlang/Elixir
-
Go
-
Java
-
JavaScript
-
PHP
-
Python
-
Ruby
-
Rust
-
Swift
OpenTelemetry 追踪概念
我们在这里将重点讨论 OpenTelemetry 的追踪概念,跳过之前我们涉及的日志记录和指标概念。
这两个主要概念是Span和Trace。
Span是工作或操作的基本单元。它有一个名称、开始时间和持续时间。如果一个操作启动另一个操作,Span 可以嵌套。Span 通过唯一的 ID 和上下文传播。Trace是由同一个请求发起的 Spans 构成的非循环图,这些 Spans 共享相同的上下文。Trace代表了请求在系统中执行的路径。下图说明了 Trace 与 Spans 之间的关系:

图 13.11:OpenTelemetry 中 Trace 与 Span 的关系
现在我们了解了 OpenTelemetry 的概况,让我们来看看 Jaeger 项目。
介绍 Jaeger
Jaeger(www.jaegertracing.io/)是另一个 CNCF 毕业项目,就像 Fluentd 和 Prometheus 一样。它完成了 Kubernetes 的 CNCF 毕业可观察性项目的三位一体。Jaeger 最初由 Uber 开发,并迅速成为 Kubernetes 的领先分布式追踪解决方案。
还有其他开源分布式追踪系统,如 Zipkin(zipkin.io)和 SigNoz(signoz.io)。这些系统(以及 Jaeger)的灵感来自 Google 的 Dapper(research.google.com/pubs/pub36356.html)。云平台提供了自己的追踪器,如 AWS X-Ray。这个领域也有多个商业产品:
-
Aspecto(
www.aspecto.io) -
蜂窝(
www.honeycomb.io) -
Lightstep(
lightstep.com)
Jaeger 的优点包括:
-
可扩展设计
-
支持多种协议——OpenTelemetry、OpenTracing 和 Zipkin
-
轻量级内存占用
-
代理通过 UDP 收集指标
-
高级采样控制
Jaeger 架构
Jaeger 是一个可扩展的系统。它可以作为一个包含所有组件的单一二进制文件部署,并将数据存储在内存中;也可以作为一个分布式系统,其中跨度和跟踪信息存储在持久化存储中。
Jaeger 拥有多个组件,它们协同工作,提供世界级的分布式追踪体验。以下图示展示了其架构:

图 13.12:Jaeger 架构
让我们了解每个组件的目的。
客户端库
最初,Jaeger 有自己的客户端库,实施了 OpenTracing API,用于在服务或应用程序中实现分布式追踪。现在,Jaeger 推荐使用 OpenTelemetry 客户端库,Jaeger 客户端库已经被淘汰。
Jaeger 代理
代理部署在每个节点本地。它通过 UDP 监听跨度信息——这使得它的性能非常好——将它们批量处理后发送到收集器。这样,服务无需发现收集器或担心与其连接。被仪表化的服务只需将其跨度发送给本地代理。代理还可以通知客户端关于采样策略的信息。
Jaeger 收集器
收集器接收来自所有代理的跟踪信息。它负责验证和转换跟踪信息。然后将这些跟踪信息发送到数据存储或 Kafka 实例,从而实现异步处理跟踪。
Jaeger ingester
Ingester 为后续的查询对跟踪信息进行索引,以便更高效地查询,并将其存储在数据存储中,数据存储可以是 Cassandra 或 Elasticsearch 集群。
Jaeger 查询
Jaeger 查询服务负责呈现一个用户界面,用于查询收集器存储的跟踪和跨度。
这涵盖了 Jaeger 的架构及其组件。接下来,我们来看如何安装并使用它。
安装 Jaeger
有 Helm 图表可供安装 Jaeger 和 Jaeger 操作员:
$ helm repo add jaegertracing https://jaegertracing.github.io/helm-charts
"jaegertracing" has been added to your repositories
$ helm search repo jaegertracing
NAME CHART VERSION APP VERSION DESCRIPTION
jaegertracing/jaeger 0.62.1 1.37.0 A Jaeger Helm chart for Kubernetes
jaegertracing/jaeger-operator 2.36.0 1.38.0 jaeger-operator Helm chart for Kubernetes
Jaeger 操作员需要 cert-manager,但不会自动安装它。让我们先安装它:
$ helm repo add jetstack https://charts.jetstack.io
"jetstack" has been added to your repositories
$ helm install \
cert-manager jetstack/cert-manager \
--namespace cert-manager \
--create-namespace \
--version v1.9.1 \
--set installCRDs=true
NAME: cert-manager
LAST DEPLOYED: Mon Oct 17 10:28:43 2022
NAMESPACE: cert-manager
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
cert-manager v1.9.1 has been deployed successfully!
In order to begin issuing certificates, you will need to set up a ClusterIssuer
or Issuer resource (for example, by creating a 'letsencrypt-staging' issuer).
More information on the different types of issuers and how to configure them
can be found in our documentation:
https://cert-manager.io/docs/configuration/
For information on how to configure cert-manager to automatically provision
Certificates for Ingress resources, take a look at the `ingress-shim`
documentation:
https://cert-manager.io/docs/usage/ingress/
现在,我们可以将 Jaeger 操作员安装到可观察性命名空间:
$ helm install jaeger jaegertracing/jaeger-operator \
-n observability --create-namespace
NAME: jaeger
LAST DEPLOYED: Mon Oct 17 10:30:58 2022
NAMESPACE: observability
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
jaeger-operator is installed.
Check the jaeger-operator logs
export POD=$(kubectl get pods -l app.kubernetes.io/instance=jaeger -l app.kubernetes.io/name=jaeger-operator --namespace observability --output name)
kubectl logs $POD --namespace=observability
部署被称为jaeger-jaeger-operator:
$ k get deploy -n observability
NAME READY UP-TO-DATE AVAILABLE AGE
jaeger-jaeger-operator 1/1 1 1 3m21s
现在,我们可以使用 Jaeger CRD 创建一个 Jaeger 实例。操作员会监控这个自定义资源并创建所有必要的资源。这里是最简单的 Jaeger 配置。它使用默认的 AllInOne 策略,部署一个包含所有组件(代理、收集器、查询、Ingester 和 Jaeger UI)的单一 Pod,并使用内存存储。这适用于本地开发和测试:
$ cat <<EOF | k apply -f -
apiVersion: jaegertracing.io/v1
kind: Jaeger
metadata:
name: simplest
namespace: observability
EOF
jaeger.jaegertracing.io/simplest created
$ k get jaegers -n observability
NAME STATUS VERSION STRATEGY STORAGE AGE
simplest 5m54s
让我们启动 Jaeger 用户界面:
$ k port-forward deploy/simplest 8080:16686 -n observability
Forwarding from 127.0.0.1:8080 -> 16686
Forwarding from [::1]:8080 -> 16686
现在,我们可以浏览到http://localhost:8080并查看 Jaeger 用户界面:

图 13.13:Jaeger 用户界面
在下一章,第十四章,利用服务网格中,我们将更多地了解 Jaeger 以及如何具体使用它来追踪通过网格的请求。现在,让我们将注意力转向使用我们讨论过的所有监控和可观察性机制来进行故障排除。
故障排除问题
排查一个复杂的分布式系统问题并非易事。抽象、关注点分离、信息隐藏和封装在开发、测试和系统变更时非常有效。但当问题出现时,你需要跨越所有这些边界和抽象层,从用户在应用中的操作,通过整个技术栈,一直到基础设施,跨越所有业务逻辑、异步处理、遗留系统和第三方集成。这对于大型单体系统来说已经是一个挑战,但对于基于微服务的分布式系统来说更是如此。监控会为你提供帮助,但我们首先需要讨论准备、流程和最佳实践。
利用预生产环境
在构建大型系统时,开发人员在本地机器上工作(这里暂时忽略云开发环境),最终,代码会部署到生产环境。但在这两者之间有一些步骤。复杂的系统运行在一个不容易在本地复制的环境中。
你应该在一个与生产环境相似的环境中测试代码或配置的变更。这就是你的预生产环境,在这里你应该能够发现大多数无法通过开发人员在本地开发环境中运行测试捕获到的问题。
软件交付过程应该尽早检测到不良代码和配置。但有时候,不良变更只能在生产环境中被检测到,并导致事故。你应该建立一个事故管理流程,通常包括恢复到引发问题的组件的先前版本,然后通过查看日志、指标和追踪——有时也需要在预生产环境中调试——来寻找根本原因。
但是,有时候问题并不在于你的代码或配置。最终,你的 Kubernetes 集群运行在节点上(即便它是托管的),这些节点可能会遇到许多问题。
在节点级别检测问题
在 Kubernetes 的概念模型中,工作单元是 pod。然而,pod 是被调度到节点上的。在基础设施的监控和可靠性方面,节点是最需要关注的部分,因为 Kubernetes 本身(调度器、副本集和水平 pod 自动扩展器)会管理 pod。kubelet 能够识别节点上的许多问题,并会更新 API 服务器。你可以使用以下命令查看节点状态及其是否准备就绪:
$ k describe no kind-control-plane | grep Conditions -A 6
Conditions:
Type Status LastHeartbeatTime LastTransitionTime Reason Message
---- ------ ----------------- ------------------ ------ -------
MemoryPressure False Fri, 21 Oct 2022 01:09:33 -0700 Mon, 17 Oct 2022 10:27:24 -0700 KubeletHasSufficientMemory kubelet has sufficient memory available
DiskPressure False Fri, 21 Oct 2022 01:09:33 -0700 Mon, 17 Oct 2022 10:27:24 -0700 KubeletHasNoDiskPressure kubelet has no disk pressure
PIDPressure False Fri, 21 Oct 2022 01:09:33 -0700 Mon, 17 Oct 2022 10:27:24 -0700 KubeletHasSufficientPID kubelet has sufficient PID available
Ready True Fri, 21 Oct 2022 01:09:33 -0700 Mon, 17 Oct 2022 10:27:52 -0700 KubeletReady kubelet is posting ready status
注意最后一个条件,Ready。这意味着 Kubernetes 可以将待调度的 pod 安排到此节点上。
但是,可能会有一些问题是 kubelet 无法检测到的。以下是一些问题:
-
坏 CPU
-
坏内存
-
坏磁盘
-
内核死锁
-
文件系统损坏
-
容器运行时的问题(例如,Docker 守护进程)
我们需要另一个解决方案。引入节点问题检测器。
节点问题检测器是一个在每个节点上运行的 Pod。它需要解决一个困难的问题。它必须能够在不同的环境、不同的硬件和不同的操作系统中检测各种低级问题。它必须足够可靠,不受自身影响(否则,它无法报告问题),并且需要具有相对较低的开销,以避免对控制平面造成垃圾邮件影响。源代码位于 github.com/kubernetes/node-problem-detector。
最自然的方式是将节点问题检测器部署为 DaemonSet,这样每个节点上总会有一个节点问题检测器在运行。在 Google 的 GKE 集群中,它作为附加组件运行。
问题守护进程
节点问题检测器(字面上的双关语)的问题在于,它需要处理的“问题”太多了。试图将所有这些问题塞进一个单一的代码库中可能导致一个复杂、臃肿、永远无法稳定的代码库。节点问题检测器的设计要求将报告节点问题的核心功能与特定问题检测分开。报告 API 基于通用条件和事件。问题检测应该由独立的问题守护进程(每个进程在自己的容器中)来完成。这样,可以在不影响核心节点问题检测器的情况下,添加和发展新的问题检测器。此外,控制平面可能有一个补救控制器,能够自动解决一些节点问题,从而实现自愈。
此时,问题守护进程被嵌入到节点问题检测器的二进制文件中,并以 Goroutine 形式执行,因此你还不能享受到松耦合设计的好处。未来,每个问题守护进程将运行在自己的容器中。
除了节点问题,另一个可能发生故障的领域是网络。我们之前讨论过的各种监控工具可以帮助我们识别基础设施、代码或第三方依赖中的问题。
让我们来谈谈我们工具箱中的各种选项,它们如何比较,并且如何最大化它们的效果。
仪表盘 vs. 警报
仪表盘纯粹是为人类设计的。一个好的仪表盘的设计理念是通过一眼就能提供大量关于系统或特定组件状态的有用信息。设计一个好的仪表盘有很多用户体验的元素,和设计任何用户界面一样。监控仪表盘可以覆盖许多组件的数据,跨越长时间段,并且可能支持深入挖掘到更细节的层次。
警报则是定期检查某些条件(通常基于指标),一旦触发,可能会自动解决警报的原因,或者最终通知人类,后者通常会通过查看一些仪表板开始调查。
自愈系统可以自动处理某些警报(或者理想情况下,在警报被触发之前解决问题)。人类通常会参与故障排除。即使系统在某些情况下自动恢复了问题,也会有人类回顾系统所采取的措施,并验证当前行为,包括自动恢复是否足够。
在许多情况下,由人类通过查看仪表板(不可扩展)或通过警报通知发现的严重问题(即事件)将需要一些调查、修复,并在之后进行事后分析。在这些阶段,下一层的监控发挥作用。
日志与指标与错误报告
让我们了解每个工具的优势,并如何将它们的优点结合起来以调试难题。假设我们有良好的测试覆盖率,并且我们的业务/领域逻辑代码大体上是正确的。我们遇到的问题发生在生产环境中。可能会有几种只在生产环境中发生的问题:
-
配置错误(生产环境配置错误或过时)
-
基础设施的配置
-
权限不足,无法访问数据、服务或第三方集成
-
环境特定代码
-
生产输入暴露的软件漏洞
-
扩展性和性能问题
这确实是一个长长的清单,而且可能还不完整。通常,当出现问题时,都是响应某些变化。那么我们在说什么样的变化呢?以下是一些例子:
-
部署新版本的代码
-
部署应用程序的动态重新配置
-
新用户或现有用户改变与系统交互的方式
-
底层基础设施的变化(例如,由云服务提供商引起)
-
代码中新路径首次被使用(例如,回退到另一个区域)
由于问题和原因的范围非常广泛,因此很难建议一个线性的解决路径。例如,如果故障导致错误,那么查看错误报告可能是最好的起点。但如果问题是某个本应发生的动作没有发生,那么就没有错误可供查看。在这种情况下,可能需要查看日志并将其与之前成功请求的日志进行比较。在基础设施或扩展性问题的情况下,指标可能为我们提供最初的洞察。
底线是,调试分布式系统需要将多个工具结合使用,以追寻那永远难以捉摸的根本原因。
当然,在拥有大量组件和微服务的分布式系统中,根本不清楚该从哪里入手。这就是分布式追踪的亮点所在,它能帮助我们缩小范围并识别罪魁祸首。
使用分布式追踪检测性能和根本原因
一旦启用了分布式追踪,每个请求都会生成一个包含多个跨度的追踪图。Jaeger 默认使用 1/1000 的采样率,因此偶尔可能会漏掉一些问题,但对于持续性的问题,我们仍然能够追踪请求的路径,查看每个跨度的处理时长,如果请求的处理由于某种原因中断,我们也能很容易地发现。此时,你就回到了日志、指标和错误中,去追寻根本原因。
正如你所见,在像 Kubernetes 这样复杂的系统中排查问题远非易事。你需要全面的可观察性,包括日志、指标和分布式追踪。你还需要对你的系统有深入的了解,才能快速、可靠地配置、监控并解决问题。
总结
在这一章中,我们介绍了监控、可观察性和故障排查的相关内容。我们从回顾监控的各个方面开始:日志、指标、错误报告和分布式追踪。然后,我们讨论了如何将监控能力集成到 Kubernetes 集群中。我们查看了多个 CNCF 项目,例如用于日志聚合的 Fluentd、用于指标收集和告警管理的 Prometheus、用于可视化的 Grafana,以及用于分布式追踪的 Jaeger。接着,我们探讨了如何排查大型分布式系统中的故障。我们意识到这有多么困难,以及为什么我们需要这么多不同的工具来解决这些问题。
在下一章,我们将把话题提升到一个新的层次,深入探讨服务网格。我对服务网格非常兴奋,因为它们将许多与云原生微服务应用程序相关的复杂性抽象并外部化到微服务之外。这具有很大的现实价值。
加入我们的 Discord 群组吧!
与其他用户、云计算专家、作者和志同道合的专业人士一起阅读本书。
提出问题、为其他读者提供解决方案、通过“问我任何事”环节与作者聊天,更多精彩内容等你参与。
扫描二维码或访问链接立即加入社区。

第十四章:使用服务网格
在上一章中,我们讨论了监控和可观察性。全面监控的一个障碍是它需要对与业务逻辑无关的代码进行大量更改。
在本章中,我们将学习服务网格如何让你将许多横切关注点从应用程序代码中外部化。服务网格是设计、演化和操作 Kubernetes 上的分布式系统的一种真正的范式转变。我喜欢把它看作是云原生分布式系统的面向切面的编程。我们还将深入探讨 Istio 服务网格。我们将涵盖的主题有:
-
什么是服务网格?
-
选择一个服务网格
-
理解 Istio 架构
-
将 Istio 集成到你的 Kubernetes 集群中
-
使用 Istio
让我们直接进入正题。
什么是服务网格?
服务网格是一种适用于由多个微服务组成的大规模云原生应用程序的架构模式。当你的应用程序按微服务集合的结构组织时,Kubernetes 集群内的微服务之间的边界会发生许多交互。这与传统的单体应用程序不同,后者大多数工作由单个操作系统进程完成。
以下是与每个微服务或微服务之间的交互相关的一些问题:
-
高级负载均衡
-
服务发现
-
支持金丝雀部署
-
缓存
-
跨多个微服务追踪请求
-
服务之间的身份验证
-
限制服务在给定时间内处理的请求数量
-
自动重试失败的请求
-
当一个组件持续失败时,切换到一个备用组件
-
收集指标
所有这些问题与服务的领域逻辑完全无关,但它们都非常重要。一种天真的做法是直接在每个微服务中编写所有这些问题的代码。这显然无法扩展。因此,一种典型的做法是将所有这些功能打包成一个大型库或一组库,并在每个服务中使用这些库。

图 14.1:典型的基于库的架构
大型库方法存在几个问题:
-
你需要在所有使用的编程语言中实现这个库,并确保它们是兼容的
-
如果你想更新你的库,你必须为所有服务更新版本
-
如果库的新版本不向后兼容,则很难逐步升级服务
与此相比,服务网格不会直接接触到你的应用程序。它会将一个边车代理容器注入到每个 Pod 中,并使用服务网格控制器。代理拦截所有 Pod 之间的通信,并与网格控制器协作,可以处理所有横切关注点。

图 14.2:边车服务网格架构
以下是代理注入方法的一些属性:
-
应用程序对服务网格一无所知
-
你可以为每个 pod 开启或关闭网格,并独立更新网格
-
无需在每个节点上部署代理
-
同一节点上的不同 pod 可以有不同的 sidecar(或版本)
-
每个 pod 都有自己的一份代理
在 Kubernetes 上,它看起来是这样的:

图 14.3:Kubernetes 中的服务网格架构
还有一种方式将服务网格代理实现为节点代理,而不是注入到每个 pod 中。这种方式较为少见,但在某些情况下(特别是在非 Kubernetes 环境中),它是有用的。它可以节省在运行许多小 pod 的节点上的资源,因为所有 sidecar 容器的开销会累计起来。

图 14.4:节点代理服务网格架构
在服务网格的世界中,有一个控制平面,通常是 Kubernetes 上的一组控制器,还有一个数据平面,它由连接网格中所有服务的代理组成。数据平面由所有拦截服务之间通信的 sidecar 容器(或节点代理)组成。控制平面负责管理代理,并配置当任何服务之间的流量或服务与外部世界的流量被拦截时实际发生的情况。
现在,我们已经对服务网格是什么、如何工作以及它为何如此有用有了清晰的了解,让我们回顾一下目前存在的一些服务网格。
选择一个服务网格
服务网格的概念相对较新,但目前已有许多选择。我们将在本章后面使用 Istio。不过,你也许更喜欢为你的用例选择其他的服务网格。下面是对当前几种服务网格的简要回顾。
Envoy
Envoy (www.envoyproxy.io) 是另一个 CNCF 毕业的项目。它是一个非常多用途且高性能的 L7 代理。它提供了许多服务网格功能;然而,它被认为是较低级别且配置起来较为困难的。它也不是专门针对 Kubernetes 的。一些 Kubernetes 服务网格使用 Envoy 作为底层数据平面,并提供一个 Kubernetes 原生的控制平面来配置和与其交互。如果你想直接在 Kubernetes 上使用 Envoy,推荐使用其他开源项目,如 Ambassador 和 Gloo,作为入口控制器和/或 API 网关。
Linkerd 2
Linkerd 2 (linkerd.io) 是一个专为 Kubernetes 设计的服务,同时也是 CNCF 孵化项目。它由 Buoyant (buoyant.io) 开发。Buoyant 是“服务网格”这个术语的创始人,并将其引入了全球。他们从一个基于 Scala 的多平台服务网格 Linkerd 开始,其中包括 Kubernetes。但他们决定开发一个更好、更高效的服务网格,专门针对 Kubernetes。这就是 Linkerd 2 的由来,它是专为 Kubernetes 设计的。他们使用 Rust 实现了数据平面(代理层),并使用 Go 实现了控制平面。
Kuma
Kuma (kuma.io/) 是一个由 Envoy 提供支持的开源服务网格。它最初由 Kong 开发,Kong 在 Kuma 的基础上提供了一个名为 Kong Mesh 的企业产品。它不仅可以在 Kubernetes 上运行,也可以在其他环境中使用。它的特点是配置非常简单,并且允许在同一个网格中混合使用 Kubernetes 和基于虚拟机的系统。
AWS App Mesh
当然,AWS 也有自己的专有服务网格——AWS App Mesh (aws.amazon.com/app-mesh)。App Mesh 同样使用 Envoy 作为其数据平面。它可以运行在 EC2、Fargate、ECS 和 EKS 以及普通 Kubernetes 上。App Mesh 在服务网格领域稍显迟到,因此还没有其他一些服务网格那么成熟,但它正在赶上。它基于强大的 Envoy,并且由于与 AWS 服务的紧密集成,可能是最佳选择。
Mæsh
Mæsh (mae.sh) 由 Træfik (containo.us/traefik) 的开发者开发。它的有趣之处在于,它采用了节点代理方式,而不是 Sidecar 容器。它在很大程度上依赖于 Traefik 中间件来实现服务网格功能。你可以通过在服务上使用注解来配置它。如果你在集群边缘使用 Traefik,这可能是尝试服务网格的一种有趣且轻量级的方法。
Istio
Istio (istio.io/) 是 Kubernetes 上最著名的服务网格。它建立在 Envoy 之上,并允许你通过 YAML 清单以 Kubernetes 原生方式进行配置。Istio 由 Google、IBM 和 Lyft(Envoy 开发者)发起。它在 Google GKE 上可以一键安装,但在 Kubernetes 社区的任何环境中都有广泛应用。它也是 Knative 的默认入口/API 网关解决方案,这进一步促进了它的采用。
OSM (Open Service Mesh)
OSM (openservicemesh.io) 是另一个基于 Envoy 的服务网格。它可以通过 SMI (Service Mesh Interface 服务网格接口) 进行配置,SMI 是一个旨在提供与提供者无关的 API 集合的规范,用于配置服务网格。详情请参见 smi-spec.io。OSM 和 SMI 都是 CNCF 沙盒项目。
OSM 是由微软开发并贡献给 CNCF 的。
Cilium 服务网格
Cilium 服务网格 (isovalent.com/blog/post/cilium-service-mesh) 是服务网格领域的新兴者。它由 Isovalent (isovalent.com) 开发。它的特点是尝试将 eBPF 的好处引入服务网格,并采用无边车的方式。虽然它仍处于早期阶段,并且不如其他服务网格成熟,但它有一个有趣的概念,即允许你使用自有的控制平面。它可以与 Istio 集成,并与边车进行互操作。值得关注。
在讨论了各种服务网格选择后,让我们来试试 Istio。我们选择 Istio 的原因是它是最成熟的服务网格之一,拥有一个庞大的社区、众多用户,并且得到了行业领导者的支持。
了解 Istio 架构
在本节中,我们将更深入地了解 Istio。
首先,让我们认识一下 Istio 的主要组件,了解它们的功能以及它们之间的关系。
Istio 是一个大型框架,提供了很多功能,并且包含多个相互交互的部分,这些部分与 Kubernetes 组件(大多是间接和不显眼地)交互。它被分为控制平面和数据平面。数据平面是一组代理(每个 pod 一个)。它们的控制平面是一组负责配置代理和收集遥测数据的组件。
以下图表展示了 Istio 的不同部分,它们之间的关系以及它们之间交换的信息。

图 14.5:Istio 架构
如你所见,主要有两个组件:Envoy 代理,它是附加到每个服务实例(每个 pod)的边车容器,以及 istiod,它负责服务发现、配置和证书管理。Istiod 是一个单一的二进制文件,实际上包含了多个组件:Pilot、Citadel 和 Galley。这些组件曾经是独立的二进制文件,在 Istio 1.5 中被合并为一个二进制文件,以简化安装、运行和升级 Istio 的体验。
让我们深入了解一下每个组件,从 Envoy 代理开始。
Envoy
当我们回顾 Kubernetes 的服务网格时,我们简要讨论了 Envoy。在这里,它作为 Istio 的数据平面。Envoy 是用 C++ 实现的,是一个高性能的代理。对于服务网格中的每个 pod,Istio 会注入(通过自动或通过 istioctl CLI)一个 Envoy 边车容器,负责处理繁重的工作。
以下是 Envoy 执行的一些任务:
-
在 pods 之间代理 HTTP、HTTP/2 和 gRPC 流量
-
精密的负载均衡
-
mTLS 终止
-
HTTP/2 和 gRPC 代理
-
提供服务健康状况
-
对于不健康服务的断路器
-
基于百分比的流量整形
-
注入故障以进行测试
-
详细的度量指标
Envoy 代理控制所有进出其 pod 的通信。它是目前 Istio 中最重要的组件。Envoy 的配置并非简单,这也是 Istio 控制平面所处理的主要内容之一。
下一个组件是 Pilot。
Pilot
Pilot 负责平台无关的服务发现、动态负载均衡和路由。它将高级路由规则转换为 Envoy 配置。这一抽象层使得 Istio 可以在多个编排平台上运行。Pilot 获取所有平台特定的信息,将其转换为 Envoy 数据平面配置格式,并通过 Envoy 数据平面 API 将其传播到每个 Envoy 代理。Pilot 是无状态的;在 Kubernetes 中,所有配置都作为自定义资源定义(CRDs)存储在 etcd 中。
下一个组件是 Citadel。
Citadel
Citadel 负责证书和密钥管理,是 Istio 安全的核心部分。Citadel 集成了多个平台,并与它们的身份机制保持一致。例如,在 Kubernetes 中,它使用服务账户;在 AWS 中,它使用 AWS IAM;在 Azure 中,它使用 AAD;在 GCP/GKE 中,它可以使用 GCP IAM。Istio PKI 基于 Citadel,它使用 SPIFEE 格式的 X.509 证书作为服务身份的载体。
这是 Kubernetes 中强身份到 envoy 代理的工作流程:
-
Citadel 为现有服务账户创建证书和密钥对。
-
Citadel 监视 Kubernetes API 服务器,查看是否有新的服务账户需要配置证书和密钥对。
-
Citadel 将证书和密钥作为 Kubernetes 秘密存储。
-
Kubernetes 将秘密挂载到与服务账户关联的每个新 pod 中(这是标准的 Kubernetes 做法)。
-
Citadel 在证书过期时自动轮换 Kubernetes 秘密。
-
Pilot 生成安全的命名信息,将服务账户与 Istio 服务关联。然后,Pilot 将安全的命名信息传递给 Envoy 代理。
我们将介绍的最后一个主要组件是 Galley。
Galley
Galley 负责抽象不同平台上的用户配置。它将处理过的配置提供给 Pilot。这是一个相当简单的组件。
现在我们已经将 Istio 拆解成其主要组件,让我们动手操作 Istio,并将其集成到 Kubernetes 集群中。
将 Istio 集成到你的 Kubernetes 集群中
在本节中,我们将安装 Istio 到一个全新的集群,并探索它提供的所有服务功能。
为 Istio 准备 minikube 集群
我们将使用 minikube 集群来检查 Istio。在安装 Istio 之前,我们应该确保我们的集群有足够的容量来处理 Istio 及其示例应用程序 BookInfo。我们将以 16 GB 内存和四个 CPU 启动 minikube,这应该是足够的。确保你正在使用的 Docker 虚拟机(例如,Rancher Desktop)有足够的 CPU 和内存:
$ minikube start --memory=16384 --cpus=4
Minikube 可以为 Istio 提供负载均衡器。让我们在一个单独的终端中运行这个命令,因为它会阻塞(在完成之前不要停止隧道):
$ minikube tunnel
 Tunnel successfully started
 NOTE: Please do not close this terminal as this process must stay alive for the tunnel to be accessible ...
有时 Minikube 没有清理隧道网络,所以在停止集群后,建议运行以下命令:
minikube tunnel --cleanup
安装 Istio
在 Minikube 启动并运行后,我们可以安装 Istio 本身。有多种方式可以安装 Istio:
-
使用 istioctl(Istio CLI)进行自定义安装
-
使用 Istio 运维器通过 Helm 进行自定义安装(支持,但不推荐)
-
多集群安装
-
外部控制平面
-
虚拟机安装
我们将选择推荐的 istioctl 选项。Istio 版本可能高于 1.15:
$ curl -L https://istio.io/downloadIstio | sh -
istioctl 工具位于istio-1.15.2/bin(下载时版本可能不同)。确保它在你的路径中。Kubernetes 安装清单位于istio-1.15.2/install/kubernetes,示例位于istio-1.15.2/samples。
让我们先运行一些安装前的检查:
$ istioctl x precheck
 No issues found when checking the cluster. Istio is safe to install or upgrade!
To get started, check out https://istio.io/latest/docs/setup/getting-started/
我们将安装内置的 demo 配置文件,这对于评估 Istio 非常有用:
$ istioctl install --set profile=demo -y
 Istio core installed
 Istiod installed
 Egress gateways installed
 Ingress gateways installed
 Installation complete
Making this installation the default for injection and validation.
Thank you for installing Istio 1.15
让我们还安装一些可观察性插件,如prometheus、grafana、jaeger和kiali:
$ k apply -f https://raw.githubusercontent.com/istio/istio/release-1.15/samples/addons/prometheus.yaml
serviceaccount/prometheus created
configmap/prometheus created
clusterrole.rbac.authorization.k8s.io/prometheus created
clusterrolebinding.rbac.authorization.k8s.io/prometheus created
service/prometheus created
deployment.apps/prometheus created
$ kapply -f https://raw.githubusercontent.com/istio/istio/release-1.15/samples/addons/grafana.yaml
serviceaccount/grafana created
configmap/grafana created
service/grafana created
deployment.apps/grafana created
configmap/istio-grafana-dashboards created
configmap/istio-services-grafana-dashboards created
$ k apply -f https://raw.githubusercontent.com/istio/istio/release-1.15/samples/addons/jaeger.yaml
deployment.apps/jaeger created
service/tracing created
service/zipkin created
service/jaeger-collector created
$ k apply -f https://raw.githubusercontent.com/istio/istio/release-1.15/samples/addons/kiali.yaml
serviceaccount/kiali created
configmap/kiali created
clusterrole.rbac.authorization.k8s.io/kiali-viewer created
clusterrole.rbac.authorization.k8s.io/kiali created
clusterrolebinding.rbac.authorization.k8s.io/kiali created
role.rbac.authorization.k8s.io/kiali-controlplane created
rolebinding.rbac.authorization.k8s.io/kiali-controlplane created
service/kiali created
deployment.apps/kiali created
让我们检查一下集群,看看实际安装了哪些内容。Istio 将自己安装在istio-system命名空间中,这非常方便,因为它安装了很多内容。让我们看看 Istio 安装了哪些服务:
$ k get svc -n istio-system -o name
service/grafana
service/istio-egressgateway
service/istio-ingressgateway
service/istiod
service/jaeger-collector
service/kiali
service/prometheus
service/tracing
service/zipkin
有很多以istio-为前缀的服务,后面跟着其他一些服务:
-
Prometheus
-
Grafana
-
Jaeger
-
Zipkin
-
跟踪
-
Kiali
好的,我们已经成功安装了 Istio 和各种集成。接下来,让我们在集群中安装 BookInfo 应用程序,这是 Istio 的示例应用程序。
安装 BookInfo
BookInfo 是一个简单的基于微服务的应用程序,正如其名字所示,它展示了一本书的基本信息,如名称、描述、ISBN,甚至是评论。BookInfo 的开发者真正拥抱了多语言编程的理念,每个微服务都是用不同的编程语言实现的:
-
用 Python 编写的 ProductPage 服务
-
用 Java 编写的 Reviews 服务
-
用 Ruby 编写的 Details 服务
-
用 JavaScript(Node.js)编写的 Ratings 服务
以下图表描述了 BookInfo 服务之间的关系和信息流动:

图 14.6:BookInfo 服务之间信息流动的示意图
我们将把它安装在自己的bookinfo命名空间中。让我们先创建该命名空间,然后通过向命名空间添加标签来启用 Istio 自动注入边车代理:
$ k create ns bookinfo
namespace/bookinfo created
$ k label namespace bookinfo istio-injection=enabled
namespace/bookinfo labeled
安装应用程序本身是一个简单的一行命令:
$ k apply -f https://raw.githubusercontent.com/istio/istio/release-1.15/samples/bookinfo/platform/kube/bookinfo.yaml -n bookinfo
service/details created
serviceaccount/bookinfo-details created
deployment.apps/details-v1 created
service/ratings created
serviceaccount/bookinfo-ratings created
deployment.apps/ratings-v1 created
service/reviews created
serviceaccount/bookinfo-reviews created
deployment.apps/reviews-v1 created
deployment.apps/reviews-v2 created
deployment.apps/reviews-v3 created
service/productpage created
serviceaccount/bookinfo-productpage created
deployment.apps/productpage-v1 created
好的,应用已经成功部署,包括为每个服务创建了单独的服务账户。正如你所看到的,已经部署了三个版本的 reviews 服务。稍后在我们进行升级、高级路由和部署模式时,这将非常有用。
在继续之前,我们仍然需要等待所有 pod 初始化完成,然后 Istio 将注入它的 sidecar 代理容器。当一切就绪时,你应该看到类似这样的内容:
$ k get po -n bookinfo
NAME READY STATUS RESTARTS AGE
details-v1-5ffd6b64f7-c62l6 2/2 Running 0 3m48s
productpage-v1-979d4d9fc-7hzkj 2/2 Running 0 3m48s
ratings-v1-5f9699cfdf-mns6n 2/2 Running 0 3m48s
reviews-v1-569db879f5-jmfrj 2/2 Running 0 3m48s
reviews-v2-65c4dc6fdc-cc8nn 2/2 Running 0 3m48s
reviews-v3-c9c4fb987-bpk9f 2/2 Running 0 3m48s
请注意,在 READY 列下,每个 pod 显示为 2/2,这意味着每个 pod 中有两个容器。一个是应用程序容器,另一个是注入的代理。
由于我们将在 bookinfo 命名空间中操作,让我们定义一个小别名,这样会让我们的操作更简单:
$ alias kb='kubectl -n bookinfo'
现在,凭借我们的小kb别名,我们可以验证是否可以从评分服务获取产品页面:
$ kb exec -it $(kb get pod -l app=ratings -o jsonpath='{.items[0].metadata.name}') -c ratings -- curl productpage:9080/productpage | grep -o "<title>.*</title>"
<title>Simple Bookstore App</title>
但应用程序目前还不能从外部访问。这时,Istio 网关就派上用场了。让我们来部署它:
$ kb apply -f https://raw.githubusercontent.com/istio/istio/release-1.15/samples/bookinfo/networking/bookinfo-gateway.yaml
gateway.networking.istio.io/bookinfo-gateway created
virtualservice.networking.istio.io/bookinfo created
让我们获取从外部访问应用程序的 URL:
$ export INGRESS_HOST=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
$ export INGRESS_PORT=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.spec.ports[?(@.name=="http2")].port}')
$ export SECURE_INGRESS_PORT=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.spec.ports[?(@.name=="https")].port}')
$ export GATEWAY_URL=${INGRESS_HOST}:${INGRESS_PORT}
现在我们可以从外部尝试访问:
$ http http://${GATEWAY_URL}/productpage | grep -o "<title>.*</title>"
<title>Simple Bookstore App</title>
你也可以在浏览器中打开 URL,查看一些关于莎士比亚《错误的喜剧》的信息:

图 14.7:BookInfo 评论示例
好的,我们已经准备好开始探索 Istio 带来的功能。
与 Istio 配合使用
在本节中,我们将使用 Istio 资源和策略,并利用它们来改善 BookInfo 应用程序的操作。
让我们从流量管理开始。
流量管理
Istio 流量管理是根据你定义的目标规则将流量路由到你的服务。Istio 为你的所有服务及其端点保持服务注册表。基本流量管理允许服务之间的流量,并在每个服务实例之间进行简单的轮询负载均衡。但 Istio 能做更多。Istio 的流量管理 API 包含五个资源:
-
虚拟服务
-
目标规则
-
网关
-
服务条目
-
Sidecar 容器
让我们首先为 BookInfo 应用默认的目标规则:
$ kb apply -f https://raw.githubusercontent.com/istio/istio/release-1.15/samples/bookinfo/networking/destination-rule-all.yaml
destinationrule.networking.istio.io/productpage created
destinationrule.networking.istio.io/reviews created
destinationrule.networking.istio.io/ratings created
destinationrule.networking.istio.io/details created
然后,让我们创建表示网格中服务的 Istio 虚拟服务:
$ kb apply -f https://raw.githubusercontent.com/istio/istio/release-1.15/samples/bookinfo/networking/virtual-service-all-v1.yaml
virtualservice.networking.istio.io/productpage created
virtualservice.networking.istio.io/reviews created
virtualservice.networking.istio.io/ratings created
virtualservice.networking.istio.io/details created
我们需要等一会儿,直到虚拟服务配置传播完成。然后,让我们使用 neat 的 kubectl 插件查看产品页面虚拟服务。如果你还没有安装它,请按照 github.com/itaysk/kubectl-neat 上的说明操作。
$ kb get virtualservices productpage -o yaml | k neat
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: productpage
namespace: default
spec:
hosts:
- productpage
http:
- route:
- destination:
host: productpage
subset: v1
这非常简单,指定 HTTP 路径和版本。v1 子集对于评论服务非常重要,因为评论服务有多个版本。产品页面服务将访问其 v1 版本,因为该子集已被配置。
让我们做得更有趣一些,根据登录用户进行路由。Istio 本身没有用户身份的概念,但它是根据请求头来路由流量的。BookInfo 应用程序为所有请求添加了一个最终用户头部。
以下命令将更新路由规则:
$ kb apply -f https://raw.githubusercontent.com/istio/istio/release-1.15/samples/bookinfo/networking/virtual-service-reviews-test-v2.yaml
virtualservice.networking.istio.io/reviews configured
让我们检查一下新的规则:
$ kb get virtualservice reviews -o yaml | k neat
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: reviews
namespace: default
spec:
hosts:
- reviews
http:
- match:
- headers:
end-user:
exact: jason
route:
- destination:
host: reviews
subset: v2
- route:
- destination:
host: reviews
subset: v1
如你所见,如果 HTTP 头部的 end-user 与 jason 匹配,则请求将路由到评论服务的子集 2,否则路由到子集 1。评论服务的版本 2 在页面的评论部分添加了星级评分。为了测试它,我们可以以用户 jason 登录(密码可以任意),刷新浏览器,并看到评论旁边有星级评分:

图 14.8:带星级评分的示例 BookInfo 评论
Istio 在流量管理领域可以做更多的事情:
-
测试目的的故障注入
-
HTTP 和 TCP 流量转移(逐步将流量从一个版本转移到下一个版本)
-
请求超时
-
电路断路
-
流量镜像
除了内部流量管理,Istio 还支持配置进入集群和从集群退出的流量,包括使用 TLS 和互斥 TLS 的安全选项。
安全性
安全性是 Istio 的核心组成部分。它提供身份管理、身份验证和授权、安全策略以及加密。安全支持分布在多个层级,采用多个行业标准协议和最佳实践安全原则,如深度防御、默认安全和零信任。
以下是 Istio 安全架构的全貌:

图 14.9:Istio 安全架构
Istio 通过以下功能实现强大的安全性:
-
Sidecar 和边界代理实现了客户端和服务器之间经过认证和授权的通信
-
控制平面管理密钥和证书
-
控制平面将安全策略和安全命名信息分发给代理
-
控制平面管理审计
让我们逐步解析。
Istio 身份
Istio 利用安全命名,将通过服务发现机制(例如 DNS)定义的服务名称映射到基于证书的服务器身份。客户端验证服务器身份。服务器可以配置为验证客户端的身份。所有的安全策略都适用于给定的身份。服务器根据客户端的身份决定其访问权限。
Istio 身份模型可以利用其运行平台上现有的身份基础设施。在 Kubernetes 上,当然是使用 Kubernetes 服务账户。
Istio 安全地为每个工作负载分配一个 x.509 证书,代理与 Envoy 代理一起运行。该代理与 istiod 协作,自动配置和轮换证书和私钥。
Istio 证书管理
以下是证书和密钥配置的工作流程:
-
istiod 提供一个 gRPC 服务,监听 证书签名请求 (CSRs)。
-
该过程从 Istio 代理开始,启动时生成一个私钥和一个证书签名请求(CSR)。然后,它将 CSR 与其自身的凭证一起传输给 istiod 的 CSR 服务。
-
此时,istiod 证书授权机构(CA)检查 CSR 中包含的代理凭据。如果凭据有效,istiod CA 将继续签署 CSR,从而创建证书。
-
当工作负载启动时,位于同一容器内的 Envoy 代理利用 Envoy SDS(秘密发现服务)API,从 Istio 代理请求证书和相应的密钥。
-
Istio 代理会主动监控工作负载证书的过期情况,启动定期刷新证书和密钥的过程,确保它们保持最新。
Istio 身份验证
安全身份模型是 Istio 身份验证框架的基础。Istio 支持两种身份验证模式:对等身份验证和请求身份验证。
对等身份验证
对等身份验证用于服务间身份验证。它的一个亮点是,Istio 无需修改代码即可提供此功能。它确保只有在您配置了身份验证策略的服务之间,服务间的通信才会发生。
这是对 reviews 服务的身份验证策略,要求使用双向 TLS:
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: "example-peer-policy"
namespace: "foo"
spec:
selector:
matchLabels:
app: reviews
mtls:
mode: STRICT
请求身份验证
请求身份验证用于终端用户的身份验证。Istio 将验证发起请求的终端用户是否被允许进行该请求。这种请求级别的身份验证使用JWT(JSON Web Token)并支持许多 OpenID Connect 后端。
一旦呼叫者的身份被确定,身份验证框架将其和其他声明传递到链中的下一个环节——授权框架。
Istio 授权
Istio 可以在多个层级上授权请求:
-
整个网格
-
整个命名空间
-
工作负载级别
这是 Istio 的授权架构:

图 14.10:Istio 授权架构
授权基于授权策略。每个策略都有一个选择器(适用的工作负载)和规则(谁被允许访问某个资源以及在什么条件下)。
如果在工作负载上没有定义策略,则允许所有请求。但是,如果为工作负载定义了策略,则只有符合策略中规则的请求才被允许。您还可以定义排除规则。
这是一个授权策略,允许两个来源(服务账户cluster.local/ns/default/sa/sleep和命名空间dev)在请求携带有效的 JWT 令牌时,访问具有标签app: httpbin和version: v1的工作负载,且该请求来自命名空间foo。
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: httpbin
namespace: foo
spec:
selector:
matchLabels:
app: httpbin
version: v1
action: ALLOW
rules:
- from:
- source:
principals: ["cluster.local/ns/default/sa/sleep"]
- source:
namespaces: ["dev"]
to:
- operation:
methods: ["GET"]
when:
- key: request.auth.claims[iss]
values: ["https://accounts.google.com"]
授权粒度不必限制在工作负载级别。我们也可以限制对特定端点和方法的访问。除了精确匹配外,我们还可以使用前缀匹配、后缀匹配或存在匹配来指定操作。例如,以下策略允许访问所有以/test/开头和以/info结尾的路径。允许的请求方法只有GET和HEAD:
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: tester
namespace: default
spec:
selector:
matchLabels:
app: products
rules:
- to:
- operation:
paths: ["/test/*", "*/info"]
methods: ["GET", "HEAD"]
如果我们想做得更复杂一点,可以指定条件。例如,我们可以仅允许带有特定头部的请求。这里有一个要求版本头部的策略,值为 v1 或 v2:
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: httpbin
namespace: foo
spec:
selector:
matchLabels:
app: httpbin
version: v1
rules:
- from:
- source:
principals: ["cluster.local/ns/default/sa/sleep"]
to:
- operation:
methods: ["GET"]
when:
- key: request.headers[version]
values: ["v1", "v2"]
对于 TCP 服务,操作的 paths 和 methods 字段不适用。Istio 会忽略它们。但是,我们可以为特定端口指定策略:
apiVersion: "security.istio.io/v1beta1"
kind: AuthorizationPolicy
metadata:
name: mongodb-policy
namespace: default
spec:
selector:
matchLabels:
app: mongodb
rules:
- from:
- source:
principals: ["cluster.local/ns/default/sa/bookinfo-ratings-v2"]
to:
- operation:
ports: ["27017"]
让我们看看 Istio 提供巨大价值的一个领域——遥测。
监控与可观测性
为应用程序添加遥测监控是一项吃力不讨好的工作。工作量巨大。你需要记录日志、收集指标,并创建用于追踪的跨度。全面的可观测性对于故障排除和缓解事件至关重要,但这远非易事:
-
一开始做这件事需要时间和精力
-
确保它在集群中所有服务间保持一致需要更多的时间和精力
-
你可能会错过一个重要的监控点,或者配置不正确
-
如果你想更换日志提供者或分布式追踪解决方案,可能需要修改所有服务
-
它会使你的代码充满杂乱的内容,从而掩盖业务逻辑
-
你可能需要显式地关闭它用于测试
如果这一切都能自动处理,并且不需要任何代码更改,那会怎么样?这就是服务网格遥测的承诺。当然,你可能需要在应用程序/服务层做一些工作,尤其是当你想捕获自定义指标或进行特定日志记录时。如果你的系统被划分为沿着真正代表你的领域和工作流的边界的连贯微服务,那么 Istio 可以帮助你从一开始就获得不错的监控工具。其核心思想是,Istio 可以跟踪服务间的连接点发生了什么。
Istio 访问日志
我们可以捕获 Envoy 代理的访问日志,从每个工作负载的角度展示网络流量的情况。
在本节中,我们将使用两个新的工作负载:sleep 和 httpbin。让我们部署它们:
$ kb apply -f https://raw.githubusercontent.com/istio/istio/release-1.15/samples/sleep/sleep.yaml
serviceaccount/sleep created
service/sleep created
deployment.apps/sleep created
$ kb apply -f https://raw.githubusercontent.com/istio/istio/release-1.15/samples/httpbin/httpbin.yaml
serviceaccount/httpbin created
service/httpbin created
deployment.apps/httpbin created
此外,让我们将 OpenTelemetry 收集器部署到 istio-system 命名空间:
$ k apply -f https://raw.githubusercontent.com/istio/istio/release-1.15/samples/open-telemetry/otel.yaml -n istio-system
configmap/opentelemetry-collector-conf created
service/opentelemetry-collector created
deployment.apps/opentelemetry-collector created
Istio 在 Istio ConfigMap 中配置了提供者和更多内容,ConfigMap 中已经包含了 opentelemetry-collector 服务的提供者条目。让我们使用 yq (github.com/mikefarah/yq) 来查看 ConfigMap 的数据字段:
$ k get cm istio -n istio-system -o yaml | yq .data
mesh: |-
accessLogFile: /dev/stdout
defaultConfig:
discoveryAddress: istiod.istio-system.svc:15012
proxyMetadata: {}
tracing:
zipkin:
address: zipkin.istio-system:9411
enablePrometheusMerge: true
extensionProviders:
- envoyOtelAls:
port: 4317
service: opentelemetry-collector.istio-system.svc.cluster.local
name: otel
rootNamespace: istio-system
trustDomain: cluster.local
meshNetworks: 'networks: {}'
要启用从 sleep 工作负载到 otel 收集器的日志记录,我们需要配置一个 Telemetry 资源:
$ cat <<EOF | kb apply -f -
apiVersion: telemetry.istio.io/v1alpha1
kind: Telemetry
metadata:
name: sleep-logging
spec:
selector:
matchLabels:
app: sleep
accessLogging:
- providers:
- name: otel
EOF
telemetry.telemetry.istio.io/sleep-logging created
默认的访问日志格式是:
[%START_TIME%] \"%REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)% %PROTOCOL%\" %RESPONSE_CODE% %RESPONSE_FLAGS% %RESPONSE_CODE_DETAILS% %CONNECTION_TERMINATION_DETAILS%
\"%UPSTREAM_TRANSPORT_FAILURE_REASON%\" %BYTES_RECEIVED% %BYTES_SENT% %DURATION% %RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)% \"%REQ(X-FORWARDED-FOR)%\" \"%REQ(USER-AGENT)%\" \"%REQ(X-REQUEST-ID)%\"
\"%REQ(:AUTHORITY)%\" \"%UPSTREAM_HOST%\" %UPSTREAM_CLUSTER% %UPSTREAM_LOCAL_ADDRESS% %DOWNSTREAM_LOCAL_ADDRESS% %DOWNSTREAM_REMOTE_ADDRESS% %REQUESTED_SERVER_NAME% %ROUTE_NAME%\n
这很冗长,但在调试或故障排除时,你希望获得尽可能多的信息。如果你想更改日志格式,它是可配置的。
好的,让我们试试。sleep工作负载实际上只是一个 pod,我们可以从中向 httpbin 应用程序发起网络请求。httpbin 服务运行在8000端口,并在集群内被称为httpbin。我们将从sleep pod 查询httpbin,了解著名的 418 HTTP 状态码 (developer.mozilla.org/en-US/docs/Web/HTTP/Status/418):
$ kb exec deploy/sleep -c sleep -- curl -sS -v httpbin:8000/status/418
* Trying 10.101.189.162:8000...
* Connected to httpbin (10.101.189.162) port 8000 (#0)
> GET /status/418 HTTP/1.1
> Host: httpbin:8000
> User-Agent: curl/7.86.0-DEV
> Accept: */*
>
-=[ teapot ]=-
_...._
.' _ _ `.
| ."` ^ `". _,
\_;`"---"`|//
| ;/
\_ _/
`"""`
* Mark bundle as not supporting multiuse
< HTTP/1.1 418 Unknown
< server: envoy
< date: Sat, 29 Oct 2022 04:35:07 GMT
< x-more-info: http://tools.ietf.org/html/rfc2324
< access-control-allow-origin: *
< access-control-allow-credentials: true
< content-length: 135
< x-envoy-upstream-service-time: 61
<
{ [135 bytes data]
* Connection #0 to host httpbin left intact
耶,我们得到了预期的茶壶响应。现在,让我们检查访问日志:
$ k logs -l app=opentelemetry-collector -n istio-system
LogRecord #0
ObservedTimestamp: 1970-01-01 00:00:00 +0000 UTC
Timestamp: 2022-10-29 04:35:07.599108 +0000 UTC
Severity:
Body: [2022-10-29T04:35:07.599Z] "GET /status/418 HTTP/1.1" 418 - via_upstream - "-" 0 135 63 61 "-" "curl/7.86.0-DEV" "d36495d6-642a-9790-9b9a-d10b2af096f5" "httpbin:8000" "172.17.0.17:80" outbound|8000||httpbin.bookinfo.svc.cluster.local 172.17.0.16:33876 10.101.189.162:8000 172.17.0.16:45986 - default
Trace ID:
Span ID:
Flags: 0
如你所见,我们根据默认访问日志格式获得了大量信息,包括时间戳、请求 URL、响应状态、用户代理以及源和目标的 IP 地址。
在生产系统中,你可能希望将收集器的日志转发到集中式日志系统。让我们看看 Istio 在度量方面提供了什么。
度量
Istio 收集三种类型的度量:
-
代理度量
-
控制面度量
-
服务度量
收集的度量涵盖了所有进出服务网格的流量。作为操作员,我们需要为度量收集配置 Istio。我们之前安装了 Prometheus 和 Grafana 来进行度量收集和可视化后端。Istio 遵循四个黄金信号原则,记录延迟、流量、错误和饱和度。
让我们看一个代理级别(Envoy)度量的示例:
envoy_cluster_internal_upstream_rq{response_code_class="2xx",cluster_name="xds-grpc"} 7163
envoy_cluster_upstream_rq_completed{cluster_name="xds-grpc"} 7164
envoy_cluster_ssl_connection_error{cluster_name="xds-grpc"} 0
envoy_cluster_lb_subsets_removed{cluster_name="xds-grpc"} 0
envoy_cluster_internal_upstream_rq{response_code="503",cluster_name="xds-grpc"} 1
这是一个服务级别度量的示例:
istio_requests_total{
connection_security_policy="mutual_tls",
destination_app="details",
destination_principal="cluster.local/ns/default/sa/default",
destination_service="details.default.svc.cluster.local",
destination_service_name="details",
destination_service_namespace="default",
destination_version="v1",
destination_workload="details-v1",
destination_workload_namespace="default",
reporter="destination",
request_protocol="http",
response_code="200",
response_flags="-",
source_app="productpage",
source_principal="cluster.local/ns/default/sa/default",
source_version="v1",
source_workload="productpage-v1",
source_workload_namespace="default"
} 214
我们还可以收集 TCP 服务的度量。让我们安装使用 MongoDB(一个 TCP 服务)的 v2 版本的评分服务:
$ kb apply -f https://raw.githubusercontent.com/istio/istio/release-1.15/samples/bookinfo/platform/kube/bookinfo-ratings-v2.yaml
serviceaccount/bookinfo-ratings-v2 created
deployment.apps/ratings-v2 created
接下来,我们安装 MongoDB 本身:
$ kb apply -f https://raw.githubusercontent.com/istio/istio/release-1.15/samples/bookinfo/platform/kube/bookinfo-db.yaml
service/mongodb created
deployment.apps/mongodb-v1 created
最后,我们需要为评论和评分服务创建虚拟服务:
$ kb apply -f https://raw.githubusercontent.com/istio/istio/release-1.15/samples/bookinfo/networking/virtual-service-ratings-db.yaml
virtualservice.networking.istio.io/reviews configured
virtualservice.networking.istio.io/ratings configured
让我们访问产品页面以生成流量:
$ http http://${GATEWAY_URL}/productpage | grep -o "<title>.*</title>"
<title>Simple Bookstore App</title>
此时,我们可以直接暴露 Prometheus:
$ k -n istio-system port-forward deploy/prometheus 9090:9090
Forwarding from 127.0.0.1:9090 -> 9090
Forwarding from [::1]:9090 -> 9090
或者,你也可以使用istioctl dashboard prometheus,它不仅会进行端口转发,还会在转发后的http://localhost:9090/网址自动启动浏览器。
我们可以查看从 Istio 服务、Istio 控制面以及特别是 Envoy 提供的大量新度量。这里是一些可用度量的非常小的子集:

图 14.11:可用的 Istio 度量
可观察性的最后一个支柱是分布式追踪。
分布式追踪
Istio 配置 Envoy 代理以生成与其相关联服务的追踪跨度。服务本身负责转发请求上下文。Istio 可以与多种追踪后端一起工作,例如:
-
Jaeger
-
Zipkin
-
LightStep
-
DataDog
以下是服务应传播的请求头(根据追踪后端的不同,每个请求可能仅包含其中的一部分):
x-request-id
x-b3-traceid
x-b3-spanid
x-b3-parentspanid
x-b3-sampled
x-b3-flags
x-ot-span-context
x-cloud-trace-context
traceparent
grpc-trace-bin
追踪的采样率由网格配置控制。默认值是 1%。让我们将其更改为 100%,用于演示目的:
$ cat <<'EOF' > ./tracing.yaml
apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
spec:
meshConfig:
enableTracing: true
defaultConfig:
tracing:
sampling: 100
EOF
$ istioctl install -f ./tracing.yaml
让我们验证采样率已更新为100:
$ k get cm -n istio-system istio -o yaml | yq .data
mesh: |-
defaultConfig:
discoveryAddress: istiod.istio-system.svc:15012
proxyMetadata: {}
tracing:
sampling: 100
zipkin:
address: zipkin.istio-system:9411
enablePrometheusMerge: true
enableTracing: true
rootNamespace: istio-system
trustDomain: cluster.local
meshNetworks: 'networks: {}'
让我们多次访问产品页面:
$ http http://${GATEWAY_URL}/productpage | grep -o "<title>.*</title>"
<title>Simple Bookstore App</title>
$ http http://${GATEWAY_URL}/productpage | grep -o "<title>.*</title>"
<title>Simple Bookstore App</title>
$ http http://${GATEWAY_URL}/productpage | grep -o "<title>.*</title>"
<title>Simple Bookstore App</title>
现在,我们可以启动 Jaeger UI 并探索追踪:
$ istioctl dashboard jaeger
http://localhost:52466
Handling connection for 9090
你的浏览器会自动打开,你应该能看到熟悉的 Jaeger 仪表板,在那里你可以选择一个服务并搜索跟踪记录:

图 14.12:Jaeger 仪表板
你可以点击一个跟踪记录,查看请求在系统中流动的详细视图:

图 14.13:请求在系统中的流动
让我们来看看一个专门的服务网格可视化工具。
使用 Kiali 可视化你的服务网格
Kiali 是一个开源项目,它将 Prometheus、Grafana 和 Jaeger 集成在一起,为你的 Istio 服务网格提供一个可观察性控制台。它可以回答以下问题:
-
哪些微服务参与了 Istio 服务网格?
-
这些微服务是如何连接的?
-
这些微服务的表现如何?
它有多种视图,真正允许你通过缩放、过滤和选择显示的各种属性来切分和分析你的服务网格。它有几个视图,你可以在它们之间切换。
你可以像这样启动它:
$ istioctl dashboard kiali
这里是概览页面:

图 14.14:Kiali 概览页面
但是,最有趣的视图是图形视图,它可以展示你的服务及其相互关系,并完全了解不同工作负载之间流动的版本和请求,包括请求的百分比和延迟。它可以显示 HTTP 和 TCP 服务,并且真正提供了一个关于你的服务网格如何运作的清晰图像。

图 14.15:Kiali 图形视图
我们已经涵盖了 Istio 的监控和可观察性,包括日志、度量和分布式追踪,并展示了如何使用 Kiali 可视化你的服务网格。
总结
在本章中,我们对 Kubernetes 上的服务网格进行了非常全面的研究。服务网格将持续存在,它们是操作复杂分布式系统的正确方式。将所有操作关注点从代理中分离出来,并让服务网格来控制它们,这是一种范式转变。当然,Kubernetes 主要是为复杂的分布式系统而设计的,所以服务网格的价值立即显现出来。看到 Kubernetes 上有许多服务网格的选择也令人高兴。尽管大多数服务网格并非特定于 Kubernetes,但它是最重要的部署平台之一。此外,我们还对 Istio 进行了详细的回顾——毫无疑问,它是最具动力的服务网格,并对其进行了全面的演示。我们展示了服务网格的许多好处,以及它们如何与其他系统集成。你应该能够评估服务网格对你的系统有多大用处,以及是否应该部署一个并开始享受其带来的好处。
在下一章中,我们将讨论多种方法来扩展 Kubernetes,并利用其模块化和灵活的设计。这是 Kubernetes 的标志性特点之一,也是它为何能如此迅速被众多社区采纳的原因。
第十五章:扩展 Kubernetes
在本章中,我们将深入探索 Kubernetes 的内部。我们将从 Kubernetes API 开始,学习如何通过直接访问 API、使用 controller-runtime Go 库以及自动化 kubectl 来以编程方式操作 Kubernetes。然后,我们将研究如何使用自定义资源扩展 Kubernetes API。最后一部分将介绍 Kubernetes 支持的各种插件。Kubernetes 操作的许多方面都是模块化的,并且设计为可扩展的。我们将研究 API 聚合层以及几种类型的插件,如自定义调度器、授权、准入控制、自定义度量和存储卷。最后,我们将探讨如何扩展 kubectl 并添加自己的命令。
涵盖的主题如下:
-
使用 Kubernetes API
-
扩展 Kubernetes API
-
编写 Kubernetes 和 kubectl 插件
-
编写 Webhook
使用 Kubernetes API
Kubernetes API 是全面的,涵盖了 Kubernetes 的所有功能。正如你可能预料的,它非常庞大。但它使用最佳实践设计,非常出色,并且保持一致性。如果你理解了基本原理,你可以发现所有你需要知道的内容。我们在第一章《理解 Kubernetes 架构》中已经介绍了 Kubernetes API。如果你需要回顾一下,可以去看看。在本节中,我们将深入探讨,学习如何访问和使用 Kubernetes API。但首先,让我们了解一下 OpenAPI,它是为整个 Kubernetes API 提供结构化的正式基础。
理解 OpenAPI
OpenAPI(前身为 Swagger)是一种开放标准,定义了一种与语言和框架无关的方式来描述 RESTful API。它提供了一种标准化、机器可读的格式来描述 API,包括其端点、参数、请求和响应体、身份验证及其他元数据。
在 Kubernetes 的上下文中,OpenAPI 用于定义和文档化 Kubernetes 集群的 API 接口。OpenAPI 被用于 Kubernetes,提供了一种标准化的方式来文档化和定义可以用来配置和管理集群的 API 对象。Kubernetes API 基于声明式模型,用户通过 YAML 或 JSON 清单定义所需资源的状态。这些清单遵循 OpenAPI 架构,定义了每个资源的结构和属性。Kubernetes 使用 OpenAPI 架构来验证清单,在 API 客户端中提供自动补全和文档,并生成 API 参考文档。
在 Kubernetes 中使用 OpenAPI 的一个主要好处是,它支持为客户端库生成代码。这使得开发者可以使用他们选择的编程语言和生成的客户端库与 Kubernetes API 进行交互,提供了一种本地化且类型安全的方式与 API 交互。
此外,OpenAPI 还允许像 kubectl 这样的工具为 Kubernetes 资源提供自动补全和验证功能。
OpenAPI 还支持自动化生成 Kubernetes API 文档。通过 OpenAPI 架构,Kubernetes 可以自动生成 API 参考文档,作为一个全面且最新的资源,帮助理解 Kubernetes API 及其功能。
自 Kubernetes 1.27 起,Kubernetes 就稳定支持 OpenAPI v3。
详情请访问 www.openapis.org。
为了在本地使用 Kubernetes API,我们需要设置一个代理。
设置代理
为了简化访问,您可以使用 kubectl 设置一个代理:
$ k proxy --port 8080
现在,你可以在 http://localhost:8080 上访问 API 服务器,它将连接到与 kubectl 配置相同的 Kubernetes API 服务器。
直接探索 Kubernetes API
Kubernetes API 是高度可发现的。你只需要浏览到 http://localhost:8080 的 API 服务器 URL,就能获取一个描述所有可用操作的 JSON 文档,其中包含在 paths 键下的所有操作。
由于空间限制,这里是部分列表:
{
"paths": [
"/api",
"/api/v1",
"/apis",
"/apis/",
"/apis/admissionregistration.k8s.io",
"/apis/admissionregistration.k8s.io/v1",
"/apis/apiextensions.k8s.io",
"/livez/poststarthook/storage-object-count-tracker-hook",
"/logs",
"/metrics",
"/openapi/v2",
"/openapi/v3",
"/openapi/v3/",
"/openid/v1/jwks",
"/readyz/shutdown",
"/version"
]
}
你可以深入查看任意一个路径。例如,要发现 default 命名空间的端点,我首先访问了 /api 端点,然后发现了 /api/v1,它告诉我有 /api/v1/namespaces,接着指向了 /api/v1/namespaces/default。以下是来自 /api/v1/namespaces/default 端点的响应:
{
"kind": "Namespace",
"apiVersion": "v1",
"metadata": {
"name": "default",
"uid": "7e39c279-949a-4fb6-ae47-796bb797082d",
"resourceVersion": "192",
"creationTimestamp": "2022-11-13T04:33:00Z",
"labels": {
"kubernetes.io/metadata.name": "default"
},
"managedFields": [
{
"manager": "kube-apiserver",
"operation": "Update",
"apiVersion": "v1",
"time": "2022-11-13T04:33:00Z",
"fieldsType": "FieldsV1",
"fieldsV1": {
"f:metadata": {
"f:labels": {
".": {},
"f:kubernetes.io/metadata.name": {}
}
}
}
}
]
},
"spec": {
"finalizers": [
"kubernetes"
]
},
"status": {
"phase": "Active"
}
}
您可以通过命令行工具,如 cURL 或者 kubectl 本身,来探索 Kubernetes API,但有时使用图形界面应用程序会更加方便。
使用 Postman 探索 Kubernetes API
Postman (www.getpostman.com) 是一个非常精致的应用程序,用于操作 RESTful API。如果你更倾向于使用图形界面,可能会发现它非常有用。
以下截图展示了批量 v1 API 组下的可用端点:

图 15.1:批量 v1 API 组下的可用端点
Postman 提供了很多选项,而且它以一种非常舒适的方式组织信息。试试看吧。
使用 HTTPie 和 jq 过滤输出
API 的输出有时可能过于冗长。通常,你只对从 JSON 响应中的一部分值感兴趣。例如,如果你想获取所有正在运行的服务的名称,可以访问 /api/v1/services 端点。然而,响应中包含了很多无关的信息。以下是输出的一个非常小的子集:
$ http http://localhost:8080/api/v1/services
{
"kind": "ServiceList",
"apiVersion": "v1",
"metadata": {
"resourceVersion": "3237"
},
"items": [
...
{
"metadata": {
"name": "kube-dns",
"namespace": "kube-system",
...
},
"spec": {
...
"selector": {
"k8s-app": "kube-dns"
},
"clusterIP": "10.96.0.10",
"type": "ClusterIP",
"sessionAffinity": "None",
},
"status": {
"loadBalancer": {}
}
}
]
}
完整的输出共有 193 行!让我们看看如何使用 HTTPie 和 jq 来完全控制输出,并仅显示服务的名称。我更喜欢 HTTPie(httpie.org/) 来与命令行中的 REST API 进行交互,相较于 cURL。jq (stedolan.github.io/jq/) 是一个非常棒的命令行 JSON 处理工具,适合切割和处理 JSON 数据。
检查完整的输出,你会看到服务名称位于 items 数组中每个项的 metadata 部分。用于选择名称的 jq 表达式如下:
.items[].metadata.name
这是在一个新建的 kind 集群上的完整命令和输出:
$ http http://localhost:8080/api/v1/services | jq '.items[].metadata.name'
"kubernetes"
"kube-dns"
通过 Python 客户端访问 Kubernetes API
使用 HTTPie 和 jq 交互式地探索 API 很棒,但 API 的真正力量在于它们可以与其他软件进行消费和集成。Kubernetes Incubator 项目提供了一个完整且文档齐全的 Python 客户端库。可以在 github.com/kubernetes-incubator/client-python 获取。
首先,确保你已安装 Python(wiki.python.org/moin/BeginnersGuide/Download)。然后安装 Kubernetes 包:
$ pip install kubernetes
要开始与 Kubernetes 集群进行交互,你需要连接到它。启动一个交互式 Python 会话:
$ python
Python 3.9.12 (main, Aug 25 2022, 11:03:34)
[Clang 13.1.6 (clang-1316.0.21.2.3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>
Python 客户端可以读取你的 kubectl 配置:
>>> from kubernetes import client, config
>>> config.load_kube_config()
>>> v1 = client.CoreV1Api()
或者,它也可以直接连接到已经运行的代理:
>>> from kubernetes import client, config
>>> client.Configuration().host = 'http://localhost:8080'
>>> v1 = client.CoreV1Api()
请注意,client 模块提供了访问不同组版本的方法,例如 CoreV1Api。
解构 CoreV1Api 组
让我们深入了解并理解 CoreV1Api 组。这个 Python 对象有 407 个公共属性!
>>> attributes = [x for x in dir(v1) if not x.startswith('__')]
>>> len(attributes)
407
我们忽略以双下划线开头的属性,因为它们是与 Kubernetes 无关的特殊类/实例方法。
让我们选择十个随机方法,看看它们的样子:
>>> import random
>>> from pprint import pprint as pp
>>> pp(random.sample(attributes, 10))
['replace_namespaced_persistent_volume_claim',
'list_config_map_for_all_namespaces_with_http_info',
'connect_get_namespaced_pod_attach_with_http_info',
'create_namespaced_event',
'connect_head_node_proxy_with_path',
'create_namespaced_secret_with_http_info',
'list_namespaced_service_account',
'connect_post_namespaced_pod_portforward_with_http_info',
'create_namespaced_service_account_token',
'create_namespace_with_http_info']
非常有趣。属性名以动词开头,例如 replace、list 或 create。其中许多属性涉及命名空间,并且许多属性有一个 with_http_info 后缀。为了更好地理解这一点,让我们统计一下有多少个动词存在,以及每个动词使用的属性数量(动词是下划线前的第一个标记):
>>> from collections import Counter
>>> verbs = [x.split('_')[0] for x in attributes]
>>> pp(dict(Counter(verbs)))
{'api': 1,
'connect': 96,
'create': 38,
'delete': 58,
'get': 2,
'list': 56,
'patch': 50,
'read': 54,
'replace': 52}
我们可以深入查看特定属性的交互式帮助:
>>> help(v1.create_node)
Help on method create_node in module kubernetes.client.apis.core_v1_api:
create_node(body, **kwargs) method of kubernetes.client.api.core_v1_api.CoreV1Api instance
create_node # noqa: E501
create a Node # noqa: E501
This method makes a synchronous HTTP request by default. To make an
asynchronous HTTP request, please pass async_req=True
>>> thread = api.create_node(body, async_req=True)
>>> result = thread.get()
:param async_req bool: execute request asynchronously
:param V1Node body: (required)
:param str pretty: If 'true', then the output is pretty printed.
:param str dry_run: When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed
:param str field_manager: fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.
:param str field_validation: fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields, provided that the `ServerSideFieldValidation` feature gate is also enabled. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23 and is the default behavior when the `ServerSideFieldValidation` feature gate is disabled. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default when the `ServerSideFieldValidation` feature gate is enabled. - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.
:param _preload_content: if False, the urllib3.HTTPResponse object will
be returned without reading/decoding response
data. Default is True.
:param _request_timeout: timeout setting for this request. If one
number provided, it will be total request
timeout. It can also be a pair (tuple) of
(connection, read) timeouts.
:return: V1Node
If the method is called
returns the request thread.
我们看到 API 非常庞大,这也很合理,因为它代表了整个 Kubernetes API。我们还学习了如何发现相关方法的组,并且如何获取特定方法的详细信息。
你可以自己探索并了解更多关于 API 的内容。让我们看看一些常见的操作,如列出、创建和观察对象。
列出对象
你可以列出不同种类的对象。方法名以 list_ 开头。这里是一个列出所有命名空间的示例:
>>> for ns in v1.list_namespace().items:
... print(ns.metadata.name)
...
default
kube-node-lease
kube-public
kube-system
local-path-storage
创建对象
要创建一个对象,你需要将 body 参数传递给 create 方法。body 必须是一个 Python 字典,相当于你使用 kubectl 时的 YAML 配置清单。最简单的方法是直接使用 YAML 清单,然后使用 Python YAML 模块(这不是标准库的一部分,必须单独安装)读取 YAML 文件并将其加载到字典中。例如,要创建一个具有 3 个副本的 nginx-deployment,我们可以使用这个 YAML 清单(nginx-deployment.yaml):
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
要安装 yaml Python 模块,输入以下命令:
$ pip install yaml
然后,以下 Python 程序(create_nginx_deployment.py)将创建部署:
from os import path
import yaml
from kubernetes import client, config
def main():
# Configs can be set in Configuration class directly or using
# helper utility. If no argument provided, the config will be
# loaded from default location.
config.load_kube_config()
with open(path.join(path.dirname(__file__),
'nginx-deployment.yaml')) as f:
dep = yaml.safe_load(f)
k8s = client.AppsV1Api()
dep = k8s.create_namespaced_deployment(body=dep,
namespace="default")
print(f"Deployment created. status='{dep.status}'")
if __name__ == '__main__':
main()
让我们运行它并使用 kubectl 检查部署是否已创建:
$ python create_nginx_deployment.py
Deployment created. status='{'available_replicas': None,
'collision_count': None,
'conditions': None,
'observed_generation': None,
'ready_replicas': None,
'replicas': None,
'unavailable_replicas': None,
'updated_replicas': None}'
$ k get deploy
NAME READY UP-TO-DATE AVAILABLE AGE
nginx-deployment 3/3 3 3 56s
监视对象
监视对象是一项高级功能。它是通过一个单独的 watch 模块实现的。以下是监视 10 个命名空间事件并将其打印到屏幕上的示例(watch_demo.py):
from kubernetes import client, config, watch
# Configs can be set in Configuration class directly or using helper utility
config.load_kube_config()
v1 = client.CoreV1Api()
count = 10
w = watch.Watch()
for event in w.stream(v1.list_namespace, _request_timeout=60):
print(f"Event: {event['type']} {event['object'].metadata.name}")
count -= 1
if count == 0:
w.stop()
print('Done.')
这是输出:
$ python watch_demo.py
Event: ADDED kube-node-lease
Event: ADDED default
Event: ADDED local-path-storage
Event: ADDED kube-system
Event: ADDED kube-public
请注意,只有 5 个事件被打印出来(每个命名空间一个),程序继续监视更多事件。
我们在一个单独的终端窗口中创建并删除一些命名空间,以便程序能够结束:
$ k create ns ns-1
namespace/ns-1 created
$ k delete ns ns-1
namespace "ns-1" deleted
$ k create ns ns-2
namespace/ns-2 created
The final output is:
$ python watch_demo.py
Event: ADDED default
Event: ADDED local-path-storage
Event: ADDED kube-system
Event: ADDED kube-public
Event: ADDED kube-node-lease
Event: ADDED ns-1
Event: MODIFIED ns-1
Event: MODIFIED ns-1
Event: DELETED ns-1
Event: ADDED ns-2
Done.
你当然可以在事件发生时做出反应并执行有用的操作(例如,在每个新命名空间中自动部署工作负载)。
通过 Kubernetes API 创建一个 pod
API 也可以用于创建、更新和删除资源。与使用 kubectl 不同,API 需要以 JSON 格式而不是 YAML 语法指定清单(尽管每个 JSON 文档也是有效的 YAML)。以下是一个 JSON 格式的 pod 定义(nginx-pod.json):
{
"kind": "Pod",
"apiVersion": "v1",
"metadata":{
"name": "nginx",
"namespace": "default",
"labels": {
"name": "nginx"
}
},
"spec": {
"containers": [{
"name": "nginx",
"image": "nginx",
"ports": [{"containerPort": 80}]
}]
}
}
以下命令将通过 API 创建 pod:
$ http POST http://localhost:8080/api/v1/namespaces/default/pods @nginx-pod.json
为了验证是否成功,让我们提取当前 pod 的名称和状态。端点是 /api/v1/namespaces/default/pods。
jq 表达式是 items[].metadata.name,.items[].status.phase。
以下是完整的命令和输出:
$ FILTER='.items[].metadata.name,.items[].status.phase'
$ http http://localhost:8080/api/v1/namespaces/default/pods | jq $FILTER
"nginx"
"Running"
使用 Go 和 controller-runtime 控制 Kubernetes
Python 很酷且易于使用,但对于生产级工具、控制器和操作器,我更倾向于使用 Go,特别是 controller-runtime 项目。controller-runtime 是用于访问 Kubernetes API 的标准 Go 客户端。
通过 go-k8s 使用 controller-runtime
controller-runtime 项目是一组 Go 库,可以非常高效地查询和操作 Kubernetes(例如,使用高级缓存来避免压垮 API 服务器)。
直接使用 controller-runtime 并不容易。它有许多相互关联的部分,并且有不同的方法来完成任务。
查看:pkg.go.dev/sigs.k8s.io/controller-runtime。
我创建了一个名为 go-k8s 的小型开源项目,它封装了一些复杂性,并帮助以更少的麻烦使用 controller-runtime 功能的子集。
查看详情:github.com/the-gigi/go-k8s/tree/main/pkg/client。
请注意,go-k8s 项目还有其他库,但我们将重点介绍客户端库。
go-k8s 客户端包支持两种类型的客户端:Clientset 和 DynamicClient。Clientset 客户端支持与已知类型的交互,但需要明确指定 API 版本、类型和操作作为方法名称。例如,使用 Clientset 列出所有 pods 如下所示:
podList, err := clientset.CoreV1().Pods("ns-1").List(context.Background(), metav1.ListOptions{})
它返回一个 pod 列表和一个错误。如果一切正常,错误为 nil。pod 列表是结构体类型 PodList,定义见此处:github.com/kubernetes/kubernetes/blob/master/pkg/apis/core/types.go#L2514。
方便的是,你可以在同一个文件中找到所有 Kubernetes API 类型。API 结构非常嵌套,例如,PodList 如你所料,是 Pod 对象的列表。每个 Pod 对象都有 TypeMeta、ObjectMeta、PodSpec 和 PodStatus:
type Pod struct {
metav1.TypeMeta
metav1.ObjectMeta
Spec PodSpec
Status PodStatus
}
实际上,这意味着当你通过 Clientset 发起调用时,你会得到一个强类型的嵌套对象,非常容易操作。例如,如果我们想检查一个 Pod 是否有一个名为 app 的标签及其值,我们可以用一行代码完成:
app, ok := pods[0].ObjectMeta.Labels["app"]
如果标签不存在,ok 将为 false。如果存在,则其值将存储在 app 变量中。
现在,让我们来看一下 DynamicClient。在这里,你获得了终极的灵活性,能够与知名类型以及自定义类型一起工作。特别是,如果你想创建任意资源,动态客户端可以以通用方式操作任何 Kubernetes 类型。
然而,使用动态客户端时,你总是会返回一个通用对象,类型为 Unstructured,定义见此处:github.com/kubernetes/apimachinery/blob/master/pkg/apis/meta/v1/unstructured/unstructured.go#L41。
它实际上只是一个围绕通用 Go 类型 map[string]interface{} 的薄包装。它有一个名为 Object 的字段,类型是 map[string]interface{}。这意味着你返回的对象是一个字段名称到任意其他对象(表示为 interface{})的映射。要深入层次结构,我们必须进行类型转换,这意味着将一个 interface{} 值显式转换为其实际类型。下面是一个简单的示例:
var i interface{} = 5
x, ok := i.(int)
现在,x 是一个类型为 int 的变量,值为 5,可以作为整数使用。原始的 i 变量不能作为整数使用,因为它的类型是通用的 interface{},即使它包含一个整数值。
对于从动态客户端返回的对象,我们必须不断地将interface{}类型转换为map[string]interface{},直到我们找到感兴趣的字段。要获取我们 Pod 的 app 标签,我们需要遵循以下路径:
pod := pods[0].Object
metadata := pod["metadata"].(map[string]interface{})
labels := metadata["labels"].(map[string]interface{})
app, ok := labels["app"].(string)
这非常繁琐且容易出错。幸运的是,有一种更好的方法。Kubernetes 的 apimachinery/runtime 包提供了一个转换函数,可以将一个非结构化的对象转换为已知类型:
pod := pods[0].Object
var p corev1.Pod
err = runtime.DefaultUnstructuredConverter.FromUnstructured(pod, &p)
if err != nil {
return err
}
app, ok = p.ObjectMeta.Labels["app"]
controller-runtime 非常强大,但处理所有类型时可能会感到乏味。一个“作弊”的方法是使用 kubectl,它实际上是在幕后使用 controller-runtime。这在使用 Python 和其动态类型时尤其容易。
从 Python 和 Go 中程序化地调用 kubectl
如果你不想直接处理 REST API 或客户端库,你还有另一种选择。Kubectl 主要作为一个交互式命令行工具使用,但没有什么能阻止你通过脚本和程序来自动化它并调用它。使用 kubectl 作为 Kubernetes API 客户端有一些好处:
-
容易找到任何用法的示例
-
在命令行上很容易进行实验,以找到命令和参数的正确组合
-
kubectl 支持 JSON 或 YAML 格式的输出,便于快速解析
-
通过 kubectl 配置内建了身份验证
使用 Python 的 subprocess 来运行 kubectl
首先让我们使用 Python,这样你可以比较使用官方 Python 客户端和自己编写代码的不同。Python 有一个名为subprocess的模块,可以运行外部进程,比如 kubectl,并捕获输出。
这是一个 Python 3 示例,运行 kubectl 并显示使用输出的开头:
>>> import subprocess
>>> out = subprocess.check_output('kubectl').decode('utf-8')
>>> print(out[:276])
Kubectl 控制 Kubernetes 集群管理器。
更多信息请访问kubernetes.io/docs/reference/kubectl/overview/。
check_output()函数将输出捕获为字节数组,需要解码为utf-8才能正确显示。我们可以稍微通用化一下,创建一个名为k()的便利函数,放在k.py文件中。它接受任意数量的参数,传递给 kubectl,然后解码输出并返回:
from subprocess import check_output
def k(*args):
out = check_output(['kubectl'] + list(args))
return out.decode('utf-8')
让我们用它来列出默认命名空间中所有正在运行的 Pod:
>>> from k import k
>>> print(k('get', 'po'))
NAME READY STATUS RESTARTS AGE
nginx 1/1 Running 0 4h48m
nginx-deployment-679f9c75b-c79mv 1/1 Running 0 132m
nginx-deployment-679f9c75b-cnmvk 1/1 Running 0 132m
nginx-deployment-679f9c75b-gzfgk 1/1 Running 0 132m
这对于显示很不错,但 kubectl 本身就已经实现了这一点。真正的强大之处在于,当你使用带有-o标志的结构化输出选项时。然后结果可以自动转换为 Python 对象。以下是修改版的k()函数,它接受一个布尔值use_json关键字参数(默认为False),如果为True,则添加-o json,然后解析 JSON 输出为 Python 对象(字典):
from subprocess import check_output
import json
def k(*args, use_json=False):
cmd = ['kubectl'] + list(args)
if use_json:
cmd += ['-o', 'json']
out = check_output(cmd).decode('utf-8')
if use_json:
out = json.loads(out)
return out
这将返回一个完备的 API 对象,可以像访问 REST API 或使用官方 Python 客户端一样进行导航和深入探查:
result = k('get', 'po', use_json=True)
>>> for r in result['items']:
... print(r['metadata']['name'])
...
nginx-deployment-679f9c75b-c79mv
nginx-deployment-679f9c75b-cnmvk
nginx-deployment-679f9c75b-gzfgk
让我们来看看如何删除部署并等待所有 Pod 消失。kubectl 的delete命令不支持-o json选项(尽管它有-o name),因此我们将省略use_json:
>>> k('delete', 'deployment', 'nginx-deployment')
while len(k('get', 'po', use_json=True)['items']) > 0:
print('.')
print('Done.')
.
.
.
.
Done.
Python 很棒,但如果你更喜欢用 Go 来自动化 kubectl 呢?别担心,我有适合你的包。kugo包提供了一个简单的 Go API 来自动化 kubectl。你可以在这里找到代码:github.com/the-gigi/kugo。
它提供了三个功能:Run(),Get()和Exec()。
Run()函数是你的瑞士军刀。它可以直接运行任何 kubectl 命令。以下是一个示例:
cmd := fmt.Sprintf("create deployment test-deployment --image nginx --replicas 3 -n ns-1")
_, err := kugo.Run(cmd)
这非常方便,因为你可以交互式地组合你需要的准确命令和参数,然后,一旦你弄清楚所有内容,你可以直接将相同的命令传递给 Go 程序中的kuge.Run()。
Get() 函数是围绕 kubectl get 的一个智能包装器。它接受一个 GetRequest 参数,并提供多种便利功能:支持字段选择器、按标签获取和不同的输出类型。以下是使用自定义 kube 配置文件和自定义 kube 上下文获取所有命名空间名称的示例:
output, err := kugo.Get(kugo.GetRequest{
BaseRequest: kugo.BaseRequest{
KubeConfigFile: c.kubeConfigFile,
KubeContext: c.GetKubeContext(),
},
Kind: "ns",
Output: "name",
})
最后,Exec() 函数是围绕 kubectl exec 的一个包装器,允许您在运行的 pod/container 上执行命令。它接受一个看起来像这样的 ExecRequest:
type GetRequest struct {
BaseRequest
Kind string
FieldSelectors []string
Label string
Output string
}
让我们来看看 Exec() 函数的代码。它非常简单直接。它进行基本验证,确保提供了像 Command 和 Target 这样的必需字段,然后构建一个从 exec 命令开始的 kubectl 参数列表,最后调用 Run() 函数:
// Exec executes a command in a pod
//
// The target pod can specified by name or an arbitrary pod
// from a deployment or service.
//
// If the pod has multiple containers you can choose which
// container to run the command in
func Exec(r ExecRequest) (result string, err error) {
if r.Command == "" {
err = errors.New("Must specify Command field")
return
}
if r.Target == "" {
err = errors.New("Must specify Target field")
return
}
args := []string{"exec", r.Target}
if r.Container != "" {
args = append(args, "-c", r.Container)
}
args = handleCommonArgs(args, r.BaseRequest)
args = append(args, "--", r.Command)
return Run(args...)
}
现在,通过其 REST API、客户端库的编程方式访问 Kubernetes,并通过控制 kubectl,是学习如何扩展 Kubernetes 的时候了。
扩展 Kubernetes API
Kubernetes 是一个极其灵活的平台。从一开始就设计为可扩展的,并随着演变,越来越多的 Kubernetes 组件被开放,并通过稳健的接口暴露出来,可以被替换为替代实现。我敢说,Kubernetes 在初创公司、大公司、基础设施提供商和云提供商中的指数级采用,直接源于 Kubernetes 提供了大量的开箱即用功能,同时又允许与其他组件轻松集成。在本节中,我们将涵盖许多可用的扩展点,例如:
-
用户定义类型(自定义资源)
-
API 访问扩展
-
基础设施扩展
-
操作者
-
调度程序扩展
让我们了解您可以如何以各种方式扩展 Kubernetes。
理解 Kubernetes 的扩展点和模式
Kubernetes 由多个组件组成:API 服务器、etcd 状态存储、控制器管理器、kube-proxy、kubelet 和容器运行时。您可以深入扩展和定制每一个这些组件,还可以添加自己的定制组件,以监视和响应事件,处理新请求,并修改关于传入请求的一切。
下图显示了一些可用的扩展点及其与各种 Kubernetes 组件的连接方式:

图 15.2:可用的扩展点
让我们看看如何使用插件扩展 Kubernetes。
使用插件扩展 Kubernetes
Kubernetes 定义了几个接口,允许其与各种基础设施提供商的插件进行交互。我们在前几章节详细讨论了其中一些接口和插件。在这里,我们只列出它们以供完整性考虑:
-
容器网络接口(CNI)– CNI 支持大量的连接节点和容器的网络解决方案
-
容器存储接口(CSI)– CSI 支持大量的 Kubernetes 存储选项
-
设备插件——允许节点发现除 CPU 和内存之外的新节点资源(例如,GPU)
使用云控制器管理器扩展 Kubernetes
Kubernetes 最终需要部署在一些节点上,并使用一些存储和网络资源。最初,Kubernetes 仅支持 Google Cloud Platform 和 AWS。其他云提供商必须定制多个 Kubernetes 核心组件(如 Kubelet、Kubernetes 控制器管理器和 Kubernetes API 服务器),才能与 Kubernetes 集成。Kubernetes 开发者将其视为采用过程中的问题,并创建了云控制器管理器(CCM)。CCM 清晰地定义了 Kubernetes 与其部署基础设施层之间的交互。现在,云提供商只需要提供一个适应其基础设施的 CCM 实现,并可以利用上游 Kubernetes,而无需对 Kubernetes 代码进行代价高昂且容易出错的修改。所有 Kubernetes 组件通过预定义接口与 CCM 交互,Kubernetes 则完全不关心它运行在哪个云(或没有云)上。
以下图示演示了 Kubernetes 与云提供商通过 CCM 的交互:

图 15.3:Kubernetes 与云提供商通过 CCM 的交互
如果你想了解更多关于 CCM 的信息,可以查看我几年前写的这篇简明文章:medium.com/@the.gigi/kubernetes-and-cloud-providers-b7a6227d3198。
使用 webhook 扩展 Kubernetes
插件在集群中运行,但在某些情况下,更好的扩展性模式是将一些功能委托给集群外部的服务。这在访问控制领域非常常见,因为公司和组织可能已经有一个集中化的身份和访问控制解决方案。在这些情况下,webhook 扩展模式非常有用。其核心思想是,你可以通过配置 Kubernetes 与一个端点(webhook)进行交互。Kubernetes 将调用该端点,你可以在其中实现自己的自定义功能,然后 Kubernetes 会根据响应采取相应的行动。我们在第四章《保护 Kubernetes》中讨论身份验证、授权和动态准入控制时,看到过这种模式。
Kubernetes 定义了每个 webhook 的预期有效载荷。Webhook 的实现必须遵循这些定义,才能成功与 Kubernetes 进行交互。
使用控制器和操作符扩展 Kubernetes
控制器模式是编写一个可以在集群内或集群外运行的程序,监视事件并响应它们。控制器的概念模型是将集群的当前状态(控制器感兴趣的部分)与期望状态进行协调。控制器的常见做法是读取对象的 Spec,采取一些行动,并更新其 Status。Kubernetes 的许多核心逻辑是由由控制器管理器管理的大量控制器实现的,但没有任何限制阻止我们将自己的控制器部署到集群中,或者运行访问 API 服务器的远程控制器。
操作员模式是控制器模式的另一种形式。可以将操作员视为一种控制器,它还有自己的一组自定义资源,表示它管理的应用程序。操作员的目标是管理部署在集群内或某些集群外基础设施中的应用程序的生命周期。可以查看 operatorhub.io 了解现有操作员的示例。
如果你计划构建自己的控制器,我推荐从 Kubebuilder 开始 (github.com/kubernetes-sigs/kubebuilder)。这是一个由 Kubernetes API Machinery SIG 维护的开源项目,支持使用 CRD 定义多个自定义 API,并自动生成控制器代码来监视这些资源。你将在 Go 中实现控制器。
然而,仍然有多个其他框架可以编写控制器和操作员,采用不同的方法,并使用其他编程语言:
-
操作员框架
-
Kopf
-
kube-rs
-
KubeOps
-
KUDO
-
元控制器
在做决定之前,先了解一下它们。
扩展 Kubernetes 调度
Kubernetes 的主要工作,简而言之,就是在节点上调度 Pods。调度是 Kubernetes 的核心功能,它做得非常好。Kubernetes 调度器可以通过非常高级的方式进行配置(守护进程集、污点、容忍度等)。但即便如此,Kubernetes 开发人员也认识到,在某些特殊情况下,你可能希望控制核心调度算法。你可以用自己的调度器替换核心 Kubernetes 调度器,或者与内置调度器并行运行另一个调度器,来控制一部分 Pod 的调度。我们将在本章稍后部分看到如何实现这一点。
使用自定义容器运行时扩展 Kubernetes
Kubernetes 最初只支持 Docker 作为容器运行时。Docker 的支持被嵌入到 Kubernetes 核心代码中。后来,专门支持 rkt 也被加入。Kubernetes 的开发者意识到这一点,并引入了容器运行时接口(CRI),这是一个 gRPC 接口,任何实现它的容器运行时都可以与 kubelet 通信。最终,Docker 和 rkt 的硬编码支持被淘汰,现在 kubelet 仅通过 CRI 与容器运行时进行通信:

图 15.4:Kubelet 通过 CRI 与容器运行时通信
自 CRI(容器运行时接口)引入以来,支持 Kubernetes 的容器运行时数量激增。
我们已经讨论了多种扩展 Kubernetes 不同方面的方法。接下来,让我们聚焦于自定义资源的主要概念,它允许你扩展 Kubernetes API 本身。
引入自定义资源
扩展 Kubernetes 的主要方式之一是定义新类型的资源,称为自定义资源。你可以用自定义资源做什么?很多。你可以通过 Kubernetes API 管理那些存在于 Kubernetes 集群外,但你的 Pod 与之通信的资源。通过将这些外部资源作为自定义资源,你能更全面地了解你的系统,并能享受到 Kubernetes API 的众多功能,如:
-
自定义 CRUD REST 端点
-
版本控制
-
监听
-
与通用 Kubernetes 工具的自动集成
自定义资源的其他应用场景包括自定义控制器和自动化程序的元数据。
让我们深入了解自定义资源的具体内容。
为了与 Kubernetes API 服务器兼容,自定义资源必须符合一些基本要求。类似于内建的 API 对象,它们必须包含以下字段:
-
apiVersion:apiextensions.k8s.io/v1 -
metadata: 标准 Kubernetes 对象元数据 -
kind:CustomResourceDefinition -
spec: 描述资源在 API 和工具中的表现方式 -
status: 表示 CRD 的当前状态
spec包含一个内部结构,其中包括group、names、scope、validation和version等字段。status包括acceptedNames和Conditions字段。在接下来的章节中,我将展示一个示例,帮助你理解这些字段的含义。
开发自定义资源定义
你通过自定义资源定义(也叫 CRD)来开发自定义资源。目的是让 CRD 能够顺利地与 Kubernetes、其 API 和工具集成。这意味着你需要提供大量信息。以下是一个名为 Candy 的自定义资源的示例:
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
# name must match the spec fields below, and be in the form: <plural>.<group>
name: candies.awesome.corp.com
spec:
# group name to use for REST API: /apis/<group>/<version>
group: awesome.corp.com
# version name to use for REST API: /apis/<group>/<version>
versions:
- name: v1
# Each version can be enabled/disabled by Served flag.
served: true
# One and only one version must be marked as the storage version.
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
flavor:
type: string
# either Namespaced or Cluster
scope: Namespaced
names:
# plural name to be used in the URL: /apis/<group>/<version>/<plural>
plural: candies
# singular name to be used as an alias on the CLI and for display
singular: candy
# kind is normally the CamelCased singular type. Your resource manifests use this.
kind: Candy
# shortNames allow shorter string to match your resource on the CLI
shortNames:
- cn
Candy CRD 有几个有趣的部分。metadata 中包含一个完全限定的名称,应该是唯一的,因为 CRD 是集群范围的。spec 中有一个 versions 部分,它可以包含多个版本,每个版本都有一个指定自定义资源字段的 schema。该 schema 遵循 OpenAPI v3 规范(github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#schemaObject)。scope 字段可以是 Namespaced 或 Cluster。如果 scope 是 Namespaced,那么从 CRD 创建的自定义资源将仅存在于其创建的命名空间中,而集群范围的自定义资源可以在任何命名空间中使用。最后,names 部分指的是自定义资源的名称(而不是来自 metadata 部分的 CRD 名称)。names 部分有 plural、singular、kind 和 shortNames 选项。
让我们来创建 CRD:
$ k create -f candy-crd.yaml
customresourcedefinition.apiextensions.k8s.io/candies.awesome.corp.com created
请注意,返回的是 metadata 名称。通常使用复数名称。现在,让我们验证是否可以访问它:
$ k get crd
NAME CREATED AT
candies.awesome.corp.com 2022-11-24T22:56:27Z
还有一个 API 端点用于管理这个新资源:
/apis/awesome.corp.com/v1/namespaces/<namespace>/candies/
集成自定义资源
一旦 CustomResourceDefinition 对象创建完成,你可以创建该资源类型的自定义资源——在本例中是 Candy(candy 变为 CamelCase 的 Candy)。自定义资源必须遵守 CRD 的 schema。在以下示例中,flavor 字段被设置在名为 chocolate 的 Candy 对象上。apiVersion 字段是从 CRD 的 spec 中的 group 和 versions 字段推导出来的:
apiVersion: awesome.corp.com/v1
kind: Candy
metadata:
name: chocolate
spec:
flavor: sweeeeeeet
让我们创建它:
$ k create -f chocolate.yaml
candy.awesome.corp.com/chocolate created
请注意,spec 中必须包含 schema 中的 flavor 字段。
此时,kubectl 可以像操作内建对象一样操作 Candy 对象。使用 kubectl 时,资源名称是不区分大小写的:
$ k get candies
NAME AGE
chocolate 34s
我们还可以使用标准的 -o json 标志查看原始 JSON 数据。这次我们使用短名称 cn:
$ k get cn -o json
{
"apiVersion": "v1",
"items": [
{
"apiVersion": "awesome.corp.com/v1",
"kind": "Candy",
"metadata": {
"creationTimestamp": "2022-11-24T23:11:01Z",
"generation": 1,
"name": "chocolate",
"namespace": "default",
"resourceVersion": "750357",
"uid": "49f68d80-e9c0-4c20-a87d-0597a60c4ed8"
},
"spec": {
"flavor": "sweeeeeeet"
}
}
],
"kind": "List",
"metadata": {
"resourceVersion": ""
}
}
处理未知字段
规范中的 schema 是在 apiextensions.k8s.io/v1 版本的 CRD 中引入的,该版本在 Kubernetes 1.17 中变得稳定。使用 apiextensions.k8s.io/v1beta 时,不需要 schema,因此可以使用任意字段。如果你只是试图将 CRD 的版本从 v1beta 更改为 v1,你将面临一次痛苦的觉醒。Kubernetes 会允许你更新 CRD,但当你试图使用未知字段创建自定义资源时,它会失败。
你必须为所有的 CRD 定义一个 schema。如果你必须处理可能包含额外未知字段的自定义资源,可以关闭验证,但额外的字段会被剥离。
这是一个 Candy 资源,它有一个额外的字段 texture,该字段在 schema 中未指定:
apiVersion: awesome.corp.com/v1
kind: Candy
metadata:
name: gummy-bear
spec:
flavor: delicious
texture: rubbery
如果我们尝试使用验证来创建,它将失败:
$ k create -f gummy-bear.yaml
Error from server (BadRequest): error when creating "gummy-bear.yaml": Candy in version "v1" cannot be handled as a Candy: strict decoding error: unknown field "spec.texture"
但是,如果我们关闭验证,一切都会正常,除了只有 flavor 字段会出现,而 texture 字段不会:
$ k create -f gummy-bear.yaml --validate=false
candy.awesome.corp.com/gummy-bear created
$ k get cn gummy-bear -o yaml
apiVersion: awesome.corp.com/v1
kind: Candy
metadata:
creationTimestamp: "2022-11-24T23:13:33Z"
generation: 1
name: gummy-bear
namespace: default
resourceVersion: "750534"
uid: d77d9bdc-5a53-4f8e-8468-c29e2d46f919
spec:
flavor: delicious
有时,保留未知字段是有用的。CRDs 可以通过向架构中添加一个特殊字段来支持未知字段。
让我们删除当前的 Candy CRD,并用一个支持未知字段的 CRD 替代它:
$ k delete -f candy-crd.yaml
customresourcedefinition.apiextensions.k8s.io "candies.awesome.corp.com" deleted
$ k create -f candy-with-unknown-fields-crd.yaml
customresourcedefinition.apiextensions.k8s.io/candies.awesome.corp.com created
新的 CRD 在 spec 属性中将 x-kubernetes-preserve-unknown-fields 字段设置为 true:
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
x-kubernetes-preserve-unknown-fields: true
properties:
flavor:
type: string
让我们重新创建一个带有验证的糖果,并检查未知的 texture 字段是否存在:
$ k create -f gummy-bear.yaml
candy.awesome.corp.com/gummy-bear created
$ k get cn gummy-bear -o yaml
apiVersion: awesome.corp.com/v1
kind: Candy
metadata:
creationTimestamp: "2022-11-24T23:38:01Z"
generation: 1
name: gummy-bear
namespace: default
resourceVersion: "752234"
uid: 6863f767-5dc0-43f7-91f3-1c734931b979
spec:
flavor: delicious
texture: rubbery
完成自定义资源的最终化
自定义资源支持最终化器,就像标准 API 对象一样。最终化器是一种机制,对象不会立即被删除,而是必须等待在后台运行并监视删除请求的特殊控制器。控制器可以执行任何必要的清理操作,然后从目标对象中移除其最终化器。一个对象可能有多个最终化器。Kubernetes 会等待所有最终化器被移除后才会删除对象。元数据中的最终化器只是一些任意字符串,对应的控制器可以识别它们。Kubernetes 并不知道它们的含义。
它只是在删除对象之前耐心等待所有最终化器被移除。以下是一个示例,展示了一个包含两个最终化器(eat-me 和 drink-me)的 Candy 对象:
apiVersion: awesome.corp.com/v1
kind: Candy
metadata:
name: chocolate
finalizers:
- eat-me
- drink-me
spec:
flavor: sweeeeeeet
添加自定义打印列
默认情况下,当你使用 kubectl 列出自定义资源时,只会显示资源的名称和年龄:
$ k get cn
NAME AGE
chocolate 11h
gummy-bear 16m
但是 CRD 架构允许你添加自己的列。让我们将“flavor”和“age”作为可打印列添加到我们的 Candy 对象中:
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: candies.awesome.corp.com
spec:
group: awesome.corp.com
versions:
- name: v1
...
additionalPrinterColumns:
- name: Flavor
type: string
description: The flavor of the candy
jsonPath: .spec.flavor
- name: Age
type: date
jsonPath: .metadata.creationTimestamp
...
然后我们可以应用它,重新添加我们的糖果,并列出它们:
$ k apply -f candy-with-flavor-crd.yaml
customresourcedefinition.apiextensions.k8s.io/candies.awesome.corp.com configured
$ k get cn
NAME FLAVOR AGE
chocolate sweeeeeeet 13m
gummy-bear delicious 18m
理解 API 服务器聚合
当你只需要对自己的类型进行一些 CRUD 操作时,CRDs 非常有用。你可以直接依赖 Kubernetes API 服务器,它会存储你的对象,并提供 API 支持及与工具(如 kubectl)的集成。如果你需要更多功能,可以运行控制器,监视你的自定义资源,并在它们被创建、更新或删除时执行某些操作。Kubebuilder (github.com/kubernetes-sigs/kubebuilder) 项目是一个很好的框架,能帮助你在 CRDs 上构建 Kubernetes API,并创建自己的控制器。
但是 CRDs 有一定的限制。如果你需要更高级的功能和自定义,可以使用 API 服务器聚合,并编写你自己的 API 服务器,Kubernetes API 服务器将委托给它。你的 API 服务器将使用与 Kubernetes API 服务器本身相同的 API 机制。一些高级功能仅通过聚合层提供:
-
让你的 API 服务器采用不同的存储 API,而不是 etcd
-
扩展类似 WebSocket 的长期运行子资源/端点,以支持你自己的资源
-
将你的 API 服务器与任何外部系统集成
-
控制对象的存储(自定义资源始终存储在 etcd 中)
-
超出 CRUD 的自定义操作(例如,exec 或 scale)
-
使用协议缓冲区有效负载
编写扩展 API 服务器是一项不容小觑的工作。如果你决定需要所有这些功能,有几个不错的起点。你可以参考示例 API 服务器获取灵感(github.com/kubernetes/sample-apiserver)。你可能还想查看 apiserver-builder-alpha 项目(github.com/kubernetes-sigs/apiserver-builder-alpha)。它处理了很多必要的样板代码。API 构建器提供以下功能:
-
引导完整的类型定义、控制器、测试以及文档
-
一个可以在本地集群或实际远程集群上运行的扩展控制平面
-
你生成的控制器将能够监视并更新 API 对象
-
添加资源(包括子资源)
-
如果需要,你可以覆盖的默认值
这里也有一个教程:kubernetes.io/docs/tasks/extend-kubernetes/setup-extension-api-server/。
构建类似 Kubernetes 的控制平面
如果你想使用 Kubernetes 模型来管理其他资源,而不仅仅是 pod,怎么办?事实证明,这是一个非常受欢迎的功能。有一个 momentum 十足的项目提供了这个功能:github.com/kcp-dev/kcp。
kcp 还涉足了多集群管理。
kcp 能带来什么好处?
-
它是一个用于多个概念性集群(称为工作区)的控制平面
-
它使外部 API 服务提供商能够通过多租户操作符与中央控制平面进行集成
-
用户可以轻松地在工作区中使用 API
-
灵活地将工作负载调度到物理集群
-
在兼容的物理集群之间透明地迁移工作负载
-
用户可以在部署工作负载时,利用地理复制和跨云复制等功能。
我们已经介绍了通过添加控制器和聚合 API 服务器来扩展 Kubernetes 的不同方法。现在让我们来看看扩展 Kubernetes 的另一种方式:编写插件。
编写 Kubernetes 插件
在这一部分,我们将深入了解 Kubernetes 的内部结构,学习如何利用其著名的灵活性和可扩展性。我们将了解可以通过插件自定义的不同方面,以及如何实现这些插件并将它们与 Kubernetes 集成。
编写自定义调度器
Kubernetes 主要负责调度容器化工作负载。最基本的职责是将 pod 调度到集群节点上。在我们编写自己的调度器之前,我们需要理解 Kubernetes 中调度是如何工作的
理解 Kubernetes 调度器的设计
Kubernetes 调度器的角色非常简单——当需要创建一个新 Pod 时,将其分配到目标节点。就是这样。目标节点上的 Kubelet 会接管并指示节点上的容器运行时运行 Pod 的容器。
Kubernetes 调度器实现了控制器模式:
-
监控待处理的 Pod
-
为 Pod 选择合适的节点
-
通过设置
nodeName字段更新节点的规格
唯一复杂的部分是选择目标节点。这个过程涉及多个步骤,分为两个周期:
-
调度周期
-
绑定周期
虽然调度周期是顺序执行的,但绑定周期可以并行执行。如果目标 Pod 被认为不可调度或发生内部错误,周期将被终止,Pod 将被放回队列中,稍后重试。
调度器通过可扩展的调度框架实现。该框架定义了多个扩展点,你可以插入这些扩展点来影响调度过程。下图显示了整个过程和扩展点:

图 15.5:Kubernetes 调度器的工作流程
调度器会考虑大量的信息和配置。筛选过程会将不符合硬性约束的节点从候选列表中剔除。排名节点会为剩余的节点分配分数,并选择最佳节点。
以下是调度器在筛选节点时评估的因素:
-
验证 Pod 请求的端口是否在节点上可用,确保所需的网络连接。
-
确保 Pod 调度到主机名与指定节点偏好匹配的节点上。
-
验证节点上请求的资源(CPU 和内存)是否可用,以满足 Pod 的需求。
-
将节点的标签与 Pod 的节点选择器或节点亲和性匹配,以确保正确的调度。
-
确认节点支持请求的卷类型,考虑存储的故障域限制。
-
评估节点是否有能力容纳 Pod 的卷请求,并考虑现有的挂载卷。
-
通过检查内存压力或 PID 压力等指标来确保节点的健康。
-
评估 Pod 的容忍度,以确定与节点的污点兼容性,从而相应地启用或限制调度。
一旦节点被筛选,调度器将根据以下策略对节点进行评分(你可以配置这些策略):
-
在分配 Pod 时考虑将 Pod 分布在主机之间,同时考虑属于同一服务、StatefulSet 或 ReplicaSet 的 Pod。
-
优先考虑 Pod 间的亲和性,即偏好那些倾向于在同一节点上运行的 Pod。
-
应用“最少请求”优先级,偏向那些请求资源较少的节点。这一策略旨在将 Pod 均匀分布到集群中的所有节点。
-
应用“最请求”优先级,优先选择请求资源最多的节点。这项策略通常会将 Pods 集中到较少的节点上。
-
使用“请求与容量比”优先级,该优先级根据请求资源与节点容量的比例计算优先级。它使用默认的资源评分函数形状。
-
优先选择资源分配均衡的节点,偏向于资源使用均衡的节点。
-
利用“节点偏好避免 Pods”优先级,该优先级根据节点注解
scheduler.alpha.kubernetes.io/preferAvoidPods优先选择节点。此注解用于指示两个不同的 Pods 不应在同一个节点上运行。 -
应用节点亲和性优先级,根据
PreferredDuringSchedulingIgnoredDuringExecution中指定的节点亲和性调度偏好,优先选择节点。 -
考虑污点容忍优先级,根据每个节点上不可容忍污点的数量为所有节点准备优先级列表。这项策略会调整节点的排名,考虑污点因素。
-
使用“镜像本地性”优先级,优先选择那些已经包含 Pod 所需容器镜像的节点。
-
优先选择将支持服务的 Pods 分布在不同节点上的“服务分布”优先级。
-
应用 Pod 反亲和性,这意味着根据反亲和性规则避免在已运行相似 Pod 的节点上调度 Pod。
-
使用“相等优先级映射”,其中所有节点的权重相同,不存在偏爱或偏见。
查看 kubernetes.io/docs/concepts/scheduling-eviction/scheduling-framework/ 了解更多详情。
如你所见,默认调度器非常复杂,可以通过精细的配置来满足大多数需求。但是,在某些情况下,它可能不是最佳选择。
特别是在大规模集群中,节点数量众多(数百或数千个)时,每次调度一个 Pod,所有节点都需要经历这个严格且重量级的过滤和评分过程。现在,假设你需要一次性调度大量的 Pods(例如,训练机器学习模型)。这可能会给你的集群带来很大的压力,并导致性能问题。
Kubernetes 通过允许你仅对部分 Pods 进行过滤和评分,可以使过滤和评分过程变得更加轻量,但你仍然可能希望获得更好的控制。
幸运的是,Kubernetes 允许你通过多种方式影响调度过程。这些方式包括:
-
将 Pods 直接调度到节点
-
使用你自己的调度器替代默认调度器
-
扩展调度器并添加额外的过滤器
-
添加一个与默认调度器并行运行的调度器
让我们回顾一下你可以用来影响 Pod 调度的各种方法。
手动调度 Pods
猜猜看?我们可以在创建 pod 时直接告诉 Kubernetes 将 pod 放置在哪个节点。只需在 pod 的 spec 中指定节点名称,调度器将忽略它。如果你考虑到控制器模式的松耦合特性,这一切都能理解。调度器会监听那些尚未分配节点名称的待处理 pod。如果你自己传递了节点名称,那么目标节点上的 Kubelet 将会确保创建一个新的 pod。
让我们来看看我们的 k3d 集群的节点:
$ k get no
NAME STATUS ROLES AGE VERSION
k3d-k3s-default-agent-1 Ready <none> 155d v1.23.6+k3s1
k3d-k3s-default-server-0 Ready control-plane,master 155d v1.23.6+k3s1
k3d-k3s-default-agent-0 Ready <none> 155d v1.23.6+k3s1```
Here is a pod with a pre-defined node name, k3d-k3s-default-agent-1:
apiVersion: v1
kind: Pod
metadata:
name: some-pod-manual-scheduling
spec:
containers:
- name: some-container
image: registry.k8s.io/pause:3.8
nodeName: k3d-k3s-default-agent-1
schedulerName: no-such-scheduler
Let’s create the pod and see that it was indeed scheduled to the k3d-k3s-default-agent-1 node as requested:
$ k create -f some-pod-manual-scheduling.yaml
pod/some-pod-manual-scheduling 创建成功
$ k get po some-pod-manual-scheduling -o wide
名称 准备就绪 状态 重启次数 存活时长 IP 地址 节点 提名节点 就绪门控
some-pod-manual-scheduling 1/1 正在运行 0 26 秒 10.42.2.213 k3d-k3s-default-agent-1 <none> <none>
Direct scheduling is also useful for troubleshooting when you want to schedule a temporary pod to a tainted node without mucking around with adding tolerations.
Let’s create our own custom scheduler now.
Preparing our own scheduler
Our scheduler will be super simple. It will just schedule all pending pods that request to be scheduled by the custom-scheduler to the node k3d-k3s-default-agent-0. Here is a Python implementation that uses the kubernetes client package:
from kubernetes import client, config, watch
def schedule_pod(cli, name):
target = client.V1ObjectReference()
target.kind = 'Node'
target.apiVersion = 'v1'
target.name = 'k3d-k3s-default-agent-0'
meta = client.V1ObjectMeta()
meta.name = name
body = client.V1Binding(metadata=meta, target=target)
return cli.create_namespaced_binding('default', body)
def main():
config.load_kube_config()
cli = client.CoreV1Api()
w = watch.Watch()
for event in w.stream(cli.list_namespaced_pod, 'default'):
o = event['object']
if o.status.phase != 'Pending' or o.spec.scheduler_name != 'custom-scheduler':
continue
schedule_pod(cli, o.metadata.name)
if __name__ == '__main__':
main()
If you want to run a custom scheduler long term, then you should deploy it into the cluster just like any other workload as a deployment. But, if you just want to play around with it, or you’re still developing your custom scheduler logic, you can run it locally as long as it has the correct credentials to access the cluster and has permissions to watch for pending pods and update their node name.
Note that I strongly recommend building production custom schedulers on top of the scheduling framework (kubernetes.io/docs/concepts/scheduling-eviction/scheduling-framework/).
Assigning pods to the custom scheduler
OK. We have a custom scheduler that we can run alongside the default scheduler. But how does Kubernetes choose which scheduler to use to schedule a pod when there are multiple schedulers?
The answer is that Kubernetes doesn’t care. The pod can specify which scheduler it wants to schedule it. The default scheduler will schedule any pod that doesn’t specify the schedule or that specifies explicitly default-scheduler. Other custom schedulers should be responsible and only schedule pods that request them. If multiple schedulers try to schedule the same pod, we will probably end up with multiple copies or naming conflicts.
For example, our simple custom scheduler is looking for pending pods that specify a scheduler name of custom-scheduler. All other pods will be ignored by it:
if o.status.phase != 'Pending' or o.spec.scheduler_name != 'custom-scheduler':
continue
Here is a pod spec that specifies custom-scheduler:
apiVersion: v1
kind: Pod
metadata:
name: some-pod-with-custom-scheduler
spec:
containers:
- name: some-container
image: registry.k8s.io/pause:3.8
schedulerName: custom-scheduler
What happens if our custom scheduler is not running and we try to create this pod?
$ k create -f some-pod-with-custom-scheduler.yaml
pod/some-pod-with-custom-scheduler 创建成功
$ k get po
名称 准备就绪 状态 重启次数 存活时长
some-pod-manual-scheduling 1/1 正在运行 0 9 分钟 33 秒
some-pod-with-custom-scheduler 0/1 待处理 0 14 秒
The pod is created just fine (meaning the Kubernetes API server stored it in etcd), but it is pending, which means it wasn’t scheduled yet. Since it specified an explicit scheduler, the default scheduler ignores it.
But, if we run our scheduler… it will immediately get scheduled:
python custom_scheduler.py
等待待处理的 pod...
正在调度 pod: some-pod-with-custom-scheduler
Now, we can see that the pod was assigned to a node, and it is in a running state:
$ k get po -o wide
名称 准备就绪 状态 重启次数 存活时长 IP 地址 节点 提名节点 就绪门控
some-pod-manual-scheduling 1/1 正在运行 0 4 小时 5 分钟 10.42.2.213 k3d-k3s-default-agent-1 <none> <none>
some-pod-with-custom-scheduler 1/1 正在运行 0 87 秒 10.42.0.125 k3d-k3s-default-agent-0 <none> <none>
That was a deep dive into scheduling and custom schedulers. Let’s check out kubectl plugins.
Writing kubectl plugins
Kubectl is the workhorse of the aspiring Kubernetes developer and admin. There are now very good visual tools like k9s (github.com/derailed/k9s), octant (github.com/vmware-tanzu/octant), and Lens Desktop (k8slens.dev). But, for many engineers, kubectl is the most complete way to work interactively with your cluster, as well to participate in automation workflows.
Kubectl encompasses an impressive list of capabilities, but you will often need to string together multiple commands or a long chain of parameters to accomplish some tasks. You may also want to run some additional tools installed in your cluster.
You can package such functionality as scripts or containers, or any other way, but then you’ll run into the issue of where to place them, how to discover them, and how to manage them. Kubectl plugins give you a one-stop shop for those extended capabilities. For example, recently I needed to periodically list and move around files on an SFTP server managed by a containerized application running on a Kubernetes cluster. I quickly wrote a few kubectl plugins that took advantage of my KUBECONFIG credentials to get access to secrets in the cluster that contained the credentials to access the SFTP server and then implemented a lot of application-specific logic for accessing and managing those SFTP directories and files.
Understanding kubectl plugins
Until Kubernetes 1.12, kubectl plugins required a dedicated YAML file where you specified various metadata and other files that implemented the functionality. In Kubernetes 1.12, kubectl started using the Git extension model where any executable on your path with the prefix kubectl- is treated as a plugin.
Kubectl provides the kubectl plugins list command to list all your current plugins. This model was very successful with Git and it is extremely simple now to add your own kubectl plugins.
If you add an executable called kubectl-foo, then you can run it via kubectl foo. You can have nested commands too. Add kubectl-foo-bar to your path and run it via kubectl foo bar. If you want to use dashes in your commands, then in your executable, use underscores. For example, the executable kubectl-do_stuff can be run using kubectl do-stuff.
The executable itself can be implemented in any language, have its own command-line arguments and flags, and display its own usage and help information.
Managing kubectl plugins with Krew
The lightweight plugin model is great for writing your own plugins, but what if you want to share your plugins with the community? Krew (github.com/kubernetes-sigs/krew) is a package manager for kubectl plugins that lets you discover, install, and manage curated plugins.
You can install Krew with Brew on Mac or follow the installation instructions for other platforms. Krew is itself a kubectl plugin as its executable is kubectl-krew. This means you can either run it directly with kubectl-krew or through kubectl kubectl krew. If you have a k alias for kubectl, you would probably prefer the latter:
$ k krew
krew 是 kubectl 插件管理器。
你可以通过 kubectl 调用 krew:“kubectl krew [命令]...”
Usage:
kubectl krew [命令]
可用命令:
completion 为指定的 shell 生成自动补全脚本
help 获取关于任何命令的帮助
index 管理自定义插件索引
info 显示可用插件的信息
install 安装 kubectl 插件
list 列出已安装的 kubectl 插件
search 查找 kubectl 插件
uninstall 卸载插件
update 更新插件索引的本地副本
upgrade 升级已安装的插件到更新版本
version 显示 krew 版本和诊断信息
Flags:
-h, --help 获取 krew 帮助
-v, --v Level 日志级别的数字
使用 "kubectl krew [命令] --help" 获取更多关于某个命令的信息。
Note that the krew list command shows only Krew-managed plugins and not all kubectl plugins. It doesn’t even show itself.
I recommend that you check out the available plugins. Some of them are very useful, and they may inspire you to write your own plugins. Let’s see how easy it is to write our own plugin.
Creating your own kubectl plugin
Kubectl plugins can range from super simple to very complicated. I work a lot these days with AKS node pools created using the Cluster API and CAPZ (the Cluster API provider for Azure). I’m often interested in viewing all the node pools on a specific cloud provider. All the node pools are defined as custom resources in a namespace called cluster-registry. The following kubectl command lists all the node pools:
$ k get -n cluster-registry azuremanagedmachinepools.infrastructure.cluster.x-k8s.io
aks-centralus-cluster-001-nodepool001 116d
aks-centralus-cluster-001-nodepool002 116d
aks-centralus-cluster-002-nodepool001 139d
aks-centralus-cluster-002-nodepool002 139d
aks-centralus-cluster-002-nodepool003 139d
...
This is not a lot of information. I’m interested in information like the SKU (VM type and size) of each node pool, its Kubernetes version, and the number of nodes in each node pool. The following kubectl command can provide this information:
$ k get -n cluster-registry azuremanagedmachinepools.infrastructure.cluster.x-k8s.io -o custom-columns=NAME:.metadata.name,SKU:.spec.sku,VERSION:.status.version,NODES:.status.replicas
NAME SKU VERSION NODES
aks-centralus-cluster-001-nodepool001 Standard_D4s_v4 1.23.8 10
aks-centralus-cluster-001-nodepool002 Standard_D8s_v4 1.23.8 20
aks-centralus-cluster-002-nodepool001 Standard_D16s_v4 1.23.8 30
aks-centralus-cluster-002-nodepool002 Standard_D8ads_v5 1.23.8 40
aks-centralus-cluster-002-nodepool003 Standard_D8ads_v5 1.23.8 50
However, this is a lot to type. I simply put this command in a file called kubectl-npa-get and stored it in /usr/local/bin. Now, I can invoke it just by calling k npa get. I could define a little alias or shell function, but a kubectl plugin is more appropriate as it is a central place for all kubectl-related enhancements. It enforces a uniform convention and it is discoverable via kubectl list plugins.
This was an example of an almost trivial kubectl plugin. Let’s look at a more complicated example – deleting namespaces. It turns out that reliably deleting namespaces in Kubernetes is far from trivial. Under certain conditions, a namespace can be stuck forever in a terminating state after you try to delete it. I created a little Go program to reliably delete namespaces. You can check it out here: github.com/the-gigi/k8s-namespace-deleter.
This is a perfect use case for a kubectl plugin. The instructions in the README recommend building the executable and then saving it in your path as kubectl-ns-delete. Now, when you want to delete a namespace, you can just use k ns delete <namespace> to invoke k8s-namespace-deleter and reliably get rid of your namespace.
If you want to develop plugins and share them on Krew, there is a more rigorous process there. I highly recommend developing the plugin in Go and taking advantage of projects like cli-runtime (github.com/kubernetes/cli-runtime/) and krew-plugin-template (github.com/replicatedhq/krew-plugin-template).
Kubectl plugins are awesome, but there are some gotchas you should be aware of. I ran into some of these issues when working with kubectl plugins.
Don’t forget your shebangs!
If you don’t specify a shebang for your shell-based executables, you will get an obscure error message:
$ k npa get
错误:执行格式错误
Naming your plugin
Choosing a name for your plugin is not easy. Luckily, there are some good guidelines: krew.sigs.k8s.io/docs/developer-guide/develop/naming-guide.
Those naming guidelines are not just for Krew plugins, but make sense for any kubectl plugin.
Overriding existing kubectl commands
I originally named the plugin kubectl-get-npa. In theory, kubectl should try to match the longest plugin name to resolve ambiguities. But, apparently, it doesn’t work with built-in commands like kubectl get. This is the error I got:
$ k get npa
error: 服务器没有资源类型 "npa"
Renaming the plugin to kubectl-npa-get solved the problem.
Flat namespace for Krew plugins
The space of kubectl plugins is flat. If you choose a generic plugin name like kubectl-login, you’ll have a lot of problems. Even if you qualify it with something like kubectl-gcp-login, you might conflict with some other plugin. This is a scalability problem. I think the solution should involve a strong naming scheme for plugins like DNS and the ability to define short names and aliases for convenience.
We have covered kubectl plugins, how to write them, and how to use them. Let’s take a look at extending access control with webhooks.
Employing access control webhooks
Kubernetes provides several ways for you to customize access control. In Kubernetes, access control can be denoted with triple-A: Authentication, Authorization, and Admission control. In early versions, access control happened through plugins that required Go programming, installing them into your cluster, registration, and other invasive procedures. Now, Kubernetes lets you customize authentication, authorization, and admission control via web hooks. Here is the access control workflow:

Figure 15.6: Access control workflow
Using an authentication webhook
Kubernetes lets you extend the authentication process by injecting a webhook for bearer tokens. It requires two pieces of information: how to access the remote authentication service and the duration of the authentication decision (it defaults to two minutes).
To provide this information and enable authentication webhooks, start the API server with the following command-line arguments:
- --authentication-token-webhook-config-file=<身份验证配置文件>
- --authentication-token-webhook-cache-ttl(缓存身份验证决策的时间,默认 2 分钟)
The configuration file uses the kubeconfig file format. Here is an example:
# Kubernetes API 版本
apiVersion: v1
# API 对象的类型
kind: Config
# clusters 指的是远程服务。
clusters:
- name: name-of-remote-authn-service
cluster:
certificate-authority: /path/to/ca.pem # 用于验证远程服务的 CA
server: https://authn.example.com/authenticate # 远程服务查询的 URL。必须使用 'https'。
# users 指的是 API 服务器的 webhook 配置。
users:
- name: name-of-api-server
user:
client-certificate: /path/to/cert.pem # 用于 webhook 插件的证书
client-key: /path/to/key.pem # 与证书匹配的密钥
# kubeconfig 文件需要一个上下文。为 API 服务器提供一个。
current-context: webhook
contexts:
- context:
cluster: name-of-remote-authn-service
user: name-of-api-sever
name: webhook
Note that a client certificate and key must be provided to Kubernetes for mutual authentication against the remote authentication service.
The cache TTL is useful because often users will make multiple consecutive requests to Kubernetes. Having the authentication decision cached can save a lot of round trips to the remote authentication service.
When an API HTTP request comes in, Kubernetes extracts the bearer token from its headers and posts a TokenReview JSON request to the remote authentication service via the webhook:
{
"apiVersion": "authentication.k8s.io/v1",
"kind": "TokenReview",
"spec": {
"token": "<来自原始请求头的 bearer token>"
}
}
The remote authentication service will respond with a decision. The status authentication will either be true or false. Here is an example of a successful authentication:
{
"apiVersion": "authentication.k8s.io/v1",
"kind": "TokenReview",
"status": {
"authenticated": true,
"user": {
"username": "gigi@gg.com",
"uid": "42",
"groups": [
"developers",
],
"extra": {
"extrafield1": [
"extravalue1",
"extravalue2"
]
}
}
}
}
A rejected response is much more concise:
{
"apiVersion": "authentication.k8s.io/v1",
"kind": "TokenReview",
"status": {
"authenticated": false
}
}
Using an authorization webhook
The authorization webhook is very similar to the authentication webhook. It requires just a configuration file, which is in the same format as the authentication webhook configuration file. There is no authorization caching because, unlike authentication, the same user may make lots of requests to different API endpoints with different parameters, and authorization decisions may be different, so caching is not a viable option.
You configure the webhook by passing the following command-line argument to the API server:
--authorization-webhook-config-file=<configuration filename>
When a request passes authentication, Kubernetes will send a SubjectAccessReview JSON object to the remote authorization service. It will contain the requesting user (and any user groups it belongs to) and other attributes such as the requested API group, namespace, resource, and verb:
{
"apiVersion": "authorization.k8s.io/v1",
"kind": "SubjectAccessReview",
"spec": {
"resourceAttributes": {
"namespace": "awesome-namespace",
"verb": "get",
"group": "awesome.example.org",
"resource": "pods"
},
"user": "gigi@gg.com",
"group": [
"group1",
"group2"
]
}
}
The request will either be allowed:
{
"apiVersion": "authorization.k8s.io/v1",
"kind": "SubjectAccessReview",
"status": {
"allowed": true
}
}
Or denied with a reason:
{
"apiVersion": "authorization.k8s.io/v1beta1",
"kind": "SubjectAccessReview",
"status": {
"allowed": false,
"reason": "user does not have read access to the namespace"
}
}
A user may be authorized to access a resource, but not some non-resource attributes, such as /api, /apis, /metrics, /resetMetrics, /logs, /debug, /healthz, /swagger-ui/, /swaggerapi/, /ui, and /version.
Here is how to request access to the logs:
{
"apiVersion": "authorization.k8s.io/v1",
"kind": "SubjectAccessReview",
"spec": {
"nonResourceAttributes": {
"path": "/logs",
"verb": "get"
},
"user": "gigi@gg.com",
"group": [
"group1",
"group2"
]
}
}
We can check, using kubectl, if we are authorized to perform an operation using the can-i command. For example, let’s see if we can create deployments:
$ k auth can-i create deployments
yes
We can also check if other users or service accounts are authorized to do something. The default service account is NOT allowed to create deployments:
$ k auth can-i create deployments --as default
no
Using an admission control webhook
Dynamic admission control supports webhooks too. It has been generally available since Kubernetes 1.16. Depending on your Kubernetes version, you may need to enable the MutatingAdmissionWebhook and ValidatingAdmissionWebhook admission controllers using --enable-admission-plugins=Mutating,ValidatingAdmissionWebhook flags to kube-apiserver.
There are several other admission controllers that the Kubernetes developers recommend running (the order matters):
--admission-control=NamespaceLifecycle,LimitRanger,ServiceAccount,DefaultStorageClass,DefaultTolerationSeconds,MutatingAdmissionWebhook,ValidatingAdmissionWebhook,ResourceQuota
In Kubernetes 1.25, these plugins are enabled by default.
Configuring a webhook admission controller on the fly
Authentication and authorization webhooks must be configured when you start the API server. Admission control webhooks can be configured dynamically by creating MutatingWebhookConfiguration or ValidatingWebhookConfiguration API objects. Here is an example:
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
...
webhooks:
- name: admission-webhook.example.com
rules:
- operations: ["CREATE", "UPDATE"]
apiGroups: ["apps"]
apiVersions: ["v1", "v1beta1"]
resources: ["deployments", "replicasets"]
scope: "Namespaced"
...
An admission server accesses AdmissionReview requests such as:
{
"apiVersion": "admission.k8s.io/v1",
"kind": "AdmissionReview",
"request": {
"uid": "705ab4f5-6393-11e8-b7cc-42010a800002",
"kind": {"group":"autoscaling","version":"v1","kind":"Scale"},
"resource": {"group":"apps","version":"v1","resource":"deployments"},
"subResource": "scale",
"requestKind": {"group":"autoscaling","version":"v1","kind":"Scale"},
"requestResource": {"group":"apps","version":"v1","resource":"deployments"},
"requestSubResource": "scale",
"name": "cool-deployment",
"namespace": "cool-namespace",
"operation": "UPDATE",
"userInfo": {
"username": "admin",
"uid": "014fbff9a07c",
"groups": ["system:authenticated","my-admin-group"],
"extra": {
"some-key":["some-value1", "some-value2"]
}
},
"object": {"apiVersion":"autoscaling/v1","kind":"Scale",...},
"oldObject": {"apiVersion":"autoscaling/v1","kind":"Scale",...},
"options": {"apiVersion":"meta.k8s.io/v1","kind":"UpdateOptions",...},
"dryRun": false
}
}
If the request is admitted, the response will be:
{
"apiVersion": "admission.k8s.io/v1",
"kind": "AdmissionReview",
"response": {
"uid": "<value from request.uid>",
"allowed": true
}
}
If the request is not admitted, then allowed will be False. The admission server may provide a status section too with an HTTP status code and message:
{
"apiVersion": "admission.k8s.io/v1",
"kind": "AdmissionReview",
"response": {
"uid": "<value from request.uid>",
"allowed": false,
"status": {
"code": 403,
"message": "You cannot do this because I say so!!!!"
}
}
}
That concludes our discussion of dynamic admission control. Let’s look at some more extension points.
Additional extension points
There are some additional extension points that don’t fit into the categories we have discussed so far.
Providing custom metrics for horizontal pod autoscaling
Prior to Kubernetes 1.6, custom metrics were implemented as a Heapster model. In Kubernetes 1.6, new custom metrics APIs landed and matured gradually. As of Kubernetes 1.9, they are enabled by default. As you may recall, Keda (keda.sh) is a project that focuses on custom metrics for autoscaling. However, if for some reason Keda doesn’t meet your needs, you can implement your own custom metrics. Custom metrics rely on API aggregation. The recommended path is to start with the custom metrics API server boilerplate, available here: github.com/kubernetes-sigs/custom-metrics-apiserver.
Then, you can implement the CustomMetricsProvider interface:
type CustomMetricsProvider interface {
// GetRootScopedMetricByName 获取特定根范围对象的特定指标。
GetRootScopedMetricByName(groupResource schema.GroupResource, name string, metricName string) (*custom_metrics.MetricValue, error)
// GetRootScopedMetricByName 获取一组根范围对象的特定指标
// 匹配给定的标签选择器。
GetRootScopedMetricBySelector(groupResource schema.GroupResource, selector labels.Selector, metricName string) (*custom_metrics.MetricValueList, error)
// GetNamespacedMetricByName 获取特定命名空间对象的特定指标。
GetNamespacedMetricByName(groupResource schema.GroupResource, namespace string, name string, metricName string) (*custom_metrics.MetricValue, error)
// GetNamespacedMetricByName 获取一组命名空间对象的特定指标
// 匹配给定的标签选择器。
GetNamespacedMetricBySelector(groupResource schema.GroupResource, namespace string, selector labels.Selector, metricName string) (*custom_metrics.MetricValueList, error)
// ListAllMetrics 提供所有可用指标的列表
// 当前时间。请注意,不允许返回此信息
// 一个错误,因此建议实现者缓存并
// 定期更新此列表,而不是每次查询。
ListAllMetrics() []CustomMetricInfo
}
Extending Kubernetes with custom storage
Volume plugins are yet another type of plugin. Prior to Kubernetes 1.8, you had to write a kubelet plugin, which required registration with Kubernetes and linking with the kubelet. Kubernetes 1.8 introduced the FlexVolume, which is much more versatile. Kubernetes 1.9 took it to the next level with the CSI, which we covered in Chapter 6, Managing Storage. At this point, if you need to write storage plugins, the CSI is the way to go. Since the CSI uses the gRPC protocol, the CSI plugin must implement the following gRPC interface:
service Controller {
rpc CreateVolume (CreateVolumeRequest)
returns (CreateVolumeResponse) {}
rpc DeleteVolume (DeleteVolumeRequest)
returns (DeleteVolumeResponse) {}
rpc ControllerPublishVolume (ControllerPublishVolumeRequest)
returns (ControllerPublishVolumeResponse) {}
rpc ControllerUnpublishVolume (ControllerUnpublishVolumeRequest)
returns (ControllerUnpublishVolumeResponse) {}
rpc ValidateVolumeCapabilities (ValidateVolumeCapabilitiesRequest)
returns (ValidateVolumeCapabilitiesResponse) {}
rpc ListVolumes (ListVolumesRequest)
returns (ListVolumesResponse) {}
rpc GetCapacity (GetCapacityRequest)
returns (GetCapacityResponse) {}
rpc ControllerGetCapabilities (ControllerGetCapabilitiesRequest)
returns (ControllerGetCapabilitiesResponse) {}
}
这不是一项简单的工作,通常只有存储解决方案提供商才应该实现 CSI 插件。
自定义指标和自定义存储解决方案的额外扩展点展示了 Kubernetes 致力于真正可扩展,并允许用户自定义其几乎所有操作方面的承诺。
Summary
在本章中,我们覆盖了三个主要话题:与 Kubernetes API 的交互、扩展 Kubernetes API 和编写 Kubernetes 插件。Kubernetes API 支持 OpenAPI 规范,是一个很好的 REST API 设计示例,遵循了所有当前的最佳实践。它非常一致,组织良好,文档完善。然而,它是一个庞大的 API,并不容易理解。你可以通过 HTTP 的 REST 接口直接访问 API,使用包括官方 Python 客户端在内的客户端库,甚至通过编程调用 kubectl。
扩展 Kubernetes API 可能涉及定义自定义资源、编写控制器/操作符,并可选择通过 API 聚合来扩展 API 服务器本身。
插件和 webhooks 是 Kubernetes 设计的基础。Kubernetes 一直旨在通过用户扩展以满足各种需求。我们查看了各种插件,如自定义调度器、kubectl 插件和访问控制 webhooks。Kubernetes 提供了一个无缝的体验,支持编写、注册和集成所有这些插件,真是太棒了。
我们还研究了自定义指标,甚至讨论了如何通过自定义存储选项来扩展 Kubernetes。
到这一点,你应该已经清楚了通过 API 访问、自定义资源、控制器、操作符和自定义插件扩展、定制和控制 Kubernetes 的所有主要机制。你已经处于一个非常有利的位置,可以利用这些能力来增强 Kubernetes 的现有功能,并根据你的需求和系统进行调整。
在下一章中,我们将探讨如何通过策略引擎管理 Kubernetes。这将继续扩展 Kubernetes 的主题,因为策略引擎是增强版的动态准入控制器。我们将介绍治理的概念,回顾现有的策略引擎,并深入分析 Kyverno,它是我认为最适合 Kubernetes 的策略引擎。
第十六章:Kubernetes 治理
在上一章中,我们详细讨论了扩展 Kubernetes 的不同方式,包括在接纳控制阶段验证和变更请求。
在这一章中,我们将学习 Kubernetes 在大型企业组织中日益增长的作用,什么是治理,以及它在 Kubernetes 中的应用。我们将探讨策略引擎,回顾一些流行的引擎,并深入了解 Kyverno。
这与上一章的内容相得益彰,因为策略引擎是建立在 Kubernetes 接纳控制机制之上的。
越来越多的企业组织将更多的资源投入到 Kubernetes 平台。这些大型组织有着严格的安全性、合规性和治理需求。Kubernetes 的策略引擎应运而生,旨在解决这些问题,并确保企业组织能够完全拥抱 Kubernetes。
我们将涵盖以下主题:
-
Kubernetes 在企业中的应用
-
什么是 Kubernetes 治理?
-
策略引擎
-
深入了解 Kyverno
让我们直接进入并了解 Kubernetes 在企业中的日益重要性。
Kubernetes 在企业中的应用
Kubernetes 平台的推广和采用率是前所未有的。它于 2016 年正式推出,仅仅几年时间,它已经征服了基础设施的世界。在最近的 CNCF 调查中,96%的参与组织正在使用或评估 Kubernetes。Kubernetes 的渗透率跨越了多个维度:组织规模、地理位置,以及生产环境和非生产环境。更令人印象深刻的是,Kubernetes 可以深入底层,成为其他技术和平台的基础。
你可以从所有云服务提供商广泛采用 Kubernetes 平台中看到这一点,许多提供商也推出了各种托管 Kubernetes 平台即服务的产品。请查看 CNCF 认证的 Kubernetes 软件合规性列表:www.cncf.io/certification/software-conformance。
拥有多个认证供应商、增值经销商、多个公司的生态系统等,对于企业组织来说极为重要。企业组织不仅仅需要最新的闪亮技术。风险很高,大型基础设施项目的失败率也很高,失败的后果十分严峻。将这些因素结合起来,结果是企业组织在技术方面非常抗拒变革,且回避风险。许多在交通控制、保险、医疗、通信系统和航空等领域的关键软件系统,依然运行在 40 至 50 年前编写的软件上,使用的是 COBOL 和 Fortran 等语言。
企业软件的需求
让我们看一下企业软件的一些需求:
-
处理大量数据
-
与其他系统和应用集成
-
提供强大的安全功能
-
可扩展性和可用性
-
灵活且可定制
-
合规性
-
获得受信任供应商的支持
-
拥有强大的治理(稍后将详细讨论)
Kubernetes 如何符合要求?
Kubernetes 与企业软件
Kubernetes 在企业软件领域的使用增长如此迅猛,原因在于它实际上满足了所有需求并不断改进。
作为容器编排平台的事实标准,它可以作为所有基于容器的部署的基础。它的生态系统满足任何集成需求,因为每个供应商都必须能够在 Kubernetes 上运行。Kubernetes 的长期前景极为广阔,因为它是众多公司和组织的集体努力,并且由一个开放且成功的过程推动,不断地交付成果。Kubernetes 引领着向多云和混合云部署的转变,遵循行业广泛的标准。
Kubernetes 的扩展性和灵活性意味着它可以满足特定企业所需的任何定制需求。
它真的是一个了不起的项目,设计上基于坚实的概念架构,能够在现实世界中始终如一地交付结果。
到此为止,Kubernetes 对于企业组织来说显然是非常合适的,但它如何满足治理的需求呢?
什么是 Kubernetes 治理?
治理是企业组织的重要需求之一。简而言之,它意味着控制组织的运作方式。治理的某些要素包括:
-
政策
-
伦理
-
进程
-
风险管理
-
管理
治理包括一种指定政策和机制以执行这些政策的方式,以及报告和审计。让我们来看看 Kubernetes 中治理的各个领域和实践。
镜像管理
容器运行的是嵌入在镜像中的软件。管理这些镜像是运行基于 Kubernetes 的系统中的一项关键活动。有几个方面需要考虑:如何构建镜像?如何审核第三方镜像?镜像存储在哪里?在这些决策上做出不当选择可能会影响系统的性能(例如,如果你使用了过大或臃肿的基础镜像),并且至关重要的是影响系统的安全性(例如,如果你使用了被破坏或存在漏洞的基础镜像)。镜像管理政策可以强制进行镜像扫描,或者确保你只能使用来自特定镜像注册表的经过审查的镜像。
Pod 安全
Kubernetes 的工作单元是 pod。你可以为 pod 及其容器设置许多安全设置。不幸的是,默认的安全设置非常宽松。验证并强制执行 pod 安全策略可以解决这个问题。Kubernetes 对 pod 安全标准有强有力的支持和指导,并且提供了多个内置的安全配置文件。每个 pod 都有一个安全上下文,正如我们在第四章《确保 Kubernetes 安全》中讨论的那样。
请参阅 kubernetes.io/docs/concepts/security/pod-security-standards/ 获取更多详细信息。
网络策略
Kubernetes 网络策略在 OSI 网络模型的第 3 层和第 4 层(IP 地址和端口)之间控制 pod 和其他网络实体之间的流量。网络实体可以是具有特定标签集的 pod,也可以是某个命名空间中所有具有特定标签集的 pod。最后,网络策略还可以阻止 pod 访问特定的 IP 块。
在治理的背景下,网络策略可以通过控制 pod 与其他资源之间的网络访问和通信来强制执行安全性和合规性要求。
例如,网络策略可以用来防止 pod 与某些外部网络进行通信。网络策略还可以用来强制执行职能分离,并防止未经授权的访问集群中的敏感资源。
请参阅 kubernetes.io/docs/concepts/services-networking/network-policies/ 获取更多详细信息。
配置约束
Kubernetes 非常灵活,提供了许多控制选项,涵盖其操作的各个方面。在基于 Kubernetes 的系统中,DevOps 实践通常允许团队对工作负载的部署方式、扩展方式以及使用的资源拥有大量控制权。Kubernetes 提供了诸如配额和限制之类的配置约束。借助更先进的准入控制器,你可以验证并强制执行控制资源创建任何方面的策略,例如自动扩展部署的最大大小、持久卷声明的总量,并要求内存请求始终等于内存限制(虽然这不一定是个好主意)。
RBAC 和准入控制
Kubernetes RBAC(基于角色的访问控制)在资源和操作级别运行。每个 Kubernetes 资源都有可执行的操作(动词)。使用 RBAC,你可以定义角色,这些角色是对资源的权限集合,可以在命名空间级别或集群级别应用。它是一个粗粒度的工具,但非常方便,特别是当你在命名空间级别划分资源,并仅使用集群级别权限来管理跨整个集群运行的工作负载时。
如果你需要更细粒度的控制,依赖于资源的特定属性,那么准入控制器可以处理此类需求。我们将在本章后续部分探讨此选项,讨论策略引擎时会详细说明。
策略管理
治理是围绕策略建立的。管理所有这些策略、组织它们,并确保它们满足组织的治理需求需要大量的努力,并且是一个持续的任务。准备好为不断发展和维护你的策略投入资源。
策略验证与执行
一旦一组策略确定下来,你需要验证 Kubernetes API 服务器的请求是否符合这些策略,并拒绝违反这些政策的请求。还有一种实施策略的方法是通过修改传入请求,使其符合政策。例如,如果一项政策要求每个 Pod 的内存请求最多为 2 GiB,那么一个变更策略可以将内存请求大于 2 GiB 的 Pod 的内存请求减少到 2 GiB。
政策不必是僵化的。对于特殊情况,可以做出例外和排除。
报告
当你管理大量的政策并审核所有请求时,了解你的政策如何帮助治理系统、预防问题并从使用模式中学习是很重要的。报告可以通过捕捉和汇总政策决策的结果来提供洞察。作为人工用户,你可以查看关于政策违规、被拒绝和被修改请求的报告,并检测趋势或异常。在更高层次上,你可以采用自动化分析,包括基于机器学习的模型,从大量详细报告中提取意义。
审计
Kubernetes 审计日志提供了每个事件的时间戳详细记录。当你将审计数据与治理报告结合时,可以拼凑出事件的时间线,特别是安全事件,通过结合多个来源的数据,从政策违规开始,最终追溯到根本原因,从而识别罪魁祸首。
到目前为止,我们已经涵盖了治理的基本概念及其与 Kubernetes 的具体关系。我们强调了政策在治理系统中的重要性。接下来,我们将探讨策略引擎以及它们如何实现这些概念。
策略引擎
Kubernetes 中的策略引擎提供了全面的治理需求覆盖,并补充了内建机制,如网络策略和 RBAC。策略引擎可以验证并确保系统遵循最佳实践,遵守安全指南,并符合外部政策。在这一部分,我们将介绍准入控制作为策略引擎接入系统的主要机制,策略引擎的职责,以及现有策略引擎的回顾。接下来,我们将深入探讨其中最优秀的策略引擎之一——Kyverno。
准入控制作为策略引擎的基础
准入控制是请求进入 Kubernetes API 服务器生命周期的一部分。我们在第十五章《扩展 Kubernetes》中进行了深入讨论。如你所记得,动态准入控制器是监听准入审核请求的 Webhook 服务器,能够接受、拒绝或修改请求。策略引擎首先是复杂的准入控制器,它们注册并监听与其策略相关的所有请求。
当请求到来时,策略引擎会应用所有相关的策略来决定请求的处理方式。例如,如果某个策略确定只有在名为load_balancer的命名空间中,才可以创建LoadBalancer类型的 Kubernetes 服务,那么策略引擎会注册监听所有 Kubernetes 服务创建和更新请求。当服务创建或更新请求到达时,策略引擎会检查服务的类型和命名空间。如果服务类型是LoadBalancer且命名空间不是load_balancer,则策略引擎会拒绝该请求。请注意,这种操作无法通过 RBAC 完成。因为 RBAC 无法查看服务的类型来确定请求是否有效。
现在我们理解了策略引擎如何利用 Kubernetes 的动态准入控制过程,让我们来看看策略引擎的职责。
策略引擎的职责
策略引擎是对 Kubernetes 基础系统执行治理的主要工具。策略引擎应该允许管理员定义超出内建 Kubernetes 策略(如 RBAC 和网络策略)的策略。这通常意味着需要创建一个策略声明语言。该策略声明语言需要足够丰富,能够覆盖 Kubernetes 的所有细节,包括针对不同资源的细粒度应用,以及获取所有相关信息以便对每个资源的接受或拒绝做出决策。
策略引擎还应该提供一种组织、查看和管理策略的方式。理想情况下,策略引擎提供了一种在将策略应用到实际集群之前测试策略的良好方式。
策略引擎必须提供一种将策略部署到集群的方法,并且当然,它需要应用与每个请求相关的策略,决定该请求是否应该按原样接受、拒绝或修改(变异)。策略引擎还可以提供一种在请求到来时生成额外资源的方式。例如,当创建新的 Kubernetes 部署时,策略引擎可能会自动为该部署生成一个水平 Pod 自动扩展器。策略引擎还可以监听集群中发生的事件并采取行动。请注意,这种能力超出了动态准入控制的范围,但它仍然会在集群上执行策略。
让我们回顾一下几个 Kubernetes 的策略引擎,以及它们是如何履行这些责任的。
开源策略引擎的快速回顾
在评估解决方案时,提前制定评估标准非常有帮助,因为政策引擎可能会深刻影响 Kubernetes 集群的运行,而集群工作负载的成熟度是一个关键要素。优秀的文档也至关重要,因为政策引擎的范围非常广泛,你需要了解如何与其协作。政策引擎的功能决定了它能处理哪些用例。编写政策是管理员向政策引擎传达治理意图的方式。评估编写和测试政策的用户体验,以及支持这些活动的工具也很重要。将政策部署到集群中是另一个必不可少的元素。最后,查看报告并理解治理状态可能会被忽视。
我们将根据以下维度评估五种政策引擎。
OPA/Gatekeeper
开放政策代理 (OPA) 是一个通用的政策引擎,超越了 Kubernetes(www.openpolicyagent.org)。它的范围非常广泛,能够处理任何 JSON 值。
Gatekeeper(open-policy-agent.github.io/gatekeeper)通过将 OPA 政策引擎打包成一个准入控制 webhook,将其引入 Kubernetes。
OPA/Gatekeeper 无疑是最成熟的政策引擎。它创建于 2017 年,是一个已毕业的 CNCF 项目,在撰写本文时,它在 GitHub 上有 2.9k 个星标。它甚至被用作 Azure AKS 上政策的基础。详见 learn.microsoft.com/en-us/azure/governance/policy/concepts/policy-for-kubernetes。
OPA 有自己独特的语言,叫做 Rego(www.openpolicyagent.org/docs/latest/policy-language/),用来定义政策。Rego 有强大的理论基础,受到 Datalog 的启发,但它可能不太直观,理解起来也不容易。
下图展示了 OPA/Gatekeeper 的架构:

图 16.1:OPA/Gatekeeper 架构
总体而言,OPA/Gatekeeper 非常强大,但与其他 Kubernetes 政策引擎相比,似乎有些笨重,因为 OPA 政策引擎是通过 Gatekeeper 被附加到 Kubernetes 上的。
OPA/Gatekeeper 的文档较为一般,导航不太方便。不过,它确实有一个可以作为起点使用的政策库。
然而,如果你看重其成熟度,并且不太担心使用 Rego 和一些摩擦,它可能是一个不错的选择。
Kyverno
Kyverno (kyverno.io) 是一个成熟且强大的策略引擎,专门为 Kubernetes 从一开始就设计。它创建于 2019 年,并在此之后取得了巨大进展。它是一个 CNCF 孵化项目,在 GitHub 上的受欢迎程度已经超过了 OPA/Gatekeeper,截至写作时有 3.3k 个星标。Kyverno 使用 YAML JMESPath (jmespath.org) 来定义策略,实际上这些策略只是 Kubernetes 自定义资源。它有优秀的文档支持,并且提供了许多示例来帮助你开始编写自己的策略。
总体而言,Kyverno 功能强大且易于使用。它背后有巨大的支持势头,持续不断地改进和提升其性能与规模化操作。在我看来,它目前是最好的 Kubernetes 策略引擎。我们将在本章后续部分深入探讨 Kyverno。
jsPolicy
jsPolicy (www.jspolicy.com) 是 Loft 推出的一个有趣项目,它为 Kubernetes 社区带来了虚拟集群。它的亮点在于,策略在一个安全且高效的浏览器式沙箱中运行,而你可以用 JavaScript 或 TypeScript 来定义策略。这个方法非常新颖,项目也很精致、简洁,且有很好的文档支持。不幸的是,Loft 似乎将精力放在其他项目上,jsPolicy 并没有受到太多关注。在写作时,它在 GitHub 上只有 242 个星标 (github.com/loft-sh/jspolicy),而且最后一次提交是在 6 个月前。
利用 JavaScript 生态系统来打包和共享策略,并利用其强大的工具来测试和调试策略,这个思路有很多优点。
jsPolicy 提供验证、变异和控制策略。控制策略允许你响应集群中发生的事件,超出了准入控制的范围。
以下图表展示了 jsPolicy 的架构:

图 16.2:jsPolicy 架构
目前,我不会承诺使用 jsPolicy,因为它可能已经被放弃。然而,如果 Loft 或其他人决定投资于它,它可能成为 Kubernetes 策略引擎领域的一个有力竞争者。
Kubewarden
Kubewarden (www.kubewarden.io) 是另一个创新的策略引擎。它是一个 CNCF 沙箱项目。Kubewarden 专注于语言无关性,允许你用多种语言编写策略。然后,这些策略会被打包成 WebAssembly 模块,存储在任何 OCI 注册表中。
理论上,你可以使用任何可以编译成 WebAssembly 的语言。实际上,以下语言被支持,但有一些限制:
-
Rust(当然,这是最成熟的)
-
Go(你需要使用一个特殊的编译器 TinyGo,它不支持 Go 的所有功能)
-
Rego(直接使用 OPA 或 Gatekeeper – 缺少变异策略)
-
Swift(使用 SwiftWasm,需进行一些后构建优化)
-
TypeScript(或更准确地说,是一个名为 AssemblyScript 的子集)
Kubewarden 支持验证、变更和上下文感知策略。上下文感知策略是指利用额外信息来判断是否应该允许请求。额外信息可能包括集群中存在的命名空间、服务和入口等列表。
Kubewarden 有一个 CLI 工具叫做 kwctl(github.com/kubewarden/kwctl),用于管理你的策略。
下面是 Kubewarden 架构的示意图:

图 16.3:Kubewarden 架构
Kubewarden 仍在不断发展和成长。它有一些不错的创意和动机,但在这个阶段,如果你是 Rust 语言的支持者并偏爱用 Rust 编写策略,它可能最适合你。
现在我们已经了解了 Kubernetes 开源策略引擎的整体情况,让我们深入探讨并仔细了解 Kyverno。
Kyverno 深入分析
Kyverno 是 Kubernetes 策略引擎领域的一颗新星。让我们动手实践一下,看看它如何工作,以及为什么它如此受欢迎。在这一节中,我们将介绍 Kyverno,安装它,并学习如何编写、应用和测试策略。
Kyverno 简介
Kyverno 是一个专门为 Kubernetes 设计的策略引擎。如果你有使用 kubectl、Kubernetes 清单或 YAML 的经验,那么 Kyverno 会让你感觉非常熟悉。你通过 YAML 清单和 JMESPath 语言定义策略和配置,后者与 kubectl 的 JSONPATH 格式非常接近。
以下图示展示了 Kyverno 架构:

图 16.4:Kyverno 架构
Kyverno 覆盖了很多领域并拥有许多功能:
-
使用 GitOps 进行策略管理
-
资源验证(拒绝无效资源)
-
资源变更(修改无效资源)
-
资源生成(自动生成额外的资源)
-
验证容器镜像(对软件供应链安全非常重要)
-
检查镜像元数据
-
使用标签选择器和通配符匹配和排除资源(Kubernetes 原生)
-
使用叠加层验证和变更资源(类似 Kustomize!)
-
在命名空间间同步配置
-
以报告或强制模式运行
-
使用动态准入 Webhook 应用策略
-
在 CI/CD 阶段使用 Kyverno CLI 应用策略
-
使用 Kyverno CLI 进行临时策略测试和资源验证
-
高可用模式
-
失败时开放或关闭(在 Kyverno 准入 Webhook 不可用时,允许或拒绝资源)
-
策略违规报告
-
提供 Web UI,方便可视化
-
可观察性支持
这是一个令人印象深刻的功能和能力列表。Kyverno 的开发者不断发展和改进它。Kyverno 在可扩展性、性能和处理大量策略与资源的能力方面取得了巨大进展。
让我们安装 Kyverno 并进行配置。
安装和配置 Kyverno
Kyverno 遵循与 Kubernetes 本身类似的升级策略,其中节点组件版本最多只能比控制平面版本低两个小版本。撰写时,Kyverno 1.8 是最新版本,支持 Kubernetes 版本 1.23–1.25。
我们可以使用 kubectl 或 Helm 安装 Kyverno。我们选择 Helm 选项:
$ helm repo add kyverno https://kyverno.github.io/kyverno/
"kyverno" has been added to your repositories
$ helm repo update
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "kyverno" chart repository
Update Complete. Happy Helming!
让我们安装 Kyverno,使用默认的单副本模式,安装到它自己的命名空间中。使用单副本不推荐用于生产环境,但对于实验 Kyverno 来说是可以的。要以高可用模式安装,添加 --set replicaCount=3 标志:
$ helm install kyverno kyverno/kyverno -n kyverno --create-namespace
NAME: kyverno
LAST DEPLOYED: Sat Dec 31 15:34:11 2022
NAMESPACE: kyverno
STATUS: deployed
REVISION: 1
NOTES:
Chart version: 2.6.5
Kyverno version: v1.8.5
Thank you for installing kyverno! Your release is named kyverno.
 WARNING: Setting replicas count below 3 means Kyverno is not running in high availability mode.
 Note: There is a trade-off when deciding which approach to take regarding Namespace exclusions. Please see the documentation at https://kyverno.io/docs/installation/#security-vs-operability to understand the risks.
让我们使用 ketall kubectl 插件观察一下刚刚安装的内容:(github.com/corneliusweig/ketall):
$ k get-all -n kyverno
NAME NAMESPACE AGE
configmap/kube-root-ca.crt kyverno 2m27s
configmap/kyverno kyverno 2m26s
configmap/kyverno-metrics kyverno 2m26s
endpoints/kyverno-svc kyverno 2m26s
endpoints/kyverno-svc-metrics kyverno 2m26s
pod/kyverno-7c444878f7-gfht8 kyverno 2m26s
secret/kyverno-svc.kyverno.svc.kyverno-tls-ca kyverno 2m22s
secret/kyverno-svc.kyverno.svc.kyverno-tls-pair kyverno 2m21s
secret/sh.helm.release.v1.kyverno.v1 kyverno 2m26s
serviceaccount/default kyverno 2m27s
serviceaccount/kyverno kyverno 2m26s
service/kyverno-svc kyverno 2m26s
service/kyverno-svc-metrics kyverno 2m26s
deployment.apps/kyverno kyverno 2m26s
replicaset.apps/kyverno-7c444878f7 kyverno 2m26s
lease.coordination.k8s.io/kyverno kyverno 2m23s
lease.coordination.k8s.io/kyverno-health kyverno 2m13s
lease.coordination.k8s.io/kyvernopre kyverno 2m25s
lease.coordination.k8s.io/kyvernopre-lock kyverno 2m24s
endpointslice.discovery.k8s.io/kyverno-svc-7ghzl kyverno 2m26s
endpointslice.discovery.k8s.io/kyverno-svc-metrics-qflr5 kyverno 2m26s
rolebinding.rbac.authorization.k8s.io/kyverno:leaderelection kyverno 2m26s
role.rbac.authorization.k8s.io/kyverno:leaderelection kyverno 2m26s
如你所见,Kyverno 安装了所有预期的资源:部署、服务、角色及角色绑定、配置映射和密钥。我们可以看出,Kyverno 还暴露了度量指标并使用了领导者选举。
此外,Kyverno 还安装了许多 CRD(在集群范围内):
$ k get crd
NAME CREATED AT
admissionreports.kyverno.io 2022-12-31T23:34:12Z
backgroundscanreports.kyverno.io 2022-12-31T23:34:12Z
clusteradmissionreports.kyverno.io 2022-12-31T23:34:12Z
clusterbackgroundscanreports.kyverno.io 2022-12-31T23:34:12Z
clusterpolicies.kyverno.io 2022-12-31T23:34:12Z
clusterpolicyreports.wgpolicyk8s.io 2022-12-31T23:34:12Z
generaterequests.kyverno.io 2022-12-31T23:34:12Z
policies.kyverno.io 2022-12-31T23:34:12Z
policyreports.wgpolicyk8s.io 2022-12-31T23:34:12Z
updaterequests.kyverno.io 2022-12-31T23:34:12Z
最后,Kyverno 配置了几个准入控制 Webhook:
$ k get validatingwebhookconfigurations
NAME WEBHOOKS AGE
kyverno-policy-validating-webhook-cfg 1 40m
kyverno-resource-validating-webhook-cfg 1 40m
$ k get mutatingwebhookconfigurations
NAME WEBHOOKS AGE
kyverno-policy-mutating-webhook-cfg 1 40m
kyverno-resource-mutating-webhook-cfg 0 40m
kyverno-verify-mutating-webhook-cfg 1 40m
下图展示了典型的 Kyverno 安装结果:

图 16.5:典型的 Kyverno 安装
安装 pod 安全策略
Kyverno 拥有一个庞大的预构建策略库。我们也可以使用 Helm 安装 pod 安全标准策略(见 kyverno.io/policies/pod-security/):
$ helm install kyverno-policies kyverno/kyverno-policies -n kyverno-policies --create-namespace
NAME: kyverno-policies
LAST DEPLOYED: Sat Dec 31 15:48:26 2022
NAMESPACE: kyverno-policies
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
Thank you for installing kyverno-policies 2.6.5 
We have installed the "baseline" profile of Pod Security Standards and set them in audit mode.
Visit https://kyverno.io/policies/ to find more sample policies.
注意,策略本身是集群策略,不会在命名空间 kyverno-policies 中显示:
$ k get clusterpolicies.kyverno.io
NAME BACKGROUND VALIDATE ACTION READY
disallow-capabilities true audit true
disallow-host-namespaces true audit true
disallow-host-path true audit true
disallow-host-ports true audit true
disallow-host-process true audit true
disallow-privileged-containers true audit true
disallow-proc-mount true audit true
disallow-selinux true audit true
restrict-apparmor-profiles true audit true
restrict-seccomp true audit true
restrict-sysctls true audit true
我们稍后会深入回顾其中的一些策略。首先,让我们看看如何配置 Kyverno。
配置 Kyverno
你可以通过编辑 Kyverno 配置映射来配置 Kyverno 的行为:
$ k get cm kyverno -o yaml -n kyverno | yq .data
resourceFilters: '[*,kyverno,*][Event,*,*][*,kube-system,*][*,kube-public,*][*,kube-node-lease,*][Node,*,*][APIService,*,*][TokenReview,*,*][SubjectAccessReview,*,*][SelfSubjectAccessReview,*,*][Binding,*,*][ReplicaSet,*,*][AdmissionReport,*,*][ClusterAdmissionReport,*,*][BackgroundScanReport,*,*][ClusterBackgroundScanReport,*,*][ClusterRole,*,kyverno:*][ClusterRoleBinding,*,kyverno:*][ServiceAccount,kyverno,kyverno][ConfigMap,kyverno,kyverno][ConfigMap,kyverno,kyverno-metrics][Deployment,kyverno,kyverno][Job,kyverno,kyverno-hook-pre-delete][NetworkPolicy,kyverno,kyverno][PodDisruptionBudget,kyverno,kyverno][Role,kyverno,kyverno:*][RoleBinding,kyverno,kyverno:*][Secret,kyverno,kyverno-svc.kyverno.svc.*][Service,kyverno,kyverno-svc][Service,kyverno,kyverno-svc-metrics][ServiceMonitor,kyverno,kyverno-svc-service-monitor][Pod,kyverno,kyverno-test]'
webhooks: '[{"namespaceSelector": {"matchExpressions":
[{"key":"kubernetes.io/metadata.name","operator":"NotIn","values":["kyverno"]}]}}]'
resourceFilters 标志是一个格式为 [kind,namespace,name] 的列表,其中每个元素也可以是通配符,用于告诉 Kyverno 哪些资源应被忽略。匹配任何过滤器的资源将不受任何 Kyverno 策略的约束。如果你有大量策略,这是个良好的实践,可以节省评估所有策略的工作量。
webHooks 标志允许你过滤掉整个命名空间。
excludeGroupRole 标志是一个由逗号分隔的角色字符串。它将排除具有指定角色的用户的请求,这些请求将不经过 Kyverno 准入控制。默认列表为 system:serviceaccounts:kube-system,system:nodes,system:kube-scheduler。
excludeUsername 标志表示一个由逗号分隔的 Kubernetes 用户名字符串。当用户在 generate policy 中启用 Synchronize 时,Kyverno 成为唯一能够更新或删除生成资源的实体。然而,管理员可以排除特定的用户名,使其无法访问删除/更新生成资源的功能。
generateSuccessEvents 标志是一个布尔参数,用于确定是否应生成成功事件。默认情况下,此标志设置为 false,表示不会生成成功事件。
此外,Kyverno 容器提供了几个可以配置的容器参数,以自定义其行为和功能。这些参数允许精细调整和定制 Kyverno 在容器内的行为。你可以编辑 Kyverno 部署中的参数列表:
$ k get deploy kyverno -n kyverno -o yaml | yq '.spec.template.spec.containers[0].args'
- --autogenInternals=true
- --loggingFormat=text
除了预配置的 --autogenInternals 和 --loggingFormat 外,还有以下标志可用:
-
admissionReports -
allowInsecureRegistry -
autoUpdateWebhooks -
backgroundScan -
clientRateLimitBurst -
clientRateLimitQPS -
disableMetrics -
enableTracing -
genWorkers -
imagePullSecrets -
imageSignatureRepository -
kubeconfig -
maxQueuedEvents -
metricsPort -
otelCollector -
otelConfig -
profile -
profilePort -
protectManagedResources -
reportsChunkSize -
serverIP -
splitPolicyReport(已弃用 – 将在 1.9 版本中移除) -
transportCreds -
webhookRegistrationTimeout -
webhookTimeout
所有标志都有默认值,只有在你想覆盖默认值时才需要指定它们。
查看 kyverno.io/docs/installation/#container-flags 以获取每个标志的详细信息。
我们安装了 Kyverno,观察了它安装的各种资源,并查看了它的配置。现在是时候查看 Kyverno 的策略和规则了。
应用 Kyverno 策略
在用户级别,Kyverno 的工作单位是策略。你可以将策略作为 Kubernetes 资源应用,编写和编辑自己的策略,并使用 Kyverno CLI 测试策略。
应用 Kyverno 策略就像应用其他资源一样简单。让我们看一下我们之前安装的一个策略:
$ k get clusterpolicies.kyverno.io disallow-capabilities
NAME BACKGROUND VALIDATE ACTION READY
disallow-capabilities true audit true
该策略的目的是防止 Pod 请求超出允许列表的额外 Linux 能力(见 linux-audit.com/linux-capabilities-101/)。不允许的能力之一是 NET_ADMIN。让我们创建一个请求该能力的 Pod:
$ cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: some-pod
spec:
containers:
- name: some-container
command: [ "sleep", "999999" ]
image: g1g1/py-kube:0.3
securityContext:
capabilities:
add: ["NET_ADMIN"]
EOF
pod/some-pod created
Pod 已创建,我们可以验证它是否具有 NET_ADMIN 能力。我使用的是 kind 集群,所以集群节点只是一个 Docker 进程,我们可以进入该进程:
$ docker exec -it kind-control-plane sh
#
现在我们已经进入节点内部的 shell,我们可以搜索容器的进程,它会休眠 999,999 秒:
# ps aux | grep 'PID\|sleep' | grep -v grep
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 4549 0.0 0.0 148276 6408 ? Ssl 02:54 0:00 /usr/bin/qemu-x86_64 /bin/sleep 999999
让我们检查一下进程 4549 的能力:
# getpcaps 4549
4549: cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_admin,cap_net_raw,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap=ep
如你所见,cap_net_admin 已经存在。
Kyverno 没有阻止创建 pod,因为策略仅在审计模式下运行:
$ k get clusterpolicies.kyverno.io disallow-capabilities -o yaml | yq .spec.validationFailureAction
audit
让我们删除 pod 并将策略更改为“强制”模式:
$ k delete po some-pod
pod "some-pod" deleted
$ k patch clusterpolicies.kyverno.io disallow-capabilities --type merge -p '{"spec": {"validationFailureAction": "enforce"}}'
clusterpolicy.kyverno.io/disallow-capabilities patched
现在,如果我们尝试再次创建 pod,结果会截然不同:
$ cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: some-pod
spec:
containers:
- name: some-container
command: [ "sleep", "999999" ]
image: g1g1/py-kube:0.3
securityContext:
capabilities:
add: ["NET_ADMIN"]
EOF
Error from server: error when creating "STDIN": admission webhook "validate.kyverno.svc-fail" denied the request:
policy Pod/kyverno-policies/some-pod for resource violation:
disallow-capabilities:
adding-capabilities: Any capabilities added beyond the allowed list (AUDIT_WRITE,
CHOWN, DAC_OVERRIDE, FOWNER, FSETID, KILL, MKNOD, NET_BIND_SERVICE, SETFCAP, SETGID,
SETPCAP, SETUID, SYS_CHROOT) are disallowed.
Kyverno 准入 webhook 强制执行策略并拒绝了 pod 的创建。甚至告诉我们是哪个策略负责(disallow-capabilities),并显示了一个详细的消息,解释了拒绝的原因,包括允许的能力列表。
应用策略非常简单。编写策略要复杂得多,需要理解资源请求、Kyverno 匹配规则和 JMESPath 语言。在编写策略之前,我们需要了解它们的结构及其不同元素。
深入了解 Kyverno 策略
在本节中,我们将学习有关 Kyverno 策略的所有细节。Kyverno 策略包含一组规则,定义策略的实际功能,并具有几个通用设置,定义策略在不同场景下的行为。让我们从策略设置开始,然后深入到规则和不同的使用案例,如验证、变异和生成资源。
理解策略设置
Kyverno 策略可以具有以下设置:
-
applyRules -
validationFailureAction -
validationFailureActionOverrides -
background设置确定在后台扫描期间是否应用策略到现有资源。默认为“true”。 -
schemaValidation -
failurePolicy -
webhookTimeoutSeconds
applyRules 设置确定是否仅适用一个或多个规则于匹配资源。有效值为“One”和“All”(默认)。如果 applyRules 设置为“One”,则将评估第一个匹配规则,忽略其他规则。
validationFailureAction 设置确定失败的验证策略规则是否应拒绝接受请求或仅报告它。有效值为“audit”(默认 - 始终允许并仅报告违规)和“enforce”(阻止无效请求)。
validationFailureActionOverrides 设置是 ClusterPolicy 属性,用于为特定命名空间覆盖 validationFailureAction。
background 设置确定在后台扫描期间是否应用策略到现有资源。默认值为“true”。
schemaValidation 设置确定是否应用策略验证检查。默认为“true”。
failurePolicy 设置确定 API 服务器在 webhook 未能响应时的行为。有效值为“Ignore”和“Fail”(默认)。如果设置为“Fail”,即使资源请求有效,也将被拒绝,同时 webhook 不可达。
webhookTimeoutSeconds 设置确定 webhook 允许评估策略的最长时间(以秒为单位)。有效值为 1 到 30 秒。默认为 10 秒。如果 webhook 未能及时响应,则 failurePolicy(见上文)决定请求的结果。
理解 Kyverno 策略规则
每个 Kyverno 策略都有一个或多个规则。每个规则都有一个match声明,一个可选的exclude声明,一个可选的preconditions声明,并且有且只有以下声明之一:
-
validate -
mutate -
generate -
verifyImages
下图演示了 Kyverno 策略及其规则的结构(省略了策略设置):

图 16.6:Kyverno 规则结构
让我们回顾一下不同的声明,并探讨一些高级话题。
匹配请求
当资源请求到达时,Kyverno webhook 需要为每个策略确定请求的资源和/或操作是否与当前策略相关。强制性的match声明有多个过滤器,用来确定策略是否应该评估当前请求。这些过滤器是:
-
resources -
subjects -
roles -
clusterRoles
match声明可以有多个过滤器,这些过滤器可以在any语句或all语句下分组。当过滤器在any下分组时,Kyverno 将应用 OR 语义进行匹配,如果任何过滤器匹配请求,则请求被视为匹配。当过滤器在all下分组时,Kyverno 将应用 AND 语义,所有过滤器必须匹配才能认为请求是匹配的。
这可能有些让人感到不知所措。让我们来看一个示例。以下策略规范有一个名为some-rule的单个规则。该规则有一个match声明,包含两个资源过滤器,且它们在any语句下分组。第一个资源过滤器匹配类型为Service的资源,名称为service-1或service-2。第二个资源过滤器匹配在ns-1命名空间中的类型为Service的资源。这个规则将匹配任何名为service-1或service-2的 Kubernetes 服务,无论它们在哪个命名空间中,以及ns-1命名空间中的任何服务。
spec:
rules:
- name: some-rule
match:
any:
- resources:
kinds:
- Service
names:
- "service-1"
- "service-2"
- resources:
kinds:
- Service
namespaces:
- "ns-1"
让我们看一个不同的例子。这次我们添加一个集群角色过滤器。以下规则将匹配类型为名为service-1的服务,并且请求用户具有名为some-cluster-role的集群角色的请求。
rules:
- name: some-rule
match:
all:
- resources:
kinds:
- Service
names:
- "service-1"
clusterRoles:
- some-cluster-role
访问审核资源包含所有与请求用户或服务帐户绑定的角色和集群角色。
排除资源
排除资源与匹配非常相似。设置策略以禁止所有请求创建或更新某些资源,除非它们是在特定命名空间中或由具有特定角色的用户发出的,这是一种常见做法。下面是一个示例,匹配所有服务,但排除ns-1命名空间:
rules:
- name: some-rule
match:
any:
- resources:
kinds:
- Service
exclude:
any:
- resources:
namespaces:
- "ns-1"
另一个常见的排除是针对特定角色,如cluster-admin。
使用前提条件
使用match和exclude来限制策略的范围是很好的,但在许多情况下,这不足以满足需求。有时,你需要根据细粒度的细节(如内存请求)来选择资源。下面是一个示例,匹配所有请求内存小于 1 GiB 的 pod。
键值语法使用 JMESPath (jmespath.org) 在内置请求对象上:
rules:
- name: memory-limit
match:
any:
- resources:
kinds:
- Pod
preconditions:
any:
- key: "{{request.object.spec.containers[*].resources.requests.memory}}"
operator: LessThan
value: 1Gi
验证请求
Kyverno 的主要用例是验证请求。验证规则有一个 validate 语句。validate 语句包含一个 message 字段,当请求验证失败时会显示该消息。验证规则有两种形式,基于模式的验证和基于拒绝的验证。让我们逐一查看它们。正如你可能记得的那样,资源验证失败的结果取决于 validationFailureAction 字段,可以是 audit 或 enforce。
基于模式的验证
一个基于模式的验证规则在 validate 语句下有一个 pattern 字段。如果资源不匹配该模式,则规则失败。这里是一个基于模式的验证示例,要求资源必须有一个名为 app 的标签:
validate:
message: "The resource must have a label named `app`."
pattern:
metadata:
labels:
some-label: "app"
验证部分只会应用于符合 match 和 preconditions 语句的请求,并且如果有 exclude 语句,也不会排除这些请求。
你还可以对模式中的值应用运算符——例如,这里有一个验证规则,要求部署的副本数至少为 3:
rules:
- name: validate-replica-count
match:
any:
- resources:
kinds:
- Deployment
validate:
message: "Replica count for a Deployment must be at least 3."
pattern:
spec:
replicas: ">=3"
基于拒绝的验证
一个基于拒绝的验证规则在 validate 语句下有一个 deny 字段。拒绝规则类似于我们之前看到的用于选择资源的前置条件。每个拒绝条件都有一个键、一个运算符和一个值。拒绝条件的常见用途是禁止某个特定操作,如 DELETE。以下示例使用基于拒绝的验证来防止删除部署和 StatefulSets。请注意消息和键使用请求变量。对于 DELETE 操作,已删除的对象定义为 request.oldObject,而不是 request.object:
rules:
- name: block-deletes-of-deployments-and-statefulsets
match:
any:
- resources:
kinds:
- Deployment
- Statefulset
validate:
message: "Deleting {{request.oldObject.kind}}/{{request.oldObject.metadata.name}} is not allowed"
deny:
conditions:
any:
- key: "{{request.operation}}"
operator: Equals
value: DELETE
还有更多的验证内容,你可以在这里探索:kyverno.io/docs/writing-policies/validate/
现在让我们关注变更操作。
变更资源
变更听起来可能很可怕,但其实它只是以某种方式修改请求中的资源。请注意,即使变更后的请求符合任何策略,它仍然会通过验证。无法更改请求对象的类型,但可以更改其属性。变更的好处是你可以自动修复无效请求,这通常比阻止无效请求更好的用户体验。缺点是(特别是如果无效资源是作为 CI/CD 流水线的一部分创建的)它会在源代码和集群中的实际资源之间产生不一致。然而,它在某些情况下非常有用,特别是当你希望控制一些用户无需关注的方面,或者在迁移过程中。
足够的理论—让我们来看一下 Kyverno 中的变更。你仍然需要选择要变更的资源,这意味着变更策略仍然需要 match、exclude 和 precondition 语句。
然而,代替 validate 语句,你将使用 mutate 语句。这里是一个示例,使用 patchStrategicMerge 类型来设置使用 latest 标签镜像的容器的 imagePullPolicy。语法类似于 Kustomize 的覆盖和合并现有资源。image 字段用括号括起来是因为 JMESPath 的一个特性叫做锚点(kyverno.io/docs/writing-policies/validate/#anchors),在这种情况下,只有当给定字段匹配时,剩余的子树才会被应用。这意味着 imagePullPolicy 只会为符合条件的镜像设置:
mutate:
patchStrategicMerge:
spec:
containers:
# match images which end with :latest
- (image): "*:latest"
# set the imagePullPolicy to "IfNotPresent"
imagePullPolicy: "IfNotPresent"```
The other flavor of mutation is JSON Patch (jsonpatch.com), which is specified in RFC 6902 (datatracker.ietf.org/doc/html/rfc6902). JSON Patch has similar semantics to preconditions and deny rules. The patch has an operation, path, and value. It applies the operation to the patch with the value. The operation can be one of:
addremovereplacecopymovetest
Here is an example of adding some data to a config map using JSON Patch. It adds multiple fields to the /data/properties path and a single value to the /data/key path:
规格:
规则:
- 名称:patch-config-map
匹配:
任何:
- 资源:
名称:
- the-config-map
类型:
- ConfigMap
变更:
patchesJson6902: |-
- 路径:"/data/properties"
操作:添加
值:|
prop-1=value-1
prop-2=value-2
- 路径:"/data/key"
操作:添加
值:some-string
Generating resources
Generating resources is an interesting use case. Whenever a request comes in, Kyverno may create new resources instead of mutating or validating the request (other policies may validate or mutate the original request).
A policy with a generate rule has the same match and/or exclude statements as other policies. This means it can be triggered by any resource request as well as existing resources. However, instead of validating or mutating, it generates a new resource when the origin resource is created. A generate rule has an important property called synchronize. When synchronize is true, the generated resource is always in sync with the origin resource (when the origin resource is deleted, the generated resource is deleted as well). Users can’t modify or delete a generated resource. When synchronize is false, Kyverno doesn’t keep track of the generated resource, and users can modify or delete it at will.
Here is a generate rule that creates a NetworkPolicy that prevents any traffic when a new Namespace is created. Note the data field, which defines the generated resource:
规格:
规则:
- 名称:deny-all-traffic
匹配:
任何:
- 资源:
类型:
- 命名空间
生成:
类型:NetworkPolicy
apiVersion: networking.k8s.io/v1
名称:deny-all-traffic
命名空间:"{{request.object.metadata.name}}"
数据:
规格:
# 选择命名空间中的所有 pods
podSelector: {}
策略类型:
- Ingress
- Egress
When generating resources for an existing origin resource instead of a data field, a clone field is used. For example, if we have a config map called config-template in the default namespace, the following generate rule will clone that config map into every new namespace:
规格:
规则:
- 名称:clone-config-map
匹配:
任何:
- 资源:
类型:
- 命名空间
生成:
类型:ConfigMap
apiVersion: v1
# 生成的资源名称
名称:default-config
命名空间:"{{request.object.metadata.name}}"
同步:true
克隆:
命名空间:default
名称:config-template
It’s also possible to clone multiple resources by using a cloneList field instead of a clone field.
Advanced policy rules
Kyverno has some additional advanced capabilities, such as external data sources and autogen rules for pod controllers.
External data sources
So far we’ve seen how Kyverno uses information from an admission review object to perform validation, mutation, and generation. However, sometimes additional data is needed. This is done by defining a context field with variables that can be populated from an external config map, the Kubernetes API server, or an image registry.
Here is an example of defining a variable called dictionary and using it to mutate a pod and add a label called environment, where the value comes from the config map variable:
规则:
- 名称:configmap-lookup
上下文:
- 名称:dictionary
configMap:
名称:some-config-map
命名空间:some-namespace
匹配:
任何:
- 资源:
类型:
- Pod
变更:
patchStrategicMerge:
元数据:
标签:
环境:"{{dictionary.data.env}}"
The way it works is that the context named “dictionary” points to a config map. Inside the config map there is a section called “data” with a key called “env”.
Autogen rules for pod controllers
Pods are one of the most common resources to apply policies to. However, pods can be created indirectly by many types of resources: Pods (directly), Deployments, StatefulSets, DaemonSets, and Jobs. If we want to verify that every pod has a label called “app” then we will be forced to write complex match rules with an any statement that covers all the various resources that create pods. Kyverno provides a very elegant solution in the form of autogen rules for pod controllers.
The auto-generated rules can be observed in the status of the policy object. We will see an example in the next section.
We covered in detail a lot of the powerful capabilities Kyverno brings to the table. Let’s write some policies and see them in action.
Writing and testing Kyverno policies
In this section, we will actually write some Kyverno policies and see them in action. We will use some of the rules we explored in the previous section and embed them in full-fledged policies, apply the policies, create resources that comply with the policies as well as resources that violate the policies (in the case of validating policies), and see the outcome.
Writing validating policies
Let’s start with a validating policy that disallows services in the namespace ns-1 as well as services named service-1 or service-2 in any namespace:
$ cat <<EOF | k apply -f -
apiVersion: kyverno.io/v1
类型:ClusterPolicy
元数据:
名称:disallow-some-services
规格:
validationFailureAction: Enforce
规则:
- 名称:some-rule
匹配:
任何:
- 资源:
类型:
- 服务
名称:
- "service-1"
- "service-2"
- 资源:
类型:
- 服务
命名空间:
- "ns-1"
验证:
消息:>-
服务名为 service-1 和 service-2,以及
任何在 ns-1 命名空间中的服务不允许
拒绝:{}
EOF
clusterpolicy.kyverno.io/disallow-some-services 已创建
Now that the policy is in place, let’s try to create a service named “service-1” in the default namespace that violates the policy. Note that there is no need to actually create resources to check the outcome of admission control. It is sufficient to run in dry-run mode as long as the dry-run happens on the server side:
$ k create service clusterip service-1 -n default --tcp=80 --dry-run=server
错误:创建 ClusterIP 服务失败:admission webhook "validate.kyverno.svc-fail" 拒绝了请求:
策略 Service/default/service-1 对资源违规:
不允许某些服务:
some-rule: 服务名为 service-1 和 service-2,以及命名空间中的任何服务
ns-1 不允许
exclude-services-namespace:
some-rule: 除了 ns-1 命名空间,其他服务都不允许
As you can see, the request was rejected, with a nice message from the policy that explains why.
If we try to do the dry-run on the client side, it succeeds (but doesn’t actually create any service), as the admission control check happens only on the server:
$ k create service clusterip service-1 -n default --tcp=80 --dry-run=client
service/service-1 已创建(干运行)
Now that we have proved the point, we will use only a server-side dry-run.
Let’s try to create a service called service-3 in the default namespace, which should be allowed:
$ k create service clusterip service-3 -n default --tcp=80 --dry-run=server
service/service-3 已创建(服务器干运行)
Let’s try to create service-3 in the forbidden ns-1 namespace:
$ k create ns ns-1
$ k create service clusterip service-3 -n ns-1 --tcp=80 --dry-run=server
error: 创建 ClusterIP 服务失败:入驻 webhook "validate.kyverno.svc-fail" 拒绝了请求:
policy Service/ns-1/service-3 资源违规:
disallow-some-services:
some-rule: 名为 service-1 和 service-2 的服务,以及命名空间中的任何服务
ns-1 不允许
Yep. That failed as expected. Let’s see what happens if we change the validationFailureAction from Enforce to Audit:
$ k patch clusterpolicies.kyverno.io disallow-some-services --type merge -p '{"spec": {"validationFailureAction": "Audit"}}'
clusterpolicy.kyverno.io/disallow-some-services 已修补
$ k create service clusterip service-3 -n ns-1 --tcp=80 --dry-run=server
service/service-3 已创建(服务器干运行)
However, it generated a report of validation failure:
$ k get policyreports.wgpolicyk8s.io -n ns-1
NAME PASS FAIL WARN ERROR SKIP AGE
cpol-disallow-some-services 0 1 0 0 0 2m4s
Now, the service passes the admission control, but a record of the violation was captured in the policy report. We will look at reports in more detail later in the chapter.
For now, let’s look at mutating policies.
Writing mutating policies
Mutating policies are a lot of fun. They quietly modify incoming requests to comply with the policy. They don’t cause failures like validating policies in “enforce” mode, and they don’t generate reports you need to scour through like validating policies in “audit” mode. If an invalid or incomplete request comes in, you just change it until it’s valid.
Here is a policy that sets the imagePullPolicy to IfNotPresent when the tag is latest (by default it is Always).
$ cat <<EOF | k apply -f -
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: set-image-pull-policy
spec:
rules:
- name: set-image-pull-policy
match:
any:
- 资源:
kinds:
- Pod
mutate:
patchStrategicMerge:
spec:
containers:
# 匹配以 :latest 结尾的镜像
- (image): "*:latest"
# set the imagePullPolicy to "IfNotPresent"
imagePullPolicy: "IfNotPresent"
EOF
clusterpolicy.kyverno.io/set-image-pull-policy 已创建
Let’s see it in action. Note that for a mutating policy, we can’t use dry-run because the whole point is to actually mutate a resource.
The following pod matches our policy and doesn’t have imagePullPolicy set:
$ cat <<EOF | k apply -f -
apiVersion: v1
kind: Pod
metadata:
name: some-pod
spec:
containers:
- name: some-container
image: g1g1/py-kube:latest
command:
- sleep
- "9999"
EOF
pod/some-pod 已创建
Let’s verify that the mutation worked and check the container’s imagePullPolicy:
$ k get po some-pod -o yaml | yq '.spec.containers[0].imagePullPolicy'
IfNotPresent
Yes. It was set correctly. Let’s confirm that Kyverno was responsible for setting the imagePullPolicy by deleting the policy and then creating another pod:
$ k delete clusterpolicy set-image-pull-policy
clusterpolicy.kyverno.io "set-image-pull-policy" 已删除
$ cat <<EOF | k apply -f -
apiVersion: v1
kind: Pod
metadata:
name: another-pod
spec:
containers:
- name: some-container
image: g1g1/py-kube:latest
command:
- sleep
- "9999"
EOF
pod/another-pod 已创建
The Kyverno policy was deleted, and another pod called another-pod with the same image g1g1/py-kube:latest was created. Let’s see if its imagePullPolicy is the expected Always (the default for images with the latest image tag):
$ k get po another-pod -o yaml | yq '.spec.containers[0].imagePullPolicy'
Always
Yes, it works how it should! Let’s move on to another type of exciting Kyverno policy – a generating policy, which can create new resources out of thin air.
Writing generating policies
Generating policies create new resources in addition to the requested resource when a new resource is created. Let’s take our previous example of creating an automatic network policy for new namespaces that prevents any network traffic from coming in and out. This is a cluster policy that applies to any new namespace except the excluded namespaces:
cat <<EOF | k apply -f -
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: deny-all-traffic
spec:
rules:
- name: deny-all-traffic
match:
any:
- 资源:
kinds:
- 命名空间
exclude:
any:
- 资源:
namespaces:
- kube-system
- default
- kube-public
- kyverno
generate:
kind: NetworkPolicy
apiVersion: networking.k8s.io/v1
name: deny-all-traffic
namespace: "{{request.object.metadata.name}}"
data:
spec:
# 选择命名空间中的所有 pods
podSelector: {}
policyTypes:
- Ingress
- Egress
EOF
clusterpolicy.kyverno.io/deny-all-traffic 已创建
The deny-all-traffic Kyverno policy was created successfully. Let’s create a new namespace, ns-2, and see if the expected NetworkPolicy is generated:
$ k create ns ns-2
namespace/ns-2 已创建
$ k get networkpolicy -n ns-2
NAME POD-SELECTOR AGE
deny-all-traffic <none> 15s
Yes, it worked! Kyverno lets you easily generate additional resources.
Now that we have some hands-on experience in creating Kyverno policies, let’s learn about how to test them and why.
Testing policies
Testing Kyverno policies before deploying them to production is very important because Kyverno policies are very powerful, and they could easily cause outages and incidents if misconfigured by blocking valid requests, allowing invalid requests, improperly mutating resources, and generating resources in the wrong namespaces.
Kyverno offers tooling as well as guidance about testing its policies.
The Kyverno CLI
The Kyverno CLI is a versatile command-line program that lets you apply policies on the client side and see the results, run tests, and evaluate JMESPath expressions.
Follow these instructions to install the Kyverno CLI: kyverno.io/docs/kyverno-cli/#building-and-installing-the-cli.
Verify that it was installed correctly by checking the version:
$ kyverno version
Version: 1.8.5
Time: 2022-12-20T08:41:43Z
Git commit ID: c19061758dc4203106ab6d87a245045c20192721
Here is the help screen if you just type kyverno with no additional command:
$ kyverno
Kubernetes 原生策略管理
Usage:
kyverno [command]
可用命令:
apply 将策略应用于资源
completion 生成指定 Shell 的自动补全脚本
help 获取任何命令的帮助
jp 提供一个命令行界面,用于 JMESPath,并增强了 Kyverno 特定的自定义函数
test 从目录运行测试
version 显示当前版本的 kyverno
Flags:
--add_dir_header 如果为 true,在日志消息的头部添加文件目录
-h, --help kyverno 的帮助信息
--log_file string 如果不为空,使用此日志文件(当-logtostderr=true 时无效)
--log_file_max_size uint 定义日志文件最大增长的大小(当-logtostderr=true 时无效)。单位为兆字节。如果值为 0,则最大文件大小无限制。(默认值 1800)
--one_output 如果为 true,仅将日志写入其本机严重性级别(与将日志写入每个较低严重性级别的效果不同;当-logtostderr=true 时无效)
--skip_headers 如果为 true,避免在日志消息中使用头部前缀
--skip_log_headers 如果为 true,打开日志文件时避免显示头部(当-logtostderr=true 时无效)
-v, --v Level 设置日志级别的详细程度
使用 "kyverno [command] --help" 获取关于命令的更多信息。
Earlier in the chapter we saw how to evaluate the results of a validating Kyverno policy without actually creating resources, using a dry-run. This is not possible for mutating or generating policies. With kyverno apply we can achieve the same effect for all policy types.
Let’s see how to apply a mutating policy to a resource and examine the results. We will apply the set-image-pull-policy to a pod stored in the file some-pod.yaml. The policy was defined earlier, and is available in the attached code as the file mutate-image-pull-policy.yaml.
First, let’s see what the result would be if we just created the pod without applying the Kyverno policy:
$ k apply -f some-pod.yaml -o yaml --dry-run=server | yq '.spec.containers[0].imagePullPolicy'
Always
It is Always. Now, we will apply the Kyverno policy to this pod resource and check the outcome:
$ kyverno apply mutate-image-pull-policy.yaml --resource some-pod.yaml
将 1 个策略规则应用于 1 个资源...
mutate policy set-image-pull-policy 已应用于 default/Pod/some-pod:
apiVersion: v1
kind: Pod
metadata:
name: some-pod
namespace: default
spec:
containers:
- command:
- sleep
- "9999"
image: g1g1/py-kube:latest
imagePullPolicy: IfNotPresent
name: some-container
---
pass: 1, fail: 0, warn: 0, error: 0, skip: 2
As you can see, after the mutating policy is applied to some-pod, the imagePullPolicy is IfNotPresent as expected.
Let’s play with the kyverno jp sub-command. It accepts standard input or can take a file.
Here is an example that checks how many arguments the command of the first container in a pod has. We will use this pod manifest as input:
$ cat some-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: some-pod
spec:
containers:
- name: some-container
image: g1g1/py-kube:latest
command:
- sleep
- "9999"
Note that it has a command called sleep with a single argument, “9999”. We expect the answer to be 1. The following command does the trick:
$ cat some-pod.yaml | kyverno jp 'length(spec.containers[0].command) | subtract(@, `1`)'
1
How does it work? First it pipes the content of some-pod.yaml to the kyverno jp command with the JMESPath expression that takes the length of the command of the first container (an array with two elements, “sleep” and “9000”), and then it pipes it to the subtract() function, which subtracts 1 and, hence, ends up with the expected result of 1.
The Kyverno CLI commands apply and jp are great for ad hoc exploration and the quick prototyping of complex JMESPath expressions. However, if you use Kyverno policies at scale (and you should), then I recommend a more rigorous testing practice. Luckily Kyverno has good support for testing via the kyverno test command. Let’s see how to write and run Kyverno tests.
Understanding Kyverno tests
The kyverno test command operates on a set of resources and policies governed by a file called kyverno-test.yaml, which defines what policy rules should be applied to which resources and what the expected outcome is. It then returns the results.
The result of applying a policy rule to a resource can be one of the following four:
pass– the resource matches the policy and doesn’t trigger thedenystatement (only for validating policies)fail– the resource matches the policy and triggers the deny statement (only for validating policies)skip– the resource doesn’t match the policy definition and the policy wasn’t appliedwarn– the resource doesn’t comply with the policy but has an annotation:policies.kyverno.io/scored: "false"
If the expected outcome of the test doesn’t match the result of applying the policy to the resource, then the test will be considered a failure.
For mutating and generating policies, the test will include patchedResource and generatedResource respectively.
Let’s see what the kyverno-test.yaml file looks like:
name: <some name>
policies:
- <path/to/policy.yaml>
- <path/to/policy.yaml>
resources:
- <path/to/resource.yaml>
- <path/to/resource.yaml>
variables: variables.yaml # 可选文件,用于声明变量
userinfo: user_info.yaml # 可选文件,用于声明申请信息(角色、集群角色和主体)
results:
- policy: <name>
rule: <name>
resource: <name>
resources: # 可选,主要用于`validate`规则。必须指定`resource`或`resources[]`中的一个。当多个不同的资源应该共享相同的测试结果时,使用`resources[]`。
- <name_1>
- <name_2>
namespace: <name> # 测试特定命名空间中的资源时使用
patchedResource: <file_name.yaml> # 测试变更规则时此字段是必需的
生成资源: <file_name.yaml> # 在测试生成规则时,此字段是必需的。
类型: <kind>
结果: 通过
Many different test cases can be defined in a single kyverno-test.yaml file. The file has five sections:
policiesresourcesvariablesuserInforesults
The policies and resources sections specify paths to all the policies and resources that participate in the tests. The variables and userInfo optional sections can define additional information that will be used by the test cases.
The results section is where the various test cases are specified. Each test case tests the application of a single policy rule to a single resource. If it’s a validating rule, then the result field should contain the expected outcome.
If it’s a mutating or generating rule, then the corresponding patchedResource or generatedResource should contain the expected outcome.
Let’s write some Kyverno tests for our policies.
Writing Kyverno tests
All the files mentioned here are available in the tests sub-directory of the code attached to the chapter.
Let’s start by writing our kyverno-test.yaml file:
名称: test-some-rule
策略:
- ../disallow-some-services-policy.yaml
资源:
- test-service-ok.yaml
- test-service-bad-name.yaml
- test-service-bad-namespace.yaml
结果:
- 策略: disallow-some-services
规则: some-rule
资源:
- service-ok
类型: 服务
结果: 跳过
- 策略: disallow-some-services
规则: some-rule
资源:
- service-1
类型: 服务
结果: 失败
- 策略: disallow-some-services
规则: some-rule
资源:
- service-in-ns-1
类型: 服务
命名空间: ns-1
结果: 失败
The policies section contains the disallow-some-services-policy.yaml file. This policy rejects services named service-1 or service-2 and any service in the ns-1 namespace.
The resources section contains three different files that all contain a Service resource:
test-service-ok.yamltest-service-bad-name.yamltest-service-bad-namespace.yaml
The test-service-ok.yaml file contains a service that doesn’t match any of the rules of the policy. The test-service-bad-name.yaml file contains a service named service-1, which is not allowed. Finally, the test-service-bad-namespace.yaml file contains a resource named service-in-ns-1, which is allowed. However, it has the ns-1 namespace, which is not allowed.
Let’s look at the results section. There are three different test cases here. They all test the same rule in our policy, but each test case uses a different resource name. This comprehensively covers the behavior of the policy.
The first test case verifies that a service that doesn’t match the rule is skipped. It specifies the policy, the rule name, the resources the test case should be applied to, and most importantly, the expected result, which is skip in this case:
- 策略: disallow-some-services
规则: some-rule
资源:
- service-ok
结果: 跳过
The second test case is similar except that the resource name is different and the expected result is fail:
- 策略: disallow-some-services
规则: some-rule
资源:
- service-1
类型: 服务
结果: 失败
There is one more slight difference. In this test case, the kind of the target resource is explicitly specified (kind: Service). This may seem redundant at first glance because the service-1 resource defined in test-service-bad-name.yaml already has the kind listed:
apiVersion: v1
类型: 服务
元数据:
标签:
应用: service-1
名称: service-1
命名空间: ns-2
规格:
端口:
- 名称: https
端口: 443
目标端口: https
选择器:
应用: some-app
The reason the kind field is needed is to disambiguate which resource is targeted, in case the resource file contains multiple resources with the same name.
The third test case is the same as the second test case, except it targets a different resource and, as a consequence, a different part of the rule (disallowing services in the ns-1 namespace):
- 策略: disallow-some-services
规则: some-rule
资源:
- service-in-ns-1
类型: 服务
命名空间: ns-1
结果: 失败
OK. We have our test cases. Let’s see how to run these tests.
Running Kyverno tests
Running Kyverno tests is very simple. You just type kyverno test and the path to the folder containing a kyverno-test.yaml file or a Git repository and a branch.
Let’s run our tests:
$ kyverno 测试 .
执行 test-some-rule...
应用 1 个策略到 3 个资源...
│───│────────────────────────│───────────│──────────────────────────────│────────│
│ # │ 策略 │ 规则 │ 资源 │ 结果 │
│───│────────────────────────│───────────│──────────────────────────────│────────│
│ 1 │ disallow-some-services │ some-rule │ ns-2//service-ok │ 通过 │
│ 2 │ disallow-some-services │ some-rule │ ns-2/Service/service-1 │ 通过 │
│ 3 │ disallow-some-services │ some-rule │ ns-1/Service/service-in-ns-1 │ 通过 │
│───│────────────────────────│───────────│──────────────────────────────│────────│
测试总结: 3 个测试通过,0 个测试失败
We get a nice output that lists each test case and then a one-line summary. All three tests passed, so that’s great.
When you have test files that contain a lot of test cases and you try to tweak one specific rule, you may want to run a specific test case only. Here is the syntax:
kyverno 测试 . --test-case-selector "policy=disallow-some-services, rule=some-rule, resource=service-ok"
The kyverno test command has very good documentation with a lot of examples. Just type kyverno test -h.
So far, we have written policies, rules, and policy tests and executed them. The last piece of the puzzle is viewing reports when Kyverno is running.
Viewing Kyverno reports
Kyverno generates reports for policies with validate or verifyImages rules. Only policies in audit mode or that have spec.background: true will generate reports.
As you recall Kyverno can generate two types of reports in the form of custom resources. PolicyReports are generated for namespace-scoped resources (like services) in the namespace the resource was applied. ClusterPolicyReports are generated for cluster-scoped resources (like namespaces).
Our disallow-some-services policy has a validate rule and operates in audit mode, which means that if we create a service that violates the rule, the service will be created, but a report will be generated. Here we go:
$ k 创建 服务 clusterip service-3 -n ns-1 --tcp=80
服务/service-3 创建
We created a service in the forbidden ns-1 namespace. Kyverno didn’t block the creation of the service because of audit mode. Let’s review the report (that polr is shorthand for policyreports):
$ k 获取 polr -n ns-1
名称 通过 失败 警告 错误 跳过 时长
cpol-disallow-some-services 0 1 0 0 0 1m
A report named cpol-disallow-some-services was created. We can see that it counted one failure. What happens if we create another service?
$ k 创建 服务 clusterip service-4 -n ns-1 --tcp=80
服务/service-4 创建
$ k 获取 polr -n ns-1
名称 通过 失败 警告 错误 跳过 时长
cpol-disallow-some-services 0 2 0 0 0 2m
Yep. Another failure is reported. The meaning of these failures is that the resource failed to pass the validate rule. Let’s peek inside. The report has a metadata field, which includes an annotation for the policy it represents. Then there is a results section where each failed resource is listed. The info for each result includes the resource that caused the failure and the rule it violated. Finally, the summary contains aggregate information about the results:
$ k 获取 polr cpol-disallow-some-services -n ns-1 -o yaml
apiVersion: wgpolicyk8s.io/v1alpha2
类型: PolicyReport
元数据:
创建时间戳: "2023-01-22T04:01:12Z"
生成: 3
标签:
app.kubernetes.io/managed-by: kyverno
cpol.kyverno.io/disallow-some-services: "2472317"
名称: cpol-disallow-some-services
命名空间: ns-1
资源版本: "2475547"
uid: dadcd6ae-a867-4ec8-bf09-3e6ca76da7ba
结果:
- 消息: 服务名为 service-1 和 service-2,以及命名空间 ns-1 中的任何服务
不允许
策略: disallow-some-services
资源:
- apiVersion: v1
类型: 服务
名称: service-4
命名空间: ns-1
uid: 4d473ac1-c1b1-4929-a70d-fad98a411428
结果:失败
规则:some-rule
已评分:true
来源:kyverno
时间戳:
纳秒:0
秒数:1674361576
- 消息:名为 service-1 和 service-2 的服务,以及命名空间 ns-1 中的任何服务
不允许
策略:disallow-some-services
资源:
- apiVersion: v1
类型:Service
名称:service-3
命名空间:ns-1
uid: 62458ac4-fe39-4854-9f5a-18b26109511a
结果:失败
规则:some-rule
已评分:true
来源:kyverno
时间戳:
纳秒:0
秒数:1674361426
摘要:
错误:0
失败:2
通过:0
跳过:0
警告:0
This is pretty nice, but it might not be the best option to keep track of your cluster if you have a lot of namespaces. It’s considered best practice to collect all the reports and periodically export them to a central location. Check out the Policy reporter project: github.com/kyverno/policy-reporter. It also comes with a web-based policy reporter UI.
Let’s install it:
$ helm repo add policy-reporter https://kyverno.github.io/policy-reporter
“policy-reporter”已添加到您的仓库
$ helm repo update
请稍等,我们正在从您的图表仓库中获取最新信息...
...成功从 "policy-reporter" 图表仓库获取更新
...成功从 "kyverno" 图表仓库获取更新
更新完成。 祝您使用 Helm 愉快!
$ helm upgrade --install policy-reporter policy-reporter/policy-reporter --create-namespace -n policy-reporter --set ui.enabled=true
发布的“policy-reporter”不存在。现在正在安装。
名称:policy-reporter
最后部署时间:2023 年 1 月 21 日 20:39:42
命名空间:policy-reporter
状态:已部署
修订版:1
测试套件:无
The policy reporter has been installed successfully in the policy-reporter namespace, and we enabled the UI.
The next step is to do port-forwarding to access the UI:
$ k port-forward service/policy-reporter-ui 8080:8080 -n policy-reporter
正在从 127.0.0.1:8080 转发 -> 8080
正在从 [::1]:8080 转发 -> 8080
现在,我们可以浏览 http://localhost:8080 并直观地查看策略报告。
仪表板显示失败的策略报告。我们可以看到在 kube-system 命名空间中的 20 个失败,以及在 ns-1 命名空间中的 2 个失败。

图 16.7:策略报告仪表板 UI
kube-system 中的故障是由于我们与 Kyverno 一起安装的最佳实践安全策略导致的。
我们可以向下滚动并查看有关失败的更多详细信息:

图 16.8:策略报告仪表板 UI – 结果
我们还可以从侧边栏选择“策略报告”选项,然后查看通过的结果。我们还可以通过不同的标准过滤策略报告,例如策略、种类、类别、严重性和命名空间:

图 16.9:策略报告仪表板 UI – 策略报告
总体而言,策略报告 UI 具有流畅的外观,提供了一个很好的选项,用于浏览、筛选和搜索策略报告。
摘要
在本章中,我们探讨了 Kubernetes 在大型企业组织中的日益普及以及在管理这些部署中的治理重要性。我们介绍了策略引擎的概念,以及它们是如何建立在 Kubernetes 准入控制机制之上的。我们讨论了策略引擎如何用来解决安全性、合规性和治理方面的问题。我们还回顾了流行的策略引擎。最后,我们深入探讨了 Kyverno,详细解释了它是如何工作的。接着,我们动手编写了一些策略,进行了测试,并回顾了策略报告。如果你在 Kubernetes 上运行一个非平凡的生产系统,你应该非常认真地考虑将 Kyverno(或其他策略引擎)作为核心组件。这为下一章的内容做好了完美的过渡,接下来我们将讨论 Kubernetes 在生产环境中的应用。
第十七章:在生产环境中运行 Kubernetes
在上一章中,我们讨论了治理和策略引擎。这是管理大规模基于 Kubernetes 的生产系统的一个重要部分。然而,它只是其中的一部分。在本章中,我们将重点关注 Kubernetes 在生产环境中的整体管理。重点将放在如何在云中运行多个托管 Kubernetes 集群。
我们将涵盖的主题包括:
-
了解云中的托管 Kubernetes
-
管理多个集群
-
为大规模 Kubernetes 部署构建有效的流程
-
处理大规模基础设施
-
管理集群和节点池
-
升级 Kubernetes
-
故障排除
-
成本管理
了解云中的托管 Kubernetes
托管 Kubernetes是由云服务提供商(如亚马逊 Web 服务(AWS)、谷歌云平台(GCP)和微软 Azure)提供的服务,简化了在云中部署、管理和扩展容器化应用程序的过程。通过托管 Kubernetes,组织可以专注于开发和部署应用程序,而无需过多担心底层的基础设施。
托管 Kubernetes 提供了一个预配置且优化的环境,用于部署容器,消除了手动设置和维护 Kubernetes 集群的需求。这使得组织能够快速部署和扩展应用程序,减少市场响应时间,并释放宝贵的资源。
此外,托管 Kubernetes 与云服务提供商的其他服务(如数据库、网络、存储解决方案、安全性、身份验证和可观测性功能)集成,使得管理和保护整个应用堆栈变得更加容易。这也使得组织能够利用服务提供商在管理大规模基础设施、确保高可用性和减少停机时间方面的专业知识。
总体而言,托管 Kubernetes 提供了一种简化且高效的方式来部署和管理云中的容器化应用程序,减少了运营开销,并提高了市场响应时间。这使得它成为各类组织的一个有吸引力的选择,帮助他们利用容器和云计算的优势。
深度集成
云服务提供商利用 Kubernetes 的扩展性,通过 CNI、CSI 和身份验证/授权插件,提供其托管 Kubernetes 解决方案与云服务的深度集成。云服务提供商还实现了云控制器接口(CCI),使其计算基础设施能够为 Kubernetes 节点提供服务。
然而,集成更深层次。云服务提供商通常会配置 kubelet,控制每个节点上运行的容器运行时,并在每个节点上部署各种 DaemonSets。
例如,AKS 利用了许多 Azure 服务:
-
Azure Compute:AKS 利用 Azure Compute 资源,如 虚拟机 (VMs)、可用性集和扩展集,提供托管的 Kubernetes 体验。
-
Azure Virtual Network:AKS 与 Azure Virtual Network 集成,使用户能够创建和管理自己的虚拟网络和子网。这为用户提供了对网络布局的控制,并能够紧密控制网络流量。
-
Azure Blob Storage:AKS 与 Azure Blob Storage 集成,使用户能够在云中存储和管理其应用程序数据。这为用户提供了可扩展、安全且高可用的存储解决方案。
-
Azure Key Vault:AKS 与 Azure Key Vault 集成,允许用户安全地管理和存储密码、密钥和证书等机密。这为用户提供了安全存储应用程序机密的解决方案。
-
Azure Monitor:AKS 与 Azure Monitor 集成,使用户能够收集和分析来自其应用程序的度量、日志和跟踪信息。这为用户提供了监控和故障排除其工作负载的能力。
-
Azure Active Directory (AAD):AKS 与 AAD 集成,提供一个安全、可靠且高可用的平台来运行 Kubernetes 集群。AAD 提供了一种高效且安全的方式来验证和授权用户及应用程序访问集群。AAD 还可以与 Kubernetes RBAC(基于角色的访问控制)集成,为集群资源访问提供细粒度的控制。
接下来,我们将讨论成功管理基于 Kubernetes 的生产系统的关键要素之一。
配额和限制
云基础设施彻底改变了组织存储和管理数据及运行工作负载的方式。然而,云服务提供商使用配额和限制是一个需要考虑和注意的重要问题。这些配额和限制虽然对确保云基础设施的稳定性和安全性至关重要,但也可能是用户面临的主要挫折来源,甚至可能导致宕机。
配额和限制是对用户可消耗资源数量的限制。例如,可能会限制在每个区域环境中可以创建的特定类型的虚拟机数量,或对可用的存储空间数量设定配额。这些配额和限制旨在防止单个用户消耗过多资源,从而可能影响云基础设施的整体性能。它们还保护用户避免无意中配置大量不必要的资源,从而产生不必要的费用。
理论上,云是无限可扩展和弹性的。但在实际操作中,这只有在配额和限制范围内才成立。
让我们在接下来的章节中查看一些实际示例。
配额和限制的实际示例
在 GCP 上,配额通常可以增加,而限制是固定的。此外,每个服务都有自己独立的配额和限制页面。虚拟专用云(VPC)页面可以在cloud.google.com/vpc/docs/quota找到。
你可以在 GCP 控制台中查看配额,并请求增加配额:console.cloud.google.com/iam-admin/quotas。
当前有 9,441 个配额!
这里是显示 GCP 计算引擎服务一些配额的截图:

图 17.1:GCP 计算引擎配额截图
现在我们了解了配额和限制是什么,让我们来讨论容量规划。
容量规划
在过去,容量规划意味着思考你在数据中心需要多少台服务器、硬盘应该多大,以及网络带宽是多少。这些都基于工作负载的使用情况,并确保有足够的冗余空间和增长空间。然后,你需要考虑硬件升级,以及如何逐步淘汰过时的硬件。在云中,你不需要担心硬件。然而,你需要围绕配额和限制进行规划。这意味着你需要监控配额和限制,并且每当接近当前配额时,提出增加请求。对于像某个 VM 系列的虚拟机实例数量这样的配额,我建议尽量保持在 50%-60%以下。这应该为你提供足够的灾难恢复空间和增长空间,同时也适用于蓝绿部署,即同时运行新版本和旧版本一段时间。
什么时候不应该使用托管 Kubernetes?
托管 Kubernetes 很好,但它不是万灵药。有几种情况和使用场景,你可能更愿意自行管理 Kubernetes,例如:
-
显而易见的使用场景是,如果你在本地运行 Kubernetes,而托管解决方案根本不可用。然而,你可以通过像 GKE Anthos、AWS Outposts 和 Azure Arc 等平台,运行类似于云托管 Kubernetes 的技术栈。
-
你需要对控制平面、节点组件和运行在每个节点上的守护进程进行极端控制。
-
你已经具备了自己运行 Kubernetes 的内部专业知识,而使用托管 Kubernetes 将需要陡峭的学习曲线,并且可能成本更高。
-
你管理着高度敏感的信息,必须完全控制这些信息,且无法将其交给云服务提供商。
-
你在多个云提供商和/或混合环境中运行 Kubernetes,并且希望在所有环境中以统一的方式管理 Kubernetes。
-
你希望确保不会被锁定到某个特定的云服务提供商。
简而言之,在各种情况下,你可能会选择自行管理 Kubernetes。让我们看看你可能如何在不同环境中部署和管理多个 Kubernetes 集群。
管理多个集群
一个 Kubernetes 集群非常强大,可以管理大量的工作负载(成千上万的节点,和数十万个 Pod)。作为初创公司,您可能只需一个集群就能走得很远。然而,在企业级规模下,您将需要不止一个集群。让我们来看看一些用例。
地理分布式集群
地理分布式集群是指在不同位置运行的集群。使用地理分布式集群的主要原因有三:
-
将您的数据和工作负载靠近其消费者。
-
合规性和数据隐私法律要求数据必须保留在其原始国家。
-
在地区性停机的情况下实现高可用性和灾难恢复。
多云
如果您在多个云平台上运行,那么您自然需要每个云提供商至少一个集群。
在多个云平台上运行可能会很复杂,但在企业级规模下,这可能是不可避免的,有时也是必要的。例如,您的公司可能在云 X 上运行 Kubernetes,而收购的公司在云 Y 上运行 Kubernetes。从 Y 迁移到 X 可能风险太大且费用过高。
另一个有效的在多个云平台上运行的理由是,通过与云提供商的谈判,争取更好的折扣,并确保您不会被完全锁定。最后,这也可能让您在完全云服务提供商停机的情况下,仍能保持容错(这可不是简单的事情)。
混合云
混合 Kubernetes 意味着在云中(使用一个或多个云提供商)运行一些 Kubernetes 集群,同时在本地运行一些 Kubernetes 集群。
这种情况可能会像之前一样发生,因为收购,或者即使您正在将本地 Kubernetes 迁移到云端。大型系统的迁移可能需要几年时间,而在迁移过程中,您将不得不运行一个混合环境中的 Kubernetes 集群。
您也可以采用像“突发到云”这样的模式,其中大部分 Kubernetes 集群运行在本地,但您可以灵活地将工作负载部署到云中的 Kubernetes 集群,这样在遇到无法预料的负载时,或者当您的本地基础设施出现问题时,云端的集群可以迅速扩展。
边缘上的 Kubernetes
大多数企业数据(约 90%)是在云端和私有数据中心生成的;然而,根据 Gartner 的预测,到 2025 年,这一比例将降至仅 25%。
这简直让人震撼。边缘计算(AKS IoT)将是这种巨大转变的主要推动力。
很多设备分布在各个地方,将会产生不断的数据流。这些数据中的一部分将被送回后端进行处理、聚合和存储。然而,将各种数据处理方式靠近数据本地进行而不是将原始数据直接发送回来,是非常有意义的。在某些情况下,您甚至可以在本地近距离运行工作负载并完全在边缘服务用户。
这就是边缘计算的承诺。在边缘运行 Kubernetes 使组织能够将数据处理更接近数据生成源,从而减少将数据发送到集中数据中心或云的延迟和带宽需求。这将提高响应时间并实现数据的实时处理,使其成为工业物联网、自动驾驶车辆以及其他实时数据处理应用场景的理想解决方案。
然而,在边缘运行 Kubernetes 会带来一系列挑战。边缘设备通常资源受限,因此必须优化 Kubernetes 的部署以适应边缘环境。组织还必须考虑边缘设备的网络连接性和可靠性,以及在边缘部署 Kubernetes 集群的安全性和隐私问题。
像 CNCF KubeEdge (kubeedge.io) 这样的项目可以帮助你入门。
然而,本章接下来的部分将专注于云中基于 Kubernetes 的大规模系统。
为大规模 Kubernetes 部署构建有效的流程
要在生产环境中运行多集群 Kubernetes 系统,你必须制定一套有效的流程和最佳实践,涵盖操作的各个方面。以下是一些需要解决的关键领域。
开发生命周期
在生产环境中,多集群 Kubernetes 系统的开发生命周期可能是一个复杂的过程,但通过正确的方法,可以将其简化。
你应该绝对实施一个 CI/CD 流水线,自动构建、测试和部署代码变更。该流水线应与版本控制系统(如 Git)集成,并且还应包含自动化测试以确保代码质量。
管理不同代码库区域的所有权和审批流程是非常重要的。
Kubernetes 命名空间可以提供一种方便的方式来组织工作负载及其相应的软件资产,并将它们与团队和相关方关联起来。
你应该对每个工作负载的变更、正在进行的构建和部署有完整的跟踪,并能够冻结活动并回滚变更。
同样重要的是控制逐步部署到不同集群和区域,以避免在所有系统中同时部署坏的变更,导致整个系统崩溃。
环境
代码审查和小心的增量部署,同时监控结果,是必要的,但对于拥有多个 Kubernetes 集群的大型企业系统来说,这些措施并不足够。某些变更可能只有在运行一段时间后或在特定条件下才会显示出负面影响,这将逃脱我们前面提到的控制机制。最佳实践是拥有多个运行时环境,例如生产环境、预发布环境和开发环境。环境的具体划分可以有所不同,但通常至少需要一个生产环境,这个环境是实际管理所有数据并且用户进行交互的系统,以及一个预发布环境,它模拟生产系统,你可以在其中测试变更和新版本,而不会影响用户并且避免将生产环境置于风险之中。
让我们考虑使用多个环境的一些方面。
分离的环境
至关重要的是,预发布环境不能意外污染并影响生产环境。例如,如果你在预发布环境中运行压力测试,而某些工作负载在预发布环境中配置错误并访问生产端点,那么你将会经历一段非常不愉快的时间,试图解开这一混乱。
严格的网络分段使得预发布环境无法访问生产环境,是一个很好的第一步。你仍然需要注意通过公共端点在预发布和生产环境之间的交互。预发布的工作负载不应包含能够访问生产环境的密钥和身份。
预发布环境的保真度
预发布环境的主要目的是在将变更部署到生产环境之前,测试与其他子系统的交互及变更。这意味着预发布环境应尽可能地模拟生产环境。然而,运行一个完全与生产环境相同的副本成本极高。
预发布环境应使用相同的自动化 CI/CD 流水线进行配置和设置,该流水线能够部署预发布和生产环境。
预发布基础设施和资源也应使用与生产环境相同的工具进行配置,尽管通常分配给预发布的资源较少。
你可能希望能够暂时缩减,甚至完全关闭预发布环境中的某些部分,并且仅在需要进行大规模测试时再将其重新启动。
资源配额
预发布和生产环境中的资源配额可以确保配置错误或甚至攻击不会导致资源使用过度。
提升过程
一旦在暂存环境中彻底测试了更改,就应该有一个明确的推广过程将其部署到生产环境中。这个过程可能因更改的范围和影响而异。推广过程可能完全自动化,CI/CD 管道检测到暂存测试成功完成并继续进行生产部署,或者涉及额外的步骤和可能另一个明确的生产部署。
权限和访问控制
当您管理运行在云基础设施上的 Kubernetes 集群星座时,需要特别注意权限模型和访问控制。这是在之前开发生命周期和环境的最佳实践基础上构建的。
最小权限原则
最小权限原则源自安全领域,但即使超越安全性,也对可靠性、性能和成本有用。人员或工作负载不应该比完成任务所需的权限更多。通过将访问权限降至最低限度,确保不会发生禁止资源的意外或恶意活动。
同时,在调查事故时,它会自动将可能的罪魁祸首缩小到那些具有操作不当配置资源或执行无效操作权限的人员。
如果您遵循 GitOps 模型,可以创建一种工作流程,其中对集群和基础设施的每一次更改都由 CI/CD 和专用工具完成。人类工程师只有只读访问权限。在本章的打破玻璃部分可以做一些特殊例外。
将权限分配给组
强烈建议将权限分配给组或团队,而不是个人。即使只有一个人当前正在执行需要某些权限集的任务,也应定义一个该人员是唯一成员的组。这将使以后添加其他人员或替换该人员更加容易。
优化你的权限模型
然而,有时候过于严格的权限模型可能是有害的。您将不得不对大量资源维护非常细粒度的权限设置。每当发生最轻微的更改以至于需要在某些资源上执行其他操作时,您将不得不修改权限。
找到在授予每个人超级管理员权限和为每个资源创建数百甚至数千个角色之间的黄金平衡点。
特别是考虑在开发环境甚至是暂存环境中放宽权限模型。这是许多实验进行的地方,您可以在这里发现需要执行的操作和实际所需的权限,然后在部署到生产环境之前调整您的权限模型。
打破玻璃
有时,CI/CD 流水线本身可能会出现故障,或者由于覆盖不全,一些资源可能是手动配置的。在这些情况下,人工工程师必须介入,可以说是“打破玻璃”,直接对 Kubernetes 或云基础设施进行操作。
建议制定正式的流程来获取紧急访问权限,明确谁可以获得该权限、权限持续时间以及记录谁曾经拥有该权限。
这引出了我们下一部分的内容——可观察性。
可观察性
一个全面的可观察性栈是绝对必不可少的。由多个 Kubernetes 集群组成的复杂系统可以从理论上推理并理解。你必须拥有来自云服务提供商、Kubernetes 本身以及工作负载的完整事件记录。你的 CI/CD 流水线及其他工具也必须具备完全的可观察性。
让我们看看多集群可观察性的一些元素。
一站式可观察性
云服务提供商和 Kubernetes 本身提供了大量的日志和度量指标作为开箱即用的可观察性。然而,这些通常是按集群级别组织的。如果你正在处理跨多个集群的广泛问题,那么进入每个集群、提取可观察性数据并尝试理解它将变得非常困难,甚至在某些规模下是不可能的。你必须将所有可观察性数据传输到一个单一的集中系统中,在那里它可以被汇总、总结,并准备好进行多集群分析和响应。
排查你的可观察性栈
你的可观察性栈是你系统中不可或缺的组成部分。如果它出现故障或性能下降,你可能会处于“盲飞”状态,无法有效地应对问题。而且,跨集群问题可能会影响你的可观察性栈,进而影响整个系统。非常仔细地考虑这个场景,确保在你的主要可观察性栈暂时无法应对任务时,拥有足够的冗余和可观察性替代方案。例如,如果你的集中式可观察性栈出现问题,你可以依赖集群内部的可观察性解决方案。如果你想要完全冗余,可以在两个云服务提供商上部署并行的可观察性栈。
考虑测试这些严苛场景:CI/CD 流水线和可观察性栈出现故障时,看看你的操作流程如何。
让我们更具体地看看不同类型的基础设施,以及如何在大规模环境中处理它们。
大规模处理基础设施
在云端运行大规模多集群 Kubernetes 的任务中,处理云基础设施是最具挑战性的任务之一。从某些方面来说,它比负责底层计算、网络和存储基础设施要好得多。然而,你会失去很多控制权,排查问题变得非常具有挑战性。
在深入每个基础设施类别之前,先来看一些通用的云级别考虑事项。
云级别考虑事项
在云中,你会将资源组织为如 AWS 账户、GCP 项目和 Azure 订阅这样的实体。一个组织可能拥有多个这样的组,每个组都有自己的限制和配额。为了简便起见,我们称之为账户。企业组织的基础设施需求将超过单一账户的容量。决定如何将基础设施拆分为不同账户至关重要。一种好的启发式方法是将不同的环境——生产、预发布和开发——拆分到不同的账户中。账户级别的隔离对于这些环境是有益的。然而,这可能还不够,在单一环境中,你可能需要更多的资源,单一账户无法容纳。
拥有一个健全的账户管理策略是关键。账户也可以作为访问控制的边界,因为即使是账户管理员也无法访问其他账户。
请与你的安全团队协商关于基于安全考虑的账户划分。
另一个重要方面是区域划分。如果你在多个区域管理基础设施,并且这些区域中的工作负载需要相互通信,那么这将带来严重的延迟和成本影响。特别是,跨区域出口通常不是免费的。
让我们来看一下每种基础设施类别。
计算
管理 Kubernetes 的计算基础设施包括 Kubernetes 集群本身及其工作节点。工作节点通常被分组到节点池中,这不是 Kubernetes 的概念。如何将系统拆分为 Kubernetes 集群以及每个集群中存在的节点池类型,将极大地影响你在规模化管理系统时的能力。
理想情况下,你可以像管理牲畜一样管理集群,配置相同的集群,并且可以轻松地在不同位置添加或移除集群。每个集群将拥有相同的节点池。
这种统一一致的集群组织方式并非总是可能的。有时为了特定目的,可能需要不同的集群。你仍然应该尽量保持少数几种集群类型,以便轻松复制。
设计你的集群划分
云中的集群通常从一个区域的虚拟网络中为节点和 Pod 分配私有 IP 地址。是的,跨区域的大型集群是可能的,但这是例外而非常规。强烈建议在可能的情况下将集群作为牲畜来管理,并在所有操作区域自动配置集群。
设计你的节点池划分
节点池是具有相同实例类型的节点组。它们通常可以通过集群自动扩展器自动扩展,以满足集群的需求。选择在集群中配置哪些节点池是一个基础性决策,它会影响性能、成本和操作复杂度。
我们将在本章稍后深入讨论这一点。
网络
网络是基础设施中一个非常动态的领域。它有许多自由度。延迟与带宽之间的相互作用有其细微之处。工作负载不能请求一定的带宽或保证的延迟。此外,还有许多外部因素会影响网络的连接性、可达性和性能。让我们来看看一些我们必须考虑和规划的重要议题。
IP 地址空间管理
当你运行一个基于多集群的 Kubernetes 系统时,集群中的每个 pod 都会获得一个唯一的私有 IP 地址。然而,如果你想连接多个集群,并且让一个集群中的工作负载通过私有 IP 地址访问其他集群中的工作负载,那么必须满足以下两个条件:
-
不同集群的网络必须进行对等连接。
-
pods 的私有 IP 地址在所有集群中必须唯一。
这需要集中管理私有 IP 地址空间,并且要小心地为不同的集群分配 IP 范围。
云服务提供商在为集群分配 IP 地址范围的方式上有所不同。AKS 要求每个集群都属于一个具有自己 IP 地址范围的 VNet,然后子网被分配给节点和 pods,子网的 IP 地址范围来自 VNet 的 IP 地址范围。GKE 则提供一个没有自己 IP 地址范围的默认网络。集群是通过具有自己 IP 地址范围的子网来配置的。
此外,服务也需要自己的 IP 地址,以及可能的其他组件。
整个私有 IPv4 地址空间由多个块组成:
-
10.0.0.0/8(A 类) -
172.16.0.0/12(B 类) -
192.168.0.0/8(C 类)
在大规模部署中,最重要的是10.0.0.0/8,它包含了 2²⁴ 个 IP 地址,总数超过 1600 万个地址。这是一个非常庞大的 IP 地址范围,但如果不仔细规划,可能会导致碎片化,即使有很多未使用的 IP 地址,也可能会耗尽大块地址空间。
下面是管理 IP 地址空间的一些最佳实践:
-
为 pods、nodes 和 services 分配足够并且不会浪费过多空间的 CIDR 块。
-
了解 Kubernetes 和云服务提供商在支持的节点和 pods 数量方面的限制。
-
考虑跨集群的网络对等连接和服务网格。
-
确保连接的集群使用不重叠的 CIDR 块。
-
使用适当的工具来管理地址空间。
-
考虑 pod 密度对 IP 地址空间的影响(例如,在 AKS 上,即使没有使用,IP 地址也会预先分配给节点上 pod 的最大数量)。
-
注意一些限制,例如一个区域中可用的最大 IP 地址数量。
网络拓扑
所有云提供商都提供虚拟网络或 VPC 概念。所有云提供商也都有地区的概念,地区是云提供商托管资源的地理区域。具体来说,虚拟网络总是局限于一个单一的地区。由于云中的 Kubernetes 集群与虚拟网络相关联,因此一个单一的 Kubernetes 集群不能跨越多个地区。这对可用性和可靠性有影响。如果你想在地区性故障中存活下来,你需要在不同地区的多个集群中运行每个关键工作负载。此外,所有这些集群通常需要相互连接。我们将在接下来的 跨集群通信 部分中进一步讨论这个问题。然而,就网络拓扑而言,将同一区域内的多个集群共享同一虚拟网络可能会更好。
网络分段
网络分段是将一个网络划分为更小的子网。在 Kubernetes 的上下文中,最重要的子网是节点、Pod 和服务的子网。在某些情况下,节点和 Pod 共享同一子网,而在其他情况下,节点和 Pod 会有单独的子网。无论如何,你需要规划并理解你的集群能够容纳多少个节点和 Pod,并据此调整集群子网的规模。
跨集群通信
在运行多个 Kubernetes 集群时,考虑将集群组视为一个单一的概念集群通常是有益的。这意味着,任何集群中的 Pod 都可以通过它们的私有 IP 地址直接访问其他集群中的 Pod。这种扁平化的 IP 地址模型是将标准 Kubernetes 网络模型从单一集群扩展到多个集群。这需要我们之前讨论过的两个要素:
-
所有连接的集群之间的 Pod 应该使用不冲突的 IP 地址范围。
-
集群之间的网络对等连接。
网络对等连接要求可能会很繁琐,因为集群会不断地变动。拥有一个单一的区域虚拟网络可以减少开销,并且只需要对区域虚拟网络进行对等连接。然而,如果你在同一地区运行大量共享同一虚拟网络的集群,你可能会遇到云服务商的限制,这会限制你的发展。例如,在 Azure 上,一个虚拟网络最多可以有 64K 个唯一的 IP 地址。
跨云通信
如果你的系统跨多个云提供商,你需要考虑如何在不同云提供商之间连接你的 Kubernetes 集群。有几种方法,各有优缺点。
首先,你可能决定避免在不同云提供商上的集群之间进行直接通信。如果部署在不同云上的 Kubernetes 集群需要相互通信,它们可以通过公共 API 实现。这种方法简单,但会排除统一概念集群的想法,即 Pod 可以直接相互通信,无论它们位于何处。
点对点 VPN 是一种通信方法,通过 BGP 连接不同的云提供商系统,并通过位于虚拟网络前面的 VPN 网关与另一云提供商管理的网络建立 VPN 连接。这建立了一个安全通道;然而,设置 VPN 网关并不是件小事,并会产生显著的开销。
直接连接(也称为直接对等连接)是另一种选项,需要在云提供商的点对点位置安装路由器。该方法允许将运行在私有数据中心中的 Kubernetes 集群连接到云中的集群。此外,性能要好得多,因为中间没有 VPN 网关。缺点是设置相当复杂,您可能需要遵守各种要求。对于具有深入低级网络专业知识的组织来说,这是一个不错的选择。
运营商或合作伙伴对等连接类似于直连;但是,您可以利用专业提供此服务的第三方的专业知识,该第三方已经与云提供商建立了关系并获得了认证。当然,您需要为此服务付费。
跨集群服务网格
如我们在第十四章 利用服务网格中所讨论的那样,服务网格为 Kubernetes 带来了巨大的价值。在生产环境中运行多个 Kubernetes 集群时,通过服务网格连接所有集群可能更加重要。服务网格的高级功能和策略可以应用于管理所有集群之间的连接和路由。
在此需要考虑两种方法:
-
单个完全连接的服务网格。
-
将您的集群划分为多个网格。
单个完全连接的单一网格在概念上与单一概念的 Kubernetes 集群方法一致。一切都很简单。新集群只需加入网格,并配置网格以允许每个 Pod 与其他所有 Pod 通信(只要路由策略允许)。
然而,单个网格可能会遇到可扩展性障碍,因为网格控制平面需要处理所有集群中所有工作负载的策略,并且为所有 Pod 更新 Sidecar 可能会给您的集群带来很大负担。
另一种方法是拥有多个独立的网格。属于特定网格的集群中的 Pod 可以直接与同一服务网格中所有其他集群中的 Pod 通信,但必须通过公共端点访问其他服务网格中的集群。
多网格方法更具可扩展性,但更为复杂。您需要考虑如何将系统划分为不同的服务网格,以及新集群加入或离开系统时对整体架构的影响。
在多服务网格的情况下,私有 IP 地址空间管理可能会更加复杂,因为不同的服务网格可能会有冲突的 IP 地址。这意味着你可以为每个网格单独管理 IP 地址空间。
服务网格提供了另一种有趣的解决方案来处理跨集群的连接问题,那就是东西向网关。通过东西向网关方法,不同集群中的工作负载通过每个集群中的专用网关间接通信。这意味着每个集群的私有 IP 地址是未知的,并且每次跨集群通信都会多出一个跳跃。
大规模管理出口流量
一些系统需要积极地访问外部系统。也许你经常从外部系统获取数据,或者你的系统的目的是通过 API 管理一些外部系统。
对于出口流量,可能会有一些特殊问题需要关注。一些第三方组织或甚至某些国家可能会有政策,阻止或限制来自特定地理区域或特定 IP 地址范围(CIDR 块)的流量。例如,中国及其“长城防火墙”因屏蔽和审查大量公司(如谷歌和 Facebook)而闻名。如果你在 GCP 上运行并且需要访问中国,这可能会是一个严重的问题。
除了完全阻止之外,如果你试图大规模访问某些第三方 API,可能还会有流量限制和限速措施。
如果你持续访问这些第三方 API,你甚至可能会被举报,云服务提供商也有可能对你采取各种制裁措施。
让我们考虑一些解决方案来应对这些现实世界中的问题。
如果你当前的云服务提供商无法访问你的目标地址,那么你必须在云服务提供商之外建立一个出口连接。这可以是在另一个云提供商上,或通过一个信誉良好的中介组织。这种代理方法有许多形式,超出了本节的讨论范围。
如果你遇到限流问题,那么可能是因为你从同一个源 IP 地址发送了过多请求。一个有效的解决方案是创建一个具有不同公网 IP 地址的出口节点池,并通过多个不同的 IP 地址分发请求。如果定期轮换公网 IP 地址也会有所帮助,在云平台中通过重新创建实例就可以很容易地获得新的公网 IP 地址。
另一种问题是,如果你与某个第三方公司有协议,并且他们通过白名单允许你提供的某些 IP 地址访问流量,那么你需要管理那些不会变化的静态公网 IP 地址,并确保所有发往该第三方组织的请求都通过这些白名单 IP 地址发送。
最后,为了应对被云服务提供商报告并标记的风险,你可能需要将出口访问隔离到一个单独的账户。大多数云服务提供商的制裁是基于账户级别的。如果你的出口账户被云服务提供商禁用,至少不会影响整个企业。
集群级别的 DNS 管理
大规模的集群,包含大量的 pod 和服务,可能会给 CoreDNS(Kubernetes 的内部 DNS 服务器)带来较高的负载。确保足够的 DNS 容量非常重要,因为集群内大多数工作负载之间的内部通信是通过 DNS 名称进行寻址,而不是直接使用 IP 地址。
建议使用 DNS 自动扩展,默认情况下通常不会启用。详情请参见 kubernetes.io/docs/tasks/administer-cluster/dns-horizontal-autoscaling/。
存储
存储可以说是你基础设施中最为关键的元素。这里存储着你的持久化数据,它是组织的长期记忆。
选择合适的存储解决方案
云中有许多适用于 Kubernetes 集群的存储解决方案,如云原生对象存储、托管存储服务、托管数据库和托管文件系统。你应该深入了解每种存储解决方案的性能、耐用性和成本,并根据存储使用场景进行匹配。
你的基础设施应该始终以云原生的对象存储(即桶存储),如 AWS S3、GCP Google Cloud Storage 或 Azure Blob Storage 为基础。很难想象一个大型的托管 Kubernetes 企业不使用桶存储。
然后,考虑更为结构化或高层次的存储解决方案。如果你希望保持云中立,你可以忽略基于云的托管存储解决方案,部署你自己的解决方案,就像我们在第六章《管理存储》中所看到的那样。
在企业规模下,考虑不同重要性层级的数据存取速度和成本可能是值得的。
数据备份与恢复
规划数据备份与恢复。你的数据是非常宝贵的,数据备份与恢复对于生产环境至关重要。考虑实施可靠且可扩展的数据备份与恢复流程,并确保它们定期经过测试与更新。
你还应该考虑数据保留策略,并且不要自动假设所有数据都必须永久保存。
当然,为了遵守数据隐私法和类似 GDPR 的法规,你还需要具备选择性删除数据的能力。
存储监控
设置存储监控。仅此而已。监控存储性能、使用情况和容量对于在问题影响应用程序的可用性或性能之前识别并解决问题至关重要。设置监控和警报,以便及时了解存储使用、延迟和吞吐量。这对托管存储非常重要,也适用于节点存储,因为日志容易积累并导致节点无法正常工作。
数据安全
实施数据安全措施。保护敏感数据并确保符合数据保护法规对于生产环境至关重要。实施访问控制、加密和数据安全策略以保护数据。
优化存储使用
云中的 Kubernetes 集群可能会很昂贵,存储成本也可能迅速累积。通过删除未使用的数据、使用数据压缩或去重,并设置存储分层,优化存储使用。
测试并验证存储性能
在生产环境中部署应用程序之前,请测试并验证您的存储解决方案的性能,以确保它满足工作负载的性能要求。
通过考虑这些因素并实施最佳实践来管理云中 Kubernetes 集群的生产环境存储,您可以确保应用程序的存储性能既可靠又可扩展。
现在我们已经涵盖了大规模管理 Kubernetes 云基础设施的许多指南和最佳实践,让我们把重点放在集群和节点池的管理上,这是在生产环境中管理多集群 Kubernetes 的核心。
管理集群和节点池
管理您的集群和节点池是大规模 Kubernetes 企业基础设施管理的核心活动。在本节中,我们将讨论几个重要方面,包括配置、资源利用率、升级、故障排除和成本管理。
配置托管集群和节点池
配置集群和节点池有不同的方法。您应根据实际情况明智地选择最适合的方法,因为在这一环节的失败可能导致灾难性的停机。让我们来回顾一下几种选项。所有云服务提供商都通过 API、CLI 和 UI 提供集群和节点池的配置。我强烈建议避免直接使用这些方法,而应使用基于 GitOps 的声明式方法。这里有一些值得考虑的可靠选项。
集群 API
Cluster API 是来自 Cluster Lifecycle SIG 的开源项目。它的目标是使多个 Kubernetes 集群的配置、升级和操作更加简单。它专注于集群和节点池的生命周期。然而,它最初主要是使用 kubeadm 来配置集群的一种方法。稍后添加了对不同云提供商的托管集群支持,但它仍然很年轻。特别是,GKE 不受支持(尽管您可以作为基础设施层在 GCP 上配置 Kubernetes 集群)。AKS 和 EKS 是支持的。
Cluster API 有很大的动力,如果您不操作 GKE,您应该确实研究一下它。
Terraform/Pulumi
Terraform 和 Pulumi 在方法上类似。它们可以在所有云提供商上为您配置集群和节点池。然而,这些工具本身无法响应带外变更,并且在配置后不监控基础设施的状态。它们的内部状态可能会偏离真实世界,这可能导致需要小心“手术”的难以恢复的情况。特别是,节点池经常需要配置或更新,而 Terraform 或 Pulumi 可能无法胜任。如果您对这些工具有很多经验,并且了解它们的怪癖和特殊要求,它们仍然可能是一个不错的选择。
Kubernetes 运算符
另一种选择是使用 Kubernetes 运算符,它们可以将 CRD 与集群和节点池规范与云提供商进行协调。在幕后,运算符将调用云提供商的托管 Kubernetes API。这需要非平凡的工作和编写 Kubernetes 运算符的专业知识,但可以给您带来最终的控制。
您可以尝试使用 Crossplane 而不是编写自己的运算符;然而,目前 Crossplane 的支持似乎相当基础和不完整。扩展范围的一个选择是使用 Upjet 项目(github.com/upbound/upjet)从 Terraform 提供程序生成 Crossplane 控制器。
使用托管节点
您也可以尝试使用托管节点,这样您就永远不需要直接处理节点池和节点的配置。所有云提供商都提供某种形式的托管节点,如 GKE AutoPilot、EKS + Fargate 和 AKS + ACI。对于企业使用情况,我认为您可能需要比完全托管的节点池提供更多的控制。这可能是您工作负载的一部分的一个不错选择。然而,在规模化之后,您将希望优化资源使用和性能,而托管节点池的限制可能会过于严格。
一旦您找出如何配置和管理您的集群和节点池,您应该将注意力转向有效地使用您配置的资源。
装箱和利用率
云资源是昂贵的。在 Kubernetes 上有效使用资源有两个部分:根据其资源请求有效地调度 pod 到节点,以及实际使用它们请求的资源的 pod。
容器打包(Bin packing)是指确保资源请求的总和尽可能接近目标节点上可分配的资源。一旦工作负载被调度到某个节点,通常情况下,即使该节点的资源使用率很低,工作负载也不会被驱逐,但像集群自动扩展器这样的组件可以在这里提供帮助。
资源利用率衡量的是实际使用的资源占请求资源的百分比。一般来说,资源利用率并不是固定的,因为工作负载的资源使用情况可能会在其生命周期中发生显著变化。
容器打包、资源利用率及它们之间的相互作用有许多细微之处。例如,存在不同类型的资源,如 CPU、内存、磁盘和网络。一个节点可能对 CPU 实现了 100% 的容器打包,但内存的容器打包率可能只有 20%。该节点上的网络和非临时磁盘是共享资源,Pod 可以请求它们以确保始终获得一定的资源量。这增加了操作的复杂性和可靠性问题。我们将讨论一些原则和概念,帮助理解这一复杂话题。
理解工作负载形态
工作负载形态是工作负载 CPU 请求与内存请求的比率。在云中,标准的比例是 1 个 CPU 对应 4 GiB 的内存。因此,您可以选择的大多数虚拟机类型都提供此比例的资源容量。有些工作负载需要比这个比例更多的内存或 CPU。所有云服务提供商也提供高内存虚拟机类型,比例为 1 个 CPU 对 8 GiB 内存,以及高 CPU 虚拟机类型,比例为 1 个 CPU 对 2 GiB 内存。
理解工作负载的资源形态对于选择合适的虚拟机类型以优化资源使用至关重要。
例如,如果某个工作负载需要 1 个 CPU 和 8 GiB 的内存,并且你将其调度到一个 CPU 与内存比例为 1:4 的虚拟机类型上,那么你需要将其运行在一个有 2 个 CPU 和 8 GiB 内存的节点上。由于原始工作负载使用了全部的 8 GiB 内存,因此该节点不能运行其他任何 Pod。然而,2 个 CPU 中有 1 个完全没有使用。将该工作负载调度到一个 CPU 与内存比例为 1:8 的节点上会更好,这样可以确保最优的容器打包。
设置请求和限制
为工作负载设置请求和限制是实现适当资源利用的关键。正如你在为 Pod 的容器设置请求时会注意到的那样,Pod 会被调度到一个节点,该节点上至少有足够的资源来满足所有容器请求资源的总和。这些请求的资源将被专门分配给每个容器,直到 Pod 在该节点上运行时。容器如果有空闲资源,可能会使用超出请求的资源。如果你为 CPU 设置了限制,并且容器尝试使用超出限制的 CPU,那么该 Pod 可能会被限制使用。如果你为内存设置了限制,并且容器尝试使用超出限制的内存,那么容器将会因为 OOM(内存溢出)被杀死并重启。
最佳实践是为 CPU、内存,甚至是短暂存储(如果容器使用的话)设置资源请求。你如何知道应该请求多少资源?你可以从一个粗略的估算开始,随着时间的推移监控实际的资源使用情况,并在后续进行微调。
但即便是这种直接的方法也有一些微妙之处。假设一个工作负载使用 2 个至 4 个 CPU,平均为 3 个 CPU。你应该请求 4 个 CPU 并确保工作负载永远不会被限速吗?但这样一来,你就浪费了一个 CPU,因为平均使用量只有 3 个 CPU。如果你请求 3 个 CPU,每当工作负载需要超过 3 个 CPU 时,是否会被限速?这取决于 Pod 调度到的节点上的可用 CPU。如果节点上的总体 CPU 已经饱和,因为所有 Pod 都需要大量的 CPU,那么这种情况是可能发生的。
除了普通的资源请求,你还可以为工作负载分配优先级,这样可以控制高优先级工作负载的调度顺序,并确保它们优先于那些没有优先级或低优先级的工作负载。
是的,调度远非简单。如果你需要复习,可以查看第十五章中理解 Kubernetes 调度器的设计部分,章节来自于扩展 Kubernetes。
让我们将注意力转向限制。一个简单的方法是将限制设置为与请求相等。这通常可以确保容器不会使用超过请求的资源,这使得容器打包变得容易。然而,在实际情况中,工作负载的资源使用量是变化的。通常,申请少于最大使用量,或者有时甚至少于平均使用量,是更经济的。在这种情况下,你可以选择不设置限制,或者将限制设置得比请求值更高。例如,如果一个工作负载使用 1 到 4 个 CPU,你可以决定请求 2 个 CPU,并将限制设置为 4 个 CPU。只请求 2 个 CPU 将允许在同一个节点上容纳更多的 Pod,或者将 Pod 调度到较小的节点上。那么,为什么还要设置限制呢?嗯,设置一些限制可以确保 Pods 不会失控,霸占所有 CPU,导致其他 Pods 的请求资源较低但实际上需要更多 CPU 的 Pods 受到资源饥饿。
设置内存高限制更加重要,特别是对于那些更敏感且不应频繁重启的工作负载,因为任何超出分配内存限制的尝试都会导致容器重启。
使用初始化容器
一些工作负载在启动时需要做大量工作,然后它们的资源需求较低。例如,一个工作负载需要 10 GiB 的内存和 4 个 CPU 来获取一些数据并在内存中处理它,然后才准备好处理请求。然而,一旦运行,它不需要超过 1 个 CPU 和 4 GiB 内存。如果 Pod 是一个长期运行的工作负载,申请 4 个 CPU 和 10 GiB 会非常浪费。在这种情况下,init 容器非常有用。你可以将工作负载拆分为两个容器。所有需要 4 个 CPU 和 10 GiB 内存的初始化工作可以由 init 容器完成,然后主容器只需要请求 1 个 CPU 和 4 GiB 内存。
共享节点与专用节点
在设计节点池时,你需要做出两个基本的选择。共享节点池会调度多个不同的工作负载,并将它们并排运行在同一节点上。专用节点池则会将一个工作负载分配到单一节点(可能是同一工作负载的多个实例)。
共享节点池比较简单。极端情况下,你只有一个节点池,所有 Pod 都调度到该节点池的节点上。如果你有多个共享节点池(例如,一个包含常规节点,另一个包含抢占实例),那么你需要为节点池分配污点,并为工作负载分配容忍度,还需要处理节点和 Pod 的亲和性。
由于你无法确切知道哪一组 Pod 最终会被调度到哪个节点,因此可能会出现装箱效率低下的情况。然而,只要整体的平均工作负载形态与节点的资源比例匹配,大规模的装箱应该接近最优。
工作负载可以请求所需的 CPU、内存和临时存储。然而,节点上存在一些共享资源,如网络和磁盘 I/O,当同一节点上的其他工作负载也可能尝试使用这些资源时,你无法轻松为你的工作负载划分出这些资源。
这就是专用节点池派上用场的地方。像数据库或事件队列这样的关键工作负载需要可预测的网络和磁盘 I/O。将这些工作负载调度到专用节点上,确保它们不必担心其他工作负载争夺共享资源。
在这种情况下,工作负载请求超过 50% 的标准资源(如 CPU 或内存)是合理的,以确保关键工作负载的确切一个 Pod 被调度到节点上。
请记住,系统守护进程也会在节点上运行并具有更高的优先级。如果你的专用工作负载请求了过多的资源,它可能变得无法调度。
在升级后,我遇到过这种问题:节点上的守护进程需要更多资源,导致专用工作负载无法调度,直到它减少了资源请求。
大节点与小节点
在云环境中,节点有多种尺寸,从 1 个核心到数十个甚至数百个核心。你是应该使用大量小节点还是使用较少的大节点?
首先,您必须确保有足够的节点可以容纳您最大的工作负载。例如,如果一个工作负载请求 8 个 CPU,那么您必须有一个至少可用 8 个 CPU 的节点。
那么更大的节点呢?对于更大的节点,效率上有优势。在云中,当您配置(例如)一个具有 1 个 CPU 核心和 4 GiB 内存的节点时,您实际上并没有真正获得这些资源。首先,操作系统、容器运行时和 kube-proxy 会占用一些资源,然后是云提供商决定在每个节点上运行的额外进程,再然后是各种系统守护进程和您自己的守护进程。最终,剩下的资源才可供您的工作负载使用。所有这些在每个节点上始终运行的进程和工作负载需要大量资源。然而,它们所需的资源并不与节点的大小成正比。这意味着在小型节点上,您支付的资源中,只有较小一部分会真正分配给您的 pod。我们来看看一个例子。
这是一个在 AKS 生产集群上运行的真实节点的资源明细。它的虚拟机类型为 Standard_F2s_v2 (learn.microsoft.com/en-us/azure/virtual-machines/fsv2-series)。它有 2 个 CPU 和 4 GiB 内存。然而,可分配的 CPU 和内存为 1.9 个 CPU 和 2.1 GiB。是的,这是正确的。您几乎只能使用节点上 50% 以上的内存:
Capacity:
cpu: 2
memory: 4019488Ki
Allocatable:
cpu: 1900m
memory: 2202912Ki
但故事并未结束。kube-system 中运行着系统守护进程。您可以使用以下命令找到它们:
$ k get po --field-selector spec.nodeName=<node-name> -n kube-system
让我们看看这些工作负载在我们节点上请求的资源:
Namespace Name CPU Requests Memory Requests
--------- ---- ------------ ---------------
kube-system azure-ip-masq-agent-8pkqx 100m (5%) 50Mi (2%)
kube-system azure-npm-twjlx 250m (13%) 300Mi (13%)
kube-system cloud-node-manager-fv5gs 50m (2%) 50Mi (2%)
kube-system csi-azuredisk-node-kqnn7 30m (1%) 60Mi (2%)
kube-system csi-azurefile-node-h8zpw 30m (1%) 60Mi (2%)
kube-system kube-proxy-lgzcf 100m (5%) 0 (0%)
这总共需要 0.56 个 CPU 和 520Mi 内存。如果从可分配的 CPU 和内存中减去这些,我们将剩下 1.4 个 CPU 和 1.58 GiB 的内存。
这真是让人眼前一亮。在一个 2 个 CPU 和 4 GiB 内存的小节点上,我们只能使用 70% 的 CPU 和不到 40% 的内存。除了成本影响之外,如果您计算错误并假设您可以在一个 4 GiB 的节点上调度一个请求 2 GiB 内存的 pod,当您的 pod 因为无法在此节点上运行而处于待定状态时,您将会有一个不愉快的惊讶。
让我们看看大节点。一个 Standard_D64ads_v5 的 Azure 虚拟机有 64 个核心和 256 GiB 的内存。它无疑是一个庞然大物。让我们看看它的容量和可分配资源:
Capacity:
cpu: 64
memory: 263932684Ki
Allocatable:
cpu: 63260m
memory: 250707724Ki
在这里,我们失去了 740 mcpu(相比之下,小节点上是 100 mcpu)和 17 GiB 的内存。听起来很多,但按比例计算,这要好得多。让我们看看系统工作负载,以便了解整个情况:
Namespace Name CPU Requests Memory Requests
--------- ---- ------------ ---------------
kube-system azure-ip-masq-agent-lgzxq 50m (0%) 50Mi (0%)
kube-system azure-npm-s5gsd 100m (0%) 300Mi (0%)
kube-system cloud-node-manager-jntvt 10m (0%) 50Mi (0%)
kube-system csi-azuredisk-node-srcfg 30m (0%) 60Mi (0%)
kube-system csi-azurefile-node-gx247 75m (0%) 60Mi (0%)
kube-system kube-proxy-xgppg 100m (0%) 0 (0%)
这总共需要 0.365 个 CPU 和 520Mi 内存。令人惊讶的是,所请求的 CPU 比小型节点还少,内存请求相同。如果从可分配的 CPU 和内存中减去这些,我们将剩下 62.9 个 CPU 和 238.48 GiB 的内存。
在一个拥有 64 个核心和 256 GiB 内存的大节点上,我们可以使用超过 98% 的 CPU 和超过 93% 的内存。
在资源配置效率方面,大型节点显然在为工作负载提供更多资源方面占据优势。
然而,还有更多的细节和考虑事项需要关注。让我们来看看小型且短暂的工作负载。
小型且短暂的工作负载
假设我们使用大型节点,并且我们的集群已经进行了非常高效的二进制打包。一些部署需要扩展并创建一个新的 pod。如果现有的任何节点都没有足够的空间容纳新 pod,那么就必须为其配置一个新节点。然而,如果新 pod 很小,那么我们实际上会浪费大量资源,因为只在一个大节点上运行一个小 pod。在大规模应用中,尤其是当大量 pod 快速进出时,这可能不是一个问题。然而,考虑以下场景——我们的集群通常运行在 100 个大型节点上。在一次短暂的活动峰值期间,我们的集群扩展到 200 个大型节点,然后活动恢复正常。此时我们的资源利用率为 50%(集群需要 100 个节点中的 200 个)。在理想的情况下,集群自动缩放器最终会缩减空闲节点,直到我们有 100 个合适的二进制打包节点。但在现实世界中,尤其是在存在小型短暂生命周期 pod 的情况下,新的 pod 可能会被任意调度到所有 200 个节点,自动缩放器可能很难进行缩减。稍后我们将在自定义调度部分看到一些选项。
短暂工作负载的另一个问题是,即使它们在现有节点上有空间,如果它们准备时间较长,也可能会浪费资源。考虑一个需要 1 分钟才能准备好,并且平均运行 1 分钟的 pod。这个 pod 即便资源得到了最佳利用,仍然无法做到 50% 以上的效率,因为在调度后,它会在节点上保留 2 分钟的资源,但实际上只工作 1 分钟。如果 pod 需要拉取镜像,那么准备过程可能需要几分钟。
Kubernetes 调度器非常复杂,并且也可以扩展,正如我们在 第十五章、扩展 Kubernetes 中所讨论的那样。我们提到的在不同使用案例中出现的无效 pod 调度问题,可能通过选择不同的评分策略来解决。默认的评分策略 RequestedToCapacityRatio 旨在将工作负载均匀分配到所有节点。这对于紧密的二进制打包并不理想。此时,MostAllocated 评分策略可能更为适合。
查看 kubernetes.io/docs/concepts/scheduling-eviction/resource-bin-packing 获取更多详细信息。
Pod 密度
Pod 密度是每个节点的最大 Pod 数量(Kubernetes 默认为 110)。正如前面提到的,某些资源如私有 IP 地址或系统守护程序的 CPU 和内存可能与 Pod 密度相关。如果 Pod 密度过高,则可能浪费预分配给每个节点的资源以支持多个 Pod。但如果将 Pod 密度设置过低,则可能无法在节点上调度足够多的 Pod。让我们考虑一个具有 64 个 CPU 核心和 256 GiB 内存的大节点。如果 Pod 密度为 100,则最多可以在此节点上运行 100 个 Pod。假设我们有很多只使用 10 mcpu 和 100 MiB 内存的小 Pod。100 个 Pod 只需合计 10 CPU 核心和 10 GiB 内存。如果将 100 个这样的 Pod 安排到一个大节点上,该节点将被严重地低效利用。将浪费 54 个 CPU 核心和 246 GiB 内存。
如果选择共享节点池模型,则是各种工作负载形状的任意混合,资源需求可以安排到节点上。
备用节点池
云服务提供商偶尔会遇到临时容量问题,因此无法提供新节点。此外,如果普通节点需求量很大,抢占式实例随时可能消失。好消息是,这些故障是区域性的,通常仅限于特定的实例类型或虚拟机家族。
解决这个问题的一个好策略是使用备用节点池。
备用节点池是一个空的节点池,禁用自动扩展,具有与另一个活动节点池相同的标签和污点,但使用不同的 VM 家族或不同的节点类型(例如普通与抢占式)。如果活动节点池无法提供更多节点且有未调度的 Pod,则备用节点池可以调整大小并/或变为自动扩展。这将允许未调度的 Pod 被安排到备用节点池,直到与本地节点池的情况得到解决。
如果选择这条路线,则需要制定适当的流程来激活备用节点池,其中包括检测活动节点池中的问题、备用节点池激活的手动或自动化过程,以及在活动节点池恢复正常时的缩减过程。
确保备用节点池在需要时有足够的配额来替换其活动节点池非常重要。
这是对装箱和资源利用的非常彻底的处理。现在让我们把注意力转向升级。
升级 Kubernetes
升级 Kubernetes 可能是一个非常紧张的操作。仓促的升级可能会删除对资源版本的支持,如果部署了不受支持的资源,将会遇到灾难性的故障。使用托管 Kubernetes 有其利弊。在升级方面,任何时候都有一系列支持的版本。
你可以升级到更近期的受支持版本。然而,如果你拖延并忽视升级,云服务提供商将在你落后于支持版本的前沿时自动升级你的集群和节点池。让我们来看看升级 Kubernetes 时你必须掌握的各个要素。
了解你的云服务提供商的生命周期
云服务提供商不能支持所有存在的 Kubernetes 版本。了解你的集群和节点池当前版本的淘汰时间是至关重要的。所有云服务提供商都有一套系统化的流程,并广泛分享相关信息。以下是三个主要云服务提供商的相关链接:
-
AWS EKS:
docs.aws.amazon.com/eks/latest/userguide/kubernetes-versions.html -
Azure AKS:
learn.microsoft.com/en-us/azure/aks/supported-kubernetes-versions -
Google GKE:
cloud.google.com/kubernetes-engine/docs/release-schedule
例如,当前在写作时,AKS 支持版本 1.23 至 1.26。此外,每个版本都有一个官方的生命周期结束日期。例如,1.23 的生命周期结束日期是 2023 年 4 月。如果你的集群仍在 1.23 版本上,那么 AKS 可能会自动将你的集群升级到 1.24 版本。云服务提供商的升级过程是渐进的,按区域进行,可能需要几周时间。
所有云服务提供商都提供 API 和 CLI,用于检查每个区域的版本(包括补丁版本)的准确列表。
例如,目前这是支持的在美国中部地区的 AKS 版本:
$ az aks get-versions --location centralus --output table
KubernetesVersion Upgrades
------------------- -----------------------
1.26.0(preview) None available
1.25.5 1.26.0(preview)
1.25.4 1.25.5, 1.26.0(preview)
1.24.9 1.25.4, 1.25.5
1.24.6 1.24.9, 1.25.4, 1.25.5
1.23.15 1.24.6, 1.24.9
1.23.12 1.23.15, 1.24.6, 1.24.9
正如你所看到的,对于每个次要版本,都有多个补丁版本。甚至会明确指出你可以自行升级到哪些版本。由于安全原因,云服务提供商可能随时停止对某些补丁版本的支持。
让我们来讨论控制平面升级的过程。
升级集群
使用云中的托管 Kubernetes 时,你不需要负责控制平面的操作,但你仍然需要管理升级过程。你有两个选择:
-
自动升级
-
手动升级
在自动升级中,云服务提供商会根据他们的计划更新你的集群,但你仍然需要确保集群中的资源版本与你的新版兼容。手动升级则要求你自己进行升级,但可以让你对升级时机有更多的控制。例如,你可以选择提前更新,以便享受一些新功能的好处。
记住,手动升级并不意味着你可以永远停留在同一个 Kubernetes 版本上。如果你落后于最小支持版本,云服务提供商将强制进行升级。
Kubernetes 每大约三个月发布一个新版本。云服务提供商通常支持大约 4 个版本。这意味着,如果你刚刚升级到最新的支持版本,你可以大约等待一年再进行升级,但那时你将处于最低支持版本,这意味着你将需要每三个月进行一次升级。
请注意,应该一次只升级一个小版本的控制平面。如果你当前使用的是 1.24 版本,并且想要升级到 1.26,你必须先从 1.24 升级到 1.25,然后再从 1.25 升级到 1.26。
最终结论是,升级 Kubernetes 控制平面是一个标准操作,每年会进行多次。你应该为此制定一个简化的流程。
让我们来看看涉及的内容。
升级计划
你应该规划你的升级,并与集群用户协调。控制平面升级通常需要 20-45 分钟。这是一个不会干扰工作负载的操作。你的工作负载会继续运行,并且新 pod 会被调度到现有节点。然而,节点池操作在控制平面升级期间可能会被阻塞。
如果你运行多个集群,并采用冗余方案,最好逐步进行升级,并从非关键集群(例如开发或预生产环境)开始。
我建议为每个命名空间指定负责人(工程师或团队)。通知所有负责人有关即将进行的升级,以便他们为转换不兼容资源预留时间。
检测不兼容的资源
升级的主要问题是,系统的功能可能会受到影响或完全损坏,因为它使用了不再支持的资源。在大多数情况下,一个特定版本的资源将被移除,并且会提供更新的版本。
但你不必等到最后一刻才去忙着替换已移除的资源或版本。Kubernetes 有一个弃用政策,资源会在完全移除之前被弃用几个版本。我建议在每次升级之前确保所有弃用的资源都已更新或替换。这将确保升级过程不那么紧张,因为即使你没有及时更新所有资源,弃用的资源仍然会被新版本支持,你将有更多时间在它们被完全移除之前更新它们。
Kubernetes 会发布迁移指南,详细说明每个版本中已弃用和移除的 API。请参阅 kubernetes.io/docs/reference/using-api/deprecation-guide。
例如,Kubernetes 1.25 停止提供 API 版本为batch/v1beta1的CronJob资源。从 Kubernetes 1.21 起,batch/v1版本的CronJob资源已经可用。理想情况下,在你升级到 Kubernetes 1.21 后,你应该已将所有的CronJob资源更新为使用batch/v1,并且当你升级到 Kubernetes 1.25 时,batch/v1beta1被移除就不会成为问题,因为你已经使用了受支持的版本。
有几种方法可以确保你检测到当前使用的所有已弃用和/或已移除的资源。你可以通过手动阅读弃用指南,并扫描你的代码来检测不兼容的资源。大多数发布版本并没有很多弃用或移除的资源。然而,某些版本可能有多达十个不同的资源被弃用或移除。例如,Kubernetes 1.25 停止提供七个不同版本的资源。
更系统的方法是使用像kube-no-trouble这样的工具(github.com/doitintl/kube-no-trouble),它扫描你的集群,并能输出一个弃用资源的列表。
这里是如何安装它的方法:
$ sh -c "$(curl -sSL 'https://git.io/install-kubent')"
>>> kubent installation script <<<
> Detecting latest version
> Downloading version 0.7.0
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0
100 11.7M 100 11.7M 0 0 4154k 0 0:00:02 0:00:02 --:--:-- 8738k
> Done. kubent was installed to /usr/local/bin/.
我有一个 1.25 集群,目前没有包含任何已弃用的资源。然而,在 Kubernetes 1.26 中,版本 autoscaling/v2beta2 的HorizontalPodAutoscaler将被移除,因为它自 Kubernetes 1.23 起已被弃用。让我们创建这样的资源。集群中有一个kyverno部署:
$ k get deploy -n kyverno
NAME READY UP-TO-DATE AVAILABLE AGE
kyverno 1/1 1 1 64d
这是一个将最小副本数设置为 1,最大副本数设置为 3 的 HPA:
$ cat <<EOF | k apply -f -
apiVersion: autoscaling/v2beta2
kind: HorizontalPodAutoscaler
metadata:
name: kyverno
namespace: kyverno
spec:
maxReplicas: 3
metrics:
- resource:
name: cpu
target:
averageUtilization: 80
type: Utilization
type: Resource
minReplicas: 1
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: kyverno
EOF
Warning: autoscaling/v2beta2 HorizontalPodAutoscaler is deprecated in v1.23+, unavailable in v1.26+; use autoscaling/v2 HorizontalPodAutoscaler
horizontalpodautoscaler.autoscaling/kyverno created
如你所见,当你创建一个已弃用的资源版本时,kubectl 会给出一个非常好的警告,告诉你该资源何时被弃用(1.23),何时会被移除(1.26),以及用哪个版本来替代它(autoscaling/v2)。
这很好,但还不够。你可能通过 CI/CD 创建资源,这可能不会收到相同的警告,甚至即使收到,可能也不会显示出来,因为这不是一个错误。然而,如果你在集群版本低于 1.23 时创建了 HPA,那么你就不会收到任何警告,因为当时它并未被弃用。
让我们看看 kubent 是否能检测到已弃用的 HPA:
$ kubent
6:27AM INF >>> Kube No Trouble `kubent` <<<
6:27AM INF version 0.7.0 (git sha d1bb4e5fd6550b533b2013671aa8419d923ee042)
6:27AM INF Initializing collectors and retrieving data
6:27AM INF Target K8s version is 1.25.3
6:28AM INF Retrieved 7 resources from collector name=Cluster
6:28AM INF Retrieved 109 resources from collector name="Helm v3"
6:28AM INF Loaded ruleset name=custom.rego.tmpl
6:28AM INF Loaded ruleset name=deprecated-1-16.rego
6:28AM INF Loaded ruleset name=deprecated-1-22.rego
6:28AM INF Loaded ruleset name=deprecated-1-25.rego
6:28AM INF Loaded ruleset name=deprecated-1-26.rego
6:28AM INF Loaded ruleset name=deprecated-future.rego
__________________________________________________________________________________________
>>> Deprecated APIs removed in 1.26 <<<
------------------------------------------------------------------------------------------
KIND NAMESPACE NAME API_VERSION REPLACE_WITH (SINCE)
HorizontalPodAutoscaler kyverno kyverno autoscaling/v2beta2 autoscaling/v2 (1.23.0)
是的,它能。你会得到相同的信息:什么时候会被移除,什么时候被弃用,以及该用什么替代它。
更新不兼容的资源
更新不兼容的资源可能需要对你的清单文件进行一些更改。如果 API 的更改只是增加了新的字段,那么你只需要更改 API 版本就可以了。然而,有时可能需要额外的更改。
好的,我们即将升级我们的集群,并且检测到了一些不兼容的资源。Kubectl 和 kubectl-convert 插件可以帮助我们。按照此处的说明安装插件:kubernetes.io/docs/tasks/tools/#kubectl。让我们转换我们的 HPA 清单并看看它的样子:
$ k convert -f hpa.yaml
apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
creationTimestamp: null
name: kyverno
namespace: kyverno
spec:
maxReplicas: 3
minReplicas: 1
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: kyverno
targetCPUUtilizationPercentage: 80
status:
currentReplicas: 0
desiredReplicas: 0
转换成功,但创建了一些不必要的字段。creationTimestamp: null 是无用的,因为它会在资源实时更新时被更新。另外,状态也是无用的,因为这只是一个清单文件,状态会在运行时更新。
然而,主要的区别在于 apiVersion 已更改为 apiVersion: autoscaling/v1,并且目标 CPU 百分比现在作为单一字段指定:
targetCPUUtilizationPercentage: 80
in autoscaling/v1beta1 it was specified as:
metrics:
- resource:
name: cpu
target:
averageUtilization: 80
type: Utilization
使用 kubectl-convert 可以节省时间,并且它是一个经过充分测试的工具。
处理移除的功能
还有一种情况需要处理,那就是完全移除一个没有升级路径的功能。Kubernetes 1.25 完全移除了对 Pod 安全策略(PSP)的支持。PSP 应用于 Pod 的做法曾让许多用户感到困惑。请查看此链接了解更多详情:kubernetes.io/blog/2021/04/06/podsecuritypolicy-deprecation-past-present-and-future/。
如果你使用了 PSP,那么当升级到 Kubernetes 1.25 时,你的 PSP 将不再有效。Kubernetes 开发者并非直接移除该功能而没有替代方案。实际上,PSP 有两个替代方案:
-
Pod 安全准入
-
第三方准入控制器
Pod 安全管理员是一个简化的解决方案,可能是 PSP(Pod 安全策略)的完整替代品,也可能不是。Kubernetes 开发者发布了一个详细的迁移指南。请查看:kubernetes.io/docs/tasks/configure-pod-container/migrate-from-psp/。
如果选择第三方(例如 Kyverno),则应查看其文档。Kyverno 提供了许多用于 Pod 安全的示例策略,迁移过程相当简单直接。
升级节点池
升级多个集群的节点池可能是一项重大任务。如果你有几十到几百个集群,并且每个集群都有多个节点池(5-20 个节点池并不少见),那么你得准备好进行一场大冒险。控制平面升级(在确保工作负载与新版本兼容后)通常是快速且无痛的。节点池升级则非常困难。实际上,升级一个大型基于 Kubernetes 的系统,涉及数十到数百个节点池、成千上万个节点,可能需要几周的时间才能完成。
与控制平面升级同步
Kubernetes 对控制平面和工作节点的版本施加了限制。Kubernetes 节点组件的版本可能比控制平面落后两个小版本。如果控制平面处于版本 N,那么节点池可能处于版本 N-2。由于节点池的升级比控制平面的升级更具破坏性且劳动强度更大,我建议只将节点池升级到每个 Kubernetes 的偶数版本。例如,假设我们从一个 Kubernetes 集群开始,控制平面和节点的版本都是 1.24。当我们升级到 1.25 时,我们只升级控制平面到 1.25,并将节点池保持在兼容的 1.24 版本。然后,当需要升级到 1.26 时,我们首先将控制平面从 1.25 升级到 1.26,接着开始将所有节点池从 1.24 直接升级到 1.26。让我们看看如何进行节点池的升级。
如何执行节点池升级
节点池的升级需要创建一个新的节点池。无法在现有节点池中升级节点,也不能将新版本的节点添加到现有节点池中。节点版本是节点池的基本属性之一。这意味着你实际上并不是在升级现有的节点池,而是在替换你的节点池。首先,你创建一个新的节点池,扩展它,并开始从旧节点池排空节点,直到所有的 pods 都运行在新的节点池中,然后你就可以删除旧的(现在空的)节点池。
如果你原先的节点池是一个自动扩展节点池,那么在开始升级过程之前,必须关闭自动扩展;否则,你从旧节点池中驱逐的 pods 可能会重新调度到旧节点池。以下是升级节点池时你需要采取的确切步骤:
-
创建一个新的节点池,确保其规格(实例类型、标签、容忍度)与现有节点池完全相同,并使用新的版本。
-
你可以提前为新节点池分配一些实例,以便它们准备好调度来自旧节点池的 pods。
-
在旧节点池中关闭自动扩展。
-
将旧节点池中的所有节点设置为隔离状态,以防新 pods 被调度到旧池。
-
排空旧节点池的节点。
-
观察并处理问题。
-
等待集群自动扩展器删除空的旧池节点,或者你自己删除它们以加速过程。
让我们来看看一些可能会延迟甚至阻碍升级过程的问题。
并行排空
如果你需要升级多个节点池,每个节点池中有大量节点,你可能会决定配置新的节点池,并开始同时或大批量地排空所有的节点池。这可能会导致你超出配额或遇到云提供商的容量问题。
你应该关注配额,并确保在升级时你的配额充足。如果你接近配额上限,建议在进行复杂操作(如节点池升级)之前先提高配额。你最不希望发生的情况是,在进行升级过程中,由于业务需求需要扩展容量时,才发现已经达到了配额上限。
处理容量问题并加速云服务提供商为新节点分配实例的速度的一个好策略是预先分配这些实例。同样,这要求你同时为旧节点池和新节点池拥有足够的配额。
让我们来了解一下如果你没有预先分配新节点池中的节点会发生什么。当你从旧节点池中清理一个节点时,所有的 Pod 会被驱逐出该节点并变为待处理 Pod。Kubernetes 会尝试将这些 Pod 调度到现有的节点上,如果有可用的节点。旧节点池会被标记为不可用,因此 Kubernetes 要么可以在其他现有节点池中找到合适的节点(这是一件提高箱子装载效率的好事),要么集群自动扩展器需要为新节点池配置一个新的节点。这需要几分钟的时间。如果你同时清理多个节点,那么这些节点上的所有 Pod 都会在几分钟内处于待处理状态,直到可以为其配置新的节点,这时你的系统容量会下降。另外,如果云服务提供商的容量出现问题,可能它无法为新节点分配资源,你的 Pod 会一直处于待处理状态,直到资源能够提供。
预先分配节点意味着新节点池中的节点已经准备就绪。只要一个 Pod 从旧节点池中被驱逐,它就会立即被调度到新节点池中的可用节点上。
处理没有 PDB 的工作负载
当清理节点时,Kubernetes 会考虑 Pod 中断预算 (PDBs) 的情况。如果某个部署的 PDB 设置为只能有一个 Pod 不可用,而该节点上有两个此部署的 Pod,那么 Kubernetes 会只驱逐一个 Pod,并等待它被调度完成后再驱逐另一个 Pod。然而,如果你的工作负载没有 PDB,这意味着 Kubernetes 可以同时驱逐这些工作负载的所有 Pod。对于大多数工作负载来说,这是不可接受的。你应该识别出这些工作负载,并与其所有者协作为其添加 PDB。需要注意的是,在一次清理所有节点的场景下,即使工作负载在不同节点上运行了多个 Pod,没有 PDB 的工作负载仍然非常脆弱。
处理 PDB 为零的工作负载
然而,无法驱逐的 Pod 是节点池升级中的另一大问题。如果一个节点上有无法驱逐的 Pod,那么该节点就无法被清理,Kubernetes 会无限期地等待(或者直到该 Pod 被手动删除)才能完成节点的清理过程。这可能会无限期地阻碍升级进程,通常需要与工作负载所有者协作来解决这一问题。
如果某个工作负载有一个 PDB,且其 minUnavailable: 0,则意味着 Kubernetes 不允许驱逐任何一个 Pod,无论该工作负载有多少副本。
一些工作负载(通常是有状态的)比其他工作负载更为敏感,更不希望被打扰。当然,这是不现实的期望,因为节点本身可能出现故障,或者底层虚拟机由于云服务提供商问题可能消失,导致调度到该节点上的 Pod 必须被驱逐。最好与工作负载的拥有者合作,提出一个最小化干扰的解决方案,同时仍能让升级过程继续进行。这些工作需要在升级过程开始之前就做好准备。你不希望遇到某个工作负载让整个节点池升级过程停滞不前的情况,并且还不得不请求工作负载的拥有者允许你驱逐一个 Pod。
但是,除了严格的 PDB-zero 工作负载之外,你还可能遇到有效的 PDB-zero 情况。考虑一个 PDB 为 minUnavailable: 1 的工作负载。这种情况比较常见,意味着工作负载允许一次一个 Pod 不可用。在对该工作负载的 Pod 进行驱逐时,Kubernetes 允许驱逐一个 Pod,只要所有其他 Pod 都在正常运行。然而,如果该工作负载的任何一个 Pod 因为某种原因处于待处理状态或无法准备就绪,那么实际上该工作负载已经有一个 Pod 不可用了,从而导致升级过程再次被暂停。
这里的最佳实践是,在升级过程开始之前识别这些工作负载,并确保所有工作负载都处于健康状态,并能够参与节点池的升级过程。
然而,即使你提前做好了所有准备工作,在升级过程中一些工作负载可能会进入不健康状态(记住,我们讨论的是一个可能持续数周的过程)。
我建议对升级过程的进度进行强有力的监控,及时发现卡住的 Pod,并与相关人员协作解决问题。如果某些 Pod 被调度到旧的节点池且无法准备就绪,那么简单的解决方案是删除这些 Pod,确保它们被调度到新的节点池,然后让工作负载的拥有者在新节点池上解决问题。
其他情况可能需要更具创造性的解决方案。
让我们将注意力转向集群中可能出现的各种问题,以及如何处理它们,特别是在大规模情况下。
故障排除
在本节中,我们将介绍生产集群中的故障排除过程以及采取的逻辑步骤。Pod 生命周期涉及多个阶段,且每个阶段都有可能发生故障。此外,Pod 容器经历自己的小生命周期,其中初始化容器运行完成后,主容器才会开始运行。让我们看看在这一过程中可能出现的问题以及如何处理它们。
首先,我们来看待处理待处理的 Pod。
处理待处理的 Pod
当一个新 Pod 被创建时,Kubernetes 过去会将其置于待处理状态,并尝试找到一个节点来调度它。然而,从 Kubernetes 1.26 开始,Pod 可以进入一个更早的状态,此时 Pod 无法被调度。
让我们创建一个新的 1.26 版本的 kind 集群,命名为“trouble”,并启用 Pod 调度就绪功能。这里是配置文件(cluster-config.yaml):
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
name: trouble
nodes:
- role: control-plane
featureGates:
"PodSchedulingReadiness": true
这是创建 kind 集群的方法:
$ kind create cluster -n trouble --config cluster-config.yaml --image kindest/node:v1.26.0
Creating cluster "trouble" ...
 Ensuring node image (kindest/node:v1.26.0) 
 Preparing nodes 
 Writing configuration 
 Starting control-plane 
 Installing CNI 
 Installing StorageClass 
Set kubectl context to "kind-trouble"
You can now use your cluster with:
kubectl cluster-info --context kind-trouble
接下来,我们将创建一个新的命名空间,名为 trouble,然后从那里开始。
$ k create ns trouble
namespace/trouble created
让我们创建一个带有调度门控的 Pod,名为 no-scheduling-yet:
$ cat <<EOF | k apply -f -
apiVersion: v1
kind: Pod
metadata:
name: some-pod
namespace: trouble
labels:
app: the-app
spec:
schedulingGates:
- name: no-schedule-yet
containers:
- name: pause
image: registry.k8s.io/pause:3.8
EOF
pod/some-pod created
$ k get po -n trouble
NAME READY STATUS RESTARTS AGE
some-pod 0/1 SchedulingGated 0 10s
如你所见,该 Pod 的状态为 SchedulingGated。
调度门控的好处在于,如果 Pod 因为配额等问题无法调度,而这些问题需要外部解决,那么处于这种状态的 Pod 不会对 Kubernetes 调度器产生太多的干扰,调度器会忽略它。外部问题解决后,你(或更可能是运维人员)可以移除该功能门控,Pod 将变为待处理状态,准备被调度。
现在,让我们把注意力转向待处理的 Pods。Pod 处于待处理状态是正常的;然而,如果一个 Pod 已经待处理超过几分钟,说明有问题,我们需要进行调查。我建议为待处理超过 X 分钟的 Pods 设置一个警报(X 的合理范围通常是 10 到 60 分钟)。
待处理的 Pods 有两种类型:暂时待处理的 Pods 和永久待处理的 Pods。暂时待处理的 Pods 可能会被调度到现有的某个节点池;然而,当前没有任何节点有足够的空间。如果节点池启用了自动扩容,集群自动扩容器将尝试创建新节点。如果节点池没有启用自动扩容,那么该 Pod 将保持待处理状态,直到其他 Pods 完成或被驱逐以腾出空间。另一类暂时不可调度的 Pods 是当目标命名空间的资源配额已达到上限时。例如,命名空间的资源配额为 1 CPU,并创建了一个包含 3 个副本的部署,其中每个 Pod 请求 0.5 CPU。此时,只有 2 个 Pods 能符合命名空间配额,第三个 Pod 会处于待处理状态:
cat <<EOF | k apply -f -
apiVersion: v1
kind: ResourceQuota
metadata:
name: cpu-requests
namespace: trouble
spec:
hard:
requests.cpu: "1"
EOF
resourcequota/cpu-requests created
现在,让我们创建部署并看看会发生什么:
cat <<EOF | k apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: some-deployment
namespace: trouble
labels:
app: the-app
spec:
replicas: 3
selector:
matchLabels:
app: the-app
template:
metadata:
namespace: trouble
labels:
app: the-app
spec:
containers:
- name: pause
image: registry.k8s.io/pause:3.8
resources:
requests:
cpu: "0.5"
EOF
deployment.apps/some-deployment created
最终我们只会得到两个正在运行的 Pods,正如预期的那样:
$ k get deploy -n trouble
NAME READY UP-TO-DATE AVAILABLE AGE
some-deployment 2/3 2 2 5m3s
$ k get po -n trouble
NAME READY STATUS RESTARTS AGE
some-deployment-7f876df998-fc7kq 1/1 Running 0 4m34s
some-deployment-7f876df998-htdsn 1/1 Running 0 4m34s
注意,在这种情况下并没有第三个待处理的 Pod。Kubernetes 足够智能,能够在这种情况下只创建两个 Pods。
原因是命名空间配额:
$ k get deploy some-deployment -o yaml -n trouble | grep forbidden -A 2
message: 'pods "some-deployment-7f876df998-84z9s" is forbidden: exceeded quota:
cpu-requests, requested: requests.cpu=500m, used: requests.cpu=1, limited: requests.cpu=1'
reason: FailedCreate
永久待处理的 Pods 是指无法在任何可用的节点池上调度的 Pods,因此,创建一个新节点也无法解决问题。这类永久不可调度的 Pods 分为几类:
-
所有节点池都有污点,而该 Pod 没有适当的容忍度。
-
Pod 请求的资源超过了任何节点池上的可用资源。
-
该 Pod 正在等待持久卷。
-
Pod 的
nodeSelector或nodeAffinity设置不正确。
让我们删除之前的部署和资源配额,并查看一个请求了过多资源(666 个 CPU 核心)的 Pod 示例:
$ k delete resourcequota cpu-requests -n trouble
resourcequota "cpu-requests" deleted
$ k delete deploy some-deployment –n troublet
deployment.apps "some-deployment" deleted
$ cat <<EOF | k apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: some-deployment
namespace: trouble
labels:
app: the-app
spec:
replicas: 1
selector:
matchLabels:
app: the-app
template:
metadata:
labels:
app: the-app
spec:
containers:
- name: pause
image: registry.k8s.io/pause:3.8
resources:
requests:
cpu: "666"
memory: 1Gi
EOF
deployment.apps/some-deployment created
如果我们查看当前创建的 Pod,能够看到它确实处于等待状态:
$ k get po -n trouble
NAME READY STATUS RESTARTS AGE
some-deployment-bbf88d559-pdf8p 0/1 Pending 0 2m37s
要理解为什么它处于待处理状态,我们可以查看 Pod 的状态:
$ k get po some-deployment-bbf88d559-pdf8p -o yaml -n trouble | yq .status
conditions:
- lastProbeTime: null
lastTransitionTime: "2023-03-26T05:35:30Z"
message: '0/1 nodes are available: 1 Insufficient cpu. preemption: 0/1 nodes are available: 1 No preemption victims found for incoming pod..'
reason: Unschedulable
status: "False"
type: PodScheduled
phase: Pending
qosClass: Burstable
信息非常明确,说明 1 个节点中有 0 个可用于调度。它甚至指出 1 个节点的 CPU 不足。如果集群中有其他节点存在其他原因,信息中也会列出。
待处理的 Pod 不占用资源,也不占用节点空间;然而,它们会给 API 服务器带来一定压力,同时这意味着一些工作负载无法完成任务,它们在等待节点的调度,这在生产环境中可能非常严重。请留意你的待处理 Pod,确保快速解决任何问题。
下一类问题是关于已调度到节点但无法运行的 Pod。
处理调度到节点但未运行的 Pod
有多种原因可能导致调度的 Pod 无法运行。其中一个最常见的原因是无法拉取 Pod 容器所需的镜像。kubelet 会不断尝试,而 Pod 的状态将显示为 ErrImagePull:
cat <<EOF | k apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: no-such-image
namespace: trouble
labels:
app: no-such-image
spec:
replicas: 1
selector:
matchLabels:
app: no-such-image
template:
metadata:
labels:
app: no-such-image
spec:
containers:
- name: no-such-image
image: no-such-image:6.6.6
EOF
deployment.apps/no-such-image created
让我们检查一下 Pod:
$ k get po -l app=no-such-image -n trouble
NAME READY STATUS RESTARTS AGE
no-such-image-77585fd5b4-tqpv2 0/1 ErrImagePull 0 27s
要查看更详细的消息,我们可以检查状态中的 containerStatuses 字段:
$ k get po no-such-image-77585fd5b4-tqpv2 -n trouble -o yaml | yq '.status.containerStatuses[0].state'
waiting:
message: Back-off pulling image "no-such-image:6.6.6"
reason: ImagePullBackOff
镜像拉取错误可能与镜像配置错误有关。镜像名称或标签可能错误。然而,镜像本身可能正确,但可能已被意外删除。如果尝试从私有注册表拉取镜像,可能是因为没有正确的权限。最后,镜像注册表可能不可用。例如,Docker Hub 经常会有速率限制。
你可能更倾向于从一个你控制的单一来源拉取所有镜像,在那里你可以扫描和管理镜像,并确保镜像不会从你这里消失。如果你使用的是云服务,每个云提供商都会提供自己的镜像注册表。在大多数情况下,这应该是首选选项。使用私有注册表可能能节省一些费用,且在混合云场景下,你可能更倾向于使用不同的解决方案。
另一个导致 Pod 无法启动的原因是 init 容器没有完成:
cat <<EOF | k apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: infinite-init
namespace: trouble
labels:
app: infinite-init
spec:
replicas: 1
selector:
matchLabels:
app: infinite-init
template:
metadata:
name: infinite-init
labels:
app: infinite-init
spec:
initContainers:
- name: init-pause
image: registry.k8s.io/pause:3.8
containers:
- name: main-pause
image: registry.k8s.io/pause:3.8
EOF
deployment.apps/infinite-init created
检查 Pod 状态时,我们可以看到它卡在 init 阶段,因为我们的 init 容器处于暂停状态,这个状态永远不会完成:
$ k get po -l app=infinite-init -n trouble
NAME READY STATUS RESTARTS AGE
infinite-init-d555945fb-dccbk 0/1 Init:0/1 0 46s
有时 Pod 启动了,但容器不断失败。在这种情况下,你需要检查 Pod 的日志或 Dockerfile 以查明原因。下面是一个因为 Dockerfile 命令以退出代码 1 退出而不断崩溃的 Pod:
cat <<EOF | k apply -f -
apiVersion: v1
kind: Pod
metadata:
name: run-container-error
namespace: trouble
spec:
containers:
- name: run-container-error
image: bash
command:
- exit
- "1"
EOF
pod/run-container-error created
结果是 Pod 会有 RunContainerError 状态。Kubernetes 会继续重启 Pod(假设默认的重启策略是一直重启):
$ k get po run-container-error -n trouble
NAME READY STATUS RESTARTS AGE
run-container-error 0/1 RunContainerError 4 (8s ago) 100s
让你的 Pods 和容器处于运行状态是一个好的开始,但这还不够。为了让 Kubernetes 向你的 Pods 发送请求,所有容器必须处于就绪状态。让我们看看你可能会遇到哪些问题。
处理未就绪的运行中 Pods
如果所有初始化容器都已完成,且所有主容器都在没有错误的情况下运行,那么探针将发挥作用。如果没有定义任何探针,或者所有容器的所有探针都成功,Kubernetes 会认为具有运行容器的 Pod 准备好接收请求。启动探针最初会检查,直到它成功。如果失败,Pod 将不被认为是就绪的。如果容器的启动挂起,Pod 将永远不会就绪。
Kubernetes 最终会杀死并重启你的容器,启动探针将有另一次机会。
这是一个部署示例,其中主容器有一个总是失败的启动探针(暂停容器甚至不在端口 80 上监听)。Pod 永远不会进入就绪状态。经过启动探针定义的几次重试和延迟后,Kubernetes 会重启容器,循环将继续:
Cat <<EOF | k apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: bad-startup-probe
namespace: trouble
labels:
app: bad-startup-probe
spec:
replicas: 1
selector:
matchLabels:
app: bad-startup-probe
template:
metadata:
name: bad-startup-probe
labels:
app: bad-startup-probe
spec:
containers:
- name: pause
image: registry.k8s.io/pause:3.8
startupProbe:
httpGet:
path: /
port: 80
failureThreshold: 3
periodSeconds: 10
initialDelaySeconds: 5
timeoutSeconds: 2
EOF
deployment/bad-startup-probe created
检查 Pod 显示 Pod 处于 CrashloopBackoff 状态,Kubernetes 一直在重启容器:
$ k get po -l app=bad-startup-probe –n trouble
NAME READY STATUS RESTARTS AGE
bad-startup-probe-66fb7cf4fd-qw8kp 0/1 CrashLoopBackOff 160 (3m56s ago) 8h
注意,重启之间的延迟会呈指数增长,以避免一个坏容器给 API 服务器带来很大的压力,并且 kubelet 需要频繁重启容器。
如果没有启动探针或启动探针成功,接下来的障碍就是就绪探针。它的工作方式与启动探针非常相似,但 Kubernetes 不会重启容器。它只会继续检查就绪探针。当就绪探针失败时,Pod 将从任何匹配其标签的服务的端点列表中移除,因此它不会处理任何请求。然而,Pod 会保持存活并消耗节点上的资源。
让我们来看实际操作:
cat <<EOF | k apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: bad-readiness-probe
namespace: trouble
labels:
app: bad-readiness-probe
spec:
replicas: 1
selector:
matchLabels:
app: bad-readiness-probe
template:
metadata:
name: bad-readiness-probe
labels:
app: bad-readiness-probe
spec:
containers:
- name: pause
image: registry.k8s.io/pause:3.8
readinessProbe:
httpGet:
path: /
port: 80
failureThreshold: 3
periodSeconds: 10
initialDelaySeconds: 5
timeoutSeconds: 2
EOF
deployment.apps/bad-readiness-probe created
如你所见,Pod 已运行了一小时,但从未处于就绪状态,并且没有被重启:
$ k get po -l app=bad-readiness-probe –n trouble
NAME READY STATUS RESTARTS AGE
bad-readiness-probe-7458b65d98-5jrjq 0/1 Running 0 60m
最后一种探针是存活探针。它的工作方式与启动探针类似(容器将被重启,Pod 将进入 CrashloopBackoff),不同之处在于它会定期检查,即使启动探针只会检查一次,一旦成功,就不会再进行检查。
启动探针和存活探针都需要的原因是,有些容器需要更长的启动时间,但一旦初始化完成,存活检查的周期应该较短。
这涵盖了故障排除 Pod 生命周期的问题。当 Pods 被调度但没有运行或未就绪时,会带来成本影响。让我们继续讨论成本管理。
成本管理
在云中大规模运行 Kubernetes 时,一个主要的关注点是基础设施的成本。云提供商为你的 Kubernetes 集群提供了各种基础设施选项和服务,这些服务非常昂贵。为了控制成本,确保你遵循最佳实践,如:
-
拥有成本意识
-
成本可观察性
-
智能资源选择
-
高效使用资源
-
折扣、预留实例和抢占实例
-
投资本地环境
让我们逐一回顾它们。
成本思维
工程师通常忽视成本或将其排在优先级列表的后面。我通常按以下顺序思考:让它工作,确保它快速,让它持久,确保它安全,最后才考虑让它便宜。这不一定是坏事,特别是对于初创公司或新项目来说。增长和速度通常是最优先的。毕竟,如果你没有一个好的产品,也没有客户,那么即使成本为零,业务也会失败。
此外,当系统规模较小时,即使存在大量浪费,绝对成本可能相对较小。再加上云提供商通过慷慨的积分吸引公司。
然而,如果你是一个大型企业的一部分,或者你的创业公司成功并发展壮大,那么在某个时刻,成本将成为一个重要的关注点。到那时,你需要转变思维,把成本作为你所做一切事情的首要考虑。成本可能与其他计划不一致。例如,每个人都喜欢帕累托改善。如果你通过使用少 20%的虚拟机来完成同样的任务,那么你将自动节省大量资金,而不会对其他方面产生负面影响。
但那些轻松的胜利和低挂的果实最终会枯竭。然后,你将面临更困难的决策。例如,将过去一周的数据缓存到内存中将提供非常好的响应时间,但代价很大。如果你只缓存一天呢?
可用性和冗余通常与成本相冲突。你是否真的需要在多个可用区、区域和云提供商之间设置一个完整的零停机时间的主动-主动架构,还是在发生灾难性故障时,可以接受一些停机并从备份中恢复?
你可能最终会选择更昂贵的选项,但你应该明确了解你为此支付了多少,并确保你获得的价值大于成本。
这引出了下一个话题:成本可观察性。
成本可观察性
要在 Kubernetes 和云环境中管理你的基础设施成本,你必须拥有强大的成本导向可观察性。让我们来看看一些实现这一目标的方法。
标签
标签是将每个资源与一组标签或标识相关联。从成本的角度来看,标签应该使你能够将任何基础设施资源的成本归因于相关的利益相关者。例如,你可能有团队标签或所有者标签。如果某个特定团队所配置的资源突然快速增长,你就可以更快地缩小问题范围。具体标签由你决定。常见的标签可能包括环境(生产、预发布和开发)、发布版本和 git sha。许多云资源附带云提供商标签,你可以利用这些标签。
政策和预算
策略和预算让你能够控制基础设施上的过度支出。一些策略是隐性的,例如命名空间资源配额,它会阻止超额资源的配置。然而,其他策略可能更具成本针对性,并通过成本追踪来提供信息。预算让你在不同的范围内设置硬性支出上限。所有云服务提供商都将预算作为其成本管理解决方案的一部分:
-
教程:创建和管理 Azure 预算:
learn.microsoft.com/en-us/azure/cost-management-billing/costs/tutorial-acm-create-budgets -
使用 AWS Budgets 管理成本:
docs.aws.amazon.com/cost-management/latest/userguide/budgets-managing-costs.html -
在 GCP 中创建、编辑或删除预算和预算警报:
cloud.google.com/billing/docs/how-to/budgets
策略和预算很重要,但有时它们不足以应对所有情况,或者你没有时间去指定和更新它们。这时,警报功能就发挥作用了。
警报
预算通常是应对不当基础设施配置或意外超支配置的最后一道防线。例如,你可能会为几个总体类别设置宽泛的预算,如计算支出不超过 500,000 美元。这些预算当然需要与业务增长保持一致,并确保在你临时需要配置更多基础设施以应对需求短期激增时,不会引发任何事故。预算通常只在高层管理审批后才会设定或修改。
精细化的、日常的成本管理警报更加灵活和实用。如果你超过或接近某个成本限制,可以设置警报来提醒你,并在必要时进行升级。警报依赖于适当的标签,这样你就可以获得有意义的信息来评估成本增加的原因和负责方。
如你所见,成本管理是一个动态且复杂的活动。你需要好的工具来帮助你。
工具
所有云服务提供商都拥有强大的成本管理工具。了解更多:
-
AWS 成本探查器:
aws.amazon.com/aws-cost-management/aws-cost-explorer/ -
Azure 成本分析器:
www.serverless360.com/azure-cost-analysis -
GCP 成本管理:
cloud.google.com/cost-management/
此外,你可以选择使用像 kubecost (www.kubecost.com) 这样的多云开源工具,或者像 cast.ai (cast.ai) 这样的付费产品。当然,你也可以自己做这一切,从云服务提供商获取成本指标到 Prometheus,然后在其上构建 Grafana 仪表盘和警报。
记住,选择合适的工具组合可以迅速为你带来收益。
智能选择资源
云服务提供了丰富的资源选择和组合,如虚拟机、网络和磁盘等。我们在本章前面的 Bin packing 和利用率 部分已经覆盖了所有相关的考虑因素。然而,在关注成本的同时,你应该确保自己理解,可能会有更便宜的替代方案能够完成工作,并大幅降低成本。熟悉资源清单,并保持更新,因为云服务商会不断更新其产品并可能调整价格。
资源的高效使用
在削减成本时,任何未使用的资源都应该引起警惕。你正在浪费金钱。通常有必要增加灵活性。例如,行业平均的 CPU 利用率在 40%-60%之间。这看起来可能比较低,但在一个非常动态的环境中,要在高可用性、快速恢复和快速扩展等限制下提升这一点并不容易。
折扣、预留实例和竞价实例
降低成本的最佳方法之一就是为相同的资源支付更少的钱。常见的实现方式包括折扣、预留实例和竞价实例。
折扣和积分
折扣是最好的选择。它们只有好处。你只需支付低于标价的费用。就是这么简单。嗯,事情并非那么简单。你需要通过谈判获得最优惠的价格,并且通常需要展示出增长潜力和承诺,表示你会长期使用该平台。
积分是另一种抵消初期云开支的好方法。所有主要的云服务商都提供各种积分计划,你也许还可以协商获得更多的积分。
AWS 有 Activate 计划,主要面向初创公司,在该计划下,你可以获得最多 $100,000 的 AWS 积分。详情请见 aws.amazon.com/activate/。
Azure 有 Microsoft for Startups 计划,提供最多$150,000 的 Azure 积分。详情请见 www.microsoft.com/en-us/startups。
GCP 有 Google for Startups 云计划,提供(与 AWS 类似)最多 $100,000 的 GCP 积分。详情请见 cloud.google.com/startup。
这些程序旨在帮助年轻的初创公司,在收入不多的情况下推动其发展。接下来我们来看看一些适用于已经成立的企业组织的选项,这些组织仍然希望减少云开支。
预留实例
预留实例是降低成本的一个非常好的方法。它们要求你批量购买容量,并承诺长期使用(一年或三年)。承诺更长时间将带来更好的折扣。总体而言,折扣非常可观,与按需价格相比,折扣可以从 30%到 75%不等。
除了价格外,预留实例相比按需实例还能确保容量,后者在特定的可用区内可能会暂时无法提供某些实例类型。
预留实例的缺点是你通常需要预付费,而且承诺通常与特定的实例类型和区域挂钩。你可能可以交换等效的预留实例,但你需要查看云提供商的条款和条件。此外,如果你未能使用完所有的预留容量,你仍然需要为其支付费用(尽管价格大幅折扣)。
如果你考虑使用预留实例(RIs),你可以选择一个有限容量的预留实例,这样你就可以保证始终能够使用它们,然后使用按需实例和抢占式实例来处理流量高峰并利用云的弹性。如果后来发现你在按需实例上的支出过多,你总是可以预留更多实例,并制定一个三年期、1 年期预留实例、按需实例和抢占式实例的组合。这也是引入下一个话题的好时机——抢占式实例。
抢占式实例
云服务提供商喜欢预留实例。他们销售这些实例、提供这些实例、从中获利,并可以忘记它们(除了确保它们正常运行)。按需服务则完全不同。云服务提供商必须确保,当客户需要时,他们能够合理地提供更多的容量。特别是,云服务提供商应该大致有足够的容量来处理每个客户的配额,即使客户的配额使用量非常低。这意味着在实际操作中,通常情况下,云服务提供商有大量的闲置或未充分利用的容量。这就是抢占式实例的作用。云服务提供商可以销售这些多余的容量,这些容量理论上是为按需使用分配的。如果需要,云服务提供商就可以收回抢占式实例,并将其提供给按需客户。
为什么要使用抢占式实例?因为它们便宜得多。由于它们可能是临时性的,因此提供了高达 90% 的显著折扣。记住,从云服务提供商的角度来看,这其实是免费的收入。这些实例已经通过按需实例的加价和每个客户的配额比例计算和支付过了。
在实践中,抢占式实例不会很快从你下面消失。Kubernetes 的方式提倡工作负载不应过于关心特定节点。如果你在抢占式实例上运行并且它消失了,所有你的 Pod 都会被驱逐并调度到其他节点。如果你有敏感的工作负载,无法很好地处理从节点上的驱逐,那么这些工作负载本身就不是好的 Kubernetes 公民。节点时常变得不健康,无论如何,你的工作负载应该能够处理驱逐。
然而,有一种情况需要特别关注,尤其是在你使用抢占实例运行关键工作负载时。如果某个实例类型所在的可用区发生故障,可能会有许多抢占实例被云服务提供商回收。我建议你配置空的后备按需节点池,使用相同或类似的实例类型,并且设置相同的标签和污点。如果使用抢占实例的节点池突然失去大量节点,且无法扩展(因为抢占实例不可用),你就可以扩展空的按需节点池,直到抢占实例重新可用,这时你的 Pod 会被调度到空的节点池中。
确保你为后备节点池预留足够的配额,以便在相应的抢占实例不可用时能够顶上。
接下来,让我们讨论本地环境以及它们如何帮助我们节省成本。
投资本地环境
各个组织采用不同的开发和测试协议。有些组织在云环境中进行大量的测试,尤其是在预发布和开发环境中。有时,工程师会为各种实验和测试配置基础设施。这类开发和测试环境通常很难有效管理,因为基础设施往往是临时配置的。像一次性 Kubernetes 集群和虚拟集群这样的解决方案已经出现。另一种方向是投资本地开发环境,工程师可以在本地机器上运行这些环境。它们的优点是,这些环境通常非常快速地搭建和销毁,而且不会产生高昂的云费用。缺点是,这些环境可能无法完全代表预发布或生产环境。我建议考虑本地环境,并寻找可以节省云费用的使用场景,同时不影响系统的其他关键方面。
总结
在这一章中,我们深入探讨了运行大规模托管 Kubernetes 系统所需的各项要求。我们讨论了如何管理多个集群、建立有效的流程、处理大规模的基础设施、管理集群和节点池、二进制打包和资源利用、升级 Kubernetes、故障排除以及成本管理。这些内容涵盖了很多,但即便如此,也只是冰山一角。对于你的用例和特殊问题,深入的了解是不可替代的。最终结论是,大规模的企业系统管理起来非常复杂,但 Kubernetes 为你提供了大量工业级的工具来完成这项工作。
下一章将总结本书的内容。我们将展望 Kubernetes 的未来及其前景。剧透警告:未来非常光明。Kubernetes 已经确立了自己在云原生计算领域的黄金标准地位。它正在各个领域广泛使用,并且不断负责任地发展。围绕 Kubernetes,已经建立了完整的支持体系,包括培训、开源项目、工具和产品。社区非常强大,发展势头十足。
加入我们的 Discord!
与其他用户、云计算专家、作者及志同道合的专业人士一起阅读这本书。
提问、为其他读者提供解决方案、通过问我任何问题的环节与作者互动,还有更多。
扫描二维码或访问链接立即加入社区。

第十八章:Kubernetes 的未来
在本章中,我们将从多个角度审视 Kubernetes 的未来。我们将从 Kubernetes 自诞生以来在社区、生态系统和思维领导力等多个维度的势头开始。剧透一下——Kubernetes 在容器编排战争中以压倒性优势获胜。随着 Kubernetes 的发展和成熟,竞争对手的较量逐渐转变为与自身复杂性作斗争。可用性、工具链和教育将发挥重要作用,因为容器编排仍然是一个全新的、快速发展的领域,而且尚未被充分理解。接下来,我们将看看一些非常有趣的模式和趋势,最后,我们将回顾我在第二版中的预测,并做出一些新的预测。
涵盖的主题如下:
-
Kubernetes 的势头
-
CNCF 的重要性
-
Kubernetes 的可扩展性
-
服务网格集成
-
在 Kubernetes 上实现无服务器计算
-
Kubernetes 与虚拟机(VM)
-
集群自动扩展
-
无处不在的操作员
-
Kubernetes 与人工智能
-
Kubernetes 的挑战
Kubernetes 的势头
Kubernetes 无可否认地是一个巨头。Kubernetes 不仅打败了所有其他容器编排工具,而且它还是公共云上的事实标准,许多私有云也在使用它,甚至虚拟机公司 VMware 也专注于 Kubernetes 解决方案并将其产品与 Kubernetes 集成。
由于其可扩展的设计,Kubernetes 在多云和混合云场景中表现非常出色。
此外,Kubernetes 还在边缘计算方面取得了进展,通过定制发行版进一步扩大了其广泛适用性。
Kubernetes 项目每三个月就会发布一个新版本,几乎像时钟一样精准。社区也在不断壮大。
Kubernetes 的 GitHub 仓库几乎有 100,000 颗星。推动这一惊人增长的主要因素之一是CNCF(云原生计算基金会)。

图 18.1:明星历史图表
CNCF 的重要性
CNCF 已成为云计算领域一个非常重要的组织。尽管它并非专门针对 Kubernetes,Kubernetes 的主导地位却是不可否认的。Kubernetes 是第一个毕业的项目,其他大多数项目也都在很大程度上依赖 Kubernetes。特别是,CNCF 仅为 Kubernetes 提供认证和培训。CNCF 除了其他角色之外,还确保云技术不会遭受厂商锁定。查看这张展示整个 CNCF 生态的疯狂图表:landscape.cncf.io。
项目策划
CNCF 为项目分配成熟度级别:已毕业、孵化中和沙箱:

图 18.2:CNCF 成熟度级别
项目从某个级别开始——沙箱或孵化阶段——并随着时间的推移,逐步毕业。这并不意味着只有毕业的项目才能安全使用。许多孵化阶段甚至沙箱阶段的项目在生产环境中被大量使用。例如,etcd 是 Kubernetes 本身的持久状态存储,它只是一个孵化项目,但显然是一个高度可信赖的组件。虚拟 Kubelet 是一个沙箱项目,它支持 AWS Fargate 和 Microsoft ACI。显然,这是一个企业级软件。
CNCF 对项目的策划和管理的主要好处是帮助人们导航 Kubernetes 周围庞大的生态系统。当你想通过额外的技术和工具扩展你的 Kubernetes 解决方案时,CNCF 项目是一个很好的起点。
认证
当技术开始提供认证项目时,可以看出它们会长期存在。CNCF 提供几种类型的认证:
-
认证 Kubernetes,适用于符合标准的 Kubernetes 发行版和安装程序(大约 90 个)。
-
Kubernetes 认证服务提供商(KCSP),适用于具有深厚 Kubernetes 经验的经过验证的服务提供商(134 个提供商)。
-
Kubernetes 认证管理员(CKA),适用于管理员。
-
Kubernetes 认证应用开发者(CKAD),适用于开发者。
-
Kubernetes 认证安全专家(CKS),适用于安全专家。
培训
CNCF 也提供培训。它提供免费的 Kubernetes 入门课程,以及与 CKA 和 CKAD 认证考试对接的多个付费课程。此外,CNCF 维护了一份 Kubernetes 培训伙伴名单(landscape.cncf.io/card-mode?category=kubernetes-training-partner&grouping=category)。
如果你在寻找免费的 Kubernetes 培训,以下是一些选择:
-
VMware Kubernetes 学院
-
Coursera 上的 Google Kubernetes Engine
社区与教育
CNCF 还组织诸如 KubeCon、CloudNativeCon 等会议和聚会,并维护多个通信渠道,如 Slack 频道和邮件列表。它还发布调查和报告。
参会者和参与者的数量每年都在不断增长。
工具
管理容器和集群的工具、各种附加组件、扩展和插件不断增加。以下是 Kubernetes 生态系统中一些参与的工具、项目和公司:

图 18.3:Kubernetes 工具
托管 Kubernetes 平台的崛起
几乎每个云服务提供商现在都有强大的托管 Kubernetes 服务。有时,在某些云服务提供商上,运行 Kubernetes 的方式和版本也会有所不同。
公共云 Kubernetes 平台
以下是一些著名的托管平台:
-
Google GKE
-
Microsoft AKS
-
Amazon EKS
-
Digital Ocean
-
Oracle Cloud
-
IBM Cloud Kubernetes 服务
-
Alibaba ACK
-
腾讯 TKE
当然,您总是可以自己构建并将公共云服务提供商作为基础设施提供商使用。这是 Kubernetes 中非常常见的用例。
裸金属、私有云和边缘计算上的 Kubernetes
在这里,您可以找到为在特定环境中运行而设计或配置的 Kubernetes 发行版,通常是在您的数据中心作为私有云,或者在边缘计算等更受限的环境中运行,例如在小型设备上:
-
Google Anthos for GKE
-
OpenStack
-
Rancher k3S
-
Raspberry PI 上的 Kubernetes
-
KubeEdge
Kubernetes PaaS(平台即服务)
这一类产品旨在抽象出 Kubernetes 的一些复杂性,并为其提供一个更简单的外壳。这里有许多种类。其中一些迎合了多云和混合云的场景,一些提供了功能即服务的接口,另一些则专注于更好的安装和支持体验:
-
Google Cloud Run
-
VMware PKS
-
Platform 9 PMK
-
Giant Swarm
-
OpenShift
-
Rancher RKE
即将到来的趋势
让我们谈谈一些 Kubernetes 世界中即将到来的技术趋势,这些趋势将在不久的将来变得重要。
安全性
安全性当然是大规模系统中的首要问题。Kubernetes 主要是一个用于管理容器化工作负载的平台。这些容器化的工作负载通常运行在一个多租户环境中。租户之间的隔离非常重要。容器轻量高效,因为它们共享一个操作系统,并通过各种机制(如命名空间隔离、文件系统隔离和 cgroup 资源隔离)来保持它们的隔离。理论上,这应该足够了。但在实践中,表面面积很大,容器隔离出现了多个突破。
为了应对这一风险,设计了多个轻量级虚拟机,加入了一个超监视器(机器级虚拟化),作为容器和操作系统内核之间的额外隔离层。大型云服务提供商已经支持这些技术,而 Kubernetes CRI 接口提供了一种简化的方式来利用这些更安全的运行时。
例如,FireCracker 通过 firecracker-containerd 与 containerd 集成。Google gVisor 是另一种沙盒技术,它是一个用户空间内核,实施大多数 Linux 系统调用,并在应用程序和宿主操作系统之间提供缓冲区。它也可以通过 gvisor-containerd-shim 通过 containerd 使用。
网络
网络是另一个持续创新的领域。Kubernetes CNI 允许在一个简单的接口后面使用各种创新的网络解决方案。一个主要的主题是将 eBPF(一种相对较新的 Linux 内核技术)整合到 Kubernetes 中。
eBPF代表扩展伯克利数据包过滤器。eBPF 的核心是 Linux 内核中的一个迷你虚拟机,它在发生特定事件(如数据包传输或接收)时执行附加到内核对象的特殊程序。最初,只支持套接字,这项技术当时称为 BPF。后来,更多的对象被添加到这个技术中,这就是为什么会有e代表扩展。eBPF 的著名之处在于其性能,因为它在内核中运行经过高度优化的编译 BPF 程序,并且无需通过内核模块扩展内核。
eBPF 有很多应用场景:
-
动态网络控制:基于 iptables 的方法在像 Kubernetes 集群这样的动态环境中不太适用,因为 Kubernetes 集群中有不断变化的 Pods 和服务。用 BPF 程序替代 iptables 不仅更高效,而且更易管理。Cilium 专注于使用 eBPF 进行流量路由和过滤。
-
监控连接:通过附加一个跟踪套接字级事件的 BPF 程序 kprobes,可以创建一个容器之间 TCP 连接的最新映射。WeaveScope 通过在每个节点上运行一个代理来收集这些信息,并将其发送到一个服务器,通过流畅的 UI 提供可视化展示。
-
限制系统调用:Linux 内核提供了 300 多个系统调用。在一个对安全要求较高的容器环境中,限制这些系统调用是非常有益的。原始的 seccomp 功能相对基础。在 Linux 3.5 中,seccomp 被扩展以支持 BPF 进行高级自定义过滤。
-
原始性能:eBPF 提供了显著的性能优势,像 Calico 这样的项目利用这一点实现了一个更快的数据平面,且使用了更少的资源。
定制硬件和设备
Kubernetes 在较高层次上管理节点、网络和存储。但将特定硬件集成到更细粒度的层次上有很多好处。例如,GPU、高性能网卡、FPGA、InfiniBand 适配器以及其他计算、网络和存储资源。这时,设备插件框架就派上用场了,可以在这里找到:kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/device-plugins。自 Kubernetes 1.26 以来,它已进入 GA 阶段,并且这一领域的创新仍在持续。例如,自 Kubernetes 1.15 起,设备插件资源的监控也处于 beta 阶段。非常有趣的是,看到哪些设备将被集成到 Kubernetes 中。该框架本身通过使用 gRPC 遵循现代 Kubernetes 的可扩展性实践。
服务网格
服务网格可以说是过去几年中网络领域最重要的趋势。我们在第十四章《利用服务网格》中深入探讨了服务网格的内容。服务网格的采用令人印象深刻,我预测大多数 Kubernetes 发行版将提供默认的服务网格,并允许与其他服务网格进行轻松集成。服务网格带来的好处是无价的。因此,提供一个包含集成服务网格的默认平台是合情合理的。然而,Kubernetes 本身不会吸收某些服务网格并通过其 API 进行暴露。这与保持 Kubernetes 核心简洁的理念背道而驰。
Google Anthos 是一个很好的例子,它将 Kubernetes + Knative + Istio 结合在一起,提供了一个统一的平台,提供了一套有指导性的最佳实践组合,而这些组合如果由组织自行构建,将需要花费大量时间和资源,且必须基于原生 Kubernetes。
另一个推动这一方向的举措是边车容器的 KEP,相关信息可以在这里找到:github.com/kubernetes/enhancements/blob/master/keps/sig-node/753-sidecar-containers/README.md。
边车容器模式从一开始就是 Kubernetes 的一个重要组成部分。毕竟,Pod 可以包含多个容器。但在早期并没有明确区分主容器和边车容器,Pod 中的所有容器地位是平等的。大多数服务网格使用边车容器来拦截流量并执行其任务。规范化边车容器将有助于这些工作,并进一步推动服务网格的发展。
当前尚不清楚 Kubernetes 和服务网格是否会在大多数平台上隐藏在更简化的抽象层后面,或者它们是否会被直接暴露。
无服务器计算
无服务器计算是另一个注定会持续的趋势。我们在第十二章《Kubernetes 上的无服务器计算》中进行了详细讨论。Kubernetes 和无服务器计算可以在多个层次上结合使用。Kubernetes 可以利用像 AWS Fargate 和AKS Azure 容器实例(ACI)这样的无服务器云解决方案,减轻集群管理员管理节点的负担。该方法还可支持与 Kubernetes 的轻量级虚拟机透明集成,因为云平台并没有使用裸 Linux 容器作为其容器即服务平台的基础。
另一种途径是反转角色,将容器作为由 Kubernetes 提供支持的服务进行暴露。这正是 Google Cloud Run 所做的。在这里,界限变得模糊,因为 Google 提供了多个产品来管理容器和/或 Kubernetes,从仅仅是 GKE,到 Anthos GKE(将自己的集群带入 GKE 环境,供私有数据中心使用),Anthos(托管 Kubernetes + 服务网格),以及 Anthos Cloud Run。
最后,有一些功能即服务和按需扩展的项目正在你的 Kubernetes 集群中运行。Knative 可能会成为这方面的领导者,因为它已经被许多框架使用,并且通过各种 Google 产品进行广泛部署。
边缘上的 Kubernetes
Kubernetes 是云原生计算的代表,但随着物联网(IoT)的革命,越来越需要在网络边缘执行计算。将所有数据发送到后台进行处理存在几个缺点:
-
延迟
-
足够带宽的需求
-
成本
随着边缘位置通过传感器、视频摄像头等收集大量数据,边缘数据量不断增长,进行越来越复杂的处理在边缘端变得更加合理。Kubernetes 源自 Google 的 Borg,后者显然并非为了在网络边缘运行而设计。但 Kubernetes 的设计证明其足够灵活,可以适应这一需求。我预计,我们将看到越来越多的 Kubernetes 部署在网络边缘,这将催生出由多个 Kubernetes 集群组成的非常有趣的系统,这些集群需要进行集中管理。
KubeEdge 是一个开源框架,建立在 Kubernetes 和 Mosquito(一个开源的 MQTT 消息代理实现)之上,提供一个用于云端和边缘之间的网络、应用部署以及元数据同步的基础。
原生 CI/CD
对开发者而言,最重要的问题之一是构建 CI/CD 管道。市场上有很多选择,做出选择可能非常困难。CD 基金会是一个开源基金会,旨在标准化管道和工作流等概念,并定义行业规范,使不同的工具和社区能够更好地互操作。
当前的项目有:
-
Jenkins
-
Tekton
-
Spinnaker
-
Jenkins X
-
Screwdriver.cd
-
Ortelius
-
CDEvents
-
Pyrsia
-
Shipwright
请注意,只有 Jenkins 和 Tekton 被视为毕业项目,其它项目(甚至 Spinnaker)仍处于孵化阶段。
我最喜欢的原生 CD 项目之一,Argo CD,并未成为 CD 基金会的一部分。事实上,我曾在 GitHub 上开设了一个 issue,要求将 Argo CD 提交给 CDF,但 Argo 团队决定 CNCF 更适合他们的项目。
另一个值得关注的项目是 CNB——云原生构建包。该项目接受源代码并创建 OCI(类似 Docker)镜像。对于 FaaS 框架和集群内原生 CI 至关重要。它也是一个 CNCF 沙箱项目。
操作员
Operator 模式源于 2016 年的 CoreOS(被 RedHat 收购,后又被 IBM 收购),并在社区中获得了广泛的成功。Operator 是自定义资源和控制器的结合,用于管理应用程序。在我目前的工作中,我编写 Operator 来管理基础设施的各个方面,这是一项令人愉快的工作。它已经成为向 Kubernetes 集群分发复杂应用程序的成熟方式。请查看 operatorhub.io/ 获取大量现有的 Operator 列表。我预计这一趋势将继续并加剧。
Kubernetes 和 AI
AI 是目前最热门的趋势。大型语言模型(LLMs)和生成式预训练变换器(GPT)凭借其强大的能力让大多数专业人士感到惊讶。OpenAI 发布的 ChatGPT 3.5 版是一个分水岭时刻。AI 突然在曾被视为人类智慧堡垒的领域,如创意写作、绘画、理解、回答复杂问题以及当然还有编程方面表现出色。我的观点是,先进的 AI 是解决大数据问题的答案。我们学会了收集大量数据,但从中分析和提取洞察是一个困难且劳动密集的过程。AI 似乎是消化所有数据并自动理解、总结、整理成对人类和其他系统(很可能是 AI 系统)有用形式的正确技术。
让我们来看一下,为什么 Kubernetes 非常适合 AI 工作负载。
Kubernetes 和 AI 协同效应
现代 AI 的核心是深度学习网络和具有数十亿参数的大型模型,这些模型在庞大的数据集上进行训练,通常需要专用硬件。Kubernetes 非常适合处理这种工作负载,因为它能够快速适应工作负载的需求,充分利用新的硬件,并提供强大的可观察性。
最有力的证据来自实践。Kubernetes 是 OpenAI 流水线的核心,越来越多的公司正在开发和部署庞大的 AI 应用程序。请阅读这篇文章,了解 OpenAI 如何使用 Kubernetes 推动技术进步,并运行一个包含 7,500 个节点的大型集群:openai.com/research/scaling-kubernetes-to-7500-nodes。
让我们考虑一下在 Kubernetes 上训练 AI 模型。
在 Kubernetes 上训练 AI 模型
训练大型 AI 模型可能既慢又昂贵。参与在 Kubernetes 上训练 AI 模型的组织从其许多特性中受益:
-
可扩展性:Kubernetes 提供了一个高度可扩展的基础设施,用于部署和管理 AI 工作负载。借助 Kubernetes,可以根据需求快速扩展或缩减资源,使组织能够快速高效地训练 AI 模型。
-
资源利用:Kubernetes 允许高效的资源利用,使得组织能够使用最具成本效益的基础设施训练 AI 模型。通过 Kubernetes,可以自动分配和管理资源,确保工作负载所需的资源得到有效配置。
-
灵活性:Kubernetes 提供了高度的灵活性,可以使用不同的基础设施来训练 AI 模型。Kubernetes 支持包括 GPU、FPGA 和 TPU 在内的多种硬件,使得可以根据工作负载选择最合适的硬件。
-
可移植性:Kubernetes 提供了高度可移植的基础设施,用于部署和管理 AI 工作负载。Kubernetes 支持多种云服务提供商和本地基础设施,使得可以在任何环境中训练 AI 模型。
-
生态系统:Kubernetes 拥有一个充满活力的开源工具和框架生态系统,可以用于训练 AI 模型。例如,Kubeflow 是一个流行的开源框架,用于在 Kubernetes 上构建和部署机器学习工作流。
在 Kubernetes 上运行基于 AI 的系统
一旦你训练好了模型并在其上构建了应用程序,就需要部署并运行它。Kubernetes 当然是一个优秀的工作负载部署平台。基于 AI 的工作负载通常被设计为快速响应超人类水平的需求。Kubernetes 提供的高可用性以及根据需求快速扩展和缩减的能力,能够满足这些要求。
此外,如果系统被设计为持续学习(而不是像 GPT 这样固定的预训练系统),那么 Kubernetes 提供了强大的安全性和控制,支持安全运行。
让我们来看看新兴的 AIOps 领域。
Kubernetes 和 AIOps
AIOps 是一种利用 AI 和机器学习自动化和优化基础设施管理的范式。AIOps 可以帮助组织提高 IT 基础设施的可靠性、性能和安全性,同时减轻人类工程师的负担。
Kubernetes 是实践 AIOps 的理想平台。它可以完全通过编程访问,且通常会与深度可观察性一起部署。这两个条件是使 AI 能够审视系统状态并在必要时采取行动的必要且充分条件。
Kubernetes 的未来看起来很光明,但它也面临一些挑战。
Kubernetes 的挑战
Kubernetes 是解决与基础设施相关的所有问题的答案吗?当然不是。让我们来看一下 Kubernetes 的一些挑战,比如它的复杂性,以及一些针对开发、部署和管理大规模系统的替代解决方案。
Kubernetes 的复杂性
Kubernetes 是一个庞大、强大且可扩展的平台。它大多是非意见化的,且非常灵活。它有一个巨大的表面面积,包含许多资源和 API。此外,Kubernetes 拥有庞大的生态系统。这意味着它是一个极其难以学习和掌握的系统。这对 Kubernetes 的未来有什么影响?一个可能的情景是,大多数开发者将不会直接与 Kubernetes 互动。建立在 Kubernetes 上的简化解决方案将成为大多数开发者的主要接入点。
如果 Kubernetes 完全抽象化,那么它可能会对未来构成威胁,因为作为底层实现的 Kubernetes 可能会被解决方案提供商替代。最终用户可能根本不需要对他们的代码或配置做任何改变。
另一个场景是,越来越多的组织开始对在 Kubernetes 上构建的成本持负面看法,相比之下,轻量级的容器编排平台如 Nomad 可能更具吸引力。这可能会导致从 Kubernetes 的流失。
让我们来看一些可能在不同领域与 Kubernetes 竞争的技术。
无服务器函数平台
无服务器函数平台为组织和开发者提供了类似 Kubernetes 的好处,但采用了一个更简单(尽管不那么强大)的范式。你不需要将系统建模为一组长期运行的应用程序和服务,而是只需要实现一组可以按需触发的函数。你不需要管理集群、节点池和服务器。一些解决方案也提供长期运行的服务,可能是预打包成容器或直接从源代码运行。我们在第十二章中详细讲解了这个内容,在 Kubernetes 上的无服务器计算。随着无服务器平台的不断改进,以及 Kubernetes 变得越来越复杂,更多组织可能会倾向于至少开始使用无服务器解决方案,之后可能会迁移到 Kubernetes。
首先,所有云服务提供商都提供各种无服务器解决方案。纯粹的云函数模型有:
-
AWS Lambda
-
Google Cloud Functions
-
Azure Functions
还有许多强大且易于使用的解决方案并未与大型云服务提供商相关联:
-
Cloudflare Workers
-
Fly.io
-
渲染
-
Vercel
这就是我们对 Kubernetes 挑战的总结。让我们总结一下本章的内容。
总结
在本章中,我们探讨了 Kubernetes 的未来,看起来非常光明!其技术基础、社区支持、广泛的支持以及势头都非常令人印象深刻。Kubernetes 仍然年轻,但创新和稳定的步伐非常鼓舞人心。Kubernetes 的模块化和可扩展性原则使其成为现代云原生应用程序的通用基础。尽管如此,Kubernetes 也面临一些挑战,并且可能并不会主导每个场景。这是一件好事。多样性、竞争以及其他解决方案的启发会让 Kubernetes 变得更好。
到目前为止,你应该清楚 Kubernetes 目前的状态以及它未来的发展方向。你应该有信心,Kubernetes 不仅会长期存在,而且将成为未来多年主导的容器编排平台,能够与各种大型平台和环境集成——从全球规模的公有云平台、私有云、数据中心、边缘计算位置,一直到你的开发笔记本电脑和树莓派。
就这样!这本书已经结束了。
现在,轮到你运用所学的知识,使用 Kubernetes 构建令人惊叹的项目了!
加入我们的 Discord 社区!
与其他用户、云计算专家、作者以及志同道合的专业人士一起阅读本书。
提出问题,为其他读者提供解决方案,参加作者的“问我任何问题”环节,等等。
扫描二维码或访问链接,即可立即加入社区。


订阅我们的在线数字图书馆,全面访问超过 7,000 本书籍和视频,以及行业领先的工具,帮助你规划个人发展并推进职业生涯。欲了解更多信息,请访问我们的网站。
第十九章:为什么要订阅?
-
通过来自 4,000 多位行业专业人士的实用电子书和视频,减少学习时间,更多时间进行编程
-
使用专为你设计的技能计划提升学习效果
-
每月免费获取一本电子书或视频
-
完全可搜索,轻松访问重要信息
-
复制、粘贴、打印并收藏内容
在 www.packt.com 上,你还可以阅读一系列免费的技术文章,注册多种免费的新闻简报,并获得 Packt 图书和电子书的独家折扣和优惠。
你可能喜欢的其他书籍
如果你喜欢这本书,你可能对 Packt 出版的其他书籍感兴趣:
多云策略:云架构师第二版
Jeroen Mulder
ISBN: 9781804616734
-
借助使用案例选择合适的云平台
-
精通多云概念,包括 IaC、SaaS、PaaS 和 CaC
-
使用 Azure、AWS 和 GCP 提供的技术和工具来集成安全性
-
利用 Azure、AWS 和 GCP 框架为企业架构最大化云潜力
-
使用 FinOps 定义成本模型并通过 showback 和 chargeback 优化云成本
Kubernetes 企业指南(第二版)
Marc Boorshtein
Scott Surovich
ISBN: 9781803230030
-
使用 KinD 创建多节点 Kubernetes 集群
-
实现 Ingress、MetalLB、ExternalDNS 和新的沙盒项目 K8GB,配置集群 OIDC 和模拟
-
在 Istio 服务网格中部署单体应用
-
将企业授权映射到 Kubernetes
-
使用 OPA 和 GateKeeper 保护集群
-
使用 Falco 和 ECK 增强审计
-
为灾难恢复和集群迁移备份工作负载
-
使用 Tekton、GitLab 和 ArgoCD 部署到 GitOps 平台
Packt 正在寻找像你这样的作者
如果你有兴趣成为 Packt 的作者,请访问 authors.packtpub.com 并今天就申请。我们已经与成千上万的开发者和技术专家合作,帮助他们与全球技术社区分享见解。你可以进行一般申请,申请我们正在招募作者的特定热门话题,或者提交自己的创意。
分享你的想法
现在您已经完成了《Kubernetes 掌握指南(第四版)》,我们很想听听您的想法!如果您是通过亚马逊购买的这本书,请点击这里直接进入亚马逊的书籍评论页面,分享您的反馈或在您购买书籍的站点上留下评论。
您的评价对我们和技术社区非常重要,将帮助我们确保提供优质的内容。


浙公网安备 33010602011771号