Node.js 应用的内存泄漏问题的检测方法
Debugging Memory Leaks in Node.js Applications
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 編譯的。
var restify = require('restify');var server = restify.createServer();var tasks = [];server.pre(function(req, res, next) {tasks.push(function() {return req.headers;});// Synchronously get user from session, maybe jwt tokenreq.user = {id: 1,username: 'Leaky Master',};return next(); });server.get('/', function(req, res, next) {res.send('Hi ' + req.user.username);return next(); });server.listen(3000, function() {console.log('%s listening at %s', server.name, server.url); });這里的應用很簡單,有很明顯的泄露。 陣列任務會隨著應用程序生命周期的增長而增長,導致它變慢并最終崩潰。 問題是我們不僅泄漏了閉包,還泄漏了整個請求對象。
V8 中的 GC 使用 stop-the-world 策略,因此這意味著內存中的對象越多,收集垃圾所需的時間就越長。 在下面的日志中,您可以清楚地看到,在應用程序生命周期開始時,收集垃圾平均需要 20 毫秒,但幾十萬個請求之后需要大約 230 毫秒。 由于 GC,試圖訪問我們應用程序的人現在必須等待 230 毫秒。 您還可以看到每隔幾秒就會調用一次 GC,這意味著每隔幾秒用戶就會在訪問我們的應用程序時遇到問題。 延遲會越來越大,直到應用程序崩潰。
當使用 –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=5858 to start debugging.
如果應用程序在生產環境中運行并且您有防火墻,我們可以通過隧道將遠程端口 8080 連接到本地主機:
ssh -L 8080:localhost:8080 admin@example.com
現在,您可以打開 Chrome 網絡瀏覽器并完全訪問附加到遠程生產應用程序的 Chrome 開發工具。
Let’s Find a Leak!
V8 中的內存泄漏并不是我們從 C/C++ 應用程序中知道的真正的內存泄漏。 在 JavaScript 中,變量不會成為 void,它們只會被“遺忘”。 我們的目標是找到這些被開發人員遺忘的變量。
在 Chrome 開發者工具中,我們可以訪問多個分析器。 我們對記錄堆分配特別感興趣,它會隨著時間的推移運行并拍攝多個堆快照。 這讓我們可以清楚地看到哪些對象正在泄漏。
開始記錄堆分配,讓我們使用 Apache Benchmark 在我們的主頁上模擬 50 個并發用戶。
ab -c 50 -n 1000000 -k http://example.com/
在拍攝新快照之前,V8 會執行標記-清除垃圾收集,所以我們肯定知道快照中沒有舊垃圾。
Fixing the Leak on the Fly
在 3 分鐘內收集堆分配快照后,我們最終得到如下結果:
我們可以清楚地看到,堆中有一些巨大的數組,還有很多 IncomingMessage、ReadableState、ServerResponse 和 Domain 對象。讓我們嘗試分析泄漏的來源。
在圖表上從 20 秒到 40 秒選擇堆差異后,我們只會看到從您啟動分析器時起 20 秒后添加的對象。這樣您就可以排除所有正常數據。
記下系統中每種類型的對象有多少,我們將過濾器從 20 秒擴展到 1 分鐘。我們可以看到,已經相當龐大的陣列還在不斷增長。在“(array)”下我們可以看到有很多等距的對象“(object properties)”。這些對象是我們內存泄漏的源頭。
我們也可以看到“(閉包)”對象也在快速增長。
查看字符串也可能很方便。在字符串列表下有很多“Hi Leaky Master”短語。這些也可能給我們一些線索。
在我們的例子中,我們知道字符串“Hi Leaky Master”只能在“GET /”路由下組裝。
如果您打開保留器路徑,您將看到此字符串以某種方式通過 req 引用,然后創建了上下文并將所有這些添加到一些巨大的閉包數組中。
所以在這一點上我們知道我們有某種巨大的閉包數組。 讓我們在“源”選項卡下實時為所有閉包命名。
完成代碼編輯后,我們可以按 CTRL+S 來保存和重新編譯代碼!
現在讓我們記錄另一個堆分配快照,看看哪些閉包正在占用內存。
很明顯 SomeKindOfClojure() 是我們的 target。 現在我們可以看到 SomeKindOfClojure() 閉包被添加到全局空間中一些名為任務的數組中。
很容易看出這個數組是沒有用的。 我們可以注釋掉。 但是我們如何釋放已經占用的內存呢? 很簡單,我們只需為任務分配一個空數組,下一次請求時它將被覆蓋并在下一次 GC 事件后釋放內存。
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
當所有閉包和函數都有名稱時,檢查堆棧跟蹤和堆會容易得多。
db.query('GIVE THEM ALL', function GiveThemAllAName(error, data) {... })AVOID LARGE OBJECTS IN HOT FUNCTIONS
理想情況下,您希望避免在 hot function 內部使用大對象,以便所有數據都適合新空間。 所有 CPU 和內存綁定操作都應在后臺執行。 還要避免 hot function 的去優化觸發器,優化的 hot function 比未優化的 hot function 使用更少的內存。
AVOID POLYMORPHISM FOR IC’S IN HOT FUNCTIONS
內聯緩存 ( Inline Caches ) 用于通過緩存對象屬性訪問 obj.key 或某些簡單函數來加速某些代碼塊的執行。
function x(a, b) {return a + b; }x(1, 2); // monomorphic x(1, “string”); // polymorphic, level 2 x(3.14, 1); // polymorphic, level 3當 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 編譯的應用程序相當的性能。
更多Jerry的原創文章,盡在:“汪子熙”:
總結
以上是生活随笔為你收集整理的Node.js 应用的内存泄漏问题的检测方法的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 李彦宏:有人说百度错失了 ChatGPT
- 下一篇: 字节跳动创始人张一鸣再捐 2 亿元,支持