MongoDB JavaScript 驱动器测试
在对 node.js + MongoDB 做了一周不到的测试之后,我们决定放弃这对组合。放弃的原因有二:
- MongoDB 对数据的保障性不是我们所需要的。这不是 MongoDB 的错误,这是我们选择产品的错误。我觉得 MongoDB 其实就是放弃了这样的数据保障性才获得了更好的性能。所以才更适合类似 facebook twitter 对消息保障性要求不高,但是量大的应用。
- Javascript 的 driver 略显不成熟。其实各类开发速度都很快,同时我对他们的熟悉程度还不够好。所以总的感觉现在还没到用的时候。
这里对第二点做个流水账式样的记录,在学习的过程中发现相关的英文和中文资料都比较缺乏。
我所测试到的 Driver 有:
- node-mongodb-native
- mongolian
- mongoose
这三个 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');
测试结果: 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。只是我现在没找到正确的写法。
希望有经验的朋友给予一些帮助。