第一步:用 ss 看清连接状态分布
排查 TCP 问题,第一条命令永远是 `ss -s`,它直接给出各状态计数:ESTAB 有多少、TIME_WAIT 有多少、CLOSE_WAIT 有多少。TIME_WAIT 堆积和 CLOSE_WAIT 堆积是完全不同的故障,状态计数一秒就能区分,而两者的根因与处置相反。
`ss -tan state time-wait` 列出所有 TIME_WAIT 连接,`-t` TCP、`-a` 全部状态、`-n` 不解析服务名(重要:不加 `-n` 会触发一次反向 DNS 查询,在连接数上万时可能等上几十秒)。加 `| wc -l` 可以直接计数。
区分客户端与服务端身份很关键:TIME_WAIT 多在主动关闭方,CLOSE_WAIT 多在被动关闭方。如果 TIME_WAIT 集中在连出去的连接(`ss -tan state time-wait dst 10.0.0.1`),那是你本机作为客户端过于频繁地建连;如果是大量入站连接上的 TIME_WAIT,那是服务端的问题。同一个现象,出错位置完全不同。
按本地端口聚合能快速找出「谁在耗端口」:`ss -tan | awk '{print $4}' | sed 's/.*://' | sort | uniq -c | sort -rn | head`。对本地 Web 服务端口看到上万条连接,再结合 ESTAB 数量就能判断是否超过应用自身的 backlog 或文件描述符限制。
ss -s
ss -tan state time-wait | wc -l
ss -tan state close-wait | wc -l
ss -tan state time-wait dst 10.0.0.1 | wc -l # 判断方向端口耗尽:范围、保留与真正的原因
每个 TCP 连接都占用一个四元组,唯一性由「源 IP、源端口、目标 IP、目标端口」共同决定。所以对服务端来说,连接数上限受**目标端口与源 IP 组合**的乘积限制;对客户端主动发起连接来说,限制来自源 IP 与源端口的组合。这两个上限完全不同,混淆它们是排查方向出错的主要原因。
默认的临时端口范围通常是 32768–60999,约 28000 个。这个范围偏小:对于需要大量出站连接的场景(爬虫、连接池客户端、消息队列消费者、数据库连接池),单个源 IP 很快就会把可用源端口用尽。调整方式是 `net.ipv4.ip_local_port_range`,写进 `/etc/sysctl.d/99-network.conf` 并执行 `sysctl --system` 生效。
报 `Cannot assign requested address`(EADDRNOTAVAIL)时,第一反应应该是查这个参数,而不是查磁盘或 DNS。可以直接用 `sysctl net.ipv4.ip_local_port_range` 确认当前范围,并用 `ss -s` 看 TIME_WAIT 是否接近这个数量级——如果 TIME_WAIT 数量和端口范围接近同一量级,那就基本确认了。
还有一个常被忽略的保留段:`/etc/sysctl.d` 中 `net.ipv4.ip_local_reserved_ports` 可以预留一段端口给特权服务,避免重启时临时端口被瞬时占用冲突。它不减少可用端口,只是不让内核把它们分配出去。另外要注意目标端口范围受限的常见情况:某些云环境的出站目标端口只允许 443/80 等少数端口,这会在连接层表现为握手后立刻被重置。
sysctl net.ipv4.ip_local_port_range
sysctl -w net.ipv4.ip_local_port_range="10000 65000"
echo 'net.ipv4.ip_local_port_range = 10000 65000' > /etc/sysctl.d/99-network.conf
ss -s # 看 TIME_WAIT 是否逼近范围上限TIME_WAIT 到底是什么,该不该管
TIME_WAIT 是 TCP 协议设计的一部分,不是错误。任何主动关闭连接的一方,在发出最后一个 FIN 并收到 ACK 后,必须停留 2MSL(Linux 默认 60 秒)才能关闭,这个状态的存在是为了保证最后的 ACK 能重传,并让旧连接的延迟报文在网络中消散,避免污染新建的同四元组连接。
因此 TIME_WAIT 数量大本身不等于故障,关键看两点:一是**是否有出口带宽/吞吐损失**(TIME_WAIT 会占端口、占内存,但现代内核的开销已经很小);二是**是否导致源端口耗尽**。如果源端口充足,TIME_WAIT 到几十万的唯一成本是 `ss` 查询变慢和内核内存占用,通常不需要处理。
真正需要治理的是「主动关闭方是我们」这个模式。数据库连接池、HTTP 客户端这类组件如果每次用完就主动关闭,就会让本机堆积海量 TIME_WAIT。正确做法不是打开内核参数,而是改应用行为:让对端先关闭(协议层协商),或者用长连接 + 连接池,避免频繁建连。
缩短 `net.ipv4.tcp_fin_timeout` 曾经是流传很广的建议,但它**不作用于 TIME_WAIT**(只作用于孤儿 socket 的 FIN_WAIT2 回收),改小它并不会减少 TIME_WAIT 数量。这个误解每年都会浪费大量时间,遇到时可以直接排除了。
ss -tan state time-wait | wc -l
cat /proc/sys/net/ipv4/tcp_fin_timeout # 与 TIME_WAIT 数量无关
# 排查应用侧:确认是哪个组件频繁主动关闭连接backlog 与 somaxconn:三个常被混为一谈的队列
连接排队的三个层次要分清。第一层是服务端应用自己的 accept 队列,由 listen 调用的 backlog 参数决定;第二层是该 listen socket 的 `net.core.somaxconn` 上限——实际生效值是 `min(backlog, somaxconn)`;第三层是 SYN 队列,内核里由 `tcp_max_syn_backlog` 控制(现代内核里 SYN 队列实际按连接哈希桶组织,通常不构成瓶颈)。
所以最常见的误判是:应用写了 `listen(fd, 4096)` 就以为队列有 4096,实际如果 `somaxconn` 还是默认的 4096(或更小),上限就是两者较小值。临时端口冲突或 SYN 队列溢出时,客户端看到的现象是连接超时而不是明确的拒绝。
把 `somaxconn` 调大很容易,但要真正生效需要应用同时正确设置 backlog,并且现代 Linux 允许通过 `TCP_DEFER_ACCEPT` 延迟握手完成——服务器可以在收到 SYN 后立刻 accept 而不必等完整握手,这在短连接高并发场景下能明显降低资源占用。
还有一层常被忽略:即使队列不满,连接也可能因为 SYN flood 或半开连接堆积而失败。`net.ipv4.tcp_syncookies=1` 可以在 SYN 队列溢出时启用 SYN cookies,用编码在序列号里的状态代替服务器端队列存储,这是抵御 SYN flood 的标准手段,代价是丢失部分 TCP 选项。
sysctl net.core.somaxconn net.ipv4.tcp_max_syn_backlog net.ipv4.tcp_syncookies
ss -ltn # Send-Q 列即当前 backlog 上限,Recv-Q 为当前排队数
ss -ltn "sport = :8080"tcp_tw_reuse 的适用边界
`net.ipv4.tcp_tw_reuse=1` 允许内核在**出站连接**上复用处于 TIME_WAIT 的四元组,只对客户端方向生效,对服务端入站连接无效——因为服务端根本没有「新建连接」这个动作可言。启用后,客户端可以跳过 2MSL 等待,短连接客户端的源端口耗尽问题通常因此缓解。
边界条件必须讲清楚:它要求新的四元组在 TIME_WAIT 停留时间之后才可复用,安全性依赖于四元组不复用得太快,否则可能把旧连接的延迟报文当成新连接的一部分,理论上造成数据错配。实践中把 `tcp_tw_reuse` 与缩短后的 `tcp_fin_timeout` 一起改是危险组合,因为后者并不缩短 TIME_WAIT 停留时间,只是让人误以为缩短了。
还有一个版本差异:Linux 4.12 之后该参数的语义改为「仅在连接时间戳(TCP timestamps)可用的对端才复用」,这实际上让它在现代内核上更安全,因为时间戳充当了四元组代际标记。所以在新内核上开这个参数基本是安全的,但仍应先确认现象确实是源端口耗尽,而不是 CLOSE_WAIT 泄漏或文件描述符不足。
务必区分它与 `tcp_tw_recycle`——后者已在 Linux 4.12 被移除,因为会破坏中间设备对 NAT 后的连接做正确跟踪。它和 `tcp_tw_reuse` 名字相近但完全不是一回事,在任何现代系统上都不应该出现 recycle 相关参数。
sysctl net.ipv4.tcp_tw_reuse net.ipv4.tcp_tw_recycle
sysctl -w net.ipv4.tcp_tw_reuse=1
uname -r # 4.12+ 语义已变,recycle 已被移除
ss -tan state time-wait | wc -l # 生效后应逐步下降