Kubernetes-实战-全-
Kubernetes 实战(全)
原文:Kubernetes in Action
译者:飞龙
第一部分. 概述
第一章. 介绍 Kubernetes
本章涵盖
-
理解近年来软件开发和部署的变化
-
使用容器隔离应用程序并减少环境差异
-
理解 Kubernetes 如何使用容器和 Docker
-
使用 Kubernetes 使开发者和系统管理员的工作变得更简单
几年前,大多数软件应用程序都是大型单体,要么作为一个单独的进程运行,要么作为少量分散在几台服务器上的进程运行。这些遗留系统今天仍然很普遍。它们的发布周期缓慢,更新相对较少。在每个发布周期的末尾,开发者将整个系统打包,并将其交给运维团队,然后运维团队部署并监控它。在硬件故障的情况下,运维团队手动将其迁移到剩余的健康服务器。
今天,这些大型单体遗留应用程序正在逐渐被分解成更小、独立运行的组件,称为微服务。由于微服务彼此解耦,它们可以单独开发、部署、更新和扩展。这使得您能够快速且频繁地更改组件,以跟上今天快速变化的业务需求。
但是,随着可部署组件数量的增加和数据中心规模的不断扩大,配置、管理和保持整个系统平稳运行变得越来越困难。确定每个组件放置的位置以实现高资源利用率并因此降低硬件成本变得更加困难。手动完成所有这些工作是一项艰巨的任务。我们需要自动化,这包括自动将这些组件调度到我们的服务器上、自动配置、监督和故障处理。这正是 Kubernetes 发挥作用的地方。
Kubernetes 允许开发者自行部署他们的应用程序,并且可以随时进行部署,无需从运维(ops)团队获得任何帮助。但 Kubernetes 的好处不仅限于开发者。它还通过在硬件故障的情况下自动监控和重新调度这些应用程序来帮助运维团队。系统管理员(sysadmins)的焦点从监督单个应用程序转移到主要监督和管理 Kubernetes 以及其他基础设施,而 Kubernetes 本身则负责应用程序。
注意
Kubernetes 在希腊语中意为飞行员或舵手(握有船舵的人)。人们对 Kubernetes 的发音有几种不同的方式。许多人将其发音为 Koo-ber-nay-tace,而其他人则更接近于 Koo-ber-netties。无论您使用哪种形式,人们都会理解您的意思。
Kubernetes 抽象化了硬件基础设施,并将您的整个数据中心暴露为一个单一的巨大计算资源。它允许您部署和运行软件组件,而无需了解实际的服务器。当通过 Kubernetes 部署多组件应用程序时,它会为每个组件选择一个服务器,部署它,并使其能够轻松地找到并与其他应用程序的所有其他组件进行通信。
这使得 Kubernetes 非常适合大多数本地数据中心,但它在被用于最大的数据中心时开始发光,例如由云服务提供商建设和运营的那些数据中心。Kubernetes 允许他们为开发者提供一个简单的平台,用于部署和运行任何类型的应用程序,同时不需要云服务提供商的自己的系统管理员了解在其硬件上运行的数万个应用程序的任何信息。
随着越来越多的大型公司接受 Kubernetes 模型作为运行应用程序的最佳方式,它正在成为在云中以及本地本地基础设施上运行分布式应用程序的标准方式。
1.1. 理解需要像 Kubernetes 这样的系统
在您开始详细了解 Kubernetes 之前,让我们快速看一下近年来应用程序的开发和部署是如何发生变化的。这种变化既是将大型单体应用程序拆分为更小的微服务的结果,也是运行这些应用程序的基础设施变化的结果。理解这些变化将帮助您更好地看到使用 Kubernetes 和 Docker 等容器技术的好处。
1.1.1. 从单体应用程序迁移到微服务
单体应用程序由所有紧密耦合在一起的组件组成,必须作为一个整体进行开发、部署和管理,因为它们都作为一个单独的操作系统进程运行。应用程序某一部分的更改需要整个应用程序的重部署,随着时间的推移,部分之间缺乏明确的边界导致复杂性增加,由于这些部分之间相互依赖的无约束增长,整个系统的质量因此恶化。
运行单体应用程序通常需要少量强大的服务器,这些服务器能够提供足够的资源来运行应用程序。为了应对系统负载的增加,你随后可以选择通过增加 CPU、内存和其他服务器组件来垂直扩展服务器(也称为向上扩展),或者通过设置额外的服务器并运行应用程序的多个副本(或副本)来水平扩展整个系统(向外扩展)。虽然向上扩展通常不需要对应用程序进行任何更改,但它相对较快地变得昂贵,并且在实践中总是有一个上限。另一方面,向外扩展在硬件方面相对便宜,但可能需要在应用程序代码中进行重大更改,并且并不总是可行——应用程序的某些部分在水平扩展方面极其困难或几乎不可能(例如关系数据库)。如果单体应用程序的任何部分不可扩展,整个应用程序就变得不可扩展,除非你能以某种方式将其拆分。
将应用程序拆分为微服务
这些以及其他问题迫使我们必须开始将复杂的单体应用程序拆分成更小的、可以独立部署的组件,这些组件被称为微服务。每个微服务作为一个独立的过程运行(参见图 1.1),并通过简单、定义良好的接口(API)与其他微服务进行通信。
图 1.1. 单体应用程序内部的组件与独立微服务

微服务通过同步协议(如 HTTP)进行通信,通常通过 RESTful(表示状态传输)API 公开,或者通过异步协议(如 AMQP,高级消息队列协议)进行通信。这些协议简单、大多数开发者都理解,并且与任何特定的编程语言无关。每个微服务都可以使用最适合实现该特定微服务的语言编写。
由于每个微服务都是一个具有相对静态外部 API 的独立进程,因此可以单独开发和部署每个微服务。只要 API 没有更改或仅以向后兼容的方式进行更改,对其中任何一个的更改就不需要更改或重新部署任何其他服务。
微服务的扩展
与需要整体扩展系统的单体系统不同,微服务的扩展是基于每个服务的,这意味着你可以选择只扩展那些需要更多资源的特定服务,而将其他服务保持在原始规模。图 1.2 展示了一个示例。某些组件被复制并在不同的服务器上作为多个进程部署,而其他组件则作为一个单一的应用程序进程运行。当一个单体应用程序无法扩展,因为其某个部分不可扩展时,将应用程序拆分为微服务允许你水平扩展允许扩展的部分,而对于不扩展的部分则垂直扩展。
图 1.2. 每个微服务都可以单独扩展。

微服务的部署
像往常一样,微服务也有其缺点。当你的系统只包含少量可部署的组件时,管理这些组件是容易的。决定每个组件部署位置是微不足道的,因为没有那么多选择。当这些组件的数量增加时,与部署相关的决策变得越来越困难,因为不仅部署组合的数量增加了,而且组件之间的相互依赖关系也以更大的比例增加。
微服务作为一个团队一起执行工作,因此它们需要找到彼此并进行通信。在部署它们时,需要有人或某种机制来正确配置所有这些服务,以便它们能够作为一个单一系统协同工作。随着微服务数量的增加,这变得既繁琐又容易出错,尤其是当考虑到当服务器失败时运维/系统管理员团队需要做什么时。
微服务还带来其他问题,例如使调试和跟踪执行调用变得困难,因为它们跨越多个进程和机器。幸运的是,这些问题现在正在通过分布式跟踪系统如 Zipkin 得到解决。
理解环境需求差异
正如我之前提到的,微服务架构中的组件不仅独立部署,而且也是这样开发的。由于它们的独立性和通常有单独的团队开发每个组件的事实,没有任何东西阻碍每个团队使用不同的库,并在需要时替换它们。应用程序组件之间的依赖关系差异,如图 1.3 所示,其中应用程序需要同一库的不同版本,是不可避免的。
图 1.3. 在同一主机上运行的多应用程序可能会有冲突的依赖关系。

部署需要不同版本的共享库,以及/或其他环境特定要求的动态链接应用程序,对于在生产服务器上部署和管理它们的运维团队来说,很快就会变成一场噩梦。你需要部署在同一主机上的组件数量越多,管理它们的所有依赖关系以满足所有要求就越困难。
1.1.2. 为应用程序提供一致的环境
无论你正在开发和部署多少个单独的组件,开发和运维团队始终必须处理的一个最大的问题是他们在其中运行应用程序的环境差异。不仅开发和生产环境之间存在巨大差异,个别生产机器之间也存在差异。另一个不可避免的事实是,单个生产机器的环境会随着时间的推移而变化。
这些差异从硬件到操作系统,再到每台机器上可用的库都有所不同。生产环境由运维团队管理,而开发者通常自己负责他们的开发笔记本电脑。差异在于这两组人对系统管理的了解程度,这可以理解地导致这两个系统之间存在相当大的差异,更不用说系统管理员更注重保持系统与最新的安全补丁保持更新,而许多开发者并不那么关心这一点。
此外,生产系统可以运行来自多个开发者或开发团队的应用程序,这对于开发者的计算机来说并不一定是真的。生产系统必须为它所托管的所有应用程序提供适当的环境,即使它们可能需要不同版本,甚至是冲突的库版本。
为了减少仅在生产环境中出现的问题数量,如果应用程序在开发和生产过程中能够在完全相同的环境中运行,那么它们将拥有完全相同的操作系统、库、系统配置、网络环境以及所有其他内容,这将是非常理想的。此外,你也不希望这个环境随着时间的推移发生太大的变化,如果可能的话。另外,如果可能的话,你希望有将应用程序添加到同一服务器上的能力,而不会影响该服务器上现有的任何应用程序。
1.1.3. 转向持续交付:DevOps 和 NoOps
在过去几年里,我们也看到了整个应用程序开发过程以及应用程序在生产中的维护方式的转变。在过去,开发团队的工作是创建应用程序并将其移交给运维团队,然后运维团队负责部署、维护并确保其正常运行。但现在,组织意识到最好让开发应用程序的同一团队也参与部署和维护其整个生命周期。这意味着开发人员、QA 和运维团队现在需要在整个过程中进行协作。这种做法被称为 DevOps。
理解好处
让开发人员更多地参与到应用程序的生产运行中,使他们更好地理解用户的需要和问题,以及运维团队在维护应用程序时面临的问题。现在,应用程序开发人员也更倾向于尽早向用户提供应用程序,并利用他们的反馈来引导应用程序的进一步开发。
为了更频繁地发布应用程序的新版本,你需要简化部署流程。理想情况下,你希望开发人员自己部署应用程序,而无需等待运维人员。但部署应用程序通常需要了解底层基础设施和数据中心的硬件组织结构。开发人员并不总是知道这些细节,而且大多数时候,他们甚至不想了解这些。
让开发人员和系统管理员发挥他们最擅长的
尽管开发人员和系统管理员都致力于实现同一个目标,即成功运行软件应用程序作为服务提供给客户,但他们有不同的个人目标和激励因素。开发人员喜欢创建新功能和改进用户体验。他们通常不希望成为确保底层操作系统保持最新状态、所有安全补丁等的人。他们更愿意将这项工作留给系统管理员。
运维团队负责生产部署以及它们运行的硬件基础设施。他们关心系统安全、利用率和其他对开发人员来说不是高优先级的问题。运维人员不希望处理所有应用程序组件的隐含依赖关系,也不希望考虑底层操作系统或基础设施的更改如何影响应用程序的整体运行,但他们必须这样做。
理想情况下,你希望开发人员自己部署应用程序,而不需要了解任何关于硬件基础设施的信息,也不需要与运维团队打交道。这被称为 NoOps。显然,你仍然需要有人来维护硬件基础设施,但理想情况下,无需处理运行在其上的每个应用程序的特定问题。
正如您将看到的,Kubernetes 使我们能够实现所有这些功能。通过抽象化实际硬件并将其作为部署和运行应用程序的单个平台暴露出来,它允许开发者配置和部署他们的应用程序,而无需任何系统管理员的帮助,并允许系统管理员专注于保持底层基础设施的正常运行,而无需了解其上运行的实际应用程序。
1.2. 介绍容器技术
在第 1.1 节中,我列举了当今开发和运维团队面临的一些问题。虽然您有多种方法来处理这些问题,但本书将专注于它们如何通过 Kubernetes 得到解决。
Kubernetes 使用 Linux 容器技术来提供运行应用程序的隔离,因此在我们深入研究 Kubernetes 本身之前,您需要熟悉容器的基本知识,以了解 Kubernetes 本身做什么,以及它将哪些任务卸载给容器技术,如 Docker 或 rkt(发音为“rock-it”)。
1.2.1. 理解容器是什么
在第 1.1.1 节中,我们看到了在同一台机器上运行的不同的软件组件将需要不同、可能冲突的依赖库版本,或者通常具有其他不同的环境要求。
当一个应用程序仅由少量大型组件组成时,为每个组件分配一个专用的虚拟机(VM)并为他们各自提供操作系统实例来隔离其环境是完全可接受的。但当这些组件开始变小且数量开始增加时,如果您不想浪费硬件资源并保持硬件成本较低,您就不能为每个组件分配自己的虚拟机。但这不仅仅是关于浪费硬件资源。因为每个虚拟机通常都需要单独配置和管理,虚拟机数量的增加也会导致人力资源的浪费,因为它们大大增加了系统管理员的负担。
使用 Linux 容器技术隔离组件
与使用虚拟机来隔离每个微服务(或一般软件流程)的环境相比,开发者正在转向 Linux 容器技术。它们允许您在同一台主机机器上运行多个服务,不仅为每个服务提供不同的环境,而且将它们彼此隔离,类似于虚拟机,但开销要小得多。
在容器中运行的过程是在宿主操作系统中运行的,就像所有其他进程一样(与虚拟机不同,虚拟机中的进程在单独的操作系统中运行)。但容器中的进程仍然与其他进程隔离。对于进程本身来说,它看起来就像它是这台机器和其操作系统中唯一运行的进程。
比较虚拟机与容器
与虚拟机相比,容器要轻量得多,这使得你可以在相同的硬件上运行更多的软件组件,主要是因为每个虚拟机都需要运行自己的系统进程集,这需要额外的计算资源,除了组件自身进程消耗的资源之外。另一方面,容器不过是在宿主操作系统上运行的单个隔离进程,仅消耗应用程序消耗的资源,而不需要任何额外进程的开销。
由于虚拟机的开销,你通常会将多个应用程序组合到每个虚拟机中,因为你没有足够的资源为每个应用程序分配整个虚拟机。当使用容器时,你可以(并且应该)为每个应用程序使用一个容器,如图图 1.4 所示。最终结果是,你可以在同一裸机机器上容纳更多的应用程序。
图 1.4. 使用虚拟机隔离应用程序组与使用容器隔离单个应用程序

当你在主机上运行三个虚拟机时,有三个完全独立的操作系统在相同的裸机硬件上运行和共享。在这些虚拟机下面是宿主操作系统和一个虚拟机管理程序,它将物理硬件资源划分为更小的虚拟资源集,这些资源可以被每个虚拟机内部的操作系统使用。在那些虚拟机内部运行的应用程序会对虚拟机中的客户操作系统内核执行系统调用,然后内核通过虚拟机管理程序在宿主物理 CPU 上执行 x86 指令。
注意
存在两种类型的虚拟机管理程序。类型 1 虚拟机管理程序不使用宿主操作系统,而类型 2 则使用。
另一方面,容器都在宿主操作系统运行的相同内核上执行系统调用。这个单一的内核是唯一在宿主 CPU 上执行 x86 指令的内核。CPU 不需要像虚拟机那样进行任何类型的虚拟化(见图 1.5)。
图 1.5. 虚拟机中的应用程序使用 CPU 的方式与容器中应用程序使用 CPU 的方式的区别

虚拟机的主要优势是它们提供的完全隔离,因为每个虚拟机都运行自己的 Linux 内核,而容器都调用相同的内核,这显然可能构成安全风险。如果你硬件资源有限,当需要隔离少量进程时,虚拟机可能是一个选择。要在一个机器上运行更多数量的隔离进程,容器由于它们的低开销,是一个更好的选择。记住,每个虚拟机都运行自己的系统服务集,而容器则不需要,因为它们都在同一个操作系统上运行。这也意味着运行容器时,不需要像虚拟机那样启动任何东西。在容器中运行的进程会立即启动。
介绍使容器隔离成为可能机制
到目前为止,你可能想知道容器如何在同一操作系统中运行时如何精确地隔离进程。两种机制使得这一点成为可能。第一种是 Linux 命名空间(Linux Namespaces),确保每个进程看到自己的系统视图(文件、进程、网络接口、主机名等)。第二种是 Linux 控制组(cgroups),它限制了进程可以消耗的资源量(CPU、内存、网络带宽等)。
使用 Linux 命名空间隔离进程
默认情况下,每个 Linux 系统最初只有一个命名空间。所有系统资源,如文件系统、进程 ID、用户 ID、网络接口等,都属于单个命名空间。但你可以创建额外的命名空间并在它们之间组织资源。当运行一个进程时,你将在其中一个命名空间内运行它。进程将只能看到同一命名空间内的资源。嗯,存在多种命名空间,所以一个进程不属于一个命名空间,而是属于每种类型的命名空间。
存在以下类型的命名空间:
-
挂载(mnt)
-
进程 ID(pid)
-
网络(net)
-
进程间通信(ipc)
-
UTS
-
用户 ID(user)
每种类型的命名空间用于隔离一组特定的资源。例如,UTS 命名空间决定了运行在该命名空间内的进程可以看到的主机名和域名。通过将两个不同的 UTS 命名空间分配给一对进程,你可以使它们看到不同的本地主机名。换句话说,对于这两个进程来说,它们似乎在不同的机器上运行(至少在主机名方面是这样)。
同样,进程所属的网络命名空间决定了运行在该进程内的应用程序可以看到哪些网络接口。每个网络接口属于恰好一个命名空间,但可以从一个命名空间移动到另一个命名空间。每个容器使用自己的网络命名空间,因此每个容器都看到自己的网络接口集。
这应该能给你一个基本的概念,了解命名空间是如何用来隔离容器中运行的应用程序的。
限制进程可用的资源
容器隔离的另一部分是限制容器可以消耗的系统资源量。这是通过 cgroups 实现的,它是 Linux 内核的一个功能,可以限制进程(或一组进程)的资源使用。进程不能使用超过配置的 CPU、内存、网络带宽等资源量。这样,进程就不能占用为其他进程保留的资源,这类似于每个进程都在单独的机器上运行。
1.2.2. 介绍 Docker 容器平台
尽管容器技术已经存在很长时间了,但随着 Docker 容器平台的兴起,它们变得更加广为人知。Docker 是第一个使容器能够在不同机器之间轻松便携的容器系统。它简化了打包应用程序的过程,不仅包括应用程序,还包括所有库和其他依赖项,甚至包括整个操作系统文件系统,使其成为一个简单、便携的包,可用于将应用程序部署到任何运行 Docker 的其他机器上。
当你运行一个用 Docker 打包的应用程序时,它会看到你与之捆绑的确切文件系统内容。无论它是在你的开发机器上运行还是在生产机器上运行,它都会看到相同的文件,即使生产服务器运行的是完全不同的 Linux 操作系统。应用程序不会看到它运行的服务器上的任何内容,因此如果服务器安装的库与你的开发机器完全不同,这并不重要。
例如,如果你已经将应用程序与整个 Red Hat Enterprise Linux (RHEL)操作系统的文件打包在一起,那么当你在运行 Fedora 的开发计算机上运行它,或者在运行 Debian 或其他 Linux 发行版的服务器上运行它时,应用程序都会相信它是在 RHEL 内部运行的。唯一可能不同的是内核。
这类似于通过在虚拟机中安装操作系统来创建 VM 镜像,然后在其中安装应用程序,并将整个 VM 镜像分发并运行。Docker 实现了相同的效果,但它不是使用 VM 来实现应用程序隔离,而是使用上一节中提到的 Linux 容器技术来提供(几乎)与 VM 相同的隔离级别。它不是使用庞大的单体 VM 镜像,而是使用容器镜像,这些镜像通常更小。
Docker 容器镜像与 VM 镜像之间的一大区别是,容器镜像由层组成,这些层可以在多个镜像之间共享和重用。这意味着如果之前在运行包含相同层的不同容器镜像时已经下载了其他层,则只需要下载该镜像的某些层。
理解 Docker 概念
Docker 是一个用于打包、分发和运行应用程序的平台。正如我们之前所述,它允许你将应用程序及其整个环境打包在一起。这可以是应用程序所需的几个库,甚至是通常在已安装操作系统的文件系统上可用的所有文件。Docker 使得将此包传输到中央存储库成为可能,然后可以从该存储库将其传输到任何运行 Docker 的计算机上,并在那里执行(大多数情况下是这样,但并非总是如此,我们很快会解释)。
Docker 中的三个主要概念构成了这个场景:
-
镜像——基于 Docker 的容器镜像是将您的应用程序及其环境打包进的东西。它包含将可供应用程序和其他元数据使用的文件系统,例如,当运行镜像时应执行的可执行文件路径。
-
注册表——Docker 注册表是一个存储您的 Docker 镜像并便于不同人之间和计算机之间共享这些镜像的仓库。当您构建镜像时,您可以在构建它的计算机上运行它,或者将镜像推送到注册表,然后在另一台计算机上拉取(下载)它并运行。某些注册表是公开的,允许任何人从中拉取镜像,而另一些则是私有的,只有某些人或机器可以访问。
-
容器——基于 Docker 的容器是从基于 Docker 的容器镜像创建的常规 Linux 容器。运行中的容器是在运行 Docker 的主机上运行的进程,但它与主机以及所有其他在其上运行的进程完全隔离。该进程也受到资源限制,这意味着它只能访问和使用分配给它的资源(CPU、RAM 等)。
构建、分发和运行 Docker 镜像
图 1.6 展示了这三个概念以及它们之间的关系。开发者首先构建一个镜像,然后将其推送到注册表。因此,任何可以访问注册表的人都可以使用该镜像。然后,他们可以将镜像拉取到任何运行 Docker 的其他机器上并运行它。Docker 基于该镜像创建一个隔离的容器,并运行作为镜像一部分指定的二进制可执行文件。
图 1.6. Docker 镜像、注册表和容器

比较虚拟机和 Docker 容器
我已经解释了 Linux 容器通常与虚拟机相似,但更轻量。现在让我们看看 Docker 容器具体是如何与虚拟机(以及 Docker 镜像与 VM 镜像)相比的。图 1.7 再次显示了相同的六个应用程序在虚拟机和 Docker 容器中同时运行。
图 1.7. 在三个虚拟机上运行六个应用程序与在 Docker 容器中运行它们的比较

您会注意到,当在虚拟机中运行以及作为两个独立的容器运行时,应用程序 A 和 B 都可以访问相同的二进制文件和库。在虚拟机中,这是显而易见的,因为两个应用程序都看到相同的文件系统(虚拟机的文件系统)。但我们说过每个容器都有自己的隔离文件系统。应用程序 A 和应用程序 B 如何共享相同的文件?
理解镜像层
我已经说过,Docker 镜像由层组成。不同的镜像可以包含完全相同的层,因为每个 Docker 镜像都是建立在另一个镜像之上的,并且两个不同的镜像都可以使用相同的父镜像作为其基础。这加快了图像在网络上的分发,因为作为第一个图像的一部分已经传输的层在传输其他图像时不需要再次传输。
但层不仅使分发更高效,还有助于减少镜像的存储占用。每个层只存储一次。因此,从基于相同基础层的两个镜像创建的两个容器可以读取相同的文件,但如果其中一个覆盖了这些文件,另一个则看不到这些更改。因此,即使它们共享文件,它们之间仍然是隔离的。这是因为容器镜像层是只读的。当容器运行时,在镜像的层之上创建一个新的可写层。当容器中的进程向位于底层之一的文件写入时,在顶层创建整个文件的副本,并且进程向副本写入。
理解容器镜像的可移植性限制
理论上,容器镜像可以在运行 Docker 的任何 Linux 机器上运行,但存在一个小小的限制——与所有容器都在主机上使用主机的 Linux 内核这一事实相关。如果一个容器化应用程序需要特定的内核版本,它可能不会在每台机器上运行。如果机器运行的是不同的 Linux 内核版本或没有相同的内核模块可用,该应用程序就不能在该机器上运行。
与虚拟机相比,容器要轻量得多,但它们对容器内运行的应用程序施加了某些限制。虚拟机没有这样的限制,因为每个虚拟机都运行自己的内核。
不仅关乎内核。还应该明确,为特定硬件架构构建的容器化应用程序只能在具有相同架构的其他机器上运行。你不能将针对 x86 架构构建的应用程序容器化并期望它在基于 ARM 的机器上运行,因为这也运行 Docker。你仍然需要虚拟机来做到这一点。
1.2.3. 介绍 rkt——Docker 的替代品
Docker 是第一个使容器主流化的容器平台。我希望我已经清楚地说明,Docker 本身并不提供进程隔离。容器的实际隔离是在 Linux 内核级别通过使用如 Linux Namespaces 和 cgroups 等内核功能来实现的。Docker 只是使这些功能更容易使用。
在 Docker 成功之后,开放容器倡议(OCI)诞生,旨在围绕容器格式和运行时创建开放行业标准。Docker 是这一倡议的一部分,rkt(发音为“rock-it”)也是,它是另一个 Linux 容器引擎。
与 Docker 类似,rkt 是一个运行容器的平台。它非常重视安全性、可组合性和符合开放标准。它使用 OCI 容器镜像格式,甚至可以运行常规的 Docker 容器镜像。
本书侧重于使用 Docker 作为 Kubernetes 的容器运行时,因为它是 Kubernetes 初始支持的唯一一种。最近,Kubernetes 也开始支持 rkt,以及其他容器运行时。
我在这里提到 rkt 的原因是为了防止你犯这样一个错误:认为 Kubernetes 是一个专门为基于 Docker 的容器设计的容器编排系统。实际上,在本书的整个过程中,你会发现 Kubernetes 的本质并不是编排容器。它要复杂得多。容器恰好是运行在不同集群节点上的最佳方式。考虑到这一点,让我们最终深入探讨本书的核心内容——Kubernetes。
1.3. 介绍 Kubernetes
我们已经展示了随着你的系统中可部署的应用程序组件数量的增加,管理它们变得越来越困难。谷歌可能是第一家意识到它需要一种更好的方式来部署和管理其软件组件及其基础设施以实现全球扩展的公司。它是世界上为数不多的运行数十万台服务器并不得不处理如此大规模部署管理的公司之一。这迫使他们开发解决方案,以使成千上万的软件组件的开发和部署变得可管理且成本效益高。
1.3.1. 了解其起源
经过多年的发展,谷歌开发了一个内部系统,称为 Borg(后来又开发了一个新的系统,称为 Omega),该系统帮助应用程序开发者和系统管理员管理那些成千上万的应用程序和服务。除了简化开发和管理工作外,它还帮助他们实现了基础设施的高效利用,这对于组织规模很大时尤为重要。当你运行数十万台机器时,即使是利用率的微小提升也能带来数百万美元的节省,因此开发这样一个系统的激励措施是显而易见的。
在将 Borg 和 Omega 保密整整十年之后,2014 年谷歌推出了 Kubernetes,这是一个基于 Borg、Omega 和其他内部谷歌系统经验积累的开源系统。
1.3.2. 从山顶俯瞰 Kubernetes
Kubernetes 是一个软件系统,它允许你轻松地在它之上部署和管理容器化应用。它依赖于 Linux 容器的特性,以运行异构应用,而无需了解这些应用的任何内部细节,也无需在每个主机上手动部署这些应用。因为这些应用在容器中运行,所以它们不会影响同一服务器上运行的其他应用,这在运行针对完全不同组织的应用时至关重要。这对云服务提供商来说至关重要,因为他们力求最大限度地利用其硬件,同时仍需保持托管应用之间的完全隔离。
Kubernetes 允许你在数千个计算机节点上运行你的软件应用,就像所有这些节点都是一个单一、巨大的计算机一样。它抽象出底层基础设施,通过这样做,简化了开发和运维团队的开发、部署和管理。
无论你的集群只包含几个节点还是数千个节点,通过 Kubernetes 部署应用总是相同的。集群的大小根本无关紧要。额外的集群节点仅仅代表了可供部署应用使用的额外资源量。
理解 Kubernetes 的核心功能
图 1.8 展示了 Kubernetes 系统最简单的视图。该系统由一个主节点和任意数量的工作节点组成。当开发者向主节点提交应用列表时,Kubernetes 将它们部署到工作节点集群中。组件最终落在哪个节点上(并且不应该)并不重要——无论是对于开发者还是系统管理员来说。
图 1.8. Kubernetes 将整个数据中心暴露为单个部署平台。

开发者可以指定某些应用必须一起运行,Kubernetes 将它们部署在同一个工作节点上。其他应用将被分散在集群中,但它们可以以相同的方式相互通信,无论它们部署在哪里。
帮助开发者专注于核心应用功能
可以将 Kubernetes 视为集群的操作系统。它使应用开发者从将某些基础设施相关服务实现到他们的应用中解脱出来;相反,他们依赖 Kubernetes 提供这些服务。这包括服务发现、扩展、负载均衡、自我修复甚至领导者选举。因此,应用开发者可以专注于实现应用的真正功能,而无需浪费时间考虑如何将它们与基础设施集成。
帮助运维团队实现更好的资源利用率
Kubernetes 将在集群的某个位置运行您的容器化应用,向其组件提供信息以了解如何找到彼此,并保持所有组件的运行。因为您的应用不关心它运行在哪个节点上,所以 Kubernetes 可以在任何时候重新定位应用,并通过混合匹配应用,实现比手动调度更好的资源利用率。
1.3.3.理解 Kubernetes 集群的架构
我们已经看到了 Kubernetes 架构的全貌。现在让我们更详细地看看 Kubernetes 集群是由什么组成的。在硬件层面,Kubernetes 集群由许多节点组成,这些节点可以分为两种类型:
-
主节点,它托管 Kubernetes 控制平面,该平面控制和管理工作整个 Kubernetes 系统
-
运行您部署的实际应用的 Worker 节点
图 1.9 显示了在这些两组节点上运行的组件。我将在下面解释它们。
图 1.9。组成 Kubernetes 集群的组件

控制平面
控制平面是控制集群并使其运行的部分。它由多个组件组成,这些组件可以运行在单个主节点上,也可以跨多个节点分割并复制以确保高可用性。这些组件包括
-
Kubernetes API 服务器,您和其他控制平面组件与之通信
-
调度器,它调度您的应用(为您的应用的每个可部署组件分配一个 Worker 节点)
-
控制器管理器,它执行集群级别的功能,例如复制组件、跟踪 Worker 节点、处理节点故障等
-
etcd,一个可靠的分布式数据存储,用于持久化存储集群配置。
控制平面的组件持有并控制集群的状态,但它们不运行您的应用。这项工作由(Worker)节点来完成。
节点
Worker 节点是运行您的容器化应用的机器。运行、监控和为您的应用提供服务的工作由以下组件完成:
-
Docker、rkt 或另一个容器运行时,它运行您的容器
-
Kubelet,它与 API 服务器通信并管理其节点上的容器
-
Kubernetes 服务代理(kube-proxy),它在应用组件之间进行网络流量负载均衡
我们将在第十一章中详细解释所有这些组件。我不喜欢在解释事物的工作原理之前先解释事物的作用,并教人们如何使用它。这就像学习开车一样。您不想知道引擎盖下是什么。您首先想学习如何从 A 点到 B 点驾驶。只有在你学会了如何做到这一点之后,你才会对汽车是如何做到这一点感兴趣。毕竟,了解引擎盖下的事情可能会在汽车抛锚并让您被困在路边时帮助您再次启动汽车。
1.3.4. 在 Kubernetes 中运行应用程序
要在 Kubernetes 中运行应用程序,您首先需要将其打包成一个或多个容器镜像,将这些镜像推送到镜像仓库,然后将您的应用程序描述发布到 Kubernetes API 服务器。
描述包括有关容器镜像或包含您的应用程序组件的镜像的信息,以及这些组件之间是如何相互关联的,以及哪些组件需要运行在同一个节点上(协同运行)以及哪些不需要。对于每个组件,您还可以指定您想要运行多少个副本(或副本)。此外,描述还包括哪些组件为内部或外部客户端提供服务,并且应该通过单个 IP 地址公开,并使其他组件能够发现。
理解描述如何导致运行中的容器
当 API 服务器处理您的应用程序描述时,调度器根据每个组所需的计算资源以及每个节点在该时刻未分配的资源,将指定的容器组调度到可用的工作节点上。然后,这些节点上的 Kubelet 指示容器运行时(例如 Docker)拉取所需的容器镜像并运行容器。
查看图 1.10,以更好地理解应用程序如何在 Kubernetes 中部署。应用程序描述列出了四个容器,分为三个集合(这些集合被称为 pod;我们将在第三章中解释它们是什么)。前两个 pod 每个只包含一个容器,而最后一个包含两个。这意味着两个容器都需要协同运行,并且不应该相互隔离。在每个 pod 旁边,您还可以看到一个表示每个 pod 需要并行运行的副本数量的数字。在将描述提交给 Kubernetes 后,它将指定每个 pod 的副本数量调度到可用的工作节点上。节点上的 Kubelets 然后将告诉 Docker 从镜像仓库拉取容器镜像并运行容器。
图 1.10。Kubernetes 架构的基本概述以及在其上运行的应用程序

保持容器运行
一旦应用程序开始运行,Kubernetes 将不断确保应用程序的部署状态始终与您提供的描述相匹配。例如,如果您指定您始终想要运行五个 Web 服务器的实例,Kubernetes 将始终保持恰好五个实例运行。如果这些实例中的一个停止正常工作,比如其进程崩溃或停止响应,Kubernetes 将自动重启它。
类似地,如果整个工作节点死亡或变得不可访问,Kubernetes 将为在节点上运行的所有容器选择新的节点,并在新选择的节点上运行它们。
扩展副本数量
当应用程序运行时,您可以决定您想要增加或减少副本数量,Kubernetes 将启动额外的副本或停止多余的副本。您甚至可以将决定最佳副本数量的任务留给 Kubernetes。它可以根据实时指标自动调整数量,例如 CPU 负载、内存消耗、每秒查询数或任何其他应用程序公开的指标。
打击移动目标
我们已经说过 Kubernetes 可能需要将您的容器在集群中移动。这可能会发生在它们运行的节点失败或因为需要为其他容器腾出空间而被从节点驱逐时。如果容器正在为外部客户端或集群中运行的容器提供服务,当容器不断在集群中移动时,它们如何正确使用容器?当这些容器被复制并分散在整个集群中时,客户端如何连接到提供服务的容器?
为了让客户端能够轻松找到提供特定服务的容器,您可以告诉 Kubernetes 哪些容器提供相同的服务,Kubernetes 将将它们全部暴露在单个静态 IP 地址上,并将该地址暴露给集群中运行的所有应用程序。这是通过环境变量完成的,但客户端也可以通过古老的 DNS 查找服务 IP。kube-proxy 将确保连接到服务的连接在提供服务的所有容器之间进行负载均衡。服务的 IP 地址保持不变,因此客户端可以始终连接到其容器,即使它们在集群中移动。
1.3.5. 理解使用 Kubernetes 的好处
如果您在所有服务器上部署了 Kubernetes,运维团队就不再需要处理应用程序的部署了。因为容器化应用程序已经包含了运行所需的所有内容,系统管理员不需要安装任何东西来部署和运行应用程序。在任何已部署 Kubernetes 的节点上,Kubernetes 可以立即运行应用程序,无需系统管理员的帮助。
简化应用程序部署
因为 Kubernetes 将所有工作节点暴露为一个单一的部署平台,应用程序开发者可以自行开始部署应用程序,而无需了解构成集群的服务器。
从本质上讲,所有节点现在都是一组等待应用程序消耗的计算资源。开发者通常不关心应用程序运行在哪种服务器上,只要服务器能够为应用程序提供足够的系统资源即可。
确实存在某些情况下,开发者关心应用程序应该运行在哪种硬件上。如果节点是异构的,你会发现有些情况下你希望某些应用程序运行在具有特定功能的节点上,而其他应用程序则运行在其他节点上。例如,你的某个应用程序可能需要在具有 SSD 的系统上运行,而其他应用程序在 HDD 上运行也运行良好。在这种情况下,显然你希望确保该特定应用程序始终被调度到具有 SSD 的节点上。
在不使用 Kubernetes 的情况下,系统管理员会选择一个具有 SSD 的特定节点并将应用程序部署在那里。但是,当使用 Kubernetes 时,而不是选择应用程序应该运行的特定节点,更合适的是告诉 Kubernetes 只在具有 SSD 的节点中选择。你将在第三章(index_split_028.html#filepos271328)中学习如何做到这一点。
实现更好的硬件利用率
通过在你的服务器上设置 Kubernetes 并使用它来运行你的应用程序而不是手动运行它们,你已经将应用程序与基础设施解耦。当你告诉 Kubernetes 运行你的应用程序时,你是在让它根据应用程序的资源需求描述和每个节点上的可用资源选择最合适的节点来运行你的应用程序。
通过使用容器而不是将应用程序绑定到集群中的特定节点,你允许应用程序在任何时间自由地在集群中移动,因此集群上运行的不同应用程序组件可以混合匹配,紧密打包到集群节点上。这确保了节点的硬件资源得到尽可能好的利用。
能够在任何时间在集群中移动应用程序的能力,使得 Kubernetes 能够比手动实现更好地利用基础设施。人类在寻找最佳组合方面并不擅长,尤其是当所有可能选项的数量巨大时,例如当你有许多应用程序组件和许多可以部署的服务器节点时。计算机显然可以比人类更好地、更快地完成这项工作。
健康检查和自我修复
拥有一个系统,可以在任何时间将应用程序移动到集群中,在服务器故障的情况下也很有价值。随着你的集群规模增加,你将更频繁地处理失败的计算机组件。
Kubernetes 监控你的应用程序组件以及它们运行的节点,并在节点故障的情况下自动将它们重新调度到其他节点。这使运维团队从手动迁移应用程序组件的负担中解脱出来,并允许团队立即专注于修复节点本身并将其返回到可用硬件资源池,而不是专注于重新定位应用程序。
如果您的基础设施有足够的备用资源,即使在没有失败节点的正常系统操作中,运维团队也不必立即对故障做出反应,例如凌晨 3 点,他们可以安心入睡,在正常工作时间处理失败的节点。
自动扩展
使用 Kubernetes 来管理您的已部署应用程序还意味着运维团队不需要不断监控单个应用程序的负载以应对突发的负载峰值。如前所述,Kubernetes 可以被告知监控每个应用程序使用的资源,并持续调整每个应用程序运行的实例数量。
如果 Kubernetes 运行在云基础设施上,添加额外的节点就像通过云提供商的 API 请求它们一样简单,那么 Kubernetes 甚至可以根据部署的应用程序的需求自动调整整个集群的大小,进行扩展或缩减。
简化应用程序开发
上一个章节中描述的功能主要对运维团队有益。但开发者呢?Kubernetes 为他们带来了什么?当然,它确实带来了。
如果您回顾一下应用程序在开发和生产环境中都在同一环境中运行的事实,这将对何时发现错误产生重大影响。我们都同意,您发现错误越早,修复它就越容易,修复它需要的劳动也越少。修复错误的是开发者,这意味着他们需要做的工作更少。
然后还有这样一个事实,开发者不需要实现他们通常需要实现的功能。这包括在集群应用程序中发现服务和/或对等节点。Kubernetes 会代替应用程序来做这件事。通常,应用程序只需要查找某些环境变量或执行 DNS 查询。如果这还不够,应用程序可以直接查询 Kubernetes API 服务器以获取这些和/或其他信息。以这种方式查询 Kubernetes API 服务器甚至可以节省开发者实现复杂的机制,如领导者选举。
作为 Kubernetes 带来的最终例证,您还需要考虑开发者将感受到的信心增加,知道当他们的应用程序的新版本即将推出时,Kubernetes 可以自动检测新版本是否不良,并立即停止其发布。这种信心增加通常加速了应用程序的持续交付,这对整个组织都有益。
1.4. 摘要
在本章的介绍中,您已经看到了应用程序在近年来是如何变化的,以及它们现在为什么更难部署和管理。我们介绍了 Kubernetes,并展示了它是如何与 Docker 和其他容器平台一起帮助部署和管理应用程序及其运行的基础设施的。您已经了解到
-
单体应用程序部署起来更容易,但随着时间的推移维护起来更困难,有时甚至无法进行扩展。
-
基于微服务的应用程序架构使得每个组件的开发更加容易,但部署和配置为一个单一系统则更为困难。
-
Linux 容器提供了与虚拟机几乎相同的优势,但它们更加轻量级,并允许更有效地利用硬件。
-
Docker 通过允许更轻松、更快速地提供容器化应用程序及其操作系统环境,改进了现有的 Linux 容器技术。
-
Kubernetes 将整个数据中心暴露为单个计算资源,用于运行应用程序。
-
开发者可以在不依赖系统管理员的情况下通过 Kubernetes 部署应用程序。
-
系统管理员可以通过让 Kubernetes 自动处理失败的节点来睡得更香。
在下一章中,你将通过构建一个应用程序并在 Docker 和 Kubernetes 中运行它来亲自动手。
第二章:Docker 和 Kubernetes 的第一步
本章涵盖
-
使用 Docker 创建、运行和共享容器镜像
-
在本地运行单节点 Kubernetes 集群
-
在 Google Kubernetes Engine 上设置 Kubernetes 集群
-
设置和使用
kubectl命令行客户端 -
在 Kubernetes 上部署应用程序并水平扩展
在您开始详细了解 Kubernetes 概念之前,让我们看看如何创建一个简单的应用程序,将其打包到容器镜像中,并在管理的 Kubernetes 集群(在 Google Kubernetes Engine 中)或本地单节点集群中运行。这应该会给你提供一个对整个 Kubernetes 系统的略微更好的概述,并使你更容易跟随接下来的几章,我们将介绍 Kubernetes 中的基本构建块和概念。
2.1. 创建、运行和共享容器镜像
正如你在上一章中学到的,在 Kubernetes 中运行应用程序需要将它们打包到容器镜像中。如果你还没有使用过 Docker,我们将对其进行基本介绍。在接下来的几节中,你将
-
安装 Docker 并运行您的第一个“Hello world”容器
-
创建一个简单的 Node.js 应用程序,你稍后将在 Kubernetes 中部署它
-
将应用程序打包到容器镜像中,以便您可以将其作为隔离的容器运行
-
基于镜像运行容器
-
将镜像推送到 Docker Hub,以便任何人都可以在任何地方运行它
2.1.1. 安装 Docker 并运行 Hello World 容器
首先,你需要在你的 Linux 机器上安装 Docker。如果你不使用 Linux,你需要启动一个 Linux 虚拟机(VM),并在该 VM 中运行 Docker。如果你使用 Mac 或 Windows,并按照说明安装 Docker,Docker 会为你设置一个 VM,并在该 VM 中运行 Docker 守护进程。Docker 客户端可执行文件将可用在你的主机操作系统上,并与 VM 内的守护进程进行通信。
要安装 Docker,请遵循docs.docker.com/engine/installation/中针对您特定操作系统的说明。完成安装后,您可以使用 Docker 客户端可执行文件运行各种 Docker 命令。例如,您可以尝试从 Docker Hub 拉取并运行一个现有的镜像,Docker Hub 是公共 Docker 注册库,其中包含许多知名软件包的现成容器镜像。其中之一是busybox镜像,您将使用它来运行简单的echo "Hello world"命令。
运行 Hello World 容器
如果你不太熟悉 busybox,它是一个包含许多标准 UNIX 命令行工具的单个可执行文件,例如echo、ls、gzip等。除了busybox镜像外,您还可以使用任何其他完整的 OS 容器镜像,如 Fedora、Ubuntu 或其他类似镜像,只要它包含echo可执行文件即可。
你如何运行busybox镜像?你不需要下载或安装任何东西。使用docker run命令并指定要下载和运行的镜像以及(可选)要执行的命令,如下所示。
列表 2.1. 使用 Docker 运行“Hello world”容器
$ docker run busybox echo "Hello world" 无法在本地找到镜像 'busybox:latest' latest: 正在从 docker.io/busybox 拉取 9a163e0b8d13: 拉取完成 fef924a0204a: 拉取完成 Digest: sha256:97473e34e311e6c1b3f61f2a721d038d1e5eef17d98d1353a513007cf46ca6bd 状态: 下载了较新的镜像 for docker.io/busybox:latest Hello world
这看起来并不那么令人印象深刻,但当你考虑到整个“应用”都是通过单个命令下载和执行的,而你无需安装该应用或任何其他东西,你就会同意这真是太棒了。在你的情况下,应用是一个单独的可执行文件(busybox),但它也可能是一个具有许多依赖关系的极其复杂的应用。设置和运行应用的全过程将完全相同。同样重要的是,应用是在容器内执行的,完全隔离于你机器上运行的所有其他进程。
理解幕后发生的事情
图 2.1 展示了当你执行docker run命令时发生了什么。首先,Docker 检查busybox:latest镜像是否已经存在于你的本地机器上。它不在,所以 Docker 从 Docker Hub 注册表docker.io中拉取了它。镜像下载到你的机器后,Docker 从该镜像创建了一个容器并在其中运行了命令。echo命令将文本打印到 STDOUT,然后进程终止,容器停止。
图 2.1. 在基于 busybox 容器镜像的容器中运行 echo “Hello world”

运行其他镜像
运行其他现有的容器镜像与运行busybox镜像的方式非常相似。实际上,它通常甚至更简单,因为你通常不需要指定要执行什么命令,就像在示例中那样(echo "Hello world")。应该执行的命令通常已经嵌入到镜像本身中,但如果你想的话,可以覆盖它。在搜索或浏览了hub.docker.com或另一个公共注册表上公开可用的镜像后,你告诉 Docker 这样运行镜像:
$ docker run <image>
容器镜像的版本控制
所有软件包都会更新,因此通常存在一个软件包的多个版本。Docker 支持在相同名称下拥有同一镜像的多个版本或变体。每个变体都必须有一个唯一的标签。当引用未明确指定标签的镜像时,Docker 将假设你指的是所谓的最新标签。要运行镜像的不同版本,你可以像这样指定标签和镜像名称:
$ docker run <image>:<tag>
2.1.2. 创建一个简单的 Node.js 应用
现在你已经有一个工作的 Docker 设置,你将创建一个应用。你将构建一个简单的 Node.js 网络应用并将其打包成容器镜像。该应用将接受 HTTP 请求并返回运行在其中的机器的主机名。这样,你就会看到容器中的应用看到的是它自己的主机名,而不是宿主机的,尽管它像任何其他进程一样在宿主机上运行。这将在你部署应用在 Kubernetes 上并扩展它(水平扩展;也就是说,运行应用的多个实例)时很有用。你会看到你的 HTTP 请求击中应用的不同实例。
你的应用将包含一个名为 app.js 的单个文件,其内容如下所示。
列表 2.2. 一个简单的 Node.js 应用:app.js
const http = require('http'); const os = require('os'); console.log("Kubia 服务器启动..."); var handler = function(request, response) { console.log("收到来自 " + request.connection.remoteAddress + " 的请求"); response.writeHead(200); response.end("您已击中 " + os.hostname() + "\n"); }; var www = http.createServer(handler); www.listen(8080);
应该很清楚这段代码的作用。它启动了 8080 端口的 HTTP 服务器。服务器对每个请求返回 HTTP 响应状态码200 OK和文本"您已击中 <hostname>"。请求处理程序还将客户端的 IP 地址记录到标准输出,这你以后会用到。
注意
返回的主机名是服务器的实际主机名,而不是客户端在 HTTP 请求的Host头中发送的那个。
现在,你可以下载并安装 Node.js 并直接测试你的应用,但这不是必需的,因为你会使用 Docker 将应用打包成容器镜像,并使其能够在任何地方运行,无需下载或安装任何东西(除了 Docker,它确实需要在你想运行镜像的机器上安装)。
2.1.3. 为镜像创建 Dockerfile
要将你的应用打包成镜像,你首先需要创建一个名为 Dockerfile 的文件,该文件将包含 Docker 在构建镜像时将执行的指令列表。Dockerfile 需要与 app.js 文件在同一个目录中,并应包含以下列表中显示的命令。
列表 2.3. 为你的应用构建容器镜像的 Dockerfile
FROM node:7 ADD app.js /app.js ENTRYPOINT ["node", "app.js"]
FROM行定义了你将用作起始点的容器镜像(你将在其上构建的基础镜像)。在你的情况下,你使用的是node容器镜像,标签7。在第二行,你将你的 app.js 文件从本地目录添加到镜像的根目录下,使用相同的名称(app.js)。最后,在第三行,你定义了当有人运行镜像时应执行什么命令。在你的情况下,命令是node app.js。
选择基础镜像
你可能会想知道为什么我们选择了这个特定的镜像作为基础。因为你的应用是一个 Node.js 应用,你需要你的镜像包含 node 二进制可执行文件来运行应用。你可以使用包含该二进制的任何镜像,或者甚至可以使用像 fedora 或 ubuntu 这样的 Linux 发行版基础镜像,并在镜像构建时将 Node.js 安装到容器中。但是,因为 node 镜像是专门为运行 Node.js 应用而制作的,并且包含了运行你的应用所需的一切,所以你会使用它作为基础镜像。
2.1.4. 构建容器镜像
现在你有了 Dockerfile 和 app.js 文件,你已经拥有了构建镜像所需的一切。要构建它,运行以下 Docker 命令:
$ docker build -t kubia .
图 2.2 展示了构建过程中的情况。你正在告诉 Docker 基于当前目录的内容构建一个名为 kubia 的镜像(注意构建命令末尾的点)。Docker 将在目录中查找 Dockerfile,并根据文件中的说明构建镜像。
图 2.2. 从 Dockerfile 构建新的容器镜像

理解镜像的构建过程
构建过程不是由 Docker 客户端执行的。相反,整个目录的内容被上传到 Docker 守护进程,并在那里构建镜像。客户端和守护进程不需要在同一个机器上。如果你在非 Linux 操作系统上使用 Docker,客户端位于你的宿主操作系统上,但守护进程在虚拟机内部运行。因为构建目录中的所有文件都上传到了守护进程,如果它包含许多大文件且守护进程没有本地运行,上传可能需要更长的时间。
小贴士
不要在构建目录中包含任何不必要的文件,因为它们会减慢构建过程——尤其是在 Docker 守护进程在远程机器上时。
在构建过程中,Docker 将首先从公共镜像仓库(Docker Hub)拉取基础镜像(node:7),除非该镜像已经被拉取并存储在你的机器上。
理解镜像层
镜像不是一个单一的、大的二进制块,而是由多个层组成,你可能已经在运行 busybox 示例时注意到了这一点(有多个 Pull complete 行——每一行对应一个层)。不同的镜像可能共享几个层,这使得存储和传输镜像变得更加高效。例如,如果你基于相同的基镜像创建了多个镜像(例如示例中的 node:7),构成基镜像的所有层将只存储一次。此外,在拉取镜像时,Docker 将单独下载每一层。可能已经有几个层存储在你的机器上,所以 Docker 只会下载那些尚未下载的层。
你可能会认为每个 Dockerfile 只创建一个新层,但这并不是事实。在构建镜像时,Dockerfile 中的每个单独命令都会创建一个新的层。在你的镜像构建过程中,在拉取所有基础镜像层之后,Docker 将在它们之上创建一个新的层并将 app.js 文件添加进去。然后它将创建另一个层,该层将指定在运行镜像时应执行的命令。这个最后的层将被标记为kubia:latest。这如图 2.3figure 2.3 所示,它还显示了另一个名为other:latest的不同镜像如何使用与你的镜像相同的 Node.js 镜像层。
图 2.3. 容器镜像由可以供不同镜像共享的层组成。

当构建过程完成后,你将在本地存储一个新镜像。你可以通过告诉 Docker 列出所有本地存储的镜像来查看它,如下面的列表所示。
列表 2.4. 列出本地存储的镜像
$ docker images REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE kubia latest d30ecc7419e7 1 minute ago 637.1 MB ...
比较使用 Dockerfile 构建镜像与手动构建
Dockerfile 是使用 Docker 构建容器镜像的常用方式,但你也可以通过从现有镜像运行一个容器、在容器中执行命令、退出容器并将最终状态提交为新的镜像来手动构建镜像。这正是从 Dockerfile 构建时发生的情况,但它会自动执行且可重复,这允许你随时修改 Dockerfile 并重新构建镜像,而无需再次手动输入所有命令。
2.1.5. 运行容器镜像
你现在可以使用以下命令运行你的镜像:
$ docker run --name kubia-container -p 8080:8080 -d kubia
这告诉 Docker 从一个名为kubia的镜像运行一个新的容器kubia-container。容器将从控制台分离(-d标志),这意味着它将在后台运行。本地机器上的 8080 端口将被映射到容器内的 8080 端口(-p 8080:8080选项),因此你可以通过 http://localhost:8080 访问应用。
如果你没有在本地机器上运行 Docker 守护进程(如果你使用 Mac 或 Windows,守护进程在虚拟机中运行),你需要使用运行守护进程的虚拟机的主机名或 IP 地址而不是 localhost。你可以通过DOCKER_HOST环境变量来查找它。
访问你的应用
现在尝试通过 http://localhost:8080 访问你的应用(如果需要,请确保将 localhost 替换为 Docker 主机的主机名或 IP 地址):
$ curl localhost:8080 您已访问 44d76963e8e1
那就是您应用程序的响应。您的小型应用程序现在正在容器内运行,与其他所有内容隔离。如您所见,它正在返回44d76963e8e1作为其主机名,而不是您主机机的实际主机名。这个十六进制数是 Docker 容器的 ID。
列出所有正在运行的容器
让我们在下面的列表中列出所有正在运行的容器,这样您就可以检查列表(我已经编辑了输出以使其更易于阅读——想象最后两行是前两行的延续)。
列表 2.5. 列出正在运行的容器
$ docker ps CONTAINER ID IMAGE COMMAND CREATED ... 44d76963e8e1 kubia:latest "/bin/sh -c 'node ap 6 minutes ago ... ... STATUS PORTS NAMES ... Up 6 minutes 0.0.0.0:8080->8080/tcp kubia-container
单个容器正在运行。对于每个容器,Docker 都会打印出其 ID 和名称、运行容器的镜像以及容器内正在执行的命令。
获取关于容器的额外信息
docker ps命令只显示关于容器最基本的信息。要查看更多信息,您可以使用docker inspect:
$ docker inspect kubia-container
Docker 将打印出一个包含关于容器低级信息的长 JSON。
2.1.6. 探索正在运行的容器内部
如果您想看看容器内部的环境是什么样的呢?因为多个进程可以在同一个容器内运行,所以您总是可以在其中运行一个额外的进程来查看内部情况。如果您提供的镜像中包含 shell 的二进制可执行文件,您甚至可以运行一个 shell。
在现有容器内运行 shell
您基于的 Node.js 镜像包含 bash shell,因此您可以在容器内部像这样运行 shell:
$ docker exec -it kubia-container bash
这将在现有的kubia-container容器内部运行bash。bash进程将具有与主容器进程相同的 Linux 命名空间。这允许您从容器内部探索容器,并查看 Node.js 和您的应用程序在容器内运行时如何看待系统。-it选项是两个选项的缩写:
-
-i,这确保 STDIN 保持打开。您需要这个来向 shell 输入命令。 -
-t,这会分配一个伪终端(TTY)。
如果您想像平时一样使用 shell,您需要两者。 (如果您省略了第一个,您就不能输入任何命令,如果您省略了第二个,命令提示符将不会显示,并且某些命令会抱怨TERM变量未设置。)
从容器内部探索容器
让我们看看如何在以下列表中使用 shell 来查看容器内正在运行的进程。
列表 2.6. 从容器内部列出进程
root@44d76963e8e1:/# ps aux USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 0.0 0.1 676380 16504 ? Sl 12:31 0:00 node app.js root 10 0.0 0.0 20216 1924 ? Ss 12:31 0:00 bash root 19 0.0 0.0 17492 1136 ? R+ 12:38 0:00 ps aux
你只能看到三个进程。你看不到宿主 OS 上的任何其他进程。
了解容器中的进程是在宿主操作系统上运行的
如果你现在打开另一个终端并列出宿主 OS 上的进程,你将在所有其他宿主进程之间,也会看到容器中运行的进程,如列表 2.7 所示。
注意
如果你使用的是 Mac 或 Windows,你需要登录到 Docker 守护进程正在运行的虚拟机中,才能看到这些进程。
列表 2.7. 容器的进程在宿主 OS 上运行
$ ps aux | grep app.js USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 382 0.0 0.1 676380 16504 ? Sl 12:31 0:00 node app.js
这证明了在容器中运行的进程是在宿主 OS 上运行的。如果你有敏锐的观察力,你可能已经注意到容器内部与宿主上的进程 ID 不同。容器正在使用自己的 PID Linux 命名空间,并且有一个完全隔离的进程树,有自己的数字序列。
容器的文件系统也是隔离的
就像有一个隔离的进程树一样,每个容器也有一个隔离的文件系统。列出容器内根目录的内容只会显示容器中的文件,并将包括镜像中的所有文件以及容器运行期间创建的任何文件(日志文件等),如下面的列表所示。
列表 2.8. 容器拥有自己的完整文件系统
root@44d76963e8e1:/# ls / app.js boot etc lib media opt root sbin sys usr bin dev home lib64 mnt proc run srv tmp var
它包含app.js文件和其他系统目录,这些目录是node:7基础镜像的一部分。要退出容器,通过运行exit命令退出 shell,你将返回到宿主机器(例如,从 ssh 会话注销)。
提示
以这种方式进入正在运行的容器非常有用,当你调试容器中运行的程序时。当出现问题的时候,你首先想探索的是应用程序所看到的实际系统状态。请记住,应用程序不仅会看到它自己的唯一文件系统,还会看到进程、用户、主机名和网络接口。
2.1.7. 停止和删除容器
要停止你的应用程序,你告诉 Docker 停止kubia-container容器:
$ docker stop kubia-container
这将停止容器中运行的主进程,从而停止容器,因为容器内没有其他进程在运行。容器本身仍然存在,你可以使用docker ps -a来查看它。-a选项会打印出所有容器,包括正在运行的和已经停止的。要真正删除一个容器,你需要使用docker rm命令来删除它:
$ docker rm kubia-container
这将删除容器。所有内容都将被删除,并且无法再次启动。
2.1.8. 将图像推送到图像注册表
你构建的图像到目前为止只在你本地机器上可用。为了允许你在任何其他机器上运行它,你需要将图像推送到外部图像注册表。为了简化,你不会设置私有图像注册表,而是将图像推送到 Docker Hub (hub.docker.com),这是公开可用的注册表之一。其他广泛使用的注册表包括 Quay.io 和 Google Container Registry。
在你这样做之前,你需要根据 Docker Hub 的规则重新标记你的图像。如果图像的仓库名称以你的 Docker Hub ID 开头,Docker Hub 将允许你推送图像。你通过在hub.docker.com注册来创建你的 Docker Hub ID。在下面的示例中,我将使用我的 ID(luksa)。请将所有出现的地方都替换为你的 ID。
在附加标签下标记图像
一旦你知道你的 ID,你就可以准备重命名你的图像,当前标记为kubia,为luksa/kubia(将luksa替换为你的 Docker Hub ID):
$ docker tag kubia luksa/kubia
这不会重命名标签;它为相同的图像创建了一个额外的标签。你可以通过使用docker images命令列出你系统上存储的图像来确认这一点,如下面的列表所示。
列表 2.9. 一个容器图像可以有多个标签
$ docker images | head REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE luksa/kubia latest d30ecc7419e7 大约一个小时前 654.5 MB kubia latest d30ecc7419e7 大约一个小时前 654.5 MB docker.io/node 7.0 04c0ca2a8dad 两天前 654.5 MB ...
如你所见,kubia和luksa/kubia都指向相同的图像 ID,因此它们实际上是一个带有两个标签的单个图像。
将图像推送到 Docker Hub
在你可以将图像推送到 Docker Hub 之前,你需要使用docker login命令以你的用户 ID 登录。一旦登录,你就可以像这样最终将yourid/kubia图像推送到 Docker Hub:
$ docker push luksa/kubia
在不同机器上运行图像
在将图像推送到 Docker Hub 完成后,图像将对每个人可用。你现在可以在任何运行 Docker 的机器上运行该图像,只需执行以下命令:
$ docker run -p 8080:8080 -d luksa/kubia
没有什么比这更简单了。而且,最好的事情是,你的应用程序每次运行时都会在相同的环境中运行。如果它在你的机器上运行良好,它也应该在其他任何 Linux 机器上运行良好。无需担心主机机器是否安装了 Node.js。实际上,即使安装了,你的应用程序也不会使用它,因为它将使用镜像内部安装的版本。
2.2. 设置 Kubernetes 集群
现在你已经将你的应用程序打包在容器镜像中并通过 Docker Hub 提供了,你可以在 Kubernetes 集群中部署它,而不是直接在 Docker 中运行。但首先,你需要设置集群本身。
设置一个完整的、多节点的 Kubernetes 集群不是一项简单的任务,尤其是如果你不熟悉 Linux 和网络管理。一个合适的 Kubernetes 安装跨越多个物理或虚拟机器,并需要正确设置网络,以便 Kubernetes 集群内部运行的所有容器都能通过相同的扁平网络空间相互连接。
安装 Kubernetes 集群的方法有很多。这些方法在 kubernetes.io 的文档中有详细描述。我们不会在这里列出所有方法,因为列表不断在变化,但 Kubernetes 可以运行在你的本地开发机器上,你自己的组织机器集群上,在提供虚拟机的云服务提供商(如 Google Compute Engine、Amazon EC2、Microsoft Azure 等)上,或者通过使用管理的 Kubernetes 集群,例如 Google Kubernetes Engine(之前称为 Google Container Engine)。
在本章中,我们将介绍两种简单的方法来获取一个正在运行的 Kubernetes 集群。你将看到如何在你的本地机器上运行一个单节点 Kubernetes 集群,以及如何访问运行在 Google Kubernetes Engine(GKE)上的托管集群。
第三个选项,涵盖了使用 kubeadm 工具安装集群,在 附录 B 中有解释。那里的说明展示了如何使用虚拟机设置一个三节点 Kubernetes 集群,但我建议你在阅读本书的前 11 章后再尝试。
另一个选项是将 Kubernetes 安装在亚马逊的 AWS(Amazon Web Services)上。为此,你可以查看 kops 工具,它建立在前面提到的 kubeadm 之上,可在 github.com/kubernetes/kops 找到。它可以帮助你在 AWS 上部署生产级别的、高可用的 Kubernetes 集群,并最终支持其他平台(如 Google Kubernetes Engine、VMware、vSphere 等)。
2.2.1. 使用 Minikube 运行本地单节点 Kubernetes 集群
使用 Minikube 是实现一个完全功能化的 Kubernetes 集群最简单、最快的方法。Minikube 是一个工具,它设置了一个单节点集群,非常适合测试 Kubernetes 和本地开发应用程序。
尽管我们无法展示与多节点上管理应用程序相关的某些 Kubernetes 功能,但单节点集群应该足以探索本书中讨论的大多数主题。
安装 Minikube
Minikube 是一个单二进制文件,需要下载并放置到你的路径中。它适用于 OSX、Linux 和 Windows。要安装它,最佳做法是访问 GitHub 上的 Minikube 仓库 (github.com/kubernetes/minikube) 并遵循那里的说明。
例如,在 OSX 和 Linux 上,Minikube 可以通过单个命令下载和设置。对于 OSX,命令如下所示:
$ curl -Lo minikube https://storage.googleapis.com/minikube/releases/
v0.23.0/minikube-darwin-amd64 && chmod +x minikube && sudo mv minikube
/usr/local/bin/
在 Linux 上,你需要下载不同的版本(在 URL 中将“darwin”替换为“linux”)。在 Windows 上,你可以手动下载文件,将其重命名为 minikube.exe,并将其放置到你的路径中。Minikube 在通过 VirtualBox 或 KVM 运行的 VM 内运行 Kubernetes,因此在你开始 Minikube 集群之前,你还需要安装其中一个。
使用 Minikube 启动 Kubernetes 集群
一旦你在本地安装了 Minikube,你就可以立即使用以下列表中的命令启动 Kubernetes 集群。
列表 2.10. 启动 Minikube 虚拟机
$ minikube start Starting local Kubernetes cluster... Starting VM... SSH-ing files into VM... ... Kubectl is now configured to use the cluster.
启动集群需要超过一分钟,所以不要在命令完成之前中断它。
安装 Kubernetes 客户端(kubectl)
要与 Kubernetes 交互,你还需要 kubectl CLI 客户端。同样,你所需要做的只是下载它并将其放置到你的路径中。例如,OSX 的最新稳定版本可以通过以下命令下载和安装:
$ curl -LO https://storage.googleapis.com/kubernetes-release/release
/$(curl -s https://storage.googleapis.com/kubernetes-release/release
/stable.txt)/bin/darwin/amd64/kubectl
&& chmod +x kubectl
&& sudo mv kubectl /usr/local/bin/
要下载 Linux 或 Windows 的 kubectl,将 URL 中的 darwin 替换为 linux 或 windows。
注意
如果你将使用多个 Kubernetes 集群(例如,同时使用 Minikube 和 GKE),请参阅附录 A(index_split_135.html#filepos1721130)以获取有关如何设置和在不同 kubectl 上下文之间切换的信息。
检查集群是否启动并且 kubectl 可以与之通信
要验证您的集群是否正常工作,您可以使用以下列表中的 kubectl cluster-info 命令。
列表 2.11. 显示集群信息
$ kubectl cluster-info Kubernetes master is running at https://192.168.99.100:8443 KubeDNS is running at https://192.168.99.100:8443/api/v1/proxy/... kubernetes-dashboard is running at https://192.168.99.100:8443/api/v1/...
这表明集群正在运行。它显示了各种 Kubernetes 组件的 URL,包括 API 服务器和网页控制台。
提示
您可以运行 minikube ssh 登录到 Minikube 虚拟机并从内部探索它。例如,您可能想查看节点上正在运行哪些进程。
2.2.2. 使用 Google Kubernetes Engine 托管 Kubernetes 集群
如果您想探索一个完整的、多节点的 Kubernetes 集群,您可以使用托管 Google Kubernetes Engine (GKE) 集群。这样,您就不需要手动设置所有集群节点和网络,这对于刚开始使用 Kubernetes 的人来说通常太多。使用像 GKE 这样的托管解决方案可以确保您不会得到一个配置错误、无法工作或部分工作的集群。
设置 Google Cloud 项目并下载必要的客户端二进制文件
在您设置新的 Kubernetes 集群之前,您需要设置您的 GKE 环境。因为过程可能会改变,所以我这里没有列出具体的指令。要开始,请遵循cloud.google.com/container-engine/docs/before-you-begin上的说明。
大概来说,整个过程包括
-
在不太可能的情况下,如果您还没有 Google 账户,请注册一个。
-
在 Google Cloud Platform 控制台中创建一个项目。
-
启用计费。这确实需要您的信用卡信息,但 Google 提供了 12 个月的免费试用期。而且他们足够好,不会在免费试用期结束后自动开始收费。)
-
启用 Kubernetes Engine API。
-
下载并安装 Google Cloud SDK。(这包括 gcloud 命令行工具,您需要它来创建 Kubernetes 集群。)
-
使用
gcloud components install kubectl安装kubectl命令行工具。
注意
某些操作(例如步骤 2 中的操作)可能需要几分钟才能完成,所以请放松,同时喝杯咖啡。
创建一个包含三个节点的 Kubernetes 集群
安装完成后,您可以使用以下列表中的命令创建一个包含三个工作节点的 Kubernetes 集群。
列表 2.12. 在 GKE 中创建一个三节点集群
$ gcloud container clusters create kubia --num-nodes 3
--machine-type f1-micro Creating cluster kubia...done. Created [https://container.googleapis.com/v1/projects/kubia1-1227/zones/europe-west1-d/clusters/kubia]. kubeconfig entry generated for kubia. NAME ZONE MST_VER MASTER_IP TYPE NODE_VER NUM_NODES STATUS kubia eu-w1d 1.5.3 104.155.92.30 f1-micro 1.5.3 3 RUNNING
您现在应该有一个运行中的 Kubernetes 集群,其中包含三个工作节点,如图 2.4 所示。您使用三个节点来更好地展示适用于多个节点的功能。如果您想的话,可以使用更少的节点。
图 2.4. 您如何与您的三节点 Kubernetes 集群交互

获取集群概览
为了让您对集群的外观和如何与之交互有一个基本的了解,请参阅图 2.4。每个节点都运行 Docker、Kubelet 和 kube-proxy。您将通过kubectl命令行客户端与集群交互,该客户端向运行在主节点上的 Kubernetes API 服务器发出 REST 请求。
通过列出集群节点来检查集群是否启动
您现在将使用kubectl命令列出您集群中的所有节点,如下所示。
列表 2.13. 使用kubectl列出集群节点
$ kubectl get nodes NAME STATUS AGE VERSION gke-kubia-85f6-node-0rrx Ready 1m v1.5.3 gke-kubia-85f6-node-heo1 Ready 1m v1.5.3 gke-kubia-85f6-node-vs9f Ready 1m v1.5.3
kubectl get命令可以列出各种 Kubernetes 对象。您将经常使用它,但它通常只显示列出对象的最低级信息。
提示
您可以使用gcloud compute ssh <node-name>登录到其中一个节点,以探索节点上正在运行的内容。
获取对象的额外详细信息
要查看关于对象的更详细信息,您可以使用kubectl describe命令,它显示的信息更多:
$ kubectl describe node gke-kubia-85f6-node-0rrx
我省略了describe命令的实际输出,因为它相当宽,在这里的书本中会完全无法阅读。输出显示了节点的状态、其 CPU 和内存数据、系统信息、节点上运行的容器以及更多内容。
在之前的kubectl describe示例中,您明确指定了节点的名称,但您也可以简单地执行一个不带节点名称的kubectl describe node,它将打印出所有节点的详细描述。
提示
当只有一个特定类型的对象存在时,不指定对象名称来运行describe和get命令会很有用,这样您就不会浪费时间输入或复制粘贴对象的名称。
当我们谈论减少按键次数时,让我在继续运行你的第一个 Kubernetes 应用程序之前,给你一些额外的建议,让你使用 kubectl 的工作变得更简单。
2.2.3. 为 kubectl 设置别名和命令行补全
你会经常使用 kubectl。很快你就会意识到每次都必须输入完整的命令真的很痛苦。在你继续之前,花一分钟时间设置一个别名和 kubectl 的自动补全,让你的生活变得更轻松。
创建别名
在整本书中,我都会使用 kubectl 可执行文件的完整名称,但你可能想添加一个简短的别名,比如 k,这样你就不必每次都输入 kubectl。如果你还没有使用过别名,这里是如何定义一个别名的方法。将以下行添加到你的 ~/.bashrc 或等效文件中:
alias k=kubectl
注意
如果你使用 gcloud 设置集群,你可能已经有了 k 可执行文件。
为 kubectl 配置自动补全
即使是像 k 这样简短的别名,你仍然需要输入比你想的多的内容。幸运的是,kubectl 命令还可以为 bash 和 zsh shell 输出自动补全代码。它不仅启用了命令名称的自动补全,还启用了实际对象名称的自动补全。例如,在先前的例子中,你不需要输入整个节点名称,你只需要输入
$ kubectl desc<TAB> no<TAB> gke-ku<TAB>
要在 bash 中启用自动补全,你首先需要安装一个名为 bash-completion 的包,然后运行以下命令(你可能还希望将其添加到 ~/.bashrc 或等效文件中):
$ source <(kubectl completion bash)
但有一个注意事项。当你运行前面的命令时,自动补全只会在你使用完整的 kubectl 名称时才起作用(使用 k 别名时不会起作用)。为了解决这个问题,你需要稍微修改一下 kubectl completion 命令的输出:
$ source <(kubectl completion bash | sed s/kubectl/k/g)
注意
不幸的是,在我写这篇文章的时候,在 MacOS 上,shell 自动补全对别名不起作用。如果你想使用自动补全,你必须使用完整的 kubectl 命令名称。
现在你已经准备好开始与你的集群交互,而无需输入太多。你终于可以在 Kubernetes 上运行你的第一个应用程序了。
2.3. 运行你的第一个 Kubernetes 应用程序
因为这可能是你的第一次,你将使用最简单的方法在 Kubernetes 上运行应用程序。通常,你会准备一个 JSON 或 YAML 清单,其中包含你想要部署的所有组件的描述,但由于我们还没有讨论你可以在 Kubernetes 中创建的组件类型,你将使用一个简单的单行命令来运行一些内容。
2.3.1. 部署你的 Node.js 应用程序
部署您的应用程序最简单的方法是使用kubectl run命令,该命令将创建所有必要的组件,而无需处理 JSON 或 YAML。这样,我们就不必深入研究每个对象的架构。尝试运行您之前创建并推送到 Docker Hub 的镜像。以下是您如何在 Kubernetes 中运行它的方法:
$ kubectl run kubia --image=luksa/kubia --port=8080 --generator=run/v1 replicationcontroller "kubia" created
--image=luksa/kubia部分显然指定了您想要运行的容器镜像,而--port=8080选项告诉 Kubernetes 您的应用程序正在监听 8080 端口。最后一个标志(--generator)确实需要解释。通常,您不会使用它,但在这里您使用它,以便 Kubernetes 创建一个 ReplicationController 而不是 Deployment。您将在本章后面了解 ReplicationController 是什么,但我们不会在第九章之前讨论 Deployment。这就是为什么我不想让kubectl现在就创建一个 Deployment。
如前一个命令的输出所示,已创建了一个名为kubia的 ReplicationController。如前所述,我们将在本章后面了解它是什么。现在,让我们从底部开始,关注您创建的容器(您可以根据在run命令中指定的容器镜像来假设已经创建了一个容器)。
Pod 介绍
您可能想知道您是否可以在显示所有运行容器的列表中看到您的容器。也许像kubectl get containers这样的命令?嗯,这并不是 Kubernetes 的工作方式。它不会直接处理单个容器。相反,它使用多个协同定位容器的概念。这个容器组被称为 Pod。
Pod 是一组紧密相关的容器,这些容器将始终在同一个工作节点和同一个 Linux 命名空间(s)上一起运行。每个 Pod 就像一个独立的逻辑机器,拥有自己的 IP 地址、主机名、进程等,运行单个应用程序。该应用程序可以是一个单独的进程,在单个容器中运行,或者它可以是主应用程序进程和附加的辅助进程,每个进程都在自己的容器中运行。Pod 中的所有容器看起来就像在同一个逻辑机器上运行,而其他 Pod 中的容器,即使它们运行在同一个工作节点上,看起来就像在另一个节点上运行。
为了更好地理解容器、Pod 和节点之间的关系,请查看图 2.5。正如您所看到的,每个 Pod 都有自己的 IP 地址,并包含一个或多个容器,每个容器运行一个应用程序进程。Pod 被分散在不同的工作节点上。
图 2.5. 容器、Pod 和物理工作节点之间的关系

列出 Pod
由于您不能列出单个容器,因为它们不是独立的 Kubernetes 对象,您能否列出 pods?是的,您可以。让我们看看如何告诉 kubectl 列出 pods,如下面的列表所示。
列表 2.14. 列出 pods
$ kubectl get pods NAME READY STATUS RESTARTS AGE kubia-4jfyf 0/1 Pending 0 1m
这是你的 pod。其状态仍然是 Pending,pod 的单个容器显示为尚未就绪(这就是 READY 列中的 0/1 的含义)。pod 尚未运行的原因是因为分配给 pod 的工作节点正在下载容器镜像,以便运行它。下载完成后,pod 的容器将被创建,然后 pod 将过渡到 Running 状态,如下面的列表所示。
列表 2.15. 再次列出 pods 以查看 pod 的状态是否已更改
$ kubectl get pods NAME READY STATUS RESTARTS AGE kubia-4jfyf 1/1 Running 0 5m
要查看有关 pod 的更多信息,您还可以使用 kubectl describe pod 命令,就像您之前为工作节点之一所做的那样。如果 pod 一直处于 Pending 状态,可能是因为 Kubernetes 无法从注册表中拉取镜像。如果您使用的是自己的镜像,请确保它在 Docker Hub 上标记为公共。为了确保镜像可以成功拉取,请尝试在另一台机器上使用 docker pull 命令手动拉取镜像。
理解幕后发生的事情
为了帮助你可视化所发生的事情,请查看 图 2.6。它显示了您必须执行的两个步骤,以便在 Kubernetes 内运行容器镜像。首先,您构建了镜像并将其推送到 Docker Hub。这是必要的,因为仅在您的本地机器上构建镜像只会使其在本地机器上可用,但您需要使其对运行在工作节点上的 Docker 守护进程可访问。
图 2.6. 在 Kubernetes 中运行 luksa/kubia 容器镜像

当您运行 kubectl 命令时,它通过向 Kubernetes API 服务器发送 REST HTTP 请求,在集群中创建了一个新的 ReplicationController 对象。然后 ReplicationController 创建了一个新的 pod,该 pod 由调度器调度到工作节点之一。该节点上的 Kubelet 看到 pod 被调度到它,并指示 Docker 从注册表中拉取指定的镜像,因为镜像在本地不可用。下载镜像后,Docker 创建并运行了容器。
显示其他两个节点是为了提供上下文。它们在过程中没有发挥作用,因为 pod 没有被调度到它们。
定义
术语调度意味着将 pod 分配到节点。pod 将立即运行,而不是像术语可能让你认为的那样在未来某个时间运行。
2.3.2. 访问您的 Web 应用程序
当您的 Pod 运行时,您如何访问它?我们提到每个 Pod 都有自己的 IP 地址,但这个地址是集群内部的,并且无法从集群外部访问。为了使 Pod 可以从外部访问,您将通过服务对象将其公开。您将创建一个特殊的服务类型LoadBalancer,因为如果您创建一个普通的服务(一个ClusterIP服务),就像 Pod 一样,它也只可以从集群内部访问。通过创建LoadBalancer类型的服务,将创建一个外部负载均衡器,您可以通过负载均衡器的公网 IP 连接到 Pod。
创建服务对象
要创建服务,您将告诉 Kubernetes 公开您之前创建的 ReplicationController:
$ kubectl expose rc kubia --type=LoadBalancer --name kubia-http service "kubia-http" exposed
注意
我们使用缩写rc代替replicationcontroller。大多数资源类型都有这样的缩写,这样您就不必输入完整的名称(例如,po代表pods,svc代表services等)。
列出服务
expose命令的输出提到了一个名为kubia-http的服务。服务是像 Pod 和 Node 这样的对象,因此您可以通过运行kubectl get services命令来查看新创建的服务对象,如下所示。
列表 2.16. 列出服务
$ kubectl get services NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes 10.3.240.1 <none> 443/TCP 34m kubia-http 10.3.246.185 <pending> 8080:31348/TCP 4s
列表显示了两个服务。现在忽略kubernetes服务,仔细查看您创建的kubia-http服务。它还没有外部 IP 地址,因为云基础设施创建负载均衡器需要时间。一旦负载均衡器启动,服务的公网 IP 地址应该会显示出来。让我们稍等片刻,再次列出服务,如下所示。
列表 2.17. 再次列出服务以查看是否已分配外部 IP
$ kubectl get svc NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes 10.3.240.1 <none> 443/TCP 35m kubia-http 10.3.246.185 104.155.74.57 8080:31348/TCP 1m
哎,这就是外部 IP。现在您的应用程序可以从世界任何地方通过 http://104.155.74.57:8080 访问。
注意
Minikube 不支持LoadBalancer类型的服务,因此服务永远不会获得外部 IP。但您仍然可以通过其外部端口访问该服务。如何在下一节的提示中描述。
通过其外部 IP 访问您的服务
您现在可以通过服务的公网 IP 和端口向您的 Pod 发送请求:
$ curl 104.155.74.57:8080 您已访问 kubia-4jfyf
哇哦!你的应用程序现在正在你的三个节点 Kubernetes 集群(或者如果你使用 Minikube,则是一个单节点集群)的某个地方运行。如果你不计入设置整个集群所需的步骤,只需两个简单的命令就能让你的应用程序运行起来,并使其对全球用户可访问。
小贴士
当使用 Minikube 时,你可以通过运行 minikube service kubia-http 来获取访问服务的 IP 地址和端口。
如果你仔细观察,你会看到应用程序正在报告 Pod 的名称作为其主机名。正如之前提到的,每个 Pod 都像一台独立的机器,拥有自己的 IP 地址和主机名。尽管应用程序运行在工作节点的操作系统上,但对于应用程序来说,它似乎是在一个专门为该应用程序本身运行的独立机器上运行——没有其他进程与它并行运行。
2.3.3. 系统的逻辑部分
到目前为止,我主要解释了系统的实际物理组件。你有三个工作节点,它们是运行 Docker 和 Kubelet 的虚拟机,你还有一个控制整个系统的主节点。老实说,我们不知道是否单个主节点托管了 Kubernetes 控制平面的所有单个组件,或者它们分散在多个节点上。这并不重要,因为你只与 API 服务器交互,该服务器可以通过单个端点访问。
除了这个系统的物理视图之外,还有一个独立的逻辑视图。我已经提到了 Pod、ReplicationController 和 Service。所有这些内容将在接下来的几章中解释,但让我们快速看一下它们是如何结合在一起以及它们在你的小配置中扮演的角色。
理解 ReplicationController、Pod 和 Service 如何结合
正如我已经解释的,你并不是直接创建和操作容器。相反,Kubernetes 的基本构建块是 Pod。但是,你也没有真正创建任何 Pod,至少不是直接创建。通过运行 kubectl run 命令,你创建了一个 ReplicationController,而这个 ReplicationController 就是创建实际 Pod 对象的东西。为了使该 Pod 从集群外部可访问,你告诉 Kubernetes 将该 ReplicationController 管理的所有 Pod 作为单个 Service 公开。这三个元素的大致图示在图 2.7 中展示。
图 2.7. 你的系统由 ReplicationController、Pod 和 Service 组成。

理解 Pod 及其容器
你系统中的主要和最重要的组件是 Pod。它只包含一个容器,但通常 Pod 可以包含你想要的任意数量的容器。在容器内部是你的 Node.js 进程,它绑定到 8080 端口,并等待 HTTP 请求。Pod 有其唯一的私有 IP 地址和主机名。
理解 ReplicationController 的作用
下一个组件是kubia ReplicationController。它确保始终有一个 Pod 实例在运行。通常,ReplicationControllers 用于复制 Pod(即创建 Pod 的多个副本)并保持它们运行。在您的例子中,您没有指定您想要多少个 Pod 副本,所以 ReplicationController 创建了一个单一的副本。如果您的 Pod 因任何原因消失,ReplicationController 将创建一个新的 Pod 来替换缺失的 Pod。
理解为什么你需要一个服务
您系统的第三个组件是kubia-http服务。要了解为什么你需要服务,你需要了解关于 Pod 的一个关键细节。它们是短暂的。Pod 可能在任何时候消失——因为运行它的节点失败了,因为有人删除了 Pod,或者因为 Pod 被从其他健康的节点驱逐出去。当这些情况中的任何一种发生时,ReplicationController 将用新的 Pod 替换缺失的 Pod,如前所述。这个新的 Pod 将获得一个与被替换的 Pod 不同的 IP 地址。这就是服务发挥作用的地方——解决不断变化的 Pod IP 地址的问题,以及在一个单一的恒定 IP 和端口对中公开多个 Pod。
当创建服务时,它会获得一个静态 IP,在整个服务生命周期中都不会改变。客户端不应直接连接到 Pod,而应通过其恒定的 IP 地址连接到服务。服务确保其中一个 Pod 接收连接,无论 Pod 当前运行在哪里(以及它的 IP 地址是什么)。
服务代表了一组一个或多个提供相同服务的 Pod 的静态位置。发送到服务 IP 和端口的请求将被转发到该服务当时属于的一个 Pod 的 IP 和端口。
2.3.4. 横向扩展应用程序
您现在有一个正在运行的应用程序,由 ReplicationController 监控并保持运行,并通过服务向世界公开。现在让我们看看会发生什么额外的魔法。
使用 Kubernetes 的主要好处之一是您可以轻松地扩展您的部署。让我们看看扩展 Pod 数量的过程有多简单。您将增加运行实例的数量到三个。
您的 Pod 由 ReplicationController 管理。让我们用kubectl get命令来看看:
$ kubectl get replicationcontrollers NAME DESIRED CURRENT AGE kubia 1 1 17m
使用 kubectl get 列出所有资源类型
您一直在使用相同的kubectl get命令来列出集群中的内容。您已经使用此命令列出 Node、Pod、Service 和 ReplicationController 对象。您可以通过不指定类型来调用kubectl get以获取所有可能的对象类型的列表。然后您可以使用这些类型与各种kubectl命令,如get、describe等。列表还显示了之前提到的缩写。
列表中显示了一个名为 kubia 的单个 ReplicationController。DESIRED 列显示了您希望 ReplicationController 保持的 pod 副本数量,而 CURRENT 列显示了当前实际运行的 pod 数量。在您的例子中,您希望运行单个 pod 副本,并且目前正好有一个副本正在运行。
增加期望副本计数
要增加 pod 副本的数量,您需要像这样更改 ReplicationController 上的期望副本计数:
$ kubectl scale rc kubia --replicas=3
您现在已告诉 Kubernetes 确保始终运行三个 pod 实例。请注意,您没有指示 Kubernetes 应采取什么行动。您没有告诉它添加两个额外的 pod。您只是设置了新的期望实例数量,并让 Kubernetes 确定需要采取哪些行动来实现请求的状态。
这是 Kubernetes 最基本的原则之一。您不是告诉 Kubernetes 应该执行哪些具体操作,而是仅声明性地更改系统的期望状态,并让 Kubernetes 检查当前的实际情况,并将其与期望状态进行协调。这在 Kubernetes 的所有方面都是如此。
查看扩展后的结果
回到您的副本计数增加。让我们再次列出 ReplicationControllers 以查看更新的副本计数:
$ kubectl get rc
由于实际的 pod 数量已经增加到三个(如 CURRENT 列所示),现在列出所有 pod 应该显示三个 pod 而不是一个:
$ kubectl get pods
正如您所看到的,现在存在三个 pod 而不是一个。其中两个已经运行,一个仍在挂起状态,但应该在几秒钟内准备好,一旦容器镜像下载完成并且容器启动。
正如您所看到的,扩展应用程序非常简单。一旦您的应用程序在生产环境中运行,并且需要扩展应用程序,您就可以通过单个命令添加额外的实例,而无需手动安装和运行额外的副本。
请记住,应用程序本身需要支持水平扩展。Kubernetes 并不会神奇地使您的应用程序可扩展;它只是使扩展应用程序变得非常简单。
当访问服务时,看到请求击中所有三个 pod
由于您现在有多个应用程序实例正在运行,让我们看看再次访问服务 URL 时会发生什么。您是否会始终击中相同的应用程序实例?
$ curl 104.155.74.57:8080 您已访问 kubia-hczji $ curl 104.155.74.57:8080 您已访问 kubia-iq9y6 $ curl 104.155.74.57:8080 您已访问 kubia-iq9y6 $ curl 104.155.74.57:8080 您已访问 kubia-4jfyf
请求随机地击中不同的 pods。当有多个 pod 实例支持服务时,这就是 Kubernetes 中的服务所做的事情。它们充当多个 pod 前面的负载均衡器。当只有一个 pod 时,服务为单个 pod 提供一个静态地址。无论服务是由单个 pod 还是多个 pod 支持,这些 pod 在集群中移动时来来去去,这意味着它们的 IP 地址会改变,但服务始终在同一个地址上。这使得客户端很容易连接到 pods,无论存在多少个以及它们的位置如何频繁变化。
可视化您系统的最新状态
让我们再次可视化您的系统,看看与之前相比有什么变化。图 2.8 显示了您系统的最新状态。您仍然只有一个服务和一个 Replication-Controller,但现在您有三个 pod 实例,所有这些实例都由 ReplicationController 管理。服务不再将所有请求发送到单个 pod,而是像前一小节中用 curl 实验所示,将它们分散到所有三个 pod 上。
图 2.8. 由同一个 ReplicationController 管理、通过单个服务 IP 和端口暴露的三个 pod 实例。

作为一项练习,您现在可以通过增加 ReplicationController 的副本计数来启动更多实例,然后再将其缩小。
2.3.5. 检查您的应用运行在哪些节点上
您可能想知道您的 pods 被调度到了哪些节点。在 Kubernetes 世界里,pod 运行在哪个节点上并不是那么重要,只要它被调度到了一个可以提供 pod 正常运行所需的 CPU 和内存的节点。
无论它们被调度到哪个节点,容器内运行的所有应用都具有相同类型的操作系统环境。每个 pod 都有自己的 IP 地址,并且可以与任何其他 pod 通信,无论该 pod 是否也在同一个节点上运行或在不同的节点上。每个 pod 都提供了请求的计算资源量,因此这些资源是由一个节点还是另一个节点提供并不重要。
列出 pods 时显示 pod IP 和 pod 的节点
如果您一直很注意,您可能已经注意到 kubectl get pods 命令甚至没有显示任何关于 pods 被调度到哪些节点的信息。这是因为这通常不是一条重要的信息。
但您可以使用 -o wide 选项请求显示额外的列。当列出 pods 时,此选项会显示 pod 的 IP 地址以及 pod 运行的节点:
$ kubectl get pods -o wide NAME READY STATUS RESTARTS AGE IP NODE kubia-hczji 1/1 Running 0 7s 10.1.0.2 gke-kubia-85...
使用 kubectl describe 检查 pod 的其他详细信息
你还可以使用 kubectl describe 命令查看节点,该命令显示了 pod 的许多其他详细信息,如下所示。
列表 2.18. 使用 kubectl describe 描述 pod
$ kubectl describe pod kubia-hczji Name: kubia-hczji Namespace: default Node: gke-kubia-85f6-node-vs9f/10.132.0.3 1 Start Time: Fri, 29 Apr 2016 14:12:33 +0200 Labels: run=kubia Status: Running IP: 10.1.0.2 Controllers: ReplicationController/kubia Containers: ... Conditions: Type Status Ready True Volumes: ... Events: ...
- 1 这里是 pod 被调度到的节点。
这显示了包括节点在内的其他信息,例如 pod 被调度到的节点、启动时间、运行的镜像(们)以及其他有用的信息。
2.3.6. 介绍 Kubernetes 仪表板
在我们结束这个初步的实践章节之前,让我们看看另一种探索 Kubernetes 集群的方法。
到目前为止,你只使用过 kubectl 命令行工具。如果你更喜欢图形化网页用户界面,你将很高兴听到 Kubernetes 还附带了一个很好的(但仍在发展中)网页仪表板。
仪表板允许你列出集群中部署的所有 Pods、ReplicationControllers、Services 以及其他对象,以及创建、修改和删除它们。图 2.9 展示了仪表板。
图 2.9. Kubernetes 网页仪表板的截图

虽然在这本书中你不会使用仪表板,但你随时可以打开它,以便快速查看通过 kubectl 创建或修改对象后集群中部署的图形视图。
在 GKE 中运行 Kubernetes 时访问仪表板
如果你使用 Google Kubernetes Engine,你可以通过 kubectl cluster-info 命令找到仪表板的 URL,我们之前已经介绍过:
$ kubectl cluster-info | grep dashboard kubernetes-dashboard is running at https://104.155.108.191/api/v1/proxy/
namespaces/kube-system/services/kubernetes-dashboard
如果你在这个 URL 中打开浏览器,你会看到一个用户名和密码提示。你可以通过运行以下命令找到用户名和密码:
$ gcloud container clusters describe kubia | grep -E "(username|password):" password: 32nENgreEJ632A12 1 username: admin 1
- 1 仪表板的用户名和密码
使用 Minikube 访问仪表板
在使用 Minikube 运行 Kubernetes 集群时,在浏览器中打开仪表板的命令如下:
$ minikube dashboard
仪表板将在你的默认浏览器中打开。与 GKE 不同,你访问它时不需要输入任何凭证。
2.4. 摘要
希望这个初步的实践章节已经向你展示了 Kubernetes 不是一个复杂的平台,你准备好深入学习它所能提供的一切。在阅读这一章之后,你现在应该知道如何
-
拉取并运行任何公开可用的容器镜像
-
将你的应用打包成容器镜像,并通过将镜像推送到远程镜像仓库使任何人都可以访问
-
进入一个正在运行的容器并检查其环境
-
在 Google Kubernetes Engine 上设置一个多节点 Kubernetes 集群
-
为
kubectl命令行工具配置别名和 Tab 补全 -
在 Kubernetes 集群中列出并检查 Nodes、Pods、Services 和 ReplicationControllers
-
在 Kubernetes 中运行一个容器并使其可以从集群外部访问
-
对 Pods、ReplicationControllers 和 Services 三者之间的关系有一个基本的认识
-
通过更改 ReplicationController 的副本数量水平扩展应用
-
在 Minikube 和 GKE 上访问基于 Web 的 Kubernetes 仪表板
第二部分. 核心概念
第三章. Pods:在 Kubernetes 中运行容器
本章涵盖
-
创建、运行和停止 Pod
-
使用标签组织 Pod 和其他资源
-
对具有特定标签的所有 Pod 执行操作
-
使用命名空间将 Pod 分成非重叠组
-
将 Pod 调度到特定类型的 Worker 节点上
上一章应该已经为您提供了一个关于在 Kubernetes 中创建的基本组件的大致轮廓以及它们至少的概述。现在,我们将更详细地回顾所有类型的 Kubernetes 对象(或资源),以便您了解何时、如何以及为什么使用它们。我们将从 Pod 开始,因为它们是 Kubernetes 中中心、最重要的概念。其他所有内容要么管理、公开,要么被 Pod 使用。
3.1. Pod 简介
您已经了解到 Pod 是一组位于同一位置的容器,并且代表 Kubernetes 中的基本构建块。您不是单独部署容器,而是始终部署和操作容器 Pod。我们并不是暗示 Pod 总是包含多个容器——Pod 只包含单个容器是很常见的。关于 Pod 的关键点是,当 Pod 确实包含多个容器时,所有这些容器都总是在单个 Worker 节点上运行——它永远不会跨越多个 Worker 节点,如图 3.1 所示。
图 3.1. Pod 中的所有容器都在同一个节点上运行。Pod 永远不会跨越两个节点。

3.1.1. 理解为什么我们需要 Pod
但为什么我们甚至需要 Pod?为什么我们不能直接使用容器?为什么我们甚至需要一起运行多个容器?我们不能把所有进程都放入一个容器中吗?我们现在就来回答这些问题。
理解为什么多个容器比一个容器运行多个进程更好
想象一个由多个进程组成的 app,这些进程要么通过 IPC(进程间通信)通信,要么通过本地存储的文件通信,这要求它们在同一台机器上运行。因为在 Kubernetes 中,您总是运行容器中的进程,每个容器都类似于一个隔离的机器,您可能会认为在单个容器中运行多个进程是有意义的,但您不应该这样做。
容器被设计为每个容器只运行一个进程(除非进程本身产生子进程)。如果您在单个容器中运行多个不相关的进程,那么您有责任保持所有这些进程的运行,管理它们的日志等。例如,如果您必须包括一个机制来自动重启崩溃的个别进程。此外,所有这些进程都会记录到相同的标准输出,因此您很难弄清楚哪个进程记录了什么。
因此,您需要为每个进程运行其自己的容器。这就是 Docker 和 Kubernetes 被设计成使用的样子。
3.1.2. 理解 Pod
由于你不应该将多个进程组合到一个容器中,显然你需要另一个更高级的结构,这样你就可以将容器绑定在一起,并将它们作为一个单一单元来管理。这就是 pod 的推理依据。
容器 pod 允许你将紧密相关的进程一起运行,并为它们提供(几乎)与它们都在单个容器中运行时相同的环境,同时保持它们在一定程度上隔离。这样,你就可以兼得两者之利。你可以利用容器提供的所有功能,同时同时给进程一种它们在一起运行的错觉。
理解同一 pod 中容器之间的部分隔离
在上一章中,你了解到容器是完全相互隔离的,但现在你看到你想要隔离容器组而不是单个容器。你希望组内的容器共享某些资源,尽管不是全部,这样它们就不会完全隔离。Kubernetes 通过配置 Docker,让 pod 中的所有容器共享同一组 Linux 命名空间来实现这一点,而不是每个容器都有自己的命名空间集。
因为一个 pod 中的所有容器都在相同的网络和 UTS 命名空间下运行(这里我们谈论的是 Linux 命名空间),它们都共享相同的主机名和网络接口。同样,一个 pod 中的所有容器都在相同的 IPC 命名空间下运行,并且可以通过 IPC 进行通信。在最新的 Kubernetes 和 Docker 版本中,它们还可以共享相同的 PID 命名空间,但这个功能默认是未启用的。
注意
当同一 pod 中的容器使用单独的 PID 命名空间时,你在容器中运行 ps aux 命令时只能看到容器的自身进程。
但是,当涉及到文件系统时,情况就有些不同了。因为大多数容器的文件系统来自容器镜像,默认情况下,每个容器的文件系统与其他容器完全隔离。然而,可以使用 Kubernetes 的一个概念——Volume,让它们共享文件目录,我们将在第六章([index_split_055.html#filepos588298])中讨论。
理解容器如何共享相同的 IP 和端口空间
这里要强调的一点是,因为 pod 中的容器运行在相同的网络命名空间中,它们共享相同的 IP 地址和端口空间。这意味着在相同 pod 的容器中运行的过程需要注意不要绑定到相同的端口,否则会遇到端口冲突。但这只涉及同一 pod 中的容器。不同 pod 的容器永远不会遇到端口冲突,因为每个 pod 都有独立的端口空间。pod 中的所有容器也具有相同的回环网络接口,因此一个容器可以通过 localhost 与同一 pod 中的其他容器通信。
介绍扁平的 pod 间网络
Kubernetes 集群中的所有 Pod 都位于一个单一的、共享的、网络地址空间中(如图 3.2 所示 figure 3.2),这意味着每个 Pod 都可以通过其他 Pod 的 IP 地址访问其他 Pod。它们之间不存在 NAT(网络地址转换)网关。当两个 Pod 之间发送网络数据包时,它们各自都会看到对方实际的 IP 地址作为数据包中的源 IP。
图 3.2. 每个 Pod 都分配一个可路由的 IP 地址,所有其他 Pod 都能看到该 IP 地址下的 Pod。

因此,Pod 之间的通信总是简单的。无论两个 Pod 是否被调度到同一个或不同的工作节点上,在这两种情况下,Pod 内部的容器都可以通过扁平的、无 NAT 的网络相互通信,就像局域网(LAN)中的计算机一样,无论实际的节点间网络拓扑结构如何。就像局域网中的计算机一样,每个 Pod 都有自己的 IP 地址,并且可以通过为 Pod 专门建立的这一网络从所有其他 Pod 访问。这通常是通过在真实网络之上添加一个额外的软件定义网络来实现的。
总结本节所涵盖的内容:Pod 是逻辑主机,在非容器世界中表现得就像物理主机或虚拟机。在同一 Pod 中运行的进程就像在同一个物理或虚拟机上运行的进程一样,只是每个进程都被封装在一个容器中。
3.1.3. 正确组织 Pod 间的容器
你应该将 Pod 视为独立的机器,但每个机器只托管特定的应用程序。与过去我们经常将各种应用程序挤在同一台主机上的做法不同,我们不会这样做 Pod。因为 Pod 相对较轻量级,你可以拥有你需要的任意多个,而几乎不会产生任何开销。而不是将所有内容都塞入一个 Pod 中,你应该将应用程序组织到多个 Pod 中,其中每个 Pod 只包含紧密相关的组件或进程。
话虽如此,你认为一个由前端应用服务器和后端数据库组成的多层应用程序应该配置为一个 Pod 还是两个 Pod?
将多层应用程序拆分为多个 Pod
虽然没有阻止你在单个 Pod 中使用两个容器同时运行前端服务器和数据库,但这并不是最合适的方式。我们说过,同一个 Pod 中的所有容器总是运行在同一个位置,但网页服务器和数据库真的需要运行在同一台机器上吗?答案显然是否定的,所以你不希望将它们放入同一个 Pod 中。但这样做是否错误呢?从某种意义上说,是的。
如果前端和后端都在同一个 Pod 中,那么它们将始终在同一个机器上运行。如果你有一个两节点 Kubernetes 集群,并且只有一个这个单独的 Pod,那么你将只使用一个工作节点,而不会利用你在第二个节点上可用的计算资源(CPU 和内存)。将 Pod 拆分为两个将允许 Kubernetes 将前端调度到一个节点,而后端调度到另一个节点,从而提高你的基础设施利用率。
将其拆分为多个 Pod 以实现单独扩展
不应该将它们都放入同一个 Pod 的另一个原因是扩展。Pod 也是扩展的基本单位。Kubernetes 无法水平扩展单个容器;相反,它扩展整个 Pod。如果你的 Pod 由前端和后端容器组成,当你将 Pod 实例的数量扩展到,比如说两个时,你最终会得到两个前端容器和两个后端容器。
通常,前端组件的扩展需求与后端完全不同,所以我们倾向于单独扩展它们。更不用说后端(如数据库)通常比(无状态的)前端 Web 服务器更难扩展。如果你需要单独扩展容器,这清楚地表明它需要部署在单独的 Pod 中。
理解何时在 Pod 中使用多个容器
将多个容器放入单个 Pod 的主要原因是当应用程序由一个主进程和一个或多个补充进程组成时,如图 3.3 所示。
图 3.3. Pod 应该包含紧密耦合的容器,通常是一个主容器和支撑主容器的容器。

例如,Pod 中的主容器可能是一个从特定文件目录提供文件的 Web 服务器,而一个附加容器(边车容器)会定期从外部源下载内容并将其存储在 Web 服务器的目录中。在第六章中,你会看到你需要使用一个 Kubernetes 卷,并将其挂载到两个容器中。
边车容器的其他例子包括日志轮换器和收集器、数据处理程序、通信适配器等。
决定何时在 Pod 中使用多个容器
为了回顾容器应该如何分组到 Pod 中——在决定是否将两个容器放入同一个 Pod 还是两个独立的 Pod 中时,你总是需要问自己以下问题:
-
它们是否需要一起运行,或者是否可以在不同的主机上运行?
-
它们是否代表一个整体或独立的组件?
-
它们是否需要一起扩展或单独扩展?
基本上,你应该始终倾向于在单独的 Pod 中运行容器,除非有特定的原因要求它们成为同一个 Pod 的一部分。图 3.4 将帮助你记住这一点。
图 3.4. 容器不应该运行多个进程。如果不需要在同一个机器上运行,pod 不应包含多个容器。

尽管 pods 可以包含多个容器,但为了保持简单,你将在本章中只处理单容器 pod。你将在第六章(index_split_055.html#filepos588298)中看到如何在同一个 pod 中使用多个容器。
3.2. 从 YAML 或 JSON 描述符创建 pod
Pods 和其他 Kubernetes 资源通常是通过向 Kubernetes REST API 端点发送 JSON 或 YAML 清单来创建的。此外,你也可以使用其他更简单的方式来创建资源,例如你在上一章中使用的 kubectl run 命令,但它们通常只允许你配置一组有限的属性,而不是全部。此外,将所有 Kubernetes 对象定义在 YAML 文件中,使得可以将它们存储在版本控制系统(VCS)中,从而带来所有这些好处。
为了配置每种类型资源的所有方面,你需要了解和理解 Kubernetes API 对象定义。随着你在本书中学习每种资源类型,你将了解其中大部分。我们不会解释每个属性,因此当创建对象时,你也应参考 Kubernetes API 参考文档 kubernetes.io/docs/reference/。
3.2.1. 检查现有 pod 的 YAML 描述符
你已经在上一章创建了一些现有的 pod,所以让我们看看其中一个 pod 的 YAML 定义是什么样的。你可以使用带有 -o yaml 选项的 kubectl get 命令来获取 pod 的完整 YAML 定义,如下所示。
列表 3.1. 已部署 pod 的完整 YAML
$ kubectl get po kubia-zxzij -o yaml apiVersion: v1 1 kind: Pod 2 metadata: 3 annotations: 3 kubernetes.io/created-by: ... 3 creationTimestamp: 2016-03-18T12:37:50Z 3 generateName: kubia- 3 labels: 3 run: kubia 3 name: kubia-zxzij 3 namespace: default 3 resourceVersion: "294" 3 selfLink: /api/v1/namespaces/default/pods/kubia-zxzij 3 uid: 3a564dc0-ed06-11e5-ba3b-42010af00004 3 spec: 4 containers: 4 - image: luksa/kubia 4 imagePullPolicy: IfNotPresent 4 name: kubia 4 ports: 4 - containerPort: 8080 4 protocol: TCP 4 resources: 4 requests: 4 cpu: 100m 4 terminationMessagePath: /dev/termination-log 4 volumeMounts: 4 - mountPath: /var/run/secrets/k8s.io/servacc 4 name: default-token-kvcqa 4 readOnly: true 4 dnsPolicy: ClusterFirst 4 nodeName: gke-kubia-e8fe08b8-node-txje 4 restartPolicy: Always 4 serviceAccount: default 4 serviceAccountName: default 4 terminationGracePeriodSeconds: 30 4 volumes: 4 - name: default-token-kvcqa 4 secret: 4 secretName: default-token-kvcqa 4 status: 5 conditions: 5 - lastProbeTime: null 5 lastTransitionTime: null 5 status: "True" 5 type: Ready 5 containerStatuses: 5 - containerID: docker://f0276994322d247ba... 5 image: luksa/kubia 5 imageID: docker://4c325bcc6b40c110226b89fe... 5 lastState: {} 5 name: kubia 5 ready: true 5 restartCount: 0 5 state: 5 running: 5 startedAt: 2016-03-18T12:46:05Z 5 hostIP: 10.132.0.4 5 phase: Running 5 podIP: 10.0.2.3 5 startTime: 2016-03-18T12:44:32Z 5
-
1 在此 YAML 描述符中使用的 Kubernetes API 版本
-
2 Kubernetes 对象/资源的类型
-
3 Pod 元数据(名称、标签、注解等)
-
4 Pod 规范/内容(Pod 的容器、卷等的列表)
-
5 Pod 及其容器的详细状态
我知道这看起来很复杂,但一旦你了解了基础知识并且知道如何区分重要部分和细节,它就会变得简单。此外,你可以放心,当你创建一个新的 Pod 时,你需要编写的 YAML 文件会短得多,就像你稍后将会看到的那样。
介绍 Pod 定义的主要部分
Pod 定义由几个部分组成。首先,是 YAML 中使用的 Kubernetes API 版本以及 YAML 描述的资源类型。然后,在几乎所有的 Kubernetes 资源中都可以找到三个重要的部分:
-
元数据包括名称、命名空间、标签以及其他关于 Pod 的信息。
-
Spec 包含 Pod 内容的实际描述,例如 Pod 的容器、卷和其他数据。
-
状态包含关于运行 Pod 的当前信息,例如 Pod 的状态、每个容器的描述和状态,以及 Pod 的内部 IP 和其他基本信息。
列表 3.1 展示了一个正在运行的 pod 的完整描述,包括其状态。status 部分包含只读的运行时数据,显示了资源在某一时刻的状态。在创建新的 pod 时,您永远不需要提供 status 部分。
之前描述的三个部分展示了 Kubernetes API 对象的典型结构。正如您将在本书的其余部分看到的那样,所有其他对象都具有相同的结构。这使得理解新对象相对容易。
在之前的 YAML 中逐个检查所有单个属性没有太多意义,所以,让我们看看创建 pod 的最基本 YAML 看起来是什么样子。
3.2.2. 创建一个简单的 YAML 描述符用于 pod
您将创建一个名为 kubia-manual.yaml 的文件(您可以在任何目录中创建它),或者下载本书的代码存档,您将在 Chapter03 目录中找到该文件。以下列表显示了文件的全部内容。
列表 3.2. 基本 pod 清单:kubia-manual.yaml
apiVersion: v1 1 kind: Pod 2 metadata: name: kubia-manual 3 spec: containers: - image: luksa/kubia 4 name: kubia 5 ports: - containerPort: 8080 6 protocol: TCP
-
1 描述符符合 Kubernetes API 的 v1 版本
-
2 您正在描述一个 pod。
-
3 pod 的名称
-
4 从容器创建的容器镜像
-
5 容器的名称
-
6 应用程序监听的端口
我相信您会同意这比 列表 3.1 中的定义要简单得多。让我们详细检查这个描述符。它符合 Kubernetes API 的 v1 版本。您所描述的资源类型是 pod,名称为 kubia-manual。该 pod 由基于 luksa/kubia 镜像的单个容器组成。您还为容器命名,并指明它正在监听端口 8080。
指定容器端口
在 pod 定义中指定端口纯粹是信息性的。省略它们不会影响客户端是否可以通过端口连接到 pod。如果容器通过绑定到 0.0.0.0 地址的端口接受连接,其他 pod 总是能够连接到它,即使该端口没有在 pod 规范中明确列出。但是,明确定义端口是有意义的,这样每个人都可以快速看到每个 pod 公开的端口。明确定义端口还允许您为每个端口分配一个名称,这在本书后面的内容中会很有用。
使用 kubectl explain 来发现可能的 API 对象字段
在准备清单时,您可以选择查阅 Kubernetes 参考文档 kubernetes.io/docs/api,以查看每个 API 对象支持哪些属性,或者您可以使用 kubectl explain 命令。
例如,当从头开始创建 pod 清单时,您可以首先让 kubectl 解释 pods:
$ kubectl explain pods 描述:Pod 是一组可以在主机上运行的容器。该资源由客户端创建并调度到主机上。 字段: kind <字符串> 类型是一个字符串值,表示此对象表示的 REST 资源... metadata <对象> 标准对象元数据... spec <对象> pod 的期望行为规范... status <对象> pod 最近观察到的状态。这些数据可能不是最新的...
Kubectl 打印出对象的说明并列出对象可以包含的属性。然后你可以深入了解以了解更多关于每个属性的信息。例如,你可以像这样检查spec属性:
$ kubectl explain pod.spec 资源:spec <对象> 描述: pod 的期望行为规范... podSpec 是 pod 的描述。 字段: hostPID <布尔型> 使用主机的 pid 命名空间。可选:默认为 false。 ... volumes <[]对象> pod 中容器可以挂载的卷列表。 Containers <[]对象> -必需- pod 中属于容器的列表。容器目前不能添加或删除。pod 中必须至少有一个容器。 不能更新。更多信息: http://releases.k8s.io/release-1.4/docs/user-guide/containers.md
3.2.3. 使用 kubectl create 创建 pod
要从你的 YAML 文件创建 pod,请使用kubectl create命令:
$ kubectl create -f kubia-manual.yaml pod "kubia-manual" 已创建
kubectl create -f 命令用于从 YAML 或 JSON 文件创建任何资源(不仅仅是 pod)。
获取正在运行的 pod 的整个定义
在创建 pod 之后,你可以向 Kubernetes 请求 pod 的完整 YAML。你会看到它与之前看到的 YAML 类似。你将在下一节中了解返回定义中出现的附加字段。请使用以下命令查看 pod 的完整描述符:
$ kubectl get po kubia-manual -o yaml
如果你更喜欢 JSON,你也可以让kubectl返回 JSON 而不是 YAML,如下所示(即使你使用 YAML 创建了 pod,这也适用):
$ kubectl get po kubia-manual -o json
在 pod 列表中查看你新创建的 pod
你的 pod 已经创建,但你怎么知道它在运行呢?让我们列出 pod 以查看它们的状态:
$ kubectl get pods 名称 就绪 状态 重启次数 年龄 kubia-manual 1/1 运行中 0 32 秒 kubia-zxzij 1/1 运行中 0 1 天
这就是你的kubia-manual pod。其状态显示它正在运行。如果你像我一样,你可能想通过与 pod 通信来确认这一点。你将在下一分钟这样做。首先,你将查看应用程序日志以检查是否有任何错误。
3.2.4. 查看应用程序日志
你小巧的 Node.js 应用程序将日志记录到进程的标准输出。容器化应用程序通常将日志记录到标准输出和标准错误流,而不是将日志写入文件。这是为了让用户能够以简单、标准的方式查看不同应用程序的日志。
容器运行时(在你的情况下是 Docker)将这些流重定向到文件,并允许你通过运行以下命令来获取容器的日志
$ docker logs <容器 ID>
你可以使用ssh登录到你的 Pod 运行的节点,并使用docker logs检索其日志,但 Kubernetes 提供了一个更简单的方法。
使用 kubectl logs 检索 Pod 的日志
要查看你的 Pod 日志(更准确地说,是容器的日志),你需要在本地机器上运行以下命令(无需ssh到任何地方):
$ kubectl logs kubia-manual Kubia 服务器启动...
你还没有向你的 Node.js 应用程序发送任何 Web 请求,所以日志只显示一条关于服务器启动的单个日志语句。正如你所见,如果 Pod 只包含一个容器,那么在 Kubernetes 中检索运行的应用程序的日志非常简单。
注意
容器日志会自动每天轮换,并且每当日志文件达到 10MB 大小时也会轮换。kubectl logs命令只显示上一次轮换后的日志条目。
在获取多容器 Pod 的日志时指定容器名称
如果你的 Pod 包含多个容器,你必须在使用kubectl logs时显式指定容器名称,包括-c <容器名称>选项。在你的kubia-manual Pod 中,你将容器的名称设置为kubia,所以如果 Pod 中存在其他容器,你必须像这样获取其日志:
$ kubectl logs kubia-manual -c kubia Kubia 服务器启动...
注意,你只能检索仍然存在的 Pod 的容器日志。当一个 Pod 被删除时,其日志也会被删除。为了在 Pod 删除后仍然可以访问 Pod 的日志,你需要设置集中式、集群范围内的日志记录,将所有日志存储到中央存储中。第十七章解释了集中式日志记录是如何工作的。
3.2.5. 向 Pod 发送请求
Pod 现在正在运行——至少kubectl get和你的应用程序日志是这样说的。但你是如何看到它在实际中的运行情况的?在前一章中,你使用了kubectl expose命令来创建一个服务以外部访问 Pod。现在你不会这样做,因为整章都致力于服务,而且你有其他方法连接到 Pod 进行测试和调试。其中之一是通过端口转发。
将本地网络端口转发到 Pod 中的端口
当你想与特定的 Pod 通信而不通过服务(用于调试或其他原因)时,Kubernetes 允许你配置到 Pod 的端口转发。这是通过kubectl port-forward命令完成的。以下命令将你的机器的本地端口8888转发到kubia-manual Pod 的端口8080:
$ kubectl port-forward kubia-manual 8888:8080 ... 正在转发从 127.0.0.1:8888 到 8080 ... 正在转发从 [::1]:8888 到 8080
端口转发器正在运行,你现在可以通过本地端口连接到你的 Pod。
通过端口转发器连接到 Pod
在不同的终端中,你现在可以使用curl通过运行在localhost:8888上的kubectl port-forward代理向你的 Pod 发送 HTTP 请求:
$ curl localhost:8888 您已访问 kubia-manual
图 3.5 显示了一个过于简化的视图,展示了发送请求时会发生什么。实际上,在kubectl进程和 Pod 之间有几个额外的组件,但它们现在并不相关。
图 3.5. 使用kubectl port-forward与 curl 一起使用时的简化视图

使用这种方式进行端口转发是测试单个 Pod 的有效方法。你将在本书的其余部分了解其他类似的方法。
3.3. 组织带有标签的 Pod
到目前为止,你的集群中已经运行了两个 Pod。在部署实际应用程序时,大多数用户最终会运行更多的 Pod。随着 Pod 数量的增加,将它们分类到子集的需求变得越来越明显。
例如,在微服务架构中,部署的微服务数量很容易超过 20 个或更多。这些组件可能会被复制(将部署相同组件的多个副本)并且会同时运行多个版本或发布(稳定版、测试版、金丝雀版等)。这可能导致系统中出现数百个 Pod。如果没有组织它们的机制,你最终会得到一个庞大且难以理解的一团糟,就像图 3.6 中所示的那样。该图显示了多个微服务的 Pod,其中一些运行多个副本,而其他则运行同一微服务的不同版本。
图 3.6. 微服务架构中的未分类 Pod

显然,你需要一种方法将它们根据任意标准组织成更小的组,这样每个处理你系统的开发人员和系统管理员都可以轻松地看到哪个 Pod 是哪个。你还将希望对属于某个组的每个 Pod 执行单个操作,而不是对每个 Pod 单独执行操作。
组织 Pod 和所有其他 Kubernetes 对象是通过标签完成的。
3.3.1. 介绍标签
标签是 Kubernetes 中一个简单而又极其强大的功能,用于组织不仅限于 Pod,还包括所有其他 Kubernetes 资源。标签是你附加到资源上的任意键值对,然后在使用标签选择器选择资源时被利用(资源根据是否包含选择器中指定的标签进行过滤)。一个资源可以有多个标签,只要这些标签的键在该资源内是唯一的。你通常在创建资源时附加标签,但也可以稍后添加额外的标签或甚至修改现有标签的值,而无需重新创建资源。
让我们回到图 3.6 中的微服务示例。通过向这些 Pod 添加标签,你将得到一个组织得更好、大家都能轻松理解的系统。每个 Pod 都被标记为两个标签:
-
app,它指定 Pod 属于哪个应用程序、组件或微服务。 -
rel,它显示 Pod 中运行的应用程序是稳定版、测试版还是金丝雀发布。
定义
金丝雀发布是指你在稳定版本旁边部署应用程序的新版本,并且只让一小部分用户访问新版本,以观察其行为,然后再将其推广给所有用户。这可以防止不良发布版本暴露给太多用户。
通过添加这两个标签,你实际上已经将你的 Pod 组织成两个维度(水平方向按应用程序,垂直方向按发布),如图 3.7 所示。
图 3.7. 使用 Pod 标签组织微服务架构中的 Pod

现在每个有权访问你的集群的开发者或运维人员都可以通过查看 Pod 的标签,轻松地看到系统的结构和每个 Pod 的位置。
3.3.2. 在创建 Pod 时指定标签
现在,你将通过创建一个带有两个标签的新 Pod 来看到标签的实际应用。创建一个名为 kubia-manual-with-labels.yaml 的新文件,并包含以下内容的列表。
列表 3.3. 带有标签的 Pod:kubia-manual-with-labels.yaml
apiVersion: v1 kind: Pod metadata: name: kubia-manual-v2 labels: creation_method: manual 1 env: prod 1 spec: containers: - image: luksa/kubia name: kubia ports: - containerPort: 8080 protocol: TCP
- 1 附加了两个标签到 Pod 上。
你已经包含了 creation_method=manual 和 env=data.labels 部分。你现在将创建这个 Pod:
$ kubectl create -f kubia-manual-with-labels.yaml pod "kubia-manual-v2" created
默认情况下,kubectl get pods 命令不会列出任何标签,但你可以通过使用 --show-labels 开关来查看它们:
$ kubectl get po --show-labels NAME READY STATUS RESTARTS AGE LABELS kubia-manual 1/1 Running 0 16m <none> kubia-manual-v2 1/1 Running 0 2m creat_method=manual,env=prod kubia-zxzij 1/1 Running 0 1d run=kubia
如果您只对某些标签感兴趣,而不是列出所有标签,您可以使用 -L 开关指定它们,并将每个标签单独显示在其自己的列中。再次列出 pods 并显示您附加到 kubia-manual-v2 pod 的两个标签的列:
$ kubectl get po -L creation_method,env NAME READY STATUS RESTARTS AGE CREATION_METHOD ENV kubia-manual 1/1 Running 0 16m <none> <none> kubia-manual-v2 1/1 Running 0 2m manual prod kubia-zxzij 1/1 Running 0 1d <none> <none>
3.3.3. 修改现有 pods 的标签
标签还可以添加到现有 pods 上并对其进行修改。因为 kubia-manual pod 也是手动创建的,所以让我们向它添加 creation_method=manual 标签:
$ kubectl label po kubia-manual creation_method=manual pod "kubia-manual" labeled
现在,让我们也将 kubia-manual-v2 pod 上的 env=prod 标签更改为 env=debug,以查看现有标签如何更改。
注意
更改现有标签时,需要使用 --overwrite 选项。
$ kubectl label po kubia-manual-v2 env=debug --overwrite pod "kubia-manual-v2" labeled
再次列出 pods 以查看更新的标签:
$ kubectl get po -L creation_method,env NAME READY STATUS RESTARTS AGE CREATION_METHOD ENV kubia-manual 1/1 Running 0 16m manual``<none> kubia-manual-v2 1/1 Running 0 2m manual debug kubia-zxzij 1/1 Running 0 1d <none> <none>
如您所见,将标签附加到资源是微不足道的,更改现有资源上的标签也是如此。现在可能还不明显,但这是一个非常强大的功能,您将在下一章中看到。但首先,让我们看看除了在列出 pods 时显示它们之外,您还可以用这些标签做什么。
3.4. 通过标签选择器列出 pods 的子集
将标签附加到资源以便在列出时可以看到每个资源旁边的标签并不那么有趣。但标签与标签选择器密不可分。标签选择器允许您选择带有特定标签的 pod 的子集,并对这些 pod 执行操作。标签选择器是一个标准,它根据资源是否包含具有特定值的特定标签来过滤资源。
标签选择器可以根据资源是否包含具有特定值的特定标签来选择资源
-
包含(或不包含)具有特定键的标签
-
包含具有特定键和值的标签
-
包含具有特定键但值不等于您指定的标签
3.4.1. 使用标签选择器列出 pods
让我们使用标签选择器来查看您迄今为止创建的 pods。要查看您手动创建的所有 pods(您用 creation_method=manual 标记了它们),请执行以下操作:
$ kubectl get po -l creation_method=manual NAME READY STATUS RESTARTS AGE kubia-manual 1/1 运行中 0 51 分钟 kubia-manual-v2 1/1 运行中 0 37 分钟
要列出所有包含env标签的 Pod,无论其值是什么:
$ kubectl get po -l env NAME READY STATUS RESTARTS AGE kubia-manual-v2 1/1 运行中 0 37 分钟
以及那些没有env标签的 Pod:
$ kubectl get po -l '!env' NAME READY STATUS RESTARTS AGE kubia-manual 1/1 运行中 0 51 分钟 kubia-zxzij 1/1 运行中 0 10 天
注意
请确保在!env周围使用单引号,这样 bash shell 就不会评估感叹号。
类似地,你也可以使用以下标签选择器来匹配 Pod:
-
creation_method!=manual用于选择标签creation_method设置为除manual之外任何值的 Pod -
env in (prod,devel)用于选择标签env设置为prod或devel的 Pod -
env notin (prod,devel)用于选择标签env设置为除prod或devel之外任何值的 Pod
回到面向微服务的架构示例中的 Pod,你可以通过使用app=pc标签选择器(如图所示)来选择所有属于产品目录微服务的 Pod。
图 3.8. 使用“app=pc”标签选择器选择产品目录微服务的 Pod

3.4.2. 在标签选择器中使用多个条件
选择器也可以包含多个以逗号分隔的标准。资源需要匹配所有这些标准才能匹配选择器。例如,如果你想选择仅运行产品目录微服务 beta 版本的 Pod,你会使用以下选择器:app=pc,rel=beta(如图 3.9 所示)。
图 3.9. 使用多个标签选择器选择 Pod

标签选择器不仅对列出 Pod 有用,还可以用于对所有 Pod 的子集执行操作。例如,在本章的后面,你会看到如何使用标签选择器一次性删除多个 Pod。但标签选择器不仅被kubectl使用,它们也被内部使用,你将在下面看到。
3.5. 使用标签和选择器来约束 Pod 调度
你迄今为止创建的所有 Pod 都已被随机调度到你的工作节点上。正如我在上一章提到的,这是在 Kubernetes 集群中工作的正确方式。因为 Kubernetes 将集群中的所有节点暴露为一个单一的、大型的部署平台,所以你不需要关心 Pod 被调度到哪个节点。因为每个 Pod 都获得了它请求的确切计算资源(CPU、内存等),并且它从其他 Pod 的访问性并不受 Pod 被调度到的节点的影响,通常你不需要告诉 Kubernetes 确切地在哪里调度你的 Pod。
然而,在某些情况下,你可能希望至少对 Pod 应该调度到哪个位置有少许发言权。一个很好的例子是当你的硬件基础设施不均匀时。如果你的部分工作节点有旋转硬盘,而其他节点有 SSD,你可能希望将某些 Pod 调度到一组节点,其余的调度到另一组。另一个例子是当你需要将执行基于 GPU 的密集计算的 Pod 调度到提供所需 GPU 加速的节点上。
你永远不希望明确指出 Pod 应该调度到哪个节点,因为这会将应用程序与基础设施耦合在一起,而 Kubernetes 整个理念是隐藏实际基础设施,使其对在其上运行的应用程序不可见。但如果你想对 Pod 应该调度到哪个位置有发言权,而不是指定一个确切的节点,你应该描述节点需求,然后让 Kubernetes 选择一个符合这些需求的节点。这可以通过节点标签和节点标签选择器来实现。
3.5.1. 使用标签对工作节点进行分类
如你之前所学的,Pod 不是唯一可以附加标签的 Kubernetes 资源类型。标签可以附加到任何 Kubernetes 对象,包括节点。通常,当运维团队向集群添加新节点时,他们会通过附加标签来对节点进行分类,这些标签指定了节点提供的硬件类型或其他可能在调度 Pod 时有用的任何内容。
让我们假设你的集群中的一个节点包含一个用于通用 GPU 计算的 GPU。你想要给这个节点添加一个标签来显示这个特性。你将向你的一个节点添加标签 gpu=true(从 kubectl get nodes 返回的列表中选择一个):
$ kubectl label node gke-kubia-85f6-node-0rrx gpu=true node "gke-kubia-85f6-node-0rrx" labeled
现在,你可以在列出节点时使用标签选择器,就像之前列出 Pod 时所做的那样。仅列出包含标签 gpu=true 的节点:
$ kubectl get nodes -l gpu=true NAME STATUS AGE gke-kubia-85f6-node-0rrx Ready 1d
如预期的那样,只有一个节点有这个标签。你也可以尝试列出所有节点,并告诉 kubectl 显示每个节点的 gpu 标签值(kubectl get nodes -L gpu)。
3.5.2. 将 Pod 调度到特定节点
现在假设你想要部署一个新的 Pod,该 Pod 需要 GPU 来执行其工作。为了要求调度器只从提供 GPU 的节点中选择,你将在 Pod 的 YAML 中添加一个节点选择器。创建一个名为 kubia-gpu.yaml 的文件,包含以下内容的列表,然后使用 kubectl create -f kubia-gpu.yaml 来创建 Pod。
列表 3.4. 使用标签选择器将 Pod 调度到特定节点:kubia-gpu.yaml
apiVersion: v1 kind: Pod metadata: name: kubia-gpu spec: nodeSelector: 1 gpu: "true" 1 containers: - image: luksa/kubia name: kubia
- 1 个
nodeSelector告诉 Kubernetes 只将此 Pod 部署到包含gpu=true标签的节点。
您在spec部分下添加了一个nodeSelector字段。当您创建 Pod 时,调度器将只从包含gpu=true标签的节点中选择(在这种情况下,只有一个节点)。
3.5.3. 将调度指定到特定节点
类似地,您也可以将 Pod 调度到特定的节点,因为每个节点都有一个唯一的标签,键为kubernetes.io/hostname,值为节点的实际主机名。但通过主机名标签将nodeSelector设置为特定节点可能会导致节点离线时 Pod 不可调度。您不应该从单个节点的角度思考。始终考虑满足通过标签选择器指定的某些标准节点的逻辑组。
这只是一个快速演示,说明了标签和标签选择器的工作原理以及如何使用它们来影响 Kubernetes 的操作。在下一章讨论 Replication-Controllers 和 Services 时,标签选择器的重要性和实用性将变得更加明显。
注意
影响 Pod 调度到哪个节点的方法的其他方式在第十六章中介绍。
3.6. 注解 Pod
除了标签外,Pod 和其他对象还可以包含注解。注解也是键值对,因此本质上与标签相似,但它们不是为了持有标识信息而设计的。它们不能像标签那样用于分组对象。虽然可以通过标签选择器选择对象,但不存在注解选择器。
另一方面,注解可以包含大量信息,主要用于工具使用。某些注解是由 Kubernetes 自动添加到对象中的,但其他注解则是由用户手动添加的。
在引入 Kubernetes 的新功能时,注解也经常被使用。通常,新功能的 alpha 和 beta 版本不会向 API 对象引入任何新字段。注解代替字段使用,一旦所需的 API 更改变得明确并被 Kubernetes 开发者同意,就会引入新字段,并弃用相关的注解。
使用注解的一个很好的用途是为每个 Pod 或其他 API 对象添加描述,这样集群中的每个人都可以快速查找有关每个单独对象的信息。例如,用于指定创建对象的人名的注解可以使在集群上工作的每个人的协作变得更加容易。
3.6.1. 查找对象的注解
让我们看看 Kubernetes 自动添加到上一章创建的 Pod 中的注解示例。要查看注解,您需要请求 Pod 的完整 YAML 文件或使用kubectl describe命令。以下列表中将使用第一种方法。
列表 3.5. Pod 的注解
$ kubectl get po kubia-zxzij -o yaml apiVersion: v1 kind: pod metadata: annotations: kubernetes.io/created-by: | {"kind":"SerializedReference", "apiVersion":"v1", "reference":{"kind":"ReplicationController", "namespace":"default", ...
不深入细节的话,如您所见,kubernetes.io/created-by 注解包含有关创建容器的对象的 JSON 数据。这不是您想放入标签中的内容。标签应该是简短的,而注解可以包含相对较大的数据块(总大小最多为 256 KB)。
注意
kubernetes.io/created-by 注解在版本 1.8 中已被弃用,并在 1.9 中将被移除,因此您在 YAML 中将不再看到它。
3.6.2. 添加和修改注解
注解显然可以在创建容器时添加,就像标签一样。它们也可以在现有的容器中添加或修改。向现有对象添加注解的最简单方法是使用 kubectl annotate 命令。
现在您将尝试向您的 kubia-manual 容器添加一个注解:
$ kubectl annotate pod kubia-manual mycompany.com/someannotation="foo bar" pod "kubia-manual" annotated
您已添加注解 mycompany.com/someannotation,其值为 foo bar。使用这种格式作为注解键是一个好主意,以防止键冲突。当不同的工具或库向对象添加注解时,如果它们没有使用像您在这里使用的唯一前缀,它们可能会意外地覆盖彼此的注解。
您可以使用 kubectl describe 来查看您添加的注解:
$ kubectl describe pod kubia-manual ... Annotations: mycompany.com/someannotation=foo bar ...
3.7. 使用命名空间来分组资源
让我们暂时回到标签。我们已经看到它们如何将容器和其他对象组织成组。因为每个对象可以有多个标签,这些对象组可能会重叠。此外,当通过 kubectl 等工具与集群交互时,如果您没有明确指定标签选择器,您将始终看到所有对象。
但有时您可能想要将对象分成单独的、不重叠的组?您可能只想一次操作一个组。出于这些和其他原因,Kubernetes 还将对象分组到命名空间中。这些不是我们在第二章中讨论的 Linux 命名空间,它们用于隔离进程。Kubernetes 命名空间为对象名称提供了作用域。您可以将所有资源放在一个命名空间中,也可以将它们分成多个命名空间,这还允许您在不同命名空间中使用相同的资源名称多次。
3.7.1. 理解命名空间的需求
使用多个命名空间允许你将具有许多组件的复杂系统分割成更小的独立组。它们也可以用于在多租户环境中分离资源,将资源分割成生产、开发和 QA 环境,或者以任何其他你可能需要的方式。资源名称只需要在命名空间内是唯一的。两个不同的命名空间可以包含具有相同名称的资源。但是,尽管大多数类型的资源都是命名空间化的,但有一些资源不是。其中之一是 Node 资源,它是全局的,不与单个命名空间绑定。你将在后面的章节中了解其他集群级资源。
现在我们来看看如何使用命名空间。
3.7.2. 发现其他命名空间及其 Pod
首先,让我们列出你集群中的所有命名空间:
$ kubectl get ns NAME LABELS STATUS AGE default <none> Active 1h kube-public <none> Active 1h kube-system <none> Active 1h
到目前为止,你只在使用default命名空间。当使用kubectl get命令列出资源时,你从未明确指定命名空间,所以kubectl总是默认使用default命名空间,只显示该命名空间中的对象。但是,正如你所看到的列表,kube-public和kube-system命名空间也存在。让我们通过告诉kubectl只在该命名空间中列出 Pod 来查看属于kube-system命名空间的 Pod:
$ kubectl get po --namespace kube-system NAME READY STATUS RESTARTS AGE fluentd-cloud-kubia-e8fe-node-txje 1/1 Running 0 1h heapster-v11-fz1ge 1/1 Running 0 1h kube-dns-v9-p8a4t 0/4 Pending 0 1h kube-ui-v4-kdlai 1/1 Running 0 1h l7-lb-controller-v0.5.2-bue96 2/2 Running 92 1h
小贴士
你也可以使用-n代替--namespace。
你将在本书的后面部分了解这些 Pod(如果这里显示的 Pod 与你的系统中的 Pod 不完全匹配,请不要担心)。从命名空间的名字可以看出,这些是与 Kubernetes 系统本身相关的资源。通过将它们放在这个独立的命名空间中,可以保持一切井然有序。如果它们都在默认命名空间中,与你自己创建的资源混合在一起,你将很难看到哪些属于哪里,你可能会意外删除系统资源。
命名空间允许你将不属于一起的资源分离成不重叠的组。如果多个用户或用户组正在使用同一个 Kubernetes 集群,并且他们各自管理自己独特的一组资源,那么他们应该各自使用自己的命名空间。这样,他们不需要特别注意不要无意中修改或删除其他用户的资源,也不需要担心名称冲突,因为命名空间为资源名称提供了范围,正如之前提到的。
除了隔离资源外,命名空间还用于仅允许某些用户访问特定的资源,甚至可以限制分配给单个用户的计算资源量。你将在第十二章到 14 章中了解这一点。
3.7.3. 创建命名空间
命名空间就像 Kubernetes 中的任何其他资源一样,因此你可以通过向 Kubernetes API 服务器提交 YAML 文件来创建它。现在让我们看看如何做。
从 YAML 文件创建命名空间
首先,创建一个包含以下内容的 custom-namespace.yaml 文件(你可以在本书的代码存档中找到该文件)。
列表 3.6. 命名空间的 YAML 定义:custom-namespace.yaml
apiVersion: v1 kind: Namespace 1 metadata: name: custom-namespace 2
-
1 这表示你正在定义一个命名空间。
-
2 这是命名空间的名字。
现在,使用 kubectl 将文件提交到 Kubernetes API 服务器:
$ kubectl create -f custom-namespace.yaml namespace "custom-namespace" created
使用 kubectl create namespace 创建命名空间
虽然编写像之前的文件这样的内容并不是什么大事,但仍然有些麻烦。幸运的是,你还可以使用专门的 kubectl create namespace 命令来创建命名空间,这比编写 YAML 文件要快。通过让你为命名空间创建 YAML 清单,我想强调在 Kubernetes 中,所有内容都有一个相应的 API 对象,你可以通过向 API 服务器提交 YAML 清单来创建、读取、更新和删除。
你可以像这样创建命名空间:
$ kubectl create namespace custom-namespace namespace "custom-namespace" created
注意
尽管大多数对象的名称必须符合 RFC 1035(域名)中指定的命名约定,这意味着它们可能只包含字母、数字、破折号和点,但命名空间(以及其他一些对象)不允许包含点。
3.7.4. 管理其他命名空间中的对象
要在创建的资源中创建资源,你可以在 metadata 部分添加 namespace: custom-namespace 条目,或者在使用 kubectl create 命令创建资源时指定命名空间:
$ kubectl create -f kubia-manual.yaml -n custom-namespace pod "kubia-manual" created
你现在有两个具有相同名称(kubia-manual)的 pod。一个在 default 命名空间中,另一个在你的 custom-namespace 中。
当在其它命名空间中列出、描述、修改或删除对象时,您需要向 kubectl 命令传递 --namespace(或 -n)标志。如果您没有指定命名空间,kubectl 将在当前 kubectl 上下文中配置的默认命名空间中执行操作。当前上下文的命名空间和当前上下文本身可以通过 kubectl config 命令进行更改。要了解更多关于管理 kubectl 上下文的信息,请参阅附录 A。
提示
要快速切换到不同的命名空间,您可以设置以下别名:alias kcd='kubectl config set-context $(kubectl config current-context) --namespace '。然后您可以使用 kcd some-namespace 在命名空间之间切换。
3.7.5. 理解命名空间提供的隔离
为了总结关于命名空间的部分,让我解释一下命名空间不提供什么——至少不是默认提供的。尽管命名空间允许您将对象隔离到不同的组中,从而允许您仅操作指定命名空间中的对象,但它们不提供对运行对象的任何隔离。
例如,您可能会认为当不同用户在不同命名空间中部署 Pod 时,这些 Pod 之间是隔离的,并且不能相互通信,但这并不一定正确。命名空间是否提供网络隔离取决于与 Kubernetes 一起部署的网络解决方案。当解决方案不提供命名空间间的网络隔离时,如果 foo 命名空间中的 Pod 知道 bar 命名空间中 Pod 的 IP 地址,那么没有任何东西可以阻止它向另一个 Pod 发送流量,例如 HTTP 请求。
3.8. 停止和删除 Pod
您已创建了许多 Pod,它们都应该仍在运行。您在 default 命名空间中有四个 Pod,在 custom-namespace 中有一个 Pod。您现在将停止所有这些 Pod,因为您不再需要它们。
3.8.1. 通过名称删除 Pod
首先,通过名称删除 kubia-gpu Pod:
$ kubectl delete po kubia-gpu pod "kubia-gpu" 已删除
通过删除 Pod,您指示 Kubernetes 终止该 Pod 中所有容器的运行。Kubernetes 向进程发送 SIGTERM 信号并等待一定时间(默认为 30 秒)以优雅地关闭。如果它没有及时关闭,则通过 SIGKILL 杀死进程。要确保您的进程始终优雅地关闭,它们需要正确处理 SIGTERM 信号。
提示
您也可以通过指定多个以空格分隔的名称来删除多个 Pod(例如,kubectl delete po pod1 pod2)。
3.8.2. 使用标签选择器删除 Pod
与通过名称指定每个要删除的 Pod 相比,您现在将使用您所学的标签选择器来停止 kubia-manual 和 kubia-manual-v2 Pod。这两个 Pod 都包含 creation_method=manual 标签,因此您可以使用标签选择器来删除它们:
$ kubectl delete po -l creation_method=manual pod "kubia-manual" deleted pod "kubia-manual-v2" deleted
在早期的微服务示例中,您有数十(或可能数百)个 pods,例如,您可以通过指定rel=canary标签选择器(如图 3.10 所示)一次性删除所有金丝雀 pods:
$ kubectl delete po -l rel=canary
图 3.10. 通过rel=canary标签选择器选择和删除所有金丝雀 pods

3.8.3. 通过删除整个命名空间来删除 pods
好吧,回到您的真实 pods。关于custom-namespace中的 pods 怎么办?您不再需要该命名空间中的 pods,或者命名空间本身。您可以使用以下命令删除整个命名空间(pods 将随着命名空间的删除而自动删除):
$ kubectl delete ns custom-namespace namespace "custom-namespace" deleted
3.8.4. 删除命名空间中的所有 pods,同时保留命名空间
您现在几乎清理了一切。但是,关于您在第二章中使用kubectl run命令创建的 pods 怎么办?它仍在运行:
$ kubectl get pods NAME READY STATUS RESTARTS AGE kubia-zxzij 1/1 Running 0 1d
这次,不是删除特定的 pods,而是告诉 Kubernetes 使用--all选项删除当前命名空间中的所有 pods:
$ kubectl delete po --all pod "kubia-zxzij" deleted
现在,请再次确认没有 pods 仍在运行:
$ kubectl get pods NAME READY STATUS RESTARTS AGE kubia-09as0 1/1 Running 0 1d kubia-zxzij 1/1 Terminating 0 1d
等等!?!kubia-zxzij pods 正在终止,但一个名为kubia-09as0的新 pods 出现了,之前并不存在。无论您删除所有 pods 多少次,都会出现一个名为 kubia-something 的新 pods。
您可能记得您是使用kubectl run命令创建的第一个 pods。在第二章中,我提到这并不直接创建 pods,而是创建一个 ReplicationController,然后它再创建 pods。一旦您删除由 ReplicationController 创建的 pods,它立即创建一个新的。要删除 pods,您还需要删除 ReplicationController。
3.8.5. 删除命名空间中的(几乎)所有资源
您可以通过删除当前命名空间中的所有资源,使用单个命令来删除 ReplicationController、pods 以及您创建的所有服务:
$ kubectl delete all --all pod "kubia-09as0" deleted replicationcontroller "kubia" deleted service "kubernetes" deleted service "kubia-http" deleted
命令中的第一个 all 指定您正在删除所有类型的资源,而 --all 选项指定您正在删除所有资源实例,而不是按名称指定它们(您在运行上一个删除命令时已经使用了此选项)。
注意
使用 all 关键字删除所有内容并不会删除所有内容。某些资源(如我们在第七章中将要介绍的 Secrets)被保留,需要显式删除。
在删除资源时,kubectl 将打印出它删除的每个资源的名称。在列表中,您应该看到您在第二章中创建的 kubia ReplicationController 和 kubia-http 服务。
注意
kubectl delete all --all 命令也会删除 kubernetes 服务,但它应该会在几秒钟内自动重新创建。
3.9. 摘要
在阅读本章之后,您现在应该对 Kubernetes 的核心构建块有了一定的了解。您在接下来的几章中学习的每个其他概念都与 Pod 直接相关。
在本章中,您已经学习了
-
如何决定某些容器是否应该在一个 Pod 中分组在一起。
-
Pod 可以运行多个进程,在非容器世界中类似于物理主机。
-
YAML 或 JSON 描述符可以编写并用于创建 Pod,然后检查 Pod 的规范及其当前状态。
-
标签和标签选择器应用于组织 Pod 并轻松地对多个 Pod 同时执行操作。
-
您可以使用节点标签和选择器来调度仅具有特定功能的节点上的 Pod。
-
注释允许通过人员或工具和库将更大的数据块附加到 Pod 上。
-
命名空间可用于允许不同的团队使用同一个集群,就像他们使用单独的 Kubernetes 集群一样。
-
如何使用
kubectl explain命令快速查找任何 Kubernetes 资源的信息。
在下一章中,您将学习关于 ReplicationController 和其他管理 Pod 的资源。
第四章. 复制和其他控制器:部署托管 Pod
本章涵盖
-
保持 Pod 健康
-
运行相同 Pod 的多个实例
-
在节点失败后自动重新调度 Pod
-
横向扩展 Pod
-
在每个集群节点上运行系统级 Pod
-
运行批处理作业
-
定期或将来运行作业的调度
如你所知,Pod 是 Kubernetes 中的基本可部署单元。你知道如何手动创建、监督和管理它们。但在实际应用场景中,你希望你的部署能够自动保持运行状态,无需任何手动干预,并保持健康。为此,你几乎从不直接创建 Pod。相反,你创建其他类型的资源,例如 ReplicationControllers 或 Deployments,然后由它们创建和管理实际的 Pod。
当你创建未管理的 Pod(例如你在上一章中创建的 Pod)时,集群节点被选中来运行 Pod,然后在该节点上运行其容器。在本章中,你将了解到 Kubernetes 随后会监控这些容器,并在它们失败时自动重新启动它们。但如果整个节点失败,该节点上的 Pod 将丢失,并且不会用新的 Pod 替换,除非这些 Pod 由之前提到的 ReplicationControllers 或类似资源管理。在本章中,你将了解 Kubernetes 如何检查容器是否仍然存活,并在它不存活时重新启动它。你还将了解如何运行托管 Pod——包括那些无限期运行和那些执行单个任务后停止的 Pod。
4.1. 保持 Pod 健康
使用 Kubernetes 的主要好处之一是能够给它一个容器列表,并让它保持这些容器在集群中的某个位置运行。你通过创建 Pod 资源并让 Kubernetes 为它选择一个工作节点,并在该节点上运行 Pod 的容器来实现这一点。但如果其中一个容器死亡怎么办?如果一个 Pod 的所有容器都死亡了怎么办?
一旦一个 Pod 被调度到某个节点,该节点上的 Kubelet 将运行其容器,并且从那时起,只要 Pod 存在,就会保持它们运行。如果容器的主进程崩溃,Kubelet 将重新启动容器。如果你的应用程序有一个导致它偶尔崩溃的 bug,Kubernetes 会自动重新启动它,因此即使应用程序本身没有做任何特殊处理,在 Kubernetes 中运行应用程序也会自动赋予它自我修复的能力。
但有时应用程序停止工作,而其进程并未崩溃。例如,一个存在内存泄漏的 Java 应用程序将开始抛出 OutOfMemoryErrors,但 JVM 进程将继续运行。如果能有一种方法让应用程序向 Kubernetes 发出信号,表明它不再正常工作,并且让 Kubernetes 重新启动它,那就太好了。
我们已经说过,如果一个容器崩溃了,它会自动重启,所以你可能认为你可以在应用程序中捕获这些类型的错误,并在它们发生时退出进程。你当然可以这样做,但这仍然不能解决你所有的问题。
例如,当你的应用程序停止响应,因为它陷入无限循环或死锁时,这种情况怎么办?为了确保在这种情况下应用程序被重启,你必须从外部检查应用程序的健康状况,而不是依赖应用程序内部进行。
4.1.1. 介绍存活探针
Kubernetes 可以通过存活探针检查容器是否仍然存活。你可以在 Pod 规范中为每个容器指定一个存活探针。Kubernetes 将定期执行探针,如果探针失败,则重启容器。
注意
Kubernetes 还支持就绪性探针,我们将在下一章中学习。务必不要混淆这两个概念。它们用于不同的事情。
Kubernetes 可以使用三种机制之一来探测容器:
-
HTTP GET 探针对容器 IP 地址、你指定的端口和路径执行 HTTP GET 请求。如果探针收到响应,并且响应代码不表示错误(换句话说,如果 HTTP 响应代码是 2xx 或 3xx),则探针被视为成功。如果服务器返回错误响应代码或根本不响应,则探针被视为失败,容器将被重启。
-
TCP Socket 探针尝试与容器的指定端口建立 TCP 连接。如果连接成功建立,则探针成功。否则,容器将被重启。
-
Exec 探针在容器内部执行任意命令并检查命令的退出状态码。如果状态码为 0,则探针成功。所有其他代码都被视为失败。
4.1.2. 创建基于 HTTP 的存活探针
让我们看看如何将存活探针添加到你的 Node.js 应用程序中。因为它是一个 Web 应用程序,添加一个检查其 Web 服务器是否正在处理请求的存活探针是有意义的。但是,因为这个特定的 Node.js 应用程序过于简单,永远不会失败,所以你需要人为地使应用程序失败。
为了正确演示存活探针,你将稍微修改应用程序,使其在第五个请求之后对每个请求返回 500 内部服务器错误 HTTP 状态码——你的应用程序将正确处理前五个客户端请求,然后在随后的每个请求上返回错误。多亏了存活探针,它应该在发生这种情况时重启,以便它可以再次正确处理客户端请求。
你可以在本书的代码存档中找到新应用程序的代码(在 Chapter04/kubia-unhealthy 文件夹中)。我已经将容器镜像推送到 Docker Hub,所以你不需要自己构建它。
你将创建一个新的 Pod,该 Pod 包含一个基于 HTTP GET 的存活探针。以下列表显示了 Pod 的 YAML。
列表 4.1. 向 pod 添加存活探针:kubia-liveness-probe.yaml
apiVersion: v1 kind: pod metadata: name: kubia-liveness spec: containers: - image: luksa/kubia-unhealthy 1 name: kubia livenessProbe: 2 httpGet: 2 path: / 3 port: 8080 4
-
1 这是包含(有些)损坏的应用程序的镜像。
-
2 将执行 HTTP GET 的存活探针
-
3 HTTP 请求中请求的路径
-
4 探针应连接到的网络端口
pod 描述符定义了一个 httpGet 存活探针,它告诉 Kubernetes 定期在端口 8080 上的路径 / 上执行 HTTP GET 请求,以确定容器是否仍然健康。这些请求在容器启动时立即开始。
在进行五次此类请求(或实际客户端请求)后,您的应用程序开始返回 HTTP 状态码 500,Kubernetes 将将其视为探针失败,因此会重启容器。
4.1.3. 观察存活探针的实际操作
要查看存活探针的作用,现在尝试创建 pod。大约一分半钟后,容器将被重启。您可以通过运行 kubectl get 来查看:
$ kubectl get po kubia-liveness NAME READY STATUS RESTARTS AGE kubia-liveness 1/1 运行中 1 2m
RESTARTS 列显示 pod 的容器已重启一次(如果您再等一分半钟,它将再次重启,然后无限期地继续循环)。
获取崩溃容器的应用程序日志
在上一章中,您学习了如何使用 kubectl logs 打印应用程序的日志。如果您的容器被重启,kubectl logs 命令将显示当前容器的日志。
当您想找出前一个容器终止的原因时,您会想查看那些日志而不是当前容器的日志。这可以通过使用 --previous 选项来完成:
$ kubectl logs mypod --previous
您可以通过查看 kubectl describe 输出的内容来了解为什么容器必须重启,如下所示。
列表 4.2. 容器重启后的 pod 描述
$ kubectl describe po kubia-liveness Name: kubia-liveness ... Containers: kubia: Container ID: docker://480986f8 Image: luksa/kubia-unhealthy Image ID: docker://sha256:2b208508 Port: State: Running 1 Started: Sun, 14 May 2017 11:41:40 +0200 1 Last State: Terminated 2 Reason: Error 2 Exit Code: 137 2 Started: Mon, 01 Jan 0001 00:00:00 +0000 2 Finished: Sun, 14 May 2017 11:41:38 +0200 2 Ready: True 3 Restart Count: 1 3 Liveness: http-get http://:8080/ delay=0s timeout=1s period=10s #success=1 #failure=3 ... Events: ... Killing container with id docker://95246981:pod "kubia-liveness ..." container "kubia" is unhealthy, it will be killed and re-created.
-
1 当前容器正在运行。
-
2 前一个容器因错误终止并退出,退出代码为 137。
-
3 容器已重启一次。
你可以看到容器当前正在运行,但它之前因为错误而终止。退出代码是 137,它有特殊含义——表示进程被外部信号终止。数字 137 是两个数字的和:128+x,其中 x 是发送给导致进程终止的进程的信号号。在示例中,x 等于 9,这是 SIGKILL 信号的数量,意味着进程被强制终止。
列在底部的事件显示了容器被终止的原因——Kubernetes 检测到容器不健康,因此将其终止并重新创建。
注意
当容器被终止时,会创建一个全新的容器——它不是再次重启的同一个容器。
4.1.4. 配置存活探测的附加属性
你可能已经注意到 kubectl describe 还显示了关于存活探测的附加信息:
Liveness: http-get http://:8080/ delay=0s timeout=1s period=10s #success=1
#failure=3
除了你明确指定的存活探测选项之外,你还可以看到其他附加属性,例如 delay、timeout、period 等。delay=0s 部分表示探测在容器启动后立即开始。timeout 设置为仅 1 秒,因此容器必须在 1 秒内返回响应,否则探测被视为失败。容器每 10 秒探测一次(period=10s),容器在探测连续三次失败后重启(#failure=3)。
这些附加参数可以在定义探测时进行自定义。例如,要设置初始延迟,将 initialDelaySeconds 属性添加到存活探测中,如下面的列表所示。
列表 4.3. 带有初始延迟的存活探测:kubia-liveness-probe-initial-delay.yaml
livenessProbe: httpGet: path: / port: 8080 initialDelaySeconds: 15 1
- 1 Kubernetes 将在执行第一个探测前等待 15 秒。
如果你没有设置初始延迟,探测器将在容器启动时立即开始探测,这通常会导致探测失败,因为应用程序还没有准备好开始接收请求。如果失败次数超过失败阈值,容器在能够正确响应请求之前就会被重启。
小贴士
总是要记得设置一个初始延迟来考虑你的应用程序的启动时间。
我在很多情况下都见过这种情况,用户们困惑为什么他们的容器会被重启。但如果他们使用了 kubectl describe,他们就会看到容器以退出代码 137 或 143 终止,这告诉他们 pod 是被外部终止的。此外,pod 事件的列表将显示容器被杀死是因为存活探测失败。如果你在 pod 启动时看到这种情况发生,那是因为你没有适当地设置 initialDelaySeconds。
注意
退出代码 137 表示进程被外部信号终止(退出代码是 128 + 9(SIGKILL))。同样,退出代码 143 对应于 128 + 15(SIGTERM)。
4.1.5. 创建有效的存活探测
对于在生产中运行的 pod,你应该始终定义一个存活探测。如果没有,Kubernetes 没有办法知道你的应用程序是否仍然存活。只要进程仍在运行,Kubernetes 就会认为容器是健康的。
存活探测应该检查的内容
你的简单存活探测只是检查服务器是否响应。虽然这可能看起来过于简单,但即使是这样的存活探测也能产生奇迹,因为它会导致容器在运行在容器内的 Web 服务器停止响应 HTTP 请求时重启。与没有存活探测相比,这是一个重大的改进,在大多数情况下可能已经足够。
但为了更好的存活检查,你应配置探测在特定的 URL 路径上执行请求(例如 /health),并让应用程序对应用程序内部运行的所有关键组件进行内部状态检查,以确保它们都没有死亡或无响应。
小贴士
确保 /health HTTP 端点不需要身份验证;否则,探测将始终失败,导致你的容器无限重启。
一定要检查应用程序的内部,而不是受外部因素影响的任何内容。例如,如果前端 Web 服务器无法连接到后端数据库,其存活探测不应该返回失败。如果根本原因在数据库本身,重启 Web 服务器容器并不能解决问题。因为存活探测将再次失败,你最终会看到容器反复重启,直到数据库再次可访问。
保持探测轻量
生存探针不应使用过多的计算资源,也不应花费太长时间完成。默认情况下,探针执行相对频繁,并且只允许一秒钟完成。拥有一个执行大量工作的探针可能会显著减慢容器的速度。在本书的后面部分,您还将了解到如何限制容器可用的 CPU 时间。探针的 CPU 时间计入容器的 CPU 时间配额,因此拥有一个重量级的生存探针会减少主应用程序进程可用的 CPU 时间。
小贴士
如果您在容器中运行 Java 应用程序,请务必使用 HTTP GET 生存探针而不是 Exec 探针,后者会启动一个新的 JVM 来获取生存信息。对于任何基于 JVM 或类似的应用程序,其启动过程需要大量的计算资源也是如此。
不要在您的探针中实现重试循环
您已经看到探针的失败阈值是可配置的,并且通常探针必须失败多次,容器才会被杀死。但即使您将失败阈值设置为 1,Kubernetes 也会在将其视为单个失败尝试之前重试探针几次。因此,在探针中实现自己的重试循环是徒劳的。
生存探针总结
您现在明白,Kubernetes 通过在容器崩溃或生存探针失败时重启它们来保持容器运行。这项工作由运行在托管 Pod 的节点上的 Kubelet 执行——运行在主节点上的 Kubernetes 控制平面组件在这个过程中没有发挥作用。
但如果节点本身崩溃,那么控制平面必须为与节点一起宕机的所有 Pod 创建替代品。它不会为直接创建的 Pod 做这件事。这些 Pod 除了由 Kubelet 管理之外,没有其他管理方式,但由于 Kubelet 运行在节点本身上,如果节点失败,它什么也无法做。
为了确保您的应用程序在另一个节点上重启,您需要让 Pod 由 ReplicationController 或类似机制管理,我们将在本章的其余部分讨论这一点。
4.2. 介绍 ReplicationController
ReplicationController 是 Kubernetes 资源,确保其 Pod 始终运行。如果 Pod 因任何原因消失,例如节点从集群中消失或 Pod 被从节点驱逐,ReplicationController 会注意到缺失的 Pod 并创建一个替代 Pod。
图 4.1 展示了当一个节点宕机并带走两个 Pod 时会发生什么。Pod A 是直接创建的,因此是一个未管理的 Pod,而 Pod B 由 ReplicationController 管理。节点失败后,ReplicationController 创建一个新的 Pod(Pod B2)来替换缺失的 Pod B,而 Pod A 则完全丢失——它永远不会被重新创建。
图 4.1。当一个节点失败时,只有由 ReplicationController 支持的 Pod 会被重新创建。

图中的 ReplicationController 只管理一个 Pod,但通常情况下,ReplicationController 的目的是创建和管理 Pod 的多个副本(replicas)。这就是 ReplicationController 名字的由来。
4.2.1. ReplicationController 的操作
ReplicationController 持续监控正在运行的 Pod 列表,并确保“类型”的 Pod 实际数量始终与目标数量匹配。如果运行的此类 Pod 太少,它会从 Pod 模板创建新的副本。如果运行的此类 Pod 太多,它会删除多余的副本。
您可能会想知道为什么副本数量会超过目标数量。这可能是由于以下几个原因:
-
人工创建了一个相同类型的 Pod。
-
人工更改现有 Pod 的“类型”。
-
人工减少所需的 Pod 数量,等等。
我多次使用了“Pod 类型”这个术语。但事实上并不存在这样的类型。ReplicationController 不是在 Pod 类型上操作,而是在匹配特定标签选择器的 Pod 集合上操作(您在上一章中已经了解过它们)。
介绍控制器的协调循环
ReplicationController 的职责是确保 Pod 的确切数量始终与其标签选择器匹配。如果不匹配,ReplicationController 将采取适当的行动来协调实际数量与目标数量。ReplicationController 的操作在图 4.2 中显示。
图 4.2. ReplicationController 的协调循环

理解 ReplicationController 的三个部分
ReplicationController 有三个基本部分(也显示在图 4.3 中):
-
一个标签选择器,它决定了哪些 Pod 位于 ReplicationController 的作用域内
-
一个副本数量,它指定了应该运行的目标 Pod 数量
-
一个 Pod 模板,用于创建新的 Pod 副本
图 4.3. ReplicationController 的三个关键部分(pod 选择器、副本数量和 Pod 模板)

ReplicationController 的副本数量、标签选择器,甚至 Pod 模板都可以随时修改,但只有副本数量的更改会影响现有的 Pod。
理解更改控制器标签选择器或 Pod 模板的影响
标签选择器和 Pod 模板的更改对现有 Pod 没有影响。更改标签选择器会使现有 Pod 脱离 ReplicationController 的作用域,因此控制器不再关心它们。ReplicationController 在创建 Pod 后,也不再关心其 Pod 的实际“内容”(容器镜像、环境变量和其他事物)。因此,模板只影响由该 ReplicationController 创建的新 Pod。您可以将它视为切割新 Pod 的模具。
理解使用 ReplicationController 的好处
像 Kubernetes 中的许多事物一样,尽管 ReplicationController 是一个极其简单的概念,但它提供了或启用了以下强大的功能:
-
它通过在现有的 Pod 失踪时启动一个新的 Pod,确保一个 Pod(或多个 Pod 副本)始终在运行。
-
当集群节点失败时,它会为在失败的节点上运行的所有 Pod 创建替换副本(那些在 Replication-Controller 控制下的 Pod)。
-
它使得 Pod 的水平扩展变得简单——无论是手动还是自动(参见第十五章中的水平 Pod 自动扩展 [index_split_113.html#filepos1423922])。
注意
Pod 实例永远不会被转移到另一个节点。相反,Replication-Controller 会创建一个全新的 Pod 实例,它与它所替换的实例没有任何关系。
4.2.2. 创建一个 ReplicationController
让我们看看如何创建一个 ReplicationController,然后看看它是如何保持 Pod 运行的。像 Pod 和其他 Kubernetes 资源一样,您通过向 Kubernetes API 服务器发送 JSON 或 YAML 描述符来创建 ReplicationController。
您将创建一个名为 kubia-rc.yaml 的 YAML 文件,用于您的 ReplicationController,如下所示。
列表 4.4. 一个 ReplicationController 的 YAML 定义:kubia-rc.yaml
apiVersion: v1 kind: ReplicationController 1 metadata: name: kubia 2 spec: replicas: 3 3 selector: 4 app: kubia 4 template: 5 metadata: 5 labels: 5 app: kubia 5 spec: 5 containers: 5 - name: kubia 5 image: luksa/kubia 5 ports: 5 - containerPort: 8080 5
-
1 此清单定义了一个 ReplicationController(RC)
-
2 这个 ReplicationController 的名称
-
3 所需的 Pod 实例数量
-
4 确定 RC 正在操作的 Pod 的 Pod 选择器
-
5 创建新 Pod 的 Pod 模板
当您将文件发送到 API 服务器时,Kubernetes 创建一个名为 kubia 的新 Replication-Controller,确保三个 Pod 实例始终匹配标签选择器 app=kubia。当 Pod 数量不足时,将根据提供的 Pod 模板创建新的 Pod。模板的内容几乎与您在上一章中创建的 Pod 定义相同。
模板中的 Pod 标签必须显然与 ReplicationController 的标签选择器匹配;否则,控制器将无限期地创建新的 Pod,因为启动一个新的 Pod 并不会使实际副本数更接近所需的副本数。为了防止此类情况,API 服务器会验证 ReplicationController 的定义,如果配置错误则不会接受它。
完全不指定选择器也是一种选择。在这种情况下,它将自动从 Pod 模板中的标签配置。
提示
在定义 ReplicationController 时不要指定 Pod 选择器。让 Kubernetes 从 Pod 模板中提取它。这将使您的 YAML 更短、更简单。
要创建 ReplicationController,使用你已知的kubectl create命令:
$ kubectl create -f kubia-rc.yaml replicationcontroller "kubia" created
一旦创建 ReplicationController,它就会开始工作。让我们看看它做了什么。
4.2.3. 查看 ReplicationController 的实际操作
因为没有 Pod 具有app=kubia标签,ReplicationController 应该从 Pod 模板启动三个新的 Pod。列出 Pods 以查看 ReplicationController 是否完成了它应该做的事情:
$ kubectl get pods NAME READY STATUS RESTARTS AGE kubia-53thy 0/1 ContainerCreating 0 2s kubia-k0xz6 0/1 ContainerCreating 0 2s kubia-q3vkg 0/1 ContainerCreating 0 2s
的确如此!你想要三个 Pod,它创建了三个 Pod。现在它正在管理这三个 Pod。接下来,你会稍微干扰它们,以查看 Replication-Controller 如何响应。
查看 ReplicationController 对已删除 Pod 的响应
首先,你将手动删除一个 Pod,以查看 ReplicationController 如何立即启动一个新的 Pod,将匹配的 Pod 数量恢复到三个:
$ kubectl delete pod kubia-53thy pod "kubia-53thy" deleted
再次列出 Pods 时,显示四个 Pod,因为你要删除的那个 Pod 正在终止,并且已经创建了一个新的 Pod:
$ kubectl get pods NAME READY STATUS RESTARTS AGE kubia-53thy 1/1 Terminating 0 3m kubia-oini2 0/1 ContainerCreating 0 2s kubia-k0xz6 1/1 Running 0 3m kubia-q3vkg 1/1 Running 0 3m
ReplicationController 再次完成了它的任务。它是一个很好的小助手,不是吗?
获取 ReplicationController 的信息
现在,让我们看看kubectl get命令对于 ReplicationControllers 显示的信息:
$ kubectl get rc NAME DESIRED CURRENT READY AGE kubia 3 3 2 3m
注意
我们使用rc作为replicationcontroller的简称。
你看到有三列显示所需的 Pod 数量、实际 Pod 数量以及有多少个是就绪的(你将在下一章讨论就绪性探针时了解这意味着什么)。
你可以使用kubectl describe命令查看关于你的 ReplicationController 的更多信息,如下所示。
列表 4.5. 使用kubectl describe显示 ReplicationController 的详细信息
$ kubectl describe rc kubia 名称: kubia 命名空间: default 选择器: app=kubia 标签: app=kubia 注解: 无 副本: 3 当前 / 3 期望 1 Pods 状态: 4 运行 / 0 等待 / 0 成功 / 0 失败 2 Pod 模板: 标签: app=kubia 容器: ... 卷: 无 事件: 3 来源 类型 原因 消息 ---- ------- ------ ------- replication-controller 正常 成功创建 Created pod: kubia-53thy replication-controller 正常 成功创建 Created pod: kubia-k0xz6 replication-controller 正常 成功创建 Created pod: kubia-q3vkg replication-controller 正常 成功创建 Created pod: kubia-oini2
-
1 实际与期望的 Pod 实例数量
-
2 每个 Pod 状态的 Pod 实例数量
-
3 与此 ReplicationController 相关的事件
当前副本数量与期望数量匹配,因为控制器已经创建了一个新的 Pod。它显示有四个正在运行的 Pod,因为一个正在终止的 Pod 仍然被视为正在运行,尽管它不计入当前的副本计数。
底部的活动列表显示了 Replication-Controller 采取的行动——它已经创建了四个 Pod。
确切理解导致控制器创建新 Pod 的原因
控制器通过创建一个新的替换 Pod 来响应 Pod 的删除(见图 4.4)。好吧,技术上讲,它并不是在响应删除本身,而是响应的结果——Pod 数量不足。
图 4.4。如果一个 Pod 消失了,ReplicationController 会看到 Pod 数量过少,并创建一个新的替换 Pod。

虽然 ReplicationController 会立即通知关于 Pod 被删除的情况(API 服务器允许客户端监视资源及其列表的变化),但这并不是导致它创建替换 Pod 的原因。通知触发控制器检查实际的 Pod 数量并采取适当的行动。
响应节点故障
观察 ReplicationController 对 Pod 手动删除的响应并不太有趣,所以让我们看看更好的例子。如果你使用 Google Kubernetes Engine 来运行这些示例,你有一个三节点 Kubernetes 集群。你将断开其中一个节点与网络的连接来模拟节点故障。
注意
如果你使用 Minikube,你不能做这个练习,因为你只有一个节点同时作为主节点和工作节点。
在非 Kubernetes 世界中,如果一个节点失败,运维团队需要手动将运行在该节点上的应用程序迁移到其他机器上。另一方面,Kubernetes 会自动完成这项工作。ReplicationController 检测到其 Pod 挂起后不久,就会启动新的 Pod 来替换它们。
让我们看看实际操作。你需要使用gcloud compute ssh命令登录到节点之一,然后使用sudo ifconfig eth0 down关闭其网络接口,如下所示。
注意
通过使用-o wide选项列出 Pod 来选择至少运行了一个 Pod 的节点。
列表 4.6. 通过关闭网络接口来模拟节点故障
$ gcloud compute ssh gke-kubia-default-pool-b46381f1-zwko 输入密钥对/home/luksa/.ssh/google_compute_engine的密码:欢迎使用 Kubernetes v1.6.4! ... luksa@gke-kubia-default-pool-b46381f1-zwko ~ $ sudo ifconfig eth0 down`
当你关闭网络接口时,ssh会话将停止响应,因此你需要打开另一个终端或从ssh会话中硬退出。在新的终端中,你可以列出节点以查看 Kubernetes 是否检测到节点已关闭。这需要大约一分钟的时间。然后,节点的状态将显示为NotReady:
$ kubectl get node NAME STATUS AGE gke-kubia-default-pool-b46381f1-opc5 Ready 5h gke-kubia-default-pool-b46381f1-s8gj Ready 5h gke-kubia-default-pool-b46381f1-zwko NotReady 5h 1
- 1 节点未就绪,因为它已断开网络连接
如果你现在列出 Pod,你仍然会看到之前相同的三个 Pod,因为 Kubernetes 在重新调度 Pod 之前会等待一段时间(以防节点因临时网络故障或 Kubelet 重启而不可达)。如果节点连续几分钟都不可达,分配给该节点的 Pod 的状态将变为Unknown。到那时,ReplicationController 将立即启动一个新的 Pod。你可以通过再次列出 Pod 来看到这一点:
$ kubectl get pods NAME READY STATUS RESTARTS AGE kubia-oini2 1/1 Running 0 10m kubia-k0xz6 1/1 Running 0 10m kubia-q3vkg 1/1 Unknown 0 10m 1 kubia-dmdck 1/1 Running 0 5s 2
-
1 这个 Pod 的状态是未知,因为它的节点不可达。
-
2 这个 Pod 是五秒前创建的。
通过观察 Pod 的年龄,你可以看到kubia-dmdck Pod 是新的。你现在又有三个 Pod 实例正在运行,这意味着 ReplicationController 再次完成了其工作,将系统的实际状态带到期望状态。
如果节点失败(无论是崩溃还是变得不可达),也不需要立即的人工干预。系统会自动修复。
要将节点恢复,你需要使用以下命令:
$ gcloud compute instances reset gke-kubia-default-pool-b46381f1-zwko
当节点再次启动时,其状态应该返回到Ready,状态为Unknown的 Pod 将被删除。
4.2.4. 在 ReplicationController 的作用范围内移动 Pod
由 ReplicationController 创建的 pod 与 ReplicationController 没有任何绑定关系。在任何时刻,ReplicationController 管理着匹配其标签选择器的 pod。通过更改 pod 的标签,它可以从 ReplicationController 的作用域中移除或添加。它甚至可以被移动到另一个 ReplicationController。
小贴士
尽管 pod 没有绑定到 ReplicationController,但 pod 确实在metadata.ownerReferences字段中引用了它,你可以使用这个字段轻松地找到 pod 所属的 ReplicationController。
如果你更改 pod 的标签,使其不再匹配 ReplicationController 的标签选择器,那么 pod 就像任何其他手动创建的 pod 一样。它不再由任何东西管理。如果运行 pod 的节点失败,pod 显然不会被重新调度。但请注意,当你更改 pod 的标签时,复制控制器注意到丢失了一个 pod,并启动了一个新的 pod 来替换它。
让我们用你的 pod 试一试。因为你的 ReplicationController 管理着带有 app=kubia 标签的 pod,所以你需要移除这个标签或更改其值,将 pod 移出 ReplicationController 的作用域。添加另一个标签将没有任何效果,因为 ReplicationController 不关心 pod 是否有任何额外的标签。它只关心 pod 是否具有标签选择器中引用的所有标签。
向由 ReplicationController 管理的 pod 添加标签
让我们确认 ReplicationController 不关心你是否为其管理的 pod 添加额外的标签:
$ kubectl label pod kubia-dmdck type=special pod "kubia-dmdck" labeled $ kubectl get pods --show-labels NAME READY STATUS RESTARTS AGE LABELS kubia-oini2 1/1 Running 0 11m app=kubia kubia-k0xz6 1/1 Running 0 11m app=kubia kubia-dmdck 1/1 Running 0 1m app=kubia,type=special
你已经将 type=special 标签添加到了一个 pod 上。再次列出所有 pod,应该显示与之前相同的三个 pod,因为就 ReplicationController 而言没有发生变化。
更改管理 pod 的标签
现在,你将更改 app=kubia 标签为其他内容。这将使 pod 无法匹配 ReplicationController 的标签选择器,使其只能匹配两个 pod。因此,ReplicationController 应该启动一个新的 pod,将数量恢复到三个:
$ kubectl label pod kubia-dmdck app=foo --overwrite pod "kubia-dmdck" labeled
--overwrite 参数是必要的;否则 kubectl 将只会打印出一个警告,而不会更改标签,以防止你在意图添加新标签时意外更改现有标签的值。
再次列出所有 pod 现在应该显示四个 pod:
$ kubectl get pods -L app 名称 READY 状态 RESTARTS AGE APP kubia-2qneh 0/1 容器创建中 0 2s kubia 1 kubia-oini2 1/1 运行中 0 20m kubia kubia-k0xz6 1/1 运行中 0 20m kubia kubia-dmdck 1/1 运行中 0 10m foo 2
-
1 新创建的 Pod,用于替换你从 ReplicationController 作用范围中移除的 Pod
-
2 Pod 不再由 ReplicationController 管理
注意
你正在使用-L app选项在列中显示app标签。
现在,你总共有四个 Pod:一个不受你的 Replication-Controller 管理,另外三个是。其中就包括新创建的 Pod。
图 4.5 展示了当你更改 Pod 的标签,使其不再匹配 ReplicationController 的 Pod 选择器时发生了什么。你可以看到你的三个 Pod 和你的 ReplicationController。在你将 Pod 的标签从app=kubia更改为app=foo后,ReplicationController 就不再关心这个 Pod 了。因为控制器的副本计数设置为 3,只有两个 Pod 匹配标签选择器,所以 ReplicationController 启动了kubia-2qneh Pod,将数量恢复到 3。kubia-dmdck Pod 现在完全独立,将一直运行,直到你手动删除它(你现在可以这样做,因为你不再需要它了)。
图 4.5. 通过更改标签从 ReplicationController 的作用范围中移除 Pod

实际中从控制器中移除 Pod
当你想对特定的 Pod 执行操作时,从 ReplicationController 的作用范围中移除 Pod 会很有用。例如,你可能有一个 bug,导致 Pod 在经过一段时间或特定事件后开始表现不佳。如果你知道某个 Pod 出现故障,你可以将其从 ReplicationController 的作用范围中移除,让控制器用一个新的 Pod 替换它,然后以任何你想要的方式调试或操作这个 Pod。一旦完成,你就可以删除这个 Pod。
修改 ReplicationController 的标签选择器
作为检验你是否完全理解 ReplicationController 的练习,如果你不是更改 Pod 的标签,而是修改 Replication-Controller 的标签选择器,你认为会发生什么?
如果你的答案是这会使所有 Pod 脱离 ReplicationController 的作用范围,从而导致它创建三个新的 Pod,你完全正确。这表明你理解了 ReplicationController 是如何工作的。
Kubernetes 允许你更改 ReplicationController 的标签选择器,但本章后半部分介绍的其他资源(这些资源也用于管理 Pod)则不是这样。你永远不会更改控制器的标签选择器,但你会经常更改其 Pod 模板。让我们看看这一点。
4.2.5. 修改 Pod 模板
ReplicationController 的 pod 模板可以在任何时候进行修改。更改 pod 模板就像更换一个模具。它只会影响之后切出的饼干,而不会影响你已经切出的饼干(参见图 4.6)。要修改旧 pod,你需要删除它们,并让 Replication-Controller 用基于新模板的新 pod 替换它们。
图 4.6. 更改 ReplicationController 的 pod 模板只会影响之后创建的 pod,而不会影响现有的 pod。

作为练习,你可以尝试编辑 ReplicationController 并为 pod 模板添加一个标签。你可以使用以下命令编辑 ReplicationController:
$ kubectl edit rc kubia
这将在你的默认文本编辑器中打开 ReplicationController 的 YAML 定义。找到 pod 模板部分并添加一个额外的标签到元数据。在你保存更改并退出编辑器后,kubectl 将更新 ReplicationController 并打印以下消息:
replicationcontroller "kubia" edited
你现在可以再次列出 pod 和它们的标签,并确认它们没有改变。但如果你删除 pod 并等待它们的替代品被创建,你会看到新的标签。
通过这种方式编辑 ReplicationController 以更改 pod 模板中的容器镜像,删除现有的 pod,并让它们由新模板中的新 pod 替换,这可以用于升级 pod,但你将在第九章(index_split_074.html#filepos865425)中学习更好的方法。
配置 kubectl edit 以使用不同的文本编辑器
你可以通过设置 KUBE_EDITOR 环境变量来告诉 kubectl 使用你选择的文本编辑器。例如,如果你想使用 nano 编辑 Kubernetes 资源,执行以下命令(或将它放入你的 ~/.bashrc 或等效文件中):
export KUBE_EDITOR="/usr/bin/nano"
如果没有设置 KUBE_EDITOR 环境变量,kubectl edit 将回退到使用默认编辑器,通常通过 EDITOR 环境变量配置。
4.2.6. 水平扩展 pod
你已经看到了 ReplicationControllers 如何确保始终运行特定数量的 pod 实例。由于更改期望副本数量的操作极其简单,这也意味着水平扩展 pod 是微不足道的。
上调或下调 pod 的数量就像更改 ReplicationController 资源中 replicas 字段的值一样简单。更改后,Replication-Controller 将会看到 pod 数量过多(在缩小时)并删除其中的一部分,或者看到 pod 数量过少(在扩小时)并创建额外的 pod。
扩展 ReplicationController
你的 ReplicationController 一直在运行你的 Pod 的三个实例。现在你将把这个数字增加到 10。你可能还记得,你已经在第二章中扩展了一个 ReplicationController。你可以使用之前的相同命令:
$ kubectl scale rc kubia --replicas=10
但这次你会用不同的方式来做。
通过编辑定义扩展 ReplicationController
而不是使用kubectl scale命令,你将通过编辑 ReplicationController 的定义以声明式的方式对其进行扩展:
$ kubectl edit rc kubia
当文本编辑器打开时,找到spec.replicas字段,并将其值更改为10,如下所示。
列表 4.7. 通过运行kubectl edit在文本编辑器中编辑 RC
# 请编辑以下对象。以'#'开头的行将被忽略,# 一个空文件将终止编辑。如果在保存此文件时发生错误,则将重新打开此文件并显示相关失败。apiVersion: v1 kind: ReplicationController metadata: ... spec: replicas: 3 1 selector: app: kubia ...
- 1 将此行中的数字 3 更改为数字 10。
当你保存文件并关闭编辑器时,ReplicationController 将被更新,并且它立即将 Pod 的数量扩展到 10:
$ kubectl get rc NAME DESIRED CURRENT READY AGE kubia 10 10 4 21m
就这样。如果kubectl scale命令让你看起来像是在告诉 Kubernetes 确切要做什么,那么现在就更加清楚,你是在对 ReplicationController 的期望状态进行声明式更改,而不是告诉 Kubernetes 做什么。
使用 kubectl scale 命令缩小规模
现在将其缩放到 3。你可以使用kubectl scale命令:
$ kubectl scale rc kubia --replicas=3
这个命令所做的只是修改 ReplicationController 定义中的spec.replicas字段——就像你通过kubectl edit修改它一样。
理解声明式扩展方法
在 Kubernetes 中水平扩展 Pod 是一个表达你愿望的问题:“我想运行 x 个实例。”你并没有告诉 Kubernetes 做什么或如何做。你只是指定了期望的状态。
这种声明式方法使得与 Kubernetes 集群交互变得简单。想象一下,如果你必须手动确定当前运行的实例数量,然后明确告诉 Kubernetes 要运行多少额外的实例。那将是一项更多的工作,并且更容易出错。更改一个简单的数字要容易得多,在第十五章中,你将了解到,如果你启用了水平 Pod 自动扩展,甚至那也可以由 Kubernetes 本身完成。
4.2.7. 删除一个 ReplicationController
当你通过kubectl delete删除 ReplicationController 时,Pod 也会被删除。但由于由 ReplicationController 创建的 Pod 不是 ReplicationController 的组成部分,并且仅由它管理,因此你可以仅删除 ReplicationController 并保持 Pod 运行,如图 4.7 所示。
图 4.7. 使用--cascade=false删除 replication controller 会留下未管理的 Pod。

这可能在你最初有一组由 ReplicationController 管理的 Pod 时很有用,然后决定用 ReplicaSet 替换 ReplicationController,例如(你将在下一章中了解它们)。你可以这样做而不影响 Pod,并在替换管理它们的 ReplicationController 时保持它们的无间断运行。
当使用kubectl delete删除 ReplicationController 时,可以通过向命令传递--cascade=false选项来保持其 Pod 运行。现在尝试一下:
$ kubectl delete rc kubia --cascade=false replicationcontroller "kubia" deleted
你已经删除了 ReplicationController,所以 Pod 们现在独立运行。它们不再受管理。但你可以始终创建一个新的 ReplicationController,并使用适当的标签选择器来再次管理它们。
4.3. 使用 ReplicaSet 代替 ReplicationController
最初,ReplicationController 是 Kubernetes 中用于复制 Pod 并在节点失败时重新调度 Pod 的唯一组件。后来,引入了一个类似资源,称为 ReplicaSet。它是 ReplicationController 的新一代,并完全取代了它(ReplicationController 最终将被弃用)。
您本可以以创建 ReplicaSet 而不是 ReplicationController 开始本章,但我认为从 Kubernetes 最初可用的内容开始是一个好主意。此外,你仍然会在野外看到 ReplicationController 的使用,所以了解它们对你来说是有好处的。话虽如此,从现在开始,你应该始终创建 ReplicaSet 而不是 ReplicationController。它们几乎相同,所以你应该不会在使用它们时遇到任何麻烦。
你通常不会直接创建它们,而是在创建更高层次的 Deployment 资源时自动创建它们,你将在第九章(index_split_074.html#filepos865425)中了解它。无论如何,你应该了解 ReplicaSet,让我们看看它们与 ReplicationController 有何不同。
4.3.1. 将 ReplicaSet 与 ReplicationController 进行比较
ReplicaSet 的行为与 ReplicationController 完全相同,但它具有更丰富的 Pod 选择器。而 ReplicationController 的标签选择器仅允许匹配包含特定标签的 Pod,ReplicaSet 的选择器还允许匹配缺少特定标签或包含特定标签键的 Pod,无论其值如何。
此外,例如,单个 ReplicationController 不能同时匹配带有标签env=production和带有标签env=devel的 Pods。它只能匹配带有env=production标签的 Pods 或带有env=devel标签的 Pods。但单个 ReplicaSet 可以匹配这两组 Pods 并将它们视为一个单一组。
同样,ReplicationController 不能仅仅根据标签键的存在来匹配 Pods,无论其值如何,而 ReplicaSet 可以。例如,ReplicaSet 可以匹配所有包含键为env的标签的 Pods,无论其实际值是什么(你可以将其视为env=*)。
4.3.2. 定义 ReplicaSet
你现在将创建一个 ReplicaSet 来查看之前由你的 ReplicationController 创建然后被遗弃的孤儿 Pods 现在如何可以被 ReplicaSet 所采用。首先,你需要将你的 ReplicationController 重写为一个 ReplicaSet,通过创建一个名为 kubia-replicaset.yaml 的新文件,并包含以下列表中的内容。
列表 4.8. ReplicaSet 的 YAML 定义:kubia-replicaset.yaml
apiVersion: apps/v1beta2 1 kind: ReplicaSet 1 metadata: 1 name: kubia 1 spec: 1 replicas: 3 1 selector: 1 matchLabels: 2 app: kubia 2 template: 3 metadata: 3 labels: 3 app: kubia 3 spec: 3 containers: 3 - name: kubia 3 image: luksa/kubia 3
-
1 ReplicaSets 不是 v1 API 的一部分,但属于
appsAPI 组,版本为 v1beta2。 -
2 你在这里使用的是更简单的 matchLabels 选择器,这与 ReplicationController 的选择器非常相似。
-
3 模板与 ReplicationController 中的相同。
首先要注意的是,ReplicaSets 不是 v1 API 的一部分,所以在创建资源时你需要确保指定正确的apiVersion。你正在创建一个类型为 ReplicaSet 的资源,其内容与之前创建的 Replication-Controller 非常相似。
唯一的区别在于选择器。你不需要在selector属性下直接列出 Pods 需要拥有的标签,而是在selector.matchLabels下指定它们。这是在 ReplicaSet 中定义标签选择器的一种更简单(且不那么表达性)的方式。稍后,你还将看到更表达性的选项。
关于 API 版本属性
这是你第一次看到apiVersion属性指定了两件事:
-
API 组(在这个例子中是
apps) -
实际的 API 版本(
v1beta2)
你会在整本书中看到,某些 Kubernetes 资源位于所谓的核心 API 组中,在apiVersion字段中不需要指定(你只需指定版本——例如,在定义 Pod 资源时,你一直使用apiVersion: v1)。其他资源,在后来的 Kubernetes 版本中引入,被分类到几个 API 组中。查看书的封面内部以查看所有资源和它们各自的 API 组。
由于你仍然有三个匹配 app=kubia 选择器的 pod 在之前运行,创建这个 ReplicaSet 不会导致创建任何新的 pod。ReplicaSet 将将现有的三个 pod 纳入其翼下。
4.3.3. 创建和检查 ReplicaSet
使用 kubectl create 命令从 YAML 文件创建 ReplicaSet。之后,你可以使用 kubectl get 和 kubectl describe 检查 ReplicaSet:
$ kubectl get rs NAME DESIRED CURRENT READY AGE kubia 3 3 3 3s
提示
使用 rs 简写,代表 replicaset。
$ kubectl describe rs Name: kubia Namespace: default Selector: app=kubia Labels: app=kubia Annotations: <none> Replicas: 3 current / 3 desired Pods Status: 3 Running / 0 Waiting / 0 Succeeded / 0 Failed Pod Template: Labels: app=kubia Containers: ... Volumes: <none> Events: <none>
如你所见,ReplicaSet 与 ReplicationController 没有任何不同。它显示有三个副本匹配选择器。如果你列出所有 pod,你会看到它们仍然是之前的那三个 pod。ReplicaSet 没有创建任何新的 pod。
4.3.4. 使用 ReplicaSet 的更表达式标签选择器
ReplicaSet 相比 ReplicationController 的主要改进是它们更表达式的标签选择器。你故意在第一个 ReplicaSet 示例中使用了更简单的 matchLabels 选择器,以查看 ReplicaSet 与 Replication-Controllers 没有任何不同。现在,你将重写选择器以使用更强大的 matchExpressions 属性,如下所示。
列表 4.9. 一个 matchExpressions 选择器:kubia-replicaset-matchexpressions.yaml
selector: matchExpressions: - key: app operator: In values: - kubia
-
1 此选择器要求 pod 包含一个具有“app”键的标签。
-
2 标签的值必须是“kubia”。
注意
只显示了选择器。你可以在书籍的代码存档中找到整个 ReplicaSet 定义。
你可以向选择器添加额外的表达式。例如,每个表达式必须包含一个 key、一个 operator 以及可能(根据操作符)的 values 列表。你将看到四个有效的操作符:
-
In—标签的值必须匹配指定的values。 -
NotIn—标签的值必须不匹配指定的任何values。 -
Exists—Pod 必须包含具有指定键的标签(值不重要)。当使用此操作符时,你不应该指定values字段。 -
DoesNotExist—Pod 必须不包含具有指定键的标签。values属性不得指定。
如果你指定了多个表达式,所有这些表达式都必须评估为真,选择器才能匹配 pod。如果你指定了 matchLabels 和 matchExpressions,所有标签都必须匹配,所有表达式都必须评估为真,pod 才能匹配选择器。
4.3.5. 总结 ReplicaSets
这是对 ReplicaSets 作为 ReplicationControllers 的替代方法的快速介绍。记住,始终使用它们而不是 ReplicationControllers,但你可能会在其他人的部署中找到 ReplicationControllers。
现在,删除 ReplicaSet 以清理你的集群。你可以用删除 ReplicationController 的相同方式删除 ReplicaSet:
$ kubectl delete rs kubia replicaset "kubia" deleted
删除 ReplicaSet 应该会删除所有 pod。列出 pod 以确认这一点。
4.4. 在每个节点上使用 DaemonSet 运行一个 pod
ReplicationControllers 和 ReplicaSets 都用于在 Kubernetes 集群中的任何位置运行特定数量的 pod。但在某些情况下,你可能希望 pod 在集群的每个节点上运行(并且每个节点需要运行 pod 的确切一个实例,如图 4.8 所示 图 4.8)。
图 4.8. DaemonSet 在每个节点上只运行一个 pod 副本,而 ReplicaSets 则将它们随机散布在整个集群中。

这些情况包括与基础设施相关的 pod,它们执行系统级操作。例如,你可能在每个节点上运行日志收集器和资源监控器。另一个很好的例子是 Kubernetes 自身的 kube-proxy 进程,它需要在所有节点上运行以使服务工作。
在 Kubernetes 之外,此类进程通常会在节点启动时通过系统初始化脚本或 systemd 守护进程启动。在 Kubernetes 节点上,你仍然可以使用 systemd 运行你的系统进程,但那时你就无法利用 Kubernetes 提供的所有功能。
4.4.1. 使用 DaemonSet 在每个节点上运行 pod
要在所有集群节点上运行 pod,你需要创建一个 DaemonSet 对象,这与 ReplicationController 或 ReplicaSet 非常相似,只不过由 DaemonSet 创建的 pod 已经指定了目标节点,并跳过了 Kubernetes 调度器。它们不会在集群中随机分布。
DaemonSet 确保创建与节点数量相等的 pod,并将每个 pod 部署在其自己的节点上,如图 4.8 所示 图 4.8。
虽然 ReplicaSet(或 ReplicationController)确保集群中存在所需数量的 pod 副本,但 DaemonSet 没有任何关于所需副本数量的概念。它不需要它,因为它的任务是确保匹配其 pod 选择器的 pod 在每个节点上运行。
如果节点宕机,DaemonSet 不会在其他地方创建 pod。但当集群中添加新节点时,DaemonSet 会立即在该节点上部署一个新的 pod 实例。如果有人意外删除了其中一个 pod,导致节点没有 DaemonSet 的 pod,它也会这样做。像 ReplicaSet 一样,DaemonSet 会从配置在其内的 pod 模板中创建 pod。
4.4.2. 使用 DaemonSet 仅在特定节点上运行 pod
DaemonSet 将 Pod 部署到集群中的所有节点,除非您指定 Pod 应仅在所有节点的一个子集上运行。这是通过在 Pod 模板中指定 node-Selector 属性来完成的,该属性是 DaemonSet 定义的一部分(类似于 ReplicaSet 或 ReplicationController 中的 Pod 模板)。
您已经在 第三章 中使用节点选择器将 Pod 部署到特定的节点。在 DaemonSet 中的节点选择器类似——它定义了 DaemonSet 必须部署其 Pod 的节点。
注意
在本书的后面部分,您将了解到节点可以被设置为不可调度,防止 Pod 部署到它们。DaemonSet 仍会将 Pod 部署到这样的节点,因为不可调度属性仅由调度器使用,而由 DaemonSet 管理的 Pod 完全绕过调度器。这通常是期望的,因为 DaemonSets 的目的是运行系统服务,这些服务通常需要在不可调度的节点上运行。
以示例解释 DaemonSet
让我们想象有一个名为 ssd-monitor 的守护进程需要在包含固态硬盘(SSD)的所有节点上运行。您将创建一个 DaemonSet,在标记为具有 SSD 的所有节点上运行此守护进程。集群管理员已将这些节点的 disk=ssd 标签添加到所有此类节点,因此您将创建一个带有节点选择器的 DaemonSet,仅选择具有该标签的节点,如图 4.9 所示。
图 4.9. 使用带有节点选择器的 DaemonSet 仅在特定节点上部署系统 Pod

创建 DaemonSet YAML 定义
您将创建一个运行模拟 ssd-monitor 进程的 DaemonSet,该进程每五秒向标准输出打印一次“SSD OK”。我已经准备好了模拟容器镜像并将其推送到 Docker Hub,因此您可以使用它而不是自己构建。创建 DaemonSet 的 YAML,如下所示。
列表 4.10. DaemonSet 的 YAML:ssd-monitor-daemonset.yaml
apiVersion: apps/v1beta2 1 kind: DaemonSet 1 metadata: 1 name: ssd-monitor 1 spec: 1 selector: 1 matchLabels: 1 app: ssd-monitor 1 template: 1 metadata: 1 labels: 1 app: ssd-monitor 1 spec: 1 nodeSelector: 2 disk: ssd 2 containers: 1 - name: main 1 image: luksa/ssd-monitor
-
1 DaemonSet 位于 apps API 组,版本 v1beta2。
-
2 Pod 模板包括一个节点选择器,该选择器选择具有 disk=ssd 标签的节点。
您正在定义一个 DaemonSet,该 DaemonSet 将基于 luksa/ssd-monitor 容器镜像运行一个包含单个容器的 Pod。对于每个具有 disk=ssd 标签的节点,都将创建一个此 Pod 的实例。
创建 DaemonSet
您将像创建 YAML 文件中的资源一样创建 DaemonSet:
$ kubectl create -f ssd-monitor-daemonset.yaml daemonset "ssd-monitor" created
让我们看看创建的 DaemonSet:
$ kubectl get ds NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE-SELECTOR ssd-monitor 0 0 0 0 0 disk=ssd
那些零看起来很奇怪。DaemonSet 没有部署任何 pod 吗?列出 pod:
$ kubectl get po No resources found.
那些 pod 在哪里?你知道发生了什么吗?是的,你忘记给你的节点添加 disk=ssd 标签了。没问题——你现在可以这么做。DaemonSet 应该会检测到节点的标签已更改,并将 pod 部署到所有匹配标签的节点上。让我们看看这是否是真的。
给你的节点(们)添加所需的标签
无论你是在使用 Minikube、GKE 还是其他多节点集群,你首先需要列出节点,因为当你标记节点时,你需要知道节点的名称:
$ kubectl get node NAME STATUS AGE VERSION minikube Ready 4d v1.6.0
现在,像这样给你的一个节点添加 disk=ssd 标签:
$ kubectl label node minikube disk=ssd node "minikube" labeled
注意
如果你不是使用 Minikube,将 minikube 替换为你节点的一个名称。
DaemonSet 现在应该创建了一个 pod。让我们看看:
$ kubectl get po NAME READY STATUS RESTARTS AGE ssd-monitor-hgxwq 1/1 Running 0 35s
好吧;到目前为止一切顺利。如果你有多个节点,并且你给更多的节点添加相同的标签,你会看到 DaemonSet 会为每个节点启动 pod。
从节点移除所需的标签
现在,假设你犯了一个错误,错误地标记了一个节点。它有一个旋转的磁盘驱动器,而不是 SSD。如果你更改节点的标签会发生什么?
$ kubectl label node minikube disk=hdd --overwrite node "minikube" labeled
让我们看看这个变化是否对那个节点上运行的 pod 有任何影响:
$ kubectl get po NAME READY STATUS RESTARTS AGE ssd-monitor-hgxwq 1/1 Terminating 0 4m
Pod 正在终止。但你知道这是要发生的,对吧?这标志着你对 DaemonSet 的探索结束,所以你可能想要删除你的 ssd-monitor DaemonSet。如果你还有其他正在运行的 daemon pod,你会看到删除 DaemonSet 也会删除那些 pod。
4.5. 运行执行单个可完成任务的 pod
到目前为止,我们只讨论了需要持续运行的 pod。你会有一些只想运行在完成工作后终止的任务的情况。ReplicationControllers、ReplicaSets 和 DaemonSets 运行的是持续任务,这些任务永远不会被认为已完成。此类 pod 中的进程在退出时会被重启。但在可完成的任务中,在其进程终止后,它不应该再次重启。
4.5.1. 介绍 Job 资源
Kubernetes 通过作业资源支持这一点,这与我们在本章中讨论的其他资源类似,但它允许你运行一个容器,当运行在其中的进程成功完成后,该容器不会被重启。一旦完成,该 Pod 就被认为是完成了。
在节点故障的情况下,由作业管理的该节点上的 Pod 将像 ReplicaSet Pod 一样重新调度到其他节点。如果进程本身发生故障(当进程返回错误退出代码时),可以配置作业是重启容器还是不重启。
图 4.10 展示了如果最初调度的节点失败,由作业创建的 Pod 将如何重新调度到新的节点。该图还显示了两种类型的 Pod:一种是由作业管理的 Pod,它不会被重新调度;另一种是由 ReplicaSet 支持的 Pod,它会被重新调度。
图 4.10. 由作业管理的 Pod 将在成功完成之前重新调度。

例如,作业对于临时任务很有用,在这些任务中,任务正确完成至关重要。你可以在未管理的 Pod 中运行任务并等待其完成,但在节点故障或 Pod 在执行任务时被从节点驱逐的情况下,你需要手动重新创建它。手动这样做没有意义——特别是如果作业需要数小时才能完成。
这样的作业的一个例子是,如果你在某处存储了数据,你需要将其转换并导出到某处。你将通过运行基于 busybox 图像构建的容器镜像来模拟这个过程,该镜像将执行两分钟的 sleep 命令。我已经构建了镜像并将其推送到 Docker Hub,但你可以在本书的代码存档中查看其 Dockerfile。
4.5.2. 定义作业资源
按照以下列表创建作业清单。
列表 4.11. 作业的 YAML 定义:exporter.yaml
apiVersion: batch/v1 1 kind: Job 1 metadata: name: batch-job spec: 2 template: metadata: labels: 2 app: batch-job 2 spec: restartPolicy: OnFailure 3 containers: - name: main image: luksa/batch-job
-
1 作业位于 batch API 组,版本 v1。
-
2 你没有指定 Pod 选择器(它将基于 Pod 模板中的标签创建)。
-
3 作业不能使用默认的重启策略,即 Always。
作业是 batch API 组和 v1 API 版本的一部分。YAML 定义了一个类型为 Job 的资源,该资源将运行 luksa/batch-job 镜像,该镜像调用一个运行恰好 120 秒然后退出的进程。
在 Pod 的规范中,你可以指定当容器中运行的进程结束时 Kubernetes 应该做什么。这是通过restartPolicy Pod 规范属性完成的,默认为Always。作业 Pod 不能使用默认策略,因为它们不是无限期运行的。因此,你需要明确地将重启策略设置为OnFailure或Never。这个设置是防止容器在完成时重启的原因(而不是 Pod 由作业资源管理)。
4.5.3. 观察作业运行 Pod
使用kubectl create命令创建此作业后,你应该立即看到它启动一个 Pod:
$ kubectl get jobs NAME DESIRED SUCCESSFUL AGE batch-job 1 0 2s $ kubectl get po NAME READY STATUS RESTARTS AGE batch-job-28qf4 1/1 Running 0 4s
经过两分钟后,Pod 将不再出现在 Pod 列表中,作业将被标记为完成。默认情况下,完成后的 Pod 在列出 Pod 时不会显示,除非你使用--show-all(或-a)开关:
$ kubectl get po -a NAME READY STATUS RESTARTS AGE batch-job-28qf4 0/1 Completed 0 2m
Pod 在完成时不会被删除的原因是允许你检查其日志;例如:
$ kubectl logs batch-job-28qf4 Fri Apr 29 09:58:22 UTC 2016 Batch job starting Fri Apr 29 10:00:22 UTC 2016 Finished successfully
当你删除它或创建它的作业时,Pod 将被删除。在你这样做之前,让我们再次看看作业资源:
$ kubectl get job NAME DESIRED SUCCESSFUL AGE batch-job 1 1 9m
作业显示为成功完成。但为什么这条信息以数字的形式显示,而不是yes或true?DESIRED列表示什么?
4.5.4. 在作业中运行多个 Pod 实例
作业可以被配置为创建多个 Pod 实例,并可以并行或顺序地运行它们。这是通过在作业规范中设置completions和parallelism属性来完成的。
顺序运行作业 Pod
如果你需要作业运行多次,你可以将completions设置为作业 Pod 需要运行的次数。以下列表显示了一个示例。
列表 4.12. 需要多次完成的作业:multi-completion-batch-job.yaml
apiVersion: batch/v1 kind: Job metadata: name: multi-completion-batch-job spec: completions: 5 1 template: <template is the same as in listing 4.11>
- 1 将完成数设置为 5 使得此作业顺序运行五个 Pod。
此作业将依次运行五个 Pod。它最初创建一个 Pod,当 Pod 的容器完成时,它创建第二个 Pod,依此类推,直到五个 Pod 成功完成。如果一个 Pod 失败,作业将创建一个新的 Pod,因此作业总共可能创建超过五个 Pod。
并行运行作业 Pod
除了依次运行单个作业 Pod 之外,你还可以让作业并行运行多个 Pod。你可以通过parallelism作业规范属性指定允许并行运行的 Pod 数量,如下所示。
列表 4.13. 并行运行作业 Pod:multi-completion-parallel-batch-job.yaml
apiVersion: batch/v1 kind: Job metadata: name: multi-completion-batch-job spec: completions: 5 1 parallelism: 2 2 template: <same as in listing 4.11>
-
1 这个作业必须确保五个 Pod 成功完成。
-
2 最多可以并行运行两个 Pod。
通过将parallelism设置为 2,作业会创建两个 Pod 并在并行运行:
$ kubectl get po NAME READY STATUS RESTARTS AGE multi-completion-batch-job-lmmnk 1/1 Running 0 21s multi-completion-batch-job-qx4nq 1/1 Running 0 21s
一旦其中一个完成,作业将运行下一个 Pod,直到五个 Pod 成功完成。
扩展作业
你甚至可以在作业运行时更改作业的parallelism属性。这类似于扩展 ReplicaSet 或 ReplicationController,可以使用kubectl scale命令来完成:
$ kubectl scale job multi-completion-batch-job --replicas 3 job "multi-completion-batch-job" scaled
由于你已经将parallelism从 2 增加到 3,因此立即启动了另一个 Pod,现在有三个 Pod 正在运行。
4.5.5. 限制作业 Pod 完成所需的时间
我们还需要讨论关于作业的最后一件事。作业应该等待 Pod 完成多长时间?如果 Pod 卡住并且根本无法完成(或者无法足够快地完成)怎么办?
可以通过在 Pod 规范中设置activeDeadlineSeconds属性来限制 Pod 的运行时间。如果 Pod 运行时间超过这个时间,系统将尝试终止它,并将作业标记为失败。
注意
你可以通过在作业清单中指定spec.backoffLimit字段来配置作业在标记为失败之前可以重试的次数。如果你没有明确指定它,则默认为 6。
4.6. 定期或未来一次性运行作业的调度
当你创建作业资源时,作业资源会立即运行其 Pod。但许多批处理作业需要在未来的某个特定时间运行,或者在指定的间隔内重复运行。在 Linux 和 UNIX-like 操作系统上,这些作业通常被称为 cron 作业。Kubernetes 也支持它们。
Kubernetes 中的 cron 作业通过创建 CronJob 资源进行配置。作业运行的计划是在众所周知的 cron 格式中指定的,所以如果你熟悉常规的 cron 作业,你将在几秒钟内理解 Kubernetes 的 CronJobs。
在配置的时间,Kubernetes 将根据 CronJob 对象中配置的工作模板创建一个工作资源。当工作资源被创建时,根据工作 Pod 模板,将创建一个或多个 Pod 副本并启动,正如你在上一节中学到的。除此之外没有其他的事情。
让我们看看如何创建 CronJob。
4.6.1. 创建 CronJob
假设你需要每 15 分钟运行一次你之前示例中的批处理作业。为此,创建一个具有以下规范的 CronJob 资源。
列表 4.14. CronJob 资源 YAML:cronjob.yaml
apiVersion: batch/v1beta1 1 kind: CronJob metadata: name: batch-job-every-fifteen-minutes spec: schedule: "0,15,30,45 * * * *" 2 jobTemplate: spec: template: 3 metadata: 3 labels: 3 app: periodic-batch-job 3 spec: 3 restartPolicy: OnFailure 3 containers: 3 - name: main 3 image: luksa/batch-job 3
-
1 API 组是批处理,版本是 v1beta1
-
2 这个工作应该在每小时的第 0 分钟、第 15 分钟、第 30 分钟和第 45 分钟运行。
-
3 由这个 CronJob 创建的工作资源模板
如你所见,这并不复杂。你已经指定了一个日程安排和一个模板,从这个模板中将会创建工作对象。
配置日程
如果你不太熟悉 cron 日程格式,你可以在网上找到很好的教程和解释,但作为一个快速介绍,从左到右,日程包含以下五个条目:
-
分钟
-
小时
-
月份中的某一天
-
月份
-
星期中的某一天。
在示例中,你希望每 15 分钟运行一次工作,所以日程需要设置为"0,15,30,45 * * * *",这意味着在每个小时的第 0 分钟、第 15 分钟、第 30 分钟和第 45 分钟(第一个星号),每月的每一天(第二个星号),每月的每一天(第三个星号)以及星期的每一天(第四个星号)。
如果你希望它每 30 分钟运行一次,但只在每月的第一天运行,你可以将日程设置为"0,30 * 1 *",如果你想它在每周日早上 3 点运行,你可以设置为"0 3 * * 0"(最后的零代表周日)。
配置工作模板
CronJob 从 CronJob 规范中配置的jobTemplate属性创建工作资源,因此请参阅第 4.5 节以获取有关如何配置的更多信息。
4.6.2. 理解预定作业的运行方式
工作资源将在大约预定时间从 CronJob 资源创建。然后工作会创建 Pod。
可能会出现工作或 Pod 创建和运行相对较晚的情况。你可能对工作开始时间有严格的要求,不能超过预定时间太远。在这种情况下,你可以通过在 CronJob 规范中指定startingDeadlineSeconds字段来设置一个截止日期,如下面的列表所示。
列表 4.15. 为 CronJob 指定startingDeadlineSeconds
apiVersion: batch/v1beta1 kind: CronJob spec: schedule: "0,15,30,45 * * * *" startingDeadlineSeconds: 15 1 ...
-
- 最晚,Pod 必须在预定时间后的 15 秒开始运行。
在列表 4.15 的示例中,作业应该运行的时间之一是 10:30:00。如果由于任何原因在 10:30:15 之前没有启动,作业将不会运行,并显示为失败。
在正常情况下,CronJob 总是为调度中配置的每次执行创建单个作业,但有时可能会同时创建两个作业,或者一个都不创建。为了解决第一个问题,你的作业应该是幂等的(多次运行而不是单次运行不应导致不希望的结果)。对于第二个问题,确保下一个作业运行执行之前(错过)运行应该完成的所有工作。
4.7. 摘要
你现在已经学会了如何在节点故障的情况下保持 Pod 运行并重新调度。你应该现在知道
-
你可以指定一个存活探针,以便 Kubernetes 在容器不再健康时立即重启它(其中应用程序定义了什么被认为是健康的)。
-
Pod 不应该直接创建,因为如果它们被错误删除、运行它们的节点失败或从节点中驱逐,它们将不会被重新创建。
-
ReplicationControllers 总是保持所需数量的 Pod 副本运行。
-
水平扩展 Pod 与在 ReplicationController 上更改所需副本计数一样简单。
-
Pod 不属于 ReplicationControllers,如果需要,可以在它们之间移动。
-
ReplicationController 从 Pod 模板创建新的 Pod。更改模板对现有 Pod 没有影响。
-
应该用 ReplicaSets 和 Deployments 替换 ReplicationControllers,它们提供相同的功能,但具有额外的强大功能。
-
ReplicationControllers 和 ReplicaSets 将 Pod 调度到集群的随机节点,而 DaemonSets 确保每个节点运行一个在 DaemonSet 中定义的 Pod 的单个实例。
-
执行批处理任务的 Pod 应该通过 Kubernetes 作业资源创建,而不是直接创建或通过 ReplicationController 或类似对象创建。
-
需要在未来某个时间运行的作业可以通过 CronJob 资源创建。
第五章. 服务:使客户端能够发现并与服务 Pod 通信
本章涵盖
-
创建服务资源以在单个地址公开一组 Pod
-
在集群中查找服务
-
将服务暴露给外部客户端
-
从集群内部连接到外部服务
-
控制 Pod 是否准备好成为服务的一部分
-
服务故障排除
你已经了解了 Pod 以及如何通过 ReplicaSet 等资源部署它们以确保它们持续运行。尽管某些 Pod 可以在没有外部刺激的情况下独立工作,但如今许多应用程序都是为了响应外部请求而设计的。例如,在微服务的情况下,Pod 通常会响应来自集群内部其他 Pod 或来自集群外部客户端的 HTTP 请求。
如果 Pod 想要消费它们提供的服务,它们需要一种方式来找到其他 Pod。与 Kubernetes 世界之外的情况不同,在那里系统管理员会通过指定客户端配置文件中提供服务的服务器确切的 IP 地址或主机名来配置每个客户端应用程序,在 Kubernetes 中这样做是不行的,因为
-
Pods 是短暂的——它们可能随时出现或消失,无论是由于 Pod 被从节点中移除以腾出空间给其他 Pod,有人减少了 Pod 的数量,还是因为集群节点故障。
-
Kubernetes 在 Pod 被调度到节点并启动之前为其分配一个 IP 地址——因此客户端无法事先知道服务器 Pod 的 IP 地址。
-
水平扩展意味着多个 Pod 可能提供相同的服务——每个 Pod 都有自己的 IP 地址。客户端不需要关心支持服务的 Pod 数量以及它们的 IP 地址。他们不需要保留所有 Pod 的 IP 地址列表。相反,所有这些 Pod 都应该可以通过一个单一的 IP 地址访问。
为了解决这些问题,Kubernetes 还提供了另一种资源类型——服务,我们将在本章中讨论。
5.1. 介绍服务
Kubernetes 服务是一种资源,你创建它来为提供相同服务的多个 Pod 提供一个单一、恒定的入口点。每个服务都有一个 IP 地址和端口,在服务存在期间这些地址和端口不会改变。客户端可以打开到该 IP 和端口的连接,然后这些连接会被路由到支持该服务的某个 Pod。这样,服务的客户端不需要知道提供服务的单个 Pod 的位置,允许这些 Pod 在集群中随时移动。
用例子解释服务
让我们回顾一下这个例子:你有一个前端 Web 服务器和一个后端数据库服务器。可能有多个 Pod 都充当前端,但可能只有一个后端数据库 Pod。你需要解决两个问题才能使系统正常工作:
-
外部客户端需要连接到前端 Pod,无需关心是否只有一个 Web 服务器或数百个。
-
前端 Pod 需要连接到后端数据库。因为数据库运行在 Pod 内部,它可能会随着时间的推移在集群中移动,导致其 IP 地址发生变化。你不想每次后端数据库移动时都重新配置前端 Pod。
通过为前端 Pod 创建一个服务并配置它可以从集群外部访问,你暴露了一个单一的、恒定的 IP 地址,外部客户端可以通过这个 IP 地址连接到 Pod。同样,通过为后端 Pod 也创建一个服务,你为后端 Pod 创建了一个稳定的地址。即使 Pod 的 IP 地址发生变化,服务地址也不会改变。此外,通过创建服务,你还可以使前端 Pod 能够通过环境变量或 DNS 通过名称轻松找到后端服务。你的系统中的所有组件(两个服务、支持这些服务的两个 Pod 集合以及它们之间的相互依赖关系)都在图 5.1 中展示。
图 5.1. 内部和外部客户端通常通过服务连接到 Pod。

现在,你已经理解了服务背后的基本概念。现在,让我们通过首先了解它们是如何被创建的来深入探讨。
5.1.1. 创建服务
正如你所看到的,服务可以由多个 Pod 支持。对服务的连接在所有支持 Pod 之间进行负载均衡。但你是如何定义哪些 Pod 是服务的一部分,哪些不是的呢?
你可能还记得标签选择器和它们如何在 Replication-Controllers 和其他 Pod 控制器中使用,以指定哪些 Pod 属于同一个集合。服务以相同的方式使用相同的机制,正如你在图 5.2 中可以看到的那样。
图 5.2. 标签选择器确定哪些 Pod 属于服务。

在上一章中,你创建了一个 ReplicationController,然后运行了包含 Node.js 应用的 Pod 的三个实例。再次创建 ReplicationController 并验证三个 Pod 实例是否启动并运行。之后,你将为这三个 Pod 创建一个 Service。
通过 kubectl expose 创建服务
创建服务最简单的方法是通过 kubectl expose,你已经在第二章中使用它来暴露你之前创建的 ReplicationController。expose 命令创建了一个与 ReplicationController 使用相同的 pod 选择器的 Service 资源,从而通过单个 IP 地址和端口暴露了所有 Pod。
现在,你将不再使用 expose 命令,而是通过向 Kubernetes API 服务器提交 YAML 来手动创建服务。
通过 YAML 描述符创建服务
创建一个名为 kubia-svc.yaml 的文件,并包含以下内容列表。
列表 5.1. 服务的定义:kubia-svc.yaml
apiVersion: v1 kind: Service metadata: name: kubia spec: ports: - port: 80 1 targetPort: 8080 2 selector: 3 app: kubia 3
-
1 该服务将可用的端口
-
2 服务将转发的容器端口
-
3 所有带有 app=kubia 标签的 Pod 都将成为此服务的一部分。
您正在定义一个名为kubia的服务,该服务将在端口 80 上接受连接,并将每个连接路由到匹配app=kubia标签选择器的 Pod 的端口 8080。
使用kubectl create上传文件来创建服务。
检查您的新服务
上传 YAML 文件后,您可以列出您命名空间中的所有服务资源,并看到已为您的服务分配了一个内部集群 IP:
$ kubectl get svc NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes 10.111.240.1 <none> 443/TCP 30d kubia 10.111.249.153 <none> 80/TCP 6m 1
- 1 这里是您的服务。
列表显示分配给服务的 IP 地址是 10.111.249.153。因为这是集群 IP,所以它只能在集群内部访问。服务的主要目的是将 Pod 组暴露给集群中的其他 Pod,但您通常还希望将服务外部暴露。您将在稍后看到如何做到这一点。现在,让我们从集群内部使用您的服务并查看它的工作情况。
在集群内部测试您的服务
您可以通过几种方式在集群内部向您的服务发送请求:
-
明显的方法是创建一个 Pod,它会将请求发送到服务的集群 IP 并记录响应。然后您可以检查 Pod 的日志以查看服务的响应。
-
您可以
ssh连接到 Kubernetes 的一个节点并使用curl命令。 -
您可以通过
kubectl exec命令在您的现有 Pod 中执行curl命令。
让我们选择最后一个选项,这样您也可以学习如何在现有 Pod 中运行命令。
在运行容器中远程执行命令
kubectl exec命令允许您在 Pod 的现有容器中远程运行任意命令。当您想检查容器的内容、状态和/或环境时,这非常有用。使用kubectl get pods命令列出 Pod,并选择一个作为exec命令的目标(在以下示例中,我选择了kubia-7nog1 Pod 作为目标)。您还需要获取您服务的集群 IP(例如,使用kubectl get svc)。当您自己运行以下命令时,请确保将 Pod 名称和服务 IP 替换为您自己的:
$ kubectl exec kubia-7nog1 -- curl -s http://10.111.249.153 您已连接到 kubia-gzwli
如果您之前使用ssh在远程系统上执行过命令,您会认识到kubectl exec并没有太大区别。
为什么是双横线?
命令中的双横线(--)表示kubectl命令选项的结束。双横线之后是应该在 Pod 内部执行的命令。如果没有以短横线开头的参数,则不需要使用双横线。但在你的情况下,如果你不使用双横线,-s选项将被解释为kubectl exec的选项,并导致以下奇怪且极具误导性的错误:
$ kubectl exec kubia-7nog1 curl -s http://10.111.249.153 连接到服务器 10.111.249.153 被拒绝 – 你是否指定了正确的宿主或端口?
这与你的服务拒绝连接无关。这是因为kubectl无法连接到 10.111.249.153(API 服务器)的 API 服务器(-s选项用于告诉kubectl连接到非默认的 API 服务器)。
让我们回顾一下当你运行命令时发生了什么。图 5.3 显示了事件的序列。你指示 Kubernetes 在其中一个 Pod 的容器内执行curl命令。Curl 向服务 IP 发送了一个 HTTP 请求,该 IP 由三个 Pod 支持。Kubernetes 服务代理拦截了连接,从三个 Pod 中随机选择了一个 Pod,并将请求转发给它。然后在该 Pod 内部运行的 Node.js 处理了请求,并返回了一个包含 Pod 名称的 HTTP 响应。Curl 随后将响应打印到标准输出,然后由kubectl拦截并打印到你的本地机器的标准输出。
图 5.3. 使用 kubectl exec 通过在 Pod 中运行 curl 测试对服务的连接

在上一个例子中,你作为单独的进程执行了curl命令,但它在 Pod 的主容器内部。这与容器中的实际主进程与服务的通信并没有太大的区别。
在服务上配置会话亲和性
如果你多次执行相同的命令,你应该在每次调用时遇到不同的 Pod,因为服务代理通常将每个连接转发到随机选择的支撑 Pod,即使连接来自同一客户端。
另一方面,如果你希望某个客户端发出的所有请求每次都重定向到同一个 Pod,你可以将服务的sessionAffinity属性设置为ClientIP(而不是默认的None),如下所示。
列表 5.2. 配置了ClientIP会话亲和性的服务示例
apiVersion: v1 kind: Service spec: sessionAffinity: ClientIP ...
这使得服务代理将所有来自同一客户端 IP 的请求重定向到同一个 Pod。作为一个练习,你可以创建一个额外的服务,将会话亲和性设置为ClientIP,并尝试向它发送请求。
Kubernetes 只支持两种类型的服务会话亲和性:None和ClientIP。你可能对它没有基于 cookie 的会话亲和性选项感到惊讶,但你需要理解 Kubernetes 服务不在 HTTP 级别上操作。服务处理 TCP 和 UDP 数据包,并不关心它们携带的负载。因为 cookie 是 HTTP 协议的一部分,服务不知道它们,这也解释了为什么会话亲和性不能基于 cookie。
在同一服务中暴露多个端口
你的服务只暴露单个端口,但服务也可以支持多个端口。例如,如果你的 Pod 监听两个端口——假设 8080 用于 HTTP 和 8443 用于 HTTPS——你可以使用单个服务将端口 80 和 443 都转发到 Pod 的端口 8080 和 8443。在这种情况下,你不需要创建两个不同的服务。使用单个多端口服务可以通过单个集群 IP 暴露所有服务端口。
注意
当创建具有多个端口的 服务时,你必须为每个端口指定一个名称。
多端口服务的规范如下所示。
列表 5.3. 在服务定义中指定多个端口
apiVersion: v1 kind: Service metadata: name: kubia spec: ports: - name: http 1 port: 80 1 targetPort: 8080 1 - name: https 2 port: 443 2 targetPort: 8443 2 selector: 3 app: kubia 3
-
端口 80 映射到 Pod 的端口 8080。
-
端口 443 映射到 Pod 的端口 8443。
-
标签选择器始终应用于整个服务。
注意
标签选择器应用于整个服务——不能为每个端口单独配置。如果你想让不同的端口映射到不同的 Pod 子集,你需要创建两个服务。
因为你的kubia Pods 没有监听多个端口,创建多端口服务和多端口 Pod 留作练习。
使用命名端口
在所有这些示例中,你都是通过端口号来引用目标端口的,但你也可以为每个 Pod 的端口命名,并在服务规范中通过名称引用它。这使得服务规范稍微清晰一些,尤其是如果端口号不是很知名的话。
例如,假设你的 Pod 定义了如下所示端口名称。
列表 5.4. 在 Pod 定义中指定端口名称
kind: Pod spec: containers: - name: kubia ports: - name: http 1 containerPort: 8080 1 - name: https 2 containerPort: 8443 2
-
容器的端口 8080 被称为 http
-
端口 8443 被称为 https。
你可以在服务规范中通过名称引用这些端口,如下所示。
列表 5.5. 在服务中引用命名端口
apiVersion: v1 kind: Service spec: ports: - name: http 1 port: 80 1 targetPort: http 1 - name: https 2 port: 443 2 targetPort: https 2
-
1 端口 80 映射到名为 http 的容器端口。
-
端口 443 映射到名为 https 的容器端口。
但你为什么要费心去命名端口呢?这样做最大的好处是,它允许你在以后更改端口号,而无需更改服务规范。你的 Pod 当前使用 8080 端口进行 http 通信,但如果你后来决定想将其移动到 80 端口怎么办?
如果你正在使用命名端口,你只需要更改 Pod 规范中的端口号(同时保持端口号名称不变)。当你启动具有新端口号的 Pod 时,客户端连接将根据接收连接的 Pod(旧 Pod 上的 8080 端口和新 Pod 上的 80 端口)被转发到相应的端口号。
5.1.2. 发现服务
通过创建服务,你现在有一个单一且稳定的 IP 地址和端口号,你可以通过它来访问你的 Pod。在整个服务生命周期中,这个地址将保持不变。位于此服务后面的 Pod 可能会来来去去,它们的 IP 可能会改变,它们的数量可能会增加或减少,但它们将通过服务的单一且恒定的 IP 地址始终可访问。
但客户端 Pod 如何知道服务的 IP 和端口号呢?你需要先创建服务,然后手动查找其 IP 地址,并将其传递给客户端 Pod 的配置选项吗?实际上并不需要。Kubernetes 还提供了客户端 Pod 发现服务 IP 和端口号的方法。
通过环境变量发现服务
当 Pod 启动时,Kubernetes 初始化一组环境变量,指向当时存在的每个服务。如果你在创建客户端 Pod 之前创建服务,那些 Pod 中的进程可以通过检查它们的环境变量来获取服务的 IP 地址和端口号。
让我们通过检查你运行中的 Pod 的环境来查看这些环境变量是什么样的。你已经了解到你可以使用kubectl exec命令在 Pod 中运行命令,但由于你是在 Pod 创建后才创建服务的,因此服务环境变量还没有被设置。首先你需要解决这个问题。
在你能够看到服务环境变量之前,首先需要删除所有 Pod,并让 ReplicationController 创建新的 Pod。你可能记得你可以这样不指定 Pod 名称来删除所有 Pod:
$ kubectl delete po --all pod "kubia-7nog1" deleted pod "kubia-bf50t" deleted pod "kubia-gzwli" deleted
现在,你可以列出新的 Pod(我确信你知道如何做),并选择一个作为kubectl exec命令的目标。一旦你选择了目标 Pod,你可以在容器内部运行env命令来列出环境变量,如下所示。
列表 5.6. 容器中的服务相关环境变量
$ kubectl exec kubia-3inly env PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin HOSTNAME=kubia-3inly KUBERNETES_SERVICE_HOST=10.111.240.1 KUBERNETES_SERVICE_PORT=443 ... KUBIA_SERVICE_HOST=10.111.249.153``1``KUBIA_SERVICE_PORT=80``2 ...
-
1 这里是服务的集群 IP。
-
2 这里是服务可用的端口。
在你的集群中定义了两个服务:kubernetes和kubia服务(你之前通过kubectl get svc命令看到了这一点);因此,有两个与服务相关的环境变量集。在章节开头创建的kubia服务相关的变量中,你会看到KUBIA_SERVICE_HOST和KUBIA_SERVICE_PORT环境变量,分别持有kubia服务的 IP 地址和端口。
回到本章开始时我们讨论的前端-后端示例,当你有一个需要使用后端数据库服务器 Pod 的前端 Pod 时,你可以通过一个名为backend-database的服务来暴露后端 Pod,然后让前端 Pod 通过环境变量BACKEND_DATABASE_SERVICE_HOST和BACKEND_DATABASE_SERVICE_PORT查找其 IP 地址和端口。
注意
当服务名称用作环境变量名称的前缀时,服务名称中的破折号会被转换为下划线,并且所有字母都会转换为大写。
环境变量是查找服务 IP 和端口的一种方式,但这通常不是 DNS 的领域吗?为什么 Kubernetes 不包含一个 DNS 服务器并允许你通过 DNS 查找服务 IP 呢?实际上,它确实包含了!
通过 DNS 发现服务
记得在第三章中你列出了kube-system命名空间中的 Pod 吗?其中一个 Pod 被称作kube-dns。kube-system命名空间还包括一个同名的对应服务。
如其名所示,该 Pod 运行一个 DNS 服务器,集群中所有其他运行的 Pod 都会自动配置使用该服务器(Kubernetes 通过修改每个容器的/etc/resolv.conf文件来实现这一点)。在 Pod 中运行的任何进程执行的 DNS 查询都将由 Kubernetes 自己的 DNS 服务器处理,该服务器了解系统中运行的所有服务。
注意
是否使用内部 DNS 服务器是由每个 Pod 的 spec 中的dnsPolicy属性配置的。
每个服务在内部 DNS 服务器中都有一个 DNS 条目,知道服务名称的客户 Pod 可以通过其完全限定域名(FQDN)访问它,而不是求助于环境变量。
通过 FQDN 连接到服务
为了回顾前端-后端示例,前端 Pod 可以通过打开以下 FQDN 的连接来连接到后端-database 服务:
backend-database.default.svc.cluster.local
backend-database对应于服务名,default表示服务定义的命名空间,而svc.cluster.local是用于所有集群本地服务名的可配置集群域名后缀。
注意
客户端仍然需要知道服务的端口号。如果服务使用标准端口(例如,HTTP 的 80 或 Postgres 的 5432),这通常不会成问题。如果不是,客户端可以从环境变量中获取端口号。
连接到服务甚至可以比这更简单。当前端 pod 与数据库 pod 位于同一命名空间时,你可以省略svc.cluster.local后缀甚至命名空间。因此,你可以简单地通过backend-database来引用服务。这真是太简单了,对吧?
让我们尝试一下。你将尝试通过其 FQDN 而不是 IP 访问kubia服务。同样,你需要在现有的 pod 内部进行此操作。你已经知道如何使用kubectl exec在 pod 的容器中运行单个命令,但这次,你将运行bash shell 而不是直接运行curl命令,这样你就可以在容器中运行多个命令。这与你使用docker exec -it bash命令进入使用 Docker 运行的容器时在第二章中做的事情类似。
在 pod 的容器中运行 shell
你可以使用kubectl exec命令在 pod 的容器内运行bash(或任何其他 shell)。这样,你可以自由地探索容器,而无需为每个要运行的命令执行kubectl exec。
注意
为了使 shell 正常工作,shell 的二进制可执行文件必须在容器镜像中可用。
要正确使用 shell,你需要将-it选项传递给kubectl exec:
$ kubectl exec -it kubia-3inly bash root@kubia-3inly:/#
你现在已经在容器内部了。你可以使用curl命令以下任何一种方式访问kubia服务:
root@kubia-3inly:/# curl http://kubia.default.svc.cluster.local You've hit kubia-5asi2 root@kubia-3inly:/# curl http://kubia.default You've hit kubia-3inly root@kubia-3inly:/# curl http://kubia You've hit kubia-8awf3
你可以使用服务名作为请求 URL 中的主机名来访问你的服务。你可以省略命名空间和svc.cluster.local后缀,因为每个 pod 容器内部的 DNS 解析器是如何配置的。查看容器中的/etc/resolv.conf文件,你就会明白:
root@kubia-3inly:/# cat /etc/resolv.conf search default.svc.cluster.local svc.cluster.local cluster.local ...
理解为什么无法 ping 通服务 IP
在我们继续之前,还有最后一件事。你现在知道如何创建服务了,所以你很快就会创建自己的服务。但如果你因为任何原因无法访问你的服务怎么办?
你可能会尝试通过进入现有的 Pod 并尝试像上一个例子中那样访问服务来找出问题所在。然后,如果你仍然无法使用简单的curl命令访问服务,你可能尝试 ping 服务 IP 以查看它是否在线。我们现在试试看:
root@kubia-3inly:/# ping kubia PING kubia.default.svc.cluster.local (10.111.249.153): 56 data bytes ^C--- kubia.default.svc.cluster.local ping statistics --- 54 packets transmitted, 0 packets received, 100% packet loss
嗯。使用curl访问服务是可行的,但 ping 它却不行。这是因为服务的集群 IP 是一个虚拟 IP,只有与服务端口结合时才有意义。我们将在第十一章(index_split_087.html#filepos1036287)中解释这意味着什么以及服务是如何工作的。我想在这里提一下,因为这是用户在尝试调试损坏的服务时做的第一件事,而且它会让大多数人感到措手不及。
5.2. 连接到集群外部的服务
到目前为止,我们讨论了由集群内部运行的一个或多个 Pod 支持的服务。但存在一些情况,你可能希望通过 Kubernetes 服务功能公开外部服务。你不想让服务将连接重定向到集群中的 Pod,而是希望它重定向到外部 IP 和端口。
这允许你利用服务负载均衡和服务发现的优势。运行在集群中的客户端 Pod 可以像连接内部服务一样连接到外部服务。
5.2.1. 介绍服务端点
在进入如何实现这一点之前,让我首先更详细地解释一下服务。服务并不直接链接到 Pod。相反,一个资源位于其中间——Endpoints 资源。如果你在服务上使用了kubectl describe命令,你可能已经注意到了端点,如下所示。
列表 5.7. 使用kubectl describe显示的服务详细信息
$ kubectl describe svc kubia Name: kubia Namespace: default Labels: <none> Selector: app=kubia 1 Type: ClusterIP IP: 10.111.249.153 Port: <unset> 80/TCP Endpoints: 10.108.1.4:8080,10.108.2.5:8080,10.108.2.6:8080 2 Session Affinity: None No events.
-
1 该服务的 Pod 选择器用于创建端点列表。
-
2 代表此服务端点的 Pod IP 和端口列表
Endpoints 资源(是的,复数形式)是一个列出暴露服务的 IP 地址和端口的列表。Endpoints 资源就像其他 Kubernetes 资源一样,因此你可以使用kubectl get来显示其基本信息:
$ kubectl get endpoints kubia NAME ENDPOINTS AGE kubia 10.108.1.4:8080,10.108.2.5:8080,10.108.2.6:8080 1h
虽然 pod 选择器在服务规范中定义,但在重定向传入连接时并不直接使用。相反,选择器用于构建一个 IP 和端口号列表,然后存储在端点资源中。当客户端连接到服务时,服务代理会从这些 IP 和端口号对中选择一个,并将传入的连接重定向到在该位置监听的服务器。
5.2.2. 手动配置服务端点
你可能已经意识到了这一点,但将服务的端点与服务解耦允许它们手动配置和更新。
如果你创建一个没有 pod 选择器的服务,Kubernetes 甚至不会创建端点资源(毕竟,没有选择器,它不知道要包含哪些 pod 在服务中)。创建端点资源以指定服务端点列表的责任在你。
要创建手动管理的端点服务,你需要创建一个服务和端点资源。
创建没有选择器的服务
你首先需要创建服务本身的 YAML,如下所示。
列表 5.8. 没有 pod 选择器的服务:external-service.yaml
apiVersion: v1 kind: Service metadata: name: external-service 1 spec: 2 ports: - port: 80
-
1 服务的名称必须与端点对象的名称匹配(见下一列表)。
-
2 此服务未定义选择器。
你正在定义一个名为external-service的服务,该服务将在端口 80 上接受传入连接。你没有为服务定义 pod 选择器。
为没有选择器的服务创建端点资源
端点是单独的资源,而不是服务的属性。因为你创建的服务没有选择器,相应的端点资源还没有自动创建,所以你需要自己创建它。下面的列表显示了它的 YAML 表示。
列表 5.9. 手动创建的端点资源:external-service-endpoints.yaml
apiVersion: v1 kind: Endpoints metadata: name: external-service 1 subsets: - addresses: - ip: 11.11.11.11 2 - ip: 22.22.22.22 2 ports: - port: 80 3
-
1 端点对象的名称必须与服务的名称匹配(见上一列表)。
-
2 服务将转发连接到的端点 IP
-
3 端点的目标端口
端点对象需要与服务的名称相同,并包含服务的目标 IP 地址和端口号列表。在服务和端点资源都提交到服务器后,服务就可以像任何带有 pod 选择器的常规服务一样使用了。在服务创建后创建的容器将包含服务的环境变量,并且所有连接到其 IP:端口对的连接将在服务的端点之间进行负载均衡。
图 5.4 显示了三个 pod 连接到具有外部端点的服务。
图 5.4. 消费具有两个外部端点的服务的 Pod。

如果您后来决定将外部服务迁移到在 Kubernetes 内运行的 Pod 中,您可以在服务中添加一个选择器,从而自动管理其端点。反之亦然——通过从服务中移除选择器,Kubernetes 停止更新其端点。这意味着服务 IP 地址可以保持不变,而服务的实际实现可以更改。
5.2.3. 创建外部服务的别名
与手动配置服务的端点以暴露外部服务相比,一种更简单的方法允许您通过其完全限定域名(FQDN)来引用外部服务。
创建一个 ExternalName 服务
要创建一个作为外部服务别名的服务,您需要创建一个 type 字段设置为 ExternalName 的服务资源。例如,让我们假设有一个公开的 API 在 api.somecompany.com 上可用。您可以定义一个指向它的服务,如下所示。
列表 5.10. ExternalName 类型的服务:external-service-externalname.yaml
apiVersion: v1 kind: Service metadata: name: external-service spec: type: ExternalName 1 externalName: someapi.somecompany.com 2 ports: - port: 80
-
1 服务类型设置为 ExternalName
-
2 实际服务的完全限定域名
服务创建后,Pod 可以通过 external-service.default.svc.cluster.local 域名(或甚至 external-service)连接到外部服务,而不是使用服务的实际 FQDN。这隐藏了实际服务名称及其位置,从而允许您在以后任何时候仅通过更改 externalName 属性或将类型更改为 ClusterIP 并为服务创建一个端点对象(手动或通过在服务上指定标签选择器并自动创建)来修改服务定义并指向不同的服务。
ExternalName 服务仅在 DNS 层面实现——为服务创建一个简单的 CNAME DNS 记录。因此,连接到服务的客户端将直接连接到外部服务,完全绕过服务代理。因此,这些类型的服务甚至没有集群 IP。
注意
CNAME 记录指向一个完全限定域名,而不是一个数字 IP 地址。
5.3. 向外部客户端暴露服务
到目前为止,我们只讨论了服务如何被集群内部的 Pod 消费。但您可能还想将某些服务,如前端 web 服务器,暴露给外部,以便外部客户端可以访问它们,如图 5.5 所示。
图 5.5. 向外部客户端暴露服务

您有几种方法可以使服务对外部可访问:
-
将服务类型设置为
NodePort——对于NodePort服务,每个集群节点在其自身上打开一个端口(因此得名),并将该端口接收到的流量重定向到底层服务。服务不仅可以通过内部集群 IP 和端口访问,还可以通过所有节点上的专用端口访问。 -
将服务类型设置为
LoadBalancer,这是NodePort类型的扩展——这使得服务可以通过一个专用的负载均衡器访问,该负载均衡器由 Kubernetes 运行的云基础设施提供。负载均衡器将流量重定向到所有节点上的节点端口。客户端通过负载均衡器的 IP 连接到服务。 -
创建一个 Ingress 资源,这是一种通过单个 IP 地址公开多个服务的根本不同的机制——它在 HTTP 层(网络层 7)上运行,因此可以提供比层 4 服务更多的功能。我们将在 第 5.4 节 解释 Ingress 资源。
5.3.1. 使用 NodePort 服务
将一组 pod 公开给外部客户端的第一种方法是创建一个服务并将其类型设置为 NodePort。通过创建 NodePort 服务,你让 Kubernetes 在所有节点上保留一个端口(所有这些节点都使用相同的端口号)并将传入的连接转发到属于该服务的 pod。
这与常规服务类似(它们的实际类型是 ClusterIP),但 NodePort 服务不仅可以通过服务的内部集群 IP 访问,还可以通过任何节点的 IP 和保留的节点端口访问。
当你尝试与 NodePort 服务交互时,这会更有意义。
创建 NodePort 服务
你现在将创建一个 NodePort 服务来查看如何使用它。以下列表显示了服务的 YAML 格式。
列表 5.11. NodePort 服务定义:kubia-svc-nodeport.yaml
apiVersion: v1 kind: Service metadata: name: kubia-nodeport spec: type: NodePort 1 ports: - port: 80 2 targetPort: 8080 3 nodePort: 30123 4 selector: app: kubia
-
1 将服务类型设置为 NodePort。
-
2 这是服务内部集群 IP 的端口。
-
3 这是支持 pod 的目标端口。
-
4 服务将通过您每个集群节点的 30123 端口访问。
你将类型设置为 NodePort 并指定该服务应在所有集群节点上绑定的节点端口。指定端口不是强制性的;如果你省略它,Kubernetes 将选择一个随机端口。
注意
当你在 GKE 中创建服务时,kubectl 会打印出一个警告,关于需要配置防火墙规则。我们很快就会看到如何做。
检查你的 NodePort 服务
让我们查看您服务的详细信息,以了解更多信息:
$ kubectl get svc kubia-nodeport NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubia-nodeport 10.111.254.223 <nodes> 80:30123/TCP 2m
查看外部 IP 列。它显示<nodes>,表示服务可以通过任何集群节点的 IP 地址访问。端口(PORT(S))列显示了集群 IP 的内部端口(80)和节点端口(30123)。服务可通过以下地址访问:
-
10.11.254.223:80 -
<1st node's IP>:30123 -
<2nd node's IP>:30123,等等。
图 5.6 显示了您的服务在两个集群节点的 30123 端口上公开(如果您在 GKE 上运行此操作,则适用;Minikube 只有一个节点,但原理相同)。连接到这些端口之一的传入连接将被重定向到随机选择的 Pod,这可能或可能不是连接到的节点上运行的 Pod。
图 5.6. 外部客户端通过节点 1 或 2 连接到 NodePort 服务

在第一个节点的 30123 端口上接收到的连接可能会被转发到第一个节点上运行的 Pod,或者转发到第二个节点上运行的 Pod 之一。
更改防火墙规则以允许外部客户端访问我们的 NodePort 服务
如我之前所述,在您可以通过节点端口访问服务之前,您需要配置 Google Cloud Platform 的防火墙,以允许在该端口上对您的节点进行外部连接。您现在将这样做:
$ gcloud compute firewall-rules create kubia-svc-rule --allow=tcp:30123 Created [https://www.googleapis.com/compute/v1/projects/kubia-1295/global/firewalls/kubia-svc-rule]. NAME NETWORK SRC_RANGES RULES SRC_TAGS TARGET_TAGS kubia-svc-rule default 0.0.0.0/0 tcp:30123
您可以通过节点 IP 的 30123 端口之一访问您的服务。但您首先需要找出节点的 IP。有关如何操作的说明,请参考侧边栏。
使用 JSONPath 获取所有节点的 IP 地址
您可以在节点的 JSON 或 YAML 描述符中找到 IP 地址。但您不必在相对较大的 JSON 中筛选,您可以告诉kubectl仅打印节点 IP 而不是整个服务定义:
$ kubectl get nodes -o jsonpath='{.items[*].status.
addresses[?(@.type=="ExternalIP")].address}' 130.211.97.55 130.211.99.206
您通过指定 JSONPath 来告诉kubectl仅输出您想要的信息。您可能熟悉 XPath 及其在 XML 中的应用。JSONPath 基本上是 XPath 的 JSON 版本。上一个示例中的 JSONPath 指示kubectl执行以下操作:
-
遍历
items属性中的所有元素。 -
对于每个元素,进入
status属性。 -
过滤
addresses属性的元素,仅选择那些将type属性设置为ExternalIP的元素。 -
最后,打印过滤元素的
address属性。
要了解有关如何使用kubectl与 JSONPath 的更多信息,请参阅kubernetes.io/docs/user-guide/jsonpath文档。
一旦您知道了节点的 IP 地址,您可以通过它们尝试访问您的服务:
$ curl http://130.211.97.55:30123 您已访问 kubia-ym8or $ curl http://130.211.99.206:30123 您已访问 kubia-xueq1
提示
当使用 Minikube 时,您可以通过运行 minikube service <service-name> [-n <namespace>] 命令,轻松通过浏览器访问您的 NodePort 服务。
如您所见,您的 pod 现在可以通过您任何节点的 30123 端口访问整个互联网。客户端发送请求到哪个节点无关紧要。但是,如果您只将客户端指向第一个节点,当该节点失败时,客户端将无法再访问服务。这就是为什么在节点前放置负载均衡器以确保您正在将请求分散到所有健康节点,并且永远不会将请求发送到当时离线的节点是有意义的。
如果您的 Kubernetes 集群支持此功能(当 Kubernetes 部署在云基础设施上时通常支持),则可以通过创建一个 LoadBalancer 而不是 NodePort 服务来自动配置负载均衡器。我们将在下一节中探讨这一点。
5.3.2. 通过外部负载均衡器公开服务
在云提供商上运行的 Kubernetes 集群通常支持从云基础设施自动配置负载均衡器。您需要做的只是将服务的类型设置为 LoadBalancer 而不是 NodePort。负载均衡器将拥有自己的唯一、公开可访问的 IP 地址,并将所有连接重定向到您的服务。因此,您可以通过负载均衡器的 IP 地址访问您的服务。
如果 Kubernetes 运行在不支持 LoadBalancer 服务的环境中,则不会配置负载均衡器,但服务仍将像 NodePort 服务一样运行。这是因为 LoadBalancer 服务是 NodePort 服务的扩展。您将在支持 LoadBalancer 服务的 Google Kubernetes Engine 上运行此示例。Minikube 不支持,至少在本写作时如此。
创建负载均衡器服务
要创建一个前面有负载均衡器的服务,请从以下 YAML 清单创建服务,如下所示。
列表 5.12. LoadBalancer 类型服务:kubia-svc-loadbalancer.yaml
apiVersion: v1 kind: Service metadata: name: kubia-loadbalancer spec: type: LoadBalancer 1 ports: - port: 80 targetPort: 8080 selector: app: kubia
- 1 此类服务从托管 Kubernetes 集群的底层基础设施中获取负载均衡器。
服务类型设置为 LoadBalancer 而不是 NodePort。您没有指定特定的节点端口,尽管您可以(您让 Kubernetes 选择一个)。
通过负载均衡器连接到服务
在创建服务后,云基础设施需要时间来创建负载均衡器并将它的 IP 地址写入服务对象。一旦完成,IP 地址将作为服务的公网 IP 地址列出:
$ kubectl get svc kubia-loadbalancer NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubia-loadbalancer 10.111.241.153 130.211.53.173`` 80:32143/TCP 1m
在这种情况下,负载均衡器在 IP 130.211.53.173 上可用,因此你现在可以通过该 IP 地址访问服务:
$ curl http://130.211.53.173 你访问了 kubia-xueq1
成功!正如你可能注意到的,这次你不需要像之前使用NodePort服务那样去配置防火墙。
会话亲和性与网页浏览器
因为你的服务现在已对外公开,你可以尝试使用你的网页浏览器访问它。你会发现一些可能让你觉得奇怪的事情——浏览器每次都会访问到完全相同的 Pod。服务在这期间是否改变了会话亲和性?使用kubectl explain,你可以再次确认服务的会话亲和性仍然设置为None,那么为什么不同的浏览器请求没有击中不同的 Pod,就像使用curl时那样呢?
让我解释一下正在发生的事情。浏览器正在使用持久连接,并通过单个连接发送所有请求,而curl每次都会打开一个新的连接。服务在连接级别上工作,所以当第一次打开到服务的连接时,会随机选择一个 Pod,然后所有属于该连接的网络包都会发送到那个单个 Pod。即使会话亲和性设置为None,用户也总是会击中同一个 Pod(直到连接关闭)。
见图 5.7 了解 HTTP 请求是如何被发送到 Pod 的。外部客户端(在你的情况下是curl)连接到负载均衡器的 80 端口,并被路由到某个节点上隐式分配的节点端口。从那里,连接被转发到 Pod 实例之一。
图 5.7. 一个外部客户端连接到LoadBalancer服务

如前所述,LoadBalancer类型的服务是一个带有额外基础设施提供的负载均衡器的NodePort服务。如果你使用kubectl describe来显示有关服务的更多信息,你会看到为服务选择了一个节点端口。如果你像在关于NodePort服务的上一节中那样为该端口打开防火墙,你也可以通过节点 IP 访问服务。
小贴士
如果你使用 Minikube,即使负载均衡器永远不会被配置,你仍然可以通过节点端口(在 Minikube 虚拟机的 IP 地址)访问服务。
5.3.3. 理解外部连接的特有之处
你必须注意与外部发起的服务连接相关的几个事项。
理解和防止不必要的网络跳转
当外部客户端通过节点端口连接到服务(这也包括首先通过负载均衡器的情况),随机选择的 Pod 可能或可能不在接收连接的同一节点上运行。需要额外的网络跳转才能到达 Pod,但这可能并不总是希望的。
您可以通过配置服务,仅将外部流量重定向到运行在接收连接的节点上的 Pod 来防止这种额外的跳转。这是通过在服务的spec部分设置externalTrafficPolicy字段来完成的:
spec: externalTrafficPolicy: Local ...
如果服务定义包括此设置,并且通过服务的节点端口打开外部连接,服务代理将选择本地运行的 Pod。如果没有本地 Pod 存在,连接将挂起(不会像不使用注释时那样将连接转发到随机全局 Pod)。因此,您需要确保负载均衡器仅将连接转发到至少有一个此类 Pod 的节点。
使用此注释也有其他缺点。通常,连接会在所有 Pod 之间均匀分配,但使用此注释时,情况就不再是这样了。
想象有两个节点和三个 Pod。假设节点 A 运行一个 Pod,节点 B 运行另外两个 Pod。如果负载均衡器将连接均匀地分配到两个节点,节点 A 上的 Pod 将接收所有连接的 50%,但节点 B 上的两个 Pod 每个只能接收 25%,如图 5.8 所示。
图 5.8. 使用Local外部流量策略的服务可能导致 Pod 之间的负载分布不均。

了解客户端 IP 不保留的情况
通常,当集群内的客户端连接到服务时,支持服务的 Pod 可以获取客户端的 IP 地址。但通过节点端口接收连接时,数据包的源 IP 会改变,因为数据包在源网络地址转换(SNAT)上执行。
支持 Pod 看不到实际的客户端 IP,这可能对一些需要知道客户端 IP 的应用程序来说是个问题。例如,对于 Web 服务器来说,这意味着访问日志不会显示浏览器的 IP。
上一个章节中描述的Local外部流量策略会影响客户端 IP 的保留,因为没有在接收连接的节点和托管目标 Pod 的节点之间增加额外的跳转(不会执行 SNAT)。
5.4. 通过 Ingress 资源外部公开服务
你现在已经看到了两种将服务公开给集群外客户端的方法,但还存在另一种方法——创建 Ingress 资源。
定义
Ingress(名词)——进入或进入的行为;进入的权利;进入的手段或地方;入口。
让我先解释一下为什么你需要另一种从外部访问 Kubernetes 服务的方法。
理解为什么需要 Ingress
一个重要的原因是,每个 LoadBalancer 服务都需要自己的负载均衡器及其自己的公网 IP 地址,而 Ingress 只需要一个是足够的,即使它提供了对数十个服务的访问。当客户端向 Ingress 发送 HTTP 请求时,请求中的主机和路径决定了请求被转发到哪个服务,如图 5.9 所示。
图 5.9. 可以通过单个 Ingress 暴露多个服务。

Ingress 在网络堆栈的应用层(HTTP)中运行,可以提供诸如基于 cookie 的会话亲和力等功能,这是服务所不能提供的。
理解需要 Ingress 控制器
在我们深入了解 Ingress 对象提供的功能之前,让我强调一点,为了使 Ingress 资源工作,集群中需要运行 Ingress 控制器。不同的 Kubernetes 环境使用不同的控制器实现,但其中一些根本不提供默认控制器。
例如,Google Kubernetes Engine 使用 Google Cloud Platform 自身的 HTTP 负载均衡功能来提供 Ingress 功能。最初,Minikube 并没有提供默认的控制器,但现在它包含了一个可以启用的插件,让你可以尝试 Ingress 功能。按照以下侧边栏中的说明确保已启用。
在 Minikube 中启用 Ingress 插件
如果你使用 Minikube 运行本书中的示例,你需要确保 Ingress 插件已启用。你可以通过列出所有插件来检查它是否已启用:
$ minikube addons list - default-storageclass: enabled - kube-dns: enabled - heapster: disabled - ingress: disabled 1 - registry-creds: disabled - addon-manager: enabled - dashboard: enabled
- 1 Ingress 插件尚未启用。
你将在本书中了解这些插件是什么,但应该很清楚 dashboard 和 kube-dns 插件的作用。启用 Ingress 插件以便你可以看到 Ingress 的实际操作:
$ minikube addons enable ingress ingress was successfully enabled
这应该已经启动了一个 Ingress 控制器作为另一个 pod。最可能的情况是,控制器 pod 将位于 kube-system 命名空间中,但不一定是这样,所以使用 --all-namespaces 选项列出所有命名空间中的所有正在运行的 pod:
$ kubectl get po --all-namespaces NAMESPACE NAME READY STATUS RESTARTS AGE default kubia-rsv5m 1/1 Running 0 13h default kubia-fe4ad 1/1 Running 0 13h default kubia-ke823 1/1 Running 0 13h kube-system default-http-backend-5wb0h 1/1 Running 0 18m kube-system kube-addon-manager-minikube 1/1 Running 3 6d kube-system kube-dns-v20-101vq 3/3 Running 9 6d kube-system kubernetes-dashboard-jxd9l 1/1 Running 3 6d kube-system nginx-ingress-controller-gdts0 1/1 Running 0 18m
在输出底部,你可以看到 Ingress 控制器 Pod。名称表明 Nginx(一个开源的 HTTP 服务器和反向代理)被用来提供 Ingress 功能。
提示
侧边栏中提到的--all-namespaces选项在你不知道你的 Pod(或其他类型的资源)位于哪个命名空间,或者你想列出所有命名空间中的资源时很有用。
5.4.1. 创建 Ingress 资源
你已确认你的集群中正在运行 Ingress 控制器,因此你现在可以创建 Ingress 资源。以下列表显示了 Ingress 的 YAML 清单的外观。
列表 5.13. Ingress 资源定义:kubia-ingress.yaml
apiVersion: extensions/v1beta1 kind: Ingress metadata: name: kubia spec: rules: - host: kubia.example.com 1 http: paths: - path: / 2 backend: serviceName: kubia-nodeport 2 servicePort: 80 2
-
1 此 Ingress 将 kubia.example.com 域名映射到你的服务。
-
2 所有请求都将发送到 kubia-nodeport 服务的 80 端口。
这定义了一个包含单个规则的 Ingress,确保所有通过 Ingress 控制器接收到的、请求主机kubia.example.com的 HTTP 请求都将发送到端口80上的kubia-nodeport服务。
注意
云提供商上的 Ingress 控制器(例如在 GKE 上)需要 Ingress 指向一个NodePort服务。但这不是 Kubernetes 本身的必要条件。
5.4.2. 通过 Ingress 访问服务
要通过 http://kubia.example.com 访问你的服务,你需要确保域名解析到 Ingress 控制器的 IP 地址。
获取 Ingress 的 IP 地址
要查找 IP,你需要列出 Ingress:
$ kubectl get ingresses NAME HOSTS ADDRESS PORTS AGE kubia kubia.example.com 192.168.99.100 80 29m
注意
当在云提供商上运行时,地址可能需要一段时间才能出现,因为 Ingress 控制器在幕后配置了一个负载均衡器。
IP 地址显示在ADDRESS列中。
确保 Ingress 中配置的主机指向 Ingress 的 IP 地址
一旦您知道了 IP,您可以选择配置您的 DNS 服务器将 kubia.example.com 解析到该 IP,或者您可以在/etc/hosts(或在 Windows 上的C:\windows\system32\drivers\etc\hosts)中添加以下行:
192.168.99.100 kubia.example.com
通过 Ingress 访问 Pod
一切都已设置好,因此您可以通过 http://kubia.example.com(使用浏览器或curl)访问服务:
$ curl http://kubia.example.com 您已访问 kubia-ke823
您已成功通过 Ingress 访问了服务。让我们更详细地看看这是如何展开的。
理解 Ingress 的工作原理
图 5.10 显示了客户端如何通过 Ingress 控制器连接到一个 Pod。客户端首先对 kubia.example.com 执行 DNS 查找,DNS 服务器(或本地操作系统)返回 Ingress 控制器的 IP。然后客户端向 Ingress 控制器发送 HTTP 请求,并在Host头中指定kubia.example.com。从该头信息中,控制器确定了客户端试图访问哪个服务,通过服务关联的 Endpoints 对象查找 Pod IP,并将客户端的请求转发到其中一个 Pod。
图 5.10. 通过 Ingress 访问 Pod

如您所见,Ingress 控制器没有将请求转发到服务。它只是用它来选择一个 Pod。大多数,如果不是所有控制器都是这样工作的。
5.4.3. 通过同一 Ingress 暴露多个服务
如果您仔细查看 Ingress 规范,您会看到rules和paths都是数组,因此它们可以包含多个项目。Ingress 可以将多个主机和路径映射到多个服务,您将在下面看到。让我们首先关注paths。
将不同的服务映射到同一主机的不同路径
您可以将同一主机上的多个path映射到不同的服务,如下列所示。
列表 5.14. 在同一主机上暴露多个服务,但不同的path
... - host: kubia.example.com http: paths: - path: /kubia 1 backend: 1 serviceName: kubia 1 servicePort: 80 1 - path: /foo 2 backend: 2 serviceName: bar 2 servicePort: 80 2
-
1 请求到 kubia.example.com/kubia 将被路由到 kubia 服务。
-
2 请求到 kubia.example.com/bar 将被路由到 bar 服务。
在这种情况下,根据请求的 URL 路径,请求将被发送到两个不同的服务。因此,客户端可以通过单个 IP 地址(即 Ingress 控制器所在的 IP 地址)访问两个不同的服务。
将不同的服务映射到不同的主机
类似地,您可以使用 Ingress 根据 HTTP 请求中的主机而不是(仅)路径来映射到不同的服务,如下列所示。
列表 5.15. 在不同主机上暴露多个服务的 Ingress
spec: rules: - host: foo.example.com 1 http: paths: - path: / backend: serviceName: foo 1 servicePort: 80 - host: bar.example.com 2 http: paths: - path: / backend: serviceName: bar 2 servicePort: 80
-
1 请求 foo.example.com 将被路由到服务 foo。
-
2 请求 bar.example.com 将被路由到服务 bar。
控制器接收到的请求将被转发到服务 foo 或 bar,具体取决于请求中的 Host 头(类似于在 Web 服务器中处理虚拟主机的方式)。DNS 需要将 foo.example.com 和 bar.example.com 域名都指向 Ingress 控制器的 IP 地址。
5.4.4. 配置 Ingress 以处理 TLS 流量
您已经看到了 Ingress 如何转发 HTTP 流量。那么 HTTPS 呢?让我们快速了解一下如何配置 Ingress 以支持 TLS。
为 Ingress 创建 TLS 证书
当客户端打开到 Ingress 控制器的 TLS 连接时,控制器将终止 TLS 连接。客户端与控制器之间的通信是加密的,而控制器与后端 Pod 之间的通信则不是。Pod 中运行的应用程序不需要支持 TLS。例如,如果 Pod 运行的是 Web 服务器,它只能接受 HTTP 流量,而让 Ingress 控制器处理所有与 TLS 相关的事情。为了使控制器能够这样做,您需要将证书和私钥附加到 Ingress 上。这两个文件需要存储在名为 Secret 的 Kubernetes 资源中,然后在 Ingress 清单中引用它。我们将在第七章(index_split_063.html#filepos687721)中详细解释 Secrets。现在,您将创建 Secret,但不必过分关注它。
首先,您需要创建私钥和证书:
$ openssl genrsa -out tls.key 2048 $ openssl req -new -x509 -key tls.key -out tls.cert -days 360 -subj
/CN=kubia.example.com
然后,您可以根据这两个文件创建 Secret,如下所示:
$ kubectl create secret tls tls-secret --cert=tls.cert --key=tls.key secret "tls-secret" created
通过 CertificateSigningRequest 资源签名证书
您可以通过创建 CertificateSigningRequest(CSR)资源来获取证书的签名,而不是自己签名证书。用户或他们的应用程序可以创建一个常规证书请求,将其放入 CSR 中,然后由人工操作员或自动化流程批准请求,如下所示:
$ kubectl certificate approve <CSR 名称>
签名的证书可以从 CSR 的 status.certificate 字段中检索。
注意,必须在集群中运行证书签名组件;否则,创建 CertificateSigningRequest 以及批准或拒绝它们将没有任何效果。
私钥和证书现在存储在名为tls-secret的 Secret 中。现在,您可以更新 Ingress 对象,使其也接受 kubia.example.com 的 HTTPS 请求。Ingress 清单现在应如下所示。
列表 5.16. Ingress 处理 TLS 流量:kubia-ingress-tls.yaml
apiVersion: extensions/v1beta1 kind: Ingress metadata: name: kubia spec: tls: 1 - hosts: 2 - kubia.example.com 2 secretName: tls-secret 3 rules: - host: kubia.example.com http: paths: - path: / backend: serviceName: kubia-nodeport servicePort: 80
-
1 整个 TLS 配置都包含在这个属性中。
-
2 将接受对 kubia.example.com 主机名的 TLS 连接。
-
3 应从之前创建的 tls-secret 中获取私钥和证书。
提示
您无需删除 Ingress 并从新文件中重新创建它,而是可以调用kubectl apply -f kubia-ingress-tls.yaml,这将使用文件中指定的内容更新 Ingress 资源。
现在,您可以通过 Ingress 使用 HTTPS 访问您的服务:
$ curl -k -v https://kubia.example.com/kubia * 即将连接()到 kubia.example.com 端口 443 (#0) ... * 服务器证书: * 主题: CN=kubia.example.com ... > GET /kubia HTTP/1.1 > ... 您已访问 kubia-xueq1
命令的输出显示了应用响应以及与 Ingress 配置的服务器证书。
注意
不同 Ingress 控制器实现之间对 Ingress 特性的支持各不相同,因此请检查特定实现的文档以了解支持的内容。
Ingress 是相对较新的 Kubernetes 特性,因此您预计在将来会看到许多改进和新特性。尽管它们目前仅支持 L7(HTTP/HTTPS)负载均衡,但计划也支持 L4 负载均衡。
5.5. 当 Pod 准备好接受连接时发出信号
关于服务和 Ingress,我们还需要讨论一点。您已经了解到,如果 Pod 的标签与服务的 Pod 选择器匹配,则 Pod 被视为服务的端点。一旦创建了带有适当标签的新 Pod,它就成为服务的一部分,请求开始被重定向到该 Pod。但如果 Pod 尚未准备好立即开始处理请求怎么办?
Pod 可能需要时间来加载配置或数据,或者可能需要执行预热程序以防止第一个用户请求耗时过长并影响用户体验。在这种情况下,您不希望 Pod 立即开始接收请求,尤其是当已运行的实例可以正确且快速地处理请求时。在 Pod 完全准备好之前不将请求转发到正在启动的 Pod 是有意义的。
5.5.1. 介绍就绪探针
在上一章中,你学习了存活探针以及它们如何通过确保不健康的容器自动重启来帮助保持应用程序的健康状态。与存活探针类似,Kubernetes 允许你为你的 Pod 定义一个准备探针。
准备探针定期调用,并确定特定的 Pod 是否应该接收客户端请求。当一个容器的准备探针返回成功时,它表示容器已准备好接受请求。
准备这一概念显然是针对每个容器特定的。Kubernetes 可以检查容器中的应用程序是否响应简单的 GET / 请求,或者它可以直接访问特定的 URL 路径,这会导致应用程序执行一系列检查以确定其是否已准备好。这种详细的准备探针,考虑到应用程序的特定情况,是应用程序开发者的责任。
准备探针的类型
与存活探针一样,存在三种类型的准备探针:
-
执行探针,其中执行一个进程。容器的状态由进程的退出状态码确定。
-
HTTP GET 探针,它向容器发送 HTTP
GET请求,响应的 HTTP 状态码确定容器是否已准备好。 -
TCP Socket 探针,它打开到容器指定端口的 TCP 连接。如果连接建立,则认为容器已准备好。
理解准备探针的操作
当容器启动时,Kubernetes 可以配置为在执行第一次准备检查之前等待一段可配置的时间。之后,它定期调用探针并根据准备探针的结果采取行动。如果一个 Pod 报告它未准备好,它将被从服务中移除。如果 Pod 之后再次准备好,它将被重新添加。
与存活探针不同,如果容器失败准备检查,它不会被杀死或重启。这是存活探针和准备探针之间的重要区别。存活探针通过杀死不健康的容器并替换为新的、健康的容器来保持 Pod 的健康状态,而准备探针确保只有准备好接收请求的 Pod 才能接收它们。这在容器启动期间是必要的,但容器运行一段时间后也非常有用。
如 图 5.11 所示,如果 Pod 的准备探针失败,Pod 将从 Endpoints 对象中移除。连接到服务的客户端不会重定向到该 Pod。效果与 Pod 完全不匹配服务标签选择器时相同。
图 5.11. 准备探针失败的 Pod 作为服务的一个端点被移除。

理解为什么准备探针很重要
假设有一组 Pod(例如,运行应用程序服务器的 Pod)依赖于另一个 Pod(例如,后端数据库)提供的服务。如果任何一个前端 Pod 在任何时候遇到连接问题,无法再访问数据库,那么它的就绪探测向 Kubernetes 信号表明 Pod 在当时无法处理任何请求可能是明智的。如果其他 Pod 实例没有遇到相同的连接问题,它们可以正常处理请求。就绪探测确保客户端只与健康的 Pod 通信,并且永远不会注意到系统有任何问题。
5.5.2. 向 Pod 添加就绪探测
接下来,您将通过修改 Replication-Controller 的 Pod 模板来向现有的 Pod 添加就绪探测。
向 Pod 模板添加就绪探测
您将使用 kubectl edit 命令将探测添加到现有 ReplicationController 的 Pod 模板中:
$ kubectl edit rc kubia
当 ReplicationController 的 YAML 在文本编辑器中打开时,找到 Pod 模板中的容器规范,并在 spec.template.spec.containers. 下的第一个容器中添加以下就绪探测定义。YAML 应该看起来像以下列表。
列表 5.17. RC 创建带有就绪探测的 Pod:kubia-rc-readinessprobe.yaml
apiVersion: v1 kind: ReplicationController ... spec: ... template: ... spec: containers: - name: kubia image: luksa/kubia readinessProbe:``1``exec:``1``command:``1``- ls``1``- /var/ready``1 ...
- 1 每个 Pod 中的容器都可以定义一个就绪探测。
就绪探测将定期在容器内执行 ls /var/ready 命令。如果文件存在,ls 命令返回退出代码零,否则返回非零退出代码。如果文件存在,就绪探测将成功;否则,它将失败。
您定义这样一个奇怪的就绪探测的原因是可以通过创建或删除相关文件来切换其结果。该文件尚不存在,因此所有 Pod 应该现在都报告未就绪,对吧?嗯,并不完全是这样。如您在前一章中记得的那样,更改 ReplicationController 的 Pod 模板对现有 Pod 没有影响。
换句话说,您现有的所有 Pod 都还没有定义就绪探测。您可以通过使用 kubectl get pods 列出 Pod 并查看 READY 列来查看这一点。您需要删除 Pod,并让 Replication-Controller 重新创建它们。新的 Pod 将会失败就绪检查,并且直到您在每个 Pod 中创建 /var/ready 文件,它们都不会被包括为服务的端点。
观察和修改 Pod 的就绪状态
再次列出 Pod 并检查它们是否就绪:
$ kubectl get po NAME READY STATUS RESTARTS AGE kubia-2r1qb 0/1``Running 0 1m kubia-3rax1 0/1``Running 0 1m kubia-3yw4s 0/1`` Running 0 1m
READY列显示没有任何容器就绪。现在通过创建/var/ready文件来使其中一个就绪探针开始返回成功状态,该文件的存在使得模拟的就绪探针成功:
$ kubectl exec kubia-2r1qb -- touch /var/ready
你已经使用kubectl exec命令在kubia-2r1qb Pod 的容器内执行了touch命令。touch命令会在文件不存在时创建该文件。现在 Pod 的就绪探针命令应该以状态码 0 退出,这意味着探针成功,Pod 现在应该显示为就绪。让我们看看它是否就绪:
$ kubectl get po kubia-2r1qb NAME READY STATUS RESTARTS AGE kubia-2r1qb 0/1`` 运行 0 2m
Pod 仍然没有就绪。是出了问题还是这是预期的结果?使用kubectl describe查看 Pod 的详细信息。输出应包含以下行:
就绪状态:执行 [ls /var/ready] 延迟=0s 超时=1s 周期=10s #成功=1
#失败=3
就绪探针会定期检查——默认情况下每 10 秒检查一次。由于就绪探针尚未被调用,因此 Pod 尚未就绪。但最迟在 10 秒后,Pod 应该变为就绪状态,并且其 IP 应该被列为服务的唯一端点(运行kubectl get endpoints kubia-loadbalancer以确认)。
单个就绪 Pod 访问服务
你现在可以多次访问服务 URL,以查看每个请求都被重定向到这个 Pod:
$ curl http://130.211.53.173 您已访问 kubia-2r1qb $ curl http://130.211.53.173 您已访问 kubia-2r1qb ... $ curl http://130.211.53.173 您已访问 kubia-2r1qb
尽管有三个 Pod 正在运行,但只有单个 Pod 报告为就绪状态,因此它是唯一接收请求的 Pod。如果你现在删除该文件,Pod 将再次从服务中移除。
5.5.3. 理解现实世界的就绪探针应该做什么
这个模拟的就绪探针仅用于演示就绪探针的作用。在现实世界中,就绪探针应根据应用是否能够(并且愿意)接收客户端请求来返回成功或失败。
手动从服务中删除 Pod 应通过删除 Pod 或更改 Pod 的标签来完成,而不是手动在探针中切换开关。
小贴士
如果你想手动添加或删除 Pod 到服务中,请将enabled=true作为标签添加到你的 Pod 和服务的标签选择器中。当你想从服务中删除 Pod 时,请移除该标签。
总是定义一个就绪探针
在我们结束本节之前,还有两个关于就绪探针的最终注意事项需要强调。首先,如果你没有为你的 Pod 添加就绪探针,它们几乎会立即成为服务端点。如果你的应用程序启动并开始监听传入连接需要太长时间,那么击中服务的客户端请求将在 Pod 仍在启动且尚未准备好接受传入连接时被转发到 Pod。因此,客户端将看到“连接被拒绝”类型的错误。
小贴士
你应该始终定义一个就绪探针,即使它只是发送一个 HTTP 请求到基本 URL 也是如此。
不要将 Pod 关闭逻辑包含在就绪探针中
我需要提到的另一件事与 Pod 生命周期的另一端(Pod 关闭)有关,并且也与客户端遇到连接错误有关。
当 Pod 正在关闭时,运行在其内的应用程序通常会一收到终止信号就停止接受连接。正因为如此,你可能会认为你需要让你的就绪探针在关闭程序启动时立即开始失败,确保 Pod 从它所属的所有服务中被移除。但这是不必要的,因为 Kubernetes 在你删除 Pod 时立即将其从所有服务中移除。
5.6. 使用无头服务来发现单个 Pod
你已经看到服务可以用来提供稳定的 IP 地址,允许客户端连接到每个服务背后的 Pod(或其他端点)。每个对服务的连接都会被转发到随机选择的支持 Pod。但是,如果客户端需要连接到所有这些 Pod 怎么办?如果支持 Pod 本身需要每个都连接到所有其他支持 Pod 怎么办?通过服务连接显然不是这样做的方式。那是什么?
为了让客户端连接到所有 Pod,它需要找出每个单独 Pod 的 IP。一个选项是让客户端调用 Kubernetes API 服务器,并通过 API 调用获取 Pod 列表及其 IP 地址,但因为你应该始终努力保持你的应用程序与 Kubernetes 无关,所以使用 API 服务器并不是最佳选择。
幸运的是,Kubernetes 允许客户端通过 DNS 查找来发现 Pod IP。通常,当你对服务执行 DNS 查找时,DNS 服务器会返回单个 IP——服务的集群 IP。但是,如果你告诉 Kubernetes 你不需要为你的服务提供集群 IP(你通过在服务规范中将clusterIP字段设置为None来完成此操作)`,DNS 服务器将返回 Pod IP 而不是单个服务 IP。
DNS 服务器不会返回单个 DNS A记录,而是为服务返回多个A记录,每个记录都指向在那一刻支持该服务的单个 Pod 的 IP。因此,客户端可以执行简单的 DNS A记录查找,并获取所有属于该服务的 Pod 的 IP。客户端然后可以使用该信息连接到其中一个、多个或所有 Pod。
5.6.1. 创建无头服务
将服务规范中的clusterIP字段设置为None使得服务成为无头服务,因为 Kubernetes 不会为其分配一个客户端可以通过它连接到后端 pod 的集群 IP。
现在,你将创建一个名为kubia-headless的无头服务。以下列表显示了其定义。
列表 5.18. 无头服务:kubia-svc-headless.yaml
apiVersion: v1 kind: Service metadata: name: kubia-headless spec: clusterIP: None 1 ports: - port: 80 targetPort: 8080 selector: app: kubia
- 1 这使得服务成为无头服务。
在你使用kubectl create创建服务后,你可以使用kubectl get和kubectl describe来检查它。你会发现它没有集群 IP,其端点包括(部分)匹配其 pod 选择器的 pod。我说“部分”,因为你的 pod 包含就绪性检查,所以只有就绪的 pod 才会被列为服务的端点。在继续之前,请确保至少有两个 pod 报告为就绪,通过创建与上一个示例中的/var/ready文件一样的方法:
$ kubectl exec <pod name> -- touch /var/ready
5.6.2. 通过 DNS 发现 pod
在你的 pod 就绪后,你现在可以尝试执行 DNS 查找,看看你是否能得到实际的 pod IP。你需要从其中一个 pod 内部执行查找。不幸的是,你的kubia容器镜像不包括nslookup(或dig)二进制文件,所以你不能用它来执行 DNS 查找。
你所尝试做的只是从集群中运行的 pod 内部执行 DNS 查找。为什么不基于包含所需二进制的镜像运行一个新的 pod 呢?为了执行与 DNS 相关的操作,你可以使用 Docker Hub 上可用的tutum/dnsutils容器镜像,它包含nslookup和dig二进制文件。要运行 pod,你可以通过为其创建 YAML 清单并将其传递给kubectl create的整个过程,但这太费事了,对吧?幸运的是,有一个更快的方法。
不编写 YAML 清单运行 pod
在第一章中,你已经通过使用kubectl run命令创建了不需要编写 YAML 清单的 pod。但这次你只想创建一个 pod——你不需要创建 ReplicationController 来管理 pod。你可以这样做:
$ kubectl run dnsutils --image=tutum/dnsutils --generator=run-pod/v1
--command -- sleep infinity pod "dnsutils" created
关键在于--generator=run-pod/v1选项,它告诉kubectl直接创建 pod,而不需要任何类型的 ReplicationController 或类似的后台。
理解无头服务返回的 DNS A 记录
让我们使用新创建的 pod 来执行 DNS 查找:
$ kubectl exec dnsutils nslookup kubia-headless ... Name: kubia-headless.default.svc.cluster.local Address: 10.108.1.4 Name: kubia-headless.default.svc.cluster.local Address: 10.108.2.5
DNS 服务器为kubia-headless.default.svc.cluster.local FQDN 返回两个不同的 IP。这些是报告已准备就绪的两个 Pod 的 IP。你可以通过使用kubectl get pods -o wide列出 Pod 来确认这一点,它显示了 Pod 的 IP。
这与 DNS 为常规(非无头)服务返回的内容不同,例如你的kubia服务,其中返回的 IP 是服务的集群 IP:
$ kubectl exec dnsutils nslookup kubia ... Name: kubia.default.svc.cluster.local Address: 10.111.249.153
尽管无头服务看起来与常规服务不同,但从客户端的角度来看,它们并没有那么不同。即使是无头服务,客户端也可以通过连接到服务的 DNS 名称来连接到其 Pod,就像它们可以连接到常规服务一样。但在无头服务中,因为 DNS 返回 Pod 的 IP,客户端直接连接到 Pod,而不是通过服务代理。
注意
无头服务仍然通过 DNS 轮询机制而不是通过服务代理在 Pod 之间提供负载均衡。
5.6.3. 发现所有 Pod——即使那些尚未准备就绪的 Pod
你已经看到只有准备就绪的 Pod 才会成为服务的端点。但有时你希望使用服务发现机制来找到所有匹配服务标签选择器的 Pod,即使它们尚未准备就绪。
幸运的是,你不必求助于查询 Kubernetes API 服务器。你可以使用 DNS 查找机制来找到甚至那些未准备就绪的 Pod。为了告诉 Kubernetes 你希望将所有 Pod 添加到服务中,无论 Pod 的准备状态如何,你必须将以下注释添加到服务中:
kind: Service metadata: annotations: service.alpha.kubernetes.io/tolerate-unready-endpoints: "true"
警告
如注释名称所暗示的,在我写这篇文章的时候,这是一个 alpha 特性。Kubernetes 服务 API 已经支持一个新的服务规范字段,称为publishNotReadyAddresses,它将取代tolerate-unready-endpoints注释。在 Kubernetes 版本 1.9.0 中,该字段尚未得到尊重(注释是确定未准备端点是否包含在 DNS 中的因素)。请查看文档以了解是否已更改。
5.7. 服务故障排除
服务是 Kubernetes 的一个关键概念,也是许多开发者的挫折之源。我见过许多开发者花费大量时间试图弄清楚为什么他们无法通过服务 IP 或 FQDN 连接到他们的 Pod。因此,简要了解如何故障排除服务是有必要的。
当你无法通过服务访问你的 Pod 时,你应该首先检查以下列表:
-
首先,确保你是在集群内部连接到服务的集群 IP,而不是从外部连接。
-
不要费心 ping 服务 IP 来检查服务是否可访问(记住,服务的集群 IP 是一个虚拟 IP,ping 它永远不会起作用)。
-
如果你已定义就绪探测,请确保它成功;否则,pod 不会成为服务的一部分。
-
要确认一个 pod 是否是服务的一部分,使用
kubectl get endpoints检查相应的 Endpoints 对象。 -
如果你试图通过其 FQDN 或其一部分(例如,myservice.mynamespace.svc.cluster.local 或 myservice.mynamespace)来访问服务,但不起作用,请尝试使用其集群 IP 而不是 FQDN 来访问。
-
检查你是否连接到了服务暴露的端口,而不是目标端口。
-
尝试直接连接到 pod IP 以确认你的 pod 正在正确的端口上接受连接。
-
如果你甚至无法通过 pod 的 IP 访问你的应用程序,请确保你的应用程序不仅绑定到 localhost。
这应该有助于你解决大部分与服务相关的问题。你将在第十一章 chapter 11 中学习更多关于服务如何工作的内容。通过了解它们的确切实现方式,你应该更容易对它们进行故障排除。
5.8. 摘要
在本章中,你学习了如何创建 Kubernetes Service 资源以暴露应用程序中可用的服务,无论有多少个 pod 实例提供每个服务。你学习了 Kubernetes
-
在单个稳定的 IP 地址和端口下暴露匹配特定标签选择器的多个 pod
-
默认情况下使服务在集群内部可访问,但允许你通过将其类型设置为
NodePort或LoadBalancer来使服务从集群外部可访问 -
通过查找环境变量,使 pod 能够与其 IP 地址和端口一起发现服务
-
通过创建一个不指定选择器的 Service 资源,而不是创建关联的 Endpoints 资源,允许发现和与集群外部的服务进行通信
-
使用
ExternalName服务类型为外部服务提供 DNSCNAME别名 -
通过单个 Ingress(消耗单个 IP)暴露多个 HTTP 服务
-
使用 pod 容器的就绪探测来确定 pod 是否应该或不应作为服务端点包含
-
当你创建一个无头服务时,通过 DNS 发现 pod IP
除了更好地理解服务外,你还学习了如何
-
故障排除它们
-
修改 Google Kubernetes/Compute Engine 的防火墙规则
-
通过
kubectl exec在 pod 容器中执行命令 -
在现有 pod 的容器中运行
bashshell -
通过
kubectl apply命令修改 Kubernetes 资源 -
使用
kubectl run --generator=run-pod/v1运行一个未管理的 ad hoc pod
第六章. 卷:将磁盘存储附加到容器
本章涵盖
-
创建多容器 Pod
-
创建卷以在容器之间共享磁盘存储
-
在 Pod 内使用 Git 仓库
-
将持久存储(如 GCE 持久磁盘)附加到 Pod
-
使用预先配置的持久存储
-
持久存储的动态配置
在前三章中,我们介绍了 Pod 以及其他与之交互的 Kubernetes 资源,例如 ReplicationControllers、ReplicaSets、DaemonSets、Jobs 和 Services。现在,我们回到 Pod 内部,学习其容器如何访问外部磁盘存储以及/或它们之间共享存储。
我们提到,Pod 类似于逻辑主机,其中运行的进程共享资源,如 CPU、RAM、网络接口等。人们可能会预期进程也会共享磁盘,但事实并非如此。你会记得,Pod 中的每个容器都有自己的独立文件系统,因为文件系统来自容器的镜像。
每个新容器都以在构建时添加到镜像中的确切文件集开始。结合 Pod 中的容器会重启(无论是由于进程死亡还是因为存活探针向 Kubernetes 发出容器不再健康的信号)的事实,你就会意识到新容器将看不到之前容器写入文件系统的任何内容,即使新启动的容器在同一个 Pod 中运行。
在某些场景下,你希望新容器从上一个容器结束的地方继续,例如在物理机上重启进程时。你可能不需要(或不想)持久化整个文件系统,但你确实想保留包含实际数据的目录。
Kubernetes 通过定义存储卷来实现这一点。它们不是像 Pod 这样的顶级资源,而是作为 Pod 的一部分定义,并具有与 Pod 相同的生命周期。这意味着当 Pod 启动时创建卷,当 Pod 被删除时销毁卷。正因为如此,卷的内容将在容器重启之间持续存在。容器重启后,新容器可以看到之前容器写入卷的所有文件。此外,如果 Pod 包含多个容器,卷可以同时被所有容器使用。
6.1. 介绍卷
Kubernetes 卷是 Pod 的组成部分,因此定义在 Pod 的规范中——就像容器一样。它们不是独立的 Kubernetes 对象,不能单独创建或删除。卷对 Pod 中的所有容器都可用,但必须挂载在每个需要访问它的容器中。在每个容器中,你可以在其文件系统的任何位置挂载卷。
6.1.1. 通过示例解释卷
想象一下,你有一个包含三个容器的舱(如图 6.1 所示)。一个容器运行一个 Web 服务器,从/var/htdocs 目录提供 HTML 页面,并将访问日志存储到/var/logs。第二个容器运行一个代理,创建 HTML 文件并将它们存储在/var/html。第三个容器处理它在/var/logs 目录中找到的日志(旋转、压缩、分析或任何其他操作)。
图 6.1. 没有共享存储的同一舱的三个容器

每个容器都有一个定义良好的单一职责,但单独来看,每个容器都不会有很大的用处。如果没有共享磁盘存储,创建一个包含这三个容器的舱就没有意义,因为内容生成器会在自己的容器内写入生成的 HTML 文件,而 Web 服务器无法访问这些文件,因为它运行在一个独立的隔离容器中。相反,它会提供一个空目录或在其容器镜像的/var/htdocs 目录中放置的任何内容。同样,日志旋转器永远不会有什么事情要做,因为它的/var/logs 目录总是会保持空,没有任何地方写入日志。基本上,这样一个包含这三个容器且没有卷的舱什么也不做。
但是,如果你向舱中添加两个卷并将它们挂载到三个容器中适当的路径,如图 6.2 所示,你就创建了一个远大于其各部分总和的系统。Linux 允许你在文件树中的任意位置挂载一个文件系统。当你这样做时,挂载的文件系统的内容可以在挂载到的目录中访问。通过将相同的卷挂载到两个容器中,它们可以操作相同的文件。在你的情况下,你在三个容器中挂载了两个卷。通过这样做,你的三个容器可以协同工作并完成一些有用的事情。让我来解释一下。
图 6.2. 三个容器共享两个在不同挂载路径挂载的卷

首先,舱有一个名为publicHtml的卷。这个卷被挂载到WebServer容器中的/var/htdocs,因为这是 Web 服务器提供文件的目录。相同的卷也被挂载到ContentAgent容器中,但挂载到/var/html,因为这是代理写入文件的目录。通过这样挂载单个卷,Web 服务器现在将提供由内容代理生成的内容。
同样,舱还有一个名为logVol的卷用于存储日志。这个卷被挂载到WebServer和LogRotator容器的/var/logs。请注意,它没有被挂载到ContentAgent容器中。即使容器和卷都属于同一个舱,容器也无法访问其文件。仅定义舱中的卷是不够的;如果你想让容器能够访问它,你还需要在容器的规范中定义一个VolumeMount。
在这个示例中,这两个卷最初都可以为空,因此你可以使用一种称为 emptyDir 的卷类型。Kubernetes 还支持其他类型的卷,这些卷在从外部源初始化卷或挂载现有目录到卷中时被填充。这个过程是在 pod 的容器启动之前执行的。
卷绑定到 pod 的生命周期,并且只有在 pod 存在期间才会存在,但根据卷类型的不同,卷的文件可能在 pod 和卷消失后仍然保持完整,并且可以稍后挂载到新的卷中。让我们看看存在哪些类型的卷。
6.1.2. 介绍可用的卷类型
可用的卷类型多种多样。其中一些是通用的,而另一些则是针对实际使用的存储技术的特定类型。如果你从未听说过这些技术,请不要担心——至少有一半以上我都没听说过。你可能只会使用你已知并使用的技术的卷类型。以下是一些可用的卷类型列表:
-
emptyDir—一个简单的空目录,用于存储临时数据。 -
hostPath—用于将工作节点文件系统中的目录挂载到 pod 中。 -
gitRepo—通过检出 Git 仓库的内容来初始化的卷。 -
nfs—挂载到 pod 中的 NFS 共享。 -
gcePersistentDisk(Google Compute Engine 持久磁盘),awsElasticBlockStore(Amazon Web Services 弹性块存储卷),azureDisk(Microsoft Azure 磁盘卷)—用于挂载特定云提供商的存储。 -
cinder,cephfs,iscsi,flocker,glusterfs,quobyte,rbd,flexVolume,vsphere-Volume,photonPersistentDisk,scaleIO—用于挂载其他类型的网络存储。 -
configMap,secret,downwardAPI—用于将某些 Kubernetes 资源和集群信息暴露给 pod 的特殊类型的卷。 -
persistentVolumeClaim—一种使用预先或动态预配的持久存储的方式。(我们将在本章的最后部分讨论它们。)
这些卷类型服务于各种目的。你将在以下章节中了解其中的一些。特殊类型的卷(secret,downwardAPI,configMap)将在下一两章中介绍,因为它们不是用于存储数据,而是用于将 Kubernetes 元数据暴露给在 pod 中运行的应用程序。
单个 pod 可以同时使用多种不同类型的多个卷,并且,正如我们之前提到的,pod 的每个容器都可以选择挂载或不挂载卷。
6.2. 使用卷在容器之间共享数据
尽管卷在单个容器使用时也可能很有用,但让我们首先关注它是如何用于在 pod 中的多个容器之间共享数据的。
6.2.1. 使用 emptyDir 卷
最简单的卷类型是 emptyDir 卷,所以让我们在定义 pod 中卷的第一个例子中看看它。正如其名所示,卷最初是一个空目录。pod 内运行的程序可以将其需要的任何文件写入其中。因为卷的寿命与 pod 相关联,所以当 pod 被删除时,卷的内容也会丢失。
一个 emptyDir 卷对于在同一个 pod 中运行的容器之间共享文件特别有用。但它也可以由单个容器使用,当容器需要临时将数据写入磁盘时,例如在对大型数据集进行排序操作时,这些数据无法适应可用的内存。数据也可以写入容器的文件系统本身(记得容器中顶层的可读写层吗?),但这两个选项之间存在细微的差异。容器的文件系统可能甚至不可写(我们将在本书的末尾讨论这个问题),因此写入挂载的卷可能是唯一的选择。
在 pod 中使用 emptyDir 卷
让我们回顾一下之前的例子,其中 Web 服务器、内容代理和日志轮转器共享两个卷,但让我们简化一下。你将构建一个只包含 Web 服务器容器和内容代理以及单个 HTML 卷的 pod。
你将使用 Nginx 作为 Web 服务器和 UNIX fortune 命令来生成 HTML 内容。每次运行 fortune 命令时,它都会打印出一个随机引言。你将创建一个脚本,每 10 秒调用一次 fortune 命令,并将输出存储在 index.html 中。你可以在 Docker Hub 上找到一个现有的 Nginx 镜像,但你需要自己创建 fortune 镜像或者使用我已经构建并推送到 Docker Hub 上的 luksa/fortune。如果你想要复习如何构建 Docker 镜像,请参考侧边栏。
构建 fortune 容器镜像
下面是如何构建镜像。创建一个名为 fortune 的新目录,然后在其中创建一个名为 fortuneloop.sh 的 shell 脚本,内容如下:
#!/bin/bash trap "exit" SIGINT mkdir /var/htdocs while : do echo $(date) Writing fortune to /var/htdocs/index.html /usr/games/fortune > /var/htdocs/index.html sleep 10 done
然后,在同一个目录中,创建一个名为 Dockerfile 的文件,内容如下:
FROM ubuntu:latest RUN apt-get update ; apt-get -y install fortune ADD fortuneloop.sh /bin/fortuneloop.sh ENTRYPOINT /bin/fortuneloop.sh
镜像是基于 ubuntu:latest 镜像的,默认情况下它不包括 fortune 二进制文件。这就是为什么在 Dockerfile 的第二行中,你使用 apt-get 安装它。之后,你将 fortuneloop.sh 脚本添加到镜像的 /bin 文件夹中。在 Dockerfile 的最后一行中,你指定当镜像运行时应该执行 fortuneloop.sh 脚本。
准备好这两个文件后,使用以下两个命令构建并上传镜像到 Docker Hub(将luksa替换为您自己的 Docker Hub 用户 ID):
$ docker build -t luksa/fortune . $ docker push luksa/fortune
创建 Pod
现在您已经有了运行 Pod 所需的两个镜像,是时候创建 Pod 清单了。创建一个名为 fortune-pod.yaml 的文件,其内容如下所示。
列表 6.1.具有两个容器共享相同卷的 Pod:fortune-pod.yaml
apiVersion: v1 kind: Pod metadata: name: fortune spec: containers: - image: luksa/fortune 1 name: html-generator 1 volumeMounts: 2 - name: html 2 mountPath: /var/htdocs 2 - image: nginx:alpine 3 name: web-server 3 volumeMounts: 4 - name: html 4 mountPath: /usr/share/nginx/html 4 readOnly: true 4 ports: - containerPort: 80 protocol: TCP volumes: 5 - name: html 5 emptyDir: {} 5
-
1 第一个容器被称为 html-generator,运行 luksa/fortune 镜像。
-
2 将名为 html 的卷挂载在容器的/var/htdocs 上。
-
3 第二个容器被称为 web-server,运行 nginx:alpine 镜像。
-
4 与上述相同的卷挂载在/usr/share/nginx/html 上,为只读。
-
5 一个名为 html 的单个 emptyDir 卷,在上述两个容器中挂载
Pod 包含两个容器和一个名为 html 的单个卷,该卷在两个容器中挂载,但路径不同。当html-generator容器启动时,它每 10 秒将fortune命令的输出写入到/var/htdocs/index.html 文件中。因为卷挂载在/var/htdocs 上,所以 index.html 文件被写入到卷中而不是容器的顶层。一旦web-server容器启动,它就开始服务/usr/share/nginx/html 目录中的任何 HTML 文件(这是 Nginx 默认的服务文件目录)。因为您在那个确切位置挂载了卷,所以 Nginx 将服务由运行幸运循环的容器写入的 index.html 文件。最终效果是,向 Pod 的 80 端口发送 HTTP 请求的客户将收到当前的幸运信息作为响应。
观察 Pod 运行
要查看幸运信息,您需要启用对 Pod 的访问权限。您可以通过将本地计算机上的端口转发到 Pod 来实现:
$ kubectl port-forward fortune 8080:80 Forwarding from 127.0.0.1:8080 -> 80 Forwarding from [::1]:8080 -> 80
注意
作为练习,您还可以通过服务而不是使用端口转发来公开 Pod。
现在,您可以通过本地计算机的 8080 端口访问 Nginx 服务器。使用curl来做到这一点:
$ curl http://localhost:8080 小心一个高个子金发男子,他穿着一只黑色鞋子。
如果你等待几秒钟后再次发送请求,你应该会收到不同的消息。通过组合两个容器,你创建了一个简单的应用程序来查看一个卷如何将两个容器粘合在一起并增强它们各自的功能。
指定用于 emptyDir 的中介
你用作卷的emptyDir是在托管你的 Pod 的工作节点实际磁盘上创建的,因此其性能取决于节点磁盘的类型。但是,你可以告诉 Kubernetes 在 tmpfs 文件系统(内存而不是磁盘)上创建emptyDir。为此,将emptyDir的medium设置为Memory,如下所示:
volumes: - name: html emptyDir: medium: Memory 1
- 1 这个 emptyDir 的文件应该存储在内存中。
emptyDir卷是最简单的卷类型,但其他类型都是基于它构建的。在创建空目录之后,它们会填充数据。其中一种卷类型是gitRepo卷类型,我们将在下一节介绍。
6.2.2. 将 Git 仓库作为卷的起点
一个gitRepo卷基本上是一个emptyDir卷,它在 Pod 启动时(但在创建容器之前)通过克隆 Git 仓库并检出特定版本来填充。展示了这个过程。
图 6.3. 一个gitRepo卷最初是一个emptyDir卷,其中包含 Git 仓库的内容。

注意
在创建gitRepo卷之后,它不会与它引用的仓库保持同步。当你向 Git 仓库推送额外的提交时,卷中的文件不会更新。然而,如果你的 Pod 由 ReplicationController 管理,删除 Pod 将导致创建一个新的 Pod,而这个新 Pod 的卷将包含最新的提交。
例如,你可以使用 Git 仓库来存储你网站的静态 HTML 文件,并创建一个包含 Web 服务器容器和gitRepo卷的 Pod。每次创建 Pod 时,它都会拉取你网站的最新版本并开始提供服务。这个方法的唯一缺点是,每次你向gitRepo推送更改并想要开始提供网站的新版本时,都需要删除 Pod。
让我们立即这样做。这并不像你之前所做的那样不同。
运行一个从克隆的 Git 仓库提供文件的 Web 服务器 Pod
在你创建 Pod 之前,你需要一个包含 HTML 文件的实际 Git 仓库。我在 GitHub 上创建了一个仓库,网址为github.com/luksa/kubia-website-example.git。你需要将其分叉(在 GitHub 上创建仓库的副本),这样你就可以稍后向它推送更改。
一旦你创建了分叉,你就可以继续创建 Pod。这次,你只需要 Pod 中的一个 Nginx 容器和一个gitRepo卷(确保将gitRepo卷指向你自己的分叉),如下所示。
列表 6.2. 使用 gitRepo 卷的 pod:gitrepo-volume-pod.yaml
`apiVersion: v1 kind: Pod metadata: name: gitrepo-volume-pod spec: containers: - image: nginx:alpine name: web-server volumeMounts: - name: html mountPath: /usr/share/nginx/html readOnly: true ports: - containerPort: 80 protocol: TCP volumes: - name: html gitRepo: repository: https://github.com/luksa/kubia-website-example.git revision: master directory: .
-
1 您正在创建一个 gitRepo 卷。
-
2 该卷将克隆此 Git 仓库。
-
3 将检出主分支。
-
4 您希望将仓库克隆到卷的根目录中。
当您创建 pod 时,卷首先初始化为一个空目录,然后将指定的 Git 仓库克隆到其中。如果您没有将目录设置为 .(点),则仓库将被克隆到 kubia-website-example 子目录中,这不是您想要的。您希望将仓库克隆到卷的根目录中。除了仓库外,您还指定了您希望 Kubernetes 检出在创建卷时主分支指向的任何修订版本。
当 pod 运行时,您可以通过端口转发、服务或从 pod 内部(或集群中的任何其他 pod)执行 curl 命令来尝试连接到它。
确认文件没有与 git 仓库保持同步
现在,您将对 GitHub 仓库中的 index.html 文件进行更改。如果您没有在本地使用 Git,您可以直接在 GitHub 上编辑该文件——点击 GitHub 仓库中的文件以打开它,然后点击铅笔图标开始编辑。更改文本后,通过点击底部的按钮提交更改。
Git 仓库的主分支现在包含了您对 HTML 文件所做的更改。这些更改在您的 Nginx 网络服务器上尚不可见,因为 gitRepo 卷没有与 Git 仓库保持同步。您可以通过再次连接到 pod 来确认这一点。
要查看网站的最新版本,您需要删除 pod 并重新创建它。而不是每次更改都要删除 pod,您可以运行一个额外的进程,该进程将您的卷与 Git 仓库保持同步。我不会详细解释如何做这件事。相反,尝试自己作为练习来做这件事,但这里有一些提示。
介绍边车容器
Git 同步进程不应在运行 Nginx 网络服务器的同一容器中运行,而应在第二个容器中运行:一个边车容器。边车容器是一种增强 pod 主容器操作的容器。您向 pod 添加边车是为了使用现有的容器镜像,而不是将额外的逻辑塞入主应用程序的代码中,这会使它过于复杂且可重用性降低。
要找到一个现有的容器镜像,该镜像将本地目录与 Git 仓库同步,请访问 Docker Hub 并搜索“git sync”。你会找到很多这样的镜像。然后,在先前的示例 Pod 中的新容器中使用该镜像,将 Pod 现有的gitRepo卷挂载到新容器中,并配置 Git 同步容器以保持文件与你的 Git 仓库同步。如果你设置正确,你应该会看到 Web 服务器所服务的文件与你的 GitHub 仓库保持同步。
注意
第十八章中的一个示例包括使用这里解释的 Git 同步容器,所以你可以等到你到达第十八章并按照那里的逐步说明进行,而不是现在自己进行这个练习。
使用 gitRepo 卷与私有 Git 仓库
需要使用 Git 同步侧边容器还有另一个原因。我们还没有讨论过你是否可以使用gitRepo卷与私有 Git 仓库一起使用。结果是,你不能。Kubernetes 开发者的当前共识是保持gitRepo卷简单,不添加通过 SSH 协议克隆私有仓库的支持,因为这需要向gitRepo卷添加额外的配置选项。
如果你想在容器中克隆一个私有 Git 仓库,你应该使用 gitsync 侧边容器或类似的方法,而不是gitRepo卷。
总结 gitRepo 卷
gitRepo卷,就像emptyDir卷一样,基本上是为包含卷的 Pod 专门创建的目录,并且仅由该 Pod 专用。当 Pod 被删除时,卷及其内容也会被删除。然而,其他类型的卷不会创建新的目录,而是将现有的外部目录挂载到 Pod 容器的文件系统中。该卷的内容可以在多个 Pod 实例化中存活。我们将在下一节学习这些类型的卷。
6.3. 访问工作节点文件系统上的文件
大多数 Pod 应该对其宿主节点一无所知,因此它们不应该访问节点文件系统上的任何文件。但某些系统级别的 Pod(记住,这些通常将由 DaemonSet 管理)确实需要读取节点的文件或使用节点的文件系统通过文件系统访问节点的设备。Kubernetes 通过hostPath卷来实现这一点。
6.3.1. 介绍 hostPath 卷
hostPath卷指向节点文件系统上的特定文件或目录(见图 6.4)。在同一个节点上运行并使用相同路径的hostPath卷的 Pod 可以看到相同的文件。
图 6.4. hostPath卷将工作节点上的文件或目录挂载到容器的文件系统中。

hostPath卷是我们首先介绍的一种持久化存储类型,因为当 Pod 被销毁时,gitRepo和emptyDir卷的内容会被删除,而hostPath卷的内容则不会。如果一个 Pod 被删除,并且下一个 Pod 使用指向主机上相同路径的hostPath卷,新的 Pod 将看到前一个 Pod 留下的任何内容,但仅当它被调度到与第一个 Pod 相同的节点上时。
如果你正在考虑将hostPath卷作为存储数据库数据目录的位置,请重新考虑。因为卷的内容存储在特定节点的文件系统上,当数据库 Pod 被重新调度到另一个节点时,它将无法看到数据。这解释了为什么使用hostPath卷对于常规 Pod 来说不是一个好主意,因为它使得 Pod 对它被调度到的节点敏感。
6.3.2. 检查使用 hostPath 卷的系统 Pod
让我们看看如何正确使用hostPath卷。我们不是创建一个新的 Pod,而是看看是否有任何现有的系统级 Pod 已经在使用这种类型的卷。如你可能在之前的章节中记得,有几个这样的 Pod 正在kube-system命名空间中运行。让我们再次列出它们:
$ kubectl get pod s --namespace kube-system NAME READY STATUS RESTARTS AGE fluentd-kubia-4ebc2f1e-9a3e 1/1 Running 1 4d fluentd-kubia-4ebc2f1e-e2vz 1/1 Running 1 31d ...
选择第一个,看看它使用了哪些类型的卷(如下面的列表所示)。
列表 6.3. 使用hostPath卷访问节点日志的 Pod
$ kubectl describe po fluentd-kubia-4ebc2f1e-9a3e --namespace kube-system Name: fluentd-cloud-logging-gke-kubia-default-pool-4ebc2f1e-9a3e Namespace: kube-system ... Volumes: varlog:``Type: HostPath (裸主机目录卷)``Path: /var/log``varlibdockercontainers:``Type: HostPath (裸主机目录卷)``Path: /var/lib/docker/containers
小贴士
如果你正在使用 Minikube,尝试使用kube-addon-manager-minikube Pod。
哎!这个 Pod 使用了两个hostPath卷来访问节点的/var/log和/var/lib/docker/containers目录。你可能会认为你很幸运第一次就找到了使用hostPath卷的 Pod,但实际上并不是(至少在 GKE 上不是)。检查其他 Pod,你会发现大多数 Pod 使用这种类型的卷要么是为了访问节点的日志文件,要么是为了访问 kubeconfig(Kubernetes 配置文件),或者是为了访问 CA 证书。
如果你检查其他 Pod,你会发现它们都没有使用hostPath卷来存储它们自己的数据。它们都使用它来访问节点的数据。但正如我们将在本章后面看到的,hostPath卷通常用于在单节点集群中尝试持久化存储,例如由 Minikube 创建的集群。继续阅读,了解在多节点集群中正确存储持久数据应使用的卷类型。
小贴士
记住,只有在你需要读取或写入节点上的系统文件时才使用 hostPath 卷。永远不要使用它们在 pod 之间持久化数据。
6.4. 使用持久存储
当在 pod 中运行的应用程序需要将数据持久化到磁盘,并且即使在 pod 被重新调度到另一个节点时也能访问相同的数据时,你不能使用我们之前提到的任何卷类型。因为此数据需要从任何集群节点访问,它必须存储在某种类型的网络附加存储 (NAS) 上。
要了解允许持久化数据的卷,你将创建一个运行 MongoDB 文档型 NoSQL 数据库的 pod。在没有卷或非持久卷的情况下运行数据库 pod 没有意义,除非是为了测试目的,所以你将为 pod 添加适当类型的卷并将其挂载到 MongoDB 容器中。
6.4.1. 在 pod 卷中使用 GCE 持久磁盘
如果你一直在 Google Kubernetes Engine 上运行这些示例,该引擎在 Google Compute Engine (GCE) 上运行你的集群节点,你将使用 GCE 持久磁盘作为你的底层存储机制。
在早期版本中,Kubernetes 不会自动配置底层存储——你必须手动完成。现在可以实现自动配置,你将在本章后面了解它,但首先,你将从手动配置存储开始。这将给你一个机会了解底层到底发生了什么。
创建 GCE 持久磁盘
你将从首先创建 GCE 持久磁盘开始。你需要在与你的 Kubernetes 集群相同的区域中创建它。如果你不记得你在哪个区域创建了集群,你可以通过使用 gcloud 命令列出你的 Kubernetes 集群来查看它,如下所示:
$ gcloud container clusters list NAME ZONE MASTER_VERSION MASTER_IP ... kubia europe-west1-b 1.2.5 104.155.84.137 ...
这表明你已在区域 europe-west1-b 中创建了你的集群,因此你需要在同一区域创建 GCE 持久磁盘。你可以这样创建磁盘:
$ gcloud compute disks create --size=1GiB --zone=europe-west1-b mongodb 警告:你选择了一个小于 [200GB] 的磁盘大小。这可能会导致 I/O 性能不佳。更多信息,请参阅:https://developers.google.com/compute/docs/disks#pdperformance. 已创建 [https://www.googleapis.com/compute/v1/projects/rapid-pivot- 136513/zones/europe-west1-b/disks/mongodb]. NAME ZONE SIZE_GB TYPE STATUS mongodb europe-west1-b 1 pd-standard READY
此命令创建了一个 1 GiB 大小的 GCE 持久磁盘,名为 mongodb。你可以忽略关于磁盘大小的警告,因为你对即将运行的测试中磁盘的性能并不关心。
使用 gcePersistentDisk 卷创建 pod
现在你已经正确设置了物理存储,你可以在 MongoDB pod 的卷中使用它。你将准备 pod 的 YAML 文件,如下所示。
列表 6.4. 使用gcePersistentDisk卷的 pod:mongodb-pod-gcepd.yaml
apiVersion: v1 kind: Pod metadata: name: mongodb spec: volumes: - name: mongodb-data 1 gcePersistentDisk: 2 pdName: mongodb 3 fsType: ext4 4 containers: - image: mongo name: mongodb volumeMounts: - name: mongodb-data 1 mountPath: /data/db 5 ports: - containerPort: 27017 protocol: TCP
-
1 卷的名称(在挂载卷时也会引用)
-
2 卷的类型是 GCE 持久磁盘。
-
3 持久磁盘的名称必须与之前创建的实际 PD 匹配。
-
4 文件系统类型是 EXT4(一种 Linux 文件系统)。
-
5 MongoDB 存储数据的位置
注意
如果你使用 Minikube,你不能使用 GCE 持久磁盘,但你可以部署mongodb-pod-hostpath.yaml,它使用hostPath卷而不是 GCE PD。
Pod 包含一个容器和一个卷,该卷由你创建的 GCE 持久磁盘支持(如图 6.5 所示 figure 6.5)。你将卷挂载到容器内的/data/db,因为那是 MongoDB 存储数据的地方。
图 6.5. 运行 MongoDB 的单个容器,挂载了一个引用外部 GCE 持久磁盘的卷

通过向你的 MongoDB 数据库添加文档来写入持久存储
现在你已经创建了 pod,并且容器已经启动,你可以在容器内运行 MongoDB shell 并使用它来向数据存储写入一些数据。
你将按照以下列表运行 shell。
列表 6.5. 进入mongodb pod 内的 MongoDB shell
$ kubectl exec -it mongodb mongo MongoDB shell 版本:3.2.8 连接到:mongodb://127.0.0.1:27017 欢迎使用 MongoDB shell。要获取交互式帮助,请输入"help"。要获取更全面的文档,请参阅 http://docs.mongodb.org/ 有问题?尝试支持小组 http://groups.google.com/group/mongodb-user ... >
MongoDB 允许存储 JSON 文档,所以你会存储一个以查看它是否被持久存储,并且在 pod 重新创建后可以检索。使用以下命令插入一个新的 JSON 文档:
> use mystore 切换到数据库 mystore > db.foo.insert({name:'foo'}) WriteResult({ "nInserted" : 1 })
你插入了一个简单的 JSON 文档,包含一个属性(name: 'foo')。现在,使用find()命令查看你插入的文档:
> db.foo.find() { "_id" : ObjectId("57a61eb9de0cfd512374cc75"), "name" : "foo" }
就在这里。文档现在应该存储在你的 GCE 持久磁盘上了。
重新创建 pod 并验证它是否可以读取前一个 pod 持久存储的数据
现在,你可以退出 mongodb shell(输入 exit 并按 Enter),然后删除 pod 并重新创建它:
$ kubectl delete pod mongodb pod "mongodb" deleted $ kubectl create -f mongodb-pod-gcepd.yaml pod "mongodb" created
新的 pod 使用与上一个 pod 完全相同的 GCE 持久磁盘,因此运行在其内部的 MongoDB 容器应该看到完全相同的数据,即使 pod 被调度到不同的节点。
提示
你可以通过运行 kubectl get po -o wide 来查看一个 pod 被调度到哪个节点。
容器启动后,你还可以再次运行 MongoDB shell 并检查你之前存储的文档是否仍然可以检索,如下所示。
列表 6.6. 在新的 pod 中检索 MongoDB 的持久数据
$ kubectl exec -it mongodb mongo MongoDB shell version: 3.2.8 connecting to: mongodb://127.0.0.1:27017 Welcome to the MongoDB shell. ... > use mystore switched to db mystore > db.foo.find() { "_id" : ObjectId("57a61eb9de0cfd512374cc75"), "name" : "foo" }
如预期,数据仍然存在,即使你删除了 pod 并重新创建了它。这证实了你可以使用 GCE 持久磁盘在多个 pod 实例之间持久化数据。
你已经玩够了 MongoDB pod,所以继续删除它,但不要删除底层的 GCE 持久磁盘。你将在本章的后面再次使用它。
6.4.2. 使用其他类型的卷与底层持久存储
你创建 GCE 持久磁盘卷的原因是因为你的 Kubernetes 集群运行在 Google Kubernetes Engine 上。当你将你的集群运行在其他地方时,你应该使用其他类型的卷,具体取决于底层基础设施。
如果你的 Kubernetes 集群运行在亚马逊的 AWS EC2 上,例如,你可以使用 awsElasticBlockStore 卷为你的 pod 提供持久存储。如果你的集群运行在微软的 Azure 上,你可以使用 azureFile 或 azureDisk 卷。这里我们不会详细介绍如何操作,但与前面的例子几乎相同。首先,你需要创建实际的底层存储,然后在卷定义中设置适当的属性。
使用 AWS 弹性块存储卷
例如,要使用 AWS 弹性块存储而不是 GCE 持久磁盘,你只需更改卷定义,如下所示(查看那些用粗体打印的行)。
列表 6.7. 使用 awsElasticBlockStore 卷的 pod:mongodb-pod-aws.yaml
apiVersion: v1 kind: Pod metadata: name: mongodb spec: volumes: - name: mongodb-data awsElasticBlockStore:``1``volumeId: my-volume``2``fsType: ext4``3 containers: - ...
-
1 使用 awsElasticBlockStore 代替 gcePersistentDisk
-
2 指定你创建的 EBS 卷的 ID。
-
3 文件系统类型与之前相同为 EXT4。
使用 NFS 卷
如果你的集群运行在你自己的服务器上,你有一系列其他支持选项来在你的卷内挂载外部存储。例如,为了挂载一个简单的 NFS 共享,你只需要指定 NFS 服务器和服务器导出的路径,如下面的列表所示。
列表 6.8. 使用 nfs 卷的 pod:mongodb-pod-nfs.yaml
volumes: - name: mongodb-data nfs: server: 1.2.3.4 path: /some/path
-
1 这个卷由一个 NFS 共享支持。
-
2 NFS 服务器的 IP 地址
-
3 服务器导出的路径
使用其他存储技术
其他支持选项包括用于挂载 ISCSI 磁盘资源的 iscsi,用于 GlusterFS 挂载的 glusterfs,用于 RADOS 块设备的 rbd,flexVolume,cinder,cephfs,flocker,fc(光纤通道)以及其他选项。如果你没有使用它们,你不需要了解所有这些。这里提到它们是为了向你展示 Kubernetes 支持广泛的存储技术,你可以使用你喜欢的和熟悉的任何一种。
要查看为每种卷类型需要设置的属性详情,你可以查阅 Kubernetes API 参考中的 Kubernetes API 定义,或者通过 kubectl explain 查找信息,如第三章中所示。如果你已经熟悉某种特定的存储技术,使用 explain 命令应该能让你轻松地找出如何挂载正确类型的卷并在你的 pod 中使用它。
但是开发者是否需要知道所有这些信息?当创建 pod 时,开发者是否需要处理与基础设施相关的存储细节,或者这应该留给集群管理员?
让 pod 的卷指向实际的底层基础设施并不是 Kubernetes 的核心所在,对吧?例如,要求开发者指定 NFS 服务器的主机名感觉是不对的。而且这甚至不是最糟糕的。
将此类与基础设施相关的信息包含在 pod 定义中意味着 pod 定义几乎与特定的 Kubernetes 集群绑定。你不能在另一个集群中使用相同的 pod 定义。这就是为什么使用这种类型的卷不是将持久存储附加到 pod 的最佳方式。你将在下一节中了解如何改进这一点。
6.5. 将 pod 与底层存储技术解耦
我们迄今为止探索的所有持久卷类型都要求 pod 的开发者了解集群中可用的实际网络存储基础设施。例如,为了创建一个基于 NFS 的卷,开发者必须知道 NFS 导出所在的实际服务器。这与 Kubernetes 的基本理念相悖,其目标是隐藏实际基础设施,无论是对于应用程序还是其开发者,使他们免于担心基础设施的细节,并使应用程序能够在广泛的云提供商和本地数据中心之间迁移。
理想情况下,在 Kubernetes 上部署应用程序的开发者永远不需要知道底层使用的是哪种存储技术,就像他们不需要知道运行 pod 所使用的物理服务器类型一样。与基础设施相关的事务应该是集群管理员的专属领域。
当开发者为他们的应用程序需要一定量的持久存储时,他们可以从 Kubernetes 中请求它,就像他们在创建 pod 时可以请求 CPU、内存和其他资源一样。系统管理员可以配置集群,使其能够提供应用程序所需的内容。
6.5.1. 介绍 PersistentVolumes 和 PersistentVolumeClaims
为了使应用程序能够在 Kubernetes 集群中请求存储而无需处理基础设施的特定细节,引入了两种新的资源。它们是 Persistent-Volumes 和 PersistentVolumeClaims。名称可能有些误导,因为正如你在前几节中看到的,即使是常规的 Kubernetes 卷也可以用来存储持久数据。
在 pod 内部使用 PersistentVolume 比使用常规 pod 卷要复杂一些,所以让我们通过 图 6.6 来说明 pods、PersistentVolumeClaims、PersistentVolumes 和实际底层存储之间的关系。
图 6.6. PersistentVolumes 由集群管理员提供,通过 PersistentVolumeClaims 被 pod 消耗。

与开发者为他们的 pod 添加特定技术的卷不同,是集群管理员设置底层存储,然后通过在 Kubernetes API 服务器上创建 PersistentVolume 资源来将其注册到 Kubernetes 中。在创建 PersistentVolume 时,管理员指定其大小和它支持的访问模式。
当集群用户需要在他们的 pod 中使用持久存储时,他们首先创建一个 PersistentVolumeClaim 清单,指定所需的最低大小和访问模式。然后用户将 PersistentVolumeClaim 清单提交给 Kubernetes API 服务器,Kubernetes 找到合适的 PersistentVolume 并将卷绑定到请求上。
PersistentVolumeClaim 可以用作 pod 内部的一个卷。其他用户不能使用同一个 PersistentVolume,直到它通过删除绑定的 PersistentVolumeClaim 来释放。
6.5.2. 创建 PersistentVolume
让我们回顾 MongoDB 的例子,但与之前不同,你不会直接在 pod 中引用 GCE Persistent Disk。相反,你首先假设集群管理员的角色,并创建一个由 GCE Persistent Disk 支持的 PersistentVolume。然后你将扮演应用程序开发者的角色,首先声明 PersistentVolume,然后在你的 pod 中使用它。
在第 6.4.1 节中,你通过配置 GCE Persistent Disk 来设置物理存储,因此你不需要再次这样做。你所需要做的就是通过准备以下列表中显示的清单并在 API 服务器上发布它,在 Kubernetes 中创建 Persistent-Volume 资源。
列表 6.9. gcePersistentDisk PersistentVolume:mongodb-pv-gcepd.yaml
apiVersion: v1 kind: PersistentVolume metadata: name: mongodb-pv spec: capacity: 1 storage: 1Gi 1 accessModes: 2 - ReadWriteOnce 2 - ReadOnlyMany 2 persistentVolumeReclaimPolicy: Retain 3 gcePersistentDisk: 4 pdName: mongodb 4 fsType: ext4 4
-
1 定义 PersistentVolume 的大小
-
2 它可以被单个客户端用于读写,或者被多个客户端仅用于读取。
-
3 在释放声明后,PersistentVolume 应该保留(不删除或删除)。
-
4 PersistentVolume 由你之前创建的 GCE Persistent Disk 支持。
注意
如果你使用 Minikube,请使用 mongodb-pv-hostpath.yaml 文件创建 PV。
在创建 PersistentVolume 时,管理员需要告诉 Kubernetes 其容量是多少,以及是否可以被单个节点或多个节点同时读取和/或写入。他们还需要告诉 Kubernetes 在释放 PersistentVolume 时应该做什么(当它所绑定的 PersistentVolumeClaim 被删除时)。最后,但同样重要的是,他们需要指定这个 PersistentVolume 所支持的存储的类型、位置和其他属性。如果你仔细观察,这部分与之前直接在 pod 卷中引用 GCE Persistent Disk 时完全相同(如下所示)。
列表 6.10. 在 pod 的卷中引用 GCE PD
spec: volumes: - name: mongodb-data gcePersistentDisk: pdName: mongodb fsType: ext4 ...
使用kubectl create命令创建 PersistentVolume 后,它应该准备好被声明。通过列出所有 PersistentVolumes 来检查它是否可用:
$ kubectl get pv NAME CAPACITY RECLAIMPOLICY ACCESSMODES STATUS CLAIM mongodb-pv 1Gi Retain RWO,ROX Available
注意
省略了几个列。此外,pv用作persistentvolume的简称。
如预期的那样,PersistentVolume 显示为可用状态,因为你还没有创建 PersistentVolumeClaim。
注意
PersistentVolumes 不属于任何命名空间(见图 6.7)。它们是类似于节点的集群级资源。
图 6.7. PersistentVolumes,与集群节点一样,不属于任何命名空间,与 pods 和 PersistentVolumeClaims 不同。

6.5.3. 通过创建 PersistentVolumeClaim 来声明 PersistentVolume
现在,让我们放下管理员帽子,重新戴上开发者帽子。假设你需要部署一个需要持久存储的 Pod。你将使用你之前创建的持久卷。但是你不能直接在 Pod 中使用它。你需要首先声明它。
声明持久卷是一个完全独立于创建 Pod 的过程,因为即使 Pod 被重新调度(记住,重新调度意味着之前的 Pod 被删除并创建一个新的 Pod),你也希望相同的 PersistentVolumeClaim 保持可用。
创建 PersistentVolumeClaim
你现在将创建声明。你需要准备一个像以下列表中所示的 PersistentVolumeClaim 清单,并通过 kubectl create 将其发布到 Kubernetes API:
列表 6.11. 一个 PersistentVolumeClaim: mongodb-pvc.yaml
apiVersion: v1 kind: PersistentVolumeClaim metadata: name: mongodb-pvc 1 spec: resources: requests: 2 storage: 1Gi 2 accessModes: 3 - ReadWriteOnce 3 storageClassName: "" 4
-
1 声明的名称——你稍后在使用声明作为 Pod 的卷时需要这个名称。
-
2 请求 1 GiB 的存储
-
3 你希望存储支持单个客户端(执行读取和写入操作)。
-
4 你将在关于动态预配的部分了解这一点。
一旦创建声明,Kubernetes 就会找到适当的持久卷并将其绑定到声明上。持久卷的容量必须足够大,以容纳声明请求的内容。此外,卷的访问模式必须包括声明请求的访问模式。在你的情况下,声明请求了 1 GiB 的存储和一个 ReadWriteOnce 访问模式。你之前创建的持久卷符合这两个要求,因此它被绑定到你的声明上。你可以通过检查声明来看到这一点。
列出 PersistentVolumeClaims
列出所有 PersistentVolumeClaims 以查看你的 PVC 状态:
$ kubectl get pvc 名称 状态 卷 容量 访问模式 年龄 mongodb-pvc 已绑定 mongodb-pv 1Gi RWO,ROX 3s
注意
我们使用 pvc 作为 persistentvolumeclaim 的简称。
声明显示为 已绑定 到持久卷 mongodb-pv。注意访问模式所使用的缩写:
-
RWO—读写一次—只有单个节点可以挂载卷进行读写。 -
ROX—只读多—多个节点可以挂载卷进行读取。 -
RWX—读写多—多个节点可以挂载卷进行读写。
注意
RWO, ROX, 和 RWX 指的是可以同时使用卷的工作节点数量,而不是 Pod 的数量!
列出 PersistentVolumes
你也可以通过使用 kubectl get 检查来看到持久卷现在已经是 已绑定 而不再是 可用:
$ kubectl get pv 名称 CAPACITY 访问模式 状态 声明 年龄 mongodb-pv 1Gi RWO,ROX 已绑定 default/mongodb-pvc 1m
PersistentVolume 显示它绑定到了 default/mongodb-pvc 的声明。default 部分是声明所在的命名空间(你在默认命名空间中创建了声明)。我们之前已经说过,PersistentVolume 资源是集群范围的,因此不能在特定命名空间中创建,但 PersistentVolumeClaims 只能创建在特定命名空间中。它们然后只能由同一命名空间中的 pod 使用。
6.5.4. 在 pod 中使用 PersistentVolumeClaim
PersistentVolume 现在由你使用。在你释放它之前,其他人无法声明相同的卷。要在 pod 内部使用它,你需要通过 pod 的卷名称引用 PersistentVolumeClaim(是的,是 PersistentVolumeClaim,而不是 PersistentVolume 直接!),如下面的列表所示。
列表 6.12. 使用 PersistentVolumeClaim 卷的 pod:mongodb-pod-pvc.yaml
apiVersion: v1 kind: Pod metadata: name: mongodb spec: containers: - image: mongo name: mongodb volumeMounts: - name: mongodb-data mountPath: /data/db ports: - containerPort: 27017 protocol: TCP volumes: - name: mongodb-data persistentVolumeClaim: 1 claimName: mongodb-pvc 1
- 1 在 pod 卷中按名称引用 PersistentVolumeClaim
继续创建 pod。现在,检查 pod 是否确实使用了相同的 PersistentVolume 和其底层的 GCE PD。你可以通过再次运行 MongoDB shell 来查看之前存储的数据,如下面的列表所示。
列表 6.13. 使用 PVC 和 PV 在 pod 中检索 MongoDB 的持久化数据
$ kubectl exec -it mongodb mongo MongoDB shell version: 3.2.8 connecting to: mongodb://127.0.0.1:27017 Welcome to the MongoDB shell. ... > use mystore switched to db mystore > db.foo.find() { "_id" : ObjectId("57a61eb9de0cfd512374cc75"), "name" : "foo" }
就这样。你能够检索之前存储到 MongoDB 中的文档。
6.5.5. 理解使用 PersistentVolumes 和 claims 的好处
检查 图 6.8,它显示了 pod 可以使用 GCE Persistent Disk 的两种方式——直接或通过 PersistentVolume 和 claim。
图 6.8. 直接使用 GCE Persistent Disk 或通过 PVC 和 PV 使用

考虑一下使用这种从基础设施中获取存储的间接方法对于应用程序开发者(或集群用户)来说要简单得多。是的,它确实需要创建 PersistentVolume 和 Persistent-VolumeClaim 的额外步骤,但开发者不需要了解底层使用的实际存储技术。
此外,相同的 pod 和 claim 清单现在可以用于许多不同的 Kubernetes 集群,因为它们不涉及任何特定于基础设施的内容。声明指出,“我需要 x 量的存储,并且我需要能够由单个客户端一次性读写它”,然后 pod 通过其卷之一按名称引用该声明。
6.5.6. 回收 PersistentVolumes
在你结束关于 PersistentVolumes 的这一节之前,让我们做一个最后的快速实验。删除 pod 和 PersistentVolumeClaim:
$ kubectl delete pod mongodb pod "mongodb" deleted $ kubectl delete pvc mongodb-pvc persistentvolumeclaim "mongodb-pvc" deleted
如果你再次创建 PersistentVolumeClaim 会怎样?它会被绑定到 Persistent-Volume 吗?在你创建声明后,kubectl get pvc 会显示什么?
$ kubectl get pvc NAME STATUS VOLUME CAPACITY ACCESSMODES AGE mongodb-pvc Pending 13s
该声明的状态显示为 Pending。有趣。当你之前创建声明时,它立即绑定到了 PersistentVolume 上,那么为什么现在没有绑定呢?也许列出 PersistentVolumes 可以提供更多线索:
$ kubectl get pv NAME CAPACITY ACCESSMODES STATUS CLAIM REASON AGE mongodb-pv 1Gi RWO,ROX Released default/mongodb-pvc 5m
STATUS 列显示 PersistentVolume 为 Released,而不是之前的 Available。因为你已经使用了该卷,它可能包含数据,并且不应该在没有给集群管理员清理机会的情况下绑定到全新的声明。如果没有这样做,使用相同 PersistentVolume 的新 pod 可能会读取之前 pod 存储在那里的数据,即使声明和 pod 是在不同的命名空间中创建的(因此很可能属于不同的集群租户)。
手动回收 PersistentVolumes
当你创建 PersistentVolume 时,你告诉 Kubernetes 它应该这样表现——通过将其 persistentVolumeReclaimPolicy 设置为 Retain。你希望 Kubernetes 在释放其声明后保留卷及其内容。据我所知,手动回收 PersistentVolume 以使其再次可用的唯一方法是删除并重新创建 PersistentVolume 资源。当你这样做时,你决定如何处理底层存储上的文件:你可以删除它们,或者让它们保持原样,以便它们可以被下一个 pod 重新使用。
自动回收 PersistentVolumes
存在两种其他可能的回收策略:Recycle 和 Delete。第一种会删除卷的内容,并使卷可再次被声明。这样,PersistentVolume 可以被不同的 PersistentVolumeClaims 和不同的 pods 多次重用,正如你在 图 6.9 中看到的。
图 6.9. PersistentVolume、PersistentVolumeClaims 及其使用的 pods 的生命周期

另一方面,Delete 策略会删除底层存储。请注意,Recycle 选项目前对于 GCE Persistent Disks 不可用。这种类型的 PersistentVolume 只支持 Retain 或 Delete 策略。其他 Persistent-Volume 类型可能或可能不支持这些选项中的每一个,因此在创建自己的 PersistentVolume 之前,请务必检查您将在卷中使用的特定底层存储支持的回收策略。
提示
您可以在现有的 PersistentVolume 上更改其 PersistentVolume 回收策略。例如,如果它最初设置为 Delete,您可以轻松地将其更改为 Retain 以防止丢失有价值的数据。
6.6. 动态 PersistentVolumes 的配置
您已经看到,使用 PersistentVolumes 和 PersistentVolumeClaims 如何使开发者能够轻松地获得持久存储,而无需处理底层使用的实际存储技术。但这仍然需要集群管理员预先配置实际的存储。幸运的是,Kubernetes 可以通过 PersistentVolumes 的动态配置自动执行此任务。
集群管理员,而不是创建 PersistentVolumes,可以部署 PersistentVolume provisioner 并定义一个或多个 StorageClass 对象,让用户选择他们想要的 PersistentVolume 类型。用户可以在他们的 PersistentVolumeClaims 中引用 StorageClass,provisioner 将在配置持久存储时考虑这一点。
注意
与 PersistentVolumes 类似,StorageClass 资源不是命名空间化的。
Kubernetes 包含了大多数流行云提供商的 provisioner,因此管理员并不总是需要部署 provisioner。但如果 Kubernetes 部署在本地,则需要部署一个自定义 provisioner。
而不是管理员预先配置大量 PersistentVolumes,他们需要定义一个或两个(或更多)StorageClasses,并让系统在每次通过 PersistentVolumeClaim 请求时创建一个新的 PersistentVolume。这一点的好处是,不可能耗尽 PersistentVolumes(显然,您可能会耗尽存储空间)。
6.6.1. 通过 StorageClass 资源定义可用的存储类型
在用户可以创建 PersistentVolumeClaim(这将导致新的 Persistent-Volume 被配置)之前,管理员需要创建一个或多个 StorageClass 资源。以下是一个示例。
列表 6.14. StorageClass 定义:storageclass-fast-gcepd.yaml
apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: fast provisioner: kubernetes.io/gce-pd 1 parameters: 1 type: pd-ssd 2 zone: europe-west1-b 2
-
1 用于配置 PersistentVolume 的卷插件
-
2 传递给 provisioner 的参数
注意
如果使用 Minikube,请部署文件 storageclass-fast-hostpath.yaml。
存储类资源指定在 PersistentVolumeClaim 请求此存储类时用于预配 PersistentVolume 的提供者。存储类定义中定义的参数传递给提供者,并且对每个提供者插件都是特定的。
存储类使用 Google Compute Engine (GCE) 持久磁盘 (PD) 提供者,这意味着当 Kubernetes 在 GCE 上运行时可以使用它。对于其他云提供商,需要使用其他提供者。
6.6.2. 在 PersistentVolumeClaim 中请求存储类
在创建存储类资源后,用户可以在他们的 PersistentVolumeClaims 中通过名称引用存储类。
在 PersistentVolumeClaim 中请求特定存储类
您可以将您的 mongodb-pvc 修改为使用动态预配。以下列表显示了 PVC 的更新 YAML 定义。
列表 6.15. 具有动态预配的 PVC:mongodb-pvc-dp.yaml
apiVersion: v1 kind: PersistentVolumeClaim metadata: name: mongodb-pvc spec: storageClassName: fast 1 resources: requests: storage: 100Mi accessModes: - ReadWriteOnce
- 1 此 PVC 请求自定义存储类。
除了指定大小和访问模式外,您的 PersistentVolumeClaim 现在还指定了您想要使用的存储类别。当您创建请求时,PersistentVolume 由 fast 存储类资源中引用的提供者创建。即使存在与 PersistentVolumeClaim 匹配的现有手动预配的 PersistentVolume,也会使用提供者。
注意
如果在 PVC 中引用了一个不存在的存储类,PV 的预配将失败(当您在 PVC 上使用 kubectl describe 时,您将看到 ProvisioningFailed 事件)。
检查创建的 PVC 和动态预配的 PV
接下来,您将创建 PVC,然后使用 kubectl get 来查看它:
$ kubectl get pvc mongodb-pvc 名称 状态 卷 容量 访问模式 存储类 mongodb-pvc 已绑定 pvc-1e6bc048 1Gi RWO fast
VOLUME 列显示绑定到此请求的 PersistentVolume(实际名称比上面显示的更长)。您现在可以尝试列出 PersistentVolumes,以查看是否已自动创建新的 PV:
$ kubectl get pv 名称 容量 访问模式 回收策略 状态 存储类 mongodb-pv 1Gi RWO,ROX 保留 已释放 pvc-1e6bc048 1Gi RWO 删除 已绑定 fast
注意
仅显示相关列。
您可以看到动态配置的 PersistentVolume。其容量和访问模式是您在 PVC 中请求的。其回收策略是Delete,这意味着当 PVC 被删除时,PersistentVolume 也将被删除。除了 PV 之外,提供者还配置了实际的存储。您的fast StorageClass 配置为使用kubernetes.io/gce-pd提供者,该提供者配置 GCE 持久磁盘。您可以使用以下命令查看磁盘:
$ gcloud compute disks list NAME ZONE SIZE_GB TYPE STATUS gke-kubia-dyn-pvc-1e6bc048 europe-west1-d 1 pd-ssd READY gke-kubia-default-pool-71df europe-west1-d 100 pd-standard READY gke-kubia-default-pool-79cd europe-west1-d 100 pd-standard READY gke-kubia-default-pool-blc4 europe-west1-d 100 pd-standard READY mongodb europe-west1-d 1 pd-standard READY
如您所见,第一个持久磁盘的名称表明它是动态配置的,其类型显示它是一个 SSD,正如您在之前创建的存储类中指定的那样。
理解如何使用存储类
集群管理员可以创建具有不同性能或其他特性的多个存储类。然后,开发者决定他们创建的每个声明最适合哪一个。
StorageClasses 的好处是声明通过名称引用它们。因此,PVC 定义可以在不同的集群之间移植,只要所有集群中的 StorageClass 名称都相同。为了亲自体验这种可移植性,如果您之前一直在使用 GKE,您可以尝试在 Minikube 上运行相同的示例。作为集群管理员,您将不得不创建一个不同的存储类(但名称相同)。在 storageclass-fast-hostpath.yaml 文件中定义的存储类是为 Minikube 量身定制的。然后,一旦您部署了存储类,作为集群用户,您可以部署与之前完全相同的 PVC 清单和 Pod 清单。这显示了 Pod 和 PVC 如何在不同的集群之间移植。
6.6.3. 不指定存储类进行动态配置
随着我们进入本章,将持久化存储附加到 Pod 的过程变得越来越简单。本章各节反映了存储配置从早期 Kubernetes 版本到现在的演变过程。在本章的最后部分,我们将探讨将 PersistentVolume 附加到 Pod 的最新和最简单方法。
列出存储类
当您创建名为fast的自定义存储类时,您没有检查集群中是否已经定义了任何现有的存储类。为什么现在不这样做呢?以下是 GKE 中可用的存储类:
$ kubectl get sc NAME TYPE fast kubernetes.io/gce-pd standard (default) kubernetes.io/gce-pd
注意
我们使用sc作为storageclass的简称。
除了您自己创建的fast存储类之外,还存在一个standard存储类,并标记为默认。您将在稍后了解这意味着什么。让我们列出 Minikube 中可用的存储类,以便进行比较:
$ kubectl get sc NAME TYPE fast k8s.io/minikube-hostpath standard (default) k8s.io/minikube-hostpath
再次,fast存储类是由您创建的,这里也存在一个默认的standard存储类。比较两个列表中的TYPE列,您可以看到 GKE 使用的是kubernetes.io/gce-pd提供者,而 Minikube 使用的是k8s.io/minikube-hostpath。
检查默认存储类
您将使用kubectl get来查看 GKE 集群中标准存储类的更多信息,如下所示列表。
列表 6.16. GKE 上标准存储类的定义
$ kubectl get sc standard -o yaml apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: annotations: storageclass.beta.kubernetes.io/is-default-class: "true" 1 creationTimestamp: 2017-05-16T15:24:11Z labels: addonmanager.kubernetes.io/mode: EnsureExists kubernetes.io/cluster-service: "true" name: standard resourceVersion: "180" selfLink: /apis/storage.k8s.io/v1/storageclassesstandard uid: b6498511-3a4b-11e7-ba2c-42010a840014 parameters: 2 type: pd-standard 2 provisioner: kubernetes.io/gce-pd 3
-
1 这个注释将存储类标记为默认。
-
2 类型参数由提供者用于知道要创建哪种类型的 GCE PD。
-
3 GCE 持久磁盘提供者用于配置此类的 PV。
如果您仔细查看列表的顶部,存储类定义中包含一个注释,这使得它成为默认存储类。默认存储类是在 PersistentVolumeClaim 没有明确指定要使用哪个存储类时动态配置 PersistentVolume 所使用的。
创建一个未指定存储类的 PersistentVolumeClaim
您可以创建一个不指定storageClassName属性的 PVC,并且在(在 Google Kubernetes Engine 上)将为您自动配置一个类型为pd-standard的 GCE 持久磁盘。通过以下列表中的 YAML 创建一个声明来尝试一下。
列表 6.17. 未定义存储类的 PVC:mongodb-pvc-dp-nostorageclass.yaml
apiVersion: v1 kind: PersistentVolumeClaim metadata: name: mongodb-pvc2 spec: 1 resources: 1 requests: 1 storage: 100Mi 1 accessModes: 1 - ReadWriteOnce 1
- 1 您没有指定
storageClassName属性(与前面的示例不同)。
这个 PVC 定义仅包括存储大小请求和所需的访问模式,但没有存储类。当您创建 PVC 时,将使用标记为默认的任何存储类。您可以确认这一点:
$ kubectl get pvc mongodb-pvc2 NAME STATUS VOLUME CAPACITY ACCESSMODES STORAGECLASS mongodb-pvc2 Bound pvc-95a5ec12 1Gi RWO standard $ kubectl get pv pvc-95a5ec12 NAME CAPACITY ACCESSMODES RECLAIMPOLICY STATUS STORAGECLASS pvc-95a5ec12 1Gi RWO Delete Bound standard $ gcloud compute disks list NAME ZONE SIZE_GB TYPE STATUS gke-kubia-dyn-pvc-95a5ec12 europe-west1-d 1 pd-standard READY ...
强制 PersistentVolumeClaim 绑定到预配置的 PersistentVolume 之一
这最终带我们来到了为什么在列表 6.11(当您想使 PVC 绑定到您手动配置的 PV 时)中将 storageClassName 设置为空字符串的原因。让我在这里重复该 PVC 定义的相关行:
kind: PersistentVolumeClaim spec: storageClassName: "" 1
- 1 将存储类名称指定为空字符串确保 PVC 绑定到预配置的 PV 而不是动态配置一个新的。
如果您没有将 storageClassName 属性设置为空字符串,动态卷提供程序会配置一个新的 PersistentVolume,尽管已经存在合适的预配置 PersistentVolume。在那个时刻,我想展示一个请求是如何绑定到一个手动预配置的 PersistentVolume 的。我不想让动态提供程序干扰。
提示
如果您想使 PVC 使用预配置的 PersistentVolume,请显式设置 storageClassName 为 ""。
理解动态 PersistentVolume 配置的完整情况
这就带我们结束了这一章。总结来说,将持久化存储附加到 Pod 的最佳方式是仅创建 PVC(如果需要,可以显式指定 storageClassName)和 Pod(通过名称引用 PVC)。其他所有事情都由动态 PersistentVolume 提供程序处理。
要了解动态配置 PersistentVolume 所涉及的所有步骤的完整情况,请检查图 6.10。
图 6.10. PersistentVolumes 动态配置的完整情况

6.7. 概述
本章向您展示了如何使用卷为 Pod 的容器提供临时或持久存储。您已经学习了如何
-
创建一个多容器 Pod,并通过向 Pod 添加卷并在每个容器中挂载它,使 Pod 的容器操作相同的文件
-
使用
emptyDir卷来存储临时、非持久数据 -
使用
gitRepo卷在 Pod 启动时轻松填充目录,以包含 Git 仓库的内容 -
使用
hostPath卷从主机节点访问文件 -
在卷中挂载外部存储,以在 Pod 重启之间持久化 Pod 数据
-
通过使用 PersistentVolumes 和 PersistentVolumeClaims 解耦 Pod 与存储基础设施
-
为每个 PersistentVolumeClaim 动态配置所需(或默认)的 PersistentVolumes
-
当你希望 PersistentVolumeClaim 绑定到预先配置的 PersistentVolume 时,防止动态配置器干扰
在下一章中,你将看到 Kubernetes 提供了哪些机制来提供配置数据、秘密信息和关于 pod 和容器的元数据给 pod 内部运行的过程。这是通过我们在本章中提到的特殊类型的卷来完成的,但尚未进行探索。
第七章. ConfigMaps 和 Secrets:配置应用程序
本章涵盖
-
更改容器的主体进程
-
将命令行选项传递给应用程序
-
设置对应用暴露的环境变量
-
通过
ConfigMaps 配置应用程序 -
通过
Secrets 传递敏感信息
到目前为止,你还没有必要将任何类型的配置数据传递给你在本书练习中运行的应用程序。因为几乎所有的应用程序都需要配置(不同部署实例之间的设置、访问外部系统的凭证等),这些配置不应该烘焙到构建的应用程序本身中,让我们看看如何在 Kubernetes 中运行应用程序时传递配置选项。
7.1. 配置容器化应用程序
在我们介绍如何将配置数据传递给在 Kubernetes 中运行的应用程序之前,让我们看看容器化应用程序通常是如何配置的。
如果你忽略了可以将配置烘焙到应用程序本身的事实,那么在开始开发新应用程序时,你通常会从通过命令行参数配置应用程序开始。然后,随着配置选项列表的增长,你可以将配置移动到一个配置文件中。
另一种在容器化应用程序中广泛流行的方法是通过环境变量将配置选项传递给应用程序。而不是让应用程序读取配置文件或命令行参数,应用程序会查找某个环境变量的值。例如,官方 MySQL 容器镜像使用名为MYSQL_ROOT_PASSWORD的环境变量来设置 root 超级用户账户的密码。
但为什么环境变量在容器中如此受欢迎?在 Docker 容器内部使用配置文件有点棘手,因为你必须将配置文件烘焙到容器镜像本身中,或者将包含文件的卷挂载到容器中。显然,将文件烘焙到镜像中类似于将配置硬编码到应用程序的源代码中,因为这要求你每次想要更改配置时都必须重新构建镜像。此外,任何可以访问镜像的人都可以看到配置,包括任何应该保密的信息,如凭证或加密密钥。使用卷更好,但仍需要你确保在容器启动之前文件已写入卷。
如果你已经阅读了上一章,你可能会考虑使用gitRepo卷作为配置源。这并不是一个坏主意,因为它允许你保持配置的版本控制良好,并在必要时轻松回滚配置更改。但有一个更简单的方法,允许你将配置数据放入顶级 Kubernetes 资源中,并将所有其他资源定义存储在同一 Git 仓库或任何其他基于文件的存储中。用于存储配置数据的 Kubernetes 资源称为 ConfigMap。我们将在本章中学习如何使用它。
无论你是否使用 ConfigMap 来存储配置数据,你都可以通过以下方式配置你的应用程序:
-
向容器传递命令行参数
-
为每个容器设置自定义环境变量
-
通过特殊类型的卷将配置文件挂载到容器中
我们将在接下来的几节中详细讨论所有这些选项,但在开始之前,让我们从安全的角度来审视配置选项。尽管大多数配置选项不包含任何敏感信息,但其中一些确实包含。这些包括凭证、私有加密密钥以及需要保持安全的数据。这类信息需要特别小心处理,这也是为什么 Kubernetes 提供了一种称为 Secret 的一类一等对象。我们将在本章的最后部分了解它。
7.2. 向容器传递命令行参数
到目前为止的所有示例中,你创建的容器都运行了容器镜像中定义的默认命令,但 Kubernetes 允许在你想运行不同于镜像中指定的可执行文件,或者想用不同的命令行参数运行它时,作为 pod 容器定义的一部分来覆盖命令。我们现在将看看如何做到这一点。
7.2.1. 在 Docker 中定义命令和参数
我需要首先解释的是,在容器中执行的整体命令由两部分组成:命令和参数。
理解 ENTRYPOINT 和 CMD
在 Dockerfile 中,两条指令定义了两个部分:
-
ENTRYPOINT定义了容器启动时调用的可执行文件。 -
CMD指定传递给ENTRYPOINT的参数。
虽然你可以使用CMD指令来指定在运行镜像时要执行的命令,但正确的方式是通过ENTRYPOINT指令来执行,并且只有当你想定义默认参数时才指定CMD。然后可以不指定任何参数来运行镜像
$ docker run <image>
或者使用额外的参数,这些参数将覆盖在 Dockerfile 中CMD下设置的任何内容:
$ docker run <image> <arguments>
理解 shell 和 exec 形式的区别
但还有更多。这两条指令支持两种不同的形式:
-
shell形式——例如,ENTRYPOINT node app.js.。 -
exec形式——例如,ENTRYPOINT ["node", "app.js"]。
区别在于指定的命令是否在 shell 内部调用。
在你创建的 kubia 镜像中,你使用了ENTRYPOINT指令的exec形式:
ENTRYPOINT ["node", "app.js"]
这将直接运行 node 进程(不在 shell 内部),正如你可以通过列出容器内部正在运行的过程所看到的那样:
$ docker exec 4675d ps x PID TTY STAT TIME COMMAND 1 ? Ssl 0:00 node app.js 12 ? Rs 0:00 ps x
如果您使用了shell形式(ENTRYPOINT node app.js),这些将是容器的进程:
$ docker exec -it e4bad ps x PID TTY STAT TIME COMMAND 1 ? Ss 0:00 /bin/sh -c node app.js 7 ? Sl 0:00 node app.js 13 ? Rs+ 0:00 ps x
如您所见,在这种情况下,主进程(PID 1)将是shell进程而不是 node 进程。node 进程(PID 7)将从该 shell 启动。shell进程是不必要的,这就是为什么您应该始终使用ENTRYPOINT指令的exec形式。
在您的 fortune 镜像中使间隔可配置
让我们修改您的 fortune 脚本和镜像,以便循环中的延迟间隔可配置。您将添加一个INTERVAL变量,并将其初始化为第一个命令行参数的值,如下所示。
列表 7.1. 通过参数可配置间隔的 fortune 脚本:fortune-args/fortuneloop.sh
#!/bin/bash trap "exit" SIGINT INTERVAL=$1 echo Configured to generate new fortune every $INTERVAL seconds mkdir -p /var/htdocs while : do echo $(date) Writing fortune to /var/htdocs/index.html /usr/games/fortune > /var/htdocs/index.html sleep $INTERVAL done
您已添加或修改了粗体字体的行。现在,您将修改 Dockerfile,使其使用ENTRYPOINT指令的exec版本,并使用CMD指令将默认间隔设置为 10 秒,如下所示。
列表 7.2. 更新后的fortune镜像的 Dockerfile:fortune-args/Dockerfile
FROM ubuntu:latest RUN apt-get update ; apt-get -y install fortune ADD fortuneloop.sh /bin/fortuneloop.sh ENTRYPOINT ["/bin/fortuneloop.sh"] 1 CMD ["10"] 2
-
ENTRYPOINT指令的 exec 形式
-
- 可执行文件的默认参数
您现在可以构建并将镜像推送到 Docker Hub。这次,您将使用args而不是latest标记镜像:
$ docker build -t docker.io/luksa/fortune:args . $ docker push docker.io/luksa/fortune:args
您可以通过在本地使用 Docker 运行它来测试镜像:
$ docker run -it docker.io/luksa/fortune:args Configured to generate new fortune every 10 seconds Fri May 19 10:39:44 UTC 2017 Writing fortune to /var/htdocs/index.html
注意
您可以使用 Control+C 停止脚本。
您可以通过传递它作为参数来覆盖默认的睡眠间隔:
$ docker run -it docker.io/luksa/fortune:args 15 Configured to generate new fortune every 15 seconds
现在您确信您的镜像尊重传递给它的参数,让我们看看如何在 pod 中使用它。
7.2.2. 覆盖 Kubernetes 中的命令和参数
在 Kubernetes 中,当指定容器时,您可以选择覆盖ENTRYPOINT和CMD。为此,您在容器规范中设置command和args属性,如下所示。
列表 7.3. 指定自定义命令和参数的 pod 定义
kind: Pod spec: containers: - image: some/image command: ["/bin/command"]``args: ["arg1", "arg2", "arg3"]
在大多数情况下,你只会设置自定义参数,很少会覆盖命令(除非是在通用镜像,如 busybox,它根本未定义 ENTRYPOINT)。
注意
command 和 args 字段在 Pod 创建后不能更新。
两个 Dockerfile 指令及其等效的 Pod 规范字段显示在表 7.1 中。
表 7.1. 在 Docker 与 Kubernetes 中指定可执行文件及其参数
| Docker | Kubernetes | 描述 |
|---|---|---|
| ENTRYPOINT | command | 在容器内执行的可执行文件 |
| CMD | args | 传递给可执行文件的参数 |
使用自定义间隔运行 fortune Pod
要使用自定义延迟间隔运行 fortune Pod,你需要将你的 fortune-pod.yaml 复制到 fortune-pod-args.yaml 并按以下列表进行修改。
列表 7.4. 在 Pod 定义中传递参数:fortune-pod-args.yaml
apiVersion: v1 kind: Pod metadata: name: fortune2s``1 spec: containers: - image: luksa/fortune:args``2``args: ["2"]``3 name: html-generator volumeMounts: - name: html mountPath: /var/htdocs ...
-
1 你更改了 Pod 的名称。
-
2 使用 fortune:args 而不是 fortune:latest
-
3 此参数使脚本每两秒生成一个新的 fortune。
你已将 args 数组添加到容器定义中。现在尝试创建此 Pod。数组中的值将在容器运行时作为命令行参数传递。
在此列表中使用的数组表示法,如果你只有一个或几个参数时非常好用。如果你有多个参数,你也可以使用以下表示法:
args: - foo - bar - "15"
小贴士
你不需要将字符串值用引号括起来(但必须将数字括起来)。
指定参数是向容器通过命令行参数传递配置选项的一种方式。接下来,你将看到如何通过环境变量来实现。
7.3. 为容器设置环境变量
正如我之前提到的,容器化应用程序通常使用环境变量作为配置选项的来源。Kubernetes 允许你为 Pod 的每个容器指定一个自定义的环境变量列表,如图 7.1 所示。图 7.1。虽然定义 Pod 级别的环境变量并使其被其容器继承将非常有用,但目前尚无此选项。
注意
与容器的命令和参数一样,环境变量列表在创建 Pod 之后也不能更新。
图 7.1. 可以为每个容器设置环境变量。

通过环境变量使 fortune 镜像的间隔可配置
让我们再次看看如何修改你的 fortuneloop.sh 脚本,以便它可以从环境变量中进行配置,如下面的列表所示。
列表 7.5. 通过环境变量配置间隔的 fortune 脚本:fortune-env/fortuneloop.sh
#!/bin/bash trap "exit" SIGINT echo Configured to generate new fortune every $INTERVAL seconds mkdir -p /var/htdocs while : do echo $(date) Writing fortune to /var/htdocs/index.html /usr/games/fortune > /var/htdocs/index.html sleep $INTERVAL done
你所需要做的就是删除初始化 INTERVAL 变量的那一行。因为你的“应用”是一个简单的 bash 脚本,所以你不需要做其他任何事情。如果应用是用 Java 编写的,你会使用 System.getenv("INTERVAL"),而在 Node.JS 中你会使用 process.env.INTERVAL,在 Python 中你会使用 os.environ['INTERVAL']。
7.3.1. 在容器定义中指定环境变量
在构建新的镜像(这次我将其标记为 luksa/fortune:env)并将其推送到 Docker Hub 后,你可以通过创建一个新的 pod 来运行它,在容器定义中包含环境变量,如下面的列表所示。
列表 7.6. 在 pod 中定义环境变量:fortune-pod-env.yaml
kind: Pod spec: containers: - image: luksa/fortune:env env:``1``- name: INTERVAL``1``value: "30"``1 name: html-generator ...
- 1 向环境变量列表中添加单个变量
如前所述,你是在容器定义中设置环境变量,而不是在 pod 层级上。
注意
不要忘记,在每个容器中,Kubernetes 还会自动为同一命名空间中的每个服务暴露环境变量。这些环境变量基本上是自动注入的配置。
7.3.2. 在变量的值中引用其他环境变量
在前面的例子中,你为环境变量设置了一个固定值,但你也可以通过使用 $(VAR) 语法来引用先前定义的环境变量或任何其他现有变量。如果你定义了两个环境变量,第二个可以包含第一个的值,如下面的列表所示。
列表 7.7. 在另一个环境变量中引用环境变量
env: - name: FIRST_VAR value: "foo" - name: SECOND_VAR value: "$(FIRST_VAR)bar"
在这种情况下,SECOND_VAR 的值将是 "foobar"。同样,你曾在 第 7.2 节 中学到的 command 和 args 属性也可以这样引用环境变量。你将在 第 7.4.5 节 中使用这种方法。
7.3.3. 理解硬编码环境变量的缺点
将值硬编码在 Pod 定义中实际上意味着你需要为生产环境和开发环境分别拥有不同的 Pod 定义。为了在多个环境中重用相同的 Pod 定义,将配置与 Pod 描述符解耦是有意义的。幸运的是,你可以使用 ConfigMap 资源来实现这一点,并使用valueFrom字段而不是value字段作为环境变量值的来源。你将在下一节中了解这一点。
7.4. 使用 ConfigMap 解耦配置
应用程序配置的整个目的是将不同环境之间或频繁变化的配置选项与应用程序的源代码分开。如果你将 Pod 描述符视为应用程序的源代码(在微服务架构中这确实是如此,因为它定义了如何将单个组件组合成一个功能系统),那么很明显你应该将配置从 Pod 描述符中移出。
7.4.1. 介绍 ConfigMap
Kubernetes 允许将配置选项分离到称为 ConfigMap 的单独对象中,它是一个包含键/值对的映射,其值范围从简短的字面量到完整的配置文件。
应用程序不需要直接读取 ConfigMap 或甚至知道它的存在。映射的内容作为环境变量或作为卷中的文件传递给容器(见图 7.2)。并且因为可以使用$(ENV_VAR)语法在命令行参数中引用环境变量,所以你还可以将 ConfigMap 条目作为命令行参数传递给进程。
图 7.2. Pods 通过环境变量和configMap卷使用 ConfigMap。

当然,如果需要,应用程序也可以直接通过 Kubernetes REST API 端点读取 ConfigMap 的内容,但除非你真的有这个需求,否则你应该尽可能保持你的应用程序与 Kubernetes 无关。
无论应用程序如何消费 ConfigMap,将配置保存在这样的独立对象中,都允许你为具有相同名称的 ConfigMap 保留多个清单,每个清单针对不同的环境(开发、测试、QA、生产等)。因为 Pod 通过名称引用 ConfigMap,所以你可以在每个环境中使用不同的配置,同时在所有环境中使用相同的 Pod 规范(见图 7.3)。
图 7.3. 在不同环境中使用相同名称的两种不同的 ConfigMap

7.4.2. 创建 ConfigMap
让我们看看如何在您的 Pod 中使用 ConfigMap。首先,从最简单的例子开始,你将首先创建一个包含单个键的映射,并使用它来填充您之前示例中的INTERVAL环境变量。你将使用特殊的kubectl create configmap命令来创建 ConfigMap,而不是使用通用的kubectl create -f命令来发布 YAML。
使用 kubectl create configmap 命令
你可以通过向 kubectl 命令传递字面量来定义映射的条目,或者你可以从你的磁盘上的文件创建 ConfigMap。首先使用一个简单的字面量:
$ kubectl create configmap fortune-config --from-literal=sleep-interval=25 configmap "fortune-config" created
注意
ConfigMap 键必须是有效的 DNS 子域(它们只能包含字母数字字符、破折号、下划线和点)。它们可以可选地包含一个前导点。
这将创建一个名为 fortune-config 的 ConfigMap,包含单个条目 sleep-interval =25 (图 7.4)。
图 7.4. 包含单个条目的 fortune-config ConfigMap

ConfigMap 通常包含多个条目。要创建包含多个字面条目的 ConfigMap,你需要在命令中添加多个 --from-literal 参数:
$ kubectl create configmap myconfigmap
--from-literal=foo=bar --from-literal=bar=baz --from-literal=one=two
让我们检查使用 kubectl get 命令创建的 ConfigMap 的 YAML 描述符,如下所示。
列表 7.8. 一个 ConfigMap 定义
$ kubectl get configmap fortune-config -o yaml apiVersion: v1 data: sleep-interval: "25" 1 kind: ConfigMap 2 metadata: creationTimestamp: 2016-08-11T20:31:08Z name: fortune-config 3 namespace: default resourceVersion: "910025" selfLink: /api/v1/namespaces/default/configmaps/fortune-config uid: 88c4167e-6002-11e6-a50d-42010af00237
-
1 该映射中的单个条目
-
2 此描述符描述了一个 ConfigMap。
-
3 该映射的名称(你将通过此名称引用它)
没有什么特别之处。你很容易就能自己编写这个 YAML(当然,你不需要在 metadata 部分指定任何内容,除了名称之外)并将其发布到 Kubernetes API 中,使用众所周知的
$ kubectl create -f fortune-config.yaml
从文件内容创建 ConfigMap 条目
ConfigMap 还可以存储粗粒度的配置数据,例如完整的配置文件。为此,kubectl create configmap 命令还支持从磁盘读取文件并将它们作为 ConfigMap 中的单独条目存储:
$ kubectl create configmap my-config --from-file=config-file.conf
当你运行前面的命令时,kubectl 会查找你在其中运行 kubectl 的目录中的 config-file.conf 文件。然后,它将在 ConfigMap 中以 config-file.conf 为键存储文件的全部内容(文件名用作映射键),但你也可以像这样手动指定键:
$ kubectl create configmap my-config --from-file=customkey=config-file.conf
此命令将在键 customkey 下存储文件的全部内容。与字面量一样,你可以通过多次使用 --from-file 参数添加多个文件。
从目录中的文件创建 ConfigMap
你甚至可以导入文件目录中的所有文件,而不是逐个导入每个文件:
$ kubectl create configmap my-config --from-file=/path/to/dir
在这种情况下,kubectl将为指定目录中的每个文件创建单独的映射条目,但仅限于文件名是有效的 ConfigMap 键。
结合不同的选项
当创建 ConfigMap 时,你可以使用这里提到的所有选项的组合(请注意,这些文件不包括在本书的代码存档中——如果你想要尝试命令,你可以自己创建它们):
$ kubectl create configmap my-config 
--from-file=foo.json 1
--from-file=bar=foobar.conf 2
--from-file=config-opts/ 3
--from-literal=some=thing 4
-
1 一个单独的文件
-
2 存储在自定义键下的文件
-
3 整个目录
-
4 一个字面值
在这里,你已从多个来源创建了 ConfigMap:整个目录、一个文件、另一个文件(但存储在自定义键下而不是使用文件名作为键),以及一个字面值。图 7.5 显示了所有这些来源和生成的 ConfigMap。
图 7.5. 从单个文件、目录和字面值创建 ConfigMap

7.4.3. 将 ConfigMap 条目作为环境变量传递给容器
你现在如何将这个映射的值放入一个容器的容器中?你有三个选项。让我们从最简单的开始——设置一个环境变量。你将使用我提到的valueFrom字段,在第 7.3.3 节。Pod 描述符应该看起来像以下列表。
列表 7.9. 从 ConfigMap 获取env var的 Pod:fortune-pod-env-configmap.yaml
apiVersion: v1 kind: Pod metadata: name: fortune-env-from-configmap spec: containers: - image: luksa/fortune:env env: 1- name: INTERVAL1valueFrom:2configMapKeyRef:2name: fortune-config3key: sleep-interval4 ...`
-
1 你正在设置名为 INTERVAL 的环境变量。
-
2 你不是设置一个固定值,而是从 ConfigMap 键初始化它。
-
3 引用的 ConfigMap 的名称
-
4 你正在将变量设置为 ConfigMap 中此键下存储的任何内容。
你定义了一个名为INTERVAL的环境变量,并将其值设置为存储在fortune-config ConfigMap 中sleep-interval键下的任何内容。当在html-generator容器中运行的进程读取INTERVAL环境变量时,它将看到值25(如图 7.6 所示)。
图 7.6. 将 ConfigMap 条目作为环境变量传递给容器

在 Pod 中引用不存在的 ConfigMap
您可能会想知道,当您创建 pod 时,如果引用的 ConfigMap 不存在会发生什么。Kubernetes 会正常调度 pod 并尝试运行其容器。引用不存在 ConfigMap 的容器将无法启动,但其他容器将正常启动。如果您随后创建了缺失的 ConfigMap,失败的容器将启动,而无需您重新创建 pod。
注意
您还可以将 ConfigMap 的引用标记为可选(通过设置 configMapKeyRef.optional: true)。在这种情况下,即使 ConfigMap 不存在,容器也会启动。
此示例展示了如何将配置与 pod 规范解耦。这允许您将所有配置选项紧密地放在一起(即使对于多个 pod),而不是让它们散布在 pod 定义中(或在多个 pod 清单中重复)。
7.4.4. 一次性将 ConfigMap 的所有条目作为环境变量传递
当您的 ConfigMap 包含的条目不止几个时,从每个条目单独创建环境变量变得既繁琐又容易出错。幸运的是,Kubernetes 版本 1.6 提供了一种方法,可以将 ConfigMap 的所有条目作为环境变量暴露。
想象一下有一个名为 FOO、BAR 和 FOO-BAR 的三个键的 ConfigMap。您可以通过使用 envFrom 属性将它们全部暴露为环境变量,而不是像之前示例中那样使用 env。以下列表显示了一个示例。
列表 7.10. 包含 ConfigMap 所有条目的 env 变量的 Pod
spec: containers: - image: some-image envFrom: - prefix: CONFIG_ 1 configMapRef: 2 name: my-config-map 2 ...
-
1 使用 envFrom 而不是 env
-
2 所有环境变量都将带有前缀 CONFIG_。
-
3 引用名为 my-config-map 的 ConfigMap
如您所见,您还可以为环境变量指定一个前缀(在这种情况下为 CONFIG_)。这导致以下两个环境变量存在于容器内部:CONFIG_FOO 和 CONFIG_BAR。
注意
前缀是可选的,所以如果您省略它,环境变量将与键具有相同的名称。
你注意到我说了两个变量,但之前我说 ConfigMap 有三个条目(FOO、BAR 和 FOO-BAR)吗?为什么没有 FOO-BAR ConfigMap 条目的环境变量?
原因是 CONFIG_FOO-BAR 不是一个有效的环境变量名称,因为它包含一个连字符。Kubernetes 不会以任何方式转换键(例如,它不会将连字符转换为下划线)。如果 ConfigMap 键不是正确的格式,它会跳过该条目(但它会记录一个事件通知您它跳过了它)。
7.4.5. 将 ConfigMap 条目作为命令行参数传递
现在,让我们也看看如何将 ConfigMap 的值作为参数传递给容器中运行的主进程。你无法直接在pod.spec.containers.args字段中引用 ConfigMap 条目,但你可以首先从 ConfigMap 条目初始化一个环境变量,然后像图 7.7 中所示的那样在参数中引用该变量。
图 7.7. 将 ConfigMap 条目作为命令行参数传递

列表 7.11 展示了如何在 YAML 中完成这个例子。
列表 7.11. 使用 ConfigMap 条目作为参数:fortune-pod-args-configmap.yaml
apiVersion: v1 kind: Pod metadata: name: fortune-args-from-configmap spec: containers: - image: luksa/fortune:args 1 env: 2 - name: INTERVAL 2 valueFrom: 2 configMapKeyRef: 2 name: fortune-config 2 key: sleep-interval 2 args: ["$(INTERVAL)"] 3 ...
-
1 使用从第一个参数而不是从环境变量获取间隔的镜像
-
2 正如之前定义环境变量一样
-
3 在参数中引用环境变量
你定义环境变量的方式与之前完全相同,但之后你使用了$(ENV_VARIABLE_NAME)语法来让 Kubernetes 将变量的值注入到参数中。
7.4.6. 使用 configMap 卷将 ConfigMap 条目作为文件暴露
将配置选项作为环境变量或命令行参数传递通常用于短变量值。正如你所见,ConfigMap 也可以包含整个配置文件。当你想要将这些暴露给容器时,你可以使用我在上一章中提到的特殊卷类型之一,即configMap卷。
configMap卷将 ConfigMap 的每个条目都暴露为一个文件。容器中运行的进程可以通过读取文件的 内容来获取条目的值。
虽然这种方法主要用于将大型配置文件传递到容器中,但没有任何东西阻止你以这种方式传递短的单个值。
创建 ConfigMap
代替再次修改你的fortuneloop.sh脚本,你现在将尝试一个不同的例子。你将使用一个配置文件来配置运行在fortune pod 的 web-server 容器内的 Nginx web 服务器。假设你希望你的 Nginx 服务器压缩它发送给客户端的响应。为了启用压缩,Nginx 的配置文件需要看起来像以下列表。
列表 7.12. 启用 gzip 压缩的 Nginx 配置:my-nginx-config.conf
server { listen 80; server_name www.kubia-example.com; gzip on; 1 gzip_types text/plain application/xml; 1 location / { root /usr/share/nginx/html; index index.html index.htm; } }
- 1 这将启用纯文本和 XML 文件的 gzip 压缩。
现在删除现有的 fortune-config ConfigMap,使用 kubectl delete configmap fortune-config,以便你可以用包含 Nginx 配置文件的新一个替换它。你将从存储在本地磁盘上的文件创建 ConfigMap。
创建一个名为 configmap-files 的新目录,并将前一个列表中的 Nginx 配置存储到 configmap-files/my-nginx-config.conf 中。为了使 ConfigMap 也包含 sleep-interval 条目,在同一个目录中添加一个名为 sleep-interval 的纯文本文件,并在其中存储数字 25(参见图 7.8)。
图 7.8. configmap-files 目录及其文件的内容

现在创建一个 ConfigMap,如下所示:
$ kubectl create configmap fortune-config --from-file=configmap-files configmap "fortune-config" created
下面的列表显示了此 ConfigMap 的 YAML 格式。
列表 7.13. 从文件创建的 ConfigMap 的 YAML 定义
$ kubectl get configmap fortune-config -o yaml apiVersion: v1 data: my-nginx-config.conf: | 1 server { 1 listen 80; 1 server_name www.kubia-example.com; 1 gzip on; 1 gzip_types text/plain application/xml; 1 location / { 1 root /usr/share/nginx/html; 1 index index.html index.htm; 1 } 1 } 1 sleep-interval: | 2 25 2 kind: ConfigMap ...
-
1 保存 Nginx 配置文件内容的条目
-
2 保存 sleep-interval 条目的条目
注意
在两个条目的第一行冒号后面的管道字符表示后面跟着一个字面多行值。
ConfigMap 包含两个条目,其键对应于它们创建的文件的实际名称。你现在将使用 ConfigMap 中的两个容器。
在卷中使用 ConfigMap 的条目
创建一个包含 ConfigMap 内容的卷与通过名称引用 ConfigMap 并在容器中挂载卷一样简单。你已经学习了如何创建卷和挂载它们,所以剩下要学习的就是如何使用 ConfigMap 的条目初始化卷。
Nginx 从 /etc/nginx/nginx.conf 读取其配置文件。Nginx 镜像已经包含了这个文件,并带有默认配置选项,你不希望覆盖这些选项,因此你不想替换整个文件。幸运的是,默认配置文件自动包含 /etc/nginx/conf.d/ 子目录下的所有 .conf 文件,所以你应该在那里添加你的配置文件。图 7.9 显示了你想要实现的内容。
图 7.9. 将 ConfigMap 条目作为卷中的文件传递给 pod

pod 描述符在列表 7.14 中显示(省略了无关部分,但你可以从代码存档中找到完整的文件)。
列表 7.14. 将 ConfigMap 条目挂载为文件的 pod:fortune-pod-configmap-volume.yaml
apiVersion: v1 kind: Pod metadata: name: fortune-configmap-volume spec: containers: - image: nginx:alpine name: web-server volumeMounts: ... - name: config mountPath: /etc/nginx/conf.d 1 readOnly: true ... volumes: ... - name: config configMap: 2 name: fortune-config 2 ...
-
1 你正在将 configMap 卷挂载到这个位置。
-
2 该卷指的是你的 fortune-config ConfigMap。
这个 pod 定义包括一个卷,它引用了你的 fortune-config Config-Map。你将卷挂载到 /etc/nginx/conf.d 目录,以便 Nginx 使用它。
验证 Nginx 是否正在使用挂载的配置文件
现在 Web 服务器应该被配置为压缩它发送的响应。你可以通过从 localhost:8080 到 pod 的端口 80 启用端口转发,并使用 curl 检查服务器的响应来验证这一点,如下面的列表所示。
列表 7.15. 检查 nginx 响应是否启用了压缩
$ kubectl port-forward fortune-configmap-volume 8080:80 & Forwarding from 127.0.0.1:8080 -> 80 Forwarding from [::1]:8080 -> 80 $ curl -H "Accept-Encoding: gzip" -I localhost:8080 HTTP/1.1 200 OK Server: nginx/1.11.1 Date: Thu, 18 Aug 2016 11:52:57 GMT Content-Type: text/html Last-Modified: Thu, 18 Aug 2016 11:52:55 GMT Connection: keep-alive ETag: W/"57b5a197-37" Content-Encoding: gzip 1
- 1 这表明响应已被压缩。
检查挂载的 configMap 卷的内容
响应显示你已经实现了想要的结果,但现在让我们看看 /etc/nginx/conf.d 目录现在有什么:
$ kubectl exec fortune-configmap-volume -c web-server ls /etc/nginx/conf.d my-nginx-config.conf sleep-interval
ConfigMap 中的两个条目已经被添加到目录中。sleep-interval 条目也被包括在内,尽管它不应该在那里,因为它仅意味着要由 fortuneloop 容器使用。你可以创建两个不同的 ConfigMap,并使用一个来配置 fortuneloop 容器,另一个来配置 web-server 容器。但不知何故,使用多个 ConfigMap 来配置同一 pod 的容器感觉是错误的。毕竟,同一个 pod 中的容器意味着它们紧密相关,可能也应该作为一个单元来配置。
在卷中暴露某些 ConfigMap 条目
幸运的是,你可以只使用 ConfigMap 条目的一部分来填充 configMap 卷——在你的情况下,只有 my-nginx-config.conf 条目。这不会影响 fortuneloop 容器,因为你通过环境变量而不是通过卷将 sleep-interval 条目传递给它。
要定义哪些条目应作为文件在 configMap 卷中暴露,请使用卷的 items 属性,如下面的列表所示。
列表 7.16. 将特定的 ConfigMap 条目挂载到文件目录中的 pod:fortune-pod-configmap-volume-with-items.yaml
volumes: - name: config configMap: name: fortune-config items: 1 - key: my-nginx-config.conf 2 path: gzip.conf 3
-
1 通过列出条目来选择要包含在卷中的条目
-
2 你希望包含此键下的条目。
-
3 条目的值应存储在此文件中。
当指定单个条目时,你需要为每个单个条目设置文件名以及条目的键。如果你从上一个列表中运行 pod,/etc/nginx/conf.d 目录将保持整洁,因为它只包含 gzip.conf 文件,没有其他文件。
理解挂载目录会隐藏该目录中的现有文件
在这一点上,有一件重要的事情需要讨论。在这两个例子中,你都将卷挂载为目录,这意味着你隐藏了存储在容器镜像中的 /etc/nginx/conf.d 目录中的任何文件。
这通常是在 Linux 中将文件系统挂载到非空目录时发生的情况。然后该目录只包含挂载的文件系统中的文件,而该目录中原有的文件在文件系统挂载期间无法访问。
在你的情况下,这没有严重的副作用,但想象一下将卷挂载到通常包含许多重要文件的 /etc 目录。这很可能会破坏整个容器,因为应该位于 /etc 目录中的所有原始文件将不再存在。如果你需要将文件添加到像 /etc 这样的目录中,你根本不能使用这种方法。
将单个 ConfigMap 条目作为文件挂载,而不隐藏目录中的其他文件
自然地,你现在想知道如何将 ConfigMap 中的单个文件添加到现有目录中,而不会隐藏其中存储的现有文件。volumeMount 上的附加 subPath 属性允许你挂载卷中的单个文件或单个目录,而不是挂载整个卷。也许这通过视觉方式更容易解释(见图 7.10)。
图 7.10. 从卷中挂载单个文件

假设你有一个包含 myconfig.conf 文件的 configMap 卷,你希望将其添加到 /etc 目录中作为 someconfig.conf。你可以使用 subPath 属性将其挂载到那里,而不会影响该目录中的其他任何文件。pod 定义的相应部分如下所示。
列表 7.17. 将特定的 ConfigMap 条目挂载到特定文件中的 pod
spec: containers: - image: some/image volumeMounts: - name: myvolume mountPath: /etc/someconfig.conf 1 subPath: myconfig.conf 2
-
1 你正在挂载到文件,而不是目录。
-
2 你不是挂载整个卷,而是只挂载 myconfig.conf 条目。
当挂载任何类型的卷时,可以使用subPath属性。您不必挂载整个卷,而是可以挂载其一部分。但是,这种方法挂载单个文件与文件更新相关联的缺点相对较大。您将在下一节中了解更多关于此信息,但首先,让我们通过简要说明文件权限来结束对configMap卷初始状态的讨论。
设置 configMap 卷中文件的文件权限
默认情况下,configMap卷中所有文件的权限设置为 644(-rw-r--r--)。您可以通过在卷规范中设置defaultMode属性来更改此设置,如下所示。
列表 7.18. 设置文件权限:fortune-pod-configmap-volume-defaultMode.yaml
volumes: - name: config configMap: name: fortune-config defaultMode: "6600" 1
- 1 这将设置所有文件的权限为-rw-rw-----。
虽然 ConfigMaps 应用于非敏感配置数据,但您可能只想让文件对其所属的用户和组可读和可写,正如前一个示例所示。
7.4.7. 无需重启应用程序即可更新应用程序的配置
我们已经说过,使用环境变量或命令行参数作为配置源的一个缺点是,在进程运行时无法更新它们。使用 ConfigMap 并通过卷公开它,可以在不重新创建 Pod 或甚至重启容器的情况下更新配置。
当您更新 ConfigMap 时,所有引用它的卷中的文件都会更新。然后,取决于进程检测到它们已更改并重新加载它们。但 Kubernetes 最终可能也会支持在更新文件后向容器发送信号。
警告
注意,在我撰写本文时,在更新 ConfigMap 后,文件更新所需的时间出奇地长(可能需要整整一分钟)。
编辑 ConfigMap
让我们看看您如何更改 ConfigMap,并让在 Pod 中运行的进程重新加载configMap卷中公开的文件。您将修改之前示例中的 Nginx 配置文件,并让 Nginx 使用新的配置而不重启 Pod。尝试通过使用kubectl edit编辑fortune-config ConfigMap 来关闭 gzip 压缩:
$ kubectl edit configmap fortune-config
一旦您的编辑器打开,将gzip on行更改为gzip off,保存文件,然后关闭编辑器。然后 ConfigMap 将被更新,不久之后,卷中的实际文件也将更新。您可以通过使用kubectl exec打印文件的 内容来确认这一点:
$ kubectl exec fortune-configmap-volume -c web-server
cat /etc/nginx/conf.d/my-nginx-config.conf
如果你还没有看到更新,请稍等片刻再试一次。文件更新需要一段时间。最终,你会在配置文件中看到变化,但你可能会发现这并没有对 Nginx 产生影响,因为它不会监视文件并自动重新加载它们。
向 Nginx 发送信号以重新加载配置
Nginx 将继续压缩其响应,直到你告诉它重新加载其配置文件,你可以使用以下命令来完成:
$ kubectl exec fortune-configmap-volume -c web-server -- nginx -s reload
现在,如果你再次使用 curl 尝试访问服务器,你应该会看到响应不再被压缩(不再包含 Content-Encoding: gzip 标头)。你实际上已经更改了应用的配置,而无需重新启动容器或重新创建 pod。
理解文件是如何原子性地更新的
你可能会想知道,如果一个应用能够自己检测配置文件的变化并在 Kubernetes 完成更新 configMap 卷中的所有文件之前重新加载它们,会发生什么。幸运的是,这种情况不会发生,因为所有文件都是原子性地更新的,这意味着所有更新都是同时发生的。Kubernetes 通过使用符号链接来实现这一点。如果你列出挂载的 configMap 卷中的所有文件,你会看到如下类似的列表。
列表 7.19. 挂载的 configMap 卷中的文件
$ kubectl exec -it fortune-configmap-volume -c web-server -- ls -lA
/etc/nginx/conf.d total 4 drwxr-xr-x ... 12:15 ..4984_09_04_12_15_06.865837643 lrwxrwxrwx ... 12:15 ..data -> ..4984_09_04_12_15_06.865837643 lrwxrwxrwx ... 12:15 my-nginx-config.conf -> ..data/my-nginx-config.conf lrwxrwxrwx ... 12:15 sleep-interval -> ..data/sleep-interval
如你所见,挂载的 configMap 卷中的文件是符号链接,指向 ..data 目录中的文件。..data 目录也是一个符号链接,指向一个名为 ..4984_09_04_something 的目录。当 ConfigMap 被更新时,Kubernetes 创建一个这样的新目录,将所有文件写入其中,然后重新链接 ..data 符号链接到新目录,从而一次性更改所有文件。
理解挂载到现有目录中的文件不会更新
一个大问题是关于更新由 ConfigMap 支持的卷。如果你在容器中挂载了一个单独的文件而不是整个卷,该文件将不会被更新!至少,在撰写本章时是这样的。
目前,如果你需要添加一个单独的文件,并在更新其源 ConfigMap 时更新它,一个解决方案是将整个卷挂载到不同的目录中,然后创建一个指向该文件的符号链接。这个符号链接可以在容器镜像本身中创建,或者你可以在容器启动时创建这个符号链接。
理解更新 ConfigMap 的后果
容器最重要的特性之一是不可变性,这使我们确信从同一镜像创建的多个运行容器之间不存在差异,那么通过修改运行容器使用的 ConfigMap 来绕过这种不可变性是否正确呢?
主要问题发生在应用程序不支持重新加载其配置时。这导致不同的运行实例配置不同——在 ConfigMap 更改后创建的 Pod 将使用新的配置,而旧的 Pod 仍然使用旧的配置。这并不限于新的 Pod。如果 Pod 的容器重新启动(无论什么原因),新的进程也会看到新的配置。因此,如果应用程序不自动重新加载其配置,修改正在使用的现有 ConfigMap(同时 Pod 正在使用它)可能不是一个好主意。
如果应用程序支持重新加载,修改 ConfigMap 通常不是什么大问题,但你确实需要意识到,由于 ConfigMap 卷中的文件不会在所有运行实例之间同步更新,单个 Pod 中的文件可能最多会有一分钟的同步问题。
7.5. 使用秘密将敏感数据传递到容器中
你迄今为止传递给容器的所有信息都是常规的非敏感配置数据,不需要保持安全。但如我们在本章开头提到的,配置通常还包括敏感信息,如凭证和私有加密密钥,这些信息需要保持安全。
7.5.1. 介绍秘密
为了存储和分发此类信息,Kubernetes 提供了一个名为 Secret 的单独对象。秘密与 ConfigMap 非常相似——它们也是包含键值对的映射。它们可以像 ConfigMap 一样使用。你可以
-
将秘密条目作为环境变量传递给容器
-
将秘密条目作为卷中的文件暴露
Kubernetes 通过确保每个秘密只分发给需要访问秘密的 Pod 运行的节点来帮助保护你的秘密。此外,在节点本身上,秘密始终存储在内存中,永远不会写入物理存储,这需要在从它们中删除秘密后擦除磁盘。
在主节点本身(更具体地说在 etcd 中),秘密以前是以未加密的形式存储的,这意味着需要确保主节点安全,以保持存储在秘密中的敏感数据安全。这不仅包括确保 etcd 存储安全,还包括防止未经授权的用户使用 API 服务器,因为任何可以创建 Pod 的人都可以将秘密挂载到 Pod 中并通过它访问敏感数据。从 Kubernetes 版本 1.7 开始,etcd 以加密形式存储秘密,使系统更加安全。正因为如此,正确选择何时使用秘密或 ConfigMap 至关重要。在它们之间进行选择很简单:
-
使用 ConfigMap 来存储非敏感的纯配置数据。
-
使用密钥来存储任何本质上是敏感的并且需要保密的数据。如果一个配置文件同时包含敏感和非敏感数据,你应该将文件存储在密钥中。
你已经在第五章中使用了密钥,当时你创建了一个密钥来存储 Ingress 资源所需的 TLS 证书。现在你将更详细地探索密钥。
7.5.2. 介绍默认令牌密钥
你将通过检查每个运行的容器中安装的密钥来开始学习关于密钥的知识。你可能在使用kubectl describe命令查看 pod 时注意到了它。命令的输出始终包含类似以下内容:
卷: default-token-cfee9: 类型: 密钥(由密钥填充的卷) 密钥名称: default-token-cfee9
每个 pod 都会自动附加一个secret卷。前面的kubectl describe输出中的卷指的是名为default-token-cfee9的密钥。因为密钥是资源,你可以使用kubectl get secrets来列出它们,并在列表中找到default-token密钥。让我们看看:
$ kubectl get secrets 名称 类型 数据 年龄 default-token-cfee9 kubernetes.io/service-account-token 3 39d
你也可以使用kubectl describe来了解更多关于它的信息,如下面的列表所示。
列表 7.20. 描述密钥
$ kubectl describe secrets 名称: default-token-cfee9 名称空间: default 标签: <无> 注解: kubernetes.io/service-account.name=default kubernetes.io/service-account.uid=cc04bb39-b53f-42010af00237 类型: kubernetes.io/service-account-token 数据 ==== ca.crt: 1139 字节 1 namespace: 7 字节 1 token: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9... 1
- 1 此密钥包含三个条目。
你可以看到,密钥包含三个条目——ca.crt、namespace和token——这些代表了你从你的 pod 中安全地与 Kubernetes API 服务器通信所需的一切。尽管理想情况下你希望你的应用程序完全与 Kubernetes 无关,但在没有其他选择只能直接与 Kubernetes 通信的情况下,你将使用通过此secret卷提供的文件。
kubectl describe pod命令显示了secret卷的挂载位置:
挂载点: /var/run/secrets/kubernetes.io/serviceaccount 来自 default-token-cfee9
注意
默认情况下,default-token密钥会被挂载到每个容器中,但你可以在每个 pod 的规范中设置automountService-AccountToken字段为false,或者将服务账户设置为false来禁用这一功能。(你将在本书的后面学习到服务账户。)
为了帮助你可视化默认令牌密钥在哪里以及如何挂载,请参阅图 7.11。
图 7.11. default-token 秘密会自动创建,并且每个 pod 都会自动挂载相应的卷。

我们说过秘密就像 ConfigMaps,所以因为这个秘密包含三个条目,你可以预期在挂载 secret 卷的目录中看到三个文件。你可以用 kubectl exec 轻松检查:
$ kubectl exec mypod ls /var/run/secrets/kubernetes.io/serviceaccount/ ca.crt namespace token
你将在下一章中看到你的应用程序如何使用这些文件来访问 API 服务器。
7.5.3. 创建一个 Secret
现在,你将创建自己的小秘密。你将通过配置 Nginx 容器以也服务 HTTPS 流量来改进你的 fortune-serving Nginx 容器。为此,你需要创建一个证书和私钥。私钥需要保持安全,所以你会将它们和证书放入一个秘密中。
首先,生成证书和私钥文件(在你的本地机器上执行此操作)。你也可以使用书中的代码存档中的文件(证书和密钥文件位于 fortune-https 目录中):
$ openssl genrsa -out https.key 2048``$ openssl req -new -x509 -key https.key -out https.cert -days 3650 -subj``/CN=www.kubia-example.com
现在,为了更好地演示关于 Secrets 的几个方面,创建一个额外的名为 foo 的虚拟文件,并使其包含字符串 bar。你将在几分钟后理解为什么需要这样做:
$ echo bar > foo
现在,你可以使用 kubectl create secret 从三个文件创建一个秘密:
$ kubectl create secret generic fortune-https --from-file=https.key
--from-file=https.cert --from-file=foo secret "fortune-https" created
这与创建 ConfigMaps 并没有太大区别。在这种情况下,你正在创建一个名为 fortune-https 的 generic 秘密,并在其中包含两个条目(https.key 包含 https.key 文件的内容,同样适用于 https.cert 密钥/文件)。正如你之前所学的,你也可以使用 --from-file=fortune-https 包括整个目录,而不是逐个指定每个文件。
注意
你正在创建一个通用的秘密,但你也可以使用 kubectl create secret tls 命令创建一个 tls 秘密,就像你在第五章(index_split_046.html#filepos469093)中所做的那样。这将创建具有不同条目名称的秘密。
7.5.4. 比较 ConfigMaps 和 Secrets
Secrets 和 ConfigMaps 有很大的不同。这是 Kubernetes 开发者在 Kubernetes 已经支持 Secrets 一段时间后创建 ConfigMaps 的原因。以下列表显示了您创建的秘密的 YAML。
列表 7.21. 秘密的 YAML 定义
$ kubectl get secret fortune-https -o yaml apiVersion: v1 data: foo: YmFyCg== https.cert: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURCekNDQ... https.key: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcE... kind: Secret ...
现在将此与前面创建的 ConfigMap 的 YAML 进行比较,如下所示。
列表 7.22. ConfigMap 的 YAML 定义
$ kubectl get configmap fortune-config -o yaml apiVersion: v1 data: my-nginx-config.conf: | server { ... } sleep-interval: | 25 kind: ConfigMap ...
注意到区别了吗?Secret 条目的内容以 Base64 编码的字符串形式显示,而 ConfigMap 的内容以明文形式显示。这最初使得在 YAML 和 JSON 清单中使用 Secret 有点痛苦,因为你在设置和读取它们的条目时必须进行编码和解码。
使用 Secret 存储二进制数据
使用 Base64 编码的原因很简单。Secret 的条目可以包含二进制值,而不仅仅是纯文本。Base64 编码允许你在 YAML 或 JSON 中包含二进制数据,这两种格式都是纯文本格式。
小贴士
你甚至可以使用 Secret 存储非敏感的二进制数据,但请注意,Secret 的最大大小限制为 1MB。
引入 stringData 字段
因为并非所有敏感数据都是二进制形式,Kubernetes 也允许通过 stringData 字段设置 Secret 的值。以下列表显示了如何使用它。
列表 7.23. 使用 stringData 字段向 Secret 添加纯文本条目
kind: Secret apiVersion: v1 stringData: 1 foo: plain text 2 data: https.cert: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURCekNDQ... https.key: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcE...
-
1 可以使用
stringData来存储非二进制 Secret 数据。 -
2 注意,“纯文本”不是 Base64 编码的。
stringData 字段是只写的(注意:只写,不是只读)。它只能用来设置值。当你使用 kubectl get -o yaml 获取 Secret 的 YAML 时,stringData 字段将不会显示。相反,你指定在 stringData 字段中的所有条目(如前一个示例中的 foo 条目)将显示在 data 下,并且像所有其他条目一样进行 Base64 编码。
在 pod 中读取 Secret 的条目
当你通过 secret 卷将 Secret 暴露给容器时,Secret 条目的值将被解码并以其实际形式写入文件(无论它是纯文本还是二进制)。同样,当通过环境变量暴露 Secret 条目时也是如此。在这两种情况下,应用程序不需要对其进行解码,可以直接读取文件的正文或查找环境变量的值并直接使用它。
7.5.5. 在 pod 中使用 Secret
在你的 fortune-https Secret 包含证书和密钥文件后,你现在需要做的只是配置 Nginx 使用它们。
修改 fortune-config ConfigMap 以启用 HTTPS
为了做到这一点,你需要再次修改配置文件,通过编辑 ConfigMap:
$ kubectl edit configmap fortune-config
在文本编辑器打开后,修改定义 my-nginx-config.conf 条目内容的部分,使其看起来如下所示。
列表 7.24. 修改 fortune-config ConfigMap 的数据
... data: my-nginx-config.conf: | server { listen 80; listen 443 ssl; server_name www.kubia-example.com; ssl_certificate certs/https.cert; ssl_certificate_key certs/https.key; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_ciphers HIGH:!aNULL:!MD5; location / { root /usr/share/nginx/html; index index.html index.htm; } } sleep-interval: | ...
- 1 路径相对于 /etc/nginx。
这配置了服务器从 /etc/nginx/certs 读取证书和密钥文件,因此你需要将 secret 卷挂载在那里。
在 pod 中挂载 fortune-https Secret
接下来,你将创建一个新的 fortune-https 容器,并将包含证书和密钥的 secret 卷挂载到 web-server 容器中的正确位置,如下所示。
列表 7.25. fortune-https 容器的 YAML 定义:fortune-pod-https.yaml
`apiVersion: v1 kind: Pod metadata: name: fortune-https spec: containers: - image: luksa/fortune:env name: html-generator env: - name: INTERVAL valueFrom: configMapKeyRef: name: fortune-config key: sleep-interval volumeMounts: - name: html mountPath: /var/htdocs - image: nginx:alpine name: web-server volumeMounts: - name: html mountPath: /usr/share/nginx/html readOnly: true - name: config mountPath: /etc/nginx/conf.d readOnly: true - name: certs mountPath: /etc/nginx/certs readOnly: true ports: - containerPort: 80 - containerPort: 443 volumes: - name: html emptyDir: {} - name: config configMap: name: fortune-config items: - key: my-nginx-config.conf path: https.conf - name: certs secret: secretName: fortune-https
-
1 你已配置 Nginx 从 /etc/nginx/certs 读取证书和密钥文件,因此需要将 Secret 卷挂载在那里。
-
2 你在这里定义了 secret 卷,引用了 fortune-https Secret。
这个 pod 描述符中有很多内容,让我帮你可视化它。图 7.12 显示了在 YAML 中定义的组件。default-token Secret、卷和卷挂载,虽然不是 YAML 的一部分,但会自动添加到你的 pod 中,在图中没有显示。
图 7.12. 结合 ConfigMap 和 Secret 运行 fortune-https pod

注意
与 configMap 卷类似,secret 卷也支持通过 defaultMode 属性指定在卷中暴露的文件的文件权限。
测试 Nginx 是否正在使用 Secret 中的证书和密钥
当 pod 运行后,你可以通过打开到 pod 的端口 443 的端口转发隧道,并使用 curl 向服务器发送请求来查看它是否正在服务 HTTPS 流量:
$ kubectl port-forward fortune-https 8443:443 & 转发自 127.0.0.1:8443 -> 443 转发自 [::1]:8443 -> 443 $ curl https://localhost:8443 -k
如果您正确配置了服务器,您应该会收到响应。您可以通过检查服务器的证书来查看它是否与您之前生成的证书匹配。这也可以通过使用 -v 选项打开详细日志记录来使用 curl 完成,如下面的列表所示。
列表 7.26. 显示 Nginx 发送的服务器证书
$ curl https://localhost:8443 -k -v * 即将连接()到 localhost 端口 8443 (#0) * 尝试 ::1... * 连接到 localhost (::1) 端口 8443 (#0) * 使用 certpath: sql:/etc/pki/nssdb 初始化 NSS * 跳过 SSL 证书验证 * 使用 TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 的 SSL 连接 * 服务器证书: * * 主题: CN=www.kubia-example.com 1 * * 开始日期: aug 16 18:43:13 2016 GMT 1 * * 过期日期: aug 14 18:43:13 2026 GMT 1 * * 公共名称: www.kubia-example.com 1 * * 发行者: CN=www.kubia-example.com 1
- 1 证书与您创建并存储在 Secret 中的证书匹配。
理解 Secret 卷存储在内存中
您通过在 /etc/nginx/certs 目录树中挂载 secret 卷,成功将证书和私钥传递到容器中。secret 卷使用内存文件系统 (tmpfs) 来存储 Secret 文件。如果您列出容器中的挂载点,您可以看到这一点:
$ kubectl exec fortune-https -c web-server -- mount | grep certs tmpfs on /etc/nginx/certs type tmpfs (ro,relatime)
由于使用了 tmpfs,存储在 Secret 中的敏感数据永远不会写入磁盘,这可能会被泄露。
通过环境变量暴露 Secret 的条目
除了使用卷之外,您还可以将 secret 中的单个条目作为环境变量暴露,就像您处理 ConfigMap 中的 sleep-interval 条目那样。例如,如果您想将 Secret 中的 foo 键作为环境变量 FOO_SECRET 暴露,您将需要将以下列表中的片段添加到容器定义中。
列表 7.27. 将 Secret 的条目作为环境变量暴露
env: - name: FOO_SECRET valueFrom: 1 secretKeyRef: 1 name: fortune-https 2 key: foo 3
-
1 变量应从 Secret 的条目中设置。
-
2 存有密钥的 Secret 名称
-
3 要暴露的 Secret 的密钥
这几乎与您设置 INTERVAL 环境变量时的情况完全相同,只是这次您使用 secretKeyRef 而不是 configMapKeyRef 来引用 Secret,后者用于引用 ConfigMap。
尽管 Kubernetes 允许你通过环境变量来暴露 Secret,但这可能不是最好的主意。应用程序通常会在错误报告中丢弃环境变量,甚至在启动时将它们写入应用程序日志,这可能会无意中暴露它们。此外,子进程继承了父进程的所有环境变量,所以如果你的应用程序运行第三方二进制文件,你无法知道你的秘密数据发生了什么。
小贴士
在使用环境变量将你的 Secret 传递给容器之前,请三思,因为它们可能会无意中暴露。为了安全起见,始终使用secret卷来暴露 Secret。
7.5.6. 理解镜像拉取 Secret
你已经学会了如何将 Secret 传递给你的应用程序并使用它们包含的数据。但有时 Kubernetes 本身也需要你向它传递凭证——例如,当你想使用私有容器镜像仓库中的镜像时。这也是通过 Secret 完成的。
到目前为止,你的所有容器镜像都存储在公共镜像仓库中,这些仓库不需要任何特殊凭证来拉取镜像。但大多数组织都不希望他们的镜像对每个人可用,因此使用私有镜像仓库。当部署一个容器镜像位于私有仓库中的 pod 时,Kubernetes 需要知道拉取镜像所需的凭证。让我们看看如何做到这一点。
在 Docker Hub 上使用私有镜像仓库
除了公共镜像仓库外,Docker Hub 还允许你创建私有仓库。你可以通过在浏览器中登录hub.docker.com,找到仓库并勾选复选框来标记仓库为私有。
要运行使用私有仓库中镜像的 pod,你需要做两件事:
-
创建一个包含 Docker 注册表凭证的 Secret。
-
在 pod 清单的
imagePullSecrets字段中引用该 Secret。
为与 Docker 注册表进行身份验证创建 Secret
为与 Docker 注册表进行身份验证创建 Secret 与你在第 7.5.3 节中创建的通用 Secret 没有太大区别。你使用相同的kubectl create secret命令,但使用不同的类型和选项:
$ kubectl create secret docker-registry mydockerhubsecret --docker-username=myusername --docker-password=mypassword --docker-email=my.email@provider.com
你不是创建一个generic Secret,而是创建一个名为mydockerhubsecret的docker-registry Secret。你指定了你的 Docker Hub 用户名、密码和电子邮件。如果你使用kubectl describe检查新创建的 Secret 的内容,你会看到一个名为.dockercfg的单个条目。这相当于你家目录中的.dockercfg 文件,它是当你运行docker login命令时 Docker 创建的。
在 pod 定义中使用 docker-registry Secret
要让 Kubernetes 在从你的私有 Docker Hub 仓库拉取镜像时使用 Secret,你只需要在 Pod 规范中指定 Secret 的名称,如下面的列表所示。
列表 7.28. 使用镜像拉取 Secret 的 Pod 定义:pod-with-private-image.yaml
apiVersion: v1 kind: Pod metadata: name: private-pod spec: imagePullSecrets: 1 - name: mydockerhubsecret 1 containers: - image: username/private:tag name: main
- 1 这使得可以从私有镜像仓库拉取镜像。
在前面列表中的 Pod 定义中,你指定了mydockerhubsecret Secret 作为imagePullSecrets之一。我建议你自己尝试一下,因为你很可能很快就会处理私有容器镜像。
不需要在每个 Pod 上指定镜像拉取 Secret
考虑到人们通常在系统中运行许多不同的 Pod,这让你想知道是否需要将相同的镜像拉取 Secret 添加到每个 Pod 中。幸运的是,情况并非如此。在第十二章中,你将学习如果将 Secret 添加到 ServiceAccount 中,如何自动将镜像拉取 Secret 添加到所有 Pod 中。
7.6. 总结
这部分内容总结了如何将配置数据传递给容器。你学习了如何
-
在 Pod 定义中覆盖容器镜像中定义的默认命令
-
将命令行参数传递给主容器进程
-
为容器设置环境变量
-
将配置与 Pod 规范解耦并将其放入 ConfigMap 中
-
在 Secret 中存储敏感数据并将其安全地传递给容器
-
创建一个
docker-registrySecret 并使用它从私有镜像仓库拉取镜像
在下一章中,你将学习如何将 Pod 和容器元数据传递给运行在其内部的程序。你还将看到我们在这章中学到的默认 token Secret 是如何在 Pod 内部与 API 服务器通信的。
第八章. 从应用程序访问 Pod 元数据和其它资源
本章涵盖
-
使用 Downward API 将信息传递到容器中
-
探索 Kubernetes REST API
-
将认证和服务器验证留给
kubectl proxy -
从容器内访问 API 服务器
-
理解大使容器模式
-
使用 Kubernetes 客户端库
应用程序通常需要了解它们运行的环境信息,包括自身和其他集群组件的详细信息。您已经看到了 Kubernetes 如何通过环境变量或 DNS 进行服务发现,但其他信息怎么办?在本章中,您将了解某些 Pod 和容器元数据如何传递到容器中,以及一个运行在容器中的应用程序与 Kubernetes API 服务器通信以获取集群中部署的资源信息是多么容易,甚至如何创建或修改这些资源。
8.1. 通过 Downward API 传递元数据
在上一章中,您看到了如何通过环境变量或通过configMap和secret卷将配置数据传递给您的应用程序。这对于您自己设置且在 Pod 调度到节点并运行之前已知的数据来说效果很好。但对于直到那时才未知的数据怎么办——例如 Pod 的 IP、主机节点的名称,甚至是 Pod 自己的名称(当名称生成时;例如,当 Pod 由 ReplicaSet 或类似控制器创建时)?以及对于已经指定在其他地方的数据怎么办,例如 Pod 的标签和注解?您不希望在多个地方重复相同的信息。
这两个问题都由 Kubernetes Downward API 解决。它允许您通过环境变量或文件(在downwardAPI卷中)传递 Pod 及其环境的相关元数据。不要被这个名字迷惑。Downward API 不像一个 REST 端点,您的应用程序需要击中它以获取数据。它是一种将环境变量或文件填充为 Pod 规范或状态中的值的方式,如图 8.1 所示。
图 8.1. Downward API 通过环境变量或文件暴露 Pod 元数据。

8.1.1. 理解可用的元数据
Downward API 允许您将 Pod 的自身元数据暴露给在该 Pod 内运行的进程。目前,它允许您将以下信息传递给您的容器:
-
Pod 的名称
-
Pod 的 IP 地址
-
Pod 所属的命名空间
-
Pod 运行所在的节点名称
-
Pod 运行下的服务账户名称
-
每个容器的 CPU 和内存请求
-
每个容器的 CPU 和内存限制
-
Pod 的标签
-
Pod 的注解
列表中的大多数项目不需要进一步解释,除非可能是服务账户以及 CPU/内存请求和限制,这些我们尚未介绍。我们将在第十二章章节 12 中详细介绍服务账户。目前,您需要知道的是,服务账户是 Pod 在与 API 服务器通信时进行身份验证的账户。CPU 和内存请求和限制在第十四章章节 14 中解释。它们是保证给容器的 CPU 和内存量以及它可能获取的最大量。
列表中的大多数项目可以通过环境变量或通过downwardAPI卷传递给容器,但标签和注解只能通过卷暴露。部分数据可以通过其他方式获取(例如,直接从操作系统获取),但 Downward API 提供了一个更简单的替代方案。
让我们通过一个例子来看看如何将元数据传递给您的容器化进程。
8.1.2. 通过环境变量公开元数据
首先,让我们看看您如何通过环境变量将 Pod 和容器的元数据传递给容器。您将从以下清单的 manifest 创建一个简单的单容器 Pod。
列表 8.1. 用于环境变量的 Downward API:downward-api-env.yaml
apiVersion: v1 kind: Pod metadata: name: downward spec: containers: - name: main image: busybox command: ["sleep", "9999999"] resources: requests: cpu: 15m memory: 100Ki limits: cpu: 100m memory: 4Mi env: - name: POD_NAME valueFrom: 1 fieldRef: 1 fieldPath: metadata.name 1 - name: POD_NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace - name: POD_IP valueFrom: fieldRef: fieldPath: status.podIP - name: NODE_NAME valueFrom: fieldRef: fieldPath: spec.nodeName - name: SERVICE_ACCOUNT valueFrom: fieldRef: fieldPath: spec.serviceAccountName - name: CONTAINER_CPU_REQUEST_MILLICORES valueFrom: 2 resourceFieldRef: 2 resource: requests.cpu 2 divisor: 1m 3 - name: CONTAINER_MEMORY_LIMIT_KIBIBYTES valueFrom: resourceFieldRef: resource: limits.memory divisor: 1Ki
-
1 而不是指定一个绝对值,您正在引用 Pod 清单中的 metadata.name 字段。
-
2 容器的 CPU 和内存请求和限制是通过使用 resourceFieldRef 而不是 fieldRef 来引用的。
-
3 对于资源字段,您定义一个除数以获取所需单位的值。
当你的进程运行时,它可以查找 Pod 规范中定义的所有环境变量。图 8.2 显示了环境变量及其值的来源。Pod 的名称、IP 和命名空间将通过POD_NAME、POD_IP和POD_NAMESPACE环境变量分别暴露。容器运行的节点名称将通过NODE_NAME变量暴露。服务账户的名称将通过SERVICE_ACCOUNT环境变量提供。你还创建了两个环境变量,将保存此容器请求的 CPU 数量和容器允许消耗的最大内存量。
图 8.2. Pod 的元数据和属性可以通过环境变量暴露给 Pod。

对于暴露资源限制或请求的环境变量,你指定一个除数。限制或请求的实际值将被除以除数,并通过环境变量暴露结果。在之前的例子中,你为 CPU 请求设置了除数为1m(一个毫核心,或 CPU 核心的千分之一)。因为你已将 CPU 请求设置为15m,所以环境变量CONTAINER_CPU_REQUEST_MILLICORES将被设置为15。同样,你将内存限制设置为4Mi(4 米字节)和除数为1Ki(1 Kibibyte),因此CONTAINER_MEMORY_LIMIT_KIBIBYTES环境变量将被设置为4096。
CPU 限制和请求的除数可以是1,表示一个完整的核心,或者1m,表示一个毫核心。内存限制/请求的除数可以是1(字节)、1k(千字节)或1Ki(Kibibyte)、1M(兆字节)或1Mi(Mebibyte)等。
在创建 Pod 之后,你可以使用kubectl exec来查看容器中的所有这些环境变量,如下所示。
列表 8.2. downward Pod 中的环境变量
$ kubectl exec downward env PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin HOSTNAME=downward CONTAINER_MEMORY_LIMIT_KIBIBYTES=4096 POD_NAME=downward POD_NAMESPACE=default POD_IP=10.0.0.10 NODE_NAME=gke-kubia-default-pool-32a2cac8-sgl7 SERVICE_ACCOUNT=default CONTAINER_CPU_REQUEST_MILLICORES=15 KUBERNETES_SERVICE_HOST=10.3.240.1 KUBERNETES_SERVICE_PORT=443 ...
容器内运行的所有进程都可以读取这些变量并按需使用它们。
8.1.3. 通过 downwardAPI 卷中的文件传递元数据
如果你更喜欢通过文件而不是环境变量来暴露元数据,你可以定义一个downwardAPI卷并将其挂载到你的容器中。你必须使用downwardAPI卷来暴露 Pod 的标签或其注解,因为它们都不能通过环境变量来暴露。我们稍后会讨论原因。
与环境变量一样,如果你想将元数据公开给进程,你需要明确指定每个元数据字段。让我们看看如何修改前面的示例,使用卷而不是环境变量,如下面的列表所示。
列表 8.3. 带有downwardAPI卷的 Pod:downward-api-volume.yaml
apiVersion: v1 kind: Pod metadata: name: downward labels: 1 foo: bar 1 annotations: 1 key1: value1 1 key2: | 1 multi 1 line 1 value 1 spec: containers: - name: main image: busybox command: ["sleep", "9999999"] resources: requests: cpu: 15m memory: 100Ki limits: cpu: 100m memory: 4Mi volumeMounts: 2 - name: downward 2 mountPath: /etc/downward 2 volumes: - name: downward 3 downwardAPI: 3 items: - path: "podName" 4 fieldRef: 4 fieldPath: metadata.name 4 - path: "podNamespace" fieldRef: fieldPath: metadata.namespace - path: "labels" 5 fieldRef: 5 fieldPath: metadata.labels 5 - path: "annotations" 6 fieldRef: 6 fieldPath: metadata.annotations 6 - path: "containerCpuRequestMilliCores" resourceFieldRef: containerName: main resource: requests.cpu divisor: 1m - path: "containerMemoryLimitBytes" resourceFieldRef: containerName: main resource: limits.memory divisor: 1
-
1 这些标签和注解将通过 downwardAPI 卷公开。
-
2 你正在将 downward 卷挂载到/etc/downward 目录下。
-
3 你正在定义一个名为 downward 的 downwardAPI 卷。
-
4 Pod 的名称(来自清单中的 metadata.name 字段)将被写入到 podName 文件中。
-
5 Pod 的标签将被写入到/etc/downward/labels 文件中。
-
6 Pod 的注解将被写入到/etc/downward/annotations 文件中。
你不是通过环境变量传递元数据,而是定义一个名为downward的卷,并将其挂载到你的容器中的/etc/downward 目录下。这个卷将包含的文件在卷指定中的downwardAPI.items属性下进行配置。
每个项目指定了元数据应该写入的path(文件名)以及引用一个 Pod 级别的字段或一个容器资源字段,其值你想存储在文件中(参见图 8.3)。
图 8.3. 使用downwardAPI卷将元数据传递到容器

删除之前的 Pod,并从上一个列表中的清单创建一个新的 Pod。然后查看挂载的downwardAPI卷目录的内容。你将卷挂载到/etc/downward/下,所以列出那里的文件,如下面的列表所示。
列表 8.4. downwardAPI卷中的文件
$ kubectl exec downward ls -lL /etc/downward -rw-r--r-- 1 root root 134 May 25 10:23 annotations -rw-r--r-- 1 root root 2 May 25 10:23 containerCpuRequestMilliCores -rw-r--r-- 1 root root 7 May 25 10:23 containerMemoryLimitBytes -rw-r--r-- 1 root root 9 May 25 10:23 labels -rw-r--r-- 1 root root 8 May 25 10:23 podName -rw-r--r-- 1 root root 7 May 25 10:23 podNamespace
注意
与 configMap 和 secret 卷一样,您可以通过 pod 规范中 downwardAPI 卷的 defaultMode 属性更改文件权限。
每个文件对应卷定义中的一个条目。文件的内容与上一个示例中的相同元数据字段相对应,与您之前使用的环境变量值相同,因此我们在此不展示它们。但因为你之前无法通过环境变量暴露标签和注释,请查看以下列表以了解你在其中暴露它们的两个文件的内容。
列表 8.5. 在 downwardAPI 卷中显示标签和注释
$ kubectl exec downward cat /etc/downward/labels foo="bar" $ kubectl exec downward cat /etc/downward/annotations key1="value1" key2="multi\nline\nvalue\n" kubernetes.io/config.seen="2016-11-28T14:27:45.664924282Z" kubernetes.io/config.source="api"
如您所见,每个标签/注释都单独一行,以 key=value 格式书写。多行值写入一行,换行符用 \n 表示。
更新标签和注释
您可能记得,标签和注释可以在 pod 运行时进行修改。正如您所预期的那样,当它们发生变化时,Kubernetes 会更新包含它们的文件,使 pod 总是能看到最新的数据。这也解释了为什么标签和注释不能通过环境变量暴露。因为环境变量值之后无法更新,如果 pod 的标签或注释通过环境变量暴露,那么在它们被修改后就没有办法暴露新的值。
在卷规范中引用容器级元数据
在我们结束本节之前,需要指出一点。当暴露容器级元数据,例如容器的资源限制或请求(使用 resourceFieldRef 完成)时,您需要指定您引用的资源字段所属容器的名称,如下所示。
列表 8.6. 在 downwardAPI 卷中引用容器级元数据
spec: volumes: - name: downward downwardAPI: items: - path: "containerCpuRequestMilliCores" resourceFieldRef: containerName: main 1 resource: requests.cpu divisor: 1m
- 1 容器名称必须指定
如果你考虑到卷是在 pod 级别定义的,而不是在容器级别定义的,那么这个原因就变得很明显。当在卷规范中引用容器的资源字段时,你需要明确指定你引用的容器的名称。即使是单容器 pod 也适用。
使用卷来暴露容器的资源请求和/或限制比使用环境变量稍微复杂一些,但好处是它允许你在需要时将一个容器的资源字段传递给另一个容器(但这两个容器需要位于同一个 pod 中)。使用环境变量时,容器只能传递其自身的资源限制和请求。
理解何时使用 Downward API
正如你所看到的,使用 Downward API 并不复杂。它允许你保持应用程序与 Kubernetes 无关。当你处理一个期望在环境变量中获取某些数据的现有应用程序时,这特别有用。Downward API 允许你将数据暴露给应用程序,而无需重写应用程序或将其包装在 shell 脚本中,该脚本收集数据并通过环境变量将其暴露。
但通过 Downward API 可用的元数据相当有限。如果你需要更多,你需要直接从 Kubernetes API 服务器获取它。你将在下一节中学习如何做到这一点。
8.2. 与 Kubernetes API 服务器通信
我们已经看到了 Downward API 如何提供一种简单的方法将某些 pod 和容器元数据传递给它们内部运行的进程。它只暴露 pod 的自身元数据和 pod 数据的一个子集。但有时你的应用程序需要了解更多关于其他 pod 以及在集群中定义的其他资源。在这种情况下,Downward API 并不能提供帮助。
正如你在整本书中看到的那样,可以通过查看服务相关的环境变量或通过 DNS 获取关于服务和 pod 的信息。但当应用程序需要关于其他资源的数据或需要尽可能访问最新信息时,它需要直接与 API 服务器通信(如图 8.4 所示)。
图 8.4. 从 pod 内与 API 服务器通信以获取其他 API 对象的信息

在你看到 pod 内的应用程序如何与 Kubernetes API 服务器通信之前,让我们首先从你的本地机器上探索服务器的 REST 端点,这样你可以看到与 API 服务器通信的样子。
8.2.1. 探索 Kubernetes REST API
你已经了解了不同的 Kubernetes 资源类型。但如果你计划开发与 Kubernetes API 通信的应用程序,你首先需要了解 API。
要做到这一点,你可以尝试直接击中 API 服务器。你可以通过运行 kubectl cluster-info 获取其 URL:
$ kubectl cluster-info Kubernetes master is running at https://192.168.99.100:8443
因为服务器使用 HTTPS 并需要身份验证,所以直接与之通信并不简单。您可以尝试使用 curl 并使用 curl 的 --insecure(或 -k)选项来跳过服务器证书检查,但这不会让您走得太远:
$ curl https://192.168.99.100:8443 -k 未授权
幸运的是,您不必自己处理身份验证,可以通过运行 kubectl proxy 命令通过代理与服务器通信。
通过 kubectl proxy 访问 API 服务器
kubectl proxy 命令运行一个代理服务器,该服务器在您的本地机器上接受 HTTP 连接并将它们代理到 API 服务器,同时处理身份验证,因此您不需要在每次请求中传递身份验证令牌。它还确保您正在与实际的 API 服务器通信,而不是中间人(通过在每次请求上验证服务器的证书)。
运行代理非常简单。您只需运行以下命令:
$ kubectl proxy 开始服务于 127.0.0.1:8001
您无需传递任何其他参数,因为 kubectl 已经知道它需要的一切(API 服务器 URL、授权令牌等)。一旦启动,代理就会在本地端口 8001 上开始接受连接。让我们看看它是否工作:
$ curl localhost:8001 { "paths": "/api", "/api/v1", ...
哇!您向代理发送了请求,它向 API 服务器发送了请求,然后代理返回了服务器返回的内容。现在,让我们开始探索。
通过 kubectl proxy 探索 Kubernetes API
您可以继续使用 curl,或者您也可以打开您的网页浏览器并将其指向 http://localhost:8001。让我们更仔细地检查当您点击其基本 URL 时 API 服务器返回的内容。服务器响应了一个路径列表,如下所示。
列表 8.7. 列出 API 服务器的 REST 端点:http://localhost:8001
$ curl http://localhost:8001 { "paths": [ "/api", "/api/v1", 1 "/apis", "/apis/apps", "/apis/apps/v1beta1", ... "/apis/batch", 2 "/apis/batch/v1", 2 "/apis/batch/v2alpha1", 2 ...
-
1 大多数资源类型都可以在这里找到。
-
2 批量 API 组及其两个版本
这些路径对应于您在创建资源(如 Pods、Services 等)时在资源定义中指定的 API 组和版本。
您可能已经注意到 /apis/batch/v1 路径中的 batch/v1 是您在 [第四章 中了解到的 Job 资源所属的 API 组和版本。同样,/api/v1 对应于您在创建的常见资源(Pods、Services、ReplicationControllers 等)中引用的 apiVersion: v1。最常见的资源类型,在 Kubernetes 最早版本中引入,不属于任何特定组,因为 Kubernetes 初始时甚至没有使用 API 组的概念;它们是在后来引入的。
注意
这些没有 API 组的初始资源类型现在被认为是属于核心 API 组。
探索批处理 API 组的 REST 端点
让我们探索作业资源 API。你将从查看 /apis/batch 路径背后的内容开始(现在暂时省略版本),如下所示。
列表 8.8. /apis/batch下的端点列表:http://localhost:8001/apis/batch
$ curl http://localhost:8001/apis/batch { "kind": "APIGroup", "apiVersion": "v1", "name": "batch", "versions": [ { "groupVersion": "batch/v1", 1 "version": "v1" 1 }, { "groupVersion": "batch/v2alpha1", 1 "version": "v2alpha1" 1 } ], "preferredVersion": { 2 "groupVersion": "batch/v1", 2 "version": "v1" 2 }, "serverAddressByClientCIDRs": null }
-
1 批处理 API 组包含两个版本。
-
2 客户端应使用 v1 版本而不是 v2alpha1。
响应显示了 batch API 组的描述,包括可用的版本和客户端应使用的首选版本。让我们继续,看看 /apis/batch/v1 路径背后的内容。如下所示。
列表 8.9. batch/v1中的资源类型:http://localhost:8001/apis/batch/v1
$ curl http://localhost:8001/apis/batch/v1 { "kind": "APIResourceList", 1 "apiVersion": "v1", "groupVersion": "batch/v1", 1 "resources": [ 2 { "name": "jobs", 3 "namespaced": true, 3 "kind": "Job", 3 "verbs": [ 4 "create", 4 "delete", 4 "deletecollection", 4 "get", 4 "list", 4 "patch", 4 "update", 4 "watch" 4 ] }, { "name": "jobs/status", 5 "namespaced": true, "kind": "Job", "verbs": [ 6 "get", 6 "patch", 6 "update" 6 ] } ] }
-
1 这是
batch/v1API 组中的 API 资源列表。 -
2 这里是一个包含该组所有资源类型的数组。
-
3 这描述了命名空间中的作业资源。
-
4 这里列出了可以与该资源一起使用的动词(你可以创建作业;删除单个或多个作业;以及检索、监视和更新它们)。
-
5 资源还有一个用于修改其状态的特殊 REST 端点。
-
6 可以检索、修补或更新状态。
如您所见,API 服务器返回了 batch/v1 API 组中的资源类型和 REST 端点的列表。其中之一是作业资源。除了资源的 name 和相关的 kind 之外,API 服务器还包含了关于资源是否 namespaced 的信息,以及(如果有的话)其简称(作业没有),以及你可以与资源一起使用的 verbs 列表。
返回的列表描述了 API 服务器中公开的 REST 资源。"name": "jobs"行告诉你 API 包含/apis/batch/v1/jobs端点。"verbs"数组表示你可以通过该端点检索、更新和删除 Job 资源。对于某些资源,还公开了额外的 API 端点(例如,jobs/status路径,它允许仅修改 Job 的状态)。
列出集群中的所有 Job 实例
要获取集群中 Job 的列表,对路径/apis/batch/v1/jobs执行 GET 请求,如下所示。
列表 8.10. Job 列表:http://localhost:8001/apis/batch/v1/jobs
$ curl http://localhost:8001/apis/batch/v1/jobs { "kind": "JobList", "apiVersion": "batch/v1", "metadata": { "selfLink": "/apis/batch/v1/jobs", "resourceVersion": "225162" }, "items": { "metadata": { "name": "my-job", "namespace": "default", ...
你可能没有在集群中部署任何 Job 资源,所以 items 数组将是空的。你可以尝试部署第八章中的my-job.yaml文件,并再次调用 REST 端点以获取与[列表 8.10 相同的输出。
通过名称检索特定的 Job 实例
之前的端点返回了所有命名空间中所有 Job 的列表。要获取一个特定的 Job,你需要指定其名称和命名空间在 URL 中。要检索之前列表中显示的 Job(name: my-job;namespace: default),你需要请求以下路径:/apis/batch/v1/namespaces/default/jobs/my-job,如下所示。
列表 8.11. 通过名称在特定命名空间中检索资源
$ curl http://localhost:8001/apis/batch/v1/namespaces/default/jobs/my-job { "kind": "Job", "apiVersion": "batch/v1", "metadata": { "name": "my-job", "namespace": "default", ...
如你所见,你得到了my-job Job 资源的完整 JSON 定义,就像你运行以下命令时得到的一样:
$ kubectl get job my-job -o json
你已经看到,你可以不使用任何特殊工具来浏览 Kubernetes REST API 服务器,但要完全探索 REST API 并与它交互,本章末尾将描述一个更好的选项。现在,使用curl像这样探索它就足够让你理解运行在 Pod 中的应用程序是如何与 Kubernetes 通信的。
8.2.2. 在 Pod 内部与 API 服务器通信
你已经学会了如何使用kubectl proxy从本地机器与 API 服务器通信。现在,让我们看看如何从 Pod 内部与它通信,因为在 Pod 内部(通常)没有kubectl。因此,要从 Pod 内部与 API 服务器通信,你需要注意以下三件事:
-
找到 API 服务器的位置。
-
确保你是在与 API 服务器通信,而不是与冒充它的东西通信。
-
使用服务器进行身份验证;否则,它不会让你看到或做任何事情。
你将在接下来的三个部分中看到这是如何完成的。
运行 Pod 以尝试与 API 服务器通信
你首先需要的是一个可以与 API 服务器通信的 Pod。你将运行一个什么也不做的 Pod(它在唯一的容器中运行 sleep 命令),然后使用kubectl exec在容器中运行 shell。然后你将尝试在那个 shell 中使用 curl 访问 API 服务器。
因此,你需要使用包含curl二进制的容器镜像。如果你在 Docker Hub 上搜索这样的镜像,你会找到tutum/curl镜像,所以使用它(你也可以使用任何其他包含curl二进制的现有镜像,或者你可以构建自己的)。Pod 定义如下所示。
列表 8.12. 尝试与 API 服务器通信的 Pod:curl.yaml
apiVersion: v1 kind: Pod metadata: name: curl spec: containers: - name: main image: tutum/curl 1 command: ["sleep", "9999999"] 2
-
1 使用 tutum/curl 镜像,因为需要在容器中提供 curl
-
2 你正在运行带有长时间延迟的 sleep 命令,以保持容器运行。
在创建完 Pod 之后,使用kubectl exec命令在容器内部运行 bash shell:
$ kubectl exec -it curl bash root@curl:/#
现在,你已经准备好与 API 服务器通信了。
查找 API 服务器的地址
首先,你需要找到 Kubernetes API 服务器的 IP 地址和端口号。这很简单,因为名为kubernetes的服务在默认命名空间中自动暴露,并配置为指向 API 服务器。你可能记得每次使用kubectl get svc列出服务时都会看到它:
$ kubectl get svc NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes 10.0.0.1 <none> 443/TCP 46d
你可以从第五章中回忆起,每个服务都会配置环境变量。你可以通过查找KUBERNETES_SERVICE_HOST和KUBERNETES_SERVICE_PORT变量(在容器内部)来获取 API 服务器的 IP 地址和端口号:
root@curl:/# env | grep KUBERNETES_SERVICE KUBERNETES_SERVICE_PORT=443 KUBERNETES_SERVICE_HOST=10.0.0.1 KUBERNETES_SERVICE_PORT_HTTPS=443
你也可能记得,每个服务也会有一个 DNS 条目,所以你甚至不需要查找环境变量,只需将 curl 指向 https://kubernetes。公平地说,如果你不知道服务在哪个端口可用,你也需要查找环境变量或执行 DNS SRV 记录查找以获取服务的实际端口号。
之前显示的环境变量表明 API 服务器正在 443 端口监听,这是 HTTPS 的默认端口,因此尝试通过 HTTPS 连接到服务器:
root@curl:/# curl https://kubernetes curl: (60) SSL certificate problem: unable to get local issuer certificate ... If you'd like to turn off curl's verification of the certificate, use the -k (or --insecure) option.
虽然绕过此问题的最简单方法是使用建议的 -k 选项(并且这是您在手动操作 API 服务器时通常会使用的方法),但让我们看看更长(且正确)的路线。您不会盲目信任您连接到的服务器是真实的 API 服务器,而是通过让 curl 检查其证书来验证其身份。
提示
在实际应用中,切勿跳过检查服务器的证书。这样做可能会导致您的应用将认证令牌暴露给使用中间人攻击的攻击者。
验证服务器的身份
在上一章中,当我们讨论机密(Secrets)时,我们查看了一个自动创建的名为 default-token-xyz 的机密,该机密被挂载到每个容器的 /var/run/secrets/kubernetes.io/serviceaccount/ 目录下。让我们再次查看该机密的目录内容:
root@curl:/# ls /var/run/secrets/kubernetes.io/serviceaccount/ ca.crt namespace token
该机密有三个条目(因此机密卷中有三个文件)。现在,我们将重点关注 ca.crt 文件,该文件包含用于签署 Kubernetes API 服务器证书的证书颁发机构(CA)的证书。为了验证您正在与 API 服务器通信,您需要检查服务器的证书是否由 CA 签署。curl 允许您使用 --cacert 选项指定 CA 证书,因此请再次尝试调用 API 服务器:
root@curl:/# curl --cacert /var/run/secrets/kubernetes.io/serviceaccount
/ca.crt https://kubernetes Unauthorized
注意
您可能会看到比“未授权”更长的错误描述。
好的,您已经取得了进展。curl 验证了服务器的身份,因为其证书是由您信任的 CA 签署的。正如“未授权”响应所暗示的,您仍然需要处理认证。您将在稍后进行,但首先让我们看看如何通过设置 CURL_CA_BUNDLE 环境变量来使生活更轻松,这样您就不需要在每次运行 curl 时都指定 --cacert:
root@curl:/# export CURL_CA_BUNDLE=/var/run/secrets/kubernetes.io/
serviceaccount/ca.crt
您现在可以不使用 --cacert 就调用 API 服务器:
root@curl:/# curl https://kubernetes Unauthorized
现在好多了。您的客户端(curl)现在信任 API 服务器了,但 API 服务器本身表示您无权访问它,因为它不知道您是谁。
使用 API 服务器进行认证
您需要与服务器进行认证,以便它允许您读取,甚至更新和/或删除在集群中部署的 API 对象。为了认证,您需要一个认证令牌。幸运的是,该令牌是通过之前提到的默认令牌机密(Secret)提供的,并存储在 secret 卷中的 token 文件中。正如机密的名字所暗示的,这就是机密的主要用途。
您将使用令牌来访问 API 服务器。首先,将令牌加载到环境变量中:
root@curl:/# TOKEN=$(cat /var/run/secrets/kubernetes.io/
serviceaccount/token)
令牌现在存储在 TOKEN 环境变量中。您可以在向 API 服务器发送请求时使用它,如下面的列表所示。
列表 8.13. 从 API 服务器获取适当的响应
root@curl:/# curl -H "Authorization: Bearer $TOKEN" https://kubernetes { "paths": [ "/api", "/api/v1", "/apis", "/apis/apps", "/apis/apps/v1beta1", "/apis/authorization.k8s.io", ... "/ui/", "/version" ] }
禁用基于角色的访问控制(RBAC)
如果您使用的是启用了 RBAC 的 Kubernetes 集群,服务账户可能没有权限访问(API 服务器的)部分。您将在第十二章(index_split_095.html#filepos1145244)中学习有关服务账户和 RBAC 的内容。现在,允许您查询 API 服务器的最简单方法是通过运行以下命令来绕过 RBAC:
$ kubectl create clusterrolebinding permissive-binding \ --clusterrole=cluster-admin \ --group=system:serviceaccounts
这将为所有服务账户(我们也可以说所有 Pod)赋予集群管理员权限,允许它们做任何想做的事情。显然,这样做是危险的,不应在生产集群上执行。出于测试目的,这是可以的。
如您所见,您在请求中将令牌传递给了 Authorization HTTP 头部。API 服务器识别了令牌为有效的,并返回了适当的响应。现在您可以探索集群中的所有资源,就像您在前面几个部分所做的那样。
例如,您可以列出同一命名空间中的所有 Pod。但首先您需要知道 curl Pod 在哪个命名空间中运行。
获取 Pod 运行的命名空间
在本章的第一部分,您看到了如何通过 Downward API 将命名空间传递给 Pod。但如果您注意到了,您可能已经注意到您的 secret 卷还包含一个名为 namespace 的文件。它包含 Pod 运行的命名空间,因此您可以读取该文件而不是必须通过环境变量显式地将命名空间传递给您的 Pod。将文件的全部内容加载到 NS 环境变量中,然后列出所有 Pod,如下面的列表所示。
列表 8.14. 在 Pod 的自身命名空间中列出 Pod
root@curl:/# NS=$(cat /var/run/secrets/kubernetes.io/
serviceaccount/namespace)``root@curl:/# curl -H "Authorization: Bearer $TOKEN"
https://kubernetes/api/v1/namespaces/$NS/pods { "kind": "PodList", "apiVersion": "v1", ...
就这样。通过使用挂载的 secret 卷目录中的三个文件,你列出了与你的 Pod 在同一命名空间中运行的所有 Pod。同样,你也可以通过发送 PUT 或 PATCH 而不是简单的 GET 请求来检索其他 API 对象,甚至更新它们。
回顾 Pod 如何与 Kubernetes 通信
让我们回顾一下运行在 Pod 内的应用程序如何正确访问 Kubernetes API:
-
应用程序应验证 API 服务器的证书是否由证书颁发机构签名,该证书的证书存储在 ca.crt 文件中。
-
应用程序应通过发送包含
token文件中的 bearer 令牌的Authorization标头来自动进行身份验证。 -
应使用
namespace文件在 Pod 的命名空间内对 API 对象执行 CRUD 操作时将命名空间传递给 API 服务器。
定义
CRUD 代表创建(Create)、读取(Read)、更新(Update)和删除(Delete)。相应的 HTTP 方法分别是 POST、GET、PATCH/PUT 和 DELETE。
Pod 与 API 服务器通信的三个方面在图 8.5 中展示。
图 8.5. 使用默认-token Secret 的文件与 API 服务器通信

8.2.3. 使用大使容器简化 API 服务器通信
处理 HTTPS、证书和身份验证令牌有时似乎对开发者来说过于复杂。我见过很多次开发者禁用了服务器证书的验证(我承认自己也做过几次)。幸运的是,你可以在保持安全的同时使通信变得更加简单。
记得我们在第 8.2.1 节中提到的 kubectl proxy 命令吗?你在本地机器上运行该命令是为了更容易地访问 API 服务器。你并不是直接向 API 服务器发送请求,而是将它们发送到代理,并让它处理身份验证、加密和服务器验证。同样,你可以在你的 Pod 内使用这种方法。
介绍大使容器模式
想象一下有一个应用程序(以及其他一些功能),它需要查询 API 服务器。而不是像上一节中那样直接与 API 服务器通信,你可以在主容器旁边运行 kubectl proxy 命令,并通过它与 API 服务器通信。
而不是直接与 API 服务器通信,主容器中的应用程序可以通过 HTTP(而不是 HTTPS)连接到大使,并让大使代理处理与 API 服务器的 HTTPS 连接,透明地处理安全问题(参见图 8.6)。它是通过使用默认令牌的 secret 卷中的文件来做到这一点的。
图 8.6. 使用大使连接到 API 服务器

由于 Pod 中的所有容器共享相同的回环网络接口,因此你的应用程序可以通过 localhost 的端口访问代理。
运行带有额外大使容器的 curl Pod
要查看使节容器模式的作用,你将创建一个新的 pod,类似于你之前创建的 curl pod,但这次,你将在 pod 中运行一个额外的基于通用 kubectl-proxy 容器镜像的使节容器,该镜像我已经创建并推送到 Docker Hub。如果你想自己构建它,可以在代码存档(在 /Chapter08/kubectl-proxy/)中找到该镜像的 Dockerfile。
pod 的配置文件如下所示。
列表 8.15. 带有使节容器的 pod:curl-with-ambassador.yaml
apiVersion: v1 kind: Pod metadata: name: curl-with-ambassador spec: containers: - name: main image: tutum/curl command: ["sleep", "9999999"] - name: ambassador 1 image: luksa/kubectl-proxy:1.6.2 1
- 1 使节容器,运行 kubectl-proxy 镜像
pod 规范几乎与之前相同,但 pod 名称不同,还有一个额外的容器。运行 pod,然后使用以下命令进入主容器:
$ kubectl exec -it curl-with-ambassador -c main bash root@curl-with-ambassador:/#
你的 pod 现在有两个容器,你想要在 main 容器中运行 bash,因此使用了 -c main 选项。如果你想在 pod 的第一个容器中运行命令,则不需要明确指定容器。但如果你想在任何其他容器中运行命令,你确实需要使用 -c 选项指定容器的名称。
通过使节容器与 API 服务器通信
接下来,你将尝试通过使节容器连接到 API 服务器。默认情况下,kubectl proxy 绑定到端口 8001,由于 pod 中的两个容器共享相同的网络接口,包括回环接口,因此你可以将 curl 指向 localhost:8001,如下所示。
列表 8.16. 通过使节容器访问 API 服务器
root@curl-with-ambassador:/# curl localhost:8001 { "paths": [ "/api", ... ] }
成功!curl 打印的输出与之前看到的相同,但这次你不需要处理身份验证令牌和服务器证书。
为了清楚地了解到底发生了什么,请参考图 8.7。curl 向运行在使节容器内部的代理发送了纯 HTTP 请求(没有任何身份验证头),然后代理向 API 服务器发送了 HTTPS 请求,通过发送令牌处理客户端身份验证,并通过验证证书检查服务器的身份。
图 8.7. 在使节容器中将加密、身份验证和服务器验证卸载到 kubectl proxy

这是一个很好的例子,说明了如何使用大使容器来隐藏连接到外部服务的复杂性,并简化在主容器中运行的应用程序。大使容器可以在许多不同的应用程序之间重用,无论主应用程序是用什么语言编写的。缺点是运行了额外的进程,并消耗了额外的资源。
8.2.4. 使用客户端库与 API 服务器通信
如果您的应用程序只需要在 API 服务器上执行一些简单的操作,您通常可以使用常规的 HTTP 客户端库并执行简单的 HTTP 请求,尤其是如果您像上一个示例中那样利用了 kubectl-proxy 大使容器。但如果您计划执行比简单的 API 请求更复杂的操作,最好使用现有的 Kubernetes API 客户端库之一。
使用现有客户端库
目前,有两个 Kubernetes API 客户端库由 API Machinery 特别兴趣小组(SIG)支持:
-
Golang 客户端—
github.com/kubernetes/client-go
注意
Kubernetes 社区有几个特别兴趣小组(SIG)和工作组,专注于 Kubernetes 生态系统的特定部分。您可以在 github.com/kubernetes/community/blob/master/sig-list.md 找到它们的列表。
除了两个官方支持的库之外,这里还有许多其他语言的用户贡献的客户端库列表:
-
由 Fabric8 提供的 Java 客户端—
github.com/fabric8io/kubernetes-client -
由 Amdatu 提供的 Java 客户端—
bitbucket.org/amdatulabs/amdatu-kubernetes -
tenxcloud 提供的 Node.js 客户端—
github.com/tenxcloud/node-kubernetes-client -
GoDaddy 提供的 Node.js 客户端—
github.com/godaddy/kubernetes-client -
另一个 PHP 客户端—
github.com/maclof/kubernetes-client -
另一个 Ruby 客户端—
github.com/abonas/kubeclient
这些库通常支持 HTTPS 并处理认证,因此您不需要使用大使容器。
Fabric8 Java 客户端与 Kubernetes 交互的示例
为了让您了解客户端库如何使您能够与 API 服务器通信,以下列表展示了如何使用 Fabric8 Kubernetes 客户端在 Java 应用程序中列出服务的示例。
列出 8.17。使用 Fabric8 Java 客户端列出、创建、更新和删除 Pod
import java.util.Arrays; import io.fabric8.kubernetes.api.model.Pod; import io.fabric8.kubernetes.api.model.PodList; import io.fabric8.kubernetes.client.DefaultKubernetesClient; import io.fabric8.kubernetes.client.KubernetesClient; public class Test { public static void main(String[] args) throws Exception { KubernetesClient client = new DefaultKubernetesClient(); // list pods in the default namespace PodList pods = client.pods().inNamespace("default").list(); pods.getItems().stream() .forEach(s -> System.out.println("Found pod: " + s.getMetadata().getName())); // create a pod System.out.println("Creating a pod"); Pod pod = client.pods().inNamespace("default") .createNew() .withNewMetadata() .withName("programmatically-created-pod") .endMetadata() .withNewSpec() .addNewContainer() .withName("main") .withImage("busybox") .withCommand(Arrays.asList("sleep", "99999")) .endContainer() .endSpec() .done(); System.out.println("Created pod: " + pod); // edit the pod (add a label to it) client.pods().inNamespace("default") .withName("programmatically-created-pod") .edit() .editMetadata() .addToLabels("foo", "bar") .endMetadata() .done(); System.out.println("Added label foo=bar to pod"); System.out.println("Waiting 1 minute before deleting pod..."); Thread.sleep(60000); // delete the pod client.pods().inNamespace("default") .withName("programmatically-created-pod") .delete(); System.out.println("Deleted the pod"); } }
代码应该是自我解释的,尤其是因为 Fabric8 客户端提供了一个优雅的、流畅的领域特定语言(DSL)API,这使得阅读和理解变得容易。
使用 Swagger 和 OpenAPI 构建自己的库
如果您选择的编程语言没有可用的客户端,您可以使用 Swagger API 框架生成客户端库和文档。Kubernetes API 服务器在/swaggerapi 处公开 Swagger API 定义,在/swagger.json 处公开 OpenAPI 规范。
要了解更多关于 Swagger 框架的信息,请访问swagger.io。
使用 Swagger UI 探索 API
在本章的早期,我说我会向您介绍一种更好的方法来探索 REST API,而不是使用curl来打击 REST 端点。在上一节中提到的 Swagger,不仅是一个指定 API 的工具,而且还提供了一个用于探索 REST API 的 Web UI(如果它们公开 Swagger API 定义)。探索 REST API 的更好方法是使用此 UI。
Kubernetes 不仅公开了 Swagger API,而且还集成了 Swagger UI 到 API 服务器中,尽管默认情况下它是禁用的。您可以通过使用带有--enable-swagger-ui=true选项的 API 服务器来启用它。
提示
如果您正在使用 Minikube,您可以在启动集群时启用 Swagger UI:minikube start --extra-config=apiserver.Features.Enable-SwaggerUI=true
在您启用 UI 之后,您可以通过将其指向以下链接在浏览器中打开它:
http(s)://<api server>:<port>/swagger-ui
我敦促您尝试使用 Swagger UI。它不仅允许您浏览 Kubernetes API,还可以与之交互(例如,您可以POST JSON 资源规范,PATCH资源或DELETE它们)。
8.3. 摘要
在阅读本章之后,您现在知道您的应用,在 Pod 内部运行,可以获取有关自身、其他 Pod 以及集群中部署的其他组件的数据。您已经学习了
-
如何通过环境变量或
downwardAPI卷中的文件将 Pod 的名称、命名空间和其他元数据暴露给进程 -
如何将 CPU 和内存请求和限制以应用所需的任何单位传递给应用
-
Pod 如何使用
downwardAPI卷来获取最新的元数据,这些元数据可能在 Pod 的生命周期内发生变化(例如标签和注解) -
如何通过
kubectl proxy浏览 Kubernetes REST API -
如何通过环境变量或 DNS,类似于在 Kubernetes 中定义的任何其他 Service,Pod 可以找到 API 服务器的位置
-
在 Pod 中运行的应用程序如何验证它正在与 API 服务器通信,以及它如何进行身份验证
-
如何使用大使容器使从应用内部与 API 服务器通信变得更加简单
-
客户端库如何让您在几分钟内与 Kubernetes 交互
在本章中,您学习了如何与 API 服务器通信,所以下一步是学习它是如何工作的。您将在第十一章(index_split_087.html#filepos1036287)中这样做,但在我们深入这些细节之前,您仍然需要了解两个其他 Kubernetes 资源——Deployments 和 StatefulSets。它们将在接下来的两个章节中解释。
第九章. 部署:声明式更新应用程序
本章涵盖
-
用新版本替换 Pod
-
更新托管 Pod
-
使用 Deployment 资源声明式地更新 Pod
-
执行滚动更新
-
自动阻止坏版本的发布
-
控制滚动发布的速率
-
将 Pod 回滚到之前的版本
现在,你已经知道如何将你的应用程序组件打包到容器中,将它们分组到 Pod 中,为它们提供临时或永久存储,将秘密和非秘密配置数据传递给它们,并允许 Pod 相互发现和通信。你知道如何运行由独立运行的小组件组成的完整系统——如果你愿意,可以称之为微服务。还有其他什么吗?
最终,你将想要更新你的应用程序。本章介绍了如何在 Kubernetes 集群中更新应用程序以及 Kubernetes 如何帮助你向真正的零停机更新过程迈进。虽然这可以通过仅使用 ReplicationControllers 或 ReplicaSets 来实现,但 Kubernetes 还提供了一个位于 ReplicaSets 之上的 Deployment 资源,它允许声明式应用程序更新。如果你对此完全不确定,请继续阅读——它并不像听起来那么复杂。
9.1. 更新在 Pod 中运行的应用程序
让我们从简单的例子开始。想象有一组 Pod 实例为其他 Pod 和/或外部客户端提供服务。在阅读这本书的这一部分之后,你可能会认识到这些 Pod 由 ReplicationController 或 ReplicaSet 支持。也存在一个 Service,客户端(在另一个 Pod 中运行的或外部客户端)通过它访问 Pod。这就是在 Kubernetes 中基本应用程序的外观(如图 9.1 所示 figure 9.1)。
图 9.1. 在 Kubernetes 中运行的应用程序的基本结构

初始时,Pod 运行你的应用程序的第一个版本——假设其镜像被标记为v1。然后你开发应用程序的新版本并将其作为新镜像推送到镜像仓库,标记为v2。接下来,你希望用这个新版本替换所有 Pod。因为 Pod 创建后不能更改现有 Pod 的镜像,所以你需要删除旧 Pod 并用运行新镜像的新 Pod 替换它们。
你有两种方式来更新所有这些 Pod。你可以执行以下操作之一:
-
首先删除所有现有 Pod,然后启动新的。
-
开始新的,一旦它们启动,就删除旧的。你可以通过一次性添加所有新 Pod 并删除所有旧 Pod,或者通过逐步添加新 Pod 和移除旧 Pod 来实现。
这两种策略都有其优点和缺点。第一种选择会导致你的应用程序在短时间内不可用。第二种选择要求你的应用程序同时运行两个版本的程序。如果你的应用程序在数据存储中存储数据,新版本不应该修改数据模式或以破坏旧版本的方式修改数据。
你如何在 Kubernetes 中执行这两种更新方法?首先,让我们看看如何手动执行;然后,一旦你知道这个过程涉及的内容,你将学习如何让 Kubernetes 自动执行更新。
9.1.1. 删除旧 Pods 并用新 Pods 替换
你已经知道如何让 ReplicationController 用运行新版本的 Pods 替换所有 Pod 实例。你可能记得 ReplicationController 的 Pod 模板可以在任何时候更新。当 ReplicationController 创建新实例时,它使用更新的 Pod 模板来创建它们。
如果你有一个管理一组v1 Pods 的 ReplicationController,你可以通过修改 Pod 模板来轻松替换它们,使其引用图像的v2版本,然后删除旧的 Pod 实例。ReplicationController 会注意到没有 Pod 匹配其标签选择器,然后它会启动新实例。整个过程如图 9.2 所示。
图 9.2. 通过更改 ReplicationController 的 Pod 模板和删除旧 Pods 来更新 Pods

如果你可以接受在删除旧 Pods 和新 Pods 启动之间的短暂停机时间,这是更新一组 Pods 的最简单方法。
9.1.2. 启动新 Pods 然后删除旧 Pods
如果你不想看到任何停机时间,并且你的应用程序支持同时运行多个版本,你可以反转这个过程,首先启动所有新的 Pods,然后才删除旧的 Pods。这将需要更多的硬件资源,因为在一小段时间内,你将有两倍数量的 Pods 同时运行。
与之前的方法相比,这是一种稍微复杂的方法,但你应该能够通过结合到目前为止你学到的关于 ReplicationControllers 和 Services 的知识来完成它。
一次性从旧版本切换到新版本
Pods 通常由 Service 作为前端。在你启动运行新版本的 Pods 时,Service 可以仅作为 Pods 的初始版本的代理。一旦所有新的 Pods 都启动,你可以更改 Service 的标签选择器,让 Service 切换到新的 Pods,如图 9.3 所示。这被称为蓝绿部署。切换后,一旦你确定新版本功能正常,你可以自由地通过删除旧的 ReplicationController 来删除旧 Pods。
注意
你可以使用kubectl set selector命令更改 Service 的 Pod 选择器。
图 9.3. 将 Service 从旧 Pods 切换到新 Pods

执行滚动更新
除了同时启动所有新 pod 并删除旧 pod 之外,你也可以执行滚动更新,逐步替换 pod。你可以通过逐渐减少上一个 ReplicationController 的规模并增加新的 ReplicationController 的规模来实现这一点。在这种情况下,你希望服务的 pod 选择器包括旧的和新的 pod,以便将请求指向这两组 pod。参见图 9.4。
图 9.4. 使用两个 ReplicationController 对 pod 执行滚动更新

手动执行滚动更新既费时又容易出错。根据副本数量的不同,你可能需要按正确的顺序运行一打或更多的命令来执行更新过程。幸运的是,Kubernetes 允许你使用单个命令执行滚动更新。你将在下一节中学习如何操作。
9.2. 使用 ReplicationController 执行自动滚动更新
你可以不使用 ReplicationController 手动执行滚动更新,而是让 kubectl 来执行。使用 kubectl 执行更新使过程变得容易得多,但正如你将在后面看到的,这现在是一种过时的更新应用的方式。尽管如此,我们首先会介绍这个选项,因为它在历史上是执行自动滚动更新的第一种方式,同时也允许我们讨论这个过程而不引入太多额外的概念。
9.2.1. 运行应用的初始版本
显然,在更新应用之前,你需要有一个应用部署。你将使用你在第二章中创建的稍作修改的 kubia NodeJS 应用作为你的初始版本。如果你不记得它做什么,它是一个简单的 web 应用,在 HTTP 响应中返回 pod 的主机名。
创建 v1 应用
你将修改应用,使其在响应中也返回其版本号,这将允许你区分你即将构建的不同版本。我已经将应用镜像构建并推送到 Docker Hub,名称为 luksa/kubia:v1。下面的列表显示了应用的代码。
列表 9.1. 我们应用的 v1 版本:v1/app.js
const http = require('http'); const os = require('os'); console.log("Kubia 服务器启动..."); var handler = function(request, response) { console.log("收到来自 " + request.connection.remoteAddress + " 的请求"); response.writeHead(200); response.end("这是运行在 pod " + os.hostname() + " 上的 v1\n"); }; var www = http.createServer(handler); www.listen(8080);
使用单个 YAML 文件运行应用并通过服务公开
要运行你的应用程序,你将创建一个 ReplicationController 和一个 LoadBalancer Service,以便你可以从外部访问应用程序。这次,你将创建一个包含这两个资源的单个 YAML 文件,并使用单个 kubectl create 命令将其发布到 Kubernetes API。YAML 清单可以包含多个对象,这些对象由包含三个短横线的行分隔,如下所示。
列表 9.2. 包含 RC 和 Service 的 YAML:kubia-rc-and-service-v1.yaml
apiVersion: v1 kind: ReplicationController metadata: name: kubia-v1 spec: replicas: 3 template: metadata: name: kubia labels: 1 app: kubia 1 spec: containers: - image: luksa/kubia:v1 2 name: nodejs --- 3 apiVersion: v1 kind: Service metadata: name: kubia spec: type: LoadBalancer selector: 1 app: kubia 1 ports: - port: 80 targetPort: 8080
-
1 该 Service 前端代理了由 ReplicationController 创建的所有 pods。
-
2 你正在为运行此镜像的 pods 创建一个 ReplicationController。
-
3 YAML 文件可以包含多个资源定义,这些定义由包含三个短横线的行分隔。
YAML 定义了一个名为 kubia-v1 的 ReplicationController 和一个名为 kubia 的 Service。将 YAML 发布到 Kubernetes。过了一会儿,你的三个 v1 pods 和负载均衡器都应该在运行,这样你就可以查找 Service 的外部 IP 并使用 curl 开始访问服务,如下所示。
列表 9.3. 获取 Service 的外部 IP 并使用 curl 在循环中访问服务
$ kubectl get svc kubia NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubia 10.3.246.195 130.211.109.222``80:32143/TCP 5m $ while true; do curl http://130.211.109.222; done 这是在 pod kubia-v1-qr192 中运行的 v1 这是在 pod kubia-v1-kbtsk 中运行的 v1 这是在 pod kubia-v1-qr192 中运行的 v1 这是在 pod kubia-v1-2321o 中运行的 v1 ...
注意
如果你使用 Minikube 或任何不支持负载均衡器服务的其他 Kubernetes 集群,你可以使用 Service 的节点端口来访问应用程序。这已在第五章中解释。
9.2.2. 使用 kubectl 执行滚动更新
接下来,你将创建应用程序的第二个版本。为了保持简单,你只需更改响应内容为“这是 v2”:
response.end("This is v2 running in pod " + os.hostname() + "\n");
这个新版本在 Docker Hub 上的镜像 luksa/kubia:v2 中可用,因此你不需要自己构建它。
向相同的镜像标签推送更新
修改应用程序并将其更改推送到相同的镜像标签并不是一个好主意,但我们所有人都在开发过程中倾向于这样做。如果你正在修改latest标签,那不是问题,但当你使用不同的标签(例如,标签v1而不是latest)标记镜像时,一旦镜像被工作节点拉取,该镜像将存储在节点上,并且当运行使用相同镜像的新 Pod 时不会再次拉取(至少这是拉取镜像的默认策略)。
这意味着如果你将更改推送到相同的标签,那么对镜像所做的任何更改都不会被选中。如果新的 Pod 被调度到相同的节点,Kubelet 将运行旧版本的镜像。另一方面,尚未运行旧版本的节点将拉取并运行新镜像,因此你可能会同时运行两个不同版本的 Pod。为了确保这种情况不会发生,你需要将容器的imagePullPolicy属性设置为Always。
你需要意识到默认的imagePullPolicy取决于镜像标签。如果容器引用了latest标签(无论是显式引用还是根本未指定标签),则imagePullPolicy默认为Always,但如果容器引用了任何其他标签,则策略默认为IfNotPresent。
当使用除latest之外的标签时,如果你在未更改标签的情况下更改了镜像,你需要正确设置imagePullPolicy。或者更好的做法是,确保你总是将更改推送到一个新标签下的镜像。
保持curl循环运行,并打开另一个终端,在那里你可以启动滚动更新。要执行更新,你需要运行kubectl rolling-update命令。你需要做的就是告诉它你正在替换哪个 ReplicationController,为新 ReplicationController 提供一个名称,并指定你想要替换原始镜像的新镜像。以下列表显示了执行滚动更新的完整命令。
列表 9.4. 使用kubectl启动 ReplicationController 的滚动更新
$ kubectl rolling-update kubia-v1 kubia-v2 --image=luksa/kubia:v2 已创建 kubia-v2 将 kubia-v2 从 0 扩展到 3,将 kubia-v1 从 3 缩减到 0(保持 3 个可用 Pod,不超过 4 个 Pod)...
由于你正在用运行版本 2 的 kubia 应用程序替换 ReplicationController kubia-v1,你希望新的 ReplicationController 被称为kubia-v2并使用luksa/kubia:v2容器镜像。
当你运行命令时,会立即创建一个新的 ReplicationController,名为kubia-v2。此时系统的状态如图 9.5 所示 figure 9.5。
图 9.5. 滚动更新启动后系统的状态

新的 ReplicationController 的 Pod 模板引用了luksa/kubia:v2镜像,并且其初始期望副本数设置为 0,如下列所示。
列表 9.5. 描述滚动更新创建的新 ReplicationController
$ kubectl describe rc kubia-v2 名称: kubia-v2 命名空间: default 镜像(s): luksa/kubia:v2 1 选择器: app=kubia,deployment=757d16a0f02f6a5c387f2b5edb62b155 标签: app=kubia 副本: 0 当前 / 0 期望 2 ...
-
1 新的 ReplicationController 引用 v2 镜像。
-
2 初始时,期望的副本数量为零。
在滚动更新开始之前,理解 kubectl 执行的步骤
kubectl通过复制kubia-v1控制器并更改其 Pod 模板中的镜像来创建这个 ReplicationController。如果你仔细查看控制器的标签选择器,你会注意到它也被修改了。它不仅包括简单的app=kubia标签,还包括一个额外的deployment标签,Pod 必须具有这个标签才能由这个 ReplicationController 管理。
你可能已经知道了这一点,但这是必要的,以避免新旧 ReplicationControllers 在相同的 Pod 集上运行。但即使新控制器创建的 Pod 除了app=kubia标签外还有额外的deployment标签,这并不意味着它们会被第一个 ReplicationController 的选择器选中,因为它设置为app=kubia吗?
是的,这正是会发生的事情,但有一个转折。滚动更新过程也修改了第一个 ReplicationController 的选择器:
$ kubectl describe rc kubia-v1 名称: kubia-v1 命名空间: default 镜像(s): luksa/kubia:v1 选择器: app=kubia, deployment=3ddd307978b502a5b975ed4045ae4964-orig`
好吧,但这不是意味着第一个控制器现在看到没有 Pod 匹配其选择器,因为之前由它创建的三个 Pod 只包含app=kubia标签吗?不,因为kubectl在修改 ReplicationController 的选择器之前也修改了活动 Pod 的标签:
$ kubectl get po --show-labels 名称 就绪 状态 重启次数 年龄 标签 kubia-v1-m33mv 1/1 运行 0 2m app=kubia, deployment=3ddd... kubia-v1-nmzw9 1/1 运行 0 2m app=kubia, deployment=3ddd... kubia-v1-cdtey 1/1 运行 0 2m app=kubia, deployment=3ddd...`
如果这变得太复杂,请查看图 9.6,它显示了 Pods、它们的标签以及两个 ReplicationControllers,以及它们的 Pod 选择器。
图 9.6. 滚动更新开始时旧的和新的 ReplicationControllers 以及 Pods 的详细状态

kubectl在开始缩放任何东西之前必须完成所有这些。现在想象一下手动进行滚动更新。很容易想象自己在这里犯了一个错误,并且可能让 ReplicationController 杀死了所有的 Pods——这些 Pods 正在积极地为你的生产客户提供服务!
通过缩放两个 ReplicationControllers 来替换旧 Pods
在设置好所有这些之后,kubectl首先通过将新控制器扩展到 1 来替换 pods。因此,控制器创建了第一个v2 pod。然后kubectl通过减少 1 来缩减旧的 ReplicationController。这显示在kubectl打印的下一行中:
将 kubia-v2 扩展到 1 将 kubia-v1 缩减到 2
因为服务正在针对所有带有app=kubia标签的 pods,你应该开始看到你的curl请求在每几个循环迭代中被重定向到新的v2 pod:
这是运行在 pod kubia-v2-nmzw9 中的 v2 1 这是运行在 pod kubia-v1-kbtsk 中的 v1 这是运行在 pod kubia-v1-2321o 中的 v1 这是运行在 pod kubia-v2-nmzw9 中的 v2 1 ...
- 1 击中运行新版本的 pod 的请求
图 9.7 显示了系统的当前状态。
图 9.7. 在滚动更新期间,服务正在将请求重定向到旧的和新的 pods。

随着kubectl继续进行滚动更新,你开始看到越来越多的请求击中v2 pods,因为更新过程删除了更多的v1 pods,并用运行你新镜像的 pods 替换它们。最终,原始的 ReplicationController 扩展到零,导致最后一个v1 pod 被删除,这意味着服务现在只由v2 pods 支持。在那个时刻,kubectl将删除原始的 ReplicationController,更新过程将完成,如下所示。
列表 9.6. kubectl rolling-update执行的最终步骤
... 将 kubia-v2 扩展到 2 将 kubia-v1 缩减到 1 将 kubia-v2 扩展到 3 将 kubia-v1 缩减到 0 更新成功。删除 kubia-v1 replicationcontroller "kubia-v1",滚动更新到"kubia-v2"
现在,你只剩下kubia-v2 ReplicationController 和三个v2 pods。在整个更新过程中,你每次都击中了你的服务并得到了响应。实际上,你已经执行了一个零停机时间的滚动更新。
9.2.3. 理解为什么 kubectl rolling-update 现在已过时
在本节的开头,我提到了一种比通过kubectl rolling-update进行更新更好的方法。这个流程有什么问题,以至于需要引入一个更好的流程?
好吧,首先,至少对我来说,我不喜欢 Kubernetes 修改我创建的对象。好吧,调度器在我创建 pods 之后分配节点给我是完全正常的,但 Kubernetes 修改我的 pods 的标签和 ReplicationController 的标签选择器是我没有预料到的,这可能会让我在办公室里对着同事大喊,“谁在捣鼓我的控制器!?!?”
但更重要的是,如果你仔细注意我使用的词语,你可能已经注意到,我一直在明确地说kubectl客户端是执行滚动更新所有这些步骤的那个。
你可以通过在触发滚动更新时使用--v选项来开启详细日志记录来看到这一点:
$ kubectl rolling-update kubia-v1 kubia-v2 --image=luksa/kubia:v2 --v 6
提示
使用--v 6选项可以将日志级别提高足够,以便让你看到kubectl发送给 API 服务器的请求。
使用此选项,kubectl将打印出它发送给 Kubernetes API 服务器的每个 HTTP 请求。你会看到对 PUT 请求的响应。
/api/v1/namespaces/default/replicationcontrollers/kubia-v1
这是表示你的kubia-v1 ReplicationController 资源的 RESTful URL。这些请求是缩小你的 ReplicationController 的请求,这表明kubectl客户端正在执行扩展,而不是由 Kubernetes 主节点执行。
提示
在运行其他kubectl命令时使用详细日志记录选项,以了解更多关于kubectl和 API 服务器之间通信的信息。
但为什么客户端执行更新过程而不是在服务器上执行它是一件坏事呢?好吧,在你的情况下,更新过程进行得很顺利,但如果你在kubectl执行更新时失去了网络连接怎么办?更新过程会在中途中断。Pod 和 ReplicationController 最终会处于中间状态。
另一个原因是,像这样的更新并不像它本可以做到的那样好,因为它是一种命令式操作。在这本书的整个过程中,我强调了 Kubernetes 是关于你告诉它系统的期望状态,然后由 Kubernetes 自己通过找出最佳方式来实现这一状态。这就是 Pod 的部署和扩展上下文的方式。你永远不会告诉 Kubernetes 添加额外的 Pod 或移除多余的 Pod——你只需更改期望副本的数量即可。
同样,你也会想要更改你的 Pod 定义中期望的镜像标签,并让 Kubernetes 用运行新镜像的新 Pod 替换它们。这正是引入名为 Deployment 的新资源的原因,现在它是 Kubernetes 中部署应用程序的首选方式。
9.3. 使用 Deployment 声明式更新应用
Deployment 是一个高级资源,旨在部署和声明式更新应用程序,而不是通过 ReplicationController 或 ReplicaSet 进行,这两者都被认为是低级概念。
当你创建一个 Deployment 时,会在其下方创建一个 ReplicaSet 资源(最终会有更多)。你可能还记得第四章,ReplicaSets 是 ReplicationControllers 的新一代,应该代替它们使用。ReplicaSet 也会复制和管理 Pod。当使用 Deployment 时,实际的 Pod 是由 Deployment 的 ReplicaSet 创建和管理的,而不是直接由 Deployment 管理(关系如图 9.8 所示)。
图 9.8. Deployment 由 ReplicaSet 支持,它监督部署的 pod。

您可能会想知道,为什么要在 ReplicationController 或 ReplicaSet 之上引入另一个对象来使事情复杂化,因为它们已经足够保持一组 pod 实例的运行。正如 第 9.2 节 中的滚动更新示例所证明的,在更新应用程序时,您需要引入一个额外的 ReplicationController 并协调两个控制器,使它们在彼此之间跳舞而不会互相干扰。您需要某种协调这种舞蹈的东西。Deployment 资源负责处理此事(这并不是 Deployment 资源本身,而是在 Kubernetes 控制平面中运行的控制器进程;但我们将在这 第十一章 中讨论这一点)。
使用 Deployment 而不是较低级别的结构可以使更新应用程序变得更加容易,因为您通过单个 Deployment 资源定义所需状态,并让 Kubernetes 处理其余部分,正如您将在接下来的几页中看到的。
9.3.1. 创建 Deployment
创建 Deployment 与创建 ReplicationController 并无太大区别。Deployment 也由标签选择器、期望副本数和 pod 模板组成。除此之外,它还包含一个字段,该字段指定了部署策略,该策略定义了在修改 Deployment 资源时如何执行更新。
创建 Deployment 清单
让我们看看如何使用本章前面提到的 kubia-v1 ReplicationController 示例,并对其进行修改,使其描述 Deployment 而不是 ReplicationController。正如您将看到的,这只需要进行三个微小的更改。以下列表显示了修改后的 YAML。
列表 9.7. 部署定义:kubia-deployment-v1.yaml
apiVersion: apps/v1beta1 1 kind: Deployment 2 metadata: 3 name: kubia 4 spec: 5 replicas: 3 6 template: 7 metadata: 8 name: kubia 9 labels: 10 app: kubia 11 spec: 12 containers: 13 - image: luksa/kubia:v1 14 name: nodejs
-
1 Deployment 位于 apps API 组,版本 v1beta1。
-
2 你已将类型从 ReplicationController 更改为 Deployment。
-
3 在 Deployment 的名称中不需要包含版本号。
注意
你可以在 extensions/ v1beta1 中找到 Deployment 资源的老版本,在 apps/v1beta2 中找到新版本,它们有不同的必填字段和默认值。请注意,kubectl explain 显示的是老版本。
由于之前的 ReplicationController 管理的是特定版本的 pod,所以你将其称为 kubia-v1。另一方面,Deployment 不涉及版本问题。在某个特定时间点,Deployment 可以在其下运行多个 pod 版本,因此其名称不应引用应用程序版本。
创建 Deployment 资源
在你创建这个 Deployment 之前,确保删除任何仍在运行的 ReplicationController 和 pod,但暂时保留 kubia 服务。你可以使用 --all 开关来删除所有这些 ReplicationController,如下所示:
$ kubectl delete rc --all
你现在可以创建 Deployment 了:
$ kubectl create -f kubia-deployment-v1.yaml --record deployment "kubia" created
小贴士
在创建时,务必包含 --record 命令行选项。这将记录命令在修订历史中,这在以后会很有用。
显示部署滚动的状态
你可以使用常规的 kubectl get deployment 和 kubectl describe deployment 命令来查看 Deployment 的详细信息,但让我指向一个额外的命令,这个命令是专门用于检查 Deployment 状态的:
$ kubectl rollout status deployment kubia deployment kubia successfully rolled out
根据这个信息,Deployment 已经成功部署,你应该看到三个 pod 副本正在运行。让我们看看:
$ kubectl get po NAME READY STATUS RESTARTS AGE kubia-1506449474-otnnh 1/1 Running 0 14s kubia-1506449474-vmn7s 1/1 Running 0 14s kubia-1506449474-xis6m 1/1 Running 0 14s
理解 Deployment 如何创建 ReplicaSet,然后 ReplicaSet 创建 pod
注意这些 pod 的名称。早些时候,当你使用 ReplicationController 创建 pod 时,它们的名称由控制器的名称加上一个随机生成的字符串(例如,kubia-v1-m33mv)组成。Deployment 创建的三个 pod 名称中包含一个额外的数字值。这究竟是什么意思?
数字对应于 Deployment 和管理这些 pod 的 ReplicaSet 中的 pod 模板的哈希值。正如我们之前所说的,Deployment 并不直接管理 pod。相反,它创建 ReplicaSet 并将管理任务交给它们,所以让我们看看由你的 Deployment 创建的 ReplicaSet:
$ kubectl get replicasets NAME DESIRED CURRENT AGE kubia-1506449474 3 3 10s
ReplicaSet 的名称也包含其 pod 模板的哈希值。正如你稍后将会看到的,Deployment 会创建多个 ReplicaSet——每个 pod 模板版本一个。使用 pod 模板的哈希值这样做允许 Deployment 总是使用相同(可能已存在)的 ReplicaSet 来处理特定版本的 pod 模板。
通过服务访问 pod
由于这个 ReplicaSet 创建的三个副本现在正在运行,你可以使用你之前创建的服务来访问它们,因为新 pod 的标签与服务的标签选择器相匹配。
到目前为止,你可能还没有看到足够好的理由说明为什么你应该使用部署(Deployments)而不是副本控制器(ReplicationControllers)。幸运的是,创建部署也没有比创建副本控制器更困难。现在,你将开始使用这个部署,这将清楚地说明为什么部署更优越。这一点将在接下来的几分钟内变得清晰,当你看到如何通过部署资源更新应用程序与通过副本控制器更新应用程序进行比较时。
9.3.2. 更新部署
在以前,当你使用副本控制器运行应用程序时,你必须明确告诉 Kubernetes 通过运行kubectl rolling-update来执行更新。你甚至必须指定应该替换旧副本控制器的新的副本控制器名称。Kubernetes 在过程结束时用新的 Pod 替换了所有原始 Pod,并删除了原始副本控制器。在这个过程中,你基本上必须待在原地,保持终端开启,等待kubectl完成滚动更新。
现在比较一下你即将如何更新一个部署。你需要做的唯一一件事是修改部署资源中定义的 Pod 模板,Kubernetes 将执行所有必要的步骤,以将实际系统状态转换为资源中定义的状态。类似于扩展或缩减副本控制器或副本集,你只需要在部署的 Pod 模板中引用一个新的镜像标签,然后让 Kubernetes 转换你的系统,使其与新的期望状态相匹配。
理解可用的部署策略
如何实现这个新状态由部署本身上配置的部署策略所控制。默认策略是执行滚动更新(该策略称为RollingUpdate)。另一种策略是Recreate策略,它一次性删除所有旧的 Pod,然后创建新的 Pod,类似于修改副本控制器的 Pod 模板然后删除所有 Pod(我们已在第 9.1.1 节中讨论过)。
Recreate策略会在创建新 Pod 之前删除所有旧的 Pod。当你的应用程序不支持并行运行多个版本,并且需要在启动新版本之前完全停止旧版本时,请使用此策略。此策略确实涉及一个短暂的时间,此时你的应用程序将完全不可用。
相反,RollingUpdate策略会逐个删除旧的 Pod,同时添加新的 Pod,在整个过程中保持应用程序可用,并确保其处理请求的能力不会下降。这是默认策略。可配置的上限和下限是期望副本数量以上或以下的 Pod 数量。你应该只在你的应用程序可以同时运行旧版本和新版本时使用此策略。
为了演示目的减慢滚动更新
在下一个练习中,你将使用 RollingUpdate 策略,但你需要稍微减慢更新过程,以便可以看到更新确实是按滚动方式进行的。你可以通过设置 Deployment 上的 minReadySeconds 属性来实现这一点。我们将在本章末尾解释这个属性的作用。现在,使用 kubectl patch 命令将其设置为 10 秒。
$ kubectl patch deployment kubia -p '{"spec": {"minReadySeconds": 10}}' "kubia" patched
提示
kubectl patch 命令对于修改资源的单个属性或有限数量的属性非常有用,而无需在文本编辑器中编辑其定义。
你使用了 patch 命令来更改 Deployment 的规范。这不会引起任何类型的 pod 更新,因为你没有更改 pod 模板。更改其他 Deployment 属性,如期望的副本数或部署策略,也不会触发滚动更新,因为它不会以任何方式影响现有的单个 pod。
触发滚动更新
如果你想要跟踪更新过程,首先在另一个终端中再次运行 curl 循环,以查看请求的情况(别忘了将 IP 替换为你的服务的实际外部 IP):
$ while true; do curl http://130.211.109.222; done
要触发实际的滚动更新,你需要将单个 pod 容器中使用的镜像更改为 luksa/kubia:v2。你不需要编辑 Deployment 对象的整个 YAML 文件或使用 patch 命令来更改镜像,而是使用 kubectl set image 命令,该命令允许更改包含容器的任何资源的镜像(ReplicationControllers、ReplicaSets、Deployments 等)。你将使用它来修改你的 Deployment,如下所示:
$ kubectl set image deployment kubia nodejs=luksa/kubia:v2 deployment "kubia" image updated
当你执行此命令时,你正在更新 kubia Deployment 的 pod 模板,使其 nodejs 容器中使用的镜像更改为 luksa/kubia:v2(从 :v1)。这如图 9.9 所示。
图 9.9. 更新部署的 pod 模板以指向新镜像

修改 Deployments 和其他资源的方法
在本书的整个过程中,你已经学习了多种修改现有对象的方法。让我们一起列出它们,以刷新你的记忆。
表 9.1. 在 Kubernetes 中修改现有资源
| 方法 | 它的作用 |
|---|---|
| kubectl edit | 在你的默认编辑器中打开对象的规范。在做出更改、保存文件并退出编辑器后,对象将被更新。例如:kubectl edit deployment kubia |
| kubectl patch | 修改对象的单个属性。例如:kubectl patch deployment kubia -p '{"spec": {"template": {"spec": {"containers": [{"name": "nodejs", "image": "luksa/kubia:v2"}]}}}}' |
| kubectl apply | 通过应用完整的 YAML 或 JSON 文件中的属性值来修改对象。如果 YAML/JSON 中指定的对象尚不存在,则将其创建。该文件需要包含资源的完整定义(它不能仅包含你想要更新的字段,正如 kubectl patch 的情况一样)。例如:kubectl apply -f kubia-deployment-v2.yaml |
| kubectl replace | 使用 YAML/JSON 文件中的新对象替换对象。与 apply 命令相比,此命令要求对象存在;否则它将打印错误。例如:kubectl replace -f kubia-deployment-v2.yaml |
| kubectl set image | 更改 Pod、ReplicationController 模板、Deployment、DaemonSet、Job 或 ReplicaSet 中定义的容器镜像。例如:kubectl set image deployment kubia nodejs=luksa/kubia:v2 |
所有这些方法在 Deployments 方面都是等效的。它们所做的就是更改 Deployments 的规范。然后,这种更改会触发滚动过程。
如果你已经运行了curl循环,你将看到请求最初只击中v1 pods;然后越来越多的请求击中 v2 pods,直到最后,所有请求都只击中剩余的v2 pods,在所有v1 pods 被删除之后。这与kubectl执行的滚动更新非常相似。
理解 Deployments 的神奇之处
让我们思考一下发生了什么。通过更改 Deployment 资源中的 Pod 模板,你已经将你的应用程序更新到了一个新版本——只需更改一个字段!
运行在 Kubernetes 控制平面部分的控制器随后执行了更新。这个过程不是由kubectl客户端执行的,就像你使用kubectl rolling-update时那样。我不知道你,但我觉得这比运行一个特殊的命令告诉 Kubernetes 要做什么然后等待过程完成要简单得多。
注意
注意,如果 Deployment 中的 Pod 模板引用了 ConfigMap(或 Secret),则修改 ConfigMap 不会触发更新。当你需要修改应用程序的配置时,触发更新的方法之一是创建一个新的 ConfigMap,并修改 Pod 模板以便它引用新的 ConfigMap。
更新期间在 Deployment 表面下发生的事件与kubectl rolling-update期间发生的事件类似。创建了一个额外的 ReplicaSet,然后缓慢地将其扩展,同时将之前的 ReplicaSet 扩展到零(初始和最终状态在图 9.10 中显示)。
图 9.10. 滚动更新开始和结束时 Deployment 的状态

如果你列出它们,你仍然可以看到新 ReplicaSet 旁边旧的 ReplicaSet:
$ kubectl get rs NAME DESIRED CURRENT AGE kubia-1506449474 0 0 24m kubia-1581357123 3 3 23m
与 ReplicationControllers 类似,你现在所有的新的 pod 都由新的 ReplicaSet 管理。与之前不同,旧的 ReplicaSet 仍然存在,而旧的 Replication-Controller 在滚动更新过程的末尾被删除了。你很快就会看到这个不活跃的 ReplicaSet 的用途。
但是你在这里不应该关心 ReplicaSets,因为你没有直接创建它们。你创建并只操作了 Deployment 资源;底层的 ReplicaSets 是一个实现细节。你会同意,管理单个 Deployment 对象比处理和跟踪多个 ReplicationControllers 要容易得多。
虽然当滚动更新一切顺利时,这种差异可能并不那么明显,但在滚动更新过程中遇到问题时,它就会变得非常明显。现在让我们模拟一个问题。
9.3.3. 回滚部署
你目前运行的是你的镜像的 v2 版本,所以你需要先准备版本 3。
创建你的应用的版本 3
在版本 3 中,你将引入一个错误,使得你的应用只能正确处理前四个请求。从第五个请求开始的请求都将返回内部服务器错误(HTTP 状态码 500)。你将通过在处理函数的开始处添加一个 if 语句来模拟这种情况。以下列表显示了新的代码,所有必要的更改都以粗体显示。
列表 9.8. 我们应用的版本 3(一个有问题的版本):v3/app.js
const http = require('http'); const os = require('os'); var requestCount = 0; console.log("Kubia 服务器启动..."); var handler = function(request, response) { console.log("收到来自 " + request.connection.remoteAddress + " 的请求"); if (++requestCount >= 5) {``response.writeHead(500);``response.end("发生了一些内部错误!这是 pod " + os.hostname() + "\n");``return;``} response.writeHead(200); response.end("这是 v3 在 pod " + os.hostname() + "\n"); }; var www = http.createServer(handler); www.listen(8080);
如你所见,在第五次及随后的请求中,代码返回了一个带有消息“发生了一些内部错误...”的 500 错误。
部署版本 3
我已经将 v3 版本的镜像作为 luksa/kubia:v3 提供。你将通过再次更改 Deployment 规范中的镜像来部署这个新版本:
$ kubectl set image deployment kubia nodejs=luksa/kubia:v3 deployment "kubia" image updated
你可以使用 kubectl rollout status 跟踪部署的进度:
$ kubectl rollout status deployment kubia 等待滚动更新完成:3 个新副本中有 1 个已更新... 等待滚动更新完成:3 个新副本中有 2 个已更新... 等待滚动更新完成:1 个旧副本正在等待终止... deployment "kubia" 滚动更新成功
新版本现在已上线。如下列表所示,经过几次请求后,你的网络客户端开始收到错误。
列表 9.9. 打破你的版本 3
$ while true; do curl http://130.211.109.222; done This is v3 running in pod kubia-1914148340-lalmx This is v3 running in pod kubia-1914148340-bz35w This is v3 running in pod kubia-1914148340-w0voh ... This is v3 running in pod kubia-1914148340-w0voh Some internal error has occurred! This is pod kubia-1914148340-bz35w This is v3 running in pod kubia-1914148340-w0voh Some internal error has occurred! This is pod kubia-1914148340-lalmx This is v3 running in pod kubia-1914148340-w0voh Some internal error has occurred! This is pod kubia-1914148340-lalmx Some internal error has occurred! This is pod kubia-1914148340-bz35w Some internal error has occurred! This is pod kubia-1914148340-w0voh
回滚部署
您不能让用户遇到内部服务器错误,因此您需要迅速采取措施。在第 9.3.6 节中,您将看到如何自动阻止不良的回滚,但就现在而言,让我们看看您如何手动处理不良的回滚。幸运的是,Deployment 通过告诉 Kubernetes 撤销 Deployment 的最后一次回滚,使得回滚到之前部署的版本变得容易:
$ kubectl rollout undo deployment kubia deployment "kubia" rolled back
这将 Deployment 回滚到上一个修订版。
小贴士
undo命令也可以在回滚过程仍在进行时使用,以实质上中止回滚。在回滚过程中已创建的 Pod 将被移除,并再次替换为旧的 Pod。
显示 Deployment 的回滚历史
由于 Deployments 保留修订历史,因此回滚回滚是可能的。您将在稍后看到,历史记录存储在底层的 ReplicaSets 中。当回滚完成时,旧的 ReplicaSet 不会被删除,这使得您可以回滚到任何修订版,而不仅仅是上一个修订版。可以使用kubectl rollout history命令显示修订历史:
$ kubectl rollout history deployment kubia deployments "kubia": REVISION CHANGE-CAUSE 2 kubectl set image deployment kubia nodejs=luksa/kubia:v2 3 kubectl set image deployment kubia nodejs=luksa/kubia:v3
记得您在创建 Deployment 时使用的--record命令行选项吗?没有它,修订历史中的CHANGE-CAUSE列将是空的,这将使得确定每个修订版背后的内容变得更加困难。
回滚到特定的 Deployment 修订版
您可以通过在undo命令中指定修订版来回滚到特定的修订版。例如,如果您想回滚到第一个版本,您将执行以下命令:
$ kubectl rollout undo deployment kubia --to-revision=1
记得你在第一次修改 Deployment 时留下的那个不活动的 ReplicaSet 吗?ReplicaSet 代表了你的 Deployment 的第一个修订版本。由 Deployment 创建的所有 ReplicaSet 都代表了完整的修订历史,如图 9.11 所示。figure 9.11。每个 ReplicaSet 存储了在该特定修订版本下 Deployment 的完整信息,因此你不应该手动删除它。如果你删除了它,你将失去 Deployment 历史中的那个特定修订版本,这将阻止你回滚到它。
图 9.11. Deployment 的 ReplicaSet 也充当其修订历史。

但是,让旧的 ReplicaSet 杂乱地充斥着你的 ReplicaSet 列表并不是理想的做法,因此修订历史的长度由 Deployment 资源上的 revisionHistoryLimit 属性限制。默认值为两个,所以通常只有当前和上一个修订版本会显示在历史记录中(并且只保留当前和上一个 ReplicaSet)。旧的 ReplicaSet 将自动被删除。
注意
Deployments 的 extensions/v1beta1 版本没有默认的 revisionHistoryLimit,而 apps/v1beta2 版本的默认值是 10。
9.3.4. 控制 rollout 的速率
当你执行到 v3 版本的 rollout 并使用 kubectl rollout status 命令跟踪其进度时,你会看到首先创建了一个新的 pod,当它变得可用时,其中一个旧的 pod 被删除,并创建了一个新的 pod。这个过程一直持续到没有旧的 pod 为止。新 pod 的创建和旧 pod 的删除方式可以通过滚动更新策略的另外两个属性进行配置。
介绍滚动更新策略的 maxSurge 和 maxUnavailable 属性
两个属性会影响在 Deployment 的滚动更新过程中一次替换多少个 pod。它们是 maxSurge 和 maxUnavailable,可以作为 Deployment 的 strategy 属性的 rollingUpdate 子属性的一部分进行设置,如下面的列表所示。
列表 9.10. 指定 rollingUpdate 策略的参数
spec: strategy: rollingUpdate: maxSurge: 1 maxUnavailable: 0 type: RollingUpdate
这些属性的作用在 表 9.2 中解释。
表 9.2. 配置滚动更新速率的属性
| 属性 | 它的作用 |
|---|---|
| maxSurge | 确定你允许存在的 pod 实例数量,这些实例的数量超过了在 Deployment 上配置的期望副本数。默认值为 25%,因此 pod 实例的数量最多可以比期望的数量多 25%。如果期望的副本数设置为四个,则在更新过程中运行的所有 pod 实例的数量永远不会超过五个。将百分比转换为绝对数时,数值将向上取整。除了百分比之外,该值也可以是绝对值(例如,可以允许一个或两个额外的 pod)。 |
| maxUnavailable | 确定在更新期间相对于所需副本数量可以不可用的 Pod 实例数量。它也默认为 25%,因此可用的 Pod 实例数量必须始终不低于所需副本数量的 75%。在这里,将百分比转换为绝对数时,数值会向下取整。如果所需的副本数量设置为四个且百分比为 25%,则只能有一个 Pod 不可用。在整个滚动过程中,始终至少有三个 Pod 实例可用于处理请求。与 maxSurge 一样,您也可以指定绝对值而不是百分比。 |
由于您的情况中所需的副本数量是三个,并且这两个属性默认为 25%,maxSurge允许所有 Pod 的数量达到四个,而maxUnavailable不允许有任何不可用的 Pod(换句话说,必须始终有三个 Pod 可用)。这如图 9.12 所示。#filepos930521。
图 9.12. 具有三个副本和默认maxSurge和maxUnavailable的部署滚动更新

理解 maxUnavailable 属性
Deployments 的extensions/v1beta1版本使用不同的默认值——它将maxSurge和maxUnavailable都设置为1而不是25%。在三个副本的情况下,maxSurge与之前相同,但maxUnavailable不同(1 而不是 0)。这使得滚动过程有所不同,如图 9.13 所示。#filepos931452。
图 9.13. 具有maxSurge=1和maxUnavailable=1的部署滚动更新

在这种情况下,一个副本可以不可用,因此如果所需的副本数量是三个,则只需要两个副本可用。这就是为什么滚动过程会立即删除一个 Pod 并创建两个新的 Pod。这确保了有两个 Pod 可用,并且 Pod 的最大数量不会超过(在这种情况下最大数量是四个——三个加上maxSurge中的一个)。一旦两个新的 Pod 可用,剩余的两个旧 Pod 就会被删除。
这有点难以理解,特别是由于maxUnavailable属性会让你认为这是允许的最大不可用 Pod 数量。如果你仔细观察前面的图,你会看到第二列中有两个不可用的 Pod,尽管maxUnavailable设置为 1。
需要记住的是,maxUnavailable是相对于所需副本数量的。如果副本数量设置为三个且maxUnavailable设置为一个,这意味着更新过程必须始终至少保持两个 Pod(3 减去 1)可用,而不可用的 Pod 数量可以超过一个。
9.3.5. 暂停滚动过程
在你应用版本 3 的糟糕体验之后,想象你已经修复了 bug 并推送了镜像的版本 4。你对以之前的方式将版本 4 滚动发布到所有 Pod 上有些担忧。你希望的是运行一个单独的v4 Pod,与现有的v2 Pods 并排运行,并观察它只与所有用户中的一小部分用户交互的表现。然后,一旦你确认一切正常,你可以用新的 Pod 替换所有旧的 Pod。
你可以通过运行一个额外的 Pod,无论是直接运行还是通过额外的 Deployment、ReplicationController 或 ReplicaSet 来实现,但你还有在 Deployment 本身上可用的另一个选项。在滚动发布过程中,Deployment 也可以被暂停。这允许你在继续其他滚动发布之前验证新版本是否一切正常。
暂停滚动发布
我已经准备好了v4镜像,所以请继续操作,通过将镜像改为luksa/kubia:v4来触发滚动发布,但随后立即(在几秒钟内)暂停滚动发布:
$ kubectl set image deployment kubia nodejs=luksa/kubia:v4 deployment "kubia" image updated $ kubectl rollout pause deployment kubia deployment "kubia" paused
应该已经创建了一个新的 Pod,但所有原始 Pod 也应该仍然在运行。一旦新的 Pod 启动,所有请求服务的一部分将被重定向到新的 Pod。这样,你实际上已经运行了一个金丝雀发布。金丝雀发布是一种最小化发布不良版本的应用程序风险的技术,它不会影响到所有用户。而不是将新版本滚动发布给所有人,你只替换一个或少数几个旧的 Pod 为新 Pod。这样,最初只有少数用户会接触到新版本。然后你可以验证新版本是否运行正常,然后继续在所有剩余的 Pod 上滚动发布或回滚到上一个版本。
恢复滚动发布
在你的情况下,通过暂停滚动发布过程,只有一小部分客户端请求会击中你的v4 Pod,而大多数请求仍然会击中v3 Pods。一旦你确信新版本按预期工作,你可以恢复部署以用新的 Pod 替换所有旧的 Pod:
$ kubectl rollout resume deployment kubia deployment "kubia" resumed
显然,在滚动发布过程中在某个确切点暂停部署不是你想要的。将来,新的升级策略可能会自动执行此操作,但当前,执行金丝雀发布的正确方式是使用两个不同的 Deployment 并相应地扩展它们。
使用暂停功能来防止滚动发布
暂停部署也可以用来防止对部署的更新启动滚动过程,这样你可以对部署进行多次更改,并在完成所有必要的更改后才开始滚动。
注意
如果一个 Deployment 被暂停,undo 命令不会撤销它,直到你恢复 Deployment。
9.3.6. 阻止坏版本滚动
在你完成这一章之前,我们需要讨论 Deployment 资源的一个更多属性。记得你在 9.3.2 节 开始时设置的 minReadySeconds 属性吗?你用它来减慢滚动速度,这样你可以看到它确实是在执行滚动更新,而不是一次性替换所有 pod。minReadySeconds 的主要功能是防止部署故障版本,而不是为了好玩而减慢部署。
理解 minReadySeconds 的适用性
minReadySeconds 属性指定新创建的 pod 在被视为可用之前应该就绪多长时间。在 pod 可用之前,滚动过程将不会继续(记得 maxUnavailable 属性吗?)。当 pod 的所有容器的就绪检查都返回成功时,pod 就处于就绪状态。如果一个新 pod 运行不正常,并且在其就绪检查在 minReadySeconds 过去之前开始失败,新版本的滚动将实际上被阻止。
你使用这个属性通过让 Kubernetes 在 pod 就绪后等待 10 秒再继续滚动来减慢你的滚动过程。通常,你会将 minReadySeconds 设置得更高,以确保 pod 在开始接收实际流量后继续报告它们处于就绪状态。
虽然你显然应该在将 pod 部署到生产环境之前在测试和预发布环境中对其进行测试,但使用 minReadySeconds 就像安全气囊一样,在你已经让有缺陷的版本进入生产后,可以防止你的应用程序造成大混乱。
通过正确配置就绪检查和适当的 minReadySeconds 设置,Kubernetes 本可以阻止我们更早地部署有缺陷的 v3 版本。让我给你展示一下。
定义就绪检查以防止我们的 v3 版本完全滚动
你将再次部署版本 v3,但这次,你将在 pod 上定义适当的就绪检查。你的 Deployment 当前处于版本 v4,所以在开始之前,再次回滚到版本 v2,这样你可以假装这是你第一次升级到 v3。如果你愿意,你可以直接从 v4 跳到 v3,但接下来的文本假设你首先回到了 v2。
与之前不同,之前你只更新了 pod 模板中的图像,现在你还将同时为容器引入就绪性检查。到目前为止,因为没有定义明确的就绪性检查,容器和 pod 始终被认为是就绪的,即使应用程序实际上并没有准备好或者正在返回错误。Kubernetes 无法知道应用程序正在出现故障,不应该暴露给客户端。
要一次性更改图像并引入就绪性检查,你将使用 kubectl apply 命令。你将使用以下 YAML 来更新部署(你将把它存储为 kubia-deployment-v3-with-readinesscheck.yaml),如下所示。
列表 9.11. 带有就绪性检查的 Deployment:kubia-deployment-v3-with-readinesscheck.yaml
apiVersion: apps/v1beta1 kind: Deployment metadata: name: kubia spec: replicas: 3 minReadySeconds: 10 1 strategy: rollingUpdate: maxSurge: 1 maxUnavailable: 0 2 type: RollingUpdate template: metadata: name: kubia labels: app: kubia spec: containers: - image: luksa/kubia:v3 name: nodejs readinessProbe: periodSeconds: 1 3 httpGet: 4 path: / 4 port: 8080 4
-
1 你将保持 minReadySeconds 设置为 10。
-
2 你将保持 maxUnavailable 设置为 0,以便部署逐个替换 pod
-
3 你正在定义一个每秒执行的就绪性检查。
-
4 就绪性检查将对我们的容器执行 HTTP GET 请求。
使用 kubectl apply 更新 Deployment
要更新 Deployment,这次你将使用 kubectl apply 如下所示:
$ kubectl apply -f kubia-deployment-v3-with-readinesscheck.yaml 部署 "kubia" 已配置
apply 命令更新 Deployment,使其包含 YAML 文件中定义的所有内容。它不仅更新了图像,还添加了就绪性检查定义以及你在 YAML 中添加或修改的任何其他内容。如果新的 YAML 也包含与现有 Deployment 上副本数量不匹配的 replicas 字段,apply 操作也会扩展 Deployment,这通常不是你想要的。
小贴士
要在更新 Deployment 时保持期望的副本数量不变,不要在 YAML 中包含 replicas 字段。
运行 apply 命令将启动更新过程,你可以再次使用 rollout status 命令来跟踪:
$ kubectl rollout status deployment kubia 等待滚动更新完成:3 个新副本中有 1 个已更新...
因为状态显示已创建一个新 pod,你的服务应该偶尔会访问它,对吧?让我们看看:
$ while true; do curl http://130.211.109.222; done This is v2 running in pod kubia-1765119474-jvslk This is v2 running in pod kubia-1765119474-jvslk This is v2 running in pod kubia-1765119474-xk5g3 This is v2 running in pod kubia-1765119474-pmb26 This is v2 running in pod kubia-1765119474-pmb26 This is v2 running in pod kubia-1765119474-xk5g3 ...
没错,你从未触碰到v3 Pod。为什么没有?它甚至存在吗?列出 Pod:
$ kubectl get po NAME READY STATUS RESTARTS AGE kubia-1163142519-7ws0i 0/1 运行中 0 30 秒 kubia-1765119474-jvslk 1/1 运行中 0 9 分钟 kubia-1765119474-pmb26 1/1 运行中 0 9 分钟 kubia-1765119474-xk5g3 1/1 运行中 0 8 分钟
哎呀!这就是你的问题(或者正如你很快就会学到的那样,你的祝福)!Pod 显示为未就绪,但你是不是一直都在期待这个结果,对吧?发生了什么?
理解就绪检查如何防止不良版本被部署
一旦你的新 Pod 开始运行,就绪检查每秒就会开始被触发(你在 Pod 规范中将检查的间隔设置为 1 秒)。在第五次请求时,就绪检查开始失败,因为你的应用程序从第五次请求开始返回 HTTP 状态码 500。
因此,Pod 被从服务中移除作为端点(见图 9.14)。在你开始通过curl循环访问服务之前,Pod 已经被标记为未就绪。这解释了为什么你从未用curl触碰到新 Pod。这正是你想要的,因为你不希望客户端触碰到一个未正确运行的 Pod。
图 9.14. 新 Pod 中因失败的就绪检查而阻止部署

但关于滚动部署过程呢?rollout status命令只显示有一个新的副本已启动。幸运的是,滚动部署过程不会继续,因为新的 Pod 永远不会变得可用。要被视为可用,它至少需要就绪 10 秒钟。在它就绪之前,滚动部署过程不会创建任何新的 Pod,也不会移除任何原始 Pod,因为你已经将maxUnavailable属性设置为 0。
部署停滞不前是个好事,因为如果它继续用新 Pod 替换旧 Pod,你最终会得到一个完全无法工作的服务,就像你最初推出版本 3 时那样,当时你没有使用就绪检查。但现在,由于就绪检查已经到位,对用户几乎没有负面影响。可能有一些用户遇到了内部服务器错误,但这并不像如果部署替换了所有 Pod 为有缺陷的版本 3 那样成为一个大问题。
小贴士
如果你只定义了就绪探测而没有正确设置 minReadySeconds,则新 pod 在就绪探测第一次调用成功时立即被视为可用。如果就绪探测在之后很快开始失败,则不良版本将在所有 pod 上滚动。因此,你应该适当地设置 minReadySeconds。
为滚动更新配置截止日期
默认情况下,如果在 10 分钟内滚动更新无法取得任何进展,则被视为失败。如果你使用 kubectl describe 部署命令,你会看到它显示一个 ProgressDeadlineExceeded 条件,如下面的列表所示。
列表 9.12. 使用 kubectl describe 查看部署的条件
$ kubectl describe deploy kubia Name: kubia ... Conditions: Type Status Reason ---- ------ ------ Available True MinimumReplicasAvailable Progressing False ProgressDeadlineExceeded 1
- 1 部署取得进展的时间过长。
部署被视为失败的时间可以通过部署规范中的 progressDeadlineSeconds 属性进行配置。
注意
Deployments 的 extensions/v1beta1 版本没有设置截止日期。
终止不良的滚动更新
由于滚动更新永远不会继续,现在唯一要做的就是通过撤销操作来终止滚动更新:
$ kubectl rollout undo deployment kubia deployment "kubia" rolled back
注意
在未来的版本中,当超过 progressDeadlineSeconds 中指定的时间时,滚动更新将自动终止。
9.4. 摘要
本章向您展示了如何通过使用声明性方法在 Kubernetes 中部署和更新应用程序来使您的生活更加轻松。现在您已经阅读了本章,您应该知道如何
-
对由 ReplicationController 管理的 pod 执行滚动更新
-
创建部署而不是较低级别的 ReplicationControllers 或 ReplicaSets
-
通过编辑部署规范中的 pod 模板来更新你的 pod
-
将部署回滚到上一个修订版或回滚到修订历史中列出的任何较早修订版
-
在中途终止部署
-
暂停部署以检查新版本的单个实例在生产中的行为,然后再允许额外的 pod 实例替换旧版本
-
通过
maxSurge和maxUnavailable属性控制滚动更新的速率 -
使用
minReadySeconds和就绪探测来自动阻止有缺陷版本的滚动
除了这些特定的部署任务外,你还学习了如何
-
使用三个短横线作为分隔符,在单个 YAML 文件中定义多个资源
-
打开
kubectl的详细日志记录功能,以查看它幕后正在做什么
你现在已经知道了如何部署和管理由相同的 pod 模板创建的 pod 集合,并且它们共享相同的持久存储。你甚至知道如何声明式地更新它们。但对于需要每个实例使用自己持久存储的 pod 集合的运行,我们还没有探讨过。这正是我们下一章的主题。
第十章。StatefulSets:部署复制的有状态应用
本章涵盖
-
部署有状态集群应用
-
为复制的 pod 的每个实例提供单独的存储
-
保证 pod 副本具有稳定的名称和主机名
-
以可预测的顺序启动和停止 pod 副本
-
通过 DNS SRV 记录发现对等节点
你现在知道如何运行单实例和复制的无状态 pod,甚至利用持久存储的状态 pod。你可以运行多个复制的 web-server pod 实例,你也可以运行一个使用持久存储的单一数据库 pod 实例,无论是通过普通的 pod 卷还是通过由 PersistentVolumeClaim 绑定的 Persistent-Volumes。但是,你能使用 ReplicaSet 来复制数据库 pod 吗?
10.1. 复制有状态 pod
ReplicaSets 从单个 pod 模板创建多个 pod 副本。这些副本之间除了名称和 IP 地址外没有区别。如果 pod 模板包含一个卷,该卷引用特定的 PersistentVolumeClaim,则 ReplicaSet 的所有副本都将使用完全相同的 PersistentVolumeClaim 和因此相同的 PersistentVolume(如图 10.1 所示)。
图 10.1. 同一个 ReplicaSet 的所有 pod 总是使用相同的 PersistentVolumeClaim 和 PersistentVolume。

因为引用的声明在 pod 模板中,该模板用于打印多个 pod 副本,所以你不能让每个副本使用它自己的单独的 Persistent-VolumeClaim。你不能使用 ReplicaSet 来运行分布式数据存储,其中每个实例都需要其自己的单独存储——至少不能通过使用单个 ReplicaSet 来实现。说实话,你迄今为止看到的任何 API 对象都无法运行此类数据存储。你需要其他东西。
10.1.1. 使用每个副本单独存储运行多个副本
如何运行 pod 的多个副本,并让每个 pod 使用其自己的存储卷?ReplicaSets 创建 pod 的精确副本(副本);因此,你不能使用它们来处理这些类型的 pod。你可以使用什么?
手动创建 pod
你可以手动创建 pod,并让每个 pod 使用它自己的 PersistentVolumeClaim,但由于没有 ReplicaSet 管理它们,你需要手动管理它们,并在它们消失时(例如节点故障事件)重新创建它们。因此,这不是一个可行的选项。
为每个 pod 实例使用一个 ReplicaSet
而不是直接创建 pod,你可以创建多个 ReplicaSets——每个 pod 一个,每个 ReplicaSet 的期望副本数设置为 1,每个 ReplicaSet 的 pod 模板引用一个专用的 PersistentVolumeClaim(如图 10.2 所示)。
图 10.2. 为每个 pod 实例使用一个 ReplicaSet

尽管这可以处理节点故障或意外删除 Pod 的情况下的自动重新调度,但与单个 ReplicaSet 相比,操作要繁琐得多。例如,考虑一下在这种情况下如何扩展 Pod。你无法更改期望的副本数量——你将不得不创建额外的 ReplicaSet。
因此,使用多个 ReplicaSet 并不是最佳解决方案。但你是否可以使用单个 ReplicaSet,并且让每个 Pod 实例保持其自己的持久状态,尽管它们都在使用相同的存储卷?
在同一卷中使用多个目录
你可以使用的一个技巧是让所有 Pod 使用相同的 PersistentVolume,然后在卷内为每个 Pod 创建一个单独的文件目录(如图 10.3 所示)。
图 10.3. 通过让每个 Pod 使用不同的文件目录来解决共享存储问题

由于你无法将 Pod 副本配置与单个 Pod 模板不同,因此你无法告诉每个实例它应该使用哪个目录,但你可以让每个实例自动选择(并可能创建)一个当时未被任何其他实例使用的数据目录。这种解决方案确实需要实例之间的协调,并且不容易正确执行。这也使得共享存储卷成为瓶颈。
10.1.2. 为每个 Pod 提供稳定的身份
除了存储之外,某些集群应用程序还要求每个实例具有长期稳定的身份。Pod 有时会被杀死并替换为新的 Pod。当 ReplicaSet 替换 Pod 时,新的 Pod 是一个全新的 Pod,具有新的主机名和 IP 地址,尽管其存储卷中的数据可能是被杀死的 Pod 的数据。对于某些应用程序,使用旧实例的数据但具有完全新的网络身份启动可能会引起问题。
为什么某些应用程序强制要求稳定的网络身份?这种要求在分布式有状态应用程序中相当常见。某些应用程序要求管理员在成员的配置文件中列出所有其他集群成员及其 IP 地址(或主机名)。但在 Kubernetes 中,每次 Pod 重新调度时,新的 Pod 都会获得一个新的主机名和新的 IP 地址,因此每次成员重新调度时,整个应用程序集群都需要重新配置。
使用为每个 Pod 实例提供专用服务
解决这个问题的另一个技巧是为集群成员提供一个稳定的网络地址,通过为每个成员创建一个专门的 Kubernetes Service。因为服务 IP 是稳定的,所以你可以在配置中将每个成员指向其服务 IP(而不是 Pod IP)。
这类似于为每个成员创建一个 ReplicaSet 以提供它们各自的存储,如前所述。结合这两种技术,结果就是图 10.4 中所示(还显示了覆盖所有集群成员的额外服务,因为通常你需要一个用于集群客户端)。
图 10.4. 使用每个 Pod 一个 Service 和一个 ReplicaSet 来提供每个 Pod 的稳定网络地址和各自的存储

这个解决方案不仅丑陋,而且仍然没有解决所有问题。单个 Pod 无法知道它们通过哪个 Service 暴露(因此无法知道它们的稳定 IP),所以它们无法使用该 IP 在其它 Pod 中进行自我注册。
幸运的是,Kubernetes 帮助我们避免了求助于如此复杂的解决方案。在 Kubernetes 中运行这些特殊类型应用程序的正确、简单方式是通过 StatefulSet。
10.2. 理解 StatefulSet
而不是使用 ReplicaSet 来运行这些类型的 Pod,你创建一个 StatefulSet 资源,该资源专门针对那些必须将应用程序的实例视为不可互换的个体,每个实例都有一个稳定的名称和状态。
10.2.1. 比较 StatefulSet 与 ReplicaSet
要理解 StatefulSet 的目的,最好是将它们与 ReplicaSet 或 ReplicationController 进行比较。但首先让我用这个在领域内广泛使用的类比来解释它们。
使用宠物与牛的类比理解有状态 Pod
你可能已经听说过宠物与牛的类比。如果没有,让我来解释一下。我们可以将我们的应用程序视为宠物或牛。
备注
StatefulSet 最初被称为 PetSet。这个名字来源于这里解释的宠物与牛的类比。
我们倾向于将应用程序实例视为宠物,为每个实例命名并单独照顾每个实例。但通常将实例视为牛,不对每个个体实例给予特别注意会更好。这使得替换不健康的实例变得容易,无需多加思考,类似于农民替换不健康的牛。
无状态应用程序的实例,例如,表现得非常像牛头。一个实例死亡并不重要——你可以创建一个新的实例,人们不会注意到差异。
另一方面,对于有状态的应用程序,一个应用程序实例更像是一个宠物。当宠物死亡时,你不能去买一个新的,并期望人们不会注意到。要替换丢失的宠物,你需要找到一个看起来和表现完全像旧宠物的新宠物。在应用程序的情况下,这意味着新实例需要与旧实例具有相同的状态和身份。
比较 StatefulSet 与 ReplicaSet 或 ReplicationController
由 ReplicaSet 或 ReplicationController 管理的 Pod 副本就像牛群一样。因为它们大多数是无状态的,所以可以在任何时候用完全新的 Pod 副本替换它们。有状态的 Pod 需要不同的方法。当一个有状态的 Pod 实例死亡(或运行它的节点失败)时,Pod 实例需要在另一个节点上复活,但新的实例需要获得与它所替代的实例相同的名称、网络标识和状态。这就是当 Pod 通过 StatefulSet 管理时发生的情况。
StatefulSet 确保 Pod 以保留其标识和状态的方式进行重新调度。它还允许你轻松地增加或减少宠物的数量。StatefulSet,就像 ReplicaSet 一样,有一个期望的副本计数字段,它决定了你希望在该时间运行多少个宠物。类似于 ReplicaSets,Pod 是从作为 StatefulSet 一部分指定的 Pod 模板创建的(还记得切片机的类比吗?)。但是,与由 ReplicaSet 创建的 Pod 不同,由 StatefulSet 创建的 Pod 不是彼此的精确副本。每个 Pod 都可以有自己的卷集——换句话说,存储(以及持久状态),这使其与其同伴区分开来。宠物 Pod 也有可预测的(和稳定的)标识,而不是每个新的 Pod 实例都获得一个完全随机的标识。
10.2.2. 提供稳定的网络标识
由 StatefulSet 创建的每个 Pod 都被分配一个序号索引(从零开始),然后用于推导 Pod 的名称和主机名,并将稳定的存储附加到 Pod 上。因此,Pod 的名称是可预测的,因为每个 Pod 的名称都是根据 StatefulSet 的名称和实例的序号索引推导出来的。与 Pod 具有随机名称不同,它们被很好地组织起来,如图所示。
图 10.5. 由 StatefulSet 创建的 Pod 具有可预测的名称(和主机名),这与由 ReplicaSet 创建的 Pod 不同

介绍管理服务
但这不仅仅是 Pod 具有可预测的名称和主机名。与常规 Pod 不同,有状态的 Pod 有时需要通过其主机名来寻址,而无状态的 Pod 通常不需要。毕竟,每个无状态 Pod 就像其他任何 Pod 一样。当你需要时,你可以选择任何一个。但是,对于有状态的 Pod,你通常希望对组中的特定 Pod 进行操作,因为它们彼此不同(例如,它们持有不同的状态)。
因此,StatefulSet 需要你创建一个相应的管理无头 Service,该 Service 用于为每个 pod 提供实际的网络身份。通过这个 Service,每个 pod 都会获得自己的 DNS 条目,因此它的对等节点和集群中的其他客户端可以通过主机名来定位 pod。例如,如果管理 Service 属于 default 命名空间,并且被命名为 foo,其中一个 pod 被命名为 A-0,你可以通过其完全限定域名 a-0.foo.default.svc.cluster.local 来访问该 pod。你不能用由 ReplicaSet 管理的 pod 做到这一点。
此外,你还可以使用 DNS 通过查找 foo.default.svc.cluster.local 域的 SRV 记录来查找所有 StatefulSet 的 pod 名称。我们将在第 10.4 节中解释 SRV 记录,并了解它们是如何用于发现 StatefulSet 的成员的。
替换丢失的宠物
当由 StatefulSet 管理的 pod 实例消失(因为 pod 运行的节点失败,被从节点驱逐,或有人手动删除 pod 对象)时,StatefulSet 确保用新的实例替换它——类似于 ReplicaSet 的做法。但与 ReplicaSet 相比,替换的 pod 将获得与消失的 pod 相同的名称和主机名(ReplicaSet 和 StatefulSet 之间的这种区别在图 10.6 中展示)。
图 10.6. StatefulSet 用具有相同身份的新 pod 替换丢失的 pod,而 ReplicaSet 则用完全无关的新 pod 替换它。

新 pod 不一定会被调度到同一个节点,但正如你早期所学的,pod 运行的节点不应该很重要。这对于有状态的 pod 也是成立的。即使 pod 被调度到不同的节点,它仍然会在之前的主机名下可用和可访问。
扩展 StatefulSet
扩展 StatefulSet 会创建一个新的 pod 实例,并使用下一个未使用的序号索引。如果你从两个实例扩展到三个实例,新实例将获得索引 2(现有的实例显然有索引 0 和 1)。
缩小 StatefulSet 的好处是,你总是知道哪个 pod 将被移除。再次强调,这与缩小 ReplicaSet 相比形成对比,在缩小 ReplicaSet 时,你不知道哪个实例将被删除,甚至无法指定首先删除哪个(但这个功能可能在未来被引入)。缩小 StatefulSet 总是首先删除具有最高序号索引的实例(如图 10.7 所示)。这使得缩小的效果可预测。
图 10.7. 缩小 StatefulSet 总是首先删除具有最高序号索引的 pod。

由于某些有状态的应用程序不能很好地处理快速的缩放操作,StatefulSets 一次只缩放一个 Pod 实例。例如,一个分布式数据存储,如果多个节点同时宕机,可能会丢失数据。例如,如果一个复制数据存储被配置为存储每个数据条目的两份副本,在两个节点同时宕机的情况下,如果数据条目正好存储在这两个节点上,那么数据条目就会丢失。如果缩放是顺序的,分布式数据存储就有时间在其他地方创建数据条目的额外副本来替换丢失的(单个)副本。
正因为如此,如果任何实例不健康,StatefulSets 也永远不会允许缩放操作。如果一个实例不健康,同时你减少一个实例,你实际上一次就失去了两个集群成员。
10.2.3. 为每个有状态实例提供稳定的专用存储
你已经看到了 StatefulSets 如何确保有状态的 Pod 拥有稳定的身份,但关于存储呢?每个有状态的 Pod 实例都需要使用自己的存储,而且如果有一个有状态的 Pod 被重新调度(用新的实例替换,但保持与之前相同的身份),新的实例也必须附加相同的存储。StatefulSets 是如何实现这一点的呢?
显然,有状态 Pod 的存储需要是持久的,并且与 Pod 解耦。在第六章章节 6 中,你学习了持久卷和持久卷声明,它们允许通过在 Pod 中按名称引用持久卷声明来将持久存储附加到 Pod。因为持久卷声明与持久卷是一对一映射的,所以 StatefulSet 中的每个 Pod 都需要引用不同的持久卷声明,以便拥有自己的独立持久卷。因为所有 Pod 实例都是从相同的 Pod 模板生成的,它们如何各自引用不同的持久卷声明呢?而且谁会创建这些声明?当然,你不应该期望在 StatefulSet 中预先创建与计划拥有的 Pod 数量一样多的持久卷声明,对吧?
将 Pod 模板与卷声明模板配对
StatefulSet 还必须创建持久卷声明,就像它创建 Pod 一样。因此,StatefulSet 也可以有一个或多个卷声明模板,这使它能够与每个 Pod 实例一起打印出持久卷声明(参见图 10.8)。
图 10.8. StatefulSet 创建 Pod 和持久卷声明。

对于持久卷声明,管理员可以提前配置持久卷,或者通过持久卷的动态配置来即时配置,如第六章末所述章节 6。
理解持久卷声明的创建和删除
通过增加一个 StatefulSet 来进行缩放会创建两个或更多的 API 对象(pod 和 pod 引用的一个或多个 PersistentVolumeClaims)。然而,缩放回时,只会删除 pod,而不会删除声明。这个原因很明显,如果你考虑一下删除声明时会发生什么。在删除声明后,与之绑定的 PersistentVolume 会被回收或删除,其内容也会丢失。
因为状态化 pod 是为了运行状态化应用程序,这意味着它们在卷中存储的数据很重要,所以在 Stateful-Set 缩小时删除声明可能是灾难性的——特别是触发缩放只需像减小 StatefulSet 的 replicas 字段一样简单。因此,你必须手动删除 PersistentVolumeClaims 来释放底层的 PersistentVolume。
将 PersistentVolumeClaim 重新附加到同一 pod 的新实例
缩放回后 PersistentVolumeClaim 仍然存在,这意味着后续的缩放可以将相同的声明以及与之绑定的 PersistentVolume 和其内容重新附加到新的 pod 实例(如图 10.9 所示 figure 10.9)。如果你意外地缩小了 StatefulSet,你可以通过再次缩放来撤销错误,新的 pod 将再次获得相同的持久状态(以及相同的名称)。
图 10.9. StatefulSets 在缩放时不会删除 PersistentVolumeClaims;然后在缩放回时重新附加它们。

10.2.4. 理解 StatefulSet 保证
如你所见,StatefulSets 的行为与 ReplicaSets 或 ReplicationControllers 不同。但这并不仅限于 pod 具有稳定的身份和存储。StatefulSets 还对其 pod 有不同的保证。
理解稳定身份和存储的影响
虽然,常规的无状态 pod 是可互换的,但状态化 pod 不是。我们已经看到状态化 pod 总是会被一个相同的 pod 替换(一个具有相同名称和主机名,使用相同的持久存储等)。当 Kubernetes 发现旧 pod 已不再存在时(例如,当你手动删除 pod 时),这种情况就会发生。
但如果 Kubernetes 无法确定 pod 的状态怎么办?如果它创建了一个具有相同身份的替换 pod,系统中可能会运行两个具有相同身份的应用实例。这两个实例也会绑定到相同的存储,因此具有相同身份的两个进程会覆盖相同的文件。由 ReplicaSet 管理的 pod,这并不是一个问题,因为应用显然是为了在相同的文件上工作而设计的。此外,ReplicaSets 会创建具有随机生成身份的 pod,因此两个进程以相同的身份运行是不可能的。
介绍 StatefulSet 的最多一个语义
因此,Kubernetes 必须非常小心,确保两个有状态 Pod 实例永远不会以相同的身份运行,并且绑定到相同的 PersistentVolumeClaim。StatefulSet 必须保证有状态 Pod 实例最多只有一个语义。
这意味着 StatefulSet 必须在创建替换 Pod 之前绝对确定 Pod 已经不再运行。这对处理节点故障有很大影响。我们将在本章后面演示这一点。然而,在我们这样做之前,你需要创建一个 StatefulSet 并观察它的行为。你还将在这个过程中了解它们的一些其他特性。
10.3. 使用 StatefulSet
为了正确展示 StatefulSet 的作用,你将构建自己的小型集群化数据存储。没有什么花哨的——更像是石器时代的数据存储。
10.3.1. 创建应用和容器镜像
你将使用本书中一直使用的 kubia 应用作为起点。你将扩展它,使其能够存储和检索每个 Pod 实例的单个数据条目。
你的数据存储源代码的重要部分如下所示。
列表 10.1. 一个简单的有状态应用:kubia-pet-image/app.js
... const dataFile = "/var/data/kubia.txt"; ... var handler = function(request, response) { if (request.method == 'POST') { var file = fs.createWriteStream(dataFile); 1 file.on('open', function (fd) { 1 request.pipe(file); 1 console.log("New data has been received and stored."); 1 response.writeHead(200); 1 response.end("Data stored on pod " + os.hostname() + "\n"); 1 }); } else { var data = fileExists(dataFile) 2 ? fs.readFileSync(dataFile, 'utf8') 2 : "No data posted yet"; 2 response.writeHead(200); 2 response.write("You've hit " + os.hostname() + "\n"); 2 response.end("Data stored on this pod: " + data + "\n"); 2 } }; var www = http.createServer(handler); www.listen(8080);
-
1 在 POST 请求中,将请求体存储到数据文件中。
-
2 在 GET(以及所有其他类型的)请求中,返回你的主机名和数据文件的内容。
当应用收到 POST 请求时,它会将请求体中的数据写入到文件 /var/data/kubia.txt。在 GET 请求中,它返回主机名和存储的数据(文件内容)。很简单,对吧?这是你应用的第一个版本。它还没有集群化,但足以让你开始。你将在本章后面扩展应用。
构建容器镜像的 Dockerfile 如下所示,与之前没有变化。
列表 10.2. 有状态应用的 Dockerfile:kubia-pet-image/Dockerfile
FROM node:7 ADD app.js /app.js ENTRYPOINT ["node", "app.js"]
现在开始构建镜像,或者使用我推送到 docker.io/luksa/kubia-pet 的镜像。
10.3.2. 通过 StatefulSet 部署应用
要部署你的应用,你需要创建两种(或三种)不同类型的对象:
-
用于存储你的数据文件的 PersistentVolumes(只有当集群不支持 PersistentVolumes 的动态供应时,你才需要创建这些卷)。
-
状态集所需的管理服务。
-
状态集本身。
对于每个 Pod 实例,状态集将创建一个 PersistentVolumeClaim,并将其绑定到一个 PersistentVolume。如果你的集群支持动态供应,你不需要手动创建任何 PersistentVolumes(你可以跳过下一节)。如果不支持,你将需要按照下一节所述创建它们。
创建持久卷
你需要三个 PersistentVolumes,因为你将扩展状态集到三个副本。如果你计划将状态集扩展得更多,你必须创建更多的 PersistentVolumes。
如果你使用 Minikube,请在书籍代码存档中的 Chapter06/persistent-volumes-hostpath.yaml 文件中部署定义的 PersistentVolumes。
如果你使用 Google Kubernetes Engine,你首先需要创建实际的 GCE Persistent Disks,如下所示:
$ gcloud compute disks create --size=1GiB --zone=europe-west1-b pv-a $ gcloud compute disks create --size=1GiB --zone=europe-west1-b pv-b $ gcloud compute disks create --size=1GiB --zone=europe-west1-b pv-c
注意
确保在节点运行的同一区域创建磁盘。
然后从 persistent-volumes-gcepd.yaml 文件创建 PersistentVolumes,该文件如下所示。
列表 10.3. 三个 PersistentVolumes:persistent-volumes-gcepd.yaml
kind: List 1 apiVersion: v1 items: - apiVersion: v1 kind: PersistentVolume 1 metadata: name: pv-a 2 spec: capacity: storage: 1Mi 3 accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Recycle 4 gcePersistentDisk: 5 pdName: pv-a 5 fsType: nfs4 5 - apiVersion: v1 kind: PersistentVolume metadata: name: pv-b ...
-
1 文件描述了一个包含三个持久卷的列表
-
2 持久卷的名称是 pv-a、pv-b 和 pv-c
-
3 每个持久卷的容量是 1 梅吉字节
-
4 当卷被声明释放时,它将被回收以再次使用。
-
5 该卷使用 GCE Persistent Disk 作为底层存储机制。
注意
在上一章中,你通过使用三横线分隔来在同一个 YAML 文件中指定多个资源。在这里,你使用了一种不同的方法,通过定义一个List对象并将资源作为对象的项目列出。这两种方法都是等效的。
此清单创建了名为pv-a、pv-b和pv-c的 PersistentVolumes。它们使用 GCE Persistent Disks 作为底层存储机制,因此它们不适用于不在 Google Kubernetes Engine 或 Google Compute Engine 上运行的集群。如果你在其他地方运行集群,你必须修改 PersistentVolume 定义并使用适当的卷类型,例如 NFS(网络文件系统),或类似类型。
创建管理服务
如前所述,在部署状态集之前,您首先需要创建一个无头服务,该服务将用于为您的有状态 Pod 提供网络标识。以下列表显示了服务清单。
列表 10.4. 状态集将使用的无头服务:kubia-service-headless.yaml
apiVersion: v1 kind: Service metadata: name: kubia 1 spec: clusterIP: None 2 selector: 3 app: kubia 3 ports: - name: http port: 80
-
1 服务的名称
-
2 状态集的主控服务必须是无头服务。
-
3 所有带有 app=kubia 标签的 Pod 都属于此服务。
您将clusterIP字段设置为None,这使得这是一个无头服务。它将启用 Pod 之间的对等发现(您稍后会需要)。一旦创建服务,您就可以继续创建实际的状态集。
创建状态集清单
现在,您终于可以创建状态集了。以下列表显示了清单。
列表 10.5. 状态集清单:kubia-statefulset.yaml
apiVersion: apps/v1beta1 kind: StatefulSet metadata: name: kubia spec: serviceName: kubia replicas: 2 template: metadata: labels: 1 app: kubia 1 spec: containers: - name: kubia image: luksa/kubia-pet ports: - name: http containerPort: 8080 volumeMounts: - name: data 2 mountPath: /var/data 2 volumeClaimTemplates: - metadata: 3 name: data 3 spec: 3 resources: 3 requests: 3 storage: 1Mi 3 accessModes: 3 - ReadWriteOnce 3
-
1 状态集创建的 Pod 将具有 app=kubia 标签。
-
2 Pod 内部的容器将在此路径上挂载 pvc 卷。
-
3 将从该模板创建持久卷声明。
状态集清单与您迄今为止创建的副本集或部署清单没有太大区别。新的内容是volumeClaimTemplates列表。在其中,您定义了一个名为data的卷声明模板,该模板将用于为每个 Pod 创建一个持久卷声明。如您从第六章中可能记得的那样,Pod 通过在清单中包含一个persistentVolumeClaim卷来引用声明。在前一个 Pod 模板中,您找不到这样的卷。状态集会自动将其添加到 Pod 规范中,并将卷配置为绑定到状态集为特定 Pod 创建的声明。
创建状态集
现在您将创建状态集:
$ kubectl create -f kubia-statefulset.yaml statefulset "kubia" created
现在,列出您的 Pod:
$ kubectl get po NAME READY STATUS RESTARTS AGE kubia-0 0/1 ContainerCreating 0 1s
注意到有什么奇怪的地方吗?还记得副本控制器或副本集是如何同时创建所有 Pod 实例的吗?您的状态集配置为创建两个副本,但它只创建了一个 Pod。
别担心,没有问题。第二个 pod 将在第一个 pod 启动并就绪后创建。StatefulSets 以这种方式运行,因为某些集群状态应用程序对同时启动的两个或多个集群成员的竞争条件敏感,所以在继续启动其他成员之前,先完全启动每个成员更安全。
再次列出 pods 以查看 pod 创建的进度:
$ kubectl get po NAME READY STATUS RESTARTS AGE kubia-0 1/1 Running 0 8s kubia-1 0/1 ContainerCreating 0 2s
看看,第一个 pod 现在正在运行,第二个 pod 已经被创建并正在启动。
检查生成的有状态 pod
让我们仔细看看以下列表中第一个 pod 的 spec,以了解 StatefulSet 是如何从 pod 模板和 PersistentVolumeClaim 模板构建 pod 的。
列表 10.6. 由 StatefulSet 创建的有状态 pod
$ kubectl get po kubia-0 -o yaml apiVersion: v1 kind: Pod metadata: ... spec: containers: - image: luksa/kubia-pet ... volumeMounts: - mountPath: /var/data 1 name: data 1 - mountPath: /var/run/secrets/kubernetes.io/serviceaccount name: default-token-r2m41 readOnly: true ... volumes: - name: data 2 persistentVolumeClaim: 2 claimName: data-kubia-0 3 - name: default-token-r2m41 secret: secretName: default-token-r2m41
-
1 如清单中指定的卷挂载
-
2 由 StatefulSet 创建的卷
-
3 由这个卷引用的声明
使用 PersistentVolumeClaim 模板创建了 PersistentVolumeClaim 以及 pod 内部的卷,这指的是创建的 PersistentVolumeClaim。
检查生成的 PersistentVolumeClaims
现在列出生成的 PersistentVolumeClaims 以确认它们已被创建:
$ kubectl get pvc NAME STATUS VOLUME CAPACITY ACCESSMODES AGE data-kubia-0 Bound pv-c 0 37s data-kubia-1 Bound pv-a 0 37s
生成的 PersistentVolumeClaims 的名称由volumeClaimTemplate中定义的名称和每个 pod 的名称组成。你可以检查声明的 YAML 以查看它们是否与模板匹配。
10.3.3. 玩转你的 pods
由于你的数据存储集群的节点现在正在运行,你可以开始探索它。你不能通过你创建的服务与你的 pods 通信,因为它是无头的。你需要直接连接到单个 pods(或者创建一个常规服务,但这不会允许你与特定的 pod 通信)。
你已经看到了直接连接到 pod 的方法:通过在其他 pod 上附加并运行curl,通过端口转发,等等。这次,你将尝试另一个选项。你将使用 API 服务器作为 pods 的代理。
通过 API 服务器与 pods 通信
API 服务器的一个有用功能是能够直接代理连接到单个 pod。如果你想对你的 kubia-0 pod 执行请求,你将访问以下 URL:
<apiServerHost>:<port>/api/v1/namespaces/default/pods/kubia-0/proxy/<path>
由于 API 服务器是受保护的,通过 API 服务器向 pods 发送请求是繁琐的(例如,你需要在每个请求中传递授权令牌)。幸运的是,在 第八章 中你学习了如何使用 kubectl proxy 来与 API 服务器通信,而无需处理身份验证和 SSL 证书。再次运行代理:
$ kubectl proxy 开始服务在 127.0.0.1:8001
现在,因为你将通过 kubectl 代理与 API 服务器通信,所以你会使用 localhost:8001 而不是实际的 API 服务器主机和端口。你将像这样向 kubia-0 pod 发送请求:
$ curl localhost:8001/api/v1/namespaces/default/pods/kubia-0/proxy/ 你已访问 kubia-0 存储在此 pod 上的数据:No data posted yet
响应显示,请求确实已被运行在你的 pod kubia-0 中的应用程序接收和处理。
注意
如果你收到一个空响应,请确保你没有在 URL 的末尾遗漏最后一个斜杠字符(或者确保 curl 通过使用其 -L 选项跟随重定向)。
因为你是通过连接到 kubectl 代理的 API 服务器与 pod 通信的,所以请求通过了两个不同的代理(第一个是 kubectl 代理,另一个是代理请求到 pod 的 API 服务器)。为了更清晰地了解,请查看 图 10.10。
图 10.10. 通过 kubectl 代理和 API 服务器代理连接到 pod

你发送到 pod 的请求是一个 GET 请求,但你也可以通过 API 服务器发送 POST 请求。这是通过向发送 GET 请求的相同代理 URL 发送 POST 请求来完成的。
当你的应用程序收到一个 POST 请求时,它会将请求体中的内容存储到一个本地文件中。向 kubia-0 pod 发送一个 POST 请求:
$ curl -X POST -d "Hey there! This greeting was submitted to kubia-0."
localhost:8001/api/v1/namespaces/default/pods/kubia-0/proxy/ 存储在 pod kubia-0 上的数据
你发送的数据现在应该存储在那个 pod 中。让我们看看当你再次执行 GET 请求时,它是否会返回存储的数据:
$ curl localhost:8001/api/v1/namespaces/default/pods/kubia-0/proxy/ 你已访问 kubia-0 存储在此 pod 上的数据:Hey there! This greeting was submitted to kubia-0.
好的,到目前为止一切顺利。现在让我们看看其他集群节点(kubia-1 pod)会说什么:
$ curl localhost:8001/api/v1/namespaces/default/pods/kubia-1/proxy/ 你已访问 kubia-1 存储在此 pod 上的数据:No data posted yet
如预期,每个节点都有自己的状态。但这个状态是否持久化?让我们来查明。
删除有状态的 Pod 以查看重新调度的 Pod 是否连接到相同的存储
你将要删除kubia-0 Pod 并等待它被重新调度。然后你会看到它是否仍然像之前一样提供相同的数据:
$ kubectl delete po kubia-0 pod "kubia-0" deleted
如果你列出 Pod,你会看到 Pod 正在终止:
$ kubectl get po NAME READY STATUS RESTARTS AGE kubia-0 1/1 Terminating 0 3m kubia-1 1/1 Running 0 3m
一旦成功终止,StatefulSet 就会创建一个具有相同名称的新 Pod:
$ kubectl get po NAME READY STATUS RESTARTS AGE kubia-0 0/1 ContainerCreating 0 6s kubia-1 1/1 Running 0 4m $ kubectl get po NAME READY STATUS RESTARTS AGE kubia-0 1/1 Running 0 9s kubia-1 1/1 Running 0 4m
让我再提醒你一次,这个新 Pod 可能被调度到集群中的任何节点,而不一定是旧 Pod 被调度到的相同节点。旧 Pod 的整个身份(名称、主机名和存储)实际上被移动到了新节点(如图 10.11 所示)。如果你使用 Minikube,你无法看到这一点,因为它只运行一个节点,但在多节点集群中,你可能看到 Pod 被调度到了与之前不同的节点。
图 10.11. 有状态的 Pod 可能被重新调度到不同的节点,但它保留了名称、主机名和存储。

新 Pod 现在正在运行,让我们检查它是否与其之前的形态具有完全相同的身份。Pod 的名称是相同的,但主机名和持久数据呢?你可以要求 Pod 本身来确认:
$ curl localhost:8001/api/v1/namespaces/default/pods/kubia-0/proxy/ You've hit kubia-0 Data stored on this pod: Hey there! This greeting was submitted to kubia-0.
Pod 的响应显示,主机名和数据与之前相同,这证实了 StatefulSet 总是用实际上完全相同的 Pod 替换被删除的 Pod。
缩放 StatefulSet
缩小 StatefulSet 并在较长时间后将其扩大,应该与删除 Pod 并让 StatefulSet 立即重新创建它没有区别。记住,缩小 StatefulSet 只会删除 Pod,但会保留 PersistentVolumeClaims 不变。我会让你自己尝试缩小 StatefulSet 并确认这种行为。
需要记住的关键点是缩放(缩小和扩大)是逐步进行的——类似于当 StatefulSet 最初创建时创建单个 Pod 的方式。当缩放超过一个实例时,首先删除具有最高序号的 Pod。只有当 Pod 完全终止后,才会删除具有第二高序号的 Pod。
通过常规的非无头 Service 暴露有状态的 Pod
在您继续本章的最后部分之前,您将在 Pod 前面添加一个适当的、非无头服务,因为客户端通常通过服务而不是直接连接来连接到 Pod。
您现在知道如何创建服务,但以防万一您不知道,以下列表显示了清单。
列表 10.7. 用于访问有状态 Pod 的常规服务:kubia-service-public.yaml
apiVersion: v1 kind: Service metadata: name: kubia-public spec: selector: app: kubia ports: - port: 80 targetPort: 8080
因为这不是一个公开暴露的服务(它是一个常规的ClusterIP服务,而不是NodePort或LoadBalancer类型的服务),您只能从集群内部访问它。您需要一个 Pod 来访问它,对吗?不一定。
通过 API 服务器连接到集群内部服务
与使用背负式舱室从集群内部访问服务相比,您可以使用 API 服务器提供的相同代理功能以您访问单个 Pod 的方式访问服务。
代理请求到服务的 URI 路径如下所示:
/api/v1/namespaces/<namespace>/services/<service name>/proxy/<path>
因此,您可以在本地机器上运行curl并通过kubectl代理访问服务,如下所示(您之前已运行kubectl proxy,并且它应该仍在运行):
$ curl localhost:8001/api/v1/namespaces/default/services/kubia-
public/proxy/ 您已访问 kubia-1 数据存储在此 Pod 上:尚未发布数据
同样,客户端(在集群内部)可以使用kubia-public服务将数据存储到您的集群数据存储中,并从中读取数据。当然,每个请求都落在随机集群节点上,因此您每次都会从随机节点获取数据。您将在下一部分改进这一点。
10.4. 在 StatefulSet 中发现同伴
我们还需要讨论一个更重要的事情。集群应用程序的一个重要要求是节点发现——找到集群中其他成员的能力。StatefulSet 的每个成员都需要轻松地找到所有其他成员。当然,它可以通过与 API 服务器通信来实现这一点,但 Kubernetes 的目标之一是提供帮助保持应用程序完全 Kubernetes 无关的特性。因此,应用程序与 Kubernetes API 通信是不希望的。
Pod 如何在不与 API 通信的情况下发现其同伴?是否存在一种现有、众所周知的您可以使用的技术来实现这一点?域名系统(DNS)怎么样?根据您对 DNS 了解的程度,您可能理解 A、CNAME 或 MX 记录的用途。其他不太为人所知的 DNS 记录类型也存在。其中之一是 SRV 记录。
介绍 SRV 记录
SRV 记录用于指向提供特定服务的服务器的主机名和端口号。Kubernetes 创建 SRV 记录以指向无头服务后端的 Pod 的主机名。
你将通过在新的临时 Pod 中运行 dig DNS 查询工具来列出你的有状态 Pod 的 SRV 记录。这是你将使用的命令:
$ kubectl run -it srvlookup --image=tutum/dnsutils --rm
--restart=Never -- dig SRV kubia.default.svc.cluster.local
该命令运行一个一次性 Pod (--restart=Never),名为 srvlookup,它连接到控制台 (-it),并在终止后立即删除 (--rm)。该 Pod 运行来自 tutum/dnsutils 镜像的单个容器,并执行以下命令:
dig SRV kubia.default.svc.cluster.local
以下列表显示了该命令打印的内容。
列表 10.8. 列出无头服务的 DNS SRV 记录
... ;; ANSWER SECTION: k.d.s.c.l. 30 IN SRV 10 33 0 kubia-0.kubia.default.svc.cluster.local. k.d.s.c.l. 30 IN SRV 10 33 0 kubia-1.kubia.default.svc.cluster.local. ;; ADDITIONAL SECTION: kubia-0.kubia.default.svc.cluster.local. 30 IN A 172.17.0.4 kubia-1.kubia.default.svc.cluster.local. 30 IN A 172.17.0.6 ...
注意
我不得不缩短实际名称以便将记录放入单行,所以 kubia.d.s.c.l 实际上是 kubia.default.svc.cluster.local。
ANSWER SECTION 显示了两个指向支持你的无头服务的 Pod 的 SRV 记录。每个 Pod 也都有自己的 A 记录,如 ADDITIONAL SECTION 所示。
为了让 Pod 获取 StatefulSet 中所有其他 Pod 的列表,你只需要执行一个 SRV DNS 查询。例如,在 Node.js 中,查询是这样执行的:
dns.resolveSrv("kubia.default.svc.cluster.local", callBackFunction);
你将在你的应用中使用这个命令来使每个 Pod 能够发现其 peers。
注意
返回的 SRV 记录的顺序是随机的,因为它们都具有相同的优先级。不要期望总是看到 kubia-0 在 kubia-1 之前列出。
10.4.1. 通过 DNS 实现对等发现
你的石器时代数据存储还没有集群化。每个数据存储节点完全独立于所有其他节点运行——它们之间不存在通信。你将在下一部分让它们相互交谈。
通过 kubia-public 服务连接到你的数据存储集群的客户发布的数据会落在随机集群节点上。集群可以存储多个数据条目,但客户目前没有很好的方法来查看所有这些条目。因为服务随机转发请求到 Pod,如果客户想要从所有 Pod 获取数据,就需要执行许多请求,直到它击中所有 Pod。
你可以通过让节点响应来自所有集群节点的数据来改进这一点。为此,节点需要找到所有它的 peers。你将使用你关于 StatefulSets 和 SRV 记录的知识来完成这个任务。
你将按照以下列表修改你的应用程序的源代码(完整的源代码可在本书的代码存档中找到;列表只显示了重要的部分)。
列表 10.9. 在示例应用中查找 peers:kubia-pet-peers-image/app.js
... const dns = require('dns'); const dataFile = "/var/data/kubia.txt"; const serviceName = "kubia.default.svc.cluster.local"; const port = 8080; ... var handler = function(request, response) { if (request.method == 'POST') { ... } else { response.writeHead(200); if (request.url == '/data') { var data = fileExists(dataFile) ? fs.readFileSync(dataFile, 'utf8') : "No data posted yet"; response.end(data); } else { response.write("您已访问 " + os.hostname() + "\n"); response.write("存储在集群中的数据:\n"); dns.resolveSrv(serviceName, function (err, addresses) { 1 if (err) { response.end("无法查找 DNS SRV 记录: " + err); return; } var numResponses = 0; if (addresses.length == 0) { response.end("未发现对等节点."); } else { addresses.forEach(function (item) { 2 var requestOptions = { host: item.name, port: port, path: '/data' }; httpGet(requestOptions, function (returnedData) { 2 numResponses++; response.write("- " + item.name + ": " + returnedData); response.write("\n"); if (numResponses == addresses.length) { response.end(); } }); }); } }); } } }; ...
-
1 应用执行 DNS 查询以获取 SRV 记录。
-
2 然后,会联系由 SRV 记录指向的每个 pod 以获取其数据。
图 10.12 展示了当你的应用收到一个 GET 请求时会发生什么。接收请求的服务器首先查找无头 kubia 服务的 SRV 记录,然后向服务后端的每个 pod 发送 GET 请求(甚至包括它自己,显然这是不必要的,但我希望保持代码尽可能简单)。然后,它返回所有节点及其上存储的数据列表。
图 10.12. 你简单分布式数据存储的操作

包含此应用新版本的容器镜像可在 docker.io/ luksa/kubia-pet-peers 上找到。
10.4.2. 更新 StatefulSet
你的 StatefulSet 已经在运行,让我们看看如何更新其 pod 模板,以便 pod 使用新的镜像。同时,你也会将副本数设置为 3。要更新 StatefulSet,请使用 kubectl edit 命令(patch 命令也是另一种选择):
$ kubectl edit statefulset kubia
这将在你的默认编辑器中打开 StatefulSet 定义。在定义中,将spec.replicas更改为3,并修改spec.template.spec.containers.image属性,使其指向新的镜像(luksa/kubia-pet-peers而不是luksa/kubia-pet)。保存文件并退出编辑器以更新 StatefulSet。之前有运行了两个副本,所以你现在应该看到一个新的副本kubia-2正在启动。列出 pod 以确认:
$ kubectl get po 名称 就绪 状态 重启次数 年龄 kubia-0 1/1 运行 0 25m kubia-1 1/1 运行 0 26m kubia-2 0/1 容器创建中 0 4s
新的 pod 实例正在运行新的镜像。但是现有的两个副本呢?从它们的年龄来看,它们似乎没有被更新。这是预期的,因为最初,StatefulSets 更像是 ReplicaSets 而不是 Deployments,所以当模板被修改时,它们不会执行滚动更新。你需要手动删除副本,然后 StatefulSet 将根据新的模板重新启动它们:
$ kubectl delete po kubia-0 kubia-1 pod "kubia-0" 已删除 pod "kubia-1" 已删除
注意
从 Kubernetes 版本 1.7 开始,StatefulSets 支持与 Deployments 和 DaemonSets 相同的滚动更新。使用kubectl explain查看 StatefulSet 的spec.updateStrategy字段文档以获取更多信息。
10.4.3. 尝试你的集群数据存储
两个 pod 启动后,你可以查看你闪亮的新石器时代数据存储是否按预期工作。向集群发送几个请求,如下所示。
列表 10.10. 通过服务写入集群数据存储
$ curl -X POST -d "The sun is shining" \
localhost:8001/api/v1/namespaces/default/services/kubia-public/proxy/ 存储在 pod kubia-1 上的数据 $ curl -X POST -d "The weather is sweet" \
localhost:8001/api/v1/namespaces/default/services/kubia-public/proxy/ 存储在 pod kubia-0 上的数据
现在,读取存储的数据,如下所示。
列表 10.11. 从数据存储读取
$ curl localhost:8001/api/v1/namespaces/default/services
/kubia-public/proxy/ 你已访问 kubia-2 数据存储在每个集群节点上:- kubia-0.kubia.default.svc.cluster.local: The weather is sweet - kubia-1.kubia.default.svc.cluster.local: The sun is shining - kubia-2.kubia.default.svc.cluster.local: 尚未发布数据
太棒了!当客户端请求到达你的集群节点之一时,它会发现所有对等节点,从它们那里收集数据,并将所有数据发送回客户端。即使你扩展或缩减 StatefulSet,服务客户端请求的 pod 也总能找到当时运行的所有对等节点。
应用本身可能并不那么有用,但我希望你能找到一种有趣的方式来展示复制的有状态应用实例如何发现它们的对等节点,并且可以轻松地处理水平扩展。
10.5. 理解有状态集如何处理节点故障
在 第 10.2.4 节 中,我们指出 Kubernetes 必须在创建替换 pod 之前绝对确定有状态 pod 已经不再运行。当一个节点突然失败时,Kubernetes 无法知道节点或其 pod 的状态。它无法知道 pod 是否已经不再运行,或者它们是否仍在运行,甚至可能仍然可访问,而且只有 Kubelet 停止向主节点报告节点的状态。
由于有状态集保证不会有两个具有相同身份和存储的 pod 同时运行,当节点看起来已经失败时,有状态集不能也不应该在没有确定 pod 已经不再运行之前创建替换 pod。
它只能知道这一点,当集群管理员告诉它时。为此,管理员需要删除 pod 或删除整个节点(这样做会删除所有调度到该节点的 pod)。
作为本章的最后一个练习,你将查看当集群中的一个节点从网络断开连接时,有状态集及其 pod 会发生什么。
10.5.1. 模拟节点从网络断开
如 第四章 中所述,你将通过关闭节点的 eth0 网络接口来模拟节点从网络断开。由于此示例需要多个节点,你无法在 Minikube 上运行它。你将使用 Google Kubernetes Engine。
关闭节点的网络适配器
要关闭节点的 eth0 接口,你需要像这样通过 ssh 登录到节点之一:
$ gcloud compute ssh gke-kubia-default-pool-32a2cac8-m0g1
然后,在节点内部运行以下命令:
$ sudo ifconfig eth0 down
你的 ssh 会话将停止工作,因此你需要打开另一个终端以继续。
检查 Kubernetes 主节点看到的节点状态
当节点的网络接口关闭时,运行在节点上的 Kubelet 将无法联系 Kubernetes API 服务器并通知它节点及其所有 pod 仍在运行。
一段时间后,控制平面会将节点标记为 NotReady。你可以通过列出节点时看到这一点,如下所示。
列表 10.12. 观察失败节点的状态变为 NotReady
$ kubectl get node NAME STATUS AGE VERSION gke-kubia-default-pool-32a2cac8-596v Ready 16m v1.6.2 gke-kubia-default-pool-32a2cac8-m0g1 NotReady`` 16m v1.6.2 gke-kubia-default-pool-32a2cac8-sgl7 Ready 16m v1.6.2
由于控制平面不再从节点接收状态更新,该节点上所有 pod 的状态都是 Unknown。这在下述 pod 列表中显示。
列表 10.13. 观察节点变为 NotReady 状态后 pod 的状态变化
$ kubectl get po NAME READY STATUS RESTARTS AGE kubia-0 1/1 Unknown 0 15m kubia-1 1/1 Running 0 14m kubia-2 1/1 Running 0 13m
如您所见,kubia-0 Pod 的状态不再已知,因为 Pod(仍然)运行在您关闭了网络接口的节点上。
理解状态未知的 Pod 会发生什么
如果节点重新上线并再次报告其及其 Pod 的状态,Pod 将被再次标记为Running。但如果 Pod 的状态未知超过几分钟(这个时间是可以配置的),Pod 将被自动从节点中移除。这是由主节点(Kubernetes 控制平面)完成的。它通过删除 Pod 资源来移除 Pod。
当 Kubelet 看到 Pod 被标记为删除时,它开始终止 Pod。在这种情况下,Kubelet 无法再连接到主节点(因为您已将节点从网络中断开),这意味着 Pod 将继续运行。
让我们检查当前的情况。使用kubectl describe来显示kubia-0 Pod 的详细信息,如下所示。
列表 10.14. 显示状态未知的 Pod 的详细信息
$ kubectl describe po kubia-0 Name: kubia-0 Namespace: default Node: gke-kubia-default-pool-32a2cac8-m0g1/10.132.0.2 ... Status: Terminating (expires Tue, 23 May 2017 15:06:09 +0200) Reason: NodeLost Message: Node gke-kubia-default-pool-32a2cac8-m0g1 which was running pod kubia-0 is unresponsive
Pod 显示为Terminating,终止原因列为NodeLost。信息表明节点被认为已丢失,因为它无响应。
注意
这里显示的是控制平面的世界视图。实际上,Pod 的容器仍在正常运行,根本就没有终止。
10.5.2. 手动删除 Pod
您知道节点不会回来,但您需要所有三个 Pod 运行以正确处理客户端。您需要将kubia-0 Pod 重新调度到健康的节点。如前所述,您需要手动删除节点或 Pod。
以常规方式删除 Pod
按照您通常删除 Pod 的方式删除 Pod:
$ kubectl delete po kubia-0 pod "kubia-0" deleted
完成了,对吧?通过删除 Pod,StatefulSet 应该立即创建一个替换 Pod,该 Pod 将被调度到剩余的节点之一。再次列出 Pod 以确认:
$ kubectl get po NAME READY STATUS RESTARTS AGE kubia-0 1/1 Unknown 0 15m kubia-1 1/1 Running 0 14m kubia-2 1/1 Running 0 13m
这很奇怪。您刚才删除了 Pod,kubectl说它已经删除了。为什么同一个 Pod 还在那里?
注意
列表中的 kubia-0 Pod 并不是一个具有相同名称的新 Pod——通过查看 AGE 列可以清楚地看出这一点。如果是新的,它的年龄将仅仅是几秒钟。
理解为什么 Pod 没有被删除
即使你在删除之前,Pod 已经被标记为删除。这是因为控制平面本身已经将其删除(为了将其从节点中驱逐出去)。
如果再次查看列表 10.14,你会看到 Pod 的状态是 Terminating。Pod 之前已经被标记为删除,并且一旦其节点上的 Kubelet 通知 API 服务器 Pod 的容器已经终止,它就会被移除。因为节点的网络已断开,所以这种情况永远不会发生。
强制删除 Pod
你唯一能做的就是告诉 API 服务器删除 Pod,而无需等待 Kubelet 确认 Pod 已不再运行。你可以这样做:
$ kubectl delete po kubia-0 --force --grace-period 0 警告:立即删除不会等待确认正在运行的资源已被终止。资源可能会在集群中无限期地继续运行。pod "kubia-0" 已删除
你需要同时使用 --force 和 --grace-period 0 选项。kubectl 显示的警告通知你做了什么。如果你再次列出 Pod,你最终会看到一个新的 kubia-0 Pod 被创建:
$ kubectl get po NAME READY STATUS RESTARTS AGE kubia-0 0/1 ContainerCreating 0 8s kubia-1 1/1 Running 0 20m kubia-2 1/1 Running 0 19m
警告
除非你知道节点不再运行或无法访问(并且将永远如此),否则不要强制删除有状态 Pod。
在继续之前,你可能想将你断开连接的节点重新上线。你可以通过 GCE 网络控制台或通过终端执行以下命令来实现:
$ gcloud compute instances reset <节点名称>
10.6. 摘要
这就结束了关于使用 StatefulSets 部署有状态应用程序的章节。本章向你展示了如何
-
为复制的 Pod 提供单独的存储
-
为 Pod 提供稳定的标识
-
创建一个 StatefulSet 和相应的无头管理服务
-
扩展和更新 StatefulSet
-
通过 DNS 发现 StatefulSet 的其他成员
-
通过主机名连接到其他成员
-
强制删除有状态 Pod
现在你已经知道了可以用来让 Kubernetes 运行和管理你的应用程序的主要构建块,我们可以更详细地了解它是如何做到这一点的。在下一章中,你将学习控制 Kubernetes 集群并保持你的应用程序运行的单个组件。
第三部分. 超越基础
第十一章. 理解 Kubernetes 内部机制
本章涵盖
-
构成 Kubernetes 集群的组件
-
每个组件的功能以及它是如何实现的
-
创建 Deployment 对象如何导致运行中的 Pod
-
运行中的 Pod 是什么
-
Pod 之间的网络是如何工作的
-
Kubernetes 服务是如何工作的
-
如何实现高可用性
通过阅读这本书到这一点,你已经熟悉了 Kubernetes 提供的内容以及它所做的工作。但到目前为止,我故意没有花太多时间解释它到底是如何做到这一切的,因为在我看来,在你对系统的工作有良好理解之前,深入探讨系统的工作原理是没有意义的。这就是为什么我们没有详细讨论 Pod 是如何被调度的,以及运行在 Controller Manager 内部的各个控制器是如何使部署的资源变得活跃的。因为你现在已经了解了 Kubernetes 中可以部署的大多数资源,现在是时候深入了解它们的实现方式了。
11.1. 理解架构
在你查看 Kubernetes 是如何工作的之前,让我们更仔细地看看构成 Kubernetes 集群的组件。在第一章中,你看到 Kubernetes 集群被分为两部分:
-
Kubernetes 控制平面
-
(工作)节点
让我们更仔细地看看这两部分的功能以及它们内部运行的内容。
控制平面的组件
控制平面是控制和使整个集群运行的部分。为了刷新你的记忆,构成控制平面的组件包括
-
etcd 分布式持久化存储
-
API 服务器
-
调度器
-
控制器管理器
这些组件存储和管理集群的状态,但它们不是运行应用程序容器的部分。
工作节点上运行的组件
运行你的容器的任务由每个工作节点上的组件来完成:
-
Kubelet
-
Kubernetes 服务代理(kube-proxy)
-
容器运行时(Docker、rkt 或其他)
附加组件
除了控制平面组件和运行在节点上的组件之外,集群还需要一些附加组件来提供到目前为止所讨论的所有内容。这包括
-
Kubernetes DNS 服务器
-
仪表板
-
入口控制器
-
Heapster,我们将在第十四章中讨论
-
容器网络接口网络插件(我们将在本章后面解释)
11.1.1. Kubernetes 组件的分布式特性
之前提到的组件都作为独立进程运行。组件及其相互依赖关系在图 11.1 中显示。
图 11.1. 控制平面和工作节点上的 Kubernetes 组件

为了获得 Kubernetes 提供的所有功能,所有这些组件都需要运行。但其中一些也可以在没有其他组件的情况下单独执行有用的任务。你将在我们检查每个组件时看到这一点。
检查控制平面组件的状态
API 服务器公开了一个名为 ComponentStatus 的 API 资源,它显示了每个控制平面组件的健康状态。您可以使用kubectl列出组件及其状态:
$ kubectl get componentstatuses NAME STATUS MESSAGE ERROR scheduler Healthy ok controller-manager Healthy ok etcd-0 Healthy {"health": "true"}
这些组件的通信方式
Kubernetes 系统组件仅与 API 服务器通信。它们不会直接相互交谈。API 服务器是唯一与 etcd 通信的组件。其他组件不会直接与 etcd 通信,而是通过与 API 服务器交谈来修改集群状态。
API 服务器与其他组件之间的连接几乎总是由组件发起的,如图 11.1 所示。figure 11.1。但当你使用kubectl获取日志、使用kubectl attach连接到正在运行的容器或使用kubectl port-forward命令时,API 服务器会连接到 Kubelet。
注意
kubectl attach命令与kubectl exec类似,但它连接到容器中运行的主进程,而不是运行额外的进程。
运行单个组件的多个实例
虽然工作节点上的组件都需要在同一个节点上运行,但控制平面的组件可以轻松地跨多个服务器分割。可以有多个控制平面组件的实例运行,以确保高可用性。虽然 etcd 和 API 服务器的多个实例可以同时激活并并行执行其工作,但调度器和控制器管理器在任何给定时间可能只有一个实例处于活动状态,其他则处于待机模式。
组件的运行方式
控制平面组件以及 kube-proxy 可以直接部署在系统上,或者作为 pods 运行(如图 11.1 所示。listing 11.1)。您可能会对此感到惊讶,但当我们谈到 Kubelet 时,这一切都会变得有意义。
Kubelet 是唯一始终以常规系统组件形式运行的组件,它通过运行其他组件作为 pods 来运行。要将控制平面组件作为 pods 运行,Kubelet 也部署在主节点上。下面的列表显示了使用kubeadm创建的集群中kube-system命名空间下的 pods,这将在附录 B 中解释。appendix B。
列表 11.1. Kubernetes 以 pods 形式运行的组件
$ kubectl get po -o custom-columns=POD:metadata.name,NODE:spec.nodeName
--sort-by spec.nodeName -n kube-system POD NODE kube-controller-manager-master master 1 kube-dns-2334855451-37d9k master 1 etcd-master master 1 kube-apiserver-master master 1 kube-scheduler-master master 1 kube-flannel-ds-tgj9k node1 2 kube-proxy-ny3xm node1 2 kube-flannel-ds-0eek8 node2 2 kube-proxy-sp362 node2 2 kube-flannel-ds-r5yf4 node3 2 kube-proxy-og9ac node3 2
-
1 etcd、API 服务器、调度器、控制器管理器和 DNS 服务器都在主节点上运行。
-
2 这三个节点各自运行一个 Kube Proxy pod 和一个 Flannel 网络 pod。
如列表所示,所有控制平面组件都在主节点上作为 pod 运行。有三个工作节点,每个节点运行一个 kube-proxy 和一个 Flannel 网络 pod,为 pod 提供覆盖网络(我们稍后会讨论 Flannel)。
提示
如列表所示,您可以使用-o custom-columns选项告诉kubectl显示自定义列,并使用--sort-by对资源列表进行排序。
现在,让我们逐一近距离观察每个组件,从控制平面的最低级组件——持久存储开始。
11.1.2. Kubernetes 如何使用 etcd
在这本书的整个过程中创建的所有对象——Pods、ReplicationControllers、Services、Secrets 等等——都需要以持久的方式存储在某处,以便它们的 manifest 在 API 服务器重启和故障中幸存。为此,Kubernetes 使用 etcd,它是一个快速、分布式且一致性的键值存储。由于它是分布式的,您可以运行多个 etcd 实例,以提供高可用性和更好的性能。
与 etcd 直接通信的唯一组件是 Kubernetes API 服务器。所有其他组件都通过 API 服务器间接读取和写入数据到 etcd。这带来了一些好处,其中包括更健壮的乐观锁系统以及验证;并且,通过将实际的存储机制从所有其他组件中抽象出来,未来替换它要简单得多。值得强调的是,etcd 是 Kubernetes 存储集群状态和元数据的唯一位置。
关于乐观并发控制
乐观并发控制(有时称为乐观锁定)是一种方法,在这种方法中,不是锁定数据块并防止在锁定期间读取或更新数据,而是在数据块中包含一个版本号。每次更新数据时,版本号都会增加。在更新数据时,会检查版本号是否在客户端读取数据和提交更新之间增加。如果发生这种情况,更新将被拒绝,客户端必须重新读取新数据并再次尝试更新。
结果是,当两个客户端尝试更新相同的数据条目时,只有第一个客户端会成功。
所有 Kubernetes 资源都包含一个 metadata.resourceVersion 字段,客户端在更新对象时需要将其传递回 API 服务器。如果版本与 etcd 中存储的版本不匹配,API 服务器将拒绝更新。
资源在 etcd 中的存储方式
当我写这篇文章时,Kubernetes 可以使用 etcd 的版本 2 或版本 3,但现在推荐使用版本 3,因为其性能得到了提升。etcd v2 在分层键空间中存储键,这使得键值对类似于文件系统中的文件。etcd 中的每个键要么是一个目录,包含其他键,要么是一个具有相应值的常规键。etcd v3 不支持目录,但由于键格式保持不变(键可以包含斜杠),你仍然可以将它们视为分组到目录中。Kubernetes 将其所有数据存储在 /registry 下的 etcd 中。以下列表显示了 /registry 下存储的键列表。
列表 11.2. Kubernetes 在 etcd 中存储的顶级条目
$ etcdctl ls /registry /registry/configmaps /registry/daemonsets /registry/deployments /registry/events /registry/namespaces /registry/pods ...
你会认出这些键对应于你在前几章中学到的资源类型。
注意
如果你使用的是 etcd API 的 v3 版本,你不能使用 ls 命令来查看目录的内容。相反,你可以使用 etcdctl get /registry --prefix=true 来列出所有以给定前缀开始的键。
以下列表显示了 /registry/pods 目录的内容。
列表 11.3. /registry/pods 目录中的键
$ etcdctl ls /registry/pods /registry/pods/default /registry/pods/kube-system
从名称可以推断,这两个条目对应于 default 和 kube-system 命名空间,这意味着每个命名空间存储了不同的 pod。以下列表显示了 /registry/pods/default 目录中的条目。
列表 11.4. default 命名空间中 pod 的 etcd 条目
$ etcdctl ls /registry/pods/default /registry/pods/default/kubia-159041347-xk0vc /registry/pods/default/kubia-159041347-wt6ga /registry/pods/default/kubia-159041347-hp2o5
每个条目对应一个单独的 pod。这些不是目录,而是键值条目。以下列表显示了其中一个条目中存储的内容。
列表 11.5. 代表 Pod 的 etcd 条目
$ etcdctl get /registry/pods/default/kubia-159041347-wt6ga {"kind":"Pod","apiVersion":"v1","metadata":{"name":"kubia-159041347-wt6ga", "generateName":"kubia-159041347-","namespace":"default","selfLink":...
你会认出这不过是一个 JSON 格式的 Pod 定义。API 服务器将资源的完整 JSON 表示存储在 etcd 中。由于 etcd 的分层键空间,你可以将所有存储的资源视为文件系统中的 JSON 文件。简单,对吧?
警告
在 Kubernetes 版本 1.7 之前,Secret资源的 JSON 清单也是这样存储的(它没有被加密)。如果有人直接访问了 etcd,他们就能知道你所有的机密信息。从版本 1.7 开始,Secrets 被加密,因此存储得更加安全。
确保存储对象的致性和有效性
记得在第一章中提到的 Google 的 Borg 和 Omega 系统吗?它们是 Kubernetes 的基础?就像 Kubernetes 一样,Omega 也使用一个集中存储来保存集群的状态,但与之不同的是,多个控制平面组件直接访问存储。所有这些组件都需要确保它们都遵循相同的乐观锁机制来正确处理冲突。如果一个组件没有完全遵循该机制,可能会导致数据不一致。
Kubernetes 通过要求所有其他控制平面组件通过 API 服务器来改进这一点。这样,集群状态更新总是保持一致,因为乐观锁机制只在单一位置实现,因此错误的可能性更小。API 服务器还确保写入存储的数据始终有效,并且数据的变化只能由授权客户端执行。
当 etcd 集群化时确保一致性
为了确保高可用性,你通常会运行多个 etcd 实例。多个 etcd 实例需要保持一致性。这样的分布式系统需要就实际状态达成共识。etcd 使用 RAFT 共识算法来实现这一点,这确保了在任何给定时刻,每个节点的状态要么是大多数节点同意的当前状态,要么是之前达成共识的状态之一。
连接到 etcd 集群不同节点的客户端将看到实际当前状态或过去的状态之一(在 Kubernetes 中,唯一的 etcd 客户端是 API 服务器,但可能有多个实例)。
一致性算法要求集群拥有多数(或法定多数)才能进步到下一个状态。因此,如果集群分裂成两个不连接的节点组,两组的状态永远不会分歧,因为要从上一个状态过渡到新状态,需要超过一半的节点参与状态变化。如果一个组包含所有节点的多数,另一个组显然不包含。第一个组可以修改集群状态,而另一个组不能。当两个组重新连接时,第二个组可以赶上第一个组的状态(参见图 11.2)。
图 11.2。在脑裂场景中,只有仍然拥有多数(法定多数)的一侧才接受状态变化。

为什么 etcd 实例的数量应该是奇数
etcd 通常以奇数个实例部署。我相信你一定想知道为什么。让我们比较有两个实例和有一个实例的情况。有两个实例需要两个实例都存在才能拥有多数。如果其中任何一个失败,由于没有多数存在,etcd 集群无法过渡到新状态。有两个实例比只有一个实例更糟糕。通过有两个实例,整个集群失败的可能性比单节点集群失败的可能性增加了 100%。
当比较三个实例和四个实例时,情况也相同。有三个实例时,一个实例可以失败,而多数(两个)仍然存在。有四个实例时,需要三个节点才能拥有多数(两个不够)。在三个和四个实例的集群中,只有一个实例可能失败。但运行四个实例时,如果其中一个实例失败,剩余三个实例中另一个实例失败的可能性更高(与一个失败节点和两个剩余节点的三个节点集群相比)。
通常情况下,对于大型集群,五个或七个节点的 etcd 集群就足够了。它可以分别处理两个或三个节点的故障,这在几乎所有情况下都足够了。
11.1.3. API 服务器的作用
Kubernetes API 服务器是所有其他组件以及客户端(如kubectl)使用的中心组件。它通过 RESTful API 提供了一个 CRUD(创建、读取、更新、删除)接口,用于查询和修改集群状态。它将这种状态存储在 etcd 中。
除了在 etcd 中提供一致的方式存储对象外,它还执行这些对象的验证,因此客户端不能存储配置不当的对象(如果它们直接写入存储,它们可以这样做)。除了验证外,它还处理乐观锁,因此对象的变化在并发更新的情况下永远不会被其他客户端覆盖。
API 服务器的一个客户端是从本书开始就一直在使用的命令行工具kubectl。例如,从 JSON 文件创建资源时,kubectl通过 HTTP POST 请求将文件内容发送到 API 服务器。图 11.3 显示了 API 服务器在收到请求时内部发生的事情。这将在接下来的几段中详细解释。
图 11.3. API 服务器的操作

使用认证插件认证客户端
首先,API 服务器需要验证发送请求的客户端。这是通过 API 服务器中配置的一个或多个认证插件完成的。API 服务器依次调用这些插件,直到其中一个确定谁在发送请求。它是通过检查 HTTP 请求来做到这一点的。
根据认证方法,用户可以从客户端证书或 HTTP 头(例如Authorization,您在第八章中使用过)中提取出来。插件提取客户端的用户名、用户 ID 以及用户所属的组。然后,这些数据在下一阶段使用,即授权阶段。
使用授权插件授权客户端
除了认证插件外,API 服务器还配置了使用一个或多个授权插件。它们的工作是确定经过认证的用户是否可以在请求的资源上执行请求的操作。例如,在创建 Pod 时,API 服务器依次咨询所有授权插件,以确定用户是否可以在请求的命名空间中创建 Pod。一旦某个插件表示用户可以执行该操作,API 服务器就进入下一阶段。
使用准入控制插件验证和/或修改请求中的资源
如果请求尝试创建、修改或删除资源,请求将通过准入控制。同样,服务器配置了多个准入控制插件。这些插件可以出于不同原因修改资源。它们可能将资源规范中缺失的字段初始化为配置的默认值,甚至覆盖它们。它们甚至可以修改其他相关资源,这些资源不在请求中,并且也可以根据任何原因拒绝请求。资源将通过所有准入控制插件。
注意
当请求仅尝试读取数据时,请求不会通过准入控制。
准入控制插件的例子包括
-
AlwaysPullImages—覆盖 Pod 的imagePullPolicy为Always,强制每次部署 Pod 时都拉取镜像。 -
ServiceAccount—将默认服务账户应用于未明确指定的 Pod。 -
NamespaceLifecycle—防止在正在被删除的命名空间以及不存在命名空间中创建 Pod。 -
ResourceQuota—确保特定命名空间中的 Pod 只使用分配给该命名空间的 CPU 和内存。我们将在第十四章中了解更多关于这个内容。第十四章。
你可以在 Kubernetes 文档中找到额外的 Admission Control 插件的列表,网址为kubernetes.io/docs/admin/admission-controllers/。
验证资源并持久存储
在让请求通过所有 Admission Control 插件之后,API 服务器随后验证对象,将其存储在 etcd 中,并向客户端返回响应。
11.1.4. 理解 API 服务器如何通知客户端资源变化
API 服务器除了我们讨论的内容外,不做任何事情。例如,当你创建 ReplicaSet 资源时,它不会创建 Pod,它也不会管理服务的端点。这是 Controller Manager 中的控制器所做的事情。
但 API 服务器甚至不会告诉这些控制器要做什么。它所做的只是使这些控制器和其他组件能够观察已部署资源的更改。控制平面组件可以请求在资源被创建、修改或删除时被通知。这使得组件能够在集群元数据发生变化时执行所需的任何任务。
客户端通过打开到 API 服务器的 HTTP 连接来监视变化。通过这个连接,客户端将接收对监视对象的修改流。每当一个对象被更新时,服务器会将对象的最新版本发送给所有连接的、正在监视该对象的客户端。图 11.4 展示了客户端如何监视 Pod 的变化,以及当一个 Pod 发生变化时,它是如何存储到 etcd 中,然后传递给当时所有监视该 Pod 的客户端的。
图 11.4. 当一个对象被更新时,API 服务器将更新后的对象发送给所有感兴趣的监视者。

API 服务器的一个客户端是kubectl工具,它也支持监视资源。例如,在部署 Pod 时,你不需要通过反复执行kubectl get pods来不断轮询 Pod 列表。相反,你可以使用--watch标志,并在 Pod 的创建、修改或删除时收到通知,如下面的列表所示。
列表 11.6. 监视 Pod 的创建和删除
$ kubectl get pods --watch 名称 就绪 状态 重启次数 年龄 kubia-159041347-14j3i 0/1 待定 0 0s kubia-159041347-14j3i 0/1 待定 0 0s kubia-159041347-14j3i 0/1 容器创建中 0 1s kubia-159041347-14j3i 0/1 运行中 0 3s kubia-159041347-14j3i 1/1 运行中 0 5s kubia-159041347-14j3i 1/1 终止中 0 9s kubia-159041347-14j3i 0/1 终止中 0 17s kubia-159041347-14j3i 0/1 终止中 0 17s kubia-159041347-14j3i 0/1 终止中 0 17s
你甚至可以让 kubectl 在每个监视事件上打印出整个 YAML,如下所示:
$ kubectl get pods -o yaml --watch
监视机制也被调度器使用,这是你接下来将要了解更多信息的下一个控制平面组件。
11.1.5. 理解调度器
你已经了解到,通常不指定 pod 应该运行在哪个集群节点上。这由调度器来决定。从远处看,调度器的操作看起来很简单。它所做的只是通过 API 服务器的监视机制等待新创建的 pod,并为每个尚未设置节点的新的 pod 分配一个节点。
调度器不会指示所选节点(或在该节点上运行的 Kubelet)运行 pod。调度器所做的所有事情只是通过 API 服务器更新 pod 定义。然后 API 服务器通知 Kubelet(再次,通过之前描述的监视机制)pod 已被调度。一旦目标节点上的 Kubelet 看到 pod 已被调度到其节点,它就会创建并运行 pod 的容器。
虽然对调度过程的粗略视图看起来很简单,但实际选择最佳节点以供 pod 使用的工作并不简单。当然,最简单的调度器可能会随机选择一个节点,而不关心该节点上已经运行的 pod。在光谱的另一端,调度器可以使用诸如机器学习等高级技术来预测在接下来的几分钟或几小时内将要调度的 pod 类型,并将 pod 调度到最大化未来硬件利用率,而无需重新调度现有的 pod。Kubernetes 的默认调度器介于两者之间。
理解默认调度算法
节点的选择可以分为两个部分,如图 11.5 所示:
-
过滤所有节点列表以获得一个可接受的节点列表,该节点列表可以调度 pod。
-
优先选择可接受的节点并选择最佳节点。如果有多个节点具有最高分数,则使用轮询确保 pod 均匀地部署在所有这些节点上。
图 11.5. 调度器为 pod 找到可接受的节点,然后选择最佳的节点。

寻找可接受的节点
为了确定哪些节点适合 Pod,调度器将每个节点通过一系列配置的谓词函数。这些检查各种事情,例如
-
节点能否满足 Pod 对硬件资源的需求?你将在第十四章中学习如何指定它们。
-
节点是否资源耗尽(是否报告内存或磁盘压力条件)?
-
如果 Pod 请求被调度到特定的节点(按名称),这是否是这个节点?
-
节点是否有标签与 Pod 规范中的节点选择器匹配(如果已定义)?
-
如果 Pod 请求绑定到特定的主机端口(在第十三章中讨论,index_split_099.html#filepos1232167),这个端口是否已经被这个节点占用?
-
如果 Pod 请求某种类型的卷,这个卷是否可以在这个节点上为这个 Pod 挂载,或者节点上是否已经有其他 Pod 正在使用相同的卷?
-
Pod 是否容忍节点的污点?污点和容忍在第十六章中解释(index_split_118.html#filepos1486732)。
-
Pod 是否指定了节点和/或亲和性或反亲和性规则?如果是,将 Pod 调度到这个节点是否会违反这些规则?这也在第十六章中解释(index_split_118.html#filepos1486732)。
所有这些检查都必须通过,节点才有资格托管 Pod。在每个节点上执行这些检查后,调度器最终会得到一个节点子集。这些节点中的任何一个都可以运行 Pod,因为它们有足够的可用资源来运行 Pod,并且符合你在 Pod 定义中指定的所有要求。
选择最佳的 Pod 节点
尽管所有这些节点都是可接受的并且可以运行 Pod,但其中一些可能比其他节点更好。假设你有一个双节点集群。两个节点都是合格的,但其中一个节点已经运行了 10 个 Pod,而另一个节点,由于某种原因,目前没有运行任何 Pod。在这种情况下,显然调度器应该优先考虑第二个节点。
或者是吗?如果这两个节点由云基础设施提供,那么将 Pod 调度到第一个节点并将第二个节点归还给云服务提供商以节省资金可能更好。
Pod 的高级调度
考虑另一个例子。想象一下有多个 Pod 副本。理想情况下,你希望它们尽可能分散在多个节点上,而不是全部调度到单个节点上。该节点的故障会导致由这些 Pod 支持的服务不可用。但如果 Pod 分散在不同的节点上,单个节点的故障几乎不会对服务的容量造成影响。
默认情况下,属于同一 Service 或 ReplicaSet 的 Pod 会分散在多个节点上。但这并不保证这种情况总是成立。但你可以通过定义 Pod 亲和性和反亲和性规则来强制 Pod 在集群中分散或保持紧密,这些规则在第十六章中解释(index_split_118.html#filepos1486732)。
即使是这两个简单的案例也展示了调度可能有多么复杂,因为它取决于众多因素。因此,调度器可以被配置以适应您的特定需求或基础设施细节,或者甚至可以完全用自定义实现替换。您也可以在没有调度器的情况下运行 Kubernetes 集群,但那样您就必须手动执行调度。
使用多个调度器
您可以在集群中运行多个调度器,而不是运行单个调度器。然后,对于每个 Pod,您通过在 Pod 规范中设置 schedulerName 属性来指定应该调度此特定 Pod 的调度器。
没有设置此属性的 Pod 将使用默认调度器进行调度,schedulerName 设置为 default-scheduler 的 Pod 也同样。所有其他 Pod 都会被默认调度器忽略,因此它们需要手动调度或由另一个监视此类 Pod 的调度器进行调度。
您可以在集群中实现自己的调度器并将它们部署进去,或者您可以部署 Kubernetes 调度器的不同配置选项的额外实例。
11.1.6. 控制器管理器中运行的控制器介绍
如前所述,API 服务器除了在 etcd 中存储资源并通知客户端关于更改之外,不做任何事情。调度器只为 Pod 分配节点,因此您需要其他活跃组件来确保系统的实际状态收敛到 API 服务器通过部署的资源指定的所需状态。这项工作由控制器管理器内部运行的控制器完成。
目前单个控制器管理器进程结合了执行各种协调任务的众多控制器。最终,这些控制器将被拆分为单独的进程,使您能够在必要时用自定义实现替换每个控制器。这些控制器包括
-
复制管理器(ReplicationController 资源的控制器)
-
ReplicaSet、DaemonSet 和作业控制器
-
部署控制器
-
有状态集控制器
-
节点控制器
-
服务控制器
-
端点控制器
-
命名空间控制器
-
持久卷控制器
-
其他
这些控制器各自的功能应该从其名称中显而易见。从列表中,您可以知道几乎为每个可以创建的资源都有一个控制器。资源是对集群中应该运行什么的描述,而控制器是执行实际工作的活跃 Kubernetes 组件,这些工作是由部署的资源触发的。
理解控制器做什么以及它们是如何做的
控制器执行许多不同的事情,但它们都监视 API 服务器以获取资源(部署、服务等)的更改,并对每个更改执行操作,无论是新对象的创建、更新还是现有对象的删除。大多数时候,这些操作包括创建其他资源或更新被监视的资源本身(例如更新对象的status)。
通常,控制器运行一个协调循环,该循环将实际状态与期望状态(在资源的spec部分指定)进行协调,并将新的实际状态写入资源的status部分。控制器使用监视机制来通知更改,但由于使用监视并不能保证控制器不会错过事件,它们还定期执行重新列表操作,以确保没有错过任何内容。
控制器之间从不直接交流。它们甚至不知道其他控制器的存在。每个控制器都连接到 API 服务器,并通过第 11.1.3 节中描述的监视机制,请求在控制器负责的任何类型资源的列表发生变化时通知它。
我们将简要地看看每个控制器都做了什么,但如果你想深入了解它们的功能,我建议你直接查看它们的源代码。侧边栏解释了如何开始。
探索控制器源代码的一些提示
如果你感兴趣,想确切地看到这些控制器是如何操作的,我强烈建议你浏览它们的源代码。为了使其更容易,这里有一些提示:
控制器的源代码可在github.com/kubernetes/kubernetes/blob/master/pkg/controller找到。
每个控制器通常都有一个构造函数,在其中它创建一个Informer,这基本上是一个在 API 对象更新时被调用的监听器。通常,Informer 监听特定类型资源的更改。查看构造函数将显示控制器正在监视哪些资源。
接下来,去找worker()方法。在其中,你会发现每次控制器需要做某事时都会被调用的方法。实际函数通常存储在一个名为syncHandler或类似字段的字段中。这个字段也在构造函数中初始化,所以你会在那里找到被调用的函数的名称。那个函数就是所有魔法发生的地方。
复制管理器
使 ReplicationController 资源活跃起来的控制器被称为副本管理器。我们在第四章中讨论了 ReplicationController 的工作方式。真正执行工作的是副本管理器,而不是 ReplicationController。让我们快速回顾一下控制器做了什么,因为这将帮助你理解其他控制器。
在第四章中,我们说 ReplicationController 的操作可以被视为一个无限循环,在每次迭代中,控制器找到匹配其 pod 选择器的 pods 数量,并将这个数量与所需的副本数量进行比较。
现在你已经知道了 API 服务器如何通过 watch 机制通知客户端,那么很明显,控制器并不是在每次迭代中都轮询 pods,而是由 watch 机制通知每个可能影响所需副本数量或匹配 pods 数量的变化(见图 11.6)。任何此类变化都会触发控制器重新检查所需的副本数量与实际副本数量,并相应地采取行动。
图 11.6。副本管理器监视 API 对象的变化。

你已经知道,当运行的 pod 实例太少时,Replication-Controller 会运行额外的实例。但实际上它并不运行这些实例。它创建新的 Pod 清单,将它们发布到 API 服务器,并让调度器和 Kubelet 完成它们的任务,即调度和运行 pod。
副本管理器通过 API 服务器操作 Pod API 对象来完成其工作。所有控制器都是这样操作的。
ReplicaSet、DaemonSet 和 Job 控制器
ReplicaSet 控制器几乎与之前描述的副本管理器做相同的事情,所以我们在这里没有太多要补充的。DaemonSet 和 Job 控制器也是类似的。它们从各自资源中定义的 pod 模板创建 Pod 资源。像副本管理器一样,这些控制器不运行 pods,而是将 Pod 定义发布到 API 服务器,让 Kubelet 创建它们的容器并运行它们。
Deployment 控制器
Deployment 控制器负责确保部署的实际状态与在相应的 Deployment API 对象中指定的所需状态保持同步。
每次修改 Deployment 对象时(如果修改应该影响已部署的 pods),Deployment 控制器都会执行新版本的发布。它是通过创建 ReplicaSet,然后根据 Deployment 中指定的策略适当地扩展旧的和新的 ReplicaSet 来实现的,直到所有旧 pods 都被新 pods 替换。它不会直接创建任何 pods。
状态集控制器
状态集控制器,类似于副本集控制器和其他相关控制器,根据状态集资源的规范创建、管理和删除 Pod。但与其他控制器只管理 Pod 不同,状态集控制器还为每个 Pod 实例实例化和管理持久卷声明。
节点控制器
节点控制器管理节点资源,这些资源描述了集群的工作节点。在众多事情中,节点控制器保持节点对象列表与集群中实际运行的机器列表同步。它还监控每个节点的健康状况,并将 Pod 从不可达的节点驱逐出去。
节点控制器并不是唯一对节点对象进行更改的组件。Kubelet 也会更改它们,并且显然也可以通过 REST API 调用由用户修改。
服务控制器
在第五章中,当我们讨论服务时,你了解到存在几种不同类型。其中之一是LoadBalancer服务,它从基础设施请求负载均衡器以使服务外部可用。当创建或删除LoadBalancer类型的服务时,服务控制器是请求和释放基础设施中的负载均衡器的控制器。
端点控制器
你会记得服务并不是直接链接到 Pod,而是包含一个端点列表(IP 和端口),该列表是手动或根据服务上定义的 Pod 选择器自动创建和更新的。端点控制器是保持端点列表不断更新为匹配标签选择器的 Pod 的 IP 和端口的主动组件。
如图 11.7 所示,控制器监视着服务和 Pod。当服务被添加或更新,或者 Pod 被添加、更新或删除时,它会选择与服务的 Pod 选择器匹配的 Pod,并将它们的 IP 和端口添加到端点资源中。记住,端点对象是一个独立对象,因此控制器在必要时会创建它。同样,当服务被删除时,它也会删除端点对象。
图 11.7. 端点控制器监视服务和 Pod 资源,并管理端点。

命名空间控制器
记得命名空间(我们在第三章中讨论过)?大多数资源都属于特定的命名空间。当一个命名空间资源被删除时,该命名空间中的所有资源也必须被删除。这就是命名空间控制器所做的事情。当它被通知命名空间对象的删除时,它会通过 API 服务器删除属于该命名空间的所有资源。
持久卷控制器
在第六章中,你学习了关于 PersistentVolumes 和 PersistentVolumeClaims 的内容。一旦用户创建 PersistentVolumeClaim,Kubernetes 必须找到一个合适的 PersistentVolume 并将其绑定到声明。这是由 PersistentVolume 控制器执行的。
当 PersistentVolumeClaim 出现时,控制器通过选择与请求中声明匹配的访问模式且声明容量高于请求容量的最小 PersistentVolume 来找到最佳匹配。它是通过按容量升序保持每个访问模式的 PersistentVolume 有序列表,并从列表中返回第一个卷来实现的。
然后,当用户删除 PersistentVolumeClaim 时,卷将解除绑定并根据卷的回收策略(保持原样、删除或清空)回收。
控制器总结
你现在应该对每个控制器做什么以及控制器通常如何工作有一个很好的感觉。再次强调,所有这些控制器都通过 API 服务器操作 API 对象。它们不会直接与 Kubelets 通信或向它们发出任何类型的指令。实际上,它们甚至不知道 Kubelets 的存在。在控制器更新 API 服务器中的资源后,Kubelets 和 Kubernetes 服务代理(同样对控制器的存在一无所知)执行它们的工作,例如启动 Pod 的容器并将网络存储附加到它们,或者在服务的情况下,在 Pod 之间设置实际的负载均衡。
控制面处理整个系统操作的一部分,因此要完全理解 Kubernetes 集群中事情的发展,还需要了解 Kubelet 和 Kubernetes 服务代理的作用。我们将在下一节学习这一点。
11.1.7. Kubelet 执行的操作
与所有作为 Kubernetes 控制平面一部分并在主节点(s)上运行的控制器不同,Kubelet 和服务代理都在运行实际 Pod 容器的 worker 节点上运行。Kubelet 究竟做什么?
理解 Kubelet 的工作
简而言之,Kubelet 是负责在 worker 节点上运行一切组件。它的初始任务是创建 API 服务器中的 Node 资源来注册其运行的节点。然后它需要持续监控 API 服务器以查找已调度到节点的 Pod,并启动 Pod 的容器。它是通过告诉配置的容器运行时(Docker、CoreOS 的 rkt 或其他)从特定的容器镜像运行一个容器来实现的。然后 Kubelet 持续监控正在运行的容器并向 API 服务器报告它们的状态、事件和资源消耗。
Kubelet 也是运行容器存活探测的组件,当探测失败时重启容器。最后,当它们的 Pod 从 API 服务器删除时,它会终止容器并通知服务器 Pod 已终止。
在没有 API 服务器的情况下运行静态 pods
虽然 Kubelet 与 Kubernetes API 服务器通信并从那里获取 pod 清单,但它也可以根据特定本地目录中的 pod 清单文件运行 pods,如图 11.8 所示。此功能用于运行控制平面组件的容器化版本作为 pods,正如你在本章开头所见。
图 11.8. Kubelet 根据来自 API 服务器的 pod 规范和本地文件目录运行 pods。

除了以原生方式运行 Kubernetes 系统组件外,您可以将它们的 pod 清单放入 Kubelet 的清单目录中,并让 Kubelet 运行和管理它们。您还可以使用相同的方法运行您自定义的系统容器,但通过 DaemonSet 执行是推荐的方法。
11.1.8. Kubernetes 服务代理的作用
除了 Kubelet 之外,每个工作节点还运行 kube-proxy,其目的是确保客户端可以通过 Kubernetes API 连接到你定义的服务。kube-proxy 确保连接到服务 IP 和端口的连接最终到达支持该服务的某个 pod(或其他非 pod 服务端点)。当一个服务由多个 pod 支持时,代理在这些 pod 之间执行负载均衡。
为什么叫代理
kube-proxy 的初始实现是 userspace 代理。它使用实际的服务器进程来接受连接并将它们代理到 pods。为了拦截目的地为服务 IP 的连接,代理配置了 iptables 规则(iptables 是管理 Linux 内核数据包过滤功能的工具)以将连接重定向到代理服务器。userspace 代理模式的粗略图示见 图 11.9。
图 11.9. userspace 代理模式

kube-proxy 得名于它实际上是一个代理,但当前的、性能更好的实现仅使用 iptables 规则将数据包重定向到随机选择的后端 pod,而不通过实际的代理服务器。这种模式称为 iptables 代理模式,如图 11.10 所示。
图 11.10. iptables 代理模式

这两种模式之间的主要区别在于数据包是否通过 kube-proxy 并必须在用户空间中处理,或者是否仅由内核(在内核空间)处理。这对性能有重大影响。
另一个较小的区别是,userspace 代理模式以真正的轮询方式平衡 pod 之间的连接,而 iptables 代理模式则不这样做——它随机选择 pod。当只有少数客户端使用服务时,它们可能不会均匀地分布在 pod 上。例如,如果一个服务有两个后端 pod 但只有大约五个客户端,如果你看到四个客户端连接到 pod A 而只有一个客户端连接到 pod B,请不要感到惊讶。随着客户端或 pod 数量的增加,这个问题就不那么明显了。
你将在第 11.5 节中详细了解 iptables 代理模式的工作原理。
11.1.9. 介绍 Kubernetes 附加组件
我们现在已经讨论了使 Kubernetes 集群工作的核心组件。但在本章的开头,我们也列出了一些附加组件,尽管它们并非总是必需的,但它们启用了诸如 Kubernetes 服务的 DNS 查找、通过单个外部 IP 地址公开多个 HTTP 服务、Kubernetes 网络仪表板等功能。
附加组件的部署方式
这些组件作为附加组件提供,并通过向 API 服务器提交 YAML 清单的方式部署为 pod,正如你在本书中一直所做的那样。其中一些组件通过 Deployment 资源或 ReplicationController 资源部署,而另一些则通过 DaemonSet 部署。
例如,在我撰写本文时,在 Minikube 中,Ingress 控制器和仪表板附加组件作为 ReplicationControllers 部署,如下面的列表所示。
列表 11.7. 在 Minikube 中与 ReplicationControllers 部署的附加组件
$ kubectl get rc -n kube-system NAME DESIRED CURRENT READY AGE default-http-backend 1 1 1 6d kubernetes-dashboard 1 1 1 6d nginx-ingress-controller 1 1 1 6d
DNS 附加组件作为 Deployment 部署,如下面的列表所示。
列表 11.8. kube-dns Deployment
$ kubectl get deploy -n kube-system NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE kube-dns 1 1 1 1 6d
让我们看看 DNS 和 Ingress 控制器是如何工作的。
DNS 服务器的工作原理
默认情况下,集群中的所有 pod 都配置为使用集群的内部 DNS 服务器。这允许 pod 通过名称轻松查找服务,甚至在无头服务的情况下查找 pod 的 IP 地址。
DNS 服务器 Pod 通过kube-dns服务暴露,允许 Pod 在集群中移动,就像任何其他 Pod 一样。该服务的 IP 地址被指定为每个容器内部的/etc/resolv.conf文件中的nameserver。kube-dns Pod 使用 API 服务器的监视机制来观察服务和服务端点的变化,并在每次变化时更新其 DNS 记录,使其客户端始终获得(相对)最新的 DNS 信息。我说相对是因为在服务或端点资源更新和 DNS Pod 收到监视通知之间的时间,DNS 记录可能无效。
(大多数)Ingress 控制器是如何工作的
与 DNS 附加组件不同,你会发现有几个不同的 Ingress 控制器实现,但它们大多数以相同的方式工作。Ingress 控制器运行一个反向代理服务器(例如 Nginx),并根据集群中定义的 Ingress、服务和端点资源进行配置。因此,控制器需要观察这些资源(再次,通过监视机制),并在它们中的任何一个发生变化时更改代理服务器的配置。
尽管 Ingress 资源的定义指向服务,但 Ingress 控制器直接将流量转发到服务的 Pod,而不是通过服务 IP。这影响了外部客户端通过 Ingress 控制器连接时客户端 IP 的保留,这使得它们在某些用例中比服务更受欢迎。
使用其他附加组件
你已经看到 DNS 服务器和 Ingress 控制器附加组件与在 Controller Manager 中运行的控制器类似,只是它们也接受客户端连接,而不是仅通过 API 服务器观察和修改资源。
其他附加组件类似。它们都需要观察集群状态,并在状态发生变化时执行必要的操作。我们将在本章和剩余章节中介绍几个其他附加组件。
11.1.10. 将一切整合
现在,你已经了解到整个 Kubernetes 系统由相对较小、松散耦合的组件组成,具有良好的关注点分离。API 服务器、调度器、在 Controller Manager 中运行的各个控制器、Kubelet 和 kube-proxy 共同工作,以保持系统的实际状态与您指定的期望状态同步。
例如,将 Pod 规范提交给 API 服务器会触发各种 Kubernetes 组件的协调舞蹈,最终导致 Pod 的容器运行。你将在下一节中了解这个舞蹈是如何展开的。
11.2. 控制器之间的协作
现在,你已经了解了 Kubernetes 集群由哪些组件组成。现在,为了巩固你对 Kubernetes 工作原理的理解,让我们回顾一下当创建 pod 资源时会发生什么。因为你通常不会直接创建 pod,所以你将创建一个部署资源,并查看启动 pod 容器必须发生的所有操作。
11.2.1. 理解涉及哪些组件
在你开始整个过程之前,控制器、调度器和 Kubelet 都在监视 API 服务器,以查看它们各自资源类型的更改。这如图 11.11 所示。图中所示的所有组件都将参与你即将触发的过程。该图不包括 etcd,因为它隐藏在 API 服务器后面,你可以将 API 服务器视为对象存储的地方。
图 11.11. Kubernetes 组件通过 API 服务器监视 API 对象

11.2.2. 事件链
想象你已经准备好了包含部署清单的 YAML 文件,并且你即将通过 kubectl 将其提交给 Kubernetes。kubectl 会以 HTTP POST 请求的形式将清单发送到 Kubernetes API 服务器。API 服务器验证部署规范,将其存储在 etcd 中,并返回响应给 kubectl。现在,一系列事件开始展开,如图 11.12 所示。
图 11.12. 当部署资源提交到 API 服务器时展开的事件链

部署控制器创建副本集
所有通过 API 服务器监视部署列表的 API 服务器客户端在部署资源创建后立即收到通知。其中之一是部署控制器,正如我们之前讨论的,它是负责处理部署的活跃组件。
如你从 第九章 记忆的那样,部署背后由一个或多个副本集支持,然后创建实际的 pod。当部署控制器检测到新的部署对象时,它会为部署的当前规范创建一个副本集。这涉及到通过 Kubernetes API 创建一个新的副本集资源。部署控制器根本不处理单个 pod。
副本集控制器创建 pod 资源
新创建的副本集随后被副本集控制器获取,该控制器监视 API 服务器中副本集资源的创建、修改和删除。控制器考虑副本集定义的副本数量和 pod 选择器,并验证是否有足够的现有 pod 与选择器匹配。
控制器随后根据副本集中的 pod 模板(当部署控制器创建副本集时,pod 模板从部署复制过来)创建 pod 资源。
调度器为新创建的 Pod 分配节点
这些新创建的 Pod 现在存储在 etcd 中,但它们各自仍然缺少一个重要的事情——它们还没有关联的节点。它们的nodeName属性尚未设置。调度器监视这样的 Pod,当它遇到这样的 Pod 时,会选择最适合该 Pod 的节点并将 Pod 分配给该节点。Pod 的定义现在包括它应该在哪个节点上运行的名字。
到目前为止,所有的事情都在 Kubernetes 控制平面中发生。参与整个过程的控制器除了通过 API 服务器更新资源外,没有做任何有形的工作。
Kubelet 运行 Pod 的容器
到目前为止,工作节点还没有做任何事情。Pod 的容器还没有启动。Pod 容器的镜像甚至还没有下载。
但随着 Pod 现在被调度到特定的节点,该节点上的 Kubelet 终于可以开始工作了。Kubelet 正在监视 API 服务器上 Pod 的变化,看到一个新的 Pod 被调度到它的节点,因此它检查 Pod 定义并指示 Docker 或它所使用的任何容器运行时启动 Pod 的容器。容器运行时随后运行这些容器。
11.2.3. 观察集群事件
控制平面组件和 Kubelet 在执行这些操作时都会向 API 服务器发送事件。它们通过创建事件资源来完成,这些资源类似于其他任何 Kubernetes 资源。每次你使用kubectl describe来检查这些资源时,你都已经看到了与特定资源相关的事件,但你也可以直接使用kubectl get events来检索事件。
可能是我个人的感受,但使用kubectl get来检查事件很痛苦,因为它们没有按照正确的时间顺序显示。相反,如果一个事件发生多次,事件只会显示一次,显示它首次出现的时间、最后一次出现的时间和发生次数。幸运的是,使用--watch选项来监视事件对眼睛来说更容易接受,并且有助于了解集群中正在发生的事情。
以下列表显示了之前描述过程中发出的事件(一些列已被删除,输出经过大量编辑以在页面有限的空白空间中可读)。
列表 11.9. 监视控制器发出的事件
$ kubectl get events --watch NAME KIND REASON SOURCE ... kubia Deployment ScalingReplicaSet deployment-controller
Scaled up replica set kubia-193 to 3 ... kubia-193 ReplicaSet SuccessfulCreate replicaset-controller
Created pod: kubia-193-w7ll2 ... kubia-193-tpg6j Pod Scheduled default-scheduler
Successfully assigned kubia-193-tpg6j to node1 ... kubia-193 ReplicaSet SuccessfulCreate replicaset-controller
Created pod: kubia-193-39590 ... kubia-193 ReplicaSet SuccessfulCreate replicaset-controller
Created pod: kubia-193-tpg6j ... kubia-193-39590 Pod Scheduled default-scheduler
Successfully assigned kubia-193-39590 to node2 ... kubia-193-w7ll2 Pod Scheduled default-scheduler
Successfully assigned kubia-193-w7ll2 to node2 ... kubia-193-tpg6j Pod Pulled kubelet, node1
Container image already present on machine ... kubia-193-tpg6j Pod Created kubelet, node1
Created container with id 13da752 ... kubia-193-39590 Pod Pulled kubelet, node2
Container image already present on machine ... kubia-193-tpg6j Pod Started kubelet, node1
Started container with id 13da752 ... kubia-193-w7ll2 Pod Pulled kubelet, node2
Container image already present on machine ... kubia-193-39590 Pod Created kubelet, node2
Created container with id 8850184 ...
如您所见,SOURCE列显示了执行操作的控制器,而NAME和KIND列显示了控制器正在对其采取行动的资源。REASON列和MESSAGE列(每行显示一次)提供了控制器所做操作的更多详细信息。
11.3. 理解运行中的 Pod 是什么
现在 Pod 已经开始运行,让我们更仔细地看看一个运行中的 Pod 究竟是什么。如果一个 Pod 只包含一个容器,你认为 Kubelet 只是运行这个单个容器,还是还有其他的事情要做?
您在这本书中运行了几个 Pod。如果您是调查型的人,您可能已经偷偷地看了一眼创建 Pod 时 Docker 到底运行了什么。如果不是,让我来解释您会看到什么。
假设您运行了一个单个容器的 Pod。比如说,您创建了一个 Nginx Pod:
$ kubectl run nginx --image=nginx deployment "nginx" created
现在,你可以使用ssh连接到运行 Pod 的工作节点,并检查正在运行的 Docker 容器列表。我在使用 Minikube 进行测试,所以为了连接到单个节点,我使用minikube ssh。如果你使用 GKE,你可以使用gcloud compute ssh <node name>连接到节点。
一旦你进入了节点内部,你可以使用docker ps列出所有正在运行的容器,如下面的列表所示。
列表 11.10. 列出正在运行的 Docker 容器
docker@minikubeVM:~$ docker ps CONTAINER ID IMAGE COMMAND CREATED c917a6f3c3f7 nginx "nginx -g 'daemon off" 4 seconds ago 98b8bf797174 gcr.io/.../pause:3.0 "/pause" 7 seconds ago ...
注意
我已经从之前的列表中移除了无关信息——这包括列和行。我还移除了所有其他正在运行的容器。如果你自己尝试这个操作,请注意几秒钟前创建的两个容器。
如预期的那样,你看到了 Nginx 容器,但还有一个额外的容器。从COMMAND列来看,这个额外的容器并没有做任何事情(容器的命令是"pause")。如果你仔细观察,你会看到这个容器是在 Nginx 容器之前几秒钟创建的。它的作用是什么?
这个暂停容器是包含一个 Pod 中所有容器的容器。还记得 Pod 中的所有容器共享相同的网络和其他 Linux 命名空间吗?暂停容器是一个基础设施容器,它的唯一目的是包含所有这些命名空间。然后,Pod 中的所有其他用户定义的容器都使用 Pod 基础设施容器的命名空间(参见图 11.13)。
图 11.13. 一个包含两个容器的 Pod 导致三个运行中的容器共享相同的 Linux 命名空间。

实际的应用容器可能会死亡并重新启动。当这样的容器再次启动时,它需要成为之前相同的 Linux 命名空间的一部分。基础设施容器使得这一点成为可能,因为它的生命周期与 Pod 的生命周期绑定——容器从 Pod 被调度开始运行,直到 Pod 被删除。如果在此时基础设施 Pod 被杀死,Kubelet 会重新创建它以及 Pod 的所有容器。
11.4. Pod 间网络
到现在为止,你知道每个 Pod 都有自己的唯一 IP 地址,并且可以通过一个平坦的、无 NAT 的网络与其他所有 Pod 进行通信。Kubernetes 究竟是如何实现这一点的呢?简而言之,它并没有实现。网络的设置是由系统管理员或容器网络接口(CNI)插件完成的,而不是由 Kubernetes 本身完成。
11.4.1. 网络必须具备的特点
Kubernetes 不要求你使用特定的网络技术,但它确实要求 pods(或者更准确地说,它们的容器)能够相互通信,无论它们是否运行在同一个工作节点上。pods 用于通信的网络必须是这样的,即 pod 看到的自己的 IP 地址与所有其他 pod 看到的该 pod 的 IP 地址完全相同。
看看 图 11.14。当 pod A 连接到(向 pod B 发送网络数据包)pod B 时,pod B 看到的源 IP 地址必须与 pod A 看到的自己的 IP 地址相同。在两者之间不应执行网络地址转换(NAT)——pod A 发送的数据包必须以源和目标地址不变的方式到达 pod B。
图 11.14. Kubernetes 要求 pods 通过无 NAT 网络连接。

这很重要,因为它使得在 pods 内运行的应用程序的网络变得简单,就像它们在连接到同一网络交换机的机器上运行一样。pod 之间没有 NAT 的存在使得它们内部运行的应用程序能够在其他 pod 中进行自我注册。
例如,假设你有一个客户端 pod X 和 pod Y,它为所有注册到它的 pods 提供一种通知服务。pod X 连接到 pod Y 并告诉它:“嘿,我是 pod X,可在 IP 1.2.3.4 上找到;请将更新发送到这个 IP 地址。”提供服务的 pod 可以通过使用接收到的 IP 地址连接到第一个 pod。
对于 pods 之间无 NAT 通信的要求也扩展到了 pod 到节点和节点到 pod 的通信。但是,当 pod 与互联网上的服务通信时,pod 发送的数据包的源 IP 地址确实需要更改,因为 pod 的 IP 地址是私有的。出站数据包的源 IP 地址被更改为主机工作节点的 IP 地址。
构建合适的 Kubernetes 集群需要根据这些要求设置网络。有各种方法和技术可供选择,每种方法在特定场景下都有其自身的优点或缺点。因此,我们不会深入探讨具体技术。相反,让我们解释一下 pod 之间的网络是如何工作的。
11.4.2. 深入了解网络工作原理
在 第 11.3 节 中,我们了解到 pod 的 IP 地址和网络命名空间是由基础设施容器(即 pause 容器)设置和保留的。然后 pod 的容器使用其网络命名空间。因此,pod 的网络接口就是基础设施容器中设置的内容。让我们看看接口是如何创建的,以及它是如何连接到所有其他 pod 的接口的。看看 图 11.15。我们将在下一节讨论它。
图 11.15. 节点上的 pods 通过虚拟以太网接口对连接到同一个网桥。

启用同一节点上 pods 之间的通信
在启动基础设施容器之前,为容器创建一个虚拟以太网接口对(veth 对)。这对中的一个接口保留在主机命名空间中(当你在这个节点上运行ifconfig时,你会看到它被列为vethXXX),而另一个被移动到容器的网络命名空间并重命名为eth0。这两个虚拟接口就像管道的两端(或者就像通过以太网线连接的两个网络设备)——一侧进入的内容会在另一侧出来,反之亦然。
主机网络命名空间中的接口连接到容器运行时配置的网络桥接器。容器中的eth0接口分配了桥接器地址范围内的 IP 地址。容器内部运行的应用程序发送到eth0网络接口(容器命名空间中的那个)的内容,会从主机命名空间中的另一个 veth 接口出来,并发送到桥接器。这意味着它可以被连接到桥接器的任何网络接口接收。
如果 pod A 向 pod B 发送网络数据包,数据包首先通过 pod A 的 veth 对到达桥接器,然后通过 pod B 的 veth 对。一个节点上的所有容器都连接到同一个桥接器,这意味着它们可以相互通信。但为了使运行在不同节点上的容器之间的通信成为可能,那些节点上的桥接器需要以某种方式连接起来。
启用不同节点上 pod 之间的通信
你有多种方式连接不同节点上的桥接器。这可以通过覆盖网络或底层网络或通过常规的第三层路由来实现,我们将在下一节中探讨。
你知道 pod IP 地址在整个集群中必须是唯一的,因此节点之间的桥接器必须使用不重叠的地址范围,以防止不同节点上的 pod 获得相同的 IP。在图 11.16 的示例中,节点 A 上的桥接器使用 10.1.1.0/24 IP 范围,而节点 B 上的桥接器使用 10.1.2.0/24,这确保了不存在 IP 地址冲突。
图 11.16。为了使不同节点上的 pod 能够通信,桥接器需要以某种方式连接起来。

图 11.16 显示,为了通过普通的第三层网络在两个节点之间的 pod 之间启用通信,节点的物理网络接口需要连接到桥接器。节点 A 上的路由表需要配置,以便所有目标为 10.1.2.0/24 的数据包被路由到节点 B,而节点 B 的路由表需要配置,以便发送到 10.1.1.0/24 的数据包被路由到节点 A。
在这种配置下,当一个节点上的容器向另一个节点上的容器发送数据包时,数据包首先通过 veth 对,然后通过桥接到节点的物理适配器,接着通过电线到达另一个节点的物理适配器,通过另一个节点的桥接器,最后通过目标容器的 veth 对。
这只在节点连接到相同的网络交换机时才有效,中间没有路由器;否则,那些路由器会丢弃数据包,因为它们引用的是 Pod IP,这些 IP 是私有的。当然,中间的路由器可以被配置为在节点之间路由数据包,但随着节点之间路由器数量的增加,这变得越来越困难且容易出错。因此,使用软件定义网络(SDN)更容易,它使得节点看起来就像它们连接到相同的网络交换机一样,无论实际的底层网络拓扑多么复杂。从 Pod 发送的数据包被封装并通过网络发送到运行另一个 Pod 的节点,在那里它们被解封装并以原始形式交付给 Pod。
11.4.3. 介绍容器网络接口
为了更容易地将容器连接到网络,启动了一个名为容器网络接口(CNI)的项目。CNI 允许 Kubernetes 配置使用任何现有的 CNI 插件。这些插件包括
-
Calico
-
Flannel
-
Romana
-
Weave Net
-
以及其他
我们不会深入探讨这些插件的细节;如果你想要了解更多关于它们的信息,请参考kubernetes.io/docs/concepts/cluster-administration/addons/。
安装网络插件并不困难。你只需要部署一个包含 DaemonSet 和其他一些支持资源的 YAML 文件。这个 YAML 文件在每个插件的项目页面上提供。正如你可以想象的那样,DaemonSet 用于在所有集群节点上部署网络代理。然后它连接到节点的 CNI 接口,但请注意,Kubelet 需要以--network-plugin=cni的方式启动才能使用 CNI。
11.5. 服务的实现方式
在第五章中,你学习了关于服务的内容,服务允许在持久稳定 IP 地址和端口上暴露一组 Pod。为了专注于服务的目的以及它们的使用方式,我们故意没有深入探讨它们的工作原理。但为了真正理解服务,并在事情没有按预期进行时更好地了解查找方向,你需要了解它们是如何实现的。
11.5.1. 介绍 kube-proxy
与服务相关的一切都由每个节点上运行的 kube-proxy 进程处理。最初,kube-proxy 是一个实际的代理,等待连接,并为每个传入连接打开到 Pod 的一个新连接。这被称为userspace代理模式。后来,性能更好的iptables代理模式取代了它。现在这是默认模式,但如果你愿意,仍然可以配置 Kubernetes 使用旧模式。
在我们继续之前,让我们快速回顾一下关于服务的一些内容,这些内容对于理解接下来的几段很重要。
我们了解到每个服务都拥有自己的稳定 IP 地址和端口。客户端(通常是 Pod)通过连接到这个 IP 地址和端口来使用服务。这个 IP 地址是虚拟的——它没有分配给任何网络接口,并且在包离开节点时,该 IP 地址永远不会被列为网络包的源 IP 地址或目标 IP 地址。服务的一个关键细节是,它们由一个 IP 和端口号对(或多个 IP 和端口号对,在多端口服务的情况下)组成,因此服务 IP 本身并不代表任何东西。这就是为什么你不能 ping 它们的原因。
11.5.2. kube-proxy 如何使用 iptables
当在 API 服务器中创建服务时,虚拟 IP 地址会立即分配给它。随后不久,API 服务器通知所有在工作节点上运行的 kube-proxy 代理,已创建了一个新的服务。然后,每个 kube-proxy 在其运行的节点上使该服务地址可访问。它是通过设置一些iptables规则来做到这一点的,这些规则确保每个目标为服务 IP/端口号对的包被拦截,并且其目标地址被修改,因此包被重定向到支持该服务的某个 Pod。
除了监视 API 服务器中服务的更改外,kube-proxy 还监视端点对象的更改。我们在第五章(index_split_046.html#filepos469093)中讨论了它们,但让我来刷新一下你的记忆,因为很容易忘记它们的存在,因为你很少手动创建它们。端点对象包含所有支持服务的 Pod 的 IP/端口号对(IP/端口号对也可以指向除 Pod 之外的东西)。这就是为什么 kube-proxy 也必须监视所有端点对象。毕竟,每当创建或删除一个新的后端 Pod,或者 Pod 的就绪状态改变,或者 Pod 的标签改变,并且它进入或退出服务的范围时,端点对象都会发生变化。
现在,让我们看看 kube-proxy 是如何使客户端能够通过服务连接到那些 Pod 的。这如图 11.17 所示。
图 11.17. 发送到服务虚拟 IP/端口号对的网络包被修改并重定向到随机选择的后端 Pod。

图表显示了kube-proxy做了什么以及客户端 Pod 发送的包如何到达支持服务的某个 Pod。让我们检查当客户端 Pod(图中的 Pod A)发送该包时发生了什么。
包的初始目标设置为服务的 IP 和端口(在示例中,服务位于 172.30.0.1:80)。在发送到网络之前,该包首先由节点 A 的内核根据节点上设置的iptables规则进行处理。
内核检查该包是否匹配那些iptables规则之一。其中之一规定,如果任何包的目标 IP 等于 172.30.0.1 且目标端口等于 80,则该包的目标 IP 和端口应替换为随机选择的 Pod 的 IP 和端口。
示例中的数据包符合该规则,因此其目标 IP/端口被更改。在示例中,Pod B2 被随机选择,因此数据包的目标 IP 被更改为 10.1.2.1(Pod B2 的 IP),端口更改为 8080(在 Service 规范中指定的目标端口)。从现在开始,这就像客户端 Pod 直接将数据包发送到 Pod 而不是通过服务一样。
这比那稍微复杂一点,但这是你需要理解的最重要部分。
11.6. 运行高度可用的集群
在 Kubernetes 中运行应用程序的原因之一是确保它们在没有中断的情况下运行,在基础设施故障的情况下没有或有限的手动干预。为了无中断地运行服务,不仅应用程序需要始终处于运行状态,Kubernetes 控制平面组件也需要始终处于运行状态。我们将探讨实现高可用性所涉及的内容。
11.6.1. 使你的应用程序高度可用
在 Kubernetes 中运行应用程序时,各种控制器确保即使在节点失败的情况下,应用程序也能平稳运行并达到指定的规模。为了确保应用程序高度可用,你只需要通过部署资源运行它们并配置适当数量的副本;其他所有事情都由 Kubernetes 处理。
运行多个实例以降低中断的可能性
这要求你的应用程序具有水平扩展性,即使你的应用程序不具备这种特性,你也应该使用一个副本计数设置为 1 的部署。如果副本变得不可用,它将很快被一个新的副本替换,尽管这并不是瞬间发生的。所有涉及的控制器都需要时间来注意到一个节点已经失败,创建新的 Pod 副本,并启动 Pod 的容器。在这之间不可避免地会有一个短暂的中断期。
为非水平扩展应用程序使用领导者选举
为了避免中断,你需要运行额外的非活动副本与活动副本一起,并使用快速响应的租约或领导者选举机制来确保只有一个处于活动状态。如果你不熟悉领导者选举,它是一种在分布式环境中运行多个应用程序实例的方式,以便就哪个是领导者达成一致。这个领导者要么是唯一执行任务的实例,而其他所有实例都在等待领导者失败然后自己成为领导者,或者它们都可以是活动的,其中领导者是唯一执行写入操作的实例,而其他所有实例则提供对数据的只读访问,例如。这确保了两个实例永远不会执行相同的工作,如果那样做会导致由于竞争条件而出现不可预测的系统行为。
该机制不需要集成到应用程序本身中。你可以使用一个侧边容器,该容器执行所有领导者选举操作,并在应该变为活动状态时向主容器发出信号。你可以在 Kubernetes 中找到领导者选举的示例,见github.com/kubernetes/contrib/tree/master/election。
确保你的应用高度可用相对简单,因为 Kubernetes 几乎处理了所有事情。但如果是 Kubernetes 本身失败了怎么办?如果运行 Kubernetes 控制平面组件的服务器宕机了怎么办?这些组件是如何实现高度可用的?
11.6.2. 使 Kubernetes 控制平面组件高度可用
在本章的开头,你学习了组成 Kubernetes 控制平面的少数几个组件。为了使 Kubernetes 高度可用,你需要运行多个主节点,这些节点运行以下组件的多个实例:
-
etcd,这是所有 API 对象存储的分布式数据存储
-
API 服务器
-
控制管理器,这是所有控制器运行的进程
-
调度器
不深入探讨如何安装和运行这些组件的具体细节,让我们看看使每个组件高度可用所涉及的内容。图 11.18 展示了一个高度可用的集群概览。
图 11.18. 具有三个主节点的高度可用集群

运行一个 etcd 集群
由于 etcd 被设计为一个分布式系统,其关键特性之一就是能够运行多个 etcd 实例,因此使其高度可用并不是什么大问题。你所需要做的就是在一个适当数量的机器上运行它(如本章前面所述的三台、五台或七台)并使它们相互了解。你可以通过在每个实例的配置中包含所有其他实例的列表来实现这一点。例如,在启动一个实例时,你指定其他 etcd 实例可以访问的 IP 地址和端口号。
etcd 将在所有实例之间复制数据,因此当三机集群运行时,一个节点的故障仍然允许集群接受读写操作。为了提高超过单个节点的容错能力,你需要运行五个或七个 etcd 节点,这分别允许集群处理两个或三个节点故障。拥有超过七个 etcd 实例几乎是不必要的,并且开始影响性能。
运行多个 API 服务器实例
使 API 服务器高可用性甚至更简单。因为 API 服务器(几乎完全)是无状态的(所有数据都存储在 etcd 中,但 API 服务器会缓存它),你可以运行你需要的任何数量的 API 服务器,它们根本不需要相互了解。通常,每个 API 服务器都与每个 etcd 实例一起运行。通过这样做,etcd 实例不需要在它们前面有任何类型的负载均衡器,因为每个 API 服务器实例只与本地 etcd 实例通信。
与 API 服务器不同,API 服务器确实需要由负载均衡器进行前端处理,因此客户端(kubectl,但也包括控制器管理器、调度器和所有 Kubelets)始终只连接到健康的 API 服务器实例。
确保控制器和调度器的高可用性
与可以同时运行多个副本的 API 服务器相比,运行多个控制器管理器或调度器的实例并不简单。因为控制器和调度器都积极监视集群状态,并在状态发生变化时采取行动,可能会进一步修改集群状态(例如,当 ReplicaSet 期望的副本数增加一个时,ReplicaSet 控制器会创建一个额外的 Pod),运行这些组件的多个实例会导致它们都执行相同的操作。它们会相互竞争,这可能会导致不期望的效果(如前例中提到的,创建两个而不是一个新 Pod)。
因此,当运行这些组件的多个实例时,在任何给定时间只能有一个实例处于活动状态。幸运的是,所有这些都是由组件本身处理的(这是通过--leader-elect选项控制的,默认为 true)。每个单独的组件只有在它是选定的领导者时才会处于活动状态。只有领导者执行实际工作,而所有其他实例都处于待命状态,等待当前领导者失败。当它失败时,剩余的实例将选举一个新的领导者,然后接管工作。这种机制确保两个组件永远不会同时操作并执行相同的工作(参见图 11.19)。
图 11.19。只有一个控制器管理器和单个调度器处于活动状态;其他都在待命。

控制器管理器和调度器可以与 API 服务器和 etcd 一起运行,或者它们可以运行在不同的机器上。当它们一起运行时,它们可以直接与本地 API 服务器通信;否则,它们通过负载均衡器连接到 API 服务器。
理解在控制平面组件中使用的领导者选举机制
我觉得这里最有趣的是,这些组件不需要直接相互通信来选举领导者。领导者选举机制完全通过在 API 服务器中创建资源来实现。这甚至不是一种特殊类型的资源——端点资源被用来实现这一点(滥用可能是一个更合适的词)。
使用端点对象(Endpoints object)来完成这个操作并没有什么特别之处。之所以使用它,是因为只要没有同名的服务存在,它就不会产生副作用。任何其他资源都可以使用(实际上,领导选举机制很快将使用 ConfigMaps 而不是端点)。
我相信你对如何使用资源来完成这个目的很感兴趣。以调度器(Scheduler)为例。所有调度器的实例都试图创建(稍后更新)一个名为kube-scheduler的端点资源。你可以在kube-system命名空间中找到它,如下所示。
列表 11.11. 用于领导选举的kube-scheduler端点资源
$ kubectl get endpoints kube-scheduler -n kube-system -o yaml apiVersion: v1 kind: Endpoints metadata: annotations: control-plane.alpha.kubernetes.io/leader: '{"holderIdentity":
"minikube","leaseDurationSeconds":15,"acquireTime":
"2017-05-27T18:54:53Z","renewTime":"2017-05-28T13:07:49Z",
"leaderTransitions":0}' creationTimestamp: 2017-05-27T18:54:53Z name: kube-scheduler namespace: kube-system resourceVersion: "654059" selfLink: /api/v1/namespaces/kube-system/endpoints/kube-scheduler uid: f847bd14-430d-11e7-9720-080027f8fa4e subsets: []
control-plane.alpha.kubernetes.io/leader注解是重要的部分。正如你所见,它包含一个名为holderIdentity的字段,该字段包含当前领导者的名称。第一个成功将其名称放入那里的实例将成为领导者。实例们相互竞争以完成这项任务,但总是只有一个赢家。
记得我们之前解释的乐观并发吗?那就是确保如果多个实例尝试将它们的名称写入资源,只有其中一个能够成功。根据写入是否成功,每个实例都知道自己是否是领导者。
一旦成为领导者,它必须定期更新资源(默认情况下每两秒更新一次),这样所有其他实例都知道它仍然活跃。当领导者失败时,其他实例会看到资源已经有一段时间没有更新了,并尝试通过将自己的名称写入资源来成为领导者。简单,对吧?
11.7. 摘要
希望这章内容有趣,并且有助于提高你对 Kubernetes 内部运作的了解。本章展示了
-
Kubernetes 集群由哪些组件组成以及每个组件负责什么
-
API 服务器、调度器、在控制器管理器中运行的各个控制器以及 Kubelet 是如何协同工作,使 Pod 得以启动
-
基础设施容器是如何将一个 Pod 中的所有容器绑定在一起的
-
Pod 是如何通过网络桥接与其他节点上运行的 Pod 进行通信的,以及这些不同节点上的桥接是如何连接的,这样不同节点上的 Pod 就可以互相通信
-
kube-proxy 如何通过在节点上配置
iptables规则,在同一个服务中的 Pod 之间执行负载均衡 -
如何运行每个控制平面组件的多个实例以使集群具有高可用性
接下来,我们将探讨如何保护 API 服务器,以及如何通过扩展保护整个集群。
第十二章。保护 Kubernetes API 服务器
本章涵盖
-
理解认证
-
服务账户是什么以及为什么使用它们
-
理解基于角色的访问控制(RBAC)插件
-
使用 Roles 和 RoleBindings
-
使用 ClusterRoles 和 ClusterRoleBindings
-
理解默认角色和绑定
在第八章中,你学习了运行在 Pod 中的应用程序如何与 API 服务器通信以检索或更改集群中部署的资源的状态。为了与 API 服务器进行认证,你使用了挂载到 Pod 中的服务账户令牌。在本章中,你将了解服务账户是什么以及如何配置它们的权限,以及如何配置集群中其他主体的权限。
12.1. 理解认证
在上一章中,我们提到 API 服务器可以配置一个或多个认证插件(同样适用于授权插件)。当 API 服务器收到请求时,它会通过认证插件列表,这样它们可以分别检查请求并尝试确定谁发送了请求。第一个能够从请求中提取该信息的插件将用户名、用户 ID 和客户端所属的组返回给 API 服务器核心。API 服务器停止调用剩余的认证插件并继续到授权阶段。
可用几种认证插件。它们使用以下方法获取客户端的身份:
-
从客户端证书
-
从 HTTP 头中传递的认证令牌
-
基本 HTTP 认证
-
其他
认证插件通过启动 API 服务器时的命令行选项启用。
12.1.1. 用户和组
认证插件返回已认证用户的用户名和组。Kubernetes 不会在任何地方存储该信息;它使用这些信息来验证用户是否有权执行操作。
理解用户
Kubernetes 区分了两种连接到 API 服务器的客户端:
-
实际的人类(用户)
-
Pods(更具体地说,运行在其内部的应用程序)
这两种类型的客户端都使用前面提到的认证插件进行认证。用户应由外部系统管理,例如单点登录(SSO)系统,但 Pod 使用一种称为服务账户的机制,这些服务账户作为 ServiceAccount 资源在集群中创建和存储。相比之下,没有资源代表用户账户,这意味着您无法通过 API 服务器创建、更新或删除用户。
我们不会详细介绍如何管理用户,但我们将详细探讨服务账户,因为它们对于运行 Pod 至关重要。有关如何配置集群以认证人类用户的更多信息,集群管理员应参考 Kubernetes 集群管理员指南kubernetes.io/docs/admin。
理解组
人类用户和 ServiceAccounts 都可以属于一个或多个组。我们提到身份验证插件会返回与用户名和用户 ID 一起的组。组用于一次性授予多个用户的权限,而不是必须为每个用户单独授予。
插件返回的组仅仅是字符串,代表任意组名,但内置组有特殊含义:
-
system:unauthenticated组用于那些没有任何身份验证插件能够验证客户端的请求。 -
system:authenticated组会自动分配给成功认证的用户。 -
system:serviceaccounts组包含了系统中的所有 ServiceAccounts。 -
system:serviceaccounts:<namespace>包含了特定命名空间中的所有 ServiceAccounts。
12.1.2. 介绍 ServiceAccounts
让我们近距离探索 ServiceAccounts。你已经了解到 API 服务器要求客户端在允许它们在服务器上执行操作之前进行身份验证。你已经看到 pod 可以通过发送文件 /var/run/secrets/kubernetes.io/serviceaccount/token 的内容来进行身份验证,该文件通过 secret 卷挂载到每个容器的文件系统中。
但这个文件究竟代表什么呢?每个 pod 都关联着一个 Service-Account,它代表了 pod 中运行的应用程序的标识。令牌文件包含了 ServiceAccount 的身份验证令牌。当应用程序使用此令牌连接到 API 服务器时,身份验证插件会验证 ServiceAccount 并将 ServiceAccount 的用户名返回给 API 服务器核心。Service-Account 用户名格式如下:
system:serviceaccount:<namespace>:<service account name>
API 服务器将此用户名传递给配置的身份验证插件,这些插件确定应用程序尝试执行的操作是否允许由 ServiceAccount 执行。
ServiceAccounts 只是一种方式,让运行在 pod 内部的应用程序通过 API 服务器进行身份验证。正如之前提到的,应用程序通过在请求中传递 ServiceAccount 的令牌来实现这一点。
理解 ServiceAccount 资源
ServiceAccounts 和 Pods、Secrets、ConfigMaps 等一样,是资源,并且范围限定在单个命名空间内。每个命名空间都会自动创建一个默认的 ServiceAccount(这就是你的 pod 一直使用的那个)。
你可以像列出其他资源一样列出 ServiceAccounts:
$ kubectl get sa NAME SECRETS AGE default 1 1d
注意
serviceaccount 的简写是 sa。
如你所见,当前命名空间只包含default ServiceAccount。在需要时可以添加额外的 ServiceAccounts。每个 Pod 与一个 ServiceAccount 关联,但多个 Pod 可以使用同一个 ServiceAccount。如图 12.1 所示,Pod 只能使用同一命名空间下的 ServiceAccount。
图 12.1. 每个 Pod 与其命名空间中的单个 ServiceAccount 关联。

理解 ServiceAccount 如何与授权相关联
你可以通过在 Pod 清单中指定账户的名称来将 ServiceAccount 分配给 Pod。如果你没有明确指定,Pod 将使用命名空间中的默认 ServiceAccount。
通过将不同的 ServiceAccount 分配给 Pod,你可以控制每个 Pod 可以访问哪些资源。当 API 服务器接收到携带认证令牌的请求时,服务器使用令牌来验证发送请求的客户端,然后确定相关的 ServiceAccount 是否被允许执行请求的操作。API 服务器从集群管理员配置的全局授权插件中获取这些信息。可用的授权插件之一是基于角色的访问控制(RBAC)插件,这将在本章后面讨论。从 Kubernetes 版本 1.6 开始,RBAC 插件是大多数集群应该使用的插件。
12.1.3. 创建 ServiceAccount
我们已经说过每个命名空间都包含其自己的默认 ServiceAccount,但如果需要,还可以创建额外的。但为什么你应该费心去创建 ServiceAccount 而不是使用所有 Pod 的默认 ServiceAccount 呢?
显而易见的原因是集群安全。不需要读取任何集群元数据的 Pod 应该在限制账户下运行,该账户不允许它们检索或修改集群中部署的任何资源。需要检索资源元数据的 Pod 应该在只允许读取这些对象元数据的 ServiceAccount 下运行,而需要修改这些对象的 Pod 应该在允许修改 API 对象的自己的 ServiceAccount 下运行。
让我们看看如何创建额外的 ServiceAccounts,它们如何与 Secrets 相关联,以及如何将它们分配给你的 Pod。
创建 ServiceAccount
创建 ServiceAccount 非常简单,多亏了专门的kubectl create serviceaccount命令。让我们创建一个名为foo的新 ServiceAccount:
$ kubectl create serviceaccount foo serviceaccount "foo" created
现在,你可以使用describe命令检查 ServiceAccount,如下所示。
列表 12.1. 使用kubectl describe检查 ServiceAccount
$ kubectl describe sa foo Name: foo Namespace: default Labels: <none> Image pull secrets: <none> 1 Mountable secrets: foo-token-qzq7j 2 Tokens: foo-token-qzq7j 3
-
1 这些将被自动添加到所有使用此 ServiceAccount 的 pod 中。
-
使用此 ServiceAccount 的 2 个 Pod 只能挂载这些密钥,如果强制执行可挂载密钥。
-
3 个身份验证令牌。第一个被挂载在容器内部。
你可以看到已经创建了一个自定义令牌密钥,并将其与 ServiceAccount 关联。如果你使用 kubectl describe secret foo-token-qzq7j 查看密钥的数据,你会看到它包含与默认 ServiceAccount 令牌相同的项(CA 证书、命名空间和令牌),如以下列表所示。
列表 12.2. 检查自定义 ServiceAccount 的密钥
$ kubectl describe secret foo-token-qzq7j ... ca.crt: 1066 bytes namespace: 7 bytes token: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
注意
你可能听说过 JSON Web Tokens(JWT)。ServiceAccounts 中使用的身份验证令牌是 JWT 令牌。
理解 ServiceAccount 的可挂载密钥
当你使用 kubectl describe 检查 ServiceAccount 时,令牌会显示在“可挂载密钥”列表中。让我解释一下这个列表代表什么。在第七章中,你学习了如何创建密钥并在 pod 内部挂载它们。默认情况下,pod 可以挂载它想要的任何密钥。但是,pod 的 ServiceAccount 可以配置为只允许 pod 挂载 ServiceAccount 上列出的可挂载密钥。要启用此功能,ServiceAccount 必须包含以下注解:kubernetes.io/enforce-mountable-secrets="true"。
如果 ServiceAccount 被注解了此注解,任何使用它的 pod 只能挂载 ServiceAccount 的可挂载密钥——它们不能使用任何其他密钥。
理解 ServiceAccount 的镜像拉取密钥
ServiceAccount 还可以包含一个镜像拉取密钥列表,我们在第七章中进行了探讨。如果你不记得,它们是包含从私有镜像仓库拉取容器镜像凭证的密钥。
以下列表显示了一个 ServiceAccount 定义示例,其中包含你在第七章中创建的镜像拉取密钥。
列表 12.3. 带有镜像拉取密钥的 ServiceAccount:sa-image-pull-secrets.yaml
apiVersion: v1 kind: ServiceAccount metadata: name: my-service-account imagePullSecrets: - name: my-dockerhub-secret
ServiceAccount 的镜像拉取密钥的行为与其可挂载密钥略有不同。与可挂载密钥不同,它们不决定 pod 可以使用哪些镜像拉取密钥,而是自动添加到所有使用 Service-Account 的 pod 中。将镜像拉取密钥添加到 ServiceAccount 中可以节省你逐个添加到每个 pod 中的时间。
12.1.4. 将 ServiceAccount 分配给 pod
在创建额外的 ServiceAccounts 后,你需要将它们分配给 pod。这是通过在 pod 定义中的 spec.serviceAccountName 字段中设置 ServiceAccount 的名称来完成的。
注意
创建 pod 时必须设置 pod 的 ServiceAccount。之后不能更改。
创建使用自定义 ServiceAccount 的 pod
在 第八章 中,你部署了一个 pod,该 pod 运行基于 tutum/curl 镜像的容器以及旁边的代理容器。你用它来探索 API 服务器的 REST 接口。代理容器运行了 kubectl proxy 进程,该进程使用 pod 的 ServiceAccount 的令牌与 API 服务器进行身份验证。
现在,你可以修改 pod,使其使用你几分钟前创建的 foo ServiceAccount。下一个列表显示了 pod 定义。
列表 12.4. 使用非默认 ServiceAccount 的 pod:curl-custom-sa.yaml
apiVersion: v1 kind: Pod metadata: name: curl-custom-sa spec: serviceAccountName: foo 1 containers: - name: main image: tutum/curl command: ["sleep", "9999999"] - name: ambassador image: luksa/kubectl-proxy:1.6.2
- 1 此 pod 使用 foo ServiceAccount 而不是默认的。
为了确认自定义 ServiceAccount 的令牌已挂载到两个容器中,你可以打印出令牌的内容,如下所示。
列表 12.5. 检查挂载到 pod 容器中的令牌
$ kubectl exec -it curl-custom-sa -c main
cat /var/run/secrets/kubernetes.io/serviceaccount/token eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
你可以通过比较 列表 12.5 中的令牌字符串与 列表 12.2 中的令牌字符串来看到令牌来自 foo ServiceAccount。
使用自定义 ServiceAccount 的令牌与 API 服务器通信
让我们看看你是否可以使用这个令牌与 API 服务器通信。如前所述,代理容器在与服务器通信时使用令牌,因此你可以通过代理容器进行测试,该代理容器监听 localhost:8001,如下所示。
列表 12.6. 使用自定义 ServiceAccount 与 API 服务器通信
$ kubectl exec -it curl-custom-sa -c main curl localhost:8001/api/v1/pods``{ "kind": "PodList", "apiVersion": "v1", "metadata": { "selfLink": "/api/v1/pods", "resourceVersion": "433895" }, "items": ...
好的,你从服务器得到了一个正确的响应,这意味着自定义 Service-Account 被允许列出 pod。这可能是因为你的集群没有使用 RBAC 授权插件,或者你按照 [第八章 中的说明赋予了所有 ServiceAccounts 完全权限。
当你的集群没有使用适当的授权时,创建和使用额外的 ServiceAccounts 并没有太多意义,因为默认的 ServiceAccount 被允许做任何事情。在这种情况下使用 ServiceAccounts 的唯一原因是为了强制挂载 Secrets 或通过 Service-Account 提供图像拉取 Secrets,正如之前所解释的。
但在使用 RBAC 授权插件时,创建额外的 ServiceAccounts 实际上几乎是必须的,我们将在下一节中探讨这一点。
12.2. 集群基于角色的访问控制
从 Kubernetes 版本 1.6.0 开始,集群安全性得到了显著提升。在早期版本中,如果你设法从某个 Pod 获取了认证令牌,你就可以用它来在集群中做任何你想做的事情。如果你在网上搜索,你会找到一些演示,展示了路径遍历(或目录遍历)攻击(客户端可以检索位于 Web 服务器 Web 根目录之外的文件)是如何被用来获取令牌,并使用它来在不受保护的 Kubernetes 集群中运行恶意 Pod 的。
但在版本 1.8.0 中,RBAC 授权插件升级为 GA(通用可用性),现在许多集群默认启用(例如,当使用 kubadm 部署集群时,如附录 B 中所述 appendix B)。RBAC 阻止未经授权的用户查看或修改集群状态。默认 Service-Account 不允许查看集群状态,更不用说以任何方式修改它了,除非你授予它额外的权限。要编写与 Kubernetes API 服务器通信的应用程序(如第八章中所述 chapter 8),你需要了解如何通过 RBAC 特定资源管理授权。
注意
除了 RBAC 之外,Kubernetes 还包括其他授权插件,例如基于属性的访问控制(ABAC)插件、Web 钩子插件和自定义插件实现。尽管如此,RBAC 仍然是标准。
12.2.1. 介绍 RBAC 授权插件
Kubernetes API 服务器可以被配置为使用授权插件来检查用户请求的操作是否被允许执行。因为 API 服务器公开了一个 REST 接口,用户通过向服务器发送 HTTP 请求来执行操作。用户通过在请求中包含凭证(认证令牌、用户名和密码或客户端证书)来自我认证。
理解操作
但有哪些操作呢?正如你所知,REST 客户端通过向特定 URL 路径发送GET、POST、PUT、DELETE和其他类型的 HTTP 请求来发送操作,这些路径代表特定的 REST 资源。在 Kubernetes 中,这些资源包括 Pods、Services、Secrets 等。以下是一些 Kubernetes 中操作示例:
-
获取 Pods
-
创建服务
-
更新 Secrets
-
等等
以下示例中的动词(get、create、update)映射到客户端(客户端执行的 HTTP 方法)执行的 HTTP 方法(GET、POST、PUT)。名词(Pods、Service、Secrets)显然映射到 Kubernetes 资源。
API 服务器内部运行的授权插件 RBAC 确定客户端是否被允许在请求的资源上执行请求的动词。
表 12.1. 将 HTTP 方法映射到授权动词
| HTTP 方法 | 单个资源的动词 | 集合的动词 |
|---|---|---|
| GET, HEAD | get(以及监视) | list(以及监视) |
| POST | create | n/a |
| PUT | update | n/a |
| PATCH | patch | n/a |
| DELETE | delete | deletecollection |
注意
额外的动词 use 用于 PodSecurityPolicy 资源,这些资源将在下一章中解释。
除了将安全权限应用于整个资源类型之外,RBAC 规则还可以应用于资源的特定实例(例如,名为 myservice 的服务)。稍后您将看到,权限还可以应用于非资源 URL 路径,因为并非 API 服务器公开的每个路径都映射到资源(例如,/api 路径本身或 /healthz 上的服务器健康信息)。
理解 RBAC 插件
如其名称所示,RBAC 授权插件使用用户角色作为确定用户是否可以执行操作的关键因素。一个主体(可能是人类、ServiceAccount 或用户或 ServiceAccount 的组)与一个或多个角色相关联,并且每个角色都允许在特定资源上执行某些动词。
如果用户拥有多个角色,他们可以执行任何他们的角色允许他们做的事情。如果用户的所有角色中都没有更新 Secrets 的权限,API 服务器将阻止用户对 Secrets 执行 PUT 或 PATCH 请求。
通过 RBAC 插件管理授权很简单。所有操作都是通过创建四个特定的 RBAC Kubernetes 资源来完成的,我们将在下一节中探讨这些资源。
12.2.2. 介绍 RBAC 资源
RBAC 授权规则通过四个资源进行配置,这些资源可以分为两组:
-
角色和 ClusterRoles,它们指定可以在哪些资源上执行哪些动词。
-
角色绑定和 ClusterRoleBinding,它们将上述角色绑定到特定的用户、组或 ServiceAccount。
角色定义可以做什么,而绑定定义谁可以做(这可以在 图 12.2 中看到)。
图 12.2. 角色授予权限,而角色绑定将角色绑定到主体。

角色与 ClusterRole,或角色绑定与 ClusterRoleBinding 之间的区别在于,角色和角色绑定是命名空间资源,而 ClusterRole 和 ClusterRoleBinding 是集群级别的资源(非命名空间)。这可以在 图 12.3 中看到。
图 12.3. 角色和 RoleBindings 是命名空间级别的;ClusterRoles 和 ClusterRoleBindings 不是。

如图中所示,一个命名空间中可以存在多个 RoleBindings(对于 Roles 也是如此)。同样,可以创建多个 ClusterRoleBindings 和 Cluster-Roles。图中还显示,尽管 RoleBindings 是命名空间级别的,但它们也可以引用 ClusterRoles,而 ClusterRoles 不是。
了解这四种资源及其影响的最佳方式是通过实际操作练习来尝试它们。你现在将这样做。
设置你的练习
在你能够探索 RBAC 资源如何影响通过 API 服务器可以执行的操作之前,你需要确保你的集群中启用了 RBAC。首先,确保你至少使用 Kubernetes 的 1.6 版本,并且 RBAC 插件是唯一配置的授权插件。可以并行启用多个插件,如果其中之一允许执行某个操作,则该操作将被允许。
注意
如果你使用的是 GKE 1.6 或 1.7,你需要通过使用 --no-enable-legacy-authorization 选项创建集群来显式禁用旧版授权。如果你使用的是 Minikube,你也可能需要通过使用 --extra-config=apiserver.Authorization.Mode=RBAC 选项启动 Minikube 来启用 RBAC。
如果你按照 第八章 中的说明禁用了 RBAC,现在是时候通过运行以下命令重新启用它:
$ kubectl delete clusterrolebinding permissive-binding
为了尝试 RBAC,你将运行一个 pod,通过这个 pod 你将尝试与 API 服务器通信,就像你在 第八章 中做的那样。但这次你将在不同的命名空间中运行两个 pod,以查看每个命名空间的网络安全行为。
在 第八章 中的示例中,你运行了两个容器来演示一个容器中的应用程序如何使用另一个容器与 API 服务器通信。这次,你将运行一个单独的容器(基于 kubectl-proxy 镜像),并使用 kubectl exec 在该容器内部直接运行 curl。代理将负责身份验证和 HTTPS,这样你就可以专注于 API 服务器安全性的授权方面。
创建命名空间并运行 pod
你将要在命名空间 foo 中创建一个 pod,在命名空间 bar 中创建另一个 pod,如下面的列表所示。
列表 12.7. 在不同命名空间中运行测试 pod
$ kubectl create ns foo namespace "foo" created $ kubectl run test --image=luksa/kubectl-proxy -n foo deployment "test" created $ kubectl create ns bar namespace "bar" created $ kubectl run test --image=luksa/kubectl-proxy -n bar deployment "test" created
现在打开两个终端,并使用 kubectl exec 在两个 pod 中运行一个 shell(每个终端一个 pod)。例如,要在命名空间 foo 中的 pod 中运行 shell,首先获取 pod 的名称:
$ kubectl get po -n foo NAME READY STATUS RESTARTS AGE test-145485760-ttq36`` 1/1 Running 0 1m
然后使用kubectl exec命令中的名称:
$ kubectl exec -it test-145485760-ttq36 -n foo sh / #
在另一个终端中执行相同的操作,但针对bar命名空间中的 Pod。
从您的 Pod 中列出服务
要验证 RBAC 是否启用并阻止 Pod 读取集群状态,请使用curl列出foo命名空间中的服务:
/ # curl localhost:8001/api/v1/namespaces/foo/services 用户"system:serviceaccount:foo:default"无法在命名空间"foo"中列出服务。
您正在连接到localhost:8001,这是kubectl proxy进程监听的位置(如第八章所述)。进程收到您的请求,并将其发送到 API 服务器,同时以foo命名空间中的默认 ServiceAccount 进行身份验证(如 API 服务器的响应所示)。
API 服务器响应称,ServiceAccount 不允许在foo命名空间中列出服务,即使 Pod 在该命名空间中运行。您正在看到 RBAC(基于角色的访问控制)的实际应用。ServiceAccount 的默认权限不允许其列出或修改任何资源。现在,让我们学习如何允许 ServiceAccount 执行这些操作。首先,您需要创建一个角色资源。
12.2.3. 使用角色和角色绑定
角色资源定义了可以在哪些资源上执行哪些操作(或者,如前所述,可以在哪些 RESTful 资源上执行哪些类型的 HTTP 请求)。以下列表定义了一个角色,允许用户在foo命名空间中get和list服务。
列表 12.8. 角色定义:service-reader.yaml
apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: namespace: foo 1 name: service-reader rules: - apiGroups: [""] 2 verbs: ["get", "list"] 3 resources: ["services"] 4
-
1 角色是命名空间级别的(如果省略命名空间,则使用当前命名空间)。
-
2 服务是核心 apiGroup 中的资源,该 apiGroup 没有名称,因此为“”。
-
3 允许通过名称获取单个服务并列出所有服务。
-
4 此规则适用于服务(必须使用复数形式!)。
警告
指定资源时必须使用复数形式。
此角色资源将在foo命名空间中创建。在第八章中,您学习了每种资源类型都属于一个 API 组,您在资源的清单中指定该 API 组(以及版本)字段。在角色定义中,您需要指定定义中包含的每个规则中列出的资源的apiGroup。如果您允许访问属于不同 API 组的资源,则使用多个规则。
注意
在示例中,您允许访问所有服务资源,但您也可以通过指定额外的resourceNames字段来仅限制对特定服务实例的访问。
图 12.4 显示了角色、其动词和资源以及它将被创建的命名空间。
图 12.4. service-reader角色允许在 foo 命名空间中获取和列出服务。

创建角色
现在在foo命名空间中创建前面的角色:
$ kubectl create -f service-reader.yaml -n foo role "service-reader" created
注意
-n选项是--namespace的缩写。
注意,如果您正在使用 GKE,前面的命令可能会失败,因为您没有集群管理员权限。要授予权限,请运行以下命令:
$ kubectl create clusterrolebinding cluster-admin-binding
--clusterrole=cluster-admin --user=your.email@address.com
除了从 YAML 文件创建service-reader角色外,您还可以使用特殊的kubectl create role命令创建它。让我们使用这种方法在bar命名空间中创建角色:
$ kubectl create role service-reader --verb=get --verb=list
--resource=services -n bar role "service-reader" created
这两个角色将允许您从您的两个 Pod 中列出foo和bar命名空间中的服务(分别在foo和bar命名空间中运行)。但是创建两个角色还不够(您可以通过再次执行curl命令来检查)。您需要将每个角色绑定到它们各自命名空间中的服务账户。
将角色绑定到服务账户
角色定义了可以执行哪些操作,但它没有指定谁可以执行它们。为了做到这一点,您必须将角色绑定到主体,该主体可以是用户、服务账户或组(用户或服务账户的组)。
通过创建 RoleBinding 资源将角色绑定到主体。要将角色绑定到default服务账户,请运行以下命令:
$ kubectl create rolebinding test --role=service-reader
--serviceaccount=foo:default -n foo rolebinding "test" created
命令应该是自解释的。您正在创建一个 RoleBinding,它将service-reader角色绑定到foo命名空间中的default服务账户。您正在foo命名空间中创建 RoleBinding。RoleBinding 以及引用的服务账户和角色在图 12.5 中显示。
图 12.5. 测试 RoleBinding 将default ServiceAccount与service-reader角色绑定。

注意
要将角色绑定到用户而不是服务账户,请使用--user参数指定用户名。要将它绑定到组,请使用--group。
下面的列表显示了您创建的 RoleBinding 的 YAML。
列表 12.9. 引用角色的 RoleBinding
$ kubectl get rolebinding test -n foo -o yaml
-
1 此 RoleBinding 引用了 service-reader Role。
-
2 并将其绑定到 foo 命名空间中的默认 ServiceAccount。
如您所见,RoleBinding 始终引用单个 Role(从roleRef属性中可以明显看出),但可以将 Role 绑定到多个subjects(例如,一个或多个 Service-Accounts 以及任意数量的用户或组)。因为这个 RoleBinding 将 Role 绑定到了在foo命名空间中运行的 pod 的 ServiceAccount,所以您现在可以从该 pod 内部列出 Services。
列表 12.10. 从 API 服务器获取 Services
/ # curl localhost:8001/api/v1/namespaces/foo/services
- 1 项目列表为空,因为没有 Services 存在。
在 RoleBinding 中包含来自其他命名空间的 ServiceAccounts
命名空间bar中的 pod 无法列出其自身的 Services,显然也无法列出foo命名空间中的 Services。但您可以在foo命名空间中编辑您的 RoleBinding 并添加另一个 pod 的 ServiceAccount,即使它位于不同的命名空间中。运行以下命令:
$ kubectl edit rolebinding test -n foo
然后将以下行添加到subjects列表中,如下所示。
列表 12.11. 从另一个命名空间引用 ServiceAccount
subjects: - kind: ServiceAccount name: default
- 1 您正在引用 bar 命名空间中的默认 ServiceAccount。
现在,您也可以从运行在 bar 命名空间中的 pod 内列出foo命名空间中的 Services。运行与列表 12.10 中相同的命令,但在另一个终端中运行,在那里您在另一个 pod 中运行 shell。
在继续到 ClusterRoles 和 ClusterRoleBindings 之前,让我们总结一下您目前拥有的 RBAC 资源。您在foo命名空间中有一个 RoleBinding,它引用了service-reader Role(也在foo命名空间中),并将foo和bar命名空间中的default ServiceAccounts 绑定,如图 12.6 所示。
图 12.6. 将不同命名空间中的 ServiceAccounts 绑定到同一 Role。

12.2.4. 使用 ClusterRoles 和 ClusterRoleBindings
Roles 和 RoleBindings 是命名空间资源,这意味着它们位于并应用于单个命名空间中的资源,但,正如我们所见,RoleBindings 也可以引用其他命名空间中的 Service-Accounts。
除了这些命名空间资源外,还存在两个集群级别的 RBAC 资源:集群角色和集群角色绑定。它们不是命名空间化的。让我们看看为什么你需要它们。
常规角色仅允许访问与角色所在同一命名空间中的资源。如果您想允许某人访问不同命名空间中的资源,您必须在每个命名空间中创建一个角色和角色绑定。如果您想扩展到所有命名空间(这可能是一个集群管理员可能需要的),您需要在每个命名空间中创建相同的角色和角色绑定。在创建附加命名空间时,您必须记得在那里创建这两个资源。
正如您在本书中学到的,某些资源根本不是命名空间化的(这包括节点、持久卷、命名空间等)。我们还提到 API 服务器公开了一些不表示资源的 URL 路径(例如/healthz)。常规角色无法授予对这些资源或非资源 URL 的访问权限,但集群角色可以。
集群角色是一个集群级别的资源,用于允许访问非命名空间资源或非资源 URL,或用作在单个命名空间内绑定的通用角色,从而节省您在每个命名空间中重新定义相同角色的麻烦。
允许访问集群级别资源
如前所述,集群角色可以用来允许访问集群级别的资源。让我们看看如何允许您的 Pod 在您的集群中列出持久卷。首先,您将创建一个名为pv-reader的集群角色:
$ kubectl create clusterrole pv-reader --verb=get,list
--resource=persistentvolumes 集群角色 "pv-reader" 已创建
集群角色的 YAML 配置如下所示。
列表 12.12. 集群角色定义
$ kubectl get clusterrole pv-reader -o yaml apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: 1 name: pv-reader 1 resourceVersion: "39932" 1 selfLink: ... 1 uid: e9ac1099-30e2-11e7-955c-080027e6b159 1 rules: - apiGroups: 2 - "" 2 resources: 2 - persistentvolumes 2 verbs: 2 - get 2 - list 2
-
1 集群角色不是命名空间化的,因此没有命名空间字段。
-
2 在这种情况下,规则与常规角色中的规则完全相同。
在将此集群角色绑定到您的 Pod 的 ServiceAccount 之前,请验证 Pod 是否可以列出持久卷。在第一个终端中运行以下命令,其中您在foo命名空间内运行 Pod 内部的 shell:
/ # curl localhost:8001/api/v1/persistentvolumes 用户 "system:serviceaccount:foo:default" 无法在集群范围内列出持久卷。
注意
URL 中不包含命名空间,因为持久卷不是命名空间化的。
如预期,默认 ServiceAccount 无法列出 PersistentVolumes。你需要将 ClusterRole 绑定到你的 ServiceAccount 以允许它执行此操作。ClusterRoles 可以通过常规 RoleBindings 绑定到主题,因此你现在将创建一个 RoleBinding:
$ kubectl create rolebinding pv-test --clusterrole=pv-reader
--serviceaccount=foo:default -n foo rolebinding "pv-test" created
你现在可以列出 PersistentVolumes 吗?
/ # curl localhost:8001/api/v1/persistentvolumes User "system:serviceaccount:foo:default" cannot list persistentvolumes at the cluster scope.
嗯,这很奇怪。让我们检查以下列表中 RoleBinding 的 YAML。你能说出它(如果有的话)有什么问题吗?
列表 12.13. 引用 ClusterRole 的 RoleBinding
$ kubectl get rolebindings pv-test -o yaml apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: pv-test namespace: foo ... roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole 1 name: pv-reader 1 subjects: - kind: ServiceAccount 2 name: default 2 namespace: foo 2
-
1 绑定引用了 pv-reader ClusterRole。
-
2 绑定的主题是 foo 命名空间中的默认 ServiceAccount。
YAML 看起来完全正常。你引用了正确的 ClusterRole 和正确的 ServiceAccount,如图 12.7 所示,那么问题出在哪里?
图 12.7. 引用 ClusterRole 的 RoleBinding 不授予对集群级别资源的访问权限。

虽然你可以在想要启用对命名空间资源访问时创建 RoleBinding 并使其引用 ClusterRole,但你不能对集群级别(非命名空间)资源使用相同的方法。要授予对集群级别资源的访问权限,你必须始终使用 ClusterRoleBinding。
幸运的是,创建 ClusterRoleBinding 并不像创建 Role-Binding 那样不同,但你需要先清理并删除 RoleBinding:
$ kubectl delete rolebinding pv-test rolebinding "pv-test" deleted
现在创建 ClusterRoleBinding:
$ kubectl create clusterrolebinding pv-test --clusterrole=pv-reader
--serviceaccount=foo:default clusterrolebinding "pv-test" created
如你所见,你在命令中将 rolebinding 替换为 clusterrolebinding,并且没有(需要)指定命名空间。图 12.8 显示了你现在的情况。
图 12.8. 使用 ClusterRoleBinding 和 ClusterRole 授予对集群级别资源的访问权限。

现在看看你是否可以列出 PersistentVolumes:
/ # curl localhost:8001/api/v1/persistentvolumes { "kind": "PersistentVolumeList", "apiVersion": "v1", ...
你可以!实际上,在授予对集群级别资源的访问权限时,你必须使用 ClusterRole 和 ClusterRoleBinding。
提示
记住,即使 RoleBinding 引用了 ClusterRoleBinding,它也无法授予对集群级别资源的访问权限。
允许访问非资源 URL
我们已经提到 API 服务器也公开了非资源 URL。对这些 URL 的访问必须明确授权;否则,API 服务器将拒绝客户端的请求。通常,这会通过 system:discovery ClusterRole 和同名 ClusterRoleBinding 自动完成,它们出现在其他预定义的 ClusterRoles 和 ClusterRoleBindings 中(我们将在 第 12.2.5 节 中探讨它们)。
让我们检查以下列表中显示的 system:discovery ClusterRole。
列表 12.14. 默认的 system:discovery ClusterRole
$ kubectl get clusterrole system:discovery -o yaml apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: system:discovery ... rules: - nonResourceURLs: 1 - /api 1 - /api/* 1 - /apis 1 - /apis/* 1 - /healthz 1 - /swaggerapi 1 - /swaggerapi/* 1 - /version 1 verbs: 2 - get 2
-
1 与引用资源不同,此规则引用非资源 URL。
-
仅允许对这些 URL 使用 HTTP GET 方法。
您可以看到此 ClusterRole 指向 URL 而不是资源(使用 nonResource-URLs 字段而不是 resources 字段)。verbs 字段仅允许在这些 URL 上使用 GET HTTP 方法。
注意
对于非资源 URL,使用 post、put 和 patch 等纯 HTTP 动词,而不是 create 或 update。动词需要小写指定。
与集群级别的资源一样,非资源 URL 的 ClusterRole 必须与 ClusterRoleBinding 绑定。使用 RoleBinding 绑定它们将没有任何效果。system:discovery ClusterRole 有对应的 system:discovery Cluster-RoleBinding,因此让我们通过以下列表检查其内容。
列表 12.15. 默认的 system:discovery ClusterRoleBinding
$ kubectl get clusterrolebinding system:discovery -o yaml apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: system:discovery ... roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole 1 name: system:discovery 1 subjects: - apiGroup: rbac.authorization.k8s.io kind: Group 2 name: system:authenticated 2 - apiGroup: rbac.authorization.k8s.io kind: Group 2 name: system:unauthenticated 2
-
1 此 ClusterRoleBinding 引用了 system:discovery ClusterRole。
-
2 它将 ClusterRole 绑定到所有已认证和未认证的用户(即所有人)。
YAML 显示 ClusterRoleBinding 指向预期的 system:discovery ClusterRole。它绑定到两个组,system:authenticated 和 system:unauthenticated,这使得它绑定到所有用户。这意味着绝对每个人都可以访问 ClusterRole 中列出的 URL。
注意
组属于身份验证插件的范围。当 API 服务器收到请求时,它会调用身份验证插件以获取用户所属的组列表。然后,这些信息用于授权。
你可以通过从 Pod 内部(通过kubectl proxy,这意味着你将以 Pod 的 ServiceAccount 身份进行认证)和从你的本地机器访问/api URL 路径来确认这一点,而不需要指定任何认证令牌(使你成为一个未经认证的用户):
$ curl https://$(minikube ip):8443/api -k { "kind": "APIVersions", "versions": [...]
你现在已经使用了 ClusterRole 和 ClusterRoleBinding 来授予对集群级资源和非资源 URL 的访问权限。现在让我们看看 ClusterRole 如何与命名空间 RoleBinding 一起使用,以授予对 Role-Binding 命名空间中命名空间资源的访问权限。
使用 ClusterRole 授予特定命名空间中资源的访问权限
ClusterRole 不一定需要与集群级别的 ClusterRoleBinding 绑定。它们也可以与常规的命名空间 RoleBinding 绑定。你已经开始查看预定义的 ClusterRole,那么让我们看看另一个名为view的 ClusterRole,它将在下面的列表中展示。
列表 12.16. 默认的view ClusterRole
$ kubectl get clusterrole view -o yaml apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: view ... rules: - apiGroups: - "" resources: 1 - configmaps 1 - endpoints 1 - persistentvolumeclaims 1 - pods 1 - replicationcontrollers 1 - replicationcontrollers/scale 1 - serviceaccounts 1 - services 1 verbs: 2 - get 2 - list 2 - watch ...
-
1 此规则适用于这些资源(注意:它们都是命名空间资源)。
-
2 如 ClusterRole 的名称所暗示的,它只允许读取,而不是写入列出的资源。
此 ClusterRole 有许多规则。列表中只显示了第一个规则。该规则允许获取、列出和监视像 ConfigMaps、Endpoints、Persistent-VolumeClaims 等资源。尽管你正在查看 ClusterRole(而不是常规的命名空间 Role),但这些资源是命名空间资源。这个 Cluster-Role 究竟做了什么?
这取决于它是否与 ClusterRoleBinding 或 RoleBinding 绑定(它可以与任一绑定)。如果你创建一个 ClusterRoleBinding 并在其中引用 ClusterRole,则绑定中列出的主题可以查看所有命名空间中指定的资源。另一方面,如果你创建一个 RoleBinding,则绑定中列出的主题只能查看 RoleBinding 命名空间中的资源。现在你将尝试这两种选项。
你将看到这两种选项如何影响你的测试 Pod 列出 Pod 的能力。首先,让我们看看在没有任何绑定的情况下会发生什么:
/ # curl localhost:8001/api/v1/pods User "system:serviceaccount:foo:default" cannot list pods at the cluster scope./ # / # curl localhost:8001/api/v1/namespaces/foo/pods User "system:serviceaccount:foo:default" cannot list pods in the namespace "foo".
使用第一个命令,你正在尝试列出所有命名空间中的 Pod。使用第二个命令,你正在尝试列出foo命名空间中的 Pod。服务器不允许你这样做。
现在,让我们看看当你创建一个 ClusterRoleBinding 并将其绑定到 Pod 的 ServiceAccount 时会发生什么:
$ kubectl create clusterrolebinding view-test --clusterrole=view
--serviceaccount=foo:default clusterrolebinding "view-test" created
现在 Pod 可以列出foo命名空间中的 Pod 吗?
/ # curl localhost:8001/api/v1/namespaces/foo/pods { "kind": "PodList", "apiVersion": "v1", ...
可以!因为您创建了一个 ClusterRoleBinding,它适用于所有命名空间。在foo命名空间中的 Pod 也可以列出bar命名空间中的 Pod:
/ # curl localhost:8001/api/v1/namespaces/bar/pods { "kind": "PodList", "apiVersion": "v1", ...
好的,Pod 被允许列出不同命名空间中的 Pod。它也可以通过访问/api/v1/pods URL 路径来检索所有命名空间中的 Pod:
/ # curl localhost:8001/api/v1/pods { "kind": "PodList", "apiVersion": "v1", ...
如预期,该 Pod 可以获取集群中所有 Pod 的列表。总结来说,将 ClusterRoleBinding 与引用命名空间资源的 ClusterRole 结合使用,允许 Pod 访问任何命名空间中的命名空间资源,如图 12.9 所示。链接。
图 12.9。ClusterRoleBinding 和 ClusterRole 授予跨所有命名空间的资源权限。

现在,让我们看看如果你用 RoleBinding 替换 ClusterRoleBinding 会发生什么。首先,删除 ClusterRoleBinding:
$ kubectl delete clusterrolebinding view-test clusterrolebinding "view-test" deleted
接下来创建一个 RoleBinding。因为 RoleBinding 是命名空间级别的,你需要指定你想要在其中创建它的命名空间。在foo命名空间中创建它:
$ kubectl create rolebinding view-test --clusterrole=view
--serviceaccount=foo:default -n foo rolebinding "view-test" created
现在,你在foo命名空间中有一个 RoleBinding,将同一命名空间中的default Service-Account 与view ClusterRole 绑定在一起。你的 Pod 现在可以访问什么?
/ # curl localhost:8001/api/v1/namespaces/foo/pods { "kind": "PodList", "apiVersion": "v1", ... / # curl localhost:8001/api/v1/namespaces/bar/pods 用户 "system:serviceaccount:foo:default" 无法在命名空间 "bar" 中列出 Pod。 / # curl localhost:8001/api/v1/pods 用户 "system:serviceaccount:foo:default" 无法在集群范围内列出 Pod。
如您所见,您的 Pod 可以列出foo命名空间中的 Pod,但不能列出任何其他特定命名空间或所有命名空间中的 Pod。这如图 12.10 所示。链接。
图 12.10。仅授予 RoleBinding 命名空间内资源的访问权限的 ClusterRole 引用。

总结 Role、ClusterRole、RoleBinding 和 ClusterRoleBinding 组合
我们已经介绍了许多不同的组合,可能对您来说记住何时使用每个组合可能会有点困难。让我们看看我们是否可以通过按特定用例对它们进行分类来理解所有这些组合。请参阅表 12.2。
表 12.2. 使用特定角色和绑定类型组合的时机
| 用于访问 | 要使用的角色类型 | 要使用的绑定类型 |
|---|---|---|
| 集群级别的资源(节点、持久卷、...) | ClusterRole | ClusterRoleBinding |
| 非资源 URL(/api, /healthz, ...) | ClusterRole | ClusterRoleBinding |
| 任何命名空间中的命名空间资源(以及所有命名空间) | ClusterRole | ClusterRoleBinding |
| 特定命名空间中的命名空间资源(在多个命名空间中重复使用相同的 ClusterRole) | ClusterRole | RoleBinding |
| 特定命名空间中的命名空间资源(角色必须在每个命名空间中定义) | Role | RoleBinding |
希望现在您对四个 RBAC 资源之间的关系有了更清晰的认识。如果您仍然觉得您还没有完全掌握所有内容,请不要担心。随着我们探索下一节中预配置的 ClusterRoles 和 ClusterRoleBindings,事情可能会变得明朗起来。
12.2.5. 理解默认的 ClusterRoles 和 ClusterRoleBindings
Kubernetes 附带一组默认的 ClusterRoles 和 ClusterRoleBindings,每次 API 服务器启动时都会更新。这确保了如果您不小心删除了它们,或者如果 Kubernetes 的新版本使用了不同的集群角色和绑定配置,所有默认的角色和绑定都会被重新创建。
您可以在以下列表中查看默认的集群角色和绑定。
列表 12.17. 列出所有 ClusterRoleBindings和 ClusterRoles
$ kubectl get clusterrolebindings NAME AGE cluster-admin 1d system:basic-user 1d system:controller:attachdetach-controller 1d ... system:controller:ttl-controller 1d system:discovery 1d system:kube-controller-manager 1d system:kube-dns 1d system:kube-scheduler 1d system:node 1d system:node-proxier 1d $ kubectl get clusterroles NAME AGE admin 1d cluster-admin 1d edit 1d system:auth-delegator 1d system:basic-user 1d system:controller:attachdetach-controller 1d ... system:controller:ttl-controller 1d system:discovery 1d system:heapster 1d system:kube-aggregator 1d system:kube-controller-manager 1d system:kube-dns 1d system:kube-scheduler 1d system:node 1d system:node-bootstrapper 1d system:node-problem-detector 1d system:node-proxier 1d system:persistent-volume-provisioner 1d view 1d
最重要的角色是view、edit、admin和cluster-admin ClusterRoles。它们旨在绑定到用户定义的 Pod 使用的 ServiceAccounts。
允许对具有视图 ClusterRole 的资源进行只读访问
在前面的示例中,您已经使用了默认的view ClusterRole。它允许读取命名空间中的大多数资源,除了 Roles、RoleBindings 和 Secrets。您可能想知道,为什么不包括 Secrets?因为那些 Secrets 中可能包含一个比view ClusterRole 中定义的权限更大的认证令牌,这可能会允许用户伪装成不同的用户以获得额外的权限(权限提升)。
使用 edit ClusterRole 修改资源
接下来是edit ClusterRole,它允许您修改命名空间中的资源,同时也允许读取和修改 Secrets。然而,它不允许查看或修改 Roles 或 RoleBindings——这同样是出于防止权限提升的目的。
使用管理员 ClusterRole 授予命名空间的全权控制
在 admin ClusterRole 中授予命名空间中资源的完全控制权。具有此 ClusterRole 的主题可以读取和修改命名空间中的任何资源,除了 ResourceQuotas(我们将在第十四章章节 14 中学习这些内容)和命名空间资源本身。edit 和 admin Cluster-Roles 之间的主要区别在于查看和修改命名空间中的角色和 RoleBindings 的能力。
注意
为了防止权限提升,API 服务器只允许用户在已经拥有该角色中列出的所有权限(以及相同的范围)的情况下创建和更新角色。
使用 cluster-admin ClusterRole 允许完全控制
可以通过将 cluster-admin ClusterRole 分配给主题来授予对 Kubernetes 集群的完全控制权。正如你之前看到的,admin ClusterRole 不允许用户修改命名空间的 ResourceQuota 对象或命名空间资源本身。如果你想允许用户这样做,你需要创建一个引用 cluster-admin ClusterRole 的 Role-Binding。这会给 RoleBinding 中包含的用户在创建 RoleBinding 的命名空间中的所有方面提供完全控制权。
如果你注意到了,你可能已经知道如何让用户完全控制集群中的所有命名空间。是的,通过在 ClusterRoleBinding 中引用 cluster-admin ClusterRole 而不是 RoleBinding 来实现。
理解其他默认 ClusterRoles
默认的 ClusterRoles 列表中包含大量以 system: 前缀开头的其他 ClusterRoles,这些 ClusterRoles 旨在被各种 Kubernetes 组件使用。其中,你可以找到诸如 system:kube-scheduler 这样的角色,显然它被调度器使用,system:node 被 kubelets 使用,等等。
尽管控制器管理器作为一个单独的 pod 运行,但其中运行的每个控制器都可以使用单独的 ClusterRole 和 ClusterRoleBinding(它们以 system: controller: 前缀开头)。
这些系统 ClusterRoles 每个都有一个匹配的 ClusterRoleBinding,它将其绑定到系统组件认证的用户。例如,system:kube-scheduler ClusterRoleBinding 将同名 ClusterRole 分配给 system:kube-scheduler 用户,这是调度器认证时使用的用户名。
12.2.6. 聪明地授予授权权限
默认情况下,命名空间中的默认 ServiceAccount 除了未认证用户的权限外没有其他权限(正如你可能从之前的示例中记得的,system:discovery ClusterRole 和相关绑定允许任何人对一些非资源 URL 进行 GET 请求)。因此,默认情况下,pods 甚至无法查看集群状态。这取决于你授予它们适当的权限来执行此操作。
显然,将所有 ServiceAccounts 都赋予cluster-admin ClusterRole 是一个糟糕的想法。正如在安全方面总是那样,最好只给每个人他们完成工作所需的权限,而不是更多的权限(最小权限原则)。
为每个 Pod 创建特定的 ServiceAccounts。
为每个 Pod(或一组 Pod 副本)创建一个特定的 ServiceAccount,然后通过 RoleBinding(而不是 ClusterRoleBinding)将其与定制的 Role(或 ClusterRole)关联起来是一个好主意,因为 ClusterRoleBinding 会给予 Pod 访问其他命名空间资源的权限,这可能不是你想要的)。
如果你的某个 Pod(其中运行的应用程序)只需要读取 Pod,而另一个 Pod 还需要修改它们,那么就创建两个不同的 ServiceAccounts,并通过在 Pod 规范中指定serviceAccountName属性来让这些 Pod 使用它们,正如你在本章的第一部分所学。不要将两个 Pod 所需的所有必要权限都添加到命名空间中的默认 ServiceAccount 中。
预期你的应用程序会被入侵
你的目标是减少入侵者获取你的集群的可能性。今天的复杂应用程序包含许多漏洞。你应该预料到不受欢迎的人最终会接触到 ServiceAccount 的认证令牌,因此你应该始终限制 ServiceAccount,以防止他们造成任何真正的损害。
12.3. 摘要
本章为你提供了如何保护 Kubernetes API 服务器的基础知识。你学习了以下内容:
-
API 服务器的客户端包括人类用户和运行在 Pod 中的应用程序。
-
Pod 中的应用程序与 ServiceAccount 相关联。
-
用户和 ServiceAccounts 都与组相关联。
-
默认情况下,Pod 在默认 ServiceAccount 下运行,该 ServiceAccount 为每个命名空间自动创建。
-
可以手动创建额外的 ServiceAccounts 并将其与 Pod 关联。
-
可以配置 ServiceAccounts,以允许在给定的 Pod 中仅挂载受限的 Secrets 列表。
-
ServiceAccount 也可以用来将图像拉取 Secrets 附加到 Pod 上,这样你就不需要在每个 Pod 中指定 Secrets。
-
Roles 和 ClusterRoles 定义了可以在哪些资源上执行哪些操作。
-
RoleBindings 和 ClusterRoleBindings 将 Roles 和 ClusterRoles 绑定到用户、组和 ServiceAccounts。
-
每个集群都包含默认的 ClusterRoles 和 ClusterRoleBindings。
在下一章中,你将学习如何保护集群节点免受 Pod 的攻击,以及如何通过网络安全来隔离 Pod。
第十三章. 保护集群节点和网络
本章涵盖
-
在 Pod 中使用节点的默认 Linux 命名空间
-
以不同用户运行容器
-
运行特权容器
-
添加或删除容器的内核能力
-
定义安全策略以限制 Pod 可以做什么
-
保护 Pod 网络
在上一章中,我们讨论了保护 API 服务器。如果一个攻击者获得了对 API 服务器的访问权限,他们可以通过将他们的代码打包成容器镜像并在 Pod 中运行它来执行任何他们喜欢的事情。但他们能造成真正的损害吗?容器不是与其他容器以及它们运行的节点隔离的吗?
不一定。在本章中,你将学习如何允许 Pod 访问它们运行的节点上的资源。你还将学习如何配置集群,使用户无法随意使用他们的 Pod。然后,在章节的最后部分,你还将学习如何保护 Pod 用于通信的网络。
13.1. 在 Pod 中使用主机节点的命名空间
Pod 中的容器通常在单独的 Linux 命名空间中运行,这使它们的进程与其他容器或节点默认命名空间中运行的进程隔离。
例如,我们了解到每个 Pod 都有自己的 IP 和端口空间,因为它使用自己的网络命名空间。同样,每个 Pod 都有自己的进程树,因为它有自己的 PID 命名空间,它还使用自己的 IPC 命名空间,只允许同一 Pod 中的进程通过进程间通信机制(IPC)相互通信。
13.1.1. 在 Pod 中使用节点的网络命名空间
某些 Pod(通常是系统 Pod)需要在主机的默认命名空间中运行,这样它们就可以查看和操作节点级别的资源和设备。例如,一个 Pod 可能需要使用节点的网络适配器而不是自己的虚拟网络适配器。这可以通过将 Pod 规范中的 hostNetwork 属性设置为 true 来实现。
在这种情况下,Pod 可以使用节点的网络接口,而不是拥有自己的集合,如图 13.1 所示。这意味着 Pod 没有自己的 IP 地址,如果它运行一个绑定到端口的进程,该进程将绑定到节点的端口。
图 13.1. hostNetwork: true 的 Pod 使用节点的网络接口而不是自己的。

你可以尝试运行这样的 Pod。下面的列表显示了一个示例 Pod 清单。
列表 13.1. 使用节点网络命名空间的 Pod:pod-with-host-network.yaml
apiVersion: v1 kind: Pod metadata: name: pod-with-host-network spec: hostNetwork: true 1 containers: - name: main image: alpine command: ["/bin/sleep", "999999"]
- 1 使用主机节点的网络命名空间
在你运行 Pod 之后,你可以使用以下命令来查看它确实使用了主机的网络命名空间(例如,它可以看到所有主机的网络适配器)。
列表 13.2. 使用主机网络命名空间的 pod 中的网络接口
$ kubectl exec pod-with-host-network ifconfig docker0 链路封装:以太网 硬件地址 02:42:14:08:23:47 inet 地址:172.17.0.1 广播:0.0.0.0 子网掩码:255.255.0.0 ... eth0 链路封装:以太网 硬件地址 08:00:27:F8:FA:4E inet 地址:10.0.2.15 广播:10.0.2.255 子网掩码:255.255.255.0 ... lo 链路封装:本地回环 inet 地址:127.0.0.1 子网掩码:255.0.0.0 ... veth1178d4f 链路封装:以太网 硬件地址 1E:03:8D:D6:E1:2C inet6 地址: fe80::1c03:8dff:fed6:e12c/64 范围:链路 UP 广播 运行 多播 MTU:1500 度量:1 ...
当 Kubernetes 控制平面组件作为 pod 部署时(例如,当您使用 kubeadm 部署您的集群,如附录 B 所解释的那样),您会发现这些 pod 使用 hostNetwork 选项,实际上使它们表现得好像它们没有在 pod 内运行。
13.1.2. 不使用主机网络命名空间绑定到主机端口
一个相关的功能允许 pod 绑定到节点默认命名空间中的端口,但仍然有自己的网络命名空间。这是通过在 spec.containers.ports 字段中定义的容器端口之一使用 hostPort 属性来实现的。
不要混淆使用 hostPort 的 pod 与通过 NodePort 服务暴露的 pod。它们是两件不同的事情,如图 13.2 所解释的那样。
图 13.2. 使用 hostPort 的 pod 与位于 NodePort 服务后面的 pod 之间的区别。

在图中,您首先会注意到,当一个 pod 使用 hostPort 时,节点端口的连接会直接转发到该节点上运行的 pod,而使用 NodePort 服务时,节点端口的连接会转发到随机选择的 pod(可能在另一个节点上)。另一个区别是,使用 hostPort 的 pod,节点端口仅在运行此类 pod 的节点上绑定,而 NodePort 服务将端口绑定到所有节点,即使在那些没有运行此类 pod 的节点上(如图中的节点 3)。
重要的是要理解,如果一个 pod 使用特定的主机端口,则每个节点只能调度一个 pod 实例,因为两个进程不能绑定到同一个主机端口。调度器在调度 pod 时会考虑这一点,因此不会将多个 pod 调度到同一个节点,如图 13.3 所示。如果您有三个节点并想部署四个 pod 副本,则只有三个会被调度(一个 pod 将保持挂起状态)。
图 13.3. 如果使用主机端口,则只能将单个 pod 实例调度到节点。

让我们看看如何在 pod 的 YAML 定义中定义 hostPort。以下列表显示了运行您的 kubia pod 并将其绑定到节点端口 9000 的 YAML。
列表 13.3. 将 pod 绑定到节点端口空间中的端口:kubia-hostport.yaml
apiVersion: v1 kind: Pod metadata: name: kubia-hostport spec: containers: - image: luksa/kubia name: kubia ports: - containerPort: 8080 1 hostPort: 9000 2 protocol: TCP
-
1 容器可以通过 pod 的 IP 地址的 8080 端口访问。
-
2 它也可以通过其部署的节点的 9000 端口访问。
在你创建这个 pod 之后,你可以通过它被调度到的节点的 9000 端口访问它。如果你有多个节点,你会看到你不能通过其他节点的该端口访问 pod。
注意
如果你正在 GKE 上尝试此操作,你需要使用gcloud compute firewall-rules正确配置防火墙,就像你在第五章中做的那样章节 5。
hostPort功能主要用于暴露系统服务,这些服务使用 DaemonSets 部署到每个节点。最初,人们也用它来确保同一 pod 的两个副本永远不会调度到同一节点,但现在你有更好的方法来实现这一点——它将在第十六章中解释章节 16。
13.1.3. 使用节点的 PID 和 IPC 命名空间
与hostNetwork选项类似的是hostPID和hostIPC pod 规范属性。当你将它们设置为true时,pod 的容器将使用节点的 PID 和 IPC 命名空间,允许在容器中运行的进程看到节点上的所有其他进程或通过 IPC 与它们通信。以下列表提供了一个示例。
列表 13.4. 使用主机的 PID 和 IPC 命名空间:pod-with-host-pid-and-ipc.yaml
apiVersion: v1 kind: Pod metadata: name: pod-with-host-pid-and-ipc spec: hostPID: true 1 hostIPC: true 2 containers: - name: main image: alpine command: ["/bin/sleep", "999999"]
-
1 你希望 pod 使用主机的 PID 命名空间。
-
2 你还希望 pod 使用主机的 IPC 命名空间。
你会记得,pods 通常只能看到它们自己的进程,但如果你运行这个 pod 然后从其容器中列出进程,你会看到在主机节点上运行的所有进程,而不仅仅是容器中运行的进程,如下面的列表所示。
列表 13.5. hostPID: true的 pod 中可见的进程
$ kubectl exec pod-with-host-pid-and-ipc ps aux PID USER TIME COMMAND 1 root 0:01 /usr/lib/systemd/systemd --switched-root --system ... 2 root 0:00 [kthreadd] 3 root 0:00 [ksoftirqd/0] 5 root 0:00 [kworker/0:0H] 6 root 0:00 [kworker/u2:0] 7 root 0:00 [migration/0] 8 root 0:00 [rcu_bh] 9 root 0:00 [rcu_sched] 10 root 0:00 [watchdog/0] ...
通过将hostIPC属性设置为true,pod 中的容器进程也可以通过进程间通信(Inter-Process Communication)与节点上运行的所有其他进程进行通信。
13.2. 配置容器的安全上下文
除了允许 Pod 使用主机的 Linux 命名空间外,还可以通过 security-Context 属性在 Pod 及其容器上配置其他安全相关功能,这些属性可以直接在 Pod 规范下以及单个容器的规范内部指定。
理解安全上下文中可配置的内容
配置安全上下文允许你执行各种操作:
-
指定容器中进程将运行的用户(用户的 ID)。
-
防止容器以 root(容器默认运行的默认用户通常在容器镜像本身中定义,因此你可能希望防止容器以 root 运行)运行。
-
以特权模式运行容器,使其能够完全访问节点的内核。
-
通过添加或删除能力来配置细粒度的权限,这与在特权模式下运行容器以授予所有可能的权限形成对比。
-
将 SELinux(安全增强型 Linux)选项设置为强锁定容器。
-
防止进程向容器的文件系统写入。
我们将在下一节中探讨这些选项。
不指定安全上下文运行 Pod
首先,运行具有默认安全上下文选项的 Pod(通过完全不指定它们),这样你可以看到它与具有自定义安全上下文的 Pod 的行为有何不同:
$ kubectl run pod-with-defaults --image alpine --restart Never
-- /bin/sleep 999999 pod "pod-with-defaults" created
让我们看看容器运行的用户和组 ID,以及它属于哪些组。你可以通过在容器内部运行 id 命令来查看:
$ kubectl exec pod-with-defaults id uid=0(root) gid=0(root) groups=0(root), 1(bin), 2(daemon), 3(sys), 4(adm), 6(disk), 10(wheel), 11(floppy), 20(dialout), 26(tape), 27(video)
容器以用户 ID (uid) 0 运行,即 root,并且以组 ID (gid) 0(也是 root)运行。它也是多个其他组的成员。
注意
容器运行的用户由容器镜像指定。在 Dockerfile 中,这是通过使用 USER 指令来完成的。如果省略,容器将以 root 运行。
现在,你将运行一个容器以不同用户运行的 Pod。
13.2.1. 以特定用户运行容器
要在不同于容器镜像中内置的用户 ID 下运行 Pod,你需要设置 Pod 的 securityContext.runAsUser 属性。你将使容器以用户 guest 运行,其用户 ID 在 alpine 容器镜像中为 405,如下所示。
列表 13.6. 以特定用户运行容器:pod-as-user-guest.yaml
apiVersion: v1 kind: Pod metadata: name: pod-as-user-guest spec: containers: - name: main image: alpine command: ["/bin/sleep", "999999"] securityContext: runAsUser: 405 1
- 1 你需要指定用户 ID,而不是用户名(ID 405 对应于 guest 用户)。
现在,为了看到 runAsUser 属性的效果,像之前一样在这个新 pod 中运行 id 命令:
$ kubectl exec pod-as-user-guest id uid=405(guest) gid=100(users)
如请求,容器正在以 guest 用户运行。
13.2.2. 防止容器以 root 用户运行
如果你不在乎容器运行的用户是谁,但仍然想防止它以 root 用户运行,会怎样?
想象一下,部署了一个使用 Dockerfile 中的 USER daemon 指令构建的容器镜像,该指令使得容器以 daemon 用户运行。如果攻击者获取了对你的镜像仓库的访问权限,并在相同的标签下推送了不同的镜像,会怎样?攻击者的镜像被配置为以 root 用户运行。当 Kubernetes 调度你的 pod 的新实例时,Kubelet 将下载攻击者的镜像并运行其中放入的任何代码。
尽管容器大多数情况下与主机系统隔离,但以 root 用户运行它们的进程仍然被认为是一种不良做法。例如,当主机目录被挂载到容器中时,如果容器中运行的过程是以 root 运行的,它将完全访问挂载的目录,而如果它是以非 root 运行的,则不会。
为了防止之前描述的攻击场景,你可以指定 pod 的容器需要以非 root 用户运行,如下面的列表所示。
列表 13.7. 防止容器以 root 用户运行:pod-run-as-non-root.yaml
apiVersion: v1 kind: Pod metadata: name: pod-run-as-non-root spec: containers: - name: main image: alpine command: ["/bin/sleep", "999999"] securityContext: runAsNonRoot: true
- 1 这个容器只允许以非 root 用户运行。
如果你部署了这个 pod,它会被调度,但不会被允许运行:
$ kubectl get po pod-run-as-non-root NAME READY STATUS pod-run-as-non-root 0/1 container has runAsNonRoot and image will run
as root`
现在,如果有人篡改你的容器镜像,他们也不会走得太远。
13.2.3. 以特权模式运行 pod
有时 pod 需要做它在运行的节点上能做的一切,比如使用受保护的系统设备或其他内核功能,这些功能对常规容器是不可访问的。
这样的 pod 的一个例子是 kube-proxy pod,它需要修改节点的 iptables 规则以使服务工作,正如在第十一章中解释的那样。第十一章。如果你遵循附录 B 中的说明并使用 kubeadm 部署集群,你会看到每个集群节点都运行一个 kube-proxy pod,你可以检查其 YAML 规范以查看它使用的所有特殊功能。
要完全访问节点的内核,pod 的容器以特权模式运行。这是通过将容器 security-Context 属性中的 privileged 属性设置为 true 来实现的。您将从以下列表中的 YAML 创建一个特权 pod。
列表 13.8. 带有特权容器的 pod:pod-privileged.yaml
apiVersion: v1 kind: Pod metadata: name: pod-privileged spec: containers: - name: main image: alpine command: ["/bin/sleep", "999999"] securityContext: privileged: true 1
- 1 此容器将以特权模式运行
继续部署此 pod,以便您可以将其与您之前运行的非特权 pod 进行比较。
如果您熟悉 Linux,您可能知道它有一个特殊的文件目录,称为 /dev,其中包含系统上所有设备的设备文件。这些不是磁盘上的常规文件,而是用于与设备通信的特殊文件。让我们看看您之前部署的非特权容器(pod-with-defaults pod)中可见的设备,如下所示,通过列出其 /dev 目录中的文件。
列表 13.9. 非特权 pod 中可用的设备列表
$ kubectl exec -it pod-with-defaults ls /dev core null stderr urandom fd ptmx stdin zero full pts stdout fuse random termination-log mqueue shm tty
列表显示了所有设备。列表相当短。现在,将此与以下列表进行比较,该列表显示了您的特权 pod 可以看到的设备文件。
列表 13.10. 特权 pod 中可用的设备列表
$ kubectl exec -it pod-privileged ls /dev autofs snd tty46 bsg sr0 tty47 btrfs-control stderr tty48 core stdin tty49 cpu stdout tty5 cpu_dma_latency termination-log tty50 fd tty tty51 ... ... ...
我没有包括整个列表,因为列表太长,不适合本书,但很明显,设备列表比之前长得多。实际上,特权容器可以看到宿主节点上的所有设备。这意味着它可以自由使用任何设备。
例如,当我想要在运行在树莓派上的 pod 中控制连接到它的 LED 时,我必须使用类似这样的特权模式。
13.2.4. 向容器添加单个内核功能
在前面的部分中,您看到了赋予容器无限权力的方法之一。在过去的岁月里,传统的 UNIX 实现只区分特权和非特权进程,但多年来,Linux 通过内核能力支持了一个更细粒度的权限系统。
与将容器设置为特权并赋予它无限权限相比,一种更安全的方法(从安全角度考虑)是只给它提供它真正需要的内核功能访问权限。Kubernetes 允许您为每个容器添加能力或删除其中的一部分,这允许您微调容器的权限并限制攻击者潜在入侵的影响。
例如,容器通常不允许更改系统时间(硬件时钟的时间)。您可以通过尝试在您的 pod-with-defaults pod 中设置时间来确认这一点:
$ kubectl exec -it pod-with-defaults -- date +%T -s "12:00:00" date: can't set date: Operation not permitted
如果您想允许容器更改系统时间,您可以将名为 CAP_SYS_TIME 的能力添加到容器的 capabilities 列表中,如下所示。
列表 13.11 添加 CAP_SYS_TIME 能力:pod-add-settime-capability.yaml
apiVersion: v1 kind: Pod metadata: name: pod-add-settime-capability spec: containers: - name: main image: alpine command: ["/bin/sleep", "999999"] securityContext: 1 capabilities: 1 add: 2 - SYS_TIME 2
-
1 能力是在 securityContext 属性下添加或删除的。
-
2 您正在添加 SYS_TIME 能力。
注意
Linux 内核能力通常以 CAP_. 为前缀。但在 pod 规范中指定它们时,必须省略前缀。
如果您在这个新 pod 的容器中运行相同的命令,系统时间将成功更改:
$ kubectl exec -it pod-add-settime-capability -- date +%T -s "12:00:00" 12:00:00 $ kubectl exec -it pod-add-settime-capability -- date Sun May 7 12:00:03 UTC 2017
警告
如果您亲自尝试,请注意这可能会使您的工作节点变得不可用。在 Minikube 中,尽管系统时间被网络时间协议(NTP)守护进程自动重置,但我不得不重新启动虚拟机来调度新的 pod。
您可以通过检查运行 pod 的节点上的时间来确认节点的时间已更改。在我的情况下,我使用 Minikube,所以我只有一个节点,我可以这样获取它的时间:
$ minikube ssh date Sun May 7 12:00:07 UTC 2017
以这种方式添加能力比使用 privileged: true 给容器赋予全部权限要好得多。诚然,这需要您了解并理解每个能力的作用。
小贴士
您可以在 Linux man 页面中找到 Linux 内核能力的列表。
13.2.5 从容器中删除能力
你已经看到了如何添加能力,但你也可以移除容器可能拥有的能力。例如,默认授予容器的能力包括CAP_CHOWN能力,它允许进程更改文件系统中的文件所有权。
你可以通过将pod-with-defaults pod 中的/tmp 目录的所有权更改为guest用户来验证这一点,例如:
$ kubectl exec pod-with-defaults chown guest /tmp``$ kubectl exec pod-with-defaults -- ls -la / | grep tmp drwxrwxrwt 2 guest root 6 May 25 15:18 tmp
为了防止容器执行此操作,你需要通过在容器的securityContext.capabilities.drop属性下列出它来移除能力,如下面的列表所示。
列表 13.12. 从容器中移除能力:pod-drop-chown-capability.yaml
apiVersion: v1 kind: Pod metadata: name: pod-drop-chown-capability spec: containers: - name: main image: alpine command: ["/bin/sleep", "999999"] securityContext: capabilities: drop: 1 - CHOWN 1
- 1 你没有允许这个容器更改文件所有权。
通过移除CHOWN能力,你将不允许在这个 pod 中更改/tmp 目录的所有权:
$ kubectl exec pod-drop-chown-capability chown guest /tmp chown: /tmp: Operation not permitted
你几乎完成了对容器安全上下文选项的探索。让我们再看一个。
13.2.6. 防止进程向容器的文件系统写入
你可能希望防止在容器中运行的进程向容器的文件系统写入,而只允许它们写入挂载的卷。你主要会出于安全原因这样做。
让我们想象你正在运行一个具有隐藏漏洞的 PHP 应用程序,这个漏洞允许攻击者向文件系统写入。PHP 文件在构建时被添加到容器镜像中,并从容器的文件系统中提供服务。由于这个漏洞,攻击者可以修改这些文件并向其中注入恶意代码。
这些类型的攻击可以通过防止容器向其文件系统写入来阻止,通常应用程序的可执行代码存储在该文件系统中。这是通过将容器的securityContext.readOnlyRootFilesystem属性设置为true来实现的,如下面的列表所示。
列表 13.13. 具有只读文件系统的容器:pod-with-readonly-filesystem.yaml
apiVersion: v1 kind: Pod metadata: name: pod-with-readonly-filesystem spec: containers: - name: main image: alpine command: ["/bin/sleep", "999999"] securityContext: 1 readOnlyRootFilesystem: true 1 volumeMounts: 2 - name: my-volume 2 mountPath: /volume 2 readOnly: false 2 volumes: - name: my-volume emptyDir:
-
1 这个容器的文件系统无法写入...
-
2 ...但是写入/volume 是被允许的,因为在那里挂载了一个卷。
当你部署这个 Pod 时,容器以 root 用户身份运行,对/目录有写权限,但尝试在那里写入文件会失败:
$ kubectl exec -it pod-with-readonly-filesystem touch /new-file touch: /new-file: Read-only file system
另一方面,写入挂载的卷是允许的:
$ kubectl exec -it pod-with-readonly-filesystem touch /volume/newfile``$ kubectl exec -it pod-with-readonly-filesystem -- ls -la /volume/newfile -rw-r--r-- 1 root root 0 May 7 19:11 /mountedVolume/newfile
如示例所示,当你将容器的文件系统设置为只读时,你可能希望在每个应用程序写入的目录中挂载一个卷(例如,日志、磁盘缓存等)。
小贴士
为了提高安全性,在生产环境中运行 Pod 时,将它们的容器readOnlyRootFilesystem属性设置为true。
在 Pod 级别设置安全上下文选项
在所有这些例子中,你都设置了单个容器的安全上下文。这些选项中的几个也可以在 Pod 级别设置(通过pod.spec.security-Context属性)。它们作为 Pod 中所有容器的默认值,但可以在容器级别被覆盖。Pod 级别的安全上下文还允许你设置额外的属性,我们将在下一节中解释。
13.2.7. 当容器以不同用户运行时共享卷
在第六章中,我们解释了如何使用卷在 Pod 的容器之间共享数据。你在一个容器中写入文件并在另一个容器中读取它们时没有遇到任何麻烦。
但这仅仅是因为两个容器都以 root 用户身份运行,从而能够访问卷中的所有文件。现在想象一下使用我们之前解释的runAsUser选项。你可能需要以两个不同的用户运行这两个容器(也许你正在使用两个第三方容器镜像,每个镜像在其自己的特定用户下运行其进程)。如果这两个容器使用卷来共享文件,它们可能不一定能够读取或写入对方的文件。
这就是为什么 Kubernetes 允许你为在容器中运行的所有 Pod 指定补充组,无论它们以什么用户 ID 运行,都可以共享文件。这是通过以下两个属性完成的:
-
fsGroup -
supplementalGroups
他们所做的事情最好通过一个例子来解释,所以让我们看看如何在 Pod 中使用它们,然后看看它们的效果。接下来的列表描述了一个包含两个容器共享相同空间的 Pod。
列表 13.14. fsGroup & supplementalGroups: pod-with-shared-volume-fsgroup.yaml
apiVersion: v1 kind: Pod metadata: name: pod-with-shared-volume-fsgroup spec: securityContext: 1 fsGroup: 555 1 supplementalGroups: [666, 777] 1 containers: - name: first image: alpine command: ["/bin/sleep", "999999"] securityContext: 2 runAsUser: 1111 2 volumeMounts: 3 - name: shared-volume 3 mountPath: /volume readOnly: false - name: second image: alpine command: ["/bin/sleep", "999999"] securityContext: 4 runAsUser: 2222 4 volumeMounts: 3 - name: shared-volume 3 mountPath: /volume readOnly: false volumes: 3 - name: shared-volume 3 emptyDir:
-
1 fsGroup 和 supplementalGroups 在 Pod 级别的安全上下文中定义。
-
2 第一个容器以用户 ID 1111 运行。
-
3 两个容器使用相同的卷
-
4 第二个容器以用户 ID 2222 运行。
在创建此 Pod 之后,在其第一个容器中运行一个 shell,查看容器运行时的用户和组 ID:
$ kubectl exec -it pod-with-shared-volume-fsgroup -c first sh``/ $ id uid=1111 gid=0(root) groups=555,666,777
id 命令显示容器正在以用户 ID 1111 运行,正如 Pod 定义中所指定。实际组 ID 是 0(root),但组 ID 555、666 和 777 也与用户相关联。
在 Pod 定义中,您将 fsGroup 设置为 555。因此,挂载的卷将由组 ID 555 拥有,如下所示:
/ $ ls -l / | grep volume drwxrwsrwx 2 root 555 6 May 29 12:23 volume
如果您在挂载卷的目录中创建文件,该文件将由用户 ID 1111(这是容器运行时的用户 ID)和组 ID 555 所拥有:
/ $ echo foo > /volume/foo``/ $ ls -l /volume total 4 -rw-r--r-- 1 1111 555`` 4 May 29 12:25 foo
这与为新创建的文件设置所有权的常规方式不同。通常,用户的实际组 ID(在你的情况下是0)在用户创建文件时使用。您可以通过在容器的文件系统中创建文件而不是在卷中创建文件来查看这一点:
/ $ echo foo > /tmp/foo``/ $ ls -l /tmp total 4 -rw-r--r-- 1 1111 root`` 4 May 29 12:41 foo
如您所见,当进程在卷中创建文件时使用fsGroup安全上下文属性(但这取决于所使用的卷插件),而supplementalGroups属性定义了与用户关联的附加组 ID 列表。
这部分关于容器安全上下文配置的内容到此结束。接下来,我们将看到集群管理员如何限制用户进行此类操作。
13.3. 限制 Pod 中安全相关功能的用法
上一节中的示例展示了部署 pod 的人如何通过在节点上部署一个特权 pod 等方式,在任意集群节点上做他们想做的事情。显然,必须有一种机制来防止用户执行所解释的部分或全部操作。集群管理员可以通过创建一个或多个 PodSecurityPolicy 资源来限制之前描述的安全相关功能的使用。
13.3.1. PodSecurityPolicy 资源介绍
PodSecurityPolicy 是一个集群级别的(非命名空间)资源,它定义了用户可以在他们的 pod 中使用或不能使用的与安全相关的功能。维护 PodSecurityPolicy 资源中配置的策略的任务由运行在 API 服务器上的 PodSecurity-Policy 接受控制插件执行(我们在第十一章中解释了接受控制插件章节 11)。
注意
PodSecurityPolicy 接受控制插件可能未在您的集群中启用。在运行以下示例之前,请确保它已启用。如果您使用 Minikube,请参考下一侧边栏。
当有人将 pod 资源发布到 API 服务器时,PodSecurityPolicy 接受控制插件会根据配置的 PodSecurityPolicies 验证 pod 定义。如果 pod 符合集群的策略,它将被接受并存储到 etcd 中;否则,它将被立即拒绝。插件还可以根据策略中配置的默认值修改 pod 资源。
在 Minikube 中启用 RBAC 和 PodSecurityPolicy 接受控制
我使用 Minikube 版本 v0.19.0 来运行这些示例。该版本既没有启用 PodSecurityPolicy 接受控制插件,也没有启用 RBAC 授权,这在部分练习中是必需的。一项练习还需要以不同的用户身份进行身份验证,因此您还需要在定义在文件中的用户处启用基本身份验证插件。
要运行所有这些插件都启用的 Minikube,您可能需要使用此(或类似)命令,具体取决于您使用的版本:
$ minikube start --extra-config apiserver.Authentication.PasswordFile.
BasicAuthFile=/etc/kubernetes/passwd --extra-config=apiserver.
Authorization.Mode=RBAC --extra-config=apiserver.GenericServerRun
Options.AdmissionControl=NamespaceLifecycle,LimitRanger,Service
Account,PersistentVolumeLabel,DefaultStorageClass,ResourceQuota,
DefaultTolerationSeconds,PodSecurityPolicy
在您在命令行选项中指定的密码文件创建之前,API 服务器不会启动。以下是创建文件的方法:
$ cat <<EOF | minikube ssh sudo tee /etc/kubernetes/passwd password,alice,1000,basic-user password,bob,2000,privileged-user EOF
您将在第十三章的代码存档 Chapter13/minikube-with-rbac-and-psp-enabled.sh 中找到一个运行这两个命令的 shell 脚本。
理解 PodSecurityPolicy 可以做什么
PodSecurityPolicy 资源定义了如下内容:
-
一个 pod 是否可以使用主机的 IPC、PID 或网络命名空间
-
pod 可以绑定到哪些主机端口
-
容器可以运行哪些用户 ID
-
是否可以创建具有特权容器的 pod
-
哪些内核能力是被允许的,哪些是默认添加的,哪些总是被丢弃
-
容器可以使用哪些 SELinux 标签
-
容器是否可以使用可写根文件系统
-
容器可以运行在哪些文件系统组中
-
pod 可以使用哪些卷类型
如果你已经阅读到本章的这一部分,那么前一个列表中的所有内容都应该熟悉。最后一个条目也应该相当清晰。
检查一个示例 PodSecurityPolicy
以下列表显示了一个示例 PodSecurityPolicy,它阻止 pod 使用主机的 IPC、PID 和网络命名空间,并阻止运行特权容器以及使用大多数主机端口(除了 10000-11000 和 13000-14000 的端口)。该策略不对容器可以运行的用户、组或 SELinux 组设置任何约束。
列表 13.15. 一个示例 PodSecurityPolicy:pod-security-policy.yaml
apiVersion: extensions/v1beta1 kind: PodSecurityPolicy metadata: name: default spec: hostIPC: false 1 hostPID: false 1 hostNetwork: false 1 hostPorts: 2 - min: 10000 2 max: 11000 2 - min: 13000 2 max: 14000 2 privileged: false 3 readOnlyRootFilesystem: true 4 runAsUser: 5 rule: RunAsAny 5 fsGroup: 5 rule: RunAsAny 5 supplementalGroups: 5 rule: RunAsAny 5 seLinux: 6 rule: RunAsAny 6 volumes: 7 - '*' 7
-
1 容器不允许使用主机的 IPC、PID 或网络命名空间。
-
2 它们只能绑定到主机端口 10000 到 11000(包含)或主机端口 13000 到 14000。
-
3 容器不能以特权模式运行。
-
4 容器被强制以只读根文件系统运行。
-
5 容器可以以任何用户和任何组运行。
-
6 它们还可以使用它们想要的任何 SELinux 组。
-
7 所有卷类型都可以在 pod 中使用。
在示例中指定的大多数选项应该是自解释的,尤其是如果你已经阅读了前面的章节。在此 PodSecurityPolicy 资源发布到集群之后,API 服务器将不再允许你部署之前使用的特权 pod。例如
$ kubectl create -f pod-privileged.yaml Error from server (Forbidden): error when creating "pod-privileged.yaml": pods "pod-privileged" is forbidden: unable to validate against any pod security policy: [spec.containers[0].securityContext.privileged: Invalid value: true: Privileged containers are not allowed]
同样,你不能再部署想要使用主机 PID、IPC 或网络命名空间的 pod。此外,因为你将策略中的 readOnlyRootFilesystem 设置为 true,所以所有 pod 中的容器文件系统都将为只读(容器只能写入卷)。
13.3.2. 理解 runAsUser、fsGroup 和 supplementalGroups 策略
之前示例中的策略没有对容器可以运行的哪些用户和组施加任何限制,因为你在 run-As-User、fsGroup 和 supplementalGroups 字段中使用了 RunAsAny 规则。如果你想限制允许的用户或组 ID 列表,请将规则更改为 MustRunAs 并指定允许的 ID 范围。
使用 MustRunAs 规则
让我们看看一个例子。为了仅允许容器以用户 ID 2 运行,并将默认文件系统组 ID 和补充组 ID 限制在 2–10 或 20–30(全部包含),你需要在 PodSecurityPolicy 资源中包含以下片段。
列表 13.16. 指定容器必须运行的 ID:psp-must-run-as.yaml
runAsUser: rule: MustRunAs ranges: - min: 2 1 max: 2 1 fsGroup: rule: MustRunAs ranges: - min: 2 2 max: 10 2 - min: 20 2 max: 30 2 supplementalGroups: rule: MustRunAs ranges: - min: 2 2 max: 10 2 - min: 20 2 max: 30 2
-
1 添加一个范围,最小值等于最大值,以设置一个特定的 ID。
-
2 支持多个范围——在这里,组 ID 可以是 2–10 或 20–30(包含)。
如果 pod 规范尝试将其中任何一个字段设置为这些范围之外的值,则 API 服务器将不接受该 pod。要尝试此操作,请删除之前的 PodSecurity-Policy 并从 psp-must-run-as.yaml 文件创建新的一个。
注意
更改策略对现有 pod 没有影响,因为 Pod-Security-Policies 仅在创建或更新 pod 时强制执行。
在策略范围之外部署具有 runAsUser 的 pod
如果你尝试部署之前提到的 pod-as-user-guest.yaml 文件,其中指定容器应以用户 ID 405 运行,API 服务器将拒绝该 pod:
$ kubectl create -f pod-as-user-guest.yaml 错误来自服务器(禁止):创建 "pod-as-user-guest.yaml" 时出错:pods "pod-as-user-guest" 被禁止:无法验证任何 pod 安全策略:[securityContext.runAsUser: 无效值:405:容器 main 上的 UID 不符合所需范围。找到 405,允许:[{2 2}]]
好的,这是显而易见的。但是,如果你部署了一个没有设置 runAs-User 属性的 pod,但用户 ID 已嵌入到容器镜像中(使用 Dockerfile 中的 USER 指令),会发生什么?
使用超出范围的用户 ID 部署容器镜像的 pod
我为书中使用的 Node.js 应用程序创建了一个替代镜像。该镜像配置为容器将以用户 ID 5 运行。该镜像的 Dockerfile 如下所示。
列表 13.17. 包含 USER 指令的 Dockerfile:kubia-run-as-user-5/Dockerfile
FROM node:7 ADD app.js /app.js USER 5 1 ENTRYPOINT ["node", "app.js"]
- 1 从此镜像运行的容器将以用户 ID 5 运行。
我将镜像推送到 Docker Hub,命名为luksa/kubia-run-as-user-5。如果我用这个镜像部署一个 Pod,API 服务器不会拒绝它:
$ kubectl run run-as-5 --image luksa/kubia-run-as-user-5 --restart Never pod "run-as-5" created
与之前不同,API 服务器接受了 Pod,并且 Kubelet 已经运行了其容器。让我们看看容器正在以哪个用户 ID 运行:
$ kubectl exec run-as-5 -- id uid=2(bin) gid=2(bin) groups=2(bin)
正如你所见,容器正在以用户 ID 2 运行,这是你在 PodSecurityPolicy 中指定的 ID。PodSecurityPolicy 可以用来覆盖容器镜像中硬编码的用户 ID。
在 runAsUser 字段中使用 MustRunAsNonRoot 规则
对于runAsUser字段,可以使用一个额外的规则:MustRunAsNonRoot。正如其名所示,它阻止用户部署以 root 身份运行的容器。要么容器规范必须指定一个runAsUser字段,该字段不能为零(零是 root 用户的 ID),要么容器镜像本身必须以非零用户 ID 运行。我们之前解释了为什么这是好事。
13.3.3. 配置允许、默认和禁止的能力
如你所学,容器可以以特权模式或非特权模式运行,你可以通过在每个容器中添加或删除 Linux 内核能力来定义更细粒度的权限配置。有三个字段影响容器可以使用或不能使用的功能:
-
allowedCapabilities -
defaultAddCapabilities -
requiredDropCapabilities
我们首先来看一个例子,然后讨论这三个字段各自的作用。下面的列表显示了一个 PodSecurityPolicy 资源片段,定义了与能力相关的三个字段。
列表 13.18. 在 PodSecurityPolicy 中指定能力:psp-capabilities.yaml
apiVersion: extensions/v1beta1 kind: PodSecurityPolicy spec: allowedCapabilities: 1 - SYS_TIME 1 defaultAddCapabilities: 2 - CHOWN 2 requiredDropCapabilities: 3 - SYS_ADMIN 3 - SYS_MODULE 3 ...
-
1 允许容器添加
SYS_TIME能力。 -
2 自动将
CHOWN能力添加到每个容器。 -
3 要求容器放弃
SYS_ADMIN和SYS_MODULE能力。
注意
SYS_ADMIN能力允许一系列管理操作,而SYS_MODULE能力允许加载和卸载 Linux 内核模块。
指定可以添加到容器中的能力
allowedCapabilities字段用于指定 Pod 作者可以在容器规范中的securityContext.capabilities字段中添加哪些能力。在先前的某个例子中,你向你的容器添加了SYS_TIME能力。如果 Pod-Security-Policy 准入控制插件已被启用,你将无法添加该能力,除非它在 PodSecurityPolicy 中指定,如列表 13.18 所示。
向所有容器添加能力
在defaultAddCapabilities字段下列出的所有功能都将添加到每个部署 Pod 的容器中。如果用户不希望某些容器具有这些功能,他们需要在那些容器的规范中明确删除它们。
列表 13.18 中的示例启用了将CAP_CHOWN功能自动添加到每个容器的功能,从而允许在容器中运行的进程更改容器中文件的拥有权(例如,使用chown命令)。
从容器中删除功能
本例中的最后一个字段是requiredDropCapabilities。我必须承认,一开始这个名称对我来说有些奇怪,但它并不复杂。该字段中列出的功能将自动从每个容器中删除(PodSecurityPolicy 准入控制插件将它们添加到每个容器的security-Context.capabilities.drop字段)。
如果用户尝试创建一个 Pod,并明确添加策略requiredDropCapabilities字段中列出的功能之一,Pod 将被拒绝:
$ kubectl create -f pod-add-sysadmin-capability.yaml Error from server (Forbidden): error when creating "pod-add-sysadmin-capability.yaml": pods "pod-add-sysadmin-capability" is forbidden: unable to validate against any pod security policy: [capabilities.add: Invalid value: "SYS_ADMIN": capability may not be added]
13.3.4. 限制 Pod 可以使用的卷类型
PodSecurityPolicy 资源可以做的最后一件事是定义用户可以向其 Pod 添加哪些卷类型。至少,PodSecurityPolicy 应该允许使用至少emptyDir、configMap、secret、downwardAPI和persistentVolumeClaim卷。此类 PodSecurityPolicy 资源的相关部分如下所示。
列表 13.19. 允许仅使用特定卷类型的 PSP 片段:psp-volumes.yaml
kind: PodSecurityPolicy spec: volumes: - emptyDir - configMap - secret - downwardAPI - persistentVolumeClaim
如果存在多个 PodSecurityPolicy 资源,Pod 可以使用任何策略中定义的卷类型(使用所有volumes列表的并集)。
13.3.5. 为不同的用户和组分配不同的 PodSecurityPolicies
我们提到 PodSecurityPolicy 是一个集群级别的资源,这意味着它不能存储并应用于特定的命名空间。这意味着它总是应用于所有命名空间吗?不,因为那样会使它们相对不可用。毕竟,系统 Pod 通常需要执行常规 Pod 不应执行的操作。
通过 RBAC 机制将不同的策略分配给不同的用户是上一章中描述的。想法是创建尽可能多的策略,并通过创建 ClusterRole 资源并按名称指向它们,使它们对单个用户或组可用。通过将那些 ClusterRoles 绑定到特定的用户或组(使用 ClusterRoleBindings),当 PodSecurityPolicy 接受控制插件需要决定是否接受 pod 定义时,它将只考虑创建 pod 的用户可访问的策略。
你将在下一节练习中看到如何做到这一点。你将首先创建一个额外的 PodSecurityPolicy。
创建允许部署具有特权容器的 PodSecurityPolicy
你将创建一个特殊的 PodSecurityPolicy,它将允许特权用户创建具有特权容器的 pod。以下列表显示了策略的定义。
列表 13.20. 为特权用户创建的 PodSecurityPolicy:psp-privileged.yaml
apiVersion: extensions/v1beta1 kind: PodSecurityPolicy metadata: name: privileged 1 spec: privileged: true 2 runAsUser: rule: RunAsAny fsGroup: rule: RunAsAny supplementalGroups: rule: RunAsAny seLinux: rule: RunAsAny volumes: - '*'
-
1 该策略的名称是“privileged。”
-
2 它允许运行特权容器。
在将此策略发布到 API 服务器后,集群中就有两个策略:
$ kubectl get psp NAME PRIV CAPS SELINUX RUNASUSER FSGROUP ... default false [] RunAsAny RunAsAny RunAsAny ... privileged true [] RunAsAny RunAsAny RunAsAny ...
注意
PodSecurityPolicy 的缩写是 psp。
如你在 PRIV 列中看到的,default 策略不允许运行特权容器,而 privileged 策略则允许。因为你目前以集群管理员身份登录,所以你可以看到所有策略。当创建 pod 时,如果任何策略允许你部署具有特定功能的 pod,API 服务器将接受你的 pod。
现在想象有两个额外的用户正在使用你的集群:Alice 和 Bob。你希望 Alice 只能部署受限(非特权)的 pod,但你希望允许 Bob 也能部署特权 pods。你可以通过确保 Alice 只能使用默认的 PodSecurityPolicy,同时允许 Bob 使用两者来实现这一点。
使用 RBAC 为不同的用户分配不同的 PodSecurityPolicies
在上一章中,你使用了 RBAC 来授予用户访问特定资源类型的权限,但我提到可以通过引用它们的名称来授予对特定资源实例的访问权限。这就是你将用来让用户使用不同的 PodSecurityPolicy 资源的方法。
首先,你将创建两个 ClusterRoles,每个 ClusterRole 允许使用一个策略。你将第一个命名为 psp-default,并在其中允许使用 default PodSecurityPolicy 资源。你可以使用 kubectl create clusterrole 来完成这个任务:
$ kubectl create clusterrole psp-default --verb=use
--resource=podsecuritypolicies --resource-name=default clusterrole "psp-default" created
注意
你使用特殊的动词use而不是get、list、watch或类似的。
如你所见,你通过使用--resource-name选项来引用 PodSecurityPolicy 资源的特定实例。现在,创建另一个名为psp-privileged的 ClusterRole,指向privileged策略:
$ kubectl create clusterrole psp-privileged --verb=use
--resource=podsecuritypolicies --resource-name=privileged clusterrole "psp-privileged" created
现在,你需要将这些策略绑定到用户。如你可能在上一章中记得的,如果你绑定一个授予对集群级资源访问权限的 ClusterRole(PodSecurityPolicy 资源就是这样的),你需要使用 Cluster-RoleBinding 而不是(命名空间级别的)RoleBinding。
你将把psp-default ClusterRole 绑定到所有已认证用户,而不仅仅是 Alice。这是必要的,因为否则没有人可以创建任何 pods,因为 Admission Control 插件会抱怨没有设置策略。所有已认证用户都属于system:authenticated组,所以你需要将 ClusterRole 绑定到该组:
$ kubectl create clusterrolebinding psp-all-users
--clusterrole=psp-default --group=system:authenticated clusterrolebinding "psp-all-users" created
你将只将psp-privileged ClusterRole 绑定到 Bob:
$ kubectl create clusterrolebinding psp-bob
--clusterrole=psp-privileged --user=bob clusterrolebinding "psp-bob" created
作为已认证用户,Alice 现在应该能够访问default PodSecurityPolicy,而 Bob 应该能够访问default和privileged PodSecurityPolicies。Alice 不应该能够创建特权 pods,而 Bob 应该可以。让我们看看这是否正确。
为 kubectl 创建额外用户
但你如何以 Alice 或 Bob 的身份进行认证,而不是当前认证的身份?本书的附录 A 解释了如何使用kubectl与多个集群以及多个上下文一起使用。一个上下文包括用于与集群通信的用户凭据。转向附录 A 以了解更多信息。在这里,我们将展示允许你以 Alice 或 Bob 身份使用kubectl的基本命令。
首先,你将在kubectl的配置中创建两个新用户,使用以下两个命令:
$ kubectl config set-credentials alice --username=alice --password=password User "alice" set. $ kubectl config set-credentials bob --username=bob --password=password User "bob" set.
命令的作用应该是显而易见的。因为你正在设置用户名和密码凭证,kubectl将为这两个用户使用基本 HTTP 认证(其他认证方法包括令牌、客户端证书等)。
以不同用户创建 Pod
你现在可以尝试以 Alice 的身份认证创建一个特权 Pod。你可以通过使用--user选项来告诉kubectl使用哪个用户的凭证:
$ kubectl --user alice create -f pod-privileged.yaml Error from server (Forbidden): error when creating "pod-privileged.yaml": pods "pod-privileged" is forbidden: unable to validate against any pod security policy: [spec.containers[0].securityContext.privileged: Invalid value: true: Privileged containers are not allowed]
如预期的那样,API 服务器不允许 Alice 创建特权 Pod。现在,让我们看看它是否允许 Bob 这样做:
$ kubectl --user bob create -f pod-privileged.yaml pod "pod-privileged" created
然后,你就成功了。你已经成功使用 RBAC 使 Admission Control 插件为不同的用户使用不同的 PodSecurityPolicy 资源。
13.4. 独立 Pod 网络
到目前为止,在本章中,我们已经探讨了适用于单个 Pod 及其容器的许多安全相关配置选项。在本章的剩余部分,我们将探讨如何通过限制哪些 Pod 可以与哪些 Pod 通信来保护 Pod 之间的网络。
这是否可配置取决于集群中使用的容器网络插件。如果网络插件支持,你可以通过创建 NetworkPolicy 资源来配置网络隔离。
NetworkPolicy 应用于匹配其标签选择器的 Pod,并指定哪些源可以访问匹配的 Pod 或从匹配的 Pod 可以访问哪些目标。这是通过入口和出口规则分别配置的。这两种类型的规则只能匹配匹配 Pod 选择器的 Pod、标签匹配命名空间选择器的命名空间中的所有 Pod,或使用无类别域间路由(CIDR)表示法指定的网络 IP 块(例如,192.168.1.0/24)。
我们将查看入口和出口规则以及所有三个匹配选项。
注意
NetworkPolicy 中的入口规则与第五章中讨论的入口资源(chapter 5)无关。
13.4.1. 在命名空间中启用网络隔离
默认情况下,给定命名空间中的 Pod 可以被任何人访问。首先,你需要改变这一点。你将创建一个default-deny NetworkPolicy,这将阻止所有客户端连接到你的命名空间中的任何 Pod。NetworkPolicy 的定义如下所示。
列表 13.21. 一个default-deny NetworkPolicy: network-policy-default-deny.yaml
apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: default-deny spec: podSelector: 1
- 1 空 Pod 选择器匹配同一命名空间中的所有 Pod
当你在某个命名空间中创建此 NetworkPolicy 时,没有人可以连接到该命名空间中的任何 Pod。
注意
集群中使用的 CNI 插件或其他类型的网络解决方案必须支持 NetworkPolicy,否则将对 Pod 之间的连接没有影响。
13.4.2. 允许命名空间中的一些 Pod 连接到服务器 Pod
为了让客户端连接到命名空间中的 Pod,你现在必须明确指出谁可以连接到 Pod。通过“谁”我指的是哪些 Pod。让我们通过一个例子来探讨如何做到这一点。
想象有一个运行在foo命名空间中的 PostgreSQL 数据库 Pod 和一个使用数据库的 web-server Pod。其他 Pod 也位于该命名空间中,你不希望允许它们连接到数据库。为了保护网络,你需要在数据库 Pod 相同的命名空间中创建以下列表中所示的 NetworkPolicy 资源。
列表 13.22. Postgres Pod 的 NetworkPolicy:network-policy-postgres.yaml
apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: postgres-netpolicy spec: podSelector: matchLabels: app: database ingress: - from: podSelector: matchLabels: app: webserver ports: - port: 5432
-
1 此策略保护了带有 app=database 标签的 Pod 的访问。
-
2 它只允许来自带有 app=webserver 标签的 Pod 的入站连接。
-
3 允许连接到此端口的连接。
示例 NetworkPolicy 允许带有app=webserver标签的 Pod 连接到带有app=database标签的 Pod,并且仅在端口 5432 上。其他 Pod 不能连接到数据库 Pod,而且没有人(甚至不是 webserver Pods)可以连接到数据库 Pod 的其他端口。这如图图 13.4 所示。
图 13.4. 只允许某些 Pod 在特定端口上访问其他 Pod 的 NetworkPolicy

客户端 Pod 通常通过 Service 而不是直接连接到 Pod 来连接到服务器 Pod,但这不会改变任何事情。当通过 Service 连接时,也会强制执行 NetworkPolicy。
13.4.3. 在 Kubernetes 命名空间之间隔离网络
现在让我们看看另一个例子,其中多个租户正在使用同一个 Kubernetes 集群。每个租户可以使用多个命名空间,每个命名空间都有一个标签指定它所属的租户。例如,其中一个是 Manning。他们所有的命名空间都被标记为tenant: manning。在他们其中一个命名空间中,他们运行了一个需要对其所有命名空间中运行的任何 Pod 都可见的购物车微服务。显然,他们不希望其他租户访问他们的微服务。
为了保护他们的微服务,他们创建了以下列表中所示的 NetworkPolicy 资源。
列表 13.23. 购物车 Pod 的 NetworkPolicy:network-policy-cart.yaml
apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: shoppingcart-netpolicy spec: podSelector: 1 matchLabels: 1 app: shopping-cart 1 ingress: - from: - namespaceSelector: 2 matchLabels: 2 tenant: manning 2 ports: - port: 80
-
1 此策略适用于标记为 microservice= shopping-cart 的 Pod。
-
2 只有在标记为 tenant=manning 的命名空间中运行的 Pod 才允许访问微服务。
此 NetworkPolicy 确保只有标记为tenant: manning的命名空间中运行的 Pod 可以访问其购物车微服务,如图 13.5 所示 figure 13.5。
图 13.5. 只允许匹配namespaceSelector的命名空间中的 Pod 访问特定 Pod 的 NetworkPolicy。

如果购物车提供商也希望允许其他租户(或许是其合作伙伴公司之一)访问,他们可以创建一个额外的 NetworkPolicy 资源,或者向现有的 NetworkPolicy 添加一个额外的入口规则。
注意
在多租户 Kubernetes 集群中,租户通常不能自己向他们的命名空间添加标签(或注释)。如果他们可以,他们就能绕过基于namespaceSelector的入口规则。
13.4.4. 使用 CIDR 表示法进行隔离
除了指定 Pod 或命名空间选择器来定义谁可以访问 NetworkPolicy 中指定的目标 Pod 外,还可以指定 CIDR 表示法中的 IP 块。例如,要允许上一节中的shopping-cart Pod 只能从 192.168.1.1 到.255 范围内的 IP 访问,你将在下一列表中指定入口规则。
列表 13.24. 在入口规则中指定 IP 块:network-policy-cidr.yaml
ingress: - from: - ipBlock: 1 cidr: 192.168.1.0/24 1
- 1 此入口规则仅允许来自 192.168.1.0/24 IP 块的客户端流量。
13.4.5. 限制一组 Pod 的出站流量
在所有之前的示例中,你都是通过入口规则限制匹配 NetworkPolicy 的 Pod 选择器的 Pod 的入站流量,但你也可以通过出口规则限制它们的出站流量。下一个列表中展示了示例。
列表 13.25. 在 NetworkPolicy 中使用出口规则:network-policy-egress.yaml
spec: podSelector: 1 matchLabels: 1 app: webserver 1 egress: 2 - to: 3 - podSelector: 3 matchLabels: 3 app: database 3
-
1 此策略适用于带有 app=webserver 标签的 Pod。
-
2 它限制了 Pod 的出站流量。
-
3 Webserver Pods 只能连接到带有 app=database 标签的 Pod。
之前列出的 NetworkPolicy 允许带有app=webserver标签的 Pod 只能访问带有app=database标签的 Pod,其他什么都不能访问(无论是其他 Pod,还是任何其他 IP,无论它是在集群内部还是外部)。
13.5. 摘要
在本章中,你学习了如何从 Pod 保护集群节点,以及从其他 Pod 保护 Pod。你了解到
-
Pods 可以使用节点的 Linux 命名空间,而不是使用它们自己的。
-
容器可以被配置为以不同于容器镜像中定义的用户和/或组运行。
-
容器还可以以特权模式运行,允许它们访问节点上其他情况下不对 Pod 暴露的设备。
-
容器可以以只读模式运行,防止进程向容器的文件系统写入(并且只允许它们写入挂载的卷)。
-
可以创建集群级别的 PodSecurityPolicy 资源,以防止用户创建可能危害节点的 Pod。
-
PodSecurityPolicy 资源可以通过 RBAC 的 ClusterRoles 和 ClusterRoleBindings 与特定用户关联。
-
NetworkPolicy 资源用于限制 Pod 的入站和/或出站流量。
在下一章中,你将学习如何限制 Pod 可用的计算资源,以及如何配置 Pod 的服务质量。
第十四章 管理 Pod 的计算资源
本章涵盖
-
为容器请求 CPU、内存和其他计算资源
-
为 CPU 和内存设置硬性限制
-
理解 Pod 服务质量保证
-
在命名空间中为 Pod 设置默认、最小和最大资源
-
限制命名空间中可用的总资源量
到目前为止,你创建 Pod 时并没有关心它们可以消耗多少 CPU 和内存。但正如你将在本章中看到的,设置 Pod 预期消耗的量和最大允许消耗的量是任何 Pod 定义的关键部分。设置这两组参数确保 Pod 只占用 Kubernetes 集群提供的资源中的公平份额,并影响 Pod 在集群中的调度方式。
14.1. 为 Pod 的容器请求资源
在创建一个 Pod 时,你可以指定容器需要的 CPU 和内存量(这些被称为请求)以及它可能消耗的硬性限制(称为限制)。这些限制是针对每个容器单独指定的,而不是针对整个 Pod。Pod 的资源请求和限制是所有容器请求和限制的总和。
14.1.1. 使用资源请求创建 Pod
让我们看看一个示例 Pod 清单,它为其单个容器指定了 CPU 和内存请求,如下所示。
列表 14.1. 具有资源请求的 Pod:requests-pod.yaml
apiVersion: v1 kind: Pod metadata: name: requests-pod spec: containers: - image: busybox command: ["dd", "if=/dev/zero", "of=/dev/null"] name: main 1 resources: 1 requests: 1 cpu: 200m 2 memory: 10Mi 3
-
1 你正在为主容器指定资源请求。
-
2 容器请求 200 毫核(即单个 CPU 核心时间的 1/5)。
-
3 容器还请求 10 兆字节(mebibytes)的内存。
在 Pod 清单中,你的单个容器需要 CPU 核心的五分之一(200 毫核)来正常运行。五个这样的 Pod/容器可以在单个 CPU 核心上足够快地运行。
当你没有指定 CPU 的请求时,你表示你不在乎运行在容器中的进程被分配了多少 CPU 时间。在最坏的情况下,它可能根本得不到任何 CPU 时间(这发生在 CPU 上有其他进程的重需求时)。虽然这可能适用于低优先级的批处理作业,这些作业不是时间敏感的,但它显然不适用于处理用户请求的容器。
在 Pod 规范中,你还在容器中请求了 10 兆字节(mebibytes)的内存。通过这样做,你表示你期望容器内运行的进程最多使用 10 兆字节的 RAM。它们可能使用更少,但你并不期望它们在正常情况下使用超过这个量。稍后在本章中,你将看到如果它们这样做会发生什么。
现在,您将运行 pod。当 pod 启动时,您可以通过在容器内运行 top 命令来快速查看进程的 CPU 消耗,如下所示。
列表 14.2. 从容器内部检查 CPU 和内存使用情况
$ kubectl exec -it requests-pod top 内存:1288116K 已使用,760368K 可用,9196K 共享,25748K 缓冲,814840K 缓存 CPU:9.1% 用户 42.1% 系统 0.0% 网络接口 48.4% 空闲 0.0% I/O 0.0% 中断 0.2% 软中断 负载平均:0.79 0.52 0.29 2/481 10 PID PPID 用户 STAT VSZ %VSZ CPU %CPU 命令 1 0 root R 1192 0.0 1 50.2 dd if /dev/zero of /dev/null 7 0 root R 1200 0.0 0 0.0 top
您在容器中运行的 dd 命令会尽可能多地消耗 CPU,但它只运行一个线程,因此它只能使用一个核心。Minikube 虚拟机,这是本例运行的地方,分配给它两个 CPU 核心。这就是为什么进程显示消耗了整个 CPU 的 50%。
两个核心的百分之五十显然是一个完整的核心,这意味着容器正在使用超过您在 pod 规范中请求的 200 millicores。这是预期的,因为请求并不限制容器可以使用的 CPU 量。您需要指定 CPU 限制才能做到这一点。您稍后会尝试这样做,但首先,让我们看看在 pod 中指定资源请求如何影响 pod 的调度。
14.1.2. 理解资源请求如何影响调度
通过指定资源请求,您指定了 pod 需要的最小资源量。调度器在将 pod 调度到节点时使用这些信息。每个节点可以分配给 pod 一定数量的 CPU 和内存。在调度 pod 时,调度器将只考虑有足够未分配资源来满足 pod 资源要求的节点。如果未分配的 CPU 或内存量小于 pod 请求的量,Kubernetes 不会将 pod 调度到该节点,因为节点无法提供 pod 所需的最小量。
理解调度器如何确定 pod 是否可以放置在节点上
这里重要的是并且有些令人惊讶的是,调度器在调度时不会查看每个单独的资源在确切时间点使用了多少,而是查看节点上已部署的 pod 请求的资源总和。即使现有 pod 可能使用的资源少于它们请求的量,基于实际资源消耗来调度另一个 pod 也会破坏对已部署 pod 给出的保证。
这在图 14.1 中得到了可视化。节点上部署了三个 pod。总共,它们请求了节点 CPU 的 80% 和内存的 60%。图中最右下角的 pod D 不能调度到该节点,因为它请求了 25% 的 CPU,这超过了未分配 CPU 的 20%。这三个 pod 当前只使用了 70% 的 CPU 的事实无关紧要。
图 14.1. 调度器只关心请求,而不是实际使用情况。

理解调度器在为 Pod 选择最佳节点时如何使用 Pod 的请求
你可能还记得从第十一章中提到的,调度器首先过滤掉那些 Pod 无法放置的节点列表,然后根据配置的优先级函数对剩余节点进行排序。其中,两个优先级函数根据请求的资源量对节点进行排名:LeastRequestedPriority和MostRequestedPriority。第一个优先级函数更倾向于具有较少请求资源(具有更多未分配资源)的节点,而第二个优先级函数则正好相反——它更倾向于具有最多请求资源(较少未分配 CPU 和内存)的节点。但是,正如我们之前讨论的,它们都考虑了请求的资源量,而不是实际消耗的资源量。
调度器被配置为只使用这些功能中的一个。你可能想知道为什么有人会想使用MostRequestedPriority函数。毕竟,如果你有一组节点,你通常希望在这些节点之间均匀地分配 CPU 负载。然而,在云基础设施上运行时并非如此,你可以随时添加和删除节点。通过配置调度器使用Most-RequestedPriority函数,你可以确保 Kubernetes 在使用节点时,仍然能够为每个 Pod 提供其请求的 CPU/内存量,同时使用尽可能少的节点。通过紧密打包 Pod,某些节点会空闲出来,可以被移除。因为你为单个节点付费,这可以为你节省费用。
检查节点的容量
让我们看看调度器是如何工作的。你将部署一个 Pod,其请求的资源量是之前的四倍。但在你这样做之前,让我们看看你的节点容量。因为调度器需要知道每个节点有多少 CPU 和内存,Kubelet 会将这些数据报告给 API 服务器,使其通过节点资源可用。你可以通过使用kubectl describe命令来查看,如下面的列表所示。
列表 14.3. 节点的容量和可分配资源
$ kubectl describe nodes Name: minikube ... Capacity: 1 cpu: 2 1 memory: 2048484Ki 1 pods: 110 1 Allocatable: 2 cpu: 2 2 memory: 1946084Ki 2 pods: 110 2 ...
-
1 节点的总体容量
-
2 可分配给 Pod 的资源
输出显示了与节点上可用资源相关的两组金额:节点的容量和可分配资源。容量代表节点的总资源,其中可能并非所有资源都对 Pod 可用。某些资源可能被保留给 Kubernetes 和/或系统组件。调度器仅基于可分配资源量做出决策。
在前面的例子中,名为minikube的节点在一个具有两个核心的虚拟机上运行,没有预留 CPU,因此整个 CPU 都可以分配给 Pod。因此,调度器应该没有问题安排另一个请求 800 毫核的 Pod。
现在运行 Pod。你可以使用代码存档中的 YAML 文件,或者使用以下kubectl run命令运行 Pod:
$ kubectl run requests-pod-2 --image=busybox --restart Never
--requests='cpu=800m,memory=20Mi' -- dd if=/dev/zero of=/dev/null pod "requests-pod-2" created
让我们看看是否已经安排好了:
$ kubectl get po requests-pod-2 NAME READY STATUS RESTARTS AGE requests-pod-2 1/1 Running 0 3m
好的,Pod 已经被安排并正在运行。
创建一个无法在任何节点上运行的 Pod
你现在有两个 Pod 已部署,它们总共请求了 1,000 毫核或正好 1 个核心。因此,你应该还有 1,000 毫核可用于其他 Pod,对吧?你可以部署另一个请求 1,000 毫核资源的 Pod。使用与之前类似的命令:
$ kubectl run requests-pod-3 --image=busybox --restart Never
--requests='cpu=1,memory=20Mi' -- dd if=/dev/zero of=/dev/null pod "requests-pod-2" created
注意
这次你指定的是整核 CPU 请求(cpu=1),而不是毫核(cpu=1000m)。
到目前为止,一切顺利。Pod 已被 API 服务器接受(你可能会记得,在前一章中,如果 Pod 以任何方式无效,API 服务器可以拒绝 Pod)。现在,检查 Pod 是否正在运行:
$ kubectl get po requests-pod-3 NAME READY STATUS RESTARTS AGE requests-pod-3 0/1 Pending`` 0 4m
即使你等待一段时间,Pod 仍然处于挂起状态。你可以通过使用kubectl describe命令来查看更多关于这种情况的信息,如下所示。
列表 14.4. 使用kubectl describe pod检查 Pod 为什么处于挂起状态
$ kubectl describe po requests-pod-3 Name: requests-pod-3 Namespace: default Node: / 1 ... Conditions: Type Status PodScheduled False 2 ... Events: ... Warning FailedScheduling``No nodes are available 3 that match all of the 3 following predicates:: 3``Insufficient cpu (1). 3
-
1 没有节点与该 Pod 关联。
-
2 Pod 没有被安排。
-
3 由于 CPU 不足,调度失败。
输出显示 Pod 尚未被安排,因为它由于你的单个节点上 CPU 不足而无法在任何节点上运行。但为什么会这样呢?所有三个 Pod 的 CPU 请求总和为 2,000 毫核或正好两个核心,这正是你的节点可以提供的。问题出在哪里?
确定 Pod 为什么没有被安排
你可以通过检查节点资源来找出 pod 为什么没有被调度。再次使用 kubectl describe node 命令,并仔细检查以下列表的输出。
列表 14.5. 使用 kubectl describe node 检查节点上的已分配资源
$ kubectl describe node Name: minikube ... 非终止 Pods: (总共 7 个) Namespace Name CPU Requ. CPU Lim. Mem Req. Mem Lim. --------- ---- ---------- -------- --------- -------- default requests-pod 200m (10%) 0 (0%) 10Mi (0%) 0 (0%) default requests-pod-2 800m (40%) 0 (0%) 20Mi (1%) 0 (0%) kube-system dflt-http-b... 10m (0%) 10m (0%) 20Mi (1%) 20Mi (1%) kube-system kube-addon-... 5m (0%) 0 (0%) 50Mi (2%) 0 (0%) kube-system kube-dns-26... 260m (13%) 0 (0%) 110Mi (5%) 170Mi (8%) kube-system kubernetes-... 0 (0%) 0 (0%) 0 (0%) 0 (0%) kube-system nginx-ingre... 0 (0%) 0 (0%) 0 (0%) 0 (0%) 已分配资源: (总限制可能超过 100%,即超配。) CPU 请求``CPU 限制 内存请求 内存限制 ------------ ---------- --------------- ------------- 1275m (63%)`` 10m (0%) 210Mi (11%) 190Mi (9%)
如果你查看列表的左下角,你会看到正在运行的 pods 请求了总共 1,275 个毫核心,比你部署的前两个 pods 请求的多了 275 个毫核心。有些东西正在消耗额外的 CPU 资源。
你可以在上一列表中的 pods 列表中找到罪魁祸首。在 kube-system 命名空间中有三个 pods 明确请求了 CPU 资源。这些 pods 加上你的两个 pods,只剩下 725 个毫核心可用于额外的 pods。因为你的第三个 pod 请求了 1,000 个毫核心,调度器不会将其调度到这个节点,因为这会使节点超配。
释放资源以使 pod 被调度
只有当释放足够的 CPU 资源时(例如删除前两个 pods 中的一个),pod 才会被调度。如果你删除第二个 pod,调度器会通过 第十一章 中描述的监视机制通知删除操作,并在第二个 pod 终止后立即调度你的第三个 pod。这在下述列表中显示。
列表 14.6. 删除另一个 pod 后 pod 被调度
$ kubectl delete po requests-pod-2 pod "requests-pod-2" 已删除 $ kubectl get po NAME READY STATUS RESTARTS AGE requests-pod 1/1 运行中 0 2h requests-pod-2 1/1 正在终止``0 1h requests-pod-3 0/1 挂起``0 1h $ kubectl get po NAME READY STATUS RESTARTS AGE requests-pod 1/1 运行中 0 2h requests-pod-3 1/1 运行中`` 0 1h
在所有这些示例中,你指定了内存请求,但在调度中它并没有发挥作用,因为你的节点有足够的可分配内存来容纳所有 Pod 的请求。调度器以相同的方式处理 CPU 和内存请求,但与内存请求不同,Pod 的 CPU 请求在 Pod 运行时也发挥作用。你将在下一节中了解这一点。
14.1.3. 理解 CPU 请求如何影响 CPU 时间共享
现在,你的集群中有两个 Pod 正在运行(你可以暂时忽略系统 Pod,因为它们大部分是空闲的)。一个请求了 200 毫芯,另一个请求了五倍于此。在章节开头,我们说 Kubernetes 区分资源请求和限制。你还没有定义任何限制,所以这两个 Pod 在 CPU 消耗方面没有任何限制。如果每个 Pod 内部的进程尽可能多地消耗 CPU 时间,每个 Pod 将获得多少 CPU 时间?
CPU 请求不仅影响调度,还决定了剩余(未使用)的 CPU 时间如何在 Pod 之间分配。因为你的第一个 Pod 请求了 200 毫芯的 CPU,而另一个请求了 1000 毫芯,任何未使用的 CPU 将以 1 到 5 的比例在两个 Pod 之间分配,如图 14.2 所示。如果两个 Pod 都尽可能多地消耗 CPU,第一个 Pod 将获得六分之一或 16.7%的 CPU 时间,而另一个 Pod 将获得剩余的五分之六或 83.3%。


但如果一个容器想要尽可能多地使用 CPU,而另一个容器在某个时刻处于空闲状态,第一个容器将被允许使用整个 CPU 时间(减去第二个容器使用的少量时间,如果有的话)。毕竟,如果没有人使用,使用所有可用的 CPU 是有意义的,对吧?一旦第二个容器需要 CPU 时间,它将获得它,第一个容器将被限制。
14.1.4. 定义和请求自定义资源
Kubernetes 还允许你向节点添加自己的自定义资源,并在 Pod 的资源请求中请求它们。最初这些被称为不透明整数资源,但在 Kubernetes 1.8 版本中被扩展资源所取代。
首先,显然你需要通过将其添加到节点对象的capacity字段中,让 Kubernetes 知道你的自定义资源。这可以通过执行PATCH HTTP 请求来完成。资源名称可以是任何东西,例如example.org/my-resource,只要它不以kubernetes.io域开头。数量必须是一个整数(例如,你不能将其设置为 100 毫秒,因为 0.1 不是一个整数;但你可以将其设置为 1000m 或 2000m,或者简单地设置为 1 或 2)。值将自动从capacity复制到allocatable字段。
然后,在创建 Pod 时,你可以在容器规范中的 resources.requests 字段下指定相同的资源名称和请求的数量,或者在像之前示例中那样使用 kubectl run 时使用 --requests。调度器将确保 Pod 只部署到具有所需自定义资源数量的节点。显然,每个部署的 Pod 都会减少资源的可分配单元数量。
自定义资源的例子可以是节点上可用的 GPU 单元数量。需要使用 GPU 的 Pod 在其请求中指定这一点。然后调度器确保 Pod 只被调度到至少有一个未分配 GPU 的节点。
14.2. 限制容器可用的资源
在一个 Pod 中为容器设置资源请求确保每个容器都能获得其所需的最小资源量。现在让我们看看硬币的另一面——容器将被允许消耗的最大资源量。
14.2.1. 为容器可使用的资源数量设置硬限制
我们已经看到,如果其他所有进程都在空闲状态,容器可以消耗掉所有的 CPU。但你可能希望防止某些容器使用超过特定数量的 CPU。而且你总是希望限制容器可以消耗的内存量。
CPU 是一种可压缩资源,这意味着容器使用的数量可以被限制,而不会以不利的方式影响容器中运行的进程。内存显然不同——它是不可压缩的。一旦进程被分配了一块内存,这块内存就不能从它那里拿走,直到进程本身释放它。这就是为什么你需要限制容器可以获得的内存最大量。
如果不限制内存,运行在工作节点上的容器(或 Pod)可能会消耗掉所有可用的内存,并影响节点上的所有其他 Pod 以及任何新调度到该节点的 Pod(记住,新 Pod 是根据内存请求而不是实际内存使用来调度到节点的)。一个故障或恶意 Pod 实际上可以使整个节点无法使用。
创建具有资源限制的 Pod
为了防止这种情况发生,Kubernetes 允许你为每个容器指定资源限制(与资源请求一样,实际上以相同的方式)。以下列表显示了一个具有资源限制的示例 Pod 清单。
列表 14.7. 具有硬限制的 CPU 和内存的 Pod:limited-pod.yaml
apiVersion: v1 kind: Pod metadata: name: limited-pod spec: containers: - image: busybox command: ["dd", "if=/dev/zero", "of=/dev/null"] name: main resources: limits: cpu: 1 memory: 20Mi
-
1 为容器指定资源限制
-
2 此容器将被允许使用最多 1 个 CPU 核心。
-
3 容器将被允许使用最多 20 兆字节的内存。
这个 pod 的容器为 CPU 和内存都配置了资源限制。容器内运行的进程或进程将不允许消耗超过 1 个 CPU 核心和 20 兆字节的内存。
注意
因为您没有指定任何资源请求,所以它们将被设置为与资源限制相同的值。
超出限制的过度提交
与资源请求不同,资源限制不受节点可分配资源数量的约束。节点上所有 pod 的所有限制总和允许超过节点容量的 100% (图 14.3)。重申一下,资源限制可以被过度提交。这有一个重要的后果——当节点资源使用率达到 100% 时,某些容器将需要被杀死。
图 14.3. 节点上所有 pod 的资源限制总和可能超过节点容量的 100%。

您将在第 14.3 节中看到 Kubernetes 如何决定杀死哪些容器,但即使容器试图使用超过其资源限制指定的更多资源,也可以杀死单个容器。您将在下一部分了解更多关于这一点。
14.2.2. 超出限制
当一个在容器中运行的过程试图使用比允许的更多资源时,会发生什么?
您已经了解到 CPU 是一种可压缩的资源,当进程不在等待 I/O 操作时,它想要消耗所有 CPU 时间是很自然的。正如您所学的,进程的 CPU 使用率是受限制的,因此当为容器设置 CPU 限制时,进程不会获得超过配置限制的更多 CPU 时间。
对于内存来说,情况不同。当进程试图分配超过其限制的内存时,进程将被杀死(容器被称为 OOMKilled,其中 OOM 代表内存不足)。如果 pod 的重启策略设置为 Always 或 OnFailure,进程将立即重启,因此您可能甚至没有注意到它被杀死。但如果它继续超过内存限制并被杀死,Kubernetes 将开始以增加的重启间隔重启它。在这种情况下,您将看到 CrashLoopBackOff 状态:
$ kubectl get po NAME READY STATUS RESTARTS AGE memoryhog 0/1 CrashLoopBackOff`` 3 1m
CrashLoopBackOff 状态并不意味着 Kubelet 已经放弃。这意味着在每次崩溃后,Kubelet 都会增加在重启容器之前的时间间隔。第一次崩溃后,它立即重启容器,然后如果再次崩溃,将等待 10 秒后再重启它。在随后的崩溃中,这个延迟将以指数方式增加到 20、40、80 和 160 秒,并最终限制为 300 秒。一旦间隔达到 300 秒的限制,Kubelet 将无限期地每五分钟重启容器,直到 pod 停止崩溃或被删除。
要检查容器崩溃的原因,你可以检查 pod 的日志和/或使用 kubectl describe pod 命令,如下所示。
列表 14.8. 检查容器为何以 kubectl describe pod 终止
$ kubectl describe pod Name: memoryhog ... Containers: main: ... State: Terminated 1 Reason: OOMKilled 1 Exit Code: 137 Started: Tue, 27 Dec 2016 14:55:53 +0100 Finished: Tue, 27 Dec 2016 14:55:58 +0100 Last State: Terminated 2 Reason: OOMKilled 2 Exit Code: 137 Started: Tue, 27 Dec 2016 14:55:37 +0100 Finished: Tue, 27 Dec 2016 14:55:50 +0100 Ready: False ...
-
1 当前容器被杀死是因为它内存不足(OOM)。
-
2 之前的容器也是因为 OOM 被杀死的
OOMKilled 状态告诉你,容器被杀死是因为内存不足。在先前的列表中,容器超过了其内存限制并被立即杀死。
如果你不希望容器被杀死,不要设置太低的内存限制。但即使容器没有超过其限制,它们也可能被 OOMKilled。你将在 第 14.3.2 节 中看到原因,但首先,让我们讨论一下大多数用户在第一次开始为他们的容器指定限制时通常会感到意外的某个问题。
14.2.3. 理解容器中的应用程序如何看到限制
如果你还没有从 列表 14.7 部署 pod,现在就部署它:
$ kubectl create -f limited-pod.yaml pod "limited-pod" created
现在,在容器中运行 top 命令,就像你在本章开头所做的那样。命令的输出如下所示。
列表 14.9. 在 CPU 和内存受限的容器中运行 top 命令
$ kubectl exec -it limited-pod top``Mem: 1450980K used, 597504K free``, 22012K shrd, 65876K buff, 857552K cached CPU: 10.0% usr 40.0% sys`` 0.0% nic 50.0% idle 0.0% io 0.0% irq 0.0% sirq Load average: 0.17 1.19 2.47 4/503 10 PID PPID USER STAT VSZ %VSZ CPU %CPU COMMAND 1 0 root R 1192 0.0 1 49.9 dd if /dev/zero of /dev/null 5 0 root R 1196 0.0 0 0.0 top
首先,让我提醒你,pod 的 CPU 限制设置为 1 个核心,其内存限制设置为 20 MiB。现在,仔细检查 top 命令的输出。有什么让你觉得奇怪的吗?
查看已使用和空闲的内存量。这些数字与你为容器设置的 20 MiB 限制相去甚远。同样,你将 CPU 限制设为单个核心,但看起来主进程只使用了 50% 的可用 CPU 时间,尽管 dd 命令在你使用它的方式下,通常会用尽它所有的 CPU。这是怎么回事?
理解容器总是看到节点的内存,而不是容器的内存
top命令显示了容器运行的整个节点的内存量。即使你为容器设置了内存限制,容器也不会意识到这个限制。
这对任何查找系统上可用的内存量并使用该信息来决定它想要保留多少内存的应用程序都有不幸的影响。
当运行 Java 应用程序时,这个问题尤为明显,尤其是如果你没有使用-Xmx选项为 Java 虚拟机指定最大堆大小。在这种情况下,JVM 将根据主机的总内存而不是容器的可用内存来设置最大堆大小。当你将你的容器化 Java 应用程序在你的笔记本电脑上的 Kubernetes 集群中运行时,问题不会显现出来,因为你在 Pod 上设置的内存限制与你的笔记本电脑上总内存之间的差异并不大。
但是,当你将你的 Pod 部署到生产系统上,其中节点拥有更多的物理内存时,JVM 可能会超过你配置的容器内存限制,并会被OOMKilled。
如果你认为正确设置-Xmx选项可以解决这个问题,那么很遗憾,你错了。-Xmx选项仅限制堆大小,但对 JVM 的堆外内存没有任何作用。幸运的是,Java 的新版本通过考虑配置的容器限制来减轻这个问题。
理解容器也会看到所有节点的 CPU 核心
就像内存一样,容器也会看到所有节点的 CPU,无论为容器配置的 CPU 限制是多少。将 CPU 限制设置为单个核心并不会神奇地只向容器暴露一个 CPU 核心。所有 CPU 限制所做的只是限制容器可以使用的 CPU 时间量。
在 64 核心 CPU 上运行的单核心 CPU 限制的容器将获得整体 CPU 时间的 1/64。即使其限制设置为单个核心,容器的进程也不会只在单个核心上运行。在不同的时间点,其代码可能会在不同的核心上执行。
这并没有什么问题,对吧?虽然通常情况下是这样的,但至少存在一种情况,这种情况下这种情况是灾难性的。
某些应用程序会查找系统上的 CPU 数量来决定它们应该运行多少个工作线程。同样,这样的应用程序在开发笔记本电脑上运行良好,但当部署到具有更多核心的节点上时,它将启动过多的线程,所有线程都在竞争(可能是)有限的 CPU 时间。此外,每个线程都需要额外的内存,导致应用程序的内存使用量激增。
你可能想使用 Downward API 将 CPU 限制传递给容器,并使用它而不是依赖于你的应用程序在系统上可以看到的 CPU 数量。你也可以直接访问 cgroups 系统,通过读取以下文件来获取配置的 CPU 限制:
-
/sys/fs/cgroup/cpu/cpu.cfs_quota_us
-
/sys/fs/cgroup/cpu/cpu.cfs_period_us
14.3. 理解 pod QoS 类别
我们已经提到,资源限制可以被过度承诺,并且节点不一定能为其所有 pod 提供其资源限制中指定的资源量。
想象有两个 pod,其中 pod A 正在使用节点内存的 90%,然后 pod B 突然需要比之前使用更多的内存,而节点无法提供所需的内存量。应该杀死哪个容器?应该杀死 pod B,因为其内存请求无法得到满足,还是应该杀死 pod A 以释放内存,以便提供给 pod B?
显然,这取决于具体情况。Kubernetes 无法独立做出正确的决定。你需要一种方式来指定在这种情况下哪些 pod 具有优先级。Kubernetes 通过将 pod 分为三个服务质量(QoS)类别来实现这一点:
-
BestEffort(最低优先级) -
Burstable -
Guaranteed(最高)
14.3.1. 为 pod 定义 QoS 类
你可能期望这些类别可以通过清单中的单独字段来分配,但实际上并非如此。QoS 类别是从 pod 容器的资源请求和限制的组合中派生出来的。以下是具体方法。
将 pod 分配到 BestEffort 类
最低优先级的 QoS 类别是 BestEffort 类。它被分配给没有任何请求或限制设置的 pod(在任何容器中)。这是在前面章节中创建的所有 pod 分配的 QoS 类别。在这些 pod 中运行的容器没有任何资源保证。在最坏的情况下,它们可能几乎得不到任何 CPU 时间,并且当需要为其他 pod 释放内存时,它们将是首先被杀死的。但是,由于 BestEffort pod 没有设置内存限制,如果可用内存足够,其容器可以使用尽可能多的内存。
将 pod 分配到 Guaranteed 类
在另一端是 Guaranteed QoS 类别。这个类别分配给那些容器请求等于所有资源限制的 pod。对于一个 pod 的类别要成为 Guaranteed,需要满足以下三个条件:
-
需要为 CPU 和内存设置请求和限制。
-
需要为每个容器设置这些。
-
它们需要相等(限制需要与每个容器中每个资源的请求相匹配)。
由于容器的资源请求(如果没有明确设置),默认为限制,因此为所有资源(对于 pod 中的每个容器)指定限制就足够使 pod 成为 Guaranteed。这些 pod 中的容器获得请求的资源量,但不能消耗额外的资源(因为它们的限制不高于它们的请求)。
将 Burstable QoS 类别分配给 pod
在 BestEffort 和 Guaranteed 之间是 Burstable QoS 类别。所有其他 pod 都属于这个类别。这包括容器限制与其请求不匹配的单容器 pod,以及至少有一个容器指定了资源请求但没有指定限制的所有 pod。它还包括一个容器的请求与其限制相匹配,但另一个容器没有指定请求或限制的 pod。Burstable pod 会获得其请求的资源,但在需要时可以额外使用资源(最多达到限制)。
理解请求和限制之间的关系如何定义 QoS 类别
所有三个 QoS 类别及其与请求和限制的关系都在图 14.4 中展示。
图 14.4. 资源请求、限制和 QoS 类别

考虑一个 pod 的 QoS 类别可能会让你感到困惑,因为它涉及到多个容器、多个资源以及请求和限制之间所有可能的关系。如果你从容器级别的 QoS(尽管 QoS 类别是 pod 的属性,而不是容器的属性)开始思考,然后从容器的 QoS 类别推导出 pod 的 QoS 类别,那就更容易理解了。
确定容器的 QoS 类别
表 14.1 展示了基于单个容器上定义的资源请求和限制的 QoS 类别。对于单容器 pod,QoS 类别也适用于 pod。
表 14.1. 基于资源请求和限制的单容器 pod 的 QoS 类别
| CPU 请求与限制 | 内存请求与限制 | 容器 QoS 类别 |
|---|---|---|
| 未设置 | 未设置 | BestEffort |
| 未设置 | 请求 < 限制 | 可扩展 |
| 未设置 | 请求 = 限制 | 可扩展 |
| 请求 < 限制 | 未设置 | 可扩展 |
| 请求 < 限制 | 请求 < 限制 | 可扩展 |
| 请求 < 限制 | 请求 = 限制 | 可扩展 |
| 请求 = 限制 | 请求 = 限制 | Guaranteed |
注意
如果只设置了请求,但没有设置限制,请参考请求小于限制的表格行。如果只设置了限制,请求默认为限制值,因此请参考请求等于限制的行。
确定具有多个容器的 pod 的 QoS 类别
对于多容器 pod,如果所有容器具有相同的 QoS 类别,那么这也是 pod 的 QoS 类别。如果至少有一个容器具有不同的类别,那么 pod 的 QoS 类别是 Burstable,无论容器的类别是什么。表 14.2 展示了两个容器 pod 的 QoS 类别如何与其两个容器的类别相关。你可以轻松地将这个扩展到具有两个以上容器的 pod。
表 14.2. 从其容器的类别推导出的 Pod 的 QoS 类别
| 容器 1 QoS 类别 | 容器 2 QoS 类别 | Pod 的 QoS 类别 |
|---|---|---|
| BestEffort | BestEffort | BestEffort |
| BestEffort | 可扩展 | 可扩展 |
| BestEffort | Guaranteed | 可扩展 |
| 可扩展 | 可扩展 | 可扩展 |
| Burstable | Guaranteed | Burstable |
| Guaranteed | Guaranteed | Guaranteed |
注意
当运行kubectl describe pod时,pod 的 QoS 类别会显示出来,并在 pod 的 YAML/JSON 表示中的status.qosClass字段中。
我们已经解释了如何确定 QoS 类别,但我们还需要看看它们是如何在过载系统中确定哪个容器会被杀死的。
14.3.2. 理解内存低时哪个进程会被杀死
当系统过载时,服务质量(QoS)类别决定了哪个容器首先被杀死,以便释放的资源可以分配给优先级更高的 pod。首先被杀死的是BestEffort类别的 pod,其次是Burstable pod,最后是Guaranteed pod,只有当系统进程需要内存时,Guaranteed pod 才会被杀死。
理解 QoS 类别的排列方式
让我们看看图 14.5 中显示的示例。想象有两个单容器 pod,第一个 pod 具有BestEffort QoS 类别,第二个 pod 的类别是Burstable。当节点的全部内存已经达到最大值,并且节点上的某个进程尝试分配更多内存时,系统需要杀死一个进程(可能是尝试分配额外内存的进程)以遵守分配请求。在这种情况下,运行在BestEffort pod 中的进程将始终在运行在Burstable pod 中的进程之前被杀死。
图 14.5. 哪些 pod 先被杀死

显然,在杀死任何Guaranteed pod 的进程之前,BestEffort pod 的进程也会被杀死。同样,在杀死Guaranteed pod 之前,Burstable pod 的进程也会被杀死。但如果只有两个Burstable pod 呢?显然,选择过程需要优先考虑其中一个。
理解具有相同 QoS 类别的容器是如何处理的
每个正在运行的过程都有一个内存不足(OOM)分数。系统通过比较所有运行进程的 OOM 分数来选择要杀死的进程。当需要释放内存时,分数最高的进程会被杀死。
OOM 分数由两件事计算得出:进程消耗的可用内存百分比和一个固定的 OOM 分数调整,该调整基于 pod 的 QoS 类别和容器请求的内存。当存在两个单容器 pod,且两者都属于Burstable类别时,系统将杀死使用其请求内存比例更高的那个。这就是为什么在图 14.5 中,使用 90%请求内存的 pod B 在只使用 70%的 pod C 之前被杀死,尽管 pod B 使用的内存兆字节比 pod C 多。
这表明您需要留心的不只是请求和限制之间的关系,还要注意请求和预期实际内存消耗之间的关系。
14.4. 为每个命名空间中的 pod 设置默认请求和限制
我们已经探讨了如何为每个单独的容器设置资源请求和限制。如果你没有设置它们,容器就会受制于所有其他指定了资源请求和限制的容器。为每个容器设置请求和限制是一个好主意。
14.4.1. 介绍 LimitRange 资源
除了为每个容器执行此操作外,您还可以通过创建 Limit-Range 资源来完成此操作。它允许您指定(对于每个命名空间)您可以为每个资源在容器上设置的最低和最高限制,以及未明确指定请求的容器的默认资源请求,如图 14.6 所示。
图 14.6. LimitRange 用于验证和默认 pod。

LimitRange 资源由 LimitRanger 接受控制插件使用(我们在第十一章中解释了这些插件)。当一个 pod 清单被提交到 API 服务器时,LimitRanger 插件会验证 pod 规范。如果验证失败,则立即拒绝清单。正因为如此,LimitRange 对象的一个很好的用途是防止用户创建比集群中任何节点都大的 pod。如果没有这样的 LimitRange,API 服务器会欣然接受 pod,但随后永远不会调度它。
LimitRange 资源中指定的限制适用于与 LimitRange 对象在同一命名空间中创建的每个单独的 pod/容器或其他类型的对象。它们不会限制命名空间中所有 pod 可用资源的总量。这通过 ResourceQuota 对象来指定,该对象在第 14.5 节中进行了说明。
14.4.2. 创建 LimitRange 对象
让我们看看一个完整的 LimitRange 示例,看看各个属性的作用。以下列表显示了 LimitRange 资源的完整定义。
列表 14.10. LimitRange 资源:limits.yaml
apiVersion: v1 kind: LimitRange metadata: name: example spec: limits: - type: Pod min: cpu: 50m memory: 5Mi max: cpu: 1 memory: 1Gi - type: Container defaultRequest: cpu: 100m memory: 10Mi default: cpu: 200m memory: 100Mi min: cpu: 50m memory: 5Mi max: cpu: 1 memory: 1Gi maxLimitRequestRatio: cpu: 4 memory: 10 - type: PersistentVolumeClaim min: storage: 1Gi max: storage: 10Gi
-
1 指定整个 pod 的限制
-
2 pod 的容器可以请求的总最小 CPU 和内存
-
3 每个 pod 的容器可以请求(和限制)的最大 CPU 和内存
-
4 容器限制在此行以下指定。
-
5 将应用于未明确指定它们的容器的 CPU 和内存的默认请求
-
6 未指定限制的容器的默认限制
-
容器可以拥有的最小和最大请求/限制
-
每个资源限制与请求之间的最大比率是 8
-
9 LimitRange 还可以设置 PVC 可以请求的最小和最大存储量。
如前例所示,可以配置整个 Pod 的最小和最大限制。它们适用于 Pod 中所有容器的请求和限制的总和。
在容器级别,您不仅可以设置最小和最大值,还可以设置默认资源请求(defaultRequest)和默认限制(default),这些将应用于每个未明确指定的容器。
除了最小、最大和默认值之外,您甚至可以设置限制与请求之间的最大比率。前面的列表将 CPU maxLimitRequestRatio 设置为 4,这意味着容器的 CPU 限制将不允许超过其 CPU 请求的四倍。如果 CPU 限制设置为 801 毫核或更高,则请求 200 毫核的容器将不被接受。对于内存,最大比率设置为 10。
在第六章中,我们探讨了 PersistentVolumeClaims (PVC),它允许您以类似于 Pod 的容器请求 CPU 和内存的方式请求一定量的持久存储。同样地,您应该限制单个 PVC 可以请求的存储量。LimitRange 对象允许您这样做,如示例底部所示。
示例显示了一个包含所有内容的单个 LimitRange 对象,但您也可以根据偏好将它们拆分为多个对象(例如,一个用于 Pod 限制,另一个用于容器限制,还有一个用于 PVC)。在验证 Pod 或 PVC 时,来自多个 LimitRange 对象的所有限制都将合并。
由于 LimitRange 对象中配置的验证(和默认值)是在 API 服务器接收到新的 Pod 或 PVC 清单时执行的,因此如果您之后修改了限制,现有的 Pod 和 PVC 将不会被重新验证——新的限制将仅适用于之后创建的 Pod 和 PVC。
14.4.3. 执行限制
在设置了限制之后,您现在可以尝试创建一个请求比 LimitRange 允许的更多 CPU 的 Pod。您可以在代码存档中找到 Pod 的 YAML。下一个列表仅显示与讨论相关的部分。
列表 14.11. 一个 CPU 请求大于限制的 Pod:limits-pod-too-big.yaml
资源: 请求: cpu: 2
Pod 的单个容器请求了两个 CPU,这超过了您之前在 LimitRange 中设置的极限。创建 Pod 的结果如下:
$ kubectl create -f limits-pod-too-big.yaml Error from server (Forbidden): error when creating "limits-pod-too-big.yaml": pods "too-big" is forbidden: [ maximum cpu usage per Pod is 1, but request is 2., maximum cpu usage per Container is 1, but request is 2.]
我稍微修改了输出,使其更易于阅读。服务器错误信息的好处是它列出了导致 pods 被拒绝的所有原因,而不仅仅是第一个遇到的原因。正如你所看到的,pods 被拒绝了两个原因:你为容器请求了两个 CPU,但容器的最大 CPU 限制是一个。同样,整个 pods 请求了两个 CPU,但最大限制是一个 CPU(如果这是一个多容器 pods,即使每个容器请求的 CPU 少于最大量,它们加在一起仍然需要请求少于两个 CPU 才能通过 pods 的最大 CPU 限制)。
14.4.4. 应用默认资源请求和限制
现在我们也来看看默认资源请求和限制是如何设置在未指定它们的容器上的。再次部署kubia-manual pod,来自第三章:
$ kubectl create -f ../Chapter03/kubia-manual.yaml pod "kubia-manual" created
在设置你的 LimitRange 对象之前,所有的 pods 都是没有资源请求或限制被创建的,但现在在创建 pods 时会自动应用默认值。你可以通过描述kubia-manual pod 来确认这一点,如下所示。
列表 14.12. 检查自动应用到 pods 上的限制
$ kubectl describe po kubia-manual Name: kubia-manual ... Containers: kubia: Limits: cpu: 200m memory: 100Mi Requests: cpu: 100m memory: 10Mi
容器的请求和限制与你在 LimitRange 对象中指定的相匹配。如果你在另一个命名空间中使用了不同的 LimitRange 规范,那么在该命名空间中创建的 pods 将显然具有不同的请求和限制。这允许管理员为每个命名空间配置 pods 的默认、最小和最大资源。如果命名空间被用来分隔不同的团队,或者用来分隔在同一 Kubernetes 集群中运行的开发、QA、预发布和生产 pods,那么在每个命名空间中使用不同的 LimitRange 可以确保只能在某些命名空间中创建大型 pods,而其他则被限制在较小的 pods。
但请记住,LimitRange 中配置的限制仅适用于每个单独的 pods/容器。仍然有可能创建许多 pods 并消耗掉集群中所有的资源。LimitRanges 不能提供对此类情况的保护。另一方面,Resource-Quota 对象可以。你将在下一部分学习它们。
14.5. 限制命名空间中可用的总资源
正如您所看到的,LimitRanges 仅适用于单个 Pod,但集群管理员还需要一种方法来限制命名空间中可用的资源总量。这是通过创建资源配额对象来实现的。
14.5.1. 介绍资源配额对象
在第十章中,我们提到 API 服务器内部运行的几个准入控制插件会验证 Pod 是否可以创建。在前一节中,我说 LimitRanger 插件强制执行 LimitRange 资源中配置的策略。同样,资源配额准入控制插件会检查正在创建的 Pod 是否会超出配置的资源配额。如果是这种情况,Pod 的创建将被拒绝。因为资源配额在 Pod 创建时生效,所以资源配额对象只会影响在资源配额对象创建之后创建的 Pod——创建它对现有 Pod 没有影响。
资源配额(ResourceQuota)限制了命名空间中可以消耗的计算资源量以及持久卷声明(PersistentVolumeClaims)的存储量。它还可以限制用户在命名空间内可以创建的 Pod、声明和其他 API 对象的数量。由于你到目前为止主要处理的是 CPU 和内存,让我们先看看如何为它们指定配额。
创建 CPU 和内存的资源配额
通过创建资源配额对象,定义了命名空间中所有 Pod 允许消耗的总 CPU 和内存量,如下所示列表。
列表 14.13. CPU 和内存的资源配额:quota-cpu-memory.yaml
apiVersion: v1 kind: ResourceQuota metadata: name: cpu-and-mem spec: hard: requests.cpu: 400m requests.memory: 200Mi limits.cpu: 600m limits.memory: 500Mi
与为每个资源定义单个总量不同,您为 CPU 和内存的请求和限制分别定义单独的总量。您会注意到结构与 LimitRange 的结构略有不同。在这里,所有资源的请求和限制都在一个地方定义。
此资源配额将命名空间中 Pod 请求的最大 CPU 量设置为 400 毫核。命名空间中最大总 CPU 限制设置为 600 毫核。对于内存,最大总请求设置为 200 MiB,而限制设置为 500 MiB。
资源配额对象应用于其创建的命名空间,就像 Limit-Range 一样,但它应用于所有 Pod 的资源请求和限制的总量,而不是每个单独的 Pod 或容器,如图 14.7 所示。
图 14.7. LimitRanges 应用于单个 Pod;资源配额应用于命名空间中的所有 Pod。

检查配额和配额使用情况
在将资源配额对象发布到 API 服务器后,您可以使用kubectl describe命令查看已使用的配额量,如下所示。
列表 14.14. 使用kubectl describe quota检查 ResourceQuota
$ kubectl describe quota Name: cpu-and-mem Namespace: default Resource Used Hard -------- ---- ---- limits.cpu 200m 600m limits.memory 100Mi 500Mi requests.cpu 100m 400m requests.memory 10Mi 200Mi
我只运行了kubia-manual Pod,所以Used列与它的资源请求和限制相匹配。当我运行额外的 Pod 时,它们的请求和限制会被加到已使用量中。
与 ResourceQuota 一起创建 LimitRange
创建 ResourceQuota 时有一个注意事项,那就是你也会想同时创建一个 Limit-Range 对象。在你的情况下,你已经从上一节配置了 LimitRange,但如果你没有配置,你就无法运行kubia-manual Pod,因为它没有指定任何资源请求或限制。以下是这种情况会发生什么:
$ kubectl create -f ../Chapter03/kubia-manual.yaml 错误来自服务器(禁止):创建"../Chapter03/kubia-manual.yaml"时出错:pods "kubia-manual"被禁止: 失败配额:cpu-and-mem:必须指定 limits.cpu,limits.memory,requests.cpu,requests.memory
当为特定资源(CPU 或内存)配置配额(请求或限制)时,Pod 需要为该资源设置相应的请求或限制(分别);否则 API 服务器将不接受该 Pod。这就是为什么为这些资源设置默认值的 LimitRange 可以使创建 Pod 的人生活变得容易一些。
14.5.2. 指定持久存储的配额
ResourceQuota 对象还可以限制在命名空间中可以声明的持久存储量,如下面的列表所示。
列表 14.15. 存储资源的 ResourceQuota:quota-storage.yaml
apiVersion: v1 kind: ResourceQuota metadata: name: storage spec: hard: requests.storage: 500Gi 1 ssd.storageclass.storage.k8s.io/requests.storage: 300Gi 2 standard.storageclass.storage.k8s.io/requests.storage: 1Ti
-
1 总共可声明的存储量
-
2 在 StorageClass ssd 中可声明的存储量
在这个例子中,一个命名空间中所有 PersistentVolumeClaims 可以请求的存储量被限制为 500 GiB(由 ResourceQuota 对象中的requests.storage条目限制)。但正如你从第六章中记得的那样,PersistentVolumeClaims 可以请求特定 StorageClass 的动态预配 PersistentVolume。这就是为什么 Kubernetes 还允许为每个 StorageClass 单独定义存储配额。前面的例子将可声明的总 SSD 存储量(由ssd StorageClass 指定)限制为 300 GiB。性能较低的 HDD 存储(StorageClass standard)限制为 1 TiB。
14.5.3. 限制可以创建的对象数量
ResourceQuota 也可以配置为限制单个命名空间内 Pods、Replication-Controllers、Services 和其他对象的数量。这允许集群管理员根据用户的付费计划等限制用户可以创建的对象数量,也可以限制公共 IP 或节点端口 Services 可以使用的数量。
以下列表显示了限制对象数量的 ResourceQuota 对象可能的样子。
列表 14.16. 限制资源最大数量的 ResourceQuota:quota-object-count.yaml
apiVersion: v1 kind: ResourceQuota metadata: name: objects spec: hard: pods: 10 1 replicationcontrollers: 5 1 secrets: 10 1 configmaps: 10 1 persistentvolumeclaims: 4 1 services: 5 2 services.loadbalancers: 1 2 services.nodeports: 2 2 ssd.storageclass.storage.k8s.io/persistentvolumeclaims: 2 3
-
1 在命名空间中可以创建最多 10 个 Pods、5 个 ReplicationControllers、10 个 Secrets、10 个 ConfigMaps 和 4 个 PersistentVolumeClaims。
-
2 总共可以创建五个服务,其中最多一个可以是 LoadBalancer 服务,最多两个可以是 NodePort 服务。
-
3 只有两个 PVC 可以使用 ssd StorageClass 声明存储。
在此列表中,ResourceQuota 允许用户在命名空间中创建最多 10 个 Pods,无论它们是手动创建还是由 ReplicationController、ReplicaSet、DaemonSet、Job 等创建。它还限制了 ReplicationController 的数量为五个。最多可以创建五个服务,其中只有一个可以是 LoadBalancer 类型的服务,并且只有两个可以是 NodePort 类型的服务。类似于可以指定每个 StorageClass 的最大请求存储量,也可以按 StorageClass 限制 PersistentVolumeClaims 的数量。
当前可以为以下对象设置对象计数配额:
-
Pods
-
ReplicationControllers
-
Secrets
-
ConfigMaps
-
PersistentVolumeClaims
-
Services(一般),以及两种特定类型的 Services,例如
Load-BalancerServices (services.loadbalancers) 和NodePortServices (services.nodeports)
最后,您甚至可以为 ResourceQuota 对象本身设置对象计数配额。其他对象(如 ReplicaSets、Jobs、Deployments、Ingresses 等)的数量还不能限制(但自本书出版以来,这可能已经改变,所以请查阅文档以获取最新信息)。
14.5.4. 为特定 pod 状态和/或 QoS 类指定配额
您迄今为止创建的配额已应用于所有 Pods,无论它们的当前状态和 QoS 类别如何。但是,配额也可以限制到一组配额范围。目前有四个范围可用:BestEffort、NotBestEffort、Terminating 和 NotTerminating。
BestEffort 和 NotBestEffort 范围决定了配额是否适用于具有 BestEffort QoS 类的 Pods 或其他两个类别(即 Burstable 和 Guaranteed)的 Pods。
其他两个作用域(Terminating和NotTerminating)不适用于正在(或未在)关闭过程中的 pods,正如名称可能让您所想的那样。我们还没有讨论这一点,但您可以指定每个 pods 在终止并标记为Failed之前允许运行的最长时间。这是通过在 pods 规范中设置active-Deadline-Seconds字段来完成的。此属性定义了 pods 在节点上相对于其启动时间允许活跃的秒数,在标记为Failed并终止之前。Terminating配额作用域适用于已设置active-DeadlineSeconds的 pods,而Not-Terminating适用于未设置的 pods。
在创建 ResourceQuota 时,您可以指定它应用的作用域。一个 pods 必须匹配所有指定的作用域,配额才能应用于它。此外,配额可以限制的内容取决于配额的作用域。BestEffort作用域只能限制 pods 的数量,而其他三个作用域可以限制 pods 的数量、CPU/内存请求和 CPU/内存限制。
例如,如果您希望配额仅应用于BestEffort、NotTerminating pods,您可以创建以下列表中所示的 ResourceQuota 对象。
列表 14.17. BestEffort/NotTerminating pods 的 ResourceQuota:quota-scoped.yaml
apiVersion: v1 kind: ResourceQuota metadata: name: besteffort-notterminating-pods spec: scopes: 1 - BestEffort 1 - NotTerminating 1 hard: pods: 4 2
-
1 此配额仅适用于具有 BestEffort QoS 且未设置活动截止日期的 pods。
-
2 只能存在四个这样的 pods。
此配额确保最多存在四个具有BestEffort QoS 类且没有活动截止日期的 pods。如果配额针对的是NotBestEffort pods,您也可以指定requests.cpu、requests.memory、limits.cpu和limits.memory。
注意
在您进入本章的下一节之前,请删除您创建的所有 ResourceQuota 和 LimitRange 资源。您将不再需要它们,并且它们可能会干扰以下章节中的示例。
14.6. 监控 pod 资源使用情况
正确设置资源请求和限制对于充分利用您的 Kubernetes 集群至关重要。如果请求设置得太高,您的集群节点将得不到充分利用,您会浪费金钱。如果设置得太低,您的应用程序可能会因 CPU 不足而被杀死。您如何找到请求和限制的最佳平衡点?
您可以通过监控容器在预期负载水平下的实际资源使用情况来找到它。一旦应用程序向公众开放,您应该继续监控它,并在必要时调整资源请求和限制。
14.6.1. 收集和检索实际资源使用情况
如何监控在 Kubernetes 中运行的应用程序?幸运的是,Kubelet 本身已经包含了一个名为 cAdvisor 的代理,它执行节点上运行的每个容器以及整个节点的资源消耗数据的基本收集。为了在整个集群中集中收集这些统计信息,您需要运行一个名为 Heapster 的附加组件。
Heapster 作为 pod 在节点上运行,并通过常规 Kubernetes 服务暴露,使其可以通过稳定的 IP 地址访问。它从集群中的所有 cAdvisors 收集数据,并在单个位置暴露。 展示了从 pod 到 cAdvisor,再到 Heapster 的指标数据流。
![图 14.8. 指标数据流入 Heapster 的流程]

图中的箭头显示了指标数据的流动方式。它们不显示哪个组件连接到哪个以获取数据。pod(或在其中运行的容器)对 cAdvisor 一无所知,cAdvisor 也不知道 Heapster。是 Heapster 连接到所有 cAdvisors,而 cAdvisors 收集容器和节点使用数据,无需与 pod 容器内运行的进程交谈。
启用 Heapster
如果您在 Google Kubernetes Engine 上运行集群,Heapster 默认启用。如果您使用 Minikube,它作为附加组件可用,可以使用以下命令启用:
$ minikube addons enable heapster heapster was successfully enabled
要在其他类型的 Kubernetes 集群中手动运行 Heapster,您可以参考位于 github.com/kubernetes/heapster 的说明。
启用 Heapster 后,您需要等待几分钟,以便它收集指标,然后您才能看到集群的资源使用统计信息,所以请耐心等待。
显示集群节点的 CPU 和内存使用情况
在您的集群中运行 Heapster 可以通过 kubectl top 命令获取节点和单个 pod 的资源使用情况。要查看您的节点上使用了多少 CPU 和内存,可以运行以下列表中的命令。
列表 14.18. 节点的实际 CPU 和内存使用情况
$ kubectl top node NAME CPU(cores) CPU% MEMORY(bytes) MEMORY% minikube 170m 8% 556Mi 27%
这显示了节点上运行的所有 pod 的实际、当前 CPU 和内存使用情况,与 kubectl describe node 命令不同,后者显示的是 CPU 和内存请求量以及限制量,而不是实际的运行时使用数据。
显示单个 pod 的 CPU 和内存使用情况
要查看每个 pod 使用了多少资源,可以使用 kubectl top pod 命令,如下所示。
列表 14.19. pod 的实际 CPU 和内存使用情况
$ kubectl top pod --all-namespaces NAMESPACE NAME CPU(cores) MEMORY(bytes) kube-system influxdb-grafana-2r2w9 1m 32Mi kube-system heapster-40j6d 0m 18Mi default kubia-3773182134-63bmb 0m 9Mi kube-system kube-dns-v20-z0hq6 1m 11Mi kube-system kubernetes-dashboard-r53mc 0m 14Mi kube-system kube-addon-manager-minikube 7m 33Mi
这两个命令的输出相当简单,所以你可能不需要我解释,但我确实需要提醒你一件事。有时 top pod 命令会拒绝显示任何指标,而是打印出类似这样的错误:
$ kubectl top pod W0312 22:12:58.021885 15126 top_pod.go:186] 指标对于 pod default/kubia-3773182134-63bmb 不可用,年龄:1h24m19.021873823s 错误:对于 pod default/kubia-3773182134-63bmb 指标不可用,年龄:1h24m19.021873823s
如果发生这种情况,请不要立即寻找错误的根源。放松一下,稍等片刻,然后重新运行命令——可能需要几分钟,但指标最终应该会显示出来。kubectl top 命令从 Heapster 获取指标,Heapster 会将数据聚合几分钟,并不会立即暴露出来。
小贴士
要查看跨各个容器的资源使用情况而不是 pod,可以使用 --containers 选项。
14.6.2. 存储和分析历史资源消耗统计
top 命令只显示当前资源使用情况——它不会显示你的 pod 在过去一小时、昨天或一周前消耗了多少 CPU 或内存,例如。实际上,cAdvisor 和 Heapster 只保留短时间窗口的资源使用数据。如果你想分析你的 pod 在更长的时间段内的资源消耗,你需要运行额外的工具。
当使用 Google Kubernetes Engine 时,你可以使用 Google Cloud Monitoring 监控你的集群,但当你运行自己的本地 Kubernetes 集群(无论是通过 Minikube 还是其他方式)时,人们通常使用 InfluxDB 存储统计数据,并使用 Grafana 进行可视化和分析。
介绍 InfluxDB 和 Grafana
InfluxDB 是一个开源的时间序列数据库,非常适合存储应用程序指标和其他监控数据。Grafana,也是开源的,是一个具有美观网页控制台的分析和可视化套件,它允许你可视化存储在 InfluxDB 中的数据,并发现你的应用程序资源使用随时间的变化情况(如图 14.9 所示的三个 Grafana 图表示例)。
图 14.9. 显示集群 CPU 使用情况的 Grafana 仪表板

在你的集群中运行 InfluxDB 和 Grafana
InfluxDB 和 Grafana 都可以作为 Pod 运行。部署它们很简单。所有必要的清单都可在 Heapster Git 仓库的github.com/kubernetes/heapster/tree/master/deploy/kube-config/influxdb中找到。
当使用 Minikube 时,你甚至不需要手动部署它们,因为当你启用 Heapster 附加组件时,它们会与 Heapster 一起部署。
使用 Grafana 分析资源使用情况
要发现你的 Pod 随时间需要多少每种资源,请打开 Grafana Web 控制台并探索预定义的仪表板。通常,你可以使用kubectl cluster-info找到 Grafana Web 控制台的 URL:
$ kubectl cluster-info ... monitoring-grafana 运行在 https://192.168.99.100:8443/api/v1/proxy/namespaces/kube- system/services/monitoring-grafana
当使用 Minikube 时,Grafana 的 Web 控制台通过NodePort服务暴露,因此你可以使用以下命令在浏览器中打开它:
$ minikube service monitoring-grafana -n kube-system 在默认浏览器中打开 kubernetes 服务 kube-system/monitoring-grafana...
将会打开一个新的浏览器窗口或标签页,显示 Grafana 的主屏幕。在右侧,你会看到一个包含两个条目的仪表板列表:
-
集群
-
Pods
要查看节点的资源使用统计信息,请打开集群仪表板。在那里,你会看到几个图表,显示整体集群使用情况、按节点使用情况以及 CPU、内存、网络和文件系统的单个使用情况。图表不仅会显示实际使用情况,还会显示那些资源的请求和限制(如果适用)。
如果你切换到 Pods 仪表板,你可以检查每个单独 Pod 的资源使用情况,同样会显示请求和限制与实际使用情况并列。
初始时,图表显示过去 30 分钟的统计数据,但你可以放大并查看更长时间段的数据:几天、几个月,甚至几年。
使用图表中显示的信息
通过查看图表,你可以快速了解为你的 Pod 设置的资源请求或限制是否需要提高,或者是否可以降低以允许更多的 Pod 适合你的节点。让我们看看一个例子。图 14.10 显示了 Pod 的 CPU 和内存图表。
图 14.10. Pod 的 CPU 和内存使用图表

在顶部图表的最右侧,您可以看到 Pod 使用的 CPU 比 Pod 的清单中请求的要多。尽管当这是节点上唯一运行的 Pod 时,这并不成问题,但您应该记住,Pod 只能保证通过资源请求请求的资源量。您的 Pod 现在可能运行良好,但当其他 Pod 部署到同一节点并开始使用 CPU 时,您的 Pod 的 CPU 时间可能会被限制。因此,为了确保 Pod 在任何时候都能使用它所需的尽可能多的 CPU,您应该提高 Pod 容器的 CPU 资源请求。
底部图表显示了 Pod 的内存使用情况和请求。这里的情况正好相反。Pod 使用的内存量远远低于 Pod 规范中请求的量。请求的内存为 Pod 保留,不会对其他 Pod 可用。因此,未使用的内存因此被浪费。您应该降低 Pod 的内存请求,以便将内存提供给节点上运行的其他 Pod。
14.7. 摘要
本章向您展示了您需要考虑您的 Pod 资源使用情况,并为您的 Pod 配置资源请求和限制,以保持一切运行顺畅。本章的关键要点是
-
指定资源请求有助于 Kubernetes 在集群中调度 Pod。
-
指定资源限制可以防止 Pod 使其他 Pod 的资源匮乏。
-
未使用的 CPU 时间是根据容器的 CPU 请求分配的。
-
如果容器尝试使用过多的 CPU,它们不会被杀死,但如果它们尝试使用过多的内存,它们会被杀死。
-
在过度承诺的系统中,容器也会被杀死,以释放内存,为更重要的 Pod 提供内存,这基于 Pod 的 QoS 类别和实际内存使用情况。
-
您可以使用 LimitRange 对象来定义单个 Pod 的最小、最大和默认资源请求和限制。
-
您可以使用 ResourceQuota 对象来限制命名空间中所有 Pod 可用的资源量。
-
要知道如何设置 Pod 的资源请求和限制,您需要监控 Pod 在足够长的时间内的资源使用情况。
在下一章中,您将看到这些指标如何被 Kubernetes 用来自动扩展您的 Pod。
第十五章. Pod 和集群节点自动扩展
本章涵盖
-
根据 CPU 利用率配置 Pod 的自动水平扩展
-
根据自定义指标配置 Pod 的自动水平扩展
-
理解为什么 Pod 的垂直扩展目前不可行
-
理解集群节点自动水平扩展
运行在 Pod 中的应用程序可以通过在 ReplicationController、ReplicaSet、Deployment 或其他可扩展资源中增加replicas字段来手动扩展。Pod 也可以通过增加其容器的资源请求和限制(尽管目前只能在 Pod 创建时进行,不能在 Pod 运行时进行)进行垂直扩展。尽管手动扩展在可以提前预测负载峰值或在较长时间内负载逐渐变化时是可行的,但需要手动干预来处理突然、不可预测的流量增加并不是理想的做法。
幸运的是,Kubernetes 可以监控您的 Pod,并在检测到 CPU 使用量或其他指标增加时自动扩展它们。如果在云基础设施上运行,它甚至可以在现有节点无法接受更多 Pod 时启动额外的节点。本章将解释如何让 Kubernetes 同时进行 Pod 和节点自动扩展。
Kubernetes 中的自动扩展功能在 1.6 和 1.7 版本之间完全重写,因此请注意,您可能会在网上找到关于这个主题的过时信息。
15.1. 水平 Pod 自动扩展
水平 Pod 自动扩展是由控制器管理的 Pod 副本数量的自动扩展。它由水平控制器执行,该控制器通过创建 HorizontalPodAutoscaler (HPA) 资源来启用和配置。控制器定期检查 Pod 指标,计算满足 HorizontalPodAutoscaler 资源中配置的目标指标值所需的副本数量,并调整目标资源(Deployment、ReplicaSet、Replication-Controller 或 StatefulSet)上的replicas字段。
15.1.1. 理解自动扩展过程
自动扩展过程可以分为三个步骤:
-
获取由扩展资源对象管理的所有 Pod 的指标。
-
计算需要多少 Pod 才能将指标提升到(或接近)指定的目标值。
-
更新扩展资源的
replicas字段。
让我们接下来检查所有三个步骤。
获取 Pod 指标
自动扩展器本身不执行 pod 指标的收集。它从不同的来源获取指标。正如我们在上一章中看到的,pod 和节点指标由一个称为 cAdvisor 的代理收集,该代理在每个节点上运行 Kubelet,然后由集群范围内的组件 Heapster 进行聚合。水平 pod 自动扩展器控制器通过 REST 调用查询 Heapster 来获取所有 pod 的指标。指标数据的流程如图 15.1 所示(尽管所有连接都是相反方向发起的)。
图 15.1. 从 pod 到水平 pod 自动扩展器的指标流程

这意味着 Heapster 必须在集群中运行才能使自动扩展工作。如果你使用 Minikube 并且在上一章中跟随操作,Heapster 应该已经在你的集群中启用。如果没有,确保在尝试任何自动扩展示例之前启用 Heapster 扩展。
虽然你不需要直接查询 Heapster,但如果你有兴趣这样做,你将在 kube-system 命名空间中找到 Heapster Pod 和它暴露的 Service。
查看与自动扩展器获取指标相关联的更改
在 Kubernetes 版本 1.6 之前,水平 pod 自动扩展器直接从 Heapster 获取指标。在版本 1.8 中,自动扩展器可以通过启动 Controller Manager 时使用 --horizontal-pod-autoscaler-use-rest-clients=true 标志来通过资源指标 API 的聚合版本获取指标。从版本 1.9 开始,此行为将默认启用。
核心 API 服务器不会自己公开指标。从版本 1.7 开始,Kubernetes 允许注册多个 API 服务器并将它们表现为单个 API 服务器。这允许它通过这些底层 API 服务器之一公开指标。我们将在最后一章中解释 API 服务器聚合。
选择在他们的集群中使用哪个指标收集器将由集群管理员决定。通常需要一个简单的翻译层来在适当的 API 路径和适当的格式中公开指标。
计算所需的 pod 数量
一旦自动扩展器获得了正在扩展的资源(部署、副本集、复制控制器或有状态集资源)所属的所有 pod 的指标,它就可以使用这些指标来确定所需的副本数量。它需要找到将所有这些副本的指标平均值尽可能接近配置的目标值的数字。此计算的输入是一组 pod 指标(每个 pod 可能可能有多个指标),输出是一个整数(pod 副本的数量)。
当自动扩展器配置为仅考虑单个指标时,计算所需的副本数量是简单的。只需将所有 Pod 的指标值相加,然后除以 HorizontalPodAutoscaler 资源上设置的目标值,最后向上取整到下一个更大的整数。实际的计算比这要复杂一些,因为它还确保当指标值不稳定且快速变化时,自动扩展器不会频繁波动。
当自动扩展基于多个 Pod 指标(例如,CPU 使用率和每秒查询数[QPS])时,计算并没有变得复杂多少。自动扩展器会为每个指标单独计算副本数量,然后取最高值(例如,如果需要四个 Pod 来实现目标 CPU 使用率,而需要三个 Pod 来实现目标 QPS,自动扩展器将扩展到四个 Pod)。图 15.2 展示了这个例子。
图 15.2. 从两个指标计算副本数量

更新扩展资源上的期望副本数量
自动扩展操作的最终步骤是在扩展资源对象(例如 ReplicaSet)上更新期望副本数量字段,然后让 ReplicaSet 控制器负责启动额外的 Pod 或删除多余的 Pod。
自动扩展器控制器通过 Scale 子资源修改扩展资源的replicas字段。它使自动扩展器能够在不知道它所扩展的资源任何细节的情况下(除了通过 Scale 子资源暴露的内容之外),完成其工作(参见图 15.3)。
图 15.3. 水平 Pod 自动扩展器仅修改 Scale 子资源。

这使得自动扩展器可以在任何可扩展资源上操作,只要 API 服务器为其公开 Scale 子资源。目前,它已公开用于
-
Deployments
-
ReplicaSets
-
ReplicationControllers
-
StatefulSets
这些是目前您可以附加自动扩展器的唯一对象。
理解整个自动扩展过程
您现在已经了解了自动扩展涉及的三步,那么让我们可视化自动扩展过程中涉及的所有组件。它们在图 15.4 中展示。
图 15.4. 自动扩展器如何获取指标并调整目标部署

从 Pod 指向 cAdvisors 的箭头,这些箭头继续指向 Heapster,最后指向 Horizontal Pod Autoscaler,表示指标数据的流向。重要的是要意识到,每个组件都会定期从其他组件获取指标(即 cAdvisor 以连续循环的方式从 Pod 获取指标;Heapster 和 HPA 控制器也是如此)。最终结果是,指标数据传播和重新扩展操作需要相当长的时间。这不是立即发生的。当你观察自动扩展器的工作时,请记住这一点。
15.1.2. 基于 CPU 利用率的扩展
可能你最希望基于的最重要的指标是运行在你 Pod 内的进程消耗的 CPU 数量。想象一下有几个 Pod 提供一种服务。当它们的 CPU 使用率达到 100%时,很明显它们已经无法再满足需求,需要扩展,要么向上(垂直扩展——增加 Pod 可以使用的 CPU 数量),要么向外(水平扩展——增加 Pod 的数量)。因为我们在这里讨论的是 Horizontal Pod Autoscaler,所以我们只关注向外扩展(增加 Pod 的数量)。通过这样做,平均 CPU 使用率应该会下降。
由于 CPU 使用率通常不稳定,因此在 CPU 完全饱和之前进行扩展是有意义的——例如,当 Pod 的平均 CPU 负载达到或超过 80%时。但这里的 80%是指什么?
小贴士
总是设置目标 CPU 使用率低于 100%(并且绝对不能超过 90%),以留出足够的空间来处理突发的负载峰值。
如你从上一章所记得的,运行在容器内的进程通过为容器指定的资源请求保证获得 CPU 数量。但在没有其他进程需要 CPU 的时候,进程可能会使用节点上所有可用的 CPU。当有人说一个 Pod 消耗了 80%的 CPU 时,并不清楚他们是指节点的 80%的 CPU,Pod 保证的 80%的 CPU(资源请求),还是通过资源限制为 Pod 配置的硬限制的 80%。
对于自动扩展器而言,在确定 Pod 的 CPU 利用率时,只有 Pod 保证的 CPU 数量(即 CPU 请求)是重要的。自动扩展器比较 Pod 的实际 CPU 消耗和其 CPU 请求,这意味着你正在自动扩展的 Pod 需要设置 CPU 请求(无论是直接设置还是通过 LimitRange 对象间接设置),以便自动扩展器能够确定 CPU 利用率百分比。
基于 CPU 使用情况创建 HorizontalPodAutoscaler
现在我们来看看如何创建一个 HorizontalPodAutoscaler,并配置它根据 Pod 的 CPU 利用率进行扩展。你将创建一个类似于第九章中提到的 Deployment,但正如我们讨论的那样,你需要确保由 Deployment 创建的所有 Pod 都指定了 CPU 资源请求,以便实现自动扩展。你必须在 Deployment 的 Pod 模板中添加一个 CPU 资源请求,如下所示。
列表 15.1. 设置了 CPU 请求的 Deployment:deployment.yaml
apiVersion: extensions/v1beta1 kind: Deployment metadata: name: kubia spec: replicas: 3 1 template: metadata: name: kubia labels: app: kubia spec: containers: - image: luksa/kubia:v1 2 name: nodejs resources: 3 requests: 3 cpu: 100m 3
-
1 手动设置(初始)所需的副本数为三个
-
2 运行 kubia:v1 镜像
-
每个 Pod 请求 100 毫核 CPU
这是一个常规的 Deployment 对象——它还没有使用自动扩展。它将运行三个kubia NodeJS 应用的实例,每个实例请求 100 毫核的 CPU。
在创建 Deployment 之后,为了启用其 Pod 的水平自动扩展,你需要创建一个 HorizontalPodAutoscaler (HPA)对象并将其指向 Deployment。你可以准备和发布 HPA 的 YAML 清单,但有一个更简单的方法——使用kubectl autoscale命令:
$ kubectl autoscale deployment kubia --cpu-percent=30 --min=1 --max=5 deployment "kubia" autoscaled
这将为你创建 HPA 对象,并将名为kubia的 Deployment 设置为扩展目标。你正在设置 Pod 的目标 CPU 利用率为 30%,并指定了最小和最大副本数。自动扩展器将不断调整副本数,以保持其 CPU 利用率在 30%左右,但绝不会将副本数缩减到少于一个或增加到多于五个。
小贴士
总是要确保自动扩展 Deployment 而不是底层的 ReplicaSet。这样,你确保了在应用程序更新期间保留所需的副本数(记住,Deployment 为每个版本创建一个新的 ReplicaSet)。同样的规则也适用于手动扩展。
让我们看看 HorizontalPodAutoscaler 资源的定义,以便更好地理解它。它如下所示。
列表 15.2. HorizontalPodAutoscaler 的 YAML 定义
$ kubectl get hpa.v2beta1.autoscaling kubia -o yaml apiVersion: autoscaling/v2beta1 1 kind: HorizontalPodAutoscaler 1 metadata: name: kubia 2 ... spec: maxReplicas: 5 3 metrics: 4 - resource: 4 name: cpu 4 targetAverageUtilization: 30 4 type: Resource 4 minReplicas: 1 3 scaleTargetRef: 5 apiVersion: extensions/v1beta1 5 kind: Deployment 5 name: kubia 5 status: currentMetrics: [] 6 currentReplicas: 3 6 desiredReplicas: 0 6
-
1 HPA 资源位于 autoscaling API 组中。
-
2 每个 HPA 都有一个名称(它不需要与部署的名称匹配,如本例所示)。
-
3 你指定的副本的最小和最大数量
-
4 你希望自动扩展器调整 Pod 的数量,使每个 Pod 利用 30%的请求 CPU。
-
5 这个自动扩展器将要作用的资源目标
-
6 自动扩展器的当前状态
注意
HPA 资源存在多个版本:新的autoscaling/v2beta1和旧的autoscaling/v1。你在这里请求的是新版本。
看到第一次自动缩放事件
在自动扩展器可以采取行动之前,cAdvisor 需要获取 CPU 指标,Heapster 需要收集它们,这需要一段时间。在这段时间内,如果你使用kubectl get显示 HPA 资源,TARGETS列将显示<unknown>:
$ kubectl get hpa NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS kubia Deployment/kubia <unknown> / 30% 1 5 0
由于你正在运行三个当前没有收到请求的 Pod,这意味着它们的 CPU 使用率应该接近零,因此你应该预期自动扩展器将它们缩放到单个 Pod,因为即使只有一个 Pod,CPU 利用率仍然会低于 30%的目标。
确实如此,自动扩展器正是这样做的。它很快将 Deployment 缩放到单个副本:
$ kubectl get deployment NAME DESIRED``CURRENT UP-TO-DATE AVAILABLE AGE kubia 1`` 1 1 1 23m
记住,自动扩展器只调整 Deployment 上的期望副本数。然后,Deployment 控制器负责更新 ReplicaSet 对象上的期望副本数,这会导致 ReplicaSet 控制器删除两个多余的 Pod,留下一个 Pod 运行。
你可以使用kubectl describe来查看关于 HorizontalPod-Autoscaler 和底层控制器操作的更多信息,如下面的列表所示。
列表 15.3. 使用kubectl describe检查 HorizontalPodAutoscaler
$ kubectl describe hpa Name: kubia Namespace: default Labels: <none> Annotations: <none> CreationTimestamp: Sat, 03 Jun 2017 12:59:57 +0200 Reference: Deployment/kubia Metrics: ( current / target ) resource cpu on pods (as a percentage of request): 0% (0) / 30% Min replicas: 1 Max replicas: 5 Events: From Reason Message ---- ------ --- horizontal-pod-autoscaler SuccessfulRescale New size: 1; reason: All metrics below target
注意
输出已被修改以使其更易于阅读。
将你的注意力转向列表底部的事件表。你看到水平 pod 自动扩展器已成功调整到 1 个副本,因为所有指标都低于目标。
触发扩容
你已经见证了你的第一个自动调整大小事件(缩小)。现在,你将开始向你的 pod 发送请求,从而增加其 CPU 使用率,你应该看到自动扩展器检测到这一点并启动额外的 pods。
你需要通过一个 Service 来暴露 pods,这样你就可以通过一个 URL 访问它们。你可能记得,这样做最简单的方式是使用kubectl expose:
$ kubectl expose deployment kubia --port=80 --target-port=8080 service "kubia" exposed
在你开始对你的 pod(s)发送请求之前,你可能想在另一个终端中运行以下命令,以便密切关注 HorizontalPodAutoscaler 和 Deployment 的情况,如下所示。
列表 15.4.并行监视多个资源
$ watch -n 1 kubectl get hpa,deployment 每 1.0 秒:kubectl get hpa,deployment NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE hpa/kubia Deployment/kubia 0% / 30% 1 5 1 45m NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE deploy/kubia 1 1 1 1 56m
小贴士
使用逗号分隔符,通过kubectl get列出多个资源类型。
如果你使用的是 OSX,你必须将watch命令替换为循环,手动定期运行kubectl get,或者使用kubectl的--watch选项。但尽管普通的kubectl get可以同时显示多种类型的资源,但在使用上述--watch选项时并非如此,所以如果你想同时监视 HPA 和 Deployment 对象,你需要使用两个终端。
在运行负载生成 pod 时,请密切关注这两个对象的状态。你需要在另一个终端中运行以下命令:
$ kubectl run -it --rm --restart=Never loadgenerator --image=busybox
-- sh -c "while true; do wget -O - -q http://kubia.default; done"
这将运行一个 Pod,该 Pod 会反复调用kubia服务。你已经多次在运行kubectl exec命令时看到-it选项。正如你所见,它也可以与kubectl run一起使用。这允许你将控制台附加到进程,这不仅会直接显示进程的输出,而且在你按下 CTRL+C 时也会终止进程。--rm选项会在之后删除 Pod,而--restart=Never选项会导致kubectl run直接创建一个未管理的 Pod,而不是通过 Deployment 对象,这你不需要。这种选项组合对于在集群内部运行命令而不需要依赖现有 Pod 非常有用。它不仅表现得与你在本地运行命令时一样,而且在命令终止时还会清理所有内容。
观察自动扩展器扩展部署
随着负载生成器 Pod 的运行,你会看到它最初攻击单个 Pod。和之前一样,更新指标需要时间,但一旦更新,你就会看到自动扩展器增加副本的数量。在我的情况下,Pod 的 CPU 利用率最初跳升到 108%,这导致自动扩展器将 Pod 的数量增加到四个。然后,单个 Pod 的利用率下降到 74%,然后稳定在大约 26%左右。
注意
如果你情况下的 CPU 负载不超过 30%,尝试运行额外的负载生成器。
再次强调,你可以使用kubectl describe来检查自动扩展器的事件,以查看自动扩展器做了什么(以下列表中只显示了最重要的信息)。
列表 15.5. 水平 Pod 自动扩展器的事件
From Reason Message ---- ------ ------- h-p-a SuccessfulRescale New size: 1; reason: All metrics below target h-p-a SuccessfulRescale New size: 4; reason: cpu resource utilization (percentage of request) above target
你是否觉得奇怪,在我只有一个 Pod 的情况下,初始的平均 CPU 利用率竟然达到了 108%,这超过了 100%?记住,容器的 CPU 利用率是容器的实际 CPU 使用量除以其请求的 CPU。请求的 CPU 定义了容器可用的 CPU 的最小量,而不是最大量,因此容器可能消耗的 CPU 超过请求量,导致百分比超过 100%。
在我们继续之前,让我们做一点数学计算,看看自动扩展器是如何得出需要四个副本的结论的。最初,有一个副本处理请求,其 CPU 利用率激增到 108%。将 108 除以 30(目标 CPU 利用率百分比)得到 3.6,然后自动扩展器将其四舍五入到 4。如果你将 108 除以 4,你得到 27%。如果自动扩展器扩展到四个 Pod,它们的平均 CPU 利用率预计将在 27%左右,这接近于 30%的目标值,并且几乎与观察到的 CPU 利用率完全一致。
理解最大扩展速率
在我的情况下,CPU 使用率飙升至 108%,但一般来说,初始 CPU 使用率可能会更高。即使初始平均 CPU 利用率更高(比如说 150%),需要五个副本才能达到 30%的目标,自动扩展器在第一步中仍然只会扩展到四个 Pod,因为它对单次扩展操作中可以添加的副本数量有限制。如果当前副本数量超过两个,自动扩展器在单次操作中最多只会将副本数量翻倍。如果只有一到两个副本,它将单步扩展到最多四个副本。
此外,它还对后续自动扩展操作可以在前一个操作之后多快发生有限制。目前,只有在最后三分钟内没有发生重新缩放事件时,才会发生扩展。缩放事件发生的频率更低——每五分钟一次。请记住这一点,以免您不明白为什么即使指标清楚地显示应该进行缩放操作,自动扩展器也拒绝执行缩放操作。
在现有的 HPA 对象上修改目标度量值
为了总结本节内容,让我们进行最后一个练习。也许您最初的 30% CPU 利用率目标有点低,所以将其提高到 60%。您可以通过使用kubectl edit命令编辑 HPA 资源来完成此操作。当文本编辑器打开时,将targetAverageUtilization字段更改为60,如下所示。
列表 15.6. 通过编辑 HPA 资源提高目标 CPU 利用率
... spec: maxReplicas: 5 metrics: - resource: name: cpu targetAverageUtilization: 60 1 type: Resource ...
- 1 将此从 30 更改为 60。
与大多数其他资源一样,修改资源后,自动扩展器控制器会检测到您的更改并采取行动。您也可以删除资源,并使用不同的目标值重新创建它,因为通过删除 HPA 资源,您只是禁用了目标资源(在这种情况下是 Deployment)的自动扩展,并使其保持在当时的规模。创建新的 HPA 资源后,自动扩展将重新启动。
15.1.3. 基于内存消耗进行缩放
你已经看到了如何轻松地配置水平自动扩展器以保持 CPU 利用率在目标水平。但基于 Pod 内存使用量的自动扩展又如何呢?
基于内存的自动扩展比基于 CPU 的自动扩展问题更多。主要原因是在扩展之后,旧的 Pod 需要以某种方式被迫释放内存。这需要由应用程序本身完成——系统无法完成。系统所能做的就是杀死并重新启动应用程序,希望它使用的内存比之前少。但如果应用程序随后使用与之前相同的内存量,自动扩展器会再次将其扩展。一次又一次,直到达到 HPA 资源上配置的最大 Pod 数量。显然,这不是任何人想要的。基于内存的自动扩展是在 Kubernetes 版本 1.8 中引入的,其配置方式与基于 CPU 的自动扩展完全相同。探索它留给读者自行完成。
15.1.4. 基于其他和自定义指标的扩展
您已经看到,根据 CPU 使用情况扩展 Pod 是多么容易。最初,这是唯一在实践中可用的自动扩展选项。要让自动扩展器使用自定义的应用定义指标来驱动其自动扩展决策相当复杂。自动扩展器的初始设计并没有使其容易超越简单的基于 CPU 的扩展。这促使 Kubernetes 自动扩展特别兴趣小组(SIG)完全重新设计自动扩展器。
如果您对使用初始自动扩展器与自定义指标有多么复杂感兴趣,我邀请您阅读我的一篇博客文章,标题为“不使用主机端口进行基于自定义指标的 Kubernetes 自动扩展”,您可以在网上找到medium.com/@marko.luksa。您将了解我在尝试根据自定义指标设置自动扩展时遇到的所有其他问题。幸运的是,Kubernetes 的新版本没有这些问题。我将在一篇新的博客文章中介绍这个主题。
在这里不通过一个完整的示例,让我们快速了解一下如何配置自动扩展器以使用不同的指标源。我们将首先检查我们在上一个示例中定义了什么指标。以下列表显示了您的上一个 HPA 对象是如何配置为使用 CPU 使用率指标的。
列表 15.7. 基于 CPU 的自动扩展的 HorizontalPodAutoscaler 定义
... spec: maxReplicas: 5 metrics: - type: Resource 1 resource: name: cpu 2 targetAverageUtilization: 30 3 ...
-
1 定义指标类型
-
2 将要监控的资源利用率
-
3 该资源的目标利用率
如您所见,metrics字段允许您定义多个要使用的指标。在列表中,您正在使用单个指标。每个条目定义了指标的type——在这种情况下,是一个Resource指标。在 HPA 对象中,您可以使用三种类型的指标:
-
Resource -
Pods -
Object
理解资源指标类型
Resource类型使自动扩展器基于资源指标(如容器资源请求中指定的指标)进行自动扩展决策。我们已经看到了如何做到这一点,所以让我们关注其他两种类型。
理解 Pods 指标类型
Pods类型用于引用与 Pod 直接相关的任何其他(包括自定义)指标。此类指标的一个例子可以是已经提到的每秒查询数(QPS)或消息代理队列中的消息数量(当消息代理作为 Pod 运行时)。要配置自动扩展器使用 Pod 的 QPS 指标,HPA 对象需要在它的metrics字段下包含以下列表中显示的条目。
列表 15.8.在 HPA 中引用自定义 Pod 指标
... spec: metrics: - type: Pods 1 resource: metricName: qps 2 targetAverageValue: 100 3 ...
-
1 定义 Pod 指标
-
2 指标的名称
-
3 在所有目标 Pod 中的目标平均值
列表中给出的示例配置了自动扩展器,使其保持由该 HPA 资源针对的 ReplicaSet(或其他)控制器管理的所有 Pod 的平均 QPS 为100。
理解 Object 指标类型
当你想根据不直接与这些 Pod 相关的指标来使自动扩展器扩展 Pod 时,使用Object指标类型。例如,你可能想根据另一个集群对象(如 Ingress 对象)的指标来扩展 Pod。该指标可以是列表 15.8 中的 QPS,平均请求延迟或其他完全不同的指标。
与前一个案例不同,在前一个案例中,自动扩展器需要获取所有目标 Pod 的指标,然后使用这些值的平均值,当你使用Object指标类型时,自动扩展器从单个对象获取单个指标。在 HPA 定义中,你需要指定目标对象和目标值。以下列表显示了一个示例。
列表 15.9.在 HPA 中引用不同对象的指标
... spec: metrics: - type: Object 1 resource: metricName: latencyMillis 2 target: apiVersion: extensions/v1beta1 3 kind: Ingress 3 name: frontend 3 targetValue: 20 4 scaleTargetRef: 5 apiVersion: extensions/v1beta1 5 kind: Deployment 5 name: kubia 5 ...
-
1 使用特定对象的指标
-
2 指标的名称
-
3 自动扩展器应获取其指标的特定对象
-
4 自动扩展器应该扩展,使指标的值保持接近这个值。
-
5 自动扩展器将扩展的可伸缩资源
在此示例中,HPA 被配置为使用frontend Ingress 对象的latencyMillis指标。该指标的的目标值是20。水平 Pod 自动扩展器将监控 Ingress 的指标,如果它上升得太远高于目标值,自动扩展器将扩展kubia Deployment 资源。
15.1.5.确定哪些指标适合自动扩展
你需要理解并非所有指标都适合用作自动扩展的基础。如前所述,Pod 容器的内存消耗不是自动扩展的好指标。如果增加副本数不会导致观察到的指标的平均值线性减少(或者至少接近线性),自动扩展器将无法正常工作。
例如,如果你只有一个 Pod 实例,并且指标的值为 X,自动扩展器将副本数扩展到两个,那么指标值需要下降到大约 X/2。这种自定义指标的例子是每秒查询数(QPS),在 Web 应用程序的情况下,它报告每秒应用程序接收到的请求数量。增加副本数将始终导致 QPS 成比例下降,因为更多的 Pod 将处理相同数量的总请求。
在你决定基于你应用程序自己的自定义指标设置自动扩展器之前,请务必考虑当 Pod 数量增加或减少时其值的行为。
15.1.6. 缩放到零副本
水平 Pod 自动扩展器目前不允许将minReplicas字段设置为 0,因此自动扩展器永远不会缩放到零,即使 Pod 没有做任何事情。允许 Pod 的数量缩放到零可以显著提高硬件的利用率。当你运行每几小时或甚至每天只接收一次请求的服务时,让它们一直运行,消耗其他 Pod 可能使用的资源是没有意义的。但当你需要这些服务在客户端请求到来时立即可用时,你仍然希望它们存在。
这被称为空闲和取消空闲。它允许提供特定服务的 Pod 被缩放到零。当新的请求到来时,请求将被阻塞,直到 Pod 启动,然后请求最终被转发到 Pod。
Kubernetes 目前还没有提供这个功能,但最终会提供。请检查文档以查看是否已实现空闲状态。
15.2. 垂直 Pod 自动扩展
水平扩展很棒,但并非每个应用程序都可以水平扩展。对于这类应用程序,唯一的选项是将它们垂直扩展——给它们更多的 CPU 和/或内存。因为节点通常拥有的资源比单个 Pod 请求的资源多,所以几乎总是可以垂直扩展 Pod,对吧?
由于一个 Pod 的资源请求是通过 Pod 清单中的字段配置的,因此垂直扩展 Pod 将通过更改这些字段来实现。我说“将”是因为目前无法更改现有 Pod 的资源请求或限制。在我开始写这本书(超过一年前)的时候,我确信在我写这一章的时候,Kubernetes 已经支持了适当的垂直 Pod 自动扩展,所以我将其包含在我的目录提案中。遗憾的是,似乎是一生的时间之后,垂直 Pod 自动扩展仍然还没有实现。
15.2.1. 自动配置资源请求
一个实验性功能为新创建的 Pod 设置了 CPU 和内存请求,如果它们的容器没有明确设置这些值。这个功能由一个名为 InitialResources 的准入控制插件提供。当创建一个没有资源请求的新 Pod 时,该插件会查看 Pod 容器的历史资源使用数据(根据底层容器镜像和标签),并相应地设置请求。
你可以部署 Pod 而不指定资源请求,并依赖 Kubernetes 最终确定每个容器的资源需求。实际上,Kubernetes 正在垂直扩展 Pod。例如,如果一个容器不断耗尽内存,那么下次创建具有该容器镜像的 Pod 时,其内存资源请求将自动设置得更高。
15.2.2. 在 Pod 运行时修改资源请求
最终,将使用相同的机制来修改现有 Pod 的资源请求,这意味着它将在 Pod 运行时垂直扩展 Pod。在我写这段话的时候,一个新的垂直 Pod 自动扩展提案正在最终确定。请参考 Kubernetes 文档以了解垂直 Pod 自动扩展是否已经实现。
15.3. 集群节点的水平扩展
水平 Pod 自动扩展器在需要时创建额外的 Pod 实例。但是,当所有节点都达到容量并且无法运行更多 Pod 时怎么办?显然,这个问题不仅限于当自动扩展器创建新的 Pod 实例时。即使手动创建 Pod,你也可能遇到没有任何节点可以接受新 Pod 的问题,因为节点的资源已经被现有的 Pod 用完。
在那种情况下,你需要删除一些现有的 Pod,垂直缩小它们的规模,或者向你的集群添加额外的节点。如果你的 Kubernetes 集群在本地运行,你需要物理添加一台新机器并将其作为 Kubernetes 集群的一部分。但如果你的集群在云基础设施上运行,添加额外的节点通常只需要几点击或对云基础设施的 API 调用。这可以自动完成,对吧?
Kubernetes 包含了在检测到需要额外的节点时自动从云服务提供商请求额外节点的功能。这是由集群自动扩展器执行的。
15.3.1. 介绍集群自动扩展器
集群自动扩展器负责在检测到由于节点资源不足而无法将 pod 调度到现有节点时,自动提供额外的节点。当节点长时间未被充分利用时,它也会取消节点分配。
从云基础设施请求额外的节点
如果在创建新的 pod 之后,调度器无法将其调度到任何现有节点,则会分配一个新的节点。集群自动扩展器会关注这类 pod,并要求云服务提供商启动一个额外的节点。但在这样做之前,它会检查新节点是否能够容纳该 pod。毕竟,如果不行,启动这样的节点就没有意义了。
云服务提供商通常将节点分组为相同大小(或具有相同功能)的节点组(或池)。因此,集群自动扩展器不能简单地说“给我一个额外的节点。”它还需要指定节点类型。
集群自动扩展器通过检查可用的节点组,以确定是否至少有一种节点类型能够容纳未调度的 pod。如果恰好存在一个这样的节点组,自动扩展器可以增加节点组的大小,让云服务提供商向该组添加另一个节点。如果存在多个选项,自动扩展器必须选择最佳方案。显然,“最佳”的确切含义将需要可配置。在最坏的情况下,它会随机选择一个。集群自动扩展器对不可调度的 pod 的简单反应概述如图 15.5 所示。图 15.5。
图 15.5. 当集群自动扩展器找到一个无法调度到现有节点的 pod 时,它会进行扩展。

当新节点启动时,该节点上的 Kubelet 会联系 API 服务器,并通过创建节点资源来注册节点。从那时起,该节点就成为了 Kubernetes 集群的一部分,pod 可以被调度到该节点。
简单吗?那缩小规模怎么办?
放弃节点
当节点未被充分利用时,集群自动扩展器还需要缩小节点数量。自动扩展器通过监控所有节点上请求的 CPU 和内存来完成这项工作。如果给定节点上所有 pod 的 CPU 和内存请求都低于 50%,则认为该节点是不必要的。
这不是决定是否关闭节点的唯一决定因素。自动缩放器还会检查是否有任何系统 pod 仅在该节点上运行(除了那些在每个节点上运行的 pod,因为它们是由 DaemonSet 等部署的)。如果一个节点上运行了系统 pod,则该节点不会被释放。同样,如果一个未管理的 pod 或具有本地存储的 pod 在该节点上运行,因为这会导致 pod 提供的服务中断,也会出现这种情况。换句话说,只有当集群自动缩放器知道节点上运行的 pod 将被重新调度到其他节点时,节点才会返回到云提供商。
当一个节点被选中关闭时,该节点首先被标记为不可调度,然后从节点驱逐所有运行的 pod。因为这些 pod 都属于 ReplicaSet 或其他控制器,它们的替代品将被创建并调度到剩余的节点(这就是为什么正在关闭的节点首先被标记为不可调度)。
手动隔离和驱逐节点
节点也可以通过以下 kubectl 命令手动标记为不可调度并驱逐:
-
kubectl cordon <node>将节点标记为不可调度(但对该节点上运行的 pod 不做任何操作)。 -
kubectl drain <node>将节点标记为不可调度,然后从节点驱逐所有 pod。
在这两种情况下,在您再次使用 kubectl uncordon <node> 命令解除节点隔离之前,不会将新的 pod 调度到该节点。
15.3.2. 启用集群自动缩放
集群自动缩放目前可在
-
谷歌 Kubernetes 引擎 (GKE)
-
谷歌计算引擎 (GCE)
-
亚马逊网络服务 (AWS)
-
微软 Azure
启动自动缩放器的方式取决于您的 Kubernetes 集群运行的位置。对于在 GKE 上运行的 kubia 集群,您可以通过以下方式启用集群自动缩放:
$ gcloud container clusters update kubia --enable-autoscaling \``--min-nodes=3 --max-nodes=5
如果您的集群运行在 GCE 上,在运行 kube-up.sh 之前,您需要设置三个环境变量:
-
KUBE_ENABLE_CLUSTER_AUTOSCALER=true -
KUBE_AUTOSCALER_MIN_NODES=3 -
KUBE_AUTOSCALER_MAX_NODES=5
有关在其他平台上启用自动缩放器的信息,请参阅集群自动缩放器 GitHub 仓库:github.com/kubernetes/autoscaler/tree/master/cluster-autoscaler。
注意
集群自动缩放器将其状态发布到 kube-system 命名空间中的 cluster-autoscaler-status ConfigMap。
15.3.3. 限制集群缩小时服务中断
当一个节点意外失败时,您无法阻止其 pod 变得不可用。但是当一个节点自愿关闭,无论是集群自动缩放器还是由人工操作员关闭,您可以通过一个附加功能确保操作不会中断在该节点上运行的 pod 提供的服务。
某些服务要求始终运行一定数量的 Pod;这对于基于法定人数的集群应用程序尤其如此。因此,Kubernetes 提供了一种指定在执行这些类型操作时需要保持运行的最小 Pod 数量的方法。这是通过创建一个 PodDisruptionBudget 资源来实现的。
尽管资源的名称听起来很复杂,但它是最简单的 Kubernetes 资源之一。它仅包含一个 Pod 标签选择器和指定必须始终可用的最小 Pod 数量的数字,或者从 Kubernetes 版本 1.7 开始,可以不可用的最大 Pod 数量。我们将查看 PodDisruptionBudget (PDB) 资源清单的样子,但您将使用 kubectl create pod-disruptionbudget 创建它,然后稍后获取并检查 YAML。
如果您想确保您的 kubia Pod 的三个实例始终运行(它们具有标签 app=kubia),则可以创建一个类似这样的 PodDisruptionBudget 资源:
$ kubectl create pdb kubia-pdb --selector=app=kubia --min-available=3 poddisruptionbudget "kubia-pdb" created
简单,对吧?现在,检索 PDB 的 YAML。它将在下一个列表中显示。
列表 15.10. PodDisruptionBudget 定义
$ kubectl get pdb kubia-pdb -o yaml apiVersion: policy/v1beta1 kind: PodDisruptionBudget metadata: name: kubia-pdb spec: minAvailable: 3 1 selector: 2 matchLabels: 2 app: kubia 2 status: ...
-
1 应该始终有多少个 Pod 可用
-
2 确定此预算应用于哪些 Pod 的标签选择器
您还可以在 minAvailable 字段中使用百分比而不是绝对数字。例如,您可以声明所有带有 app=kubia 标签的 Pod 中有 60% 需要始终运行。
注意
从 Kubernetes 1.7 版本开始,PodDisruptionBudget 资源也支持 maxUnavailable 字段,如果您想阻止在更多 Pod 不可用的情况下驱逐,可以使用它而不是 minAvailable。
我们对这个资源没有更多要说的。只要它存在,集群自动扩展器和 kubectl drain 命令都将遵守它,并且如果驱逐 Pod 会将此类 Pod 的数量降至三个以下,则永远不会驱逐带有 app=kubia 标签的 Pod。
例如,如果有四个 Pod 总共,并且 minAvailable 设置为三个,就像示例中那样,Pod 驱逐过程将逐个驱逐 Pod,等待被驱逐的 Pod 被副本集控制器替换,然后再驱逐另一个 Pod。
15.4. 摘要
本章向您展示了 Kubernetes 如何扩展您的 Pod 和节点。您已经了解到
-
配置 Pod 的自动水平扩展与创建一个 Horizontal-PodAutoscaler 对象并将它指向 Deployment、ReplicaSet 或 ReplicationController,并指定 Pod 的目标 CPU 利用率一样简单。
-
除了让水平 Pod 自动缩放根据 Pod 的 CPU 利用率执行缩放操作外,您还可以配置它根据您自己的应用程序提供的自定义指标或与集群中部署的其他对象相关的指标进行缩放。
-
垂直 Pod 自动缩放目前尚不可行。
-
如果您的 Kubernetes 集群运行在受支持的云服务提供商上,甚至集群节点也可以自动缩放。
-
您可以使用带有
-it和--rm选项的kubectl run命令在 Pod 中运行一次性进程,并在您按下 CTRL+C 后自动停止并删除 Pod。
在下一章中,您将探索高级调度功能,例如如何将某些 Pod 与某些节点隔离开来,以及如何将 Pod 调度得既紧密又分散。
第十六章. 高级调度
本章涵盖
-
使用节点污点和 pod 容忍度将 pod 保持远离某些节点
-
将节点亲和性规则定义为节点选择器的替代方案
-
使用 pod 亲和性将 pod 部署在一起
-
使用 pod 反亲和性将 pod 保持彼此远离
Kubernetes 允许你影响 pod 的调度位置。最初,这仅通过在 pod 规范中指定节点选择器来完成,但后来又添加了额外的机制来扩展这一功能。这些内容在本章中介绍。
16.1. 使用污点和容忍度将 pod 从某些节点排斥
我们在这里将要探索的前两个与高级调度相关的功能是节点污点以及 pod 对这些污点的容忍度。它们用于限制哪些 pod 可以使用某个节点。只有当 pod 容忍节点的污点时,它才能被调度到该节点。
这与使用节点选择器和节点亲和性有些不同,你将在本章后面的内容中了解到。节点选择器和节点亲和性规则使得可以通过在 pod 中特别添加相关信息来选择 pod 可以或不可以被调度到哪些节点,而污点允许通过仅在节点上添加污点来拒绝 pod 的部署,而无需修改现有的 pod。你想要部署在污点节点上的 pod 需要选择加入以使用该节点,而与节点选择器不同,pod 明确指定它们想要部署到的节点。
16.1.1. 介绍污点和容忍度
了解节点污点的最佳途径是查看现有的污点。附录 B 展示了如何使用 kubeadm 工具设置多节点集群。默认情况下,此类集群中的主节点被污点标记,因此只有控制平面 pod 可以部署在其上。
显示节点的污点
你可以使用 kubectl describe node 来查看节点的污点,如下面的列表所示。
列表 16.1. 描述使用 kubeadm 创建的集群中的主节点
$ kubectl describe node master.k8s Name: master.k8s Role: master.k8s Labels: beta.kubernetes.io/arch=amd64 beta.kubernetes.io/os=linux kubernetes.io/hostname=master.k8s node-role.kubernetes.io/master= Annotations: node.alpha.kubernetes.io/ttl=0 volumes.kubernetes.io/controller-managed-attach-detach=true Taints: node-role.kubernetes.io/master:NoSchedule 1 ...
- 1 主节点有一个污点。
主节点有一个单一的污点。污点有一个键、一个值和一个效果,表示为 <key>=<value>:<effect>。前一个列表中显示的主节点的污点具有键 node-role.kubernetes.io/master,一个 null 值(在污点中未显示)和效果 NoSchedule。
这个污点阻止 pod 被调度到主节点,除非这些 pod 容忍这个污点。容忍它的 pod 通常都是系统 pod(参见图 16.1)。
图 16.1. 只有当 pod 容忍节点的污点时,才会将 pod 调度到节点。

显示 pod 的容忍度
在使用kubeadm安装的集群中,kube-proxy 集群组件作为 pod 在每个节点上运行,包括主节点,因为作为 pod 运行的 master 组件可能也需要访问 Kubernetes 服务。为了确保 kube-proxy pod 也运行在主节点上,它包括适当的容忍度。总共,该 pod 有三个容忍度,如下所示。
列表 16.2. pod 的容忍度
$ kubectl describe po kube-proxy-80wqm -n kube-system ... 容忍度: node-role.kubernetes.io/master=:NoSchedule node.alpha.kubernetes.io/notReady=:Exists:NoExecute node.alpha.kubernetes.io/unreachable=:Exists:NoExecute ...
如您所见,第一个容忍度与主节点的污点匹配,允许此 kube-proxy pod 被调度到主节点。
备注
忽略显示在 pod 的容忍度中但不在节点的污点中的等号。Kubectl 在污点/容忍度的值为null时似乎以不同的方式显示污点和容忍度。
理解污点效果
kube-proxy pod 上的另外两个容忍度定义了 pod 在未就绪或不可达的节点上运行的时间长度(秒数未显示,但可以在 pod 的 YAML 中看到)。这两个容忍度指的是NoExecute效果而不是NoSchedule效果。
每个污点都与一个效果相关联。存在三种可能的效果:
-
NoSchedule,这意味着如果 pod 不容忍污点,则不会将其调度到节点。 -
PreferNoSchedule是NoSchedule的软版本,意味着调度器会尝试避免将 pod 调度到节点,但如果无法在其他地方调度,则会将其调度到节点。 -
NoExecute与仅影响调度的NoSchedule和PreferNoSchedule不同,它还会影响节点上已运行的 pod。如果您向节点添加NoExecute污点,则已在该节点上运行且不容忍NoExecute污点的 pod 将被从节点驱逐。
16.1.2. 向节点添加自定义污点
想象一下拥有一个单一的 Kubernetes 集群,您在其中运行生产和非生产工作负载。非生产 pod 永远不会在生产节点上运行这一点至关重要。这可以通过向生产节点添加污点来实现。要添加污点,您使用kubectl taint命令:
$ kubectl taint node node1.k8s node-type=production:NoSchedule node "node1.k8s" 污点
这添加了一个具有键node-type、值production和NoSchedule效果的污点。如果您现在部署常规 pod 的多个副本,您将看到它们都没有被调度到您已污点的节点,如下所示。
列表 16.3. 部署没有容忍度的 pod
$ kubectl run test --image busybox --replicas 5 -- sleep 99999 deployment "test" created $ kubectl get po -o wide NAME READY STATUS RESTARTS AGE IP NODE test-196686-46ngl 1/1 Running 0 12s 10.47.0.1 node2.k8s test-196686-73p89 1/1 Running 0 12s 10.47.0.7 node2.k8s test-196686-77280 1/1 Running 0 12s 10.47.0.6 node2.k8s test-196686-h9m8f 1/1 Running 0 12s 10.47.0.5 node2.k8s test-196686-p85ll 1/1 Running 0 12s 10.47.0.4 node2.k8s
现在,没有人可以意外地将 Pod 部署到生产节点上。
16.1.3. 为 Pod 添加容忍度
要将生产 Pod 部署到生产节点,它们需要容忍您添加到节点上的污点。您生产 Pod 的清单需要包含以下列表中所示的 YAML 片段。
列表 16.4. 具有容忍度的生产 Deployment:production-deployment.yaml
apiVersion: extensions/v1beta1 kind: Deployment metadata: name: prod spec: replicas: 5 template: spec: ... tolerations: - key: node-type 1 Operator: Equal 1 value: production 1 effect: NoSchedule 1
- 1 此容忍度允许 Pod 被调度到生产节点。
如果您部署此 Deployment,您将看到其 Pod 被部署到生产节点,如下一列表所示。
列表 16.5. 具有容忍度的 Pod 被部署在生产node1上
$ kubectl get po -o wide NAME READY STATUS RESTARTS AGE IP NODE prod-350605-1ph5h 0/1 Running 0 16s 10.44.0.3 node1``.k8s prod-350605-ctqcr 1/1 Running 0 16s 10.47.0.4 node2.k8s prod-350605-f7pcc 0/1 Running 0 17s 10.44.0.6 node1``.k8s prod-350605-k7c8g 1/1 Running 0 17s 10.47.0.9 node2.k8s prod-350605-rp1nv 0/1 Running 0 17s 10.44.0.4 node1``.k8s
正如列表所示,生产型 Pod 也被部署到了node2,而它并不是一个生产节点。为了防止这种情况发生,您还需要对非生产节点施加一个污点,例如node-type=non-production:NoSchedule。然后,您还需要为所有非生产 Pod 添加相应的容忍度。
16.1.4. 理解污点和容忍度可以用于什么
节点可以有多个污点,Pod 也可以有多个容忍度。如您所见,污点只能有一个键和一个效果,不需要值。容忍度可以通过指定Equal运算符(如果不指定,这也是默认运算符)来容忍特定值,或者如果使用Exists运算符,它们可以容忍特定污点键的任何值。
在调度过程中使用污点和容忍度
污点可以用来防止新 Pod 的调度(NoSchedule效果),定义不首选的节点(PreferNoSchedule效果),甚至从节点驱逐现有的 Pod(NoExecute)。
你可以按任何你认为合适的方式设置污点和容忍度。例如,你可以将集群划分为多个分区,允许你的开发团队只将 Pod 调度到各自的节点。你也可以在多个节点提供特殊硬件且只有部分 Pod 需要使用该硬件时使用污点和容忍度。
配置节点故障后 Pod 重新调度的延迟时间
你还可以使用容忍度来指定在 Pod 所在的节点变得不可就绪或不可达后,Kubernetes 应该在重新调度 Pod 到另一个节点之前等待多长时间。如果你查看你的 Pod 的容忍度,你会看到两个容忍度,如下所示。
列表 16.6. 默认容忍度的 Pod
$ kubectl get po prod-350605-1ph5h -o yaml ... tolerations: - effect: NoExecute 1 key: node.alpha.kubernetes.io/notReady 1 operator: Exists 1 tolerationSeconds: 300 1 - effect: NoExecute 2 key: node.alpha.kubernetes.io/unreachable 2 operator: Exists 2 tolerationSeconds: 300 2
-
1 Pod 可以容忍节点 notReady 状态持续 300 秒,然后需要重新调度。
-
2 同样适用于节点不可达的情况。
这两个容忍度说明,这个 Pod 可以容忍节点在notReady或unreachable状态持续300秒。当 Kubernetes 控制平面检测到节点不再就绪或不可达时,它会在删除 Pod 并将其重新调度到另一个节点之前等待 300 秒。
这两个容忍度会自动添加到未定义它们的 Pod 中。如果这个五分钟的延迟对于你的 Pod 来说太长了,你可以通过将这两个容忍度添加到 Pod 的规范中来缩短延迟。
注意
这目前是一个 alpha 特性,因此它可能在 Kubernetes 的未来版本中发生变化。基于污点的驱逐默认也是未启用的。你可以通过运行带有--feature-gates=Taint-BasedEvictions=true选项的 Controller Manager 来启用它们。
16.2. 使用节点亲和将 Pod 吸引到特定节点
正如你所学的,污点(taints)用于将 Pod 从某些节点上移除。现在你将了解一种称为节点亲和(node affinity)的新机制,它允许你告诉 Kubernetes 只将 Pod 调度到特定的节点子集。
比较节点亲和与节点选择器
Kubernetes 早期版本中的初始节点亲和机制是 Pod 规范中的node-Selector字段。节点必须包含该字段中指定的所有标签才能有资格成为 Pod 的目标。
节点选择器完成了工作且很简单,但它们并不提供你可能需要的所有功能。因此,引入了一种更强大的机制。节点选择器最终将被弃用,因此了解新的节点亲和规则非常重要。
与节点选择器类似,每个 Pod 都可以定义自己的节点亲和性规则。这允许你指定硬要求或偏好。通过指定偏好,你告诉 Kubernetes 你希望为特定 Pod 使用的节点,Kubernetes 将尝试将 Pod 调度到这些节点之一。如果不可能,它将选择其他节点之一。
检查默认节点标签
节点亲和性根据节点的标签选择节点,这与节点选择器的方式相同。在你了解如何使用节点亲和性之前,让我们检查 Google Kubernetes Engine 集群(GKE)中的一个节点的标签,以查看默认节点标签是什么。它们在以下列表中显示。
列表 16.7. GKE 中节点的默认标签
$ kubectl describe node gke-kubia-default-pool-db274c5a-mjnf Name: gke-kubia-default-pool-db274c5a-mjnf Role: Labels: beta.kubernetes.io/arch=amd64 beta.kubernetes.io/fluentd-ds-ready=true beta.kubernetes.io/instance-type=f1-micro beta.kubernetes.io/os=linux cloud.google.com/gke-nodepool=default-pool failure-domain.beta.kubernetes.io/region=europe-west1 1 failure-domain.beta.kubernetes.io/zone=europe-west1-d 1 kubernetes.io/hostname=gke-kubia-default-pool-db274c5a-mjnf 1
- 1 这三个标签是与节点亲和性最相关的最重要的标签。
节点有许多标签,但在节点亲和性和 Pod 亲和性方面,最后三个标签是最重要的。你将在后面了解这些标签的含义。以下是对这三个标签含义的说明:
-
failure-domain.beta.kubernetes.io/region指定了节点所在的地理区域。 -
failure-domain.beta.kubernetes.io/zone指定了节点所在的可用区。 -
kubernetes.io/hostname显然是节点的主机名。
这些和其他标签可以在 Pod 亲和性规则中使用。在第三章中,你已经学习了如何向节点添加自定义标签并在 Pod 的节点选择器中使用它。你通过向 Pod 添加节点选择器来使用自定义标签,以便只在该标签的节点上部署 Pod。现在,你将看到如何使用节点亲和性规则来完成同样的操作。
16.2.1. 指定硬节点亲和规则
在第三章的示例中,你使用了节点选择器来部署一个只要求在具有 GPU 的节点上运行的 Pod。Pod 规范中包含了以下列表所示的nodeSelector字段。
列表 16.8. 使用节点选择器的 Pod:kubia-gpu-nodeselector.yaml
apiVersion: v1 kind: Pod metadata: name: kubia-gpu spec: nodeSelector: 1 gpu: "true" 1 ...
- 1 此 Pod 仅调度到具有
gpu=true标签的节点。
nodeSelector字段指定 Pod 应该只部署在包含gpu=true标签的节点上。如果你用节点亲和性规则替换节点选择器,Pod 定义将类似于以下列表。
列表 16.9. 使用nodeAffinity规则的 Pod:kubia-gpu-nodeaffinity.yaml
apiVersion: v1 kind: Pod metadata: name: kubia-gpu spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: gpu operator: In values: - "true"
您首先会注意到,这比简单的节点选择器要复杂得多。但这是因为它具有更强的表达能力。让我们详细检查这个规则。
理解长节点亲和性属性名称
如您所见,Pod 的 spec 部分包含一个包含nodeAffinity字段的affinity字段,该字段包含一个极其长的字段,所以让我们首先关注这个字段。
让我们将其分为两部分,并检查它们的意义:
-
requiredDuringScheduling...表示在此字段下定义的规则指定了节点必须具有的标签,以便 Pod 可以调度到该节点。 -
...IgnoredDuringExecution表示在此字段下定义的规则不会影响已经在节点上运行的 Pod。
在这一点上,让我通过让您知道亲和性目前仅影响 Pod 调度,永远不会导致 Pod 从节点中被驱逐,来简化事情。这就是为什么所有当前的规则都以IgnoredDuringExecution结束。最终,Kubernetes 也将支持RequiredDuringExecution,这意味着如果您从节点中删除一个标签,需要该标签的节点才能调度 Pod 的 Pod 将被从该节点驱逐。正如我所说的,这还不是 Kubernetes 支持的功能,所以我们不再关注那个长字段的第二部分。
理解 nodeSelectorTerms
通过记住上一节中解释的内容,很容易理解nodeSelectorTerms字段和matchExpressions字段定义了节点标签必须匹配哪些表达式,以便 Pod 可以调度到该节点。示例中的单个表达式很容易理解。节点必须有一个值为true的gpu标签。
因此,这个 Pod 将只调度到具有gpu=true标签的节点,如图 16.2 所示。
图 16.2. Pod 的节点亲和性指定了节点必须具有哪些标签,以便 Pod 可以调度到该节点。

现在是更有趣的部分。节点亲和性还允许您在调度期间优先考虑节点。我们将在下一部分查看这一点。
16.2.2. 在调度 Pod 时优先考虑节点
新引入的节点亲和性功能最大的好处是能够指定调度特定 Pod 时调度器应该优先考虑哪些节点。这是通过preferredDuringSchedulingIgnoredDuringExecution字段实现的。
想象一下,你拥有多个分布在不同国家的数据中心。每个数据中心代表一个单独的可用区。在每个区域,你有一些仅用于你自己的机器,以及一些你的合作伙伴公司可以使用的机器。现在你想要部署几个 Pod,你希望它们被调度到zone1以及为你公司部署预留的机器上。如果那些机器没有足够的空间来容纳 Pod,或者存在其他重要原因阻止它们在那里调度,你也会接受它们被调度到你的合作伙伴使用的机器和其他区域。节点亲和性允许你做到这一点。
标记节点
首先,节点需要被适当地标记。每个节点都需要一个标签来指定节点所属的可用区,以及一个标记来表明它是一个专用节点还是一个共享节点。
附录 B 解释了如何在本地运行的 VM 中设置一个三节点集群(一个主节点和两个工作节点)。在以下示例中,我将使用该集群中的两个工作节点,但你也可以使用 Google Kubernetes Engine 或任何其他多节点集群。
注意
Minikube 不是运行这些示例的最佳选择,因为它只运行一个节点。
首先,按照以下列表标记节点。
列表 16.10. 标记节点
$ kubectl label node node1.k8s availability-zone=zone1 node "node1.k8s" labeled $ kubectl label node node1.k8s share-type=dedicated node "node1.k8s" labeled $ kubectl label node node2.k8s availability-zone=zone2 node "node2.k8s" labeled $ kubectl label node node2.k8s share-type=shared node "node2.k8s" labeled $ kubectl get node -L availability-zone -L share-type NAME STATUS AGE VERSION AVAILABILITY-ZONE SHARE-TYPE master.k8s Ready 4d v1.6.4 <none> <none> node1.k8s Ready 4d v1.6.4 zone1 dedicated node2.k8s Ready 4d v1.6.4 zone2 shared
指定优先节点亲和规则
在设置了节点标签之后,你现在可以创建一个偏好于zone1中的dedicated节点的 Deployment。以下列表显示了 Deployment 的清单。
列表 16.11. 带有首选节点亲和性的 Deployment:preferred-deployment.yaml
apiVersion: extensions/v1beta1 kind: Deployment metadata: name: pref spec: ... spec: affinity: nodeAffinity: preferredDuringSchedulingIgnoredDuringExecution: 1 - weight: 80 2 preference: 2 matchExpressions: 2 - key: availability-zone 2 operator: In 2 values: 2 - zone1 2 - weight: 20 3 preference: 3 matchExpressions: 3 - key: share-type 3 operator: In 3 values: 3 - dedicated 3 ...
-
1 你指定的是偏好,而不是硬性要求。
-
2 你希望 Pod 被调度到 zone1。这是你最重要的偏好。
-
你还希望你的 Pod 被调度到专用节点上,但这比你的区域偏好重要四倍。
让我们仔细检查列表。你正在定义一个节点亲和性偏好,而不是一个硬性要求。你希望 Pod 被调度到包含标签availability-zone=zone1和share-type=dedicated的节点上。你通过将其weight设置为80来说明第一个偏好规则很重要,而第二个则不那么重要(weight设置为20)。
理解节点偏好如何工作
如果你的集群有很多节点,当在前面列表中调度 Deployment 的 Pod 时,节点会被分成四个组,如图 16.3 所示。图 16.3。具有与 Pod 节点亲和性匹配的availability-zone和share-type标签的节点排名最高。然后,由于 Pod 节点亲和性规则中配置的权重,接下来是zone1中的shared节点,然后是其他区域的dedicated节点,最后是所有其他节点。
图 16.3. 根据 Pod 的节点亲和性偏好优先级节点

在双节点集群中部署 Pod
如果你在一个双节点集群中创建这个 Deployment,你应该会看到大多数(如果不是全部)Pod 都部署到了node1。查看以下列表,看看这是否属实。
列表 16.12. 查看 Pod 的调度位置
$ kubectl get po -o wide
在创建的五个 Pod 中,有四个部署到了node1,只有一个部署到了node2。为什么其中一个 Pod 会部署到node2而不是node1呢?原因在于,除了节点亲和性优先级函数之外,调度器还会使用其他优先级函数来决定 Pod 的调度位置。其中之一就是Selector-SpreadPriority函数,它确保属于同一 ReplicaSet 或 Service 的 Pod 被分散到不同的节点上,这样节点故障就不会导致整个服务崩溃。这很可能是导致其中一个 Pod 被调度到node2的原因。
你可以尝试将 Deployment 扩展到 20 个或更多,你会发现大多数 Pod 都会被调度到node1。在我的测试中,只有两个 Pod 被调度到了node2。如果你没有定义任何节点亲和性偏好,Pod 将被均匀地分散到两个节点上。
16.3. 将具有亲和性和反亲和性的 Pod 放置在一起
你已经看到了如何使用节点亲和性规则来影响 Pod 调度到哪个节点。但这些规则只影响 Pod 与节点之间的亲和性,而有时你可能希望有指定 Pod 之间亲和性的能力。
例如,想象有一个前端 Pod 和一个后端 Pod。将这些 Pod 部署在彼此附近可以减少延迟并提高应用程序的性能。你可以使用节点亲和性规则来确保它们都部署到同一节点、机架或数据中心,但这样你就必须指定确切哪个节点、机架或数据中心来调度它们,这并不是最佳解决方案。更好的做法是让 Kubernetes 将你的 Pod 部署到它认为合适的地方,同时保持前端和后端 Pod 靠近。这可以通过使用 Pod 亲和性来实现。让我们通过一个例子来了解更多。
16.3.1. 使用节点亲和性将 Pod 部署到同一节点
你将部署一个后端 Pod 和五个前端 Pod 副本,并配置 Pod 亲和性,以确保它们都部署在后端 Pod 相同的节点上。
首先,部署后端 Pod:
$ kubectl run backend -l app=backend --image busybox -- sleep 999999 deployment "backend" created
这个 Deployment 没有任何特殊之处。你需要注意的唯一一件事是使用 -l 选项添加到 Pod 中的 app=backend 标签。这就是你将在前端 Pod 的 podAffinity 配置中使用的标签。
在 Pod 定义中指定 pod affinity
前端 Pod 的定义如下所示。
列表 16.13. 使用 podAffinity 的 Pod:frontend-podaffinity-host.yaml
apiVersion: extensions/v1beta1 kind: Deployment metadata: name: frontend spec: replicas: 5 template: ... spec: affinity: podAffinity: 1 requiredDuringSchedulingIgnoredDuringExecution: 2 - topologyKey: kubernetes.io/hostname 3 labelSelector: 3 matchLabels: 3 app: backend 3 ...
-
1 定义 podAffinity 规则
-
2 定义硬性要求,而不是偏好
-
3 此 Deployment 的 Pod 必须部署在与选择器匹配的 Pod 相同的节点上。
列表显示,这个 Deployment 将创建具有硬性要求部署在具有 app=backend 标签的 Pod 相同节点(由 topologyKey 字段指定)上的 Pod(参见图 16.4)。
图 16.4. Pod 亲和性允许将 Pod 调度到具有特定标签的其他 Pod 所在的节点。

注意
除了更简单的 matchLabels 字段外,你也可以使用更表达性的 matchExpressions 字段。
部署具有 Pod 亲和性的 Pod
在你创建此 Deployment 之前,让我们看看后端 Pod 之前被调度到了哪个节点:
$ kubectl get po -o wide NAME READY STATUS RESTARTS AGE IP NODE backend-257820-qhqj6 1/1 Running 0 8m 10.47.0.1 node2``.k8s
当你创建前端 Pod 时,它们也应该部署到 node2 上。你将创建 Deployment 并查看 Pod 的部署位置。这将在下一个列表中展示。
列表 16.14. 部署前端 Pod 并查看它们被调度到哪个节点
$ kubectl create -f frontend-podaffinity-host.yaml deployment "frontend" created $ kubectl get po -o wide NAME READY STATUS RESTARTS AGE IP NODE backend-257820-qhqj6 1/1 Running 0 8m 10.47.0.1 node2``.k8s frontend-121895-2c1ts 1/1 Running 0 13s 10.47.0.6 node2``.k8s frontend-121895-776m7 1/1 Running 0 13s 10.47.0.4 node2``.k8s frontend-121895-7ffsm 1/1 Running 0 13s 10.47.0.8 node2``.k8s frontend-121895-fpgm6 1/1 Running 0 13s 10.47.0.7 node2``.k8s frontend-121895-vb9ll 1/1 Running 0 13s 10.47.0.5 node2``.k8s
所有前端 Pod 确实都被调度到了与后端 Pod 相同的节点。在调度前端 Pod 时,调度器首先找到所有与前端 Pod podAffinity 配置中定义的 labelSelector 匹配的 Pod,然后将前端 Pod 调度到同一节点。
理解调度器如何使用 Pod 亲和性规则
有趣的是,如果你现在删除后端 Pod,即使调度器本身没有定义任何 Pod 亲和性规则(规则仅在前端 Pod 上),调度器也会将 Pod 调度到 node2。这是有道理的,因为否则如果后端 Pod 被意外删除并重新调度到不同的节点,前端 Pod 的亲和性规则将会被破坏。
如果你增加调度器的日志级别并检查其日志,你可以确认调度器考虑了其他 Pod 的 Pod 亲和性规则。以下列表显示了相关的日志行。
列表 16.15. 调度器日志显示为什么后端 Pod 被调度到 node2
... 尝试调度 Pod:default/backend-257820-qhqj6 ... ... ... backend-qhqj6 -> node2.k8s: 污点容忍优先级,得分:(10) ... backend-qhqj6 -> node1.k8s: 污点容忍优先级,得分:(10) ... backend-qhqj6 -> node2.k8s: Pod 亲和性优先级,得分:(10)``... backend-qhqj6 -> node1.k8s: Pod 亲和性优先级,得分:(0) ... backend-qhqj6 -> node2.k8s: 选择器扩散优先级,得分:(10) ... backend-qhqj6 -> node1.k8s: 选择器扩散优先级,得分:(10) ... backend-qhqj6 -> node2.k8s: 节点亲和性优先级,得分:(0) ... backend-qhqj6 -> node1.k8s: 节点亲和性优先级,得分:(0) ... 主节点 node2.k8s => 得分 100030 ... 主节点 node1.k8s => 得分 100022 ... 尝试将 backend-257820-qhqj6 绑定到 node2.k8s
如果你关注两条加粗的行,你将看到在调度后端 Pod 时,由于 Pod 亲和性,node2 收到的得分高于 node1。
16.3.2. 在同一机架、可用区或地理区域内部署 Pod
在前面的例子中,您使用了 podAffinity 将前端 Pod 部署到与后端 Pod 相同的节点上。您可能不希望所有前端 Pod 都运行在同一台机器上,但您仍然希望将它们保持靠近后端 Pod——例如,在同一个可用区运行它们。
在同一可用区协同放置 Pod
我使用的集群在我的本地机器上的三个虚拟机上运行,所以所有节点都在同一个可用区,换句话说。但如果节点在不同的区域,要运行与后端 Pod 在同一区域的前端 Pod,只需将 topologyKey 属性更改为 failure-domain.beta.kubernetes.io/zone。
在同一地理区域内协同放置 Pod
要允许 Pod 在同一区域而不是同一区域(云服务提供商通常在不同的地理区域拥有数据中心,并在每个区域中分割成多个可用区)部署,topologyKey 应设置为 failure-domain.beta.kubernetes.io/region。
理解 topologyKey 的工作原理
topologyKey 的工作方式很简单。我们之前提到的三个键并不特殊。如果您愿意,可以轻松地使用自己的 topologyKey,例如 rack,以便将 Pod 调度到同一服务器机架。唯一的前提是向您的节点添加一个 rack 标签。这种情况在 图 16.5 中有所展示。
图 16.5. podAffinity 中的 topologyKey 决定了 Pod 应该被调度到的范围。

例如,如果你有 20 个节点,每个机架有 10 个,你将前十个标记为 rack=rack1,其余的标记为 rack=rack2。然后,当定义 Pod 的 podAffinity 时,你会将 toplogyKey 设置为 rack。
当调度器决定部署 Pod 的位置时,它会检查 Pod 的 pod-Affinity 配置,找到匹配标签选择器的 Pod,并查找它们运行的节点。具体来说,它会查找节点标签中键与 podAffinity 中指定的 topologyKey 字段匹配的标签。然后,它选择所有标签与它之前找到的 Pod 的值匹配的节点。在 图 16.5 中,标签选择器匹配了运行在 Node 12 上的后端 Pod。该节点上 rack 标签的值等于 rack2,因此当调度前端 Pod 时,调度器将只选择具有 rack=rack2 标签的节点。
注意
默认情况下,标签选择器仅匹配与正在调度的 Pod 在同一命名空间中的 Pod。但您也可以通过添加与 label-Selector 同级的 namespaces 字段来选择来自其他命名空间的 Pod。
16.3.3. 表达 Pod 亲和力偏好而不是硬性要求
之前,当我们讨论节点亲和性时,你看到nodeAffinity可以用来表达一个硬性要求,这意味着 Pod 只被调度到符合节点亲和性规则的节点上。它也可以用来指定节点偏好,指示调度器将 Pod 调度到特定的节点,如果这些节点因为任何原因无法容纳 Pod,则允许调度到其他任何地方。
同样,这也适用于podAffinity。你可以告诉调度器你希望你的前端 Pod 被调度到与你的后端 Pod 相同的节点上,但如果那不可能,你也能接受它们被调度到其他地方。下面是一个使用preferredDuringSchedulingIgnoredDuringExecution Pod 亲和性规则的 Deployment 示例。
列表 16.16. Pod 亲和性偏好
apiVersion: extensions/v1beta1 kind: Deployment metadata: name: frontend spec: replicas: 5 template: ... spec: affinity: podAffinity: preferredDuringSchedulingIgnoredDuringExecution: 1 - weight: 80 2 podAffinityTerm: 2 topologyKey: kubernetes.io/hostname 2 labelSelector: 2 matchLabels: 2 app: backend 2 containers: ...
-
1 偏好而非必需
-
2 在上一个示例中指定了权重和一个 Pod 亲和性项
与nodeAffinity偏好规则一样,你需要为每个规则定义一个权重。你还需要指定topologyKey和labelSelector,就像硬性要求的podAffinity规则中那样。图 16.6 展示了这个场景。
图 16.6. 可以使用 Pod 亲和性来使调度器偏好运行具有特定标签的 Pod 的节点。

部署这个 Pod,就像你的nodeAffinity示例一样,将四个 Pod 部署到与后端 Pod 相同的节点上,另一个 Pod 部署到其他节点上(见以下列表)。
列表 16.17. 使用podAffinity偏好的 Pod 部署
$ kubectl get po -o wide NAME READY STATUS RESTARTS AGE IP NODE backend-257820-ssrgj 1/1 Running 0 1h 10.47.0.9 node2.k8s frontend-941083-3mff9 1/1 Running 0 8m 10.44.0.4 node1.k8s frontend-941083-7fp7d 1/1 Running 0 8m 10.47.0.6 node2.k8s frontend-941083-cq23b 1/1 Running 0 8m 10.47.0.1 node2.k8s frontend-941083-m70sw 1/1 Running 0 8m 10.47.0.5 node2.k8s frontend-941083-wsjv8 1/1 Running 0 8m 10.47.0.4 node2.k8s
16.3.4. 使用 Pod 反亲和性将 Pod 调度到彼此远离的位置
你已经看到了如何告诉调度器将 Pod 放置在一起,但有时你可能想要完全相反的效果。你可能希望将 Pod 彼此隔离开来。这被称为 Pod 反亲和性。它与 Pod 亲和性的指定方式相同,只是你使用podAntiAffinity属性而不是podAffinity,这会导致调度器永远不会选择运行匹配podAntiAffinity标签选择器的 Pod 的节点,如图 16.7 所示。
图 16.7. 使用 Pod 反亲和性将 Pod 与运行具有特定标签的 Pod 的节点隔离开来。

使用 Pod 反亲和性的一个例子是,当两组 Pod 在同一个节点上运行时,它们会相互干扰性能。在这种情况下,你希望告诉调度器永远不要将这些 Pod 调度到同一个节点上。另一个例子是强制调度器将同一组的 Pod 分散到不同的可用区或区域,这样整个区域(或区域)的故障永远不会完全使服务中断。
使用反亲和性将同一 Deployment 的 Pod 分散开来
让我们看看如何强制将你的前端 Pod 调度到不同的节点。以下列表显示了如何配置 Pod 的反亲和性。
列表 16.18. 具有反亲和性的 Pod:frontend-podantiaffinity-host.yaml
apiVersion: extensions/v1beta1 kind: Deployment metadata: name: frontend spec: replicas: 5 template: metadata: labels: app: frontend spec: affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: topologyKey: kubernetes.io/hostname labelSelector: matchLabels: app: frontend containers: ...
-
1 前端 Pod 具有 app=frontend 标签。
-
2 定义 Pod 反亲和性的硬性要求
-
3 前端 Pod 不得被调度到与具有 app=frontend 标签的 Pod 相同的机器上。
这次,你定义的是podAntiAffinity而不是podAffinity,并且你使labelSelector与 Deployment 创建的相同 Pod 匹配。让我们看看创建此 Deployment 时会发生什么。它创建的 Pod 如下所示。
列表 16.19. 由 Deployment 创建的 Pod
$ kubectl get po -l app=frontend -o wide NAME READY STATUS RESTARTS AGE IP NODE frontend-286632-0lffz 0/1 Pending 0 1m <none> frontend-286632-2rkcz 1/1 Running 0 1m 10.47.0.1 node2.k8s frontend-286632-4nwhp 0/1 Pending 0 1m <none> frontend-286632-h4686 0/1 Pending 0 1m <none> frontend-286632-st222 1/1 Running 0 1m 10.44.0.4 node1.k8s
如您所见,只有两个 Pod 被调度——一个调度到node1,另一个调度到node2。剩下的三个 Pod 都是Pending状态,因为调度器不允许将它们调度到相同的节点。
使用优先 Pod 反亲和性
在这种情况下,你可能应该指定一个软性要求而不是硬性要求(使用preferredDuringSchedulingIgnoredDuringExecution属性)。毕竟,如果两个前端 Pod 运行在同一节点上并不是一个大问题。但在那种情况下,使用requiredDuringScheduling是合适的。
与 Pod 亲和性一样,topologyKey属性决定了 Pod 不应部署到的范围。你可以使用它来确保 Pod 不会被部署到同一机架、可用区、区域或任何你使用自定义节点标签创建的任何自定义范围。
16.4. 摘要
在本章中,我们探讨了如何确保 Pod 不会被调度到某些节点或只被调度到特定节点,这可能是由于节点的标签或运行在其上的 Pod。
你了解到
-
如果你向节点添加污点,除非 Pod 容忍该污点,否则 Pod 不会被调度到该节点。
-
存在三种类型的污点:
NoSchedule完全阻止调度,Prefer-NoSchedule不那么严格,而NoExecute甚至可以将现有 Pod 从节点中驱逐出去。 -
NoExecute污点也用于指定当节点变得不可达或未准备好时,控制平面应该等待多长时间才重新调度 Pod。 -
节点亲和性允许你指定 Pod 应该调度到哪些节点。它可以用来指定硬性要求或仅表达节点偏好。
-
Pod 亲和性用于使调度器将 Pod 部署到运行另一个 Pod 的同一节点(基于 Pod 的标签)。
-
Pod 亲和性的
topologyKey指定了 Pod 应该部署得有多接近另一个 Pod(在同一节点上或在同一机架、可用区或可用区域内节点上)。 -
Pod 反亲和性可以用来保持某些 Pod 彼此远离。
-
与节点亲和性一样,Pod 亲和性和反亲和性都可以指定硬性要求或偏好。
在下一章中,你将了解开发应用程序的最佳实践以及如何在 Kubernetes 环境中使它们运行顺畅。
第十七章. 开发应用程序的最佳实践
本章涵盖
-
理解在典型应用程序中哪些 Kubernetes 资源出现
-
添加 post-start 和 pre-stop Pod 生命周期钩子
-
正确终止应用程序而不会破坏客户端请求
-
使应用程序在 Kubernetes 中易于管理
-
在 Pod 中使用 init 容器
-
使用 Minikube 在本地开发
我们现在已经涵盖了您在 Kubernetes 中运行应用程序所需了解的大部分内容。我们已经探讨了每个单独的资源做什么以及如何使用它。现在我们将看到如何在 Kubernetes 上运行的典型应用程序中将它们结合起来。我们还将看看如何使应用程序运行顺畅。毕竟,这就是使用 Kubernetes 的全部意义,对吧?
希望本章能帮助澄清任何误解,并解释那些尚未明确说明的事情。在这个过程中,我们还将介绍一些到目前为止尚未提到的额外概念。
17.1. 将一切整合
让我们先看看一个实际应用程序由什么组成。这将也给你一个机会看看你是否记得你所学到的所有内容,并看看大局。显示了在典型应用程序中使用的 Kubernetes 组件。
图 17.1. 典型应用程序中的资源

典型的应用程序清单包含一个或多个 Deployment 和/或 StatefulSet 对象。这些对象包括一个包含一个或多个容器的 Pod 模板,每个容器都有一个存活探针,以及为容器提供的(如果有的话)服务(s)的就绪探针。为其他 Pod 提供服务的 Pod 通过一个或多个 Service 进行暴露。当它们需要从集群外部可达时,Service 可以配置为 LoadBalancer 或 NodePort 类型的 Service,或者通过 Ingress 资源进行暴露。
Pod 模板(以及从中创建的 Pod)通常引用两种类型的 Secrets——用于从私有镜像仓库拉取容器镜像的 Secrets 和直接由 Pod 内运行的进程使用的 Secrets。Secrets 本身通常不是应用程序清单的一部分,因为它们不是由应用程序开发者配置的,而是由运维团队配置的。Secrets 通常分配给 Service-Accounts,这些 Service-Accounts 被分配给单个 Pod。
应用程序还包含一个或多个 ConfigMaps,这些 ConfigMaps 既可以用来初始化环境变量,也可以作为 configMap 卷挂载到 Pod 中。某些 Pod 使用额外的卷,例如 emptyDir 或 gitRepo 卷,而需要持久存储的 Pod 则使用 persistentVolumeClaim 卷。Persistent-VolumeClaims 也是应用程序清单的一部分,而它们所引用的 StorageClasses 则是由系统管理员预先创建的。
在某些情况下,应用程序还需要使用 Jobs 或 CronJobs。DaemonSets 通常不是应用程序部署的一部分,但通常由系统管理员创建,以在所有或部分节点上运行系统服务。HorizontalPodAutoscalers 要么由开发者包含在清单中,要么由运维团队在系统后期添加。集群管理员还会创建 LimitRange 和 ResourceQuota 对象,以保持单个 Pod 和所有 Pod(作为一个整体)的计算资源使用量在可控范围内。
应用程序部署后,各种 Kubernetes 控制器会自动创建额外的对象。这包括由 Endpoints 控制器创建的服务端点对象,由 Deployment 控制器创建的 ReplicaSet,以及由 ReplicaSet(或 Job、CronJob、StatefulSet 或 Daemon-Set)控制器实际创建的 Pod。
资源通常被标记为一个或多个标签以保持其组织有序。这不仅适用于 Pod,也适用于所有其他资源。除了标签之外,大多数资源还包含描述每个资源的注释,列出负责该资源的人员或团队的联系方式,或为管理和其他工具提供额外的元数据。
所有这些的中心是 Pod,可以说是 Kubernetes 最重要的资源。毕竟,每个应用程序都在其中运行。为了确保你知道如何开发能够充分利用其环境的应用程序,让我们最后一次仔细看看 Pod——这次是从应用程序的角度来看。
17.2. 理解 Pod 的生命周期
我们已经说过,Pod 可以与仅运行单个应用的 VM 进行比较。尽管在 Pod 内运行的应用程序与在 VM 中运行的应用程序没有太大区别,但确实存在一些显著差异。一个例子是,在 Pod 中运行的应用程序可以随时被终止,因为 Kubernetes 需要将 Pod 重新定位到另一个节点,原因可能是出于某种原因,也可能是由于缩放请求。我们将在下一节探讨这个方面。
17.2.1. 应用程序必须预期会被终止和重新定位
在 Kubernetes 之外,运行在 VM 中的应用程序很少从一个机器移动到另一个机器。当操作员移动应用程序时,他们也可以重新配置应用程序,并手动检查应用程序在新位置是否运行良好。在 Kubernetes 中,应用程序的迁移更加频繁和自动——没有人类操作员重新配置它们并确保迁移后仍然正常运行。这意味着应用程序开发者需要确保他们的应用程序允许相对频繁地移动。
预期本地 IP 和主机名会发生变化
当一个 Pod 被终止并在其他地方运行时(技术上,这是一个新的 Pod 实例替换了旧的 Pod;Pod 并没有被重新定位),它不仅有一个新的 IP 地址,还有一个新的名称和主机名。大多数无状态应用程序通常可以处理这种情况而不会产生任何不利影响,但状态化应用程序通常不能。我们已经了解到,可以通过 StatefulSet 运行状态化应用程序,这确保了当应用程序在重新调度后在新的节点上启动时,它仍然会看到之前相同的宿主机名和持久状态。尽管如此,Pod 的 IP 地址仍然会改变。应用程序需要为此做好准备。因此,应用程序开发者永远不应该基于集群应用程序成员的 IP 地址来确定成员资格,如果基于主机名,则始终应使用 StatefulSet。
期待写入磁盘的数据消失
另一点需要记住的是,如果应用程序将数据写入磁盘,那么在应用程序在新的 Pod 内部启动后,这些数据可能不可用,除非你在应用程序写入数据的位置挂载持久存储。应该清楚,当 Pod 重新调度时会发生这种情况,但即使在不涉及任何重新调度的场景中,写入磁盘的文件也会消失。即使在单个 Pod 的生命周期内,Pod 中运行的应用程序写入磁盘的文件也可能消失。让我用一个例子来解释这一点。
想象一个应用程序有一个漫长且计算密集型的初始启动过程。为了帮助应用程序在后续启动时更快地启动,开发者将应用程序的初始启动结果缓存到磁盘上(例如,在启动时扫描所有 Java 类以查找注解,然后将结果写入索引文件)。由于 Kubernetes 中的应用程序默认在容器中运行,这些文件会被写入容器的文件系统。如果容器随后被重启,它们都会丢失,因为新的容器从完全新的可写层开始(参见图 17.2)。
图 17.2. 当容器重启时,写入容器文件系统的文件会丢失。

不要忘记,单个容器可能由于多种原因被重启,例如进程崩溃、存活性检查返回失败,或者因为节点开始运行内存不足,进程被 OOMKiller 杀死。当这种情况发生时,Pod 仍然是相同的,但容器本身是完全新的。Kubelet 不会再次运行相同的容器;它总是创建一个新的容器。
使用卷来在容器重启之间保留数据
当其容器重启时,示例中的应用程序需要再次执行密集的启动程序。这可能或可能不是期望的。为了确保像这样的数据不会丢失,你需要至少使用一个 Pod 作用域的卷。因为卷与 Pod 共存亡,新的容器将能够重用前一个容器写入卷的数据(图 17.3)。
图 17.3. 使用卷在容器重启之间持久化数据

使用卷在容器重启之间保留文件有时是个好主意,但并不总是如此。如果数据被损坏并导致新创建的进程再次崩溃怎么办?这将导致持续崩溃循环(Pod 将显示CrashLoopBackOff状态)。如果你没有使用卷,新容器将从头开始启动,并且很可能会崩溃。像这样使用卷在容器重启之间保留文件是一把双刃剑。你需要仔细考虑是否使用它们。
17.2.2. 已死亡或部分死亡 Pod 的重新调度
如果一个 Pod 的容器持续崩溃,Kubelet 会无限期地重启它。重启之间的时间将以指数级增加,直到达到五分钟。在这五分钟的时间间隔内,Pod 实际上已经死亡,因为其容器的进程没有运行。公平地说,如果这是一个多容器 Pod,某些容器可能正常运行,所以 Pod 只是部分死亡。但如果 Pod 只包含一个容器,Pod 实际上已经死亡,完全无用,因为其中不再有进程运行。
你可能会惊讶地发现,即使这些 Pod 是 ReplicaSet 或类似控制器的一部分,它们也不会自动被移除并重新调度。如果你创建一个期望副本数为三的 ReplicaSet,然后其中一个 Pod 中的一个容器开始崩溃,Kubernetes 不会删除并替换 Pod。最终结果是 ReplicaSet 只有两个正确运行的副本,而不是期望的三(图 17.4)。
图 17.4. ReplicaSet 控制器不会重新调度已死亡的 Pod。

你可能预期 Pod 会被删除,并替换为另一个可能在其他节点上成功运行的 Pod 实例。毕竟,容器可能因为与节点相关的问题而崩溃,这个问题在其他节点上没有表现出来。遗憾的是,情况并非如此。ReplicaSet 控制器不在乎 Pod 是否已死亡——它只关心 Pod 的数量是否与期望的副本数匹配,在这种情况下,它确实匹配。
如果你想亲自查看,我已包含一个 ReplicaSet 的 YAML 清单,其 Pod 将不断崩溃(请参阅代码存档中的文件 replicaset-crashingpods.yaml)。如果你创建了 ReplicaSet 并检查创建的 Pod,你将看到以下列表。
列表 17.1. ReplicaSet 和持续崩溃的 Pod
$ kubectl get po NAME READY STATUS RESTARTS AGE crashing-pods-f1tcd 0/1 CrashLoopBackOff``5 6m 1 crashing-pods-k7l6k 0/1 CrashLoopBackOff 5 6m crashing-pods-z7l3v 0/1 CrashLoopBackOff 5 6m $ kubectl describe rs crashing-pods Name: crashing-pods Replicas: 3 current / 3 desired``2 Pods Status: 3 Running / 0 Waiting / 0 Succeeded / 0 Failed 3``$ kubectl describe po crashing-pods-f1tcd Name: crashing-pods-f1tcd Namespace: default Node: minikube/192.168.99.102 Start Time: Thu, 02 Mar 2017 14:02:23 +0100 Labels: app=crashing-pods Status: Running 4
-
1 Pod 的状态显示 Kubelet 正在延迟重启,因为容器持续崩溃。
-
2 控制器没有采取任何行动,因为当前副本数与期望副本数匹配
-
3 显示了 3 个副本正在运行。
-
4 kubectl describe 同样显示 Pod 的状态为运行
在某种程度上,Kubernetes 以这种方式运行是可以理解的。容器每五分钟会重启一次,希望解决崩溃的根本原因。其逻辑是,将 Pod 重新调度到另一个节点最可能也无法解决问题,因为应用程序运行在容器内,所有节点应该大致相同。这并不总是情况,但大多数情况下是这样的。
17.2.3. 按特定顺序启动 Pod
Pod 中运行的应用程序与手动管理中的应用程序之间还有一个区别是,部署这些应用的运维人员了解它们之间的依赖关系。这使得他们可以按顺序启动应用程序。
理解 Pod 的启动过程
当你使用 Kubernetes 运行你的多 Pod 应用程序时,你没有内置的方式来告诉 Kubernetes 先运行某些 Pod,其余的只有在第一个 Pod 已经启动并准备好服务时才运行。当然,你可以在发布第一个应用的清单之后等待 Pod 准备好,然后再发布第二个清单,但你的整个系统通常定义在单个 YAML 或 JSON 文件中,包含多个 Pod、服务和其他对象。
Kubernetes API 服务器确实会按照列表中的顺序处理 YAML/JSON 中的对象,但这仅仅意味着它们会按照这个顺序写入 etcd。你不能保证 Pod 也会按照这个顺序启动。
但是,你可以通过在 Pod 中包含一个 init 容器来防止 Pod 的主容器在满足预条件之前启动。
引入 Init 容器
除了常规容器之外,Pod 还可以包含 init 容器。正如其名所示,它们可以用来初始化 Pod——这通常意味着将数据写入 Pod 的卷,然后这些卷会被挂载到 Pod 的主容器中。
一个 pod 可以有任意数量的初始化容器。它们按顺序执行,并且只有最后一个完成之后,pod 的主容器才会启动。这意味着初始化容器也可以用来延迟 pod 主容器的启动——例如,直到满足某个先决条件。初始化容器可以等待 pod 主容器所需的服务启动并就绪。当它就绪时,初始化容器终止,并允许主容器启动。这样,主容器就不会在服务就绪之前使用该服务。
让我们来看一个使用初始化容器来延迟主容器启动的 pod 示例。还记得你在第七章中创建的 fortune pod 吗?它是一个返回幸运名言作为对客户端请求响应的 Web 服务器。现在,让我们假设你有一个 fortune-client pod,它需要在主容器启动之前,fortune 服务必须处于运行状态。你可以添加一个初始化容器,该容器会检查服务是否对请求做出响应。在得到响应之前,初始化容器会不断重试。一旦得到响应,初始化容器就会终止,并允许主容器启动。
将初始化容器添加到 pod 中
初始化容器可以在 pod 规范中定义,就像主容器一样,但通过 spec.initContainers 字段。你可以在本书的代码存档中找到 fortune-client pod 的完整 YAML。以下列表显示了定义初始化容器的部分。
列表 17.2. 在 pod 中定义的初始化容器:fortune-client.yaml
spec: initContainers: 1 - name: init image: busybox command: - sh - -c - 'while true; do echo "等待 fortune 服务启动...";' 2 wget http://fortune -q -T 1 -O /dev/null >/dev/null 2>/dev/null 2 && break; sleep 1; done; echo "服务已启动!启动主 2 容器。"``
-
1 你正在定义一个初始化容器,而不是一个普通容器。
-
2 初始化容器运行一个循环,直到 fortune 服务启动。
当你部署此 pod 时,只有它的初始化容器会启动。这在你使用 kubectl get 列出 pod 时 pod 的状态中显示:
$ kubectl get po NAME READY STATUS RESTARTS AGE fortune-client 0/1 Init:0/1 0 1m
STATUS 列显示零个或一个初始化容器已完成。你可以使用 kubectl logs 查看初始化容器的日志:
$ kubectl logs fortune-client -c init 等待 fortune 服务启动...
当运行 kubectl logs 命令时,你需要使用 -c 开关指定初始化容器的名称(在示例中,pod 的初始化容器名称为 init,如你在列表 17.2 中看到的)。
主容器将在你部署 fortune 服务和 fortune-server pod 之后才运行。你可以在 fortune-server.yaml 文件中找到它们。
处理 pod 间依赖关系的最佳实践
你已经看到了如何使用初始化容器来延迟启动 pod 的主容器(直到满足某个条件,例如确保 pod 所依赖的服务已就绪),但编写不需要在应用启动前依赖的所有服务都就绪的应用程序会更好。毕竟,服务也可能在应用已经运行后离线。
应用需要内部处理其依赖项可能未就绪的可能性。别忘了就绪性探针。如果一个应用因为其依赖项之一缺失而无法执行其工作,它应该通过其就绪性探针发出信号,这样 Kubernetes 就知道它也不就绪。你想要这样做不仅因为这样可以防止应用被添加为服务端点,而且因为应用的就绪性也被 Deployment 控制器在执行滚动更新时使用,从而防止推出一个坏版本。
17.2.4. 添加生命周期钩子
我们已经讨论了如何使用初始化容器来挂钩 pod 的启动,但 pod 也允许你定义两个生命周期钩子:
-
启动后钩子
-
预停止钩子
这些生命周期钩子是针对每个容器指定的,与初始化容器不同,初始化容器适用于整个 pod。正如它们的名称所暗示的,它们在容器启动时执行,在容器停止前执行。
生命周期钩子与存活性和就绪性探针类似,因为它们可以
-
在容器内执行命令
-
对一个 URL 执行 HTTP GET 请求
让我们分别看看这两个钩子,看看它们对容器生命周期有什么影响。
使用启动后容器生命周期钩子
一个启动后钩子在容器的主进程启动后立即执行。你用它来在应用启动时执行额外的操作。当然,如果你是运行在容器中的应用的作者,你总是可以在应用代码内部执行这些操作。但当你运行由其他人开发的应用时,你通常不希望(或不能)修改其源代码。启动后钩子允许你在不接触应用的情况下运行额外的命令。这些可能向外部监听器发出应用正在启动的信号,或者初始化应用以便它可以开始执行其工作。
钩子与主进程并行运行。这个名字可能有些误导,因为它不会等待主进程完全启动(如果进程有一个初始化过程,Kubelet 显然不能等待该过程完成,因为它没有方法知道何时完成)。
但即使钩子是异步运行的,它也会以两种方式影响容器。直到钩子完成,容器将保持 Waiting 状态,原因标记为 ContainerCreating。因此,Pod 的状态将是 Pending 而不是 Running。如果钩子运行失败或返回非零退出代码,主容器将被终止。
包含启动后钩子的 Pod 清单看起来如下所示。
列表 17.3. 包含启动后生命周期钩子的 Pod:post-start-hook.yaml
apiVersion: v1 kind: Pod metadata: name: pod-with-poststart-hook spec: containers: - image: luksa/kubia name: kubia lifecycle: 1 postStart: 1 exec: 2 command: 2 - sh 2 - -c 2 - "echo 'hook will fail with exit code 15'; sleep 5; exit 15" 2
-
1 钩子在容器启动时执行。
-
2 它在容器内 /bin 目录中执行 postStart.sh 脚本。
在示例中,echo、sleep 和 exit 命令与容器的主进程一起在容器创建时执行。你通常不会运行这样的命令,而是会运行存储在容器镜像中的 shell 脚本或二进制可执行文件。
很遗憾,如果由钩子启动的过程将日志记录到标准输出,你将无法在任何地方看到输出。这使得调试生命周期钩子变得痛苦。如果钩子失败,你只能在 Pod 的事件中看到 FailedPostStartHook 警告(你可以使用 kubectl describe pod 来查看它们)。稍后,你将看到更多关于钩子失败原因的信息,如下所示。
列表 17.4. 显示失败命令基于钩子的退出代码的 Pod 事件
FailedSync``Error syncing pod, skipping: failed to "StartContainer" for "kubia" with PostStart handler: command 'sh -c echo 'hook will fail with exit code 15'; sleep 5 ; exit 15' exited with 15``: : "PostStart Hook Failed"
最后行中的数字 15 是命令的退出代码。当使用 HTTP GET 钩子处理程序时,原因可能看起来如下所示(你可以通过从本书的代码存档中部署 post-start-hook-httpget.yaml 文件来尝试此操作)。
列表 17.5. 显示 HTTP GET 钩子失败原因的 Pod 事件
FailedSync``Error syncing pod, skipping: failed to "StartContainer" for "kubia" with PostStart handler: Get http://10.32.0.2:9090/postStart: dial tcp 10.32.0.2:9090: getsockopt: connection refused``: "PostStart Hook Failed"
注意
启动后钩子故意配置错误,使用端口 9090 而不是正确的端口 8080,以展示钩子失败时会发生什么。
基于命令的启动后钩子的标准输出和错误输出不会记录在任何地方,因此你可能希望钩子调用的进程将日志记录到容器文件系统中的文件,这将允许你使用类似以下方式检查文件内容:
$ kubectl exec my-pod cat logfile.txt
如果由于任何原因(包括钩子失败)容器被重新启动,文件可能在你可以检查它之前就已经消失了。你可以通过将emptyDir卷挂载到容器中,并让钩子将其写入,来解决这个问题。
使用预停止容器生命周期钩子
预停止钩子在容器终止前立即执行。当容器需要被终止时,如果配置了预停止钩子,Kubelet 将运行它,然后只向进程发送SIGTERM信号(如果进程没有优雅地终止,稍后将其杀死)。
如果容器在接收到SIGTERM信号后没有优雅地关闭,可以使用预停止钩子来启动容器的优雅关闭。它们还可以在关闭前执行任意操作,而无需在应用程序本身中实现这些操作(当你运行一个第三方应用程序,且没有访问其源代码或无法修改它时,这很有用)。
在 pod 清单中配置预停止钩子与添加启动后钩子没有太大区别。之前的例子展示了执行命令的启动后钩子,所以现在我们将看看执行 HTTP GET 请求的预停止钩子。以下列表显示了如何在 pod 中定义预停止 HTTP GET 钩子。
列表 17.6. 预停止钩子 YAML 片段:pre-stop-hook-httpget.yaml
lifecycle: preStop: 1 httpGet: 1 port: 8080 2 path: shutdown 2
-
1 这是一个执行 HTTP GET 请求的预停止钩子。
-
2 请求发送到
POD_IP:8080/shutdown。
本列表中定义的预停止钩子会在 Kubelet 开始终止容器时立即执行一个 HTTP GET 请求到POD_IP:8080/shutdown。除了列表中显示的port和path之外,你还可以设置scheme字段(HTTP 或 HTTPS)、host以及应发送到请求中的httpHeaders。host字段默认为 pod IP。请确保不要将其设置为 localhost,因为 localhost 将指向节点,而不是 pod。
与启动后钩子相比,无论钩子的结果如何——使用基于命令的钩子时,错误 HTTP 响应代码或非零退出代码都不会阻止容器被终止。如果预停止钩子失败,你将在 pod 的事件中看到FailedPreStopHook警告事件,但由于 pod 随后很快被删除(毕竟,pod 的删除最初触发了预停止钩子),你可能甚至都没有注意到预停止钩子未能正确运行。
提示
如果预停止钩子的成功完成对于系统的正常操作至关重要,请验证它是否真的被执行了。我曾目睹过预停止钩子没有运行,而开发者甚至都没有意识到这一点。
由于你的应用程序没有接收到 SIGTERM 信号而使用停止前钩子
许多开发者犯了一个错误,就是仅仅为了在停止前钩子中向他们的应用程序发送SIGTERM信号而定义一个停止前钩子。他们这样做是因为他们没有看到他们的应用程序接收到由 Kubelet 发送的SIGTERM信号。应用程序没有接收到信号的原因并不是 Kubernetes 没有发送它,而是信号没有传递到容器内部的 app 进程。如果你的容器镜像配置为运行一个 shell,该 shell 又运行 app 进程,信号可能会被 shell 本身消耗掉,而不是传递给子进程。
在这种情况下,与其在停止前钩子中直接向你的应用程序发送信号,正确的修复方法是确保 shell 将信号传递给应用程序。这可以通过在作为主容器进程运行的 shell 脚本中处理信号并将其传递给应用程序来实现。或者你也可以不配置容器镜像以运行 shell,而是直接运行应用程序的二进制文件。你可以通过在 Dockerfile 中使用ENTRYPOINT或CMD的 exec 形式来实现这一点:ENTRYPOINT ["/mybinary"]而不是ENTRYPOINT /mybinary。
使用第一种形式的容器以mybinary可执行文件作为其主进程运行,而第二种形式则以 shell 作为主进程,并将mybinary进程作为 shell 进程的子进程执行。
理解生命周期钩子针对的是容器,而不是 Pod
关于启动后和停止前的钩子,让我最后强调一点,这些生命周期钩子与容器相关,而不是与 Pod 相关。你不应该使用停止前钩子来执行在 Pod 终止时需要执行的操作。原因是停止前钩子在容器被终止时会被调用(很可能是由于存活性探测失败)。这种情况在 Pod 的生命周期中可能发生多次,而不仅仅是当 Pod 正在关闭过程中。
17.2.5. 理解 Pod 关闭
我们已经提到了 Pod 终止的话题,那么让我们更详细地探讨这个话题,并了解在 Pod 关闭期间确切发生了什么。这对于理解如何干净地关闭在 Pod 中运行的应用程序非常重要。
让我们从开始讲起。Pod 的关闭是由 API 服务器通过删除 Pod 对象触发的。当收到 HTTP DELETE 请求时,API 服务器不会立即删除对象,而是在其中设置一个deletionTimestamp字段。设置了deletionTimestamp字段的 Pod 正在终止。
一旦 Kubelet 注意到 Pod 需要被终止,它就开始终止 Pod 中的每个容器。它给每个容器时间来优雅地关闭,但时间是有限的。这个时间被称为终止宽限期,并且可以按 Pod 进行配置。计时器在终止过程开始时启动。然后执行以下事件序列:
-
如果已配置,运行预停止钩子,并等待其完成。
-
向容器的主进程发送
SIGTERM信号。 -
等待容器干净地关闭或直到终止宽限期结束。
-
如果进程还没有优雅地终止,则使用
SIGKILL强制终止进程。
事件序列如图 17.5 所示。figure 17.5。
图 17.5. 容器终止序列

指定终止宽限期
可以通过设置 spec.terminationGracePeriodSeconds 字段在 pod 规范中配置终止宽限期。默认值为 30,这意味着 pod 的容器在被强制杀死之前将获得 30 秒的时间来优雅地终止。
提示
你应该将宽限期设置得足够长,以便你的进程可以在那段时间内完成清理。
在删除 pod 时,也可以覆盖 pod 规范中指定的宽限期:
$ kubectl delete po mypod --grace-period=5
这将使 Kubelet 等待五秒钟,直到 pod 优雅地关闭。当 pod 的所有容器都停止时,Kubelet 会通知 API 服务器,并且 Pod 资源最终被删除。你可以通过将宽限期设置为零并添加 --force 选项来强制 API 服务器立即删除资源,而不必等待确认,如下所示:
$ kubectl delete po mypod --grace-period=0 --force
使用此选项时要小心,特别是与有状态集的 pod 一起使用。有状态集控制器非常小心,从不同时运行相同 pod 的两个实例(具有相同序号索引和名称,并附加到同一持久卷的两个 pod)。通过强制删除 pod,你将导致控制器在没有等待被删除 pod 的容器关闭的情况下创建替换 pod。换句话说,同一 pod 的两个实例可能会同时运行,这可能导致你的有状态集群出现故障。只有在你绝对确定 pod 已经不再运行或无法与集群的其他成员通信时(当你确认托管 pod 的节点已失败或已从网络断开且无法重新连接时),才强制删除有状态 pod。
现在你已经了解了容器是如何关闭的,让我们从应用程序的角度来看,并回顾一下应用程序应该如何处理关闭过程。
在你的应用程序中实现适当的关闭处理程序
应用程序应该通过启动它们的关闭过程并在完成后终止来响应 SIGTERM 信号。除了处理 SIGTERM 信号外,应用程序还可以通过预停止钩子来通知关闭。在两种情况下,应用程序都只有固定的时间来干净地终止。
但如果你无法预测应用程序干净关闭需要多长时间怎么办?例如,想象你的应用程序是一个分布式数据存储。在缩小时,一个 Pod 实例将被删除并因此关闭。在关闭过程中,Pod 需要将所有数据迁移到剩余的 Pod 中,以确保数据不会丢失。Pod 是否应该在收到终止信号(通过 SIGTERM 信号或通过 pre-stop 钩子)时开始迁移数据?
绝对不行!至少有以下两个原因不建议这样做:
-
容器终止并不一定意味着整个 Pod 正在被终止。
-
你没有任何保证关闭程序会在进程被杀死之前完成。
这种第二种情况不仅发生在应用程序在优雅关闭过程中 grace period 超出之前,还发生在运行 Pod 的节点在容器关闭序列中途失败时。即使节点随后重新启动,Kubelet 也不会重新启动关闭程序(甚至不会再次启动容器)。绝对没有保证 Pod 能够完成整个关闭程序。
用专用关闭程序 Pod 替换关键关闭程序
你如何确保绝对必须运行到完成的临界关闭程序确实运行到完成(例如,确保 Pod 的数据迁移到其他 Pod)?
一种解决方案是应用程序(在收到终止信号后)创建一个新的 Job 资源,该资源将运行一个新的 Pod,其唯一任务是迁移被删除 Pod 的数据到剩余的 Pod 中。但如果你一直很注意,你就会知道你没有任何保证应用程序确实每次都能成功创建 Job 对象。如果节点在应用程序尝试这样做时失败,那会怎样?
处理这个问题的正确方法是运行一个专用、持续运行的 Pod,不断检查孤儿数据的存在。当这个 Pod 发现孤儿数据时,它可以将其迁移到剩余的 Pod 中。除了持续运行的 Pod 之外,您还可以使用 CronJob 资源定期运行 Pod。
你可能会认为 StatefulSets 可以在这里有所帮助,但它们并不能。正如你将记得的那样,缩小 StatefulSet 会导致 PersistentVolumeClaims 成孤儿,使得存储在 PersistentVolume 上的数据变得孤立。是的,在随后的扩展中,Persistent-Volume 将重新连接到新的 Pod 实例,但如果没有发生扩展(或者发生得很晚),那会怎样?因此,当使用 StatefulSets 时,你可能还想要运行一个数据迁移 Pod(此场景在图 17.6 中显示)。为了防止在应用程序升级期间发生迁移,数据迁移 Pod 可以配置为等待一段时间,以便在执行迁移之前给有状态的 Pod 时间重新启动。
图 17.6. 使用专用 Pod 迁移数据

17.3. 确保所有客户端请求得到妥善处理
您现在对如何使 Pod 干净地关闭有了很好的理解。现在,我们将从 Pod 客户端的角度来看待 Pod 的生命周期(客户端正在消费 Pod 提供的服务)。如果您不想在扩展 Pod 时遇到问题,这一点很重要。
毋庸置疑,您希望所有客户端请求都得到妥善处理。显然,您不希望在 Pod 启动或关闭时看到断开连接。仅凭 Kubernetes 本身并不能防止这种情况发生。您的应用程序需要遵循一些规则来防止断开连接。首先,让我们专注于确保在 Pod 启动时所有连接都得到妥善处理。
17.3.1. 防止 Pod 启动时客户端连接断开
如果您理解服务和服务端点的工作方式,确保在 Pod 启动时每个连接都得到妥善处理是简单的。当 Pod 启动时,它被添加为所有标签选择器与 Pod 标签匹配的服务的端点。如您可能记得的第五章,Pod 还需要向 Kubernetes 发出就绪信号。直到它就绪,它不会成为服务端点,因此不会从客户端接收任何请求。
如果您在 Pod 规范中没有指定就绪探针,则 Pod 始终被认为是就绪的。它将几乎立即开始接收请求——一旦第一个 kube-proxy 在其节点上更新了iptables规则,并且第一个客户端 Pod 尝试连接到服务。如果您的应用程序当时还没有准备好接受连接,客户端将看到“连接拒绝”类型的错误。
您需要做的只是确保您的就绪探针仅在您的应用程序准备好妥善处理传入请求时返回成功。一个好的第一步是添加一个 HTTP GET 就绪探针,并将其指向您应用程序的基本 URL。在许多情况下,这足以让您走得很远,并让您免于在应用程序中实现特殊的就绪端点。
17.3.2. 防止 Pod 关闭期间的连接断开
现在我们来看看一个 Pod 生命周期的另一端会发生什么——当 Pod 被删除并且其容器被终止时。我们已经讨论了 Pod 的容器应该在接收到SIGTERM信号(或者当其预停止钩子被执行)后立即干净地关闭。但是,这能确保所有客户端请求都得到妥善处理吗?
当应用程序接收到终止信号时,它应该如何表现?它应该继续接受请求吗?对于已经接收但尚未完成的请求怎么办?对于可能处于请求之间但处于打开状态的持久 HTTP 连接怎么办(当连接上没有活跃请求时)?在我们可以回答这些问题之前,我们需要详细查看当 Pod 被删除时在集群中展开的事件链。
理解 pod 删除时发生的事件序列
在 第十一章 中,我们深入探讨了构成 Kubernetes 集群的组件。你需要始终记住,这些组件在多台机器上作为单独的进程运行。它们并不都是单一大型单体进程的一部分。所有组件都达到关于集群状态的共识需要时间。让我们通过查看 pod 被删除时集群中发生的情况来探索这一事实。
当 API 服务器接收到 pod 删除请求时,它首先在 etcd 中修改状态,然后通知其监视者关于删除的消息。在这些监视者中包括 Kubelet 和端点控制器。这两个并行发生的事件序列(用 A 或 B 标记),在 图 17.7 中显示。
图 17.7. 删除 pod 时发生的事件序列

在事件序列 A 中,你会看到,一旦 Kubelet 收到 pod 应该终止的通知,它就会启动关闭序列,如 第 17.2.5 节 所解释的那样(运行预停止钩子,发送 SIGTERM,等待一段时间,如果容器尚未自行终止,则强制杀死容器)。如果应用程序通过立即停止接收客户端请求来响应 SIGTERM,那么任何尝试连接到它的客户端都会收到连接拒绝错误。由于从 pod 删除到发生此事件的时间相对较短,这是由于 API 服务器到 Kubelet 的直接路径。
现在,让我们看看在另一个事件序列中会发生什么——即 pod 从 iptables 规则中移除的事件序列(图中的序列 B)。当端点控制器(在 Kubernetes 控制平面的控制器管理器中运行)接收到 pod 被删除的通知时,它会将 pod 从 pod 所在的所有服务中的端点移除。它是通过向 API 服务器发送 REST 请求来修改端点 API 对象来做到这一点的。然后 API 服务器通知所有监视端点对象的客户端。在这些监视者中包括所有在工作节点上运行的 kube-proxies。然后,每个代理都会更新其节点上的 iptables 规则,这是防止新连接被转发到正在终止的 pod 的原因。这里的一个重要细节是,移除 iptables 规则对现有连接没有影响——已经连接到 pod 的客户端仍然会通过这些现有连接向 pod 发送额外的请求。
这两个事件序列都是并行发生的。很可能关闭 Pod 中应用程序进程所需的时间略短于更新 iptables 规则所需的时间。导致更新 iptables 规则的事件链相当长(见 图 17.8),因为事件必须首先到达 Endpoints 控制器,然后控制器向 API 服务器发送新的请求,然后 API 服务器必须通知 kube-proxy,最后代理才会修改 iptables 规则。有很大可能性,SIGTERM 信号会在所有节点上的 iptables 规则更新之前被发送。
图 17.8. 删除 Pod 时的事件时间线

最终结果是,Pod 在收到终止信号后可能仍然会收到客户端请求。如果应用程序立即关闭服务器套接字并停止接受连接,这将导致客户端收到“连接被拒绝”类型的错误(类似于 Pod 启动时如果应用程序无法立即接受连接且没有为其定义就绪探针时发生的情况)。
解决问题
在 Google 上搜索这个问题的解决方案,似乎添加一个就绪探针到你的 Pod 就能解决这个问题。据说,你所需要做的就是让就绪探针在 Pod 收到 SIGTERM 信号后立即开始失败。这应该会导致 Pod 作为服务的端点被移除。但是,移除只会发生在就绪探针连续失败几次之后(这在就绪探针规范中是可配置的)。显然,移除后还需要到达 kube-proxy,然后 Pod 才会从 iptables 规则中移除。
实际上,就整个流程而言,就绪探针根本没有任何影响。当 Endpoints 控制器收到删除 Pod 的通知(当 Pod 的 spec 中的 deletionTimestamp 字段不再是 null 时),它就会立即将 Pod 从服务端点中移除。从那时起,就绪探针的结果就无关紧要了。
问题的正确解决方案是什么?你如何确保所有请求都得到完全处理?
很明显,Pod 需要在接收到终止信号后继续接受连接,直到所有 kube-proxies 完成更新 iptables 规则。嗯,不仅仅是 kube-proxies。还可能有 Ingress 控制器或负载均衡器直接将连接转发到 Pod,而不通过 Service (iptables)。这还包括使用客户端负载均衡的客户端。为了确保没有任何客户端经历断开连接的情况,你必须等待所有客户端以某种方式通知你,他们将不再将连接转发到 Pod。
这是不可能的,因为所有这些组件都分布在不同计算机上。即使你知道每一个组件的位置,并且可以等待它们都表示可以关闭 pod,如果其中一个没有响应怎么办?你等待响应需要多长时间?记住,在这段时间里,你正在阻碍关闭过程。
你唯一合理能做的事情是等待足够长的时间以确保所有代理都完成了它们的工作。但多长时间才算足够长?在大多数情况下,几秒钟应该足够了,但无法保证每次都足够。当 API 服务器或端点控制器过载时,通知到达 kube-proxy 可能需要更长的时间。重要的是要理解你无法完美解决这个问题,但即使添加 5 或 10 秒的延迟也应该显著改善用户体验。你可以使用更长的延迟,但不要过度,因为延迟将阻止容器及时关闭,并导致 pod 在被删除很久之后仍然出现在列表中,这对删除 pod 的用户来说总是令人沮丧的。
总结本节内容
总结一下——正确关闭应用包括以下步骤:
-
等待几秒钟,然后停止接受新的连接。
-
关闭所有不在请求中间的保持连接。
-
等待所有活跃的请求完成。
-
然后完全关闭。
要了解在此过程中连接和请求的情况,仔细检查图 17.9。
图 17.9. 接收到终止信号后正确处理现有和新连接

这并不像一收到终止信号就立即退出进程那么简单,对吧?这样做值得吗?这由你决定。但至少你可以添加一个预停止钩子,等待几秒钟,就像下面列表中的那样,也许。
列表 17.7. 用于防止连接损坏的预停止钩子
lifecycle: preStop: exec: command: - sh - -c - "sleep 5"
这样,你根本不需要修改你应用的代码。如果你的应用已经确保所有进行中的请求都被完全处理,那么这个预停止延迟可能就是你所需要的全部。
17.4. 制作易于在 Kubernetes 中运行和管理的应用
希望你现在对如何让你的应用优雅地处理客户端有了更好的理解。现在我们将探讨应用应该如何构建以便在 Kubernetes 中更容易管理。
17.4.1. 制作可管理的容器镜像
当你将应用打包成镜像时,你可以选择包含应用的二进制可执行文件和它需要的任何附加库,或者你可以将整个操作系统文件系统与应用一起打包。太多的人这样做,尽管这通常是不必要的。
你是否需要在图像中包含操作系统分布的每个文件?可能不是。大多数文件永远不会被使用,会使你的图像比所需的更大。当然,图像分层确保每个单独的层只下载一次,但即使第一次将 pod 调度到节点时需要等待更长的时间也是不理想的。
部署新的 pod 并对其进行扩展应该是快速的。这要求拥有没有不必要的冗余的小图像。如果你使用 Go 语言构建应用程序,你的图像不需要包含除了应用程序的单个可执行二进制文件之外的其他任何内容。这使得基于 Go 的容器图像非常小,非常适合 Kubernetes。
小贴士
在 Dockerfile 中使用FROM scratch指令来创建这些图像。
但在实践中,你很快会发现这些最小图像极其难以调试。当你第一次需要在容器内部运行像ping、dig、curl或类似工具时,你会意识到容器图像也至少需要包括这些工具的有限集合是多么重要。我无法告诉你应该在图像中包含什么和排除什么,因为这取决于你如何做事,所以你需要自己找到最佳平衡点。
17.4.2. 正确标记您的图像并明智地使用 imagePullPolicy
你很快就会了解到,在您的 pod 配置文件中引用latest图像标签会导致问题,因为你无法确定每个单独的 pod 副本正在运行哪个版本的图像。即使最初所有 pod 副本都运行相同的图像版本,如果你在latest标签下推送了新版本的图像,然后 pod 被重新调度(或者你扩展了 Deployment),新的 pod 将运行新版本,而旧的 pod 仍然会运行旧版本。此外,使用latest标签使得无法回滚到之前的版本(除非你再次推送图像的旧版本)。
几乎强制使用包含适当版本指定符的标签,而不是latest,除了可能在开发环境中。记住,如果你使用可变标签(你向相同的标签推送更改),你需要将 pod 规范中的imagePullPolicy字段设置为Always。但如果你在生产 pod 中使用它,请注意与之相关的大问题。如果图像拉取策略设置为Always,容器运行时会每次部署新 pod 时都联系图像注册库。这会稍微减慢 pod 的启动速度,因为节点需要检查图像是否已被修改。更糟糕的是,此策略阻止 pod 在无法联系注册库时启动。
17.4.3. 使用多维标签而不是单维标签
不要忘记标记所有资源,而不仅仅是 Pod。确保为每个资源添加多个标签,这样它们就可以在每个单独的维度上被选择。当资源数量增加时,你(或运维团队)会感激你这样做。
标签可能包括以下内容
-
资源所属的应用程序(或可能是微服务)的名称
-
应用程序层(前端、后端等)
-
环境(开发、QA、预发布、生产等)
-
版本
-
发布类型(稳定、金丝雀、绿色或蓝色用于绿色/蓝色部署等)
-
客户(如果您为每个客户运行单独的 pod 而不是使用命名空间)
-
分片系统中的分片
这将允许您以组为单位而不是单个资源来管理资源,并使查看每个资源属于何处变得容易。
17.4.4. 通过注释描述每个资源
要向资源添加更多信息,请使用注释。至少,资源应包含一个描述资源的注释以及负责人的联系信息注释。
在微服务架构中,pod 可能包含一个注释,列出 pod 正在使用的其他服务的名称。这使得显示 pod 之间的依赖关系成为可能。其他注释可能包括构建和版本信息以及由工具或图形用户界面(图标名称等)使用的元数据。
标签和注释都使管理运行中的应用程序变得容易得多,但没有什么比应用程序开始崩溃而您不知道原因更糟糕的了。
17.4.5. 提供进程终止原因的信息
没有什么比不得不弄清楚为什么容器终止(或者甚至持续终止)更令人沮丧的了,尤其是在最糟糕的时刻发生时。对运维人员友好,通过在日志文件中包含所有必要的调试信息来使他们的生活变得更轻松。
但为了使分类更加容易,您还可以使用另一个 Kubernetes 功能,该功能可以显示 pod 中容器终止的原因。您通过让进程将终止消息写入容器文件系统中的特定文件来实现这一点。当容器终止时,Kubelet 会读取该文件的内容,并在 kubectl describe pod 的输出中显示。如果应用程序使用此机制,操作员可以快速看到应用程序终止的原因,甚至无需查看容器日志。
进程需要写入消息的默认文件是 /dev/termination-log,但可以通过在 pod 规范中的容器定义中设置 terminationMessagePath 字段来更改。
您可以通过运行一个容器立即死亡的 pod 来看到这个功能在行动,如下面的列表所示。
列表 17.8. Pod 写入终止消息:termination-message.yaml
apiVersion: v1 kind: Pod metadata: name: pod-with-termination-message spec: containers: - image: busybox name: main terminationMessagePath: /var/termination-reason command: - sh - -c - 'echo "I\'ve had enough" > /var/termination-reason ; exit 1' 1 2
-
1 您正在覆盖终止消息文件默认路径。
-
2 容器将在退出前将消息写入文件。
当运行此 Pod 时,您很快就会看到 Pod 的状态显示为CrashLoopBackOff。如果您此时使用kubectl describe,您可以看到容器死亡的原因,而无需深入查看其日志,如下所示。
列表 17.9. 使用 kubectl describe 查看容器的终止消息
$ kubectl describe po Name: pod-with-termination-message ... Containers: ... State: Waiting Reason: CrashLoopBackOff Last State: Terminated Reason: Error Message: I've had enough``1 Exit Code: 1 Started: Tue, 21 Feb 2017 21:38:31 +0100 Finished: Tue, 21 Feb 2017 21:38:31 +0100 Ready: False Restart Count: 6
- 1 您可以查看容器死亡的原因,而无需检查其日志。
如您所见,进程写入文件/var/termination-reason的“I've had enough”消息显示在容器的Last State(最后状态)部分。请注意,此机制不仅限于崩溃的容器。它也可以用于运行可完成任务并成功终止的 Pod(您可以在文件termination-message-success.yaml中找到一个示例)。
此机制非常适合已终止的容器,但你可能会同意,类似的机制对于显示运行中(而不仅仅是已终止)容器的应用程序特定状态消息也同样有用。Kubernetes 目前不提供此类功能,我也不了解有任何计划引入它。
注意
如果容器没有将消息写入任何文件,您可以设置terminationMessagePolicy字段为FallbackToLogsOnError。在这种情况下,容器日志的最后几行用作其终止消息(但仅当容器未成功终止时)。
17.4.6. 处理应用程序日志
在我们讨论应用程序日志的问题上,让我们重申,应用程序应该写入标准输出而不是文件。这使得使用kubectl logs命令查看日志变得容易。
小贴士
如果容器崩溃并被新的容器替换,您将看到新容器的日志。要查看前一个容器的日志,请使用kubectl logs的--previous选项。
如果应用程序将日志记录到文件而不是标准输出,您可以使用另一种方法显示日志文件:
$ kubectl exec <pod> cat <logfile>
这将在容器内部执行cat命令并将日志流回 kubectl,kubectl 在您的终端中打印它们。
将日志和其他文件复制到和从容器中
您还可以使用我们尚未讨论的kubectl cp命令将日志文件复制到您的本地机器。它允许您从容器中复制文件到容器中。例如,如果 Pod 名为foo-pod且其单个容器在/var/log/foo.log位置有一个文件,您可以使用以下命令将其传输到您的本地机器:
$ kubectl cp foo-pod:/var/log/foo.log foo.log
要将文件从您的本地机器复制到 Pod 中,请在第二个参数中指定 Pod 的名称:
$ kubectl cp localfile foo-pod:/etc/remotefile
这会将本地文件复制到 Pod 容器内的 /etc/remotefile。如果 Pod 有多个容器,您可以使用 -c containerName 选项指定容器。
使用集中式日志
在生产系统中,您会希望使用集中式、集群范围内的日志解决方案,以便将所有日志收集并(永久)存储在中央位置。这允许您检查历史日志并分析趋势。如果没有这样的系统,Pod 的日志仅在 Pod 存在期间可用。一旦删除,其日志也会被删除。
Kubernetes 本身不提供任何类型的集中式日志。提供所有容器日志集中存储和分析所需组件必须由额外的组件提供,这些组件通常作为在集群中运行的常规 Pod 运行。
部署集中式日志解决方案很简单。您只需部署几个 YAML/JSON 清单,然后就可以开始了。在 Google Kubernetes Engine 上,这甚至更简单。在设置集群时,请勾选启用 Stackdriver 日志复选框。在本地 Kubernetes 集群上设置集中式日志超出了本书的范围,但我将简要概述通常是如何操作的。
您可能已经听说过由 ElasticSearch、Logstash 和 Kibana 组成的 ELK 堆栈。略有修改的变体是 EFK 堆栈,其中 Logstash 被替换为 FluentD。
当使用 EFK 堆栈进行集中式日志时,每个 Kubernetes 集群节点运行一个 FluentD 代理(通常作为通过 DaemonSet 部署的 Pod),负责从容器收集日志,用 Pod 特定的信息标记它们,并将它们交付给 ElasticSearch,它将它们持久化存储。ElasticSearch 也作为 Pod 部署在集群的某个位置。然后可以通过 Kibana 在网络浏览器中查看和分析日志,Kibana 是一个用于可视化 ElasticSearch 数据的 Web 工具。它通常也作为 Pod 运行,并通过服务暴露。EFK 堆栈的三个组件在以下图中显示。
图 17.10. 使用 FluentD、ElasticSearch 和 Kibana 的集中式日志

注意
在下一章中,您将了解 Helm 图表。您可以使用 Kubernetes 社区创建的图表来部署 EFK 堆栈,而不是创建自己的 YAML 清单。
处理多行日志语句
FluentD 代理将日志文件的每一行存储为 ElasticSearch 数据存储中的一个条目。这里有一个问题。跨越多行的日志语句,如 Java 中的异常堆栈跟踪,在集中式日志系统中显示为单独的条目。
要解决这个问题,你可以让应用输出 JSON 而不是纯文本。这样,多行日志语句就可以作为一个单独的条目存储和显示在 Kibana 中。但这样做会让使用 kubectl logs 查看日志变得不太人性化。
解决方案可能是继续将可读日志输出到标准输出,同时将 JSON 日志写入文件,并由 FluentD 处理。这需要适当地配置节点级别的 FluentD 代理或为每个 Pod 添加一个日志边车容器。
17.5. 开发和测试的最佳实践
我们已经讨论了在开发应用时需要注意的事项,但还没有讨论那些可以帮助你简化这些流程的开发和测试工作流程。我不想在这里过多地详细说明,因为每个人都需要找到最适合他们的方法,但这里有一些起点。
17.5.1. 在开发期间在 Kubernetes 之外运行应用
当你开发一个将在生产 Kubernetes 集群中运行的应用时,这意味着你也需要在开发期间在 Kubernetes 中运行它吗?实际上并不是。每次进行小修改后都必须构建应用,然后构建容器镜像,推送到注册表,然后重新部署 Pod,这会让开发变得缓慢且痛苦。幸运的是,你不需要经历所有这些麻烦。
你总是可以在你的本地机器上开发和运行应用,就像你习惯的那样。毕竟,在 Kubernetes 中运行的应用是一个在集群节点上运行的常规(尽管是隔离的)进程。如果应用依赖于 Kubernetes 环境提供的某些功能,你可以在你的开发机器上轻松地复制该环境。
我甚至不是在谈论在容器中运行应用。大多数时候,你不需要那样做——你通常可以直接从你的 IDE 中运行应用。
连接到后端服务
在生产环境中,如果应用连接到后端服务并使用 BACKEND_SERVICE_HOST 和 BACKEND_SERVICE_PORT 环境变量来查找服务的坐标,你显然可以在本地机器上手动设置这些环境变量并将它们指向后端服务,无论它是在 Kubernetes 集群外部还是内部运行。如果它在 Kubernetes 内部运行,你始终可以(至少暂时地)通过将其更改为 NodePort 或 LoadBalancer 类型的服务来使服务对外部可访问。
连接到 API 服务器
同样,如果你的应用在 Kubernetes 集群内部运行时需要访问 Kubernetes API 服务器,它可以在开发期间轻松地从集群外部与 API 服务器通信。如果它使用 ServiceAccount 的令牌进行身份验证,你可以始终使用 kubectl cp 将 ServiceAccount 的 Secret 文件复制到你的本地机器。API 服务器不会关心访问它的客户端是在集群内部还是外部。
如果应用使用像第八章中描述的代理容器(chapter 8),你甚至不需要那些 Secret 文件。在你的本地机器上运行 kubectl proxy,本地运行你的应用,它应该已经准备好与你的本地 kubectl proxy 进行通信(只要它和代理容器将代理绑定到相同的端口)。
在这种情况下,你需要确保你的本地 kubectl 所使用的用户账户具有与应用将运行的 ServiceAccount 相同的权限。
在开发过程中在容器内运行
当你在开发过程中出于任何原因绝对需要在容器中运行应用时,有一种方法可以避免每次都需要构建容器镜像。你不需要将二进制文件烘焙到镜像中,你总是可以通过 Docker 卷将你的本地文件系统挂载到容器中,例如。这样,在你构建了应用二进制的新版本后,你所需要做的就是重启容器(或者如果支持热重载,甚至不需要这样做)。无需重新构建镜像。
17.5.2. 在开发中使用 Minikube
如你所见,没有强制要求你在开发期间在 Kubernetes 内运行你的应用。但你仍然可以这样做,以查看应用在真实 Kubernetes 环境中的行为。
你可能已经使用 Minikube 运行过本书中的示例。尽管 Minikube 集群只运行一个工作节点,但无论如何,它都是一个在 Kubernetes 中尝试你的应用(当然,还包括开发构成完整应用的资源清单)的有价值的方法。Minikube 并不提供像正常的多个节点 Kubernetes 集群那样的一切,但在大多数情况下,这并不重要。
将本地文件挂载到 minikube VM 然后挂载到你的容器中
当你在使用 Minikube 进行开发,并希望尝试将你的应用的所有更改在 Kubernetes 集群中运行时,你可以使用 minikube mount 命令将你的本地文件系统挂载到 Minikube VM 中,然后通过 hostPath 卷将其挂载到容器中。你可以在 Minikube 文档中找到如何操作的额外说明,文档地址为 github.com/kubernetes/minikube/tree/master/docs。
使用 minikube VM 内部的 Docker 守护进程构建你的镜像
如果你使用 Minikube 开发你的应用,并计划在每次更改后构建容器镜像,你可以使用 Minikube VM 内部的 Docker 守护进程来构建镜像,而不是通过你的本地 Docker 守护进程构建镜像,推送到仓库,然后由 VM 内的守护进程拉取。要使用 Minikube 的 Docker 守护进程,你只需要将你的 DOCKER_HOST 环境变量指向它。幸运的是,这比听起来要简单得多。你只需要在本地机器上运行以下命令:
$ eval $(minikube docker-env)
这将为您设置所有必需的环境变量。然后,您将以与 Docker 守护进程在本地机器上运行相同的方式构建您的镜像。构建镜像后,您不需要将其推送到任何地方,因为它已经存储在 Minikube VM 的本地,这意味着新的 Pod 可以立即使用该镜像。如果您的 Pod 已经在运行,您需要删除它们或终止它们的容器以便重新启动。
在本地构建镜像并将其直接复制到 minikube VM
如果您无法在 VM 内部使用守护进程来构建镜像,您仍然有方法避免将镜像推送到注册表,并让运行在 Minikube VM 中的 Kubelet 拉取它。如果您在本地机器上构建镜像,可以使用以下命令将其复制到 Minikube VM:
$ docker save <image> | (eval $(minikube docker-env) && docker load)
与之前一样,镜像立即准备好在 Pod 中使用。但请确保您的 Pod 规范中的imagePullPolicy没有设置为Always,因为这会导致镜像再次从外部注册表中拉取,您将丢失复制过的更改。
将 Minikube 与合适的 Kubernetes 集群结合使用
在使用 Minikube 开发应用程序时,您几乎没有任何限制。您甚至可以将 Minikube 集群与一个合适的 Kubernetes 集群结合起来。我有时在我的本地 Minikube 集群中运行我的开发工作负载,并让它们与部署在数千英里外的远程多节点 Kubernetes 集群中的其他工作负载通信。
一旦开发完成,我可以将本地工作负载无缝迁移到远程集群,无需任何修改,也无需任何问题,这得益于 Kubernetes 如何将底层基础设施从应用程序中抽象出来。
17.5.3. 版本控制和自动部署资源清单
由于 Kubernetes 使用声明性模型,您永远不需要确定已部署资源的当前状态并发出 imperative 命令来将状态带到您所期望的状态。您需要做的只是告诉 Kubernetes 您所期望的状态,它将采取所有必要的行动来使集群状态与期望状态相协调。
您可以将资源清单的集合存储在版本控制系统(Version Control System)中,这样您就可以执行代码审查、保留审计跟踪,并在必要时回滚更改。在每次提交后,您都可以运行kubectl apply命令,以便您的更改反映在已部署的资源中。
如果你运行一个代理,该代理定期(或当它检测到新的提交时)从版本控制系统(VCS)检出你的清单,然后运行 apply 命令,你可以通过将更改提交到 VCS 来简单地管理你的运行中的应用程序,而无需手动与 Kubernetes API 服务器通信。幸运的是,Box 的人(巧合的是,他们使用了这个书的手稿和其他材料)开发和发布了一个名为 kube-applier 的工具,它正好做了我描述的事情。你可以在 github.com/box/kube-applier 找到这个工具的源代码。
你可以使用多个分支将清单部署到开发、QA、预发布和生产集群(或在同一集群的不同命名空间中)。
17.5.4. 介绍 Ksonnet 作为编写 YAML/JSON 清单的替代方案
我们在书中看到了许多 YAML 清单。我认为编写 YAML 并不是太大的问题,尤其是当你学会了如何使用 kubectl explain 来查看可用选项时,但有些人确实觉得有困难。
正当我正在完成这本书的手稿时,一个名为 Ksonnet 的新工具被宣布推出。它是一个建立在 Jsonnet 之上的库,而 Jsonnet 是一种用于构建 JSON 数据结构的数据模板语言。它允许你定义参数化的 JSON 片段,给它们命名,然后通过引用这些片段的名称来构建完整的 JSON 清单,而不是在多个位置重复相同的 JSON 代码——这就像你在编程语言中使用函数或方法一样。
Ksonnet 定义了你在 Kubernetes 资源清单中找到的片段,允许你用更少的代码快速构建完整的 Kubernetes 资源 JSON 清单。以下列表显示了一个示例。
列表 17.10. 使用 Ksonnet 编写的 kubia 部署:kubia.ksonnet
local k = import "../ksonnet-lib/ksonnet.beta.1/k.libsonnet"; local container = k.core.v1.container; local deployment = k.apps.v1beta1.deployment; local kubiaContainer = 1 container.default("kubia", "luksa/kubia:v1") + 1 container.helpers.namedPort("http", 8080); 1 deployment.default("kubia", kubiaContainer) + 2 deployment.mixin.spec.replicas(3) 2
-
1 这定义了一个名为 kubia 的容器,它使用 luksa/kubia:v1 镜像,并包含一个名为 http 的端口。
-
2 这将被扩展为一个完整的 Deployment 资源。这里定义的 kubiaContainer 将包含在 Deployment 的 pod 模板中。
当你运行以下命令时,将列表中显示的 kubia.ksonnet 文件转换为完整的 JSON Deployment 清单:
$ jsonnet kubia.ksonnet
当你意识到你可以定义自己的高级片段,并使所有清单保持一致且无重复时,Ksonnet 和 Jsonnet 的强大功能就显现出来了。你可以在 github.com/ksonnet/ksonnet-lib 找到有关使用和安装 Ksonnet 和 Jsonnet 的更多信息。
17.5.5. 采用持续集成和持续交付 (CI/CD)
我们在前两节中提到了自动化部署 Kubernetes 资源,但你可能希望设置一个完整的 CI/CD 流水线,用于构建你的应用程序二进制文件、容器镜像和资源清单,然后在一个或多个 Kubernetes 集群中部署它们。
你会发现许多在线资源都在讨论这个主题。在这里,我想特别指出 Fabric8 项目 (fabric8.io),这是一个针对 Kubernetes 的集成开发平台。它包括知名的、开源的自动化系统 Jenkins,以及其他各种工具,以提供完整的 CI/CD 流水线,用于 DevOps 风格的开发、部署和管理 Kubernetes 上的微服务。
如果你想要构建自己的解决方案,我还建议查看 Google Cloud Platform 的在线实验室之一,该实验室讨论了这个主题。它可在 github.com/GoogleCloudPlatform/continuous-deployment-on-kubernetes 找到。
17.6. 摘要
希望这一章的信息能让你对 Kubernetes 的工作原理有更深入的了解,并帮助你构建在 Kubernetes 集群中部署时感觉如鱼得水的应用程序。本章的目标是
-
展示这本书中涵盖的所有资源是如何结合在一起,以表示在 Kubernetes 中运行的典型应用程序。
-
让你思考很少在机器之间移动的应用程序和作为 pods 运行的应用程序之间的区别,后者被重新定位得更加频繁。
-
帮助你理解你的多组件应用程序(或者如果你愿意,微服务)不应该依赖于特定的启动顺序。
-
介绍初始化容器,它们可以用来初始化一个 pod 或在满足先决条件之前延迟 pod 的主要容器的启动。
-
教你关于容器生命周期钩子和何时使用它们。
-
深入了解 Kubernetes 组件的分布式性质及其最终一致性模型的后果。
-
学习如何让你的应用程序正确关闭,而不会中断客户端连接。
-
给你一些小贴士,如何通过保持镜像大小小、为所有资源添加注释和多维标签,以及使查看应用程序终止原因更容易,来使你的应用程序更容易管理。
-
教你如何开发 Kubernetes 应用程序,并在将它们部署到真正的多节点集群之前,在本地或 Minikube 中运行它们。
在下一章和最后一章中,我们将学习如何使用你自己的自定义 API 对象和控制器扩展 Kubernetes,以及其他人是如何做到的,以在 Kubernetes 之上创建完整的 Platform-as-a-Service 解决方案。
第十八章. 扩展 Kubernetes
本章涵盖
-
将自定义对象添加到 Kubernetes
-
为自定义对象创建控制器
-
添加自定义 API 服务器
-
使用 Kubernetes 服务目录进行服务自助配置
-
Red Hat 的 OpenShift 容器平台
-
Deis Workflow 和 Helm
你几乎完成了。为了总结,我们将探讨如何定义自己的 API 对象并为这些对象创建控制器。我们还将探讨其他人如何扩展 Kubernetes,并在其上构建 Platform-as-a-Service 解决方案。
18.1. 定义自定义 API 对象
在整本书中,你已经学习了 Kubernetes 提供的 API 对象以及它们如何用于构建应用程序系统。目前,Kubernetes 用户主要只使用这些对象,尽管它们代表相对低级、通用的概念。
随着 Kubernetes 生态系统的不断发展,你将看到越来越多的高级对象,这些对象将比 Kubernetes 今天支持的资源更加专业化。你将不再处理 Deployments、Services、ConfigMaps 等资源,而是创建和管理代表整个应用程序或软件服务的对象。一个自定义控制器将观察这些高级对象,并根据它们创建低级对象。例如,要在 Kubernetes 集群内运行消息代理,你只需要创建一个 Queue 资源实例,所有必要的 Secrets、Deployments 和 Services 都将由自定义 Queue 控制器创建。Kubernetes 已经提供了添加此类自定义资源的方法。
18.1.1. 介绍 CustomResourceDefinitions
要定义一个新的资源类型,你所需要做的就是将 CustomResourceDefinition 对象(CRD)发布到 Kubernetes API 服务器。CustomResourceDefinition 对象是自定义资源类型的描述。一旦 CRD 被发布,用户就可以通过将 JSON 或 YAML 清单发布到 API 服务器来创建自定义资源的实例,就像对任何其他 Kubernetes 资源一样。
注意
在 Kubernetes 1.7 版本之前,自定义资源是通过 ThirdPartyResource 对象定义的,这些对象与 CustomResourceDefinitions 类似,但在版本 1.8 中被移除。
创建 CRD 以便用户可以创建新类型的对象,如果这些对象在集群中不产生任何实际效果,那么这个功能就没有什么用处。每个 CRD 通常也会有一个相关的控制器(一个基于自定义对象执行某些操作的活跃组件),就像所有核心 Kubernetes 资源都有相关的控制器一样,这在第十一章中已经解释过了。因此,为了正确展示 CustomResourceDefinitions 允许你做什么,除了添加自定义对象的实例之外,还需要部署一个控制器。你将在下一个示例中这样做。
介绍示例 CustomResourceDefinition
让我们想象一下,你希望尽可能容易地让 Kubernetes 集群的用户运行静态网站,而无需处理 Pod、Service 和其他 Kubernetes 资源。你想要实现的是用户创建类型为 Website 的对象,这些对象除了包含网站名称和获取网站文件(HTML、CSS、PNG 等)的来源之外,不包含任何其他内容。你将使用 Git 仓库作为这些文件的来源。当用户创建 Website 资源实例时,你希望 Kubernetes 启动一个新的 web 服务器 Pod,并通过 Service 进行暴露,如图 18.1 所示 figure 18.1。
图 18.1. 每个 Website 对象都应导致创建一个 Service 和一个 HTTP 服务器 Pod。

为了创建 Website 资源,你希望用户提交类似于以下列表中的清单。
列表 18.1. 一个假想的自定义资源:imaginary-kubia-website.yaml
kind: Website 1 metadata: name: kubia 2 spec: gitRepo: https://github.com/luksa/kubia-website-example.git 3
-
1 一个自定义对象类型
-
2 网站的名称(用于命名生成的 Service 和 Pod)
-
3 存放网站文件的 Git 仓库
与所有其他资源一样,你的资源包含一个 kind 和 metadata.name 字段,并且与大多数资源一样,它还包含一个 spec 部分。它包含一个名为 gitRepo 的单个字段(你可以选择名称)——它指定包含网站文件的 Git 仓库。你还需要包含一个 apiVersion 字段,但你还不清楚自定义资源需要什么值。
如果你尝试将此资源提交到 Kubernetes,你会收到错误,因为 Kubernetes 还不知道 Website 对象是什么:
$ kubectl create -f imaginary-kubia-website.yaml error: unable to recognize "imaginary-kubia-website.yaml": no matches for
/, Kind=Website
在你可以创建自定义对象实例之前,你需要让 Kubernetes 识别它们。
创建自定义资源定义对象
为了让 Kubernetes 接受你的自定义 Website 资源实例,你需要将以下列表中的自定义资源定义提交到 API 服务器。
列表 18.2. 自定义资源定义清单:website-crd.yaml
apiVersion: apiextensions.k8s.io/v1beta1 1 kind: CustomResourceDefinition 1 metadata: name: websites.extensions.example.com 2 spec: scope: Namespaced 3 group: extensions.example.com 4 version: v1 4 names: 5 kind: Website 5 singular: website 5 plural: websites 5
-
1 自定义资源定义属于此 API 组和版本。
-
2 你的自定义对象的完整名称
-
3 你希望 Website 资源是命名空间的。
-
4 定义 Website 资源的 API 组和版本。
-
5 你需要指定自定义对象名称的各种形式。
在你将描述符发布到 Kubernetes 之后,它将允许你创建任意数量的自定义网站资源实例。
你可以从代码存档中提供的 website-crd.yaml 文件创建 CRD:
$ kubectl create -f website-crd-definition.yaml customresourcedefinition "websites.extensions.example.com" created
我相信你一定在好奇 CRD 的长名称。为什么不叫它网站呢?原因是为了防止名称冲突。通过在 CRD 的名称后添加后缀(通常包括创建 CRD 的组织的名称),你可以保持 CRD 名称的唯一性。幸运的是,长名称并不意味着你需要使用 kind: websites.extensions.example.com 来创建你的网站资源,而是按照 CRD 中 names.kind 属性指定的 kind: Website 来创建。extensions.example.com 部分是你的资源 API 组。
你已经看到了创建部署对象时需要将 apiVersion 设置为 apps/v1beta1 而不是 v1 的原因。斜杠之前的部分是 API 组(部署属于 apps API 组),斜杠之后的部分是版本名称(在部署的情况下是 v1beta1)。当创建自定义网站资源的实例时,需要将 apiVersion 属性设置为 extensions.example.com/v1。
创建自定义资源实例
考虑到你所学到的知识,你现在将为你网站资源实例创建适当的 YAML。YAML 清单如下所示。
列表 18.3. 自定义网站资源:kubia-website.yaml
apiVersion: extensions.example.com/v1 1 kind: Website 2 metadata: name: kubia 3 spec: gitRepo: https://github.com/luksa/kubia-website-example.git
-
1 你的自定义 API 组和版本
-
2 此清单描述了一个网站资源实例。
-
3 网站实例的名称
你的资源类型是网站,而 apiVersion 是由你在自定义资源定义中定义的 API 组和版本号组成的。
现在创建你的网站对象:
$ kubectl create -f kubia-website.yaml website "kubia" created
响应告诉你 API 服务器已接受并存储了你的自定义网站对象。让我们看看你现在是否能检索到它。
获取自定义资源实例
列出你集群中的所有网站:
$ kubectl get websites NAME KIND kubia Website.v1.extensions.example.com
与现有的 Kubernetes 资源一样,你可以创建并列出自定义资源的实例。你也可以使用 kubectl describe 来查看自定义对象的详细信息,或者使用 kubectl get 获取整个 YAML,如下所示。
列表 18.4. 从 API 服务器检索到的完整网站资源定义
$ kubectl get website kubia -o yaml apiVersion: extensions.example.com/v1 kind: Website metadata: creationTimestamp: 2017-02-26T15:53:21Z name: kubia namespace: default resourceVersion: "57047" selfLink: /apis/extensions.example.com/v1/.../default/websites/kubia uid: b2eb6d99-fc3b-11e6-bd71-0800270a1c50 spec: gitRepo: https://github.com/luksa/kubia-website-example.git
注意,资源包括原始 YAML 定义中的所有内容,以及 Kubernetes 以与其他所有资源相同的方式初始化了额外的元数据字段。
删除自定义对象的实例
显然,除了创建和检索自定义对象实例之外,您还可以删除它们:
$ kubectl delete website kubia website "kubia" deleted
注意
您正在删除一个网站实例,而不是网站 CRD 资源。您也可以删除 CRD 对象本身,但让我们先暂时不这么做,因为在下一节中您将创建更多的网站实例。
让我们回顾一下您所做的一切。通过创建一个 CustomResourceDefinition 对象,您现在可以通过 Kubernetes API 服务器存储、检索和删除自定义对象。这些对象目前还没有做任何事情。您需要创建一个控制器来使它们执行某些操作。
通常,创建此类自定义对象的目的并不总是要在对象创建时发生某些操作。某些自定义对象用于存储数据,而不是使用更通用的机制,如 ConfigMap。运行在 Pod 内的应用程序可以查询 API 服务器以获取这些对象,并读取它们存储的内容。
但在这种情况下,我们说您希望网站对象的存在导致启动一个提供 Git 仓库内容的 web 服务器。我们将在下一节中查看如何实现这一点。
18.1.2. 使用自定义控制器自动化自定义资源
要使您的网站对象运行一个通过 Service 公开的 web 服务器 Pod,您需要构建和部署一个网站控制器,该控制器将监视 API 服务器以创建网站对象,然后为每个对象创建 Service 和 web 服务器 Pod。
为了确保 Pod 被管理并能在节点故障中存活,控制器将创建一个 Deployment 资源而不是直接创建一个未管理的 Pod。控制器的工作总结在图 18.2 中。
图 18.2. 网站控制器监视网站对象,并为每个对象创建一个 Deployment 和一个 Service。

我已经编写了一个简单的初始版本的控制器,它足以展示 CRDs 和控制器的作用,但它远未达到生产就绪状态,因为它过于简化。容器镜像可在 docker.io/luksa/website-controller:latest 找到,源代码在github.com/luksa/k8s-website-controller。我将不会通过其源代码进行说明,而是解释控制器的作用。
理解 Website 控制器的作用
启动后立即,控制器开始通过请求以下 URL 监视 Website 对象:
http://localhost:8001/apis/extensions.example.com/v1/websites?watch=true
你可能已经识别出主机名和端口号——控制器不是直接连接到 API 服务器,而是连接到kubectl proxy进程,该进程在同一个 pod 中的 sidecar 容器中运行,并作为 API 服务器的使者(我们在第八章中考察了使者模式章节 8)。代理将请求转发到 API 服务器,同时处理 TLS 加密和认证(见图 18.3)。
图 18.3. Website 控制器通过代理(在 ambassador 容器中)与 API 服务器通信

通过这个 HTTP GET 请求打开的连接,API 服务器将为任何 Website 对象的每个更改发送监视事件。
每当创建一个新的 Website 对象时,API 服务器都会发送ADDED监视事件。当控制器收到此类事件时,它会从接收到的监视事件中的 Website 对象中提取 Website 的名称和 Git 仓库的 URL,并通过将它们的 JSON 清单发布到 API 服务器来创建 Deployment 和 Service 对象。
Deployment 资源包含一个具有两个容器的 pod 模板(如图 18.4 所示):一个运行 nginx 服务器,另一个运行 gitsync 进程,该进程将本地目录与 Git 仓库的内容同步。本地目录通过emptyDir卷与 nginx 容器共享(你在第六章中做了类似的事情章节 6,但不是将本地目录与 Git 仓库同步,而是使用gitRepo卷在 pod 启动时下载 Git 仓库的内容;在之后,卷的内容没有与 Git 仓库保持同步)。服务是一个NodePort服务,它通过每个节点上的随机端口公开你的 web 服务器 pod(所有节点上使用相同的端口)。当 Deployment 对象创建 pod 时,客户端可以通过节点端口访问网站。
图 18.4. 服务器端 pod 为 Website 对象指定的网站提供服务

当 Website 资源实例被删除时,API 服务器也会发送一个 DELETED 观察事件。在接收到事件后,控制器会删除它之前创建的 Deployment 和 Service 资源。一旦用户删除了 Website 实例,控制器将关闭并移除为该网站服务的 web 服务器。
注意
我过于简化的控制器并没有得到适当的实现。它观察 API 对象的方式并不能保证不会错过单个观察事件。通过 API 服务器观察对象的正确方式不仅是要观察它们,还要定期重新列出所有对象,以防错过任何观察事件。
以 pod 的形式运行控制器
在开发过程中,我在我的本地开发笔记本电脑上运行了控制器,并使用本地运行的 kubectl proxy 进程(不以 pod 的形式运行)作为 Kubernetes API 服务器的使者。这使我能够快速开发,因为我不需要在每次更改源代码后构建容器镜像,然后再在 Kubernetes 内运行它。
当我准备将控制器部署到生产环境时,最好的方式是将控制器在 Kubernetes 本身运行,就像运行所有其他核心控制器一样。要在 Kubernetes 中运行控制器,你可以通过 Deployment 资源部署它。以下列表显示了一个这样的 Deployment 示例。
列表 18.5. Website 控制器 Deployment:website-controller.yaml
apiVersion: apps/v1beta1 kind: Deployment metadata: name: website-controller spec: replicas: 1 1 template: metadata: name: website-controller labels: app: website-controller spec: serviceAccountName: website-controller 2 containers: 3 - name: main 3 image: luksa/website-controller 3 - name: proxy 3 image: luksa/kubectl-proxy:1.6.2 3
-
1 你将运行控制器的单个副本。
-
2 它将在一个特殊的 ServiceAccount 下运行。
-
3 两个容器:主容器和代理侧容器
如您所见,Deployment 部署了一个包含两个容器的 pod 的单个副本。一个容器运行您的控制器,而另一个容器是用于与 API 服务器进行简单通信的使者容器。该 pod 在其自己的特殊 ServiceAccount 下运行,因此您在部署控制器之前需要创建它:
$ kubectl create serviceaccount website-controller serviceaccount "website-controller" created
如果你的集群启用了基于角色的访问控制(RBAC),Kubernetes 将不允许控制器观察 Website 资源或创建 Deployment 或 Service。为了允许它这样做,你需要通过创建一个类似这样的 ClusterRoleBinding 将 website-controller ServiceAccount 绑定到 cluster-admin ClusterRole:
$ kubectl create clusterrolebinding website-controller
--clusterrole=cluster-admin
--serviceaccount=default:website-controller clusterrolebinding "website-controller" created
一旦设置了 ServiceAccount 和 ClusterRoleBinding,你就可以部署控制器的部署。
观察控制器的工作情况
控制器现在正在运行,再次创建 kubia 网站资源:
$ kubectl create -f kubia-website.yaml website "kubia" created
现在,让我们检查控制器的日志(如下所示列表)以查看它是否收到了监视事件。
列表 18.6. 显示网站控制器的日志
$ kubectl logs website-controller-2429717411-q43zs -c main 2017/02/26 16:54:41 website-controller started. 2017/02/26 16:54:47 Received watch event: ADDED: kubia: https://github.c... 2017/02/26 16:54:47 Creating services with name kubia-website in namespa... 2017/02/26 16:54:47 Response status: 201 Created 2017/02/26 16:54:47 Creating deployments with name kubia-website in name... 2017/02/26 16:54:47 Response status: 201 Created
日志显示控制器收到了 ADDED 事件,并且它为 kubia-website 网站创建了服务和服务。API 服务器响应了 201 Created 状态,这意味着这两个资源现在应该存在。让我们验证部署、服务和由此产生的 Pod 是否已创建。以下列表列出了所有部署、服务和 Pod。
列表 18.7. 为 kubia-website 创建的部署、服务和 Pod
$ kubectl get deploy,svc,po NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE deploy/kubia-website``1 1 1 1 4s deploy/website-controller 1 1 1 1 5m NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE svc/kubernetes 10.96.0.1 <none> 443/TCP 38d svc/kubia-website``10.101.48.23 <nodes> 80:32589/TCP 4s NAME READY STATUS RESTARTS AGE po/kubia-website-1029415133-rs715`` 2/2 Running 0 4s po/website-controller-1571685839-qzmg6 2/2 Running 1 5m
就在这里。你可以通过 kubia-website 服务访问你的网站,该服务在所有集群节点上的 32589 端口可用。你可以用浏览器访问它。太棒了,不是吗?
你的 Kubernetes 集群的用户现在可以在几秒钟内部署静态网站,无需了解关于 Pod、服务或任何其他 Kubernetes 资源的信息,除了你的自定义网站资源。
显然,你仍有改进的空间。例如,控制器可以监视服务对象,一旦节点端口分配,就将网站可访问的 URL 写入网站资源实例的 status 部分。或者它也可以为每个网站创建一个 Ingress 对象。我将将这些额外功能的实现留给你作为练习。
18.1.3. 验证自定义对象
您可能已经注意到,您在 Website CustomResourceDefinition 中没有指定任何类型的验证架构。用户可以在他们的 Website 对象的 YAML 中包含他们想要的任何字段。API 服务器不会验证 YAML 的内容(除了通常的字段如 apiVersion、kind 和 metadata),因此用户可以创建无效的 Website 对象(例如,没有 gitRepo 字段)。
是否有可能向控制器添加验证并防止无效对象被 API 服务器接受?这是不可能的,因为 API 服务器首先存储对象,然后向客户端(kubectl)返回成功响应,然后才通知所有监视器(控制器就是其中之一)。控制器真正能做的就是当它在一个监视事件中接收到对象时验证对象,如果对象无效,就将错误信息写入 Website 对象(通过向 API 服务器发送一个新的请求来更新对象)。用户不会自动收到错误通知。他们必须通过查询 API 服务器以获取 Website 对象来注意错误信息。除非用户这样做,否则他们无法知道对象是否有效。
这显然不是理想的。您希望 API 服务器验证对象并立即拒绝无效对象。自定义对象的验证是在 Kubernetes 版本 1.8 中作为一个 alpha 特性引入的。要使 API 服务器验证您的自定义对象,您需要在 API 服务器中启用 CustomResourceValidation 功能门,并在 CRD 中指定一个 JSON 架构。
18.1.4. 为您的自定义对象提供自定义 API 服务器
在 Kubernetes 中添加对自定义对象支持的一种更好的方式是实现您自己的 API 服务器,并让客户端直接与其通信。
引入 API 服务器聚合
在 Kubernetes 版本 1.7 中,您可以通过 API 服务器聚合将您的自定义 API 服务器与主 Kubernetes API 服务器集成。最初,Kubernetes API 服务器是一个单一的模块化组件。从 Kubernetes 版本 1.7 开始,多个聚合的 API 服务器将在单个位置暴露。客户端可以连接到聚合的 API,并且他们的请求将透明地转发到适当的 API 服务器。这样,客户端甚至不会意识到有多个 API 服务器在幕后处理不同的对象。甚至核心 Kubernetes API 服务器最终也可能被分割成多个较小的 API 服务器,并通过聚合器作为一个单独的服务器暴露,如图 18.5 所示。
图 18.5. API 服务器聚合

在您的案例中,您可以创建一个负责处理您的 Website 对象的 API 服务器。它可以像核心 Kubernetes API 服务器验证对象那样验证这些对象。您就不再需要创建 CRD 来表示这些对象,因为您将直接在自定义 API 服务器中实现 Website 对象类型。
通常,每个 API 服务器负责存储它们自己的资源。如图 18.5 所示,它可以运行自己的 etcd 实例(或整个 etcd 集群),或者它可以通过在核心 API 服务器中创建 CRD 实例将资源存储在核心 API 服务器 etcd 存储中。在这种情况下,它需要首先创建 CRD 对象,然后再创建 CRD 实例,就像您在示例中所做的那样。
注册自定义 API 服务器
要将自定义 API 服务器添加到您的集群中,您需要将其作为 pod 部署并通过服务公开。然后,为了将其集成到主 API 服务器,您需要部署一个描述 APIService 资源(如下所示列表)的 YAML 清单。
列表 18.8. 一个 APIService YAML 定义
apiVersion: apiregistration.k8s.io/v1beta1 1 kind: APIService 1 metadata: 2 name: v1alpha1.extensions.example.com 2 spec: 3 group: extensions.example.com 3 version: v1alpha1 3 priority: 150 4 service: 4 name: website-api 4 namespace: default 4
-
1 这是一个 APIService 资源。
-
2 该 API 服务器负责的 API 组
-
3 支持的 API 版本
-
4 自定义 API 服务器通过的服务
在从上一列表创建 APIService 资源之后,发送到主 API 服务器且包含来自 extensions.example.com API 组和版本 v1alpha1 的任何资源的客户端请求将被转发到通过 website-api 服务公开的自定义 API 服务器 pod(s)。
创建自定义客户端
虽然您可以使用常规的 kubectl 客户端从 YAML 文件创建自定义资源,但要使自定义对象的部署更加容易,除了提供自定义 API 服务器外,您还可以构建自定义 CLI 工具。这将允许您添加用于操作这些对象的专用命令,类似于 kubectl 通过资源特定的命令(如 kubectl create secret 或 kubectl create deployment)允许创建 Secrets、Deployments 和其他资源。
正如我已经提到的,自定义 API 服务器、API 服务器聚合以及其他与扩展 Kubernetes 相关的功能目前正在积极开发中,因此它们可能在本书出版后发生变化。要获取有关该主题的最新信息,请参阅 Kubernetes GitHub 仓库github.com/kubernetes。
18.2. 使用 Kubernetes 服务目录扩展 Kubernetes
通过 API 服务器聚合添加到 Kubernetes 的第一个附加 API 服务器之一是服务目录 API 服务器。服务目录是 Kubernetes 社区的一个热门话题,因此您可能想了解它。
目前,为了一个 Pod 能够消费一个服务(这里我使用这个术语是一般性的,而不是与 Service 资源相关;例如,数据库服务包括允许用户在他们的应用程序中使用数据库所需的所有内容),需要有人部署提供服务的 Pods、Service 资源,以及可能的一个 Secret,以便客户端 Pod 可以使用它来与该服务进行身份验证。这个人通常是部署客户端 Pod 的同一用户,或者如果有一个团队专门负责部署这些类型的通用服务,用户需要提交一个工单并等待团队提供该服务。这意味着用户需要创建服务所有组件的清单,知道在哪里找到现有的清单集合,知道如何正确配置它,并手动部署它,或者等待其他团队来完成。
但 Kubernetes 应该是一个易于使用、自助服务的系统。理想情况下,需要特定服务的应用程序(例如,需要后端数据库的 Web 应用程序)的用户应该能够对 Kubernetes 说:“嘿,我需要一个 PostgreSQL 数据库。请为我提供它,并告诉我如何连接到它。”这将通过 Kubernetes 服务目录很快实现。
18.2.1. 介绍服务目录
如其名所示,服务目录是服务的目录。用户可以浏览目录,并自行提供目录中列出的服务的实例,而无需处理 Pods、Services、ConfigMaps 和其他服务运行所需的其他资源。你会认识到这与你对 Website 自定义资源所做的是类似的。
而不是为每种服务向 API 服务器添加自定义资源,服务目录引入了以下四个通用 API 资源:
-
ClusterServiceBroker,它描述了一个可以提供服务的(外部)系统
-
ClusterServiceClass,它描述了可以提供的服务类型
-
ServiceInstance,它是已经提供的服务的一个实例
-
ServiceBinding,它代表了一组客户端(Pods)与 ServiceInstance 之间的绑定
这四个资源之间的关系在图 18.6 中展示,并在以下段落中解释。
图 18.6. 服务目录 API 资源之间的关系。

简而言之,集群管理员为每个他们希望在集群中提供服务的服务代理创建一个 ClusterServiceBroker 资源。Kubernetes 随后要求代理提供一个它可以提供的服务的列表,并为每个服务创建一个 ClusterServiceClass 资源。当用户需要提供一项服务时,他们创建一个 ServiceInstance 资源,然后创建一个 ServiceBinding 将 Service-Instance 绑定到他们的 Pods 上。然后,这些 Pods 被注入一个 Secret,其中包含连接到提供的服务实例所需的所有必要凭证和其他数据。
服务目录系统架构如图 18.7 所示。
图 18.7. 服务目录的架构

图中所示组件将在以下章节中进行解释。
18.2.2. 介绍服务目录 API 服务器和控制器管理器
与核心 Kubernetes 类似,服务目录是一个由三个组件组成的分布式系统:
-
服务目录 API 服务器
-
etcd 作为存储
-
控制器管理器,所有控制器都在这里运行
我们之前介绍的四个与服务目录相关的资源是通过向 API 服务器发送 YAML/JSON 清单创建的。然后它将它们存储到自己的 etcd 实例中,或者使用主 API 服务器中的 CustomResourceDefinitions 作为替代存储机制(在这种情况下,不需要额外的 etcd 实例)。
在控制器管理器中运行的控制器是那些对这些资源进行操作的控制器。它们显然会与服务目录 API 服务器通信,就像其他核心 Kubernetes 控制器与核心 API 服务器通信一样。这些控制器不会自己提供请求的服务。它们将这项工作留给外部服务代理,这些代理通过在服务目录 API 中创建 ServiceBroker 资源进行注册。
18.2.3. 介绍服务代理和 OpenServiceBroker API
群集管理员可以在服务目录中注册一个或多个外部服务代理。每个代理都必须实现 OpenServiceBroker API。
介绍 OpenServiceBroker API
服务目录通过该 API 与代理通信。该 API 相对简单。它是一个提供以下操作的 REST API:
-
使用
GET /v2/catalog获取服务列表 -
配置服务实例 (
PUT /v2/service_instances/:id) -
更新服务实例 (
PATCH /v2/service_instances/:id) -
绑定服务实例 (
PUT /v2/service_instances/:id/service_bindings/:binding_id) -
解除实例绑定 (
DELETE /v2/service_instances/:id/service_bindings/:binding_id) -
取消配置服务实例 (
DELETE /v2/service_instances/:id)
您可以在 github.com/openservicebrokerapi/servicebroker 找到 OpenServiceBroker API 规范。
在服务目录中注册代理
群集管理员通过向服务目录 API 发送 ServiceBroker 资源清单来注册代理,如下所示。
列表 18.9. ClusterServiceBroker 清单:database-broker.yaml
apiVersion: servicecatalog.k8s.io/v1alpha1 1 kind: ClusterServiceBroker 1 metadata: name: database-broker 2 spec: url: http://database-osbapi.myorganization.org 3
-
1 资源类型和 API 组及版本
-
2 此代理的名称
-
3 服务目录可以联系代理的位置(其 OpenServiceBroker [OSB] API URL)
列表描述了一个可以配置不同类型数据库的虚拟代理。在管理员创建 ClusterServiceBroker 资源后,Service Catalog Controller Manager 中的控制器连接到资源中指定的 URL 以检索此代理可以配置的服务列表。
在服务目录检索服务列表后,它为每个服务创建一个 ClusterServiceClass 资源。每个 ClusterServiceClass 资源描述了一种可以配置的服务类型(ClusterServiceClass 的一个例子是“PostgreSQL 数据库”)。每个 ClusterServiceClass 都与一个或多个服务计划相关联。这些允许用户选择他们需要的服务的级别(例如,数据库 ClusterServiceClass 可以提供一个“免费”计划,其中数据库的大小有限,底层存储是旋转磁盘,以及一个“高级”计划,具有无限大小和 SSD 存储)。
列出集群中的可用服务
Kubernetes 集群的用户可以使用kubectl get serviceclasses检索集群中可以配置的所有服务的列表,如下所示。
列表 18.10. 集群中 ClusterServiceClasses 的列表
$ kubectl get clusterserviceclasses NAME KIND postgres-database ClusterServiceClass.v1alpha1.servicecatalog.k8s.io mysql-database ServiceClass.v1alpha1.servicecatalog.k8s.io mongodb-database ServiceClass.v1alpha1.servicecatalog.k8s.io
列表显示了可以为您的虚拟数据库代理提供的服务 ClusterServiceClasses。您可以将 ClusterServiceClasses 与我们在第六章中讨论的 StorageClasses 进行比较。StorageClasses 允许您选择在您的 Pod 中想要使用的存储类型,而 ClusterServiceClasses 允许您选择服务类型。
您可以通过检索其 YAML 来查看 ClusterServiceClass 的详细信息。以下是一个示例。
列表 18.11. ClusterServiceClass 定义
$ kubectl get serviceclass postgres-database -o yaml apiVersion: servicecatalog.k8s.io/v1alpha1 bindable: true brokerName: database-broker 1 description: A PostgreSQL database kind: ClusterServiceClass metadata: name: postgres-database ... planUpdatable: false plans: - description: A free (but slow) PostgreSQL instance 2 name: free 2 osbFree: true 2 ... - description: A paid (very fast) PostgreSQL instance 3 name: premium 3 osbFree: false 3 ...
-
1 此 ClusterServiceClass 由数据库代理提供。
-
2 此服务的免费计划
-
3 一个付费计划
列表中的 ClusterServiceClass 包含两个计划——一个免费计划和一个高级计划。您可以看到此 ClusterServiceClass 是由database-broker代理提供的。
18.2.4. 服务配置和使用
让我们想象一下你部署的 Pod 需要使用数据库。你已经检查了可用的 ClusterServiceClasses 列表,并选择了使用postgres-database ClusterServiceClass 的free计划。
配置一个 ServiceInstance
为了让你使用的数据库被配置好,你所需要做的就是创建一个 Service-Instance 资源,如下所示。
列表 18.12. ServiceInstance 清单:database-instance.yaml
apiVersion: servicecatalog.k8s.io/v1alpha1 kind: ServiceInstance metadata: name: my-postgres-db 1 spec: clusterServiceClassName: postgres-database 2 clusterServicePlanName: free 2 parameters: init-db-args: --data-checksums 3
-
1 你正在给这个实例命名。
-
2 你想要的 ServiceClass 和 Plan
-
3 传递给代理的附加参数
你创建了一个名为my-postgres-db的 ServiceInstance(这将是你要部署的资源名称),并指定了 ClusterServiceClass 和所选的计划。你还指定了一个参数,这是针对每个代理和 ClusterServiceClass 特定的。让我们假设你查阅了代理文档中可能的参数。
一旦你创建了此资源,服务目录将联系属于 ClusterServiceClass 的代理,并要求它配置服务。它将传递所选的 ClusterServiceClass 和计划名称,以及你指定的所有参数。
然后,完全取决于代理如何处理这些信息。在你的情况下,你的数据库代理可能会在某个地方启动一个新的 PostgreSQL 数据库实例——不一定是在同一个 Kubernetes 集群中,甚至可能根本不在 Kubernetes 中。它可以在虚拟机上运行,并在那里运行数据库。服务目录不在乎,请求服务的用户也不在乎。
你可以通过检查你创建的 my-postgres-db ServiceInstance 的status部分来验证服务是否已成功配置,如下所示。
列表 18.13. 检查 ServiceInstance 的状态
$ kubectl get instance my-postgres-db -o yaml apiVersion: servicecatalog.k8s.io/v1alpha1 kind: ServiceInstance ... status: asyncOpInProgress: false conditions: - lastTransitionTime: 2017-05-17T13:57:22Z message: The instance was provisioned successfully 1 reason: ProvisionedSuccessfully 1 status: "True" type: Ready 2
-
1 数据库已成功配置。
-
2 它现在可以使用了。
数据库实例现在在某个地方运行,但如何在你的 Pod 中使用它?为了做到这一点,你需要将其绑定。
绑定一个 ServiceInstance
要在你的 Pod 中使用已配置的 ServiceInstance,你需要创建一个 ServiceBinding 资源,如下所示。
列表 18.14. ServiceBinding:my-postgres-db-binding.yaml
apiVersion: servicecatalog.k8s.io/v1alpha1 kind: ServiceBinding metadata: name: my-postgres-db-binding spec: instanceRef: 1 name: my-postgres-db 1 secretName: postgres-secret 2
-
1 您正在引用您之前创建的实例。
-
2 您希望将访问服务的凭据存储在这个 Secret 中。
列表显示您正在定义一个名为my-postgres-db-binding的 ServiceBinding 资源,在其中您引用了您之前创建的my-postgres-db服务实例。您还指定了一个 Secret 的名称。您希望服务目录将访问服务实例所需的所有必要凭据放入名为postgres-secret的 Secret 中。但您将 ServiceInstance 绑定到 Pod 的哪里?实际上是没有地方。
目前,服务目录还不能使 Pod 能够注入服务实例的凭据。当一个新的 Kubernetes 功能 PodPresets 可用时,这将成为可能。在此之前,您可以为要存储凭据的 Secret 选择一个名称,并将该 Secret 手动挂载到您的 Pod 中。
当您将上一列表中的 ServiceBinding 资源提交给服务目录 API 服务器时,控制器将再次联系数据库代理,并为您之前配置的服务实例创建一个绑定。代理响应一个包含连接到数据库所需凭据和其他数据的列表。服务目录创建一个新的 Secret,其名称与您在 Service-Binding 资源中指定的名称相同,并将所有这些数据存储在 Secret 中。
使用新创建的 Secret 在客户端 Pod 中
服务目录系统创建的 Secret 可以挂载到 Pod 中,这样它们就可以读取其内容,并使用这些内容连接到配置的服务实例(例如,示例中的 PostgreSQL 数据库)。Secret 可能看起来像以下列表中的那样。
列表 18.15. 存储连接到服务实例凭据的 Secret
$ kubectl get secret postgres-secret -o yaml apiVersion: v1 data: host: <数据库的主机 base64 编码名称> 1 username: <用户名的 base64 编码> 1 password: <密码的 base64 编码> 1 kind: Secret metadata: name: postgres-secret namespace: default ... type: Opaque
- 1 这就是 Pod 应该用来连接到数据库服务的内容。
因为您可以自己选择 Secret 的名称,您可以在配置或绑定服务之前部署 Pod。正如您在第七章中学到的,Pod 将在这样的 Secret 存在之前不会启动。
如果需要,可以为不同的 Pod 创建多个绑定。服务代理可以选择在每个绑定中使用相同的凭据集,但为每个绑定实例创建一个新的凭据集会更好。这样,可以通过删除 ServiceBinding 资源来防止 Pod 使用服务。
18.2.5. 解除绑定和取消配置
一旦不再需要 ServiceBinding,您就可以像删除其他资源一样删除它:
$ kubectl delete servicebinding my-postgres-db-binding servicebinding "my-postgres-db-binding" deleted
当您这样做时,服务目录控制器将删除 Secret 并调用代理执行解绑操作。服务实例(在您的情况下是一个 PostgreSQL 数据库)仍在运行。因此,如果您想的话,可以创建一个新的 ServiceBinding。
但如果您不再需要数据库实例,也应该删除 Service-Instance 资源:
$ kubectl delete serviceinstance my-postgres-db serviceinstance "my-postgres-db" deleted
删除 ServiceInstance 资源会导致服务目录在服务代理上执行解配操作。再次强调,这具体意味着什么取决于服务代理,但在您的情况下,代理应该关闭我们在提供服务实例时创建的 PostgreSQL 数据库实例。
18.2.6. 理解服务目录带来的好处
正如您所学的,服务目录使服务提供商能够通过在该集群中注册代理来在任何 Kubernetes 集群中公开这些服务。例如,我从一开始就参与了服务目录,并实现了一个代理,这使得在 Kubernetes 集群中提供消息系统并将其公开给 pod 变得非常简单。另一个团队实现了一个代理,使得提供亚马逊网络服务变得容易。
通常,服务代理允许在 Kubernetes 中轻松提供和公开服务,这将使 Kubernetes 成为部署应用程序的更加强大的平台。
18.3. 基于 Kubernetes 的平台
我相信您会同意 Kubernetes 本身就是一个伟大的系统。鉴于它很容易扩展到其所有组件,也就难怪之前开发自己定制平台的公司现在正在 Kubernetes 之上重新实现它们。实际上,Kubernetes 正在成为新一代 PaaS 提供的广泛接受的基础。
在基于 Kubernetes 的最知名 PaaS 系统中,有 Deis Workflow 和 Red Hat 的 OpenShift。我们将快速概述这两个系统,以便您了解它们在 Kubernetes 已经提供的所有精彩功能之上还能提供什么。
18.3.1. Red Hat OpenShift 容器平台
Red Hat OpenShift 是一个平台即服务(PaaS),因此它非常注重开发者体验。其目标包括实现应用程序的快速开发、易于部署、扩展以及长期维护。OpenShift 的历史比 Kubernetes 悠久得多。版本 1 和 2 是从头开始构建的,与 Kubernetes 无关,但 Kubernetes 发布后,Red Hat 决定从头开始重建 OpenShift 版本 3——这次是在 Kubernetes 之上。当像 Red Hat 这样的公司决定放弃其软件的旧版本,并在现有技术如 Kubernetes 之上构建新版本时,对每个人来说都应该很清楚 Kubernetes 有多么出色。
Kubernetes 自动化了发布和应用程序扩展,而 OpenShift 还自动构建应用程序镜像及其自动部署,无需您将持续集成解决方案集成到您的集群中。
OpenShift 还提供了用户和组管理,这使得您可以运行一个安全的多租户 Kubernetes 集群,其中单个用户只能访问他们自己的 Kubernetes 命名空间,并且在这些命名空间中运行的应用程序默认情况下也完全网络隔离。
介绍 OpenShift 中可用的额外资源
除了 Kubernetes 中所有可用的 API 对象之外,OpenShift 还提供了一些额外的 API 对象。我们将在接下来的几段中解释它们,以便您对 OpenShift 做什么以及它提供什么有一个良好的概述。
额外的资源包括
-
Users & Groups
-
Projects
-
Templates
-
BuildConfigs
-
DeploymentConfigs
-
ImageStreams
-
Routes
-
以及其他
理解用户、组和项目
我们说过,OpenShift 为用户提供了一个合适的多租户环境。与 Kubernetes 不同,Kubernetes 没有用于表示集群单个用户的 API 对象(但确实有 ServiceAccounts 表示在其中运行的服务),OpenShift 提供了强大的用户管理功能,这使得可以指定每个用户可以做什么以及他们不能做什么。这些功能早于基于角色的访问控制,而基于角色的访问控制现在是纯 Kubernetes 的标准。
每个用户都有访问某些项目的权限,这些项目不过是带有额外注解的 Kubernetes 命名空间。用户只能对其有权访问的项目中的资源采取行动。项目访问权由集群管理员授予。
介绍应用程序模板
Kubernetes 通过单个 JSON 或 YAML 清单部署一组资源。OpenShift 更进一步,允许该清单可参数化。在 OpenShift 中,可参数化的列表称为模板;它是一系列对象,其定义可以包括占位符,当您处理并实例化模板时,这些占位符将被参数值替换(参见图 18.8)。
图 18.8. OpenShift 模板

模板本身是一个 JSON 或 YAML 文件,其中包含在相同 JSON/YAML 中定义的资源中引用的参数列表。模板可以像任何其他对象一样存储在 API 服务器上。在模板可以实例化之前,需要对其进行处理。要处理模板,你需要提供模板参数的值,然后 OpenShift 将参数的引用替换为这些值。结果是经过处理的模板,它就像一个 Kubernetes 资源列表,然后可以通过单个 POST 请求创建。
OpenShift 提供了一系列预先构建的模板,允许用户通过指定少量参数(如果没有提供参数,因为模板为这些参数提供了良好的默认值)快速运行复杂的应用程序。例如,一个模板可以启用创建所有必要的 Kubernetes 资源,以便在应用程序服务器内运行 Java EE 应用程序,该服务器连接到后端数据库,也作为该模板的一部分部署。所有这些组件都可以通过单个命令部署。
使用 BuildConfigs 从源构建镜像
OpenShift 最优秀的功能之一是能够通过指向包含应用程序源代码的 Git 仓库来构建和立即在 OpenShift 集群中部署应用程序。你根本不需要构建容器镜像——OpenShift 会为你完成这项工作。这是通过创建一个名为 Build-Config 的资源来实现的,它可以配置为在源 Git 仓库提交更改后立即触发容器镜像的构建。
尽管 OpenShift 本身不监控 Git 仓库,但仓库中的钩子可以通知 OpenShift 有新的提交。然后 OpenShift 将从 Git 仓库拉取更改并开始构建过程。一种名为“源到镜像”的构建机制可以检测 Git 仓库中应用类型,并为其运行适当的构建过程。例如,如果检测到 pom.xml 文件,这是 Java Maven 格式项目使用的,它将运行 Maven 构建。生成的工件被打包到适当的容器镜像中,然后推送到内部容器注册库(由 OpenShift 提供)。从那里,它们可以立即被拉取并在集群中运行。
通过创建 BuildConfig 对象,开发人员可以指向 Git 仓库,而不必担心构建容器镜像。开发人员几乎不需要了解任何关于容器的内容。一旦运维团队部署了 OpenShift 集群并允许开发人员访问它,那些开发人员就可以像以前一样开发他们的代码,提交并推送到 Git 仓库。然后 OpenShift 负责从该代码构建、部署和管理应用程序。
使用 DeploymentConfigs 自动部署新构建的镜像
一旦构建了新的容器镜像,它也可以在集群中自动部署。这是通过创建一个 DeploymentConfig 对象并将其指向 ImageStream 来实现的。正如其名所示,ImageStream 是一系列镜像。当镜像构建时,它会被添加到 ImageStream 中。这使得 DeploymentConfig 能够注意到新构建的镜像,并允许它采取行动并启动新镜像的部署(见图 18.9)。
图 18.9. OpenShift 中的 BuildConfigs 和 DeploymentConfigs

DeploymentConfig 几乎与 Kubernetes 中的 Deployment 对象相同,但它更早出现。像 Deployment 对象一样,它有一个可配置的策略,用于在部署之间进行转换。它包含一个用于创建实际 Pods 的 Pod 模板,但它还允许你配置部署前和部署后的钩子。与 Kubernetes 的 Deployment 相比,它创建 ReplicationControllers 而不是 ReplicaSets,并提供了一些额外的功能。
使用 Routes 公开服务
在早期,Kubernetes 没有提供 Ingress 对象。要将服务暴露给外部世界,你需要使用NodePort或LoadBalancer类型的 Service。但那时,OpenShift 已经通过 Route 资源提供了一个更好的选择。Route 类似于 Ingress,但它提供了与 TLS 终止和流量分割相关的额外配置。
与 Ingress 控制器类似,Route 需要一个 Router,这是一个提供负载均衡器或代理的控制器。与 Kubernetes 不同,Router 在 OpenShift 中是开箱即用的。
尝试 OpenShift
如果你感兴趣尝试 OpenShift,你可以从使用 Minishift 开始,它是 Minikube 的 OpenShift 等价物,或者你可以尝试在manage.openshift.com上的 OpenShift Online Starter,这是一个免费的多租户托管解决方案,旨在帮助你开始使用 OpenShift。
18.3.2. Deis Workflow 和 Helm
一家名为 Deis 的公司,最近已被微软收购,它也提供了一种名为 Workflow 的 PaaS 服务,该服务也是建立在 Kubernetes 之上的。除了 Workflow 之外,他们还开发了一个名为 Helm 的工具,它在 Kubernetes 社区中作为一种在 Kubernetes 中部署现有应用的标准方式而受到欢迎。我们将简要地看看这两个工具。
介绍 Deis Workflow
你可以将 Deis Workflow 部署到任何现有的 Kubernetes 集群中(与 OpenShift 不同,OpenShift 是一个完整的集群,具有修改后的 API 服务器和其他 Kubernetes 组件)。当你运行 Workflow 时,它会创建一组服务和 ReplicationControllers,然后为开发者提供一个简单、友好的环境。
通过 git push deis master 推送您的更改并让 Workflow 处理其余部分来触发部署您应用程序的新版本。类似于 OpenShift,Workflow 也提供了源到镜像机制、应用程序部署和回滚、边缘路由,以及核心 Kubernetes 中不可用的日志聚合、指标和警报。
要在您的 Kubernetes 集群中运行 Workflow,您首先需要安装 Deis Workflow 和 Helm CLI 工具,然后将 Workflow 安装到您的集群中。我们这里不会详细介绍如何操作,但如果您想了解更多信息,请访问 deis.com/workflow 网站。我们将在这里探讨 Helm 工具,它可以不使用 Workflow 使用,并在社区中获得了流行。
通过 Helm 部署资源
Helm 是 Kubernetes 的包管理器(类似于 Linux 中的 yum 或 apt 或 MacOS 中的 homebrew 这样的操作系统包管理器)。
Helm 由两部分组成:
-
helmCLI 工具(客户端)。 -
Tiller,一个在 Kubernetes 集群内部运行的 Pod 中的服务器组件。
这两个组件用于在 Kubernetes 集群中部署和管理应用程序包。Helm 应用程序包被称为 Charts。它们与一个包含配置信息的 Config 结合,合并成一个 Chart,以创建一个 Release,即应用程序的运行实例(一个结合了 Chart 和 Config 的组合)。您可以使用 helm CLI 工具部署和管理 Release,该工具与 Tiller 服务器通信,Tiller 服务器是创建 Chart 中定义的所有必要 Kubernetes 资源的组件,如图 18.10 所示。链接。
图 18.10. Helm 概览

您可以自己创建图表并将它们保存在本地磁盘上,或者您可以使用任何现有的图表,这些图表由社区维护的 helm 图表列表中提供,该列表在 github.com/kubernetes/charts 上不断增长。列表包括 PostgreSQL、MySQL、MariaDB、Magento、Memcached、MongoDB、OpenVPN、PHPBB、RabbitMQ、Redis、WordPress 等应用程序的图表。
与您手动构建和安装其他人开发的 Linux 系统中的应用程序一样,您可能不想手动构建和管理自己的 Kubernetes 清单,对吧?这就是为什么您会想使用 Helm 和我提到的 GitHub 仓库中可用的图表。
当您想在 Kubernetes 集群中运行 PostgreSQL 或 MySQL 数据库时,不要开始编写它们的清单。相反,检查是否有人已经为此准备了 Helm 图表。
一旦有人为特定的应用程序准备了一个 Helm 图表并将其添加到 Helm 图表 GitHub 存储库中,安装整个应用程序只需要一条单行命令。例如,要在您的 Kubernetes 集群中运行 MySQL,您只需将图表 Git 存储库克隆到您的本地机器上,并运行以下命令(假设您在集群中运行了 Helm 的 CLI 工具和 Tiller):
$ helm install --name my-database stable/mysql
这将创建运行 MySQL 所需的所有必要的部署、服务、机密和持久卷声明。您无需担心需要哪些组件以及如何配置它们以正确运行 MySQL。我相信您会同意这很棒。
小贴士
在存储库中可用的最有趣的图表之一是 OpenVPN 图表,它可以在您的 Kubernetes 集群内部运行 OpenVPN 服务器,并允许您通过 VPN 进入 pod 网络,就像您的本地机器是集群中的 pod 一样访问服务。当您在本地开发和运行应用程序时,这很有用。
这些是 Kubernetes 可以如何扩展以及像 Red Hat 和 Deis(现在是微软)这样的公司如何扩展它的几个例子。现在去开始自己驾驭 Kubernetes 的浪潮吧!
18.4. 摘要
这最后一章向您展示了如何超越 Kubernetes 提供的现有功能,以及像 Dies 和 Red Hat 这样的公司是如何做到的。您已经学会了如何
-
可以通过创建自定义资源定义对象来在 API 服务器中注册自定义资源。
-
可以存储、检索、更新和删除自定义对象的实例,而无需更改 API 服务器代码。
-
可以实现一个自定义控制器来使这些对象变得活跃。
-
Kubernetes 可以通过 API 聚合扩展到自定义 API 服务器。
-
Kubernetes 服务目录使得能够自助配置外部服务并将它们暴露给在 Kubernetes 集群中运行的 pod。
-
建立在 Kubernetes 之上的平台服务使得在同一个 Kubernetes 集群内部构建容器化应用程序变得容易,然后运行它们。
-
一个名为 Helm 的包管理器使得部署现有应用程序而无需为它们构建资源清单变得容易。
感谢您抽出时间阅读这本长篇巨著。我希望您从阅读中学到的知识和我从写作中学到的知识一样多。
附录 A. 使用 kubectl 管理多个集群
A.1. 在 Minikube 和 Google Kubernetes Engine 之间切换
本书中的示例可以在使用 Minikube 创建的集群中运行,也可以在使用 Google Kubernetes Engine (GKE)创建的集群中运行。如果你计划使用两者,你需要知道如何在它们之间切换。如何在多个集群中使用kubectl的详细说明将在下一节中描述。这里我们来看看如何切换 Minikube 和 GKE。
切换到 Minikube
幸运的是,每次你使用minikube start启动 Minikube 集群时,它也会重新配置kubectl以使用它:
$ minikube start 启动本地 Kubernetes 集群... ... 设置 kubeconfig... 1 Kubectl 现在已配置为使用该集群。 1
- 1 Minikube 每次启动集群时都会设置 kubectl。
从 Minikube 切换到 GKE 后,可以通过停止 Minikube 并重新启动它来切换回来。此时,kubectl将重新配置以再次使用 Minikube 集群。
切换到 GKE
要切换到使用 GKE 集群,可以使用以下命令:
$ gcloud container clusters get-credentials my-gke-cluster
这将配置kubectl以使用名为my-gke-cluster的 GKE 集群。
进一步了解
这两种方法应该足以让你快速入门,但要了解使用kubectl管理多个集群的完整情况,请学习下一节。
A.2. 使用 kubectl 管理多个集群或命名空间
如果你需要在不同 Kubernetes 集群之间切换,或者如果你想在默认命名空间之外工作,并且不想每次运行kubectl时都指定--namespace选项,以下是操作方法。
A.2.1. 配置 kubeconfig 文件的位置
kubectl使用的配置通常存储在~/.kube/config文件中。如果它存储在其他位置,则KUBECONFIG环境变量需要指向其位置。
注意
你可以使用多个配置文件,并通过在KUBECONFIG环境变量中指定所有这些文件来让kubectl一次性使用它们(用冒号分隔)。
A.2.2. 理解 kubeconfig 文件的内容
以下列出的是一个示例配置文件。
列表 A.1. 示例 kubeconfig 文件
apiVersion: v1 clusters: - cluster: 1 certificate-authority: /home/luksa/.minikube/ca.crt 1 server: https://192.168.99.100:8443 1 name: minikube 1 contexts: - context: 2 cluster: minikube 2 user: minikube 2 namespace: default 2 name: minikube 2 current-context: minikube 3 kind: Config preferences: {} users: - name: minikube 4 user: 4 client-certificate: /home/luksa/.minikube/apiserver.crt 4 client-key: /home/luksa/.minikube/apiserver.key 4
-
1 包含有关 Kubernetes 集群的信息
-
2 定义 kubectl 上下文
-
3
kubectl当前使用的上下文 -
4 包含用户的凭据
kubeconfig 文件包含四个部分:
-
簇列表
-
用户列表
-
上下文名称列表
-
当前上下文名称
每个集群、用户和上下文都有一个名称。该名称用于引用上下文、用户或集群。
集群
集群条目表示一个 Kubernetes 集群,包含 API 服务器 URL、证书颁发机构(CA)文件,以及可能与 API 服务器通信相关的其他一些配置选项。CA 证书可以存储在单独的文件中并在 kubeconfig 文件中引用,或者可以直接包含在 certificate-authority-data 字段中。
用户
每个用户定义在访问 API 服务器时使用的凭证。这可以是一对用户名和密码,一个身份验证令牌,或者一个客户端密钥和证书。证书和密钥可以包含在 kubeconfig 文件中(通过 client-certificate-data 和 client-key-data 属性),或者存储在单独的文件中并在配置文件中引用,如列表 A.1 所示。
上下文
上下文将一个集群、一个用户和 kubectl 在执行命令时应使用的默认命名空间 kubectl 连接起来。多个上下文可以指向同一个用户或集群。
当前上下文
虽然可以在 kubeconfig 文件中定义多个上下文,但在任何给定时间只有一个上下文是当前上下文。稍后我们将看到如何更改当前上下文。
A.2.3. 列出、添加和修改 kube 配置条目
您可以手动编辑文件以添加、修改和删除集群、用户或上下文,但您也可以通过 kubectl config 命令之一来完成此操作。
添加或修改集群
要添加另一个集群,使用 kubectl config set-cluster 命令:
$ kubectl config set-cluster my-other-cluster
--server=https://k8s.example.com:6443
--certificate-authority=path/to/the/cafile
这将添加一个名为 my-other-cluster 的集群,API 服务器位于 https://k8s.example.com:6443。要查看可以向命令传递的附加选项,请运行 kubectl config set-cluster 以打印用法示例。
如果已存在同名集群,则 set-cluster 命令将覆盖其配置选项。
添加或修改用户凭证
添加和修改用户与添加或修改集群类似。要添加一个使用用户名和密码通过 API 服务器进行身份验证的用户,运行以下命令:
$ kubectl config set-credentials foo --username=foo --password=pass
要使用基于令牌的认证,运行以下命令代替:
$ kubectl config set-credentials foo --token=mysecrettokenXFDJIQ1234
这两个示例都将用户凭证存储在名称为 foo 下。如果您使用相同的凭证对不同的集群进行身份验证,您可以定义一个单独的用户并使用它与两个集群一起使用。
将集群和用户凭证关联起来
上下文定义了与哪个集群使用哪个用户,但也可以定义kubectl在未使用--namespace或-n选项显式指定命名空间时应使用的命名空间。
以下命令用于创建一个新的上下文,该上下文将您创建的集群和用户关联起来:
$ kubectl config set-context some-context --cluster=my-other-cluster
--user=foo --namespace=bar
这创建了一个名为some-context的上下文,它使用my-other-cluster集群和foo用户凭据。在此上下文中,默认命名空间设置为bar。
您也可以使用相同的命令更改当前上下文的命名空间,例如。您可以通过以下方式获取当前上下文名称:
$ kubectl config current-context minikube
然后您可以通过以下方式更改命名空间:
$ kubectl config set-context minikube --namespace=another-namespace
只运行一次这个简单的命令,比每次运行kubectl时都要包含--namespace选项要用户友好得多。
提示
要轻松地在命名空间之间切换,可以定义一个别名,例如:alias kcd='kubectl config set-context $(kubectl config current-context) --namespace '。然后您可以使用kcd some-namespace在命名空间之间切换。
A.2.4. 使用 kubectl 与不同的集群、用户和上下文
当您运行kubectl命令时,将使用 kubeconfig 当前上下文中定义的集群、用户和命名空间,但您可以使用以下命令行选项来覆盖它们:
-
--user用于使用 kubeconfig 文件中的不同用户。 -
--username和--password用于使用不同的用户名和/或密码(它们不需要在配置文件中指定)。如果使用其他类型的身份验证,可以使用--client-key和--client-certificate或--token。 -
--cluster用于使用不同的集群(必须在配置文件中定义)。 -
--server用于指定不同服务器的 URL(该服务器不在配置文件中)。 -
--namespace用于使用不同的命名空间。
A.2.5. 在上下文之间切换
与之前示例中修改当前上下文不同,您还可以使用set-context命令创建一个额外的上下文,然后在这些上下文之间切换。当与多个集群一起工作时,这非常有用(使用set-cluster为它们创建集群条目)。
一旦设置了多个上下文,切换它们就变得非常简单:
$ kubectl config use-context my-other-context
这会将当前上下文切换到my-other-context。
A.2.6. 列出上下文和集群
要列出在您的 kubeconfig 文件中定义的所有上下文,请运行以下命令:
$ kubectl config get-contexts CURRENT NAME CLUSTER AUTHINFO NAMESPACE * minikube minikube minikube default rpi-cluster rpi-cluster admin/rpi-cluster rpi-foo rpi-cluster admin/rpi-cluster foo
如您所见,我正在使用三个不同的上下文。rpi-cluster和rpi-foo上下文使用相同的集群和凭据,但默认使用不同的命名空间。
列出集群的方式类似:
$ kubectl config get-clusters 名称 rpi-cluster minikube
由于安全原因,无法列出凭据。
A.2.7. 删除上下文和集群
要清理上下文或集群列表,您可以手动从 kubeconfig 文件中删除条目,或者使用以下两个命令:
$ kubectl config delete-context my-unused-context
和
$ kubectl config delete-cluster my-old-cluster
附录 B. 使用 kubeadm 设置多节点集群
本附录展示了如何使用多个节点安装 Kubernetes 集群。您将通过 VirtualBox 在虚拟机中运行节点,但也可以使用不同的虚拟化工具或裸机。为了设置主节点和工作节点,您将使用kubeadm工具。
B.1. 设置操作系统和所需软件包
首先,如果您还没有安装,需要下载并安装 VirtualBox。您可以从www.virtualbox.org/wiki/Downloads下载。启动后,从www.centos.org/download下载 CentOS 7 最小化 ISO 镜像。您也可以使用不同的 Linux 发行版,但请确保它由kubernetes.io网站支持。
B.1.1. 创建虚拟机
接下来,您将创建 Kubernetes 主节点的虚拟机。首先,点击左上角的“新建”图标。然后输入“k8s-master”作为名称,选择 Linux 作为类型,选择 Red Hat(64 位)作为版本,如图 B.1 所示。
图 B.1. 在 VirtualBox 中创建虚拟机

点击下一步按钮后,您可以设置虚拟机的内存大小并设置硬盘。如果您有足够的内存,至少选择 2GB(请注意,您将运行三个这样的虚拟机)。在创建硬盘时,保留默认选项。以下是我案例中的选项:
-
硬盘文件类型:VDI(VirtualBox 磁盘镜像)
-
物理硬盘上的存储:动态分配
-
文件位置和大小:k8s-master,大小 8GB
B.1.2. 配置虚拟机的网络适配器
创建完虚拟机后,您需要配置其网络适配器,因为默认设置不会允许您正确运行多个节点。您将配置适配器使其使用桥接适配器模式。这将使您的虚拟机连接到与您的宿主机相同的网络。每个虚拟机将获得自己的 IP 地址,就像它是一个连接到与宿主机相同交换机的物理机器一样。其他选项要复杂得多,因为它们通常需要设置两个网络适配器。
要配置网络适配器,请确保在 VirtualBox 主窗口中选择虚拟机,然后点击设置图标(在您之前点击的新图标旁边)。
将出现一个类似于图 B.2 的窗口。在左侧,选择网络,然后在右侧的主面板中,选择附加到:桥接适配器,如图所示。在名称下拉菜单中,选择您用于连接机器到网络的宿主机适配器。
图 B.2. 配置虚拟机的网络适配器

B.1.3. 安装操作系统
您现在可以运行虚拟机并安装操作系统了。确保虚拟机仍在列表中选中,然后在 VirtualBox 主窗口的顶部点击“开始”图标。
选择启动磁盘
在虚拟机启动之前,VirtualBox 将询问您要使用哪个启动磁盘。点击下拉列表旁边的图标(如图 B.3 所示),然后找到并选择您之前下载的 CentOS ISO 映像。然后点击“开始”以启动虚拟机。
图 B.3. 选择安装 ISO 映像

启动安装
当虚拟机启动时,将出现一个文本菜单屏幕。使用光标向上键选择“安装 CentOS Linux 7”选项,然后按 Enter 键。
设置安装选项
几分钟后,将出现一个图形化的“欢迎使用 CentOS Linux 7”屏幕,允许您选择要使用的语言。我建议您保持语言设置为英语。点击“继续”按钮进入如图 B.4 所示的主设置屏幕。
图 B.4. 主设置屏幕

小贴士
当您点击进入虚拟机的窗口时,您的键盘和鼠标将被虚拟机捕获。要释放它们,请按虚拟机运行的 VirtualBox 窗口右下角显示的键。这通常是 Windows 和 Linux 上的右控制键或 MacOS 上的左 Command 键。
首先,点击“安装位置”,然后立即点击出现的屏幕上的“完成”按钮(您不需要点击其他任何地方)。
然后点击“网络与主机名”。在下一屏幕上,首先通过点击右上角的开关启用网络适配器。然后输入左下角的字段中的主机名,如图 B.5 所示。您目前正在设置主节点,因此将主机名设置为 master.k8s。点击文本字段旁边的“应用”按钮以确认新的主机名。
图 B.5. 设置主机名和配置网络适配器

要返回主设置屏幕,点击左上角的“完成”按钮。
您还需要设置正确的时间区域。点击“日期和时间”,然后在打开的屏幕上选择区域和城市或点击地图上的您的位置。通过点击左上角的“完成”按钮返回主屏幕。
运行安装
要开始安装,点击右下角的“开始安装”按钮。将出现如图 B.6 所示的屏幕。在安装操作系统时,设置 root 密码,如果您想的话,还可以创建用户账户。安装完成后,点击右下角的“重启”按钮。
图 B.6. 在安装操作系统并重启后设置 root 密码

B.1.4. 安装 Docker 和 Kubernetes
以 root 身份登录机器。首先,您需要禁用两个安全功能:SELinux 和防火墙。
禁用 SELinux
要禁用 SELinux,运行以下命令:
# setenforce 0
但这只会临时禁用(直到下一次重启)。要永久禁用,编辑/etc/selinux/config文件,将SELINUX=enforcing行更改为SELINUX=permissive。
禁用防火墙
你还将禁用防火墙,以免遇到任何与防火墙相关的问题。运行以下命令:
# systemctl disable firewalld && systemctl stop firewalld Removed symlink /etc/systemd/system/dbus-org.fedoraproject.FirewallD1... Removed symlink /etc/systemd/system/basic.target.wants/firewalld.service.
添加 Kubernetes yum 仓库
要使 Kubernetes RPM 包对 yum 包管理器可用,你需要在/etc/yum.repos.d/目录中添加一个 kubernetes.repo 文件,如下所示。
列表 B.1. 添加 Kubernetes RPM 仓库
# cat <<EOF > /etc/yum.repos.d/kubernetes.repo [kubernetes] name=Kubernetes baseurl=http://yum.kubernetes.io/repos/kubernetes-el7-x86_64 enabled=1 gpgcheck=1 repo_gpgcheck=1 gpgkey=https://packages.cloud.google.com/yum/doc/yum-key.gpg https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg EOF
注意
如果你正在复制粘贴,请确保 EOF 后没有空格。
安装 Docker、Kubelet、kubeadm、kubectl 和 Kubernetes-CNI
现在你已经准备好安装所有需要的包了:
# yum install -y docker kubelet kubeadm kubectl kubernetes-cni
如你所见,你正在安装相当多的包。以下是它们的内容:
-
docker——容器运行时 -
kubelet——Kubernetes 节点代理,将为你运行一切 -
kubeadm——用于部署多节点 Kubernetes 集群的工具 -
kubectl——与 Kubernetes 交互的命令行工具 -
kubernetes-cni——Kubernetes 容器网络接口
一旦安装完成,你需要手动启用docker和kubelet服务:
# systemctl enable docker && systemctl start docker``# systemctl enable kubelet && systemctl start kubelet
启用 net.bridge.bridge-nf-call-iptables 内核选项
我注意到有某个东西禁用了bridge-nf-call-iptables内核参数,这是 Kubernetes 服务正常运行所必需的。为了解决这个问题,你需要运行以下两个命令:
# sysctl -w net.bridge.bridge-nf-call-iptables=1``# echo "net.bridge.bridge-nf-call-iptables=1" > /etc/sysctl.d/k8s.conf
禁用交换
如果启用了交换,Kubelet 将不会运行,所以你需要使用以下命令来禁用它:
# swapoff -a && sed -i '/ swap / s/^/#/' /etc/fstab
B.1.5. 克隆虚拟机
到目前为止你做的所有事情都必须在计划用于你的集群的每台机器上执行。如果你是在裸机上执行此操作,你需要至少重复前一个章节中描述的过程两次——对于每个工作节点。如果你正在使用虚拟机构建集群,现在是克隆虚拟机的时候了,这样你最终会有三个不同的虚拟机。
关闭虚拟机
要在 VirtualBox 中克隆机器,首先通过运行 shutdown 命令关闭虚拟机:
# shutdown now
克隆虚拟机
现在,在 VirtualBox UI 中右键单击虚拟机并选择克隆。输入新机器的名称,如图 B.7 所示(例如,第一个克隆为 k8s-node1 或第二个克隆为 k8s-node2)。确保您选中了“重新初始化所有网络卡的 MAC 地址”选项,以便每个虚拟机使用不同的 MAC 地址(因为它们将位于同一网络中)。
图 B.7. 克隆主虚拟机

点击“下一步”按钮,然后确保在再次点击“下一步”之前选中了“完整克隆”选项。然后,在下一屏幕上,点击“克隆”(保留“当前机器状态”选项选中)。
对第二个节点的虚拟机重复此过程,然后通过选择所有三个并点击“启动”图标来启动所有三个虚拟机。
在克隆的虚拟机上更改主机名
由于您从主虚拟机创建了两个克隆,因此所有三个虚拟机都配置了相同的主机名。因此,您需要更改两个克隆的主机名。为此,以 root 身份登录到两个节点中的每一个(作为 root)并运行以下命令:
# hostnamectl --static set-hostname node1.k8s
注意
确保在第二个节点上将主机名设置为 node2.k8s。
配置所有三个主机的名称解析
您需要确保所有三个节点都可以解析,无论是通过向 DNS 服务器添加记录还是通过编辑所有节点的 /etc/hosts 文件来实现。例如,您需要将以下三行添加到 hosts 文件中(用您虚拟机的 IP 地址替换),如下所示。
列表 B.2. 需要添加到每个集群节点 /etc/hosts 中的条目
192.168.64.138 master.k8s 192.168.64.139 node1.k8s 192.168.64.140 node2.k8s
您可以通过以 root 身份登录到节点,运行 ip addr 并找到与 enp0s3 网络适配器关联的 IP 地址来获取每个节点的 IP,如下所示。
列表 B.3. 查找每个节点的 IP 地址
# ip addr 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever inet6 ::1/128 scope host valid_lft forever preferred_lft forever 2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000 link/ether 08:00:27:db:c3:a4 brd ff:ff:ff:ff:ff:ff inet 192.168.64.138``/24 brd 192.168.64.255 scope global dynamic enp0s3 valid_lft 59414sec preferred_lft 59414sec inet6 fe80::77a9:5ad6:2597:2e1b/64 scope link valid_lft forever preferred_lft forever
上一列表中的命令输出显示,机器的 IP 地址是 192.168.64.138。您需要在每个节点上运行此命令以获取所有节点的 IP 地址。
B.2. 使用 kubeadm 配置主节点
您现在可以最终在主节点上设置 Kubernetes 控制平面了。
运行 kubeadm init 以初始化主节点
感谢出色的kubeadm工具,初始化主节点您只需运行一个命令,如下所示。
列表 B.4. 使用 kubeadm init 初始化主节点
# kubeadm init [kubeadm] WARNING: kubeadm is in beta, please do not use it for production clusters. [init] Using Kubernetes version: v.1.8.4 ... You should now deploy a pod network to the cluster. Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at: http://kubernetes.io/docs/admin/addons/ You can now join any number of machines by running the following on each node as root: kubeadm join --token eb3877.3585d0423978c549 192.168.64.138:6443 --discovery-token-ca-cert-hash sha256:037d2c5505294af196048a17f184a79411c7b1eac48aaa0ad137075be3d7a847
注意
记下 kubeadm init 输出最后一行的命令。您稍后需要用到它。
Kubeadm 已部署所有必要的控制平面组件,包括 etcd、API 服务器、调度器和控制器管理器。它还部署了 kube-proxy,使 Kubernetes 服务从主节点可用。
B.2.1. 理解 kubeadm 如何运行组件
所有这些组件都以容器形式运行。您可以使用docker ps命令来确认这一点。但kubeadm并不直接使用 Docker 来运行它们。它将它们的 YAML 描述符部署到/etc/kubernetes/manifests目录。该目录由 Kubelet 监控,然后通过 Docker 运行这些组件。这些组件作为 Pod 运行。您可以使用kubectl get命令查看它们。但首先,您需要配置kubectl。
在主节点上运行 kubectl
在初始步骤之一中,您安装了kubectl、docker、kubeadm和其他软件包。但您必须首先通过 kubeconfig 文件配置它,才能使用kubectl与您的集群通信。
幸运的是,必要的配置存储在/etc/kubernetes/admin.conf文件中。您需要做的只是通过设置KUBECONFIG环境变量来让kubectl使用它,如附录 A(index_split_135.html#filepos1721130)中所述:
# export KUBECONFIG=/etc/kubernetes/admin.conf
列出 pods
要测试kubectl,您可以列出控制平面(它们位于kube-system命名空间中)的 pods,如下所示。
列表 B.5. kube-system 命名空间中的系统 pods
# kubectl get po -n kube-system NAME READY STATUS RESTARTS AGE etcd-master.k8s 1/1 Running 0 21m kube-apiserver-master.k8s 1/1 Running 0 22m kube-controller-manager-master.k8s 1/1 Running 0 21m kube-dns-3913472980-cn6kz 0/3 Pending 0 22m kube-proxy-qb709 1/1 Running 0 22m kube-scheduler-master.k8s 1/1 Running 0 21m
列出节点
你已经完成了主节点的设置,但你仍然需要设置节点。尽管你已经在两个工作节点上安装了 Kubelet(你可能是分别安装每个节点,或者在安装所有必需的包后克隆了初始 VM),但它们还不是你的 Kubernetes 集群的一部分。你可以通过使用 kubectl 列出节点来查看这一点:
# kubectl get node NAME STATUS ROLES AGE VERSION master.k8s NotReady master 2m v1.8.4
看看,只有主节点被列为节点。甚至主节点也被显示为不就绪。你稍后会看到原因。现在,你将设置你的两个节点。
B.3. 使用 kubeadm 配置工作节点
当使用 kubeadm 时,配置工作节点甚至比配置主节点更容易。实际上,当你运行 kubeadm init 命令来设置你的主节点时,它已经告诉你如何配置你的工作节点(在下一列表中重复)。
列表 B.6. kubeadm init 命令输出的最后部分
你现在可以通过在每个节点上以 root 用户运行以下命令来加入任意数量的机器: kubeadm join --token eb3877.3585d0423978c549 192.168.64.138:6443 --discovery-token-ca-cert-hash sha256:037d2c5505294af196048a17f184a79411c7b1eac48aaa0ad137075be3d7a847
你需要做的就是运行带有指定令牌和主节点 IP 地址/端口的 kubeadm join 命令在你的两个节点上。然后节点注册到主节点上只需不到一分钟的时间。你可以通过在主节点上再次运行 kubectl get node 命令来确认它们已经注册:
# kubectl get nodes NAME STATUS ROLES AGE VERSION master.k8s NotReady master 3m v1.8.4 node1.k8s NotReady <none> 3s v1.8.4 node2.k8s NotReady <none> 5s v1.8.4
好的,你已经取得了进展。你的 Kubernetes 集群现在由三个节点组成,但它们都不处于就绪状态。让我们来调查一下。
让我们使用以下列表中的 kubectl describe 命令来查看更多信息。在顶部某个地方,你会看到一个 Conditions 列表,显示节点上的当前状态。其中之一将显示以下 Reason 和 Message。
列表 B.7. kubectl describe 显示节点不就绪的原因
# kubectl describe node node1.k8s ... KubeletNotReady runtime network not ready: NetworkReady=false reason:NetworkPluginNotReady message:docker: network plugin is not ready: cni config uninitialized
根据这个情况,Kubelet 并未完全就绪,因为容器网络(CNI)插件尚未就绪,这是预期的,因为你尚未部署 CNI 插件。你现在将部署一个。
B.3.1. 设置容器网络
你将安装 Weave Net 容器网络插件,但还有其他几种替代方案也可用。它们列在可用的 Kubernetes 附加组件中,见kubernetes.io/docs/admin/addons/。
部署 Weave Net 插件(就像大多数其他附加组件一样)非常简单:
$ kubectl apply -f "https://cloud.weave.works/k8s/net?k8s-version=$(kubectl``version | base64 | tr -d '\n')
这将部署一个 DaemonSet 和一些与安全相关的资源(有关 ClusterRole 和 ClusterRoleBinding 的解释,请参阅第十二章,这些资源与 DaemonSet 一起部署)。
一旦 DaemonSet 控制器创建了 pod 并在所有节点上启动,节点应该变为就绪状态:
# k get node NAME STATUS ROLES AGE VERSION master.k8s Ready master 9m v1.8.4 node1.k8s Ready <none> 5m v1.8.4 node2.k8s Ready <none> 5m v1.8.4
就这样。你现在拥有了一个完全功能的三节点 Kubernetes 集群,Weave Net 提供了覆盖网络。所有必需的组件(除了 Kubelet 本身)都作为 pod 运行,由 Kubelet 管理,如下所示。
列表 B.8. 部署 Weave Net 后 kube-system 命名空间中的系统 pod
# kubectl get po --all-namespaces NAMESPACE NAME READY STATUS AGE kube-system etcd-master.k8s 1/1 Running 1h kube-system kube-apiserver-master.k8s 1/1 Running 1h kube-system kube-controller-manager-master.k8s 1/1 Running 1h kube-system kube-dns-3913472980-cn6kz 3/3 Running 1h kube-system kube-proxy-hcqnx 1/1 Running 24m kube-system kube-proxy-jvdlr 1/1 Running 24m kube-system kube-proxy-qb709 1/1 Running 1h kube-system kube-scheduler-master.k8s 1/1 Running 1h kube-system weave-net-58zbk 2/2 Running 7m kube-system weave-net-91kjd 2/2 Running 7m kube-system weave-net-vt279 2/2 Running 7m
B.4. 在本地机器上使用集群
到目前为止,你已经在主节点上使用 kubectl 与集群通信。你可能还希望配置本地机器上的 kubectl 实例。
要做到这一点,您需要使用以下命令将 /etc/kubernetes/admin.conf 文件从主机关复制到您的本地机器:
$ scp root@192.168.64.138:/etc/kubernetes/admin.conf ~/.kube/config2
将 IP 替换为您的主机的 IP。然后,将 KUBECONFIG 环境变量指向 ~/.kube/config2 文件,如下所示:
$ export KUBECONFIG=~/.kube/config2
Kubectl 将现在使用此配置文件。要切换回使用之前的配置文件,取消设置环境变量。
现在,您已经准备好从您的本地机器使用集群了。
附录 C. 使用其他容器运行时
C.1. 用 rkt 替换 Docker
我们在这本书中多次提到了 rkt(发音为 rock-it)。与 Docker 一样,它使用与 Docker 相同的 Linux 技术在隔离的容器中运行应用程序。让我们看看 rkt 与 Docker 的区别以及如何在 Minikube 中尝试它。
rkt 的第一个优点是它直接支持 Pod 的概念(运行多个相关容器),这与仅运行单个容器的 Docker 不同。rkt 基于开放标准,并且从一开始就考虑了安全性(例如,镜像已签名,因此您可以确信它们没有被篡改)。与最初基于客户端-服务器架构且与 systemd 等初始化系统不兼容的 Docker 不同,rkt 是一个直接运行容器的 CLI 工具,而不是告诉守护进程运行它。rkt 的一个好处是它可以运行现有的 Docker 格式容器镜像,因此您无需重新打包应用程序即可开始使用 rkt。
C.1.1. 配置 Kubernetes 以使用 rkt
如您从第十一章中可能记得的,Kubelet 是唯一与容器运行时通信的 Kubernetes 组件。要使 Kubernetes 使用 rkt 而不是 Docker,您需要通过运行带有 --container-runtime=rkt 命令行选项的 Kubelet 来配置它。但请注意,rkt 的支持并不像 Docker 那样成熟。
请参考 Kubernetes 文档以获取有关如何使用 rkt 以及哪些功能受支持的更多信息。在此,我们将快速浏览一个示例,以帮助您入门。
C.1.2. 尝试使用 Minikube 配置 rkt
幸运的是,要开始在 Kubernetes 上使用 rkt,您只需要与您已经使用的相同的 Minikube 可执行文件。要在 Minikube 中使用 rkt 作为容器运行时,您只需使用以下两个选项启动 Minikube 即可:
$ minikube start --container-runtime=rkt --network-plugin=cni
注意
您可能需要先运行 minikube delete 来删除现有的 Minikube VM。
--container-runtime=rkt 选项显然配置了 Kubelet 以使用 rkt 作为容器运行时,而 --network-plugin=cni 则使其使用容器网络接口作为网络插件。没有此选项,Pod 将无法运行,因此您必须使用它。
运行 Pod
一旦 Minikube VM 启动,您就可以像以前一样与 Kubernetes 交互。例如,您可以使用 kubectl run 命令部署 kubia 应用程序:
$ kubectl run kubia --image=luksa/kubia --port 8080 deployment "kubia" created
当 Pod 启动时,您可以通过使用 kubectl describe 检查其容器来看到它是通过 rkt 运行的,如下所示。
列表 C.1. 使用 rkt 运行的 Pod
$ kubectl describe pods Name: kubia-3604679414-l1nn3 ... Status: Running IP: 10.1.0.2 Controllers: ReplicaSet/kubia-3604679414 Containers: kubia: Container ID: rkt://87a138ce-...-96e375852997:kubia 1 Image: luksa/kubia Image ID: rkt://sha512-5bbc5c7df6148d30d74e0... 1 ...
- 1 容器和镜像 ID 提到 rkt 而不是 Docker。
你也可以尝试访问 pod 的 HTTP 端口,看看它是否正确响应 HTTP 请求。你可以通过创建一个 NodePort 服务或使用 kubectl port-forward 等方式来实现。
检查 Minikube VM 中运行的容器
要更熟悉 rkt,你可以尝试使用以下命令登录到 Minikube 虚拟机:
$ minikube ssh
然后,你可以使用 rkt list 来查看正在运行的 pods 和容器,如下所示。
列出 C.2. 使用 rkt list 列出正在运行的容器
$ rkt list UUID APP IMAGE NAME STATE ... 4900e0a5 k8s-dashboard gcr.io/google_containers/kun... running ... 564a6234 nginx-ingr-ctrlr gcr.io/google_containers/ngi... running ... 5dcafffd dflt-http-backend gcr.io/google_containers/def... running ... 707a306c kube-addon-manager gcr.io/google-containers/kub... running ... 87a138ce kubia`` registry-1.docker.io/luksa/k... running ... d97f5c29 kubedns gcr.io/google_containers/k8s... running ... dnsmasq gcr.io/google_containers/k8... sidecar gcr.io/google_containers/k8...
你可以看到 kubia 容器,以及其他正在运行的系统容器(在 kube-system 命名空间中部署的 pod)。注意底部两个容器在 UUID 或 STATE 列中没有任何内容?这是因为它们属于与上面列出的 kubedns 容器相同的 pod。
Rkt 将属于同一 pod 的容器分组在一起打印。每个 pod(而不是每个容器)都有自己的 UUID 和状态。如果你在以前使用 Docker 作为容器运行时尝试过这样做,你会欣赏使用 rkt 查看所有 pods 和它们的容器有多么容易。你会注意到每个 pod 都没有基础设施容器(我们在第十一章章节 11 中解释了它们)。这是因为 rkt 对 pods 的原生支持。
列出容器镜像
如果你玩过 Docker CLI 命令,你会很快熟悉 rkt 的命令。运行 rkt 不带任何参数,你会看到你可以运行的所有命令。例如,要列出容器镜像,你运行以下列表中的命令。
列出 C.3. 使用 rkt image list 列出镜像
$ rkt image list ID NAME SIZE IMPORT TIME LAST USED sha512-a9c3 ...addon-manager:v6.4-beta.1 245MiB 24 min ago 24 min ago sha512-a078 .../rkt/stage1-coreos:1.24.0 224MiB 24 min ago 24 min ago sha512-5bbc ...ker.io/luksa/kubia:latest 1.3GiB 23 min ago 23 min ago sha512-3931 ...es-dashboard-amd64:v1.6.1 257MiB 22 min ago 22 min ago sha512-2826 ...ainers/defaultbackend:1.0 15MiB 22 min ago 22 min ago sha512-8b59 ...s-controller:0.9.0-beta.4 233MiB 22 min ago 22 min ago sha512-7b59 ...dns-kube-dns-amd64:1.14.2 100MiB 21 min ago 21 min ago sha512-39c6 ...nsmasq-nanny-amd64:1.14.2 86MiB 21 min ago 21 min ago sha512-89fe ...-dns-sidecar-amd64:1.14.2 85MiB 21 min ago 21 min ago
这些都是 Docker 格式的容器镜像。您也可以尝试使用 acbuild 工具(可在github.com/containers/build找到)构建 OCI 镜像格式(OCI 代表开放容器倡议)的镜像,并使用 rkt 运行它们。这样做超出了本书的范围,所以我会让您自己尝试。
到目前为止,本附录中解释的信息应该足以让您开始使用 rkt 与 Kubernetes 一起使用。有关 rkt 的更多信息,请参阅coreos.com/rkt文档,有关 Kubernetes 的更多信息,请参阅kubernetes.io/docs文档。
C.2. 通过 CRI 使用其他容器运行时
Kubernetes 对其他容器运行时的支持不仅限于 Docker 和 rkt。这两个运行时最初是直接集成到 Kubernetes 中的,但在 Kubernetes 版本 1.5 中,引入了容器运行时接口(CRI)。CRI 是一个插件 API,它使得其他容器运行时能够轻松地与 Kubernetes 集成。现在,人们可以自由地将其他容器运行时插入到 Kubernetes 中,而无需深入挖掘 Kubernetes 的代码。他们只需要实现几个接口方法即可。
从 Kubernetes 版本 1.6 开始,CRI 是 Kubelet 使用的默认接口。现在,Docker 和 rkt 都是通过 CRI 使用的(不再是直接使用)。
C.2.1. 介绍 CRI-O 容器运行时
除了 Docker 和 rkt 之外,一个新的 CRI 实现 CRI-O 允许 Kubernetes 直接启动和管理符合 OCI 规范的容器,而无需您部署任何额外的容器运行时。
您可以通过使用--container-runtime=crio启动 Minikube 来尝试 CRI-O。
C.2.2. 在虚拟机中而不是在容器中运行应用
Kubernetes 是一个容器编排系统,对吧?在本书中,我们探讨了众多特性,表明它远不止是一个编排系统,但归根结底,当您使用 Kubernetes 运行应用时,应用总是运行在容器内部,对吧?您可能会惊讶地发现,这种情况已经不再适用了。
正在开发新的 CRI 实现,使得 Kubernetes 能够在虚拟机中而不是在容器中运行应用程序。其中一个名为 Frakti 的实现允许你通过虚拟机管理程序直接运行基于 Docker 的常规容器镜像,这意味着每个容器运行自己的内核。这比它们使用相同内核时提供了更好的容器间隔离。
还有更多。另一个 CRI 实现是 Mirantis Virtlet,它使得运行实际的虚拟机镜像(在 QCOW2 镜像文件格式下,这是 QEMU 虚拟机工具使用的格式之一)成为可能,而不是容器镜像。当你使用 Virtlet 作为 CRI 插件时,Kubernetes 为每个 pod 启动一个虚拟机。这有多么酷?
附录 D. 集群联邦
在第十一章关于高可用性的部分中,我们探讨了 Kubernetes 如何处理单个机器甚至整个服务器机架或支持基础设施的故障。但整个数据中心如果出现故障怎么办呢?
为了确保您不会受到数据中心级故障的影响,应用应该部署在多个数据中心或云可用区。当其中一个数据中心或可用区不可用时,客户端请求可以被路由到剩余健康的数据中心或区域中运行的应用。
虽然 Kubernetes 不要求您在同一个数据中心运行控制平面和节点,但您几乎总是想这样做,以保持它们之间的网络延迟低,并减少它们彼此断开连接的可能性。而不是有一个跨多个位置的单一集群,更好的选择是在每个位置都有一个独立的 Kubernetes 集群。我们将在此附录中探讨这种方法。
D.1. 介绍 Kubernetes 集群联邦
Kubernetes 允许您通过集群联邦将多个集群组合成一个集群的集群。它允许用户在世界不同位置的多个集群中部署和管理应用,也可以跨不同云提供商与本地集群(混合云)结合。集群联邦的动机不仅是为了确保高可用性,而且是为了将多个异构集群合并成一个由单个管理界面管理的超级集群。
例如,通过将本地集群与运行在云提供商基础设施上的集群相结合,您可以在本地运行应用程序系统的隐私敏感组件,而无需敏感部分运行在云中。另一个例子是最初仅在小型本地集群中运行您的应用程序,但当应用程序的计算需求超过集群的容量时,允许应用程序溢出到基于云的集群,该集群在云提供商的基础设施上自动配置。
D.2. 理解架构
让我们快速了解一下 Kubernetes 集群联邦是什么。集群的集群可以与一个常规集群相比较,其中不是节点,而是完整的集群。正如 Kubernetes 集群由控制平面和多个工作节点组成一样,联邦集群由联邦控制平面和多个 Kubernetes 集群组成。类似于 Kubernetes 控制平面如何管理一组工作节点上的应用,联邦控制平面做的是同样的事情,但跨一组集群而不是节点。
联邦控制平面由三部分组成:
-
etcd 用于存储联邦 API 对象
-
联邦 API 服务器
-
联邦控制器管理器
这与常规 Kubernetes 控制平面没有太大区别。etcd 存储联邦 API 对象,API 服务器是所有其他组件通信的 REST 端点,联邦控制器管理器运行各种基于您通过 API 服务器创建的 API 对象的操作执行的联邦控制器。
用户通过联邦 API 服务器创建联邦 API 对象(或联邦资源)。联邦控制器监视这些对象,然后与底层集群的 API 服务器通信以创建常规 Kubernetes 资源。联邦集群的架构如图 D.1 所示。
图 D.1. 不同地理位置的集群联邦

D.3. 理解联邦 API 对象
联邦 API 服务器允许您创建本书中提到的对象的联邦版本。
D.3.1. 介绍 Kubernetes 资源的联邦版本
在撰写本文时,以下联邦资源得到支持:
-
命名空间
-
ConfigMaps 和 Secrets
-
服务和入口
-
部署、副本集、作业和守护进程集
-
HorizontalPodAutoscalers
注意
请查阅 Kubernetes 集群联邦文档,以获取支持的联邦资源的最新列表。
除了这些资源外,联邦 API 服务器还支持集群对象,该对象代表底层 Kubernetes 集群,就像节点对象代表常规 Kubernetes 集群中的工作节点一样。为了帮助您可视化联邦对象与底层集群中创建的对象之间的关系,请参阅图 D.2。
图 D.2. 联邦资源与底层集群中常规资源之间的关系

D.3.2. 理解联邦资源的作用
对于部分联邦对象,当您在联邦 API 服务器中创建对象时,运行在联邦控制器管理器中的控制器将在所有底层 Kubernetes 集群中创建常规集群范围资源,并管理它们,直到联邦对象被删除。
对于某些联邦资源类型,底层集群中创建的资源是联邦资源的精确副本;对于其他类型,它们是略微修改的版本,而某些联邦资源在底层集群中根本不会引起任何创建。副本与原始联邦版本保持同步。但同步是单向的,仅从联邦服务器到底层集群。如果您在底层集群中修改资源,更改将不会同步到联邦 API 服务器。
例如,如果您在联邦 API 服务器中创建一个命名空间,所有底层集群中都会创建一个具有相同名称的命名空间。如果您然后在那个命名空间内创建一个联邦 ConfigMap,所有底层集群中都会创建一个具有该确切名称和内容的 ConfigMap,位于相同的命名空间中。这也适用于 Secrets、Services 和 DaemonSets。
ReplicaSets 和 Deployments 是不同的。它们不会被盲目地复制到底层集群,因为这不是用户通常想要的。毕竟,如果您创建了一个期望副本数为 10 的 Deployment,您可能不希望每个底层集群都运行 10 个 Pod 副本。您希望总共有 10 个副本。因此,当您在 Deployment 或 ReplicaSet 中指定期望副本数时,联邦控制器会创建底层 Deployments/ReplicaSets,使得它们的期望副本数之和等于联邦 Deployment 或 ReplicaSet 中指定的期望副本数。默认情况下,副本会在集群之间均匀分布,但这可以被覆盖。
备注
目前,您需要单独连接到每个集群的 API 服务器,以获取在该集群中运行的 Pod 列表。您无法通过联邦 API 服务器列出所有集群的 Pod。
另一方面,联邦 Ingress 资源不会在底层集群中创建任何 Ingress 对象。您可能还记得第五章,Ingress 代表外部客户端访问服务的单个入口点。因此,联邦 Ingress 资源创建了一个全局的、跨所有底层集群的服务的多集群入口点。
备注
对于常规的 Ingress,需要联邦 Ingress 控制器来实现这一点。
设置联邦 Kubernetes 集群超出了本书的范围,因此您可以通过参考 Kubernetes 在线文档中的用户和管理指南中的集群联邦部分来了解更多关于该主题的信息,kubernetes.io/docs/。
Kubernetes 书中涵盖的资源
| 资源(缩写。)[API 版本] | 描述 | 章节 | |
|---|---|---|---|
| 命名空间^([*])(ns)[v1] | 允许将资源组织成非重叠的组(例如,按租户划分) | 3.7 | |
| 部署工作负载 | Pod(po)[v1] | 包含一个或多个在相邻容器中协同运行的进程的基本可部署单元 | 3.1 |
| 副本集(rs)apps/v1beta2^([[**])] | 保持一个或多个 pod 副本运行 | 4.3 | |
| 复制控制器(rc)[v1] | 旧版、功能较弱的副本集等效物 | 4.2 | |
| 作业 [batch/v1] | 运行执行可完成任务的 pod | 4.5 | |
| CronJob [batch/v1beta1] | 运行一次或周期性运行的计划任务 | 4.6 | |
| 守护集(ds)apps/v1beta2[**] | 在每个节点上运行一个 pod 副本(在所有节点上或在匹配节点选择器的节点上) | 4.4 | |
| 有状态集(sts)apps/v1beta1[**] | 运行具有稳定身份的有状态 pod | 10.2 | |
| 部署(deploy)apps/v1beta1[**] | 声明式部署和更新 pod | 9.3 | |
| 服务 | 服务(svc)[v1] | 在单个和稳定的 IP 地址和端口对上公开一个或多个 pod | 5.1 |
| 端点(ep)[v1] | 定义通过服务公开哪些 pod(或其他服务器) | 5.2.1 | |
| 入口(ing)[extensions/v1beta1] | 通过单个外部可达 IP 地址将一个或多个服务公开给外部客户端 | 5.4 | |
| 配置 | 配置映射(cm)[v1] | 存储非敏感配置选项的键值映射,并将其公开给应用程序 | 7.4 |
| 机密 [v1] | 与配置映射类似,但用于敏感数据 | 7.5 | |
| 存储 | 持久卷*(pv)[v1] | 指向可以通过持久卷声明挂载到 pod 中的持久存储 | 6.5 |
| 持久卷声明(pvc)[v1] | 对持久卷的请求和声明 | 6.5 | |
| 存储类*(sc)[storage.k8s.io/v1] | 定义在持久卷声明中可声明的动态配置存储的类型 | 6.6 | |
| 缩放 | 水平 pod 自动缩放器(hpa)autoscaling/v2beta1[**] | 根据 CPU 使用率或其他指标自动缩放 pod 副本数量 | 15.1 |
| Pod 故障预算(pdb)[policy/v1beta1] | 定义在疏散节点时必须保持运行的最小 pod 数量 | 15.3.3 | |
| 资源 | 资源限制(limits)[v1] | 定义命名空间中 pod 的最小、最大、默认限制和默认请求 | 14.4 |
| 资源配额(quota)[v1] | 定义命名空间中 pod 可用的计算资源量 | 14.5 | |
| 集群状态 | 节点*(no)[v1] | 代表 Kubernetes 工作节点 | 2.2.2 |
| 集群* [federation/v1beta1] | Kubernetes 集群(用于集群联邦) | 附录 D | |
| 组件状态*(cs)[v1] | 控制平面组件的状态 | 11.1.1 | |
| 事件 (ev) [v1] | 关于集群中发生的事情的报告 | 11.2.3 | |
| 安全 | 服务账户 (sa) [v1] | 由在 pods 中运行的应用程序使用的账户 | 12.1.2 |
| 角色 [rbac.authorization.k8s.io/v1] | 定义了主题可以在哪些资源上执行哪些操作(按命名空间划分) | 12.2.3 | |
| 集群角色* [rbac.authorization.k8s.io/v1] | 与角色类似,但用于集群级别的资源或授予跨所有命名空间访问资源的权限 | 12.2.4 | |
| 角色绑定 [rbac.authorization.k8s.io/v1] | 定义了谁可以在命名空间内执行角色或集群角色中定义的操作 | 12.2.3 | |
| 集群角色绑定* [rbac.authorization.k8s.io/v1] | 与角色绑定类似,但跨越所有命名空间 | 12.2.4 | |
| Pod 安全策略* (psp) [extensions/v1beta1] | 一个集群级别的资源,定义了 pods 可以使用哪些安全敏感功能 | 13.3.1 | |
| 网络策略 (netpol) [networking.k8s.io/v1] | 通过指定哪些 pods 可以相互连接来隔离 pods 之间的网络 | 13.4 | |
| 证书签名请求* (csr) [certificates.k8s.io/v1beta1] | 对签名公钥证书的请求 | 5.4.4 | |
| Ext. | 自定义资源定义* (crd) [apiextensions.k8s.io/v1beta1] | 定义了一个自定义资源,允许用户创建自定义资源的实例 | 18.1 |
^*
集群级别的资源(非命名空间)
^(**)
也在其他 API 版本中;列出的版本是本书中使用的版本


浙公网安备 33010602011771号