记一次内存泄漏处理
近期在检查服务器状态时,发现每个Node.js进程占用的内存大小在400-500M之间,根据平时的经验判断是代码中出现了内存泄漏导致,本文就记录这次内存泄漏问题的处理过程。
定位内存泄漏原因过程没有用到什么高难度技术,只是靠经验凭感觉找到的问题代码,简言之就是应用中有一个合成朋友圈分享海报功能,考虑前端canvas兼容性不够好以及该功能使用频次低,所以采用的是服务端合成,就是这个功能导致了应用出现内存泄漏。
合成方式
图片合成功能是借助sharp这个库实现的,原理是通过sharp提供的api往背景图上不断的叠加可变元素(头像、昵称、文案),原始代码比较长,定位问题的时候简化了处理代码,如下所示:
const fs = require('fs');
const _ = require('lodash');
const sharp = require('sharp');
let count = 0;
setInterval(() => {
if (++count % 20 === 0) {
console.log(count, _.mapValues(process.memoryUsage(), item => (item / 1024 / 1024).toFixed('4') + 'M'));
}
_makePost();
}, 300);
function _makePost() {
let text = `
<svg height="96" width="450">
<text x="0" y="24" font-size="26" fill="white" font-family="Microsoft YaHei">
极品装备,点击就有,海量红包,点击就送
</text>
</svg>
`;
return sharp(fs.readFileSync('./share.jpg'))
.overlayWith(fs.readFileSync('./avatar.jpg'), {
top: 100,
left: 100
})
.overlayWith(Buffer.from(text), {
top: 195,
left: 212
})
.toBuffer();
}
以上代码中share.jpg
是背景图,avatar.jpg
是用户头像,代码逻辑是间隔300ms模拟一次合成,每20次合成打印一次进程的内存使用情况。
分析
通过node --trace_gc --trace_gc_verbose index.js
运行上面的代码,可以发现应用内存在不断上升,海报合成运行360次后,应用内存会涨到500M左右,再往后内存还会继续涨下去。
360 { rss: '478.2734M',
heapTotal: '10.9063M',
heapUsed: '8.9829M',
external: '12.2104M' }
针对process.memoryUsage()
的输出,官方有相应解释:
- rss,Resident Set Size,实际使用物理内存
- heapTotal,heapUsed,V8内存使用情况
- external,绑定到V8管理的JavaScript对象的C++对象内存使用情况
除process.memoryUsage
获得的内存使用情况外,GC也打印了一些日志:
[26236:0x104800000] Fast promotion mode: false survival rate: 0%
[26236:0x104800000] 119240 ms: Scavenge 9.4 (12.4) -> 8.4 (12.4) MB, 10.6 / 5.4 ms allocation failure
[26236:0x104800000] Memory allocator, used: 12704 KB, available: 1453664 KB
[26236:0x104800000] New space, used: 7 KB, available: 999 KB, committed: 1024 KB
[26236:0x104800000] Old space, used: 4824 KB, available: 498 KB, committed: 5436 KB
[26236:0x104800000] Code space, used: 1216 KB, available: 0 KB, committed: 2048KB
[26236:0x104800000] Map space, used: 419 KB, available: 0 KB, committed: 532 KB
[26236:0x104800000] Large object space, used: 2108 KB, available: 1453143 KB, committed: 2128 KB
[26236:0x104800000] All spaces, used: 8576 KB, available: 1454641 KB, committed: 11168KB
[26236:0x104800000] External memory reported: 374 KB
[26236:0x104800000] External memory global 0 KB
[26236:0x104800000] Total time spent in GC : 46.7 ms
两者结合,可以发现V8所使用的内存与process.memoryUsage()中heapTotal,heapUsed的基本一致,只占10M左右。由此判断内存泄漏并不是发生在V8当中,进而联想到代码频繁使用到Buffer(Buffer对象不经过V8的内存分配机制),内存泄漏的原因极大可能是由于这部分Buffer没有被释放导致。
Buffer内存不被释放,原因可能是指向Buffer的引用在V8中一直存在,但仔细分析代码后并没有发现可疑代码,只能逐步拆解代码细化问题原因,最终可重现问题的代码如下所示:
const sharp = require('sharp');
let count = 0;
setInterval(() => {
if (++count % 100 === 0) {
console.log(count, process.memoryUsage());
}
sharp(Buffer.from(`
<svg>
<text>example</text>
</svg>
`)).toBuffer();
}, 100);
在sharp中使用svg + text会导致内存泄漏,这个问题跟应用代码没什么关系,初步判断是sharp底层出了问题,有点难办。
给库的作者提了一个issue等待回复,搞不定的话只能再物色一下其他库完成这事,最终完成后再来更新这篇文章。
如果你的代码也用到了sharp以及svg+text,可以检查一下是否会导致内存泄漏。
2018-07-31更新
sharp库svg + text问题还没解决,但找到另外一种实现合成文字到图片的方式:用opentype.js将文字转换成path,在继续使用svg的方式合成,这样就不会存在内存泄漏,代码示例:
const sharp = require('sharp');
const font = require('opentype.js').loadSync('xxx.ttf'); // xxx.ttf: 字体文件路径
let text = '木有鱼丸';
let width = font.getAdvanceWidth(text, 24); // 动态计算文字宽度
let txtBuffer = Buffer.from(_svg4Text(text, {
x: 0,
y: 20,
color: 'white',
size: 24,
height: 24,
width: width
}));
sharp(input) // input: 图片路径或buffer
.overlayWith(text, {
top: 100,
left: (750 - width) / 2 // 文字居中
})
.jpeg()
.toBuffer()
.then(ret => {
// ret: 合成后的图片buffer
});
function _svg4Text(text, options) {
let {
x,
y,
color,
size,
height,
width
} = options;
let path = font.getPath(text, x, y, size);
path.fill = color;
return `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
height="${height}" width="${width}">
${path.toSVG()}
</svg>`;
}
如果设置x = 0, y = 0,画出来的文字只能看到些许底部,我认为所看到的底部恰好好是“字粗”,自己通过实践得到y的取值采用与字体大小1:1.2
的关系,比如字体大小是24,那么y = 24 / 1.2 = 20,这个潜规则并不严谨...
2018-08-07更新
使用svg+text的代码在OS X下运行的确看似有内存泄漏问题,在自己机器上运行8000次时RSS大小约4.8G,但这有可能是因为可用内存较大所以进程一直没有释放内存,水平不够,没去仔细研究这一块。
但svg+text的代码在linux下运行时,占用内存一直保持在30-40M,搭建完整的应用环境后进一步测试发这并不是内存泄漏,而是RSS占用过高。
应用进程因为这一个功能要多占用300M内存,考虑调用频率确实较低,计划使用子进程调用的方式实现该功能。