Node調試指南-內存篇

主要摘自: - Node.js 調試指南 Node 案發現場揭祕前端

Node.js 發展到今天,已經被愈來愈普遍地應用到 BFF 先後端分離全棧開發客戶端工具 等領域。然而,相對於應用層的蓬勃發展,其 Runtime 對於絕大部分前端出身的開發者來講,處於黑盒的狀態,這一點並無獲得很好的改善,從而也阻礙了 Node.js 在業務中的應用和推廣。node

內存泄漏問題

  • 對於緩慢上漲最終 OOM 這種類型的內存泄漏,咱們有充足的時間去抓 Heapsnapshot,進而分析堆快照來定位泄漏點。(可參見以前的文章 『Node 案發現場揭祕 —— 快速定位線上內存泄漏』 )git

  • 對於諸如 while 循環跳出條件失敗 、 長正則執行致使進程假死 、以及 因爲異常請求致使應用短期內 OOM 的狀況,每每來不及抓取 Heapsnapshot,一直沒有特別好的辦法進行處理。github

生成 Coredump 文件有兩種方式express

  • 當咱們的應用意外崩潰終止時,操做系統將自動記錄。 這種方式通常用於 「死後驗屍」,用於分析由雪崩觸發 OOM,來對出現未捕獲的異常時也進行自動 Core dump。

這裏須要注意的是,這是一個並無那麼安全的操做:線上通常會 pm2 等具有自動重啓功能的守護工具進行進程守護,這意味着若是咱們的程序在某些狀況下頻繁 crash 和重啓,那麼會生成大量的 Coredump 文件,甚至可能會將服務器磁盤寫滿。因此開啓這個選項後,請務必記得對服務器磁盤進行監控和告警。npm

  • 手動調用 gcore <pid> 的方式來手動生成。 這種方式通常用於 「活體檢驗」,用於 Node.js 進程假死狀態 下的問題定位。

本文將介紹幾種Node調試內存指南編程

1 gcore + llnode

1.1 Core & Core Dump

在開始以前,咱們先了解下什麼是 Core 和 Core Dump。json

什麼是 Core?後端

在使用半導體做爲內存材料前,人類是利用線圈看成內存的材料,線圈就叫做 core ,用線圈作的內存就叫做 core memory。現在 ,半導體工業澎勃發展,已經沒有人用 core memory 了,不過在許多狀況下, 人們仍是把記憶體叫做 core 。數組

什麼是 Core Dump?

當程序運行的過程當中異常終止或崩潰,操做系統會將程序當時的內存狀態記錄下來,保存在一個文件中,這種行爲就叫作 Core Dump(中文有的翻譯成 「核心轉儲」)。咱們能夠認爲 Core Dump 是 「內存快照」,但實際上,除了內存信息以外,還有些關鍵的程序運行狀態也會同時 dump 下來,例如寄存器信息(包括程序指針、棧指針等)、內存管理信息、其餘處理器和操做系統狀態和信息。Core Dump 對於編程人員診斷和調試程序是很是有幫助的,由於對於有些程序錯誤是很難重現的,例如指針異常,而 Core Dump 文件能夠再現程序出錯時的情景。

1.2 測試環境

$ uname -a
Darwin xiaopinguodeMBP 16.7.0 Darwin Kernel Version 16.7.0: Wed Oct 10 20:06:00 PDT 2018; root:xnu-3789.73.24~1/RELEASE_X86_64 x86_64
複製代碼

1.3 開啓 Core Dump

在終端中輸入:

$ ulimit -c
複製代碼

查看容許 Core Dump 生成的文件的大小,若是是 0 則表示關閉了 Core Dump。使用如下命令開啓 Core Dump 功能,而且不限制 Core Dump 生成的文件大小:

$ ulimit -c unlimited
複製代碼

以上命令只針對當前終端環境有效,若是想永久生效,須要修改 /etc/security/limits.conf 文件,以下:

1.4 gcore

使用 gcore 能夠不重啓程序而 dump 出特定進程的 core 文件。gcore 使用方法以下:

$ gcore [-o filename] pid
# 用法以下
$gcore
gcore: no pid specified
usage:
        gcore [-s] [-v] [[-o file] | [-c pathfmt ]] [-b size] pid
複製代碼

Core Dump 時,默認會在執行 gcore 命令的目錄生成 core.pid 的文件。

1.5 llnode

什麼是 llnode?

Node.js v4.x+ C++ plugin for LLDB - a next generation, high-performance debugger.

什麼是 LLDB?

LLDB is a next generation, high-performance debugger. It is built as a set of reusable components which highly leverage existing libraries in the larger LLVM Project, such as the Clang expression parser and LLVM disassembler.

安裝 llnode + lldb:

github.com/nodejs/llno…

# Prerequisites: Install LLDB and its Library
brew update && brew install --with-lldb --with-toolchain llvm
# instal
npm install -g llnode
複製代碼

1.6 測試內存實例

下面用一個典型的全局變量緩存致使的內存泄漏的例子來測試 llnode 的用法。代碼以下:

const leaks = []
function LeakingClass() {
  this.name = Math.random().toString(36)
  this.age = Math.floor(Math.random() * 100)
}
setInterval(() => {
  for (let i = 0; i < 100; i++) {
    leaks.push(new LeakingClass)
  }
  console.warn('Leaks: %d', leaks.length)
}, 1000)
複製代碼

運行該程序:

$ node app.js
複製代碼

等待幾秒,打開另外一個終端運行 gcore:

$ ulimit -c unlimited
$ pgrep -n node
$ 33833
$ sudo gcore -c core.33833  33833
複製代碼

生成 core.33833 文件。

1.7 分析 Core 文件

使用 lldb 加載剛纔生成的 Core 文件:

llnode -c ./core.33833 
(lldb) target create --core "./core.33833"
Core file '/Users/xiaopingguo/repos/my_repos/node_repos/node-in-debugging/./core.33833' (x86_64) was loaded.
(lldb) plugin load '/usr/local/lib/node_modules/llnode/llnode.dylib'
複製代碼

輸入 v8 查看使用文檔,有如下幾條命令:

v8
The following subcommands are supported:
      bt                -- Show a backtrace with node.js JavaScript functions and their args. An optional argument is accepted; if that argument is a number, it
                           specifies the number of frames to display. Otherwise all frames will be dumped.
                           Syntax: v8 bt [number]
      findjsinstances   -- List every object with the specified type name.
                           Flags:
                           * -v, --verbose                  - display detailed `v8 inspect` output for each object.
                           * -n <num>  --output-limit <num> - limit the number of entries displayed to `num` (use 0 to show all). To get next page repeat
                           command or press [ENTER].
                           Accepts the same options as `v8 inspect`
      findjsobjects     -- List all object types and instance counts grouped by type name and sorted by instance count. Use -d or --detailed to get an output
                           grouped by type name, properties, and array length, as well as more information regarding each type.
      findrefs          -- Finds all the object properties which meet the search criteria.
                           The default is to list all the object properties that reference the specified value.
                           Flags:
                           * -v, --value expr     - all properties that refer to the specified JavaScript object (default)
                           * -n, --name  name     - all properties with the specified name
                           * -s, --string string  - all properties that refer to the specified JavaScript string value
      getactivehandles  -- Print all pending handles in the queue. Equivalent to running process._getActiveHandles() on the living process.
      getactiverequests -- Print all pending requests in the queue. Equivalent to running process._getActiveRequests() on the living process.
      inspect           -- Print detailed description and contents of the JavaScript value.
                           Possible flags (all optional):
                           * -F, --full-string    - print whole string without adding ellipsis
                           * -m, --print-map      - print object's map address                           * -s, --print-source   - print source code for function objects                           * -l num, --length num - print maximum of `num` elements from string/array                           Syntax: v8 inspect [flags] expr      nodeinfo          -- Print information about Node.js      print             -- Print short description of the JavaScript value.                           Syntax: v8 print expr      settings          -- Interpreter settings      source            -- Source code information For more help on any particular subcommand, type 'help <command> <subcommand>'. 複製代碼
  • bt
  • findjsinstances
  • findjsobjects
  • findrefs
  • inspect
  • nodeinfo
  • print
  • source

運行 v8 findjsobjects 查看全部對象實例及總共佔內存大小

(llnode) v8 findjsobjects
 Instances  Total Size Name
 ---------- ---------- ----
        ...
        356      11392 (Array)
        632      35776 Object
       8300     332000 LeakingClass
      14953      53360 (String)
 ---------- ---------- 
      24399     442680
      
複製代碼

能夠看出:LeakingClass 有8300 個實例,佔內存332000 byte。使用v8 findjsinstances 查看全部 LeakingClass 實例:

(lldb) v8 findjsinstances LeakingClass
...
0x221fb297fbb9:<Object: LeakingClass>
0x221fb297fc29:<Object: LeakingClass>
0x221fb297fc99:<Object: LeakingClass>
0x221fb297fd09:<Object: LeakingClass>
0x221fb297fd79:<Object: LeakingClass>
0x221fb297fde9:<Object: LeakingClass>
0x221fb297fe59:<Object: LeakingClass>
0x221fb297fec9:<Object: LeakingClass>
0x221fb297ff39:<Object: LeakingClass>
0x221fb297ffa9:<Object: LeakingClass>
(Showing 1 to 8300 of 8300 instances)
複製代碼

使用 v8 i檢索實例的具體內容

(llnode) v8 i 0x221fb297ffa9
0x221fb297ffa9:<Object: LeakingClass properties {
    .name=0x221f9bc82201:<String: "0.s3psjp4ctzj">,
    .age=<Smi: 95>}>
(llnode) v8 i 0x221fb297ff39
0x221fb297ff39:<Object: LeakingClass properties {
    .name=0x221fb297ff71:<String: "0.q1t4gikp9a">,
    .age=<Smi: 6>}>
(llnode) v8 i 0x221fb297fec9
0x221fb297fec9:<Object: LeakingClass properties {
    .name=0x221fb297ff01:<String: "0.zzomfpcmgn">,
    .age=<Smi: 52>}>
複製代碼

能夠看到每一個 LeakingClass 實例的 name 和 age 字段的值。

使用 v8 findrefs 查看引用

(llnode) v8 findrefs 0x221fb297ffa9
0x221fd136cb51: (Array)[7041]=0x221fb297ffa9
(llnode) v8 i 0x221fd136cb51
0x221fd136cb51:<Array: length=10018 {
    [0]=0x221f9b627171:<Object: LeakingClass>,
    [1]=0x221f9b627199:<Object: LeakingClass>,
    [2]=0x221f9b6271c1:<Object: LeakingClass>,
    [3]=0x221f9b6271e9:<Object: LeakingClass>,
    [4]=0x221f9b627211:<Object: LeakingClass>,
    [5]=0x221f9b627239:<Object: LeakingClass>,
    [6]=0x221f9b627261:<Object: LeakingClass>,
    [7]=0x221f9b627289:<Object: LeakingClass>,
    [8]=0x221f9b6272b1:<Object: LeakingClass>,
    [9]=0x221f9b6272d9:<Object: LeakingClass>,
    [10]=0x221f9b627301:<Object: LeakingClass>,
    [11]=0x221f9b627329:<Object: LeakingClass>,
    [12]=0x221f9b627351:<Object: LeakingClass>,
    [13]=0x221f9b627379:<Object: LeakingClass>,
    [14]=0x221f9b6273a1:<Object: LeakingClass>,
    [15]=0x221f9b6273c9:<Object: LeakingClass>}>
複製代碼

能夠看出:經過一個 LeakingClass 實例的內存地址,咱們使用 v8 findrefs找到了引用它的數組的內存地址,而後經過這個地址去檢索數組,獲得這個數組長度爲10018,每一項都是一個 LeakingClass 實例,這不就是咱們代碼中的 leaks 數組嗎?

小提示: v8 i 是 v8 inspect的縮寫,v8 p是 v8 print的縮寫。

1.8 --abort-on-uncaught-exception

在 Node.js 程序啓動時添加 —-abort-on-uncaught-exception 參數,當程序 crash 的時候,會自動 Core Dump,方便 「死後驗屍」。

添加 --abort-on-uncaught-exception 參數,啓動測試程序:

$ ulimit -c unlimited
$ node --abort-on-uncaught-exception app.js
複製代碼

啓動另一個終端運行:

$ kill -BUS `pgrep -n node`
複製代碼

第 1 個終端會顯示:

Leaks: 100
Leaks: 200
Leaks: 300
Leaks: 400
Leaks: 500
Leaks: 600
Leaks: 700
Leaks: 800
Bus error (core dumped)
複製代碼

調試步驟與上面一致:

(llnode) v8 findjsobjects
 Instances  Total Size Name
 ---------- ---------- ----
        ...
        356      11392 (Array)
        632      35776 Object
       8300     332000 LeakingClass
      14953      53360 (String)
 ---------- ---------- 
      24399     442680
      
複製代碼

1.9 總結

咱們的測試代碼很簡單,沒有引用任何第三方模塊,若是項目較大且引用的模塊較多,則 v8 findjsobjects 的結果將難以甄別,這個時候能夠屢次使用 gcore 進行 Core Dump,對比發現增加的對象,再進行診斷。

2 使用 heapdump

heapdump 是一個 dump V8 堆信息的工具。v8-profiler 也包含了這個功能,這兩個工具的原理都是一致的,都是 v8::Isolate::GetCurrent()->GetHeapProfiler()->TakeHeapSnapshot(title, control),可是 heapdump 的使用簡單些。下面咱們以 heapdump 爲例講解如何分析 Node.js 的內存泄漏。

這裏以一段經典的內存泄漏代碼做爲測試代碼:

const heapdump = require('heapdump')
let leakObject = null
let count = 0
setInterval(function testMemoryLeak() {
  const originLeakObject = leakObject
  const unused = function () {
    if (originLeakObject) {
      console.log('originLeakObject')
    }
  }
  leakObject = {
    count: String(count++),
    leakStr: new Array(1e7).join(''),
    leakMethod: function () {
      console.log('leakMessage')
    }
  }
}, 1000)
複製代碼

爲何這段程序會發生內存泄漏呢?首先咱們要明白閉包的原理:同一個函數內部的閉包做用域只有一個,全部閉包共享。在執行函數的時候,若是遇到閉包,則會建立閉包做用域的內存空間,將該閉包所用到的局部變量添加進去,而後再遇到閉包,會在以前建立好的做用域空間添加此閉包會用到而前閉包沒用到的變量。函數結束時,清除沒有被閉包做用域引用的變量。

這段代碼內存泄露緣由是:在 testMemoryLeak 函數內有兩個閉包:unused 和 leakMethod。unused 這個閉包引用了父做用域中的 originLeakObject 變量,若是沒有後面的 leakMethod,則會在函數結束後被清除,閉包做用域也跟着被清除了。由於後面的 leakObject 是全局變量,即 leakMethod 是全局變量,它引用的閉包做用域(包含了 unused 所引用的 originLeakObject)不會釋放。而隨着 testMemoryLeak 不斷的調用,originLeakObject 指向前一次的 leakObject,下次的 leakObject.leakMethod 又會引用以前的 originLeakObject,從而造成一個閉包引用鏈,而 leakStr 是一個大字符串,得不到釋放,從而形成了內存泄漏。

解決方法:在 testMemoryLeak 函數內部的最後添加originLeakObject = null便可。

運行測試代碼:

$ node app
複製代碼

而後前後執行兩次:

$ kill -USR2 `pgrep -n node`
複製代碼

在當前目錄下生成了兩個 heapsnapshot 文件:

heapdump-100427359.61348.heapsnapshot
heapdump-100438986.797085.heapsnapshot
複製代碼

2.1 Chrome DevTools

咱們使用 Chrome DevTools 來分析前面生成的 heapsnapshot 文件。調出 Chrome DevTools -> Memory -> Load,按順序依次加載前面生成的 heapsnapshot 文件。單擊第 2 個堆快照,在左上角有個下拉菜單,有以下 4 個選項:

  • Summary:以構造函數名分類顯示。
  • Comparison:比較多個快照之間的差別。
  • Containment:查看整個 GC 路徑。
  • Statistics:以餅狀圖顯示內存佔用信息。 一般咱們只會用前兩個選項;第 3 個選項通常用不到,由於在展開 Summary 和 Comparison 中的每一項時,均可以看到從 GC roots 到這個對象的路徑;第 4 個選項只能看到內存佔用比,以下圖所示:

切換到 Summary 頁,能夠看到有以下 5 個屬性:

  • Contructor:構造函數名,例如 Object、Module、Socket,(array)、(string)、(regexp) 等加了括號的分別表明內置的 Array、String 和 Regexp。
  • Distance:到 GC roots (GC 根對象)的距離。GC 根對象在瀏覽器中通常是 window 對象,在 Node.js 中是 global 對象。距離越大,則說明引用越深,有必要重點關注一下,極有多是內存泄漏的對象。
  • Objects Count:對象個數。
  • Shallow Size:對象自身的大小,不包括它引用的對象。
  • Retained Size:對象自身的大小和它引用的對象的大小,即該對象被 GC 以後所能回收的內存大小。

小提示:

  • 一個對象的 Retained Size = 該對象的 Shallow Size + 該對象可直接或間接引用到的對象的 Shallow Size 之和。
  • Shallow Size == Retained Size 的有 (boolean)、(number)、(string),它們沒法引用其餘值,而且始終是葉子節點。

咱們單擊 Retained Size 選擇降序展現,能夠看到 (closure) 這一項引用的內容達到 99%,繼續展開以下:

能夠看出:一個 leakStr 佔了 5% 的內存,而 leakMethod 引用了 88% 的內存。對象保留樹(Retainers,老版本 Chrome 叫 Object’s retaining tree)展現了對象的 GC path,單擊如上圖中的 leakStr(Distance 是 13),Retainers 會自動展開,Distance 從 13 遞減到 1。

咱們繼續展開 leakMethod,以下所示:

能夠看出:有一個 count=」18」originLeakObject 的 leakMethod 函數的 context(即上下文) 引用了一個 count=」17」originLeakObject 對象,而這個 originLeakObject 對象的 leakMethod 函數的 context 又引用了 count=」16」originLeakObject 對象,以此類推。而每一個 originLeakObject 對象上都有一個大字符串 leakStr(佔用 8% 的內存),從而形成內存泄漏,符合咱們以前的推斷。

小提示:若是背景色是黃色的,則表示這個對象在 JavaScript 中還存在引用,因此可能沒有被清除。若是背景色是紅色的,則表示這個對象在 JavaScript 中不存在引用,可是依然存活在內存中,通常常見於 DOM 對象,它們存放的位置和 JavaScript 中的對象仍是有不一樣的,在 Node.js 中不多碰見。

2.2 對比快照

切換到 Comparison 視圖下,能夠看到一些 #New、#Deleted、#Delta 等屬性,+ 和 - 表明相對於比較的堆快照而言。咱們對比第 2 個快照和第 1 個快照,以下所示:

能夠看出:(string) 增長了 5 個,每一個 string 大小爲 10000024 字節。

3 使用 memwatch-next

memwatch-next(如下簡稱 memwatch)是一個用來監測 Node.js 的內存泄漏和堆信息比較的模塊。下面咱們以一段事件監聽器致使內存泄漏的代碼爲例,講解如何使用 memwatch。

測試代碼以下:

let count = 1
const memwatch = require('memwatch-next')
memwatch.on('stats', (stats) => { 
  console.log(count++, stats)
})
memwatch.on('leak', (info) => {
  console.log('---')
  console.log(info)
  console.log('---')
})
const http = require('http')
const server = http.createServer((req, res) => {
  for (let i = 0; i < 10000; i++) {
    server.on('request', function leakEventCallback() {})
  }
  res.end('Hello World')
  global.gc()
}).listen(3000)
複製代碼

在每一個請求到來時,給 server 註冊 10000 個 request 事件的監聽函數(大量的事件監聽函數存儲到內存中,形成了內存泄漏),而後手動觸發一次 GC。

運行該程序:

$ node --expose-gc app.js
複製代碼

注意:這裏添加 —expose-gc 參數啓動程序,這樣咱們才能夠在程序中手動觸發 GC。

memwatch 能夠監聽兩個事件:

  • stats: GC 事件,每執行一次 GC,都會觸發該函數,打印 heap 相關的信息。以下:
{
  num_full_gc: 1,// 完整的垃圾回收次數
  num_inc_gc: 1,// 增加的垃圾回收次數
  heap_compactions: 1,// 內存壓縮次數
  usage_trend: 0,// 使用趨勢
  estimated_base: 5350136,// 預期基數
  current_base: 5350136,// 當前基數
  min: 0,// 最小值
  max: 0// 最大值
}
複製代碼
  • leak: 內存泄露事件,觸發該事件的條件是:連續 5 次 GC 後內存都是增加的。打印以下:
{ 
  growth: 3616040,
  reason: 'heap growth over 5 consecutive GCs (0s) - -2147483648 bytes/hr' 
}
複製代碼

運行:

$ ab -c 1 -n 5 http://localhost:3000/
複製代碼

輸出:

(node:35513) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 request listeners added. Use emitter.setMaxListeners() to increase limit
1 { num_full_gc: 1,
  num_inc_gc: 2,
  heap_compactions: 1,
  usage_trend: 0,
  estimated_base: 5674608,
  current_base: 5674608,
  min: 0,
  max: 0 }
2 { num_full_gc: 2,
  num_inc_gc: 4,
  heap_compactions: 2,
  usage_trend: 0,
  estimated_base: 6668760,
  current_base: 6668760,
  min: 0,
  max: 0 }
3 { num_full_gc: 3,
  num_inc_gc: 5,
  heap_compactions: 3,
  usage_trend: 0,
  estimated_base: 7570424,
  current_base: 7570424,
  min: 7570424,
  max: 7570424 }
4 { num_full_gc: 4,
  num_inc_gc: 7,
  heap_compactions: 4,
  usage_trend: 0,
  estimated_base: 8488368,
  current_base: 8488368,
  min: 7570424,
  max: 8488368 }
--------------
{ growth: 3616040,
  reason: 'heap growth over 5 consecutive GCs (0s) - -2147483648 bytes/hr' }
--------------
5 { num_full_gc: 5,
  num_inc_gc: 9,
  heap_compactions: 5,
  usage_trend: 0,
  estimated_base: 9290648,
  current_base: 9290648,
  min: 7570424,
  max: 9290648 }
  
複製代碼

能夠看出:Node.js 已經警告咱們事件監聽器超過了 11 個,可能形成內存泄露。連續 5 次內存增加觸發 leak 事件打印出增加了多少內存(bytes)和預估每小時增加多少 bytes。

3.1 Heap Diffing

memwatch 有一個 HeapDiff 函數,用來對比並計算出兩次堆快照的差別。修改測試代碼以下:

const memwatch = require('memwatch-next')
const http = require('http')
const server = http.createServer((req, res) => {
  for (let i = 0; i < 10000; i++) {
    server.on('request', function leakEventCallback() {})
  }
  res.end('Hello World')
  global.gc()
}).listen(3000)
const hd = new memwatch.HeapDiff()
memwatch.on('leak', (info) => {
  const diff = hd.end()
  console.dir(diff, { depth: 10 })
})
運行這段代碼並執行一樣的 ab 命令,打印以下:

(node:35690) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 request listeners added. Use emitter.setMaxListeners() to increase limit
{ before: { nodes: 35864, size_bytes: 4737664, size: '4.52 mb' },
  after: { nodes: 87476, size_bytes: 8946784, size: '8.53 mb' },
  change: 
   { size_bytes: 4209120,
     size: '4.01 mb',
     freed_nodes: 894,
     allocated_nodes: 52506,
     details: 
      [ ...
        { what: 'Array',
          size_bytes: 533008,
          size: '520.52 kb',
          '+': 1038,
          '-': 517 },
        { what: 'Closure',
          size_bytes: 3599856,
          size: '3.43 mb',
          '+': 50001,
          '-': 3 }
        ...
      ]
    }
}
複製代碼

能夠看出:內存由 4.52mb 漲到了 8.53mb,其中 Closure 和 Array 漲了絕大部分,而咱們知道註冊事件監聽函數的本質就是將事件函數(Closure)push 到相應的數組(Array)裏。

3.2 結合 heapdump

memwatch 在結合 heapdump 使用時才能發揮更好的做用。一般用 memwatch 監測到發生內存泄漏,用 heapdump 導出多份堆快照,而後用 Chrome DevTools 分析和比較,定位內存泄漏的元兇。

修改代碼以下:

const memwatch = require('memwatch-next')
const heapdump = require('heapdump')
const http = require('http')
const server = http.createServer((req, res) => {
  for (let i = 0; i < 10000; i++) {
    server.on('request', function leakEventCallback() {})
  }
  res.end('Hello World')
  global.gc()
}).listen(3000)
dump()
memwatch.on('leak', () => {
  dump()
})
function dump() {
  const filename = `${__dirname}/heapdump-${process.pid}-${Date.now()}.heapsnapshot`
  heapdump.writeSnapshot(filename, () => {
    console.log(`${filename} dump completed.`)
  })
}
複製代碼

以上程序在啓動後先執行一次 heap dump,當觸發 leak 事件時再執行一次 heap dump。運行這段代碼並執行一樣的 ab 命令,生成兩個 heapsnapshot 文件:

heapdump-21126-1519545957879.heapsnapshot
heapdump-21126-1519545975702.heapsnapshot
複製代碼

用 Chrome DevTools 加載這兩個 heapsnapshot 文件,選擇 comparison 比較視圖,以下所示:

能夠看出:增長了 5 萬個 leakEventCallback 函數,單擊其中任意一個,能夠從 Retainers 中看到更詳細的信息,例如 GC path 和所在的文件等信息。

前面介紹了 heapdumpmemwatch-next 的用法,但在實際使用時並不那麼方便,咱們總不能一直盯着服務器的情況,在發現內存持續增加並超過內心的閾值時,再手動去觸發 Core Dump 吧?在大多數狀況下發現問題時,就已經錯過了現場。因此,咱們可能須要 cpu-memory-monitor。顧名思義,這個模塊能夠用來監控 CPU 和 Memory 的使用狀況,並能夠根據配置策略自動 dump CPU 的使用狀況(cpuprofile)和內存快照(heapsnapshot)。

4 使用 cpu-memory-monitor

咱們先來看看如何使用 cpu-memory-monitor,其實很簡單,只需在進程啓動的入口文件中引入如下代碼:

require('cpu-memory-monitor')({
  cpu: {
    interval: 1000,
    duration: 30000,
    threshold: 60,
    profileDir: '/tmp',
    counter: 3,
    limiter: [5, 'hour']
  }
})
複製代碼

上述代碼的做用是:每 1000ms(interval)檢查一次 CPU 的使用狀況,若是發現連續 3(counter)次 CPU 使用率大於 60%(threshold),則 dump 30000ms(duration) CPU 的使用狀況,生成 cpu-${process.pid}-${Date.now()}.cpuprofile 到/tmp(profileDir) 目錄下,1(limiter[1]) 小時最多 dump 5(limiter[0]) 次。

以上是自動 dump CPU 使用狀況的策略。dump Memory 使用狀況的策略同理:

require('cpu-memory-monitor')({
  memory: {
    interval: 1000,
    threshold: '1.2gb',
    profileDir: '/tmp',
    counter: 3,
    limiter: [3, 'hour']
  }
})
複製代碼

上述代碼的做用是:每 1000ms(interval) 檢查一次 Memory 的使用狀況,若是發現連續 3(counter) 次 Memory 大於 1.2gb(threshold),則 dump 一次 Memory,生成memory-${process.pid}-${Date.now()}.heapsnapshot 到 /tmp(profileDir) 目錄下,1(limiter[1]) 小時最多 dump 3(limiter[0]) 次。

注意:memory 的配置沒有 duration 參數,由於 Memroy 的 dump 只是某一時刻的,而不是一段時間的。

那聰明的你確定會問了:能不能將 cpu 和 memory 配置一塊使用?好比:

require('cpu-memory-monitor')({
  cpu: {
    interval: 1000,
    duration: 30000,
    threshold: 60,
    ...
  },
  memory: {
    interval: 10000,
    threshold: '1.2gb',
    ...
  }
})
複製代碼

答案是:能夠,但不要這麼作。由於這樣作可能會出現這種狀況:

內存高了且達到設定的閾值 -> 觸發 Memory Dump/GC -> 致使 CPU 使用率高且達到設定的閾值 -> 觸發 CPU Dump -> 致使堆積的請求愈來愈多(好比內存中堆積了不少 SQL 查詢)-> 觸發 Memory Dump -> 致使雪崩。

一般狀況下,只使用其中一種就能夠了。

4.1 源碼解讀

cpu-memory-monitor 的源代碼不過百餘行,大致邏輯以下:

const processing = {
  cpu: false,
  memory: false
}
const counter = {
  cpu: 0,
  memory: 0
}
function dumpCpu(cpuProfileDir, cpuDuration) { ... }
function dumpMemory(memProfileDir) { ... }
module.exports = function cpuMemoryMonitor(options = {}) {
  ...
  if (options.cpu) {
    const cpuTimer = setInterval(() => {
      if (processing.cpu) {
        return
      }
      pusage.stat(process.pid, (err, stat) => {
        if (err) {
          clearInterval(cpuTimer)
          return
        }
        if (stat.cpu > cpuThreshold) {
          counter.cpu += 1
          if (counter.cpu >= cpuCounter) {
            memLimiter.removeTokens(1, (limiterErr, remaining) => {
              if (limiterErr) {
                return
              }
              if (remaining > -1) {
                dumpCpu(cpuProfileDir, cpuDuration)
                counter.cpu = 0
              }
            })
          } else {
            counter.cpu = 0
          }
        }
      })
    }, cpuInterval)
  }
  if (options.memory) {
    ...
    memwatch.on('leak', () => {
      dumpMemory(...)
    })
  }
}
複製代碼

能夠看出:cpu-memory-monitor 沒有用到什麼新鮮的東西,仍是以前講解過的 v8-profilerheapdumpmemwatch-next 的組合使用而已。

有如下幾點須要注意:

只有傳入了 cpu 或者 memory 的配置,纔會去監聽相應的 CPU 或者 Memory。 在傳入 memory 配置時,用了 memwatch-next 額外監聽了 leak 事件,也會 dump Memory,格式是 leak-memory-${process.pid}-${Date.now()}.heapsnapshot。 頂部引入了 heapdump,因此即便沒有 memory 配置,也能夠經過kill -USR2 <PID>手動觸發 Memory Dump。

參考連接

相關文章
相關標籤/搜索