Kubernetes 基础体系 · 第 60/83 篇。示例基于 Kubernetes 当前稳定 API;弃用、版本偏差、云厂商差异和生产风险会明确说明。

Kubernetes 节点自动扩缩容:Pending Pod、Node Group、缩容和成本

节点自动扩缩容解决的不是“Pod 数量变化”本身,而是另一个更具体的问题:

当 Kubernetes 调度器无法为 Pod 找到合适的节点时,是否应该增加节点;当节点长期空闲时,是否能够安全删除节点。

在云环境中,这通常由 Cluster Autoscaler(CA) 完成。CA 观察调度结果,调用云厂商提供的 Node Group 或节点池接口,改变节点组的目标容量;云厂商再创建或删除虚拟机,节点上的 kubelet 加入或离开集群。

因此,完整链路不是:

Pod 变多 -> 节点自动增加

而是:

工作负载产生 Pod
  -> Pod 声明 resources.requests
  -> kube-scheduler 尝试调度
  -> Pod 进入 Pending 且被判定为 unschedulable
  -> Cluster Autoscaler 模拟扩容结果
  -> 增加某个 Node Group 的目标节点数
  -> 云平台创建实例
  -> kubelet 注册节点
  -> scheduler 将 Pod 绑定到新节点

缩容则是反方向的安全迁移过程:

节点长期低利用率
  -> CA 判断该节点上的 Pod 是否可迁移
  -> 驱逐 Pod
  -> 删除节点
  -> 云平台减少 Node Group 的目标节点数

这里的“利用率”主要基于 Pod 的资源请求,而不是 CPU 或内存实时使用率。理解这一点,是理解 CA 成本行为的起点。


1. 先区分三个对象:Pod、Node 和 Node Group

1.1 Pod 是调度单位

Pod 是 Kubernetes 调度器放置的最小工作负载单元。Pod 中容器的 resources.requests 会参与调度:

apiVersion: v1
kind: Pod
metadata:
  name: example
spec:
  containers:
    - name: app
      image: nginx:1.27
      resources:
        requests:
          cpu: "500m"
          memory: "512Mi"
        limits:
          cpu: "1"
          memory: "1Gi"

这里:

  • cpu: "500m" 表示申请 0.5 个 CPU;
  • memory: "512Mi" 表示申请 512 MiB 内存;
  • requests 是调度器预留和判断可放置性的主要输入;
  • limits 是运行时资源上限,不等同于调度容量。

如果一个 Pod 没有设置 CPU 或内存 request,调度器可能认为它对相应资源的申请为零或很小,但它运行时仍可能消耗资源。这会造成“调度看起来成功,运行时却过载”的问题。CA 不会通过实时 CPU 使用率修正这种错误的 request 建模。

1.2 Node 是 Kubernetes 的计算资源

Node 是集群中的一台工作节点。一个 Node 的容量至少需要区分:

  • capacity:节点提供的总资源;
  • allocatable:可以分配给普通 Pod 的资源;
  • 已被现有 Pod requests 占用的资源;
  • kubelet、容器运行时、系统守护进程和 DaemonSet 消耗的资源。

例如:

kubectl describe node worker-1

可能看到:

Capacity:
  cpu:                4
  memory:             16384Mi
Allocatable:
  cpu:                3800m
  memory:             14800Mi
Allocated resources:
  Resource           Requests       Limits
  cpu                2600m (68%)     ...
  memory             9000Mi (60%)     ...

对于一个需要 1000m CPU6Gi 内存的 Pod,不能简单使用 capacity 计算是否可调度,而应近似比较:

剩余可调度 CPU     = allocatable CPU - 现有 Pod CPU requests
剩余可调度内存     = allocatable memory - 现有 Pod memory requests

同时还要满足节点标签、污点、亲和性、拓扑约束、端口和扩展资源等条件。

1.3 Node Group 是云平台的节点集合

Node Group、Node Pool、Managed Instance Group、Auto Scaling Group 等名称因云厂商而异,但通常代表同一个抽象:

一组具有相似配置、伸缩边界和生命周期策略的节点。

一个 Node Group 通常至少有:

当前节点数 desired/current
最小节点数 min
最大节点数 max
节点模板或启动配置
标签 labels
污点 taints
可用区或区域约束
实例类型
云厂商身份与权限

CA 通常不直接创建虚拟机。它调用云厂商适配器,把 Node Group 的目标数量从例如 3 改为 4:

CA: setDesiredSize(node-group-a, 4)
云平台: 创建一台符合节点模板的实例
实例: 启动 kubelet
kubelet: 向 API Server 注册 Node
scheduler: 看到新 Node

因此,Kubernetes API 中看到的 Node 数量变化,可能比云平台扩容请求晚几十秒到数分钟。实例启动失败、配额不足、镜像拉取失败、节点无法注册,都会使 CA 的决策与最终结果出现时间差。


2. Pending 不等于“需要扩容”

2.1 Pending 是 Pod 生命周期状态

Pod 处于 Pending,表示它还没有进入运行阶段。原因可能包括:

  • 尚未被调度;
  • 已经被调度,但容器镜像尚未拉取完成;
  • 等待卷挂载;
  • 等待初始化容器;
  • 节点或网络尚未准备好。

所以仅凭下面的命令不能判断节点不足:

kubectl get pods

应进一步查看 Pod 事件:

kubectl describe pod <pod-name>

如果是调度失败,通常会看到类似:

Warning  FailedScheduling  default-scheduler
0/3 nodes are available: 3 Insufficient cpu.

也可能是:

0/3 nodes are available:
1 node(s) had untolerated taint,
2 node(s) didn't match Pod's node affinity/selector.

只有一部分调度失败适合由 CA 通过增加节点解决。

2.2 Unschedulable 是 CA 关注的关键条件

对 CA 来说,重要的不是“Pod 处于 Pending”这一个状态,而是:

  1. Pod 尚未绑定到任何节点;
  2. 调度器认为当前节点均不适合;
  3. CA 能够通过增加某类节点改变模拟调度结果;
  4. 该 Node Group 尚未超过扩容边界;
  5. 新节点模板满足 Pod 的调度约束。

例如:

spec:
  nodeSelector:
    workload: gpu

如果所有普通节点都没有 workload=gpu,CA 不能随便增加一个普通节点。只有某个 GPU Node Group 的节点模板带有该标签,且该组仍有扩容空间,扩容才有意义。

反例是:

spec:
  nodeSelector:
    workload: nonexistent

如果没有任何 Node Group 的模板能够提供这个标签,增加节点永远无法解决问题。Pod 会持续 Pending,CA 可能记录“没有可扩容的 Node Group”。

2.3 常见 Pending 原因与 CA 能否解决

Pending 原因 增加节点通常能否解决
Insufficient cpuInsufficient memory 可能可以
节点数量或 Pod 数量上限 可能可以,取决于新节点上限
节点缺少匹配标签 只有扩容正确的 Node Group 才可以
未容忍污点 通常不能,除非扩容带有对应污点且 Pod 有 toleration
节点亲和性无匹配 只有存在合适模板才可以
PVC 拓扑约束冲突 取决于存储类型和节点拓扑,不能假定可以
Pod 反亲和性冲突 可能可以,但需要足够拓扑空间
maxPods 已满 增加节点可能可以
镜像拉取失败 不能
Admission webhook 失败 不能
PVC 尚未绑定 通常不能直接靠 CA 解决
Pod 使用了不受支持或不可模拟的调度扩展 结果取决于 CA 与调度器兼容性

CA 的扩容模拟必须尽量复现调度器的约束。如果扩容后理论上仍不能调度,CA 不应扩容;如果 CA 的模拟模型与实际调度器行为存在版本或插件差异,也可能出现误判。


3. Cluster Autoscaler 的组件与数据流

CA 通常以 Kubernetes Deployment 运行:

Cluster Autoscaler
  ├── 读取 API Server 中的 Pod、Node、NodeGroup 相关状态
  ├── 观察 unschedulable Pod
  ├── 对可扩容 Node Group 执行调度模拟
  ├── 选择一个或多个 Node Group
  ├── 修改云平台 Node Group 的目标容量
  ├── 判断低利用率节点是否可安全迁移
  └── 请求云平台删除节点或减少目标容量

一条典型扩容路径如下:

sequenceDiagram
    participant D as Deployment/HPA
    participant A as API Server
    participant S as Scheduler
    participant C as Cluster Autoscaler
    participant G as Cloud Node Group
    participant N as New Node

    D->>A: 创建或增加 Pod 副本
    A->>S: 发现未绑定 Pod
    S->>A: 记录 FailedScheduling / Unschedulable
    C->>A: 读取 Pending Pod 和 Node 状态
    C->>C: 模拟 Pod 在各 NodeGroup 模板上的调度
    C->>G: 增加 desired size
    G->>N: 创建实例
    N->>A: kubelet 注册 Node
    S->>A: 将 Pod 绑定到新 Node
    A-->>C: Pending Pod 数量下降

这条链路中有多个独立的异步状态:

  • Deployment 已经增加副本,但 Pod 可能还没有被调度;
  • CA 已经发起扩容,但 Node 尚未注册;
  • Node 已注册,但 CNI、CSI 或 DaemonSet 尚未就绪;
  • Pod 已有可用节点,但镜像仍在拉取;
  • 云平台已显示实例运行,但 kubelet 尚未 Ready。

因此,不能用“CA 日志里出现 scale-up”证明业务已经获得容量,也不能用“云平台实例已创建”证明 Pod 已经运行。


4. CA 如何决定扩容

4.1 扩容的逻辑条件

可以把扩容抽象为:

存在 Pod p,使得:
  p 未绑定节点
  p 被调度器判定为当前不可调度
  存在 Node Group g,使得:
    g 的节点模板 t_g 可以满足 p 的调度约束
    g.current < g.max

在资源维度上,一个简化的可放置条件是:

request(p, r) + used(node, r) <= allocatable(node, r)

其中:

  • r 可以是 CPU、内存、临时存储或扩展资源;
  • request(p, r) 是 Pod 对资源 r 的 request;
  • used(node, r) 是节点上已有 Pod requests 的总和;
  • allocatable(node, r) 是节点可分配资源。

但完整条件还包括:

节点标签满足 nodeSelector / nodeAffinity
Pod tolerates 节点 taints
拓扑分布约束可满足
Pod 反亲和性可满足
卷的节点拓扑可满足
端口和扩展资源可满足
调度器插件允许该绑定

CA 通常使用 Node Group 的节点模板进行扩容模拟。它并不是先创建实例,再测试 Pod 是否能运行。

4.2 一个完整的扩容算例

假设某 Node Group 的每个节点:

allocatable CPU    = 3800m
allocatable memory = 14800Mi

现有两个节点,每个节点的已分配 request 如下:

节点 已用 CPU request 已用内存 request
node-a 3000m 9000Mi
node-b 2800m 12000Mi

现在 Deployment 扩容产生两个 Pod,每个 Pod 需要:

CPU    = 1000m
memory = 4000Mi

先计算现有节点剩余量:

node-a:
  CPU    = 3800 - 3000 = 800m
  memory = 14800 - 9000 = 5800Mi

node-b:
  CPU    = 3800 - 2800 = 1000m
  memory = 14800 - 12000 = 2800Mi

第一个 Pod 需要同时满足 CPU 和内存:

  • node-a:CPU 只有 800m,不足;
  • node-b:内存只有 2800Mi,不足。

所以第一个 Pod 无法调度。第二个 Pod 也不能调度。CA 模拟增加一个新节点:

new-node:
  CPU    = 3800m
  memory = 14800Mi

两个 Pod 可以同时放在新节点上:

CPU    = 1000m + 1000m = 2000m <= 3800m
memory = 4000Mi + 4000Mi = 8000Mi <= 14800Mi

因此,从纯资源角度看,增加一个节点足够。

但是,如果这两个 Pod 设置了:

topologySpreadConstraints:
  - topologyKey: kubernetes.io/hostname
    maxSkew: 1
    whenUnsatisfiable: DoNotSchedule

那么两个 Pod 可能不能都放到同一新节点,实际需要的节点数可能变成两个。CA 的扩容数量不是简单地把所有 Pending Pod 的 CPU request 相加再除以节点 CPU,而是要考虑装箱和调度约束。

4.3 为什么“总容量足够”仍然会扩容

考虑一个节点:

allocatable CPU = 4000m
allocatable memory = 16000Mi

现有 Pod requests:

CPU:    1500m + 1500m = 3000m
memory: 2000Mi + 10000Mi = 12000Mi

剩余资源:

CPU    = 1000m
memory = 4000Mi

新 Pod 需要:

CPU = 1200m
memory = 1000Mi

集群的总剩余 CPU 可能在其他节点上还有 1000m,但如果没有任何单个节点同时满足 1200m CPU1000Mi memory,Pod 仍然无法调度。这称为资源碎片或装箱失败。

因此,CA 扩容解决的是“存在合适的新节点”,而不是“集群所有节点的资源总和不足”。

4.4 多个 Node Group 如何选择

一个集群可能有:

general-small:  普通 CPU 节点,min=2, max=10
general-large:  大内存节点,min=0, max=5
gpu:            GPU 节点,min=0, max=4

当 Pod Pending 时,CA 会模拟它在各 Node Group 模板上的可调度性,然后根据配置选择扩容组。常见选择因素包括:

  • 是否能满足 Pod 的资源和调度约束;
  • Node Group 是否已达到 max;
  • 云厂商和 CA 支持的 expander 策略;
  • 价格、节点数量或资源浪费;
  • 是否存在优先级配置;
  • 是否有 GPU、架构、可用区等特殊要求。

不同 CA 版本和云厂商实现支持的 expander 选项可能不同,不能把某个部署中的参数当作所有环境都支持的标准接口。生产环境应以所使用 CA 版本的参数说明和云厂商适配器文档为准。

尤其要注意:CA 的成本选择通常是“在满足调度的候选 Node Group 中选择一个”,不是全局最优的长期成本优化器。它不会自动理解业务优先级、抢占实例的中断风险、跨区域流量成本或数据库副本的故障域策略,除非这些因素已经被节点标签、亲和性、优先级和外部配置表达出来。


5. Node Group 的边界决定扩容上限

一个常见故障是 Pod 已经明确因为资源不足而 Pending,但 CA 没有继续扩容。原因可能是:

当前节点数 = max 节点数

例如:

general:
  min = 3
  max = 6
  current = 6

此时即使还有大量 Pending Pod,CA 也不能把该组扩到 7。必须先检查:

kubectl -n kube-system logs deploy/cluster-autoscaler

云厂商控制台或 CLI 也应检查 Node Group 的:

  • 当前节点数;
  • 目标节点数;
  • 最小值和最大值;
  • 实例启动失败原因;
  • 云资源配额;
  • 子网 IP 数量;
  • 可用区容量;
  • 实例类型库存;
  • IAM 或服务账号权限。

CA 能否扩容,至少需要同时满足三层边界:

Pod 调度约束满足
  且 Node Group max 未达到
  且 云平台能够实际创建节点

缺一不可。

5.1 节点模板必须与真实节点一致

CA 的模拟依赖 Node Group 的模板信息。如果模板声明的标签、污点、资源或实例类型与实际新节点不一致,就会出现两类风险:

  1. CA 判断可以扩容,但新节点实际上无法运行该 Pod;
  2. CA 判断不能扩容,但实际新节点本来可以运行该 Pod。

例如 Node Group 模板带有:

workload=batch

但云平台启动脚本没有把这个标签正确注册到 Kubernetes Node。CA 可能基于模板做出正确决策,却在真实节点注册后发现标签缺失,Pod 仍然 Pending。

因此,扩容验证必须同时查看:

kubectl get nodes --show-labels
kubectl describe node <new-node>

并与 Node Group 的启动模板、污点配置和实际实例规格对比。


6. 缩容不是“删掉 CPU 使用率最低的节点”

6.1 CA 的缩容目标

缩容必须回答两个问题:

  1. 这个节点是否浪费了足够多的容量?
  2. 删除后,该节点上的所有可迁移 Pod 能否放到其他节点?

可以将节点资源利用率简化为:

utilization_cpu =
  sum(Pod CPU requests on node) / node allocatable CPU

utilization_memory =
  sum(Pod memory requests on node) / node allocatable memory

如果 CPU 和内存都低于某个缩容阈值,节点可能成为候选节点。实际实现还会考虑其他资源、节点状态、不可驱逐 Pod 和时间窗口。阈值与时间参数由 CA 版本和启动参数控制,不能假定不同发行版的默认值完全一致。

关键点是:CA 通常根据 requests 计算“请求利用率”,而不是根据 Metrics Server 报告的实时 CPU 使用率。

例如:

节点 allocatable CPU = 4
Pod CPU request       = 3.6
实际 CPU 使用         = 0.4

从实时监控看它很空闲,但从调度容量看它已经承诺了 90% 的 CPU request。CA 可能不认为它适合缩容。

反过来:

节点 allocatable CPU = 4
Pod CPU request       = 0.5
实际 CPU 使用         = 3.5

从 request 利用率看它很空闲,但实际运行已经很忙。此时如果 CA 允许缩容,Pod 可能被迁移到其他节点并造成运行时过载。这说明错误的 requests 会同时破坏调度、扩缩容和成本判断。

6.2 缩容的安全迁移过程

缩容不能直接删除 Node,因为这会让节点上的 Pod 突然失去运行位置。典型过程是:

stateDiagram-v2
    [*] --> Normal
    Normal --> Unneeded: 低利用率且满足缩容条件
    Unneeded --> Candidate: 持续超过缩容等待时间
    Candidate --> Drainable: 所有 Pod 可迁移
    Candidate --> Blocked: PDB、亲和性或不可驱逐条件阻塞
    Blocked --> Candidate: 条件解除
    Drainable --> Draining: cordon + eviction
    Draining --> Removed: Pod 已迁移且节点删除成功
    Draining --> Normal: 驱逐失败或节点恢复使用
    Removed --> [*]

通常需要经历:

  1. 将节点标记为不可调度,避免新 Pod 继续放入;
  2. 使用 eviction 机制迁移可驱逐 Pod;
  3. 让 Deployment、StatefulSet 等控制器创建替代 Pod;
  4. 等待这些 Pod 在其他节点成功运行;
  5. 从集群和云 Node Group 中删除该节点。

CA 不应只看到“节点当前空闲”就删除它,因为节点上可能有:

  • PodDisruptionBudget(PDB)不允许继续驱逐;
  • safe-to-evict=false 注解;
  • 本地临时存储;
  • 没有控制器管理的裸 Pod;
  • DaemonSet Pod;
  • 使用节点本地数据的工作负载;
  • 严格的节点亲和性或反亲和性;
  • 拓扑分布约束;
  • 终止宽限期很长的 Pod;
  • 不能在其他节点获得卷或设备的 Pod。

不同 CA 版本和配置对这些情况的细节处理可能不同,但生产上不能把“CA 能够自动排除”当作数据安全保证。持久数据应放在适合迁移的持久化存储中,应用应具有重建和恢复能力。

6.3 PDB 保护的是可用性,不是永不缩容

例如:

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: api-pdb
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: api

如果当前只有两个 api Pod,驱逐其中一个会使可用副本低于 2,CA 的缩容操作可能被阻塞。

PDB 只约束自愿中断(voluntary disruption)的一部分场景。它不能保证:

  • 节点宕机时 Pod 一定不中断;
  • 应用一定有足够副本;
  • 资源一定足够完成迁移;
  • 所有控制器都能快速拉起替代 Pod。

如果所有节点上的应用都设置过于严格的 PDB,集群可能永远不能缩容;如果 PDB 太宽松,缩容可能影响服务容量。PDB 必须和副本数、故障余量、滚动发布策略一起设计。

6.4 DaemonSet 与系统 Pod 的影响

每个节点通常都有 DaemonSet,例如:

  • CNI 网络代理;
  • kube-proxy;
  • 节点监控;
  • 日志采集;
  • CSI 节点插件。

这些 Pod 通常会在新节点上重新创建,因此它们本身不一定阻止缩容,但会影响:

  1. 节点实际可分配容量;
  2. 缩容迁移后的资源余量;
  3. 节点是否具备删除前的必要条件;
  4. CA 是否将某类 DaemonSet 或系统 Pod 视为可处理。

如果一个新节点上的系统 DaemonSet requests 很大,节点模板的可用容量会显著小于云实例标称容量。扩容计算必须使用 allocatable 和系统开销,而不能按裸机规格计算。


7. 缩容为什么可能失败:一个反例

假设集群有三个节点,每个节点的 allocatable 为:

CPU = 4

节点上的 Pod requests:

node-a: 0.5
node-b: 1.0
node-c: 2.5

若缩容阈值为 50%,node-anode-b 都可能成为低利用率候选节点。

现在 node-b 上有一个 Pod:

spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
          - matchExpressions:
              - key: kubernetes.io/hostname
                operator: In
                values: ["node-b"]

这个 Pod 只能运行在 node-b。删除 node-b 时,它没有其他合法落点,因此 node-b 无法被安全缩容。

再看没有硬亲和性、但有资源碎片的情况:

node-a 剩余 CPU = 0.8
node-c 剩余 CPU = 0.7
node-b 上待迁移 Pod request = 1.0

即使集群总剩余 CPU 为 1.5,也没有单个节点能容纳该 Pod。迁移模拟失败,node-b 不能删除。

这说明缩容的判定是一个重新调度问题,而不是简单的利用率排序问题:

低利用率
  + Pod 可驱逐
  + 其他节点能够重新容纳这些 Pod
  + PDB 允许
  + Node Group 不低于 min
  -> 才可能缩容

8. HPA、VPA 与 CA 如何共同影响节点数量

8.1 HPA 产生 Pod,CA 提供节点

HPA 通过修改 Deployment 或其他工作负载的副本数来响应指标。一个简化的 CPU 利用率算法是:

desiredReplicas =
  ceil(currentReplicas * currentMetric / targetMetric)

例如:

currentReplicas = 4
currentMetric   = 80%
targetMetric    = 50%

则:

ceil(4 * 80 / 50) = 7

HPA 将副本数从 4 调整为 7。新增的 3 个 Pod 如果无法被现有节点容纳,才会触发 CA 扩容。

完整关系是:

指标升高
  -> HPA 增加副本
  -> 新 Pod Pending
  -> CA 增加节点
  -> 新 Pod 运行
  -> HPA 的业务指标可能下降

CA 不直接读取 HPA 的目标值,也不负责决定应用需要多少副本。HPA 扩容过快、节点启动过慢或指标存在延迟时,可能出现较长的 Pending 窗口。

8.2 HPA 缩容与 CA 缩容有时间差

当 HPA 减少副本数时,Pod 被删除,节点上的 requests 下降。CA 通常还需要等待缩容稳定窗口,确认节点不会立即重新变忙,才尝试删除节点。

如果 HPA 在阈值附近反复扩缩容:

HPA 增副本 -> CA 增节点 -> HPA 减副本 -> CA 减节点

就可能形成节点抖动。HPA 的稳定窗口、扩缩容策略与 CA 的扩缩容等待时间需要共同观察,而不是分别调到“响应最快”。

8.3 VPA 可能改变 CA 的输入

VPA 的核心作用是调整 Pod 容器的 CPU 和内存 request/limit 建议或实际配置。它会改变:

Pod requests
  -> 调度容量
  -> 节点利用率判断
  -> Pending 可能性
  -> CA 扩缩容结果

例如一个 Pod 的内存 request 从 512Mi 调整到 2Gi

  • 它可能无法继续放在原节点;
  • 可能触发 CA 扩容;
  • 原节点的 request 利用率可能变高,缩容候选减少。

HPA 和 VPA 同时作用于同一资源指标时也可能冲突。典型风险是:

  • HPA 根据 CPU 利用率扩副本;
  • VPA 同时修改 CPU request;
  • 由于 CPU 利用率常按 usage / request 计算,request 改变会改变 HPA 的输入;
  • HPA 和 VPA 可能互相放大或抵消。

这不是 CA 的特殊问题,而是自动控制回路共享输入后的耦合问题。生产设计必须明确谁负责副本数量、谁负责单 Pod 资源大小,并观察它们对节点 requests 的共同影响。


9. 成本模型:节点数量不是唯一成本

9.1 一个简化的成本公式

设 Node Group gg 的节点数量为 ngn_g,单节点单位时间成本为 pgp_g,则计算实例成本近似为:

Ccompute=gng×pgC_{\text{compute}} = \sum_g n_g \times p_g

但集群总成本还可以写成:

Ctotal=Ccompute+Cstorage+Cnetwork+Ccontrol-plane+Cobservability+CwasteC_{\text{total}} = C_{\text{compute}} + C_{\text{storage}} + C_{\text{network}} + C_{\text{control-plane}} + C_{\text{observability}} + C_{\text{waste}}

其中 waste 不是云账单上的独立项目,而是因为资源碎片、过大的 request、故障余量或错误的节点类型导致的未利用容量。

CA 主要改变 ngn_g,不能自动优化所有成本项。

9.2 完整成本算例

假设有两种 Node Group:

small:
  allocatable CPU = 3.8
  单节点成本 = 0.10 / 小时

large:
  allocatable CPU = 7.6
  单节点成本 = 0.18 / 小时

工作负载需要:

总 CPU requests = 10.0

只看 CPU 理论下界:

small 至少需要 ceil(10.0 / 3.8) = 3 台
成本 = 3 * 0.10 = 0.30 / 小时

large 至少需要 ceil(10.0 / 7.6) = 2 台
成本 = 2 * 0.18 = 0.36 / 小时

从 CPU 和纯计算价格看,3 台 small 更便宜。

但如果 Pod 的内存、反亲和性、DaemonSet 开销或故障余量使 small 实际需要 4 台:

4 * 0.10 = 0.40 / 小时

large 可能反而更便宜。更大的节点也可能造成更严重的单节点故障影响,或产生更多空闲内存。因此成本比较必须同时看:

  • requests 的装箱效率;
  • CPU 与内存的共同约束;
  • 节点故障时的剩余容量;
  • Pod 拓扑分布和副本策略;
  • 节点启动速度;
  • 实例中断和可用性;
  • 跨可用区网络费用;
  • 系统 DaemonSet 开销;
  • 资源峰值与持续时间。

9.3 为什么把 min 设置为零不一定最便宜

如果一个 Node Group 的 min=0,可以在低峰时归零,减少持续成本。但下一次使用时要支付冷启动代价:

扩容请求
  -> 实例创建
  -> 节点启动
  -> kubelet 注册
  -> CNI/CSI/DaemonSet 就绪
  -> 镜像拉取
  -> Pod Ready

如果业务不能承受这段延迟,可能需要保留一定的基线节点。成本优化实际是延迟、可用性和费用的权衡,而不是始终追求最小节点数。

此外,过于频繁的扩缩容会造成:

  • 实例启动和删除反复发生;
  • 镜像重复拉取;
  • 节点缓存丢失;
  • Pod 频繁被驱逐;
  • 业务延迟波动;
  • 可能增加跨区流量和日志成本。

所以缩容稳定窗口的作用不是让系统变慢,而是过滤短时波动,避免用节点生命周期操作响应每一次瞬时指标变化。


10. 一个可验证的最小示例

下面的 Deployment 使用当前常用的 apps/v1 API。它只是演示资源请求和 Pending,不包含云厂商 Node Group 配置。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: cpu-demand
spec:
  replicas: 1
  selector:
    matchLabels:
      app: cpu-demand
  template:
    metadata:
      labels:
        app: cpu-demand
    spec:
      containers:
        - name: app
          image: nginx:1.27
          resources:
            requests:
              cpu: "1500m"
              memory: "1Gi"
            limits:
              cpu: "1500m"
              memory: "1Gi"

应用并观察:

kubectl apply -f cpu-demand.yaml
kubectl get pods -l app=cpu-demand -o wide
kubectl describe pod -l app=cpu-demand

如果现有每个节点都没有至少 1500m CPU1Gi 内存的同时空闲容量,事件中可能出现:

FailedScheduling
0/2 nodes are available: 2 Insufficient cpu.

将副本数改为 4:

kubectl scale deployment cpu-demand --replicas=4
kubectl get pods -l app=cpu-demand -w

如果只有一个副本能被现有节点容纳,其他副本会 Pending。此时 CA 可能开始扩容,但只有在以下条件同时满足时才会真正增加节点:

  • CA 正确部署并拥有云平台权限;
  • 至少一个 Node Group 的 max 尚未达到;
  • 节点模板能满足该 Pod 的调度条件;
  • 云平台配额、子网和实例库存正常。

验证路径:

kubectl get events --sort-by=.lastTimestamp
kubectl get nodes
kubectl get pods -l app=cpu-demand -o wide
kubectl -n kube-system logs deploy/cluster-autoscaler

清理示例:

kubectl delete deployment cpu-demand

清理 Deployment 会删除其 Pod,Pending 数量下降后,CA 可能在等待窗口结束后评估缩容。不要为了测试直接删除生产 Node;正确的缩容测试应使用非生产 Node Group,并确认 PDB、数据和控制器行为。


11. 生产诊断应沿着状态链路进行

11.1 Pod Pending:先确认是不是调度问题

kubectl get pod <pod> -o yaml
kubectl describe pod <pod>
kubectl get events --field-selector involvedObject.name=<pod>

重点看:

  • PodScheduled 条件;
  • FailedScheduling 原因;
  • request 是否异常大;
  • nodeSelector 和 affinity;
  • toleration 是否缺失;
  • PVC 是否绑定;
  • 拓扑约束是否过严。

如果 Pod 已经被调度,但状态仍为 Pending,应检查:

kubectl describe pod <pod>

事件可能显示拉取镜像、挂载卷或初始化失败。这类问题不能靠增加节点解决。

11.2 CA 没有扩容:确认它看到的事实

应同时检查:

kubectl -n kube-system get deploy cluster-autoscaler
kubectl -n kube-system logs deploy/cluster-autoscaler
kubectl get nodes
kubectl get pods -A --field-selector=status.phase=Pending

常见原因包括:

  • CA 未运行或反复重启;
  • CA 与 Kubernetes 版本不兼容;
  • 云平台权限不足;
  • Node Group 未被 CA 发现;
  • Node Group 已到 max
  • 没有 Node Group 能满足 Pod;
  • 扩容被冷却时间或失败退避逻辑暂时抑制;
  • Pod 使用了 CA 无法正确模拟的调度特性;
  • 触发的是镜像、卷或 Admission 问题而不是容量问题。

日志中的结论需要与 Pod 的调度事件交叉验证,不能只看 CA 的一行摘要。

11.3 CA 发起了扩容但节点没有出现

此时要把问题从 Kubernetes 转向云平台和节点启动链路:

CA 修改目标容量
  -> 云平台是否接受请求
  -> 实例是否创建
  -> 实例是否成功启动
  -> 网络和安全组是否正确
  -> kubelet 是否能访问 API Server
  -> 节点证书和身份是否有效
  -> Node 是否注册
  -> Node 是否 Ready

检查:

kubectl get nodes -o wide
kubectl get events -A --sort-by=.lastTimestamp
kubectl describe node <node>

如果云平台已有实例但 Kubernetes 没有 Node,重点检查启动脚本、kubelet 日志、网络连通性、身份权限和集群引导配置。

11.4 CA 不缩容:区分“没有候选节点”和“候选但不可删除”

kubectl -n kube-system logs deploy/cluster-autoscaler
kubectl describe node <node>
kubectl get pdb -A
kubectl get pods -A -o wide --field-selector spec.nodeName=<node>

检查:

  • request 利用率是否真的低;
  • 节点是否已低利用率足够长时间;
  • Node Group 是否已达到 min
  • Pod 是否有 safe-to-evict=false
  • PDB 是否阻止驱逐;
  • Pod 是否有硬亲和性;
  • 其他节点是否能容纳迁移后的 requests;
  • 本地存储和裸 Pod 是否阻止删除;
  • 节点是否处于 NotReady 或其他异常状态。

“监控显示 CPU 只有 5%,但不缩容”并不矛盾,因为 CA 可能看到的是 70% 的 CPU request 利用率,或者 Pod 根本无法迁移。


12. 与手工 kubectl drain 的边界

手工排空节点通常是:

kubectl cordon <node>
kubectl drain <node> --ignore-daemonsets --delete-emptydir-data

这两个命令具有明显风险:

  • cordon 只阻止新 Pod 调度,不会迁移现有 Pod;
  • drain 会触发 Pod 驱逐;
  • --delete-emptydir-data 可能删除节点本地临时数据;
  • --force 可能处理没有控制器管理的 Pod,风险更高;
  • PDB 可能使驱逐失败;
  • 不应在不了解 StatefulSet、卷和应用副本策略时直接执行。

CA 的自动缩容应被理解为受约束的自动化 drain,而不是“删除低利用率节点”。人工维护时可以使用 drain,但必须提前确认:

工作负载有控制器
PDB 允许中断
替代节点有容量
持久数据不依赖 emptyDir 或本地盘
拓扑约束不会导致 Pending

完成迁移后再由云平台删除实例或减少 Node Group 目标容量。不要只在云平台强制终止实例,因为这会绕过 Kubernetes 的优雅驱逐过程。


13. 版本、实现和云厂商差异

13.1 CA 不是 Kubernetes 核心控制器

Kubernetes API 定义了 Pod、Node、Deployment、HPA 等对象,但 Cluster Autoscaler 通常作为集群外部组件部署。它依赖:

  • Kubernetes API;
  • scheduler 的调度结果;
  • 云厂商或自建基础设施的 Node Group 接口;
  • 与 Kubernetes 版本相匹配的 CA 版本。

因此,CA 的配置参数、镜像标签、日志格式、expander 支持和云厂商能力不是 Kubernetes 核心 API 的统一保证。

13.2 云厂商差异必须单独验证

不同环境中的 Node Group 可能是:

  • 公有云托管节点池;
  • 虚拟机伸缩组;
  • Cluster API 管理的机器池;
  • 裸机池;
  • 本地虚拟化平台;
  • 通过自定义 cloud provider 接入的节点组。

它们在以下方面可能不同:

  • 节点组发现方式;
  • 目标容量修改接口;
  • 节点模板元数据;
  • 可用区处理;
  • 扩容失败重试;
  • 节点删除顺序;
  • Spot/抢占实例支持;
  • 最大节点数量和资源配额。

不能仅凭某个云厂商的参数推断另一个环境也支持同样行为。

13.3 调度插件和新特性可能造成模拟偏差

CA 需要对调度进行模拟,而 Kubernetes scheduler 可能启用复杂插件和约束,例如:

  • PodTopologySpread;
  • 节点和 Pod affinity/anti-affinity;
  • VolumeBinding;
  • 设备插件资源;
  • 自定义调度器;
  • 调度框架扩展;
  • 多架构节点选择。

如果 CA 版本、scheduler 配置、云厂商节点模板或 Kubernetes 版本不一致,可能出现“CA 认为能调度,实际不能调度”或相反的情况。升级时应把 Kubernetes、CA、云厂商插件和节点镜像作为一组验证,而不是只升级其中一个组件。


14. 容量规划决定 CA 的上限

CA 不是容量规划的替代品。它只能在已定义的 Node Group 中调整节点数量,不能自动解决以下问题:

  • 所有 Node Group 的 max 都太小;
  • 单个故障域没有故障余量;
  • Pod requests 明显偏小或偏大;
  • 大 Pod 无法装箱;
  • 关键工作负载没有副本;
  • 关键节点组没有足够的最小容量;
  • 云平台没有实例库存或配额;
  • 节点启动时间超过业务可接受窗口。

一个基本规划模型是:

Nnormal=max(RcpuAcpu,RmemoryAmemory)N_{\text{normal}} = \left\lceil \max\left( \frac{R_{\text{cpu}}}{A_{\text{cpu}}}, \frac{R_{\text{memory}}}{A_{\text{memory}}} \right) \right\rceil

其中:

  • RcpuR_{\text{cpu}}:所有工作负载和必要系统 Pod 的 CPU requests;
  • RmemoryR_{\text{memory}}:所有工作负载和必要系统 Pod 的内存 requests;
  • AcpuA_{\text{cpu}}:单节点 allocatable CPU;
  • AmemoryA_{\text{memory}}:单节点 allocatable 内存。

这只是没有碎片和拓扑限制的理论下界。若需要容忍一个节点故障,可以近似规划为:

NplannedNnormal+NfailureN_{\text{planned}} \geq N_{\text{normal}} + N_{\text{failure}}

或者要求删除任意一台节点后,剩余节点仍能容纳关键工作负载。实际还要按可用区分布、Pod 反亲和性、PDB 和峰值负载进行压测。

如果 max 只按平均负载设置,而没有覆盖发布峰值、HPA 峰值和节点故障后的恢复容量,CA 在最需要扩容时仍会把 Pod 留在 Pending。


15. 最容易混淆的几个结论

“Pending 就会自动加节点。”
错误。只有调度失败、扩容后可解决、存在可用 Node Group 且未超过边界时,CA 才可能扩容。

“CA 根据实时 CPU 使用率扩容和缩容。”
不准确。HPA 通常使用指标系统,CA 的节点装箱和缩容判断主要依赖 Pod requests 与调度可迁移性。

“集群总剩余资源足够,Pod 就一定能调度。”
错误。调度受单节点容量和各种约束影响,碎片会导致总量足够但没有合适落点。

“缩容就是删除最空闲的节点。”
错误。低利用率只是候选条件,节点上的 Pod 还必须能够被安全驱逐并在其他节点重新调度。

“扩大 Node Group 的 max 就一定能解决 Pending。”
不一定。如果标签、污点、PVC 拓扑、亲和性或云平台配额不匹配,增加上限没有效果。

“节点数量越少,成本越低。”
只在服务容量、故障余量、迁移成本和冷启动延迟都不受影响时成立。过度缩容可能增加抖动、故障风险和业务延迟。


16. 建立正确的验证闭环

一次完整的扩缩容验证,不应只观察节点数,而应记录以下时间点:

T0: HPA 或人工创建 Pod
T1: Pod 被 scheduler 判定为 Unschedulable
T2: CA 识别 Pending Pod
T3: CA 请求 Node Group 扩容
T4: 云平台实例创建
T5: Node 注册
T6: Node Ready
T7: Pod 被调度
T8: Pod Ready

缩容则记录:

S0: 工作负载副本或 requests 下降
S1: 节点进入低利用率候选
S2: 等待窗口结束
S3: Pod 驱逐开始
S4: 替代 Pod Ready
S5: 节点删除
S6: Node Group 目标容量下降

这些时间点分别对应不同责任域:

  • T0、S0:工作负载控制器或 HPA/VPA;
  • T1、T7:scheduler;
  • T2、T3、S1、S2、S3:CA;
  • T4、S6:云平台 Node Group;
  • T5、T6、S4:节点引导、kubelet、CNI、CSI 和系统 DaemonSet;
  • T8:镜像、应用启动和就绪探针。

只有把整条链路串起来,才能判断问题究竟是容量不足、调度约束、云平台失败、节点启动失败,还是应用自身启动缓慢。

节点自动扩缩容的本质,是把“Pod 的调度需求”转换为“Node Group 的容量变化”,再把“可迁移的空闲容量”转换为“安全的节点删除”。Pending Pod 是扩容的触发信号,Node Group 是扩容的执行边界,requests 和调度约束决定容量是否真实可用,PDB 与迁移条件决定缩容是否安全,而成本则取决于装箱效率、故障余量、启动延迟和工作负载控制器共同形成的长期状态。


系列导航与关联阅读

官方资料

本文依据 Kubernetes、CNCF 与相关项目官方文档重新梳理;正文和生产清单由 WR BLOG 编写。