精通-Kubernetes-全-

精通 Kubernetes(全)

原文:zh.annas-archive.org/md5/0FB6BD53079686F120215D277D8C163C

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

Kubernetes 是一个开源系统,自动化部署、扩展和管理容器化应用程序。如果您运行的不仅仅是一些容器,或者想要自动化管理容器,您就需要 Kubernetes。本书重点介绍了如何通过高级管理 Kubernetes 集群。

本书首先解释了 Kubernetes 架构背后的基本原理,并详细介绍了 Kubernetes 的设计。您将了解如何在 Kubernetes 上运行复杂的有状态微服务,包括水平 Pod 自动缩放、滚动更新、资源配额和持久存储后端等高级功能。通过真实的用例,您将探索网络配置的选项,并了解如何设置、操作和排除各种 Kubernetes 网络插件。最后,您将学习自定义资源开发和在自动化和维护工作流中的利用。本书还将涵盖基于 Kubernetes 1.10 发布的一些额外概念,如 Promethus、基于角色的访问控制和 API 聚合。

通过本书,您将了解从中级到高级水平所需的一切。

本书适合对象

本书适用于具有 Kubernetes 中级知识水平的系统管理员和开发人员,现在希望掌握其高级功能。您还应该具备基本的网络知识。这本高级书籍为掌握 Kubernetes 提供了一条路径。

充分利用本书

为了跟随每一章的示例,您需要在您的计算机上安装最新版本的 Docker 和 Kubernetes,最好是 Kubernetes 1.10。如果您的操作系统是 Windows 10 专业版,您可以启用 hypervisor 模式;否则,您需要安装 VirtualBox 并使用 Linux 客户操作系统。

下载彩色图片

我们还提供了一个 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图片。您可以从以下网址下载:www.packtpub.com/sites/default/files/downloads/MasteringKubernetesSecondEdition_ColorImages.pdf

使用的约定

本书中使用了许多文本约定。

CodeInText:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。这是一个例子:“让我们使用get nodes来检查集群中的节点。”

代码块设置如下:

type Scheduler struct { 
    config *Config 
} 

任何命令行输入或输出都以以下方式编写:

> kubectl create -f candy.yaml
candy "chocolate" created 

粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词会以这种方式出现在文本中。这是一个例子:“让我们点击 kubedns pod。”

警告或重要提示会以这种方式出现。提示和技巧会以这种方式出现。

第一章:了解 Kubernetes 架构

Kubernetes 是一个庞大的开源项目和生态系统,拥有大量的代码和功能。Kubernetes 由谷歌开发,但加入了Cloud Native Computing FoundationCNCF),成为容器应用领域的明确领导者。简而言之,它是一个用于编排基于容器的应用程序部署、扩展和管理的平台。您可能已经了解过 Kubernetes,甚至在一些项目中使用过它,甚至在工作中使用过它。但要理解 Kubernetes 的全部内容,如何有效使用它以及最佳实践是什么,需要更多的知识。在本章中,我们将建立必要的知识基础,以充分利用 Kubernetes 的潜力。我们将首先了解 Kubernetes 是什么,Kubernetes 不是什么,以及容器编排的确切含义。然后,我们将介绍一些重要的 Kubernetes 概念,这些概念将构成我们在整本书中将使用的词汇。之后,我们将更详细地深入了解 Kubernetes 的架构,并看看它如何为用户提供所有这些功能。然后,我们将讨论 Kubernetes 支持的各种运行时和容器引擎(Docker 只是其中一种选择),最后,我们将讨论 Kubernetes 在完整的持续集成和部署流水线中的作用。

在本章结束时,您将对容器编排有扎实的了解,了解 Kubernetes 解决了哪些问题,Kubernetes 设计和架构的基本原理,以及它支持的不同运行时。您还将熟悉开源存储库的整体结构,并准备好随时跳入并找到任何问题的答案。

Kubernetes 是什么?

Kubernetes 是一个涵盖大量服务和功能的平台,不断增长。它的核心功能是在您的基础设施中安排容器工作负载,但它并不止步于此。以下是 Kubernetes 带来的其他一些功能:

  • 挂载存储系统

  • 分发密钥

  • 检查应用程序健康状况

  • 复制应用程序实例

  • 使用水平 Pod 自动缩放

  • 命名和发现

  • 负载均衡

  • 滚动更新

  • 监控资源

  • 访问和摄取日志

  • 调试应用程序

  • 提供身份验证和授权

Kubernetes 不是什么

Kubernetes 不是平台即服务PaaS)。它不规定您所需系统的许多重要方面;相反,它将它们留给您或其他构建在 Kubernetes 之上的系统,如 Deis、OpenShift 和 Eldarion。例如:

  • Kubernetes 不需要特定的应用程序类型或框架

  • Kubernetes 不需要特定的编程语言

  • Kubernetes 不提供数据库或消息队列

  • Kubernetes 不区分应用程序和服务

  • Kubernetes 没有点击即部署的服务市场

  • Kubernetes 允许用户选择自己的日志记录、监控和警报系统

理解容器编排

Kubernetes 的主要责任是容器编排。这意味着确保执行各种工作负载的所有容器都被安排在物理或虚拟机上运行。容器必须被有效地打包,并遵循部署环境和集群配置的约束。此外,Kubernetes 必须监视所有运行的容器,并替换死掉的、无响应的或其他不健康的容器。Kubernetes 提供了许多其他功能,您将在接下来的章节中了解到。在本节中,重点是容器及其编排。

物理机器、虚拟机器和容器

一切都始于硬件,也以硬件结束。为了运行您的工作负载,您需要一些真实的硬件。这包括实际的物理机器,具有一定的计算能力(CPU 或核心)、内存和一些本地持久存储(旋转磁盘或固态硬盘)。此外,您还需要一些共享的持久存储和网络,以连接所有这些机器,使它们能够找到并相互通信。在这一点上,您可以在物理机器上运行多个虚拟机,或者保持裸金属级别(没有虚拟机)。Kubernetes 可以部署在裸金属集群(真实硬件)或虚拟机集群上。反过来,Kubernetes 可以在裸金属或虚拟机上直接编排它管理的容器。理论上,Kubernetes 集群可以由裸金属和虚拟机的混合组成,但这并不常见。

容器的好处

容器代表了大型复杂软件系统开发和运行中的真正范式转变。以下是与传统模型相比的一些好处:

  • 敏捷的应用程序创建和部署

  • 持续开发、集成和部署

  • 开发和运维的关注点分离

  • 在开发、测试和生产环境中保持环境一致性

  • 云和操作系统的可移植性

  • 以应用为中心的管理

  • 松散耦合、分布式、弹性、自由的微服务

  • 资源隔离

  • 资源利用

云中的容器

容器非常适合打包微服务,因为它们在为微服务提供隔离的同时非常轻量,并且在部署许多微服务时不会产生很多开销,就像使用虚拟机一样。这使得容器非常适合云部署,因为为每个微服务分配整个虚拟机的成本是禁止的。

所有主要的云服务提供商,如亚马逊 AWS、谷歌的 GCE、微软的 Azure,甚至阿里巴巴云,现在都提供容器托管服务。谷歌的 GKE 一直以来都是基于 Kubernetes。AWS ECS 基于他们自己的编排解决方案。微软 Azure 的容器服务是基于 Apache Mesos 的。Kubernetes 可以部署在所有云平台上,但直到今天它才没有与其他服务深度集成。但在 2017 年底,所有云服务提供商宣布直接支持 Kubernetes。微软推出了 AKS,AWS 发布了 EKS,阿里巴巴云开始开发一个 Kubernetes 控制器管理器,以无缝集成 Kubernetes。

牲畜与宠物

在过去,当系统规模较小时,每台服务器都有一个名字。开发人员和用户清楚地知道每台机器上运行的软件是什么。我记得,在我工作过的许多公司中,我们经常讨论几天来决定服务器的命名主题。例如,作曲家和希腊神话人物是受欢迎的选择。一切都非常舒适。你对待你的服务器就像珍爱的宠物一样。当一台服务器死掉时,这是一场重大危机。每个人都争先恐后地想弄清楚从哪里获取另一台服务器,死掉的服务器上到底运行了什么,以及如何在新服务器上让它工作。如果服务器存储了一些重要数据,那么希望你有最新的备份,也许你甚至能够恢复它。

显然,这种方法是不可扩展的。当你有几十台或几百台服务器时,你必须开始像对待牲畜一样对待它们。你考虑的是整体,而不是个体。你可能仍然有一些宠物,但你的 Web 服务器只是牲畜。

Kubernetes 将牲畜的方法应用到了极致,并全权负责将容器分配到特定的机器上。大部分时间你不需要与单独的机器(节点)进行交互。这对于无状态的工作负载效果最好。对于有状态的应用程序,情况有些不同,但 Kubernetes 提供了一个称为 StatefulSet 的解决方案,我们很快会讨论它。

在这一部分,我们涵盖了容器编排的概念,并讨论了主机(物理或虚拟)和容器之间的关系,以及在云中运行容器的好处,并最后讨论了牲畜与宠物的区别。在接下来的部分,我们将了解 Kubernetes 的世界,并学习它的概念和术语。

Kubernetes 概念

在这一部分,我将简要介绍许多重要的 Kubernetes 概念,并为您提供一些背景,说明它们为什么需要以及它们如何与其他概念互动。目标是熟悉这些术语和概念。稍后,我们将看到这些概念如何被编织在一起,并组织成 API 组和资源类别,以实现令人敬畏的效果。你可以把许多这些概念看作是构建块。一些概念,比如节点和主节点,被实现为一组 Kubernetes 组件。这些组件处于不同的抽象级别,我会在专门的部分* Kubernetes 组件*中详细讨论它们。

这是著名的 Kubernetes 架构图:

集群

集群是 Kubernetes 用来运行组成系统的各种工作负载的计算、存储和网络资源的集合。请注意,您的整个系统可能由多个集群组成。我们将在后面详细讨论联邦的这种高级用例。

节点

一个节点是一个单独的主机。它可以是物理机或虚拟机。它的工作是运行 pod,我们马上会看到。每个 Kubernetes 节点都运行几个 Kubernetes 组件,比如 kubelet 和 kube 代理。节点由 Kubernetes 主节点管理。节点是 Kubernetes 的工作蜂,肩负着所有繁重的工作。过去,它们被称为仆从。如果你读过一些旧的文档或文章,不要感到困惑。仆从就是节点。

主节点

主节点是 Kubernetes 的控制平面。它由多个组件组成,如 API 服务器、调度程序和控制器管理器。主节点负责全局的集群级别的 pod 调度和事件处理。通常,所有主节点组件都设置在单个主机上。在考虑高可用性场景或非常大的集群时,您会希望有主节点冗余。我将在第四章中详细讨论高可用性集群,高可用性和可靠性

Pod

Pod 是 Kubernetes 中的工作单位。每个 pod 包含一个或多个容器。Pod 总是一起调度(即它们总是在同一台机器上运行)。Pod 中的所有容器具有相同的 IP 地址和端口空间;它们可以使用 localhost 或标准的进程间通信进行通信。此外,pod 中的所有容器都可以访问托管 pod 的节点上的共享本地存储。共享存储可以挂载在每个容器上。Pod 是 Kubernetes 的一个重要特性。通过在单个 Docker 容器中运行多个应用程序,例如通过将supervisord作为运行多个进程的主要 Docker 应用程序,可以实现这种做法,但出于以下原因,这种做法经常受到指责:

  • 透明度:使得 pod 内的容器对基础设施可见,使得基础设施能够为这些容器提供服务,如进程管理和资源监控。这为用户提供了许多便利的功能。

  • 解耦软件依赖关系:单个容器可以独立进行版本控制、重建和重新部署。Kubernetes 可能会支持单个容器的实时更新。

  • 易用性:用户不需要运行自己的进程管理器,担心信号和退出代码的传播等问题。

  • 效率:由于基础设施承担了更多的责任,容器可以更轻量化。

Pod 为管理彼此紧密相关且需要在同一主机上合作以完成其目的的容器组提供了一个很好的解决方案。重要的是要记住,pod 被认为是短暂的、可以随意丢弃和替换的实体。任何 pod 存储都会随着 pod 的丢弃而被销毁。每个 pod 都有一个唯一 IDUID),因此在必要时仍然可以区分它们。

标签

标签是用于将一组对象(通常是 pod)分组在一起的键值对。这对于其他一些概念非常重要,比如复制控制器、副本集和操作动态对象组并需要识别组成员的服务。对象和标签之间存在 NxN 的关系。每个对象可能有多个标签,每个标签可能应用于不同的对象。标签有一些设计上的限制。对象上的每个标签必须具有唯一的键。标签键必须遵守严格的语法。它有两部分:前缀和名称。前缀是可选的。如果存在,则它与名称之间用斜杠(/)分隔,并且必须是有效的 DNS 子域。前缀最多可以有 253 个字符。名称是必需的,最多可以有 63 个字符。名称必须以字母数字字符(a-z,A-Z,0-9)开头和结尾,并且只能包含字母数字字符、点、破折号和下划线。值遵循与名称相同的限制。请注意,标签专用于识别对象,而不是附加任意元数据到对象。这就是注释的作用(请参见下一节)。

注释

注释允许您将任意元数据与 Kubernetes 对象关联起来。Kubernetes 只存储注释并提供它们的元数据。与标签不同,它们对允许的字符和大小限制没有严格的限制。

根据我的经验,对于复杂的系统,你总是需要这样的元数据,很高兴 Kubernetes 认识到了这个需求,并且提供了这个功能,这样你就不必自己想出一个单独的元数据存储并将对象映射到它们的元数据。

我们已经涵盖了大多数,如果不是全部,Kubernetes 的概念;我简要提到了一些其他概念。在下一节中,我们将继续探讨 Kubernetes 的架构,深入了解其设计动机、内部和实现,甚至研究源代码。

标签选择器

标签选择器用于根据它们的标签选择对象。基于相等性的选择器指定键名和值。有两个运算符,=(或==)和!=,表示基于值的相等性或不相等性。例如:

role = webserver  

这将选择所有具有该标签键和值的对象。

标签选择器可以有多个要求,用逗号分隔。例如:

role = webserver, application != foo  

基于集合的选择器扩展了功能,并允许基于多个值进行选择:

role in (webserver, backend)

复制控制器和副本集

复制控制器和副本集都管理由标签选择器标识的一组 pod,并确保某个特定数量始终处于运行状态。它们之间的主要区别在于,复制控制器通过名称相等来测试成员资格,而副本集可以使用基于集合的选择。副本集是更好的选择,因为它们是复制控制器的超集。我预计复制控制器在某个时候会被弃用。

Kubernetes 保证您始终会有与复制控制器或副本集中指定的相同数量的运行中的 pod。每当数量因托管节点或 pod 本身的问题而下降时,Kubernetes 都会启动新的实例。请注意,如果您手动启动 pod 并超出指定数量,复制控制器将会终止额外的 pod。

复制控制器曾经是许多工作流程的核心,比如滚动更新和运行一次性作业。随着 Kubernetes 的发展,它引入了对许多这些工作流程的直接支持,使用了专门的对象,比如DeploymentJobDaemonSet。我们稍后会遇到它们。

服务

服务用于向用户或其他服务公开某种功能。它们通常包括一组 pod,通常由标签标识。您可以拥有提供对外部资源或直接在虚拟 IP 级别控制的 pod 的访问权限的服务。原生 Kubernetes 服务通过便捷的端点公开。请注意,服务在第 3 层(TCP/UDP)操作。Kubernetes 1.2 添加了Ingress对象,它提供对 HTTP 对象的访问——稍后会详细介绍。服务通过 DNS 或环境变量之一进行发布或发现。Kubernetes 可以对服务进行负载均衡,但开发人员可以选择在使用外部资源或需要特殊处理的服务的情况下自行管理负载均衡。

与 IP 地址、虚拟 IP 地址和端口空间相关的细节很多。我们将在未来的章节中深入讨论它们。

Pod 上的本地存储是临时的,并随着 Pod 的消失而消失。有时这就是您所需要的,如果目标只是在节点的容器之间交换数据,但有时对数据的存活超过 Pod 或者需要在 Pod 之间共享数据是很重要的。卷的概念支持这种需求。请注意,虽然 Docker 也有卷的概念,但它相当有限(尽管它变得更加强大)。Kubernetes 使用自己独立的卷。Kubernetes 还支持其他容器类型,如 rkt,因此即使原则上也不能依赖 Docker 卷。

有许多卷类型。Kubernetes 目前直接支持许多卷类型,但通过容器存储接口CSI)来扩展 Kubernetes 的现代方法是我稍后会详细讨论的。emptyDir卷类型在每个容器上挂载一个卷,该卷默认由主机上可用的内容支持。如果需要,您可以请求内存介质。当 Pod 因任何原因终止时,此存储将被删除。针对特定云环境、各种网络文件系统甚至 Git 存储库有许多卷类型。一个有趣的卷类型是persistentDiskClaim,它稍微抽象了一些细节,并使用环境中的默认持久存储(通常在云提供商中)。

StatefulSet

Pod 会来来去去,如果您关心它们的数据,那么您可以使用持久存储。这都很好。但有时您可能希望 Kubernetes 管理分布式数据存储,例如 Kubernetes 或 MySQL Galera。这些集群存储将数据分布在唯一标识的节点上。您无法使用常规 Pod 和服务来建模。这就是StatefulSet的作用。如果您还记得,我之前讨论过将服务器视为宠物或牲畜以及牲畜是更好的方式。嗯,StatefulSet处于中间某个位置。StatefulSet确保(类似于复制集)在任何给定时间运行一定数量具有唯一标识的宠物。这些宠物具有以下属性:

  • 可用于 DNS 的稳定主机名

  • 序数索引

  • 与序数和主机名相关联的稳定存储

StatefulSet可以帮助进行对等发现,以及添加或删除宠物。

秘密

Secrets 是包含敏感信息(如凭据和令牌)的小对象。它们存储在etcd中,可以被 Kubernetes API 服务器访问,并且可以被挂载为文件到需要访问它们的 pod 中(使用专用的秘密卷,这些卷依附在常规数据卷上)。同一个秘密可以被挂载到多个 pod 中。Kubernetes 本身为其组件创建秘密,您也可以创建自己的秘密。另一种方法是将秘密用作环境变量。请注意,pod 中的秘密始终存储在内存中(在挂载秘密的情况下为tmpfs),以提高安全性。

名称

Kubernetes 中的每个对象都由 UID 和名称标识。名称用于在 API 调用中引用对象。名称应该长达 253 个字符,并使用小写字母数字字符、破折号(-)和点(.)。如果删除一个对象,您可以创建另一个具有与已删除对象相同名称的对象,但 UID 必须在集群的生命周期内是唯一的。UID 是由 Kubernetes 生成的,所以您不必担心这个问题。

命名空间

命名空间是一个虚拟集群。您可以拥有一个包含多个由命名空间隔离的虚拟集群的单个物理集群。每个虚拟集群与其他虚拟集群完全隔离,它们只能通过公共接口进行通信。请注意,node对象和持久卷不属于命名空间。Kubernetes 可能会调度来自不同命名空间的 pod 在同一节点上运行。同样,来自不同命名空间的 pod 可以使用相同的持久存储。

在使用命名空间时,您必须考虑网络策略和资源配额,以确保对物理集群资源的适当访问和分配。

深入了解 Kubernetes 架构

Kubernetes 有非常雄心勃勃的目标。它旨在管理和简化在各种环境和云提供商中的分布式系统的编排、部署和管理。它提供了许多能力和服务,应该能够在所有这些多样性中工作,同时演变并保持足够简单,以便普通人使用。这是一个艰巨的任务。Kubernetes 通过遵循清晰的高级设计,并使用深思熟虑的架构来实现这一目标,促进可扩展性和可插拔性。Kubernetes 的许多部分仍然是硬编码或环境感知的,但趋势是将它们重构为插件,并保持核心的通用性和抽象性。在本节中,我们将像剥洋葱一样剥开 Kubernetes,从各种分布式系统设计模式开始,以及 Kubernetes 如何支持它们,然后介绍 Kubernetes 的机制,包括其一套 API,然后看一下组成 Kubernetes 的实际组件。最后,我们将快速浏览源代码树,以更好地了解 Kubernetes 本身的结构。

在本节结束时,您将对 Kubernetes 的架构和实现有扎实的了解,以及为什么会做出某些设计决策。

分布式系统设计模式

所有快乐(工作)的分布式系统都是相似的,借用托尔斯泰在《安娜·卡列尼娜》中的话。这意味着,为了正常运行,所有设计良好的分布式系统都必须遵循一些最佳实践和原则。Kubernetes 不只是想成为一个管理系统。它希望支持和促进这些最佳实践,并为开发人员和管理员提供高级服务。让我们来看看其中一些设计模式。

边车模式

边车模式是指在一个 pod 中除了主应用容器之外,还有另一个容器。应用容器对边车容器一无所知,只是按照自己的业务进行操作。一个很好的例子是中央日志代理。你的主容器可以直接记录到stdout,但是边车容器会将所有日志发送到一个中央日志服务,这样它们就会与整个系统的日志聚合在一起。使用边车容器与将中央日志添加到主应用容器中相比的好处是巨大的。首先,应用不再被中央日志所拖累,这可能会很麻烦。如果你想升级或更改你的中央日志策略,或者切换到一个全新的提供者,你只需要更新边车容器并部署它。你的应用容器都不会改变,所以你不会意外地破坏它们。

大使模式

大使模式是指将远程服务表示为本地服务,并可能强制执行某种策略。大使模式的一个很好的例子是,如果你有一个 Redis 集群,有一个主节点用于写入,还有许多副本用于读取。一个本地的大使容器可以作为代理,将 Redis 暴露给主应用容器在本地主机上。主应用容器只需连接到localhost:6379(Redis 的默认端口),但它连接到在同一个 pod 中运行的大使,大使会过滤请求,将写请求发送到真正的 Redis 主节点,将读请求随机发送到其中一个读取副本。就像我们在边车模式中看到的一样,主应用并不知道发生了什么。这在对真实的本地 Redis 进行测试时会有很大帮助。此外,如果 Redis 集群配置发生变化,只需要修改大使;主应用仍然毫不知情。

适配器模式

适配器模式是关于标准化主应用程序容器的输出。考虑一个逐步推出的服务的情况:它可能生成的报告格式与以前的版本不符。消费该输出的其他服务和应用程序尚未升级。适配器容器可以部署在与新应用程序容器相同的 pod 中,并可以修改其输出以匹配旧版本,直到所有消费者都已升级。适配器容器与主应用程序容器共享文件系统,因此它可以监视本地文件系统,每当新应用程序写入内容时,它立即进行适应。

多节点模式

Kubernetes 直接支持单节点模式,通过 pod。多节点模式,如领导者选举、工作队列和分散收集,不受直接支持,但通过使用标准接口组合 pod 来实现它们是一种可行的方法。

Kubernetes API

如果你想了解一个系统的能力和提供的功能,你必须非常关注它的 API。这些 API 为用户提供了对系统可以做什么的全面视图。Kubernetes 通过 API 组向不同目的和受众暴露了几套 REST API。一些 API 主要由工具使用,一些可以直接由开发人员使用。关于 API 的一个重要事实是它们在不断发展。Kubernetes 开发人员通过尝试扩展它(通过向现有对象添加新对象和新字段)并避免重命名或删除现有对象和字段来使其可管理。此外,所有 API 端点都是有版本的,并且通常也有 alpha 或 beta 标记。例如:

/api/v1
/api/v2alpha1  

您可以通过kubectl cli、客户端库或直接通过 REST API 调用访问 API。我们将在后面的章节中探讨详细的身份验证和授权机制。如果您有适当的权限,您可以列出、查看、创建、更新和删除各种 Kubernetes 对象。在这一点上,让我们来一窥 API 的表面积。探索这些 API 的最佳方式是通过 API 组。一些 API 组是默认启用的。其他组可以通过标志启用/禁用。例如,要禁用批处理 V1 组并启用批处理 V2 alpha 组,您可以在运行 API 服务器时设置--runtime-config标志如下:

--runtime-config=batch/v1=false,batch/v2alpha=true 

默认情况下启用以下资源,除了核心资源:

  • DaemonSets

  • Deployments

  • HorizontalPodAutoscalers

  • Ingress

  • Jobs

  • ReplicaSets

发现和负载平衡

默认情况下,工作负载只能在集群内访问,必须使用LoadBalancerNodePort服务将其外部暴露。在开发过程中,可以通过使用kubectl proxy命令通过 API 主机访问内部可访问的工作负载:

  • Endpoints: 核心

  • Ingress: 扩展

  • Service: 核心

资源类别

除了 API 组之外,可用 API 的另一个有用的分类是功能。Kubernetes API 非常庞大,将其分成不同类别在你试图找到自己的路时非常有帮助。Kubernetes 定义了以下资源类别:

  • 工作负载:您用来管理和运行集群上的容器的对象。

  • 发现和负载平衡:您用来将工作负载暴露给外部可访问、负载平衡服务的对象。

  • 配置和存储:您用来初始化和配置应用程序,并持久化容器外的数据的对象。

  • 集群:定义集群本身配置的对象;这些通常只由集群操作员使用。

  • 元数据:您用来配置集群内其他资源行为的对象,例如用于扩展工作负载的HorizontalPodAutoscaler

在以下小节中,我将列出属于每个组的资源,以及它们所属的 API 组。我不会在这里指定版本,因为 API 从 alpha 迅速转移到 beta 到一般可用性GA),然后从 V1 到 V2,依此类推。

工作负载 API

工作负载 API 包含以下资源:

  • Container: 核心

  • CronJob: 批处理

  • DaemonSet: 应用

  • Deployment: 应用

  • Job: 批处理

  • Pod: 核心

  • ReplicaSet: 应用

  • ReplicationController: 核心

  • StatefulSet: 应用

容器是由控制器使用 pod 创建的。Pod 运行容器并提供环境依赖项,如共享或持久存储卷,以及注入到容器中的配置或秘密数据。

以下是最常见操作之一的详细描述,它以 REST API 的形式获取所有 pod 的列表:

GET /api/v1/pods 

它接受各种查询参数(全部可选):

  • pretty: 如果为 true,则输出将被漂亮地打印

  • labelSelector: 限制结果的选择器表达式

  • watch: 如果为 true,则会监视更改并返回事件流

  • resourceVersion: 仅返回在该版本之后发生的事件

  • timeoutSeconds: 列表或监视操作的超时

配置和存储

Kubernetes 的动态配置而无需重新部署是在您的 Kubernetes 集群上运行复杂分布式应用的基石:

  • ConfigMap: 核心

  • Secret: 核心

  • PersistentVolumeClaim: 核心

  • StorageClass: 存储

  • VolumeAttachment: 存储

元数据

元数据资源通常嵌入为它们配置的资源的子资源。例如,限制范围将成为 pod 配置的一部分。大多数情况下,您不会直接与这些对象交互。有许多元数据资源。您可以在kubernetes.io/docs/reference/generated/kubernetes-api/v1.10/#-strong-metadata-strong-找到完整的列表。

集群

集群类别中的资源是为集群操作员设计的,而不是开发人员。这个类别中也有许多资源。以下是一些最重要的资源:

  • Namespace: 核心

  • Node: 核心

  • PersistentVolume: 核心

  • ResourceQuota:核心

  • ClusterRole: Rbac

  • NetworkPolicy:网络

Kubernetes 组件

Kubernetes 集群有几个主要组件,用于控制集群,以及在每个集群节点上运行的节点组件。让我们了解所有这些组件以及它们如何一起工作。

主要组件

主要组件通常在一个节点上运行,但在高可用性或非常大的集群中,它们可能分布在多个节点上。

API 服务器

Kube API 服务器公开了 Kubernetes REST API。由于它是无状态的,并且将所有数据存储在etcd集群中,因此可以轻松地进行水平扩展。API 服务器是 Kubernetes 控制平面的具体体现。

Etcd

Etcd 是一个高度可靠的分布式数据存储。Kubernetes 使用它来存储整个集群状态。在一个小型的瞬态集群中,一个etcd的实例可以在与所有其他主要组件相同的节点上运行,但对于更大的集群,通常会有一个三节点甚至五节点的etcd集群,以实现冗余和高可用性。

Kube 控制器管理器

Kube 控制器管理器是各种管理器的集合,汇总成一个二进制文件。它包含复制控制器、Pod 控制器、服务控制器、端点控制器等。所有这些管理器通过 API 监视集群的状态,它们的工作是将集群引导到期望的状态。

云控制器管理器

在云中运行时,Kubernetes 允许云提供商集成其平台,以管理节点、路由、服务和卷。云提供商代码与 Kubernetes 代码进行交互。它替换了 Kube 控制器管理器的一些功能。在使用云控制器管理器运行 Kubernetes 时,必须将 Kube 控制器管理器标志--cloud-provider设置为external。这将禁用云控制器管理器正在接管的控制循环。云控制器管理器是在 Kubernetes 1.6 中引入的,并且已被多个云提供商使用。

关于 Go 的一个快速说明,以帮助您解析代码:方法名首先出现,然后是方法的参数在括号中。每个参数都是一对,由名称和类型组成。最后,指定返回值。Go 允许多个返回类型。通常会返回一个error对象,除了实际结果。如果一切正常,error对象将为 nil。

这是cloudprovider包的主要接口:

package cloudprovider 

import ( 
    "errors" 
    "fmt" 
    "strings" 

    "k8s.io/api/core/v1" 
    "k8s.io/apimachinery/pkg/types" 
    "k8s.io/client-go/informers" 
    "k8s.io/kubernetes/pkg/controller" 
) 

// Interface is an abstract, pluggable interface for cloud providers. 
type Interface interface { 
    Initialize(clientBuilder controller.ControllerClientBuilder) 
    LoadBalancer() (LoadBalancer, bool) 
    Instances() (Instances, bool) 
    Zones() (Zones, bool) 
    Clusters() (Clusters, bool) 
    Routes() (Routes, bool) 
    ProviderName() string 
    HasClusterID() bool 
} 

大多数方法返回具有自己方法的其他接口。例如,这是LoadBalancer接口:

type LoadBalancer interface {
    GetLoadBalancer(clusterName string, 
                                 service *v1.Service) (status *v1.LoadBalancerStatus, 
                                                                   exists bool, 
                                                                   err error)
    EnsureLoadBalancer(clusterName string, 
                                       service *v1.Service, 
                                       nodes []*v1.Node) (*v1.LoadBalancerStatus, error)
    UpdateLoadBalancer(clusterName string, service *v1.Service, nodes []*v1.Node) error
    EnsureLoadBalancerDeleted(clusterName string, service *v1.Service) error
}

Kube-scheduler

kube-scheduler负责将 Pod 调度到节点。这是一个非常复杂的任务,因为它需要考虑多个相互作用的因素,例如:

  • 资源要求

  • 服务要求

  • 硬件/软件策略约束

  • 节点亲和性和反亲和性规范

  • Pod 亲和性和反亲和性规范

  • 污点和容忍

  • 数据本地性

  • 截止日期

如果您需要一些默认 Kube 调度程序未涵盖的特殊调度逻辑,可以用自己的自定义调度程序替换它。您还可以将自定义调度程序与默认调度程序并行运行,并且让自定义调度程序仅调度一部分 Pod。

DNS

自 Kubernetes 1.3 以来,DNS 服务已成为标准 Kubernetes 集群的一部分。它被安排为一个常规的 pod。每个服务(除了无头服务)都会收到一个 DNS 名称。Pods 也可以收到 DNS 名称。这对于自动发现非常有用。

节点组件

集群中的节点需要一些组件来与集群主组件交互,并接收要执行的工作负载并更新其状态。

代理

Kube 代理在每个节点上进行低级别的网络维护。它在本地反映 Kubernetes 服务,并可以进行 TCP 和 UDP 转发。它通过环境变量或 DNS 找到集群 IP。

Kubelet

kubelet 是节点上的 Kubernetes 代表。它负责与主组件通信并管理正在运行的 pod。这包括以下操作:

  • 从 API 服务器下载 pod 的秘密

  • 挂载卷

  • 运行 pod 的容器(通过 CRI 或 rkt)

  • 报告节点和每个 pod 的状态

  • 运行容器的活动探测

在本节中,我们深入研究了 Kubernetes 的内部,探索了其架构(从非常高层次的角度),并支持了设计模式,通过其 API 和用于控制和管理集群的组件。在下一节中,我们将快速浏览 Kubernetes 支持的各种运行时。

Kubernetes 运行时

Kubernetes 最初只支持 Docker 作为容器运行时引擎。但现在不再是这样。Kubernetes 现在支持几种不同的运行时:

  • Docker(通过 CRI shim)

  • Rkt(直接集成将被 rktlet 替换)

  • Cri-o

  • Frakti(在 hypervisor 上的 Kubernetes,以前是 Hypernetes)

  • Rktlet(rkt 的 CRI 实现)

  • cri-containerd

一个主要的设计政策是 Kubernetes 本身应该完全与特定的运行时解耦。容器运行时接口CRI)使这成为可能。

在本节中,您将更仔细地了解 CRI,并了解各个运行时引擎。在本节结束时,您将能够就哪种运行时引擎适合您的用例做出明智的决定,并在何种情况下可以切换或甚至在同一系统中组合多个运行时。

容器运行时接口(CRI)

CRI 是一个 gRPC API,包含容器运行时与节点上的 kubelet 集成的规范/要求和库。在 Kubernetes 1.7 中,Kubernetes 中的内部 Docker 集成被 CRI-based 集成所取代。这是一件大事。它为利用容器领域的进步打开了多种实现的大门。Kubelet 不需要直接与多个运行时进行接口。相反,它可以与任何符合 CRI 的容器运行时进行通信。以下图表说明了流程:

有两个 gRPC 服务接口——ImageServiceRuntimeService——CRI 容器运行时(或 shims)必须实现。ImageService 负责管理镜像。以下是 gRPC/protobuf 接口(这不是 Go):

service ImageService { 
    rpc ListImages(ListImagesRequest) returns (ListImagesResponse) {} 
    rpc ImageStatus(ImageStatusRequest) returns (ImageStatusResponse) {} 
    rpc PullImage(PullImageRequest) returns (PullImageResponse) {} 
    rpc RemoveImage(RemoveImageRequest) returns (RemoveImageResponse) {} 
    rpc ImageFsInfo(ImageFsInfoRequest) returns (ImageFsInfoResponse) {} 
} 

RuntimeService 负责管理 pod 和容器。以下是 gRPC/profobug 接口:

service RuntimeService { 
    rpc Version(VersionRequest) returns (VersionResponse) {} 
    rpc RunPodSandbox(RunPodSandboxRequest) returns (RunPodSandboxResponse) {} 
    rpc StopPodSandbox(StopPodSandboxRequest) returns (StopPodSandboxResponse) {} 
    rpc RemovePodSandbox(RemovePodSandboxRequest) returns (RemovePodSandboxResponse) {} 
    rpc PodSandboxStatus(PodSandboxStatusRequest) returns (PodSandboxStatusResponse) {} 
    rpc ListPodSandbox(ListPodSandboxRequest) returns (ListPodSandboxResponse) {} 
    rpc CreateContainer(CreateContainerRequest) returns (CreateContainerResponse) {} 
    rpc StartContainer(StartContainerRequest) returns (StartContainerResponse) {} 
    rpc StopContainer(StopContainerRequest) returns (StopContainerResponse) {} 
    rpc RemoveContainer(RemoveContainerRequest) returns (RemoveContainerResponse) {} 
    rpc ListContainers(ListContainersRequest) returns (ListContainersResponse) {} 
    rpc ContainerStatus(ContainerStatusRequest) returns (ContainerStatusResponse) {} 
    rpc UpdateContainerResources(UpdateContainerResourcesRequest) returns (UpdateContainerResourcesResponse) {} 
    rpc ExecSync(ExecSyncRequest) returns (ExecSyncResponse) {} 
    rpc Exec(ExecRequest) returns (ExecResponse) {} 
    rpc Attach(AttachRequest) returns (AttachResponse) {} 
    rpc PortForward(PortForwardRequest) returns (PortForwardResponse) {} 
    rpc ContainerStats(ContainerStatsRequest) returns (ContainerStatsResponse) {} 
    rpc ListContainerStats(ListContainerStatsRequest) returns (ListContainerStatsResponse) {} 
    rpc UpdateRuntimeConfig(UpdateRuntimeConfigRequest) returns (UpdateRuntimeConfigResponse) {} 
    rpc Status(StatusRequest) returns (StatusResponse) {} 
} 

用作参数和返回类型的数据类型称为消息,并且也作为 API 的一部分进行定义。以下是其中之一:

message CreateContainerRequest { 
    string pod_sandbox_id = 1; 
    ContainerConfig config = 2; 
    PodSandboxConfig sandbox_config = 3; 
} 

正如您所看到的,消息可以嵌套在彼此之内。CreateContainerRequest 消息有一个字符串字段和另外两个字段,它们本身也是消息:ContainerConfigPodSandboxConfig

现在您已经在代码级别熟悉了 Kubernetes 运行时引擎,让我们简要地看一下各个运行时引擎。

Docker

当然,Docker 是容器的大象级存在。Kubernetes 最初设计仅用于管理 Docker 容器。多运行时功能首次在 Kubernetes 1.3 中引入,而 CRI 则在 Kubernetes 1.5 中引入。在那之前,Kubernetes 只能管理 Docker 容器。

如果您正在阅读本书,我假设您非常熟悉 Docker 及其带来的功能。Docker 受到了巨大的欢迎和增长,但也受到了很多批评。批评者经常提到以下关注点:

  • 安全性

  • 难以设置多容器应用程序(特别是网络)

  • 开发、监控和日志记录

  • Docker 容器运行一个命令的限制

  • 发布不完善的功能太快

Docker 意识到了这些批评,并解决了其中一些问题。特别是,Docker 已经投资于其 Docker Swarm 产品。Docker Swarm 是一个与 Kubernetes 竞争的 Docker 本地编排解决方案。它比 Kubernetes 更容易使用,但不如 Kubernetes 强大或成熟。

自 Docker 1.12 以来,swarm 模式已经内置在 Docker 守护程序中,这让一些人感到不满,因为它的臃肿和范围扩大。这反过来使更多的人转向 CoreOS rkt 作为替代解决方案。

自 Docker 1.11 发布于 2016 年 4 月以来,Docker 已经改变了运行容器的方式。运行时现在使用containerdrunC来在容器中运行Open Container Initiative(OCI)图像:

Rkt

Rkt 是来自 CoreOS 的容器管理器(CoreOS Linux 发行版、etcd、flannel 等的开发者)。Rkt 运行时以其简单性和对安全性和隔离性的强调而自豪。它没有像 Docker 引擎那样的守护程序,而是依赖于操作系统的 init 系统,比如systemd,来启动 rkt 可执行文件。Rkt 可以下载图像(包括应用容器(appc)图像和 OCI 图像),验证它们,并在容器中运行。它的架构要简单得多。

应用容器

CoreOS 在 2014 年 12 月启动了一个名为 appc 的标准化工作。这包括标准图像格式(ACI)、运行时、签名和发现。几个月后,Docker 开始了自己的标准化工作,推出了 OCI。目前看来,这些努力将会融合。这是一件好事,因为工具、图像和运行时将能够自由地互操作。但我们还没有达到这一点。

Cri-O

Cri-o 是一个 Kubernetes 孵化器项目。它旨在为 Kubernetes 和符合 OCI 标准的容器运行时(如 Docker)之间提供集成路径。其想法是 Cri-O 将提供以下功能:

  • 支持多种图像格式,包括现有的 Docker 图像格式

  • 支持多种下载图像的方式,包括信任和图像验证

  • 容器镜像管理(管理镜像层、叠加文件系统等)

  • 容器进程生命周期管理

  • 满足 CRI 所需的监控和日志记录

  • 根据 CRI 所需的资源隔离

然后任何符合 OCI 标准的容器运行时都可以被插入,并将与 Kubernetes 集成。

Rktnetes

Rktnetes 是 Kubernetes 加上 rkt 作为运行时引擎。Kubernetes 仍在抽象化运行时引擎的过程中。Rktnetes 实际上并不是一个单独的产品。从外部来看,只需要在每个节点上运行 Kubelet 并加上几个命令行开关。

rkt 准备好投入生产使用了吗?

我对 rkt 没有太多的实际经验。然而,它被 Tectonic 使用——这是基于 CoreOS 的商业 Kubernetes 发行版。如果你运行不同类型的集群,我建议你等到 rkt 通过 CRI/rktlet 与 Kubernetes 集成。在使用 rkt 与 Kubernetes 相比,有一些已知的问题需要注意,例如,缺少的卷不会自动创建,Kubectl 的 attach 和 get logs 不起作用,以及init容器不受支持,还有其他问题。

超级容器

超级容器是另一个选择。超级容器具有轻量级虚拟机(自己的客户机内核),并在裸金属上运行。它不依赖于 Linux cgroups 进行隔离,而是依赖于一个虚拟化程序。与难以设置的标准裸金属集群和在重量级虚拟机上部署容器的公共云相比,这种方法呈现出有趣的混合。

Stackube

Stackube(之前称为 Hypernetes)是一个多租户分发,它使用超级容器以及一些 OpenStack 组件进行身份验证、持久存储和网络。由于容器不共享主机内核,因此可以安全地在同一物理主机上运行不同租户的容器。当然,Stackube 使用 Frakti 作为其容器运行时。

在本节中,我们已经涵盖了 Kubernetes 支持的各种运行时引擎,以及标准化和融合的趋势。在下一节中,我们将退一步,看看整体情况,以及 Kubernetes 如何适应 CI/CD 流水线。

持续集成和部署

Kubernetes 是运行基于微服务的应用程序的绝佳平台。但归根结底,它只是一个实现细节。用户,甚至大多数开发人员,可能不知道系统是部署在 Kubernetes 上的。但 Kubernetes 可以改变游戏规则,使以前难以实现的事情成为可能。

在本节中,我们将探讨 CI/CD 流水线以及 Kubernetes 带来了什么。在本节结束时,您将能够设计利用 Kubernetes 属性的 CI/CD 流水线,例如易扩展性和开发-生产一致性,以提高您日常开发和部署的生产力和稳健性。

什么是 CI/CD 流水线?

CI/CD 流水线是由开发人员或运营人员实施的一组步骤,用于修改系统的代码、数据或配置,对其进行测试,并将其部署到生产环境。一些流水线是完全自动化的,而一些是半自动化的,需要人工检查。在大型组织中,可能会有测试和暂存环境,更改会自动部署到这些环境,但发布到生产环境需要手动干预。下图描述了一个典型的流水线。

值得一提的是,开发人员可以完全与生产基础设施隔离开来。他们的接口只是一个 Git 工作流程——Deis 工作流程(在 Kubernetes 上的 PaaS;类似于 Heroku)就是一个很好的例子。

为 Kubernetes 设计 CI/CD 流水线

当你的部署目标是一个 Kubernetes 集群时,你应该重新思考一些传统的做法。首先,打包是不同的。你需要为你的容器烘焙镜像。使用智能标签可以轻松且即时地回滚代码更改。这给了你很多信心,即使一个糟糕的更改通过了测试网,你也能立即回滚到上一个版本。但你要小心。模式更改和数据迁移不能自动回滚。

Kubernetes 的另一个独特能力是开发人员可以在本地运行整个集群。当你设计你的集群时,这需要一些工作,但由于构成系统的微服务在容器中运行,并且这些容器通过 API 进行交互,这是可能和实际可行的。与往常一样,如果你的系统非常依赖数据,你需要为此做出调整,并提供数据快照和合成数据供开发人员使用。

摘要

在本章中,我们涵盖了很多内容,你了解了 Kubernetes 的设计和架构。Kubernetes 是一个用于运行容器化微服务应用程序的编排平台。Kubernetes 集群有主节点和工作节点。容器在 pod 中运行。每个 pod 在单个物理或虚拟机上运行。Kubernetes 直接支持许多概念,如服务、标签和持久存储。您可以在 Kubernetes 上实现各种分布式系统设计模式。容器运行时只需实现 CRI。支持 Docker、rkt、Hyper 容器等等。

在第二章中,创建 Kubernetes 集群,我们将探讨创建 Kubernetes 集群的各种方式,讨论何时使用不同的选项,并构建一个多节点集群。

第二章:创建 Kubernetes 集群

在上一章中,我们了解了 Kubernetes 的全部内容,它的设计方式,支持的概念,如何使用其运行时引擎,以及它如何适用于 CI/CD 流水线。

创建 Kubernetes 集群是一项非常重要的任务。有许多选择和工具可供选择,需要考虑许多因素。在本章中,我们将动手构建一些 Kubernetes 集群。我们还将讨论和评估诸如 Minikube、kubeadm、kube-spray、bootkube 和 stackube 等工具。我们还将研究部署环境,如本地、云和裸机。我们将涵盖的主题如下:

  • 使用 Minikube 创建单节点集群

  • 使用 kubeadm 创建多节点集群

  • 在云中创建集群

  • 从头开始创建裸机集群

  • 审查其他创建 Kubernetes 集群的选项

在本章结束时,您将对创建 Kubernetes 集群的各种选项有扎实的了解,并了解支持创建 Kubernetes 集群的最佳工具;您还将构建一些集群,包括单节点和多节点。

使用 Minikube 快速创建单节点集群

在本节中,我们将在 Windows 上创建一个单节点集群。我们之所以使用 Windows,是因为 Minikube 和单节点集群对于本地开发者机器非常有用。虽然 Kubernetes 通常在生产环境中部署在 Linux 上,但许多开发人员使用 Windows PC 或 Mac。也就是说,如果您确实想在 Linux 上安装 Minikube,也没有太多区别:

准备工作

在创建集群之前,有一些先决条件需要安装。这些包括 VirtualBox,用于 Kubernetes 的kubectl命令行界面,当然还有 Minikube 本身。以下是撰写时的最新版本列表:

在 Windows 上

安装 VirtualBox 并确保 kubectl 和 Minikube 在你的路径上。我个人只是把我使用的所有命令行程序都放在 c:\windows 中。你可能更喜欢另一种方法。我使用优秀的 ConEMU 来管理多个控制台、终端和 SSH 会话。它可以与 cmd.exe、PowerShell、PuTTY、Cygwin、msys 和 Git-Bash 一起使用。在 Windows 上没有比这更好的了。

在 Windows 10 Pro 中,你可以选择使用 Hyper-V hypervisor。这在技术上是比 VirtualBox 更好的解决方案,但它需要 Windows 的专业版,并且完全是 Windows 特有的。当使用 VirtualBox 时,这些说明是通用的,并且很容易适应其他版本的 Windows,或者其他操作系统。如果你已经启用了 Hyper-V,你必须禁用它,因为 VirtualBox 无法与 Hyper-V 共存。

我建议在管理员模式下使用 PowerShell。你可以将以下别名和函数添加到你的 PowerShell 配置文件中:

Set-Alias -Name k -Value kubectl 
function mk  
{  
minikube-windows-amd64 ` 
--show-libmachine-logs ` 
--alsologtostderr      ` 
@args 
} 

在 macOS 上

你可以在你的 .bashrc 文件中添加别名(类似于 Windows 上的 PowerShell 别名和函数):

alias k='kubectl' 
alias mk='/usr/local/bin/minikube' 

现在我可以使用 kmk 并且输入更少。mk 函数中的 Minikube 标志提供更好的日志记录方式,并将输出直接输出到控制台,以及文件中(类似于 tee)。

输入 mk version 来验证 Minikube 是否正确安装并运行:

> mk version 

minikube version: v0.26.0 

输入 k version 来验证 kubectl 是否正确安装并运行:

> k version
Client Version: version.Info{Major:"1", Minor:"9", GitVersion:"v1.9.0", GitCommit:"925c127ec6b946659ad0fd596fa959be43f0cc05", GitTreeState:"clean", BuildDate:"2017-12-16T03:15:38Z", GoVersion:"go1.9.2", Compiler:"gc", Platform:"darwin/amd64"}
Unable to connect to the server: dial tcp 192.168.99.100:8443: getsockopt: operation timed out

不要担心最后一行的错误。没有运行的集群,所以 kubectl 无法连接到任何东西。这是预期的。

你可以探索 Minikube 和 kubectl 的可用命令和标志。我不会逐个介绍每一个,只介绍我使用的命令。

创建集群

Minikube 工具支持多个版本的 Kubernetes。在撰写本文时,支持的版本列表如下:

> mk get-k8s-versions 
The following Kubernetes versions are available when using the localkube bootstrapper:  
- v1.10.0
- v1.9.4
- v1.9.0 
- v1.8.0 
- v1.7.5 
- v1.7.4 
- v1.7.3 
- v1.7.2 
- v1.7.0 
- v1.7.0-rc.1 
- v1.7.0-alpha.2 
- v1.6.4 
- v1.6.3 
- v1.6.0 
- v1.6.0-rc.1 
- v1.6.0-beta.4 
- v1.6.0-beta.3 
- v1.6.0-beta.2 
- v1.6.0-alpha.1 
- v1.6.0-alpha.0 
- v1.5.3 
- v1.5.2 
- v1.5.1 
- v1.4.5 
- v1.4.3 
- v1.4.2 
- v1.4.1 
- v1.4.0 
- v1.3.7 
- v1.3.6 
- v1.3.5 
- v1.3.4 
- v1.3.3 
- v1.3.0 

我将选择 1.10.0,最新的稳定版本。让我们使用 start 命令并指定 v1.10.0 作为版本来创建集群。

这可能需要一段时间,因为 Minikube 可能需要下载镜像,然后设置本地集群。让它运行就好了。这是预期的输出(在 Mac 上):

> mk start --kubernetes-version="v1.10.0" 
Starting local Kubernetes v1.10.0 cluster... 
Starting VM... 
Getting VM IP address... 
Moving files into cluster... 
Finished Downloading kubeadm v1.10.0 **Finished Downloading kubelet v1.10.0** Setting up certs... 
Connecting to cluster... 
Setting up kubeconfig... 
Starting cluster components... 
Kubectl is now configured to use the cluster. 
Loading cached images from config file. 

让我们通过跟踪输出来回顾 Minikube 的操作。当从头开始创建集群时,你需要做很多这样的操作:

  1. 启动 VirtualBox 虚拟机

  2. 为本地机器和虚拟机创建证书

  3. 下载镜像

  4. 在本地机器和虚拟机之间设置网络

  5. 在虚拟机上运行本地 Kubernetes 集群

  6. 配置集群

  7. 启动所有 Kubernetes 控制平面组件

  8. 配置 kubectl 以与集群通信

故障排除

如果在过程中出现问题,请尝试遵循错误消息。您可以添加--alsologtostderr标志以从控制台获取详细的错误信息。Minikube 所做的一切都整齐地组织在~/.minikube下。以下是目录结构:

> tree ~/.minikube -L 2
/Users/gigi.sayfan/.minikube
├── addons
├── apiserver.crt
├── apiserver.key
├── ca.crt
├── ca.key
├── ca.pem
├── cache
│ ├── images
│ ├── iso
│ └── localkube
├── cert.pem
├── certs
│ ├── ca-key.pem
│ ├── ca.pem
│ ├── cert.pem
│ └── key.pem
├── client.crt
├── client.key
├── config
│ └── config.json
├── files
├── key.pem
├── last_update_check
├── logs
├── machines
│ ├── minikube
│ ├── server-key.pem
│ └── server.pem
├── profiles
│ └── minikube
├── proxy-client-ca.crt
├── proxy-client-ca.key
├── proxy-client.crt
└── proxy-client.key

13 directories, 21 files

检查集群

既然我们已经有一个运行中的集群,让我们来看看里面。

首先,让我们ssh进入虚拟机:

> mk ssh
 _ _
 _ _ ( ) ( )
 ___ ___ (_) ___ (_)| |/') _ _ | |_ __
/' _ ` _ `\| |/' _ `\| || , < ( ) ( )| '_`\ /'__`\
| ( ) ( ) || || ( ) || || |\`\ | (_) || |_) )( ___/
(_) (_) (_)(_)(_) (_)(_)(_) (_)`\___/'(_,__/'`\____)

$ uname -a

Linux minikube 4.9.64 #1 SMP Fri Mar 30 21:27:22 UTC 2018 x86_64 GNU/Linux$ 

太棒了!成功了。奇怪的符号是minikube的 ASCII 艺术。现在,让我们开始使用kubectl,因为它是 Kubernetes 的瑞士军刀,并且对所有集群(包括联合集群)都很有用。

我们将在我们的旅程中涵盖许多kubectl命令。首先,让我们使用cluster-info检查集群状态:

> k cluster-info    

Kubernetes 主节点正在运行在https://192.168.99.101:8443

KubeDNS 正在运行在https://192.168.99.1010:8443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

要进一步调试和诊断集群问题,请使用kubectl cluster-info dump。您可以看到主节点正在正常运行。要以 JSON 类型查看集群中所有对象的更详细视图,请使用k cluster-info dump。输出可能有点令人生畏,因此让我们使用更具体的命令来探索集群。

让我们使用get nodes检查集群中的节点:

> k get nodes
NAME       STATUS    ROLES     AGE       VERSION

NAME       STATUS    ROLES     AGE       VERSION
minikube   Ready      master   15m       v1.10.0  

所以,我们有一个名为minikube的节点。要获取有关它的大量信息,请输入k describe node minikube。输出是冗长的;我会让您自己尝试。

做工作

我们有一个漂亮的空集群正在运行(好吧,不完全是空的,因为 DNS 服务和仪表板作为kube-system命名空间中的 pod 运行)。现在是时候运行一些 pod 了。让我们以echo服务器为例:

k run echo --image=gcr.io/google_containers/echoserver:1.8 --port=8080 deployment "echo" created  

Kubernetes 创建了一个部署,我们有一个正在运行的 pod。注意echo前缀:

> k get pods  
NAME                    READY    STATUS    RESTARTS    AGE echo-69f7cfb5bb-wqgkh    1/1     Running     0          18s  

要将我们的 pod 公开为服务,请输入以下内容:

> k expose deployment echo --type=NodePort service "echo" exposed  

将服务公开为NodePort类型意味着它对主机公开端口,但它不是我们在其上运行 pod 的8080端口。端口在集群中映射。要访问服务,我们需要集群 IP 和公开的端口:

> mk ip
192.168.99.101
> k get service echo --output='jsonpath="{.spec.ports[0].nodePort}"'
30388  

现在我们可以访问echo服务,它会返回大量信息:

> curl http://192.168.99.101:30388/hi  

恭喜!您刚刚创建了一个本地 Kubernetes 集群并部署了一个服务。

使用仪表板检查集群

Kubernetes 有一个非常好的 web 界面,当然是部署为一个 pod 中的服务。仪表板设计得很好,提供了对集群的高级概述,还可以深入到单个资源,查看日志,编辑资源文件等。当你想要手动检查你的集群时,它是一个完美的武器。要启动它,输入minikube dashboard

Minikube 将打开一个带有仪表板 UI 的浏览器窗口。请注意,在 Windows 上,Microsoft Edge 无法显示仪表板。我不得不在不同的浏览器上运行它。

这是工作负载视图,显示部署、副本集、复制控制器和 Pod:

它还可以显示守护进程集、有状态集和作业,但在这个集群中我们没有这些。

在这一部分,我们在 Windows 上创建了一个本地的单节点 Kubernetes 集群,使用kubectl进行了一些探索,部署了一个服务,并尝试了 web UI。在下一部分,我们将继续创建一个多节点集群。

使用 kubeadm 创建一个多节点集群

在这一部分,我将向您介绍kubeadm,这是在所有环境中创建 Kubernetes 集群的推荐工具。它仍在积极开发中,但这是因为它是 Kubernetes 的一部分,并且始终体现最佳实践。为了使其对整个集群可访问,我们将以虚拟机为基础。这一部分是为那些想要亲自部署多节点集群的读者准备的。

设定期望

在踏上这段旅程之前,我想明确指出,这可能不会一帆风顺。kubeadm的任务很艰巨:它必须跟随 Kubernetes 本身的发展,而 Kubernetes 是一个不断变化的目标。因此,它并不总是稳定的。当我写第一版《精通 Kubernetes》时,我不得不深入挖掘并寻找各种解决方法来使其正常工作。猜猜?我在第二版中也不得不做同样的事情。准备好做一些调整并寻求帮助。如果你想要一个更简化的解决方案,我将在后面讨论一些非常好的选择。

准备工作

Kubeadm 在预配置的硬件(物理或虚拟)上运行。在创建 Kubernetes 集群之前,我们需要准备一些虚拟机并安装基本软件,如dockerkubeletkubeadmkubectl(仅在主节点上需要)。

准备一个 vagrant 虚拟机集群

以下 vagrant 文件将创建一个名为n1n2n3n4的四个 VM 的集群。键入vagrant up以启动并运行集群。它基于 Bento/Ubuntu 版本 16.04,而不是 Ubuntu/Xenial,后者存在各种问题:

# -*- mode: ruby -*- 
# vi: set ft=ruby : 
hosts = { 
  "n1" => "192.168.77.10", 
  "n2" => "192.168.77.11", 
  "n3" => "192.168.77.12", 
  "n4" => "192.168.77.13" 
} 
Vagrant.configure("2") do |config| 
  # always use Vagrants insecure key 
  config.ssh.insert_key = false 
  # forward ssh agent to easily ssh into the different machines 
  config.ssh.forward_agent = true 

  check_guest_additions = false 
  functional_vboxsf     = false 

  config.vm.box = "bento/ubuntu-16.04" 
 hosts.each do |name, ip| 
    config.vm.hostname = name 
    config.vm.define name do |machine| 
      machine.vm.network :private_network, ip: ip 
      machine.vm.provider "virtualbox" do |v| 
        v.name = name 
      end 
    end 
  end 
end 

安装所需的软件

我非常喜欢 Ansible 进行配置管理。我在运行 Ubuntu 16.04 的n4 VM 上安装了它。从现在开始,我将使用n4作为我的控制机器,这意味着我们正在在 Linux 环境中操作。我可以直接在我的 Mac 上使用 Ansible,但由于 Ansible 无法在 Windows 上运行,我更喜欢更通用的方法:

> vagrant ssh n4
Welcome to Ubuntu 16.04.3 LTS (GNU/Linux 4.4.0-87-generic x86_64)

* Documentation:  https://help.ubuntu.com
* Management:     https://landscape.canonical.com
* Support:        https://ubuntu.com/advantage

0 packages can be updated.
0 updates are security updates.
   vagrant@vagrant:~$ sudo apt-get -y --fix-missing install python-pip sshpass
vagrant@vagrant:~$ sudo pip install  ansible   

我使用的是 2.5.0 版本。你应该使用最新版本:

vagrant@vagrant:~$ ansible --version
ansible 2.5.0
 config file = None
 configured module search path = [u'/home/vagrant/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
 ansible python module location = /home/vagrant/.local/lib/python2.7/site-packages/ansible
 executable location = /home/vagrant/.local/bin/ansible

 python version = 2.7.12 (default, Dec 4 2017, 14:50:18) [GCC 5.4.0 20160609] 
python version = 2.7.12 (default, Dec 4 2017, 14:50:18) [GCC 5.4.0 20160609]

我安装的sshpass程序将帮助ansible连接到所有带有内置 vagrant 用户的 vagrant VM。这仅对本地基于 VM 的多节点集群重要。

我创建了一个名为ansible的目录,并在其中放置了三个文件:hostsvars.ymlplaybook.yml

主机文件

host文件是清单文件,告诉ansible目录要在哪些主机上操作。这些主机必须可以从控制机器进行 SSH 访问。以下是将安装集群的三个 VM:

[all] 
192.168.77.10 ansible_user=vagrant ansible_ssh_pass=vagrant 
192.168.77.11 ansible_user=vagrant ansible_ssh_pass=vagrant 
192.168.77.12 ansible_user=vagrant ansible_ssh_pass=vagrant 

vars.yml 文件

vars.yml文件只是保留了我想要在每个节点上安装的软件包列表。vimhtoptmux是我在需要管理的每台机器上安装的喜爱软件包。其他软件包是 Kubernetes 所需的:

--- 
PACKAGES: 
  - vim  - htop  - tmux  - docker.io 
  - kubelet 
  - kubeadm 
  - kubectl 
  - kubernetes-cni

playbook.yml 文件

playbook.yml文件是您在所有主机上安装软件包时运行的文件:

---  
- hosts: all  
  become: true  
  vars_files:  
    - vars.yml  
  strategy: free  

  tasks: 
   - name: hack to resolve Problem with MergeList Issue 
     shell: 'find /var/lib/apt/lists -maxdepth 1 -type f -exec rm -v {} \;' 
   - name: update apt cache directly (apt module not reliable) 
     shell: 'apt-get clean && apt-get update' 
   - name: Preliminary installation     
     apt:  name=apt-transport-https force=yes 
   - name: Add the Google signing key  
     apt_key: url=https://packages.cloud.google.com/apt/doc/apt-key.gpg  state=present  
   - name: Add the k8s APT repo  
     apt_repository: repo='deb http://apt.kubernetes.io/ kubernetes-xenial main' state=present  
   - name: update apt cache directly (apt module not reliable) 
     shell: 'apt-get update'      
   - name: Install packages  
     apt: name={{ item }} state=installed force=yes 
     with_items: "{{ PACKAGES }}"  

由于一些软件包来自 Kubernetes APT 存储库,我需要添加它,以及 Google 签名密钥:

连接到n4

> vagrant ssh n4  

您可能需要对n1n2n3节点中的每一个进行一次ssh

vagrant@vagrant:~$ ssh 192.168.77.10
vagrant@vagrant:~$ ssh 192.168.77.11
vagrant@vagrant:~$ ssh 192.168.77.12 

一个更持久的解决方案是添加一个名为~/.ansible.cfg的文件,其中包含以下内容:

[defaults]
host_key_checking = False      

n4运行 playbook 如下:

vagrant@n4:~$ ansible-playbook -i hosts playbook.yml  

如果遇到连接失败,请重试。Kubernetes APT 存储库有时会响应缓慢。您只需要对每个节点执行一次此操作。

创建集群

现在是创建集群本身的时候了。我们将在第一个 VM 上初始化主节点,然后设置网络并将其余的 VM 添加为节点。

初始化主节点

让我们在n1192.168.77.10)上初始化主节点。在基于 vagrant VM 的云环境中,使用--apiserver-advertise-address标志是至关重要的:

> vagrant ssh n1

vagrant@n1:~$ sudo kubeadm init --apiserver-advertise-address 192.168.77.10  

在 Kubernetes 1.10.1 中,这导致了以下错误消息:

[init] Using Kubernetes version: v1.10.1
[init] Using Authorization modes: [Node RBAC]
[preflight] Running pre-flight checks.
 [WARNING FileExisting-crictl]: crictl not found in system path
[preflight] Some fatal errors occurred:
 [ERROR Swap]: running with swap on is not supported. Please disable swap
[preflight] If you know what you are doing, you can make a check non-fatal with `--ignore-preflight-errors=...`

原因是默认情况下未安装所需的 cri-tools。我们正在处理 Kubernetes 的最前沿。我创建了一个额外的 playbook 来安装 Go 和 cri-tools,关闭了交换,并修复了 vagrant VM 的主机名:

---
- hosts: all
 become: true
 strategy: free
 tasks:
 - name: Add the longsleep repo for recent golang version
 apt_repository: repo='ppa:longsleep/golang-backports' state=present
 - name: update apt cache directly (apt module not reliable)
 shell: 'apt-get update'
 args:
 warn: False
 - name: Install Go
 apt: name=golang-go state=present force=yes
 - name: Install crictl
 shell: 'go get github.com/kubernetes-incubator/cri-tools/cmd/crictl'
 become_user: vagrant
 - name: Create symlink in /usr/local/bin for crictl
 file:
 src: /home/vagrant/go/bin/crictl
 dest: /usr/local/bin/crictl
 state: link
 - name: Set hostname properly
 shell: "hostname n$((1 + $(ifconfig | grep 192.168 | awk '{print $2}' | tail -c 2)))"
 - name: Turn off swap
 shell: 'swapoff -a'
 –

记得再次在n4上运行它,以更新集群中的所有节点。

以下是成功启动 Kubernetes 的一些输出:

vagrant@n1:~$ sudo kubeadm init --apiserver-advertise-address 192.168.77.10
[init] Using Kubernetes version: v1.10.1
[init] Using Authorization modes: [Node RBAC]
[certificates] Generated ca certificate and key.
[certificates] Generated apiserver certificate and key.
[certificates] Valid certificates and keys now exist in "/etc/kubernetes/pki"
.
.
.
[addons] Applied essential addon: kube-dns
[addons] Applied essential addon: kube-proxy
Your Kubernetes master has initialized successfully!

以后加入其他节点到集群时,你需要写下更多的信息。要开始使用你的集群,你需要以普通用户身份运行以下命令:

vagrant@n1:~$ mkdir -p $HOME/.kube
vagrant@n1:~$ sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
vagrant@n1:~$ sudo chown $(id -u):$(id -g) $HOME/.kube/config 

现在你可以通过在每个节点上以 root 身份运行一个命令来加入任意数量的机器。使用从kubeadm init命令返回的命令:sudo kubeadm join --token << token>> --discovery-token-ca-cert-hash <<discvery token>> --skip-prflight-cheks

设置 Pod 网络

集群的网络是重中之重。Pod 需要能够相互通信。这需要一个 Pod 网络插件。有几种选择。由kubeadm生成的集群需要基于 CNI 的插件。我选择使用 Weave Net 插件,它支持网络策略资源。你可以选择任何你喜欢的。

在主 VM 上运行以下命令:

vagrant@n1:~$ sudo sysctl net.bridge.bridge-nf-call-iptables=1
net.bridge.bridge-nf-call-iptables = 1vagrant@n1:~$ kubectl apply -f "https://cloud.weave.works/k8s/net?k8s-version=$(kubectl version | base64 | tr -d '\n')"      

你应该看到以下内容:

serviceaccount "weave-net" created
clusterrole.rbac.authorization.k8s.io "weave-net" created
clusterrolebinding.rbac.authorization.k8s.io "weave-net" created
role.rbac.authorization.k8s.io "weave-net" created
rolebinding.rbac.authorization.k8s.io "weave-net" created
daemonset.extensions "weave-net" created  

要验证,请使用以下命令:

vagrant@n1:~$ kubectl get po --all-namespaces 
NAMESPACE NAME READY STATUS RESTARTS AGE
kube-system etcd-n1 1/1 Running 0 2m
kube-system kube-apiserver-n1 1/1 Running 0 2m
kube-system kube-controller-manager-n1 1/1 Running 0 2m
kube-system kube-dns-86f4d74b45-jqctg 3/3 Running 0 3m
kube-system kube-proxy-l54s9 1/1 Running 0 3m
kube-system kube-scheduler-n1 1/1 Running 0 2m
kube-system weave-net-fl7wn 2/2 Running 0 31s

最后一个 Pod 是我们的weave-net-fl7wn,这正是我们要找的,以及kube-dns pod。两者都在运行。一切都很好!

添加工作节点

现在我们可以使用之前获得的令牌将工作节点添加到集群中。在每个节点上,运行以下命令(不要忘记sudo)并使用在主节点上初始化 Kubernetes 时获得的令牌:

sudo kubeadm join --token <<token>>  --discovery-token-ca-cert-hash  <<discovery token>> --ignore-preflight-errors=all  

在撰写本书时(使用 Kubernetes 1.10),一些预检查失败,但这是一个错误的负面结果。实际上一切都很好,你可以通过添加--ignore-preflight-errors=all来跳过这些预检查。希望当你阅读本书时,这些问题已经解决。你应该看到以下内容:

[discovery] Trying to connect to API Server "192.168.77.10:6443"
[discovery] Created cluster-info discovery client, requesting info from "https://192.168.77.10:6443"
[discovery] Requesting info from "https://192.168.77.10:6443" again to validate TLS against the pinned public key
[discovery] Cluster info signature and contents are valid and TLS certificate validates against pinned roots, will use API Server "192.168.77.10:6443"
[discovery] Successfully established connection with API Server "192.168.77.10:6443"     

此节点已加入集群:

* Certificate signing request was sent to master and a response
  was received.
* The Kubelet was informed of the new secure connection details.  

在主节点上运行kubectl get nodes,查看此节点加入集群。

由于 CNI 插件初始化的问题,某些组合可能无法正常工作。

在云中创建集群(GCP,AWS 和 Azure)

在本地创建集群很有趣,在开发过程中以及在尝试在本地解决问题时很重要。但最终,Kubernetes 是为云原生应用程序(在云中运行的应用程序)而设计的。Kubernetes 不希望了解单个云环境,因为这不可扩展。相反,Kubernetes 具有云提供程序接口的概念。每个云提供程序都可以实现此接口,然后托管 Kubernetes。请注意,截至 1.5 版本,Kubernetes 仍在其树中维护许多云提供程序的实现,但在将来,它们将被重构。

云提供程序接口

云提供程序接口是一组 Go 数据类型和接口。它在一个名为cloud.go的文件中定义,可在bit.ly/2fq4NbW上找到。这是主要接口:

type Interface interface { 
    Initialize(clientBuilder controller.ControllerClientBuilder) 
    LoadBalancer() (LoadBalancer, bool) 
    Instances() (Instances, bool) 
    Zones() (Zones, bool) 
    Clusters() (Clusters, bool) 
    Routes() (Routes, bool) 
    ProviderName() string 
    HasClusterID() bool 
} 

这很清楚。Kubernetes 以实例,区域集群路由运行,并且需要访问负载均衡器和提供者名称。主要接口主要是一个网关。大多数方法返回其他接口。

例如,Clusters接口非常简单:

type Clusters interface { 
  ListClusters() ([]string, error) 
  Master(clusterName string) (string, error) 
} 

ListClusters()方法返回集群名称。Master()方法返回主节点的 IP 地址或 DNS 名称。

其他接口并不复杂。整个文件有 214 行(截至目前为止),包括很多注释。重点是,如果您的云平台使用这些基本概念,实现 Kubernetes 提供程序并不太复杂。

谷歌云平台(GCP)

谷歌云平台GCP)支持 Kubernetes 开箱即用。所谓的谷歌 Kubernetes 引擎GKE)是建立在 Kubernetes 上的容器管理解决方案。您不需要在 GCP 上安装 Kubernetes,可以使用 Google Cloud API 创建 Kubernetes 集群并进行配置。Kubernetes 作为 GCP 的内置部分意味着它将始终被很好地集成和经过充分测试,您不必担心底层平台的更改会破坏云提供程序接口。

总的来说,如果您计划基于 Kubernetes 构建系统,并且在其他云平台上没有任何现有代码,那么 GCP 是一个可靠的选择。

亚马逊网络服务(AWS)

亚马逊网络服务AWS)有自己的容器管理服务叫做 ECS,但它不是基于 Kubernetes 的。你可以在 AWS 上很好地运行 Kubernetes。它是一个受支持的提供者,并且有很多关于如何设置它的文档。虽然你可以自己提供一些 VM 并使用kubeadm,但我建议使用Kubernetes 运维Kops)项目。Kops 是一个在 GitHub 上可用的 Kubernetes 项目(bit.ly/2ft5KA5)。它不是 Kubernetes 本身的一部分,但是由 Kubernetes 开发人员开发和维护。

它支持以下功能:

  • 云端(AWS)自动化 Kubernetes 集群 CRUD

  • 高可用(HA)的 Kubernetes 集群

  • 它使用状态同步模型进行干运行和自动幂等性

  • kubectl的自定义支持插件

  • Kops 可以生成 Terraform 配置

  • 它基于一个在目录树中定义的简单元模型

  • 简单的命令行语法

  • 社区支持

要创建一个集群,你需要通过route53进行一些最小的 DNS 配置,设置一个 S3 存储桶来存储集群配置,然后运行一个命令:

kops create cluster --cloud=aws --zones=us-east-1c ${NAME}  

完整的说明可以在bit.ly/2f7r6EK找到。

在 2017 年底,AWS 加入了 CNCF,并宣布了两个关于 Kubernetes 的重大项目:自己的基于 Kubernetes 的容器编排解决方案(EKS)和一个按需的容器解决方案(Fargate)。

亚马逊弹性容器服务用于 Kubernetes(EKS)

亚马逊弹性容器服务用于 Kubernetes是一个完全托管且高可用的 Kubernetes 解决方案。它有三个主节点在三个可用区运行。EKS 还负责升级和打补丁。EKS 的好处是它运行的是原始的 Kubernetes,没有任何改动。这意味着你可以使用社区开发的所有标准插件和工具。它还为与其他云提供商和/或你自己的本地 Kubernetes 集群方便的集群联合开启了大门。

EKS 与 AWS 基础设施深度集成。IAM 认证与 Kubernetes 的基于角色的访问控制RBAC)集成。

如果你想直接从你自己的 Amazon VPC 访问你的 Kubernetes 主节点,你也可以使用PrivateLink。使用PrivateLink,你的 Kubernetes 主节点和 Amazon EKS 服务端点将显示为弹性网络接口,具有 Amazon VPC 中的私有 IP 地址。

拼图的另一个重要部分是一个特殊的 CNI 插件,它让您的 Kubernetes 组件可以使用 AWS 网络相互通信。

Fargate

Fargate让您可以直接运行容器,而不必担心硬件配置。它消除了操作复杂性的很大一部分,但代价是失去了一些控制。使用 Fargate 时,您将应用程序打包到容器中,指定 CPU 和内存要求,并定义网络和 IAM 策略,然后就可以运行了。Fargate 可以在 ECS 和 EKS 上运行。它是无服务器阵营中非常有趣的一员,尽管它与 Kubernetes 没有直接关联。

Azure

Azure曾经拥有自己的容器管理服务。您可以使用基于 Mesos 的 DC/OS 或 Docker Swarm 来管理它们,当然也可以使用 Kubernetes。您也可以自己配置集群(例如,使用 Azure 的期望状态配置),然后使用kubeadm创建 Kubernetes 集群。推荐的方法曾经是使用另一个非核心的 Kubernetes 项目,称为kubernetes-anywherebit.ly/2eCS7Ps)。kubernetes-anywhere的目标是提供一种在云环境中创建集群的跨平台方式(至少对于 GCP、AWS 和 Azure)。

这个过程非常简单。您需要安装 Docker、makekubectl,当然还需要您的 Azure 订阅 ID。然后,您克隆kubernetes-anywhere存储库,运行一些make命令,您的集群就可以运行了。

创建 Azure 集群的完整说明请参见bit.ly/2d56WdA

然而,在 2017 年下半年,Azure 也跳上了 Kubernetes 的列车,并推出了 AKS-Azure 容器服务。它类似于 Amazon EKS,尽管在实施上稍微领先一些。

AKS 提供了一个 REST API,以及一个 CLI,用于管理您的 Kubernetes 集群,但您也可以直接使用kubectl和任何其他 Kubernetes 工具。

以下是使用 AKS 的一些好处:

  • 自动化的 Kubernetes 版本升级和修补

  • 轻松扩展集群

  • 自愈托管控制平面(主控)

  • 节省成本-只为运行的代理节点付费

在本节中,我们介绍了云服务提供商接口,并介绍了在各种云服务提供商上创建 Kubernetes 集群的各种推荐方法。这个领域仍然很年轻,工具在迅速发展。我相信融合很快就会发生。诸如kubeadmkopsKargokubernetes-anywhere等工具和项目最终将合并,并提供一种统一且简单的方式来引导 Kubernetes 集群。

阿里巴巴云

中国的阿里巴巴云是云平台领域的新秀。它与 AWS 非常相似,尽管其英文文档还有很大的改进空间。我在阿里云上部署了一个生产应用,但没有使用 Kubernetes 集群。似乎阿里云对 Kubernetes 有官方支持,但文档是中文的。我在一个英文论坛帖子中找到了详细介绍如何在阿里云上部署 Kubernetes 集群的信息,链接为www.alibabacloud.com/forum/read-830

从头开始创建裸机集群

在上一节中,我们讨论了在云服务提供商上运行 Kubernetes。这是 Kubernetes 的主要部署方式,但在裸机上运行 Kubernetes 也有很强的用例。我在这里不关注托管与本地部署;这是另一个维度。如果您已经在本地管理了很多服务器,那么您就处于最佳决策位置。

裸机的用例

裸机集群是一种特殊情况,特别是如果您自己管理它们。有一些公司提供裸机 Kubernetes 集群的商业支持,比如 Platform 9,但这些产品尚不成熟。一个坚实的开源选择是 Kubespray,它可以在裸机、AWS、GCE、Azure 和 OpenStack 上部署工业强度的 Kubernetes 集群。

以下是一些情况下使用裸机集群是有意义的:

  • 预算问题:如果您已经管理了大规模的裸机集群,那么在您的物理基础设施上运行 Kubernetes 集群可能会更便宜

  • 低网络延迟:如果您的节点之间必须有低延迟,那么虚拟机的开销可能会太大

  • 监管要求:如果您必须遵守法规,可能不允许使用云服务提供商

  • 您想要对硬件拥有完全控制权:云服务提供商为您提供了许多选择,但您可能有特殊需求

何时应考虑创建裸机集群?

从头开始创建集群的复杂性是显著的。Kubernetes 集群并不是一个微不足道的东西。关于如何设置裸机集群的文档很多,但随着整个生态系统的不断发展,许多这些指南很快就会过时。

如果您有操作能力,可以花时间在堆栈的每个级别调试问题,那么您应该考虑走这条路。大部分问题可能与网络有关,但文件系统和存储驱动程序也可能会困扰您,还有一般的不兼容性和组件之间的版本不匹配,比如 Kubernetes 本身、Docker(或 rkt,如果您敢尝试)、Docker 镜像、您的操作系统、您的操作系统内核以及您使用的各种附加组件和工具。

这个过程

有很多事情要做。以下是您需要解决的一些问题的列表:

  • 实现自己的云提供商接口或绕过它

  • 选择网络模型以及如何实现它(使用 CNI 插件或直接编译)

  • 是否使用网络策略

  • 选择系统组件的镜像

  • 安全模型和 SSL 证书

  • 管理员凭据

  • 组件的模板,如 API 服务器、复制控制器和调度器

  • 集群服务,如 DNS、日志记录、监控和 GUI

我建议阅读 Kubernetes 网站上的指南(bit.ly/1ToR9EC),以更深入地了解从头开始创建集群所需的步骤。

使用虚拟私有云基础设施

如果您的用例属于裸机用例,但您没有必要的熟练人手或者不愿意处理裸机的基础设施挑战,您可以选择使用私有云,比如 OpenStack(例如,使用 stackube)。如果您想在抽象层次上再高一点,那么 Mirantis 提供了一个建立在 OpenStack 和 Kubernetes 之上的云平台。

在本节中,我们考虑了构建裸机集群 Kubernetes 集群的选项。我们研究了需要它的用例,并突出了挑战和困难。

Bootkube

Bootkube也非常有趣。它可以启动自托管的 Kubernetes 集群。自托管意味着大多数集群组件都作为常规 pod 运行,并且可以使用与您用于容器化应用程序相同的工具和流程进行管理、监控和升级。这种方法有显著的好处,简化了 Kubernetes 集群的开发和运行。

总结

在这一章中,我们进行了一些实际的集群创建。我们使用 Minikube 创建了一个单节点集群,使用kubeadm创建了一个多节点集群。然后我们看了很多使用云提供商创建 Kubernetes 集群的选项。最后,我们触及了在裸机上创建 Kubernetes 集群的复杂性。当前的情况非常动态。基本组件在迅速变化,工具仍然很年轻,每个环境都有不同的选择。建立 Kubernetes 集群并不是完全简单的,但通过一些努力和细节的关注,你可以快速完成。

在下一章中,我们将探讨监控、日志记录和故障排除等重要主题。一旦您的集群正常运行并开始部署工作负载,您需要确保它正常运行并满足要求。这需要持续关注和对现实世界中发生的各种故障做出响应。

第三章:监控、日志记录和故障排除

在第二章中,创建 Kubernetes 集群,您学习了如何在不同环境中创建 Kubernetes 集群,尝试了不同的工具,并创建了一些集群。

创建 Kubernetes 集群只是故事的开始。一旦集群运行起来,您需要确保它是可操作的,所有必要的组件都齐全并正确配置,并且部署了足够的资源来满足要求。响应故障、调试和故障排除是管理任何复杂系统的重要部分,Kubernetes 也不例外。

本章将涵盖以下主题:

  • 使用 Heapster 进行监控

  • 使用 Kubernetes 仪表板进行性能分析

  • 中央日志记录

  • 在节点级别检测问题

  • 故障排除场景

  • 使用 Prometheus

在本章结束时,您将对监视 Kubernetes 集群的各种选项有扎实的了解,知道如何访问日志以及如何分析它们。您将能够查看健康的 Kubernetes 集群并验证一切正常。您还将能够查看不健康的 Kubernetes 集群,并系统地诊断它,定位问题并解决它们。

使用 Heapster 监控 Kubernetes

Heapster 是一个为 Kubernetes 集群提供强大监控解决方案的 Kubernetes 项目。它作为一个 pod(当然)运行,因此可以由 Kubernetes 本身管理。Heapster 支持 Kubernetes 和 CoreOS 集群。它具有非常模块化和灵活的设计。Heapster 从集群中的每个节点收集操作指标和事件,将它们存储在持久后端(具有明确定义的模式)中,并允许可视化和编程访问。Heapster 可以配置为使用不同的后端(或在 Heapster 术语中称为 sinks)及其相应的可视化前端。最常见的组合是 InfluxDB 作为后端,Grafana 作为前端。谷歌云平台将 Heapster 与谷歌监控服务集成。还有许多其他不太常见的后端,如下所示:

  • 日志

  • 谷歌云监控

  • 谷歌云日志

  • Hawkular-Metrics(仅指标)

  • OpenTSDB

  • Monasca(仅指标)

  • Kafka(仅指标)

  • Riemann(仅指标)

  • Elasticsearch

您可以通过在命令行上指定 sinks 来使用多个后端:

--sink=log --sink=influxdb:http://monitoring-influxdb:80/  

cAdvisor

cAdvisor 是 kubelet 的一部分,它在每个节点上运行。它收集有关每个容器的 CPU/核心使用情况、内存、网络和文件系统的信息。它在端口4194上提供基本 UI,但是对于 Heapster 来说,最重要的是它通过 Kubelet 提供了所有这些信息。Heapster 记录了由 cAdvisor 在每个节点上收集的信息,并将其存储在其后端以进行分析和可视化。

如果您想快速验证特定节点是否设置正确,例如,在 Heapster 尚未连接时创建新集群,那么 cAdvisor UI 非常有用。

这是它的样子:

安装 Heapster

Heapster 组件可能已安装或尚未安装在您的 Kubernetes 集群中。如果 Heapster 尚未安装,您可以使用几个简单的命令进行安装。首先,让我们克隆 Heapster 存储库:

> git clone https://github.com/kubernetes/heapster.git
> cd heapster 

在早期版本的 Kubernetes 中,Heapster 默认将服务公开为NodePort。现在,默认情况下,它们被公开为ClusterIP,这意味着它们仅在集群内可用。为了使它们在本地可用,我在deploy/kube-config/influxdb中的每个服务的规范中添加了 type: NodePort。例如,对于deploy/kube-config/influxdb/influxdb.yaml

> git diff deploy/kube-config/influxdb/influxdb.yaml
diff --git a/deploy/kube-config/influxdb/influxdb.yaml b/deploy/kube-config/influxdb/influxdb.yaml
index 29408b81..70f52d2c 100644
--- a/deploy/kube-config/influxdb/influxdb.yaml
+++ b/deploy/kube-config/influxdb/influxdb.yaml
@@ -33,6 +33,7 @@ metadata:
 name: monitoring-influxdb
 namespace: kube-system
 spec:
+ type: NodePort
 ports:
 - port: 8086
 targetPort: 8086

我对deploy/kube-config/influxdb/grafana.yaml进行了类似的更改,其中+ type: NodePort这一行被注释掉了,所以我只是取消了注释。现在,我们实际上可以安装 InfluxDB 和 Grafana:

> kubectl create -f deploy/kube-config/influxdb  

您应该看到以下输出:

deployment "monitoring-grafana" created
service "monitoring-grafana" created
serviceaccount "heapster" created
deployment "heapster" created
service "heapster" created
deployment "monitoring-influxdb" created
service "monitoring-influxdb" created  

InfluxDB 后端

InfluxDB 是一个现代而强大的分布式时间序列数据库。它非常适合用于集中式指标和日志记录,并被广泛使用。它也是首选的 Heapster 后端(在谷歌云平台之外)。唯一的问题是 InfluxDB 集群;高可用性是企业提供的一部分。

存储模式

InfluxDB 存储模式定义了 Heapster 在 InfluxDB 中存储的信息,并且可以在以后进行查询和绘图。指标分为多个类别,称为测量。您可以单独处理和查询每个指标,或者您可以将整个类别作为一个测量进行查询,并将单独的指标作为字段接收。命名约定是<category>/<metrics name>(除了正常运行时间,它只有一个指标)。如果您具有 SQL 背景,可以将测量视为表。每个指标都存储在每个容器中。每个指标都带有以下信息标签:

  • pod_id: 一个 pod 的唯一 ID

  • pod_name: pod 的用户提供的名称

  • pod_namespace: pod 的命名空间

  • container_base_image: 容器的基础镜像

  • container_name: 容器的用户提供的名称或系统容器的完整cgroup名称

  • host_id: 云服务提供商指定或用户指定的节点标识符

  • hostname: 容器运行的主机名

  • labels: 用户提供的标签的逗号分隔列表;格式为key:value

  • namespace_id: pod 命名空间的 UID

  • resource_id: 用于区分同一类型多个指标的唯一标识符,例如,文件系统/使用下的 FS 分区

以下是按类别分组的所有指标,可以看到,它非常广泛。

CPU

CPU 指标包括:

  • cpu/limit: 毫核的 CPU 硬限制

  • cpu/node_capacity: 节点的 CPU 容量

  • cpu/node_allocatable: 节点的可分配 CPU

  • cpu/node_reservation: 节点可分配的 CPU 保留份额

  • cpu/node_utilization: CPU 利用率占节点可分配资源的份额

  • cpu/request: CPU 请求(资源的保证数量)(毫核)

  • cpu/usage: 所有核心的累积 CPU 使用率

  • cpu/usage_rate: 所有核心的 CPU 使用率(毫核)

文件系统

文件系统指标包括:

  • filesystem/usage: 文件系统上消耗的总字节数

  • filesystem/limit: 文件系统的总大小(字节)

  • filesystem/available: 文件系统中剩余的可用字节数

内存

内存指标包括:

  • memory/limit: 内存的硬限制(字节)

  • memory/major_page_faults: 主要页面错误的数量

  • memory/major_page_faults_rate: 每秒的主要页面错误数

  • memory/node_capacity: 节点的内存容量

  • memory/node_allocatable: 节点的可分配内存

  • memory/node_reservation: 节点可分配内存上保留的份额

  • memory/node_utilization: 内存利用率占内存可分配资源的份额

  • memory/page_faults: 页面错误的数量

  • memory/page_faults_rate: 每秒的页面错误数

  • memory/request: 内存请求(资源的保证数量)(字节)

  • memory/usage: 总内存使用量

  • memory/working_set: 总工作集使用量;工作集是内存的使用部分,不容易被内核释放

网络

网络指标包括:

  • network/rx: 累积接收的网络字节数

  • network/rx_errors: 接收时的累积错误数

网络

  • network/rx_errors_rate:在网络接收过程中每秒发生的错误次数

  • network/rx_rate:每秒通过网络接收的字节数

  • network/tx:通过网络发送的累积字节数

  • network/tx_errors:在网络发送过程中的累积错误次数

  • network/tx_errors_rate:在网络发送过程中发生的错误次数

  • network/tx_rate:每秒通过网络发送的字节数

正常运行时间

正常运行时间是容器启动以来的毫秒数。

如果您熟悉 InfluxDB,可以直接使用它。您可以使用其自己的 API 连接到它,也可以使用其 Web 界面。键入以下命令以查找其端口和端点:

> k describe service monitoring-influxdb --namespace=kube-system | grep NodePort
Type:               NodePort
NodePort:           <unset>  32699/TCP 

现在,您可以使用 HTTP 端口浏览 InfluxDB Web 界面。您需要将其配置为指向 API 端口。默认情况下,用户名密码rootroot

设置完成后,您可以选择要使用的数据库(请参阅右上角)。Kubernetes 数据库的名称为k8s。现在,您可以使用 InfluxDB 查询语言查询指标。

Grafana 可视化

Grafana 在其自己的容器中运行,并提供一个与 InfluxDB 作为数据源配合良好的复杂仪表板。要找到端口,请键入以下命令:

k describe service monitoring-influxdb --namespace=kube-system | grep NodePort

Type:                NodePort
NodePort:            <unset> 30763/TCP  

现在,您可以在该端口上访问 Grafana Web 界面。您需要做的第一件事是设置数据源指向 InfluxDB 后端:

确保测试连接,然后去探索仪表板中的各种选项。有几个默认的仪表板,但您应该能够根据自己的喜好进行自定义。Grafana 旨在让您根据自己的需求进行调整。

发现和负载平衡

发现和负载平衡类别通常是您开始的地方。服务是您的 Kubernetes 集群的公共接口。严重的问题将影响您的服务,从而影响您的用户:

当您通过单击服务进行深入了解时,您将获得有关服务的一些信息(最重要的是标签选择器)和一个 Pods 视图。

使用仪表板进行性能分析

迄今为止,我最喜欢的工具,当我只想知道集群中发生了什么时,就是 Kubernetes 仪表板。以下是几个原因:

  • 它是内置的(始终与 Kubernetes 同步和测试)

  • 它很快

  • 它提供了一个直观的深入界面,从集群级别一直到单个容器

  • 它不需要任何定制或配置

虽然 Heapster、InfluxDB 和 Grafana 更适合定制和重型视图和查询,但 Kubernetes 仪表板的预定义视图可能能够在 80-90%的时间内回答所有你的问题。

您还可以通过上传适当的 YAML 或 JSON 文件,使用仪表板部署应用程序并创建任何 Kubernetes 资源,但我不会涉及这个,因为这对于可管理的基础设施来说是一种反模式。在玩测试集群时可能有用,但对于实际修改集群状态,我更喜欢使用命令行。您的情况可能有所不同。

让我们先找到端口:

k describe service kubernetes-dashboard --namespace=kube-system | grep NodePort

Type:                   NodePort
NodePort:               <unset> 30000/TCP  

顶层视图

仪表板以左侧的分层视图组织(可以通过单击汉堡菜单隐藏),右侧是动态的、基于上下文的内容。您可以深入分层视图,以深入了解相关信息。

有几个顶层类别:

  • 集群

  • 概述

  • 工作负载

  • 发现和负载平衡

  • 配置和存储

您还可以通过特定命名空间过滤所有内容或选择所有命名空间。

集群

集群视图有五个部分:命名空间、节点、持久卷、角色和存储类。它主要是观察集群的物理资源:

一眼就可以获得大量信息:所有节点的 CPU 和内存使用情况,可用的命名空间,它们的状态和年龄。对于每个节点,您可以看到它的年龄、标签,以及它是否准备就绪。如果有持久卷和角色,您也会看到它们,然后是存储类(在这种情况下只是主机路径)。

如果我们深入节点并点击 minikube 节点本身,我们会得到有关该节点和分配资源的详细信息,以一个漂亮的饼图显示。这对处理性能问题至关重要。如果一个节点没有足够的资源,那么它可能无法满足其 pod 的需求:

如果您向下滚动,您会看到更多有趣的信息。条件窗格是最重要的地方。您可以清晰、简洁地查看每个节点的内存和磁盘压力:

还有 Pods 和 Events 窗格。我们将在下一节讨论 pod。

工作负载

工作负载类别是主要类别。它组织了许多类型的 Kubernetes 资源,如 CronJobs、Daemon Sets、Deployments、Jobs、Pods、Replica Sets、Replication Controllers 和 Stateful Sets。您可以沿着任何这些维度进行深入。这是默认命名空间的顶级工作负载视图,目前只部署了 echo 服务。您可以看到部署、副本集和 pod:

让我们切换到所有命名空间并深入研究 Pods 子类别。这是一个非常有用的视图。在每一行中,您可以看出 pod 是否正在运行,它重新启动了多少次,它的 IP,甚至嵌入了 CPU 和内存使用历史记录作为漂亮的小图形:

您也可以通过点击文本符号(从右边数第二个)查看任何 pod 的日志。让我们检查 InfluxDB pod 的日志。看起来一切都井井有条,Heapster 成功地向其写入:

还有一个我们尚未探讨的更详细的层次。我们可以进入容器级别。让我们点击 kubedns pod。我们得到以下屏幕,显示了各个容器及其run命令;我们还可以查看它们的日志:

添加中央日志记录

中央日志记录或集群级日志记录是任何具有多个节点、pod 或容器的集群的基本要求。首先,单独查看每个 pod 或容器的日志是不切实际的。您无法获得系统的全局图片,而且将有太多的消息需要筛选。您需要一个聚合日志消息并让您轻松地切片和切块的解决方案。第二个原因是容器是短暂的。有问题的 pod 通常会死掉,它们的复制控制器或副本集将启动一个新实例,丢失所有重要的日志信息。通过记录到中央日志记录服务,您可以保留这些关键的故障排除信息。

规划中央日志记录

在概念上,中央日志非常简单。在每个节点上,您运行一个专用代理,拦截节点上所有 pod 和容器的所有日志消息,并将它们连同足够的元数据发送到一个中央存储库,其中它们被安全地存储。

像往常一样,如果您在谷歌平台上运行,那么 GKE 会为您提供支持,并且有一个谷歌集中日志服务集成得很好。对于其他平台,一个流行的解决方案是 fluentd、Elasticsearch 和 Kibana。有一个官方的附加组件来为每个组件设置适当的服务。fluentd-elasticsearch附加组件位于bit.ly/2f6MF5b

它被安装为 Elasticsearch 和 Kibana 的一组服务,并且在每个节点上安装了 fluentd 代理。

Fluentd

Fluentd 是一个统一的日志记录层,位于任意数据源和任意数据接收器之间,并确保日志消息可以从 A 流向 B。Kubernetes 带有一个附加组件,其中有一个部署 fluentd 代理的 Docker 镜像,它知道如何读取与 Kubernetes 相关的各种日志,如Docker日志、etcd日志和Kube日志。它还为每条日志消息添加标签,以便用户以后可以轻松地按标签进行过滤。这是fluentd-es-configmap.yaml文件的一部分:

# Example:
# 2016/02/04 06:52:38 filePurge: successfully removed file 
/var/etcd/data/member/wal/00000000000006d0-00000000010a23d1.wal
<source>
 type tail
 # Not parsing this, because it doesn't have anything particularly 
useful to
 # parse out of it (like severities).
 format none
 path /var/log/etcd.log
 pos_file /var/log/es-etcd.log.pos
 tag etcd
</source>

Elasticsearch

Elasticsearch 是一个很棒的文档存储和全文搜索引擎。它在企业中很受欢迎,因为它非常快速、可靠和可扩展。它作为一个 Docker 镜像在 Kubernetes 中央日志附加组件中使用,并且部署为一个服务。请注意,一个完全成熟的 Elasticsearch 生产集群(将部署在 Kubernetes 集群上)需要自己的主节点、客户端节点和数据节点。对于大规模和高可用的 Kubernetes 集群,中央日志本身将被集群化。Elasticsearch 可以使用自我发现。这是一个企业级的解决方案:github.com/pires/kubernetes-elasticsearch-cluster

Kibana

Kibana 是 Elasticsearch 的搭档。它用于可视化和与 Elasticsearch 存储和索引的数据进行交互。它也作为一个服务被附加组件安装。这是 Kibana 的 Dockerfile 模板(bit.ly/2lwmtpc)。

检测节点问题

在 Kubernetes 的概念模型中,工作单位是 pod。但是,pod 被调度到节点上。在监控和可靠性方面,节点是最需要关注的,因为 Kubernetes 本身(调度器和复制控制器)负责 pod。节点可能遭受各种问题,而 Kubernetes 并不知晓。因此,它将继续将 pod 调度到有问题的节点上,而 pod 可能无法正常运行。以下是节点可能遭受的一些问题,尽管看起来是正常的:

  • CPU 问题

  • 内存问题

  • 磁盘问题

  • 内核死锁

  • 损坏的文件系统

  • Docker 守护进程问题

kubelet 和 cAdvisor 无法检测到这些问题,需要另一个解决方案。进入节点问题检测器。

节点问题检测器

节点问题检测器是在每个节点上运行的一个 pod。它需要解决一个困难的问题。它需要检测不同环境、不同硬件和不同操作系统上的各种问题。它需要足够可靠,不受影响(否则,它无法报告问题),并且需要具有相对较低的开销,以避免向主节点发送大量信息。此外,它需要在每个节点上运行。Kubernetes 最近收到了一个名为 DaemonSet 的新功能,解决了最后一个问题。

源代码位于github.com/kubernetes/node-problem-detector

守护进程集

DaemonSet 是每个节点的一个 pod。一旦定义了 DaemonSet,集群中添加的每个节点都会自动获得一个 pod。如果该 pod 死掉,Kubernetes 将在该节点上启动该 pod 的另一个实例。可以将其视为带有 1:1 节点- pod 亲和性的复制控制器。节点问题检测器被定义为一个 DaemonSet,这与其要求完全匹配。可以使用亲和性、反亲和性和污点来更精细地控制 DaemonSet 的调度。

问题守护进程

节点问题检测器的问题(双关语)在于它需要处理太多问题。试图将所有这些问题都塞进一个代码库中会导致一个复杂、臃肿且永远不稳定的代码库。节点问题检测器的设计要求将报告节点问题的核心功能与特定问题检测分离开来。报告 API 基于通用条件和事件。问题检测应该由单独的问题守护程序(每个都在自己的容器中)来完成。这样,就可以添加和演进新的问题检测器,而不会影响核心节点问题检测器。此外,控制平面可能会有一个补救控制器,可以自动解决一些节点问题,从而实现自愈。

在这个阶段(Kubernetes 1.10),问题守护程序已经嵌入到节点问题检测器二进制文件中,并且它们作为 Goroutines 执行,因此您还没有获得松耦合设计的好处。

在这一部分,我们涵盖了节点问题的重要主题,这可能会妨碍工作负载的成功调度,以及节点问题检测器如何帮助解决这些问题。在下一节中,我们将讨论各种故障场景以及如何使用 Heapster、中央日志、Kubernetes 仪表板和节点问题检测器进行故障排除。

故障排除场景

在一个大型的 Kubernetes 集群中,有很多事情可能会出错,而且它们确实会出错,这是可以预料的。您可以采用最佳实践并最小化其中一些问题(主要是人为错误),通过严格的流程来减少一些问题。然而,一些问题,比如硬件故障和网络问题是无法完全避免的。即使是人为错误,如果这意味着开发时间变慢,也不应该总是被最小化。在这一部分,我们将讨论各种故障类别,如何检测它们,如何评估它们的影响,并考虑适当的应对措施。

设计健壮的系统

当您想设计一个强大的系统时,首先需要了解可能的故障模式,每种故障的风险/概率以及每种故障的影响/成本。然后,您可以考虑各种预防和缓解措施、损失削减策略、事件管理策略和恢复程序。最后,您可以制定一个与风险相匹配的缓解方案,包括成本。全面的设计很重要,并且需要随着系统的发展而进行更新。赌注越高,您的计划就应该越彻底。这个过程必须为每个组织量身定制。错误恢复和健壮性的一个角落是检测故障并能够进行故障排除。以下小节描述了常见的故障类别,如何检测它们以及在哪里收集额外信息。

硬件故障

Kubernetes 中的硬件故障可以分为两组:

  • 节点无响应

  • 节点有响应

当节点无响应时,有时很难确定是网络问题、配置问题还是实际的硬件故障。显然,您无法使用节点本身的日志或运行诊断。你能做什么?首先,考虑节点是否曾经有响应。如果这是一个刚刚添加到集群中的节点,更有可能是配置问题。如果这是集群中的一个节点,那么您可以查看来自 Heapster 或中央日志的节点的历史数据,并查看日志中是否有任何错误或性能下降的迹象,这可能表明硬件故障。

当节点有响应时,它可能仍然遭受冗余硬件的故障,例如非操作系统磁盘或一些核心。如果节点问题检测器在节点上运行并引起一些事件或节点条件引起主节点的注意,您可以检测硬件故障。或者,您可能会注意到 Pod 不断重新启动或作业完成时间较长。所有这些都可能是硬件故障的迹象。另一个硬件故障的强烈暗示是,如果问题局限在单个节点上,并且标准维护操作(如重新启动)不能缓解症状。

如果您的集群部署在云中,替换一个您怀疑存在硬件问题的节点是微不足道的。只需手动提供一个新的 VM 并删除坏的 VM 即可。在某些情况下,您可能希望采用更自动化的流程并使用一个补救控制器,正如节点问题检测器设计所建议的那样。您的补救控制器将监听问题(或缺少的健康检查),并可以自动替换坏的节点。即使在私有托管或裸金属中,这种方法也可以运行,只要您保留一些额外的节点准备投入使用。大规模集群即使在大部分时间内容量减少也可以正常运行。您可以容忍少量节点宕机时的轻微容量减少,或者您可以略微过度配置。这样,当一个节点宕机时,您就有了一些余地。

配额、份额和限制

Kubernetes 是一个多租户系统。它旨在高效利用资源,但是它根据命名空间中可用配额和限制以及 pod 和容器对保证资源的请求之间的一套检查和平衡系统来调度 pod 并分配资源。我们将在本书的后面深入讨论细节。在这里,我们只考虑可能出现的问题以及如何检测它。您可能会遇到几种不良结果:

  • 资源不足:如果一个 pod 需要一定数量的 CPU 或内存,而没有可用容量的节点,那么该 pod 就无法被调度。

  • 资源利用不足:一个 pod 可能声明需要一定数量的 CPU 或内存,Kubernetes 会满足,但是 pod 可能只使用其请求资源的一小部分。这只是浪费。

  • 节点配置不匹配:一个需要大量 CPU 但很少内存的 pod 可能被调度到一个高内存的节点上,并使用所有的 CPU 资源,从而占用了节点,因此无法调度其他 pod,但未使用的内存却被浪费了。

查看仪表板是一种通过视觉寻找可疑情况的好方法。过度订阅或资源利用不足的节点和 pod 都是配额和资源请求不匹配的候选者。

一旦您检测到一个候选项,您可以深入使用describe命令来查看节点或 pod 级别。在大规模集群中,您应该有自动化检查,以比较利用率与容量规划。这很重要,因为大多数大型系统都有一定程度的波动,而不会期望均匀的负载。确保您了解系统的需求,并且您的集群容量在正常范围内或可以根据需要弹性调整。

错误的配置

错误的配置是一个总称。您的 Kubernetes 集群状态是配置;您的容器的命令行参数是配置;Kubernetes、您的应用服务和任何第三方服务使用的所有环境变量都是配置;所有配置文件都是配置。在一些数据驱动的系统中,配置存储在各种数据存储中。配置问题非常常见,因为通常没有建立良好的实践来测试它们。它们通常具有各种回退(例如,配置文件的搜索路径)和默认值,并且生产环境配置与开发或暂存环境不同。

在 Kubernetes 集群级别,可能存在许多可能的配置问题,如下所示:

  • 节点、pod 或容器的标签不正确

  • 在没有复制控制器的情况下调度 pod

  • 服务端口的规范不正确

  • 不正确的 ConfigMap

大多数这些问题可以通过拥有适当的自动化部署流程来解决,但您必须深入了解您的集群架构以及 Kubernetes 资源如何配合。

配置问题通常发生在您更改某些内容之后。在每次部署或手动更改集群后,验证其状态至关重要。

Heapster 和仪表板在这里是很好的选择。我建议从服务开始,并验证它们是否可用、响应和功能正常。然后,您可以深入了解系统是否也在预期的性能参数范围内运行。

日志还提供了有用的提示,并可以确定特定的配置选项。

成本与性能

大型集群并不便宜。特别是在云中运行时。操作大规模系统的一个重要部分是跟踪开支。

在云上管理成本

云的最大好处之一是它可以满足弹性需求,满足系统根据需要自动扩展和收缩,通过根据需要分配和释放资源。Kubernetes 非常适合这种模型,并且可以扩展以根据需要提供更多节点。风险在于,如果不适当地限制,拒绝服务攻击(恶意的、意外的或自我造成的)可能导致昂贵资源的任意分配。这需要仔细监控,以便及早发现。命名空间的配额可以避免这种情况,但您仍然需要能够深入了解并准确定位核心问题。根本原因可能是外部的(僵尸网络攻击),配置错误,内部测试出错,或者是检测或分配资源的代码中的错误。

在裸金属上管理成本

在裸金属上,您通常不必担心资源分配失控,但是如果您需要额外的容量并且无法快速提供更多资源,您很容易遇到瓶颈。容量规划和监控系统性能以及及早检测需求是 OPS 的主要关注点。Heapster 可以显示历史趋势,并帮助识别高峰时段和总体需求增长。

管理混合集群的成本

混合集群在裸金属和云上运行(可能还在私人托管服务上)。考虑因素是相似的,但您可能需要汇总您的分析。我们将在稍后更详细地讨论混合集群。

使用 Prometheus

Heapster 和 Kubernetes 默认的监控和日志记录是一个很好的起点。然而,Kubernetes 社区充满了创新,有几种替代方案可供选择。其中最受欢迎的解决方案之一是 Prometheus。在本节中,我们将探索运营商的新世界,Prometheus 运营商,如何安装它以及如何使用它来监视您的集群。

什么是运营商?

运营商是一种新型软件,它封装了在 Kubernetes 之上开发、管理和维护应用程序所需的操作知识。这个术语是由 CoreOS 在 2016 年底引入的。运营商是一个特定于应用程序的控制器,它扩展了 Kubernetes API,以代表 Kubernetes 用户创建、配置和管理复杂有状态应用程序的实例。它建立在基本的 Kubernetes 资源和控制器概念之上,但包括领域或应用程序特定的知识,以自动化常见任务。

Prometheus Operator

Prometheus (prometheus.io)是一个用于监控集群中应用程序的开源系统监控和警报工具包。它受 Google 的 Borgmon 启发,并设计用于 Kubernetes 模型的工作单元分配和调度。它于 2016 年加入 CNCF,并在整个行业广泛采用。InfluxDB 和 Prometheus 之间的主要区别在于,Prometheus 使用拉模型,任何人都可以访问/metrics 端点,其查询语言非常表达性强,但比 InfluxDB 的类似 SQL 的查询语言更简单。

Kubernetes 具有内置功能来支持 Prometheus 指标,而 Prometheus 对 Kuberneres 的认识不断改进。Prometheus Operator 将所有这些监控功能打包成一个易于安装和使用的捆绑包。

使用 kube-prometheus 安装 Prometheus

安装 Prometheus 的最简单方法是使用 kube-prometheus。它使用 Prometheus Operator 以及 Grafana 进行仪表板和AlertManager的管理。要开始,请克隆存储库并运行deploy脚本:

> git clone https://github.com/coreos/prometheus-operator.git 
> cd contrib/kube-prometheus
> hack/cluster-monitoring/deploy 

该脚本创建一个监控命名空间和大量的 Kubernetes 实体和支持组件。

  • Prometheus Operator 本身

  • Prometheus node_exporter

  • kube-state metrics

  • 覆盖监控所有 Kubernetes 核心组件和导出器的 Prometheus 配置

  • 集群组件健康的默认一组警报规则

  • 为集群指标提供仪表板的 Grafana 实例

  • 一个由三个节点组成的高可用性 Alertmanager 集群

让我们验证一切是否正常:

> kg po --namespace=monitoring
NAME                                READY     STATUS    RESTARTS   AGE
alertmanager-main-0                  2/2       Running   0          1h
alertmanager-main-1                  2/2       Running   0          1h
alertmanager-main-2                  0/2       Pending   0          1h
grafana-7d966ff57-rvpwk              2/2       Running   0          1h
kube-state-metrics-5dc6c89cd7-s9n4m  2/2       Running   0          1h
node-exporter-vfbhq                  1/1       Running   0          1h
prometheus-k8s-0                     2/2       Running   0          1h
prometheus-k8s-1                     2/2       Running   0          1h
prometheus-operator-66578f9cd9-5t6xw 1/1       Running   0          1h  

请注意,alertmanager-main-2处于挂起状态。我怀疑这是由于 Minikube 在两个核心上运行。在我的设置中,这实际上并没有造成任何问题。

使用 Prometheus 监控您的集群

一旦 Prometheus Operator 与 Grafana 和 Alertmanager 一起运行,您就可以访问它们的 UI 并与不同的组件进行交互:

  • 节点端口30900上的 Prometheus UI

  • 节点端口30903上的 Alertmanager UI

  • 节点端口30902上的 Grafana

Prometheus 支持选择的指标种类繁多。以下是一个屏幕截图,显示了按容器分解的微秒级 HTTP 请求持续时间:

要将视图限制为prometheus-k8s服务的仅 0.99 分位数,请使用以下查询:

http_request_duration_microseconds{service="prometheus-k8s", quantile="0.99"}  

Alertmanager 是 Prometheus 监控故事的另一个重要部分。这是一个 Web UI 的截图,让您可以根据任意指标定义和配置警报。

总结

在本章中,我们讨论了监控、日志记录和故障排除。这是操作任何系统的关键方面,特别是像 Kubernetes 这样有许多移动部件的平台。每当我负责某件事情时,我最担心的是出现问题,而我没有系统化的方法来找出问题所在以及如何解决它。Kubernetes 内置了丰富的工具和设施,如 Heapster、日志记录、DaemonSets 和节点问题检测器。您还可以部署任何您喜欢的监控解决方案。

在第四章中,高可用性和可靠性,我们将看到高可用和可扩展的 Kubernetes 集群。这可以说是 Kubernetes 最重要的用例,它在与其他编排解决方案相比的时候表现出色。

第四章:高可用性和可靠性

在上一章中,我们讨论了监视您的 Kubernetes 集群,在节点级别检测问题,识别和纠正性能问题以及一般故障排除。

在本章中,我们将深入探讨高度可用的集群主题。这是一个复杂的主题。Kubernetes 项目和社区尚未就实现高可用性的真正方式达成一致。高度可用的 Kubernetes 集群有许多方面,例如确保控制平面在面对故障时能够继续运行,保护etcd中的集群状态,保护系统的数据,并快速恢复容量和/或性能。不同的系统将有不同的可靠性和可用性要求。如何设计和实现高度可用的 Kubernetes 集群将取决于这些要求。

通过本章的学习,您将了解与高可用性相关的各种概念,并熟悉 Kubernetes 高可用性最佳实践以及何时使用它们。您将能够使用不同的策略和技术升级实时集群,并能够根据性能、成本和可用性之间的权衡选择多种可能的解决方案。

高可用性概念

在这一部分,我们将通过探索可靠和高可用系统的概念和构建模块来开始我们的高可用性之旅。百万(万亿?)美元的问题是,我们如何从不可靠的组件构建可靠和高可用的系统?组件会失败,你可以把它带到银行;硬件会失败;网络会失败;配置会出错;软件会有 bug;人会犯错误。接受这一点,我们需要设计一个系统,即使组件失败,也能可靠和高可用。这个想法是从冗余开始,检测组件故障,并快速替换坏组件。

冗余

冗余是可靠和高可用系统在硬件和数据级别的基础。如果关键组件失败并且您希望系统继续运行,您必须准备好另一个相同的组件。Kubernetes 本身通过复制控制器和副本集来管理无状态的 pod。然而,您的etcd中的集群状态和主要组件本身需要冗余以在某些组件失败时继续运行。此外,如果您的系统的重要组件没有受到冗余存储的支持(例如在云平台上),那么您需要添加冗余以防止数据丢失。

热插拔

热插拔是指在不关闭系统的情况下替换失败的组件的概念,对用户的中断最小(理想情况下为零)。如果组件是无状态的(或其状态存储在单独的冗余存储中),那么热插拔新组件来替换它就很容易,只需要将所有客户端重定向到新组件。然而,如果它存储本地状态,包括内存中的状态,那么热插拔就很重要。有以下两个主要选项:

  • 放弃在飞行中的交易

  • 保持热备份同步

第一个解决方案要简单得多。大多数系统都足够弹性,可以应对故障。客户端可以重试失败的请求,而热插拔的组件将为它们提供服务。

第二个解决方案更加复杂和脆弱,并且会产生性能开销,因为每次交互都必须复制到两个副本(并得到确认)。对于系统的某些部分可能是必要的。

领导者选举

领导者或主选举是分布式系统中常见的模式。您经常有多个相同的组件协作和共享负载,但其中一个组件被选为领导者,并且某些操作通过领导者进行序列化。您可以将具有领导者选举的分布式系统视为冗余和热插拔的组合。这些组件都是冗余的,当当前领导者失败或不可用时,将选举出一个新的领导者并进行热插拔。

智能负载平衡

负载平衡是指在多个组件之间分配服务传入请求的工作负载。当一些组件失败时,负载平衡器必须首先停止向失败或不可达的组件发送请求。第二步是提供新的组件来恢复容量并更新负载平衡器。Kubernetes 通过服务、端点和标签提供了支持这一点的很好的设施。

幂等性

许多类型的故障都可能是暂时的。这在网络问题或过于严格的超时情况下最常见。不响应健康检查的组件将被视为不可达,另一个组件将取而代之。原本计划发送到可能失败的组件的工作可能被发送到另一个组件,但原始组件可能仍在工作并完成相同的工作。最终结果是相同的工作可能会执行两次。很难避免这种情况。为了支持精确一次语义,您需要付出沉重的代价,包括开销、性能、延迟和复杂性。因此,大多数系统选择支持至少一次语义,这意味着可以多次执行相同的工作而不违反系统的数据完整性。这种属性被称为幂等性。幂等系统在多次执行操作时保持其状态。

自愈

当动态系统中发生组件故障时,通常希望系统能够自我修复。Kubernetes 复制控制器和副本集是自愈系统的很好例子,但故障可能远不止于 Pod。在上一章中,我们讨论了资源监控和节点问题检测。补救控制器是自愈概念的一个很好的例子。自愈始于自动检测问题,然后是自动解决。配额和限制有助于创建检查和平衡,以确保自动自愈不会因不可预测的情况(如 DDOS 攻击)而失控。

在本节中,我们考虑了创建可靠和高可用系统涉及的各种概念。在下一节中,我们将应用它们,并展示部署在 Kubernetes 集群上的系统的最佳实践。

高可用性最佳实践

构建可靠和高可用的分布式系统是一项重要的工作。在本节中,我们将检查一些最佳实践,使基于 Kubernetes 的系统能够可靠地运行,并在面对各种故障类别时可用。

创建高可用集群

要创建一个高可用的 Kubernetes 集群,主要组件必须是冗余的。这意味着etcd必须部署为一个集群(通常跨三个或五个节点),Kubernetes API 服务器必须是冗余的。辅助集群管理服务,如 Heapster 的存储,如果必要的话也可以部署为冗余。以下图表描述了一个典型的可靠和高可用的 Kubernetes 集群。有几个负载均衡的主节点,每个节点都包含整个主要组件以及一个etcd组件:

这不是配置高可用集群的唯一方式。例如,您可能更喜欢部署一个独立的etcd集群,以优化机器的工作负载,或者如果您的etcd集群需要比其他主节点更多的冗余。

自托管的 Kubernetes,其中控制平面组件部署为集群中的 pod 和有状态集,是简化控制平面组件的健壮性、灾难恢复和自愈的一个很好的方法,通过将 Kubernetes 应用于 Kubernetes。

使您的节点可靠

节点会失败,或者一些组件会失败,但许多故障是暂时的。基本的保证是确保 Docker 守护程序(或任何 CRI 实现)和 Kubelet 在发生故障时能够自动重启。

如果您运行 CoreOS,一个现代的基于 Debian 的操作系统(包括 Ubuntu >= 16.04),或者任何其他使用systemd作为其init机制的操作系统,那么很容易将Dockerkubelet部署为自启动守护程序:

systemctl enable docker 
systemctl enable kublet 

对于其他操作系统,Kubernetes 项目选择了 monit 作为高可用示例,但您可以使用任何您喜欢的进程监视器。

保护您的集群状态

Kubernetes 集群状态存储在etcd中。etcd集群被设计为超级可靠,并分布在多个节点上。利用这些功能对于一个可靠和高可用的 Kubernetes 集群非常重要。

集群化 etcd

您的 etcd 集群中应该至少有三个节点。如果您需要更可靠和冗余性,可以增加到五个、七个或任何其他奇数节点。节点数量必须是奇数,以便在网络分裂的情况下有明确的多数。

为了创建一个集群,etcd节点应该能够发现彼此。有几种方法可以实现这一点。我建议使用 CoreOS 的优秀的etcd-operator

操作员负责处理 etcd 操作的许多复杂方面,例如:

  • 创建和销毁

  • 调整大小

  • 故障转移

  • 滚动升级

  • 备份和恢复

安装 etcd 操作员

安装etcd-operator的最简单方法是使用 Helm-Kubernetes 包管理器。如果您尚未安装 Helm,请按照github.com/kubernetes/helm#install中给出的说明进行操作。

然后,初始化helm

> helm init 
Creating /Users/gigi.sayfan/.helm 
Creating /Users/gigi.sayfan/.helm/repository 
Creating /Users/gigi.sayfan/.helm/repository/cache 
Creating /Users/gigi.sayfan/.helm/repository/local 
Creating /Users/gigi.sayfan/.helm/plugins 
Creating /Users/gigi.sayfan/.helm/starters 
Creating /Users/gigi.sayfan/.helm/cache/archive 
Creating /Users/gigi.sayfan/.helm/repository/repositories.yaml 
Adding stable repo with URL: https://kubernetes-charts.storage.googleapis.com 
Adding local repo with URL: http://127.0.0.1:8879/charts 
$HELM_HOME has been configured at /Users/gigi.sayfan/.helm. 

Tiller (the Helm server-side component) has been installed into your Kubernetes Cluster. 
Happy Helming! 

我们将在第十三章中深入探讨 Helm,处理 Kubernetes 包管理器。目前,我们只是用它来安装etcd操作员。在支持 Kubernetes 1.8 的 Minikube 0.24.1 上(尽管 Kubernetes 1.10 已经发布),默认情况下存在一些权限问题。为了克服这些问题,我们需要创建一些角色和角色绑定。以下是rbac.yaml文件:

# Wide open access to the cluster (mostly for kubelet) 
kind: ClusterRole 
apiVersion: rbac.authorization.k8s.io/v1beta1 
metadata: 
  name: cluster-writer 
rules: 
  - apiGroups: ["*"] 
    resources: ["*"] 
    verbs: ["*"] 
  - nonResourceURLs: ["*"] 
    verbs: ["*"] 

--- 

# Full read access to the api and resources 
kind: ClusterRole 
apiVersion: rbac.authorization.k8s.io/v1beta1metadata: 
  name: cluster-reader 
rules: 
  - apiGroups: ["*"] 
    resources: ["*"] 
    verbs: ["get", "list", "watch"] 
  - nonResourceURLs: ["*"] 
    verbs: ["*"] 
--- 
# Give admin, kubelet, kube-system, kube-proxy god access 
kind: ClusterRoleBinding 
apiVersion: rbac.authorization.k8s.io/v1beta1metadata: 
  name: cluster-write 
subjects: 
  - kind: User 
    name: admin 
  - kind: User 
    name: kubelet 
  - kind: ServiceAccount 
    name: default 
    namespace: kube-system 
  - kind: User 
    name: kube-proxy 
roleRef: 
  kind: ClusterRole 
  name: cluster-writer 
  apiGroup: rbac.authorization.k8s.io 

您可以像应用其他 Kubernetes 清单一样应用它:

kubectl apply -f rbac.yaml.  

现在,我们终于可以安装etcd-operator了。我使用x作为一个简短的发布名称,以使输出更简洁。您可能希望使用更有意义的名称:

> helm install stable/etcd-operator --name x 
NAME:   x 
LAST DEPLOYED: Sun Jan  7 19:29:17 2018 
NAMESPACE: default 
STATUS: DEPLOYED 

RESOURCES: 
==> v1beta1/ClusterRole 
NAME                           AGE 
x-etcd-operator-etcd-operator  1s 

==> v1beta1/ClusterRoleBinding 
NAME                                   AGE 
x-etcd-operator-etcd-backup-operator   1s 
x-etcd-operator-etcd-operator          1s 
x-etcd-operator-etcd-restore-operator  1s 

==> v1/Service 
NAME                   TYPE       CLUSTER-IP    EXTERNAL-IP  PORT(S)    AGE 
etcd-restore-operator  ClusterIP  10.96.236.40  <none>       19999/TCP  1s 

==> v1beta1/Deployment 
NAME                                   DESIRED  CURRENT  UP-TO-DATE  AVAILABLE  AGE 
x-etcd-operator-etcd-backup-operator   1        1        1         0          1s 
x-etcd-operator-etcd-operator          1        1        1         0          1s 
x-etcd-operator-etcd-restore-operator  1        1        1         0          1s 

==> v1/ServiceAccount 
NAME                                   SECRETS  AGE 
x-etcd-operator-etcd-backup-operator   1        1s 
x-etcd-operator-etcd-operator          1        1s 
x-etcd-operator-etcd-restore-operator  1        1s 

NOTES: 
1\. etcd-operator deployed. 
  If you would like to deploy an etcd-cluster set cluster.enabled to true in values.yaml 
  Check the etcd-operator logs 
    export POD=$(kubectl get pods -l app=x-etcd-operator-etcd-operator --namespace default --output name) 
    kubectl logs $POD --namespace=default 

创建 etcd 集群

将以下内容保存到etcd-cluster.yaml中:

apiVersion: "etcd.database.coreos.com/v1beta2" 
kind: "EtcdCluster" 
metadata: 
  name: "etcd-cluster" 
spec: 
  size: 3 
  version: "3.2.13" 

要创建集群类型:

> k create -f etcd-cluster.yaml
etcdcluster "etcd-cluster" created

Let's verify the cluster pods were created properly:
> k get pods | grep etcd-cluster
etcd-cluster-0000                         1/1       Running   0          4m
etcd-cluster-0001                         1/1       Running   0          4m
etcd-cluster-0002                         1/1       Running   0          4m

验证 etcd 集群

一旦etcd集群正常运行,您可以访问etcdctl工具来检查集群状态和健康状况。Kubernetes 允许您通过exec命令(类似于 Docker exec)直接在 pod 或容器内执行命令。

以下是如何检查集群是否健康:

> k exec etcd-cluster-0000 etcdctl cluster-health
member 898a228a043c6ef0 is healthy: got healthy result from http://etcd-cluster-0001.etcd-cluster.default.svc:2379
member 89e2f85069640541 is healthy: got healthy result from http://etcd-cluster-0002.etcd-cluster.default.svc:2379
member 963265fbd20597c6 is healthy: got healthy result from http://etcd-cluster-0000.etcd-cluster.default.svc:2379
cluster is healthy  

以下是如何设置和获取键值对:

> k exec etcd-cluster-0000 etcdctl set test "Yeah, it works"
Yeah, it works
> k exec etcd-cluster-0000 etcdctl get test  

是的,它有效!

保护您的数据

保护集群状态和配置非常重要,但更重要的是保护您自己的数据。如果一些方式集群状态被损坏,您可以始终从头开始重建集群(尽管在重建期间集群将不可用)。但如果您自己的数据被损坏或丢失,您将陷入深深的麻烦。相同的规则适用;冗余是王道。然而,尽管 Kubernetes 集群状态非常动态,但您的许多数据可能不太动态。例如,许多历史数据通常很重要,可以进行备份和恢复。实时数据可能会丢失,但整个系统可以恢复到较早的快照,并且只会遭受暂时的损害。

运行冗余的 API 服务器

API 服务器是无状态的,可以从etcd集群中动态获取所有必要的数据。这意味着您可以轻松地运行多个 API 服务器,而无需在它们之间进行协调。一旦有多个 API 服务器运行,您可以在它们前面放置一个负载均衡器,使客户端对此毫无察觉。

在 Kubernetes 中运行领导者选举

一些主要组件,如调度程序和控制器管理器,不能同时处于活动状态。这将是一片混乱,因为多个调度程序尝试将相同的 pod 调度到多个节点或多次调度到同一节点。拥有高度可扩展的 Kubernetes 集群的正确方法是使这些组件以领导者选举模式运行。这意味着多个实例正在运行,但一次只有一个实例处于活动状态,如果它失败,另一个实例将被选为领导者并接替其位置。

Kubernetes 通过leader-elect标志支持这种模式。调度程序和控制器管理器可以通过将它们各自的清单复制到/etc/kubernetes/manifests来部署为 pod。

以下是调度程序清单中显示标志使用的片段:

command:
- /bin/sh
- -c
- /usr/local/bin/kube-scheduler --master=127.0.0.1:8080 --v=2 --leader-elect=true 1>>/var/log/kube-scheduler.log
2>&1

以下是控制器管理器清单中显示标志使用的片段:

- command:
- /bin/sh
- -c
- /usr/local/bin/kube-controller-manager --master=127.0.0.1:8080 --cluster-name=e2e-test-bburns
--cluster-cidr=10.245.0.0/16 --allocate-node-cidrs=true --cloud-provider=gce  --service-account-private-key-file=/srv/kubernetes/server.key
--v=2 --leader-elect=true 1>>/var/log/kube-controller-manager.log 2>&1
image: gcr.io/google_containers/kube-controller-manager:fda24638d51a48baa13c35337fcd4793 

请注意,这些组件无法像其他 pod 一样由 Kubernetes 自动重新启动,因为它们正是负责重新启动失败的 pod 的 Kubernetes 组件,因此如果它们失败,它们无法重新启动自己。必须已经有一个准备就绪的替代品正在运行。

应用程序的领导者选举

领导者选举对你的应用也可能非常有用,但实现起来非常困难。幸运的是,Kubernetes 来拯救了。有一个经过记录的程序,可以通过 Google 的leader-elector容器来支持你的应用进行领导者选举。基本概念是使用 Kubernetes 端点结合ResourceVersionAnnotations。当你将这个容器作为你的应用 pod 的 sidecar 时,你可以以非常简化的方式获得领导者选举的能力。

让我们使用三个 pod 运行leader-elector容器,并进行名为 election 的选举:

> kubectl run leader-elector --image=gcr.io/google_containers/leader-elector:0.5 --replicas=3 -- --election=election -http=0.0.0.0:4040

过一段时间,你会在你的集群中看到三个名为leader-elector-xxx的新 pod。

> kubectl get pods | grep elect
leader-elector-57746fd798-7s886                1/1       Running   0          39s
leader-elector-57746fd798-d94zx                1/1       Running   0          39s
leader-elector-57746fd798-xcljl                1/1       Running   0          39s 

好了。但是谁是主人?让我们查询选举端点:

    > kubectl get endpoints election -o json
    {
        "apiVersion": "v1",
        "kind": "Endpoints",
        "metadata": {
            "annotations": {
                "control-plane.alpha.kubernetes.io/leader": "{\"holderIdentity\":\"leader-elector-57746fd798-xcljl\",\"leaseDurationSeconds\":10,\"acquireTime\":\"2018-01-08T04:16:40Z\",\"renewTime\":\"2018-01-08T04:18:26Z\",\"leaderTransitions\":0}"
            },
            "creationTimestamp": "2018-01-08T04:16:40Z",
            "name": "election",
            "namespace": "default",
            "resourceVersion": "1090942",
            "selfLink": "/api/v1/namespaces/default/endpoints/election",
            "uid": "ba42f436-f42a-11e7-abf8-080027c94384"
        },
        "subsets": null
    }  

如果你仔细查看,你可以在metadata.annotations中找到它。为了方便检测,我推荐使用神奇的jq程序来切割和解析 JSON(stedolan.github.io/jq/)。它非常有用,可以解析 Kubernetes API 或kubectl的输出:

> kubectl get endpoints election -o json | jq -r .metadata.annotations[] | jq .holderIdentity
"leader-elector-57746fd798-xcljl"

为了证明领导者选举有效,让我们杀死领导者,看看是否选举出了新的领导者:

> kubectl delete pod leader-elector-916043122-10wjj
pod "leader-elector-57746fd798-xcljl" deleted 

我们有了一个新的领导者:

> kubectl get endpoints election -o json | jq -r .metadata.annotations[] | jq .holderIdentity
"leader-elector-57746fd798-d94zx"  

你也可以通过 HTTP 找到领导者,因为每个leader-elector容器都通过一个本地 web 服务器(运行在端口4040上)来暴露领导者,尽管一个代理:

> kubectl proxy 
In a separate console:

> curl http://localhost:8001/api/v1/proxy/namespaces/default/pods/leader-elector-57746fd798-d94zx:4040/ | jq .name
"leader-elector-57746fd798-d94zx"  

本地 web 服务器允许 leader-elector 容器作为同一个 pod 中主应用容器的 sidecar 容器运行。你的应用容器与leader-elector容器共享同一个本地网络,因此它可以访问http://localhost:4040并获取当前领导者的名称。只有与当选领导者共享 pod 的应用容器才会运行应用程序;其他 pod 中的应用容器将处于休眠状态。如果它们收到请求,它们将把请求转发给领导者,或者可以通过一些巧妙的负载均衡技巧自动将所有请求发送到当前的领导者。

使你的暂存环境高度可用

高可用性的设置很重要。如果您费心设置高可用性,这意味着存在高可用性系统的业务案例。因此,您希望在部署到生产环境之前测试可靠且高可用的集群(除非您是 Netflix,在那里您在生产环境中进行测试)。此外,理论上,对集群的任何更改都可能破坏高可用性,而不会影响其他集群功能。关键点是,就像其他任何事物一样,如果您不进行测试,就假设它不起作用。

我们已经确定您需要测试可靠性和高可用性。最好的方法是创建一个尽可能接近生产环境的分阶段环境。这可能会很昂贵。有几种方法可以管理成本:

  • 临时高可用性(HA)分阶段环境:仅在 HA 测试期间创建一个大型 HA 集群

  • 压缩时间:提前创建有趣的事件流和场景,输入并快速模拟情况

  • 将 HA 测试与性能和压力测试相结合:在性能和压力测试结束时,超载系统,看可靠性和高可用性配置如何处理负载

测试高可用性

测试高可用性需要计划和对系统的深入了解。每项测试的目标是揭示系统设计和/或实施中的缺陷,并提供足够的覆盖范围,如果测试通过,您将对系统的行为感到满意。

在可靠性和高可用性领域,这意味着您需要找出破坏系统并观察其自我修复的方法。

这需要几个部分,如下:

  • 可能故障的全面列表(包括合理的组合)

  • 对于每种可能的故障,系统应该如何做出清晰的响应

  • 诱发故障的方法

  • 观察系统反应的方法

这些部分都不是微不足道的。根据我的经验,最好的方法是逐步进行,并尝试提出相对较少的通用故障类别和通用响应,而不是详尽且不断变化的低级故障列表。

例如,一个通用的故障类别是节点无响应;通用的响应可能是重启节点。诱发故障的方法可以是停止节点的虚拟机(如果是虚拟机),观察应该是,尽管节点宕机,系统仍然根据标准验收测试正常运行。节点最终恢复正常,系统恢复正常。您可能还想测试许多其他事情,比如问题是否已记录,相关警报是否已发送给正确的人,以及各种统计数据和报告是否已更新。

请注意,有时故障无法在单一响应中解决。例如,在我们的节点无响应案例中,如果是硬件故障,那么重启是无济于事的。在这种情况下,第二种响应方式就会发挥作用,也许会启动、配置并连接到节点的新虚拟机。在这种情况下,您不能太通用,可能需要为节点上的特定类型的 pod/角色创建测试(etcd、master、worker、数据库和监控)。

如果您有高质量的要求,那么准备花费比生产环境更多的时间来设置适当的测试环境和测试。

最后,一个重要的观点是尽量不要侵入。这意味着,理想情况下,您的生产系统不会具有允许关闭部分系统或导致其配置为以减少容量运行进行测试的测试功能。原因是这会增加系统的攻击面,并且可能会因配置错误而意外触发。理想情况下,您可以在不修改将部署在生产环境中的代码或配置的情况下控制测试环境。使用 Kubernetes,通常很容易向暂存环境中的 pod 和容器注入自定义测试功能,这些功能可以与系统组件交互,但永远不会部署在生产环境中。

在本节中,我们看了一下实际上拥有可靠和高可用的集群所需的条件,包括 etcd、API 服务器、调度器和控制器管理器。我们考虑了保护集群本身以及您的数据的最佳实践,并特别关注了启动环境和测试的问题。

实时集群升级

在运行 Kubernetes 集群中涉及的最复杂和风险最高的任务之一是实时升级。不同版本系统的不同部分之间的交互通常很难预测,但在许多情况下是必需的。具有许多用户的大型集群无法承担维护期间的离线状态。攻击复杂性的最佳方法是分而治之。微服务架构在这里非常有帮助。您永远不会升级整个系统。您只是不断升级几组相关的微服务,如果 API 已更改,那么也会升级它们的客户端。一个经过良好设计的升级将至少保留向后兼容性,直到所有客户端都已升级,然后在几个版本中弃用旧的 API。

在本节中,我们将讨论如何使用各种策略升级您的集群,例如滚动升级和蓝绿升级。我们还将讨论何时适合引入破坏性升级与向后兼容升级。然后,我们将进入关键的模式和数据迁移主题。

滚动升级

滚动升级是逐渐将组件从当前版本升级到下一个版本的升级。这意味着您的集群将同时运行当前版本和新版本的组件。这里有两种情况需要考虑:

  • 新组件向后兼容

  • 新组件不向后兼容

如果新组件向后兼容,那么升级应该非常容易。在 Kubernetes 的早期版本中,您必须非常小心地使用标签管理滚动升级,并逐渐改变旧版本和新版本的副本数量(尽管kubectl滚动更新是复制控制器的便捷快捷方式)。但是,在 Kubernetes 1.2 中引入的部署资源使其变得更加容易,并支持副本集。它具有以下内置功能:

  • 运行服务器端(如果您的机器断开连接,它将继续运行)

  • 版本控制

  • 多个并发部署

  • 更新部署

  • 汇总所有 pod 的状态

  • 回滚

  • 金丝雀部署

  • 多种升级策略(滚动升级是默认值)

这是一个部署三个 NGINX pod 的部署示例清单:

apiVersion: extensions/v1beta1 
kind: Deployment 
metadata: 
  name: nginx-deployment 
spec: 
  replicas: 3 
  template: 
    metadata: 
      labels: 
        app: nginx 
    spec: 
      containers: 
      - name: nginx 
        image: nginx:1.7.9 
        ports: 
        - containerPort: 80 

资源种类是部署,它的名称是nginx-deployment,您可以在以后引用此部署(例如,用于更新或回滚)。最重要的部分当然是规范,其中包含一个 pod 模板。副本确定集群中将有多少个 pod,并且模板规范包含每个容器的配置:在这种情况下,只有一个容器。

要开始滚动更新,您需要创建部署资源:

$ kubectl create -f nginx-deployment.yaml --record  

您可以稍后使用以下命令查看部署的状态:

$ kubectl rollout status deployment/nginx-deployment  

复杂的部署

当您只想升级一个 pod 时,部署资源非常有用,但您经常需要升级多个 pod,并且这些 pod 有时存在版本相互依赖关系。在这种情况下,有时您必须放弃滚动更新或引入临时兼容层。例如,假设服务 A 依赖于服务 B。服务 B 现在有一个破坏性的变化。服务 A 的 v1 pod 无法与服务 B 的 v2 pod 进行交互操作。从可靠性和变更管理的角度来看,让服务 B 的 v2 pod 支持旧的和新的 API 是不可取的。在这种情况下,解决方案可能是引入一个适配器服务,该服务实现了 B 服务的 v1 API。该服务将位于 A 和 B 之间,并将跨版本转换请求和响应。这增加了部署过程的复杂性,并需要多个步骤,但好处是 A 和 B 服务本身很简单。您可以在不兼容版本之间进行滚动更新,一旦所有人升级到 v2(所有 A pod 和所有 B pod),所有间接性都将消失。

蓝绿升级

滚动更新对可用性来说非常好,但有时管理正确的滚动更新涉及的复杂性被认为太高,或者它增加了大量工作,推迟了更重要的项目。在这些情况下,蓝绿升级提供了一个很好的替代方案。使用蓝绿发布,您准备了一个完整的生产环境的副本,其中包含新版本。现在你有两个副本,旧的(蓝色)和新的(绿色)。蓝色和绿色哪个是哪个并不重要。重要的是你有两个完全独立的生产环境。目前,蓝色是活动的并处理所有请求。您可以在绿色上运行所有测试。一旦满意,您可以切换绿色变为活动状态。如果出现问题,回滚同样容易;只需从绿色切换回蓝色。我在这里优雅地忽略了存储和内存状态。这种立即切换假设蓝色和绿色只由无状态组件组成,并共享一个公共持久层。

如果存储发生了变化或对外部客户端可访问的 API 发生了破坏性变化,则需要采取额外的步骤。例如,如果蓝色和绿色有自己的存储,那么所有传入的请求可能需要同时发送到蓝色和绿色,并且绿色可能需要从蓝色那里摄取历史数据以在切换之前同步。

管理数据合同变更

数据合同描述数据的组织方式。这是结构元数据的一个总称。数据库模式是最典型的例子。最常见的例子是关系数据库模式。其他例子包括网络负载、文件格式,甚至字符串参数或响应的内容。如果有配置文件,那么这个配置文件既有文件格式(JSON、YAML、TOML、XML、INI 和自定义格式),也有一些内部结构,描述了什么样的层次结构、键、值和数据类型是有效的。有时,数据合同是显式的,有时是隐式的。无论哪种方式,您都需要小心管理它,否则当读取、解析或验证数据时遇到不熟悉的结构时,就会出现运行时错误。

迁移数据

数据迁移是一件大事。如今,许多系统管理着以 TB、PB 或更多为单位的数据。在可预见的未来,收集和管理的数据量将继续增加。数据收集的速度超过了硬件创新的速度。关键点是,如果您有大量数据需要迁移,这可能需要一段时间。在以前的一家公司,我负责一个项目,将近 100TB 的数据从一个传统系统的 Cassandra 集群迁移到另一个 Cassandra 集群。

第二个 Cassandra 集群具有不同的架构,并且由 Kubernetes 集群全天候访问。该项目非常复杂,因此在紧急问题出现时,它一直被推迟。传统系统仍然与新一代系统并存,远远超出了最初的估计时间。

有很多机制可以将数据分割并发送到两个集群,但是我们遇到了新系统的可扩展性问题,我们必须在继续之前解决这些问题。历史数据很重要,但不必以与最近的热数据相同的服务水平访问。因此,我们着手进行了另一个项目,将历史数据发送到更便宜的存储中。当然,这意味着客户库或前端服务必须知道如何查询两个存储并合并结果。当您处理大量数据时,您不能认为任何事情都是理所当然的。您会遇到工具、基础设施、第三方依赖和流程的可扩展性问题。大规模不仅仅是数量的变化,通常也是质量的变化。不要指望一切都会顺利进行。这远不止是从 A 复制一些文件到 B。

弃用 API

API 的弃用有两种情况:内部和外部。内部 API 是由您和您的团队或组织完全控制的组件使用的 API。您可以确保所有 API 用户将在短时间内升级到新的 API。外部 API 是由您直接影响范围之外的用户或服务使用的。有一些灰色地带的情况,您在一个庞大的组织(比如谷歌)工作,甚至内部 API 可能需要被视为外部 API。如果您很幸运,所有外部 API 都由自更新的应用程序或通过您控制的 Web 界面使用。在这些情况下,API 实际上是隐藏的,您甚至不需要发布它。

如果您有很多用户(或者一些非常重要的用户)使用您的 API,您应该非常谨慎地考虑弃用。弃用 API 意味着您强迫用户更改其应用程序以与您合作,或者保持与早期版本的锁定。

有几种方法可以减轻痛苦:

  • 不要弃用。扩展现有 API 或保持以前的 API 活动。尽管这有时很简单,但会增加测试负担。

  • 为您的目标受众提供所有相关编程语言的客户端库。这总是一个很好的做法。它允许您对底层 API 进行许多更改,而不会干扰用户(只要您保持编程语言接口稳定)。

  • 如果必须弃用,请解释原因,为用户提供充足的升级时间,并尽可能提供支持(例如,带有示例的升级指南)。您的用户会感激的。

大型集群的性能、成本和设计权衡

在前一节中,我们看了现场集群升级。我们探讨了各种技术以及 Kubernetes 如何支持它们。我们还讨论了一些困难的问题,比如破坏性变化、数据合同变化、数据迁移和 API 弃用。在本节中,我们将考虑大型集群的各种选项和配置,具有不同的可靠性和高可用性属性。当您设计您的集群时,您需要了解您的选项,并根据您组织的需求进行明智的选择。

在本节中,我们将涵盖各种可用性要求,从尽力而为一直到零停机的圣杯,对于每个可用性类别,我们将考虑从性能和成本的角度来看它意味着什么。

可用性要求

不同的系统对可靠性和可用性有非常不同的要求。此外,不同的子系统有非常不同的要求。例如,计费系统总是高优先级,因为如果计费系统停机,您就无法赚钱。然而,即使在计费系统内部,如果有时无法争议费用,从业务角度来看可能也可以接受。

快速恢复

快速恢复是高可用集群的另一个重要方面。某些时候会出现问题。您的不可用时钟开始运行。您能多快恢复正常?

有时候,这不取决于你。例如,如果你的云服务提供商出现故障(而且你没有实施联合集群,我们稍后会讨论),那么你只能坐下来等待他们解决问题。但最有可能的罪魁祸首是最近部署的问题。当然,还有与时间相关的问题,甚至与日历相关的问题。你还记得 2012 年 2 月 29 日使微软 Azure 崩溃的闰年错误吗?

快速恢复的典范当然是蓝绿部署-如果在发现问题时保持之前的版本运行。

另一方面,滚动更新意味着如果问题早期被发现,那么大多数你的 pod 仍在运行之前的版本。

数据相关的问题可能需要很长时间才能解决,即使你的备份是最新的,而且你的恢复程序实际上是有效的(一定要定期测试)。

像 Heptio Ark 这样的工具在某些情况下可以帮助,它可以创建集群的快照备份,以防出现问题并且你不确定如何解决。

尽力而为

尽力而为意味着,反直觉地,根本没有任何保证。如果有效,太好了!如果不起作用-哦,好吧。你打算怎么办?这种可靠性和可用性水平可能适用于经常更改的内部组件,而使它们健壮的努力不值得。这也可能适用于作为测试版发布到公众的服务。

尽力而为对开发人员来说是很好的。开发人员可以快速移动并破坏事物。他们不担心后果,也不必经历严格测试和批准的考验。尽力而为服务的性能可能比更健壮的服务更好,因为它通常可以跳过昂贵的步骤,比如验证请求、持久化中间结果和复制数据。然而,另一方面,更健壮的服务通常经过了大量优化,其支持硬件也经过了对其工作负载的精细调整。尽力而为服务的成本通常较低,因为它们不需要使用冗余,除非运营商忽视了基本的容量规划,只是不必要地过度配置。

在 Kubernetes 的背景下,一个重要问题是集群提供的所有服务是否都是尽力而为的。如果是这种情况,那么集群本身就不需要高可用性。你可能只需要一个单一的主节点和一个单一的etcd实例,而 Heapster 或其他监控解决方案可能不需要部署。

维护窗口

在有维护窗口的系统中,专门的时间用于执行各种维护活动,如应用安全补丁、升级软件、修剪日志文件和数据库清理。在维护窗口期间,系统(或子系统)将不可用。这是计划中的非工作时间,并且通常会通知用户。维护窗口的好处是你不必担心维护操作会如何与系统中的实时请求交互。这可以极大地简化操作。系统管理员和开发人员一样喜欢维护窗口和尽力而为的系统。

当然,缺点是系统在维护期间会停机。这可能只适用于用户活动受限于特定时间(美国办公时间或仅工作日)的系统。

使用 Kubernetes,你可以通过将所有传入的请求重定向到负载均衡器上的网页(或 JSON 响应)来进行维护窗口。

但在大多数情况下,Kubernetes 的灵活性应该允许你进行在线维护。在极端情况下,比如升级 Kubernetes 版本,或从 etcd v2 切换到 etcd v3,你可能需要使用维护窗口。蓝绿部署是另一种选择。但集群越大,蓝绿部署的成本就越高,因为你必须复制整个生产集群,这既昂贵又可能导致你遇到配额不足的问题。

零停机

最后,我们来到了零停机系统。没有所谓的零停机系统。所有系统都会失败,所有软件系统肯定会失败。有时,故障严重到足以使系统或其某些服务宕机。把零停机看作是最佳努力的分布式系统设计。你设计零停机是指你提供了大量的冗余和机制来解决预期的故障,而不会使系统宕机。一如既往,要记住,即使有零停机的商业案例,也不意味着每个组件都必须是。

零停机计划如下:

  • 每个级别都要有冗余:这是一个必要的条件。你的设计中不能有单一的故障点,因为一旦它失败,你的系统就会宕机。

  • 自动热插拔故障组件:冗余只有在冗余组件能够在原始组件失败后立即启动时才有效。一些组件可以共享负载(例如无状态的 Web 服务器),因此不需要明确的操作。在其他情况下,比如 Kubernetes 调度器和控制器管理器,你需要进行领导者选举,以确保集群保持运行。

  • 大量的监控和警报以及早发现问题:即使设计再谨慎,你可能会漏掉一些东西,或者一些隐含的假设可能会使你的设计失效。通常这样微妙的问题会悄悄地出现在你身上,如果足够关注,你可能会在它变成全面系统故障之前发现它。例如,假设当磁盘空间超过 90%时有一个清理旧日志文件的机制,但出于某种原因它不起作用。如果你设置一个警报,当磁盘空间超过 95%时,那么你就能发现并防止系统故障。

  • 部署到生产环境之前的顽强测试:全面的测试已被证明是提高质量的可靠方法。为一个运行庞大的分布式系统的大型 Kubernetes 集群做全面测试是一项艰苦的工作,但你需要这样做。你应该测试什么?一切。没错,为了实现零停机,你需要同时测试应用程序和基础设施。你的 100%通过的单元测试是一个很好的开始,但它们并不能提供足够的信心,即当你在生产 Kubernetes 集群上部署应用程序时,它仍然会按预期运行。最好的测试当然是在蓝绿部署或相同集群后的生产集群上进行。如果没有完全相同的集群,可以考虑一个尽可能与生产环境相符的暂存环境。以下是你应该运行的测试列表。每个测试都应该全面,因为如果你留下一些未经测试的东西,它可能是有问题的:

    • 单元测试
  • 验收测试

  • 性能测试

  • 压力测试

  • 回滚测试

  • 数据恢复测试

  • 渗透测试

听起来疯狂吗?很好。零停机的大规模系统很难。微软、谷歌、亚马逊、Facebook 和其他大公司有数以万计的软件工程师(合计)专门负责基础设施、运营,并确保系统正常运行。

  • 保留原始数据:对于许多系统来说,数据是最关键的资产。如果保留原始数据,可以从后续发生的任何数据损坏和处理数据丢失中恢复。这并不能真正帮助实现零停机,因为重新处理原始数据可能需要一段时间,但它可以帮助实现零数据丢失,这通常更为重要。这种方法的缺点是,与处理后的数据相比,原始数据通常要大得多。一个好的选择可能是将原始数据存储在比处理数据更便宜的存储设备中。

  • 感知到的正常运行时间作为最后的手段:好吧,系统的某个部分出现了问题。你可能仍然能够保持一定水平的服务。在许多情况下,你可能可以访问略旧的数据版本,或者让用户访问系统的其他部分。这并不是一个很好的用户体验,但从技术上讲,系统仍然可用。

性能和数据一致性

当你开发或操作分布式系统时,CAP 定理应该时刻放在脑后。CAP 代表一致性(Consistency)、可用性(Availability)和分区容忍性(Partition Tolerance)。该定理表示你最多只能拥有其中的两个。由于任何分布式系统在实践中都可能遭受网络分区,你可以在 CP 和 AP 之间做出选择。CP 意味着为了保持一致性,系统在网络分区发生时将不可用。AP 意味着系统将始终可用,但可能不一致。例如,来自不同分区的读取可能返回不同的结果,因为其中一个分区没有接收到写入。在本节中,我们将专注于高可用系统,即 AP。为了实现高可用性,我们必须牺牲一致性,但这并不意味着我们的系统将具有损坏或任意数据。关键词是最终一致性。我们的系统可能会落后一点,并提供对略微陈旧数据的访问,但最终你会得到你期望的结果。当你开始考虑最终一致性时,这将为潜在的性能改进打开大门。

举例来说,如果某个重要数值频繁更新(例如,每秒一次),但你只每分钟发送一次数值,那么你已经将网络流量减少了 60 倍,平均只落后实时更新 30 秒。这非常重要,非常巨大。你刚刚让系统能够处理 60 倍的用户或请求。

总结

在本章中,我们看了可靠且高可用的大规模 Kubernetes 集群。这可以说是 Kubernetes 的甜蜜点。虽然能够编排运行少量容器的小集群很有用,但并非必需,但在大规模情况下,你必须有一个编排解决方案,可以信任其与系统一起扩展,并提供工具和最佳实践来实现这一点。

你现在对分布式系统中可靠性和高可用性的概念有了扎实的理解。你已经深入了解了运行可靠且高可用的 Kubernetes 集群的最佳实践。你已经探讨了活动 Kubernetes 集群升级的微妙之处,并且可以在可靠性和可用性水平以及其性能和成本方面做出明智的设计选择。

在下一章中,我们将讨论 Kubernetes 中重要的安全主题。我们还将讨论保护 Kubernetes 的挑战和涉及的风险。您将学习有关命名空间、服务账户、准入控制、身份验证、授权和加密的所有内容。

第五章:配置 Kubernetes 安全性、限制和账户

在第四章中,高可用性和可靠性,我们讨论了可靠且高可用的 Kubernetes 集群,基本概念,最佳实践,如何进行实时集群升级,以及关于性能和成本的许多设计权衡。

在本章中,我们将探讨安全这一重要主题。Kubernetes 集群是由多个层次的相互作用组件组成的复杂系统。在运行关键应用程序时,不同层的隔离和分隔非常重要。为了保护系统并确保对资源、能力和数据的适当访问,我们必须首先了解 Kubernetes 作为一个运行未知工作负载的通用编排平台所面临的独特挑战。然后,我们可以利用各种安全、隔离和访问控制机制,确保集群和运行在其上的应用程序以及数据都是安全的。我们将讨论各种最佳实践以及何时适合使用每种机制。

在本章的结尾,您将对 Kubernetes 安全挑战有很好的理解。您将获得如何加固 Kubernetes 以抵御各种潜在攻击的实际知识,建立深度防御,并且甚至能够安全地运行多租户集群,同时为不同用户提供完全隔离以及对他们在集群中的部分拥有完全控制的能力。

理解 Kubernetes 安全挑战

Kubernetes 是一个非常灵活的系统,以通用方式管理非常低级别的资源。Kubernetes 本身可以部署在许多操作系统和硬件或虚拟机解决方案上,可以部署在本地或云端。Kubernetes 运行由运行时实现的工作负载,通过定义良好的运行时接口与之交互,但不了解它们是如何实现的。Kubernetes 操作关键资源,如网络、DNS 和资源分配,代表或为了应用程序服务,而对这些应用程序一无所知。这意味着 Kubernetes 面临着提供良好的安全机制和能力的艰巨任务,以便应用程序管理员可以使用,同时保护自身和应用程序管理员免受常见错误的影响。

在本节中,我们将讨论 Kubernetes 集群的几个层次或组件的安全挑战:节点、网络、镜像、Pod 和容器。深度防御是一个重要的安全概念,要求系统在每个层面都保护自己,既要减轻渗透其他层的攻击,也要限制入侵的范围和损害。认识到每个层面的挑战是向深度防御迈出的第一步。

节点挑战

节点是运行时引擎的主机。如果攻击者能够访问节点,这是一个严重的威胁。它至少可以控制主机本身和运行在其上的所有工作负载。但情况会变得更糟。节点上运行着一个与 API 服务器通信的 kubelet。一个复杂的攻击者可以用修改过的版本替换 kubelet,并通过与 Kubernetes API 服务器正常通信来有效地逃避检测,而不是运行预定的工作负载,收集有关整个集群的信息,并通过发送恶意消息来破坏 API 服务器和集群的其余部分。节点将可以访问共享资源和秘密,这可能使其渗透得更深。节点入侵非常严重,既因为可能造成的损害,也因为事后很难检测到它。

节点也可能在物理级别上受到损害。这在裸机上更相关,您可以知道哪些硬件分配给了 Kubernetes 集群。

另一个攻击向量是资源耗尽。想象一下,您的节点成为了一个与您的 Kubernetes 集群无关的机器人网络的一部分,它只运行自己的工作负载并耗尽 CPU 和内存。这里的危险在于 Kubernetes 和您的基础设施可能会自动扩展并分配更多资源。

另一个问题是安装调试和故障排除工具,或者在自动部署之外修改配置。这些通常是未经测试的,如果被遗留并激活,它们至少会导致性能下降,但也可能引起更严重的问题。至少会增加攻击面。

在涉及安全性的地方,这是一个数字游戏。您希望了解系统的攻击面以及您的脆弱性。让我们列出所有的节点挑战:

  • 攻击者控制主机

  • 攻击者替换 kubelet

  • 攻击者控制运行主要组件(API 服务器、调度器和控制器管理器)的节点

  • 攻击者获得对节点的物理访问权限

  • 攻击者耗尽与 Kubernetes 集群无关的资源

  • 通过安装调试和故障排除工具或更改配置造成自我伤害

任何重要的 Kubernetes 集群至少跨越一个网络。与网络相关的挑战很多。您需要非常详细地了解系统组件是如何连接的。哪些组件应该相互通信?它们使用什么网络协议?使用什么端口?它们交换什么数据?您的集群如何与外部世界连接?

暴露端口和服务的复杂链路:

  • 容器到主机

  • 主机到内部网络中的主机

  • 主机到世界

使用覆盖网络(将在第十章中更多讨论,高级 Kubernetes 网络)可以帮助进行深度防御,即使攻击者获得对 Docker 容器的访问权限,它们也会被隔离,无法逃脱到底层网络基础设施。

发现组件也是一个很大的挑战。这里有几个选项,比如 DNS、专用发现服务和负载均衡器。每种方法都有一套利弊,需要仔细规划和洞察力才能在您的情况下得到正确的解决方案。

确保两个容器能够找到彼此并交换信息非常重要。

您需要决定哪些资源和端点应该是公开访问的。然后,您需要想出一个适当的方法来对用户和服务进行身份验证,并授权它们对资源进行操作。

敏感数据必须在进入和离开集群时进行加密,有时也需要在静态状态下进行加密。这意味着密钥管理和安全密钥交换,这是安全领域中最难解决的问题之一。

如果您的集群与其他 Kubernetes 集群或非 Kubernetes 进程共享网络基础设施,那么您必须对隔离和分离非常谨慎。

这些要素包括网络策略、防火墙规则和软件定义网络(SDN)。这个方案通常是定制的。这在本地和裸机集群中尤其具有挑战性。让我们回顾一下:

  • 制定连接计划

  • 选择组件、协议和端口

  • 找出动态发现

  • 公共与私有访问

  • 身份验证和授权

  • 设计防火墙规则

  • 决定网络策略

  • 密钥管理和交换

在网络层面,容器、用户和服务之间相互找到并交流变得更加容易,与此同时,也需要限制访问并防止网络攻击或对网络本身的攻击之间保持不断的紧张关系。

这些挑战中许多并非特定于 Kubernetes。然而,Kubernetes 是一个管理关键基础设施并处理低级网络的通用平台,这使得有必要考虑动态和灵活的解决方案,可以将系统特定要求整合到 Kubernetes 中。

图像挑战

Kubernetes 运行符合其运行时引擎之一的容器。它不知道这些容器在做什么(除了收集指标)。您可以通过配额对容器施加一定的限制。您还可以通过网络策略限制它们对网络其他部分的访问。然而,最终,容器确实需要访问主机资源、网络中的其他主机、分布式存储和外部服务。图像决定了容器的行为。图像存在两类问题:

  • 恶意图像

  • 易受攻击的图像

恶意图像是包含由攻击者设计的代码或配置的图像,用于造成一些伤害或收集信息。恶意代码可以被注入到您的图像准备流水线中,包括您使用的任何图像存储库。或者,您可能安装了被攻击的第三方图像,这些图像现在可能包含恶意代码。

易受攻击的图像是您设计的图像(或您安装的第三方图像),恰好包含一些漏洞,允许攻击者控制正在运行的容器或造成其他伤害,包括以后注入他们自己的代码。

很难说哪一类更糟。在极端情况下,它们是等价的,因为它们允许完全控制容器。其他防御措施已经就位(记得深度防御吗?),并且对容器施加的限制将决定它可以造成多大的破坏。减少恶意镜像的危险非常具有挑战性。使用微服务的快速移动公司可能每天生成许多镜像。验证镜像也不是一件容易的事。例如,考虑 Docker 镜像由多层组成。包含操作系统的基础镜像可能在发现新漏洞时随时变得容易受攻击。此外,如果您依赖他人准备的基础镜像(非常常见),那么恶意代码可能会进入这些您无法控制并且绝对信任的基础镜像中。

总结镜像挑战:

  • Kubernetes 不知道镜像在做什么

  • Kubernetes 必须为指定功能提供对敏感资源的访问

  • 保护镜像准备和交付管道(包括镜像仓库)是困难的

  • 快速开发和部署新镜像的速度可能与仔细审查更改的冲突

  • 包含操作系统的基础镜像很容易过时并变得容易受攻击

  • 基础镜像通常不受您控制,更容易受到恶意代码的注入

  • 集成静态镜像分析器,如 CoreOS Clair,可以帮助很多。

配置和部署的挑战

Kubernetes 集群是远程管理的。各种清单和策略确定了集群在每个时间点的状态。如果攻击者能够访问具有对集群的管理控制的机器,他们可以造成严重破坏,比如收集信息、注入恶意镜像、削弱安全性和篡改日志。通常情况下,错误和失误可能同样有害,影响重要的安全措施,并使集群容易受到攻击。如今,拥有对集群的管理访问权限的员工经常在家或咖啡店远程工作,并随身携带笔记本电脑,他们离打开防护门只有一个 kubectl 命令的距离。

让我们重申挑战:

  • Kubernetes 是远程管理的

  • 具有远程管理访问权限的攻击者可以完全控制集群

  • 配置和部署通常比代码更难测试

  • 远程或外出办公的员工面临延长的暴露风险,使攻击者能够以管理员权限访问他们的笔记本电脑或手机

Pod 和容器方面的挑战

在 Kubernetes 中,pod 是工作单位,包含一个或多个容器。Pod 只是一个分组和部署构造,但在实践中,部署在同一个 pod 中的容器通常通过直接机制进行交互。所有容器共享相同的本地主机网络,并经常共享来自主机的挂载卷。同一 pod 中容器之间的轻松集成可能导致主机的部分暴露给所有容器。这可能允许一个恶意或易受攻击的恶意容器打开对其他容器的升级攻击的途径,然后接管节点本身。主要附加组件通常与主要组件共同存在,并呈现出这种危险,特别是因为它们中的许多是实验性的。对于在每个节点上运行 pod 的守护程序集也是如此。

多容器 pod 的挑战包括以下内容:

  • 相同的 pod 容器共享本地主机网络

  • 相同的 pod 容器有时会共享主机文件系统上的挂载卷

  • 恶意容器可能会影响 pod 中的其他容器

  • 如果与访问关键节点资源的其他容器共同存在,恶意容器更容易攻击节点

  • 实验性的附加组件与主要组件共同存在时,可能是实验性的并且安全性较低

组织、文化和流程方面的挑战

安全性通常与生产力相矛盾。这是一种正常的权衡,无需担心。传统上,当开发人员和运营是分开的时,这种冲突是在组织层面上管理的。开发人员推动更多的生产力,并将安全要求视为业务成本。运营控制生产环境,并负责访问和安全程序。DevOps 运动打破了开发人员和运营之间的壁垒。现在,开发速度往往占据主导地位。诸如持续部署-在没有人为干预的情况下每天部署多次-这样的概念在大多数组织中是闻所未闻的。Kubernetes 是为这种新型云原生应用程序的世界而设计的。然而,它是基于谷歌的经验开发的。谷歌有大量时间和熟练的专家来开发平衡快速部署和安全性的适当流程和工具。对于较小的组织,这种平衡可能非常具有挑战性,安全性可能会受到影响。

采用 Kubernetes 的组织面临的挑战如下:

  • 控制 Kubernetes 操作的开发人员可能不太关注安全性

  • 开发速度可能被认为比安全性更重要

  • 持续部署可能会使难以在达到生产之前检测到某些安全问题

  • 较小的组织可能没有足够的知识和专业技能来正确管理 Kubernetes 集群的安全性

在本节中,我们回顾了在尝试构建安全的 Kubernetes 集群时所面临的许多挑战。这些挑战大多数并非特定于 Kubernetes,但使用 Kubernetes 意味着系统的大部分是通用的,并且不知道系统正在做什么。在试图锁定系统时,这可能会带来问题。这些挑战分布在不同的层次上:

  • 节点挑战

  • 网络挑战

  • 镜像挑战

  • 配置和部署挑战

  • Pod 和容器挑战

  • 组织和流程挑战

在下一节中,我们将看一下 Kubernetes 提供的设施,以解决其中一些挑战。许多挑战需要在更大的系统级别上找到解决方案。重要的是要意识到仅仅使用所有 Kubernetes 安全功能是不够的。

加固 Kubernetes

前一节列出了开发人员和管理员在部署和维护 Kubernetes 集群时面临的各种安全挑战。在本节中,我们将专注于 Kubernetes 提供的设计方面、机制和功能,以解决其中一些挑战。通过审慎使用功能,如服务账户、网络策略、身份验证、授权、准入控制、AppArmor 和秘密,您可以达到一个相当良好的安全状态。

记住,Kubernetes 集群是一个更大系统的一部分,包括其他软件系统、人员和流程。Kubernetes 不能解决所有问题。您应始终牢记一般安全原则,如深度防御、需要知道的基础和最小特权原则。此外,记录您认为在攻击事件中可能有用的所有内容,并设置警报,以便在系统偏离其状态时进行早期检测。这可能只是一个错误,也可能是一次攻击。无论哪种情况,您都希望了解并做出响应。

了解 Kubernetes 中的服务账户

Kubernetes 在集群外部管理常规用户,用于连接到集群的人员(例如,通过kubectl命令),并且它还有服务账户。

常规用户是全局的,可以访问集群中的多个命名空间。服务账户受限于一个命名空间。这很重要。它确保了命名空间的隔离,因为每当 API 服务器从一个 pod 接收到请求时,其凭据只适用于其自己的命名空间。

Kubernetes 代表 pod 管理服务账户。每当 Kubernetes 实例化一个 pod 时,它会为 pod 分配一个服务账户。当 pod 进程与 API 服务器交互时,服务账户将标识所有的 pod 进程。每个服务账户都有一组凭据挂载在一个秘密卷中。每个命名空间都有一个名为default的默认服务账户。当您创建一个 pod 时,它会自动分配默认服务账户,除非您指定其他服务账户。

您可以创建额外的服务账户。创建一个名为custom-service-account.yaml的文件,其中包含以下内容:

apiVersion: v1 
kind: ServiceAccount 
metadata: 
  name: custom-service-account 
Now type the following: 
kubectl create -f custom-service-account.yaml 
That will result in the following output: 
serviceaccount "custom-service-account" created 
Here is the service account listed alongside the default service account: 
> kubectl get serviceAccounts 
NAME                     SECRETS   AGE 
custom-service-account   1         3m 
default                  1         29d 

请注意,为您的新服务账户自动创建了一个秘密。

要获取更多详细信息,请输入以下内容:

> kubectl get serviceAccounts/custom-service-account -o yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  creationTimestamp: 2018-01-15T18:24:40Z
  name: custom-service-account
  namespace: default
  resourceVersion: "1974321"
  selfLink: /api/v1/namespaces/default/serviceaccounts/custom-service-account
  uid: 59bc3515-fa21-11e7-beab-080027c94384
  secrets:
  - name: custom-service-account-token-w2v7v  

您可以通过输入以下内容查看秘密本身,其中包括一个ca.crt文件和一个令牌:

kubectl get secrets/custom-service-account-token-w2v7v -o yaml  

Kubernetes 如何管理服务账户?

API 服务器有一个名为服务账户准入控制器的专用组件。它负责在 pod 创建时检查是否有自定义服务账户,如果有,则确保自定义服务账户存在。如果没有指定服务账户,则分配默认服务账户。

它还确保 pod 具有ImagePullSecrets,当需要从远程镜像注册表中拉取镜像时是必要的。如果 pod 规范没有任何密钥,它将使用服务账户的ImagePullSecrets

最后,它添加了一个包含 API 访问令牌的卷和一个volumeSource挂载在/var/run/secrets/kubernetes.io/serviceaccount上。

API 令牌是由另一个名为令牌控制器的组件创建并添加到密钥中,每当创建服务账户时。令牌控制器还监视密钥,并在密钥被添加或从服务账户中删除时添加或删除令牌。

服务账户控制器确保每个命名空间都存在默认的服务账户。

访问 API 服务器

访问 API 需要一系列步骤,包括身份验证、授权和准入控制。在每个阶段,请求可能会被拒绝。每个阶段由多个链接在一起的插件组成。以下图表说明了这一点:

用户身份验证

当您首次创建集群时,会为您创建一个客户端证书和密钥。Kubectl使用它们在端口443上通过 TLS 与 API 服务器进行身份验证,反之亦然(加密的 HTTPS 连接)。您可以通过检查您的.kube/config文件找到您的客户端密钥和证书:

> cat ~/.kube/config | grep client

client-certificate: /Users/gigi.sayfan/.minikube/client.crt
client-key: /Users/gigi.sayfan/.minikube/client.key  

请注意,如果多个用户需要访问集群,创建者应以安全的方式向其他用户提供客户端证书和密钥。

这只是与 Kubernetes API 服务器本身建立基本的信任。您还没有进行身份验证。各种身份验证模块可能会查看请求并检查各种额外的客户端证书、密码、持有者令牌和 JWT 令牌(用于服务账户)。大多数请求需要经过身份验证的用户(常规用户或服务账户),尽管也有一些匿名请求。如果请求未能通过所有身份验证器进行身份验证,它将被拒绝,并返回 401 HTTP 状态码(未经授权,这有点名不副实)。

集群管理员通过向 API 服务器提供各种命令行参数来确定要使用的认证策略:

  • --client-ca-file=(用于文件中指定的 x509 客户端证书)

  • --token-auth-file=(用于文件中指定的持有者令牌)

  • --basic-auth-file=(用于文件中指定的用户/密码对)

  • --experimental-bootstrap-token-auth(用于kubeadm使用的引导令牌)

服务账户使用自动加载的认证插件。管理员可以提供两个可选标志:

  • --service-account-key-file=(用于签署持有者令牌的 PEM 编码密钥。如果未指定,将使用 API 服务器的 TLS 私钥。)

  • --service-account-lookup(如果启用,从 API 中删除的令牌将被撤销。)

还有其他几种方法,例如开放 ID 连接,Web 钩子,Keystone(OpenStack 身份服务)和认证代理。主题是认证阶段是可扩展的,并且可以支持任何认证机制。

各种认证插件将检查请求,并根据提供的凭据,将关联以下属性:

  • 用户名(用户友好的名称)

  • uid(唯一标识符,比用户名更一致)

  • (用户所属的一组组名)

  • 额外字段(将字符串键映射到字符串值)

认证器完全不知道特定用户被允许做什么。他们只是将一组凭据映射到一组身份。授权者的工作是弄清楚请求对经过身份验证的用户是否有效。任何认证器接受凭据时,认证成功。认证器运行的顺序是未定义的。

模拟

用户可以模拟不同的用户(经过适当授权)。例如,管理员可能希望以权限较低的不同用户身份解决一些问题。这需要将模拟头传递给 API 请求。这些头是:

  • 模拟用户:要扮演的用户名。

  • 模拟组:这是要扮演的组名,可以多次提供以设置多个组。这是可选的,需要模拟用户

  • 模拟额外-(额外名称):这是用于将额外字段与用户关联的动态标头。这是可选的,需要模拟用户

使用kubectl,您可以传递--as--as-group参数。

授权请求

一旦用户经过身份验证,授权就开始了。Kubernetes 具有通用的授权语义。一组授权模块接收请求,其中包括经过身份验证的用户名和请求的动词(listgetwatchcreate等)。与身份验证不同,所有授权插件都将有机会处理任何请求。如果单个授权插件拒绝请求或没有插件发表意见,则将以403 HTTP 状态码(禁止)拒绝请求。只有在至少有一个插件被接受且没有其他插件拒绝时,请求才会继续。

集群管理员通过指定--authorization-mode命令行标志来确定要使用哪些授权插件,这是一个逗号分隔的插件名称列表。

支持以下模式:

  • --authorization-mode=AlwaysDeny拒绝所有请求;在测试期间很有用。

  • -authorization-mode=AlwaysAllow允许所有请求;如果不需要授权,则使用。

  • --authorization-mode=ABAC允许使用简单的、基于本地文件的、用户配置的授权策略。ABAC代表基于属性的访问控制

  • --authorization-mode=RBAC是一种基于角色的机制,授权策略存储在并由 Kubernetes API 驱动。RBAC代表基于角色的访问控制

  • --authorization-mode=Node是一种特殊模式,用于授权 kubelet 发出的 API 请求。

  • --authorization-mode=Webhook允许授权由使用 REST 的远程服务驱动。

您可以通过实现以下简单的 Go 接口来添加自定义授权插件:

type Authorizer interface {
  Authorize(a Attributes) (authorized bool, reason string, err error)
    } 

Attributes输入参数也是一个接口,提供了您需要做出授权决定的所有信息:

type Attributes interface {
  GetUser() user.Info
  GetVerb() string
  IsReadOnly() bool
  GetNamespace() string
  GetResource() string
  GetSubresource() string
  GetName() string
  GetAPIGroup() string
  GetAPIVersion() string
  IsResourceRequest() bool
  GetPath() string
} 

使用准入控制插件

好的。请求已经经过身份验证和授权,但在执行之前还有一步。请求必须通过一系列的准入控制插件。与授权者类似,如果单个准入控制器拒绝请求,则请求将被拒绝。

准入控制器是一个很好的概念。其思想是可能存在全局集群关注点,这可能是拒绝请求的理由。没有准入控制器,所有授权者都必须意识到这些问题并拒绝请求。但是,有了准入控制器,这个逻辑可以执行一次。此外,准入控制器可以修改请求。准入控制器以验证模式或变异模式运行。通常情况下,集群管理员通过提供名为admission-control的命令行参数来决定运行哪些准入控制插件。该值是一个逗号分隔的有序插件列表。以下是 Kubernetes >= 1.9 的推荐插件列表(顺序很重要):

--admission-control=NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel,DefaultStorageClass,MutatingAdmissionWebhook,ValidatingAdmissionWebhook,ResourceQuota,DefaultTolerationSeconds  

让我们看一些可用的插件(随时添加更多):

  • AlwaysAdmit: 透传(我不确定为什么需要它)。

  • AlwaysDeny: 这拒绝一切(用于测试很有用)。

  • AlwaysPullImages: 这将新的 Pod 镜像拉取策略设置为 Always(在多租户集群中很有用,以确保没有凭据拉取私有镜像的 Pod 不使用它们)。

  • DefaultStorageClass: 这为未指定存储类的PersistentVolumeClaim创建请求添加了一个默认存储类。

  • DefaultTollerationSeconds: 这设置了 Pod 对污点的默认容忍时间(如果尚未设置):notready:NoExecutenotreachable:NoExecute

  • DenyEscalatingExec: 这拒绝对以提升的特权运行并允许主机访问的 Pod 执行和附加命令。这包括以特权运行、具有主机 IPC 命名空间访问权限和具有主机 PID 命名空间访问权限的 Pod。

  • EventRateLimit: 这限制了 API 服务器的事件洪水(Kubernetes 1.9 中的新功能)。

  • ExtendedResourceToleration: 这将节点上的污点与 GPU 和 FPGA 等特殊资源结合起来,与请求这些资源的 Pod 的容忍结合起来。最终结果是具有额外资源的节点将专门用于具有适当容忍的 Pod。

  • ImagePolicyWebhook: 这个复杂的插件连接到外部后端,根据镜像决定是否拒绝请求。

  • Initializers: 这通过修改要创建的资源的元数据来设置挂起的初始化器(基于InitializerConfiguration)。

  • InitialResources(实验性的):如果未指定,这将根据历史使用情况分配计算资源和限制。

  • LimitPodHardAntiAffinity:拒绝定义了除 kubernetes.io/hostname 之外的反亲和拓扑键的任何 Pod 在 requiredDuringSchedulingRequiredDuringExecution 中。

  • LimitRanger:拒绝违反资源限制的请求。

  • MutatingAdmissionWebhook:按顺序调用已注册的能够修改目标对象的变异 Webhook。请注意,由于其他变异 Webhook 的潜在更改,不能保证更改会生效。

  • NamespaceLifecycle:拒绝在正在终止或不存在的命名空间中创建对象。

  • ResourceQuota:拒绝违反命名空间资源配额的请求。

  • ServiceAccount:这是服务账户的自动化。

  • ValidatingAdmissionWebhook:此准入控制器调用与请求匹配的任何验证 Webhook。匹配的 Webhook 会并行调用;如果其中任何一个拒绝请求,请求将失败。

正如您所看到的,准入控制插件具有多样的功能。它们支持命名空间范围的策略,并主要从资源管理的角度执行请求的有效性。这使授权插件可以专注于有效的操作。ImagePolicyWebHook 是验证镜像的入口,这是一个很大的挑战。Initializers 是动态准入控制的入口,您可以在其中部署自己的准入控制器,而无需将其编译到 Kubernetes 中。还有外部准入 Webhook,适用于诸如资源的语义验证(所有 Pod 是否具有标准的标签集?)等任务。

通过身份验证、授权和准入的各个阶段分别负责验证传入请求的责任划分,每个阶段都有自己的插件,使得复杂的过程变得更容易理解和使用。

保护 Pod

Pod 安全是一个主要关注点,因为 Kubernetes 调度 Pod 并让它们运行。为了保护 Pod 和容器,有几种独立的机制。这些机制一起支持深度防御,即使攻击者(或错误)绕过一个机制,也会被另一个机制阻止。

使用私有镜像仓库

这种方法让您非常有信心,您的集群只会拉取您之前审查过的镜像,并且您可以更好地管理升级。您可以在每个节点上配置$HOME/.dockercfg$HOME/.docker/config.json。但是,在许多云提供商上,您无法这样做,因为节点是自动为您配置的。

ImagePullSecrets

这种方法适用于云提供商上的集群。其思想是,注册表的凭据将由 pod 提供,因此无论它被安排在哪个节点上运行都无所谓。这避开了节点级别的.dockercfg问题。

首先,您需要为凭据创建一个secret对象:

> kubectl create secret the-registry-secret 
  --docker-server=<docker registry server> 
  --docker-username=<username> 
  --docker-password=<password> 
  --docker-email=<email>
secret "docker-registry-secret" created.  

如果需要,您可以为多个注册表(或同一注册表的多个用户)创建 secret。kubelet 将合并所有ImagePullSecrets

然而,因为 pod 只能在其自己的命名空间中访问 secret,所以您必须在希望 pod 运行的每个命名空间中创建一个 secret。

一旦定义了 secret,您可以将其添加到 pod 规范中,并在集群上运行一些 pod。pod 将使用 secret 中的凭据从目标镜像注册表中拉取镜像:

apiVersion: v1 
kind: Pod 
metadata: 
  name: cool-pod 
  namespace: the-namespace 
spec: 
  containers: 
    - name: cool-container 
      image: cool/app:v1 
  imagePullSecrets: 
    - name: the-registry-secret 

指定安全上下文

安全上下文是一组操作系统级别的安全设置,例如 UID、gid、功能和 SELinux 角色。这些设置应用于容器级别作为容器安全内容。您可以指定将应用于 pod 中所有容器的 pod 安全上下文。pod 安全上下文还可以将其安全设置(特别是fsGroupseLinuxOptions)应用于卷。

以下是一个示例 pod 安全上下文:

apiVersion: v1 
kind: Pod 
metadata: 
  name: hello-world 
spec: 
  containers: 
    ... 
  securityContext: 
    fsGroup: 1234 
    supplementalGroups: [5678] 
    seLinuxOptions: 
      level: "s0:c123,c456" 

容器安全上下文应用于每个容器,并覆盖了 pod 安全上下文。它嵌入在 pod 清单的容器部分中。容器上下文设置不能应用于卷,卷保持在 pod 级别。

以下是一个示例容器安全内容:

apiVersion: v1 
kind: Pod 
metadata: 
  name: hello-world 
spec: 
  containers: 
    - name: hello-world-container 
      # The container definition 
      # ... 
      securityContext: 
        privileged: true 
        seLinuxOptions: 
          level: "s0:c123,c456" 

使用 AppArmor 保护您的集群

AppArmor是一个 Linux 内核安全模块。使用AppArmor,您可以限制在容器中运行的进程对一组有限的资源的访问,例如网络访问、Linux 功能和文件权限。您可以通过配置AppArmor来配置配置文件。

要求

在 Kubernetes 1.4 中,AppArmor 支持作为 beta 版本添加。它并不适用于每个操作系统,因此您必须选择一个受支持的操作系统发行版才能利用它。Ubuntu 和 SUSE Linux 支持 AppArmor,并默认启用。其他发行版则具有可选的支持。要检查 AppArmor 是否已启用,请输入以下代码:

cat /sys/module/apparmor/parameters/enabled
 Y 

如果结果是Y,则已启用。

配置文件必须加载到内核中。请检查以下文件:

/sys/kernel/security/apparmor/profiles  

此时,只有 Docker 运行时支持AppArmor

使用 AppArmor 保护 Pod

由于AppArmor仍处于 beta 阶段,因此您需要将元数据指定为注释,而不是bonafide字段;当它退出 beta 阶段时,这将发生变化。

要将配置文件应用于容器,请添加以下注释:

container.apparmor.security.beta.kubernetes.io/<container-name>: <profile-ref>

配置文件引用可以是默认配置文件,runtime/default,或者主机localhost/<profile-name>上的配置文件。

以下是一个防止写入文件的示例配置文件:

#include <tunables/global> 

profile k8s-apparmor-example-deny-write flags=(attach_disconnected) { 
  #include <abstractions/base> 

  file, 

  # Deny all file writes. 
  deny /** w, 
} 

AppArmor 不是 Kubernetes 资源,因此其格式不是您熟悉的 YAML 或 JSON。

要验证配置文件是否正确附加,请检查进程1的属性:

kubectl exec <pod-name> cat /proc/1/attr/current  

默认情况下,Pod 可以在集群中的任何节点上调度。这意味着配置文件应该加载到每个节点中。这是 DaemonSet 的一个经典用例。

编写 AppArmor 配置文件

手动编写AppArmor配置文件很重要。有一些工具可以帮助:aa-genprofaa-logprof可以为您生成配置文件,并通过在应用程序中使用AppArmor的 complain 模式来帮助微调它。这些工具会跟踪应用程序的活动和AppArmor警告,并创建相应的配置文件。这种方法有效,但感觉有些笨拙。

我的最爱工具是 bane(github.com/jessfraz/bane),它可以根据 TOML 语法生成AppArmor配置文件。Bane 配置文件非常易读且易于理解。以下是一个 bane 配置文件的片段:

    Name = "nginx-sample"
    [Filesystem]
    # read only paths for the container
    ReadOnlyPaths = [
      "/bin/**",
      "/boot/**",
      "/dev/**",
    ]

    # paths where you want to log on write
    LogOnWritePaths = [
      "/**"
    ]

    # allowed capabilities
    [Capabilities]
    Allow = [
      "chown",
      "setuid",
    ]

    [Network]
    Raw = false
    Packet = false
    Protocols = [
      "tcp",
      "udp",
      "icmp"
    ] 

生成的AppArmor配置文件相当复杂。

Pod 安全策略

Pod 安全策略PSP)自 Kubernetes 1.4 以来就作为 Beta 版本可用。必须启用它,并且还必须启用 PSP 准入控制来使用它们。PSP 在集群级别定义,并为 Pod 定义安全上下文。使用 PSP 和直接在 Pod 清单中指定安全内容之间有一些区别,就像我们之前所做的那样:

  • 将相同的策略应用于多个 Pod 或容器

  • 让管理员控制 Pod 的创建,以便用户不会创建具有不适当安全上下文的 Pod

  • 通过准入控制器为 Pod 动态生成不同的安全内容

PSPs 真的扩展了安全上下文的概念。通常,与 Pod(或者说,Pod 模板)的数量相比,您将拥有相对较少的安全策略。这意味着许多 Pod 模板和容器将具有相同的安全策略。没有 PSP,您必须为每个 Pod 清单单独管理它。

这是一个允许一切的示例 PSP:

    {
      "kind": "PodSecurityPolicy",
      "apiVersion":"policy/v1beta1",
      "metadata": {
        "name": "permissive"
      },
      "spec": {
          "seLinux": {
              "rule": "RunAsAny"
          },
          "supplementalGroups": {
              "rule": "RunAsAny"
          },
          "runAsUser": {
              "rule": "RunAsAny"
          },
          "fsGroup": {
              "rule": "RunAsAny"
          },
          "volumes": ["*"]
      }
    }

通过 RBAC 授权 Pod 安全策略

这是启用策略使用的推荐方法。让我们创建clusterRoleRole也可以)来授予使用目标策略的访问权限。它应该是这样的:

kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
 name: <role name>
rules:
- apiGroups: ['policy']
 resources: ['podsecuritypolicies']
 verbs: ['use']
 resourceNames:
 - <list of policies to authorize>

然后,我们需要将集群角色绑定到授权用户:

kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
 name: <binding name>
roleRef:
 kind: ClusterRole
 name: <role name>
 apiGroup: rbac.authorization.k8s.io
subjects:
# Authorize specific service accounts:
- kind: ServiceAccount
 name: <authorized service account name>
 namespace: <authorized pod namespace>
# Authorize specific users (not recommended):
- kind: User
 apiGroup: rbac.authorization.k8s.io
 name: <authorized user name>

如果使用角色绑定而不是集群角色,则它将仅适用于与绑定相同命名空间中的 Pod。这可以与系统组配对,以授予对在命名空间中运行的所有 Pod 的访问权限:

# Authorize all service accounts in a namespace:
- kind: Group
 apiGroup: rbac.authorization.k8s.io
 name: system:serviceaccounts
# Or equivalently, all authenticated users in a namespace:
- kind: Group
 apiGroup: rbac.authorization.k8s.io
 name: system:authenticated

管理网络策略

节点、Pod 和容器的安全性至关重要,但这还不够。网络分割对于设计安全的 Kubernetes 集群至关重要,它允许多租户使用,并且可以最小化安全漏洞的影响。深度防御要求您对不需要相互通信的系统部分进行分隔,并允许您仔细管理流量的方向、协议和端口。

网络策略可以让您对集群的命名空间和通过标签选择的 Pod 进行细粒度控制和适当的网络分割。在其核心,网络策略是一组防火墙规则,应用于一组由标签选择的命名空间和 Pod。这非常灵活,因为标签可以定义虚拟网络段,并且可以作为 Kubernetes 资源进行管理。

选择支持的网络解决方案

一些网络后端不支持网络策略。例如,流行的 Flannel 无法应用于策略。

这是一个支持的网络后端列表:

  • Calico

  • WeaveNet

  • Canal

  • Cillium

  • Kube-Router

  • Romana

定义网络策略

您可以使用标准的 YAML 清单来定义网络策略。

这是一个示例策略:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
 name: the-network-policy
 namespace: default
spec:
 podSelector:
 matchLabels:
 role: db
 ingress:
 - from:
 - namespaceSelector:
 matchLabels:
 project: cool-project
 - podSelector:
 matchLabels:
 role: frontend
 ports:
 - protocol: tcp
 port: 6379

spec部分有两个重要部分——podSelectoringresspodSelector管理此网络策略适用于哪些 pod。ingress管理哪些命名空间和 pod 可以访问这些 pod,以及它们可以使用哪些协议和端口。

在示例网络策略中,pod选择器指定了网络策略的目标,即所有标记为role: db的 pod。ingress部分有一个from子部分,其中包括一个namespace选择器和一个pod选择器。集群中所有标记为project: cool-project的命名空间,以及这些命名空间中所有标记为role: frontend的 pod,都可以访问标记为role: db的目标 pod。ports部分定义了一对对(协议和端口),进一步限制了允许的协议和端口。在这种情况下,协议是tcp,端口是6379(Redis 标准端口)。

请注意,网络策略是集群范围的,因此集群中多个命名空间的 pod 可以访问目标命名空间。当前命名空间始终包括在内,因此即使它没有project: cool标签,带有role: frontendpods仍然可以访问。

网络策略以白名单方式运行很重要。默认情况下,所有访问都被禁止,网络策略可以打开某些协议和端口,以匹配标签的某些 pod。这意味着,如果您的网络解决方案不支持网络策略,所有访问将被拒绝。

白名单性质的另一个含义是,如果存在多个网络策略,则所有规则的并集都适用。如果一个策略允许访问端口1234,另一个策略允许访问端口5678,那么一个 pod 可能访问端口12345678

限制对外部网络的出口

Kubernetes 1.8 添加了出口网络策略支持,因此您也可以控制出站流量。以下是一个示例,阻止访问外部 IP1.2.3.4order: 999确保在其他策略之前应用该策略:

apiVersion: v1
kind: policy
metadata:
 name: default-deny-egress
spec:
 order: 999
 egress:
 - action: deny
 destination:
 net: 1.2.3.4
 source: {}

跨命名空间策略

如果将集群划分为多个命名空间,有时如果 pod 跨命名空间通信,这可能会很方便。您可以在网络策略中指定ingress.namespaceSelector字段,以允许从多个命名空间访问。例如,如果您有生产和暂存命名空间,并且定期使用生产数据的快照填充暂存环境。

使用秘密

秘密在安全系统中至关重要。它们可以是凭据,如用户名和密码、访问令牌、API 密钥或加密密钥。秘密通常很小。如果您有大量要保护的数据,您应该对其进行加密,并将加密/解密密钥保留为秘密。

在 Kubernetes 中存储秘密

Kubernetes 默认将秘密以明文存储在etcd中。这意味着对etcd的直接访问是有限的并且受到仔细保护。从 Kubernetes 1.7 开始,您现在可以在休息时加密您的秘密(当它们由etcd存储时)。

秘密是在命名空间级别管理的。Pod 可以通过秘密卷将秘密挂载为文件,也可以将其作为环境变量。从安全的角度来看,这意味着可以创建命名空间中的任何用户或服务都可以访问为该命名空间管理的任何秘密。如果要限制对秘密的访问,请将其放在一组有限用户或服务可访问的命名空间中。

当秘密挂载到一个 pod 上时,它永远不会被写入磁盘。它存储在tmpfs中。当 kubelet 与 API 服务器通信时,通常使用 TLS,因此秘密在传输过程中受到保护。

配置休息时的加密

启动 API 服务器时,您需要传递此参数:

--experimental-encryption-provider-config <encryption config file>   

以下是一个样本加密配置:

kind: EncryptionConfig
apiVersion: v1
resources:
 - resources:
 - secrets
 providers:
 - identity: {}
 - aesgcm:
 keys:
 - name: key1
 secret: c2VjcmV0IGlzIHNlY3VyZQ==
 - name: key2
 secret: dGhpcyBpcyBwYXNzd29yZA==
 - aescbc:
 keys:
 - name: key1
 secret: c2VjcmV0IGlzIHNlY3VyZQ==
 - name: key2
 secret: dGhpcyBpcyBwYXNzd29yZA==
 - secretbox:
 keys:
 - name: key1
 secret: YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoxMjM0NTY=

创建秘密

在尝试创建需要它们的 pod 之前,必须先创建秘密。秘密必须存在;否则,pod 创建将失败。

您可以使用以下命令创建秘密:

kubectl create secret. 

在这里,我创建了一个名为hush-hash的通用秘密,其中包含两个键—用户名和密码:

> kubectl create secret generic hush-hush --from-literal=username=tobias --from-literal=password=cutoffs 

生成的秘密是Opaque

> kubectl describe secrets/hush-hush
Name:           hush-hush
Namespace:      default
Labels:         <none>
Annotations:    <none>

Type:   Opaque

Data
====
password:       7 bytes
username:       6 bytes

您可以使用--from-file而不是--from-literal从文件创建秘密,并且如果将秘密值编码为base64,还可以手动创建秘密。

秘密中的键名必须遵循 DNS 子域的规则(不包括前导点)。

解码秘密

要获取秘密的内容,可以使用kubectl get secret

> kubectl get secrets/hush-hush -o yaml
apiVersion: v1
data:
 password: Y3V0b2Zmcw==
 username: dG9iaWFz
kind: Secret
metadata:
 creationTimestamp: 2018-01-15T23:43:50Z
 name: hush-hush
 namespace: default
 resourceVersion: "2030851"
 selfLink: /api/v1/namespaces/default/secrets/hush-hush
 uid: f04641ef-fa4d-11e7-beab-080027c94384
type: Opaque
The values are base64-encoded. You need to decode them yourself:
> echo "Y3V0b2Zmcw==" | base64 --decode
cutoofs

这些值是base64编码的。您需要自己解码它们:

> echo "Y3V0b2Zmcw==" | base64 --decode
cutoofs 

在容器中使用秘密

容器可以通过从 pod 中挂载卷来将秘密作为文件访问。另一种方法是将秘密作为环境变量访问。最后,容器(如果其服务账户具有权限)可以直接访问 Kubernetes API 或使用 kubectl get secret。

要使用作为卷挂载的秘密,pod 清单应声明卷,并且应在容器的规范中挂载:

{ 
 "apiVersion": "v1", 
 "kind": "Pod", 
  "metadata": { 
    "name": "pod-with-secret", 
    "namespace": "default" 
  }, 
  "spec": { 
    "containers": [{ 
      "name": "the-container", 
      "image": "redis", 
      "volumeMounts": [{ 
        "name": "secret-volume", 
        "mountPath": "/mnt/secret-volume", 
        "readOnly": true 
      }] 
    }], 
    "volumes": [{ 
      "name": "secret-volume", 
      "secret": { 
        "secretName": "hush-hush" 
      } 
    }] 
  } 
} 

卷名称(secret-volume)将 pod 卷绑定到容器中的挂载点。多个容器可以挂载相同的卷。

当此 pod 运行时,用户名和密码将作为文件出现在/etc/secret-volume下:

> kubectl exec pod-with-secret cat /mnt/secret-volume/username
tobias

> kubectl exec pod-with-secret cat /mnt/secret-volume/password
cutoffs  

运行多用户集群

在本节中,我们将简要讨论使用单个集群来托管多个用户或多个用户社区的系统的选项。这个想法是这些用户是完全隔离的,甚至可能不知道他们与其他用户共享集群。每个用户社区都将拥有自己的资源,并且它们之间不会有通信(除非通过公共端点)。Kubernetes 命名空间概念是这个想法的最终表达。

多用户集群的情况

为什么要为多个隔离的用户或部署运行单个集群?每个用户都有一个专用的集群不是更简单吗?主要有两个原因:成本和运营复杂性。如果您有许多相对较小的部署,并且希望为每个部署创建一个专用的集群,那么您将需要为每个部署单独的主节点,可能还需要一个三节点的etcd集群。这可能会增加成本。运营复杂性也非常重要。管理数十甚至数百个独立的集群并不容易。每次升级和每次补丁都需要应用到每个集群。运营可能会失败,您将不得不管理一群集群,其中一些集群的状态可能与其他集群略有不同。跨所有集群的元操作可能更加困难。您将不得不聚合并编写您的工具来执行操作并从所有集群收集数据。

让我们看一些多个隔离社区或部署的用例和要求:

  • 作为<Blank>-服务的平台或服务提供商

  • 管理单独的测试、暂存和生产环境

  • 将责任委托给社区/部署管理员

  • 对每个社区强制执行资源配额和限制

  • 用户只能看到他们社区中的资源

使用命名空间进行安全的多租户管理

Kubernetes 命名空间是安全的多租户集群的完美解决方案。这并不奇怪,因为这是命名空间的设计目标之一。

您可以轻松地创建除内置 kube 系统和默认之外的命名空间。以下是一个将创建一个名为custom-namespace的新命名空间的 YAML 文件。它只有一个名为name的元数据项。没有比这更简单的了:

apiVersion: v1 
kind: Namespace 
metadata: 
  name: custom-namespace 

让我们创建命名空间:

> Kubectl create -f custom-namespace.yaml
namespace "custom-namespace" created

> kubectl get namesapces
NAME               STATUS    AGE
custom-namespace   Active    39s
default            Active    32d
kube-system        Active    32d

状态字段可以是activeterminating。当您删除一个命名空间时,它将进入 terminating 状态。当命名空间处于此状态时,您将无法在此命名空间中创建新资源。这简化了命名空间资源的清理,并确保命名空间真正被删除。如果没有它,当现有 pod 被删除时,复制控制器可能会创建新的 pod。

要使用命名空间,您需要在kubectl命令中添加--namespace参数:

> kubectl create -f some-pod.yaml --namespace=custom-namespace
pod "some-pod" created

在自定义命名空间中列出 pod 只返回我们刚刚创建的 pod:

> kubectl get pods --namespace=custom-namespace
NAME       READY     STATUS    RESTARTS   AGE
some-pod   1/1       Running   0          6m 

在不带命名空间的情况下列出 pod 会返回默认命名空间中的 pod:

> Kubectl get pods
NAME                           READY     STATUS    RESTARTS   AGE
echo-3580479493-n66n4          1/1       Running   16         32d
leader-elector-191609294-lt95t 1/1       Running   4          9d
leader-elector-191609294-m6fb6 1/1       Running   4          9d
leader-elector-191609294-piu8p 1/1       Running   4          9d pod-with-secret                1/1       Running   1          1h

避免命名空间陷阱

命名空间很棒,但可能会增加一些摩擦。当您只使用默认命名空间时,可以简单地省略命名空间。当使用多个命名空间时,必须使用命名空间限定所有内容。这可能是一个负担,但不会带来任何危险。但是,如果一些用户(例如,集群管理员)可以访问多个命名空间,那么您就有可能意外修改或查询错误的命名空间。避免这种情况的最佳方法是将命名空间密封起来,并要求为每个命名空间使用不同的用户和凭据。

此外,工具可以帮助清楚地显示您正在操作的命名空间(例如,如果从命令行工作,则是 shell 提示,或者在 Web 界面中突出显示命名空间)。

确保可以在专用命名空间上操作的用户不能访问默认命名空间。否则,每当他们忘记指定命名空间时,他们将在默认命名空间上悄悄操作。

总结

在本章中,我们介绍了开发人员和管理员在 Kubernetes 集群上构建系统和部署应用程序时面临的许多安全挑战。但我们也探讨了许多安全功能和灵活的基于插件的安全模型,提供了许多限制、控制和管理容器、pod 和节点的方法。Kubernetes 已经为大多数安全挑战提供了多功能解决方案,随着诸如 AppArmor 和各种插件从 alpha/beta 状态转移到一般可用状态,它将变得更加完善。最后,我们考虑了如何使用命名空间来支持同一 Kubernetes 集群中的多个用户社区或部署。

在下一章中,我们将深入研究许多 Kubernetes 资源和概念,以及如何有效地使用它们并将它们组合起来。Kubernetes 对象模型是建立在一小部分通用概念(如资源、清单和元数据)的坚实基础之上的。这使得一个可扩展的、但令人惊讶地一致的对象模型能够为开发人员和管理员提供非常多样化的能力集。

第六章:使用关键的 Kubernetes 资源

在本章中,我们将设计一个挑战 Kubernetes 能力和可伸缩性的大规模平台。Hue 平台的目标是创建一个无所不知、无所不能的数字助手。Hue 是你的数字延伸。它将帮助你做任何事情,找到任何东西,并且在许多情况下,将代表你做很多事情。它显然需要存储大量信息,与许多外部服务集成,响应通知和事件,并且在与你的互动方面非常智能。

在本章中,我们将有机会更好地了解 Kubectl 和其他相关工具,并将详细探讨我们之前见过的资源,如 pod,以及新资源,如jobs。在本章结束时,你将清楚地了解 Kubernetes 有多么令人印象深刻,以及它如何可以作为极其复杂系统的基础。

设计 Hue 平台

在本节中,我们将为惊人的 Hue 平台设定舞台并定义范围。Hue 不是老大哥,Hue 是小弟!Hue 将做任何你允许它做的事情。它可以做很多事情,但有些人可能会担心,所以你可以选择 Hue 可以帮助你多少。准备好进行一次疯狂的旅行!

定义 Hue 的范围

Hue 将管理你的数字人格。它将比你自己更了解你。以下是 Hue 可以管理并帮助你的一些服务列表:

  • 搜索和内容聚合

  • 医疗

  • 智能家居

  • 金融-银行、储蓄、退休、投资

  • 办公室

  • 社交

  • 旅行

  • 健康

  • 家庭

  • 智能提醒和通知:让我们想想可能性。Hue 将了解你,也了解你的朋友和所有领域的其他用户的总和。Hue 将实时更新其模型。它不会被陈旧的数据所困扰。它将代表你采取行动,提供相关信息,并持续学习你的偏好。它可以推荐你可能喜欢的新节目或书籍,根据你的日程安排和家人或朋友的情况预订餐厅,并控制你的家庭自动化。

  • 安全、身份和隐私:Hue 是您在线的代理。有人窃取您的 Hue 身份,甚至只是窃听您的 Hue 互动的后果是灾难性的。潜在用户甚至可能不愿意信任 Hue 组织他们的身份。让我们设计一个非信任系统,让用户随时有权终止 Hue。以下是一些朝着正确方向的想法:

  • 通过专用设备和多因素授权实现强大的身份验证,包括多种生物识别原因

  • 频繁更换凭证

  • 快速服务暂停和所有外部服务的身份重新验证(将需要向每个提供者提供原始身份证明)

  • Hue 后端将通过短暂的令牌与所有外部服务进行交互

  • 将 Hue 构建为一组松散耦合的微服务的架构。

Hue 的架构将需要支持巨大的变化和灵活性。它还需要非常可扩展,其中现有的功能和外部服务不断升级,并且新的功能和外部服务集成到平台中。这种规模需要微服务,其中每个功能或服务都与其他服务完全独立,除了通过标准和/或可发现的 API 进行定义的接口。

Hue 组件

在着手进行微服务之旅之前,让我们回顾一下我们需要为 Hue 构建的组件类型。

  • 用户资料:

用户资料是一个重要组成部分,有很多子组件。它是用户的本质,他们的偏好,跨各个领域的历史,以及 Hue 对他们了解的一切。

  • 用户图:

用户图组件模拟了用户在多个领域之间的互动网络。每个用户参与多个网络:社交网络(如 Facebook 和 Twitter)、专业网络、爱好网络和志愿者社区。其中一些网络是临时的,Hue 将能够对其进行结构化以使用户受益。Hue 可以利用其对用户连接的丰富资料,即使不暴露私人信息,也能改善互动。

  • 身份:

身份管理是至关重要的,正如前面提到的,因此它值得一个单独的组件。用户可能更喜欢管理具有独立身份的多个互斥配置文件。例如,也许用户不愿意将他们的健康配置文件与社交配置文件混合在一起,因为这样做可能会意外地向朋友透露个人健康信息的风险。

  • 授权者

授权者是一个关键组件,用户明确授权 Hue 代表其执行某些操作或收集各种数据。这包括访问物理设备、外部服务的帐户和主动程度。

  • 外部服务

Hue 是外部服务的聚合器。它并非旨在取代您的银行、健康提供者或社交网络。它将保留大量关于您活动的元数据,但内容将保留在您的外部服务中。每个外部服务都需要一个专用组件来与外部服务的 API 和政策进行交互。当没有 API 可用时,Hue 通过自动化浏览器或原生应用程序来模拟用户。

  • 通用传感器

Hue 价值主张的一个重要部分是代表用户行事。为了有效地做到这一点,Hue 需要意识到各种事件。例如,如果 Hue 为您预订了假期,但它感觉到有更便宜的航班可用,它可以自动更改您的航班或要求您确认。有无限多件事情可以感知。为了控制感知,需要一个通用传感器。通用传感器将是可扩展的,但提供一个通用接口,供 Hue 的其他部分统一使用,即使添加了越来越多的传感器。

  • 通用执行器

这是通用传感器的对应物。Hue 需要代表您执行操作,比如预订航班。为了做到这一点,Hue 需要一个通用执行器,可以扩展以支持特定功能,但可以以统一的方式与其他组件交互,比如身份管理器和授权者。

  • 用户学习者

这是 Hue 的大脑。它将不断监视您所有的互动(经您授权的),并更新对您的模型。这将使 Hue 随着时间变得越来越有用,预测您的需求和兴趣,提供更好的选择,在合适的时候提供更相关的信息,并避免让人讨厌和压抑。

Hue 微服务

每个组件的复杂性都非常巨大。一些组件,比如外部服务、通用传感器和通用执行器,需要跨越数百、数千甚至更多不断变化的外部服务进行操作,而这些服务是在 Hue 的控制范围之外的。甚至用户学习器也需要学习用户在许多领域和领域的偏好。微服务通过允许 Hue 逐渐演变并增加更多的隔离能力来满足这种需求,而不会在自身的复杂性下崩溃。每个微服务通过标准接口与通用 Hue 基础设施服务进行交互,并且可以通过明确定义和版本化的接口与其他一些服务进行交互。每个微服务的表面积是可管理的,微服务之间的编排基于标准最佳实践:

  • 插件

插件是扩展 Hue 而不会产生大量接口的关键。关于插件的一件事是,通常需要跨越多个抽象层的插件链。例如,如果我们想要为 Hue 添加与 YouTube 的新集成,那么你可以收集大量特定于 YouTube 的信息:你的频道、喜欢的视频、推荐以及你观看过的视频。为了向用户显示这些信息并允许他们对其进行操作,你需要跨越多个组件并最终在用户界面中使用插件。智能设计将通过聚合各种操作类别,如推荐、选择和延迟通知,来帮助许多不同的服务。

插件的好处在于任何人都可以开发。最初,Hue 开发团队将不得不开发插件,但随着 Hue 变得更加流行,外部服务将希望与 Hue 集成并构建 Hue 插件以启用其服务。

当然,这将导致插件注册、批准和策划的整个生态系统。

  • 数据存储

Hue 将需要多种类型的数据存储和每种类型的多个实例来管理其数据和元数据:

    • 关系数据库
  • 图数据库

  • 时间序列数据库

  • 内存缓存

由于 Hue 的范围,每个数据库都将需要进行集群化和分布式处理。

  • 无状态微服务

微服务应该大部分是无状态的。这将允许特定实例被快速启动和关闭,并根据需要在基础设施之间迁移。状态将由存储管理,并由短暂的访问令牌访问微服务。

  • 基于队列的交互

所有这些微服务需要相互通信。用户将要求 Hue 代表他们执行任务。外部服务将通知 Hue 各种事件。与无状态微服务相结合的队列提供了完美的解决方案。每个微服务的多个实例将监听各种队列,并在从队列中弹出相关事件或请求时做出响应。这种安排非常健壮且易于扩展。每个组件都可以是冗余的并且高度可用。虽然每个组件都可能出现故障,但系统非常容错。

队列可以用于异步的 RPC 或请求-响应式的交互,其中调用实例提供一个私有队列名称,被调用者将响应发布到私有队列。

规划工作流程

Hue 经常需要支持工作流程。典型的工作流程将得到一个高层任务,比如预约牙医;它将提取用户的牙医详情和日程安排,与用户的日程安排匹配,在多个选项之间进行选择,可能与用户确认,预约,并设置提醒。我们可以将工作流程分类为完全自动和涉及人类的人工工作流程。然后还有涉及花钱的工作流程。

自动工作流程

自动工作流程不需要人为干预。Hue 有完全的权限来执行从开始到结束的所有步骤。用户分配给 Hue 的自主权越多,它的效果就会越好。用户应该能够查看和审计所有的工作流程,无论是过去还是现在。

人工工作流程

人工工作流程需要与人的交互。最常见的情况是用户需要从多个选项中进行选择或批准某项操作,但也可能涉及到另一个服务上的人员。例如,要预约牙医,您可能需要从秘书那里获取可用时间的列表。

预算意识工作流程

有些工作流程,比如支付账单或购买礼物,需要花钱。虽然从理论上讲,Hue 可以被授予对用户银行账户的无限访问权限,但大多数用户可能更愿意为不同的工作流程设置预算,或者只是将花钱作为经过人工批准的活动。

使用 Kubernetes 构建 Hue 平台

在本节中,我们将看看各种 Kubernetes 资源以及它们如何帮助我们构建 Hue。首先,我们将更好地了解多才多艺的 Kubectl,然后我们将看看在 Kubernetes 中运行长时间运行的进程,内部和外部暴露服务,使用命名空间限制访问,启动临时作业以及混合非集群组件。显然,Hue 是一个庞大的项目,所以我们将在本地 Minikube 集群上演示这些想法,而不是实际构建一个真正的 Hue Kubernetes 集群。

有效使用 Kubectl

Kubectl 是您的瑞士军刀。它几乎可以做任何与集群相关的事情。在幕后,Kubectl 通过 API 连接到您的集群。它读取您的.kube/config文件,其中包含连接到您的集群或集群所需的信息。命令分为多个类别:

  • 通用命令:以通用方式处理资源:creategetdeleterunapplypatchreplace等等

  • 集群管理命令:处理节点和整个集群:cluster-infocertificatedrain等等

  • 故障排除命令describelogsattachexec等等

  • 部署命令:处理部署和扩展:rolloutscaleauto-scale等等

  • 设置命令:处理标签和注释:labelannotate等等

Misc commands: help, config, and version 

您可以使用 Kubernetes config view查看配置。

这是 Minikube 集群的配置:

~/.minikube > k config view 
apiVersion: v1 
clusters: 
- cluster: 
    certificate-authority: /Users/gigi.sayfan/.minikube/ca.crt 
    server: https://192.168.99.100:8443 
  name: minikube 
contexts: 
- context: 
    cluster: minikube 
    user: minikube 
  name: minikube 
current-context: minikube 
kind: Config 
preferences: {} 
users: 
- name: minikube 
  user: 
    client-certificate: /Users/gigi.sayfan/.minikube/client.crt 
    client-key: /Users/gigi.sayfan/.minikube/client.key 

理解 Kubectl 资源配置文件

许多 Kubectl 操作,比如create,需要复杂的分层输出(因为 API 需要这种输出)。Kubectl 使用 YAML 或 JSON 配置文件。这是一个用于创建 pod 的 JSON 配置文件:

apiVersion: v1
kind: Pod
metadata:
  name: ""
  labels:
    name: ""
  namespace: ""
  annotations: []
  generateName: ""
spec:
     ...  
  • apiVersion:非常重要的 Kubernetes API 不断发展,并且可以通过 API 的不同版本支持相同资源的不同版本。

  • kindkind告诉 Kubernetes 它正在处理的资源类型,在本例中是pod。这是必需的。

  • metadata:这是描述 pod 及其操作位置的大量信息:

  • 名称:在其命名空间中唯一标识 pod

  • 标签:可以应用多个标签

  • 命名空间:pod 所属的命名空间

  • 注释:可查询的注释列表

  • 规范规范是一个包含启动 pod 所需的所有信息的 pod 模板。它可能非常复杂,所以我们将在多个部分中探讨它:

"spec": {
  "containers": [
  ],
  "restartPolicy": "",
  "volumes": [
  ]
}  
  • 容器规范:pod 规范的容器是容器规范的列表。每个容器规范都有以下结构:
        {
          "name": "",
          "image": "",
          "command": [
            ""
          ],
          "args": [
            ""
          ],
          "env": [
            {
              "name": "",
              "value": ""
            }
          ],
          "imagePullPolicy": "",
          "ports": [
            {
              "containerPort": 0,
              "name": "",
              "protocol": ""
            }
          ],
          "resources": {
            "cpu": ""
            "memory": ""
          }
        }

每个容器都有一个镜像,一个命令,如果指定了,会替换 Docker 镜像命令。它还有参数和环境变量。然后,当然还有镜像拉取策略、端口和资源限制。我们在前几章中已经涵盖了这些。

在 pod 中部署长时间运行的微服务

长时间运行的微服务应该在 pod 中运行,并且是无状态的。让我们看看如何为 Hue 的一个微服务创建 pod。稍后,我们将提高抽象级别并使用部署。

创建 pod

让我们从一个常规的 pod 配置文件开始,为创建 Hue 学习者内部服务。这个服务不需要暴露为公共服务,它将监听一个队列以获取通知,并将其见解存储在一些持久存储中。

我们需要一个简单的容器来运行 pod。这可能是有史以来最简单的 Docker 文件,它将模拟 Hue 学习者:

FROM busybox
CMD ash -c "echo 'Started...'; while true ; do sleep 10 ; done"  

它使用busybox基础镜像,打印到标准输出Started...,然后进入一个无限循环,这显然是长时间运行的。

我构建了两个标记为g1g1/hue-learn:v3.0g1g1/hue-learn:v4.0的 Docker 镜像,并将它们推送到 Docker Hub 注册表(g1g1是我的用户名)。

docker build . -t g1g1/hue-learn:v3.0
docker build . -t g1g1/hue-learn:v4.0
docker push g1g1/hue-learn:v3.0
docker push g1g1/hue-learn:v4.0  

现在,这些镜像可以被拉入 Hue 的 pod 中的容器。

我们将在这里使用 YAML,因为它更简洁和易读。这里是样板和元数据标签:

apiVersion: v1
kind: Pod
metadata:
  name: hue-learner
  labels:
    app: hue 
    runtime-environment: production
    tier: internal-service 
  annotations:
    version: "3.0"

我使用注释而不是标签的原因是,标签用于标识部署中的一组 pod。不允许修改标签。

接下来是重要的容器规范,为每个容器定义了强制的名称镜像

spec:
  containers:
  - name: hue-learner
    image: g1g1/hue-learn:v3.0  

资源部分告诉 Kubernetes 容器的资源需求,这允许更高效和紧凑的调度和分配。在这里,容器请求200毫 CPU 单位(0.2 核心)和256 MiB:

resources:
  requests:
    cpu: 200m
    memory: 256Mi 

环境部分允许集群管理员提供将可用于容器的环境变量。这里告诉它通过dns发现队列和存储。在测试环境中,可能会使用不同的发现方法:

env:
- name: DISCOVER_QUEUE
  value: dns
- name: DISCOVER_STORE
  value: dns 

用标签装饰 pod

明智地为 pod 贴上标签对于灵活的操作至关重要。它让您可以实时演变您的集群,将微服务组织成可以统一操作的组,并以自发的方式深入观察不同的子集。

例如,我们的 Hue 学习者 pod 具有以下标签:

  • 运行环境:生产

  • 层级:内部服务

版本注释可用于支持同时运行多个版本。如果需要同时运行版本 2 和版本 3,无论是为了提供向后兼容性还是在从v2迁移到v3期间暂时运行,那么具有版本注释或标签允许独立扩展不同版本的 pod 并独立公开服务。runtime-environment标签允许对属于特定环境的所有 pod 执行全局操作。tier标签可用于查询属于特定层的所有 pod。这只是例子;在这里,您的想象力是限制。

使用部署部署长时间运行的进程

在大型系统中,pod 不应该只是创建并放任不管。如果由于任何原因 pod 意外死亡,您希望另一个 pod 替换它以保持整体容量。您可以自己创建复制控制器或副本集,但这也会留下错误的可能性以及部分故障的可能性。在启动 pod 时指定要创建多少副本更有意义。

让我们使用 Kubernetes 部署资源部署我们的 Hue 学习者微服务的三个实例。请注意,部署对象在 Kubernetes 1.9 时变得稳定:

apiVersion: apps/v1 (use apps/v1beta2 before 1.9)
 kind: Deployment
 metadata:
 name: hue-learn
 labels:
 app: hue
 spec:
 replicas: 3
 selector:
 matchLabels:
 app: hue
 template:
 metadata:
 labels:
 app: hue
 spec:
 <same spec as in the pod template>

pod spec与我们之前使用的 pod 配置文件中的spec部分相同。

让我们创建部署并检查其状态:

> kubectl create -f .\deployment.yaml
deployment "hue-learn" created
> kubectl get deployment hue-learn
NAME        DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
hue-learn   3          3           3             3        4m

> kubectl get pods | grep hue-learn
NAME                        READY     STATUS    RESTARTS   AGE
hue-learn-237202748-d770r   1/1       Running   0          2m
hue-learn-237202748-fwv2t   1/1       Running   0          2m
hue-learn-237202748-tpr4s   1/1       Running   0          2m  

您可以使用kubectl describe命令获取有关部署的更多信息。

更新部署

Hue 平台是一个庞大且不断发展的系统。您需要不断升级。部署可以更新以无痛的方式推出更新。您可以更改 pod 模板以触发由 Kubernetes 完全管理的滚动更新。

目前,所有的 pod 都在运行版本 3.0:

> kubectl get pods -o json | jq .items[0].spec.containers[0].image
"3.0"  

让我们更新部署以升级到版本 4.0。修改部署文件中的镜像版本。不要修改标签;这会导致错误。通常,您会修改镜像和一些相关的元数据在注释中。然后我们可以使用apply命令来升级版本:

> kubectl apply -f hue-learn-deployment.yaml
deployment "hue-learn" updated
> kubectl get pods -o json | jq .items[0].spec.containers[0].image
"4.0"  

分离内部和外部服务

内部服务是只有其他服务或集群中的作业(或登录并运行临时工具的管理员)直接访问的服务。在某些情况下,内部服务根本不被访问,只是执行其功能并将结果存储在其他服务以解耦方式访问的持久存储中。

但是一些服务需要向用户或外部程序公开。让我们看一个虚假的 Hue 服务,它管理用户的提醒列表。它实际上并不做任何事情,但我们将用它来说明如何公开服务。我将一个虚假的hue-reminders镜像(与hue-learn相同)推送到 Docker Hub:

docker push g1g1/hue-reminders:v2.2  

部署内部服务

这是部署,它与 Hue-learner 部署非常相似,只是我删除了annotationsenvresources部分,只保留了一个标签以节省空间,并在容器中添加了一个ports部分。这是至关重要的,因为服务必须通过一个端口公开,其他服务才能访问它:

apiVersion: apps/v1a1
kind: Deployment
metadata:
 name: hue-reminders
spec:
 replicas: 2 
 template:
 metadata:
 name: hue-reminders
 labels:
 app: hue-reminders
 spec: 
 containers:
 - name: hue-reminders
 image: g1g1/hue-reminders:v2.2 
 ports:
 - containerPort: 80 

当我们运行部署时,两个 Hue reminders pod 被添加到集群中:

> kubectl create -f hue-reminders-deployment.yaml
> kubectl get pods
NAME                           READY   STATUS    RESTARTS   AGE
hue-learn-56886758d8-h7vm7      1/1    Running       0      49m
hue-learn-56886758d8-lqptj      1/1    Running       0      49m
hue-learn-56886758d8-zwkqt      1/1    Running       0      49m
hue-reminders-75c88cdfcf-5xqtp  1/1    Running       0      50s
hue-reminders-75c88cdfcf-r6jsx  1/1    Running       0      50s 

好的,pod 正在运行。理论上,其他服务可以查找或配置其内部 IP 地址,并直接访问它们,因为它们都在同一个网络空间中。但这并不具有可扩展性。每当一个 reminders pod 死掉并被新的 pod 替换,或者当我们只是扩展 pod 的数量时,所有访问这些 pod 的服务都必须知道这一点。服务通过提供所有 pod 的单一访问点来解决这个问题。服务如下:

apiVersion: v1
kind: Service
metadata:
 name: hue-reminders
 labels:
 app: hue-reminders 
spec:
 ports:
 - port: 80
 protocol: TCP
 selector:
 app: hue-reminders

该服务具有一个选择器,选择所有具有与其匹配的标签的 pod。它还公开一个端口,其他服务将使用该端口来访问它(它不必与容器的端口相同)。

创建 hue-reminders 服务

让我们创建服务并稍微探索一下:

> kubectl create -f hue-reminders-service.yaml
service "hue-reminders" created
> kubectl describe svc hue-reminders
Name:              hue-reminders
Namespace:         default
Labels:            app=hue-reminders
Annotations:       <none>
Selector:          app=hue-reminders
Type:              ClusterIP
IP:                10.108.163.209
Port:              <unset>  80/TCP
TargetPort:        80/TCP
Endpoints:         172.17.0.4:80,172.17.0.6:80
Session Affinity:  None
Events:            <none>  

服务正在运行。其他 pod 可以通过环境变量或 DNS 找到它。所有服务的环境变量都是在 pod 创建时设置的。这意味着如果在创建服务时已经有一个 pod 在运行,您将不得不将其终止,并让 Kubernetes 使用环境变量重新创建它(您通过部署创建 pod,对吧?):

> kubectl exec hue-learn-56886758d8-fjzdd -- printenv | grep HUE_REMINDERS_SERVICE

HUE_REMINDERS_SERVICE_PORT=80
HUE_REMINDERS_SERVICE_HOST=10.108.163.209  

但是使用 DNS 要简单得多。您的服务 DNS 名称是:

<service name>.<namespace>.svc.cluster.local
> kubectl exec hue-learn-56886758d8-fjzdd -- nslookup hue-reminders
Server:    10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local 
Name:      hue-reminders
Address 1: 10.108.163.209 hue-reminders.default.svc.cluster.local  

将服务暴露给外部

该服务在集群内可访问。如果您想将其暴露给外部世界,Kubernetes 提供了两种方法:

  • 为直接访问配置NodePort

  • 如果在云环境中运行,请配置云负载均衡器

在为外部访问配置服务之前,您应该确保其安全。Kubernetes 文档中有一个涵盖所有细节的很好的示例:

github.com/kubernetes/examples/blob/master/staging/https-nginx/README.md

我们已经在第五章中介绍了原则,“配置 Kubernetes 安全性、限制和账户”。

以下是通过NodePort向外界暴露 Hue-reminders 服务的spec部分:

spec:
  type: NodePort
  ports:
  - port: 8080
    targetPort: 80
    protocol: TCP
    name: http
 - port: 443
   protocol: TCP
   name: https
 selector:
   app: hue-reminders

Ingress

Ingress是 Kubernetes 的一个配置对象,它可以让您将服务暴露给外部世界,并处理许多细节。它可以执行以下操作:

  • 为您的服务提供外部可见的 URL

  • 负载均衡流量

  • 终止 SSL

  • 提供基于名称的虚拟主机

要使用Ingress,您必须在集群中运行一个Ingress控制器。请注意,Ingress 仍处于测试阶段,并且有许多限制。如果您在 GKE 上运行集群,那么可能没问题。否则,请谨慎操作。Ingress控制器目前的一个限制是它不适用于扩展。因此,它还不是 Hue 平台的一个好选择。我们将在第十章“高级 Kubernetes 网络”中更详细地介绍Ingress控制器。

以下是Ingress资源的外观:

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
 name: test
spec:
 rules:
 - host: foo.bar.com
 http:
 paths:
 - path: /foo
 backend:
 serviceName: fooSvc
 servicePort: 80
 - host: bar.baz.com
 http:
 paths:
 - path: /bar
 backend:
 serviceName: barSvc
 servicePort: 80

Nginx Ingress控制器将解释此Ingress请求,并为 Nginx Web 服务器创建相应的配置文件:

http { 
  server { 
    listen 80; 
    server_name foo.bar.com; 

    location /foo { 
      proxy_pass http://fooSvc; 
    } 
  } 
  server { 
    listen 80; 
    server_name bar.baz.com; 

    location /bar { 
      proxy_pass http://barSvc; 
    } 
  } 
} 

可以创建其他控制器。

使用命名空间限制访问

Hue 项目进展顺利,我们有几百个微服务和大约 100 名开发人员和 DevOps 工程师在其中工作。相关微服务组出现,并且您会注意到许多这些组是相当自治的。它们完全不知道其他组。此外,还有一些敏感领域,如健康和财务,您将希望更有效地控制对其的访问。输入命名空间。

让我们创建一个新的服务,Hue-finance,并将其放在一个名为restricted的新命名空间中。

这是新的restricted命名空间的 YAML 文件:

kind: Namespace 
 apiVersion: v1
 metadata:
     name: restricted
     labels:
       name: restricted

> kubectl create -f restricted-namespace.yaml
namespace "restricted" created  

创建命名空间后,我们需要为命名空间配置上下文。这将允许限制访问仅限于此命名空间:

> kubectl config set-context restricted --namespace=restricted --cluster=minikube --user=minikube
Context "restricted" set.

> kubectl config use-context restricted
Switched to context "restricted". 

让我们检查我们的cluster配置:

> kubectl config view
apiVersion: v1
clusters:
- cluster:
 certificate-authority: /Users/gigi.sayfan/.minikube/ca.crt
 server: https://192.168.99.100:8443
 name: minikube
contexts:
- context:
 cluster: minikube
 user: minikube
 name: minikube
- context:
 cluster: minikube
 namespace: restricted
 user: minikube
 name: restricted
current-context: restricted
kind: Config
preferences: {}
users:
- name: minikube
 user:
 client-certificate: /Users/gigi.sayfan/.minikube/client.crt
 client-key: /Users/gigi.sayfan/.minikube/client.key

如您所见,当前上下文是restricted

现在,在这个空的命名空间中,我们可以创建我们的hue-finance服务,它将独立存在:

> kubectl create -f hue-finance-deployment.yaml
deployment "hue-finance" created

> kubectl get pods
NAME                           READY     STATUS    RESTARTS   AGE
hue-finance-7d4b84cc8d-gcjnz   1/1       Running   0          6s
hue-finance-7d4b84cc8d-tqvr9   1/1       Running   0          6s
hue-finance-7d4b84cc8d-zthdr   1/1       Running   0          6s  

不需要切换上下文。您还可以使用--namespace=<namespace>--all-namespaces命令行开关。

启动作业

Hue 部署了许多长时间运行的微服务进程,但也有许多运行、完成某个目标并退出的任务。Kubernetes 通过作业资源支持此功能。Kubernetes 作业管理一个或多个 pod,并确保它们运行直到成功。如果作业管理的 pod 中的一个失败或被删除,那么作业将运行一个新的 pod 直到成功。

这是一个运行 Python 进程计算 5 的阶乘的作业(提示:它是 120):

apiVersion: batch/v1
kind: Job
metadata:
  name: factorial5
spec:
  template:
    metadata:
      name: factorial5
    spec:
      containers:
      - name: factorial5
        image: python:3.6
        command: ["python", 
                  "-c", 
                  "import math; print(math.factorial(5))"]
      restartPolicy: Never      

请注意,restartPolicy必须是NeverOnFailure。默认的Always值是无效的,因为作业在成功完成后不应重新启动。

让我们启动作业并检查其状态:

> kubectl create -f .\job.yaml
job "factorial5" created

> kubectl get jobs
NAME         DESIRED   SUCCESSFUL   AGE
factorial5   1         1            25s  

默认情况下不显示已完成任务的 pod。您必须使用--show-all选项:

> kubectl get pods --show-all
NAME                           READY     STATUS      RESTARTS   AGE
factorial5-ntp22               0/1       Completed   0          2m
hue-finance-7d4b84cc8d-gcjnz   1/1       Running     0          9m
hue-finance-7d4b84cc8d-tqvr9   1/1       Running     0          8m
hue-finance-7d4b84cc8d-zthdr   1/1       Running     0          9m  

factorial5 pod 的状态为Completed。让我们查看它的输出:

> kubectl logs factorial5-ntp22
120  

并行运行作业

您还可以使用并行运行作业。规范中有两个字段,称为completionsparallelismcompletions默认设置为1。如果您需要多个成功完成,则增加此值。parallelism确定要启动多少个 pod。作业不会启动比成功完成所需的更多的 pod,即使并行数更大。

让我们运行另一个只睡眠20秒直到完成三次成功的作业。我们将使用parallelism因子为6,但只会启动三个 pod:

apiVersion: batch/v1
kind: Job
metadata:
 name: sleep20
spec:
 completions: 3
 parallelism: 6 
 template:
 metadata:
 name: sleep20
 spec:
 containers:
 - name: sleep20
 image: python:3.6
 command: ["python", 
 "-c", 
 "import time; print('started...'); 
 time.sleep(20); print('done.')"]
 restartPolicy: Never 

> Kubectl get pods 
NAME              READY  STATUS   RESTARTS  AGE
sleep20-1t8sd      1/1   Running    0       10s
sleep20-sdjb4      1/1   Running    0       10s
sleep20-wv4jc      1/1   Running    0       10s

清理已完成的作业

当作业完成时,它会保留下来 - 它的 pod 也是如此。这是有意设计的,这样您就可以查看日志或连接到 pod 并进行探索。但通常,当作业成功完成后,它就不再需要了。清理已完成的作业及其 pod 是您的责任。最简单的方法是简单地删除job对象,这将同时删除所有的 pod:

> kubectl delete jobs/factroial5
job "factorial5" deleted
> kubectl delete jobs/sleep20
job "sleep20" deleted  

安排 cron 作业

Kubernetes cron 作业是在指定时间内运行一次或多次的作业。它们的行为类似于常规的 Unix cron 作业,指定在/etc/crontab文件中。

在 Kubernetes 1.4 中,它们被称为ScheduledJob。但是,在 Kubernetes 1.5 中,名称更改为CronJob。从 Kubernetes 1.8 开始,默认情况下在 API 服务器中启用了CronJob资源,不再需要传递--runtime-config标志,但它仍处于beta阶段。以下是启动一个每分钟提醒您伸展的 cron 作业的配置。在计划中,您可以用?替换*

apiVersion: batch/v1beta1
kind: CronJob
metadata:
 name: stretch
spec:
 schedule: "*/1 * * * *"
 jobTemplate:
 spec:
 template:
 metadata:
 labels:
 name: stretch 
 spec:
 containers:
 - name: stretch
 image: python
 args:
 - python
 - -c
 - from datetime import datetime; print('[{}] Stretch'.format(datetime.now()))
 restartPolicy: OnFailure

在 pod 规范中,在作业模板下,我添加了一个名为name的标签。原因是 Kubernetes 会为 cron 作业及其 pod 分配带有随机前缀的名称。该标签允许您轻松发现特定 cron 作业的所有 pod。请参阅以下命令行:

> kubectl get pods
NAME                       READY     STATUS              RESTARTS   AGE
stretch-1482165720-qm5bj   0/1       ImagePullBackOff    0          1m
stretch-1482165780-bkqjd   0/1       ContainerCreating   0          6s  

请注意,每次调用 cron 作业都会启动一个新的job对象和一个新的 pod:

> kubectl get jobs
NAME                 DESIRED   SUCCESSFUL   AGE
stretch-1482165300   1         1            11m
stretch-1482165360   1         1            10m
stretch-1482165420   1         1            9m
stretch-1482165480   1         1            8m 

当 cron 作业调用完成时,它的 pod 进入Completed状态,并且不会在没有-show-all-a标志的情况下可见:

> Kubectl get pods --show-all
NAME                       READY     STATUS      RESTARTS   AGE
stretch-1482165300-g5ps6   0/1       Completed   0          15m
stretch-1482165360-cln08   0/1       Completed   0          14m
stretch-1482165420-n8nzd   0/1       Completed   0          13m
stretch-1482165480-0jq31   0/1       Completed   0          12m  

通常情况下,您可以使用logs命令来检查已完成的 cron 作业的 pod 的输出:

> kubectl logs stretch-1482165300-g5ps6
[2016-12-19 16:35:15.325283] Stretch 

当您删除一个 cron 作业时,它将停止安排新的作业,并删除所有现有的作业对象以及它创建的所有 pod。

您可以使用指定的标签(在本例中名称等于STRETCH)来定位由 cron 作业启动的所有作业对象。您还可以暂停 cron 作业,以便它不会创建更多的作业,而无需删除已完成的作业和 pod。您还可以通过设置在 spec 历史限制中管理以前的作业:spec.successfulJobsHistoryLimit.spec.failedJobsHistoryLimit

混合非集群组件

Kubernetes 集群中的大多数实时系统组件将与集群外的组件进行通信。这些可能是完全外部的第三方服务,可以通过某些 API 访问,但也可能是在同一本地网络中运行的内部服务,由于各种原因,这些服务不是 Kubernetes 集群的一部分。

这里有两个类别:网络内部和网络外部。为什么这种区别很重要?

集群外网络组件

这些组件无法直接访问集群。它们只能通过 API、外部可见的 URL 和公开的服务来访问。这些组件被视为任何外部用户一样。通常,集群组件将只使用外部服务,这不会造成安全问题。例如,在我以前的工作中,我们有一个将异常报告给第三方服务的 Kubernetes 集群(sentry.io/welcome/)。这是从 Kubernetes 集群到第三方服务的单向通信。

网络内部组件

这些是在网络内部运行但不受 Kubernetes 管理的组件。有许多原因可以运行这些组件。它们可能是尚未 Kubernetized 的传统应用程序,或者是一些不容易在 Kubernetes 内部运行的分布式数据存储。将这些组件运行在网络内部的原因是为了性能,并且与外部世界隔离,以便这些组件和 pod 之间的流量更加安全。作为相同网络的一部分确保低延迟,并且减少了身份验证的需求既方便又可以避免身份验证开销。

使用 Kubernetes 管理 Hue 平台

在这一部分,我们将看一下 Kubernetes 如何帮助操作像 Hue 这样的大型平台。Kubernetes 本身提供了许多功能来编排 Pod 和管理配额和限制,检测和从某些类型的通用故障(硬件故障、进程崩溃和无法访问的服务)中恢复。但是,在 Hue 这样一个复杂的系统中,Pod 和服务可能正在运行,但处于无效状态或等待其他依赖项以执行其职责。这很棘手,因为如果一个服务或 Pod 还没有准备好,但已经收到请求,那么你需要以某种方式管理它:失败(将责任放在调用者身上),重试(多少次? 多长时间? 多频繁?),并排队等待以后(谁来管理这个队列?)。

如果整个系统能够意识到不同组件的就绪状态,或者只有当组件真正就绪时才可见,通常会更好。Kubernetes 并不了解 Hue,但它提供了几种机制,如活跃性探针、就绪性探针和 Init Containers,来支持你的集群的应用程序特定管理。

使用活跃性探针来确保你的容器是活着的

Kubectl 监视着你的容器。如果容器进程崩溃,Kubelet 会根据重启策略来处理它。但这并不总是足够的。你的进程可能不会崩溃,而是陷入无限循环或死锁。重启策略可能不够微妙。通过活跃性探针,你可以决定何时认为容器是活着的。这里有一个 Hue 音乐服务的 Pod 模板。它有一个livenessProbe部分,使用了httpGet探针。HTTP 探针需要一个方案(HTTP 或 HTTPS,默认为 HTTP),一个主机(默认为PodIp),一个path和一个port。如果 HTTP 状态码在200399之间,探针被认为是成功的。你的容器可能需要一些时间来初始化,所以你可以指定一个initialDelayInSeconds。在这段时间内,Kubelet 不会进行活跃性检查:

apiVersion: v1
kind: Pod
metadata:
 labels:
 app: hue-music
 name: hue-music
spec:
 containers:
 image: the_g1g1/hue-music
 livenessProbe:
 httpGet:
 path: /pulse
 port: 8888
 httpHeaders:
 - name: X-Custom-Header
 value: ItsAlive
 initialDelaySeconds: 30
 timeoutSeconds: 1
 name: hue-music

如果任何容器的活跃性探针失败,那么 Pod 的重启策略就会生效。确保你的重启策略不是Never,因为那会使探针变得无用。

有两种其他类型的探针:

  • TcpSocket:只需检查端口是否打开

  • Exec:运行一个返回0表示成功的命令

使用就绪性探针来管理依赖关系

就绪探针用于不同的目的。您的容器可能已经启动运行,但可能依赖于此刻不可用的其他服务。例如,Hue-music 可能依赖于访问包含您听歌历史记录的数据服务。如果没有访问权限,它将无法执行其职责。在这种情况下,其他服务或外部客户端不应该向 Hue 音乐服务发送请求,但没有必要重新启动它。就绪探针解决了这种情况。当一个容器的就绪探针失败时,该容器的 pod 将从其注册的任何服务端点中移除。这确保请求不会涌入无法处理它们的服务。请注意,您还可以使用就绪探针暂时移除过载的 pod,直到它们排空一些内部队列。

这是一个示例就绪探针。我在这里使用 exec 探针来执行一个 custom 命令。如果命令退出时的退出代码为非零,容器将被关闭:

readinessProbe:
 exec:
 command: 
 - /usr/local/bin/checker
 - --full-check
 - --data-service=hue-multimedia-service
 initialDelaySeconds: 60
 timeoutSeconds: 5

在同一个容器上同时拥有就绪探针和存活探针是可以的,因为它们有不同的用途。

使用初始化容器进行有序的 pod 启动

存活和就绪探针非常好用。它们认识到,在启动时,可能会有一个容器尚未准备好的时间段,但不应被视为失败。为了适应这一点,有一个 initialDelayInSeconds 设置,容器在这段时间内不会被视为失败。但是,如果这个初始延迟可能非常长呢?也许,在大多数情况下,一个容器在几秒钟后就准备好处理请求了,但是因为初始延迟设置为五分钟以防万一,当容器处于空闲状态时,我们会浪费很多时间。如果容器是高流量服务的一部分,那么在每次升级后,许多实例都可能在五分钟后处于空闲状态,几乎使服务不可用。

初始化容器解决了这个问题。一个 pod 可能有一组初始化容器,在其他容器启动之前完成运行。初始化容器可以处理所有非确定性的初始化,并让应用容器通过它们的就绪探针尽量减少延迟。

初始化容器在 Kubernetes 1.6 中退出了 beta 版。您可以在 pod 规范中指定它们,作为 initContainers 字段,这与 containers 字段非常相似。以下是一个示例:

apiVersion: v1
kind: Pod
metadata:
 name: hue-fitness
spec:
 containers: 
 name: hue-fitness
 Image: hue-fitness:v4.4
 InitContainers:
 name: install
 Image: busybox
 command: /support/safe_init
 volumeMounts:
 - name: workdir
 mountPath: /workdir   

与 DaemonSet pods 共享

DaemonSet pods 是自动部署的 pod,每个节点一个(或指定节点的子集)。它们通常用于监视节点并确保它们正常运行。这是一个非常重要的功能,我们在第三章“监控、日志记录和故障排除”中讨论了节点问题检测器。但它们可以用于更多的功能。默认的 Kubernetes 调度程序的特性是根据资源可用性和请求来调度 pod。如果有很多不需要大量资源的 pod,许多 pod 将被调度到同一个节点上。让我们考虑一个执行小任务的 pod,然后,每秒钟,将其所有活动的摘要发送到远程服务。想象一下,平均每秒钟会有 50 个这样的 pod 被调度到同一个节点上。这意味着,每秒钟,50 个 pod 会进行 50 次几乎没有数据的网络请求。我们能不能将它减少 50 倍,只进行一次网络请求?使用DaemonSet pod,其他 50 个 pod 可以与其通信,而不是直接与远程服务通信。DaemonSet pod 将收集来自 50 个 pod 的所有数据,并且每秒钟将其汇总报告给远程服务。当然,这需要远程服务 API 支持汇总报告。好处是 pod 本身不需要修改;它们只需配置为与DaemonSet pod 在本地主机上通信,而不是与远程服务通信。DaemonSet pod 充当聚合代理。

这个配置文件的有趣之处在于,hostNetworkhostPIDhostIPC选项都设置为true。这使得 pod 能够有效地与代理通信,利用它们在同一物理主机上运行的事实。

apiVersion: apps/v1
kind: DaemonSet
metadata:
 name: hue-collect-proxy
 labels:
 tier: stats
 app: hue-collect-proxy
spec:
 template:
 metadata:
 labels:
 hue-collect-proxy
 spec:
 hostPID: true
 hostIPC: true
 hostNetwork: true
 containers:
 image: the_g1g1/hue-collect-proxy
 name: hue-collect-proxy

使用 Kubernetes 发展 Hue 平台

在本节中,我们将讨论扩展 Hue 平台和服务其他市场和社区的其他方法。问题始终是,“我们可以使用哪些 Kubernetes 功能和能力来解决新的挑战或要求?”

在企业中利用 Hue

企业通常无法在云中运行,要么是因为安全和合规性原因,要么是因为性能原因,因为系统必须处理数据和传统系统,这些系统不适合迁移到云上。无论哪种情况,企业的 Hue 必须支持本地集群和/或裸金属集群。

虽然 Kubernetes 最常部署在云上,甚至有一个特殊的云提供商接口,但它并不依赖于云,可以在任何地方部署。它需要更多的专业知识,但已经在自己的数据中心上运行系统的企业组织拥有这方面的专业知识。

CoreOS 提供了大量关于在裸机集群上部署 Kubernetes 集群的材料。

用 Hue 推动科学的进步

Hue 在整合来自多个来源的信息方面非常出色,这对科学界将是一个福音。想象一下 Hue 如何帮助来自不同领域的科学家进行多学科合作。

科学社区的网络可能需要在多个地理分布的集群上部署。这就是集群联邦。Kubernetes 考虑到了这种用例,并不断发展其支持。我们将在后面的章节中详细讨论这个问题。

用 Hue 教育未来的孩子

Hue 可以用于教育,并为在线教育系统提供许多服务。但隐私问题可能会阻止将 Hue 作为单一的集中系统用于儿童。一个可能的选择是建立一个单一的集群,为不同学校设立命名空间。另一个部署选项是每个学校或县都有自己的 Hue Kubernetes 集群。在第二种情况下,Hue 教育必须非常易于操作,以满足没有太多技术专长的学校。Kubernetes 可以通过提供自愈和自动扩展功能来帮助 Hue,使其尽可能接近零管理。

总结

在本章中,我们设计和规划了 Hue 平台的开发、部署和管理——一个想象中的全知全能的服务,建立在微服务架构上。当然,我们使用 Kubernetes 作为底层编排平台,并深入探讨了许多它的概念和资源。特别是,我们专注于为长期运行的服务部署 pod,而不是为启动短期或定期作业部署作业,探讨了内部服务与外部服务,还使用命名空间来分割 Kubernetes 集群。然后,我们研究了像活跃性和就绪性探针、初始化容器和守护进程集这样的大型系统的管理。

现在,您应该能够设计由微服务组成的 Web 规模系统,并了解如何在 Kubernetes 集群中部署和管理它们。

在下一章中,我们将深入研究存储这个非常重要的领域。数据为王,但通常是系统中最不灵活的元素。Kubernetes 提供了一个存储模型,并提供了许多与各种存储解决方案集成的选项。

第七章:处理 Kubernetes 存储

在本章中,我们将看一下 Kubernetes 如何管理存储。存储与计算非常不同,但在高层次上它们都是资源。作为一个通用平台,Kubernetes 采取了在编程模型和一组存储提供者插件后面抽象存储的方法。首先,我们将详细介绍存储的概念模型以及如何将存储提供给集群中的容器。然后,我们将介绍常见的云平台存储提供者,如 AWS、GCE 和 Azure。然后我们将看一下著名的开源存储提供者(来自红帽的 GlusterFS),它提供了一个分布式文件系统。我们还将研究一种替代方案——Flocker——它将您的数据作为 Kubernetes 集群的一部分进行管理。最后,我们将看看 Kubernetes 如何支持现有企业存储解决方案的集成。

在本章结束时,您将对 Kubernetes 中存储的表示有扎实的了解,了解每个部署环境(本地测试、公共云和企业)中的各种存储选项,并了解如何为您的用例选择最佳选项。

持久卷演练

在这一部分,我们将看一下 Kubernetes 存储的概念模型,并了解如何将持久存储映射到容器中,以便它们可以读写。让我们先来看看存储的问题。容器和 Pod 是短暂的。当容器死亡时,容器写入自己文件系统的任何内容都会被清除。容器也可以挂载宿主节点的目录并进行读写。这样可以在容器重新启动时保留,但节点本身并不是不朽的。

还有其他问题,比如当容器死亡时,挂载的宿主目录的所有权。想象一下,一堆容器将重要数据写入它们的宿主机上的各个数据目录,然后离开,留下所有这些数据散落在节点上,没有直接的方法告诉哪个容器写入了哪些数据。您可以尝试记录这些信息,但您会在哪里记录呢?很明显,对于大规模系统,您需要从任何节点访问持久存储以可靠地管理数据。

基本的 Kubernetes 存储抽象是卷。容器挂载绑定到其 Pod 的卷,并访问存储,无论它在哪里,都好像它在它们的本地文件系统中一样。这并不新鲜,但很棒,因为作为一个需要访问数据的应用程序开发人员,您不必担心数据存储在何处以及如何存储。

使用 emptyDir 进行 Pod 内通信

使用共享卷在同一 Pod 中的容器之间共享数据非常简单。容器 1 和容器 2 只需挂载相同的卷,就可以通过读写到这个共享空间进行通信。最基本的卷是emptyDiremptyDir卷是主机上的empty目录。请注意,它不是持久的,因为当 Pod 从节点中移除时,内容会被擦除。如果容器崩溃,Pod 将继续存在,稍后可以访问它。另一个非常有趣的选项是使用 RAM 磁盘,通过指定介质为Memory。现在,您的容器通过共享内存进行通信,这样做速度更快,但当然更易失。如果节点重新启动,emptyDir卷的内容将丢失。

这是一个pod配置文件,其中有两个容器挂载名为shared-volume的相同卷。这些容器在不同的路径上挂载它,但当hue-global-listener容器将文件写入/notifications时,hue-job-scheduler将在/incoming下看到该文件。

apiVersion: v1
kind: Pod
metadata:
  name: hue-scheduler
spec:
  containers:
  - image: the_g1g1/hue-global-listener
    name: hue-global-listener
    volumeMounts:
    - mountPath: /notifications
      name: shared-volume
  - image: the_g1g1/hue-job-scheduler
    name: hue-job-scheduler
    volumeMounts:
    - mountPath: /incoming
      name: shared-volume
  volumes:
  - name: shared-volume
    emptyDir: {}

要使用共享内存选项,我们只需要在emptyDir部分添加medium:Memory

volumes:
- name: shared-volume
  emptyDir:
   medium: Memory 

使用 HostPath 进行节点内通信

有时,您希望您的 Pod 可以访问一些主机信息(例如 Docker 守护程序),或者您希望同一节点上的 Pod 可以相互通信。如果 Pod 知道它们在同一主机上,这将非常有用。由于 Kubernetes 根据可用资源调度 Pod,Pod 通常不知道它们与哪些其他 Pod 共享节点。有两种情况下,Pod 可以依赖于其他 Pod 与其一起在同一节点上调度:

  • 在单节点集群中,所有 Pod 显然共享同一节点

  • DaemonSet Pod 始终与与其选择器匹配的任何其他 Pod 共享节点

例如,在第六章中,使用关键的 Kubernetes 资源,我们讨论了一个作为聚合代理的 DaemonSet pod 到其他 pod 的。实现此行为的另一种方法是让 pod 将其数据简单地写入绑定到host目录的挂载卷,然后 DaemonSet pod 可以直接读取并对其进行操作。

在决定使用 HostPath 卷之前,请确保您了解限制:

  • 具有相同配置的 pod 的行为可能会有所不同,如果它们是数据驱动的,并且它们主机上的文件不同

  • 它可能会违反基于资源的调度(即将推出到 Kubernetes),因为 Kubernetes 无法监视 HostPath 资源

  • 访问主机目录的容器必须具有privileged设置为true的安全上下文,或者在主机端,您需要更改权限以允许写入

这是一个配置文件,将/coupons目录挂载到hue-coupon-hunter容器中,该容器映射到主机的/etc/hue/data/coupons目录:

apiVersion: v1
kind: Pod
metadata:
  name: hue-coupon-hunter
spec:
  containers:
  - image: the_g1g1/hue-coupon-hunter
    name: hue-coupon-hunter
    volumeMounts:
    - mountPath: /coupons
      name: coupons-volume 
  volumes:
  - name: coupons-volume
    host-path: 
        path: /etc/hue/data/coupons

由于 pod 没有privileged安全上下文,它将无法写入host目录。让我们改变容器规范以通过添加安全上下文来启用它:

- image: the_g1g1/hue-coupon-hunter
   name: hue-coupon-hunter
   volumeMounts:
   - mountPath: /coupons
     name: coupons-volume
   securityContext:
          privileged: true

在下图中,您可以看到每个容器都有自己的本地存储区,其他容器或 pod 无法访问,并且主机的/data目录被挂载为卷到容器 1 和容器 2:

使用本地卷进行持久节点存储

本地卷类似于 HostPath,但它们在 pod 重新启动和节点重新启动时保持不变。在这种意义上,它们被视为持久卷。它们在 Kubernetes 1.7 中添加。截至 Kubernetes 1.10 需要启用功能门。本地卷的目的是支持 StatefulSet,其中特定的 pod 需要被调度到包含特定存储卷的节点上。本地卷具有节点亲和性注释,简化了将 pod 绑定到它们需要访问的存储的过程:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: example-pv
  annotations:
        "volume.alpha.kubernetes.io/node-affinity": '{
            "requiredDuringSchedulingIgnoredDuringExecution": {
                "nodeSelectorTerms": [
                    { "matchExpressions": [
                        { "key": "kubernetes.io/hostname",
                          "operator": "In",
                          "values": ["example-node"]
                        }
                    ]}
                 ]}
              }'
spec:
    capacity:
      storage: 100Gi
    accessModes:
    - ReadWriteOnce
    persistentVolumeReclaimPolicy: Delete
    storageClassName: local-storage
    local:
      path: /mnt/disks/ssd1

提供持久卷

emptyDir 卷可以被挂载和容器使用,但它们不是持久的,也不需要任何特殊的配置,因为它们使用节点上的现有存储。HostPath 卷在原始节点上持久存在,但如果 pod 在不同的节点上重新启动,它无法访问先前节点上的 HostPath 卷。Local 卷在节点上持久存在,可以在 pod 重新启动、重新调度甚至节点重新启动时幸存下来。真正的持久卷使用提前由管理员配置的外部存储(不是物理连接到节点的磁盘)。在云环境中,配置可能非常简化,但仍然是必需的,作为 Kubernetes 集群管理员,您至少要确保您的存储配额是充足的,并且要认真监控使用情况与配额的对比。

请记住,持久卷是 Kubernetes 集群类似于节点使用的资源。因此,它们不受 Kubernetes API 服务器的管理。您可以静态或动态地配置资源。

  • 静态配置持久卷:静态配置很简单。集群管理员提前创建由某些存储介质支持的持久卷,这些持久卷可以被容器声明。

  • 动态配置持久卷:当持久卷声明与静态配置的持久卷不匹配时,动态配置可能会发生。如果声明指定了存储类,并且管理员为该类配置了动态配置,那么持久卷可能会被即时配置。当我们讨论持久卷声明和存储类时,我们将在后面看到示例。

  • 外部配置持久卷:最近的一个趋势是将存储配置器从 Kubernetes 核心移出到卷插件(也称为 out-of-tree)。外部配置器的工作方式与 in-tree 动态配置器相同,但可以独立部署和更新。越来越多的 in-tree 存储配置器迁移到 out-of-tree。查看这个 Kubernetes 孵化器项目:github.com/kubernetes-incubator/external-storage

创建持久卷

以下是 NFS 持久卷的配置文件:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-1
  labels:
     release: stable
     capacity: 100Gi 
spec:
  capacity:
    storage: 100Gi
  volumeMode: Filesystem
  accessModes:
    - ReadWriteOnce
   - ReadOnlyMany
  persistentVolumeReclaimPolicy: Recycle
  storageClassName: normal
  nfs:
    path: /tmp
    server: 172.17.0.8

持久卷具有包括名称在内的规范和元数据。让我们在这里关注规范。有几个部分:容量、卷模式、访问模式、回收策略、存储类和卷类型(例如示例中的nfs)。

容量

每个卷都有指定的存储量。存储索赔可以由至少具有该存储量的持久卷满足。例如,持久卷的容量为100 Gibibytes(2³⁰字节)。在分配静态持久卷时,了解存储请求模式非常重要。例如,如果您配置了 100 GiB 容量的 20 个持久卷,并且容器索赔了 150 GiB 的持久卷,则即使总体容量足够,该索赔也不会得到满足:

capacity:
    storage: 100Gi 

卷模式

可选的卷模式在 Kubernetes 1.9 中作为静态配置的 Alpha 功能添加(即使您在规范中指定它作为字段,而不是在注释中)。它允许您指定是否需要文件系统("Filesystem")或原始存储("Block")。如果不指定卷模式,则默认值是"Filesystem",就像在 1.9 之前一样。

访问模式

有三种访问模式:

  • ReadOnlyMany:可以由多个节点挂载为只读

  • ReadWriteOnce:可以由单个节点挂载为读写

  • ReadWriteMany:可以由多个节点挂载为读写

存储被挂载到节点,所以即使使用ReadWriteOnce,同一节点上的多个容器也可以挂载该卷并对其进行写入。如果这造成问题,您需要通过其他机制来处理(例如,您可以只在您知道每个节点只有一个的 DaemonSet pods 中索赔该卷)。

不同的存储提供程序支持这些模式的一些子集。当您配置持久卷时,可以指定它将支持哪些模式。例如,NFS 支持所有模式,但在示例中,只启用了这些模式:

accessModes:
    - ReadWriteMany
   - ReadOnlyMany

回收策略

回收策略确定持久卷索赔被删除时会发生什么。有三种不同的策略:

  • Retain:需要手动回收卷

  • Delete:关联的存储资产,如 AWS EBS、GCE PD、Azure 磁盘或 OpenStack Cinder 卷,将被删除

  • Recycle:仅删除内容(rm -rf /volume/*

RetainDelete策略意味着持久卷将不再对未来索赔可用。recycle策略允许再次索赔该卷。

目前,只有 NFS 和 HostPath 支持回收。AWS EBS、GCE PD、Azure 磁盘和 Cinder 卷支持删除。动态配置的卷总是被删除。

存储类

您可以使用规范的可选storageClassName字段指定存储类。如果这样做,那么只有指定相同存储类的持久卷要求才能绑定到持久卷。如果不指定存储类,则只有不指定存储类的持久卷要求才能绑定到它。

卷类型

卷类型在规范中通过名称指定。没有volumeType部分。

在前面的示例中,nfs是卷类型:

nfs:
    path: /tmp
    server: 172.17.0.8 

每种卷类型可能有自己的一组参数。在这种情况下,它是一个path

server

我们将在本章后面讨论各种卷类型。

提出持久卷要求

当容器需要访问某些持久存储时,它们会提出要求(或者说,开发人员和集群管理员会协调必要的存储资源

要求)。以下是一个与上一节中的持久卷匹配的示例要求:

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: storage-claim
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 80Gi
  storageClassName: "normal"
  selector:
    matchLabels:
      release: "stable"
    matchExpressions:
      - {key: capacity, operator: In, values: [80Gi, 100Gi]}

名称storage-claim在将要将要求挂载到容器中时将变得重要。

规范中的访问模式为ReadWriteOnce,这意味着如果要求得到满足,则不能满足其他具有ReadWriteOnce访问模式的要求,但仍然可以满足ReadOnlyMany的要求。

资源部分请求 80 GiB。这可以通过我们的持久卷满足,它的容量为 100 GiB。但这有点浪费,因为 20 GiB 将不会被使用。

存储类名称为"normal"。如前所述,它必须与持久卷的类名匹配。但是,对于持久卷要求PVC),空类名("")和没有类名之间存在差异。前者(空类名)与没有存储类名的持久卷匹配。后者(没有类名)只有在关闭DefaultStorageClass准入插件或者打开并且使用默认存储类时才能绑定到持久卷。

Selector部分允许您进一步过滤可用的卷。例如,在这里,卷必须匹配标签release: "stable",并且还必须具有标签capacity: 80 Gicapacity: 100 Gi。假设我们还有其他几个容量为 200 Gi 和 500 Gi 的卷。当我们只需要 80 Gi 时,我们不希望索赔 500 Gi 的卷。

Kubernetes 始终尝试匹配可以满足索赔的最小卷,但如果没有 80 Gi 或 100 Gi 的卷,那么标签将阻止分配 200 Gi 或 500 Gi 的卷,并使用动态配置。

重要的是要意识到索赔不会按名称提及卷。匹配是由基于存储类、容量和标签的 Kubernetes 完成的。

最后,持久卷索赔属于命名空间。将持久卷绑定到索赔是排他的。这意味着持久卷将绑定到一个命名空间。即使访问模式是ReadOnlyManyReadWriteMany,所有挂载持久卷索赔的 Pod 必须来自该索赔的命名空间。

将索赔作为卷

好的。我们已经配置了一个卷并对其进行了索赔。现在是时候在容器中使用索赔的存储了。这其实非常简单。首先,持久卷索赔必须在 Pod 中用作卷,然后 Pod 中的容器可以像任何其他卷一样挂载它。这是一个pod配置文件,指定了我们之前创建的持久卷索赔(绑定到我们配置的 NFS 持久卷)。

kind: Pod
apiVersion: v1
metadata:
  name: the-pod
spec:
  containers:
    - name: the-container
      image: some-image
      volumeMounts:
      - mountPath: "/mnt/data"
        name: persistent-volume
  volumes:
    - name: persistent-volume
      persistentVolumeClaim:
        claimName: storage-claim

关键在volumes下的persistentVolumeClaim部分。索赔名称(这里是storage-claim)在当前命名空间内唯一标识特定索赔,并使其作为卷命名为persistent-volume。然后,容器可以通过名称引用它,并将其挂载到/mnt/data

原始块卷

Kubernetes 1.9 将此功能作为 alpha 功能添加。您必须使用功能门控来启用它:--feature-gates=BlockVolume=true

原始块卷提供对底层存储的直接访问,不经过文件系统抽象。这对需要高存储性能的应用程序非常有用,比如数据库,或者需要一致的 I/O 性能和低延迟。光纤通道、iSCSI 和本地 SSD 都适用于用作原始块存储。目前(Kubernetes 1.10),只有Local VolumeFiberChannel存储提供程序支持原始块卷。以下是如何定义原始块卷:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: block-pv
spec:
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteOnce
  volumeMode: Block
  persistentVolumeReclaimPolicy: Retain
  fc:
    targetWWNs: ["50060e801049cfd1"]
    lun: 0
    readOnly: false

匹配的 PVC 必须指定volumeMode: Block。这是它的样子:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: block-pvc
spec:
  accessModes:
    - ReadWriteOnce
  volumeMode: Block
  resources:
    requests:
      storage: 10Gi

Pods 将原始块卷作为/dev下的设备而不是挂载的文件系统来消耗。容器可以访问这个设备并对其进行读/写。实际上,这意味着对块存储的 I/O 请求直接传递到底层块存储,而不经过文件系统驱动程序。理论上这更快,但实际上如果您的应用程序受益于文件系统缓冲,它实际上可能会降低性能。

这是一个带有容器的 Pod,它将block-pvc与原始块存储绑定为名为/dev/xdva的设备:

apiVersion: v1
kind: Pod
metadata:
  name: pod-with-block-volume
spec:
  containers:
    - name: fc-container
      image: fedora:26
      command: ["/bin/sh", "-c"]
      args: [ "tail -f /dev/null" ]
      volumeDevices:
        - name: data
          devicePath: /dev/xvda
  volumes:
    - name: data
      persistentVolumeClaim:
        claimName: block-pvc

存储类

存储类允许管理员使用自定义持久存储配置集群(只要有适当的插件支持)。存储类在metadata中有一个name,一个provisionerparameters

kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: standard
provisioner: kubernetes.io/aws-ebs
parameters:
  type: gp2

您可以为同一个提供程序创建多个存储类,每个提供程序都有自己的参数。

目前支持的卷类型如下:

  • AwsElasticBlockStore

  • AzureFile

  • AzureDisk

  • CephFS

  • Cinder

  • FC

  • FlexVolume

  • Flocker

  • GcePersistentDisk

  • GlusterFS

  • ISCSI

  • PhotonPersistentDisk

  • Quobyte

  • NFS

  • RBD

  • VsphereVolume

  • PortworxVolume

  • ScaleIO

  • StorageOS

  • Local

这个列表不包含其他卷类型,比如gitReposecret,这些类型不是由典型的网络存储支持的。Kubernetes 的这个领域仍然在变化中,将来它会进一步解耦,设计会更清晰,插件将不再是 Kubernetes 本身的一部分。智能地利用卷类型是架构和管理集群的重要部分。

默认存储类

集群管理员还可以分配一个默认的storage类。当分配了默认的存储类并且打开了DefaultStorageClass准入插件时,那么没有存储类的声明将使用默认的storage类进行动态配置。如果默认的storage类没有定义或者准入插件没有打开,那么没有存储类的声明只能匹配没有storage类的卷。

演示持久卷存储的端到端

为了说明所有的概念,让我们进行一个小型演示,创建一个 HostPath 卷,声明它,挂载它,并让容器写入它。

让我们首先创建一个hostPath卷。将以下内容保存在persistent-volume.yaml中:

kind: PersistentVolume
apiVersion: v1
metadata:
 name: persistent-volume-1
spec:
 StorageClassName: dir
 capacity:
 storage: 1Gi
 accessModes:
 - ReadWriteOnce
 hostPath:
 path: "/tmp/data"

> kubectl create -f persistent-volume.yaml
persistentvolume "persistent-volume-1" created

要查看可用的卷,可以使用persistentvolumes资源类型,或者简称为pv

> kubectl get pv
NAME:             persistent-volume-1 
CAPACITY:         1Gi
ACCESS MODES:     RWO 
RECLAIM POLICY:   Retain 
STATUS:           Available 
CLAIM: 
STORAGECLASS:    dir
REASON: 
AGE:             17s 

我稍微编辑了一下输出,以便更容易看到。容量为 1 GiB,符合要求。回收策略是Retain,因为HostPath卷是保留的。状态为Available,因为卷尚未被声明。访问模式被指定为RWX,表示ReadWriteMany。所有访问模式都有一个简写版本:

  • RWOReadWriteOnce

  • ROXReadOnlyMany

  • RWXReadWriteMany

我们有一个持久卷。让我们创建一个声明。将以下内容保存到persistent-volume-claim.yaml中:

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
 name: persistent-volume-claim
spec:
 accessModes:
 - ReadWriteOnce
 resources:
 requests:
 storage: 1Gi

然后,运行以下命令:

> kubectl create -f  persistent-volume-claim.yaml
persistentvolumeclaim "persistent-volume-claim" created  

让我们检查一下claimvolume

> kubectl get pvc
NAME                                  STATUS  VOLUME                     CAPACITY   ACCESSMODES   AGE
persistent-volume-claim   Bound     persistent-volume-1   1Gi        RWO            dir            1m

> kubectl get pv
NAME:                 persistent-volume-1
CAPACITY:             1Gi
ACCESS MODES:         RWO 
RECLAIM POLICY:       Retain
STATUS:               Bound 
CLAIM:               default/persistent-volume-claim 
STORAGECLASS:        dir
REASON: 
AGE:                 3m  

如您所见,claimvolume已经绑定在一起。最后一步是创建一个pod并将claim分配为volume。将以下内容保存到shell-pod.yaml中:

kind: Pod
apiVersion: v1
metadata:
 name: just-a-shell
 labels:
 name: just-a-shell
spec:
 containers:
 - name: a-shell
 image: ubuntu
 command: ["/bin/bash", "-c", "while true ; do sleep 10 ; done"]
 volumeMounts:
 - mountPath: "/data"
 name: pv
 - name: another-shell
 image: ubuntu
 command: ["/bin/bash", "-c", "while true ; do sleep 10 ; done"]
 volumeMounts:
 - mountPath: "/data"
 name: pv
 volumes:
 - name: pv
 persistentVolumeClaim:
 claimName: persistent-volume-claim

这个 pod 有两个容器,它们使用 Ubuntu 镜像,并且都运行一个shell命令,只是在无限循环中睡眠。这样做的目的是让容器保持运行,这样我们以后可以连接到它们并检查它们的文件系统。该 pod 将我们的持久卷声明挂载为pv的卷名。两个容器都将其挂载到它们的/data目录中。

让我们创建pod并验证两个容器都在运行:

> kubectl create -f shell-pod.yaml
pod "just-a-shell" created

> kubectl get pods
NAME           READY     STATUS    RESTARTS   AGE
just-a-shell   2/2       Running   0           1m 

然后,ssh到节点。这是主机,其/tmp/data是 pod 的卷,挂载为每个正在运行的容器的/data

> minikube ssh
$

在节点内部,我们可以使用 Docker 命令与容器进行通信。让我们看一下最后两个正在运行的容器:

$ docker ps -n 2 --format '{{.ID}}\t{{.Image}}\t{{.Command}}'
820fc954fb96     ubuntu    "/bin/bash -c 'whi..."
cf4502f14be5     ubuntu    "/bin/bash -c 'whi..."

然后,在主机的/tmp/data目录中创建一个文件。它应该通过挂载的卷对两个容器都可见:

$ sudo touch /tmp/data/1.txt

让我们在其中一个容器上执行一个shell,验证文件1.txt确实可见,并创建另一个文件2.txt

$ docker exec -it 820fc954fb96  /bin/bash
root@just-a-shell:/# ls /data
1.txt
root@just-a-shell:/# touch /data/2.txt
root@just-a-shell:/# exit
Finally, we can run a shell on the other container and verify that both 1.txt and 2.txt are visible:
docker@minikube:~$ docker exec -it cf4502f14be5 /bin/bash
root@just-a-shell:/# ls /data
1.txt  2.txt

公共存储卷类型 - GCE,AWS 和 Azure

在本节中,我们将介绍一些主要公共云平台中可用的常见卷类型。在规模上管理存储是一项困难的任务,最终涉及物理资源,类似于节点。如果您选择在公共云平台上运行您的 Kubernetes 集群,您可以让您的云提供商处理所有这些挑战,并专注于您的系统。但重要的是要了解每种卷类型的各种选项、约束和限制。

AWS 弹性块存储(EBS)

AWS 为 EC2 实例提供 EBS 作为持久存储。AWS Kubernetes 集群可以使用 AWS EBS 作为持久存储,但有以下限制:

  • pod 必须在 AWS EC2 实例上作为节点运行

  • Pod 只能访问其可用区中配置的 EBS 卷

  • EBS 卷可以挂载到单个 EC2 实例

这些是严重的限制。单个可用区的限制,虽然对性能有很大帮助,但消除了在规模或地理分布系统中共享存储的能力,除非进行自定义复制和同步。单个 EBS 卷限制为单个 EC2 实例意味着即使在同一可用区内,pod 也无法共享存储(甚至是读取),除非您确保它们在同一节点上运行。

在解释所有免责声明之后,让我们看看如何挂载 EBS 卷:

apiVersion: v1
kind: Pod
metadata:
 name: some-pod
spec:
 containers:
 - image: some-container
 name: some-container
 volumeMounts:
 - mountPath: /ebs
 name: some-volume
 volumes:
 - name: some-volume
 awsElasticBlockStore:
 volumeID: <volume-id>
 fsType: ext4

您必须在 AWS 中创建 EBS 卷,然后将其挂载到 pod 中。不需要声明或存储类,因为您通过 ID 直接挂载卷。awsElasticBlockStore卷类型为 Kubernetes 所知。

AWS 弹性文件系统

AWS 最近推出了一项名为弹性文件系统EFS)的新服务。这实际上是一个托管的 NFS 服务。它使用 NFS 4.1 协议,并且与 EBS 相比有许多优点:

  • 多个 EC2 实例可以跨多个可用区(但在同一区域内)访问相同的文件

  • 容量根据实际使用情况自动扩展和缩减

  • 您只支付您使用的部分

  • 您可以通过 VPN 将本地服务器连接到 EFS

  • EFS 运行在自动在可用区之间复制的 SSD 驱动器上

话虽如此,即使考虑到自动复制到多个可用区(假设您充分利用了 EBS 卷),EFS 比 EBS 更加广泛。它正在使用外部供应商,部署起来并不是微不足道的。请按照这里的说明进行操作:

github.com/kubernetes-incubator/external-storage/tree/master/aws/efs

一旦一切都设置好了,并且您已经定义了存储类,并且持久卷存在,您可以创建一个声明,并将其以ReadWriteMany模式挂载到尽可能多的pod中。这是持久声明:

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
 name: efs
 annotations:
 volume.beta.kubernetes.io/storage-class: "aws-efs"
spec:
 accessModes:
 - ReadWriteMany
 resources:
 requests:
 storage: 1Mi

这是一个使用它的pod

kind: Pod
apiVersion: v1
metadata:
 name: test-pod
spec:
 containers:
 - name: test-pod
 image: gcr.io/google_containers/busybox:1.24
 command:
 - "/bin/sh"
 args:
 - "-c"
 - "touch /mnt/SUCCESS exit 0 || exit 1"
 volumeMounts:
 - name: efs-pvc
 mountPath: "/mnt"
 restartPolicy: "Never"
 volumes:
 - name: efs-pvc
 persistentVolumeClaim:
 claimName: efs

GCE 持久磁盘

gcePersistentDisk卷类型与awsElasticBlockStore非常相似。您必须提前规划磁盘。它只能被同一项目和区域中的 GCE 实例使用。但是同一卷可以在多个实例上以只读方式使用。这意味着它支持ReadWriteOnceReadOnlyMany。您可以使用 GCE 持久磁盘在同一区域的多个pod之间共享数据。

使用ReadWriteOnce模式中的持久磁盘的pod必须由复制控制器、副本集或具有01个副本计数的部署控制。尝试扩展到1之外的数量将因明显原因而失败:

apiVersion: v1
kind: Pod
metadata:
 name: some-pod
spec:
 containers:
 - image: some-container
 name: some-container
 volumeMounts:
 - mountPath: /pd
 name: some-volume
 volumes:
 - name: some-volume
 gcePersistentDisk:
 pdName: <persistent disk name>
 fsType: ext4 

Azure 数据磁盘

Azure 数据磁盘是存储在 Azure 存储中的虚拟硬盘。它的功能类似于 AWS EBS。这是一个示例pod配置文件:

apiVersion: v1
kind: Pod
metadata:
 name: some-pod
spec:
 containers:
 - image: some-container
 name: some-container
 volumeMounts:
 - name: some-volume
 mountPath: /azure
 volumes:
 - name: some-volume
 azureDisk:
 diskName: test.vhd
 diskURI: https://someaccount.blob.microsoft.net/vhds/test.vhd 

除了强制的diskNamediskURI参数之外,它还有一些可选参数:

  • cachingMode:磁盘缓存模式。必须是NoneReadOnlyReadWrite之一。默认值为None

  • fsType:文件系统类型设置为mount。默认值为ext4

  • readOnly:文件系统是否以readOnly模式使用。默认值为false

Azure 数据磁盘的限制为 1,023 GB。每个 Azure VM 最多可以有 16 个数据磁盘。您可以将 Azure 数据磁盘附加到单个 Azure VM 上。

Azure 文件存储

除了数据磁盘,Azure 还有一个类似于 AWS EFS 的共享文件系统。但是,Azure 文件存储使用 SMB/CIFS 协议(支持 SMB 2.1 和 SMB 3.0)。它基于 Azure 存储平台,具有与 Azure Blob、Table 或 Queue 相同的可用性、耐用性、可扩展性和地理冗余能力。

为了使用 Azure 文件存储,您需要在每个客户端 VM 上安装cifs-utils软件包。您还需要创建一个secret,这是一个必需的参数:

apiVersion: v1
kind: Secret
metadata:
 name: azure-file-secret
type: Opaque
data:
 azurestorageaccountname: <base64 encoded account name>
 azurestorageaccountkey: <base64 encoded account key>

这是一个 Azure 文件存储的配置文件:

apiVersion: v1
kind: Pod
metadata:
 name: some-pod
spec:
 containers:
  - image: some-container
    name: some-container
    volumeMounts:
      - name: some-volume
        mountPath: /azure
 volumes:
      - name: some-volume
        azureFile:
          secretName: azure-file-secret
         shareName: azure-share
          readOnly: false

Azure 文件存储支持在同一地区内共享以及连接本地客户端。以下是说明工作流程的图表:

Kubernetes 中的 GlusterFS 和 Ceph 卷

GlusterFS 和 Ceph 是两个分布式持久存储系统。GlusterFS 在其核心是一个网络文件系统。Ceph 在核心是一个对象存储。两者都公开块、对象和文件系统接口。两者都在底层使用xfs文件系统来存储数据和元数据作为xattr属性。您可能希望在 Kubernetes 集群中使用 GlusterFS 或 Ceph 作为持久卷的几个原因:

  • 您可能有很多数据和应用程序访问 GlusterFS 或 Ceph 中的数据

  • 您具有管理和操作 GlusterFS 的专业知识

或 Ceph

  • 您在云中运行,但云平台持久存储的限制是一个非起点。

使用 GlusterFS

GlusterFS 故意简单,将底层目录公开,并留给客户端(或中间件)处理高可用性、复制和分发。GlusterFS 将数据组织成逻辑卷,其中包括包含文件的多个节点(机器)的砖块。文件根据 DHT(分布式哈希表)分配给砖块。如果文件被重命名或 GlusterFS 集群被扩展或重新平衡,文件可能会在砖块之间移动。以下图表显示了 GlusterFS 的构建模块:

要将 GlusterFS 集群用作 Kubernetes 的持久存储(假设您已经运行了 GlusterFS 集群),您需要遵循几个步骤。特别是,GlusterFS 节点由插件作为 Kubernetes 服务进行管理(尽管作为应用程序开发人员,这与您无关)。

创建端点

这是一个端点资源的示例,您可以使用kubectl create创建为普通的 Kubernetes 资源:

{
  "kind": "Endpoints",
  "apiVersion": "v1",
  "metadata": {
    "name": "glusterfs-cluster"
  },
  "subsets": [
    {
      "addresses": [
        {
          "ip": "10.240.106.152"
        }
      ],
      "ports": [
        {
          "port": 1
        }
      ]
    },
    {
      "addresses": [
        {
          "ip": "10.240.79.157"
        }
      ],
      "ports": [
        {
          "port": 1
        }
      ]
    }
  ]
}

添加 GlusterFS Kubernetes 服务

为了使端点持久,您可以使用一个没有选择器的 Kubernetes 服务来指示端点是手动管理的:

{
  "kind": "Service",
  "apiVersion": "v1",
  "metadata": {
    "name": "glusterfs-cluster"
  },
  "spec": {
    "ports": [
      {"port": 1}
    ]
  }
}

创建 Pods

最后,在 pod 规范的volumes部分中,提供以下信息:

"volumes": [
            {
                "name": "glusterfsvol",
                "glusterfs": {
                    "endpoints": "glusterfs-cluster",
                    "path": "kube_vol",
                    "readOnly": true
                }
            }
        ] 

然后容器可以按名称挂载glusterfsvol

endpoints告诉 GlusterFS 卷插件如何找到 GlusterFS 集群的存储节点。

使用 Ceph

Ceph 的对象存储可以使用多个接口访问。Kubernetes 支持RBD(块)和CEPHFS(文件系统)接口。以下图表显示了 RADOS - 底层对象存储 - 如何在多天内访问。与 GlusterFS 不同,Ceph 会自动完成大量工作。它自行进行分发、复制和自我修复:

使用 RBD 连接到 Ceph

Kubernetes 通过RadosBlockDeviceRBD)接口支持 Ceph。您必须在 Kubernetes 集群中的每个节点上安装ceph-common。一旦您的 Ceph 集群正常运行,您需要在pod配置文件中提供 Ceph RBD 卷插件所需的一些信息:

  • monitors:Ceph 监视器。

  • pool:RADOS 池的名称。如果未提供,则使用默认的 RBD 池。

  • image:RBD 创建的镜像名称。

  • user:RADOS 用户名。如果未提供,则使用默认的admin

  • keyringkeyring文件的路径。如果未提供,则使用默认的/etc/ceph/keyring

  • * secretName:认证密钥的名称。如果提供了一个,则secretName会覆盖keyring。注意:请参阅下一段关于如何创建secret的内容。

  • fsType:在其上格式化的文件系统类型(ext4xfs等)。

设备。

  • readOnly:文件系统是否以readOnly方式使用。

如果使用了 Ceph 认证secret,则需要创建一个secret对象:

apiVersion: v1
kind: Secret
metadata:
  name: ceph-secret
type: "kubernetes.io/rbd" 
data:
  key: QVFCMTZWMVZvRjVtRXhBQTVrQ1FzN2JCajhWVUxSdzI2Qzg0SEE9PQ==

secret类型为kubernetes.io/rbd

pod 规范的volumes部分看起来与此相同:

"volumes": [
    {
        "name": "rbdpd",
        "rbd": {
            "monitors": [
          "10.16.154.78:6789",
      "10.16.154.82:6789",
          "10.16.154.83:6789"
        ],
            "pool": "kube",
            "image": "foo",
            "user": "admin",
            "secretRef": {
      "name": "ceph-secret"
      },
            "fsType": "ext4",
            "readOnly": true
        }
    }
]

Ceph RBD 支持ReadWriteOnceReadOnlyMany访问模式。

使用 CephFS 连接到 Ceph

如果您的 Ceph 集群已经配置了 CephFS,则可以非常轻松地将其分配给 pod。此外,CephFS 支持ReadWriteMany访问模式。

配置类似于 Ceph RBD,只是没有池、镜像或文件系统类型。密钥可以是对 Kubernetes secret对象的引用(首选)或secret文件:

apiVersion: v1
kind: Pod
metadata:
  name: cephfs
spec:
  containers:
  - name: cephfs-rw
    image: kubernetes/pause
    volumeMounts:
    - mountPath: "/mnt/cephfs"
      name: cephfs
  volumes:
  - name: cephfs
    cephfs:
      monitors:
      - 10.16.154.78:6789
      - 10.16.154.82:6789
      - 10.16.154.83:6789
      user: admin
      secretFile: "/etc/ceph/admin.secret"
      readOnly: true

您还可以在cephfs系统中提供路径作为参数。默认为/

内置的 RBD 供应程序在外部存储 Kubernetes 孵化器项目中有一个独立的副本。

Flocker 作为集群容器数据卷管理器

到目前为止,我们已经讨论了将数据存储在 Kubernetes 集群之外的存储解决方案(除了emptyDir和 HostPath,它们不是持久的)。Flocker 有点不同。它是 Docker 感知的。它旨在让 Docker 数据卷在容器在节点之间移动时一起传输。如果你正在将基于 Docker 的系统从不同的编排平台(如 Docker compose 或 Mesos)迁移到 Kubernetes,并且你使用 Flocker 来编排存储,你可能想使用 Flocker 卷插件。就个人而言,我觉得 Flocker 所做的事情和 Kubernetes 为抽象存储所做的事情之间存在很多重复。

Flocker 有一个控制服务和每个节点上的代理。它的架构与 Kubernetes 非常相似,其 API 服务器和每个节点上运行的 Kubelet。Flocker 控制服务公开了一个 REST API,并管理着整个集群的状态配置。代理负责确保其节点的状态与当前配置匹配。例如,如果一个数据集需要在节点 X 上,那么节点 X 上的 Flocker 代理将创建它。

以下图表展示了 Flocker 架构:

为了在 Kubernetes 中使用 Flocker 作为持久卷,你首先必须有一个正确配置的 Flocker 集群。Flocker 可以与许多后备存储一起工作(再次,与 Kubernetes 持久卷非常相似)。

然后你需要创建 Flocker 数据集,这时你就可以将其连接为持久卷了。经过你的辛勤工作,这部分很容易,你只需要指定 Flocker 数据集的名称:

apiVersion: v1
kind: Pod
metadata:
  name: some-pod
spec:
  containers:
    - name: some-container
      image: kubernetes/pause
      volumeMounts:
          # name must match the volume name below
          - name: flocker-volume
            mountPath: "/flocker"
  volumes:
    - name: flocker-volume
      flocker:
        datasetName: some-flocker-dataset

将企业存储集成到 Kubernetes 中

如果你有一个通过 iSCSI 接口公开的现有存储区域网络SAN),Kubernetes 为你提供了一个卷插件。它遵循了我们之前看到的其他共享持久存储插件的相同模型。你必须配置 iSCSI 启动器,但你不必提供任何启动器信息。你只需要提供以下内容:

  • iSCSI 目标的 IP 地址和端口(如果不是默认的3260

  • 目标的iqn(iSCSI 合格名称)—通常是反向域名

  • LUN—逻辑单元号

  • 文件系统类型

  • readonly布尔标志

iSCSI 插件支持ReadWriteOnceReadonlyMany。请注意,目前无法对设备进行分区。以下是卷规范:

volumes:
  - name: iscsi-volume
    iscsi:
      targetPortal: 10.0.2.34:3260
      iqn: iqn.2001-04.com.example:storage.kube.sys1.xyz
      lun: 0
      fsType: ext4
      readOnly: true  

投影卷

可以将多个卷投影到单个目录中,使其显示为单个卷。支持的卷类型有:secretdownwardAPIconfigMap。如果您想将多个配置源挂载到一个 pod 中,这将非常有用。您可以将它们全部捆绑到一个投影卷中,而不必为每个源创建单独的卷。这是一个例子:

apiVersion: v1
kind: Pod
metadata:
  name: the-pod
spec:
  containers:
  - name: the-container
    image: busybox
    volumeMounts:
    - name: all-in-one
      mountPath: "/projected-volume"
      readOnly: true
  volumes:
  - name: all-in-one
    projected:
      sources:
      - secret:
          name: the-secret
          items:
            - key: username
              path: the-group/the-user
      - downwardAPI:
          items:
            - path: "labels"
              fieldRef:
                fieldPath: metadata.labels
            - path: "cpu_limit"
              resourceFieldRef:
                containerName: the-container
                resource: limits.cpu
      - configMap:
          name: the-configmap
          items:
            - key: config
              path: the-group/the-config

使用 FlexVolume 的外部卷插件

FlexVolume 在 Kubernetes 1.8 中已经普遍可用。它允许您通过统一 API 消耗外部存储。存储提供商编写一个驱动程序,您可以在所有节点上安装。FlexVolume 插件可以动态发现现有的驱动程序。以下是使用 FlexVolume 绑定到外部 NFS 卷的示例:

apiVersion: v1
kind: Pod
metadata:
  name: nginx-nfs
  namespace: default
spec:
  containers:
  - name: nginx-nfs
    image: nginx
    volumeMounts:
    - name: test
      mountPath: /data
    ports:
    - containerPort: 80
  volumes:
  - name: test
    flexVolume:
      driver: "k8s/nfs"
      fsType: "nfs"
      options:
        server: "172.16.0.25"
        share: "dws_nas_scratch"

容器存储接口

容器存储接口CSI)是标准化容器编排器和存储提供商之间交互的一个倡议。它由 Kubernetes、Docker、Mesos 和 Cloud Foundry 推动。其想法是存储提供商只需要实现一个插件,容器编排器只需要支持 CSI。这相当于存储的 CNI。与 FlexVolume 相比,有几个优点:

  • CSI 是一个行业标准

  • FlexVolume 插件需要访问节点和主节点根文件系统来部署驱动程序

  • FlexVolume 存储驱动程序通常需要许多外部依赖项

  • FlexVolume 的 EXEC 风格接口很笨拙

Kubernetes 1.9 中添加了一个 CSI 卷插件作为 alpha 功能,并在 Kubernetes 1.10 中已经升级为 beta 状态。FlexVolume 将保持向后兼容,至少一段时间。但随着 CSI 的发展和更多存储提供商实现 CSI 卷驱动程序,我确实可以看到 Kubernetes 只提供内部 CSI 卷插件,并通过 CSI 驱动程序与任何存储提供商进行通信。

这是一个演示 CSI 在 Kubernetes 中如何工作的图表:

总结

在本章中,我们深入研究了 Kubernetes 中的存储。我们看了基于卷、声明和存储类的通用概念模型,以及卷插件的实现。Kubernetes 最终将所有存储系统映射到容器中的挂载文件系统或原始块存储中。这种直接的模型允许管理员配置和连接任何存储系统,从本地的host目录到基于云的共享存储,再到企业存储系统。存储供应商从内部到外部的过渡对存储生态系统是一个好兆头。现在,您应该清楚地了解了存储在 Kubernetes 中的建模和实现,并能够在您的 Kubernetes 集群中做出明智的存储实现选择。

在第八章中,《使用 Kubernetes 运行有状态应用程序》,我们将看到 Kubernetes 如何提高抽象级别,并在存储之上,利用 StatefulSets 等概念来开发、部署和操作有状态的应用程序。

第八章:使用 Kubernetes 运行有状态应用

在这一章中,我们将探讨在 Kubernetes 上运行有状态应用所需的条件。Kubernetes 通过根据复杂的要求和配置(如命名空间、限制和配额)自动在集群节点上启动和重新启动 pod,从而减少了我们的工作量。但是,当 pod 运行存储感知软件(如数据库和队列)时,重新定位一个 pod 可能会导致系统崩溃。首先,我们将了解有状态 pod 的本质,以及它们在 Kubernetes 中管理起来更加复杂的原因。我们将探讨一些管理复杂性的方法,比如共享环境变量和 DNS 记录。在某些情况下,冗余的内存状态、DaemonSet 或持久存储声明可以解决问题。Kubernetes 为有状态 pod 推广的主要解决方案是 StatefulSet(以前称为 PetSet)资源,它允许我们管理具有稳定属性的索引集合的 pod。最后,我们将深入探讨在 Kubernetes 上运行 Cassandra 集群的一个完整示例。

Kubernetes 中有状态与无状态应用

在 Kubernetes 中,无状态应用是指不在 Kubernetes 集群中管理其状态的应用。所有状态都存储在集群外,集群容器以某种方式访问它。在本节中,我们将了解为什么状态管理对于分布式系统的设计至关重要,以及在 Kubernetes 集群内管理状态的好处。

理解分布式数据密集型应用的性质

让我们从基础知识开始。分布式应用程序是在多台计算机上运行的一组进程,处理输入,操作数据,公开 API,并可能具有其他副作用。每个进程是其程序、运行时环境和输入输出的组合。你在学校写的程序会作为命令行参数获取输入,也许它们会读取文件或访问数据库,然后将结果写入屏幕、文件或数据库。一些程序在内存中保持状态,并可以通过网络提供请求。简单的程序在单台计算机上运行,可以将所有状态保存在内存中或从文件中读取。它们的运行时环境是它们的操作系统。如果它们崩溃,用户必须手动重新启动它们。它们与它们的计算机绑定在一起。分布式应用程序是一个不同的动物。单台计算机不足以处理所有数据或足够快地提供所有请求。单台计算机无法容纳所有数据。需要处理的数据如此之大,以至于无法以成本效益的方式下载到每个处理机器中。机器可能会出现故障,需要被替换。需要在所有处理机器上执行升级。用户可能分布在全球各地。

考虑所有这些问题后,很明显传统方法行不通。限制因素变成了数据。用户/客户端必须只接收摘要或处理过的数据。所有大规模数据处理必须在数据附近进行,因为传输数据的速度慢且昂贵。相反,大部分处理代码必须在相同的数据中心和网络环境中运行。

共享环境变量与 DNS 记录用于发现

Kubernetes 为集群中的全局发现提供了几种机制。如果您的存储集群不是由 Kubernetes 管理,您仍然需要告诉 Kubernetes pod 如何找到它并访问它。主要有两种方法:

  • DNS

  • 环境变量

在某些情况下,您可能希望同时使用环境变量和 DNS,其中环境变量可以覆盖 DNS。

为什么要在 Kubernetes 中管理状态?

在 Kubernetes 中管理状态的主要原因是,与在单独的集群中管理相比,Kubernetes 已经提供了许多监视、扩展、分配、安全和操作存储集群所需的基础设施。运行并行存储集群将导致大量重复的工作。

为什么要在 Kubernetes 之外管理状态?

让我们不排除其他选择。在某些情况下,将状态管理在一个单独的非 Kubernetes 集群中可能更好,只要它与相同的内部网络共享(数据接近性胜过一切)。

一些有效的原因如下:

  • 您已经有一个单独的存储集群,不想引起麻烦

  • 您的存储集群被其他非 Kubernetes 应用程序使用

  • Kubernetes 对您的存储集群的支持还不够稳定或成熟

您可能希望逐步在 Kubernetes 中处理有状态的应用程序,首先从一个单独的存储集群开始,然后再与 Kubernetes 更紧密地集成。

通过 DNS 访问外部数据存储

DNS 方法简单直接。假设您的外部存储集群是负载均衡的,并且可以提供稳定的端点,那么 pod 可以直接命中该端点并连接到外部集群。

通过环境变量访问外部数据存储

另一种简单的方法是使用环境变量传递连接信息到外部存储集群。Kubernetes 提供ConfigMap资源作为一种将配置与容器镜像分开的方式。配置是一组键值对。配置信息可以作为环境变量暴露在容器内部以及卷中。您可能更喜欢使用秘密来存储敏感的连接信息。

创建 ConfigMap

以下配置文件将创建一个保留地址列表的配置文件:

apiVersion: v1 
kind: ConfigMap 
metadata: 
  name: db-config 
  namespace: default 
data: 
  db-ip-addresses: 1.2.3.4,5.6.7.8 

> kubectl create -f .\configmap.yamlconfigmap
 "db-config" created

data部分包含所有的键值对,这种情况下,只有一个键名为db-ip-addresses的键值对。在后面消耗configmap时将会很重要。您可以检查内容以确保它是正确的:

> kubectl get configmap db-config -o yaml
apiVersion: v1
data:
  db-ip-addresses: 1.2.3.4,5.6.7.8
kind: ConfigMap
metadata:
 creationTimestamp: 2017-01-09T03:14:07Z
 name: db-config
 namespace: default
 resourceVersion: "551258"
 selfLink: /api/v1/namespaces/default/configmaps/db-config
 uid: aebcc007-d619-11e6-91f1-3a7ae2a25c7d  

还有其他创建ConfigMap的方法。您可以直接使用--from-value--from-file命令行参数来创建它们。

将 ConfigMap 作为环境变量消耗

当您创建一个 pod 时,可以指定一个ConfigMap并以多种方式使用其值。以下是如何将我们的配置映射为环境变量:

apiVersion: v1 
kind: Pod 
metadata: 
  name: some-pod 
spec: 
  containers: 
    - name: some-container 
      image: busybox 
      command: [ "/bin/sh", "-c", "env" ] 
      env: 
        - name: DB_IP_ADDRESSES 
          valueFrom: 
            configMapKeyRef: 
              name: db-config 
              key: db-ip-addresses         
  restartPolicy: Never 

这个 pod 运行busybox最小容器,并执行env bash命令,然后立即退出。db-config映射中的db-ip-addresses键被映射到DB_IP_ADDRESSES环境变量,并反映在输出中:

> kubectl logs some-pod
HUE_REMINDERS_SERVICE_PORT=80
HUE_REMINDERS_PORT=tcp://10.0.0.238:80
KUBERNETES_PORT=tcp://10.0.0.1:443
KUBERNETES_SERVICE_PORT=443
HOSTNAME=some-pod
SHLVL=1
HOME=/root
HUE_REMINDERS_PORT_80_TCP_ADDR=10.0.0.238
HUE_REMINDERS_PORT_80_TCP_PORT=80
HUE_REMINDERS_PORT_80_TCP_PROTO=tcp
DB_IP_ADDRESSES=1.2.3.4,5.6.7.8
HUE_REMINDERS_PORT_80_TCP=tcp://10.0.0.238:80
KUBERNETES_PORT_443_TCP_ADDR=10.0.0.1
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
KUBERNETES_PORT_443_TCP_PORT=443
KUBERNETES_PORT_443_TCP_PROTO=tcp
KUBERNETES_SERVICE_PORT_HTTPS=443
KUBERNETES_PORT_443_TCP=tcp://10.0.0.1:443
HUE_REMINDERS_SERVICE_HOST=10.0.0.238
PWD=/
KUBERNETES_SERVICE_HOST=10.0.0.1 

使用冗余的内存状态

在某些情况下,您可能希望在内存中保留瞬态状态。分布式缓存是一个常见情况。时间敏感的信息是另一个情况。对于这些用例,不需要持久存储,通过服务访问多个 Pod 可能是正确的解决方案。我们可以使用标签等标准 Kubernetes 技术来识别属于存储冗余副本的 Pod,并通过服务公开它。如果一个 Pod 死掉,Kubernetes 将创建一个新的 Pod,并且在它赶上之前,其他 Pod 将服务于该状态。我们甚至可以使用 Pod 的反亲和性 alpha 功能来确保维护相同状态的冗余副本的 Pod 不被调度到同一节点。

使用 DaemonSet 进行冗余持久存储

一些有状态的应用程序,如分布式数据库或队列,会冗余地管理它们的状态并自动同步它们的节点(我们稍后将深入研究 Cassandra)。在这些情况下,重要的是将 Pod 调度到单独的节点。同样重要的是,Pod 应该被调度到具有特定硬件配置的节点,甚至专门用于有状态应用程序。DaemonSet 功能非常适合这种用例。我们可以为一组节点打上标签,并确保有状态的 Pod 被逐个地调度到所选的节点组。

应用持久卷索赔

如果有状态的应用程序可以有效地使用共享的持久存储,那么在每个 Pod 中使用持久卷索赔是正确的方法,就像我们在第七章中演示的那样,处理 Kubernetes 存储。有状态的应用程序将被呈现为一个看起来就像本地文件系统的挂载卷。

利用 StatefulSet

StatefulSet 控制器是 Kubernetes 的一个相对较新的添加(在 Kubernetes 1.3 中作为 PetSets 引入,然后在 Kubernetes 1.5 中更名为 StatefulSet)。它专门设计用于支持分布式有状态应用程序,其中成员的身份很重要,如果一个 Pod 被重新启动,它必须保留在集合中的身份。它提供有序的部署和扩展。与常规 Pod 不同,StatefulSet 的 Pod 与持久存储相关联。

何时使用 StatefulSet

StatefulSet 非常适合需要以下一项或多项功能的应用程序:

  • 稳定、独特的网络标识符

  • 稳定的持久存储

  • 有序、优雅的部署和扩展

  • 有序、优雅的删除和终止

StatefulSet 的组件

有几个部分需要正确配置,才能使 StatefulSet 正常工作:

  • 一个负责管理 StatefulSet pod 的网络标识的无头服务

  • 具有多个副本的 StatefulSet 本身

  • 动态或由管理员持久存储提供

这是一个名为nginx的服务的示例,将用于 StatefulSet:

apiVersion: v1 
kind: Service 
metadata: 
  name: nginx 
  labels: 
    app: nginx 
spec: 
  ports: 
  - port: 80 
    name: web 
  clusterIP: None 
  selector: 
    app: nginx 

现在,StatefulSet配置文件将引用该服务:

apiVersion: apps/v1 
kind: StatefulSet 
metadata: 
  name: web 
spec: 
  serviceName: "nginx" 
  replicas: 3 
  template: 
    metadata: 
      labels: 
        app: nginx 

接下来是包含名为www的挂载卷的 pod 模板:

spec: 
  terminationGracePeriodSeconds: 10 
  containers: 
  - name: nginx 
    image: gcr.io/google_containers/nginx-slim:0.8 
    ports: 
    - containerPort: 80 
      name: web 
      volumeMounts: 
    - name: www 
      mountPath: /usr/share/nginx/html 

最后,volumeClaimTemplates使用名为www的声明匹配挂载的卷。声明请求1Gib存储,具有ReadWriteOnce访问权限:

volumeClaimTemplates: 
- metadata: 
    name: www 
  spec: 
    accessModes: [ "ReadWriteOnce" ] 
    resources: 
      requests: 
        storage: 1Gib 

在 Kubernetes 中运行 Cassandra 集群

在本节中,我们将详细探讨配置 Cassandra 集群在 Kubernetes 集群上运行的一个非常大的示例。完整的示例可以在这里访问:

github.com/kubernetes/kubernetes/tree/master/examples/storage/cassandra

首先,我们将学习一些关于 Cassandra 及其特殊性的知识,然后按照逐步的步骤来使其运行,使用我们在前一节中介绍的几种技术和策略。

Cassandra 的简要介绍

Cassandra 是一个分布式列式数据存储。它从一开始就为大数据而设计。Cassandra 快速、健壮(没有单点故障)、高可用性和线性可扩展。它还支持多数据中心。它通过专注于并精心打造支持的功能,以及同样重要的是不支持的功能,来实现所有这些。在以前的公司中,我运行了一个使用 Cassandra 作为传感器数据主要数据存储的 Kubernetes 集群(约 100 TB)。Cassandra 根据分布式哈希表DHT)算法将数据分配给一组节点(节点环)。集群节点通过八卦协议相互通信,并迅速了解集群的整体状态(哪些节点加入,哪些节点离开或不可用)。Cassandra 不断压缩数据并平衡集群。数据通常被复制多次以实现冗余、健壮性和高可用性。从开发者的角度来看,Cassandra 非常适合时间序列数据,并提供了一个灵活的模型,可以在每个查询中指定一致性级别。它还是幂等的(对于分布式数据库来说非常重要的特性),这意味着允许重复插入或更新。

这是一个图表,显示了 Cassandra 集群的组织方式,以及客户端如何访问任何节点,请求将如何自动转发到具有所请求数据的节点:

Cassandra Docker 镜像

在 Kubernetes 上部署 Cassandra 与独立的 Cassandra 集群部署相反,需要一个特殊的 Docker 镜像。这是一个重要的步骤,因为这意味着我们可以使用 Kubernetes 来跟踪我们的 Cassandra pod。该镜像在这里可用:

github.com/kubernetes/kubernetes/tree/master/examples/storage/cassandra/image

以下是 Docker 文件的基本部分。该镜像基于 Ubuntu Slim:

FROM gcr.io/google_containers/ubuntu-slim:0.9  

添加和复制必要的文件(Cassandra.jar,各种配置文件,运行脚本和读取探测脚本),创建一个data目录供 Cassandra 存储其 SSTable,并挂载它:

ADD files / 

RUN set -e && echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections \
  && apt-get update && apt-get -qq -y --force-yes install --no-install-recommends \  
    openjdk-8-jre-headless \
    libjemalloc1 \ 
    localepurge  \
    wget && \
  mirror_url=$( wget -q -O - http://www.apache.org/dyn/closer.cgi/cassandra/ \
        | sed -n 's#.*href="\(http://.*/cassandra\/[^"]*\)".*#\1#p' \
        | head -n 1 \
    ) \
    && wget -q -O - ${mirror_url}/${CASSANDRA_VERSION}/apache-cassandra-${CASSANDRA_VERSION}-bin.tar.gz \
        | tar -xzf - -C /usr/local \
    && wget -q -O - https://github.com/Yelp/dumb-init/releases/download/v${DI_VERSION}/dumb-init_${DI_VERSION}_amd64 > /sbin/dumb-init \
    && echo "$DI_SHA  /sbin/dumb-init" | sha256sum -c - \
    && chmod +x /sbin/dumb-init \
    && chmod +x /ready-probe.sh \
    && mkdir -p /cassandra_data/data \
    && mkdir -p /etc/cassandra \
    && mv /logback.xml /cassandra.yaml /jvm.options /etc/cassandra/ \ 
    && mv /usr/local/apache-cassandra-${CASSANDRA_VERSION}/conf/cassandra-env.sh /etc/cassandra/ \
    && adduser --disabled-password --no-create-home --gecos '' --disabled-login cassandra \
    && chown cassandra: /ready-probe.sh \ 

VOLUME ["/$CASSANDRA_DATA"] 

暴露访问 Cassandra 的重要端口,并让 Cassandra 节点相互通信:

# 7000: intra-node communication 
# 7001: TLS intra-node communication 
# 7199: JMX 
# 9042: CQL 
# 9160: thrift service 

EXPOSE 7000 7001 7199 9042 9160 

最后,使用dumb-init命令运行run.sh脚本,这是一个来自 yelp 的简单容器init系统:

CMD ["/sbin/dumb-init", "/bin/bash", "/run.sh"] 

探索run.sh脚本

run.sh脚本需要一些 shell 技能,但这是值得的。由于 Docker 只允许运行一个命令,对于非平凡的应用程序来说,有一个设置环境并为实际应用程序做准备的启动脚本是非常常见的。在这种情况下,镜像支持几种部署选项(有状态集、复制控制器、DaemonSet),我们稍后会介绍,而运行脚本通过环境变量非常可配置。

首先,为/etc/cassandra/cassandra.yaml中的 Cassandra 配置文件设置了一些本地变量。CASSANDRA_CFG变量将在脚本的其余部分中使用:

set -e 
CASSANDRA_CONF_DIR=/etc/cassandra 
CASSANDRA_CFG=$CASSANDRA_CONF_DIR/cassandra.yaml 

如果没有指定CASSANDRA_SEEDS,那么设置HOSTNAME,它在 StatefulSet 解决方案中使用:

# we are doing StatefulSet or just setting our seeds 
if [ -z "$CASSANDRA_SEEDS" ]; then 
  HOSTNAME=$(hostname -f) 
Fi 

然后是一长串带有默认值的环境变量。语法${VAR_NAME:-<default>}使用VAR_NAME环境变量,如果定义了的话,或者使用默认值。

类似的语法${VAR_NAME:=<default}也可以做同样的事情,但同时也赋值

如果未定义环境变量,则将默认值分配给它。

这里都用到了两种变体:

CASSANDRA_RPC_ADDRESS="${CASSANDRA_RPC_ADDRESS:-0.0.0.0}" 
CASSANDRA_NUM_TOKENS="${CASSANDRA_NUM_TOKENS:-32}" 
CASSANDRA_CLUSTER_NAME="${CASSANDRA_CLUSTER_NAME:='Test Cluster'}" 
CASSANDRA_LISTEN_ADDRESS=${POD_IP:-$HOSTNAME} 
CASSANDRA_BROADCAST_ADDRESS=${POD_IP:-$HOSTNAME} 
CASSANDRA_BROADCAST_RPC_ADDRESS=${POD_IP:-$HOSTNAME} 
CASSANDRA_DISK_OPTIMIZATION_STRATEGY="${CASSANDRA_DISK_OPTIMIZATION_STRATEGY:-ssd}" 
CASSANDRA_MIGRATION_WAIT="${CASSANDRA_MIGRATION_WAIT:-1}" 
CASSANDRA_ENDPOINT_SNITCH="${CASSANDRA_ENDPOINT_SNITCH:-SimpleSnitch}" 
CASSANDRA_DC="${CASSANDRA_DC}" 
CASSANDRA_RACK="${CASSANDRA_RACK}" 
CASSANDRA_RING_DELAY="${CASSANDRA_RING_DELAY:-30000}" 
CASSANDRA_AUTO_BOOTSTRAP="${CASSANDRA_AUTO_BOOTSTRAP:-true}" 
CASSANDRA_SEEDS="${CASSANDRA_SEEDS:false}" 
CASSANDRA_SEED_PROVIDER="${CASSANDRA_SEED_PROVIDER:-org.apache.cassandra.locator.SimpleSeedProvider}" 
CASSANDRA_AUTO_BOOTSTRAP="${CASSANDRA_AUTO_BOOTSTRAP:false}" 

# Turn off JMX auth 
CASSANDRA_OPEN_JMX="${CASSANDRA_OPEN_JMX:-false}" 
# send GC to STDOUT 
CASSANDRA_GC_STDOUT="${CASSANDRA_GC_STDOUT:-false}" 

然后是一个部分,其中所有变量都打印到屏幕上。让我们跳过大部分内容:

echo Starting Cassandra on ${CASSANDRA_LISTEN_ADDRESS}
echo CASSANDRA_CONF_DIR ${CASSANDRA_CONF_DIR}
...

接下来的部分非常重要。默认情况下,Cassandra 使用简单的 snitch,不知道机架和数据中心。当集群跨多个数据中心和机架时,这并不是最佳选择。

Cassandra 是机架和数据中心感知的,可以优化冗余性和高可用性,同时适当地限制跨数据中心的通信:

# if DC and RACK are set, use GossipingPropertyFileSnitch 
if [[ $CASSANDRA_DC && $CASSANDRA_RACK ]]; then 
  echo "dc=$CASSANDRA_DC" > $CASSANDRA_CONF_DIR/cassandra-rackdc.properties 
  echo "rack=$CASSANDRA_RACK" >> $CASSANDRA_CONF_DIR/cassandra-rackdc.properties 
  CASSANDRA_ENDPOINT_SNITCH="GossipingPropertyFileSnitch" 
fi 

内存管理很重要,您可以控制最大堆大小,以确保 Cassandra 不会开始抖动并开始与磁盘交换:

if [ -n "$CASSANDRA_MAX_HEAP" ]; then 
  sed -ri "s/^(#)?-Xmx[0-9]+.*/-Xmx$CASSANDRA_MAX_HEAP/" "$CASSANDRA_CONF_DIR/jvm.options" 
  sed -ri "s/^(#)?-Xms[0-9]+.*/-Xms$CASSANDRA_MAX_HEAP/" "$CASSANDRA_CONF_DIR/jvm.options" 
fi 

if [ -n "$CASSANDRA_REPLACE_NODE" ]; then 
   echo "-Dcassandra.replace_address=$CASSANDRA_REPLACE_NODE/" >> "$CASSANDRA_CONF_DIR/jvm.options" 
fi 

机架和数据中心信息存储在一个简单的 Java properties文件中:

for rackdc in dc rack; do 
  var="CASSANDRA_${rackdc^^}" 
  val="${!var}" 
  if [ "$val" ]; then 
  sed -ri 's/^('"$rackdc"'=).*/1 '"$val"'/' "$CASSANDRA_CONF_DIR/cassandra-rackdc.properties" 
  fi 
done 

接下来的部分循环遍历之前定义的所有变量,在Cassandra.yaml配置文件中找到相应的键,并进行覆盖。这确保了每个配置文件在启动 Cassandra 本身之前都是动态定制的:

for yaml in \  
  broadcast_address \ 
  broadcast_rpc_address \ 
  cluster_name \ 
  disk_optimization_strategy \ 
  endpoint_snitch \ 
  listen_address \ 
  num_tokens \  
  rpc_address \ 
  start_rpc \  
  key_cache_size_in_mb \ 
  concurrent_reads \ 
  concurrent_writes \ 
  memtable_cleanup_threshold \  
  memtable_allocation_type \ 
  memtable_flush_writers \ 
  concurrent_compactors \  
  compaction_throughput_mb_per_sec \ 
  counter_cache_size_in_mb \ 
  internode_compression \ 
  endpoint_snitch \ 
  gc_warn_threshold_in_ms \  
  listen_interface  \
  rpc_interface  \
  ; do 
  var="CASSANDRA_${yaml^^}" 
  val="${!var}" 
  if [ "$val" ]; then 
    sed -ri 's/^(# )?('"$yaml"':).*/\2 '"$val"'/' "$CASSANDRA_CFG" 
  fi 
done 

echo "auto_bootstrap: ${CASSANDRA_AUTO_BOOTSTRAP}" >> $CASSANDRA_CFG 

接下来的部分都是关于根据部署解决方案(StatefulSet 或其他)设置种子或种子提供程序。对于第一个 pod 来说,有一个小技巧可以作为自己的种子引导:

# set the seed to itself.  This is only for the first pod, otherwise 
# it will be able to get seeds from the seed provider 
if [[ $CASSANDRA_SEEDS == 'false' ]]; then 
  sed -ri 's/- seeds:.*/- seeds: "'"$POD_IP"'"/' $CASSANDRA_CFG 
else # if we have seeds set them.  Probably StatefulSet 
  sed -ri 's/- seeds:.*/- seeds: "'"$CASSANDRA_SEEDS"'"/' $CASSANDRA_CFG 
fi 

sed -ri 's/- class_name: SEED_PROVIDER/- class_name: '"$CASSANDRA_SEED_PROVIDER"'/' $CASSANDRA_CFG 

以下部分设置了远程管理和 JMX 监控的各种选项。在复杂的分布式系统中,拥有适当的管理工具至关重要。Cassandra 对普遍的Java 管理扩展JMX)标准有深入的支持:

# send gc to stdout 
if [[ $CASSANDRA_GC_STDOUT == 'true' ]]; then 
  sed -ri 's/ -Xloggc:\/var\/log\/cassandra\/gc\.log//' $CASSANDRA_CONF_DIR/cassandra-env.sh 
fi 

# enable RMI and JMX to work on one port 
echo "JVM_OPTS=\"\$JVM_OPTS -Djava.rmi.server.hostname=$POD_IP\"" >> $CASSANDRA_CONF_DIR/cassandra-env.sh 

# getting WARNING messages with Migration Service 
echo "-Dcassandra.migration_task_wait_in_seconds=${CASSANDRA_MIGRATION_WAIT}" >> $CASSANDRA_CONF_DIR/jvm.options 
echo "-Dcassandra.ring_delay_ms=${CASSANDRA_RING_DELAY}" >> $CASSANDRA_CONF_DIR/jvm.options 

if [[ $CASSANDRA_OPEN_JMX == 'true' ]]; then 
  export LOCAL_JMX=no 
  sed -ri 's/ -Dcom\.sun\.management\.jmxremote\.authenticate=true/ -Dcom\.sun\.management\.jmxremote\.authenticate=false/' $CASSANDRA_CONF_DIR/cassandra-env.sh 
  sed -ri 's/ -Dcom\.sun\.management\.jmxremote\.password\.file=\/etc\/cassandra\/jmxremote\.password//' $CASSANDRA_CONF_DIR/cassandra-env.sh 
fi 

最后,CLASSPATH设置为Cassandra JAR 文件,并将 Cassandra 作为 Cassandra 用户在前台(非守护进程)启动:

export CLASSPATH=/kubernetes-cassandra.jar

su cassandra -c "$CASSANDRA_HOME/bin/cassandra -f"  

连接 Kubernetes 和 Cassandra

连接 Kubernetes 和 Cassandra 需要一些工作,因为 Cassandra 被设计为非常自给自足,但我们希望让它在适当的时候连接 Kubernetes 以提供功能,例如自动重新启动失败的节点、监视、分配 Cassandra pods,并在其他 pods 旁边提供 Cassandra pods 的统一视图。Cassandra 是一个复杂的系统,有许多控制选项。它带有一个Cassandra.yaml配置文件,您可以使用环境变量覆盖所有选项。

深入了解 Cassandra 配置

有两个特别相关的设置:seed 提供程序和 snitch。seed 提供程序负责发布集群中节点的 IP 地址(seeds)列表。每个启动的节点都连接到 seeds(通常至少有三个),如果成功到达其中一个,它们立即交换有关集群中所有节点的信息。随着节点之间的 gossip,这些信息会不断更新每个节点。

Cassandra.yaml中配置的默认 seed 提供程序只是一个静态的 IP 地址列表,在这种情况下只有环回接口:

seed_provider: 
    - class_name: SEED_PROVIDER 
      parameters: 
          # seeds is actually a comma-delimited list of addresses. 
          # Ex: "<ip1>,<ip2>,<ip3>" 
          - seeds: "127.0.0.1"  

另一个重要的设置是 snitch。它有两个角色:

  • 它教会 Cassandra 足够了解您的网络拓扑以有效地路由请求。

  • 它允许 Cassandra 在集群中分散副本以避免相关故障。它通过将机器分组到数据中心和机架来实现这一点。Cassandra 会尽量避免在同一机架上拥有多个副本(这实际上可能不是一个物理位置)。

Cassandra 预装了几个 snitch 类,但它们都不了解 Kubernetes。默认是SimpleSnitch,但可以被覆盖。

# You can use a custom Snitch by setting this to the full class  
# name of the snitch, which will be assumed to be on your classpath. 
endpoint_snitch: SimpleSnitch 

自定义 seed 提供程序

在 Kubernetes 中将 Cassandra 节点作为 pod 运行时,Kubernetes 可能会移动 pod,包括 seeds。为了适应这一点,Cassandra seed 提供程序需要与 Kubernetes API 服务器进行交互。

这是自定义的KubernetesSeedProvider Java 类的一个简短片段,它实现了 Cassandra 的SeedProvider API:

public class KubernetesSeedProvider implements SeedProvider { 
   ... 
    /** 
     * Call kubernetes API to collect a list of seed providers 
     * @return list of seed providers 
     */ 
    public List<InetAddress> getSeeds() { 
        String host = getEnvOrDefault("KUBERNETES_PORT_443_TCP_ADDR", "kubernetes.default.svc.cluster.local"); 
        String port = getEnvOrDefault("KUBERNETES_PORT_443_TCP_PORT", "443"); 
        String serviceName = getEnvOrDefault("CASSANDRA_SERVICE", "cassandra"); 
        String podNamespace = getEnvOrDefault("POD_NAMESPACE", "default"); 
        String path = String.format("/api/v1/namespaces/%s/endpoints/", podNamespace); 
        String seedSizeVar = getEnvOrDefault("CASSANDRA_SERVICE_NUM_SEEDS", "8"); 
        Integer seedSize = Integer.valueOf(seedSizeVar); 
        String accountToken = getEnvOrDefault("K8S_ACCOUNT_TOKEN", "/var/run/secrets/kubernetes.io/serviceaccount/token"); 

        List<InetAddress> seeds = new ArrayList<InetAddress>(); 
        try { 
            String token = getServiceAccountToken(accountToken); 

            SSLContext ctx = SSLContext.getInstance("SSL"); 
            ctx.init(null, trustAll, new SecureRandom()); 

            String PROTO = "https://"; 
            URL url = new URL(PROTO + host + ":" + port + path + serviceName); 
            logger.info("Getting endpoints from " + url); 
            HttpsURLConnection conn = (HttpsURLConnection)url.openConnection(); 

            conn.setSSLSocketFactory(ctx.getSocketFactory()); 
            conn.addRequestProperty("Authorization", "Bearer " + token); 
            ObjectMapper mapper = new ObjectMapper(); 
            Endpoints endpoints = mapper.readValue(conn.getInputStream(), Endpoints.class);    }    
            ... 
        } 
        ... 

    return Collections.unmodifiableList(seeds);    
} 

创建一个 Cassandra 无头服务

无头服务的作用是允许 Kubernetes 集群中的客户端通过标准的 Kubernetes 服务连接到 Cassandra 集群,而不是跟踪节点的网络标识或在所有节点前面放置专用的负载均衡器。Kubernetes 通过其服务提供了所有这些功能。

这是配置文件:

apiVersion: v1 
kind: Service 
metadata: 
  labels: 
    app: cassandra 
  name: cassandra 
spec: 
  clusterIP: None 
  ports: 
    - port: 9042 
  selector: 
    app: Cassandra 

app: Cassandra标签将把所有参与服务的 pod 分组。Kubernetes 将创建端点记录,DNS 将返回一个用于发现的记录。clusterIPNone,这意味着服务是无头的,Kubernetes 不会进行任何负载平衡或代理。这很重要,因为 Cassandra 节点直接进行通信。

9042端口被 Cassandra 用于提供 CQL 请求。这些可以是查询、插入/更新(Cassandra 总是使用 upsert),或者删除。

使用 StatefulSet 创建 Cassandra 集群

声明 StatefulSet 并不是一件简单的事情。可以说它是最复杂的 Kubernetes 资源。它有很多组成部分:标准元数据,StatefulSet 规范,Pod 模板(通常本身就相当复杂),以及卷索赔模板。

解析 StatefulSet 配置文件

让我们按部就班地查看声明一个三节点 Cassandra 集群的示例 StatefulSet 配置文件。

这是基本的元数据。请注意,apiVersion字符串是apps/v1(StatefulSet 从 Kubernetes 1.9 开始普遍可用):

apiVersion: "apps/v1" 
kind: StatefulSet 
metadata: 
  name: cassandra 

StatefulSet 的spec定义了无头服务的名称,StatefulSet 中有多少个 pod,以及 pod 模板(稍后解释)。replicas字段指定了 StatefulSet 中有多少个 pod:

spec: 
  serviceName: cassandra 
  replicas: 3  
  template: ... 

对于 pod 来说,术语replicas是一个不幸的选择,因为这些 pod 并不是彼此的副本。它们共享相同的 pod 模板,但它们有独特的身份,它们负责一般状态的不同子集。在 Cassandra 的情况下,这更加令人困惑,因为它使用相同的术语replicas来指代冗余复制一些状态的节点组(但它们并不相同,因为每个节点也可以管理额外的状态)。我向 Kubernetes 项目提出了一个 GitHub 问题,要求将术语从replicas更改为members

github.com/kubernetes/kubernetes.github.io/issues/2103

Pod 模板包含一个基于自定义 Cassandra 镜像的单个容器。以下是带有app: cassandra标签的 Pod 模板:

template: 
  metadata: 
    labels: 
      app: cassandra 
  spec: 
    containers: ...   

容器规范有多个重要部分。它以name和我们之前查看的image开始:

containers: 
   - name: cassandra 
      image: gcr.io/google-samples/cassandra:v12 
      imagePullPolicy: Always 

然后,它定义了 Cassandra 节点需要的多个容器端口,用于外部和内部通信:

ports: 
- containerPort: 7000 
  name: intra-node 
- containerPort: 7001 
  name: tls-intra-node 
- containerPort: 7199 
  name: jmx 
- containerPort: 9042 
  name: cql 

资源部分指定容器所需的 CPU 和内存。这很关键,因为存储管理层不应因cpumemory而成为性能瓶颈。

resources: 
  limits: 
    cpu: "500m" 
    memory: 1Gi 
  requests: 
    cpu: "500m" 
    memory: 1Gi 

Cassandra 需要访问IPC,容器通过安全内容的功能请求它:

securityContext: 
capabilities: 
  add: 
       - IPC_LOCK 

env部分指定容器内可用的环境变量。以下是必要变量的部分列表。CASSANDRA_SEEDS变量设置为无头服务,因此 Cassandra 节点可以在启动时与 seeds 通信并发现整个集群。请注意,在此配置中,我们不使用特殊的 Kubernetes 种子提供程序。POD_IP很有趣,因为它利用向status.podIP的字段引用通过 Downward API 填充其值:

 env: 
   - name: MAX_HEAP_SIZE 
     value: 512M 
   - name: CASSANDRA_SEEDS 
     value: "cassandra-0.cassandra.default.svc.cluster.local" 
  - name: POD_IP 
    valueFrom: 
      fieldRef: 
        fieldPath: status.podIP 

容器还有一个就绪探针,以确保 Cassandra 节点在完全在线之前不会收到请求:

readinessProbe: 
  exec: 
    command: 
    - /bin/bash 
    - -c 
    - /ready-probe.sh 
  initialDelaySeconds: 15 
  timeoutSeconds: 5 

当然,Cassandra 需要读写数据。cassandra-data卷挂载就是这样的:

volumeMounts: 
- name: cassandra-data 
  mountPath: /cassandra_data 

容器规范就是这样。最后一部分是卷索赔模板。在这种情况下,使用了动态配置。强烈建议为 Cassandra 存储使用 SSD 驱动器,特别是其日志。在这个例子中,请求的存储空间是1 Gi。通过实验,我发现单个 Cassandra 节点的理想存储空间是 1-2 TB。原因是 Cassandra 在后台进行大量的数据重排、压缩和数据再平衡。如果一个节点离开集群或一个新节点加入集群,你必须等到数据被正确再平衡,然后才能重新分布来自离开节点的数据或者填充新节点。请注意,Cassandra 需要大量的磁盘空间来进行所有这些操作。建议保留 50%的空闲磁盘空间。当考虑到你还需要复制(通常是 3 倍)时,所需的存储空间可能是你的数据大小的 6 倍。如果你愿意冒险,也许根据你的用例,你可以用 30%的空闲空间,甚至只使用 2 倍的复制。但是,即使是在单个节点上,也不要低于 10%的空闲磁盘空间。我以艰难的方式得知,Cassandra 会简单地卡住,无法在没有极端措施的情况下进行压缩和再平衡这样的节点。

访问模式当然是ReadWriteOnce

volumeClaimTemplates: 
- metadata: 
  name: cassandra-data 
  annotations: 
    volume.beta.kubernetes.io/storage-class: fast 
spec: 
  accessModes: [ "ReadWriteOnce" ] 
  resources: 
    requests: 
      storage: 1Gi 

在部署有状态集时,Kubernetes 根据索引号按顺序创建 pod。当扩展或缩减规模时,也是按顺序进行的。对于 Cassandra 来说,这并不重要,因为它可以处理节点以任何顺序加入或离开集群。当销毁一个 Cassandra pod 时,持久卷仍然存在。如果以后创建了具有相同索引的 pod,原始的持久卷将被挂载到其中。这种稳定的连接使得 Cassandra 能够正确管理状态。

使用复制控制器来分发 Cassandra

StatefulSet 非常好,但是如前所述,Cassandra 已经是一个复杂的分布式数据库。它有很多机制可以自动分发、平衡和复制集群中的数据。这些机制并不是为了与网络持久存储一起工作而进行优化的。Cassandra 被设计为与直接存储在节点上的数据一起工作。当一个节点死机时,Cassandra 可以通过在其他节点上存储冗余数据来进行恢复。让我们来看看在 Kubernetes 集群上部署 Cassandra 的另一种方式,这种方式更符合 Cassandra 的语义。这种方法的另一个好处是,如果您已经有一个现有的 Kubernetes 集群,您不必将其升级到最新版本,只是为了使用一个有状态的集。

我们仍将使用无头服务,但是我们将使用常规的复制控制器,而不是有状态集。有一些重要的区别:

  • 复制控制器而不是有状态集

  • 节点上安排运行的 pod 的存储

  • 使用了自定义的 Kubernetes 种子提供程序类

解剖复制控制器配置文件

元数据非常简单,只有一个名称(标签不是必需的):

apiVersion: v1 
kind: ReplicationController 
metadata: 
  name: cassandra 
  # The labels will be applied automatically 
  # from the labels in the pod template, if not set 
  # labels: 
    # app: Cassandra 

spec指定了replicas的数量:

spec: 
  replicas: 3 
  # The selector will be applied automatically 
  # from the labels in the pod template, if not set. 
  # selector: 
      # app: Cassandra 

pod 模板的元数据是指定app: Cassandra标签的地方。复制控制器将跟踪并确保具有该标签的 pod 恰好有三个:

template: 
    metadata: 
      labels: 
        app: Cassandra 

pod 模板的spec描述了容器的列表。在这种情况下,只有一个容器。它使用相同的名为cassandra的 Cassandra Docker 镜像,并运行run.sh脚本:

spec: 
  containers: 
    - command: 
        - /run.sh 
      image: gcr.io/google-samples/cassandra:v11 
      name: cassandra 

在这个例子中,资源部分只需要0.5个 CPU 单位:

 resources: 
            limits: 
              cpu: 0.5 

环境部分有点不同。CASSANDRA_SEED_PROVDIER指定了我们之前检查过的自定义 Kubernetes 种子提供程序类。这里的另一个新添加是POD_NAMESPACE,它再次使用 Downward API 从元数据中获取值:

 env: 
    - name: MAX_HEAP_SIZE 
      value: 512M 
    - name: HEAP_NEWSIZE 
      value: 100M 
    - name: CASSANDRA_SEED_PROVIDER 
      value: "io.k8s.cassandra.KubernetesSeedProvider" 
    - name: POD_NAMESPACE 
      valueFrom: 
         fieldRef: 
           fieldPath: metadata.namespace 
    - name: POD_IP 
      valueFrom: 
         fieldRef: 
           fieldPath: status.podIP 

ports部分是相同的,暴露节点内通信端口(70007001),7199 JMX 端口用于外部工具(如 Cassandra OpsCenter)与 Cassandra 集群通信,当然还有9042 CQL 端口,通过它客户端与集群通信:

 ports: 
    - containerPort: 7000 
      name: intra-node 
    - containerPort: 7001 
      name: tls-intra-node 
    - containerPort: 7199 
      name: jmx 
    - containerPort: 9042 
      name: cql 

一次又一次,卷被挂载到/cassandra_data中。这很重要,因为同样配置正确的 Cassandra 镜像只期望其data目录位于特定路径。Cassandra 不关心后备存储(尽管作为集群管理员,你应该关心)。Cassandra 只会使用文件系统调用进行读写。

volumeMounts: 
  - mountPath: /cassandra_data 
    name: data 

卷部分是与有状态集解决方案最大的不同之处。有状态集使用持久存储索赔将特定的 pod 与特定的持久卷连接起来,以便具有稳定身份。复制控制器解决方案只是在托管节点上使用emptyDir

volumes: 
  - name: data 
    emptyDir: {} 

这有许多影响。你必须为每个节点提供足够的存储空间。如果 Cassandra pod 死掉,它的存储空间也会消失。即使 pod 在同一台物理(或虚拟)机器上重新启动,磁盘上的数据也会丢失,因为emptyDir一旦其 pod 被删除就会被删除。请注意,容器重新启动是可以的,因为emptyDir可以在容器崩溃时幸存下来。那么,当 pod 死掉时会发生什么呢?复制控制器将启动一个带有空数据的新 pod。Cassandra 将检测到集群中添加了一个新节点,为其分配一些数据,并通过从其他节点移动数据来自动开始重新平衡。这就是 Cassandra 的亮点所在。它不断地压缩、重新平衡和均匀地分布数据到整个集群中。它会自动弄清楚该为你做什么。

为节点分配 pod

复制控制器方法的主要问题是多个 pod 可以被调度到同一 Kubernetes 节点上。如果你的复制因子是三,负责某个键空间范围的所有三个 pod 都被调度到同一个 Kubernetes 节点上会怎么样?首先,所有对该键范围的读取或写入请求都将发送到同一个节点,增加了更多的压力。但更糟糕的是,我们刚刚失去了冗余性。我们有一个单点故障SPOF)。如果该节点死掉,复制控制器将愉快地在其他 Kubernetes 节点上启动三个新的 pod,但它们都不会有数据,而且集群中的其他 Cassandra 节点(其他 pod)也没有数据可供复制。

这可以通过使用 Kubernetes 调度概念中的反亲和性来解决。在将 pod 分配给节点时,可以对 pod 进行注释,以便调度程序不会将其调度到已经具有特定标签集的节点上。将此添加到 pod 的spec中,以确保最多只有一个 Cassandra pod 被分配给一个节点:

spec: 
  affinity: 
    podAntiAffinity: 
      requiredDuringSchedulingIgnoredDuringExecution: 
      - labelSelector: 
          matchExpressions: 
          - key: app 
            operator: In 
            values: 
            - cassandra 
          topologyKey: kubernetes.io/hostname 

使用 DaemonSet 来分发 Cassandra

解决将 Cassandra pod 分配给不同节点的问题的更好方法是使用 DaemonSet。DaemonSet 具有类似于复制控制器的 pod 模板。但是 DaemonSet 有一个节点选择器,用于确定在哪些节点上调度其 pod。它没有特定数量的副本,它只是在与其选择器匹配的每个节点上调度一个 pod。最简单的情况是在 Kubernetes 集群中的每个节点上调度一个 pod。但是节点选择器也可以使用标签的匹配表达式来部署到特定的节点子集。让我们为在 Kubernetes 集群上部署我们的 Cassandra 集群创建一个 DaemonSet:

apiVersion: apps/v1 
kind: DaemonSet 
metadata: 
  name: cassandra-daemonset 

DaemonSet 的spec包含一个常规的 pod 模板。nodeSelector部分是魔术发生的地方,它确保每个带有app: Cassandra标签的节点上始终会被调度一个且仅有一个 pod:

spec: 
  template: 
    metadata: 
      labels: 
        app: cassandra 
    spec: 
      # Filter only nodes with the label "app: cassandra": 
      nodeSelector: 
        app: cassandra 
      containers: 

其余部分与复制控制器相同。请注意,预计nodeSelector将被弃用,而亲和性将被取代。这将在何时发生,目前尚不清楚。

总结

在本章中,我们涵盖了有关有状态应用程序以及如何将其与 Kubernetes 集成的主题。我们发现有状态应用程序很复杂,并考虑了几种发现机制,例如 DNS 和环境变量。我们还讨论了几种状态管理解决方案,例如内存冗余存储和持久存储。本章的大部分内容围绕在 Kubernetes 集群内部部署 Cassandra 集群,使用了几种选项,例如有状态集、复制控制器和 DaemonSet。每种方法都有其优缺点。在这一点上,您应该对有状态应用程序有深入的了解,以及如何在基于 Kubernetes 的系统中应用它们。您已经掌握了多种用例的多种方法,也许甚至学到了一些关于 Cassandra 的知识。

在下一章中,我们将继续我们的旅程,探讨可扩展性的重要主题,特别是自动扩展性,以及在集群动态增长时如何部署和进行实时升级和更新。这些问题非常复杂,特别是当集群上运行有状态应用程序时。

第九章:滚动更新、可伸缩性和配额

在本章中,我们将探讨 Kubernetes 提供的自动 Pod 可伸缩性,以及它如何影响滚动更新,以及它如何与配额交互。我们将涉及重要的供应主题,以及如何选择和管理集群的大小。最后,我们将介绍 Kubernetes 团队如何测试 5000 节点集群的极限。以下是我们将涵盖的主要内容:

  • 水平 Pod 自动缩放器

  • 使用自动缩放执行滚动更新

  • 使用配额和限制处理稀缺资源

  • 推动 Kubernetes 性能的边界

在本章结束时,您将能够规划一个大规模的集群,经济地进行供应,并就性能、成本和可用性之间的各种权衡做出明智的决策。您还将了解如何设置水平 Pod 自动缩放,并聪明地使用资源配额,让 Kubernetes 自动处理体积的间歇性波动。

水平 Pod 自动缩放

Kubernetes 可以监视您的 Pod,并在 CPU 利用率或其他指标超过阈值时对其进行扩展。自动缩放资源指定了细节(CPU 百分比,检查频率),相应的自动缩放控制器会调整副本的数量,如果需要的话。

以下图表说明了不同参与者及其关系:

正如您所看到的,水平 Pod 自动缩放器不会直接创建或销毁 Pod。相反,它依赖于复制控制器或部署资源。这非常聪明,因为您不需要处理自动缩放与复制控制器或部署尝试扩展 Pod 数量而不知道自动缩放器的努力之间的冲突。

自动缩放器会自动执行我们以前必须自己执行的操作。如果没有自动缩放器,如果我们有一个副本控制器,副本设置为3,但我们确定基于平均 CPU 利用率实际上需要4,那么我们将把副本控制器从3更新到4,并继续手动监视所有 Pod 中的 CPU 利用率。自动缩放器会为我们完成这项工作。

声明水平 Pod 自动缩放器

要声明水平 Pod 自动缩放器,我们需要一个复制控制器或部署,以及一个自动缩放资源。这是一个简单的复制控制器,配置为维护三个nginx Pod:

apiVersion: v1 
kind: ReplicationController 
metadata: 
   name: nginx 
spec: 
   replicas: 3 
   template: 
     metadata: 
       labels: 
         run: nginx 
     spec: 
       containers: 
       - name: nginx 
         image: nginx 
         ports: 
         - containerPort: 80 

autoscaling资源引用了scaleTargetRef中的 NGINX 复制控制器:

apiVersion: autoscaling/v1 
kind: HorizontalPodAutoscaler 
metadata: 
  name: nginx 
  namespace: default 
spec: 
  maxReplicas: 4 
  minReplicas: 2 
  targetCPUUtilizationPercentage: 90 
  scaleTargetRef: 
    apiVersion: v1 
    kind: ReplicationController 
    name: nginx 

minReplicasmaxReplicas指定了扩展的范围。这是为了避免因某些问题而发生的失控情况。想象一下,由于某个错误,每个 pod 立即使用 100%的 CPU,而不考虑实际负载。如果没有maxReplicas限制,Kubernetes 将不断创建更多的 pod,直到耗尽所有集群资源。如果我们在具有自动缩放 VM 的云环境中运行,那么我们将产生巨大的成本。这个问题的另一面是,如果没有minReplicas并且活动出现了停滞,那么所有的 pod 都可能被终止,当新的请求进来时,所有的 pod 都将被重新创建和调度。如果存在开关型活动模式,那么这个循环可能会重复多次。保持最小数量的副本运行可以平滑这种现象。在前面的例子中,minReplicas设置为2maxReplicas设置为4。Kubernetes 将确保始终有24个 NGINX 实例在运行。

目标 CPU利用率百分比是一个冗长的词。让我们把它缩写为TCUP。您可以指定一个像 80%这样的单个数字。如果平均负载在 TCUP 周围徘徊,这可能会导致不断的抖动。Kubernetes 将频繁地在增加更多副本和删除副本之间交替。这通常不是期望的行为。为了解决这个问题,您可以为扩展或缩减指定延迟。kube-controller-manager有两个标志来支持这一点:

  • --horizontal-pod-autoscaler-downscale-delay:此选项的值是一个持续时间,指定了在当前操作完成后,自动缩放器必须等待多长时间才能执行另一个缩减操作。默认值为 5 分钟(5m0s)。

  • --horizontal-pod-autoscaler-upscale-delay:此选项的值是一个持续时间,指定了在当前操作完成后,自动缩放器必须等待多长时间才能执行另一个扩展操作。默认值为 3 分钟(3m0s)。

自定义指标

CPU 利用率是一个重要的指标,用于判断是否应该扩展受到过多请求的 Pod,或者它们是否大部分处于空闲状态并且可以缩小规模。但是 CPU 并不是唯一的,有时甚至不是最好的指标。内存可能是限制因素,还有更专业的指标,例如 Pod 内部磁盘队列的深度、请求的平均延迟或服务超时的平均次数。

水平 Pod 自定义指标在 1.2 版本中作为 alpha 扩展添加。在 1.6 版本中,它们升级为 beta 状态。现在可以根据多个自定义指标自动调整 Pod 的规模。自动缩放器将评估所有指标,并根据所需的最大副本数量进行自动缩放,因此会尊重所有指标的要求。

使用自定义指标

使用自定义指标的水平 Pod 自动缩放器在启动集群时需要进行一些配置。首先,您需要启用 API 聚合层。然后,您需要注册您的资源指标 API 和自定义指标 API。Heapster 提供了一个资源指标 API 的实现,您可以使用。只需使用--api-server标志启动 Heapster,并将其设置为true。您需要运行一个单独的服务器来公开自定义指标 API。一个很好的起点是这个:github.com/kubernetes-incubator/custom-metrics-apiserver

下一步是使用以下标志启动kube-controller-manager

--horizontal-pod-autoscaler-use-rest-clients=true
--kubeconfig <path-to-kubeconfig> OR --master <ip-address-of-apiserver>  

如果同时指定了--master标志和--kubeconfig标志,则--master标志将覆盖--kubeconfig标志。这些标志指定了 API 聚合层的位置,允许控制器管理器与 API 服务器通信。

在 Kubernetes 1.7 中,Kubernetes 提供的标准聚合层与kube-apiserver一起运行,因此可以使用以下命令找到目标 IP 地址:

> kubectl get pods --selector k8s-app=kube-apiserver --namespace kube-system -o jsonpath='{.items[0].status.podIP}'  

使用 kubectl 进行自动缩放

kubectl可以使用标准的create命令并接受一个配置文件来创建自动缩放资源。但是kubectl还有一个特殊的命令autoscale,可以让您轻松地在一个命令中设置自动缩放器,而无需特殊的配置文件:

  1. 首先,让我们启动一个复制控制器,确保有三个简单 Pod 的副本,这些 Pod 只运行一个无限的bash-loop
apiVersion: v1 
kind: ReplicationController 
metadata: 
   name: bash-loop-rc 
spec: 
   replicas: 3 
   template: 
     metadata: 
       labels: 
         name: bash-loop-rc 
     spec: 
       containers: 
         - name: bash-loop 
           image: ubuntu 
           command: ["/bin/bash", "-c", "while true; do sleep 10;   
                      done"] 
  1. 让我们创建一个复制控制器:
     > kubectl create -f bash-loop-rc.yaml
     replicationcontroller "bash-loop-rc" created 
  1. 以下是生成的复制控制器:
     > kubectl get rc
     NAME              DESIRED   CURRENT   READY     AGE
     bash-loop-rc        3          3       3         1m  
  1. 您可以看到所需和当前计数都是三,意味着有三个 pod 正在运行。让我们确保一下:
     > kubectl get pods
     NAME                     READY    STATUS    RESTARTS    AGE
     bash-loop-rc-8h59t        1/1     Running    0          50s
     bash-loop-rc-lsvtd        1/1     Running    0          50s
     bash-loop-rc-z7wt5        1/1     Running    0          50s  
  1. 现在,让我们创建一个自动缩放器。为了使其有趣,我们将将最小副本数设置为 4,最大副本数设置为 6
 > kubectl autoscale rc bash-loop-rc --min=4 --max=6 --cpu- percent=50
replicationcontroller "bash-loop-rc" autoscaled
  1. 这是生成的水平 pod 自动缩放器(您可以使用 hpa)。它显示了引用的复制控制器、目标和当前 CPU 百分比,以及最小/最大 pod 数。名称与引用的复制控制器匹配:
 > kubectl get hpa
 NAME          REFERENCE    TARGETS  MINPODS  MAXPODS  REPLICAS  AGE bash-loop-rc  bash-loop-rc  50%     4        6         4        16m
  1. 最初,复制控制器被设置为具有三个副本,但自动缩放器的最小值为四个 pod。这对复制控制器有什么影响?没错。现在所需的副本数是四个。如果平均 CPU 利用率超过 50%,则可能会增加到五个,甚至六个:
     > kubectl get rc
     NAME              DESIRED  CURRENT  READY    AGE
     bash-loop-rc       4       4        4        21m
  1. 为了确保一切正常运行,让我们再看一下 pod。请注意,由于自动缩放,创建了一个新的 pod(17 分钟前):
     > kubectl get pods
     NAME                READY   STATUS    RESTARTS   AGE
     bash-loop-rc-8h59t   1/1     Running   0         21m
     bash-loop-rc-gjv4k   1/1     Running   0         17m
     bash-loop-rc-lsvtd    1/1    Running   0         21m
     bash-loop-rc-z7wt5   1/1     Running   0         21m
  1. 当我们删除水平 pod 自动缩放器时,复制控制器会保留最后所需的副本数(在这种情况下为四个)。没有人记得复制控制器是用三个副本创建的:
     > kubectl  delete hpa bash-loop-rc
     horizontalpodautoscaler "bash-loop-rc" deleted 
  1. 正如您所看到的,即使自动缩放器消失,复制控制器也没有重置,仍然保持四个 pod:
     > kubectl get rc
     NAME              DESIRED   CURRENT   READY      AGE
     bash-loop-rc       4           4       4         28m

让我们尝试其他方法。如果我们创建一个新的水平 pod 自动缩放器,范围为 26,并且相同的 CPU 目标为 50%,会发生什么?

> kubectl autoscale rc bash-loop-rc --min=2 --max=6 --cpu-percent=50
    replicationcontroller "bash-loop-rc" autoscaled  

好吧,复制控制器仍然保持其四个副本,这在范围内:

> kubectl get rc
NAME           DESIRED   CURRENT   READY     AGE
bash-loop-rc   4         4         4         29m  

然而,实际 CPU 利用率为零,或接近零。副本计数应该已经缩减到两个副本,但由于水平 pod 自动缩放器没有从 Heapster 接收到 CPU 指标,它不知道需要缩减复制控制器中的副本数。

使用自动缩放进行滚动更新

滚动更新是管理大型集群的基石。Kubernetes 支持在复制控制器级别和使用部署进行滚动更新。使用复制控制器进行滚动更新与水平 pod 自动缩放器不兼容。原因是在滚动部署期间,会创建一个新的复制控制器,而水平 pod 自动缩放器仍然绑定在旧的复制控制器上。不幸的是,直观的 kubectl rolling-update 命令会触发复制控制器的滚动更新。

由于滚动更新是如此重要的功能,我建议您始终将水平 Pod 自动缩放器绑定到部署对象,而不是复制控制器或副本集。当水平 Pod 自动缩放器绑定到部署时,它可以设置部署规范中的副本,并让部署负责必要的底层滚动更新和复制。

这是我们用于部署hue-reminders服务的部署配置文件:

apiVersion: extensions/v1beta1 
kind: Deployment 
metadata: 
  name: hue-reminders 
spec: 
  replicas: 2   
  template: 
    metadata: 
      name: hue-reminders 
      labels: 
        app: hue-reminders 
    spec:     
      containers: 
      - name: hue-reminders 
        image: g1g1/hue-reminders:v2.2     
        ports: 
        - containerPort: 80  

为了支持自动缩放并确保我们始终有1015个实例在运行,我们可以创建一个autoscaler配置文件:

apiVersion: autoscaling/v1 
 kind: HorizontalPodAutoscaler 
 metadata: 
   name: hue-reminders 
   namespace: default 
 spec: 
   maxReplicas: 15 
   minReplicas: 10 
   targetCPUUtilizationPercentage: 90 
   scaleTargetRef: 
     apiVersion: v1 
     kind: Deployment 
     name: hue-reminders 

scaleTargetRef字段的kind现在是Deployment,而不是ReplicationController。这很重要,因为我们可能有一个同名的复制控制器。为了消除歧义并确保水平 Pod 自动缩放器绑定到正确的对象,kindname必须匹配。

或者,我们可以使用kubectl autoscale命令:

> kubectl autoscale deployment hue-reminders --min=10--max=15
--cpu-percent=90  

处理稀缺资源的限制和配额

随着水平 Pod 自动缩放器动态创建 pod,我们需要考虑如何管理我们的资源。调度很容易失控,资源的低效使用是一个真正的问题。有几个因素可以以微妙的方式相互作用:

  • 整个集群的容量

  • 每个节点的资源粒度

  • 按命名空间划分工作负载

  • DaemonSets

  • StatefulSets

  • 亲和性、反亲和性、污点和容忍

首先,让我们了解核心问题。Kubernetes 调度器在调度 pod 时必须考虑所有这些因素。如果存在冲突或许多重叠的要求,那么 Kubernetes 可能会在安排新的 pod 时遇到问题。例如,一个非常极端但简单的情况是,一个守护进程集在每个节点上运行一个需要 50%可用内存的 pod。现在,Kubernetes 无法安排任何需要超过 50%内存的 pod,因为守护进程集 pod 具有优先级。即使您提供新节点,守护进程集也会立即占用一半的内存。

Stateful sets 类似于守护程序集,因为它们需要新节点来扩展。向 Stateful set 添加新成员的触发器是数据的增长,但影响是从 Kubernetes 可用于调度其他成员的池中获取资源。在多租户情况下,嘈杂的邻居问题可能会在供应或资源分配上出现。您可能会在命名空间中精确地计划不同 pod 和它们的资源需求之间的比例,但您与来自其他命名空间的邻居共享实际节点,甚至可能无法看到。

大多数这些问题可以通过谨慎使用命名空间资源配额和对跨多个资源类型(如 CPU、内存和存储)的集群容量进行仔细管理来缓解。

启用资源配额

大多数 Kubernetes 发行版都支持开箱即用的资源配额。API 服务器的--admission-control标志必须将ResourceQuota作为其参数之一。您还必须创建一个ResourceQuota对象来强制执行它。请注意,每个命名空间最多只能有一个ResourceQuota对象,以防止潜在的冲突。这是由 Kubernetes 强制执行的。

资源配额类型

我们可以管理和控制不同类型的配额。这些类别包括计算、存储和对象。

计算资源配额

计算资源是 CPU 和内存。对于每个资源,您可以指定限制或请求一定数量。以下是与计算相关的字段列表。请注意,requests.cpu可以简单地指定为cpurequests.memory可以简单地指定为 memory:

  • limits.cpu: 在非终端状态的所有 pod 中,CPU 限制的总和不能超过此值

  • limits.memory: 在非终端状态的所有 pod 中,内存限制的总和不能超过此值

  • requests.cpu: 在非终端状态的所有 pod 中,CPU 请求的总和不能超过此值

  • requests.memory: 在非终端状态的所有 pod 中,内存请求的总和不能超过此值

存储资源配额

存储资源配额类型有点复杂。您可以限制每个命名空间的两个实体:存储量和持久卷索赔的数量。但是,除了全局设置总存储配额或持久卷索赔总数之外,您还可以按storage类别设置。storage类别资源配额的表示法有点冗长,但它可以完成工作:

  • requests.storage: 在所有持久卷索赔中,存储请求的总和不能超过此值

  • persistentvolumeclaims: 可以存在于命名空间中的持久卷索赔的总数

  • <storage-class>.storageclass.storage.k8s.io/requests.storage: 与storage-class-name相关联的所有持久卷索赔中,存储请求的总和不能超过此值

  • <storage-class>.storageclass.storage.k8s.io/persistentvolumeclaims: 与storage-class-name相关联的所有持久卷索赔中,可以存在于命名空间中的持久卷索赔的总数

Kubernetes 1.8 还增加了对临时存储配额的 alpha 支持:

  • requests.ephemeral-storage: 在命名空间中的所有 Pod 中,本地临时存储请求的总和不能超过此值

  • limits.ephemeral-storage: 在命名空间中的所有 Pod 中,本地临时存储限制的总和不能超过此值

对象计数配额

Kubernetes 还有另一类资源配额,即 API 对象。我猜想目标是保护 Kubernetes API 服务器免受管理太多对象的影响。请记住,Kubernetes 在幕后做了很多工作。它经常需要查询多个对象来进行身份验证、授权,并确保操作不违反可能存在的许多策略。一个简单的例子是基于复制控制器的 Pod 调度。想象一下,您有 10 亿个复制控制器对象。也许您只有三个 Pod,大多数复制控制器都没有副本。但是,Kubernetes 将花费大量时间来验证这 10 亿个复制控制器确实没有其 Pod 模板的副本,并且它们不需要终止任何 Pod。这是一个极端的例子,但这个概念适用。太多的 API 对象意味着 Kubernetes 需要做很多工作。

可以限制的对象的超额有点零散。例如,可以限制复制控制器的数量,但不能限制副本集的数量,副本集几乎是复制控制器的改进版本,如果有太多副本集存在,它们可能会造成完全相同的破坏。

最明显的遗漏是命名空间。对命名空间的数量没有限制。由于所有限制都是针对命名空间的,因此通过创建太多的命名空间,可以轻松地压倒 Kubernetes,因为每个命名空间只有少量的 API 对象。

以下是所有支持的对象:

  • 配置映射:可以存在于命名空间中的配置映射的总数。

  • 持久卷索赔:可以存在于命名空间中的持久卷索赔的总数。

  • Pods:可以存在于命名空间中的非终端状态的 Pod 的总数。如果status.phase在(FailedSucceeded)中为true,则 Pod 处于终端状态。

  • 复制控制器:可以存在于命名空间中的复制控制器的总数。

  • 资源配额:可以存在于命名空间中的资源配额的总数。

  • 服务:可以存在于命名空间中的服务的总数。

  • 服务负载均衡器:可以存在于命名空间中的负载均衡器服务的总数。

  • 服务节点端口:可以存在于命名空间中的节点端口服务的总数。

  • 秘密:可以存在于命名空间中的秘密的总数。

配额范围

一些资源,如 Pod,可能处于不同的状态,为这些不同的状态设置不同的配额是有用的。例如,如果有许多正在终止的 Pod(这在滚动更新期间经常发生),即使总数超过配额,也可以创建更多的 Pod。这可以通过仅将pod对象计数配额应用于非终止的 Pod 来实现。以下是现有的范围:

  • 终止:匹配spec.activeDeadlineSeconds >= 0的 Pod。

  • 非终止:匹配spec.activeDeadlineSeconds为空的 Pod。

  • 最佳努力:匹配具有最佳努力的服务质量的 Pod

  • 非最佳努力:匹配没有最佳努力服务质量的 Pod

虽然BestEffort范围仅适用于 Pod,但TerminatingNotTerminatingNotBestEffort范围也适用于 CPU 和内存。这很有趣,因为资源配额限制可以阻止 Pod 终止。以下是支持的对象:

  • CPU

  • 限制 CPU

  • 限制内存

  • 内存

  • pods

  • requests.cpu

  • requests.memory

请求和限制

在资源配额的背景下,请求和限制的含义是它要求容器明确指定目标属性。这样,Kubernetes 可以管理总配额,因为它确切地知道为每个容器分配了什么范围的资源。

使用配额

首先让我们创建一个namespace

> kubectl create namespace ns
namespace "ns" created  

使用特定于命名空间的上下文

在与默认值不同的命名空间中工作时,我更喜欢使用context,这样我就不必为每个命令不断输入--namespace=ns

> kubectl config set-context ns --cluster=minikube --user=minikube --namespace=ns
Context "ns" set.
> kubectl config use-context ns
Switched to context "ns".  

创建配额

  1. 创建一个compute quota对象:
    apiVersion: v1
    kind: ResourceQuota
    metadata:
      name: compute-quota
    spec:
      hard:
        pods: "2"
        requests.cpu: "1"
        requests.memory: 20Mi
        limits.cpu: "2"
        limits.memory: 2Gi

    > kubectl create -f compute-quota.yaml
    resourcequota "compute-quota" created
  1. 接下来,让我们添加一个count quota对象:
    apiVersion: v1
    kind: ResourceQuota
    metadata:
      name: object-counts-quota
    spec:
      hard:
        configmaps: "10"
        persistentvolumeclaims: "4"
        replicationcontrollers: "20"
        secrets: "10"
        services: "10"
        services.loadbalancers: "2"

    > kubectl create -f object-count-quota.yaml
    resourcequota "object-counts-quota" created 
  1. 我们可以观察所有的配额:
    > kubectl get quota
    NAME                     AGE
    compute-resources        17m
    object-counts            15m
  1. 我们甚至可以使用describe获取所有信息:
    > kubectl describe quota compute-quota
    Name:            compute-quota
    Namespace:       ns
    Resource         Used  Hard
    --------          ----     ----
    limits.cpu          0        2
    limits.memory       0        2Gi
    pods                0        2
    requests.cpu        0        1
    requests.memory     0        20Mi

    > kubectl describe quota object-counts-quota
    Name:                   object-counts-quota
    Namespace:              ns
    Resource                Used    Hard
    --------                ----    ----
    configmaps              0       10
    persistentvolumeclaims  0       4
    replicationcontrollers  0       20
    secrets                 1       10
    services                0       10
    services.loadbalancers  0       2

这个视图让我们立即了解集群中重要资源的全局资源使用情况,而无需深入研究太多单独的对象。

  1. 让我们向我们的命名空间添加一个 NGINX 服务器:
    > kubectl run nginx --image=nginx --replicas=1 
    deployment "nginx" created
    > kubectl get pods
    No resources found.
  1. 哦哦。没有找到资源。但是在创建deployment时没有错误。让我们检查一下deployment资源:
    > kubectl describe deployment nginx
    Name:                   nginx
    Namespace:              ns
    CreationTimestamp:      Sun, 11 Feb 2018 16:04:42 -0800
    Labels:                 run=nginx
    Annotations:            deployment.kubernetes.io/revision=1
    Selector:               run=nginx
    Replicas:               1 desired | 0 updated | 0 total | 0 available | 1 unavailable
    StrategyType:           RollingUpdate
    MinReadySeconds:        0
    RollingUpdateStrategy:  1 max unavailable, 1 max surge
    Pod Template:
      Labels:  run=nginx
      Containers:
       nginx:
        Image:        nginx
        Port:         <none>
        Environment:  <none>
        Mounts:       <none>
      Volumes:        <none>
    Conditions:
      Type                   Status  Reason
      ----                   ------     ------
      Available            True     MinimumReplicasAvailable
      ReplicaFailure       True     FailedCreate
    OldReplicaSets:       <none>
    NewReplicaSet:     nginx-8586cf59 (0/1 replicas created)
    Events:
      Type    Reason       Age  From                 Message
      ----        ------               ----  ----              -------
Normal  ScalingReplicaSet  16m  deployment-controller  Scaled up replica set nginx-8586cf59 to 1

conditions部分就在那里。ReplicaFailure状态是True,原因是FailedCreate。您可以看到部署创建了一个名为nginx-8586cf59的新副本集,但它无法创建它应该创建的 pod。我们仍然不知道原因。让我们检查一下副本集:

    > kubectl describe replicaset nginx-8586cf59
    Name:           nginx-8586cf59
    Namespace:      ns
    Selector:       pod-template-hash=41427915,run=nginx
    Labels:         pod-template-hash=41427915
                    run=nginx
    Annotations:    deployment.kubernetes.io/desired-replicas=1
                    deployment.kubernetes.io/max-replicas=2
                    deployment.kubernetes.io/revision=1
    Controlled By:  Deployment/nginx
    Replicas:       0 current / 1 desired
    Pods Status:    0 Running / 0 Waiting / 0 Succeeded / 0 Failed
    Conditions:
      Type             Status  Reason
      ----             ------  ------
      ReplicaFailure   True    FailedCreate
    Events:
      Type     Reason        Age                From                   Message
      ----     ------        ----               ----                   -------
      Warning  FailedCreate  17m (x8 over 22m)  replicaset-controller  (combined from similar events): Error creating: pods "nginx-8586cf59-sdwxj" is forbidden: failed quota: compute-quota: must specify limits.cpu,limits.memory,requests.cpu,requests.memory  

输出非常宽,所以它跨越了几行,但是消息非常清晰。由于命名空间中有计算配额,因此每个容器必须指定其 CPU、内存请求和限制。配额控制器必须考虑每个容器的计算资源使用情况,以确保总命名空间配额得到尊重。

好的。我们理解了问题,但如何解决呢?一种方法是为我们想要使用的每种 pod 类型创建一个专用的deployment对象,并仔细设置 CPU 和内存请求和限制。但如果我们不确定呢?如果有很多 pod 类型,我们不想管理一堆deployment配置文件呢?

另一个解决方案是在运行deployment时在命令行上指定限制:

    > kubectl run nginx \
      --image=nginx \
      --replicas=1 \
      --requests=cpu=100m,memory=4Mi \
      --limits=cpu=200m,memory=8Mi \
      --namespace=ns

这样做是有效的,但是通过大量参数动态创建部署是管理集群的一种非常脆弱的方式:

    > kubectl get pods
    NAME                     READY     STATUS    RESTARTS   AGE
    nginx-2199160687-zkc2h   1/1       Running   0          2m 

使用默认计算配额的限制范围

  1. 更好的方法是指定默认的计算限制。输入限制范围。这是一个设置一些容器默认值的配置文件:
    apiVersion: v1
    kind: LimitRange
    metadata:
      name: limits
    spec:
      limits:
      - default:
          cpu: 200m
          memory: 6Mi
        defaultRequest:
          cpu: 100m
          memory: 5Mi
    type: Container 

    > kubectl create -f limits.yaml
    limitrange "limits" created  
  1. 这是当前默认的limits
> kubectl describe limits limitsName:  limits
Namespace:  ns
Type Resource Min Max Default Request Default Limit Max Limit/Request Ratio
----          --------        ---     ---     ---------------            -------------     -----------------------
Container cpu     -   -   100m         200m       -
Container memory    -       -      5Mi      6Mi                 -
  1. 现在,让我们再次运行 NGINX,而不指定任何 CPU 或内存请求和限制。但首先,让我们删除当前的 NGINX 部署:
 > kubectl delete deployment nginx
 deployment "nginx" deleted
 > kubectl run nginx --image=nginx --replicas=1
 deployment "nginx" created
  1. 让我们看看 Pod 是否已创建。是的,它已经创建了:
         > kubectl get pods
         NAME                   READY     STATUS    RESTARTS  AGE
         nginx-8586cf59-p4dp4   1/1       Running    0        16m

选择和管理集群容量

通过 Kubernetes 的水平 Pod 自动缩放、守护进程集、有状态集和配额,我们可以扩展和控制我们的 Pod、存储和其他对象。然而,最终,我们受限于 Kubernetes 集群可用的物理(虚拟)资源。如果所有节点的容量都达到 100%,您需要向集群添加更多节点。没有其他办法。Kubernetes 将无法扩展。另一方面,如果您的工作负载非常动态,那么 Kubernetes 可以缩小您的 Pod,但如果您不相应地缩小节点,您仍然需要支付额外的容量费用。在云中,您可以停止和启动实例。

选择您的节点类型

最简单的解决方案是选择一个已知数量的 CPU、内存和本地存储的单一节点类型。但这通常不是最有效和成本效益的解决方案。这使得容量规划变得简单,因为唯一的问题是需要多少个节点。每当添加一个节点,就会向集群添加已知数量的 CPU 和内存,但大多数 Kubernetes 集群和集群内的组件处理不同的工作负载。我们可能有一个流处理管道,许多 Pod 在一个地方接收一些数据并对其进行处理。这种工作负载需要大量 CPU,可能需要大量内存,也可能不需要。其他组件,如分布式内存缓存,需要大量内存,但几乎不需要 CPU。其他组件,如 Cassandra 集群,需要每个节点连接多个 SSD 磁盘。

对于每种类型的节点,您应考虑适当的标记和确保 Kubernetes 调度设计为在该节点类型上运行的 Pod。

选择您的存储解决方案

存储是扩展集群的重要因素。有三种可扩展的存储解决方案:

  • 自定义解决方案

  • 使用您的云平台存储解决方案

  • 使用集群外解决方案

当您使用自定义解决方案时,在 Kubernetes 集群中安装某种存储解决方案。优点是灵活性和完全控制,但您必须自行管理和扩展。

当您使用云平台存储解决方案时,您可以获得很多开箱即用的功能,但您失去了控制,通常需要支付更多费用,并且根据服务的不同,您可能会被锁定在该提供商那里。

当你使用集群外的解决方案时,数据传输的性能和成本可能会更大。通常情况下,如果你需要与现有系统集成,你会选择这个选项。

当然,大型集群可能会有来自所有类别的多个数据存储。这是你必须做出的最关键的决定之一,你的存储需求可能会随着时间的推移而发生变化和演变。

权衡成本和响应时间

如果金钱不是问题,你可以过度配置你的集群。每个节点都将拥有最佳的硬件配置,你将拥有比处理工作负载所需更多的节点,以及大量可用的存储空间。猜猜?金钱总是一个问题!

当你刚开始并且你的集群处理的流量不多时,你可能会通过过度配置来解决问题。即使大部分时间只需要两个节点,你可能只运行五个节点。将一切乘以 1,000,如果你有成千上万台空闲机器和宠字节的空闲存储,有人会来问问题。

好吧。所以,你仔细测量和优化,你得到了每个资源的 99.99999%利用率。恭喜,你刚创造了一个系统,它无法处理额外的负载或单个节点的故障,而不会丢弃请求或延迟响应。

你需要找到一个折中的方法。了解你的工作负载的典型波动,并考虑过剩容量与减少响应时间或处理能力之间的成本效益比。

有时,如果你有严格的可用性和可靠性要求,你可以通过设计在系统中构建冗余来过度配置。例如,你希望能够在没有停机和没有明显影响的情况下热插拔失败的组件。也许你甚至不能失去一笔交易。在这种情况下,你将为所有关键组件提供实时备份,这种额外的容量可以用来缓解临时波动,而无需任何特殊操作。

有效地使用多个节点配置

有效的容量规划需要你了解系统的使用模式以及每个组件可以处理的负载。这可能包括系统内部产生的大量数据流。当你对典型的工作负载有很好的理解时,你可以查看工作流程以及哪些组件处理负载的哪些部分。然后你可以计算 Pod 的数量和它们的资源需求。根据我的经验,有一些相对固定的工作负载,一些可以可预测变化的工作负载(比如办公时间与非办公时间),然后你有一些完全疯狂的工作负载,表现得不稳定。你必须根据每个工作负载进行规划,并且你可以设计几个节点配置系列,用于安排与特定工作负载匹配的 Pod。

受益于弹性云资源

大多数云提供商都可以让你自动扩展实例,这是对 Kubernetes 水平 Pod 自动缩放的完美补充。如果你使用云存储,它也会在你无需做任何事情的情况下神奇地增长。然而,有一些需要注意的地方。

自动缩放实例

所有大型云提供商都已经实现了实例自动缩放。虽然有一些差异,但基于 CPU 利用率的扩展和缩减始终可用,有时也可以使用自定义指标。有时也提供负载均衡。你可以看到,这里与 Kubernetes 有一些重叠。如果你的云提供商没有适当的自动缩放和适当的控制,相对容易自己实现,这样你就可以监控集群资源使用情况并调用云 API 来添加或删除实例。你可以从 Kubernetes 中提取指标。

这是一个图表,显示了基于 CPU 负载监视器添加了两个新实例的情况。

注意你的云配额

在与云提供商合作时,一些最让人讨厌的事情是配额。我曾与四个不同的云提供商合作过(AWS,GCP,Azure 和阿里云),总会在某个时候受到配额的限制。配额的存在是为了让云提供商进行自己的容量规划(也是为了保护您免受意外启动 100 万个无法支付的实例),但从您的角度来看,这又是一个可能让您遇到麻烦的事情。想象一下,您设置了一个像魔术一样工作的美丽的自动扩展系统,突然当您达到 100 个节点时,系统不再扩展。您很快发现自己被限制在 100 个节点,并且打开了一个支持请求来增加配额。然而,配额请求必须由人员批准,这可能需要一两天的时间。与此同时,您的系统无法处理负载。

谨慎管理区域

云平台按区域和可用性区域组织。某些服务和机器配置仅在某些区域可用。云配额也是在区域级别管理的。区域内数据传输的性能和成本要比跨区域低得多(通常是免费)。在规划您的集群时,您应该仔细考虑您的地理分布策略。如果您需要在多个区域运行您的集群,您可能需要做出一些关于冗余、可用性、性能和成本的艰难决定。

考虑 Hyper.sh(和 AWS Fargate)

Hyper.sh是一个容器感知的托管服务。您只需启动容器。该服务负责分配硬件。容器在几秒钟内启动。您永远不需要等待几分钟来获取新的虚拟机。Hypernetes 是在 Hyper.sh 上的 Kubernetes,它完全消除了扩展节点的需要,因为在您看来根本没有节点。只有容器(或 Pod)。

在下图中,您可以看到右侧的Hyper 容器直接在多租户裸金属容器云上运行:

AWS 最近发布了 Fargate,类似地将底层实例抽象化,并允许您在云中安排容器。与 EKS 结合使用,可能成为部署 Kubernetes 的最流行方式。

使用 Kubernetes 推动信封

在本节中,我们将看到 Kubernetes 团队如何将 Kubernetes 推向极限。这些数字相当说明问题,但一些工具和技术,如 Kubemark,是巧妙的,您甚至可以使用它们来测试您的集群。在野外,有一些拥有 3,000 个节点的 Kubernetes 集群。在 CERN,OpenStack 团队实现了每秒 2 百万次请求:

superuser.openstack.org/articles/scaling-magnum-and-kubernetes-2-million-requests-per-second/

Mirantis 在其扩展实验室进行了性能和扩展测试,部署了 5,000 个 Kubernetes 节点(在虚拟机中)在 500 台物理服务器上。

有关 Mirantis 的更多详细信息,请参阅:bit.ly/2oijqQY

OpenAI 将其机器学习 Kubernetes 集群扩展到 2,500 个节点,并学到了一些宝贵的经验教训,比如注意日志代理的查询负载,并将事件存储在单独的etcd集群中:

blog.openai.com/scaling-kubernetes-to-2500-nodes/

在本节结束时,您将欣赏到改进大规模 Kubernetes 所需的努力和创造力,您将了解单个 Kubernetes 集群的极限以及预期的性能,您将深入了解一些工具和技术,可以帮助您评估自己的 Kubernetes 集群的性能。

改进 Kubernetes 的性能和可扩展性

Kubernetes 团队在 Kubernetes 1.6 中大力专注于性能和可扩展性。当 Kubernetes 1.2 发布时,它支持 Kubernetes 服务水平目标内的最多 1,000 个节点的集群。Kubernetes 1.3 将该数字增加到 2,000 个节点,而 Kubernetes 1.6 将其提高到惊人的 5,000 个节点每个集群。我们稍后会详细介绍这些数字,但首先让我们来看看 Kubernetes 是如何实现这些令人印象深刻的改进的。

在 API 服务器中缓存读取

Kubernetes 将系统状态保存在 etcd 中,这是非常可靠的,尽管不是超级快速的(尽管 etcd3 专门提供了巨大的改进,以便实现更大的 Kubernetes 集群)。各种 Kubernetes 组件在该状态的快照上操作,并不依赖于实时更新。这一事实允许在一定程度上以一些延迟换取吞吐量。所有快照都曾由 etcd 监视更新。现在,API 服务器具有用于更新状态快照的内存读取缓存。内存读取缓存由 etcd 监视更新。这些方案显著减少了 etcd 的负载,并增加了 API 服务器的整体吞吐量。

Pod 生命周期事件生成器

增加集群中节点的数量对于水平扩展至关重要,但 Pod 密度也至关重要。Pod 密度是 Kubelet 在一个节点上能够有效管理的 Pod 数量。如果 Pod 密度低,那么你就不能在一个节点上运行太多的 Pod。这意味着你可能无法从更强大的节点(每个节点的 CPU 和内存更多)中受益,因为 Kubelet 将无法管理更多的 Pod。另一种选择是强迫开发人员妥协他们的设计,并创建粗粒度的 Pod,每个 Pod 执行更多的工作。理想情况下,Kubernetes 在 Pod 粒度方面不应该强迫你的决定。Kubernetes 团队非常了解这一点,并投入了大量工作来改善 Pod 密度。

在 Kubernetes 1.1 中,官方(经过测试和宣传)的数量是每个节点 30 个 Pod。我实际上在 Kubernetes 1.1 上每个节点运行了 40 个 Pod,但我付出了过多的 kubelet 开销,这从工作 Pod 中窃取了 CPU。在 Kubernetes 1.2 中,这个数字跳升到每个节点 100 个 Pod。

Kubelet 以自己的 go 例程不断轮询容器运行时,以获取每个 pod 的状态。这给容器运行时带来了很大的压力,因此在性能高峰期会出现可靠性问题,特别是 CPU 利用率方面。解决方案是Pod 生命周期事件生成器PLEG)。PLEG 的工作方式是列出所有 pod 和容器的状态,并将其与先前的状态进行比较。这只需要一次,就可以对所有的 pod 和容器进行比较。然后,通过将状态与先前的状态进行比较,PLEG 知道哪些 pod 需要再次同步,并只调用这些 pod。这一变化导致 Kubelet 和容器运行时的 CPU 使用率显著降低了四倍。它还减少了轮询周期,提高了响应性。

以下图表显示了 Kubernetes 1.1 和 Kubernetes 1.2 上120 个 pod 的 CPU 利用率。您可以清楚地看到 4 倍的因素:

使用协议缓冲区对 API 对象进行序列化

API 服务器具有 REST API。REST API 通常使用 JSON 作为其序列化格式,Kubernetes API 服务器也不例外。然而,JSON 序列化意味着将 JSON 编组和解组为本机数据结构。这是一个昂贵的操作。在大规模的 Kubernetes 集群中,许多组件需要频繁查询或更新 API 服务器。所有这些 JSON 解析和组合的成本很快就会累积起来。在 Kubernetes 1.3 中,Kubernetes 团队添加了一个高效的协议缓冲区序列化格式。JSON 格式仍然存在,但 Kubernetes 组件之间的所有内部通信都使用协议缓冲区序列化格式。

etcd3

Kubernetes 在 Kubernetes 1.6 中从 etcd2 切换到 etcd3。这是一件大事。由于 etcd2 的限制,尤其是与 watch 实现相关的限制,将 Kubernetes 扩展到 5000 个节点是不可能的。Kubernetes 的可扩展性需求推动了 etcd3 的许多改进,因为 CoreOS 将 Kubernetes 作为一个衡量标准。一些重要的项目如下:

  • GRPC 而不是 REST-etcd2 具有 REST API,etcd3 具有 gRPC API(以及通过 gRPC 网关的 REST API)。在 gRPC 基础上的 http/2 协议可以使用单个 TCP 连接来处理多个请求和响应流。

  • 租约而不是 TTL-etcd2 使用生存时间TTL)来过期键,而 etcd3 使用带有 TTL 的租约,多个键可以共享同一个键。这显著减少了保持活动的流量。

  • etcd3 的 watch 实现利用了 GRPC 双向流,并维护单个 TCP 连接以发送多个事件,这至少减少了一个数量级的内存占用。

  • 使用 etcd3,Kubernetes 开始将所有状态存储为 protobug,这消除了许多浪费的 JSON 序列化开销。

其他优化

Kubernetes 团队进行了许多其他优化:

  • 优化调度程序(导致调度吞吐量提高了 5-10 倍)

  • 将所有控制器切换到新的推荐设计,使用共享通知器,这减少了控制器管理器的资源消耗-有关详细信息,请参阅此文档github.com/kubernetes/community/blob/master/contributors/devel/controllers.md

  • 优化 API 服务器中的单个操作(转换、深拷贝、补丁)

  • 减少 API 服务器中的内存分配(这对 API 调用的延迟有显著影响)

测量 Kubernetes 的性能和可伸缩性

为了提高性能和可伸缩性,您需要清楚地知道您想要改进什么,以及如何去衡量这些改进。您还必须确保在追求性能和可伸缩性的过程中不违反基本属性和保证。我喜欢性能改进的地方在于它们通常可以免费为您带来可伸缩性的改进。例如,如果一个 pod 需要节点的 50% CPU 来完成其工作,而您改进了性能,使得该 pod 只需要 33% 的 CPU 就能完成相同的工作,那么您可以在该节点上突然运行三个 pod 而不是两个,从而将集群的可伸缩性整体提高了 50%(或者将成本降低了 33%)。

Kubernetes 的 SLO

Kubernetes 有服务水平目标SLOs)。在尝试改进性能和可伸缩性时,必须遵守这些保证。Kubernetes 对 API 调用有一秒的响应时间。那是 1,000 毫秒。它实际上在大多数情况下实现了一个数量级的更快响应时间。

测量 API 的响应速度

API 有许多不同的端点。没有简单的 API 响应性数字。每个调用都必须单独测量。此外,由于系统的复杂性和分布式特性,更不用说网络问题,结果可能会有很大的波动。一个可靠的方法是将 API 测量分成单独的端点,然后随着时间的推移进行大量测试,并查看百分位数(这是标准做法)。

使用足够的硬件来管理大量对象也很重要。Kubernetes 团队在这次测试中使用了一个 32 核心、120GB 的虚拟机作为主节点。

下图描述了 Kubernetes 1.3 各种重要 API 调用延迟的 50th、90th 和 99th 百分位数。你可以看到,90th 百分位数非常低,低于 20 毫秒。甚至对于DELETE pods 操作,99th 百分位数也低于 125 毫秒,对于其他所有操作也低于 100 毫秒:

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

这很好,但是看看 Kubernetes 1.6 在一个 5000 个节点的集群上的 API 调用延迟:

衡量端到端的 Pod 启动时间

大型动态集群最重要的性能特征之一是端到端的 Pod 启动时间。Kubernetes 一直在创建、销毁和调度 Pod。可以说,Kubernetes 的主要功能是调度 Pod。

在下图中,您可以看到 Pod 启动时间比 API 调用不太波动。这是有道理的,因为有很多工作需要做,比如启动一个不依赖于集群大小的运行时的新实例。在拥有 1,000 个节点的 Kubernetes 1.2 上,启动 Pod 的 99th 百分位端到端时间不到 3 秒。在 Kubernetes 1.3 上,启动 Pod 的 99th 百分位端到端时间略高于 2.5 秒。值得注意的是,时间非常接近,但在拥有 2,000 个节点的 Kubernetes 1.3 上,比拥有 1,000 个节点的集群稍微好一点:

Kubernetes 1.6 将其提升到了下一个级别,并在更大的集群上表现得更好:

在规模上测试 Kubernetes

拥有数千个节点的集群是昂贵的。即使像 Kubernetes 这样得到 Google 和其他行业巨头支持的项目,仍然需要找到合理的测试方法,而不会让自己破产。

Kubernetes 团队每次发布都会在真实集群上运行全面的测试,以收集真实世界的性能和可伸缩性数据。然而,还需要一种轻量级和更便宜的方法来尝试潜在的改进,并检测回归。这就是 Kubemark。

介绍 Kubemark 工具

Kubemark 是一个运行模拟节点(称为空心节点)的 Kubernetes 集群,用于针对大规模(空心)集群运行轻量级基准测试。一些在真实节点上可用的 Kubernetes 组件,如 kubelet,被替换为空心 kubelet。空心 kubelet 模拟了真实 kubelet 的许多功能。空心 kubelet 实际上不会启动任何容器,也不会挂载任何卷。但从 Kubernetes 集群的角度来看 - 存储在 etcd 中的状态 - 所有这些对象都存在,您可以查询 API 服务器。空心 kubelet 实际上是带有注入的模拟 Docker 客户端的真实 kubelet,该客户端不执行任何操作。

另一个重要的空心组件是hollow-proxy,它模拟了 Kubeproxy 组件。它再次使用真实的 Kubeproxy 代码,具有一个不执行任何操作并避免触及 iptables 的模拟 proxier 接口。

设置 Kubemark 集群

Kubemark 集群利用了 Kubernetes 的强大功能。要设置 Kubemark 集群,请执行以下步骤:

  1. 创建一个常规的 Kubernetes 集群,我们可以在其中运行N hollow-nodes

  2. 创建一个专用的 VM 来启动 Kubemark 集群的所有主要组件。

  3. 在基本 Kubernetes 集群上安排N 个空节点 pods。这些空节点被配置为与运行在专用 VM 上的 Kubemark API 服务器进行通信。

  4. 通过在基本集群上安排并配置它们与 Kubemark API 服务器进行通信来创建附加的 pods。

GCP 上提供了完整的指南,网址为bit.ly/2nPMkwc

将 Kubemark 集群与真实世界集群进行比较

Kubemark 集群的性能与真实集群的性能非常相似。对于 pod 启动的端到端延迟,差异可以忽略不计。对于 API 的响应性,差异较大,尽管通常不到两倍。然而,趋势完全相同:真实集群中的改进/退化在 Kubemark 中表现为类似百分比的下降/增加。

总结

在本章中,我们涵盖了许多与扩展 Kubernetes 集群相关的主题。我们讨论了水平 pod 自动缩放器如何根据 CPU 利用率或其他指标自动管理运行的 pod 数量,如何在自动缩放的情况下正确安全地执行滚动更新,以及如何通过资源配额处理稀缺资源。然后,我们转向了整体容量规划和管理集群的物理或虚拟资源。最后,我们深入探讨了将单个 Kubernetes 集群扩展到处理 5,000 个节点的真实示例。

到目前为止,您已经对 Kubernetes 集群面对动态和不断增长的工作负载时涉及的所有因素有了很好的理解。您有多种工具可供选择,用于规划和设计自己的扩展策略。

在下一章中,我们将深入探讨高级 Kubernetes 网络。Kubernetes 具有基于通用网络接口CNI)的网络模型,并支持多个提供程序。

第十章:高级 Kubernetes 网络

在本章中,我们将研究网络这一重要主题。作为一个编排平台,Kubernetes 管理在不同机器(物理或虚拟)上运行的容器/Pod,并需要一个明确的网络模型。我们将讨论以下主题:

  • Kubernetes 网络模型

  • Kubernetes 支持的标准接口,如 EXEC、Kubenet,特别是 CNI

  • 满足 Kubernetes 网络要求的各种网络解决方案

  • 网络策略和负载均衡选项

  • 编写自定义 CNI 插件

在本章结束时,您将了解 Kubernetes 对网络的处理方式,并熟悉标准接口、网络实现和负载均衡等方面的解决方案空间。甚至可以自己编写自己的 CNI 插件。

理解 Kubernetes 网络模型

Kubernetes 网络模型基于一个扁平的地址空间。集群中的所有 Pod 都可以直接相互通信。每个 Pod 都有自己的 IP 地址。无需配置任何 NAT。此外,同一 Pod 中的容器共享其 Pod 的 IP 地址,并且可以通过 localhost 相互通信。这个模型非常有见地,一旦设置好,就会极大地简化开发人员和管理员的生活。它特别容易将传统网络应用迁移到 Kubernetes。一个 Pod 代表一个传统节点,每个容器代表一个传统进程。

Pod 内通信(容器到容器)

运行中的 Pod 始终被调度到一个(物理或虚拟)节点上。这意味着所有的容器都在同一个节点上运行,并且可以以各种方式相互通信,比如本地文件系统、任何 IPC 机制,或者使用 localhost 和众所周知的端口。不同的 Pod 之间不会发生端口冲突,因为每个 Pod 都有自己的 IP 地址,当 Pod 中的容器使用 localhost 时,它只适用于 Pod 的 IP 地址。因此,如果 Pod 1 中的容器 1 连接到 Pod 1 上的端口1234,而 Pod 1 上的容器 2 监听该端口,它不会与同一节点上运行的 Pod 2 中的另一个容器监听的端口1234发生冲突。唯一需要注意的是,如果要将端口暴露给主机,那么应该注意 Pod 到节点的亲和性。这可以通过多种机制来处理,比如 DaemonSet 和 Pod 反亲和性。

Pod 间通信(Pod 到 Pod)

在 Kubernetes 中,Pod 被分配了一个网络可见的 IP 地址(不是私有的节点)。Pod 可以直接通信,无需网络地址转换、隧道、代理或任何其他混淆层的帮助。可以使用众所周知的端口号来进行无需配置的通信方案。Pod 的内部 IP 地址与其他 Pod 看到的外部 IP 地址相同(在集群网络内;不暴露给外部世界)。这意味着标准的命名和发现机制,如 DNS,可以直接使用。

Pod 与服务之间的通信

Pod 可以使用它们的 IP 地址和众所周知的端口直接相互通信,但这需要 Pod 知道彼此的 IP 地址。在 Kubernetes 集群中,Pod 可能会不断被销毁和创建。服务提供了一个非常有用的间接层,因为即使实际响应请求的 Pod 集合不断变化,服务也是稳定的。此外,您会获得自动的高可用负载均衡,因为每个节点上的 Kube-proxy 负责将流量重定向到正确的 Pod:

外部访问

最终,一些容器需要从外部世界访问。Pod IP 地址在外部不可见。服务是正确的载体,但外部访问通常需要两次重定向。例如,云服务提供商负载均衡器是 Kubernetes 感知的,因此它不能直接将流量定向到运行可以处理请求的 Pod 的节点。相反,公共负载均衡器只是将流量定向到集群中的任何节点,该节点上的 Kube-proxy 将再次重定向到适当的 Pod,如果当前节点不运行必要的 Pod。

下图显示了右侧的外部负载均衡器所做的一切只是将流量发送到达到代理的所有节点,代理负责进一步路由,如果需要的话。

Kubernetes 网络与 Docker 网络的对比

Docker 网络遵循不同的模型,尽管随着时间的推移,它已经趋向于 Kubernetes 模型。在 Docker 网络中,每个容器都有自己的私有 IP 地址,来自172.xxx.xxx.xxx地址空间,限定在自己的节点上。它可以通过它们自己的172.xxx.xxx.xxx IP 地址与同一节点上的其他容器进行通信。这对 Docker 来说是有意义的,因为它没有多个交互容器的 pod 的概念,所以它将每个容器建模为一个具有自己网络身份的轻量级 VM。请注意,使用 Kubernetes,运行在同一节点上的不同 pod 的容器不能通过 localhost 连接(除非暴露主机端口,这是不鼓励的)。整个想法是,一般来说,Kubernetes 可以在任何地方杀死和创建 pod,因此不同的 pod 一般不应该依赖于节点上可用的其他 pod。守护进程集是一个值得注意的例外,但 Kubernetes 网络模型旨在适用于所有用例,并且不为同一节点上不同 pod 之间的直接通信添加特殊情况。

Docker 容器如何跨节点通信?容器必须将端口发布到主机。这显然需要端口协调,因为如果两个容器尝试发布相同的主机端口,它们将互相冲突。然后容器(或其他进程)连接到被通道化到容器中的主机端口。一个很大的缺点是,容器无法自我注册到外部服务,因为它们不知道它们所在主机的 IP 地址。您可以通过在运行容器时将主机的 IP 地址作为环境变量传递来解决这个问题,但这需要外部协调并且使过程复杂化。

以下图表显示了 Docker 的网络设置。每个容器都有自己的 IP 地址;Docker 在每个节点上创建了docker0桥接:

查找和发现

为了使 pod 和容器能够相互通信,它们需要找到彼此。容器定位其他容器或宣布自己有几种方法。还有一些架构模式允许容器间间接交互。每种方法都有其优缺点。

自注册

我们已经多次提到自注册。让我们确切地理解它的含义。当一个容器运行时,它知道其 pod 的 IP 地址。每个希望对集群中的其他容器可访问的容器都可以连接到某个注册服务并注册其 IP 地址和端口。其他容器可以查询注册服务以获取所有已注册容器的 IP 地址和端口,并连接到它们。当一个容器被销毁(正常情况下),它将取消注册。如果一个容器非正常死亡,那么需要建立一些机制来检测。例如,注册服务可以定期 ping 所有已注册的容器,或者要求容器定期向注册服务发送保持活动的消息。

自注册的好处在于一旦通用注册服务就位(无需为不同目的定制),就无需担心跟踪容器。另一个巨大的好处是,容器可以采用复杂的策略,并决定在本地条件下暂时取消注册,比如如果一个容器很忙,不想在这一刻接收更多请求。这种智能和分散的动态负载平衡在全球范围内很难实现。缺点是注册服务是另一个非标准组件,容器需要了解它以便定位其他容器。

服务和端点

Kubernetes 服务可以被视为注册服务。属于服务的 pod 会根据其标签自动注册。其他 pod 可以查找端点以找到所有服务 pod,或者利用服务本身直接发送消息到服务,消息将被路由到其中一个后端 pod。尽管大多数情况下,pod 将消息直接发送到服务本身,由服务转发到其中一个后端 pod。

与队列松散耦合的连接

如果容器可以相互通信,而不知道它们的 IP 地址和端口,甚至不知道服务 IP 地址或网络名称呢?如果大部分通信可以是异步和解耦的呢?在许多情况下,系统可以由松散耦合的组件组成,这些组件不仅不知道其他组件的身份,甚至不知道其他组件的存在。队列有助于这种松散耦合的系统。组件(容器)监听来自队列的消息,响应消息,执行它们的工作,并在队列中发布有关进度、完成状态和错误的消息。队列有许多好处:

  • 无需协调即可添加处理能力;只需添加更多监听队列的容器

  • 通过队列深度轻松跟踪整体负载

  • 通过对消息和/或主题进行版本控制,轻松同时运行多个组件的不同版本

  • 通过使多个消费者以不同模式处理请求,轻松实现负载均衡以及冗余

队列的缺点包括:

  • 需要确保队列提供适当的耐用性和高可用性,以免成为关键的单点故障。

  • 容器需要使用异步队列 API(可以抽象化)

  • 实现请求-响应需要在响应队列上进行有些繁琐的监听

总的来说,队列是大规模系统的一个很好的机制,可以在大型 Kubernetes 集群中使用,以简化协调工作。

与数据存储松散耦合的连接

另一种松散耦合的方法是使用数据存储(例如 Redis)存储消息,然后其他容器可以读取它们。虽然可能,但这不是数据存储的设计目标,结果通常是繁琐、脆弱,并且性能不佳。数据存储针对数据存储进行了优化,而不是用于通信。也就是说,数据存储可以与队列一起使用,其中一个组件将一些数据存储在数据存储中,然后发送一条消息到队列,表示数据已准备好进行处理。多个组件监听该消息,并且都开始并行处理数据。

Kubernetes 入口

Kubernetes 提供了一个入口资源和控制器,旨在将 Kubernetes 服务暴露给外部世界。当然,您也可以自己做,但定义入口所涉及的许多任务在特定类型的入口(如 Web 应用程序、CDN 或 DDoS 保护器)的大多数应用程序中是常见的。您还可以编写自己的入口对象。

“入口”对象通常用于智能负载平衡和 TLS 终止。您可以从内置入口中受益,而不是配置和部署自己的 NGINX 服务器。如果您需要复习,请转到第六章,使用关键的 Kubernetes 资源,在那里我们讨论了带有示例的入口资源。

Kubernetes 网络插件

Kubernetes 具有网络插件系统,因为网络如此多样化,不同的人希望以不同的方式实现它。Kubernetes 足够灵活,可以支持任何场景。主要的网络插件是 CNI,我们将深入讨论。但 Kubernetes 还配备了一个更简单的网络插件,称为 Kubenet。在我们详细讨论之前,让我们就 Linux 网络的基础知识达成一致(只是冰山一角)。

基本的 Linux 网络

默认情况下,Linux 具有单个共享网络空间。物理网络接口都可以在此命名空间中访问,但物理命名空间可以分成多个逻辑命名空间,这与容器网络非常相关。

IP 地址和端口

网络实体通过其 IP 地址进行标识。服务器可以在多个端口上监听传入连接。客户端可以连接(TCP)或向其网络内的服务器发送数据(UDP)。

网络命名空间

命名空间将一堆网络设备分组在一起,以便它们可以在同一命名空间中到达其他服务器,但即使它们在物理上位于同一网络上,也不能到达其他服务器。通过桥接、交换机、网关和路由可以连接网络或网络段。

子网、网络掩码和 CIDR

在设计和维护网络时,网络段的细分非常有用。将网络划分为具有共同前缀的较小子网是一种常见做法。这些子网可以由表示子网大小(可以包含多少主机)的位掩码来定义。例如,255.255.255.0的子网掩码意味着前三个八位字节用于路由,只有 256(实际上是 254)个单独的主机可用。无类别域间路由(CIDR)表示法经常用于此目的,因为它更简洁,编码更多信息,并且还允许将来自多个传统类别(A、B、C、D、E)的主机组合在一起。例如,172.27.15.0/24表示前 24 位(三个八位字节)用于路由。

虚拟以太网设备

虚拟以太网veth)设备代表物理网络设备。当您创建一个与物理设备连接的veth时,您可以将该veth(以及物理设备)分配到一个命名空间中,其他命名空间的设备无法直接访问它,即使它们在物理上位于同一个本地网络上。

桥接器

桥接器将多个网络段连接到一个聚合网络,以便所有节点可以彼此通信。桥接是在 OSI 网络模型的 L1(物理)和 L2(数据链路)层进行的。

路由

路由连接不同的网络,通常基于路由表,指示网络设备如何将数据包转发到其目的地。路由是通过各种网络设备进行的,如路由器、桥接器、网关、交换机和防火墙,包括常规的 Linux 框。

最大传输单元

最大传输单元MTU)确定数据包的大小限制。例如,在以太网网络上,MTU 为 1500 字节。MTU 越大,有效载荷和标头之间的比率就越好,这是一件好事。缺点是最小延迟减少,因为您必须等待整个数据包到达,而且如果出现故障,您必须重新传输整个数据包。

Pod 网络

以下是一个描述通过veth0在网络层面上描述 pod、主机和全局互联网之间关系的图表:

Kubenet

回到 Kubernetes。Kubenet 是一个网络插件;它非常基础,只是创建一个名为cbr0的 Linux 桥接和为每个 pod 创建一个veth。云服务提供商通常使用它来设置节点之间的通信路由规则,或者在单节点环境中使用。veth对将每个 pod 连接到其主机节点,使用来自主机 IP 地址范围的 IP 地址。

要求

Kubenet 插件有以下要求:

  • 必须为节点分配一个子网,以为其 pod 分配 IP 地址

  • 版本 0.2.0 或更高版本需要标准的 CNI 桥接、lo和 host-local 插件

  • Kubelet 必须使用--network-plugin=kubenet参数运行

  • Kubelet 必须使用--non-masquerade-cidr=<clusterCidr>参数运行

设置 MTU

MTU 对于网络性能至关重要。Kubernetes 网络插件(如 Kubenet)会尽最大努力推断最佳 MTU,但有时它们需要帮助。如果现有的网络接口(例如 Docker 的docker0桥接)设置了较小的 MTU,则 Kubenet 将重用它。另一个例子是 IPSEC,由于 IPSEC 封装开销增加,需要降低 MTU,但 Kubenet 网络插件没有考虑到这一点。解决方案是避免依赖 MTU 的自动计算,只需通过--network-plugin-mtu命令行开关告诉 Kubelet 应该为网络插件使用什么 MTU,这个开关提供给所有网络插件。然而,目前只有 Kubenet 网络插件考虑了这个命令行开关。

容器网络接口(CNI)

CNI 既是一个规范,也是一组用于编写网络插件以配置 Linux 容器中的网络接口的库(不仅仅是 Docker)。该规范实际上是从 rkt 网络提案演变而来的。CNI 背后有很多动力,正在快速成为行业标准。一些使用 CNI 的组织有:

  • Kubernetes

  • Kurma

  • 云原生

  • Nuage

  • 红帽

  • Mesos

CNI 团队维护一些核心插件,但也有很多第三方插件对 CNI 的成功做出了贡献:

  • Project Calico:三层虚拟网络

  • Weave:多主机 Docker 网络

  • Contiv 网络:基于策略的网络

  • Cilium:用于容器的 BPF 和 XDP

  • Multus:一个多插件

  • CNI-Genie:通用 CNI 网络插件

  • Flannel:为 Kubernetes 设计的容器网络布局

  • Infoblox:企业级容器 IP 地址管理

容器运行时

CNI 为网络应用容器定义了插件规范,但插件必须插入提供一些服务的容器运行时中。在 CNI 的上下文中,应用容器是一个可寻址的网络实体(具有自己的 IP 地址)。对于 Docker,每个容器都有自己的 IP 地址。对于 Kubernetes,每个 pod 都有自己的 IP 地址,而 pod 是 CNI 容器,而不是 pod 内的容器。

同样,rkt 的应用容器类似于 Kubernetes 中的 pod,因为它们可能包含多个 Linux 容器。如果有疑问,只需记住 CNI 容器必须有自己的 IP 地址。运行时的工作是配置网络,然后执行一个或多个 CNI 插件,以 JSON 格式将网络配置传递给它们。

以下图表显示了一个容器运行时使用 CNI 插件接口与多个 CNI 插件进行通信:

CNI 插件

CNI 插件的工作是将网络接口添加到容器网络命名空间,并通过veth对将容器桥接到主机。然后,它应通过 IPAM(IP 地址管理)插件分配 IP 地址并设置路由。

容器运行时(Docker,rkt 或任何其他符合 CRI 标准的运行时)将 CNI 插件作为可执行文件调用。插件需要支持以下操作:

  • 将容器添加到网络

  • 从网络中删除容器

  • 报告版本

插件使用简单的命令行界面,标准输入/输出和环境变量。以 JSON 格式的网络配置通过标准输入传递给插件。其他参数被定义为环境变量:

  • CNI_COMMAND:指示所需操作的命令;ADDDELVERSION

  • CNI_CONTAINERID:容器 ID。

  • CNI_NETNS:网络命名空间文件的路径。

  • * CNI_IFNAME:要设置的接口名称;插件必须遵守此接口名称或返回一个error

  • * CNI_ARGS:用户在调用时传入的额外参数。字母数字键值对由分号分隔,例如,FOO=BAR;ABC=123

  • CNI_PATH:要搜索 CNI 插件可执行文件的路径列表。路径由操作系统特定的列表分隔符分隔,例如,在 Linux 上是:,在 Windows 上是

如果命令成功,插件将返回零退出代码,并且生成的接口(在ADD命令的情况下)将作为 JSON 流式传输到标准输出。这种低技术接口很聪明,因为它不需要任何特定的编程语言、组件技术或二进制 API。CNI 插件编写者也可以使用他们喜欢的编程语言。

使用ADD命令调用 CNI 插件的结果如下:

{
 "cniVersion": "0.3.0",
 "interfaces": [ (this key omitted by IPAM plugins)
 {
 "name": "<name>",
 "mac": "<MAC address>", (required if L2 addresses are meaningful)
 "sandbox": "<netns path or hypervisor identifier>" (required for container/hypervisor interfaces, empty/omitted for host interfaces)
 }
 ],
 "ip": [
 {
 "version": "<4-or-6>",
 "address": "<ip-and-prefix-in-CIDR>",
 "gateway": "<ip-address-of-the-gateway>", (optional)
 "interface": <numeric index into 'interfaces' list>
 },
 ...
 ],
 "routes": [ (optional)
 {
 "dst": "<ip-and-prefix-in-cidr>",
 "gw": "<ip-of-next-hop>" (optional)
 },
 ...
 ]
 "dns": {
 "nameservers": <list-of-nameservers> (optional)
 "domain": <name-of-local-domain> (optional)
 "search": <list-of-additional-search-domains> (optional)
 "options": <list-of-options> (optional)
 }
}

输入网络配置包含大量信息:cniVersion、名称、类型、args(可选)、ipMasq(可选)、ipamdnsipamdns参数是具有自己指定键的字典。以下是网络配置的示例:

{
 "cniVersion": "0.3.0",
 "name": "dbnet",
 "type": "bridge",
 // type (plugin) specific
 "bridge": "cni0",
 "ipam": {
 "type": "host-local",
 // ipam specific
 "subnet": "10.1.0.0/16",
 "gateway": "10.1.0.1"
 },
 "dns": {
 "nameservers": [ "10.1.0.1" ]
 }
}  

请注意,可以添加额外的特定于插件的元素。在这种情况下,bridge: cni0元素是特定的bridge插件理解的自定义元素。

CNI 规范还支持网络配置列表,其中可以按顺序调用多个 CNI 插件。稍后,我们将深入研究一个完全成熟的 CNI 插件实现。

Kubernetes 网络解决方案

网络是一个广阔的话题。有许多设置网络和连接设备、pod 和容器的方法。Kubernetes 对此不能有意见。对于 pod 的高级网络模型是 Kubernetes 规定的。在这个空间内,有许多有效的解决方案是可能的,具有不同环境的各种功能和策略。在本节中,我们将研究一些可用的解决方案,并了解它们如何映射到 Kubernetes 网络模型。

裸金属集群上的桥接

最基本的环境是一个只有 L2 物理网络的原始裸金属集群。您可以使用 Linux 桥设备将容器连接到物理网络。该过程非常复杂,需要熟悉低级 Linux 网络命令,如brctlip addrip routeip linknsenter等。如果您打算实施它,这篇指南可以作为一个很好的起点(搜索使用 Linux 桥设备部分):blog.oddbit.com/2014/08/11/four-ways-to-connect-a-docker/

Contiv

Contiv 是一个通用的容器网络插件,可以直接与 Docker、Mesos、Docker Swarm 以及当然 Kubernetes 一起使用,通过一个 CNI 插件。Contiv 专注于与 Kubernetes 自身网络策略对象有些重叠的网络策略。以下是 Contiv net 插件的一些功能:

  • 支持 libnetwork 的 CNM 和 CNI 规范

  • 功能丰富的策略模型,提供安全、可预测的应用部署

  • 用于容器工作负载的最佳吞吐量

  • 多租户、隔离和重叠子网

  • 集成 IPAM 和服务发现

  • 各种物理拓扑:

  • Layer2(VLAN)

  • Layer3(BGP)

  • 覆盖(VXLAN)

  • 思科 SDN 解决方案(ACI)

  • IPv6 支持

  • 可扩展的策略和路由分发

  • 与应用蓝图的集成,包括以下内容:

  • Docker-compose

  • Kubernetes 部署管理器

  • 服务负载平衡内置东西向微服务负载平衡

  • 用于存储、控制(例如,etcd/consul)、网络和管理流量的流量隔离

  • Contiv 具有许多功能和能力。由于其广泛的适用范围以及它适用于多个平台,我不确定它是否是 Kubernetes 的最佳选择。

Open vSwitch

Open vSwitch 是一个成熟的基于软件的虚拟交换解决方案,得到许多大公司的认可。Open Virtualization NetworkOVN)解决方案可以让您构建各种虚拟网络拓扑。它有一个专门的 Kubernetes 插件,但设置起来并不简单,正如这个指南所示:github.com/openvswitch/ovn-kubernetes。Linen CNI 插件可能更容易设置,尽管它不支持 OVN 的所有功能:github.com/John-Lin/linen-cni。这是 Linen CNI 插件的图表:

Open vSwitch 可以连接裸机服务器、虚拟机和 pod/容器,使用相同的逻辑网络。它实际上支持覆盖和底层模式。

以下是一些其关键特性:

  • 标准的 802.1Q VLAN 模型,带有干线和接入端口

  • 上游交换机上带或不带 LACP 的 NIC 绑定

  • NetFlow、sFlow(R)和镜像,以增加可见性

  • QoS(服务质量)配置,以及流量控制

  • Geneve、GRE、VXLAN、STT 和 LISP 隧道

  • 802.1ag 连接故障管理

  • OpenFlow 1.0 加上许多扩展

  • 具有 C 和 Python 绑定的事务配置数据库

  • 使用 Linux 内核模块进行高性能转发

Nuage 网络 VCS

Nuage 网络的虚拟化云服务VCS)产品提供了一个高度可扩展的基于策略的软件定义网络SDN)平台。这是一个建立在开源 Open vSwitch 数据平面之上的企业级产品,配备了基于开放标准构建的功能丰富的 SDN 控制器。

Nuage 平台使用覆盖层在 Kubernetes Pods 和非 Kubernetes 环境(VM 和裸金属服务器)之间提供无缝的基于策略的网络。Nuage 的策略抽象模型是针对应用程序设计的,使得声明应用程序的细粒度策略变得容易。该平台的实时分析引擎实现了对 Kubernetes 应用程序的可见性和安全监控。

此外,所有 VCS 组件都可以安装在容器中。没有特殊的硬件要求。

Canal

Canal 是两个开源项目的混合体:Calico 和 Flannel。Canal这个名字是这两个项目名称的混成词。由 CoreOS 开发的 Flannel 专注于容器网络,Calico专注于网络策略。最初,它们是独立开发的,但用户希望将它们一起使用。目前,开源的 Canal 项目是一个部署模式,可以将这两个项目作为独立的 CNI 插件进行安装。由 Calico 创始人组建的 Tigera 现在正在引导这两个项目,并计划更紧密地集成,但自从他们发布了用于 Kubernetes 的安全应用连接解决方案后,重点似乎转向了为 Flannel 和 Calico 做出贡献,以简化配置和集成,而不是提供统一的解决方案。以下图表展示了 Canal 的当前状态以及它与 Kubernetes 和 Mesos 等容器编排器的关系:

请注意,与 Kubernetes 集成时,Canal 不再直接使用etcd,而是依赖于 Kubernetes API 服务器。

法兰绒

Flannel 是一个虚拟网络,为每个主机提供一个子网,用于容器运行时。它在每个主机上运行一个flaneld代理,该代理从存储在etcd中的保留地址空间中为节点分配子网。容器之间以及最终主机之间的数据包转发由多个后端之一完成。最常见的后端使用默认情况下通过端口8285进行的 TUN 设备上的 UDP 进行隧道传输(确保防火墙中已打开)。

以下图表详细描述了 Flannel 的各个组件、它创建的虚拟网络设备以及它们如何通过docker0桥与主机和 pod 进行交互。它还显示了数据包的 UDP 封装以及它们在主机之间的传输:

其他后端包括以下内容:

  • vxlan:使用内核 VXLAN 封装数据包。

  • host-gw:通过远程机器 IP 创建到子网的 IP 路由。请注意,这需要在运行 Flannel 的主机之间直接的二层连接。

  • aws-vpc:在 Amazon VPC 路由表中创建 IP 路由。

  • gce:在 Google 计算引擎网络中创建 IP 路由。

  • alloc:仅执行子网分配(不转发数据包)。

  • ali-vpc:在阿里云 VPC 路由表中创建 IP 路由。

Calico 项目

Calico 是一个多功能的容器虚拟网络和网络安全解决方案。Calico 可以与所有主要的容器编排框架集成

和运行时:

  • Kubernetes(CNI 插件)

  • Mesos(CNI 插件)

  • Docker(libnework 插件)

  • OpenStack(Neutron 插件)

Calico 还可以在本地部署或在公共云上部署,具有完整的功能集。Calico 的网络策略执行可以针对每个工作负载进行专门化,并确保流量被精确控制,数据包始终从其源头到经过审查的目的地。Calico 可以自动将编排平台的网络策略概念映射到自己的网络策略。Kubernetes 网络策略的参考实现是 Calico。

Romana

Romana 是一个现代的云原生容器网络解决方案。它在第 3 层操作,利用标准 IP 地址管理技术。整个网络可以成为隔离单元,因为 Romana 使用 Linux 主机创建网关和网络的路由。在第 3 层操作意味着不需要封装。网络策略作为分布式防火墙在所有端点和服务上执行。跨云平台和本地部署的混合部署更容易,因为无需配置虚拟覆盖网络。

新的 Romana 虚拟 IP 允许本地用户通过外部 IP 和服务规范在第 2 层 LAN 上公开服务。

Romana 声称他们的方法带来了显著的性能改进。以下图表显示了 Romana 如何消除与 VXLAN 封装相关的大量开销。

Weave 网络

Weave 网络主要关注易用性和零配置。它在底层使用 VXLAN 封装和每个节点上的微型 DNS。作为开发人员,您在高抽象级别上操作。您为容器命名,Weave 网络让您连接并使用标准端口进行服务。这有助于您将现有应用程序迁移到容器化应用程序和微服务中。Weave 网络具有用于与 Kubernetes(和 Mesos)接口的 CNI 插件。在 Kubernetes 1.4 及更高版本上,您可以通过运行一个部署 DaemonSet 的单个命令将 Weave 网络集成到 Kubernetes 中。

kubectl apply -f https://git.io/weave-kube 

每个节点上的 Weave 网络 pod 将负责将您创建的任何新 pod 连接到 Weave 网络。Weave 网络支持网络策略 API,提供了一个完整而易于设置的解决方案。

有效使用网络策略

Kubernetes 网络策略是关于管理流向选定的 pod 和命名空间的网络流量。在部署和编排了数百个微服务的世界中,通常情况下是 Kubernetes,管理 pod 之间的网络和连接至关重要。重要的是要理解,它并不是主要的安全机制。如果攻击者可以访问内部网络,他们可能能够创建符合现有网络策略并与其他 pod 自由通信的自己的 pod。在前一节中,我们看了不同的 Kubernetes 网络解决方案,并侧重于容器网络接口。在本节中,重点是网络策略,尽管网络解决方案与如何在其之上实现网络策略之间存在着紧密的联系。

了解 Kubernetes 网络策略设计

网络策略是选择的 pod 之间以及其他网络端点之间如何通信的规范。NetworkPolicy资源使用标签选择 pod,并定义白名单规则,允许流量到达选定的 pod,除了给定命名空间的隔离策略允许的流量之外。

网络策略和 CNI 插件

网络策略和 CNI 插件之间存在复杂的关系。一些 CNI 插件同时实现了网络连接和网络策略,而其他一些只实现了其中一个方面,但它们可以与另一个实现了另一个方面的 CNI 插件合作(例如,Calico 和 Flannel)。

配置网络策略

网络策略是通过NetworkPolicy资源进行配置的。以下是一个示例网络策略:

apiVersion: networking.k8s.io/v1kind: NetworkPolicy 
metadata: 
 name: test-network-policy 
 namespace: default 
spec: 
 podSelector: 
  matchLabels: 
    role: db 
 ingress: 
  - from: 
     - namespaceSelector: 
        matchLabels: 
         project: awesome-project 
     - podSelector: 
        matchLabels: 
         role: frontend 
    ports: 
     - protocol: tcp 
       port: 6379 

实施网络策略

虽然网络策略 API 本身是通用的,并且是 Kubernetes API 的一部分,但实现与网络解决方案紧密耦合。这意味着在每个节点上都有一个特殊的代理或守门人,执行以下操作:

  • 拦截进入节点的所有流量

  • 验证其是否符合网络策略

  • 转发或拒绝每个请求

Kubernetes 提供了通过 API 定义和存储网络策略的功能。执行网络策略由网络解决方案或与特定网络解决方案紧密集成的专用网络策略解决方案来完成。Calico 和 Canal 是这种方法的很好的例子。Calico 有自己的网络解决方案和网络策略解决方案,它们可以一起工作,但也可以作为 Canal 的一部分在 Flannel 之上提供网络策略执行。在这两种情况下,这两个部分之间有紧密的集成。以下图表显示了 Kubernetes 策略控制器如何管理网络策略以及节点上的代理如何执行它:

负载均衡选项

负载均衡是动态系统中的关键能力,比如 Kubernetes 集群。节点、虚拟机和 Pod 会不断变化,但客户端无法跟踪哪个个体可以处理他们的请求。即使他们可以,也需要管理集群的动态映射,频繁刷新它,并处理断开连接、无响应或者慢速节点的复杂操作。负载均衡是一个经过验证和深入理解的机制,它增加了一层间接性,将内部动荡隐藏在集群外部的客户端或消费者之外。外部和内部负载均衡器都有选项。您也可以混合使用两者。混合方法有其特定的优缺点,比如性能与灵活性。

外部负载均衡器

外部负载均衡器是在 Kubernetes 集群之外运行的负载均衡器。必须有一个外部负载均衡器提供商,Kubernetes 可以与其交互,以配置外部负载均衡器的健康检查、防火墙规则,并获取负载均衡器的外部 IP 地址。

以下图表显示了负载均衡器(在云中)、Kubernetes API 服务器和集群节点之间的连接。外部负载均衡器有关于哪些 Pod 运行在哪些节点上的最新信息,并且可以将外部服务流量引导到正确的 Pod。

配置外部负载均衡器

通过服务配置文件或直接通过 Kubectl 配置外部负载均衡器。我们使用LoadBalancer服务类型,而不是使用ClusterIP服务类型,后者直接将 Kubernetes 节点公开为负载均衡器。这取决于外部负载均衡器提供程序在集群中是否已正确安装和配置。Google 的 GKE 是最经过充分测试的提供程序,但其他云平台在其云负载均衡器之上提供了集成解决方案。

通过配置文件

以下是一个实现此目标的示例服务配置文件:

{ 
      "kind": "Service", 
      "apiVersion": "v1", 
      "metadata": { 
        "name": "example-service" 
      }, 
      "spec": { 
        "ports": [{ 
          "port": 8765, 
          "targetPort": 9376 
        }], 
        "selector": { 
          "app": "example" 
        }, 
        "type": "LoadBalancer" 
      } 
} 

通过 Kubectl

您还可以使用直接的kubectl命令来实现相同的结果:

> kubectl expose rc example --port=8765 --target-port=9376 \
--name=example-service --type=LoadBalancer  

使用service配置文件还是kubectl命令的决定通常取决于您设置其余基础设施和部署系统的方式。配置文件更具声明性,可以说更适合生产使用,因为您希望以一种有版本控制、可审计和可重复的方式来管理基础设施。

查找负载均衡器 IP 地址

负载均衡器将有两个感兴趣的 IP 地址。内部 IP 地址可在集群内部用于访问服务。集群外部的客户端将使用外部 IP 地址。为外部 IP 地址创建 DNS 条目是一个良好的做法。要获取这两个地址,请使用kubectl describe命令。IP将表示内部 IP 地址。LoadBalancer ingress将表示外部 IP 地址:

> kubectl describe services example-service
    Name:  example-service
    Selector:   app=example
    Type:     LoadBalancer
    IP:     10.67.252.103
    LoadBalancer Ingress: 123.45.678.9
    Port:     <unnamed> 80/TCP
    NodePort:   <unnamed> 32445/TCP
    Endpoints:    10.64.0.4:80,10.64.1.5:80,10.64.2.4:80
    Session Affinity: None
    No events.

保留客户端 IP 地址

有时,服务可能对客户端的源 IP 地址感兴趣。直到 Kubernetes 1.5 版本,这些信息是不可用的。在 Kubernetes 1.5 中,通过注释仅在 GKE 上可用的 beta 功能可以获取源 IP 地址。在 Kubernetes 1.7 中,API 添加了保留原始客户端 IP 的功能。

指定原始客户端 IP 地址保留

您需要配置服务规范的以下两个字段:

  • service.spec.externalTrafficPolicy:此字段确定服务是否应将外部流量路由到节点本地端点或集群范围的端点,这是默认设置。集群选项不会显示客户端源 IP,并可能将跳转到不同节点,但会很好地分散负载。本地选项保留客户端源 IP,并且只要服务类型为LoadBalancerNodePort,就不会添加额外的跳转。其缺点是可能无法很好地平衡负载。

  • service.spec.healthCheckNodePort:此字段是可选的。如果使用,则服务健康检查将使用此端口号。默认值为分配节点端口。对于LoadBalancer类型的服务,如果其externalTrafficPolicy设置为Local,则会产生影响。

这是一个例子:

    {
      "kind": "Service",
      "apiVersion": "v1",
      "metadata": {
        "name": "example-service"
      },
      "spec": {
        "ports": [{
          "port": 8765,
          "targetPort": 9376
        }],
        "selector": {
          "app": "example"
        },
        "type": "LoadBalancer"
        "externalTrafficPolicy: "Local"
      }
    }  

即使在外部负载均衡中理解潜力

外部负载均衡器在节点级别运行;虽然它们将流量引导到特定的 pod,但负载分配是在节点级别完成的。这意味着如果您的服务有四个 pod,其中三个在节点 A 上,最后一个在节点 B 上,那么外部负载均衡器很可能会在节点 A 和节点 B 之间均匀分配负载。这将使节点 A 上的三个 pod 处理一半的负载(每个 1/6),而节点 B 上的单个 pod 将独自处理另一半的负载。未来可能会添加权重来解决这个问题。

服务负载均衡器

服务负载平衡旨在在 Kubernetes 集群内部传输内部流量,而不是用于外部负载平衡。这是通过使用clusterIP服务类型来实现的。可以通过使用NodePort服务类型直接公开服务负载均衡器,并将其用作外部负载均衡器,但它并不是为此用例而设计的。例如,诸如 SSL 终止和 HTTP 缓存之类的理想功能将不会很容易地可用。

以下图显示了服务负载均衡器(黄色云)如何将流量路由到其管理的后端 pod 之一(通过标签,当然):

入口

Kubernetes 中的入口在其核心是一组规则,允许入站连接到达集群服务。此外,一些入口控制器支持以下功能:

  • 连接算法

  • 请求限制

  • URL 重写和重定向

  • TCP/UDP 负载平衡

  • SSL 终止

  • 访问控制和授权

入口是使用入口资源指定的,并由入口控制器提供服务。重要的是要注意,入口仍处于测试阶段,尚未涵盖所有必要的功能。以下是一个管理流量进入两个服务的入口资源示例。规则将外部可见的http:// foo.bar.com/foo映射到s1服务,将http://foo.bar.com/bar映射到s2服务:

apiVersion: extensions/v1beta1 
kind: Ingress 
metadata: 
  name: test 
spec: 
  rules: 
  - host: foo.bar.com 
    http: 
      paths: 
      - path: /foo 
        backend: 
          serviceName: s1 
          servicePort: 80 
      - path: /bar 
        backend: 
          serviceName: s2 
          servicePort: 80 

目前有两个官方的入口控制器。其中一个是专门为 GCE 设计的 L7 入口控制器,另一个是更通用的 NGINX 入口控制器,可以通过 ConfigMap 配置 NGINX。NGNIX 入口控制器非常复杂,并且提供了许多目前通过入口资源直接不可用的功能。它使用端点 API 直接将流量转发到 pod。它支持 Minikube、GCE、AWS、Azure 和裸机集群。有关详细审查,请查看github.com/kubernetes/ingress-nginx

HAProxy

我们讨论了使用云提供商外部负载均衡器,使用LoadBalancer服务类型以及在集群内部使用ClusterIP的内部服务负载均衡器。如果我们想要一个自定义的外部负载均衡器,我们可以创建一个自定义的外部负载均衡器提供程序,并使用LoadBalancer或使用第三种服务类型NodePort高可用性HA)代理是一个成熟且经过实战考验的负载均衡解决方案。它被认为是在本地集群中实现外部负载均衡的最佳选择。这可以通过几种方式实现:

  • 利用NodePort并仔细管理端口分配

  • 实现自定义负载均衡器提供程序接口

  • 在集群内部运行 HAProxy 作为集群边缘前端服务器的唯一目标(无论是否经过负载平衡)

您可以使用所有方法与 HAProxy。不过,仍建议使用入口对象。service-loadbalancer项目是一个社区项目,它在 HAProxy 之上实现了一个负载均衡解决方案。您可以在github.com/kubernetes/contrib/tree/master/service-loadbalancer找到它。

利用 NodePort

每个服务将从预定义范围中分配一个专用端口。通常这是一个较高的范围,例如 30,000 及以上,以避免与使用低已知端口的其他应用程序发生冲突。在这种情况下,HAProxy 将在集群外部运行,并且将为每个服务配置正确的端口。然后它可以将任何流量转发到任何节点和 Kubernetes 通过内部服务,并且负载均衡器将其路由到适当的 pod(双重负载均衡)。当然,这是次优的,因为它引入了另一个跳跃。规避它的方法是查询 Endpoints API,并动态管理每个服务的后端 pod 列表,并直接将流量转发到 pod。

使用 HAProxy 自定义负载均衡器提供程序

这种方法稍微复杂一些,但好处是它与 Kubernetes 更好地集成,可以更容易地在本地和云端之间进行过渡。

在 Kubernetes 集群内运行 HAProxy

在这种方法中,我们在集群内部使用 HAProxy 负载均衡器。可能有多个运行 HAProxy 的节点,它们将共享相同的配置来映射传入请求并在后端服务器(以下图表中的 Apache 服务器)之间进行负载均衡。

Keepalived VIP

Keepalived 虚拟IP(VIP)并不一定是一个独立的负载均衡解决方案。它可以作为 NGINX 入口控制器或基于 HAProxy 的服务LoadBalancer的补充。主要动机是 Kubernetes 中的 pod 会移动,包括您的负载均衡器。这对需要稳定端点的网络外客户端造成了问题。由于性能问题,DNS 通常不够好。Keepalived 提供了一个高性能的虚拟 IP 地址,可以作为 NGINX 入口控制器或 HAProxy 负载均衡器的地址。Keepalived 利用核心 Linux 网络设施,如 IPVS(IP 虚拟服务器),并通过虚拟冗余路由协议VRRP)实现高可用性。一切都在第 4 层(TCP/UDP)运行。配置它需要一些努力和细节的关注。幸运的是,Kubernetes 有一个contrib项目可以帮助您入门,网址为github.com/kubernetes/contrib/tree/master/keepalived-vip

Træfic

Træfic 是一个现代的 HTTP 反向代理和负载均衡器。它旨在支持微服务。它可以与许多后端一起工作,包括 Kubernetes,以自动和动态地管理其配置。与传统的负载均衡器相比,这是一个改变游戏规则的产品。它具有令人印象深刻的功能列表:

  • 它很快

  • 单个 Go 可执行文件

  • 微型官方 Docker 镜像

  • Rest API

  • 热重新加载配置;无需重新启动进程

  • 断路器,重试

  • 轮询,重新平衡负载均衡器

  • 指标(Rest,Prometheus,Datadog,Statsd,InfluxDB)

  • 干净的 AngularJS Web UI

  • Websocket,HTTP/2,GRPC 准备就绪

  • 访问日志(JSON,CLF)

  • 支持 Let's Encrypt(自动 HTTPS 与更新)

  • 具有集群模式的高可用性

编写自己的 CNI 插件

在这一部分,我们将看看实际编写自己的 CNI 插件需要什么。首先,我们将看看可能的最简单的插件——环回插件。然后,我们将检查实现大部分样板与编写 CNI 插件相关的插件框架。最后,我们将回顾桥接插件的实现。在我们深入之前,这里是一个快速提醒 CNI 插件是什么:

  • CNI 插件是可执行的

  • 它负责将新容器连接到网络,为 CNI 容器分配唯一的 IP 地址,并负责路由

  • 容器是一个网络命名空间(在 Kubernetes 中,一个 pod 是一个 CNI 容器)

  • 网络定义以 JSON 文件的形式进行管理,但通过标准输入流传输到插件(插件不会读取任何文件)

  • 辅助信息可以通过环境变量提供

首先看看环回插件

环回插件只是添加环回接口。它非常简单,不需要任何网络配置信息。大多数 CNI 插件都是用 Golang 实现的,环回 CNI 插件也不例外。完整的源代码可在以下链接找到:

github.com/containernetworking/plugins/blob/master/plugins/main/loopback

让我们先看一下导入。来自 GitHub 上的容器网络项目的多个软件包提供了实现 CNI 插件和netlink软件包所需的许多构建块,用于添加和删除接口,以及设置 IP 地址和路由。我们很快将看到skel软件包:

package main
import (
  "github.com/containernetworking/cni/pkg/ns"
  "github.com/containernetworking/cni/pkg/skel"
  "github.com/containernetworking/cni/pkg/types/current"
  "github.com/containernetworking/cni/pkg/version"
  "github.com/vishvananda/netlink"
)

然后,插件实现了两个命令,cmdAddcmdDel,当container被添加到或从网络中移除时调用。以下是cmdAdd命令:

func cmdAdd(args *skel.CmdArgs) error { 
  args.IfName = "lo" 
  err := ns.WithNetNSPath(args.Netns, func(_ ns.NetNS) error { 
    link, err := netlink.LinkByName(args.IfName) 
    if err != nil { 
      return err // not tested 
    } 

    err = netlink.LinkSetUp(link) 
    if err != nil { 
      return err // not tested 
    } 

    return nil 
  }) 
  if err != nil { 
    return err // not tested 
  } 

  result := current.Result{} 
  return result.Print() 
} 

该功能的核心是将接口名称设置为lo(用于环回),并将链接添加到容器的网络命名空间中。del命令则相反:

func cmdDel(args *skel.CmdArgs) error { 
  args.IfName = "lo" 
  err := ns.WithNetNSPath(args.Netns, func(ns.NetNS) error { 
    link, err := netlink.LinkByName(args.IfName) 
    if err != nil { 
      return err // not tested 
    } 

    err = netlink.LinkSetDown(link) 
    if err != nil { 
      return err // not tested 
    } 

    return nil 
  }) 
  if err != nil { 
    return err // not tested 
  } 

  result := current.Result{} 
  return result.Print() 

} 

main函数只是简单地调用skel包,传递命令函数。skel包将负责运行 CNI 插件可执行文件,并在适当的时候调用addCmddelCmd函数:

func main() { 
  skel.PluginMain(cmdAdd, cmdDel, version.All) 
} 

构建 CNI 插件骨架

让我们探索skel包,并了解其在内部的工作原理。从PluginMain()入口点开始,它负责调用PluginMainWithError(),捕获错误,将其打印到标准输出并退出:

func PluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error, versionInfo version.PluginInfo) { 
  if e := PluginMainWithError(cmdAdd, cmdDel, versionInfo); e != nil { 
    if err := e.Print(); err != nil { 
      log.Print("Error writing error JSON to stdout: ", err) 
    } 
    os.Exit(1) 
  } 
} 

PluginErrorWithMain()实例化一个分发器,设置它与所有 I/O 流和环境,并调用其PluginMain()方法:

func PluginMainWithError(cmdAdd, cmdDel func(_ *CmdArgs) error, versionInfo version.PluginInfo) *types.Error { 
  return ( dispatcher{ 
    Getenv: os.Getenv, 
    Stdin:  os.Stdin, 
    Stdout: os.Stdout, 
    Stderr: os.Stderr, 
  }).pluginMain(cmdAdd, cmdDel, versionInfo) 
} 

最后,这是骨架的主要逻辑。它从环境中获取cmd参数(其中包括来自标准输入的配置),检测调用了哪个cmd,并调用适当的plugin函数(cmdAddcmdDel)。它还可以返回版本信息:

func (t *dispatcher) pluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error, versionInfo version.PluginInfo) *types.Error { 
  cmd, cmdArgs, err := t.getCmdArgsFromEnv() 
  if err != nil { 
    return createTypedError(err.Error()) 
  } 

  switch cmd { 
  case "ADD": 
    err = t.checkVersionAndCall(cmdArgs, versionInfo, cmdAdd) 
  case "DEL": 
    err = t.checkVersionAndCall(cmdArgs, versionInfo, cmdDel) 
  case "VERSION": 
    err = versionInfo.Encode(t.Stdout) 
  default: 
    return createTypedError("unknown CNI_COMMAND: %v", cmd) 
  } 

  if err != nil { 
    if e, ok := err.(*types.Error); ok { 
      // don't wrap Error in Error 
      return e 
    } 
    return createTypedError(err.Error()) 
  } 
  return nil 
} 

审查桥接插件

桥接插件更为重要。让我们看一下其实现的一些关键部分。完整的源代码可在以下链接找到:

github.com/containernetworking/plugins/blob/master/plugins/main/bridge

它定义了一个网络配置struct,具有以下字段:

type NetConf struct { 
  types.NetConf 
  BrName                string `json:"bridge"` 
  IsGW                     bool   `json:"isGateway"` 
  IsDefaultGW         bool   `json:"isDefaultGateway"` 
  ForceAddress      bool   `json:"forceAddress"` 
  IPMasq                  bool   `json:"ipMasq"` 
  MTU                       int    `json:"mtu"` 
  HairpinMode         bool   `json:"hairpinMode"` 
  PromiscMode       bool   `json:"promiscMode"` 
} 

由于空间限制,我们将不会涵盖每个参数的作用以及它如何与其他参数交互。目标是理解流程,并且如果您想要实现自己的 CNI 插件,这将是一个起点。配置通过loadNetConf()函数从 JSON 加载。它在cmdAdd()cmdDel()函数的开头被调用:

n, cniVersion, err := loadNetConf(args.StdinData) 

这是cmdAdd()函数的核心。它使用来自网络配置的信息,设置了一个veth,与 IPAM 插件交互以添加适当的 IP 地址,并返回结果:

hostInterface, containerInterface, err := setupVeth(netns, br, args.IfName, n.MTU,  
                                                                          n.HairpinMode) 
  if err != nil { 
    return err 
  } 

  // run the IPAM plugin and get back the config to apply 
  r, err := ipam.ExecAdd(n.IPAM.Type, args.StdinData) 
  if err != nil { 
    return err 
  } 

  // Convert the IPAM result was into the current Result type 
  result, err := current.NewResultFromResult(r) 
  if err != nil { 
    return err 
  } 

  if len(result.IPs) == 0 { 
    return errors.New("IPAM returned missing IP config") 
  } 

  result.Interfaces = []*current.Interface{brInterface, hostInterface, containerInterface} 

这只是完整实现的一部分。还有路由设置和硬件 IP 分配。我鼓励您追求完整的源代码,这是相当广泛的,以获得全貌。

总结

在本章中,我们涵盖了很多内容。网络是一个如此广泛的主题,有如此多的硬件、软件、操作环境和用户技能的组合,要想提出一个全面的网络解决方案,既稳健、安全、性能良好又易于维护,是一项非常复杂的工作。对于 Kubernetes 集群,云提供商大多解决了这些问题。但如果您在本地运行集群或需要定制解决方案,您有很多选择。Kubernetes 是一个非常灵活的平台,设计用于扩展。特别是网络是完全可插拔的。我们讨论的主要主题是 Kubernetes 网络模型(平面地址空间,其中 pod 可以访问其他 pod,并且在 pod 内部所有容器之间共享本地主机),查找和发现的工作原理,Kubernetes 网络插件,不同抽象级别的各种网络解决方案(许多有趣的变体),有效使用网络策略来控制集群内部的流量,负载均衡解决方案的范围,最后我们看了如何通过剖析真实实现来编写 CNI 插件。

在这一点上,您可能会感到不知所措,特别是如果您不是专家。您应该对 Kubernetes 网络的内部有很好的理解,了解实现完整解决方案所需的所有相互关联的部分,并能够根据对系统有意义的权衡来制定自己的解决方案。

在第十一章中,在多个云和集群联合上运行 Kubernetes,我们将更进一步,看看如何在多个集群、云提供商和联合上运行 Kubernetes。这是 Kubernetes 故事中的一个重要部分,用于地理分布式部署和最终可扩展性。联合的 Kubernetes 集群可以超越本地限制,但它们也带来了一系列挑战。

第十一章:在多个云上运行 Kubernetes 和集群联邦

在本章中,我们将进一步探讨在多个云上运行 Kubernetes 和集群联邦。Kubernetes 集群是一个紧密结合的单元,其中所有组件都在相对接近的地方运行,并通过快速网络(物理数据中心或云提供商可用区)连接。这对许多用例来说非常好,但有一些重要的用例需要系统扩展到超出单个集群的范围。Kubernetes 联邦是一种系统化的方法,可以将多个 Kubernetes 集群组合在一起,并将它们视为单个实体进行交互。我们将涵盖的主题包括以下内容:

  • 深入了解集群联邦的全部内容

  • 如何准备、配置和管理集群联邦

  • 如何在多个集群上运行联合工作负载

了解集群联邦

集群联邦在概念上很简单。您可以聚合多个 Kubernetes 集群,并将它们视为单个逻辑集群。有一个联邦控制平面,向客户端呈现系统的单一统一视图。

以下图表展示了 Kubernetes 集群联邦的整体情况:

联邦控制平面由联邦 API 服务器和联邦控制器管理器共同协作。联邦 API 服务器将请求转发到联邦中的所有集群。此外,联邦控制器管理器通过将请求路由到各个联邦集群成员的更改来执行控制器管理器的职责。实际上,集群联邦并不是微不足道的,也不能完全抽象化。跨 Pod 通信和数据传输可能会突然产生大量的延迟和成本开销。让我们首先看一下集群联邦的用例,了解联合组件和资源的工作方式,然后再来研究难点:位置亲和性、跨集群调度和联邦数据访问。

集群联邦的重要用例

有四类用例受益于集群联邦。

容量溢出

公共云平台,如 AWS、GCE 和 Azure,非常好,并提供许多好处,但它们并不便宜。许多大型组织在自己的数据中心投入了大量资金。其他组织与私人服务提供商合作,如 OVS、Rackspace 或 Digital Ocean。如果您有能力自行管理和操作基础设施,那么在自己的基础设施上运行 Kubernetes 集群比在云中运行更经济。但是,如果您的一些工作负载波动并且在相对短的时间内需要更多的容量呢?

例如,您的系统可能在周末或节假日受到特别严重的打击。传统方法是只是提供额外的容量。但在许多动态情况下,这并不容易。通过容量溢出,您可以在本地数据中心或私人服务提供商上运行 Kubernetes 集群中运行大部分工作,并在其中一个大型平台提供商上运行基于云的 Kubernetes 集群。大部分时间,基于云的集群将被关闭(停止实例),但在需要时,您可以通过启动一些停止的实例来弹性地为系统增加容量。Kubernetes 集群联合可以使这种配置相对简单。它消除了许多关于容量规划和支付大部分时间未使用的硬件的头疼。

这种方法有时被称为云爆发

敏感工作负载

这几乎是容量溢出的相反情况。也许您已经接受了云原生的生活方式,整个系统都在云上运行,但是一些数据或工作负载涉及敏感信息。监管合规性或您组织的安全政策可能要求数据和工作负载必须在完全由您控制的环境中运行。您的敏感数据和工作负载可能会受到外部审计。确保私有 Kubernetes 集群中的信息永远不会泄漏到基于云的 Kubernetes 集群可能至关重要。但是,希望能够查看公共集群并能够从私有集群启动非敏感工作负载可能是可取的。如果工作负载的性质可以动态地从非敏感变为敏感,那么就需要通过制定适当的策略和实施来解决。例如,您可以阻止工作负载改变其性质。或者,您可以迁移突然变得敏感的工作负载,并确保它不再在基于云的集群上运行。另一个重要的例子是国家合规性,根据法律要求,某些数据必须保留在指定的地理区域(通常是一个国家)内,并且只能从该地区访问。在这种情况下,必须在该地理区域创建一个集群。

避免供应商锁定

大型组织通常更喜欢有选择,并不希望被绑定在单一供应商上。风险往往太大,因为供应商可能会关闭或无法提供相同级别的服务。拥有多个供应商通常也有利于谈判价格。Kubernetes 旨在成为供应商无关的。您可以在不同的云平台、私有服务提供商和本地数据中心上运行它。

然而,这并不是微不足道的。如果您想确保能够快速切换供应商或将一些工作负载从一个供应商转移到另一个供应商,您应该已经在多个供应商上运行系统。您可以自己操作,或者有一些公司提供在多个供应商上透明运行 Kubernetes 的服务。由于不同的供应商运行不同的数据中心,您自动获得了一些冗余和对供应商范围内的故障的保护。

地理分布的高可用性

高可用性意味着即使系统的某些部分出现故障,服务仍将对用户保持可用。在联邦 Kubernetes 集群的背景下,故障的范围是整个集群,这通常是由于托管集群的物理数据中心出现问题,或者可能是平台提供商出现更广泛的问题。高可用性的关键是冗余。地理分布式冗余意味着在不同位置运行多个集群。这可能是同一云提供商的不同可用区,同一云提供商的不同地区,甚至完全不同的云提供商(参见“避免供应商锁定”部分)。在运行具有冗余的集群联邦时,有许多问题需要解决。我们稍后将讨论其中一些问题。假设技术和组织问题已经解决,高可用性将允许将流量从失败的集群切换到另一个集群。这对用户来说应该是透明的(切换期间的延迟,以及一些正在进行的请求或任务可能会消失或失败)。系统管理员可能需要采取额外步骤来支持切换和处理原始集群的故障。

联邦控制平面

联邦控制平面由两个组件组成,共同使得 Kubernetes 集群的联邦可以看作和作为一个统一的 Kubernetes 集群。

联邦 API 服务器

联邦 API 服务器正在管理组成联邦的 Kubernetes 集群。它在etcd数据库中管理联邦状态(即哪些集群是联邦的一部分),与常规 Kubernetes 集群一样,但它保持的状态只是哪些集群是联邦的成员。每个集群的状态存储在该集群的etcd数据库中。联邦 API 服务器的主要目的是与联邦控制器管理器进行交互,并将请求路由到联邦成员集群。联邦成员不需要知道它们是联邦的一部分:它们的工作方式完全相同。

以下图表展示了联邦 API 服务器、联邦复制控制器和联邦中的 Kubernetes 集群之间的关系:

联邦控制器管理器

联邦控制器管理器确保联邦的期望状态与实际状态匹配。它将任何必要的更改转发到相关的集群或集群。联邦控制器管理器二进制文件包含多个控制器,用于本章后面将介绍的所有不同的联邦资源。尽管控制逻辑相似:它观察变化并在集群状态偏离时将集群状态带到期望状态。这是针对集群联邦中的每个成员进行的。

以下图表展示了这个永久控制循环:

联邦资源

Kubernetes 联邦仍在不断发展中。截至 Kubernetes 1.10,只有一些标准资源可以进行联邦。我们将在这里介绍它们。要创建联邦资源,您可以使用 Kubectl 的--context=federation-cluster命令行参数。当您使用--context=federation-cluster时,该命令将发送到联邦 API 服务器,该服务器负责将其发送到所有成员集群。

联邦 ConfigMap

联邦 ConfigMaps 非常有用,因为它们帮助集中配置可能分布在多个集群中的应用程序。

创建联邦 ConfigMap

以下是创建联邦 ConfigMap 的示例:

> kubectl --context=federation-cluster create -f configmap.yaml  

正如您所看到的,创建单个 Kubernetes 集群中的 ConfigMap 时唯一的区别是上下文。创建联邦 ConfigMap 时,它存储在控制平面的etcd数据库中,但每个成员集群中也存储了一份副本。这样,每个集群可以独立运行,不需要访问控制平面。

查看联邦 ConfigMap

您可以通过访问控制平面或访问成员集群来查看 ConfigMap。要访问成员集群中的 ConfigMap,请在上下文中指定联邦集群成员名称:

> kubectl --context=cluster-1 get configmap configmap.yaml  

更新联邦 ConfigMap

重要的是要注意,通过控制平面创建时,ConfigMap 将在所有成员集群中都是相同的。然而,由于它除了在控制平面集群中存储外,还在每个集群中单独存储,因此没有单一的“真实”来源。可以(尽管不建议)稍后独立修改每个成员集群的 ConfigMap。这会导致联邦中的配置不一致。联邦中不同集群的不同配置有有效的用例,但在这些情况下,我建议直接配置每个集群。当你创建一个联邦 ConfigMap 时,你是在表明整个集群应该共享这个配置。然而,通常情况下,你会希望通过指定 --context=federation-cluster 来更新联邦集群中的所有 ConfigMap。

删除联邦 ConfigMap

没错,你猜对了。你像往常一样删除,但指定上下文:

> kubectl --context=federation-cluster delete configmap      

只有一个小小的变化。从 Kubernetes 1.10 开始,当你删除一个联邦 ConfigMap 时,每个集群中自动创建的单独的 ConfigMap 仍然存在。你必须在每个集群中分别删除它们。也就是说,如果你的联邦中有三个集群分别叫做 cluster-1cluster-2cluster-3,你将不得不运行这额外的三个命令来摆脱联邦中的 ConfigMap:

> kubectl --context=cluster-1 delete configmap
> kubectl --context=cluster-2 delete configmap
> kubectl --context=cluster-3 delete configmap 

这将在将来得到纠正。

联邦守护进程

联邦守护进程基本上与常规的 Kubernetes 守护进程相同。你通过控制平面创建它并与之交互(通过指定 --context=federation-cluster),控制平面将其传播到所有成员集群。最终,你可以确保你的守护程序在联邦的每个集群的每个节点上运行。

联邦部署

联邦部署更加智能。当您创建一个具有 X 个副本的联邦部署,并且您有N个集群时,默认情况下副本将在集群之间均匀分布。如果您有 3 个集群,并且联邦部署有 15 个 pod,那么每个集群将运行 5 个副本。与其他联邦资源一样,控制平面将存储具有 15 个副本的联邦部署,然后创建 3 个部署(每个集群一个),每个部署都有 5 个副本。您可以通过添加注释federation.kubernetes.io/deployment-preferences来控制每个集群的副本数量。截至 Kubernetes 1.10,联邦部署仍处于 Alpha 阶段。在将来,该注释将成为联邦部署配置中的一个正确字段。

联邦事件

联邦事件与其他联邦资源不同。它们仅存储在控制平面中,不会传播到底层 Kubernetes 成员集群。

您可以像往常一样使用--context=federation-cluster查询联邦事件:

> kubectl --context=federation-cluster get events  

联邦水平 Pod 扩展

最近在 Kubernetes 1.9 中作为 Alpha 功能添加了联邦水平 Pod 扩展HPA)。为了使用它,您必须在启动 API 服务器时提供以下标志:

--runtime-config=api/all=true  

这是一个重要的功能,因为集群联合的主要动机之一是在没有手动干预的情况下在多个集群之间流畅地转移工作负载。联邦 HPA 利用了集群内的 HPA 控制器。联邦 HPA 根据请求的最大和最小副本数量在成员集群之间均匀分配负载。在将来,用户将能够指定更高级的 HPA 策略。

例如,考虑一个具有 4 个集群的联邦;我们希望始终至少有 6 个 pod 和最多有 16 个 pod 在运行。以下清单将完成工作:

apiVersion: autoscaling/v1 
kind: HorizontalPodAutoscaler 
metadata: 
  name: cool-app 
  namespace: default 
spec: 
  scaleTargetRef: 
    apiVersion: apps/v1beta1 
    kind: Deployment 
    name: cool-app 
  minReplicas: 6 
  maxReplicas: 16 
  targetCPUUtilizationPercentage: 80 

使用以下命令启动联邦 HPA:

> kubectl --context=federation-cluster create federated-hpa.yaml  

现在会发生什么?联邦控制平面将在 4 个集群中的每个集群中创建标准 HPA,最多有 4 个副本和最少有 2 个副本。原因是这是最经济地满足联邦要求的设置。让我们了解一下为什么。如果每个集群最多有 4 个副本,那么我们最多会有 4 x 4 = 16 个副本,这符合我们的要求。至少 2 个副本的保证意味着我们至少会有 4 x 2 = 8 个副本。这满足了我们至少会有 6 个副本的要求。请注意,即使系统上没有负载,我们也将始终至少有 8 个副本,尽管我们指定 6 个也可以。鉴于跨集群的均匀分布的限制,没有其他办法。如果集群 HPA 的minReplicas=1,那么集群中的总副本数可能是 4 x 1 = 4,这少于所需的联邦最小值 6。未来,用户可能可以指定更复杂的分布方案。

可以使用集群选择器(在 Kubernetes 1.7 中引入)来将联邦对象限制为成员的子集。因此,如果我们想要至少 6 个最多 15 个,可以将其均匀分布在 3 个集群中,而不是 4 个,每个集群将至少有 2 个最多 5 个。

联邦入口

联邦入口不仅在每个集群中创建匹配的入口对象。联邦入口的主要特点之一是,如果整个集群崩溃,它可以将流量引导到其他集群。从 Kubernetes 1.4 开始,联邦入口在 Google Cloud Platform 上得到支持,包括 GKE 和 GCE。未来,联邦入口将增加对混合云的支持。

联邦入口执行以下任务:

  • 在联邦的每个集群成员中创建 Kubernetes 入口对象

  • 为所有集群入口对象提供一个一站式逻辑 L7 负载均衡器,具有单个 IP 地址

  • 监视每个集群中入口对象后面的服务后端 pod 的健康和容量

  • 确保在各种故障情况下将客户端连接路由到健康的服务端点,例如 pod、集群、可用区或整个区域的故障,只要联邦中有一个健康的集群

创建联邦入口

通过寻址联邦控制平面来创建联邦入口

> kubectl --context=federation-cluster create -f ingress.yaml  

联合控制平面将在每个集群中创建相应的入口。所有集群将共享相同的命名空间和ingress对象的名称:

> kubectl --context=cluster-1 get ingress myingress
NAME        HOSTS     ADDRESS           PORTS     AGE
ingress      *         157.231.15.33    80, 443   1m  

使用联合入口进行请求路由

联合入口控制器将请求路由到最近的集群。入口对象通过Status.Loadbalancer.Ingress字段公开一个或多个 IP 地址,这些 IP 地址在入口对象的生命周期内保持不变。当内部或外部客户端连接到特定集群入口对象的 IP 地址时,它将被路由到该集群中的一个 pod。然而,当客户端连接到联合入口对象的 IP 地址时,它将自动通过最短的网络路径路由到请求源最近的集群中的一个健康 pod。因此,例如,来自欧洲互联网用户的 HTTP(S)请求将直接路由到具有可用容量的欧洲最近的集群。如果欧洲没有这样的集群,请求将被路由到下一个最近的集群(通常在美国)。

使用联合入口处理故障

有两种广义的失败类别:

  • Pod 故障

  • 集群故障

Pod 可能因多种原因而失败。在正确配置的 Kubernetes 集群(无论是集群联合成员还是不是),pod 将由服务和 ReplicaSets 管理,可以自动处理 pod 故障。这不应影响联合入口进行的跨集群路由和负载均衡。整个集群可能由于数据中心或全球连接的问题而失败。在这种情况下,联合服务和联合 ReplicaSets 将确保联合中的其他集群运行足够的 pod 来处理工作负载,并且联合入口将负责将客户端请求从失败的集群中路由出去。为了从这种自动修复功能中受益,客户端必须始终连接到联合入口对象,而不是单个集群成员。

联合作业

联合作业与集群内作业类似。联合控制平面在基础集群中创建作业,并根据任务的并行性均匀分配负载,并跟踪完成情况。例如,如果联合有 4 个集群,并且您创建了一个并行性为 8 和完成数为 24 的联合作业规范,那么将在每个集群中创建一个并行性为 2 和完成数为 6 的作业。

联合命名空间

Kubernetes 命名空间在集群内用于隔离独立区域并支持多租户部署。联合命名空间在整个集群联合中提供相同的功能。API 是相同的。当客户端访问联合控制平面时,他们只能访问他们请求的命名空间,并且被授权访问联合中所有集群的命名空间。

您可以使用相同的命令并添加--context=federation-cluster

> kubectl --context=federation-cluster create -f namespace.yaml
> kubectl --context=cluster-1 get namespaces namespace
> kubectl --context=federation-cluster create -f namespace.yaml  

联合复制 ReplicaSet

最好使用部署和联合部署来管理集群或联合中的副本。但是,如果出于某种原因您更喜欢直接使用 ReplicaSets 进行工作,那么 Kubernetes 支持联合ReplicaSet。没有联合复制控制器,因为 ReplicaSets 超越了复制控制器。

当您创建联合 ReplicaSets 时,控制平面的工作是确保整个集群中的副本数量与您的联合 ReplicaSets 配置相匹配。控制平面将在每个联合成员中创建一个常规 ReplicaSet。每个集群将默认获得相等(或尽可能接近相等)数量的副本,以便总数将达到指定的副本数量。

您可以使用以下注释来控制每个集群的副本数量:federation.kubernetes.io/replica-set-preferences

相应的数据结构如下:

type FederatedReplicaSetPreferences struct { 
  Rebalance bool 
  Clusters map[string]ClusterReplicaSetPreferences 
} 

如果Rebalancetrue,则正在运行的副本可能会根据需要在集群之间移动。集群映射确定每个集群的 ReplicaSets 偏好。如果将*指定为键,则所有未指定的集群将使用该偏好集。如果没有*条目,则副本将仅在映射中显示的集群上运行。属于联合但没有条目的集群将不会安排 pod(对于该 pod 模板)。

每个集群的单独 ReplicaSets 偏好使用以下数据结构指定:

type ClusterReplicaSetPreferences struct { 
  MinReplicas int64 
  MaxReplicas *int64 
  Weight int64 
} 

MinReplicas默认为0MaxReplicas默认情况下是无限制的。权重表示向这个 ReplicaSets 添加额外副本的偏好,默认为0

联合秘密

联合秘密很简单。当您通过控制平面像往常一样创建联合秘密时,它会传播到整个集群。就是这样。

困难的部分

到目前为止,联邦似乎几乎是直截了当的。将一堆集群组合在一起,通过控制平面访问它们,一切都会被复制到所有集群。但是有一些困难因素和基本概念使这种简化的观点变得复杂。Kubernetes 的许多功能来自于其在幕后执行大量工作的能力。在一个完全部署在单个物理数据中心或可用性区域的单个集群中,所有组件都连接到快速网络,Kubernetes 本身非常有效。在 Kubernetes 集群联邦中,情况就不同了。延迟、数据传输成本以及在集群之间移动 Pods 都有不同的权衡。根据用例,使联邦工作可能需要系统设计师和运营商额外的注意、规划和维护。此外,一些联合资源不如其本地对应物成熟,这增加了更多的不确定性。

联邦工作单元

Kubernetes 集群中的工作单元是 Pod。在 Kubernetes 中无法打破 Pod。整个 Pod 将始终一起部署,并受到相同的生命周期处理。Pod 是否应该保持集群联邦的工作单元?也许将更大的单元(如整个 ReplicaSet、部署或服务)与特定集群关联起来会更有意义。如果集群失败,整个 ReplicaSet、部署或服务将被调度到另一个集群。那么一组紧密耦合的 ReplicaSets 呢?这些问题的答案并不总是容易的,甚至可能随着系统的演变而动态改变。

位置亲和性

位置亲和力是一个主要关注点。Pods 何时可以分布在集群之间?这些 Pods 之间的关系是什么?是否有亲和力要求,比如 Pods 之间或 Pods 与其他资源(如存储)之间?有几个主要类别:

  • 严格耦合

  • 松散耦合

  • 优先耦合

  • 严格解耦

  • 均匀分布

在设计系统以及如何在联邦中分配和调度服务和 Pods 时,确保始终尊重位置亲和性要求非常重要。

严格耦合

严格耦合的要求适用于必须在同一集群中的应用程序。如果对 pod 进行分区,应用程序将失败(可能是由于实时要求无法在集群间进行网络传输),或者成本可能太高(pod 可能正在访问大量本地数据)。将这种紧密耦合的应用程序移动到另一个集群的唯一方法是在另一个集群上启动完整的副本(包括数据),然后关闭当前集群上的应用程序。如果数据量太大,该应用程序可能实际上无法移动,并对灾难性故障敏感。这是最难处理的情况,如果可能的话,您应该设计系统以避免严格耦合的要求。

松耦合

松耦合的应用程序在工作负载尴尬地并行时表现最佳,每个 pod 不需要了解其他 pod 或访问大量数据。在这些情况下,pod 可以根据联邦中的容量和资源利用率安排到集群中。必要时,pod 可以在不出问题的情况下从一个集群移动到另一个集群。例如,一个无状态的验证服务执行一些计算,并在请求本身中获取所有输入,不查询或写入任何联邦范围的数据。它只验证其输入并向调用者返回有效/无效的判断。

优先耦合

在所有 pod 都在同一集群中或 pod 和数据共同位于同一位置时,优先耦合的应用程序表现更好,但这不是硬性要求。例如,它可以与仅需要最终一致性的应用程序一起工作,其中一些联邦范围的应用程序定期在所有集群之间同步应用程序状态。在这些情况下,分配是明确地针对一个集群进行的,但在压力下留下了一个安全舱口,可以在其他集群中运行或迁移。

严格解耦

一些服务具有故障隔离或高可用性要求,这要求在集群之间进行分区。如果所有副本最终可能被安排到同一集群中,那么运行关键服务的三个副本就没有意义,因为该集群只成为一个临时的单点故障(SPOF)。

均匀分布

均匀分布是指服务、ReplicaSet 或 pod 的实例必须在每个集群上运行。这类似于 DaemonSet,但不是确保每个节点上有一个实例,而是每个集群一个实例。一个很好的例子是由一些外部持久存储支持的 Redis 缓存。每个集群中的 pod 应该有自己的集群本地 Redis 缓存,以避免访问可能更慢或成为瓶颈的中央存储。另一方面,每个集群不需要超过一个 Redis 服务(它可以分布在同一集群中的几个 pod 中)。

跨集群调度

跨集群调度与位置亲和力相辅相成。当创建新的 pod 或现有的 pod 失败并且需要安排替代时,它应该去哪里?当前的集群联邦不能处理我们之前提到的所有场景和位置亲和力的选项。在这一点上,集群联邦很好地处理了松散耦合(包括加权分布)和严格耦合(通过确保副本的数量与集群的数量相匹配)的类别。其他任何情况都需要您不使用集群联邦。您将不得不添加自己的自定义联邦层,以考虑更多专门的问题,并且可以适应更复杂的调度用例。

联邦数据访问

这是一个棘手的问题。如果您有大量数据和在多个集群中运行的 pod(可能在不同的大陆上),并且需要快速访问它,那么您有几个不愉快的选择:

  • 将数据复制到每个集群(复制速度慢,传输昂贵,存储昂贵,同步和处理错误复杂)

  • 远程访问数据(访问速度慢,每次访问昂贵,可能成为单点故障)

  • 制定一个复杂的混合解决方案,对一些最热门的数据进行每个集群缓存(复杂/陈旧的数据,仍然需要传输大量数据)

联邦自动扩展

目前不支持联邦自动调用。可以利用两个维度的扩展,以及组合:

  • 每个集群的扩展

  • 将集群添加/移除联邦

  • 混合方法

考虑一个相对简单的场景,即在三个集群上运行一个松散耦合的应用程序,每个集群有五个 pod。在某个时候,15 个 pod 无法再处理负载。我们需要增加更多的容量。我们可以增加每个集群中的 pod 数量,但如果我们在联邦级别这样做,那么每个集群将有六个 pod 在运行。我们通过三个 pod 增加了联邦的容量,而只需要一个 pod。当然,如果您有更多的集群,问题会变得更糟。另一个选择是选择一个集群并只改变其容量。这是可能的,但现在我们明确地在整个联邦中管理容量。如果我们有许多集群运行数百个具有动态变化需求的服务,情况会很快变得复杂。

添加一个全新的集群更加复杂。我们应该在哪里添加新的集群?没有额外的可用性要求可以指导决策。这只是额外的容量问题。创建一个新的集群通常需要复杂的首次设置,并且可能需要几天来批准公共云平台上的各种配额。混合方法增加了联邦中现有集群的容量,直到达到某个阈值,然后开始添加新的集群。这种方法的好处是,当您接近每个集群的容量限制时,您开始准备新的集群,以便在必要时立即启动。除此之外,它需要大量的工作,并且您需要为灵活性和可伸缩性付出增加的复杂性。

管理 Kubernetes 集群联邦

管理 Kubernetes 集群联邦涉及许多超出管理单个集群的活动。有两种设置联邦的方式。然后,您需要考虑级联资源删除,跨集群负载平衡,跨集群故障转移,联邦服务发现和联邦发现。让我们详细讨论每一种。

从头开始设置集群联邦

注意:这种方法现在已经不推荐使用Kubefed。我在这里描述它是为了让使用较旧版本 Kubernetes 的读者受益。

建立 Kubernetes 集群联邦,我们需要运行控制平面的组件,如下所示:

etcd 
federation-apiserver 
federation-controller-manager 

其中一个最简单的方法是使用全能的 hyperkube 镜像:

github.com/kubernetes/kubernetes/tree/master/cluster/images/hyperkube

联邦 API 服务器和联邦控制器管理器可以作为现有 Kubernetes 集群中的 pod 运行,但正如前面讨论的那样,最好从容错和高可用性的角度来看,将它们运行在自己的集群中。

初始设置

首先,您必须运行 Docker,并获取包含我们在本指南中将使用的脚本的 Kubernetes 版本。当前版本是 1.5.3。您也可以下载最新可用版本:

> curl -L https://github.com/kubernetes/kubernetes/releases/download/v1.5.3/kubernetes.tar.gz | tar xvzf -
> cd kubernetes  

我们需要为联邦配置文件创建一个目录,并将FEDERATION_OUTPUT_ROOT环境变量设置为该目录。为了方便清理,最好创建一个新目录:

> export FEDERATION_OUTPUT_ROOT="${PWD}/output/federation"
> mkdir -p "${FEDERATION_OUTPUT_ROOT}"  

现在,我们可以初始化联邦:

> federation/deploy/deploy.sh init 

使用官方的 Hyperkube 镜像

作为每个 Kubernetes 版本的一部分,官方发布的镜像都被推送到gcr.io/google_containers。要使用该存储库中的镜像,您可以将配置文件中的容器镜像字段设置为${FEDERATION_OUTPUT_ROOT}指向gcr.io/google_containers/hyperkube镜像,其中包括federation-apiserverfederation-controller-manager二进制文件。

运行联邦控制平面

我们准备通过运行以下命令部署联邦控制平面:

> federation/deploy/deploy.sh deploy_federation  

该命令将启动控制平面组件作为 pod,并为联邦 API 服务器创建一个LoadBalancer类型的服务,并为etcd创建一个由动态持久卷支持的持久卷索赔。

要验证联邦命名空间中的所有内容是否正确创建,请输入以下内容:

> kubectl get deployments --namespace=federation  

你应该看到这个:

NAME                        DESIRED CURRENT UP-TO-DATE      
federation-controller-manager   1         1         1 federation-apiserver 1         1         1 

您还可以使用 Kubectl config view 检查kubeconfig文件中的新条目。请注意,动态配置目前仅适用于 AWS 和 GCE。

向联邦注册 Kubernetes 集群

要向联邦注册集群,我们需要一个与集群通信的秘钥。

让我们在主机 Kubernetes 集群中创建秘钥。假设目标集群的kubeconfig位于|cluster-1|kubeconfig。您可以运行以下命令

创建secret

> kubectl create secret generic cluster-1 --namespace=federation 
--from-file=/cluster-1/kubeconfig  

集群的配置看起来和这个一样:

apiVersion: federation/v1beta1 
kind: Cluster 
metadata: 
  name: cluster1 
spec: 
  serverAddressByClientCIDRs: 
  - clientCIDR: <client-cidr> 
    serverAddress: <apiserver-address> 
  secretRef: 
    name: <secret-name> 

我们需要设置<client-cidr><apiserver-address><secret-name>。这里的<secret-name>是您刚刚创建的秘密的名称。serverAddressByClientCIDRs包含客户端可以根据其 CIDR 使用的各种服务器地址。我们可以使用CIDR 0.0.0.0/0设置服务器的公共 IP 地址,所有客户端都将匹配。此外,如果要内部客户端使用服务器的clusterIP,可以将其设置为serverAddress。在这种情况下,客户端 CIDR 将是仅匹配在该集群中运行的 pod 的 IP 的 CIDR。

让我们注册集群:

> kubectl create -f /cluster-1/cluster.yaml --context=federation-cluster  

让我们看看集群是否已正确注册:

> kubectl get clusters --context=federation-cluster
NAME       STATUS    VERSION   AGE
cluster-1   Ready               1m 

更新 KubeDNS

集群已注册到联邦。现在是时候更新kube-dns,以便您的集群可以路由联邦服务请求。从 Kubernetes 1.5 或更高版本开始,通过kube-dns ConfigMap传递--federations标志来完成:

--federations=${FEDERATION_NAME}=${DNS_DOMAIN_NAME}    

ConfigMap的外观如下:

apiVersion: v1 
kind: ConfigMap 
metadata: 
  name: kube-dns 
  namespace: kube-system 
data: 
  federations: <federation-name>=<federation-domain-name> 

federation-namefederation-domain-name替换为正确的值。

关闭联邦

如果要关闭联邦,只需运行以下命令:

federation/deploy/deploy.sh destroy_federation 

使用 Kubefed 设置集群联合

Kubernetes 1.5 引入了一个名为Kubefed的新的 Alpha 命令行工具,帮助您管理联合集群。Kubefed的工作是使部署新的 Kubernetes 集群联合控制平面变得容易,并向现有联合控制平面添加或删除集群。自 Kubernetes 1.6 以来一直处于 beta 阶段。

获取 Kubefed

直到 Kubernetes 1.9,Kubefed 是 Kubernetes 客户端二进制文件的一部分。您将获得 Kubectl 和 Kubefed。以下是在 Linux 上下载和安装的说明:

curl -LO https://storage.googleapis.com/kubernetes-release/release/${RELEASE-VERSION}/kubernetes-client-linux-amd64.tar.gztar -xzvf kubernetes-client-linux-amd64.tar.gz
    sudo cp kubernetes/client/bin/kubefed /usr/local/bin
    sudo chmod +x /usr/local/bin/kubefed
    sudo cp kubernetes/client/bin/kubectl /usr/local/bin
    sudo chmod +x /usr/local/bin/kubectl

如果您使用不同的操作系统或想安装不同的版本,则需要进行必要的调整。自 Kubernetes 1.9 以来,Kubefed 已在专用联邦存储库中可用:

curl -LO https://storage.cloud.google.com/kubernetes-federation-release/release/${RELEASE-VERSION}/federation-client-linux-amd64.tar.gztar -xzvf federation-client-linux-amd64.tar.gz
    sudo cp federation/client/bin/kubefed /usr/local/binsudo chmod +x /usr/local/bin/kubefed

您可以按照此处的说明单独安装 Kubectl:

https://kubernetes.io/docs/tasks/tools/install-kubectl/

选择主机集群

联邦控制平面可以是其自己的专用集群,也可以与现有集群一起托管。您需要做出这个决定。主机集群托管组成联邦控制平面的组件。确保您在本地kubeconfig中具有与主机集群对应的kubeconfig条目。

要验证是否具有所需的kubeconfig条目,请键入以下内容:

> kubectl config get-contexts  

您应该看到类似于这样的东西:

CURRENT   NAME      CLUSTER   AUTHINFO  NAMESPACE
cluster-1 cluster-1  cluster-1  

在部署联邦控制平面时,将稍后提供上下文名称cluster-1

部署联邦控制平面

是时候开始使用 Kubefed 了。kubefed init命令需要三个参数:

  • 联邦名称

  • 主机集群上下文

  • 用于您的联邦服务的域名后缀

以下示例命令部署了一个带有联邦控制平面的

名称联邦;一个主机集群上下文,cluster-1;一个 coredns DNS 提供程序(google-clouddnsaes-route53也是有效的);和域后缀,kubernetes-ftw.com

> kubefed init federation --host-cluster-context=cluster-1 --dns-provider coredns --dns-zone-name="kubernetes-ftw.com"  

DNS 后缀应该是您管理的 DNS 域名。

kubefed init在主机集群中设置联邦控制平面,并在本地kubeconfig中为联邦 API 服务器添加条目。由于错误,Kubernetes 可能不会创建默认命名空间。在这种情况下,您将不得不自己执行。键入以下命令:

> kubectl create namespace default --context=federation  

不要忘记将当前上下文设置为联邦,以便 Kubectl 将目标设置为联邦控制平面:

> kubectl config use-context federation 

联邦服务发现

联邦服务发现与联邦负载平衡紧密耦合。一个实用的设置包括一个全局 L7 负载均衡器,将请求分发到联邦集群中的联邦入口对象。

这种方法的好处是控制权留在 Kubernetes 联邦,随着时间的推移,它将能够与更多的集群类型(目前只有 AWS 和 GCE)一起工作,并了解集群利用率和其他约束。

拥有专用的查找服务并让客户端直接连接到各个集群上的服务的替代方案会失去所有这些好处。

将集群添加到联邦

一旦控制平面成功部署,我们应该将一些 Kubernetes 集群添加到联邦中。Kubefed 为此目的提供了join命令。kubefed join命令需要以下参数:

  • 要添加的集群名称

  • 主机集群上下文

例如,要将名为cluster-2的新集群添加到联邦中,请键入

以下:

kubefed join cluster-2 --host-cluster-context=cluster-1 

命名规则和自定义

您提供给kubefed join的集群名称必须是有效的 RFC 1035 标签。RFC 1035 只允许字母、数字和连字符,并且标签必须以字母开头。

此外,联邦控制平面需要加入集群的凭据才能对其进行操作。这些凭据是从本地的kubeconfig中获取的。Kubefed join命令使用指定为参数的集群名称来查找本地kubeconfig中的集群上下文。如果它找不到匹配的上下文,它将以错误退出。

这可能会导致问题,因为联邦中每个集群的上下文名称不遵循 RFC 1035 标签命名规则。在这种情况下,您可以指定符合 RFC 1035 标签命名规则的集群名称,并使用--cluster-context标志指定集群上下文。例如,如果您要加入的集群的上下文是cluster-3(不允许使用下划线),您可以通过运行此命令加入该集群:

kubefed join cluster-3 --host-cluster-context=cluster-1 --cluster-context=cluster-3  

秘密名称

联邦控制平面在上一节中描述的集群凭据作为主机集群中的一个秘密存储。秘密的名称也是从集群名称派生的。

但是,在 Kubernetes 中secret对象的名称应符合 RFC 1123 中描述的 DNS 子域名规范。如果不是这种情况,您可以使用--secret-name标志将secret name传递给kubefed join。例如,如果集群名称是cluster-4secret name4secret(不允许以字母开头),您可以通过运行此命令加入该集群:

kubefed join cluster-4 --host-cluster-context=cluster-1 --secret-name=4secret  

kubefed join命令会自动为您创建秘密。

从联邦中删除一个集群

要从联邦中删除一个集群,请使用集群名称和联邦主机集群上下文运行kubefed unjoin命令:

kubefed unjoin cluster-2 --host-cluster-context=cluster-1  

关闭联邦

在 Kubefed 的 beta 版本中,联邦控制平面的适当清理尚未完全实现。但是,暂时删除联邦系统命名空间应该会删除除联邦控制平面的etcd动态配置的持久存储卷之外的所有资源。您可以通过运行以下命令delete联邦命名空间来删除联邦命名空间:

> kubectl delete ns federation-system  

资源的级联删除

Kubernetes 集群联邦通常在控制平面中管理联合对象,以及每个成员 Kubernetes 集群中的相应对象。级联删除联合对象意味着成员 Kubernetes 集群中的相应对象也将被删除。

这不会自动发生。默认情况下,只删除联合控制平面对象。要激活级联删除,您需要设置以下选项:

DeleteOptions.orphanDependents=false 

在 Kuberentes 1.5 中,只有以下联合对象支持级联删除:

  • 部署

  • 守护进程集

  • 入口管理

  • 命名空间

  • 副本集

  • 秘密

对于其他对象,您必须进入每个集群并明确删除它们。幸运的是,从 Kubernetes 1.6 开始,所有联合对象都支持级联删除。

跨多个集群的负载均衡

跨集群的动态负载均衡并不是微不足道的。最简单的解决方案是说这不是 Kubernetes 的责任。负载均衡将在 Kubernetes 集群联合之外执行。但考虑到 Kubernetes 的动态特性,即使外部负载均衡器也必须收集关于每个集群上正在运行的服务和后端 pod 的大量信息。另一种解决方案是联合控制平面实现一个作为整个联合的流量导向器的 L7 负载均衡器。在较简单的用例中,每个服务在一个专用集群上运行,负载均衡器只是将所有流量路由到该集群。在集群故障的情况下,服务被迁移到另一个集群,负载均衡器现在将所有流量路由到新的集群。这提供了一个粗略的故障转移和集群级别的高可用性解决方案。

最佳解决方案将能够支持联合服务,并考虑其他因素,例如以下因素:

  • 客户端的地理位置

  • 每个集群的资源利用率

  • 资源配额和自动扩展

以下图表显示了 GCE 上的 L7 负载均衡器如何将客户端请求分发到最近的集群:

跨多个集群的故障转移

联合故障转移很棘手。假设联合中的一个集群失败;一个选择是让其他集群接管工作。现在的问题是,如何在其他集群之间分配负载?

  • 统一吗?

  • 启动一个新的集群?

  • 选择一个尽可能接近的现有集群(可能在同一地区)?

这些解决方案与联合负载平衡有微妙的相互作用,

地理分布的高可用性,跨不同集群的成本管理,

和安全。

现在,失败的集群再次上线。它应该逐渐重新接管其原始工作负载吗?如果它回来了,但容量减少或网络不稳定怎么办?有许多故障模式的组合可能使恢复变得复杂。

联邦迁移

联邦迁移与我们讨论过的几个主题相关,例如位置亲和性、联邦调度和高可用性。在其核心,联邦迁移意味着将整个应用程序或其部分从一个集群移动到另一个集群(更一般地从 M 个集群移动到 N 个集群)。联邦迁移可能是对各种事件的响应,例如以下事件:

  • 集群中的低容量事件(或集群故障)

  • 调度策略的更改(我们不再使用云提供商 X)

  • 资源定价的更改(云提供商 Y 降低了价格,所以让我们迁移到那里)

  • 联邦中添加或删除了一个新集群(让我们重新平衡应用程序的 Pods)

严格耦合的应用程序可以轻松地一次移动一个 Pod 或整个 Pod 到一个或多个集群(在适用的策略约束条件下,例如“仅限私有云”)。

对于优先耦合的应用程序,联邦系统必须首先找到一个具有足够容量来容纳整个应用程序的单个集群,然后预留该容量,并逐步将应用程序的一个(或多个)资源在一定的时间段内移动到新集群中(可能在预定义的维护窗口内)。

严格耦合的应用程序(除了被认为完全不可移动的应用程序)需要联邦系统执行以下操作:

  • 在目标集群中启动整个副本应用程序

  • 将持久数据复制到新的应用程序实例(可能在之前

启动 Pods)

  • 切换用户流量

  • 拆除原始应用程序实例

发现联邦服务

Kubernetes 提供 KubeDNS 作为内置核心组件。 KubeDNS 使用

cluster-local DNS 服务器以及命名约定来组成合格的

(按命名空间)DNS 名称约定。例如,the-service解析为默认namespace中的the-service服务,而the-service.the-namespace解析为the-namespace namespace中名为the-service的服务,该服务与默认的the-service不同。Pod 可以使用 KubeDNS 轻松找到和访问内部服务。Kubernetes 集群联邦将该机制扩展到多个集群。基本概念是相同的,但增加了另一级联邦。现在服务的 DNS 名称由<service name>.<namespace name>.<federation name>组成。这样,仍然可以使用原始的<service name>.<namepace name>命名约定来访问内部服务。但是,想要访问联邦服务的客户端使用联邦名称,最终将被转发到联邦成员集群中的一个来处理请求。

这种联邦限定的命名约定还有助于防止内部集群流量错误地到达其他集群。

使用前面的 NGINX 示例服务和刚刚描述的联邦服务 DNS 名称形式,让我们考虑一个例子:位于 cluster-1 可用区的集群中的一个 pod 需要访问 NGINX 服务。它现在可以使用服务的联邦 DNS 名称,即nginx.the-namespace.the-federation,这将自动扩展并解析为 NGINX 服务的最近健康的分片,无论在世界的哪个地方。如果本地集群中存在健康的分片,该服务的集群本地(通常为10.x.y.z)IP 地址将被返回(由集群本地的 KubeDNS)。这几乎等同于非联邦服务解析(几乎因为 KubeDNS 实际上为本地联邦服务返回了 CNAME 和 A 记录,但应用程序对这种微小的技术差异是无感的)。

然而,如果服务在本地集群中不存在(或者没有健康的后端 pod),DNS 查询会自动扩展。

运行联邦工作负载

联合工作负载是在多个 Kubernetes 集群上同时处理的工作负载。这对于松散耦合和分布式应用程序来说相对容易。然而,如果大部分处理可以并行进行,通常在最后会有一个连接点,或者至少需要查询和更新一个中央持久存储。如果同一服务的多个 pod 需要在集群之间合作,或者一组服务(每个服务可能都是联合的)必须共同工作并同步以完成某些任务,情况就会变得更加复杂。

Kubernetes 联合支持提供了联合工作负载的良好基础的联合服务。

联合服务的一些关键点是服务发现,跨集群

负载均衡和可用性区容错。

创建联合服务

联合服务在联合成员集群中创建相应的服务。

例如,要创建一个联合 NGINX 服务(假设您在nginx.yaml中有服务配置),请输入以下内容:

> kubectl --context=federation-cluster create -f nginx.yaml 

您可以验证每个集群中是否创建了一个服务(例如,在cluster-2中):

> kubectl --context=cluster-2 get services nginx
NAME      CLUSTER-IP     EXTERNAL-IP      PORT(S)   AGE
nginx     10.63.250.98   104.199.136.89   80/TCP    9m 

所有集群中创建的服务将共享相同的命名空间和服务名称,这是有道理的,因为它们是一个单一的逻辑服务。

您的联合服务的状态将自动反映基础 Kubernetes 服务的实时状态:

> kubectl --context=federation-cluster describe services nginx
Name:                   nginx
Namespace:              default
Labels:                 run=nginx
Selector:               run=nginx
Type:                   LoadBalancer
IP: 
LoadBalancer Ingress:   105.127.286.190, 122.251.157.43, 114.196.14.218, 114.199.176.99, ...
Port:                   http    80/TCP
Endpoints:              <none>
Session Affinity:       None
No events.  

添加后端 pod

截至 Kubernetes 1.10,我们仍然需要将后端 pod 添加到每个联合成员集群。这可以通过kubectl run命令完成。在将来的版本中,Kubernetes 联合 API 服务器将能够自动执行此操作。这将节省一步。请注意,当您使用kubectl run命令时,Kubernetes 会根据镜像名称自动向 pod 添加运行标签。在下面的示例中,该示例在五个 Kubernetes 集群上启动了一个 NGINX 后端 pod,镜像名称为nginx(忽略版本),因此将添加以下标签:

run=nginx 

这是因为服务使用该标签来识别其 pod。如果使用另一个标签,需要显式添加它:

for C in cluster-1 
    cluster-2 
    cluster-3 
    cluster-4 
              cluster-5
    do
      kubectl --context=$C run nginx --image=nginx:1.11.1-alpine --port=80
    done

验证公共 DNS 记录

一旦前面的 pod 成功启动并监听连接,Kubernetes 将把它们报告为该集群中服务的健康端点(通过自动健康检查)。Kubernetes 集群联邦将进一步考虑这些服务分片中的每一个为健康,并通过自动配置相应的公共 DNS 记录将它们放入服务中。你可以使用你配置的 DNS 提供商的首选界面来验证这一点。例如,你的联邦可能配置为使用 Google Cloud DNS 和一个托管的 DNS 域,example.com

> gcloud dns managed-zones describe example-dot-com 
creationTime: '2017-03-08T18:18:39.229Z'
description: Example domain for Kubernetes Cluster Federation
dnsName: example.com.
id: '7228832181334259121'
kind: dns#managedZone
name: example-dot-com
nameServers:
- ns-cloud-a1.googledomains.com.
- ns-cloud-a2.googledomains.com.
- ns-cloud-a3.googledomains.com.
- ns-cloud-a4.googledomains.com.

跟进以下命令以查看实际的 DNS 记录:

> gcloud dns record-sets list --zone example-dot-com  

如果你的联邦配置为使用aws route53 DNS 服务,请使用以下命令:

> aws route53 list-hosted-zones  

然后使用这个命令:

> aws route53 list-resource-record-sets --hosted-zone-id K9PBY0X1QTOVBX  

当然,你可以使用标准的 DNS 工具,比如nslookupdig来验证 DNS 记录是否被正确更新。你可能需要等一会儿才能使你的更改传播开来。或者,你可以直接指向你的 DNS 提供商:

> dig @ns-cloud-e1.googledomains.com ... 

然而,我总是更喜欢在 DNS 更改在正确传播后观察它们的变化,这样我就可以通知用户一切都准备就绪。

DNS 扩展

如果服务在本地集群中不存在(或者存在但没有健康的后端 pod),DNS 查询会自动扩展,以找到最接近请求者可用区域的外部 IP 地址。KubeDNS 会自动执行这个操作,并返回相应的CNAME。这将进一步解析为服务的一个后备 pod 的 IP 地址。

你不必依赖自动 DNS 扩展。你也可以直接提供特定集群中或特定区域中服务的CNAME。例如,在 GCE/GKE 上,你可以指定nginx.the-namespace.svc.europe-west1.example.com。这将被解析为欧洲某个集群中服务的一个后备 pod 的 IP 地址(假设那里有集群和健康的后备 pod)。

外部客户端无法使用 DNS 扩展,但如果他们想要针对联邦的某个受限子集(比如特定区域),他们可以提供服务的完全限定的CNAME,就像例子一样。由于这些名称往往又长又笨重,一个好的做法是添加一些静态方便的CNAME记录:

eu.nginx.example.com        CNAME nginx.the-namespace.the-federation.svc.europe-west1.example.com.
us.nginx.example.com        CNAME nginx.the-namespace.the-federation.svc.us-central1.example.com.
nginx.example.com           CNAME nginx.the-namespace.the-federation.svc.example.com.  

下图显示了联邦查找在多个集群中是如何工作的:

处理后端 pod 和整个集群的故障

Kubernetes 将在几秒内将无响应的 Pod 从服务中移除。联邦控制平面监视集群的健康状况,以及不同集群中联邦服务的所有分片后面的端点。根据需要,它将将它们加入或移出服务,例如当服务后面的所有端点、整个集群或整个可用区都宕机时。DNS 缓存的固有延迟(默认情况下联邦服务 DNS 记录为 3 分钟),可能会在发生灾难性故障时将客户端的故障转移至另一个集群。然而,考虑到每个区域服务端点可以返回的离散 IP 地址数量(例如,us-central1有三个备用项),许多客户端将在比这更短的时间内自动切换到其中一个备用 IP,前提是进行了适当的配置。

故障排除

当事情出现问题时,您需要能够找出问题所在以及如何解决。以下是一些常见问题以及如何诊断/解决它们。

无法连接到联邦 API 服务器

请参考以下解决方案:

  • 验证联邦 API 服务器正在运行

  • 验证客户端(Kubectl)是否正确配置了适当的 API 端点和凭据

联邦服务成功创建,但基础集群中未创建服务

  • 验证集群是否已注册到联邦

  • 验证联邦 API 服务器能够连接并对所有集群进行身份验证

  • 检查配额是否足够

  • 检查日志是否有其他问题:

   Kubectl logs federation-controller-manager --namespace federation

总结

在本章中,我们已经涵盖了 Kubernetes 集群联邦的重要主题。集群联邦仍处于测试阶段,有些粗糙,但已经可以使用。部署并不多,目前官方支持的目标平台是 AWS 和 GCE/GKE,但云联邦背后有很大的动力。这对于在 Kubernetes 上构建大规模可扩展系统非常重要。我们讨论了 Kubernetes 集群联邦的动机和用例,联邦控制平面组件以及联邦 Kubernetes 对象。我们还研究了联邦的一些不太受支持的方面,比如自定义调度、联邦数据访问和自动扩展。然后,我们看了如何运行多个 Kubernetes 集群,包括设置 Kubernetes 集群联邦,向联邦添加和移除集群以及负载平衡、联邦故障转移、服务发现和迁移。然后,我们深入研究了在多个集群上运行联合工作负载的情况,包括联合服务以及与此场景相关的各种挑战。

到目前为止,您应该对联邦的当前状态有清晰的了解,知道如何利用 Kubernetes 提供的现有功能,并了解您需要自己实现哪些部分来增强不完整或不成熟的功能。根据您的用例,您可能会决定现在还为时过早,或者您想要冒险尝试。致力于 Kubernetes 联邦的开发人员行动迅速,因此很可能在您需要做出决定时,它将更加成熟和经过实战检验。

在下一章中,我们将深入研究 Kubernetes 的内部结构以及如何自定义它。Kubernetes 的一个显著的架构原则是,它可以通过一个完整的 REST API 进行访问。Kubectl 命令行工具是建立在 Kubernetes API 之上的,并为 Kubernetes 的整个范围提供交互性。然而,编程 API 访问为您提供了许多灵活性,以增强和扩展 Kubernetes。许多语言中都有客户端库,允许您从外部利用 Kubernetes 并将其集成到现有系统中。

除了其 REST API 之外,Kubernetes 在设计上是一个非常模块化的平台。它的核心操作的许多方面都可以定制和/或扩展。特别是,你可以添加用户定义的资源,并将它们与 Kubernetes 对象模型集成,并从 Kubernetes 的管理服务、etcd中的存储、通过 API 的暴露以及对内置和自定义对象的统一访问中受益。

我们已经看到了一些非常可扩展的方面,比如通过 CNI 插件和自定义存储类进行网络和访问控制。然而,Kubernetes 甚至可以让你定制调度器本身,这个调度器控制着 pod 分配到节点上。

第十二章:自定义 Kubernetes - API 和插件

在本章中,我们将深入研究 Kubernetes 的内部。我们将从 Kubernetes API 开始,学习如何通过直接访问 API、Python 客户端来以编程方式使用 Kubernetes,然后我们将自动化 Kubectl。然后,我们将研究如何使用自定义资源扩展 Kubernetes API。最后一部分是关于 Kubernetes 支持的各种插件。Kubernetes 操作的许多方面都是模块化的,并且设计用于扩展。我们将研究几种类型的插件,如自定义调度程序、授权、准入控制、自定义指标和卷。最后,我们将研究如何扩展 Kubectl 并添加自己的命令。

我们涵盖的主题如下:

  • 使用 Kubernetes API

  • 扩展 Kubernetes API

  • 编写 Kubernetes 和 Kubectl 插件

  • 编写 webhooks

使用 Kubernetes API

Kubernetes API 是全面的,涵盖了 Kubernetes 的全部功能。正如您所期望的那样,它是庞大的。但它采用了最佳实践进行了良好设计,并且是一致的。如果您了解基本原则,您可以发现您需要了解的一切。

理解 OpenAPI

OpenAPI 允许 API 提供者定义其操作和模型,并使开发人员能够自动化其工具并生成其喜爱的语言客户端以与该 API 服务器通信。Kubernetes 已经支持 Swagger 1.2(OpenAPI 规范的旧版本)一段时间了,但规范是不完整和无效的,这使得基于它生成工具/客户端变得困难。

在 Kubernetes 1.4 中,为 OpenAPI 规范(在捐赠给 OpenAPI 倡议之前被称为 Swagger 2.0)添加了 alpha 支持,并更新了当前的模型和操作。在 Kubernetes 1.5 中,通过直接从 Kubernetes 源自动生成规范来完成了对 OpenAPI 规范的支持,这使得规范和文档与操作/模型的未来变化完全同步。

新规范使 API 文档更好,并且我们将在以后探索自动生成的 Python 客户端。

该规范是模块化的,并按组版本划分。这是未来的保证。您可以运行支持不同版本的多个 API 服务器。应用程序可以逐渐过渡到更新的版本。

规范的结构在 OpenAPI 规范定义中有详细解释。Kubernetes 团队使用操作标签来分隔每个组版本,并尽可能填写有关路径/操作和模型的信息。对于特定操作,所有参数、调用方法和响应都有文档记录。结果令人印象深刻。

设置代理

为了简化访问,您可以使用 Kubectl 设置代理:

> kubectl proxy --port 8080  

现在,您可以访问http://localhost:8080的 API 服务器,并且它将到达与 Kubectl 配置相同的 Kubernetes API 服务器。

直接探索 Kubernetes API

Kubernetes API 很容易找到。您只需浏览到 API 服务器的 URLhttp://localhost:8080,就可以获得一个描述路径键下所有可用操作的漂亮 JSON 文档。

由于空间限制,这里是部分列表:

{
 "paths": [
 "/api",
 "/api/v1",
 "/apis",
 "/apis/apps",
 "/apis/storage.k8s.io/v1",
 .
 .
 .
 "/healthz",
 "/healthz/ping",
 "/logs",
 "/metrics",
 "/swaggerapi/",
 "/ui/",
 "/version"
 ]
}

您可以深入了解任何一个路径。例如,这是来自/api/v1/namespaces/default端点的响应:

{
 "apiVersion": "v1",
 "kind": "Namespace",
 "metadata": {
 "creationTimestamp": "2017-12-25T10:04:26Z",
 "name": "default",
 "resourceVersion": "4",
 "selfLink": "/api/v1/namespaces/default",
 "uid": "fd497868-e95a-11e7-adce-080027c94384"
 },
 "spec": {
 "finalizers": [
 "kubernetes"
 ]
 },
 "status": {
 "phase": "Active"
 }
}

我首先通过访问/api,然后发现了/api/v1,告诉我有/api/v1/namespaces,指引我到/api/v1/namespaces/default

使用 Postman 探索 Kubernetes API

Postman (www.getpostman.com)是一个非常成熟的用于处理 RESTful API 的应用程序。如果您更倾向于 GUI 界面,您可能会发现它非常有用。

以下截图显示了批处理V1 API 组下可用的端点:

Postman 有很多选项,并且以非常令人愉悦的方式组织信息。试试看吧。

使用 httpie 和 jq 过滤输出

API 的输出有时可能太冗长。通常,您只对大量 JSON 响应中的一个值感兴趣。例如,如果您想获取所有运行服务的名称,可以访问/api/v1/services端点。然而,响应中包含许多无关的附加信息。这里是输出的一个非常部分子集:

$ http http://localhost:8080/api/v1/services
{
 "apiVersion": "v1",
 "items": [
 {
 "metadata": {
 "creationTimestamp": "2018-03-03T05:18:30Z",
 "labels": {
 "component": "apiserver",
 "provider": "kubernetes"
 },
 "name": "kubernetes",
 …
 },
 "spec": {
 …
 },
 "status": {
 "loadBalancer": {}
 }
 },
 …
 ],
 "kind": "ServiceList",
 "metadata": {
 "resourceVersion": "1076",
 "selfLink": "/api/v1/services"
 }
}

完整的输出有 121 行长!让我们看看如何使用httpiejq来完全控制输出,并仅显示服务的名称。我更喜欢(httpie.org/)而不是 CURL 来与命令行上的 REST API 进行交互。jq (stedolan.github.io/jq/)命令行 JSON 处理器非常适合切割和处理 JSON。

检查完整的输出,您会看到服务名称在 items 数组中每个项目的 metadata 部分。将选择namejq表达式如下:

.items[].metadata.name

以下是完整的命令和输出:

$ http http://localhost:8080/api/v1/services | jq .items[].metadata.name
"kubernetes"
"kube-dns"
"kubernetes-dashboard"  

通过 Kubernetes API 创建一个 pod

API 也可以用于创建、更新和删除资源,只要提供nginx-pod.json中的 pod 清单:

{
 "kind": "Pod",
 "apiVersion": "v1",
 "metadata":{
 "name": "nginx",
 "namespace": "default",
 "labels": {
 "name": "nginx"
 }
 },
 "spec": {
 "containers": [{
 "name": "nginx",
 "image": "nginx",
 "ports": [{"containerPort": 80}]
 }]
 }
}

以下命令将通过 API 创建 pod:

> http POST http://localhost:8080/api/v1/namespaces/default/pods @nginx-pod.json  

验证它是否有效,让我们提取当前 pod 的名称和状态。端点如下:

/api/v1/namespaces/default/pods  

jq表达式如下:

items[].metadata.name,.items[].status.phase  

以下是完整的命令和输出:

> FILTER='.items[].metadata.name,.items[].status.phase'
> http http://localhost:8080/api/v1/namespaces/default/pods | jq $FILTER 
"nginx"
"Running"  

通过 Python 客户端访问 Kubernetes API

使用httpiejq交互地探索 API 非常棒,但当您将其与其他软件消耗和集成时,API 的真正力量就会显现出来。Kubernetes 孵化器项目提供了一个功能齐全且文档非常完善的 Pythonclient库。它可以在https://github.com/kubernetes-incubator/client-python上找到。

首先确保已安装 Python(2.7 或 3.5+)。然后安装 Kubernetes 包:

> pip install kubernetes

要开始与 Kubernetes 集群通信,您需要连接到它。启动一个交互式的 Python 会话:

> python
Python 3.6.4 (default, Mar  1 2018, 18:36:42)
[GCC 4.2.1 Compatible Apple LLVM 9.0.0 (clang-900.0.39.2)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>  

Python 客户端可以读取您的 Kubectl 配置:

>>> from kubernetes import client, config
>>> config.load_kube_config()
>>> v1 = client.CoreV1Api()  

或者它可以直接连接到已经运行的代理:

>>> from kubernetes import client, config
>>> client.Configuration().host = 'http://localhost:8080'
>>> v1 = client.CoreV1Api()  

请注意,客户端模块提供了访问不同组版本的方法,例如CoreV1API

解剖 CoreV1API 组

让我们深入了解CoreV1API组。Python 对象有481 个公共属性

>>> attributes = [x for x in dir(v1) if not x.startswith('__')]
>>> len(attributes)
481  

忽略以双下划线开头的属性,因为它们是与 Kubernetes 无关的特殊class/instance方法。

让我们随机挑选十个方法,看看它们是什么样子的:

>>> import random
>>> from pprint import pprint as pp
>>> pp(random.sample(attributes, 10))
['patch_namespaced_pod',
 'connect_options_node_proxy_with_path_with_http_info',
 'proxy_delete_namespaced_pod_with_path',
 'delete_namespace',
 'proxy_post_namespaced_pod_with_path_with_http_info',
 'proxy_post_namespaced_service',
 'list_namespaced_pod_with_http_info',
 'list_persistent_volume_claim_for_all_namespaces',
 'read_namespaced_pod_log_with_http_info',
 'create_node']  

非常有趣。属性以动词开头,比如 list、patch 或 read。其中许多都有namespace的概念,许多都有with_http_info后缀。为了更好地理解,让我们统计有多少动词存在,以及每个动词使用了多少属性(其中动词是下划线之前的第一个标记):

>>> from collections import Counter
>>> verbs = [x.split('_')[0] for x in attributes]
>>> pp(dict(Counter(verbs)))
{'api': 1,
 'connect': 96,
 'create': 36,
 'delete': 56,
 'get': 2,
 'list': 56,
 'patch': 48,
 'proxy': 84,
 'read': 52,
 'replace': 50}  

我们可以进一步深入,查看特定属性的交互式帮助:

>>> help(v1.create_node)
Help on method create_node in module kuber-netes.client.apis.core_v1_api:

create_node(body, **kwargs) method of kuber-netes.client.apis.core_v1_api.CoreV1Api instance
 create a Node
 This method makes a synchronous HTTP request by default. To make an
 asynchronous HTTP request, please pass async=True
 >>> thread = api.create_node(body, async=True)
 >>> result = thread.get()

 :param async bool
 :param V1Node body: (required)
 :param str pretty: If 'true', then the output is pretty printed.
 :return: V1Node
 If the method is called asynchronously,
 returns the request thread.

您可以自己查看并了解有关 API 的更多信息。让我们看一些常见操作,如列出、创建、观察和删除对象。

列出对象

您可以列出不同类型的对象。方法名称以list_开头。以下是列出所有命名空间的示例:

 >>> for ns in v1.list_namespace().items:
...     print(ns.metadata.name)
...
default
kube-public
kube-system  

创建对象

要创建一个对象,您需要将一个 body 参数传递给 create 方法。body 必须是一个等同于您在 Kubectl 中使用的 YAML 配置文件的 Python 字典。最简单的方法是实际使用 YAML,然后使用 Python YAML 模块(这不是标准库的一部分,必须单独安装)来读取 YAML 文件并将其加载到字典中。例如,要创建一个带有3个副本的nginx-deployment,我们可以使用这个 YAML 配置文件:

apiVersion: apps/v1
kind: Deployment
metadata:
 name: nginx-deployment
spec:
 replicas: 3
 template:
 metadata:
 labels:
 app: nginx
 spec:
 containers:
 - name: nginx
 image: nginx:1.7.9
 ports:
 - containerPort: 80

要安装yaml Python 模块,请输入以下命令:

> pip install yaml

然后以下 Python 程序将创建部署:

from os import path
import yaml
from kubernetes import client, config

def main():
 # Configs can be set in Configuration class directly or using
 # helper utility. If no argument provided, the config will be 
 # loaded from default location.
 config.load_kube_config()

 with open(path.join(path.dirname(__file__), 
 'nginx-deployment.yaml')) as f:
 dep = yaml.load(f)
 k8s = client.AppsV1Api()
 status = k8s_beta.create_namespaced_deployment(
 body=dep, namespace="default").status
 print("Deployment created. status='{}'".format(status))

if __name__ == '__main__':
 main()

观察对象

观察对象是一种高级功能。它是使用单独的观察模块实现的。以下是一个示例,用于观察10个命名空间事件并将它们打印到屏幕上:

from kubernetes import client, config, watch

# Configs can be set in Configuration class directly or using helper utility
config.load_kube_config()
v1 = client.CoreV1Api()
count = 10
w = watch.Watch()
for event in w.stream(v1.list_namespace, _request_timeout=60):
 print(f"Event: {event['type']} {event['object'].metadata.name}") 
 count -= 1
 if count == 0:
 w.stop()

print('Done.')

以编程方式调用 Kubectl

如果您不是 Python 开发人员,也不想直接处理 REST API,那么您还有另一种选择。Kubectl 主要用作交互式命令行工具,但没有任何阻止您自动化它并通过脚本和程序调用它。使用 Kubectl 作为 Kubernetes API 客户端的一些好处包括:

  • 易于找到任何用法的示例

  • 在命令行上轻松实验,找到正确的命令和参数组合

  • Kubectl 支持以 JSON 或 YAML 格式输出,以便快速解析。

  • 身份验证是通过 Kubectl 配置内置的

使用 Python 子进程运行 Kubectl

我将再次使用 Python,这样您可以比较使用官方 Python 客户端和自己编写的客户端。Python 有一个名为subprocess的模块,可以运行 Kubectl 等外部进程并捕获输出。以下是一个 Python 3 示例,独立运行 Kubectl 并显示用法输出的开头:

>>> import subprocess
>>> out = subprocess.check_output('kubectl').decode('utf-8')
>>> print(out[:276])  

Kubectl 控制 Kubernetes 集群管理器。在github.com/kubernetes/kubernetes找到更多信息。

以下是一些初学者的基本命令:

  • create:使用文件名或stdin创建资源

  • expose:获取复制控制器、服务、部署或 pod

check_checkout()函数将输出捕获为一个需要解码为utf-8以正确显示的字节数组。我们可以将其概括一点,并创建一个名为k的便利函数,它接受参数并将其传递给 Kubectl,然后解码输出并返回它:

from subprocess import check_output

def k(*args):
     out = check_output(['kubectl'] + list(args))
     return out.decode('utf-8')
Let's use it to list all the running pods in the default namespace:
>>> print(k('get', 'po'))

NAME                               Ready   Status  Restarts      Age 
nginx-deployment-6c54bd5869-9mp2g   1/1    Running    0          18m
nginx-deployment-6c54bd5869-lgs84   1/1    Running    0          18m
nginx-deployment-6c54bd5869-n7468   1/1    Running    0        . 18m  

这对于显示很好,但 Kubectl 已经做到了。当您使用带有-o标志的结构化输出选项时,真正的力量就会显现出来。然后结果可以自动转换为 Python 对象。这是k()函数的修改版本,它接受一个布尔值use_json关键字参数(默认为False);如果为True,则添加-o json,然后将 JSON 输出解析为 Python 对象(字典):

from subprocess import check_output
import json

def k(use_json=False, *args):
 cmd = ['kubectl']

 cmd += list(args)
 if use_json:
 cmd += ['-o', 'json']
 out = check_output(cmd)
 if use_json:
 out = json.loads(out)
 else:
 out = out.decode('utf-8')
 return out

返回一个完整的 API 对象,可以像直接访问 REST API 或使用官方 Python 客户端一样进行导航和钻取:

result = k('get', 'po', use_json=True)
for r in result['items']:
    print(r['metadata']['name'])

nginx-deployment-6c54bd5869-9mp2g
nginx-deployment-6c54bd5869-lgs84
nginx-deployment-6c54bd5869-n7468  

让我们看看如何删除deployment并等待所有 pod 消失。Kubectl delete 命令不接受-o json选项(尽管有-o名称),所以让我们不使用use_json

k('delete', 'deployment', 'nginx-deployment')
while len(k('get', 'po', use_json=True)['items']) > 0:
   print('.')
print('Done.')

Done. 

扩展 Kubernetes API

Kubernetes 是一个非常灵活的平台。它允许您通过称为自定义资源的新类型资源扩展自己的 API。如果这还不够,您甚至可以提供与 Kubernetes API 服务器集成的 API 聚合机制。您可以用自定义资源做什么?很多。您可以使用它们来管理 Kubernetes 集群外部的 Kubernetes API 资源,您的 pod 与之通信。

通过将这些外部资源添加为自定义资源,您可以全面了解系统,并从许多 Kubernetes API 功能中受益,例如以下功能:

  • 自定义 CRUD REST 端点

  • 版本控制

  • 观察

  • 与通用 Kubernetes 工具的自动集成

自定义控制器和自动化程序的元数据

在 Kubernetes 1.7 中引入的自定义资源是对现在已弃用的第三方资源的重大改进。让我们深入了解一下自定义资源的全部内容。

理解自定义资源的结构

为了与 Kubernetes API 服务器协作,第三方资源必须符合一些基本要求。与内置 API 对象类似,它们必须具有以下字段:

  • apiVersionapiextensions.k8s.io/v1beta1

  • metadata:标准 Kubernetes 对象元数据

  • kindCustomResourceDefinition

  • spec:描述资源在 API 和工具中的外观

  • status:指示 CRD 的当前状态

规范具有内部结构,包括组、名称、范围、验证和版本等字段。状态包括字段acceptedNamesConditions。在下一节中,我将为您展示一个示例,以阐明这些字段的含义。

开发自定义资源定义

您可以使用自定义资源定义(也称为 CRD)开发自定义资源。CRD 的目的是与 Kubernetes、其 API 和其工具平稳集成,因此您需要提供大量信息。这是一个名为Candy的自定义资源的示例:

apiVersion: apiextensions.k8s.io/v1beta1 
kind: CustomResourceDefinition 
metadata: 
  # name must match the spec fields below, and be in the form: <plural>.<group> 
  name: candies.awesome.corp.com 
spec: 
  # group name to use for REST API: /apis/<group>/<version> 
  group: awesome.corp.com 
  # version name to use for REST API: /apis/<group>/<version> 
  version: v1 
  # either Namespaced or Cluster 
  scope: Namespaced 
  names: 
    # plural name to be used in the URL: /apis/<group>/<version>/<plural> 
    plural: candies 
    # singular name to be used as an alias on the CLI and for display 
    singular: candy 
    # kind is normally the CamelCased singular type. Your resource manifests use this. 
    kind: Candy 
    # shortNames allow shorter string to match your resource on the CLI 
    shortNames: 
    - cn 

让我们创建它:

> kubectl create -f crd.yaml 
customresourcedefinition "candies.awesome.corp.com" created 

注意,返回的元数据名称带有复数标记。现在,让我们验证一下我们是否可以访问它:

> kubectl get crd
NAME                                      AGE
candies.awesome.corp.com                  17m

还有一个新的 API 端点用于管理这种新资源:

/apis/awesome.corp.com/v1/namespaces/<namespace>/candies/  

让我们使用我们的 Python 代码来访问它:

>>> config.load_kube_config()
>>> print(k('get', 'thirdpartyresources'))
NAME                                           AGE
candies.awesome.corp.com                       24m 

集成自定义资源

创建CustomResourceDefinition对象后,您可以特定地创建该资源类型的自定义资源,例如,在这种情况下是Candycandy变为CamelCase Candy)。Candy对象可以包含任意字段和任意 JSON。在下面的示例中,flavor自定义字段设置在Candy对象上。apiVersion字段是从 CRD 规范的组和版本字段派生的:

apiVersion: "awesome.corp.com/v1" 
kind: Candy 
metadata: 
  name: chocolatem 
spec: 
  flavor: "sweeeeeeet" 

您可以向自定义资源添加任意字段。这些值可以是任何 JSON 值。请注意,这些字段未在 CRD 中定义。不同的对象可以具有不同的字段。让我们创建它:

> kubectl create -f candy.yaml
candy "chocolate" created 

此时,kubectl可以像操作内置对象一样操作Candy对象。请注意,在使用kubectl时,资源名称不区分大小写:

$ kubectl get candies
NAME        AGE
chocolate   2m  

我们还可以使用标准的-o json标志查看原始 JSON 数据。这次我会使用简称cn

> kubectl get cn -o json
{
 "apiVersion": "v1",
 "items": [
 {
 "apiVersion": "awesome.corp.com/v1",
 "kind": "Candy",
 "metadata": {
 "clusterName": "",
 "creationTimestamp": "2018-03-07T18:18:42Z",
 "name": "chocolate",
 "namespace": "default",
 "resourceVersion": "4791773",
 "selfLink": "/apis/awesome.corp.com/v1/namespaces/default/candies/chocolate",
 "uid": "f7a6fd80-2233-11e8-b432-080027c94384"
 },
 "spec": {
 "flavor": "sweeeeeeet"
 }
 }
 ],
 "kind": "List",
 "metadata": {
 "resourceVersion": "",
 "selfLink": ""
 }
}  

完成自定义资源

自定义资源支持与标准 API 对象一样的 finalizers。finalizer 是一种机制,对象不会立即被删除,而是必须等待后台运行并监视删除请求的特殊控制器。控制器可以执行任何必要的清理操作,然后从目标对象中删除其 finalizer。对象上可能有多个 finalizer。Kubenetes 将等待直到所有 finalizer 都被删除,然后才删除对象。元数据中的 finalizer 只是它们对应的控制器可以识别的任意字符串。这里有一个示例,其中Candy对象有两个 finalizer,eat-medrink-me

apiVersion: "awesome.corp.com/v1" 
kind: Candy 
metadata: 
  name: chocolate 
  finalizers: 
  - eat-me 
  - drink-me 
spec: 
  flavor: "sweeeeeeet" 

验证自定义资源

您可以向 CRD 添加任何字段。这可能导致无效的定义。Kubernetes 1.9 引入了基于 OpenAPI V3 模式的 CRD 验证机制。它仍处于测试阶段,并且可以在启动 API 服务器时使用功能开关进行禁用:

--feature-gates=CustomResourceValidation=false    

在您的 CRD 中,您可以在规范中添加一个验证部分:

validation:
 openAPIV3Schema:
 properties:
 spec:
 properties:
 cronSpec:
 type: string
 pattern: '^(\d+|\*)(/\d+)?(\s+(\d+|\*)(/\d+)?){4}$'
 replicas:
 type: integer
 minimum: 1
 maximum: 10

如果您尝试创建违反规范验证的对象,您将收到错误消息。您可以在此处阅读有关 OpenAPI 模式的更多信息:http://bit.ly/2FsBfWA

了解 API 服务器聚合

当您只需要对自己的类型进行一些 CRUD 操作时,CRDs 非常好。您可以直接在 Kubernetes API 服务器上运行,它将存储您的对象并提供 API 支持和与诸如 Kubectl 之类的工具集成。您可以运行控制器来监视您的对象,并在创建、更新或删除时执行一些操作。但 CRDs 有局限性。如果您需要更高级的功能和定制,您可以使用 API 服务器聚合并编写自己的 API 服务器,Kubernetes API 服务器将委托给它。

您的 API 服务器将使用与 Kubernetes API 服务器本身相同的 API 机制。一些高级功能如下:

  • 控制对象的存储

  • 多版本

  • 超出 CRUD 的自定义操作(如 exec 或 scale)

  • 使用协议缓冲区有效载荷

编写扩展 API 服务器是一项非常艰巨的工作。如果您决定需要所有这些功能,我建议使用 API 构建器项目:

github.com/kubernetes-incubator/apiserver-builder

这是一个年轻的项目,但它处理了许多必要的样板代码。API 构建器提供以下功能:

  • 引导完整的类型定义、控制器和测试,以及文档

  • 您可以在 Minikube 内部本地运行扩展控制平面,也可以在实际的远程集群上运行

  • 您生成的控制器将能够监视和更新 API 对象

  • 添加资源(包括子资源)

  • 如果需要,您可以覆盖默认值

利用服务目录

Kubernetes 服务目录项目允许您平滑且无痛地集成任何支持 Open Service Broker API 规范的外部服务:

github.com/openservicebrokerapi/servicebroker

开放服务经纪人 API 的目的是通过支持文档和全面的测试套件,通过标准规范将外部服务暴露给任何云环境。这使提供商可以实现单一规范,并支持多个云环境。当前的环境包括 Kubernetes 和 CloudFoundry。该项目致力于广泛的行业采用。

服务目录对于集成云平台提供商的服务特别有用。以下是一些此类服务的示例:

  • Microsoft Azure Cloud Queue

  • Amazon Simple Queue Service

  • Google Cloud Pub/Sub

这种能力对于致力于云计算的组织来说是一个福音。您可以在 Kubernetes 上构建系统,但不必自己部署、管理和维护集群中的每个服务。您可以将这些工作外包给您的云提供商,享受深度集成,并专注于您的应用程序。

服务目录有可能使您的 Kubernetes 集群完全自主,因为它允许您通过服务经纪人来配置云资源。我们还没有达到那一步,但这个方向非常有前途。

这结束了我们对从外部访问和扩展 Kubernetes 的讨论。在下一节中,我们将把目光投向内部,并研究通过插件自定义 Kubernetes 内部工作的方法。

编写 Kubernetes 插件

在本节中,我们将深入研究 Kubernetes 的内部,并学习如何利用其著名的灵活性和可扩展性。我们将了解可以通过插件自定义的不同方面,以及如何实现这些插件并将其与 Kubernetes 集成。

编写自定义调度程序插件

Kubernetes 将自己定义为容器调度和管理系统。因此,调度程序是 Kubernetes 最重要的组件。Kubernetes 带有默认调度程序,但允许编写额外的调度程序。要编写自己的自定义调度程序,您需要了解调度程序的功能,它是如何打包的,如何部署您的自定义调度程序以及如何集成您的调度程序。调度程序源代码在这里可用:

github.com/kubernetes/kubernetes/tree/master/pkg/scheduler

在本节的其余部分,我们将深入研究源代码,并检查数据类型、算法和代码。

了解 Kubernetes 调度程序的设计

调度程序的工作是为新创建或重新启动的 pod 找到一个节点,并在 API 服务器中创建一个绑定并在那里运行它。如果调度程序找不到适合 pod 的节点,它将保持在挂起状态。

调度程序

调度程序的大部分工作都是相当通用的——它会找出哪些 pod 需要被调度,更新它们的状态,并在选定的节点上运行它们。定制部分是如何将 pod 映射到节点。Kubernetes 团队意识到了需要定制调度的需求,通用调度程序可以配置不同的调度算法。

主要数据类型是包含许多属性的调度程序struct,其中包含一个Config struct(这很快将被configurator接口替换):

type Scheduler struct { 
    config *Config 
} 

这是Config struct

type Config struct { 
    SchedulerCache schedulercache.Cache 
    Ecache     *core.EquivalenceCache 
    NodeLister algorithm.NodeLister 
    Algorithm  algorithm.ScheduleAlgorithm 
    GetBinder  func(pod *v1.Pod) Binder 
    PodConditionUpdater PodConditionUpdater 
    PodPreemptor PodPreemptor 
    NextPod func() *v1.Pod 
    WaitForCacheSync func() bool 
    Error func(*v1.Pod, error) 
    Recorder record.EventRecorder 
    StopEverything chan struct{} 
    VolumeBinder *volumebinder.VolumeBinder 
} 

其中大多数是接口,因此您可以使用自定义功能配置调度程序。特别是,如果您想要自定义 pod 调度,则调度程序算法是相关的。

注册算法提供程序

调度程序具有算法提供程序和算法的概念。它们一起让您使用内置调度程序的重要功能,以替换核心调度算法。

算法提供程序允许您使用工厂注册新的算法提供程序。已经注册了一个名为ClusterAutoScalerProvider的自定义提供程序。稍后我们将看到调度程序如何知道使用哪个算法提供程序。关键文件如下:

github.com/kubernetes/kubernetes/blob/master/pkg/scheduler/algorithmprovider/defaults/defaults.go

init()函数调用registerAlgorithmProvider(),您应该扩展它以包括您的算法提供程序以及默认和autoscaler提供程序:

func registerAlgorithmProvider(predSet, priSet sets.String) { 
    // Registers algorithm providers. By default we use 'DefaultProvider' 
    // but user can specify one to be used by specifying flag. 
    factory.RegisterAlgorithmProvider(factory.DefaultProvider, predSet, priSet) 
    // Cluster autoscaler friendly scheduling algorithm. 
    factory.RegisterAlgorithmProvider(ClusterAutoscalerProvider, predSet, 
        copyAndReplace(priSet, "LeastRequestedPriority", "MostRequestedPriority")) 
} 

除了注册提供程序,您还需要注册适合谓词和优先级函数,这些函数用于实际执行调度。

您可以使用工厂的RegisterFitPredicate()RegisterPriorityFunction2()函数。

配置调度程序

调度程序算法作为配置的一部分提供。自定义调度程序可以实现ScheduleAlgorithm接口:

type ScheduleAlgorithm interface { 
    Schedule(*v1.Pod, NodeLister) (selectedMachine string, err error) 
    Preempt(*v1.Pod, NodeLister, error) (selectedNode *v1.Node,  
                                         preemptedPods []*v1.Pod,  
                                         cleanupNominatedPods []*v1.Pod,  
                                         err error) 
    Predicates() map[string]FitPredicate 
    Prioritizers() []PriorityConfig 
} 

当您运行调度程序时,您可以提供自定义调度程序的名称或自定义算法提供程序作为命令行参数。如果没有提供,则将使用默认的算法提供程序。调度程序的命令行参数是--algorithm-provider--scheduler-name

打包调度程序

自定义调度程序作为一个 pod 在同一个 Kubernetes 集群中运行。它需要被打包为一个容器镜像。让我们使用标准 Kubernetes 调度程序的副本进行演示。我们可以从源代码构建 Kubernetes 以获取调度程序镜像:

git clone https://github.com/kubernetes/kubernetes.git
cd kubernetes
make  

创建以下 Dockerfile:

FROM busybox
ADD ./_output/bin/kube-scheduler /usr/local/bin/kube-scheduler  

使用它来构建一个 Docker 镜像类型:

docker build -t custom-kube-scheduler:1.0 .  

最后,将镜像推送到容器注册表。我会在这里使用 DockerHub。您需要在 DockerHub 上创建一个帐户并登录,然后再推送您的镜像。

> docker login
> docker push g1g1/custom-kube-scheduler 

请注意,我在本地构建了调度程序,并且在 Dockerfile 中,我只是将它从主机复制到镜像中。当您在与构建相同的操作系统上部署时,这种方法是有效的。如果不是这种情况,那么最好将构建命令插入 Dockerfile 中。你需要付出的代价是需要将所有 Kubernetes 内容都拉入镜像中。

部署自定义调度程序

现在调度程序镜像已构建并在注册表中可用,我们需要为其创建一个 Kubernetes 部署。调度程序当然是关键的,所以我们可以使用 Kubernetes 本身来确保它始终在运行。以下 YAML 文件定义了一个部署,其中包含一个单一副本和一些其他功能,如活跃和就绪探针:

apiVersion: apps/v1 
kind: Deployment 
metadata: 
  labels: 
    component: scheduler 
    tier: control-plane 
  name: custom-scheduler 
  namespace: kube-system 
spec: 
  replicas: 1 
  template: 
    metadata: 
      labels:  
        component: scheduler 
        tier: control-plane 
        version: second 
    spec: 
      containers: 
      - command: 
        - /usr/local/bin/kube-scheduler 
        - --address=0.0.0.0 
        - --leader-elect=false 
        - --scheduler-name=custom-scheduler 
        image: g1g1/custom-kube-scheduler:1.0 
        livenessProbe: 
          httpGet: 
            path: /healthz 
            port: 10251 
          initialDelaySeconds: 15 
        name: kube-second-scheduler 
        readinessProbe: 
          httpGet: 
            path: /healthz 
            port: 10251 
        resources: 
          requests: 
            cpu: '0.1' 

调度程序的名称(这里是custom-scheduler)很重要,必须是唯一的。稍后将用它来将 pod 与调度程序关联起来进行调度。请注意,自定义调度程序属于kube-system命名空间。

在集群中运行另一个自定义调度程序

运行另一个自定义调度程序就像创建部署一样简单。这就是这种封装方法的美妙之处。Kubernetes 将运行第二个调度程序,这是一件大事,但 Kubernetes 并不知道发生了什么。它只是部署一个 pod,就像部署任何其他 pod 一样,只是这个 pod 碰巧是一个自定义调度程序:

$ kubectl create -f custom-scheduler.yaml  

让我们验证调度程序 pod 是否在运行:

$ kubectl get pods --namespace=kube-system
NAME                              READY    STATUS  RESTARTS   AGE
....
custom-scheduler-7cfc49d749-lwzxj  1/1     Running     0        2m
...  

我们的自定义调度程序正在运行。

将 pod 分配给自定义调度程序

好的。自定义调度程序正在与默认调度程序一起运行。但是当 pod 需要调度时,Kubernetes 如何选择要使用的调度程序呢?答案是 pod 决定而不是 Kubernetes。pod 规范具有一个可选的调度程序名称字段。如果缺少,将使用默认调度程序;否则,将使用指定的调度程序。这就是自定义调度程序名称必须是唯一的原因。默认调度程序的名称是default-scheduler,如果您想在 pod 规范中明确表示。以下是将使用默认调度程序安排的 pod 定义:

apiVersion: v1 
kind: Pod 
metadata: 
  name: some-pod 
  labels: 
    name: some-pod 
spec: 
  containers: 
  - name: some-container 
    image: gcr.io/google_containers/pause:2.0 

要让custom-scheduler安排此 pod,请将 pod 规范更改为以下内容:

apiVersion: v1 
kind: Pod 
metadata: 
  name: some-pod 
  labels: 
    name: some-pod 
spec: 
  schedulerName: custom-scheduler 
  containers: 
  - name: some-container 
    image: gcr.io/google_containers/pause:2.0 

验证使用自定义调度程序安排了 pod

有两种主要方法可以验证 pod 是否由正确的调度程序安排。首先,您可以创建需要由自定义调度程序安排的 pod,然后部署自定义调度程序。这些 pod 将保持在挂起状态。然后,部署自定义调度程序,挂起的 pod 将被安排并开始运行。

另一种方法是检查事件日志,并使用以下命令查找已安排的事件:

$ kubectl get events  

使用访问控制 webhooks

Kubernetes 始终为您提供自定义访问控制的方法。在 Kubernetes 中,访问控制可以表示为三 A:认证、授权和准入控制。在早期版本中,通过需要 Go 编程的插件来完成,安装到您的集群中,注册和其他侵入性程序。现在,Kubernetes 允许您自定义认证、授权和准入控制 webhooks。

使用认证 webhook

Kubernetes 允许您通过注入用于 bearer tokens 的 webhook 来扩展认证过程。它需要两个信息:如何访问远程认证服务以及认证决定的持续时间(默认为两分钟)。

要提供这些信息并启用认证 webhooks,请使用以下命令行参数启动 API 服务器:

  • --runtime-config=authentication.k8s.io/v1beta1=true

  • --authentication-token-webhook-config-file

  • --authentication-token-webhook-cache-ttl

配置文件使用kubeconfig文件格式。以下是一个例子:

clusters:
 - name: remote-authentication-service
 cluster:
 certificate-authority: /path/to/ca.pem
 server: https://example.com/authenticate

users:
 - name: k8s-api-server
 user:
 client-certificate: /path/to/cert.pem
 client-key: /path/to/key.pem

current-context: webhook
contexts:
- context:
 cluster: remote-authentication-service
 user: k8s-api-sever
 name: webhook

请注意,必须向 Kubernetes 提供客户端证书和密钥,以进行与远程认证服务的相互认证。

缓存 TTL 很有用,因为通常用户会对 Kubernetes 进行多次连续请求。缓存认证决策可以节省大量与远程认证服务的往返。

当 API HTTP 请求到来时,Kubernetes 会从其标头中提取持有者令牌,并通过 Webhook 将TokenReview JSON 请求发送到远程认证服务:

{
  "apiVersion": "authentication.k8s.io/v1beta1",
  "kind": "TokenReview",
  "spec": {
    "token": "<bearer token from original request headers>"
  }
}    

远程认证服务将做出响应。认证状态将是 true 或 false。以下是成功认证的示例:

{ 
  "apiVersion": "authentication.k8s.io/v1beta1", 
  "kind": "TokenReview", 
  "status": { 
    "authenticated": true, 
    "user": { 
      "username": "gigi@gg.com", 
      "uid": "42", 
      "groups": [ 
        "developers", 
      ], 
      "extra": { 
        "extrafield1": [ 
          "extravalue1", 
          "extravalue2" 
        ] 
      } 
    } 
  } 
} 

拒绝的响应要简洁得多:

{ 
  "apiVersion": "authentication.k8s.io/v1beta1", 
  "kind": "TokenReview", 
  "status": { 
    "authenticated": false 
  } 
} 

使用授权 Webhook

授权 Webhook 与认证 Webhook 非常相似。它只需要一个与认证 Webhook 配置文件格式相同的配置文件。由于与认证不同,同一用户可能会对不同参数的不同 API 端点进行大量请求,并且授权决策可能不同,因此缓存不是一个可行的选项。

您可以通过向 API 服务器传递以下命令行参数来配置 Webhook:

  • --runtime-config=authorization.k8s.io/v1beta1=true

  • --authorization-webhook-config-file=<configuration filename>

当请求经过认证时,Kubernetes 将向远程授权服务发送SubjectAccessReview JSON 对象。它将包含请求用户以及请求的资源和其他请求属性:

{
  "apiVersion": "authorization.k8s.io/v1beta1",
  "kind": "SubjectAccessReview",
  "spec": {
    "resourceAttributes": {
      "namespace": "awesome-namespace",
      "verb": "get",
      "group": "awesome.example.org",
      "resource": "pods"
    },
    "user": "gigi@gg.com",
    "group": [
      "group1",
      "group2"
    ]
  }
}

请求将被允许:

{
  "apiVersion": "authorization.k8s.io/v1beta1",
  "kind": "SubjectAccessReview",
  "status": {
    "allowed": true
  }
} 

否则将被拒绝(并附带原因):

{
  "apiVersion": "authorization.k8s.io/v1beta1",
  "kind": "SubjectAccessReview",
  "status": {
    "allowed": false,
    "reason": "user does not have read access to the namespace"
  }
}

用户可能被授权访问资源,但不能访问非资源属性,如/api、/apis、/metrics、/resetMetrics、/logs、/debug、/healthz/swagger-ui//swaggerapi//ui/version

以下是如何请求访问日志:

{ 
  "apiVersion": "authorization.k8s.io/v1beta1", 
  "kind": "SubjectAccessReview", 
  "spec": { 
    "nonResourceAttributes": { 
      "path": "/logs", 
      "verb": "get" 
    }, 
    "user": "gigi@gg.com", 
    "group": [ 
      "group1", 
      "group2" 
    ] 
  } 
} 

使用准入控制 Webhook

动态准入控制也支持 Webhook。它仍处于 alpha 阶段。您需要通过向 API 服务器传递以下命令行参数来启用通用准入 Webhook:

  • --admission-control=GenericAdmissionWebhook

  • --runtime-config=admissionregistration.k8s.io/v1alpha1

动态配置 Webhook 准入控制器

在启动 API 服务器时,必须配置认证和授权 Webhook。可以通过创建externaladmissionhookconfiguration对象来动态配置准入控制 Webhook:

apiVersion: admissionregistration.k8s.io/v1alpha1 
kind: ExternalAdmissionHookConfiguration 
metadata: 
  name: example-config 
externalAdmissionHooks: 
- name: pod-image.k8s.io 
  rules: 
  - apiGroups: 
    - "" 
    apiVersions: 
    - v1 
    operations: 
    - CREATE 
    resources: 
    - pods 
  failurePolicy: Ignore 
  clientConfig: 
    caBundle: <pem encoded ca cert that signs the server cert used by the webhook> 
    service: 
      name: <name of the front-end service> 
      namespace: <namespace of the front-end service> 

为水平 Pod 自动缩放提供自定义指标

在 Kubernetes 1.6 之前,自定义指标是作为 Heapster 模型实现的。在 Kubernetes 1.6 中,引入了一个新的自定义指标 API,并逐渐成熟。截至 Kubernetes 1.9,它们已默认启用。自定义指标依赖于 API 聚合。推荐的路径是从这里开始使用自定义指标 API 服务器样板:

github.com/kubernetes-incubator/custom-metrics-apiserver

然后,您实现CustomMetricsProvider接口:

type CustomMetricsProvider interface { 
    GetRootScopedMetricByName(groupResource schema.GroupResource,  
                              name string,  
                              metricName string) (*custom_metrics.MetricValue, error) 
    GetRootScopedMetricBySelector(groupResource schema.GroupResource,  
                                  selector labels.Selector,  
                                  metricName string) (*custom_metrics.MetricValueList,  
                                                                  error) 
    GetNamespacedMetricByName(groupResource schema.GroupResource,  
                              namespace string,  
                              name string,  
                              metricName string) (*custom_metrics.MetricValue, error) 
    GetNamespacedMetricBySelector(groupResource schema.GroupResource,  
                                                           namespace string,  
                                                           selector labels.Selector,  
                                                           metricName string)  (*MetricValueList, error) 
    ListAllMetrics() []MetricInfo 
}  

使用自定义存储扩展 Kubernetes

卷插件是另一种类型的插件。在 Kubernetes 1.8 之前,您必须编写一个需要实现、注册到 Kubernetes 并与 Kubelet 链接的 Kublet 插件。Kubernetes 1.8 引入了 FlexVolume,这是更加灵活的。Kubernetes 1.9 通过容器存储接口CSI)将其提升到了下一个级别。

利用 FlexVolume

Kubernetes 卷插件旨在支持特定类型的存储或存储提供程序。有许多卷插件,我们在第七章中介绍了这些内容,处理 Kubernetes 存储。现有的卷插件对于大多数用户来说已经足够了,但是如果您需要集成一个不受支持的存储解决方案,您必须实现自己的卷插件,这并不是微不足道的。如果您希望它被接受为官方的 Kubernetes 插件,那么您必须经过严格的批准流程。但是FlexVolume提供了另一条路径。它是一个通用插件,允许您连接不受支持的存储后端,而无需与 Kubernetes 本身进行深度集成。

FlexVolume允许您向规范添加任意属性,并通过调用接口与您的后端通信,该接口包括以下操作:

  • 附加:将卷附加到 Kubernetes Kubelet 节点

  • 分离:从 Kubernetes Kubelet 节点分离卷

  • 挂载:挂载附加的卷

  • 卸载:卸载附加的卷

每个操作都由后端驱动程序作为二进制实现,FlexVolume 在正确的时间调用。驱动程序必须安装在/usr/libexec/kubernetes/kubelet-plugins/volume/exec/<vendor>~<driver>/<driver>中。

受益于 CSI

FlexVolume 提供了超出树插件的能力,但仍需要 FlexVolume 插件本身和一种相当繁琐的安装和调用模型。CSI 将通过让供应商直接实现它来显著改进。最好的是,作为开发人员,你不必创建和维护这些插件。实现和维护 CSI 是存储解决方案提供商的责任,他们有兴趣尽可能地使其稳健,以便人们不选择在 Kubernetes(以及与 CSI 集成的其他平台)上直接使用的不同存储解决方案。

总结

在本章中,我们涵盖了三个主要主题:使用 Kubernetes API、扩展 Kubernetes API 和编写 Kubernetes 插件。Kubernetes API 支持 OpenAPI 规范,是 REST API 设计的一个很好的例子,遵循了所有当前的最佳实践。它非常一致、组织良好、文档完善,但它是一个庞大的 API,不容易理解。你可以通过 REST over HTTP 直接访问 API,使用包括官方 Python 客户端在内的客户端库,甚至通过调用 Kubectl。

扩展 Kubernetes API 涉及定义自己的自定义资源,并通过 API 聚合可选地扩展 API 服务器本身。当你外部查询和更新它们时,自定义资源在与额外的自定义插件或控制器相结合时最有效。

插件和 webhooks 是 Kubernetes 设计的基础。Kubernetes 始终旨在由用户扩展以适应任何需求。我们看了一些你可以编写的插件和 webhooks,以及如何注册和无缝集成它们到 Kubernetes 中。

我们还研究了自定义指标,甚至如何通过自定义存储选项扩展 Kubernetes。

在这一点上,你应该很清楚通过 API 访问、自定义资源和自定义插件来扩展、定制和控制 Kubernetes 的所有主要机制。你处于一个很好的位置,可以利用这些能力来增强 Kubernetes 的现有功能,并使其适应你的需求和系统。

在下一章中,我们将看一下 Helm,Kubernetes 包管理器,以及它的图表。正如你可能已经意识到的那样,在 Kubernetes 上部署和配置复杂系统远非简单。Helm 允许将一堆清单组合成一个图表,可以作为单个单元安装。

第十三章:处理 Kubernetes 软件包管理器

在本章中,我们将深入了解 Helm,即 Kubernetes 软件包管理器。每个成功和重要的平台都必须有一个良好的打包系统。Helm 由 Deis 开发(于 2017 年 4 月被微软收购),后来直接贡献给了 Kubernetes 项目。我们将从理解 Helm 的动机、架构和组件开始。然后,我们将亲身体验并了解如何在 Kubernetes 中使用 Helm 及其图表。这包括查找、安装、自定义、删除和管理图表。最后但同样重要的是,我们将介绍如何创建自己的图表,并处理版本控制、依赖关系和模板化。

将涵盖以下主题:

  • 理解 Helm

  • 使用 Helm

  • 创建自己的图表

理解 Helm

Kubernetes 提供了许多在运行时组织和编排容器的方式,但缺乏将一组图像进行更高级别组织的方式。这就是 Helm 的用武之地。在本节中,我们将讨论 Helm 的动机、其架构和组件,并讨论从 Helm Classic 过渡到 Helm 时发生了什么变化。

Helm 的动机

Helm 支持几个重要的用例:

  • 管理复杂性

  • 轻松升级

  • 简单共享

  • 安全回滚

图表可以描述甚至最复杂的应用程序,提供可重复的应用程序安装,并作为单一的权威点。原地升级和自定义钩子允许轻松更新。共享图表很简单,可以在公共或私有服务器上进行版本控制和托管。当您需要回滚最近的升级时,Helm 提供了一个命令来回滚基础设施的一致变化集。

Helm 架构

Helm 旨在执行以下操作:

  • 从头开始创建新图表

  • 将图表打包成图表存档(tgz)文件

  • 与存储图表的图表存储库进行交互

  • 将图表安装和卸载到现有的 Kubernetes 集群中

  • 管理使用 Helm 安装的图表的发布周期

Helm 使用客户端-服务器架构来实现这些目标。

Helm 组件

Helm 有一个在 Kubernetes 集群上运行的服务器组件和一个在本地机器上运行的客户端组件。

Tiller 服务器

服务器负责管理发布。它与 Helm 客户端以及 Kubernetes API 服务器进行交互。其主要功能如下:

  • 监听来自 Helm 客户端的传入请求

  • 结合图表和配置以构建发布

  • 将图表安装到 Kubernetes 中

  • 跟踪后续发布

  • 通过与 Kubernetes 交互来升级和卸载图表

Helm 客户端

您在您的机器上安装 Helm 客户端。它负责以下工作:

  • 本地图表开发

  • 管理存储库

  • 与 Tiller 服务器交互

  • 发送图表以安装

  • 询问有关发布的信息

  • 请求升级或卸载现有版本

使用 Helm

Helm 是一个丰富的软件包管理系统,可以让您执行管理集群上安装的应用程序所需的所有必要步骤。让我们卷起袖子,开始吧。

安装 Helm

安装 Helm 涉及安装客户端和服务器。Helm 是用 Go 实现的,同一个二进制可用作客户端或服务器。

安装 Helm 客户端

您必须正确配置 Kubectl 以与您的 Kubernetes 集群通信,因为 Helm 客户端使用 Kubectl 配置与 Helm 服务器(Tiller)通信。

Helm 为所有平台提供二进制发布,网址为github.com/kubernetes/helm/releases/latest

对于 Windows,您也可以使用chocolatey软件包管理器,但它可能比官方版本慢一点,https://chocolatey.org/packages/kubernetes-helm/<version>

对于 macOS 和 Linux,您可以从脚本安装客户端:

$ curl https://raw.githubusercontent.com/kubernetes/helm/master/scripts/get > get_helm.sh
$ chmod 700 get_helm.sh
$ ./get_helm.sh  

在 macOS X 上,您还可以使用 Homebrew:

brew install kubernetes-helm  

安装 Tiller 服务器

Tiller 通常在集群内运行。对于开发来说,有时在本地运行 Tiller 会更容易一些。

在集群中安装 Tiller

安装 Tiller 的最简单方法是从安装了 Helm 客户端的机器上进行。运行以下命令:

helm init

这将在远程 Kubernetes 集群上初始化客户端和 Tiller 服务器。安装完成后,您将在集群的kube-system命名空间中有一个正在运行的 Tiller pod:

> kubectl get po --namespace=kube-system -l name=tiller
NAME                            READY  STATUS   RESTARTS   AGE
tiller-deploy-3210613906-2j5sh  1/1    Running  0          1m  

您还可以运行helm version来查看客户端和服务器的版本:

> helm version
Client: &version.Version{SemVer:"v2.2.3", GitCommit:"1402a4d6ec9fb349e17b912e32fe259ca21181e3", GitTreeState:"clean"}
Server: &version.Version{SemVer:"v2.2.3", GitCommit:"1402a4d6ec9fb349e17b912e32fe259ca21181e3", GitTreeState:"clean"} 

在本地安装 Tiller

如果要在本地运行 Tiller,首先需要构建它。这在 Linux 和 macOS 上都受支持:

> cd $GOPATH
> mkdir -p src/k8s.io
> cd src/k8s.io
> git clone https://github.com/kubernetes/helm.git
> cd helm
> make bootstrap build 

引导目标将尝试安装依赖项,重建vendor/树,并验证配置。

构建目标将编译 Helm 并将其放置在bin/helm中。Tiller 也被编译并放置在bin/tiller中。

现在您可以运行 bin/tiller。Tiller 将通过您的 Kubectl 配置连接到 Kubernetes 集群。

需要告诉 Helm 客户端连接到本地的 Tiller 服务器。您可以通过设置环境变量来实现:

> export HELM_HOST=localhost:44134 

否则,您可以将其作为命令行参数传递:--host localhost:44134

使用替代存储后端

Helm 2.7.0 添加了将发布信息存储为 secrets 的选项。早期版本总是将发布信息存储在 ConfigMaps 中。secrets 后端增加了图表的安全性。它是一种通用 Kubernetes 加密的补充。要使用 Secrets 后端,您需要使用以下命令行运行 Helm:

> helm init --override 'spec.template.spec.containers[0].command'='{/tiller,--storage=secret}' 

查找图表

为了使用 Helm 安装有用的应用程序和软件,您需要先找到它们的图表。这就是 helm search 命令发挥作用的地方。默认情况下,Helm 搜索官方的 Kubernetes chart 仓库,名为 stable

>  helm search
NAME                            VERSION     DESCRIPTION
stable/acs-engine-autoscaler     2.1.1      Scales worker nodes within agent pools
stable/aerospike                0.1.5       A Helm chart for Aerospike in Kubernetes
stable/artifactory             6.2.4        Universal Repository Manager supporting all maj...
stable/aws-cluster-autoscaler  0.3.2        Scales worker nodes within autoscaling groups.
stable/buildkite               0.2.0        Agent for Buildkite
stable/centrifugo              2.0.0        Centrifugo is a real-time messaging server.
stable/chaoskube               0.6.1        Chaoskube periodically kills random pods in you...
stable/chronograf              0.4.0        Open-source web application written in Go and R..
stable/cluster-autoscaler      0.3.1        Scales worker nodes within autoscaling groups. 

官方仓库拥有丰富的图表库,代表了所有现代开源数据库、监控系统、特定于 Kubernetes 的辅助工具,以及一系列其他提供,比如 Minecraft 服务器。您可以搜索特定的图表,例如,让我们搜索包含 kube 在其名称或描述中的图表:

> helm search kube
NAME                            VERSION    DESCRIPTION
stable/chaoskube                0.6.1     Chaoskube periodically kills random pods in you...
stable/kube-lego               0.3.0      Automatically requests certificates from Let's ...
stable/kube-ops-view           0.4.1      Kubernetes Operational View - read-only system ...
stable/kube-state-metrics      0.5.1      Install kube-state-metrics to generate and expo...
stable/kube2iam                0.6.1      Provide IAM credentials to pods based on annota...
stable/kubed                   0.1.0      Kubed by AppsCode - Kubernetes daemon
stable/kubernetes-dashboard    0.4.3      General-purpose web UI for Kubernetes clusters
stable/sumokube                0.1.1      Sumologic Log Collector
stable/aerospike               0.1.5      A Helm chart for Aerospike in Kubernetes
stable/coredns                 0.8.0      CoreDNS is a DNS server that chains plugins and...
stable/etcd-operator           0.6.2      CoreOS etcd-operator Helm chart for Kubernetes
stable/external-dns            0.4.4      Configure external DNS servers (AWS Route53...
stable/keel                    0.2.0      Open source, tool for automating Kubernetes dep...
stable/msoms                   0.1.1      A chart for deploying omsagent as a daemonset... 
stable/nginx-lego              0.3.0      Chart for nginx-ingress-controller and kube-lego
stable/openvpn                 2.0.2      A Helm chart to install an openvpn server insid...
stable/risk-advisor            2.0.0      Risk Advisor add-on module for Kubernetes
stable/searchlight             0.1.0      Searchlight by AppsCode - Alerts for Kubernetes
stable/spartakus               1.1.3      Collect information about Kubernetes clusters t...
stable/stash                   0.2.0      Stash by AppsCode - Backup your Kubernetes Volumes
stable/traefik                 1.15.2     A Traefik based Kubernetes ingress controller w...
stable/voyager                 2.0.0       Voyager by AppsCode - Secure Ingress Controller...
stable/weave-cloud             0.1.2      Weave Cloud is a add-on to Kubernetes which pro...
stable/zetcd                   0.1.4      CoreOS zetcd Helm chart for Kubernetes
stable/buildkite               0.2.0      Agent for Buildkite 

让我们尝试另一个搜索:

> helm search mysql
NAME                      VERSION    DESCRIPTION
stable/mysql              0.3.4      Fast, reliable, scalable, and easy to use open-...
stable/percona            0.3.0      free, fully compatible, enhanced, open source d...
stable/gcloud-sqlproxy    0.2.2      Google Cloud SQL Proxy
stable/mariadb            2.1.3       Fast, reliable, scalable, and easy to use open-... 

发生了什么?为什么 mariadb 出现在结果中?原因是 mariadb(它是 MySQL 的一个分支)在其描述中提到了 MySQL,即使在截断的输出中看不到。要获取完整的描述,请使用 helm inspect 命令:

> helm inspect stable/mariadb
appVersion: 10.1.30
description: Fast, reliable, scalable, and easy to use open-source relational database
 system. MariaDB Server is intended for mission-critical, heavy-load production systems
 as well as for embedding into mass-deployed software.
engine: gotpl
home: https://mariadb.org
icon: https://bitnami.com/assets/stacks/mariadb/img/mariadb-stack-220x234.png
keywords:
- mariadb
- mysql
- database
- sql
- prometheus
maintainers:
- email: containers@bitnami.com
 name: bitnami-bot
name: mariadb
sources:
- https://github.com/bitnami/bitnami-docker-mariadb
- https://github.com/prometheus/mysqld_exporter
version: 2.1.3

安装软件包

好了。您找到了梦想的软件包。现在,您可能想要在 Kubernetes 集群上安装它。当您安装一个软件包时,Helm 会创建一个发布,您可以使用它来跟踪安装进度。让我们使用 helm install 命令安装 MariaDB。让我们详细查看输出。输出的第一部分列出了发布的名称 - 在这种情况下是 cranky-whippet(您可以使用 --name 标志选择自己的名称)、命名空间和部署状态:

> helm install stable/mariadb
NAME:   cranky-whippet
LAST DEPLOYED: Sat Mar 17 10:21:21 2018
NAMESPACE: default
STATUS: DEPLOYED  

输出的第二部分列出了此图表创建的所有资源。请注意,资源名称都是根据发布名称派生的:

RESOURCES:
==> v1/Service
NAME                    TYPE       CLUSTER-IP      EXTERNAL-IP  PORT(S)   AGE
cranky-whippet-mariadb  ClusterIP  10.106.206.108  <none>       3306/TCP  1s

==> v1beta1/Deployment
NAME                    DESIRED  CURRENT  UP-TO-DATE  AVAILABLE  AGE
cranky-whippet-mariadb  1        1        1           0          1s

==> v1/Pod(related)
NAME                                     READY  STATUS    RESTARTS  AGE
cranky-whippet-mariadb-6c85fb4796-mttf7  0/1    Init:0/1  0         1s

==> v1/Secret
NAME                    TYPE    DATA  AGE
cranky-whippet-mariadb  Opaque  2     1s

==> v1/ConfigMap
NAME                          DATA  AGE
cranky-whippet-mariadb        1     1s
cranky-whippet-mariadb-tests  1     1s

==> v1/PersistentVolumeClaim
NAME                    STATUS  VOLUME                                    CAPACITY  ACCESS MODES  STORAGECLASS  AGE
cranky-whippet-mariadb  Bound   pvc-9cb7e176-2a07-11e8-9bd6-080027c94384  8Gi       RWO           standard      1s  

最后一部分是提供如何在 Kubernetes 集群中使用 MariaDB 的易于理解的说明:

NOTES:
MariaDB can be accessed via port 3306 on the following DNS name from within your cluster:
cranky-whippet-mariadb.default.svc.cluster.local

To get the root password run:

MARIADB_ROOT_PASSWORD=$(kubectl get secret --namespace default cranky-whippet-mariadb -o jsonpath="{.data.mariadb-root-password}" | base64 --decode)

To connect to your database:

1\. Run a pod that you can use as a client:

kubectl run cranky-whippet-mariadb-client --rm --tty -i --env MARIADB_ROOT_PASSWORD=$MARIADB_ROOT_PASSWORD --image bitnami/mariadb --command -- bash

2\. Connect using the mysql cli, then provide your password:
mysql -h cranky-whippet-mariadb -p$MARIADB_ROOT_PASSWORD

检查安装状态

Helm 不会等待安装完成,因为这可能需要一些时间。helm status命令以与初始helm install命令的输出相同的格式显示发布的最新信息。在install命令的输出中,您可以看到PersistentVolumeClaimPENDING状态。现在让我们来检查一下:

> helm status cranky-whippet | grep Persist -A 3
==> v1/PersistentVolumeClaim
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS  AGE
cranky-whippet-mariadbBoundpvc-9cb7e176-2a07-11e8-9bd6-080027c943848Gi             RWO                         standard                   5m  

万岁!它现在已绑定,并且附加了一个容量为 8GB 的卷。

让我们尝试连接并验证mariadb是否确实可访问。让我们稍微修改一下注释中建议的命令以进行连接。我们可以直接在容器上运行mysql命令,而不是运行bash然后再运行mysql

> kubectl run cranky-whippet-mariadb-client --rm --tty -i --image bitnami/mariadb --command -- mysql -h cranky-whippet-mariadb  

如果您看不到命令提示符,请尝试按Enter键。

MariaDB [(none)]> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
+--------------------+
3 rows in set (0.00 sec) 

自定义图表

作为用户,您经常希望自定义或配置您安装的图表。Helm 完全支持通过config文件进行自定义。要了解可能的自定义,您可以再次使用helm inspect命令,但这次要专注于值。以下是部分输出:

> helm inspect values stable/mariadb
## Bitnami MariaDB image version
## ref: https://hub.docker.com/r/bitnami/mariadb/tags/
##
## Default: none
image: bitnami/mariadb:10.1.30-r1

## Specify an imagePullPolicy (Required)
## It's recommended to change this to 'Always' if the image tag is 'latest'
## ref: http://kubernetes.io/docs/user-guide/images/#updating-images
imagePullPolicy: IfNotPresent

## Use password authentication
usePassword: true

## Specify password for root user
## Defaults to a random 10-character alphanumeric string if not set and usePassword is true
## ref: https://github.com/bitnami/bitnami-docker-mariadb/blob/master/README.md#setting-the-root-password-on-first-run
##
# mariadbRootPassword:

## Create a database user
## Password defaults to a random 10-character alphanumeric string if not set and usePassword is true
## ref: https://github.com/bitnami/bitnami-docker-mariadb/blob/master/README.md#creating-a-database-user-on-first-run
##
# mariadbUser:
# mariadbPassword:

## Create a database
## ref: https://github.com/bitnami/bitnami-docker-mariadb/blob/master/README.md#creating-a-database-on-first-run
##
# mariadbDatabase:  

例如,如果您想在安装mariadb时设置根密码并创建数据库,您可以创建以下 YAML 文件并将其保存为mariadb-config.yaml

mariadbRootPassword: supersecret
mariadbDatabase: awesome_stuff 

然后,运行helm并传递yaml文件:

> helm install -f config.yaml stable/mariadb  

您还可以使用--set在命令行上设置单个值。如果--f--set都尝试设置相同的值,则--set优先。例如,在这种情况下,根密码将是evenbettersecret

helm install -f config.yaml --set mariadbRootPassword=evenbettersecret stable/mariadb 

您可以使用逗号分隔的列表指定多个值:--set a=1,b=2

其他安装选项

helm install命令可以从多个来源安装:

  • 一个chart repository(正如我们所见)

  • 本地图表存档(helm install foo-0.1.1.tgz

  • 一个解压的chart目录(helm install path/to/foo

  • 完整的 URL(helm install https://example.com/charts/foo-1.2.3.tgz

升级和回滚发布

您可能希望将安装的软件包升级到最新版本。Helm 提供了upgrade命令,它可以智能地操作,并且只更新已更改的内容。例如,让我们检查我们mariadb安装的当前值:

> helm get values cranky-whippet
mariadbDatabase: awesome_stuff
mariadbRootPassword: evenbettersecret 

现在,让我们运行、升级并更改数据库的名称:

> helm upgrade cranky-whippet --set mariadbDatabase=awesome_sauce stable/mariadb
$ helm get values cranky-whippet
mariadbDatabase: awesome_sauce 

请注意,我们已经丢失了root密码。当您升级时,所有现有值都将被替换。好的,让我们回滚。helm history命令显示了我们可以回滚到的所有可用修订版本:

> helm history cranky-whippet
REVISION         STATUS           CHART            DESCRIPTION
1               SUPERSEDED      mariadb-2.1.3      Install complete
2               SUPERSEDED      mariadb-2.1.3      Upgrade complete
3               SUPERSEDED      mariadb-2.1.3      Upgrade complete
4               DEPLOYED        mariadb-2.1.3      Upgrade complete  

让我们回滚到修订版本3

> helm rollback cranky-whippet 3
Rollback was a success! Happy Helming!

> helm history cranky-whippet
REVISION        STATUS             CHART            DESCRIPTION
1                SUPERSEDED      mariadb-2.1.3     Install complete
2                SUPERSEDED      mariadb-2.1.3     Upgrade complete
3                SUPERSEDED      mariadb-2.1.3     Upgrade complete
4                SUPERSEDED      mariadb-2.1.3     Upgrade complete 
5                DEPLOYED        mariadb-2.1.3     Rollback to 3   

让我们验证一下我们的更改是否已回滚:

> helm get values cranky-whippet
mariadbDatabase: awesome_stuff
mariadbRootPassword: evenbettersecret  

删除发布

当然,您也可以使用helm delete命令删除一个发布。

首先,让我们检查发布的列表。我们只有cranky-whippet

> helm list
NAME            REVISION     STATUS      CHART           NAMESPACE
cranky-whippet    5         DEPLOYED   mariadb-2.1.3      default  

现在,让我们删除它:

> helm delete cranky-whippet 
release "cranky-whippet" deleted 

所以,没有更多的发布了:

> helm list 

但是,Helm 也会跟踪已删除的发布。您可以使用--all标志查看它们:

> helm list --all
NAME                 REVISION  STATUS    CHART          NAMESPACE
cranky-whippet        5        DELETED   mariadb-2.1.3  default 

要完全删除一个发布,添加--purge标志:

> helm delete --purge cranky-whippet   

使用存储库

Helm 将图表存储在简单的 HTTP 服务器存储库中。任何标准的 HTTP 服务器都可以托管 Helm 存储库。在云中,Helm 团队验证了 AWS S3 和 Google Cloud 存储都可以在 Web 启用模式下作为 Helm 存储库。Helm 还附带了一个用于开发人员测试的本地包服务器。它在客户端机器上运行,因此不适合共享。在一个小团队中,您可以在本地网络上的共享机器上运行 Helm 包服务器,所有团队成员都可以访问。

要使用本地包服务器,请键入helm serve。请在单独的终端窗口中执行此操作,因为它会阻塞。Helm 将默认从~/.helm/repository/local开始提供图表服务。您可以将您的图表放在那里,并使用helm index生成索引文件。

生成的index.yaml文件列出了所有的图表。

请注意,Helm 不提供将图表上传到远程存储库的工具,因为这将需要远程服务器了解 Helm,知道在哪里放置图表,以及如何更新index.yaml文件。

在客户端方面,helm repo命令允许您listaddremoveindexupdate

> helm repo  

该命令由多个子命令组成,用于与chart存储库交互。

它可以用来addremovelistindex图表存储库:

  • 示例用法
$ helm repo add [NAME] [REPO_URL]
  • 用法
helm repo [command]
  • 可用命令
  add         add a chart repository
  index       generate an index file for a given a directory 
  list        list chart repositories
  remove      remove a chart repository
  update     update information on available charts 

使用 Helm 管理图表

Helm 提供了几个命令来管理图表。它可以为您创建一个新的图表:

> helm create cool-chart
Creating cool-chart  

Helm 将在cool-chart下创建以下文件和目录:

-rw-r--r--  1 gigi.sayfan  gigi.sayfan   333B Mar 17 13:36 .helmignore
-rw-r--r--  1 gigi.sayfan  gigi.sayfan   88B Mar 17 13:36 Chart.yaml
drwxr-xr-x  2 gigi.sayfan  gigi.sayfan   68B Mar 17 13:36 charts
drwxr-xr-x  7 gigi.sayfan  gigi.sayfan   238B Mar 17 13:36 templates
-rw-r--r--  1 gigi.sayfan  gigi.sayfan   1.1K Mar 17 13:36 values.yaml 

编辑图表后,您可以将其打包成一个 targzipped存档:

> helm package cool-chart  

Helm 将创建一个名为cool-chart-0.1.0.tgz的存档,并将两者存储在local目录和local repository中。

您还可以使用 helm 来帮助您找到图表格式或信息的问题:

> helm lint cool-chart
==> Linting cool-chart
[INFO] Chart.yaml: icon is recommended

1 chart(s) linted, no failures  

利用入门包

helm create命令带有一个可选的--starter标志,让您指定一个入门图表。

启动器是位于$HELM_HOME/starters中的常规图表。作为图表开发者,您可以编写专门用作启动器的图表。这样的图表应该考虑以下几点:

  • Chart.yaml将被生成器覆盖

  • 用户希望修改这样一个图表的内容,因此文档应该说明用户如何做到这一点

目前,没有办法将图表安装到$HELM_HOME/starters,用户必须手动复制。如果您开发启动包图表,请确保在您的图表文档中提到这一点。

创建您自己的图表

图表是描述一组相关的 Kubernetes 资源的文件集合。一个单独的图表可以用来部署一些简单的东西,比如一个memcached pod,或者一些复杂的东西,比如一个完整的 Web 应用堆栈,包括 HTTP 服务器、数据库和缓存。

图表是以特定目录树布局的文件创建的。然后,它们可以被打包成版本化的存档进行部署。关键文件是Chart.yaml

Chart.yaml 文件

Chart.yaml文件是 Helm 图表的主文件。它需要一个名称和版本字段:

  • name: 图表的名称(与目录名称相同)

  • version: SemVer 2 版本

它还可以包含各种可选字段:

  • kubeVersion: 兼容的 Kubernetes 版本的 SemVer 范围

  • description: 该项目的单句描述

  • keywords: 关于这个项目的关键字列表

  • home: 该项目主页的 URL

  • sources: 该项目源代码的 URL 列表

  • maintainers:

  • name: 维护者的名称(每个维护者都需要)

  • email: 维护者的电子邮件(可选)

  • url: 维护者的 URL(可选)

  • engine: 模板引擎的名称(默认为gotpl

  • icon: 用作图标的 SVG 或 PNG 图像的 URL

  • appVersion: 包含的应用程序版本

  • deprecated: 这个图表是否已被弃用?(布尔值)

  • tillerVersion: 该图表所需的 Tiller 版本

图表版本控制

Chart.yaml中的版本字段由 CLI 和 Tiller 服务器使用。helm package命令将使用在Chart.yaml中找到的版本来构建包名。图表包名中的版本号必须与Chart.yaml中的版本号匹配。

appVersion 字段

appVersion字段与版本字段无关。Helm 不使用它,它作为用户的元数据或文档,用于了解他们正在部署的内容。Helm 不强制正确性。

弃用图表

有时,您可能希望弃用一个图表。您可以通过将Chart.yaml中的弃用字段设置为true来标记图表为弃用状态。弃用最新版本的图表就足够了。稍后您可以重用图表名称并发布一个未弃用的新版本。kubernetes/charts项目使用的工作流程是:

  • 更新图表的Chart.yaml以标记图表为弃用状态并提升版本

  • 发布图表的新版本

  • 源代码库中删除图表

图表元数据文件

图表可能包含各种元数据文件,例如README.mdLICENSENOTES.txt,用于描述图表的安装、配置、使用和许可。README.md文件应格式化为 markdown。它应提供以下信息:

  • 图表提供的应用程序或服务的描述

  • 运行图表的任何先决条件或要求

  • values.yaml中选项的描述和默认值

  • 安装或配置图表的任何其他信息

templates/NOTES.txt文件将在安装后或查看发布状态时显示。您应该保持NOTES简洁,并指向README.md以获取详细说明。通常会放置使用说明和下一步操作,例如有关连接到数据库或访问 Web UI 的信息。

管理图表依赖关系

在 Helm 中,一个图表可以依赖任意数量的其他图表。通过在requirements.yaml文件中列出它们或在安装期间将依赖图表复制到 charts/子目录中来明确表示这些依赖关系。

依赖可以是图表存档(foo-1.2.3.tgz)或未解压的图表目录。但是,其名称不能以_.开头。图表加载程序会忽略这样的文件。

使用requirements.yaml管理依赖关系

不要手动将图表放在charts/子目录中,最好使用requirements.yaml文件在图表内声明依赖关系。

requirements.yaml文件是一个简单的文件,用于列出图表的依赖关系:

dependencies:
 - name: foo
   version: 1.2.3
   repository: http://example.com/charts
 - name: bar
   version: 4.5.6
   repository: http://another.example.com/charts

name字段是您想要的图表名称。

version字段是您想要的图表版本。

repository字段是指向图表存储库的完整 URL。请注意,您还必须使用helm repo将该存储库添加到本地。

一旦您有了一个依赖文件,您可以运行helm dep up,它将使用您的依赖文件将所有指定的图表下载到 charts 子目录中:

$ helm dep up foo-chart
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "local" chart repository
...Successfully got an update from the "stable" chart repository
...Successfully got an update from the "example" chart repository
...Successfully got an update from the "another" chart repository
Update Complete. Happy Helming!
Saving 2 charts
Downloading Foo from repo http://example.com/charts
Downloading Bar from repo http://another.example.com/charts

Helm 存储依赖图表在charts/目录中作为图表存档进行检索。对于前面的示例,这些文件将存在于charts目录中:

charts/
  foo-1.2.3.tgz
  bar-4.5.6.tgz 

使用requirements.yaml管理图表及其依赖项是最佳实践,既可以明确记录依赖关系,也可以在团队之间共享,并支持自动化流程。

在 requirements.yaml 中使用特殊字段

requirements.yaml文件中的每个条目还可以包含可选的fields标签和条件。

这些字段可用于动态控制图表的加载(默认情况下,所有图表都会加载)。当存在标签或条件时,Helm 将评估它们并确定是否应加载目标图表:

  • conditioncondition字段包含一个或多个 YAML 路径(用逗号分隔)。如果此路径存在于顶级父级的值中并解析为布尔值,则图表将根据该布尔值启用或禁用。仅评估列表中找到的第一个有效路径,如果没有路径存在,则条件不起作用。

  • tagstags字段是一个 YAML 标签列表,用于与该图表关联。在顶级父级的值中,可以通过指定标签和布尔值来启用或禁用具有标签的所有图表。

  • 以下是一个很好地利用条件和标签来启用和禁用依赖项安装的requirements.yamlvalues.yaml示例。requirements.yaml文件根据global enabled字段的值和特定的sub-charts enabled字段定义了安装其依赖项的两个条件:

    # parentchart/requirements.yaml
    dependencies:
          - name: subchart1
            repository: http://localhost:10191
            version: 0.1.0
            condition: subchart1.enabled, global.subchart1.enabled
            tags:
              - front-end
              - subchart1
          - name: subchart2
            repository: http://localhost:10191
            version: 0.1.0
            condition: subchart2.enabled,global.subchart2.enabled
            tags:
              - back-end
              - subchart2 

values.yaml文件为一些条件变量分配了值。subchart2标签没有获得值,因此被认为是启用的:

# parentchart/values.yaml
 subchart1:
   enabled: true
  tags:
   front-end: false
   back-end: true

在安装图表时,也可以从命令行设置标签和条件值,并且它们将优先于values.yaml文件:

helm install --set subchart2.enabled=false

标签和条件的解析如下:

  • 条件(在值中设置)始终会覆盖标签。存在的第一个条件路径获胜,该图表的后续条件将被忽略。

  • 如果图表的任何标签为 true,则启用该图表。

  • 标签和条件值必须在顶级父值中设置。

  • 值中的标签:键必须是顶级键。不支持全局和嵌套标签。

使用模板和值

任何重要的应用程序都需要配置和适应特定的用例。Helm 图表是使用 Go 模板语言填充占位符的模板。Helm 支持来自Sprig库和其他一些专门函数的附加功能。模板文件存储在图表的templates/子目录中。Helm 将使用模板引擎渲染此目录中的所有文件,并应用提供的值文件。

编写模板文件

模板文件只是遵循 Go 模板语言规则的文本文件。它们可以生成 Kubernetes 配置文件。以下是 artifactory 图表中的服务模板文件:

kind: Service
apiVersion: v1
kind: Service
metadata:
  name: {{ template "artifactory.fullname" . }}
  labels:
    app: {{ template "artifactory.name" . }}
    chart: {{ .Chart.Name }}-{{ .Chart.Version }}
    component: "{{ .Values.artifactory.name }}"
    heritage: {{ .Release.Service }}
    release: {{ .Release.Name }}
{{- if .Values.artifactory.service.annotations }}
  annotations:
{{ toYaml .Values.artifactory.service.annotations | indent 4 }}
{{- end }}
spec:
  type: {{ .Values.artifactory.service.type }}
  ports:
  - port: {{ .Values.artifactory.externalPort }}
    targetPort: {{ .Values.artifactory.internalPort }}
    protocol: TCP
    name: {{ .Release.Name }}
  selector:
    app: {{ template "artifactory.name" . }}
    component: "{{ .Values.artifactory.name }}"
    release: {{ .Release.Name }}

使用管道和函数

Helm 允许在模板文件中使用内置的 Go 模板函数、sprig 函数和管道的丰富和复杂的语法。以下是一个利用这些功能的示例模板。它使用 repeat、quote 和 upper 函数来处理 food 和 drink 键,并使用管道将多个函数链接在一起:

apiVersion: v1 
kind: ConfigMap 
metadata: 
  name: {{ .Release.Name }}-configmap 
data: 
  greeting: "Hello World" 
  drink: {{ .Values.favorite.drink | repeat 3 | quote }} 
  food: {{ .Values.favorite.food | upper | quote }}  

查看值文件是否具有以下部分:

favorite: 
  drink: coffee 
  food: pizza 

如果是,则生成的图表将如下所示:

apiVersion: v1 
kind: ConfigMap 
metadata: 
  name: cool-app-configmap 
data: 
  greeting: "Hello World" 
  drink: "coffeecoffeecoffee" 
  food: "PIZZA" 

嵌入预定义值

Helm 提供了一些预定义的值,您可以在模板中使用。在先前的 artifactory 图表模板中,Release.NameRelease.ServiceChart.NameChart.Version是 Helm 预定义值的示例。其他预定义值如下:

  • Release.Time

  • Release.Namespace

  • Release.IsUpgrade

  • Release.IsInstall

  • Release.Revision

  • Chart

  • Files

  • Capabilities

图表是Chart.yaml的内容。文件和功能预定义值是类似于映射的对象,允许通过各种函数进行访问。请注意,模板引擎会忽略Chart.yaml中的未知字段,并且无法用于传递任意结构化数据到模板中。

从文件中提供值

这是artifactory默认值文件的一部分。该文件中的值用于填充多个模板。例如,先前的服务模板中使用了artifactory nameinternalPort的值:

artifactory:
  name: artifactory
   replicaCount: 1
   image:
   # repository: "docker.bintray.io/jfrog/artifactory-oss"
   repository: "docker.bintray.io/jfrog/artifactory-pro"
   version: 5.9.1
   pullPolicy: IfNotPresent
    service:
     name: artifactory
      type: ClusterIP
      annotations: {}
      externalPort: 8081
      internalPort: 8081
      persistence:
        mountPath: "/var/opt/jfrog/artifactory"
        enabled: true
        accessMode: ReadWriteOnce
        size: 20Gi 

您可以提供自己的 YAML 值文件来在安装命令期间覆盖默认值:

> helm install --values=custom-values.yaml gitlab-ce 

范围、依赖和值

值文件可以声明顶层图表的值,以及该图表的charts/目录中包含的任何图表的值。例如,artifactory-ce values.yaml文件包含其依赖图表postgresql的一些默认值:

## Configuration values for the postgresql dependency
## ref: https://github.com/kubernetes/charts/blob/master/stable/postgressql/README.md
##
postgresql:
postgresUser: "artifactory"
postgresPassword: "artifactory"
postgresDatabase: "artifactory"
persistence:
 enabled: true

顶层图表可以访问其依赖图表的值,但反之则不行。还有一个全局值可供所有图表访问。例如,您可以添加类似以下内容:

global:
 app: cool-app  

当全局存在时,它将被复制到每个依赖图表的值中,如下所示:

global:
  app: cool-app

 postgresql:
   global:
     app: cool-app
     ...  

总结

在本章中,我们看了一下 Helm,Kubernetes 的包管理器。Helm 使 Kubernetes 能够管理由许多 Kubernetes 资源组成的复杂软件,这些资源之间存在相互依赖。它的作用与操作系统的包管理器相同。它组织软件包,让您搜索图表,安装和升级图表,并与合作者共享图表。您可以开发自己的图表并将它们存储在存储库中。

在这一点上,您应该了解 Helm 在 Kubernetes 生态系统和社区中的重要作用。您应该能够有效地使用它,甚至开发和分享您自己的图表。

在下一章中,我们将展望 Kubernetes 的未来,审查其路线图以及我愿望清单中的一些个人项目。

第十四章:Kubernetes 的未来

在这一章中,我们将从多个角度探讨 Kubernetes 的未来。我们将从路线图和即将推出的产品功能开始,包括深入探讨 Kubernetes 的设计过程。然后,我们将涵盖 Kubernetes 自诞生以来的动力,包括社区、生态系统和知名度等方面。Kubernetes 未来的一个重要部分将取决于它在竞争中的表现。教育也将发挥重要作用,因为容器编排是一个新的、快速发展的、并且不是一个被充分理解的领域。然后,我们将讨论我心中最希望的一个功能——动态插件。

涵盖的主题如下:

  • 前方的道路

  • 竞争

  • Kubernetes 的动力

  • 教育和培训

  • 模块化和树外插件

  • 服务网格和无服务器框架

前方的道路

Kubernetes 是一个庞大的开源项目。让我们来看看一些计划中的功能和即将推出的版本,以及专注于特定领域的各种特别兴趣小组。

Kubernetes 发布和里程碑

Kubernetes 有相当规律的发布。截至 2018 年 4 月,当前版本是 1.10。下一个版本 1.11 目前已经完成 33%。以下是 1.11 版本的一些问题,让你了解正在进行的工作:

  • 更新到 Go 1.10.1 并将默认的etcd服务器更新到 3.2

  • 支持树外认证提供者

  • 将 kublet 标志迁移到kublet.config.k8s.io

  • 添加对 Azure 标准负载均衡器和公共 IP 的支持

  • 添加 kubectl api-resources命令

  • 次要版本每 3 个月发布一次,补丁版本填补漏洞和问题,直到下一个次要版本。以下是最近三个版本的发布日期:

  • 10.0 版本于 2018 年 3 月 26 日发布,1.9.6 版本于 2018 年 3 月 21 日发布

  • 9.0 版本于 2017 年 12 月 15 日发布,1.8.5 版本于 2017 年 12 月 7 日发布

  • 8.0 和 1.7.7 版本于 2017 年 9 月 28 日发布(我的生日!)

了解即将推出的内容的另一个好方法是查看 alpha 和 beta 版本的工作。您可以在这里查看更改日志:github.com/kubernetes/kubernetes/blob/master/CHANGELOG.md

以下是 1.10 版本的一些主要主题:

  • 节点

  • 网络

  • 存储

  • Windows

  • OpenStack

  • API 机械

  • 认证

  • Azure

  • CLI

Kubernetes 特别兴趣和工作组

作为一个大型的开源社区项目,Kubernetes 的大部分开发工作都是在多个工作组中进行的。完整的列表在这里:

github.com/kubernetes/community/blob/master/sig-list.md

未来版本的规划主要在这些 SIG 和工作组内进行,因为 Kubernetes 太大了,无法集中处理。SIG 定期会面并讨论。

竞争

《精通 Kubernetes》的第一版于 2017 年 5 月出版。当时,Kubernetes 的竞争格局完全不同。以下是我当时写的内容:

"Kubernetes 在容器编排技术领域是最热门的之一。Kubernetes 的未来必须作为整个市场的一部分来考虑。正如你将看到的,一些可能的竞争对手也可能是促进他们自己的产品以及 Kubernetes 的合作伙伴(或至少 Kubernetes 可以在他们的平台上运行)"。

不到一年的时间,情况发生了巨大变化。简而言之,Kubernetes 获胜了。所有云提供商都提供托管的 Kubernetes 服务。IBM 为裸机集群上的 Kubernetes 提供支持。开发容器编排软件和附加组件的公司都专注于 Kubernetes,而不是创建支持多种编排解决方案的产品。

捆绑的价值

容器编排平台如 Kubernetes 直接和间接地与更大和更小的范围竞争。例如,Kubernetes 可能在特定的云平台上可用,比如 AWS,但可能不是默认/首选解决方案。另一方面,Kubernetes 是 Google 云平台上 GKE 的核心。选择更高级抽象级别的开发人员,比如云平台或甚至 PaaS,往往会选择默认解决方案。但一些开发人员或组织担心供应商锁定或需要在多个云平台或混合公共/私有上运行。Kubernetes 在这里有很大的优势。捆绑曾经是 Kubernetes 采用的一个潜在严重威胁,但势头太大了,现在每个主要参与者都直接在他们的平台或解决方案上提供 Kubernetes。

Docker Swarm

Docker 目前是容器的事实标准(尽管 CoreOS rkt 正在崭露头角),人们经常在说容器时指的是 Docker。Docker 希望在编排领域分一杯羹,并发布了 Docker Swarm 产品。Docker Swarm 的主要优点是它作为 Docker 安装的一部分,并使用标准的 Docker API。因此,学习曲线不会那么陡峭,更容易上手。然而,Docker Swarm 在功能和成熟度方面远远落后于 Kubernetes。此外,当涉及到高质量工程和安全性时,Docker 的声誉并不好。关心系统稳定性的组织和开发人员可能会远离 Docker Swarm。Docker 意识到了这个问题,并正在采取措施加以解决。它发布了企业版,并通过 Moby 项目重塑了 Docker 的内部作为一组独立的组件。但是,最近 Docker 承认了 Kubernetes 作为容器编排平台的重要地位。Docker 现在直接支持 Kubernetes 和 Docker Swarm 并存。我猜想 Docker Swarm 会逐渐消失,只会用于非常小的原型。

Mesos/Mesosphere

Mesosphere 是开源 Apache Mesos 背后的公司,DC/OS 产品是在云中运行容器和大数据的现有产品。该技术已经成熟,Mesosphere 在不断发展,但他们没有 Kubernetes 拥有的资源和动力。我相信 Mesosphere 会做得很好,因为这是一个大市场,但它不会威胁 Kubernetes 作为头号容器编排解决方案。此外,Mesosphere 也意识到他们无法击败 Kubernetes,并选择加入它。在 DC/OS 1.11 中,您可以获得 Kubernetes 即服务。DC/OS 提供的是一个高可用、易于设置和默认安全的 Kubernetes 部署,经过了在 Google、AWS 和 Azure 上的测试。

云平台

许多组织和开发人员涌向公共云平台,以避免基础设施的低级管理带来的麻烦。这些公司的主要动机通常是快速前进,专注于他们的核心竞争力。因此,他们通常会选择云提供商提供的默认部署解决方案,因为集成是最无缝和流畅的。

AWS

通过官方的 Kubernetes Kops 项目,Kubernetes 在 AWS 上运行得非常好:github.com/kubernetes/kops

Kops 的一些特性如下:

  • 自动化在 AWS 中部署 Kubernetes 集群

  • 部署高可用的 Kubernetes 主节点

  • 生成 Terraform 配置的能力

然而,Kops 不是官方的 AWS 解决方案。如果您通过 AWS 控制台和 API 管理基础设施,最简单的方法过去是 AWS 弹性容器服务ECS)——这是一个不基于 Kubernetes 的内置容器编排解决方案。

现在,AWS 完全致力于 Kubernetes,并正在发布弹性 Kubernetes 服务EKS),这是一个完全托管且高可用的上游 Kubernetes 集群,没有修改,但通过附加组件和插件与 AWS 服务紧密集成。

我在第一版中推测 AWS 会坚守立场,支持 ECS,但我错了。即使强大的 AWS 也向 Kubernetes 让步;ECS 会继续存在,因为许多组织已经投资其中,可能不想迁移到 Kubernetes。然而,我预测随着时间的推移,ECS 将被降级为传统服务状态,维护以支持那些没有足够动力迁移到 Kubernetes 的组织。

Azure

Azure 提供了 Azure 容器服务,他们不偏袒。您可以选择是否要使用 Kubernetes、Docker Swarm 或 DC/OS。这很有趣,因为最初,Azure 是基于 Mesosphere DC/OS 的,后来他们添加了 Kubernetes 和 Docker Swarm 作为编排选项。随着 Kubernetes 在功能、成熟度和知名度上的提升,我相信它也将成为 Azure 上的头号编排选项。

在 2017 年下半年,Azure 正式发布了Azure Kubernetes 服务AKS),微软完全支持 Kubernetes 作为容器编排解决方案。它在 Kubernetes 社区非常活跃,收购了 Deis(Helm 的开发者),并贡献了许多工具、代码修复和集成。对 Kubernetes 的 Windows 支持也在不断改进,与 Azure 的集成也在不断加强。

阿里巴巴云

阿里云在很多方面都是中国的 AWS。他们的 API 故意设计得非常像 AWS 的 API。阿里云曾经提供基于 Docker Swarm 的容器管理服务。我在阿里云上部署了一些小规模的应用,他们似乎能够跟上领域的变化并迅速跟随大公司。在过去的一年里,阿里云加入了 Kubernetes 支持者的行列。在阿里云上部署和管理 Kubernetes 集群的资源有几个,包括云提供商接口的 GitHub 实现。

Kubernetes 势头

Kubernetes 背后有巨大的势头;社区非常强大。随着 Kubernetes 的知名度增加,用户纷纷涌向 Kubernetes,技术媒体承认其领导地位,生态系统火热,许多大公司(除了谷歌)积极支持它,还有许多公司在评估并在生产中运行它。

社区

Kubernetes 社区是其最大的资产之一。Kubernetes 最近成为第一个从云原生计算基金会(CNCF)毕业的项目。

GitHub

Kubernetes 在 GitHub 上开发,是 GitHub 上排名靠前的项目之一。它在星星数量上排在前 0.01%,在活跃度上排名第一。请注意,过去一年,Kubernetes 变得更加模块化,现在许多部分都是分开开发的。

更多专业人士在他们的 LinkedIn 档案中列出 Kubernetes,比其他类似产品多得多。

一年前,Kubernetes 有大约 1,100 名贡献者和大约 34,000 次提交。现在,这个数字激增到了超过 1,600 名贡献者和超过 63,000 次提交。

会议和聚会

Kubernetes 势头的另一个指标是会议、聚会和参与者的数量。KubeCon 迅速增长,新的 Kubernetes 聚会每天都在开展。

思维份额

Kubernetes 受到了很多关注和部署。进入容器/DevOps/微服务领域的大大小小公司都采用 Kubernetes,趋势是明显的。一个有趣的指标是随着时间推移,Stack Overflow 上的问题数量。社区积极回答问题并促进合作。其增长超过了竞争对手,趋势非常明显:

生态系统

Kubernetes 生态系统非常令人印象深刻,从云提供商到 PaaS 平台和提供简化环境的初创公司。

公共云提供商

所有主要的云提供商都直接支持 Kubernetes。显然,谷歌在 GKE 方面处于领先地位,这是谷歌云平台上的本地容器引擎。前面提到的 Kops 项目是 AWS 上一个得到良好支持、维护和记录的解决方案,EKS 即将推出。Azure 提供 AKS。IBM 的容器云服务由 Kubernetes 提供支持。Oracle 密切关注 Kubernetes,并基于上游 Kubernetes 和 Kubeadm 提供 Oracle 容器服务。

OpenShift

OpenShift 是 RedHat 的容器应用产品,构建在开源的 OpenShift origin 之上,后者基于 Kubernetes。OpenShift 在 Kubernetes 之上增加了应用程序生命周期管理和 DevOps 工具,并为 Kubernetes 做出了很多贡献(如自动扩展)。这种互动非常健康和鼓舞人心。RedHat 最近收购了 CoreOS,CoreOS Tectonic 与 OpenShift 的合并可能会产生很大的协同效应。

OpenStack

OpenStack 是开源的私有云平台,最近决定将 Kubernetes 作为底层编排平台进行标准化。这是一件大事,因为希望在公有云和私有云混合部署的大型企业将会更好地与 Kubernetes 云联邦和以 Kubernetes 为私有云平台的 OpenStack 进行集成。

2017 年 11 月的最新 OpenStack 调查显示,Kubernetes 是迄今为止最受欢迎的容器编排解决方案:

其他参与者

还有许多其他公司将 Kubernetes 作为基础,比如 Rancher 和 Apprenda。大量初创公司开发了运行在 Kubernetes 集群内部的附加组件和服务。未来是光明的。

教育和培训

教育将至关重要。随着 Kubernetes 的早期采用者向大多数人过渡,为组织和开发人员提供正确的资源快速掌握 Kubernetes 并提高生产力非常重要。已经有一些非常好的资源,未来我预计数量和质量将会增加。当然,你现在正在阅读的这本书也是这一努力的一部分。

官方的 Kubernetes 文档越来越好。在线教程非常适合入门:

此外,还有许多付费的 Kubernetes 培训选项。随着 Kubernetes 的流行程度进一步增长,将会有越来越多的选择。

模块化和外部插件

自第一版以来,Kubernetes 在模块化方面取得了巨大进步。Kubernetes 一直是灵活和可扩展的典范。然而,最初您必须将代码构建并链接到 Kubernetes API 服务器或 Kublet(除了 CNI 插件)。您还必须获得代码的审查并将其集成到主要的 Kubernetes 代码库中,以使其对其他开发人员可用。当时,我对 Go 1.8 动态插件以及它们如何以更加灵活的方式扩展 Kubernetes 感到非常兴奋。Kubernetes 的开发人员和社区选择了不同的路径,并决定使 Kubernetes 成为一个通用和多功能的引擎,几乎可以通过标准接口从外部定制或扩展每个方面。您在第十二章中看到了许多示例,自定义 Kubernetes - API 和插件。外部插件的方法意味着您将插件或扩展集成到 GitHub 上 Kubernetes 代码树之外的 Kubernetes 中。有几种正在使用的机制:

  • CNI 插件使用标准输入和输出通过单独的可执行文件

  • CSI 插件使用 Pods gRPC

  • Kubectl 插件使用 YAML 描述符和二进制命令

  • API 聚合器使用自定义 API 服务器

  • Webhooks 使用远程 HTTP 接口

  • 各种其他插件可以部署为 Pod

  • 外部凭证提供者

服务网格和无服务器框架

Kubernetes 在容器编排和成本降低方面帮助了很多繁重的工作。但是,在云原生世界中有两个趋势正在蓬勃发展。服务网格与 Kubernetes 完美契合,运行无服务器框架也发挥了 Kubernetes 的优势。

服务网格

服务网格在容器编排的层次上操作。服务网格管理服务。服务网格在运行具有数百或数千个不同服务的系统时提供了各种必要的能力。

  • 动态路由

  • 感知延迟的东西向负载平衡(集群内部)

  • 自动重试幂等请求

  • 运营指标

过去,应用程序必须在核心功能之上解决这些责任。现在,服务网格减轻了负担,并提供了一个基础架构层,使应用程序可以专注于它们的主要目标。

最著名的服务网格是 Buoyant 的 Linkered。Linkered 支持 Kubernetes 以及其他编排器。但是,鉴于 Kubernetes 的势头。

Buoyant 决定开发一个名为 Conduit(用 Rust 编写)的新的仅限于 Kubernetes 的服务网格。这是对 Kubernetes 受欢迎程度的又一个证明,所有的创新都在这里发生。另一个 Kubernetes 服务网格是 Istio。Istio 由来自 Google、IBM 和 Lyft 的团队创建。它是建立在 Lyft 的 Envoy 之上,并且发展迅速。

无服务器框架

无服务器计算是云原生领域的一个令人兴奋的新趋势。AWS Lambda 函数是最受欢迎的,但现在所有云平台都提供它们。其思想是你不必预留硬件、实例和存储。相反,你只需编写你的代码,打包它(通常在一个容器中),并在需要时调用它。云平台在调用时负责分配资源来运行你的代码,并在代码运行结束时释放资源。这可以节省大量成本(你只支付你使用的资源)并消除了预留和管理基础设施的需求。然而,云提供商提供的无服务器能力通常带有附加条件(运行时和内存限制),或者它们不够灵活(无法控制你的代码将在哪种硬件上运行)。一旦你的集群被预留,Kubernetes 也可以提供无服务器能力。有多个不同成熟度水平的框架可用,如下所示:

  • Fast-netes

  • Nuclio.io

  • Apache OpenWhisk

  • Platform9 Fission

  • Kubless.io

对于在裸金属上运行 Kubernetes 或需要比云平台提供的更灵活性的人来说,这是个好消息。

总结

在本章中,我们展望了 Kubernetes 的未来,前景看好!技术基础、社区、广泛支持和势头都非常令人印象深刻。Kubernetes 仍然年轻,但创新和稳定化的速度非常令人鼓舞。Kubernetes 的模块化和可扩展性原则使其成为现代云原生应用程序的通用基础。

到目前为止,您应该清楚地了解 Kubernetes 目前的状况以及未来的发展方向。您应该相信 Kubernetes 不仅会留下来,而且将成为未来许多年的主要容器编排平台,并将与更大的产品和环境集成。

现在,轮到您利用所学知识,用 Kubernetes 构建令人惊奇的东西了!

posted @ 2024-05-20 12:04  绝不原创的飞龙  阅读(28)  评论(0编辑  收藏  举报