27届大模型面试准备(七十二):大模型流式推理服务与长连接工程——SSE、背压与首包优化 📅 发布时间:2026/9/2 1:35:02 👁 浏览次数: 27届大模型面试准备七十二大模型流式推理服务与长连接工程——SSE、背压与首包优化引言前面几篇把怎么把请求调度到卡上、怎么把卡喂满讲透了。本篇聊用户真正感知到的那一端流式输出打字机效果。用户问一个问题前端要能在 200ms 内看到第一个字然后稳定地一个 token 一个 token 蹦出来断网还能续上。这背后是 SSE/WebSocket 长连接、服务端背压、首包TTFT优化、断点续传一整套工程。也是华为云大模型 API 岗、对话产品后端岗必问。前文链接A71 多租户 GPU 虚拟化与推理隔离、A70 推理服务负载均衡与智能请求路由、A61 服务可观测性与延迟吞吐剖析。Browser/App 网关(LB) 推理服务(vLLM/TGI) GPU │ │ │ │ │── GET /v1/chat ──▶ │ │ │ │ (SSE, stream) │── forward ───────────▶ │ prefill │ │ │ │───────▶ │ │◀── data: {照) ──│◀── token#1 ────────────│◀────── │ │◀── data: {片) ──│◀── token#2 ────────────│ decode 流式 │ │ ... │ ... │ (逐 token 回传) │ │◀── data: [DONE] ──│◀── 结束 ───────────────│ │ │ │ │ │ SSE 长连接全程保持服务端按生成节奏 push客户端按渲染节奏消费表流式协议选型协议方向自动重连实现复杂度适用SSE (text/event-stream)服务端→客户端单向浏览器原生 EventSource低对话流式输出首选WebSocket双向需自实现中需客户端上行语音/打断普通 HTTP 客户端轮询双向轮询无低但体验差不支持 SSE 的古老环境一、SSE 长连接的最小实现SSE 本质是Content-Type: text/event-stream的分块 HTTP 响应服务端用flush把每个 token 立即推给客户端而不是等全部生成完才返回。# FastAPI 流式端点SSEfromfastapiimportFastAPI,Requestfromfastapi.responsesimportStreamingResponseimportasyncio,jsonappFastAPI()asyncdeftoken_stream(prompt:str):# 假设 engine.generate_async 是异步逐 token 生成器asyncforpieceinengine.generate_async(prompt,streamTrue):# SSE 格式以 data: 开头两个换行结尾yieldfdata:{json.dumps({content:piece},ensure_asciiFalse)}\n\nawaitasyncio.sleep(0)# 让出事件循环避免阻塞其他连接yielddata: [DONE]\n\napp.post(/v1/chat/stream)asyncdefchat(req:Request):bodyawaitreq.json()returnStreamingResponse(token_stream(body[prompt]),media_typetext/event-stream,headers{Cache-Control:no-cache,X-Accel-Buffering:no},)关键头X-Accel-Buffering: no关掉 Nginx 缓冲否则 SSE 会被攒批Cache-Control: no-cache防止中间代理缓存。二、服务端背压生成快于消费怎么办GPU 生成 token 的速度可能 100 tok/s常快于客户端网络/渲染速度。如果服务端无脑猛推内存里会堆满未发送的 chunk连接一多就 OOM。正确做法是背压客户端消费慢时服务端生成器被卡住。importasyncioasyncdefbackpressure_stream(raw_gen,client_queue_max128):qasyncio.Queue(maxsizeclient_queue_max)# 有界队列即天然背压asyncdefproducer():asyncfortokinraw_gen:awaitq.put(tok)# 队列满则 producer 自动挂起awaitq.put(None)# 结束哨兵asyncdefconsumer():asyncio.create_task(producer())whileTrue:tokawaitq.get()iftokisNone:yielddata: [DONE]\n\n;breakyieldfdata:{json.dumps({content:tok},ensure_asciiFalse)}\n\nasyncforchunkinconsumer():yieldchunk当q满producer的await q.put挂起推理引擎的generate_async也跟着停自然把压力传导回调度层连续批处理里该请求暂时不取新 token。这就是背压链客户端慢 → 队列满 → 引擎暂停 → GPU 让给其他请求。三、首包优化TTFT用户最在意的是多久出第一个字。TTFT 路由耗时 排队 Prefill 耗时 首 token 网络传输。优化点预取/预热长连接复用避免每次握手建连。优先级队列流式对话请求进高优队列减少排队。Prefix Cache系统提示词命中前缀缓存Prefill 直接复用 KV省掉重复计算。分块 Prefill超大上下文时把 Prefill 切成多块穿插 decode避免单次卡死。# 网关层首包计时观测 TTFT 分布importtimeapp.middleware(http)asyncdefmeasure_ttft(request,call_next):starttime.perf_counter()responseawaitcall_next(request)# 用 response 的流式回调记录首个 chunk 到达时刻request.state.ttft_startstartreturnresponse# 在 token_stream 第一个 yield 时打点ttft now - request.state.ttft_start四、断线续传与去重弱网环境下 SSE 可能中途断开。浏览器EventSource会自动重连但重连会重新发请求、从头生成浪费且重复。生产做法服务端给每条流式会话发stream_id客户端重连带Last-Event-ID。网关按stream_id缓存最近 N 个已发 token重连后从断点续推不重新进模型。客户端做幂等同一stream_id的data按序号去重拼接。# 断点续传网关缓存已发事件classStreamResumer:def__init__(self):self.sent{}# stream_id - list[(seq, payload)]defon_send(self,sid,seq,payload):self.sent.setdefault(sid,[]).append((seq,payload))defresume_from(self,sid,last_seq):# 只补发 last_seq 之后的事件return[(s,p)fors,pinself.sent.get(sid,[])ifslast_seq]五、长连接资源治理每个 SSE 连接占一个 worker 协程 一个网关连接。万人同时对话连接数爆炸。治理连接数上限 空闲超时无 token 超过 N 秒断连。网关层用支持长连接的反向代理如 Envoy而非短连接为主的 Nginx 默认配置。多实例下流式会话要粘性同一 stream 落到同一后端否则续传缓存失效。面试速答问SSE 和 WebSocket 怎么选答纯服务端推对话输出用 SSE浏览器原生 EventSource 自动重连、实现简单需要客户端上行语音打断、实时控制才上 WebSocket。问服务端生成比客户端消费快怎么不 OOM答有界队列做背压队列满时推理生成器挂起压力传导回引擎和调度层让 GPU 服务其他请求而不是在内存堆 chunk。问断网重连为什么会重复输出怎么解答EventSource 重连会重新请求从头生成。解法服务端发 stream_id 序号重连带 Last-Event-ID网关从断点续推客户端按序号去重。高频追问清单Nginx 默认会缓冲 SSE 吗哪个响应头能关掉背压链断在哪一层最危险网关/引擎/客户端流式会话在多副本部署下怎么做粘性路由和续传缓存TTFT 的构成拆解哪一段最难优化、为什么Prefix Cache 对首包优化的收益上限受什么限制万人长连接下网关连接数和后端 worker 怎么配比客户端怎么区分模型真的说完了和网络断了流式输出如何保证 token 顺序不乱并发 decode 多候选