ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Nginx WebSocket长连接与数据容量配置实战:从握手到心跳一次讲透

Nginx WebSocket长连接与数据容量配置实战:从握手到心跳一次讲透 Nginx搭配WebSocket几乎是每个做实时业务的团队绕不开的组合。很多项目前期用开发服务器直连WebSocket一切正常一旦上Nginx就出各种幺蛾子连接建不上、连上就断、数据发不过来、发一个大包直接被吞。这些问题十有八九不是代码的锅而是Nginx默认配置压根没为长连接和大数据量做适配。这篇文章把我这几年在生产环境里调Nginx WebSocket长连接和数据容量配置的完整经验整理出来从Upgrade头传递、超时保活、Buffer容量到心跳联动和负载均衡一次说清楚。1. 为什么Nginx默认配置跑不了WebSocketNginx从1.3.13版本开始就支持WebSocket反向代理但默认配置的很多参数是给普通HTTP短连接设计的。HTTP请求一来一回就断连接生命周期短Nginx的timeout、buffer、连接数这些默认值在短连接场景下没什么感知。可WebSocket是长连接一条连接可能要挂几小时甚至几天问题就全部暴露出来了。1.1 WebSocket和普通HTTP请求的本质差异普通HTTP请求是典型的请求-响应模型客户端发一个请求Nginx转发给后端后端返回响应连接随即关闭。哪怕开了keep-alive也只是复用同一条TCP连接去做多次短请求每次请求依然有明确的边界。WebSocket则完全不同。客户端先通过HTTP Upgrade机制发起握手一旦后端返回101 Switching Protocols这条连接就升级为全双工长连接。升级成功之后Nginx在这条连接上的角色从请求转发变成了透明隧道——它不再关心请求和响应的边界只是把客户端和后端之间的字节流互相搬运。这个模式下的关键区别在于HTTP短连接下长时间没数据是异常WebSocket长连接下长时间静默是常态。用户挂在页面上不操作不代表连接应该被断开。如果Nginx还用处理短连接的逻辑来管这条长连接必然出问题。1.2 请求升级到数据透传的完整路径要配置好先得知道一条WebSocket请求在Nginx里到底怎么走的。简化流程如下客户端发起HTTP GET请求携带Upgrade: websocket和Connection: Upgrade头Nginx根据location匹配规则转发给后端后端确认支持WebSocket后返回101 Switching ProtocolsNginx把这个101响应回给客户端连接升级成功进入双向数据转发模式。这个流程里任何一个环节出问题都会导致握手失败或连接异常。最常见的三个坑Nginx默认不转发Upgrade和Connection这两个头后端根本不知道这是WebSocket握手请求Nginx默认用HTTP/1.0向后端发起请求而HTTP/1.0不支持Upgrade机制后端返回101后Nginx如果按普通HTTP响应处理可能会因为超时或缓冲问题掐断连接把这个链路刻在脑子里后面调参的时候才不会乱。我见过不少人配置WebSocket时东改一个参数西改一个参数但根本不知道每个参数管的是哪个环节出了问题只能瞎试。2. 长连接配置连接建得上和不断开长连接配置是整套WebSocket反向代理的地基。地基不打牢后面调什么都白搭。这一节把几个核心参数一个一个拆开讲明白。2.1 Upgrade与Connection响应头的显式传递这是新手遇到的第一道坎。Nginx转发请求时默认会带上大部分原始请求头但Upgrade和Connection这两个头属于逐跳hop-by-hop头理论上不该被代理转发。Nginx遵循HTTP规范默认不会把客户端的Connection头原样传给后端。所以location里必须显式加上proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade;第一行把客户端的Upgrade头值websocket取出来传给后端第二行强制告诉后端这条连接需要升级。少了任何一个后端收到的就是普通HTTP请求WebSocket握手必然失败。前端表现就是WebSocket一直停在CONNECTING状态几秒后报错浏览器控制台提示WebSocket connection failed。实际排查的时候我发现一个很容易被忽略的细节Connection头的值必须是小写的upgrade。HTTP头名称不区分大小写但部分后端的WebSocket实现会对值做大小写敏感判断写成Upgrade或者UPGRADE都会导致握手失败。这个坑我踩过一次报错日志里完全看不出来最后是抓包才发现值的大小写不对。2.2 超时参数proxy_read_timeout与proxy_send_timeout连接建立起来之后最闹心的问题就是莫名掉线。Nginx默认的proxy_read_timeout是60秒含义是Nginx从后端读取数据时如果60秒内没读到任何字节就主动断开这条连接。这个默认值在HTTP短连接下完全没问题——正常请求响应都在秒级完成。但WebSocket长连接下用户挂在页面上不操作一分钟内没有数据流动是再正常不过的事。结果就是用户挂机超过60秒连接被Nginx静默掐断客户端那边还没反应过来下次服务端推送数据时才发现连不上了。生产环境我一般这样设置proxy_read_timeout 3600s; proxy_send_timeout 3600s;1小时是起步值有些业务场景比如在线文档协同、IoT设备长连接上报我会直接设成86400s也就是24小时。这里有个权衡超时时间设太长如果后端进程崩溃导致连接假死Nginx不会主动回收会一直占用连接资源设太短用户频繁掉线。我的经验是用心跳周期乘以3到5倍作为超时值既留足网络抖动的余量又不会让僵尸连接挂太久。心跳这块后面专门讲这两个配置是联动的。2.3 HTTP版本与后端子连接保活proxy_http_version必须设为1.1原因前面提到过HTTP/1.0不支持Upgrade机制。不设置这一项Nginx默认以HTTP/1.0向后端发请求即使Upgrade和Connection头都传了后端WebSocket服务也可能直接忽略。proxy_http_version 1.1;除了HTTP版本还有个容易被忽视的参数叫proxy_socket_keepalive。这个参数控制Nginx与后端TCP连接层面的keepalive探测proxy_socket_keepalive on;开启后Nginx会定期向后端发送TCP keepalive探测包用来发现半开连接比如后端主机断电、网络设备静默丢包。TCP keepalive默认探测间隔是2小时虽然长了点但作为兜底机制很有价值。WebSocket应用层的心跳负责业务保活TCP层的keepalive负责物理链路探测两者不冲突建议都打开。另外还有一个keepalive指令配置的是Nginx与后端空闲连接的复用数量通常是配合upstream用的upstream backend_ws { server 10.0.0.11:8080; keepalive 32; }这个参数在WebSocket场景下意义不大因为WebSocket长连接本身就长期占用谈不上复用。但在同一个upstream里如果还跑了普通HTTP接口keepalive 32能减少大量短连接频繁建立TCP握手的开销顺手写上没坏处。2.4 可复用的长连接配置模板把上面的参数整合起来一个能直接抄作业的长连接location配置如下location /ws/ { proxy_pass http://backend_ws; 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_connect_timeout 10s; proxy_read_timeout 3600s; proxy_send_timeout 3600s; proxy_socket_keepalive on; proxy_buffering off; }proxy_connect_timeout是Nginx与后端建立TCP连接的超时设成10秒足够。proxy_buffering off建议在WebSocket场景下一律关闭原因下一节细说这里先记住结论。Host、X-Real-IP、X-Forwarded-For这几个头是通用配置转发真实客户端IP和域名信息后端业务做日志审计、限流、地区判断都依赖它们顺手都配上。3. 数据容量配置别让大帧数据在Nginx层被截断长连接稳定了下一个高频问题就是数据容量。WebSocket的数据帧可以承载各种业务数据——图片、日志、批量消息单帧几MB甚至几十MB都不稀奇。Nginx默认的容量参数是给普通HTTP设计的遇到大帧数据会出现各种诡异问题。3.1 client_max_body_size与请求头大小限制client_max_body_size很多人以为只管HTTP上传实际上它在WebSocket握手阶段也有影响。握手请求虽然本身不含大体积body但如果业务把一些参数放在请求头或者URL参数里传头的大小就会受另一个参数约束。Nginx默认的large_client_header_buffers是4个8KB如果请求头总大小超过这个限制Nginx会直接返回400错误。有些业务会在WebSocket握手时通过Cookie或者其他自定义头携带大量上下文信息比如用户权限数据、埋点参数头很容易撑爆默认值。建议配置client_max_body_size 50m; large_client_header_buffers 4 16k;client_max_body_size 50m的意义在于如果业务需要WebSocket握手时在body里带点数据或者同一个server块里还有文件上传接口这个值能避免上传大文件时被Nginx拒绝。注意如果WebSocket升级成功后传输的数据走的是数据帧而不是HTTP body这个参数就不再起作用了——真正管数据帧的是buffer系列参数。3.2 proxy_buffer_size与proxy_buffers原理拆解Nginx收到后端响应后默认先把数据写进缓冲区。缓冲区不够了要么写临时文件要么直接转发到客户端。这个机制在HTTP短连接下是性能优化在WebSocket下却可能成为瓶颈。先解释三个参数各自管什么proxy_buffer_size单个缓冲区的初始大小用于读取后端响应的第一部分通常包含响应头proxy_buffers指定缓冲区的数量和单个大小用于缓冲后端响应的主体数据proxy_busy_buffers_sizeNginx向客户端发送数据时可以同时使用的忙碌缓冲区上限默认的proxy_buffer_size通常只有4k或8kproxy_buffers通常是8个4k或8k。这在普通HTTP下够用因为响应头一般就几百字节body通过文件或流式转发处理。但WebSocket的数据帧是实时流式的Nginx分不清哪里是头哪里是body它就是无脑往缓冲区里塞。缓冲区太小会导致什么我用压测环境实际测过推送一个2MB的JSON数据帧默认buffer4k/8k连接直接异常客户端收到的数据不完整部分场景触发断连改成16k单个8个buffer数据完整到达但推送耗时比直连增加约15%加大buffer并合理配置数据完整耗时接近直连原因是buffer太小的时候Nginx的转发逻辑会频繁触发缓冲区已满→写临时文件→读回文件→转发的路径不仅性能差数据边界也可能在拆分转发中丢失。3.3 大小帧业务下的缓冲策略选择不同业务对缓冲的需求是相反的这块一定要根据实际场景来选不能一套配置打天下。高频小包业务聊天消息、行情价格、实时位置更新数据帧通常只有几百字节到几KB。这类业务追求的是低延迟建议关掉缓冲proxy_buffering off;关闭后Nginx收到数据就立刻转发给客户端不攒不囤延迟最低。这也是我在长连接配置模板里直接写proxy_buffering off的原因——大多数实时业务都是高频小包。低频大包业务批量数据同步、大图传输、日志批量上报数据帧动辄几MB。这类业务追求的是完整性关掉缓冲反而可能导致Nginx转发时拆包异常。建议开缓冲并加大容量proxy_buffering on; proxy_buffer_size 16k; proxy_buffers 16 32k; proxy_busy_buffers_size 64k;16个32KB的缓冲区总共512KB。这个容量能扛住大多数业务场景的大帧推送。如果单帧超过10MB再把proxy_buffers的数量往上加但每增加一倍容量每个连接就多占512KB内存并发1000个连接就是500MB内存规划要心里有数。3.4 数据容量参数完整示例一条同时兼顾握手和数据传输的容量配置长这样location /ws/ { proxy_pass http://backend_ws; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; client_max_body_size 50m; large_client_header_buffers 4 16k; proxy_buffer_size 16k; proxy_buffers 8 16k; proxy_busy_buffers_size 32k; proxy_temp_file_write_size 64k; proxy_read_timeout 3600s; proxy_send_timeout 3600s; proxy_buffering on; }proxy_temp_file_write_size是缓冲区满后写入临时文件时的一次性写入大小设成64k可以减少磁盘IO次数。这套配置适用大多数可能有较大数据帧但不确定多大的业务。如果确定是高频小包把proxy_buffering改成off其它保持不变。4. 心跳机制让长连接真正长久存活前面反复提到心跳和超时是联动的这一节专门把心跳机制讲透。很多人以为WebSocket长连接天然长其实不然——WebSocket协议本身没有内置心跳连接能活多久完全取决于两端和中间设备的耐心。4.1 没有心跳的连接会怎样TCP的keepalive默认是关闭的即便开启默认探测间隔是2小时对应用层感知连接状态来说太迟钝。没有心跳的话下面几种场景都会产生僵尸连接用户Wi-Fi断开又重连IP变了旧连接在网络设备上被清掉但两端都不知道后端服务发布重启进程没了客户端还傻傻等着推送中间有NAT设备、负载均衡器长期空闲的连接被它们强制回收这些场景下没有心跳的WebSocket连接就像断了线的风筝表面上还挂着实际上收发数据全失败。用户侧的表现是页面突然收不到消息但没有任何报错因为浏览器认为连接还活着。4.2 心跳周期与Nginx超时时间的联动设计心跳的本质是周期性发送小数据包让链路保持活跃同时让两端感知到对方的存在。WebSocket协议里对应的就是ping/pong帧。注意Nginx对WebSocket帧的处理是透传的——心跳ping帧经过NginxNginx能看到有数据流动proxy_read_timeout就不会触发。这里有个非常实用的配置联动规则心跳间隔建议设在30到60秒之间。Nginx的proxy_read_timeout设成心跳间隔的3倍以上。比如心跳30秒一次read_timeout设90秒以上心跳60秒一次read_timeout设180秒以上。留出3倍余量是为了容忍偶发的网络抖动——某次心跳丢了还有下一次心跳能续命。后端业务如果有真实的业务消息在推送心跳间隔可以适当放宽如果业务本身是长时间静默的比如IoT设备状态上报几分钟才一条心跳就必须勤快一些。4.3 真实故障案例被误删的心跳逻辑分享一个真实的线上事故。某个在线协作项目某次前端重构时把心跳逻辑当无用代码删掉了上线后第一周一切正常因为用户活跃度高连接上总有数据流动。到了第二周开始有用户反馈挂机半小时再回来页面就收不到实时更新了必须刷新。排查过程先看Nginx error.log没有报错看access.log发现这些用户连接的请求在某个时间点之后就没有任何记录了——连接被掐了查看当时的proxy_read_timeout配置正好是600秒推断链路是用户挂机→心跳缺失→10分钟无数据→Nginx触发read_timeout断连客户端没有自动重连逻辑断了就断了必须手动刷新。加上心跳和自动重连后这个问题彻底消失。我个人的建议是无论前端还是后端WebSocket应用必须配套心跳自动重连双机制。心跳负责保活重连负责恢复。只做心跳不做重连遇到网络闪断还是会卡死只做重连不做心跳连接资源被僵尸连接白白占用。5. 多节点部署与wss加密场景的进阶配置单节点WebSocket配好之后接下来就是上规模的问题。生产环境不可能只有一个后端节点一旦涉及多节点负载均衡和连接黏着这两个问题必须面对。5.1 upstream负载均衡与连接黏着问题upstream的基础配置很简单upstream backend_ws { server 10.0.0.11:8080; server 10.0.0.12:8080; server 10.0.0.13:8080; }但WebSocket和普通HTTP在负载均衡上有个本质区别。HTTP请求是无状态的每个请求可以随机打到任意节点响应结果一致。WebSocket连接一旦建立同一条连接的所有数据帧必须打到同一个后端节点——如果客户端A的会话数据在节点11下一条数据帧被轮询算法转发到节点12业务状态就对不上了。默认的轮询策略在WebSocket场景下是绝对不能用的。同一连接的不同数据帧被分散到不同节点轻则业务数据错乱重则后端直接抛异常断连。5.2 ip_hash策略的使用与注意点WebSocket最常用的负载均衡策略是ip_hashupstream backend_ws { ip_hash; server 10.0.0.11:8080; server 10.0.0.12:8080; server 10.0.0.13:8080; }ip_hash根据客户端IP计算hash值把同一个IP的所有请求固定分配到同一个后端节点。这样WebSocket连接建立后同一连接的数据帧都会打到同一个节点业务状态不会乱。但ip_hash有几个注意点第一节点数量变化会导致hash结果变化。新增一个节点部分老IP的hash结果会变新连接可能被打到新的节点上。连接已经建立的不受影响TCP连接是固定的但新连接会重新分布这可能让某个节点瞬时承接大量新连接。第二某个节点宕机Nginx会把本来hash到该节点的请求转发给其他节点这个没问题但该节点上存活的WebSocket连接会全部中断。客户端必须有重连机制才能恢复。第三如果是大规模客户端比如上万用户且都从同一个出口IP访问公司办公网、NAT网关ip_hash会把这些用户全部打在同一个节点上导致严重的负载倾斜。这种情况下更适合在上层再做一次分组或者用sticky模块基于Cookie做黏着。5.3 wss://场景下的SSL配置要点HTTPS站点下WebSocket地址必须用wss://。这意味着Nginx需要在SSL层终止加密连接再把解密后的数据转发给后端。SSL配置本身不复杂server { listen 443 ssl; server_name example.com; ssl_certificate /etc/nginx/ssl/example.com.pem; ssl_certificate_key /etc/nginx/ssl/example.com.key; location /ws/ { proxy_pass http://backend_ws; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_read_timeout 3600s; proxy_send_timeout 3600s; proxy_buffering off; } }这里有个关键点Nginx与后端之间走的是http://而不是https://因为SSL在Nginx这一层已经终结了内部链路用明文HTTP更高效。如果Nginx和后端不在同一内网或者内网链路本身不可信才需要再走一层TLS也就是proxy_pass https://backend_ws但绝大多数场景不需要。wss下容易踩的坑是证书链不完整。有些证书提供商给了多个证书文件必须按服务器证书中间证书的顺序合并成一个pem文件合并顺序错了会导致部分客户端尤其iOS设备握手失败。排查方法是openssl s_client -connect example.com:443 -servername example.com -tlsextdebug看到verify return:1说明证书链正常其他任何提示都要检查证书配置。另外一个容易被忽略的参数是ssl_session_cache建议开启会话缓存减少TLS握手开销ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m;10m共享缓存大约能存5万个会话凭据对大多数站点足够。这能显著减少客户端频繁重连时的TLS握手延迟。6. 网上实战踩坑记录与排查速查最后整理一下我在实际运维中遇到的典型问题每条都对应一个真实的排障过程。如果你照着前几节的配置做了还是有问题来这里对照排查。6.1 浏览器一直CONNECTING排查思路症状前端WebSocket停在CONNECTING状态几秒后报错。排查顺序确认location里Upgrade、Connection、proxy_http_version 1.1三件套有没有配齐绕过Nginx直连后端WebSocket服务确认后端本身没问题查看Nginx error.log搜索upgrade关键字确认proxy_pass的URL路径和location匹配是否正确——location /ws/配上proxy_pass http://backend_ws/和proxy_pass http://backend_ws是有区别的前者会带上/ws/前缀后者不带后端路由不匹配就会握手失败最隐蔽的坑是proxy_pass末尾的斜杠。location /ws/proxy_pass http://backend_ws;无斜杠会把完整的/ws/xxx路径传给后端proxy_pass http://backend_ws/;有斜杠会把/ws/前缀替换成/。后端WebSocket路由通常是固定路径比如/ws/chat配错了直接404前端表现就是连不上。6.2 连接几分钟就断开的常见原因症状WebSocket能建立但过一会儿就断非常有规律。几乎都是proxy_read_timeout太短。默认60秒用户挂机超过60秒连接必断。把timeout调大之后还要确认心跳有没有正常工作。如果后端有心跳但Nginx还是断连排查心跳数据是不是走了代理路径——有些业务把心跳请求单独发到另一个域名或端口绕过了Nginx导致Nginx看不到数据流动照样掐连接。另一个少见但真实的原因是TCP keepalive被网络设备拦截。云环境里的负载均衡器或者安全组可能会丢弃长时间空闲的TCP连接即使Nginx的timeout设得再大也没用。这种情况只能靠应用层心跳兜底缩短心跳间隔。6.3 小数据正常大数据丢失的buffer问题症状普通消息收发正常一旦推送大数据帧比如超过1MB客户端收到的数据不完整或者连接直接断掉。优先检查proxy_buffer_size和proxy_buffers。我遇到过一次推送3MB数据帧Nginx只转发了一部分就断连查了半天发现proxy_buffer_size还是默认的4k数据还没转发完连接就异常了。缓冲区调大之后如果还出问题检查是否触发了临时文件路径。proxy_max_temp_file_size默认1MB响应超过这个值会落盘写临时文件如果临时文件目录权限不对或者磁盘满了数据就会丢失。把这几个值统一调大问题基本都能解决。6.4 常用排查命令与验证方法实战中最常用的排查手段浏览器开发者工具Network面板直接查看WebSocket帧的收发记录确认数据是否到达客户端。# 看Nginx访问日志是否有101响应 tail -f /var/log/nginx/access.log | grep 101 # 看Nginx错误日志 tail -f /var/log/nginx/error.log # 查看当前WebSocket长连接数量 ss -tnp | grep :8080 | grep ESTAB | wc -l # 查看nginx worker进程的连接数 ss -tnp | grep nginx | wc -l手动验证Nginx是否成功代理WebSocket可以用curl带Upgrade头做一次握手测试curl -i -N \ -H Connection: Upgrade \ -H Upgrade: websocket \ -H Sec-WebSocket-Version: 13 \ -H Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw \ http://your-server/ws/如果返回HTTP/1.1 101 Switching Protocols说明Nginx这一层的握手转发没问题。后续的帧数据用curl看不出内容但至少能确认代理链路是通的。最后分享一个调参心得。Nginx的WebSocket配置没有一套万能模板必须根据业务特征来定实时消息追求低延迟就关缓冲批量传输追求完整性就开缓冲加大容量心跳间隔决定超时时间的下限节点数决定连接策略的选择。我踩过最多的坑就是把HTTP短连接的思维带到WebSocket场景里所有参数都按默认值走。把上面这套配置吃透再结合自己的业务压一遍WebSocket在Nginx后面基本不会再有脾气。
返回列表