共计 3929 个字符,预计需要花费 10 分钟才能阅读完成。
最近在给团队搭建一套私有化推理集群,底层用两台双卡 RTX 4090 跑 vLLM(OpenAI 兼容接口),前端通过 Next.js 接入内部协同系统。刚跑通功能时一切看起来很美好,但一上真实压力测试,前端开发立马找上门吐槽:“这大模型吐字怎么跟便秘一样?要么半天不出来,出来就是一大坨;稍微让它写个长代码或者跑 DeepSeek-R1 这种长思维链(CoT),跑满 60 秒直接给我弹 504 Gateway Timeout!”
这基本上是所有自建大模型 API 转发网关团队都会踩的第一道坎。大模型的输出本质上是基于 HTTP Server-Sent Events (SSE) 的长文本流式传输,与传统的短平快 REST API 有着本质的传输特征差异。今天聊聊怎么把网关层的配置调明白,彻底解决缓冲积压、超时断流以及显存空转问题。
一、痛点拆解:为什么普通反代会干碎 SSE 流?
传统的 Web 反向代理(如 Nginx、Traefik 默认模式)是为高吞吐的静态资源和普通 API 设计的。其核心设计原则是:尽可能在内存中攒齐上游的一整块响应(Buffer),再一次性刷给客户端,以此减少网络系统调用的上下文切换,提高传输效率。
但对于 LLM 流式输出(stream=true),这种缓冲机制是致命的:
- 缓冲积压(Proxy Buffering): vLLM 一个 Token 一个 Token 地往外发,Nginx 却在后台默默攒着,非要等攒够 4KB 或缓冲区满了才肯往客户端发一个分块。用户在界面上看到的就是打字机彻底失效,“卡顿 3 秒,突发一堆字”。
- 超时截断(Upstream Timeout): 推理模型遇到极难 Prompt 时,TTFT(Time To First Token)可能长达数秒,中间如果生成复杂的 Reasoning 过程,上游很长时间没有数据产出,反代服务器默认的 60 秒 read timeout 会直接把连接斩断。
- 无效算力浪费(显卡空转): 用户觉得回答不对,点击了前端的“停止生成(Stop Generation)”,前端断开了 HTTP 连接。但由于网关和后端推理引擎没做好取消信号传递,后端显卡还在全力以赴地 Decode,白白占满算力。
二、Nginx 生产级配置:驯服 SSE 长连接
如果你用 Nginx 做入口网关,千万不要直接拿默认的 proxy_pass 去套 vLLM。下面是我在生产环境中反复调优后固化的 vllm_gateway.conf 核心片段:
upstream vllm_backend {
# 负载均衡算法:最少连接优先(适合大模型这种耗时极不均一的场景)least_conn;
server 192.168.10.101:8000 max_fails=2 fail_timeout=10s;
server 192.168.10.102:8000 max_fails=2 fail_timeout=10s;
# 极关键:保持与后端长连接的连接池大小,避免每次握手浪费几毫秒
keepalive 64;
}
server {
listen 443 ssl http2;
server_name llm-api.internal.local;
ssl_certificate /etc/nginx/certs/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/privkey.pem;
location /v1/ {
proxy_pass http://vllm_backend;
# 1. 强制使用 HTTP/1.1 协议,配合清空 Connection Header 维持后端 Keepalive
proxy_http_version 1.1;
proxy_set_header Connection "";
# 2. 彻底关闭上游缓冲,实现实时逐 Token 吐字
proxy_buffering off;
proxy_cache off;
chunked_transfer_encoding on;
# 3. 超时时间调大(应对思维链长响应),关闭代理层连接自动断开
proxy_connect_timeout 60s;
proxy_send_timeout 600s;
proxy_read_timeout 600s;
# 4. SSE 核心透传 Header
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# 5. 禁用客户端请求体的缓冲,避免超大 Prompt 上传延迟
proxy_request_buffering off;
}
}
核心参数避坑点解析:
proxy_buffering off;:这一行直接切除 Nginx 的缓冲区。只要后端推理引擎发出来一个 Chunk,Nginx 毫秒级直接转发给客户端。proxy_http_version 1.1; proxy_set_header Connection "";:Nginx 默认向上游转发用的是 HTTP/1.0,这会导致keepalive失效。在大并发请求下,网关和 vLLM 之间频繁进行 TCP 三次握手和四次挥手,压测时很容易把系统本地可用端口(Local Port Range)跑崩爆出Cannot assign requested address。proxy_read_timeout 600s;:常规配置只有 60s。对于需要深思熟虑的推理模型(如 DeepSeek-R1 满血版或 Qwen-32B-Thinking),跑一个复杂数学题思考几百个 Token 很容易越过 60 秒的大关,务必拉长到 10 分钟。
三、现代极客选择:用 Caddy 搞定自动化配置
如果你嫌 Nginx 配置太啰嗦,且不想折腾证书,Caddy 是目前个人开发或中小团队最顺手的替代品。Caddy 默认原生支持对 text/event-stream 响应做 Flush,但如果要达到工业级稳定,依然需要显式声明一些参数。
这是我另一套环境里跑的 Caddyfile:
llm-api.internal.local {
reverse_proxy http://192.168.10.101:8000 http://192.168.10.102:8000 {
# 负载策略:基于活跃请求数
lb_policy least_conn
# 健康检查(检查 vLLM 的 health 接口)health_uri /health
health_interval 5s
health_timeout 2s
# 传输层调优
transport http {
keepalive 300s
keepalive_idle_conns 128
# 禁用响应缓冲
response_header_timeout 600s
read_timeout 600s
# 强制即时刷新
write_timeout 600s
}
# 针对 SSE 的直接缓冲清空
flush_interval -1
}
}
注意配置中的 flush_interval -1,这相当于告诉 Caddy:一收到后端的数据包立刻往外 Flush,一纳秒都不要等。在体验上,前端的打字机流式效果瞬间变得丝滑无比。
四、实操暗坑:用户点击“停止”,后端卡顿依然在继续?
调通了网关后,我们在压测中抓到一个隐秘的性能损耗:在 Web 界面中,用户经常在看到前几十个字不满意后,直接按 Esc 或点“Stop”。前端通常会直接调用 AbortController.abort() 掐断 HTTP 连接。
但此时翻看 vLLM 的后台终端日志,你会发现它依然在吭哧吭哧地打 Log,直到 max_tokens 耗尽!这意味着 你宝贵且昂贵的显存计算资源,在为空气做 Decode。
解决方案:前后端协同中断
vLLM 启动参数中,必须确保没有关闭中断监听。另外,Nginx 在客户端断开时,默认并不会主动切断上游连接,除非配置了:
# 客户端断开连接时,Nginx 立即中断上游代理请求
proxy_ignore_client_abort off;
将 proxy_ignore_client_abort 设为 off 之后,只要前端浏览器一中断 SSE 连接,Nginx 会迅速发送 RST 或 FIN 关闭与 vLLM 的 HTTP 连接。vLLM 的异步调度器(AsyncLLMEngine)感知到客户端 Socket 异常后,会立即调用内部的 abort_request(request_id),显存直接释放,下一轮请求马上能被调度进去。
五、性能调优前后数据对比
我们在双机 4 卡环境下,使用自定义的 Python 异步脚本并发模拟 50 个客户端长连接请求,Prompt 固定在 1.5k Tokens,输出 1k Tokens:
| 配置状态 | TTFT (首字延迟) | 客户端体感延迟 (P99) | 连接意外断开率 | 并发显存释放响应 |
|---|---|---|---|---|
| 默认 Nginx 配置 | ~3.2s | 长达 4~6s(由于 Buffer 积攒) | 18.4%(长推理 504 超时) | 延迟至生成完毕 |
| 关闭 Buffer + Keepalive + 超时优化 | ~0.8s | < 50ms(逐字秒推) | 0.0% | 毫秒级释放(Abort 触发) |
总结与避坑心得
大模型服务和传统微服务架构有很大的不同。很多时候你花几十万搞了双卡甚至八卡服务器,结果被最外层一个默认的 proxy_buffering on 和 read_timeout 60s 卡住了实际交付体验。总结下来就三句话:
- 流式业务务必关掉反代 Buffer(Nginx 设置
proxy_buffering off;,Caddy 设置flush_interval -1)。 - 读写超时必须放宽到 300~600 秒,给模型的深度推理留出足够的物理时间。
- 一定要允许客户端取消信号传递到后端(
proxy_ignore_client_abort off;),在