JavaScript泄漏内存(Node.js / Restify / MongoDB)

时间:2014-03-22 10:29:54

标签: javascript node.js memory-leaks mongoose restify

更新4 :通过在函数外部实例化restify客户端(请参阅controllers / messages.js)并在每次请求后调用global.gc(),内存增长率似乎已经降低了很多(每10秒约500KB)。然而,内存使用量仍在不断增长。

Update3 :看过这篇文章:https://journal.paul.querna.org/articles/2011/04/05/openssl-memory-use/

值得注意的是,我正在使用HTTPS和Restify。

更新2 :将以下代码更新为当前状态。我试过用Express换掉Restify。可悲的是,这没有任何区别。似乎链条末端的api调用(restify - > mongodb - > external api)会导致所有内容保留在内存中。

更新1 :我已使用标准MongoDB驱动程序替换了Mongoose。内存使用似乎增长速度较慢,但​​泄漏仍然存在。


我一直在努力寻找这种泄漏几天。

我正在使用RestifyMongoose运行API,并且对于每个API调用,我至少执行一次MongoDB查找。我有大约1-2k用户在一天内多次点击API。

我尝试了什么

  • 我已将我的代码隔离到仅使用Restify并使用ApacheBench来发出大量请求(100k +)。测试期间内存使用率保持在60MB左右。
  • 我已将我的代码分离为仅使用Restify和Mongoose并以与上述相同的方式对其进行测试。内存使用率保持在80MB左右。
  • 我使用ApacheBench在本地测试了完整的生产代码。内存使用率保持在80MB左右。
  • 我已经按时间间隔自动转储了堆。我最大的堆转储是400MB。我只能看到有大量的字符串和数组,但我无法清楚地看到它的模式。

那么,可能出现什么问题?

我只使用一个API用户完成了上述测试。这意味着Mongoose只能一遍又一遍地抓取同一个文档。与生产的不同之处在于许多不同的用户都会使用API​​,这意味着mongoose会获得大量不同的文档。

当我启动nodejs服务器时,内存会迅速增长到100MB-200MB。它最终稳定在500MB左右。这是否意味着它会为每个用户泄漏内存?一旦每个用户访问过API,它都会稳定下来吗?

我在下面列出了我的代码,其中概述了我的API的一般结构。我很想知道我的代码或任何其他方法是否存在严重错误,以找出导致高内存使用的原因。

代码

app.js

var restify = require('restify');
var MongoClient = require('mongodb').MongoClient;

// ... setup restify server and mongodb

require('./api/message')(server, db);

API / message.js

module.exports = function(server, db) {

    // Controllers used for retrieving accounts via MongoDB and communication with an external api
    var accountController = require('../controllers/accounts')(db);        
    var messageController = require('../controllers/messages')();

    // Restify bind to put
    server.put('/api/message', function(req, res, next) {
        // Token from body
        var token = req.body.token;

        // Get account by token
        accountController.getAccount(token, function(error, account) {

            // Send a message using external API
            messageController.sendMessage(token, account.email, function() {
                res.send(201, {});
                return next();
            });
        });
    });
};

控制器/ accounts.js

module.exports = function(db) {

    // Gets account by a token
    function getAccount(token, callback) {
        var ObjectID = require('mongodb').ObjectID;

        var collection = db.collection('accounts');

        collection.findOne({
            token: token
        }, function(error, account) {

            if (error) {
                return callback(error);
            }

            if (account) {
                return callback('', account);
            }

            return callback('Account not found');
        });
    }
};

控制器/ messages.js

module.exports = function() {

    function sendMessage(token, email, callback) {

        // Get a token used for external API
        getAccessToken(function() {}

            // ... Setup client

            // Do POST
            client.post('/external_api', values, function(err, req, res, obj) {
                return callback();
            });

        });
    }

    return {
        sendMessage: sendMessage
    };
};

怀疑泄漏的快照 enter image description here

4 个答案:

答案 0 :(得分:2)

可能是getter中的一个错误,我在使用mongoose模式的虚拟或getter时得到了它https://github.com/LearnBoost/mongoose/issues/1565

答案 1 :(得分:2)

仅查看字符串和数组实际上是正常的,因为大多数程序主要基于它们。因此,允许按总对象计数进行排序的分析器没有多大用处,因为它们多次为许多不同的程序提供相同的结果。

使用chrome的内存分析的一种更好的方法是在一个用户调用API之后获取一个快照,然后在第二个用户调用API之后获取第二个堆快照。

分析器可以比较两个快照,看看它们之间有什么区别(见tutorial),这有助于理解为什么内存以意想不到的方式增长。

对象保留在内存中,因为仍然存在对它们的引用,以防止对象被垃圾回收。

因此,尝试使用分析器查找内存泄漏的另一种方法是查找您认为不应该存在的对象并查看它是retaining paths是什么,并查看是否存在任何意外路径。 / p>

答案 2 :(得分:1)

不确定这是否有帮助,但您是否可以尝试删除不必要的退货?

API / message.js

        // Send a message using external API
        messageController.sendMessage(token, account.email, function() {
            res.send(201, {});
            next(); // remove 'return'
        });

控制器/ accounts.js

module.exports = function(db) {

    // Gets account by a token
    function getAccount(token, callback) {
        var ObjectID = require('mongodb').ObjectID;

        var collection = db.collection('accounts');

        collection.findOne({
            token: token
        }, function(error, account) {

            if (error) {
                callback(error); // remove 'return'
            } else if (account) {
                callback('', account); // remove 'return'
            } else {
                callback('Account not found'); // remove 'return'
            }
        });
    }

    return { // I guess you missed to copy this onto the question.
        getAccount: getAccount
    };
};

控制器/ messages.js

            // Do POST
            client.post('/external_api', values, function(err, req, res, obj) {
                callback(); // remove 'return'
            });

答案 3 :(得分:0)

您的问题在于getAccount与GC工作方式的混合。

当你链接很多功能时,GC一次只清除一个,旧的东西在内存中收集的机会就越少,所以在你的获取帐户上,你至少需要6次调用全局。 gc()或auto在执行之前执行,此时GC会假定它可能不会收集的内容,因此无论如何它都不会检查它。

collection{
   findOne{
      function(error, account){
         callback('', account)
            sendMessage(...)
               getAccessToken(){
                  Post
               }
            }
         }
      }
   }
}

基因建议删除此链接。

PS:这只是GC工作方式的一种表示,取决于实施,但你明白了。