記一次調(diào)試python內(nèi)存泄露的問題

這兩天由于公司需要, 自己編寫了一個用于接收dicom文件(醫(yī)學(xué)圖像文件)的server. 經(jīng)過各種coding-debuging-coding-debuging之后, 終于上線了, 上線后心里美滋滋的, 一切正常.

第二天一上班, 負(fù)責(zé)人和我說接收太慢了, 卡的要死. 我想難道是python本身的問題?(程序員本征思維)我好奇的打開了終端輸入

ps -aux | grep python

找到進(jìn)程id



即 21610

我這里還沒傳幾張圖片就到78m了, 看來是內(nèi)存問題. 其實生產(chǎn)環(huán)境占用更多, 因為生產(chǎn)環(huán)境保密所以只能在測試環(huán)境測試比較少的數(shù)據(jù), 生產(chǎn)環(huán)境曾一度上升到3.7g的內(nèi)存占用.

這樣果斷不行啊. 我發(fā)現(xiàn)有新的文件上傳之后內(nèi)存占用就會增大, 初步斷定是dicom文件相關(guān)對象占用的內(nèi)存. 現(xiàn)在的首要工作就是找到一個能進(jìn)行內(nèi)存泄露的調(diào)試工具了.

說道這里可能大家會有疑問, python作為動態(tài)類型語言同時擁有垃圾回收機怎么會有內(nèi)存泄露? 其實也有可能出現(xiàn)內(nèi)存泄露的情況, 有如下幾種:

  1. 對象一直被全局變量所引用, 全局變量生命周期長.
  2. 垃圾回收機被禁用或者設(shè)置成debug狀態(tài), 垃圾回收的內(nèi)存不會被釋放.
  3. 也是非常罕見的內(nèi)存泄露的方式就是今天遇到的問題, 我周旋這個問題兩天才debug出來, 現(xiàn)在分享給大家.客官請您繼續(xù)往下看

說到查看python內(nèi)存泄露的工具, 其實有挺多, 現(xiàn)在簡短介紹一下

  • gc: python 內(nèi)置模塊, 函數(shù)少功能基本, 使用簡單, 作為python開發(fā)者里邊的內(nèi)容必須過一遍
  • objgraph: 可以繪制對象引用圖, 對于對象種類較少, 結(jié)構(gòu)比較簡單的程序適用, 我這個一個庫套一個庫, 內(nèi)存還用的這么多,
  • guppy: 可以對堆里邊的對象進(jìn)行統(tǒng)計, 算是比較實用
  • pympler: 可以統(tǒng)計內(nèi)存里邊各種類型的使用, 獲取對象的大小

上邊這些雖然有用但是總是搞不到點子上, 上邊這些都需要改我的源程序, 比較費勁, 線上的代碼不是說改就能改的, 而且他們功能也都比較弱, 后來發(fā)現(xiàn)兩個強大的工具:

  • tracemalloc: 究極強, 可以直接看到哪個(哪些)對象占用了最大的空間, 這些對象是誰, 調(diào)用棧是啥樣的, python3直接內(nèi)置, python2如果安裝的話需要編譯
  • pyrasite: 牛逼的第三方庫, 可以滲透進(jìn)入正在運行的python進(jìn)程動態(tài)修改里邊的數(shù)據(jù)和代碼(其實修改代碼就是通過修改數(shù)據(jù)實現(xiàn))

我開始的時候非常想用tracemalloc, 可是對python2特別不友好, 需要重新編譯python, 而且只能用python2.7.8編譯, 編譯好了也不容易嵌入到虛擬環(huán)境中, 頭大, 果斷換第二個.

: pyrasite使用之前需要在root用戶下運行命令 echo 0 > /proc/sys/kernel/yama/ptrace_scope后才能正常使用

pyrasite里邊有一個工具叫pyrasite-memory-viewer, 功能和guppy差不多, 不過可以對內(nèi)存使用統(tǒng)計和對象之間的引用關(guān)系進(jìn)行快照保存, 很易用也很強大.運行

pyrasite-memory-viewer <pid>

可以看到占用內(nèi)存最多的是DicomFileLike這種類型的對象.已經(jīng)達(dá)到上萬個, 這是不能忍受的.
就目前來看可能會有上邊說的兩種內(nèi)存泄露原因?qū)е虏荒芑厥者@個對象.打開pyrasite-shell

pyrasite-shell <pid>

我先通過

gc.isenabled()

判斷gc是否在工作, 結(jié)果發(fā)現(xiàn)是True, 也就是正常工作的, 而且使用gc.setdebug(gc.STATUS)設(shè)置gc為debug模式, 然后gc.collect()進(jìn)行垃圾回收發(fā)現(xiàn)并沒有更多內(nèi)存釋放,則否認(rèn)了第二種泄露的可能.
現(xiàn)在來看gc.garbage中不能被釋放的對象, 讓我來檢查一下是否有全局變量指向它們(這里極有可能是一個列表或者是一個字典)

gc.garbage 可以看到被塞滿了各種DicomFileLike對象


所以我們的目的就是先找到一個對象然后一級一級的向上尋找相互的引用.

>>> d = gc.garbage[-1]  # 隨便找一個DicomFileLike對象
>>> d
<dicom.filebase.DicomFileLike object at 0x7f362c305390>
>>> objs = gc.get_referrers(d)
>>> len(objs)
8
>>> objs.remove(gc.garbage)
>>> objs.remove(locals())
>>> objs[0]
# 這里的輸出是一個大字典, 包括了builtins, 應(yīng)該是<pid>下的locals().

>>> objs[1]
<bound method DicomFileLike.write_leUS of <dicom.filebase.DicomFileLike object at 0x7f362c305390>>

>>> objs[2]
<bound method DicomFileLike.read_leUL of <dicom.filebase.DicomFileLike object at 0x7f362c305390>>

>>> objs[3]
<bound method DicomFileLike.read_leUS of <dicom.filebase.DicomFileLike object at 0x7f362c305390>>

>>> objs[4]
<bound method DicomFileLike.write_leUL of <dicom.filebase.DicomFileLike object at 0x7f362c305390>>

>>> objs[5]
<bound method DicomFileLike.read_le_tag of <dicom.filebase.DicomFileLike object at 0x7f362c305390>>

到這里發(fā)現(xiàn)其實沒有更多的全局變量指向這個d了, 而且發(fā)現(xiàn)所以有的方法的對象地址和d是相同的, 說明了這個對象其實是自循環(huán)引用的.

那么python不可能不支持循環(huán)引用對象的回收吧? 跟著這個問題我查了一下stackoverflow

Does Python GC deal with reference-cycles like this?

這個問題的第一個回答介紹的很清楚了, 如果用戶不自定類的__del__方法, gc可以回收帶有自引用的對象, 但是你自己實現(xiàn)了__del__方法就不行了.

這就是python內(nèi)存泄露的第三個可能.

回頭看DicomFileLike的源碼, 果然在__init__函數(shù)上方定義了一個__del__函數(shù), 我這里使用了一個猴子補丁刪除了這個方法, 內(nèi)存泄露的問題就得以解決了.

def monkey_patch_dicom():
    """
    修正dicom中DicomFileLike對象不釋放內(nèi)存問題
    """
    del dicom.filebase.DicomIO.__del__

總結(jié)

到這里整個調(diào)試過程就結(jié)束了, 然而實際上過程中做了很多曲折的工作, 在pyrasite中會找到幾個引用DicomFileLike對象的object, 比較不容易辨別, 最開始我以為是某個全局的對象引用的DicomFileLike, 比如是列表什么的, 后來發(fā)現(xiàn)其實是locals()和globals()字典, 如果使用pyrasite-memory-viewer保存下來的數(shù)據(jù)會發(fā)現(xiàn)有一個大列表指向所有沒有回收的DicomFileLike對象, 捯飭半天發(fā)現(xiàn)其實是gc.garbage, 好囧, 曾讓我一度懷疑是第一種泄露方式, 但是怎么找這個對象都沒有找到. 其中還有幾次看到線程達(dá)到140+, 后來發(fā)現(xiàn)其實和線程一點關(guān)系沒有, 線程維持在這個數(shù)目上邊很穩(wěn)定.

在這個過程中用到的其他幾個hack的技巧有:

  • 查看 進(jìn)程的線程數(shù)量

    ps -o nlwp <pid>
    
  • 根據(jù)對象的id/address動態(tài)獲取對象

    import ctypes
    obj = ctypes.cast(<addr_or_id>, ctypes.py_object).value
    
  • 查看垃圾回收的日志

    gc.set_debug(...)
    
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市蒿囤,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌惠拭,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件付燥,死亡現(xiàn)場離奇詭異粗井,居然都是意外死亡舔糖,警方通過查閱死者的電腦和手機娱两,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來金吗,“玉大人十兢,你說我怎么就攤上這事∫∶恚” “怎么了旱物?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長跟匆。 經(jīng)常有香客問我异袄,道長,這世上最難降的妖魔是什么玛臂? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任烤蜕,我火速辦了婚禮,結(jié)果婚禮上迹冤,老公的妹妹穿的比我還像新娘讽营。我一直安慰自己,他們只是感情好泡徙,可當(dāng)我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布橱鹏。 她就那樣靜靜地躺著,像睡著了一般堪藐。 火紅的嫁衣襯著肌膚如雪莉兰。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天礁竞,我揣著相機與錄音糖荒,去河邊找鬼。 笑死模捂,一個胖子當(dāng)著我的面吹牛捶朵,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播狂男,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼综看,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了岖食?” 一聲冷哼從身側(cè)響起红碑,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎泡垃,沒想到半個月后析珊,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡兔毙,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年唾琼,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片澎剥。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡锡溯,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出哑姚,到底是詐尸還是另有隱情祭饭,我是刑警寧澤,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布叙量,位于F島的核電站倡蝙,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏绞佩。R本人自食惡果不足惜寺鸥,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一猪钮、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧胆建,春花似錦烤低、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至凉驻,卻和暖如春腻要,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背涝登。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工雄家, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人缀拭。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓咳短,卻偏偏與公主長得像,于是被迫代替她去往敵國和親蛛淋。 傳聞我的和親對象是個殘疾皇子咙好,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內(nèi)容