运维知识
悠悠
2026年4月7日

生产环境 Nginx 代理 WebSocket 总断连?这几个隐藏配置坑了多少人

前两天半夜正准备关机睡觉,微信突然响个不停。消息连着发了好几条,全是感叹号,说他们新上线的即时通讯服务炸了,客户端连两分钟都撑不住就掉线,重启服务好了一小会儿,马上又不行了。

我看了一眼他发来的 Nginx 配置截图,再看了看报错日志,心里大概就有数了。这情况太典型了,很多兄弟在搞 WebSocket 代理的时候,都是照着网上的教程一顿抄,proxy_pass 一配,upstream 一挂,测试环境通了一下就觉得完事了。结果一上生产,并发上来,时间一长,各种稀奇古怪的问题全冒出来。

WebSocket 这玩意儿,看着简单,不就是 HTTP 协议升个级嘛,但在 Nginx 这一层面,如果不把它的脾气摸透,真的能把人折腾死。今天咱们就借着这个实际案例,把 Nginx 代理 WebSocket 这件事彻底掰开了揉碎了讲讲,特别是那些没人告诉你、但生产环境一定会遇到的坑。

咱们先得搞清楚,WebSocket 到底是个啥,为什么 Nginx 默认的 HTTP 代理配置搞不定它。

平时咱们浏览网页,大多是 HTTP 协议。这玩意儿是个“哑巴”模式,客户端发个请求,服务器回个响应,这就完事了,连接断开。就像你去食堂打饭,你喊一句“阿姨来份红烧肉”,阿姨给你打一勺,交易结束,你俩就没关系了。下次想要米饭,得重新排队、重新喊。

但是 WebSocket 不一样。它是一次握手,长久通话。就像你跟对象打电话,拨通那一刻(握手),双方的话筒(连接)就一直通着。你想说话就说,对方想回话就回,不用每次都重新拨号。这就是所谓的全双工通信。对于即时通讯、在线游戏、股票行情这种需要服务器主动推数据给客户端的场景,简直是神器。

那问题来了,Nginx 作为一个反向代理,默认是处理 HTTP 请求的。它就像一个中间商,负责把客户的请求转给后端的服务器。对于普通的 HTTP 请求,Nginx 转发完就等着后端回包,然后发给客户,任务结束。

但 WebSocket 来了,Nginx 就有点懵了。WebSocket 的建立过程,首先是发一个 HTTP 请求,但是这个请求头里带了点“私货”:

Upgrade: websocket
Connection: Upgrade

这两句话的意思就是告诉服务器:“哥们,我不发普通网页请求了,咱们把协议升个级,换成 WebSocket 玩玩吧。”

如果 Nginx 不做特殊配置,它看到这个请求,会把它当成普通 HTTP 请求转给后端。后端服务器(比如 Node.js、Go 或者 Java)收到请求,一看头信息,知道要升级协议,就会返回一个 101 Switching Protocols 的状态码。

这时候,如果 Nginx 还傻傻地等着后端返回 HTTP 200 或者 404,那连接就断了。因为 101 状态码意味着协议切换,之后的数据流不再是标准的 HTTP 报文,而是 TCP 数据流了。

所以,要让 Nginx 成功代理 WebSocket,核心就是得让它“睁一只眼闭一只眼”,允许协议升级,并且把这个升级的请求头原封不动地传给后端,同时还要保持这条连接长时间不断开。

咱们来看看小张发给我的那个“有问题”的配置,估计很多人也是这么配的:

upstream ws_backend {
    server 192.168.1.100:8080;
}

server {
    listen 80;
    server_name ws.example.com;

    location /ws {
        proxy_pass http://ws_backend;
    }
}

这配置看着挺干净,没毛病吧?测试环境测一下,发个消息,能收到。然后大家就以为搞定了。

实际上,这配置在生产环境就是个“定时炸弹”。

第一个坑,也是最直接的坑,就是握手失败。很多时候你会发现,浏览器控制台报 WebSocket connection failed,状态码 400 或者 502。

这是因为 Nginx 默认转发请求的时候,可能会修改或者丢弃 UpgradeConnection 这两个头。后端收不到这两个头,就不知道你要升级协议,自然就按普通 HTTP 请求处理了,结果就是握手失败。

所以,咱们得明确告诉 Nginx,把这两个头给我传过去。标准配置得这么写:

location /ws {
    proxy_pass http://ws_backend;
  
    # 关键配置:必须使用 HTTP 1.1 版本
    proxy_http_version 1.1;
  
    # 透传 Upgrade 头
    proxy_set_header Upgrade $http_upgrade;
  
    # 设置 Connection 头
    proxy_set_header Connection "Upgrade";
}

这里有个细节,proxy_http_version 1.1; 这一行千万别漏了。WebSocket 是依赖 HTTP/1.1 的特性的,HTTP/1.0 不支持。你要是不写,Nginx 可能会用 1.0 去连后端,那肯定不行。

还有个细节,proxy_set_header Connection "Upgrade"; 这里直接写死了 Upgrade。其实网上有些教程会写 proxy_set_header Connection $connection_upgrade;,然后在 http 块里做个 map 映射。那种写法更严谨一点,可以根据客户端的请求头动态设置,但在大多数简单场景下,直接写死 Upgrade 也能用,咱们先以简单为主,能用再说。

配置改完,重载 Nginx,以为这就完了?小张说,他们改了配置后,握手确实成功了,消息也能发了,但是过了一会儿,大概一两分钟没操作,连接就自动断了。

这就是第二个大坑:超时设置。

咱们刚才说了,WebSocket 是长连接。可能客户端和服务器建立连接后,很久都不说话,就那么挂着,等有消息了再推。比如你看股票软件,可能几分钟都不动一下。

但是 Nginx 默认是个急性子。它有个参数叫 proxy_read_timeout,默认值是 60 秒。这玩意儿的逻辑是:如果 Nginx 在 60 秒内没有从后端服务器读到任何数据,它就认为后端挂了或者网络出问题了,为了释放资源,它会主动切断这个连接。

这下你明白了吧?用户连上来,发两句消息,然后去倒杯水,回来一看,断了。因为 Nginx 等了 60 秒没见后端有数据过来,直接把连接掐了。

解决办法很简单,把超时时间设长点。

location /ws {
    # ... 之前的配置 ...
  
    # 延长超时时间,比如设成 1 小时,或者更长
    proxy_read_timeout 3600s;
    proxy_send_timeout 3600s;
}

这里我设成了 3600 秒(1 小时)。具体设多少,得看你的业务场景。有些业务是 24 小时挂机的,那你就得设得更大,或者干脆在业务层做心跳机制。

说到心跳,这其实是解决超时问题的最佳实践。光靠 Nginx 延长超时有时候不够稳妥,因为网络链路上不光有 Nginx,还有防火墙、负载均衡器(比如阿里云的 SLB、AWS 的 ELB),它们也有超时时间。

最稳妥的办法是,客户端每隔 30 秒或者 45 秒,给服务器发一个心跳包,内容可以是空的,或者一个简单的字符串。服务器收到心跳包,回一个响应。这样,Nginx 就能检测到连接上有数据流过,就不会触发超时断连了。这就像是给连接“续费”一样。

小张他们后来加了心跳,超时问题解决了,以为这就稳了。结果压力测试一来,又出幺蛾子了。用户反馈,有时候发消息,服务器收不到,或者服务器推的消息,用户收不到,而且这现象是随机的。

一查日志,发现是因为他们后端做了集群,用了 Nginx 做负载均衡。

这就引出了第三个坑:负载均衡的会话保持。

WebSocket 是有状态的。客户端跟哪台后端服务器建立了连接,后续的通信就必须在这台服务器上进行。因为连接的上下文是保存在那台服务器的内存里的。

如果 Nginx 的负载均衡策略配置不当,比如用了默认的轮询,就会出大问题。

举个例子,客户端第一次请求握手,Nginx 把请求转发给了服务器 A,握手成功,连接建立。这时候,连接是存在于服务器 A 上的。

过了一会儿,客户端发了一条消息,这个请求到了 Nginx。因为负载均衡是轮询,Nginx 把这个请求转发给了服务器 B。服务器 B 一看:“哥们,你是谁啊?我内存里没你的连接信息啊。” 于是服务器 B 就无法处理,或者直接报错,连接也就废了。

所以,WebSocket 的负载均衡,必须得做会话保持,也就是“粘性会话”。

最简单的办法是用 ip_hash。根据客户端的 IP 地址,算一个 hash 值,同一个 IP 的请求永远转发给同一台后端服务器。

upstream ws_backend {
    ip_hash;
    server 192.168.1.100:8080;
    server 192.168.1.101:8080;
}

这样,同一个客户端的连接就会一直打在同一台机器上,问题就解决了。

但是 ip_hash 有个缺点,如果某个 IP 的流量特别大,那台后端服务器压力就会很大,而且如果那台服务器挂了,hash 算法会重新计算,可能会导致一部分用户掉线。

更高级一点的玩法是用 sticky cookie 模块,或者后端用 Redis 共享连接状态。不过对于大多数中小规模的应用,ip_hash 已经够用了。后来上了 ip_hash,随机掉线的问题立马就没了。

还有个事儿,也是容易被忽略的,就是 HTTPS。现在谁还敢用 HTTP 啊,全是 HTTPS。WebSocket 对应的就是 WSS

如果你的前端页面是 HTTPS 的,那 WebSocket 连接也必须是 WSS,否则浏览器会直接拦截,报混合内容错误。

Nginx 配置 WSS 其实也不难,就是普通的 HTTPS 配置加上刚才那些 WebSocket 的配置。

server {
    listen 443 ssl;
    server_name ws.example.com;
  
    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;
  
    location /ws {
        proxy_pass http://ws_backend; # 这里依然是 http,因为 Nginx 到后端通常走内网 HTTP
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_set_header Host $host;
      
        # 别忘了超时
        proxy_read_timeout 3600s;
    }
}

这里有个小细节,proxy_pass 后面写的是 http://ws_backend。这说明 Nginx 到后端服务之间,走的是 HTTP 协议(握手阶段)。有些人会问,我要不要在后面也配 SSL?一般来说没必要,因为 Nginx 已经在入口处把 SSL 卸载了,内网通信再加密反而增加开销,除非你的安全等级要求极高。

另外,因为前端是 HTTPS,Nginx 这里监听 443 端口,所以前端连接的时候地址得写成 wss://ws.example.com/ws,别写成 ws:// 了,否则肯定连不上。

最后再聊聊排错的事儿。如果你配完了,还是连不上,怎么查?

第一招,看浏览器 F12 控制台。这是最快的方法。切到 Network 标签,点 WS 过滤器。看那个请求的状态码。

如果是 101 Switching Protocols,恭喜你,握手成功了。后面如果断了,那就是连接保持的问题,查超时,查心跳。
如果是 400 Bad Request,通常是 Upgrade 头没传过去,或者 Nginx 配置有语法错误。
如果是 502 Bad Gateway,那是 Nginx 连不上后端服务,查后端服务起没起,端口对不对,防火墙有没有开。
如果是 504 Gateway Timeout,那是后端处理太慢,或者直接没响应。

第二招,看 Nginx 错误日志。tail -f /var/log/nginx/error.log。有时候 Nginx 会把具体的错误原因打出来,比如 upstream prematurely closed connection,这通常意味着后端服务崩了,或者后端主动断开了连接。

第三招,用 tcpdump 抓包。这是大招。在 Nginx 服务器上抓包,看 TCP 握手有没有成功,看 HTTP 请求头里有没有 Upgrade: websocket。如果头没发出来,那是客户端的问题;如果头发了,Nginx 没转发,那是 Nginx 配置的问题。

其实搞运维这么多年,WebSocket 代理这事儿,说难不难,说简单也不简单。难就难在它跨越了 HTTP 和 TCP 的界限,而且涉及到了长连接的维护。咱们平时习惯了 HTTP 这种“打一枪换个地方”的短连接思维,遇到长连接就容易按惯性思维去配,结果就踩坑。

总结一下,Nginx 代理 WebSocket 的核心要点就这几条:

  1. 协议升级头UpgradeConnection 必须透传,HTTP 版本必须是 1.1。
  2. 超时时间proxy_read_timeout 必须设长,配合心跳机制保活。
  3. 负载均衡:必须配置会话保持,推荐 ip_hashsticky
  4. 安全配置:前端 HTTPS 对应后端 WSS,注意混合内容限制。

把这些点都记住了,以后遇到 WebSocket 连接问题,按图索骥,基本都能解决。小张的问题最后排查下来,其实就是超时没设,加上负载均衡没做会话保持,改完配置,压测一把过,服务稳得一批。

运维这活儿,很多时候就是这样,原理通了,配置对了,问题自然就没了。希望今天这一通唠叨,能帮大家把 WebSocket 代理这点事儿彻底搞明白,以后再遇到类似需求,直接一套带走,再也不用半夜发微信求救了。

行了,今天就先聊到这儿,我也得去补个觉了。如果觉得这篇文章对你有帮助,哪怕只是帮你避开了一个坑,也请点个赞或者在看支持一下。咱们下期见!

公众号:运维躬行录
个人博客:躬行笔记

文章目录

博主介绍

热爱技术的云计算运维工程师,Python全栈工程师,分享开发经验与生活感悟。
欢迎关注我的微信公众号@运维躬行录,领取海量学习资料

微信二维码