Nodejs Mongoose/MongoDB,更新subdocument的数组奇慢无比

Nodejs Mongoose/MongoDB,更新subdocument的数组奇慢无比

新手真诚求教。

我现在正在用Mongoose/MongoDB,为一个小网站存储用户的信息。

在做数据迁移的时候,发现保存的速度奇慢无比。存储前200个数据,只需要几秒,到后来随着数据的增多存储速度直线下降。当到600个的时候已经下降到了1个/秒,1500个的时候已经变成了2个/秒。。。这个速度实在无法接受啊。

经过测试后,我发现问题出现在了插入时与另一个Model交互的地方。是这样的,我有一个User的Model存储信息,同时还有一个Group的Model用来控制用户权限。User有一个groups的数组存储它所属于哪个,Group有一个member的subdocument的数组,存储属于他的user的ObjectId和username方便查询,简而言之就是这俩是个多对多的关系。Group有几十个,用户大概一万左右。

下面是那部分的代码,通过输出的log信息可以发现,速度就是慢在了这部分。数据量越大,这部分的运行时间越长,保存一个User的文档一直都是ms级别的。我尝试着把更改member的操作由addToSet改为了push,但是性能没有显著的提升。

User.prototype.addGroupsAndSave = function (groups, callback) {
  log.debug('before adding groups');
  // ToDo May have duplicate problems
  if (!Array.isArray(groups)) {
    groups = [groups];
  }
  var user = this;
  var userRef = {objId: user.id, username: user.username};
  async.map(groups, function addToGroup(group, cb) {
    Group.findOne({groupName: group}, function (err, group) {
      if (err) {
        return cb(err);
      }
      if (_.isEmpty(group)) {
        return cb(new Error('No such groups'));
      }
      // group.member.addToSet(userRef);
      group.member.push(userRef);
      group.save(cb);
    });
  },
  function (err) {
    if (err) {
      return callback(err);
    }
    log.debug('after adding groups');
    user.groups.addToSet.apply(user.groups,groups);
    user.save(callback);
  });
};

如果直接用mongo_native迁移的话应该会快一些,但是因为以前的数据,有一些有问题,我想用Mongoose做验证,而且这样也能测试使用环境下的性能。

问题出在哪了呢?不解决这个问题,不敢部署网站啊,性能太渣了。


9 回复

您的问题确实涉及到Mongoose在处理嵌套文档(subdocuments)数组时的性能问题。主要原因在于每次调用group.save(cb)时,MongoDB都会执行一次完整的更新操作,导致性能严重下降。

性能瓶颈分析

  1. 多次保存操作:每次调用group.save(cb)时,都会触发一次数据库写入操作。对于大量数据,这种频繁的I/O操作会导致性能显著下降。
  2. 同步问题:使用async.map来并行处理每个group的更新可能会导致并发问题,尤其是在处理大量数据时。

解决方案

我们可以采用批量更新的方式,减少数据库的I/O操作次数。具体做法是先将所有需要更新的数据收集起来,然后一次性进行更新。

示例代码

const mongoose = require('mongoose');
const User = mongoose.model('User', new mongoose.Schema({}));
const Group = mongoose.model('Group', new mongoose.Schema({
  groupName: String,
  member: [{ objId: mongoose.Schema.Types.ObjectId, username: String }]
}));

User.prototype.addGroupsAndSave = async function (groups, callback) {
  log.debug('before adding groups');

  if (!Array.isArray(groups)) {
    groups = [groups];
  }

  const userRef = { objId: this.id, username: this.username };
  const updatePromises = [];

  for (const group of groups) {
    const groupToUpdate = await Group.findOne({ groupName: group }).exec();
    if (!groupToUpdate) {
      return callback(new Error('No such group'));
    }

    groupToUpdate.member.push(userRef);
    updatePromises.push(groupToUpdate.save());
  }

  try {
    await Promise.all(updatePromises);
    log.debug('after adding groups');
    this.groups.addToSet(...groups);
    await this.save();
    callback(null);
  } catch (err) {
    callback(err);
  }
};

解释

  1. 异步处理:使用async/await来处理异步操作,使代码更简洁易读。
  2. 批量更新:将所有需要更新的Group对象收集到一个数组中,然后一次性保存。
  3. 错误处理:通过Promise.all来确保所有更新操作都成功完成,否则捕获错误并返回给回调函数。

通过这种方式,您可以显著提高性能,避免频繁的数据库I/O操作。希望这对您有所帮助!


问题出在nodejs的并发上面,它是并发了,但是数据库后面的兄弟们就受苦了,人家哪处理的过来啊,我之前做统计的时候也遇到这个问题了,后来还是在nodejs这边的程序上下手搞定的

当然了,我是这个理解的,也可能不正确

是因为mongodb 文档增大导致移动了吧,预先用垃圾数据填充下试试。一般是建议不断增大的子文档弄到其它集合去

这个问题考虑过了,我在插入的使用async.eachSeries进行串行插入。

解决方案看我楼下的回复

作死的典型代表。。。。你調一下你MongoDB的ProfilingLevel看一下 你這個更新操作我覺得應該是秒級別的。。。

几番Google之后,发现问题出在了Mongoose上,对于所有的查询到的结果,其都会进行一番处理,转成MongooseDocuments。结果就是对速度会有很大的影响,相较于MongoDB_native大约是3倍的时间消耗。这个转化是为了方便再一次save时进行验证的,所以如果对大文档是只读的查询,最好在查询时设置 lean。

但是我的情况不一样,我需要更新我的member数组,不过我并不特别关心更新了后的member数组会变成什么样,而且对于member也不关心验证。所以我使用了这样的更改。

    Group.findOneAndUpdate({groupName: groupName}, {
      $addToSet: {
        member: userRef,
      }
    },{
      lean: true,
      select: 'groupName',
    }, callback);

就是说直接使用Model.findOneAndUpdate跳过Mongoose的验证,并且查询时避开member这个大数组。

是啊。。。已解决。。。

不过乃回复的时候,能不能附上原因,否则对其他人没有帮助啊

从描述来看,性能瓶颈可能在于每次添加用户到组时都进行一次数据库查询和保存操作。这种情况下,每次调用 Group.findOnegroup.save 都会产生一次数据库 I/O 操作,导致性能急剧下降。

可以通过以下几种方式来优化:

  1. 批量处理:减少数据库查询和保存操作的次数。可以一次性加载所有需要修改的组,并且一次性保存。
  2. 避免重复查询:将已查询过的组缓存起来,避免重复查询。

示例代码如下:

User.prototype.addGroupsAndSave = async function (groups, callback) {
  log.debug('before adding groups');
  
  if (!Array.isArray(groups)) {
    groups = [groups];
  }

  const userRef = { objId: this.id, username: this.username };

  try {
    // 加载所有需要修改的组
    const groupPromises = groups.map(groupName => 
      Group.findOne({ groupName }).exec()
    );
    
    const groupsToUpdate = await Promise.all(groupPromises);

    for (const group of groupsToUpdate) {
      if (!group) {
        throw new Error('No such group');
      }
      group.member.push(userRef);
    }

    // 批量保存所有组
    await Promise.all(groupsToUpdate.map(group => group.save()));

    // 更新用户信息
    this.groups.addToSet(...groups);
    await this.save();

    log.debug('after adding groups');
    callback(null);
  } catch (err) {
    callback(err);
  }
};

解释

  1. 批量加载组:使用 Promise.all 同时加载所有需要修改的组,而不是逐一加载。
  2. 批量保存组:在所有组加载完毕后,一次性保存所有组,而不是逐个保存。
  3. 用户更新:最后更新用户的 groups 字段并保存。

这种方法减少了不必要的数据库查询和保存操作,从而提高整体性能。

回到顶部