在使用Nginx作为反向代理承载WebSocket业务时,Upgrade协议升级失败是最让人头疼的问题之一。客户端明明发起了一个标准的101 Switching Protocols握手,经过Nginx转发后却返回400、426甚至502,而普通HTTP请求一切正常。这类问题的根源在于HTTP/1.1的协议升级机制依赖两个关键的请求头,一旦代理层没有正确传递,整条链路就会断掉。本文将从协议原理、Nginx配置、回源日志分析三个层面,完整讲清Upgrade协议升级在Nginx中的处理方式。

一、Upgrade协议升级的工作原理
HTTP/1.1引入了Upgrade机制,允许客户端在同一个TCP连接上请求将协议切换到另一种协议,最典型的应用就是WebSocket。客户端在请求中携带Upgrade: websocket和Connection: Upgrade两个头部,服务端如果同意升级,会返回101状态码,之后双方在这个连接上不再使用HTTP语义,而是直接走WebSocket帧协议。
这里的关键在于Connection头。HTTP/1.1默认是长连接,但代理服务器在转发请求时,通常只把请求视为普通的HTTP事务处理。Connection头本身属于逐跳头部(hop-by-hop header),按照RFC 7230的约定,代理在转发前应该删除它,而Upgrade头必须与Connection: Upgrade同时出现才有意义。因此,如果Nginx没有显式配置,默认情况下这两个头部不会传递给上游服务器,上游收不到升级请求,自然只会当成普通GET处理,握手失败也就不可避免。
另外要注意回源协议版本。Nginx的proxy_pass默认使用HTTP/1.0与上游通信,而HTTP/1.0并不支持Upgrade机制和Keepalive。如果没有显式设置proxy_http_version 1.1,即使头部传递正确,上游也可能因为协议版本过低而拒绝升级,表现为返回426 Upgrade Required。这是排查升级失败时最容易忽略的一个点。
二、Nginx处理Upgrade的标准配置
Nginx官方推荐使用map指令配合$http_upgrade变量来动态设置转发头部。这样做的好处是,同一个server块既可以服务普通HTTP请求,也可以服务WebSocket请求,没有升级请求时Connection会被置为close,不影响普通代理行为。完整配置示例如下:
# 根据客户端是否携带Upgrade头,动态决定转发的Connection值
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
server_name ws.example.ipipp.com;
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 $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;
# WebSocket是长连接,必须放大读写超时
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
}
这段配置中有三个必须同时满足的条件:proxy_http_version 1.1保证回源使用HTTP/1.1,proxy_set_header Upgrade把客户端的升级请求透传给上游,proxy_set_header Connection告知上游这条连接要按升级语义处理。三者缺一不可,只配其中一两个是实践中最常见的错误。
超时配置同样重要。Nginx默认的proxy_read_timeout是60秒,如果WebSocket连接在60秒内没有任何数据交互,Nginx会主动断开连接,客户端表现为连接莫名掉线、需要反复重连。如果你的业务没有心跳机制,要么放大这个超时值,要么在应用层实现ping/pong心跳,二者取其一即可,当然更推荐后者,因为它还能顺便检测半死连接。
三、通过日志定位回源升级失败
当升级失败发生时,靠猜测往往效率低下,合理利用Nginx日志可以快速锁定问题在哪一跳。默认的combined日志不包含上游信息,需要自定义log_format,把上游地址、上游状态码、上游连接状态记录下来:
log_format ws_log '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'upstream=$upstream_addr '
'upstream_status=$upstream_status '
'upstream_connect_time=$upstream_connect_time '
'upstream_header_time=$upstream_header_time '
'request_time=$request_time '
'upgrade="$http_upgrade" conn="$http_connection"';
access_log /var/log/nginx/ws_access.log ws_log;
有了这份日志,排查思路就非常清晰了。如果日志中upgrade字段为空,说明客户端根本没发升级请求,问题在客户端或者中间的CDN、负载均衡层把头部剥掉了;如果upgrade有值但upstream_status是400,多半是Nginx没把头部传给上游,检查proxy_set_header配置是否生效;如果upstream_status是426,检查是否遗漏了proxy_http_version 1.1;如果upstream_status是502且upstream_connect_time为-,说明连上游都没连上,要检查回源地址和端口。
还有一种隐蔽的情况:升级成功了,但连接在一分钟后被掐断。此时日志里状态码是101,request_time却接近60的整数倍,基本可以断定是proxy_read_timeout或proxy_send_timeout超时导致。将日志中的时间特征与超时配置对照,可以快速验证结论。
四、多层代理链路下的注意事项
实际生产环境中,流量往往要经过CDN、外层Nginx、内层Nginx再到应用服务器,形成多层代理。Upgrade头部是逐跳的,意味着链路上的每一跳都必须单独配置升级转发,任何一跳丢失Connection: Upgrade都会导致失败。排查时应逐层查看日志,确认101状态码在哪一跳变成了400或426。
此外,如果链路中启用了CDN或WAF,要确认它们支持WebSocket透传。部分CDN默认将请求按普通HTTP缓存处理,不仅会剥掉Upgrade头,还可能对连接设置较短的超时。这类问题在Nginx侧的日志表现是正常的,但客户端视角连握手都无法完成,需要从客户端抓包入手反向定位断点。
总结来说,Nginx处理Upgrade协议升级的核心是三点:回源用HTTP/1.1、透传Upgrade和Connection头、为长连接设置合理的超时。配合包含上游状态的自定义日志,绝大多数升级失败都可以在几分钟内定位到具体环节,不必再靠盲改配置碰运气。