python內存泄漏排查小技巧

      網友投稿 1008 2025-04-02

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


      首先搞清楚了本次問題的現象:

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

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

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

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

      自研代碼

      二方依賴代碼

      從上述兩個角度出發:

      一方面,分別用Git歷史信息和BeyondCompare工具對比了兩個版本的源碼,并重點走讀了下A、B兩款芯片代碼單獨處理的部分,均未發現任何異常。

      另一方面,通過pip list命令對比兩個鏡像包中的二方包,發現僅有pytz時區工具依賴的版本有變化。

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

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

      思路二:監測新舊版本內存變化差異

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

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

      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個小時,一切穩如老狗,各類型的數量沒有一絲波瀾。

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

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

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

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

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

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

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

      python內存泄漏排查小技巧

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

      異常聲明如下:

      拋異常代碼如下:

      問題所在

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

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

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

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

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

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

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

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

      進階思路

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

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

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

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

      使用objgraph工具

      objgraph.show_growth(limit=20)

      使用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異常調用后就無法回收等這些現象;同時再做一些小實驗,相信很快就能定位到問題根源。

      另,關于 python3中 緩存Exception導致的內存泄漏問題,知乎有一篇講的相對更清楚一點:https://zhuanlan.zhihu.com/p/38600861

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

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

      另外,研究過程中還發現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()

      參考文章

      https://testerhome.com/articles/19870?order_by=created_at&

      https://blog.51cto.com/u_3423936/3019476

      https://segmentfault.com/a/1190000038277797

      https://www.cnblogs.com/zzbj/p/13532156.html

      https://drmingdrmer.github.io/tech/programming/2017/05/06/python-mem.html

      https://zhuanlan.zhihu.com/p/38600861

      Python

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      上一篇:統計報表從哪報的?。槭裁匆獔蠼y計報表)
      下一篇:excel如何找出重復值并提取
      相關文章
      自怕偷自怕亚洲精品| 国产偷v国产偷v亚洲高清| 亚洲人成在线电影| 亚洲区小说区图片区QVOD| 在线精品亚洲一区二区三区| 亚洲av日韩片在线观看| 国产精品成人亚洲| 国产精品亚洲lv粉色| 色偷偷噜噜噜亚洲男人| 亚洲成a人片在线观看天堂无码 | 亚洲午夜理论片在线观看| 久久亚洲国产成人影院| 亚洲中文无码永久免| 亚洲人成77777在线观看网| 最新亚洲春色Av无码专区| 亚洲中文无码永久免费| 亚洲无人区码一二三码区别图片| 国产成人精品日本亚洲18图| 亚洲天堂免费在线| 亚洲色大成网站www| 亚洲成a人片在线不卡一二三区 | 精品亚洲视频在线| 亚洲成AV人网址| 中文字幕亚洲乱码熟女一区二区| 国产亚洲色婷婷久久99精品91| 亚洲精品成人片在线观看精品字幕| 国产AV无码专区亚洲AV毛网站| 亚洲v高清理论电影| 67pao强力打造67194在线午夜亚洲 | 亚洲AV日韩AV无码污污网站| 国产精品亚洲综合网站| 亚洲天堂在线视频| 国产亚洲成av片在线观看| 亚洲国产综合精品中文第一区 | 亚洲成av人片天堂网老年人| 在线观看亚洲成人| 亚洲综合婷婷久久| 色偷偷女男人的天堂亚洲网| 亚洲成在人线aⅴ免费毛片| 国产精品亚洲视频| 亚洲AV第一页国产精品|