凌晨三点,你的手机响了。监控报警:服务不可用。你迷迷糊糊打开电脑,发现后端服务进程还在,端口也在监听,但就是连不上数据库。重启大法?试了,没用。流量切走?切了,还是不通。
最后发现,是服务器新建连接失败了。报错信息里有个你见过但从来没认真研究过的词:Cannot assign requested address。
恭喜你,遇到传说中大名鼎鼎的 TIME_WAIT 问题。
从一次真实故障说起
那是某个深夜,流量比平时高了30%。我们的MySQL连接池配置的是500个连接,看起来很够用。但突然之间,业务开始大量报错:Too many connections——MySQL明明还有连接数余量,连接池也没满,为什么会这样?
排查了一圈,最后用 netstat -s 看了系统统计,发现一个惊人的数字:
Passive connections rejected due to time wait: 48732
四万多个连接被系统扔进了 TIME_WAIT 队列,根本没有机会完成握手。
问题根源?我们有个接口会频繁创建到下游服务的短连接,每次请求结束主动关闭连接。流量一大,本地端口被 TIME_WAIT 占满,根本分不出新的端口来建连。
这不是MySQL的问题,不是连接池的问题,就是一个被所有人忽视的TCP基础知识的锅。
TIME_WAIT 到底是什么
先说结论:TIME_WAIT是TCP连接关闭时,主动关闭一方进入的一个等待状态,持续2MSL(Maximum Segment Lifetime)。
MSL是报文最大生存时间,一般是60秒。所以TIME_WAIT最少要等120秒。
为什么要有这个状态?很多人第一反应是"防止数据包迷路"——老生常谈,没错,但不完整。TIME_WAIT 存在的真实目的有两个:
第一,防止旧连接的延迟报文干扰新连接。
TCP是面向字节流的协议,连接由四元组标识。假设A和B建立了一个连接,关闭后马上又建了一个相同的四元组连接。如果没有TIME_WAIT的等待期,旧连接中某个迷路的迟到报文到达新连接,就会被错误地当成合法数据。这就是所谓的历史报文问题(Stale Segments)。
第二,确保对端收到了最后的ACK。
如果最后那个ACK丢了,对端会重传FIN。进入TIME_WAIT状态的一方必须能够重发这个ACK。如果没有这个等待期,对端重传的FIN会收到一个RST(端口已不可用),而不是正确的ACK,连接就无法优雅关闭。
所以TIME_WAIT是TCP协议层面的保护机制,不是操作系统闲得蛋疼故意卡你。
端口号枯竭——真正的噩梦
理解完原理,我们来看实际影响。
Linux上,一个TCP连接的本地端口范围是 net.ipv4.ip_local_port_range,默认通常是 32768 60999,也就是大约28000多个端口可用。
每个TCP连接,无论服务端还是客户端,都需要占用一个本地端口。服务端监听端口(如80、443)不占连接端口,那是特殊用途。但客户端发起连接时,内核会从空闲端口池里分配一个临时端口。
问题来了:如果你的服务大量创建短连接(请求完就关),每关闭一个连接,本地端口就进入TIME_WAIT状态,需要等待2MSL(通常60秒)才能被释放。这意味着在这60秒内,这个端口是不可用的。
高并发场景下,28000个端口根本不够用。新建连接时内核会报错:Cannot assign requested address——不是因为地址被占用,是因为没有可用端口了。
这就是开头那个故障的真相。
四招教你优雅应对TIME_WAIT
第一招:合理配置本地端口范围
临时调大端口范围能缓解问题,但治标不治本:
sysctl -w net.ipv4.ip_local_port_range="1024 65535"
不过65535是理论上限,而且太多端口被TIME_WAIT占着,对其他服务也有影响。
第二招:重用TIME_WAIT状态的连接(重点!)
Linux 3.7+支持 net.ipv4.tcp_tw_reuse,开启后,主动关闭的TIME_WAIT连接可以被新连接复用。复用的条件是:新的时间戳大于旧连接的时间戳,且满足一定时间窗口要求。
sysctl -w net.ipv4.tcp_tw_reuse=1
这个参数在大多数场景下是安全的(NAT环境除外,但服务端到服务端的短连接完全没问题)。强烈建议生产环境开启。
第三招:调整MSL时间(慎用)
可以通过修改 net.ipv4.tcp_fin_timeout 来缩短FIN_WAIT_2的等待时间,但MSL本身是网络层面的概念,不能直接改。不过可以间接影响TIME_WAIT的持续时间。
这个参数控制的是连接进入FIN_WAIT_2后多久自动关闭,不是TIME_WAIT的。
第四招:让客户端不要主动关闭连接(最推荐)
这是架构层面的解法。长连接才是王道。HTTP的Keep-Alive、gRPC的连接复用、数据库连接池——核心思想就一个:不要频繁创建和销毁连接。
如果你的服务确实需要频繁调用下游,而且下游支持HTTP/2或gRPC,那就用连接池+长连接,连接复用率上去了,TIME_WAIT自然就少了。
服务端也有TIME_WAIT问题?
有人问:"我是服务端,客户端主动关闭连接,我作为服务端进入TIME_WAIT,不也一样占端口吗?"
好问题。答案是:服务端的TIME_WAIT也会占端口,但通常影响较小。
因为服务端的监听端口是固定的,不参与四元组中的端口分配。新连接的本地端口是客户端的端口,服务端使用的是固定的监听端口+已建立的连接套接字,不受 ip_local_port_range 限制。
但如果你的服务端是主动发起连接的一方(比如服务A连接MySQL),那它就同样面临端口耗尽的问题。
所以正确的理解是:谁主动关闭,谁承担TIME_WAIT的代价。
一个小实验让你看清楚
用 ss -tan state time-wait | wc -l 可以查看当前TIME_WAIT连接数。
再配合 ss -tan state time-wait | head -20 看看具体是哪些连接在TIME_WAIT状态。
结合 netstat -ant 或者 lsof -i :8080,你就能清楚地看到连接的生命周期。
建议在自己机器上跑个压测,故意创建大量短连接,观察端口从生成到TIME_WAIT的全过程。看过一遍,你对这个问题的理解就从"听说过"变成"刻骨铭心"了。
说点扎心的
TIME_WAIT这个问题,在后端面试里是高频题,但实际工作中真正被它坑过的人并不多——因为大多数业务流量根本没大到触发端口枯竭。
但一旦碰上,就是凌晨三点的电话、群里的连环call、和"重启试试"的无力感。
很多后端工程师对TCP的理解止步于"三次握手四次挥手",稍微深入一点就不知道了。TIME_WAIT、滑动窗口、Nagle算法、糊涂窗口综合症……这些东西在日常CRUD里用不到,但碰到性能问题和高并发场景,就是硬桥硬马的真功夫。
所以建议:花半小时把TIME_WAIT搞清楚,下次遇到类似的故障,你就是那个"一眼看出问题"的人,而不是那个"重启了三次还没好"的人。
技术这东西,欠下的债,迟早要还的。而且通常是在你最不想还的时候还。