为什么 Nginx 明明监听了 80,转发后端时却用了 4xxxx 端口?
我第一次认真看 Nginx 反向代理链路的时候,也被两个现象绕住过。
第一个现象是:
Nginx 明明监听的是:
text
0.0.0.0:80
但一到它转发后端时,抓到的连接却经常是这样:
text
client:50001 -> nginx:80
nginx:40001 -> backend:8080
我当时最直接的疑问就是:
既然 Nginx 都已经有 80 端口了,为什么它连后端时不用 80,而要再冒出一个 40001?
第二个现象是:
不开 upstream keepalive 时,压测一上来,代理机上很快就能看到大量连接创建和关闭;开了 keepalive 以后,这一段又明显稳定很多。
于是问题又变成:
连接池到底复用了什么?它到底是在复用客户端连接,还是在复用 Nginx 到后端的连接?
这两个问题看起来像 Nginx 配置问题,本质上其实是同一个问题:
反向代理不是一条连接直通,而是两段 TCP 连接。
一、Nginx 反向代理,不是一条连接穿过去
很多人脑子里会把反向代理想成这样:
text
client -> nginx -> backend
看起来像一条连接从客户端一路穿到后端。
但 TCP 层面不是这么工作的。
真实情况是两段连接:
text
client:50001 -> nginx:80
nginx:40001 -> backend:8080
第一段里:
- 客户端是客户端
- Nginx 是服务端
第二段里:
- Nginx 变成客户端
- backend 变成服务端
也就是说,Nginx 并不是把客户端那条 TCP 连接"递给后端"。
它做的是:
- 先接住客户端请求
- 解析 HTTP
- 再由自己建立到后端的连接
- 把请求转发过去
- 收到响应以后再回给客户端
这一步如果没想清楚,后面两个问题都会绕。
二、为什么前面监听了 80,后面连后端时却不能继续用 80?
因为监听 80 和连接后端,根本不是同一个 socket,也不是同一个角色。
当你写:
nginx
server {
listen 80;
}
这表示 Nginx 在本机监听 80,职责是:
text
等别人连进来
所以你会看到很多客户端都在连:
text
clientA:50001 -> nginx:80
clientB:50002 -> nginx:80
clientC:50003 -> nginx:80
这些连接可以同时存在,是因为每条 TCP 连接都由四元组唯一确定:
text
源 IP + 源端口 + 目标 IP + 目标端口
虽然它们的目标都是 nginx:80,但客户端 IP 或客户端端口不同,所以四元组不同。
但 Nginx 一旦去连后端,它的身份就变了。
这时候它不再是"等人连进来"的服务端,而是"主动发起连接"的客户端。
假设后端是:
text
backend:8080
那 Nginx 正常会建立这样的连接:
text
nginx:40001 -> backend:8080
nginx:40002 -> backend:8080
nginx:40003 -> backend:8080
这里的 40001、40002、40003,就是系统给它分配的本地临时端口。
为什么必须这样?
因为如果你硬要它都用:
text
nginx:80 -> backend:8080
那么多条连接的四元组就会完全一样:
text
nginx_ip:80 -> backend_ip:8080
而 TCP 不允许多条完全相同四元组的连接同时存在。
所以关键不是"80 不能连后端"。
真正的关键是:
同一个本地 IP:端口,连接同一个远端 IP:端口,不能同时形成很多条完全相同的 TCP 连接。
三、临时端口不是多余的,它是 Nginx 作为客户端时的出口身份
很多人会把这个问题理解成:
我不是已经绑定了 80 吗?为什么不能拿这个端口继续出去连?
这句话里真正混淆的,是两种完全不同的职责。
监听端口的职责是:
text
接收入站连接
而临时端口的职责是:
text
标识这次出站连接的本地身份
所以更准确的理解不是:
text
我已经有 80 端口了,所以出去也该用 80
而是:
text
80 是入口
临时端口是出口身份
前者回答的是:
text
谁能连我
后者回答的是:
text
我连别人时,我是谁
这两个问题,在内核里本来就是两套不同的 socket。
四、这样再看 keepalive,问题就顺了
一旦你接受了"反向代理其实是两段连接",那么 upstream keepalive 的复用对象就很清楚了。
它复用的不是:
text
client -> nginx
这一段。
它复用的是:
text
nginx -> backend
这一段 TCP 连接。
如果没有 keepalive,那么很多请求都会反复经历:
text
建立 TCP 连接
发送 HTTP 请求
接收 HTTP 响应
关闭 TCP 连接
也就是:
text
connect
send request
read response
close
请求一多,Nginx 到后端这一段就会不断:
- 申请本地临时端口
- 建立到 backend 的连接
- 关闭连接
- 进入 TIME_WAIT
- 再申请新的临时端口
所以不开复用时,你看到的很多连接开销,其实不一定是后端业务代码本身慢,而是:
Nginx 到 upstream 这一段,在反复建连和关连。
五、upstream keepalive 到底保留了什么?
它保留的是:
已经建立好、暂时空闲、后面还可以继续拿来用的 upstream TCP 连接。
也就是说:
不用 keepalive 时:
text
请求 1 -> 新建连接 -> 关闭
请求 2 -> 新建连接 -> 关闭
请求 3 -> 新建连接 -> 关闭
用了 keepalive 时:
text
请求 1 -> 新建连接 -> 保留
请求 2 -> 复用连接
请求 3 -> 复用连接
所以它省掉的不是"请求处理逻辑"。
它省掉的是这几个成本:
- TCP 三次握手
- 频繁分配本地临时端口
- TIME_WAIT 堆积
- 内核维护大量短连接的成本
- Nginx 到 backend 这一段反复建连的开销
六、一个常见误解:keepalive 32 不是最大并发 32
很多人看到这段配置:
nginx
upstream backend {
server 127.0.0.1:8080;
keepalive 32;
}
server {
listen 80;
location / {
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_pass http://backend;
}
}
会下意识觉得:
text
keepalive 32 = 最多只能有 32 条连接
这不对。
它更接近的意思是:
每个 worker 进程,最多缓存 32 条空闲的、可复用的 upstream 连接。
注意这里的关键词是:
text
空闲
可复用
upstream 连接
不是总连接数上限,也不是最大并发请求数上限。
如果有 4 个 worker,那么理论上最多可能保留:
text
4 * 32 = 128
条空闲 upstream keepalive 连接。
七、为什么这个问题一到压测就特别明显?
因为压测会把连接生命周期的问题放大。
平时请求量不大时,即使你每次都新建:
text
nginx -> backend
这段连接,也不一定马上出事。
但压测一上来,Nginx 就会不断重复:
text
申请临时端口
connect
转发请求
close
TIME_WAIT
再来一轮
这时候你看到的"代理层压力变大",很多时候不是一个抽象的说法。
它背后真的对应着:
- 更多握手
- 更多短连接
- 更多 TIME_WAIT
- 更多内核连接管理开销
所以有些性能问题,表面上看像"后端扛不住",实际上真正先抖的地方,可能是:
text
Nginx -> backend
这一段连接管理。
最后一句
Nginx 监听 80,只说明它在入口处是服务端;它转发后端时,自己又变成了客户端,所以需要用本地临时端口建立另一段 TCP 连接。upstream keepalive 复用的,也不是客户端连接,而是 Nginx 到后端这一段已经建好的出口连接。