跳至正文

AI 回答一个字都不出?Nginx 这三层缓冲,把流式输出全憋死了

上个月帮朋友把内部知识库的问答接口改成流式输出,后端 Python + SSE,本地测试逐字往外蹦,效果很丝滑。结果一上生产,前端半天一个字都不出,等十几秒之后整段回答“啪”地一次性砸出来,跟没改一样。

逐字节流输出与缓冲后一次性输出的对比

前端同事第一反应是 WebSocket 帧解析有 bug,差点去重写渲染层。我说先别动前端——流式这种事,最怕的不是慢,是“攒”。链路上任何一层攒够了再往下发,流式就退化成非流式。而你本地是直连,生产中间隔着好几层代理,每一层默认都在攒。

一、先确认问题在哪一层:curl -N 是最省事的探针

排查流式问题,别开浏览器,先上终端:

curl -N -H "Content-Type: application/json" \
  https://example.com/api/chat/stream \
  -d '{"q":"用三句话解释什么是SSE"}'

-N(–no-buffer)会关掉 curl 自己的缓冲,让数据到多少显示多少。看结果分两种情况:

  • curl -N 也是卡住后一次性吐出:问题在服务端到 Nginx 这一段,Nginx 配置或后端缓冲没关。
  • curl -N 是逐字吐,浏览器才一次性出:问题在 Nginx 之后的链路,重点查 CDN 和浏览器缓存。
用终端排查流式接口,定位缓冲问题

这一步 30 秒就能把故障域砍掉一半。我当时 curl -N 就是一次性吐出,问题锁定在 Nginx。

二、Nginx 的三层“蓄水池”,一个都别留

请求经过多层网关,缓冲层把数据流堵住

Nginx 对上游响应默认开着代理缓冲(proxy_buffering):先把响应攒进自己的 buffer,攒不下再写临时文件,攒完才往客户端发。这对普通页面是优化,对流式是灾难。除了它,还有 gzip 压缩和 fastcgi 缓冲两层也在偷偷攒。流式路径的 location 大概要长这样:

location /api/chat/stream {
    proxy_pass http://127.0.0.1:8000;
    proxy_http_version 1.1;
    proxy_set_header Connection "";

    # ① 关代理缓冲:数据到多少发多少
    proxy_buffering off;
    proxy_cache off;

    # ② 禁用临时文件兜底,防止 Nginx 又把流写盘攒批
    proxy_max_temp_file_size 0;

    # ③ 默认 60s,模型排队久了直接 504
    proxy_read_timeout 300s;
    proxy_send_timeout 300s;

    # ④ 尊重后端的缓冲声明,双保险
    add_header X-Accel-Buffering no;

    # ⑤ gzip 需要攒数据判断压缩比,对流式是隐形杀手
    gzip off;
}

逐条说下为什么:proxy_http_version 1.1 必须写,Nginx 默认用 HTTP/1.0 访问上游,1.0 没有 chunked 传输编码,Nginx 只能等完整 body 才知道长度——这一条漏了,前面全白配。Connection "" 是配合 1.1 复用长连接的标准写法。X-Accel-Buffering no 这个响应头还会被传到下游,让中间的 CDN 也别缓冲。

三、后端是 PHP-FPM 的话,还有第四层

如果你的流式接口跑在 PHP 上,fastcgi 这条链路也有一层缓冲,光关 Nginx 的 proxy_buffering 没用:

location ~ \.php$ {
    ...
    fastcgi_buffering off;
}

PHP 代码侧也要把输出缓冲清干净,并主动声明流式:

header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('X-Accel-Buffering: no');
while (ob_get_level() > 0) { ob_end_flush(); }
ob_implicit_flush(true);

另外 php.ini 里 zlib.output_compression = On 的话也得关,它和 Nginx 的 gzip 是一回事:压缩算法要攒够数据才划算,流式直接被攒死。

四、五个真实踩坑

  • gzip 是最隐蔽的一层。全局 gzip on 开着时,location 里的 proxy_buffering off 照样白搭,症状一模一样。流式路径必须显式 gzip off,CDN 层的自动压缩也要顺手关掉或加旁路规则。
  • proxy_http_version 默认 1.0。没有 chunked 编码,Nginx 只能等完整响应,这是“配置都加了还是不行”的头号原因。
  • proxy_read_timeout 60s 不够用。大模型排队久时 60 秒没吐第一个 token 就 504,前端报“网络错误”,实际后端还在跑,用户一刷新请求翻倍。
  • 后端误设了 Content-Length。流式响应一旦带上固定长度,浏览器就必须等满这个字节数才渲染。SSE / chunked 响应绝对不能手设 Content-Length。
  • CDN 缓存规则把 /api/* 也缓存了。接口响应被当成静态资源命中缓存,流式直接变“录播”。给 API 路径显式设 Bypass Cache。

五、上线前一分钟自检

  • 终端 curl -N 请求接口,确认数据是一行一行往外蹦,而不是转半天一次性出;
  • 浏览器 Network 面板看 Response,流式正常时内容会持续增长;
  • 临时绕过 CDN 直连源站再测一遍,确认问题不在 CDN 层。

流式输出不是“打开一个开关”,而是让链路上每一层都允许它流。浏览器、CDN、Nginx、PHP/应用四层里只要有一层在攒,用户的体感就是“AI 卡死了”。下次再遇到半天不出字,先 curl -N,再从外往里逐层关缓冲,基本十分钟内能定位。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注