time_wait再探timestamp;

http://blog.csdn.net/dog250/article/details/13760985

 

TCP的TIME_WAIT快速回收与重用

 

目录(?)[+]

 

声明一点:

Linux中是无法修改tcp的TIME_WAIT值的,除非重新编译,起码我是没有找到怎么改。值得注意的是,net.ipv4.tcp_fin_timeout这个参数是FIN_WAIT_2的值,而不是TIME_WAIT的值。我不知道为何很多人都会把它当成是TIME_WAIT的值,想了一下,我觉得是两点:
1.TIME_WAIT过于耀眼,以至于所有出现timeout,加上里面有个tcp的配置,都会想当然往TIME_WAIT上联系;
2.FIN_WAIT_2过于默默无闻,以至于很少有人知道它也是一种状态。

所以,我想大家在学习的时候,不能想当然。

TIME_WAIT的作用

TCP初见于互联网早期,当时的网络很不稳定,大量的丢包,为了冗余,大量包复制多径传输,网速慢--正因为如此,TCP才会更有意义。为了保证TCP的严格语义,就要避免上述冗余机制以及网速慢导致的问题,这些问题集中体现在连接关闭时的四次挥手上。
        由于TCP是全双工的,因此关闭连接必须在两个方向上分别进行。首先发起关闭的一方为主动关闭方,另一方为被动关闭方。很多人都会在这里晕掉,实际上四次挥手比三次握手还简单。四次挥手简单地分为三个过程:
过程一.主动关闭方发送FIN,被动关闭方收到后发送该FIN的ACK;
过程二.被动关闭方发送FIN,主动关闭方收到后发送该FIN的ACK;
过程三.被动关闭方收到ACK。

以上三步下来,圆圈就闭合了!也就是说,在过程三后,被动关闭方就可以100%确认连接已经关闭,因此它便可以直接进入CLOSE状态了,然而主动关闭的一方,它无法确定最后的那个发给被动关闭方的ACK是否已经被收到,据TCP协议规范,不对ACK进行ACK,因此它不可能再收到被动关闭方的任何数据了,因此在这里就陷入了僵局,TCP连接的主动关闭方如何来保证圆圈的闭合?这里,协议外的东西起作用了,和STP(Spanning tree)依靠各类超时值来收敛一样,IP也有一个超时值,即MSL,这类超时值超级重要,因为它们给出了一个物理意义上的不可逾越的界限,它们是自洽协议的唯一外部输入。MSL表明这是IP报文在地球上存活的最长时间,如果在火星上,Linux的代码必须要重新定义MSL的值并且要重新编译。
       于是问题就解决了,主动关闭一方等待MSL时间再释放连接,这个状态就是TIME_WAIT。对于被动关闭的一方,发出FIN之后就处在了LAST_ACK状态了,既然已经发出FIN了,缺的无非也就是个ACK,连接本身其实已经关闭了,因此被动关闭的一方就没有TIME_WAIT状态。
       实际上,两倍MSL才能说明一个报文的彻底丢失,因为还要记入其ACK返回时的MSL。

TIME_WAIT的问题

这个就不多说了,由于TIME_WAIT的存在,短连接时关闭的socket会长时间占据大量的tuple空间。

TIME_WAIT的快速回收

Linux实现了一个TIME_WAIT状态快速回收的机制,即无需等待两倍的MSL这么久的时间,而是等待一个Retrans时间即释放,也就是等待一个重传时间(一般超级短,以至于你都来不及能在netstat -ant中看到TIME_WAIT状态)随即释放。释放了之后,一个连接的tuple元素信息就都没有了,而此时,新建立的TCP却面临着危险,什么危险呢?即:
1.可能被之前迟到的FIN包给终止的危险;
2.被之前连接劫持的危险;
...

于是需要有一定的手段避免这些危险。什么手段呢?虽然曾经连接的tuple信息没有了,但是在IP层还可以保存一个peer信息,注意这个信息不单单是用于TCP这个四层协议的,路由逻辑也会使用它,其字段包括但不限于:
对端IP地址
peer最后一次被TCP触摸到的时间戳

...
在快速释放掉TIME_WAIT连接之后,peer依然保留着。丢失的仅仅是端口信息。不过有了peer的IP地址信息以及TCP最后一次触摸它的时间戳就足够了,TCP规范给出一个优化,即一个新的连接除了同时触犯了以下几点,其它的均可以快速接入,即使它本应该处在TIME_WAIT状态(但是被即快速回收了):
1.来自同一台机器的TCP连接携带时间戳;
2.之前同一台peer机器(仅仅识别IP地址,因为连接被快速释放了,没了端口信息)的某个TCP数据在MSL秒之内到过本机;
3.新连接的时间戳小于peer机器上次TCP到来时的时间戳,且差值大于重放窗口戳。

看样子只有以上的3点的同时满足才能拒绝掉一个新连接,要比TIME_WAIT机制设置的障碍导致的连接拒绝几率小很多,但是要看到,上述的快速释放机制没有端口信息!这就把几率扩大了65535倍。然而,如果对于单独的机器而言,这不算什么,因此单台机器的时间戳不可能倒流的,出现上述的3点均满足时,一定是老的重复数据包又回来了。
        但是,一旦涉及到NAT设备,就悲催了,因为NAT设备将数据包的源IP地址都改成了一个地址(或者少量的IP地址),但是却基本上不修改TCP包的时间戳,这就带来了问题。假设PC1和PC2均启用了TCP时间戳,它们经过NAT设备N1往服务器S1的22端口连接:
PC1:192.168.100.1
PC2:192.168.100.2
N1外网口(即NAT后的地址):172.16.100.1
S1:172.16.100.2
所有涉事机器的配置:
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_timestamps = 1
TCP的时间戳是根据本机的jiffers,uptime计算的,现在我能保证PC2的时间戳肯定远小于PC1。现在在PC1上先做一个telnet:
telnet 172.16.100.2 22
连接成功,S1上抓包,得到时间戳timestamps:TS val 698583769
为了让S1主动关闭进而快速回收TIME_WAIT,在S1上执行:
kill $(ps -ef|grep [s]sh|grep acce|awk -F ' ' '{print $2}');
目的是把仅仅光完成三次握手的连接终止掉而不触动已经连接的ssh。此时马上在PC2上telnet:
telnet 172.16.100.2 22
不通!在S1上抓包,得到时间戳timestamps:TS val 27727766。明显小于PC1的!由于有NAT设备,S1看来是同一台机器发出的,且出现了时间戳倒流,连接拒绝!此时在S1上查看计数值:
cat /proc/net/netstat
发现了PAWSPassive对应的值增加了1,每次PC2重发SYN,该计数值均会增加1,直到一个MSL时间过后,才能连接成功。如果反过来就没有问题,即先在PC2上telnet,然后S1主动关闭,然后紧接着PC1上telnet依然可以成功,这是因为时间戳是递增的,不满足上述的第三点。

       仅仅两台机器就出现了这个问题,试问如果大量的源端机器在服务器的入口处遇到了NAT设备会怎样?即一台三层NAT设备部署在高负载网站的入口处...没有谁能保证时间戳小的机器一定先发起连接,各个机器频繁连接断开后依然按照时间戳从小到大的顺序连接!!
       TIME_WAIT快速回收在Linux上通过net.ipv4.tcp_tw_recycle启用,由于其根据时间戳来判定,所以必须开启TCP时间戳才有效。建议:如果前端部署了三/四层NAT设备,尽量关闭快速回收,以免发生NAT背后真实机器由于时间戳混乱导致的SYN拒绝问题。

TIME_WAIT重用

如果说TIME_WAIT(输入法切换太烦人了,后面简称TW)回收只是一种特定系统的优化实现的话,那么TW重用则有相关的规范,即:如果能保证以下任意一点,一个TW状态的四元组(即一个socket连接)可以重新被新到来的SYN连接使用:
1.初始序列号比TW老连接的末序列号大
2.如果使能了时间戳,那么新到来的连接的时间戳比老连接的时间戳大
Linux上完美实现了上述的特性,可以通过下面的实验来证实:
S1上的服务程序:侦听端口1234,accept新连接,发送一段数据后调用close主动关闭连接。
S1上的额外配置:通过iptables禁止RESET包进入第四层,因为它会将TW状态终结。
PC1上客户端程序:绑定192.168.100.1,2000端口,连接S1,获取数据后调用close关闭连接。
PC2上客户端程序:使用IP_TRANSPARENT选项同样绑定192.168.100.1地址和2000端口,其它和PC1的程序相同。
启动服务端S1:172.16.100.2,不断侦听端口1234;
启动PC1上的C1:192.168.100.1,端口2000,连接S1的1234端口;
此时在S1上抓包,获取正常的三次握手/数据传输/四次挥手数据包。此时在S1上netstat -ant可以看到一个TW状态的连接。
启动PC2上的C2:192.168.100.1,端口2000,连接S1的1234端口;
此时在S1上抓包,SYN序列号seq 3934898078大于PC1发起连接时的最后一个序列号[F.], seq 2513913083, ack 3712390788,S1正常回复SYNACK:Flags [S.], seq 3712456325, ack 3934898079, ...对于这种TW重用的情况,S1的SYNACK的初始序列号是通过TW状态老连接的最后一个ack,即3712390788,加上常量65535+2算出来的!
      以上的实验是在关闭时间戳的情况下完成的,实际上开启时间戳的话,重用的可能性更高一些,毕竟是否能重用一个TW连接是通过以上的条件之一来判断的!

从外部干掉TIME_WAIT

TIME_WAIT状态时则一个阑尾!Linux系统上,除了使能recycle tw,在Linux系统上你无法更简单地缩短TW状态的时间,但是80%的问题却都是由TW状态引发,在Windows系统上,你需要在注册表添加一个隐式的项,稍微拼写错误都会引发沉默的失败!TW确实让人生气,因此我一直都希望干掉TW状态的连接,特别是干掉服务端TW状态的连接!我们可以通过TCP的RESET来干死TW连接。这个怎么说呢?
       根据TCP规范,收到任何的发送到未侦听端口或者序列号乱掉(窗口外)的数据,都要回执以RESET,这就是可以利用的!一个连接等待在TW,它自身无能为力,但是可以从外部间接杀掉它!具体来讲就是利用了IP_TRANSPARENT这个socket选项,它可以bind不属于本地的地址,因此可以从任意机器绑定TW连接的peer地址以及端口,然后发起一个连接,TW连接收到后由于序列号乱序会直接发送一个ACK,该ACK会回到TW连接的peer处,由于99%的可能该peer已经释放了连接(对端由于不能收到FIN-ACK的ACK,进而不放心ACK是否已经到达对端,等待MSL以便所有的老数据均已经丢失),因此peer由于没有该连接会回复RESET,TW连接收到RESET后会释放连接,进而为后续的连接腾出地方!

Linux实现Tips

Linux照顾到了一种特殊情况,即杀死进程的情况,在系统kill进程的时候,会直接调用连接的close函数单方面关闭一个方向的连接,然后并不会等待对端关闭另一个方向的连接进程即退出。现在的问题是,TCP规范和UNIX进程的文件描述符规范直接冲突!进程关闭了,套接字就要关闭,但是TCP是全双工的,你不能保证对端也在同一个时刻同意并且实施关闭动作,既然连接不能关闭,作为文件描述符,进程就不会关闭得彻底!所以,Linux使用了一种“子状态”的机制,即在进程退出的时候,单方面发送FIN,然后不等后续的关闭序列即将连接拷贝到一个占用资源更少的TW套接字,状态直接转入TIMW_WAIT,此时记录一个子状态FIN_WAIT_2,接下来的套接字就和原来的属于进程描述符的连接没有关系了。等到新的连接到来的时候,直接匹配到这个主状态为TW,子状态为FIN_WAIT_2的TW连接上,它负责处理FIN,FIN ACK等数据。

TIME_WAIT快速回收与重用

通过以上描述,我们看到TW状态的连接既可以被快速回收又可以被重用,但是二者的副作用是不同的。对于快速回收,由于丢失了TW连接的端口信息,全部映射到了IP地址信息,所以整个IP地址,也就是整机均被列入了考察对象,这本身并没有什么问题,因为快速回收只考虑时间戳信息,只要其保持单调递增即可,一般的机器时间是不会倒流的,但是遇到NAT合并就不行了,NAT设备为所有的内部设备代理一个IP地址即主机标识,然而却不触动其时间戳,而各个机器的时间戳并不满足任何规律...
        TW重用解决了整机范围拒绝接入的问题,但是却面临资源消耗的问题。它这个做法的依据之一仍然为,一般一个单独的主机是不可能在MSL内用同一个端口连接同一个服务的,除非它做了bind。因此等待一些遗留的数据丢失或者到达是有盼头的。有一点我有异议,我个人感觉,如果处在默默地TW等待中,有默默地非递增SYN或者递增时间戳SYN到来,千万别发ACK,只要默默丢弃即可,因为你发了ACK,对方在已经终止了连接的情况下,就会发RESET,进而终止掉本段连接。

TIME_WAIT的80/20悲剧

80%的问题都由20%的TW引发,甚至在各种的TCP实现中,大量的代码在处理TW!我个人觉得这有点过了!引入TW状态是为了确认老数据到来或者消失,且等待时延那么久,这已经是很多年以前的事了,那时我可能刚出生,家里可能还没有装电话...那时的网络条件,引入这些机制是确实需要的,但是随着网络技术的发展,TW已经慢慢成了鸡肋。即便新的TCP连接被老的FIN终止又怎样,即使新的连接被老的劫持又能怎样,即便不考虑这些,MSL未免也太长了些吧,话说当年DDN年代,这个值就已经很久了...不要试图保持TCP的安全了,即使面对中间人又能怎样?我们不是可以用SSL吗?TCP作为一种底层的传输协议,一定要简单,可是现在呢?虽然其内核保持着原汁原味,但是其细节使多少求知若渴的人踱步门外啊,不得不说,TCP的细节太复杂了,即使是再好的作家,也无法写出一本让人彻底明白的关于TCP细节的书。
        看看规范,各种公式,各种不可插拔的算法,各种魔术字,即使作者本人估计都很难说清楚内中细节。不得不说,TCP有点过度设计了,作为当年的设计精品,在当今越发往上层移动的年代,不合适了。如今越来越多的协议或者开元软件使用简单的UDP做扩展,在实现按序到达,确认,否认,时间戳,可靠连接等机制中实现自己需要的而不是所有,从TLS到OpenVPN,无一没有把UDP当成下一代的天骄。我很讨厌TCP,很讨厌这种乱七八糟的东西。你可能会反驳我,但我觉得你被洗脑了,你要知道,如果让你设计一个可靠的有连接协议,你可能做的真的比TCP更好。

===============================================

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

再叙TIME_WAIT

之所以起这样一个题目是因为很久以前我曾经写过一篇介绍TIME_WAIT的文章,不过当时基本属于浅尝辄止,并没深入说明问题的来龙去脉,碰巧这段时间反复被别人问到相关的问题,让我觉得有必要全面总结一下,以备不时之需。

 

讨论前大家可以拿手头的服务器摸摸底,记住「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网络连接耗费的资源无非就是一个端口、一点内存,但是架不住基数大,所以这始终是一个需要面对的问题。

为什么会存在TIME_WAIT?

TCP在建立连接的时候需要握手,同理,在关闭连接的时候也需要握手。为了更直观的说明关闭连接时握手的过程,我们引用「The TCP/IP Guide」中的例子

TCP Close

TCP Close

因为TCP连接是双向的,所以在关闭连接的时候,两个方向各自都需要关闭。先发FIN包的一方执行的是主动关闭;后发FIN包的一方执行的是被动关闭。主动关闭的一方会进入TIME_WAIT状态,并且在此状态停留两倍的MSL时长。

穿插一点MSL的知识:MSL指的是报文段的最大生存时间,如果报文段在网络活动了MSL时间,还没有被接收,那么会被丢弃。关于MSL的大小,RFC 793协议中给出的建议是两分钟,不过实际上不同的操作系统可能有不同的设置,以Linux为例,通常是半分钟,两倍的MSL就是一分钟,也就是60秒,并且这个数值是硬编码在内核中的,也就是说除非你重新编译内核,否则没法修改它:

#define TCP_TIMEWAIT_LEN (60*HZ)

如果每秒的连接数是一千的话,那么一分钟就可能会产生六万个TIME_WAIT。

为什么主动关闭的一方不直接进入CLOSED状态,而是进入TIME_WAIT状态,并且停留两倍的MSL时长呢?这是因为TCP是建立在不可靠网络上的可靠的协议。例子:主动关闭的一方收到被动关闭的一方发出的FIN包后,回应ACK包,同时进入TIME_WAIT状态,但是因为网络原因,主动关闭的一方发送的这个ACK包很可能延迟,从而触发被动连接一方重传FIN包。极端情况下,这一去一回,就是两倍的MSL时长。如果主动关闭的一方跳过TIME_WAIT直接进入CLOSED,或者在TIME_WAIT停留的时长不足两倍的MSL,那么当被动关闭的一方早先发出的延迟包到达后,就可能出现类似下面的问题:

  • 旧的TCP连接已经不存在了,系统此时只能返回RST包
  • 新的TCP连接被建立起来了,延迟包可能干扰新的连接

不管是哪种情况都会让TCP不再可靠,所以TIME_WAIT状态有存在的必要性。

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

参考文档:

  1. tcp短连接TIME_WAIT问题解决方法大全(1)——高屋建瓴
  2. tcp短连接TIME_WAIT问题解决方法大全(2)——SO_LINGER
  3. tcp短连接TIME_WAIT问题解决方法大全(3)——tcp_tw_recycle
  4. tcp短连接TIME_WAIT问题解决方法大全(4)——tcp_tw_reuse
  5. tcp短连接TIME_WAIT问题解决方法大全(5)——tcp_max_tw_buckets

此条目由老王发表在Technical分类目录,并贴了LinuxTCP标签。将固定链接加入收藏夹。

《再叙TIME_WAIT》上有15条评论

    1. sky在2014-01-0109:35:24说道:

      总结的不错~

    2. simple在2014-01-1510:28:34说道:

      这个问题的认识,又深刻了一步~

    3. Pingback引用通告: RTB Dev | Nginx做前端Proxy时TIME_WAIT过多的问题

    4. panzhc在2014-03-2716:26:26说道:

      总结很深刻

    5. rockybean在2014-04-1323:56:39说道:

      文章总结的很棒!但是关于tcp_tw_reuse这个参数,我觉得您理解的不对。这个参数也是针对服务端即被连接方的。它的本意就是重用处于TIME_WAIT状态的连接(相同的四元组)。您可以看下这篇文章http://blog.csdn.net/dog250/article/details/13760985 ,这里面很详细的讲了快速回收和重用的场景。

      • 老王在2014-04-1409:48:29说道:

        非常感谢您的反馈!

         
      • rockybean在2014-04-1411:32:12说道:

        不客气!另外tcp_tw_reuse即便不开timestamp也是会生效的,开了timestamp只是增大了重用的概率,触发重用满足两个条件之一就可以。

        1. 新连接的syn序列号比处于TIME_WAIT状态的连接最后收到的序列号大
        2. 新连接时间戳比老连接时间戳大。

        我的理解是,tcp_tw_reuse并不会显著的减少timewait状态的连接,最好的方法还是你文章最后总结的,开启keepalive,要客户端主动close。

         
      • 老王在2014-04-1509:04:25说道:

        我查阅了一些相关资料,感觉这个问题还真值得探讨一下,连接有incoming和outgoing之分,tcp_tw_reuse仅仅对outgoing有效。相关连接http://vincent.bernat.im/en/blog/2014-tcp-time-wait-state-linux.html

         
    6. 石头在2014-05-1716:41:28说道:

      问个问题:
      如果服务器在处理完客户端的连接后,主动关闭,就会有timewait状态,这个状态时针对连接fd还是listen fd?如果是listen fd那么会不会影响后续的accept,如果不是,那么同样的客户端再来请求,是不是就不成功接受了呢?

    7. beepony在2014-07-0210:01:50说道:

      客户端向服务端发起HTTP请求,服务端响应后主动关闭连接,于是TIME_WAIT便留在了服务端。这里的关键在于主动关闭连接的是服务端!在关闭TCP连接的时候,先出手的一方注定逃不开TIME_WAIT的宿命
      _______________________________________________________________
      你号,博主,感谢你的科普。我对上面的话,有点理解不透。按照上面说法,主动关闭的连接的一方是服务端,所以 TIME_WAIT 在服务端,但是看最上的图示,好像 TIME_WAIT 是在客户端的啊,有点凌乱了,希望指教下,谢谢。

      • 老王在2014-07-0215:50:43说道:

        因为图(客户端主动关闭)是网上找的,所以和文章内容有点不协调。

         
    8. 总结的不错。

    9. 此文正解!

    10. ling在2015-03-0918:41:10说道:

      客户端向服务端发起HTTP请求,服务端响应后主动关闭连接,于是TIME_WAIT便留在了服务端。这里的关键在于主动关闭连接的是服务端!在关闭TCP连接的时候,先出手的一方注定逃不开TIME_WAIT的宿命
      _______________________________________________________________
      如果把关闭连接的主动权放到客户端,这样会不会有风险,如果有大量恶意的空链接(只链接,不发数据),那会不会一样会耗掉大量的链接,而新的用户就连不上的问题

      • 老王在2015-03-1318:52:50说道:

        服务端(比如 nginx)有相应的 timeout 设置,例:client_header_timeout、client_body_timeout。

         

         

         

         

        http://115.29.247.234/thread-413-1-1.html

        #netstat -n | awk ‘/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}’

        LAST_ACK 14
        SYN_RECV 348
        ESTABLISHED 70
        FIN_WAIT1 229
        FIN_WAIT2 30
        CLOSING 33
        TIME_WAIT 18122

        状态:描述
        CLOSED:无连接是活动的或正在进行
        LISTEN:服务器在等待进入呼叫
        SYN_RECV:一个连接请求已经到达,等待确认
        SYN_SENT:应用已经开始,打开一个连接
        ESTABLISHED:正常数据传输状态
        FIN_WAIT1:应用说它已经完成
        FIN_WAIT2:另一边已同意释放
        ITMED_WAIT:等待所有分组死掉
        CLOSING:两边同时尝试关闭
        TIME_WAIT:另一边已初始化一个释放
        LAST_ACK:等待所有分组死掉

        也就是说,这条命令可以把当前系统的网络连接状态分类汇总。

        下面解释一下为啥要这样写:

        一个简单的管道符连接了netstat和awk命令。

        ——————————————————————

        先来看看netstat:

        netstat -n

        Active Internet connections (w/o servers)
        Proto Recv-Q Send-Q Local Address Foreign Address State
        tcp 0 0 123.123.123.123:80 234.234.234.234:12345 TIME_WAIT

        你实际执行这条命令的时候,可能会得到成千上万条类似上面的记录,不过我们就拿其中的一条就足够了。

        ——————————————————————

        再来看看awk:

        /^tcp/
        滤出tcp开头的记录,屏蔽udp, socket等无关记录。

        state[]
        相当于定义了一个名叫state的数组

        NF
        表示记录的字段数,如上所示的记录,NF等于6

        $NF
        表示某个字段的值,如上所示的记录,$NF也就是$6,表示第6个字段的值,也就是TIME_WAIT

        state[$NF]
        表示数组元素的值,如上所示的记录,就是state[TIME_WAIT]状态的连接数

        ++state[$NF]
        表示把某个数加一,如上所示的记录,就是把state[TIME_WAIT]状态的连接数加一

        END
        表示在最后阶段要执行的命令

        for(key in state)
        遍历数组

        print key,”\t”,state[key]
        打印数组的键和值,中间用\t制表符分割,美化一下。

        如发现系统存在大量TIME_WAIT状态的连接,通过调整内核参数解决,
        vim /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 让参数生效。

        ===========2014-11-13更新========================

        net.ipv4.ip_local_port_range = 1024 65000
        net.ipv4.tcp_max_tw_buckets = 35000
        net.ipv4.tcp_timestamps = 1
        #for centos6
        net.ipv4.netfilter.ip_conntrack_max = 122104 # 64bit4G=131072,64bit8G=262144
        net.ipv4.netfilter.ip_conntrack_tcp_timeout_established = 36000

        如果使用tcp_tw_reuse,必需设置tcp_timestamps=1,否则无效
        ===================================================

        net.ipv4.tcp_syncookies = 1 表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭;
        net.ipv4.tcp_tw_reuse = 1 表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭,需要tcp_timestamps在两边都被打开;
        net.ipv4.tcp_tw_recycle = 1 表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭,不要在NAT网络里使用。
        net.ipv4.tcp_fin_timeout 修改系統默认的 TIMEOUT 时间

        下面附上TIME_WAIT状态的意义:

        客户端与服务器端建立TCP/IP连接后关闭SOCKET后,服务器端连接的端口
        状态为TIME_WAIT

        是不是所有执行主动关闭的socket都会进入TIME_WAIT状态呢?
        有没有什么情况使主动关闭的socket直接进入CLOSED状态呢?

        主动关闭的一方在发送最后一个 ack 后
        就会进入 TIME_WAIT 状态 停留2MSL(max segment lifetime)时间
        这个是TCP/IP必不可少的,也就是“解决”不了的。

        也就是TCP/IP设计者本来是这么设计的
        主要有两个原因
        1。防止上一次连接中的包,迷路后重新出现,影响新连接
        (经过2MSL,上一次连接中所有的重复包都会消失)
        2。可靠的关闭TCP连接
        在主动关闭方发送的最后一个 ack(fin) ,有可能丢失,这时被动方会重新发
        fin, 如果这时主动方处于 CLOSED 状态 ,就会响应 rst 而不是 ack。所以
        主动方要处于 TIME_WAIT 状态,而不能是 CLOSED 。

        TIME_WAIT 并不会占用很大资源的,除非受到攻击。

        还有,如果一方 send 或 recv 超时,就会直接进入 CLOSED 状态

        附上centos5.8 64bit 16G内存web服务器的配置

        net.ipv4.netfilter.ip_conntrack_max = 255128 # 64bit4G=131072,64bit8G=262144
        net.ipv4.netfilter.ip_conntrack_tcp_timeout_established = 36000

        net.ipv4.tcp_max_tw_buckets = 35000
        net.ipv4.tcp_sack = 1
        net.ipv4.tcp_window_scaling = 1
        net.ipv4.tcp_rmem = 4096        87380   4194304
        net.ipv4.tcp_wmem = 4096        16384   4194304

        net.ipv4.tcp_max_syn_backlog = 65536
        net.core.netdev_max_backlog =  32768
        net.core.somaxconn = 32768

        net.core.wmem_default = 8388608
        net.core.rmem_default = 8388608
        net.core.rmem_max = 16777216
        net.core.wmem_max = 16777216

        net.ipv4.tcp_timestamps = 1
        net.ipv4.tcp_synack_retries = 2
        net.ipv4.tcp_syn_retries = 2

        net.ipv4.tcp_tw_recycle = 1
        #net.ipv4.tcp_tw_len = 1
        net.ipv4.tcp_tw_reuse = 1

        net.ipv4.tcp_mem = 94500000 915000000 927000000
        net.ipv4.tcp_max_orphans = 3276800

        #net.ipv4.tcp_fin_timeout = 30

        #net.ipv4.tcp_keepalive_time = 300
        net.ipv4.ip_local_port_range = 1024    65000

        kernel.shmmax=6662000256
        参考:

        http://coolshell.cn/articles/11564.html

         

         

        http://blog.sina.com.cn/s/blog_781b0c850100znjd.html

        近来线上陆续出现了一些connect失败的问题,经过分析试验,最终确认和proc参数tcp_tw_recycle/tcp_timestamps相关;
        1. 现象
            第一个现象:模块A通过NAT网关访问服务S成功,而模块B通过NAT网关访问服务S经常性出现connect失败,抓包发现:服务S端已经收到了syn包,但没有回复synack;另外,模块A关闭了tcp timestamp,而模块B开启了tcp timestamp;
            第二个现象:不同主机上的模块C(开启timestamp),通过NAT网关(1个出口ip)访问同一服务S,主机C1 connect成功,而主机C2 connect失败;

        2. 分析
            根据现象上述问题明显和tcp timestmap有关;查看linux 2.6.32内核源码,发现tcp_tw_recycle/tcp_timestamps都开启的条件下,60s内同一源ip主机的socket connect请求中的timestamp必须是递增的。
            源码函数:tcp_v4_conn_request(),该函数是tcp层三次握手syn包的处理函数(服务端);
            源码片段:
               if (tmp_opt.saw_tstamp &&
                    tcp_death_row.sysctl_tw_recycle &&
                    (dst = inet_csk_route_req(sk, req)) != NULL &&
                    (peer = rt_get_peer((struct rtable *)dst)) != NULL &&
                    peer->v4daddr == saddr) {
                    if (get_seconds() < peer->tcp_ts_stamp + TCP_PAWS_MSL &&
                        (s32)(peer->tcp_ts - req->ts_recent) >
                                    TCP_PAWS_WINDOW) {
                        NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_PAWSPASSIVEREJECTED);
                        goto drop_and_release;
                    }
                }
                tmp_opt.saw_tstamp:该socket支持tcp_timestamp
                sysctl_tw_recycle:本机系统开启tcp_tw_recycle选项
                TCP_PAWS_MSL:60s,该条件判断表示该源ip的上次tcp通讯发生在60s内
                TCP_PAWS_WINDOW:1,该条件判断表示该源ip的上次tcp通讯的timestamp 大于 本次tcp

            分析:主机client1和client2通过NAT网关(1个ip地址)访问serverN,由于timestamp时间为系统启动到当前的时间,因此,client1和client2的timestamp不相同;根据上述syn包处理源码,在tcp_tw_recycle和tcp_timestamps同时开启的条件下,timestamp大的主机访问serverN成功,而timestmap小的主机访问失败;

            参数:/proc/sys/net/ipv4/tcp_timestamps - 控制timestamp选项开启/关闭
                  /proc/sys/net/ipv4/tcp_tw_recycle - 减少timewait socket释放的超时时间

        3. 解决方法
            echo 0 > /proc/sys/net/ipv4/tcp_tw_recycle;
            tcp_tw_recycle默认是关闭的,有不少服务器,为了提高性能,开启了该选项;
            为了解决上述问题,个人建议关闭tcp_tw_recycle选项,而不是timestamp;因为 在tcp timestamp关闭的条件下,开启tcp_tw_recycle是不起作用的;而tcp timestamp可以独立开启并起作用。
            源码函数:  tcp_time_wait()
            源码片段:
                if (tcp_death_row.sysctl_tw_recycle && tp->rx_opt.ts_recent_stamp)
                    recycle_ok = icsk->icsk_af_ops->remember_stamp(sk);
                ......
               
                if (timeo < rto)
                    timeo = rto;

                if (recycle_ok) {
                    tw->tw_timeout = rto;
                } else {
                    tw->tw_timeout = TCP_TIMEWAIT_LEN;
                    if (state == TCP_TIME_WAIT)
                        timeo = TCP_TIMEWAIT_LEN;
                }

                inet_twsk_schedule(tw, &tcp_death_row, timeo,
                           TCP_TIMEWAIT_LEN);

            timestamp和tw_recycle同时开启的条件下,timewait状态socket释放的超时时间和rto相关;否则,超时时间为TCP_TIMEWAIT_LEN,即60s;

            内核说明文档 对该参数的介绍如下:
            tcp_tw_recycle - BOOLEAN
            Enable fast recycling TIME-WAIT sockets. Default value is 0.
            It should not be changed without advice/request of technical
            experts.

        原文链接:http://blog.sina.com.cn/u/2015038597

         

         

        http://ju.outofmemory.cn/entry/126820

        linux TIME_WAIT 相关参数:

        net.ipv4.tcp_tw_reuse = 0    表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭
        net.ipv4.tcp_tw_recycle = 0  表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭
        net.ipv4.tcp_fin_timeout = 60  表示如果套接字由本端要求关闭,这个参数决定了它保持在FIN-WAIT-2状态的时间(可改为30,一般来说FIN-WAIT-2的连接也极少)

        注意:

        – 不像Windows 可以修改注册表修改2MSL 的值,linux 是没有办法修改MSL的,tcp_fin_timeout 不是2MSL 而是Fin-WAIT-2状态.

        – tcp_tw_reuse 和SO_REUSEADDR 是两个完全不同的东西

        1. tw_reuse,tw_recycle 必须在客户端和服务端timestamps 开启时才管用(默认打开)

        2. tw_reuse 只对客户端起作用,开启后客户端在1s内回收

        3. tw_recycle 对客户端和服务器同时起作用,开启后在 3.5*RTO 内回收,RTO 200ms~ 120s 具体时间视网络状况。

        内网状况比tw_reuse 稍快,公网尤其移动网络大多要比tw_reuse 慢,优点就是能够回收服务端的TIME_WAIT数量

        对于客户端

        1. 作为客户端因为有端口65535问题,TIME_OUT过多直接影响处理能力,打开tw_reuse 即可解决,不建议同时打开tw_recycle,帮助不大。

        2. tw_reuse 帮助客户端1s完成连接回收,基本可实现单机6w/s请求,需要再高就增加IP数量吧。

        3. 如果内网压测场景,且客户端不需要接收连接,同时tw_recycle 会有一点点好处。

        4. 业务上也可以设计由服务端主动关闭连接

        对于服务端

        1. 打开tw_reuse无效

        2. 线上环境 tw_recycle 不要打开

        服务器处于NAT 负载后,或者客户端处于NAT后(这是一定的事情,基本公司家庭网络都走NAT);

        公网服务打开就可能造成部分连接失败,内网的话到时可以视情况打开;

        像我所在公司对外服务都放在负载后面,负载会把timestamp 都给清空,好吧,就算你打开也不起作用。

        3. 服务器TIME_WAIT 高怎么办

        不像客户端有端口限制,处理大量TIME_WAIT Linux已经优化很好了,每个处于TIME_WAIT 状态下连接内存消耗很少,

        而且也能通过tcp_max_tw_buckets = 262144 配置最大上限,现代机器一般也不缺这点内存。

        下面像我们一台每秒峰值1w请求的http 短连接服务,长期处于tw_buckets 溢出状态,

        tw_socket_TCP 占用70M, 因为业务简单服务占用CPU 200% 运行很稳定。

        复制代码

        slabtop

        262230 251461  95%    0.25K  17482       15     69928K tw_sock_TCP

         ss -s
        Total: 259 (kernel 494)
        TCP:   262419 (estab 113, closed 262143, orphaned 156, synrecv 0, timewait 262143/0), ports 80
        Transport Total     IP        IPv6
        *         494       -         -        
        RAW       1         1         0        
        UDP       0         0         0        
        TCP       276       276       0        
        INET      277       277       0        
        FRAG      0         0         0
        复制代码

        唯一不爽的就是:

        系统日志中overflow 错误一直再刷屏,也许该buckets 调大一下了

        TCP: time wait bucket table overflow
        TCP: time wait bucket table overflow
        TCP: time wait bucket table overflow
        TCP: time wait bucket table overflow
        TCP: time wait bucket table overflow

        5. 业务上也可以设计由客户端主动关闭连接

        原理分析

        1. MSL 由来

        发起连接关闭方回复最后一个fin 的ack,为避免对方ack 收不到、重发的或还在中间路由上的fin 把新连接给干掉了,等个2MSL,4min。

        也就是连接有谁关闭的那一方有time_wait问题,被关那方无此问题。

        2. reuse、recycle

        通过timestamp的递增性来区分是否新连接,新连接的timestamp更大,那么小的timestamp的fin 就不会fin掉新连接。

        3. reuse

        通过timestamp 递增性,客户端、服务器能够处理outofbind fin包

        4. recycle

        对于服务端,同一个src ip,可能会是NAT后很多机器,这些机器timestamp递增性无可保证,服务器会拒绝非递增请求连接。

        细节之处还得好好阅读tcp 协议栈源码了

        建议阅读以下参考:

        Coping with the TCP TIME-WAITstate on busy Linux servers 

posted @ 2015-04-20 11:39  陳聽溪  阅读(350)  评论(0)    收藏  举报