從Linux源碼看TIME_WAIT狀態(tài)的持續(xù)時(shí)間

前言

筆者一直以為在Linux下TIME_WAIT狀態(tài)的Socket持續(xù)狀態(tài)是60s左右央星。線上實(shí)際卻存在TIME_WAIT超過100s的Socket截驮。由于這牽涉到最近出現(xiàn)的一個(gè)復(fù)雜Bug的分析争便。所以,筆者就去Linux源碼里面爽航,一探究竟传藏。

首先介紹下Linux環(huán)境

TIME_WAIT這個(gè)參數(shù)通常和五元組重用扯上關(guān)系。在這里屉更,筆者先給出機(jī)器的內(nèi)核參數(shù)設(shè)置徙融,以免和其它問題相混淆。

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

可以看到偶垮,我們設(shè)置了tcp_tw_recycle為0,這可以避免NAT下tcp_tw_recycle和tcp_timestamps同時(shí)開啟導(dǎo)致的問題张咳。具體問題可以看筆者的以往博客。

https://www.cnblogs.com/alchemystar/p/13801368.html

TIME_WAIT狀態(tài)轉(zhuǎn)移圖

提到Socket的TIME_WAIT狀態(tài)似舵,不得就不亮出TCP狀態(tài)轉(zhuǎn)移圖了:

持續(xù)時(shí)間就如圖中所示的2MSL。但圖中并沒有指出2MSL到底是多長時(shí)間葱峡,但筆者從Linux源碼里面翻到了下面這個(gè)宏定義砚哗。

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

? * state, about 60 seconds */

如英文字面意思所示,60s后銷毀TIME_WAIT狀態(tài)砰奕,那么2MSL肯定就是60s嘍蛛芥?

持續(xù)時(shí)間真如TCP_TIMEWAIT_LEN所定義么?

筆者之前一直是相信60秒TIME_WAIT狀態(tài)的socket就能夠被Kernel回收的。甚至筆者自己做實(shí)驗(yàn)telnet一個(gè)端口號(hào)军援,人為制造TIME_WAIT仅淑,自己計(jì)時(shí),也是60s左右即可回收胸哥。

但在追查一個(gè)問題時(shí)候涯竟,發(fā)現(xiàn),TIME_WAIT有時(shí)候能夠持續(xù)到111s,不然完全無法解釋問題的現(xiàn)象。這就逼得筆者不得不推翻自己的結(jié)論庐船,重新細(xì)細(xì)閱讀內(nèi)核對(duì)于TIME_WAIT狀態(tài)處理的源碼银酬。當(dāng)然,這個(gè)追查的問題也會(huì)寫成博客分享出來筐钟,敬請期待_揩瞪。

TIME_WAIT定時(shí)器源碼

談到TIME_WAIT何時(shí)能夠被回收,不得不談到TIME_WAIT定時(shí)器,這個(gè)就是專門用來銷毀到期的TIME_WAIT Socket的篓冲。而每一個(gè)Socket進(jìn)入TIME_WAIT時(shí)李破,必然會(huì)經(jīng)過下面的代碼分支:

tcp_v4_rcv

|->tcp_timewait_state_process

/* 將time_wait狀態(tài)的socket鏈入時(shí)間輪

|->inet_twsk_schedule

由于我們的kernel并沒有開啟tcp_tw_recycle,所以最終的調(diào)用為:

/* 這邊TCP_TIMEWAIT_LEN 60 * HZ */

inet_twsk_schedule(tw, &tcp_death_row, TCP_TIMEWAIT_LEN,

TCP_TIMEWAIT_LEN);

這里不詳細(xì)講了,感興趣的可以訪問

https://www.xiaoyuani.com/

了解更多好了壹将,讓我們按下這個(gè)核心函數(shù)吧喷屋。

inet_twsk_schedule

在閱讀源碼前,先看下大致的處理流程瞭恰。Linux內(nèi)核是通過時(shí)間輪來處理到期的TIME_WAIT socket屯曹,如下圖所示:

內(nèi)核將60s的時(shí)間分為8個(gè)slot(INET_TWDR_RECYCLE_SLOTS),每個(gè)slot處理7.5(60/8)范圍time_wait狀態(tài)的socket。

void inet_twsk_schedule(struct inet_timewait_sock *tw,struct inet_timewait_death_row *twdr,const int timeo, const int timewait_len)

{

......

// 計(jì)算時(shí)間輪的slot

slot = (timeo + (1 << INET_TWDR_RECYCLE_TICK) - 1) >> INET_TWDR_RECYCLE_TICK;

......

// 慢時(shí)間輪的邏輯惊畏,由于沒有開啟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當(dāng)前正在處理的slot

// 在TIME_WAIT_LEN下恶耽,這個(gè)邏輯一般7

slot = (twdr->slot + slot) & (INET_TWDR_TWKILL_SLOTS - 1);

list = &twdr->cells[slot];

} else{

// 走短時(shí)間定時(shí)器,由于篇幅原因颜启,不在這里贅述

......

}

......

/* 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狀態(tài)的socket即將鏈接到當(dāng)前處理slot最遠(yuǎn)的slot(+7)以便處理涌萤。如下圖所示:

如果Kernel不停的產(chǎn)生TIME_WAIT,那么整個(gè)slow timer時(shí)間輪就會(huì)如下圖所示:

所有的slot全部掛滿了TIME_WAIT狀態(tài)的Socket。

具體的清理函數(shù)

每次調(diào)用inet_twsk_schedule時(shí)候傳入的處理函數(shù)都是:

/*參數(shù)中的tcp_death_row即為承載時(shí)間輪處理函數(shù)的結(jié)構(gòu)體*/

inet_twsk_schedule(tw,&tcp_death_row,TCP_TIMEWAIT_LEN,TCP_TIMEWAIT_LEN)

/* 具體的處理結(jié)構(gòu)體 */

struct inet_timewait_death_row tcp_death_row = {

......

/* slow_timer時(shí)間輪處理函數(shù) */

.tw_timer = TIMER_INITIALIZER(inet_twdr_hangman, 0,

? ? (unsigned long)&tcp_death_row),

/* slow_timer時(shí)間輪輔助處理函數(shù)*/

.twkill_work = __WORK_INITIALIZER(tcp_death_row.twkill_work,

? ? inet_twdr_twkill_work),

/* 短時(shí)間輪處理函數(shù) */

.twcal_timer = TIMER_INITIALIZER(inet_twdr_twcal_tick, 0,

? ? (unsigned long)&tcp_death_row),

};

由于我們這邊主要考慮的是設(shè)置為TCP_TIMEWAIT_LEN(60s)的處理時(shí)間口猜,所以直接考察slow_timer時(shí)間輪處理函數(shù)负溪,也就是inet_twdr_hangman。這個(gè)函數(shù)還是比較簡短的:

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已經(jīng)達(dá)到了100個(gè)济炎,且還沒處理完

if (inet_twdr_do_twkill_work(twdr, twdr->slot)) {

twdr->thread_slots |= (1 << twdr->slot);

// 將余下的任務(wù)交給work queue處理

schedule_work(&twdr->twkill_work);

need_timer = 1;

} else {

/* We purged the entire slot, anything left?? */

// 判斷是否還需要繼續(xù)處理

if (twdr->tw_count)

need_timer = 1;

// 如果當(dāng)前slot處理完了川抡,才跳轉(zhuǎn)到下一個(gè)slot

twdr->slot = ((twdr->slot + 1) & (INET_TWDR_TWKILL_SLOTS - 1));

}

// 如果還需要繼續(xù)處理,則在7.5s后再運(yùn)行此函數(shù)

if (need_timer)

mod_timer(&twdr->tw_timer, jiffies + twdr->period);

out:

spin_unlock(&twdr->death_lock);

}

雖然簡單须尚,但這個(gè)函數(shù)里面有不少細(xì)節(jié)崖堤。第一個(gè)細(xì)節(jié),就在inet_twdr_do_twkill_work,為了防止這個(gè)slot的time_wait過多耐床,卡住當(dāng)前的流程密幔,其會(huì)在處理完100個(gè)time_wait socket之后就回返回。這個(gè)slot余下的time_wait會(huì)交給Kernel的work_queue機(jī)制去處理撩轰。

值得注意的是胯甩。由于在這個(gè)slow_timer時(shí)間輪判斷里面昧廷,根本不判斷精確時(shí)間,直接全部刪除蜡豹。所以輪到某個(gè)slot,例如到了52.5-60s這個(gè)slot麸粮,直接清理52.5-60s的所有time_wait。即使time_wait還沒有到60s也是如此镜廉。而小時(shí)間輪(tw_cal)會(huì)精確的判定時(shí)間,由于篇幅原因弄诲,就不在這里細(xì)講了。

注: 小時(shí)間輪(tw\_cal)在tcp\_tw\_recycle開啟的情況下會(huì)使用

先作出一個(gè)假設(shè)

我們假設(shè)娇唯,一個(gè)時(shí)間輪的數(shù)據(jù)最多能在一個(gè)slot間隔時(shí)間齐遵,也就是(60/8=7.5)內(nèi)肯定能處理完畢。由于系統(tǒng)有tcp_tw_max_buckets設(shè)置,如果設(shè)置的比較合理,這個(gè)假設(shè)還是比較靠譜的塔插。

注: 這里的60/8為什么需要精確到小數(shù)梗摇,而不是7。

因?yàn)閷?shí)際計(jì)算的時(shí)候是拿60*HZ進(jìn)行計(jì)算想许,

如果HZ是1024的話伶授,那么period應(yīng)該是7680,即精度精確到ms級(jí)。

所以在本文中計(jì)算的時(shí)候需要精確到小數(shù)流纹。

如果一個(gè)slot中的TIME_WAIT<=100

如果一個(gè)slot的TIME_WAIT<=100,很自然的糜烹,我們的處理函數(shù)并不會(huì)啟用work_queue。同時(shí)漱凝,還將slot+1,使得在下一個(gè)period的時(shí)候可以處理下一個(gè)slot疮蹦。如下圖所示:

如果一個(gè)slot中的TIME_WAIT>100

如果一個(gè)slot的TIME_WAIT>100,Kernel會(huì)將余下的任務(wù)交給work_queue處理。同時(shí)茸炒,slot不變愕乎!也即是說,下一個(gè)period(7.5s后)到達(dá)的時(shí)候壁公,還會(huì)處理同樣的slot感论。按照我們的假設(shè),這時(shí)候slot已經(jīng)處理完畢贮尖,那么在第7.5s的時(shí)候才將slot向前推進(jìn)笛粘。也就是說,假設(shè)slot一開始為0湿硝,到真正處理slot 1需要15s!

假設(shè)每一個(gè)slot的TIME_WAIT都>100的話,那么每個(gè)slot的處理都需要15s润努。

對(duì)于這種情況关斜,筆者寫了個(gè)程序進(jìn)行模擬。

public class TimeWaitSimulator {

? ? public static void main(String[] args) {

? ? ? ? double delta = (60) * 1.0 / 8;

? ? ? ? // 0表示開始清理铺浇,1表示清理完畢

? ? ? ? // 清理完畢之后slot向前推進(jìn)

? ? ? ? int startPurge = 0;

? ? ? ? double sum = 0;

? ? ? ? int slot = 0;

? ? ? ? while (slot < 8) {

? ? ? ? ? ? if (startPurge == 0) {

? ? ? ? ? ? ? ? sum += delta;

? ? ? ? ? ? ? ? startPurge = 1;

? ? ? ? ? ? ? ? if (slot == 7) {

? ? ? ? ? ? ? ? ? ? // 因?yàn)榧僭O(shè)進(jìn)入work_queue之后痢畜,很快就會(huì)清理完

? ? ? ? ? ? ? ? ? ? // 所以在slot為7的時(shí)候并不需要等最后的那個(gè)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才應(yīng)該向前推進(jìn)

? ? ? ? ? ? ? ? slot++;

? ? ? ? ? ? }

? ? ? ? }

? ? }

}

得出結(jié)果如下面所示:

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這個(gè)時(shí)間輪的時(shí)候,其實(shí)外面時(shí)間已經(jīng)過去了112.5s丁稀,處理已經(jīng)完全滯后了吼拥。不過由于TIME_WAIT狀態(tài)下的Socket(inet_timewait_sock)所占用內(nèi)存很少,所以不會(huì)對(duì)系統(tǒng)可用資源造成太大的影響线衫。但是凿可,這會(huì)在NAT環(huán)境下造成一個(gè)坑,這也是筆者文章前面提到過的Bug授账。

上面的計(jì)算如果按照圖和時(shí)間線畫出來枯跑,應(yīng)該是這么個(gè)情況:

也即TIME_WAIT狀態(tài)的Socket在一個(gè)period(7.5s)內(nèi)能處理完當(dāng)前slot的情況下,最多能夠存在112.5s!

如果7.5s內(nèi)還處理不完白热,那么響應(yīng)時(shí)間輪的輪轉(zhuǎn)還得繼續(xù)加上一個(gè)或多個(gè)perod敛助。但在tcp_tw_max_buckets的限制,應(yīng)該無法達(dá)到這么嚴(yán)苛的條件屋确。

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

事實(shí)上纳击,以上結(jié)論還是不夠嚴(yán)謹(jǐn)。TIME_WAIT時(shí)間還可以繼續(xù)延長攻臀!看下這段源碼:

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.

*/

/* 如果有回繞校驗(yàn)失敗的包到達(dá)的情況下,或者其實(shí)ack包

* 重置定時(shí)器到新的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.

*/

/* 向?qū)Χ税l(fā)送當(dāng)前time wait狀態(tài)應(yīng)該返回的ACK */

return TCP_TW_ACK;

}

inet_twsk_put(tw);

/* 注意焕数,這邊通過paws校驗(yàn)的包,會(huì)返回tcp_tw_success,使得time_wait狀態(tài)的

* socket五元組也可以三次握手成功重新復(fù)用

* /

return TCP_TW_SUCCESS;

}

上面的邏輯如下圖所示:

注意代碼最后的return TCP_TW_SUCCESS,通過PAWS校驗(yàn)的包茵烈,會(huì)返回TCP_TW_SUCCESS,使得TIME_WAIT狀態(tài)的Socket(五元組)也可以三次握手成功重新復(fù)用百匆!

這段邏輯很微妙,會(huì)在筆者下一篇<<解Bug之路>>里面進(jìn)行詳解呜投!

總結(jié)

如果不仔細(xì)分析就下定結(jié)論加匈,很容就被自己之前先入為主的一些不夠嚴(yán)謹(jǐn)?shù)慕Y(jié)論所困擾。導(dǎo)致排查一些復(fù)雜問題的時(shí)候?qū)⑺悸芬龑?dǎo)向錯(cuò)誤的方向仑荐。筆者在追查某個(gè)問題的時(shí)候就犯了這樣的錯(cuò)誤雕拼。當(dāng)種種猜測都和事實(shí)矛盾時(shí),必須懷疑起自己之前篤定的結(jié)論并嘗試著推翻它,整個(gè)過程即艱辛又快樂粘招!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末啥寇,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子洒扎,更是在濱河造成了極大的恐慌辑甜,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件袍冷,死亡現(xiàn)場離奇詭異磷醋,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)胡诗,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門邓线,熙熙樓的掌柜王于貴愁眉苦臉地迎上來淌友,“玉大人,你說我怎么就攤上這事骇陈≌鹜ィ” “怎么了?”我有些...
    開封第一講書人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵你雌,是天一觀的道長器联。 經(jīng)常有香客問我,道長匪蝙,這世上最難降的妖魔是什么主籍? 我笑而不...
    開封第一講書人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮逛球,結(jié)果婚禮上千元,老公的妹妹穿的比我還像新娘。我一直安慰自己颤绕,他們只是感情好幸海,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著奥务,像睡著了一般物独。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上氯葬,一...
    開封第一講書人閱讀 49,007評(píng)論 1 284
  • 那天挡篓,我揣著相機(jī)與錄音,去河邊找鬼帚称。 笑死官研,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的闯睹。 我是一名探鬼主播戏羽,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼楼吃!你這毒婦竟也來了始花?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤孩锡,失蹤者是張志新(化名)和其女友劉穎酷宵,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體躬窜,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡忧吟,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了斩披。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片溜族。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖垦沉,靈堂內(nèi)的尸體忽然破棺而出煌抒,到底是詐尸還是另有隱情,我是刑警寧澤厕倍,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布寡壮,位于F島的核電站,受9級(jí)特大地震影響讹弯,放射性物質(zhì)發(fā)生泄漏况既。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一组民、第九天 我趴在偏房一處隱蔽的房頂上張望棒仍。 院中可真熱鬧,春花似錦臭胜、人聲如沸莫其。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽乱陡。三九已至,卻和暖如春仪壮,著一層夾襖步出監(jiān)牢的瞬間憨颠,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來泰國打工积锅, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留爽彤,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓乏沸,卻偏偏與公主長得像淫茵,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子蹬跃,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345