一个让实测指标尽量贴近设定目标、从而自动调整副本数的控制器。

Horizontal Pod Autoscaler 按目标利用率扩缩容

由于它依据一段时间序列的平均值而不是单个尖峰来决定动作,你往往还要配一个最大副本数,避免扩张失控。 把平滑后的均值当作决策依据 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() 四个场景锁定关键行为:合并场景证明中央处理器要七个而队列要十二个时取十二个;单个缺失证明一路失败仍由中央处理器接管;全部缺失证明保持不动而非缩零;限幅场景证明算完后最大副本数依然生效。预期输出以通过信息结尾,说明四组场景成立,最大值优先与缺失保持均已验证,并打印解释器版本。 ...

十二月 22, 2027 · 1 分钟 · XB Field Notes