如何定位 Node.js 的記憶體洩漏

發表於2016-04-18

如何定位 Node.js 的記憶體洩漏

《一次 Node.js 應用記憶體暴漲分析》中,我們處理了一個 Node.js vm 引發的記憶體洩漏問題,處理過程也是比較艱辛。而在我們實際開發中,可能經常會碰到記憶體洩漏的問題,但很多情況下,我們對於這種問題的處理是有些迷茫的,沒有一定的操作流程,效率比較低。雖然這種問題對於經驗的要求比較高,但如果有一個簡單的排查流程,還是會有一定幫助的。

這裡簡單整理一個流程,歡迎一起探討,補充。

基礎知識

Node.js 程式的記憶體管理,都是有 V8 自動處理的,包括記憶體分配和釋放。那麼 V8 什麼時候會將記憶體釋放呢?

在 V8 內部,會為程式中的所有變數構建一個圖,來表示變數間的關聯關係,當變數從根節點無法觸達時,就意味著這個變數不會再被使用了,就是可以回收的了。
而這個回收是一個過程性的,從快速 GC 到 最後的 Full GC,是需要一段時間的。
另外,Full GC 是有觸發閾值的,所以可能會出現記憶體長期佔用在一個高值,也可以算是一種記憶體洩漏,可以從《一次 Node.js 應用記憶體暴漲分析》中找到例子。還有一種就是引用不釋放,導致無法進入 GC 環節,並且一直產生新的佔用,這一般會發生在 Javascript 層面。

所以,定位記憶體洩漏問題,一般方案就是找那些不被使用又不會被釋放的變數,處理了這些變數,問題一般就可以解決了。如果是 Node.js 底層變數不釋放,除了提交 issue 等待解決外,只能通過優化啟動引數來解決。

如何找出並解決問題

工具

工欲善其事必先利其器,在排查時,我們還是需要一些工具來幫忙的。

devTool

這個是今年初出的 Node.js 除錯工具,基於 Electron 將 Node.js 和 Chromium 的功能融合在了一起。操作起來比 node-inspector 方便,開放的 Timeline 功能還是比較實用的,雖然不是實時顯示。
僅需要 devtool xxx.js,還可以通過 .devtoolrc 來進行引數定製,具體見 GitHub

heapdump + chrome devTool

這個是比較傳統的定位記憶體洩漏的組合。heapdump 可以直接在程式碼中呼叫生成記憶體快照,然後將快照檔案匯入到 chrome devTool 進行分析,之後操作其實和前者就差不多了。不過,這個方案和前者有一點區別就是,前者實際還是在瀏覽器環境中,所以生成的記憶體快照會有一些 DOM 物件的存在,會有一定的干擾。而這個方案,是直接呼叫底層 V8 的方法,生成的快照只有 Node.js 環境中的物件。

memwatch

這個可以在程式碼裡直接使用,實時檢測記憶體動態,當發生記憶體洩漏的時候,會觸發 ‘leak’ 事件,會傳遞當前的堆狀態,配合 heapdump 有奇效。詳見 memwatch

流程

一、重現問題

對於垃圾回收,V8 引擎有很複雜的邏輯來決定什麼時候進行回收。很多時候,當我們發現 Node.js 程式所使用的記憶體快速增長的時候,並不能確定是否是記憶體洩漏導致的,很有可能是程式設計問題,導致記憶體的不合理利用。只有當垃圾回收觸發,未使用記憶體被釋放後,記憶體增長還在持續,我們才能確定是發生了記憶體洩漏。

隱藏的記憶體洩漏問題,大多是有觸發條件的,重現問題是需要這些條件的,所以我們在平時寫程式碼的時候,可以將一些重要環節的引數細節列印在 log 中,這樣我們在重現問題是就不會摸不著頭腦,亂試一氣。

有了引數可以用來重現問題,接下來要確定問題。我們要確定,這部分記憶體是否沒有被 GC 正確釋放。那麼問題來了,我們如何知道程式進行了垃圾回收呢?很顯然,等待並不是辦法,我們要主動。

在 Node.js 的啟動引數中,提供了暴露手動呼叫 GC 方法的引數,即 --expose-gc。我們用這個引數來啟動應用後,就可以在程式碼中呼叫 global.gc() 手動觸發垃圾回收操作。同時,使用 process.memoryUsage().heapUsed 獲取程式執行時所佔用的記憶體。如果 GC 之後,記憶體依然沒有下降,就可以確定是記憶體洩露了。

二、生成記憶體快照

既然記憶體是問題,我們就需要獲取程式執行的記憶體快照來幫助定位問題。但記憶體快照並不是隨便打得,是有一定技巧的。

我們至少要生成三次記憶體快照,才能更好的定位問題。這三次中又一次要在問題出現前生成,之後可以在問題持續的過程中生成兩次或更多。

為什麼要這樣做呢?理解起來很簡單。第一次是為了獲取正常情況下的堆疊資訊,而在問題出現後,堆疊資訊一定會發生變化,有了第一次的資訊,我們才好進行後面的比對,過濾一些無用的資訊。而後兩次的快照,用來比對某一物件的堆疊變化,來確定是否是有問題的物件。下面會詳細應用到。

三、定位問題

用 devTool 的可以忽略下面的過程:

開啟 Chrome Devtools ,進入到 Profiles 選項卡,點 Load 按鈕,載入之前生成的快照。

對於記憶體快照,有四個檢視,Summary,Comparison,Containment,Statistics,這裡面常用的是前三個。

在 Summary 檢視中,我們可以看到當前快照的全部資訊,以及多個快照之間的資訊。在列表裡顯示的都是物件的建構函式名字,可以先忽略被括號包裹的物件,優先觀察其他的物件,最後再來看他們。後面的 shallow size 表示的是物件自身的大小,retained size 表示的是物件和它依賴物件的大小,一般是 GC 不可達的。

在 Comparison 檢視中,我們可以進行多個快照之間的對比,這個用處比較大,如果我們將前兩次快照進行對比,可能比較快速的定位出問題的物件。注意觀察 New、Deleted、Delta,如果是記憶體洩漏的物件,可能是一直在 New,而沒有 Deleted。

在 Containment 檢視中,我們可以檢視整個 GC 路徑,當然一般不會用到。因為展開在 Summary 和 Comparison 列舉的每一項,都可以看到從 GC roots 到這個物件的路徑。通過這些路徑,你可以看到這個物件的控制程式碼被什麼持有,從而定位問題產生的原因。值的注意的是,其中背景色黃色的,表示這個物件在 Javascript 中還存在引用,所以可能沒有被清除。如果是紅色的,表示的是這個物件在 Javascript 中不存在引用,但是依然存活在記憶體中,一般常見於 DOM 物件,它們存放的位置和 Javascript 中物件還是有不同的,在 Node.js 中很少遇見。

更多的操作方法,可以看這個視訊 Memory Profiling with Chrome DevToolsMemory Management Masterclass。還有 Chrome 的文件 Memory Profiling(舊) 和 Memory Diagnosis(新)。講的還是很詳細的。(請自備梯子)

四、解決問題

一般在 Javascript 中存在引用而導致記憶體洩漏的情況,是比較好處理的,只需要在使用後及時的將引用釋放掉即可。

但像 《一次 Node.js 應用記憶體暴漲分析》 所存在的那種記憶體問題,是屬於底層機制的問題,如果等不了 bugfix,就只能先通過一些啟動引數來優化記憶體管理。常用的引數:

  • --max-old-space-size 限制老生區大小,可以控制記憶體佔用的最大值,即使發生洩漏,也不會讓記憶體佔用保持很高。可以根據開啟程式數以及是否同機部署來優化。
  • --gc_global 這其實是個 V8 的 debug flag,讓 GC 永遠都是 Full GC,使用上會有一定的效能損耗,根據應用複雜度不同,損耗不同。

當我們找到問題,進行修復後,重複上面的步驟,確認問題已經被解決。有時可能一次並不能解決問題,所以耐心還是很重要的。

實戰

可以在這裡下載使用到的程式碼, GitHub,進入 memory-leak 資料夾。
我們來舉個例子,應用上面的步驟排查問題,使用 leak-memory 的例子,程式碼還有另外一個例子,可以自己實踐。

這裡我們為了方便,我們使用了 devTool。

devTool leak-memory.js

然後在開啟的介面中進入記憶體快照介面,生成第一次快照。當控制檯有輸出後,間隔的生成兩次快照,結果如下。

screenshot

我們切換檢視,對比下三次快照間的區別,可以看到 Foo 這個物件一直在建立而沒有被刪除。

screenshot

screenshot

我們展開 Foo,選擇下面的一個例項,檢視它的 GC path,可以看到它一直被 neverRelease 持有引用(黃色),所以沒有被釋放,之後就可以進行問題的處理了。

screenshot

去掉 // neverRelease.splice(index, 1); 前的註釋,然後在重複上面的步驟,你會發現記憶體的變化已經正常了。

在使用 devTool 時,可以檢視執行時的 memory timeline,如果影象呈現階梯式增長,一般就是存在記憶體洩漏問題了。正常的應用曲線會類似於鋸齒,如圖:

screenshot

總結

  1. 記憶體洩漏問題的定位,經驗很重要,但有了良好工具的輔助,可以節省很多時間。如果懶得自己一步步的操作,可以接入 alinode,這個可以幫助你很方便的生成快照等執行時資料,並有一定的分析輔助,還是方便的。
  2. 你可能看到很多記憶體分析的文章會有一些圖來表示記憶體的增長,可以使用 python 來快速生成相關的圖片,使用 matplotlib.pyplot 這個包。

screenshot

參考

相關文章