1. 背景:大促流量洪峰下的容量失控
2025 年双 11 备战期,我们负责的电商订单中台面临一个棘手的容量问题。业务侧日均订单量约 200 万,大促峰值预估达到日常的 8 倍。订单创建链路涉及库存预占、优惠计算、支付回调等多个下游依赖,其中优惠计算服务(coupon-service)对 CPU 的消耗并不突出,真正的瓶颈在于 Redis 热点 key 的访问延迟和本地缓存命中率。
为什么我会接触到 HPA 自定义指标?起因很简单:大促压测时,订单接口的 P99 延迟从平时的 120ms 飙升到 2.3s,优惠计算线程池任务堆积超过 5 万,但 Kubernetes 默认的 HPA 却判定"无需扩容"。我需要搞清楚:为什么 CPU 指标在 IO 密集型服务上失效,以及如何让 HPA 感知真实的业务压力。
2. 踩坑与现状:CPU 单指标方案的三个真实痛点
我们最初采用 Kubernetes 默认的 HPA 配置,仅基于 CPU 使用率进行扩缩容:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: coupon–service–hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: coupon–service
minReplicas: 6
maxReplicas: 60
metrics:
– type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
压测结束后我们拉取了监控数据,发现三个关键痛点:
根因清楚了:默认 HPA 只认识资源指标,不认识业务指标。我们需要让 HPA 感知"线程池排队深度"和"请求处理延迟"这类直接反映服务健康状况的信号。
3. 方案:三种自定义指标接入路径与选型
我们评估了三条技术路线,各有取舍。
方案 A:Prometheus Adapter + 自定义 Metrics API
在集群中部署 Prometheus 采集业务指标,通过 prometheus-adapter 将其暴露为 custom.metrics.k8s.io 接口,HPA 直接消费。
优点:生态成熟,Prometheus 已是可观测性标配,指标定义灵活。
缺点:需要维护 adapter 的指标映射规则(rules 配置),且 Prometheus 拉取周期(默认 30s)与 HPA 同步周期(默认 15s)叠加,端到端响应延迟约 45–60s,对突发流量仍偏慢。
方案 B:KEDA(Kubernetes Event-Driven Autoscaling)
KEDA 作为 HPA 的上层封装,内置 Redis、Kafka、RabbitMQ 等几十种 Scaler,可以直接读取 Redis 的 LLEN 命令获取队列长度,无需自建指标链路。
优点:Redis Scaler 开箱即用,配置简单,响应速度快(秒级)。
缺点:多引入一个控制器组件,且队列长度指标只能反映"积压",无法表达"处理能力余量"。
方案 C:Kubernetes 原生 HPA + 自定义外部指标(External Metrics)
通过 external.metrics.k8s.io 接口暴露业务指标,HPA 原生支持,不引入额外控制器。
优点:架构最简洁,指标来源可以是任意监控系统。
缺点:需要自己实现 Metrics API 服务,开发成本最高。
选型结论
我们最终选择 方案 A:Prometheus Adapter。理由有三:一是集群已有 Prometheus + Grafana 监控体系,指标采集链路现成;二是我们后续还要接入更多业务指标(如订单积压数、缓存命中率),adapter 的规则配置比 KEDA 的 Scaler 更灵活;三是团队对 Prometheus 的 PromQL 足够熟悉,排查问题成本低。KEDA 的 Redis Scaler 我们保留作为备选,在队列积压场景确实更简单,但当前阶段不想引入新的控制器。
4. 实操:基于线程池活跃度的自定义 HPA
4.1 业务侧暴露指标
在 coupon-service 中引入 Micrometer(配合 Spring Boot 3.2),将线程池状态暴露为 Prometheus 指标:
// pom.xml 关键依赖
// io.micrometer:micrometer-registry-prometheus:1.12.5
// org.springframework.boot:spring-boot-starter-actuator:3.2.4
@Configuration
public class ThreadPoolMetricsConfig {
@Bean
public ThreadPoolTaskExecutor couponExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(50);
executor.setMaxPoolSize(200);
executor.setQueueCapacity(5000);
executor.setThreadNamePrefix("coupon-worker-");
executor.initialize();
// 暴露活跃线程数与队列深度
Metrics.gauge("coupon_executor_active_threads", executor,
e -> e.getThreadPoolExecutor().getActiveCount());
Metrics.gauge("coupon_executor_queue_depth", executor,
e -> e.getThreadPoolExecutor().getQueue().size());
return executor;
}
}
4.2 部署 Prometheus Adapter
使用 Helm 安装 prometheus-adapter 4.10.0,核心是配置指标映射规则:
# values.yaml 关键片段
rules:
– seriesQuery: 'coupon_executor_queue_depth'
resources:
overrides:
namespace: { resource: "namespace" }
pod: { resource: "pod" }
metricsQuery: 'sum(coupon_executor_queue_depth{<<.LabelMatchers>>}) by (<<.GroupBy>>)'
4.3 定义基于自定义指标的 HPA
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: coupon–service–hpa–v2
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: coupon–service
minReplicas: 6
maxReplicas: 60
metrics:
– type: Pods
pods:
metric:
name: coupon_executor_queue_depth
target:
type: AverageValue
averageValue: 800
– type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
behavior:
scaleUp:
stabilizationWindowSeconds: 30
policies:
– type: Percent
value: 100
periodSeconds: 15
scaleDown:
stabilizationWindowSeconds: 300
这里的关键设计是双指标策略:队列深度超过 800 立即扩容,CPU 作为兜底;behavior.scaleUp 将扩容步长放宽到 100%/15s,缩短扩容响应时间。
5. 踩坑记录:三个真实问题与解决过程
5.1 指标采集不到:adapter 规则 namespace 映射错误
报错:
E0712 14:23:11.882312 1 manager.go:112] unable to get metric coupon_executor_queue_depth:
no matching metrics found in the list of metrics provided by the custom metrics API
排查:先确认 Pod 的 /actuator/prometheus 端点能拉到 coupon_executor_queue_depth 指标,再用 kubectl get –raw "/apis/custom.metrics.k8s.io/v1beta1" 查看 adapter 暴露的指标列表,发现指标名存在但 namespace 标签缺失。
解决:adapter 的 resources.overrides 中 namespace 和 pod 的映射写反了,修正后指标正常暴露。
5.2 扩容风暴:指标抖动导致 Pod 频繁创建销毁
现象:大促流量回落时,队列深度在 700–900 之间波动,HPA 在 15 分钟内反复扩容缩容 8 次,造成 Pod 频繁重建,连接池反复预热。
解决:调大 scaleDown.stabilizationWindowSeconds 到 300s,并增加 scaleDown.policies 限制单次缩容比例不超过 20%。扩容侧保持激进,缩容侧保持保守,这是大促容量治理的基本原则。
5.3 指标延迟:Prometheus 拉取周期导致扩容滞后
现象:压测发现从流量突增到 HPA 触发扩容,端到端延迟约 50s,比预期慢。
解决:将 Prometheus 的 scrape_interval 从 30s 调到 10s(仅对 coupon-service 生效),HPA 的 –horizontal-pod-autoscaler-sync-period 保持默认 15s。同时把扩容步长从 100% 放宽到 200%,让单次扩容能拉起更多副本。实测端到端延迟降到 25s 以内。
6. 验证数据与效果
双 11 当天压测和实战数据对比如下:
| 扩容触发延迟 | 50s+ | 25s 以内 |
| P99 延迟(峰值) | 2.3s | 480ms |
| 峰值 Pod 数 | 42 | 58 |
| 线程池队列积压峰值 | 5 万+ | 3000 以内 |
| 扩容次数(4 小时) | 6 次 | 23 次(更贴合流量曲线) |
大促当天订单峰值 1600 万单/小时,优惠计算服务全程无容量告警,P99 稳定在 500ms 以内。相比去年同期的 CPU 单指标方案,扩容响应快了近一倍,且没有出现一次因容量不足导致的订单失败。
7. 复盘:什么场景该用,什么场景不该用
适用场景:
- IO 密集型服务(大量等待 Redis、数据库、外部 API 响应),CPU 无法反映真实负载。
- 有明确业务信号的场景:队列深度、请求延迟、积压消息数等。
- 流量呈脉冲式突增(大促、秒杀、热点事件),需要秒级响应的场景。
不适用场景:
- 纯 CPU 密集型计算服务(如图片处理、加解密),CPU 指标已经足够准确,引入自定义指标徒增复杂度。
- 指标采集链路本身不稳定的小集群,自定义指标依赖 Prometheus 的可用性,监控挂了扩容也就挂了。
- 对成本极度敏感、不希望频繁扩缩容的场景,自定义指标的高灵敏度会带来更多的 Pod 创建销毁。
最后的建议:自定义指标 HPA 不是银弹,它解决的是"指标选得对不对"的问题,而不是"容量规划做没做"的问题。大促容量治理的根基仍然是压测、限流、熔断和预案演练,HPA 只是让这些预案执行得更快、更精准。

