服务器入坑指北:TIME_WAIT 多不等于出故障

举报
Tulpartner 发表于 2026/10/20 08:12:55 2026/10/20
【摘要】 几百条 TIME-WAIT 不等于故障。它是正常连接状态,只出现在主动关闭方;计数高不是判据,资源是否真耗尽才是。本文给出一条固定顺序:先判谁先关连接,再判临时端口池等是否见底,优先在应用层用长连接、keep-alive、连接池从源头减少短连接,最后才动内核参数;并澄清 60 秒不可调、tcp_fin_timeout 管的是 FIN_WAIT_2、tcp_tw_recycle 已移除等常见误解。

一条命令看过去,ss 里几百条 TIME-WAIT,多数人的第一反应是"出问题了",接着去搜"TIME_WAIT 优化",照着一篇老教程改几个内核参数,重启后一看——计数没降多少,问题还在。更糟的是,改的东西可能压根不在这一侧生效。
这件事的坑在于:TIME_WAIT 是一种正常的连接状态,不是故障;它只出现在主动关闭的那一方,所以"谁先关连接"决定了你该改哪一侧;而计数本身也不是判据——真正的判据是有没有耗尽。角色判错、判据取错,后面所有动作都会做在错误的地方。
这篇的顺序是:先判角色 → 再判是否真耗尽 → 应用层先改 → 最后才轮到内核参数,中间配一张判定表、一组只读取证命令和一张验证清单。文中的数值都是示例,参数请以本机现读为准。

先判角色:TIME_WAIT 属于先发 FIN 的那一侧

TCP 关闭连接要四次挥手。先发出 FIN 的那一方在完成挥手后会进入 TIME_WAIT,停留 2×MSL 之后才彻底释放(主干内核里这个时长是一个固定常量,约 60 秒)。它在等两件事:万一对方没收到最后一个 ACK,还能重传;以及让这条连接的旧报文在网络里彻底消失,不至于串到之后复用的同一个四元组上。
机制决定了推论:服务端出现大量 TIME_WAIT,通常意味着服务端是主动关闭方。常见成因就三类——用的是每请求一关的短连接(HTTP/1.0 式行为)、没有启用或没协商成 keep-alive、连接池开得太小导致连接被频繁新建又关掉。
判错角色的代价很直接:如果服务端是主动关闭方,你在服务端去改那个"给主动发起方用"的参数,对本地这一侧的堆积毫无作用;反过来,如果只有一端对外发请求,堆积就只可能出现在那一端。所以第一步永远是同一句话:看清楚是谁先关的。

计数高不等于出故障:真判据是有没有耗尽

先记住一件事:ss -tan state time-wait | wc -l 的输出本身不是问题。这个数字只说明"有多少条连接停在收尾阶段",它既不消耗监听槽位,也不等于资源紧张。真正值得关注的是三个具体资源:连接表槽位、内核为 socket 分配的内存、以及额外的 CPU 开销——而这三样是否吃紧,要看有没有实际错误,而不是看计数。
于是判据分成两类,取决于角色:

  • 主动发起连接的一方:风险是临时端口池耗尽。每个出站连接要从本地临时端口范围里占一个端口,而这个端口在连接进入 TIME_WAIT 后的约 60 秒内不能复用(同一个四元组不能被新连接重复使用)。默认临时端口范围是 32768–60999(约 2.8 万个端口);配合固定的 60 秒,对单一目的地址的短连接速率就存在一个硬上限,量级在数百次每秒。超过了就会分配不到本地端口,应用侧报出"不能分配请求的地址"。
  • 监听中的服务端:TIME_WAIT 不占用监听队列,入站连接被拒与它无关。如果你看到的是"外部连不进来",该查的是 backlog、资源限制、上游链路,而不是这一堆状态。
    把这些合成一张表,动手前先对号入座:
    | 你看到的现象 | 判断 | 下一步 |
    | — | — | — |
    | 计数高,但没有报错、服务正常 | 正常状态 | 不动参数;只在容量规划时留意趋势 |
    | 对外连接报"不能分配请求的地址" | 真耗尽:主动方临时端口池被占满 | 先做连接复用,再评估端口范围 |
    | 入站连接被拒或超时 | 与 TIME_WAIT 无关 | 转查 backlog、资源限制、上游 |
    | 服务端的 TIME_WAIT 集中在短连接上 | 连接管理问题,不是内核问题 | 改应用:keep-alive / 连接池 |

60 秒不可调,以及两个最常见的误解

那个约 60 秒的时长在主干内核里是编译期常量。历史上有人提议把它做成可调项,被拒绝了——理由很朴素:TIME_WAIT 状态本身是好事,它保护的是连接的可靠收尾与报文的静默时间。所以:

  • 看到"把 TIME_WAIT 改短"的参数名,先确认它在你当前内核里是否存在,而不是照抄教程。非主干内核的私有参数不具普遍性,不能当成通用做法写进任何一台机器。
  • tcp_fin_timeout 管的是 FIN_WAIT_2,不影响 TIME_WAIT。这是最常见的误解:它的名字里有 fin,但控制的是"等对方 FIN 的等待时长",跟收尾阶段的 60 秒不是一回事。改它之后 TIME_WAIT 不降,属于预期之内。
  • 把 TIME_WAIT 调短会破坏 TCP 的"静默时间":这段时间的存在是为了让旧连接的迟到报文彻底消失;缩短之后,可能出现旧报文被当成新数据、或者合法的新报文被当成重复而丢弃——问题会从"状态多"变成"偶发数据错乱",而且很难查。

已经过时的那个参数:tcp_tw_recycle

如果你照着某篇老教程准备打开 net.ipv4.tcp_tw_recycle,可以直接停下来了:它已经从内核 4.12 中移除。更早之前,随着时间戳随机化的引入,它就已经失效;在 NAT 环境下它还会造成静默丢包——包被悄悄丢掉,日志里什么都不留。
所以这条可以当成一条反向边界用:任何让你开 tcp_tw_recycle 的教程,直接判定为过时,整篇的其余结论也不必再看。

只帮主动发起方的那个参数:tcp_tw_reuse

net.ipv4.tcp_tw_reuse 的作用是:在协议层面判定安全的前提下,允许复用仍处于 TIME_WAIT 的套接字去建立新的出站连接。注意"出站"这两个字——它只帮主动发起连接的一方,对入站连接没有任何帮助,也不会减少监听侧的状态数量。
内核文档对它的态度是"非专家勿改"。取舍是这样的:如果你这台机器的主要负载就是大量短连接对外请求(例如爬取、批量调用外部接口),它确实能缓解端口池压力;如果负载是被人访问的服务端,改它等于用错方向。
真要用,也要按"先现读、留回滚"的方式做,而不是先跑一下看看:

# 先现读当前值(只读)
sysctl net.ipv4.tcp_tw_reuse net.ipv4.ip_local_port_range net.ipv4.tcp_fin_timeout
# 若确有必要再改:先记下原值,写好回滚,再落到 /etc/sysctl.d/ 下
# sysctl -w net.ipv4.tcp_tw_reuse=1                    # 临时生效,重启丢失
# echo 'net.ipv4.tcp_tw_reuse = 1' > /etc/sysctl.d/99-tw.conf   # 持久化
# 回滚:删掉该文件,sysctl --system 使配置重新加载

只读取证命令组

下面这些都不改变系统状态,先看清再动手:

ss -Htan state time-wait | wc -l                                        # 总计数(不是判据,是起点)
ss -Htan state time-wait | awk '{print $5}' | cut -d: -f1 | sort | uniq -c | sort -rn | head   # 按对端聚合,找热点
ss -Htan state time-wait | awk '{print $4}' | cut -d: -f2 | sort | uniq -c | sort -rn | head   # 按本地端口聚合,看端口池水位
ss -s                                                                    # 概览:各类状态数量与占用
sysctl net.ipv4.ip_local_port_range net.ipv4.tcp_tw_reuse net.ipv4.tcp_fin_timeout            # 现读相关参数
nstat -az 2>/dev/null | grep -i -E 'tw|timewait'                        # 内核统计(装了 iproute2 才可用)

判据:按对端聚合的结果告诉你"在跟谁短连接",按本地端口聚合的结果告诉你"端口池用得有多满";这两条比总数有用得多。nstat 缺失时跳过即可,前五条已经足够完成判定。

修复顺序:应用层先改,参数最后

顺序反了就是白折腾。正确顺序是四步:

  1. 判角色:谁先关的连接(上一节)。
  2. 判是否真耗尽:有没有实际的报错,端口池有没有见底。
  3. 应用层先改:这是收益最大的一步。长连接、keep-alive、连接池——它们的作用是从源头减少"主动关闭的短连接数量"。TIME_WAIT 的条数正比于一段时间内被主动关闭的连接数,业务量不变而连接被复用,新建连接数下降,TIME_WAIT 自然跟着下降。这一步不需要动内核。
  4. 最后才是内核参数,而且只改有依据的那一项:主动方的临时端口范围,或(仅当负载确实是大批出站短连接时)tcp_tw_reuse。
    关于端口范围,有一个必须一并评估的副作用:扩大 ip_local_port_range 会同时增加连接跟踪表与安全策略的负担——端口多了,同一时间可能存在的连接数上限也随之提高,conntrack 表要更大,防火墙放行面也要重新确认。所以它是"评估后可选",不是"默认要做"。
    任何参数改动都按同一个模板走:先现读原值 → 写清回滚方式 → 再落到配置文件,不要在没有回滚的情况下改生产机。

改完怎么验证:每项一条判据

验证方式按"改了什么、看什么"来组织,别只看状态计数:

  • 连接复用是否生效:在一段固定时间里观察新建连接数(应用侧连接计数,或 ss -s 的概览变化)——复用生效时,同样的请求量对应的新建连接数应明显下降。
  • 报错是否消失:应用日志里"不能分配请求的地址"不再出现,且是在原来的高峰时段没有再出现。
  • 端口池水位:按本地端口聚合的分布去看,落在端口范围上沿的高密度是否缓解;如果在高峰期仍能看到分配失败,说明端口范围或复用策略还不够。
  • 服务端短连接比例:服务端的 TIME_WAIT 数量与请求量之比下降,才说明"从源头治理"真的生效了。
  • 参数现值:sysctl 现读确认改动已生效(而不是只写进了文件没加载)。
  • 回滚可行:按你记下的原值能一条命令恢复——这条也要验,否则等于没有回滚。

这篇不解决什么

  • 不写防火墙规则:讲规则设计的那篇(NFTables 防御)在 netfilter 层;这篇在 TCP 状态机与端口池层,两者不是一回事。
  • 不碰拥塞控制:讲内核网络参数的那篇(从连上到跑通)讲的是拥塞控制一类的参数;这篇只在需要时借用它的一条方法——参数要现读生效值。
  • 不讨论应用框架怎么配 keep-alive:不同语言、不同框架的写法差异太大,这里只讲"该往哪个方向改"。

踩坑清单

  • 去开 tcp_tw_recycle:它已从内核 4.12 移除,教程里出现它就该整篇作废。
  • 把 tcp_fin_timeout 当成 TIME_WAIT 超时去调:它管的是 FIN_WAIT_2,改完计数不会降。
  • 只看计数就动手:计数高不是判据,有没有耗尽才是。
  • 在服务端开 tcp_tw_reuse 期待入站改善:它只帮主动发起连接的一方。
  • 用"立刻发 RST"的强制关闭来消灭状态:破坏可靠关闭,可能丢数据,问题只是从看得见变成看不见。
  • 把 TIME_WAIT 归因成内存不足:它是连接状态,不是内存指标;资源是否吃紧要看实际错误。
  • 把约 60 秒说成可调:主干内核里它是固定常量,提案被拒是因为这个状态本身有价值。
  • 改端口范围不评估连接跟踪与防火墙策略:端口上限一变,两者的压力与放行面都跟着变。
  • 忽略回环与 NAT 场景的差异:同一台机器上经 NAT 与不经 NAT 的连接,四元组与端口占用情况不同,结论不能直接照搬。
  • 服务端短连接堆积却一直在内核参数里找答案:源头在连接管理,不在参数。
  • 改完不复测,凭"感觉好点了"收工:没有在原来的高峰时段复现原操作,等于没验证。
  • 改参数不留回滚:先把原值记下来,再动手。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。