Cluster Autoscaler 节点组伸缩
一次版本发布把 API 副本从 20 提到 60,三十多个 Pod 很快进入 Pending。云控制台里的节点组允许从 6 扩到 30,Cluster Autoscaler Pod 也保持 Running,可十分钟后节点数仍没有变化。调度事件写着 Insufficient cpu 和 node(s) didn't match Pod's node affinity,Autoscaler 日志却给出 NotTriggerScaleUp:现有节点组模板没有业务要求的磁盘标签,增加再多同类节点也无法让这些 Pod 可调度。
团队临时补上标签后,节点终于创建,却又卡在 NotReady。启动模板里的 kubelet 使用了错误的集群引导凭证,云实例已经计费,但没有注册为 Kubernetes Node。修好引导后流量恢复,夜间缩容又被一个没有控制器 owner 的调试 Pod 和严格 PDB 阻挡。这个事故横跨调度器、节点组模板、云 API、实例引导和驱逐保护;Cluster Autoscaler 只负责其中的模拟与节点组大小调整,不负责让错误的节点自动加入集群,也不会绕过不可迁移 Pod。
它扩的是预配置节点组,不是任意机器
Cluster Autoscaler 是独立控制器。扩容时,它观察被调度器判定为不可调度的 Pod,读取其 requests、node selector、亲和性、污点容忍、存储拓扑等约束,然后用每个候选节点组的模板 NodeInfo 模拟这些 Pod 能否放入。只有“增加该组节点能够帮助调度”的组才进入选择;多个组都可行时由 expander 决定优先扩哪一个。
选中节点组后,Autoscaler 调用云 provider API 调高该组期望容量。实例创建、网络、镜像、kubelet bootstrap、providerID 和 Node 注册由云平台及节点引导链负责。Node Ready 后,真正把 Pod 放上去的仍是 kube-scheduler。Kubernetes 的节点自动伸缩概念明确指出,Autoscaler 预测调度结果但不控制实际调度;并发新 Pod、模板差异或注册失败都可能让预测与结果分离。
缩容时,它按 requests 与节点 allocatable 计算利用率,寻找持续不需要的节点,再模拟节点上非 DaemonSet Pod 是否能迁移到别处。通过模拟后执行 cordon 与 Eviction,并由 provider 终止底层实例、降低节点组大小;残留 Node 对象最终由 cloud node controller 清理。PDB、无控制器 Pod、本地存储、系统 Pod、亲和性和剩余容量都可能阻止删除。它不是基于 CPU 实际使用率的云主机伸缩器,也不是 kube-scheduler 或节点生命周期管理器。
Cluster Autoscaler 1.36.0 应与 Kubernetes 控制面 minor 对齐。它从预先存在的固定节点组中选择,不能像 Karpenter 那样根据约束动态组合任意实例。版本、支持 provider 和行为差异应以 cluster-autoscaler-1.36.0 发行说明与对应标签下的项目文档为准。
部署入口取决于谁拥有节点组
托管 Kubernetes 常提供官方 add-on 或受支持的安装模板,优点是云身份、镜像和控制面版本组合由平台约束;缺点是参数、升级节奏和日志入口可能受托管边界限制。自管理 Deployment 适合需要明确 flags、镜像摘要和变更窗口的团队,但必须自行维护 RBAC、Pod 安全、云身份、leader election、兼容矩阵和高可用恢复。
启用前需要一组已能手工扩缩并正确注册 Node 的节点组。节点组最小值保留系统与故障冗余,最大值受云配额、子网 IP、磁盘、负载均衡器和预算共同限制。组内节点必须在实例类型、标签、污点、系统 DaemonSet 和可分配资源上保持同质;直接修改单台节点会让模板模拟失真。
先记录集群和节点组事实:
kubectl version
kubectl get nodes -L topology.kubernetes.io/zone,node.kubernetes.io/instance-type
kubectl get pods -A --field-selector=status.phase=Pending
kubectl describe pod -n <namespace> <pending-pod>
kubectl get pdb -A自管理安装通常从目标 provider 目录的官方 Deployment/RBAC 示例开始,再固定镜像为 registry.k8s.io/autoscaling/cluster-autoscaler:v1.36.0 或经过供应链审批的摘要。下面只展示关键容器参数骨架,不能脱离 provider 官方清单直接当作完整安装文件:
containers:
- name: cluster-autoscaler
image: registry.k8s.io/autoscaling/cluster-autoscaler:v1.36.0
command:
- ./cluster-autoscaler
- --cloud-provider=<provider>
- --node-group-auto-discovery=<provider-specific-discovery>
- --balance-similar-node-groups=true
- --expander=least-waste
- --scale-down-unneeded-time=10m
- --scale-down-utilization-threshold=0.5
- --skip-nodes-with-local-storage=true
- --skip-nodes-with-system-pods=true
- --v=4
resources:
requests: {cpu: 100m, memory: 300Mi}
limits: {cpu: 1, memory: 1Gi}--node-group-auto-discovery 是 provider 专属语法,例如 AWS 可按 ASG 标签发现;不要复制一个 provider 的参数到另一个云。AWS 的自动发现标签、IAM 最小权限、节点模板标签和多 ASG 行为可核对官方 AWS provider README。Azure、GCE、Cluster API 等必须使用同标签下各自 provider 文档。
部署后检查实际镜像、leader、日志和状态 ConfigMap:
kubectl -n kube-system rollout status deploy/cluster-autoscaler
kubectl -n kube-system get deploy cluster-autoscaler \
-o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'
kubectl -n kube-system logs deploy/cluster-autoscaler --since=10m
kubectl -n kube-system get configmap cluster-autoscaler-status -o yamlRunning 只证明进程存在。日志还应显示发现了预期节点组及 min/max/current,云 API 没有持续拒绝,主循环能完成。若自托管多副本,只有 leader 执行动作;副本数不是并行扩容开关。
关键参数是在定义风险窗口
--scan-interval 决定主循环频率,越短响应越快,但会增加 Kubernetes 与云 API 请求;云实例启动需要数分钟时,把扫描缩到一秒通常只会增加限流。--max-node-provision-time 约束等待节点供给的时间,过短会让慢启动组过早失败,过长会延迟切换到可用候选。
--expander=least-waste 倾向选择放置 Pending Pod 后剩余 CPU/内存较少的组;most-pods 选择能容纳最多 Pod 的组;priority 用 ConfigMap 表达业务优先级;random 适合候选等价时。expander 只在“候选组都能帮助调度”之后选择,不能修复缺标签、无容量或配额耗尽。
--balance-similar-node-groups=true 在相似节点组间平衡扩容,常用于多可用区;相似性依赖实例能力与标签集合,不应靠它保证每区严格相同节点数。拓扑分布约束、卷可用区和实际云容量仍可能让某个区无法增加。
--scale-down-unneeded-time 是节点持续不需要多久才进入删除,--scale-down-utilization-threshold 是基于 requests 的阈值。--scale-down-delay-after-add 保护刚扩出的节点不被立即回收。跳过本地存储或系统 Pod 的 flags 会改变保护面,放宽它们之前必须有明确的可迁移证明。
--cluster-snapshot-parallelism 已弃用,应迁移到 --predicate-parallelism。升级时不能只让旧参数“还能启动”,还要检查日志中的 deprecated 警告和真实并发资源消耗。完整 flags 与默认值可在对应版本的 Cluster Autoscaler FAQ核对。
正向实验:用可调度的 Pending Pod 触发扩容
实验应在允许产生短时云费用的沙箱节点组执行。假设组内节点约有 2 vCPU,可从 1 扩到 3,并且节点模板带 capacity-lab=true 标签。先确认当前组只有一个可调度节点,再创建两个各请求 1500m CPU 的 Pod:
apiVersion: apps/v1
kind: Deployment
metadata:
name: ca-positive
namespace: ca-lab
spec:
replicas: 2
selector:
matchLabels: {app: ca-positive}
template:
metadata:
labels: {app: ca-positive}
spec:
nodeSelector:
capacity-lab: "true"
containers:
- name: hold
image: registry.k8s.io/pause:3.10
resources:
requests: {cpu: 1500m, memory: 256Mi}kubectl create namespace ca-lab
kubectl apply -f ca-positive.yaml
kubectl -n ca-lab get pods -w
kubectl -n ca-lab get events --sort-by=.lastTimestamp
kubectl -n kube-system logs deploy/cluster-autoscaler -f | grep -E 'scale.?up|node group|unschedulable'
kubectl get nodes -w预期至少一个 Pod 因 Insufficient cpu 暂时 Pending,随后事件可能出现 TriggeredScaleUp,日志记录被选中的节点组和目标大小。云实例创建后,新的 Node 应经历注册、Ready,Pending Pod 最终由调度器绑定。不同 provider 和版本的事件文本可能不同,因此应以 Pod condition、Autoscaler 决策日志、云节点组 activity、Node Ready 和 Pod Scheduled 五层证据拼出时间线。
若当前节点可容纳两个 Pod,调高实验 request;若单个 request 超过节点模板 allocatable,实验会变成不可满足反例。数值必须根据 kubectl describe node 中的 Allocatable 与 DaemonSet requests 调整。不要使用真实业务镜像,也不要把节点组最大值为零、云配额不足等环境误差写成工具成功或失败。
反向实验:制造永远不匹配的节点约束
把 Deployment 的 selector 改成任何候选节点组模板都没有的标签:
kubectl -n ca-lab patch deploy ca-positive --type=merge -p '
{"spec":{"template":{"spec":{"nodeSelector":{"capacity-lab":"true","accelerator.example.com/type":"missing"}}}}}'
kubectl -n ca-lab get pods -w
kubectl -n ca-lab describe pod -l app=ca-positive
kubectl -n kube-system logs deploy/cluster-autoscaler --since=10m \
| grep -E 'NotTriggerScaleUp|didn.t match|no expansion options|unschedulable'调度事件应指出 node affinity/selector 不匹配;Autoscaler 应拒绝为现有节点组扩容,因为增加同样模板的节点没有帮助。某些版本会在 Pod 事件中写 NotTriggerScaleUp 或在日志中给出 no expansion options。真正判据是节点组期望容量没有因这个不可满足 Pod 增加,且日志解释了模板约束,不应拘泥于单一字符串。
第二种反例是让单 Pod CPU request 大于所有节点模板 allocatable。它同样不会通过模拟。修复动作不是把节点组 max 调大,而是降低经过基准验证的 request、增加能容纳它的大规格预配置节点组,或改变工作负载拆分方式。
用 JSON Patch 删除反例标签,避免 merge patch 把错误约束永久留在 Pod 模板里:
kubectl -n ca-lab patch deploy ca-positive --type=json -p='[
{"op":"remove","path":"/spec/template/spec/nodeSelector/accelerator.example.com~1type"}
]'
kubectl -n ca-lab rollout status deploy/ca-positive这里的 ~1 是 JSON Pointer 对键名中 / 的转义。恢复后应看到新一代 Pod 不再报告标签不匹配;是否再次扩容仍取决于当时剩余容量,不能把“Pod 已可调度”机械等同于“节点数必然增加”。
节点创建成功后还要证明注册和应用恢复
云节点组 activity 成功只表示虚机出现。Node 长期未注册时,检查实例启动日志、kubelet、CA 信任、bootstrap token、OIDC/实例身份、控制面网络、防火墙、DNS、容器运行时与 CNI。Autoscaler 只能在超时后回退或重试,不能修复 bootstrap。
Node 已注册但 NotReady,先看 conditions 和事件:
kubectl describe node <new-node>
kubectl get node <new-node> -o jsonpath='{.spec.providerID}{"\n"}'
kubectl get pods -A --field-selector=spec.nodeName=<new-node>
kubectl -n kube-system get pods -o wide | grep <new-node>缺少 providerID 会破坏 Node 与云实例映射;CNI、CSI 或 kube-proxy DaemonSet 未就绪会延迟业务调度;启动 taint 未移除则 Node Ready 也可能不接业务。最终容量恢复要看应用 availableReplicas、ready latency 和业务 SLO,而不是只看节点数。
建议给时间线保留这些量:不可调度 Pod 数、每次主循环耗时、候选节点组数、扩容尝试和错误、云 API 延迟、实例创建到 Node 注册、Node 注册到 Ready、Pod Pending 到 Scheduled。Cluster Autoscaler 暴露 Prometheus 指标,但指标名称可能随版本演进,采集前应从目标实例 /metrics 发现并固定告警查询,不从旧博客复制名称。
缩容实验:可迁移不等于可以粗暴驱逐
先恢复正向实验 selector,等待 Pod Running,然后把 Deployment 缩到零,使新增节点可能变成空闲:
kubectl -n ca-lab scale deploy ca-positive --replicas=0
kubectl get nodes
kubectl -n kube-system logs deploy/cluster-autoscaler -f \
| grep -E 'unneeded|scale.?down|remov|evict'节点必须持续低于阈值并经过 scale-down-unneeded-time,才可能被 cordon、驱逐和删除。观察云节点组目标大小是否下降、Node 对象是否消失、实例和附属磁盘是否终止。DaemonSet 通常不会阻止空节点回收,但系统 Pod、本地存储和特定保护项会影响结果。
再创建一个没有控制器 owner 的普通 Pod,或为可迁移工作负载设置无法满足的 PDB,可稳定演示缩容阻塞。日志常会出现 no.scale.down.node.pod.not.backed.by.controller、PDB 或不可驱逐相关原因;具体 reason 取决于 provider 与版本。修复应先把 Pod 纳入 Deployment/Job 等控制器、建立合理 PDB,或在确认数据可丢弃后删除调试 Pod。不要用 --skip-nodes-with-system-pods=false 之类全局放宽掩盖单个错误对象。
需要临时保护节点时可以添加注解:
kubectl annotate node <node-name> cluster-autoscaler.kubernetes.io/scale-down-disabled=true
# 维护结束后显式撤销
kubectl annotate node <node-name> cluster-autoscaler.kubernetes.io/scale-down-disabled-保护注解必须有 owner 和到期时间。长期遗留会制造无法解释的空闲成本。PDB 在节点缩容中是可用性约束,不是容量来源;如果剩余节点没有空间,满足 PDB 也无法迁移 Pod。
云权限、RBAC 和凭证要分成两条链
Kubernetes RBAC 允许 Autoscaler读取 Node、Pod、PDB、DaemonSet、StatefulSet、StorageClass 等调度事实,更新 Node、Event、Lease/ConfigMap,并执行必要驱逐。权限应从目标版本 provider 的官方 RBAC 清单出发,避免自行删掉看似无关的资源导致模拟残缺,也避免直接授予 cluster-admin。
云权限用于发现节点组、读取启动模板或实例类型,并调整目标容量。优先使用 workload identity、托管身份或实例角色,不在 Deployment 环境变量和 Secret 中保存长期 AK/SK。权限资源条件要绑定集群专属标签和节点组 ARN/ID,避免一个集群的 Autoscaler 修改另一个集群。日志与事件只能记录资源 ID 和错误码,不能输出临时令牌、完整启动脚本或 kubeconfig。
云权限拒绝时,Kubernetes Pod 仍可能 Running,日志则持续出现 AccessDenied、限流或 discovery 失败。凭证轮换后要观察至少一个完整发现循环,并用沙箱组执行一次扩缩。退出时同时撤销云身份信任、IAM policy、OIDC binding 与 Kubernetes ServiceAccount;只删除 Deployment 会留下可被冒用的云角色。
节点启动模板也是敏感资产。user-data、bootstrap token、镜像仓库凭证和 CA 材料应通过受控引导与短期身份交付,不能写进节点组标签。Autoscaler 需要读取模板能力,不代表它应读取业务 Secret。
容量与成本由 requests、碎片和冷启动共同决定
Cluster Autoscaler 按 requests 模拟,不看业务容器的瞬时 CPU usage。request 偏低会让模拟认为更多 Pod 能塞进节点,实际运行却争抢资源;request 偏高会提前扩容并阻止缩容。DaemonSet、kube-reserved、system-reserved、驱逐阈值、Pod 数上限和拓扑约束都会减少节点名义规格可用于业务的部分。
节点组不能只用一种超大规格。规格过大时扩一次成本跳跃大、空闲尾部重;规格过小时 DaemonSet 税和 Pod 上限占比高。多个节点组可以覆盖通用、内存型、GPU、Spot/抢占式和不同故障域,但组越多,模板维护、expander 选择和容量碎片越复杂。
恢复时间预算应拆成:调度器确认 Unschedulable、Autoscaler 扫描与模拟、云 API 排队、实例启动、Node 注册、CNI/CSI 就绪、镜像拉取、应用启动与 readiness。只靠节点扩容承接秒级流量尖峰通常来不及,关键在线服务仍要保留 headroom、最小副本或预扩容。
成本证据至少包括节点组 min/max/current、requests 装箱率、不可迁移 Pod、空闲节点年龄、扩容失败原因、实例与磁盘残留。Spot 组价格低但中断与容量不足更频繁;priority expander 可以先选低成本组并保留按需回退,但必须证明高优先组耗尽时能切换,不能让 Pending Pod 无限等待。
选型时看节点供应模型与团队控制权
已有稳定托管节点组、希望跨多种 provider 使用一致的组级伸缩模型时,Cluster Autoscaler 是成熟选择。它的边界清楚:组的实例类型、标签、污点和启动模板由平台预先设计,Autoscaler 只调整组大小。这也让变更、配额、成本和故障域更易审计。
需要按每批 Pending Pod 动态选择实例类型、容量类型和可用区,或希望统一处理节点到期、漂移和整合时,可评估 Karpenter 或云厂商托管节点供给能力。它们不是简单“更快的 Cluster Autoscaler”,对象模型、云权限、回收语义和 provider 覆盖都不同。迁移期间两个节点伸缩器不得管理同一节点组或同一供给边界。
静态节点池适合容量稳定、合规窗口严格或云 API 不可靠的环境;定时调整适合可预测峰值,但不能替代异常流量保护。自建裸金属若没有可被 provider 驱动的机器组生命周期,仅安装 Cluster Autoscaler 不会凭空增加服务器,应先建立 Cluster API 或等价供给控制面。
升级和退出要把节点组控制权交还平台
升级先核对控制面 minor、Autoscaler minor、provider 文档、镜像摘要和已弃用 flags。先在沙箱节点组回放可满足 Pending、不可满足约束、云限流、注册超时、PDB 阻塞和空节点回收,再灰度到生产。控制器升级不应同时修改节点组启动模板和大批 requests,否则无法归因。
回滚保留旧 Deployment、RBAC 与镜像摘要,但不能在同一集群让新旧两个 leader 分别操作同一节点组。若新版本出现错误,暂停 rollout、恢复单一旧版本,确认 leader election 和主循环恢复。参数回滚还要检查新版本是否已写入 ConfigMap、状态对象或 provider 侧标签。
退出前先冻结 HPA/KEDA 可能制造的新 Pending Pod,把业务副本固定到现有容量;将节点组期望值调整到经评审的静态值,并关闭 Autoscaler 对这些组的自动发现或修改权限。随后删除 Deployment,观察云节点组不再被自动改写,再撤销 RBAC 和云身份。
最后处理节点保护注解、Autoscaler 状态 ConfigMap、监控规则、IAM/托管身份、实例 profile、专用标签和告警。若准备删除节点组,必须先迁移业务、排空节点并核销实例、磁盘、IP 和负载均衡器附件。退出完成不是“Autoscaler Pod 不见了”,而是节点组有明确的新 owner、容量不会静默变化、凭证已撤销、云资源与账单都能对账。
清理实验并确认节点组回到基线
先删除实验工作负载和所有临时 Pod,撤销实验节点的保护注解,再等待节点组回到实验前记录的期望容量;这个基线不一定等于节点组最小值:
kubectl delete namespace ca-lab
kubectl get pods -A --field-selector=status.phase=Pending
kubectl get nodes
kubectl -n kube-system get configmap cluster-autoscaler-status -o yaml同时在云节点组 activity 中确认目标容量回到实验前基线,新增实例、根盘、数据盘和公网 IP 已终止或释放。若缩容被 PDB、无 owner Pod 或本地存储阻挡,先修复阻挡原因并保留证据,不要为省实验费用强删承载其他工作负载的 Node。
共享集群不应卸载 Cluster Autoscaler。只有专用临时集群才按“固定节点组容量、删除控制器、撤销云身份、删除 RBAC、核销云资源”的顺序退出。最终检查没有实验 Pending Pod、没有遗留 scale-down-disabled 注解、没有孤儿实例,节点组当前值与平台容量记录一致。
