为什么Node.js以这种方式执行?

时间:2014-07-07 22:19:37

标签: node.js mongodb coffeescript mongoose async.js

我有一个Node.js应用程序,用于将记录从MySql迁移到MongoDB。我正在使用Mongoose和async.js来做这个,我注意到一些我不理解的行为。如果我有以下Coffeescript代码(javascript here):

           # users is a collection of about 70k records
async.each users, ((user, callback) =>
    # console.log "saving user: #{user.id} of #{users[users.length-1].id}"
    model = new User
        id: user.id
        name:
            first: user.fname
            last: user.lname
    model.save (err) ->
        console.log "saving user: #{user.id}"
        model = null
        callback(err)
), (err) ->
    users = null
    callback(err)

永远不会达到model.save的回调,我的Node进程将慢慢爬升到1.5gb。如果我检查我的mongodb实例,我可以看到在users集合中的所有70k项目都已处理完毕后,记录将开始保存到mongodb,但它们会在41k左右停止。

我注意到,如果我从async.each切换到async.eachSeries,则会为每条记录恢复model.save的回调,并且迁移成功完成。

我假设由于某种原因,Node在执行users回调之前对model.save集合中的每个项目运行async.each的每次迭代,这会导致内存问题,但我不明白为什么会这样。任何人都可以告诉我为什么Node会这样做,以及为什么切换到async.eachSeries会解决这个问题?

2 个答案:

答案 0 :(得分:4)

Neil在提供解决方案方面做得很好,但我只是想谈谈你的问题:

  

任何人都可以告诉我为什么Node会这样做,以及为什么切换到async.eachSeries可以解决这个问题?

如果您查看async.eachasync.eachSeries的详细信息,您可能会注意到async.each州的文档:

  

将函数迭代器应用于arr中的每个项目,并行

但是,async.eachSeries声明:

  

与每个相同,只有迭代器应用于arr中的每个项目。只有当前的迭代器完成后才会调用下一个迭代器。这意味着迭代器函数将按顺序完成。

详细地说,如果我们查看代码,您会看到each的代码最终调用数组本身的本地forEach函数,并且每个元素都调用迭代器({{ 3}}):

_each(arr, function (x) {
    iterator(x, only_once(done) );
});

调用:

var _each = function (arr, iterator) {
    if (arr.forEach) {
        return arr.forEach(iterator);
    }

但是,每次调用迭代器函数都会调用model.save。此Mongoose函数(以及其他内容)最终执行I / O以将数据保存到数据库。如果您要跟踪代码路径,则会看到它最终出现在调用process.nextTicklink to source)的函数中。

节点的link to source函数通常用于此类(I / O)的情况,并将在执行流程结束后处理回调。在这种情况下,只有forEach循环完成后才会调用每个回调。 (这是有目的的,并且意味着不阻止任何代码执行。)

总结一下:

使用async.each时,上面的代码将遍历所有用户,排队保存,但只有在代码完成迭代所有用户后才开始处理它们。

使用async.eachSeries时,上面的代码将逐个处理每个用户,并且只有在保存完成后才会处理下一个用户 - 当调用eachSeries回调时。

答案 1 :(得分:1)

在您的过程中抛出厨房水槽肯定存在问题。它基本上是你所要求的,因此试图异步地#34;旋转"所有这些"保存"立即行动。基本的现实是,你只能与MongoDB建立如此多的连接,以便在执行此操作时可能会出现瓶颈。

比在"系列"中更好的方法。如果你实际上不需要以明确的顺序完成操作,那就是使用"限制"关于你排队的操作数量。有async.eachLimit()来做这件事。

调用约定在这里看起来有点奇怪,所以至少对我来说这似乎有点清晰:

async.eachLimit(users,500,function(user,callback){
    var model = new Model({
        id: user.id,
        name: {
            first: user.fname,
            last: user.lname
        }
    });
    model.save(function(err, model) {
        console.log("saving user: " + model.id);
        callback(err);
    });
}, function(err) {
    if (err) {
        console.log("there was a problem");
    } else {
        console.log("all successful");
    }
});

或者作为基本翻译的coffeescript:

async.eachLimit users, 500, ((user, callback) ->
  model = new Model(
    id: user.id
    name:
      first: user.fname
      last: user.lname
  )
  model.save (err, model) ->
    console.log "saving user: " + model.id
    callback err
    return

  return
), (err) ->
  if err
    console.log "there was a problem"
  else
    console.log "all successful"
  return

然后最终回调将在所有回调返回后进行处理,但是你是"限制"你在mongoose和MongoDB上投掷的是什么。

除非您明确需要使用"验证"否则您可能还需要查看MongoDB的Bulk Operations API。模型中的函数或其他函数。这基本上允许您发送"批次"一次插入,而不是将每个文档一次发送到数据库"一次一个"。

这里有一个例子,使用每个系列,但实际的"写"分组:

var async = require("async"),
    mongoose = require("mongoose"),
    Schema = mongoose.Schema;

mongoose.connect('mongodb://localhost/test');

var tenSchema = new Schema({
  value: Number
});

var Ten = mongoose.model( "Ten", tenSchema, "ten" );

var ten = [1,2,4,5,6,7,8,9,10];
var pos = 0;

mongoose.connection.on("open",function(err,conn) {

  var bulk = Ten.collection.initializeOrderedBulkOp();

  async.eachSeries(ten,function(item,callback) {

    bulk.insert({ "value": item });
    pos++;

    if ( pos % 2 == 0 ) {
      bulk.execute(function(err,res) {
        pos = 0;
        bulk = Ten.collection.initializeOrderedBulkOp();
        callback(err);
      });
    } else {
      callback();
    }

  },function(err) {

    if (err)
      throw err;

    if ( pos != 0 ) {
      bulk.execute(function(err,result) {
        console.log("done");
      });
    } else {
      console.log("done");
    }

  });

});

所以在你的情况下只是" up"要计算模数的值,比如500,这将处理数组,但每500项只写一次数据库。

唯一要注意的是这是一个本机驱动程序函数,而不是使用mongoose API。因此,在引用这些方法之前,需要注意(在迁移脚本或类似的情况下)以确保建立当前连接。这里人为的方法是寻找" open&#34 ;,但基本上你只是想通过其他方式来确定。

你可以通过并行排队"批量写入"来获得更好的体验,但是一般性能应该比任何其他方法都要好,而不需要进一步考虑。