这篇文章是阅读了 Borg, Omega, and Kubernetes 这篇论文写下来的。
简介
Borg 是 Google 开发的一个大规模集群管理系统,用来调度 Google 内部批任务和长时间运行的服务,还包括服务发现、负载均衡、任务调度,以提高资源利用率和减低成本。
Omega 是 Borg 的继承者,继承了 Borg 的很多优点,并且提供更多的一致性。Omega 使用了面向事务的基于 Paxos 的中心化存储来保存状态数据,它可以被集群内控制平面的多个组件访问,提供了乐观一致性控制解决少有的并非冲突问题。这种解耦方式使得 Borgmaster 的功能可以被划分成多个对等的组件,而不是让每个修改操作都通过单一、中心化的 master 来管理。 Omega 的多创新技术反哺给 Borg,包括多调度器。
由于容器技术的发展(Google 也因为 Borg 项目向 Linux 贡献了容器相关的代码,比如 cgroup 资源控制),出现了 Docker 技术,而业界 Paas 平台也逐渐转移并倾向使用容器部署。由于 Google 多年集群系统管理经验,Google 与开源社区开发者一起开发了 Kubernetes 项目,实现容器编排。
Kubernetes 解决了很多 Borg 和 Omega 的优点,同时也进行了改进。相同点是都使用一个共享存储服务来保存集群状态,不同的是 Omega 直接将状态数据暴露给受信的控制平面组件访问,而 Kubernetes 则是通过特定领域的 REST API 访问,支持更多不同类型的客户端访问。Kubernetes 的主要设计目标是使其易于部署和管理复杂的分布式系统,同时仍然受益于容器使能的利用率的提高。
容器
最高提供容器隔离的是 chroot 方法,后来经过多年发展逐渐形成今天的 cgroup(linux control group)。容器提供的资源隔离使得 Google 能够充分控制资源利用率。容器不仅仅是自愿隔离,还有镜像隔离。所谓镜像就是在容器里运行的应用程序是由哪些文件组成的。
面向应用的架构
容器化不仅仅是更高的资源利用率,而且将数据中心从面向机器转移到面向应用。主要是两点:
- 容器封装了应用环境,为应用开发者和部署基础设置隐藏了很多机器和操作系统的细节,对底层做了一层抽象;
- 由于精心设计的容器和容器镜像仅限于单个应用程序,因此管理容器意味着管理应用程序而不是管理机器。从面向机器到面向应用的管理API的转变极大的提高了应用部署和自省能力;
应用环境
操作系统内核提供 cgroup,chroot 和 namespace 的本意是保护应用不受干扰,集合容器镜像还可以起到操作系统隔离作用。容器镜像和操作系统解耦使得开发环境和线上环境的部署是一样的,提高了部署可靠性,通过减少不一致和摩擦(阻碍)来加速部署。
作这一层抽闲的关键有一个容器它可以封装几乎所有应用的依赖并将其打包好,然后就可以在容器中运行。如果这一些都做得正确,那么唯一的本地外部依赖就是 linux 内核的系统调用接口。尽管这个受限的接口可以极大的提高镜像的移植性,但是却并不完美,因为应用程序仍然可能会受到操作系统接口的干扰,特别是 socket 的选项、/proc 目录和 ioctl 的参数。我们希望 OCI 正在进行的工作能够近一步的使容器抽象的表面层更清晰。
不管怎么样,隔离和最小化依赖在 Google 内部已经被证实是非常有效的。容器已经成为 Google 基础设施上唯一运行的实体。有很多方法来实现密闭的容器镜像,比如二进制程序是在编译阶段静态链接的。尽管如此,Borg 的容器已经也没有它应该有的那种封闭性,因为应用共享了所谓的基础镜像,它不是随应用一起打包的,而是在机器上直接安装的。所以升级基础容器镜像就会影响到其他应用,这也成为很多问题的根源。
容器作为一个管理单元
围绕容器而不是机器来构建管理 API 是将数据中心从机器转移到容器的关键。有如下优点:
- 解放应用开发者和运维人员,因为他们基本不会在关注到机器和操作系统的特殊细节;
- 更换硬件和升级操作系统的影响降到最低,为基础设施团队提供更多弹性支持;
- 管理系统收集的是应用而不是机器的健康指标,这极大的提高了应用的监控和自省能力,特别是扩容、机器故障、维护导致的应用飘移;
容器可以暴露一些 HTTP 接口来让管理系统检查应用的运行状态,如果应用没有正确回应则意味着其已经崩溃,管理系统就会重启该应用。
额外的信息可以由容器提供,也可以为容器提供,并显示在各种用户界面中。例如,Borg应用程序可以提供简单的文本状态消息,可以动态更新,Kubernetes提供存储在每个对象的元数据中的键值注释(key-value annotation),可以用于通信应用程序结构。这样的注释可以由容器本身或者管理系统中的其他参与者来设置。
在另一个方向上,容器管理系统可以将资源限制、容器元数据传播到日志和监控,并在节点维护之前提供优雅终止警告的通知。
监控只是一个例子,面向应用的转变已经蔓延到整个管理基础设置。我们的复杂均衡器不再按照机器均衡流量,而是应用实例。日志是应用程序的关键,而不是机器关键,因此它们可以很容易地从多个实例中收集和汇总,而不会受到来自多个应用程序或系统操作的污染。从根本上说,由于容器管理器所管理的实例的特征与应用程序开发人员所期望的实例的特征完全一致,因此更容易构建、管理和调试应用程序。
在 Borg 中,最外层的容器被称为资源分配,或者 alloc;在 Kubernetes 中,它被称为 pod。Borg 还允许顶级应用容器在 alloc 之外运行;这给使用带来了诸多不便,因此 Kubernetes 将事情规范化,并且总是在一个顶级的 pod 中运行一个应用程序容器,即使这个pod包含一个容器。
一个常用的使用模式是一个 pod 容纳一个复杂应用的实例。应用程序的主要部分位于其中一个子容器中,其他子容器运行支持的功能,如日志旋转或点击日志卸载到分布式文件系统。这种被称为边车模式(sidecar)。
编排只是开始
Borg 系统使用比较复杂,Kubernetes 进行了改进,比如将 Kubernetes 的基本对象划分 3 个字段:ObjectMetadata, Specification (Spec) 和 Status。对象元数据(ObjectMetadata)对于系统中的所有对象都是一样的;它包含对象名称、UID ( unique identifier )、对象版本号(为了实现乐观的并发控制)和标签(键值对)等信息。Spec 和 Status 的内容因对象类型而异,但它们的概念并不相同:Spec 用于描述对象的期望状态,而 Status 只提供关于对象当前状态的可读信息。
这种统一的 API 提供了许多好处。学习该系统更加简单:相似的信息适用于所有对象,编写跨对象工作的通用工具更加简单,进而能够开发出一致的用户体验。借鉴 Borg 和 Omega 的做法,Kubernetes 是由一组易于用户扩展的可组合构建块构建而成的。一个通用的 API 和对象元数据结构使这一点变得容易得多。例如,pod API可以被人、内部 Kubernetes 组件和外部自动化工具使用。为了进一步增强这种一致性,Kubernetes 正在进行扩展,使用户能够在 Kubernetes 核心功能的基础上动态添加自己的API。
一致性也是通过 Kubernetes API 中的解耦实现的。API 组件之间的关注点分离意味着更高级别的服务都共享相同的公共基础构建块。比较好的一个例子是 replication 控制器和 autoscaler ,replication 控制器保证了给定角色所需 pod 的数量。反过来,autoscaler 依靠这种能力,简单地调整所需的 pod 数量,而不用担心这些 pod 是如何创建或删除的。autoscaler 的实现可以专注于需求和使用预测,而忽略了如何实现其决策的细节。
一致性也是通过不同 Kubernetes 组件的通用设计模式来实现的。控制器循环的思想在 Borg、Omega 和 Kubernetes 中得到了体现,以提高系统的弹性:它将期望状态(例如,一个标签选择器查询需要匹配多少个 pod)与观测状态(它所能找到的这种 pod 的数量)进行比较,并采取行动使观测状态和期望状态收敛。
避坑事项
Borg、Omega 尽管非常优秀,但是仍然存在缺陷,这里列出已存在的问题,希望大家不要重复这些错误。
不要让容器系统管理端口号
在 Borg 机器上运行的所有容器共享主机的 IP 地址,因此 Borg 为容器分配唯一的端口号,作为调度过程的一部分。服务的客户事先并不知道分配给服务的端口号,必须被告知;端口号不能嵌入在 URL 中,需要基于名字的重定向机制;而依赖于简单 IP 地址的工具需要重写来处理 IP:端口对。
由于 Borg 的设计缺陷,Kubernetes 会为每个 pod 分配一个 IP 地址,从而将网络标识(IP地址)与应用标识对齐。这使得在 Kubernetes 上运行现成的软件变得更加容易。
不要给容器编号,而是用标签
标题的意思是不要用数组下标的方式来标记容器,也是用标签来标记。
Borg 为 job 一组相同的 task,一个 job 是一个或多个相同 task 的紧凑的向量,从零开始依次索引。这提供了许多能力,简单而直接,但随着时间的推移,我们开始后悔:它变得越来越僵化、难用。比如,当一个任务死亡,必须在另一台机器上重新启动时,任务向量中的同一个下标必须执行双重任务:识别新任务并指向旧任务,以防调试场景。当位于向量中间的任务退出时,向量以空洞结束。该向量使得 Borg 上面一层中跨越多个集群的 job 很难得到支持。
相反,Kubernetes 主要使用标签来识别容器组。标签是一个键/值对,其中包含有助于识别对象的信息。标签可以由自动化工具或用户动态地添加、删除和修改,不同的团队可以在很大程度上独立地管理自己的标签。对象集合由标签选择器定义。集合可以重叠,一个对象可以在多个集合中,因此标签在本质上比对象的显式列表或简单的静态属性更加灵活。因为一个集合是由一个动态查询定义的,所以在任何时候都可以创建一个新的集合。标签选择器是 Kubernetes 中的分组机制,定义了所有可以跨越多个实体的管理操作的范围。
小心所有权
在 Borg,任务不会脱离 job 独立存在。创建一个 job 就会创建一个任务,这些任务永远都与特定的 job 关联起来,删除 job 同时也会删除任务。这很方便,同时也有缺点:因为只有一个组合机制,它需要处理所有场景。比如,一个 job 必须存储只对服务或批处理作业有意义而对两者都没有意义的参数,并且当 job 抽象不处理用例时,用户必须开发变通方法。
在 Kubernetes 中,replication 控制器等 pod 生命周期管理组件决定了它们使用标签选择器负责哪些 pod,因此多个控制器可能会认为它们对单个 pod 具有管辖权。重要的是通过适当的配置选择来防止这种冲突。但是灵活的标签具有补偿优势,比如,控制器和 pod 的分离意味着可以”孤儿”和”采用”容器。考虑一种负载均衡的服务,该服务使用标签选择器来识别发送流量到的 pod 集合。如果其中一个 pod 开始出错,则可以通过移除那些被 Kubernetes 服务负载均衡器使用的一个或多个标签来隔离该 pod 对服务请求的影响。pod 不再有流量进来,但它会保持继续运行,并且可以被用来调试。同时,管理这个 pod 的 replication 控制器自动创建一个 pod 替代它。
不要暴露原始状态
Borg、Omega 和 Kubernetes 的一个关键区别在于它们的 API 架构。Borgmaster 是一个单体组件,它知道每个 API 操作的语义。它包含了集群管理逻辑,如 job 、任务和机器的状态机,并且它运行基于 Paxos 的复制存储系统,用于记录 master 的状态。相比之下,Omega 除了存储外没有中心化的组件,只保存被动的状态信息,并执行乐观的并发控制:所有的逻辑和语义都被推送到客户端的存储,由客户端直接读写存储内容。在实际应用中,每个 Omega 组件都使用相同的客户端库来进行存储,对数据结构进行序列化/方序列化,检索,并实现语义一致性。
Kubernetes 使用一个中立方案,提供了 Omega 那种弹性和可扩展性组件化架构,同时还实现系统层面的不变性、策略、数据转换。它强制对存储的访问都通过一个中心化的 API 服务来完成,该服务隐藏了存储实现的细节,并提供对象验证、默认值和版本控制服务。正如在 Omega 中一样,客户端组件是相互解耦的,可以独立演化或替换,但是,中心化使得强制执行通用语义、不变性和策略变得容易。
一些开放且困难的问题
尽管我们有多年的容器系统管理经验,但是我们也感觉到有一些问题依然没有很好的答案。
配置
在我们所面临的所有问题中,最耗费脑力、墨水和代码的问题是与管理配置有关的:提供给应用程序的一组值,而不是硬编码到应用程序中。以下是几个亮点。
首先,应用配置成为实现所有事情的首要位置,而这些事情容器管理系统并没有做。纵观 Borg 的历史,这些包括:
- 减少模板,比如适合工作负载的任务重启策略;
- 调整和严重应用的参数和命令行参数
- 实现 API 抽象层缺少的功能;
- 应用参数模板的库;
- 发版管理工具;
- 镜像版本规范;
为了应对这些需求,配置管理系统倾向于发明一种特定于领域的配置语言,从对配置中的数据进行计算的愿望出发,该语言(最终)成为图灵完备的。其结果是,人们试图通过消除应用程序源代码中的硬编码参数来避免那种令人费解的”配置就是代码”。它不会降低操作的复杂性,也不会使配置更容易调试或更改,它只是将计算从真正的编程语言转移到特定领域的编程语言,而特定领域的编程语言通常具有较弱的开发工具,如调试器和单元测试框架。
我们认为最有效的方法是接受这种需求,接受程序化配置的必然性,并保持计算和数据之间的清晰分离。用来表示数据的语音应该足够简单,比如 JSON 或者 YAML,他们很容易以编程的方式来修改,也很容易理解。
依赖管理
集群管理系统如何处理服务或者应用之间的依赖关系呢?
处理依赖关系并不是像启动一个新的服务那边简单,它需要向已存在的服务注册消费者,并且在多个转移依赖中传递认证、授权等信息。然而,几乎没有系统获取、维护、暴露这些依赖信息。所以就算是在基础架构级别完成通用的自动化依赖管理也几乎不可能。