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在沒有釋放的情況下程序 就直接返回了。