网络-TIME_WAIT和CLOSE_WAIT
转载声明
本文大量内容系转载自以下文章,有删改,并参考其他文档资料加入了一些内容:
-
再谈应用环境下的TIME_WAIT和CLOSE_WAIT
作者:谐音太郎 -
服务器开发之大量time_wait 和 close_wait现象
作者:codergeek -
高性能网络 | 你所不知道的TIME_WAIT和CLOSE_WAIT
作者:CFC4N
转载仅为方便学习查看,一切权利属于原作者,本人只是做了整理和排版,如果带来不便请联系我删除。
1 背景
昨天解决了一个HttpClient调用错误导致的服务器异常,具体过程如下:
http://blog.csdn.net/shootyou/article/details/6615051
里头的分析过程有提到,通过查看服务器网络状态检测到服务器有大量的CLOSE_WAIT的状态。
使用netstat
查看本机连接信息
netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
结果如下:
TIME_WAIT 814
CLOSE_WAIT 1
FIN_WAIT1 1
ESTABLISHED 634
SYN_RECV 2
LAST_ACK 1
2 TCP报文格式
TCP报文格式如图:
上图中有几个字段需要重点介绍下:
-
序号:Seq序号,占32位,用来标识从TCP源端向目的端发送的字节流,发起方发送数据时对此进行标记。
-
确认序号:Ack序号,占32位,只有ACK标志位为1时,确认序号字段才有效,Ack=Seq+1。
-
标志位:共6个,即URG、ACK、PSH、RST、SYN、FIN等,具体含义如下:
- URG:紧急指针(urgent pointer)有效。
- ACK:确认序号有效。
只有ACK标志位为1时,确认序号字段才有效 - PSH:接收方应该尽快将这个报文交给应用层。
- RST:重置连接。
- SYN:发起一个新连接。
- FIN:释放一个连接。
需要注意的是:
- 不要将确认序号Ack与标志位中的ACK搞混了。
- 确认方Ack=发起方Req+1,两端配对。
3 常用连接状态
3.1 概述
- ESTABLISHED 表示正在通信
- TIME_WAIT 表示主动关闭
- CLOSE_WAIT 表示被动关闭
3.2 三次握手
3.2.1 概述
注意区分上图ACK和ack,大写的ACK为标志位,小写ack为确认序号总是为接收到的来源的seq+1。
3.2.2 三次握手步骤
所谓三次握手(Three-Way Handshake)是指建立一个TCP连接时,需要客户端和服务端总共发送3个包以确认连接的建立。步骤如下:
-
第一次握手
Client将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给Server,Client进入SYN_SENT状态,等待Server确认。 -
第二次握手
Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将标志位SYN和ACK都置为1,ack (number )=J+1,随机产生一个值seq=K,并将该数据包发送给Client以确认连接请求,Server进入SYN_RCVD状态。 -
第三次握手
Client收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给Server。随后Server检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,Client和Server进入ESTABLISHED状态,完成三次握手,随后Client与Server之间可以开始传输数据了。
3.2.3 三次握手实例
3.2.4 SYN攻击
三次握手过程中,Server发送SYN-ACK之后,收到Client的ACK之前的TCP连接称为半连接(half-open connect),此时Server处于SYN_RCVD状态,当收到ACK后,Server才转入ESTABLISHED状态。
SYN攻击就是Client在短时间内伪造大量不存在的IP地址,并向Server不断地发送SYN包,Server回复确认包,并等待Client的确认,由于源地址是不存在的,因此,Server需要不断重发直至超时,这些伪造的SYN包将长时间占用未连接队列,导致正常的SYN请求因为队列满而被丢弃,从而引起网络堵塞甚至系统瘫痪。
SYN攻击时一种典型的DDOS攻击,检测SYN攻击的方式非常简单,即当Server上有大量半连接状态且源IP地址是随机的,则可以断定遭到SYN攻击了,使用如下命令可以让之现行:
netstat -nap | grep SYN_RECV
3.3 四次挥手
3.3.1 概述
所谓四次挥手(Four-Way Wavehand)即终止TCP连接,就是指断开一个TCP连接时,需要客户端和服务端总共发送4个包以确认连接的断开。在socket编程中,这一过程由客户端或服务端任一方执行close来触发。
3.3.2 四次挥手步骤
-
第一次挥手
Client发送一个FIN,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态。 -
第二次挥手
Server收到FIN后,发送一个ACK给Client,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),Server进入CLOSE_WAIT
状态。此时Sever协议层在等待上层的应用程序,主动调用close操作后才主动关闭这条连接,发送FIN包。 -
第三次挥手
在等待数据处理完毕后不再发送数据后,Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态,表示同意关闭连接。 -
第四次挥手
Client收到FIN后,立刻发送一个ACK给Server,确认序号为收到序号+1,还会等待个2MSL时间此时Client进入TIME_WAIT状态。等待2MSL是为了确保ACK包到达Server。Server收到ACK包后进入CLOSED状态,完成四次挥手。
3.3.3 从客户端来看
- 客户端主动断开连接时,会先发送FIN包,客户端此时进入FIN_WAIT_1状态;
- 客户端收到服务器的ACK包(对步骤1中FIN包的应答)后,客户端进入FIN_WAIT_2状态;
- 客户端接收到服务器的FIN包并回复ACK包给服务端,然后客户端进入TIME_WAIT状态,此时会等待2个MSL的时间,确保发送的ACK包是否达到了对端。
- 客户端在等待了2个MSL的时间没有收到服务器重传的FIN包,就默认ACK数据包已经抵达了对端。
3.3.4 从服务端来看
- 服务器收到客户端发送的FIN数据包后,回复ACK包给客户端,此时服务器进入CLOSE_WAIT状态,开始做一些收尾工作。
- 等待服务器将剩余的数据全部发送给客户端时,然后执行断开操作,(老夫把该做的事都做了,然后再给这小子发送FIN包来结束,哈哈,姜还是老的辣!)服务器向客户端发送出FIN包后,服务器端进入LAST_ACK状态,等待最后一个ACK确认包。
- 服务端收到客户端发送的ACK包后,从LAST_ACK状态转为CLOSED状态,服务器正式关闭了
3.3.5 为什么要4次挥手?
确保被动关闭方的数据能够完整传输。
当被动方收到主动方的FIN报文通知时,它仅仅表示主动方没有数据再发送给被动方了。
但未必被动方所有的数据都完整的发送给了主动方,所以被动方不会马上关闭SOCKET,它可能还需要发送一些数据给主动方后,最后再发送FIN报文给主动方,告诉主动方同意关闭连接,所以这里的ACK报文和FIN报文多数情况下都是分开发送的。
由于TCP连接时全双工的,因此,每个方向都必须要单独进行关闭,这一原则是当一方完成数据发送任务后,发送一个FIN来终止这一方向的连接,收到一个FIN只是意味着这一方向上没有数据流动了,即不会再收到数据了,但是在这个TCP连接上仍然能够发送数据,直到这一方向也发送了FIN。
首先进行关闭的一方将执行主动关闭,而另一方则执行被动关闭。
3.3.6 四次挥手实例
3.3.7 同时发起主动关闭的情况
上面是一方主动关闭,另一方被动关闭的情况,实际中还会出现同时发起主动关闭的情况,具体流程如下图:
3.3.8 为什么TIME_WAIT和CLOSE_WAIT会占用资源?
因为在一个连接没有进入CLOSED状态之前,这个连接是不能被重用的。
3.3.9 TimeWait的作用
TimeWait为了解决网络的丢包和网络不稳定所带来的其他问题:
首先,防止前一个连接【五元组,我们继续以 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掉),此时,前面一个连接上的数据被后面的一个连接错误的接收。
第二,确保连接方能在时间范围内,关闭自己的连接。其实,也是因为丢包造成的,参见下图:
另外一个作用是,当最后一个ACK丢失时,远程连接进入LAST-ACK状态,它可以确保远程已经关闭当前TCP连接。如果没有TIME-WAIT状态,当远程仍认为这个连接是有效的,则会继续与其通讯,导致这个连接会被重新打开。当远程收到一个SYN 时,会回复一个RST包,因为这SEQ不对,那么新的连接将无法建立成功,报错终止。具体如下:
- 主动关闭方关闭了连接,发送了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,还是有很高的机率,造成数据错乱,或者短暂性的连接失败。
3.3.10 为什么TIME_WAIT状态要持续2MSL
MSL为 Maximum Segment Lifetime,即最大报文生存时间,具体来说是报文在网络传输时超时时间,超过的报文会被丢弃。那么传输一个来回就是2MSL。
2MSL即2倍的max segment lifetime
。
这个2MSL,是RFC 793里定义的。
如果第四次挥手时,服务端没有收到客户端的最终ACK,则会重发FIN;如果收到了就不会再发。
所以需要2MSL:
- 第一个MSL用来确保ACK发送到了服务端
- 第二个MSL用来确保如果丢失了ACK,被动关闭的一方再次重发FIN并等待主动关闭方回复的ACK,一来一去两个来回。
这个定义,更多的是一种保障(IP数据包里的TTL,即数据最多存活的跳数,真正反应的才是数据在网络上的存活时间),确保最后如果丢失了ACK,被动关闭的一方再次重发FIN并等待主动关闭方回复的ACK,一来一去两个来回。内核里,写死了这个MSL的时间为:30秒(有读者提醒,RFC里建议的MSL其实是2分钟,但是很多实现都是30秒),所以TIME_WAIT的即为1分钟。
另一个原因是为了防止过期连接包被当做新连接包处理:
4 问题
4.1 概述
如果服务器出了异常,百分之八九十都是下面两种情况:
- 服务器保持了大量
TIME_WAIT
状态 - 服务器保持了大量
CLOSE_WAIT
状态
因为linux分配给一个用户的文件句柄是有限的(可以参考:http://blog.csdn.net/shootyou/article/details/6579139),而TIME_WAIT
和CLOSE_WAIT
两种状态如果一直被保持,意味着对应数目的通道就一直被占着不用。这样持续积累,直到达到句柄数上限造成新的请求无法被处理,接着就是大量Too Many Open Files异常,tomcat崩溃等严重问题
4.2 服务器保持大量TIME_WAIT
4.2.1 原因分析
这种情况比较常见,一些爬虫服务器或者WEB服务器(如果网管在安装的时候没有做内核参数优化的话)上经常会遇到这个问题,这个问题是怎么产生的呢?
从上面的示意图可以看得出来,TIME_WAIT是主动关闭连接的一方保持的状态,对于爬虫服务器来说他本身就是“客户端”,在完成一个爬取任务之后,他就会发起主动关闭连接,从而进入TIME_WAIT的状态,然后在保持这个状态2MSL(max segment lifetime)时间之后,彻底关闭回收资源。为什么要这么做?明明就已经主动关闭连接了为啥还要保持资源一段时间呢?这个是TCP/IP的设计者规定的,主要出于以下两个方面的考虑:
-
防止上一次连接中的包,迷路后重新出现,影响新连接(经过2MSL,上一次连接中所有的重复包都会消失)
-
可靠的关闭TCP连接。在主动关闭方发送的最后一个
ack(fin)
,有可能丢失,这时被动方会重新发fin
。如果这时主动方处于
CLOSED
状态 ,就会响应rst
而不是ack
。所以主动方要处于TIME_WAIT
状态,而不能是CLOSED
。另外这么设计TIME_WAIT 会定时的回收资源,并不会占用很大资源的,除非短时间内接受大量请求或者受到攻击。
-
关于MSL引用下面一段话:
MSL 为一个TCP Segment (某一块TCP 网路封包) 从来源送到目的之间可续存的时间(也就是一个网路封包在网路上传输时能存活的时间),由于RFC 793 TCP 传输协定是在1981 年定义的,当时的网路速度不像现在的网际网路那样发达,你可以想像你从浏览器输入网址等到第一个byte 出现要等4 分钟吗?在现在的网路环境下几乎不可能有这种事情发生,因此我们大可将 TIME_WAIT 状态的续存时间大幅调低,好让端口 (Ports) 能更快空出来给其他连接使用。 -
再引用网络资源的一段话:
值得一说的是,对于基于TCP的HTTP协议,关闭TCP连接的是Server端,这样,Server端会进入TIME_WAIT状态,可 想而知,对于访问量大的Web Server,会存在大量的TIME_WAIT状态,假如server一秒钟接收1000个请求,那么就会积压240*1000=240,000个 TIME_WAIT的记录,维护这些状态给Server带来负担。当然现代操作系统都会用快速的查找算法来管理这些TIME_WAIT,所以对于新的 TCP连接请求,判断是否hit中一个TIME_WAIT不会太费时间,但是有这么多状态要维护总是不好。
HTTP协议1.1版规定default行为是Keep-Alive,也就是会重用TCP连接传输多个 request/response,一个主要原因就是发现了这个问题。
也就是说HTTP的交互跟上面画的那个图是不一样的,关闭连接的不是客户端,而是服务器,所以web服务器也是会出现大量的TIME_WAIT的情况的。
4.2.2 带来的问题
-
占用连接
处于TIME-WAIT状态的TCP连接,在链接表槽中存活1分钟,意味着另一个相同四元组(源地址,源端口,目标地址,目标端口)的连接不能出现,也就是说新的TCP(相同四元组)连接无法建立。再次回想一下前面的问题,如果一条连接,即使在四次握手关闭了,由于TIME_WAIT的存在,这个连接,在1分钟之内,也无法再次被复用,那么,如果你用一台机器做压测的客户端,你一分钟能发送多少并发连接请求?如果这台是一个负载均衡服务器,一台负载均衡服务器,一分钟可以有多少个连接同时访问后端的服务器呢?所以大量TIME_WAIT会影响服务器网络性能。
-
占用内存
一条Socket处于TIME_WAIT状态,它也是一条“存在”的socket,内核里也需要有保持它的数据:- 内核里有保存所有连接的一个hash table,这个hash table里面既包含TIME_WAIT状态的连接,也包含其他状态的连接。主要用于有新的数据到来的时候,从这个hash table里快速找到这条连接。
- 还有一个hash table用来保存所有的bound ports,主要用于可以快速的找到一个可用的端口或者随机端口:
- 内核里有保存所有连接的一个hash table,这个hash table里面既包含TIME_WAIT状态的连接,也包含其他状态的连接。主要用于有新的数据到来的时候,从这个hash table里快速找到这条连接。
-
占用内存CPU
当然!每次找到一个随机端口,还是需要遍历一遍bound ports的吧,这必然需要一些CPU时间。
4.2.3 问题解决
TIME_WAIT很多,既占内存又消耗CPU,这也是为什么很多人,看到TIME_WAIT很多,就蠢蠢欲动的想去干掉他们。其实,如果你再进一步去研究,1万条TIME_WAIT的连接,也就多消耗1M左右的内存,对现代的很多服务器,已经不算什么了。至于CPU,能减少它当然更好,但是不至于因为1万多个hash item就担忧。
解决思路很简单,就是让服务器能够快速回收和重用那些TIME_WAIT的资源。
下面来看一下我们网管对/etc/sysctl.conf文件的修改:
#对于一个新建连接,内核要发送多少个 SYN 连接请求才决定放弃,不应该大于255,默认值是5,对应于180秒左右时间
net.ipv4.tcp_syn_retries=2
#net.ipv4.tcp_synack_retries=2
#表示当keepalive起用的时候,TCP发送keepalive消息的频度。缺省是2小时,改为300秒
net.ipv4.tcp_keepalive_time=1200
net.ipv4.tcp_orphan_retries=3
#表示如果套接字由本端要求关闭,这个参数决定了它保持在FIN-WAIT-2状态的时间
net.ipv4.tcp_fin_timeout=30
#表示SYN队列的长度,默认为1024,加大队列长度为8192,可以容纳更多等待连接的网络连接数。
net.ipv4.tcp_max_syn_backlog = 4096
#表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭
net.ipv4.tcp_syncookies = 1
#表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭
net.ipv4.tcp_tw_reuse = 1
#表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭
net.ipv4.tcp_tw_recycle = 1
##减少超时前的探测次数
net.ipv4.tcp_keepalive_probes=5
##优化网络设备接收队列
net.core.netdev_max_backlog=3000
修改完之后执行/sbin/sysctl -p
让参数生效。
这里主要注意到这几个参数:
- net.ipv4.tcp_tw_reuse和net.ipv4.tcp_tw_recycle
开启都是为了回收处于TIME_WAIT状态的资源。 - net.ipv4.tcp_fin_timeout
这个时间可以减少在异常情况下服务器从FIN-WAIT-2转到TIME_WAIT的时间。 - net.ipv4.tcp_keepalive_*
一系列参数,是用来设置服务器检测连接存活的相关配置。 - 关于keepalive的用途可以参考:http://hi.baidu.com/tantea/blog/item/580b9d0218f981793812bb7b.html
[2015.01.13更新]
注意tcp_tw_recycle开启的风险:http://blog.csdn.net/wireless_tech/article/details/6405755
4.3 服务器保持大量CLOSE_WAIT
TIME_WAIT状态可以通过优化服务器参数得到解决,因为发生TIME_WAIT的情况是服务器自己可控的,要么就是对方连接的异常,要么就是自己没有迅速回收资源,总之不是由于自己程序错误导致的。
但是CLOSE_WAIT就不一样了,从上面的图可以看出来,如果一直保持在CLOSE_WAIT状态,那么只有一种情况,就是在对方关闭连接之后收到FIN M
的程序自己没有进一步发出ack信号。换句话说,就是在对方连接关闭之后,程序里没有检测到,或者程序压根就忘记了这个时候需要关闭连接,于是这个资源就一直被程序占着。
个人觉得这种情况,通过服务器内核参数也没办法解决,服务器对于程序抢占的资源没有主动回收的权利,除非终止程序运行。
如果你使用的是HttpClient并且你遇到了大量CLOSE_WAIT的情况,那么这篇日志也许对你有用:http://blog.csdn.net/shootyou/article/details/6615051
在那边日志里头我举了个场景,来说明CLOSE_WAIT和TIME_WAIT的区别,这里重新描述一下:
服务器A是一台爬虫服务器,它使用简单的HttpClient去请求资源服务器B上面的apache获取文件资源,正常情况下,如果请求成功,那么在抓取完资源后,服务器A会主动发出关闭连接的请求,这个时候就是主动关闭连接,服务器A的连接状态我们可以看到是TIME_WAIT。
如果一旦发生异常呢?假设请求的资源服务器B上并不存在,那么这个时候就会由服务器B发出关闭连接的请求,服务器A就是被动的关闭了连接,如果服务器A被动关闭连接之后程序员忘了让HttpClient释放连接,那就不会发出FIN包,就会造成原来的主动方服务器A一直维持在CLOSE_WAIT
的状态了。
所以如果将大量CLOSE_WAIT的解决办法总结为一句话那就是:查代码。因为问题出在服务器程序里。