HorizontalPodAutoscaler 根据配置的指标目标调整可伸缩工作负载。它的输出是期望副本数,不是“额外 Pod 已经接收流量”的保证。调度、镜像启动、初始化、就绪状态以及节点容量,都会决定这个建议多久才能转变成真正有用的服务能力。设计控制循环时,必须把这些延迟一起考虑,不能只根据一条公式估计恢复时间。

指标产生的是副本建议

对于简单的逐 Pod 平均值,期望副本等于当前副本数乘以当前指标除以目标指标,再向上取整。比值为二时,在额外控制介入之前会建议翻倍。比值低于一可能建议缩容,但容差、缺失测量、就绪处理、上下限和扩缩行为,都可能阻止或限制最终动作。公式描述起点,不是所有条件下都直接生效的完整算法。

CPU 利用率以资源 requests 为分母。改变请求值,即使用掉的 CPU 没变,也可能改变利用率,所以纵向与横向伸缩需要协调。指标接口必须可用,目标还要支持 scale 子资源。HPA 修改 Deployment 的副本数量,再由 Deployment 控制器管理 ReplicaSet 与 Pod;这几层职责不同,不应把它们统称为一个瞬间完成的操作。

默认协调周期为十五秒,但它不是端到端响应时间的承诺。指标自身还有采集窗口和传输延迟。需要区分跨 Pod 的平均用量、测量管线里的时间平均,以及控制器对建议值的稳定处理。一条过旧但格式有效的数字,仍可能误导运行正常的控制器,造成尖峰过去以后才扩容的现象。

稳定的是建议历史,不是上次调整时间

缩容稳定机制保留近期副本建议,并在配置窗口中选择最高值。默认缩容窗口为五分钟,autoscaling/v2 的 behavior 可以进一步调整。它不是从上一次真正改变副本数开始计时的简单冷却期。即使某次较高建议没有改变现有数量,只要仍在窗口里,也可能推迟下一次缩容,这正是下面模型专门展示的差异。

速率策略另外限制一段时间内可以改变多少 Pod 或百分比。因此扩容也可能被限制,不能说它永远立即、无限进行。最小副本应考虑启动延迟所需余量,最大副本应符合节点容量和成本。稳定机制能降低反复变化,却不能修复选错瓶颈或目标值的问题;把窗口加长只是改变响应方式,不会让错误信号变正确。

选择增加副本能够缓解的压力

CPU 对计算密集型请求可能有效,队列深度或每副本工作量可能更适合后台工作者。但延迟或任意资源图表,并不会天然成为合适的控制输入。如果所有副本都在等待同一台已经饱和的数据库,继续增加副本可能扩大争用,反而延长等待时间。需要先确认瓶颈能否随着应用副本增加而解除。

具体检查新增一个副本后会发生什么:工作能否分配给它,每副本压力是否下降,下游是否还有足够容量。长连接、分区不均与缓存预热都会推迟效果。控制器仅仅改变数量,不会替应用重新分片或自动迁移已有连接。观察这些行为,可以判断问题应该通过扩容解决,还是需要改变工作分配或下游容量。

坏掉的样子

缺失指标表示未知需求,不表示需求为零。多指标配置中,即使一路失败,另一条有效扩容建议仍可能使用;但失败来源会阻止其余指标建议的缩容。个别 Pod 缺失样本也有保守处理。应通过条件和事件区分这些情况,而不是假定每次缺少一个样本就停止所有动作,也不要据此手工删除仍在服务的容量。

maxReplicas 过低时,计算出的建议被限制,ScalingLimited 可以显示这一情况,但队列仍在增长。过高的下限也会掩盖真实低需求。部署自动化反复写固定 replicas 会与 HPA 竞争。还应检查 CPU 请求变化和启动就绪状态,再归咎于普通滚动更新:HPA 支持 Deployment 发布,并不是把每个初始化 Pod 都当成可以随意缩掉容量的零负载样本。

快速验证

保存为 hpa_loop.py,执行 python3 hpa_loop.py。它用标准库建模比例计算、上下限,以及取近期最高建议的缩容窗口。模型明确省略容差、速率策略、逐 Pod 样本和真实 Kubernetes 协调,不应被当作生产控制器的完整复制品。运行只涉及本地算术与列表状态,不访问集群。

import math

TARGET = 50.0
MIN_REPLICAS = 2
MAX_REPLICAS = 10
WINDOW_S = 300


def desired(replicas, value):
    raw = math.ceil(replicas * value / TARGET)
    return max(MIN_REPLICAS, min(MAX_REPLICAS, raw))


def step(replicas, history, now, value):
    if value is None:
        print(f"t={now}s metric=missing hold={replicas}")
        return replicas
    recommendation = desired(replicas, value)
    history[:] = [(t, count) for t, count in history if now - t <= WINDOW_S]
    history.append((now, recommendation))
    if recommendation < replicas:
        result = min(replicas, max(count for _, count in history))
    else:
        result = recommendation
    print(f"t={now}s raw={recommendation} {replicas}->{result}")
    return result


def main():
    history = []
    replicas = step(3, history, 0, 100)
    assert replicas == 6
    replicas = step(replicas, history, 240, 50)
    assert replicas == 6
    replicas = step(replicas, history, 400, 15)
    assert replicas == 6  # A recent recommendation, not a recent scale event.
    replicas = step(replicas, history, 541, 15)
    assert replicas == 2
    replicas = step(replicas, history, 600, None)
    assert replicas == 2
    assert desired(2, 500) == 10
    assert desired(2, 5) == 2
    print("PASS: recommendation history holds 6 at t=400, then shrinks to 2")


if __name__ == "__main__":
    main()

零秒时模型从三个副本扩到六个。二百四十秒时再次建议六个,数量并未变化。四百秒时新的原始建议为两个,但近期的六副本建议仍在窗口内,所以继续保持六个;此时距离上次实际扩容已超过五分钟。五百四十一秒时,那个较高建议离开窗口,模型才缩到两个。这说明约束来自建议历史,而不是上次调整的时间戳。

两种语言中的代码均在本轮通过 Python 3.12.14,包含输入缺失保持和两个边界断言。这些结果只证明模型声明的行为。调试真实集群时,还应按时间对照 HPA 条件、副本建议、实际数量、Pod 就绪和应用延迟。本轮没有运行真实控制器或指标管线,本地成功不能代替真实系统验证。

延伸阅读

来源