这篇文章已超过一年。较早的文章可能包含过时内容。请检查页面中的信息自发布以来是否已发生变化。
用于协调高可用应用的自定义 Kubernetes 调度器
只要你愿意遵守规则,在 Kubernetes 上部署应用和乘坐飞机旅行一样,可以相当愉快。很多时候,事情会“自然而然地顺利进行”。然而,如果你有兴趣带着一只必须活着的鳄鱼旅行,或者扩展一个必须保持可用的数据库,情况可能会变得复杂一些。甚至自己造飞机或数据库都可能更容易。撇开带着爬行动物旅行不谈,扩展一个高可用的有状态系统绝非易事。
扩展任何系统都有两个主要组成部分
- 添加或移除系统将运行的基础设施,以及
- 确保系统知道如何处理自身被添加和移除的额外实例。
大多数无状态系统,例如 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 集群看起来像这样
当向集群添加额外资源时,我们也将它们分布在不同的可用区。为了最快的用户体验,我们同时添加所有 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)
内。当将 StatefulSet 缩容 m 时,Kubernetes 会移除 m 个 Pod,从最高序号开始向最低序号移动,这与 它们添加的顺序相反。考虑下面的集群拓扑结构
当序号 5 到 3 从此集群中移除时,StatefulSet 继续在所有 3 个可用区中保持存在。
然而,Kubernetes 的调度器并没有如我们最初预期的那样 保证 上述放置方式。
我们对以下内容的综合了解导致了这种误解。
- Kubernetes 自动将 Pod 分布在可用区 的能力
- 拥有 n 个副本的 StatefulSet 的行为是,当 Pod 正在部署时,它们会按顺序创建,从
{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 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 自动化工作。