目录
一、前言
笔者始终以为在Linux下TIME_WAIT状态的Socket持续状态是60s左右。线上实际却存在TIME_WAIT超过100s的Socket。因为这牵扯到近来出现的一个复杂Bug的剖析。所以,笔者就去Linux源码上面,一探究竟。
二、首先介绍下Linux环境
TIME_WAIT这个参数一般和五元组重用扯上关系。在这儿,笔者先给出机器的内核参数设置,以免和其它问题相混淆。
cat/proc/sys/net/ipv4/tcp_tw_reuse0
cat/proc/sys/net/ipv4/tcp_tw_recycle0
cat/proc/sys/net/ipv4/tcp_timestamps1
可以看见,我们设置了tcp_tw_recycle为0,这可以防止NAT下tcp_tw_recycle和tcp_timestamps同时开启造成的问题。
三、TIME_WAIT状态转移图
提及Socket的TIME_WAIT状态,不得就不亮出TCP状态转移图了:
持续时间就如图中所示的2MSL。但图中并没有强调2MSL究竟是多长时间,但笔者从Linux源码上面翻到了下边这个宏定义。
#define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-WAIT * state, about 60 seconds */
如英语字面意思所示,60s后销毁TIME_WAIT状态,这么2MSL肯定就是60s喽?
四、持续时间真如TCP_TIMEWAIT_LEN所定义么?
笔者之前仍然是相信60秒TIME_WAIT状态的socket就能否被Kernel回收的。甚至笔者自己做实验telnet一个端标语,人为制造TIME_WAIT,自己计时,也是60s左右即可回收。
但在追缉一个问题时侯,发觉,TIME_WAIT有时侯才能持续到111s,不然完全没法解释问题的现象。这就逼得笔者不得不推翻自己的推论linux源码结构,重新细细阅读内核对于TIME_WAIT状态处理的源码。其实,这个追缉的问题也会写成博客分享下来,敬请期盼_。
五、TIME_WAIT定时器源码
提到TIME_WAIT何时才能被回收,不得不提到TIME_WAIT定时器,这个就是专门拿来销毁到期的TIME_WAITSocket的。而每一个Socket步入TIME_WAIT时,必然会经过下边的代码分支:
tcp_v4_rcv
|->tcp_timewait_state_process
/*将time_wait状态的socket链入时间轮
|->inet_twsk_schedule
因为我们的kernel并没有开启tcp_tw_recycle,所以最终的调用为:
/* 这边TCP_TIMEWAIT_LEN 60 * HZ */ inet_twsk_schedule(tw, &tcp_death_row, TCP_TIMEWAIT_LEN, TCP_TIMEWAIT_LEN);
好了,让我们按下这个核心函数吧。
5.1、inet_twsk_schedule
在阅读源码前,先看下大致的处理流程。Linux内核是通过时间轮来处理到期的TIME_WAITsocket,如右图所示:
内核将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; ...... // 慢时间轮的逻辑,由于没有开启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,这么整个slowtimer时间轮都会如右图所示:
所有的slot全部挂满了TIME_WAIT状态的Socket。
5.2、具体的清除函数
每次调用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时间轮处理函数linux源码结构,也就是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 <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_waitsocket以后就回返回。这个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开启的情况下会使用
5.3、先做出一个假定
我们假定,一个时间轮的数据最多能在一个slot间隔时间,也就是(60/8=7.5)内肯定能处理完毕。因为系统有tcp_tw_max_buckets设置,假如设置的比较合理,这个假定还是比较靠谱的。
注:这儿的60/8为何须要精确到小数,而不是7。
由于实际估算的时侯是拿60*HZ进行估算,
假如HZ是1024的话,这么period应当是7680,即精度精确到ms级。
所以在本文中估算的时侯须要精确到小数。
5.4、如果一个slot中的TIME_WAIT100,Kernel会将余下的任务交给work_queue处理。同时,slot不变!也即是说,下一个period(7.5s后)抵达的时侯,都会处理同样的slot。根据我们的假定,这时侯slot早已处理完毕,这么在第7.5s的时侯才将slot往前加快。也就是说,假定slot一开始为0,到真正处理slot1须要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++; } } } }
得出结果如下边所示:
slotmovetonextattime15.0
slotmovetonextattime30.0
slotmovetonextattime45.0
slotmovetonextattime60.0
slotmovetonextattime75.0
slotmovetonextattime90.0
slotmovetonextattime105.0
slot7hasreachthelast112.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的限制linux关机命令,应当难以达到如此苛刻的条件。
5.6、PAWS(ProtectionAgainstWrappedSequences)致使TIME_WAIT延长
事实上嵌入式linux论坛,以上推论还是不够严谨。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 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; }
里面的逻辑如右图所示:
注意代码最后的returnTCP_TW_SUCCESS,通过PAWS校准的包,会返回TCP_TW_SUCCESS,致使TIME_WAIT状态的Socket(五元组)也可以三次握手成功重新复用!
以上就是剖析从Linux源码看TIME_WAIT的持续时间的详尽内容,更多关于Linux源码TIME_WAIT持续时间的资料请关注云海天教程其它相关文章!