HTTP請求的TCP瓶頸分析

灰主流創業者發表於2015-07-06

這篇文章基本是對《Web效能權威指南》第一章和第二章的讀書筆記,另外加一些擴充套件內容,這本書確實贊,推薦

tcp-stack

針對三次握手、流量控制(接收視窗)、慢啟動(cwnd,擁塞視窗)、隊首阻塞等方面看下TCP對HTTP的影響

高頻寬和低延遲

所有網路通訊都有決定性影響的兩個方面:延遲和頻寬

  • 延遲 分組從資訊源傳送到目的地所需的時間。
  • 頻寬 邏輯或物理通訊路徑最大的吞吐量

viewport-index

延遲的因素

  • 傳播延遲 訊息從傳送端到接收端需要的時間(不超過光速)
  • 傳輸延遲(頻寬/視窗) 把訊息中的所有位元轉移到鏈路中需要的時間,是訊息長度和鏈路速率的函式(10M/s和1M/s的線路,時間不一樣)
  • 處理延遲 處理分組首部、檢查位錯誤及確定分組目標所需的時間(路由器分包)
  • 排隊延遲 到來的分組排隊等待處理的時間

速度延時

假定光通過光纖的速度 約為每秒 200 000 000 米,對應的折射率約為 1.5,從紐約到悉尼的一個往返(RTT)也要花 160 ms,分組旅行的距離比這要長得多。這條線路中的 每一跳都會涉及尋路、處理、排隊和傳輸延遲。結果呢,紐約到悉尼的實際 RTT, 大約在 200~300 ms 之間。

中美骨幹網單向資料延時≈60ms,所以中國使用者訪問美國主機資料傳輸的延時理論值高於120ms(RTT)

頻寬延時

核心資料路徑的骨幹或光纖連線,每秒能夠處理數百太位元資訊,比如中美之間的海底光纖。光纖就是一根“光導管”,傳送光訊號。金屬線則用於傳送電訊號,但訊號損失、電磁干擾較大,同時維護成本也較高。

通過波長分波多工(WDM,Wavelength-Division Multiplexing)技術,光纖可以同時傳輸很多不同波長(通道)的光,2010年初,研究人員已經可以在每個通道中耦合400多種波長的光線,最大容量可達171Gbit/s,而一條光纖的總頻寬能夠達到70Tbit/s

最後一公里延時-tracerouter

骨幹線路可以有TB的頻寬,但是網路邊緣的容量就小得多了,而且很大程度上取決於部署技術,比如拔號連線、 DSL、電纜、各種無線技術、光纖到戶。akamai每季度都會發布全球的頻寬報告

通過tracerouter工具,可以檢視路由拓撲,最後一公里的延遲與提供商、部署方法、網路拓撲,甚至一天中的哪個時段都有很 大關係。作為終端使用者,如果你想提高自己上網的速度,那選擇延遲最短的ISP是最關鍵的

Traceroute sends out three packets per TTL increment. Each column corresponds to the 
time is took to get one packet back (round-trip-time).

traceroute to xx.com (121.41.167.43), 30 hops max, 60 byte packets
 1  198.11.175.248 (198.11.175.248)  0.879 ms  0.898 ms  0.950 ms
 2  10.107.64.14 (10.107.64.14)  0.945 ms 10.107.64.22 (10.107.64.22)  1.033 ms 10.107.64.18 (10.107.64.18)  75.379 ms
 3  198.11.128.162 (198.11.128.162)  135.636 ms 198.11.128.154 (198.11.128.154)  0.913 ms 198.11.128.178 (198.11.128.178)  5.472 ms
 4  218.30.53.93 (218.30.53.93)  4.542 ms 218.30.53.97 (218.30.53.97)  2.144 ms 218.30.53.126 (218.30.53.126)  2.334 ms
 5  202.97.51.253 (202.97.51.253)  160.089 ms  160.170 ms  160.077 ms
 6  202.97.35.105 (202.97.35.105)  188.541 ms  190.518 ms  188.903 ms
 7  202.97.33.37 (202.97.33.37)  168.075 ms  168.109 ms  168.016 ms
 8  202.97.55.14 (202.97.55.14)  192.583 ms  192.572 ms  192.591 ms
 9  220.191.135.106 (220.191.135.106)  201.476 ms  201.542 ms  201.465 ms
10  115.236.101.209 (115.236.101.209)  211.315 ms  211.305 ms *
11  42.120.244.194 (42.120.244.194)  270.211 ms 42.120.244.178 (42.120.244.178)  163.768 ms  163.700 ms
12  42.120.244.238 (42.120.244.238)  191.543 ms 42.120.244.246 (42.120.244.246)  248.825 ms  248.910 ms

目標

高頻寬
目前還沒有理由認為頻寬會停止增長的腳步,就算技術停滯不前,還是可以鋪設更多的光纜

低延時

減少延遲相比頻寬困難的多,從很多方面來看,我們的基礎設施似乎也已經達到了這個極限。這就顯得理解和調優網路協議顯得特別重要

TCP三次握手

viewport-index
客戶端可以在傳送 ACK分組之後立即傳送資料,而伺服器必須等接收到ACK分組之後才能傳送資料。這個啟動通訊的過程適用於所有 TCP 連線,因此對所有使用TCP的應用具有非常大的效能影響,每次傳輸應用資料之前,都必須經歷一次完整的往返

中美之間一次RTT最低120,假設你發起一個簡單的HTTP請求,需要一次握手+一次資料傳輸 = 240ms,浪費了50%的時間,這也意味著提高TCP應用效能的關鍵在於想辦法重用連線

擴充套件:TFO(TCP Fast Open,TCP 快速打 開)機制,致力於減少新建 TCP 連線帶來的效能損失

流量控制(視窗rwnd)

rwnd是端到端的控制機制,預防傳送過多的資料,TCP連線的每一方都會通告自己的接收視窗,其中包含能夠儲存資料的緩衝區空間大小資訊。TCP 連線的整個生命週期:每個 ACK 分組都會攜帶相應的最新rwnd 值,以便兩端動態調整資料流速,使之適應傳送端和接收端的容量及處理能力

視窗的值原來只有16位,即65535,所以以前rwnd的最大值不能超過64K。現在基本都有了“TCP 視窗縮放”(TCP Window Scaling),把接收視窗大小由 65 535 位元組提高到 1G 位元組,在 Linux 中,可以通過如下命 令檢查和啟用視窗縮放選項:

$> sysctl net.ipv4.tcp_window_scaling
$> sysctl -w net.ipv4.tcp_window_scaling=1

rwnd的設定

如果我們出於傳輸效能的考慮,當然這個值設定的越大越好,Linux中通過配置核心引數裡接收緩衝的大小,進而可以控制接收視窗的大小:

shell> sysctl -a | grep mem
net.ipv4.tcp_rmem = <MIN> <DEFAULT> <MAX>

還有個問題,當大量請求同時到達時,記憶體會不會爆掉?通常不會,因為Linux本身有一個緩衝大小自動調優的機制,視窗的實際大小會自動在最小值和最大值之間浮動,以期找到效能和資源的平衡點。

通過如下方式可以確認緩衝大小自動調優機制的狀態(0:關閉、1:開啟):
shell> sysctl -a | grep tcp_moderate_rcvbuf

慢啟動(cwnd,擁塞視窗)

兩端流量控制確實可以防止傳送端向接收端過多傳送資料,但卻沒有機制預防任何一端向潛在網路過多傳送資料。換句話說,傳送端和接收端在連線建立之初,誰也不知道可用頻寬是多少,因此需要一個估算機制,然後根據網路中不斷變化的條件 而動態改變速度:TCP能傳輸的最大資料 = MIN(rand,cwnd)

慢啟動的演算法如下(cwnd全稱Congestion Window):

  • 1)連線建好的開始先初始化cwnd = 1,表明可以傳一個MSS大小的資料。
  • 2)每當收到一個ACK,cwnd++; 呈線性上升
  • 3)每當過了一個RTT,cwnd = cwnd*2; 呈指數讓升
  • 4)還有一個ssthresh(slow start threshold),是一個上限,當cwnd >= ssthresh時,就會進入“擁塞避免演算法”(後面會說這個演算法)

最初,cwnd 的值只有1個TCP segment。99 年 4 月,RFC 2581 將其增加到了 4 個 TCP segment。2013 年 4 月,RFC 6928 再次將其提高到 10 個 TCP segment

慢啟動過程

伺服器向客戶端傳送 4 個 TCP segment,然後就必須停下來等待確認。此後,每收到一個 ACK, 慢啟動演算法就會告訴伺服器可以將它的 cwnd 視窗增加 1 個 TCP segment.這個階段通常被稱為指數增長階段,因為客戶端和伺服器都在向兩者之間網路路徑的有效頻寬迅速靠攏

viewport-index
計算達到指定視窗所需要的時間公式:

viewport-index
  • 客戶端和伺服器的接收視窗為 65 535 位元組(64 KB);
  • 初始的擁塞視窗:4 個segment(一個segment 一般是1460B);
  • 往返時間是 56 ms(倫敦到紐約)。

為了達到64KB限制,我們將需要把擁塞視窗增加到45個segment,這將花費224毫秒。

viewport-index

慢啟動的影響

無論頻寬多大,每個 TCP 連線都必須經過慢 啟動階段。換句話說,我們不可能一上來就完全利用連線的最大頻寬。

慢啟動導致客戶端與伺服器之間經過幾百 ms 才能達到接近最大速度的問題,對於大型流式下載服務的影響不顯著,因為慢啟動的時間可以分攤到整個傳輸週期內消化掉。

對於很多 HTTP 連線,特別是一些短暫、突發的連線而言,常常會出現還沒 有達到最大視窗請求就被終止的情況。換句話說,很多 Web 應用的效能經常受到服 務器與客戶端之間往返時間的制約。因為慢啟動限制了可用的吞吐量,而這對於小 檔案傳輸非常不利。

慢啟動對HTTP影響的一次計算

假設通過HTTP傳輸一個20K的檔案,初始條件:

  • 往返時間:56 ms。
  • 客戶端到伺服器的頻寬:5 Mbit/s。
  • 客戶端和伺服器接收視窗:65 535 位元組。
  • 初始的擁塞視窗:4 segment(4×1460 位元組 ≈ 5.7 KB)。
  • 伺服器生成響應的處理時間:40 ms。
  • 沒有分組丟失、每個分組都要確認、GET 請求只佔 1 段。

viewport-index
  • 0 ms:客戶端傳送 SYN 分組開始 TCP 握手。
  • 28 ms:伺服器響應 SYN-ACK 並指定其 rwnd 大小。
  • 56 ms:客戶端確認 SYN-ACK,指定其 rwnd 大小,並立即傳送 HTTP GET 請求。
    8 84 ms:伺服器收到 HTTP 請求。
  • 124 ms:伺服器生成 20 KB 的響應,併傳送 4 個 TCP 段(初始 cwnd 大小為 4),
    然後等待 ACK。
  • 152 ms:客戶端收到 4 個段,並分別傳送 ACK 確認。
  • 180 ms:伺服器針對每個 ACK 遞增 cwnd,然後傳送 8 個 TCP 段。
  • 208 ms:客戶端接收 8 個段,並分別傳送 ACK 確認。
  • 236 ms:伺服器針對每個 ACK 遞增 cwnd,然後傳送剩餘的 TCP 段。
  • 264 ms:客戶端收到剩餘的 TCP 段,並分別傳送 ACK 確認。

作為對比,重用連線,再發一次請求

  • 0 ms:客戶端傳送 HTTP 請求。
  • 28 ms:伺服器收到 HTTP 請求。
  • 68 ms:伺服器生成 20 KB 響應,但 cwnd 已經大於傳送檔案所需的 15 段了,因
    此一次性傳送所有資料段。
  • 96 ms:客戶端收到所有 15 個段,分別傳送 ACK 確認。

同一個連線、同樣的請求,但沒有三次握手和慢啟動,只花了 96 ms,效能提升幅 度達 275% !

擁塞視窗的合適值

Google在這方面做了大量的研究,權衡了效率和穩定性之後,最終給出的建議是10MSS。如果你的Linux版本不太舊的話,那麼可以通過如下方法來調整「cwnd」初始值:

shell> ip route | while read p; do
           ip route change $p initcwnd 10;
       done

需要提醒的是片面的提升傳送端「cwnd」的大小並不一定有效,這是因為前面我們說過網路中實際傳輸的未經確認的資料大小取決於「rwnd」和「cwnd」中的小值,所以一旦接收方的「rwnd」比較小的話,會阻礙「cwnd」的發揮。

擁塞預防

擁塞預防演算法把丟包作為網路擁塞的標誌,即路徑中某個連線或路由器已經擁堵了, 以至於必須採取刪包措施。因此,必須調整視窗大小,以避免造成更多的包丟失, 從而保證網路暢通。

重置擁塞視窗後,擁塞預防機制按照自己的演算法來增大視窗以儘量避免丟包。某個 時刻,可能又會有包丟失,於是這個過程再從頭開始。如果你看到過 TCP 連線的吞 吐量跟蹤曲線,發現該曲線呈鋸齒狀,那現在就該明白為什麼了。這是擁塞控制和 預防演算法在調整擁塞視窗,進而消除網路中的丟包問題。

最初,TCP 使用 AIMD(Multiplicative Decrease and Additive Increase,倍減加增) 演算法,即發生丟包時,先將擁塞視窗減半,然後每次往返再緩慢地給視窗增加一 個固定的值。不過,很多時候 AIMD 演算法太過保守,因此又有了很多新的演算法,比如DSACK:可以讓協議知道是什麼原因丟包,是重傳還是丟失

頻寬延遲積

傳送端和接收端之間在途未確認的最大資料量,取決於擁塞窗 口(cwnd)和接收視窗(rwnd)的最小值。接收視窗會隨每次 ACK 一起傳送,而 擁塞視窗則由傳送端根據擁塞控制和預防演算法動態調整.

BDP(Bandwidth-delay product,頻寬延遲積)
資料鏈路的容量與其端到端延遲的乘積。這個結果就是任意時刻處於在途未確認 狀態的最大資料量。無論傳送端傳送的資料還是接收端接收的資料超過了未確認的最大資料量,都必須停 下來等待另一方 ACK 確認某些分組才能繼續

viewport-index

那麼,流量控制視窗(rwnd)和擁塞控制視窗(cwnd)的值多大合適?實際上,計算過程很簡單。首先,假設 cwnd 和 rwnd 的最小值為 16 KB,往返時間為 100 ms:

viewport-index

不管傳送端和接收端的實際頻寬多大,這個 TCP 連線的資料傳輸速率不會超過 1.31 Mbit/s !想提高吞吐量,要麼增大最小視窗值,要麼減少往返時間。視窗至少需要 122.1 KB 才能充分利用 10 Mbit/s 頻寬!如果沒有“視窗 縮放(RFC 1323)”,TCP 接收視窗最大隻有 64 KB

隊首阻塞造成的延時

每個 TCP 分組都會帶著一個唯一的序列號被髮出,而 所有分組必須按順序傳送到接收端。如果中途有一個分組沒能到達接收 端,那麼後續分組必須儲存在接收端的 TCP 緩衝區,等待丟失的分組重發併到達接 收端。這一切都發生在 TCP 層,應用程式對 TCP 重發和緩衝區中排隊的分組一無所 知,必須等待分組全部到達才能訪問資料。在此之前,應用程式只能在通過套接字 讀資料時感覺到延遲交付。這種效應稱為 TCP 的隊首(HOL,Head of Line)阻塞

隊首阻塞造成的延遲可以讓我們的應用程式不用關心分組重排和重組,從而讓程式碼 保持簡潔。然而,程式碼簡潔也要付出代價,那就是分組到達時間會存在無法預知的 延遲變化。這個時間變化通常被稱為抖動

 

viewport-index

事實上,某些場景下,丟包是讓 TCP 達到最佳效能的關鍵。有些應用程式可以容忍丟失一 定數量的包,比如語音和遊戲狀態通訊,就不需要可靠傳輸或按序交付

 

針對TCP的優化建議

每個演算法和反饋機制的具體細節可能會繼續發展,但核心原理以及 它們的影響是不變的:

  • TCP 三次握手增加了整整一次往返時間;
  • TCP 慢啟動將被應用到每個新連線;
  • TCP 流量及擁塞控制會影響所有連線的吞吐量;
  • TCP 的吞吐量由當前擁塞視窗大小控制。

結果,現代高速網路中 TCP 連線的資料傳輸速度,往往會受到接收端和傳送端之 間往返時間的限制。另外,儘管頻寬不斷增長,但延遲依舊受限於光速,而且已經 限定在了其最大值的一個很小的常數因子之內。大多數情況下,TCP 的瓶頸都是延遲,而非頻寬

伺服器配置調優

  • 增大TCP的初始擁塞視窗
    加大起始擁塞視窗可以讓 TCP 在第一次往返就傳輸較多資料,而隨後的速度提 升也會很明顯。對於突發性的短暫連線,這也是特別關鍵的一個優化。
  • 慢啟動重啟
    在連線空閒時禁用慢啟動可以改善瞬時傳送資料的長 TCP 連線的效能。
  • 視窗縮放(RFC 1323) 啟用視窗縮放可以增大最大接收視窗大小,可以讓高延遲的連線達到更好吞 吐量。
  • TCP快速開啟
    在某些條件下,允許在第一個 SYN 分組中傳送應用程式資料。TFO(TCP Fast Open,TCP 快速開啟)是一種新的優化選項,需要客戶端和伺服器共同支援。 為此,首先要搞清楚你的應用程式是否可以利用這個特性。

以上幾個設定再加上最新的核心,可以確保最佳效能:每個 TCP 連線都會具有較低 的延遲和較高的吞吐量。

應用程式行為調優

  • 再快也快不過什麼也不用傳送,能少發就少發。
  • 我們不能讓資料傳輸得更快,但可以讓它們傳輸的距離更短。
  • 重用 TCP 連線是提升效能的關鍵

效能檢查清單

  • 把伺服器核心升級到最新版本(Linux:3.2+);
  • 確保 cwnd 大小為 10;
  • 禁用空閒後的慢啟動;
  • 確保啟動視窗縮放;
  • 減少傳輸冗餘資料;
  • 壓縮要傳輸的資料;
  • 把伺服器放到離使用者近的地方以減少往返時間;
  • 盡最大可能重用已經建立的 TCP 連線

參考

相關文章