+-
OpenKruise v0.10.0 新特性 WorkloadSpread 解读

背景

Workload 分布在不同 zone,不同的硬件类型,甚至是不同的集群和云厂商已经是一个非常普遍的需求。过去一般只能将一个应用拆分为多个 workload(比如 Deployment)来部署,由 SRE 团队手工管理或者对 PaaS 层深度定制,来支持对一个应用多个 workload 的精细化管理。

进一步来说,在应用部署的场景下有着多种多样的拓扑打散以及弹性的诉求。其中最常见就是按某种或多种拓扑维度打散,比如:

应用部署需要按 node 维度打散,避免堆叠(提高容灾能力)。 应用部署需要按 AZ(available zone)维度打散(提高容灾能力)。 按 zone 打散时,需要指定在不同 zone 中部署的比例数。

随着云原生在国内外的迅速普及落地,应用对于弹性的需求也越来越多。各公有云厂商陆续推出了 Serverless 容器服务来支撑弹性部署场景,如阿里云的弹性容器服务 ECI,AWS 的 Fragate 容器服务等。以 ECI 为例,ECI 可以通过Virtual Kubelet对接 Kubernetes 系统,给予 Pod 一定的配置就可以调度到 virtual-node 背后的 ECI 集群。总结一些常见的弹性诉求,比如:

应用优先部署到自有集群,资源不足时再部署到弹性集群。缩容时,优先从弹性节点缩容以节省成本。 用户自己规划基础节点池和弹性节点池。应用部署时需要固定数量或比例的 Pod 部署在基础节点池,其余的都扩到弹性节点池。

针对这些需求,OpenKruise 在 v0.10.0 版本中新增了 WorkloadSpread 特性。目前它支持配合 Deployment、ReplicaSet、CloneSet 这些 workload,来管理它们下属 Pod 的分区部署与弹性伸缩。下文会深入介绍 WorkloadSpread 的应用场景和实现原理,帮助用户更好的了解该特性。

WorkloadSpread介绍

官方文档(见文末相关链接一)

简而言之,WorkloadSpread 能够将 workload 所属的 Pod 按一定规则分布到不同类型的 Node 节点上,能够同时满足上述的打散与弹性场景。

现有方案对比

简单对比一些社区已有的方案。

Pod Topology Spread Constrains(见文末相关链接二)

Pod Topology Spread Constrains 是 Kubernetes 社区提供的方案,可以定义按 topology key 的水平打散。用户在定义完后,调度器会依据配置选择符合分布条件的 node。

由于 PodTopologySpread 更多的是均匀打散,无法支持自定义的分区数量以及比例配置,且缩容时会破坏分布。WorkloadSpread 可以自定义各个分区的数量,并且管理着缩容的顺序。因此在一些场景下可以避免 PodTopologySpread 的不足。

UnitedDeployment(见文末相关链接三)

UnitedDeployment 是 Kruise 社区提供的方案,通过创建和管理多个 workload 管理多个区域下的 Pod。

UnitedDeployment非常好的支持了打散与弹性的需求,不过它是一个全新的 workload,用户的使用和迁移成本会比较高。而 WorkloadSpread 是一种轻量化的方案,只需要简单的配置并关联到 workload 即可。

应用场景

下面我会列举一些 WorkloadSpread 的应用场景,给出对应的配置,帮助大家快速了解 WorkloadSpread 的能力。

1. 基础节点池至多部署 100 个副本,剩余的部署到弹性节点池

subsets:
- name: subset-normal
maxReplicas: 100
requiredNodeSelectorTerm:
matchExpressions:
- key: app.deploy/zone
operator: In
values:
- normal
- name: subset-elastic #副本数量不限
requiredNodeSelectorTerm:
matchExpressions:
- key: app.deploy/zone
operator: In
values:
- elastic

当 workload 少于 100 副本时,全部部署到 normal 节点池,超过 100 个部署到 elastic 节点池。缩容时会优先删除 elastic 节点上的 Pod。

由于 WorkloadSpread 不侵入 workload,只是限制住了 workload 的分布,我们还可以通过结合 HPA 根据资源负载动态调整副本数,这样当业务高峰时会自动调度到 elastic 节点上去,业务低峰时会优先释放 elastic 节点池上的资源。

2. 优先部署到基础节点池,资源不足再部署到弹性资源池

scheduleStrategy:
type: Adaptive
adaptive:
rescheduleCriticalSeconds: 30
disableSimulationSchedule: false
subsets:
- name: subset-normal #副本数量不限
requiredNodeSelectorTerm:
matchExpressions:
- key: app.deploy/zone
operator: In
values:
- normal
- name: subset-elastic #副本数量不限
requiredNodeSelectorTerm:
matchExpressions:
- key: app.deploy/zone
operator: In
values:
- elastic

两个 subset 都没有副本数量限制,且启用 Adptive 调度策略的模拟调度和 Reschedule 能力。部署效果是优先部署到 normal 节点池,normal 资源不足时,webhook 会通过模拟调度选择 elastic 节点。当 normal 节点池中的 Pod 处于 pending 状态超过 30s 阈值, WorkloadSpread controller 会删除该 Pod 以触发重建,新的 Pod 会被调度到 elastic 节点池。缩容时还是优先缩容 elastic 节点上的 Pod,为用户节省成本。

3. 打散到3个zone,比例分别为1:1:3

subsets:
- name: subset-a
maxReplicas: 20%
requiredNodeSelectorTerm:
matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values:
- zone-a
- name: subset-b
maxReplicas: 20%
requiredNodeSelectorTerm:
matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values:
- zone-b
- name: subset-c
maxReplicas: 60%
requiredNodeSelectorTerm:
matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values:
- zone-c

按照不同 zone 的实际情况,将 workload 按照 1:1:3 的比例打散。WorkloadSpread 会确保 workload 扩缩容时按照定义的比例分布。

4. workload在不同CPU Arch上配置不同的资源配额

workload 分布的 Node 可能有不同的硬件配置,CPU 架构等,这就可能需要为不同的 subset 分别制定 Pod 配置。这些配置可以是 label 和 annotation 等元数据也可以是 Pod 内部容器的资源配额,环境变量等。

subsets:
- name: subset-x86-arch
# maxReplicas...
# requiredNodeSelectorTerm...
patch:
metadata:
labels:
resource.cpu/arch: x86
spec:
containers:
- name: main
resources:
limits:
cpu: "500m"
memory: "800Mi"
- name: subset-arm-arch
# maxReplicas...
# requiredNodeSelectorTerm...
patch:
metadata:
labels:
resource.cpu/arch: arm
spec:
containers:
- name: main
resources:
limits:
cpu: "300m"
memory: "600Mi"

从上面的样例中我们为两个 subset 的 Pod 分别 patch 了不同的 label, container resources,方便我们对 Pod 做更精细化的管理。当 workload 的 Pod 分布在不同的 CPU 架构的节点上,配置不同的资源配额以更好的利用硬件资源。

实现原理

WorkloadSpread 是一个纯旁路的弹性/拓扑管控方案。用户只需要针对自己的 Deployment/CloneSet/Job 对象创建对应的 WorkloadSpread 即可,无需对 workload 做改动,也不会对用户使用 workload 造成额外成本。

1. subset优先级与副本数量控制

WorkloadSpread 中定义了多个 subset,每个 subset 代表一个逻辑域。用户可以自由的根据节点配置,硬件类型,zone 等来划分 subset。特别的,我们规定了 subset 的优先级:

按定义从前往后的顺序,优先级从高到低。 优先级越高,越先扩容;优先级越低,越先缩容。

2. 如何控制缩容优先级

理论上,WorkloadSpread 这种旁路方案是无法干涉到 workload 控制器里的缩容顺序逻辑的。

不过,这个问题在近期得以解决—— 经过一代代用户的不懈努力(反馈),K8s 从 1.21 版本开始为 ReplicaSet(Deployment)支持了通过设置 controller.kubernetes.io/pod-deletion-cost 这个 annotation 来指定 Pod 的 “删除代价”:deletion-cost 越高的 Pod,删除的优先级越低。

而 Kruise 从 v0.9.0 版本开始,就在 CloneSet 中支持了 deletion-cost 特性。

因此,WorkloadSpread controller通过调整各个 subset 下属 Pod 的 deletion-cost,来控制workload的缩容顺序。

举个例子:对于以下 WorkloadSpread,以及它关联的 CloneSet 有 10 个副本:

subsets:
- name: subset-a
maxReplicas: 8
- name: subset-b # 副本数量不限

则 deletion-cost 数值以及删除顺序为:

2 个在 subset-b上的 Pod,deletion-cost 为 100(优先缩容) 8 个在 subset-a上的 Pod,deletion-cost 为 200(最后缩容)

然后,如果用户修改了 WorkloadSpread 为:

subsets:
- name: subset-a
maxReplicas: 5 # 8-3,
- name: subset-b

则 workloadspread controller 会将其中 3 个在 susbet-a 上 Pod 的 deletion-cost 值由 200 改为 -100

3 个在 subset-a 上的 Pod,deletion-cost 为 -100(优先缩容) 2 个在 subset-b 上的 Pod,deletion-cost 为 100(其次缩容) 5 个在 subset-a 上的 Pod,deletion-cost 为 200(最后缩容)

这样就能够优先缩容那些超过 subset 副本限制的 Pod 了,当然总体还是按照 subset 定义的顺序从后向前缩容。

3. 数量控制

如何确保 webhook 严格按照 subset 优先级顺序、maxReplicas 数量来注入 Pod 规则是 WorkloadSpread 实现层面的重点难题。

3.1 解决并发一致性问题

在 workloadspread 的 status 中有对应每个 subset 的 status,其中 missingReplicas 字段表示了这个 subset 需要的 Pod 数量,-1 表示没有数量限制(subset 没有配置 maxReplicas)。

spec:
subsets:
- name: subset-a
maxReplicas: 1
- name: subset-b
# ...
status:
subsetStatuses:
- name: subset-a
missingReplicas: 1
- name: subset-b
missingReplicas: -1
# ...

当 webhook 收到 Pod create请求时:

根据 subsetStatuses 顺序依次找 missingReplicas 大于 0 或为 -1 的 suitable subset。 找到suitable subset后,如果 missingReplicas 大于 0,则先减 1 并尝试更新 workloadspread status。 如果更新成功,则将该 subset定义的规则注入到 pod 中。 如果更新失败,则重新 get 这个 workloadspread以获取最新的 status,并回到步骤 1(有一定重试次数限制)。

同样,当 webhook 收到 Pod delete/eviction 请求时,则将 missingReplicas 加 1 并更新。

毫无疑问,我们在使用乐观锁来解决更新冲突。但是仅使用乐观锁是不合适的,因为 workload 在创建 Pod 时会并行创建大量的 Pod,apiserver 会在一瞬间发送很多 Pod create 请求到 webhook,并行处理会产生非常多的冲突。大家都知道,冲突太多就不适合使用乐观锁了,因为它解决冲突的重试成本非常高。为此我们还加入了 workloadspread 级别的互斥锁,将并行处理限制为串行处理。加入互斥锁还有新的问题,即当前 groutine 获取锁后,极有可能从 infromer 中拿的 workloadspread 不是最新的,还是会冲突。所以 groutine 在更新完 workloadspread 之后,先将最新的 workloadspread 对象缓存起来再释放锁,这样新的 groutine 获取锁后就可以直接从缓存中拿到最新的 workloadspread。当然,多个 webhook 的情况下还是需要结合乐观锁机制来解决冲突。

3.2 解决数据一致性问题

那么,missingReplicas 数值是否交由 webhook 控制即可呢?答案是不行,因为:

webhook 收到的 Pod create 请求,最终不一定真的能成功(比如 Pod 不合法,或在后续 quota 等校验环节失败了)。 webhook 收到的 Pod delete/eviction 请求,最终也不一定真的能成功(比如后续被 PDB、PUB 等拦截了)。 K8s 里总有种种的可能性,导致 Pod 没有经过 webhook 就结束或没了(比如 phase 进入 Succeeded/Failed,或是 etcd 数据丢了等等)。 同时,这也不符合面向终态的设计理念。

因此,workloadspread status 是由 webhook 与 controller 协作来控制的:

webhook 在 Pod create/delete/eviction 请求链路拦截,修改 missingReplicas 数值。 同时 controller 的 reconcile 中也会拿到当前 workload 下的所有 Pod,根据 subset 分类,并将 missingReplicas 更新为当前实际缺少的数量。 从上面的分析中,controller 从 informer 中获取 Pod 很可能存在延时,所以我们还在status中增加了 creatingPods map, webook 注入的时候会记录 key 为pod.name, value 为时间戳的一条 entry 到 map,controller 再结合 map 维护真实的 missingReplicas。同理还有一个 deletingPods map 来记录 Pod 的delete/eviction 事件。

4. 自适应调度能力

在 WorkloadSpread 中支持配置 scheduleStrategy。默认情况下,type 为 Fixed,即固定按照各个 subset 的前后顺序、maxReplicas 限制来将 Pod 调度到对应的 subset 中。

但真实的场景下,很多时候 subset 分区或拓扑的资源,不一定能完全满足 maxReplicas 数量。用户需要按照实际的资源情况,来为 Pod 选择有资源的分区扩容。这就需要用 Adaptive 这种自适应的调度分配。

WorkloadSpread 提供的 Adaptive 能力,逻辑上分为两种:

SimulationSchedule:在 Kruise webhook 中根据 informer 里已有的 nodes/pods 数据,组装出调度账本,对 Pod 进行模拟调度。即通过 nodeSelector/affinity、tolerations、以及基本的 resources 资源,做一次简单的过滤。(对于 vk 这种节点不太适用) Reschedule:在将 Pod 调度到一个 subset 后,如果调度失败超过 rescheduleCriticalSeconds 时间,则将该 subset 暂时标记为 unschedulable,并删除 Pod 触发重建。默认情况下,unschedulable 会保留 5min,即在 5min 内的 Pod 创建会跳过这个 subset。

小结

WorkloadSpread 通过结合一些 kubernetes 现有的特性以一种旁路的形式赋予 workload 弹性部署与多域部署的能力。我们希望用户通过使用 WorkloadSpread 降低 workload 部署复杂度,利用其弹性伸缩能力切实降低成本。

目前阿里云内部正在积极的落地,落地过程中的调整会及时反馈社区。未来 WorkloadSpread 还有一些新能力计划,比如让 WorkloadSpread 支持 workload 的存量 Pod 接管,支持批量的 workload 约束,甚至是跨过 workload 层级使用 label 来匹配 Pod。其中一些能力需要实际考量社区用户的需求场景。希望大家多多参与到 Kruise 社区,多提 issue 和 pr,帮助用户解决更多云原生部署方面的难题,构建一个更好的社区。

Github: https://github.com/openkruise/kruise Official: https://openkruise.io/ Slack: Channel in Kubernetes Slack 钉钉交流群:

相关链接: 链接一:WorkloadSpread 官方文档: https://openkruise.io/zh-cn/docs/workloadspread.html 链接二:Pod Topology Spread Constrains : https://kubernetes.io/docs/concepts/workloads/pods/pod-topology-spread-constraints/ 链接三:UnitedDeployment : https://openkruise.io/zh-cn/docs/uniteddeployment.html

点击下方链接,立即了解 OpenKruise 项目! https://github.com/openkruise/kruise