Kubernetes HPA 实战:Pod 水平自动扩缩容

当流量在凌晨突增三倍,你是被报警叫醒手动 kubectl scale,还是希望集群自己把 Pod 拉起来?Kubernetes HPA(Horizontal Pod Autoscaler,水平自动扩缩容)正是解决这个问题的原生方案:它根据 CPU、内存或自定义指标,自动调整 Deployment 的副本数,让服务在高峰自动扩容、低谷自动缩容。本文从原理到 yaml 配置,带你把 HPA 真正跑起来,并避开内存指标、缩容抖动等常见坑。

为什么需要 HPA

固定副本数(比如永远 3 个 Pod)要么在流量高峰扛不住、要么在深夜白白烧钱。kubectl scale 手动扩缩依赖人肉判断,既慢又容易遗漏。HPA 把”扩缩容”这件事变成声明式配置:你告诉集群”CPU 超过 60% 就加 Pod,最少 2 个、最多 10 个”,剩下的交给控制器循环。它和 Kubernetes 的 Deployment / Pod 基础 配合,是绝大多数在线服务弹性能力的基石。

HPA 工作原理:指标从哪来

HPA 控制器默认每 15 秒轮询一次 Metrics API,拿到当前 Pod 的平均利用率,再按目标值反推期望副本数:期望副本 = ceil(当前副本 × 当前指标 / 目标指标)。这里的”指标”有三个来源层级,理解它们决定了你能用 HPA 做什么:

指标类型来源典型场景
Resource(CPU/内存)metrics-server(核心指标)Web 服务随负载扩缩
Pod / Object(自定义)Prometheus Adapter 等(Custom Metrics)按 QPS、队列长度扩缩
ExternalKEDA 等(外部事件)按 Kafka 积压、消息数扩缩

最常见也最稳妥的是第一层的 Resource 指标,它依赖 metrics-server 把每个节点的 kubelet 指标聚合起来。没有它,HPA 的状态会永远卡在 unable to fetch metrics。

前置准备:部署 metrics-server

大部分托管集群(ACK、EKS、GKE)已预装 metrics-server;自建集群用下面一条命令即可:

# 标准安装(国内可换用阿里云镜像对应的 yaml)
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml

# 若 kubelet 使用自签证书,需加 --kubelet-insecure-tls 参数
kubectl edit deployment metrics-server -n kube-system
# 在 args 中追加:
#   - --kubelet-insecure-tls

装好后验证指标是否就绪——能列出节点和 Pod 的实时 CPU/内存,说明采集链路通了:

kubectl top nodes
kubectl top pods -n default

# 期望输出示例:
# NAME          CPU(cores)   MEMORY(bytes)
# node-1        320m        1800Mi

基于 CPU 的 HPA(核心实战)

先准备一个可被压测的 Deployment,再挂上 HPA。注意:HPA 要求 Pod 必须配置 resources.requests,否则控制器算不出利用率百分比。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 2
  selector:
    matchLabels: { app: web }
  template:
    metadata:
      labels: { app: web }
    spec:
      containers:
      - name: web
        image: nginx:1.27
        resources:
          requests: { cpu: "100m", memory: "64Mi" }
          limits:   { cpu: "500m", memory: "128Mi" }

接着创建 HPA:目标平均 CPU 利用率 60%,副本区间 2–10。

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: web-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: web
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 60
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300   # 缩容前观察 5 分钟,避免抖动

用 kubectl get hpa 观察实时状态,TARGETS 列会显示当前利用率 / 目标值:

真实压测时的曲线通常是这样的:压测开始后 30 秒,利用率从 45% 爬到 85%,HPA 在第 2 个采集周期把副本从 2 拉到 4;随着 Pod 变多,单 Pod 平均 CPU 回落到 62%,于是稳定在 4 个副本。停掉压测后,利用率跌回 20%,但因为有 300 秒缩容稳定窗口,副本不会立刻减半,而是每分钟最多退 1 个,5 分钟后才回到 2。这条”快扩慢缩”的曲线,正是 behavior 字段在起作用。

kubectl get hpa web-hpa -w
# NAME      REFERENCE        TARGETS   MINPODS   MAXPODS   REPLICAS
# web-hpa   Deployment/web   45%/60%   2         10        2

# 压测制造 CPU 负载(换你的 Service 地址)
hey -z 60s -c 50 http://web.default.svc.cluster.local/
kubectl get hpa web-hpa
# 几轮后 REPLICAS 会从 2 涨到 4、6……直到利用率回落到 60% 附近

基于内存的 HPA(以及为什么它很危险)

内存和 CPU 不同:内存不会随请求结束而”自然回落”,Go/Java 进程常常占用后不释放。因此用内存做扩容指标尚可,做缩容指标极易误杀正在服务的 Pod。如果一定要用,务必配合较长的缩容稳定窗口:

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: cache-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: cache
  minReplicas: 2
  maxReplicas: 8
  metrics:
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 75   # 仅在内存紧张时扩容,缩容靠 behavior 兜底

经验法则:内存 HPA 只用于”扩容抗住”,缩容节奏交给 stabilizationWindowSeconds 拉长,避免 Pod 被频繁杀掉导致连接抖动。

自定义指标 HPA:用 Prometheus Adapter 接入 QPS

很多服务 CPU 没满但连接数/QPS 已经爆了,这时 Resource 指标不够用。通过 Prometheus 监控 采集指标,再用 prometheus-adapter 把 PromQL 暴露成 Custom Metrics API,HPA 就能按 QPS 扩缩:

# prometheus-adapter 的 ConfigMap 片段:把 http 请求率映射成自定义指标
rules:
- seriesQuery: 'http_requests_total{namespace!="",pod!=""}'
  resources: { overrides: { namespace: {resource: "namespace"}, pod: {resource: "pod"} } }
  name: { matches: "http_requests_total", as: "requests_per_second" }
  metricsQuery: 'sum(rate(http_requests_total{<<.LabelMatchers>>}[1m])) by (<<.GroupBy>>)'

adapter 注册成功后,HPA 直接引用这个指标名:

metrics:
- type: Pods
  pods:
    metric:
      name: requests_per_second
    target:
      type: AverageValue
      averageValue: "500"   # 每个 Pod 平均承载 500 QPS

扩缩容行为精细控制(behavior 字段)

behavior 是 HPA 最被低估的字段。它能分别控制扩容(scaleUp)和缩容(scaleDown)的速度与稳定窗口,直接决定线上是否”抖动”。常用参数如下:

参数作用建议值
stabilizationWindowSeconds缩容前观察期,避免指标瞬时回落就杀 Pod300(5 分钟)
scaleUp.selectPolicy多指标时取最大/最小扩容量Max
scaleDown.policies[].periodSeconds缩容速率,如 60s 内最多减 1 个100%/60s 或 1/60s
maxReplicas / minReplicas副本上下限,防止无限扩张烧钱按容量评估
behavior:
  scaleUp:
    stabilizationWindowSeconds: 0
    selectPolicy: Max
    policies:
    - type: Percent  # 一次最多扩 100%
      value: 100
      periodSeconds: 15
  scaleDown:
    stabilizationWindowSeconds: 300
    selectPolicy: Min
    policies:
    - type: Pods     # 5 分钟内最多缩 1 个,平稳退场
      value: 1
      periodSeconds: 300

常见坑与排障清单

现象根因修复
HPA 状态 unknown / unable to fetch metricsmetrics-server 未装或 kubelet 证书问题部署 metrics-server,加 –kubelet-insecure-tls
副本数不变Pod 未设 resources.requests,算不出利用率给容器补 requests.cpu
扩容后立刻缩回缩容稳定窗口太短,指标抖动scaleDown.stabilizationWindowSeconds 调到 300+
内存 HPA 频繁重启 Pod进程不释放内存,被误判可缩只做内存扩容,缩容交给长窗口
自定义指标找不到adapter 未注册到 Custom Metrics APIkubectl get –raw /apis/custom.metrics.k8s.io 验证

把 HPA 接进 Grafana 监控面板,把副本数与 CPU/QPS 画在同一张图,能最直观地确认扩缩容是否按预期工作。流量入口侧配合 Nginx 反向代理做负载均衡,扩出来的 Pod 才能被均匀分摊。整套弹性发布链路也能用 GitHub Actions CI/CD 自动提交 yaml 上线。

节点层弹性:HPA 之外别忘了 Cluster Autoscaler

HPA 只解决”Pod 数量”,当副本涨到 maxReplicas 仍不够、或节点资源被占满时,新 Pod 会卡在 Pending。这时需要 Cluster Autoscaler(CA) 在节点层扩容:当调度器找不到足够资源的节点,CA 自动往集群加机器。HPA 管”横向加 Pod”,CA 管”横向加节点”,两者配合才是完整的弹性闭环。在阿里云 ACK 上,CA 通过节点池的自动伸缩开关启用,并依赖 Pod 设置了正确的 resources.requests 才能算出资源缺口。

一个常见反模式是:只配了 HPA 却忘了给节点池开 CA,流量一来 Pod 疯狂扩容却全部 Pending——利用率曲线好看,服务照样挂。上线前务必确认这两条链路都通。

上线前 Checklist

检查项为什么
Pod 已设 resources.requests否则 HPA 无法计算利用率百分比
metrics-server 就绪,kubectl top 正常指标采集链路的前置条件
缩容稳定窗口 ≥ 300s避免流量毛刺误杀正在服务的 Pod
maxReplicas 按容量评估设定防止副本无限扩张导致成本失控
节点池 CA 已开启(高负载场景)Pod 扩容后需要节点资源承接

小结

HPA 是 Kubernetes 弹性的第一道闸:先用 metrics-server + CPU 指标跑通基础扩缩,再用 behavior 字段压住缩容抖动,最后在 QPS 类场景用 Prometheus Adapter 接自定义指标。记住三件事——Pod 必须设 resources.requests、内存指标只扩不急缩、缩容稳定窗口宁长勿短。弹性能力本地的容器基座可以先用 Docker Compose 一键编排 验证,再上集群接管。

上一篇 TypeScript 装饰器实战:从类装饰器到依赖注入
下一篇 Coze 实战:零代码搭建 AI 智能体工作流
最新评论
Docker 网络模式详解:bridge/host 生产选型 - FSData
Docker 网络模式详解:bridge/host 生产选型 - FSData
8月20日
[…] 注意最后那条:把端口绑定到 127.0.0.1,再由宿主机上的 Nginx 反代进来,比直接 -p 80:80 暴露安全得多,也方便统一做
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
8月20日
[…] Pinia 的工程化用法,可以参考我们之前的实战文章 《Vue3 组合式 API + Pinia:状态管理从入门到实战》,那里覆盖了 Sto
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
8月19日
[…] 状态,点一下就能起停容器,配合我们讲过的 Docker Compose 编排 […]
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
8月19日
[…] 缓存三大坑的本质都是”缓存失效导致请求穿透到数据库”。记住口诀:穿透用布隆、击穿用锁、雪崩用错峰。配合合理的过期策略
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
8月19日
[…] Docker 镜像部署。结合我们之前写的 Docker Compose 一键编排实战,可以在 CI […]