Repository Reading Site
第十二课:HPA、自动扩缩容、指标链路与副本伸缩原理
前一课我们刚把下面这些概念讲清楚: 这时候就该进入另一条很关键的主线了: 很多人学 HPA 的方式很危险: 这远远不够。 如果你以后想带团队做平台,或者能独立负责一套公司级项目架构,你至少要把下面这些问题讲明白: 1. HPA 看的到底是什么指标 2. 指标从哪里来,多久采一次 3. 为什么 HPA 经常先显示 `<unknown>` 4. 为什么基于 CP
第十二课:HPA、自动扩缩容、指标链路与副本伸缩原理
为什么这一课必须接在资源模型之后
前一课我们刚把下面这些概念讲清楚:
requests是调度账本limits是运行时约束- CPU 是可压缩资源
- memory 是不可压缩资源
OOMKilled和Eviction不是一回事
这时候就该进入另一条很关键的主线了:
Kubernetes 到底怎样根据“真实负载”自动增减副本。
很多人学 HPA 的方式很危险:
- 看一个
autoscaling/v2的 YAML - 敲一个
kubectl autoscale - 看到副本从
1变成3 - 就以为自己懂了
这远远不够。
如果你以后想带团队做平台,或者能独立负责一套公司级项目架构,你至少要把下面这些问题讲明白:
- HPA 看的到底是什么指标
- 指标从哪里来,多久采一次
- 为什么 HPA 经常先显示
<unknown> - 为什么基于 CPU 利用率的 HPA 离不开
requests - 为什么副本明明扩了,服务却未必变快
- 为什么 HPA 扩容后 Pod 还是可能
Pending - HPA、Deployment、scheduler、kubelet、Cluster Autoscaler 各自分工是什么
这课的目标不是“会用 HPA”,而是把自动扩缩容这条控制链路真正看懂。
先建立全景图:Kubernetes 里到底有几种“扩缩容”
你以后脑子里要同时有四种不同层次的调节手段。
第一种:应用自己处理更多并发
例如:
- Go 服务增加 goroutine
- Java 线程池扩大并发
- Nginx 增加 worker
这是应用内部并发能力,不是 Kubernetes 扩容。
第二种:HPA
HorizontalPodAutoscaler 做的是:
横向扩缩容,也就是增减 Pod 副本数。
它不改节点数量,不改单个 Pod 的资源配额。
第三种:VPA
VerticalPodAutoscaler 做的是:
纵向调整单个 Pod 的资源请求和限制。
比如把 requests.cpu 从 100m 调成 500m。
它解决的是“单 Pod 规格不合适”,不是“Pod 数量不够”。
第四种:Cluster Autoscaler
它解决的是:
集群节点不够时,是否自动加机器;节点空闲时,是否自动减机器。
所以你以后要牢牢记住:
- HPA 调的是
replicas - VPA 调的是
requests/limits - CA 调的是
nodes
它们不是一个东西。
HPA 在整条控制链路里处于什么位置
HPA 不是直接看容器进程,也不是直接去机器上 top。
它的工作链路大致是:
- 容器在节点上运行
- kubelet / cAdvisor 采集容器资源使用数据
metrics-server周期性向 kubelet 拉取指标metrics-server通过metrics.k8s.ioAPI 暴露资源指标kube-controller-manager里的 HPA controller 读取指标- HPA controller 计算目标副本数
- HPA 修改目标工作负载的
scale子资源 Deployment/ReplicaSet按新副本数创建或删除 Pod- 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
输出包含:
NodeMetricsPodMetrics
这说明集群里资源指标 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空闲后大约8777nno-request-demo高负载时大约999248264nwindow大约16s
这里的 n 是 nanocore。
换算关系你要知道:
1000000000n = 1 core1000m = 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 = 100mrequests.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: 1maxReplicas: 4averageUtilization: 50behavior.scaleUpbehavior.scaleDown
averageUtilization: 50 的意思
表示目标是:
让每个 Pod 的平均 CPU 使用量,大致维持在其 request 的 50% 左右。
如果长期高于 50%,就扩;如果长期低于 50%,就缩。
maxReplicas: 4 的意思
即使公式算出来应该扩到更多,HPA 也最多只会扩到 4。
这次实验里就真实看到过这个现象:
cpu-demo一度大约999% / 50%- 说明目标严重超标
- 但副本最终停在
4
describe hpa 里也明确给出:
ScalingLimited=TrueTooManyReplicas
这就是上限生效。
为什么我要专门写 behavior.scaleDown.stabilizationWindowSeconds: 30
这是本课非常重要的教学设计。
HPA 之所以不应该看到指标一降就立刻缩容,是因为:
- 指标会抖动
- 瞬时低谷不代表稳定降载
- 过快缩容会导致抖动和雪崩
所以 HPA 有“缩容稳定窗口”这个概念。
这套集群里我检查了 kube-controller-manager 的静态 Pod 配置,没有看到显式的 HPA 相关覆盖参数,所以可以合理推断:
- 控制器级默认值没有被本集群特意改写
而社区常见默认缩容稳定窗口是 5 分钟量级。
但教学实验里如果等 5 分钟,你很难把现象和原理连起来。
所以这次我在对象级配置里把它缩成了 30s,目的不是生产建议,而是让你在可观察的时间内看到:
- 负载先降下来
- HPA 不会立刻缩
- 稍等一个稳定窗口后才缩
这才是真正理解“抗抖动”的最好方式。
为什么我专门避免让 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 创建出来时,并不是立刻就能看到有效指标。
真实事件里先出现了:
FailedGetResourceMetricunable 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-labcpu-demo-*大约999m
kubectl get hpa -n autoscaling-labcpu-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,要同时看:
DeploymentReplicaSetPodEvents
否则你会误把“发布造成的副本波动”当成“自动扩缩容故障”。
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>
重点看:
MetricsConditionsEvents
尤其注意这些典型状态:
ScalingActive=FalseFailedGetResourceMetricmissing request for cpudid not receive metrics for targeted podsScalingLimited=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.cpuresources.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 cpuInsufficient memory- taint / toleration
- 节点 allocatable
这已经不是 HPA 算法问题,而是调度与容量问题。
这节课你必须真正吃透的几个判断句
以后别人问你 HPA,你至少要能说出下面这些话,而且知道背后的证据是什么。
- HPA 不是直接看机器负载,而是通过指标 API 读取工作负载指标。
- 基于 CPU utilization 的 HPA 以
requests.cpu为分母,没有 request 就没有利用率。 - HPA 改的是目标工作负载的副本数,不负责新增节点。
- 指标刚创建时出现
<unknown>很常见,先查指标预热,不要直接判故障。 - 缩容通常比扩容更保守,因为系统需要抗抖动。
- 看到扩缩容现象时,必须同时看 HPA、Deployment、ReplicaSet、Pod 和 Events。
- HPA 扩容成功不代表业务一定恢复,因为真正瓶颈可能不在 CPU。
你现在应该具备的能力
学完这一课,你至少要具备这几项能力:
- 能解释 HPA 的指标链路,而不是只会写 YAML
- 能解释为什么
requests是 HPA 的基础输入之一 - 能分清 HPA、VPA、Cluster Autoscaler 的职责边界
- 能读懂
describe hpa里的条件和事件 - 能从
<unknown>、missing request for cpu、ScalingLimited这些线索快速判断问题方向 - 能把自动扩缩容放回整个控制面和数据面的上下文里理解
下一步如果继续深入,你就可以进入:
- Service / Ingress / 流量入口与副本变化的联动
- 业务级 SLO 与扩缩容策略
- 自定义指标与外部指标扩容
- Cluster Autoscaler 与容量池设计
到这一步,你已经不是“会抄 HPA 示例”的阶段了,而是在真正建立平台工程视角。