4. 节点模板与实例类型选择

Karpenter 的核心价值在于“按需选型”:当工作负载因资源不足而处于 Pending 状态时,Karpenter 不是从预先定义好的固定节点池里挑一台机器,而是根据 NodePool 与 NodeTemplate(在较新版本中已并入 NodePool 的 spec.template)中声明的约束,实时向云厂商申请最合适的实例。因此,节点模板与实例类型选择直接决定了集群的成本、可用性与调度成功率。本节围绕 NodeTemplate 的关键参数展开,说明如何把工作负载的真实需求翻译成实例族、容量类型、可用区与 CPU 架构等约束。

NodeTemplate 的定位与关键参数

NodeTemplate 描述的是“节点长什么样”,而不是“要几个节点”。它包含几类信息:一是实例选型约束,如 requirements 中的实例族、容量类型、可用区、CPU 架构;二是节点自身属性,如 AMI 族、启动参数、用户数据、块设备映射;三是与调度相关的标签与污点。Karpenter 在扩容时,会先看 Pod 的调度约束(资源请求、nodeSelector、affinity、toleration),再与 NodeTemplate 的约束求交集,最后在满足条件的实例中按成本与可用性择优创建。

一个典型的 NodeTemplate 片段如下,它声明了只使用 x86 架构、按需容量、限定在三个可用区内的通用型实例:

apiVersion: karpenter.k8s.aws/v1beta1
kind: NodeTemplate
metadata:
  name: general-x86-od
spec:
  requirements:
    - key: kubernetes.io/arch
      operator: In
      values: ["amd64"]
    - key: karpenter.sh/capacity-type
      operator: In
      values: ["on-demand"]
    - key: topology.kubernetes.io/zone
      operator: In
      values: ["cn-north-1a", "cn-north-1b", "cn-north-1c"]
    - key: karpenter.k8s.aws/instance-family
      operator: In
      values: ["m5", "m6i", "m6a"]
  amiFamily: AL2
  tags:
    team: platform

这里的 requirements 是“硬约束”,任何不满足的实例都不会被选中。与之相对,NodePool 中的 weight 与实例的实时价格、可用区容量共同决定“软偏好”。理解硬约束与软偏好的分工,是精准选型的前提。

实例族、容量类型与架构的选型逻辑

实例族决定了 CPU 与内存的比例、网络与存储能力。通用型(m 系列)适合大多数无状态服务;计算优化型(c 系列)适合高并发计算;内存优化型(r 系列)适合缓存与内存数据库;突发型(t 系列)适合低基线负载。选择时不应只写一个实例族,而应给出一个“族集合”,让 Karpenter 在集合内按价格与库存择优。例如把 m5、m6i、m6a 放在一起,Karpenter 会优先选择当前更便宜或库存更充足的型号,从而提升扩容成功率。

容量类型分为 on-demand 与 spot。Spot 成本低但可能被回收,适合可中断的批处理、无状态副本;On-Demand 稳定但贵,适合数据库、核心网关。实践中常见做法是:为同一类工作负载准备两个 NodeTemplate,一个 Spot 优先、一个 On-Demand 兜底,通过 NodePool 的权重与 Pod 的容忍度组合使用。CPU 架构方面,amd64 兼容性最好,arm64(Graviton)性价比高但要求镜像与依赖支持多架构。若工作负载镜像未构建 arm64 版本,强行选择 arm64 会导致启动失败,因此架构约束必须与镜像构建流程对齐。

可用区与容量类型的关系

可用区约束不仅影响延迟与容灾,还直接影响 Spot 的可用性。不同可用区的 Spot 库存与价格差异明显,若把可用区限制得过窄,Spot 申请失败率会上升。建议在 NodeTemplate 中至少保留两个以上可用区,并结合 Pod 的拓扑分布约束,让副本分散到不同可用区。对于有状态服务,可用区约束应与存储卷的可用区保持一致,否则会出现卷无法挂载的问题。

工作负载需求 → NodeTemplate 约束 → 实例择优 工作负载需求 资源请求 cpu / memory / ephemeral 调度约束 nodeSelector / affinity 容忍度 toleration / taint 镜像架构 amd64 / arm64 可用区要求 zone / 存储卷位置 中断容忍:Spot 可回收 NodeTemplate 约束 instance-family m5 / m6i / m6a / c6i / r6i capacity-type on-demand / spot arch amd64 / arm64 zone cn-north-1a / 1b / 1c amiFamily / userData AL2 / Bottlerocket 实例择优 硬约束过滤 不满足 requirements 的实例直接排除 软偏好排序 价格 / 库存 / weight 综合打分 创建节点 绑定标签与污点 加入集群 Pod 调度 满足约束后运行 否则继续扩容 失败:无匹配实例 硬约束决定“能不能用”,软偏好决定“用哪台更划算”;两者共同决定扩容成功率与成本

把需求翻译成约束的实操方法

选型不是凭感觉写几个实例族,而是从工作负载的真实画像出发。第一步,统计 Pod 的资源请求分布:如果多数 Pod 请求 2 vCPU / 4 GiB,且峰值与均值差距不大,通用型即可;如果内存请求远大于 CPU,应加入 r 系列。第二步,判断可中断性:无状态、可重试、有副本冗余的服务可以走 Spot;有本地状态或长连接的服务走 On-Demand。第三步,确认镜像架构:用 docker manifest inspect 检查镜像是否包含 arm64,若包含则可把 arm64 加入候选,通常能降低约 20% 成本。第四步,确定可用区范围:结合存储卷与网络延迟要求,至少保留两个可用区。

下面给出一个按工作负载分层的 NodeTemplate 参数对照,可作为选型起点:

工作负载类型 实例族 容量类型 架构 可用区
无状态 Web / API m6i, m6a, m5 spot 优先,on-demand 兜底 amd64 + arm64 ≥2 个
批处理 / 离线任务 c6i, c6a, c5 spot amd64 + arm64 ≥2 个
内存缓存 / 队列 r6i, r6a, r5 on-demand amd64 与存储一致
核心网关 / 数据库代理 m6i, c6i on-demand amd64 固定 2 个

常见误区与验证方式

第一个误区是把实例族写得太窄,例如只写 m5.large。一旦该规格在目标可用区缺货,扩容就会失败。正确做法是写实例族集合,让 Karpenter 有腾挪空间。第二个误区是忽略架构与镜像的匹配,导致节点创建成功但 Pod 反复 CrashLoopBackOff。第三个误区是 Spot 与 On-Demand 混在同一 NodeTemplate 中,使成本与稳定性都不可控,建议拆分为两个模板并用权重组合。

验证选型是否合理,可以观察三类信号:一是 Karpenter 日志中 incompatible requirements 或 no instance type available 的出现频率,频繁出现说明约束过紧;二是节点创建后的实例类型分布,若长期只落到一种规格,说明候选集合过窄;三是 Pod 的 Pending 时长与 Spot 中断次数,若 Pending 时间过长或中断频繁,应放宽可用区或增加 On-Demand 兜底。把这些信号与 NodeTemplate 参数对照,就能持续收敛到既稳定又经济的选型方案。

建议为每类工作负载维护一份“选型卡片”,记录实例族集合、容量类型、架构、可用区与验证指标。当业务画像变化时,先改卡片再改 NodeTemplate,避免约束随意漂移。