原文地址:http://cnodejs.org/topic/5190d61263e9f8a542acd83b
mongo數據庫在nodejs平台有2個常用驅動,mongodb和mongoose,mongodb接口非常接近mongo數據庫原生的操作方式,是helloworld之類演示代碼的首選mongo數據庫連接驅動,因此成為大部分nodejs初學者最先接觸的mongo數據庫驅動。初學者在學會mongo連接的同時,卻也可悲的被helloword這種演示性質的數據庫操作習慣潛移默化了。
cat test.js
var server_options={};var db_options={ w:-1,// 設置w=-1是mongodb 1.2后的強制要求,見官方api文檔 logger:{ doDebug:true, debug:function(msg,obj){ console.log('[debug]',msg);}, log:function(msg,obj){ console.log('[log]',msg);}, error:function(msg,obj){ console.log('[error]',msg);}}};var mongodb =require("mongodb"), mongoserver =new mongodb.Server('localhost',27017,server_options ), db =new mongodb.Db('test', mongoserver, db_options);function test_save(){//前一個db和后一個db,是同一個對象。 db.open(function(err,db){if(err)return console.error(err); console.log('* mongodb connected'); db.collection('foo').save({test:1},function(err,result){ console.log(result); db.close();});})} test_save();
這是個隨處可見,大家非常熟悉的mongo數據庫helloword代碼:設置連接參數,open, 訪問collection, close。 唯一不同的是為了顯示代碼背后的實際運作,我給db_options加上一個日志(logger)選項。node test.js
, 客戶端輸出信息:
[debug] opened connection [debug] opened connection [debug] opened connection [debug] opened connection [debug] opened connection [debug] writing command to mongodb * mongodb connected [debug] writing command to mongodb { test:1, _id:51908a93718f6ad00c000001}[debug] closed connection [debug] closed connection [debug] closed connection [debug] closed connection [debug] closed connection
服務端mongo數據庫的輸出日志:
MonMay1312:54:33[initandlisten] connection accepted from127.0.0.1:2815#51MonMay1312:54:33[initandlisten] connection accepted from127.0.0.1:2816#52MonMay1312:54:33[initandlisten] connection accepted from127.0.0.1:2817#53MonMay1312:54:33[initandlisten] connection accepted from127.0.0.1:2818#54MonMay1312:54:33[initandlisten] connection accepted from127.0.0.1:2819#55MonMay1312:54:33[conn51]end connection 127.0.0.1:2815MonMay1312:54:33[conn52]end connection 127.0.0.1:2816MonMay1312:54:33[conn53]end connection 127.0.0.1:2817MonMay1312:54:33[conn54]end connection 127.0.0.1:2818MonMay1312:54:33[conn55]end connection 127.0.0.1:2819
客戶端和服務器端的日志都表明,db.open了5個連接,而並非一般同學所想象的1個連接。why?這是因為mongoserver = new mongodb.Server('localhost', 27017,server_options )
的serveroptions有個poolSize選項,默認值是5(見官方api文檔)。db對象不僅扮演着與mongo數據庫通訊的中間人角色,還同時是一個連接池。默認設置的情況下,helloword代碼打開一個有着5個連接的連接池,然后再關閉這個連接池。作為ran and quit的演示,這個操作流程當然沒問題,但如果放到http server的應用場景就成大問題了。每次http請求都打開5個數據庫連接而后又關閉5個數據庫連接,對性能影響可想而知,更為糟糕是open and close的操作流程會導致一個潛在的並發錯誤。
_cat server_1.js
var server_options={};var db_options={w:-1};var mongodb =require("mongodb"), mongoserver =new mongodb.Server('localhost',27017,server_options ), db =new mongodb.Db('test', mongoserver, db_options);var http=require('http');var server=http.createServer(function(req,res){ db.open(function(err,db){if(err)return console.error(err); console.log('* mongodb connected'); db.collection('foo').save({test:1},function(err,result){ res.end(JSON.stringify(result,null,2)); db.close();});})}); server.listen(8080,function(){ console.log('server listen to %d',this.address().port);}); setTimeout(function(){//http.get('http://localhost:8080',function(res){console.log('request ok')});//http.get('http://localhost:8080',function(res){console.log('request ok')});},2000);
node server.js
, 瀏覽器訪問ok,但如果做ab類並發測試或者把倒數2、3行的注釋去掉,問題就來了。
c:\nodejs\node_modules\mongodb\lib\mongodb\db.js:224thrownewError("db object already connecting, open cannot be called multi ^ Error: db object already connecting, open cannot be called multiple times
想象db對象是一扇門,open操作相當於開門,開門后才能閱讀房間里面的書籍(數據)。當請求1開門后,緊隨而來的請求2也想開門但是開不了,因為請求1還沒關門(db.close),門還處於“打開”的狀態。其實呢,請求2完全沒必要再開門,直接尾隨請求1進門即可。錯誤的根源在於我們要打開一扇已經打開的門。how to fix? easy, 從open and close 行為改為 open once and reuse anywhere。程序啟動的時候db.open一次,每次http請求直接訪問數據庫,扔掉db.open/db.close這2個多余的操作。
cat server_2.js
var server_options={'auto_reconnect':true,poolSize:5};var db_options={w:-1};var mongodb =require("mongodb"), mongoserver =new mongodb.Server('localhost',27017,server_options ), db =new mongodb.Db('test', mongoserver, db_options); db.open(function(err,db){if(err)throw err; console.info('mongodb connected');});var http=require('http');var server=http.createServer(function(req,res){ db.collection('foo').save({test:1},function(err,result){ res.end(JSON.stringify(result,null,2));});}); server.listen(8080,function(){ console.log('server listen to %d',this.address().port);}); setTimeout(function(){ http.get('http://localhost:8080',function(res){console.log('request ok')}); http.get('http://localhost:8080',function(res){console.log('request ok')});},2000);
這樣改后,雖然沒報錯了,卻引入另一個潛在的問題:當並發訪問>5時,因為同時可用的底層數據庫連接只有5,從而導致了阻塞。
===================================我是分隔線=====================================
實際應用場景中,直接引用db對象並不是一個好主意。默認情況下,db的poolSize=5,意味着並發只有5, 要提高並發的話,把poolSize拉到10? 20? 50? 100? NO,我們需要的是能動態調整連接數的連接池,既能滿足高峰期的連接數要求,也能在空閑期釋放閑置的連接,而不是象mongodb的內置連接池那樣保持固定連接數。怎么辦?重新發明輪子嗎?不,重用已有的連接池模塊genericpool。
_cat server_3.js
var http=require('http'), mongodb =require("mongodb"), poolModule =require('generic-pool');var pool = poolModule.Pool({ name :'mongodb', create :function(callback){var server_options={'auto_reconnect':false,poolSize:1};var db_options={w:-1};var mongoserver =new mongodb.Server('localhost',27017,server_options );var db=new mongodb.Db('test', mongoserver, db_options); db.open(function(err,db){if(err)return callback(err); callback(null,db);});}, destroy :function(db){ db.close();}, max :10,//根據應用的可能最高並發數設置 idleTimeoutMillis :30000, log :false});var server=http.createServer(function(req,res){ pool.acquire(function(err, db){if(err){ res.statusCode=500; res.end(JSON.stringify(err,null,2));}else{ db.collection('foo').save({test:1},function(err,result){ res.end(JSON.stringify(result,null,2)); pool.release(db);});}});}); server.listen(8080,function(){ console.log('server listen to %d',this.address().port);}); setTimeout(function(){ http.get('http://localhost:8080',function(res){console.log('request ok')}); http.get('http://localhost:8080',function(res){console.log('request ok')});},2000);
將poolSize設為1,一個db對象只負責一個底層的數據庫連接,generic_pool通過控制db對象的數目,間接控制實際的數據庫連接數目。如果poolSize還采取默認值5,1db=5連接,由於每次http請求期間我們實際使用到的只是1個連接,其他4個連接根本用不上,處於閑置狀態,其結果是浪費資源,拖慢響應速度。
以上。
備注1:本文mongo數據庫設置均采用本機安裝的默認設置。
備注2:當需建模的業務對象較多時,使用mongoose驅動是個好主意,它自帶的連接池比mongodb實用性強。
備注3:mongodb自1.2以后,官方推薦的連接方式改為MongoClient,將參數設置簡化為一個URL(詳細見http://mongodb.github.io/node-mongodb-native/driver-articles/mongoclient.html)。目前monodb的版本已經1.3,大家也該是時候轉變了吧。
以上文generic_pool連接池的初始化為例,看看新舊連接的對比:
舊連接方法
var pool = poolModule.Pool({ name :'mongodb', create :function(callback){var server_options={'auto_reconnect':false,poolSize:1};var db_options={w:-1};var mongoserver =new mongodb.Server('localhost',27017,server_options );var db=new mongodb.Db('test', mongoserver, db_options); db.open(function(err,db){if(err)return callback(err); callback(null,db);});},//......more code here});
新連接方法
var pool = poolModule.Pool({ name :'mongodb', create :function(callback){ mongodb.MongoClient.connect('mongodb://localhost/test',{ server:{poolSize:1}},function(err,db){ callback(err,db);});},//more code here});