文档章节

linux服务器大量TIME_WAIT状态问题

zhangyujsj
 zhangyujsj
发布于 2016/10/12 14:53
字数 5223
阅读 1307
收藏 0

上篇:问题与理论

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

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

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),可用下图来示意:
       

      可对上图做如下解释:
        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等参数,这涉及到协议的细节,本文旨在抛砖引玉,不再展开。

           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节。

 

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

非常棒!

本文转载自:http://www.cnblogs.com/javawebsoa/archive/2013/05/18/3086034.html

共有 人打赏支持
zhangyujsj
粉丝 24
博文 358
码字总数 224241
作品 0
广州
私信 提问
Socket 连接问题之大量 TIME_WAIT

简评:最近项目就出现了大量短连接导致建立新连接超时问题,最后是通过维护长连接解决的。 代理或者服务器设备都有端口限制,如果使用 TCP 连接,连接数量达到端口限制,在这种情况下,将不能...

极小光
2018/12/21
0
0
windows2003出现大量TIME_WAIT

问题1:有一台windows2003服务器,上边用的是apache+tomcat存放网站,近几天发现有大量TIME_WAIT状态的连接(400)个,ESTABLISHED状态500个左右,感觉不正常,请问如何调整才能降低TIME_WA...

枫爱若雪
2014/02/08
2.4K
4
TIME_WAIT和CLOSE_WAIT过多

转自大神(致敬):https://blog.csdn.net/shootyou/article/details/6622226 昨天解决了一个HttpClient调用错误导致的服务器异常,具体过程如下:http://blog.csdn.net/shootyou/article/d...

sailikung
2018/08/25
0
0
Linux 网络内核参数优化来提高服务器并发处理能力

简介 提高服务器性能有很多方法,比如划分图片服务器,主从数据库服务器,和网站服务器在服务器。但是硬件资源额定有限的情况下,最大的压榨服务器的性能,提高服务器的并发处理能力,是很多...

baby神
2018/07/04
0
0
linux和windows下TIME_WAIT过多的解决办法

如果使用了nginx代理,那么系统TIME_WAIT的数量会变得比较多,这是由于nginx代理使用了短链接的方式和后端交互的原因,使得nginx 和后端的ESTABLISHED变得很少而TIME_WAIT很多。这不但发生在...

红薯
2009/06/09
7.2K
2

没有更多内容

加载失败,请刷新页面

加载更多

记录replugin使用的一个坑

反复编译插件放入宿主中,一直出现如下错误: android.content.res.Resources$NotFoundException: Resource ID #0x7f050000 type #0x5 is not valid 回滚代码,重启AS还是出错。最终发现将宿...

Gemini-Lin
今天
1
0
Vert.x系列(二)--EventBusImpl源码分析

前言:Vert.x 实现了2种完成不同的eventBus: EventBusImpl(A local event bus implementation)和 它的子类 ClusteredEventBus(An event bus implementation that clusters with other Ve......

冷基
今天
1
0
Perl - 获取文件项目

参考:http://www.runoob.com/perl/perl-directories.html 下面返回JSON格式的文件列表 #!/usr/bin/perluse strict;use warnings;use utf8;use feature ':5.26';require Fi......

wffger
昨天
2
0
vue组件系列3、查询下载

直接源码,虽然样式样式不好看,逻辑也不是最优,但是可以留作纪念。毕竟以后类似的功能只需要优化就可以了,不用每次都重头开始。。。 <template> <div class="pre_upload"> <div ...

轻轻的往前走
昨天
2
0
java浅复制和深复制

之前写了数组的复制,所以这里继续总结一下浅复制和深复制。 浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝。 深拷贝:对基本数据类型进行值传递,对引用数据类型,...

woshixin
昨天
2
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部