资源优化:HPA/VPA/Cluster Autoscaler
一句话定位:让集群容量跟着流量走,而不是按峰值静态预留------本文讲清楚四种弹性组件怎么组合、怎么踩坑、怎么估滞后。
写在前面
线上跑了一年的集群,大多数资源利用率在 20-30%,但每到促销就要扩容。同学来问:"能不能让集群自己跟着流量长?"答案能,但要看清四件事:HPA(Pod 数量伸缩)、VPA(Pod 资源配额伸缩)、Cluster Autoscaler(节点伸缩)、KEDA(事件驱动伸缩)。这四个组件单独用都有明显短板,组合用才是生产方案。
这一篇我把生产里跑了三年的弹性方案整理出来,从算法原理 → 配置 → 踩坑 → 多级组合,最后给一份"伸缩滞后评估表",让你能预估业务流量翻 5 倍时到底要等多久。
核心问题
怎么让集群容量跟着流量走?
这个问题拆成三层:
- Pod 层弹性:同一个 Deployment 副本数怎么变?(HPA / KEDA)
- Pod 内部弹性:单个 Pod 的 CPU/Memory 怎么调?(VPA)
- 节点层弹性:节点不够了怎么扩?(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 的两个痛点:
- 冷启动 :KEDA 支持
minReplicaCount=0,没流量时缩到 0,来流量时激活 - 事件驱动:不用 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 hpa 看 Events,频繁的 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:节点扩容------决定基础设施够不够
四者协同的要点:指标别重叠、滞后要预估、缩容要保守、扩容要激进。下一篇我们讲成本治理------把这套弹性用起来后,集群账单怎么降下来。
思考题
- HPA 的
behavior.scaleUp.stabilizationWindowSeconds=0会不会导致扩容过度?什么时候该设 30s? - VPA
Initial模式下,老 Pod 的资源配额永远不会变,这种"半自动"方案怎么落地到生产流程? - KEDA
minReplicaCount=0+ CA 节点缩容到 0,这种"完全 serverless"在 K8s 上有什么坑? - 估算一下:流量从 1000 QPS 突增到 5000 QPS,你的服务(单 Pod 500 QPS)从触发到完全就绪要多久?哪些环节可以压缩?
延伸阅读
- HPA v2 官方文档: https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/
- KEDA 文档: https://keda.sh/docs/2.13/concepts/
- Cluster Autoscaler FAQ: https://github.com/kubernetes/autoscaler/blob/master/cluster-autoscaler/FAQ.md
- VPA 设计文档: https://github.com/kubernetes/autoscaler/tree/master/vertical-pod-autoscaler
- Prometheus Adapter 配置: https://github.com/kubernetes-sigs/prometheus-adapter
- 阿里云 KEDA 实践: https://help.aliyun.com/document_detail/206313.html