【K8S 运维实战】24-资源优化HPA与VPA

资源优化:HPA/VPA/Cluster Autoscaler

一句话定位:让集群容量跟着流量走,而不是按峰值静态预留------本文讲清楚四种弹性组件怎么组合、怎么踩坑、怎么估滞后。

写在前面

线上跑了一年的集群,大多数资源利用率在 20-30%,但每到促销就要扩容。同学来问:"能不能让集群自己跟着流量长?"答案能,但要看清四件事:HPA(Pod 数量伸缩)、VPA(Pod 资源配额伸缩)、Cluster Autoscaler(节点伸缩)、KEDA(事件驱动伸缩)。这四个组件单独用都有明显短板,组合用才是生产方案。

这一篇我把生产里跑了三年的弹性方案整理出来,从算法原理 → 配置 → 踩坑 → 多级组合,最后给一份"伸缩滞后评估表",让你能预估业务流量翻 5 倍时到底要等多久。

核心问题

怎么让集群容量跟着流量走?

这个问题拆成三层:

  1. Pod 层弹性:同一个 Deployment 副本数怎么变?(HPA / KEDA)
  2. Pod 内部弹性:单个 Pod 的 CPU/Memory 怎么调?(VPA)
  3. 节点层弹性:节点不够了怎么扩?(Cluster Autoscaler)

三层各有滞后,叠加起来才能扛住真实流量。

一、原理剖析

1.1 HPA v2 算法

HPA v2(autoscaling/v2)的核心公式:

复制代码
desiredReplicas = ceil(currentReplicas * (currentMetricValue / desiredMetricValue))

举例:当前 4 副本,CPU 使用率 80%,目标 50%:

复制代码
desiredReplicas = ceil(4 * 80 / 50) = ceil(6.4) = 7

关键行为:
#mermaid-svg-YwKkblFVLFlgd2xR{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-YwKkblFVLFlgd2xR .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-YwKkblFVLFlgd2xR .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-YwKkblFVLFlgd2xR .error-icon{fill:#552222;}#mermaid-svg-YwKkblFVLFlgd2xR .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-YwKkblFVLFlgd2xR .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-YwKkblFVLFlgd2xR .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-YwKkblFVLFlgd2xR .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-YwKkblFVLFlgd2xR .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-YwKkblFVLFlgd2xR .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-YwKkblFVLFlgd2xR .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-YwKkblFVLFlgd2xR .marker{fill:#333333;stroke:#333333;}#mermaid-svg-YwKkblFVLFlgd2xR .marker.cross{stroke:#333333;}#mermaid-svg-YwKkblFVLFlgd2xR svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-YwKkblFVLFlgd2xR p{margin:0;}#mermaid-svg-YwKkblFVLFlgd2xR .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-YwKkblFVLFlgd2xR .cluster-label text{fill:#333;}#mermaid-svg-YwKkblFVLFlgd2xR .cluster-label span{color:#333;}#mermaid-svg-YwKkblFVLFlgd2xR .cluster-label span p{background-color:transparent;}#mermaid-svg-YwKkblFVLFlgd2xR .label text,#mermaid-svg-YwKkblFVLFlgd2xR span{fill:#333;color:#333;}#mermaid-svg-YwKkblFVLFlgd2xR .node rect,#mermaid-svg-YwKkblFVLFlgd2xR .node circle,#mermaid-svg-YwKkblFVLFlgd2xR .node ellipse,#mermaid-svg-YwKkblFVLFlgd2xR .node polygon,#mermaid-svg-YwKkblFVLFlgd2xR .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-YwKkblFVLFlgd2xR .rough-node .label text,#mermaid-svg-YwKkblFVLFlgd2xR .node .label text,#mermaid-svg-YwKkblFVLFlgd2xR .image-shape .label,#mermaid-svg-YwKkblFVLFlgd2xR .icon-shape .label{text-anchor:middle;}#mermaid-svg-YwKkblFVLFlgd2xR .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-YwKkblFVLFlgd2xR .rough-node .label,#mermaid-svg-YwKkblFVLFlgd2xR .node .label,#mermaid-svg-YwKkblFVLFlgd2xR .image-shape .label,#mermaid-svg-YwKkblFVLFlgd2xR .icon-shape .label{text-align:center;}#mermaid-svg-YwKkblFVLFlgd2xR .node.clickable{cursor:pointer;}#mermaid-svg-YwKkblFVLFlgd2xR .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-YwKkblFVLFlgd2xR .arrowheadPath{fill:#333333;}#mermaid-svg-YwKkblFVLFlgd2xR .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-YwKkblFVLFlgd2xR .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-YwKkblFVLFlgd2xR .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YwKkblFVLFlgd2xR .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-YwKkblFVLFlgd2xR .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YwKkblFVLFlgd2xR .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-YwKkblFVLFlgd2xR .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-YwKkblFVLFlgd2xR .cluster text{fill:#333;}#mermaid-svg-YwKkblFVLFlgd2xR .cluster span{color:#333;}#mermaid-svg-YwKkblFVLFlgd2xR div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-YwKkblFVLFlgd2xR .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-YwKkblFVLFlgd2xR rect.text{fill:none;stroke-width:0;}#mermaid-svg-YwKkblFVLFlgd2xR .icon-shape,#mermaid-svg-YwKkblFVLFlgd2xR .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YwKkblFVLFlgd2xR .icon-shape p,#mermaid-svg-YwKkblFVLFlgd2xR .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-YwKkblFVLFlgd2xR .icon-shape .label rect,#mermaid-svg-YwKkblFVLFlgd2xR .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YwKkblFVLFlgd2xR .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-YwKkblFVLFlgd2xR .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-YwKkblFVLFlgd2xR :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} < 1.0
> 1.0 且 > 1.1
0.9~1.1
Metrics Server

每 15s 采集
HPA Controller

每 15s 计算
当前指标/目标
缩容冷却 5min
扩容无冷却
容忍区间不动作
逐步降低副本
立即提升副本

几个关键参数:

  • tolerance:默认 0.1,指标在 ±10% 内不动作,避免抖动
  • scaleDown.stabilizationSeconds:默认 300s,缩容前观察 5min,取最大值避免回弹
  • scaleDown.policies:缩容速率限制,默认每分钟 100%
  • scaleUp.policies:扩容速率限制,默认每分钟 100%,每 30 秒 0%(冷启动坑)
  • minReplicas / maxReplicas:硬上下限

1.2 VPA 的工作模式

VPA(Vertical Pod Autoscaler)有三种模式:

模式 行为 适用场景
off 仅推荐,不修改 调研期,看推荐值
initial 仅在 Pod 新建时应用推荐 生产推荐,不影响存量 Pod
auto 自动重建 Pod 应用推荐 谨慎用,会触发重建

VPA 的限制比 HPA 多得多:

  • 不能和 HPA 同时用于同一指标(CPU/Memory),否则循环依赖
  • 重建 Pod 会断流量:即使有 PDB,也会逐个杀
  • 不支持 StatefulSet 老版本:1.27+ 才稳定
  • 不保证推荐合理:对突发负载推荐值会偏高

1.3 Cluster Autoscaler 的扩缩策略

CA(Cluster Autoscaler)工作流程:
#mermaid-svg-VePDPFpNJnvf9Jln{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-VePDPFpNJnvf9Jln .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-VePDPFpNJnvf9Jln .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-VePDPFpNJnvf9Jln .error-icon{fill:#552222;}#mermaid-svg-VePDPFpNJnvf9Jln .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-VePDPFpNJnvf9Jln .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-VePDPFpNJnvf9Jln .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-VePDPFpNJnvf9Jln .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-VePDPFpNJnvf9Jln .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-VePDPFpNJnvf9Jln .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-VePDPFpNJnvf9Jln .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-VePDPFpNJnvf9Jln .marker{fill:#333333;stroke:#333333;}#mermaid-svg-VePDPFpNJnvf9Jln .marker.cross{stroke:#333333;}#mermaid-svg-VePDPFpNJnvf9Jln svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-VePDPFpNJnvf9Jln p{margin:0;}#mermaid-svg-VePDPFpNJnvf9Jln .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-VePDPFpNJnvf9Jln .cluster-label text{fill:#333;}#mermaid-svg-VePDPFpNJnvf9Jln .cluster-label span{color:#333;}#mermaid-svg-VePDPFpNJnvf9Jln .cluster-label span p{background-color:transparent;}#mermaid-svg-VePDPFpNJnvf9Jln .label text,#mermaid-svg-VePDPFpNJnvf9Jln span{fill:#333;color:#333;}#mermaid-svg-VePDPFpNJnvf9Jln .node rect,#mermaid-svg-VePDPFpNJnvf9Jln .node circle,#mermaid-svg-VePDPFpNJnvf9Jln .node ellipse,#mermaid-svg-VePDPFpNJnvf9Jln .node polygon,#mermaid-svg-VePDPFpNJnvf9Jln .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-VePDPFpNJnvf9Jln .rough-node .label text,#mermaid-svg-VePDPFpNJnvf9Jln .node .label text,#mermaid-svg-VePDPFpNJnvf9Jln .image-shape .label,#mermaid-svg-VePDPFpNJnvf9Jln .icon-shape .label{text-anchor:middle;}#mermaid-svg-VePDPFpNJnvf9Jln .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-VePDPFpNJnvf9Jln .rough-node .label,#mermaid-svg-VePDPFpNJnvf9Jln .node .label,#mermaid-svg-VePDPFpNJnvf9Jln .image-shape .label,#mermaid-svg-VePDPFpNJnvf9Jln .icon-shape .label{text-align:center;}#mermaid-svg-VePDPFpNJnvf9Jln .node.clickable{cursor:pointer;}#mermaid-svg-VePDPFpNJnvf9Jln .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-VePDPFpNJnvf9Jln .arrowheadPath{fill:#333333;}#mermaid-svg-VePDPFpNJnvf9Jln .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-VePDPFpNJnvf9Jln .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-VePDPFpNJnvf9Jln .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-VePDPFpNJnvf9Jln .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-VePDPFpNJnvf9Jln .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-VePDPFpNJnvf9Jln .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-VePDPFpNJnvf9Jln .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-VePDPFpNJnvf9Jln .cluster text{fill:#333;}#mermaid-svg-VePDPFpNJnvf9Jln .cluster span{color:#333;}#mermaid-svg-VePDPFpNJnvf9Jln div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-VePDPFpNJnvf9Jln .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-VePDPFpNJnvf9Jln rect.text{fill:none;stroke-width:0;}#mermaid-svg-VePDPFpNJnvf9Jln .icon-shape,#mermaid-svg-VePDPFpNJnvf9Jln .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-VePDPFpNJnvf9Jln .icon-shape p,#mermaid-svg-VePDPFpNJnvf9Jln .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-VePDPFpNJnvf9Jln .icon-shape .label rect,#mermaid-svg-VePDPFpNJnvf9Jln .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-VePDPFpNJnvf9Jln .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-VePDPFpNJnvf9Jln .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-VePDPFpNJnvf9Jln :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是

是 且超过延迟

Pod Pending

调度失败
CA 扫描

每 10s 一次
有 Pending Pod?
expander 选择节点模板
云厂商 ASG 扩容
新节点加入

1-3min
kubelet 注册
Pod 调度上去
节点低利用率?
驱逐 Pod
节点缩容
等待

关键参数:

  • --scan-interval:默认 10s,扫描间隔
  • --scale-down-delay-after-add:扩容后多久才开始考虑缩容,默认 10min
  • --scale-down-unneeded-time:节点持续低利用率多久才缩容,默认 30min
  • --scale-down-utilization-threshold:节点利用率阈值,默认 0.5(CPU+Memory)
  • --expander:节点模板选择策略,推荐 priority(优先 Spot)或 random

1.4 KEDA 事件驱动伸缩

KEDA 是 HPA 的"前菜",它把外部指标(消息队列长度、Cron、Prometheus)转换成 HPA 可消费的 External Metric。
#mermaid-svg-4Z2zKCgyIBUGtWoI{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-4Z2zKCgyIBUGtWoI .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-4Z2zKCgyIBUGtWoI .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-4Z2zKCgyIBUGtWoI .error-icon{fill:#552222;}#mermaid-svg-4Z2zKCgyIBUGtWoI .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-4Z2zKCgyIBUGtWoI .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-4Z2zKCgyIBUGtWoI .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-4Z2zKCgyIBUGtWoI .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-4Z2zKCgyIBUGtWoI .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-4Z2zKCgyIBUGtWoI .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-4Z2zKCgyIBUGtWoI .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-4Z2zKCgyIBUGtWoI .marker{fill:#333333;stroke:#333333;}#mermaid-svg-4Z2zKCgyIBUGtWoI .marker.cross{stroke:#333333;}#mermaid-svg-4Z2zKCgyIBUGtWoI svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-4Z2zKCgyIBUGtWoI p{margin:0;}#mermaid-svg-4Z2zKCgyIBUGtWoI .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-4Z2zKCgyIBUGtWoI .cluster-label text{fill:#333;}#mermaid-svg-4Z2zKCgyIBUGtWoI .cluster-label span{color:#333;}#mermaid-svg-4Z2zKCgyIBUGtWoI .cluster-label span p{background-color:transparent;}#mermaid-svg-4Z2zKCgyIBUGtWoI .label text,#mermaid-svg-4Z2zKCgyIBUGtWoI span{fill:#333;color:#333;}#mermaid-svg-4Z2zKCgyIBUGtWoI .node rect,#mermaid-svg-4Z2zKCgyIBUGtWoI .node circle,#mermaid-svg-4Z2zKCgyIBUGtWoI .node ellipse,#mermaid-svg-4Z2zKCgyIBUGtWoI .node polygon,#mermaid-svg-4Z2zKCgyIBUGtWoI .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-4Z2zKCgyIBUGtWoI .rough-node .label text,#mermaid-svg-4Z2zKCgyIBUGtWoI .node .label text,#mermaid-svg-4Z2zKCgyIBUGtWoI .image-shape .label,#mermaid-svg-4Z2zKCgyIBUGtWoI .icon-shape .label{text-anchor:middle;}#mermaid-svg-4Z2zKCgyIBUGtWoI .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-4Z2zKCgyIBUGtWoI .rough-node .label,#mermaid-svg-4Z2zKCgyIBUGtWoI .node .label,#mermaid-svg-4Z2zKCgyIBUGtWoI .image-shape .label,#mermaid-svg-4Z2zKCgyIBUGtWoI .icon-shape .label{text-align:center;}#mermaid-svg-4Z2zKCgyIBUGtWoI .node.clickable{cursor:pointer;}#mermaid-svg-4Z2zKCgyIBUGtWoI .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-4Z2zKCgyIBUGtWoI .arrowheadPath{fill:#333333;}#mermaid-svg-4Z2zKCgyIBUGtWoI .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-4Z2zKCgyIBUGtWoI .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-4Z2zKCgyIBUGtWoI .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4Z2zKCgyIBUGtWoI .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-4Z2zKCgyIBUGtWoI .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4Z2zKCgyIBUGtWoI .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-4Z2zKCgyIBUGtWoI .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-4Z2zKCgyIBUGtWoI .cluster text{fill:#333;}#mermaid-svg-4Z2zKCgyIBUGtWoI .cluster span{color:#333;}#mermaid-svg-4Z2zKCgyIBUGtWoI div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-4Z2zKCgyIBUGtWoI .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-4Z2zKCgyIBUGtWoI rect.text{fill:none;stroke-width:0;}#mermaid-svg-4Z2zKCgyIBUGtWoI .icon-shape,#mermaid-svg-4Z2zKCgyIBUGtWoI .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4Z2zKCgyIBUGtWoI .icon-shape p,#mermaid-svg-4Z2zKCgyIBUGtWoI .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-4Z2zKCgyIBUGtWoI .icon-shape .label rect,#mermaid-svg-4Z2zKCgyIBUGtWoI .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4Z2zKCgyIBUGtWoI .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-4Z2zKCgyIBUGtWoI .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-4Z2zKCgyIBUGtWoI :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Kafka 队列
KEDA Scaler
External Metric
HPA v2
Deployment 扩缩
Cron 触发器
Prometheus

KEDA 解决了 HPA 的两个痛点:

  1. 冷启动 :KEDA 支持 minReplicaCount=0,没流量时缩到 0,来流量时激活
  2. 事件驱动:不用 CPU/内存,直接根据"队列里有多少消息"伸缩

1.5 伸缩滞后叠加

四级弹性叠加的延迟:

层级 触发延迟 扩容动作延迟 典型总延迟
KEDA 30s 0s(只改 metric) 30s
HPA 15s 0s(改 replicas) 15s
Pod 启动 - 10-60s(镜像+应用) 30s
CA 10s 60-180s(节点起机) 120s
总计 - - 195s

意味着从流量突增到 Pod 就绪,最坏要 3+ 分钟。这是设计容量规划的硬约束。

二、实战操作

2.1 HPA v2 配置(CPU + 自定义指标 + External)

完整示例:基于 CPU 和 Kafka 消费滞后双重指标。

yaml 复制代码
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: order-service-hpa
  namespace: prod
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: order-service
  minReplicas: 3
  maxReplicas: 50
  metrics:
    # CPU 指标(基础)
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 60
    # 自定义指标(Prometheus Adapter)
    - type: Pods
      pods:
        metric:
          name: http_requests_per_second
        target:
          type: AverageValue
          averageValue: "1000"  # 单 Pod 1000 QPS
    # External 指标(Kafka 消费滞后,KEDA 提供)
    - type: External
      external:
        metric:
          name: kafka_consumergroup_lag
          selector:
            matchLabels:
              topic: orders
              consumergroup: order-service
        target:
          type: AverageValue
          averageValue: "500"  # 单 Pod 处理 500 条滞后
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 0  # 扩容立即动作
      selectPolicy: Max
      policies:
        - type: Percent
          value: 100
          periodSeconds: 30  # 30s 内最多翻倍
        - type: Pods
          value: 10
          periodSeconds: 30  # 或一次加 10 个
    scaleDown:
      stabilizationWindowSeconds: 300  # 缩容观察 5min
      selectPolicy: Min
      policies:
        - type: Percent
          value: 10
          periodSeconds: 60  # 每分钟最多缩 10%

关键点 :selectPolicy: Max 取多个 policy 中扩容最激进的,Min 取缩容最保守的。

2.2 部署 Metrics Server + Prometheus Adapter

bash 复制代码
# 1. 部署 Metrics Server(HPA 基础)
helm install metrics-server metrics-server/metrics-server \
  --namespace kube-system \
  --set args="{--kubelet-insecure-tls,--kubelet-preferred-address-types=InternalIP}"

# 验证
kubectl top nodes
kubectl top pods -n prod

# 2. 部署 Prometheus Adapter(自定义指标)
cat > prom-adapter-values.yaml <<'EOF'
rules:
  default: false
  custom:
    - seriesQuery: 'http_requests_total{namespace!="",pod!=""}'
      resources:
        overrides:
          namespace: {resource: "namespace"}
          pod: {resource: "pod"}
      name:
        matches: "^(.*)_total"
        as: "${1}_per_second"
      metricsQuery: 'sum(rate(<<.Series>>{<<.LabelMatchers>>}[2m])) by (<<.GroupBy>>)'
prometheus:
  url: http://prometheus.monitoring.svc.cluster.local:9090
EOF

helm install prom-adapter prometheus-community/prometheus-adapter \
  --namespace monitoring -f prom-adapter-values.yaml

# 验证自定义指标
kubectl get --raw "/apis/custom.metrics.k8s.io/v1beta1/namespaces/prod/pods/*/http_requests_per_second"

2.3 VPA 部署与使用

bash 复制代码
# 部署 VPA
git clone https://github.com/kubernetes/autoscaler.git
cd autoscaler/vertical-pod-autoscaler
./vpa-up.sh

# 创建 VPA 资源(仅推荐模式,生产安全)
cat > vpa-order-service.yaml <<'EOF'
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: order-service-vpa
  namespace: prod
spec:
  targetRef:
    apiVersion: "apps/v1"
    kind: Deployment
    name: order-service
  updatePolicy:
    updateMode: "Initial"  # 生产推荐 Initial,auto 谨慎
  resourcePolicy:
    containerPolicies:
      - containerName: '*'
        minReplicas: 3
        controlledResources: ["cpu", "memory"]
        controlledValues: RequestsAndLimits
        minAllowed:
          cpu: 100m
          memory: 128Mi
        maxAllowed:
          cpu: 4
          memory: 4Gi
EOF

kubectl apply -f vpa-order-service.yaml

# 查看推荐
kubectl describe vpa order-service-vpa -n prod

输出示例:

复制代码
Recommendation:
  Container Recommendations:
    Container Name: order-service
    Lower Bound:
      Cpu:     250m
      Memory:  262144k
    Target:
      Cpu:     500m
      Memory:  524288k
    Uncapped Target:
      Cpu:     500m
      Memory:  524288k
    Upper Bound:
      Cpu:     800m
      Memory:  1048576k

2.4 Cluster Autoscaler 部署(以 AWS 为例)

bash 复制代码
# 部署 CA
helm install cluster-autoscaler autoscaler/cluster-autoscaler \
  --namespace kube-system \
  --set autoDiscovery.clusterName=my-cluster \
  --set awsRegion=us-east-1 \
  --set extraArgs.scan-interval=10s \
  --set extraArgs.scale-down-delay-after-add=10m \
  --set extraArgs.scale-down-unneeded-time=30m \
  --set extraArgs.scale-down-utilization-threshold=0.5 \
  --set extraArgs.expander=priority \
  --set extraArgs.balance-similar-node-groups=true \
  --set extraArgs.skip-nodes-with-local-storage=false \
  --set extraArgs.skip-nodes-with-system-pods=true

# Priority expander 配置(优先 Spot)
cat > priority-expander.yaml <<'EOF'
apiVersion: v1
kind: ConfigMap
metadata:
  name: cluster-autoscaler-priority-expander
  namespace: kube-system
data:
  priorities: |-
    10:
      - .*-spot-.*
    50:
      - .*-on-demand-.*
EOF
kubectl apply -f priority-expander.yaml

节点组配置建议(以 AWS ASG 为例):

yaml 复制代码
# Spot 节点组(便宜,优先用)
- name: prod-spot
  instanceType:
    - c5.large
    - c5.xlarge
    - m5.large
  desiredSize: 3
  minSize: 0
  maxSize: 50
  labels:
    node-role: spot
    capacity-type: spot
  taints:
    - key: spot
      value: "true"
      effect: NoSchedule
# On-demand 节点组(兜底,贵但稳定)
- name: prod-ondemand
  instanceType:
    - c5.large
  desiredSize: 3
  minSize: 3
  maxSize: 20
  labels:
    node-role: ondemand

业务 Pod 配置 Spot 容忍:

yaml 复制代码
spec:
  template:
    spec:
      tolerations:
        - key: spot
          operator: Equal
          value: "true"
          effect: NoSchedule
      affinity:
        nodeAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              preference:
                matchExpressions:
                  - key: capacity-type
                    operator: In
                    values:
                      - spot

2.5 KEDA 部署与 ScaledObject

bash 复制代码
helm install keda kedacore/keda --namespace keda-system --create-namespace --version 2.13.0

基于 Kafka 消费滞后的 ScaledObject:

yaml 复制代码
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: order-consumer-scaler
  namespace: prod
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: order-consumer
  pollingInterval: 30          # 30s 检查一次外部指标
  cooldownPeriod: 300          # 缩到 0 前等 5min
  minReplicaCount: 1           # 保留 1 个,避免冷启动
  maxReplicaCount: 50
  idleReplicaCount: 0          # 空闲时缩到 0(激进)
  advanced:
    horizontalPodAutoscalerConfig:
      name: order-consumer-hpa  # KEDA 自动创建 HPA
      behavior:
        scaleDown:
          stabilizationWindowSeconds: 300
  triggers:
    - type: kafka
      metadata:
        bootstrapServers: kafka-broker.prod:9092
        consumerGroup: order-consumer
        topic: orders
        lagThreshold: "1000"     # 单 Pod 处理 1000 滞后
        offsetResetPolicy: latest
        partitionLimitation: "0,1,2,3"
    - type: prometheus
      metadata:
        serverAddress: http://prometheus.monitoring.svc:9090
        metricName: http_request_duration_p99
        threshold: "500"
        query: histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket{namespace="prod"}[5m])) by (le))

2.6 多级弹性组合方案

生产推荐组合:KEDA(事件驱动) + HPA(资源指标) + CA(节点扩容) + VPA(资源推荐)

yaml 复制代码
# 业务 Deployment:带 readinessGate 和 PodDisruptionBudget
apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
  namespace: prod
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 25%
      maxUnavailable: 0  # 业务关键,不允许不可用
  template:
    spec:
      terminationGracePeriodSeconds: 60
      containers:
        - name: order-service
          image: registry.example.com/order-service:v1.30
          resources:
            requests:
              cpu: 500m      # VPA 推荐后定期调整
              memory: 512Mi
            limits:
              cpu: 1000m
              memory: 1Gi
          readinessProbe:
            httpGet:
              path: /healthz/ready
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 5
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: order-service-pdb
  namespace: prod
spec:
  minAvailable: 2  # 始终保留 2 个 Pod 可用
  selector:
    matchLabels:
      app: order-service

2.7 冷启动优化清单

冷启动慢的根因有两个:镜像拉取 + 应用初始化。优化手段:

bash 复制代码
# 1. 镜像预热(节点扩容后立即拉)
cat > image-puller-ds.yaml <<'EOF'
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: image-puller
  namespace: kube-system
spec:
  selector:
    matchLabels:
      app: image-puller
  template:
    metadata:
      labels:
        app: image-puller
    spec:
      tolerations:
        - operator: Exists  # 所有节点都跑
      initContainers:
        - name: pull-image-1
          image: registry.example.com/order-service:v1.30
          command: ["true"]
        - name: pull-image-2
          image: registry.example.com/sidecar:v1.0
          command: ["true"]
      containers:
        - name: pause
          image: registry.k8s.io/pause:3.9
          resources:
            requests:
              cpu: 10m
              memory: 16Mi
EOF
yaml 复制代码
# 2. 用 preStop 给应用预热
spec:
  containers:
    - name: app
      lifecycle:
        postStart:
          exec:
            command:
              - /bin/sh
              - -c
              - "curl -s http://localhost:8080/warmup"  # 触发 JIT / 缓存加载
bash 复制代码
# 3. Proportional resizing(CA 配合)------优先在已有节点上调度
# CA 不会主动预留节点,所以提前扩容:
# 用 KEDA Cron scaler 在 8:00 提前扩容,应对 9:00 流量高峰
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: morning-peak-prewarm
spec:
  scaleTargetRef:
    name: order-service
  minReplicaCount: 3
  maxReplicaCount: 50
  triggers:
    - type: cron
      metadata:
        timezone: Asia/Shanghai
        start: "0 8 * * *"
        end: "0 10 * * *"
        desiredReplicas: "20"  # 8-10 点保持 20 副本

三、踩坑与排查

坑 1:HPA 一直显示 Targets<unknown>

现象 :kubectl describe hpa 看到 RESOURCE <unknown>

定位:

bash 复制代码
# 看 metrics-server 是否有数据
kubectl top pods -n prod
# 没有数据 → metrics-server 问题

# 看 HPA 控制器日志
kubectl -n kube-system logs -l app=metrics-server --tail=100
# 常见错误:E1234 ... "node hasn't reported metrics"

# 看 Pod 是否有 resource requests
kubectl get pod order-service-xxx -n prod -o yaml | grep -A5 resources
# HPA 必须基于 requests 计算 CPU 利用率,没 requests 就算不出

解决 :Deployment 必须配 resources.requests.cpu,且 metrics-server 能正常采集。--kubelet-preferred-address-types=InternalIP 解决节点名解析问题。

坑 2:HPA 抖动,扩缩容频繁

现象:副本数在 5-15 之间反复跳,Pod 频繁重启。

定位 :kubectl describe hpaEvents,频繁的 Succeeded rescale

原因 :tolerance 默认 0.1,指标在 50%×0.9=45% 到 50%×1.1=55% 之外就动作;scaleDown.stabilizationSeconds 默认 300s,但流量本身就在抖动。

解决:

yaml 复制代码
behavior:
  scaleDown:
    stabilizationWindowSeconds: 600  # 拉长到 10min
    policies:
      - type: Percent
        value: 10
        periodSeconds: 120  # 每 2min 最多缩 10%

业务侧给指标加 2m 滑动平均(rate(http_requests[2m])),减少抖动。

坑 3:VPA auto 模式把生产 Pod 反复重建

现象 :开启 VPA auto 模式后,Pod 每 10 分钟被杀一次,业务报错。

原因:VPA 推荐值一直在变,触发 Evict 重建。

解决:

  • 生产用 Initial 模式,只对新创建的 Pod 应用,不重建存量 Pod
  • 必须用 auto 时,配 PodDisruptionBudget + updateMode: Initial 兜底
  • VPA 不建议用于关键在线服务,适合批处理 / Job

坑 4:Cluster Autoscaler 缩容把在线业务 Pod 驱逐

现象:节点缩容时把核心业务 Pod 驱逐,流量抖动。

定位 :看 CA 日志 kubectl -n kube-system logs deploy/cluster-autoscaler | grep "scale-down"

解决:

yaml 复制代码
# 1. 关键业务配 PDB,minAvailable 保证
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: critical-pdb
spec:
  minAvailable: 3
  selector:
    matchLabels:
      tier: critical

# 2. 节点加 annotation 阻止缩容(临时)
kubectl annotate node node-1 cluster-autoscaler.kubernetes.io/scale-down-disabled=true

# 3. CA 配置不缩容带 critical pod 的节点
--skip-nodes-with-system-pods=true

坑 5:KEDA minReplicaCount=0 后无法激活

现象:Kafka 来消息了,但 Pod 还是 0。

定位:看 KEDA Operator 日志。

原因 :KEDA 的 pollingInterval=30s,且 HPA 的 behavior.scaleDown.stabilizationWindowSeconds 默认 300s,需要等 5min 才能从 0 启动。

解决:

yaml 复制代码
spec:
  pollingInterval: 15  # 加快检查
  cooldownPeriod: 60   # 缩到 0 前 60s 即可
  idleReplicaCount: 0
  minReplicaCount: 1   # 关键业务保留 1 个,避免冷启动
  advanced:
    horizontalPodAutoscalerConfig:
      behavior:
        scaleUp:
          stabilizationWindowSeconds: 0  # 从 0 扩容立即动作
          policies:
            - type: Percent
              value: 9000  # 0 → 1 没意义,允许快速扩到目标值
              periodSeconds: 15

坑 6:节点扩容后 Pod 调度不上去

现象:CA 触发扩容,新节点加入了,但 Pending Pod 还是 Pending。

定位:

bash 复制代码
kubectl describe pod pending-pod | grep -A20 Events
# 常见原因:
# 1. nodeSelector / affinity 不匹配新节点 label
# 2. taint 没配 toleration
# 3. 资源不够(新节点规格太小)

解决 :节点组配置 cluster-autoscaler.kubernetes.io/safe-to-evict=false 不行,根因是 label/taint 不匹配。确保 ASG 模板的节点 label 和业务 affinity 对齐。

四、最佳实践

HPA:

  • 业务关键路径同时用 Resource(CPU) + External(队列/QPS)指标
  • scaleUp.stabilizationWindowSeconds=0(扩容立即)
  • scaleDown.stabilizationWindowSeconds=300~600(缩容观察 5-10min)
  • maxReplicas 上限 = 业务峰值 × 1.5
  • 不用 CPU 利用率单独做指标,延迟大,加 QPS / 队列长度
  • 推荐用 prometheus-adapter 暴露自定义指标

VPA:

  • 生产用 Initial 模式,慎用 auto
  • 不与 HPA 共用同一指标(CPU/内存)
  • 适合批处理 / Job / 后台任务
  • 每周看一次 VPA 推荐,人工调整 requests(半自动)
  • 容器设 minAllowed / maxAllowed 防止异常推荐

Cluster Autoscaler:

  • scan-interval=10s(默认即可)
  • scale-down-delay-after-add=10m(扩容后 10min 内不缩容)
  • scale-down-unneeded-time=30m(节点低利用率 30min 才缩)
  • expander=priority,Spot 优先
  • 关键业务配 PDB,节点组按业务分
  • Spot + On-demand 双节点组,Spot 兜底用 On-demand

KEDA:

  • 关键业务 minReplicaCount=1,避免冷启动
  • 非实时业务 minReplicaCount=0,省成本
  • pollingInterval 不超过 30s
  • ScaledObject 不用 ScaledJob(除非是 Job)
  • 多 trigger 用 OR 关系,任一满足就扩容

多级组合:

  • 在线服务:HPA(CPU+QPS) + CA(Spot+On-demand) + VPA(Initial 调研)
  • 消费者:KEDA(队列长度) + CA + VPA(Initial)
  • 批处理:KEDA ScaledJob + CA
  • 突发流量:Cron 预热 + HPA + CA,提前 30-60min 扩

冷启动优化:

  • 镜像预热:DaemonSet image-puller 在所有节点预先拉镜像
  • 镜像分层:基础层稳定,应用层小
  • Pod postStart 触发预热
  • readinessProbe.initialDelaySeconds 不要太大
  • 业务侧实现 /warmup 接口,Pod 启动后调用

五、小结

弹性伸缩不是"配一个 HPA 就完了",而是要分清四层职责:

  • KEDA:外部事件驱动(队列、Cron、外部指标)------决定何时扩
  • HPA:Pod 内部指标驱动(CPU、QPS)------决定扩多少
  • VPA:Pod 资源配额推荐------决定单个 Pod 多大
  • CA:节点扩容------决定基础设施够不够

四者协同的要点:指标别重叠、滞后要预估、缩容要保守、扩容要激进。下一篇我们讲成本治理------把这套弹性用起来后,集群账单怎么降下来。

思考题

  1. HPA 的 behavior.scaleUp.stabilizationWindowSeconds=0 会不会导致扩容过度?什么时候该设 30s?
  2. VPA Initial 模式下,老 Pod 的资源配额永远不会变,这种"半自动"方案怎么落地到生产流程?
  3. KEDA minReplicaCount=0 + CA 节点缩容到 0,这种"完全 serverless"在 K8s 上有什么坑?
  4. 估算一下:流量从 1000 QPS 突增到 5000 QPS,你的服务(单 Pod 500 QPS)从触发到完全就绪要多久?哪些环节可以压缩?

延伸阅读

相关推荐
GlobalHRTalk1 小时前
埃及名义雇主EOR业务概述与市场发展优势分析
大数据·运维·人工智能
Zeeland1 小时前
Agent 能完成一个任务,但它能持续追一个三个月的目标吗?
人工智能·github·openai
瞬间&永恒~1 小时前
【MySQL】 主从复制多拓扑搭建实验
运维·数据库·mysql·云原生
张忠琳2 小时前
【NVIDIA】k8s-device-plugin v0.19.3 标签管理模块 (internal/lm) 深度分析之六
云原生·容器·架构·kubernetes·nvidia
小匠石钧知3 小时前
02_在多个RockyLinux10虚拟机上安装k8s集群
云原生·容器·kubernetes·k8s
星野爱8953 小时前
远程控制哪家安全性更高?ToDesk、UU远程、向日葵隐私屏深度测评!
linux·运维·网络
ALINX技术博客3 小时前
【黑金云课堂】FPGA技术教程Linux开发:系统定制
linux·运维·fpga开发
明航咨询-程老师4 小时前
增值电信业务经营许可证信息整理
github
逛逛GitHub4 小时前
找到 4 个花里胡哨的 GitHub 开源项目,推荐给你。
github