这篇文章已超过一年。较早的文章可能包含过时内容。请检查页面中的信息自发布以来是否已发生变化。

用于协调高可用应用的自定义 Kubernetes 调度器

只要你愿意遵守规则,在 Kubernetes 上部署应用和乘坐飞机旅行一样,可以相当愉快。很多时候,事情会“自然而然地顺利进行”。然而,如果你有兴趣带着一只必须活着的鳄鱼旅行,或者扩展一个必须保持可用的数据库,情况可能会变得复杂一些。甚至自己造飞机或数据库都可能更容易。撇开带着爬行动物旅行不谈,扩展一个高可用的有状态系统绝非易事。

扩展任何系统都有两个主要组成部分

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

大多数无状态系统,例如 Web 服务器,在创建时不需要感知其对等节点。有状态系统,包括像 CockroachDB 这样的数据库,必须与其对等实例协调并进行数据迁移。幸运的是,CockroachDB 处理数据再分配和复制。棘手的部分是能够在这些操作期间容忍故障,方法是确保数据和实例分布在许多故障域(可用区)中。

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

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

在底层,CockroachCloud 将 CockroachDB 的每个区域作为一个 StatefulSet 部署在其自己的 Kubernetes 集群中——参见 在单个 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 ID 必须在范围 [0,n) 内。当将 StatefulSet 缩容 m 时,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 Topology Spread Constraints

虽然这看起来像是完美的解决方案,但在撰写本文时,Kubernetes 1.18 在公有云中最常见的两个托管 Kubernetes 服务 EKS 和 GKE 上尚不可用。此外,Pod Topology Spread Constraints 在 1.18 中仍是 Beta 特性,这意味着即使 v1.18 可用,也 无法保证在托管集群中可用。整个尝试令人担忧地回想起在 Internet Explorer 8 still 存在时检查 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 Topology Spread Constraints 在 GKE 和 EKS 中可用,我们可能会考虑淘汰我们的插件,但维护开销一直低得出奇。更重要的是:该插件的实现与我们的业务逻辑正交。部署它或淘汰它,就像更改 StatefulSet 定义中的 schedulerName 字段一样简单。


Chris Seto 是 Cockroach Labs 的一名软件工程师,负责 CockroachCloud (CockroachDB 的托管服务) 的 Kubernetes 自动化工作。