3. TCP四次挥手优化:解决TIME_WAIT堆积

TCP连接关闭时的四次挥手,说白了就是双方互相说「再见」的过程。但这里面有个坑——TIME_WAIT状态。我见过不少线上事故,都是因为这个状态堆积太多,导致端口耗尽、新连接建不起来。

为什么会这样?咱们先看挥手流程,再聊怎么优化。

3.1 四次挥手,到底在挥什么?

主动关闭方发送 FIN,被动方回复 ACK,然后被动方再发 FIN,主动方最后回复 ACK。主动方发出最后一个 ACK 后,会进入 TIME_WAIT 状态,默认等 2 个 MSL(Maximum Segment Lifetime,报文最大生存时间)。

这个等待是为了什么?

  • 保证被动关闭方收到最后的 ACK
  • 让网络中残留的报文自然消亡

但问题来了——TIME_WAIT 期间,这个四元组(源IP、源端口、目标IP、目标端口)不能被重用。高并发短连接场景下,TIME_WAIT 会迅速堆积,端口被占满,新连接直接报错。

核心矛盾:TIME_WAIT 是 TCP 可靠性的保障,但也是性能的杀手。

我记得有一次,线上一个短连接服务突然大量报 Cannot assign requested address。一查,TIME_WAIT 数量超过 6 万,本地端口全被占满了。嗯,这就是典型的 TIME_WAIT 堆积。

3.2 三个内核参数,逐个拆解

Linux 提供了几个参数来应对 TIME_WAIT 堆积。我一个个说清楚。

3.2.1 tcp_fin_timeout:缩短等待时间

这个参数控制的是 FIN_WAIT_2 状态的超时时间,不是 TIME_WAIT。但很多人搞混了。

FIN_WAIT_2 是主动关闭方收到被动方的 ACK 后、等待被动方发 FIN 的状态。如果被动方一直不发 FIN,主动方就会卡在这里。默认 60 秒,太长了。

我建议调小到 15-30 秒:

# 查看当前值
cat /proc/sys/net/ipv4/tcp_fin_timeout

# 临时修改
echo 15 > /proc/sys/net/ipv4/tcp_fin_timeout

# 永久修改(写入 /etc/sysctl.conf)
net.ipv4.tcp_fin_timeout = 15

适用场景:服务端主动关闭连接,且客户端可能异常断开(比如移动端网络不稳定)。

3.2.2 tcp_tw_reuse:安全重用 TIME_WAIT 连接

这个参数允许客户端(注意,是发起连接的一端)重用处于 TIME_WAIT 状态的连接,前提是新的 SYN 报文的序列号比旧连接的最后序列号大。

说白了,就是让内核在新建连接时,优先从 TIME_WAIT 池里挑一个可用的。

# 启用
echo 1 > /proc/sys/net/ipv4/tcp_tw_reuse

# 配合 tcp_timestamps 必须开启
echo 1 > /proc/sys/net/ipv4/tcp_timestamps

注意:tcp_tw_reuse 只对出站连接有效。如果你的服务是反向代理(Nginx、HAProxy)主动连接后端,这个参数很有用。但如果是服务端被动接受连接,它帮不上忙。

我曾经在一个 API 网关节点上启用 tcp_tw_reuse,TIME_WAIT 从 4 万降到了 2000 以下。效果立竿见影。

3.2.3 tcp_tw_recycle:已废弃,别用了

这个参数曾经很火,但现在强烈不建议使用。它在 Linux 4.12 内核中已经被移除。

为什么?因为它依赖 TCP 时间戳,而且对 NAT 环境有严重副作用。NAT 后面的多个客户端共享同一个公网 IP,时间戳可能不同步,导致服务端误判,直接丢弃合法 SYN 包。

我踩过这个坑。有一次线上反馈部分用户连不上服务,查了半天,发现是 tcp_tw_recycle 开启后,NAT 用户的 SYN 包被服务端当成「旧包」丢弃了。关掉后恢复正常。

一句话总结:tcp_tw_recycle 已死,有事烧纸。别碰它。

3.3 参数对比与选型

参数 作用对象 生效方向 风险 推荐度
tcp_fin_timeout FIN_WAIT_2 双向 低 ★★★★★
tcp_tw_reuse TIME_WAIT 出站 低(需配合 timestamps) ★★★★☆
tcp_tw_recycle TIME_WAIT 入站 高(NAT 环境必坑) ★☆☆☆☆

3.4 实战部署:一套完整的优化方案

下面是我在线上常用的配置,直接贴出来:

# /etc/sysctl.conf 追加以下内容

# 缩短 FIN_WAIT_2 超时
net.ipv4.tcp_fin_timeout = 15

# 允许重用 TIME_WAIT 连接(出站方向)
net.ipv4.tcp_tw_reuse = 1

# 开启 TCP 时间戳(tcp_tw_reuse 的前置条件)
net.ipv4.tcp_timestamps = 1

# 调整 TIME_WAIT 最大数量(防止极端情况)
net.ipv4.tcp_max_tw_buckets = 20000

# 应用配置
sysctl -p

补充说明:tcp_max_tw_buckets 是 TIME_WAIT 的上限,超过后内核会直接丢弃多余的 TIME_WAIT 连接。我一般设 2 万,够用又不至于失控。

3.5 知识体系:一张图看懂

下面这张 SVG 图,把四次挥手、TIME_WAIT 堆积、三个参数的优化路径串起来了。

TCP 四次挥手优化知识体系 主动关闭方 被动关闭方 ① FIN ② ACK ③ FIN ④ ACK TIME_WAIT 端口耗尽 新连接失败 优化参数 tcp_fin_timeout tcp_tw_reuse tcp_tw_recycle(已废弃) 避坑提醒 • tcp_tw_recycle 已从内核移除 • NAT 环境开启必出故障 • 用 tcp_tw_reuse 替代 • 配合 timestamps 使用

3.6 避坑指南

最后,我把自己踩过的坑和总结的经验列出来:

  • 不要同时开 tcp_tw_reuse 和 tcp_tw_recycle——后者已经废弃,前者足够用。
  • tcp_tw_reuse 必须配合 tcp_timestamps——否则内核无法判断序列号新旧,不会重用。
  • 服务端被动连接场景,tcp_tw_reuse 无效——这时候考虑调整 tcp_fin_timeout 或增加端口范围。
  • tcp_max_tw_buckets 设太小会丢连接——我建议不低于 10000,根据并发量调整。
  • 应用层优化才是根本——比如用连接池、长连接替代短连接,减少挥手次数。

重要:任何内核参数调整,先在测试环境验证。线上直接改,搞不好就是事故。

嗯,TIME_WAIT 优化就聊到这儿。参数不多,但每个都有适用场景。你想想看,你的服务是主动关闭还是被动关闭?是短连接还是长连接?搞清楚这些,优化方向自然就清晰了。