四次挥手:TIME_WAIT 是朋友不是敌人
文章目录
引言
连接建立有三次握手,连接关闭有四次挥手。握手讲的是"怎么把路修好",挥手讲的是"怎么把路安全拆掉"——这比握手复杂得多。
三次握手只需要双方确认对方能收到自己的消息,四次挥手则要处理一个更棘手的问题:双方各自都可能还有数据没发完。一方发完数据说"我要关了",另一方可能还要继续发送数据,等那边也发完后才能真正断开。这中间的任何一步没处理好,连接就没法干净利落地关闭,残留的状态会占用端口、消耗资源,甚至在重启时引发"端口仍被占用"的报错。
这篇文章把挥手过程中的每个状态、每个参数、每个常见问题全部讲透。
一、四次挥手完整时序
先看一张完整的时序图,把四次挥手的每一步标清楚:
每一步的状态变化:
| 阶段 | 主动关闭方 | 被动关闭方 | 说明 |
|---|---|---|---|
| 应用层调用 close() | — | — | 触发关闭流程 |
| 第一次挥手 | → FIN, 进入 FIN_WAIT_1 | 收到 FIN | 数据已发完,请求关闭 |
| 第二次挥手 | 收到 ACK, 进入 FIN_WAIT_2 | → ACK, 进入 CLOSE_WAIT | 确认收到,进入半关闭 |
| 第三次挥手 | 等待对端 FIN | → FIN, 进入 LAST_ACK | 对端数据也发完了 |
| 第四次挥手 | → ACK, 进入 TIME_WAIT | 收到 ACK, 进入 CLOSED | 确认收到,计时开始 |
| 计时结束 | 进入 CLOSED | — | 连接完全关闭 |
为什么是四次而不是三次?
因为 TCP 是全双工协议。两个方向的数据流是独立的,一方发送 FIN 只是说"我这边的数据发完了",但另一方向的数据可能还没发完。所以需要两对 FIN/ACK:先关闭发送方向(第一、二次挥手),再关闭接收方向(第三、四次挥手)。
二、两个TIME_WAIT:FIN_WAIT_2 与 TIME_WAIT 的区别
这是挥手过程中最容易混淆的两个状态。
FIN_WAIT_2 出现在主动关闭端。主动端发送 FIN 并收到对端 ACK 后,进入 FIN_WAIT_2 状态,此时主动端已经不能发送数据,但仍然可以接收数据。主动端需要收到对端的 FIN 后才会进入 TIME_WAIT。
关键问题在这里:如果对端一直不发送那个 FIN(比如对端应用层卡住、没有调用 close()、或者网络丢包),主动端会永久卡在 FIN_WAIT_2。Linux 内核通过 net.ipv4.tcp_fin_timeout 控制 FIN_WAIT_2 的超时时间(默认 60 秒),超时后强制关闭连接。这个值不能设太短,否则对端稍有延迟就会导致连接被提前断开;但也不能设太长,否则大量 FIN_WAIT_2 连接会消耗内核内存。
TIME_WAIT 出现在主动关闭端收到对端的 FIN 并发送 ACK 之后,持续时间为 2MSL(Linux 默认 60 秒 × 2 = 120 秒)。TIME_WAIT 的存在有两个目的:
- 防止旧连接的延迟数据包被新连接错误接收。 2MSL 是数据包在网络中可能存活的最大时间,确保旧连接的所有数据包都从网络中消失后,同一个四元组(源IP、源端口、目标IP、目标端口)的新连接才建立。
- 确保被动关闭方收到最后的 ACK。 如果最后的 ACK 丢失,被动端会重传 FIN,主动端需要在这个重传窗口内仍然在线,以便重新发送 ACK。
2MSL 为什么是 120 秒?
MSL(Maximum Segment Lifetime)是一个数据包在网络中可以存活的最大时间,由 RFC 793 规定不超过 2 分钟。Linux 将 MSL 实现为 60 秒,因此 TIME_WAIT = 2 × MSL = 120 秒。这个值在 Linux 中不可直接配置,但可以通过调整 net.ipv4.tcp_fin_timeout 间接影响 FIN_WAIT_2 的行为。
net.ipv4.ip_default_ttl(默认 64)是 IP 层的 TTL,和 MSL 是两个完全不同的概念。TTL 控制的是数据包经过路由器的最大跳数,每经过一个路由器减 1,到 0 就丢弃;MSL 是 TCP 层的概念,衡量的是时间维度而不是跳数。
三、TIME_WAIT 堆积:什么时候是问题,什么时候不是
TIME_WAIT 堆积几乎是每个高并发服务器都会遇到的现象。大量文章会直接说"开启 tcp_tw_reuse",但这个建议在很多场景下并不适用。
TIME_WAIT 在以下场景下是正常现象,不需要处理:
高并发短连接场景下,每次请求建立新连接然后立即关闭,主动端必然进入 TIME_WAIT。端口范围够用(默认 32768 ~ 60999,约 28000 个端口),2MSL = 120 秒内理论最大 TIME_WAIT 数量约 28000 × (120 / 平均连接时长)。如果平均连接时长是 1 秒,每秒 28000 个新连接,TIME_WAIT 数量约为 280 万——看起来很多,但内核的 TIME_WAIT 状态占用的内存极小(每个约几百字节),通常不构成实质问题。
TIME_WAIT 在以下场景下是真正的问题:
客户端使用固定源端口访问大量不同目标服务器。比如爬虫或 API 客户端,每个请求使用不同目标 IP 但相同源端口(客户端通常随机选择源端口,但端口范围有限),当 TIME_WAIT 连接占满可用端口范围时,新连接会因为"无可用端口"而失败。这种情况下 tcp_tw_reuse 确实有效。
服务器端监听端口(bind(0.0.0.0:80))收到连接后,TIME_WAIT 状态在本端——这时即使开启 tcp_tw_reuse,对服务器端的 TIME_WAIT 也完全无效。因为 tcp_tw_reuse 只对从本机主动发起的连接生效,对被动接收的连接无效。
真正的端口耗尽信号:
不是 TIME_WAIT 数量本身,而是 ss -s 输出中 ress(重用状态的数量)接近 0 且 outa(出站连接失败)上升,或者 netstat -ant 看到大量 TIME-WAIT 且 Local Address 的端口号接近 60999 上限。这说明可用端口已经不够用了。
四、tcp_tw_reuse 的有效条件
net.ipv4.tcp_tw_reuse = 1 不是万能药。内核在以下条件下才会将新的出站连接重用到 TIME_WAIT 状态的 socket:
- 本机作为客户端,主动连接外部服务器
net.ipv4.tcp_timestamps = 1(默认开启)——时间戳是 PAWS 机制的基础,也是 tw_reuse 的前提- 新连接的发出时间距离 TIME_WAIT 开始时间超过 1 秒
- 目标地址是非本地地址(经过 NAT 或公网)
对于服务器端监听端口产生的 TIME_WAIT,tw_reuse 无效。真正的解决方案是:
- SO_REUSEADDR:允许 bind 到处于 TIME_WAIT 状态的地址+端口组合。服务器重启时如果端口仍被占用,开启这个选项后可以立即重新监听,而不必等待 2MSL 结束。
- 设置合理的 SO_LINGER:调用
setsockopt(sockfd, SOL_SOCKET, SO_LINGER, ...)控制 close() 行为。 linger timeout = 0 时,close() 直接发送 RST 而不是走完四次挥手,适用于对数据完整性要求不高的场景,但会丢失尚未确认的数据。 - 调整
net.ipv4.tcp_max_tw_buckets:限制 TIME_WAIT 状态的最大数量,超过后强制关闭最早的 TIME_WAIT 连接。默认值足够大(262144),不建议随意调小。
五、SO_REUSEADDR 与 SO_REUSEPORT 的精确区别
这两个选项名字相近,职责完全不同:
SO_REUSEADDR 解决的是"服务器重启后端口仍被占用"的问题。关闭时服务器进入 TIME_WAIT,120 秒内无法重新 bind 同一端口。开启 SO_REUSEADDR 后,内核允许 bind 到处于 TIME_WAIT 状态的地址+端口。这个选项在高可用服务重启场景(Keepalived 主备切换、滚动更新)下必须开启。
// 服务器端标准配置
int sock = socket(AF_INET, SOCK_STREAM, 0);
int opt = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
bind(sock, (struct sockaddr *)&addr, sizeof(addr));
listen(sock, 128);
SO_REUSEPORT 是 Linux 3.9 引入的负载均衡机制。允许多个进程(或多个线程/多个 socket)绑定到相同的地址+端口,内核根据五元组哈希(源IP、源端口、目标IP、目标端口、协议)将新连接均匀分发到各个 socket。这个机制解决了"单进程 accept() 成为瓶颈"的问题。
# Nginx 多 worker 场景下的 reuseport 配置
stream {
upstream backend {
server 127.0.0.1:8080;
}
server {
listen 443 reuseport;
proxy_pass backend;
}
}
reuseport 开启后,每个 Nginx worker 都有自己的 socket,内核负责分发连接,不再需要 accept() mutex 锁。多核场景下性能提升显著。需要注意的是,reuseport 依赖的内核负载均衡策略在哈希冲突严重时可能不均匀,可以通过 ss -ltnp 观察各 socket 的连接分布来验证。
六、挥手故障诊断
连接关闭过程中的异常排查:
七、知识框架
总结
四次挥手比三次握手更复杂,原因在于双方的数据流是独立的,每个方向都需要单独确认关闭。FIN_WAIT_2 和 TIME_WAIT 是两个完全不同的状态,前者是等待对端 FIN 的过渡期,后者是确保旧数据包消失的安全等待期。
TIME_WAIT 堆积不一定需要处理——它在高并发短连接场景下是正常现象,只有当它导致了端口耗尽才是真正的问题。tcp_tw_reuse 只对客户端出方向 NAT 场景有效,对服务器端监听端口的 TIME_WAIT 无效。真正解决服务器端 TIME_WAIT 问题的方式是 SO_REUSEADDR 和合理的 linger 配置。
下一篇文章聚焦 DNS 解析——为什么有时候域名能 ping 通但访问不到,dig +trace 能告诉你 DNS 解析卡在了哪一层的哪一台服务器。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)