2016年1月11日

Production Cluster Testing in MongoDB

基於 MongoDB 正式環境的一些基本要求,應該是要用三台機器來測試 MongoDB,但為了測試 mongodb driver 的 cluster 機制,又沒有那麼多實體的機器,這次先用單機來了解 client 由遠端連接 MongoDB 的一些狀況。

環境設定

一個由三個 replica set 組成的 shard server,三個 config server,三個 route server。

shard1: 3 replica sets
192.168.1.11:27017
192.168.1.11:28017
192.168.1.11:29017

3 config server
192.168.1.11:30000
192.168.1.11:31000
192.168.1.11:32000

3 route server
192.168.1.11:40000
192.168.1.11:41000
192.168.1.11:42000

shard1 的設定以及啟動:

mkdir -p /home/mongodb/shard/data/s1_1
mkdir -p /home/mongodb/shard/data/s1_2
mkdir -p /home/mongodb/shard/data/s1_3
mkdir -p /home/mongodb/shard/logs

/usr/share/mongodb/bin/mongod --port 27017 --fork --dbpath /home/mongodb/shard/data/s1_1 --logpath /home/mongodb/shard/logs/s1_1.log --logappend --shardsvr --replSet shard1 --directoryperdb

/usr/share/mongodb/bin/mongod --port 28017 --fork --dbpath /home/mongodb/shard/data/s1_2 --logpath /home/mongodb/shard/logs/s1_2.log --logappend --shardsvr --replSet shard1 --directoryperdb

/usr/share/mongodb/bin/mongod --port 29017 --fork --dbpath /home/mongodb/shard/data/s1_3 --logpath /home/mongodb/shard/logs/s1_3.log --logappend --shardsvr --replSet shard1 --directoryperdb

連接其中一台,並設定 shard1 的三個 replica set 成員

mongo --port 27017

config_shard1={
    _id: 'shard1',
    members: [
        {_id:0, host:'192.168.1.11:27017'},
        {_id:1, host:'192.168.1.11:28017'},
        {_id:2, host:'192.168.1.11:29017'}
    ]
};

rs.initiate(config_shard1);
rs.status();
rs.isMaster();

建立管理帳號 admin 以及連接 test database 的測試帳號 test

db.createUser({
    user: "admin",
    pwd: "pass",
    roles: [ { role: "root", db: "admin" } ]
});

use test
db.createUser(
    {
      user: "test",
      pwd: "pass",
      roles: [
         { role: "readWrite", db: "test" }
      ]
    }
);
db.getUsers();

啟動三個 config servers

mkdir -p /home/mongodb/shard/data/config1
mkdir -p /home/mongodb/shard/data/config2
mkdir -p /home/mongodb/shard/data/config3

/usr/share/mongodb/bin/mongod --configsvr --fork --port 30000 --dbpath /home/mongodb/shard/data/config1 --logpath /home/mongodb/shard/logs/config1.log --logappend

/usr/share/mongodb/bin/mongod --configsvr --fork --port 31000 --dbpath /home/mongodb/shard/data/config2 --logpath /home/mongodb/shard/logs/config2.log --logappend

/usr/share/mongodb/bin/mongod --configsvr --fork --port 32000 --dbpath /home/mongodb/shard/data/config3 --logpath /home/mongodb/shard/logs/config3.log --logappend

啟動三個 route servers

/usr/share/mongodb/bin/mongos --port 40000 --configdb 192.168.1.11:30000,192.168.1.11:31000,192.168.1.11:32000 --fork  --logpath /home/mongodb/shard/logs/route1.log --logappend --chunkSize 1

/usr/share/mongodb/bin/mongos --port 41000 --configdb 192.168.1.11:30000,192.168.1.11:31000,192.168.1.11:32000 --fork  --logpath /home/mongodb/shard/logs/route2.log --logappend --chunkSize 1

/usr/share/mongodb/bin/mongos --port 42000 --configdb 192.168.1.11:30000,192.168.1.11:31000,192.168.1.11:32000 --fork  --logpath /home/mongodb/shard/logs/route3.log --logappend --chunkSize 1

連接到 route server,設定 shard1 並設定 sharding 的 database

mongo --host 192.168.1.11 --port 40000

use admin
db.runCommand({addshard:"shard1/192.168.1.11:27017,192.168.1.11:28017,192.168.1.11:29017"});
db.runCommand({enablesharding:"test"});
db.runCommand({shardcollection:"test.users", key:{_id:1}});
db.runCommand({shardcollection:"test.doc", key:{_id:1}});

直接測試新增 500000 筆資料

for(var i=1;i<500000; i++) {
    db.users.insert({
        userid:"user_"+i,
        username:"name_"+i,
        age: NumberInt(_rand()*100)
    })
}

use test
db.users.stats();

MongoDB Client Drivers

MongoDB 官方提供的 client driver libary 很完整地支援了多種語言:C, C++, C#, Java, Node.js, Perl, PHP, Python, Motor, Ruby, Scala,另外有兩個社群提供的 driver:Go, Erlang。

使用 driver 之前,最重要的是知道連接 MongoDB 的 connection string

格式為:

mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database][?options]]

參考 node.js 的 mongoclient 說明頁面,有比較完整的 options 的設定項目。

因為我們建立了三個 route server,因此連線的 uri 填寫為以下的 connection string。

mongodb://192.168.1.11:40000,192.168.1.11:41000,192.168.1.11:42000/test?maxPoolSize=20

我們就利用 scala driver 的 QuickTour Sample,記得要同時把 Helpers.scala 以及 QuickTour.scala 都取回來編譯。

首先建立一個 scala sbt project,然後在 build.sbt 裡面加上一行 mongodb scala driver,接下來把剛剛的 Helper.scala 及 QuickTour.scala 放進 scala soruce 裡面,就可以了。

name := "mongodb"

version := "1.0"

scalaVersion := "2.11.7"

libraryDependencies += "org.mongodb.scala" %% "mongo-scala-driver" % "1.0.0"

接下來只擷取部份的程式碼,也就是我們修改的部份:

首先我們修改 mongodb 的 connection string,連接到三個 route server

    val mongoClient:MongoClient = MongoClient("mongodb://192.168.1.11:40000,192.168.1.11:41000,192.168.1.11:42000/test?maxPoolSize=20")

剛剛我們把 database:test, collection: doc 加入 shardcollection 中

    // get handle to "mydb" database
    val database: MongoDatabase = mongoClient.getDatabase("test")

    // get a handle to the "test" collection
    val collection: MongoCollection[Document] = database.getCollection("doc")

然後是清除 collection,並測試插入一筆資料

    collection.drop().results()

    // make a document and insert it
    val doc: Document = Document("_id" -> 0, "docid" -> "test", "type" -> "database",
      "count" -> 1, "page" -> Document("x" -> 100, "y" -> 200))
    collection.insertOne(doc).results()
    // get it (since it's the only one in there since we dropped the rest earlier on)
    collection.find.first().printResults()

讓 Thread 暫停 30 秒,因為我們想要測試在執行到一半,把其中一台 route server 關掉的狀況。

    Thread sleep 30*1000

當我們執行 QuickTour.scala 的時候,console 會列印以下的資訊:

資訊: Monitor thread successfully connected to server with description ServerDescription{address=192.168.1.11:41000, type=SHARD_ROUTER, state=CONNECTED, ok=true, version=ServerVersion{versionList=[3, 0, 7]}, minWireVersion=0, maxWireVersion=3, electionId=null, maxDocumentSize=16777216, roundTripTimeNanos=39787239}
十一月 20, 2015 5:25:53 下午 com.mongodb.diagnostics.logging.JULLogger log
資訊: Opened connection [connectionId{localValue:4}] to 192.168.1.11:40000
十一月 20, 2015 5:25:53 下午 com.mongodb.diagnostics.logging.JULLogger log
資訊: Opened connection [connectionId{localValue:5}] to 192.168.1.11:42000
{ "_id" : 0, "name" : "MongoDB", "type" : "database", "count" : 1, "axis" : { "x" : 100, "y" : 200 } }

這時候程式會因為 Thread sleep 30*1000 而暫停下來。 接下來有30s 的時間,可以連線到 port 40000 的 route server,並把這個 route server 關掉。

mongo --port 40000

use admin
db.shutdownServer();

當我們把 route server 關掉的瞬間,scala 程式的 console 就會馬上發現,192.168.1.11:40000 這個 route server 現在有問題,沒辦法使用了。

十一月 20, 2015 5:26:04 下午 com.mongodb.diagnostics.logging.JULLogger log
資訊: Exception in monitor thread while connecting to server 192.168.1.11:40000
com.mongodb.MongoException: java.io.IOException: 遠端電腦拒絕網路連線。

    at com.mongodb.connection.InternalStreamConnection.open(InternalStreamConnection.java:125)
    at com.mongodb.connection.DefaultServerMonitor$ServerMonitorRunnable.run(DefaultServerMonitor.java:141)
    at java.lang.Thread.run(Thread.java:745)
Caused by: java.io.IOException: 遠端電腦拒絕網路連線。

    at sun.nio.ch.Iocp.translateErrorToIOException(Iocp.java:309)
    at sun.nio.ch.Iocp.access$700(Iocp.java:46)
    at sun.nio.ch.Iocp$EventHandlerTask.run(Iocp.java:399)
    ... 1 more

接下來的程式中,當我們要繼續使用 mongodb,client driver 會自動選擇下一個 route server: 192.168.1.11:41000,然後繼續執行後面的程式。

十一月 20, 2015 5:26:23 下午 com.mongodb.diagnostics.logging.JULLogger log
資訊: Opened connection [connectionId{localValue:8}] to 192.168.1.11:41000
total # of documents after inserting 100 small ones (should be 101):  101

MongoDB 的 driver 真的做得很完整,不但有內建 connection pool 還有自動 fail over 的能力。