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。只是我現在沒找到正確的寫法。
希望有經驗的朋友給予一些幫助。
原文出處:http://latteye.com/2011/10/mongodb-javascript-driver.html