閱讀895 返回首頁    go 阿裏雲 go 技術社區[雲棲]


就是要你懂 TCP-- 最經典的TCP性能問題

就是要你懂 TCP-- 最經典的TCP性能問題

問題描述

某個PHP服務通過Nginx將後麵的tair封裝了一下,讓其他應用可以通過http協議訪問Nginx來get、set 操作tair

上線後測試一切正常,每次操作幾毫秒,但是有一次有個應用的value是300K,這個時候set一次需要300毫秒以上。 在沒有任何並發壓力單線程單次操作也需要這麼久,這個延遲是沒有道理和無法接受的。

問題的原因

是因為TCP協議為了做一些帶寬利用率、性能方麵的優化,而做了一些特殊處理。比如Delay Ack和Nagle算法。

這個原因對大家理解TCP基本的概念後能在實戰中了解一些TCP其它方麵的性能和影響。

什麼是delay ack

由我前麵的TCP介紹文章大家都知道,TCP是可靠傳輸,可靠的核心是收到包後回複一個ack來告訴對方收到了。

來看一個例子:
image.png

截圖中的Nignx(8085端口),收到了一個http request請求,然後立即回複了一個ack包給client,接著又回複了一個http response 給client。大家注意回複的ack包長度66,實際內容長度為0,ack信息放在TCP包頭裏麵,也就是這裏發了一個66字節的空包給客戶端來告訴客戶端我收到你的請求了。

這裏沒毛病,邏輯很對,符合TCP的核心可靠傳輸的意義。但是帶來的一個問題是:帶寬效率不高。那能不能優化呢?

這裏的優化就是delay ack。

delay ack是指收到包後不立即ack,而是等一小會(比如40毫秒)看看,如果這40毫秒以內正好有一個包(比如上麵的http response)發給client,那麼我這個ack包就跟著發過去(順風車,http reponse包不需要增加任何大小),這樣節省了資源。 當然如果超過這個時間還沒有包發給client(比如nginx處理需要40毫秒以上),那麼這個ack也要發給client了(即使為空,要不client以為丟包了,又要重發http request,劃不來)。

假如這個時候ack包還在等待延遲發送的時候,又收到了client的一個包,那麼這個時候server有兩個ack包要回複,那麼os會把這兩個ack包合起來**立即**回複一個ack包給client,告訴client前兩個包都收到了。

也就是delay ack開啟的情況下:ack包有順風車就搭;如果湊兩個ack包自己包個車也立即發車;再如果等了40毫秒以上也沒順風車,那麼自己打個車也發車。

截圖中Nginx**沒有開delay ack**,所以你看紅框中的ack是完全可以跟著綠框(http response)一起發給client的,但是沒有,紅框的ack立即打車跑了

什麼是Nagle算法

下麵的偽代碼就是Nagle算法的基本邏輯,摘自wiki

if there is new data to send
  if the window size >= MSS and available data is >= MSS
        send complete MSS segment now
  else
    if there is unconfirmed data still in the pipe
        enqueue data in the buffer until an acknowledge is received
    else
        send data immediately
    end if
  end if
end if

這段代碼的意思是如果要發送的數據大於 MSS的話,立即發送。
否則:
看看前麵發出去的包是不是還有沒有ack的,如果有沒有ack的那麼我這個小包不急著發送,等前麵的ack回來再發送

我總結下Nagle算法邏輯就是:如果發送的包很小(不足MSS),又有包發給了對方對方還沒回複說收到了,那我也不急著發,等前麵的包回複收到了再發。這樣可以優化帶寬利用率(早些年帶寬資源還是很寶貴的),Nagle算法也是用來優化改進tcp傳輸效率的。

如果client啟用Nagle,並且server端啟用了delay ack會有什麼後果呢?

假如client要發送一個http請求給server,這個請求有1600個bytes,握手的MSS是1460,那麼這1600個bytes就會分成2個TCP包,第一個包1460,剩下的140bytes放在第二個包。第一個包發出去後,server收到第一個包,因為delay ack所以沒有回複ack,同時因為server沒有收全這個HTTP請求,所以也沒法回複HTTP response(server等一個完整的HTTP請求,或者40毫秒的delay時間)。client這邊開啟了Nagle算法(默認開啟)第二個包比較小(140<MSS),第一個包的ack還沒有回來,那麼第二個包就不發了,等!互相等!一直到Delay Ack的Delay時間到了!

這就是悲劇的核心原因。

再來看一個經典例子和數據分析

這個案例來自:https://www.stuartcheshire.org/papers/nagledelayedack/

案例核心奇怪的問題是,如果傳輸的數據是 99,900 bytes,速度5.2M/秒;
如果傳輸的數據是 100,000 bytes 速度2.7M/秒,多了10個bytes,不至於傳輸速度差這麼多。

原因就是:

 99,900 bytes = 68 full-sized 1448-byte packets, plus 1436 bytes extra
100,000 bytes = 69 full-sized 1448-byte packets, plus   88 bytes extra

99,900 bytes:

68個整包會立即發送,因為68是偶數,對方收到最後兩個包後立即回複ack(delay ack湊夠兩個也立即ack),那麼剩下的1436也很快發出去(根據nagle算法,沒有沒ack的包了,立即發)

100,000 bytes:

前麵68個整包很快發出去也收到ack回複了,然後發了第69個整包,剩下88bytes根據nagle算法要等一等,server收到第69個ack後,因為delay ack不回複(手裏隻攢下一個沒有回複的包),所以client、server兩邊等在等,一直等到server的delay ack超時了。

挺奇怪和挺有意思吧,作者還給出了傳輸數據的圖表:

這是有問題的傳輸圖,明顯有個平台層,這個平台層就是兩邊在互相等,整個速度肯定就上不去。

如果傳輸的都是99,900,那麼整個圖形就很平整:

回到前麵的問題

服務寫好後,開始測試都沒有問題,rt很正常(一般測試的都是小對象),沒有觸發這個問題。後來碰到一個300K的rt就到幾百毫秒了,就是因為這個原因。

另外有些http post會故意把包頭和包內容分成兩個包,再加一個Expect參數之類的,更容易觸發這個問題。

這是修改後的C代碼

    struct curl_slist *list = NULL;
    //合並post包
    list = curl_slist_append(list, "Expect:");  

    CURLcode code(CURLE_FAILED_INIT);
    if (CURLE_OK == (code = curl_easy_setopt(curl, CURLOPT_URL, oss.str().c_str())) &&
            CURLE_OK == (code = curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, timeout)) &&
            CURLE_OK == (code = curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &write_callback)) &&
            CURLE_OK == (code = curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L)) &&
            CURLE_OK == (code = curl_easy_setopt(curl, CURLOPT_POST, 1L)) &&
            CURLE_OK == (code = curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, pooh.sizeleft)) &&
            CURLE_OK == (code = curl_easy_setopt(curl, CURLOPT_READFUNCTION, read_callback)) &&
            CURLE_OK == (code = curl_easy_setopt(curl, CURLOPT_READDATA, &pooh)) &&                
            CURLE_OK == (code = curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L)) && //1000 ms curl bug
            CURLE_OK == (code = curl_easy_setopt(curl, CURLOPT_HTTPHEADER, list))                
            ) {

            //這裏如果是小包就不開delay ack,實際不科學
            if (request.size() < 1024) {
                    code = curl_easy_setopt(curl, CURLOPT_TCP_NODELAY, 1L);
            } else {
                    code = curl_easy_setopt(curl, CURLOPT_TCP_NODELAY, 0L);
            }
            if(CURLE_OK == code) {
                    code = curl_easy_perform(curl);
            }

上麵中文注釋的部分是後來的改進,然後經過測試同一個300K的對象也能在幾毫米以內完成get、set了。

尤其是在Post請求將HTTP Header和Body內容分成兩個包後,容易出現這種延遲問題


就是要你懂TCP相關文章:

關於TCP 半連接隊列和全連接隊列
MSS和MTU導致的悲劇
2016年雙11通過網絡優化提升10倍性能
就是要你懂TCP的握手和揮手


總結

這個問題確實經典,非常隱晦一般不容易碰到,碰到一次決不放過她。文中所有client、server的概念都是相對的,client也有delay ack的問題。 Nagle算法一般默認開啟的

參考文章:
https://access.redhat.com/solutions/407743

https://www.stuartcheshire.org/papers/nagledelayedack/

https://en.wikipedia.org/wiki/Nagle%27s_algorithm

https://en.wikipedia.org/wiki/TCP_delayed_acknowledgment

最後更新:2017-05-31 19:34:58

  上一篇:go  零門檻學習https--(2)https中s的秘密
  下一篇:go  人工智能技術在旅遊領域的實踐