前言
TCP 性能的提升不僅考察 TCP 的理論知識,還考察了對於操心系統提供的內核參數的理解與應用。
TCP 協議是由操作系統實現,所以操作系統提供了不少調節 TCP 的參數。
以下是Linux TCP常用參數
如何正確有效的使用這些參數,來提高 TCP 性能是一個不那么簡單事情。我們需要針對 TCP 每個階段的問題來對症下葯,而不是病急亂投醫。
接下來,將以三個角度來闡述提升 TCP 的策略,分別是:
- TCP 三次握手的性能提升;
- TCP 四次揮手的性能提升;
- TCP 數據傳輸的性能提升;
正文
01 TCP 三次握手的性能提升
TCP 是面向連接的、可靠的、雙向傳輸的傳輸層通信協議,所以在傳輸數據之前需要經過三次握手才能建立連接。
那么,三次握手的過程在一個 HTTP 請求的平均時間占比 10% 以上,在網絡狀態不佳、高並發或者遭遇 SYN 攻擊等場景中,如果不能有效正確的調節三次握手中的參數,就會對性能產生很多的影響。
如何正確有效的使用這些參數,來提高 TCP 三次握手的性能,這就需要理解「三次握手的狀態變遷」,這樣當出現問題時,先用 netstat
命令查看是哪個握手階段出現了問題,再來對症下葯,而不是病急亂投醫。
客戶端和服務端都可以針對三次握手優化性能。主動發起連接的客戶端優化相對簡單些,而服務端需要監聽端口,屬於被動連接方,其間保持許多的中間狀態,優化方法相對復雜一些。
所以,客戶端(主動發起連接方)和服務端(被動連接方)優化的方式是不同的,接下來分別針對客戶端和服務端優化。
客戶端優化
三次握手建立連接的首要目的是「同步序列號」。
只有同步了序列號才有可靠傳輸,TCP 許多特性都依賴於序列號實現,比如流量控制、丟包重傳等,這也是三次握手中的報文稱為 SYN 的原因,SYN 的全稱就叫 Synchronize Sequence Numbers(同步序列號)。
SYN_SENT 狀態的優化
客戶端作為主動發起連接方,首先它將發送 SYN 包,於是客戶端的連接就會處於 SYN_SENT
狀態。
客戶端在等待服務端回復的 ACK 報文,正常情況下,服務器會在幾毫秒內返回 SYN+ACK ,但如果客戶端長時間沒有收到 SYN+ACK 報文,則會重發 SYN 包,重發的次數由 tcp_syn_retries 參數控制,默認是 5 次:
通常,第一次超時重傳是在 1 秒后,第二次超時重傳是在 2 秒,第三次超時重傳是在 4 秒后,第四次超時重傳是在 8 秒后,第五次是在超時重傳 16 秒后。沒錯,每次超時的時間是上一次的 2 倍。
當第五次超時重傳后,會繼續等待 32 秒,如果仍然服務端沒有回應 ACK,客戶端就會終止三次握手。
所以,總耗時是 1+2+4+8+16+32=63 秒,大約 1 分鍾左右。
你可以根據網絡的穩定性和目標服務器的繁忙程度修改 SYN 的重傳次數,調整客戶端的三次握手時間上限。比如內網中通訊時,就可以適當調低重試次數,盡快把錯誤暴露給應用程序。
服務端優化
當服務端收到 SYN 包后,服務端會立馬回復 SYN+ACK 包,表明確認收到了客戶端的序列號,同時也把自己的序列號發給對方。
此時,服務端出現了新連接,狀態是 SYN_RCV
。在這個狀態下,Linux 內核就會建立一個「半連接隊列」來維護「未完成」的握手信息,當半連接隊列溢出后,服務端就無法再建立新的連接。
SYN 攻擊,攻擊的是就是這個半連接隊列。
如何查看由於 SYN 半連接隊列已滿,而被丟棄連接的情況?
我們可以通過該 netstat -s
命令給出的統計結果中, 可以得到由於半連接隊列已滿,引發的失敗次數:
上面輸出的數值是累計值,表示共有多少個 TCP 連接因為半連接隊列溢出而被丟棄。隔幾秒執行幾次,如果有上升的趨勢,說明當前存在半連接隊列溢出的現象。
如何調整 SYN 半連接隊列大小?
要想增大半連接隊列,不能只單純增大 tcp_max_syn_backlog 的值,還需一同增大 somaxconn 和 backlog,也就是增大 accept 隊列。否則,只單純增大 tcp_max_syn_backlog 是無效的。
增大 tcp_max_syn_backlog 和 somaxconn 的方法是修改 Linux 內核參數
增大 backlog 的方式,每個 Web 服務都不同,比如 Nginx 增大 backlog 的方法如下:
最后,改變了如上這些參數后,要重啟 Nginx 服務,因為 SYN 半連接隊列和 accept 隊列都是在 listen()
初始化的。
如果 SYN 半連接隊列已滿,只能丟棄連接嗎?
並不是這樣,開啟 syncookies 功能就可以在不使用 SYN 半連接隊列的情況下成功建立連接。
syncookies 的工作原理:服務器根據當前狀態計算出一個值,放在己方發出的 SYN+ACK 報文中發出,當客戶端返回 ACK 報文時,取出該值驗證,如果合法,就認為連接建立成功,如下圖所示。
syncookies 參數主要有以下三個值:
- 0 值,表示關閉該功能;
- 1 值,表示僅當 SYN 半連接隊列放不下時,再啟用它;
- 2 值,表示無條件開啟功能;
那么在應對 SYN 攻擊時,只需要設置為 1 即可:
SYN_RCV 狀態的優化
當客戶端接收到服務器發來的 SYN+ACK 報文后,就會回復 ACK 給服務器,同時客戶端連接狀態從 SYN_SENT 轉換為 ESTABLISHED,表示連接建立成功。
服務器端連接成功建立的時間還要再往后,等到服務端收到客戶端的 ACK 后,服務端的連接狀態才變為 ESTABLISHED。
如果服務器沒有收到 ACK,就會重發 SYN+ACK 報文,同時一直處於 SYN_RCV 狀態。
當網絡繁忙、不穩定時,報文丟失就會變嚴重,此時應該調大重發次數。反之則可以調小重發次數。修改重發次數的方法是,調整 tcp_synack_retries 參數:
tcp_synack_retries 的默認重試次數是 5 次,與客戶端重傳 SYN 類似,它的重傳會經歷 1、2、4、8、16 秒,最后一次重傳后會繼續等待 32 秒,如果服務端仍然沒有收到 ACK,才會關閉連接,故共需要等待 63 秒。
服務器收到 ACK 后連接建立成功,此時,內核會把連接從半連接隊列移除,然后創建新的完全的連接,並將其添加到 accept 隊列,等待進程調用 accept 函數時把連接取出來。
如果進程不能及時地調用 accept 函數,就會造成 accept 隊列(也稱全連接隊列)溢出,最終導致建立好的 TCP 連接被丟棄。
accept 隊列已滿,只能丟棄連接嗎?
丟棄連接只是 Linux 的默認行為,我們還可以選擇向客戶端發送 RST 復位報文,告訴客戶端連接已經建立失敗。打開這一功能需要將 tcp_abort_on_overflow 參數設置為 1。
tcp_abort_on_overflow 共有兩個值分別是 0 和 1,其分別表示:
- 0 :如果 accept 隊列滿了,那么 server 扔掉 client 發過來的 ack ;
- 1 :如果 accept 隊列滿了,server 發送一個
RST
包給 client,表示廢掉這個握手過程和這個連接;
如果要想知道客戶端連接不上服務端,是不是服務端 TCP 全連接隊列滿的原因,那么可以把 tcp_abort_on_overflow 設置為 1,這時如果在客戶端異常中可以看到很多 connection reset by peer
的錯誤,那么就可以證明是由於服務端 TCP 全連接隊列溢出的問題。
通常情況下,應當把 tcp_abort_on_overflow 設置為 0,因為這樣更有利於應對突發流量。
舉個例子,當 accept 隊列滿導致服務器丟掉了 ACK,與此同時,客戶端的連接狀態卻是 ESTABLISHED,客戶端進程就在建立好的連接上發送請求。只要服務器沒有為請求回復 ACK,客戶端的請求就會被多次「重發」。如果服務器上的進程只是短暫的繁忙造成 accept 隊列滿,那么當 accept 隊列有空位時,再次接收到的請求報文由於含有 ACK,仍然會觸發服務器端成功建立連接。
所以,tcp_abort_on_overflow 設為 0 可以提高連接建立的成功率,只有你非常肯定 TCP 全連接隊列會長期溢出時,才能設置為 1 以盡快通知客戶端。
如何調整 accept 隊列的長度呢?
accept 隊列的長度取決於 somaxconn 和 backlog 之間的最小值,也就是 min(somaxconn, backlog),其中:
- somaxconn 是 Linux 內核的參數,默認值是 128,可以通過
net.core.somaxconn
來設置其值; - backlog 是
listen(int sockfd, int backlog)
函數中的 backlog 大小;
Tomcat、Nginx、Apache 常見的 Web 服務的 backlog 默認值都是 511。
如何查看服務端進程 accept 隊列的長度?
可以通過 ss -ltn
命令查看:
- Recv-Q:當前 accept 隊列的大小,也就是當前已完成三次握手並等待服務端
accept()
的 TCP 連接; - Send-Q:accept 隊列最大長度,上面的輸出結果說明監聽 8088 端口的 TCP 服務,accept 隊列的最大長度為 128;
如何查看由於 accept 連接隊列已滿,而被丟棄的連接?
當超過了 accept 連接隊列,服務端則會丟掉后續進來的 TCP 連接,丟掉的 TCP 連接的個數會被統計起來,我們可以使用 netstat -s 命令來查看:
上面看到的 41150 times ,表示 accept 隊列溢出的次數,注意這個是累計值。可以隔幾秒鍾執行下,如果這個數字一直在增加的話,說明 accept 連接隊列偶爾滿了。
如果持續不斷地有連接因為 accept 隊列溢出被丟棄,就應該調大 backlog 以及 somaxconn 參數。
如何繞過三次握手?
以上我們只是在對三次握手的過程進行優化,接下來我們看看如何繞過三次握手發送數據。
三次握手建立連接造成的后果就是,HTTP 請求必須在一個 RTT(從客戶端到服務器一個往返的時間)后才能發送。
在 Linux 3.7 內核版本之后,提供了 TCP Fast Open 功能,這個功能可以減少 TCP 連接建立的時延。
接下來說說,TCP Fast Open 功能的工作方式。
在客戶端首次建立連接時的過程:
- 客戶端發送 SYN 報文,該報文包含 Fast Open 選項,且該選項的 Cookie 為空,這表明客戶端請求 Fast Open Cookie;
- 支持 TCP Fast Open 的服務器生成 Cookie,並將其置於 SYN-ACK 數據包中的 Fast Open 選項以發回客戶端;
- 客戶端收到 SYN-ACK 后,本地緩存 Fast Open 選項中的 Cookie。
所以,第一次發起 HTTP GET 請求的時候,還是需要正常的三次握手流程。
之后,如果客戶端再次向服務器建立連接時的過程:
- 客戶端發送 SYN 報文,該報文包含「數據」(對於非 TFO 的普通 TCP 握手過程,SYN 報文中不包含「數據」)以及此前記錄的 Cookie;
- 支持 TCP Fast Open 的服務器會對收到 Cookie 進行校驗:如果 Cookie 有效,服務器將在 SYN-ACK 報文中對 SYN 和「數據」進行確認,服務器隨后將「數據」遞送至相應的應用程序;如果 Cookie 無效,服務器將丟棄 SYN 報文中包含的「數據」,且其隨后發出的 SYN-ACK 報文將只確認 SYN 的對應序列號;
- 如果服務器接受了 SYN 報文中的「數據」,服務器可在握手完成之前發送「數據」,這就減少了握手帶來的 1 個 RTT 的時間消耗;
- 客戶端將發送 ACK 確認服務器發回的 SYN 以及「數據」,但如果客戶端在初始的 SYN 報文中發送的「數據」沒有被確認,則客戶端將重新發送「數據」;
- 此后的 TCP 連接的數據傳輸過程和非 TFO 的正常情況一致。
所以,之后發起 HTTP GET 請求的時候,可以繞過三次握手,這就減少了握手帶來的 1 個 RTT 的時間消耗。
注:客戶端在請求並存儲了 Fast Open Cookie 之后,可以不斷重復 TCP Fast Open 直至服務器認為 Cookie 無效(通常為過期)。
Linux 下怎么打開 TCP Fast Open 功能呢?
在 Linux 系統中,可以通過設置 tcp_fastopn 內核參數,來打開 Fast Open 功能:
tcp_fastopn 各個值的意義:
- 0 關閉
- 1 作為客戶端使用 Fast Open 功能
- 2 作為服務端使用 Fast Open 功能
- 3 無論作為客戶端還是服務器,都可以使用 Fast Open 功能
TCP Fast Open 功能需要客戶端和服務端同時支持,才有效果。
小結
本小結主要介紹了關於優化 TCP 三次握手的幾個 TCP 參數。
客戶端的優化
當客戶端發起 SYN 包時,可以通過 tcp_syn_retries
控制其重傳的次數。
服務端的優化
當服務端 SYN 半連接隊列溢出后,會導致后續連接被丟棄,可以通過 netstat -s
觀察半連接隊列溢出的情況,如果 SYN 半連接隊列溢出情況比較嚴重,可以通過 tcp_max_syn_backlog、somaxconn、backlog
參數來調整 SYN 半連接隊列的大小。
服務端回復 SYN+ACK 的重傳次數由 tcp_synack_retries
參數控制。如果遭受 SYN 攻擊,應把 tcp_syncookies
參數設置為 1,表示僅在 SYN 隊列滿后開啟 syncookie 功能,可以保證正常的連接成功建立。
服務端收到客戶端返回的 ACK,會把連接移入 accpet 隊列,等待進行調用 accpet() 函數取出連接。
可以通過 ss -lnt
查看服務端進程的 accept 隊列長度,如果 accept 隊列溢出,系統默認丟棄 ACK,如果可以把 tcp_abort_on_overflow
設置為 1 ,表示用 RST 通知客戶端連接建立失敗。
如果 accpet 隊列溢出嚴重,可以通過 listen 函數的 backlog
參數和 somaxconn
系統參數提高隊列大小,accept 隊列長度取決於 min(backlog, somaxconn)。
繞過三次握手
TCP Fast Open 功能可以繞過三次握手,使得 HTTP 請求減少了 1 個 RTT 的時間,Linux 下可以通過 tcp_fastopen
開啟該功能,同時必須保證服務端和客戶端同時支持。