就是要你懂TCP–最經典的TCP效能問題

rui__發表於2017-05-31

就是要你懂 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

這就是悲劇的核心原因。

再來看一個經典例子和資料分析

這個案例來自:http://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

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

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

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


相關文章