☰
Kubernetes HPA 核心机制与生产实践:从指标原理到排坑指南
2026/10/15 6:58:08 网站建设 项目流程

HPA 这个词只要搞过 Kubernetes 生产环境的人基本都绕不过去。刚开始用的时候,我以为它就是一段 YAML 加上一个 metrics-server 的事,直到线上流量突然翻倍,Pod 却迟迟不扩容,我才真正意识到:不了解 HPA 内部的原理,你根本没法在关键时刻相信它。这次就把我对 Kubernetes Horizontal Pod Autoscaler(简称 HPA)的原理理解完整拆一遍,从它为什么存在、怎样工作、怎样配置,到生产环境里那些坑和排查方法,全部说透。内容偏实操,但原理部分也不跳过,适合已经把 Kubernetes 跑起来、正在琢磨怎么让业务自动伸缩的人。

1. HPA 是什么:从"为什么需要自动伸缩"讲起

1.1 手动扩容的困局

刚接触 Kubernetes 时,我们服务的副本数基本靠人肉调整。业务上线前评估一下峰值,给 Deployment 写死一个固定的 replicas。问题是互联网业务的流量曲线根本猜不准,白天和晚上不一样,活动日和普通日不一样,有时候上游一个服务抖动,流量瞬间就上来了。扩容靠值班同学盯监控,看到 CPU 涨到 80% 再手动执行kubectl scale deployment my-app --replicas=10,反应慢不说,容易扩过头,把集群资源打满,然后又要手动缩容。遇到深夜突发流量,等跑到电脑前操作的时候,用户已经开始报错了。

这种局面下,HPA 解决的不只是"自动",而是把伸缩这件事变得可预期、可管理。它根据观察到的负载指标,持续计算一个期望副本数,然后通过 Kubernetes 控制面自动修改 Deployment、StatefulSet 这类工作负载的副本数。你不需要人为盯着监控,也不需要提前精确预估容量,只需要告诉它"按 70% 的 CPU 利用率来扩缩",剩下的事它自己循环去做。

1.2 HPA 的核心目标与基本概念

HPA 的全称是 Horizontal Pod Autoscaler,水平 Pod 自动扩缩器。它的作用方向是横向扩容,也就是增加 Pod 的副本数量,而不是给单个 Pod 增加 CPU 或内存这种"垂直扩容"。水平扩展的核心理念是把无状态服务的负载分摊到多个副本上,副本多了,每个副本分到的流量就小,响应速度就上去。

HPA 本身是 Kubernetes 控制器的一部分,就像 Deployment 控制器管理 ReplicaSet,HPA 控制器也拥有自己的控制循环。它通过 API Server 读取你配置的伸缩规则,定期地获取 Pod 的度量数据,按照固定算法算出一个目标副本数,再把结果写回到工作负载的scale子资源上。整套机制里你只需要关心几个概念:

  • scaleTargetRef:要伸缩的目标负载,比如指向一个 Deployment。
  • metrics:伸缩依据的指标数组,可以是资源指标(CPU、内存),也可以是自定义指标。
  • minReplicas和maxReplicas:副本数的下限和上限,防止伸缩到失控。
  • behavior:可选配置,用来控制扩容和缩容的具体行为策略。

明白了这几个词,你已经理解了 HPA 大半。接下来真正重要的部分在于,它到底靠什么数据、按什么公式来决策。

2. HPA 的工作机制拆解:从指标采集到副本调整

2.1 控制循环:Reconciliation Loop

Kubernetes 里的控制器基本都是"期望状态"驱动。HPA 控制器每隔一段时间(默认通过--horizontal-pod-autoscaler-sync-period控制,通常是 15 秒)就会执行一次调和循环。这个循环做的事情可以拆成四步:

  1. 获取目标工作负载的当前副本数,以及它声明的资源请求。
  2. 获取所有 Pod 上绑定的监控指标。
  3. 代入算法计算出期望副本数。
  4. 如果期望副本数和当前副本数不一致,就通过scale子资源更新副本数。

这四步看起来简单,但每一步都有干扰项。比如 Pod 还没 Ready,指标就可能是 0;比如某个 Pod 被驱逐了,它的历史指标残留还会影响计算;又比如指标数据延迟了几分钟,那扩缩决策就基于旧数据。生产环境里 HPA 表现得"迟钝",多半就是这些环节的延迟叠加的效果。

调和的本质是"比较现状和期望,驱向期望"。它不像事件驱动系统那样来了一个请求就立刻触发扩容,而是一个采样-计算的周期过程。所以在设计 HPA 时,你需要接受一个事实:它是离散反馈控制,不是实时响应系统。流量瞬间飙升时,通常会有数十秒到几分钟的滞后,这一部分依赖 Pod 启动速度和指标聚合速度。

2.2 指标来源与 Metrics Server

要算期望副本数,必须有指标输入。Kubernetes 1.8 之前,HPA 只能读 CPU,指标来源是 Heapster。现在主流的指标源有两类:

  • 资源指标:通过 Metrics Server 提供,它从 kubelet 上的 Summary API 采集每个容器的 CPU 和内存使用量,然后聚合成 Pod 级别的指标。
  • 自定义指标:通过 Metrics API 兼容的适配器提供,比如 Prometheus Adapter、外部指标(AWS CloudWatch、Google Cloud Monitoring)等。

Metrics Server 本身也是运行在集群里的一个 Deployment,它每隔一定时间(默认 15 秒到 1 分钟)通过 kubelet 抓取数据,然后缓存在内存里。HPA 控制器调用的是metrics.k8s.io/v1beta1这个 API,如果你集群里没装 Metrics Server,执行kubectl get hpa时指标会显示<unknown>,HPA 也就无法工作。

这里有很关键的一个点:CPU 利用率指标用的是"使用量除以请求值",而不是节点的物理 CPU 总量。举例来说,一个 Pod 声明了requests.cpu=500m,当前实际用了 250m,那么它的 util 就是 50%。这个"请求值"就是整个 HPA 计算逻辑的锚点,后面还会详细说。

至于自定义指标,它走两种 API:custom.metrics.k8s.io和external.metrics.k8s.io。前者通常与 Pod 对象直接绑定,比如根据 Pod 每秒请求数 QPS 扩容;后者不绑定 Pod,比如根据队列积压消息数来扩容。适配器本质就是一组 APIService,把外部监控数据的查询结果翻译成 Kubernetes API 对象。

2.3 算法:期望副本数计算与速率限制

HPA 官方文档给了一套很直白的计算公式:

desiredReplicas = ceil[ currentReplicas * ( currentMetricValue / desiredMetricValue ) ]

举个例子:当前副本数是 4,当前 CPU 实际平均利用率是 95%,目标利用率是 50%,那么期望值就是ceil(4 * 95 / 50) = ceil(7.6) = 8。如果当前平均利用率是 20%,那就是ceil(4 * 20 / 50) = ceil(1.6) = 2。注意它不会小于minReplicas,也不会大于maxReplicas。

但公式只是基础,真正的控制器还内置了几条限速规则来防止震荡:

  • 扩容速率:默认情况下,扩容最多每 15 秒增加一倍副本数(通过--horizontal-pod-autoscaler-tolerance和--horizontal-pod-autoscaler-upscale-delay等参数控制)。也就是说,即使计算出来需要从 4 个变 100 个,第一次只会加到 8 个,下一次循环再加到 16 个。
  • 缩容速率:缩容更保守,默认 Pod 数量下降的速度由--horizontal-pod-autoscaler-downscale-stabilization控制,默认 5 分钟。意味着就算当前指标显示只需要 2 个副本,控制器也要观察 5 分钟,确认不是瞬时抖动后才允许缩。
  • 容忍度:如果currentMetricValue / desiredMetricValue在 1.0 左右的容忍区间内(默认 0.1,即 90%~110%),HPA 不会调整副本数。这个容差避免了频繁微调。

理解了这些限速规则,你就能解释很多现象。比如某次大促扩容很慢,不是因为 HPA 坏了,而是它每一轮最多只能翻倍,逻辑上可能要经过 3~4 轮,每轮间隔 15 秒,叠加指标采集延迟,实际扩容成功可能要 1 分钟以上。反过来,流量下来之后缩容要等 5 分钟,这也是稳定策略,防止副本数像心电图一样上下乱跳。

3. 核心配置与实操示例

3.1 配置清单关键字段

HPA 在 Kubernetes 里是一个资源对象,最常用的 API 版本是autoscaling/v2(Kubernetes 1.23+ 稳定可用)。一个最典型的 YAML 长这样:

apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: web-hpa namespace: default 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: scaleUp: stabilizationWindowSeconds: 0 policies: - type: Percent value: 100 periodSeconds: 15 scaleDown: stabilizationWindowSeconds: 300 policies: - type: Pods value: 1 periodSeconds: 60

字段没什么难度,但要说明一点:minReplicas和maxReplicas不只是数字,它们也是保护边界。min 设 1 的风险在于,如果某个节点故障导致 Pod 重建,一个孤零零的副本在重新调度前可能等于没有;max 设太高则可能把整个集群资源耗尽,所以需要结合集群容量评估。

metrics数组里可以写多个指标。HPA 会分别计算每个指标的期望副本数,然后取最大值作为最终副本数。比如 CPU 期望 8 个,内存期望 12 个,那就按 12 个来。这种多指标模式适合那些 CPU 不高但内存吃紧的应用。

3.2 示例:基于 CPU 的 HPA

下面是我在本地环境实际验证过的一段操作过程,直接照着做就行。

第一步,确认集群里已经装了 Metrics Server:

kubectl get deployment metrics-server -n kube-system

如果没有,就用官方 YAML 装一下。

第二步,准备一个测试 Deployment,记得一定要给容器写resources.requests。没有请求值,HPA 根本没法算利用率:

apiVersion: apps/v1 kind: Deployment metadata: name: cpu-demo spec: replicas: 1 selector: matchLabels: app: cpu-demo template: metadata: labels: app: cpu-demo spec: containers: - name: main image: myapp:v1 resources: requests: cpu: 200m

第三步,创建 HPA:

kubectl apply -f hpa-cpu.yaml

然后查看状态:

kubectl get hpa cpu-demo

如果一切正常,输出里TARGETS列会显示类似10%/60%。10%是当前所有 Pod 的 CPU 利用率平均值,60%是你设定的目标。想要验证扩容,就用一个压测工具对应用制造 CPU 压力,过一会儿你会看到副本数开始上涨。

这里有个操作细节:本地压测时,如果负载比较小,其实很难撑过容忍度区间。比如目标 60%,当前已经 55%,按公式计算1 * 55/60 = 0.9,接近 1.0,HPA 会选择不动。如果你想快速看到效果,可以把目标值设低一点,比如averageUtilization: 30。

3.3 示例:自定义指标(Prometheus + adapter)

要说生产环境最有用的 HPA 玩法,还得是自定义指标。常见的套路是用 Prometheus 抓取应用指标,再部署一个 prometheus-adapter,让它把 Prometheus 查询结果暴露成custom.metrics.k8s.io接口。

适配器的核心是一个ConfigMap,里面写规则。例如想根据 HTTP 请求 QPS 扩容,可以配置一个 series 查询:

rules: - seriesQuery: 'http_requests_total{namespace!="",pod!=""}' resources: overrides: namespace: { resource: "namespace" } pod: { resource: "pod" } name: matches: "http_requests_total" as: "http_requests_per_second" metricsQuery: 'sum(rate(http_requests_total{<<.LabelMatchers>>}[2m])) by (<<.GroupBy>>)'

然后 HPA 的 metrics 字段写成:

metrics: - type: Pods pods: metric: name: http_requests_per_second target: type: AverageValue averageValue: 100

type: Pods表示指标是按 Pod 聚合的,averageValue: 100表示每个 Pod 的平均 QPS 目标为 100。如果当前每个 Pod 达到 300 QPS,副本数就会相应增加。

这个方案成熟,但排查起来也要注意,适配器查询的速度和 Prometheus 的 step 对齐问题很容易踩。适配器默认会把 query 包装成last 6 minutes的区间并取最大值,所以你的rate函数窗口如果太短,可能取不到平滑数据,导致指标忽高忽低。

3.4 配置项细节:behavior、scaleTargetRef、metrics

behavior是配置扩缩容策略最重要的入口。它里面有scaleUp和scaleDown两个部分,各有stabilizationWindowSeconds(稳定窗口)、selectPolicy(多策略选择方式)和policies(策略列表)。

policies支持两种类型:Pods(允许改变的副本数)和Percent(允许改变副本数的百分比)。每个策略还有个periodSeconds,表示在窗口内生效的时长。比如:

behavior: scaleUp: policies: - type: Percent value: 100 periodSeconds: 15

这条规则的意思是,每 15 秒最多允许扩大 100%,也就是翻倍。如果你还想限制单次最多加 4 个,可以在 policies 里再加一条Pods类型,然后selectPolicy: Min让控制器取两条策略计算结果里更小的那个。

在生产环境,我建议把缩容的stabilizationWindowSeconds调大一些,比如 600 秒甚至 900 秒,尤其是在下游有任务队列或者数据库连接池的场景。缩容太快会导致正在处理的请求被掐断,慢一点更安全。对于扩容稳定窗口则通常设 0,因为业务流量上升时每一秒都值钱。

4. 生产环境注意事项与常见坑

4.1 为什么"抖动":稳定窗口与冷却时间

我踩过最经典一个坑:某个服务流量在一天内有几个持续半小时的高峰,HPA 设置了基于 QPS 的扩容目标,结果高峰一到,副本数猛涨,高峰一过,副本数又开始降。由于 Pod 启动比较慢,业务压力又上来了,于是反复横跳:8 个变 16 个,16 个又降回 8 个。每次缩容需要等 Pod Terminating,期间新请求仍然会路由过来,导致 Pod 数量长时间处于"高位震荡"而不是稳定在峰值。

后来我调大了scaleDown.stabilizationWindowSeconds到 900 秒,同时把缩容策略从Percent改成一次最多缩 1 个 Pod。这样即使指标瞬间降下来,缩容也是一步一步走的,给了负载评估足够的时间,基本不再震荡。

另外要注意,缩容还有一个默认的--horizontal-pod-autoscaler-downscale-delay参数,默认 5 分钟,这是在你设置了稳定窗口之前就有的保护。所以不要以为稳定窗口设小就一定会立刻缩,还要看控制平面参数。

4.2 资源请求值:HPA 的"分母"

基于 CPU 或内存的 HPA,分母永远是resources.requests,不是节点容量,也不是容器 limit。这是一个很容易被新同学忽略的细节。

假设一个 Pod 的requests.cpu=1(1000m),但limits.cpu=4,实际它用了 2 核。按 HPA 的 CPU 利用率算法,它是2000m / 1000m = 200%。如果目标利用率 60%,那期望副本数会很高。很多人觉得"我明明设置 limits=4,为什么 2 核也算 200%?"因为你设的请求值是 1,HPA 内部只看 utilisation 比值。

所以一个重要的调优经验是:不要随意把 requests 设得远低于业务实际使用量。如果经过压测发现服务的平均 CPU 使用就是 800m,那 requests 最好设置在 800m 左右。设低虽然可以压缩资源占用,但会让 HPA 反应过于激进,导致副本数浮动大。理论上你甚至可以把 requests 设得高于实际使用量来"稀释" HPA 的扩容量,但这违背了资源调度的初衷,一般不建议。合理做法是定期根据监控数据重新校准 requests。

4.3 readinessProbe 与就绪、毛刺

HPA 扩容后,新增的 Pod 什么时候开始接收流量,取决于 readinessProbe。这个探针如果配置不合理,可能出现两种情况:一种是新 Pod 明明可以接收流量但探针太严格,导致它一直 NotReady,HPA 检测到指标没变化(新 Pod 无流量),继续再扩一批;另一种是探针太宽松,新 Pod 还没初始化好就进 Service 后端,把个别请求打挂,造成指标平均线异常偏低或偏高。

这里面还有一层更隐性的问题:HPA 计算指标平均值时,只统计已就绪 Pod吗?实际上指标来源是所有的目标 Pod,而就绪状态会影响流量分配,但并不直接过滤指标数据。如果你新扩容的 Pod 还没就绪,它们可能没有业务指标上报,导致平均指标被拉低,这会影响 HPA 的判断方向。比如 CPU 平均使用率因为新加了很多空载 Pod 从 90% 降到 40%,而实际老 Pod 仍然高负载,HPA 可能会误判为不需要扩容。这种毛刺问题在自定义指标里尤其常见。

解决思路是确保 Pod 启动阶段能够上报一个"保守初始值",或者让适配器在聚合时只统计就绪 Pod。但前者依赖应用配合,后者需要自定义 adapter 的规则。对于绝大多数团队,我更推荐在业务入口的监控埋点上下功夫,把 QPS、延迟这类业务指标作为 HPA 依据,比单纯看 CPU 更贴合实际。

4.4 自定义指标排查技巧

自定义 HPA 出现问题,第一件事永远是确认 API 有没有返回数据。执行:

kubectl get --raw "/apis/custom.metrics.k8s.io/v1beta1" | jq

如果这个接口都没有注册对应的资源名,就需要检查 adapter 的 Deployment 是否正常。接下来针对具体指标查询:

kubectl get --raw "/apis/custom.metrics.k8s.io/v1beta1/namespaces/default/pods/*/http_requests_per_second" | jq

这一步能直接看到适配器返回的每个 Pod 的指标值和时间戳。如果返回[]空列表,说明 Prometheus 里没有数据;如果返回的数据里有 NaN,说明计算规则里出现了除零;如果时间戳不是最近,说明采集有延迟。

还有个容易被忽视的坑是metricName的大小写规范。adapter 的规则中as字段定义的名字通常会被转换成符合 Kubernetes API 要求的格式,比如连字符会被转换成下划线。如果你的 HPA 里写的 name 和 adapter 暴露出来的名字不一致,HPA 会报unable to fetch metrics from ...,但 Prometheus 本身监控一切正常,很容易让人怀疑 adapter 版本问题。

5. 从 v1 到 v2:演进与生态

5.1 autoscaling/v1 vs v2

早些年大家用的都是autoscaling/v1,它只有一个字段,只能设置 CPU 平均利用率:

apiVersion: autoscaling/v1 kind: HorizontalPodAutoscaler spec: scaleTargetRef: ... minReplicas: 1 maxReplicas: 10 targetCPUUtilizationPercentage: 80

这个版本在 Kubernetes 1.6 引入,简单但太死板,不支持自定义指标,也不支持多指标和缩放策略。到了 1.12 左右,autoscaling/v2beta2出现,后续稳定为autoscaling/v2,增加了metrics、behavior等字段。现在的文档和示例基本都是 v2,如果你的项目里还看到 v1 的 YAML,建议迁移。v1 依然存在是为了兼容旧 API,控制器的计算规则也相同,但无法享受新特性。

从 v1 到 v2 最大的变化不是语法,而是伸缩策略从"计算后立即生效"变成了"计算后还要受策略约束"。v2 里你可以让 HPA 即使算出来需要缩 10 个,实际也只每 5 分钟缩 1 个;可以单独针对首次扩容做加速;可以指定多策略取最大或最小。这些都大大提高了自动伸缩的可控性。

5.2 多指标组合

配置多个指标时,HPA 对每个指标独立计算期望副本数,然后取max。这个设计容易引起一个反直觉行为:某指标由于瞬时毛刺被放大,会直接影响最终结果。比如 CPU 正常需要 4 个副本,但某个内存指标因为缓存命中率降低,期望副本数算成了 20,那 HPA 会直接将副本扩大到 20。过了一段时间缓存恢复,又缩回来。这种"按最大者"的策略在某些组合下会造成资源浪费。

应对方式可以借助behavior里的缩容稳定窗口削弱突变影响,也可以在监控层面为每个指标设置合理的聚合方式。比如 Prometheus Adapter 里对指标可以使用maxValue、averageValue等聚合。更精细的办法是使用ContainerResource类型,直接针对容器而不是 Pod 级别,这在多容器 Pod 里很常见。

多指标组合还有一个最佳实践:不要把所有指标都设为"硬性伸缩依据",有的指标适合只作为观测,不作为决策。HPA 目前没有weight概念,没有"主次指标权重"的设计。如果你真的想让某指标优先,建议拆分成多个 HPA 指向同一个 Deployment,但要非常小心副本数冲突。实际上 Kubernetes 不允许两个 HPA 同时管理同一个负载,所以常规做法还是把最核心的指标放进同一个 HPA 的 metrics 数组。

5.3 与 Cluster Autoscaler 的配合

HPA 负责调整应用副本数,Cluster Autoscaler(CA)负责调整集群节点数。二者配合得好,就是一套完整的弹性伸缩方案。

典型流转路径是:业务流量增长 → 指标涨 → HPA 计算期望副本数 → 副本数增加 → 新 Pod 调度时发现集群资源不足 → Pod Pending → CA 检测到有 Pod 无法调度 → CA 触发节点扩容 → 节点就绪 → Pod 调度成功 → 业务恢复。这整个过程看起来简单,但涉及多个控制器的时间尺度:HPA 每次 15 秒循环,CA 每次 10 秒循环,节点启动几十秒,Pod 拉镜像几秒到几分钟。端到端延迟往往在数分钟级别。

所以如果你的业务对扩容速度很敏感,还需要叠加 PDB、preStop hooks 和节点池弹性策略。另一个常见问题是你给 HPA 设了maxReplicas: 20,集群只有 3 个节点,每个节点只能容纳 5 个 Pod,但你没有装 CA,那么 HPA 的扩容会停在 Pending 状态,导致 Pod 一直无法启动。排查时要同时看 HPA 状态和 Pod 事件,别只盯着kubectl get hpa的 REPLICAS 列。

生产环境里我还建议把 CA 的扩容策略和 HPA 的 maxReplicas 联动起来。比如给不同应用设置不同的 nodeSelector 和 resource requests,让突发扩容的 Pod 落在专用节点池,避免和在线业务互相争抢。这些都是构架层面的取舍,但 HPA 本身并不感知节点状态,它只负责"按指标改 replica",功过都在这个边界之内。

结尾:一点个人的实操体会

HPA 用了几年,我最大的体会是它属于"策略简单、实际复杂"的组件。文档上写清楚公式,但真正让它稳定工作,靠的是对指标质量的控制和对缩扩容行为的约束。我现在上生产前一定会做一套"扩容演练":人为制造高负载,观察 HPA 从发现指标变化到执行扩容的时间,以及 Pod 从 Pending 到 Ready 的时间,记录下来作为容量预案的一部分。这样上线后心里就有底,知道最差的情况延迟是多少。

最后再分享一个实用小技巧:如果线上 HPA 出现了指标正常但不扩容的情况,优先检查kubectl describe hpa里 Events 的内容,它会直接给出"FailedGetResourceMetric"或"unable to get metric"类似的原因。大多数时候不是算法问题,而是指标链路不通或资源请求值没配。先把这条链路打通,再谈调优,顺序别反过来。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询