系统调优,你所不知道的TIME_WAIT和CLOSE_WAIT

高性能网络 | 你所不知道的TIME_WAIT和CLOSE_WAIT

2016-02-18 大房 大房说git

本文是我将最近两篇文章,从新整理成一篇,方便收藏。若是你已经阅读过前两篇,而且已经作了收藏,能够从新收藏本文便可。github


你有收藏和整理文章的习惯吗?好好利用Evernote或者印象笔记,不要吝啬那点年费,你值得购买,并养成收藏和整理的习惯!web


本文源于你们在公众号里面的留言,既然不少人都搞不清楚TIME_WAIT和CLOSE_WAIT,那么小胖哥今天仍是抽个时间,统一帮你们理理概念吧。编程


你遇到过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%或者更高比例的开发就已经止步了。问题好像解决了,可是,要完全理解并解决这个问题,可能就没这么简单,或者说,还有很长的路要走!


什么是TIME-WAIT和CLOSE-WAIT?


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


你们知道,因为socket是全双工的工做模式,一个socket的关闭,是须要四次握手来完成的。


  • 主动关闭链接的一方,调用close();协议层发送FIN包

  • 被动关闭的一方收到FIN包后,协议层回复ACK;而后被动关闭的一方,进入CLOSE_WAIT状态,主动关闭的一方等待对方关闭,则进入FIN_WAIT_2状态;此时,主动关闭的一方 等待 被动关闭一方的应用程序,调用close操做

  • 被动关闭的一方在完成全部数据发送后,调用close()操做;此时,协议层发送FIN包给主动关闭的一方,等待对方的ACK,被动关闭的一方进入LAST_ACK状态

  • 主动关闭的一方收到FIN包,协议层回复ACK;此时,主动关闭链接的一方,进入TIME_WAIT状态;而被动关闭的一方,进入CLOSED状态

  • 等待2MSL时间,主动关闭的一方,结束TIME_WAIT,进入CLOSED状态


经过上面的一次socket关闭操做,你能够得出如下几点:


  1. 主动关闭链接的一方 - 也就是主动调用socket的close操做的一方,最终会进入TIME_WAIT状态

  2. 被动关闭链接的一方,有一个中间状态,即CLOSE_WAIT,由于协议层在等待上层的应用程序,主动调用close操做后才主动关闭这条链接

  3. TIME_WAIT会默认等待2MSL时间后,才最终进入CLOSED状态;

  4. 在一个链接没有进入CLOSED状态以前,这个链接是不能被重用的!


因此,这里凭你的直觉,TIME_WAIT并不可怕(not really,后面讲),CLOSE_WAIT才可怕,由于CLOSE_WAIT不少,表示说要么是你的应用程序写的有问题,没有合适的关闭socket;要么是说,你的服务器CPU处理不过来(CPU太忙)或者你的应用程序一直睡眠到其它地方(锁,或者文件I/O等等),你的应用程序得到不到合适的调度时间,形成你的程序无法真正的执行close操做。


这里又出现两个问题:


  1. 上文提到的链接重用,那链接究竟是个什么概念?

  2. 协议层为何要设计一个TIME_WAIT状态?这个状态为何默认等待2MSL时间才会进入CLOSED


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


Socket链接究竟是个什么概念?


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


  1. 源IP

  2. 源端口

  3. 目的IP

  4. 目的端口

  5. 类型: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有什么用?


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


第一,防止前一个链接【五元组,咱们继续以 180.172.35.150:45678, tcp, 180.97.33.108:80 为例】上延迟的数据包或者丢失重传的数据包,被后面复用的链接【前一个链接关闭后,此时你再次访问百度,新的链接可能仍是由180.172.35.150:45678, tcp, 180.97.33.108:80 这个五元组来表示,也就是源端口凑巧仍是45678】错误的接收(异常:数据丢了,或者传输太慢了),参见下图:


  • SEQ=3的数据包丢失,重传第一次,没有获得ACK确认

  • 若是没有TIME_WAIT,或者TIME_WAIT时间很是端,那么关闭的链接【180.172.35.150:45678, tcp, 180.97.33.108:80 的状态变为了CLOSED,源端口可被再次利用】,立刻被重用【对180.97.33.108:80新建的链接,复用了以前的随机端口45678】,并连续发送SEQ=1,2 的数据包

  • 此时,前面的链接上的SEQ=3的数据包再次重传,同时,seq的序号恰好也是3(这个很重要,否则,SEQ的序号对不上,就会RST掉),此时,前面一个链接上的数据被后面的一个链接错误的接收


第二,确保链接方能在时间范围内,关闭本身的链接。其实,也是由于丢包形成的,参见下图:


  • 主动关闭方关闭了链接,发送了FIN;

  • 被动关闭方回复ACK同时也执行关闭动做,发送FIN包;此时,被动关闭的一方进入LAST_ACK状态

  • 主动关闭的一方回去了ACK,主动关闭一方进入TIME_WAIT状态;

  • 可是最后的ACK丢失,被动关闭的一方还继续停留在LAST_ACK状态

  • 此时,若是没有TIME_WAIT的存在,或者说,停留在TIME_WAIT上的时间很短,则主动关闭的一方很快就进入了CLOSED状态,也便是说,若是此时新建一个链接,源随机端口若是被复用,在connect发送SYN包后,因为被动方仍认为这条链接【五元组】还在等待ACK,可是却收到了SYN,则被动方会回复RST

  • 形成主动建立链接的一方,因为收到了RST,则链接没法成功


因此,你看到了,TIME_WAIT的存在是很重要的,若是强制忽略TIME_WAIT,仍是有很高的机率,形成数据粗乱,或者短暂性的链接失败。


那么,为何说,TIME_WAIT状态会是持续2MSL(2倍的max segment lifetime)呢?这个时间能够经过修改内核参数调整吗?第一,这个2MSL,是RFC 793里定义的,参见RFC的截图标红的部分:


这个定义,更多的是一种保障(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调优,你必须理解的几个调优参数


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


net.ipv4.tcp_timestamps


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


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


net.ipv4.tcp_tw_reuse


字面意思,reuse TIME_WAIT状态的链接。


时刻记住一条socket链接,就是那个五元组,出现TIME_WAIT状态的链接,必定出如今主动关闭链接的一方。因此,当主动关闭链接的一方,再次向对方发起链接请求的时候(例如,客户端关闭链接,客户端再次链接服务端,此时能够复用了;负载均衡服务器,主动关闭后端的链接,当有新的HTTP请求,负载均衡服务器再次链接后端服务器,此时也能够复用),能够复用TIME_WAIT状态的链接。


经过字面解释,以及例子说明,你看到了,tcp_tw_reuse应用的场景:某一方,须要不断的经过“短链接”链接其余服务器,老是本身先关闭链接(TIME_WAIT在本身这方),关闭后又不断的从新链接对方。


那么,当链接被复用了以后,延迟或者重发的数据包到达,新的链接怎么判断,到达的数据是属于复用后的链接,仍是复用前的链接呢?那就须要依赖前面提到的两个时间字段了。复用链接后,这条链接的时间被更新为当前的时间,当延迟的数据达到,延迟数据的时间是小于新链接的时间,因此,内核能够经过时间判断出,延迟的数据能够安全的丢弃掉了。


这个配置,依赖于链接双方,同时对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的时间内,只能有一个客户端和本身链接成功(不一样的客户端发包的时间不一致,形成服务端直接把数据包丢弃掉)。


我尽可能尝试用文字解释清楚,可是,来点案例和图示,应该有助于咱们完全理解。


咱们来看这样一个网络状况:


  1. 客户端IP地址为:180.172.35.150,咱们能够认为是浏览器

  2. 负载均衡有两个IP,外网IP地址为 115.29.253.156,内网地址为10.162.74.10;外网地址监听80端口

  3. 负载均衡背后有两台Web服务器,一台IP地址为 10.162.74.43,监听80端口;另外一台为 10.162.74.44,监听 80 端口

  4. Web服务器会链接数据服务器,IP地址为 10.162.74.45,监听 3306 端口


这种简单的架构下,咱们来看看,在不一样的状况下,咱们今天谈论的tw_reuse/tw_recycle对网络链接的影响。


先作个假定:

  1. 客户端经过HTTP/1.1链接负载均衡,也就是说,HTTP协议投Connection为keep-alive,因此咱们假定,客户端 对 负载均衡服务器 的socket链接,客户端会断开链接,因此,TIME_WAIT出如今客户端

  2. Web服务器和MySQL服务器的链接,咱们假定,Web服务器上的程序在链接结束的时候,调用close操做关闭socket资源链接,因此,TIME_WAIT出如今 Web 服务器端。


那么,在这种假定下:

  1. Web服务器上,确定能够配置开启的配置:tcp_tw_reuse;若是Web服务器有不少连向DB服务器的链接,能够保证socket链接的复用。

  2. 那么,负载均衡服务器和Web服务器,谁先关闭链接,则决定了咱们怎么配置tcp_tw_reuse/tcp_tw_recycle了


方案一:负载均衡服务器 首先关闭链接 


在这种状况下,由于负载均衡服务器对Web服务器的链接,TIME_WAIT大都出如今负载均衡服务器上,因此,在负载均衡服务器上的配置:

  • net.ipv4.tcp_tw_reuse = 1 //尽可能复用链接

  • net.ipv4.tcp_tw_recycle = 0 //不能保证客户端不在NAT的网络啊


在Web服务器上的配置为:

  • net.ipv4.tcp_tw_reuse = 1 //这个配置主要影响的是Web服务器到DB服务器的链接复用

  • net.ipv4.tcp_tw_recycle: 设置成1和0都没有任何意义。想想,在负载均衡和它的链接中,它是服务端,可是TIME_WAIT出如今负载均衡服务器上;它和DB的链接,它是客户端,recycle对它并无什么影响,关键是reuse



方案二:Web服务器首先关闭来自负载均衡服务器的链接


在这种状况下,Web服务器变成TIME_WAIT的重灾区。负载均衡对Web服务器的链接,由Web服务器首先关闭链接,TIME_WAIT出如今Web服务器上;Web服务器对DB服务器的链接,由Web服务器关闭链接,TIME_WAIT也出如今它身上,此时,负载均衡服务器上的配置:

  • net.ipv4.tcp_tw_reuse:0 或者 1 都行,都没有实际意义

  • net.ipv4.tcp_tw_recycle=0 //必定是关闭recycle

在Web服务器上的配置:

  • net.ipv4.tcp_tw_reuse = 1 //这个配置主要影响的是Web服务器到DB服务器的链接复用

  • net.ipv4.tcp_tw_recycle=1 //因为在负载均衡和Web服务器之间并无NAT的网络,能够考虑开启recycle,加速因为负载均衡和Web服务器之间的链接形成的大量TIME_WAIT


回答几个你们提到的几个问题


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的消耗有一些,可是是能够忽略的。

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


3. 若是想深刻的学习一下网络方面的知识,有什么推荐的?

学习网络比学一门编程语言“难”不少。所谓难,其实,是由于须要花不少的时间投入。我本身不算精通,只能说入门和理解。基本书能够推荐:《TCP/IP 协议详解》,必读;《TCP/IP高效编程:改善网络程序的44个技巧》,必读;《Unix环境高级编程》,必读;《Unix网络编程:卷一》,我只读过卷一;另外,还须要熟悉一下网络工具,tcpdump以及wireshark,个人notes里有一个一站式学习Wireshark:https://github.com/dafang/notebook/issues/114,也值得一读。有了这些积累,可能就是一些实践以及碎片化的学习和积累了。


写在最后


这篇文章我断断续续写了两天,内容找了多个地方去验证,包括看到Vincent Bernat的一篇文章以及Vincent在多个地方和别人的讨论。期间,我也花了一些时间和Vincent探讨了几个我没在tcp源码里翻找到的有疑问的地方。


我力求比散布在网上的文章作到准确并尽可能整理的清晰一些。可是,也不免会

有疏漏或者有错误的地方,高手看到能够随时指正,并和我讨论,你们一块儿研究!

相关文章
相关标签/搜索