記一次內存泄漏處理
近期在檢查服務器狀態時,發現每個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內存,考慮調用頻率確實較低,計劃使用子進程調用的方式實現該功能。