python內存泄漏怎么辦?填坑排查小技巧

摘要:最近工作遇到了內存泄漏問題,運維同學緊急呼叫解決,于是在解決問題之余也系統(tǒng)記錄了下內存泄漏問題的常見解決思路。

最近工作遇到了內存泄漏問題,運維同學緊急呼叫解決,于是在解決問題之余也系統(tǒng)記錄了下內存泄漏問題的常見解決思路。

首先搞清楚了本次問題的現(xiàn)象:

  1. 服務在13號上線過一次,而從23號開始,出現(xiàn)內存不斷攀升問題,達到預警值重啟實例后,攀升速度反而更快。

  2. 服務分別部署在了A、B 2種芯片上,但除模型推理外,幾乎所有的預處理、后處理共享一套代碼。而B芯片出現(xiàn)內存泄漏警告,A芯片未出現(xiàn)任何異常。

思路一:研究新舊源碼及二方庫依賴差異

根據以上兩個條件,首先想到的是13號的更新引入的問題,而更新可能來自兩個方面:

  1. 自研代碼

  2. 二方依賴代碼

從上述兩個角度出發(fā):

  • 一方面,分別用Git歷史信息和BeyondCompare工具對比了兩個版本的源碼,并重點走讀了下A、B兩款芯片代碼單獨處理的部分,均未發(fā)現(xiàn)任何異常。
  • 另一方面,通過pip list命令對比兩個鏡像包中的二方包,發(fā)現(xiàn)僅有pytz時區(qū)工具依賴的版本有變化。

經過研究分析,認為此包導致的內存泄漏的可能性不大,因此暫且放下。

至此,通過研究新舊版本源碼變化找出內存泄漏問題這條路,似乎有點走不下去了。

思路二:監(jiān)測新舊版本內存變化差異

目前python常用的內存檢測工具有pympler、objgraph、tracemalloc 等。

首先,通過objgraph工具,對新舊服務中的TOP50變量類型進行了觀察統(tǒng)計

objraph常用命令如下:

\# 全局類型數量  
objgraph.show\_most\_common\_types(limit=50)  
\# 增量變化  
objgraph.show\_growth(limit=30)

這里為了更好的觀測變化曲線,我簡單做了個封裝,使數據直接輸出到了csv文件以便觀察。

stats = objgraph.most\_common\_types(limit=50)  
stats\_path = "./types\_stats.csv"  
tmp\_dict = dict(stats)  
req\_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())  
tmp\_dict\['req\_time'\] = req\_time  
df = pd.DataFrame.from\_dict(tmp\_dict, orient='index').T  
  
if os.path.exists(stats\_path):  
    df.to\_csv(stats\_path, mode='a', header=True, index=False)  
else:  
    df.to\_csv(stats\_path, index=False)

如下圖所示,用一批圖片在新舊兩個版本上跑了1個小時,一切穩(wěn)如老狗,各類型的數量沒有一絲波瀾。

此時,想到自己一般在轉測或上線前都會將一批異常格式的圖片拿來做個邊界驗證。

雖然這些異常,測試同學上線前肯定都已經驗證過了,但死馬當成活馬醫(yī)就順手拿來測了一下。

平靜數據就此被打破了,如下圖紅框所示:dict、function、method、tuple、traceback等重要類型的數量開始不斷攀升。

而此時鏡像內存亦不斷增加且毫無收斂跡象。

由此,雖無法確認是否為線上問題,但至少定位出了一個bug。而此時回頭檢查日志,發(fā)現(xiàn)了一個奇怪的現(xiàn)象:

正常情況下特殊圖片導致的異常,日志應該輸出如下信息,即check_image_type方法在異常棧中只會打印一次。

但現(xiàn)狀是check_image_type方法循環(huán)重復打印了多次,且重復次數隨著測試次數在一起變多。

重新研究了這塊兒的異常處理代碼。

異常聲明如下:

拋異常代碼如下:

問題所在

思考后大概想清楚了問題根源:

這里每個異常實例相當于被定義成了一個全局變量,而在拋異常的時候,拋出的也正是這個全局變量。當此全局變量被壓入異常棧處理完成之后,也并不會被回收。

因此隨著錯誤格式圖片調用的不斷增多,異常棧中的信息也會不斷增多。而且由于異常中還包含著請求圖片信息,因此內存會呈MB級別的增加。

但這部分代碼上線已久,線上如果真的也是這里導致的問題,為何之前沒有任何問題,而且為何在A芯片上也沒有出現(xiàn)任何問題?

帶著以上兩個疑問,我們做了兩個驗證:

首先,確認了之前的版本以及A芯片上同樣會出現(xiàn)此問題。

其次,我們查看了線上的調用記錄,發(fā)現(xiàn)最近剛好新接入了一個客戶,而且出現(xiàn)了大量使用類似問題的圖片調用某局點(該局點大部分為B芯片)服務的現(xiàn)象。我們找了些線上實例,從日志中也觀測到了同樣的現(xiàn)象。

由此,以上疑問基本得到了解釋,修復此bug后,內存溢出問題不再出現(xiàn)。

進階思路

講道理,問題解決到這個地步似乎可以收工了。但我問了自己一個問題,如果當初沒有打印這一行日志,或者開發(fā)人員偷懶沒有把異常棧全部打出來,那應該如何去定位?

帶著這樣的問題我繼續(xù)研究了下objgraph、pympler 工具。

前文已經定位到了在異常圖片情況下會出現(xiàn)內存泄漏,因此重點來看下此時有哪些異樣情況:

通過如下命令,我們可以看到每次異常出現(xiàn)時,內存中都增加了哪些變量以及增加的內存情況。

  1. 使用objgraph工具objgraph.show_growth(limit=20)
  1. 使用pympler工具
    
from pympler import tracker  
tr = tracker.SummaryTracker()  
tr.print\_diff()  

通過如下代碼,可以打印出這些新增變量來自哪些引用,以便進一步分析。

gth = objgraph.growth(limit=20)  
for gt in gth:  
    logger.info("growth type:%s, count:%s, growth:%s" % (gt\[0\], gt\[1\], gt\[2\]))  
    if gt\[2\] > 100 or gt\[1\] > 300:  
        continue  
    objgraph.show\_backrefs(objgraph.by\_type(gt\[0\])\[0\], max\_depth=10, too\_many=5,  
                           filename="./dots/%s\_backrefs.dot" % gt\[0\])  
    objgraph.show\_refs(objgraph.by\_type(gt\[0\])\[0\], max\_depth=10, too\_many=5,  
                       filename="./dots/%s\_refs.dot" % gt\[0\])  
    objgraph.show\_chain(  
        objgraph.find\_backref\_chain(objgraph.by\_type(gt\[0\])\[0\], objgraph.is\_proper\_module),  
        filename="./dots/%s\_chain.dot" % gt\[0\]  
    )

通過graphviz的dot工具,對上面生產的graph格式數據轉換成如下圖片:

dot -Tpng xxx.dot -o xxx.png

這里,由于dict、list、frame、tuple、method等基本類型數量太多,觀測較難,因此這里先做了過濾。

內存新增的ImageReqWrapper的調用鏈

內存新增的traceback的調用鏈:

雖然帶著前面的先驗知識,使我們很自然的就關注到了traceback和其對應的IMAGE_FORMAT_EXCEPTION異常。

但通過思考為何上面這些本應在服務調用結束后就被回收的變量卻沒有被回收,尤其是所有的traceback變量在被IMAGE_FORMAT_EXCEPTION異常調用后就無法回收等這些現(xiàn)象;同時再做一些小實驗,相信很快就能定位到問題根源。

至此,我們可以得出結論如下:

由于拋出的異常無法回收,導致對應的異常棧、請求體等變量都無法被回收,而請求體中由于包含圖片信息因此每次這類請求都會導致MB級別的內存泄漏。

另外,研究過程中還發(fā)現(xiàn)python3自帶了一個內存分析工具tracemalloc,通過如下代碼就可以觀察代碼行與內存之間的關系,雖然可能未必精確,但也能大概提供一些線索。

import tracemalloc  
  
tracemalloc.start(25)  
snapshot = tracemalloc.take\_snapshot()  
global snapshot  
gc.collect()  
snapshot1 = tracemalloc.take\_snapshot()  
top\_stats = snapshot1.compare\_to(snapshot, 'lineno')  
logger.warning("\[ Top 20 differences \]")  
for stat in top\_stats\[:20\]:  
    if stat.size\_diff < 0:  
        continue  
    logger.warning(stat)  
snapshot = tracemalloc.take\_snapshot()

如果文章對你有幫助的話,點個贊再走吧

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • HBase是Apache的Hadoop項目的子項目,是Hadoop Database的簡稱。HBase是一個高可靠...
    壟山小站閱讀 538評論 0 0
  • Cancer Dis | 分化療法抑制AML細胞分化過程的酶,治療白血病 原創(chuàng)圖靈基因圖靈基因2021-12-13...
    圖靈基因閱讀 425評論 0 0
  • 來自那些年我踩過的坑 postgres有些人估計都沒有聽說過,沒關系,可以百度自己了解一下,這里說一下Centos...
    maxzhao_閱讀 746評論 0 0
  • 本章的目標是為 Microblog 實現(xiàn)搜索功能,以便用戶可以使用自然語言查找有趣的用戶動態(tài)內容。許多不同類型的網...
    SingleDiego閱讀 394評論 0 0
  • 有一起學Python的小伙伴別忘記加入我們的Python學習交流群群:367203382 一、算法設計[https...
    ztloo閱讀 1,083評論 0 7

友情鏈接更多精彩內容