华为云CCE-HPA弹性伸缩失灵排查
华为云 CCE 弹性伸缩失灵:HPA 配置全对,流量涨了 5 倍却一个 Pod 没加
CPU 打满、Pod 数纹丝不动,HPA 的
kubectl describe里写着unable to get metrics。就算指标正常,从触发到真正接住流量,中间还有调度、拉镜像、JVM 预热三段路要走,加起来往往超过 3 分钟——而流量洪峰可能只有 90 秒。这篇文章把弹性伸缩拆成"决策"和"落地"两段,分别解释为什么会失灵。
一、现象:指标到了,扩容没动
一次典型的失效现场:
| 观察项 | 现象 |
|---|---|
| 业务表现 | 大促开始后接口 P99 从 200ms 涨到 4s |
| Pod 数量 | 始终是 4 个,没有新增 |
| 节点 CPU | 全部 95%+ |
| HPA 状态 | TARGETS: <unknown>/70% |
| HPA 事件 | failed to get cpu utilization: missing request for cpu |
三个信息拼起来,结论很清楚:HPA 根本没拿到数据,它连"该不该扩容"都无法判断。
二、第一段:决策失灵——HPA 算不出利用率
HPA 计算 CPU 利用率的公式是:
利用率 = 实际用量 / request 值
注意分母是 requests.cpu,不是 limits.cpu,也不是节点容量。
如果容器没有声明 requests.cpu,这个除法就无解,HPA 直接报 missing request for cpu,然后什么都不做。
最常见的三种决策失灵
| 原因 | 表现 | 修复 |
|---|---|---|
未设置 requests.cpu |
TARGETS: <unknown> |
补上 requests |
| metrics-server 不可用 | 全部指标 unknown | 检查 metrics-server Pod |
| 指标采集延迟 | 扩容滞后 30~60 秒 | 接受,或用自定义指标 |
正确的最小配置
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: order-api
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: order-api
minReplicas: 4
maxReplicas: 40
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
behavior:
scaleUp:
stabilizationWindowSeconds: 0 # 扩容要快,不设稳定窗口
policies:
- type: Percent
value: 100 # 一次最多翻倍
periodSeconds: 30
scaleDown:
stabilizationWindowSeconds: 300 # 缩容要慢,观察 5 分钟
policies:
- type: Percent
value: 25
periodSeconds: 60
# Deployment 里必须有的部分
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: "2"
memory: 2Gi
这里有个容易搞反的地方:requests.cpu 设得越小,同样的实际用量算出来的利用率越高,越容易触发扩容。 但如果 requests 设得过小,节点会被过度超卖,扩容后 Pod 反而抢不到 CPU。
三、第二段:落地失灵——扩容了,但来不及
这是更隐蔽的一层。即使 HPA 正确决策,Pod 从"创建"到"真正能接流量"中间还有很长的链路:
| 阶段 | 典型耗时 | 说明 |
|---|---|---|
| 指标采集 + 判定 | 30~60 s | metrics-server 默认 15s 采集一次,HPA 默认 15s 同步 |
| 调度 + 节点扩容 | 60~180 s | 如果没有空闲节点,要先扩节点 |
| 拉取镜像 | 10~120 s | 镜像越大越慢,冷节点无缓存 |
| 容器启动 | 5~30 s | 依赖初始化 |
| 应用预热 | 30~180 s | JVM 启动、连接池建立、缓存加载 |
| 就绪探针通过 | 由探针配置决定 | 探针间隔越粗,越晚接流量 |
加总:乐观情况 2 分钟,常见情况 3~5 分钟。
而流量洪峰的特征往往是:90 秒内从 1 倍涨到 5 倍,然后维持 10 分钟。 等你 3 分钟后扩容完成,业务已经超时了一大片。
三个针对性解法
① 节点池预留缓冲容量(最有效)
弹性伸缩要分两层看:Pod 层(HPA)和节点层(节点池)。如果集群没有空闲资源,HPA 扩出来的 Pod 会一直 Pending。
节点池:开启自动伸缩,但保留 1~2 个节点的缓冲
代价是多花一点钱,但换来的是"扩容不用等节点"。这是所有大促保障里性价比最高的一项。
② 应用预热,缩短"能接流量"的时间
JVM 应用尤其明显。三个手段:
- 就绪探针给足
initialDelaySeconds,避免流量打进还没预热好的实例 startupProbe单独配置,把"启动慢"和"运行慢"分开判断,避免启动期被误判为失败而重启- 预热接口:容器启动后主动调用一次热点数据加载,不要等第一个真实请求
startupProbe:
httpGet: { path: /healthz, port: 8080 }
failureThreshold: 30
periodSeconds: 2 # 最长容忍 60 秒启动
readinessProbe:
httpGet: { path: /ready, port: 8080 }
initialDelaySeconds: 5
periodSeconds: 3
③ 镜像瘦身 + 预热节点
镜像从 1.2 GB 压到 300 MB,冷节点拉取时间从 90 秒降到 20 秒。手段就是多阶段构建 + 精简基础镜像,这在弹性场景下不只是"省存储",而是直接影响扩容速度。
四、第三段:扩了也不顶用——指标选错了
有时候 HPA 工作得很正常,Pod 也确实扩了,但延迟还是高。原因往往是指标选错了。
CPU 不是好的扩容信号
对 IO 密集或依赖下游的服务,CPU 涨得慢、延迟涨得快:
流量翻倍 → CPU 从 40% 涨到 55%(没到 70% 阈值,不扩容)
→ 线程池排队 → P99 从 200ms 涨到 3s
这时候 CPU 利用率完全没反映真实压力。
更合适的信号:
| 场景 | 推荐指标 |
|---|---|
| Web 服务 | QPS、并发连接数、P99 延迟 |
| 队列消费 | 积压量(lag) |
| 线程池型应用 | 活跃线程数、队列长度 |
| 有明确 SLO 的服务 | 错误率、SLO 燃烧率 |
这些指标需要业务自己暴露(如 Prometheus 格式),再通过自定义指标接入 HPA。
一个反向陷阱:扩容反而更慢
如果服务有强依赖的共享资源(数据库连接池、下游限流配额),盲目扩容会让所有实例一起抢同一个瓶颈:
4 个 Pod → 数据库连接 40 个,正常
扩到 20 个 Pod → 数据库连接 200 个,超过数据库 max_connections
→ 新 Pod 全部连接失败 → 反而更慢
扩容前必须确认:下游能接住扩容后的并发。 否则弹性伸缩只是把故障从应用层推到数据库层。
五、诊断清单
HPA 不动的时候,按这个顺序查,基本能一次定位:
# 1. 看 HPA 状态和最近事件
kubectl describe hpa order-api
# 2. 看 metrics-server 是否有数据
kubectl top pods
# 3. 看 Pod 是不是 Pending(节点资源不足)
kubectl get pods -o wide | grep Pending
# 4. 看节点池容量
kubectl describe nodes | grep -A5 "Allocated resources"
# 5. 看扩容事件的时间线
kubectl get events --sort-by=.lastTimestamp | tail -50
判断路径:
| 现象 | 结论 |
|---|---|
TARGETS: <unknown> |
缺 requests.cpu 或 metrics-server 异常 |
| 指标正常但 Pod 数不变 | 检查 minReplicas/maxReplicas、behavior 策略 |
Pod 是 Pending |
节点资源不足,扩节点池或开自动伸缩 |
| Pod 起来了但没接流量 | 就绪探针未通过、预热未完成 |
| 全扩了但延迟不降 | 指标选错,或下游成为瓶颈 |
六、效果
| 指标 | 修复前 | 修复后 |
|---|---|---|
| HPA 指标状态 | <unknown> |
正常 |
| 扩容触发时间 | 从不触发 | 30 秒内 |
| Pod 就绪时间 | 4~5 分钟 | 70 秒 |
| 大促期间 P99 | 4 s | 320 ms |
| Pod 峰值数 | 4(未扩容) | 36 |
| 节点池缓冲 | 0 | 2 个节点 |
关键改动只有三项:补 requests.cpu、节点池留缓冲、加 startupProbe 并做应用预热。
七、小结
四句话:
- HPA 的分母是
requests.cpu,没设 requests 就等于没开弹性伸缩,TARGETS: <unknown>就是这个信号。 - 弹性伸缩分两层:Pod 层(HPA)和节点层(节点池),节点没余量时扩出来的 Pod 只会 Pending。
- 从决策到接流量要 3~5 分钟,而洪峰只有 90 秒——所以要靠预留缓冲和预热,不能指望实时扩容。
- CPU 不是万能信号,IO 密集和依赖下游的服务应该用 QPS、延迟或队列积压来驱动扩容。
如果只记一句:弹性伸缩解决的是"长期趋势",短时洪峰必须靠预留容量和快速预热来扛。
- 点赞
- 收藏
- 关注作者
评论(0)