结合内核源码来看如何调整影响TIME_WAIT状态套接字数量的参数

  这篇文件主要讨论tcp_max_tw_buckets、tcp_timestamps、tcp_tw_recycle、tcp_tw_reuse和tcp_fin_timeout参数。

  测试的时候看到系统日志中不断地出现“TCP: time wait bucket table overflow”的信息。在代码中搜索了一下,看到这条日志是在tcp_time_wait()函数中输出的,输出这条日志是在局部变量tw为NULL的情况下。局部变量tw的默认值为NULL, 在下面的代码中初始化:

    if (tcp_death_row.tw_count < tcp_death_row.sysctl_max_tw_buckets)
        tw = inet_twsk_alloc(sk, state);
其中tcp_death_row.tw_count是当前TIME_WAIT状态套接字的数量,tcp_death_row.sysctl_max_tw_buckets的值是系统参数tcp_max_tw_buckets的值,如果前者小于后者,则tw的值为NULL。假设分配内存失败,tw的值也为NULL。所以在TIME_WAIT套接字数量超过系统限制或者内存不足时,就会输出“TCP: time wait bucket table overflow”的日志信息,如下所示:
    if (tw != NULL) {
        ......

    } else {
        /* Sorry, if we're out of memory, just CLOSE this
         * socket up.  We've got bigger problems than
         * non-graceful socket closings.
         */
        LIMIT_NETDEBUG(KERN_INFO "TCP: time wait bucket table overflow\n");
    }
  我这里的问题是因为tcp_max_tw_buckets的值设置的太小了,调整大了之后就OK了。如果系统当前TIME_WAIT状态的套接字数量小于系统限制,出现这样的问题就是严重的内存泄露了。后来google了一下看怎么减小TIME_WAIT套接字的数量,设置的选项大致就是摘要中提到的那几个选项,不过大多数人都是几乎全都给设置,也根本没有考虑是否有用或者是否跟自己的业务相符合,看了内核代码之后发现这些选项并不是什么情况下都能减小time-wait套接字的数量,有些甚至会适得其反,下面来看内核是如何处理的。

一、tcp_max_tw_buckets参数
  tcp_max_tw_buckets参数是系统中TIME_WAIT套接字的最大数量。假如tcp_max_tw_buckets的值设的太小,否则会导致部分连接没法进入TIME_WAIT状态,TCP连接可能会不正常关闭,数据包会重传。假如tcp_max_tw_buckets的值设置的太大,TIME_WAIT状态套接字占用的内容可能很大。这个值通常设置的大一些比较好,利用空间来给内核足够的时间来清理之前的TIME_WAIT状态的套接字,然后再结合其他参数来减小TIME_WAIT套接字的影响。这个参数对服务器端和客户端都适用。

二、tcp_timestamps参数和tcp_tw_recycle参数
  tcp_timestamps参数用来设置是否启用时间戳选项,tcp_tw_recycle参数用来启用快速回收TIME_WAIT套接字。tcp_timestamps参数会影响到tcp_tw_recycle参数的效果。如果没有时间戳选项的话,tcp_tw_recycle参数无效,代码如下:

if (tcp_death_row.sysctl_tw_recycle && tp->rx_opt.ts_recent_stamp)
        recycle_ok = icsk->icsk_af_ops->remember_stamp(sk);
如果没有时间戳选项,tp->rx_opt.ts_recent_stamp的值为0,这样局部变量recycle_ok的值为0,在后面就会使用默认的时间TCP_TIMEWAIT_LEN(60s)作为TIME_WAIT状态的时间长度,如下所示:

       if (recycle_ok) {
            tw->tw_timeout = rto;
        } else {
            tw->tw_timeout = TCP_TIMEWAIT_LEN;
            if (state == TCP_TIME_WAIT)
                timeo = TCP_TIMEWAIT_LEN;
        }
所以在设置tcp_tw_recycle参数后要检查一下tcp_timestamps参数是否设置。
  接下来对tcp_tw_recycle参数的讨论在tcp_timestamps设置的前提下进行。
  sock结构进入TIME_WAIT状态有两种情况:一种是在真正进入了TIME_WAIT状态,还有一种是真实的状态是FIN_WAIT_2的TIME_WAIT状态。之所以让FIN_WAIT_2状态在没有接收到FIN包的情况下也可以进入TIME_WAIT状态是因为tcp_sock结构占用的资源要比tcp_timewait_sock结构占用的资源多,而且在TIME_WAIT下也可以处理连接的关闭。内核在处理时通过inet_timewait_sock结构的tw_substate成员来区分这种两种情况。如果是第一种情况,在调用tcp_time_wait()时指定的超时时间timeo参数的值为0,如果没有设置tcp_tw_recycle参数,TIME_WAIT状态持续的时间是默认值TCP_TIMEWAIT_LEN(60s);如果设置tcp_tw_recycle参数,TIME_WAIT状态持续的时间为局部变量rto的值。如果是第二种情况,在调用tcp_time_wait()时指定的超时时间timeo不为0,决定的是在子状态下等待的时间,如果在FIN_WAIT_2状态下接收到FIN包,在真正的TIME_WAIT状态下等待的时间是由tw->tw_timeout成员决定的。同样,在设置tcp_tw_recycle参数的情况下,tw->tw_timeout的值为rto,否则为TCP_TIMEWAIT_LEN。所以tcp_tw_recycle参数如果要实现对回收TIME_WAIT状态套接字的加速,需要这个时间rto小于TCP_TIMEWAIT_LEN。rto的值由下面的式子计算:
const int rto = (icsk->icsk_rto << 2) - (icsk->icsk_rto >> 1);
其中icsk->icsk_rto的值是超时重传的时间,这个值是根据网络情况动态计算的。rto的值为icsk->icsk_rto的3.5倍。在网络比较好的情况下,rto的值会小于TCP_TIMEWAIT_LEN,从而达到加速的目的;但是如果在网络情况比较差,也就是说客户端和服务器端往返的时间比较长的情况下,rto的值有可能会大于TCP_TIMEWAIT_LEN,这种情况下反而适得其反,这种情况通常是由客户端引起的。所以在设置tcp_tw_recycle的时候要考虑到客户端的情况。
  下面在来看看为什么rto的值要选择为icsk->icsk_rto的3.5倍,也就是RTO*3.5,而不是2倍、4倍呢?我们知道,在FIN_WAIT_2状态下接收到FIN包后,会给对端发送ACK包,完成TCP连接的关闭。但是最后的这个ACK包可能对端没有收到,在过了RTO(超时重传时间)时间后,对端会重新发送FIN包,这时需要再次给对端发送ACK包,所以TIME_WAIT状态的持续时间要保证对端可以重传两次FIN包。如果重传两次的话,TIME_WAIT的时间应该为RTO*(0.5+0.5+0.5)=RTO*1.5,但是这里却是RTO*3.5。这是因为在重传情况下,重传超时时间采用一种称为“指数退避”的方式计算。例如:当重传超时时间为1S的情况下发生了数据重传,我们就用重传超时时间为2S的定时器来重传数据,下一次用4S,一直增加到64S为止(参见tcp_retransmit_timer())。所以这里的RTO*3.5=RTO*0.5+RTO*1+RTO*2,其中RTO*0.5是第一次发送ACK的时间到对端的超时时间(系数就是乘以RTO的值),RTO*1是对端第一次重传FIN包到ACK包到达对端的超时时间,RTO*2是对端第二次重传FIN包到ACK包到达对端的超时时间。注意,重传超时时间的指数退避操作(就是乘以2)是在重传之后执行的,所以第一次重传的超时时间和第一次发送的超时时间相同。整个过程及时间分布如下图所示(注意:箭头虽然指向对端,只是用于描述过程,数据包并未被接收到):

结合内核源码来看如何调整影响TIME_WAIT状态套接字数量的参数_第1张图片
三、tcp_tw_reuse参数
  tcp_tw_reuse参数用来设置是否可以在新的连接中重用TIME_WAIT状态的套接字。注意,重用的是TIME_WAIT套接字占用的端口号,而不是TIME_WAIT套接字的内存等。这个参数对客户端有意义,在主动发起连接的时候会在调用的inet_hash_connect()中会检查是否可以重用TIME_WAIT状态的套接字。如果你在服务器段设置这个参数的话,则没有什么作用,因为服务器端ESTABLISHED状态的套接字和监听套接字的本地IP、端口号是相同的,没有重用的概念。但并不是说服务器端就没有TIME_WAIT状态套接字。
四、tcp_fin_timeout参数
  有些人对这个参数会有误解,认为这个参数是用来设置TIME_WAIT状态持续的时间的。linux的内核文档说的很明白,这个参数是用来设置保持在FIN_WAIT_2状态的时间,原文如下():

tcp_fin_timeout - INTEGER Time to hold socket in state FIN-WAIT-2, if it was closed
    by our side. Peer can be broken and never close its side,
    or even died unexpectedly. Default value is 60sec.
    Usual value used in 2.2 was 180 seconds, you may restore
    it, but remember that if your machine is even underloaded WEB server, you risk to overflow memory with kilotons of dead sockets,
    FIN-WAIT-2 sockets are less dangerous than FIN-WAIT-1,
    because they eat maximum 1.5K of memory, but they tend
    to live longer.    Cf. tcp_max_orphans.
  如果是正常的处理流程就是在FIN_WAIT_2情况下接收到FIN进入到TIME_WAIT的情况,tcp_fin_timeout参数对处于TIME_WAIT状态的时间没有任何影响,但是如果这个参数设的比较小,会缩短从FIN_WAIT_1到TIME_WAIT的时间,从而使连接更早地进入TIME_WAIT状态。状态开始的早,等待相同的时间,结束的也早,客观上也加速了TIME_WAIT状态套接字的清理速度。
  如果在FIN_WAIT_2状态下没有接收到FIN而进入TIME_WAIT状态(FIN_WAIT_2状态超时或者超时时间大小、TCP_LINGER2等影响),在初始进入TIME_WAIT状态时使用的超时时间为tcp_fin_time()计算出来的,tcp_fin_timeout参数的值可能会影响计算结果,取决于TCP_LINGER2选项。但是,要知道,在TIME_WAIT状态下也有两个子状态(inet_timewait_sock的tw_substate成员区分):TIME_WAIT和FIN_WAIT_2。在子状态FIN_WAIT_2状态下接收到FIN包进入子状态TIME_WAIT时,会重置定时器,此时的超时时间要么是3.5*RTO,要么是默认值TCP_TIMEWAIT_LEN(60s),所以严格意义上讲,tcp_fin_timeout参数只是用来设置保持在FIN_WAIT_2状态的时间。
  在FIN_WAIT_2状态下没有接收到FIN包就进入TIME_WAIT的情况下,如果tcp_fin_timeout的值设置的太小,可能会导致TIME_WAIT套接字(子状态为FIN_WAIT_2)过早地被释放,这样对端发送的FIN(短暂地延迟或者本来就是正常的时间到达)到达时就没有办法处理,导致连接不正常关闭,所以tcp_fin_timeout参数的值并不是越小越好,通常设置为30S比较合适。
  通过上面的讨论可以看出,在设置内核参数时一定要慎重,结合自己的业务情况,多看一看文档,弄清楚这些参数都会造成哪些影响,不要盲目地修改。
  文章中如果有错误,请帮忙指正,以免误导他人,多谢!

你可能感兴趣的:(结合内核源码来看如何调整影响TIME_WAIT状态套接字数量的参数)