筆者一直覺得在Linux下TIME_WAIT狀態的Socket持續狀態是60s左右。線上實際卻存在TIME_WAIT超過100s的Socket。因爲這牽涉到最近出現的一個複雜Bug的分析。因此,筆者就去Linux源碼裏面,一探究竟。python
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
提到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
筆者以前一直是相信60秒TIME_WAIT狀態的socket就可以被Kernel回收的。甚至筆者本身作實驗telnet一個端口號,人爲製造TIME_WAIT,本身計時,也是60s左右便可回收。
但在追查一個問題時候,發現,TIME_WAIT有時候可以持續到111s,否則徹底沒法解釋問題的現象。這就逼得筆者不得不推翻本身的結論,從新細細閱讀內核對於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);
好了,讓咱們按下這個核心函數吧。函數
在閱讀源碼前,先看下大體的處理流程。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,很天然的,咱們的處理函數並不會啓用work_queue。同時,還將slot+1,使得在下一個period的時候能夠處理下一個slot。以下圖所示:
若是一個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的限制,應該沒法達到這麼嚴苛的條件。
事實上,以上結論仍是不夠嚴謹。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,更能瞭解股票知識,一箭雙鵰!