业务流量从来不是一条直线:白天高峰、夜里低谷,大促时瞬间翻十倍。副本数写死,要么高峰扛不住,要么低谷白白烧钱。在 健康检查与资源管理 里我们学会了给容器设 requests/limits,这些数字本身也很难一次拍准。
Kubernetes 的自动扩缩分三个层次:水平扩缩(HPA) 调整 Pod 数量,垂直扩缩(VPA) 调整每个 Pod 的资源,节点扩缩(Cluster Autoscaler / Karpenter) 调整机器数量。这一课依次讲清它们的原理和配合方式,并在 kind 集群里用压测亲眼看 HPA 把副本从 1 扩到多个再缩回来。
三层扩缩全景
指标来源
metrics-server(CPU/内存) Prometheus Adapter / KEDA(自定义、外部指标)
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ HPA:改 replicas │ │ VPA:改 requests │
└────────┬─────────┘ └────────┬─────────┘
│ Pod 变多 / 变大,节点放不下 → Pending
▼
┌──────────────────────────────────────────────┐
│ Cluster Autoscaler / Karpenter:加节点、删空闲节点 │
└──────────────────────────────────────────────┘它们都是 控制平面深入 里讲过的控制循环:周期性读取指标,计算期望值,写回对象。
metrics-server:资源指标管道
HPA 要知道 Pod 用了多少 CPU,数据来自 Metrics API(metrics.k8s.io)。它的标准实现是 metrics-server:定期从每个 kubelet 的 /metrics/resource 端点抓取容器的 CPU 和内存用量,在内存中保存最新值,通过 apiserver 的聚合层(API Aggregation)对外提供。
- 它只保留最近一次采样,不是监控系统,没有历史数据,长期指标请用 可观测性 一课的 VictoriaMetrics/Prometheus。
kubectl top也依赖它。
在 kind 中安装。kind 的 kubelet 使用自签证书,需要加 --kubelet-insecure-tls(生产集群应为 kubelet 配置正规签发的服务证书,不要加这个参数):
helm repo add metrics-server https://kubernetes-sigs.github.io/metrics-server/
helm upgrade --install metrics-server metrics-server/metrics-server \
--namespace kube-system \
--set replicas=1 \
--set 'args={--kubelet-insecure-tls}'
kubectl -n kube-system rollout status deploy/metrics-server
kubectl get apiservice v1beta1.metrics.k8s.io # AVAILABLE 应为 True
kubectl top nodes
kubectl top pods -A --sort-by=cpu | headHPA:水平扩缩
HorizontalPodAutoscaler 使用 autoscaling/v2 API(v1.23 起稳定)。控制器默认每 15 秒执行一次:
期望副本数 = ceil( 当前副本数 × 当前指标值 / 目标指标值 )例如 3 个副本,平均 CPU 利用率 90%,目标 60%:ceil(3 × 90 / 60) = 5。几个细节:
- 利用率是相对 requests 计算的。容器没设 CPU requests,HPA 就无法计算 CPU 利用率,会报
missing request for cpu。 - 比值在 1 附近的容忍区间内(默认 ±10%)不触发扩缩,避免抖动。
- 多个指标时分别计算,取最大的副本数。
- 未就绪的 Pod 和刚启动的 Pod 会被特殊处理,避免启动时 CPU 尖峰导致误扩。
一个完整的 HPA
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: web
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: web
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60 # 相对 requests 的百分比
- type: Resource
resource:
name: memory
target:
type: AverageValue
averageValue: 400Mi # 也可以用绝对值
behavior:
scaleUp:
stabilizationWindowSeconds: 0 # 扩容立即响应
policies:
- type: Percent
value: 100 # 每 15 秒最多翻倍
periodSeconds: 15
- type: Pods
value: 4 # 或最多加 4 个
periodSeconds: 15
selectPolicy: Max # 取两者中更激进的
scaleDown:
stabilizationWindowSeconds: 300 # 取过去 5 分钟内的最大建议值,防止锯齿
policies:
- type: Percent
value: 20 # 每分钟最多缩 20%
periodSeconds: 60behavior 是 v2 最实用的部分:扩容要快、缩容要慢。默认的缩容稳定窗口就是 300 秒,大多数场景够用;对启动很慢的 Java 服务,可以进一步放缓缩容。
自定义指标与外部指标
除 Resource 外,metrics 还支持:
| 类型 | 来源 API | 例子 |
|---|---|---|
Pods | custom.metrics.k8s.io | 每个 Pod 的平均 QPS |
Object | custom.metrics.k8s.io | 某个 Ingress 的总请求速率 |
External | external.metrics.k8s.io | 消息队列长度、云监控指标 |
ContainerResource | metrics.k8s.io | 只看主容器的 CPU,忽略 sidecar |
custom/external 两个 API 需要一个适配器实现,常见的是 Prometheus Adapter 或 KEDA。以每 Pod QPS 为例:
metrics:
- type: Pods
pods:
metric:
name: http_requests_per_second
target:
type: AverageValue
averageValue: "100" # 每个 Pod 平均 100 QPS与 Deployment 配合的注意事项
- 启用 HPA 后,不要在 Deployment 清单里再写
replicas,否则每次kubectl apply、Helm 升级或 GitOps 同步都会把副本数改回清单里的值,和 HPA 来回打架。 minReplicas至少设 2,配合 PodDisruptionBudget,保证节点维护时服务不中断。- 就绪探针要准确:新 Pod 就绪之前不分担流量,HPA 会看到指标仍然很高而继续扩容。
动手:在 kind 中压测演示 HPA
- 部署一个会消耗 CPU 的应用,注意设置 CPU requests:
apiVersion: apps/v1
kind: Deployment
metadata:
name: php-apache
spec:
selector:
matchLabels: { run: php-apache }
template:
metadata:
labels: { run: php-apache }
spec:
containers:
- name: php-apache
image: registry.k8s.io/hpa-example
ports:
- containerPort: 80
resources:
requests: { cpu: 200m }
limits: { cpu: 500m }
---
apiVersion: v1
kind: Service
metadata:
name: php-apache
spec:
selector: { run: php-apache }
ports:
- port: 80kubectl apply -f php-apache.yaml
kubectl autoscale deployment php-apache --cpu-percent=50 --min=1 --max=8
kubectl get hpa php-apache
# NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS
# php-apache Deployment/php-apache cpu: 0%/50% 1 8 1- 另开一个终端持续观察:
kubectl get hpa php-apache --watch- 再开一个终端施加负载(每个请求都会让 php 做一段计算):
kubectl run load-generator --rm -it --image=busybox:1.36 --restart=Never -- \
/bin/sh -c "while sleep 0.01; do wget -q -O- http://php-apache; done"- 一两分钟后,TARGETS 升到 200% 以上,REPLICAS 按公式逐步增加,直到平均利用率回到 50% 左右。查看决策过程:
kubectl describe hpa php-apache | sed -n '/Conditions/,$p'
# Normal SuccessfulRescale New size: 4; reason: cpu resource utilization (percentage of request) above target- 在负载终端按
Ctrl+C停止压测。副本数不会立刻下降——要等默认 5 分钟的缩容稳定窗口过去,才会逐步缩回 1。
VPA:垂直扩缩
HPA 解决"几个 Pod",VerticalPodAutoscaler 解决"每个 Pod 要多大"。它不是 Kubernetes 内置组件,需要从 kubernetes/autoscaler 单独安装,包含三个组件:
- Recommender:根据历史用量计算推荐的 requests。
- Updater:发现 Pod 资源与推荐值偏差太大时,让它按新值更新。
- Admission Controller:在 Pod 创建时把 requests 改成推荐值。
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: web
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: web
updatePolicy:
updateMode: "Off" # 只给建议,不动 Pod
resourcePolicy:
containerPolicies:
- containerName: "*"
minAllowed: { cpu: 50m, memory: 64Mi }
maxAllowed: { cpu: "2", memory: 2Gi }updateMode 的取值:
| 模式 | 行为 |
|---|---|
Off | 只计算推荐值,写在 status.recommendation 里 |
Initial | 只在 Pod 创建时设置,不驱逐运行中的 Pod |
Recreate | 必要时驱逐 Pod 让它按新值重建 |
InPlaceOrRecreate | 优先利用 Pod 原地调整资源(In-place Pod Resize),不行再重建 |
Pod 原地调整资源(修改运行中容器的 CPU/内存而不重启)是近几个版本逐步稳定下来的特性,它让 VPA 的自动模式对有状态和启动慢的应用更友好。具体可用程度请以你所用的 Kubernetes 与 VPA 版本说明为准。
节点扩缩:Cluster Autoscaler 与 Karpenter
HPA 把副本扩到 20 个,节点装不下,新 Pod 就会 Pending。节点扩缩器正是以"有因资源不足而无法调度的 Pod"为信号的:
Cluster Autoscaler(CA)
- 基于预先定义的节点组(云上的 Auto Scaling Group、节点池)工作。
- 发现 Pending Pod 时,模拟调度"如果某个节点组加一台,这个 Pod 能不能放下",能就调用云 API 扩容该节点组。
- 周期性检查利用率低的节点,如果其上 Pod 都能挪到别处,就驱逐并删除节点。会尊重 PDB,带本地存储或
cluster-autoscaler.kubernetes.io/safe-to-evict: "false"注解的 Pod 会阻止缩容。
Karpenter
- 不依赖预定义节点组,直接根据 Pending Pod 的需求(CPU、内存、GPU、架构、可用区、竞价实例)即时挑选最合适的机型创建节点。
- 支持节点整合(Consolidation):主动把零散 Pod 合并到更少更便宜的节点上。
- 最初由 AWS 开源,现已捐给 Kubernetes SIG Autoscaling,其他云厂商也在提供实现。
KEDA:事件驱动扩缩
HPA 最小只能缩到 1 个副本(缩到 0 需要开启 alpha 特性门控)。很多异步任务(消费消息队列、处理上传文件)在没有消息时完全不需要运行。KEDA(Kubernetes Event-driven Autoscaling,CNCF 毕业项目)解决这个问题:
- 内置数十种 Scaler:Kafka、RabbitMQ、Redis、Prometheus 查询、Cron、云队列等。
- 用户创建
ScaledObject,KEDA 负责在 0 ↔ 1 之间激活/停用工作负载,并自动生成一个 HPA 处理 1 ↔ N 的扩缩,同时作为 external metrics 适配器为这个 HPA 提供指标。
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: order-worker
spec:
scaleTargetRef:
name: order-worker # Deployment 名
minReplicaCount: 0 # 没消息时缩到 0
maxReplicaCount: 30
cooldownPeriod: 300
triggers:
- type: rabbitmq
metadata:
queueName: orders
mode: QueueLength
value: "20" # 每个副本负责 20 条积压消息
authenticationRef:
name: rabbitmq-auth在大模型推理服务中,也常用 KEDA 基于 Prometheus 中的排队请求数、GPU KV Cache 使用率等指标扩缩推理副本,这部分会在 大模型推理服务 中展开。
动手练习
- 安装 metrics-server,分别用
kubectl top pod --containers和kubectl get --raw /apis/metrics.k8s.io/v1beta1/namespaces/default/pods | jq查看同一份数据。 - 完成本课的 php-apache 压测 LAB,记录从施压到副本数稳定所用的时间,以及停止压测后开始缩容的时间。
- 把 php-apache 的 HPA 改为 YAML 管理,添加
behavior.scaleDown.stabilizationWindowSeconds: 30,重新压测,对比缩容速度。 - 删除 php-apache 容器的 CPU requests 后重新部署,观察
kubectl describe hpa中出现的错误。 - (可选)安装 VPA,给 php-apache 创建
updateMode: "Off"的 VPA,施压几分钟后查看它给出的推荐值。
自测
HPA 当前有 4 个副本,平均 CPU 利用率 30%,目标 60%,minReplicas 为 3,期望副本数是多少?
ceil(4 × 30 / 60) = 2,但不能低于 minReplicas,所以是 3。另外缩容受 behavior.scaleDown 的稳定窗口(默认 300 秒)约束,不会立即执行。
为什么容器必须设置 CPU requests,HPA 才能按 CPU 利用率扩缩?
Utilization 类型的目标是"实际用量 / requests"的百分比。没有 requests,分母不存在,HPA 无法计算,会报 missing request for cpu。(使用 AverageValue 绝对值目标则不需要 requests,但仍强烈建议设置。)
启用了 HPA 的 Deployment,清单里写着 replicas: 3,会有什么问题?
每次 kubectl apply、Helm 升级或 GitOps 同步都会把副本数重置为 3,覆盖 HPA 的计算结果,导致副本数突变。应从清单中删除 replicas 字段,让 HPA 独占管理。
metrics-server 挂了会发生什么?能用它替代 Prometheus 吗?
基于资源指标的 HPA 无法获取数据,停止扩缩(保持当前副本数),kubectl top 失败。不能替代 Prometheus:metrics-server 只在内存中保存最新一次采样,没有历史数据和查询能力,只服务于自动扩缩和 kubectl top。
Cluster Autoscaler 以什么作为扩容节点的信号?为什么 CPU 利用率高不会触发扩容?
以"因资源不足而无法调度的 Pending Pod"为信号,并模拟调度确认新节点能放下它们。调度依据的是 requests 而不是实际用量,节点 CPU 利用率再高,只要 Pod 都能调度,就不需要新节点;Pod 数量的增加由 HPA 负责。