Node.js 是一個基于 Chrome 的 V8 JavaScript 引擎建構的平台,用于輕松建構快速且可擴充的網絡應用程式。
Google 的 V8 ——Node.js 背後的 JavaScript 引擎, 它的性能令人難以置信,并且 Node.js 在許多用例中運作良好的原因有很多,但您總是受到堆大小的限制。 當您需要在 Node.js 應用程式中處理更多請求時,您有兩種選擇:垂直擴充或者水準擴充。 水準擴充意味着您必須運作更多并發應用程式執行個體。 如果做得好,您最終能夠滿足更多請求。 垂直擴充意味着您必須提高應用程式的記憶體使用和性能或增加應用程式執行個體可用的資源。
Node.js Memory Leak Debugging Arsenal
MEMWATCH
如果您搜尋“如何在 node.js 中查找洩漏”,您可能會找到的第一個工具是 memwatch。 原來的包早就廢棄了,不再維護。 但是,您可以在 GitHub 的存儲庫分叉清單中輕松找到它的更新版本。 這個子產品很有用,因為它可以在看到堆增長超過 5 次連續垃圾收集時發出洩漏事件。
HEAPDUMP
很棒的工具,它允許 Node.js 開發人員拍攝堆快照并在以後使用 Chrome 開發人員工具檢查它們。
NODE-INSPECTOR
甚至是 heapdump 的更有用的替代方案,因為它允許您連接配接到正在運作的應用程式,進行堆轉儲,甚至可以即時調試和重新編譯它。
Taking “node-inspector” for a Spin
不幸的是,您将無法連接配接到在 Heroku 上運作的生産應用程式,因為它不允許将信号發送到正在運作的程序。 然而,Heroku 并不是唯一的托管平台。
為了體驗 node-inspector 的實際操作,我們将使用 restify 編寫一個簡單的 Node.js 應用程式,并在其中放置一些記憶體洩漏源。 這裡所有的實驗都是用 Node.js v0.12.7 進行的,它是針對 V8 v3.28.71.19 編譯的。

當使用 –trace_gc 标志啟動 Node.js 應用程式時,會列印這些日志行:
node --trace_gc app.js
讓我們假設我們已經使用這個标志啟動了我們的 Node.js 應用程式。 在将應用程式與節點檢查器連接配接之前,我們需要将 SIGUSR1 信号發送給正在運作的程序。 如果您在叢集中運作 Node.js,請確定您連接配接到從屬程序之一。
kill -SIGUSR1 $pid # Replace $pid with the actual process ID
通過這樣做,我們使 Node.js 應用程式(準确地說是 V8)進入調試模式。 在此模式下,應用程式會使用 V8 調試協定自動打開端口 5858。
我們的下一步是運作 node-inspector,它将連接配接到正在運作的應用程式的調試界面,并在端口 8080 上打開另一個 Web 界面。
$ node-inspector
Node Inspector v0.12.2
Visit
http://127.0.0.1:8080/?ws=127.0.0.1:8080&port=5858to start debugging.
如果應用程式在生産環境中運作并且您有防火牆,我們可以通過隧道将遠端端口 8080 連接配接到本地主機:
ssh -L 8080:localhost:8080 [email protected]
現在,您可以打開 Chrome 網絡浏覽器并完全通路附加到遠端生産應用程式的 Chrome 開發工具。
Let’s Find a Leak!
V8 中的記憶體洩漏并不是我們從 C/C++ 應用程式中知道的真正的記憶體洩漏。 在 JavaScript 中,變量不會成為 void,它們隻會被“遺忘”。 我們的目标是找到這些被開發人員遺忘的變量。
在 Chrome 開發者工具中,我們可以通路多個分析器。 我們對記錄堆配置設定特别感興趣,它會随着時間的推移運作并拍攝多個堆快照。 這讓我們可以清楚地看到哪些對象正在洩漏。
開始記錄堆配置設定,讓我們使用 Apache Benchmark 在我們的首頁上模拟 50 個并發使用者。
V8堆分為幾個不同的空間:
new space:這個空間比較小,大小在1MB到8MB之間。 大多數對象都在這裡配置設定。
old pointer space:具有可能具有指向其他對象的指針的對象。 如果對象在新空間中存活的時間足夠長,它就會被提升到舊指針空間。
old data space:僅包含原始資料,如字元串、裝箱數字和未裝箱雙精度數組。 在新空間中在 GC 中存活足夠長時間的對象也被移動到這裡。
large object space:在此空間中建立太大而無法放入其他空間的對象。 每個對象在記憶體中都有自己的 mmap 區域
code space:包含由 JIT 編譯器生成的彙編代碼。
Cell space, property cell space, map space:該空間包含單元格、屬性單元格和地圖。 這用于簡化垃圾收集。
每個空間由頁面組成。頁面是從作業系統使用 mmap 配置設定的記憶體區域。除了大對象空間中的頁面外,每個頁面的大小始終為 1MB。
V8 有兩個内置的垃圾收集機制:Scavenge、Mark-Sweep 和 Mark-Compact。
Scavenge 是一種非常快速的垃圾收集技術,可以處理 New Space 中的對象。 Scavenge 是切尼算法的實作。這個想法很簡單,New Space 被分成兩個相等的半空間:To-Space 和 From-Space。當 To-Space 已滿時,會發生 Scavenge GC。它隻是交換 To 和 From 空間并将所有活動對象複制到 To-Space 或将它們提升到舊空間之一,如果它們在兩次清除中幸存下來,然後從空間中完全删除。清理速度非常快,但是它們具有保持雙倍大小的堆和不斷在記憶體中複制對象的開銷。使用清除的原因是因為大多數對象都很年輕。
Mark-Sweep 和 Mark-Compact 是 V8 中使用的另一種類型的垃圾收集器。另一個名稱是 full garbage collector. 它标記所有活動節點,然後清除所有死節點并整理記憶體碎片。
GC Performance and Debugging Tips
雖然對于 Web 應用程式來說,高性能可能不是什麼大問題,但您仍然希望不惜一切代價避免洩漏。 在 full GC 的标記階段,應用程式實際上會暫停,直到垃圾收集完成。 這意味着堆中的對象越多,執行 GC 所需的時間就越長,使用者等待的時間也就越長。
ALWAYS GIVE NAMES TO CLOSURES AND FUNCTIONS
當所有閉包和函數都有名稱時,檢查堆棧跟蹤和堆會容易得多。
當 x(a,b) 第一次運作時,V8 建立了一個單态 IC。 當您第二次調用 x 時,V8 會擦除舊 IC 并建立一個新的多态 IC,該 IC 支援整數和字元串兩種類型的操作數。 當您第三次調用 IC 時,V8 重複相同的過程并建立另一個級别為 3 的多态 IC。
但是,有一個限制。 在 IC 級别達到 5(可以使用 –max_inlining_levels 标志更改)後,該函數變得超态,不再被認為是可優化的。
直覺上可以了解,單态函數運作速度最快,記憶體占用也更小。
DON’T ADD LARGE FILES TO MEMORY
這是顯而易見的,也是衆所周知的。 如果您有大檔案要處理,例如一個大 CSV 檔案,請逐行讀取并以小塊處理,而不是将整個檔案加載到記憶體中。 在極少數情況下,單行 csv 會大于 1mb,是以您可以将其放入新空間。
DO NOT BLOCK MAIN SERVER THREAD
如果您有一些需要一些時間來處理的熱門 API,例如調整圖像大小的 API,請将其移至單獨的線程或将其轉換為背景作業。 CPU 密集型操作會阻塞主線程,迫使所有其他客戶等待并繼續發送請求。 未處理的請求資料會堆積在記憶體中,進而迫使 full GC 需要更長的時間才能完成。
DO NOT CREATE UNNECESSARY DATA
我曾經對restify有過奇怪的經曆。 如果您向無效 URL 發送數十萬個請求,那麼應用程式記憶體将迅速增長到數百兆位元組,直到幾秒鐘後完全 GC 啟動,此時一切都會恢複正常。 事實證明,對于每個無效的 URL,restify 會生成一個新的錯誤對象,其中包含長堆棧跟蹤。 這迫使新建立的對象在大對象空間而不是新空間中配置設定。
在開發過程中通路這些資料可能非常有幫助,但在生産中顯然不需要。 是以規則很簡單——除非您确實需要,否則不要生成資料。
總結
了解 V8 的垃圾收集和代碼優化器的工作原理是提高應用程式性能的關鍵。 V8 将 JavaScript 編譯為原生程式集,在某些情況下,編寫良好的代碼可以獲得與 GCC 編譯的應用程式相當的性能。