從零開始搞監控系統(4)——記憶體泄漏
在將監控日誌的服務獨立部署後,還是發現CPU會在不特定時間段(例如21~22、23~02等)飆到70%,記憶體也是一路飆升不會下降,明顯是出現了記憶體泄漏。
需要進一步做優化,於是開通了阿里雲的 Node.js 性能平台。
一、Node.js性能平台
要使用此工具需要在自己的伺服器中安裝些組件的,具體步驟參考官網說明,公司運維操作起來蠻快的,下圖是平台中的數據趨勢。
點擊堆快照,就會生成一個*.heapsnapshot文件,通過該文件就能查看記憶體的分布和使用情況,點擊下圖中的轉儲就能查看分析了。
但是我怎麼點,每次都是失敗,後面找了阿里雲的技術人員,他說是因為文件太大,下載的時候總是會斷開,無奈,只能在伺服器上手動下載,然後在本地Chrome中載入了。
二、分析
在該平台上下載了堆快照(*.heapsnapshot文件),在Chrome的Memory選項卡中載入,可以看到下圖內容。
1)任務隊列(Kue.js)
翻看其中的幾列,發現記憶體中滯留了很多隊列任務的數據,於是鎖定記憶體暴漲與隊列有關。
然後開始查程式碼,並且在本地做了調試,發現在任務完成後沒有將其標記為成功,因為聲明的那個改變狀態的函數沒有被執行。
只有標記成功的任務才會被自動清除,由於狀態沒有更新,導致滯留在記憶體中,從而使得記憶體一直在漲而不會降。
一頓操作猛如虎,但是最後發布上去後,記憶體並沒有降下來,依然在增長中,說明不是這個問題。
在創建隊列任務時會打條日誌,然後在完成任務後,會再打一條日誌,發現一分鐘內會創建大約4、5百個任務,但是完成的任務只有200個,甚至更少。
也就是出隊的速度沒有入隊快,隊列來不及處理任務。如此下去的話,就會將任務堆積在一起。
馬上為隊列處理的方法加了個並發的參數,再用LoadTest模擬並發,效果非常理想,任務有條不紊地被處理了,於是發布了程式碼。
若要結束並發測試,mac電腦可執行命令 kill -USR2 36155,其中 36155 是埠號。
但高興的還是太早,雖然為隊列加了並發的設置,但滯留的任務並沒有減少,猜想可能是任務中的邏輯阻塞了任務的完成,繼續將耗時邏輯注釋掉,記憶體並沒有如預期那樣降下來。
再次分析,感覺是上面配置的並發沒有生效,很奇怪,查看Kue.js源碼也沒看出個所以然來。
只能另闢蹊徑了,也就是多創建幾種類型,但處理的邏輯是一樣的,以此來彌補任務隊列的吞吐量。
for (let i = 1; i <= 3; i++) { const taskName = "handleMonitor" + i; queue.process(taskName, (job, done) => { services.common .handleMonitor(job.data.monitor) .then(() => { done(); }) .catch((err) => { done(err); }); }); }
查看日誌,發現隊列的入和出已經平衡,但是記憶體仍然會升,沒有降的趨勢。
2)繼續分析
再次觀察堆快照,我一度懷疑是 Sequelize 、KOA 或 Node.js 8.0版本的問題,翻來覆去的查,雖然的確看到了記憶體泄漏的蛛絲馬跡,但仍然沒有起色。
後面將兩份堆快照做對比,在查看增長的數據時,發現我請求的 ma.gif 路徑中的變數不會釋放,存在著一個閉包,八成是這個原因導致記憶體一直漲。
於是仔細查看程式碼,將最可疑的一句程式碼注釋掉,如下所示,省略了其他邏輯,就放出了關鍵的那句程式碼,為外部的 queue 對象反覆註冊了一個error事件。
import queue from "../util/queue"; router.get("/ma.gif", async (ctx) => { queue.on('error', function( err ) { logger.trace('handleMonitor queue error', err); }); });
沒想到記憶體一下子平穩了,沒有出現暴增的情況。一波多折後發現,原來是自己寫的程式碼不對導致記憶體的泄漏。
參考資料: