天天看點

記一次典型的TCP傳輸吞吐效率問題梳理和分辨問題其實上傳到SLB也很慢分析TCP視窗解決方案

客戶在ECS上實作了一個供小圖檔上傳的接口,通過高防->SLB->ECS的網絡鍊路将接口釋出給終端使用者。但是發現上傳的速率很不理想,上傳600K左右的小圖檔大約要8秒。初看起來像是高防問題,但是通過排查最終發現這是一個典型的TCP傳輸吞吐量問題,并且是由于後端伺服器端的配置而引起,在此記錄下排查過程和相關原理。

梳理和分辨問題

初看起來像是高防問題,但我們還是需要來先分辨下問題。整個傳輸的鍊路如下:

用戶端 -> 4層高防節點 -> 4層SLB -> 後端RS (ECS)

測試用戶端機器,SLB和後端RS都在北京,使用的是4層新高防節點(節點的地理未知不在北京)。從剛開始非常小的資訊量,我們有理由懷疑因為新高防節點的引入,造成用戶端到後端RS的往返RTT增加會導緻上傳需要更多時間。但是這個時間增加到600K需要8秒是否正常,從經驗判斷是不正常的,但是需要更多資訊來判斷問題出在哪裡。

比較關心的資訊如下:

這個上傳時間增加問題是否是突然發生,以前的上傳時間是多久?--> Answer: 這是第一次,測試就發生。

直接上傳SLB是不是也比較慢?--> Answer: 看起來“不慢”。

基于上面的資訊,并且确認了高防端沒有明顯問題,唯一能懷疑的是往返RTT的增加會導緻上傳需要更多時間。要繼續排查下去,目前彙總起來的資訊已經沒有突破口。隻能做更加定量地分析,也就是分别往高防和源站SLB測試上傳,看需要多少時間,并且同時抓包來,驗證除了RTT之外還有沒有影響TCP傳輸效率的點。

其實上傳到SLB也很慢

拿到了進一步的測試結果,大緻測試結果如下:

上傳檔案大小605KB,上傳到高防需要要大約8秒:

$ time curl -X POST https://gate.customer.com/xxx/yyy -F "expression=@/Users/customer/test.jpg"
real  0m8.067s
user  0m0.016s
sys 0m0.030s           

綁host上傳到SLB大約需要2.3秒:

$ time curl -X POST https://gate.customer.com/xxx/yyy -F "expression=@/Users/customer/test.jpg"
real  0m2.283s
user  0m0.017s
sys 0m0.031s           

上面的定量分析明确了之前一個不太準确的資訊,實際上上傳到SLB的也很慢,而非之前體感的“不慢”。對于在同一城域網内,RTT時間通常小于10ms, 如果TCP視窗正常的話,用戶端将605KB的圖檔上傳到阿裡雲SLB,一定會是ms級别,而非秒級,2.3秒明顯已經很慢了。主觀感受上對2秒的體感可能還不是那麼強烈,是以容易造成誤判。

那麼剩餘的問題就是要看看為什麼上傳到高防和SLB都很慢,而且上傳到高防更慢。這個隻能從抓包裡做進一步判斷。

分析TCP視窗

通過抓包分析可以有效地收窄(Narrow down) 問題。直接拿到測試的抓包,能避免了很多彎路。用戶端上傳到高防節點的抓包如下:

記一次典型的TCP傳輸吞吐效率問題梳理和分辨問題其實上傳到SLB也很慢分析TCP視窗解決方案

可以從抓包中看到如下幾個特征:

  1. 以62-64号包為例,在上傳的最開始一段時間,用戶端每給伺服器端傳輸2個封包(每個封包的TCP payload大小是1466-14-40=1412位元組),就需要等待伺服器端的ACK,才能繼續傳下面兩個封包。
  2. 伺服器端發出的封包中的TCP接收視窗一直很小,先後隻有2920和2824位元組 (在上圖中用紅框标出)。
  3. 在75号包中,伺服器端進一步将TCP接受視窗通過TCP Window Update調小,變成2824位元組。之後用戶端隻要傳輸1個1466位元組(TCP payload 1412位元組)的封包即出現TCP Window Full,需要等伺服器的ACK,再傳輸下面一個封包。
  4. 路徑的RTT比較大,且不是很穩定。比如70号封包花費了90ms的RTT, 而61号封包隻花費了31ms的RTT。

如果比較熟悉TCP協定,那到這裡基本上有結論了:伺服器端的TCP接收視窗持續很小,同時加上經過高防的RTT比較大,導緻TCP吞吐量很小,進而上傳慢。如果不太熟悉TCP協定,那麼需要解答如下幾個問題。

發送端一次能傳多大的在途 (in flight) 未确認數量?

TCP傳輸并不是發送端發送一個資料包,接收端回ACK, 發送端在繼續發送下一個資料包。而是允許發送端一次發多個資料包,但是到了一定大小的資料量必須要等待ACK才能發一下批資料包,這個資料量即為:在途資料未确認資料量。

在這個案例中,很明顯在途未确認資料一直很小,隻有大約1-2個MSS (通常MSS是1460,下面章節會有具體介紹)大小。那麼在途未确認資料量是多少呢?這取決于擁塞視窗(cwnd)和接收視窗(rwnd)的最小值。接收視窗大小每次回由對端随着ACK一起發送,而擁塞視窗則由發送端根據鍊路狀态,通過擁塞控制和預防算法進行動态調整。

擁塞視窗

擁塞視窗是根據鍊路狀态來動态調整的,最開始發封包給對端時,沒有機會知道鍊路狀态,是以采取比較穩健的方式将擁塞視窗初始值設定得小點,這就是TCP中的慢啟動。那麼設定多小呢?

RFC的推薦:

  • 4 MSS, RFC 2581 updated this value to 4 segments in April 1999;
  • 10 MSS, most recently the value was increased once more to 10 segments by RFC 6928 in April 2013.

Linux的實作:

  • 較老版本(Linux 2.6.x) 3*MSS
  • 新版本(Linux 3.0.+) 10*MSS

随後如果鍊路沒有丢包,擁塞視窗的大小在慢啟動中會指數增長。

接收視窗

在TCP Header中有Window字段,有16個位元組。Window本身的範圍可以0 ~ 64KB (65535, 2^16-1)。64KB在比較早的網絡環境中被認為是一個合适的上限,而利用TCP Options的Window scale字段,這個視窗可以被擴大。比如Window scale為5,則視窗可以在Window字段的基礎上放大32 (2^5)倍。

接收視窗大小每次會由對端随着ACK一起發送,我們在Wireshark裡面可以看到的Window字段就是接收視窗,而非擁塞視窗。

TCP是個雙工傳輸信道,接收視窗是有方向性的。雙發各自向對端通告自己的TCP接收視窗,最終會影響對端向本端的傳輸效率。比如在這個案例中,用戶端向伺服器端上傳資料,那麼伺服器端端通告的TCP接收視窗會影響用戶端向服務端傳輸資料的效率。

記一次典型的TCP傳輸吞吐效率問題梳理和分辨問題其實上傳到SLB也很慢分析TCP視窗解決方案

MSS

上面每次用戶端發送1466個位元組(二層資料幀的總長度),取決于用戶端和伺服器在3次握手時所互相通告的MSS,這個字段在TCP Option中。在3次握手中,用戶端通告給伺服器的MSS是1460位元組,伺服器通告給用戶端的MSS是1412位元組,在傳輸中利用1412作為MSS來傳輸。是以用戶端在傳輸封包時一個二層資料幀的大小為1412+20+20+14=1466位元組。

結論

這裡出現的問題的原因為:伺服器端的TCP接收視窗很小,限制了在途未确認資料量一直為1 ~ 2個MSS大小。和高防和SLB本身都沒有關系。

對于高防的上傳封包來說,伺服器端的TCP接收視窗持續很小,同時加上經過高防的RTT比較大,導緻TCP吞吐量很小。對SLB的測試也能複現接收視窗小的問題,隻是因為用戶端到SLB是同城傳輸,是以RTT小很多,總用時也小很多。因為TCP接收視窗比較小,使得上傳高防和上傳SLB幾乎和RTT呈線性關系,這個在正常的TCP傳輸中是幾乎不可能出現的,因為正常的TCP視窗一定是在擁塞控制的過程中增大和調整的。

用戶端走高防的RTT如下圖:在35毫秒左右。

記一次典型的TCP傳輸吞吐效率問題梳理和分辨問題其實上傳到SLB也很慢分析TCP視窗解決方案

用戶端走SLB的RTT如下圖:在8毫秒左右。

記一次典型的TCP傳輸吞吐效率問題梳理和分辨問題其實上傳到SLB也很慢分析TCP視窗解決方案

解決方案

影響TCP接收視窗的因素

1. TCP receive buffer

系統層面 (net.ipv4.tcp_rmem/net.core.rmem_max/net.ipv4.tcp_adv_win_scale)

TCP接收視窗的大小在Linux系統中取決于TCP receive buffer的大小,而TCP receive buffer的大小預設由核心根據系統可用記憶體的情況和核心參數net.ipv4.tcp_rmem動态調節。net.ipv4.tcp_rmem在Linux 2.4中被引入,設定包括[min, default, max]。

  • min: 每個TCP socket receive buffer的最小size。預設值是4K。
  • default: TCP socket receive buffer的預設大小。這個值能夠覆寫全局設定net.core.rmem_default定義的初始預設buffer size。預設值是87380位元組。
  • max: 每個TCP socket receive buffer的最大size。這個值不能覆寫全局設定net.core.rmem_max。

如下是一個核心3.10.0版本,記憶體8G的ECS雲主機上的預設值設定:

sysctl -a | grep tcp_rmem

net.ipv4.tcp_rmem = 4096 87380 6291456

同時,不是TCP receive buffer的大小就等于TCP接收視窗的大小。有bytes/2^tcp_adv_win_scale的大小配置設定給應用。如果net.ipv4.tcp_adv_win_scale的大小為2,表示有1/4的TCP buffer給應用,TCP把其餘的3/4給TCP接視窗。

程序設定

程序可以利用系統調用setsockopt()設定socket屬性,用SO_RCVBUF參數手動設定TCP receive buffer大小。比如NGINX可以在listen中配置rcvbuf=size。

2. net.ipv4.tcp_window_scaling

在前面提到,如果要讓TCP接收視窗超過64KB大小,需要利用TCP Options的Window scale字段。而在系統核心參數設定裡,對應的就是net.ipv4.tcp_window_scaling參數,這個參數預設是開啟的。但是在這個案例中明顯不是因為net.ipv4.tcp_window_scaling的原因, TCP接收視窗的大小還遠遠小于64KB。

問題解決

檢視了相關核心參數并沒有問題,最終明确問題是因為在Web server中限制了過小的rcvbuf到導緻。調整參數後上傳速度明顯改善。

繼續閱讀