下面文章是对MongoDB的几个Node.js客户端的评测,在评测后,作者虽然没有选择MongoDB+Node.js的组合,但是其测试过程和结果具有参考意义,在故障情况下的测试,确实也是我们选择使用每一种工具时必要的一环。如果你在使用MongoDB,你对下面说的情况做过
下面文章是对 MongoDB 的几个 Node.js 客户端的评测,在评测后,作者虽然没有选择 MongoDB+Node.js
的组合,但是其测试过程和结果具有参考意义,在故障情况下的测试,确实也是我们选择使用每一种工具时必要的一环。如果你在使用MongoDB,你对下面说的情况做过测试吗?
本文来源于作者@latteye 的热心投搞。原文链接:latteye.com。
本站欢迎各种NoSQL方面的新闻技术文章投稿,下面是原文。
在对 node.js + MongoDB 做了一周不到的测试之后,我们决定放弃这对组合。放弃的原因有二:
MongoDB 对数据的保障性不是我们所需要的。这不是 MongoDB 的错误,这是我们选择产品的错误。我觉得 MongoDB
其实就是放弃了这样的数据保障性才获得了更好的性能。所以才更适合类似 facebook twitter
对消息保障性要求不高,但是量大的应用。
Javascript 的 driver
略显不成熟。其实各类开发速度都很快,同时我对他们的熟悉程度还不够好。所以总的感觉现在还没到用的时候。
这里对第二点做个流水账式样的记录,在学习的过程中发现相关的英文和中文资料都比较缺乏。
我所测试到的 Driver 有:
这三个 Driver 里,mongolian 和 mongoose 都是依赖 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。只是我现在没找到正确的写法。
希望有经验的朋友给予一些帮助。