• 當前位置:聯(lián)升科技 > 技術(shù)資訊 > 開(kāi)發(fā)技術(shù) >

    一個(gè)耗時(shí)4小時(shí)的內存泄漏問(wèn)題

    2020-10-23    作者:佚名    來(lái)源:C語(yǔ)言與C++編程    閱讀: 次

    上周像往常一樣例行檢查線(xiàn)上機器性能,突然發(fā)現一個(gè)服務(wù)的內存使用率是這樣的:
    很顯然該服務(wù)存在內存泄漏問(wèn)題,趕緊排查問(wèn)題。
    問(wèn)題排查
    首先確定內存泄漏問(wèn)題出現的時(shí)間,發(fā)現在該時(shí)間點(diǎn)的上線(xiàn)有兩次代碼提交,其中一個(gè)就是我的。于是立刻排查這兩次代碼的改動(dòng),確定了另一個(gè)同事的代碼不可能會(huì )有內存問(wèn)題后(因為另一個(gè)同事的上線(xiàn)僅僅修改了配置)我知道肯定是自己的代碼出現了問(wèn)題。
    確定了問(wèn)題所在后趕緊把自己的代碼回滾掉,接下來(lái)就可以放心debug了。
    Debug
    什么是內存泄漏?
    簡(jiǎn)單的講就是程序員申請的內存在使用完后沒(méi)有還給操作系統,由于筆者使用的是C++語(yǔ)言,因此內存泄漏一般是這樣的:
    obj* o = new obj();  
    ...  
    // 使用完obj后沒(méi)有delete掉 
    肯定有什么地方申請了內存后沒(méi)有調用delete釋放內存。
    在這里介紹一下筆者的代碼改動(dòng),我的任務(wù)其實(shí)是重構一段代碼,把這段代碼并行化。也就是舊的邏輯是在一個(gè)線(xiàn)程中串行執行的,現在我要把這段邏輯放到兩個(gè)線(xiàn)程中并行執行,這是最讓人頭疼的任務(wù)之一,并行化改造是比較容易出bug的。
    接下來(lái)梳理了一遍中所有內存的申請和釋放,這其中包括:
     使用new/delete分配釋放的內存
     使用內存池分配釋放的內存
    仔細梳理一遍后沒(méi)有發(fā)現任何問(wèn)題,該釋放的內存都已經(jīng)釋放掉了,這時(shí)筆者已經(jīng)開(kāi)始懷疑人生了 :) ,很顯然還有一段沒(méi)有注意到的地方出現了問(wèn)題,這是必然的,雖然知道問(wèn)題必然出現在改動(dòng)的這些代碼里但是我并不能確定出現的位置。
    沒(méi)有辦法,到這里基本上已經(jīng)要放棄自己人肉debug了,想利用一些內存檢測工具來(lái)幫助自己確定問(wèn)題。
    常見(jiàn)的內存泄漏檢測工具包括valgrind、gperftools等,valgrind的好處在于無(wú)需重新編譯代碼即可進(jìn)行內存檢測,但是缺點(diǎn)是會(huì )使得程序運行非常緩慢,官方文檔給的說(shuō)法是會(huì )比正常的程序運行慢20-30倍;gperftools則需要重新編譯可執行程序。這些工具需要下載安裝測試,其中還涉及到申請機器權限等問(wèn)題,筆者覺(jué)得還是比較麻煩,況且這個(gè)問(wèn)題也不是大海撈針一樣,問(wèn)題肯定出在了并行化的這段代碼中。
    到這里我決定再換一個(gè)思路來(lái)排查問(wèn)題,既然代碼重構后開(kāi)始并行執行,那么出現問(wèn)題大概率是因為多線(xiàn)程問(wèn)題,遇到多線(xiàn)程問(wèn)題首先重點(diǎn)排查的就是線(xiàn)程間的共享數據。
    多線(xiàn)程問(wèn)題的關(guān)鍵——共享數據
    我們知道如果線(xiàn)程之間沒(méi)有共享數據那么就不會(huì )有線(xiàn)程安全問(wèn)題,我們使用的鎖、信號量、條件變量等其實(shí)都是用來(lái)保護共享數據的,比如鎖通常是用來(lái)包括臨界區的,臨界區中的代碼操作的就是線(xiàn)程共享數據;信號量使用的一個(gè)經(jīng)典場(chǎng)景就是生產(chǎn)者消費者問(wèn)題,生產(chǎn)者線(xiàn)程以及消費者線(xiàn)程都會(huì )操作同一個(gè)隊列,這里的隊列就是共享數據。
    沿著(zhù)這個(gè)思路開(kāi)始找在兩個(gè)線(xiàn)程中都使用到的共享數據,果不其然,在一個(gè)角落中發(fā)現了這樣一段代碼:
    auto* pb = global->mutable_obj(); 
    這是分配protobuf對象的一段代碼,protobuf是Google開(kāi)發(fā)是一種類(lèi)似于JSON、XML的技術(shù),因此常用于網(wǎng)絡(luò )通信和數據交換等場(chǎng)景,比如RPC等。
    如果你不了解protobuf也沒(méi)有關(guān)系,實(shí)際上上面的這段代碼的要做的事情是這樣的:
    if (global->obj == NULL) {  
      global->obj = new obj();  
    }  
    return global->obj; 
    值得注意的是這段代碼現在會(huì )在兩個(gè)線(xiàn)程中執行,顯然問(wèn)題就出現在了這里。
    那么問(wèn)題是怎么出現的呢?
    我們假設有兩個(gè)線(xiàn)程,線(xiàn)程A和線(xiàn)程B,當這樣一段代碼在線(xiàn)程AB中同時(shí)執行時(shí)可能會(huì )有以下場(chǎng)景:
     線(xiàn)程A拿到global->obj并檢測到此時(shí)的global->obj為空,因此決定為其分配內存,但不巧的是此時(shí)發(fā)生線(xiàn)程切換,線(xiàn)程A在為global->obj分配內存前被暫停運行,如下所示: 
    if (global->obj == NULL) {  
        <------- 線(xiàn)程切換,線(xiàn)程A被暫停執行   
        global->obj = new obj();  
    }  
    return global->obj; 
     線(xiàn)程A被暫停運行后線(xiàn)程B開(kāi)始執行,這段代碼同樣會(huì )在線(xiàn)程B中執行一遍,因此線(xiàn)程B會(huì )首先檢查global->obj發(fā)現為空,因此為global->obj分配內存,分配完內存后發(fā)生線(xiàn)程切換,線(xiàn)程B被暫停運行,如下所示: 
    if (global->obj == NULL) {  
        global->obj = new obj();  
        <------- 線(xiàn)程切換,線(xiàn)程B被暫停執行   
    }  
    return global->obj; 
    線(xiàn)程B被暫停運行后調度器決定重新運行線(xiàn)程A,此時(shí)線(xiàn)程A開(kāi)始從被中斷的地方繼續運行,還記得線(xiàn)程A是從哪里被中斷的嗎,沒(méi)錯,就是在為global->obj分配內存前被中斷的,此時(shí)線(xiàn)程A繼續運行,也就是說(shuō)global->obj = new obj()這段代碼又被執行了一次,雖然線(xiàn)程B已經(jīng)為global->obj分配了內存。
    Oops,典型的內存泄漏,線(xiàn)程B分配的內存再也無(wú)法被正常釋放掉了。
    至此,我們已經(jīng)找到了問(wèn)題的原因,罪魁禍首就是共享數據,關(guān)鍵的一點(diǎn)是要意識到你的線(xiàn)程會(huì )隨時(shí)被中斷執行,CPU會(huì )隨時(shí)切換到其它線(xiàn)程。
    代碼修復也非常簡(jiǎn)單,再新增一個(gè)變量,兩個(gè)線(xiàn)程不在使用共享數據,到這里問(wèn)題就解決了,從發(fā)現問(wèn)題到完成修復耗時(shí)大概4小時(shí)。
    經(jīng)驗教訓
    代碼的并行化重構是一件非常棘手的任務(wù),很容易出現線(xiàn)程安全問(wèn)題,解決線(xiàn)程安全問(wèn)題首先要考慮的不是要不要加鎖,而是多個(gè)線(xiàn)程是否真的有必要使用共享數據,沒(méi)有必要的話(huà)多個(gè)線(xiàn)程操作私有數據根本就不會(huì )出現線(xiàn)程安全問(wèn)題。
    當出現線(xiàn)程安全問(wèn)題時(shí),第一時(shí)間重點(diǎn)排查線(xiàn)程使用的共享數據。
    內存泄漏檢測工具
    雖然這些沒(méi)有使用檢測工具全靠人肉debug其實(shí)還是因為問(wèn)題排查范圍比較小,如果我們根本就不知道問(wèn)題出現在了那次代碼改動(dòng)那么檢測工具就非常重要了,在這里簡(jiǎn)單介紹一下valgrind的使用,詳細的介紹請參考官方文檔。
    假設有這樣一段問(wèn)題代碼:
    #include <stdlib.h>  
    void f(void)    
    {  
       int* x = malloc(10 * sizeof(int));  
       x[10] = 0;        // 問(wèn)題1: 越界  
    }                    // 問(wèn)題2: 內存泄漏,x沒(méi)有被釋放掉   
    int main()   
    {  
       f();  
       return 0;  
    這段代碼中有兩個(gè)問(wèn)題:一個(gè)是數據的越界訪(fǎng)問(wèn);另一個(gè)是內存泄漏。將該程序編譯為myprog。
    接下來(lái)使用valgrind來(lái)檢查該程序,使用以下命令:
    valgrind --leak-check=yes myprog 
    運行完成后valgrind會(huì )給出檢測報告,關(guān)于程序越界訪(fǎng)問(wèn)會(huì )給出這樣的輸出:
    ==19182== Invalid write of size 4  
    ==19182==    at 0x804838F: f (example.c:6)  
    ==19182==    by 0x80483AB: main (example.c:11)  
    ==19182==  Address 0x1BA45050 is 0 bytes after a block of size 40 alloc'd  
    ==19182==    at 0x1B8FF5CD: malloc (vg_replace_malloc.c:130) 
    ==19182==    by 0x8048385: f (example.c:5)  
    ==19182==    by 0x80483AB: main (example.c:11) 
    第一行告訴你代碼中存在Invalid write,也就是無(wú)效的寫(xiě),并給出了問(wèn)題出現的位置。
    關(guān)于內存泄漏問(wèn)題會(huì )給出這樣的輸出:
    ==19182== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1  
    ==19182==    at 0x1B8FF5CD: malloc (vg_replace_malloc.c:130)  
    ==19182==    by 0x8048385: f (example.c:5)  
    ==19182==    by 0x80483AB: main (example.c:11) 
    這里第一行報告了內存"definitely lost",也就是說(shuō)一定會(huì )存在內存泄漏,并給出了問(wèn)題出現的位置。
    實(shí)際上除了"definitely lost",valgrind還會(huì )給出"probably lost"的報告,這兩種報告的含義是這樣的:
     "definitely lost":你的程序一定存在內存泄漏問(wèn)題,修復。
     "probably lost":你的程序看起來(lái)像是有內存泄漏,有可能你在使用指針完成一些特定操作,因此不一定100%存在問(wèn)題。
    總結
    編寫(xiě)正確的多線(xiàn)程代碼從來(lái)不是一件容易的事情,線(xiàn)程安全問(wèn)題的根源在于共享資源,因此在使用共享資源前務(wù)必確認我們一定要用共享資源嗎?


    相關(guān)文章

    我們很樂(lè )意傾聽(tīng)您的聲音!
    即刻與我們取得聯(lián)絡(luò )
    成為日后肩并肩合作的伙伴。

    行業(yè)資訊

    聯(lián)系我們

    13387904606

    地址:新余市仙女湖區仙女湖大道萬(wàn)商紅A2棟

    手機:13755589003
    QQ:122322500
    微信號:13755589003

    江西新余網(wǎng)站設計_小程序制作_OA系統開(kāi)發(fā)_企業(yè)ERP管理系統_app開(kāi)發(fā)-新余聯(lián)升網(wǎng)絡(luò )科技有限公司 贛ICP備19013599號-1   贛公網(wǎng)安備 36050202000267號   

    微信二維碼
    色噜噜狠狠一区二区三区果冻|欧美亚洲日本国产一区|国产精品无码在线观看|午夜视频在线观看一区|日韩少妇一区二区无码|伊人亚洲日韩欧美一区二区|国产在线码观看清码视频