Node.js 刮板中的内存泄漏

Posted

技术标签:

【中文标题】Node.js 刮板中的内存泄漏【英文标题】:Memory leak in Node.js scraper 【发布时间】:2011-08-08 18:30:02 【问题描述】:

这是一个用 Node.js 用 Ja​​vaScript 编写的简单抓取工具,用于从 Wikipedia 中抓取元素周期表元素数据。依赖项是 jsdom 用于 DOM 操作和 chain-gang 用于排队。

它在大多数情况下都能正常工作(它不会优雅地处理错误),而且代码也不算太糟糕,我敢说 for 尝试,但它有一个严重的错误——它会泄漏内存可怕的是,每个元素占用计算机内存的 0.3% 到 0.6% 不等,这样当它开始领先时,它会使用接近 20% 的地方,这显然是不可接受的。

我尝试过使用分析器,但要么发现它们没有帮助,要么难以解释数据。我怀疑这与processElement 的传递方式有关,但我很难将队列代码重写为更优雅的代码。

var fs = require('fs'),
    path = require('path'),
    jsdom = require("jsdom"),
    parseUrl = require('url').parse,
    chainGang = require('chain-gang');

var chain = chainGang.create(
    workers: 1
);

var Settings = 
    periodicUrl: 'http://en.wikipedia.org/wiki/Template:Periodic_table',
    periodicSelector: '#bodyContent > table:first',
    pathPrefix: 'data/',
    ignoredProperties: ['Pronunciation']
;

function writeToFile(output) 
    var keys = 0;

    // Huge nests for finding the name of the element... yeah
    for(var i in output) 
        if(typeof output[i] === 'object' && output[i] !== null)
            for(var l in output[i]) 
                if(l.toLowerCase() === 'name') 
                    var name = output[i][l];
                
            

            keys += Object.keys(output[i]).length;
        
    

    console.log('Scraped ' + keys + ' properties for ' + name);
    console.log('Writing to ' + Settings.pathPrefix + name + '.json');
    fs.writeFile(Settings.pathPrefix + name + '.json', JSON.stringify(output));


// Generic create task function to create a task function that
// would be passed to the chain gang
function createTask (url, callback) 
    console.log('Task added - ' + url);

    return function(worker)
        console.log('Requesting: ' +url);

        jsdom.env(url, [
            'jquery.min.js' // Local copy of jQuery
        ], function(errors, window) 
            if(errors)
                console.log('Error! ' + errors)
                createTask(url, callback);
             else 
                // Give me thy $
                var $ = window.$;

                // Cleanup - remove unneeded elements
                $.fn.cleanup = function() 
                    return this.each(function()
                        $(this).find('sup.reference, .IPA').remove().end()
                            .find('a, b, i, small, span').replaceWith(function()
                                return this.innerhtml;
                            ).end()
                            .find('br').replaceWith(' ');
                    );
                

                callback($);
            

            worker.finish();
        );
    


function processElement ($)
    var infoBox = $('.infobox'),
        image = infoBox.find('tr:contains("Appearance") + tr img:first'),
        description = $('#toc').prevAll('p').cleanup(),
        headers = infoBox.find('tr:contains("properties")'),
        output = 
            Appearance: image.attr('src'),
            Description: $('.infobox + p').cleanup().html()
        ;

    headers.each(function()
        var that = this,
            title = this.textContent.trim(),
            rowspan = 0,
            rowspanHeading = '';

        output[title] = ;

        $(this).nextUntil('tr:has(th:only-child)').each(function()
            var t = $(this).cleanup(),
                headingEle = t.children('th'),
                data = t.children('td').html().trim();

            if(headingEle.length) 
                var heading = headingEle.html().trim();
            

            // Skip to next heading if current property is ignored
            if(~Settings.ignoredProperties.indexOf(heading)) 
                return true;
            

            if (rowspan) 
                output[title][rowspanHeading][data.split(':')[0].trim()] = data.split(':')[1].trim();
                rowspan--;
             else if (headingEle.attr('rowspan'))
                rowspan = headingEle.attr('rowspan') - 1;
                rowspanHeading = heading;

                output[title][heading] = ;
                output[title][heading][data.split(':')[0]] = data.split(':')[1];
             else if (~heading.indexOf(','))
                data = data.split(',');

                heading.split(',').forEach(function(v, i)
                    output[title][v.trim()] = data[i].trim();
                );
             else 
                output[title][heading] = data;
            
        );
    );

    writeToFile(output);


function fetchElements(elements) 
    elements.forEach(function(value)
        // Element URL used here as task id (second argument)
        chain.add(createTask(value, processElement), value);
    );


function processTable($)
    var elementArray = $(Settings.periodicSelector).find('td').map(function()
        var t = $(this),
            atomicN = parseInt(t.text(), 10);

        if(atomicN && t.children('a').length) 
            var elementUrl = 'http://' + parseUrl(Settings.periodicUrl).host + t.children('a:first').attr('href');

            console.log(atomicN, t.children('a:first').attr('href').split('/').pop(), elementUrl);
            return elementUrl;
        
    ).get();

    fetchElements(elementArray);
    fs.writeFile(Settings.pathPrefix + 'elements.json', JSON.stringify(elementArray));


// Get table - init
function getPeriodicList()
    var elementsList = Settings.pathPrefix + 'elements.json';

    if(path.existsSync(elementsList))
        var fileData = JSON.parse(fs.readFileSync(elementsList, 'utf8'));
        fetchElements(fileData);
     else 
        chain.add(createTask(Settings.periodicUrl, processTable));
    


getPeriodicList();

【问题讨论】:

【参考方案1】:

jsdom 确实存在内存泄漏,这源于节点vm.runInContext() 后面的复制输入和复制输出逻辑。一直在努力使用 c++ 解决这个问题,我们希望在尝试将其推送到节点之前证明解决方案。

目前的解决方法是为每个 dom 生成一个子进程,并在完成后将其关闭。

编辑:

从 jsdom 0.2.3 开始,只要在完成后关闭窗口 (window.close()),此问题就已解决。

【讨论】:

window.close() 完全正确,确保完成后关闭窗口,GC 将按预期工作。 :) 如果可以的话:你应该把它包含在 github 的首页自述文件中。我认为让用户知道需要清理窗口对象非常重要!也许它“显而易见”,但对我来说不是;)。【参考方案2】:

对于使用节点的类 jQuery html 处理,我现在使用 cheerio 而不是 jsdom。到目前为止,我在几个小时内对超过 10K 的页面进行报废和解析时没有发现任何内存泄漏。

【讨论】:

这是一个很好的提示。我有一些代码试图解析大约 25 兆字节的 html,并且 jsdom 在长时间延迟后因内存不足错误而崩溃。使用cheerio重写代码7秒完成,没有任何错误。 在现阶段,cheerio 与 jQuery 非常不同,在选择器和 DOM 包装器方面缺乏很多特性。它肯定更快,但不兼容,如果您需要 jQuery 的熟悉度和表现力,这是一个问题。【参考方案3】:

我认为我有一个更好的解决方法,通过设置 window.document.innerHTML 属性来重用您的 jsdom 实例。解决了我的内存泄漏问题!

    // jsdom has a memory leak when using multiple instance
    // cache a single instance and swap out innerHTML
    var dom = require('jsdom');
    var win;
    var useJQuery = function(html, fnCallback) 
        if (!win) 
            var defEnv = 
                html:html,
                scripts:['jquery-1.5.min.js'],
            ;
            dom.env(defEnv, function (err, window) 
                if (err) throw new Error('failed to init dom');
                win = window;
                fnCallback(window.jQuery);
            );
        
        else 
            win.document.innerHTML = html;
            fnCallback(win.jQuery);
        
    ;
    ....
    // Use it!
    useJQuery(html, function($)  $('woohoo').val('test'); );

【讨论】:

这对我来说真的不起作用,这是我的 sn-p 有效的方法:var jsdom = require('jsdom'); var win = jsdom.jsdom().createWindow(); var useJQuery = function(html, fnCallback) jsdom.jQueryify(win, "code.jquery.com/jquery.js", function () win.document.innerHtml = html; fnCallback(win); ); ;【参考方案4】:

我知道答案不多,但我遇到了类似的问题。我有多个刮板同时运行,内存泄漏。

我最终使用了 node-jquery 而不是 JSDOM

https://github.com/coolaj86/node-jquery

【讨论】:

不,我刚才实际上已经用 jsdom 替换了 node-jquery - 两者都以惊人的相似方式泄漏内存 为什么你认为不是你的代码泄漏了内存?更改库可能无济于事。

以上是关于Node.js 刮板中的内存泄漏的主要内容,如果未能解决你的问题,请参考以下文章

Node.js中的内存泄漏分析

如何防止 node.js 中的内存泄漏?

node.js 中的长循环泄漏内存

第1651期如何分析 Node.js 中的内存泄漏

Node.js内存泄漏分析

Node.js内存泄漏分析