AI时代的Kubernetes教程36招-第21招-HPA、VPA、Cluster Autoscaler和Scale-to-Zero怎么给AI负载降成本?
缩到 0 之后扩不回来:数字人 Avatar 队列的三层弹性设计
HPA 能缩到 0,不代表它能从 0 回来
[读者] 正在把异步 GPU 任务搬上 Kubernetes 的后端 / 平台工程师
[痛点] 空闲时 Pod 白烧钱,真缩到 0 了又扩不回来
[现在读] HPA、VPA、Cluster Autoscaler 同框时,边界必须重画
[读完] 能独立设计一套 minReplicas: 0 的队列驱动弹性方案
[旧方案] 常驻 worker + CPU 阈值 HPA
|
v
[新需求] 空闲彻底缩到 0,突发按队列长度扩容
|
v
[冲突] 没有 Pod 就没有 CPU 指标,HPA 失去判断依据
|
v
[后果] 队列堆积破 SLA,或退回常驻副本继续付钱
我是老李,在一家做数字人内容的团队负责后端平台。我们的核心业务是异步 Avatar 生成:用户提交一段音频加一张形象照,系统排队、拆分、驱动、合成,最后产出一段口型对齐的短视频。整条链路里,真正贵的是 GPU 推理那一段。
系统骨架并不复杂:接入层把任务写进 Redis 队列,worker 从队列里取任务、调 GPU 推理、把结果写进对象存储。任务量有强时段性——白天集中提交,夜里几乎归零。而 GPU 节点是独占计费的,夜里空转的每一天都在账上。
我们手里其实是三层弹性:HPA 负责 Pod 副本数,VPA 负责容器的 requests 与资源建议,Cluster Autoscaler 负责节点池大小。听上去像三层楼,实际上是三条互相设限的边界。这次要动的是最底下那条:夜间队列为空时,worker 副本必须真的能到 0。
为什么「CPU 利用率」这一套扩缩容逻辑,在「从 0 恢复」这件事上会彻底失效?
如果换成队列长度作为 external metric,又该怎么避免指标自身的延迟把扩容变成一场追尾?
当 HPA、VPA、Cluster Autoscaler 同时对同一个工作负载动手,谁该先动、谁必须被限制?
01、故事
任务是:让 Avatar 异步 worker 在保持白天高峰吞吐的前提下,夜间队列为空时把副本数降到 0,并让 GPU 节点随之释放。
我们上线时的第一版配置长这样:
# V0 概念示意,不是本篇最终配置
minReplicas: 1
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
夜间观察到的现象是:CPU 稳定在 8% 左右,HPA 早就缩到了 minReplicas: 1,但 GPU 节点没走。原因不难理解——节点上还挂着一个 worker Pod,Cluster Autoscaler 看不到可以回收的空节点。
白天的现象更难受。CPU 是滞后指标:等它打到 70%,队列里已经堆了几百个任务。Pod 刚被扩出来还要拉镜像、加载模型权重才能消费第一条消息,第一个用户的等待被无限放大。
矛盾的形状就在这里:我们用来做弹性判断的指标,描述的是「已经在跑的 Pod 有多忙」;而我们真正想让系统响应的事情,是「还没被领取的任务有多少」。当副本归零,前者这条线直接就断了。
忙时看 CPU
闲时空烧卡
指标答非问
弹性成摆设
02、问题
旧方案失效可以拆成三个断面。
断面一:指标语义错位。 CPU 利用率是结果,队列长度才是原因。用结果去预测原因,中间隔着一整段调度、拉镜像、加载模型的延迟。
断面二:下限锁死节点。 minReplicas: 1 意味着永远有一个 Pod 占着 GPU 节点。只要节点上还有非 DaemonSet 的 Pod,Cluster Autoscaler 就没有机会回收这台机器,成本下限被硬编码进了配置。
断面三:三层节奏打架。 HPA 在秒到分钟级动作,Cluster Autoscaler 在分钟级动作,VPA 的推荐往往按小时级窗口更新。HPA 先把副本拉满,CA 才慢慢加节点,中间一段时间 Pod 全部 Pending——副本数看起来「到了」,可用容量并没有到。
业务上的影响是双向的:夜间 GPU 空转在持续产生成本,白天高峰的排队时长又在持续消耗体验。技术上最直观的表现是,副本落到 0 或指标缺失时,kubectl get hpa 的 TARGETS 列显示 <unknown>,Conditions 里冒出 ScalingActive: False,Reason 是 FailedGetResourceMetric 或 FailedGetExternalMetric。
所以这一篇的完成标准必须是可验证的,而不是「感觉上更省了」:
- 队列为空并持续一段观察窗口后,worker Deployment 的副本数降到 0;
- 队列长度越过阈值后,HPA 能在下一个同步周期内决策出非 0 副本;
- 扩容出来的 Pod 能进入 Ready 并真正开始消费任务;
- 在队列水位来回抖动时,副本数不产生肉眼可见的反复横跳;
- 指标链路的健康状态可以通过
kubectl get hpa -o yaml直接观察到。
旧路走到头
新需求在前
三条线要断
先量再改弦
03、原理
HPA 的控制回路并不神秘:默认每 15 秒同步一次(--horizontal-pod-autoscaler-sync-period),按 期望副本 = ceil(当前副本 × 当前指标值 / 目标指标值) 计算,中间有 0.1 的容忍度,缩容方向默认带一个稳定窗口,扩容方向默认没有。
但这些参数都不是本篇的关键。关键是这条算式里的「当前指标值」从哪里来。
Kubernetes 官方文档把 HPA 的指标分成三类:Resource、Object、External。Resource 类(CPU、Memory)的采样对象就是 Pod 自身;Object 与 External 类描述的是别的对象或者集群外的系统。官方文档明确指出,缩放至 0(minReplicas: 0)只对 object 与 external 类指标可用,CPU、Memory 这类 Resource 指标不支持——因为 Pod 数量为 0 时,这类指标不存在,HPA 拿不到有效值,自然也无法计算出一个非 0 的期望副本数。
把这句话翻译成工程语言,就是第一个反直觉判断。
反直觉判断:限制 scale-to-zero 的,不是调度、不是容器运行时,而是指标的数据源拓扑。 一个 Pod 归零之后必然会消失的指标,无论它的阈值调得多准,都不可能把系统从 0 拉回来。这是自锁,不是配置问题。
第二个反直觉判断同样重要。
反直觉判断:external metric 里的 external,指的不是「数据在集群外」,而是「这个指标不描述任何集群内对象」。 队列长度恰好符合这个定义——它是 Redis 或 Kafka 的状态,跟集群里存不存在 Pod 毫无关系。这也正是它能支撑从 0 恢复的根本原因。而那些虽然存放在集群外、但描述对象是某个 Pod 的指标,本质上还是 Resource 指标的同构体,救不了 0 副本。
关于该特性在 Kubernetes 1.37 中进入 Beta 的具体条目、Feature Gate 默认值与启用条件,需按当前官方文档核验。本篇只依赖不随版本漂移的那条事实:从 0 恢复必须由不依赖 Pod 存在的指标驱动。官方 HPA 文档见 https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/ ,Autoscaler 各组件实现见 https://github.com/kubernetes/autoscaler 。
再看另外两层的原理。VPA 修改的是容器的 requests,而 HPA 的 Utilization 目标是以 requests 为分母的百分比。两者同时作用于 CPU/Memory 时,目标含义会随 VPA 的调整漂移:VPA 把 requests 调大,同样的 CPU 用量算出来的利用率就变小,HPA 于是更不愿意扩容。稳妥做法是让两层使用互不重叠的信号。
Cluster Autoscaler 只有两条触发线:扩容看「有没有因资源不足而 Pending 的 Pod」,缩容看「节点上的 Pod 能不能被搬走、搬走之后别的节点装不装得下」。它不理解业务,只理解 requests 和 Pending。这带来一个直接推论:Pod 的 requests 写得越不准,CA 的节点选型就越不准,而 requests 恰恰又是 VPA 的管辖范围。
指标有出处
拓扑定生死
分母被谁改
目标就漂移
04、架构
[输入] Redis 队列中的待处理 Avatar 任务
|
v
[模块] prometheus-adapter(external metrics API)+ HPA + VPA + Cluster Autoscaler
|
v
[数据/状态] avatar_queue_length、Pod requests、Pending Pod 数
|
v
[处理] HPA 按队列长度算副本 -> 调度器排 Pod -> CA 按 Pending 补节点
|
v
[输出] Ready 的 worker 副本,从队列消费任务并调用 GPU 推理
这条链路里有四个必须说清楚的边界。
边界一:HPA 只在副本维度动作。 它决定「用几个 Pod 去消费」,不决定任务怎么排队、谁先谁后、失败怎么重试。把排队策略塞进 HPA 的指标里,是把两个问题混成一个。
边界二:external 指标的质量由外部系统决定。 adapter 只是翻译层,它能把 Redis 里的一条计数翻译成 external metrics API 的一个数值,但它无法让这条计数变得更准。指标口径设计错了,HPA 只会更快地做错事。
边界三:CA 只对「因资源不足而 Pending」的 Pod 生效。 本地盘绑定、hostPort 冲突、PodDisruptionBudget 过严、非 DaemonSet 的常驻 Pod,都会阻断节点回收。CA 不是「自动省钱的开关」,它是一个基于调度缺口的补洞器。
边界四:VPA 不能和 HPA 争同一个信号。 如果 HPA 的触发指标是 external,而 VPA 只负责给出 requests 建议,两层就能共存;如果两边都在动 CPU,就会互相拆台。
收益很直接:空闲时段资源可以真的归零,成本下限从「至少一台 GPU 节点常驻」变成「队列真为空时才归零」。代价同样明确:从 0 恢复要走完「指标采集 → HPA 决策 → 调度 → 拉镜像 → 加载模型 → Ready」的全程,冷启动不可能被消除,只能被前移或者被掩盖。
适用条件也因此清楚:任务必须可异步、队列必须有一个权威的水位、任务必须可重入或幂等,业务必须能接受首个任务的额外延迟。四条缺一条,minReplicas: 0 就不该开。
输入是积压
输出是就绪
中间四道界
越界即事故
05、实战一次
这一节的目标是把 V1 跑通:先不缩到 0,先让「队列驱动的扩缩容」这条链路工作起来。
环境与版本
- Kubernetes 集群:具体版本需按当前官方文档核验,本篇只使用
autoscaling/v2这一稳定 API; - 指标管道:Prometheus + prometheus-adapter,对外提供
external.metrics.k8s.io; - 队列:Redis List,用
LPUSH入队、BRPOP出队; - 工作负载:
Deployment/avatar-worker,镜像内置 GPU 推理客户端。
依赖与前置检查
APIService v1beta1.external.metrics.k8s.io 必须处于 Available,否则后面所有配置都会静默失效。
配置一:adapter 规则
rules:
- seriesQuery: 'avatar_queue_length'
resources:
overrides:
namespace:
resource: namespace
name:
matches: '^(.*)$'
as: '${1}'
metricsQuery: 'max(avatar_queue_length{<<.LabelMatchers>>}) by (<<.GroupBy>>)'
注意这里对 namespace 的 overrides 处理,它是后面 06 章里那条故障的伏笔。
配置二:HPA(V1,minReplicas 仍为 1)
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: avatar-worker
namespace: avatar
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: avatar-worker
minReplicas: 1
maxReplicas: 20
metrics:
- type: External
external:
metric:
name: avatar_queue_length
selector:
matchLabels:
queue: avatar_async
target:
type: Value
value: '4'
target.type: Value 表示绝对值语义:队列里每积累 4 个待处理任务,就值得多开一个副本。真实阈值要靠任务平均处理时长反推,这里只是把链路跑通。
启动
kubectl apply -f adapter-rules.yaml
kubectl -n avatar apply -f hpa.yaml
kubectl -n avatar get apiservice v1beta1.external.metrics.k8s.io
请求
for i in $(seq 1 60); do
redis-cli -h redis.avatar LPUSH avatar_async "task:$i"
done
kubectl -n avatar get hpa avatar-worker -w
验证
未在当前环境实测,以下为预期结果(示例输出):
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS
avatar-worker Deployment/avatar-worker 60/4 (avg) 1 20 1
avatar-worker Deployment/avatar-worker 60/4 (avg) 1 20 4
avatar-worker Deployment/avatar-worker 60/4 (avg) 1 20 15
TARGETS 从 <unknown> 变成一个真实数字,是 V1 跑通的标志。
V1 留下两个明确问题:minReplicas 还是 1,GPU 节点依然回收不了;扩容从 1 起步,冷启动延迟仍然压在第一波用户身上。这两条正是 V2 要处理的。
先让线连上
再谈要归零
一步一脚印
跑通再加速
06、排查
诊断一:external 指标一直是 <unknown>
现象:kubectl get hpa 的 TARGETS 列始终显示 <unknown>/4,副本数停在 minReplicas。
怀疑:adapter 没起来,或者 APIService 没注册成功。
检查:
kubectl get apiservice v1beta1.external.metrics.k8s.io -o yaml
kubectl get --raw '/apis/external.metrics.k8s.io/v1beta1/namespaces/avatar/avatar_queue_length?labelSelector=queue%3Davidatar_async'
kubectl -n avatar logs deploy/prometheus-adapter --tail=100
证据:APIService 的 condition 为 Available: False,adapter 日志里出现无法解析 avatar_queue_length 的记录。
根因:规则里的 seriesQuery 匹配的是 Prometheus 侧的原始序列名,而 name.as 输出的 metric 名与 HPA 里写的 metric.name 不一致。HPA 在外部指标通道里做的是「名字 + 标签选择器」的精确匹配,任何一处漂移都表现为空结果。
修复:把 adapter 规则的输出名与 HPA 的 metric.name、metric.selector 对齐。
错误尝试: 第一反应是把 minReplicas 直接改成 0,想看看是不是「副本不为 0 所以指标不生效」。这个尝试是错的。HPA 在副本为 0 时依然会去轮询 external 指标,指标缺失的根因在 adapter 的命名匹配上,跟副本数没有任何关系。改 minReplicas 只会把问题从「缩不下去」变成「根本没有副本在跑」,同时丢掉 1 副本时还能观察到的对比基线,让排查难度翻倍。
名字要对齐
标签要咬合
先证再动手
乱改添新伤
诊断二:副本归零后,队列涨了也不扩
现象:把 minReplicas 调到 0 之后,夜间副本正常归零;次日早上队列涨到 200 以上,副本仍然是 0。
怀疑:HPA 在 0 副本时不再评估 external 指标。
检查:
kubectl -n avatar get hpa avatar-worker -o yaml
kubectl -n avatar describe hpa avatar-worker
kubectl -n avatar logs deploy/prometheus-adapter --tail=200
kubectl get --raw '/apis/external.metrics.k8s.io/v1beta1/namespaces/avatar/avatar_queue_length?labelSelector=queue%3Davidatar_async'
证据:HPA 的 Conditions 里出现 ScalingActive: False,Reason 为 FailedGetExternalMetric;adapter 日志里对应的查询返回空结果集。同时可以注意到,HPA 确实在 0 副本时仍在查询指标——这一点恰好反证了 external 通道与 Pod 无关。
根因:HPA 在 external metric 查询上会默认带上 namespace 维度去匹配,而 adapter 规则没有为 namespace 做 overrides,导致最终发给 Prometheus 的查询里带了一个不存在的标签,结果为空。
修复:在 adapter 规则里补齐 namespace 的 overrides,或者把 HPA 的 metric.selector 显式限定成 adapter 真正支持的那组标签。
零副本仍问
空集非无问
标签多一个
答案就落空
诊断三:副本数到位了,Pod 却长时间 Pending
现象:HPA 把副本从 0 拉到 8,其中 5 个 Pod 长期 Pending,几分钟后才陆续 Running。
怀疑:Cluster Autoscaler 没生效。
检查:
kubectl -n avatar get pods -o wide
kubectl -n avatar describe pod <pending-pod> | tail -20
kubectl -n kube-system logs deploy/cluster-autoscaler --tail=100
证据:事件里是 0/12 nodes are available: 12 Insufficient nvidia.com/gpu,同时 CA 日志里能看到它正在为这批 Pod 选型。
根因:不是 CA 没生效,而是 HPA 与 CA 的节奏差。HPA 是秒级决策,节点从创建到 GPU 驱动就绪是分钟级。副本数「看起来」到了,实际可用容量没到。
修复方向:不要让 HPA 一次性跳到 maxReplicas,用 behavior.scaleUp 的 policy 限制每次扩容的比例与周期,把扩容曲线拉平,让 CA 跟得上。
秒级做决策
分钟级补机
曲线拉太平
中间全挂起
07、优化
V2 的每一条改动都必须能追溯到 06 章的一条证据。
根因一:命名与标签不一致导致 FailedGetExternalMetric。
修改:统一 adapter 规则与 HPA 中的指标名和标签,补齐 namespace 的 overrides。
原因:external 通道是精确匹配,任何一处漂移都表现为空结果集。
新行为:HPA 在 0 副本时也能取到有效数值,Conditions 中 ScalingActive 恢复为 True。
验证:kubectl get hpa 的 TARGETS 在副本为 0 时仍显示真实数字,而不是 <unknown>。
根因二:从 0 扩容与从 N 扩容的节奏不同,必须分开配置。
修改:显式写出 behavior:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: avatar-worker
namespace: avatar
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: avatar-worker
minReplicas: 0
maxReplicas: 20
metrics:
- type: External
external:
metric:
name: avatar_queue_length
selector:
matchLabels:
queue: avatar_async
target:
type: Value
value: '4'
behavior:
scaleUp:
stabilizationWindowSeconds: 0
policies:
- type: Percent
value: 100
periodSeconds: 30
- type: Pods
value: 4
periodSeconds: 30
selectPolicy: Min
scaleDown:
stabilizationWindowSeconds: 600
policies:
- type: Pods
value: 1
periodSeconds: 120
selectPolicy: Min
原因:从 0 恢复要尽量快,所以 scaleUp 的稳定窗口设为 0;缩容必须慢,否则队列被一瞬抽空就把刚起来的 Pod 又清掉,形成反复冷启动的锯齿。selectPolicy: Min 表示在两个策略之间取更保守的那个,避免扩容幅度被放大。
新行为:扩容侧立即响应,缩容侧以「每 120 秒最多减 1 个」的速度缓慢回收。
验证:连续制造队列脉冲,观察 kubectl get hpa -w 中 REPLICAS 的上升沿是否明显快于下降沿。
根因三:首任务延迟被冷启动吃掉。
修改方向:把冷启动的固定成本前移——镜像预拉、模型权重预热、worker 启动后先做一次自检再置 Ready。必须明确:这些手段只压缩时间常数,不能把常数变成 0。
根因四:节点回收被 Pod 生命周期挡住。
修改方向:给 worker 配 terminationGracePeriodSeconds 与 preStop,让正在执行的任务要么跑完、要么能被重新入队;用 PDB 明确「至少允许几个不可用」,让 CA 知道哪些节点可以回收。
以上每一条都只能写成「可验证的断言」,不能写成「应该会更好」。
慢在缩容上
快在扩容侧
窗口分开配
锯齿自然平
08、演进
[同一输入:Redis 中的 avatar_async 队列]
|
+--[V1] minReplicas: 1 + CPU 阈值 / 扩缩容滞后、节点永不回收
|
+--[V2] minReplicas: 0 + external 队列长度 + behavior 双窗口
|
[Trade-off]
得到:空闲成本可归零、扩容触发与真实积压对齐
失去:首任务冷启动延迟、指标管道成为新的单点依赖
边界:任务必须可异步、队列必须有权威水位、下游必须幂等
正确性。 V1 的 CPU 阈值在负载平稳时可以工作,但在队列型负载上是错位的信号:CPU 高说明 Pod 忙,CPU 低不能说明没活干。V2 的触发信号与业务目标直接对齐,队列积压就是扩容理由。
稳定性。 V1 不会遇到从 0 恢复的问题,但会抖动,尤其在水位临界时。V2 引入了从 0 恢复的路径,同时也引入了新的风险面:指标管道不可用,就等于失去了扩容能力。
复杂度。 V1 一个 HPA 就够。V2 多出 adapter 配置、指标命名契约、behavior 参数、CA 与 HPA 的节奏协调,运维面明显变大。
成本。 V1 的成本下限是「至少一台 GPU 节点常驻」。V2 的下限取决于队列是否真的会空——如果队列从不为空,V2 并不会比 V1 更省,只是多了一套复杂度。
适用范围。 队列型、可异步、可容忍首任务延迟的工作负载适合 V2。对延迟敏感、有状态、需要长会话绑定的工作负载不适合:它们的冷启动不仅贵,还可能有正确性代价。
遗留问题。 指标聚合的时间窗口精度、多队列共享一个 worker 池时的聚合口径、HPA 步长与 CA 加节点速度的比例关系,仍然需要持续观察和调参。
一层改指标
一层改幅度
一层管机器
三层各守界
09、洞见
9.1 从 0 恢复的能力,取决于指标是否寄生于 Pod
HPA 的指标分三类:Resource、Object、External。Resource 类指标的采样对象是 Pod 自身,Pod 归零,指标随之归零;Object 与 External 类描述的是别的对象或集群外系统,Pod 存不存在都不影响取值。这条差异,就是 minReplicas: 0 能用与不能用的分水岭。
反直觉判断:限制 scale-to-zero 的不是调度器或容器运行时,而是指标的数据源拓扑。 只要指标的生命周期和 Pod 绑定,无论阈值怎么调,都不可能把系统从 0 拉回来。
9.2 反直觉判断:队列长度不是「更好的 CPU 指标」
把队列长度当成 CPU 的升级版是一种误读。CPU 是「系统当前有多忙」的后视镜,队列长度是「系统还欠多少活」的欠条。两者导向的控制问题完全不同。
用 CPU 时,目标是维持在一个利用率区间,这是一个稳态控制问题;用队列长度时,目标是尽快把它压回 0,这更接近一个追赶问题。追赶问题对延迟极其敏感:采集延迟、HPA 同步周期、Pod 启动时间,每一段都直接叠加在用户的等待上。这也解释了为什么在队列型负载里,scaleUp 的稳定窗口必须压到 0。
9.3 三层弹性不是三个开关,而是三条互相设限的边界
HPA 决定「要不要更多 Pod」,VPA 决定「每个 Pod 要多少资源」,CA 决定「够不够多机器」。三者的耦合点是 requests:它既是调度的输入,也是 CA 选型的输入,还是 HPA 中 Utilization 类目标的分母。
一旦 HPA 用 CPU 百分比、VPA 又不停修改 requests,两层就在同一个变量上互相干扰。把 HPA 的触发信号换成 external 指标,本质上是在拆耦合点——HPA 看业务水位,VPA 看资源画像,CA 看调度缺口,各管一段。
反直觉判断:弹性层数越多,单层的能力上限反而越应该被压小。 任何一层「多干一点」,都会在别的层留下无法预测的副作用。
9.4 弹性的最小单位是「一次完整任务」,不是「一个 Pod」
缩到 0 的代价最终要折算到任务粒度上:一个 Pod 从创建到能消费任务的时间,等于第一个用户的额外等待。任务本身跑十分钟,冷启动三十秒可以接受;任务只跑五秒,冷启动三十秒就完全不可接受。
所以真正决定 minReplicas 该取 0 还是 1 的,不是成本表,而是「任务时长 ÷ 冷启动时长」这个比值。这个比值一旦被写进部署配置,弹性策略就从拍脑袋变成了可复核的参数。
指标定能不能
语义定快不快
边界定稳不稳
比值定值不值
10、系统落地
原来有什么。 一个常驻 1~20 副本的 Avatar worker Deployment,用 CPU 阈值扩容,GPU 节点 7×24 常驻。空闲时段的资源浪费被记在成本账上,高峰时段的排队延迟被记在体验账上。
本篇新增了什么。 一条 external 指标通道,把队列水位接进 HPA;minReplicas: 0 的归零配置与 behavior 双窗口;一份 adapter 规则与 HPA 之间的命名、标签契约;一组专门针对「从 0 恢复」的验证断言。
现在能做什么。 队列为空时副本可以真正归零,Cluster Autoscaler 有机会回收 GPU 节点;队列积压时 HPA 依据业务水位而不是 CPU 余温做决策;扩容与缩容走两条不同的时间曲线,不再互相干扰。
还缺什么。
- 指标管道本身是单点,adapter 挂掉就等于失去扩容能力;
- 多队列共享同一个 worker 池时的聚合口径尚未定义;
- HPA 的扩容步长与 CA 加节点速度之间还没有量化的比例关系;
- 首批任务的冷启动延迟只是被压缩,没有被真正掩盖。
下一步如何演进。 给 external 指标加一条降级路径:指标不可用时回落到保守的固定副本数,而不是停在 0;把「任务时长 ÷ 冷启动时长」作为参数写进部署配置,让 minReplicas 的选择有据可依;持续观察 CA 的扩容轨迹,反推每次 HPA 扩容的合理步长上限。
先有指标链
再有零副本
补上降级路
系统才算稳
11、小结
Q1 -> 因为 CPU / Memory 属于 Resource 类指标,数据源是 Pod 自身;副本为 0 时指标不存在,HPA 无法计算期望副本数,只能维持现状。
Q2 -> 改用 external 类指标(队列长度),并用 behavior 把扩容窗口压到 0、缩容窗口拉到足够长,避免锯齿式抖动。
Q3 -> HPA 管副本、VPA 管 requests、CA 管节点,三者必须使用互不重叠的信号,唯一的耦合点是 requests。
状态 -> 一套 minReplicas: 0 的队列驱动弹性方案已经成形,验证断言与降级方向已明确。
问能不能缩
问快不快回
问谁管哪段
答案在拓扑
12、作业
12.1 理解题:为什么 CPU 和 Memory 不能作为从 0 恢复的指标?
参考答案:HPA 的 Resource 类指标来自对 Pod 的采样。当目标工作负载副本数为 0 时,不存在可采样的 Pod,指标集合为空,HPA 无法计算期望副本数,只能维持当前值 0,于是形成自锁。Object 与 External 类指标描述的是 Pod 之外的对象或系统,因此可以在副本为 0 时继续提供数值。某具体 Kubernetes 版本上该特性的成熟度与默认开关状态,需按当前官方文档核验。
12.2 实战题:把一份 CPU 阈值的 HPA 配置改成可从 0 恢复的形式。
参考答案要点:minReplicas 改为 0;metrics 从 Resource 改为 External 或 Object,并确认该指标不寄生在目标 Pod 上;补齐 metric.selector,与 adapter 规则里的标签一一对应;显式设置 behavior.scaleUp.stabilizationWindowSeconds: 0,避免扩容被稳定窗口推迟;behavior.scaleDown 保留足够长的稳定窗口与保守的 selectPolicy;最后用 kubectl get hpa -w 确认副本为 0 时 TARGETS 仍是真实数值而不是 <unknown>。
12.3 排障题:HPA 显示 ScalingActive: False 且 Reason 为 FailedGetExternalMetric,怎么定位?
参考答案:先 kubectl get apiservice v1beta1.external.metrics.k8s.io -o yaml 确认 APIService 是否 Available;再用 kubectl get --raw 直接打对应的 external metrics 端点,看返回是否为空;然后逐项核对 adapter 的 seriesQuery、name.as、metricsQuery 与 HPA 中的 metric.name、metric.selector 是否完全一致;特别留意 namespace 这类默认附加标签,adapter 若未做 overrides,查询会带上 Prometheus 侧不存在的标签而返回空集。错误做法是把 minReplicas 从 0 改回 1 来绕过问题,那只会掩盖指标链路本身的故障。
12.4 架构判断题:HPA、VPA、Cluster Autoscaler 同时开启时,哪些组合必须避免?
参考答案:必须避免 HPA 与 VPA 同时作用于同一个 CPU/Memory 信号。VPA 调整 requests 会改变 HPA 中 Utilization 目标的分母,使扩缩容目标漂移,表现为「资源明显不够但 HPA 不扩」。可行组合是:HPA 用 external 或 object 指标扩副本,VPA 只提供 requests 建议而不自动写入,Cluster Autoscaler 依据 Pending Pod 与 requests 决定节点数。此外,HPA 的扩容步长需要与 CA 的加节点速度匹配,否则会长时间出现副本数到位、Pod 却全部 Pending 的状态。
一层看水位
一层看画像
一层看缺口
错位即打架
13、思考
回到最初那个冲突:我们想要的是成本跟着业务走,但弹性系统给出的从来不是「零成本」,而是「把成本换成延迟」。
CPU 阈值之所以看起来够用,是因为它默认了系统永远有一部分常驻容量在承接流量。一旦把常驻容量撤掉,判断依据也必须跟着换——不是把阈值调得更灵敏,而是换一个不寄生于 Pod 的信号源。这条规则不只适用于 Avatar 队列,任何要做 scale-to-zero 的异步任务系统都会撞上它。
第二层判断是顺序:先证明链路能连通,再谈归零;先拿到外部指标的实数,再谈阈值。把「缩到 0」当成一个开关去拨,得到的通常不是省钱,而是第二天早上队列堆满、副本数是 0 的现场。
第三层判断是取舍的可计算化。冷启动不可消除,能做的只是把它和任务时长放在一起比较,用比值而不是用感觉决定下限。当这个比值被写进配置,弹性策略才从经验变成了资产。
撤掉常驻底
换掉判断源
先证再归零
取舍可计算

浙公网安备 33010602011771号