【问题标题】:Insert performance of node-mongodb-nativenode-mongodb-native 的插入性能
【发布时间】:2012-06-20 00:21:44
【问题描述】:

我正在使用 MongoDB 测试 Node.js 的性能。我知道这些中的每一个都很好,彼此独立,但我正在尝试一些测试来感受它们。我遇到了这个问题,我无法确定来源。

问题

我正在尝试在单个 Node.js 程序中插入 1,000,000 条记录。 它绝对会爬。我们说的是 20 分钟的执行时间。无论是我的 Mac 还是 CentOS,都会发生这种情况,尽管两者的行为略有不同。它最终会完成。

效果类似于交换,但不是(内存永远不会超过 2 GB)。 MongoDB 只打开了 3 个连接,而且大多数时候没有插入数据。它似乎做了很多上下文切换,并且 Node.js CPU 内核已被最大化。

效果与this thread中提到的类似。

我尝试使用 PHP 进行相同的操作,它会在 2-3 分钟内完成。没有剧情。

为什么?

可能的原因

我目前认为这要么是 Node.js 套接字问题,要么是 libev 在幕后发生的事情,要么是其他一些 node-mongodb-native 问题。我可能完全错了,所以我在这里寻求一些指导。

至于其他 Node.js MongoDB 适配器,我尝试过蒙古语,它似乎对文档进行排队以便批量插入它们,但最终内存不足。所以就这样了。 (旁注:我也不知道为什么会这样,因为它甚至没有接近我的 16 GB 盒子限制——但我没有费心对此进行进一步调查。)

我应该提一下,实际上我确实测试了一个有 4 个工作器的主/工作器集群(在四核机器上),它在 2-3 分钟内完成。

守则

这是我的 Node.js CoffeeScript 程序:

mongodb = require "mongodb"
microtime = require "microtime"
crypto = require "crypto"

times = 1000000
server = new mongodb.Server "127.0.0.1", 27017
db = mongodb.Db "test", server
db.open (error, client) ->
  throw error if error?

  collection = mongodb.Collection client, "foo"

  for i in [0...times]
    console.log "Inserting #{i}..." if i % 100000 == 0

    hash = crypto.createHash "sha1"
    hash.update "" + microtime.now() + (Math.random() * 255 | 0)
    key = hash.digest "hex"

    doc =
      key: key,
      foo1: 1000,
      foo2: 1000,
      foo3: 1000,
      bar1: 2000,
      bar2: 2000,
      bar3: 2000,
      baz1: 3000,
      baz2: 3000,
      baz3: 3000

    collection.insert doc, safe: true, (error, response) ->
      console.log error.message if error

这是大致等效的 PHP 程序:

<?php
$mongo = new Mongo();
$collection = $mongo->test->foo;

$times = 1000000;
for ($i = 0; $i < $times; $i++) {
    if ($i % 100000 == 0) {
        print "Inserting $i...\n";
    }

    $doc = array(
        "key" => sha1(microtime(true) + rand(0, 255)),
        "foo1" => 1000,
        "foo2" => 1000,
        "foo3" => 1000,
        "bar1" => 2000,
        "bar2" => 2000,
        "bar3" => 2000,
        "baz1" => 3000,
        "baz2" => 3000,
        "baz3" => 3000
    );
    try {
        $collection->insert($doc, array("safe" => true));
    } catch (MongoCursorException $e) {
        print $e->getMessage() . "\n";
    }
}

【问题讨论】:

  • 好吧,说句公道话,你并不真正了解我的用例。这只是一个测试,几个测试之一。我遇到了一个有趣的结果,想知道它是否适用于整个 Node.js 或这个特定模块。无论哪种方式,理解原因有助于我更好地理解如何更有效地使用 Node.js 和/或这个模块。 :-)
  • 没错,这也可以是客户端库。
  • 如果 node.js CPU 内核已经用完,你是不是已经发现了问题。任何基于 JS 的单进程应用程序服务器都不会达到接近最大吞吐量的任何目标。如果你在同一个盒子上启动多个 node.js 进程会发生什么?老实说,我完全不明白为什么 node.js 会因为这些原因而在 Web 应用程序开发领域的更专业端获得任何牵引力,但是嘿。
  • 问题是为什么单线程 PHP 进程优于单线程 Node.js 进程(当然,对于使用 node- mongodb-native 适配器。固定的 CPU 只是问题的一个症状。我应该提一下,我确实测试了一个有 4 个工作人员(在四核机器上)的主/工作集群,它在 3 分钟内完成。
  • 这就是我的怀疑。我的单个进程爬网是因为 Node 在后台启动了许多事件线程,并反复轮询它们以查看哪些完成。幕后轮询的成本导致其速度急剧下降。随着回调数量的增加,执行时间似乎以对数方式增加。

标签: php performance node.js mongodb


【解决方案1】:

听起来您遇到了 V8 中的默认堆限制。我写了一封 blog post 来删除这个限制。

垃圾收集器可能会发疯并咀嚼 CPU,因为它会不断执行直到您低于 1.4GB 限制。

【讨论】:

  • 完美,这就是我一直在寻找的答案。在 Node.js 0.8.0 上,我运行了node --trace-gc(在发现此选项之后),发现一旦达到某个点,它将不断尝试将标记速度加快到 1000(可能是硬编码的最大值)。当然它会继续落后,所以它会再次尝试加快标记速度……最终,它会非常缓慢地再次开始标记扫描。
【解决方案2】:

如果在 db.open 回调函数的末尾显式返回一个值会发生什么?您生成的 javascript 代码正在将您的所有 collection.insert 返回推送到一个大的“_results”数组中,我想这会越来越慢。

db.open(function(error, client) {
  var collection, doc, hash, i, key, _i, _results;
  if (error != null) {
    throw error;
  }
  collection = mongodb.Collection(client, "foo");
  _results = [];
  for (i = _i = 0; 0 <= times ? _i < times : _i > times; i = 0 <= times ? ++_i : --_i) {
    ...
    _results.push(collection.insert(doc, {
      safe: true
    }, function(error, response) {
      if (error) {
        return console.log(error.message);
      }
    }));
  }
  return _results;
});

尝试在你的咖啡脚本末尾添加这个:

    collection.insert doc, safe: true, (error, response) ->
      console.log error.message if error

  return

*更新:* 所以,我实际上尝试运行您的程序,并发现了更多问题:

最大的问题是您试图以同步方式产生一百万个插入,这会真正杀死您的 RAM,并最终停止插入(至少对我来说是这样)。我在 800MB RAM 左右杀死了它。

您需要更改调用 collection.insert() 的方式,使其异步工作。

我像这样重写了它,为了清楚起见,分解了几个函数:

mongodb = require "mongodb"
microtime = require "microtime"
crypto = require "crypto"

gen  = () ->
  hash = crypto.createHash "sha1"
  hash.update "" + microtime.now() + (Math.random() * 255 | 0)
  key = hash.digest "hex"

  key: key,
  foo1: 1000,
  foo2: 1000,
  foo3: 1000,
  bar1: 2000,
  bar2: 2000,
  bar3: 2000,
  baz1: 3000,
  baz2: 3000,
  baz3: 3000

times = 1000000
i = times

insertDocs = (collection) ->
  collection.insert gen(), {safe:true}, () ->
    console.log "Inserting #{times-i}..." if i % 100000 == 0
    if --i > 0
      insertDocs(collection)
    else
      process.exit 0
  return

server = new mongodb.Server "127.0.0.1", 27017
db = mongodb.Db "test", server
db.open (error, db) ->
  throw error if error?
  db.collection "foo", (err, collection) ->
    insertDocs(collection)
    return
  return

大约 3 分钟后完成:

wfreeman$ time coffee mongotest.coffee
Inserting 0...
Inserting 100000...
Inserting 200000...
Inserting 300000...
Inserting 400000...
Inserting 500000...
Inserting 600000...
Inserting 700000...
Inserting 800000...
Inserting 900000...

real    3m31.991s
user    1m55.211s
sys 0m23.420s

此外,它还具有使用

【讨论】:

  • 这是一个很好的捕获,它确实在程序的部分生命周期内减少了大约 150 MB 的内存占用,但不幸的是问题不在于内存。不过,在使用 CoffeeScript 开发和处理大型集合时,牢记这一点绝对是一件好事。
  • 它也应该加快速度。没有?
  • 不——它受 CPU 限制,而不是内存限制。在这种情况下,内存不会影响进程。
  • 我自己对节点还很陌生,但看起来这样做的方式更加异步。
  • 感谢您的补充。你是对的,在正常情况下,序列化插入是这样的代码应该如何构造的,但我更关心为什么这个次优代码表现不佳。 @caustik 搞定了,但你的答案是解决它的实际答案(加上并行化)。
猜你喜欢
  • 2013-07-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-01-07
  • 1970-01-01
相关资源
最近更新 更多