被鵝廠面怕了!
上周有位讀者找我說,他在面試鵝廠的時(shí)候,遇到了這么個(gè)問題:

這個(gè)屬于 TCP 異常斷開連接的場(chǎng)景,這部分內(nèi)容在我的「圖解網(wǎng)絡(luò)」還沒有詳細(xì)介紹過,這次就乘著這次機(jī)會(huì)補(bǔ)一補(bǔ)。

這個(gè)問題有幾個(gè)關(guān)鍵詞:
沒有開啟 keepalive;
一直沒有數(shù)據(jù)交互;
進(jìn)程崩潰;
主機(jī)崩潰;
我們先來認(rèn)識(shí)認(rèn)識(shí)什么是 TCP keepalive 呢?
這東西其實(shí)就是 TCP 的保活機(jī)制,它的工作原理我之前的文章寫過,這里就直接貼下以前的內(nèi)容。

如果兩端的 TCP 連接一直沒有數(shù)據(jù)交互,達(dá)到了觸發(fā) TCP 保活機(jī)制的條件,那么內(nèi)核里的 TCP 協(xié)議棧就會(huì)發(fā)送探測(cè)報(bào)文。
如果對(duì)端程序是正常工作的。當(dāng) TCP 保活的探測(cè)報(bào)文發(fā)送給對(duì)端, 對(duì)端會(huì)正常響應(yīng),這樣 TCP 保活時(shí)間會(huì)被重置,等待下一個(gè) TCP 保活時(shí)間的到來。
如果對(duì)端主機(jī)崩潰,或?qū)Χ擞捎谄渌驅(qū)е聢?bào)文不可達(dá)。當(dāng) TCP 保活的探測(cè)報(bào)文發(fā)送給對(duì)端后,石沉大海,沒有響應(yīng),連續(xù)幾次,達(dá)到保活探測(cè)次數(shù)后,TCP 會(huì)報(bào)告該 TCP 連接已經(jīng)死亡。
所以,TCP 保活機(jī)制可以在雙方?jīng)]有數(shù)據(jù)交互的情況,通過探測(cè)報(bào)文,來確定對(duì)方的 TCP 連接是否存活。

注意,應(yīng)用程序若想使用 TCP 保活機(jī)制需要通過 socket 接口設(shè)置 SO_KEEPALIVE 選項(xiàng)才能夠生效,如果沒有設(shè)置,那么就無法使用 TCP 保活機(jī)制。
知道了 TCP keepalive 作用,我們?cè)倩剡^頭看題目中的「主機(jī)崩潰」這種情況。
在沒有開啟 TCP keepalive,且雙方一直沒有數(shù)據(jù)交互的情況下,如果客戶端的「主機(jī)崩潰」了,會(huì)發(fā)生什么。
如果客戶端主機(jī)崩潰了,服務(wù)端是無法感知到的,在加上服務(wù)端沒有開啟 TCP keepalive,又沒有數(shù)據(jù)交互的情況下,服務(wù)端的 TCP 連接將會(huì)一直處于 ESTABLISHED 連接狀態(tài),直到服務(wù)端重啟進(jìn)程。
所以,我們可以得知一個(gè)點(diǎn)。
在沒有使用 TCP 保活機(jī)制,且雙方不傳輸數(shù)據(jù)的情況下,一方的 TCP 連接處在 ESTABLISHED 狀態(tài)時(shí),并不代表另一方的 TCP 連接還一定是正常的。
那題目中的「進(jìn)程崩潰」的情況呢?
我自己做了個(gè)實(shí)驗(yàn),使用 kill -9 來模擬進(jìn)程崩潰的情況,發(fā)現(xiàn)在 kill 掉進(jìn)程后,服務(wù)端會(huì)發(fā)送 FIN 報(bào)文,與客戶端進(jìn)行四次揮手。
所以,即使沒有開啟 TCP keepalive,且雙方也沒有數(shù)據(jù)交互的情況下,如果其中一方的進(jìn)程發(fā)生了崩潰,這個(gè)過程操作系統(tǒng)是可以感知的到的,于是就會(huì)發(fā)送 FIN 報(bào)文給對(duì)方,然后與對(duì)方進(jìn)行 TCP 四次揮手。
以上就是對(duì)這個(gè)面試題的回答。
這面試題其實(shí)在變相考察 TCP 保活機(jī)制的作用。

接下來我們看看在「有數(shù)據(jù)傳輸」的場(chǎng)景下的一些異常情況:
第一種,客戶端主機(jī)宕機(jī),又迅速重啟,會(huì)發(fā)生什么?
第二種,客戶端主機(jī)宕機(jī),一直沒有重啟,會(huì)發(fā)生什么?
客戶端主機(jī)宕機(jī),又迅速重啟
在客戶端主機(jī)宕機(jī)后,服務(wù)端向客戶端發(fā)送的報(bào)文會(huì)得不到任何的響應(yīng),在一定時(shí)長后,服務(wù)端就會(huì)觸發(fā)超時(shí)重傳機(jī)制,重傳未得到響應(yīng)的報(bào)文。
服務(wù)端重傳報(bào)文的過程中,剛好客戶端主機(jī)重啟完成,這時(shí)客戶端的內(nèi)核就會(huì)接收重傳的報(bào)文,:
如果客戶端主機(jī)上沒有進(jìn)程監(jiān)聽該 TCP 報(bào)文的目標(biāo)端口號(hào),由于找不到目標(biāo)端口,客戶端內(nèi)核就會(huì)回復(fù) RST 報(bào)文,重置該 TCP 連接;
如果客戶端主機(jī)上有進(jìn)程監(jiān)聽該 TCP 報(bào)文的目標(biāo)端口號(hào),由于客戶端主機(jī)重啟后,之前的 TCP 連接的數(shù)據(jù)結(jié)構(gòu)已經(jīng)丟失了,客戶端內(nèi)核里協(xié)議棧會(huì)發(fā)現(xiàn)找不到該 TCP 連接的 socket 結(jié)構(gòu)體,于是就會(huì)回復(fù) RST 報(bào)文,重置該 TCP 連接。
所以,只要有一方重啟完成后,收到之前 TCP 連接的報(bào)文,都會(huì)回復(fù) RST 報(bào)文,以斷開連接。
客戶端主機(jī)宕機(jī),一直沒有重啟
這種情況,服務(wù)端超時(shí)重傳報(bào)文的次數(shù)達(dá)到一定閾值后,內(nèi)核就會(huì)判定出該 TCP 有問題,然后通過 Socket 接口告訴應(yīng)用程序該 TCP 連接出問題了。
那具體重傳幾次呢?
在 Linux 系統(tǒng)中,提供了一個(gè)叫 tcp_retries2 配置項(xiàng),默認(rèn)值是 15,如下圖:

這個(gè)內(nèi)核參數(shù)是控制,在 TCP 連接建立的情況下,超時(shí)重傳的最大次數(shù)。
不過 tcp_retries2 設(shè)置了 15 次,并不代表 TCP 超時(shí)重傳了 15 次才會(huì)通知應(yīng)用程序終止該 TCP 連接,內(nèi)核還會(huì)基于「最大超時(shí)時(shí)間」來判定。
每一輪的超時(shí)時(shí)間都是倍數(shù)增長的,比如第一次觸發(fā)超時(shí)重傳是在 2s 后,第二次則是在 4s 后,第三次則是 8s 后,以此類推。

內(nèi)核會(huì)根據(jù) tcp_retries2 設(shè)置的值,計(jì)算出一個(gè)最大超時(shí)時(shí)間。

在重傳報(bào)文且一直沒有收到對(duì)方響應(yīng)的情況時(shí),先達(dá)到「最大重傳次數(shù)」或者「最大超時(shí)時(shí)間」這兩個(gè)的其中一個(gè)條件后,就會(huì)停止重傳。
最后說句。
TCP 牛逼,啥異常都考慮到了
