云原生时代,节点弹性伸缩的痛点
我的处理经验
说到基于Kubernetes,很多问题都出在细节上。Kubernetes 集群里跑着几十上百个 Pod,每个 Pod 都需要 CPU 和内存。当 Pod 突然增多(比如双十一流量),而集群里的节点资源不够时,Pod 会一直处于 Pending 状态。这时候你需要“节点自动伸缩”来立刻添加新节点。
传统方案是 Cluster Autoscaler(CA)。它每隔 10 秒检查一下集群里有没有 Pending Pod,如果有,就调用云厂商 API 创建一台新虚拟机。听起来合理,但实际用起来有两个大问题:
- 慢:CA 检测周期是 10 秒,加上云厂商创建虚拟机平均需要 2-3 分钟,一个 Pod 从 Pending 到 Running 往往要等 3 分钟以上。
- 笨:CA 只能根据节点池(Node Group)的配置来扩缩容,比如你预先建好一个“大规格实例节点池”和一个“小规格实例节点池”。如果 Pod 突然需要大量内存,但只在“小规格池”里有空闲,CA 不会去“大规格池”里创建新节点,因为 Pod 没有被调度到大规格池里——这导致资源浪费和伸缩不灵活。
有没有一种方案,能让节点像 Pod 一样“秒级弹出”,并且自动选择最合适的实例规格?这就是 Karpenter 要解决的问题。
认识 Karpenter:核心概念与设计哲学——基于Kubernetes
Karpenter 是 AWS 开源的节点自动伸缩组件(现已捐赠给 CNCF),它的设计思路和 Cluster Autoscaler 完全不同:
Pod 驱动的触发机制
Karpenter 不是周期性检查 Pending Pod,而是直接监听 Kubernetes 调度器的调度决策。当一个 Pod 被调度器标记为“不可调度”时,Karpenter 在几十毫秒内就能感知到,并立刻发起创建节点的操作。这个“事件驱动”机制让它的响应速度远快于 CA 的轮询模式。
Provisioner:定义节点应该怎么建
Karpenter 用 Provisioner 这个 CRD 对象来描述你想要的节点规格。一个 Provisioner 可以指定:
- 允许使用哪些实例类型(比如 t3.medium、c5.large、m5.xlarge)
- 节点需要打哪些标签、加哪些污点
- 节点生命周期(按需还是 Spot)
- 节点创建时的网卡、磁盘大小等
与 CA 不同的是,Provisioner 不需要预先创建“节点池”,Karpenter 会自动根据 Pod 的资源需求,从你指定的实例类型里挑选最便宜且能刚好满足 Pod 运行的那一种。比如 Pod 需要 2 核 4G,Karpenter 可能会选择 t3.medium,而不是 c5.large。
NodeClass:与云厂商的接口
Karpenter 通过 AWSNodeClass(如果部署在 AWS 上)来定义节点的具体基础设施参数,比如安全组、子网、AMI、IAM 角色等。这个设计让 Karpenter 可以轻松适配不同的云厂商或者自建机房(通过 NodeClass 的接口扩展)。
实战:部署 Karpenter 并让它自动伸缩节点
以下步骤假设你有一个 AWS 上的 Kubernetes 集群(EKS 或自建都行),并且已经安装了 Helm。如果你没有现成集群,可以先创建一个 EKS 集群(最低版本 1.23 以上)。
第一步:创建必要的 IAM 角色与策略
Karpenter 需要权限来创建 EC2 实例、打标签、分配网卡等。你需要在 AWS 上创建一个 IAM 角色,并关联以下策略(最小权限):
- EC2 读写权限(创建、删除实例)
- EC2 Spot 相关权限(如果使用 Spot)
- EC2 Tags 权限
- PassRole 权限(将对应节点 IAM 角色传递给新实例)
具体的 Policy JSON 可以在 Karpenter 官方文档中找到,这里不贴冗余代码。
第二步:使用 Helm 安装 Karpenter
添加 Helm 仓库并安装:
helm repo add karpenter https://charts.karpenter.sh
helm repo update
helm upgrade --install karpenter karpenter/karpenter
--namespace karpenter --create-namespace
--set serviceAccount.annotations.eks.amazonaws.com/role-arn= arn:aws:iam::YOUR_ACCOUNT:role/karpenter-role
--set settings.aws.clusterName=your-cluster-name
--set settings.aws.clusterEndpoint=$(aws eks describe-cluster --name your-cluster-name --query "cluster.endpoint" --output json)
--set settings.aws.defaultInstanceProfile=karpenter-node-instance-profile
第三步:创建 NodeClass
创建一个 NodeClass 来告诉 Karpenter 实例使用哪些子网和安全组:
cat <<EOF | kubectl apply -f -
apiVersion: karpenter.k8s.aws/v1beta1
kind: AWSNodeClass
metadata:
name: default
spec:
amiFamily: AL2023
role: karpenter-node-role
subnetSelectorTerms:
- tags:
karpenter.sh/discovery: "your-cluster-name"
securityGroupSelectorTerms:
- tags:
karpenter.sh/discovery: "your-cluster-name"
EOF
第四步:创建 Provisioner
这是最核心的一步。我们创建一个简单配置,允许 Karpenter 在 Pod 资源不够时使用 t3.medium 和 t3.large,并且优先使用 Spot 实例来省钱:
cat <<EOF | kubectl apply -f -
apiVersion: karpenter.sh/v1beta1
kind: NodePool
metadata:
name: default
spec:
template:
spec:
nodeClassRef:
group: karpenter.k8s.aws
kind: AWSNodeClass
name: default
requirements:
- key: karpenter.k8s.aws/instance-category
operator: In
values: ["t"]
- key: karpenter.k8s.aws/instance-family
operator: In
values: ["t3"]
- key: karpenter.sh/capacity-type
operator: In
values: ["spot"]
expireAfter: 720h
limits:
cpu: 1000
disruption:
consolidationPolicy: WhenUnderutilized
expireAfter: 720h
budgets:
- nodes: 10%
duration: 1h
EOF
解释一下关键字段:
- requirements:限制实例只能使用 t3 家族的 t3.medium / t3.large / t3.xlarge 等,并且只能使用 Spot 实例。你可以根据自己的需求增加或修改。
- expireAfter:节点创建后 720 小时(30 天)会被强制替换,这是为了实例安全更新。
- disruption:在节点利用率低时,Karpenter 会自动合并并省掉多余节点。
第五步:部署一个测试工作负载来触发伸缩
创建一个 Deployment,请求较大的资源,让调度器无法找到可用节点:
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: stress-test
spec:
replicas: 10
selector:
matchLabels:
app: stress
template:
metadata:
labels:
app: stress
spec:
containers:
- name: cpuburn
image: polinux/stress
command: ["stress"]
args: ["--cpu", "4", "--timeout", "300s"]
resources:
requests:
cpu: "4"
memory: "8Gi"
limits:
cpu: "4"
memory: "8Gi"
EOF
理论上你的集群里不会同时有 40 核 CPU 和 80G 内存的空闲节点,所以这个 Deployment 的大部分 Pod 都会变成 Pending。几秒钟之后,你用 kubectl get nodes 会看到新的节点正在被创建。大约 1-2 分钟后(比 CA 少了一半的时间),所有 Pod 都变成 Running。
验证与回滚:安全操作指南——基于Kubernetes
容易忽略的细节
Karpenter 的优雅之处在于它完全与原生 Kubernetes 调度器协同工作,所以你不需要担心它创建节点时搞乱现有负载。但作为新手,你需要知道几个危险命令的规避:
- 不要随意删除正在运行的 NodePool:如果删除了一个正在被使用的 NodePool,Karpenter 会立即尝试删除该 NodePool 管理的节点,导致 Pod 被驱逐。建议先通过
kubectl annotate nodepool default karpenter.sh/do-not-disrupt=true来阻止干扰,然后再修改或删除。 - 注意 Spot 实例中断:如果你的 Provisioner 配置了 Spot,EC2 可能会在两分钟前通知你要回收实例。Karpenter 会自动监听这些通知并把节点标记为不可调度,然后驱逐 Pod。但如果你的 Pod 没有配置 PDB(PodDisruptionBudget),可能会造成短暂的服务中断。建议关键工作负载添加 PDB。
- 回滚到 Cluster Autoscaler:如果你之前用了 CA,切到 Karpenter 需要先清理 CA 的资源(Deployment、ConfigMap、IAM 角色等)。保留旧节点池的 ASG 但缩容到 0,然后安装 Karpenter。如果出现问题,可以反方向操作,但注意不要同时运行 CA 和 Karpenter,否则它们会打架。
想继续深入:此处可内链到“基于Kubernetes优化清单”文章。
Karpenter 的隐藏优势:成本优化与实例多样性
验证与回滚
传统 CA 只会在你预设的节点池里伸缩,导致你很容易过度采购某些实例规格,而其他利用率低的规格却在浪费钱。Karpenter 的 Provisioner 不需要指定具体实例类型,你只需要给出“家族范围”,比如 t3, m5, c5,Karpenter 会自动选择当前价格最低且能跑下 Pod 的组合。再加上 Spot 支持,通常可以节省 30%-60% 的节点成本。
另一个好处是 Karpenter 支持“节点腾挪”(Node Consolidation):当你的 Pod 因为调度分散导致多个节点利用率都不高时,Karpenter 会自动把 Pod 合并到更少的节点上,然后删除多余节点。这个过程不需要你配置任何东西,默认开启(在 disruption.consolidationPolicy 里设置)。
相关阅读:此处可内链到“基于Kubernetes常见问题”专题。
延伸阅读:此处可内链到“基于Kubernetes配置案例”相关文章。
适用场景:什么时候该用 Karpenter?
实际操作要点
- 频繁变动的负载:比如大数据批处理、CI/CD 流水线,Pod 突然增加又很快消失。
- 追求极致的响应速度:实时在线业务,不能忍受 Pod Pending 超过 1 分钟。
- 希望降低运维复杂度和成本:不想管理多个节点池,也不想手动调整实例规格。
如果你运行的是长期稳定的无状态服务(比如 API 服务器),节点几乎不会变化,那么 Cluster Autoscaler 或甚至固定节点池就足够了。Karpenter 的优势在“变”的情况下才明显。
总结
我的处理经验
Karpenter 通过事件驱动的 Pod 感知和智能的实例选择,让 Kubernetes 节点伸缩变得真正“弹性”。它不是一个复杂的系统,核心只需要理解 NodePool(Provisioner)和 NodeClass 两个概念。实战部署后,你会发现它像魔法一样快速——这也是我放弃 Cluster Autoscaler 的主要原因。希望这篇教程能帮你打开 Karpenter 的大门,早点用上更快的节点伸缩。后续只要定期检查关键指标,基于Kubernetes就不会变成维护负担。
延伸阅读
