尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Nginx缓冲导致SSE卡顿、WebSocket断连?关键配置与排查指南

发布时间:2026/9/11 4:47:53

资讯中心
01
ARTICLE

Nginx缓冲导致SSE卡顿、WebSocket断连?关键配置与排查指南

Nginx缓冲导致SSE卡顿、WebSocket断连?关键配置与排查指南
把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。SSEServer-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: websocket和Connection: 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这种动态内容一般不会命中但如果配置了全局缓存策略容器或响应头没排除就可能拿到旧数据。gzipNginx如果对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_buffering和proxy_cache并调整超时时间。三条配置缺一不可proxy_buffering off; proxy_cache off; proxy_read_timeout 3600s;解释一下为什么是这三条proxy_buffering off关闭缓冲区让Nginx从后端收到的数据立刻转发给客户端。这是解决SSE“一卡一卡”的核心开关。proxy_cache offSSE/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.1SSE依赖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不加-Ncurl自己也会做缓冲输出所以测试时-N必须带上。如果你测试的是WebSocketcurl没办法像浏览器那样完整模拟建议用websocat或者写一小段Node.js脚本连上去观察连接稳定性。配置改动后记得重载Nginxnginx -t nginx -s reloadnginx -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_upgrade和proxy_set_header Connection upgradeWebSocket连接断开收到1006空闲超时被Nginx掐断调大proxy_read_timeout和proxy_send_timeout检查服务端心跳后端日志显示数据已推送前端一直收不到Nginx缓冲或gzip延迟关闭proxy_bufferingSSE 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和gzipWebSocket需要关注Upgrade头。混在一个location里会变得很乱后续维护也容易出错。拆成两个location配置语义清晰出了问题也好定位。第三Nginx的版本差异会影响行为。低版本Nginx1.13之前对WebSocket的支持还不完善需要额外编译模块或打补丁高版本Nginx1.13已经内置了WebSocket代理能力但缓冲行为仍然存在。如果你用的是非常老的Nginx建议直接升级别在旧版本上浪费时间找配置。第四日志是排错的最后一道防线。Nginx的error.log里如果看到upstream timed out说明是后端长时间没有响应导致超时如果看到no live upstreams说明后端IP或端口配置有问题。SSE/WebSocket相关的排错先从access.log看请求是否到达Nginx再从后端日志看请求是否被处理一步步缩窄范围。有一个特别容易被忽略的点access.log里如果请求的status是200但内容长度一直不更新说明连接还挂着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频繁断连折磨不妨先按这篇文章里的配置改一遍大概率能少走几天的弯路。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。