为什么 Nginx 明明监听了 80,转发后端时却用了 4xxxx 端口?

为什么 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

这里的 400014000240003,就是系统给它分配的本地临时端口。

为什么必须这样?

因为如果你硬要它都用:

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 到后端这一段已经建好的出口连接。

相关推荐
JustHappy1 小时前
我汇总了身边朋友的经历才发现,其实第一份实习是最难找的......
前端·后端·面试
uhakadotcom2 小时前
在python 的 工程化架构中 ,什么是 薄包装器层?
后端·面试·github
用户1474853079746 小时前
CodeX使用Skill生成游戏美术和音乐资源,一分钟入门
后端
Melody1236 小时前
用 abort 中断 AI 流式请求,我之前做错了
后端
onething3656 小时前
Spring Boot + Spring AI 从入门到实战:7天转型计划 Day 5 —— SSE 流式输出 + 打字机效果
人工智能·后端·全栈
一个做软件开发的牛马7 小时前
MyBatis-Plus 从零实战:完整搭建可运行 Demo,BaseMapper 零 SQL、Wrapper 条件构造、分页插件与代码生成器详解
java·后端
码事漫谈7 小时前
AI 编程的「三体」架构:OpenSpec + Superpowers + GStack 如何让一个开发者撑起整个研发团队
后端
吃饱了得干活7 小时前
深入解析 OpenFeign:从重试、拦截到负载均衡的全维度实践
后端
onething3657 小时前
Spring Boot + Spring AI 从入门到实战:7天转型计划 Day 6 —— 业务完善 + 会话消息预览
人工智能·后端·全栈