自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

解Bug之路-TCP使用不當(dāng)引起的Bug

網(wǎng)絡(luò) 通信技術(shù)
筆者很熱衷于解決Bug,同時(shí)比較擅長(zhǎng)(網(wǎng)絡(luò)/協(xié)議)部分,所以經(jīng)常被喚去解決一些網(wǎng)絡(luò)IO方面的Bug?,F(xiàn)在就挑一個(gè)案例出來(lái),寫出分析思路,以饗讀者,希望讀者在以后的工作中能夠少踩點(diǎn)坑。

[[347677]]

 前言

筆者很熱衷于解決Bug,同時(shí)比較擅長(zhǎng)(網(wǎng)絡(luò)/協(xié)議)部分,所以經(jīng)常被喚去解決一些網(wǎng)絡(luò)IO方面的Bug?,F(xiàn)在就挑一個(gè)案例出來(lái),寫出分析思路,以饗讀者,希望讀者在以后的工作中能夠少踩點(diǎn)坑。

關(guān)于TCP流

TCP是流的概念,解釋如下

  • TCP窗口的大小取決于當(dāng)前的網(wǎng)絡(luò)狀況、對(duì)端的緩沖大小等等因素,
  • TCP將這些都從底層屏蔽。開(kāi)發(fā)者無(wú)法從應(yīng)用層獲取這些信息。
  • 這就意味著,當(dāng)你在接收TCP數(shù)據(jù)流的時(shí)候無(wú)法知道當(dāng)前接收了
  • 有多少數(shù)據(jù)流,數(shù)據(jù)可能在任意一個(gè)比特位(seq)上。

Bug現(xiàn)場(chǎng)

出Bug的系統(tǒng)是做與外部系統(tǒng)進(jìn)行對(duì)接之用。這兩者并不通過(guò)http協(xié)議進(jìn)行交互,而是在通過(guò)TCP協(xié)議之上封裝一層自己的報(bào)文進(jìn)行通訊。如下圖示:

通過(guò)監(jiān)控還發(fā)現(xiàn),此系統(tǒng)的業(yè)務(wù)量出現(xiàn)了不正常的飆升,大概有4倍的增長(zhǎng)。而且在監(jiān)控看來(lái),這些業(yè)務(wù)還是成功的。

第一反應(yīng),當(dāng)然是祭出重啟大法,第一時(shí)間重啟了機(jī)器。此后一切正常,交易量也回歸正常,仿佛剛才的Bug從來(lái)沒(méi)有發(fā)生過(guò)。在此之前,此系統(tǒng)已經(jīng)穩(wěn)定運(yùn)行了好幾個(gè)月,從來(lái)沒(méi)出現(xiàn)過(guò)錯(cuò)誤。

但是,這事不能就這么過(guò)去了,下次又出這種Bug怎么辦,繼續(xù)重啟么?由于筆者對(duì)分析這種網(wǎng)絡(luò)協(xié)議比較在行,于是Bug就拋到了筆者這。

錯(cuò)誤日志

線上系統(tǒng)用的框架為Mina,不停的Dump出其一堆以16進(jìn)制表示的二進(jìn)制字節(jié)流。

,并拋出異常

首先定位異常拋出點(diǎn)

以下代碼僅為筆者描述Bug之用,和當(dāng)時(shí)代碼有較大差別。

  1. private boolean handeMessage(IoBuffer in,ProtocolDecoderOutput out){ 
  2.  int lenDes = 4; 
  3.  byte[] data = new byte[lenDes]; 
  4.  in.mark(); 
  5.  in.get(data,0,lenDes); 
  6.  int messageLen = decodeLength(data); 
  7.  if(in.remaining() < messageLen){ 
  8.   logger.warn("未接收完畢"); 
  9.   in.reset(); 
  10.   return false
  11.  }else
  12.   ...... 
  13.  } 
  14.   

筆者本身經(jīng)常寫這種拆包代碼,第一眼就發(fā)現(xiàn)有問(wèn)題。讓我們?cè)倏匆谎蹐?bào)文結(jié)構(gòu):

上面的代碼首先從報(bào)文前4個(gè)字節(jié)中獲取到報(bào)文長(zhǎng)度,同時(shí)檢測(cè)在buffer中的存留數(shù)據(jù)是否夠報(bào)文長(zhǎng)度。

  1. if(in.remaining() < messageLen) 

為何沒(méi)有在一開(kāi)始檢測(cè)buffer中是否有足夠的4byte字節(jié)呢。此處有蹊蹺。直覺(jué)上就覺(jué)的是這導(dǎo)致了后來(lái)的種種現(xiàn)象。

事實(shí)上,在筆者解決各種Bug的過(guò)程中,經(jīng)常通過(guò)猜想等手段定位出Bug的原因。但是從現(xiàn)場(chǎng)取證,通過(guò)證據(jù)去解釋發(fā)生的現(xiàn)象,通過(guò)演繹去說(shuō)服同事,并對(duì)同事提出的種種問(wèn)題做出合理的解釋才是最困難的。

猜想總歸是猜想,必須要有實(shí)錘,沒(méi)有證據(jù)也說(shuō)服不了自己。

為何會(huì)拋出異常

這個(gè)異常由這句代碼拋出:

  1. int messageLen = decodeLength(data); 

從上面的Mina框架Dump出的數(shù)據(jù)來(lái)看,是解析前四個(gè)字節(jié)出了問(wèn)題,前4個(gè)字節(jié)為30,31,2E,01(16進(jìn)制)

最前面的包長(zhǎng)度是通過(guò)字符串來(lái)表示的,翻譯成十進(jìn)制就是48、49、46、1,再翻譯為字符串就是('0','1', 非數(shù)字, 非數(shù)字)

  1. 30, 31,    2E,   01  (16進(jìn)制) 
  2. 48, 49,    46,   1   (10進(jìn)制) 
  3. '0','1',非數(shù)字, 非數(shù)字 (字符串) 

很明顯,解析字符串的時(shí)候遇到前兩個(gè)byte,0和1可以解析出來(lái),但是遇到后面兩個(gè)byte就報(bào)錯(cuò)了。至于為什么是For input String,'01',而不是2E,是由于傳輸用的是小端序。

為何報(bào)文會(huì)出現(xiàn)非數(shù)字的字符串

鑒于上面的錯(cuò)誤代碼,筆者立馬意識(shí)到,應(yīng)該是TCP流處理不當(dāng)了。這時(shí)候就應(yīng)該去找發(fā)生Bug的最初時(shí)間點(diǎn)的日志,去分析為何那個(gè)時(shí)間會(huì)gg。

由于最初那個(gè)錯(cuò)誤日志Dump數(shù)來(lái)的數(shù)據(jù)過(guò)于長(zhǎng),在此就不貼出來(lái)了,以下示意圖是筆者當(dāng)時(shí)人肉decode的結(jié)果:

拋出的異常為:

這個(gè)異常拋出點(diǎn)恰恰就在筆者懷疑的

  1. in.get(data,0,lenDes); 

這里。至此,筆者就幾乎已經(jīng)確定是這個(gè)Bug導(dǎo)致的。

演繹

Mina框架在Buffer中解幀,前5幀正常。但是到第六幀的時(shí)候,只有兩個(gè)字節(jié),無(wú)法組成報(bào)文的4byte長(zhǎng)度頭,而代碼沒(méi)有針對(duì)此種情況做處理,于是報(bào)錯(cuò)。為何會(huì)出現(xiàn)這種情況:

  • TCP窗口的大小取決于當(dāng)前的網(wǎng)絡(luò)狀況、對(duì)端的緩沖大小等等因素,
  • TCP將這些都從底層屏蔽。開(kāi)發(fā)者無(wú)法從應(yīng)用層獲取這些信息。
  • 這就意味著,當(dāng)你在接收TCP數(shù)據(jù)流的時(shí)候無(wú)法知道當(dāng)前接收了
  • 有多少數(shù)據(jù)流,數(shù)據(jù)可能在任意一個(gè)比特位(seq)上。

第六幀的頭兩個(gè)字節(jié)是30,32正好和后面dump出來(lái)的30 31 2e 01中的30、31組成報(bào)文長(zhǎng)度

  • 30,32,30,31 (16進(jìn)制)
  • 48,50,48,49 (10進(jìn)制)
  • 0, 2, 0, 1 (字符串)
  • 2, 0, 1, 0 (整理成大端序)

這四個(gè)字節(jié)組合起來(lái)才是正常的報(bào)文頭,再經(jīng)過(guò)運(yùn)算得到整個(gè)Body的長(zhǎng)度。

第一次Mina解析的時(shí)候,后面的兩個(gè)30,31尚未放到buffer中,于是出錯(cuò):

  1. public ByteBuffer get(byte[] dst, int offset, int length) { 
  2.     checkBounds(offset, length, dst.length); 
  3.     // 此處拋出異常 
  4.     if (length > remaining()) 
  5.         throw new BufferUnderflowException(); 
  6.     int end = offset + length; 
  7.     for (int i = offset; i < end; i++) 
  8.         dst[i] = get(); 
  9.     return this; 

為何流量會(huì)飆升

解釋這個(gè)問(wèn)題前,我們先看一段Mina源碼:

  1. // if there is any data left that cannot be decoded, we store 
  2.         // it in a buffer in the session and next time this decoder is 
  3.         // invoked the session buffer gets appended to 
  4.         if (buf.hasRemaining()) { 
  5.             if (usingSessionBuffer && buf.isAutoExpand()) { 
  6.                 buf.compact(); 
  7.             } else { 
  8.                 storeRemainingInSession(buf, session); 
  9.             } 
  10.         } else { 
  11.             if (usingSessionBuffer) { 
  12.                 removeSessionBuffer(session); 
  13.             } 
  14.         } 

Mina框架為了解決這種問(wèn)題,會(huì)將這種尚未接收完全的包放到sessionBuffer里面,待解析完畢后把這份Buffer刪除。

如果代碼正確,對(duì)報(bào)文頭做了校驗(yàn),那么前5個(gè)報(bào)文的buffer將經(jīng)由這幾句代碼刪除,只留下最后兩個(gè)沒(méi)有被decode的兩字節(jié)。

  1. if (usingSessionBuffer && buf.isAutoExpand()) { 
  2.     buf.compact(); 
  3. else { 
  4.     storeRemainingInSession(buf, session); 

但是,由于decode的時(shí)候拋出了異常,沒(méi)有走到這段邏輯,所以前5個(gè)包還留在sessionBuffer中,下一次解包的時(shí)候,又會(huì)把這5個(gè)包給解析出來(lái),發(fā)送給后面的系統(tǒng)。

這也很好的解釋了為什么業(yè)務(wù)量激增,因?yàn)橄到y(tǒng)不停的發(fā)相同的5幀給后面系統(tǒng),導(dǎo)致監(jiān)控認(rèn)為業(yè)務(wù)量飆升。后查詢另一個(gè)系統(tǒng)的日志,發(fā)現(xiàn)一直同樣的5個(gè)序列號(hào)坐實(shí)了這個(gè)猜想。

完結(jié)了么?

NO,整個(gè)演繹還有第二段日志的推演

就是系統(tǒng)后來(lái)不停dump出的日志,再貼一次:

這個(gè)buffer應(yīng)該是Mina繼續(xù)接收外部系統(tǒng)的數(shù)據(jù)到buffer中導(dǎo)致,

Mina框架不停的接收數(shù)據(jù),直到buffer區(qū)滿,然后整個(gè)框架不停的解析出前5幀,到第6幀的時(shí)候,出錯(cuò),然后dump出其尚未被解幀的數(shù)據(jù)。這就是第二段日志。

最后的高潮

到現(xiàn)在推理似乎很完美了,但是我突然覺(jué)得不對(duì)(另一位同事也提出了相同的疑問(wèn)):

如果說(shuō)Mina接收到新的數(shù)據(jù)放到buffer中的話,第6幀的前兩個(gè)字節(jié)和后來(lái)發(fā)過(guò)來(lái)的若干字節(jié)不是又拼成了完整的一幀了么,那么后來(lái)為什么會(huì)一直出錯(cuò)了呢。如下圖所示:

丟失的兩字節(jié)

按照前面的推理,幀6的前兩個(gè)字節(jié)30、32肯定是丟了,那么怎么丟的呢?推理又陷入了困境,怎么辦?日志已經(jīng)幫不了筆者了,畢竟日志的表現(xiàn)都已解釋清楚。翻源碼吧:

Bug的源頭:

如果有問(wèn)題,肯定出在將數(shù)據(jù)放在Buffer中的環(huán)節(jié),于是筆者找到了這段代碼:

  1. if (appended) { 
  2.     buf.flip(); 
  3. else { 
  4.     // Reallocate the buffer if append operation failed due to 
  5.     // derivation or disabled auto-expansion. 
  6.     buf.flip(); 
  7.     ...... 

問(wèn)題出在buf.flip()上面,這段代碼最后調(diào)用的代碼是Java的Nio的Buffer的flip,代碼如下:

  1. public final Buffer flip() { 
  2.   // 下面這一句導(dǎo)致了最終的Bug現(xiàn)象 
  3.     limit = position; 
  4.     position = 0; 
  5.     mark = -1; 
  6.     return this; 

為什么呢?首先我們需要了解一下Nio Buffer的一些特點(diǎn):

同時(shí)當(dāng)Mina框架將數(shù)據(jù)(數(shù)據(jù)本身也是一個(gè)buffer)放到sessionBuffer的時(shí)候,也是將position到limit的數(shù)據(jù)放到新buffer中,

下面我們演繹一下第一次拋異常時(shí)候的flip前和flip后:

這樣就清楚了,在buf.flip()后,由于limit變成了原position的位置,這樣最后的兩個(gè)字節(jié)30,32就被無(wú)情的丟棄了。這樣整個(gè)sessionBuffer就變成:

為什么position在flip前沒(méi)有指向limit的位置,是由于在每次讀取前有一個(gè)checkBound的動(dòng)作,在檢查buffer數(shù)據(jù)不夠后,不會(huì)推進(jìn)position的位置,直接拋出異常:

  1. static void checkBounds(int offint len, int size) { // package-private 
  2.     if ((off | len | (off + len) | (size - (off + len))) < 0) 
  3.         throw new IndexOutOfBoundsException(); 
  4. 這樣所有的都 

這樣所有的都說(shuō)的通了,也完美了解釋了所有的現(xiàn)象。

正確代碼

  1. private boolean handeMessage(IoBuffer in,ProtocolDecoderOutput out){ 
  2.  int lenDes = 4; 
  3.  byte[] data = new byte[lenDes]; 
  4.  in.mark(); 
  5.     // 前4字節(jié)校驗(yàn)代碼 
  6.  if(in.remaining() < lenDes){ 
  7.   // 由于未消費(fèi)字節(jié),無(wú)需reset 
  8.   return false
  9.  } 
  10.  in.get(data,0,lenDes); 
  11.  int messageLen = decodeLength(data); 
  12.  if(in.remaining() < messageLen){ 
  13.   logger.warn("未接收完畢"); 
  14.   in.reset(); 
  15.   return false
  16.  }else
  17.   ...... 
  18.  } 
  19.   

為什么線上一直穩(wěn)定

隨著網(wǎng)絡(luò)不斷發(fā)展的今天,一些短小的幀很難出現(xiàn)中間斷開(kāi)的現(xiàn)象。而在一個(gè)好幾百字節(jié)的包中,前4個(gè)字節(jié)正好出錯(cuò)的概率那更是微乎其微。這樣就導(dǎo)致Bug難復(fù)現(xiàn),很難抓住。即使猜到是這里,也沒(méi)有足夠的證據(jù)來(lái)證明。

總結(jié)

Mina/Netty等各種網(wǎng)絡(luò)框架給我們處理TCP流問(wèn)題提供了非常好的解決方案。但是我們寫代碼的時(shí)候也不能掉以輕心,必須時(shí)刻以當(dāng)前可能讀不夠字節(jié)的心態(tài)去讀取buffer中的數(shù)據(jù),不然就可能遭重。

在此感謝給力的各位同事們,是你們的各種反駁讓我能夠找到最終的源頭,也讓我對(duì)網(wǎng)絡(luò)框架有了更加深刻的理解。

本文轉(zhuǎn)載自微信公眾號(hào)「解Bug之路」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系解Bug之路公眾號(hào)。

 

責(zé)任編輯:武曉燕 來(lái)源: 解Bug之路
相關(guān)推薦

2021-09-11 19:00:54

Intro元素MemoryCache

2021-05-20 10:02:50

系統(tǒng)Redis技巧

2019-10-10 15:40:17

redisbug數(shù)據(jù)庫(kù)

2021-06-10 06:59:34

Redis應(yīng)用API

2009-12-17 14:53:52

VS2008程序

2021-08-26 14:26:25

Java代碼集合

2024-02-04 08:26:38

線程池參數(shù)內(nèi)存

2024-06-28 10:01:04

2021-07-11 09:34:45

ArrayListLinkedList

2022-10-25 18:00:00

Redis事務(wù)生產(chǎn)事故

2022-06-21 11:24:05

多線程運(yùn)維

2011-08-18 13:49:32

筆記本技巧

2021-09-08 23:07:41

緩存java內(nèi)存

2024-09-05 08:07:55

2020-02-06 11:30:08

代碼JavaScript&&

2020-07-09 10:15:55

空值Bug語(yǔ)言

2021-05-31 10:08:44

工具腳本主機(jī)

2010-01-06 10:56:47

華為交換機(jī)使用

2013-11-07 10:24:31

Windows 8.1Bug

2022-09-12 06:33:15

Select多路復(fù)用
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)