TOC
1. 故障分析
最近公司某微服务集群中的一个HTTP服务突然出现大量连接失败,业务方反馈接口频繁超时、部分请求直接报错:
java.net.BindException: Address already in use: connect
进一步排查,发现该服务为高并发短连接场景(未启用 HTTP Keep-Alive),每秒新建和关闭大量 TCP 连接。通过 ss -ano | grep TIME-WAIT | wc -l
统计,TIME_WAIT 状态的连接数高达两万余个,远超正常水平。这是典型的由于本地端口被 TIME_WAIT 占用,新建连接频繁失败,导致业务异常。
TIME_WAIT 是 TCP 四次挥手关闭连接过程中,是主动关闭方进入的一个状态。在这个状态下的连接依然占据端口,过多的 TIME_WAIT 状态连接会导致本地端口资源耗尽,影响新连接的建立。通常来说,我们使用的都是长链接,没有遇到过这种问题,趁这个案例便一起来分析下。
2. TIME_WAIT 状态介绍
众所周知,TCP是面向连接的协议,建立连接和关闭连接都需要经过一系列的协商过程。
建立一个连接需要3次握手,而终止一个连接要经过4次挥手。这由TCP的半关闭(half-close)造成的。既然一个TCP连接是全双工(即数据在两个方向上能同时传递),此每个方向必须单独地进行关闭。这原则就是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向连接。 – 《TCP/IP详解-卷1: 18.2.4 连接终止协议》
具体来说,TCP面向连接意味着通信双方通过4元组(源IP地址/源端口号/目标IP地址/目标端口号/协议类型TCP)来标示一个连接。由于端口资源的有限性和可回收性,因此在确认连接不再使用后,应该有一个合理的机制来释放端口资源。
因此便有了针对两次FIN的两次ACK步骤。两次ACK完成之后,主动发起关闭连接的一方便可以释放端口资源了。但!不是马上释放,而是进入 TIME_WAIT状态,等待一段时间后(默认持续 2*MSL,Maximum Segment Lifetime,报文最大生存时间,一般为 2 分钟)再彻底关闭连接。
客户端 or 主动关闭方? 很多聊到TCP的文章都把主动关闭连接的一方称为客户端,实际上并不一定。TCP是面向连接的协议,任何一方都可以主动发起关闭连接的请求。因此主动关闭连接的一方可以是客户端,也可以是服务端。但在实际应用中,通常是客户端主动发起关闭连接请求(如浏览器关闭页面、telnet客户端键入quit退出),因此很多文章习惯性地将主动关闭方称为客户端。但是更准确的说法是“主动关闭连接的一方”,而不是简单地称为客户端。
如下图完整的4次挥手过程:
现在我们知道了TIME_WAIT状态导致端口资源被占用的原因。但是为什么要有TIME_WAIT状态呢?这主要是为了保证连接的可靠性和数据完整性。 在TCP连接关闭后,可能会有一些延迟的数据包仍然在网络中传输。如果没有TIME_WAIT状态,这些延迟的数据包可能会被错误地当作新的连接请求处理,从而导致数据混乱或丢失。TIME_WAIT状态可以确保在连接关闭后的一段时间内,所有延迟的数据包都被处理完毕,从而避免这种问题。
原理讲解到此为止,下面我们通过几个小实验来观察 TIME_WAIT 状态的实际表现。
3. 故障重现
假设有两台主机:Server(服务端)和 Client(客户端)。下面详细介绍如何在 Server 上快速搭建一个简单的 HTTP 服务,并在 Client 上模拟大量短连接请求,重现 TIME_WAIT 激增的现象。
3.1 Server 端搭建 HTTP 服务
可以使用 Python 的内置 HTTP 服务器快速启动一个简单的服务。在 Server 节点启动 HTTP 服务(监听 8080 端口):
python3 -m http.server 8080
该命令会在当前目录下启动一个简单的 HTTP 文件服务,适合用于测试短连接场景。
3.2 Client 端模拟大量短连接
使用 ab 工具发起大量短连接请求(默认每次请求新建连接):
ab -n 100000 -c 100 http://localhost:8080/
其中-n
指定总请求数,-c
指定并发数。可以根据实际情况调整这两个参数。
在 Server 节点观察 TIME_WAIT 数量:
netstat -an | grep TIME_WAIT | wc -l
3.3 现象观察
- 在压测过程中,Server 端 TIME_WAIT 状态的连接数会迅速增加。
- 持续压测后,出现新连接失败、端口耗尽等问题。
通过上述步骤,可以在受控环境下稳定复现大量 TIME_WAIT 的问题,为后续参数优化和方案验证提供基础。
4. 故障解决
针对大量 TIME_WAIT 问题,可以从以下几个方面优化:
(1)内核参数调整
- 服务端扩大本地端口范围:
sysctl -w net.ipv4.ip_local_port_range="1024 65000"
- 服务端缩短 TIME_WAIT 持续时间(默认60s):
# 临时修改(重启后失效) sysctl -w net.ipv4.tcp_fin_timeout=30 # 查看当前值 sysctl net.ipv4.tcp_fin_timeout
- 增加文件描述符限制,避免 FD 耗尽
关于
net.ipv4.tcp_tw_reuse
参数:该参数允许处于 TIME_WAIT 状态的 socket 被新连接复用。根据 Linux 官方文档: “Permits sockets in the ‘time-wait’ state to be reused for new connections. In high traffic environments, sockets are created and destroyed at very high rates. This parameter, when set, allows ‘no longer needed’ and ‘about to be destroyed’ sockets to be used for new connections. When enabled, this parameter can bypass the allocation and initialization overhead normally associated with socket creation saving CPU cycles, system load and time”
简单来说,开启后可以在高并发短连接场景下(无论客户端还是服务端,只要本机产生大量 TIME_WAIT),提升端口利用率,减少 TIME_WAIT 导致的端口耗尽和系统负载。
在本案例中,Server 端作为主动关闭连接的一方,也会产生大量 TIME_WAIT连接。开启该参数后,Server 端本地的 TIME_WAIT 端口可以更快被复用,有助于缓解端口耗尽问题,但无法彻底消除 TIME_WAIT 的影响。
注意:
- 该参数对“监听端口”无效,只对本地临时端口(主动发起连接或主动关闭连接时产生的 TIME_WAIT)有效。
- 在高并发短连接的服务端(如本案例),开启 tcp_tw_reuse 可以一定程度上缓解端口耗尽,但根本解决还需配合应用层优化(如 Keep-Alive、连接池)和架构调整。
(2)应用层优化
- 启用 HTTP Keep-Alive,减少短连接创建
- 数据库访问使用连接池,避免频繁新建连接
- 优化爬虫、批量任务等高频短连接场景
(3)架构层面
- 对于极端高并发短连接业务,建议从协议层或架构层重构,减少短连接产生
5. 总结
TIME_WAIT 是 TCP 协议为保证连接可靠性而设计的机制,但在高并发短连接场景下,容易引发端口耗尽、连接失败等问题。排查时应关注日志、端口状态、FD 使用率等指标。解决方案应结合内核参数优化与应用层设计,根本上减少短连接的产生。对于极端场景,建议从业务架构和协议层面进行优化,而非单纯依赖系统参数调整。