Node.js 调试指南

 主页   资讯   文章   代码   电子书 

memwatch-next(以下简称 memwatch)是一个用来监测 Node.js 的内存泄漏和堆信息比较的模块。下面我们以一段事件监听器导致内存泄漏的代码为例,讲解如何使用 memwatch。

2.3.1 使用 memwatch-next

测试代码如下:

app.js

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 监听以下两个事件:

  1. 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// 最大值
    }
  2. leak:可疑的内存泄露事件,触发该事件的条件是:内存在连续 5 次 GC 后都是增长的。打印如下:

    {
      growth: 4051464,
      reason: 'heap growth over 5 consecutive GCs (2s) - -2147483648 bytes/hr'
    }

运行:

$ ab -c 1 -n 5 http://localhost:3000/

输出:

(node:20989) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 request listeners added. Use emitter.setMaxListeners() to increase limit
1 { num_full_gc: 1,
  num_inc_gc: 1,
  heap_compactions: 1,
  usage_trend: 0,
  estimated_base: 5720064,
  current_base: 5720064,
  min: 0,
  max: 0 }
2 { num_full_gc: 2,
  num_inc_gc: 1,
  heap_compactions: 2,
  usage_trend: 0,
  estimated_base: 7073824,
  current_base: 7073824,
  min: 0,
  max: 0 }
3 { num_full_gc: 3,
  num_inc_gc: 1,
  heap_compactions: 3,
  usage_trend: 0,
  estimated_base: 7826368,
  current_base: 7826368,
  min: 7826368,
  max: 7826368 }
4 { num_full_gc: 4,
  num_inc_gc: 1,
  heap_compactions: 4,
  usage_trend: 0,
  estimated_base: 8964784,
  current_base: 8964784,
  min: 7826368,
  max: 8964784 }
---
{ growth: 3820272,
  reason: 'heap growth over 5 consecutive GCs (0s) - -2147483648 bytes/hr' }
---
5 { num_full_gc: 5,
  num_inc_gc: 1,
  heap_compactions: 5,
  usage_trend: 0,
  estimated_base: 9540336,
  current_base: 9540336,
  min: 7826368,
  max: 9540336 }

可以看出:Node.js 已经警告我们事件监听器超过了 11 个,可能造成内存泄露。连续 5 次内存增长触发 leak 事件打印出增长了多少内存(bytes)和预估每小时增长多少 bytes。

2.3.2 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 命令,打印如下:

{ before: { nodes: 35727, size_bytes: 4725128, size: '4.51 mb' },
  after: { nodes: 87329, size_bytes: 8929792, size: '8.52 mb' },
  change:
   { size_bytes: 4204664,
     size: '4.01 mb',
     freed_nodes: 862,
     allocated_nodes: 52464,
     details:
      [ ...
        { what: 'Array',
          size_bytes: 530200,
          size: '517.77 kb',
          '+': 1023,
          '-': 510 },
        { what: 'Closure',
          size_bytes: 3599856,
          size: '3.43 mb',
          '+': 50001,
          '-': 3 },
        ...
      ]
    }
}

可以看出:内存由 4.51mb 涨到了 8.52mb,其中 Closure 和 Array 涨了绝大部分,而我们知道注册事件监听函数的本质就是将事件函数(Closure)push 到相应的数组(Array)里。

2.3.3 结合 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 和所在的文件等信息。

2.3.4 参考链接

  • https://github.com/marcominetti/node-memwatch

上一节:2.2 heapdump

下一节:2.4 cpu-memory-monitor