機(jī):從協(xié)議原理到Linux內(nèi)核實(shí)現(xiàn)與故障排查)
1. 先搞清楚TCP狀態(tài)機(jī)到底在解決什么問(wèn)題如果你寫(xiě)過(guò)網(wǎng)絡(luò)應(yīng)用或者排查過(guò)連接超時(shí)、端口占用、連接數(shù)過(guò)多的問(wèn)題那你一定遇到過(guò)ESTABLISHED、TIME_WAIT、CLOSE_WAIT這些狀態(tài)。很多人知道這些名詞但一到線上出問(wèn)題比如服務(wù)器出現(xiàn)大量CLOSE_WAIT導(dǎo)致無(wú)法新建連接或者TIME_WAIT過(guò)多占滿端口就不知道從何下手。TCP狀態(tài)機(jī)就是用來(lái)精確描述一個(gè)TCP連接從建立到銷毀中間所經(jīng)歷的所有可能狀態(tài)以及狀態(tài)之間轉(zhuǎn)換規(guī)則的模型。它不是一個(gè)可以“下載”的獨(dú)立軟件或庫(kù)而是TCP協(xié)議實(shí)現(xiàn)比如Linux內(nèi)核中的TCP/IP協(xié)議棧必須嚴(yán)格遵循的一套核心邏輯。理解它你就能精準(zhǔn)定位網(wǎng)絡(luò)問(wèn)題看到一個(gè)連接狀態(tài)立刻知道它卡在了握手、數(shù)據(jù)傳輸還是揮手環(huán)節(jié)該查服務(wù)端還是客戶端。理解內(nèi)核參數(shù)調(diào)優(yōu)為什么tcp_fin_timeout可以調(diào)整TIME_WAIT的時(shí)長(zhǎng)為什么tcp_tw_reuse和tcp_tw_recycle后者已廢棄能影響端口復(fù)用這些參數(shù)都是在影響狀態(tài)機(jī)的行為。設(shè)計(jì)更健壯的網(wǎng)絡(luò)程序知道什么時(shí)候該優(yōu)雅關(guān)閉發(fā)送FIN而不是粗暴close知道連接池里的連接處于什么狀態(tài)才是真正可用的。所以這篇文章不是給你列一遍11種狀態(tài)的名稱就完了。我會(huì)結(jié)合Linux內(nèi)核以5.x版本為例的視角帶你走一遍數(shù)據(jù)包如何驅(qū)動(dòng)狀態(tài)變遷并告訴你每個(gè)關(guān)鍵狀態(tài)在netstat或ss命令里出現(xiàn)時(shí)對(duì)應(yīng)的實(shí)際場(chǎng)景和排查方向。最終目標(biāo)是讓你下次看到CLOSE_WAIT時(shí)能立刻反應(yīng)出是應(yīng)用程序沒(méi)有調(diào)用close而不是去重啟網(wǎng)絡(luò)服務(wù)。2. 狀態(tài)機(jī)的核心一張圖與兩個(gè)視角所有關(guān)于TCP狀態(tài)機(jī)的討論都繞不開(kāi)那張經(jīng)典的狀態(tài)轉(zhuǎn)換圖。但死記硬背那張圖沒(méi)用關(guān)鍵是要建立兩個(gè)視角協(xié)議視角這是RFC標(biāo)準(zhǔn)定義的理想化的狀態(tài)轉(zhuǎn)換。它定義了SYN,ACK,FIN,RST這些標(biāo)志位如何驅(qū)動(dòng)狀態(tài)變化。內(nèi)核實(shí)現(xiàn)視角這是Linux內(nèi)核或其他操作系統(tǒng)如何具體實(shí)現(xiàn)這個(gè)狀態(tài)機(jī)包括定時(shí)器、隊(duì)列、系統(tǒng)調(diào)用如connect,accept,close如何與協(xié)議狀態(tài)交互。我們重點(diǎn)關(guān)注內(nèi)核視角因?yàn)檫@才是你通過(guò)命令能觀測(cè)到的、能調(diào)參的真實(shí)世界。2.1 協(xié)議視角下的狀態(tài)清單先快速過(guò)一遍協(xié)議定義的11個(gè)標(biāo)準(zhǔn)狀態(tài)方便后續(xù)對(duì)照LISTEN服務(wù)器端調(diào)用listen()后等待客戶端連接。SYN-SENT客戶端調(diào)用connect()發(fā)送SYN后等待對(duì)端SYN-ACK。SYN-RECEIVED服務(wù)器收到SYN并回復(fù)SYN-ACK后等待客戶端的ACK。ESTABLISHED連接建立成功可以雙向傳輸數(shù)據(jù)。FIN-WAIT-1主動(dòng)關(guān)閉方先調(diào)用close()的一方發(fā)送FIN后進(jìn)入。FIN-WAIT-2主動(dòng)關(guān)閉方收到對(duì)端對(duì)FIN的ACK后進(jìn)入等待對(duì)端的FIN。CLOSE-WAIT被動(dòng)關(guān)閉方收到對(duì)端的FIN并回復(fù)ACK后進(jìn)入等待本地上層應(yīng)用調(diào)用close()。CLOSING一種罕見(jiàn)情況雙方幾乎同時(shí)發(fā)送FIN并都進(jìn)入了FIN-WAIT-1然后都收到了對(duì)方的FIN。LAST-ACK被動(dòng)關(guān)閉方調(diào)用close()發(fā)送自己的FIN后等待對(duì)方對(duì)這個(gè)FIN的ACK。TIME-WAIT主動(dòng)關(guān)閉方收到被動(dòng)方的FIN并回復(fù)ACK后進(jìn)入。這個(gè)狀態(tài)會(huì)持續(xù)2MSLMaximum Segment Lifetime報(bào)文最大生存時(shí)間。CLOSED連接完全關(guān)閉。2.2 內(nèi)核視角狀態(tài)是如何被驅(qū)動(dòng)的在Linux內(nèi)核中TCP狀態(tài)機(jī)不是一個(gè)孤立的模塊。它緊密耦合在協(xié)議棧處理網(wǎng)絡(luò)數(shù)據(jù)包的流程里。簡(jiǎn)單來(lái)說(shuō)驅(qū)動(dòng)狀態(tài)變遷的核心引擎是接收數(shù)據(jù)包處理路徑網(wǎng)卡驅(qū)動(dòng) - IP層 - TCP層。在TCP層會(huì)根據(jù)當(dāng)前連接的狀態(tài)存儲(chǔ)在struct sock中和收到的TCP標(biāo)志位調(diào)用對(duì)應(yīng)的狀態(tài)處理函數(shù)如tcp_rcv_state_process。本地系統(tǒng)調(diào)用當(dāng)應(yīng)用程序調(diào)用connect(),accept(),close(),shutdown()時(shí)內(nèi)核會(huì)生成相應(yīng)的TCP段如SYN, FIN并發(fā)送同時(shí)更新本地連接狀態(tài)。定時(shí)器每個(gè)TCP連接有多個(gè)定時(shí)器重傳、?;?、TIME_WAIT等。超時(shí)事件會(huì)強(qiáng)制觸發(fā)狀態(tài)變遷例如重傳超時(shí)可能導(dǎo)致連接重置RST。舉個(gè)例子當(dāng)內(nèi)核為一個(gè)處于ESTABLISHED狀態(tài)的連接收到一個(gè)FIN包時(shí)它會(huì)確認(rèn)這個(gè)FIN的序列號(hào)有效?;貜?fù)一個(gè)ACK。將連接狀態(tài)從ESTABLISHED改為CLOSE_WAIT。通知上層應(yīng)用程序通過(guò)socket可讀事件告知“對(duì)端已經(jīng)關(guān)閉了發(fā)送通道”。此時(shí)如果你用ss -antp命令查看就會(huì)看到這個(gè)連接的狀態(tài)是CLOSE-WAIT。3. 從三次握手到數(shù)據(jù)傳輸狀態(tài)變遷實(shí)戰(zhàn)拆解我們以一次完整的客戶端-服務(wù)器通信為例結(jié)合strace和tcpdump的視角看看狀態(tài)如何一步步變化。3.1 連接建立三次握手假設(shè)服務(wù)器在 8080 端口監(jiān)聽(tīng)。步驟1服務(wù)器啟動(dòng)監(jiān)聽(tīng)# 服務(wù)器程序調(diào)用 socket() - bind() - listen()此時(shí)服務(wù)器監(jiān)聽(tīng) socket 的狀態(tài)是LISTEN。用ss -lnt可以看到State Recv-Q Send-Q Local Address:Port Peer Address:Port LISTEN 0 128 *:8080 *:*步驟2客戶端發(fā)起連接# 客戶端程序調(diào)用 socket() - connect(server_ip:8080)內(nèi)核為這個(gè)新socket生成一個(gè)SYN包發(fā)送出去??蛻舳藄ocket狀態(tài)變?yōu)镾YN-SENT。tcpdump抓包能看到[S]標(biāo)志。步驟3服務(wù)器響應(yīng)SYN服務(wù)器內(nèi)核收到SYN包。創(chuàng)建一個(gè)新的socket用于這個(gè)連接狀態(tài)置為SYN-RECEIVED。回復(fù) SYN-ACK。此時(shí)這個(gè)新socket在服務(wù)器端還不完全是ESTABLISHED它處在半連接隊(duì)列syn queue中。步驟4客戶端完成握手客戶端收到SYN-ACK狀態(tài)從SYN-SENT變?yōu)镋STABLISHED?;貜?fù)ACK給服務(wù)器??蛻舳说腸onnect()系統(tǒng)調(diào)用成功返回。步驟5服務(wù)器接受連接服務(wù)器收到ACK。將socket狀態(tài)從SYN-RECEIVED變?yōu)镋STABLISHED。將該socket從半連接隊(duì)列移到全連接隊(duì)列accept queue。服務(wù)器應(yīng)用程序調(diào)用accept()會(huì)從這個(gè)全連接隊(duì)列中取出socket返回給應(yīng)用層使用。關(guān)鍵排查點(diǎn)如果服務(wù)器accept()很慢全連接隊(duì)列滿了客戶端可能會(huì)卡住。此時(shí)用ss -lnt看監(jiān)聽(tīng)端口的Send-Q全連接隊(duì)列當(dāng)前長(zhǎng)度會(huì)很大。相關(guān)內(nèi)核參數(shù)是net.core.somaxconn和listen()調(diào)用時(shí)的backlog參數(shù)。3.2 數(shù)據(jù)傳輸ESTABLISHED連接建立后雙方進(jìn)入ESTABLISHED狀態(tài)。這是最??吹降臓顟B(tài)。此時(shí)send()和recv()調(diào)用都是在操作內(nèi)核的發(fā)送和接收緩沖區(qū)由內(nèi)核負(fù)責(zé)組包、確認(rèn)、重傳。這個(gè)階段的狀態(tài)機(jī)邏輯相對(duì)簡(jiǎn)單主要處理數(shù)據(jù)確認(rèn)和窗口管理。但資源監(jiān)控很重要ss -ant可以看到大量ESTABLISHED連接。通過(guò)/proc/net/sockstat或ss -s可以查看總的TCP socket內(nèi)存使用情況。如果應(yīng)用不讀取數(shù)據(jù)接收緩沖區(qū)滿會(huì)導(dǎo)致對(duì)方的發(fā)送窗口為0傳輸暫停。3.3 連接關(guān)閉四次揮手這是狀態(tài)機(jī)最復(fù)雜也最容易出問(wèn)題的部分。我們假設(shè)客戶端先調(diào)用close()。步驟1客戶端主動(dòng)關(guān)閉# 客戶端程序調(diào)用 close(fd); // 或 shutdown(SHUT_WR)客戶端內(nèi)核發(fā)送一個(gè)FIN包??蛻舳藄ocket狀態(tài)從ESTABLISHED變?yōu)镕IN-WAIT-1。步驟2服務(wù)器確認(rèn)FIN服務(wù)器內(nèi)核收到FIN知道客戶端不再發(fā)送數(shù)據(jù)?;貜?fù)一個(gè)ACK。服務(wù)器socket狀態(tài)從ESTABLISHED變?yōu)镃LOSE-WAIT。這是第一個(gè)關(guān)鍵故障點(diǎn)如果服務(wù)器應(yīng)用程序因?yàn)锽UG死鎖、阻塞、邏輯錯(cuò)誤沒(méi)有及時(shí)調(diào)用close()來(lái)關(guān)閉這個(gè)socket這個(gè)連接就會(huì)一直停留在CLOSE-WAIT狀態(tài)。積累多了會(huì)耗盡服務(wù)器文件描述符導(dǎo)致無(wú)法新建連接。用ss -ant看到大量CLOSE-WAIT基本可以斷定是服務(wù)器程序的問(wèn)題。步驟3客戶端收到ACK客戶端收到對(duì)FIN的ACK。狀態(tài)從FIN-WAIT-1變?yōu)镕IN-WAIT-2。此時(shí)客戶端到服務(wù)器的單向通道已關(guān)閉但還可以接收服務(wù)器發(fā)來(lái)的數(shù)據(jù)。步驟4服務(wù)器被動(dòng)關(guān)閉服務(wù)器應(yīng)用程序終于調(diào)用close()。服務(wù)器內(nèi)核發(fā)送自己的FIN包。服務(wù)器socket狀態(tài)從CLOSE-WAIT變?yōu)長(zhǎng)AST-ACK。步驟5客戶端確認(rèn)服務(wù)器的FIN客戶端收到服務(wù)器的FIN?;貜?fù)ACK??蛻舳藸顟B(tài)從FIN-WAIT-2變?yōu)門(mén)IME-WAIT。步驟6服務(wù)器收到最終ACK服務(wù)器收到ACK。服務(wù)器socket狀態(tài)從LAST-ACK變?yōu)镃LOSED并釋放資源。步驟7客戶端等待2MSL后關(guān)閉客戶端在TIME-WAIT狀態(tài)需要等待 2MSL默認(rèn)60秒由net.ipv4.tcp_fin_timeout影響但更準(zhǔn)確地說(shuō)TIME-WAIT的時(shí)長(zhǎng)是固定的tcp_tw_timeout實(shí)際上現(xiàn)代Linux內(nèi)核中TIME-WAIT狀態(tài)由專門(mén)的tw_bucket管理其超時(shí)時(shí)間通常是TCP_TIMEWAIT_LEN(60秒)不受tcp_fin_timeout直接影響。tcp_fin_timeout控制的是FIN-WAIT-2狀態(tài)的超時(shí)。等待的目的是1. 確保最后一個(gè)ACK能到達(dá)服務(wù)器如果丟失服務(wù)器會(huì)重傳FIN。2. 讓本次連接的所有報(bào)文都在網(wǎng)絡(luò)中消失避免影響后續(xù)使用相同四元組源IP、源端口、目的IP、目的端口的新連接。超時(shí)后客戶端socket狀態(tài)變?yōu)镃LOSED資源釋放。關(guān)鍵排查點(diǎn)TIME-WAIT狀態(tài)本身是正常的但高并發(fā)短連接服務(wù)如HTTP服務(wù)器可能會(huì)在短時(shí)間內(nèi)產(chǎn)生大量TIME-WAIT占用大量端口和內(nèi)存。此時(shí)可以考慮開(kāi)啟net.ipv4.tcp_tw_reuse允許將TIME-WAITsocket重新用于新的出向連接需同時(shí)開(kāi)啟tcp_timestamps。切勿再使用已廢棄且危險(xiǎn)的net.ipv4.tcp_tw_recycle。使用連接池減少短連接。讓客戶端承擔(dān)關(guān)閉連接的責(zé)任作為主動(dòng)關(guān)閉方將TIME-WAIT分散到大量客戶端。4. 通過(guò)內(nèi)核日志和工具觀察狀態(tài)機(jī)理論懂了怎么驗(yàn)證除了用netstat或ss我們還可以深入內(nèi)核。4.1 使用ss命令替代netstatss(socket statistics) 是更現(xiàn)代、更快的工具來(lái)自iproute2包。# 查看所有TCP連接 ss -ant # 查看監(jiān)聽(tīng)端口 ss -lnt # 查看進(jìn)程和連接關(guān)系 ss -antp # 查看指定狀態(tài)如TIME-WAIT的連接 ss -ant state time-wait4.2 開(kāi)啟內(nèi)核動(dòng)態(tài)追蹤需要root對(duì)于更底層的問(wèn)題可以開(kāi)啟內(nèi)核的TCP調(diào)試日志。這會(huì)產(chǎn)生大量輸出僅用于臨時(shí)調(diào)試。# 開(kāi)啟所有TCP事件的調(diào)試信息非常詳細(xì) echo 1 /proc/sys/net/ipv4/tcp_debug # 然后使用 dmesg -w 或 tail -f /var/log/kern.log 查看日志 # 你會(huì)看到類似 TCP: xxx [State] ... 的信息記錄了狀態(tài)變遷和數(shù)據(jù)包處理。 # 調(diào)試完成后務(wù)必關(guān)閉 echo 0 /proc/sys/net/ipv4/tcp_debug更精細(xì)的控制可以通過(guò)sysctl設(shè)置net.ipv4.tcp_*系列參數(shù)例如調(diào)整重傳次數(shù)、超時(shí)時(shí)間等這些參數(shù)直接影響狀態(tài)機(jī)中定時(shí)器的行為。4.3 狀態(tài)異常排查清單當(dāng)網(wǎng)絡(luò)連接出現(xiàn)異常時(shí)可以按以下順序結(jié)合狀態(tài)機(jī)進(jìn)行排查連接建立失敗現(xiàn)象connect()超時(shí)或拒絕。查客戶端ss -ant看是否有大量SYN-SENT可能是網(wǎng)絡(luò)不通、防火墻攔截、服務(wù)器端口未監(jiān)聽(tīng)。查服務(wù)器ss -lnt確認(rèn)端口在LISTEN。dmesg看是否有syn flood或listen queue overflow日志檢查net.ipv4.tcp_max_syn_backlog半連接隊(duì)列和net.core.somaxconn全連接隊(duì)列大小。大量CLOSE-WAIT現(xiàn)象服務(wù)器負(fù)載不高但無(wú)法新建連接ss顯示大量CLOSE-WAIT。結(jié)論幾乎肯定是服務(wù)器應(yīng)用程序BUG。應(yīng)用沒(méi)有對(duì)檢測(cè)到對(duì)端關(guān)閉的socket調(diào)用close()。行動(dòng)用ss -antp找到持有這些socket的進(jìn)程PID審查其代碼的socket關(guān)閉邏輯尤其是異常處理分支。大量TIME-WAIT現(xiàn)象壓測(cè)后客戶端或服務(wù)器出現(xiàn)大量TIME-WAIT。判斷這是正常協(xié)議行為。如果影響新連接建立考慮調(diào)整net.ipv4.tcp_tw_reuse僅對(duì)客戶端有效或優(yōu)化架構(gòu)使用長(zhǎng)連接。連接卡在FIN-WAIT-1或FIN-WAIT-2FIN-WAIT-1長(zhǎng)時(shí)間存在對(duì)端沒(méi)有回復(fù)ACK。可能是對(duì)端程序崩潰、網(wǎng)絡(luò)問(wèn)題或防火墻丟棄了ACK。FIN-WAIT-2長(zhǎng)時(shí)間存在對(duì)端沒(méi)有發(fā)送FIN??赡苁菍?duì)端程序忘了關(guān)閉連接或者正在發(fā)送剩余數(shù)據(jù)。內(nèi)核參數(shù)net.ipv4.tcp_fin_timeout定義了FIN-WAIT-2狀態(tài)的超時(shí)時(shí)間默認(rèn)60秒超時(shí)后內(nèi)核會(huì)強(qiáng)制關(guān)閉連接。收到RST復(fù)位包狀態(tài)機(jī)遇到RST會(huì)直接跳到CLOSED。產(chǎn)生RST的原因可能是向一個(gè)未監(jiān)聽(tīng)的端口發(fā)起連接、連接已關(guān)閉后仍收到數(shù)據(jù)、程序崩潰導(dǎo)致socket未正常關(guān)閉等。抓包tcpdump看到[R]標(biāo)志可以幫助定位問(wèn)題端。5. 狀態(tài)機(jī)與高性能網(wǎng)絡(luò)編程理解狀態(tài)機(jī)對(duì)編程有直接指導(dǎo)意義。5.1 優(yōu)雅關(guān)閉Graceful Shutdown粗暴地調(diào)用close()會(huì)立即發(fā)送RST丟棄緩沖區(qū)數(shù)據(jù)。優(yōu)雅關(guān)閉使用shutdown()// 告知對(duì)端“我發(fā)完了”但還可以收 shutdown(sockfd, SHUT_WR); // 然后繼續(xù)讀取對(duì)端可能發(fā)來(lái)的剩余數(shù)據(jù) while (read(sockfd, buffer, sizeof(buffer)) 0) { /* ... */ } // 最后再close close(sockfd);這個(gè)過(guò)程清晰地對(duì)應(yīng)了狀態(tài)機(jī)SHUT_WR發(fā)送FIN進(jìn)入FIN-WAIT-1讀完數(shù)據(jù)后close()完成最終清理。5.2 連接池健康檢查連接池里的連接不能只看socket是否可寫(xiě)最好能驗(yàn)證其TCP狀態(tài)。一個(gè)簡(jiǎn)單的方法是發(fā)送一個(gè)0字節(jié)的TCP?;钐綔y(cè)如果開(kāi)啟了SO_KEEPALIVE或者應(yīng)用層心跳。一個(gè)處于ESTABLISHED狀態(tài)但實(shí)際網(wǎng)絡(luò)已斷開(kāi)的“僵死”連接會(huì)在下次讀寫(xiě)時(shí)失敗。更高級(jí)的做法是定期用getsockopt(fd, SOL_SOCKET, SO_ERROR, ...)檢查socket錯(cuò)誤。5.3 理解“端口占用”問(wèn)題重啟服務(wù)時(shí)提示“Address already in use”通常是因?yàn)樵B接還處于TIME-WAIT狀態(tài)。設(shè)置socket選項(xiàng)SO_REUSEADDR可以允許綁定處于TIME-WAIT狀態(tài)的地址這對(duì)服務(wù)器重啟非常有用。而SO_REUSEPORT則允許多個(gè)socket綁定到相同的IP地址和端口用于多進(jìn)程服務(wù)器。6. 總結(jié)把狀態(tài)機(jī)變成你的排查直覺(jué)TCP狀態(tài)機(jī)不是抽象理論而是內(nèi)嵌在每一次connect()、accept()、read()、write()、close()調(diào)用背后的精確規(guī)則。我建議你把排查思路固化成以下流程出問(wèn)題時(shí)先看狀態(tài)ss -antp | grep 端口或IP或ss -ant state 狀態(tài)。狀態(tài)直接告訴你連接生命周期的階段。結(jié)合狀態(tài)推斷角色CLOSE-WAIT在誰(shuí)那里誰(shuí)就是被動(dòng)關(guān)閉方且沒(méi)調(diào)用close。TIME-WAIT在誰(shuí)那里誰(shuí)就是最后一次發(fā)送ACK的主動(dòng)關(guān)閉方。根據(jù)狀態(tài)查對(duì)應(yīng)環(huán)節(jié)SYN-SENT/SYN-RECV問(wèn)題 - 查握手、防火墻、隊(duì)列。ESTABLISHED無(wú)數(shù)據(jù) - 查應(yīng)用讀寫(xiě)邏輯、網(wǎng)絡(luò)帶寬、緩沖區(qū)。CLOSE-WAIT堆積 - 查應(yīng)用代碼關(guān)閉邏輯。TIME-WAIT過(guò)多 - 評(píng)估是否正常考慮調(diào)整參數(shù)或架構(gòu)。借助工具深挖tcpdump抓包看標(biāo)志位序列strace跟蹤應(yīng)用系統(tǒng)調(diào)用sysctl查看和調(diào)整內(nèi)核參數(shù)。最后記住Linux內(nèi)核的TCP實(shí)現(xiàn)非常復(fù)雜包含了擁塞控制、滑動(dòng)窗口、快速重傳等大量?jī)?yōu)化狀態(tài)機(jī)是它的骨架。把這個(gè)骨架摸清網(wǎng)絡(luò)問(wèn)題的迷霧就散開(kāi)了一大半。下次再遇到連接異常別急著重啟服務(wù)先用狀態(tài)機(jī)的視角看一眼你很可能就能直接命中問(wèn)題的根源。