我最早接触这个需求,是在接手一个内部协同工具的时候。前端用 WebSocket 做实时消息推送,本地开发一切正常,代码一部署到 Nginx 后面就频繁掉线,浏览器控制台隔几分钟就刷一条WebSocket onclose code: 1006,用户那边直接说“页面老断,消息收不全”。当时的第一反应是查后端日志,结果后端进程根本没退出,消息也正常在发。后来才意识到问题出在接入层,也就是 Nginx 对 WebSocket 长连接和数据容量的默认处理方式,和我原本以为的“HTTP 反向代理”完全是两码事。
这个标题其实覆盖了三个层面:Nginx 怎么识别并转发 WebSocket 升级请求、长连接如何保活不出幺蛾子、以及消息体积变大之后 Nginx 的缓冲区和头部限制会不会把连接掐断。今天就围绕这三个方向,把我实际踩过的坑、改过的配置、查问题的思路完整写一遍,给正在被 WebSocket 掉线、握手失败、消息发不出去折腾的同学一个可直接抄的作业。
1. WebSocket 代理为什么不能直接照抄默认配置
很多人第一次在 Nginx 里配 WebSocket 的时候,想的都是“这不就是一个 HTTP 反向代理嘛,location 指向后端不就行了”。结果浏览器一打开,控制台立刻报握手失败,或者能连上但几秒就断。要搞清楚为什么,得先从 WebSocket 的握手机制说起。
1.1 HTTP Upgrade 机制与 Nginx 默认行为的差异
WebSocket 并不是一个独立于 HTTP 的协议,它的连接建立过程,本质上是客户端先发一个普通的 HTTP GET 请求,在请求头里带上Connection: Upgrade和Upgrade: websocket,服务端如果同意切换协议,就返回 101 Switching Protocols,之后这条 TCP 连接就不再走 HTTP 语义,而是变成一条双向的长连接数据通道。
关键就在这里:HTTP 协议里的Connection头是“逐跳”的(hop-by-hop),意思是这个头只对当前这一跳有效,Nginx 作为反向代理处在客户端和后端之间,它默认会把收到的 HTTP 请求按普通请求转发给后端,并不会原样把Upgrade相关头传过去。结果就是,后端收到的是一个普普通通的 GET 请求,根本不知道客户端想升级成 WebSocket,于是按普通 HTTP 返回响应,前端的握手自然就失败了。
所以做 Nginx 代理 WebSocket,第一件事就是让 Nginx 在转发请求的时候,把升级相关的能力显式告知后端。实际配置里最常见的就是这一段:
map $http_upgrade $connection_upgrade { default upgrade; '' close; } server { listen 80; server_name ws.example.com; location /ws/ { proxy_pass http://backend_server; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; } }这里面的学问挺多的。map指令先根据请求头里的$http_upgrade变量做映射:如果客户端带了Upgrade: websocket,那$connection_upgrade就置为upgrade,Nginx 转发的时候把Connection: upgrade也带上,后端就能正常识别并完成 101 切换。如果请求是普通 HTTP,不带 Upgrade 头,$connection_upgrade就会映射成close,这时转发给后端的请求头就是Connection: close,走的是普通 HTTP 语义,不会影响站内其他接口。
另外proxy_http_version 1.1这行也值得单独说。Nginx 默认向上游发请求用的是 HTTP/1.0,而 HTTP/1.0 的语义里没有 Upgrade 机制,连接默认也是短连接,即使你把Connection: upgrade的头发过去了,后端基于 HTTP/1.0 去解析也可能出兼容问题。所以升级成 1.1 是必须的,不是因为“大家都这么写所以我也写”,而是协议层面就要求 1.1 才能正确完成升级流程。
1.2 典型失败现象:从 400 到 1006 的困惑
配置不对的时候,现象通常很直观。最常见的三种:
- 浏览器控制台报
WebSocket connection to 'ws://...' failed,点开发送的消息如果带了Sec-WebSocket-Key,而 Nginx 没有转发完整请求头,后端可能直接返回 400 Bad Request。原因往往是请求头被 Nginx 裁剪了,或者后端要求的 Host、Origin 头对不上。 - 连上了但几十秒后自动关闭,前端拿到的状态码是 1006。1006 这个状态码很特殊,它不是对端主动发出的关闭帧,而是连接“异常消失”,浏览器自己判定连接已关闭。触发原因三分之二是 Nginx 的代理超时把连接掐了,这个问题在第 3 节细说。
- 部署在多个 Nginx 节点后面,A 请求打到了节点 1,B 请求打到了节点 2,WebSocket 状态不一致,表现就是消息时灵时不灵。
我在实际排障过程中养成一个习惯:新接一个 WebSocket 代理需求,第一件事不是改配置,而是先看 Nginx 的 access log 里这个请求的响应码。如果全是101,说明升级这一步没问题,掉线就得往超时和 buffer 方向查。如果看到400、502、404,升级流程大概率有配置漏洞。
2. 最小可用配置:三行头信息把连接“接”到后端
我见过很多网上给的最小配置,就三行:proxy_http_version 1.1;、proxy_set_header Upgrade $http_upgrade;、proxy_set_header Connection $connection_upgrade;。这三行确实是核心,但实际落地的时候还有不少细节需要补齐,否则开发环境能用,一到生产环境就露馅。
2.1 一个可以直接复制的完整基础配置
拿我们线上一个消息推送服务举例,后端是 Go 写的,监听在 127.0.0.1:8080,路径前缀是/ws。Nginx 侧的完整配置大概是这样的:
map $http_upgrade $connection_upgrade { default upgrade; '' close; } upstream ws_backend { server 127.0.0.1:8080; keepalive 32; } server { listen 80; server_name ws.example.com; # 其他普通接口 location /api/ { proxy_pass http://ws_backend; proxy_set_header Host $host; } # WebSocket 专用 location /ws/ { proxy_pass http://ws_backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $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; } }upstream块里加keepalive 32;很多人会忽略。这个参数的意思是 Nginx 到后端保持 32 个空闲的长连接不关闭。WebSocket 场景下,如果不配 keepalive,每次握手之后虽然连接是长连接,但代理层面的连接池没有复用能力,短连接风暴一上来,后端的文件描述符和线程开销都会暴涨。我见过一个后端服务被 Nginx 频繁重建连接压垮的案例,加了 keepalive 之后负载直接降了 40%。
2.2 为什么要设置这些 Header
逐个拆开看:
proxy_set_header Upgrade $http_upgrade和proxy_set_header Connection $connection_upgrade前面已经解释过了,负责把升级请求完整搬到后端。有一点要特别注意:Connection这个头在不同语义下值不同,不能直接写死成upgrade。如果你在同一个 server 块里既有普通 API 又有 WebSocket,写死Connection: upgrade会导致普通请求也带着 upgrade 头到后端,容易被后端的 Web 框架误解,甚至触发安全设备的告警。用map做映射,就是让 Nginx 自动区分升级请求和普通请求。
proxy_set_header Host $host这行也很容易踩坑。默认情况下 Nginx 会用proxy_pass里的 host 作为转发请求的 Host,也就是127.0.0.1:8080。如果后端的 WebSocket 服务要校验 Host,或者你的应用是同机多域名部署,Host 不对直接握手失败。设置成$host就能保留客户端访问的原始域名。
X-Real-IP和X-Forwarded-For在 WebSocket 场景下同样值得带上,特别是做日志审计和封禁策略的后端。WebSocket 长连接建立之后,后端的客户端 IP 如果全是 Nginx 的内网地址,排查用户来源会很痛苦。
2.3 验证握手是否成功
配置改完,不要着急用前端联调,我习惯先手动发一个升级请求,确认链路通不通。curl 就能模拟 WebSocket 握手:
curl -i -N \ -H "Connection: Upgrade" \ -H "Upgrade: websocket" \ -H "Sec-WebSocket-Version: 13" \ -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \ http://ws.example.com/ws如果返回内容里包含HTTP/1.1 101 Switching Protocols,说明 Nginx 到后端的升级链路已经打通。如果返回 200、400、404,就要回到上一节逐个排查头信息。
也可以用nc直接看 TCP 层是否有数据:
nc -vz ws.example.com 80但这个只能验证端口通不通,没法确认升级协议是否正确,所以日常排障我还是更依赖 curl 的输出。
3. 长连接保活:从默认 60 秒到无人值守
配置完握手,连接能建起来了,但紧接着就会遇到第二个高频问题:连接动不动就断。尤其在消息推送类服务里,用户可能几分钟都没有交互,前端和后端之间也没有心跳消息,然后某一天你会发现,所有用户都掉线了,页面必须刷新才能恢复。
3.1 proxy_read_timeout 与心跳的设计配合
Nginx 里有两个参数决定了代理连接的空闲存活时间:
proxy_read_timeout默认 60 秒,指的是 Nginx 从后端读取数据的超时时间。对于 WebSocket 来说,如果在这段时间内 Nginx 没有从后端读到任何字节,它就会主动关闭这条连接。proxy_send_timeout默认 60 秒,指的是 Nginx 向后端发送数据的超时时间,如果客户端太久没向 Nginx 发起数据,Nginx 也会掐掉连接。
这就是很多线上服务“每过 60 秒掉一次线”的根源。Nginx 的 60 秒默认值是给普通 HTTP 请求设计的,普通请求从发起到响应结束通常也就几秒,60 秒已经足够宽裕。但 WebSocket 是长连接,空闲时间完全取决于业务,一分钟没有任何消息是常态。所以代理 WebSocket 场景下,这两个时间必须调大。
我的习惯是统一设置成3600s,也就是一小时。这个数值不是拍脑袋定的,而是要看心跳机制:
注意:
proxy_read_timeout必须大于后端和客户端约定的心跳间隔,否则心跳还没到,Nginx 已经先把连接关了。比如后端每 30 秒发一次 ping,那 timeout 设为 60 秒都偏紧,建议至少是心跳间隔的 3 倍以上。
实际生产里,如果后端设置了心跳但没超过 60 秒,Nginx 的默认配置也能撑住,因为心跳本身也是数据,Nginx 读到了数据就会重置计时器。怕的是那些“伪长连接”——应用层心跳写得有问题,或者压根没做心跳,那 Nginx 60 秒的默认值就会准时掐断所有空闲连接。
3.2 TCP 层 KeepAlive 与 HTTP 层心跳不是一回事
还有一个常见的认知误区,就是把 TCP KeepAlive 和 WebSocket 心跳混为一谈。TCP 层面的 KeepAlive 是由操作系统内核维护的,默认在连接空闲 2 小时后才启动探测,Linux 下可以通过sysctl net.ipv4.tcp_keepalive_time查看,通常在 7200 秒左右。这个机制是用来探测物理链路是否中断的,跟应用层“消息是否活跃”完全无关。
WebSocket 协议里定义的 Ping/Pong 帧是应用层心跳,这个才是服务端和客户端之间真正产生数据流量的机制。所以做 Nginx 代理的时候,你不能幻想“操作系统会帮我保活”,TCP 层那两个小时的探测周期,对 WebSocket 用户体感来说太慢了。真正要做的是让后端每 30 到 60 秒发一次心跳帧,然后 Nginx 的proxy_read_timeout设成心跳间隔的三倍以上,这样双保险。
如果你引用了第三方 WebSocket 客户端库,也建议确认一下心跳逻辑。像 Go 的gorilla/websocket官方示例就包含ping和pong处理;Java 的spring-websocket自带setMaxSessionIdleTimeout;前端的vueuse包里也有useWebSocket的心跳参数。这些库默认都不一定开启心跳,需要自己显式配置。
3.3 浏览器与前端断连的常见原因
前端报1006的时候,很多时候是突然的、偶发的,后端日志里根本看不到关闭记录,因为连接被 Nginx 这一层直接断掉了,后端只会看到对端消失。我在排查这类问题时基本按下面这个顺序走:
- 看 Nginx access log 里这个连接的请求结束时间和请求开始时间相差多久,如果正好是 60 秒、300 秒这种整数,优先怀疑超时时间。
- 查看后端进程是否被杀、OOM、重启。后端一旦退出,Nginx 和后端之间那条 TCP 连接会收到 RST,客户端也会收到 1006。
- 看是否有多个 Nginx worker 或负载均衡节点在转发同一个连接。WebSocket 建立连接之后,后续帧必须走同一个连接,如果 L4/L7 负载均衡策略是轮询,帧被转发到不同节点,连接必然异常。
- 检查客户端和后端之间是否有防火墙、安全组把空闲连接回收了。云环境里的 SLB/NLB 默认闲置超时时间通常在 60 秒到 900 秒之间,长连接空闲超过阈值就会被主动断开,这个和 Nginx 无关,但体感一模一样。
我曾经排查过一个“每天固定 15 分钟掉一次线”的诡异问题,后端日志、Nginx 日志全都正常,最后发现是云负载均衡默认 900 秒的空闲超时,和 Nginx 的 3600 秒配置根本不在一个量级。所以你如果前面还有一层 LB,一定要记得把 LB 的 idle timeout 也调大,否则 Nginx 里配得再努力,也扛不住外层先动手。
4. 数据容量配置:消息体积、HTTP 头、Buffer 一次讲清
“数据容量配置”这个关键词,对应的实际场景很具体:要么是 WebSocket 握手阶段携带的令牌或认证信息过长,导致握手直接 400;要么是连接建立后,单条消息体很大,Nginx 默认缓冲策略把数据憋住,产生“消息一直发不出去”的现象。这两类问题我都处理过,下面分开讲。
4.1 大消息与响应缓冲:什么时候必须关掉 proxy_buffering
Nginx 默认开启代理响应缓冲,也就是后端返回的数据,Nginx 会先收进自己的缓冲区,攒够一定量再一次性发给客户端。对普通 HTTP 页面来说,这个机制性能很好,能减少后端到客户端之间的网络 IO 次数。但 WebSocket 一旦完成 101 升级,Nginx 和后端之间的数据就不再是普通 HTTP 响应的语义,而是双向的帧流。这时候如果proxy_buffering还开着,Nginx 会尝试把后端发来的 WebSocket 帧攒起来,带来的直接后果是:
- 消息延迟变高,小块数据迟迟不发到客户端,因为缓冲区没满
- 内存占用上升,尤其当单个 WebSocket 帧消息体很大时,Nginx 要为每条连接分配大量缓冲内存
- 极端情况下,大帧数据占据了缓冲区,后续帧排队等待,造成消息乱序感
所以 WebSocket 代理的标准配置里基本都会加一行:
proxy_buffering off;加了这行之后,Nginx 会把后端的数据以“收到即转发”的方式传给客户端,延迟最低,内存占用最可控,也更符合长连接双向通信的语义。
有些同学可能会问:那我保持默认开启行不行?行为上不是“完全不能用”,但延迟和资源开销很难受。我曾在某个语音实时通话项目里测过,开启 proxy_buffering 时,音频帧排队明显,对端人声延迟能感受到,关闭之后立刻恢复正常。所以除非有特殊需求,WebSocket 的 location 里务必加上这一行。
4.2 缓冲大小怎么调:从默认 4k 到常见需求
关闭proxy_buffering后,还有一个参数proxy_buffer_size值得关注。它的作用是设置 Nginx 接收后端响应头的缓冲区大小,默认值通常是4k或系统页面大小。普通 HTTP 响应头很小,4k 完全够用。但 WebSocket 场景下,后端在 101 切换响应里可能携带自定义头部,比如节点编号、鉴权信息、灰度标识,头如果超过 4k,Nginx 会返回upstream sent too big header while reading response header from upstream的报错,客户端表现为握手失败。
我建议在 WebSocket 的 location 里直接指定成:
proxy_buffer_size 8k;如果你的后端确实会在握手响应里塞大量自定义头,就继续加大到 16k、32k。但说实话,最佳实践不是把 buffer 无限调大,而是让后端别在握手包里塞太多没必要的元数据,认证信息放 URL 参数或首次消息里,比堆在响应头里更合理。
还有一个经常被忽略的参数是client_max_body_size,默认 1m。WebSocket 升级请求本身是 GET,没有请求体,所以这个参数对握手没影响。但如果你在同一个 server 块下既要代理 WebSocket,又要处理文件上传或大 POST,这个值就要记得改,否则前端会莫名其妙收到 413 Request Entity Too Large。
4.3 握手请求头过大:client_header_buffer_size 与 large_client_header_buffers
再讲一个我实际踩过的大坑。有一个前端项目在 WebSocket 握手时把整个 JWT、用户信息、设备信息都塞到了 Cookie 和自定义 Header 里,本地直连后端一点问题没有,经过 Nginx 后直接 400。查 Nginx 日志,能看到类似client sent too long header line的记录。
Nginx 解析请求头时,默认每个头部的缓冲区大小是client_header_buffer_size,默认值 1k;如果单个头超出这个值,Nginx 会尝试用large_client_header_buffers,默认是 4 个 8k。如果头部总大小超过 32k,或者单个头超过 8k,就会返回 400。
WebSocket 场景里,认证令牌放在自定义 Header 里很常见,比如Authorization: Bearer xxxx,这么长的字符串很容易就把 1k 顶穿。因此需要在配置里显式调大:
client_header_buffer_size 4k; large_client_header_buffers 4 16k;不过这里的建议是:client_header_buffer_size设置成 4k 或 8k,large_client_header_buffers保持 4 个 16k 基本就够用了。不要动不动就调成 64k、128k,因为每个请求都会预分配这些内存,调得越大,并发越高时浪费的内存越可观。合理的做法是后端把令牌长度控制在合理范围内,Nginx 这边给出两到三倍的余量即可。
5. 高频报错排查:1006、stream disconnected、App 打包连不上
看热搜词里出现了一大串具体的报错文案,包括[websocket] onclose, code: 1006、stream disconnected before completion: failed to send websocket request、以及“vue 使用 websocket 开发正常,打包为 app 连接不了”。这些问题我在不同项目里都遇过,逐个说一下排查路径。
5.1 code 1006 的本质与排查路径
1006在 WebSocket 协议里不是一个由对端通过关闭帧发送的状态码,它是浏览器检测到连接“非正常关闭”时自己产生的。也就是说,只要连接没有按照“先发 Close 帧再断开”的正常流程结束,浏览器就会报 1006。
常见的触发原因和排查策略整理成一个表:
| 可能原因 | 排查方法 | 处理方向 |
|---|---|---|
Nginxproxy_read_timeout过短 | 看 Nginx access log 连接持续时间是否接近超时值 | 调大proxy_read_timeout,并检查心跳 |
| 后端进程崩溃或被重启 | 看后端日志是否有 OOM、panic、重启时间点 | 修复后端稳定性,配置健康检查 |
| 外层负载均衡空闲超时 | 统计掉线时间点是否出现在连接空闲一段时间后 | 调大 LB 的 idle timeout,或调整心跳频率 |
| 多个实例没有会话保持 | 看连接是否被转发到不同后端节点 | 配置 ip_hash 或 sticky session |
| 客户端和服务器之间网络异常 | 从客户端所在网络抓包确认 RST、FIN 来源 | 排查安全组、防火墙、NAT 规则 |
我特别想强调一个习惯:做 WebSocket 排障,不要只看后端日志和浏览器端,一定要引入中间层日志。Nginx 的 access log 默认只能看到请求的开始和结束时间、状态码,但如果是长时间连接,最好把$upstream_response_time、$upstream_status配合log_format记下来。一条连接在 Nginx 层实际维持了多久,往往比后端日志更能说明问题。
5.2 stream disconnected before completion 是什么情况
这个报错我在压测环境下见过多次,经常出现在 JMeter 或自研压测工具里。完整文案一般是:
stream disconnected before completion: failed to send websocket request: io它描述的其实是握手的 TCP 层面问题:测试工具已经向服务器发起了 WebSocket 握手,但在请求还未完成时,连接就被对端关闭了。也就是说,这不是一个“连接建立后被断开”的错误,而是“连接建立过程中就断了”。
遇到过的情况有几种:
- 目标端口不是 WebSocket 服务端口,比如把请求打到了普通 HTTP 服务上,对端返回非 101 响应后直接关闭连接。
- Nginx 拒绝了 Upgrade 请求,返回 403/400/404,压测工具解析不到 101 就报 disconnected。
- 压测工具并发过高,Nginx worker 连接数被耗尽,新连接无法建立。
- 后端鉴权失败主动断开。
解决思路:先单独用 curl 验证目标地址能不能正确返回101 Switching Protocols,能通过再继续用压测工具。如果单条请求通了,并发一高就断,优先看worker_connections、ulimit -n、后端最大连接数这些资源型瓶颈。
5.3 浏览器正常但打包成 App 连不上
这个热搜词让我想起一个很典型的移动端适配问题:用 Vue 写的前端,在浏览器里 WebSocket 一切正常,但打包成安卓或 iOS App 之后,完全连不上。很多人第一反应是后端有问题,其实问题大多出在移动端环境和证书上。
- WebView 或 App 内的网络请求默认可能禁用了明文流量。安卓 9 及以上默认禁止明文流量,如果你的 WebSocket 地址是
ws://,只适用于开发调试;生产环境必须用wss://。真要本地测试,需要在 AndroidManifest 里配置android:usesCleartextTraffic="true",但这是开发期方案,上线不能这么搞。 - iOS 的 ATS 策略默认也限制了非 HTTPS 连接。开发环境如果非要走
ws://或http://,需要在 Info.plist 里临时配置NSAllowsArbitraryLoads,但同样不建议长期保留。 - 证书链不完整。WebSocket 走
wss://时,Nginx 需要配置有效的 SSL 证书,并且证书链要完整。很多站点只部署了域名证书,中间证书没配全,浏览器因为内置了根证书所以能容忍,App 内置的网络栈可能不一样,直接握手失败。 - Origin 头不同。浏览器里 WebSocket 的 Origin 是
http://localhost:8080或https://你的站点;打包成 App 后,Origin 可能是file://或null,如果后端有 Origin 白名单校验,就会拒绝连接。排查时先让后端把 Origin 限制放宽,再逐步收紧。
这类问题把抓包工具开起来最快。Android 上用 Charles 配好证书,iOS 上也能抓,看握手请求有没有发出去、Nginx 返回了什么状态码,基本一眼定位。
6. 进阶:负载均衡、WSS 与多节点部署
到这里基础配置和常见问题都覆盖了,再说几个偏进阶但真实生产里绕不开的点。尤其是服务规模上来之后,单机 Nginx 转发到单后端肯定不够,一旦引入多个节点,WebSocket 的“会话粘连”问题就会立刻冒出来。
6.1 多节点负载均衡必须解决会话绑定
普通 HTTP 请求是无状态的,负载均衡轮询到哪个节点都行。WebSocket 不一样,客户端和后端建立连接后,后续的所有帧都必须走同一条连接。如果你用默认的轮询策略,第二次请求可能就被转发到另一个节点,后端一看没有对应的连接,直接丢弃数据,客户端表现就是消息收不到、连接异常。
Nginx 做 WebSocket 负载均衡,要么用ip_hash,让同一个客户端 IP 固定打到同一个后端节点:
upstream ws_backend { ip_hash; server 127.0.0.1:8080; server 127.0.0.1:8081; keepalive 32; }要么用第三方模块做会话保持,比如nginx-sticky-module,按 Cookie 标记后端节点。ip_hash的局限在于如果用户手机网络切换,IP 变了就可能跳到别的节点,但大多数场景下它最简单可靠。如果你的 WebSocket 服务本身支持跨节点转发(比如通过 Redis pub/sub 同步消息),那轮询也可以,但这是架构层面的事了,Nginx 配置只是配合。
6.2 WSS 证书与加密链路配置
生产环境不能裸奔ws://,必须上wss://。Nginx 配置也就是加一个 443 的 server 块:
server { listen 443 ssl http2; server_name ws.example.com; ssl_certificate /etc/nginx/certs/ws.example.com.pem; ssl_certificate_key /etc/nginx/certs/ws.example.com.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; location /ws/ { proxy_pass http://ws_backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_set_header Host $host; proxy_read_timeout 3600s; proxy_buffering off; } }配置 SSL 时有一个细节:ssl_certificate文件里可能不仅包含域名证书,还要把中间证书一起合并进去,否则部分客户端会报证书链不完整。常见做法是:
cat ws.example.com.pem intermediate.pem > fullchain.pem然后ssl_certificate指向fullchain.pem。
6.3 一次完整的配置升级记录
最后分享一下我之前做过的一次真实调整。项目背景是:Nginx 1.18,后端两个 Go 节点,WebSocket 用于 IM 消息推送,上线后反馈两个问题,一是连接偶尔掉线,二是单张图片发送偶尔失败。掉线问题在前面分析过,排除了云 LB 之后,定位到proxy_read_timeout默认 60 秒,而后端心跳是 25 秒一次,理论上不该断,但查看 access log 发现部分连接持续了正好 60 秒后关闭,说明那些连接在心跳帧发出前就被闲置计时器掐了,可能是后端心跳链路偶发异常。把proxy_read_timeout调到 3600 秒后再没此类问题。
图片发送失败的问题更有意思。前端把图片转成 base64 后塞进 WebSocket 发送,单条消息接近 2MB。Nginx 默认proxy_buffering on,导致数据攒在缓冲区里积累延迟,客户端以为没发出去就重新发送,来回几次直接把连接搞乱。最终配置改成proxy_buffering off,并把client_max_body_size调到 10m(虽然不是 WebSocket 握手请求,但同一个 server 块下有其他接口要用大 body),问题消失。
调完之后的最终配置在关键位置上都加了注释,现在新同事接手时基本不需要我问就能看懂为什么这些参数要这么设置。这也是我写这篇文章的初衷之一:Nginx 的 WebSocket 配置并不复杂,难的是理解每行配置为什么存在,遇到线上问题知道往哪查。把原理吃透,配置只是顺手的事。