Kubernetes HPA 弹性伸缩调优:CPU 阈值失效的五个真实原因与自定义指标改造
一个"配了 HPA 却不扩容"的现场
上个月帮一个团队看 CCE 集群,现象很典型:接口 QPS 从 800 涨到 2600,HPA 的 CPU 阈值设的是 70%,副本数上限 30,结果整整四十分钟副本数停在 4 个没动,最后靠人工把副本拉到 24 才扛住。事后复盘,配置项一个都没写错,错的是几个默认值和几个"大家以为不重要"的字段。
这篇把排查过程中真正起作用的五个原因按出现频率排一遍,每个都给定位命令,最后给出用自定义指标替换 CPU 阈值的改法。
原因一:分母错了——requests 没设或设得离谱
HPA 的 CPU 利用率不是一个绝对数字,而是 实际用量 / requests。requests 没设,利用率算不出来,HPA 会直接放弃这个指标;requests 设成 100m 而实际稳态用量 600m,利用率常年 600%,一上来就顶到上限满扩容,然后抖动。
kubectl get hpa -n prod api-server -o yaml | grep -A3 "currentMetrics" kubectl top pod -n prod -l app=api-server
把 currentMetrics 里的 averageUtilization 和 kubectl top 的绝对值对一下,如果比值和 requests 对不上,问题就在 requests。经验值:把 requests 设成 P95 稳态用量,不要设成"最低能跑起来的量"。
原因二:limit 卡住 CPU,利用率永远到不了阈值
这是最隐蔽的一个。requests 200m、limit 300m,Pod 一忙就被 CFS 限流(throttle),实际 CPU 用量被硬压在 300m,利用率最高只有 150%。如果阈值设 200%,看起来"很宽松",实际上 Pod 一直在被限流、响应时间飙升,而 HPA 认为"还没到阈值不用扩"。
kubectl get pod -n prod api-xxx -o jsonpath='{.spec.containers[0].resources}'
# 看 cgroup 限流次数
kubectl exec -n prod api-xxx -- cat /sys/fs/cgroup/cpu.stat | grep throttled
nr_throttled 持续增长就说明被限流了。这时候要么提高 limit,要么把 limit 去掉只留 requests(CPU 是可压缩资源,留 requests 保证调度,不设 limit 让突发能跑满),要么干脆换成按 QPS 扩,见最后一节。
原因三:metrics-server 的窗口比你想象的慢
HPA 默认读的是 metrics-server 的 15 秒采样、60 秒窗口平均值。刚启动的 Pod 在 --horizontal-pod-autoscaler-cpu-initialization-period(默认 5 分钟)内的指标会被忽略,--horizontal-pod-autoscaler-initial-readiness-delay 默认 30 秒。
也就是说:流量在 10 秒内翻倍,HPA 至少要 60~90 秒才反应过来,再加上新 Pod 拉镜像、启动、readiness 通过的时间,真实生效往往在 3 分钟以后。想更快只能缩短窗口,代价是指标抖动会直接传导成副本抖动。
kubectl get --raw /apis/metrics.k8s.io/v1beta1/namespaces/prod/pods | head -c 500
这条命令返回空或者报 the server could not find the requested resource,说明 metrics-server 没装好或证书有问题——HPA 的 kubectl describe 里会出现 failed to get cpu utilization: unable to get metrics。这个报错很多人扫一眼就跳过了,其实它是根因。
原因四:behavior 的默认值把扩容压住了
从 Kubernetes 1.18 起 behavior 是稳定字段,默认 scaleUp.stabilizationWindowSeconds=0、scaleDown.stabilizationWindowSeconds=300。但很多团队从老模板抄过来的 YAML 里带了一段 policies,把每 60 秒最多扩 1 个 Pod 写死了:
behavior:
scaleUp:
stabilizationWindowSeconds: 0
policies:
- type: Pods
value: 1 # 每 60s 只加 1 个
periodSeconds: 60
流量翻三倍的时候,这个策略需要 20 分钟才能扩到目标副本。要么改成 Percent 类型一次扩 100%,要么直接删掉 policies 用默认(默认是 4 个 Pod 或 100% 取大者,60 秒周期)。
原因五:readinessProbe 让新 Pod "迟到"
HPA 扩出来的 Pod 要经过 initialDelaySeconds + 若干次 periodSeconds 才 Ready,未 Ready 的 Pod 不计入可用副本,Service 也不会把流量给它。如果 initialDelaySeconds: 120 是从单体应用时代抄过来的,扩容就等于延迟两分钟生效。
正确做法是把 initialDelaySeconds 降到 5~10 秒,用 startupProbe 单独兜住慢启动,readiness 只判断"能不能接流量"而不是"依赖全不全"。
改造:用 QPS 这类自定义指标替代 CPU
CPU 是滞后指标。请求已经在排队了 CPU 才涨,等 CPU 到阈值,用户已经在骂了。业务侧真正想表达的是"每个 Pod 每秒能扛多少请求",这个用 Prometheus Adapter 暴露成 custom metric 就能直接扩。
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api-server
namespace: prod
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api-server
minReplicas: 4
maxReplicas: 40
metrics:
- type: Pods
pods:
metric:
name: http_requests_per_second
target:
type: AverageValue
averageValue: "120"
behavior:
scaleUp:
stabilizationWindowSeconds: 15
policies:
- type: Percent
value: 100
periodSeconds: 30
scaleDown:
stabilizationWindowSeconds: 300
指标来源可以直接用华为云 AOM 的容器监控,也可以用自建 Prometheus 配 prometheus-adapter 把 rate(http_requests_total[1m]) 映射成 http_requests_per_second。注意 averageValue 是"每个 Pod 的平均值",HPA 会算出 总QPS / 目标值 得到期望副本数,所以这个数字必须来自压测:单 Pod 在 P99 可接受的前提下能扛多少。
一张排查顺序表
| 现象 | 先看什么 | 典型修法 |
|---|---|---|
| HPA 显示 unknown | kubectl describe hpa 的事件 |
修 metrics-server / 补 requests |
| 利用率很高但不扩 | 是否已达 maxReplicas | 调上限 |
| 扩了但没效果 | readinessProbe 耗时 | 降 initialDelay、加 startupProbe |
| Pod 一直 throttle | cpu.stat 的 nr_throttled |
提 limit 或去掉 limit |
| 扩缩容来回抖 | behavior 窗口 | 加大 scaleDown 窗口 |
最后一句
HPA 本身不难,难的是它依赖的三个前提:指标可信、requests 合理、扩容能及时生效。这三件事任何一件不成立,阈值调多少都是玄学。与其反复试 60% 还是 75%,不如先把 kubectl describe hpa 的事件流和 nr_throttled 这两个数看清楚,再决定要不要上自定义指标。
- 点赞
- 收藏
- 关注作者
评论(0)