2557 字
约 8 分钟
22
S02-A14 · 端口耗尽:连接数、四元组与 TIME_WAIT

一个爬取任务跑到第十万次请求时开始报 Cannot assign requested address。机器负载很低,内存充足,带宽也只用了三成。真正用尽的是可用端口:每次发起连接要占用一个本地端口,而源 IP、源端口、目标 IP、目标端口构成一个四元组——四元组满了,新连接就建不出来。

这不是"连接太多",而是四元组的组合被用尽。理解这一点,才能分清三种看起来一样的报错:端口耗尽、文件描述符耗尽、连接池耗尽。


1. 三个"耗尽"的区别

报错/现象 真正耗尽的东西 典型数量级
Cannot assign requested address 可用本地端口 约 2.8 万到 6.4 万
Too many open files 进程文件描述符 由 ulimit -n 决定
连接等待超时、队列堆积 连接池容量 由池配置决定
Connection reset / 建连慢 对端 accept 队列 由 listen backlog 决定

第一种与第二种完全不同:端口是全局资源(按源 IP 计),描述符是进程资源。所以"加大 ulimit"能解决第二种,对第一种毫无帮助。

三种耗尽在报错文本上也可能混淆,因为它们最终都表现为"连接拿不到"。区分方法是看同一时刻的进程指标:端口耗尽时进程打开的描述符数不高、CPU 空闲;描述符耗尽时 lsof | wc -l 接近上限;池耗尽时池的等待队列有长度。

第三种最容易被误判成第一种:应用侧连接池满了,表现同样是"拿不到连接",但它发生在应用内部,与内核无关。

2. 四元组怎么被用尽

一个出站连接占用一个 (源IP, 源端口, 目标IP, 目标端口) 组合。可变的部分只有源端口。

条件 可用端口数
单个目标 IP + 单个目标端口 约 3.9 万(取决于内核参数)
N 个目标 IP(同目标端口) 约 3.9 万 × N
单个目标 IP + M 个目标端口 约 3.9 万 × M
多源 IP(多网卡/多出口) 按源 IP 数倍增

可以粗算一次:如果每秒新建 500 个连接、TIME_WAIT 保留 60 秒,那么稳态下被占用的端口约 3 万个——已经接近单个目标可用的上限。这个估算不需要工具,只需要两个数。

// 端口占用的稳态估算
占用端口数 ≈ 每秒新建连接数 × TIME_WAIT 秒数
例:500/s × 60s = 30000   → 接近上限,必须复用连接
    50/s  × 60s = 3000    → 安全

关键结论:可用组合数取决于"目标端有多少种"。所以访问单一服务时最容易耗尽——所有连接的目标 IP 与端口都相同,只有源端口可变。

// 看当前端口占用:哪个目标占得最多
ss -tan state all | awk 'NR>1 {print $5}' | cut -d: -f1 | sort | uniq -c | sort -rn | head
// 看 TIME_WAIT 的集中度(同一目标大量 TIME_WAIT 就是典型瓶颈)
ss -tan state time-wait | awk 'NR>1 {print $5}' | sort | uniq -c | sort -rn | head

3. TIME_WAIT 是不是元凶

TIME_WAIT 常被当成罪魁祸首,但它只是"端口被占用一段时间"的表现,不是根因。

状态 是否占用端口 持续时间
ESTABLISHED 是 连接存活期
FIN_WAIT / CLOSE_WAIT 是 关闭握手期间
TIME_WAIT 是 约 2 倍 MSL(典型 60 秒)
CLOSED 否 —

TIME_WAIT 的作用是保证最后那个 ACK 能被重传,以及防止旧连接的延迟报文被新连接误收。把它缩短或消除会引入别的风险,所以正确做法不是"清理 TIME_WAIT",而是让连接不要这么快地反复建立和关闭。

// 三组参数,各自管不同的事,不要混调
net.ipv4.ip_local_port_range = 10240 65000     // 扩大可用端口范围
net.ipv4.tcp_tw_reuse = 1                      // 允许复用 TIME_WAIT 于出站连接(仅客户端有效)
net.ipv4.tcp_max_tw_buckets = 20000            // 上限,超过则直接丢弃 TIME_WAIT
// 注意:tcp_tw_recycle 在新内核已移除,不要再用

4. 复用连接才是主要手段

端口耗尽的根因几乎总是"连接建立得太频繁",所以最有效的手段是复用。

手段 效果 前提
连接池(HTTP keep-alive) 端口占用与并发数成正比而非请求数 服务端支持长连接
连接池上限调优 控制并发上限 池要按目标分池
多出口 IP 倍增可用组合 需要网络配置
减少短连接(批量接口) 减少建连次数 服务端提供批量能力

复用还带来一个附带收益:避免建连风暴。每次新建连接都要 TCP 三次握手 + 可能的 TLS 握手,后者的开销是前者的数倍。在高峰期,这部分开销常常比业务处理本身还大。

第一项的效果最直接:一个连接可以承载成百上千次请求。很多"端口耗尽"的场景,实际是每个请求都新建了连接——常见于没配 keep-alive 的客户端、或者中间件强制关闭连接。

// 客户端连接复用的三个要点
var pool = new ConnectionPool(target)
    .maxTotal(200)                    // 总连接上限,按目标容量定
    .maxPerRoute(50)                  // 单目标上限,防单目标打满
    .keepAlive(Duration.ofSeconds(60))// 复用窗口
    .validateAfter(Duration.ofSeconds(30)); // 空闲校验,避免用到已被对端关闭的连接
// 关键:maxPerRoute × 目标数 应远小于端口上限,否则仍可能耗尽

5. 什么时候端口耗尽反而说明设计问题

症状 含义 改法
每秒建连数极高 请求粒度太细 合并请求或加批量接口
连接建立与关闭 1:1 未复用 配 keep-alive
单目标占满所有端口 单点依赖 加目标分散或缓存
多进程各自建连 进程级池无法共享 集中出口代理

这里有一个容易忽略的事实:端口是按源 IP 计的。宿主机上的多个容器共享同一个出口 IP 时,它们争的是同一批端口;而如果每个容器有自己的出口 IP(如某些 CNI 配置),可用组合数就按容器数倍增。这条差异决定了"多副本能不能缓解端口压力"。

第四项在容器化环境里很常见:十个副本各开 200 个连接,合计 2000 个出站连接指向同一目标。单个副本看起来"池没满",合计已经接近端口或对端上限。端口是按宿主机与源 IP 计算的,不是按进程。

6. 排错:五种"连不上但不是网络问题"

现象 成因 处理方向
Cannot assign requested address 端口耗尽 复用连接、扩端口范围、多出口
建连偶发超时 对端 accept 队列满 调大 backlog 或降建连速率
大量 CLOSE_WAIT 应用没关连接 检查代码中未关闭的响应流
Too many open files 描述符耗尽 调 ulimit 与池上限
长连接被中断 中间设备空闲超时 缩短空闲时间或加心跳
// 五种状态的分布,一眼看出问题在哪
ss -tan | awk 'NR>1 {print $1}' | sort | uniq -c | sort -rn
// TIME_WAIT 占绝大多数 → 建连过频,去复用连接
// CLOSE_WAIT 持续增长 → 应用没关连接,改代码
// SYN-SENT 堆积 → 对端不可达或丢包,查网络

第三项与端口耗尽方向相反:CLOSE_WAIT 堆积说明连接被对端关闭了,而本地没调用 close——这是应用缺陷,调内核参数无效。

7. 边界:什么情况不必关心端口

场景 为什么 做法
服务端(入站连接) 入站不占本地端口,只占描述符 关注 backlog 与描述符
长连接为主的服务 连接的建立频率极低 关注连接数与内存
有统一出口代理 端口由代理承担 关注代理侧容量
请求量小(每秒几十) 组合数远远够用 不必优化

还有一条与容量相关:端口耗尽往往先在某个副本上出现,然后扩散。因为分流不均时,某一个副本承担的建连数更高,它先打满端口,随后流量被重试推向其它副本,逐个打满。看到"一个副本报错、其它正常"时,不要以为是单机故障。

判断口径一句话:每秒新建多少连接。这个数字乘以 TIME_WAIT 时长,就是同时被占用的端口数;它接近端口范围时,问题一定会来。


8. 自测

  1. 端口耗尽、描述符耗尽、连接池耗尽分别耗尽的是什么?为什么"加大 ulimit"对第一种无效?
  2. 四元组里哪一部分可变?为什么访问单一服务时最容易耗尽?
  3. TIME_WAIT 占不占端口?它存在的目的是什么,为什么不该简单消除?
  4. 为什么"十个副本各 200 连接"可能比"一个副本 2000 连接"更危险?
  5. CLOSE_WAIT 堆积说明什么?为什么调内核参数无效?
  6. 用哪两个数字可以估算"端口是否会耗尽"?
S02-A14 · 端口耗尽:连接数、四元组与 TIME_WAIT
https://www.clxhxhhr.top/posts/6405/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。