就是要你懂 TCP-- 最經(jīng)典的TCP性能問(wèn)題
問(wèn)題描述
某個(gè)PHP服務(wù)通過(guò)Nginx將后面的tair封裝了一下,讓其他應(yīng)用可以通過(guò)http協(xié)議訪問(wèn)Nginx來(lái)get、set 操作tair
上線后測(cè)試一切正常,每次操作幾毫秒,但是有一次有個(gè)應(yīng)用的value是300K,這個(gè)時(shí)候set一次需要300毫秒以上。 在沒(méi)有任何并發(fā)壓力單線程單次操作也需要這么久,這個(gè)延遲是沒(méi)有道理和無(wú)法接受的。
問(wèn)題的原因
是因?yàn)門(mén)CP協(xié)議為了做一些帶寬利用率、性能方面的優(yōu)化,而做了一些特殊處理。比如Delay Ack和Nagle算法。
這個(gè)原因?qū)Υ蠹依斫釺CP基本的概念后能在實(shí)戰(zhàn)中了解一些TCP其它方面的性能和影響。
什么是delay ack
由我前面的TCP介紹文章大家都知道,TCP是可靠傳輸,可靠的核心是收到包后回復(fù)一個(gè)ack來(lái)告訴對(duì)方收到了。
來(lái)看一個(gè)例子:
截圖中的Nignx(8085端口),收到了一個(gè)http request請(qǐng)求,然后立即回復(fù)了一個(gè)ack包給client,接著又回復(fù)了一個(gè)http response 給client。大家注意回復(fù)的ack包長(zhǎng)度66,實(shí)際內(nèi)容長(zhǎng)度為0,ack信息放在TCP包頭里面,也就是這里發(fā)了一個(gè)66字節(jié)的空包給客戶端來(lái)告訴客戶端我收到你的請(qǐng)求了。
這里沒(méi)毛病,邏輯很對(duì),符合TCP的核心可靠傳輸?shù)囊饬x。但是帶來(lái)的一個(gè)問(wèn)題是:帶寬效率不高。那能不能優(yōu)化呢?
這里的優(yōu)化就是delay ack。
delay ack是指收到包后不立即ack,而是等一小會(huì)(比如40毫秒)看看,如果這40毫秒以內(nèi)正好有一個(gè)包(比如上面的http response)發(fā)給client,那么我這個(gè)ack包就跟著發(fā)過(guò)去(順風(fēng)車(chē),http reponse包不需要增加任何大小),這樣節(jié)省了資源。 當(dāng)然如果超過(guò)這個(gè)時(shí)間還沒(méi)有包發(fā)給client(比如nginx處理需要40毫秒以上),那么這個(gè)ack也要發(fā)給client了(即使為空,要不client以為丟包了,又要重發(fā)http request,劃不來(lái))。
假如這個(gè)時(shí)候ack包還在等待延遲發(fā)送的時(shí)候,又收到了client的一個(gè)包,那么這個(gè)時(shí)候server有兩個(gè)ack包要回復(fù),那么os會(huì)把這兩個(gè)ack包合起來(lái)立即回復(fù)一個(gè)ack包給client,告訴client前兩個(gè)包都收到了。
也就是delay ack開(kāi)啟的情況下:ack包有順風(fēng)車(chē)就搭;如果湊兩個(gè)ack包自己包個(gè)車(chē)也立即發(fā)車(chē);再如果等了40毫秒以上也沒(méi)順風(fēng)車(chē),那么自己打個(gè)車(chē)也發(fā)車(chē)。
截圖中Nginx沒(méi)有開(kāi)delay ack,所以你看紅框中的ack是完全可以跟著綠框(http response)一起發(fā)給client的,但是沒(méi)有,紅框的ack立即打車(chē)跑了
什么是Nagle算法
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
這段代碼的意思是如果要發(fā)送的數(shù)據(jù)大于 MSS的話,立即發(fā)送。
否則:
看看前面發(fā)出去的包是不是還有沒(méi)有ack的,如果有沒(méi)有ack的那么我這個(gè)小包不急著發(fā)送,等前面的ack回來(lái)再發(fā)送
我總結(jié)下Nagle算法邏輯就是:如果發(fā)送的包很小(不足MSS),又有包發(fā)給了對(duì)方對(duì)方還沒(méi)回復(fù)說(shuō)收到了,那我也不急著發(fā),等前面的包回復(fù)收到了再發(fā)。這樣可以優(yōu)化帶寬利用率(早些年帶寬資源還是很寶貴的),Nagle算法也是用來(lái)優(yōu)化改進(jìn)tcp傳輸效率的。
如果client啟用Nagle,并且server端啟用了delay ack會(huì)有什么后果呢?
假如client要發(fā)送一個(gè)http請(qǐng)求給server,這個(gè)請(qǐng)求有1600個(gè)bytes,握手的MSS是1460,那么這1600個(gè)bytes就會(huì)分成2個(gè)TCP包,第一個(gè)包1460,剩下的140bytes放在第二個(gè)包。第一個(gè)包發(fā)出去后,server收到第一個(gè)包,因?yàn)閐elay ack所以沒(méi)有回復(fù)ack,同時(shí)因?yàn)閟erver沒(méi)有收全這個(gè)HTTP請(qǐng)求,所以也沒(méi)法回復(fù)HTTP response(server等一個(gè)完整的HTTP請(qǐng)求,或者40毫秒的delay時(shí)間)。client這邊開(kāi)啟了Nagle算法(默認(rèn)開(kāi)啟)第二個(gè)包比較?。?40<MSS),第一個(gè)包的ack還沒(méi)有回來(lái),那么第二個(gè)包就不發(fā)了,等!互相等!一直到Delay Ack的Delay時(shí)間到了!
這就是悲劇的核心原因。
再來(lái)看一個(gè)經(jīng)典例子和數(shù)據(jù)分析
這個(gè)案例來(lái)自:http://www.stuartcheshire.org/papers/nagledelayedack/
案例核心奇怪的問(wèn)題是,如果傳輸?shù)臄?shù)據(jù)是 99,900 bytes,速度5.2M/秒;
如果傳輸?shù)臄?shù)據(jù)是 100,000 bytes 速度2.7M/秒,多了10個(gè)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個(gè)整包會(huì)立即發(fā)送(都是整包,不受Nagle算法的影響),因?yàn)?8是偶數(shù),對(duì)方收到最后兩個(gè)包后立即回復(fù)ack(delay ack湊夠兩個(gè)也立即ack),那么剩下的1436也很快發(fā)出去(根據(jù)Nagle算法,沒(méi)有沒(méi)ack的包了,立即發(fā))
100,000 bytes:
前面68個(gè)整包很快發(fā)出去也收到ack回復(fù)了,然后發(fā)了第69個(gè)整包,剩下88bytes(不夠一個(gè)整包)根據(jù)Nagle算法要等一等,server收到第69個(gè)ack后,因?yàn)閐elay ack不回復(fù)(手里只攢下一個(gè)沒(méi)有回復(fù)的包),所以client、server兩邊等在等,一直等到server的delay ack超時(shí)了。
挺奇怪和挺有意思吧,作者還給出了傳輸數(shù)據(jù)的圖表:
這是有問(wèn)題的傳輸圖,明顯有個(gè)平臺(tái)層,這個(gè)平臺(tái)層就是兩邊在互相等,整個(gè)速度肯定就上不去。
如果傳輸?shù)亩际?9,900,那么整個(gè)圖形就很平整:
回到前面的問(wèn)題
服務(wù)寫(xiě)好后,開(kāi)始測(cè)試都沒(méi)有問(wèn)題,rt很正常(一般測(cè)試的都是小對(duì)象),沒(méi)有觸發(fā)這個(gè)問(wèn)題。后來(lái)碰到一個(gè)300K的rt就到幾百毫秒了,就是因?yàn)檫@個(gè)原因。
另外有些http post會(huì)故意把包頭和包內(nèi)容分成兩個(gè)包,再加一個(gè)Expect參數(shù)之類(lèi)的,更容易觸發(fā)這個(gè)問(wèn)題。
這是修改后的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))
) {
//這里如果是小包就不開(kāi)delay ack,實(shí)際不科學(xué)
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);
}
上面中文注釋的部分是后來(lái)的改進(jìn),然后經(jīng)過(guò)測(cè)試同一個(gè)300K的對(duì)象也能在幾毫米以內(nèi)完成get、set了。
尤其是在Post請(qǐng)求將HTTP Header和Body內(nèi)容分成兩個(gè)包后,容易出現(xiàn)這種延遲問(wèn)題
就是要你懂TCP相關(guān)文章:
關(guān)于TCP 半連接隊(duì)列和全連接隊(duì)列
MSS和MTU導(dǎo)致的悲劇
2016年雙11通過(guò)網(wǎng)絡(luò)優(yōu)化提升10倍性能
就是要你懂TCP的握手和揮手
總結(jié)
這個(gè)問(wèn)題確實(shí)經(jīng)典,非常隱晦一般不容易碰到,碰到一次決不放過(guò)她。文中所有client、server的概念都是相對(duì)的,client也有delay ack的問(wèn)題。 Nagle算法一般默認(rèn)開(kāi)啟的
參考文章:
https://access.redhat.com/solutions/407743
http://www.stuartcheshire.org/papers/nagledelayedack/