本文发表于一年多前。旧文章可能包含过时内容。请检查页面中的信息自发布以来是否已变得不正确。

一个用于编排高可用性应用程序的自定义 Kubernetes 调度器

只要您愿意遵守规则,在 Kubernetes 上部署和空中旅行都可以是相当愉快的体验。大多数情况下,事情都会“按部就班”。然而,如果一个人有兴趣带着一只必须活着的鳄鱼旅行,或者扩展一个必须保持可用的数据库,情况可能会变得更加复杂。甚至为此建造自己的飞机或数据库可能更容易。抛开带着爬行动物旅行不谈,扩展一个高可用有状态系统绝非易事。

扩展任何系统都包含两个主要组件:

  1. 添加或移除系统将运行的基础设施,以及
  2. 确保系统知道如何处理自身被添加和移除的额外实例。

大多数无状态系统,例如 Web 服务器,在创建时不需要知道它们的对等节点。而有状态系统,包括像 CockroachDB 这样的数据库,必须与其对等实例协调并重新分配数据。幸运的是,CockroachDB 负责数据重新分配和复制。棘手的部分是如何在这些操作过程中容忍故障,通过确保数据和实例分布在多个故障域(可用区)中来实现。

Kubernetes 的职责之一是将“资源”(例如磁盘或容器)放入集群中并满足它们所请求的约束。例如:“我必须在可用区 *A* 中”(请参阅 在多个区域中运行),或者“我不能与此 Pod 放置在同一节点上”(请参阅 亲和性和反亲和性)。

除了这些约束之外,Kubernetes 还提供了 StatefulSet,它为 Pod 提供身份以及“跟随”这些已识别 Pod 的持久存储。StatefulSet 中的身份通过 Pod 名称末尾递增的整数来处理。需要注意的是,这个整数必须始终是连续的:在一个 StatefulSet 中,如果 Pod 1 和 3 存在,那么 Pod 2 也必须存在。

在底层,CockroachCloud 将 CockroachDB 的每个区域作为其自己的 Kubernetes 集群中的 StatefulSet 进行部署——请参阅 在单个 Kubernetes 集群中编排 CockroachDB。在本文中,我将关注一个独立的区域、一个 StatefulSet 和一个分布在至少三个可用区中的 Kubernetes 集群。

一个三节点的 CockroachCloud 集群看起来会是这样

3-node, multi-zone cockroachdb cluster

在向集群添加额外资源时,我们也会将它们分布到不同的区域。为了获得最快的用户体验,我们同时添加所有 Kubernetes 节点,然后扩充 StatefulSet。

illustration of phases: adding Kubernetes nodes to the multi-zone cockroachdb cluster

请注意,无论 Pod 分配到 Kubernetes 节点的顺序如何,反亲和性都会得到满足。在示例中,Pod 0、1 和 2 分别被分配到区域 A、B 和 C,但 Pod 3 和 4 以不同的顺序,分别被分配到区域 B 和 A。反亲和性仍然得到满足,因为这些 Pod 仍然放置在不同的区域中。

要从集群中移除资源,我们以相反的顺序执行这些操作。

我们首先缩减 StatefulSet,然后从集群中移除所有没有 CockroachDB Pod 的节点。

illustration of phases: scaling down pods in a multi-zone cockroachdb cluster in Kubernetes

现在,请记住,大小为 *n* 的 StatefulSet 中的 Pod 必须具有范围 `[0,n)` 内的 ID。当 StatefulSet 缩容 *m* 个 Pod 时,Kubernetes 会从最高序数开始,向最低序数移除 *m* 个 Pod,与它们添加时的顺序相反。考虑下面的集群拓扑

illustration: cockroachdb cluster: 6 nodes distributed across 3 availability zones

当序数 5 到 3 从此集群中移除时,StatefulSet 仍会存在于所有 3 个可用区中。

illustration: removing 3 nodes from a 6-node, 3-zone cockroachdb cluster

然而,Kubernetes 的调度器并不 *保证* 上述放置,正如我们最初预期的那样。

我们对以下方面的综合知识导致了这种误解。

考虑以下拓扑

illustration: 6-node cockroachdb cluster distributed across 3 availability zones

这些 Pod 是按顺序创建的,并分布在集群的所有可用区中。当序数 5 到 3 被终止时,这个集群将失去在区域 C 的存在!

illustration: terminating 3 nodes in 6-node cluster spread across 3 availability zones, where 2/2 nodes in the same availability zone are terminated, knocking out that AZ

更糟糕的是,当时我们的自动化会移除节点 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 Pods 的调度方式让我们能够自信地进行扩展。一旦 Pod 拓扑散布约束在 GKE 和 EKS 中可用,我们可能会考虑淘汰我们的插件,但维护开销出人意料地低。更好的是:插件的实现与我们的业务逻辑正交。部署它或将其淘汰,就像更改 StatefulSet 定义中的 `schedulerName` 字段一样简单。


Chris Seto 是 Cockroach Labs 的软件工程师,负责 CockroachCloud 和 CockroachDB 的 Kubernetes 自动化。