memwatch-next(以下简称 memwatch)是一个用来监测 Node.js 的内存泄漏和堆信息比较的模块。下面我们以一段事件监听器导致内存泄漏的代码为例,讲解如何使用 memwatch。
测试代码如下:
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 监听以下两个事件:
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: 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。
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)里。
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.2 heapdump
下一节:2.4 cpu-memory-monitor
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8