鬼影追蹤 —— 發現 Node.js 中的記憶體洩漏

2016-09-20    分類:WEB開發、程式設計開發、首頁精華0人評論發表於2016-09-20

本文由碼農網 – 任琦磊原創翻譯,轉載請看清文末的轉載要求,歡迎參與我們的付費投稿計劃

發現 Node.js 的記憶體洩露可能是一個不小的挑戰 —— 最近我們就有這樣一個經驗可以拿來分享。

我們客戶的一個微服務產生了如下圖所示的記憶體使用情況:

通過 Trace by RisingStack(一款 Node.js 效能監控和除錯工具)抓取到的記憶體使用情況

你可能會花費幾天的時間在這類東西上:剖析應用來查詢根源問題。本文中,我會來總結一下你能夠使用什麼工具以及如何使用他們,所以希望你能從中有所收穫。

“太長,勿看(TL;DR)”版本

在我們這個特定的情況中,服務正在一臺僅有 512MB 的小例項上執行。事實上,應用並沒有洩露任何記憶體,而是 GC 甚至沒有開始回收已取消引用的物件。

這為什麼會發生呢? 預設地,Node.js 會嘗試使用大約 1.5GB 的記憶體。因此當在記憶體比這小的系統上執行時,很有必要覆寫該記憶體使用預設值,因為垃圾回收是一個很消耗資源的操作。

它的解決方案是在 Node.js 執行時新增一個額外的引數:

node --max_old_space_size=400 server.js --production

可是,如果情況沒有像上例這樣明顯的話,你又如何來發現記憶體洩漏的問題呢?

理解 V8 的記憶體處理

在我們深入研究你能夠用來尋找和修復 Node.js 應用中記憶體洩漏問題的技術前,讓我們先來看一下 V8 是如何處理記憶體的。

定義

  • 常住集大小(resident set size): 是記憶體中被程式佔用並保留的 RAM 部分,包括:
    • 程式碼本身
  • 棧(stack): 包含基本型別(primitive types)和物件的引用(references to objects)
  • 堆(heap): 儲存引用型別(reference types),如物件,字串或閉包
  • 物件的直接佔用記憶體(shallow size of an object): 物件本身自己直接佔用的記憶體空間
  • 物件的佔用總記憶體(retained size of an object): 當物件與其關聯物件一起被刪除時釋放出的記憶體空間

垃圾回收器是如何工作的

垃圾回收是將應用已不再使用的物件佔用的記憶體進行回收的過程。通常來說,記憶體的分配相當容易,但當記憶體池(memory pool)已經耗盡需要回收記憶體時卻相當困難。

當根節點不可達某個物件時,它便進入了垃圾回收的候選名單了,所以不要被根物件或其它任意有效物件引用。根物件可以是全域性物件,DOM 元素或區域性變數。

堆兩個主要的區段,新生代空間(New Space)和老生代空間(Old Space)。新生代空間用於新的記憶體分配,一般在約為 1-8MB 左右,所以這裡的垃圾回收很快。在新生代空間中的物件被稱為新生代(Young Generation)。老生代空間則存放那些免於回收從新生代空間晉升至此的物件——它們被稱為老生代(Old Generation)。老生代空間分配記憶體很方便但回收卻很困難,所以垃圾回收很少在這裡執行。

垃圾回收為什麼會變得如此困難? V8 JavaScript 引擎採用了“停止一切(stop-the-world)”垃圾回收器機制。實際使用中,這意味著垃圾回收處理過程中程式會停止執行。

通常,約 20% 的新生代會留下來進入老生代。只有到了記憶體耗盡的時候才會開始回收老生代空間的記憶體。這些 V8 引擎是通過使用兩種不同的回收演算法來實現的:

  • Scavenge 回收,快速且執行在新生代的回收上。
  • Mark-Sweep 回收,較慢且執行在老生代的回收上。

更多關於這如何工作的資訊可以參考文章 V8 之旅:垃圾回收。更過關於整體記憶體管理的資訊,可訪問記憶體管理參考

尋找 Node.js 記憶體洩漏時你可以使用的工具/技術

heapdump 模組

你可以通過使用 heapdump 模組來建立一個堆的快照以便日後檢查。把它新增到你的專案很簡單:

npm install heapdump --save

然後在你的進入點(entry point)只要新增:

var heapdump = require('heapdump');

當你完成了上述操作,你就可以開始收集 heapdump 了,你可以通過使用命令 $ kill -USR2 <pid>,或者通過呼叫:

heapdump.writeSnapshot(function(err, filename) {  
  console.log('dump written to', filename);
});

一旦你獲取了你的快照,就是時候讓它們發光發熱了。你最好確保你捕獲了不同時間的多個快照,這樣你就可以將它們進行比較了。

谷歌 Chome 開發者工具

首先你需要將你的記憶體快照載入進 Chrome 分析器。方法為:開啟 Chrome 開發者工具,進入 Profiles,然後 載入 你的堆快照。

當你完成載入後,它應該看起來像這樣:

到目前為止一切正常,但這個截圖裡到底能看出些什麼東西呢?

這裡需要注意的一個重要的事情是已選中的檢視視窗:Comparison。這個模式允許你來比較兩個(或多個)不同時間獲取的堆快照,所以你能夠準確地找出哪些物件分配到了記憶體,與此同時哪個的沒有釋放。

另一個重要的標籤是 Retainers。它用來展示到底為什麼一個物件不可以被垃圾回收掉,是什麼仍然引用著它。這種情況下,全域性變數 log會保持一個到這個物件本身的引用,以防止垃圾回收器釋放其資源。

底層工具

mdb

mdb 工具是一個用於對作業系統,系統故障轉儲,使用者程式,程式資訊轉儲和物件檔案底層的除錯和編輯的可擴充套件工具。

gcore

生成正在執行中的程式的資訊轉儲,包括程式 ID pid。

放在一起

首先我們需要建立一個轉儲用來研究。你可以簡單地實現:

gcore `pgrep node`

在你獲得之後,你可以通過下面的命令搜尋堆中全部的 JS 物件:

> ::findjsobjects

當然,你需要獲取連續的資訊轉儲來比較轉儲間的不同。

一旦你發現了可疑的物件,你可以這樣分析它們:

object_id::jsprint

現在你所需要做的便是尋找物件(根節點)的持有者(retainer)。

object_id::findjsobjects -r

這個命令將會返回持有者的 id。然後你可以再次使用 ::jsprint 來分析這些持有者。

你可以通過觀看來自 Netflix 的 Yunong Xiao 的講座來了解關於如何使用它的更詳細的版本。

分享視訊地址(YouTube,需自備牆梯)

推薦閱讀

你有關於 Node.js 記憶體洩漏更多的想法或見解嗎?在評論中分享一下吧。

譯文連結:http://www.codeceo.com/article/nodejs-memory-leak.html
英文原文:Hunting a Ghost - Finding a Memory Leak in Node.js
翻譯作者:碼農網 – 任琦磊
轉載必須在正文中標註並保留原文連結、譯文連結和譯者等資訊。]

相關文章