歡迎光臨
每天分享高質量文章

記一次除錯python記憶體泄露的問題

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

第二天一上班, 負責人和我說接收太慢了, 卡的要死. 我想難道是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

可以看到占用記憶體最多的是DicomFileLike這種型別的物件.已經達到上萬個, 這是不能忍受的.


就目前來看可能會有上邊說的兩種記憶體泄露原因導致不能回收這個物件.打開

pyrasite-shellpyrasite-shell

我先通過

gc.isenabled()

判斷gc是否在工作, 結果發現是True, 也就是正常工作的, 而且使用gc.setdebug(gc.STATUS)設置gc為debug樣式, 然後gc.collect()進行垃圾回收發現並沒有更多記憶體釋放,則否認了第二種泄露的可能.

現在來看gc.garbage中不能被釋放的物件, 讓我來檢查一下是否有全域性變數指向它們(這裡極有可能是一個串列或者是一個字典)

gc.garbage 可以看到被塞滿了各種DicomFileLike物件

所以我們的目的就是先找到一個物件然後一級一級的向上尋找相互的取用.

到這裡發現其實沒有更多的全域性變數指向這個d了, 而且發現所以有的方法的物件地址和d是相同的, 說明瞭這個物件其實是自迴圈取用的.

那麼python不可能不支持迴圈取用物件的回收吧? 跟著這個問題我查了一下stackoverflow

Does Python GC deal with reference-cycles like this?

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

這就是python記憶體泄露的第三個可能.

回頭看DicomFileLike的原始碼, 果然在__init__函式上方定義了一個__del__函式, 我這裡使用了一個猴子補丁刪除了這個方法, 記憶體泄露的問題就得以解決了.

總結

到這裡整個除錯過程就結束了, 然而實際上過程中做了很多曲折的工作, 在pyrasite中會找到幾個取用DicomFileLike物件的object, 比較不容易辨別, 最開始我以為是某個全域性的物件取用的DicomFileLike, 比如是串列什麼的, 後來發現其實是locals()和globals()字典, 如果使用pyrasite-memory-viewer儲存下來的資料會發現有一個大串列指向所有沒有回收的DicomFileLike物件, 捯飭半天發現其實是gc.garbage, 好囧, 曾讓我一度懷疑是第一種泄露方式, 但是怎麼找這個物件都沒有找到. 其中還有幾次看到執行緒達到140+, 後來發現其實和執行緒一點關係沒有, 執行緒維持在這個數目上邊很穩定.

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

查看行程的執行緒數量


根據物件的id/address動態獲取物件

查看垃圾回收的日誌

作者:weidwonder

來源:http://www.jianshu.com/p/2d06a1a01cc3


————廣告時間————


馬哥教育2018年Python自動化運維開發實戰面授班2018年3月5號開班,馬哥聯合BAT、豆瓣等一線互聯網Python開發達人,根據目前企業需求的Python開發人才進行了深度定製,加入了大量一線互聯網公司:大眾點評、餓了麽、騰訊等生產環境真是專案,課程由淺入深,從Python基礎到Python高級,讓你融匯貫通Python基礎理論,手把手教學讓你具備Python自動化開發需要的前端界面開發、Web框架、大監控系統、CMDB系統、認證堡壘機、自動化流程平臺六大實戰能力,讓你從0開始蛻變成Hold住年薪20萬的Python自動化開發人才

掃描二維碼領取學習資料

更多Python好文請點擊【閱讀原文】哦

↓↓↓

赞(0)

分享創造快樂