筆者一直以為在Linux下TIME_WAIT狀態(tài)的Socket持續狀態(tài)是60s左右。線(xiàn)上實(shí)際卻存在TIME_WAIT超過(guò)100s的Socket。由于這牽涉到最近出現的一個(gè)復雜Bug的分析。所以,筆者就去Linux源碼里面,一探究竟。
TIME_WAIT這個(gè)參數通常和五元組重用扯上關(guān)系。在這里,筆者先給出機器的內核參數設置,以免和其它問(wèn)題相混淆。
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同時(shí)開(kāi)啟導致的問(wèn)題。
提到Socket的TIME_WAIT狀態(tài),不得就不亮出TCP狀態(tài)轉移圖了:
持續時(shí)間就如圖中所示的2MSL。但圖中并沒(méi)有指出2MSL到底是多長(cháng)時(shí)間,但筆者從Linux源碼里面翻到了下面這個(gè)宏定義。
#define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-WAIT * state, about 60 seconds */
如英文字面意思所示,60s后銷(xiāo)毀TIME_WAIT狀態(tài),那么2MSL肯定就是60s嘍?
筆者之前一直是相信60秒TIME_WAIT狀態(tài)的socket就能夠被Kernel回收的。甚至筆者自己做實(shí)驗telnet一個(gè)端口號,人為制造TIME_WAIT,自己計時(shí),也是60s左右即可回收。
但在追查一個(gè)問(wèn)題時(shí)候,發(fā)現,TIME_WAIT有時(shí)候能夠持續到111s,不然完全無(wú)法解釋問(wèn)題的現象。這就逼得筆者不得不推翻自己的結論,重新細細閱讀內核對于TIME_WAIT狀態(tài)處理的源碼。當然,這個(gè)追查的問(wèn)題也會(huì )寫(xiě)成博客分享出來(lái),敬請期待_。
談到TIME_WAIT何時(shí)能夠被回收,不得不談到TIME_WAIT定時(shí)器,這個(gè)就是專(zhuān)門(mén)用來(lái)銷(xiāo)毀到期的TIME_WAIT Socket的。而每一個(gè)Socket進(jìn)入TIME_WAIT時(shí),必然會(huì )經(jīng)過(guò)下面的代碼分支:
tcp_v4_rcv
|->tcp_timewait_state_process
/* 將time_wait狀態(tài)的socket鏈入時(shí)間輪
|->inet_twsk_schedule
由于我們的kernel并沒(méi)有開(kāi)啟tcp_tw_recycle,所以最終的調用為:
/* 這邊TCP_TIMEWAIT_LEN 60 * HZ */ inet_twsk_schedule(tw, &tcp_death_row, TCP_TIMEWAIT_LEN, TCP_TIMEWAIT_LEN);
好了,讓我們按下這個(gè)核心函數吧。
在閱讀源碼前,先看下大致的處理流程。Linux內核是通過(guò)時(shí)間輪來(lái)處理到期的TIME_WAIT socket,如下圖所示:
內核將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) { ...... // 計算時(shí)間輪的slot slot = (timeo + (1 << INET_TWDR_RECYCLE_TICK) - 1) >> INET_TWDR_RECYCLE_TICK; ...... // 慢時(shí)間輪的邏輯,由于沒(méi)有開(kāi)啟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下,這個(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皆為T(mén)CP_TIMEWAIT_LEN。所以,每次剛成為的TIME_WAIT狀態(tài)的socket即將鏈接到當前處理slot最遠的slot(+7)以便處理。如下圖所示:
如果Kernel不停的產(chǎn)生TIME_WAIT,那么整個(gè)slow timer時(shí)間輪就會(huì )如下圖所示:
所有的slot全部掛滿(mǎn)了TIME_WAIT狀態(tài)的Socket。
每次調用inet_twsk_schedule時(shí)候傳入的處理函數都是:
/*參數中的tcp_death_row即為承載時(shí)間輪處理函數的結構體*/ inet_twsk_schedule(tw,&tcp_death_row,TCP_TIMEWAIT_LEN,TCP_TIMEWAIT_LEN) /* 具體的處理結構體 */ struct inet_timewait_death_row tcp_death_row = { ...... /* slow_timer時(shí)間輪處理函數 */ .tw_timer = TIMER_INITIALIZER(inet_twdr_hangman, 0, (unsigned long)&tcp_death_row), /* slow_timer時(shí)間輪輔助處理函數*/ .twkill_work = __WORK_INITIALIZER(tcp_death_row.twkill_work, inet_twdr_twkill_work), /* 短時(shí)間輪處理函數 */ .twcal_timer = TIMER_INITIALIZER(inet_twdr_twcal_tick, 0, (unsigned long)&tcp_death_row), };
由于我們這邊主要考慮的是設置為T(mén)CP_TIMEWAIT_LEN(60s)的處理時(shí)間,所以直接考察slow_timer時(shí)間輪處理函數,也就是inet_twdr_hangman。這個(gè)函數還是比較簡(jiǎn)短的:
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)達到了100個(gè),且還沒(méi)處理完 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? */ // 判斷是否還需要繼續處理 if (twdr->tw_count) need_timer = 1; // 如果當前slot處理完了,才跳轉到下一個(gè)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); }
雖然簡(jiǎn)單,但這個(gè)函數里面有不少細節。第一個(gè)細節,就在inet_twdr_do_twkill_work,為了防止這個(gè)slot的time_wait過(guò)多,卡住當前的流程,其會(huì )在處理完100個(gè)time_wait socket之后就回返回。這個(gè)slot余下的time_wait會(huì )交給Kernel的work_queue機制去處理。
值得注意的是。由于在這個(gè)slow_timer時(shí)間輪判斷里面,根本不判斷精確時(shí)間,直接全部刪除。所以輪到某個(gè)slot,例如到了52.5-60s這個(gè)slot,直接清理52.5-60s的所有time_wait。即使time_wait還沒(méi)有到60s也是如此。而小時(shí)間輪(tw_cal)會(huì )精確的判定時(shí)間,由于篇幅原因,就不在這里細講了。
注: 小時(shí)間輪(tw\_cal)在tcp\_tw\_recycle開(kāi)啟的情況下會(huì )使用
我們假設,一個(gè)時(shí)間輪的數據最多能在一個(gè)slot間隔時(shí)間,也就是(60/8=7.5)內肯定能處理完畢。由于系統有tcp_tw_max_buckets設置,如果設置的比較合理,這個(gè)假設還是比較靠譜的。
注: 這里的60/8為什么需要精確到小數,而不是7。
因為實(shí)際計算的時(shí)候是拿60*HZ進(jìn)行計算,
如果HZ是1024的話(huà),那么period應該是7680,即精度精確到ms級。
所以在本文中計算的時(shí)候需要精確到小數。
如果一個(gè)slot的TIME_WAIT<=100,很自然的,我們的處理函數并不會(huì )啟用work_queue。同時(shí),還將slot+1,使得在下一個(gè)period的時(shí)候可以處理下一個(gè)slot。如下圖所示:
如果一個(gè)slot的TIME_WAIT>100,Kernel會(huì )將余下的任務(wù)交給work_queue處理。同時(shí),slot不變!也即是說(shuō),下一個(gè)period(7.5s后)到達的時(shí)候,還會(huì )處理同樣的slot。按照我們的假設,這時(shí)候slot已經(jīng)處理完畢,那么在第7.5s的時(shí)候才將slot向前推進(jìn)。也就是說(shuō),假設slot一開(kāi)始為0,到真正處理slot 1需要15s!
假設每一個(gè)slot的TIME_WAIT都>100的話(huà),那么每個(gè)slot的處理都需要15s。
對于這種情況,筆者寫(xiě)了個(gè)程序進(jìn)行模擬。
public class TimeWaitSimulator { public static void main(String[] args) { double delta = (60) * 1.0 / 8; // 0表示開(kāi)始清理,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) { // 因為假設進(jìn)入work_queue之后,很快就會(huì )清理完 // 所以在slot為7的時(shí)候并不需要等最后的那個(gè)purge過(guò)程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才應該向前推進(jìn) 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這個(gè)時(shí)間輪的時(shí)候,其實(shí)外面時(shí)間已經(jīng)過(guò)去了112.5s,處理已經(jīng)完全滯后了。不過(guò)由于TIME_WAIT狀態(tài)下的Socket(inet_timewait_sock)所占用內存很少,所以不會(huì )對系統可用資源造成太大的影響。但是,這會(huì )在NAT環(huán)境下造成一個(gè)坑,這也是筆者文章前面提到過(guò)的Bug。
上面的計算如果按照圖和時(shí)間線(xiàn)畫(huà)出來(lái),應該是這么個(gè)情況:
也即TIME_WAIT狀態(tài)的Socket在一個(gè)period(7.5s)內能處理完當前slot的情況下,最多能夠存在112.5s!
如果7.5s內還處理不完,那么響應時(shí)間輪的輪轉還得繼續加上一個(gè)或多個(gè)perod。但在tcp_tw_max_buckets的限制,應該無(wú)法達到這么嚴苛的條件。
事實(shí)上,以上結論還是不夠嚴謹。TIME_WAIT時(shí)間還可以繼續延長(cháng)!看下這段源碼:
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. */ /* 如果有回繞校驗失敗的包到達的情況下,或者其實(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. */ /* 向對端發(fā)送當前time wait狀態(tài)應該返回的ACK */ return TCP_TW_ACK; } inet_twsk_put(tw); /* 注意,這邊通過(guò)paws校驗的包,會(huì )返回tcp_tw_success,使得time_wait狀態(tài)的 * socket五元組也可以三次握手成功重新復用 * / return TCP_TW_SUCCESS; }
上面的邏輯如下圖所示:
注意代碼最后的return TCP_TW_SUCCESS,通過(guò)PAWS校驗的包,會(huì )返回TCP_TW_SUCCESS,使得TIME_WAIT狀態(tài)的Socket(五元組)也可以三次握手成功重新復用!
以上就是分析從Linux源碼看TIME_WAIT的持續時(shí)間的詳細內容,更多關(guān)于Linux源碼 TIME_WAIT持續時(shí)間的資料請關(guān)注腳本之家其它相關(guān)文章!
免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng )、來(lái)自本網(wǎng)站內容采集于網(wǎng)絡(luò )互聯(lián)網(wǎng)轉載等其它媒體和分享為主,內容觀(guān)點(diǎn)不代表本網(wǎng)站立場(chǎng),如侵犯了原作者的版權,請告知一經(jīng)查實(shí),將立刻刪除涉嫌侵權內容,聯(lián)系我們QQ:712375056,同時(shí)歡迎投稿傳遞力量。
Copyright ? 2009-2022 56dr.com. All Rights Reserved. 特網(wǎng)科技 特網(wǎng)云 版權所有 特網(wǎng)科技 粵ICP備16109289號
域名注冊服務(wù)機構:阿里云計算有限公司(萬(wàn)網(wǎng)) 域名服務(wù)機構:煙臺帝思普網(wǎng)絡(luò )科技有限公司(DNSPod) CDN服務(wù):阿里云計算有限公司 百度云 中國互聯(lián)網(wǎng)舉報中心 增值電信業(yè)務(wù)經(jīng)營(yíng)許可證B2
建議您使用Chrome、Firefox、Edge、IE10及以上版本和360等主流瀏覽器瀏覽本網(wǎng)站