K8s HPA扩缩容震荡复盘:CPU阈值配错后,服务在凌晨被扩缩了47次
K8s HPA扩缩容震荡复盘:CPU阈值配错后,服务在凌晨被扩缩了47次
凌晨3点被告警电话叫醒,打开监控面板差点没背过气去:Deployment的Pod数量在10分钟里从3个飙到47个,又缩回3个,来回震荡了5轮。CPU使用率曲线像心电图,Pod重建次数直接刷屏。
事情的罪魁祸首,居然是HPA里两个阈值的配法。
背景:一个看似正常的HPA配置
我们有一个核心的订单处理服务,平时3个Pod就能扛住流量。为了应对早晚高峰,我给它配了HPA(Horizontal Pod Autoscaler),目标是在CPU使用率超过70%时自动扩容,低于30%时缩容。
当时的配置看起来挺合理的:
apiVersion:autoscaling/v2kind:HorizontalPodAutoscalermetadata:name:order-service-hpaspec:scaleTargetRef:apiVersion:apps/v1kind:Deploymentname:order-serviceminReplicas:3maxReplicas:50metrics:-type:Resourceresource:name:cputarget:type:UtilizationaverageUtilization:70behavior:scaleDown:stabilizationWindowSeconds:60policies:-type:Percentvalue:50periodSeconds:60scaleUp:stabilizationWindowSeconds:0policies:-type:Percentvalue:100periodSeconds:15问题出在哪?
扩容和缩容的阈值差距太大了。70%扩,30%缩,中间差了40个百分点。当Pod从3个扩容到6个后,CPU使用率会立刻掉一半(如果负载没变的话),直接从70%掉到35%左右。然后HPA一看:35%低于30%?不,还没到。但如果负载稍微波动一下,掉到28%,缩容触发,Pod从6个缩回3个。CPU使用率又飙回70%,扩容再次触发。
就这样,我们在凌晨3点见证了一场完美的"扩缩容震荡"。
排查时间线:20分钟定位根因
3:12 告警触发
Prometheus告警规则:kube_deployment_status_replicas_unavailable > 0连续3分钟触发。同时收到k8s_hpa_desired_replicas > 20的告警。
打开Grafana,看到 Pod 数量曲线:
3:05 → 3 replicas 3:07 → 6 replicas 3:08 → 12 replicas 3:09 → 24 replicas 3:10 → 47 replicas(达到maxReplicas上限) 3:11 → 24 replicas 3:12 → 12 replicas ...(震荡继续)3:15 确认不是流量攻击
先排查是不是突发流量。看 ingress-nginx 的 QPS 面板,凌晨3点流量平稳,比平时还低。排除流量突增导致。
3:18 定位HPA行为异常
执行kubectl describe hpa order-service-hpa,看到事件日志刷屏:
Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal SuccessfulRescale 10s horizontal-pod-autoscaler New size: 6; reason: cpu resource utilization (percentage) above target Normal SuccessfulRescale 15s horizontal-pod-autoscaler New size: 12; reason: cpu resource utilization (percentage) above target Normal SuccessfulRescale 20s horizontal-pod-autoscaler New size: 24; reason: cpu resource utilization (percentage) above target Normal SuccessfulRescale 30s horizontal-pod-autoscaler New size: 3; reason: All metrics below target Normal SuccessfulRescale 35s horizontal-pod-autoscaler New size: 6; reason: cpu resource utilization (percentage) above targetAll metrics below target和above target交替出现,间隔只有5秒。这就是典型的扩缩容震荡。
3:22 检查HPA配置,发现阈值问题
用kubectl get hpa order-service-hpa -o yaml导出配置,一眼看到问题:
averageUtilization:70# 扩容阈值缩容默认行为没有显式配置目标值,但默认是50%?不,K8s HPA的缩容并没有单独的阈值。它用的是同一个目标值:70%。也就是说,当使用率低于70%时,理论上都可能触发缩容。但HPA的缩容有冷却窗口(stabilizationWindowSeconds),默认是5分钟。
等等,我配置了scaleDown.stabilizationWindowSeconds: 60,只有1分钟。这意味着扩容后只要CPU在1分钟内低于目标值,就会缩容。而CPU在扩容后通常会骤降,加上 Pod 启动和就绪探针的延迟,实际使用率计算时会出现短暂低于目标的情况。
根本原因是:扩缩容阈值重叠 + 缩容冷却窗口太短 + 没有设置缩容的 targetUtilization 下限。
修复方案:三层加固
第一层:调整目标值,增加缓冲区
核心修复:把HPA的CPU目标从70%改为85%,同时引入自定义指标(请求延迟)作为辅助判断,避免单一指标导致震荡。
apiVersion:autoscaling/v2kind:HorizontalPodAutoscalermetadata:name:order-service-hpaspec:scaleTargetRef:apiVersion:apps/v1kind:Deploymentname:order-serviceminReplicas:3maxReplicas:50metrics:-type:Resourceresource:name:cputarget:type:UtilizationaverageUtilization:85# 提高扩容阈值,给缓冲区-type:Podspods:metric:name:http_request_duration_seconds_p99target:type:AverageValueaverageValue:"500m"# P99延迟超过500ms才扩容behavior:scaleDown:stabilizationWindowSeconds:300# 缩容冷却窗口延长到5分钟policies:-type:Percentvalue:10# 每次缩容最多10%periodSeconds:60scaleUp:stabilizationWindowSeconds:0policies:-type:Percentvalue:100periodSeconds:15-type:Podsvalue:4periodSeconds:15selectPolicy:Max关键改动:
- CPU目标从70%提到85%:减少扩容的敏感度,避免轻微波动就触发扩容。
- 缩容冷却窗口从60秒延长到300秒:确保扩容后的稳定期足够长,让Pod充分承载负载。
- 缩容策略改为每次最多10%:避免一次性缩回太多Pod,导致CPU反弹。
- 引入P99延迟作为辅助指标:CPU可能受JVM GC、定时任务等影响波动,但延迟更能反映真实服务质量。
第二层:给Pod加上启动期和就绪期保护
Pod刚启动时CPU使用率会短暂飙升(JVM预热、连接池初始化),这会导致HPA误判。加上startupProbe和适当的initialDelaySeconds:
apiVersion:apps/v1kind:Deploymentmetadata:name:order-servicespec:template:spec:containers:-name:order-serviceimage:order-service:v2.3.1resources:requests:cpu:"500m"memory:"1Gi"limits:cpu:"1000m"memory:"2Gi"startupProbe:httpGet:path:/actuator/healthport:8080initialDelaySeconds:10periodSeconds:5failureThreshold:12# 最多60秒启动时间readinessProbe:httpGet:path:/actuator/health/readinessport:8080initialDelaySeconds:5periodSeconds:5failureThreshold:3livenessProbe:httpGet:path:/actuator/health/livenessport:8080initialDelaySeconds:30periodSeconds:10failureThreshold:3划重点:HPA计算CPU使用率时,只统计Ready状态的Pod。如果Pod还在Startup阶段,不会被纳入计算。所以合理的启动探针能避免"未就绪Pod拖累平均值"的问题。
第三层:监控告警和应急预案
加了两个Prometheus告警规则,专门监控HPA震荡:
-alert:HPAFlappingDetectedexpr:|( count by (namespace, horizontalpodautoscaler) ( kube_horizontalpodautoscaler_status_condition{status="True", condition="ScalingLimited"} == 1 ) > 0 and count by (namespace, horizontalpodautoscaler) ( changes(kube_horizontalpodautoscaler_status_current_replicas[10m]) > 5 ) > 0 )for:2mlabels:severity:criticalannotations:summary:"HPA {{ $labels.horizontalpodautoscaler }} 震荡检测"description:"10分钟内副本数变化超过5次,疑似扩缩容震荡"-alert:HPAScaleToMaxexpr:|kube_horizontalpodautoscaler_status_current_replicas / kube_horizontalpodautoscaler_spec_max_replicas == 1for:5mlabels:severity:warningannotations:summary:"HPA {{ $labels.horizontalpodautoscaler }} 已达到最大副本数"description:"当前副本数等于maxReplicas,可能需要扩容上限或排查异常"应急预案:如果HPA震荡发生,快速执行kubectl scale deployment order-service --replicas=5固定副本数,然后手动调整HPA配置。
踩坑记录:HPA的4个血泪教训
1. 不要用默认的scaleDown配置
K8s HPA的默认缩容冷却窗口是5分钟(300秒),但默认缩容策略是每分钟缩容当前副本数的100%——这意味着只要条件满足,它可以在1分钟内把所有Pod都缩掉。生产环境务必显式配置behavior.scaleDown。
2. CPU目标值不是越低越好
很多人觉得CPU使用率超过50%就该扩容,但其实要考虑服务的负载特征。如果服务有定期任务(比如每5分钟跑一次统计),CPU会周期性波动。目标值设太低,这些正常波动也会触发扩容。对于Web服务,CPU目标在70%-85%通常比较合理。
3. HPA的CPU计算包含所有Ready Pod,但metric-server采样有延迟
metric-server默认每15秒采集一次CPU使用率,而HPA每15秒评估一次。如果Pod刚启动,CPU采样可能还没稳定。加上启动探针和适当的initialDelay,能避免"新Pod拉高平均值"或"新Pod还没数据被跳过"的问题。
4. 单一指标很容易翻车
只依赖CPU的HPA,在遇到GC、定时任务、批处理作业等场景时很容易误判。建议至少搭配一个业务指标(比如QPS、延迟、队列深度)或者自定义的external metrics。
写在最后
这次凌晨3点的故障,表面上是HPA配置的问题,本质上是对自动扩缩容的理解还不够深。K8s HPA看似简单,但target utilization、stabilization window、behavior policies这几个参数组合起来,能在生产环境玩出各种花样。
修复后监控了一周,HPA行为稳定。早高峰从3个Pod扩容到8个,耗时约90秒;晚高峰结束后,缩容到5个Pod(不是直接缩回3个),然后平稳过渡到3个。再也没有出现过震荡。
如果你也在用HPA,建议检查一下:
- 你的缩容冷却窗口是不是太短了?
- 你的CPU目标值有没有给服务留够缓冲区?
- 有没有只靠CPU一个指标在做扩缩容决策?
希望这篇复盘能帮你少走点弯路。有问题欢迎评论区交流。
