四次挥手全过程
主动方(客户端) 被动方(服务端)
│ ① FIN, seq=u │
│ ────────────────────────────→ │ 被动方 CLOSE_WAIT:你 说完了,但我还有话
│ ② ACK, ack=u+1 │
│ ←──────────────────────────── │
│ │ (被动方继续发剩下的数据……)
│ ③ FIN, ACK, seq=w, ack=u+1 │
│ ←──────────────────────────── │ 被动方发完了,也 关
│ ④ ACK, ack=w+1 │
│ ────────────────────────────→ │
│ TIME_WAIT,等 2MSL │ 被动方 CLOSED
│ CLOSED │为什么挥手比握手多一次?因为 TCP 是全双工且允许半关闭:客户端发 FIN 只表示我没有数据要发了,但仍可以收;服务端可能还有数据在路上,要等它也发完、也发 FIN,两个方向才算都关了。握手时 SYN 和 ACK 能合并,挥手时中间那次 ACK 和 FIN 之间隔着服务端收尾的时间,合不了。
TIME_WAIT:主动方的 2MSL 修行
主动关闭方发出最后一个 ACK 后,不立刻关,要等 2MSL(报文最大生存时间的两倍,Linux 上约 60 秒)。等什么?两件事:
// 1. 兜底对方的 FIN 重传
// 若最后的 ACK 丢了,被动方会重发 FIN
// TIME_WAIT 状态还能回 ACK——一关就回 RST,对方懵
//
// 2. 让本连接的旧报文自然死亡
// 等 2MSL,这条连接的所有迟到包都活不过这个窗口
// 否则同样的四元组复用给新连接,旧数据串门就完了所以 TIME_WAIT 不是 bug,是可靠性换来的税。谁主动关连接,谁交这笔税。
几千个 TIME_WAIT 要不要治
先说结论:TIME_WAIT 多本身不可怕(它几乎不耗内存,占用的是端口和表项),真正要治的是它背后的短连接习惯。常见两招:
# 统计各状态连接数——排查第一步
$ ss -s
TCP: 8421 (estab 120, closed 0, orphaned 0, timewait 3900)
# 治标:开启 TIME_WAIT 复用(出方向连接可复用旧端口)
$ sysctl -w net.ipv4.tcp_tw_reuse=1 # 只对客户端(主动连接方)生效
# 治本:改成长连接——HTTP keepalive、数据库连接池
# 让连接建一次用一万次,TIME_WAIT 从根上消失警告:网上教程常让你开 tcp_tw_recycle,千万别开——它按源 IP 记时间戳,NAT 环境下多个用户共享一个出口 IP,时间戳乱序直接丢包,堪称经典事故配方(新内核已移除该参数)。
CLOSE_WAIT 才是真警报
分清两个状态的责任人:TIME_WAIT 堆在自己机器上,说明你是主动关闭方——通常是连接管理策略问题,可用连接池化解;CLOSE_WAIT 堆积则是自己代码的锅——对方已发 FIN 关闭,你的应用收到后一直不调 close(),八成是异常分支忘了关连接、线程池阻塞在业务逻辑上没走到 finally。线上看到几千 CLOSE_WAIT,直接查应用代码,不用怀疑网络。下一篇进入传输的精髓:滑动窗口——TCP 凭什么做到不丢、不重、不乱序。
评论 (0)