你遇到过TIME_WAIT的问题吗?

我相信很多都遇到过这个问题。一旦有用户在喊:网络变慢了。第一件事情就是,netstat -a | grep TIME_WAIT | wc -l 一下。哎呀妈呀,几千个TIME_WAIT.

然后,做的第一件事情就是:打开Google或者Bing,输入关键词:too many time wait。一定能找到解决方案,而排在最前面或者被很多人到处转载的解决方案一定是:

打开 sysctl.conf 文件,修改以下几个参数:

  • net.ipv4.tcp_tw_recycle = 1
  • net.ipv4.tcp_tw_reuse = 1
  • net.ipv4.tcp_timestamps = 1

你也会被告知,开启tw_recylce和tw_reuse一定需要timestamps的支持,而且这些配置一般不建议开启,但是对解决TIME_WAIT很多的问题,有很好的用处。

接下来,你就直接修改了这几个参数,reload一下,发现,咦,没几分钟,TIME_WAIT的数量真的降低了,也没发现哪个用户说有问题,然后就没有然后了。

做到这一步,相信50%或者更高比例的开发就已经止步了。问题好像解决了,但是,要彻底理解并解决这个问题,可能就没这么简单,或者说,还有很长的路要走!

 

Socket连接到底是个什么概念?

大家经常提socket,那么,到底什么是一个socket?其实,socket就是一个五元组,包括:

  1. 源IP
  2. 源端口
  3. 目的IP
  4. 目的端口
类型:TCP or UDP

这个五元组,即标识了一条可用的连接。注意,有很多人把一个socket定义成四元组,也就是
源IP:源端口 + 目的IP:目的端口,这个定义是不正确的。

例如,如果你的本地出口IP是180.172.35.150,那么你的浏览器在连接某一个Web服务器,例如百度的时候,这条socket连接的四元组可能就是:

[180.172.35.150:45678, tcp, 180.97.33.108:80]

源IP为你的出口IP地址 180.172.35.150,源端口为随机端口 45678,目的IP为百度的某一个负载均衡服务器IP 180.97.33.108,端口为HTTP标准的80端口。

如果这个时候,你再开一个浏览器,访问百度,将会产生一条新的连接:

[180.172.35.150:43678, tcp, 180.97.33.108:80]

这条新的连接的源端口为一个新的随机端口 43678。

如此来看,如果你的本机需要压测百度,那么,你最多可以创建多少个连接呢?我在文章《云思路 | 轻松构建千万级投票系统》里也稍微提过这个问题,没有阅读过本文的,可以发送“投票系统“阅读。

 

 

什么是TIME-WAIT和CLOSE-WAIT?

所谓,要解决问题,就要先理解问题。随便改两行代码,发现bug”没有了“,也不是bug真的没有了,只是隐藏在更深的地方,你没有发现,或者以你的知识水平,你无法发现而已。

大家知道,由于socket是全双工的工作模式,一个socket的关闭,是需要四次握手来完成的。如下图讲解:

第一步:客户端(实线)打开应用程序(浏览器),发送SYN,网络端口从closed转为SYN_SENT。

第二步:服务器端(虚线)代码是已经运行起来的的,并将固定端口已经监听起来了,所以是LISTEN状态。再收到SYN后,发送SYN,ACK ,端口状态由LISTEN转为SYN_RECV

第三步:客户端收到SYN,ACK后,发送ACK,        端口状态从SYN_SENT 转为 ESTABLISHED

第四步:服务器端,收到ACK后,             直接将端口状态从SYN_RECV 转为 ESTABLISHED

第五步:客户端关闭应用程序(浏览器),发送FIN,端口状态从 ESTABLISHED转为 FIN_WAIT_1

第六步:服务端收到FIN, 发送ACK,                      端口状态从ESTABLISHED转为CLOSE_WAIT

第七步:客户端收到ACK,不发送,                       端口状态从FIN_WAIT_1转为FIN_WAIT_2

第八步:服务器端发送FIN,关闭应用程序,        端口状态从CLOSE_WAIT转为LAST_ACK

第九步:客户端收FIN,并发送ACK,                        端口状态从FIN_WAIT_2 转为TIME_WAIT

第十步:服务器端收到ACK,不发送,       端口状态从LAST_ACK转为CLOSED

第十一步:客户端等待2MSL(linux 60s)后,          端口状态从TIME_WAIT转为CLOSED

在发送数据过程中的SYN/ACK/FIN该如何理解:

  • 同步 SYN(synchronization):  同步 SYN = 1 表示这是一个连接请求或连接接受报文
  • 终止 FIN(finish):  用来释放一个连接.FIN=1 表明此报文段的发送端的数据已发送完毕,并要求释放运输连
  • 确认ACK(acknowledge):  只有当 ACK=1 时确认号字段才有效.当 ACK=0 时,确认号无效
  • 序号(seq)和确认号(ack):seq 是给报文增加了编号,ack是期望收到的报文的编号。这个ack 注意和确认ACK进行区分

    注:主动关闭方,在一个连接没有进入CLOSED状态之前,这个连接是不能被重用的

  1. 所以,这里凭你的直觉,TIME_WAIT并不可怕(not really,后面讲),time_wait只是客户端在等待最后的超时。所以他很高是很正常的事情

  2. 服务端CLOSE_WAIT才可怕,因为CLOSE_WAIT很多,表示说服务器端一直没关闭应用程序,没有正确地关闭socket;可能因为:你的服务器CPU处理不过来(CPU太忙);你的应用程序一直睡眠到其它地方(锁,或者文件I/O等等),你的应用程序获得不到合适的调度时间,造成你的程序没法真正的执行close操作。

  3. 客户端FIN_WAIT_2才是我们需要关注的地方,客户端是在收到服务端发来的FIN后才会变为FIN_WAIT_2。如果FIN_WAIT_2过多,有可能就是服务端程序CLOSE_WAIT 过多。

先解释清楚这两个问题,我们再来看,开头提到的几个网络配置究竟有什么用,以及TIME_WAIT的后遗症问题。

第二个问题,TIME_WAIT有什么用?

如果我们来做个类比的话,TIME_WAIT的出现,对应的是你的程序里的异常处理,它的出现,就是为了解决网络的丢包和网络不稳定所带来的其他问题:

1.如被关闭方在发起fin和ack时,因为中途ack丢失,而主动方又time_wait又没有,被动方在发起ack重传时,就有可能发到一个在该端口重新开启的新连接上。

2.在主动关闭方回复被动关闭方ack时,突然ack丢了,又因为主动方time_wait没有,那么ack就不能重传,被动方就只能一直停在那儿。若新的连接连接被动方,被动方就会发送一个rst,导致:"Connection reset"

 所以,你看到了,TIME_WAIT的存在是很重要的,如果强制忽略TIME_WAIT,还是有很高的机率,造成数据粗乱,或者短暂性的连接失败。那么,为什么说,TIME_WAIT状态会是持续2MSL(2倍的max segment lifetime)呢?这个时间可以通过修改内核参数调整吗?第一,这个2MSL,是RFC 793里定义的,

 

这个定义,更多的是一种保障(IP数据包里的TTL,即数据最多存活的跳数,真正反应的才是数据在网络上的存活时间),确保最后丢失了ACK,被动关闭的一方再次重发FIN并等待回复的ACK,一来一去两个来回。内核里,写死了这个MSL的时间为:30秒(有读者提醒,RFC里建议的MSL其实是2分钟,但是很多实现都是30秒),所以TIME_WAIT的即为1分钟:

所以,再次回想一下前面的问题,如果一条连接,即使在四次握手关闭了,由于TIME_WAIT的存在,这个连接,在1分钟之内,也无法再次被复用,那么,如果你用一台机器做压测的客户端,你一分钟能发送多少并发连接请求?如果这台是一个负载均衡服务器,一台负载均衡服务器,一分钟可以有多少个连接同时访问后端的服务器呢?

TIME_WAIT很多,可怕吗?

如果你通过 ss -tan state time-wait | wc -l 发现,系统中有很多TIME_WAIT,很多人都会紧张。多少算多呢?几百几千?如果是这个量级,其实真的没必要紧张。第一,这个量级,因为TIME_WAIT所占用的内存很少很少;因为记录和寻找可用的local port所消耗的CPU也基本可以忽略。

会占用内存吗?当然!任何你可以看到的数据,内核里都需要有相关的数据结构来保存这个数据啊。一条Socket处于TIME_WAIT状态,它也是一条“存在“的socket,内核里也需要有保持它的数据:

  1. 内核里有保存所有连接的一个hash table,这个hash table里面既包含TIME_WAIT状态的连接,也包含其他状态的连接。主要用于有新的数据到来的时候,从这个hash table里快速找到这条连接。不同的内核对这个hash table的大小设置不同,你可以通过dmesg命令去找到你的内核设置的大小:

  2. 还有一个hash table用来保存所有的bound ports,主要用于可以快速的找到一个可用的端口或者随机端口:

    由于内核需要保存这些数据,必然,会占用一定的内存。

    会消耗CPU吗?当然!每次找到一个随机端口,还是需要遍历一遍bound ports的吧,这必然需要一些CPU时间。

    TIME_WAIT很多,既占内存又消耗CPU,这也是为什么很多人,看到TIME_WAIT很多,就蠢蠢欲动的想去干掉他们。其实,如果你再进一步去研究,1万条TIME_WAIT的连接,也就多消耗1M左右的内存,对现代的很多服务器,已经不算什么了。至于CPU,能减少它当然更好,但是不至于因为1万多个hash item就担忧。

那么time_wait 真的就没有问题了吗?

端口没有closed前,是不能使用的,所以time_wait必然会占据客户端端口,一个系统总共端口数是65536个,但并不是所有的端口数都可以随机。所以time_wait 会导致客户端端口被大量占用。

引起问题,就必然需要解决问题:

方案一:使用多个客户端,从而扩大了客户端端口数量

方案二:开启端口复用,即:net.ipv4.tcp_tw_reuse = 1。注意:开启端口复用,就必须开启报文时间戳(net.ipv4.tcp_timestamps = 1),这样才能判断出报文哪些是过时了的,需要被丢弃。

 

回答几个大家提到的几个问题

1. 请问我们所说连接池可以复用连接,是不是意味着,需要等到上个连接time wait结束后才能再次使用?

所谓连接池复用,复用的一定是活跃的连接,所谓活跃,第一表明连接池里的连接都是ESTABLISHED的,第二,连接池做为上层应用,会有定时的心跳去保持连接的活跃性。既然连接都是活跃的,那就不存在有TIME_WAIT的概念了,在上篇里也有提到,TIME_WAIT是在主动关闭连接的一方,在关闭连接后才进入的状态。既然已经关闭了,那么这条连接肯定已经不在连接池里面了,即被连接池释放了。

2. 想请问下,作为负载均衡的机器随机端口使用完的情况下大量time_wait,不调整你文字里说的那三个参数,有其他的更好的方案吗?

第一,随机端口使用完,你可以通过调整/etc/sysctl.conf下的net.ipv4.ip_local_port_range配置,至少修改成 net.ipv4.ip_local_port_range=1024 65535,保证你的负载均衡服务器至少可以使用6万个随机端口,也即可以有6万的反向代理到后端的连接,可以支持每秒1000的并发(想一想,因为TIME_WAIT状态会持续1分钟后消失,所以一分钟最多有6万,每秒1000);如果这么多端口都使用完了,也证明你应该加服务器了,或者,你的负载均衡服务器需要配置多个IP地址,或者,你的后端服务器需要监听更多的端口和配置更多的IP(想一下socket的五元组)

第二,大量的TIME_WAIT,多大量?如果是几千个,其实不用担心,因为这个内存和CPU的消耗有一些,但是是可以忽略的。time_wait是可以忽略的,close_wait 是天理不容的

第三,如果真的量很大,上万上万的那种,可以考虑,让后端的服务器主动关闭连接,如果后端服务器没有外网的连接只有负载均衡服务器的连接(主要是没有NAT网络的连接),可以在后端服务器上配置tw_recycle,然后同时,在负载均衡服务器上,配置tw_reuse。

 

如果,你真的想去调优,才需要继续往下看

TIME_WAIT调优,你必须理解的几个调优参数

在具体的图例之前,我们还是先解析一下相关的几个参数存在的意义。

net.ipv4.tcp_timestamps

RFC 1323 在 TCP Reliability一节里,引入了timestamp,即:两个4字节长度的时间戳字段,第一个4字节字段用来保存发送该数据包的时间,第二个4字节字段用来保存最近一次接收对方发送到数据的时间。有了这两个时间字段,也就有了后续优化的余地。

tcp_tw_reuse 和 tcp_tw_recycle就依赖这些时间字段。

net.ipv4.tcp_tw_reuse

字面意思,重用 TIME_WAIT状态的连接。

时刻记住一条socket连接,就是那个五元组,出现TIME_WAIT状态的连接,一定出现在主动关闭连接的一方。所以,当主动关闭连接的一方,再次向对方发起连接请求的时候(例如,负载均衡服务器,主动关闭后端的连接,当有新的HTTP请求,负载均衡服务器再次连接后端服务器,此时也可以复用),可以复用TIME_WAIT状态的连接。

通过字面解释,以及例子说明,你看到了,tcp_tw_reuse应用的场景:某一方,需要不断的通过“短连接“连接其他服务器,总是自己先关闭连接(TIME_WAIT在自己这方),关闭后又不断的重新连接对方。

那么,当连接被复用了之后,延迟或者重发的数据包到达,新的连接怎么判断,到达的数据是属于复用后的连接,还是复用前的连接呢?那就需要依赖前面提到的【timestamp】。复用连接后,这条连接的时间被更新为当前的时间,当延迟的数据达到,延迟数据的时间是小于新连接的时间,所以,内核可以通过时间判断出,延迟的数据可以安全的丢弃掉了。

这个配置,依赖于连接双方,同时对timestamps的支持。同时,这个配置,仅仅影响outbound连接,即做为客户端的角色,连接服务端[connect(dest_ip, dest_port)]时复用TIME_WAIT的socket。

net.ipv4.tcp_tw_recycle

字面意思,销毁掉 TIME_WAIT。

当开启了这个配置后,内核会快速的回收处于TIME_WAIT状态的socket连接。多快?不再是2MSL,而是一个RTO(retransmission timeout,数据包重传的timeout时间)的时间,这个时间根据RTT动态计算出来,但是远小于2MSL。

有了这个配置,还是需要保障
丢失重传或者延迟的数据包,不会被新的连接(注意,这里不再是复用了,而是之前处于TIME_WAIT状态的连接已经被destroy掉了,新的连接,刚好是和某一个被destroy掉的连接使用了相同的五元组而已)错误的接收。在启用该配置,当一个socket连接进入TIME_WAIT状态后,内核里会记录包括该socket连接对应的五元组中的对方IP等在内的一些统计数据,当然也包括从该对方IP所接收到的最近的一次数据包时间。当有新的数据包到达,只要时间小于内核记录的这个时间,数据包都会被统统的丢掉。

这个配置,依赖于连接双方对timestamps的支持。同时,这个配置,主要影响到了inbound的连接(对outbound的连接也有影响,但是不是复用),即做为服务端角色,客户端连进来,服务端主动关闭了连接,TIME_WAIT状态的socket处于服务端,服务端快速的回收该状态的连接。

由此,如果客户端处于NAT的网络(多个客户端,同一个IP出口的网络环境),如果配置了tw_recycle,就可能在一个RTO的时间内,只能有一个客户端和自己连接成功(不同的客户端发包的时间不一致,造成服务端直接把数据包丢弃掉)。

上面介绍了time_wait的关闭或者重用,但是这都将导致被关闭方处于last_ack。其实这个不用管的,在被动方time_wait被关闭后/重用前,都会进入closed状态,此时主动方会在发送一个rst告诉被动方来关闭连接。

还需要注意一下,在修改参数前,一定要充分思考服务器的角色,服务器有可能既是主动连接方也可能是被动连接方。

posted on 2018-07-26 15:26  进_进  阅读(228)  评论(0)    收藏  举报