Nginx 反向代理 WebSocket:握手 101 成功后秒断的三个配置坑

举报
yd_225649965 发表于 2026/11/13 01:04:28 2026/11/13
【摘要】 Nginx 反向代理 WebSocket:握手 101 成功后秒断的三个配置坑最近帮一个实时看板项目把 WebSocket 服务搬到 Nginx 后面,遇到一个很迷惑的现象:客户端握手返回 101 Switching Protocols,连接明明建立了,却在 60 秒左右准时断开,重连后又是 60 秒。查了三轮才发现,问题全在 Nginx 的默认配置里——握手成功只代表升级那一刻没错,不代表...

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 是不够的,活过一个超时周期才算真的通了。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。