MongoDB JavaScript Driver 测试

下面文章是对 MongoDB 的几个 Node.js 客户端的评测,在评测后,作者虽然没有选择 MongoDB+Node.js 的组合,但是其测试过程和结果具有参考意义,在故障情况下的测试,确实也是我们选择使用每一种工具时必要的一环。如果你在使用MongoDB,你对下面说的情况做过测试吗?

本文来源于作者@latteye 的热心投搞。原文链接:latteye.com

本站欢迎各种NoSQL方面的新闻技术文章投稿,下面是原文。

在对 node.js + MongoDB 做了一周不到的测试之后,我们决定放弃这对组合。放弃的原因有二:

  • MongoDB 对数据的保障性不是我们所需要的。这不是 MongoDB 的错误,这是我们选择产品的错误。我觉得 MongoDB 其实就是放弃了这样的数据保障性才获得了更好的性能。所以才更适合类似 facebook twitter 对消息保障性要求不高,但是量大的应用。
  • Javascript 的 driver 略显不成熟。其实各类开发速度都很快,同时我对他们的熟悉程度还不够好。所以总的感觉现在还没到用的时候。

这里对第二点做个流水账式样的记录,在学习的过程中发现相关的英文和中文资料都比较缺乏。

我所测试到的 Driver 有:

这三个 Driver 里,mongolianmongoose 都是依赖 native 的。不过在这里mongolian的作者提到 mongolian对 native db class 部分并不调用。看来依赖的程度有所不一。

所测试的内容是 failover。MongoDB 推荐的 failover 方案为 Replica Set,这个架构逻辑上不难理解。至少三个节点,至多七个节点;各个节点可以有 0-99 的优先级等一系列特性让他成为非常优秀的 HA 方案。

测试方法: 插入 N 条数据,并且在插入的过程中将 Primary 进程杀死。查看客户端(node.js)是否正常转移到新的 Primary ,并且最终检查数据一致性。可以接受插入不了数据,但是一定要有错误返回。返回错误的数量一定要和数据库内未插入的数据数量一致。

一、native

先给出 native 的测试脚本:

var mongodb = require('mongodb');
var Db = require('mongodb').Db,
  Connection = require('mongodb').Connection,
  Server = require('mongodb').Server,
  ReplSetServers = require('mongodb').ReplSetServers;

var replStat = new ReplSetServers([
	new Server('172.16.5.151', 28010, { auto_reconnect: true }),
	new Server('172.16.5.152', 28010, { auto_reconnect: true }),
	new Server('172.16.5.153', 28010, { auto_reconnect: true })
	],
	{rs_name: 'rs1'}
);

var db = new Db('a', replStat);
db.open(function (error, client) {
  if (error) throw error;
  var collection = new mongodb.Collection(client, 'blogposts');

function test_read(t)
{
  console.log('enum elements...');
  var start = new Date;
  var times = 0;
  for(var i = 1; i <= t; i++ )
  {
	collection.find({'_id':i}, {limit:1}).nextObject(function(err, docs) {
	    if (err) console.warn(err.message);
	    //else console.dir(docs);
	    if(++times >= t)
		console.log('enum finished:cost time:' + (new Date - start) + 'ms');
	  });
  }
}

function test_write(t)
{
  var start = new Date;
  var times = 0;
  console.log('add elements...');
  for(var i = 1; i <= t; i++ )
  {
  	collection.insert({date: (new Date()).getTime(), body:'sadf', title:'abc', _id:i}, {safe:{w:2, wtimeout: 10000}},
                    function(err, objects) {
	    if (err) console.warn(err.message);
	    if(++times >= t)
	    {
		console.log('add finished:cost time:' + (new Date - start) + 'ms');
		test_read(t);
	    }
  	});
  }
}

var wtimes = 10000;
test_write(wtimes);
//test_read(wtimes);
});

三个 driver 中文档工作做的最好的就是 native 了,example 也比较多。不过作者在 Replica Set 的 examples 中给了个让人很莫名的开头:

var port1 = 27018;
var port2 = 27019;
var server = new Server(host, port, {});
var server1 = new Server(host, port1, {});
var server2 = new Server(host, port2, {});
var servers = new Array();
servers[0] = server2;
servers[1] = server1;
servers[2] = server;

var replStat = new ReplSetServers(servers);

对于我这种不写代码的人来说,您老写成这样着实让我纠结了一番⋯⋯

测试结果: 在插入的过程中将 Primary kill 后大约有 1/3 的概率 node.js crash 了。其余 2/3 的概率 node.js 彻底卡住。MongoDB 端 Primary 正常转移,但未见数据继续插入进来。我很想贴一点 log 上来,但 native driver 真的没有任何 log,就是单纯的卡住了⋯⋯卡住⋯⋯卡⋯⋯

Crash log:

[root@localhost bin]# ./node ~/native_test.js
2
3
4
add elements...
node: src/uv-common.c:92: uv_err_name: Assertion `0' failed.
已放弃

在经过几天的搜索以后[1 2 3 ] 我发现似乎有人和我做过类似的测试,但是从来没有得到明确的答案。昨天我也将这个问题发到的 native 论坛上,目前还没有人回复。
但是后来又随后开始怀疑自己的脚本,同时看到新的解答[4],于是开始尝试不在一个 db.open 里面写 for,而在 for 里面反复的 db.open 和 db.close。但是没有成功,循环插入10条数据,成功插入的只有第一条。无论有没有 db.close 都是这个现象。这个不工作的代码就不贴上来了,如果有那位做过类似测试希望可以交流一下。

二、mongoose

测试脚本:

var mongoose = require('mongoose');
mongoose.createSetConnection('mongodb://172.16.5.151:28010/a,mongodb://172.16.5.152:28010/a,mongodb://172.16.5.153:28010/a');

var Schema = mongoose.Schema,
    ObjectId = Schema.ObjectId;

var BlogPost = new Schema({
//    author    : ObjectId
    _id       : Number
  , title     : String
  , body      : String
  , date      : Date
});

mongoose.model('BlogPost', BlogPost);
var post = mongoose.model('BlogPost');

function test_read(t)
{
	var start = new Date();
	var times = 0;
	console.log('enum elements...');
	for(var i = 1; i <= t; i++)
	{
		//console.log("read:"+i);
		post.findById(i, function(err, doc){
			if(err)
				console.log(err);
			//else
			//	console.log(doc);
			if(++times >= t)
			{
				var end = new Date();
				console.log('enum finished:cost time:' + (end - start) + "ms");
			}
		});
	}
};

function test_write(t)
{
	var start = new Date();
	var times = 0;
	console.log('add elements...');
	for(var i = 1; i <= t; i++)
	{
		//console.log("write:"+i);

		var p = new post();
		p._id = i;
		p.title = 'abc';
		p.body = 'sadf';
		p.date = (new Date()).getTime();
		p.save(function(err){
			if(err)
			{
				console.log(err);
			}
			if(++times >= t)
			{
				var end = new Date();
				console.log('add finished:cost time:' + (end - start) + "ms");
				test_read(t);
			}
		});
	}
}

var wtimes = 10000;
test_write(wtimes);
//process.exit(0);

首先!连接 Replica Set 要用createSetConnection:

mongoose.createSetConnection('mongodb://172.16.5.151:28010/a,mongodb://172.16.5.152:28010/a,mongodb://172.16.5.153:28010/a');

你或许和我一样走过一些弯路[5 6]。

测试结果: OSE 的测试结果几乎和 native 一样,唯一好一点的是它从来没把 node.js 弄 crash 过。它唯一的反应就是 卡住⋯⋯卡住⋯⋯
OSE 和 native 在这个测试上的区别是,native 一边产生数据一边插入。OSE 先将数据在内存中产生出来以后,再一次插入数据库。而 node.js 存在一个内存限制的问题 (一个浏览器有什么理由需要2G的内存呢?),所以当 OSE driver 占用超过 1.9G 内存之后,node.js 不出意料的 crash。

PS. google 论坛上有人说可以通过参数让 node.js 支持任何大小的内存。经过我的测试(CentOS 6 x86-64,0.5.x,0.4.x)没有成功过。可工作的最高数值为 1900M。

你可以注意到了 native 驱动有一个 auto_reconnect 参数(尽管它没有 reconnect),而 mongoose 脚本里面没看到。OSE 的确也有设置 auto_reconnect 的方式[7],但是只看到给普通连接设置的方式。没有看到给 Replica Set 用的方式。自己胡乱尝试了几个设置方式无一成功。希望 ose 的作者能再多花点时间在文档方面。另一方面也可以看到,OSE 其实对 native 依赖还是蛮严重的。这种设置方式的出现似乎只是传递给 native 驱动,我猜测 OSE 自己没有对这块做任何处理。

三、mongolian

测试脚本:

var mongodb = require('mongolian');
var server = new mongodb(
    "172.16.5.151:28010",
    "172.16.5.152:28010",
    "172.16.5.153:28010"
)
var db = server.db("a")
var blogposts = db.collection("blogposts")

function test_read(t)
{
  console.log('enum elements...');
  var start = new Date;
  var times = 0;
  for(var i = 1; i <= t; i++ )
  {
	blogposts.find({'_id':i}, {limit:1}).nextObject(function(err, docs) {
	    if (err) console.warn(err.message);
	    //else console.dir(docs);
	    if(++times >= t)
		console.log('enum finished:cost time:' + (new Date - start) + 'ms');
	  });
  }
}

function test_write(t)
{
  var start = new Date;
  var times = 0;
  console.log('add elements...');
  for(var i = 1; i <= t; i++ )
  {
  	blogposts.insert({date: (new Date()).getTime(), body:'sadf', title:'abc', _id:i},
                    function(err, objects) {
	    if (err) console.warn(err.message);
	    if(++times >= t)
	    {
		console.log('add finished:cost time:' + (new Date - start) + 'ms');
		test_read(t);
	    }
  	});
  }
}

var wtimes = 10000;
test_write(wtimes);
//test_read(wtimes);

测试结果: mongolian(以下简称lian) 的反应是这三个驱动中最好的。首先当开启 node 的时候,lian 会给出 debug 信息,明确告诉你他连接到了哪台 mongodb,作者也明确说了这个 log 是为 Replica Set 做的 [89]。当 Primary 被 kill 掉之后,lian 会告诉你连接丢失。在后面的插入lian会明确的告诉你插入失败,并且是每一次插入就给出一个 log,而且程序会一路走下去,不会卡住。

[root@localhost ~]# node lian_test.js
add elements...
[debug] mongo://172.16.5.151:28010: Disconnected
[error] mongo://172.16.5.151:28010: Error: ECONNREFUSED, Connection refused
[debug] mongo://172.16.5.152:28010: Connected
[debug] mongo://172.16.5.153:28010: Connected
[debug] mongo://172.16.5.152:28010: Initialized as secondary
[debug] mongo://172.16.5.153:28010: Initialized as primary
[info] mongo://172.16.5.153:28010: Connected to primary
[debug] Finished scanning... primary? mongo://172.16.5.153:28010

我觉得 lian 的这种工作模式可以从它的代码编写方式里面体现出来。lian 的代码里面不存在打开一个 connection 或者 db.open 这样的概念,所以我估计 lian 是每一次 insert 就会尝试打开一次 connection。虽然他没有再次找到正确的 Primary,但至少他知道自己连接丢失了。
但是 lian 没能再次找到正确的 Primary 可能意味着他先打开了一个 ConnectionPool (你可以通过 poolSize 在 native 里面设置 pool 的大小),只有打开 ConnectionPool 的时候才会尝试去做 Primary 判断。
另外 lian 的插入速度也不错,感觉比 OSE 好,几乎和 native 一样。

实验做到后面,我极度怀疑自己的测试脚本写的不对。因为 native 是有 auto_reconnect 的参数的,但是缺没有工作。作者应该考虑了这个问题的。
而也肯定有一种方式让我在 for 里面打开 connection 、写完、关闭 connection。只是我现在没找到正确的写法。
希望有经验的朋友给予一些帮助。

anyShare一切看了好文章不转的行为,都是耍流氓!
          

无觅相关文章插件,快速提升流量