流式响应 线上请求出现“待处理”问题

前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站

文章目录

排坑指南:本地好用线上"假死"?详解 SSE 流式响应在 Nginx 下的缓冲问题

前言

最近在开发一个基于 SSE(Server-Sent Events)的流式接口,后端采用 Plan 模式生成数据。在本地开发调试时,体验非常丝滑:请求发出的第一秒,浏览器就能收到后端返回的第一个 Plan 片段,就像打字机一样一个个蹦出来。

然而,代码一部署到线上环境,诡异的事情发生了:

不管怎么测,线上的流式响应都像是"卡住"了一样。打开 Chrome 的 Network 面板,发现请求一直处于 Pending(待处理)状态,没有任何数据回传。

更糟糕的是,这种"待处理"状态会持续很久,最后要么是一次性把所有数据吐出来(流式变成了块式),要么是等待时间过长直接爆出 500504 Gateway Timeout 错误。

经过一番排查,终于揪出了幕后黑手。这篇文章就来复盘一下这个典型的 SSE + Nginx 缓冲(Buffering) 问题,并附上经过验证的解决方案和前端代码优化建议。


问题分析:谁"吃"了我的数据?

在本地开发时,浏览器通常直接连接后端服务,或者通过简单的本地代理,数据通道是畅通无阻的。

但到了线上,架构通常变成了:

txt 复制代码
Client (浏览器) -> Nginx (反向代理/网关) -> Backend (后端服务)

罪魁祸首就是 Nginx 的默认行为:Proxy Buffering(代理缓冲)。

Nginx 为了提升 I/O 效率,默认会开启缓冲。当后端开始吐出 SSE 数据流(比如几个字节的 Plan 片段)时,Nginx 觉得"这点数据太小了,不值得跑一趟",于是它把数据先拦截下来,存到内存缓冲区里。

这就导致了我们看到的现象:

  1. 后端:其实早就把数据发出去了。
  2. Nginx:拿着数据不发,想攒够一定大小(通常是 4k 或 8k)或者等请求结束再一起发。
  3. 前端 :收不到第一个字节,浏览器认为连接还没建立好,所以一直显示 Pending
  4. 超时:如果后端生成完整内容耗时太久(超过 Nginx 超时设置),Nginx 等不及了,直接断开连接,抛出 500/504 错误。

解决方案:如何打通"任督二脉"

既然知道了原因是 Nginx 缓冲,解决思路就是告诉 Nginx:"这还是个急件,来多少发多少,不要攒!"

✅ 方法一:修改 Nginx 配置(已验证有效)

这是最直接的运维层面手段,特别适合无法修改后端代码或者全局代理配置的场景。我们需要在 Nginx 对应的 location 中显式关闭缓冲。

配置示例:

Nginx 复制代码
location /project/content/completion {
    proxy_pass http://backend_service;
    
    # 1. 核心:关闭代理缓冲
    # 告诉 Nginx 不要缓存后端响应,收到什么直接发给客户端
    proxy_buffering off;
    
    # 2. 可选:关闭 gzip
    # 流式数据如果被 gzip 压缩,通常也需要积攒数据块,建议关闭
    gzip off; 
    
    # 3. 关键:增加超时时间
    # AI 生成/流式响应通常耗时较长,默认的 60s 很容易超时
    proxy_read_timeout 300s;
    proxy_send_timeout 300s;
}

实测效果

配置生效后,线上的 Network 面板终于恢复正常,TTFB(首字节时间)瞬间降低,流式效果与本地一致。

⚡ 方法二:后端设置响应头(推荐的最佳实践)

如果你不想改 Nginx 配置,或者希望把控制权掌握在代码里,可以让后端在返回 SSE 数据时,带上特定的 HTTP Header。这是一种更优雅的解耦方式。

在 SSE 响应头中添加:

HTTP 复制代码
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
X-Accel-Buffering: no  <-- 这一行是写给 Nginx 看的
  • X-Accel-Buffering: no:这是一个 Nginx 特有的指令,Nginx 看到这个头,就会自动禁用当前连接的缓冲,而不需要运维去改配置文件。

进阶优化:前端代码的健壮性提升

解决了网络传输问题,回过头看前端的解析逻辑。在排查过程中,我发现原本使用的 XMLHttpRequest 处理逻辑存在两个隐患:

  1. 解析逻辑冗余onload 里重复处理了整个数据流。
  2. 内存隐患xhr.responseText 是不断累加的,如果流式内容非常长,内存占用会越来越大。

针对上述问题,这里提供一份更健壮的 XHR 处理代码(兼容性好,逻辑严密):

TypeScript 复制代码
const handleStreamRequest = () => {
    const xhr = new XMLHttpRequest();
    xhr.open('POST', fullUrl, true);
    // ... 设置 header ...

    // 游标:记录已经处理过的字符长度
    let seenBytes = 0; 
    // 缓存:用于存放尚未组成完整消息的碎片数据
    let rawResponseBuffer = '';

    xhr.onprogress = (event) => {
        // 1. 增量获取:只截取本次新增的数据,避免重复处理
        const currentResponse = xhr.responseText;
        const newChunk = currentResponse.slice(seenBytes);
        seenBytes = currentResponse.length; // 更新游标

        // 2. 拼接到缓冲区
        rawResponseBuffer += newChunk;

        // 3. 按双换行符分割 SSE 消息块
        const parts = rawResponseBuffer.split('\n\n');
        
        // 核心逻辑:保留最后一个可能不完整的块
        // parts.pop() 取出的永远是最后一段(可能是空的,也可能是一半的数据)
        // 将其放回 buffer 等待下一次拼接
        rawResponseBuffer = parts.pop() || ''; 

        // 4. 处理完整的消息块
        for (const part of parts) {
            parseAndDispatch(part);
        }
    };

    xhr.onload = () => {
        // 处理最后残留在 buffer 中的数据
        if (rawResponseBuffer.trim()) {
            parseAndDispatch(rawResponseBuffer);
        }
        console.log('Stream completed');
    };
    
    // ... 发送请求 ...
};

// 抽离解析逻辑
const parseAndDispatch = (message: string) => {
    if (!message.trim()) return;
    const lines = message.split('\n');
    for (const line of lines) {
        if (line.startsWith('data:')) {
            const dataStr = line.replace(/^data:\s*/, '').trim();
            if (dataStr === '[DONE]') return;
            try {
                const parsedData = JSON.parse(dataStr);
                // 更新 UI 逻辑...
            } catch (e) {
                console.error('JSON Parse error:', e);
            }
        }
    }
};

总结

  1. 现象:本地 SSE 正常,线上一直 Pending 或 500/504。
  2. 原因 :Nginx 开启了 proxy_buffering,拦截了后端的小块数据。
  3. 对策
    • 运维侧 :Nginx 配置 proxy_buffering off;
    • 开发侧 :响应头添加 X-Accel-Buffering: no
  4. 防守:前端代码要注意"增量读取",避免全量字符串操作带来的性能损耗。

希望这篇踩坑记录能帮到同样遇到"流式卡顿"的朋友们!

相关推荐
Moment1 天前
Vibe Coding 时代,到底该选什么样的工具来提升效率❓❓❓
前端·后端·github
Victor3561 天前
MongoDB(27)什么是文本索引?
后端
可夫小子1 天前
OpenClaw基础-3-telegram机器人配置与加入群聊
后端
IT_陈寒1 天前
SpringBoot性能飙升200%?这5个隐藏配置你必须知道!
前端·人工智能·后端
小时前端1 天前
React性能优化的完整方法论,附赠大厂面试通关技巧
前端·react.js
Nicko1 天前
Jetpack Compose BOM 2026.02.01 解读与升级指南
前端
aiopencode1 天前
使用 Ipa Guard 命令行版本将 IPA 混淆接入自动化流程
后端·ios
小蜜蜂dry1 天前
nestjs学习 - 控制器、提供者、模块
前端·node.js·nestjs
优秀稳妥的JiaJi1 天前
基于腾讯地图实现电子围栏绘制与校验
前端·vue.js·前端框架
掘金者阿豪1 天前
Kavita+cpolar 打造随身数字书房,让资源不再混乱,通勤 、出差都能随心读。
后端