2026-10-27 · 深度笔记

突发流量下的弹性扩容:从固定配置到按量付费的切换点

流量突然涨十倍时,是加机器、切按量付费,还是直接限流?这篇讲清三种应对方式的成本结构,以及从固定配置切到弹性方案时最容易忽略的启动延迟。

云主机扩容成本运维

我们的 AI 接口平时 QPS 稳定在 3 左右,某天晚上被爬虫批量调用,10 分钟内冲到 180。

那晚我在监控大屏前盯了半小时,脑子里只有一个念头:扩容救不了这十分钟。

后来我认真算了一遍弹性扩容的经济账,才发现很多时候它并不是最优解——尤其在"模型要重新加载"这件事上。

先分清三种"突发"

脉冲型突发。 几秒冲到峰值立刻回落,来源通常是批量任务、健康检查风暴、爬虫。特点是持续时间短于扩容启动时间——机器还没就绪,流量已经过去。

阶梯型突发。 上一个台阶并保持住,比如定时任务触发。这是最值得投入弹性扩容的场景,扩出来的实例一直有用。

持续型高位。 上台阶之后再不回来。这不是突发,是容量规划问题,弹性方案不如直接加固定容量。

我那次是第一种,正确做法是限流 + 队列。

扩容的启动延迟,比你想的久

这是最反直觉的一点,也是我踩过最贵的坑。

大多数人以为"扩容"就是点一下按钮多一台机器。但对 AI 推理服务来说,新实例要经历:

调度 → 拉镜像 → 启动进程 → 下载/读取模型权重 → 加载进显存 → 预热 → 才是就绪

各环节实测耗时(量级示意,跟镜像大小和存储速度相关):

环节量级
调度到 Pod 运行几秒到几十秒
拉镜像(几个 GB)几十秒到几分钟
读模型权重(几十 GB)几分钟到几十分钟
加载进显存十几秒到几分钟
预热(跑几个请求)十几秒

加起来最快也要几分钟。权重没做成镜像的一部分,每次扩容都要重新从对象存储拉几十 GB——这就是我那次扩了 6 个副本,5 分钟内一个都没就绪的原因。

让扩容真正管用的三个前提

权重必须在镜像里,或者在节点本地盘

这是最关键的一条。我现在把量化后的模型直接打进镜像:

FROM vllm/vllm-openai:latest
COPY ./models/qwen2.5-7b-int4/ /models/qwen2.5-7b-int4/
ENV VLLM_MODEL_PATH=/models/qwen2.5-7b-int4

镜像变大了(几 GB 到十几 GB),拉镜像时间变长,但省掉了"从对象存储拉几十 GB"这一步。总时间通常更短——镜像走仓库的优化分发和节点缓存,模型文件往往从对象存储裸下载。

预热必须在就绪检查之前

新实例加载完模型不代表能服务,得跑几个真实请求把 CUDA graph、内存池、算子初始化好。不预热的话第一个用户要等十几秒,还容易被判定不健康踢掉,再进来又冷启动——恶性循环。

@app.on_event("startup")
async def warmup():
    """模拟真实负载做预热,完成后才置为就绪。"""
    await load_model()
    for prompt in ["你好", "写一句关于秋天的短文"]:
        async for _ in generate(prompt, max_tokens=32):
            pass
    MODEL_READY = True

就绪检查必须区分"活着"和"能干活":

readinessProbe:
  httpGet:
    path: /ready
    port: 8000
  periodSeconds: 5
  failureThreshold: 3
  initialDelaySeconds: 30

扩容指标不能只看 CPU

用 HPA 按 CPU 扩容有个前提——CPU 利用率能反映负载。对推理服务这个前提不成立:decode 是访存密集的,GPU 算力可能只用一半,CPU 更空。CPU 30% 的时候服务可能已经排队了。

按请求队列长度扩才是对的,用 KEDA 配上 Prometheus 适配器:

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: llm-autoscaler
spec:
  scaleTargetRef:
    name: llm-api
  minReplicaCount: 2
  maxReplicaCount: 20
  triggers:
    - type: prometheus
      metadata:
        serverAddress: http://prometheus:9090
        metricName: llm_request_queue_length
        threshold: "5"
        query: "sum(llm_request_queue_length{app='llm-api'})"

minReplicaCount: 2 这个下限别设成 1 或 0。设 0 的话,流量退去后实例全销毁,下次流量来又要等几分钟冷启动——弹性省下的钱全赔在冷启动上了。

三种应对方式的成本结构

假设峰值持续 30 分钟,峰值 QPS 是平时的 10 倍。

应对方式成本结构代价
固定容量峰值所需的常驻机器乘全时段那台大机器 23.5 小时空转
弹性扩容平时 2 个 + 峰值多出来的 10 个乘 0.5 小时前提是镜像和权重预置做好了
限流 + 队列最低用户要等,或降级到小模型

我现在的策略是三者组合:基础容量扛日常波动,超出的靠弹性扩容兜底,再上面是限流。

从固定配置切到按量付费,什么时候做

固定配置和按量付费是同一台机器的两种计费方式,切换是平滑的。要决策的是"要不要为弹性改变部署形态"。

情况选择
负载全天平稳,利用率长期 > 40%包年包月,别折腾弹性
有明显峰谷,谷时利用率 < 10%按量付费 + 自动停机
峰谷比超过 5 倍且峰值持续按量付费 + 弹性扩容
突发极短(分钟级)按量付费 + 限流,别扩容

利用率 10% 和 40% 这两条线是经验值不是标准,背后逻辑通用:按量付费的单价通常比包年包月贵出明显比例,溢价要靠"闲置省下的钱"覆盖。

决定切换点的还有两个问题:扩容启动时间 vs 突发持续时间,启动 5 分钟而突发只有 3 分钟的话弹性扩容无用;流量的可延迟性,聊天只能加机器,批处理能排队就限流。

弹性方案的账单不只有机器时长,几项容易漏算:

冷启动期间的空转。 扩出来的实例在预热阶段就在计费了。预热 3 分钟而突发只有 5 分钟,大部分扩容出来的机器是纯浪费。

镜像存储。 打进镜像的权重意味着仓库要存几十 GB,每个节点都要拉一份。

调参是持续工作。 阈值设太低会反复抖动(扩了又缩,缩了又扩),太高则起不到作用。

缩容时机。 缩容前要等正在处理的请求做完,否则会杀掉用户请求。

收尾

那次 QPS 冲到 180 的晚上,我最后做的事是加了限流和队列,不是扩容。

扩容解决的是"持续性的容量不足",限流解决的是"瞬时的异常流量"。 搞混了,你会花几十分钟的扩容时间,去解决一个本该在 30 秒内结束的问题。

各家的按量单价、包年折扣变动频繁,文章里给的只是量级和判断框架,具体以官方页面为准。

接着读