转载:http://huoding.com/2013/12/31/316

    http://blog.csdn.net/lxnkobe/article/details/7525317

    http://kerry.blog.51cto.com/172631/105233/

 

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

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

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

shell> cat /proc/net/sockstat

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

 

TCP终止连接一般是需要交换四个分节。具体来看: 

1、 应用进程(active close)首先调用close,于是导致TCP发送一个FIN分节,表示数据已分送完毕,请求关闭套接字。 
2、 另一端应用进程(passive close)接受收到FIN,并由该端的TCP确认(确认的过程是TCP发送ACK分节给对端套接字)。FIN的接受也作为文件结束符传递给上层应用进程。这里的文件结束符并非应用进程的EOF,在TCP字节流中,EOF的读或写通过收发一个特殊的FIN分节来实现。 
3、 另端(passive close)应用进程在接受到文件束符后,会调用close关闭它的套接字,这导致该端的TCP也发送了一个FIN分节。 
4、 主动关闭端(active close)接受到这个FIN后,TCP对它进行确认。(TCP发送ACK分节,值得注意的是主动关闭端在未接受到FIN之前,它的状态就是TIME_WAIT)。 

 

这张图在google image中,花了五六分钟才找到,觉得这张图是最直观、易懂的。 

TIME_OUT状态的存在的意义 
从图中,很清晰的看到TIME_WAIT状态发生在了active close 端,产生的时间点是发送ACK K+1 分节之后,原因是防止ACK分节在网络中丢失(lost),此时passive close进入LAST_ACK状态,意为等待ACK分节,如果此时ACK分节真的丢失了(passive close端的LAST_ACK超时),那么passive close端将会再次发送一个FIN K分节给对端。这就是为什么在图中,出现两次FIN的分节。 

 

TIME_OUT存在的理由用术语来描述,摘自UNIX Network Programming Vol1 中: 
1、 可靠地实现TCP全双工连接的终止。 
2、 允许老的重复分节在网络中消逝。 

 

TIME_OUT状态的持续时间 
图中标明了TIME_OUT状态的持续时间是最长分节生命周期(MSL)的两倍,即2MSL。RFC中的建议值是2分钟,Berkeley的实现传统上使用的是30秒,那么这意味着TIME_WAIT状态的延迟是在1~4分钟之间。TIME_WAIT是TCP协议用以保证被重新分配的socket不会受到之前残留的延迟重发报文影响的机制,是必要的逻辑保证。

(My:Linux的也是30s,编译在系统内核。要改的话,除非重新编译系统内核。)

既然TIME_OUT状态的存在是有其意义的,为什么这么多人对其如此敏感,对于CS的模式,大多是由客户机主动关闭连接,这也避免了TIME_OUT产生于服务端,但对于某些协议,如HTTP则是由服务器执行主动关闭的。(My:一般来说,尽量避免服务端过多的time_wait状态。因为这会导致socket可用的端口越来越少,影响系统的运行。【特别是短连接特别多的】系统。可以试着让系统更多的保持keepalive状态,可能会好点。)

 

解决方法:

【1】.如果可以的话,减小time_wait的时间。

  

vi /etc/sysctl.conf

编辑文件,加入以下内容:
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_fin_timeout = 30
 
然后执行 /sbin/sysctl -p 让参数生效。
 
net.ipv4.tcp_syncookies = 1 表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭;

net.ipv4.tcp_tw_reuse = 1 表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭;

net.ipv4.tcp_tw_recycle = 1 表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。

net.ipv4.tcp_fin_timeout 修改系統默认的 TIMEOUT 时间。
 
但上面只是暂时的解决方案,还需要自己去找具体的错误。
 
 
【2】.TCP的SO_LINGER 选项 
相信只要提到TIME_OUT,SO_LINGER就会现身,没错,该选项的设定控制着TCP的关闭形态,TCP默认是在close立即返回后,如果有数据残留在套接字的发送缓冲区中,系统将试着把这些数据发送给对端。 
JDK对该选项的定义为: 
Java代码  收藏代码
  1. public void setSoLinger(boolean on, int linger) throws SocketException;  

两个参数将产生下列三种情形: 
1、 on 为 false,则该选项关闭,linger 的值被忽略,这就是TCP的缺省设置,close立即返回,如果可能将会传输未发送的数据给对端; 

2、 设置on为true,linger大于0(我在很多文章中看到这里写的是非0,但Java中,给该选项设置小于0会抛出” invalid value for SO_LINGER”异常)那么当close某个连接时,内核将拖延一段时间。即linger的时间(linger的单位为秒,最大值为65535)。这里的拖延(close 阻塞)是相对于BIO来讲,如果套接字先前被设置为非阻塞(NIO),那么将不等待close完成,即使linger > 0也是如此。如果套接字发送缓冲区中仍然残留数据,那么close线程将被投入睡眠,直到所有数据都已发送完,并且均被对端确认或者拖延时间到,close才会被唤醒。 
这里有一个原则:设置SO_LINGER套接字选项后,close的成功返回只是告诉我们早先发送的数据(包括FIN)已由对端TCP确认,而不能告诉我们对端的应用进程是否已读取到数据,但如果不设置该套接字选项,那我们连对端TCP是否确认了数据都不知道; 

3、 设置on为true,linger 为0,那么当close某个连接时,TCP夭折该连接。也就是说TCP将丢弃保留在发送缓冲区中的任何数据,仅仅给对端发送一个RST分节,而没有通常所说的四分组终止序列,这样一来避免了TIME_WAIT状态。 
然而在2MSL秒内创建该连接的另一个化身,会导致老的重复分节被不正确地递送到新的化身上。这样的情况,有一个替代,就是TCP的 SO_REUSEADDR选项。 
 
 
【3】.如何控制TIME_WAIT的数量?

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

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

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,尽可能不让服务端主动关闭连接,而让客户端主动关闭连接,如此一来问题便迎刃而解了。

posted on 2017-02-11 17:30  反光的小鱼儿  阅读(2383)  评论(0编辑  收藏  举报