Nginx缓冲导致SSE卡顿、WebSocket断连?关键配置与排查指南
2026/9/11 4:47:43 网站建设 项目流程

把SpringBoot的SSE接口写完,本地一跑,事件一条条往外推,完美。结果一部署到生产,Nginx前面一站,用户反馈变成了“打字机效果”——不是那种流畅的打字机,而是卡一秒、蹦三个字、再卡两秒、蹦五个字。更崩溃的是WebSocket长连接,动不动就onclose code: 1006,客户端自动重连,连上了又断,日志里还躺着stream disconnected before completion: failed to send websocket request这种云里雾里的报错。

如果你也遇到过这类问题,大概率不是SpringBoot的锅,而是Nginx在替你“操心”,把本该实时流转的数据缓冲起来了。这篇文章就把这个坑彻底讲透:为什么Nginx会缓冲、SSE和WebSocket哪些场景会被卡、关闭哪些配置能解决、怎么验证真的生效了,最后附上我踩坑多次总结的排查心得。

1. 先复盘问题:本地正常,一上Nginx就“卡”住

1.1 两种典型的“卡顿”现象长什么样

先说SSE。SSE(Server-Sent Events)是服务端单向推送的利器,SpringBoot里用SseEmitter就能很轻松地实现。我最早是在一个AI对话模块里用到的——用户提问后,后端大模型结果分多个chunk返回,前端拿text/event-stream一边接收一边渲染,做出类似ChatGPT的打字机效果。本地开发环境直连SpringBoot的8080端口,数据流非常顺畅,每条消息几十毫秒就能到达。

部署到Nginx反向代理之后,现象就变成了:

  • 前端EventSource连接能建立,但数据不是一条条到的,而是一坨一坨地来;
  • 明明后端日志显示chunk早就推给Nginx了,前端却要等好几秒才收到;
  • 如果Nginx的proxy_read_timeout配置得短,连接还可能直接断开,前端EventSource会自动重连,造成请求轰炸。

再说WebSocket。WebSocket是双向长连接,SpringBoot原生支持WebSocketHandler,也可以走STOMP协议。上了Nginx之后常见的坑有两个:一种是握手失败,response code 400或者101迟迟不来;另一种是握手成功了,但过一会儿连接就被静默断开,客户端收到code: 1006——这个错误码在WebSocket协议里表示“连接异常关闭,没有正常的close帧”。

我在排查这类问题的时候还发现,很多人会忽略Nginx对Upgrade头的处理。WebSocket握手依赖Upgrade: websocketConnection: Upgrade这两个HTTP头,Nginx默认的代理配置不会自动透传,需要在location里手动设置。这个不处理好,握手就过不去,更别提后面的数据传输了。

1.2 受害场景不止AI对话,这些业务都容易中招

很多人以为SSE只有AI对话才用,实际上凡是“服务端主动推送”的实时场景都会被Nginx缓冲问题影响:

  • 实时日志页面:比如运维平台查看应用日志,日志是一行行刷出来的,如果被缓冲,页面就会“半天不动,突然输出一大片”;
  • 价格/库存实时更新:电商后台或者行情软件,价格变动要秒级推送,缓冲几秒钟在金融场景里可能就是事故;
  • 通知中心:站内信、告警推送、工单状态变更,这些业务对实时性要求高,缓冲会造成“已处理但用户看不到变化”的错觉;
  • WebSocket在线协同:白板协作、聊天室、多人编辑光标位置同步,这类场景一旦断连或者延迟,用户体验直接崩。

之前的标题里那些报错串在一起,其实就是一条完整的事故链:Nginx缓冲导致数据延迟,延迟导致客户端超时,超时导致连接断开,断开导致idle timeout waiting for SSE,或者WebSocket的1006,客户端重连之后又撞上同一个问题。所以解决这个问题的关键,就是让Nginx“不插手”实时数据流的传输。

2. 根因拆解:Nginx为什么非要“多管闲事”

2.1 Nginx的缓冲机制到底在干嘛

Nginx作为反向代理,最常用的功能就是“接受客户端的请求,转发给后端,把后端的响应再拿回来交给客户端”。在这个过程里,Nginx默认开启了一个叫proxy_buffering的功能——这名字直译过来就是“代理缓冲”。

它的工作方式是:Nginx向后端发起请求后,并不会把后端返回的每一块数据都立刻转发给客户端,而是先把数据放进内存缓冲区(buffer)里攒着,攒到一定量或者后端请求处理完毕,才把整块数据一次性发给客户端。这就像快递中转站——包裹到了先放进仓库,装满一卡车再统一发走,而不是来一件发一件。

这个机制对于普通API接口是友好的。比如一个查询接口,后端花300毫秒生成完整响应,Nginx攒够了再一次性返回,客户端拿到的是完整JSON,体验并不差。缓冲还能减少后端和客户端之间的TCP交互次数,降低网络小包的数量,在高并发场景下确实能减轻一些压力。

问题出在“攒够再发”这个逻辑上。SSE和WebSocket恰恰是需要“来一条发一条”的场景,Nginx这个中转仓库一囤货,实时性就毁了。

2.2 为什么SSE和WebSocket最怕缓冲

SSE的核心是“服务端主动推送事件流”。它在HTTP响应里使用Content-Type: text/event-stream,服务端可以不停地把数据写成data: xxx\n\n的形式,客户端通过EventSource API逐条接收。这里的关键是——每条事件都需要“即时”到达客户端,才能实现打字机效果或者实时刷新。

如果Nginx开了proxy_buffering,它会等缓冲区满了才转发,那么SSE的每条消息都可能积压几百毫秒到几秒。更糟糕的是,如果缓冲区分批发送,前端会看到一段数据突然涌出来,然后又卡住,视觉上就是“一顿一顿”的。

WebSocket的情况更特殊。WebSocket经过HTTP Upgrade握手后,协议就从HTTP切换成了全双工的WebSocket帧传输。Nginx在代理WebSocket时,理论上也应该全程透传数据帧。但如果在握手阶段没有配置Upgrade头,或者proxy_read_timeout设置得不合理(默认只有60秒),连接就会被Nginx在空闲超时后主动掐断。客户端拿不到close帧,只能收到一个异常断开的状态码——这就是1006的来源。

你可以这样理解:SSE怕的是“货被囤在中转仓”,WebSocket怕的是“卡车在仓库门口等太久被保安赶走”。

2.3 除了proxy_buffering,还有哪些“帮倒忙”的配置

实际工作中我发现,问题往往不是一个配置造成的,而是几个配置叠加放大。除了proxy_buffering,以下这几个都容易让SSE/WebSocket体验变差:

  • proxy_cache:如果某个URL命中了缓存,Nginx会直接把缓存内容返回,根本不往后端转发。虽然对SSE这种动态内容一般不会命中,但如果配置了全局缓存策略,容器或响应头没排除,就可能拿到旧数据。
  • gzip:Nginx如果对text/event-stream类型的响应做gzip压缩,也会引入缓冲延迟。因为gzip需要攒一段数据才能压缩得更高效,这在SSE场景下等于又加了一层缓冲。我习惯在SSE的location里直接禁用gzip。
  • proxy_read_timeout/proxy_send_timeout:默认60秒,如果SSE超过60秒没有新数据(且没有心跳),或者WebSocket在60秒内没有双向流量,连接就被Nginx断掉。对实时推送场景来说,这个超时值通常要调大,或者配合心跳机制保持活跃。
  • keepalive相关:proxy_http_version 1.0是Nginx代理的默认值,HTTP/1.0不支持长连接。如果需要Nginx与后端之间保持keepalive,需要显式改成proxy_http_version 1.1并配置proxy_set_header Connection ""

这几个配置单独看问题不大,组合在一起就让“实时”变成了“不实时”。

3. 解决方案:核心配置就这几条

3.1 先记住两个最关键的开关

面对Nginx缓冲问题,最直接的解决方法是关闭proxy_bufferingproxy_cache,并调整超时时间。三条配置缺一不可:

proxy_buffering off; proxy_cache off; proxy_read_timeout 3600s;

解释一下为什么是这三条:

  • proxy_buffering off:关闭缓冲区,让Nginx从后端收到的数据立刻转发给客户端。这是解决SSE“一卡一卡”的核心开关。
  • proxy_cache off:SSE/WebSocket都是动态数据,绝不能进缓存,显式关闭可以排除干扰。
  • proxy_read_timeout 3600s:拉长Nginx读取后端响应的超时时间。对SSE来说,如果服务端长时间没有推送事件,连接会一直挂着,默认60秒就会断;对WebSocket来说,3600秒内只要有任何双向流量,连接就能保持。业务需要更长的连接就继续调大。

这里要特别说一句:超时时间和心跳是配合用的。如果你的SSE服务端每30秒发一条心跳注释,proxy_read_timeout设置90秒就够,不需要几小时。如果服务端完全不发心跳,才需要把超时设得很长来保证连接不被Nginx掐断。

3.2 SSE场景的Nginx配置模板

一个可用的SSE反向代理配置,location至少要长这样:

server { listen 80; server_name your-domain.com; location /sse { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; 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; proxy_buffering off; proxy_cache off; proxy_read_timeout 3600s; proxy_send_timeout 3600s; proxy_set_header Connection ""; gzip off; } }

我逐行说明几个容易忽视的点:

  • proxy_http_version 1.1:SSE依赖HTTP长连接,HTTP/1.1是基本前提。不设置的话Nginx默认用HTTP/1.0向后端发请求,部分后端会直接拒绝或不能维持连接。
  • proxy_set_header Connection "":这个空值是为了关闭Nginx与后端之间默认的Connection: close头,让Nginx复用与后端的TCP连接。如果不设置,每个SSE请求都新建一个连接,并发一高后端的连接数就直接爆了。
  • gzip off:禁用gzip压缩,避免text/event-stream被压缩层缓冲。作为补充,如果你希望保留其他接口的gzip压缩,可以在server层设置gzip_types排除掉text/event-stream

SpringBoot端配合这个配置,SseEmitter的创建要设置一个合理的超时时间,比如:

SseEmitter emitter = new SseEmitter(3600_000L);

如果业务上需要更长,可以设置成0L表示永不过期,但这种做法在生产环境不推荐,最好还是让emitter超时时间略大于Nginx的proxy_read_timeout,保证前端断线时服务端能及时释放资源。

3.3 WebSocket场景的Nginx配置模板

WebSocket的场景在Nginx里的配置稍微多一点,核心在于处理Upgrade握手和长连接保活:

server { listen 80; server_name your-domain.com; location /ws { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; 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; proxy_read_timeout 3600s; proxy_send_timeout 3600s; proxy_buffering off; proxy_cache off; } }

关键区别就两个头:

  • proxy_set_header Upgrade $http_upgrade:把客户端的Upgrade: websocket头透传给后端,WebSocket握手才可能成功。
  • proxy_set_header Connection "upgrade":由于HTTP/1.1里Connection头属于“逐跳头”,Nginx默认不会透传,必须显式设置为upgrade,后端才能识别这是一次协议升级请求。

有同学可能听说过Nginx从1.13开始就支持WebSocket了,默认配置好像也能跑起来。但“能握手”和“稳定长连接”是两回事——如果你不设置超时,默认60秒的空闲就断;不设置proxy_buffering off,极端情况下WebSocket的数据帧也会被缓冲。所以别偷懒,该配的都配上。

3.4 不想改Nginx配置?还可以用响应头控制

如果你没有Nginx配置的修改权限,或者不想为某个接口单独开location,还有一种更细粒度的方式:在后端响应头里加一个X-Accel-Buffering

这个头是Nginx的“语法糖”,后端可以在响应头中声明“这个响应不要缓冲”,Nginx看到后会忽略自身的proxy_buffering设置。SpringBoot里可以通过SseEmitter发送前设置响应头,或者用ResponseBodyEmitter时手动设置:

response.setHeader("X-Accel-Buffering", "no"); response.setHeader("Cache-Control", "no-cache"); response.setContentType("text/event-stream");

这种方式的好处是只对设置了响应头的接口生效,其他普通API仍然保留Nginx的缓冲能力。但有两点要注意:

  • X-Accel-Buffering只在Nginx处理该响应时生效,如果中间还有CDN或者其他代理层,要看它们是否也支持这个头;
  • 如果Nginx配置里显式写了proxy_buffering off,这个头就没什么意义了,因为缓冲本来就已经关了。

所以我个人建议:有配置权限的情况下,优先在Nginx的location里处理;权限受限的情况下,再用响应头方案。

3.5 SpringBoot端的配合细节

Nginx配置只是“打通管道”,SpringBoot端如果没配合好,照样会有问题。这里列几个我实际踩过的坑:

第一个坑是SSE的SseEmitter超时设置过短。SpringMVC的SseEmitter默认超时是30秒,如果不显式设置,前端EventSource连接可能刚建立就准备断连。前面提到的new SseEmitter(3600_000L)就是在解决这个问题。

第二个坑是Controller返回类型写错。SSE接口必须返回SseEmitter或者在响应里显式设置Content-Type: text/event-stream,如果写成了普通对象、字符串,Spring会走JSON序列化,前端EventSource接收到非SSE格式的数据会解析失败。

第三个坑是WebSocket握手路径的冲突。SpringBoot的WebSocket端点如果配置了拦截器做鉴权,而拦截器里抛了异常,Nginx收到的是非101响应,前端表现就是握手失败。这种情况要先把后端日志打开,确认握手阶段到底走到了哪一步。

第四个坑是心跳设计。长连接类接口一定要有心跳机制,无论SSE还是WebSocket。SSE的心跳就是定时发送注释行(以冒号开头的行),WebSocket的心跳是ping/pong帧。心跳的存在可以让Nginx的proxy_read_timeout不用设置得离谱地大,也更利于前端判断连接是否健康。

4. 验证与排错:别让“以为改了”骗了你

4.1 用curl实测是否真正流式

配好Nginx之后,很多人改完配置直接让前端测,结果前端说“还是卡”,然后就开始怀疑配置没有生效。我觉得最快的验证方式,是直接在服务器上用curl发一个SSE请求,观察数据是不是实时打印的。

curl -N --no-buffer http://your-domain.com/sse

参数说明:

  • -N:禁用curl的输出缓冲,让数据一到就打印,和--no-buffer是同一个意思;
  • 如果SSE接口需要鉴权,可以加上-H "Authorization: Bearer xxx",或者把token放在URL query上。

如果curl -N执行后,后端每推一条数据,终端立刻打印一行,说明Nginx的缓冲已经关闭,管道是通的。如果终端就像“憋大招”一样半天不出内容,或者一次性蹦出一大段,说明proxy_buffering off没有真正生效。

这里有个小坑:直接用curl http://your-domain.com/sse不加-N,curl自己也会做缓冲输出,所以测试时-N必须带上。如果你测试的是WebSocket,curl没办法像浏览器那样完整模拟,建议用websocat或者写一小段Node.js脚本连上去,观察连接稳定性。

配置改动后记得重载Nginx:

nginx -t nginx -s reload

nginx -t这一步特别重要,它检查配置文件语法,有问题会直接报错。不要跳过,不要直接reload然后等出问题再回滚。

4.2 常见报错与排查速查表

把我在实际项目里遇到过的、身边同事也踩过的典型问题整理成一个速查表,方便对照排查。

现象根本原因排查方向
SSE数据一卡一卡,不实时proxy_buffering未关闭检查location是否配置proxy_buffering off,确认reload生效
SSE连接被断开,报idle timeout waiting for sseproxy_read_timeout过短或服务端无心跳调大proxy_read_timeout,或服务端增加心跳事件
WebSocket握手失败,response code 400Upgrade头未透传检查是否配置proxy_set_header Upgrade $http_upgradeproxy_set_header Connection "upgrade"
WebSocket连接断开,收到1006空闲超时被Nginx掐断调大proxy_read_timeoutproxy_send_timeout,检查服务端心跳
后端日志显示数据已推送,前端一直收不到Nginx缓冲或gzip延迟关闭proxy_buffering,SSE location里关闭gzip
本地直连正常,走Nginx就老断连多层代理超时叠加每一层代理都要检查超时时间,以最小值为准
浏览器EventSource自动重连,造成大量502后端SseEmitter超时后连接被释放设置SseEmitter超时长于Nginx超时时间

这里面最隐蔽的是“多层代理超时叠加”。如果你的架构是“客户端 -> Nginx -> 网关 -> 服务”,那么Nginx的超时、网关的超时、服务端的超时,任何一个先到,连接就会断。之前遇到过生产环境SSE连接存活时间忽长忽短,最后发现是网关层还有一个默认60秒的读超时,比Nginx的短,于是网关先把连接掐了。排查的时候要从客户端链路一直查到后端,找到最短的那块木板。

4.3 实操心得与避坑补充

最后说几个我自己的实践心得,属于那种“网上教程不会写但真的有用”的内容。

第一,不要全局关闭proxy_buffering。我看到有些文章直接建议在http块里写proxy_buffering off,这在某些情况下确实能“一劳永逸”,但对普通接口是不小的性能浪费。Nginx的缓冲机制对普通API是有优化作用的,关闭后每个响应都即时转发,增加了TCP小包的数量,高并发下吞吐量会下降。正确做法是只对SSE和WebSocket的location单独关闭。

第二,SSE和WebSocket的location要分开配置。虽然这两类长连接都要求关闭缓冲,但SSE需要关注text/event-stream和gzip,WebSocket需要关注Upgrade头。混在一个location里会变得很乱,后续维护也容易出错。拆成两个location,配置语义清晰,出了问题也好定位。

第三,Nginx的版本差异会影响行为。低版本Nginx(1.13之前)对WebSocket的支持还不完善,需要额外编译模块或打补丁;高版本Nginx(1.13+)已经内置了WebSocket代理能力,但缓冲行为仍然存在。如果你用的是非常老的Nginx,建议直接升级,别在旧版本上浪费时间找配置。

第四,日志是排错的最后一道防线。Nginx的error.log里如果看到upstream timed out,说明是后端长时间没有响应导致超时;如果看到no live upstreams,说明后端IP或端口配置有问题。SSE/WebSocket相关的排错,先从access.log看请求是否到达Nginx,再从后端日志看请求是否被处理,一步步缩窄范围。有一个特别容易被忽略的点:access.log里如果请求的status200但内容长度一直不更新,说明连接还挂着,Nginx并没有把它断开,这往往就是缓冲导致的表象。

第五,如果是HTTPS场景,SSL握手本身也会带来额外的连接建立开销。长连接明显比短连接更耗Nginx的worker连接数,所以SSE/WebSocket的并发上限通常比普通API低很多。压测的时候要按长连接场景评估,不能按普通接口的QPS标准来衡量。

第六,尽量在服务端加一个健康检查或者心跳日志。之前排查一个WebSocket频繁断开的问题,客户端每重连一次,后端就多一个线程挂起,连接数飙升,最后把线上一台机器打挂了。加了心跳后,连接虽然是长连接,但每30秒有一次双向流量,不仅Nginx不会误断,后端也能及时感知死连接并清理。

结尾

就我个人经验而言,遇到SSE/WebSocket被Nginx“卡住”的问题,第一反应不是怀疑SpringBoot代码,而是先在Nginx配置里检查这四件事:proxy_buffering是不是关掉了、proxy_cache有没有排除、Upgrade头有没有透传、超时时间是不是够长。把这四件事排查完,大部分问题都能解决。

我的习惯是:上线之前先用curl -N http://domain/sse验证一次流式输出,再写一个简单的WebSocket客户端脚本跑1分钟,观察有没有断开重连。这套检查流程只需要几分钟,但能帮你在用户反馈“卡了”“断了”之前就把问题掐死在摇篮里。如果你现在正被SSE流式输出卡顿或者WebSocket频繁断连折磨,不妨先按这篇文章里的配置改一遍,大概率能少走几天的弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询