再叙TIME_WAIT

作者:火丁笔记

原文地址:https://blog.huoding.com/2013/12/31/316

总结:

  1. 为什么会存在 TIME_WAIT?

    主动关闭的一方收到被动关闭的一方发出的 FIN 包后,回应 ACK 包,同时进入 TIME_WAIT 状态,但是因为网络原因,主动关闭的一方发送的这个 ACK 包很可能延迟,从而触发被动连接一方重传 FIN 包。极端情况下,这一去一回,就是两倍的 MSL 时长。如果主动关闭的一方跳过 TIME_WAIT 直接进入 CLOSED,或者在 TIME_WAIT 停留的时长不足两倍的 MSL,那么当被动关闭的一方早先发出的延迟包到达后,就可能出现类似下面的问题:

    • 旧的 TCP 连接已经不存在了,系统此时只能返回 RST 包

    • 新的 TCP 连接被建立起来了,延迟包可能干扰新的连接

  2. 如何控制 TIME_WAIT 的数量?

    • ip_conntrack:顾名思义就是跟踪连接,不建议使用。
    • tcp_tw_recycle:回收 TIME_WAIT 连接
    • tcp_tw_reuse:顾名思义就是复用 TIME_WAIT 连接。既然我们要复用连接,那么当然应该在连接的发起方使用,而不能在被连接方使用。
    • tcp_max_tw_buckets:顾名思义就是控制 TIME_WAIT 总数。
    • 如果客户端可控的话,那么在服务端打开 KeepAlive,尽可能不让服务端主动关闭连接,而让客户端主动关闭连接,如此一来问题便迎刃而解了。

之所以起这样一个题目是因为很久以前我曾经写过一篇介绍 TIME_WAIT 的文章,不过当时基本属于浅尝辄止,并没深入说明问题的来龙去脉,碰巧这段时间反复被别人问到相关的问题,让我觉得有必要全面总结一下,以备不时之需。

讨论前大家可以拿手头的服务器摸摸底,记住「ss」比「netstat」快:

1
2
3
shell> ss -ant | awk '
NR>1 {++s[$1]} END {for(k in s) print k,s[k]}
'

如果你只是想单独查询一下 TIME_WAIT 的数量,那么还可以更简单一些:

1
shell> cat /proc/net/sockstat

我猜你一定被巨大无比的 TIME_WAIT 网络连接总数吓到了!以我个人的经验,对于一台繁忙的 Web 服务器来说,如果主要以短连接为主,那么其 TIME_WAIT 网络连接总数很可能会达到几万,甚至十几万。虽然一个 TIME_WAIT 网络连接耗费的资源无非就是一个端口、一点内存,但是架不住基数大,所以这始终是一个需要面对的问题。

1. 为什么会存在 TIME_WAIT?

TCP 在建立连接的时候需要握手,同理,在关闭连接的时候也需要握手。为了更直观的说明关闭连接时握手的过程,我们引用「The TCP/IP Guide」中的例子

[TCP Close](https://blog.huoding.com/wp-content/uploads/2013/12/tcp_close.png)

TCP Close

因为 TCP 连接是双向的,所以在关闭连接的时候,两个方向各自都需要关闭。先发 FIN 包的一方执行的是主动关闭;后发 FIN 包的一方执行的是被动关闭。主动关闭的一方会进入 TIME_WAIT 状态,并且在此状态停留两倍的MSL时长。

穿插一点 MSL 的知识:MSL 指的是报文段的最大生存时间,如果报文段在网络活动了 MSL 时间,还没有被接收,那么会被丢弃。关于 MSL 的大小,RFC 793协议中给出的建议是两分钟,不过实际上不同的操作系统可能有不同的设置,以 Linux 为例,通常是半分钟,两倍的 MSL 就是一分钟,也就是 60 秒,并且这个数值是硬编码在内核中的,也就是说除非你重新编译内核,否则没法修改它:

1
#define TCP_TIMEWAIT_LEN (60*HZ)

如果每秒的连接数是一千的话,那么一分钟就可能会产生六万个 TIME_WAIT。

为什么主动关闭的一方不直接进入 CLOSED 状态,而是进入 TIME_WAIT 状态,并且停留两倍的 MSL 时长呢?这是因为 TCP 是建立在不可靠网络上的可靠的协议。例子:主动关闭的一方收到被动关闭的一方发出的 FIN 包后,回应 ACK 包,同时进入 TIME_WAIT 状态,但是因为网络原因,主动关闭的一方发送的这个 ACK 包很可能延迟,从而触发被动连接一方重传 FIN 包。极端情况下,这一去一回,就是两倍的 MSL 时长。如果主动关闭的一方跳过 TIME_WAIT 直接进入 CLOSED,或者在 TIME_WAIT 停留的时长不足两倍的 MSL,那么当被动关闭的一方早先发出的延迟包到达后,就可能出现类似下面的问题:

  • 旧的 TCP 连接已经不存在了,系统此时只能返回 RST 包
  • 新的 TCP 连接被建立起来了,延迟包可能干扰新的连接

不管是哪种情况都会让 TCP 不再可靠,所以 TIME_WAIT 状态有存在的必要性。

2. 如何控制 TIME_WAIT 的数量?

从前面的描述我们可以得出这样的结论:TIME_WAIT 这东西没有的话不行,不过太多可能也是个麻烦事。下面让我们看看有哪些方法可以控制 TIME_WAIT 数量,这里只说一些常规方法,另外一些诸如 SO_LINGER 之类的方法太过偏门,略过不谈。

ip_conntrack:顾名思义就是跟踪连接。一旦激活了此模块,就能在系统参数里发现很多用来控制网络连接状态超时的设置,其中自然也包括 TIME_WAIT:

1
2
shell> modprobe ip_conntrack
shell> sysctl net.ipv4.netfilter.ip_conntrack_tcp_timeout_time_wait

我们可以尝试缩小它的设置,比如十秒,甚至一秒,具体设置成多少合适取决于网络情况而定,当然也可以参考相关的案例。不过就我的个人意见来说,ip_conntrack 引入的问题比解决的还多,比如性能会大幅下降,所以不建议使用。

tcp_tw_recycle:顾名思义就是回收 TIME_WAIT 连接。可以说这个内核参数已经变成了大众处理 TIME_WAIT 的万金油,如果你在网络上搜索 TIME_WAIT 的解决方案,十有八九会推荐设置它,不过这里隐藏着一个不易察觉的陷阱

当多个客户端通过 NAT 方式联网并与服务端交互时,服务端看到的是同一个 IP,也就是说对服务端而言这些客户端实际上等同于一个,可惜由于这些客户端的时间戳可能存在差异,于是乎从服务端的视角看,便可能出现时间戳错乱的现象,进而直接导致时间戳小的数据包被丢弃。参考:tcp_tw_recycle 和 tcp_timestamps 导致 connect 失败问题

tcp_tw_reuse:顾名思义就是复用 TIME_WAIT 连接。当创建新连接的时候,如果可能的话会考虑复用相应的 TIME_WAIT 连接。通常认为「tcp_tw_reuse」比「tcp_tw_recycle」安全一些,这是因为一来 TIME_WAIT 创建时间必须超过一秒才可能会被复用;二来只有连接的时间戳是递增的时候才会被复用。官方文档里是这样说的:如果从协议视角看它是安全的,那么就可以使用。这简直就是外交辞令啊!按我的看法,如果网络比较稳定,比如都是内网连接,那么就可以尝试使用。

不过需要注意的是在哪里使用,既然我们要复用连接,那么当然应该在连接的发起方使用,而不能在被连接方使用。举例来说:客户端向服务端发起 HTTP 请求,服务端响应后主动关闭连接,于是 TIME_WAIT 便留在了服务端,此类情况使用「tcp_tw_reuse」是无效的,因为服务端是被连接方,所以不存在复用连接一说。让我们延伸一点来看,比如说服务端是 PHP,它查询另一个 MySQL 服务端,然后主动断开连接,于是 TIME_WAIT 就落在了 PHP 一侧,此类情况下使用「tcp_tw_reuse」是有效的,因为此时 PHP 相对于 MySQL 而言是客户端,它是连接的发起方,所以可以复用连接。

说明:如果使用 tcp_tw_reuse,请激活 tcp_timestamps,否则无效。

tcp_max_tw_buckets:顾名思义就是控制 TIME_WAIT 总数。官网文档说这个选项只是为了阻止一些简单的 DoS 攻击,平常不要人为的降低它。如果缩小了它,那么系统会将多余的 TIME_WAIT 删除掉,日志里会显示:「TCP: time wait bucket table overflow」。

需要提醒大家的是物极必反,曾经看到有人把「tcp_max_tw_buckets」设置成 0,也就是说完全抛弃 TIME_WAIT,这就有些冒险了,用一句围棋谚语来说:入界宜缓。

有时候,如果我们换个角度去看问题,往往能得到四两拨千斤的效果。前面提到的例子:客户端向服务端发起 HTTP 请求,服务端响应后主动关闭连接,于是 TIME_WAIT 便留在了服务端。这里的关键在于主动关闭连接的是服务端!在关闭 TCP 连接的时候,先出手的一方注定逃不开 TIME_WAIT 的宿命,套用一句歌词:把我的悲伤留给自己,你的美丽让你带走。如果客户端可控的话,那么在服务端打开KeepAlive,尽可能不让服务端主动关闭连接,而让客户端主动关闭连接,如此一来问题便迎刃而解了。

参考文档:

  1. tcp 短连接 TIME_WAIT 问题解决方法大全(1)——高屋建瓴
  2. tcp 短连接 TIME_WAIT 问题解决方法大全(2)——SO_LINGER
  3. tcp 短连接 TIME_WAIT 问题解决方法大全(3)——tcp_tw_recycle
  4. tcp 短连接 TIME_WAIT 问题解决方法大全(4)——tcp_tw_reuse
  5. tcp 短连接 TIME_WAIT 问题解决方法大全(5)——tcp_max_tw_buckets
打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!

扫一扫,分享到微信

微信分享二维码
  • Copyrights © 2015-2023 高行行
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信