日期:2026-07-25
我们把自托管的大模型推理服务接到业务系统后,第一个被投诉的就是"慢"。不是真的慢,而是首字要等很久——模型在内部把整段答案生成完才一次性返回,前端卡在加载圈,用户以为断了。更严重的在并发侧:某次大批量作业同时发起上千个长文本生成请求,GPU 显存被瞬间打满,队列里堆了几千个请求,后到的正常业务请求排了十几分钟才响应,长连接一个个超时断开,触发客户端重试,又把压力叠上去,形成雪崩。
我们当时判断,核心矛盾是"推理服务把'生成'和'返回'绑死了,且对并发毫无缓冲"。这个项目里,我们采用私有化推理底座(vLLM 类服务化框架,连续批处理 + KV 缓存)承载模型,并在服务外围做流式与背压治理。底座确定后,真正的难点是怎么在不改模型本身的前提下,把"首字时延、流式体验、并发上限、背压"四件事一起管好。
第一类场景是流式输出与首字优化。业务侧不再等整段答案,而是边生成边接收。我们把推理服务的 streaming 接口透传到前端,用户看到的是逐字出现的回答,感知时延从"整段生成完"降到"第一个 token 出来"。
第二类场景是客户端背压控制。前端不是无脑接收,而是按自身渲染能力做消费限速——后端推太快、前端来不及渲染时,通过流控信号让后端暂缓推送,避免浏览器内存暴涨或卡死。
第三类场景是并发上限与队列。推理服务前置一层网关,对并发做硬上限(如单卡并发不超过 N),超出部分进入有界队列而非无限制堆积,队列满则快速失败而非无限等待。
第四类场景是超时与降级。长请求超过阈值时,网关主动中断并降级返回"已生成部分 + 提示",而不是让连接挂死占用 GPU。
落地过程中我们还补了一块常被忽略的能力:KV 缓存占用看板。连续批处理依赖 KV 缓存,长请求会长期占用缓存块挤占他人,我们把单请求的缓存占用纳入调度权重,长请求自动降优先级。
第一个挑战是流式输出与首字时延。关键在于让推理服务吐出第一个 token 就立刻推给前端,而不是攒完再发。我们在网关层透传 SSE 流,仅做最小化封装。下面是流式与背压的脱敏配置片段。
# 推理流式与背压配置(已脱敏)
streaming:
enabled: true
first_token_timeout_ms: 3000 # 首字超时则降级
sse_flush_interval: 20ms # 推送粒度
backpressure:
client_window: 8 # 客户端未确认窗口上限(token 块)
pause_when_full: true # 窗口满则服务端暂停推送第二个挑战是并发上限与队列。我们用有界队列替代无限堆积,配置单卡并发上限与全局排队上限,超出即快速失败,把"慢死"变成"早失败"。下面是限流与队列的脱敏片段。
# 并发与队列治理(已脱敏)
gate:
max_concurrency_per_gpu: 16
queue:
max_waiting: 200 # 等待队列上限
strategy: "fair" # 公平调度,避免长请求饿死短请求
on_queue_full: "reject_429" # 队列满快速失败第三个挑战是超时与降级兜底。长请求超过首字或整体阈值,网关中断并返回已生成内容,释放 GPU 给后续请求,避免单请求长期霸占 KV 缓存。
改造前后,我们拉通了四组口径一致的脱敏示意指标:
| 指标 | 改造前 | 改造后 | 说明 |
|---|---|---|---|
| 首字时延 | 约 4.5s | 降至约 0.8s | 首个 token 返回时延 |
| 流式中断率 | 约 7% | 降至约 0.5% | 连接中断占比 |
| 队列积压峰值 | 约 3200 | 降至约 180 | 并发洪峰时排队数 |
| P99 时延 | 约 48s | 降至约 9s | 长尾请求时延 |
上表为脱敏示意值,用于说明趋势而非审计口径;不同输入长度差异大,此处取中等长度请求均值。
第一,流式先降首字再谈体验。用户感知的"快"来自第一个字出来的时间,而非整段完成时间。把首字时延压下来,体验立竿见影,远比优化后端总耗时容易见效。
第二,背压优于硬扛。客户端不是无限接收,服务端也不是无限推送。用有界窗口做背压,浏览器和后端都不会被突发流冲垮,稳定性远高于"能推多快推多快"。
第三,队列要有界,失败要早。无限堆积只会把"慢"升级成"雪崩"。给队列设上限、满则快速失败,调用方能立刻感知并退避,比挂死后再超时友好得多。
第四,长请求要主动降级。长文本生成长期占用 KV 缓存挤占他人,网关按阈值中断并返回已生成部分,既释放算力又保住大部分产出,比硬等更划算。
案例片段(已脱敏):一次营销文案批量作业误发起约 1500 个长文本请求,旧架构下 GPU 显存打满、队列堆到 3200、后到业务请求平均等 12 分钟。新治理上线后同样洪峰被有界队列吸收(满 200 即 429 快速失败),前端作业端收到 429 后按指数退避重投,峰值并发稳定在单卡 16、全局排队不超过 180,业务侧正常请求 P99 控制在 9 秒内,无雪崩。
案例片段(已脱敏):一个超长报告生成请求因输入过长,首字后持续占用 KV 缓存约 40 秒。网关在整体超时阈值触发后主动中断,返回已生成的约 1200 字摘要并附"内容截断提示",前端正常展示,该请求释放的缓存块立即被后续 3 个短请求复用,未造成连锁阻塞。