为什么 CPU/内存指标不足以支撑真实业务伸缩
去年双十一大促期间我们某核心服务因 CPU 利用率一直徘徊在 30%40%HPA 始终没有触发扩容。然而实际 QPS 已经飙到正常值的 5 倍响应延迟从 50ms 飙升到 2s最终导致大量超时和 502。事后复盘发现仅靠 CPU/内存指标做弹性伸缩在 IO 密集型、异步处理或长连接业务中几乎形同虚设。这篇文章会手把手带你配置基于业务 QPS 的自定义指标 HPA结合 Prometheus Adapter 暴露指标并对伸缩行为策略做压测调优最后总结生产环境中常见的坑和应对方案。为什么 CPU/内存指标不足以支撑真实业务伸缩原理说明K8s 原生的HorizontalPodAutoscaler默认支持 Pod 的cpu和memory利用率。但这两类指标反映的是系统资源占用而非业务负载。许多场景下异步处理服务大量请求入队列后立即返回 200CPU 空闲但队列深度陡增。I/O 密集型服务网络、磁盘 IO 成为瓶颈CPU 利用率低但吞吐已到极限。长连接 WebSocket连接数高但 CPU 消耗小按照 CPU 永远不扩容。Java 应用 GC 频繁偶尔 CPU 飙升但瞬时过去后复位HPA 来不及响应。结论生产环境必须用业务指标QPS、请求延迟、队列深度等定义弹性伸缩策略。Prometheus Adapter 暴露自定义指标的完整配置前置条件已部署 Prometheus Operator 或独立 Prometheus。业务服务已采集 QPS 指标例如http_requests_total通过rate()计算 QPS。需要安装prometheus-adapter也可用kube-metrics-adapter但社区推荐前者。配置步骤1. 确认服务指标采集假设你的业务 Pod 已通过prometheus.io/scrape: true暴露指标Prometheus 中可查到rate(http_requests_total{namespaceproduction, jobmy-service}[1m])2. 安装 prometheus-adapterHelmhelm repo add prometheus-community https://prometheus-community.github.io/helm-charts helm upgrade --install prometheus-adapter prometheus-community/prometheus-adapter \ --namespace monitoring \ -f adapter-values.yaml3. 编写核心配置文件adapter-values.yaml# adapter-values.yaml prometheus: url: http://prometheus.monitoring.svc:9090 # 指向自己的 Prometheus port: 9090 rules: default: false # 不加载默认的 CPU/Mem 指标可选 custom: - seriesQuery: http_requests_total{namespace!,job!} resources: overrides: namespace: { resource: namespace } pod: { resource: pod } # 定义如何将 Prometheus 查询映射为 K8s 自定义指标 name: as: qps_per_pod metricsQuery: | sum(rate(http_requests_total{.LabelMatchers}[1m])) by (.GroupBy)关键注释-seriesQuery用于发现指标adapter 会据此自动探测可用的标签。-resources.overrides将 Prometheus 的namespace、pod标签映射为 K8s 资源对象HPA 才能正确匹配 Pod。-name.as暴露的自定义指标名称HPA 中引用pods/qps_per_pod。-metricsQuery最终发往 Prometheus 的查询模板。.LabelMatchers会被替换为namespacexxx,podxxx.GroupBy自动分组。4. 验证指标暴露# 查看所有自定义指标 kubectl get --raw /apis/custom.metrics.k8s.io/v1beta1 | jq . # 查看特定 Pod 的 QPS kubectl get --raw /apis/custom.metrics.k8s.io/v1beta1/namespaces/production/pods/*/qps_per_pod | jq .正确输出应返回类似{ kind: MetricValueList, items: [ { metricName: qps_per_pod, value: 12345m, // 12345m 12.345 qps describedObject: {kind: Pod, name: my-service-6f8b7c9d-abc12} } ] }踩坑如果返回status: Failure检查 Prometheus 地址是否可通、seriesQuery能否匹配到指标。HPA v2 行为策略scaleUp、scaleDown 与稳定窗口调优完整 HPA 配置apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: my-service-hpa namespace: production spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: my-service minReplicas: 3 maxReplicas: 30 metrics: - type: Pods pods: metric: name: qps_per_pod target: type: AverageValue averageValue: 5000m # 目标每 Pod 5 QPS behavior: scaleUp: stabilizationWindowSeconds: 30 # 稳定窗口 30 秒 policies: - type: Pods value: 4 # 每次最多增加 4 个 Pod periodSeconds: 60 # 每 60 秒最多执行一次策略 selectPolicy: Max scaleDown: stabilizationWindowSeconds: 300 # 缩容稳定窗口 5 分钟防止抖动 policies: - type: Pods value: 2 periodSeconds: 120 selectPolicy: Max策略参数说明与调优建议参数作用推荐初始值备注stabilizationWindowSeconds(扩容)在窗口期内取指标最大值防止毛刺触发扩容3060s如果业务流量波动剧烈可设 10s窗口越小对突发越敏感stabilizationWindowSeconds(缩容)窗口期内取最小值防止短暂低谷就缩容300600s缩容一定要保守缩容后 Pod 冷启动需要时间避免频繁震荡policies.value(扩容)一次扩容最多加 Pod 数量25配合periodSeconds控制扩容速率防止瞬间启动大量 Pod 把集群打满policies.value(缩容)一次缩容最多减 Pod 数量13避免一次性杀死过多 Pod导致后续请求积压selectPolicyMax取所有策略中最大的变化量Min取最小扩容用Max缩容用Max或Min一般用Max保留决策权调优技巧将稳定窗口视为低通滤波器——窗口越长对瞬时抖动过滤越强但响应越慢。对于突发流量敏感的在线服务如 API 网关建议缩短扩容窗口、缩窄稳定窗口同时用policies限制单次扩容上限避免 Pod 冷启动抢占集群资源。压测验证QPS、延迟、Pod 数量与冷启动时间的关系测试环境服务基于 Spring Boot 的 RESTful API平均处理时间 20ms。指标Prometheus Adapter 暴露qps_per_pod。压测工具wrk持续 5 分钟初始 QPS 1000每 30 秒递增 500。冷启动时间JVM 预热约 12s加上 K8s Pod Ready 约 20s。测试步骤与结果未配置 HPA固定 3 PodQPS 到 3000 时延迟从 50ms 飙到 800ms开始超时。配置默认 HPACPU 80%CPU 仅有 30%未触发扩容结果同上。配置自定义 QPS HPA目标 5 QPS/PodQPS 到 1200 时触发扩容每 60 秒加 4 Pod。压测结束前 Pod 数达到 12延迟稳定在 80ms 左右。关键数据记录压测时间点当前 QPS期望 Pod 数理想实际 Pod 数平均延迟0s10003.3 → 4345ms60s15005 → 63 → 7首次扩容52ms120s20006.7 → 77 → 1168ms180s25008.3 → 911 → 1275ms240s300010 → 101282ms稳定可以看到由于扩容滞后和冷启动时间实际 Pod 数始终落后于理想值但在 3 个周期后趋于收敛。重要结论-扩容永远滞后于流量突增。因此业务系统必须设计合理的限流熔断防止在 Pod 启动完成前过载。- 冷启动时间越长需要的稳定窗口越大。例如 Python 应用启动只需 3s窗口可降到 10s而 Java 应用建议窗口不低于 30s。生产踩坑指标延迟、抖动、突刺流量和回滚策略坑1Prometheus 指标延迟导致扩缩容偏差现象Prometheus 的rate()计算依赖时间窗口如 1m而 HPA 每 15 秒拉取一次指标。当流量突降时rate()窗口内仍包含旧数据导致 QPS 指标缓慢下降HPA 延迟缩容。解决- 减小rate()的窗口到 30s代价是短期抖动更大。- 组合使用avg_over_time平滑或者直接使用 Prometheus 的last_over_time瞬时值。- 在metricsQuery中使用rate(...[30s])代替[1m]。坑2Pod 状态不健康导致指标异常某次我们发现 QPS 指标突然为 0导致 HPA 把 Pod 缩到 minReplicas但实际服务正常运行。原因某个 Pod 被调度到异常节点Prometheus 抓取失败sum(rate(...)) by (pod)返回 0。解决- 在metricsQuery中加入or on(pod) absent(...)填充默认值或者使用max避免被 0 拖低。- 更稳健的做法暴露存活 Pod 的指标总和而非直接用rate() by(pod)取均值。例如暴露namespaceservice级别的总 QPS然后让 HPA 用AverageValue除以 Pod 数。但这种方法需要额外计算。坑3突刺流量导致 Pod 总数暴涨双十一流量瞬时 10 倍HPA 配置了scaleUp.stabilizationWindowSeconds10结果 30s 内扩容了 20 个 Pod集群资源不足部分 Pod 启动失败。解决-硬性限制maxReplicas设合理上限根据集群资源 业务容灾冗余估算。-两阶段扩容使用policies限制单次扩容数量例如 60s 内最多加 5 个 Pod。-水平 垂直组合突发时先对现有 Pod 增加资源请求VPA再缓慢扩容。坑4回滚策略生产环境 HPA 配置错误可能导致服务雪崩。必须准备回滚方案配置版本化所有 HPA、Prometheus Adapter 配置都存 Git并关联 CI/CD 回滚。手动回滚脚本一键恢复旧的 HPA 配置并临时关闭 HPAspec.paused: true。监控告警设置 Pod 变化速率告警当 5 分钟内 Pod 数变化超过 50% 时触发通知人工介入。总结放弃纯 CPU/Memory 的迷信业务 QPS、延迟分位数、队列深度才是实际负载的准确反映。Prometheus Adapter 配置核心在于seriesQuerymetricsQuery模板的正确性务必用kubectl get --raw验证。HPA v2 的behavior字段是调优主战场扩容窗口要短捕获突发、缩容窗口要长防抖动、单次扩容数量要限制防雪崩。压测是唯一检验手段务必记录 Pod 启动时间 vs 指标收敛时间据此调整窗口参数。生产环境必须考虑指标延迟、异常 Pod、突刺流量并且准备一键回滚能力。最后一句真心话弹性伸缩不是万能的它只能缓解“可预期的繁忙”无法应对“完全的失控”。业务本身必须有限流、降级、熔断的最后一环。