Karpenter 动态节点供给:从 Pending Pod 到安全回收
发布窗口刚开始,订单服务扩到二十个副本,十二个 Pod 却停在 Pending。值班人员看到节点 CPU 平均利用率只有四成,先判断“集群还有空间”,随后又发现调度事件持续报 Insufficient cpu 和节点亲和性不匹配。几分钟后新 EC2 实例终于出现,但其中两台没有加入集群,一台加入后又被快速替换。业务恢复时间远远长于实例创建时间,云账单却已经开始增长。
第二个现场发生在低峰期。团队启用了激进整合,期待把三个低利用率节点压成一个;Karpenter 创建替代节点后,排空受到 PodDisruptionBudget 和不可驱逐 Pod 阻塞,旧节点、新节点同时计费。有人直接删除 NodeClaim,又把节点上的本地临时数据一并清掉。真正的问题不是“自动伸缩失效”,而是团队没有把调度约束、云实例供给、节点注册、Pod 就绪和中断回收看成一条有中间状态的控制链。
先看懂 Karpenter 改变了哪一层
Karpenter 不替代 kube-scheduler。调度器先尝试把 Pod 放到现有节点;当硬约束与可用资源的交集为空,Pod 进入不可调度状态。Karpenter 观察这些 Pod,汇总其 requests、架构、操作系统、节点选择器、亲和性、污点容忍、拓扑和卷拓扑,再寻找能够承载一组 Pod 的实例组合。云实例启动并注册为 Node 后,仍由调度器完成最终绑定。
这条链上有三个核心对象。NodePool 是集群级供给策略,声明允许创建什么节点、总量上限以及何时可以中断;provider-specific NodeClass 保存子网、安全组、镜像、节点角色、磁盘和启动配置;NodeClaim 是一次具体容量申请,记录选中的实例属性和生命周期状态。NodePool 类似“允许怎样买容量”,NodeClass 类似“买到的机器怎样接入”,NodeClaim 才是“这一台机器的实际订单”。官方的 NodePool、NodeClass 与 NodeClaim 文档分别给出了对象契约。
因此排障不能从 Pending 直接跳到云控制台。先保存 Pod 调度事件,再看 Karpenter 是否计算出候选 NodePool,随后检查 NodeClaim 的 Launched、Registered、Initialized 条件,最后检查 Node 与 Pod Ready。某一步没有成立,下一步的绿色状态就不能替它证明成功。
安装前把版本、基础容量和云身份固定下来
下面以 Karpenter Core/AWS Provider v1.14.0、Kubernetes 1.36 和 EKS 为可操作基线。稳定 API 是 karpenter.sh/v1,AWS NodeClass 使用 karpenter.k8s.aws/v1;旧 v1beta1 已从当前发行线移除。升级或新装时先核对 兼容矩阵 与 v1.14.0 发行版,不要只更新 controller 镜像而保留旧 CRD。
Karpenter controller 不能运行在完全依赖它自己创建的容量上,否则集群从零恢复时会形成“控制器等节点、节点等控制器”的环。至少保留一组小型托管节点、静态节点或 Fargate profile 承载 CoreDNS、网络组件与 Karpenter。首次操作前需要 kubectl、Helm、AWS CLI、可销毁的 EKS 环境、能够创建 IAM/EC2/SQS 等资源的部署身份,以及供 controller 使用的 Pod Identity 或 IRSA 角色。先记录真实身份和 API:
export KARPENTER_VERSION=1.14.0
export KARPENTER_NAMESPACE=kube-system
export CLUSTER_NAME='<lab-cluster>'
export AWS_REGION='<lab-region>'
aws sts get-caller-identity
kubectl version
helm version
kubectl get nodes -L karpenter.sh/nodepool,node.kubernetes.io/instance-type
kubectl api-resources | grep -E 'nodepools|nodeclaims|ec2nodeclasses'若 API 资源尚不存在是正常的安装前状态;若集群已有同名 CRD,则先确认它们由哪套 Helm release 管理,不能覆盖未知 owner。生产部署还要固定 Chart digest、controller 镜像 digest、CloudFormation/IaC revision 与 IAM policy revision。
用 Helm 启动控制器并验证它真的可工作
AWS 官方入门路径通过 CloudFormation 或等价 IaC 创建 controller policy、节点角色、实例配置文件、事件队列和资源发现标签,再从公开 ECR 的 OCI Chart 安装 Karpenter。这里假设这些云资源已经由隔离环境的 IaC 创建,Helm 只负责集群内组件:
helm registry logout public.ecr.aws
helm upgrade --install karpenter \
oci://public.ecr.aws/karpenter/karpenter \
--version "${KARPENTER_VERSION}" \
--namespace "${KARPENTER_NAMESPACE}" \
--create-namespace \
--set "settings.clusterName=${CLUSTER_NAME}" \
--set "settings.interruptionQueue=${CLUSTER_NAME}" \
--set controller.resources.requests.cpu=1 \
--set controller.resources.requests.memory=1Gi \
--set controller.resources.limits.cpu=1 \
--set controller.resources.limits.memory=1Gi \
--wait
kubectl -n "${KARPENTER_NAMESPACE}" rollout status deployment/karpenter --timeout=300s
kubectl -n "${KARPENTER_NAMESPACE}" get deployment,pod,serviceaccount
kubectl get crd | grep -E 'karpenter|ec2nodeclass'
kubectl -n "${KARPENTER_NAMESPACE}" logs deployment/karpenter -c controller --tail=100预期证据是 Deployment 可用副本达到期望值,三类 CRD 可发现,日志没有持续的凭证、API discovery 或队列连接错误。Running 只证明容器进程存活;还要检查 ServiceAccount 绑定的云角色、controller 是否能读取集群 endpoint、发现子网与安全组,并且事件队列名称与 IaC 一致。官方 Getting Started 给出了 v1.14.0 的 Helm 参数和 AWS 资源模板入口。
私有集群常在这里失败。controller 除 Kubernetes API 外,还要按启用能力访问 STS、EC2、SSM、SQS、EKS、Price List Query 和 IAM 等 API;拉取 controller 镜像则是承载它的基础节点与容器运行时的网络责任,不应混为同一条调用链。IAM 和 Price List Query 没有 VPC 私有端点:完全无公网出口时,EC2NodeClass 不能使用需要 Karpenter 动态管理实例配置文件的 spec.role,应由 IaC 预创建 instance profile 并改用 spec.instanceProfile;价格查询失败时 Karpenter 会退回随二进制发布的静态价格数据,选型成本可能逐渐失真。日志中的 WebIdentityErr、AccessDenied、endpoint timeout 和无候选 subnet 是不同故障,不应通过给 controller 附加管理员策略来一次性“修好”。
用 NodePool 表达容量政策,而不是列一张机型清单
下面的实验 NodePool 只允许 Linux/amd64、按需容量与三类通用实例族,总 CPU 上限为演示值。requirements 会与 Pod 的硬约束求交;limits 限制整个 NodePool 已供给容量,不是单节点规格;weight 只在多个兼容 NodePool 之间影响优先级,不会覆盖不兼容约束。
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: lab-general
spec:
template:
metadata:
labels:
capacity.example.io/pool: lab-general
spec:
requirements:
- key: kubernetes.io/arch
operator: In
values: ["amd64"]
- key: kubernetes.io/os
operator: In
values: ["linux"]
- key: karpenter.sh/capacity-type
operator: In
values: ["on-demand"]
- key: karpenter.k8s.aws/instance-category
operator: In
values: ["c", "m", "r"]
- key: karpenter.k8s.aws/instance-generation
operator: Gt
values: ["4"]
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: lab-general
expireAfter: 720h
terminationGracePeriod: 24h
limits:
cpu: "64"
memory: 256Gi
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 10m
budgets:
- nodes: "10%"consolidationPolicy 允许 Karpenter 对空闲或低利用率节点做删除/替换模拟;consolidateAfter 是节点发生可整合变化后的等待,不是 Pod 的启动超时;budgets 限制自愿中断并发,但不会阻止云故障或 Spot 中断。expireAfter 和 terminationGracePeriod 改变模板后会导致既有 NodeClaim 漂移,可能触发替换,因此它们必须经过容量与中断预算评审。
用 EC2NodeClass 收敛网络、镜像与节点角色
NodePool 不应该携带 AWS 细节。EC2NodeClass 通过 selector terms 发现子网、安全组和 AMI,并指定节点身份。实验环境给资源加唯一 discovery 标签,避免同账号多个集群误选同名网络对象:
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
name: lab-general
spec:
role: "KarpenterNodeRole-<lab-cluster>"
amiSelectorTerms:
- alias: <reviewed-ami-family>@<reviewed-ami-version>
subnetSelectorTerms:
- tags:
karpenter.sh/discovery: "<lab-cluster>"
capacity.example.io/tier: "worker"
securityGroupSelectorTerms:
- tags:
karpenter.sh/discovery: "<lab-cluster>"
blockDeviceMappings:
- deviceName: /dev/xvda
ebs:
volumeSize: 80Gi
volumeType: gp3
encrypted: true
deleteOnTermination: true
tags:
capacity.example.io/owner: platform
capacity.example.io/environment: labamiSelectorTerms 决定启动镜像。浮动 @latest 会让新旧节点在没有配置变更的情况下产生差异,生产应固定经过验证的 AMI 版本并通过漂移窗口更新。subnetSelectorTerms 影响可用区、剩余 IP 和出口路径;securityGroupSelectorTerms 影响 API、DNS、镜像和业务流量;role 是节点身份,不是 controller 身份,并且会要求 controller 调用 IAM 管理生成的 instance profile。没有 IAM 公网出口时,应由 IaC 预创建 instance profile,并在 role 与 instanceProfile 中只保留后者。磁盘加密成功也不等于节点角色有权使用目标 KMS key。
应用后先看条件,不急着制造负载:
kubectl apply -f ec2nodeclass-lab-general.yaml
kubectl apply -f nodepool-lab-general.yaml
kubectl get ec2nodeclass lab-general -o yaml
kubectl get nodepool lab-general -o yaml
kubectl describe ec2nodeclass lab-general
kubectl describe nodepool lab-general预期 NodeClass 的 Ready 条件为 True,并能看到已解析的 subnet、security group 与 AMI;NodePool 也应 Ready。若 NodeClass 未就绪,创建 Pod 只会增加噪声,先处理 selector 零命中、多安全组歧义、节点角色、实例配置文件、KMS 或 API endpoint 问题。
正向实验:沿每个中间状态观察一次扩容
创建一个专用 namespace 和初始为零副本的 Deployment。镜像与 requests 都是实验值;镜像应替换为组织允许的不可变 digest。Pod 通过 NodePool 模板标签选择实验容量,避免落到基础节点组:
apiVersion: v1
kind: Namespace
metadata:
name: karpenter-lab
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: inflate
namespace: karpenter-lab
spec:
replicas: 0
selector:
matchLabels:
app: inflate
template:
metadata:
labels:
app: inflate
spec:
nodeSelector:
capacity.example.io/pool: lab-general
terminationGracePeriodSeconds: 0
containers:
- name: pause
image: public.ecr.aws/eks-distro/kubernetes/pause:3.10
resources:
requests:
cpu: "1"
memory: 256Mi
securityContext:
allowPrivilegeEscalation: falsekubectl apply -f karpenter-positive.yaml
kubectl -n karpenter-lab scale deployment/inflate --replicas=8
kubectl -n karpenter-lab get pods -w
kubectl -n karpenter-lab get events --sort-by=.metadata.creationTimestamp
kubectl get nodeclaims -w
kubectl get nodes -L karpenter.sh/nodepool,node.kubernetes.io/instance-type,topology.kubernetes.io/zone
kubectl -n "${KARPENTER_NAMESPACE}" logs deployment/karpenter -c controller --since=15m在没有足够现有容量时,预期先看到 Pod 因资源或 selector 不满足而 Pending;随后出现引用 lab-general 的 NodeClaim。NodeClaim 应依次获得 Launched=True、Registered=True、Initialized=True,Node 进入 Ready,最终八个 Pod 被调度并 Ready。实际实例类型和数量由请求形状、DaemonSet 开销、可用区、价格、供给与限制共同决定,不应把某个固定机型写成通过条件。
把证据串起来:
kubectl get nodeclaims -o custom-columns='NAME:.metadata.name,NODEPOOL:.metadata.labels.karpenter\.sh/nodepool,INSTANCE:.status.instanceType,ZONE:.status.zone,CAPACITY:.status.capacity,READY:.status.conditions[?(@.type=="Ready")].status'
kubectl -n karpenter-lab get pods -o wide
kubectl get nodepool lab-general -o jsonpath='{.status.resources}{"\n"}'只有在目标集群依次观察到这些状态,Pod Ready 才说明业务获得了可运行容器的入口;真实服务还要继续验证启动探针、依赖连接和业务请求。任一中间条件缺失时,都不能用最终节点数量代替该层证据。
反向实验:制造一个永远求不到交集的 Pod
稳定的反例不是关掉 controller,而是提交一个 NodePool 明确禁止的硬约束。下面要求 arm64,而实验 NodePool 只允许 amd64:
apiVersion: v1
kind: Pod
metadata:
name: impossible-arch
namespace: karpenter-lab
spec:
nodeSelector:
capacity.example.io/pool: lab-general
kubernetes.io/arch: arm64
containers:
- name: pause
image: public.ecr.aws/eks-distro/kubernetes/pause:3.10
resources:
requests:
cpu: 100m
memory: 64Mikubectl apply -f karpenter-negative.yaml
kubectl -n karpenter-lab describe pod impossible-arch
kubectl -n karpenter-lab get events --field-selector involvedObject.name=impossible-arch --sort-by=.metadata.creationTimestamp
kubectl -n "${KARPENTER_NAMESPACE}" logs deployment/karpenter -c controller --since=10m | grep -E 'impossible-arch|incompatible|requirements'
kubectl get nodeclaims预期 Pod 保持 Pending,调度事件给出节点选择或亲和性不匹配,Karpenter 日志指出 Pod requirements 与 NodePool requirements 不兼容,并且不会为它创建有效 NodeClaim。若团队只看到 Pending 就扩大 IAM 权限或提高 NodePool CPU 上限,故障不会改变,因为约束交集仍为空。
修复有两种含义不同的路径:业务镜像并不支持 ARM 时,应删除错误的 Pod selector;业务确实需要 ARM 时,应建立独立的 ARM NodePool/NodeClass、镜像与依赖验证。不要为了消除事件把通用 NodePool 同时放开所有架构,混合架构会把镜像兼容和性能差异推迟到运行期。
从 NodeClaim 条件和事件给失败分层
NodeClaim 是供给链最重要的中间证据。没有 NodeClaim,通常是 Pod 约束、NodePool Ready、总量 limits 或候选实例问题;Launched=False 多半落在 EC2 配额、容量、价格、IAM、子网或 KMS;已 Launched 但未 Registered,要查 user data、节点角色、网络、DNS、集群 endpoint 与 bootstrap;已 Registered 但未 Initialized,则检查 CNI、CSI、GPU/device plugin、启动污点与必需 DaemonSet。
kubectl describe nodeclaim <nodeclaim-name>
kubectl get nodeclaim <nodeclaim-name> -o yaml
kubectl describe node <node-name>
kubectl get events -A --sort-by=.metadata.creationTimestamp | tail -n 100
kubectl -n "${KARPENTER_NAMESPACE}" logs deployment/karpenter -c controller --since=30m
aws ec2 describe-instances --filters "Name=tag:karpenter.sh/nodepool,Values=lab-general"日志应保存 controller revision、Pod UID、NodePool generation、NodeClaim UID、provider instance ID 和错误码,但要脱敏账号、私有 IP、user data 与标签中的业务信息。API Server 限流、EC2 throttling 和子网 IP 枯竭都可能表现为“扩容慢”,必须用相对时间线区分调度等待、创建等待、注册等待和应用启动等待。
整合、漂移和中断都是有状态替换
Karpenter 的 disruption 先模拟目标节点上的 Pod 是否能放到其他现有节点或替代节点,再遵守 NodePool disruption budget、PDB、karpenter.sh/do-not-disrupt 等保护完成 cordon、drain 与终止。整合不是按 CPU 利用率直接删除节点,而是基于 requests 和调度可行性评估删除或替换。详细状态机见官方 Disruption 文档。
Spot 中断、节点到期、配置漂移和低利用率整合的触发原因不同。NodePool budget 主要约束自愿 disruption;云平台发来的不可延期中断没有义务等待业务窗口。PDB 只约束支持 eviction 的路径,也不能保护没有副本或依赖本地临时盘的状态。生产启用 WhenEmptyOrUnderutilized 前,至少验证单副本、长终止时间、DaemonSet、本地盘、GPU、PVC 拓扑和 do-not-disrupt Pod。
观察时同时看候选、阻塞与实际删除:
kubectl get nodeclaims -L karpenter.sh/nodepool,karpenter.sh/capacity-type
kubectl get pdb -A
kubectl get nodes -o custom-columns='NAME:.metadata.name,UNSCHEDULABLE:.spec.unschedulable,TAINTS:.spec.taints'
kubectl -n "${KARPENTER_NAMESPACE}" logs deployment/karpenter -c controller --since=30m | grep -E 'disrupt|consolidat|drift|blocked'整合长期阻塞不一定是坏事,它可能暴露正确的可用性保护;长期同时保留替代节点和旧节点则会转化为费用与容量风险,需要检查排空超时、PDB、终止宽限和 finalizer。
云权限、Kubernetes RBAC 与敏感数据要分层收敛
controller 云角色拥有发现网络、创建与终止实例、管理实例配置文件、读取镜像参数和消费中断事件的高风险能力;节点角色则承担 kubelet 加入集群、拉取镜像、日志和工作负载访问。两者绝不能合并。部署身份可以创建 IAM policy,不代表运行时 controller 也需要 iam:*。
资源发现标签是授权边界的一部分。Karpenter 使用管理标签关联云实例与集群对象,能任意增删这些标签的身份可能间接触发实例创建或删除。应使用资源 ARN、集群名、region、请求标签和已有标签条件收窄 RunInstances、TerminateInstances、CreateTags 与 PassRole,并通过 CloudTrail 审计 NodeClaim UID 到云 API 调用的映射。
Kubernetes 侧只有平台 controller 和受控交付身份可以改 NodePool/NodeClass。允许业务团队任意增加 NodePool label 或 EC2NodeClass.spec.role,等价于允许其改变节点信任与网络边界。NodeClass 的 user data、标签、AMI、KMS、子网和安全组可能包含内部拓扑;日志与工单应只保留必要标识,不能粘贴完整启动脚本、凭证错误响应或实例元数据。
用恢复时间和碎片率计算容量成本
Karpenter 优化的是请求形状到实例供给的匹配,不会自动修正虚高 requests、错误亲和性或应用冷启动。容量模型至少拆成四段:
capacity_recovery = pending_detection + cloud_launch + node_registration + pod_startup
requested_waste = provisioned_allocatable - scheduled_requests - daemonset_requests
usable_headroom = schedulable_allocatable - protected_baseline - failure_domain_reserve实例价格只是成本的一部分。还要计入根盘、跨区流量、NAT、IPv4、日志、镜像拉取、Spot 中断重试、替代节点重叠、容量预留浪费和 controller 基础节点。NodePool limits 是防止失控供给的硬护栏,但 controller 在快速并发创建期间读取状态存在最终一致窗口,不能把它当作财务系统的精确交易上限。
生产阈值从工作负载基线和 SLO 推导。观察 Pending Pod 年龄分位数、NodeClaim 创建失败率、各条件耗时、Node Ready 到 Pod Ready 的长尾、NodePool requests/limits 比例、不可整合节点数和每次 disruption 的替代重叠时间。若只看平均节点 CPU,既看不到调度碎片,也看不到可用区和机型约束造成的容量饥饿。
清理实验时按所有权逆序释放
先删除负载,让实验节点自然变空,再观察整合是否回收。随后删除 NodePool,确认其 NodeClaim 与 Node 已处理,最后删除 NodeClass。不要先卸载 controller,否则 Node/NodeClaim finalizer 和云实例可能失去协调者。
kubectl -n karpenter-lab delete pod impossible-arch --ignore-not-found
kubectl -n karpenter-lab delete deployment inflate --ignore-not-found
kubectl delete namespace karpenter-lab --ignore-not-found --wait=true
kubectl delete nodepool lab-general --wait=false
kubectl get nodeclaims -l karpenter.sh/nodepool=lab-general -w
# 观察到该 NodePool 的 NodeClaim 全部消失后按 Ctrl-C 结束 watch,再继续
kubectl get nodeclaims,nodes
kubectl delete ec2nodeclass lab-general若这是完整实验集群,确认没有 Karpenter 管理实例后再卸载 Helm release,并由 IaC 删除事件队列、IAM policy/role、实例配置文件、启动模板和 discovery 标签:
helm uninstall karpenter -n "${KARPENTER_NAMESPACE}"
kubectl get crd | grep -E 'karpenter|ec2nodeclass'
aws ec2 describe-instances \
--filters "Name=tag:eks:eks-cluster-name,Values=${CLUSTER_NAME}" \
"Name=instance-state-name,Values=pending,running,stopping,stopped" \
--query 'Reservations[].Instances[].{Id:InstanceId,State:State.Name}'CRD 是否删除取决于后续迁移与历史对象保留计划,不能在仍有 NodeClaim 时直接清除。云 API 返回空结果、队列删除、角色撤销和账单资源核销才构成退出证据,Helm release 消失只覆盖集群内一部分。
升级与退出要把 CRD 当成数据契约
升级顺序不是简单的 helm upgrade。主 karpenter Chart 中 crds/ 目录的对象只在首次安装时创建,后续 Helm upgrade 不会替你升级 CRD;生产应使用与 controller 同版本的独立 karpenter-crd Chart 管理它们。先导出 NodePool、NodeClass、NodeClaim、Helm values、controller policy 与关键指标;检查目标版本兼容矩阵和逐版迁移说明;升级 karpenter-crd,确认 stored/served version 与 schema,再升级 controller;在隔离 NodePool 创建新 NodeClaim,验证启动、注册、初始化与 disruption;最后才让候选版本接管主要工作负载。官方 Upgrade Guide给出了独立 CRD Chart 和迁移约束。
回滚也受 CRD 存储版本约束。新 controller 写入旧版本不认识的字段后,直接回滚镜像可能启动成功却无法正确协调对象。发布窗口应保留旧 Chart、旧 CRD schema、转换测试和停止条件;若 NodeClaim 创建错误率、注册长尾或 disruption 阻塞显著偏离基线,先冻结 NodePool 变更和主动整合,再决定回滚 controller 或恢复配置。
退出 Karpenter 可以迁往固定节点组、Cluster Autoscaler 或托管节点供给,但必须先建立接管容量,给新旧节点打互斥标签/污点,迁移一类工作负载并验证 Pod Ready,再逐池删除 Karpenter NodePool。最终收回 controller 云角色、节点角色、队列、启动模板、标签授权和监控规则。做到每个 Pending Pod 都能解释为何匹配某个 NodePool、每个 NodeClaim 都能追到云实例、每次删除都能说明由谁授权,动态供给才真正进入可治理状态。
