从Linux源码看TIME_WAIT状态的持续时间

从Linux源码看TIME_WAIT状态的持续时间

前言

笔者一直觉得在Linux下TIME_WAIT状态的Socket持续状态是60s左右。线上实际却存在TIME_WAIT超过100s的Socket。因为这牵涉到最近出现的一个复杂Bug的分析。因此,笔者就去Linux源码里面,一探究竟。python

首先介绍下Linux环境

TIME_WAIT这个参数一般和五元组重用扯上关系。在这里,笔者先给出机器的内核参数设置,以避免和其它问题相混淆。linux

cat /proc/sys/net/ipv4/tcp_tw_reuse 0
cat /proc/sys/net/ipv4/tcp_tw_recycle 0
cat /proc/sys/net/ipv4/tcp_timestamps 1

能够看到,咱们设置了tcp_tw_recycle为0,这能够避免NAT下tcp_tw_recycle和tcp_timestamps同时开启致使的问题。具体问题能够看笔者的以往博客。app

https://my.oschina.net/alchemystar/blog/3119992

TIME_WAIT状态转移图

提到Socket的TIME_WAIT状态,不得就不亮出TCP状态转移图了:

持续时间就如图中所示的2MSL。但图中并无指出2MSL究竟是多长时间,但笔者从Linux源码里面翻到了下面这个宏定义。less

#define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-WAIT
				  * state, about 60 seconds	*/

如英文字面意思所示,60s后销毁TIME_WAIT状态,那么2MSL确定就是60s喽?dom

持续时间真如TCP_TIMEWAIT_LEN所定义么?

笔者以前一直是相信60秒TIME_WAIT状态的socket就可以被Kernel回收的。甚至笔者本身作实验telnet一个端口号,人为制造TIME_WAIT,本身计时,也是60s左右便可回收。

但在追查一个问题时候,发现,TIME_WAIT有时候可以持续到111s,否则彻底没法解释问题的现象。这就逼得笔者不得不推翻本身的结论,从新细细阅读内核对于TIME_WAIT状态处理的源码。固然,这个追查的问题也会写成博客分享出来,敬请期待_机器学习

TIME_WAIT定时器源码

谈到TIME_WAIT什么时候可以被回收,不得不谈到TIME_WAIT定时器,这个就是专门用来销毁到期的TIME_WAIT Socket的。而每个Socket进入TIME_WAIT时,必然会通过下面的代码分支:socket

tcp_v4_rcv
	|->tcp_timewait_state_process
		/* 将time_wait状态的socket链入时间轮
		|->inet_twsk_schedule

因为咱们的kernel并无开启tcp_tw_recycle,因此最终的调用为:tcp

/* 这边TCP_TIMEWAIT_LEN 60 * HZ */
inet_twsk_schedule(tw, &tcp_death_row, TCP_TIMEWAIT_LEN,
					 TCP_TIMEWAIT_LEN);

好了,让咱们按下这个核心函数吧。函数

inet_twsk_schedule

在阅读源码前,先看下大体的处理流程。Linux内核是经过时间轮来处理到期的TIME_WAIT socket,以下图所示:

内核将60s的时间分为8个slot(INET_TWDR_RECYCLE_SLOTS),每一个slot处理7.5(60/8)范围time_wait状态的socket。学习

void inet_twsk_schedule(struct inet_timewait_sock *tw,struct inet_timewait_death_row *twdr,const int timeo, const int timewait_len)
{
	......
	// 计算时间轮的slot
	slot = (timeo + (1 << INET_TWDR_RECYCLE_TICK) - 1) >> INET_TWDR_RECYCLE_TICK;
	......
	// 慢时间轮的逻辑,因为没有开启TCP\_TW\_RECYCLE,timeo老是60*HZ(60s)
	// 全部都走slow_timer逻辑 
	if (slot >= INET_TWDR_RECYCLE_SLOTS) {
		/* Schedule to slow timer */
		if (timeo >= timewait_len) {
			slot = INET_TWDR_TWKILL_SLOTS - 1;
		} else {
			slot = DIV_ROUND_UP(timeo, twdr->period);
			if (slot >= INET_TWDR_TWKILL_SLOTS)
				slot = INET_TWDR_TWKILL_SLOTS - 1;
		}
		tw->tw_ttd = jiffies + timeo;
		// twdr->slot当前正在处理的slot
		// 在TIME_WAIT_LEN下,这个逻辑通常7
		slot = (twdr->slot + slot) & (INET_TWDR_TWKILL_SLOTS - 1);
		list = &twdr->cells[slot];
	} else{
		// 走短期定时器,因为篇幅缘由,不在这里赘述
		......
	}
	......
	/* twdr->period 60/8=7.5 */
	if (twdr->tw_count++ == 0)
		mod_timer(&twdr->tw_timer, jiffies + twdr->period);
	spin_unlock(&twdr->death_lock);
}

从源码中能够看到,因为咱们传入的timeout皆为TCP_TIMEWAIT_LEN。因此,每次刚成为的TIME_WAIT状态的socket即将连接到当前处理slot最远的slot(+7)以便处理。以下图所示:

若是Kernel不停的产生TIME_WAIT,那么整个slow timer时间轮就会以下图所示:

全部的slot所有挂满了TIME_WAIT状态的Socket。

具体的清理函数

每次调用inet_twsk_schedule时候传入的处理函数都是:

/*参数中的tcp_death_row即为承载时间轮处理函数的结构体*/
inet_twsk_schedule(tw,&tcp_death_row,TCP_TIMEWAIT_LEN,TCP_TIMEWAIT_LEN)
/* 具体的处理结构体 */
struct inet_timewait_death_row tcp_death_row = {
	......
	/* slow_timer时间轮处理函数 */
	.tw_timer	= TIMER_INITIALIZER(inet_twdr_hangman, 0,
					    (unsigned long)&tcp_death_row),
	/* slow_timer时间轮辅助处理函数*/
	.twkill_work	= __WORK_INITIALIZER(tcp_death_row.twkill_work,
					     inet_twdr_twkill_work),
	/* 短期轮处理函数 */
	.twcal_timer	= TIMER_INITIALIZER(inet_twdr_twcal_tick, 0,
					    (unsigned long)&tcp_death_row),				
};

因为咱们这边主要考虑的是设置为TCP_TIMEWAIT_LEN(60s)的处理时间,因此直接考察slow_timer时间轮处理函数,也就是inet_twdr_hangman。这个函数仍是比较简短的:

void inet_twdr_hangman(unsigned long data)
{
	struct inet_timewait_death_row *twdr;
	unsigned int need_timer;

	twdr = (struct inet_timewait_death_row *)data;
	spin_lock(&twdr->death_lock);

	if (twdr->tw_count == 0)
		goto out;

	need_timer = 0;
	// 若是此slot处理的time_wait socket已经达到了100个,且还没处理完
	if (inet_twdr_do_twkill_work(twdr, twdr->slot)) {
		twdr->thread_slots |= (1 << twdr->slot);
		// 将余下的任务交给work queue处理
		schedule_work(&twdr->twkill_work);
		need_timer = 1;
	} else {
		/* We purged the entire slot, anything left?  */
		// 判断是否还须要继续处理
		if (twdr->tw_count)
			need_timer = 1;
		// 若是当前slot处理完了,才跳转到下一个slot
		twdr->slot = ((twdr->slot + 1) & (INET_TWDR_TWKILL_SLOTS - 1));
	}
	// 若是还须要继续处理,则在7.5s后再运行此函数
	if (need_timer)
		mod_timer(&twdr->tw_timer, jiffies + twdr->period);
out:
	spin_unlock(&twdr->death_lock);
}

虽然简单,但这个函数里面有很多细节。第一个细节,就在inet_twdr_do_twkill_work,为了防止这个slot的time_wait过多,卡住当前的流程,其会在处理完100个time_wait socket以后就回返回。这个slot余下的time_wait会交给Kernel的work_queue机制去处理。

值得注意的是。因为在这个slow_timer时间轮判断里面,根本不判断精确时间,直接所有删除。因此轮到某个slot,例如到了52.5-60s这个slot,直接清理52.5-60s的全部time_wait。即便time_wait尚未到60s也是如此。而小时间轮(tw_cal)会精确的断定时间,因为篇幅缘由,就不在这里细讲了。

注: 小时间轮(tw\_cal)在tcp\_tw\_recycle开启的状况下会使用

先做出一个假设

咱们假设,一个时间轮的数据最多能在一个slot间隔时间,也就是(60/8=7.5)内确定能处理完毕。因为系统有tcp_tw_max_buckets设置,若是设置的比较合理,这个假设仍是比较靠谱的。

注: 这里的60/8为何须要精确到小数,而不是7。
由于实际计算的时候是拿60*HZ进行计算,
若是HZ是1024的话,那么period应该是7680,即精度精确到ms级。
因此在本文中计算的时候须要精确到小数。

若是一个slot中的TIME_WAIT<=100

若是一个slot的TIME_WAIT<=100,很天然的,咱们的处理函数并不会启用work_queue。同时,还将slot+1,使得在下一个period的时候能够处理下一个slot。以下图所示:
codegen

若是一个slot中的TIME_WAIT>100

若是一个slot的TIME_WAIT>100,Kernel会将余下的任务交给work_queue处理。同时,slot不变!也便是说,下一个period(7.5s后)到达的时候,还会处理一样的slot。按照咱们的假设,这时候slot已经处理完毕,那么在第7.5s的时候才将slot向前推动。也就是说,假设slot一开始为0,到真正处理slot 1须要15s!

假设每个slot的TIME_WAIT都>100的话,那么每一个slot的处理都须要15s。

对于这种状况,笔者写了个程序进行模拟。

public class TimeWaitSimulator {

    public static void main(String[] args) {
        double delta = (60) * 1.0 / 8;

        // 0表示开始清理,1表示清理完毕
        // 清理完毕以后slot向前推动
        int startPurge = 0;
        double sum = 0;
        int slot = 0;
        while (slot < 8) {
            if (startPurge == 0) {
                sum += delta;
                startPurge = 1;
                if (slot == 7) {
                    // 由于假设进入work_queue以后,很快就会清理完
                    // 因此在slot为7的时候并不须要等最后的那个purge过程7.5s
                    System.out.println("slot " + slot + " has reach the last " + sum);
                    break;
                }
            }
            if (startPurge == 1) {
                sum += delta;
                startPurge = 0;
                System.out.println("slot " + "move to next at time " + sum);
                // 清理完以后,slot才应该向前推动
                slot++;
            }
        }
    }
}

得出结果以下面所示:

slot move to next at time 15.0
slot move to next at time 30.0
slot move to next at time 45.0
slot move to next at time 60.0
slot move to next at time 75.0
slot move to next at time 90.0
slot move to next at time 105.0
slot 7 has reach the last  112.5

也即处理到52.5-60s这个时间轮的时候,其实外面时间已通过去了112.5s,处理已经彻底滞后了。不过因为TIME_WAIT状态下的Socket(inet_timewait_sock)所占用内存不多,因此不会对系统可用资源形成太大的影响。可是,这会在NAT环境下形成一个坑,这也是笔者文章前面提到过的Bug。
上面的计算若是按照图和时间线画出来,应该是这么个状况:

也即TIME_WAIT状态的Socket在一个period(7.5s)内能处理完当前slot的状况下,最多可以存在112.5s!

若是7.5s内还处理不完,那么响应时间轮的轮转还得继续加上一个或多个perod。但在tcp_tw_max_buckets的限制,应该没法达到这么严苛的条件。

PAWS(Protection Against Wrapped Sequences)使得TIME_WAIT延长

事实上,以上结论仍是不够严谨。TIME_WAIT时间还能够继续延长!看下这段源码:

enum tcp_tw_status
tcp_timewait_state_process(struct inet_timewait_sock *tw, struct sk_buff *skb,
			   const struct tcphdr *th)
{
	......
	if (paws_reject)
		NET_INC_STATS_BH(twsk_net(tw), LINUX_MIB_PAWSESTABREJECTED);
		
	if (!th->rst) {
		/* In this case we must reset the TIMEWAIT timer.
		 *
		 * If it is ACKless SYN it may be both old duplicate
		 * and new good SYN with random sequence number <rcv_nxt.
		 * Do not reschedule in the last case.
		 */
		/* 若是有回绕校验失败的包到达的状况下,或者其实ack包 
		 * 重置定时器到新的60s以后
		 * /
		if (paws_reject || th->ack)
			inet_twsk_schedule(tw, &tcp_death_row, TCP_TIMEWAIT_LEN,
					   TCP_TIMEWAIT_LEN);

		/* Send ACK. Note, we do not put the bucket,
		 * it will be released by caller.
		 */
		/* 向对端发送当前time wait状态应该返回的ACK */
		return TCP_TW_ACK;
	}
	inet_twsk_put(tw);
	/* 注意,这边经过paws校验的包,会返回tcp_tw_success,使得time_wait状态的
	 * socket五元组也能够三次握手成功从新复用
	 * /
	return TCP_TW_SUCCESS;
}

上面的逻辑以下图所示:

注意代码最后的return TCP_TW_SUCCESS,经过PAWS校验的包,会返回TCP_TW_SUCCESS,使得TIME_WAIT状态的Socket(五元组)也能够三次握手成功从新复用!
这段逻辑很微妙,会在笔者下一篇<<解Bug之路>>里面进行详解!

总结

若是不仔细分析就下定结论,很容就被本身以前先入为主的一些不够严谨的结论所困扰。致使排查一些复杂问题的时候将思路引导向错误的方向。笔者在追查某个问题的时候就犯了这样的错误。当种种猜想都和事实矛盾时,必须怀疑起本身以前笃定的结论并尝试着推翻它,整个过程即艰辛又快乐!

最后

推荐一本我朋友写的书 《基于股票大数据分析的Python入门实战》,股票范例带领你们入门python数据分析可视化和机器学习,看了之后,不只能学python,更能了解股票知识,一箭双雕!

相关文章
相关标签/搜索