一、TCP四次挥手过程
TCP在建立连接时需要握手,同理,在关闭连接的时候也需要握手。
具体如下所示:
由于TCP连接是双向的,所以在关闭连接的时候,两个方向各自都需要关闭。先发FIN包的一方执行的是主动关闭,后发送FIN包的一方执行的是被动关闭。主动关闭的一方会进入TIME_WAIT状态,并且在此状态停留2MSL时长。
对于MSL,其指的是报文段的最大生存时间。如果报文段在网络中活动了MSL时间,还没有被接收,那么就会被丢弃。关于MSL的大小,RFC 793协议中给出的建议是2分钟,不过Linux中,通常是半分钟。
对于TIME_WAIT状态,有下图:
我们关注几个概念:
TIME_WAIT的产生条件:主动关闭方在发送四次挥手的最后一个ACK后会变为TIME_WAIT状态,持续时间为2MSL(Linux中一个MSL是30秒,是不可配置的)。
TIME_WAIT持续两个MSL的作用:首先,可靠安全地关闭TCP连接。比如网络拥塞,如果主动关闭方最后一个ACK没有被被动关闭方接收到,这时被动关闭方会对FIN进行超时重传,在这时尚未关闭的TIME_WAIT就会把这些尾巴问题处理掉,不至于对新连接及其他服务产生影响。其次,防止由于没有持续TIME_WAIT时间导致的新的TCP连接建立起来,延迟的FIN重传包会干扰新的连接。
TIME_WAIT占用的资源:少量内存(大概4K)和一个文件描述符fd。
TIME_WAIT关闭的危害:首先,当网络情况不好时,如果主动方无TIME_WAIT等待,关闭前个连接后,主动方与被动方又建立起新的TCP连接,这时被动方重传或延时过来的FIN包到达后会直接影响新的TCP连接;其次,当网络情况不好时,同时没有TIME_WAIT等待时,关闭连接后无新连接,那么当接收到被动方重传或延迟的FIN包后,会给被动方回送一个RST包,可能会影响被动方其他的服务连接。
TCP: time wait bucket table overflow
产生原因及影响:原因是当TIME_WAIT数超过了Linux系统的TW数量的阈值。危害是超过阈值后,系统会把多余的TIME_WAIT Socket删除掉,并且显示警告信息。如果是NAT网络且又存在大量访问时,会产生各种连接不稳定断开的情况。
二、相关参数优化调整
1. tcp_tw_recycle
顾名思义就是回收TIME_WAIT连接。可以说这个内核参数已经变成了处理TIME_WAIT的万金油,如果你在网络上搜索TIME_WAIT的解决方案,十有八九会推荐设置它,不过这里隐藏着一个不易察觉的陷阱:当多个客户端通过NAT方式联网并与服务端交互时,服务端看到的是同一个IP,也就是说对服务端而言这些客户端实际上等同于一个,由于这些客户端的时间戳可能存在差异,所以从服务端的视角看,便可能出现时间戳错乱的现象,进而直接导致时间戳小的数据包被丢弃。参考:tcp_tw_recycle和tcp_timestamps导致connect失败问题。
tcp_tw_recycle = 0
备注:建议不要开启该选项,现在互联网NAT使用很多,可能导致无法进行三次握手。
开启后在3.5*RTO(RTO时间是根据RTT时间计算而来)内回收TIME_WAIT,并60s内同一源ip主机的socket connect请求中的timestamp必须是递增的,对于服务端,同一个源ip可能会是NAT后很多机器,这些机器timestamp递增性无可保证,服务器会拒绝非递增请求连接,直接导致不能三次握手。
2. 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_timestamps = 1
tcp_tw_reuse = 1
3. tcp_max_tw_buckets
顾名思义就是控制TIME_WAIT总数。官网文档说这个选项只是为了阻止一些简单的DoS攻击,平常不要人为的降低它。如果缩小了它,那么系统会将多余的TIME_WAIT删除掉,日志里会显示:TCP: time wait bucket table overflow
。
需要提醒大家的是物极必反,曾经看到有人把「tcp_max_tw_buckets」设置成0,也就是说完全抛弃TIME_WAIT,这就有些冒险了,用一句围棋谚语来说:入界宜缓。
当出现TCP: time wait bucket table overflow
时,尽量调大下面参数:
tcp_max_tw_buckets = 256000
三、总结
有时候,如果我们换个角度去看问题,往往能得到四两拨千斤的效果。前面提到的例子:客户端向服务端发起HTTP请求,服务端响应后主动关闭连接,于是TIME_WAIT便留在了服务端。这里的关键在于主动关闭连接的是服务端!在关闭TCP连接的时候,先出手的一方注定逃不开TIME_WAIT的宿命,套用一句歌词:把我的悲伤留给自己,你的美丽让你带走。如果客户端可控的话,那么在服务端打开KeepAlive,尽可能不让服务端主动关闭连接,而让客户端主动关闭连接,如此一来问题便迎刃而解了。
四、补充
RST简介:
在TCP协议中,RST表示复位,用来关闭异常连接。发送RST包关闭连接时,不必等到缓冲区的数据包都发送出去,直接丢弃缓存区的数据包而发送RST包。接收端在接收到RST包后,不必发送ACK包来确认。
出现RST的几种情况:
端口未打开:客户端连接服务器程序未打开的端口。当客户端向服务器的某个端口发送SYN请求后,但是服务器上并没有打开该端口,因此其会向客户端发送RST。但是,并不是所有的操作系统都会发送RST给客户端,win7就会直接忽略该SYN报文。
在一个已关闭的socket上收到数据:如果某个socket已经关闭,但是依然接收数据,那么也会产生RST。
接收缓冲区还存在数据时,关闭连接:在请求方请求数据,未处理完缓冲区中的所有数据,就请求关闭连接时,请求方不会如预期的发送FIN包,进入4路关闭逻辑,而是可能会直接发送一个RST包强制完成连接的关闭。