65535 卡住的是谁
这一篇是连接容量的第一道关卡:端口。它要回答一个常见的误解,“端口号最大 65535,所以一台服务器最多 65535 个连接”。读完后,你应该能说清 65535 到底限制了谁,以及什么情况下端口真的会被用完。
还是那个推送网关。它只监听 443 一个端口,十万个 App 都连这一个端口。如果连接数受端口号限制,这件事从一开始就做不到。
TCP 用四个值区分一条连接
内核区分连接,看的是四个值的组合,也叫四元组:
text
源 IP + 源端口 + 目标 IP + 目标端口对推送网关来说,所有连接的“目标 IP + 目标端口”都相同,就是网关自己的地址和 443。区分它们靠的是另一半:每个 App 所在设备的 IP,以及设备系统随手分配的一个临时端口。四个值里只要有一个不同,就是一条新连接。
图的左半边是服务端视角。三条连接都指向 :443,其中两条甚至来自同一个 IP,只是源端口不同。只算 IPv4,单个监听端口理论上能对应 2³² 个客户端地址乘以 2¹⁶ 个端口,约 2.8 × 10¹⁴ 种组合,远在后面几道关卡之前用不完。所以服务端的连接数从来不是被 65535 卡住的。
右半边是另一种情况:发起连接的一方。
发起方连同一个目标时,只剩源端口能变
当一台机器反复连接同一个“目标 IP + 端口”,而且只用一个出口 IP 时,四元组里三个值都固定了,只剩源端口能变。这个源端口由内核从临时端口池里自动挑选,池子的范围由 net.ipv4.ip_local_port_range 决定,Linux 常见默认是 32768–60999,约 2.8 万个。
这正是推送网关后面的情况。网关收到消息后要调用内部的订单服务,如果它作为反向代理、只用一个出口 IP 去连同一个上游地址,那么同时存在的上游连接最多约 2.8 万条。再往上,connect() 会失败,错误是 EADDRNOTAVAIL,日志里显示为 Cannot assign requested address。
把端口池缩到 10 个,亲眼看它用完
2.8 万个端口太多,不方便观察。下面的实验把容器里的临时端口范围缩到 10 个,然后对同一个目标一直建连,直到失败;再换一个目标端口重复一次。
把脚本保存为 ports.py,放在准备实验环境时建的目录里:
python
import socket
servers = [socket.create_server(("127.0.0.1", p)) for p in (9000, 9001)]
conns = []
for port in (9000, 9001):
ok = 0
while True:
try:
conns.append(socket.create_connection(("127.0.0.1", port)))
ok += 1
except OSError as e:
print(f"连 {port}:成功 {ok} 条,第 {ok + 1} 条失败:{e}")
break
ports = sorted({c.getsockname()[1] for c in conns})
print(f"共 {len(conns)} 条连接,用到的本地端口:{ports[0]}–{ports[-1]},{len(ports)} 个")--sysctl 只修改这个容器的端口范围:
bash
docker run --rm -i --sysctl net.ipv4.ip_local_port_range="50000 50009" \
capacity-lab python - < ports.py实际输出:
text
连 9000:成功 10 条,第 11 条失败:[Errno 99] Cannot assign requested address
连 9001:成功 10 条,第 11 条失败:[Errno 99] Cannot assign requested address
共 20 条连接,用到的本地端口:50000–50009,10 个两个现象都和四元组对得上。连 :9000 时只有源端口能变,10 个端口用完,第 11 条就失败。换成 :9001,目标端口不同,同样的 10 个本地端口又能各组成一条新连接,所以又成功了 10 条。20 条连接只用了 10 个本地端口。
如果命令报 invalid argument 或 not allowed,说明你的容器运行时不允许修改这个参数,可以改在一台 Linux 虚拟机里用 sudo sysctl -w 临时设置,做完再改回原值。
真实系统里端口为什么会提前用完
默认的 2.8 万个端口并不都能同时用于活跃连接。主动关闭连接的一方,会让这条连接在 TIME_WAIT 状态停留约 60 秒,期间这个四元组不能马上复用。一个每秒新建又关闭几百条短连接的代理,光是 TIME_WAIT 就能占掉上万个端口。TIME_WAIT 的作用见 四次挥手与 TIME_WAIT。
在真实机器上,先看端口池和正在等待回收的连接数:
bash
sysctl net.ipv4.ip_local_port_range
sysctl net.ipv4.ip_local_reserved_ports
ss -tan state time-wait | wc -l
ss -tan dst 10.0.0.5:8080 | wc -l最后一条把 10.0.0.5:8080 换成你的上游地址,统计连向它的连接数。这个数字接近两三万,又频繁出现 EADDRNOTAVAIL,基本就是端口池用完了。
解决方向都在四元组上:
- 复用连接。 用连接池或 HTTP keep-alive,让一条连接承载多次请求,这是最有效的办法。
- 增加变化的维度。 给发起方加出口 IP,或让上游提供多个地址,四元组空间按倍数扩大。
- 扩大端口池。 调大
ip_local_port_range能多出一些余量,但下限不要低到和本机服务监听的端口冲突。
这道关卡什么时候需要关心
对只接收连接的服务端,端口几乎不会成为瓶颈,看到连接数上不去,应该往后面几道关卡找原因。需要关心端口的,是主动发起大量连接的一方:反向代理、网关的上游连接、压测机、爬虫,以及在同一台机器上模拟大量客户端的测试脚本。
还有一个常见误判:在一台压测机上模拟十万个客户端去连服务器,压测机先报 Cannot assign requested address,于是得出“服务器只能撑两万多连接”的结论。其实卡住的是压测机自己的端口池,需要给压测机配多个源 IP,或者用多台压测机。
端口这一关对服务端基本是敞开的。连接被区分出来以后,应用还要把它“拿到手”,这就是第二道关卡:02 为什么一千多条就报 Too many open files。
