ᕕ( ᐛ )ᕗ Jimyag's Blog

HPA 入门:一个 CPU 指标如何让 Pod 从 1 个变成 4 个?

Last modified:

一个 HPA 只看到了 164% 的 CPU 利用率,为什么最后会得到 4 个 Pod,而不是 2 个或 3 个?停止负载后,CPU 已经降到 1%,它又为什么没有马上缩容?

本文从这两个现象出发,用一次 1 → 4 → 1 的 kind 实验回答一个问题:

一个指标,经过哪些步骤才会变成真正可用的 Pod?

这是 HPA 实验系列的第一篇:

  1. 本文:CPU 指标与基本控制循环
  2. 应用的 RPS 和处理带宽如何驱动扩缩容?
  3. 队列已经空了,正在处理的任务能安全结束吗?
  4. HPA 已经要求 4 个副本,为什么只有 1 个可用?

先看答案:HPA 只负责修改期望副本数

1
2
3
业务负载 → CPU 指标 → HPA 计算建议 → Deployment /scale
实际容量 ← Pod Ready ← kubelet 启动 ← 调度器 ← ReplicaSet

HPA 周期性读取指标,计算期望副本数,再更新工作负载的 /scale 子资源。随后由 Deployment、ReplicaSet、调度器和 kubelet 完成创建与启动。HPA 写入 4,不代表已经有 4 个 Pod 可以接收流量。

这条链路至少要观察两个值:

回答的问题
spec.replicas / HPA desiredReplicas 控制器想要几个 Pod?
Deployment readyReplicas 现在有几个 Pod 真正可用?

实验环境

实验在 macOS / Apple Silicon 上运行,Docker 使用 OrbStack。配置、脚本和原始记录固定在 k8sdev c48f140

组件 版本
kind v0.32.0
Kubernetes v1.35.0
Metrics Server v0.8.1
Prometheus / Adapter v3.5.0 / v0.12.0

检出固定版本并创建隔离集群:

1
2
3
4
5
6
git clone https://github.com/jimyag/k8sdev.git k8sdev-hpa
git -C k8sdev-hpa checkout --detach c48f1405c9886b048c95c3b46a8b3e766e34f6f5
cd k8sdev-hpa/hpa/lab
export KUBECONFIG="${TMPDIR:-/tmp}/kind-hpa-lab-repro/kubeconfig"
./setup.sh
alias k='kubectl --context kind-hpa-lab -n hpa-demo'

脚本只操作 kind-hpa-lab,若同名集群已经存在会停止。Metrics Server 为适配本地 kind 使用 --kubelet-insecure-tls;生产环境应配置可信证书。

CPU 目标到底是什么

worker 的 CPU request 是 100m

1
2
3
4
5
6
7
8
# CPU request 既参与调度,也是 Utilization 的分母。
resources:
  requests:
    cpu: 100m
    memory: 32Mi
  limits:
    cpu: 500m
    memory: 128Mi

HPA 将目标设为 request 的 50%:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 根据 worker Pod 的平均 CPU 利用率扩缩容。
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: worker
  namespace: hpa-demo
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: worker
  minReplicas: 1
  maxReplicas: 4
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 50 # 相当于每 Pod 平均使用 50m CPU
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 30

Utilization: 50 不是宿主机 CPU 的 50%,也不是 CPU limit 的 50%。如果 request 改为 200m,同一个目标就相当于每 Pod 100m。若希望目标始终是绝对用量,可以写成:

1
2
3
4
# Resource 指标也可以直接指定每 Pod 平均绝对用量。
target:
  type: AverageValue
  averageValue: 50m

资源指标支持 UtilizationAverageValue;应用自定义指标没有资源 request 作为分母,通常使用 AverageValue。第二篇会用 RPS 和 B/s 实测这种差别。

从 164% 算出 4 个副本

先等资源指标就绪,再启动持续 CPU 负载:

1
2
3
4
k top pods
k get hpa
k apply -f load.yaml
k get hpa -w

在指标完整、Pod 已就绪且忽略容忍区间等修正时,可用下面的比例式理解计算:

1
2
3
ceil(当前副本数 × 当前指标 / 目标指标)
= ceil(1 × 164 / 50)
= 4

首次运行记录:

时间(UTC) CPU / condition desired Ready
13:59:35 1m / 1% 1 1
14:00:14 164% / 50% 4 1
14:00:24 扩容完成 4 4
14:01:27 463%TooManyReplicas 4 4

最后一行说明副本已经碰到 maxReplicas: 4,不表示 CPU 已回到目标范围。原始采样见 lab/evidence

9 月 8 日又从固定提交重新创建集群。高负载时保存的 HPA 状态包含具体指标和限制原因:

1
2
3
4
5
current.averageValue:       387m
current.averageUtilization: 387
currentReplicas:            1
desiredReplicas:            4
ScalingLimited:             True / TooManyReplicas

原始比例建议是 ceil(1 × 387 / 50) = 8,随后被 maxReplicas: 4 限制。Deployment 达到 4 Ready 后停止负载;降载采样为 1m / 1%,HPA 状态是 currentReplicas=4desiredReplicas=1,最终 Deployment 也回到 1 Ready。新旧两轮都验证了同一控制路径,而具体 CPU 数值会随宿主机和采样时刻变化。

实际算法还会做保守修正:

  • 指标比例落在默认容忍区间内时,不改变副本数。
  • 部分 Pod 缺指标时,扩容按低用量、缩容按高用量估算,避免过度调整。
  • CPU 路径会单独处理尚未就绪和刚启动的 Pod。
  • 完全取不到指标时返回错误,不会把负载当成零。

对应实现可以从 Kubernetes v1.35.0 的 GetResourceReplicas 开始阅读。

CPU 已降到 1%,为什么没有马上缩容

停止负载:

1
2
k delete -f load.yaml
k get hpa -w

结果不是 4 → 1 立即发生:

时间(UTC) 观测 副本
14:02:27 CPU 1%ScaleDownStabilized 4
14:02:29 HPA 发起缩容 1
14:02:42 终止完成 1 Ready

缩容稳定窗口保留近期的副本建议,并使用窗口内较大的建议约束缩容。它保存的是副本建议,不是对 CPU 指标求平均。第四篇会把稳定窗口、限速和故障路径放在同一条排障链路中实测。

回答开头的问题

164% 先通过比例计算得到 4,再受容忍区间、缺失指标、minReplicasmaxReplicas 和行为策略修正。HPA 只把最终结果写成 Deployment 的期望副本数;后续控制器、调度器、kubelet 和 readiness 才把它变成可用容量。

所以,一个指标不会直接变成 Pod。它先变成副本建议,再变成期望状态,最后才可能变成 Ready Pod。 这也是后面三篇分析自定义指标、队列任务和故障现象时共同使用的主线。

实验结束后删除集群:

1
kind delete cluster --name hpa-lab

#Kubernetes #HPA #Kind