古詩詞大全網 - 成語故事 - 傻傻分不清的TCP keepalive和HTTP keepalive

傻傻分不清的TCP keepalive和HTTP keepalive

TCP keepalive是TCP的保活定時器。通俗地說,就是TCP有壹個定時任務做倒計時,超時後會觸發任務,內容是發送壹個探測報文給對端,用來判斷對端是否存活。 (想到壹個橋段:“如果2小時後沒等到我的消息,妳們就快跑”)

正如概念中說的,用於探測對端是否存活,從而防止連接處於“半打開”狀態。

所謂半打開,就是網絡連接的雙端中,有壹端已經斷開,而另壹端仍然處於連接狀態。

(圖壹)TCP keepalive 流程圖

建立連接的雙端在通信的同時,存在壹個定時任務A,每當傳輸完壹個報文,都會重置定時任務A。如果在定時任務的時限 tcp_keepalive_time 內不再有新的報文傳輸,便會觸發定時任務A,向對端發送存活探測報文。根據響應報文的不同情況,有不同的操作分支,如上圖所示。

定時任務B會被循環執行,具體邏輯是:定時任務A的探測報文沒有得到響應報文,開始執行定時任務B。任務B的內容同樣是發送探測報文,但不同的是,B會被執行 tcp_keepalive_probes 次,時間間隔為 tcp_keepalive_intvl 。B的探測報文同樣也是在收到響應報文後,重置定時任務A,維持連接狀態。

上文提到的三個參數存在於系統文件中,具體路徑如下:

通信雙端都存在壹個文件作為數據緩沖區,對端發送給本地當前端口的數據都會緩沖在這個文件中。上文中講的“斷開連接”就是關閉這個文件,關閉後所有發送到當前端口的數據將無法存儲到緩沖區,即數據被丟棄了。

通過指令 lsof -i :8080 ,8080改成妳的端口號,便能看到這個緩沖區文件。

HTTP keepalive指的是持久連接,強調復用TCP連接。(類似場景:掛電話之前總會問句,沒啥事就先掛了,延長通話時長來確認沒有新話題)

延長TCP連接的時長,壹次TCP連接從創建到關閉期間能傳輸更多的數據。

(圖二)HTTP keepalive 流程圖

通信連接的雙端在通信的同時,存在壹個HTTP層面的keepalive定時任務。當客戶端發起Request,並且接收到Response之後,觸發定時任務。定時任務會開始計時,達到keepalive的時間距離後,關閉連接。如果在計時期間,客戶端再次發起Request,並且接收到Response,定時任務會被重置,從頭計時。

圖二用Python的socket庫為示例進行說明,在HTTP的“請求-響應”過程中,HTTP keepalive(或者稱為HTTP持久連接)在底層是如何作用於連接釋放流程,從而延長連接時長的。

為什麽不用Python的requests庫來舉例說明?requests底層也是socket連接管理,不同的是requests支持HTTP協議,可以解析出HTTP各部分信息;socket僅僅是從文件緩沖區讀取二進制流。同樣地,各種Web框架中的Request和Response對象的內部仍然是socket連接管理,只提socket可以排除很多幹擾信息。

服務端HTTP keepalive超時後的數據丟棄的說明。剛入門的同學可能也會像我壹樣感到疑惑:服務端keepalive超時後再收到數據就會丟棄,那麽服務端後續還怎麽接收端口的數據?

這就不得不提到服務端的fork模型了:服務端主進程監聽端口,當數據到來時便交給子進程來處理,主進程繼續循環監聽端口。

具體地說,當數據到來時,主進程先創建新的socket連接句柄(本質就是生成了socket文件描述符落在磁盤上,端口數據會存儲在該文件中緩沖),隨後fork出子進程;主進程關閉新的socket句柄,子進程則維持socket句柄的連接(當壹個socket句柄在所有進程中都被close之後才會開始TCP四次揮手);此後,子進程接管了與客戶端的通信。

正如(圖三)的例子,主進程會fork出很多子進程,A和B分別對接的是不同客戶端發來的請求,socket文件描述符a不會影響b的數據讀寫。

(圖三)fork模型下傳遞socket句柄的過程

結論是,服務端與外界建立的每壹個socket連接,都有獨立的文件描述符和獨立的子進程與客戶端通信。服務端斷開連接是指關閉了某個文件描述符的讀寫,並非關閉了整個端口的數據往來,不影響其他的socket連接之間通信。至於丟棄,就是說外界如果還有發往這個socket文件描述符的數據被丟棄,因為這個文件描述符已經禁止寫入,自然地數據便無法落地。

TCP keepalive更像是保障系統有序工作的兜底機制,確保系統最終能收回半打開的socket連接,否則長期運行後無法再接收更多的請求(系統的socket最大連接數限制)。

HTTP keepalive則是應用層的騷操作,使得服務端的應用程序能自主決定socket的釋放,因為TCP keepalive的倒計時默認值很長,web服務的某次連接通常不需要等待那麽久。說直白點,就是TCP有壹個計時器,HTTP也可以自己搞個計時器,如果HTTP的計時器先超時,同樣有權利讓TCP進入四次揮手流程。

在某個數據包傳輸後,兩個keepalive的定時任務同時存在且壹起進入倒計時狀態,壹個是系統內核TCP相關代碼的程序,另壹個是高級編程語言(Python/Java/Go等)Web框架代碼的程序,他們壹起運行並不沖突。

HTTP keepalive是應用層的東西,在上生產時對外提供服務的應用程序都會有keepalive參數,例如Gunicorn的keepalive、Nginx的keepalive_timeout。通過這個參數,我們能在更高級的層面控制等待下壹個數據的時長。

還有,如果同壹臺服務器有N個Web服務,TCP keepalive參數是全局生效,眾口難調。

如果妳的網絡結構是類似client-nginx-web server,那麽妳就要同時考慮nginx和web server的keepalive參數大小搭配的問題,此處引用Gunicorn對keepalive參數的使用建議:

假設web等待時間比nginx短很多,client-nginx的連接還在,nginx-web就已經斷開了,web就會錯過壹些數據,對於客戶來說好端端的我拿不到結果是無法容忍的。因此最好是和nginx的等待時間協調好,不要相差太多(不要太短,也不要長很多)。

關於不要太長,多說壹句。如果等待很久,web服務會累積維持非常多的連接,這樣子新的請求無法打進來,正在維持的連接不見得利用率很高(可能客戶端的代碼在打斷點、可能客戶端早就close)。結果就是服務端netstat顯示壹堆連接,新的請求全都被掛起甚至丟棄。