Kubernetes Autoscaler Balancer 提案解析:用 CRD 控制器在多域部署间统一分配与自动伸缩 Pod 副本
【免费下载链接】autoscalerAutoscaling components for Kubernetes项目地址: https://gitcode.com/GitHub_Trending/au/autoscaler
导读
本文基于 Kubernetes Autoscaler 仓库(au/autoscaler)中 Balancer 组件的 KEP 设计文档(balancer/proposals/balancer.md)展开,系统讲解 Balancer 的诞生背景、核心 API 设计、两种副本分配策略(Priority 与 Proportional)以及 Fallback 机制,并结合仓库内的 CRD 定义、控制器源码与 Nginx 实战示例,给出可直接落地的部署与配置方案。读完本文,你将理解如何在多可用区、混合 spot/on-demand 节点等"多域"场景下,用单个 Balancer 对象同时管理多个 Deployment 的副本分布与水平伸缩,并掌握其底层策略算法与控制器工作流。
背景:多域部署下的副本分配难题
运行 Kubernetes 工作负载时,用户常常希望把 Pod 分散部署到多个"域"(domain)中,同时保持副本数量的均衡与自动伸缩。这些域可能包括:
- 同一区域内的多个云厂商可用区(zone):保证即使某个可用区故障,应用仍能继续运行;
- 不同类型的 Kubernetes 节点:如 spot/preemptible(竞价/抢占式)实例与按需实例、不同机器族等。
一个单一的 Kubernetes Deployment 要么把放置完全交给调度器(极可能导致所有 Pod 涌入同一个域),要么只专注于单一域(无法达成跨多域分布的目标)。
PodTopologySpreading 的不足
PodTopologySpreading 解决了部分问题,但并不彻底:
- 它只支持"均匀"分布(even spreading),一旦部署发生倾斜就不会主动再平衡;
- 它(配合 skew 和/或
ScheduleAnyway标志)本质上只是一个"提示":当倾斜放置可行且被允许时,Cluster Autoscaler 不会被触发,用户最终会得到一个倾斜的部署; - 若用户配置严格的拓扑分布约束(strict pod topology spreading),那么在出问题时部署不会把 Pod 迁移到可用域;同时由于可用域过于倾斜,部署的扩容也会被完全阻塞。
多 Deployment + 多 HPA 的不稳定
如果追求完全灵活性,唯一办法是部署多个 Deployment,各自瞄准不同域。但这种方案带来一个重大问题:如何一致地对多个 Deployment 进行自动伸缩?
最简单的思路——为每个 Deployment 配置独立的 HPA——并不稳定:由于负载不同、竞态条件等因素,可能出现某些域扩容、另一些域缩容的情况。由于 HPA 与 Deployment 之间没有任何关联,倾斜状态不会自动修复,最终也许能到达一个半均衡状态,但不保证一定如此。
Balancer:多域副本分配的统一控制器
正是上述痛点催生了 Balancer 组件。它需要一个能完成以下四件事的组件:
- 保持多个 Deployment 对齐:例如维持各 Deployment 之间 Pod 数量的固定比例,或者把副本全部放入第一个域、溢出部分放入第二个域,依此类推;
- 响应单个 Deployment 的问题:无论是某个可用区宕机,还是 spot/preemptible 虚拟机短缺;
- 主动尝试再平衡,逐步回到期望的布局;
- 以单一目标对全部 Deployment 进行自动伸缩,同时维持放置策略。
什么是 Balancer
Balancer 是一个独立控制器,运行在用户空间(必要时也可运行在控制平面),对外暴露一个同样名为 Balancer 的CRD API 对象。每个 Balancer 对象持有对多个 Deployment(或其他暴露 Scale 子资源的 Pod 控制对象)的引用。Balancer 周期性检查每个目标内运行中与有问题的 Pod 数量,与期望副本数、约束及策略进行比较,并在某个目标运行过多或过少副本时调整其副本数。
为了让 Balancer 自身能够成为HPA 的伸缩目标,Balancer 同样对外暴露Scale 子资源——这一设计在 balancer/deploy/crd.yaml 中体现为subresources.scale声明(specReplicasPath: .spec.replicas、statusReplicasPath: .status.replicas、labelSelectorPath: .status.selector),HPA 可以直接把scaleTargetRef指向 Balancer。
Balancer API 详解
KEP 文档给出了完整的 API 类型定义,以下是其核心 Go 结构(该定义在仓库中的实际实现见 balancer/pkg/apis/balancer.x-k8s.io/v1alpha1/types.go):
// Balancer is an object used to automatically keep the desired number of // replicas (pods) distributed among the specified set of targets (deployments // or other objects that expose the Scale subresource). type Balancer struct { metav1.TypeMeta // Standard object metadata. // +optional metav1.ObjectMeta // Specification of the Balancer behavior. Spec BalancerSpec // Current information about the Balancer. // +optional Status BalancerStatus } // BalancerSpec is the specification of the Balancer behavior. type BalancerSpec struct { // Targets is a list of targets between which Balancer tries to distribute // replicas. Targets []BalancerTarget // Replicas is the number of pods that should be distributed among the // declared targets according to the specified policy. Replicas int32 // Selector that groups the pods from all targets together (and only those). // Ideally it should match the selector used by the Service built on top of the // Balancer. All pods selectable by targets' selector must match to this selector, // however target's selector don't have to be a superset of this one (although // it is recommended). Selector metav1.LabelSelector // Policy defines how the balancer should distribute replicas among targets. Policy BalancerPolicy } // BalancerTarget is the declaration of one of the targets between which the balancer // tries to distribute replicas. type BalancerTarget struct { // Name of the target. The name can be later used to specify // additional balancer details for this target. Name string // ScaleTargetRef is a reference that points to a target resource to balance. // The target needs to expose the Scale subresource. ScaleTargetRef hpa.CrossVersionObjectReference // MinReplicas is the minimum number of replicas inside of this target. // Balancer will set at least this amount on the target, even if the total // desired number of replicas for Balancer is lower. // +optional MinReplicas *int32 // MaxReplicas is the maximum number of replicas inside of this target. // Balancer will set at most this amount on the target, even if the total // desired number of replicas for the Balancer is higher. // +optional MaxReplicas *int32 } // BalancerPolicyName is the name of the balancer Policy. type BalancerPolicyName string const ( PriorityPolicyName BalancerPolicyName = "priority" ProportionalPolicyName BalancerPolicyName = "proportional" ) // BalancerPolicy defines Balancer policy for replica distribution. type BalancerPolicy struct { // PolicyName decides how to balance replicas across the targets. // Depending on the name one of the fields Priorities or Proportions must be set. PolicyName BalancerPolicyName // Priorities contains detailed specification of how to balance when balancer // policy name is set to Priority. // +optional Priorities *PriorityPolicy // Proportions contains detailed specification of how to balance when // balancer policy name is set to Proportional. // +optional Proportions *ProportionalPolicy // Fallback contains specification of how to recognize and what to do if some // replicas fail to start in one or more targets. No fallback happens if not-set. // +optional Fallback *Fallback } // PriorityPolicy contains details for Priority-based policy for Balancer. type PriorityPolicy struct { // TargetOrder is the priority-based list of Balancer targets names. The first target // on the list gets the replicas until its maxReplicas is reached (or replicas // fail to start). Then the replicas go to the second target and so on. MinReplicas // is guaranteed to be fulfilled, irrespective of the order, presence on the // list, and/or total Balancer's replica count. TargetOrder []string } // ProportionalPolicy contains details for Proportion-based policy for Balancer. type ProportionalPolicy struct { // TargetProportions is a map from Balancer targets names to rates. Replicas are // distributed so that the max difference between the current replica share // and the desired replica share is minimized. Once a target reaches maxReplicas // it is removed from the calculations and replicas are distributed with // the updated proportions. MinReplicas is guaranteed for a target, irrespective // of the total Balancer's replica count, proportions or the presence in the map. TargetProportions map[string]int32 } // Fallback contains information how to recognize and handle replicas // that failed to start within the specified time period. type Fallback struct { // StartupTimeout defines how long will the Balancer wait before considering // a pending/not-started pod as blocked and starting another replica in some other // target. Once the replica is finally started, replicas in other targets // may be stopped. StartupTimeout metav1.Duration } // BalancerStatus describes the Balancer runtime state. type BalancerStatus struct { // Replicas is an actual number of observed pods matching Balancer selector. Replicas int32 // Selector is a query over pods that should match the replicas count. This is same // as the label selector but in the string format to avoid introspection // by clients. The string will be in the same format as the query-param syntax. Selector string // Conditions is the set of conditions required for this Balancer to work properly, // and indicates whether or not those conditions are met. // +optional // +patchMergeKey=type // +patchStrategy=merge Conditions []metav1.Condition }API 字段与 CRD 校验规则
对照 balancer/deploy/crd.yaml 中的 OpenAPI v3 Schema,可以发现若干关键校验约束(在 types.go 中通过 kubebuilder 标记声明):
| 字段 | 类型 | 校验/默认规则 | 说明 |
|---|---|---|---|
spec.targets | 数组 | 必填,至少 2 项 | 每个目标必须有name(长度 ≥ 1)与scaleTargetRef(kind + name) |
spec.replicas | int32 | 必填,最小 0 | Balancer 期望分发的总副本数 |
spec.selector | LabelSelector | 必填 | 将所有目标的 Pod 归为一组的标签选择器 |
spec.policy.policyName | string | 必填 | 取值priority或proportional |
spec.policy.priorities.targetOrder | string 数组 | 必填,至少 2 项 | priority 策略的目标顺序 |
spec.policy.proportions.targetProportions | map | 必填,至少 2 个键 | proportional 策略的权重映射 |
spec.policy.fallback.startupTimeoutSeconds | int32 | 必填(若配置 fallback),最小 0 | 判定 Pod 启动阻塞的超时秒数 |
target.minReplicas | int32 | 可选,最小 0 | 未设置时按 0(无下限)处理 |
target.maxReplicas | int32 | 可选,最小 0 | 未设置时不设上限 |
spec.replicas与status.replicas、status.selector通过 Scale 子资源暴露,这正是 HPA 能够以 Balancer 为伸缩目标的关键所在。
部署 Balancer 控制器
Balancer 的部署清单位于 balancer/deploy/controller.yaml,包含四部分:ServiceAccount、ClusterRole、ClusterRoleBinding 与 Deployment。其核心要点如下。
RBAC 权限(ClusterRole)覆盖:
balancers与balancers/status的读写(get/list/watch/patch/update);pods的 get/list/watch,用于统计各目标内的 Pod 状态;deployments/scale的 get/list/watch/patch/update,用于读取和调整各目标的副本数;events的创建与记录,便于排查控制器行为。
控制器 Deployment:
apiVersion: apps/v1 kind: Deployment metadata: name: balancer-controller namespace: kube-system spec: replicas: 1 selector: matchLabels: app: balancer-controller template: metadata: labels: app: balancer-controller spec: serviceAccountName: balancer-controller containers: - name: controller image: gcr.io/gke-autoscaling-gcr/balancer:0.1.1 imagePullPolicy: Always args: ["-v","4"] resources: requests: cpu: 100m部署步骤:
- 先安装 CRD:
kubectl apply -f balancer/deploy/crd.yaml(注册balancers.balancer.x-k8s.io这个 Namespaced 资源,v1alpha1版本); - 再部署控制器:
kubectl apply -f balancer/deploy/controller.yaml; - 通过
kubectl -n kube-system get pods -l app=balancer-controller确认控制器运行正常。
控制器本身采用标准的 informer + 限速工作队列模式实现(见 balancer/pkg/controller/controller.go),使用-v参数控制 klog 日志级别,事件会以balancer-controller为事件源写入集群。
实战示例一:Priority 策略(优先填满顺序目标)
Priority 策略适用于"优先使用廉价/首选域,容量不足再溢出到备用域"的场景,例如优先使用 spot 节点池、溢出到按需节点池。完整示例见 balancer/examples/nginx-priority.yaml:
apiVersion: apps/v1 kind: Deployment metadata: name: nginx-1 labels: app: nginx-1 srv: nginx spec: replicas: 3 selector: matchLabels: app: nginx-1 srv: nginx template: metadata: labels: app: nginx-1 srv: nginx spec: containers: - name: nginx image: nginx:1.14.2 ports: - containerPort: 80 --- apiVersion: apps/v1 kind: Deployment metadata: name: nginx-2 labels: app: nginx-2 srv: nginx spec: replicas: 3 selector: matchLabels: app: nginx-2 srv: nginx template: metadata: labels: app: nginx-2 srv: nginx spec: containers: - name: nginx image: nginx:1.14.2 ports: - containerPort: 80 --- apiVersion: balancer.x-k8s.io/v1alpha1 kind: Balancer metadata: name: nginx spec: replicas: 5 selector: matchLabels: srv: nginx policy: policyName: priority priorities: targetOrder: [nginx-1,nginx-2] fallback: startupTimeoutSeconds: 180 targets: - name: nginx-1 scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: nginx-1 minReplicas: 1 maxReplicas: 7 - name: nginx-2 scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: nginx-2 minReplicas: 1 --- apiVersion: v1 kind: Service metadata: name: nginx spec: ports: - port: 80 protocol: TCP targetPort: 80 selector: srv: nginx该示例的分配逻辑:
spec.replicas: 5,两个 Deployment 各自minReplicas: 1;- Balancer 会先把每个目标的
minReplicas填满(各 1 个),剩余 3 个副本按targetOrder: [nginx-1, nginx-2]顺序分配:先填nginx-1(其maxReplicas: 7,不会触顶),因此最终形态为nginx-1 = 4、nginx-2 = 1; - 若
nginx-1中某个副本在 180 秒内未能启动(fallback 生效),Balancer 会在nginx-2中补一个副本,待nginx-1恢复后再缩回。
Priority 策略的源码实现位于 balancer/pkg/policy/priority.go 的distributeByPriority函数:
- 第一轮为所有目标放置
minReplicas,从总副本数中扣除;若不足(replicas < 0),记录MissingReplicas问题并将余量置 0; - 第二轮按
targetOrder依次填充,每个目标可容纳max - placement[key]个副本,直到replicas耗尽; - 每处理一个目标,若其存在
NotStartedWithinDeadline > 0(超过超时仍未启动的副本),会把这些"被阻塞"的副本加回待分配池,溢出到后续目标; - 若仍有剩余副本无处安放,则记录
OverflowReplicas。
实战示例二:Proportional 策略(按比例均衡分布)
Proportional 策略适用于"多可用区均衡承载流量"的场景,例如在两个可用区之间按 50/50 分配副本。完整示例见 balancer/examples/nginx-proportional.yaml:
apiVersion: apps/v1 kind: Deployment metadata: name: nginx-1 labels: app: nginx-1 srv: nginx spec: replicas: 3 selector: matchLabels: app: nginx-1 srv: nginx template: metadata: labels: app: nginx-1 srv: nginx spec: containers: - name: nginx image: nginx:1.14.2 ports: - containerPort: 80 --- apiVersion: apps/v1 kind: Deployment metadata: name: nginx-2 labels: app: nginx-2 srv: nginx spec: replicas: 3 selector: matchLabels: app: nginx-2 srv: nginx template: metadata: labels: app: nginx-2 srv: nginx spec: containers: - name: nginx image: nginx:1.14.2 ports: - containerPort: 80 --- apiVersion: balancer.x-k8s.io/v1alpha1 kind: Balancer metadata: name: nginx spec: replicas: 10 selector: matchLabels: srv: nginx policy: policyName: proportional proportions: targetProportions: nginx-1: 50 nginx-2: 50 fallback: startupTimeoutSeconds: 180 targets: - name: nginx-1 scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: nginx-1 minReplicas: 1 maxReplicas: 7 - name: nginx-2 scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: nginx-2 minReplicas: 1 --- apiVersion: v1 kind: Service metadata: name: nginx spec: ports: - port: 80 protocol: TCP targetPort: 80 selector: srv: nginx该示例的分配逻辑:
spec.replicas: 10,比例nginx-1: 50、nginx-2: 50,即 1:1;- 扣除两个目标的
minReplicas(各 1)后剩余 8 个副本按比例分配,最终趋近nginx-1 = 5、nginx-2 = 5; - 若某个目标达到
maxReplicas,它会从比例计算中移除,剩余副本按更新后的比例在其余目标间重新分配; minReplicas的保证与优先级、比例甚至是否出现在targetProportions映射中均无关。
Proportional 策略的源码实现位于 balancer/pkg/policy/proportional.go:
distributeByProportions先放置所有目标的最小副本数,再做第一轮比例分配(忽略未启动的副本);随后统计存在NotStartedWithinDeadline > 0的目标,把需要补的副本仅分配到无问题(not-blocked)的目标中;- 实际按比例分发由
distributeGroupProportionally完成,它采用D'Hondt 最高平均数法:每次迭代计算每个目标proportion / (1 + placement[k])作为排名,选择排名最高的目标放入 1 个副本,直至副本耗尽或所有目标容量用尽; sortedKeysWithCapacity对所有尚有容量的目标按键名排序,保证算法稳定——每次按相同顺序处理目标,避免副本在目标之间来回抖动(flapping);- 无论比例策略还是优先级策略,目标顺序都先按名称排序,这是算法确定性(不震荡)的关键设计。
与 HPA 集成:以 Balancer 为伸缩目标
Balancer 的一个核心价值是"用单一伸缩目标驱动多个 Deployment"。由于 Balancer 自身暴露 Scale 子资源,可以像 Deployment 一样作为 HPA 的scaleTargetRef。完整示例见 balancer/examples/nginx-priority-hpa.yaml,其关键差异是在两个 Deployment 的容器中增加了 CPU request(HPA 计算 CPU 利用率所必需):
apiVersion: apps/v1 kind: Deployment metadata: name: nginx-1 labels: app: nginx-1 srv: nginx spec: replicas: 3 selector: matchLabels: app: nginx-1 srv: nginx template: metadata: labels: app: nginx-1 srv: nginx spec: containers: - name: nginx image: nginx:1.14.2 ports: - containerPort: 80 resources: requests: cpu: 100m --- apiVersion: apps/v1 kind: Deployment metadata: name: nginx-2 labels: app: nginx-2 srv: nginx spec: replicas: 3 selector: matchLabels: app: nginx-2 srv: nginx template: metadata: labels: app: nginx-2 srv: nginx spec: containers: - name: nginx image: nginx:1.14.2 ports: - containerPort: 80 resources: requests: cpu: 100m --- apiVersion: balancer.x-k8s.io/v1alpha1 kind: Balancer metadata: name: nginx spec: replicas: 5 selector: matchLabels: srv: nginx policy: policyName: priority priorities: targetOrder: [nginx-1,nginx-2] fallback: startupTimeoutSeconds: 180 targets: - name: nginx-1 scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: nginx-1 minReplicas: 1 maxReplicas: 7 - name: nginx-2 scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: nginx-2 minReplicas: 1 --- apiVersion: v1 kind: Service metadata: name: nginx spec: ports: - port: 80 protocol: TCP targetPort: 80 selector: srv: nginx --- apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: nginx spec: minReplicas: 2 maxReplicas: 10 metrics: - resource: name: cpu target: averageUtilization: 50 type: Utilization type: Resource scaleTargetRef: apiVersion: balancer.x-k8s.io/v1alpha1 kind: Balancer name: nginx这里的协同工作方式是:
- HPA 负责"伸缩多少":根据 CPU 平均利用率(50%)在 2~10 之间调整 Balancer 的
spec.replicas; - Balancer 负责"分布到哪":收到新的总副本数后,按 priority 策略把副本优先分配给
nginx-1,溢出给nginx-2; - 两个 Deployment 上不再配置各自的 HPA,彻底避免多 HPA 之间的竞态与倾斜。
控制器工作流:从轮询到落地的五个阶段
Balancer 控制器的核心协调逻辑在 balancer/pkg/controller/core.go 的ProcessBalancer方法中,按五个阶段执行(每个阶段失败都会返回带阶段信息的BalancerError):
- ScaleSubresourcePolling(轮询 Scale 子资源):遍历
spec.targets,通过 ScaleClient 获取每个目标的 Scale 对象与 GroupResource; - PodListing(列出 Pod):解析每个目标 Scale 状态中的
selector,用 Pod lister 列出对应 Pod; - PodLabelsChecking(校验 Pod 标签):逐一确认这些 Pod 是否匹配 Balancer 的
spec.selector,不匹配即报错——这保证了 Balancer 统计的副本数与 Service 路由的 Pod 集合一致; - ApplyingPolicyListing(应用策略):调用 balancer/pkg/policy/policy.go 的
GetPlacement,根据policyName分发副本并返回每个目标的目标副本数ReplicaPlacement;若配置缺失(如 priority 策略未提供priorities或targetOrder),会返回incomplete policy definition类错误; - ReplicaCountSetting(设置副本数):对每个目标,若其当前
scale.Spec.Replicas与计算出的目标值不一致,则调用UpdateScale进行更新。
Pod 状态统计由 balancer/pkg/pods/summary.go 的CalculateSummary完成,它是所有策略的输入基础:
Total:Running 与 Pending 状态的 Pod 总数;Running:已运行 Pod 数;NotStartedWithinDeadline:仍处于 Pending、且创建时间超过超时阈值的 Pod 数——这些 Pod 被判定为"启动受阻",触发 Fallback 逻辑;- 未设置
fallback时,内部会使用一个约 50 年的"无穷大"截止时间(infDeadline),相当于永不触发回退。
控制器的调度侧(balancer/pkg/controller/controller.go)使用 Balancer informer 的事件回调把对象入队,并采用NewFixedItemIntervalRateLimiter(resync)限速工作队列,以固定的 resync 周期周期性处理每个 Balancer,从而实现"周期性检查、主动再平衡"的持续收敛。
Fallback 机制:应对启动受阻的副本
Fallback(回退)是 Balancer 保障可用性的关键机制,定义在spec.policy.fallback:
startupTimeoutSeconds是唯一字段(最小 0),表示 Balancer 在判定某个 Pending/未启动 Pod 为"受阻"之前等待的时间;- 一旦某个目标内的副本被视为受阻,Balancer 会在其他目标中启动额外副本,直到受阻副本最终启动后再停止这些多余副本(见
distributeByPriority与distributeByProportions中基于NotStartedWithinDeadline的 fallback 计算); - 未配置
fallback时不做任何回退。
这正好回应用了引言中的诉求:当某个可用区宕机或 spot 节点池枯竭导致 Pod 长时间无法调度时,Balancer 能主动把副本转移到可用域,而不是让整个部署停滞或倾斜。
小结
Balancer 以"一个 CRD 对象 + 一个控制器"的轻量形态,把**副本总量决策(交给 HPA)与副本分布决策(交给策略)**彻底解耦:
- Priority 策略适合"首选域优先、容量溢出"的成本优化场景;
- Proportional 策略适合"跨域均衡承载"的高可用场景;
- Fallback保证域级故障时副本可迁移;
- Scale 子资源 + Selector 约束保证 Balancer 能与 Service、HPA 无缝协作。
如果需要在同一项目中继续深入,建议阅读以下文件:
- 设计文档:balancer/proposals/balancer.md
- API 类型定义:balancer/pkg/apis/balancer.x-k8s.io/v1alpha1/types.go
- CRD 与部署清单:balancer/deploy/crd.yaml、balancer/deploy/controller.yaml
- 实战示例:balancer/examples/nginx-priority.yaml、balancer/examples/nginx-proportional.yaml、balancer/examples/nginx-priority-hpa.yaml
- 策略算法:balancer/pkg/policy/policy.go、balancer/pkg/policy/priority.go、balancer/pkg/policy/proportional.go
- 控制器实现:balancer/pkg/controller/core.go、balancer/pkg/controller/controller.go
【免费下载链接】autoscalerAutoscaling components for Kubernetes项目地址: https://gitcode.com/GitHub_Trending/au/autoscaler
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考