記一次內存泄漏處理

記一次內存泄漏處理

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

博客原文

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章