K8s Lab 把当前仓库文档整理成一个可阅读的网页站点

Repository Reading Site

第十二课:HPA、自动扩缩容、指标链路与副本伸缩原理

前一课我们刚把下面这些概念讲清楚: 这时候就该进入另一条很关键的主线了: 很多人学 HPA 的方式很危险: 这远远不够。 如果你以后想带团队做平台,或者能独立负责一套公司级项目架构,你至少要把下面这些问题讲明白: 1. HPA 看的到底是什么指标 2. 指标从哪里来,多久采一次 3. 为什么 HPA 经常先显示 `<unknown>` 4. 为什么基于 CP

Markdown12-第十二课-HPA-自动扩缩容-指标链路与副本伸缩原理.md2026年4月10日 05:54

第十二课:HPA、自动扩缩容、指标链路与副本伸缩原理

为什么这一课必须接在资源模型之后

前一课我们刚把下面这些概念讲清楚:

  • requests 是调度账本
  • limits 是运行时约束
  • CPU 是可压缩资源
  • memory 是不可压缩资源
  • OOMKilledEviction 不是一回事

这时候就该进入另一条很关键的主线了:

Kubernetes 到底怎样根据“真实负载”自动增减副本。

很多人学 HPA 的方式很危险:

  • 看一个 autoscaling/v2 的 YAML
  • 敲一个 kubectl autoscale
  • 看到副本从 1 变成 3
  • 就以为自己懂了

这远远不够。

如果你以后想带团队做平台,或者能独立负责一套公司级项目架构,你至少要把下面这些问题讲明白:

  1. HPA 看的到底是什么指标
  2. 指标从哪里来,多久采一次
  3. 为什么 HPA 经常先显示 <unknown>
  4. 为什么基于 CPU 利用率的 HPA 离不开 requests
  5. 为什么副本明明扩了,服务却未必变快
  6. 为什么 HPA 扩容后 Pod 还是可能 Pending
  7. HPA、Deployment、scheduler、kubelet、Cluster Autoscaler 各自分工是什么

这课的目标不是“会用 HPA”,而是把自动扩缩容这条控制链路真正看懂。


先建立全景图:Kubernetes 里到底有几种“扩缩容”

你以后脑子里要同时有四种不同层次的调节手段。

第一种:应用自己处理更多并发

例如:

  • Go 服务增加 goroutine
  • Java 线程池扩大并发
  • Nginx 增加 worker

这是应用内部并发能力,不是 Kubernetes 扩容。

第二种:HPA

HorizontalPodAutoscaler 做的是:

横向扩缩容,也就是增减 Pod 副本数。

它不改节点数量,不改单个 Pod 的资源配额。

第三种:VPA

VerticalPodAutoscaler 做的是:

纵向调整单个 Pod 的资源请求和限制。

比如把 requests.cpu100m 调成 500m

它解决的是“单 Pod 规格不合适”,不是“Pod 数量不够”。

第四种:Cluster Autoscaler

它解决的是:

集群节点不够时,是否自动加机器;节点空闲时,是否自动减机器。

所以你以后要牢牢记住:

  • HPA 调的是 replicas
  • VPA 调的是 requests/limits
  • CA 调的是 nodes

它们不是一个东西。


HPA 在整条控制链路里处于什么位置

HPA 不是直接看容器进程,也不是直接去机器上 top

它的工作链路大致是:

  1. 容器在节点上运行
  2. kubelet / cAdvisor 采集容器资源使用数据
  3. metrics-server 周期性向 kubelet 拉取指标
  4. metrics-server 通过 metrics.k8s.io API 暴露资源指标
  5. kube-controller-manager 里的 HPA controller 读取指标
  6. HPA controller 计算目标副本数
  7. HPA 修改目标工作负载的 scale 子资源
  8. Deployment / ReplicaSet 按新副本数创建或删除 Pod
  9. scheduler 再把新增 Pod 调度到节点

你要注意这里的职责分离:

  • HPA 负责“算要多少副本”
  • Deployment 负责“把副本变出来”
  • scheduler 负责“把新 Pod 放到哪台节点”
  • kubelet 负责“把容器真正跑起来”

这就是 Kubernetes 一直强调的控制器解耦思想。


这套集群里的真实指标链路

这一课我没有只讲理论,而是核对了这套真实集群里的链路。

1. metrics.k8s.io API 确实存在

我看到:

KUBECONFIG=~/.kube/config-k8s-lab kubectl api-resources --api-group=metrics.k8s.io

输出包含:

  • NodeMetrics
  • PodMetrics

这说明集群里资源指标 API 已经注册成功。

2. metrics-server 已安装并工作

我检查了 kube-system/metrics-server Deployment,看到参数里有:

  • --metric-resolution=15s
  • --kubelet-use-node-status-port
  • --kubelet-insecure-tls

这意味着:

  • metrics-server 大约每 15 秒刷新一次资源指标
  • 它会去 kubelet 采数据
  • 当前集群为实验便利开启了 --kubelet-insecure-tls

3. 我直接取到了原始 PodMetrics

实际拿到的 metrics.k8s.io 原始数据里可以看到:

  • cpu-demo 空闲后大约 8777n
  • no-request-demo 高负载时大约 999248264n
  • window 大约 16s

这里的 nnanocore

换算关系你要知道:

  • 1000000000n = 1 core
  • 1000m = 1 core

所以:

  • 999248264n 差不多就是 999m
  • 也就是几乎打满一个 CPU 核

这就把 kubectl top 看到的数值和底层 API 数据打通了。


HPA 到底是怎么算副本的

最重要的公式先记住:

desiredReplicas = ceil(currentReplicas * currentMetric / desiredMetric)

这里:

  • currentReplicas 是当前副本数
  • currentMetric 是当前实际指标
  • desiredMetric 是目标指标

如果指标类型是 CPU Utilization

这时 currentMetric 不是“纯 CPU 使用量”,而是:

CPU 使用量相对于 requests.cpu 的百分比

也就是你可以粗略理解成:

CPU 利用率 = 当前 CPU 使用量 / CPU request * 100%

然后 HPA 会在多个 Pod 上做平均,再代入上面的公式。

为什么这一点特别关键

因为这意味着:

HPA 基于 CPU 利用率扩容时,requests.cpu 不是可有可无,而是计算分母的一部分。

没有 requests.cpu,就没有“利用率”这个概念。


为什么 HPA 离不开 requests

这句话你以后必须能脱口而出:

HPA 按 CPU utilization 扩容时,不是看 Pod 用了多少 CPU,而是看 Pod 用掉了它“承诺值”的多少百分比。

举个最关键的例子。

例子 1:有 request

如果一个 Pod:

  • requests.cpu = 100m
  • 实际使用 1000m

那它的 CPU 利用率大约就是:

1000m / 100m = 10 = 1000%

如果目标值是 50%,那显然远远超标,HPA 会倾向大幅扩容。

例子 2:没有 request

如果一个 Pod:

  • 实际使用 1000m
  • 但根本没配 requests.cpu

那分母不存在。

这时候 HPA 并不会“猜一个值”,而是直接报错,表现为:

  • HPA 指标显示 <unknown>
  • describe hpa 里出现 missing request for cpu

这也是为什么上一课讲资源模型时我反复强调:

  • requests 不只是给 scheduler 用
  • 它也直接影响 HPA

这次实验的设计,为什么是这样

为了把原理讲透,我没有直接上复杂业务,而是用了两个极简但非常有代表性的样本。

样本 1:cpu-demo

这个 Deployment 的设计是:

  • 镜像:busybox:1.36
  • requests.cpu = 100m
  • requests.memory = 64Mi
  • 没配 CPU limit
  • 通过环境变量 MODE 决定行为

MODE=burn 时:

  • 容器执行 while true; do :; done
  • 会持续空转,尽量吃满一个核

MODE=idle 时:

  • 容器执行 while true; do sleep 5; done
  • CPU 消耗会显著下降

为什么故意不配 CPU limit?

因为我想让这个示例更容易打满一个核,让 HPA 的扩容现象足够明显。如果给它一个很低的 CPU limit,它可能先被 cgroup 节流,教学上反而不够直接。

样本 2:no-request-demo

这个 Deployment 同样持续烧 CPU,但故意不写 requests.cpu

它的作用不是成功扩容,而是演示一个现实中非常常见的失败场景:

指标能采到,但 HPA 仍然无法计算 CPU utilization。

很多生产事故都出在这里:

  • 业务 Pod 明明很忙
  • kubectl top pod 明明有数据
  • 但 HPA 就是不扩

根因往往就是:

  • 工作负载没有规范写资源请求

cpu-demo 的 HPA 配置里,有哪些真正关键的字段

这次实验的 HPA 关键配置是:

  • minReplicas: 1
  • maxReplicas: 4
  • averageUtilization: 50
  • behavior.scaleUp
  • behavior.scaleDown

averageUtilization: 50 的意思

表示目标是:

让每个 Pod 的平均 CPU 使用量,大致维持在其 request 的 50% 左右。

如果长期高于 50%,就扩;如果长期低于 50%,就缩。

maxReplicas: 4 的意思

即使公式算出来应该扩到更多,HPA 也最多只会扩到 4。

这次实验里就真实看到过这个现象:

  • cpu-demo 一度大约 999% / 50%
  • 说明目标严重超标
  • 但副本最终停在 4

describe hpa 里也明确给出:

  • ScalingLimited=True
  • TooManyReplicas

这就是上限生效。

为什么我要专门写 behavior.scaleDown.stabilizationWindowSeconds: 30

这是本课非常重要的教学设计。

HPA 之所以不应该看到指标一降就立刻缩容,是因为:

  • 指标会抖动
  • 瞬时低谷不代表稳定降载
  • 过快缩容会导致抖动和雪崩

所以 HPA 有“缩容稳定窗口”这个概念。

这套集群里我检查了 kube-controller-manager 的静态 Pod 配置,没有看到显式的 HPA 相关覆盖参数,所以可以合理推断:

  • 控制器级默认值没有被本集群特意改写

而社区常见默认缩容稳定窗口是 5 分钟量级。

但教学实验里如果等 5 分钟,你很难把现象和原理连起来。

所以这次我在对象级配置里把它缩成了 30s,目的不是生产建议,而是让你在可观察的时间内看到:

  1. 负载先降下来
  2. HPA 不会立刻缩
  3. 稍等一个稳定窗口后才缩

这才是真正理解“抗抖动”的最好方式。


为什么我专门避免让 YAML 去和 HPA 抢 replicas

本课实验里,cpu-demo 的两个 Deployment 清单都故意没有写 spec.replicas

这不是疏忽,而是刻意设计。

原因是:

一旦对象已经交给 HPA 管理,你再用带固定 replicas 的 YAML 去 apply,就可能和 HPA 争抢副本数。

这样会让实验现象变脏:

  • 你看到的扩缩容,不知道是 HPA 造成的
  • 还是你 apply 时把副本数改回去了

所以本课切换负载模式时,我使用的是:

kubectl set env deployment/cpu-demo MODE=idle

这条命令做的是:

  • 改 Deployment 模板里的环境变量
  • 触发一个新的 ReplicaSet
  • 让业务行为从烧 CPU 切到空闲
  • 但不直接改 replicas

这是更干净的实验方式。


本次实验里,真实发生了什么

下面是你必须能讲清楚的现象链。

现象 1:HPA 刚创建时先出现 <unknown>

刚把 Deployment 和 HPA 创建出来时,并不是立刻就能看到有效指标。

真实事件里先出现了:

  • FailedGetResourceMetric
  • unable to get metrics for resource cpu: no metrics returned from resource metrics API

这说明什么?

说明 HPA 自己没坏,而是:

  • Pod 刚起来
  • metrics-server 还没抓到这个 Pod 的首批指标

这就是“指标预热”。

所以你以后看到新 HPA 刚建完是 <unknown>,不要第一反应就说“系统坏了”。

先确认:

  • Pod 是否 ready
  • kubectl top pod 是否已有数据
  • metrics.k8s.io 是否已经返回该 Pod

现象 2:cpu-demo 从 1 个副本扩到了 4 个

预热后我看到:

  • kubectl top pod -n autoscaling-lab
    • cpu-demo-* 大约 999m
  • kubectl get hpa -n autoscaling-lab
    • cpu-demo 大约 999%/50%

你来算一遍就明白了。

单 Pod request 是 100m,实际用了约 1000m

1000m / 100m = 1000%

目标只有 50%

如果当时是 1 个副本,公式会倾向算出一个远大于 1 的目标值:

desiredReplicas = ceil(1 * 1000 / 50) = 20

但 HPA 又受到 maxReplicas: 4 限制,所以最终只会扩到 4。

这次真实事件也确实显示:

  • 先扩到 3
  • 再扩到 4

现象 3:no-request-demo 明明也很忙,但始终不扩

同一时间我看到:

  • kubectl top pod 里它也接近 1000m
  • kubectl get hpa 里它是 cpu: <unknown>/50%

describe hpa no-request-demo 明确报:

  • missing request for cpu in container main

这说明:

  • 不是 metrics-server 没数据
  • 不是 HPA 没运行
  • 而是利用率分母缺失

这是一个非常重要的排障训练:

“有监控数据” 不等于 “HPA 能算出副本”。

现象 4:切到 MODE=idle 后,cpu-demo 没有瞬间缩容

我把 cpu-demo 切换成空闲模式之后:

  • 新 ReplicaSet 被创建
  • 旧 ReplicaSet 逐步缩掉
  • 新 Pod 启动后 CPU 使用大幅下降

这时 kubectl top pod 看到:

  • cpu-demo 大约 1m

describe hpa cpu-demo 则显示:

  • 1% (1m) / 50%

但它不是马上从 4 变 1。

为什么?

因为我们明确配置了:

  • scaleDown.stabilizationWindowSeconds: 30

也就是:

先观察一段时间,确认低负载不是瞬时抖动,再执行缩容。

现象 5:大约 30 秒后,HPA 把 cpu-demo 缩回 1

从事件时间线可以看出:

  • 新空闲版 ReplicaSet 开始接管,大约在事件年龄 3m54s
  • HPA 给出 New size: 1; reason: All metrics below target,大约在 3m21s

两者相差大约 30 多秒。

这是一个推断,但推断依据是充分的:

  • 它和我们显式设置的 30s 缩容稳定窗口高度吻合
  • 缩容前的指标已经低到 1%

所以这次实验非常清楚地把“低负载出现”与“真正缩容发生”分成了两个阶段。


这里还有一个高级知识点:Deployment 滚动发布和 HPA 会互相影响

当我执行:

kubectl set env deployment/cpu-demo MODE=idle

Deployment 模板发生了变化,所以它会创建一个新的 ReplicaSet。

这时你看到的现象不只是:

  • HPA 扩或缩

还会叠加:

  • Deployment 自己的滚动更新行为

所以在事件流里你会看到:

  • 老 ReplicaSet cpu-demo-7588dc5965
  • 新 ReplicaSet cpu-demo-6845f656c6
  • 新旧副本短时间并存
  • Deployment 在滚动更新
  • HPA 在根据新指标重新计算目标副本

这就是为什么生产排障时你不能只看 HPA,要同时看:

  • Deployment
  • ReplicaSet
  • Pod
  • Events

否则你会误把“发布造成的副本波动”当成“自动扩缩容故障”。


HPA 扩了副本,为什么业务还是可能不行

这是 CTO 视角必须提前建立的边界意识。

情况 1:HPA 扩了,但节点没容量

HPA 只会把 replicas 调大。

如果集群节点资源已经不够,新 Pod 仍然可能:

  • 调度失败
  • 长时间 Pending

这时需要关注的是:

  • scheduler 事件
  • 节点 allocatable
  • requests
  • 是否需要 Cluster Autoscaler

所以:

HPA 不是容量魔法。

情况 2:瓶颈不在应用 CPU

如果真正瓶颈是:

  • 下游数据库连接池
  • Redis QPS 上限
  • 外部接口限流
  • JVM GC
  • 锁竞争

那你哪怕加更多 Pod,也未必能改善延迟,甚至可能让下游更快被打爆。

这说明:

HPA 只能对“与所选指标相关的瓶颈”有效。

情况 3:扩容太慢,业务还是抖

HPA 不是毫秒级反应,它受很多因素影响:

  • 指标采样窗口
  • metrics-server 刷新周期
  • HPA controller 同步周期
  • 镜像拉取时间
  • Pod 启动时间
  • readiness 探针

所以遇到突发流量时,你还要综合考虑:

  • 基线副本数
  • 预热策略
  • 镜像优化
  • 启动速度
  • 限流与削峰

作为专家,你应该怎样排 HPA 问题

我建议你形成固定排障路径。

第一步:先看 HPA 自己怎么说

KUBECONFIG=~/.kube/config-k8s-lab kubectl describe hpa -n <ns> <name>

重点看:

  • Metrics
  • Conditions
  • Events

尤其注意这些典型状态:

  • ScalingActive=False
  • FailedGetResourceMetric
  • missing request for cpu
  • did not receive metrics for targeted pods
  • ScalingLimited=True

第二步:确认指标是否真的存在

KUBECONFIG=~/.kube/config-k8s-lab kubectl top pod -n <ns>
KUBECONFIG=~/.kube/config-k8s-lab kubectl get --raw '/apis/metrics.k8s.io/v1beta1/namespaces/<ns>/pods'

如果这里都没数据,那先别怀疑 HPA,先查:

  • metrics-server
  • kubelet TLS/连通性
  • metrics API 注册情况

第三步:核对目标工作负载的 requests

KUBECONFIG=~/.kube/config-k8s-lab kubectl get deploy -n <ns> <name> -o yaml

重点看:

  • resources.requests.cpu
  • resources.requests.memory

如果用的是 CPU utilization,但没写 requests.cpu,那 HPA 算不出来就是必然。

第四步:看 HPA 改完副本后,Deployment 有没有真正执行

KUBECONFIG=~/.kube/config-k8s-lab kubectl get deploy,rs,pod -n <ns> -o wide
KUBECONFIG=~/.kube/config-k8s-lab kubectl get events -n <ns> --sort-by=.metadata.creationTimestamp

这里能帮助你区分:

  • HPA 没算出来
  • HPA 算出来了,但 Deployment 没跟上
  • Deployment 跟了,但 Pod 没 ready
  • Pod ready 了,但 Service 端还不稳定

第五步:如果副本扩了但 Pod Pending,转入容量排障

KUBECONFIG=~/.kube/config-k8s-lab kubectl describe pod -n <ns> <pending-pod>
KUBECONFIG=~/.kube/config-k8s-lab kubectl describe node <node>

重点看:

  • Insufficient cpu
  • Insufficient memory
  • taint / toleration
  • 节点 allocatable

这已经不是 HPA 算法问题,而是调度与容量问题。


这节课你必须真正吃透的几个判断句

以后别人问你 HPA,你至少要能说出下面这些话,而且知道背后的证据是什么。

  1. HPA 不是直接看机器负载,而是通过指标 API 读取工作负载指标。
  2. 基于 CPU utilization 的 HPA 以 requests.cpu 为分母,没有 request 就没有利用率。
  3. HPA 改的是目标工作负载的副本数,不负责新增节点。
  4. 指标刚创建时出现 <unknown> 很常见,先查指标预热,不要直接判故障。
  5. 缩容通常比扩容更保守,因为系统需要抗抖动。
  6. 看到扩缩容现象时,必须同时看 HPA、Deployment、ReplicaSet、Pod 和 Events。
  7. HPA 扩容成功不代表业务一定恢复,因为真正瓶颈可能不在 CPU。

你现在应该具备的能力

学完这一课,你至少要具备这几项能力:

  • 能解释 HPA 的指标链路,而不是只会写 YAML
  • 能解释为什么 requests 是 HPA 的基础输入之一
  • 能分清 HPA、VPA、Cluster Autoscaler 的职责边界
  • 能读懂 describe hpa 里的条件和事件
  • 能从 <unknown>missing request for cpuScalingLimited 这些线索快速判断问题方向
  • 能把自动扩缩容放回整个控制面和数据面的上下文里理解

下一步如果继续深入,你就可以进入:

  • Service / Ingress / 流量入口与副本变化的联动
  • 业务级 SLO 与扩缩容策略
  • 自定义指标与外部指标扩容
  • Cluster Autoscaler 与容量池设计

到这一步,你已经不是“会抄 HPA 示例”的阶段了,而是在真正建立平台工程视角。