Linux內(nèi)核TCP參數(shù)調(diào)優(yōu)全面解讀
前言
TCP 性能的提升不僅考察 TCP 的理論知識,還考察了對于操心系統(tǒng)提供的內(nèi)核參數(shù)的理解與應(yīng)用。
TCP 協(xié)議是由操作系統(tǒng)實現(xiàn),所以操作系統(tǒng)提供了不少調(diào)節(jié) TCP 的參數(shù)。
如何正確有效的使用這些參數(shù),來提高 TCP 性能是一個不那么簡單事情。我們需要針對 TCP 每個階段的問題來對癥下藥,而不是病急亂投醫(yī)。
接下來,將以三個角度來闡述提升 TCP 的策略,分別是:
- TCP 三次握手的性能提升;
- TCP 四次揮手的性能提升;
- TCP 數(shù)據(jù)傳輸?shù)男阅芴嵘?/li>
正文
01 TCP 三次握手的性能提升
TCP 是面向連接的、可靠的、雙向傳輸?shù)膫鬏攲油ㄐ艆f(xié)議,所以在傳輸數(shù)據(jù)之前需要經(jīng)過三次握手才能建立連接。
那么,三次握手的過程在一個 HTTP 請求的平均時間占比 10% 以上,在網(wǎng)絡(luò)狀態(tài)不佳、高并發(fā)或者遭遇 SYN 攻擊等場景中,如果不能有效正確的調(diào)節(jié)三次握手中的參數(shù),就會對性能產(chǎn)生很多的影響。
如何正確有效的使用這些參數(shù),來提高 TCP 三次握手的性能,這就需要理解「三次握手的狀態(tài)變遷」,這樣當(dāng)出現(xiàn)問題時,先用?netstat
?命令查看是哪個握手階段出現(xiàn)了問題,再來對癥下藥,而不是病急亂投醫(yī)。
客戶端和服務(wù)端都可以針對三次握手優(yōu)化性能。主動發(fā)起連接的客戶端優(yōu)化相對簡單些,而服務(wù)端需要監(jiān)聽端口,屬于被動連接方,其間保持許多的中間狀態(tài),優(yōu)化方法相對復(fù)雜一些。
所以,客戶端(主動發(fā)起連接方)和服務(wù)端(被動連接方)優(yōu)化的方式是不同的,接下來分別針對客戶端和服務(wù)端優(yōu)化。
客戶端優(yōu)化
三次握手建立連接的首要目的是「同步序列號」。
只有同步了序列號才有可靠傳輸,TCP 許多特性都依賴于序列號實現(xiàn),比如流量控制、丟包重傳等,這也是三次握手中的報文稱為 SYN 的原因,SYN 的全稱就叫?Synchronize Sequence Numbers(同步序列號)。
SYN_SENT 狀態(tài)的優(yōu)化
客戶端作為主動發(fā)起連接方,首先它將發(fā)送 SYN 包,于是客戶端的連接就會處于?SYN_SENT
?狀態(tài)。
客戶端在等待服務(wù)端回復(fù)的 ACK 報文,正常情況下,服務(wù)器會在幾毫秒內(nèi)返回 SYN+ACK ,但如果客戶端長時間沒有收到 SYN+ACK 報文,則會重發(fā) SYN 包,重發(fā)的次數(shù)由 tcp_syn_retries 參數(shù)控制,默認是 5 次:
cat /proc/sys/net/ipv4/tcp_syn_retries
通常,第一次超時重傳是在 1 秒后,第二次超時重傳是在 2 秒,第三次超時重傳是在 4 秒后,第四次超時重傳是在 8 秒后,第五次是在超時重傳 16 秒后。沒錯,每次超時的時間是上一次的 2 倍。
當(dāng)?shù)谖宕纬瑫r重傳后,會繼續(xù)等待 32 秒,如果仍然服務(wù)端沒有回應(yīng) ACK,客戶端就會終止三次握手。
所以,總耗時是 1+2+4+8+16+32=63 秒,大約 1 分鐘左右。
你可以根據(jù)網(wǎng)絡(luò)的穩(wěn)定性和目標服務(wù)器的繁忙程度修改 SYN 的重傳次數(shù),調(diào)整客戶端的三次握手時間上限。比如內(nèi)網(wǎng)中通訊時,就可以適當(dāng)調(diào)低重試次數(shù),盡快把錯誤暴露給應(yīng)用程序。
服務(wù)端優(yōu)化
當(dāng)服務(wù)端收到 SYN 包后,服務(wù)端會立馬回復(fù) SYN+ACK 包,表明確認收到了客戶端的序列號,同時也把自己的序列號發(fā)給對方。
此時,服務(wù)端出現(xiàn)了新連接,狀態(tài)是?SYN_RCV
。在這個狀態(tài)下,Linux 內(nèi)核就會建立一個「半連接隊列」來維護「未完成」的握手信息,當(dāng)半連接隊列溢出后,服務(wù)端就無法再建立新的連接。
SYN 攻擊,攻擊的是就是這個半連接隊列。
如何查看由于 SYN 半連接隊列已滿,而被丟棄連接的情況?
我們可以通過該?netstat -s
?命令給出的統(tǒng)計結(jié)果中, 可以得到由于半連接隊列已滿,引發(fā)的失敗次數(shù):
netstat -s | grep "SYNs to LISTEN"
上面輸出的數(shù)值是累計值,表示共有多少個 TCP 連接因為半連接隊列溢出而被丟棄。隔幾秒執(zhí)行幾次,如果有上升的趨勢,說明當(dāng)前存在半連接隊列溢出的現(xiàn)象。
如何調(diào)整 SYN 半連接隊列大???
要想增大半連接隊列,不能只單純增大 tcp_max_syn_backlog 的值,還需一同增大 somaxconn 和 backlog,也就是增大 accept 隊列。否則,只單純增大 tcp_max_syn_backlog 是無效的。
增大 tcp_max_syn_backlog 和 somaxconn 的方法是修改 Linux 內(nèi)核參數(shù):
/proc/sys/net/ipv4/tcpmaxsyn_backlog
/proc/sys/net/core/somaxconn
增大 backlog 的方式,每個 Web 服務(wù)都不同,比如 Nginx 增大 backlog 的方法如下:
最后,改變了如上這些參數(shù)后,要重啟 Nginx 服務(wù),因為 SYN 半連接隊列和 accept 隊列都是在?listen()
?初始化的。
如果 SYN 半連接隊列已滿,只能丟棄連接嗎?
并不是這樣,開啟 syncookies 功能就可以在不使用 SYN 半連接隊列的情況下成功建立連接。
syncookies 的工作原理:服務(wù)器根據(jù)當(dāng)前狀態(tài)計算出一個值,放在己方發(fā)出的 SYN+ACK 報文中發(fā)出,當(dāng)客戶端返回 ACK 報文時,取出該值驗證,如果合法,就認為連接建立成功,如下圖所示。
syncookies 參數(shù)主要有以下三個值:
- 0 值,表示關(guān)閉該功能;
- 1 值,表示僅當(dāng) SYN 半連接隊列放不下時,再啟用它;
- 2 值,表示無條件開啟功能;
那么在應(yīng)對 SYN 攻擊時,只需要設(shè)置為 1 即可:
/proc/sys/net/ipv4/tcp_syncookies
SYN_RCV 狀態(tài)的優(yōu)化
當(dāng)客戶端接收到服務(wù)器發(fā)來的 SYN+ACK 報文后,就會回復(fù) ACK 給服務(wù)器,同時客戶端連接狀態(tài)從 SYN_SENT 轉(zhuǎn)換為 ESTABLISHED,表示連接建立成功。
服務(wù)器端連接成功建立的時間還要再往后,等到服務(wù)端收到客戶端的 ACK 后,服務(wù)端的連接狀態(tài)才變?yōu)?ESTABLISHED。
如果服務(wù)器沒有收到 ACK,就會重發(fā) SYN+ACK 報文,同時一直處于 SYN_RCV 狀態(tài)。
當(dāng)網(wǎng)絡(luò)繁忙、不穩(wěn)定時,報文丟失就會變嚴重,此時應(yīng)該調(diào)大重發(fā)次數(shù)。反之則可以調(diào)小重發(fā)次數(shù)。修改重發(fā)次數(shù)的方法是,調(diào)整 tcp_synack_retries 參數(shù):
tcp_synack_retries 的默認重試次數(shù)是 5 次,與客戶端重傳 SYN 類似,它的重傳會經(jīng)歷 1、2、4、8、16 秒,最后一次重傳后會繼續(xù)等待 32 秒,如果服務(wù)端仍然沒有收到 ACK,才會關(guān)閉連接,故共需要等待 63 秒。
服務(wù)器收到 ACK 后連接建立成功,此時,內(nèi)核會把連接從半連接隊列移除,然后創(chuàng)建新的完全的連接,并將其添加到 accept 隊列,等待進程調(diào)用 accept 函數(shù)時把連接取出來。
如果進程不能及時地調(diào)用 accept 函數(shù),就會造成 accept 隊列(也稱全連接隊列)溢出,最終導(dǎo)致建立好的 TCP 連接被丟棄。
accept 隊列已滿,只能丟棄連接嗎?
丟棄連接只是 Linux 的默認行為,我們還可以選擇向客戶端發(fā)送 RST 復(fù)位報文,告訴客戶端連接已經(jīng)建立失敗。打開這一功能需要將 tcp_abort_on_overflow 參數(shù)設(shè)置為 1。
cat /proc/sys/net/ipv4/tcp_abort_on_overflow
tcp_abort_on_overflow 共有兩個值分別是 0 和 1,其分別表示:
- 0 :如果 accept 隊列滿了,那么 server 扔掉 client 發(fā)過來的 ack ;
- 1 :如果 accept 隊列滿了,server 發(fā)送一個?
RST
?包給 client,表示廢掉這個握手過程和這個連接;
如果要想知道客戶端連接不上服務(wù)端,是不是服務(wù)端 TCP 全連接隊列滿的原因,那么可以把 tcp_abort_on_overflow 設(shè)置為 1,這時如果在客戶端異常中可以看到很多?connection reset by peer
的錯誤,那么就可以證明是由于服務(wù)端 TCP 全連接隊列溢出的問題。
通常情況下,應(yīng)當(dāng)把 tcp_abort_on_overflow 設(shè)置為 0,因為這樣更有利于應(yīng)對突發(fā)流量。
舉個例子,當(dāng) accept 隊列滿導(dǎo)致服務(wù)器丟掉了 ACK,與此同時,客戶端的連接狀態(tài)卻是 ESTABLISHED,客戶端進程就在建立好的連接上發(fā)送請求。只要服務(wù)器沒有為請求回復(fù) ACK,客戶端的請求就會被多次「重發(fā)」。如果服務(wù)器上的進程只是短暫的繁忙造成 accept 隊列滿,那么當(dāng) accept 隊列有空位時,再次接收到的請求報文由于含有 ACK,仍然會觸發(fā)服務(wù)器端成功建立連接。
所以,tcp_abort_on_overflow 設(shè)為 0 可以提高連接建立的成功率,只有你非常肯定 TCP 全連接隊列會長期溢出時,才能設(shè)置為 1 以盡快通知客戶端。
如何調(diào)整 accept 隊列的長度呢?
accept 隊列的長度取決于 somaxconn 和 backlog 之間的最小值,也就是 min(somaxconn, backlog),其中:
- somaxconn 是 Linux 內(nèi)核的參數(shù),默認值是 128,可以通過?
net.core.somaxconn
?來設(shè)置其值; - backlog 是?
listen(int sockfd, int backlog)
?函數(shù)中的 backlog 大??;
Tomcat、Nginx、Apache 常見的 Web 服務(wù)的 backlog 默認值都是 511。
如何查看服務(wù)端進程 accept 隊列的長度?
可以通過?ss -ltn
?命令查看:
- Recv-Q:當(dāng)前 accept 隊列的大小,也就是當(dāng)前已完成三次握手并等待服務(wù)端?
accept()
?的 TCP 連接; - Send-Q:accept 隊列最大長度,上面的輸出結(jié)果說明監(jiān)聽 8088 端口的 TCP 服務(wù),accept 隊列的最大長度為 128;
如何查看由于 accept 連接隊列已滿,而被丟棄的連接?
當(dāng)超過了 accept 連接隊列,服務(wù)端則會丟掉后續(xù)進來的 TCP 連接,丟掉的 TCP 連接的個數(shù)會被統(tǒng)計起來,我們可以使用 netstat -s 命令來查看:
上面看到的 41150 times ,表示 accept 隊列溢出的次數(shù),注意這個是累計值??梢愿魩酌腌妶?zhí)行下,如果這個數(shù)字一直在增加的話,說明 accept 連接隊列偶爾滿了。
如果持續(xù)不斷地有連接因為 accept 隊列溢出被丟棄,就應(yīng)該調(diào)大 backlog 以及 somaxconn 參數(shù)。
如何繞過三次握手?
以上我們只是在對三次握手的過程進行優(yōu)化,接下來我們看看如何繞過三次握手發(fā)送數(shù)據(jù)。
三次握手建立連接造成的后果就是,HTTP 請求必須在一個 RTT(從客戶端到服務(wù)器一個往返的時間)后才能發(fā)送。
在 Linux 3.7 內(nèi)核版本之后,提供了 TCP Fast Open 功能,這個功能可以減少 TCP 連接建立的時延。
接下來說說,TCP Fast Open 功能的工作方式。
在客戶端首次建立連接時的過程:
- 客戶端發(fā)送 SYN 報文,該報文包含 Fast Open 選項,且該選項的 Cookie 為空,這表明客戶端請求 Fast Open Cookie;
- 支持 TCP Fast Open 的服務(wù)器生成 Cookie,并將其置于 SYN-ACK 數(shù)據(jù)包中的 Fast Open 選項以發(fā)回客戶端;
- 客戶端收到 SYN-ACK 后,本地緩存 Fast Open 選項中的 Cookie。
所以,第一次發(fā)起 HTTP GET 請求的時候,還是需要正常的三次握手流程。
之后,如果客戶端再次向服務(wù)器建立連接時的過程:
- 客戶端發(fā)送 SYN 報文,該報文包含「數(shù)據(jù)」(對于非 TFO 的普通 TCP 握手過程,SYN 報文中不包含「數(shù)據(jù)」)以及此前記錄的 Cookie;
- 支持 TCP Fast Open 的服務(wù)器會對收到 Cookie 進行校驗:如果 Cookie 有效,服務(wù)器將在 SYN-ACK 報文中對 SYN 和「數(shù)據(jù)」進行確認,服務(wù)器隨后將「數(shù)據(jù)」遞送至相應(yīng)的應(yīng)用程序;如果 Cookie 無效,服務(wù)器將丟棄 SYN 報文中包含的「數(shù)據(jù)」,且其隨后發(fā)出的 SYN-ACK 報文將只確認 SYN 的對應(yīng)序列號;
- 如果服務(wù)器接受了 SYN 報文中的「數(shù)據(jù)」,服務(wù)器可在握手完成之前發(fā)送「數(shù)據(jù)」,這就減少了握手帶來的 1 個 RTT 的時間消耗;
- 客戶端將發(fā)送 ACK 確認服務(wù)器發(fā)回的 SYN 以及「數(shù)據(jù)」,但如果客戶端在初始的 SYN 報文中發(fā)送的「數(shù)據(jù)」沒有被確認,則客戶端將重新發(fā)送「數(shù)據(jù)」;
- 此后的 TCP 連接的數(shù)據(jù)傳輸過程和非 TFO 的正常情況一致。
所以,之后發(fā)起 HTTP GET 請求的時候,可以繞過三次握手,這就減少了握手帶來的 1 個 RTT 的時間消耗。
注:客戶端在請求并存儲了 Fast Open Cookie 之后,可以不斷重復(fù) TCP Fast Open 直至服務(wù)器認為 Cookie 無效(通常為過期)。
Linux 下怎么打開 TCP Fast Open 功能呢?
在 Linux 系統(tǒng)中,可以通過設(shè)置 tcp_fastopn 內(nèi)核參數(shù),來打開 Fast Open 功能:
tcp_fastopn 各個值的意義:
- 0 關(guān)閉
- 1 作為客戶端使用 Fast Open 功能
- 2 作為服務(wù)端使用 Fast Open 功能
- 3 無論作為客戶端還是服務(wù)器,都可以使用 Fast Open 功能
TCP Fast Open 功能需要客戶端和服務(wù)端同時支持,才有效果。
小結(jié)
本小結(jié)主要介紹了關(guān)于優(yōu)化 TCP 三次握手的幾個 TCP 參數(shù)。
客戶端的優(yōu)化
當(dāng)客戶端發(fā)起 SYN 包時,可以通過?tcp_syn_retries
?控制其重傳的次數(shù)。
服務(wù)端的優(yōu)化
當(dāng)服務(wù)端 SYN 半連接隊列溢出后,會導(dǎo)致后續(xù)連接被丟棄,可以通過?netstat -s
?觀察半連接隊列溢出的情況,如果 SYN 半連接隊列溢出情況比較嚴重,可以通過?tcp_max_syn_backlog、somaxconn、backlog
參數(shù)來調(diào)整 SYN 半連接隊列的大小。
服務(wù)端回復(fù) SYN+ACK 的重傳次數(shù)由?tcp_synack_retries
?參數(shù)控制。如果遭受 SYN 攻擊,應(yīng)把?tcp_syncookies
?參數(shù)設(shè)置為 1,表示僅在 SYN 隊列滿后開啟 syncookie 功能,可以保證正常的連接成功建立。
服務(wù)端收到客戶端返回的 ACK,會把連接移入 accpet 隊列,等待進行調(diào)用 accpet() 函數(shù)取出連接。
可以通過?ss -lnt
?查看服務(wù)端進程的 accept 隊列長度,如果 accept 隊列溢出,系統(tǒng)默認丟棄 ACK,如果可以把?tcp_abort_on_overflow
?設(shè)置為 1 ,表示用 RST 通知客戶端連接建立失敗。
如果 accpet 隊列溢出嚴重,可以通過 listen 函數(shù)的?backlog
?參數(shù)和?somaxconn
?系統(tǒng)參數(shù)提高隊列大小,accept 隊列長度取決于 min(backlog, somaxconn)。
繞過三次握手
TCP Fast Open 功能可以繞過三次握手,使得 HTTP 請求減少了 1 個 RTT 的時間,Linux 下可以通過?tcp_fastopen
?開啟該功能,同時必須保證服務(wù)端和客戶端同時支持。
02 TCP 四次揮手的性能提升
接下來,我們一起看看針對 TCP 四次揮手關(guān)不連接時,如何優(yōu)化性能。
在開始之前,我們得先了解四次揮手狀態(tài)變遷的過程。
客戶端和服務(wù)端雙方都可以主動斷開連接,通常先關(guān)閉連接的一方稱為主動方,后關(guān)閉連接的一方稱為被動方。
可以看到,四次揮手過程只涉及了兩種報文,分別是 FIN 和 ACK:
- FIN 就是結(jié)束連接的意思,誰發(fā)出 FIN 報文,就表示它將不會再發(fā)送任何數(shù)據(jù),關(guān)閉這一方向上的傳輸通道;
- ACK 就是確認的意思,用來通知對方:你方的發(fā)送通道已經(jīng)關(guān)閉;
四次揮手的過程:
- 當(dāng)主動方關(guān)閉連接時,會發(fā)送 FIN 報文,此時發(fā)送方的 TCP 連接將從 ESTABLISHED 變成 FIN_WAIT1。
- 當(dāng)被動方收到 FIN 報文后,內(nèi)核會自動回復(fù) ACK 報文,連接狀態(tài)將從 ESTABLISHED 變成 CLOSE_WAIT,表示被動方在等待進程調(diào)用 close 函數(shù)關(guān)閉連接。
- 當(dāng)主動方收到這個 ACK 后,連接狀態(tài)由 FIN_WAIT1 變?yōu)?FIN_WAIT2,也就是表示主動方的發(fā)送通道就關(guān)閉了。
- 當(dāng)被動方進入 CLOSE_WAIT 時,被動方還會繼續(xù)處理數(shù)據(jù),等到進程的 read 函數(shù)返回 0 后,應(yīng)用程序就會調(diào)用 close 函數(shù),進而觸發(fā)內(nèi)核發(fā)送 FIN 報文,此時被動方的連接狀態(tài)變?yōu)?LAST_ACK。
- 當(dāng)主動方收到這個 FIN 報文后,內(nèi)核會回復(fù) ACK 報文給被動方,同時主動方的連接狀態(tài)由 FIN_WAIT2 變?yōu)?TIME_WAIT,在 Linux 系統(tǒng)下大約等待 1 分鐘后,TIME_WAIT 狀態(tài)的連接才會徹底關(guān)閉。
- 當(dāng)被動方收到最后的 ACK 報文后,被動方的連接就會關(guān)閉。
你可以看到,每個方向都需要一個 FIN 和一個 ACK,因此通常被稱為四次揮手。
這里一點需要注意是:主動關(guān)閉連接的,才有 TIME_WAIT 狀態(tài)。
主動關(guān)閉方和被動關(guān)閉方優(yōu)化的思路也不同,接下來分別說說如何優(yōu)化他們。
主動方的優(yōu)化
關(guān)閉的連接的方式通常有兩種,分別是 RST 報文關(guān)閉和 FIN 報文關(guān)閉。
如果進程異常退出了,內(nèi)核就會發(fā)送 RST 報文來關(guān)閉,它可以不走四次揮手流程,是一個暴力關(guān)閉連接的方式。
安全關(guān)閉連接的方式必須通過四次揮手,它由進程調(diào)用?close
?和?shutdown
?函數(shù)發(fā)起 FIN 報文(shutdown 參數(shù)須傳入 SHUT_WR 或者 SHUT_RDWR 才會發(fā)送 FIN)。
調(diào)用 close 函數(shù) 和 shutdown 函數(shù)有什么區(qū)別?
調(diào)用了 close 函數(shù)意味著完全斷開連接,完全斷開不僅指無法傳輸數(shù)據(jù),而且也不能發(fā)送數(shù)據(jù)。 此時,調(diào)用了 close 函數(shù)的一方的連接叫做「孤兒連接」,如果你用 netstat -p 命令,會發(fā)現(xiàn)連接對應(yīng)的進程名為空。
使用 close 函數(shù)關(guān)閉連接是不優(yōu)雅的。于是,就出現(xiàn)了一種優(yōu)雅關(guān)閉連接的?shutdown
?函數(shù),它可以控制只關(guān)閉一個方向的連接:
第二個參數(shù)決定斷開連接的方式,主要有以下三種方式:
- SHUT_RD(0):關(guān)閉連接的「讀」這個方向,如果接收緩沖區(qū)有已接收的數(shù)據(jù),則將會被丟棄,并且后續(xù)再收到新的數(shù)據(jù),會對數(shù)據(jù)進行 ACK,然后悄悄地丟棄。也就是說,對端還是會接收到 ACK,在這種情況下根本不知道數(shù)據(jù)已經(jīng)被丟棄了。
- SHUT_WR(1):關(guān)閉連接的「寫」這個方向,這就是常被稱為「半關(guān)閉」的連接。如果發(fā)送緩沖區(qū)還有未發(fā)送的數(shù)據(jù),將被立即發(fā)送出去,并發(fā)送一個 FIN 報文給對端。
- SHUT_RDWR(2):相當(dāng)于 SHUT_RD 和 SHUT_WR 操作各一次,關(guān)閉套接字的讀和寫兩個方向。
close 和 shutdown 函數(shù)都可以關(guān)閉連接,但這兩種方式關(guān)閉的連接,不只功能上有差異,控制它們的 Linux 參數(shù)也不相同。
FIN_WAIT1 狀態(tài)的優(yōu)化
主動方發(fā)送 FIN 報文后,連接就處于 FIN_WAIT1 狀態(tài),正常情況下,如果能及時收到被動方的 ACK,則會很快變?yōu)?FIN_WAIT2 狀態(tài)。
但是當(dāng)遲遲收不到對方返回的 ACK 時,連接就會一直處于 FIN_WAIT1 狀態(tài)。此時,內(nèi)核會定時重發(fā) FIN 報文,其中重發(fā)次數(shù)由 tcp_orphan_retries 參數(shù)控制(注意,orphan 雖然是孤兒的意思,該參數(shù)卻不只對孤兒連接有效,事實上,它對所有 FIN_WAIT1 狀態(tài)下的連接都有效),默認值是 0。
你可能會好奇,這 0 表示幾次?實際上當(dāng)為 0 時,特指 8 次,從下面的內(nèi)核源碼可知:
如果 FIN_WAIT1 狀態(tài)連接很多,我們就需要考慮降低 tcp_orphan_retries 的值,當(dāng)重傳次數(shù)超過 tcp_orphan_retries 時,連接就會直接關(guān)閉掉。
對于普遍正常情況時,調(diào)低 tcp_orphan_retries 就已經(jīng)可以了。如果遇到惡意攻擊,F(xiàn)IN 報文根本無法發(fā)送出去,這由 TCP 兩個特性導(dǎo)致的:
- 首先,TCP 必須報文報文是有序發(fā)送的,F(xiàn)IN 報文也不例外,當(dāng)發(fā)送緩沖區(qū)還有數(shù)據(jù)沒有發(fā)送時,F(xiàn)IN 報文也不能提前發(fā)送。
- 其次,TCP 有流量控制功能,當(dāng)接收方接收窗口為 0 時,發(fā)送方就不能再發(fā)送數(shù)據(jù)。所以,當(dāng)攻擊者下載大文件時,就可以通過接收窗口設(shè)為 0 ,這就會使得 FIN 報文都無法發(fā)送出去,那么連接會一直處于 FIN_WAIT1 狀態(tài)。
解決這種問題的方法,是調(diào)整 tcp_max_orphans 參數(shù),它定義了「孤兒連接」的最大數(shù)量:
當(dāng)進程調(diào)用了?close
?函數(shù)關(guān)閉連接,此時連接就會是「孤兒連接」,因為它無法在發(fā)送和接收數(shù)據(jù)。Linux 系統(tǒng)為了防止孤兒連接過多,導(dǎo)致系統(tǒng)資源長時間被占用,就提供了?tcp_max_orphans
?參數(shù)。如果孤兒連接數(shù)量大于它,新增的孤兒連接將不再走四次揮手,而是直接發(fā)送 RST 復(fù)位報文強制關(guān)閉。
FIN_WAIT2 狀態(tài)的優(yōu)化
當(dāng)主動方收到 ACK 報文后,會處于 FIN_WAIT2 狀態(tài),就表示主動方的發(fā)送通道已經(jīng)關(guān)閉,接下來將等待對方發(fā)送 FIN 報文,關(guān)閉對方的發(fā)送通道。
這時,如果連接是用 shutdown 函數(shù)關(guān)閉的,連接可以一直處于 FIN_WAIT2 狀態(tài),因為它可能還可以發(fā)送或接收數(shù)據(jù)。但對于 close 函數(shù)關(guān)閉的孤兒連接,由于無法在發(fā)送和接收數(shù)據(jù),所以這個狀態(tài)不可以持續(xù)太久,而 tcp_fin_timeout 控制了這個狀態(tài)下連接的持續(xù)時長,默認值是 60 秒:
它意味著對于孤兒連接(調(diào)用 close 關(guān)閉的連接),如果在 60 秒后還沒有收到 FIN 報文,連接就會直接關(guān)閉。
這個 60 秒不是隨便決定的,它與 TIME_WAIT 狀態(tài)持續(xù)的時間是相同的,后面我們在來說說為什么是 60 秒。
TIME_WAIT 狀態(tài)的優(yōu)化
TIME_WAIT 是主動方四次揮手的最后一個狀態(tài),也是最常遇見的狀態(tài)。
當(dāng)收到被動方發(fā)來的 FIN 報文后,主動方會立刻回復(fù) ACK,表示確認對方的發(fā)送通道已經(jīng)關(guān)閉,接著就處于 TIME_WAIT 狀態(tài)。在 Linux 系統(tǒng),TIME_WAIT 狀態(tài)會持續(xù) 60 秒后才會進入關(guān)閉狀態(tài)。
TIME_WAIT 狀態(tài)的連接,在主動方看來確實快已經(jīng)關(guān)閉了。然后,被動方?jīng)]有收到 ACK 報文前,還是處于 LAST_ACK 狀態(tài)。如果這個 ACK 報文沒有到達被動方,被動方就會重發(fā) FIN 報文。重發(fā)次數(shù)仍然由前面介紹過的 tcp_orphan_retries 參數(shù)控制。
TIME-WAIT 的狀態(tài)尤其重要,主要是兩個原因:
- 防止具有相同「四元組」的「舊」數(shù)據(jù)包被收到;
- 保證「被動關(guān)閉連接」的一方能被正確的關(guān)閉,即保證最后的 ACK 能讓被動關(guān)閉方接收,從而幫助其正常關(guān)閉;
原因一:防止舊連接的數(shù)據(jù)包
TIME-WAIT 的一個作用是防止收到歷史數(shù)據(jù),從而導(dǎo)致數(shù)據(jù)錯亂的問題。
假設(shè) TIME-WAIT 沒有等待時間或時間過短,被延遲的數(shù)據(jù)包抵達后會發(fā)生什么呢?
- 如上圖黃色框框服務(wù)端在關(guān)閉連接之前發(fā)送的?
SEQ = 301
?報文,被網(wǎng)絡(luò)延遲了。 - 這時有相同端口的 TCP 連接被復(fù)用后,被延遲的?
SEQ = 301
?抵達了客戶端,那么客戶端是有可能正常接收這個過期的報文,這就會產(chǎn)生數(shù)據(jù)錯亂等嚴重的問題。
所以,TCP 就設(shè)計出了這么一個機制,經(jīng)過?2MSL
?這個時間,足以讓兩個方向上的數(shù)據(jù)包都被丟棄,使得原來連接的數(shù)據(jù)包在網(wǎng)絡(luò)中都自然消失,再出現(xiàn)的數(shù)據(jù)包一定都是新建立連接所產(chǎn)生的。
原因二:保證連接正確關(guān)閉
TIME-WAIT 的另外一個作用是等待足夠的時間以確保最后的 ACK 能讓被動關(guān)閉方接收,從而幫助其正常關(guān)閉。
假設(shè) TIME-WAIT 沒有等待時間或時間過短,斷開連接會造成什么問題呢?
- 如上圖紅色框框客戶端四次揮手的最后一個?
ACK
?報文如果在網(wǎng)絡(luò)中被丟失了,此時如果客戶端?TIME-WAIT
?過短或沒有,則就直接進入了?CLOSE
?狀態(tài)了,那么服務(wù)端則會一直處在?LASE-ACK
?狀態(tài)。 - 當(dāng)客戶端發(fā)起建立連接的?
SYN
?請求報文后,服務(wù)端會發(fā)送?RST
?報文給客戶端,連接建立的過程就會被終止。
我們再回過頭來看看,為什么 TIME_WAIT 狀態(tài)要保持 60 秒呢?這與孤兒連接 FIN_WAIT2 狀態(tài)默認保留 60 秒的原理是一樣的,因為這兩個狀態(tài)都需要保持 2MSL 時長。MSL 全稱是 Maximum Segment Lifetime,它定義了一個報文在網(wǎng)絡(luò)中的最長生存時間(報文每經(jīng)過一次路由器的轉(zhuǎn)發(fā),IP 頭部的 TTL 字段就會減 1,減到 0 時報文就被丟棄,這就限制了報文的最長存活時間)。
為什么是 2 MSL 的時長呢?這其實是相當(dāng)于至少允許報文丟失一次。比如,若 ACK 在一個 MSL 內(nèi)丟失,這樣被動方重發(fā)的 FIN 會在第 2 個 MSL 內(nèi)到達,TIME_WAIT 狀態(tài)的連接可以應(yīng)對。
為什么不是 4 或者 8 MSL 的時長呢?你可以想象一個丟包率達到百分之一的糟糕網(wǎng)絡(luò),連續(xù)兩次丟包的概率只有萬分之一,這個概率實在是太小了,忽略它比解決它更具性價比。
因此,TIME_WAIT 和 FIN_WAIT2 狀態(tài)的最大時長都是 2 MSL,由于在 Linux 系統(tǒng)中,MSL 的值固定為 30 秒,所以它們都是 60 秒。
雖然 TIME_WAIT 狀態(tài)有存在的必要,但它畢竟會消耗系統(tǒng)資源。如果發(fā)起連接一方的 TIME_WAIT 狀態(tài)過多,占滿了所有端口資源,則會導(dǎo)致無法創(chuàng)建新連接。
- 客戶端受端口資源限制:如果客戶端 TIME_WAIT 過多,就會導(dǎo)致端口資源被占用,因為端口就65536個,被占滿就會導(dǎo)致無法創(chuàng)建新的連接;
- 服務(wù)端受系統(tǒng)資源限制:由于一個 四元組表示TCP連接,理論上服務(wù)端可以建立很多連接,服務(wù)端確實只監(jiān)聽一個端口 但是會把連接扔給處理線程,所以理論上監(jiān)聽的端口可以繼續(xù)監(jiān)聽。但是線程池處理不了那么多一直不斷的連接了。所以當(dāng)服務(wù)端出現(xiàn)大量 TIME_WAIT 時,系統(tǒng)資源被占滿時,會導(dǎo)致處理不過來新的連接;
另外,Linux 提供了 tcp_max_tw_buckets 參數(shù),當(dāng) TIME_WAIT 的連接數(shù)量超過該參數(shù)時,新關(guān)閉的連接就不再經(jīng)歷 TIME_WAIT 而直接關(guān)閉:
當(dāng)服務(wù)器的并發(fā)連接增多時,相應(yīng)地,同時處于 TIME_WAIT 狀態(tài)的連接數(shù)量也會變多,此時就應(yīng)當(dāng)調(diào)大?tcp_max_tw_buckets
?參數(shù),減少不同連接間數(shù)據(jù)錯亂的概率。
tcp_max_tw_buckets 也不是越大越好,畢竟內(nèi)存和端口都是有限的。
有一種方式可以在建立新連接時,復(fù)用處于 TIME_WAIT 狀態(tài)的連接,那就是打開 tcp_tw_reuse 參數(shù)。但是需要注意,該參數(shù)是只用于客戶端(建立連接的發(fā)起方),因為是在調(diào)用 connect() 時起作用的,而對于服務(wù)端(被動連接方)是沒有用的。
tcp_tw_reuse 從協(xié)議角度理解是安全可控的,可以復(fù)用處于 TIME_WAIT 的端口為新的連接所用。
什么是協(xié)議角度理解的安全可控呢?主要有兩點:
- 只適用于連接發(fā)起方,也就是 C/S 模型中的客戶端;
- 對應(yīng)的 TIME_WAIT 狀態(tài)的連接創(chuàng)建時間超過 1 秒才可以被復(fù)用。
使用這個選項,還有一個前提,需要打開對 TCP 時間戳的支持(對方也要打開 ):
由于引入了時間戳,它能帶來了些好處:
- 我們在前面提到的 2MSL 問題就不復(fù)存在了,因為重復(fù)的數(shù)據(jù)包會因為時間戳過期被自然丟棄;
- 同時,它還可以防止序列號繞回,也是因為重復(fù)的數(shù)據(jù)包會由于時間戳過期被自然丟棄;
老版本的 Linux 還提供了 tcp_tw_recycle 參數(shù),但是當(dāng)開啟了它,就有兩個坑:
- Linux 會加快客戶端和服務(wù)端 TIME_WAIT 狀態(tài)的時間,也就是它會使得 TIME_WAIT 狀態(tài)會小于 60 秒,很容易導(dǎo)致數(shù)據(jù)錯亂;
- 另外,Linux 會丟棄所有來自遠端時間戳小于上次記錄的時間戳(由同一個遠端發(fā)送的)的任何數(shù)據(jù)包。就是說要使用該選項,則必須保證數(shù)據(jù)包的時間戳是單調(diào)遞增的。那么,問題在于,此處的時間戳并不是我們通常意義上面的絕對時間,而是一個相對時間。很多情況下,我們是沒法保證時間戳單調(diào)遞增的,比如使用了 NAT,LVS 等情況;
所以,不建議設(shè)置為 1 ,建議關(guān)閉它:
在 Linux 4.12 版本后,Linux 內(nèi)核直接取消了這一參數(shù)。
另外,我們可以在程序中設(shè)置 socket 選項,來設(shè)置調(diào)用 close 關(guān)閉連接行為。
如果l_onoff
為非 0, 且l_linger
值為 0,那么調(diào)用close后,會立該發(fā)送一個 RST 標志給對端,該 TCP 連接將跳過四次揮手,也就跳過了 TIME_WAIT 狀態(tài),直接關(guān)閉。
但這為跨越 TIME_WAIT 狀態(tài)提供了一個可能,不過是一個非常危險的行為,不值得提倡。
被動方的優(yōu)化
當(dāng)被動方收到 FIN 報文時,內(nèi)核會自動回復(fù) ACK,同時連接處于 CLOSE_WAIT 狀態(tài),顧名思義,它表示等待應(yīng)用進程調(diào)用 close 函數(shù)關(guān)閉連接。
內(nèi)核沒有權(quán)利替代進程去關(guān)閉連接,因為如果主動方是通過 shutdown 關(guān)閉連接,那么它就是想在半關(guān)閉連接上接收數(shù)據(jù)或發(fā)送數(shù)據(jù)。因此,Linux 并沒有限制 CLOSE_WAIT 狀態(tài)的持續(xù)時間。
當(dāng)然,大多數(shù)應(yīng)用程序并不使用 shutdown 函數(shù)關(guān)閉連接。所以,當(dāng)你用 netstat 命令發(fā)現(xiàn)大量 CLOSE_WAIT 狀態(tài)。就需要排查你的應(yīng)用程序,因為可能因為應(yīng)用程序出現(xiàn)了 Bug,read 函數(shù)返回 0 時,沒有調(diào)用 close 函數(shù)。
處于 CLOSE_WAIT 狀態(tài)時,調(diào)用了 close 函數(shù),內(nèi)核就會發(fā)出 FIN 報文關(guān)閉發(fā)送通道,同時連接進入 LAST_ACK 狀態(tài),等待主動方返回 ACK 來確認連接關(guān)閉。
如果遲遲收不到這個 ACK,內(nèi)核就會重發(fā) FIN 報文,重發(fā)次數(shù)仍然由 tcp_orphan_retries 參數(shù)控制,這與主動方重發(fā) FIN 報文的優(yōu)化策略一致。
還有一點我們需要注意的,如果被動方迅速調(diào)用 close 函數(shù),那么被動方的 ACK 和 FIN 有可能在一個報文中發(fā)送,這樣看起來,四次揮手會變成三次揮手,這只是一種特殊情況,不用在意。
如果連接雙方同時關(guān)閉連接,會怎么樣?
由于 TCP 是雙全工的協(xié)議,所以是會出現(xiàn)兩方同時關(guān)閉連接的現(xiàn)象,也就是同時發(fā)送了 FIN 報文。
此時,上面介紹的優(yōu)化策略仍然適用。兩方發(fā)送 FIN 報文時,都認為自己是主動方,所以都進入了 FIN_WAIT1 狀態(tài),F(xiàn)IN 報文的重發(fā)次數(shù)仍由 tcp_orphan_retries 參數(shù)控制。
接下來,雙方在等待 ACK 報文的過程中,都等來了 FIN 報文。這是一種新情況,所以連接會進入一種叫做 CLOSING 的新狀態(tài),它替代了 FIN_WAIT2 狀態(tài)。接著,雙方內(nèi)核回復(fù) ACK 確認對方發(fā)送通道的關(guān)閉后,進入 TIME_WAIT 狀態(tài),等待 2MSL 的時間后,連接自動關(guān)閉。
小結(jié)
針對 TCP 四次揮手的優(yōu)化,我們需要根據(jù)主動方和被動方四次揮手狀態(tài)變化來調(diào)整系統(tǒng) TCP 內(nèi)核參數(shù)。
主動方的優(yōu)化
主動發(fā)起 FIN 報文斷開連接的一方,如果遲遲沒收到對方的 ACK 回復(fù),則會重傳 FIN 報文,重傳的次數(shù)由?tcp_orphan_retries
?參數(shù)決定。
當(dāng)主動方收到 ACK 報文后,連接就進入 FIN_WAIT2 狀態(tài),根據(jù)關(guān)閉的方式不同,優(yōu)化的方式也不同:
- 如果這是 close 函數(shù)關(guān)閉的連接,那么它就是孤兒連接。如果?
tcp_fin_timeout
?秒內(nèi)沒有收到對方的 FIN 報文,連接就直接關(guān)閉。同時,為了應(yīng)對孤兒連接占用太多的資源,tcp_max_orphans
?定義了最大孤兒連接的數(shù)量,超過時連接就會直接釋放。 - 反之是 shutdown 函數(shù)關(guān)閉的連接,則不受此參數(shù)限制;
當(dāng)主動方接收到 FIN 報文,并返回 ACK 后,主動方的連接進入 TIME_WAIT 狀態(tài)。這一狀態(tài)會持續(xù) 1 分鐘,為了防止 TIME_WAIT 狀態(tài)占用太多的資源,tcp_max_tw_buckets
?定義了最大數(shù)量,超過時連接也會直接釋放。
當(dāng) TIME_WAIT 狀態(tài)過多時,還可以通過設(shè)置?tcp_tw_reuse
?和?tcp_timestamps
?為 1 ,將 TIME_WAIT 狀態(tài)的端口復(fù)用于作為客戶端的新連接,注意該參數(shù)只適用于客戶端。
被動方的優(yōu)化
被動關(guān)閉的連接方應(yīng)對非常簡單,它在回復(fù) ACK 后就進入了 CLOSE_WAIT 狀態(tài),等待進程調(diào)用 close 函數(shù)關(guān)閉連接。因此,出現(xiàn)大量 CLOSE_WAIT 狀態(tài)的連接時,應(yīng)當(dāng)從應(yīng)用程序中找問題。
當(dāng)被動方發(fā)送 FIN 報文后,連接就進入 LAST_ACK 狀態(tài),在未等到 ACK 時,會在?tcp_orphan_retries
參數(shù)的控制下重發(fā) FIN 報文。
03 TCP 傳輸數(shù)據(jù)的性能提升
在前面介紹的是三次握手和四次揮手的優(yōu)化策略,接下來主要介紹的是 TCP 傳輸數(shù)據(jù)時的優(yōu)化策略。
TCP 連接是由內(nèi)核維護的,內(nèi)核會為每個連接建立內(nèi)存緩沖區(qū):
- 如果連接的內(nèi)存配置過小,就無法充分使用網(wǎng)絡(luò)帶寬,TCP 傳輸效率就會降低;
- 如果連接的內(nèi)存配置過大,很容易把服務(wù)器資源耗盡,這樣就會導(dǎo)致新連接無法建立;
因此,我們必須理解 Linux 下 TCP 內(nèi)存的用途,才能正確地配置內(nèi)存大小。
滑動窗口是如何影響傳輸速度的?
TCP 會保證每一個報文都能夠抵達對方,它的機制是這樣:報文發(fā)出去后,必須接收到對方返回的確認報文 ACK,如果遲遲未收到,就會超時重發(fā)該報文,直到收到對方的 ACK 為止。
所以,TCP 報文發(fā)出去后,并不會立馬從內(nèi)存中刪除,因為重傳時還需要用到它。
由于 TCP 是內(nèi)核維護的,所以報文存放在內(nèi)核緩沖區(qū)。如果連接非常多,我們可以通過 free 命令觀察到?buff/cache
?內(nèi)存是會增大。
如果 TCP 是每發(fā)送一個數(shù)據(jù),都要進行一次確認應(yīng)答。當(dāng)上一個數(shù)據(jù)包收到了應(yīng)答了, 再發(fā)送下一個。這個模式就有點像我和你面對面聊天,你一句我一句,但這種方式的缺點是效率比較低的。
所以,這樣的傳輸方式有一個缺點:數(shù)據(jù)包的往返時間越長,通信的效率就越低。
要解決這一問題不難,并行批量發(fā)送報文,再批量確認報文即刻。
然而,這引出了另一個問題,發(fā)送方可以隨心所欲的發(fā)送報文嗎?當(dāng)然這不現(xiàn)實,我們還得考慮接收方的處理能力。
當(dāng)接收方硬件不如發(fā)送方,或者系統(tǒng)繁忙、資源緊張時,是無法瞬間處理這么多報文的。于是,這些報文只能被丟掉,使得網(wǎng)絡(luò)效率非常低。
為了解決這種現(xiàn)象發(fā)生,TCP 提供一種機制可以讓「發(fā)送方」根據(jù)「接收方」的實際接收能力控制發(fā)送的數(shù)據(jù)量,這就是滑動窗口的由來。
接收方根據(jù)它的緩沖區(qū),可以計算出后續(xù)能夠接收多少字節(jié)的報文,這個數(shù)字叫做接收窗口。當(dāng)內(nèi)核接收到報文時,必須用緩沖區(qū)存放它們,這樣剩余緩沖區(qū)空間變小,接收窗口也就變小了;當(dāng)進程調(diào)用 read 函數(shù)后,數(shù)據(jù)被讀入了用戶空間,內(nèi)核緩沖區(qū)就被清空,這意味著主機可以接收更多的報文,接收窗口就會變大。
因此,接收窗口并不是恒定不變的,接收方會把當(dāng)前可接收的大小放在 TCP 報文頭部中的窗口字段,這樣就可以起到窗口大小通知的作用。
發(fā)送方的窗口等價于接收方的窗口嗎?如果不考慮擁塞控制,發(fā)送方的窗口大小「約等于」接收方的窗口大小,因為窗口通知報文在網(wǎng)絡(luò)傳輸是存在時延的,所以是約等于的關(guān)系。
從上圖中可以看到,窗口字段只有 2 個字節(jié),因此它最多能表達 65535 字節(jié)大小的窗口,也就是 64KB 大小。
這個窗口大小最大值,在當(dāng)今高速網(wǎng)絡(luò)下,很明顯是不夠用的。所以后續(xù)有了擴充窗口的方法:在 TCP 選項字段定義了窗口擴大因子,用于擴大TCP通告窗口,使 TCP 的窗口定義從 2 個字節(jié)(16 位) 增加為 4 字節(jié)(32 位),所以此時窗口的最大值可以達到 1GB。
Linux 中打開這一功能,需要把 tcp_window_scaling 配置設(shè)為 1(默認打開):
要使用窗口擴大選項,通訊雙方必須在各自的 SYN 報文中發(fā)送這個選項:
- 主動建立連接的一方在 SYN 報文中發(fā)送這個選項;
- 而被動建立連接的一方只有在收到帶窗口擴大選項的 SYN 報文之后才能發(fā)送這個選項。
這樣看來,只要進程能及時地調(diào)用 read 函數(shù)讀取數(shù)據(jù),并且接收緩沖區(qū)配置得足夠大,那么接收窗口就可以無限地放大,發(fā)送方也就無限地提升發(fā)送速度。
這是不可能的,因為網(wǎng)絡(luò)的傳輸能力是有限的,當(dāng)發(fā)送方依據(jù)發(fā)送窗口,發(fā)送超過網(wǎng)絡(luò)處理能力的報文時,路由器會直接丟棄這些報文。因此,緩沖區(qū)的內(nèi)存并不是越大越好。
如果確定最大傳輸速度?
在前面我們知道了 TCP 的傳輸速度,受制于發(fā)送窗口與接收窗口,以及網(wǎng)絡(luò)設(shè)備傳輸能力。其中,窗口大小由內(nèi)核緩沖區(qū)大小決定。如果緩沖區(qū)與網(wǎng)絡(luò)傳輸能力匹配,那么緩沖區(qū)的利用率就達到了最大化。
問題來了,如何計算網(wǎng)絡(luò)的傳輸能力呢?
相信大家都知道網(wǎng)絡(luò)是有「帶寬」限制的,帶寬描述的是網(wǎng)絡(luò)傳輸能力,它與內(nèi)核緩沖區(qū)的計量單位不同:
- 帶寬是單位時間內(nèi)的流量,表達是「速度」,比如常見的帶寬 100 MB/s;
- 緩沖區(qū)單位是字節(jié),當(dāng)網(wǎng)絡(luò)速度乘以時間才能得到字節(jié)數(shù);
這里需要說一個概念,就是帶寬時延積,它決定網(wǎng)絡(luò)中飛行報文的大小,它的計算方式:
比如最大帶寬是 100 MB/s,網(wǎng)絡(luò)時延(RTT)是 10ms 時,意味著客戶端到服務(wù)端的網(wǎng)絡(luò)一共可以存放 100MB/s * 0.01s = 1MB 的字節(jié)。
這個 1MB 是帶寬和時延的乘積,所以它就叫「帶寬時延積」(縮寫為 BDP,Bandwidth Delay Product)。同時,這 1MB 也表示「飛行中」的 TCP 報文大小,它們就在網(wǎng)絡(luò)線路、路由器等網(wǎng)絡(luò)設(shè)備上。如果飛行報文超過了 1 MB,就會導(dǎo)致網(wǎng)絡(luò)過載,容易丟包。
由于發(fā)送緩沖區(qū)大小決定了發(fā)送窗口的上限,而發(fā)送窗口又決定了「已發(fā)送未確認」的飛行報文的上限。因此,發(fā)送緩沖區(qū)不能超過「帶寬時延積」。
發(fā)送緩沖區(qū)與帶寬時延積的關(guān)系:
- 如果發(fā)送緩沖區(qū)「超過」帶寬時延積,超出的部分就沒辦法有效的網(wǎng)絡(luò)傳輸,同時導(dǎo)致網(wǎng)絡(luò)過載,容易丟包;
- 如果發(fā)送緩沖區(qū)「小于」帶寬時延積,就不能很好的發(fā)揮出網(wǎng)絡(luò)的傳輸效率。
所以,發(fā)送緩沖區(qū)的大小最好是往帶寬時延積靠近。
怎樣調(diào)整緩沖區(qū)大???
在 Linux 中發(fā)送緩沖區(qū)和接收緩沖都是可以用參數(shù)調(diào)節(jié)的。設(shè)置完后,Linux 會根據(jù)你設(shè)置的緩沖區(qū)進行動態(tài)調(diào)節(jié)。
調(diào)節(jié)發(fā)送緩沖區(qū)范圍
先來看看發(fā)送緩沖區(qū),它的范圍通過 tcp_wmem 參數(shù)配置;
上面三個數(shù)字單位都是字節(jié),它們分別表示:
- 第一個數(shù)值是動態(tài)范圍的最小值,4096 byte = 4K;
- 第二個數(shù)值是初始默認值,87380 byte ≈ 86K;
- 第三個數(shù)值是動態(tài)范圍的最大值,4194304 byte = 4096K(4M);
發(fā)送緩沖區(qū)是自行調(diào)節(jié)的,當(dāng)發(fā)送方發(fā)送的數(shù)據(jù)被確認后,并且沒有新的數(shù)據(jù)要發(fā)送,就會把發(fā)送緩沖區(qū)的內(nèi)存釋放掉。
調(diào)節(jié)接收緩沖區(qū)范圍
而接收緩沖區(qū)的調(diào)整就比較復(fù)雜一些,先來看看設(shè)置接收緩沖區(qū)范圍的 tcp_rmem 參數(shù):
上面三個數(shù)字單位都是字節(jié),它們分別表示:
- 第一個數(shù)值是動態(tài)范圍的最小值,表示即使在內(nèi)存壓力下也可以保證的最小接收緩沖區(qū)大小,4096 byte = 4K;
- 第二個數(shù)值是初始默認值,87380 byte ≈ 86K;
- 第三個數(shù)值是動態(tài)范圍的最大值,6291456 byte = 6144K(6M);
接收緩沖區(qū)可以根據(jù)系統(tǒng)空閑內(nèi)存的大小來調(diào)節(jié)接收窗口:
- 如果系統(tǒng)的空閑內(nèi)存很多,就可以自動把緩沖區(qū)增大一些,這樣傳給對方的接收窗口也會變大,因而提升發(fā)送方發(fā)送的傳輸數(shù)據(jù)數(shù)量;
- 反正,如果系統(tǒng)的內(nèi)存很緊張,就會減少緩沖區(qū),這雖然會降低傳輸效率,可以保證更多的并發(fā)連接正常工作;
發(fā)送緩沖區(qū)的調(diào)節(jié)功能是自動開啟的,而接收緩沖區(qū)則需要配置 tcp_moderate_rcvbuf 為 1 來開啟調(diào)節(jié)功能:
調(diào)節(jié) TCP 內(nèi)存范圍
接收緩沖區(qū)調(diào)節(jié)時,怎么知道當(dāng)前內(nèi)存是否緊張或充分呢?這是通過 tcp_mem 配置完成的:
上面三個數(shù)字單位不是字節(jié),而是「頁面大小」,1 頁表示 4KB,它們分別表示:
- 當(dāng) TCP 內(nèi)存小于第 1 個值時,不需要進行自動調(diào)節(jié);
- 在第 1 和第 2 個值之間時,內(nèi)核開始調(diào)節(jié)接收緩沖區(qū)的大?。?/li>
- 大于第 3 個值時,內(nèi)核不再為 TCP 分配新內(nèi)存,此時新連接是無法建立的;
一般情況下這些值是在系統(tǒng)啟動時根據(jù)系統(tǒng)內(nèi)存數(shù)量計算得到的。根據(jù)當(dāng)前 tcp_mem 最大內(nèi)存頁面數(shù)是 177120,當(dāng)內(nèi)存為 (177120 * 4) / 1024K ≈ 692M 時,系統(tǒng)將無法為新的 TCP 連接分配內(nèi)存,即 TCP 連接將被拒絕。
根據(jù)實際場景調(diào)節(jié)的策略
在高并發(fā)服務(wù)器中,為了兼顧網(wǎng)速與大量的并發(fā)連接,我們應(yīng)當(dāng)保證緩沖區(qū)的動態(tài)調(diào)整的最大值達到帶寬時延積,而最小值保持默認的 4K 不變即可。而對于內(nèi)存緊張的服務(wù)而言,調(diào)低默認值是提高并發(fā)的有效手段。
同時,如果這是網(wǎng)絡(luò) IO 型服務(wù)器,那么,調(diào)大 tcp_mem 的上限可以讓 TCP 連接使用更多的系統(tǒng)內(nèi)存,這有利于提升并發(fā)能力。需要注意的是,tcp_wmem 和 tcp_rmem 的單位是字節(jié),而 tcp_mem 的單位是頁面大小。而且,千萬不要在 socket 上直接設(shè)置 SO_SNDBUF 或者 SO_RCVBUF,這樣會關(guān)閉緩沖區(qū)的動態(tài)調(diào)整功能。
小結(jié)
本節(jié)針對 TCP 優(yōu)化數(shù)據(jù)傳輸?shù)姆绞?,做了一些介紹。
TCP 可靠性是通過 ACK 確認報文實現(xiàn)的,又依賴滑動窗口提升了發(fā)送速度也兼顧了接收方的處理能力。
可是,默認的滑動窗口最大值只有 64 KB,不滿足當(dāng)今的高速網(wǎng)絡(luò)的要求,要想要想提升發(fā)送速度必須提升滑動窗口的上限,在 Linux 下是通過設(shè)置?tcp_window_scaling
?為 1 做到的,此時最大值可高達 1GB。
滑動窗口定義了網(wǎng)絡(luò)中飛行報文的最大字節(jié)數(shù),當(dāng)它超過帶寬時延積時,網(wǎng)絡(luò)過載,就會發(fā)生丟包。而當(dāng)它小于帶寬時延積時,就無法充分利用網(wǎng)絡(luò)帶寬。因此,滑動窗口的設(shè)置,必須參考帶寬時延積。
內(nèi)核緩沖區(qū)決定了滑動窗口的上限,緩沖區(qū)可分為:發(fā)送緩沖區(qū) tcp_wmem 和接收緩沖區(qū) tcp_rmem。
Linux 會對緩沖區(qū)動態(tài)調(diào)節(jié),我們應(yīng)該把緩沖區(qū)的上限設(shè)置為帶寬時延積。發(fā)送緩沖區(qū)的調(diào)節(jié)功能是自動打開的,而接收緩沖區(qū)需要把 tcp_moderate_rcvbuf 設(shè)置為 1 來開啟。其中,調(diào)節(jié)的依據(jù)是 TCP 內(nèi)存范圍 tcp_mem。
但需要注意的是,如果程序中的 socket 設(shè)置 SO_SNDBUF 和 SO_RCVBUF,則會關(guān)閉緩沖區(qū)的動態(tài)整功能,所以不建議在程序設(shè)置它倆,而是交給內(nèi)核自動調(diào)整比較好。
有效配置這些參數(shù)后,既能夠最大程度地保持并發(fā)性,也能讓資源充裕時連接傳輸速度達到最大值。
TCP 性能優(yōu)化詳解
https://www.zhuxiaodong.net/2018/tcp-performance-optimize-instruction/
延遲( latency )
延遲是指分組從信息源發(fā)送到目的地所需要的時間。影響延遲的因素包括:
- 傳播延遲:消息從發(fā)送端到接收端需要的時間,是信號的傳播距離和速度的函數(shù)
- 傳輸延遲:把消息中的所有比特轉(zhuǎn)移到鏈路中需要的時間,是消息長度和鏈路速率的函數(shù)
- 處理延遲:處理分組首部、檢查位錯誤及確定分組目標所需的時間
- 排隊延遲:到來的分組排隊等待處理的時間
以上延遲的時間總和,就是客戶端到服務(wù)器的總延遲時間。
我們都知道,光在真空當(dāng)中的傳播速度大概是30萬公里/每秒,但是這是最理想的情況下網(wǎng)絡(luò)中傳播速度的極限值。實際上,根據(jù)不同的傳播介質(zhì),會影響到實際的傳輸速度。常見的介質(zhì)有銅絞線,光纖等。折射率是光速與分組在介質(zhì)中傳播速度之比,折射率越大,光在介質(zhì)中的傳播速度就越慢。基本上,以目前的技術(shù),傳播分組的光纖的大多數(shù)折射率在 1.4 ~ 1.6 之間。
我們假定光通過光纖的速度約20萬公里/每秒,對應(yīng)的折射率約為1.5,讓我們來看幾個實際例子:
路線 | 距離(km) | 時間:光在真空中 | 時間:光在光纖中 | 光纖中的 RTT |
---|---|---|---|---|
紐約到舊金山 | 4148 | 14 ms | 21 ms | 42 ms |
紐約到倫敦 | 5585 | 19 ms | 28 ms | 56 ms |
紐約到悉尼 | 15993 | 53 ms | 80 ms | 160 ms |
赤道周長 | 40075 | 133.7 ms | 200 ms | 400 ms |
這里推薦一個根據(jù)目的地IP或者域名測試 RTT 的網(wǎng)站:http://tools.cloudxns.net/Index/Ping
此外,我們可以使用 traceroute ( windows 下是 tracert ) 命令,追蹤數(shù)據(jù)包在網(wǎng)絡(luò)上的傳輸時的全部路徑。traceroute 通過發(fā)送小的數(shù)據(jù)包到目的設(shè)備直到其返回,來測量其需要多長時間。一條路徑上的每個設(shè)備 traceroute 要測3次。輸出結(jié)果中包括每次測試的時間(ms)和設(shè)備的名稱(如有的話)及其ip地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
traceroute?www.newegg.com traceroute?to?e5638.g.akamaiedge.net (23.35.193.40), 64?hops?max, 52?byte?packets 1 119.6.99.1 (119.6.99.1) 1.736?ms 5.620?ms 1.615?ms 2 119.6.193.73 (119.6.193.73) 2.442?ms 2.215?ms 2.322?ms 3 119.6.193.73 (119.6.193.73) 1.630?ms 2.225?ms 2.278?ms 4 119.6.195.13 (119.6.195.13) 42.078?ms 119.6.197.61 (119.6.197.61) 40.759?ms 119.6.197.57 (119.6.197.57) 38.476?ms 5 219.158.103.141 (219.158.103.141) 40.583?ms 38.999?ms 40.098?ms 6 * 219.158.4.170 (219.158.4.170) 43.155?ms 39.602?ms 7 219.158.3.50 (219.158.3.50) 39.884?ms 39.322?ms 39.744?ms 8?xe-4-1-0.r26.tokyjp05.jp.bb.gin.ntt.net (129.250.8.93) 82.547?ms 92.972?ms 82.852?ms 9?ae-1.r31.tokyjp05.jp.bb.gin.ntt.net (129.250.2.153) 114.856?ms ae-0.r30.tokyjp05.jp.bb.gin.ntt.net (129.250.2.11) 123.523?ms 82.659?ms 10?ae-2.r03.tokyjp05.jp.bb.gin.ntt.net (129.250.3.33) 78.776?ms 78.786?ms ae-3.r03.tokyjp05.jp.bb.gin.ntt.net (129.250.3.56) 111.784?ms 11 61.213.179.10 (61.213.179.10) 123.748?ms 127.927?ms 125.158?ms 12?a23-35-193-40.deploy.static.akamaitechnologies.com (23.35.193.40) 78.159?ms 77.882?ms 78.121?ms |
關(guān)于 traceroute 更多內(nèi)容可以參考:?traceroute(8) - Linux man page
在 Mac 下,我們可以使用一個用戶體驗更好的工具,?BestTrace,能夠以地圖的方式顯示出每一跳的信息,非常直觀。
如果不想安裝該軟件,也可以使用:TraceRoute查詢_專業(yè)精準的IP庫服務(wù)商_IPIP
帶寬 ( Bandwidth )
帶寬是指數(shù)據(jù)的發(fā)送速度。比如我們的百兆網(wǎng)卡,就是指網(wǎng)卡的最大發(fā)送速度為100Mbps(注意 b 是指 bit 而不是 byte)。發(fā)送速度和下面幾個因素有關(guān)系:
-
數(shù)據(jù)發(fā)送裝置將二進制信號傳送至線路的能力,也稱之為信號傳輸頻率,以及另一端的數(shù)據(jù)接收裝置對二進制信號的接收能力,同時也包括線路對傳輸頻率的支持程度。
-
數(shù)據(jù)傳播介質(zhì)的并行度,也可以稱之為寬度,完全等價于計算機系統(tǒng)總線寬度的概念。比如在光纖傳輸中,我們可以將若干條纖細的光纖并行組成光纜,這樣就可以在一個橫截面上同時傳輸多個信號,就像在32位的計算機總線中,可以同一時刻傳輸32位數(shù)據(jù)。需要注意的是,要提高計算機總線的帶寬,包括提高總線頻率和總線寬度兩種方法,比如使用64位總線系統(tǒng)或者使用主頻更高的處理器等。這兩種方法與以上數(shù)字通信帶寬的兩個決定因素完全相似。
如何實際的測試你的網(wǎng)絡(luò)帶寬?
雖然 ISP 或者 IDC 運營商有在你購買帶寬的時候,表明提供的帶寬大小,比如 100Mbps,但是那是理論值,很有可能實際的帶寬達不到官方宣傳的指標。
一般個人使用的 PC 上,可以使用?speedtest?來進行測試。 speedtest 能夠幫助你測試上傳速度和下載速度。
另外國內(nèi)有一個類似的網(wǎng)站叫: www.speedtest.cn ,但是相對于 speedtest.net 來說,缺少切換服務(wù)器進行測試的功能。
如果你是在服務(wù)器上或者命令行想進行測試,可以選擇?speedtest-cli?進行測試
1 2 3 4 5 6 7 8 9 10 11 |
./speedtest-cli Retrieving speedtest.net configuration... Testing?from xx.xx.xx.xx. (xx.xx.xx.xx)... Retrieving speedtest.net server list... Selecting best server based on ping... Hosted by xx.xx.xx.xx [4.87 km]:?31.317 ms Testing download speed................................................................................ Download:?179.52 Mbit/s Testing upload speed................................................................................................ Upload:?1.18 Mbit/s You have new mail?in /var/spool/mail/root |
可以看到,上述的測試結(jié)果還是比較準確的,Upload 顯示的為:1.18 Mbit/s ,而測試的這臺服務(wù)器的出口帶寬為 1 Mbit/s 。
關(guān)于帶寬
的更多知識,強烈建議仔細閱讀《構(gòu)建高性能Web站點》?第二章
總結(jié)
可以通過上述的概念的描述,我們可以發(fā)現(xiàn), Latency 和 Bandwidth 決定了網(wǎng)絡(luò)性能,但是在絕大部分場景下,影響傳輸性能的最關(guān)鍵的因素是 Latency 。因為就算帶寬再高,我們也無法繞過由于物理距離帶來的高延遲問題。因此,減少延遲就作為了一個非常核心的優(yōu)化指標。
TCP Fast Open
設(shè)計目標
http 的 keepalive 受限于 idle 時間,據(jù) google 的統(tǒng)計( chrome 瀏覽器),盡管 chrome 開啟了 http 的 keepalive ( chrome 默認是4分鐘 ),可是依然有 35% 的請求是重新發(fā)起一條連接。而三次握手會造成一個 RTT 的延遲,因此 TFO 的目標就是去除這個延遲,在三次握手期間也能交換數(shù)據(jù)。
定義
TCP 快速打開是對 TCP 連接的一種簡化握手手續(xù)的拓展,用于提高兩端點間連接的打開速度。它通過握手開始時的 SYN 包中的 TFO cookie (一個TCP擴展選項)來驗證一個之前連接過的客戶端。如果驗證成功,它可以在三次握手最終的 ACK 包收到之前就開始發(fā)送數(shù)據(jù),這樣能夠減少一個 RTT 的時間,從而降低了延遲。這個加密的Cookie被存儲在客戶端,在一開始的連接時被設(shè)定好。然后每當(dāng)客戶端連接時,這個Cookie被重復(fù)返回。
具體的步驟參考下圖:
開啟 nginx tcp fast open
在 Nginx 1.5.8 版本以及之后,listen 指令開始支持 fastopen 參數(shù)。需要注意的是:Linux 內(nèi)核版本必須在 3.7.1 以及以上的版本才支持 TCP fast open 。
首先需要內(nèi)核開啟對 tcp fast open 的支持:
1 2 3 4 |
sysctl -w net.ipv4.tcp_fastopen=3 # 或者要再重啟了之后永久生效,可以使用如下方式 echo?"net.ipv4.tcp_fastopen=3"?>> /etc/sysctl.conf |
然后編譯 nginx 的時候需要增加參數(shù):
1 |
--with-cc-opt='-DTCP_FASTOPEN=23' |
最后,修改 nginx 配置文件:
1 |
listen?80 fastopen=256 |
驗證請求是否有使用到 TCP Fast Open 有兩種方式:
- 在服務(wù)器端直接觀察 TCP Fast Open 的狀態(tài),查看 TCPFastOpenPassive 字段的數(shù)字是否會隨著使用而增加。
1 2 3 |
grep?'^TcpExt:' /proc/net/netstat |?cut -d ' ' -f 91-96 |?column -t TCPFastOpenPassive?TCPFastOpenPassiveFail?TCPFastOpenListenOverflow?TCPFastOpenCookieReqd?TCPSpuriousRtxHostQueues?BusyPollRxPackets 13 0 0 0 15 0 |
- 使用 Wireshark 抓包:
- 觀察客戶端出發(fā)的第一個 SYN 包,是否包含 TFO=R TCP 擴展選項
- 觀察服務(wù)端回應(yīng)的 SYN-ACK 包,是否包含 TFO=C TCP 擴展選項
- 觀察之后發(fā)出的 SYN 包,是否包含 TFO=C 標記,同時該包有 data
- 若 1 失敗,說明客戶端沒有發(fā)出 TFO 請求
- 若 2 失敗,說明服務(wù)器端配置有誤,未能正確啟動 TFO 支持
tcp fast open 目前的支持情況
目前大部分客戶端瀏覽器不支持,比如 chrome 只在 Linux,Android,Chrome OS 才支持,參考:這里;Microsoft Edge 從 Windows 10 Preview build 14352開始支持 TFO ;Mozilla Firefox 56 將支持 TFO 。
可見,在 web 瀏覽器端 TFO 并沒有得到普及。如果 google tcp fast open ,會發(fā)現(xiàn)其更多的應(yīng)用場景是用于優(yōu)化KeXueShagnWang的梯子上。例如,這里?和?這里
curl 客戶端支持 tfo ,可以使用下面的命令來開啟做測試:
1 |
curl --tcp-fastopen?https://www.zhuxiaodong.net |
ref:
https://zh.wikipedia.org/wiki/TCP%E5%BF%AB%E9%80%9F%E6%89%93%E5%BC%80
Module ngx_http_core_module
Speed up web delivery with Nginx and TFO – UnixTeacher
https://gist.github.com/denji/8359866
RFC 7413 - TCP Fast Open
TFO(tcp fast open)簡介_51CTO博客_tcp fast open
基于 TCP 流量控制和擁塞控制的優(yōu)化
窗口縮放選項
我們在上一篇文章中已經(jīng)完整討論了 TCP 流量控制中:滑動窗口、默認的 rwnd (接收窗口)的大?。?2 ^ 16 = 65536 = 64KB )、窗口縮放選項及其對性能的影響等知識點,這里就不再累述。我們只需要注意,Linux 服務(wù)器的內(nèi)核版本選擇,需要高于 2.6.8 版本,這樣默認窗口擴大選項就是開啟的。
1 2 |
sysctl -n net.ipv4.tcp_window_scaling 1 |
初始擁塞窗口
還是在上一篇文章中,我們學(xué)習(xí)了擁塞控制相關(guān)的算法依賴于 TCP 連接初始化一個新的擁塞窗口( cwnd ),會將其設(shè)置成一個初始值,即: initcwnd 。cwnd 決定了發(fā)送端對接收端 ACK 之前,可以發(fā)送數(shù)據(jù)量的限制。
新 TCP 連接傳輸?shù)淖畲髷?shù)據(jù)量取 rwnd 和 cwnd 中的最小值。雖然服務(wù)器實際上可以向客戶端發(fā)送 4個 MSS ,但是最開始的時候必須停下來等待確認。此后,每收到一個 ACK ,慢啟動算法就會告訴服務(wù)器可以將它的 cwnd 窗口增加1個 MSS 。每次收到 ACK 后,都可以多發(fā)送兩個新的分組。TCP連接的這個階段通常被稱為“指數(shù)增長”階段,因為客戶端和服務(wù)器都在向兩者之間網(wǎng)絡(luò)路徑的有效帶寬迅速靠攏。上述算法決定了,無論你的帶寬有多大,都無法在一開始的時候就完全利用連接的最大帶寬。
根據(jù)擁塞控制算法中的慢啟動算法,在分組被確認后逐步增加 cwnd 的大小。最初的時候,初始的 cwnd 的值只有 1 個 MSS ;1999年4月,RFC 2581 將其增加到了4個 MSS 。2013年4月,RFC 6928再次將其提高到10個 MSS 。
以下的公式描述了 cwnd 大小達到 N 字節(jié)大小所需要花費的時間:
我們來看實際的示例:
- 客戶端和服務(wù)器的 rwnd 為 64 KB
- initcwnd = 4 MSS ( RFC 2581 )
- RTT = 56 ms
根據(jù)上面的公式,我們可以計算出:
- 首先計算達到 N 字節(jié)的大?。ㄟ@里的 N = 64KB = 65535 byte),需要多少個 MSS,即: 65535 / 1460 = 45
- 將 N 代入公式,可以計算出所需要花費的時間為 224 ms。
要達到客戶端與服務(wù)器之間64 KB的吞吐量,需要4次 RTT,幾百毫秒的延遲!至于客戶端與服務(wù)器之間實際的連接速率是不是在 Mbps 級別,絲毫不影響這個結(jié)果。這就是慢啟動。
現(xiàn)在,假設(shè)只修改 initcwnd = 10 MSS ,其它條件都不變,根據(jù)上述公式,我們可以計算出其花費的時間只需要 168 ms。
由此,我們得出結(jié)論,要想減少 TCP 連接中完全利用到最大帶寬的所花費的時間,要么減少 RTT (減少 RTT 其實就是減少物理距離),要么增加 initcwnd (盡量使用 Linux 內(nèi)核 2.6.39 之后的版本)。
關(guān)于 initcwnd 的更多信息,強烈建議閱讀一下 cdnplanet 的?這篇文章,文中完整的介紹了,初始窗口對傳輸時間的影響、擁塞控制的慢啟動算法、如何調(diào)整和查看 initcwnd 和 initrwnd 、調(diào)整了之后的測試結(jié)論等信息。以下的圖例是各個操作系統(tǒng),初始窗口的大小默認值(注意,是初始窗口大小,而不是初始擁塞窗口或者初始接收窗口的大小,rwin = min(initcwnd, initrwnd)?)
慢啟動重啟( Slow-Start Restart )
慢啟動重啟會在連接空閑一定時間后重置連接的擁塞窗口。道理很簡單,在連接空閑的同時,網(wǎng)絡(luò)狀況也可能發(fā)生了變化,為了避免擁塞,理應(yīng)將擁塞窗口重置回“安全的”默認值。
因此,慢啟動重啟對于那些會出現(xiàn)突發(fā)空閑的長周期TCP連接(比如 HTTP 的 keep-alive 連接)有很大的影響。因此,我們建議在服務(wù)器上禁用慢啟動重啟。在Linux平臺,可以通過如下命令來檢查和禁用慢啟動重啟:
1 2 3 4 5 6 |
sysctl -n net.ipv4.tcp_slow_start_after_idle 1 vim /etc/sysctl.conf net.ipv4.tcp_slow_start_after_idle=0 sysctl -p |
三次握手和慢啟動對 HTTP 傳輸性能的影響
為了更好的說明三次握手以及慢啟動階段對 HTTP 傳輸性能的影響,讓我們看一個實際的例子。我們假設(shè)涉及的相關(guān)參數(shù)信息如下:
- http 請求文件的文件大?。?64 KB
- RTT:56 ms
- rwnd : 64 KB
- bandwidth: 5 Mbps
- initcwnd : 10 ( 10 x 1460 byte = 14 KB)
- 服務(wù)器端處理請求所花費的時間: 40 ms
- 傳輸過程中沒有發(fā)生丟包,每個數(shù)據(jù)包都要確認,GET 請求值只占 1 段。
從上述的圖示中我們可以看出,傳輸 64 KB 的文件需要總共花費 264 ms 的時間。
我們假設(shè) TCP 連接能夠重用同一個連接,重復(fù)上述的過程重新進行傳輸,其過程大概如下:
- 0 ms : 客戶端發(fā)起 http 請求
- 28 ms : 服務(wù)器端接收到 http 請求
- 68 ms : Server 端花費了 40 ms 的時間處理 64 KB 響應(yīng),此時的 cwnd 的大小已經(jīng)超過了 45 MSS 的大小,因此,直接將整個 64 KB 的文件直接一次性的進行發(fā)送。
- 96 ms : 客戶端接收到了所有的 64 KB的文件。
同樣的一次請求,相比了第一個示例中的 268 ms 處理時間,第二個示例僅僅花費了 96 ms ,性能提升了 275% 。原因在于第二個示例,沒有三次握手的 RTT 延遲,以及沒有初期慢啟動達到最佳狀態(tài)時,所帶來的時間消耗。
同時我們還可以看到,5 Mbps 的 bandwidth 實際上在 TCP 連接的初始階段,對性能沒有任何影響。主要影響因素還是 latency 和 cwnd 的大小。
采用更好的擁塞預(yù)防算法–PRR 算法
在上一篇文章中,我們也學(xué)習(xí)了擁塞預(yù)防的相關(guān)知識,這里就簡單的復(fù)習(xí)一下,加深一下印象。
慢啟動初始以 initcwnd 為大小成倍增加 cwnd 的值之后,當(dāng)超過了接收端流量控制的擁塞閾值,即 ssthresh 窗口,或者在傳輸過程有發(fā)生丟包,此時就會采用擁塞預(yù)防算法。擁塞預(yù)防算法把丟包作為網(wǎng)絡(luò)擁塞的標志,即路徑中某個連接或路由器已經(jīng)擁堵了,以至于必須采取刪包措施。因此,必須調(diào)整窗口大小,以避免造成更多的包丟失,從而保證網(wǎng)絡(luò)暢通。
確定丟包恢復(fù)的最優(yōu)方式并不容易。如果太激進,那么間歇性的丟包就會對整個連接的吞吐量造成很大影響。而如果不夠快,那么還會繼續(xù)造成更多分組丟失。
最初,TCP 使用 AIMD( Multiplicative Decrease and Additive Increase ,倍減加增)算法,即發(fā)生丟包時,先將擁塞窗口減半,然后每次往返再緩慢地給窗口增加一個固定的值。不過,很多時候 AIMD 算法太過保守,因此又有了新的算法。
PRR(Proportional Rate Reduction,比例降速)就是RFC 6937規(guī)定的一個新算法,其目標就是改進丟包后的恢復(fù)速度。根據(jù)谷歌的測量,實現(xiàn)新算法后,因丟包造成的平均連接延遲減少了3%~10% 。
需要注意的是, PRR 算法從 Linux 3.2 版本才開始支持。
帶寬延遲積與窗口大小的關(guān)系
關(guān)于 BDP 的概念我們已經(jīng)在在上一篇文章中學(xué)習(xí)過了,現(xiàn)在我們知道了,發(fā)送端和接收端之間在途未確認的最大數(shù)據(jù)量,取決于擁塞窗口( cwnd )和接收窗口( rwnd )的最小值。接收窗口會隨每次 ACK 一起發(fā)送,而擁塞窗口則由發(fā)送端根據(jù)擁塞控制和預(yù)防算法動態(tài)調(diào)整。
無論發(fā)送端發(fā)送的數(shù)據(jù)還是接收端接收的數(shù)據(jù)超過了未確認的最大數(shù)據(jù)量,都必須停下來等待另一方 ACK 確認某些分組才能繼續(xù)。要等待多長時間呢?取決于往返時間!
BDP(Bandwidth-delay product,帶寬延遲積):數(shù)據(jù)鏈路的容量與其端到端延遲的乘積。這個結(jié)果就是任意時刻處于在途未確認狀態(tài)的最大數(shù)據(jù)量。
因此想要充分利用帶寬,必須讓窗口大小接近 BDP 的大小,才能確保最大吞吐量。
我們通過如下的例子來討論一下,究竟 rwnd 和 cwnd 的值與理論最大使用帶寬的關(guān)系是什么?
- min(cwnd, rwnd) = 16 KB
- RTT = 100 ms
- 16 KB = 16 X 1024 X 8 = 131072 bits
- 131072 / 0.1 = 1310720 bits/s
- 1310720 bits/s = 1310720 / 1000000 = 1.31 Mbps
因此,無論發(fā)送端和接收端的實際帶寬為多大,當(dāng)窗口大小為 16 KB 時,傳輸速率最大只能為 1.31 Mbps 。
再來看另外一個例子,假設(shè)發(fā)送端的帶寬為 10 Mbps ,接收端的帶寬為 100 Mbps,RTT 為 100 ms。如果我們想要充分利用帶寬,也就是客戶端的 10 Mbps,那么計算出的最小窗口值為:
- 10 Mbit/s = 10 X 1000000 = 10000000 bit/s
- 10000000 bit/s = 10000000 / (8 X 1024) = 1221 KB/s
- 1221 KB/s X 0.1 s = 122.1 KB
因此,我們至少需要 122.1 KB 的窗口大小才能充分利用 10 Mbps 的帶寬。并且,如果要想盡量達到最大吞吐量的帶寬速度,要么增加窗口大小,要么減少 RTT 。
隊首阻塞 ( Head-of-line Blocking )
所謂的隊首阻塞是指,由于 TCP 的可靠性和順序到達的特性,要求所有的數(shù)據(jù)包必須按順序傳送到接收端。如果中途有任意一個數(shù)據(jù)包沒能到達接收端,那么后續(xù)的數(shù)據(jù)包必須保存在接收端的 TCP 緩沖區(qū),等待丟失的數(shù)據(jù)包重發(fā)并到達接收端。由于應(yīng)用程序?qū)?TCP 重發(fā)和緩沖區(qū)中排隊的數(shù)據(jù)包一無所知,必須等待所有數(shù)據(jù)包全部到達了之后才能訪問數(shù)據(jù),因此,應(yīng)用程序只能在通過套接字數(shù)據(jù)時感覺到延遲交付。
隊首阻塞造成的延遲可以讓我們的應(yīng)用程序不用關(guān)心分組重排和重組,但是數(shù)據(jù)包到達時間會存在無法預(yù)知的延遲變化,這個時間變化通常被稱為抖動,也是影響應(yīng)用程序性能的一個主要因素。
結(jié)論:無需按序交付數(shù)據(jù)或能夠處理分組丟失的應(yīng)用程序,以及延遲或抖動要求很高應(yīng)用程序,可以考慮 UDP 協(xié)議。
總結(jié)
盡管 TCP 協(xié)議相關(guān)的優(yōu)化算法正在不斷地發(fā)展,但是其核心原理以及它們的影響是不變的:
- TCP三次握手增加了整整一次往返時間
- TCP慢啟動將被應(yīng)用到每個新連接
- TCP流量及擁塞控制會影響所有連接的吞吐量
- TCP的吞吐量由當(dāng)前擁塞窗口大小控制
在大多數(shù)情況下,TCP 性能的瓶頸都是延遲,而非帶寬。
Check List:
-
- 把服務(wù)器內(nèi)核升級到最新版本(Linux:3.2+)
- 確保 cwnd 大小為10 ( 通過 ip route show 進行查看 )
- 禁用空閑后的慢啟動 ( tcp_slow_start_after_idle = 0 )
- 確保啟動窗口縮放( net.ipv4.tcp_window_scaling = 1 )
- 減少傳輸冗余數(shù)據(jù)
- 壓縮要傳輸?shù)臄?shù)
- 把服務(wù)器放到離用戶近的地方以減少 RTT
- 盡最大可能重用已經(jīng)建立的TCP連接
TCP參數(shù)參數(shù)調(diào)優(yōu) - lsgxeva - 博客園
https://www.cnblogs.com/lsgxeva/p/16276794.html文章來源:http://www.zghlxwxcb.cn/news/detail-516320.html
TCP參數(shù)參數(shù)調(diào)優(yōu) - 知乎
https://zhuanlan.zhihu.com/p/622683704文章來源地址http://www.zghlxwxcb.cn/news/detail-516320.html
到了這里,關(guān)于Linux內(nèi)核TCP參數(shù)調(diào)優(yōu)全面解讀的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!