Node.js 調試 GC 以及內存暴漲的分析

2021-03-02 Node全棧

Node.js 調試 GC 以及內存暴漲的分析

Posted Mar.26, 2013 under JavaScript, Web 開發 by Bruce Dou

最近做的伺服器端組件大部分都在使用 Node.js 。因為 Node.js 庫管理模式比較先進,並且依託於 Github 的流行,Node.js 開源的庫非常多,一般所需要的第三方庫都可以找到。雖然這些庫有很多明顯的 Bug 但是比從零自己開發要快很多。對於伺服器端開發,Node.js 還是個不錯的選擇,不像 Erlang 更接近底層,業務層面的庫相對要少很多。

最近寫的一個功能在本地開發的時候沒有明顯問題,但是到真實環境測試的時候發現內存不斷增長,並且增長很快,同時 CPU 佔用也很高,接近單核心的 100% 。這對於一個大部分都是 IO 操作的進程顯然是有問題的。所以嘗試分析內存和 CPU 異常的原因。最終發現是因為生產者和消費者速度差異引起的緩衝區暴增。在 MySQL 連接對象的 Queue 中積壓了大量的 Query,而不是內存洩漏。

查看 Node.js 進程的 GC log:

node --trace_gc --trace_gc_verbose test.js


關於 Node.js 的 GC

Node.js 的 GC 方式為分代 GC (Generational GC)。對象的生命周期由它的大小決定。對象首先進入佔用空間很少的 new space (8MB)。大部分對象會很快失效,會頻繁而且快速執行 Young GC (scavenging)*直接*回收這些少量內存。假如有些對象在一段時間內不能被回收,則進入 old space (64-128KB chunks of 8KB pages)。這個區域則執行不頻繁的 Old GC/Full GC (mark-sweep, compact or not),並且耗時比較長。(Node.js 的 GC 有兩類:Young GC: 頻繁的小量的回收;Old GC: 長時間存在的數據)

Node.js 最新增量 GC 方式雖然不能降低總的 GC 時間,但是避免了過大的停頓,一般大停頓也限制在了幾十 ms 。


為了減少 Full GC 的停頓,可以限制 new space 的大小

--max-new-space-size=1024 (單位為 KB)

手動在代碼中操作 GC (不推薦)

node --expose-gc test.js

修改 Node.js 默認 heap 大小

node --max-old-space-size=2048 test.js (單位為 MB)

Dump 出 heap 的內容到 Chrome 分析:

安裝庫

https://github.com/bnoordhuis/node-heapdump

在應用的開始位置添加

var heapdump = require('heapdump');

在進程運行一小段時間後執行:

kill -USR2 <pid>

這時候就會在當前目錄下生成 heapdump-xxxxxxx.heapsnapshoot 文件。
將這個文件 Down 下來,打開 Chrome 開發者工具中的 Profiles,將這個文件加載進去,就可以看到當前 Node.js heap 中的內容了。


可以看到有很多 MySQL 的 Query 堆積在處理隊列中。內存暴漲的原因應該是 MySQL 的處理速度過慢,而 Query 產生速度過快。
所以解決方式很簡單,降低 Query 的產生速度。內存暴漲還會引起 GC 持續執行,佔用了大量 CPU 資源。

node-mysql 庫中的相關代碼,其實應該限制 _queue 的 size,size 過大則拋出異常或者阻塞,就不會將錯誤擴大。

Protocol.prototype._enqueue = function(sequence) {
if (!this._validateEnqueue(sequence)) {
return sequence; } this._queue.push(sequence); var self = this; sequence .on('error', function(err) { self._delegateError(err, sequence); }) .on('packet', function(packet) { self._emitPacket(packet); }) .on('end', function() { self._dequeue(); }); if (this._queue.length === 1) { this._parser.resetPacketNumber(); sequence.start(); } return sequence;};

在不修改 node-mysql 的情況下,加入生產者和消費者的同步,調整之後,內存不再增長,一直保持在不到 100M 左右,CPU 也降低到 10% 左右。

Node.js 調試工具 node-inspector

安裝:

npm install -g node-inspector

啟動自己的程序:

node --debug test.jsnode --debug-brk test.js (在代碼第一行加斷點)

啟動調試器界面:

node-inspector

打開 http://localhost:8080/debug?port=5858 可以看到執行到第一行的斷點。
右邊為局部變量和全局變量、調用棧和常見的斷點調試按鈕,查看程序步進執行情況。並且你可以修改正在執行的代碼,比如在關鍵的位置增加 console.log 列印信息。

Node.js 命令行調試工具

以 DEBUG 模式啟動 Node.js 程序,類似於 GDB:

node debug test.jsdebug> helpCommands: run (r), cont (c), next (n), step (s), out (o), backtrace (bt), setBreakpoint (sb), clearBreakpoint (cb),watch, unwatch, watchers, repl, restart, kill, list, scripts, breakOnException, breakpoints, version

Node.js 其他常用命令參數

node --max-stack-size 設置棧大小node --v8-options 列印 V8 相關命令node --trace-opt test.jsnode --trace-bailout test.js 查找不能被優化的函數,重寫node --trace-deopt test.js 查找不能優化的函數

Node.js 的 Profiling

V8 自帶的 prof 功能:

npm install profilernode --prof test.js

會在當前文件夾下生成 v8.log

安裝 v8.log 轉換工具

sudo npm install tick -g

在當前目錄下執行

node-tick-processor v8.log

可以關注其中 Javascript 各個函數的消耗和 GC 部分

[JavaScript]:ticks total nonlib name67 18.7% 20.1% LazyCompile: *makeF /opt/data/app/test/test.js:662 17.3% 18.6% Function: ~ /opt/data/app/test/test.js:942 11.7% 12.6% Stub: FastNewClosureStub38 10.6% 11.4% LazyCompile: * /opt/data/app/test/test.js:1[GC]:ticks total nonlib name27 7.5%

參考以及一些有用的連結

https://bugzilla.mozilla.org/show_bug.cgi?id=634503

http://cs.au.dk/~jmi/VM/GC.pdf
http://lifecs.likai.org/2010/02/how-generational-garbage-collector.html
http://en.wikipedia.org/wiki/Garbage_collection_(computer_science)#Na.C3.AFve_mark-and-sweep
http://en.wikipedia.org/wiki/Cheney

http://en.wikipedia.org/wiki/Cheney’s_algorithm

https://github.com/bnoordhuis/node-heapdump

http://mrale.ph/blog/2011/12/18/v8-optimization-checklist.html
http://es5.github.com/
http://blog.caustik.com/2012/04/08/scaling-node-js-to-100k-concurrent-connections/
https://hacks.mozilla.org/2013/01/building-a-node-js-server-that-wont-melt-a-node-js-holiday-season-part-5/
https://gist.github.com/2000999
http://www.jiangmiao.org/blog/2247.html
http://blog.caustik.com/2012/04/08/scaling-node-js-to-100k-concurrent-connections/
http://blog.caustik.com/2012/04/11/escape-the-1-4gb-v8-heap-limit-in-node-js/
https://developers.google.com/v8/embed#handles
https://hacks.mozilla.org/2012/11/fully-loaded-node-a-node-js-holiday-season-part-2/

https://code.google.com/p/v8/wiki/V8Profiler

歡迎關注我的公眾號【node全棧】


相關焦點

  • 記一次 Node.js 應用內存暴漲分析
    在使用 vm 模塊時,使用姿勢錯誤,導致內存佔用無法釋放,使內存佔用暴漲。第一個問題我們今天不予討論,主要來說一下第二個問題。VM(Virtual Machine) 模塊我們就先了解下 VM 這個模塊。
  • Node.js 診斷指南 第二彈
    node.js 啟動 flag。在啟動 Node.js 應用的時候指定 --abort-on-uncaught-exception 來開啟程序觸發未捕獲的異常時自動 core dump 操作。gcore <pid>。
  • Node.js運維問題(一)
    我們項目組整體的技術架構是node.js+java。就後端Java而言,16年Spring Cloud開始出現苗頭,採用了微服務技術。就前端node.js而言,我們還是出於提升前端技術水準的目的,大膽的將node.js用來做前端的伺服器,用於適配後端各種接口。本文出於對node.js的運維問題介紹,就不在本文中討論這樣的架構以及業務特點或者適用場景等等。
  • 寫 Node.js 代碼,從學會調試開始
    在當前 Node.js v15 版本下,以前非常多的調試方式已經失效了,Node.js 傳統的調試協議也進行了許多升級,我們按照最新的方式,來告訴你如何調試。為什麼要使用調試眾所周知,代碼是寫(調)出來的,而不是猜出來的。
  • Node.js 內存管理和 V8 垃圾回收機制
    >以下代碼中 --expose-gc 參數表示允許手動執行垃圾回收機制,將 banana 對象賦為 null 後進行 GC,在第三個 print 列印出的結果可以看到 heapUsed 的使用已經從 164.24 MB 降到了 3.97 MB$ node --expose-gc example.js{"rss":"19.95 MB","heapTotal"
  • 如何調試你的 Node.js 代碼?
    本文經前端雜貨鋪(id: FEGopal)授權轉載如若轉載請聯繫原公眾號很多時候,我苦惱於 Node.js 的調試,只會使用 console.log 這種帶有侵入性的方法,但是其實 Node.js 也可以做到跟瀏覽器調試一樣的方便。
  • 讓你 nodejs 水平暴增的 debugger 技巧
    我覺得學習 nodejs 除了要掌握基礎的 api、常用的一些包外,最重要的能力是學會使用 debugger。因為當流程複雜的時候,斷點調試能夠幫你更好的理清邏輯,有 bug 的時候也能更快的定位問題。狼叔說過,是否會使用 debugger 是區分一個程式設計師 nodejs 水平的重要標誌。
  • 讓你 Node.js 水平暴增的 debugger 技巧
    我覺得學習 nodejs 除了要掌握基礎的 api、常用的一些包外,最重要的能力是學會使用 debugger。因為當流程複雜的時候,斷點調試能夠幫你更好的理清邏輯,有 bug 的時候也能更快的定位問題。狼叔說過,是否會使用 debugger 是區分一個程式設計師 nodejs 水平的重要標誌。
  • Node 調試工具入門教程
    $ node --inspect app.js上面代碼中,--inspect參數是啟動調試模式必需的。這時,打開瀏覽器訪問http://127.0.0.1:3000,就可以看到 Hello World 了。
  • 結合源碼分析 Node.js 模塊加載與運行原理
    Node.js 源碼結構一覽這裡使用 Node.js 6.x 版本源碼為例子來做分析。下文我們在分析模塊的 require 的時候,也會來到 lib/module.js 中,也會分析到 Module._load。因此我們可以看出,Node.js 啟動一個文件的過程,其實到最後,也是 require 一個文件的過程,可以理解為是立即 require 一個文件。下面就來分析 require 的原理。4.
  • 一個由 Node.js vm 引發的 OOM 血案
    /mp.weixin.qq.com/s/5W0DPXU0-S9Bw4Yi2Hxe8A大家在用 Node.js 的 vm 時,可千萬小心。有時候補了這裡可能又漏了那裡。尤其是頻繁新建 vm 的時候,例如來一個請求,組合一段代碼,放進 vm 中執行。Talk is Cheap, Show Me the Code先上一段最小復現代碼。注意:在異步 while 中每次循環的末尾都手動調用一次 gc() 函數。
  • Node.js 診斷指南 第一彈
    ('http') , name = 'My App';// fake appdebug('booting %o', name);http.createServer(function(req, res){ debug(req.method + ' ' + req.url); res.end('hello\n');})
  • Node.js 12:服務端JavaScript的未來
    Node.js 12 發布開始,JS 堆大小將根據可用內存來自動配置,這樣 Node 就不會嘗試使用比可用內存更多的內存空間了,避免了耗盡內存導致進程被迫終止的問題。新版在處理大量數據時不會再出現內存不足錯誤——以前經常會出現這種問題。需要的話,開發者還可以使用舊的 --max-old-space-size 標誌手動設置上限,但新功能出現後手動設置大小的需求應該就沒那麼多了。
  • Nodejs Stream pipe 的使用與實現原理分析
    1.1 未使用 Stream pipe 情況在 Nodejs 中 I/O 操作都是異步的,先用 util 模塊的 promisify 方法將 fs.readFile 的 callback 形式轉為 Promise 形式,這塊代碼看似沒問題,但是它的體驗不是很好,因為它是將數據一次性讀入內存再進行的返回,當數據文件很大的時候也是對內存的一種消耗
  • Node.js:10個最有用和有趣的新功能
    當 Node.js 以 --inspect 命令後參數(帶可選的埠號)運行的時候,控制臺會列印出chrome-devtools://。在 Chrome 瀏覽器中輸入這個連結,就會直接在進程中發起一個遠程調試連接。
  • 走進Node.js 之 HTTP實現分析
    既然 Node.js 的強項是處理網絡請求,那我們就來分析一個 HTTP 請求在 Node.js 中是怎麼被處理的,以及 JavaScript 在這個過程中引入的開銷到底有多大。Node.js 採用的網絡請求處理模型是 IO 多路復用。
  • 一秒鐘在 VS Code 中調試 Node
    算了,還是說一句,以下兩個技巧真的是又簡單又實用,投資回報率高達666%最簡單的調試方式: Run Current File由於 VSCode 內置 Node 調試器,調試 Node 極其簡單,遵循以下步驟Run Current File在 ToolBar 中找到 Run And Debug 按鈕並點擊,或直接 <
  • Node.js VS 瀏覽器以及事件循環機制
    原文轉自:Node.js VS 瀏覽器以及事件循環機制 https://juejin.im/post/6871832597891121166本文主要梳理node.js,瀏覽器相關及Event Loop事件循環等,會持續補充更新哦!首先我們要記住JS是一個單線程的語言。
  • Node.js應用實戰和原理解析
    說到便利,對於node.js還必須要提到NPM,在上個demo中我們用到var http = require("http"); 來引用模塊,我們也說到了http是node自身的模塊,那麼如果需要引用一些node的非自身模塊,這個任務的第一步就交給NPM,在安裝node的同時已經將NPM工具安裝完成,我們只需要執行NPM -v就能查看NPM的版本,直接輸入NPM可以查看相關幫助
  • Node.js前世今生
    特性我們使用node可能聽到最多的幾個特性就是3.1 單線程Node JS的應用程式,是使用單線程以及事件循環體系結構組合,來處理客戶端請求的,但實際上我們要明白不要誤解的一點它只是主事件循環是單線程的,其他的比如I / O處理相關的操作是有單獨的線程去處理