python 內存泄漏調試


Python應用程序內存泄漏的調試

Quake Lee

quakelee@geekcn.org

新浪網技術(中國)有限公司

Sina Research & Development

Python-LDAP是什么?

  • Python-LDAP是一個第三方的開源項目,主要目標是實現python的LDAP接口, 這是一個由C語言編寫的Python擴展模塊。
  • 該模塊的主要功能是把通過C接口調用libldap從中取出的數據,轉換成為Python的對象, 除此之外還有逆向轉換

Python-LDAP存在的問題

  • C接口調用程序內存泄漏
  • C接口程序Python引用計數器泄漏

分析可能的泄漏點

  • 測試自己編寫的程序時,發現程序嚴重消耗內存。在進行53萬次查詢后,程序占用的內存增大了4倍
  • 懷疑程序存在內存泄漏,經過測試,確認程序占用內存的數量的確隨查詢量增長而增長。確認存在內存泄漏。
  • 判斷存在三種可能性:
    • Python解釋器存在內存泄漏
    • 我們自己開發的Python程序未能及時釋放必須手動釋放的資源
    • TwistedCore存在內存泄漏
    • 調用的某個Python擴展模塊出現內存泄漏

可能性一:Python解釋器內存泄漏

  • 調試步驟:
    • 首先檢查了Python的Bug Manager,查看Python 2.4.1 以后,報告的和解決掉的關於Python的內存管理問題
    • Python近兩年來只有3個關於內存泄漏方面的問題被人報告出來,兩個是關於urllib2的。 另一個是關於dict的未能確認的泄漏,但只是出現在對dict進行特殊的使用時。
    • 據此判斷在我的程序的使用方式下Python解釋器出現內存泄漏的可能性很低。

可能性二:Python代碼未能釋放必須
手動釋放的資源

  • 調試步驟:
    • 手動釋放所有不需要持續使用的資源,再次測試
    • 內存占用沒有變化,依然保持原樣
    • 說明不是Python代碼造成的內存泄漏

可能性三:TwistedCore存在內存泄漏

  • 調試步驟:
    • 查詢了Twisted的Tickets系統
    • Twisted確實在近期出現過一些內存泄漏的情況,並且包括一個Twisted 2.1.0未修復的
    • 升級Twisted到Twisted 2.2.0
    • 再次測試,內存占用仍然沒有變化,說明:
      • 可能TwistedCore不存在內存泄漏
      • 還存在未能發現的TwistedCore內存泄漏
    • Twisted代碼比較多,結構復雜,先跳過這種可能性

可能性四:某個Python擴展模塊出現內存泄漏

  • 程序主要依賴的擴展模塊就是Python-LDAP
  • 該模塊存在內存泄漏的可能比較高
  • 如何進行調試呢?

內存泄漏調試過程

  • 使用分析工具Valgrind對整個應用程序的運行過程進行分析
  • 運行命令行
    valgrind -v --leak-check=yes --num-callers=256 --logfile=d python d.py
  • 發現openldap 2.3.19中libldap.so包含兩處內存泄漏
    • ldap初始化的時候解析域名時存在的一次性泄漏
    • 多線程並發情況下,斷開連接時忘記釋放的mutex

仍然泄漏

  • 對第二個泄漏進行了修復
  • 再次測試,發現仍然泄漏
  • 此時valgrind的報告中只剩下一個一次性的泄漏了,這個泄漏只有49個字節
  • 內存都讓誰占了呢?

分析內存占用情況

  • 為valgrind增加命令行參數--show-reachable=yes,這將會把還在使用的內存情況也打印出來
  • 運行命令行
    valgrind -v --show-reachable=yes --leak-check=yes --num-callers=256 \
    --logfile=d python d.py
    				

分析結果

  • 長時間測試后發現了如下情況:
9676796 bytes in 235920 blocks are still reachable in loss record 32 of 32
   at 0x3C032183: malloc (vg_replace_malloc.c:105)
   by 0x80D9CA0: _PyObject_GC_Malloc (gcmodule.c:1248)
   by 0x80D9D6B: _PyObject_GC_NewVar (gcmodule.c:1279)
   by 0x80858D3: PyTuple_New (tupleobject.c:68)
   by 0x808D9FF: PyType_Ready (typeobject.c:3167)
   by 0x808D9C5: PyType_Ready (typeobject.c:3153)
   by 0x807C8DB: _Py_ReadyTypes (object.c:1805)
   by 0x80D1BC5: Py_InitializeEx (pythonrun.c:167)
   by 0x80D1F8C: Py_Initialize (pythonrun.c:283)
   by 0x80558C3: Py_Main (main.c:418)
   by 0x805520C: main (python.c:23)

難道是Python解釋器泄漏了?

  • 根據上述報告分析,大量的內存都由python解釋器的gcmodule控制
  • 難道是gcmodule的回收機制出現了問題?

Python的Garbage Collector機制

  • Python的gc是分代垃圾回收機制,共分三代
  • 通過引用計數判斷數據單元是否可以回收
  • 通過擴展模塊gc中的接口可以分析調試垃圾回收的情況
  • 具體功能請參見Python手冊

透過GC看內存

  • Python的gc模塊功能還是很強大的
  • 使用get_objects( )方法可以取得當前所有不能回收的對象(引用計數不為0)的列表
  • 存放在列表中的是所有對象的wrapper

吃內存的老鼠抓住了

  • 通常情況下,不能釋放的對象不會太多,啟動時在2、3萬個左右
  • 我發現程序長時間運行后,不可釋放的對象總數大量增加。
  • 最終確認,不可釋放的對象數量隨查詢量增大
  • 寫個循環把他們都打印出來
  • 發現了大量的空白列表對象,並且這些空白的列表不能夠被GC回收,只有一種可能性, 它們的引用計數不為0

這些列表那兒來的?

  • 在Python代碼中創建的對象,使用完畢后引用計數器就會歸零
  • 我的代碼中也已經手動刪除了所有可以釋放掉的資源,尤其是列表對象
  • 嚴重懷疑,這些東西來自於Python-LDAP擴展模塊

關於Python擴展模塊

  • Python的擴展模塊在操作Python對象之后,也需要改變對象的引用計數器
  • 擴展模塊中的C程序一旦忘記修改或者不能正確的修改引用計數器,這些對象就成了僵屍對象

如何找到bug?

  • python-ldap有2k多行C代碼,雖說不是太多,但要是找起來也是大海撈針。
  • 對Python擴展模塊的機制不是很熟悉,很難看出代碼的錯誤
  • 給作者寫信?這確實不是我們的風格,拿不出來patch,那兒好意思跟大家打招呼:)
  • 開發任務時間緊,靠別人不如靠自己
  • 不過問題還真是棘手,這種問題連valgrind也查不出來啦

靜下心來好好想

  • 出問題的應該肯定是Python-LDAP模塊了
  • 我的程序調用它的次數並不是很多,每次查詢調一次
  • 現在已經知道泄漏的對象是空白的列表
  • 於是...

添加檢查點

    • 在每個調用python-ldap的語句前后都添加檢查點

抓住bug了

針對每次調用添加檢查點后,立刻就找到了泄漏點。正是在ldapsearch完畢, 獲取ldap查詢結果之后,出現了一個空白的列表。經過對那段代碼進行分析, 仍然無法確認是那行代碼寫錯了,因為對Python擴展模塊操作引用計數器的 方法不是很熟悉。當改用同步方式查詢ldap的時候,發現沒有泄漏出空白列 表。通過比較這兩種情況下代碼的執行路徑,發現在異步查詢的情況下,在 返回的ldap控制碼為空的時候,python-ldap返回了一個空白的列表給python 解釋器,但是並沒有講這個空白列表的引用計數器減掉,導致這個列表的引 用計數一直不能清零,gc也不能回收這個對象。

問題就出在這里

災難還沒有過去

  • 再次進行測試,程序終於不再內存泄漏了,能夠健康的運行了
  • 馬上上線運行!
  • 程序再次長大……真是福無雙至,禍不單行

不要慌

  • 測試的時候,明明沒有泄漏了,上線了又不行這是為什么?
    • 測試的時候,只測試了查的到的條件。
    • 上線后,多數情況下,是查不到這個結果的

再次祭出法寶-添加檢查點

這次測試查不到的情況,左測右測,再也沒有空白列表了…… 再次打出所有不能釋放的對象,發現不能釋放的對象並沒有增加 也沒有太多看起來可疑的僵屍對象。添加檢查點失敗……

再靜心想想

  • 這次有什么不同:
    • 不能釋放的對象沒有顯著增加,甚至還比剛啟動的時候減少了。
    • 打印所有的不能釋放掉的對象,沒有發現大量的相同對象

推測結論

  • 結論:這次不是引用計數器泄漏

還記得Valgrind么?

valgrind這次真的救了我們。通過valgrind,我們發現python-ldap的c代碼部分存在一個 嚴重的內存泄漏。當一個ldap記錄沒有查到的時候,一些struct在沒有釋放的情況下程序 就直接返回了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM