Skip to main content

One post tagged with "workloadspread"

View All Tags

面向Workload级别的灵活可配置Serverless弹性解决方案

· 阅读需要 1 分钟
Tianyun Zhong
Member of OpenKruise

Serverless是云计算的进一步延伸,因此其继承了云计算的最大特点,即按需弹性伸缩。这样的模型设计让开发者无需关注具体的部署资源,充分利用资源规模效应,提供更好的弹性能力,也能让企业切实享受到真正的按需使用特征。正因如此,更多的云厂商们不约而同地转向Serverless这一新的架构设计理念。

“灵活可配置”作为Serverless技术的弹性核心能力之一,所关注的是“通过简单、少侵入、灵活可配置的方法让具体用云场景能充分使用弹性资源”。其本质是解决了容量规划与实际集群负载配置间的矛盾。本文将依次介绍 WorkloadSpread 与 UnitedDeployment 两种资源可配置插件,详细探讨它们的核心能力、技术原理与优劣势,以及在真实场景中的应用。通过这些内容分享 OpenKruise 社区在应对 Serverless 负载弹性问题时的技术演进和思考。

弹性场景概述

随着 Serverless 技术的成熟,越来越多企业倾向于使用弹性资源(如阿里云 ACS 等 Serverless 容器实例)而非静态资源(如由云服务器 组成的托管资源池、自建的 IDC 机房等)来承载具有临时性、潮汐性、突发性等特征的应用,以按需使用的形式提高资源利用效率,降低整体成本。下面列出一些典型的弹性场景:

  1. 优先使用线下IDC机房的自建资源,当资源不足时,将应用副本扩展到云上承载。

  2. 优先使用包年包月购买的托管节点资源池,资源不足时使用按量付费的 Serverless 实例承载副本。

  3. 优先使用持有的高质量稳定算力(如独享型的云服务器实例),用完后再使用较低质量的算力(如 Spot 实例)。

  4. 为部署到不同算力形式(如 X86 架构、ARM 架构、Serverless 实例等)上的容器副本配置不同的资源量,以获得相似的性能表现。

  5. 为部署到节点上与 Serverless 环境的副本注入不同的中间件配置(如节点上副本使用共享 Daemon,Serverless 副本注入 Sidecar)

本文介绍的两种组件,在解决上述问题,具有各自的优势场景。用户可以根据自身实际场景选择合适的能力来用好弹性算力。

两种组件的能力及其优势场景概述

  • WorkloadSpread:通过 Webhook 工作机制拦截符合条件的 Pod 创建请求,并对其执行 Patch 操作完成差异化配置注入。适合需要将资源划分为多个弹性分区,并为各分区内 Pod 的 Metadata、Spec 等字段进行自定义配置的现存业务。

  • UnitedDeployment:一种原生支持弹性分区与 Pod 自定义配置的工作负载,具有比 WorkloadSpread 更强的弹性与容量规划能力,适合需要划分多个弹性分区并为各个分区单独进行配置的新业务。

WorkloadSpread:基于 Pod Webhook 的弹性策略插件

WorkloadSpread 是由 OpenKruise 社区提供的一个旁路组件,能够根据特定规则将目标工作负载的 Pod 分布到不同类型的节点上,从而赋予原始工作负载多区域部署和弹性部署的能力。 WorkloadSpread 支持几乎所有原生或自定义的工作负载类型。能够在避免对原始工作负载的直接修改的同时,增强了工作负载在多种环境中的适应性和灵活性,使其能够更好地应对不同的部署需求和运行条件。 下面是一个典型 WorkloadSpread 示例:

apiVersion: apps.kruise.io/v1alpha1
kind: WorkloadSpread
metadata:
name: workloadspread-demo
spec:
targetRef: # WorkloadSpread 同时支持 K8S 原生与 Kruise 工作负载
apiVersion: apps/v1 | apps.kruise.io/v1alpha1
kind: Deployment | CloneSet
name: workload-xxx
subsets:
- name: subset-a
# 前三个副本调度到该 Subset 中
maxReplicas: 3
# Pod 的亲和性配置
requiredNodeSelectorTerm:
matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values:
- zone-a
patch:
# 为调度到该 Subset 中的 Pod 额外注入一个自定义 Label
metadata:
labels:
xxx-specific-label: xxx
- name: subset-b
# 弹性 Subset 部署到 Serverless 集群,不配置容量,副本数不设上限
requiredNodeSelectorTerm:
matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values:
- acs-cn-hangzhou
scheduleStrategy:
# 调度策略,Adaptive 模式会将调度失败的 Pod 重新调度到其他 Subset
type: Adaptive | Fixed
adaptive:
rescheduleCriticalSeconds: 30

强大的分区能力

WorkloadSpread 通过 Subsets 来划分不同的弹性分区,按照 Subsets 顺序正向扩容、逆向缩容。

灵活的调度配置

在 Subset 层面,WorkloadSpread 不仅支持通过 Label 选定节点,还支持细致地自定义 Subset 中 Pod 的污点与容忍度配置。 例如,通过 requiredNodeSelectorTerm字段可以指定必须满足的节点属性、通过 preferredNodeSelectorTerms 字段设置优选的节点属性、以及通过 tolerations字段配置 Pod 对节点污点的容忍度。 这些高级配置选项使得 WorkloadSpread 能够更加精准地控制 Pod 的调度和分布,从而满足各种复杂的部署需求。

在全局层面,WorkloadSpread 支持通过scheduleStrategy字段配置两种不同的调度策略:Fixed 与 Adaptive。Fixed 策略确保 Pod 严格按照预定义的 Subsets 进行分布,即使因为各种原因调度失败。 这对于需要精确控制工作负载分布的场景非常有用。而 Adaptive 策略则提供了更高的灵活性,当某个 Subset 无法满足调度需求时,Pod 可以自动重新调度到其他有可用容量的 Subset 上,从而提高系统的弹性和可靠性。

通过强大且高效的调度策略,WorkloadSpread 能够在复杂的弹性业务场景中确保 Pod 在不同的弹性分区之间实现灵活且均衡的分配。

细致的 Pod 定制配置方式

在 WorkloadSpread 的 subset 配置中,可以通过 patch 字段对调度到该分区的 Pod 进行细致的自定义。几乎所有 Pod 中支持 patch 操作的字段都可以进行修改。 这些字段包括但不限于容器镜像、资源限制、环境变量、卷挂载、启动命令、探针配置和标签等。通过 Pod 字段的精细化定制,Pod 的基本规格(在目标工作负载模板中定义)与环境适配(在 subset 的 patch 字段中定义)实现了有效解耦,从而使得工作负载能够灵活地适配各种不同的分区环境。下面是一些常用的自定义示例:

...
# patch pod with a topology label:
patch:
metadata:
labels:
topology.application.deploy/zone: "zone-a"
...

以上片段展示了如何对 Subset 中的所有 Pod 添加或修改一个 Label。

...
# patch pod container resources:
patch:
spec:
containers:
- name: main
resources:
limit:
cpu: "2"
memory: 800Mi
...

以上片段展示了如何修改一个 Subset 中 Pod 的资源。

...
# patch pod container env with a zone name:
patch:
spec:
containers:
- name: main
env:
- name: K8S_AZ_NAME
value: zone-a
...

以上片段展示了如何修改一个 Subset 中 Pod 的环境变量。

WorkloadSpread 的 Pod Webhook 工作原理

WorkloadSpread 通过 Pod Webhook 直接作用于目标工作负载所创建的 Pod 而非工作负载本身,因此扩缩容操作依然通过操作目标工作负载的 replicas 进行。相比之下,ElasticWorkload 对原有业务的侵入性较大,而 WorkloadSpread 则因为完全不操作目标工作负载、所有功能都由单独的控制器与 Webhook 直接作用于 Pod,因而具有更高的内聚性。

当符合条件的 Pod 创建时,WorkloadSpread 的 Pod Webhook 会将其拦截,并读取相应的 WorkloadSpread 配置。Webhook 会按照扩容顺序,从配置中根据容量情况选择一个合适的 subset 分配给该 Pod,并依据 subset 中定义的调度信息和自定义信息修改 Pod 的配置。同时,控制器还会维护所有相关 Pod 的 controller.kubernetes.io/pod-deletion-cost 标签,从而确保缩容顺序的正确性。

WorkloadSpread 存在的不足方面

WorkloadSpread 在设计上涉及到多个不耦合的组件,并且执行过程较为松散,因此其存在一些不足之处。

Webhook 的潜在风险

WorkloadSpread 依赖 Pod Webhook 来实现其功能,因此它会拦截集群中所有 Pod 的创建请求。当运行 Webhook 的 Pod(即 kruise-manager)性能不足或发生故障时,可能会导致整个集群无法创建新的 Pod。此外,当集群在短时间内需要进行大量的扩缩容操作时,Webhook 也可能会成为性能瓶颈。

作用于 Pod 的局限性

尽管通过 Webhook 直接作用于 Pod 可以减少对用户业务的侵入,但这种方法也引入了一些局限性。例如,对于 CloneSet 而言,它只能通过 partition 字段来控制整个集群内灰度升级的比例,而无法精细地控制每个 subset 中的灰度比例。同样地,WorkloadSpread 的设计注定了其无法将工作负载级别的能力应用到每个 subset 中。

客户案例1:大规模压测场景下进行带宽包分配

该案例中,客户需要在某购物节大促前对线上系统进行压测。为此,客户开发了一个 load-agent 程序用于产生请求,并通过一个 CloneSet 管理 agent 的副本数来控制压测流量大小。在对业务场景进行分析后,客户判断压测需要 3000 个 agent 副本。为了节省成本,客户向云服务商购买了 10 个 200M 的共享带宽包实例(每个共享带宽包可以供300个Pod使用),期望将其动态地分配给弹性伸缩的 agent 副本。

考虑到模拟请求的 CloneSet 是通过客户的压测系统发布与动态伸缩的,不适合重建,因而客户选择给 CloneSet 外挂 WorkloadSpread 来实现带宽包的分配。具体地,客户在压测集群中创建了一个指向上述 CloneSet 的 WorkloadSpread,它包含 11 个 Subset:前 10 个 Subset 的容量为 300,并通过 patch 修改了 Pod 的 Annotations 用于绑定具体的带宽包;最后一个 Subset 没有容量,不指定带宽包,用于阻止万一创建超过 3000 个副本后额外的带宽分配。

apiVersion: apps.kruise.io/v1alpha1
kind: WorkloadSpread
metadata:
name: bandwidth-spread
namespace: loadtest
spec:
targetRef:
apiVersion: apps.kruise.io/v1alpha1
kind: CloneSet
name: load-agent-XXXXX
subsets:
- name: bandwidthPackage-1
maxReplicas: 300
patch:
metadata:
annotations:
k8s.aliyun.com/eip-common-bandwidth-package-id: <id1>

- ...

- name: bandwidthPackage-10
maxReplicas: 300
patch:
metadata:
annotations:
k8s.aliyun.com/eip-common-bandwidth-package-id: <id10>
- name: no-eip

客户案例2:为托管 K8S 集群中的服务弹性扩容到 Serverless 实例提供兼容性

该案例中,客户有一个运行于私有云的服务。由于业务发展需要扩容更多的算力,但是暂时无法扩建线下机房,因此选择使用虚拟节点能力接入云上的 Serverless 弹性算力组成混合云。客户的应用使用了一些加速服务(如 Fluid ),这些服务组件在私有云中通过 DaemonSet 等方式预先部署在了节点上提供服务;但是在云上没有部署基础服务,因而需要为 Pod 额外注入一个 sidecar 来提供加速能力。

客户诉求在现有业务扩容上云的过程中,不能变动原有工作负载(Deployment)的 8 个副本,仅对云上 Pod 的配置做修订。客户通过 WorkloadSpread 给扩容到不同 Subset 的 Pod 分别 打上了用于控制是否注入 Fluid Sidecar 的标签 serverless.fluid.io/inject,以在 Serverless 实例中注入 Sidecar。相关示例如下:

apiVersion: apps.kruise.io/v1alpha1
kind: WorkloadSpread
metadata:
name: data-processor-spread
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: data-processor
subsets:
- name: local
maxReplicas: 8
patch:
metadata:
labels:
serverless.fluid.io/inject: "false"
- name: aliyun-acs
patch:
metadata:
labels:
serverless.fluid.io/inject: "true"

UnitedDeployment:原生具备 Serverless 弹性能力的工作负载

UnitedDeployment 是 OpenKruise 社区提供的一种原生支持分区管理的轻量化高级工作负载。与 ElasticWorkload 和 WorkloadSpread 对基础工作负载进行增强的设计理念不同,UnitedDeployment 提供了一种全新的模式来管理分区弹性应用。UnitedDeployment 资源通过一个模板来定义应用,控制器则通过创建并管理多个次级工作负载来匹配不同的分区。UnitedDeployment 最显著的特征在于,它作为一个一体化的弹性负载,在单个资源中同时完成应用定义、分区定义、容量管理、扩缩容和应用升级等应用全生命周期管理。以下是一个典型的 UnitedDeployment 资源示例:

apiVersion: apps.kruise.io/v1alpha1
kind: UnitedDeployment
metadata:
name: sample-ud
spec:
replicas: 6
selector:
matchLabels:
app: sample
template:
# UnitedDeployment 会通过此模板为每个分区创建一个 CloneSet
cloneSetTemplate:
metadata:
labels:
app: sample
spec:
# CloneSet Spec
...
topology:
subsets:
- name: ecs
# 该分区的副本调度到具有标签 ecs 的托管节点池上
nodeSelectorTerm:
matchExpressions:
- key: node
operator: In
values:
- ecs
# 该分区最多有两个副本
maxReplicas: 2
- name: acs-serverless
# acs 弹性分区副本数无限
nodeSelectorTerm:
matchExpressions:
- key: node
operator: In
values:
- acs-virtual-kubelet

UnitedDeployment 的优势

一站式弹性应用管理

UnitedDeployment 的一站式应用管理体现在只需要使用一个资源就可以完成应用的定义、分区、扩缩容、升级等操作。

UnitedDeployment 控制器会根据工作负载模板,为每个分区管理一个对应类型的次级工作负载,这些次级工作负载无需用户额外关注。在上图中,用户只需要管理蓝色的应用与分区,UnitedDeployment 控制器则会根据全局配置完成后续对各个次级工作负载的管理工作,包括创建、修改、删除等。除了直接对次级工作负载的管理,在必要时,控制器还会监控这些工作负载创建的 Pod 状态以进行相应的调整。由于所有相关的资源全都由控制器直接管理,资源的操作的发生时机是可控的。因此,UnitedDeployment 控制器总能获取相关资源的正确信息,并在正确的时机对其进行操作,而不会发生一致性问题。

用户在 UnitedDeployment 资源中配置的副本数将会被控制器适当地分配到各个次级工作负载上,由工作负载控制器实施具体的扩缩容操作。因此, 使用 UnitedDeployment 的扩缩容与直接使用对应的工作负载扩缩容产生完全相同的效果,用户不需要额外学习。

得益于对次级工作负载的直接管理,UnitedDeployment 具备了对应用进行更新和升级的能力。当工作负载模板中的配置发生任何变更时,这些变更将被同步到相应的次级工作负载中,并由工作负载控制器执行具体的升级逻辑。这意味着,例如 CloneSet 的原地升级等特性可以在各个分区内正常生效。此外,如果分区内的次级工作负载支持灰度升级特性,UnitedDeployment 还能够统一它们的 partition 字段值,从而实现各分区之间更细致的灰度控制。

细致的分区配置

UnitedDeployment 内置了两种容量分配算法,让用户可以通过细致的分区容量配置来应对弹性应用的各种场景。

弹性分配算法实现了与 ElasticWorkload、WorkloadSpread 类似的经典弹性容量分配方式:通过为每个分区设定容量的上下限,让 Pod 按照分区的定义顺序正序扩容、逆序缩容。这种方法上文已经进行了充分介绍,此处不再赘述。

指定分配算法则是一种新的容量分配方式。这种方式通过固定数值或百分比直接为部分分区指定容量,并预留至少一个弹性分区用于平摊剩余的副本。指定分配算法可以适配一些传统弹性分配算法无法应对的场景。比如:固定容量适合管控节点、入口网关等组件;百分比容量则适合将核心副本分散到不同地域节点,并预留一些突发弹性资源。

除了容量分配,UnitedDeployment 还允许为每个分区分别配置 Pod 的任何 Spec 字段(包括容器镜像, Image),这为 UnitedDeployment 的分区配置赋予了更为强大的灵活性:理论上甚至可以使用一个 UnitedDeployment 管理部署一整套微服务应用或是大模型推理管线(不推荐这么做)。

自适应弹性能力

UnitedDeployment 具备强大的自适应弹性能力,能够自动地完成扩缩容和重调度等操作,而无需用户过多关注,降低运维成本。

UnitedDeployment 支持 Kubernetes 的水平自动扩展(HPA),能够根据预先配置的条件自动地进行扩缩容操作——这些操作依然严格遵循分区配置。通过 HPA,应用在某个分区资源即将耗尽时,能够自动向其他分区扩容,实现自动伸缩,从而提高资源利用率。

UnitedDeployment 还具有自适应 Pod 重调度能力。当控制器发现某个分区中存在一些由于各种原因调度失败而长时间处于 Pending 状态的 Pod 时,会将其重新调度到其他分区,以确保可用 Pod 副本的数量,并且依旧遵循分区配置的容量分配策略。在扩容过程中,控制器会避免将 Pod 分配到不可调度的分区中,即使该分区还有剩余容量。**用户能够配置调度失败的超时时间以及分区从不可用状态中恢复的时间,从而更好地对自适应调度能力进行控制。 **

得益于 UnitedDeployment 强大的自适应能力,用户在使用该组件时可以仅进行分区划分,而不必详细规划每个分区的容量。控制器将自动在不同的分区间分配副本数量,无需用户干预。

UnitedDeployment 的三级负载模式

UnitedDeployment 包含了三个级别的工作负载。UnitedDeployment 自身作为一级负载,包含负载模板、分区配置以及副本数量。控制器会为每个分区(Subset)创建并管理一个次级工作负载。

这些次级工作负载是 UnitedDeployment 中负载模板应用于对应分区配置(如调度策略、计算后的副本数、Pod 自定义等)后生成的具体实例。这些次级工作负载实例的控制器(如 Deployment、CloneSet、StatefulSet 等)将会管理作为三级负载的 Pod。

三级工作负载的模式使得 UnitedDeployment 不直接管理 Pod,而是能够复用次级工作负载的各种能力。未来,随着 UnitedDeployment 支持更多次级负载类型,其功能也将得到进一步扩展,从而支持更多、更复杂的弹性场景。

UnitedDeployment 存在的不足之处

UnitedDeployment 的许多优势均源自其作为一个独立工作负载所具备的一站式管理能力,但这也导致了其具有业务改造侵入性较高的不足。对于用户的现有业务,需要对上层平台(运维系统、发布系统等)进行改造,以便从现有的 Deployment、CloneSet 等工作负载切换到 UnitedDeployment。

客户案例1:K8S 集群中, Pod 弹性扩展到 ACS 实例,并针对 Serverless 容器做适配

现在的云服务商提供的 K8S 服务大致上可以分为以下三种:

  1. 用户购买云服务器作为固定节点的托管集群
  2. 通过虚拟节点技术直接交付容器算力的 Serverless 集群
  3. 同时包含托管节点与虚拟节点的混合集群

该案例中,客户准备上线一个新的业务,该业务预计具有很明显的峰谷:在高峰期与低谷期流量有数十倍的差距。为了应对这种业务特性,客户选择采购了一批 ECS 组成 托管集群用于承载基础流量,并在业务高峰期到来时快速地将新的副本弹性扩容到 ACS 中。同时,客户的应用程序具有一定的特殊性,在 Serverless 环境需要做一些额外的配置才能运行。

客户对这个新业务的工作负载的诉求是,保证优先用完 ECS 资源的前提下,通过 HPA 自动地弹性伸缩,同时针对不同的环境注入不同的环境变量。对于这种比较复杂的弹性应用场景,UnitedDeployment 比较符合需求。其中一个配置的实例如下:

apiVersion: apps.kruise.io/v1alpha1
kind: UnitedDeployment
metadata:
name: elastic-app
spec:
# 省略业务的工作负载模板
...
topology:
# 启动 Adaptive 调度,自适应地将 Pod 副本调度到 ECS 节点池与 ACS 实例
scheduleStrategy:
type: Adaptive
adaptive:
# ECS 节点调度失败 10 秒后开始调度到 ACS Serverless 实例
rescheduleCriticalSeconds: 10
# 上述调度失败后 1 小时内不再调度到 ECS 节点
unschedulableLastSeconds: 3600
subsets:
# 优先调度 ECS 并不设上限,直到调度失败才将 Pod 调度到 ACS
# 缩容时,先删除 ACS 实例,之后再删除 ECS 节点池中的 Pod
- name: ecs
nodeSelectorTerm:
matchExpressions:
- key: type
operator: NotIn
values:
- acs-virtual-kubelet
- name: acs-serverless
nodeSelectorTerm:
matchExpressions:
- key: type
operator: In
values:
- acs-virtual-kubelet
# 通过 patch 修改调度到弹性算力的 Pod 环境变量,启用应用的 Serverless 模式
patch:
spec:
containers:
- name: main
env:
- name: APP_RUNTIME_MODE
value: SERVERLESS
---
# 结合 HPA 自动扩缩容
apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
name: elastic-app-hpa
spec:
minReplicas: 1
maxReplicas: 100
metrics:
- resource:
name: cpu
targetAverageUtilization: 2
type: Resource
scaleTargetRef:
apiVersion: apps.kruise.io/v1alpha1
kind: UnitedDeployment
name: elastic-app

客户案例2:为调度到不同架构 ECS 上的 Pod 分配不同的资源

该案例中,客户分别采购了 Intel 、AMD 与 ARM 平台 CPU 的若干 ECS 实例,准备上线一个新的服务。用户希望调度到不同平台的 Pod 实例能表现出相近的性能,承载相同的 QPS。经过压测,发现以 Intel CPU 为基准,要承载同样的压力,AMD 平台需要更多的 CPU 核数,而 ARM 平台需要更多的内存。同时,客户采购的三种服务器中,按照算力单位,Intel 平台占约一半,AMD 与 ARM 各占约四分之一。

客户诉求这个新服务使用的 Workload 能够将 Pod 按照比例均摊到不同的算力上,并且针对不同的算力平台配置不同的资源量,以向下游调用方提供较为稳定的性能。使用 UnitedDeployment 的 Pod 自定义能力配合指定容量分配算法,可以基本满足需求。其中一个配置的实例如下:

apiVersion: apps.kruise.io/v1alpha1
kind: UnitedDeployment
metadata:
name: my-app
spec:
replicas: 4
selector:
matchLabels:
app: my-app
template:
deploymentTemplate:
... # 省略业务工作负载模板
topology:
# intel、amd、倚天710ARM机型分别承载50%、25%、25%的副本
subsets:
- name: intel
replicas: 50%
nodeSelectorTerm:
... # 通过 label 选中 Intel 节点池
patch:
spec:
containers:
- name: main
resources:
limits:
cpu: 2000m
memory: 4000Mi
- name: amd64
replicas: 25%
nodeSelectorTerm:
... # 通过 label 选中 AMD 节点池
# AMD 平台分配更多 CPU
patch:
spec:
containers:
- name: main
resources:
limits:
cpu: 3000m
memory: 4000Mi
- name: yitian-arm
replicas: 25%
nodeSelectorTerm:
... # 通过 label 选中 ARM 节点池
# ARM 平台分配更多内存
patch:
spec:
containers:
- name: main
resources:
limits:
cpu: 2000m
memory: 6000Mi

总结

弹性算力能够极大地降低业务的成本,并且有效地提高服务的性能上限。要用好弹性算力,需要根据具体业务特点,选择合适的弹性技术。下表是本文介绍的四种组件的能力对比总结,希望能够给您提供一定的参考。

组件分区原理易改造 程度 (非侵入性)分区 细致度弹性能力
WorkloadSpread通过 Webhook 修改 Pod
UnitedDeployment通过模板创建多个工作负载

OpenKruise v0.10.0 新特性WorkloadSpread解读

· 阅读需要 1 分钟
GuangLei Cao
Contributor of OpenKruise

背景

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 constraints 是Kubernetes社区提供的方案,可以定义按 topology key 的水平打散。用户在定义完后,调度器会依据配置选择符合分布条件的node。

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

UnitedDeployment

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

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


应用场景

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

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

case-1

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. 优先部署到基础节点池,资源不足再部署到弹性资源池。

case-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

case-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内部容器的资源配额,环境变量等。

case-4

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 造成额外成本。

arch

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

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

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

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请求时:

  1. 根据 subsetStatuses 顺序依次找 missingReplicas 大于 0 或为 -1 的 suitable subset。
  2. 找到suitable subset后,如果 missingReplicas 大于 0,则先减 1 并尝试更新 workloadspread status。
  3. 如果更新成功,则将该 subset定义的规则注入到 pod 中。
  4. 如果更新失败,则重新 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 控制即可呢?答案是不行,因为:

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

因此,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 能力,逻辑上分为两种:

  1. SimulationSchedule:在 Kruise webhook 中根据 informer 里已有的 nodes/pods 数据,组装出调度账本,对 Pod 进行模拟调度。即通过 nodeSelector/affinity、tolerations、以及基本的 resources 资源,做一次简单的过滤。(对于 vk 这种节点不太适用)
  2. 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,帮助用户解决更多云原生部署方面的难题,构建一个更好的社区。


参考文献