1. 程式人生 > >記一次調試python內存泄露的問題

記一次調試python內存泄露的問題

enable ini package 包括 bsp 應該 占用內存 ans blank

轉載:http://www.jianshu.com/p/2d06a1a01cc3

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

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

ps -aux | grep python

找到進程id


技術分享圖片

即 21610

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

這樣果斷不行啊. 我發現有新的文件上傳之後內存占用就會增大, 初步斷定是dicom文件相關對象占用的內存. 現在的首要工作就是找到一個能進行內存泄露的調試工具了.

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

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

說到查看python內存泄露的工具, 其實有挺多, 現在簡短介紹一下

  • gc: python 內置模塊, 函數少功能基本, 使用簡單, 作為python開發者裏邊的內容必須過一遍
  • objgraph: 可以繪制對象引用圖, 對於對象種類較少, 結構比較簡單的程序適用, 我這個一個庫套一個庫, 內存還用的這麽多,
  • guppy: 可以對堆裏邊的對象進行統計, 算是比較實用
  • pympler: 可以統計內存裏邊各種類型的使用, 獲取對象的大小

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

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

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

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

pyrasite裏邊有一個工具叫pyrasite-memory-viewer, 功能和guppy差不多, 不過可以對內存使用統計和對象之間的引用關系進行快照保存, 很易用也很強大.運行

pyrasite-memory-viewer <pid>

技術分享圖片

可以看到占用內存最多的是DicomFileLike這種類型的對象.已經達到上萬個, 這是不能忍受的.
就目前來看可能會有上邊說的兩種內存泄露原因導致不能回收這個對象.打開pyrasite-shell

pyrasite-shell <pid>

我先通過

gc.isenabled()

判斷gc是否在工作, 結果發現是True, 也就是正常工作的, 而且使用gc.setdebug(gc.STATUS)設置gc為debug模式, 然後gc.collect()進行垃圾回收發現並沒有更多內存釋放,則否認了第二種泄露的可能.
現在來看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, 應該是<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>>

到這裏發現其實沒有更多的全局變量指向這個d了, 而且發現所以有的方法的對象地址和d是相同的, 說明了這個對象其實是自循環引用的.

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

Does Python GC deal with reference-cycles like this?

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

這就是python內存泄露的第三個可能.

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

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

總結

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

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

  • 查看 進程的線程數量

    ps -o nlwp <pid>
    
  • 根據對象的id/address動態獲取對象

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

    gc.set_debug(...)


作者:weidwonder
鏈接:http://www.jianshu.com/p/2d06a1a01cc3
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請註明出處。

記一次調試python內存泄露的問題