容器与编排

Kubernetes 自动扩缩容实战:HPA、VPA 与节点扩容的分工

CPU 到了 80% 就扩副本、内存一涨就 OOM,这套直觉在 Kubernetes 上基本不成立。本文拆开三类自动化的边界:HPA 基于什么指标、metrics-server 为何是硬依赖、VPA 与 HPA 在同一资源字段上为什么会互相打架,以及什么时候该上 Custom Metrics 或直接加节点。

作者:巧匠团队·11 分钟阅读·更新于 2026-10-02

三类自动化的分工:谁决定副本数、谁决定单副本大小、谁决定节点数

把三件事分清楚,后面所有困惑都会消失。HPA(Horizontal Pod Autoscaling)改的是副本数——同一个 Deployment 下面跑 3 个还是 30 个 pod;VPA(Vertical Pod Autoscaling)改的是单个容器的 requests/limits;Cluster Autoscaler 或 Karpenter 改的是节点数量。三者作用在不同层级,理论上可以叠加,但信号来源往往重叠,这才是冲突的根源。

HPA 是响应式的:它按当前负载调整未来的副本数。VPA 是纠正式的:它观察一段时间的历史使用量,把 requests 调到贴近真实需求。Cluster Autoscaler 同样是响应式的,但它的判断依据里包含了 pod 的 requests——它估算「当前节点还能不能塞下这些 pod」,而 pod 大小又恰恰是 VPA 在改。

一个直观的反直觉点:requests 调小会让 pod 更容易被调度到已有节点上,从而让 Cluster Autoscaler 觉得「不需要新节点」,进而不再扩容。VPA 省资源的同时可能顺手抑制了集群扩容;反过来,requests 调大又会触发大规模扩容。三者串在一起时,行为往往超出单个组件的预期。

实践建议是分层落地:先用 HPA 解决「副本数不够」,用固定 requests 解决调度问题,只在确实存在资源画像差异明显的工作负载上引入 VPA,并且严格限制 VPA 的作用范围(例如只对非关键服务开启自动模式)。

# 现状盘点
kubectl get hpa -A
kubectl top pods -A --sort-by=cpu | head -20     # 需要 metrics-server
kubectl get vpa -A 2>/dev/null

# 节点层面:requests 总量 vs 实际分配量
kubectl describe nodes | grep -A6 "Allocated resources:"

HPA 基于什么指标,以及 metrics-server 为什么是硬依赖

最常见的 HPA 基于 `type: Resource` 的 CPU(或 memory)利用率。这里的「利用率」不是容器实际用掉的绝对值,而是相对于 requests 的比例:`usage / requests`。这就是 requests 在 autoscaling 里承担了第二个职责——它同时是调度依据和扩容计算的基准线。把 requests 设得虚高,扩容会明显迟钝;设得虚低,会在真正吃紧之前就疯狂扩容。

这条链路依赖 metrics-server:kubelet 采集 cAdvisor 指标 → metrics-server 聚合为 API Server 的 `metrics.k8s.io` 资源指标 API → HPA 通过该 API 读取。任何一个环节缺失,HPA 会把请求目标设为 `unknown`,并给出 `<unknown>/<unknown>` 的目标值。

集群默认装 metrics-server 的比例并不高,尤其是自建集群或精简安装的环境。最小安装里经常没有它,于是 HPA 对象创建成功、状态卡在 unknown、扩容永不发生。判断依据是 HPA 的 `TARGETS` 那一行:如果出现 `<unknown>`,问题在指标链路而不在 HPA 配置。

除了 Resource,HPA 还支持 `type: Pods`、`type: Object`、`type: External` 三类指标。其中 Pods/Object 由 HPA Controller 直接从 metrics API 拉取,而 External 需要额外的适配层(Prometheus Adapter、KEDA 等)把自定义指标转换成 `external.metrics.k8s.io`。自定义指标适合描述「真正与业务负载同向」的信号,比如队列深度或并发会话数。

还有几个细节值得注意。HPA 的 `minReplicas` 为 1 时存在已知的扩容延迟问题(历史上有 30-60 秒的 stabilization delay),这是官方文档明确记录的;`behavior` 字段可以设置 `scaleDown` 的 `stabilizationWindowSeconds` 与 `policies` 来抑制抖动。此外,任何未在 Deployment 顶层写死 `replicas` 的清单,都应确保 HPA 是唯一的副本数控制方,否则两者会互相覆盖。

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: api-server
  namespace: prod
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api-server
  minReplicas: 3
  maxReplicas: 30
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70
    - type: Resource
      resource:
        name: memory
        target:
          type: AverageValue
          averageValue: 512Mi
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
        - type: Pods
          value: 2
          periodSeconds: 60

VPA 与 HPA 的资源冲突:先搞清楚它们抢的是哪几个字段

冲突点非常具体:HPA 通过 metrics API 计算 CPU 利用率时,分母是 pod 的 CPU requests。VPA 调整的正是 requests。两者叠加时,如果 VPA 因为观察到低使用率而下调 requests,HPA 读到的利用率分母随之变小,同样的 CPU usage 会算出更高的利用率,触发不必要的扩容——这被称为由 VPA 引起的 HPA 抖动。

Kubernetes 给出的官方建议是对同一个 workload 只保留一种 autoscaler 去管理资源。常见的组合是:HPA 负责副本数、VPA 负责 requests,但要把 VPA 限制在有独立资源画像的组件上(例如批处理 worker、离线索引任务),并且明确接受这两类组件共用同一套指标的抖动风险。

如果确实要同时用,实践中有几种降低冲突的做法。其一,给 VPA 设置合理的 `--recommender` 资源下限,让它不会把 requests 压到一个不切实际的低值。其二,只让 VPA 管理 memory 而不管理 cpu(VPA 支持 per-resource 的 updatePolicy)。其三,把扩缩容信号切换到 Custom Metrics,让 HPA 不再依赖 CPU requests 作为分母,从根本上切断这条耦合。

注意 VPA 的更新模式有三种。`Off` 只在 `vpa-status` 里给建议,不会改任何东西,适合先观察一段时间;`Initial` 只在 pod 创建时注入一次;`Auto` 会在 pod 运行时驱逐并重建 pod 以应用新资源,这会打断长连接与本地缓存,务必评估业务容忍度后再开启。

apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: batch-worker
  namespace: prod
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: batch-worker
  updatePolicy:
    updateMode: "Off"      # 先只观察,不自动改
    resourcePolicy:
      containerPolicies:
        - containerName: worker
          minAllowed:
            cpu: 500m
            memory: 512Mi
          maxAllowed:
            cpu: "4"
            memory: 8Gi
          controlledResources:
            - cpu
            - memory

什么时候应该加节点,而不是加副本

HPA 扩不上去,通常只有几种原因,每一种的解法不同。第一个是单节点资源上限:pod 已经把节点吃满,`maxReplicas` 还没到,但新 pod 一直 Pending。这时该加节点,而不是调 HPA 上限。区分方法很简单,看事件:`kubectl describe pod` 里的 `Insufficient cpu` 说明是节点不够,不是 HPA 不想扩。

第二个是 metrics 断链,目标值是 `<unknown>`。第三个是上限本身设得太保守(`maxReplicas` 太小或 QPS 限制拦住)。第四个是应用层面无法水平扩展,比如所有副本共享一个有状态的单点资源(同一个本地目录、同一把分布式锁、单个有状态数据库连接池上限)。

集群自动扩容有两条技术路线。Cluster Autoscaler 找现成或可新建的节点类型,受节点组规格限制;Karpenter 直接按 pod 的 requests 拉起最贴合规格的节点,粒度更细、对混合负载更友好,也支持更激进的 consolidation 回收闲置节点。选择依据是负载的多样性:负载单一用 Cluster Autoscaler 足够,负载规格差异大则 Karpenter 的收益明显。

不论选哪条,都要设保护:PodDisruptionBudget 保证缩容不把可用副本打到阈值以下,limits 与 requests 的比值留出余量,并给自动扩容设上限与预算告警。自动扩容最怕的不是不够快,而是半夜批量拉起节点把账单打穿,或者缩容把唯一的热副本收走。

# 1) 副本扩不动时先看事件,而不是先调 HPA
kubectl describe pod <pending-pod> | sed -n '/Events/,$p'
kubectl get hpa -n prod -o wide

# 2) 目标值是 <unknown>?查指标链路
kubectl top nodes
kubectl -n kube-system get deploy metrics-server

# 3) 缩容保护
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: api-server-pdb
  namespace: prod
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: api-server

官方参考来源

下方为命令对应的官方权威文档,供你核对最新用法与深入查阅。