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

吃內存的老鼠抓住了

  • 一般狀況下,不能釋放的對象不會太多,啓動時在二、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也不能回收這個對象。python

問題就出在這裏

災難尚未過去

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

不要慌

  • 測試的時候,明明沒有泄漏了,上線了又不行這是爲何?
    • 測試的時候,只測試了查的到的條件。
    • 上線後,多數狀況下,是查不到這個結果的

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

此次測試查不到的狀況,左測右測,再也沒有空白列表了…… 再次打出全部不能釋放的對象,發現不能釋放的對象並無增長 也沒有太多看起來可疑的殭屍對象。添加檢查點失敗……多線程

再靜心想一想

  • 此次有什麼不一樣:
    • 不能釋放的對象沒有顯著增長,甚至還比剛啓動的時候減小了。
    • 打印全部的不能釋放掉的對象,沒有發現大量的相同對象

推測結論

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

還記得Valgrind麼?

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

相關文章
相關標籤/搜索