最近工作中慢慢開始用python協程相關的東西,所以用到了一些相關模塊,如aiohttp, aiomysql, aioredis等,用的過程中也碰到的很多問題,這裡整理了一次記憶體泄漏的問題 通常我們寫python程式的時候也很少關註記憶體這個問題(當然可能我的能力還有待提升),可能寫c和c++的朋友會 ...
最近工作中慢慢開始用python協程相關的東西,所以用到了一些相關模塊,如aiohttp, aiomysql, aioredis等,用的過程中也碰到的很多問題,這裡整理了一次記憶體泄漏的問題
通常我們寫python程式的時候也很少關註記憶體這個問題(當然可能我的能力還有待提升),可能寫c和c++的朋友會更多的考慮這個問題,但是一旦我們的python程式出現了
記憶體泄漏的問題,也將是一件非常麻煩的事情了,而最近的一次代碼中也碰到了這個問題,不過好在最後記憶體溢出不是我代碼的問題,而是所用到的一個包出現了記憶體的問題,下麵我通過一個簡單的代碼模擬出記憶體的問題,然後也會將解決的過程描述一下,希望能幫助到遇到同樣問題的朋友。
一、復現問題
其實這次主要是在使用aiohttp寫一個介面的時候出現的問題,其實復現出問題非常容易,我們實現一個簡單的接受post請求介面的服務端,然後實現一個併發的客戶端來訪問這個介面,來查看記憶體的情況
註意: 這個問題是在一個包的特定版本出現的:multidict==4.5.1,我在整理這個文章2個小時前作者已經修複了這個問題發佈了4.5.2版本,已經修複了記憶體的問題,並且我也進行了測試驗證
服務端代碼:
from aiohttp import web async def hello(request): return web.json_response(await request.json()) app = web.Application() app.add_routes([web.post('/', hello)]) web.run_app(app)
客戶端代碼:
import asyncio import aiohttp async def foo(times): data = {'foo': 1} async with aiohttp.ClientSession() as session: for x in range(times): resp = await session.post('http://localhost:8080', json=data) if not x % 100: print(await resp.json()) loop = asyncio.get_event_loop() loop.run_until_complete(foo(100000)) loop.close()
因為我的代碼是在linux上跑的,或者mac上我們都可以通過htop非常方面的實時查看我們程式記憶體的占用情況,我們先將服務端啟動,查看一下我們此時的記憶體情況可以看到占用的
非常少,當我們打開客戶端之後,再次觀察我們可以看到記憶體不斷增長,及時我們客戶端運行完畢記憶體也不會降低。
當客戶端結束之後的記憶體:
如果客戶端不停止的話記憶體會一直漲,最後的結果就是把你的系統記憶體吃完,然後被系統殺掉你的進程。
二、解決記憶體泄漏的過程
像上面的例子是一個非常簡單的程式,不複雜我們也並沒有做上面複雜的操作就是一個簡單的接受post請求的服務端,但是如果是在實際的項目中我們可能會寫非常複雜的業務邏輯,那到時候我們又如何找到是哪裡導致的記憶體問題,當我碰到這個問題的時候,其實我和很多接觸python不久的人差不多,也是不知道怎麼查這種問題,各種百度各種查,也找到了好多推薦的工具,memory_profiler庫,objgraph庫,graphviz工具,但是都沒有幫助我迅速的找到問題點在哪裡,最後看到標準庫中的tracemalloc,地址:https://docs.python.org/3/library/tracemalloc.html
通過這個包很快幫我找到了記憶體泄漏的地方
接下來按照官網的方法我將代碼進行改寫,來測試到底哪裡的問題導致的記憶體泄漏,更改後的服務端代碼為:
from aiohttp import web import tracemalloc async def hello(request): return web.json_response(await request.json()) async def get_info(request): snapshot2 = tracemalloc.take_snapshot() top_stats = snapshot2.compare_to(snapshot1, 'lineno') print(top_stats) return web.Response(text="ok") if __name__ == '__main__': app = web.Application() app.add_routes( [ web.post('/', hello), web.get("/get_info", get_info) ] ) tracemalloc.start() snapshot1 = tracemalloc.take_snapshot() web.run_app(app)
註意print(top_stats)這行列印的結果最後要關註
其實這裡就是新增加了一個路由get_info, 我們啟動服務端之後開啟客戶端,當我們客戶端運行完畢之後,可以看到記憶體已經漲上去了,並且沒有不會釋放,這個時候,可以直接通過瀏覽器訪問get_info這個路由看看print列印的內容,這裡將會列印出你程式運行到這個時候那一行的代碼記憶體增長的比較多,進行一次排序,前面的幾個其實都是需要你關註的,因為這裡數據較多,我就只列印如下前幾個數據
<StatisticDiff traceback=<Traceback (<Frame filename='/Users/zhaofan/anaconda3/lib/python3.6/site-packages/aiohttp/web_response.py' lineno=56>,)> size=116500672 (+116500672) count=300004 (+300004)>,
<StatisticDiff traceback=<Traceback (<Frame filename='/Users/zhaofan/anaconda3/lib/python3.6/site-packages/aiohttp/web_response.py' lineno=604>,)> size=11400000 (+11400000) count=200000 (+200000)>,
<StatisticDiff traceback=<Traceback (<Frame filename='/Users/zhaofan/anaconda3/lib/python3.6/site-packages/aiohttp/web_response.py' lineno=472>,)> size=8000000 (+8000000) count=100000 (+100000)>,<StatisticDiff traceback=<Traceback (<Frame filename='/Users/zhaofan/anaconda3/lib/python3.6/site-packages/aiohttp/web_response.py' lineno=353>,)> size=5500000 (+5500000) count=100000 (+100000)>,
<StatisticDiff traceback=<Traceback (<Frame filename='/Users/zhaofan/anaconda3/lib/python3.6/site-packages/aiohttp/web_response.py' lineno=352>,)> size=5300608 (+5300608) count=100001 (+100001)>,
我們拿第一行來說,我們可以非常清楚的指導web_response的56行代碼導致記憶體增長的最多,當然如果是我們複雜的項目也可以通過類似的方法,這樣就可以非常快捷的找到我們代碼中哪些地方會造成記憶體溢出,便於排查問題,我們點進去看看這行代碼:
我們找到最終行,這個時候我們大致就可以看出哪裡的問題了,我們接著看 CIMultiDict
class CIMultiDict(MultiDict): def _title(self, key): return key.title()
我們可以看到這個它繼承 MultiDict 其實這裡我們已經應該知道問題就是處在這個MultiDict上了
而這個最終其實最終就是MultiDict這個包,問題出在了這個包上,這個項目是在這裡維護的:https://github.com/aio-libs/multidict
查看這個包的時候看到了,果然有人和我遇到了同樣的問題,問題就是出在這裡了,已經有人提交了bug
https://github.com/aio-libs/multidict/issues/307
不過不得不說國外的程式員真的是熱愛自己的職業,很快這個問題得到了aio-libs小組中人的回應,問題也在我整理這個博客的時候被修複了,在最新的版本:4.5.2中已經測試沒有記憶體泄漏的問題
三、總結
在這裡處理的過程中,其實發現了自己很多的不足,查找問題的方式,以及遇到這種問題的解決思路,不過經過這次,至少下次遇到同樣的問題,自己能很快的去查找
以及解決問題,還有就是針對https://docs.python.org/3/library/tracemalloc.html這個庫的使用,也推薦大家多瞭解一下。