CPU 85% 但 HPA 利用率只有 14%?问题不在采集——requests 分母太大了

场景:线上服务流量翻 3 倍 → CPU 冲到 85% → HPA 没扩容 → Pod 数卡在 3 不动 路径:坐标 → 分层 → 路径 → 定位 → 标点

以下排查基于 K8s v1.25,metrics-server v0.6.x,HPA 使用 autoscaling/v2 API。


坐标

下午 3 点 CPU 飙到 85%,HPA 没扩容,Pod 数卡在 3 不动。kubectl get hpa -o wide 一看:TARGETS 14%/50%。但 kubectl top pod 显示平均 CPU 820m——矛盾就在这了。

(上篇我们讲了 CronJob 的时区/并发/退避三连坑,这篇换一个自动化的话题——HPA 自动扩缩容。)

高峰期 HPA 没动静

通知服务的 CPU 使用率从平时的 30% 飙到了 85%,但 HPA(HorizontalPodAutoscaler——K8s 的自动扩缩容控制器,根据 Pod 的 CPU/内存或自定义指标自动调整副本数)显示一直是 3 个 Pod,纹丝不动。

HPA 状态显示 TARGETS 很低

kubectl get hpa notification-service -o wide 的输出给了第一条线索:TARGETS 列显示 14%/50% (avg)——当前 CPU 利用率只有 14%,远低于 50% 的目标阈值。按这个数值看,HPA 确实不应该扩容。

kubectl top pod 显示的实际 CPU 不是这样:

NAME                                    CPU(cores)   MEMORY(bytes)
notification-service-7d4f8b6c9f-abc1   820m          256Mi
notification-service-7d4f8b6c9f-abc2   790m          248Mi
notification-service-7d4f8b6c9f-abc3   850m          262Mi

每个 Pod 的 CPU 都在 800m 以上——平均 820m。那问题来了:820m 的 CPU 消耗,为什么 utilization 只有 14%?

这篇的关键数字:实际 CPU 820m,utilization 14%,target 50%。HPA 的计算公式是 current / requests——不是看绝对 CPU 数值,是除以 Pod 的 requests。3 个 Pod 平均 820m 为什么 utilization 只有 14%?这需要分层来揭开。

第一层线索:HPA 的 utilization 是算出来的

HPA 状态详情

kubectl describe hpa notification-service 的 Conditions 段暴露了关键信息:AbleToScale 是 True,ScalingActive 是 True——HPA 控制器(horizontal-pod-autoscaler controller,负责定期计算期望副本数的控制回路,每 15s 轮询一次)工作正常。但有一个之前没注意的信号:ScalingLimited 是 True,原因是 TooFewReplicas

这意味着 HPA 其实算出了一个低于 minReplicas 的期望副本数。代进去算一遍就清楚了:

desiredReplicas = ceil[currentReplicas × (currentUtilization / targetUtilization)]
                = ceil[3 × (14% / 50%)]
                = ceil[0.84] = 1

HPA 认为 1 个 Pod 就够应对当前流量了——甚至想缩容。是 minReplicas=3 的配置把它拦在了下限上,所以有三个 Pod 不是 HPA 认为需要这么多,而是被下限卡住。ScalingLimited=True 就是在告诉你这个信息。

那核心问题就变成了:为什么 utilization 只有 14%——低到 HPA 觉得 1 个 Pod 就够了?


分层

HPA Metrics 管道:Pod → kubelet → metrics-server → HPA controller

第 1 层:反推 requests 的大小

已知 utilization = 14%,target = 50%,currentReplicas = 3,那反向算 Pod 的 requests 是多少:

currentUtilization = avg(pod CPU) / requests.cpu
14% = 820m / requests.cpu
→ requests.cpu = 820m / 0.14 ≈ 5857m

Pod 的 requests.cpu 设了接近 6 核(6000m),但实际只用不到 1 核(820m)。分母太大,这就是 utilization 只有 14% 的直接原因。

HPA 的 utilization 公式里只有一个变量你说了算:requests。设错了,算法算到天亮也不会扩容。

第 2 层:排除 metrics 采集延迟

有没有可能是 metrics 数据还没更新?检查 metrics-server(负责从每个 Node 的 kubelet 采集 CPU/内存指标,暴露给 HPA 使用的集群级聚合器)日志:

metrics-server 采集日志

metrics-server 默认 15s 采集一次,HPA 默认 15s 轮询一次,最坏 30s 延迟。这在秒级波动场景下会影响扩缩容响应速度,但不会让 utilization 14% 变成 50%——30s 后数据就更新过来了,不会持续显示低值。

所以 metrics 采集延迟在这个案例中不是根因。排除掉。

第 3 层:Pod 的资源请求

直接看 Pod 的 YAML 确认:

Pod 资源配置确认

requests.cpu 设了 6 核(6000m)。这个数值来自几个月前的一次"预防性配置"——当时团队担心业务高峰资源不够,把 requests 设高了一截。但实际业务在高峰期也只用到 820m。

这就是问题的根因:HPA 的分母(requests)设得太大,导致 utilization 分子(实际 CPU 用量)再怎么涨,百分比都远低于 50% 的 target。

查看生产集群的历史监控:过去一周通知服务 CPU 平均使用量在 500m-900m 之间波动,从未超过 1.2 核。而 requests 是 6 核——利用率最高也只有 20%。

排查顺序表

排查目标 关键命令 要点
HPA 层 状态与算法 kubectl get hpa -o widekubectl describe hpa 看 TARGETS 列和 Conditions
metrics 层 采集是否延迟 kubectl logs -n kube-system -l k8s-app=metrics-server 看采集间隔是否正常
Pod 层 资源请求 kubectl get pod -o yaml \| grep requests 确认 requests 的合理性
业务层 实际用量 kubectl top podkubectl top node 对比实际用量和 requests

路径

下次遇到 HPA 不扩容,按这个顺序查:

第 1 步:看 HPA 状态

kubectl get hpa -A -o wide

关键看 TARGETS 列的格式——14%/50% 表示当前 utilization 14%,目标 50%。如果 TARGETS 显示 <unknown><unable>,说明 metrics-server 或自定义指标 API 出了问题,是指标管道故障,不是算法问题。

第 2 步:看 HPA 详细状态

kubectl describe hpa <name> -n <namespace>

关注 Conditions 段: - AbleToScale:HPA 控制器是否在正常工作 - ScalingActive:是否获取到了有效的指标数据 - ScalingLimited:是否达到最大/最小副本数的限制

如果 AbleToScale=True 但就是没扩容,说明算法算出来不需要扩——这是典型的"requests 设太大"信号。

第 3 步:对比实际用量和 requests

# 看 Pod 实际 CPU 用量
kubectl top pod -l app=<app-name> -n <namespace>

# 看 Pod 的 requests
kubectl get pod -l app=<app-name> -n <namespace> -o yaml | grep -A 5 resources

对比: - top 显示实际 CPU - requests 是 HPA 分母 - 如果实际用量远小于 requests,utilization 必然偏低

第 4 步(可选):验证 metrics-server 采集正常

kubectl logs -n kube-system -l k8s-app=metrics-server --tail=20

看是否在持续采集(每 15s 左右更新一次)。如果日志中没有 Scraping metrics 的输出,说明 metrics-server 自身有问题,需要排查 metrics-server 的 Pod 状态和网络联通性。


定位

❌ 大多数人会怎么查

"HPA 不扩容 → metrics-server 有问题 → 指标采集延迟了"

kubectl logs -n kube-system -l k8s-app=metrics-server 看了一圈,metrics-server 在正常工作。又去调 HPA 的 --horizontal-pod-autoscaler-sync-period=5s 缩短轮询时间,还把 metrics-server 的 --metric-resolution=10s 改小——结果呢?HPA 还是不扩容。方向跑偏了。

✅ 正确的排查思路

不要怀疑 metrics 管道——先看 HPA 自己的状态。

kubectl get hpa -o wide 的 TARGETS 列已经告诉你一切:如果 TARGETS 能正常显示数字,说明 metrics 管道是通的。问题不在采集延迟,在 utilization 计算公式里。

HPA 的 utilization 公式就三要素: 1. 实际 CPU 用量(来自 metrics-server) 2. Pod 的 requests(来自 Pod 定义) 3. Target 阈值(来自 HPA 定义)

实际用量通过 kubectl top pod 拿得到,requests 通过 kubectl get pod -o yaml 看得到。两个数一比,问题在哪一目了然。

差异在哪

误判对比:错误路线 vs 正确路线

❌ 错误方向 ✅ 正确方向
怀疑对象 metrics-server 采集延迟 HPA 的 utilization 计算
验证方法 kubectl logs metrics-server kubectl get hpa -o wide 看 TARGETS
根因定位 指标管道路径 Pod requests 不合理
修复方式 调采集间隔参数 调 Pod requests 或改 target 类型

更微妙的一点是:metrics 采集延迟和 requests 设太大常常一起出现,症状都是"HPA 不灵敏"。但两者的排查路径完全不同——一个要查 metrics-server 的采集间隔,一个要查 Pod 的资源配置。跳过第一步的 HPA 状态检查,就可能跑到 metrics-server 日志里浪费半小时。


标点

修复方案 A:调整 Pod requests(推荐)

把 requests 从 6 核调到接近实际用量的值:

修复方案 Before/After 对比

调整后 HPA 的 utilization 从 14% 升到 82%——超过 50% 的 target,HPA 自动触发扩容到 5 个 Pod。

修复方案 B:改用 targetAverageValue(备选)

如果 Pod 的 CPU 使用模式波动很大,requests 不好定,可以用绝对 CPU 阈值替代百分比:

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: notification-service
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: notification-svc
  minReplicas: 3
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: AverageValue        # 用绝对数值,而非百分比
        averageValue: 500m        # 当平均 CPU 超过 500m 时扩容

targetAverageValue 绕过了 requests 这个分母——HPA 直接比较当前平均 CPU 用量和 500m 的阈值,不需要经过 "除以 requests" 的步骤。但两个隐藏坑:

坑 1:扩容过程中新增 Pod 刚启动 CPU 接近 0,会瞬间拉低所有 Pod 的平均值。如果此时 HPA 重新计算,看到的平均 CPU 可能从 820m 降到 500m 以下——导致还没完成扩容就决定不扩了。这就是 AverageValue 的"新 Pod 稀释"效应。

坑 2:失去了与 requests 的联动后,requests 完全由调度需求决定。如果 Pod 的 requests 设太小(比如 100m),调度器会把多个 Pod 挤到同一个 Node 上——即使 HPA 不想扩,资源竞争也会导致实际可用 CPU 下降,形成反向恶性循环。

所以方案 B 适合 CPU 使用稳定、新增 Pod 启动快(<10s Ready)的场景。如果 Pod 启动慢,用 averageUtilization(百分比)更稳妥——它不受新增 Pod 的低 CPU 影响,因为公式里 currentUtilization = current / requests,新增 Pod 的 utilization 也会按它的 requests 算。

修复方案 C:了解物理延迟的上限

就算所有配置都正确,从 CPU 飙升到新 Pod Ready 也要经过这段链路:metrics-server 采集 (15s) → HPA 轮询 (15s) → controller-manager 处理 → Deployment 创建 Pod → kubelet 拉镜像+启动容器。总共 30s-2min 是 HPA 的物理延迟,不是配置问题。

K8s v1.23+ 的 stabilization window 默认对扩容无延迟(stabilizationWindowSeconds: 0),缩容 5min 保护期——但这个案例中延迟不在 HPA 自身,在前面整条管道。

Check-list

  • [ ] kubectl get hpa -o wide — 看 TARGETS 列的数值,确认利用率是否接近 target
  • [ ] kubectl describe hpa — 看 Conditions 段,确认出现 AbleToScale=False 还是 ScalingActive=False
  • [ ] kubectl top pod -l app=<app> — 看 Pod 实际 CPU/内存用量
  • [ ] kubectl get pod -o yaml | grep -A 5 resources.requests — 检查 requests 数值
  • [ ] 实际用量 / requests × 100% < target → requests 设太大,需调低或改用 AverageValue
  • [ ] 实际用量 / requests × 100% > target 但 HPA 没扩容 → 检查 stabilization window 或 scaleUp 行为配置
  • [ ] TARGETS 显示 <unknown><unavailable> → 检查 metrics-server 采集状态

故障排查的终点不是修好了——是把排查路径写成 check-list。以后遇到 HPA 不灵敏,先查 TARGETS 列,再对比 top 和 requests,五分钟内定位问题。

下篇我们聊 VPA——当 K8s 自动推荐资源时,你敢信它推的数值吗?偏差大到让你怀疑人生。


附:完整命令清单

# 1. HPA 状态概览
kubectl get hpa -A -o wide

# 2. HPA 详情(含 Conditions)
kubectl describe hpa <name> -n <namespace>

# 3. Pod 实际资源用量
kubectl top pod -l app=<app> -n <namespace>

# 4. Pod 资源配置
kubectl get pod -l app=<app> -n <namespace> -o yaml | grep -A 8 resources

# 5. metrics-server 采集状态
kubectl logs -n kube-system -l k8s-app=metrics-server --tail=20

# 6. metrics-server 健康检查
kubectl get --raw /apis/metrics.k8s.io/v1beta1 | python3 -m json.tool  # 或 jq .

# 7. HPA 行为配置
kubectl get hpa <name> -n <namespace> -o yaml | grep -A 15 behavior

# 8. 节点资源使用
kubectl top node