从 HPA 到 KEDA:Kubernetes 弹性伸缩的进阶实践
摘要:上线高峰期 Pod 不够用、低谷期一堆 Pod 空转烧钱,是云原生最常见的两难。Kubernetes 内置的 HPA 能解决「按资源指标扩容」,但真的够用吗?QPS 突增、消息队列堆积、按业务定制指标......这些场景它都力不从心。本文从 HPA 的扩缩容算法与抑制策略讲起,再到自定义指标、Metrics API,最后引入事件驱动的 KEDA,把一套生产可用的弹性伸缩体系完整串起来。

一、入门:HPA 到底在算什么
HPA(Horizontal Pod Autoscaler) 是 Kubernetes 里的水平扩缩容控制器,它做的事情用一句话概括:根据指标,算出该跑多少个 Pod。 HPA 控制器的核心是一个 30 秒周期的控制循环:
- 从 Metrics API 拉取当前指标值;
- 用固定算法算出期望副本数;
- 与当前副本数比较,若超出扩容/缩容阈值,就调
ReplicaSet的副本数; - 下一拍,Deployment 据此滚动创建/销毁 Pod。
1.1 那个「固定算法」长什么样
对 CPU/内存这类资源指标,HPA 的期望副本数由下式给出:
desiredReplicas = ceil[currentReplicas × (currentMetricValue / desiredMetricValue)]
举一个具体例子:Deployment 目标 CPU 利用率设为 50%,当前 4 个 Pod,平均 CPU 利用率冲到 80%,那么下一次 HPA 计算:
desiredReplicas = ceil[4 × (80% / 50%)] = ceil[6.4] = 7
于是 HPA 会把副本数从 4 调到 7(在 maxReplicas 限制内)。GPU 上这几个算子都很好理解:比例放大、向上取整、上限封顶。
二、直击痛点:为什么裸 HPA 不够用
等真正上生产,你会发现 HPA 有四个绕不开的坎。
2.1 指标来源太窄
原生 HPA 只能吃两类指标:CPU/内存等资源指标 ,以及Pod/Object 自定义指标。但你真正关心的往往是业务指标------QPS、延迟 P99、队列堆积长度------这些它不直接支持,得自己想办法把指标「喂」进去。
2.2 从采集到生效的延迟
HPA 每 30 秒轮询一次 Metrics API,而指标本身又有从 Kafka/Prometheus 到聚合器的采集延迟。从头到尾往往几十秒到几分钟,遇到流量突刺,Pod 起不来就会看到一批请求被打到超时。这是「事后反应式扩容」的天花板。
2.3 只认「忙」,认不出「堆积」
典型的排队场景:RabbitMQ/Kafka 里消息堆积了几百万条,但 Consumer Pod 的 CPU 却很低。裸 HPA 看着 CPU 不高,「不扩」;于是积压越来越多,「越积越不扩」,死循环。资源指标在真实场景里常常与业务压力脱钩。
2.4 扩容快、缩容也「太快」
HPA 默认的缩容稳定窗口长达 300 秒,就是为了防止抖动;但一旦配置不当,缩容过快同样会把在线服务搞崩。而扩容默认又太快,经常造成副本剧烈抖动。这两端的度很难拿捏。
三、升级第一步:用 Metrics API 把业务指标喂进来
要摆脱「只有 CPU/内存」,关键在于理解 HPA 背后的数据通路。HPA 读取的是一个标准接口------Metrics API ,它再往上游聚合器(Aggregator)取数。所以要让 HPA 认业务指标,本质是往聚合器里注册一个能吐出该指标的服务。
社区里最常用的路数是:把 Prometheus 当监控底座,再用 prometheus-adapter(或称 k8s-prometheus-adapter)把某个 PromQL 查询包装成自定义指标,暴露给聚合器。配置大致长这样:
yaml
metrics:
- type: Pods
pods:
metric:
name: http_qps # 自定义指标名
target:
type: AverageValue
averageValue: 100 # 每个 Pod 的 QPS 目标
配合 HPA 引用它:
yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: web-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: web
minReplicas: 2
maxReplicas: 20
metrics:
- type: Pods
pods:
metric:
name: http_qps
target:
type: AverageValue
averageValue: 100
behavior:
scaleUp:
stabilizationWindowSeconds: 0
policies:
- type: Percent
value: 200
periodSeconds: 60
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 20
periodSeconds: 60
这段配置暴露了三个生产经验:scaleUp.stabilizationWindowSeconds 设 0 让扩容立即响应;scaleDown 保留 300 秒稳定窗口防止抖降;扩容速度(Percent 200)远快于缩容(Percent 20),实现「快扩慢缩」。HPA 缩放敏感度正是靠 behavior 字段按下快慢键。
四、终极解法:KEDA,让缩扩容听「事件」的
Prometheus adapter 能喂指标,但它仍然是被动轮询、且只能处理「数值型指标」 。真正要按事件驱动,社区推出了 KEDA(Kubernetes Event-Driven Autoscaling) 。它是 CNCF 孵化项目,定位很明确:「要用 K8s 原生信息,但原生 HPA 够不着的东西都交给 KEDA。」
4.1 KEDA 的核心架构
KEDA 的架构非常精简,本质上是一个自研 HPA 的控制器:
- Scaler(触发器):内置 70+ 个事件源适配器------Kafka、RabbitMQ、Redis、Prometheus、AWS SQS、MySQL、Azure Queue......每个 Scaler 负责从对应事件源拉取「压力值」,并把它转换成一个自定义指标暴露给 Metrics API。
- KEDA Controller:核心调度逻辑,像「代理」一样替你去创建和管理一个隐藏的 HPA,并把 Scaler 吐出的指标交给它。
- 与原生 HPA 无缝兼容 :KEDA 创建的其实就是一个普通的
HorizontalPodAutoscaler对象,K8s 原生扩缩容逻辑(副本控制、滚动更新、稳定窗口)原样生效。
4.2 一个 Kafka 堆积的落地实例
回到那个「CPU 低、队列却爆了」的死结。用 KEDA 的 Kafka Scaler 这样写:
yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: kafka-consumer
spec:
scaleTargetRef:
name: consumer # 目标 Deployment
pollingInterval: 10 # 每 10 秒查一次 broker
triggers:
- type: kafka
metadata:
topic: orders
bootstrapServers: kafka:9092
consumerGroup: cg-orders
lagThreshold: "1000" # 消息积压超 1000 条即扩容
minReplicaCount: 1
maxReplicaCount: 30
机制很好理解:KEDA 每 pollingInterval 秒去查一次消费者组的积压(lag),当 lag 超过 lagThreshold 时把 Scaler 指标置为「多」,触发扩容,Consumer 起来消费、lag 回落,再自动缩容。这一步彻底打通了「事件压力 → 业务扩容」的闭环,不再依赖 CPU 兜底。
拿一个真实的电商订单流来说:白天大促时订单消息以每秒上万条的速度灌进 Kafka,Consumer 服务 CPU 反而只有 20%------因为瓶颈在消息拉取与事务提交,不在 CPU 计算。裸 HPA 看到低 CPU 无动于衷,KEDA 的 Kafka Scaler 却能看到 lag 在指数级拉高,于是从 3 个副本一路扩到 20 个;流量回落后 lag 归零,又缩回保底的 3 个。整个过程中,Consumer 的 CPU 曲线几乎纹丝不动,真正被观测和调度的对象,是队列里的「事件压力」本身。
4.3 KEDA 缩放公式与热区优化
KEDA 的默认扩展公式(不在 HPA 内置范围)约为:
实际期望副本 ≈ max(ceil(当前lag / lagThreshold × 当前副本数), minReplicaCount)
也就是说,lag 是目标阈值的 3 倍,副本数就向 3 倍靠近,K8s 的 HPA 再做二次平滑。生产中建议配合两点:一是把 pollingInterval 调小(如 5-10s)以加速响应,二是给消费者加上消费速率反馈 ------用 KEDA 的 advanced 参数做 fallback,当事件源暂不可达时维持一个兜底副本数,避免误缩到零。

五、组合拳:KEDA × Cluster Autoscaler 的联动扩缩
到这里讲的全是「水平扩 Pod」,前提是集群里有空闲节点可放。真实生产里,集群本身也是要伸缩的。整条链路应该是这样协同的:
- 业务侧:KEDA + HPA 负责把 Deployment 的 Pod 数压到符合业务压力的值;
- 资源侧 :节点快满了,
Cluster Autoscaler看到 Pod Pending,自动扩容集群节点,扩容出来的节点承载新的 Pod; - 成本侧:业务低谷,HPA 把 Pod 缩掉,CA 把空节点回收,账单随之回落。
这三层各司其职,共同构成「应用→节点→账单 」的完整弹性。不少公司还会叠加节点池的 Spot(竞价)实例策略,把非核心负载放便宜节点上,进一步压成本。**注意:KEDA 和 HPA 调度的是应用层数,CA 调度的是节点层数,两者必须同时跑,否则会出现「Pod 在等节点、节点在等 Pod」的空转。**关键在于确保 replicas 上限 × Pod 资源上限,不超过集群可承载容量,再由 CA 兜底补节点。
六、实战避坑清单
结合多年线上踩坑,几条高频建议:
- 别把 QPS 目标设成「全部一秒的平均」:用 P99 或「单 Pod 平均」这类更贴单实例容量的口径,否则扩缩会失真。
- 给自定义指标配好「缺省值」:Prometheus 查询在冷启动或指标缺失时返回空,HPA 可能误判,务必让 adapter 输出 0 或默认真实值。
- 扩容快、缩容慢 :扩用
Percent 100-200+ 0 稳定窗口,缩用较长稳定窗口 + 更小的步长,别让副本像坐过山车。 - min/max 一定要设 :
minReplicas是保底能力,maxReplicas是成本天花板,两者是弹性安全网的两条边。 - 监控 HPA 的行为 :
kubectl describe hpa能看每次扩缩原因和指标,hpa 抖动率是判断配置好坏的直接指标。 - 重活交给 async:能异步化就异步化(消息队列 + Consumer),让 Web 层保持轻量,扩容也比同步扛大请求便宜得多。
七、小结
回到开头那个两难:HPA 解决了「看 CPU/内存扩容」的基础诉求,但真实业务的压力常常藏在队列、QPS、事件里。 从裸 HPA → Prometheus 自定义指标 → KEDA 事件驱动,是一条清晰的升级路径,绝非「用哪个代替哪个」这么简单,而是四层协同------应用层 HPA/KEDA、指标层 Metrics API、事件层 Scaler、节点层 Cluster Autoscaler 各管一段。把「数据」和「调度」分离开来,弹性才能真正既准又稳。
对团队而言,弹性伸缩永远是「指标先行、调度跟进」的工程,不是配置一个 YAML 就万事大吉。把指标口径、稳定窗口、兜底策略先想清楚,线上才不至于被流量打脸。
数据来源:HPA 算法公式与稳定窗口来自 Kubernetes 官方文档(Horizontal Pod Autoscaling);缩放速度控制(behavior.stabilizationWindowSeconds)来自 KEP-853;KEDA 概念与 Scaler 配置来自 KEDA 官方文档。图 3 联动曲线为场景示意图,非真实压测数据。