仅用Nginx静态服务器部署SSE异常:连接频繁断开重连排查
我花了一整天尝试用Nginx实现Server-sent-events(SSE),先是不通过proxy_pass直接实现,后来又试了proxy_pass,但用JavaScript的EventSource连接端点时,收到第一个事件后连接就断开并自动重连,完全失去了SSE长连接的意义,跟轮询没区别。我的需求是在生产环境部署静态Nginx服务器时,通过SSE发送当前代码提交哈希(这个哈希会在Docker构建或运行时写入Nginx配置)。下面是我接近可行的配置,不管用不用proxy_pass,两个端点都存在同样的问题:
Nginx配置
# load_module /usr/lib/nginx/modules/ngx_http_headers_more_filter_module.so; # user www www; ## Default: nobody worker_processes 1; # error_log logs/error.log; # pid logs/nginx.pid; # worker_rlimit_nofile 8192; events { worker_connections 1024; } http { # send_timeout 1800; # keepalive_timeout 10m; # client_max_body_size 128M; # proxy_max_temp_file_size 0; # proxy_buffering off; # server_names_hash_bucket_size 256; server { listen 8080; server_name localhost; # index index.html; # root /usr/share/nginx/html; location = /sse { proxy_set_header Host $host; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_pass http://proxy; proxy_buffering off; proxy_request_buffering off; proxy_cache off; # proxy_connect_timeout 159s; # proxy_send_timeout 600; # proxy_read_timeout 600; # proxy_buffer_size 64k; # proxy_buffers 16 32k; # proxy_busy_buffers_size 64k; # proxy_temp_file_write_size 64k; fastcgi_keep_conn on; # add_header Connection "keep-alive"; # add_header X-Accel-Buffering "no"; # chunked_transfer_encoding on; # more_clear_headers "Content-Length"; # default_type text/event-stream; } } server { listen 8081; server_name localhost; location = /sse { # add_header Connection "keep-alive"; # expires off; # default_type text/event-stream; default_type ""; add_header Content-Type "text/event-stream; charset=utf-8"; add_header Cache-Control "no-store"; add_header Access-Control-Allow-Origin "*"; chunked_transfer_encoding off; push_stream_eventsource_support on; add_header X-Accel-Buffering "no"; # chunked_transfer_encoding off; # keepalive_timeout 100s; return 200 "event: ping\ndata: {\"time\": \"foo\"}\n\n"; } } upstream proxy { server 127.0.0.1:8081; } }
JavaScript客户端代码
document.addEventListener('DOMContentLoaded', () => { const source = new EventSource('http://localhost:8080/sse'); source.addEventListener( 'open', function (e) { console.log('Connection was opened.', e.readyState); }, false ); source.addEventListener( 'error', function (e) { console.log(e.readyState, e); if (e.readyState == EventSource.CLOSED) { // Connection was closed. } }, false ); source.addEventListener('ping', function (event) { console.log('ping', event); }); });
现象
控制台先输出Connection was opened.,接着能成功收到ping消息,但3秒后这个过程会重复,连接是从Nginx端主动关闭的。用小型express.js服务器测试SSE是正常工作的。
问题原因与解决办法
核心原因
Nginx的return指令会一次性发送完响应后立刻关闭连接,而SSE依赖长连接来持续传输事件。EventSource检测到连接关闭后,会触发默认的自动重连机制,导致你看到的“轮询”现象。
解决步骤
1. 移除return,改用持续输出的方式
要维持SSE长连接,不能用return返回一次性响应。如果你的需求是发送一次提交哈希后,用心跳维持连接,可以借助Nginx的echo模块(需提前安装/加载)来实现:
修改8081端口的/sse location配置:
location = /sse { default_type text/event-stream; add_header Cache-Control "no-store"; add_header Access-Control-Allow-Origin "*"; add_header X-Accel-Buffering "no"; proxy_buffering off; chunked_transfer_encoding on; keepalive_timeout 65; # 发送初始的提交哈希事件 echo "event: commit\ndata: {\"hash\": \"YOUR_COMMIT_HASH_HERE\"}\n\n"; echo_flush; # 每隔30秒发送心跳,维持长连接 echo_while true; echo_delay 30; echo "event: heartbeat\ndata: {\"time\": \"$(date +%Y-%m-%dT%H:%M:%S)\"}\n\n"; echo_flush; echo_end; }
这里用echo_while true循环发送心跳,确保连接不会被Nginx主动关闭。
2. 优化proxy_pass配置(如果需要代理)
如果必须使用proxy_pass,调整前端Nginx的代理配置,确保长连接被正确维持:
location = /sse { proxy_set_header Host $host; proxy_http_version 1.1; proxy_set_header Connection "keep-alive"; # 保持长连接,不要设为空 proxy_pass http://proxy; proxy_buffering off; proxy_request_buffering off; proxy_cache off; proxy_read_timeout 86400; # 延长读取超时,避免Nginx主动断开 proxy_send_timeout 86400; add_header X-Accel-Buffering "no"; # 禁止缓冲 }
同时确保后端(8081端口)的配置已经改成上述持续输出的模式。
3. 关键配置要点
- 必须设置
X-Accel-Buffering: no,禁止Nginx缓冲响应,确保事件实时推送。 - 禁用
proxy_buffering和proxy_request_buffering,防止缓冲导致连接提前关闭。 - 设置足够长的
proxy_read_timeout和keepalive_timeout,避免Nginx因超时主动断开长连接。 - 避免使用
return、rewrite这类一次性返回响应的指令,必须让服务器持续输出内容来维持连接。
内容的提问来源于stack exchange,提问作者lmerino

