Nginx 反向代理 WebSocket:握手 101 成功后秒断的三个配置坑
Nginx 反向代理 WebSocket:握手 101 成功后秒断的三个配置坑
最近帮一个实时看板项目把 WebSocket 服务搬到 Nginx 后面,遇到一个很迷惑的现象:客户端握手返回 101 Switching Protocols,连接明明建立了,却在 60 秒左右准时断开,重连后又是 60 秒。查了三轮才发现,问题全在 Nginx 的默认配置里——握手成功只代表升级那一刻没错,不代表长连接能活下去。下面是三个真实踩过的坑。
坑一:没写 Upgrade 头,握手直接 400
WebSocket 握手本质是一次带特殊头的 HTTP 请求。客户端发:
GET /ws HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Nginx 默认用 HTTP/1.0 连上游,而且默认不会把 `Upgrade` 头透传过去。上游收到的就是一次普通 GET,自然回 400 或者当成普通请求处理。正确写法是显式声明 HTTP/1.1 并透传两个头:
location /ws {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
这里有个细节:`Connection` 头的值建议写死 `"upgrade"`,不要直接透传 `$http_connection`。有些客户端发的是 `Connection: keep-alive, Upgrade`,原样透传到上游后,部分后端框架解析不到 Upgrade 关键字,照样握手失败。写死是最稳妥的。
还有一个变体写法,用 map 做条件透传,适合同一个 location 又要服务普通 HTTP 又要服务 WebSocket 的场景:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}
普通请求没有 Upgrade 头时,`$connection_upgrade` 是 `close`,不影响正常代理;WebSocket 请求时自动变成 `upgrade`。
坑二:默认 60 秒读超时,空闲连接准时被掐
这是本次最隐蔽的坑。握手成功、消息也能收发,但连接总在 60 秒左右断开,客户端日志里只有 `1006 abnormal closure`,没有任何服务端报错。
根因是 `proxy_read_timeout` 默认 60 秒:上游 60 秒内没给 Nginx 发任何数据,Nginx 就认为上游死了,主动关连接。WebSocket 空闲时本来就没数据——心跳包往往是 30 秒或 60 秒一发,刚好卡在边界上。修复办法是把超时拉长到业务心跳周期的几倍:
location /ws {
proxy_pass http://127.0.0.1:8080;
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_read_timeout` 只管“上游多久不说话”,不管客户端。如果客户端 60 秒不发消息、服务端也不推消息,连接一样会被掐。所以光改 Nginx 还不够,应用层最好也配一个 25~30 秒的心跳(ping/pong 帧),两边都留余量。改完之后用 `curl` 验证长连接是否真的活过 60 秒:
curl -i -N -H "Connection: Upgrade" -H "Upgrade: websocket" \
-H "Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==" \
-H "Sec-WebSocket-Version: 13" \
http://127.0.0.1/ws
返回 101 后不要马上 Ctrl-C,等 90 秒,看连接是不是还活着。只看握手成功就收工,等于只测了一半。
坑三:location 末尾斜杠吃掉路径,上游 404
`proxy_pass` 后面带不带 URI,行为完全不同:
# 写法 A:上游收到 /ws
location /ws {
proxy_pass http://127.0.0.1:8080;
}
# 写法 B:上游收到 / (/ws 被替换掉了)
location /ws {
proxy_pass http://127.0.0.1:8080/;
}
如果后端只在 `/ws` 这个路径上挂了 WebSocket 处理器,写法 B 会让上游收到 `/`,直接 404,客户端看到的就是握手失败。反过来,如果后端挂在根路径,写法 A 又会多出 `/ws` 前缀。迁移旧服务时最容易在这里翻车——直连后端是好的,一上 Nginx 就 404。
排障时先看 Nginx 的 `error.log`,`upstream prematurely closed connection` 一般是上游挂了或超时,`no live upstreams` 是上游组全挂;而握手阶段的 404,先拿 `access.log` 对一下上游实际收到的 URI,比猜配置快得多。临时验证可以用 `proxy_pass` 加 `$request_uri` 打印,或者直接在后端打一条收到请求的路径日志。
核对清单
`proxy_http_version 1.1` 写了没有?没写 Upgrade 头到不了上游。
`Upgrade` 和 `Connection: upgrade` 两个头透传了没有?
`proxy_read_timeout` / `proxy_send_timeout` 是否大于心跳间隔的 3 倍以上?
`proxy_pass` 末尾的斜杠有没有吃掉本该透传的路径?
改完跑 `nginx -t` 再 reload,拿 curl 挂 90 秒验证空闲连接不断。
WebSocket 代理的坑基本都集中在“握手那一刻”和“空闲那几分钟”这两个时间点上。握手只看 101 是不够的,活过一个超时周期才算真的通了。
- 点赞
- 收藏
- 关注作者
评论(0)