节点自动伸缩器选型:Cluster Autoscaler、Karpenter 与托管能力共存
一次节点伸缩器迁移进行到一半,集群里同时运行 Cluster Autoscaler 和 Karpenter。某批 Pod Pending 后,Cluster Autoscaler 扩大了旧 Auto Scaling Group,Karpenter 也创建了新 NodeClaim。几分钟内两边都补出容量,Pod 只消费其中一部分;低峰到来后,旧节点组受最小副本数约束不能回收,新节点又因为 PDB 无法整合。监控显示“扩容成功”,账单和空闲节点却同时翻倍。
另一个团队为了减少维护工作,把自管 Karpenter 切到托管节点供给。控制台显示能力已启用,默认节点池也存在,但自定义工作负载仍带旧标签,无法匹配托管 NodePool;原 controller 被提前卸载,旧 NodeClaim 的回收和云权限核销没有 owner。托管不等于无责任,迁移也不是打开一个开关。真正需要选择的是控制权、供给模型、节点操作权、升级责任和退出路径,而不只是“哪个扩容更快”。
先区分三种供给模型
Cluster Autoscaler(CA)面向预先存在的节点组。它观察不可调度 Pod,把每个节点组的模板当作候选,模拟增加一个节点能否承载 Pod,然后通过云 provider 或节点组 API 调整 desired size。实例类型、镜像、子网和最小最大容量主要由节点组定义。它擅长让成熟的固定节点组自动增减,不负责按每批 Pod 动态组合任意机型。
自管 Karpenter 面向 NodePool 的约束集合。它根据一组 Pending Pod 的 requests 和调度约束直接选择实例供给,创建 NodeClaim,并管理节点整合、漂移和中断。它减少了“每种形状建一个节点组”的需求,但团队要维护 controller、CRD、云权限、事件队列、NodeClass、镜像策略与升级事务。
托管节点供给把 controller 或节点生命周期的一部分交给云厂商。EKS Auto Mode 基于 Karpenter 执行计算供给,并由 AWS 管理相关节点与组件;AKS Node Auto Provisioning(NAP)由 Azure 部署和管理基于 Karpenter 的能力;GKE Node Auto-Provisioning 与 ComputeClass 根据 Pod 需求创建或删除节点池。三者的 API、可定制字段、节点访问、版本节奏、费用和责任模型并不相同,不能把“托管”当作一种可跨云复制的产品。
用六个问题建立选型坐标
先问容量形状是否稳定。长期只有少数机型、强合规镜像和固定网络边界时,CA 配合托管节点组通常更直接;Pod 形状多、可用区和购买类型组合多、希望减少节点组碎片时,Karpenter 类动态供给更有优势。第二问节点是否允许被平台接管。需要 SSH、安装宿主代理或自定义内核的团队,可能无法接受某些托管节点的限制。
第三问升级由谁承担。CA 版本通常要与 Kubernetes control plane minor 对齐;自管 Karpenter 要同时管理 Chart、CRD、provider、IAM 与 AMI;托管模式减少 controller 运维,却把发布时间、节点替换和部分故障诊断交给厂商。第四问云可移植性。CA 的抽象是节点组,但 provider 实现仍不同;Karpenter Core API 相近,NodeClass 仍是 provider-specific;托管 NodePool 更直接绑定云产品。
第五问团队能否证明回收安全。PDB、本地盘、长任务、GPU、存储拓扑和单副本服务会阻塞或放大任何缩容器。第六问退出成本。选择前就要知道能否保留旧节点组作为安全底座、能否导出策略、如何撤销 controller 角色、如何处理托管节点和仍在计费的云资源。
Cluster Autoscaler 的安装点是节点组发现
下面以 Cluster Autoscaler 1.36.0 对齐 Kubernetes 1.36 为示例。实际镜像和参数必须按 provider 文档确认;官方 v1.36.0 发行版 与 Cluster Autoscaler 文档 是版本入口。不同 provider 的支持级别和参数并不完全相同。
典型 EKS 安装使用 Deployment、ServiceAccount/Pod Identity 或 IRSA,并让 CA 通过标签自动发现受管 Auto Scaling Group:
apiVersion: apps/v1
kind: Deployment
metadata:
name: cluster-autoscaler
namespace: kube-system
spec:
replicas: 1
selector:
matchLabels:
app: cluster-autoscaler
template:
metadata:
labels:
app: cluster-autoscaler
annotations:
cluster-autoscaler.kubernetes.io/safe-to-evict: "false"
spec:
serviceAccountName: cluster-autoscaler
containers:
- name: cluster-autoscaler
image: registry.k8s.io/autoscaling/cluster-autoscaler:v1.36.0
command:
- ./cluster-autoscaler
- --cloud-provider=aws
- --node-group-auto-discovery=asg:tag=k8s.io/cluster-autoscaler/enabled,k8s.io/cluster-autoscaler/<lab-cluster>
- --balance-similar-node-groups=true
- --skip-nodes-with-local-storage=false
- --stderrthreshold=info
resources:
requests:
cpu: 100m
memory: 600Minode-group-auto-discovery 决定 CA 能写哪些组,是最关键的所有权边界;balance-similar-node-groups 只对被判定为相似的组平衡扩容;skip-nodes-with-local-storage=false 允许考虑带本地存储 Pod 的节点,不代表这些数据可以安全丢失。生产不能照抄示例开关,应先审计 Pod 类型与 eviction 行为。旧 --cluster-snapshot-parallelism 已弃用,目标发行线应迁到 --predicate-parallelism。
安装后检查镜像、发现结果与云身份:
kubectl -n kube-system rollout status deployment/cluster-autoscaler --timeout=300s
kubectl -n kube-system logs deployment/cluster-autoscaler --since=10m
kubectl -n kube-system get serviceaccount cluster-autoscaler -o yaml
kubectl get nodes -L node.kubernetes.io/instance-type,topology.kubernetes.io/zone预期日志能列出受管节点组并完成周期性扫描,没有持续 AccessDenied、未发现节点组或 API 限流。CA Pod Running 但发现列表为空时,它不会为业务提供节点。
Karpenter 的启用点是 NodePool 与 NodeClass
自管 Karpenter v1.14.0 通过 OCI Helm Chart 安装,核心策略在 NodePool,provider 配置在 AWS 的 EC2NodeClass。启用后先确认 controller、CRD 和 NodeClass Ready,再创建负载。安装命令和对象配置可对照 Karpenter Getting Started 与 NodePool 概念。
共存时不能创建一个“接所有 Pod”的默认 NodePool。给 Karpenter 容量加专用标签和启动污点:
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: migration-karpenter
spec:
template:
metadata:
labels:
capacity.example.io/provider: karpenter
spec:
taints:
- key: capacity.example.io/migration
value: karpenter
effect: NoSchedule
requirements:
- key: kubernetes.io/os
operator: In
values: ["linux"]
- key: karpenter.sh/capacity-type
operator: In
values: ["on-demand"]
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: migration-karpenter
limits:
cpu: "32"
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 30m
budgets:
- nodes: "1"迁移工作负载必须同时声明 nodeSelector 和对应 toleration,普通 Pod 因没有 toleration 不能误入。NodePool 的 CPU limit、较慢整合和单节点 disruption budget 为首批迁移限制爆炸半径;它们不是长期默认值,应根据真实容量和 SLO 调整。
托管能力减少操作面,也改变故障证据
EKS Auto Mode 可以在新集群创建时启用,也可以通过 EKS API、AWS CLI、eksctl 或控制台给现有集群启用。它包含基于 Karpenter 的计算自动伸缩,并提供内置 system 与 general-purpose NodePool;自定义需求可创建 karpenter.sh/v1 NodePool 与 eks.amazonaws.com/v1 NodeClass。AWS 的 Auto Mode 说明 明确列出托管组件与共享责任,托管实例说明 则说明节点访问和运维限制。
AKS NAP 在 Standard 集群中显式启用,在 AKS Automatic 中预配置。它使用 NodePool 与 AKSNodeClass,由 Azure 管理 Karpenter 组件。启用前要核对网络、集群版本、身份和不兼容能力;节点池 API 与自管 AWS Karpenter 相似不代表字段可直接复制。以 AKS NAP 概览 的 prerequisites、limitations 与 upgrade behavior 为准。
GKE 的节点自动供给可根据 Pending Pod 需求创建节点池,ComputeClass 进一步表达优先级与自动创建策略。启用入口、资源限制、默认 ComputeClass 和 Autopilot/Standard 差异应查 GKE Node Auto-Provisioning 与 ComputeClass。它不是 Karpenter provider,迁移时不能假设存在 NodeClaim 或相同 disruption 语义。
托管模式的验证应从云 API 配置追到 Kubernetes NodePool/NodeClass、Node、Pod 和厂商事件。controller 不在用户 namespace 中,并不等于没有控制器;故障时可能需要控制面日志、云审计和支持接口,而不是只查一个 Deployment。
共存的第一原则是节点与 Pod 双向互斥
只给节点打标签还不够,因为没有 selector 的普通 Pod 仍可能落入新节点;只给 Pod 加 selector也不够,因为多个伸缩器可能都能创建带相同标签的节点。安全共存需要四层隔离:CA 只发现带旧 owner 标签的节点组;Karpenter 或托管能力只管理自己的 NodePool;新节点带专用标签和污点;迁移 Pod 同时带 selector 与 toleration。
旧节点组模板可使用:
label: capacity.example.io/provider=cluster-autoscaler
taint: capacity.example.io/migration=cluster-autoscaler:NoSchedule新池使用:
label: capacity.example.io/provider=karpenter
taint: capacity.example.io/migration=karpenter:NoSchedule不要让 CA 自动发现 Karpenter 背后的云实例或节点组,也不要让 Karpenter NodePool 接纳所有无约束 Pod。每个 Node 应能通过标签、provider ID、NodeClaim 或节点组注解唯一映射到一个 owner;映射不唯一时暂停迁移。
正向实验:让一批 Pod 只唤醒一个供给器
假设旧 CA 节点仍承载基础容量,新 Karpenter NodePool 已按前述配置创建。部署带新池 selector 和 toleration 的实验负载:
apiVersion: v1
kind: Namespace
metadata:
name: autoscaler-migration-lab
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: new-pool-workload
namespace: autoscaler-migration-lab
spec:
replicas: 6
selector:
matchLabels:
app: new-pool-workload
template:
metadata:
labels:
app: new-pool-workload
spec:
nodeSelector:
capacity.example.io/provider: karpenter
tolerations:
- key: capacity.example.io/migration
operator: Equal
value: karpenter
effect: NoSchedule
containers:
- name: pause
image: public.ecr.aws/eks-distro/kubernetes/pause:3.10
resources:
requests:
cpu: "1"
memory: 256Mikubectl apply -f autoscaler-coexist-positive.yaml
kubectl -n autoscaler-migration-lab get pods -w
kubectl get nodeclaims -w
kubectl get nodes -L capacity.example.io/provider,karpenter.sh/nodepool
kubectl -n kube-system logs deployment/cluster-autoscaler --since=15m
kubectl -n kube-system logs deployment/karpenter -c controller --since=15m预期新 Pod 只触发 Karpenter NodeClaim,最终只落到 capacity.example.io/provider=karpenter 的节点。CA 日志可以继续扫描,但不应为这批 Pod增加旧节点组 desired size,因为旧模板不满足 selector。验证还要查询云侧节点组 desired size 与 Karpenter 实例标签,证明没有双扩容。
若目标是托管能力,把 selector、toleration 和对象查询替换为该产品支持的 NodePool/ComputeClass 标签与云 API,但通过条件不变:一批 Pod 只能映射到一个供给 owner,且 Pod Ready 后另一套容量不增长。
反向实验:故意提交没有 owner 的硬约束
创建一个要求不存在 provider 值的 Pod,可以稳定验证“无归属工作负载不会触发任意扩容”:
apiVersion: v1
kind: Pod
metadata:
name: orphan-capacity-request
namespace: autoscaler-migration-lab
spec:
nodeSelector:
capacity.example.io/provider: nobody
containers:
- name: pause
image: public.ecr.aws/eks-distro/kubernetes/pause:3.10
resources:
requests:
cpu: 100m
memory: 64Mikubectl apply -f autoscaler-coexist-negative.yaml
kubectl -n autoscaler-migration-lab describe pod orphan-capacity-request
kubectl -n autoscaler-migration-lab get events --sort-by=.metadata.creationTimestamp
kubectl get nodeclaims
kubectl get nodes -L capacity.example.io/provider
kubectl -n kube-system logs deployment/cluster-autoscaler --since=10m
kubectl -n kube-system logs deployment/karpenter -c controller --since=10m预期 Pod 持续 Pending,scheduler 报 selector 不匹配,CA 给出没有可扩展节点组或类似判断,Karpenter 给出 NodePool requirements 不兼容,并且两边都不创建容量。这个反例证明隔离有效;它不是要通过放宽所有 NodePool 来“修复”。正确修复是由工作负载 owner 选择一个已批准容量类,并提交相应 selector/toleration。
若任一控制器仍创建节点,说明其模板标签、NodePool requirements 或发现规则过宽。先冻结主动缩容和迁移批次,再收紧 owner 规则;不要同时修改两个控制器,否则无法判断是哪一侧恢复了边界。
把扩容故障按责任层分型
Pod Pending 但两边都没有动作,先看调度事件:PVC 拓扑、硬亲和、架构、端口或 requests 超过单机上限时,增加普通节点无效。只有 CA 增长时,检查 Karpenter NodePool Ready、limits 和约束;只有 Karpenter 增长时,检查 CA 发现标签、节点组上限和模板;两边都增长时,几乎总是所有权重叠。
节点已创建但未 Ready,责任转到 bootstrap、CNI、节点身份、DNS、API endpoint、镜像和安全组。节点 Ready 但 Pod 未 Ready,自动伸缩器已经完成节点交付,后续要看镜像拉取、启动探针、配置、存储与业务依赖。托管模式下仍按这条链分层,只是 controller 日志可能换成控制面事件或云审计。
一份可审计时间线至少包含:Pod UID 与首次 Unschedulable 时刻、控制器决策、节点组 desired size 或 NodeClaim UID、云实例 ID、Node Ready、Pod Scheduled、Pod Ready 和业务恢复。没有这条链,“扩容耗时”会把应用冷启动、镜像拉取和云容量不足混成一个数字。
RBAC、云权限和凭证决定谁能制造账单
CA 通常需要读取 Pod、Node、PDB 等集群对象,并调整被发现节点组的 desired size;云权限应按 discovery 标签和集群资源限制。Karpenter 需要更细但更广的实例生命周期、网络发现、PassRole、标签和事件权限。托管能力减少用户持有的 controller 凭证,却仍要求集群身份、节点角色、NodeClass 引用和云资源权限正确。
业务团队不应拥有 NodeClass、节点角色或全局 NodePool 的任意写权限。平台可以提供经过准入校验的容量类,例如 general-on-demand、arm-spot、gpu-reserved,业务只在 namespace policy 允许的集合中选择。防止任意标签和 toleration 绕过隔离,否则一个 YAML 变更就可能创建高价 GPU、进入受限子网或使用高权限节点角色。
日志和事件会暴露实例 ID、账号、子网、安全组、NodeClass、Spot 价格错误和业务标签;集中存储前做访问控制与保留。controller 的 Pod Identity/IRSA、CA 云角色、节点角色和迁移部署身份分别轮换,不能共享静态 access key。退出旧方案时,撤销凭证与删除 controller 同等重要。
容量与成本比较要看浪费来源
CA 的主要浪费来自节点组模板粒度、min size、安全底座和组间碎片;Karpenter 的主要风险来自过宽候选、频繁替换、Spot 中断和 requests 失真;托管能力还可能包含管理费、受限节点形态或厂商定义的生命周期。不能只比较一台 VM 的标价。
统一使用工作负载事实计算:
delivery_latency = unschedulable_wait + provision_wait + register_wait + workload_ready_wait
idle_commitment = node_allocatable - pod_requests - daemonset_requests
migration_overlap = old_pool_cost + new_pool_cost during drain_and_rollback_window选型试验应在相同 Pod shapes、可用区、购买类型、镜像缓存和失败注入下比较 Pending 年龄、供给失败率、碎片率、节点替换次数、基础控制面成本与人工维护时间。生产门槛由业务 SLO、故障域预算和财务模型决定,不使用脱离环境的“节省百分比”。
用分批迁移保留可回退路径
迁移先建立目标容量,不先删除源控制器。给目标 NodePool 加迁移污点,只选择一个无状态、可多副本、PDB 正确的工作负载;修改 selector/toleration,观察完整扩容与回收时间线;扩大到一个 namespace,再扩大到一种容量类。每批都保留源节点组最小安全容量和回退 selector。
从 CA 迁到 Karpenter 时,保持旧 ASG 不被 Karpenter 管理,逐步把 Pod 指向 Karpenter 标签;确认旧组空闲后再降低 min/desired。AWS 提供 从 Cluster Autoscaler 迁移到 Karpenter 的具体顺序。从自管 Karpenter 迁到 EKS Auto Mode 时,AWS 的 迁移步骤 使用带污点的新 NodePool,逐批更新工作负载,再移除旧 NodePool 和 controller。
回退不是重新安装按钮。必须保留源镜像、Chart、CRD 兼容、云角色、节点组模板和足够配额;在回退窗口内,若目标供给失败率、Node Ready 长尾、Pod Ready 长尾、主动 disruption 或费用偏离基线,就停止新批次,把 selector 切回源池,并确认源供给器确实创建容量。
清理共存实验和退役旧控制器
先删反向 Pod,再把正向 Deployment 缩到零,观察目标节点是否按策略回收:
kubectl -n autoscaler-migration-lab delete pod orphan-capacity-request --ignore-not-found
kubectl -n autoscaler-migration-lab scale deployment/new-pool-workload --replicas=0
kubectl get nodeclaims,nodes -w
kubectl delete namespace autoscaler-migration-lab --wait=true退役 CA 时,先确认没有工作负载仍选择旧标签,记录所有节点组 min/max/desired,冻结自动发现标签或把 Deployment 缩到零观察,而不是直接删节点组。退役 Karpenter 时,先删除 NodePool 并等待 NodeClaim、Node 和云实例清零,再卸载 controller,最后按 IaC 删除队列、角色、启动模板和 discovery 标签。托管能力退出则要按云 API 先禁用或删除自定义 NodePool,确认托管节点排空,再处理功能开关与残留资源。
kubectl get pods -A -o wide
kubectl get nodes -L capacity.example.io/provider
kubectl get nodeclaims
kubectl -n kube-system get deployment cluster-autoscaler,karpenter最终证明至少包括:没有未知 owner 的 Node;没有仍引用旧 selector/toleration 的 Pod;旧节点组或 NodeClaim 清零;云实例、磁盘、IP、启动模板和队列已核销;旧 controller 与节点角色已撤销;监控和告警已转到新证据模型。
升级与责任治理决定长期可用性
CA 升级跟随 Kubernetes minor,并复核已弃用 flag、provider 支持等级和节点组模板;Karpenter 升级同时处理 CRD、Chart、provider、NodeClass、AMI 与 IAM;托管能力升级要接受厂商节点生命周期与维护窗口,同时验证 PDB、NodePool disruption budget 和业务 readiness。三者都不能只看 controller 版本号。
团队应为四类对象指定唯一 owner:工作负载团队拥有 requests、selector、toleration、PDB 与业务 Ready;平台团队拥有 NodePool/节点组、控制器、配额和迁移;安全团队审核节点身份、网络、镜像和标签授权;FinOps 消费实例、碎片、替换与托管费用事实。值班手册用同一时间线分层,不让每个团队只看自己的绿色状态。
当一次 Pending 能唯一找到供给 owner,一次扩容只触发一套容量,一次迁移能在保留源路径时逐批证明 Pod Ready,一次退出能撤销所有云权限和计费资源,选型才不再是产品偏好,而是可验证、可回退、可长期承担的架构决策。
