雲計算

記一次典型的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) 問題。直接拿到測試的抓包,能避免了很多彎路。客戶端上傳到高防節點的抓包如下:
image.png

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

  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接收窗口會影響客戶端向服務端傳輸數據的效率。

image.png

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毫秒左右。
image.png

客戶端走SLB的RTT如下圖:在8毫秒左右。
image.png

解決方案

影響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到導致。調整參數後上傳速度明顯改善。

Leave a Reply

Your email address will not be published. Required fields are marked *