服务器 TIME_WAIT 端口耗尽排查实战:ss 定量、tcp_tw_reuse 与连接池复用治本三招

502 不一定是后端挂了,可能是一个端口不够用

流量上来之后,网站开始零星报 502,或者某些客户端提示连接被拒绝、连不上。你去查后端服务——好好的,进程在、CPU 也不高、日志里干干净净。你重启一下,好了几分钟又犯。这种时候,很多人会往「内存」「并发数」「后端超时」上找,却忽略了一个更底层的东西:主动发起连接的一方,能用的本地端口快用完了。

这不是玄学。TCP 连接由四元组唯一标识:源 IP、源端口、目的 IP、目的端口。当你的服务器作为客户端去连上游(比如 Nginx 反代到 PHP-FPM、应用连 MySQL、爬虫抓取外部接口),每建一条连接,内核就要从本地端口范围里分配一个临时端口(ephemeral port)。而临时端口总数是有限的——默认也就两三万个。更要命的是,连接关闭后,主动关闭的那一方会进入 TIME_WAIT 状态,把那个端口「按住」几十秒(通常是 2 倍的 MSL,Linux 上约 60 秒)。高并发短连接场景下,端口就这么被一批批占着,分配不出来新的,新连接直接失败——于是你看到 502、connection refused、Cannot assign requested address。

先学会量:看清端口到底被谁占着

判断是不是端口耗尽,最快的命令是 ss 的汇总模式:

ss -s

它会给出一个总览,重点看两行:

  • TCP 的总数,以及括号里 timewait 的数量。如果 timewait 数长期维持在一两万以上,而总连接数也居高不下,就是警报。
  • 其他状态分布。TIME_WAIT 之外,还要关注 CLOSE_WAIT 是否堆积——那通常意味着应用没正确关闭连接,是另一类问题。

更精确地看 TIME_WAIT 到底连向哪里:

# 统计 TIME_WAIT 按「远端 addr:port」分组的数量,从多到少
ss -tan state time-wait | awk 'NR>1 {print $5}' \
  | sed 's/:[0-9]*$//' | sort | uniq -c | sort -rn | head

如果某一两个上游 IP 占了绝大多数的 TIME_WAIT,说明正是往它们发起的短连接在制造端口压力。再对照系统可用端口范围:

cat /proc/sys/net/ipv4/ip_local_port_range
# 典型输出:32768    60999

这个范围决定了「作为客户端可用」的端口总数,默认约 28,000 个。如果上面统计出的 TIME_WAIT 数已经逼近这个量级,基本可以确诊。

第一招:让 TIME_WAIT 的端口可以被安全复用

针对端口耗尽的经典调参是 tcp_tw_reuse。要真正理解它,得先想清楚 TIME_WAIT 为什么存在——它不是设计缺陷,而是为了两件事:一是保证最后那个 ACK 能重传,二是让旧连接的残余报文在网络里过期,不污染新连接。所以「优化」它需要谨慎。

net.ipv4.tcp_tw_reuse 的作用是:允许主动发起连接的一方,在新的 connect() 时复用处于 TIME_WAIT 的端口,前提是时间戳(timestamps)能保证新旧的区分,且该 TIME_WAIT 已经存在超过 1 秒。它的默认值是 1 或 2(视内核版本和发行版)。查看当前值:

sysctl net.ipv4.tcp_tw_reuse
sysctl net.ipv4.tcp_timestamps

tcp_tw_reuse 要生效,通常要求 tcp_timestamps 打开(默认开)。设置为 1 开启:

sysctl -w net.ipv4.tcp_tw_reuse=1

这里有个非常重要的历史教训:不要用 net.ipv4.tcp_tw_recycle。这个参数在很多老教程里和 reuse 一起出现,它试图快速回收 TIME_WAIT,但会粗暴地丢弃时间戳倒退的报文——在 NAT 环境下(大量客户端共享一个公网 IP,时间戳不同步),它会导致大量正常连接被莫名丢弃,表现为「部分用户间歇性访问不了」。这个参数早已在内核中被移除,任何还推荐它的教程都该被淘汰。端口优化的正确答案是 tcp_tw_reuse(仅对主动连接方有效),而不是 recycle。

第二招:扩大可用端口范围

如果业务流量确实大,光靠复用还不够,需要从源头增加可用端口数量。编辑 /etc/sysctl.d/99-network-tuning.conf:

net.ipv4.ip_local_port_range = 10000 65000

注意两个数字之间的关系:左值是起始,右值是结束。ip_local_port_range 的左右其实和「绑定端口」相互配合——如果应用本身要监听 10000 以上的端口,就要把范围起点调高,避免和监听端口冲突。一般 Web 服务器监听的端口(80、443、3306 之类)都在低位,把它们排除在范围之外即可。写成 10000 65000 能提供约 5.5 万个端口,比默认多出近一倍。

改完不能直接生效,要触发内核读取(也可以用 sysctl --system):

sysctl -p /etc/sysctl.d/99-network-tuning.conf

这里只提一句:ip_local_port_range 影响的是作为客户端发起连接时能用的端口。它不会影响本机监听的服务端口,也不会解决「作为服务端被连接打满」的问题——那是另一回事。

第三招:从根上把短连接变成连接复用

前面两招是在「端口层」缓解,但更好的解法是让短连接不再那么短——也就是连接池和 keep-alive。很大程度上,端口耗尽不是端口不够,而是你浪费了端口:每条请求都新建连接、用完立刻关闭,端口全堆在 TIME_WAIT 里。

以最常见的 Nginx 反向代理上游为例,配置上游连接复用只需几行:

upstream backend {
    server 127.0.0.1:9000;
    keepalive 32;          # 每个 worker 保持的连接数
    keepalive_requests 1000;   # 单条连接最多复用多少次
    keepalive_timeout 60s;     # 空闲连接存活时间
}

server {
    location / {
        proxy_pass http://backend;
        proxy_http_version 1.1;          # 关键:必须 1.1 才支持 keepalive
        proxy_set_header Connection "";  # 关键:清掉客户端的 close,才能复用
    }
}

这三行里有两个「没有它就不生效」的坑——proxy_http_version 1.1 和 proxy_set_header Connection ""。HTTP/1.0 默认是短连接,不升到 1.1,keepalive 无从谈起;而如果不清空 Connection 头,客户端发来的 Connection: close 会原样透传给上游,连接照样一条请求就关。很多人配了 upstream keepalive 却没效果,就是栽在第二行。

应用的连接池同理。PHP 里用 PDO 的持久连接、Python 用连接池、Go 用 http.Client 并正确配置 MaxIdleConnsPerHost,都能从源头减少「建连—断连」的频率。真正把连接复用做好之后,你会发现 TIME_WAIT 数量断崖式下降,端口压力自然消失。

把配置固化,别让它跑丢

sysctl -w 的改动重启即失效。规范做法是写进配置文件并让 systemd 加载。除了前面的 /etc/sysctl.d/99-network-tuning.conf,还要注意有些发行版里 /etc/sysctl.conf 仍会覆盖 sysctl.d,得确认没有冲突项。一个完整的网络调优片段:

# /etc/sysctl.d/99-network-tuning.conf
net.ipv4.ip_local_port_range = 10000 65000
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_max_tw_buckets = 262144
net.core.somaxconn = 32768
net.ipv4.tcp_max_syn_backlog = 8192

逐个解释这样配的理由:tcp_fin_timeout 缩到 15 秒能加快 FIN_WAIT2 的回收(它影响的是等待对端 FIN 的超时,不是 TIME_WAIT 本身);tcp_max_tw_buckets 是 TIME_WAIT 的全局上限,超了内核会直接杀连接并打警告,调大它能留出缓冲;somaxconn 和 tcp_max_syn_backlog 增大连接队列,配合高并发场景,否则会出现「监听队列溢出」导致的丢包和重传。

应用配置同理固化。Nginx 的 upstream keepalive 写进 conf.d/ 下的站点配置,重载使配置生效:

nginx -t && systemctl reload nginx

养成 nginx -t 先验证再 reload 的习惯——直接在 reload 时才报错,可能造成服务中断。

排障清单:从现象到结论

把这套排查做成固定动作,遇到「连不上」就不慌了:

  • 第一步永远先量:ss -s 看 timewait 总数,ss -tan state time-wait | wc -l 得到精确值。数字是判断一切的前提,不要凭感觉调参。
  • 确认是不是本地端口耗尽:如果是本机作为客户端连不上上游,且 ss -s 的 timewait 逼近 ip_local_port_range 的容量,基本确诊。
  • 如果 timewait 不多但依然连不上:方向要转。可能是 upstream 监听队列满了(看 netstat -s | grep -i listen 里的溢出计数),或者是防火墙/conntrack 表满(看 nf_conntrack_count 对比 max),也可能是 Cannot assign requested address 之外的真实资源不足。
  • 如果本机是服务端,被大量连接打:这跟本地端口耗尽无关,要看 somaxconn、accept 队列、以及 backeb 的处理能力。别把两件事混为一谈。
  • 调参顺序:先做连接复用(治本,收益最大),再开 tcp_tw_reuse(治标,代价低),最后才扩大端口范围(兜底)。反过来先扩端口,只会让问题来得更晚、更隐蔽。

小结

TIME_WAIT 端口耗尽这类问题最迷人也最坑人的地方在于——它披着「后端故障」的外衣。502、连接被拒、间歇性失败,看起来都像应用层的问题,但根子在内核的端口分配上。记住三步定性:先用 ss -s 和数据说话,确认是不是端口被占满;优先用连接池和 upstream keepalive 把短连接变成长连接,从源头减少端口消耗;再用 tcp_tw_reuse 和扩大端口范围做兜底。最后避坑一句:永远不要碰 tcp_tw_recycle,它在 NAT 环境里造成的伤害远大于收益。把量测做在前头,把复用做在日常,端口耗尽这种「午夜惊魂」就不会再找上门。

Last modification:October 1st, 2026 at 07:26 pm

Leave a Comment