模型单独能跑通,不代表接进服务后还能稳定。服务侧真正要回答的是三件事:单个实例同时处理多少请求、超出的请求排到哪里、一个请求等多久算失败。建议的处理顺序是先用阶梯压测找出单实例并发拐点,把上限写进配置并留出有界排队区,再按连接、首字节、整体三段设置超时,最后按错误类型决定哪些失败可以重试。
Naive-N0.5-Flash 接进服务时,并发上限不要凭感觉写。在目标输入长度下做 1、2、4、8 的阶梯压测,看延迟分位和排队等待:当吞吐不再上升、排队开始堆积,前一级就是可用的单实例上限。上限写进服务配置,并留出有界队列;超时按连接、首字节、整体分层设置;失败按错误类型决定是否重试。具体数值需要结合机器规格、输入输出长度与延迟目标确认。
在压测里找出单实例的并发拐点
压测要控制变量:同一份输入长度分布、同一个最大输出 token 数、同一套实例配置,只让并发变化。先小步走,比如 1、2、4、8、16、32,每一级跑固定时长或固定请求数,丢掉前一小段预热数据。这样得到的曲线才能互相比较。
每一级至少记这些指标:单实例吞吐(请求数/秒或 token/秒)、端到端延迟的 p50/p95、首 token 时间、请求在队列里的等待时间、服务端返回的错误数、以及 CPU/GPU 利用率和显存占用。拐点通常出现在下面几个信号同时或先后出现的位置:延迟 p95 相对上一级明显抬升且不是单次抖动;吞吐不再随并发增长;排队等待时间由接近零变成持续累积;开始出现超时或显存不足。取拐点前一级作为候选并发上限,如果延迟目标很紧,就直接在压测记录里按可接受的 p95 反推一个更小的值。
# 阶梯并发压测骨架,替换客户端与实例地址即可
for c in 1 2 4 8 16 32; do
run_load `--target` http://127.0.0.1:8000/generate \
`--concurrency` $c \
`--input-len` 512 \
`--max-output-tokens` 256 \
`--report` report-c${c}.json
sleep 30
done
把每一级的并发数、延迟分位、错误数、排队等待记成一张表,这张表就是后面写配置的依据,而不是拍一个数字。
把并发上限写进服务配置并留出排队区
并发上限的作用是保护单个实例不被瞬时流量压垮;排队区的作用是吸收短时突发,让请求等一会儿而不是立刻失败。两者要配对使用:只设并发上限不加队列,突发流量会直接变成错误;只加队列不设上限,队列会无限增长,内存和延迟一起失控。
runtime:
max_concurrency: 8 # 单实例同时执行的请求数,取压测得到的拐点
queue_capacity: 32 # 等待区最多容纳的请求数,写满即拒绝
queue_timeout_ms: 2000 # 请求在队列里最多等待的时间
queue_policy: fifo # 出队顺序,也可换成优先级策略
overload_status: 503 # 队列满或等待超时时返回的状态码
warmup_on_start: true # 启动后先跑少量请求再对外放量
字段含义逐项看:max_concurrency 是从压测表里搬过来的值;queue_capacity 一般取并发上限的若干倍,前提是最长等待时间不超过你能接受的范围;queue_timeout_ms 要和后面的整体超时对齐,不能比整体超时还大;queue_policy 决定谁先被执行;overload_status 让调用方一眼看出是过载而不是参数错误。这套字段名是通用骨架,实际用哪个名字以你所用服务框架为准,关键是把这四个量都显式配置出来。
给推理请求设置分层超时
一个请求的失败原因至少有两类:一类是连不上,一类是连上了但算得慢。用一个总超时同时管这两件事,结果往往是连接问题被拖到很久才暴露,或者生成稍长一点就被误杀。建议拆成三段:连接超时、首字节(首个 token)超时、整体超时。
- 连接超时:建立连接和握手的时间,通常取几百毫秒到几秒,取决于请求端和实例是否在同一机房。取值偏小能让你尽早发现实例不可达。
- 首字节超时:从请求发出到收到第一个字节或第一个 token 的时间,用来识别“连上了但模型卡住”或排队过久。它应当大于排队超时加上推理启动开销。
- 整体超时:一次请求从入队到返回的最长时间,需要覆盖排队等待、首字节等待和完整生成时间,并按输出长度的长尾留出余量。
timeouts:
connect_ms: 1000 # 建连与握手
first_byte_ms: 10000 # 首字节或首个 token
total_ms: 60000 # 含排队在内的整体上限
queue:
wait_ms: 2000 # 与整体超时保持同一套预算
配置时先算总账:整体超时必须大于排队等待加首字节加生成时间,否则压力稍大就会大面积超时。首字节超时应明显大于正常排队时间,用来兜住异常情况。
区分可重试与不可重试的失败
重试的价值在于掩盖偶发故障,代价是在过载时把实例压得更死。所以判断顺序是:先看错误是不是调用方侧问题,再看实例是否已经过载,最后才决定重试次数和退避。
| 错误类型 | 常见表现 | 处理建议 |
|---|---|---|
| 连接失败、连接超时 | 建连被拒、地址不可达 | 可重试,但连续失败时应摘除该实例或短暂熔断 |
| 过载返回(如 429、503) | 服务端明确表示繁忙或限流 | 可重试,必须退避,优先换实例而不是原地重打 |
| 首字节超时 | 长时间没有输出 | 谨慎重试一次;若是实例过载导致,重试会加重排队 |
| 参数或格式错误 | 输入过长、字段缺失、鉴权失败 | 不重试,直接把错误返回给调用方 |
| 调用方主动取消 | 客户端断开 | 不重试 |
| 未分类内部错误 | 返回 5xx 且非过载 | 最多重试一次,带退避 |
| 实例进程消失 | OOM 或重启 | 不在同一实例上立即重试,先摘除再加回 |
retry:
max_attempts: 2 # 含首次请求
base_backoff_ms: 200
max_backoff_ms: 2000
jitter: true # 加抖动,避免同时重试
retry_on: [connect_error, connect_timeout, http_429, http_503]
no_retry_on: [http_400, http_401, http_403, http_413, client_cancel]
budget_ms: 70000 # 所有尝试合计不能超过这个时间
budget_ms 这一项容易被漏掉:重试总时长必须落在调用方的整体超时之内,否则上游已经超时,下游还在重试。
在压测下验证排队长度与超时率
配置写进文件不等于生效。用超过 max_concurrency 的并发打一轮,观察请求是否进入排队而不是立刻报错;再把并发加到超过 queue_capacity,观察是否快速返回过载状态,而不是把延迟拖到整体超时。两轮都能对应上,说明并发与排队配置在起作用。
需要看的指标有:当前队列长度与队列水位峰值、排队等待时间分布、按类型拆分的超时数、过载拒绝数、重试次数、以及实例的 CPU/GPU 与显存占用。把这些指标和压测脚本的并发阶梯放在同一张时间轴上对比,就能判断拐点是否被配置正确复现。同时用日志或管理接口确认运行时读到的并发、队列、超时数值与配置文件一致,有些框架存在默认值覆盖或热更新不生效的情况。
如果验证时发现配置没有生效,回退做法是:保留上一份可用配置,通过重启或滚动发布切回;无法快速回退时,先把并发调回保守值、把队列长度调小、把整体超时放宽,优先保证请求不被打死。修改后重新做一轮阶梯压测,确认排队和超时行为回到预期,再逐步放量。