最近又收到一条凌晨三点的告警:某服务的连接失败率突然飙到 20%,登录后台看了下,ss -ant | grep TIME_WAIT | wc -l一刷就是 6 万多条。这种场景我遇到过不止一次,很多朋友第一反应就是“赶紧把tcp_tw_recycle打开”,其实这一步踩下去,后面往往会踩出更大的坑。
TIME_WAIT 这个状态本身是 TCP 四次挥手的正常产物,它保证了连接的可靠关闭。真正出问题的是它在高并发短连接场景下堆积过快,把本地端口、内存、连接表全部吃满,导致新连接无法建立。这篇内容我会从 TIME_WAIT 的产生机制、影响链路、排查命令、业务层调优、内核参数调优这几个维度,把原因和办法完整拆一遍。适合刚接手线上服务的小白,也适合正在被连接数告警折磨的运维和后端开发。
1. TIME_WAIT 到底是什么,为什么大家一听就头疼
1.1 从四次挥手说起:TIME_WAIT 是连接关闭路上的最后一个状态
TCP 连接关闭需要四次挥手。假设 A 主动关闭,A 发出 FIN,B 回复 ACK,然后 B 再发 FIN,A 再回复 ACK。注意最后这一步,A 发出最后一个 ACK 之后,并没有立刻进入 CLOSED,而是进入 TIME_WAIT 状态,并且要等待 2MSL(Maximum Segment Lifetime,报文最大生存时间)才会彻底关闭。
为什么要等这么久?两个原因:
- 万一最后一个 ACK 丢了,B 会重发 FIN,A 需要能再次响应 ACK。如果 A 直接关了,B 就会一直收不到确认,陷入反复重发。
- 防止旧连接中的延迟报文串扰到新连接。TCP 连接由四元组唯一标识,如果 A 立刻复用相同端口建立新连接,网络上可能还残留着旧连接的迟到报文,新连接就会收到脏数据。等待 2MSL 可以确保旧连接的所有报文都在网络中消失。
在 Linux 系统里,MSL 默认是 30 秒,所以 2MSL 就是 60 秒。也就是说,一个主动关闭的连接,会在 TIME_WAIT 状态里停留约 60 秒。
从方向上看,谁主动发 FIN,谁就进入 TIME_WAIT。这一点很多人搞反了,下面会反复提到。
1.2 大量 TIME_WAIT 会带来什么真实影响
很多人一看到 TIME_WAIT 数量多就慌,其实 TIME_WAIT 多本身不一定会出问题,问题在于它消耗了哪些资源。
首先,每个 TIME_WAIT 连接都占用一个本地端口(五元组中的本地 IP+端口)。Linux 默认的本地端口范围是 32768 到 60999,满打满算不到 3 万个。如果一个服务的并发短连接数超过了端口总数,并且旧连接还在 TIME_WAIT 里没释放,新的连接就无法申请到可用端口,表现就是Cannot assign requested address。
其次,TIME_WAIT 连接会占用内核内存。每个连接对应一个 socket 结构,几万条 TIME_WAIT 会吃掉可观的内存,虽然单个占用不大,但量大了之后影响不能忽视。
再者,如果服务器上同时跑着多个服务,某个服务把本地端口耗尽了,还会拖累其他需要端口的服务。连接表、内核软中断、文件描述符消耗都会相应上升。
一个简单的类比:TIME_WAIT 就像停车场里已经倒好车但还没开走的车。车不挪走,车位就空不出来。如果倒车离场的过程要保留 60 秒,高峰时段进来的车又特别多,停车场就会被这些“待离场”的车占满,新来的车只能在门口排队。
2. 别急着调参数,先搞清楚为什么会出现大量 TIME_WAIT
2.1 高并发短连接是最大的推手
高并发短连接是 TIME_WAIT 堆积的最常见场景。客户端发一个请求,服务端返回响应,然后任意一方主动断开连接,整个过程可能在几十毫秒内完成。
这类场景典型出现在:
- 客户端请求一个 HTTP 接口,服务端处理后主动断开;
- 内部服务之间用短连接进行 RPC 调用;
- 定时任务频繁连接数据库、Redis 等中间件;
- 反向代理与后端服务之间未开启长连接。
在秒级并发上千、每个连接生命周期只有几十毫秒的情况下,60 秒的 TIME_WAIT 等待期会不断累积连接,最终把端口和连接表撑满。注意:这里的“短连接”指的是应用层协议没有复用 TCP 连接,而不是说传输层有什么特殊机制。
2.2 服务端主动关闭连接也会导致 TIME_WAIT
很多人有个误区,认为只有客户端主动断开才会产生 TIME_WAIT。实际上,只要服务端主动向客户端发送 FIN,服务端就会进入 TIME_WAIT。
现实中服务端主动断开的场景非常多:
- 后端设置了空闲超时,比如 HTTP 请求处理完之后主动关闭连接;
- 应用层设置了
read timeout,超时后主动断开; - 负载均衡或反向代理配置了较短的空闲超时,自动断开闲置连接;
- 某些框架默认在每个请求结束后调用
close()。
如果服务端是一个高并发接入层,每处理完一个请求就主动断开,那么 TIME_WAIT 就会集中在服务端,直接耗尽服务端的本地端口。
2.3 客户端连接复用不足
有些场景下,TIME_WAIT 大量出现在客户端机器上。比如应用代码里每次操作数据库都新建连接、用完后 close,或者每次调用远程接口都 new 一个连接对象。
这种情况下,连接池配置缺失或连接池配置过小,导致大量短连接不断建立和销毁。客户端机器同样有本地端口限制,一旦端口耗尽,所有新请求都会被卡死,表现出的错误包括Connection timed out、Too many open files、Cannot assign requested address等。
连接复用不是简单地调大连接池就能解决,还需要考虑连接池的空闲回收策略和最大连接数,避免连接池本身成为瓶颈。
2.4 反向代理层和网关层会放大问题
互联网架构里一般都有接入层、网关层、应用层。每一层之间如果都是短连接,TIME_WAIT 就会被逐级放大。
举个例子,用户请求到达网关,网关转发到后端服务。后端返回响应后,如果网关主动断开与后端的连接,网关机器上就会出现 TIME_WAIT。如果客户端也频繁断开与网关的连接,客户端机器上也会堆积 TIME_WAIT。结果就是网关机器上同时存在大量与客户端、与后端之间的 TIME_WAIT 连接,端口压力倍增。
这种多层链路场景下,如果只优化某一层,效果往往不明显。需要从入口到出口统一规划连接策略:客户端到网关用长连接,网关到后端也用长连接,全链路尽可能减少连接建立和断开的频率。
3. 排查与确认:先用命令把现场看明白
3.1 用 ss 和 netstat 快速统计 TIME_WAIT 数量
遇到问题第一件事不是改参数,而是统计当前各种连接状态的数量,确认 TIME_WAIT 到底有多少,分布在哪些端口上。
ss -ant | awk '{print $1}' | sort | uniq -c | sort -rn # 输出示例 # 61023 TIME_WAIT # 286 ESTAB # 12 FIN_WAIT2 # 3 CLOSE_WAITss比netstat更适合大规模连接统计,因为netstat遍历/proc网络文件时在大连接数下会比较慢,而ss直接读取内核 socket 信息,速度更快。
如果只关心 TIME_WAIT 数量,可以单独统计:
ss -ant state time-wait | wc -l查看具体连接的源端口、目标地址和状态,用:
ss -ant state time-wait输出里能看到TIME-WAIT 0 0 192.168.1.10:53000 192.168.1.20:8080这样的信息,左边是本机 IP 和端口,右边是远端地址和端口。
3.2 分清 TIME_WAIT 在谁身上、和谁建连
确认 TIME_WAIT 出现在本机之后,下一步要搞清楚这些连接是哪些进程创建的,以及通信对端是谁。
通过ss -p可以查看进程信息,但 TIME_WAIT 状态的连接往往已经找不到对应的进程,因为发起关闭的进程可能已经 close 了 socket。这个时候可以使用ss -tanp | grep TIME_WAIT,如果进程还活着,能看到 PID 和进程名;如果进程已经退出,就看不到。
更实用的方法是分析端口范围。如果你看到本机源端口都在 32768~60999 范围内,且对端集中在某个服务端口,基本可以判断是本机服务作为客户端去请求对端时产生的大量短连接。比如本机是应用服务器,频繁请求数据库,那么对端端口可能就是 3306 或 6379。
如果看到 TIME_WAIT 连接的对端 IP 是终端用户出口 IP,且本机端口集中在 80/443,说明是本机作为服务端主动关闭了用户连接。
3.3 关键指标:本地端口范围、连接失败率、端口申请失败次数
排查时还需要看系统配置和失败日志,重点关注:
- 本地端口范围:
sysctl net.ipv4.ip_local_port_range,默认32768 60999 - TIME_WAIT 最大 buckets:
sysctl net.ipv4.tcp_max_tw_buckets - 连接失败日志:
dmesg | grep -i "time wait"或应用日志里的Cannot assign requested address - 端口申请失败统计:
netstat -s | grep "times the listen queue of a socket overflowed"这个不一定直接对应端口耗尽,但可以辅助判断
如果应用日志里已经出现Cannot assign requested address,说明本地端口已经满了,这是最明确的信号。此时再去调整应用层连接策略,往往已经来不及,需要先清理一部分 TIME_WAIT 连接,恢复可用性。
4. 解决办法:先优化业务和连接复用,再调内核参数
4.1 第一优先级:连接池与长连接改造
调内核参数属于“事后补救”,治本的方法是让 TIME_WAIT 不要那么快地产生。核心思路就一句话:尽量减少新建连接的频率,能复用就复用。
最直接的手段是连接池。不管是访问数据库、Redis、消息队列还是第三方 HTTP 接口,都应该使用连接池管理连接,而不是每次请求都新建连接。
以 Java 为例,数据库连接池一般配置最大连接数、最小空闲连接数、空闲超时时间,确保一批连接长期存活,反复使用。Redis 客户端也有连接池概念。HTTP 客户端比如 OkHttp、Apache HttpClient 需要配置连接池和 Keep-Alive。
这里有一个细节很多人忽略:连接池的最大连接数不能盲目设大。每个连接都要占用文件描述符和内存,连接池设置过大反而会造成资源浪费。一般根据业务并发量和单连接吞吐能力来估算,先压测得出基线,再根据监控调整。
对于内部服务之间的调用,建议使用支持长连接的 RPC 框架,默认保持连接不断开,这样最有效地减少 TIME_WAIT 产生。
4.2 前端接入层:调整 Keepalive 超时和 HTTP 版本
对于外部用户访问的 Web 服务,通过 HTTP Keep-Alive 可以让客户端与服务器之间的 TCP 连接在一段时间内保持复用,避免每个请求都重新建立连接。
Nginx 中有几个参数需要注意:
keepalive_timeout 65s; keepalive_requests 1000;keepalive_timeout是服务端保持空闲连接的时间上限,超过这个时间客户端没有新请求,服务端就会主动关闭连接。keepalive_requests限制一个连接最多处理多少个请求。
如果服务端和客户端之间不想维持太久的空闲连接,可以适当调低keepalive_timeout,但不要设置得太短。设置太短会导致连接反复建立,反而制造更多 TIME_WAIT。
对于 Nginx 与后端应用之间的连接,使用upstream时也需要开启 keepalive:
upstream backend { server 127.0.0.1:8080; keepalive 32; }这里的keepalive表示每个 worker 进程与后端服务器保持的空闲连接数。如果不配置,Nginx 与后端之间默认是短连接,每个请求都会新建 TCP 连接,TIME_WAIT 就会集中在 Nginx 服务器上。
另外,HTTP/2 协议天生支持多路复用,一个 TCP 连接可以并行跑多个请求,相比 HTTP/1.1 能显著减少连接数。如果你的服务支持,可以升级到 HTTP/2 作为额外优化手段。
4.3 内核参数:tcp_tw_reuse 与 tcp_tw_recycle 的真相
内核参数是很多文章讲得最多、也最容易讲错的部分。先说结论:
tcp_tw_recycle已经被 Linux 内核在 4.12 版本中移除,不要再配置这个参数。即使你用的是老版本内核,也建议不要开启,因为它会带来严重问题。tcp_tw_reuse是相对安全的内核参数,可以开启,但它必须依赖tcp_timestamps同时开启才生效。
tcp_tw_recycle的问题在于,它不止影响本机的 TIME_WAIT 连接,还会从内核层面回收还在 TIME_WAIT 状态的连接对应的资源,并且做时间戳校验。在 NAT 网络环境下,多台客户端机器共用一个出口 IP,它们的 TCP 时间戳差异较大,开启tcp_tw_recycle后,内核会丢弃“时间戳比预期晚到达”的 SYN 包,导致大量连接超时。这属于经典的“解决了 TIME_WAIT,引入了更大的故障”。
tcp_tw_reuse的含义是:当新的连接请求到来时,内核可以复用处于 TIME_WAIT 状态的连接,但前提是新的连接的时间戳大于之前连接的时间戳,这就保证了安全性。它不会主动清理 TIME_WAIT,而是允许“超车”。
配置方法:
vim /etc/sysctl.conf net.ipv4.tcp_timestamps = 1 net.ipv4.tcp_tw_reuse = 1 net.ipv4.tcp_fin_timeout = 30生效命令:
sysctl -ptcp_fin_timeout可以理解为 FIN_WAIT2 状态等待时间,调低一些能加速释放处于 FIN_WAIT2 状态的连接,但对 TIME_WAIT 本身的持续时间没有直接影响。TIME_WAIT 的持续时间由 2MSL 决定,Linux 里 MSL 是写死 30 秒,所以 TIME_WAIT 一般是 60 秒。
4.4 调整端口范围和定时器,缓解端口耗尽
如果连接池和长连接已经做了,但 TIME_WAIT 仍然偏多,可以考虑扩大本地端口范围。
sysctl -w net.ipv4.ip_local_port_range="1024 65535"注意,端口范围扩大会带来一个副作用:本地端口可能占用到一些知名端口之外的区间,某些服务监听时会冲突。实际使用中我一般会设为10240 65000左右,留出一些余量。
还有一个参数是tcp_max_tw_buckets。这个参数表示内核允许同时存在的 TIME_WAIT 连接的最大数量,超过这个值后,内核会以每秒约tcp_max_tw_buckets / 2的速度清除多余的 TIME_WAIT 连接。
sysctl -w net.ipv4.tcp_max_tw_buckets=100000这个参数要慎用。设置太小会导致正常的 TIME_WAIT 连接被强制销毁,可能造成连接被重置;设置太大又失去了限制意义。一般情况下,如果端口没有耗尽,不用刻意调这个值。
4.5 关于更激进的 SO_LINGER 和 RST 方案
网络上有人提到,可以让 socket 发送 RST 而不是 FIN 来直接关闭连接,避免进入 TIME_WAIT。这是通过设置SO_LINGER的l_onoff=1且l_linger=0实现的。这样关闭连接时直接发送 RST,连接会立刻释放,不进入 TIME_WAIT。
这个方案使用场景非常有限,因为它会破坏 TCP 的可靠关闭机制,可能导致对端无法正确处理数据,甚至造成数据丢失。一般只建议用在对可靠性要求极低的转发层,比如某些代理的 healthcheck。不建议在业务服务中使用这种方案。
5. 实战复盘:一次连接失败排查和调优过程
5.1 现象:某压测环境大量请求失败
某次压测中,我负责的一个模拟服务(代称 A 服务)在并发 2000 的短连接场景下,压测进行两分钟后,开始出现大量Cannot assign requested address报错。压测机上的应用日志疯狂刷错误,服务 QPS 骤降,但 CPU 和内存占用都不高,看起来不是资源瓶颈。
5.2 排查:定位到 TIME_WAIT 堆积和本地端口耗尽
登录压测机执行:
ss -ant | awk '{print $1}' | sort | uniq -c | sort -rn输出显示 TIME_WAIT 数量超过 28000,ESTAB 只有 200 左右。继续看:
ss -ant state time-wait | head -20发现所有 TIME_WAIT 连接的对端都是目标服务的 8080 端口,本机源端口集中在 35000-60000 区间。这基本确认是压测机作为客户端,每次请求都新建连接并主动关闭,导致本机端口被 TIME_WAIT 占满。
检查本地端口范围:
sysctl net.ipv4.ip_local_port_range返回值是32768 60999,总可用端口数约 28000 个,与 TIME_WAIT 数量吻合。结论明确:本地端口耗尽。
5.3 调优过程:从连接池到内核参数
第一轮优化:在压测客户端中启用 HTTP 连接池,把最大连接数调到 500,打开 Keep-Alive。压测再次运行,TIME_WAIT 数量从 28000 降到 3000 左右,连接失败消失。
第二轮优化:由于压测场景中客户端会模拟大量用户,连接池无法完全覆盖所有并发,仍然存在部分短连接。于是开启tcp_tw_reuse,允许新连接复用 TIME_WAIT 连接。修改 sysctl.conf 后使配置生效,观察压测曲线,TIME_WAIT 进一步下降,失败率降为 0。
第三轮优化:把本地端口范围适当扩大:
net.ipv4.ip_local_port_range = 10240 65000这样即使出现短连接风暴,端口容量也不会成为瓶颈。
5.4 效果与监控
调优后重新压测,稳定运行 30 分钟,TIME_WAIT 数量保持在 2000 以内,连接失败率为 0。监控方面,建议把 TIME_WAIT 数量、ESTAB 连接数、本地端口占用率统一接入监控和告警,一旦端口占用率超过 80% 就预警。
这个案例里最关键的一步不是改内核参数,而是先启用连接池。连接池解决的是“源头”,内核参数解决的是“缓冲”。两者配合才能达到最佳效果。
6. 常见问题速查与避坑清单
| 现象 | 可能原因 | 处理建议 |
|---|---|---|
| 服务端 TIME_WAIT 多 | 服务端主动关闭连接 | 开启 Keep-Alive、调长 keepalive_timeout |
| 客户端 TIME_WAIT 多 | 客户端大量短连接 | 使用连接池、复用连接 |
| 出现 Cannot assign requested address | 本地端口耗尽 | 扩大端口范围 + tcp_tw_reuse + 连接池 |
| 启用 tcp_tw_recycle 后连接超时 | NAT 环境时间戳问题 | 立即关闭该参数 |
| FIN_WAIT2 堆积 | 对端不关闭连接 | 检查对端应用逻辑,必要时调小 tcp_fin_timeout |
| CLOSE_WAIT 堆积 | 应用层没有调用 close | 检查代码中 socket 关闭逻辑 |
补充几条实操经验:
- 开启
tcp_tw_reuse之前一定要确认tcp_timestamps是开启状态,否则参数不生效。 - 不要在容器环境盲目修改
/proc/sys/net/ipv4/*,有些云环境的容器内核参数是只读的,修改会失败或者被忽略。 - 如果使用了负载均衡器(四层),注意负载均衡器后端与真实服务器之间也会产生连接,连接策略需要一起规划。
- TIME_WAIT 不等于故障。连接数在正常范围内波动时,完全不需要干预。只有连接建立失败、端口耗尽、内存明显增长时才需要处理。
- 优先从应用层解决问题,内核参数只是辅助手段。先把业务代码里频繁创建连接的逻辑找出来,效果立竿见影。
我自己在多次排查中的体会是,TIME_WAIT 问题表面上是内核参数问题,本质上是连接生命周期管理问题。90% 的情况靠连接池和 Keep-Alive 就能解决,剩下的再考虑内核参数。而且每次调内核参数都要做完整验证,避免出现tcp_tw_recycle那种“按下葫芦浮起瓢”的惨痛教训。
最后再分享一个小技巧:如果生产环境已经出现大量 TIME_WAIT 导致端口耗尽,想快速恢复可用性,可以临时调大tcp_max_tw_buckets并配合重启应用进程,让旧连接快速销毁。但这个操作只能救急,不能作为日常手段,治本还得靠长连接和连接池。