#DevOps

Kubernetes 自动伸缩:HPA、VPA 与 Cluster Autoscaler

流量来了 Pod 扛不住、流量去了 Pod 浪费。K8s 的三级伸缩(HPA 水平/VPA 垂直/CA 节点)解决不同问题。本文讲清 HPA(按 CPU/自定义指标加减 Pod)、VPA(调资源 request)、Cluster Autoscaler(加减节点),以及冷启动与预测性伸缩的实战。

本文要点

  • 主题聚焦:DevOps
  • 适合人群:独立开发者、全栈工程师、AI 应用开发者
  • 核心价值:快速理解背景、实践路径、常见误区和工程建议
作者 @admin·发布于 2026年2月16日·更新于 2026年7月23日

Kubernetes 自动伸缩:HPA、VPA 与 Cluster Autoscaler

先说一个我见过无数次的翻车现场:某团队上线一个 API,按"峰值流量 × 3"静态配了 20 个 Pod,白天跑得挺欢,晚上没人了 20 个 Pod 还在那儿烧钱;后来听说有 HPA,挂上去按 CPU 伸缩,结果大促当天 CPU 没怎么涨(瓶颈在 DB 连接数),Pod 根本没扩,直接雪崩。

这两个极端,正是 K8s 自动伸缩要解决的。但很多人对它的认知停留在"HPA = 按 CPU 加 Pod",没搞清楚K8s 其实有三层伸缩机制,各自解决不同维度的问题:

  • HPA(水平伸缩):加减 Pod 数量。扛流量的主力。
  • VPA(垂直伸缩):调 单个 Pod 的资源(request/limit)。解决"给多少"的问题。
  • Cluster Autoscaler(集群伸缩):加减 节点(Node)。Pod 没地方落了,它去要机器。

这三者不是三选一,而是组合拳。本文把它们逐个拆透,再讲那个最让团队吃亏的冷启动问题,最后给一套能直接抄的实战配置(HPA + 自定义指标 + PDB)。


一、为什么需要三级伸缩:三种维度,三个问题

先把心智模型立住。伸缩这件事,本质是回答**「当负载变化时,改什么」**:

text
┌─────────────────────────────────────────────────────────┐
│                    负载来了 / 负载走了                    │
└───────────────────────┬─────────────────────────────────┘
                        │
        ┌───────────────┼───────────────┐
        ▼               ▼               ▼
   ┌─────────┐     ┌─────────┐     ┌───────────┐
   │   HPA   │     │   VPA   │     │    CA     │
   │ 水平    │     │ 垂直    │     │ 节点      │
   ├─────────┤     ├─────────┤     ├───────────┤
   │ 加减    │     │ 调      │     │ 加减      │
   │ Pod 副本│     │ Pod资源 │     │ Node      │
   └─────────┘     └─────────┘     └───────────┘
   "多几个人干活" "给每个人更大工位" "多租一层办公楼"

这三层的关系是自底向上的触发链:

  1. HPA 发现 Pod CPU 高 → 把副本数从 3 加到 10;
  2. 但集群节点塞不下 10 个 Pod → 新 Pod 进入 Pending 状态(无节点可调度);
  3. Cluster Autoscaler 发现 Pending Pod → 向云厂商要新节点;
  4. 新节点就绪 → Pending Pod 调度上去 → 流量被分摊。

而 VPA 在这条链之外,它解决的是另一个问题:你这 Pod 一开始申请的资源就给错了。比如一个服务实际只用 100m CPU,你 request 写了 1000m,那调度器会按 1000m 给它预留节点空间——10 个这样的 Pod 就浪费了 9 核。VPA 通过观察实际用量,告诉你(或自动改成)正确值。

关键认知:HPA 和 VPA 不能同时基于同一个资源维度(比如都按 CPU)工作——因为 HPA 加副本会让每个 Pod 的 CPU 用量下降,VPA 看到用量低就调小 request,形成互相打架的反馈循环。后面会讲怎么组合。


二、HPA:水平伸缩,扛流量的主力

HPA(HorizontalPodAutoscaler)是绝大多数团队第一个上、也是用得最多的伸缩机制。它的工作循环很简单:周期性查指标 → 算出期望副本数 → 调 Deployment 的 replicas。

1. 核心算法:一个公式决定一切

HPA 的扩缩容决策,背后就是这个公式:

text
期望副本数 = ceil( 当前副本数 × ( 当前指标值 / 目标指标值 ) )

举个例子:当前 3 个副本,HPA 目标 CPU 50%,实际 CPU 90%:

text
期望副本数 = ceil( 3 × (90 / 50) ) = ceil(5.4) = 6

所以从 3 扩到 6。如果配了多个指标(CPU + 自定义 QPS),HPA 会分别算出每个指标对应的期望副本数,取最大值——保守策略,谁喊救命听谁的。

一个必须记住的细节:容忍度(tolerance)。默认 ±10% 的波动不触发任何动作。也就是说目标 50% 时,实际值在 45%~55% 之间,HPA 当没看见。这是为了防止抖动导致副本数反复横跳(flapping)。如果你发现 HPA"反应迟钝",很可能就是这 10% 的死区在起作用。

2. CPU 指标:是「相对于 request 的百分比」

这是新手踩的第一个大坑。HPA 里的 CPU 利用率,是相对 Pod 的 request 算的,不是绝对值。

yaml
resources:
  requests:
    cpu: 200m     # 0.2 核

配 HPA 目标 CPU 50%,意思是:每个 Pod 用到 100m(200m 的 50%)就达标。如果你把 request 写成 2000m,同样是 50% 目标,意味着每个 Pod 要用到 1000m 才会触发扩容——扩容阈值被悄悄抬高了 10 倍。

这就是为什么 request 设错了,HPA 的行为会完全错乱。很多团队"HPA 不工作"的根因,是 request 设得离谱(比如图省事全填 1 核 2G),导致 CPU 百分比永远在低位,HPA 永远不扩。

3. 最基础的 HPA:按 CPU 和内存

先看一个最常见、能覆盖大部分 Web 服务的配置:

yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: api-server-hpa
  namespace: prod
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api-server
  minReplicas: 3          # 兜底,再闲也别低于 3
  maxReplicas: 50         # 上限,防止失控扩容把集群打爆
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 60   # 目标 CPU 60%
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 70   # 目标内存 70%

注意几点:

  • minReplicas 别设成 1。生产服务至少给 2~3 个兜底,缩到 1 个时一旦那个 Pod 挂了,服务直接中断;
  • maxReplicas 是你的预算护栏。设一个你能承受的上限,否则一个指标异常能把副本数拉到几百,直接把节点资源耗光;
  • autoscaling/v2 是当前稳定版本(v2beta2 已废弃),支持多指标和行为配置。

4. 行为调优:scaleDown 是真正烧脑的地方

很多人 HPA 扩容很顺,但缩容时出事:流量一降,HPA 立刻把 Pod 砍掉,结果下一波流量来时来不及起,又雪崩。behavior 字段就是来解决这个的:

yaml
spec:
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 0    # 扩容立刻执行,不等
      policies:
      - type: Percent
        value: 100                     # 每次最多翻倍
        periodSeconds: 60
      selectPolicy: Max
    scaleDown:
      stabilizationWindowSeconds: 300  # 缩容前先观察 5 分钟
      policies:
      - type: Pods
        value: 2                       # 每 60s 最多缩 2 个
        periodSeconds: 60
      - type: Percent
        value: 25                      # 或最多缩 25%
        periodSeconds: 60
      selectPolicy: Min                # 取更保守的那个

这里的实战经验:

  • 扩容要快,缩容要慢。scaleUp 的 stabilization 设 0(立刻动),scaleDown 至少给 5~10 分钟稳定窗口。流量往往是尖刺型,扩晚了出事,缩快了下波又出事;
  • 缩容用 selectPolicy: Min,同时配"绝对值(Pods)"和"百分比"两条策略,取更小的,避免一次砍太多;
  • 扩容用 selectPolicy: Max,让 Pod 数尽快追上流量。

5. 自定义指标:这才是 HPA 的精华

按 CPU 扩容有个致命假设:CPU 是瓶颈。但现实里很多服务瓶颈是"队列深度""DB 连接数""在线连接数""QPS"——这些时候 CPU 根本不高,HPA 按 CPU 算就是聋子。这时候需要自定义指标。

HPA 支持四类指标,自定义指标走后两类:

类型数据来源典型场景
Resourcemetrics-server(Pod 内置 CPU/内存)通用
Pods自定义指标 API(每个 Pod 一个值)每副本 QPS、队列消费速率
Object自定义指标 API(关联某 K8s 对象)Ingress QPS、CRD 队列长度
External外部指标 API(非 K8s 资源)SQS 队列长度、Redis 连接数

要用自定义指标,得先部署一个 Metrics API 聚合器,最主流的是 Prometheus Adapter 或 KEDA。它把 Prometheus 里的指标暴露成 K8s Metrics API,HPA 才查得到。

一个基于 Ingress QPS 的 HPA 例子:

yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: api-server-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api-server
  minReplicas: 3
  maxReplicas: 30
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 60
  # 关键:按每副本 QPS 扩容,这才是真实负载信号
  - type: Pods
    pods:
      metric:
        name: http_requests_per_second   # 来自 Prometheus Adapter
      target:
        type: AverageValue
        averageValue: "100"              # 每个 Pod 扛 100 QPS 就扩

Prometheus Adapter 的规则(把 PromQL 翻译成 HPA 能查的指标)长这样:

yaml
# adapter config (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>>)'

我的强烈建议:生产服务别只按 CPU 扩,至少配一个业务语义指标(QPS、队列深度、延迟分位)。CPU 是结果,业务指标是原因——按原因扩容永远比按结果扩容快一步。如果觉得 Prometheus Adapter 配置繁琐,直接上 KEDA(后面冷启动部分会讲),它把"按 Prometheus/SQS/Kafka 等数据源伸缩"做成了 ScaledObject,配置直观得多。


三、VPA:垂直伸缩,给 Pod 调"工位大小"

VPA(VerticalPodAutoscaler)处理的是另一个问题:这个 Pod 该申请多少资源才合适。它不加减副本,而是改 Pod 的 resources.requests。

1. VPA 解决的真实痛点

每个团队都干过这种事:新服务上线,不知道吃多少资源,先拍脑袋填 cpu: 500m, memory: 512Mi。跑两周后一看监控,实际 CPU 只用了 30m,内存 80Mi——你白白占了 16 倍的资源配额。更糟的是反过来的情况:request 填小了,Pod 被限流(CPU throttling)或 OOMKilled,你还在排查"为啥这服务这么慢"。

VPA 的工作流:持续观察 Pod 的真实用量 → 用滑动窗口(默认 8 天历史 + 实时)统计 P50/P95/P99 分位 → 给出推荐值(target、lowerBound、upperBound)→ 根据模式决定要不要应用。

查推荐值:

bash
kubectl describe vpa api-server-vpa
# 输出里会有:
# Recommendation:
#   Target:  cpu 78m  memory 312Mi
#   ...

2. 三种模式:Off / Initial / Auto

yaml
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: api-server-vpa
spec:
  targetRef:
    apiVersion: "apps/v1"
    kind: Deployment
    name: api-server
  updatePolicy:
    updateMode: "Auto"   # 关键字段
  resourcePolicy:
    containerPolicies:
    - containerName: '*'
      minAllowed:
        cpu: 50m
        memory: 64Mi
      maxAllowed:
        cpu: 1
        memory: 1Gi

三种 updateMode:

  • Off:只观察、只给推荐,不改动任何东西。强烈建议第一周就这么跑,先把推荐数据看明白;
  • Initial:只在 Pod 新建时套用推荐值,不碰运行中的 Pod。这是最安全的"半自动"模式——下次滚动更新时自然生效,不重启现有 Pod;
  • Auto:会主动驱逐并重建 Pod 来套用新 request。最激进。

注意 minAllowed / maxAllowed 是护栏:防止 VPA 因为某个抖动把 request 调到离谱的值。生产环境一定要设。

3. VPA 的硬伤:改 request 要重启 Pod

这是 VPA 推广不开的根本原因。K8s 里运行中的 Pod 的资源 request 是不可变的(除非开了 InPlacePodVerticalScaling 特性,1.33 转正,目前较新)。所以 VPA 的 Auto 模式要"改 request",只能把 Pod 杀掉重建。

这带来一堆副作用:

  • 重建期间该副本下线,如果没配 PDB 或副本数太少,可能影响可用性;
  • VPA 不会一次性杀全部(它走驱逐逻辑,遵守 PDB),但频繁调资源 = 频繁重启;
  • JVM/预热型服务每次重启都要重新 warmup,代价不小。

实战建议:

  • 生产用 Initial 模式最稳——观察推荐值,滚动发布时自然套用,零额外重启;
  • 想自动调,先确认服务能频繁优雅重启,并配好 PDB;
  • 别忘了 Off 模式本身就是宝:把 VPA 当"资源调优顾问",用它给的推荐值去人工调整 Deployment,比拍脑袋准 10 倍。

4. HPA 和 VPA 怎么共存

记住一条铁律:HPA 和 VPA 绝不能基于同一个资源维度。

  • ❌ HPA 按 CPU 扩容 + VPA 调 CPU request:VPA 调大 request → 每个 Pod CPU 利用率下降 → HPA 觉得"不用那么多副本了"开始缩容 → 两个控制器互相打架,副本数和 request 反复横跳;
  • ✅ HPA 按自定义指标(QPS)扩容 + VPA 调 CPU/memory request:两者各管一摊,QPS 决定副本数,CPU/memory 决定每个副本的工位,互不干扰。

社区甚至推荐:先用 VPA(Off 模式)跑两周摸清真实资源画像 → 把 request 定准 → 再上 HPA。先把地基打好,再谈水平扩展。


四、Cluster Autoscaler:节点层面的伸缩

前面两层都在 Pod 里打转,但 Pod 总得跑在 Node 上。当 HPA 扩容出 10 个新 Pod,而集群已经塞满了,这 10 个 Pod 会卡在 Pending 状态——这时就该 Cluster Autoscaler(CA) 上场了。

1. CA 怎么工作:盯 Pending Pod + 盯空闲节点

text
┌──────────────────────────────────────────────────────┐
│ 1. HPA 扩容 → 新 Pod 进入 Pending(无节点可调度)      │
└──────────────────────┬───────────────────────────────┘
                       ▼
┌──────────────────────────────────────────────────────┐
│ 2. CA 监测到 Pending Pod,模拟调度器评估             │
│    "如果加一台 m5.xlarge,这些 Pod 能不能落地?"       │
└──────────────────────┬───────────────────────────────┘
                       ▼
┌──────────────────────────────────────────────────────┐
│ 3. 调用云厂商 API,开新节点(AWS/GCP/Azure)           │
│    节点就绪并注册 → Pod 被调度上去                    │
└──────────────────────────────────────────────────────┘
                       ▲
                       │
┌──────────────────────┴───────────────────────────────┐
│ 4. 缩容:某节点利用率 < 阈值(默认50%)且持续 10 分钟 │
│    → CA 把上面 Pod 迁走 → 节点下线                    │
└──────────────────────────────────────────────────────┘

CA 的扩容逻辑相对直接:看到 Pending Pod,找最合适的节点规格去开。缩容逻辑更微妙:它会找利用率长期低于阈值(默认 50%)的节点,把上面的 Pod 驁到别处,再关机。但有几类 Pod 不会被迁:带本地存储的、不被驱逐的、DaemonSet 的等。

2. CA 的"慢"是结构性的

CA 从"Pod Pending"到"节点就绪",在云上通常要 1~5 分钟(开机 + kubelet 注册 + 镜像拉取)。这个延迟是冷启动问题的核心来源之一,后面专门讲。

配置上,CA 本身一般以 Deployment 形式跑在集群里,关键参数:

yaml
# Cluster Autoscaler 启动参数(节选)
- --scale-down-unneeded-time=10m        # 节点空闲多久才缩
- --scale-down-utilization-threshold=0.5 # 利用率低于多少算"空"
- --max-node-provision-time=15m         # 开节点最多等多久
- --balance-similar-node-groups=true    # 跨可用区均衡
- --expander=least-waste                # 选节点规格的策略

expander 决定"开哪种节点",常见策略:random(随机)、most-pods(能塞最多 Pending Pod 的)、least-waste(浪费最少的,推荐)、priority(按预设优先级表,适合 spot+on-demand 混部)。

3. CA vs Karpenter:这个赛道在变

传统的 Cluster Autoscaler 是节点组(Node Group / ASG / MIG)驱动的——你得先定义好几组固定规格的节点池,CA 在这些池里加减机器。问题是:规格是预先固定的,Pod 形状千变万化,经常塞不进去导致碎片。

AWS 的 Karpenter(后来捐给社区,跨云支持在推进)走的是另一条路:没有节点组,直接按 Pod 需求实时挑最合适的实例规格。它把 Pending Pod 的需求(CPU/内存/GPU/拓扑/容忍度)翻译成一次"买哪台机器最划算"的决策,几秒内就能起节点。在 AWS 上,Karpenter 几乎已经是新集群的默认选择。

选型判断:

  • 在 AWS 上、节点规格多样、想用 Spot 省钱 → Karpenter,体验降维打击;
  • GCP/Azure 或需要严格节点组治理 → Cluster Autoscaler,成熟稳定;
  • 两者的共同前提:Pod 资源 request 要设准——CA/Karpenter 都按 request 调度,request 虚高 = 节点虚开 = 浪费。

五、冷启动:伸缩最难的从来不是"算出来",而是"来得及"

讲到这里,你会发现所有伸缩机制都有个共同硬伤:它们是反应式的。流量已经来了 → 指标才涨上去 → HPA 才算出要扩 → 新 Pod 才起 → 新 Pod 才 ready → 才能接流量。这条链路加起来,经常是 2~5 分钟。

可流量尖刺往往 30 秒就把现有 Pod 打挂了。这就是冷启动问题:伸缩永远慢半拍。

1. 把延迟拆开,逐段优化

先看扩容的完整时间线:

text
[流量到达]                        t=0
[指标采集窗口,通常 30~60s]        +30~60s   ← 段①:指标延迟
[HPA 计算并改 replicas]            +几秒      ← 段②:控制循环
[新 Pod 调度 + 拉镜像 + 启动]      +20~90s    ← 段③:Pod 启动
[就绪探针通过]                     +5~30s     ← 段④:首次 ready
[如果节点不够,CA 开机]            +60~300s   ← 段⑤:节点冷启动
───────────────────────────────────────────
最坏情况:5 分钟才扛上流量

逐段打:

  • 段①指标延迟:缩短采集窗口(比如 PromQL 从 [2m] 改 [1m]),但太短会抖动;
  • 段②控制循环:HPA 默认 15s 一次(--horizontal-pod-autoscaler-sync-period),可调到 10s,边际收益不大;
  • 段③Pod 启动:镜像要小(用 distroless/alpine,去掉调试工具)、用更快的拉取(镜像预热到节点、或用 lazydocker/soci 加速大镜像分层拉取)。JVM 类应用启动慢,可改用 GraalVM 原生镜像或 CRaC;
  • 段④就绪探针:readinessProbe 的 initialDelaySeconds 别设太大(很多人图省事填 30s,白白浪费 30s),改用 startupProbe 处理慢启动;
  • 段⑤节点冷启动:这是最硬的一段,靠下面两招。

2. 过量配置(Pod 过量配置 / Pause Pod):用钱换时间

最朴素有效的招:常备一些"占位 Pod",节点早就开好了,新 Pod 直接调度。

原理是用一个低优先级的 Pause Pod占着节点上的资源。真业务 Pod 来了,因为优先级高,调度器立刻把 Pause Pod 踢掉,业务 Pod 顶上——节点不用新开,省掉了段⑤那几分钟。

yaml
# 1. 定义低优先级 PriorityClass
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: overprovisioning
value: -1                    # 负值,比所有业务 Pod 都低
globalDefault: false
description: "用于占位扩容的 Pause Pod"
---
# 2. 占位 Pod(什么都不干,纯占资源)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: overprovisioning
spec:
  replicas: 3                # 占 3 个"虚拟槽位"
  selector:
    matchLabels:
      app: overprovisioning
  template:
    metadata:
      labels:
        app: overprovisioning
    spec:
      priorityClassName: overprovisioning
      containers:
      - name: reserve
        image: registry.k8s.io/pause:3.9
        resources:
          requests:
            cpu: 1
            memory: 2Gi      # 占位大小 = 一个业务 Pod 的量

这相当于用 3 个 Pod 的钱,预先买了"随时可用的扩容槽位"。流量来了,业务 Pod 秒级顶上,Pause Pod 被挤走。等流量退了,Pause Pod 又回来继续占位。简单粗暴但极其有效,是几乎所有对延迟敏感的大厂都会用的招。

3. 预测性伸缩:用时间表打败反应式延迟

如果你的负载有规律(每天高峰、每周高峰、大促),别等指标涨了再扩,提前扩。

招式一:CronHPA / 定时伸缩。Kubernetes 社区有 CronHPA(华为贡献),或用 KEDA 的 cron scaler。比如每天 10:00 扩到 20 副本,22:00 缩回 5:

yaml
# KEDA 的 cron trigger
triggers:
- type: cron
  metadata:
    timezone: Asia/Shanghai
    start: "0 9 * * *"      # 早9点前扩好
    end: "0 23 * * *"        # 晚11点后缩
    desiredReplicas: "20"

招式二:基于历史指标预测。更进阶,用历史 7 天同时段指标预测今天的负载曲线,提前 N 分钟扩容。这块目前没有官方一等公民方案,常见做法:

  • 自建脚本/Operator,查 Prometheus 历史,调 HPA 的 minReplicas 抬高;
  • 用云厂商的预测伸缩(如 AWS Predictive Scaling for EKS 节点);
  • 部分商业平台(Datadog Adaptive Scaling 等)提供托管方案。

我的判断:90% 的团队先把「过量配置 + 业务指标 HPA」做好,收益就远大于折腾预测算法。预测性伸缩是为规律性极强、或对延迟极其敏感的场景准备的,别一上来就追这个。


六、实战配置:一套能直接抄的组合拳

把前面的东西揉成一套生产可用的配置。场景:一个 HTTP API 服务,希望按 QPS + CPU 扩容,缩容要稳,缩容时不被一锅端。

1. Deployment:资源设准是前提

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-server
  namespace: prod
spec:
  replicas: 3
  selector:
    matchLabels:
      app: api-server
  template:
    metadata:
      labels:
        app: api-server
    spec:
      containers:
      - name: api
        image: registry.example.com/api-server:v1.2.3
        ports:
        - containerPort: 8080
        resources:
          requests:
            cpu: 250m       # 设准!这是 HPA 算 CPU% 的分母
            memory: 256Mi
          limits:
            memory: 512Mi   # CPU 不设 limit,避免 throttling 误判
        readinessProbe:     # 就绪探针:能不能接流量
          httpGet:
            path: /healthz
            port: 8080
          initialDelaySeconds: 2
          periodSeconds: 5
        startupProbe:        # 启动探针:慢启动专用,不挡 readiness
          httpGet:
            path: /healthz
            port: 8080
          failureThreshold: 30
          periodSeconds: 5   # 给最多 150s 启动时间
        lifecycle:
          preStop:
            exec:
              command: ["/bin/sh", "-c", "sleep 10"]  # 优雅退出,等连接排空
      terminationGracePeriodSeconds: 60

几个关键决策:

  • CPU 不设 limit(只设 request):避免 CPU throttling 把延迟拉高,同时 HPA 看到的利用率更线性。设了 limit 一旦接近上限,cgroup 限流但利用率曲线会被压平,反而误导 HPA;
  • startupProbe 单独配:慢启动服务(JVM、模型加载)用 startupProbe 给足启动时间,成功后再交给 readinessProbe,这样 initialDelaySeconds 可以设很小;
  • preStop sleep:给 kube-proxy 一点时间把 Pod 从 Service endpoints 里摘掉,避免流量打到正在退出的 Pod。

2. HPA:双指标 + 稳健缩容

yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: api-server-hpa
  namespace: prod
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api-server
  minReplicas: 3
  maxReplicas: 30
  metrics:
  # 指标1:CPU 兜底
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 65
  # 指标2:每副本 QPS(业务真实信号,来自 Prometheus Adapter)
  - type: Pods
    pods:
      metric:
        name: http_requests_per_second
      target:
        type: AverageValue
        averageValue: "150"
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 0
      policies:
      - type: Percent
        value: 100
        periodSeconds: 60
      selectPolicy: Max
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
      - type: Pods
        value: 2
        periodSeconds: 60
      selectPolicy: Min

3. PDB:缩容/驱逐时的保命索

很多人忘配 PDB(PodDisruptionBudget),结果一次节点缩容或维护把所有 Pod 一锅端。PDB 限制"自愿驱逐"(节点缩容、kubectl drain 等)能同时干掉多少 Pod:

yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: api-server-pdb
  namespace: prod
spec:
  minAvailable: 2          # 任何时候至少留 2 个
  # 或者用 maxUnavailable: 1
  selector:
    matchLabels:
      app: api-server

注意 PDB 只防自愿驱逐(CA 缩容、节点 drain、VPA 驱逐),不防 Pod 崩溃这种非自愿驱逐——那得靠副本数本身兜底。所以 minReplicas + PDB.minAvailable 要一起算,保证"最坏情况下还剩够用的副本"。

4. 把这套串起来验证

部署后,用这几条命令验证伸缩是否健康:

bash
# 看 HPA 当前决策和指标
kubectl get hpa api-server-hpa -n prod
# 看 TARGETS 列:实际值/目标值,实际远超目标却不扩 → 查指标源

# 看 HPA 的决策事件(扩了几次、为什么)
kubectl describe hpa api-server-hpa -n prod | grep -A20 Events

# 看 CA 决策(有没有 Pending Pod 触发扩节点)
kubectl logs -n kube-system deploy/cluster-autoscaler | grep "scale up"

# 压测模拟流量,观察扩容链路
kubectl get pods -n prod -w   # -w 持续看副本变化

一个常见排错:kubectl get hpa 的 TARGETS 列显示 <unknown>——说明指标查不到。CPU 显示 unknown 是 metrics-server 没装好;自定义指标 unknown 是 Prometheus Adapter 规则没匹配上。先把这个搞通,再谈调优。


小结

  • 三级伸缩各管一摊:HPA 加减 Pod 副本(扛流量)、VPA 调 Pod 的 request/limit(给资源)、Cluster Autoscaler 加减 Node(给 Pod 落脚点)。三者是自底向上的触发链,不是三选一,生产里通常组合使用。
  • HPA 按 CPU 是入门,按业务指标才是精华。CPU 是结果,QPS/队列深度/延迟才是原因。记住 HPA 的 CPU 利用率是相对 request 的百分比——request 设错,HPA 行为全错。扩容要快、缩容要慢(behavior 字段),缩容前给 5 分钟稳定窗口。
  • VPA 最有价值的用法是 Off 模式当"资源顾问"。Auto 模式会重启 Pod,生产慎用;Initial 模式最稳。HPA 和 VPA 绝不能基于同一资源维度,否则互相打架。
  • Cluster Autoscaler 是反应式且慢的(分钟级),选型上 AWS 可考虑 Karpenter(秒级、按需挑规格)。无论哪个,前提都是 request 设准——它们都按 request 调度。
  • 冷启动是伸缩最难的部分:把延迟拆成指标/控制循环/Pod 启动/就绪/节点开机五段逐个打。最有效的两招——过量配置(Pause Pod 占位)用钱换时间,定时/预测伸缩用先验打败反应式。PDB 别忘配,否则一次缩容可能把你一锅端。

金句:K8s 的自动伸缩,本质是用「反应式延迟」换「资源效率」。HPA/VPA/CA 都是「指标先涨、我才扩」的事后机制,永远慢半拍——真正决定你扛不扛得住尖刺的,不是 HPA 算得多准,而是你愿不愿意为那几分钟延迟预先付一点过量的钱、配一点占位的 Pod。伸缩调优做到最后,工程师都是在「省钱」和「睡得着觉」之间找那条线。

Related

相关推荐