連 TCP 這幾個引數都不懂,回去等通知吧!(三)

CrazyZard發表於2020-06-22

在前面介紹的是三次握手和四次揮手的優化策略,接下來主要介紹的是 TCP 傳輸資料時的優化策略。

TCP 連線是由核心維護的,核心會為每個連線建立記憶體緩衝區:

  • 如果連線的記憶體配置過小,就無法充分使用網路頻寬,TCP 傳輸效率就會降低;

  • 如果連線的記憶體配置過大,很容易把伺服器資源耗盡,這樣就會導致新連線無法建立;

因此,我們必須理解 Linux 下 TCP 記憶體的用途,才能正確地配置記憶體大小。

滑動視窗是如何影響傳輸速度的?

TCP 會保證每一個報文都能夠抵達對方,它的機制是這樣:報文發出去後,必須接收到對方返回的確認報文 ACK,如果遲遲未收到,就會超時重發該報文,直到收到對方的 ACK 為止。

所以,TCP 報文發出去後,並不會立馬從記憶體中刪除,因為重傳時還需要用到它。

由於 TCP 是核心維護的,所以報文存放在核心緩衝區。如果連線非常多,我們可以通過 free 命令觀察到 buff/cache 記憶體是會增大。

如果 TCP 是每傳送一個資料,都要進行一次確認應答。當上一個資料包收到了應答了, 再傳送下一個。這個模式就有點像我和你面對面聊天,你一句我一句,但這種方式的缺點是效率比較低的。

連 TCP 這幾個引數都不懂,回去等通知吧!(三)

所以,這樣的傳輸方式有一個缺點:資料包的往返時間越長,通訊的效率就越低

要解決這一問題不難,並行批量傳送報文,再批量確認報文即刻。

連 TCP 這幾個引數都不懂,回去等通知吧!(三)

然而,這引出了另一個問題,傳送方可以隨心所欲的傳送報文嗎?當然這不現實,我們還得考慮接收方的處理能力。

當接收方硬體不如傳送方,或者系統繁忙、資源緊張時,是無法瞬間處理這麼多報文的。於是,這些報文只能被丟掉,使得網路效率非常低。

為了解決這種現象發生,TCP 提供一種機制可以讓「傳送方」根據「接收方」的實際接收能力控制傳送的資料量,這就是滑動視窗的由來。

接收方根據它的緩衝區,可以計算出後續能夠接收多少位元組的報文,這個數字叫做接收視窗。當核心接收到報文時,必須用緩衝區存放它們,這樣剩餘緩衝區空間變小,接收視窗也就變小了;當程式呼叫 read 函式後,資料被讀入了使用者空間,核心緩衝區就被清空,這意味著主機可以接收更多的報文,接收視窗就會變大。

因此,接收視窗並不是恆定不變的,接收方會把當前可接收的大小放在 TCP 報文頭部中的視窗欄位,這樣就可以起到視窗大小通知的作用。

傳送方的視窗等價於接收方的視窗嗎?如果不考慮擁塞控制,傳送方的視窗大小「約等於」接收方的視窗大小,因為視窗通知報文在網路傳輸是存在時延的,所以是約等於的關係。

連 TCP 這幾個引數都不懂,回去等通知吧!(三)

從上圖中可以看到,視窗欄位只有 2 個位元組,因此它最多能表達 65535 位元組大小的視窗,也就是 64KB 大小。

這個視窗大小最大值,在當今高速網路下,很明顯是不夠用的。所以後續有了擴充視窗的方法:在 TCP 選項欄位定義了視窗擴大因子,用於擴大TCP通告視窗,使 TCP 的視窗大小從 2 個位元組(16 位) 擴大為 30 位,所以此時視窗的最大值可以達到 1GB(2^30)。

Linux 中開啟這一功能,需要把 tcp_window_scaling 配置設為 1(預設開啟):

連 TCP 這幾個引數都不懂,回去等通知吧!(三)

要使用視窗擴大選項,通訊雙方必須在各自的 SYN 報文中傳送這個選項:

  • 主動建立連線的一方在 SYN 報文中傳送這個選項;

  • 而被動建立連線的一方只有在收到帶視窗擴大選項的 SYN 報文之後才能傳送這個選項。

這樣看來,只要程式能及時地呼叫 read 函式讀取資料,並且接收緩衝區配置得足夠大,那麼接收視窗就可以無限地放大,傳送方也就無限地提升傳送速度。

這是不可能的,因為網路的傳輸能力是有限的,當傳送方依據傳送視窗,傳送超過網路處理能力的報文時,路由器會直接丟棄這些報文。因此,緩衝區的記憶體並不是越大越好。

在前面我們知道了 TCP 的傳輸速度,受制於傳送視窗與接收視窗,以及網路裝置傳輸能力。其中,視窗大小由核心緩衝區大小決定。如果緩衝區與網路傳輸能力匹配,那麼緩衝區的利用率就達到了最大化。

問題來了,如何計算網路的傳輸能力呢?

相信大家都知道網路是有「頻寬」限制的,頻寬描述的是網路傳輸能力,它與核心緩衝區的計量單位不同:

  • 頻寬是單位時間內的流量,表達是「速度」,比如常見的頻寬 100 MB/s;

  • 緩衝區單位是位元組,當網路速度乘以時間才能得到位元組數;

這裡需要說一個概念,就是頻寬時延積,它決定網路中飛行報文的大小,它的計算方式:

連 TCP 這幾個引數都不懂,回去等通知吧!(三)

比如最大頻寬是 100 MB/s,網路時延(RTT)是 10ms 時,意味著客戶端到服務端的網路一共可以存放 100MB/s * 0.01s = 1MB 的位元組。

這個 1MB 是頻寬和時延的乘積,所以它就叫「頻寬時延積」(縮寫為 BDP,Bandwidth Delay Product)。同時,這 1MB 也表示「飛行中」的 TCP 報文大小,它們就在網路線路、路由器等網路裝置上。如果飛行報文超過了 1 MB,就會導致網路過載,容易丟包。

由於傳送緩衝區大小決定了傳送視窗的上限,而傳送視窗又決定了「已傳送未確認」的飛行報文的上限。因此,傳送緩衝區不能超過「頻寬時延積」。

傳送緩衝區與頻寬時延積的關係:

  • 如果傳送緩衝區「超過」頻寬時延積,超出的部分就沒辦法有效的網路傳輸,同時導致網路過載,容易丟包;

  • 如果傳送緩衝區「小於」頻寬時延積,就不能很好的發揮出網路的傳輸效率。

所以,傳送緩衝區的大小最好是往頻寬時延積靠近。

怎樣調整緩衝區大小?

在 Linux 中傳送緩衝區和接收緩衝都是可以用引數調節的。設定完後,Linux 會根據你設定的緩衝區進行動態調節

調節傳送緩衝區範圍

先來看看傳送緩衝區,它的範圍通過 tcp_wmem 引數配置;

在 Linux 中傳送緩衝區和接收緩衝都是可以用引數調節的。設定完後,Linux 會根據你設定的緩衝區進行動態調節

連 TCP 這幾個引數都不懂,回去等通知吧!(三)

上面三個數字單位都是位元組,它們分別表示:

  • 第一個數值是動態範圍的最小值,4096 byte = 4K;

  • 第二個數值是初始預設值,87380 byte ≈ 86K;

  • 第三個數值是動態範圍的最大值,4194304 byte = 4096K(4M);

傳送緩衝區是自行調節的,當傳送方傳送的資料被確認後,並且沒有新的資料要傳送,就會把傳送緩衝區的記憶體釋放掉。

調節接收緩衝區範圍

而接收緩衝區的調整就比較複雜一些,先來看看設定接收緩衝區範圍的 tcp_rmem 引數:

連 TCP 這幾個引數都不懂,回去等通知吧!(三)

上面三個數字單位都是位元組,它們分別表示:

  • 第一個數值是動態範圍的最小值,表示即使在記憶體壓力下也可以保證的最小接收緩衝區大小,4096 byte = 4K;

  • 第二個數值是初始預設值,87380 byte ≈ 86K;

  • 第三個數值是動態範圍的最大值,6291456 byte = 6144K(6M);

接收緩衝區可以根據系統空閒記憶體的大小來調節接收視窗:

  • 如果系統的空閒記憶體很多,就可以自動把緩衝區增大一些,這樣傳給對方的接收視窗也會變大,因而提升傳送方傳送的傳輸資料數量;

  • 反正,如果系統的記憶體很緊張,就會減少緩衝區,這雖然會降低傳輸效率,可以保證更多的併發連線正常工作;

傳送緩衝區的調節功能是自動開啟的,而接收緩衝區則需要配置 tcp_moderate_rcvbuf 為 1 來開啟調節功能

連 TCP 這幾個引數都不懂,回去等通知吧!(三)

調節 TCP 記憶體範圍

接收緩衝區調節時,怎麼知道當前記憶體是否緊張或充分呢?這是通過 tcp_mem 配置完成的:

連 TCP 這幾個引數都不懂,回去等通知吧!(三)

上面三個數字單位不是位元組,而是「頁面大小」,1 頁表示 4KB,它們分別表示:

  • 當 TCP 記憶體小於第 1 個值時,不需要進行自動調節;

  • 在第 1 和第 2 個值之間時,核心開始調節接收緩衝區的大小;

  • 大於第 3 個值時,核心不再為 TCP 分配新記憶體,此時新連線是無法建立的;

一般情況下這些值是在系統啟動時根據系統記憶體數量計算得到的。根據當前 tcp_mem 最大記憶體頁面數是 177120,當記憶體為 (177120 * 4) / 1024K ≈ 692M 時,系統將無法為新的 TCP 連線分配記憶體,即 TCP 連線將被拒絕。

根據實際場景調節的策略

在高併發伺服器中,為了兼顧網速與大量的併發連線,我們應當保證緩衝區的動態調整的最大值達到頻寬時延積,而最小值保持預設的 4K 不變即可。而對於記憶體緊張的服務而言,調低預設值是提高併發的有效手段。

同時,如果這是網路 IO 型伺服器,那麼,調大 tcp_mem 的上限可以讓 TCP 連線使用更多的系統記憶體,這有利於提升併發能力。需要注意的是,tcp_wmem 和 tcp_rmem 的單位是位元組,而 tcp_mem 的單位是頁面大小。而且,千萬不要在 socket 上直接設定 SO_SNDBUF 或者 SO_RCVBUF,這樣會關閉緩衝區的動態調整功能。

本節針對 TCP 優化資料傳輸的方式,做了一些介紹。

連 TCP 這幾個引數都不懂,回去等通知吧!(三)

TCP 可靠性是通過 ACK 確認報文實現的,又依賴滑動視窗提升了傳送速度也兼顧了接收方的處理能力。

可是,預設的滑動視窗最大值只有 64 KB,不滿足當今的高速網路的要求,要想要想提升傳送速度必須提升滑動視窗的上限,在 Linux 下是通過設定 tcp_window_scaling為 1 做到的,此時最大值可高達 1GB。

滑動視窗定義了網路中飛行報文的最大位元組數,當它超過頻寬時延積時,網路過載,就會發生丟包。而當它小於頻寬時延積時,就無法充分利用網路頻寬。因此,滑動視窗的設定,必須參考頻寬時延積。

核心緩衝區決定了滑動視窗的上限,緩衝區可分為:傳送緩衝區 tcp_wmem 和接收緩衝區 tcp_rmem。

Linux 會對緩衝區動態調節,我們應該把緩衝區的上限設定為頻寬時延積。傳送緩衝區的調節功能是自動開啟的,而接收緩衝區需要把 tcp_moderate_rcvbuf 設定為 1 來開啟。其中,調節的依據是 TCP 記憶體範圍 tcp_mem。

但需要注意的是,如果程式中的 socket 設定 SO_SNDBUF 和 SO_RCVBUF,則會關閉緩衝區的動態整功能,所以不建議在程式設定它倆,而是交給核心自動調整比較好。

有效配置這些引數後,既能夠最大程度地保持併發性,也能讓資源充裕時連線傳輸速度達到最大值。

小林coding 的 面試官:換人!他連 TCP 這幾個引數都不懂

認真看完的話 真的會有很多不同的收穫,重新認知三次握手跟四次揮手,不在理解字面意思,而明白了為啥系統會去這麼做的緣由

本作品採用《CC 協議》,轉載必須註明作者和本文連結

快樂就是解決一個又一個的問題!

相關文章