有遇到过node out of memory的吗?
发布于 12 年前 作者 darklowly 9189 次浏览 最后一次编辑是 8 年前

为了方便大家看到重点重新编辑了一下

做了一个简单的测试,代码如下:

var mongoose = require('mongoose'),
    Schema   = mongoose.Schema;
    
var UserSchema = new Schema({
    name   : { type: String, index: true }, 
    pass   : { type: String },
    email  : { type: String },
});

var User = mongoose.model('User', UserSchema);

mongoose.connect('mongodb://127.0.0.1/outmem');

for ( var i = 0; i < 1000000; i++ ) {
    user       = new User();
    user.name  = 'outmem';
    user.pass  = '123456';
    user.email = 'outmem[@outmem](/user/outmem).com';
    
    user.save();
    console.log(i);
}

或者是加一个:

 delete user;

都会出现内存用完的情况,windows和linux截图如下:

windows截图

linux截图

不知道什么原因.是还没来得及回收内存还是因为我的代码有问题?

下面是进一步测试的结果:

代码修改如下,把分配和保存放到函数里面,任然一样:

    var mongoose = require('mongoose'),
        Schema   = mongoose.Schema;
        
    //
    // 为了让效果更快的出现,附加的数据
    //
    var AppendData = new  Schema({
        name   : { type: String }, 
        pass   : { type: String },
        email  : { type: String },
    });
        
    var UserSchema = new Schema({
        name   : { type: String, index: true }, 
        pass   : { type: String },
        email  : { type: String },
        
        append : [AppendData]
    });

    var User = mongoose.model('User', UserSchema);

    mongoose.connect('mongodb://127.0.0.1/outmem');

    var addCount = 0;

    function addUser() {
        console.log('addUser' + (addCount++));

        user       = new User();
        user.name  = 'outmem';
        user.pass  = '123456';
        user.email = 'outmem[@outmem](/user/outmem).com';
        
        for ( var j = 0; j < 1000; j++ ) {
            user.append.push({name: 'outmem', pass: '123456', email: 'outmem[@outmem](/user/outmem).com'});
        }
        
        user.save(function(err) {
            if ( err ) {
                console.log('save error');
            } 
        });
    }

    while ( true ) {
        addUser();
    }

代码修改如下,通过定时器回调,表现良好:

    var mongoose = require('mongoose'),
        Schema   = mongoose.Schema;
        
    //
    // 为了让效果更快的出现,附加的数据
    //
    var AppendData = new  Schema({
        name   : { type: String }, 
        pass   : { type: String },
        email  : { type: String },
    });
        
    var UserSchema = new Schema({
        name   : { type: String, index: true }, 
        pass   : { type: String },
        email  : { type: String },
        
        append : [AppendData]
    });

    var User = mongoose.model('User', UserSchema);

    mongoose.connect('mongodb://127.0.0.1/outmem');

    var addCount = 0;

    function addUser() {
        console.log('addUser' + (addCount++));

        user       = new User();
        user.name  = 'outmem';
        user.pass  = '123456';
        user.email = 'outmem[@outmem](/user/outmem).com';
        
        for ( var j = 0; j < 1000; j++ ) {
            user.append.push({name: 'outmem', pass: '123456', email: 'outmem[@outmem](/user/outmem).com'});
        }
        
        user.save(function(err) {
            if ( err ) {
                console.log('save error');
            } 
        });
    }

    setInterval(addUser, 1);

继续修改如下,表现良好:仔细看addUserStub这个函数,如果把里面的数字修改大一点还是会OOM.

    var mongoose = require('mongoose'),
        Schema   = mongoose.Schema;
        
    //
    // 为了让效果更快的出现,附加的数据
    //
    var AppendData = new  Schema({
        name   : { type: String }, 
        pass   : { type: String },
        email  : { type: String },
    });
        
    var UserSchema = new Schema({
        name   : { type: String, index: true }, 
        pass   : { type: String },
        email  : { type: String },
        
        append : [AppendData]
    });

    var User = mongoose.model('User', UserSchema);

    mongoose.connect('mongodb://127.0.0.1/outmem');

    var addCount = 0;

    function addUser() {
        console.log('addUser' + (addCount++));

        user       = new User();
        user.name  = 'outmem';
        user.pass  = '123456';
        user.email = 'outmem[@outmem](/user/outmem).com';
        
        for ( var j = 0; j < 1000; j++ ) {
            user.append.push({name: 'outmem', pass: '123456', email: 'outmem[@outmem](/user/outmem).com'});
        }
        
        user.save(function(err) {
            if ( err ) {
                console.log('save error');
            } 
        });
    }

    function addUserStub() {
        for ( var i = 0; i < 10; i++ ) { // 这里的值不能过大,过大的话也会出现
            addUser();
        }
    }

    setInterval(addUserStub, 1);

这个时候我就在想到底是mongoose的原因还是node的原因?为了排除是mongoose导致的,所以修改代码如下,删除了user.save(),让mongoose不和数据库之间交互,只无限的分配内存,结果出乎意料,表现很良好

    var mongoose = require('mongoose'),
        Schema   = mongoose.Schema;
        
    //
    // 为了让效果更快的出现,附加的数据
    //
    var AppendData = new  Schema({
        name   : { type: String }, 
        pass   : { type: String },
        email  : { type: String },
    });
        
    var UserSchema = new Schema({
        name   : { type: String, index: true }, 
        pass   : { type: String },
        email  : { type: String },
        
        append : [AppendData]
    });

    var User = mongoose.model('User', UserSchema);

    mongoose.connect('mongodb://127.0.0.1/outmem');

    var addCount = 0;

    function addUser() {
        console.log('addUser' + (addCount++));

        user       = new User();
        user.name  = 'outmem';
        user.pass  = '123456';
        user.email = 'outmem[@outmem](/user/outmem).com';
        
        for ( var j = 0; j < 10000; j++ ) {
            user.append.push({name: 'outmem', pass: '123456', email: 'outmem[@outmem](/user/outmem).com'});
        }
    }

    for ( var i = 0; i < 10000000; i++ ) {
        addUser();
    }

最开始我以为是node单线程导致的,因为一直循环没有机会执行内存回收,最后这段代码证明不是因为这个原因,node还是没有低级到这个地步的.

通过这些代码,初步猜测,有可能有两个原因:

  有可能是因为mongoose缓存了用户操作,当有很多的请求的时候,mongodb处理不过来,缓存的操作占用了很大的内存,并没有来得及释放.这样看的话主要原因在于mongoose缓存操作的问题.

 另一可能是mongoose在和mongodb交互的时候占用了大量内存,没来得及释放那么为什么没来得及释放呢?所以也有可能是因为node在调用路径太深的时候,内存回收算法有问题?

我是5月29号才接触node以及相关的东西,所以我的javascript水平很菜,所以暂时只能做一些黑盒测试,恕我不能继续深入分析mongoose的代码了.期待某位同学能够深入一下,不过也有可能是因为我自己写的代码什么地方不对,如果某位同学发现了,留个言解释一下,感激不尽。

感谢 @wenbob 的分析,虽然我不赞成你的结果,但是让我也进一步的测试了.具体测试在下面的回复中.

13 回复

你用方法封装的时候,方法调用时创建作用域,执行结束,释放内存。 但是第一种方式,没有这个过程,所有内存被占用了都没有被释放,直到大循环执行完毕。但是由于循环太大,内存直接就不够了。

和方法封装没有关系的.

我猜测是因为mongoose缓存请求导致的内存泄漏的可能性要大很多。

问题出在for循环,循环如果不结束,v8没有执行gc的机会,于是就OOM了。解决的办法很简单,改写这个for循环,把循环体改成

var i = 0;
function for1000000loops(){
    user       = new User();
    user.name  = 'outmem';
    user.pass  = '123456';
    user.email = 'outmem@outmem.com';
    user.save();
    console.log(i++);
    if (i < 1000000)
        process.nextTick(for1000000loops);
}

这样,gc就有执行的机会了,不会导致积累的内存消耗,而且速度仍然飞快。

这个问题涉及到v8的script执行机制,其实和数据库无关,和mongoose也无关。做个OOM的测试:

//OOM测试
for ( var i = 0; i < 100000000; i++ ) {
    var user       = {};
    user.name  = 'outmem';
    user.pass  = '123456';
    user.email = 'outmem@outmem.com';
}

这段for循环在执行的时候,会不停增长内存。适当加大循环次数,就能导致OOM。

同等功能的带gc的代码如下:

var i = 0;
function for100000000loops(){
    var user       = {};
    user.name  = 'outmem';
    user.pass  = '123456';
    user.email = 'outmem@outmem.com';

    if (i++ < 100000000)
        process.nextTick(for100000000loops);
}
for100000000loops();

上述代码不会引发OOM,但是执行时间增加了不少,因为每循环一次,都有一次机会回到v8,效率低一些。要在速度和安全之间取得平衡,可以考虑在函数体内使用适当数量的循环,只要控制次数,不导致OOM,这样就可以二者兼得了。

赞。回复的好精确。

    var mongoose = require('mongoose'),
        Schema   = mongoose.Schema;

    //
    // 为了让效果更快的出现,附加的数据
    //
    var AppendData = new  Schema({
        name   : { type: String }, 
        pass   : { type: String },
        email  : { type: String }
    });

    var UserSchema = new Schema({
        name   : { type: String, index: true }, 
        pass   : { type: String },
        email  : { type: String },

        append : [AppendData]
    });

    var User = mongoose.model('User', UserSchema);

    mongoose.connect('mongodb://127.0.0.1/outmem');

    var addCount = 0;

    function addUser() {
    console.log('addUser' + (addCount++));

        user       = new User();
        user.name  = 'outmem';
        user.pass  = '123456';
        user.email = 'outmem@outmem.com';

        for ( var j = 0; j < 10000; j++ ) {
            user.append.push({name: 'outmem', pass: '123456', email: 'outmem@outmem.com'});
        }
        
        user.save();
    }

    for ( var i = 0; i < 10000000; i++ ) {
        addUser();
    }

这段代码任然OOM,可以证明和函数无关.

我最开始也这么想,后来经过测试发现不是 直接在循环里面new不用nextTick也不会OOM,下面的代码不会OOM,可以证明不是因为循环导致没有gc.

    var mongoose = require('mongoose'),
        Schema   = mongoose.Schema;

    //
    // 为了让效果更快的出现,附加的数据
    //
    var AppendData = new  Schema({
        name   : { type: String }, 
        pass   : { type: String },
        email  : { type: String }
    });

    var UserSchema = new Schema({
        name   : { type: String, index: true }, 
        pass   : { type: String },
        email  : { type: String },

        append : [AppendData]
    });

    mongoose.connect('mongodb://127.0.0.1/outmem');
    var User = mongoose.model('User', UserSchema);

    var count = 0;

    while ( true ) {
        user       = new User();
        user.name  = 'outmem';
        user.pass  = '123456';
        user.email = 'outmem@outmem.com';

        for ( var j = 0; j < 10000; j++ ) {
            user.append.push({name: 'outmem', pass: '123456', email: 'outmem@outmem.com'});
        }
        
        console.log('new ' + count++);
    }

下面的代码运行没有问题,说明不是循环的问题

    while ( true ) {
        var user   = {};
        user.name  = 'outmem';
        user.pass  = '123456';
        user.email = 'outmem@outmem.com';
    }

当然process.nextTick是一个解决办法,上面的代码说明了,在这个问题上本质上好像不是nextTick引起的。

你最后一段代码我不明白为什么会把for写到函数里面,这个会导致无限递归。因为你限制了大小所以不会导致无线递归。当然这个不是主要问题,主要问题是在for里面调用nextTick会因为nextTick导致OOM,例如你把代码修改如下,同样的OOM,

    var i = 0;
    function for100000000loops(){
        var user       = {};
        user.name  = 'outmem';
        user.pass  = '123456';
        user.email = 'outmem@outmem.com';

        while ( true )
            process.nextTick(for100000000loops);
    }

    for100000000loops();

这里只是用了死循环来测试,如果会OOM,说明你设定的数字变大的时候这种方法也有可能会导致OOM, 为了排除是堆栈占用内存的可能性,修改该代码如下,依旧OOM:

function for100000000loops(){
    var user       = {};
    user.name  = 'outmem';
    user.pass  = '123456';
    user.email = 'outmem@outmem.com';
}

while ( true ) {
    process.nextTick(for100000000loops);
}

并且for100000000loops里面的代码根本没有执行,说明nextTick的时候会分配一个对象来记录 当数据量太大的时候也会OOM,相反如果直接循环反而不会例如:

    while ( true ) {
        var user   = {};
        user.name  = 'outmem';
        user.pass  = '123456';
        user.email = 'outmem@outmem.com';
    }

或者写到函数里面,都不会OOM

    function alloc() {
        var user   = {};
        user.name  = 'outmem';
        user.pass  = '123456';
        user.email = 'outmem@outmem.com';
    }

    while ( true ) {
        alloc();
    }

如果要试用nextTick应该像下面这样:

    function alloc() {
        var user   = {};
        user.name  = 'outmem';
        user.pass  = '123456';
        user.email = 'outmem@outmem.com';
        process.nextTick(alloc);
    }

    alloc();

可以高效运行,而且也可以保持内存回收.

最后结合nextTick和mongoose测试发现不会OOM

    var mongoose = require('mongoose'),
        Schema   = mongoose.Schema;

    //
    // 为了让效果更快的出现,附加的数据
    //
    var AppendData = new  Schema({
        name   : { type: String }, 
        pass   : { type: String },
        email  : { type: String }
    });

    var UserSchema = new Schema({
        name   : { type: String, index: true }, 
        pass   : { type: String },
        email  : { type: String },

        append : [AppendData]
    });

    var User = mongoose.model('User', UserSchema);
    mongoose.connect('mongodb://127.0.0.1/outmem');

    var addCount = 0;

    function addUser() {
        console.log('addUser' + (addCount++));

        user       = new User();
        user.name  = 'outmem';
        user.pass  = '123456';
        user.email = 'outmem@outmem.com';

        for ( var j = 0; j < 10000; j++ ) {
            user.append.push({name: 'outmem', pass: '123456', email: 'outmem@outmem.com'});
        }
        
        user.save();
        
        process.nextTick(addUser);
    }

    addUser();

不知道是因为nextTick降低了速度导致mongoose缓存的操作不会OOM,还是因为nextTick正的有机会回收内存了?但是通过上面的分析,发现循环并不会导致OOM,所以真正的原因任然不得而知.不过,遇到大量计算任务的时候通过nextTick是一个比较好的实践。知道有BUG,找到原因,尽量解决,能力不够解决不了,就只能避开BUG了.

回到顶部