本文发布已超过一年。较旧的文章可能包含过时的内容。请检查页面中的信息自发布以来是否已变得不正确。
用于协调高可用性应用程序的自定义 Kubernetes 调度器
只要您愿意遵守规则,在 Kubernetes 上部署和乘坐飞机旅行都会非常愉快。通常情况下,事情都会“正常运行”。但是,如果有人有兴趣携带必须保持存活的鳄鱼旅行或扩展必须保持可用的数据库,情况可能会变得更加复杂。甚至可能更容易为此建造自己的飞机或数据库。除了携带爬行动物旅行之外,扩展高度可用的有状态系统绝非易事。
扩展任何系统都有两个主要组成部分
- 添加或删除系统将在其上运行的基础设施,以及
- 确保系统知道如何处理自身添加和删除的其他实例。
大多数无状态系统,例如 Web 服务器,在创建时无需感知对等节点。而有状态系统,包括像 CockroachDB 这样的数据库,必须与其对等实例进行协调并来回移动数据。幸运的是,CockroachDB 可以处理数据重新分发和复制。棘手的部分在于,要确保数据和实例分布在多个故障域(可用区),从而在这些操作期间容忍故障。
Kubernetes 的职责之一是将“资源”(例如,磁盘或容器)放置到集群中,并满足它们请求的约束。例如:“我必须在可用区A 中”(请参阅在多个区域中运行),或“我不能与此其他 Pod 放置在同一节点上”(请参阅亲和性和反亲和性)。
作为这些约束的补充,Kubernetes 提供了 Statefulsets,它为 Pod 提供身份标识,以及“跟随”这些已标识 Pod 的持久存储。StatefulSet 中的身份标识通过 Pod 名称末尾的递增整数来处理。请务必注意,此整数必须始终是连续的:在 StatefulSet 中,如果存在 Pod 1 和 3,则 Pod 2 也必须存在。
在底层,CockroachCloud 将 CockroachDB 的每个区域部署为各自 Kubernetes 集群中的 StatefulSet - 请参阅在单个 Kubernetes 集群中编排 CockroachDB。在本文中,我将关注一个单独的区域,一个 StatefulSet 和一个分布在至少三个可用区中的 Kubernetes 集群。
一个三节点 CockroachCloud 集群看起来像这样
当向集群添加其他资源时,我们也会将它们分布在不同的区域中。为了获得最快的用户体验,我们同时添加所有 Kubernetes 节点,然后向上扩展 StatefulSet。
请注意,无论将 Pod 分配给 Kubernetes 节点的顺序如何,反亲和性都会得到满足。在示例中,Pod 0、1 和 2 分别分配给区域 A、B 和 C,但 Pod 3 和 4 的分配顺序不同,分别为区域 B 和 A。反亲和性仍然得到满足,因为 Pod 仍然放置在不同的区域中。
要从集群中删除资源,我们以相反的顺序执行这些操作。
我们首先缩减 StatefulSet,然后从集群中删除任何缺少 CockroachDB Pod 的节点。
现在,请记住,大小为 *n* 的 StatefulSet 中的 Pod 的 ID 必须在范围 `[0,n)` 内。当按 *m* 缩减 StatefulSet 时,Kubernetes 将删除 *m* 个 Pod,从最高的序号开始,并向最低的序号移动,与它们添加的顺序相反。考虑以下集群拓扑
当序号 5 到 3 从此集群中删除时,StatefulSet 将继续在所有 3 个可用区中存在。
但是,Kubernetes 的调度程序并不能*保证*我们最初预期的上述放置。
我们对以下内容的综合理解导致了这种误解。
- Kubernetes 自动将 Pod 分布在区域中的能力
- 当部署 Pod 时,具有 *n* 个副本的 StatefulSet 的行为是,它们会按顺序创建,顺序为 `{0..n-1}`。有关更多详细信息,请参阅 StatefulSet。
考虑以下拓扑
这些 Pod 是按顺序创建的,并且它们分布在集群中的所有可用区中。当序号 5 到 3 终止时,此集群将失去在 C 区的存在!
更糟糕的是,我们当时的自动化将删除节点 A-2、B-2 和 C-2。由于持久卷仅在它们最初创建的区域中可用,因此将导致 CRDB-1 处于未调度状态。
为了纠正后一个问题,我们现在采用“搜寻和啄食”的方法来从集群中删除机器。我们不再盲目地从集群中删除 Kubernetes 节点,而是只删除没有 CockroachDB Pod 的节点。更艰巨的任务是控制 Kubernetes 调度程序。
一次头脑风暴会议后,我们得出了 3 个选项
1. 升级到 Kubernetes 1.18 并使用 Pod 拓扑分布约束
虽然这似乎是一个完美的解决方案,但在撰写本文时,Kubernetes 1.18 在公有云中最常见的两种托管 Kubernetes 服务 EKS 和 GKE 上都不可用。此外,pod 拓扑分布约束在 1.18 中仍然是一个 Beta 功能,这意味着即使 v1.18 可用,也不保证在托管集群中可用。整个过程令人担忧地让人想起 Internet Explorer 8 还在时检查 caniuse.com 的情况。
2. 每个区域部署一个 StatefulSet。
与其让一个 StatefulSet 分布在所有可用区中,不如让每个区域都有节点亲和性的单个 StatefulSet,这将允许手动控制我们的区域拓扑。我们的团队过去曾考虑过此选项,这使得它特别有吸引力。最终,我们决定放弃此选项,因为它将需要对我们的代码库进行大规模的改造,并且在现有客户集群上执行迁移也将是一项同样庞大的任务。
3. 编写一个自定义 Kubernetes 调度程序。
感谢 Kelsey Hightower 的示例和 Banzai Cloud 的一篇博客文章,我们决定立即开始编写我们自己的自定义 Kubernetes 调度程序。一旦我们的概念验证部署并运行,我们很快就发现 Kubernetes 的调度程序还负责将持久卷映射到它调度的 Pod。 kubectl get events
的输出使我们相信还有另一个系统在运行。在我们寻找负责存储声明映射的组件的过程中,我们发现了 kube-scheduler 插件系统。我们的下一个 POC 是一个 Filter
插件,它通过 Pod 序号确定适当的可用区,并且它完美地工作!
我们的 自定义调度程序插件是开源的,并在我们所有的 CockroachCloud 集群中运行。控制我们 StatefulSet Pod 的调度方式让我们能够自信地进行扩展。一旦 pod 拓扑分布约束在 GKE 和 EKS 中可用,我们可能会考虑停用我们的插件,但维护开销却出奇地低。更好的是:该插件的实现与我们的业务逻辑是正交的。部署它,或者停用它,就像更改我们 StatefulSet 定义中的 schedulerName
字段一样简单。
Chris Seto 是 Cockroach Labs 的一名软件工程师,负责为 CockroachCloud, CockroachDB 进行 Kubernetes 自动化。