2.3.1 使用 memwatch-next

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

测试代码如下:

app.js

  1. let count = 1
  2. const memwatch = require('memwatch-next')
  3. memwatch.on('stats', (stats) => {
  4. console.log(count++, stats)
  5. })
  6. memwatch.on('leak', (info) => {
  7. console.log('---')
  8. console.log(info)
  9. console.log('---')
  10. })
  11. const http = require('http')
  12. const server = http.createServer((req, res) => {
  13. for (let i = 0; i < 10000; i++) {
  14. server.on('request', function leakEventCallback() {})
  15. }
  16. res.end('Hello World')
  17. global.gc()
  18. }).listen(3000)

在每个请求到来时,给 server 注册 10000 个 request 事件的监听函数(大量的事件监听函数存储到内存中,造成了内存泄漏),然后手动触发一次 GC。

运行该程序:

  1. $ node --expose-gc app.js

注意:这里添加 —expose-gc 参数启动程序,这样我们才可以在程序中手动触发 GC。

memwatch 可以监听两个事件:

  1. stats:GC 事件,每执行一次 GC,都会触发该函数,打印 heap 相关的信息。如下:

    1. {
    2. num_full_gc: 1,// 完整的垃圾回收次数
    3. num_inc_gc: 1,// 增长的垃圾回收次数
    4. heap_compactions: 1,// 内存压缩次数
    5. usage_trend: 0,// 使用趋势
    6. estimated_base: 5350136,// 预期基数
    7. current_base: 5350136,// 当前基数
    8. min: 0,// 最小值
    9. max: 0// 最大值
    10. }
  2. leak:内存泄露事件,触发该事件的条件是:连续 5 次 GC 后内存都是增长的。打印如下:

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

运行:

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

输出:

  1. (node:20989) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 request listeners added. Use emitter.setMaxListeners() to increase limit
  2. 1 { num_full_gc: 1,
  3. num_inc_gc: 1,
  4. heap_compactions: 1,
  5. usage_trend: 0,
  6. estimated_base: 5720064,
  7. current_base: 5720064,
  8. min: 0,
  9. max: 0 }
  10. 2 { num_full_gc: 2,
  11. num_inc_gc: 1,
  12. heap_compactions: 2,
  13. usage_trend: 0,
  14. estimated_base: 7073824,
  15. current_base: 7073824,
  16. min: 0,
  17. max: 0 }
  18. 3 { num_full_gc: 3,
  19. num_inc_gc: 1,
  20. heap_compactions: 3,
  21. usage_trend: 0,
  22. estimated_base: 7826368,
  23. current_base: 7826368,
  24. min: 7826368,
  25. max: 7826368 }
  26. 4 { num_full_gc: 4,
  27. num_inc_gc: 1,
  28. heap_compactions: 4,
  29. usage_trend: 0,
  30. estimated_base: 8964784,
  31. current_base: 8964784,
  32. min: 7826368,
  33. max: 8964784 }
  34. ---
  35. { growth: 3820272,
  36. reason: 'heap growth over 5 consecutive GCs (0s) - -2147483648 bytes/hr' }
  37. ---
  38. 5 { num_full_gc: 5,
  39. num_inc_gc: 1,
  40. heap_compactions: 5,
  41. usage_trend: 0,
  42. estimated_base: 9540336,
  43. current_base: 9540336,
  44. min: 7826368,
  45. max: 9540336 }

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

2.3.2 Heap Diffing

memwatch 有一个 HeapDiff 函数,用来对比并计算出两次堆快照的差异。修改测试代码如下:

  1. const memwatch = require('memwatch-next')
  2. const http = require('http')
  3. const server = http.createServer((req, res) => {
  4. for (let i = 0; i < 10000; i++) {
  5. server.on('request', function leakEventCallback() {})
  6. }
  7. res.end('Hello World')
  8. global.gc()
  9. }).listen(3000)
  10. const hd = new memwatch.HeapDiff()
  11. memwatch.on('leak', (info) => {
  12. const diff = hd.end()
  13. console.dir(diff, { depth: 10 })
  14. })

运行这段代码并执行同样的 ab 命令,打印如下:

  1. { before: { nodes: 35727, size_bytes: 4725128, size: '4.51 mb' },
  2. after: { nodes: 87329, size_bytes: 8929792, size: '8.52 mb' },
  3. change:
  4. { size_bytes: 4204664,
  5. size: '4.01 mb',
  6. freed_nodes: 862,
  7. allocated_nodes: 52464,
  8. details:
  9. [ ...
  10. { what: 'Array',
  11. size_bytes: 530200,
  12. size: '517.77 kb',
  13. '+': 1023,
  14. '-': 510 },
  15. { what: 'Closure',
  16. size_bytes: 3599856,
  17. size: '3.43 mb',
  18. '+': 50001,
  19. '-': 3 },
  20. ...
  21. ]
  22. }
  23. }

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

2.3.3 结合 heapdump

memwatch 在结合 heapdump 使用时才能发挥更好的作用。通常用 memwatch 监测到发生内存泄漏,用 heapdump 导出多份堆快照,然后用 Chrome DevTools 分析和比较,定位内存泄漏的元凶。

修改代码如下:

  1. const memwatch = require('memwatch-next')
  2. const heapdump = require('heapdump')
  3. const http = require('http')
  4. const server = http.createServer((req, res) => {
  5. for (let i = 0; i < 10000; i++) {
  6. server.on('request', function leakEventCallback() {})
  7. }
  8. res.end('Hello World')
  9. global.gc()
  10. }).listen(3000)
  11. dump()
  12. memwatch.on('leak', () => {
  13. dump()
  14. })
  15. function dump() {
  16. const filename = `${__dirname}/heapdump-${process.pid}-${Date.now()}.heapsnapshot`
  17. heapdump.writeSnapshot(filename, () => {
  18. console.log(`${filename} dump completed.`)
  19. })
  20. }

以上程序在启动后先执行一次 heap dump,当触发 leak 事件时再执行一次 heap dump。运行这段代码并执行同样的 ab 命令,生成两个 heapsnapshot 文件:

  1. heapdump-21126-1519545957879.heapsnapshot
  2. heapdump-21126-1519545975702.heapsnapshot

用 Chrome DevTools 加载这两个 heapsnapshot 文件,选择 comparison 比较视图,如下所示:

memwatch-next - 图1

可以看出:增加了 5 万个 leakEventCallback 函数,单击其中任意一个,可以从 Retainers 中看到更详细的信息,例如 GC path 和所在的文件等信息。

2.3.4 参考链接

上一节:2.2 heapdump

下一节:2.4 cpu-memory-monitor