linux服务器大量TIME_WAIT状态问题

上篇:问题与理论

最近遇到一个线上报警:服务器出现大量TIME_WAIT致使其没法与下游模块创建新HTTP链接,在解决过程当中,经过查阅经典教材和技术文章,加深了对TCP网络问题的理解。做为笔记,记录于此。
        备注:本文主要介绍TCP编程中涉及到的众多基础知识,关于实际工程中对由TIME_WAIT引起的不能创建新链接问题的解决方法将在下篇笔记中给出。html

1. 实际问题
        初步查看发现,没法对外新建TCP链接时,线上服务器存在大量处于TIME_WAIT状态的TCP链接(最多的一次为单机10w+,其中引发报警的那个模块产生的TIME_WAIT约2w),致使其没法跟下游模块创建新TCP链接。
        TIME_WAIT涉及到TCP释放链接过程当中的状态迁移,也涉及到具体的socket api对TCP状态的影响,下面开始逐步介绍这些概念。linux

2. TCP状态迁移
       面向链接的TCP协议要求每次peer间通讯前创建一条TCP链接,该链接可抽象为一个4元组(four-tuple,有时也称socket pair):(local_ip, local_port, remote_ip,remote_port),这4个元素惟一地表明一条TCP链接。
       1)TCP Connection Establishment
       TCP创建链接的过程,一般又叫“三次握手”(three-way handshake),可用下图来示意:
       web

      可对上图作以下解释:
        a. client向server发送SYN并约定初始包序号(sequence number)为J;
        b. server发送本身的SYN并代表初始包序号为K,同时,针对client的SYNJ返回ACKJ+1(注:J+1表示server指望的来自该client的下一个包序为J+1);
        c. client收到来自server的SYN+ACK后,发送ACKK+1,至此,TCP创建成功。
        其实,在TCP创建时的3次握手过程当中,还要经过SYN包商定各自的MSS,timestamp等参数,这涉及到协议的细节,本文旨在抛砖引玉,再也不展开。shell

           2)TCPConnection Termination
       与创建链接的3次握手相对应,释放一条TCP链接时,须要通过四步交互(又称“四次挥手”),以下图所示:
        
         可对上图作以下解释:
       a. 链接的某一方先调用close()发起主动关闭(active close),该api会促使TCP传输层向remotepeer发送FIN包,该包代表发起active close的application再也不发送数据(特别注意:这里“再也不发送数据”的承诺是从应用层角度来看的,在TCP传输层,仍是要将该application对应的内核tcp send buffer中当前还没有发出的数据发到链路上)。                
       remote peer收到FIN后,须要完成被动关闭(passive close),具体分为两步:
       b. 首先,在TCP传输层,先针对对方的FIN包发出ACK包(主要ACK的包序是在对方FIN包序基础上加1);
       c. 接着,应用层的application收到对方的EOF(end-of-file,对方的FIN包做为EOF传给应用层的application)后,得知这条链接不会再有来自对方的数据,因而也调用close()关闭链接,该close会促使TCP传输层发送FIN。
       d. 发起主动关闭的peer收到remote peer的FIN后,发送ACK包,至此,TCP链接关闭。
       注意1:TCP链接的任一方都可以首先调用close()以发起主动关闭,上图以client主动发起关闭作说明,而不是说只能client发起主动关闭。
       注意2:上面给出的TCP创建/释放链接的过程描述中,未考虑因为各类缘由引发的重传、拥塞控制等协议细节,感兴趣的同窗能够查看各类TCP RFC Documents ,好比TCP RFC793编程

        3)TCP StateTransition Diagram
       上面介绍了TCP创建、释放链接的过程,此处对TCP状态机的迁移过程作整体说明。将TCP RFC793中描述的TCP状态机迁移图摘出以下(下图引用自这里):
     
          TCP状态机共含11个状态,状态间在各类socket apis的驱动下进行迁移,虽然此图看起来错综复杂,但对于有必定TCP网络编程经验的同窗来讲,理解起来仍是比较容易的。限于篇幅,本文不许备展开详述,想了解具体迁移过程的新手同窗,建议阅读《Linux Network Programming Volume1》第2.6节。api

 

3. TIME_WAIT状态
        
通过前面的铺垫,终于要讲到与本文主题相关的内容了。 ^_^
        从TCP状态迁移图可知,只有首先调用close()发起主动关闭的一方才会进入TIME_WAIT状态,并且是必须进入(图中左下角所示的3条状态迁移线最终均要进入该状态才能回到初始的CLOSED状态)。
        从图中还可看到,进入TIME_WAIT状态的TCP链接须要通过2MSL才能回到初始状态,其中,MSL是指Max
Segment Lifetime,即数据包在网络中的最大生存时间。每种TCP协议的实现方法均要指定一个合适的MSL值,如RFC1122给出的建议值为2分钟,又如Berkeley体系的TCP实现一般选择30秒做为MSL值。这意味着TIME_WAIT的典型持续时间为1-4分钟。
       TIME_WAIT状态存在的缘由主要有两点:
    
   1)为实现TCP这种全双工(full-duplex)链接的可靠释放
       参考本文前面给出的TCP释放链接4次挥手示意图,假设发起active close的一方(图中为client)发送的ACK(4次交互的最后一个包)在网络中丢失,那么因为TCP的重传机制,执行passiveclose的一方(图中为server)须要重发其FIN,在该FIN到达client(client是active close发起方)以前,client必须维护这条链接的状态(尽管它已调用过close),具体而言,就是这条TCP链接对应的(local_ip, local_port)资源不能被当即释放或从新分配。直到romete peer重发的FIN达到,client也重发ACK后,该TCP链接才能恢复初始的CLOSED状态。若是activeclose方不进入TIME_WAIT以维护其链接状态,则当passive close方重发的FIN达到时,active close方的TCP传输层会以RST包响应对方,这会被对方认为有错误发生(而事实上,这是正常的关闭链接过程,并不是异常)。
        2)为使旧的数据包在网络因过时而消失
       为说明这个问题,咱们先假设TCP协议中不存在TIME_WAIT状态的限制,再假设当前有一条TCP链接:(local_ip, local_port, remote_ip,remote_port),因某些缘由,咱们先关闭,接着很快以相同的四元组创建一条新链接。本文前面介绍过,TCP链接由四元组惟一标识,所以,在咱们假设的状况中,TCP协议栈是没法区分先后两条TCP链接的不一样的,在它看来,这根本就是同一条链接,中间先释放再创建的过程对其来讲是“感知”不到的。这样就可能发生这样的状况:前一条TCP链接由local peer发送的数据到达remote peer后,会被该remot peer的TCP传输层当作当前TCP链接的正常数据接收并向上传递至应用层(而事实上,在咱们假设的场景下,这些旧数据到达remote peer前,旧链接已断开且一条由相同四元组构成的新TCP链接已创建,所以,这些旧数据是不该该被向上传递至应用层的),从而引发数据错乱进而致使各类没法预知的诡异现象。做为一种可靠的传输协议,TCP必须在协议层面考虑并避免这种状况的发生,这正是TIME_WAIT状态存在的第2个缘由。
       具体而言,local peer主动调用close后,此时的TCP链接进入TIME_WAIT状态,处于该状态下的TCP链接不能当即以一样的四元组创建新链接,即发起active close的那方占用的local port在TIME_WAIT期间不能再被从新分配。因为TIME_WAIT状态持续时间为2MSL,这样保证了旧TCP链接双工链路中的旧数据包均因过时(超过MSL)而消失,此后,就能够用相同的四元组创建一条新链接而不会发生先后两次链接数据错乱的状况。安全

 4. socket api: close() 和 shutdown()
       
由前面内容可知,对一条TCP链接而言,首先调用close()的一方会进入TIME_WAIT状态,除此以外,关于close()还有一些细节须要说明。
       对一个tcp socket调用close()的默认动做是将该socket标记为已关闭并当即返回到调用该api进程中。此时,从应用层来看,该socket fd不能再被进程使用,即不能再做为read或write的参数。而从传输层来看,TCP会尝试将目前send buffer中积压的数据发到链路上,而后才会发起TCP的4次挥手以完全关闭TCP链接。
       调用close()是关闭TCP链接的正常方式,但这种方式存在两个限制,而这正是引入shutdown()的缘由:
       1)close()其实只是将socket fd的引用计数减1,只有当该socket fd的引用计数减至0时,TCP传输层才会发起4次握手从而真正关闭链接。而shutdown则能够直接发起关闭链接所需的4次握手,而不用受到引用计数的限制;
       2)close()会终止TCP的双工链路。因为TCP链接的全双工特性,可能会存在这样的应用场景:local peer不会再向remote peer发送数据,而remote peer可能还有数据须要发送过来,在这种状况下,若是local peer想要通知remote peer本身不会再发送数据但还会继续收数据这个事实,用close()是不行的,而shutdown()能够完成这个任务。服务器

       close()和shutdown()的具体调用方法能够man查看,此处再也不赘述。网络

       以上就是本文要分析和解决的“因为TIME_WAIT太多致使没法对外创建新链接”问题所须要掌握的基础知识。下一篇笔记会在本文基础上介绍这个问题具体的解决方法。^_^并发

 

 

 

 

 

 

下篇:问题分析与实战

 

 上篇笔记主要介绍了与TIME_WAIT相关的基础知识,本文则从实践出发,说明如何解决文章标题提出的问题。

1. 查看系统网络配置和当前TCP状态
        在定位并处理应用程序出现的网络问题时,了解系统默认网络配置是很是必要的。以x86_64平台Linux kernelversion 2.6.9的机器为例,ipv4网络协议的默认配置能够在/proc/sys/net/ipv4/下查看,其中与TCP协议栈相关的配置项均以tcp_xxx命名,关于这些配置项的含义,请参考这里的文档,此外,还能够查看linux源码树中提供的官方文档(src/linux/Documentation/ip-sysctl.txt)。下面列出我机器上几个需重点关注的配置项及其默认值:

cat /proc/sys/net/ipv4/ip_local_port_range      32768   61000
cat /proc/sys/net/ipv4/tcp_max_syn_backlog      1024
cat /proc/sys/net/ipv4/tcp_syn_retries          5
cat /proc/sys/net/ipv4/tcp_max_tw_buckets       180000
cat /proc/sys/net/ipv4/tcp_tw_recycle           0
cat /proc/sys/net/ipv4/tcp_tw_reuse             0

        其中,前3项分别说明了local port的分配范围(默认的可用端口数不到3w)、incomplete connection queue的最大长度以及3次握手时SYN的最大重试次数,这3项配置的含义,有个概念便可。后3项配置的含义则须要理解,由于它们在定位、解决问题过程当中要用到,下面进行重点说明。
        1) tcp_max_tw_buckets
         这篇文档 是这样描述的:Maximal number of time wait sockets held by system simultaneously. If this number is exceeded TIME_WAIT socket is immediately destroyed and warning is printed. This limit exists only to prevent simple DoS attacks, you must not lower the limit artificially, but rather increase it (probably, after increasing installed memory), if network conditions require more than default value (180000).
        可见,该配置项用来防范简单的DoS攻击 ,在某些状况下,能够适当调大,但绝对不该调小,不然,后果自负。。。
         2) tcp_tw_recycle
        Enable fast recycling of sockets in TIME-WAIT status. The defaultvalue is 0 (disabled). It should not be changed without advice/request of technical experts.
        该配置项可用于快速回收处于TIME_WAIT状态的socket以便从新分配。默认是关闭的,必要时能够开启该配置。可是开启该配置项后,有一些须要注意的地方,本文后面会提到。
         3) tcp_tw_reuse
         Allow to reuse TIME-WAIT sockets for new connections when it is safe from protocol viewpoint. The default value is 0. It should not
be changed without advice/request of technical experts.
        开启该选项后,kernel会复用处于TIME_WAIT状态的socket,固然复用的前提是“从协议角度来看,复用是安全的”。关于“ 在什么状况下,协议认为复用是安全的 ”这个问题,这篇文章 从linux kernel源码中挖出了答案,感兴趣的同窗能够查看。

 2. 网络问题定位思路
        参考前篇笔记开始处描述的线上实际问题,收到某台机器没法对外创建新链接的报警时,排查定位问题过程以下:
       用netstat –at | grep “TIME_WAIT”统计发现,当时出问题的那台机器上共有10w+处于TIME_WAIT状态的TCP链接,进一步分析发现,由报警模块引发的TIME_WAIT链接有2w+。将netstat输出的统计结果重定位到文件中继续分析,通常会看到本机的port被大量占用。
        由本文前面介绍的系统配置项可知,tcp_max_tw_buckets默认值为18w,而ip_local_port_range范围不到3w,大量的TIME_WAIT状态使得local port在TIME_WAIT持续期间不能被再次分配,即没有可用的local port,这将是致使新建链接失败的最大缘由
        在这里提醒你们:上面的结论只是咱们的初步判断,具体缘由还须要根据代码的异常返回值(如socket api的返回值及errno等)和模块日志作进一步确认。没法创建新链接的缘由还多是被其它模块列入黑名单了,本人就有过这方面的教训:程序中用libcurl api请求下游模块失败,初步定位发现机器TIME_WAIT状态不少,因而没仔细分析curl输出日志就认为是TIME_WAIT引发的问题,致使浪费了不少时间,折腾了半天发现不对劲后才想起,下游模块有防攻击机制,而发起请求的机器ip被不在下游模块的访问白名单内,高峰期上游模块经过curl请求下游的次数太过频繁被列入黑名单,新建链接时被下游模块的TCP层直接以RST包断开链接,致使curl api返回”Recv failure: Connection reset by peer”的错误。惨痛的教训呀 =_=
       另外,关于什么时候发送RST包,《Unix Network Programming Volume 1》第4.3节作了说明,做为笔记,摘出以下:
       An RST is a type of TCP segment that is sent by TCP when somethingis wrong.Three conditions that generatean RST are:            
        1) when a SYN arrives for a port that has no listening server;
        2) when TCP wants to abort an existing connection;
        3) when TCP receives a segment for a connection that does not exist. (TCPv1 [pp.246–250] contains additional information.)

3. 解决方法
        能够用两种思路来解决机器TIME_WAIT过多致使没法对外创建新TCP链接的问题。
        3.1 修改系统配置
        具体来讲,须要修改本文前面介绍的tcp_max_tw_buckets、tcp_tw_recycle、tcp_tw_reuse这三个配置项。
        1)将tcp_max_tw_buckets调大,从本文第一部分可知,其默认值为18w(不一样内核可能有所不一样,需以机器实际配置为准),根据文档,咱们能够适当调大,至于上限是多少,文档没有给出说明,我也不清楚。我的认为这种方法只能对TIME_WAIT过多的问题起到缓解做用,随着访问压力的持续,该出现的问题早晚仍是会出现,治标不治本。
        2)开启tcp_tw_recycle选项:在shell终端输入命令”echo 1 > /proc/sys/net/ipv4/tcp_tw_recycle”能够开启该配置。
        须要明确的是:其实TIME_WAIT状态的socket是否被快速回收是由tcp_tw_recycle和tcp_timestamps两个配置项共同决定的,只不过因为tcp_timestamps默认就是开启的,故大多数文章只提到设置tcp_tw_recycle为1。更详细的说明(分析kernel源码)可参见这篇文章
        还须要特别注意的是:当client与server之间有如NAT这类网络转换设备时,开启tcp_tw_recycle选项可能会致使server端drop(直接发送RST)来自client的SYN包。具体的案例及缘由分析,能够参考这里这里这里以及这里的分析,本文再也不赘述。
        3)开启tcp_tw_reuse选项:echo1 > /proc/sys/net/ipv4/tcp_tw_reuse。该选项也是与tcp_timestamps共同起做用的,另外socket reuse也是有条件的,具体说明请参见这篇文章。查了不少资料,与在用到NAT或FireWall的网络环境下开启tcp_tw_recycle后可能带来反作用相比,貌似没有发现tcp_tw_reuse引发的网络问题。
        3.2 修改应用程序
       具体来讲,能够细分为两种方式:
        1)将TCP短链接改造为长链接。一般状况下,若是发起链接的目标也是本身可控制的服务器时,它们本身的TCP通讯最好采用长链接,避免大量TCP短链接每次创建/释放产生的各类开销;若是创建链接的目标是不受本身控制的机器时,可否使用长链接就须要考虑对方机器是否支持长链接方式了。
        2)经过getsockopt/setsockoptapi设置socket的SO_LINGER选项,关于SO_LINGER选项的设置方法,《UNP Volume1》一书7.5节给出了详细说明,想深刻理解的同窗能够去查阅该教材,也能够参考这篇文章,讲的还算清楚。

4. 须要补充说明的问题
        咱们说TIME_WAIT过多可能引发没法对外创建新链接,其实有一个例外但比较常见的状况:S模块做为WebServer部署在服务器上,绑定本地某个端口;客户端与S间为短链接,每次交互完成后由S主动断开链接。这样,当客户端并发访问次数很高时,S模块所在的机器可能会有大量处于TIME_WAIT状态的TCP链接。但因为服务器模块绑定了端口,故在这种状况下,并不会引发“因为TIME_WAIT过多致使没法创建新链接”的问题。也就是说,本文讨论的状况,一般只会在每次由操做系统分配随机端口的程序运行的机器上出现(每次分配随机端口,致使后面无故口可用)。

 

 

延伸阅读:kernel怎么说

 

 

tcp_tw_reuse选项的含义以下(http://www.kernel.org/doc/Documentation/networking/ip-sysctl.txt):
tcp_tw_reuse - BOOLEAN
Allow to reuse TIME-WAIT sockets for new connections when it is
safe from protocol viewpoint. Default value is 0.
    
这里的关键在于“协议什么状况下认为是安全的”,因为环境限制,没有办法进行验证,经过看源码简单分析了一下。
=====linux-2.6.37 net/ipv4/tcp_ipv4.c 114=====
int tcp_twsk_unique(struct sock *sk, struct sock *sktw, void *twp)
{
const struct tcp_timewait_sock *tcptw = tcp_twsk(sktw);
struct tcp_sock *tp = tcp_sk(sk);


/* With PAWS, it is safe from the viewpoint
  of data integrity. Even without PAWS it is safe provided sequence
  spaces do not overlap i.e. at data rates <= 80Mbit/sec.


  Actually, the idea is close to VJ's one, only timestamp cache is
  held not per host, but per port pair and TW bucket is used as state
  holder.


  If TW bucket has been already destroyed we fall back to VJ's scheme
  and use initial timestamp retrieved from peer table.
*/
    //从代码来看,tcp_tw_reuse选项和tcp_timestamps选项也必须同时打开;不然tcp_tw_reuse就不起做用
    //另外,所谓的“协议安全”,从代码来看应该是收到最后一个包后超过1s
if (tcptw->tw_ts_recent_stamp &&
   (twp == NULL || (sysctl_tcp_tw_reuse &&
    get_seconds() - tcptw->tw_ts_recent_stamp > 1))) {
tp->write_seq = tcptw->tw_snd_nxt + 65535 + 2;
if (tp->write_seq == 0)
tp->write_seq = 1;
tp->rx_opt.ts_recent  = tcptw->tw_ts_recent;
tp->rx_opt.ts_recent_stamp = tcptw->tw_ts_recent_stamp;
sock_hold(sktw);
return 1;
}


return 0;

}

 

总结一下:
1)tcp_tw_reuse选项和tcp_timestamps选项也必须同时打开;
2)重用TIME_WAIT的条件是收到最后一个包后超过1s。


官方手册有一段警告:
It should not be changed without advice/request of technical
experts.
对于大部分局域网或者公司内网应用来讲,知足条件2)都是没有问题的,所以官方手册里面的警告其实也没那么可怕:)

 

 

 

 

最后下面的连接中有一系列仔细分析这个问题的:

http://blog.csdn.net/yah99_wolf/article/category/539413

很是棒!

相关文章
相关标签/搜索