HPA 扩容了 8 个 Pod 仍然扛不住:一次 Kubernetes 弹性伸缩的链路复盘

举报
yd_276090841 发表于 2026/11/02 19:46:14 2026/11/02
【摘要】 上周有个服务在晚高峰被打穿,监控上看到的现象很典型:QPS 从 1200 涨到 2600,HPA 在 90 秒内把副本数从 4 拉到 12,但 P99 延迟不降反升,从 180ms 一路爬到 2.3s,最后触发熔断。事后复盘发现,扩容本身没有错,错在我们把"副本数"当成了"处理能力",而这条链路上有四个环节都不随副本数线性增长。 一、先把现象拆成四段一次请求的耗时可以粗略拆成:排队、调度到 ...

上周有个服务在晚高峰被打穿,监控上看到的现象很典型:QPS 从 1200 涨到 2600,HPA 在 90 秒内把副本数从 4 拉到 12,但 P99 延迟不降反升,从 180ms 一路爬到 2.3s,最后触发熔断。事后复盘发现,扩容本身没有错,错在我们把"副本数"当成了"处理能力",而这条链路上有四个环节都不随副本数线性增长。

一、先把现象拆成四段

一次请求的耗时可以粗略拆成:排队、调度到 Pod、应用内处理、依赖调用。扩容只能改善第三段(而且只在不饱和时),对另外三段几乎没有帮助。我们把这四段各自的指标拉出来对齐时间轴,问题立刻清楚了。

  • 排队:入口 SLB 的连接队列在 14:32 开始堆积,等待连接数从 3 涨到 47。SLB 的后端权重是轮询,新 Pod 注册进后端组需要 20~40 秒(就绪探针 + 注册延迟),这段时间里流量仍然压在老 Pod 上。
  • 应用内处理:单 Pod 的 goroutine 数稳定在 8000 左右,CPU 却只有 45%。这说明不是 CPU 打满,而是被下游阻塞。
  • 依赖调用:MySQL 连接池 20 个连接,12 个 Pod 共享一个 200 的数据库最大连接数上限,实际每个 Pod 只拿到 16 个左右,大量请求在 sql.DB 的 WaitCount 上排队。扩容反而加剧了连接争抢。
  • 冷启动:新 Pod 启动后要预热本地缓存(约 4 万条配置),预热期间处理能力只有稳态的 30%,这段时间它还在接收全量流量。

二、四个反直觉的结论

1. 扩容速度和流量增长速度要比较,而不是和"当前副本数"比较。
HPA 的默认 --horizontal-pod-autoscaler-sync-period 是 15 秒,配合 stabilizationWindowSeconds 默认 300 秒的缩容窗口。扩容看起来快,但从指标触发到新 Pod 真正接住流量,中间还有调度、拉镜像、启动、就绪探针、注册五道关。我们的实测端到端是 55~75 秒。如果流量在 30 秒内翻倍,HPA 一定追不上,必须提前预扩或者用队列长度这类前置指标。

2. requests 定得太低,HPA 的利用率就算不准。
HPA 计算副本数的公式是 期望副本 = ceil(当前副本 × 当前指标值 / 目标值),而 CPU 利用率的分母是 requests。我们最初把 requests.cpu 设成 200m,实际稳态用 600m,于是利用率天然是 300%,HPA 一直在扩容,直到副本数上限 12 才停。把 requests 调到接近 P50 实际用量(500m)之后,同样的负载下 HPA 只扩到 7 个副本就够了。requests 不是"最小值",它是 HPA 的度量基准。

3. 有状态依赖的服务,副本数和吞吐不是线性关系。
数据库连接、Redis 连接、下游 API 的 QPS 配额,这三样都是全局预算。副本数翻倍,单副本可用预算减半。这类服务更适合"限流 + 排队 + 少量大副本",而不是"大量小副本"。我们最后把副本上限从 12 降到 8,同时在应用层加了基于 golang.org/x/time/rate 的本地限流(每 Pod 180 QPS),P99 反而回到了 220ms。

4. 就绪探针写错,等于让扩容失效。
原来的 readinessProbe 只检查 /healthz 返回 200,这个接口只验证进程活着,不验证缓存预热完成。改成检查 /readyz(缓存条目数 > 90% 且数据库连接可用)之后,新 Pod 在预热完成前不会进后端组,避免了"新 Pod 拖慢整体"的雪崩。

三、改完之后的具体配置

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: order-api
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: order-api
  minReplicas: 4
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 65
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 0
      policies:
        - type: Percent
          value: 100
          periodSeconds: 30
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
        - type: Pods
          value: 1
          periodSeconds: 60

几个关键点:扩容窗口设为 0(允许立即扩),缩容窗口保留 300 秒(避免抖动);缩容每次只减 1 个 Pod,给缓存和连接池留出回收时间;averageUtilization 用 65% 而不是 80%,留出突发余量。

四、还可以再往前一步

上面这些都是"被动响应"。真正的解法是把扩容决策提前:

  1. 用队列深度代替 CPU 做扩容指标。CPU 是滞后指标,队列长度是领先指标。接入 KEDA 之后按 RabbitMQ 队列积压数扩容,平均响应时间从 70 秒降到 8 秒。
  2. 定时预扩。业务有明显的时间规律(比如每天 10:00 和 20:00 两个峰),用 CronHPA 在峰值前 15 分钟把 minReplicas 提到 8,比任何实时伸缩都稳。
  3. 给冷启动做真正的预热。把缓存预热做成 initContainer,或者用 startupProbe 给足 120 秒,别让业务流量参与"热身"。

五、一句话总结

弹性伸缩的瓶颈通常不在 HPA 本身,而在"副本数能线性改善的那部分容量"占整体容量的比例。先量出这个比例,再决定是加副本、加限流、还是加缓存。盲目调大 maxReplicas 往往只是把雪崩的时间点往后推了 30 秒。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。