一、問題現象
bigpipe是baidu公司內部的分布式傳輸系統,其伺服器模組broker採用非同步程式設計框架來實現,並大量使用了引用計數來管理物件資源的生命週期和釋放時機。在對broker模組進行壓力測試過程中,發現broker長時間執行後,記憶體占用逐步變大,出現了記憶體洩漏問題。
二、初步分析
針對近期broker的公升級改造點,確定broker中可能出現記憶體洩漏的物件。broker新增了監控功能,其中一項是對伺服器各個引數的監控統計,這必然對引數物件有讀取操作,每次操作都將引用計數「加一」,並在完成操作後「減一」。當前,引數物件有數個,需要確定是哪個引數物件洩漏了。
三、**&業務分析
1. 為證明之前的初步分析的結果,可能的方法有是:使用valgrind執行broker並啟動壓力程式復現可能的記憶體洩漏。但是,使用這種方法:
1) 由於記憶體洩漏的觸發條件並不簡單,可能導致復現週期很長,甚至無法復現同樣的記憶體洩漏;
2) 記憶體洩漏的物件放置在容器中,valgrind正常退出後不報告相關的記憶體洩漏;
經過另外的測試集群短時間的執行嘗試進行復現,果然valgrind報告未出現異常。
2. 分析現有擁有的條件:幸好,出現「記憶體洩漏」問題的broker程序仍然在執行中,真相就在這個程序內部。應該充分利用已有的現場,完成問題的定位。初步希望使用gdb除錯。
3. 挑戰:使用gdb attach pid的方法將會導致程序掛起,按broker的設計,一當配對另乙個主/從broker不互相傳送心跳, broker也將自動退出程式,退出後現場就無法儲存,這意味著使用gdb的機會只有一次。
4. 方案:利用gdb列印記憶體資訊並從資訊中觀察可能的記憶體洩漏點。
5. 步驟一:pmap -x 檢視記憶體資訊(如:pmap -x 24671);得到類似如下資訊,注意標記為anon的位置:
6. 步驟二:啟動gdb ./bin/broker並使用 attach 命令載入現有程序;例如上述程序號為24671,則使用:attach 24671;
7. 步驟三:使用setheight 0和 setlogging on開啟gdb日誌,日誌將儲存於gdb.txt檔案中;
8. 步驟四:使用x/a 列印出一段記憶體資訊,例如上述的anon為堆頭位址,占用了144508kb記憶體,則使用:x/18497024a0x000000000109d000;若命令列較多,可以在外圍編輯好命令列直接張貼至gdb命令列提示符中執行,或者將命令列寫到乙個文字檔案中,例如command.txt中,然後再gdb命令列提示符中使用sourcecommand.txt來執行檔案中的命令集合,下面是command.txt檔案的內容;
9. 步驟五:分析gdb.txt檔案中的資訊,gdb.txt中的內容如下:
gdb.txt中內容的說明和分析:第一列為當前記憶體位址,如0x22c2f00;第
二、三、四列分別為當前記憶體位址對應所儲存的值(使用十六進製制表示),以及gdb的debug的符號資訊,例如:0x10200d0<_ztvn7bigpipe15bigpipedienginee> 0x4600000001,分別表示:「前16位元組」、「符號資訊(注意有+16的偏移)」、「後16位元組」,但不是所有位址都會列印gdb的debug符號資訊,有時符號資訊顯示在第三列,有時顯示在第二列。上述這行記憶體位址0x22c2f00 儲存了bigpipe::bigpipediengine 類的生成的其中乙個物件的虛析構函式的函式指標,即虛函式表指標(vptr),其中位址0x10200d0附近記憶體儲存的應該是bigpipediengine類的虛函式表(vtbl),如下所示:
位址0x10200d0中的值是指向bigpipediengine類的析構函式的位址,即真正的析構函式**段頭位址0x53e2c6。可以從上述執行結果看到,位址0x53e2c6的「符號資訊」是析構函式名,其彙編命令為push。因此,可以知道最初看到的0x22c2f00位址是物件的乙個虛析構函式指標,並且有「符號資訊」bigpipediengine顯示出來,可以根據這種資訊確定出這個類(帶虛析構函式的類)生成了多少個例項,然後根據排出來的例項個數做進一步判斷。
因此,對gdb.txt排序並做適當處理獲得符號(類名/函式名稱)出現的次數的列表。例如將上述內容過濾出帶尖括號的「符號資訊」部分並按出現次數排序,可以使用類似如下命令,catgdb.txt |grep "''' |sort |uniq -c|sort -rn > result.txt,過濾出專案相關的變數字首(如bmq、bigpipe、bmeta等)cat result.txt|grep -p"bmq|bigpipe|bigpipe|bmeta"|grep "_ztv" > result2.txt,獲得類似如下的列表:
10. 然後找出和本工程專案相關的且出現次數最多的為cconnect物件;判斷出可能洩漏的物件後,還需要定位在非同步框架下,哪個引用計數出現了問題導致cconnect物件無法正常減一併得到釋放。
11. 經過追查新增的「監控」功能與cconnect相關的**,如下。
四、真相大白
檢視atomic_add函式的實現(如下),可以得知,返回值是自增(減)之前的值,而由於函式名稱atomic_add並未特別的表現出這樣的含義,導致呼叫者誤用了這個函式,認為是自增之後的值,最終引用計數誤認為不為0,導致未執行_free操作,進而導致記憶體洩漏。通常,和__sync_fetch_and_add對應的函式還有__sync_add _and_fetch,這兩者的區別在於「先獲得值再加」還是「先加值在獲取」。
五、解決方案
因此,程式的改進如下:
六、總結
1. 由於非同步框架實現的程式對問題定位跟蹤難度較高,需要綜合:日誌,gdb,pmap等手段完成問題復現和定位;
2. valgrind檢測記憶體洩漏並不是唯一的方法,且具有一定的侷限性;
3. 函式名稱定義盡量直觀表明函式功能,能夠避免呼叫方的一部分錯誤;
4. 應當仔細閱讀庫函式的說明文件,了解使用方法;
利用mtrace檢查記憶體洩漏
mtrace是linux下檢查記憶體洩漏的工具之一。它實際上是通過一對函式來檢測一段 是否存在記憶體洩漏 mtrace 與muntrace 它們的原型如下 void mtrace void void muntrace void 標頭檔案為 mcheck.h 用法 1.首先確定需要檢測那一段 2.然後...
fork 記憶體洩漏 程序
1.系統呼叫 fork 複製程序 pid t fork void pid t int pid 程序的編號 id識別符號 父子程序併發執行 並行 一種特殊的併發,不是交替,我在執行的同時,你也在執行。兩個處理器 併發執行 乙個處理器。在一段時間內交替執行,從長遠角度看是同時執行。fork 先複製pcb...
Linux程序記憶體分析和記憶體洩漏定位
在linux產品開發過程中,通常需要注意系統記憶體使用量,和評估單一程序的記憶體使用情況,便於我們選取合適的機器配置,來部署我們的產品。linux本身提供了一些工具方便我們達成這些需求,檢視程序實時資源top工具,更詳細的程序記憶體堆疊情況,pmap工具,linux程序執行時狀態資訊也會儲存在pro...