开 头
做Web开发这些年,WebSocket的代理配置几乎是每个后端工程师都会撞上的一道坎。别看它平时不显眼,一旦线上出现“连接总是断开”“升级失败返回400”“聊天消息发不出去”这类问题,十有八九都出在Nginx这层。WebSocket和普通HTTP最大的区别就是它是长连接,一旦中间隔了一个Nginx,如果Nginx不配合做协议升级,后端连接根本建立不起来。所以这篇博文围绕“Nginx如何配置WebSocket代理”这件事,把原理、最小可用配置、生产环境改造、排错技巧和调优经验全部梳理一遍,适合正在接手带实时通信功能项目的开发者,也适合刚入门Nginx但对WebSocket概念模糊的运维同学参考。
1. WebSocket代理的核心逻辑与配置思路
1.1 HTTP升级机制:Nginx必须在握手阶段放行
WebSocket连接并不是独立的TCP协议,它借用了HTTP的Upgrade机制完成握手。客户端发来的请求头里会带Upgrade: websocket和Connection: Upgrade,服务端响应101 Switching Protocols之后,双方才正式切换成WebSocket长连接。
Nginx默认会把请求当作普通HTTP处理,收到Upgrade头时并不会自动转发给后端。它需要显式地照着规范把这两个头原样传给上游,后端才可能返回101。这就是为什么网上所有的Nginx WebSocket配置里都会出现这两行:
proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade;这里有个很微妙的点:Connection头不能简单地写成固定值Upgrade。因为并非所有请求都带Upgrade,如果后端还承载普通HTTP接口,无脑把Connection改成Upgrade,反而会让普通请求出现问题。所以更常见的做法是先定义一个map变量,根据请求头里是否有upgrade来决定Connection的值。这就是大量配置里connection_upgrade这个变量的由来。
1.2 为什么要经过代理:端口收敛、流量入口和故障隔离
有人会问,WebSocket应用直接把端口暴露出去不是更省事吗?项目里加Nginx代理,通常不是为了添乱,而是为了统一入口。业务服务器可能有多台,端口也各不相同,对外只暴露80/443一个口子,通过域名和location路径把流量分流到对应后端,这是最常见的管理模式。
另外,如果WebSocket服务后面要接国内外多个云节点,或者后端会滚动重启,Nginx还能做简单的负载均衡和故障摘除。虽然WebSocket长连接需要的粘性比较高,但经过代理后动态调整后端节点、做证书卸载、加访问限制,都变得容易很多。所以Nginx代理在这里不是可选项,几乎是所有生产部署的标准姿势。
1.3 选Nginx而不直接裸连的理由
很多语言框架本身支持WebSocket,比如Node.js的ws库、Python的websockets库,它们自己也能建立连接。但实际线上绝对不止一个WebSocket服务在跑,可能有实时的消息推送、在线协同、数据看板好几个模块。如果每个模块都开独立端口,运维端口管理就是一场灾难。
Nginx统一收敛端口后,除了代理WebSocket,还能顺手处理静态文件、普通API、HTTPS终止、限流。我们的经验是,除非整个应用只有WebSocket一个功能,否则Nginx这层代理是性价比最高的选择。它帮我们把各种协议的流量理清楚,后端服务只需要专注业务逻辑即可。
2. 上手实操:Nginx配置WebSocket代理的完整步骤
2.1 环境准备与版本要求
先说版本,Nginx从1.3.13开始就支持WebSocket反向代理,所以只要是近十年的Nginx版本基本都没问题。但低版本对HTTP/2和WebSocket同时开启支持不太友好,建议在生产环境使用1.18以上版本,用起来更省心。
我这里用一个典型的开发环境做示例:后端WebSocket服务监听在本机的8080端口,Nginx监听80端口,域名先用ws.example.com占位,实际调试时可以用127.0.0.1配hosts来模拟。
确认Nginx版本的命令很简单:
nginx -v如果低于1.3.13,建议直接升级,别在旧版本上浪费时间。
2.2 最小可用配置模板
这是Nginx配置WebSocket代理时最经典的最小模板,我至今还在用,只是根据需求加一些参数。先把这块拷到你的nginx配置里试通再说:
map $http_upgrade $connection_upgrade { default upgrade; '' close; } upstream ws_backend { server 127.0.0.1:8080; } server { listen 80; server_name ws.example.com; location / { 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_read_timeout 60s; proxy_send_timeout 60s; } }看懂这段配置,WebSocket代理的基本盘就拿下了。下面逐行拆一下它的用意。
2.3 核心字段逐一拆解:为什么这么写
先看map块。这段代码的意思是:如果请求头里带了Upgrade,那connection_upgrade就取值为upgrade;如果没带,就取值为close。这样普通HTTP请求和WebSocket请求走同一个配置也不会互相干扰。网上很多教程省略map直接写死Connection Upgrade,结果普通的POST请求到了后端会变得异常,就是这个细节闹的。
再看proxy_http_version 1.1。WebSocket握手要求HTTP/1.1,Nginx默认往上游发的是HTTP/1.0,1.0不支持Upgrade机制,所以必须显式改成1.1。这一行漏掉,后端的握手必然失败。
然后是proxy_set_header Upgrade $http_upgrade,注意它用的是$http_upgrade这个Nginx内置变量,取值来自客户端请求头。这里必须动态传值,不能写死,因为普通HTTP请求没有这个头,动态传值可以完美保留不同协议的场景差异。
Host $host这行也容易忽略。如果不设置,Nginx默认会用proxy_pass里的主机名去填充Host头,也就是ws_backend,后端服务拿到的Host就不是真实域名了,有些框架做域名校验或日志分析时就会出问题。
最后是proxy_read_timeout和proxy_send_timeout。Nginx默认的代理超时时间是60秒,但WebSocket是长连接,可能几分钟甚至几小时不通信。如果沿用默认值,一旦后端超过60秒没返回数据,Nginx就会主动掐断连接。所以需要把超时时间调长,比如3600秒,或者根据业务链路最长静默时间设定。纯聊天应用心跳间隔通常30秒,调3600秒绰绰有余。
2.4 配置生效与验证握手是否成功
配置写完先检查语法,再重载:
nginx -t nginx -s reload验证能不能真的代理成功,不要只用浏览器打开页面说“能访问就行”。WebSocket握手是否成功,要在后端日志里看有没有返回101,也可以用命令行工具连接测试:
curl -i -N \ -H "Connection: Upgrade" \ -H "Upgrade: websocket" \ -H "Host: ws.example.com" \ -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \ -H "Sec-WebSocket-Version: 13" \ http://127.0.0.1/一条curl命令能帮你快速判断Nginx有没有正确转发Upgrade请求。如果返回的是101 Switching Protocols,说明代理链路已经通了;如果返回404或400,那就要回到配置里找问题。另一个更省事的方案是用websocat工具,但我通常直接用这条curl验证,不引入额外依赖也行。
用浏览器调试的话,打开DevTools的Network面板,WebSocket请求会单独分类,点击可以查看握手请求和响应头。看到101就说明通了。
2.5 踩坑经验:location路径别随意配
我见过不少人把WebSocket代理放在location /ws/下,结果前端实际连接写的是wss://domain/,导致握手请求发到/而不是/ws/,路由根本匹配不上。这里的关键是:location里的路径必须和前端WebSocket连接URL的路径一致。如果业务要求连接地址是/socket,Nginx就配location /socket;是/就配location /,别想当然。
还有一点,Nginx的location匹配遵循前缀最长匹配原则。比如同时存在location /和location /ws,连接/ws/chat会优先匹配/ws。如果遇到代理没生效,先用nginx -T查看实际加载的配置,确认请求落到了哪个location里。
3. 生产环境改造:从能跑到能用
3.1 关闭代理缓冲,让消息即时推送
Nginx默认会缓冲上游返回的数据,等攒够一包再发给客户端。对于WebSocket这种实时性要求高的协议,缓冲反而造成不必要的延迟。而且WebSocket是全双工通信,两边随时都在互相发消息,Nginx的proxy_buffering在长期连接下容易导致内存积压。
生产环境建议在location块里显式关闭缓冲:
location / { proxy_buffering off; proxy_cache off; }实测下来,加上这两行后消息推送的延迟明显降低,尤其是在后端频繁推送小数据包(比如行情行情、通知提醒)的场景里,效果非常明显。
3.2 会话粘性:负载均衡下如何保证连接稳定
WebSocket只是建立了一次TCP连接,但业务层往往带有状态。比如在线聊天,用户登录后服务端在内存里维护了会话,如果Nginx把同一个用户的不同请求分发到不同后端节点,业务上就会乱套。
Nginx做负载均衡时有几种方式处理这个问题。最简单的是ip_hash,按客户端IP计算哈希值,同一个IP固定打到同一个后端:
upstream ws_backend { ip_hash; server 10.0.0.1:8080; server 10.0.0.2:8080; }但ip_hash有个缺陷:如果后端节点数量变化(上线或摘除),同一IP的请求可能被重新映射到其他节点,长连接就会断开。更稳妥的做法是在应用层实现会话同步,后端节点之间共享会话状态,这样即使Nginx转发到不同节点也能正常工作。如果条件允许,优先做会话同步而不是依赖Nginx的粘性策略。
还有一种least_conn策略,适合长连接负载不均的场景,它会优先把新请求转发给当前活跃连接最少的后端。对WebSocket这种每个连接都长时间占用的协议来说,least_conn比ip_hash更科学,但结合业务状态时可能还需要配合sticky模块使用。
我这里给出一个带sticky的配置示例,它通过Nginx生成的cookie来固定连接(Nginx Plus或openresty自带该模块,开源版需要编译时启用):
upstream ws_backend { sticky cookie srv_id expires=1h; server 10.0.0.1:8080; server 10.0.0.2:8080; }这个方案对应用层最透明,cookie一设,会话自然固定到具体节点,不需要后端做任何改动。核心思路是:代理层的粘性只是兜底方案,真正的长期稳定还得靠后端实现无状态或会话共享。
3.3 同域名多服务共存:API和WebSocket一起代理
线上很多项目是同一个域名下既有REST API又有WebSocket接口。这时候路由设计要清晰,比如API走/api/,WebSocket走/ws/,两个location分开配置。
下面是我在实际项目中用过的同域名双location配置:
server { listen 443 ssl http2; server_name example.com; ssl_certificate /etc/nginx/certs/example.com.crt; ssl_certificate_key /etc/nginx/certs/example.com.key; location /api/ { proxy_pass http://api_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 普通HTTP接口,不需要Upgrade相关头 } 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_send_timeout 3600s; proxy_buffering off; } }这样配置的好处是前端只需要统一用wss://example.com/ws和https://example.com/api,端口统一、证书统一、跨域问题也少很多。SSL终止由Nginx完成,后端服务直接监听HTTP即可,不用每个后端都配置证书。但要注意,WebSocket前端如果用的是wss://,Nginx上必须开启SSL并正确配置证书,否则浏览器会直接拒绝连接。
3.4 容器化场景:上游地址如何写
现在服务基本都跑在Docker里了,Nginx容器和后端容器在同一网络下时,upstream里的server地址不能写127.0.0.1,要写容器名或服务名。比如docker-compose里:
services: nginx: image: nginx:1.24 ports: - "80:80" networks: - app-net ws_app: image: myapp networks: - app-netNginx配置文件里的upstream要对应写成:
upstream ws_backend { server ws_app:8080; }在Docker环境里配Nginx,还有一个老生常谈的问题:Nginx容器reload之后,DNS解析可能不会自动更新。如果你动态修改过容器的IP地址,Nginx可能还在连旧的IP。解决办法是让Nginx容器和上游容器使用同一个Docker网络,并设置resolver指令配合set变量动态解析上游地址,或者干脆重启Nginx容器让连接重置。
3.5 日志配置:连接断开时能查到原因
调试WebSocket问题,第一件事就是看Nginx日志。默认的access_log只记录请求行,根本看不出握手是否成功、连接什么时候断开。生产环境务必配置详细日志格式:
log_format wslog '$remote_addr - $remote_user [$time_local] ' '$request_method $request_uri $status ' '$http_upgrade "$http_user_agent" ' 'upstream_addr=$upstream_addr ' 'upstream_status=$upstream_status ' 'request_time=$request_time ' 'body_bytes_sent=$body_bytes_sent'; server { ... access_log /var/log/nginx/ws_access.log wslog; error_log /var/log/nginx/ws_error.log warn; }如果连接断开了,重点看错误日志里的upstream prematurely closed connection或read timed out,这类日志能直接告诉你问题出在Nginx和后端之间的链路,还是客户端和Nginx之间的链路。我自己排错时,90%的问题靠这两份日志就能锁定方向,远比抓包来的高效。
4. 常见问题与排查实战记录
4.1 握手失败:返回400 Bad Request怎么办
第一次配WebSocket代理时最容易碰到的错误就是客户端打开连接直接报400 Bad Request。原因基本都是Nginx没转对Upgrade头或Connection头。排查步骤很简单,用上面的那条curl命令发一个带Upgrade头的请求,看Nginx返回什么。
如果返回400,先检查自己的配置里proxy_http_version 1.1有没有写,proxy_set_header Upgrade $http_upgrade有没有写。如果两个都写了还报400,再检查是不是map块没定义好,导致$connection_upgrade变量为空。我曾经因为把map写到了另一个配置文件里,而那个文件没被include,结果$connection_upgrade一直是空字符串,Nginx直接报错。
另外,注意Nginx配置中map块必须放在http层,不能放到server或location里,否则语法错误或变量不生效。
4.2 连接建立后几秒就被断开
这种问题有一个典型特征:浏览器能连上,但每隔一小会儿就断开重连。如果断开间隔正好是60秒,那基本可以断定是proxy_read_timeout或proxy_send_timeout默认值60秒在作怪。
WebSocket空闲时,如果没有心跳包,Nginx就认为连接无数据流动,到超时时间直接掐断。解决方案有两个层面:
第一层,Nginx把超时时间调大,比如proxy_read_timeout 3600s。但这只是治标不治本,如果业务本身设计有静默期,建议从前端加心跳机制,每隔30秒发一个ping帧,既保活连接,也让Nginx感知到流量。前后端配合起来,连接才能稳定挂一天不脱落。
第二层,调整Nginx的keepalive_timeout参数。这个参数控制Nginx与客户端之间空闲连接的超时时间。注意它是HTTP keepalive的参数,和WebSocket的长连接并不完全一样,但同样会影响连接生命周期。生产环境我一般设置keepalive_timeout 65s,略大于前端心跳间隔,预防临界情况。
4.3 到了凌晨连接就大量断开
有一段时间客户反馈,每天晚上两点左右WebSocket连接集体断开。排查发现是因为Nginx配置了每天凌晨的重载任务,重载后旧的worker进程要退出,正在处理的长连接也被干净利落地关闭。
这里面有个容易忽略的机制:Nginx平滑重载会保留一部分连接缓冲,但WebSocket长连接通常无法无缝迁移到新worker进程。所以如果业务需要在低峰期更新配置,最好配合后端的断线重连机制,让客户端在连接断开后自动重连。
最终我们做的处理是:把Nginx的reload从定时任务里去掉,改成人肉操作,只在真正的配置变更时reload。同时后端WebSocket服务补上了完善的断线重连逻辑,客户端对瞬时断开的容忍度大大提高。
4.4 后端日志显示连接正常,但客户端推送失败
Nginx日志、后端日志都显示连接正常,但客户端收不到推送消息。这类问题隐蔽性很强,有一次我们调了很久才发现,是因为Nginx打开了gzip压缩,它对WebSocket帧进行了压缩,而客户端的WebSocket库不支持压缩扩展,导致消息解析失败。
解决办法是给WebSocket代理的网络路径关闭gzip:
location /ws/ { gzip off; ... }另外,proxy_buffering如果开着,也可能导致推送的数据积压在Nginx缓冲区里不往下发。这个前面提过,生产环境务必关掉。
4.5 前端连的是wss,Nginx证书校验失败
海量问题集中在net::ERR_CERT_COMMON_NAME_INVALID这类证书错误上。排查思路很简单:证书的CN或SAN字段必须与客户端访问的域名匹配。如果证书是给example.com签发的,但客户端连的是ws.example.com,那必然校验失败。
解决方法是换匹配域名的证书,或者让客户端使用相同域名连接。如果只是为了本地调试,可以在Nginx配置里临时关闭SSL验证,但生产环境千万别这么干。
还有一种情况是客户端缺少根证书。自签名证书在浏览器和操作系统里都不被信任,需要手动导入。如果是企业内部系统,可以考虑把自签名证书安装到所有客户端的信任库;如果是公网服务,直接用Let's Encrypt免费证书,省心得多。
4.6 WebSocket+多级代理:经过CDN时容易卡住
项目里如果套了CDN,CDN节点对WebSocket的支持参差不齐。有些CDN默认不转发Upgrade头,导致客户端连CDN节点时握手就失败。解决方法是确认CDN厂商支持WebSocket,并在CDN配置里显式开启。如果没有特殊需求,把WebSocket连接的域名直接解析到源站,不走CDN,是最省事的路子。
我在一个项目里遇到过CDN把WebSocket正常转发,但回源时用HTTP/2,导致后端收到的Upgrade头格式不对。最终处理是让CDN回源强制走HTTP/1.1,问题才解决。这种多层代理的链路中,每层都要检查协议版本和头转发策略。
5. 性能、监控与安全加固的实践经验
5.1 worker进程与连接数上限的调优
WebSocket是长连接,单个客户端会长时间占用一个连接,连接数上限比普通HTTP场景更容易触顶。Nginx的worker_connections决定每个worker进程能同时处理的连接数,如果预估客户端同时在线1万个,建议配:
worker_processes auto; events { worker_connections 10240; use epoll; }注意worker_connections是每个worker进程的连接数,不是全局的。假设4个worker进程,每个10240,全局就能处理四万个连接。实际还要给普通HTTP流量留出余量,不要卡得太死。
另外系统层面的调整也不能漏。文件描述符限制默认可能是1024,WebSocket连接一多就报错。生产环境建议把ulimit -n调到65535以上,并同步修改/etc/security/limits.conf。Nginx的worker_rlimit_nofile指令也要设置:
worker_rlimit_nofile 65535;5.2 keepalive连接复用:减少上游建连开销
Nginx和后端之间的TCP连接复用在WebSocket场景下比较特殊,因为WebSocket长连接本身就很难复用,但HTTP层面的keepalive可以帮普通API分担一部分压力。
在upstream块里加keepalive参数,可以让Nginx与后端保持一部分空闲连接,减少频繁建连的开销:
upstream ws_backend { server 127.0.0.1:8080; keepalive 64; }不过要注意,WebSocket握手成功后,该连接就进入长连接状态,不再参与keepalive复用池。所以这个参数对WebSocket本身的收益有限,主要是让同一个后端节点上的普通API请求受益。如果后端全是WebSocket服务,keepalive设置反而意义不大,可以忽略。
5.3 限制访问与连接频率
WebSocket服务对外暴露后,容易被扫描和恶意连接。Nginx层可以做基础防护:限制单个IP的连接数,限制握手频率。
limit_conn_zone $binary_remote_addr zone=ws_conn:10m; limit_req_zone $binary_remote_addr zone=ws_req:10m rate=10r/s; server { location /ws/ { limit_conn ws_conn 20; limit_req zone=ws_req burst=5; # 其他代理配置 } }limit_conn限制的是同一IP并发连接数,limit_req限制的是握手请求频率。WebSocket握手请求本身很轻量,但绝对数量一多,Nginx的CPU和内存就会吃紧。这两个限制能挡住绝大多数低水平攻击。
需要注意的是,如果用户IPv6普及程度高,$binary_remote_addr对IPv6取到的地址长度不一样,可能导致zone大小不够。实际部署时建议根据情况调整zone空间,或者按IP类型分别定义。
5.4 鉴权前置:握手阶段就把非法请求挡在门外
WebSocket协议在握手阶段可以通过HTTP头传递token或cookie,Nginx可以用简单的if判断做一层前置鉴权。比如校验是否存在特定Header:
location /ws/ { if ($http_sec_websocket_protocol !~* "^chat\.v1$") { return 403; } ... }上面的判断其实有点简陋,实际项目更常见的是让客户端在查询参数里带token,Nginx用map或者auth_request去校验。完整方案是用auth_request请求一个认证接口:
location /ws/ { auth_request /auth; proxy_pass http://ws_backend; ... } location = /auth { internal; proxy_pass http://auth_service/check_ws_token; proxy_pass_request_body off; proxy_set_header Content-Length ""; proxy_set_header X-Original-URI $request_uri; }auth_request的子请求模式是生产环境里最常用也最稳妥的方案。校验失败返回401,Nginx直接拒绝升级,后端服务完全不用感知非法流量。但要注意auth_request每来一次握手就发一个子请求,对认证服务的压力会放大,最好给认证服务加上缓存或限流。
5.5 监控连接状态:别等用户投诉才发现服务挂了
WebSocket服务不像HTTP那样每个请求都有明确的成败状态,连接挂没挂,只有客户端感知最清楚。Nginx的监控维度我一般看三点:request_time异常升高、upstream_status出现500或502、Nginx的active connections数量波动异常。
有条件的项目建议把Nginx的stub_status模块打开:
location /nginx_status { stub_status on; access_log off; allow 127.0.0.1; deny all; }配合Prometheus抓取指标,可以实时看到Active connections、Waiting、Writing等数值。对WebSocket服务来说,Writing这个数字尤其重要,它代表正在向客户端写数据的连接数。如果Writing长时间居高不下,说明后端推送的积压严重,数据发不出去,得赶紧查后端消费速度。
5.6 常见问题速查表
| 现象 | 可能原因 | 排查/处理手段 |
|---|---|---|
| 连接返回400 | HTTP版本不是1.1,或Upgrade头没转 | 确认proxy_http_version 1.1与Upgrade头设置 |
| 客户端握手成功但立刻断开 | Websocket服务端主动关闭 | 检查后端日志,确认业务层是否拒绝连接 |
| 连接坚持60秒左右断开 | 超时时间太短 | 调大proxy_read_timeout,或前端加心跳 |
| 浏览器报ERR_CERT_COMMON_NAME_INVALID | SSL证书域名不匹配 | 更换与访问域名匹配的证书 |
| 偶发断连,后端没日志 | Nginx重载或worker进程退出 | 避免频繁reload,前端设计重连机制 |
| 通过CDN连接失败 | CDN不支持/没开启WebSocket | 确认CDN功能,必要时让WebSocket直连源站 |
| 后端收不到推送消息 | 代理缓冲没关或gzip干扰 | 设置proxy_buffering off、gzip off |
| 大量连接建立时报EMFILE | 文件描述符上限不足 | 调worker_rlimit_nofile与系统ulimit |
6. 备选方案与扩展讨论
6.1 openresty的优势场景
如果项目里不止是转发,还要在Nginx层做请求改写、流量染色或自定义鉴权,可以考虑换成OpenResty。它在Nginx基础上集成了Lua脚本能力,我一个项目里用它做了按渠道动态调整WebSocket心跳间隔的逻辑,用原生Nginx写起来会非常痛苦。
用OpenResty做WebSocket代理的大体配置和Nginx基本一样,因为核心模块是共通的。但OpenResty可以让工程师在access_by_lua阶段直接读取WebSocket握手请求的Header、客户端IP、Cookie等信息,做出更灵活的鉴权和限流策略。适合那种鉴权逻辑经常变,又不想反复改Nginx配置重载的场景。
6.2 多域名多证书的WebSocket部署
业务大了之后,不可能所有WebSocket都挂在一个域名下。比如IM、工单提醒、日志推送是不同的子域名,证书也要分开。Nginx 1.19以后支持动态证书加载,配置一个server块内通过变量选择证书文件,减少重复配置。但更稳妥的仍然是分别定义server,每块独立配置证书和location,逻辑一目了然。
server { listen 443 ssl; server_name im.example.com; ssl_certificate /etc/nginx/certs/im.example.com.crt; ssl_certificate_key /etc/nginx/certs/im.example.com.key; location /ws/ { ... } } server { listen 443 ssl; server_name notify.example.com; ssl_certificate /etc/nginx/certs/notify.example.com.crt; ssl_certificate_key /etc/nginx/certs/notify.example.com.key; location /ws/ { ... } }多域名部署时别忘了检查证书的SAN字段是否覆盖所有域名,否则又回到ERR_CERT_COMMON_NAME_INVALID的老问题。
6.3 与云负载均衡组合时的注意事项
如果前面还有云厂商的负载均衡(比如阿里云SLB或腾讯云CLB),那么Nginx虽然是链路的另一端,但它和云LB之间也存在代理关系。云LB默认可能把HTTP/2打开了,而WebSocket在HTTP/2中的支持场景更复杂。最简单的稳妥配置是:云LB到Nginx这段、Nginx到后端这段,全部统一走HTTP/1.1。多一层代理就多一层变量,链路越统一,排查越简单。
还有一个典型的坑:云LB的会话保持(会话粘滞)默认基于IP,但客户端经过IPv6 NAT后源IP并不能作为可靠标识。此时需要在云LB上配置基于Cookie的会话保持,并把proxy_set_header Cookie $http_cookie和proxy_pass_header Set-Cookie都配好,防止cookie在Nginx这层被吞掉。
7. 再次总结我的实操体会
配置WebSocket代理这种事,看起来只是Nginx里的几行配置,但真正在生产环境跑顺手,需要把超时、粘性、缓冲、日志、证书、心跳这些细节点全部想清楚。我在项目里大概踩过三轮坑:第一轮是纯转发,能连上就以为完事了;第二轮开始处理超时和粘性问题,解决断连和负载不均;第三轮才把监控、限流、鉴权这些安全手段补齐,让WebSocket服务真正敢对外公开。
给后来者一个建议:WebSocket代理的配置不要照抄,先弄懂每一行的含义,再结合自己的业务场景做取舍。比如心跳周期是30秒还是2分钟,直接决定Nginx的read_timeout该设多大;后端节点是否会滚动发布,决定要不要用ip_hash依赖Nginx的粘性,还是要把会话逻辑下沉到应用层。任何生产配置脱离了业务场景,都有可能变成另一场事故的源头。
最后分享一个排错技巧:遇到诡异的WebSocket断连问题,别急着怀疑网络或中间设备,先用tcpdump在Nginx入口和出口各抓一次包,对比两端的时间戳和序列号。抓包结果能清清楚楚地告诉你连接是在哪个环节断的。Nginx日志、后端日志、客户端日志这“三方日志”对不上时,抓包是最后的决定性证据。掌握这个思路,WebSocket代理层面的问题基本都能给你快速排查干净,后面的路就好走了。