如何排查Python中的內(nèi)存問題?
譯文【51CTO.com快譯】發(fā)現(xiàn)應(yīng)用程序內(nèi)存不足是開發(fā)者遇到的糟糕問題之一。內(nèi)存問題一般很難加以診斷和修復(fù),而在Python中尤為困難。Python的自動(dòng)垃圾收集讓您易于上手該語言,但出現(xiàn)問題時(shí),開發(fā)者不知道如何識(shí)別和修復(fù)問題。
本文介紹如何診斷和修復(fù)開源AutoML庫EvalML中的內(nèi)存問題。解決內(nèi)存問題沒什么訣竅,但我希望開發(fā)者、尤其是Python開發(fā)者可以了解將來遇到這類問題時(shí)可利用的工具和優(yōu)秀實(shí)踐。
什么是內(nèi)存泄漏?
任何編程語言最重要的功能之一是能夠?qū)⑿畔⒋鎯?chǔ)在計(jì)算機(jī)內(nèi)存中。每當(dāng)您的程序創(chuàng)建一個(gè)新變量,它都會(huì)分配一些內(nèi)存用于存儲(chǔ)該變量的內(nèi)容。
內(nèi)核為程序訪問計(jì)算機(jī)的CPU、內(nèi)存和磁盤存儲(chǔ)等資源定義了接口。每種編程語言提供了要求內(nèi)核分配和釋放內(nèi)存塊供運(yùn)行中的程序使用的方法。
程序要求內(nèi)核留出內(nèi)存塊供使用,但隨后由于錯(cuò)誤或崩潰,程序完成使用該內(nèi)存后從未告訴內(nèi)核,就會(huì)發(fā)生內(nèi)存泄漏。在這種情況下,內(nèi)核將繼續(xù)認(rèn)為被遺忘的內(nèi)存塊仍被運(yùn)行中的程序使用,其他程序無法訪問這些內(nèi)存塊。
如果運(yùn)行程序時(shí)同樣的泄漏一再發(fā)生,被遺忘的內(nèi)存總量會(huì)變得很龐大,因而消耗計(jì)算機(jī)的大部分內(nèi)存!在這種情況下,如果程序隨后嘗試請(qǐng)求更多內(nèi)存,內(nèi)核會(huì)拋出“內(nèi)存不足”錯(cuò)誤,程序?qū)⑼V惯\(yùn)行,換句話說“崩潰”。
因此,找到并修復(fù)所編寫的程序中的內(nèi)存泄漏很重要,否則程序最終可能會(huì)耗盡內(nèi)存并崩潰,或者可能導(dǎo)致其他程序崩潰。
第1步:確定是內(nèi)存問題
應(yīng)用程序崩潰的原因有很多:也許運(yùn)行代碼的服務(wù)器崩潰了,也許代碼本身存在邏輯錯(cuò)誤,所以確定眼前的問題是內(nèi)存問題很重要。
EvalML性能測(cè)試悄然崩潰。突然,服務(wù)器停止記錄進(jìn)度,作業(yè)悄然停止。服務(wù)器日志會(huì)顯示編程錯(cuò)誤引起的任何堆棧追蹤,所以我有預(yù)感:崩潰是作業(yè)耗用所有的可用內(nèi)存引起的。
我又重新進(jìn)行了性能測(cè)試,但這次啟用了Python的內(nèi)存分析器,以獲取內(nèi)存使用情況圖。測(cè)試再次崩潰,當(dāng)我查看內(nèi)存圖時(shí),發(fā)現(xiàn)了該圖:
圖1.性能測(cè)試的內(nèi)存使用情況
內(nèi)存使用情況逐漸保持穩(wěn)定,但隨后達(dá)到8 GB!我知道應(yīng)用服務(wù)器有8GB 的內(nèi)存,所以該圖證實(shí)我們耗盡了內(nèi)存。此外,內(nèi)存穩(wěn)定時(shí),我們使用約4 GB的內(nèi)存,但之前版本的EvalML使用約2 GB的內(nèi)存。由于某種原因,當(dāng)前版本使用的內(nèi)存是平常的大約兩倍。
現(xiàn)在需要找出原因。
第2步:用極簡(jiǎn)示例在本地重現(xiàn)內(nèi)存問題
查明內(nèi)存問題的原因需要大量實(shí)驗(yàn)和迭代,因?yàn)榇鸢竿ǔ2⒉幻黠@。如果是這樣,您可能不會(huì)將其寫入代碼!出于這個(gè)原因,我認(rèn)為用盡可能少的代碼行重現(xiàn)問題很重要。這個(gè)極簡(jiǎn)示例使您可以在修改代碼時(shí)在分析器下快速運(yùn)行它,查看是否取得進(jìn)展。
我憑經(jīng)驗(yàn)知道,大概在我看到大峰值時(shí),應(yīng)用程序運(yùn)行含有150萬行的出租車數(shù)據(jù)集。我將應(yīng)用程序精簡(jiǎn)至僅運(yùn)行該數(shù)據(jù)集的部分。我看到了類似上述的峰值,但這次內(nèi)存使用量達(dá)到了10 GB!
見此情形,我知道有一個(gè)足夠好的極簡(jiǎn)示例可深入研究。
圖2. 出租車數(shù)據(jù)集本地重現(xiàn)的內(nèi)存使用情況
第3步:找到分配最多內(nèi)存的代碼行
一旦將問題隔離到盡可能小的代碼塊,我們可以看到程序在何處分配最多的內(nèi)存。這便于您重構(gòu)代碼和修復(fù)問題。
filprofiler是個(gè)出色的Python工具。它顯示應(yīng)用程序中每一行代碼在內(nèi)存使用高峰時(shí)的內(nèi)存分配情況。這是本地示例的輸出結(jié)果:
圖3. fil-profile的輸出
filprofiler根據(jù)內(nèi)存分配情況對(duì)應(yīng)用程序中的代碼行(以及依賴項(xiàng)的代碼)進(jìn)行排名。線越長(zhǎng)越紅,分配的內(nèi)存越多。
分配最多內(nèi)存的代碼行用來創(chuàng)建pandas數(shù)據(jù)幀(pandas/core/algorithms.py和pandas/core/internal/managers.py),數(shù)據(jù)量達(dá)4GB!我在這里截?cái)嗔薴ilprofiler的輸出,但它能夠?qū)andas代碼追溯到用EvalML來創(chuàng)建Pandas數(shù)據(jù)幀的代碼。
是的,EvalML創(chuàng)建Pandas數(shù)據(jù)幀,但這些數(shù)據(jù)幀在整個(gè)AutoML算法中都是短暫的,一旦不再使用就應(yīng)該被釋放。由于實(shí)際情況并非如此,加上這些數(shù)據(jù)幀在內(nèi)存中的時(shí)間足夠長(zhǎng),我認(rèn)為最新版本帶來了內(nèi)存泄漏。
第4步:識(shí)別泄漏對(duì)象
在Python中,泄漏對(duì)象是使用完成后沒有被Python的垃圾收集器釋放的對(duì)象。由于 Python使用引用計(jì)數(shù)作為主要的垃圾收集算法之一,這些泄漏對(duì)象通常是由對(duì)象占有引用時(shí)間過長(zhǎng)引起的。
這類對(duì)象很難找到,但可以用一些Python工具簡(jiǎn)化搜尋。第一個(gè)工具是垃圾收集器的gc.DEBUG_SAVEALL標(biāo)志。若設(shè)置該標(biāo)志,垃圾收集器將無法訪問的對(duì)象存儲(chǔ)在gc.garbage列表中。這讓您可以進(jìn)一步研究這些對(duì)象。
第二個(gè)工具是objgraph庫。一旦對(duì)象在gc.garbage列表中,我們可以根據(jù)pandas數(shù)據(jù)幀過濾該列表,并使用objgraph查看哪些其他對(duì)象引用這些數(shù)據(jù)幀、將它們保存在內(nèi)存中。
這是我在可視化其中一個(gè)數(shù)據(jù)幀后看到的對(duì)象圖的一個(gè)子集:
圖4. Pandas數(shù)據(jù)幀使用內(nèi)存圖,顯示導(dǎo)致內(nèi)存泄漏的循環(huán)引用
這就是我要找的確鑿證據(jù)!數(shù)據(jù)幀通過創(chuàng)建循環(huán)引用的PandasTableAccessor對(duì)自身進(jìn)行引用,因此這會(huì)將對(duì)象保留在內(nèi)存中,直到Python的垃圾收集器運(yùn)行并釋放它。(可以通過dict、PandasTableAccessor、dict和_dataframe 追蹤循環(huán)。)這對(duì)EvalML來說有問題,因?yàn)槔占鲗⑦@些數(shù)據(jù)幀保存在內(nèi)存中的時(shí)間太長(zhǎng),以至于我們耗盡內(nèi)存!
我能夠?qū)andasTableAccessor追溯到Woodwork庫,并將該問題提交給維護(hù)者。他們?cè)谛掳姹局行迯?fù)后,向pandas存儲(chǔ)庫提交了相關(guān)的問題單,這個(gè)例子表明了開源生態(tài)系統(tǒng)中的合作。
Woodwork更新發(fā)布后,我可視化同一個(gè)數(shù)據(jù)幀的對(duì)象圖,循環(huán)消失了!
圖5.woodwork升級(jí)后pandas數(shù)據(jù)幀的對(duì)象圖。不再有循環(huán)!
第5步:驗(yàn)證修復(fù)是否有效
我在EvalML中升級(jí)Woodwork 版本后,測(cè)量了應(yīng)用程序的內(nèi)存使用情況。結(jié)果發(fā)現(xiàn),內(nèi)存使用量現(xiàn)在不到過去的一半!
圖6. 修復(fù)后性能測(cè)試的內(nèi)存使用情況
原文標(biāo)題:How to troubleshoot memory problems in Python,作者:Freddy Boulton
【51CTO譯稿,合作站點(diǎn)轉(zhuǎn)載請(qǐng)注明原文譯者和出處為51CTO.com】





































