TCP 會保證每一個報文都能夠抵達對方,它的機制是這樣:報文發出去后,必須接收到對方返回的確認報文 ACK,如果遲遲未收到,就會超時重發該報文,直到收到對方的 ACK 為止
所以,TCP 報文發出去后,并不會立馬從內存中刪除,因為重傳時還需要用到它
由于 TCP 是內核維護的,所以報文存放在內核緩沖區。如果連接非常多,我們可以通過 free 命令觀察到 buff/cache 內存是會增大
如果 TCP 是每發送一個數據,都要進行一次確認應答。當上一個數據包收到了應答了, 再發送下一個。這個模式就有點像我和你面對面聊天,你一句我一句,但這種方式的缺點是效率比較低的
所以,這樣的傳輸方式有一個缺點:數據包的往返時間越長,通信的效率就越低
要解決這一問題不難,并行批量發送報文,再批量確認報文即可
然而,這引出了另一個問題,發送方可以隨心所欲的發送報文嗎?當然這不現實,我們還得考慮接收方的處理能力
當接收方硬件不如發送方,或者系統繁忙、資源緊張時,是無法瞬間處理這么多報文的。于是,這些報文只能被丟掉,使得網絡效率非常低
為了解決這種現象發生,TCP 提供一種機制可以讓「發送方」根據「接收方」的實際接收能力控制發送的數據量,這就是滑動窗口的由來
接收方根據它的緩沖區,可以計算出后續能夠接收多少字節的報文,這個數字叫做接收窗口。當內核接收到報文時,必須用緩沖區存放它們,這樣剩余緩沖區空間變小,接收窗口也就變小了;當進程調用read 函數后,數據被讀入了用戶空間,內核緩沖區就被清空,這意味著主機可以接收更多的報文,接收窗口就會變大
因此,接收窗口并不是恒定不變的,接收方會把當前可接收的大小放在 TCP 報文頭部中的窗口字段,這樣就可以起到窗口大小通知的作用
發送方的窗口等價于接收方的窗口嗎?如果不考慮擁塞控制,發送方的窗口大小「約等于」接收方的窗口大小,因為窗口通知報文在網絡傳輸是存在時延的,所以是約等于的關系
從上圖中可以看到,窗口字段只有 2 個字節,因此它最多能表達65535 字節大小的窗口,也就是 64KB 大小
這個窗口大小最大值,在當今高速網絡下,很明顯是不夠用的。所以后續有了擴充窗口的方法:在 TCP 選項字段定義了窗口擴大因子,用于擴大 TCP 通告窗口,其值大小是 2^14,這樣就使 TCP 的窗口大小從16 位擴大為 30 位(2^16 * 2^ 14 = 2^30),所以此時窗口的最大值可以達到 1GB
Linux 中打開這一功能,需要把 tcp_window_scaling 配置設為 1(默認打開):
要使用窗口擴大選項,通訊雙方必須在各自的 SYN 報文中發送這個選項:
- 主動建立連接的一方在 SYN 報文中發送這個選項
- 而被動建立連接的一方只有在收到帶窗口擴大選項的 SYN 報文之后才能發送這個選項
這樣看來,只要進程能及時地調用 read 函數讀取數據,并且接收緩沖區配置得足夠大,那么接收窗口就可以無限地放大,發送方也就無限地提升發送速度
這是不可能的,因為網絡的傳輸能力是有限的,當發送方依據發送窗口,發送超過網絡處理能力的報文時,路由器會直接丟棄這些報文。因此,緩沖區的內存并不是越大越好
如何確定最大傳輸速度?
在前面我們知道了 TCP 的傳輸速度,受制于發送窗口與接收窗口,以及網絡設備傳輸能力。其中,窗口大小由內核緩沖區大小決定。如果緩沖區與網絡傳輸能力匹配,那么緩沖區的利用率就達到了最大化
問題來了,如何計算網絡的傳輸能力呢?
相信大家都知道網絡是有「帶寬」限制的,帶寬描述的是網絡傳輸能力,它與內核緩沖區的計量單位不同:
- 帶寬是單位時間內的流量,表達是「速度」,比如常見的帶寬 100 MB/s
- 緩沖區單位是字節,當網絡速度乘以時間才能得到字節數
這里需要說一個概念,就是帶寬時延積,它決定網絡中飛行報文的大小,它的計算方式:
比如最大帶寬是 100 MB/s,網絡時延(RTT)是 10ms 時,意味著客戶端到服務端的網絡一共可以存放 100MB/s * 0.01s = 1MB 的字節
這個 1MB 是帶寬和時延的乘積,所以它就叫「帶寬時延積」(縮寫為BDP,Bandwidth Delay Product)。同時,這 1MB 也表示「飛行中」的TCP 報文大小,它們就在網絡線路、路由器等網絡設備上。如果飛行報文超過了 1 MB,就會導致網絡過載,容易丟包
由于發送緩沖區大小決定了發送窗口的上限,而發送窗口又決定了「已發送未確認」的飛行報文的上限。因此,發送緩沖區不能超過「帶寬時延積」
發送緩沖區與帶寬時延積的關系:
- 如果發送緩沖區「超過」帶寬時延積,超出的部分就沒辦法有效的網絡傳輸,同時導致網絡過載,容易丟包
- 如果發送緩沖區「小于」帶寬時延積,就不能很好的發揮出網絡的傳輸效率
所以,發送緩沖區的大小最好是往帶寬時延積靠近
怎樣調整緩沖區大小?
在 Linux 中發送緩沖區和接收緩沖都是可以用參數調節的。設置完后,Linux 會根據你設置的緩沖區進行動態調節
1、調節發送緩沖區范圍
先來看看發送緩沖區,它的范圍通過 tcp_wmem 參數配置;
上面三個數字單位都是字節,它們分別表示:
- 第一個數值是動態范圍的最小值,4096 byte = 4K
- 第二個數值是初始默認值,16384 byte ≈ 16K
- 第三個數值是動態范圍的最大值,4194304 byte = 4096K(4M)
發送緩沖區是自行調節的,當發送方發送的數據被確認后,并且沒有新的數據要發送,就會把發送緩沖區的內存釋放掉
2、調節接收緩沖區范圍
而接收緩沖區的調整就比較復雜一些,先來看看設置接收緩沖區范圍的 tcp_rmem 參數:
上面三個數字單位都是字節,它們分別表示:
- 第一個數值是動態范圍的最小值,表示即使在內存壓力下也可以保證的最小接收緩沖區大小,4096 byte = 4K
- 第二個數值是初始默認值,87380 byte ≈ 86K
- 第三個數值是動態范圍的最大值,6291456 byte = 6144K(6M)
接收緩沖區可以根據系統空閑內存的大小來調節接收窗口:
- 如果系統的空閑內存很多,就可以自動把緩沖區增大一些,這樣傳給對方的接收窗口也會變大,因而提升發送方發送的傳輸數據數量
- 反之,如果系統的內存很緊張,就會減少緩沖區,這雖然會降低傳輸效率,可以保證更多的并發連接正常工作
發送緩沖區的調節功能是自動開啟的,而接收緩沖區則需要配置
tcp_moderate_rcvbuf 為 1 來開啟調節功能:
3、調節 TCP 內存范圍
接收緩沖區調節時,怎么知道當前內存是否緊張或充分呢?這是通過tcp_mem 配置完成的:
上面三個數字單位不是字節,而是「頁面大小」,1 頁表示 4KB,它們分別表示:
- 當 TCP 內存小于第 1 個值時,不需要進行自動調節
- 在第 1 和第 2 個值之間時,內核開始調節接收緩沖區的大小
- 大于第 3 個值時,內核不再為 TCP 分配新內存,此時新連接是無法建立的
一般情況下這些值是在系統啟動時根據系統內存數量計算得到的。根據當前 tcp_mem 最大內存頁面數是 177120,當內存為 (177120 * 4) /1024K ≈ 692M 時,系統將無法為新的 TCP 連接分配內存,即 TCP 連接將被拒絕
4、根據實際場景調節的策略
在高并發服務器中,為了兼顧網速與大量的并發連接,我們應當保證緩沖區的動態調整的最大值達到帶寬時延積,而最小值保持默認的 4K不變即可。而對于內存緊張的服務而言,調低默認值是提高并發的有效手段
同時,如果這是網絡 IO 型服務器,那么,調大 tcp_mem 的上限可以讓 TCP 連接使用更多的系統內存,這有利于提升并發能力。需要注意的是,tcp_wmem 和 tcp_rmem 的單位是字節,而 tcp_mem 的單位是頁面大小。而且,千萬不要在 socket 上直接設置 SO_SNDBUF 或者SO_RCVBUF,這樣會關閉緩沖區的動態調整功能