在前面的基礎篇里表锻,我們對網絡編程涉及到的基礎知識進行了梳理精钮,主要內容包括 C/S 編程模型威鹿、TCP 協議、UDP 協議和本地套接字等內容轨香。在提高篇里忽你,我將結合我的經驗,引導你對 TCP 和 UDP 進行更深入的理解臂容。
學習完提高篇之后科雳,我希望你對如何提高 TCP 及 UDP 程序的健壯性有一個全面清晰的認識,從而為深入理解性能篇打下良好的基礎脓杉。
在前面的基礎篇里糟秘,我們了解了 TCP 四次揮手,在四次揮手的過程中球散,發(fā)起連接斷開的一方會有一段時間處于 TIME_WAIT 的狀態(tài)蚌堵,你知道 TIME_WAIT 是用來做什么的么?在面試和實戰(zhàn)中沛婴,TIME_WAIT 相關的問題始終是繞不過去的一道難題吼畏。下面就請跟隨我,一起找出隱藏在細節(jié)下的魔鬼吧嘁灯。
TIME_WAIT 發(fā)生的場景
讓我們先從一例線上故障說起泻蚊。在一次升級線上應用服務之后,我們發(fā)現該服務的可用性變得時好時壞丑婿,一段時間可以對外提供服務性雄,一段時間突然又不可以没卸,大家都百思不得其解。運維同學登錄到服務所在的主機上秒旋,使用 netstat 命令查看后才發(fā)現约计,主機上有成千上萬處于 TIME_WAIT 狀態(tài)的連接。
經過層層剖析后迁筛,我們發(fā)現罪魁禍首就是 TIME_WAIT煤蚌。為什么呢?我們這個應用服務需要通過發(fā)起 TCP 連接對外提供服務细卧。每個連接會占用一個本地端口尉桩,當在高并發(fā)的情況下,TIME_WAIT 狀態(tài)的連接過多贪庙,多到把本機可用的端口耗盡蜘犁,應用服務對外表現的癥狀,就是不能正常工作了止邮。當過了一段時間之后这橙,處于 TIME_WAIT 的連接被系統(tǒng)回收并關閉后,釋放出本地端口可供使用导披,應用服務對外表現為析恋,可以正常工作。這樣周而復始盛卡,便會出現了一會兒不可以,過一兩分鐘又可以正常工作的現象筑凫。
那么為什么會產生這么多的 TIME_WAIT 連接呢滑沧?
這要從 TCP 的四次揮手說起。我在文稿中放了這樣一張圖巍实。
CP 連接終止時滓技,主機 1 先發(fā)送 FIN 報文,主機 2 進入 CLOSE_WAIT 狀態(tài)棚潦,并發(fā)送一個 ACK 應答令漂,同時,主機 2 通過 read 調用獲得 EOF丸边,并將此結果通知應用程序進行主動關閉操作叠必,發(fā)送 FIN 報文。主機 1 在接收到 FIN 報文后發(fā)送 ACK 應答妹窖,此時主機 1 進入 TIME_WAIT 狀態(tài)纬朝。
主機 1 在 TIME_WAIT 停留持續(xù)時間是固定的,是最長分節(jié)生命期 MSL(maximum segment lifetime)的兩倍骄呼,一般稱之為 2MSL共苛。和大多數 BSD 派生的系統(tǒng)一樣判没,Linux 系統(tǒng)里有一個硬編碼的字段,名稱為TCP_TIMEWAIT_LEN隅茎,其值為 60 秒澄峰。也就是說,Linux 系統(tǒng)停留在 TIME_WAIT 的時間為固定的 60 秒辟犀。
#define TCP_TIMEWAIT_LEN (60*HZ)
/* how long to wait to destroy TIME-WAIT state, about 60 seconds */
過了這個時間之后俏竞,主機 1 就進入 CLOSED 狀態(tài)。為什么是這個時間呢踪蹬?你可以先想一想胞此,稍后我會給出解答。
你一定要記住一點跃捣,只有發(fā)起連接終止的一方會進入 TIME_WAIT 狀態(tài)漱牵。這一點面試的時候經常會被問到。
TIME_WAIT 的作用
你可能會問疚漆,為什么不直接進入 CLOSED 狀態(tài)酣胀,而要停留在 TIME_WAIT 這個狀態(tài)?
這要從兩個方面來說娶聘。
首先闻镶,這樣做是為了確保最后的 ACK 能讓被動關閉方接收,從而幫助其正常關閉丸升。
TCP 在設計的時候铆农,做了充分的容錯性設計,比如狡耻,TCP 假設報文會出錯墩剖,需要重傳。在這里夷狰,如果圖中主機 1 的 ACK 報文沒有傳輸成功岭皂,那么主機 2 就會重新發(fā)送 FIN 報文。
如果主機 1 沒有維護 TIME_WAIT 狀態(tài)沼头,而直接進入 CLOSED 狀態(tài)爷绘,它就失去了當前狀態(tài)的上下文,只能回復一個 RST 操作进倍,從而導致被動關閉方出現錯誤土至。
現在主機 1 知道自己處于 TIME_WAIT 的狀態(tài),就可以在接收到 FIN 報文之后猾昆,重新發(fā)出一個 ACK 報文毙籽,使得主機 2 可以進入正常的 CLOSED 狀態(tài)。
第二個理由和連接“化身”和報文迷走有關系毡庆,為了讓舊連接的重復分節(jié)在網絡中自然消失坑赡。
我們知道烙如,在網絡中,經常會發(fā)生報文經過一段時間才能到達目的地的情況毅否,產生的原因是多種多樣的亚铁,如路由器重啟,鏈路突然出現故障等螟加。如果迷走報文到達時徘溢,發(fā)現 TCP 連接四元組(源 IP,源端口捆探,目的 IP然爆,目的端口)所代表的連接不復存在,那么很簡單黍图,這個報文自然丟棄曾雕。
我們考慮這樣一個場景,在原連接中斷后助被,又重新創(chuàng)建了一個原連接的“化身”剖张,說是化身其實是因為這個連接和原先的連接四元組完全相同,如果迷失報文經過一段時間也到達揩环,那么這個報文會被誤認為是連接“化身”的一個 TCP 分節(jié)搔弄,這樣就會對 TCP 通信產生影響。
所以丰滑,TCP 就設計出了這么一個機制顾犹,經過 2MSL 這個時間,足以讓兩個方向上的分組都被丟棄褒墨,使得原來連接的分組在網絡中都自然消失炫刷,再出現的分組一定都是新化身所產生的。
劃重點貌亭,2MSL 的時間是從主機 1 接收到 FIN 后發(fā)送 ACK 開始計時的;如果在 TIME_WAIT 時間內认臊,因為主機 1 的 ACK 沒有傳輸到主機 2圃庭,主機 1 又接收到了主機 2 重發(fā)的 FIN 報文,那么 2MSL 時間將重新計時失晴。道理很簡單剧腻,因為 2MSL 的時間,目的是為了讓舊連接的所有報文都能自然消亡涂屁,現在主機 1 重新發(fā)送了 ACK 報文书在,自然需要重新計時,以便防止這個 ACK 報文對新可能的連接化身造成干擾拆又。
TIME_WAIT 的危害
過多的 TIME_WAIT 的主要危害有兩種儒旬。
第一是內存資源占用栏账,這個目前看來不是太嚴重,基本可以忽略栈源。
第二是對端口資源的占用挡爵,一個 TCP 連接至少消耗一個本地端口。要知道甚垦,端口資源也是有限的茶鹃,一般可以開啟的端口為 32768~61000 ,也可以通過net.ipv4.ip_local_port_range指定艰亮,如果 TIME_WAIT 狀態(tài)過多闭翩,會導致無法創(chuàng)建新連接。這個也是我們在一開始講到的那個例子迄埃。
如何優(yōu)化 TIME_WAIT疗韵?
在高并發(fā)的情況下,如果我們想對 TIME_WAIT 做一些優(yōu)化调俘,來解決我們一開始提到的例子伶棒,該如何辦呢?
net.ipv4.tcp_max_tw_buckets
一個暴力的方法是通過 sysctl 命令彩库,將系統(tǒng)值調小肤无。這個值默認為 18000,當系統(tǒng)中處于 TIME_WAIT 的連接一旦超過這個值時骇钦,系統(tǒng)就會將所有的 TIME_WAIT 連接狀態(tài)重置宛渐,并且只打印出警告信息。這個方法過于暴力眯搭,而且治標不治本窥翩,帶來的問題遠比解決的問題多,不推薦使用鳞仙。
調低 TCP_TIMEWAIT_LEN寇蚊,重新編譯系統(tǒng)
這個方法是一個不錯的方法,缺點是需要“一點”內核方面的知識棍好,能夠重新編譯內核仗岸。我想這個不是大多數人能接受的方式。
SO_LINGER 的設置
英文單詞“l(fā)inger”的意思為停留借笙,我們可以通過設置套接字選項扒怖,來設置調用 close 或者 shutdown 關閉連接時的行為。
int setsockopt(int sockfd, int level, int optname, const void *optval,
socklen_t optlen);
struct linger {
int l_onoff; /* 0=off, nonzero=on */
int l_linger; /* linger time, POSIX specifies units as seconds */
}
設置 linger 參數有幾種可能:
如果l_onoff為 0业稼,那么關閉本選項盗痒。l_linger的值被忽略,這對應了默認行為低散,close 或 shutdown 立即返回俯邓。如果在套接字發(fā)送緩沖區(qū)中有數據殘留骡楼,系統(tǒng)會將試著把這些數據發(fā)送出去。
如果l_onoff為非 0看成, 且l_linger值也為 0君编,那么調用 close 后,會立該發(fā)送一個 RST 標志給對端川慌,該 TCP 連接將跳過四次揮手吃嘿,也就跳過了 TIME_WAIT 狀態(tài),直接關閉梦重。這種關閉的方式稱為“強行關閉”兑燥。 在這種情況下,排隊數據不會被發(fā)送琴拧,被動關閉方也不知道對端已經徹底斷開降瞳。只有當被動關閉方正阻塞在recv()調用上時,接受到 RST 時蚓胸,會立刻得到一個“connet reset by peer”的異常挣饥。
struct linger so_linger;
so_linger.l_onoff = 1;
so_linger.l_linger = 0;
setsockopt(s,SOL_SOCKET,SO_LINGER, &so_linger,sizeof(so_linger));
如果l_onoff為非 0, 且l_linger的值也非 0沛膳,那么調用 close 后扔枫,調用 close 的線程就將阻塞,直到數據被發(fā)送出去锹安,或者設置的l_linger計時時間到短荐。
第二種可能為跨越 TIME_WAIT 狀態(tài)提供了一個可能,不過是一個非常危險的行為叹哭,不值得提倡忍宋。
net.ipv4.tcp_tw_reuse:更安全的設置
那么 Linux 有沒有提供更安全的選擇呢?
當然有风罩。這就是net.ipv4.tcp_tw_reuse選項糠排。
Linux 系統(tǒng)對于net.ipv4.tcp_tw_reuse的解釋如下:
Allow to reuse TIME-WAIT sockets for new connections when it is safe from protocol viewpoint.
Default value is 0.
It should not be changed without advice/request of technical experts.
這段話的大意是從協議角度理解如果是安全可控的,可以復用處于 TIME_WAIT 的套接字為新的連接所用超升。
那么什么是協議角度理解的安全可控呢入宦?主要有兩點:
只適用于連接發(fā)起方(C/S 模型中的客戶端);
對應的 TIME_WAIT 狀態(tài)的連接創(chuàng)建時間超過 1 秒才可以被復用廓俭。
使用這個選項云石,還有一個前提唉工,需要打開對 TCP 時間戳的支持研乒,即net.ipv4.tcp_timestamps=1(默認即為 1)。
要知道淋硝,TCP 協議也在與時俱進雹熬,RFC 1323 中實現了 TCP 拓展規(guī)范宽菜,以便保證 TCP 的高可用,并引入了新的 TCP 選項竿报,兩個 4 字節(jié)的時間戳字段铅乡,用于記錄 TCP 發(fā)送方的當前時間戳和從對端接收到的最新時間戳。由于引入了時間戳烈菌,我們在前面提到的 2MSL 問題就不復存在了阵幸,因為重復的數據包會因為時間戳過期被自然丟棄
。
總結
在今天的內容里芽世,我講了 TCP 的四次揮手挚赊,重點對 TIME_WAIT 的產生、作用以及優(yōu)化進行了講解济瓢,你需要記住以下三點:
TIME_WAIT 的引入是為了讓 TCP 報文得以自然消失荠割,同時為了讓被動關閉方能夠正常關閉;
不要試圖使用SO_LINGER設置套接字選項旺矾,跳過 TIME_WAIT蔑鹦;
現代 Linux 系統(tǒng)引入了更安全可控的方案,可以幫助我們盡可能地復用 TIME_WAIT 狀態(tài)的連接箕宙。
思考題
最后按照慣例嚎朽,我留兩道思考題,供你消化今天的內容扒吁。
最大分組 MSL 是 TCP 分組在網絡中存活的最長時間火鼻,你知道這個最長時間是如何達成的?換句話說雕崩,是怎么樣的機制魁索,可以保證在 MSL 達到之后,報文就自然消亡了呢盼铁?
答:記錄一個值粗蔚,比如60s,經過一個網關就減去一定短值饶火,值=0的時候網關決定丟棄鹏控;
RFC 1323 引入了 TCP 時間戳,那么這需要在發(fā)送方和接收方之間定義一個統(tǒng)一的時鐘嗎肤寝?
答:不需要当辐。timestamp不需要交互,只是發(fā)送方使用的鲤看。
當機器出現大量的time wait 狀態(tài)缘揪,原因該如何排查?
答:netstat看一下,看看是什么進程找筝,什么端口蹈垢,為什么會有這個現象。