每周一篇又來了。這次主要介紹netio的buffer管理器。 首先buffer管理是每一個網絡層不可回避的問題。怎么高效的使用buffer是很關鍵的問題。這里主要介紹下我們的netio是怎么處理。說實話 這是我見過比較蛋疼buffer管理。 反正我是看了好幾天 才看明白的。
最近看了下Qcon2016的視頻.里面很多大牛介紹分布式平台。 感覺特別牛逼~~。 感覺我們的分布式相比他們的這些還是簡陋了點。感興趣的同學可以去看看
http://daxue.qq.com/content/special/id/20
1.1 我們先看下 一次系統調用recv就能收到完整包的情況
1)首先通過系統調用函數recv 會每次把從TCP讀到的數據放到
m_achRecvBuf[TPT_RECV_BUF_LEN];
這個buf大小為128*1024
2、判斷包頭。
先判斷是否是0x5a5a
然后解析包頭判斷 需要發送過來的總長度 如果大於1024*1024就報錯。
1024*1024是在初始化的時候申請的大小。
我們的一個最大請求包已經限定為1M

如果發現tcp一次就能收到完整的包。
netio並不會使用我們字節的buf管理器。
m_pSink->OnRecv而是直接丟給netio的app類去處理。
然后等netio中app類對包做了具體的處理后。
網絡層 發現處理完以后就會直接 重新跳到while 循環中等待新事件
int CNetHandleMng::_RetrievePkgData(int nHandle,char* pRcvBuf,int nBufLen) { ...... //當前數據包已經讀取完成 m_pSink->OnRecv(nHandle,pRcvBuf+TPT_HEAD_LEN,dwPkgLen); return (dwPkgLen + TPT_HEAD_LEN); }
int CNetHandleMng::OnRecv(int nHandle,char* pRcvBuf,int nBufLen) { stConn* pConn = _GetConn(nHandle); if( NULL == pConn ) { std::stringstream oss; oss<<"reactor report recv data for connection handle"<<nHandle<<" but we cann't found the connection data"<<std::endl; m_pSink->ReportTptError(__FILE__,__LINE__,__func__,oss.str().c_str()); return 0; } int nReadLen =0; if( 0 == pConn->m_pRcvBuf->m_nDataLen ) { nReadLen = _RetrievePkgsData(nHandle,pRcvBuf,nBufLen); if( nReadLen < 0 ) return nReadLen;//reactor層會自動關閉連接 if( nReadLen >= nBufLen ) return 0;//數據已經處理完畢 ..... }
我們發現在一次recv能收完整個數據包的時候。平台沒用字節的buf管理器。而是直接就給netio app類來處理了
1.2 我們先看下 一次系統調用recv收不完包的情況。
5.2.1 我們先分析下一個具體的例子 然后再慢慢的歸納和總結
a)為什么是256個指針?
初始化的時候
CNetioApp::CNetioApp():CNetMsgqSvr(4096*5,1024*1024,4*1024)
buffer管理器中 最小的一個buf大小是4*1024
(1024*1024+4*1024 -1 ) / (1024*4) = 256
然后分配256個大小的二維指針。
所以初始化的時候 會設置一個大小為256的二維指針。注意這里只是創建二維指針。當時並沒有給每個指針指向的對象分配空間。
b) 當客戶端首次connet的時候。會繼續初始化一些信息
當客戶端connet請求來臨時候。會去buffer管理器取一塊buffer。
默認情況下。都是取p[0]里的buffer。
當buffer管理器 發現p[0]為空的時候。 會去創建10個buffer 。這里10是寫死的。 由於是p[0]是第一行。那么每個buffer的大小是4096.
這10個buffer 是一個鏈表。 index=0是最先創建的。index=9是最后創建的。
如上圖。 index=9被拿出去了 。但其實這個時候並沒有數據過來。
這個不管。被connet信息結構體指向的buffer。我們都認為是在使用中。
這個時候p[0] 就會跳到index=8.
注意 二維指針 永遠是指向未被使用的buffer。這很重要。如果沒有空間。會繼續創建buffer
c) 第一次recv 16384數據
這里發現收到的16384個字節 大於4096個字節。
則buffer管理器。會在p[3] 這里申請10個buffer塊。
每塊 1024*4*4=16384 剛好 放下recv的16384數據
因為不用p[0]的buffer塊。 則先回退p[0]的指向。從index=8 到index=9
然后讓connet信息塊 重新指向p[3]的index=9
前面說了 二維指針一定要指向未被使用的buffer。 所有p[3] 指向index=8
同時在m_nDatalen里面記錄 已經保存的字節數
這個時候還沒有收完 需要繼續收數據

d) 第二次recv 16384數據
注意 第二次也收到了16384.
那么第二次的16384 + 上次的16384 = 32768
這個時候p[3] 這一列的buffer放不下。 需要重新創建buffer
這個時候在p[7] 這一列上創建 4*1024*8 =32768 剛好放下所有數據
這個時候在p[7] 創建10個buffer 。 每個buffer為4*1024*8
接着還是要歸還p[3] buffer的使用權。這個時候吧p[3]的指針指向index = 9 同時把 index=9里面的m_nDatalen設置為0.
這樣就表示p[3]的index=9被 釋放了。 但是其實index=9還是有內容的並沒有清除。
接着我們把累加的數據 放到p[7]的index=9里面

e)
后面都是類似的邏輯。 歸還空間。然后申請新空間
我看總共131158個字節的內容 recv 6次。
buffer管理器 替換了包括最開始初始化的的buffer總共花了7次 才找到合適的buffer來存放內容
p[32] 的buffer大小 為4*1024*33=135168

1.2.2 . 正常情況下的buffer總大小
在netio包了一段時間后。假如各種包的大小都存在。那么最后會怎么樣~~。
這256個指針 都會被創建buffer。 沒一列的buffer大小是 4*1024*行數。比如第一行就是4*1024*1.
最后一行就是4*1024*256.
而且被創建的buffer不會被釋放。我們來計算下這個總的buffer會多大。
4*1024*(1+2+3...+256)=134742016 134742016 1048576
134742016 / (1024 * 1024) = 128 M
大概128兆。但是 這只是並發請求不高的情況下。我們來看下並發請求高的情況下會怎么樣

1.2.3 . 高並發場景下 buffer的總大小
我們假設並發來了20個請求。為了使分析簡單。我們就認為。每個請求數據都在4*1024以內。
如下圖 用戶p[0]的10個buffer以后。
p[0] 這個時候是指向了NULL的。
但是這個時候還有請求該怎么幫。
繼續分配
這個時候再分配 10個buffer

如下圖 又重新分配了10個buffer。 跟在0的后面。 在來的請求就是在后面的10個buffer中分配。
代碼中是 每次網絡層向buffer管理器申請buffer的時候。
會去查看 二維指針是否為空。不為空則把空間給出來存數據。
如果為空。則會申請10個內存
所有看到這。當並發請求很大的時候。這個buffer會突增大到一個很恐怖的數字。
而且由於 創建后的空間不會被刪除。會一直維持一個很高的內存占用

1.2.4 buffer的釋放。
我們以下圖為例子。
請求 7、4、8 先后是否空間。
那么p[0] 先是指向 index=3 然后指向index=6 最后指向index=2
那么p[0] 指向的其實是 沒有被使用的空間的 鏈表頭。
p[0] ->index2->index6->index3
下次又有新請求來的時候。 則把index2分配給新請求使用

總結下:
1)netio的buffer初看還是很麻煩的。看了2、3天才看明白。主要是實現的思想還是有點復雜。但是個人感覺看下來並沒有什么特別驚艷的地方。實現上感覺有點像google的tcmall。
2)申請不釋放的好處就是不會產生大量內存碎片。
3)但是高並發場景下回內存爆增。且不會下去。
4)還有針對一個大包。需要多次recv。那么buffer管理器會不停的替換buffer來存數據。而不是解析包頭。確定包的大小。然后指定一個剛好符合的buffer。然后每次recv數據都放在這個buffer里。而不用不停的替換buffer.