Kubernetes HPA 自动扩缩容实战——基于 CPU/自定义指标的弹性伸缩
Kubernetes HPA 自动扩缩容实战——基于 CPU/自定义指标的弹性伸缩
适读人群:需要在 K8s 上做弹性伸缩的工程师 | 阅读时长:约 14 分钟 | 核心价值:从 HPA 原理到自定义指标,彻底搞懂弹性伸缩的正确配置
双十一前两周,我们的电商平台开始做压测。压测结果把我吓了一跳:按照预期流量建模,高峰期需要 42 个 Pod 来支撑,但平时只需要 8 个。如果按峰值配置固定副本,成本浪费极大;如果不扩容,高峰期必崩。
答案当然是 HPA(Horizontal Pod Autoscaler)。但 HPA 配置不好,同样会出问题——我们第一版配置就踩了两个大坑,这篇文章把坑都写出来。
HPA 的工作原理
HPA 控制器定期(默认 15s)从 metrics-server 获取 Pod 的指标,根据目标值计算期望副本数:
desiredReplicas = ceil(currentReplicas × (currentMetricValue / desiredMetricValue))举个例子:当前 4 个 Pod,平均 CPU 使用 80%,目标是 50%:
desiredReplicas = ceil(4 × (80% / 50%)) = ceil(6.4) = 7HPA 会把 Deployment 扩到 7 个 Pod。
前置条件:安装 metrics-server
HPA 依赖 metrics-server 提供 Pod 和 Node 的资源指标。确认已安装:
kubectl get apiservice v1beta1.metrics.k8s.io
kubectl top pod -n default如果 top pod 报错,需要先安装 metrics-server:
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
# 如果集群是自建的,kubelet 没有 TLS 证书,需要加参数
kubectl patch deployment metrics-server -n kube-system --type='json' \
-p='[{"op": "add", "path": "/spec/template/spec/containers/0/args/-", "value": "--kubelet-insecure-tls"}]'基础 HPA:基于 CPU 自动扩缩
先确保 Pod 有 resources.requests.cpu 设置,否则 HPA 无法计算:
# Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-api
namespace: production
spec:
replicas: 4
selector:
matchLabels:
app: web-api
template:
spec:
containers:
- name: web-api
image: web-api:v1.5.2
resources:
requests:
cpu: "500m"
memory: "256Mi"
limits:
memory: "512Mi"对应的 HPA(v2 版本,K8s 1.23+):
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: web-api-hpa
namespace: production
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: web-api
minReplicas: 4 # 最少 4 个(保证基础可用性)
maxReplicas: 50 # 最多 50 个(控制成本上限)
metrics:
# CPU 使用率指标
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60 # 平均 CPU 使用率目标 60%
# 内存使用率(可选)
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
# 扩缩容行为配置
behavior:
scaleUp:
stabilizationWindowSeconds: 60 # 扩容稳定窗口:60s 内取最大值
policies:
- type: Pods
value: 4 # 每次最多扩 4 个 Pod
periodSeconds: 30
- type: Percent
value: 100 # 每次最多扩 100%
periodSeconds: 60
selectPolicy: Max # 取两个 policy 的较大值
scaleDown:
stabilizationWindowSeconds: 300 # 缩容稳定窗口:5 分钟(防止频繁抖动)
policies:
- type: Pods
value: 2 # 每次最多缩 2 个
periodSeconds: 60踩坑实录一:HPA 扩容后立刻缩容,抖动严重
现象:流量突然增加,HPA 触发扩容到 12 个 Pod,流量高峰持续了 3 分钟,然后 HPA 又把 Pod 缩回去,下一波流量来了又扩……整个下午 Pod 数量像心电图一样抖动,每次缩容和扩容都有短暂的延迟,体验很差。
原因:默认的缩容稳定窗口太短(K8s 1.17 之前默认 5 分钟,但参数可能被改过),导致稍微平稳一下就缩了。
解法:调整缩容行为,延长稳定窗口:
behavior:
scaleDown:
stabilizationWindowSeconds: 600 # 10 分钟内流量都在高位,才缩容
policies:
- type: Percent
value: 20 # 每次最多缩 20%
periodSeconds: 120 # 两分钟一次这样即使流量有短暂下降,10 分钟内都不会缩容,避免了抖动。
自定义指标 HPA:基于 QPS/请求队列深度
CPU 使用率并不总是最好的扩缩容指标。比如:
- 异步任务服务:CPU 不高,但消息队列积压了 10000 条,应该扩
- 批处理服务:CPU 使用率反映的是当前负载,但请求队列深度才能预测未来负载
自定义指标需要 Prometheus Adapter 把 Prometheus 指标转换成 K8s custom metrics API。
安装 Prometheus Adapter:
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm install prometheus-adapter prometheus-community/prometheus-adapter \
-n monitoring \
--set prometheus.url=http://prometheus-server.monitoring.svc.cluster.local配置自定义指标规则(ConfigMap):
apiVersion: v1
kind: ConfigMap
metadata:
name: prometheus-adapter-config
namespace: monitoring
data:
config.yaml: |
rules:
# 将 Prometheus 的 QPS 指标暴露给 HPA
- seriesQuery: 'http_requests_total{namespace!="",pod!=""}'
resources:
overrides:
namespace: {resource: "namespace"}
pod: {resource: "pod"}
name:
matches: "http_requests_total"
as: "http_requests_per_second"
metricsQuery: 'sum(rate(http_requests_total{<<.LabelMatchers>>}[2m])) by (<<.GroupBy>>)'基于 QPS 的 HPA:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: web-api-hpa-qps
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: web-api
minReplicas: 4
maxReplicas: 50
metrics:
# 自定义指标:每个 Pod 的 QPS 目标 200 req/s
- type: Pods
pods:
metric:
name: http_requests_per_second
target:
type: AverageValue
averageValue: "200"基于 Kafka 消息队列深度的 HPA
这是我用得最多的自定义指标场景。消费者服务需要根据 Kafka 积压量来扩容:
# 使用 KEDA(Kubernetes Event Driven Autoscaling)更方便
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: kafka-consumer-scaler
spec:
scaleTargetRef:
name: kafka-consumer
minReplicaCount: 2
maxReplicaCount: 30
pollingInterval: 15 # 每 15s 检查一次
cooldownPeriod: 120 # 缩容冷却 120s
triggers:
- type: kafka
metadata:
bootstrapServers: kafka-service:9092
consumerGroup: order-processor-group
topic: order-events
lagThreshold: "100" # 每个 Pod 处理 100 条积压时触发扩容
offsetResetPolicy: latestKEDA 是比原生 HPA + Prometheus Adapter 更简洁的自定义指标扩缩容方案,强烈推荐用于事件驱动的服务。
踩坑实录二:HPA 和 KEDA 冲突
现象:同一个 Deployment 同时配了 HPA(CPU 指标)和 KEDA(Kafka 指标),两个控制器相互拉扯,副本数在 5 和 20 之间频繁跳变。
原因:HPA 和 KEDA 都在修改 Deployment 的 replicas,互相覆盖。
解法:不要在同一个 Deployment 上同时配 HPA 和 KEDA ScaledObject。KEDA 本身支持多种触发器(CPU + Kafka 可以在一个 ScaledObject 里配置):
triggers:
- type: cpu
metricType: Utilization
metadata:
value: "60"
- type: kafka
metadata:
bootstrapServers: kafka-service:9092
topic: order-events
lagThreshold: "100"踩坑实录三:Pod 启动慢导致 HPA 反复扩容
现象:扩容触发了,但新 Pod 还在 ContainerCreating 或者 readinessProbe 没通过,HPA 看到负载还是高,继续扩,最后扩到了 maxReplicas 上限。
原因:HPA 控制循环频率(默认 15s)比 Pod 启动时间(我们 Spring Boot 要 40s)快,新 Pod 还没 Ready 就被计入副本数但不分流量,HPA 看到的平均负载还是高的。
解法:
- 延长 HPA 的评估间隔(需要修改 kube-controller-manager 参数,生产慎用)
- 缩短应用启动时间(优化 Spring Boot 启动,用 lazyInit 等)
- 合理设置 behavior.scaleUp 的稳定窗口,避免在 Pod 启动完成前多次扩容:
behavior:
scaleUp:
stabilizationWindowSeconds: 60 # 扩容也要稳定 60s,不要太激进
policies:
- type: Pods
value: 3
periodSeconds: 60 # 每 60s 最多扩 3 个HPA 监控与调试
# 查看 HPA 当前状态
kubectl get hpa -n production
# NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
# web-api-hpa Deployment/web-api 58%/60% 4 50 8 7d
# 查看 HPA 详细事件
kubectl describe hpa web-api-hpa -n production
# 查看 HPA 的指标当前值
kubectl get --raw "/apis/metrics.k8s.io/v1beta1/namespaces/production/pods" | jq
# 查看自定义指标
kubectl get --raw "/apis/custom.metrics.k8s.io/v1beta1" | jqHPA 是 K8s 里为数不多的"配了就真的省心"的功能,但前提是把参数调对。不急于扩容、不频繁缩容、自定义指标选对——这三点把握好,弹性伸缩就能真正跑起来。
