8.3 K8s 生产运维:HPA 与 VPA 弹性伸缩
预计阅读时间:15 分钟
📖 目录
弹性伸缩是 Kubernetes 的核心优势之一。Horizontal Pod Autoscaler(HPA)根据 CPU/内存或自定义指标自动调整 Pod 副本数,Vertical Pod Autoscaler(VPA)调整 Pod 的资源请求和限制。
学习目标
- 理解 HPA 与 VPA 的工作原理、适用场景及限制条件
- 能够配置基于 CPU、内存和自定义指标的 HPA 策略
- 掌握 HPA 行为配置(behavior)实现精细化扩缩容控制
- 了解 VPA 的三种更新模式及其在生产环境中的使用策略
前置知识
- 熟悉 Kubernetes Pod、Deployment、ReplicaSet 的基本概念
- 了解 Kubernetes 资源请求(requests)与限制(limits)的含义
- 具备 Prometheus 指标采集与查询基础(用于自定义指标 HPA)
- 了解 kubectl top 命令及 Metrics Server 的作用
HPA 水平自动扩缩
工作原理
HPA 定期(默认 15 秒)从 Metrics Server 或 Prometheus 读取指标,计算目标副本数:desiredReplicas = ceil[currentReplicas * (currentMetricValue / desiredMetricValue)]
例如:当前 CPU 使用率 80%,目标 40%,当前 5 个 Pod,则 desiredReplicas = ceil[5 * (80/40)] = 10。HPA 会将副本数从 5 扩展到 10。
HPA 控制器的工作流程
- 每隔
--horizontal-pod-autoscaler-sync-period(默认 15 秒)拉取指标 - 根据 metrics 和当前副本数计算期望副本数
- 如果期望副本数与当前副本数不同,触发扩缩容操作
- 扩缩容操作通过调整 Deployment/ReplicaSet 的
spec.replicas实现
前提条件:安装 Metrics Server
# 安装 Metrics Server
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
# 如果使用自签名证书,需要添加 --kubelet-insecure-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"}
]'
# 验证
kubectl top nodes
kubectl top pods -n default
# 查看 Metrics Server 状态
kubectl get deployment metrics-server -n kube-system
1. 基于 CPU 的 HPA
# 创建 HPA(基于 CPU 使用率)
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: webapp-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: webapp
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70 # CPU 使用率目标 70%
2. 基于内存的 HPA
# 基于内存(注意:内存使用率不会随负载降低而下降)
# 适合缓存类应用
spec:
metrics:
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
3. 基于自定义指标(Prometheus)
需要 Prometheus Adapter 或 KEDA:
# KEDA 方式(推荐)
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: queue-worker-scaler
spec:
scaleTargetRef:
name: queue-worker
minReplicaCount: 1
maxReplicaCount: 20
triggers:
- type: prometheus
metadata:
serverAddress: http://prometheus.monitoring:9090
query: |
sum(rate(rabbitmq_queue_messages_total{queue="work"}[2m]))
threshold: "100"
# KEDA 还支持多种触发器类型
# - type: kafka (Kafka 消费者组 lag)
# - type: redis-streams (Redis Stream 长度)
# - type: cron (定时触发)
# - type: http (HTTP 请求速率)
4. 基于多项指标的 HPA
spec:
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Pods
pods:
metric:
name: http_requests_per_second
target:
type: AverageValue
averageValue: 1000
# 取所有指标计算结果的最大值
HPA 行为配置(1.23+)
spec:
behavior:
scaleDown:
stabilizationWindowSeconds: 300 # 缩容稳定窗口(5 分钟)
policies:
- type: Percent
value: 10
periodSeconds: 60 # 每分钟最多缩 10%
scaleUp:
stabilizationWindowSeconds: 0
policies:
- type: Pods
value: 4
periodSeconds: 60 # 每分钟最多扩 4 个 Pod
- type: Percent
value: 100
periodSeconds: 60
selectPolicy: Max # 取两个策略中的较大值
行为配置详解
| 字段 | 说明 | 推荐值 |
|---|---|---|
| stabilizationWindowSeconds | 扩缩容后的稳定窗口期,防止频繁抖动 | 扩容 0-60s,缩容 300s |
| policies[].type | Pods(固定数量)或 Percent(百分比) | 扩容用 Percent,缩容用 Pods |
| policies[].value | 每次允许变化的最大值 | 扩容 100%,缩容 10% |
| policies[].periodSeconds | 策略评估周期(秒) | 60s |
| selectPolicy | 多策略时的选择方式:Max(取大)、Min(取小)、Disabled | Max |
VPA 垂直自动扩缩
VPA 自动调整 Pod 的 CPU/内存 requests 和 limits。更适用于无法水平扩展的有状态应用(如数据库)。
# 安装 VPA(使用官方清单)
git clone https://github.com/kubernetes/autoscaler.git
kubectl apply -k autoscaler/vertical-pod-autoscaler/deploy/
# VPA 配置示例
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: mysql-vpa
spec:
targetRef:
apiVersion: apps/v1
kind: StatefulSet
name: mysql
updatePolicy:
updateMode: "Auto" # Auto/Initial/Off
resourcePolicy:
containerPolicies:
- containerName: "*"
minAllowed:
cpu: "250m"
memory: "512Mi"
maxAllowed:
cpu: "4"
memory: "16Gi"
VPA 更新模式
| 模式 | 行为 | 适用场景 |
|---|---|---|
| Off | 仅提供建议,不自动变更 | 先观察建议值 |
| Initial | 只在创建时应用建议 | 无状态应用 |
| Auto | 自动更新并重启 Pod | 可接受重启的应用 |
VPA 组件说明
- VPA Recommender:分析历史资源使用数据,给出资源建议值
- VPA Updater:根据建议值删除 Pod 以触发重新调度(仅 Auto 模式)
- VPA Admission Controller:在 Pod 创建时注入资源 requests(Auto/Initial 模式)
VPA 资源策略配置
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: my-app-vpa
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: my-app
updatePolicy:
updateMode: "Off" # 先用 Off 模式观察
resourcePolicy:
containerPolicies:
- containerName: "app"
# 允许 VPA 调整的资源类型
controlledResources: ["cpu", "memory"]
# 是否控制 limits
controlledValues: RequestsAndLimits
minAllowed:
cpu: "100m"
memory: "128Mi"
maxAllowed:
cpu: "2"
memory: "8Gi"
- containerName: "sidecar"
mode: "Off" # 不对 sidecar 容器做调整
HPA vs VPA 对比与配合
| 维度 | HPA | VPA |
|---|---|---|
| 调整方式 | 改变 Pod 数量 | 改变单个 Pod 资源 |
| 响应速度 | 快(15 秒检测周期) | 慢(需要重启 Pod) |
| 适用业务 | 无状态应用 | 有状态应用 |
| 兼容性 | Metrics Server / Prometheus | 需 VPA Recommender |
| 资源浪费 | 扩缩期间可能有资源闲置 | 单 Pod 资源利用率高 |
| 运维复杂度 | 低 | 中(需要重启) |
推荐策略:先用 VPA 的推荐模式(updateMode: Off)观察数天确定基线资源,然后设置合理的 requests,再用 HPA 做水平伸缩。HPA 和 VPA 不要同时作用于同一个 Deployment。
测试 HPA 效果
# 部署一个消耗 CPU 的应用
kubectl create deployment php-apache --image=php:7.4-apache
kubectl expose deployment php-apache --port=80
# 部署 HPA
kubectl autoscale deployment php-apache --cpu-percent=50 --min=1 --max=10
# 使用压测工具生成负载
kubectl run -it --rm load-test --image=busybox -- sh
# 在容器内
while true; do wget -q -O- http://php-apache; done
# 观察 HPA 响应
kubectl get hpa -w
# 查看 HPA 详细信息
kubectl describe hpa php-apache
# 停止测试后,HPA 会逐步缩容
HPA 算法与计算细节
官网对 HPA 的算法描述是:desiredReplicas = ceil[currentReplicas * (currentMetricValue / desiredMetricValue)],但落到具体实现上有几个关键细节,理解了才不会在压测时"看不懂 HPA 的行为":
- 利用率口径:CPU 利用率 = Pod 当前 CPU / Pod 的 requests.cpu。容器没写 requests 时该 Pod 不参与计算(HPA 会直接报 unable to fetch)
- 取均值:对多个 Pod 的指标值取算术平均(平均值按副本数加权),再与目标值比较
- 多指标取最大值:配置多个 metrics 时,每个指标独立算出 desiredReplicas,取其中最大值作为最终目标
- 向下取整边界:计算出的目标副本数在 minReplicas 与 maxReplicas 之间裁剪;
currentMetricValue / desiredMetricValue小于 0.9(缩容 10% 以上)才触发缩容计算,避免微小波动导致抖动 - 容忍度:HPA 控制器对比值在 0.9-1.1 之间的波动不做任何操作,这是防抖的第一道防线
- 副本数为 0 的集群:HPA 不能从 0 扩到 1,也不建议对缩到 0 的场景使用 HPA(应使用 KEDA 的 scale-to-zero)
# 手动推算示例(CPU 目标 50%)
# 当前:3 个副本,实际使用 1.2 核,requests 各 1 核
# 利用率 = (1.2 / 3) / 1 * 100% = 40%
# 比值 = 40 / 50 = 0.8(小于 0.9,触发缩容计算)
# desiredReplicas = ceil(3 * 0.8) = 3(无变化,因为比例不足 10%)
# 当前:3 个副本,实际使用 2.1 核
# 利用率 = (2.1 / 3) / 1 * 100% = 70%
# 比值 = 70 / 50 = 1.4
# desiredReplicas = ceil(3 * 1.4) = 5(扩容到 5)
# 查看 HPA 的决策过程(status 字段会显示每一步)
kubectl describe hpa webapp-hpa
# 关注 Conditions 里的 AbleToScale / ScalingActive / ScalingLimited
指标不可用时的行为
当 HPA 连续 2 分钟无法获取指标(Metrics Server 宕机、Pod 重启)时,控制器进入"延迟模式":暂停所有扩缩容决策,直到指标恢复。这在故障排查中很容易被误判为"HPA 失灵"——实际它是刻意保护,避免在无数据时做出错误决策。
Metrics Server 原理
Metrics Server 是 CPU/内存 HPA 的数据来源,理解它的工作链路是排障的前提:
- Metrics Server 每隔 15s(
--metric-resolution)向每个节点的 kubelet 请求/metrics/resource端点 - kubelet 通过 cAdvisor 采集容器实际使用量(来自 cgroup 统计),并按"Pod 实际使用 vs requests"返回
- Metrics Server 把结果聚合并暴露为
metrics.k8s.ioAPI,注册到 API Server 的聚合层(aggregation layer) - HPA 控制器通过
/apis/metrics.k8s.io/v1beta1读取指标
| 现象 | 可能断点 |
|---|---|
kubectl top nodes 无输出 | kubelet 指标端口不可达 / cAdvisor 异常 |
kubectl top pods 有节点没 Pod | Metrics Server 对 Pod 的 label selector 匹配失败 |
| HPA 报 unable to fetch metrics | metrics-server 未注册到聚合层 / 证书不信任(--kubelet-insecure-tls 场景) |
# 排障 Metrics Server
kubectl -n kube-system logs -l k8s-app=metrics-server --tail=50
kubectl -n kube-system describe pod -l k8s-app=metrics-server
# 直接请求聚合层(验证 API 注册)
kubectl get --raw /apis/metrics.k8s.io/v1beta1/nodes | jq
# 检查 kubelet 的容器指标是否正常
curl -k -H "Authorization: Bearer $(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \
https://$(hostname):10250/metrics/resource 2>/dev/null | head -5
自定义指标体系详解
K8s 的指标 API 分四个层,HPA 可以消费其中三种:
| API 组 | 指标来源 | 典型用途 |
|---|---|---|
| metrics.k8s.io | Metrics Server | CPU / 内存利用率 |
| custom.metrics.k8s.io | Prometheus Adapter / 自定义收集器 | 单 Pod 或整个资源的自定义指标(QPS、连接数) |
| external.metrics.k8s.io | 云厂商 / 外部系统 | 集群外部指标(SQS 队列长度、CloudWatch 指标) |
# Prometheus Adapter 配置:把 HTTP 请求速率暴露为 custom 指标
# adapter-config.yaml
rules:
- 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 Adapter
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm install prometheus-adapter prometheus-community/prometheus-adapter \
--namespace monitoring --create-namespace \
--set prometheus.url=http://prometheus.monitoring:9090 \
-f adapter-config.yaml
# 验证指标暴露
kubectl get --raw "/apis/custom.metrics.k8s.io/v1beta1" | jq .resources
# 查询某个 Pod 的 http_requests_per_second
kubectl get --raw "/apis/custom.metrics.k8s.io/v1beta1/namespaces/default/pods/*/http_requests_per_second" | jq
扩缩容测试场景设计
光配置不验证等于没配置。生产级验证至少要覆盖三类场景:
场景一:CPU 突发扩容
# 用 hey 生成瞬时高并发
kubectl run loadgen --image=williamyeh/hey --restart=Never -- \
-z 5m -c 200 -q 50 http://webapp.default.svc:8080/api
# 观察时间线(应看到 1-2 个同步周期内完成扩容)
kubectl get hpa webapp-hpa -w
kubectl get pods -l app=webapp -w
场景二:内存增长扩容(缓存类应用)
# 在应用内注入内存占用(如 Redis 的 DEBUG JMAP 或自定义压测脚本)
# 内存指标的特点:扩容触发快,但缩容几乎不会发生
# 因为内存使用率不会随流量下降而下降——这是设计行为,不是 bug
# 验证时注意:缩容只可能在内存真正释放后发生
场景三:自定义指标(队列深度)
# 向 RabbitMQ 灌入消息,观察消费者 Pod 数量是否随队列深度变化
# 配合 KEDA 的 cooldownPeriod(默认 300s),消息清空后不会立刻缩容
kubectl get scaledobject worker-scaler -o yaml | grep -E "cooldown|polling"
# 观察指标
kubectl get --raw "/apis/external.metrics.k8s.io/v1beta1" | jq .
测试验收清单
- 扩容时延:从负载上升到副本数达到目标 ≤ 3 个同步周期(45s)
- 扩容速率符合 behavior.scaleUp 策略(不被 selectPolicy 卡住)
- 缩容时延:负载消失后,副本数回到 minReplicas 的时间符合 stabilizationWindow 设计
- 全程无 5xx:扩容期间流量不中断(应用需正确处理扩容时序,勿在启动时拒绝流量)
HPA 与 Cluster Autoscaler 的配合
HPA 扩容的终点是集群容量:节点资源不足时,新 Pod 只能 Pending。Cluster Autoscaler(CA)负责补齐节点,两者配合的时序是:
- 负载上升 → HPA 在 15s 内扩容副本数
- Pod 进入 Pending(节点资源不足)→ CA 检测到"无法调度的 Pod"(默认 10s 检查一次)
- CA 扩容节点(云厂商虚拟机,通常 3-10 分钟)
- Pod 调度到新节点,HPA 维持副本数
# 观察 Pending Pod(扩容延迟的根源)
kubectl get pods -o wide | grep Pending
kubectl describe pod <pending-pod> | grep -A 5 Events
# CA 日志(确认扩容决策)
kubectl -n kube-system logs -l app=cluster-autoscaler --tail=100 | grep "scale-up"
# 关键配置:给节点池设置合理的 min/max,防止 CA 无限扩容
# 并通过 CA 的 --max-nodes-total 设置集群级上限
扩缩容场景模式库
不同业务形态适合不同的扩缩容策略,把常见场景沉淀为可复用的模式,比每次临时配参数靠谱:
| 业务形态 | 推荐指标 | 行为配置要点 | 典型应用 |
|---|---|---|---|
| 请求驱动型 | CPU / HTTP QPS | 扩容快(0s 稳定窗口 + 100% 速率)、缩容慢(300s+) | Web 服务、API 网关 |
| 队列消费型 | 队列深度(KEDA) | 按消息积压扩容,cooldown 防抖 | 异步任务、批处理 worker |
| 内存敏感型 | 内存利用率 | 只扩不缩(或极慢缩容),防 OOM | 缓存、流计算 |
| 定时突发型 | cron 触发器(KEDA) | 提前扩容、事后缓慢缩容 | 日报生成、定时报表 |
| 有状态型 | VPA 推荐 + 手动评估 | 不用 HPA(副本数由分片决定) | 数据库、消息中间件 |
模式一:Web 服务的标准配置
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: webapp-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: webapp
minReplicas: 3
maxReplicas: 30
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60
behavior:
scaleUp:
stabilizationWindowSeconds: 0 # 扩容不设稳定窗口
policies:
- type: Percent
value: 100
periodSeconds: 15 # 每 15s 最多翻倍
scaleDown:
stabilizationWindowSeconds: 300 # 缩容观察 5 分钟
policies:
- type: Pods
value: 2
periodSeconds: 60 # 每分钟最多缩 2 个
模式二:队列消费型(KEDA)
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: worker-scaler
spec:
scaleTargetRef:
name: worker-deployment
pollingInterval: 10 # 每 10s 查一次队列深度
cooldownPeriod: 180 # 队列清空后 3 分钟才开始缩容
minReplicaCount: 1
maxReplicaCount: 20
triggers:
- type: rabbitmq
metadata:
queueName: work-queue
queueLength: "20" # 每积压 20 条消息加一个副本
advanced:
horizontalPodAutoscalerConfig:
behavior:
scaleDown:
stabilizationWindowSeconds: 600
常见错误
| 问题 | 原因 | 解决方案 |
|---|---|---|
| HPA 显示 "unable to fetch metrics" | Metrics Server 未安装或无法连接 | 确认 kubectl top nodes 能正常返回,检查 Metrics Server Pod 日志 |
| HPA 不扩容 | Pod 未设置 resource requests | 确保 Deployment 中每个容器都设置了 resources.requests.cpu |
| HPA 频繁抖动 | 缩容稳定窗口太短 | 增大 stabilizationWindowSeconds 到 300s 以上 |
| VPA 删除 Pod 但新 Pod 资源未变 | VPA Admission Controller 未正确配置 | 检查 kubectl get pods -n kube-system | grep vpa,确认 Admission Controller 正常运行 |
| VPA 建议值异常高 | 历史数据不足或应用有突发流量 | 等待 7-14 天积累足够数据,或手动设置 maxAllowed 上限 |
最佳实践
- HPA 结合 PodDisruptionBudget 实现优雅缩容
- 缩容时设置 stabilizationWindowSeconds 避免抖动
- 数据类应用(消息队列、缓存)优先用自定义指标
- VPA 的 maxAllowed 一定要设,防止意外申请过量资源
- 配合 Cluster Autoscaler 实现集群级别的弹性伸缩
- 生产环境建议先用 VPA Off 模式观察 1-2 周,确定基线后再启用 HPA
- 为 HPA 配置合理的 minReplicas,避免流量突增时扩容不及时
练习题
- HPA 配置:为一个 Nginx Deployment 创建 HPA 配置,要求:CPU 使用率目标 60%,最小 3 个 Pod,最大 15 个 Pod。配置扩缩容行为:扩容时每分钟最多扩 50%,缩容时每 5 分钟最多缩 2 个 Pod。使用压测工具验证 HPA 行为符合预期。
- VPA 观察:安装 VPA 并为一个测试应用创建 VPA(updateMode: Off),观察 7 天后获取推荐的 CPU 和 memory 值。将推荐值与你手动设置的 requests 进行对比,分析差异原因。
- 混合伸缩场景:设计一个包含 HPA、VPA 和 Cluster Autoscaler 的完整伸缩方案。说明三者的协作关系:HPA 负责 Pod 级别伸缩,VPA 负责资源推荐,Cluster Autoscaler 负责节点级别伸缩。画出决策流程图。
学习检查点
学完本章后,请检验自己是否掌握以下内容:
| 检查项 | 自测问题 | 验证方法 |
|---|---|---|
| 概念理解 | 能用自己的话解释 HPA 和 VPA 的工作原理和指标来源 | 尝试向他人讲解 |
| 命令操作 | 能不查文档完成 HPA 配置和自定义指标适配 | 在终端实际执行 |
| 原理掌握 | 能说出 HPA 的算法原理和 VPA 的推荐机制 | 画出流程图 |
| 故障排查 | 能独立排查 HPA 不触发或副本数震荡的问题 | 模拟故障并修复 |
| 最佳实践 | 能说明为什么需要为应用配置资源请求和限制 | 对比不同方案 |
KEDA 安装示例
# 通过 Helm 安装 KEDA
helm repo add kedacore https://kedacore.github.io/charts
helm repo update
helm install keda kedacore/keda --namespace keda --create-namespace
# 验证安装
kubectl get pods -n keda
# KEDA ScaledObject 示例(基于 RabbitMQ 队列深度)
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: worker-scaler
spec:
scaleTargetRef:
name: worker-deployment
pollingInterval: 15
cooldownPeriod: 300
minReplicaCount: 1
maxReplicaCount: 20
triggers:
- type: rabbitmq
metadata:
host: amqp://rabbitmq.default.svc:5672
queueName: work-queue
queueLength: "50"
本章总结
HPA 与 VPA 解决不同维度的问题:HPA 水平调整副本数、响应快,适合无状态应用;VPA 垂直调整单 Pod 资源、需重启,适合有状态应用——两者不应同时作用于同一 Deployment。生产推荐先以 VPA Off 模式观察 1-2 周确定资源基线,再配置 HPA 的行为参数(稳定窗口、扩缩容速率),并基于真实负载压测验证效果,而不是拍脑袋定阈值。
延伸阅读
- 6.2:Kubernetes 入门 Kubernetes 容器编排入门——Pod 与 Deployment
- 3.12:系统监控与告警 系统监控与告警——Prometheus 指标采集
- Kubernetes HPA 官方文档:https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscaler/
- KEDA 官方文档:https://keda.sh/docs/latest/
- Cluster Autoscaler:https://github.com/kubernetes/autoscaler/tree/master/cluster-autoscaler