一個(gè)項(xiàng)目對接第三方接口數(shù)據(jù)。對方是TCP接口,發(fā)送數(shù)據(jù)頻率很高。平均2毫秒發(fā)送三四千個(gè)字節(jié)。由于TCP協(xié)議的粘包拆包問題,我這里接收到的數(shù)據(jù)需要對粘包拆包按照對方數(shù)據(jù)的格式進(jìn)行處理。對接了一段時(shí)間后發(fā)現(xiàn),TCP連接會自動斷開。由于我這里做了斷開重連的邏輯。所以最終的現(xiàn)象就是一直在斷開,重連,再斷開,再重連。
向數(shù)據(jù)提供方咨詢,數(shù)據(jù)提供方給出的反饋是數(shù)據(jù)消費(fèi)不過來,造成數(shù)據(jù)積壓后,他們的程序就會主動斷開TCP連接。通過日志發(fā)現(xiàn),我這里確實(shí)在斷開前,消費(fèi)數(shù)據(jù)出現(xiàn)了延遲。通過日志觀察發(fā)現(xiàn),接收數(shù)據(jù)的時(shí)候,比發(fā)送數(shù)據(jù)的時(shí)間慢了幾秒,由于數(shù)據(jù)量很大,所以造成了積壓,數(shù)據(jù)提供方就斷開了連接。
問題分析
于是,問題的焦點(diǎn)就到了為何我的程序消費(fèi)不過來數(shù)據(jù)呢?首先想到的就是我寫的程序性能有問題,導(dǎo)致無法消費(fèi)平均2毫秒產(chǎn)生的三四千個(gè)字節(jié)這個(gè)數(shù)據(jù)頻率。由于粘包拆包程序是我自己自定義處理的,于是,我懷疑是自己的處理邏輯性能差。
通過研究發(fā)現(xiàn),netty框架提供了針對于TCP粘包拆包的解析類。于是,我引入了netty框架,使用netty框架提供的LengthFieldBasedFrameDecoder解析器對接收到的數(shù)據(jù)進(jìn)行處理。發(fā)現(xiàn)還是會出現(xiàn)延遲,消費(fèi)不過來的現(xiàn)象發(fā)生。
由于netty接收數(shù)據(jù)后,對數(shù)據(jù)進(jìn)行處理,默認(rèn)是使用單線程來完成的。即接收TCP數(shù)據(jù)和處理粘包拆包是在一個(gè)線程中完成的。這是不是影響了消費(fèi)的速率呢?于是,我寫了兩個(gè)線程,一個(gè)用于接收數(shù)據(jù),然后把接收到的數(shù)據(jù)存入一個(gè)集合容器中。緊接著繼續(xù)拉取下一批數(shù)據(jù)。另一個(gè)線程用于處理集合容器中的數(shù)據(jù)。這種解決方案依舊不行,還是延遲消費(fèi)數(shù)據(jù)。
難道是接收了數(shù)據(jù),往集合容器中存放這個(gè)操作,也影響了性能,使其消費(fèi)不過來了?于是,我把程序改成了只接收數(shù)據(jù),然后打印一個(gè)接收字節(jié)數(shù)的日志,其余的操作再也沒有了。嘗試這種操作是否可以不自動斷開TCP連接。因?yàn)椴蛔詣訑嚅_TCP連接證明數(shù)據(jù)消費(fèi)沒有延遲。
令人費(fèi)解的事情發(fā)生了,純接收數(shù)據(jù),就打印個(gè)接收字節(jié)數(shù)的日志,還是會自動斷開TCP連接。這就表明,無論我怎么優(yōu)化代碼,它都會延遲消費(fèi)。這就不是我代碼的問題而引起的消費(fèi)延遲了。因?yàn)槲铱隙ㄒソ邮誘CP的數(shù)據(jù)。而現(xiàn)在純接收數(shù)據(jù),不做任何處理,就發(fā)生了延遲了。這說明已經(jīng)不是我程序代碼的問題了。
起初懷疑的對象是操作系統(tǒng)是不是消費(fèi)不過來了?一定是操作系統(tǒng)先從網(wǎng)絡(luò)中拉取數(shù)據(jù),我的應(yīng)用程序再從操縱系統(tǒng)中獲取數(shù)據(jù)的。如果操作系統(tǒng)這個(gè)層面就消費(fèi)不過來了,那么我的程序肯定也消費(fèi)不過來。因?yàn)槲矣玫姆?wù)器是Window Server系統(tǒng),所以,將程序部署到了一臺linux服務(wù)器上進(jìn)行純接收數(shù)據(jù),不做任何處理的測試。最后發(fā)現(xiàn),依舊會自動斷開TCP連接。
服務(wù)器這個(gè)猜測也失敗了。因?yàn)閿?shù)據(jù)提供方也會消費(fèi)這些數(shù)據(jù),而他們的系統(tǒng)沒有出現(xiàn)過這種自動斷開的情況。說明不是操作系統(tǒng)消費(fèi)不過來了。
于是,我把目光轉(zhuǎn)到了接收字節(jié)數(shù)量的日志上。發(fā)現(xiàn)大多數(shù)日志輸出的都是一次拉取1460個(gè)字節(jié)。還發(fā)現(xiàn)會有一次拉取幾萬個(gè)字節(jié)的情況出現(xiàn)。而最大的拉取量是65536個(gè)字節(jié),不會比這個(gè)字節(jié)再大了。即使我在程序里定義的讀取數(shù)據(jù)的byte數(shù)組的長度是10萬,程序最多也是拉取65536個(gè)字節(jié)。
搞不懂這些數(shù)字代表的含義,可以看看下面這篇文章:
深入理解 TCP 協(xié)議:從原理到實(shí)戰(zhàn)【超詳細(xì)】-上
深入理解 TCP 協(xié)議:從原理到實(shí)戰(zhàn)【超詳細(xì)】-下
這里解釋一下上述日志中幾個(gè)數(shù)字的含義。首先,1460是以太網(wǎng)的MTU是1500,去掉40個(gè)字節(jié)的TCP頭和IP頭,業(yè)務(wù)數(shù)據(jù)的長度就是1460個(gè)字節(jié)。即一個(gè)包最長的業(yè)務(wù)數(shù)據(jù)就是1460。而程序每次大部分都讀取1460個(gè)字節(jié)。證明滑動窗口里的數(shù)據(jù)是沒有積壓的。也就是說當(dāng)程序讀1460個(gè)字節(jié)的時(shí)候,說明是消費(fèi)的過來的。因?yàn)槿绻麛?shù)據(jù)積壓了,那么必定在滑動窗口里有很多個(gè)字節(jié),甚至把滑動窗口填滿。那么程序單次拉取字節(jié)數(shù),就不可能是1460個(gè),而是比1460個(gè)要多。所以當(dāng)程序每次拉取也是1460的時(shí)候,說明發(fā)一次數(shù)據(jù),就可以消費(fèi)一次數(shù)據(jù),是不存在延遲現(xiàn)象的。
那么日志中小于1460個(gè)數(shù)據(jù)拉取又是如何發(fā)生的呢?加入TCP的發(fā)送方一次發(fā)送了2000個(gè)字節(jié)的業(yè)務(wù)數(shù)據(jù),而在物理層的以太網(wǎng)中,只能發(fā)送1460個(gè)字節(jié)。那么就會對數(shù)據(jù)進(jìn)行分片。前1460個(gè)字節(jié)為一個(gè)包,發(fā)送獲取了,剩余的540個(gè)字節(jié)是另一個(gè)包發(fā)送過去。這就造成了少于1460個(gè)字節(jié)的拉取情況出現(xiàn)。其本質(zhì)也是發(fā)送了多少數(shù)據(jù)就拉取了多少數(shù)據(jù),不存在延遲現(xiàn)象。
那么延遲到底是怎么發(fā)生的呢?這就要分析那些一次拉取上萬個(gè)字節(jié)的數(shù)據(jù)的情況是如何發(fā)生的了。通過觀察發(fā)現(xiàn),每次拉取上萬個(gè)字節(jié)的數(shù)據(jù),日志都會卡頓幾十毫秒甚至更長才會拉取一次數(shù)據(jù)。由于停頓的這幾十毫秒一直有數(shù)據(jù)發(fā)送過來,所以接收滑動窗口的數(shù)據(jù)就會一直增加。當(dāng)停頓結(jié)束后再次拉取數(shù)據(jù)時(shí),就從滑動窗口里拉取了更多的數(shù)據(jù)回來。所以就有上萬個(gè)字節(jié)的數(shù)據(jù)了。那程序?yàn)楹螘nD,不拉取數(shù)據(jù)了呢?
通過wireshark抓包工具發(fā)現(xiàn),當(dāng)TCP出現(xiàn)丟包時(shí),程序就不拉取數(shù)據(jù)了。因?yàn)楫?dāng)TCP丟包后,由于滑動窗口的存在,在滑動窗口范圍內(nèi),還會繼續(xù)接收發(fā)送過來的數(shù)據(jù)。但是因?yàn)閬G包了,所以應(yīng)用程序就不會再消費(fèi)數(shù)據(jù)了。此時(shí),我這里的操作系統(tǒng)會給發(fā)送方反饋丟包信息(TCP dup ack),默認(rèn)情況下是發(fā)送三次TCP dup ack后,發(fā)送方就會立馬重傳丟包的數(shù)據(jù)。但是觀察wireshark發(fā)現(xiàn),丟包后,重傳50多次TCP dup ack后,發(fā)送方才會返回丟包的數(shù)據(jù)。這就說明,TCP dup ack反饋消息也在一直丟失,直到發(fā)送了50多次后,才收到3條TCP dup ack信息。當(dāng)然,如果一直收不到TCP dup ack信息,那么只有等到超時(shí)時(shí)間后,發(fā)送方主動再次發(fā)送丟包數(shù)據(jù)了??梢?,這里的網(wǎng)絡(luò)環(huán)境是有多差。
那這就分析出了消費(fèi)延遲的根本問題所在。因?yàn)門CP發(fā)生了丟包,導(dǎo)致了應(yīng)用程序的停頓,無法讀取TCP丟包后的數(shù)據(jù)。而丟包的反饋TCP dup ack又無法第一時(shí)間被發(fā)送方接收到,所以接收方應(yīng)用程序卡頓的時(shí)間就會變長。而TCP產(chǎn)生數(shù)據(jù)的頻率又是很高的,所以在停頓的這個(gè)期間,就產(chǎn)生了很多的數(shù)據(jù)。當(dāng)丟包的數(shù)據(jù)被發(fā)送方發(fā)送過來后,應(yīng)用程序從丟包位置讀取數(shù)據(jù),而此時(shí)丟包的位置后面已經(jīng)產(chǎn)生了大量的數(shù)據(jù),所以造成了消費(fèi)的延遲。
分析到這里,問題的本質(zhì)似乎找到了。但是還有一個(gè)現(xiàn)象不可忽略。那就是每次TCP主動斷開的位置,都不是程序停頓的位置,也不是一次拉取幾萬個(gè)字節(jié)的位置。而是一次拉取1460個(gè)字節(jié)的位置。而上面我們又分析道,一次拉取1460個(gè)字節(jié),說明消費(fèi)是能跟的上的,沒有積壓數(shù)據(jù)。那為何還會消費(fèi)延遲呢?
這就涉及到了TCP網(wǎng)絡(luò)擁堵的處理機(jī)制。只要發(fā)生了丟包,TCP就認(rèn)為當(dāng)前的網(wǎng)絡(luò)環(huán)境不佳,TCP發(fā)送方會根據(jù)自己的機(jī)制,主動減少發(fā)送量,避免對網(wǎng)絡(luò)造成更大的壓力。當(dāng)發(fā)生丟包后,應(yīng)用程序停頓了幾秒。但是停頓結(jié)束后,會有幾次拉取幾萬個(gè)字節(jié)數(shù)據(jù)的情況,這幾次拉取,就會趕上積壓的數(shù)據(jù)的消費(fèi)速率。也就是這里會有延遲,但是拉取幾次大數(shù)據(jù)量后,消費(fèi)就趕上來了。而TCP發(fā)送方認(rèn)為當(dāng)前的網(wǎng)絡(luò)環(huán)境不佳,所以發(fā)送方主動減少了發(fā)送的吞吐量。這就造成了發(fā)送方產(chǎn)生了大量的數(shù)據(jù),但是發(fā)送的數(shù)據(jù)量很小。這就造成了發(fā)送方數(shù)據(jù)的積壓。當(dāng)積壓到一定程度后,發(fā)送方的應(yīng)用程序就斷開了連接。
總結(jié)
綜上所述,發(fā)生消費(fèi)延遲問題,是因?yàn)門CP頻繁丟包,觸發(fā)了TCP的擁堵處理機(jī)制,導(dǎo)致發(fā)送方發(fā)送量減少,數(shù)據(jù)產(chǎn)生了積壓,造成了消費(fèi)的延遲。
上面還有一點(diǎn)提到數(shù)據(jù)積壓后,最大的拉取量是65536,這是因?yàn)椴僮飨到y(tǒng)所能承受的單次數(shù)據(jù)拉取量,就是65535個(gè),如果頻繁的接收大于65535字節(jié)的數(shù)據(jù),會使操作系統(tǒng)崩潰。所以這個(gè)值是操作系統(tǒng)進(jìn)行的限制。而我上面又說到,數(shù)據(jù)提供方也在消費(fèi)這個(gè)數(shù)據(jù),但是他們沒有出現(xiàn)過延遲。是因?yàn)樗麄冊诒镜剡M(jìn)行的數(shù)據(jù)消費(fèi)。即數(shù)據(jù)發(fā)送和接收都是一個(gè)服務(wù)器。這種情況是不會走物理層的,也不會經(jīng)過網(wǎng)卡,所以單次傳輸就沒有1460這個(gè)限制了,就升級到了65535這個(gè)限制。而且本地傳輸也不會出現(xiàn)丟包現(xiàn)象。所以他們消費(fèi)沒出現(xiàn)延遲。
感悟
TCP為了保證數(shù)據(jù)的安全性,發(fā)生丟包后做出的一系列處理會影響性能。而在大數(shù)量的情況下,會把性能的影響放大。所以,在選擇協(xié)議時(shí),要綜合分析,選擇最合適的協(xié)議。例如本次的業(yè)務(wù)場景,完全不適合用TCP協(xié)議,而適合用UDP協(xié)議。因?yàn)榻邮盏綌?shù)據(jù)后,也會根據(jù)邏輯過濾掉大多數(shù)的數(shù)據(jù),而是保留一部分?jǐn)?shù)據(jù)。所以對數(shù)據(jù)的安全性要求不是那么高,使用UDP協(xié)議,允許丟失一部分?jǐn)?shù)據(jù),就不會出現(xiàn)這種消費(fèi)延遲的問題了。
抓包記錄
記錄一下使用wireshark抓包發(fā)現(xiàn)問題的過程。
首先,選擇要監(jiān)聽的網(wǎng)卡,然后輸入過濾器,過濾ip(host xxx),只查看發(fā)送數(shù)據(jù)的ip的TCP協(xié)議。
然后,進(jìn)入網(wǎng)絡(luò)監(jiān)控頁面,在頂部的過濾器中,輸入tcp,表示只監(jiān)控tcp協(xié)議。
第二列的TIme字段,默認(rèn)不是yyyy-MM-dd HH:mm:ss格式的,需要手動調(diào)成此格式,方便查看數(shù)據(jù)發(fā)送的時(shí)間:
然后,開始分析接收到的具體數(shù)據(jù):
如上圖所示,TCP Previous segment not captured就代表接收方收到了后面的數(shù)據(jù),但是前面的數(shù)據(jù)還沒收到。即前面的數(shù)據(jù)發(fā)生了丟包。這個(gè)提示是wireshark自己分析給出的提示,而且還標(biāo)黑了,說明接收數(shù)據(jù)發(fā)生了丟包。
發(fā)生丟包后,接收方就給發(fā)送方發(fā)送TCP Dup Ack信息,告訴接收方丟包了。默認(rèn)情況是發(fā)送三次,接收方就會把丟包數(shù)據(jù)返回,但是如上圖所示,發(fā)送了11次,還沒收到丟包數(shù)據(jù)。
直到發(fā)送了58次以后,才收到發(fā)送方返回的 TCP Fast Retransmission信息,這表明是收到了丟包的數(shù)據(jù)。
此外,在統(tǒng)計(jì)–>專家信息中,wireshark也統(tǒng)計(jì)了各種情況發(fā)生的次數(shù),如下圖:
第一行就是丟包的次數(shù),可見,發(fā)生了85次丟包。丟包現(xiàn)象很嚴(yán)重。
-
接口
+關(guān)注
關(guān)注
33文章
8645瀏覽量
151396 -
數(shù)據(jù)
+關(guān)注
關(guān)注
8文章
7081瀏覽量
89180 -
TCP
+關(guān)注
關(guān)注
8文章
1372瀏覽量
79142 -
程序
+關(guān)注
關(guān)注
117文章
3792瀏覽量
81161
發(fā)布評論請先 登錄
相關(guān)推薦
評論