Python學習 2019-10-10 18:24:39
筆者曾經開發過的幾個大型 Django 應用程式都在某個時候出現了記憶體洩漏。Python 程序緩慢地增加它們的記憶體消耗,直到崩潰。這一點也不好玩。即使自動重新啟動程序之後,仍然會有一些宕機問題。
Python 中的記憶體洩漏通常發生在無限增長的模組級變數中。這可能是一個具有無窮大 maxsize 的 lru_cache 變數,也可能是一個在錯誤範圍內宣告的簡單列表。
洩漏也不是隻有發生在你自己寫的程式碼中才會影響你。例如,看看 BuzzFeed 的 Peter Karp 寫的這篇優秀的文章(https://docs.python.org/3/library/tracemalloc.html),他在 Python 的標準庫中發現了一個記憶體洩漏(已經修復了!)
解決方法下面的解決方法都會在執行了很多請求或任務之後重新啟動 worker 程序。這是一個清除任何潛在的無限積累的 Python 物件的簡單方法。如果您的 web 伺服器、佇列 worker 或類似的應用程式有此能力,但還沒有被功能化,請告訴我,我會新增它!
即使您現在還沒有看到任何記憶體洩漏,新增這些解決方法也會提高您的應用程式的彈性。
Gunicorn如果您正在使用 Gunicorn 作為您的 Python web 伺服器,您可以使用--max-requests 設定來定期重啟 worker。與它的兄弟--max-requests-jitter 配合使用以防止所有 worker 同時重啟。這有助於減少 worker 的啟動負載。
例如,在最近的一個專案中,我將 Gunicorn 配置為:
gunicorn --max-requests 1000 --max-requests-jitter 50 ... app.wsgi
對於此專案的流量水平、worker 數量和伺服器數量來說,這將大約每1.5小時重新啟動 worker。5% 的浮動足以消除重啟負載的相關性。
Uwsgi如果您正在使用 uwsgi,你可以使用它類似的 max-requests 設定。此設定在很多次請求之後,也會重新啟動 worker。
例如,在以前的一個專案中,筆者在 uwsgi.ini 檔案中像這樣使用了這個設定:
[uwsgi]master = truemodule = app.wsgi...max-requests = 500
Uwsgi 還提供了 max-requests-delta 選項用於新增其他浮動。但由於它是一個絕對數字,所以配置起來要比 Gunicorn 更麻煩。如果你更改了 worker 的數量或 max-requests 的值,那你就需要重新計算 max-requests-delta 來保持您的浮動在一個特定的百分比。
CeleryCelery 為記憶體洩漏提供了幾個不同的設定。
首先是 worker_max_tasks_per_child 設定。這將在 worker 子程序處理了許多工之後重新啟動它們。此設定沒有浮動選項,但是 Celery 任務的執行時間範圍很廣,所以會有一些自然的浮動。
例如:
app = Celery("inventev")app.conf.worker_max_tasks_per_child = 100
或者你正在使用 Django 設定:
CELERY_WORKER_MAX_TASKS_PER_CHILD = 100
100個任務比筆者上面建議的 web 請求數要小一些。在過去,筆者最終為 Celery 使用了較小的值,因為筆者在後臺任務中看到了更多的記憶體消耗。(我想我還在 Celery 本身中遇到了記憶體洩漏。)
你可以使用的另一個設定是 worker_max_memory_per_child。這指定子程序在父程序替換它之前可以使用的最大千位元組記憶體。它有點複雜,所以我還沒用過。
如果你確實使用了 worker_max_memory_per_child 設定,那麼你可能應該將其計算為總記憶體的百分比,併除以每個子程序。這樣,如果你更改了子程序的數量或你的伺服器的可用記憶體,它將會自動擴充套件。例如(未測試):
import psutilcelery_max_mem_kilobytes = (psutil.virtual_memory().total * 0.75) / 1024app.conf.worker_max_memory_per_child = int(celery_max_mem_kilobytes / app.conf.worker_concurrency)
這使用psutil 來查詢整個系統記憶體。它將最多75%(0.75)的記憶體分配給Celery,只有在伺服器是一個專用 Celery 伺服器的情況下你才會需要它。
跟蹤洩漏在 Python 中除錯記憶體洩漏是很不容易的,因為任何函式都可以在任何模組中分配全域性物件。它們也可能出現在與 C API 整合的擴充套件程式碼中。
筆者用過的一些工具:
標準庫模組 tracemalloc.objgraph 和 guppy3 包都是在 tracemalloc 之前完成的,並嘗試做類似的事情。它們都不太友好,但我以前成功地使用過它們。Scout APM 用 CPython 的記憶體分配計數來檢測每個“span”(請求、SQL 查詢、模板標籤,等等)。少數 APM 解決方案能做到這一點。提示:我維護著 Python 整合。更新 (2019-09-19): Riccardo Magliocchetti在Twitter上提到,pyuwsgimemhog 專案可以解析 uwsgi 日誌檔案,並告訴您哪些路徑正在洩漏記憶體。很簡潔!
其他一些有用的博文:
Buzzfeed Tech撰寫了一篇《如何在生產Python web服務上使用tracemalloc 的指南》。Fugue 的文章(https://www.fugue.co/blog/diagnosing-and-fixing-memory-leaks-in-python.html)也使用了 tracemalloc。在 Benoit Bernard 的“古怪的 Python 記憶體洩漏”帖子中,(/file/2020/04/01/20200401230724_8802.jpg