由于它依据一段时间序列的平均值而不是单个尖峰来决定动作,你往往还要配一个最大副本数,避免扩张失控。
把平滑后的均值当作决策依据
HPA 不会拿一个瞬时读数当依据;它先按窗口对观测值取平均、做平滑,再拿去和期望值比较。这样是为了避免一次一秒的尖峰就重画副本数:先聚合,等到平滑后的数值真正越过了目标,再去动副本。因此真正要调的是三件事:指标源、聚合口径(按 Pod 平均还是集群总量),以及取样的窗口。窗口太短会放大噪声,太长又追赶不上需求。动手改 HPA 之前,先把文档里「怎么聚合、多久算一次」那段读明白,再做任何别的。
选一个你真正能改进的指标
选指标才是设计核心,因为伸缩的上限由目标定。CPU 在任何集群上都好取、对 CPU 密集的负载也顺,可它是个代理:真正排队等锁、等数据库的服务,CPU 往往是闲着的,变坏的是延迟。请求速率、或一个自定义的 queue_depth 这类指标测起来更麻烦,反映的却是用户真正感受到的东西。选一个你能对得上的指标:当它越线时,真实的体验确实变了;并且让 HPA 用和应用同样的口径去定义负载,于是它收敛的是那个让体验变差的数字,而不是那个恰好便宜的抄近道数。
自动化与初始值无关
replicas 的初值是起点,不是 HPA 的依据。一旦 HPA 接管,副本数归它管,初值只是下限或出发点。二者在最好的意义上互不相关:关掉控制器,你仍然有一个稳定、明码的陈述基数;开着一个无人调整的 HPA,它也只管自己那部分。所以下限要现实(它才是那个地板),上限要挡住成本风险的峰值,而初值只负责让最开始的几秒体面一点——在控制器的头几次采样落地之前,它撑起第一段安全期。
坏掉的样子
自定义指标不可用时不会自动回退到中央处理器。接口返回错误或空序列时,控制器把对应指标标为无效,把扩缩条件置为假,然后保持当前副本数而不是猜一个数。只配了队列深度的工作负载,会在最需要扩容时原地冻结。正确做法是把中央处理器与自定义指标并列配置,取算出副本数更多的一方,这样队列信号丢失时仍有降级可用的输入,至少不会把服务钉在原地。
适配器链路还会引入延迟。抓取间隔与缓存叠加后,读到的可能是几分钟前的队列深度,于是对已排空的工作继续加副本,造成超调。先看序列时间戳与查询延迟,再决定是否放宽目标值,每个工作负载只暴露一条预聚合序列,不要把原始高基数序列直接喂给扩缩器。
基数爆炸会从另一侧压垮同一条链路。按用户或按请求打标签,会把一个信号膨胀成数万条序列,拖慢接口,让拉取超时,读数翻转为未知。应先在监控侧聚合,只暴露工作负载级的平均值或总量,收敛序列规模后再对接自动扩缩。
目标类型配错同样致命。自定义指标只支持数值与平均值,利用率只适用于配好资源申请的资源指标。没有资源申请就用中央处理器利用率,或拿内存利用率当延迟代理,都会长期未知。先确认哪一路报未知,再把类型改成链路真正提供的那种,并补齐缺失的资源申请。
快速验证
把下面的代码存为水平扩缩模拟脚本并运行。它只用标准库,不碰任何集群,复刻两条核心规则:多指标取算出副本数更多的一方,缺失指标直接跳过,全部缺失时保持副本数不变,便于本地复现线上行为。
import math
import sys
def desired_for(current, value, target):
"""单个指标算出的期望副本数;返回空表示该路指标不可用。"""
if value is None or target is None or target <= 0:
return None
return math.ceil(current * value / target)
def decide(current, metrics, min_replicas, max_replicas):
"""多指标取最大值;全部缺失时保持当前副本数不变。"""
wants = []
for _, value, target in metrics:
want = desired_for(current, value, target)
if want is not None:
wants.append(want)
if not wants:
return current
return max(min_replicas, min(max_replicas, max(wants)))
def check(actual, expected, message):
if actual != expected:
raise AssertionError(f"{message}:实际 {actual},期望 {expected}")
print(f"OK: {message} -> {actual}")
def main():
current = 4
cpu = ("cpu", 80, 50) # 4 * 80/50 = 6.4 -> 7
queue = ("queue", 900, 300) # 4 * 900/300 = 12
check(decide(current, [cpu, queue], 1, 20), 12, "多指标取最大值")
check(decide(current, [cpu, ("queue", None, 300)], 1, 20), 7, "一路缺失仍按可用指标扩容")
check(decide(current, [("cpu", None, 50), ("queue", None, 300)], 1, 20), 4, "全部缺失时保持副本数")
check(decide(current, [cpu, queue], 1, 10), 10, "最大副本数依然限幅")
print("PASS: 4 scenarios; max-wins and hold-on-missing verified")
print(sys.version.split()[0])
if __name__ == "__main__":
main()
四个场景锁定关键行为:合并场景证明中央处理器要七个而队列要十二个时取十二个;单个缺失证明一路失败仍由中央处理器接管;全部缺失证明保持不动而非缩零;限幅场景证明算完后最大副本数依然生效。预期输出以通过信息结尾,说明四组场景成立,最大值优先与缺失保持均已验证,并打印解释器版本。
上述断言在苹果电脑的解释器上已通过。它只验证副本数计算,不验证抓取延迟、缓存与基数。上线前请用真实指标接口的时间戳与扩缩事件复核,再改生产目标值。
延伸阅读
来源
- Kubernetes 水平自动扩缩,说明算法与指标来源,已核验 2026-06-01。
- 资源指标流水线,说明指标服务与资源指标接口,已核验 2026-06-01。