MongoDB 3.0 レプリカセットをWindows7環境に構築する

# 概要
開発環境・検証用を目的としたMongoDBのレプリカセットをWindows7上に作成する手順をまとめました。

**環境**

* Windows7 (64bit)
* MongoDB 3.0.2

## レプリケーションの構成

|node name|port  |role           |priority|
|:-------:|:----:|:--------------|:------:|
|node1    |30001 |primary node   |2       |
|node2    |30002 |secondary node |1       |
|node3    |30003 |secondary node |1       |
|node4    |30004 |arbiter        |1       |

### 環境構築

#### ディレクトリ構成

今回の例ではD:\dev直下にbinディレクトリと各ノード別にconf、db、logディレクトリを作成しました。

```text
D:\dev\mongodb3.0
            |
            +----- \bin
            |        |
            |        +---- \mongod.exe
            |        +---- \mongo.exe
            |        +---- \shell_node1.bat    (shell_node1.bat～shell_node4.bat)
            |        +---- \startup_node1.bat  (startup_node1.bat～startup_node4.bat)
            |
            +----- \node1
            |        |
            |        +---- \conf\mongod.cfg
            |        +---- \db
            |        +---- \log
            |
            +----- \node2
            |        |
            |        +---- \conf\mongod.cfg
            |        +---- \db
            |        +---- \log
            |
            +----- \node3
            |        |
            |        +---- \conf\mongod.cfg
            |        +---- \db
            |        +---- \log
            |
            +----- \node4
                     |
                     +---- \conf\mongod.cfg
                     +---- \db
                     +---- \log

```

#### confファイル

各ノードのconfファイルを作成します。
作成したconfファイルは各ノードディレクトリのconfへ置きます。

これはnode1用のconfファイルです。

```text:mongod.cfg
systemLog:
   destination: file
   path: "D:/dev/mongodb3.0/node1/log/mongod.log"
   logAppend: true
   timeStampFormat: iso8601-local
storage:
   dbPath: "D:/dev/mongodb3.0/node1/db"
   directoryPerDB: true
   journal:
      enabled: true
net:
   bindIp: 127.0.0.1
   port: 30001
setParameter:
   enableLocalhostAuthBypass: true
operationProfiling:
   slowOpThresholdMs: 100
   mode: all
replication:
   oplogSizeMB: 128
   replSetName: "myReplica"
```

その他のノードは、下記の3箇所の値をそれぞれのノード用の値にします。

* systemLog.path
* storage.dbPath
* net.port

#### 実行ファイルの用意

##### シンボリックリンクの作成

exeファイルのシンボリックリンクをbinディレクトリへ作成します。

```bat:mklink
> mklink mongod.exe "C:\Program Files\MongoDB\Server\3.0\bin\mongod.exe"
> mklink mongo.exe "C:\Program Files\MongoDB\Server\3.0\bin\mongo.exe"
```

##### MongoDB起動batの作成

binディレクトリに各ノード用のMongoDB起動batファイルを作成します。

これはnode1用のbatファイルです。これと同じようなbatファイルをそれぞれのノード用に作成します。

```bat:startup_node1.bat
D:\dev\mongodb3.0\bin\mongod.exe --enableExperimentalIndexStatsCmd --rest --httpinterface --verbose --config D:\dev\mongodb3.0\node1\conf\mongod.cfg
pause
```

`--httpinterface`は、ブラウザからMongoDBサーバーの状態を確認できるようにするオプションです。

ポートにMongoDBのポートに1000を足した値を指定します。
node1はport:30001にしてあるのでhttpinterfaceのポートは31001になります。

http://localhost:31001

##### Mongo Shell起動batの作成

binディレクトリに各ノード用のMongo Shell起動batファイルを作成します。
これはnode1用のbatファイルです。これと同じようなbatファイルをそれぞれのノード用に作成します。

```bat:shell_node1.bat
D:\dev\mongodb3.0\bin\mongo.exe --port 30001 --host localhost --verbose
```

### レプリカセットの設定

node1からnode4までのMongoDBインスタンスを起動します。
その後、プライマリーノードになるnode1へmongo shellでログインします。

#### レプリカセットの構築

node1から下記のコマンドを実行してレプリカセットを構築します。

```js:node1
> var config = {
  _id: 'myReplica',
  members: [
    {_id:0, host:'localhost:30001', priority:2 },
    {_id:1, host:'localhost:30002', priority:1 },
    {_id:2, host:'localhost:30003', priority:1 },
    {_id:3, host:'localhost:30004', arbiterOnly: true}
  ],
  settings: {
    getLastErrorDefaults: {w:2, wtimeout:0},
    getLastErrorModes: {}
  }
}

> rs.initiate(config)
{ "ok" : 1 }
```

レプリカセットの状態を確認します。

```js:node1
> rs.status()
{
        "set" : "myReplica",
        "date" : ISODate("2015-06-06T03:39:33.374Z"),
        "myState" : 1,
        "members" : [
                {
                        "_id" : 0,
                        "name" : "localhost:30001",
                        "health" : 1,
                        "state" : 1,
                        "stateStr" : "PRIMARY",
                        "uptime" : 1399,
                        "optime" : Timestamp(1433553387, 1),
                        "optimeDate" : ISODate("2015-06-06T01:16:27Z"),
                        "electionTime" : Timestamp(1433560639, 1),
                        "electionDate" : ISODate("2015-06-06T03:17:19Z"),
                        "configVersion" : 2,
                        "self" : true
                },
                {
                        "_id" : 1,
                        "name" : "localhost:30002",
                        "health" : 1,
                        "state" : 2,
                        "stateStr" : "SECONDARY",
                        "uptime" : 1336,
                        "optime" : Timestamp(1433553387, 1),
                        "optimeDate" : ISODate("2015-06-06T01:16:27Z"),
                        "lastHeartbeat" : ISODate("2015-06-06T03:39:33.345Z"),
                        "lastHeartbeatRecv" : ISODate("2015-06-06T03:39:33.316Z"),
                        "pingMs" : 0,
                        "lastHeartbeatMessage" : "could not find member to sync from",
                        "configVersion" : 2
                },
                {
                        "_id" : 2,
                        "name" : "localhost:30003",
                        "health" : 1,
                        "state" : 2,
                        "stateStr" : "SECONDARY",
                        "uptime" : 1333,
                        "optime" : Timestamp(1433553387, 1),
                        "optimeDate" : ISODate("2015-06-06T01:16:27Z"),
                        "lastHeartbeat" : ISODate("2015-06-06T03:39:31.898Z"),
                        "lastHeartbeatRecv" : ISODate("2015-06-06T03:39:32.187Z"),
                        "pingMs" : 0,
                        "configVersion" : 2
                },
                {
                        "_id" : 3,
                        "name" : "localhost:30004",
                        "health" : 1,
                        "state" : 7,
                        "stateStr" : "ARBITER",
                        "uptime" : 1330,
                        "lastHeartbeat" : ISODate("2015-06-06T03:39:32.876Z"),
                        "lastHeartbeatRecv" : ISODate("2015-06-06T03:39:31.800Z"),
                        "pingMs" : 0,
                        "configVersion" : 2
                }
        ],
        "ok" : 1
}
```

[MongoDB manual - Replica Set Configuration] (http://docs.mongodb.org/manual/reference/replica-configuration/)

#### データレプリケーションの確認

プライマリーノードに追加するデータがセカンダリーノードへ反映されるか確認します。

プライマリーノードにログインしてテストデータを追加してみます。

```js:node1
> use sample
switched to db sample
> db.test.insert({no:1, desc:"test data"})
WriteResult({ "nInserted" : 1 })
```

#### セカンダリーノードからの読み取り

セカンダリーノード(node2,node3)からデータを参照したい場合は、下記のコマンドを実行して読み取り設定を行います。
コネクションに対する設定なので接続ごとに設定を行う必要があります。

セカンダリーノード(node2)にログインします。

```js:node2
> db.getMongo().setReadPref('secondaryPreferred')
> use sample
switched to db sample
> show collections
columbo
system.indexes
system.profile
test
> db.test.find()
{ "_id" : ObjectId("5572dcd134829ba85871fcfa"), "no" : 1, "desc" : "test data" }
```

もしくは読み込み操作毎に設定することも可能です。

```js:node2
> db.test.find().readPref({mode:'secondaryPreferred'})
{ "_id" : ObjectId("5572dcd134829ba85871fcfa"), "no" : 1, "desc" : "test data" }
```

`rs.slaveOk()`でも設定可能ですが`Deprecated`になったようです。

> Sets the slaveOk property for the current connection. Deprecated. Use readPref() and Mongo.setReadPref() to set read preference.


##### 読み取り設定について

|Read Preference Mode |description                                     |
|:--------------------|:-----------------------------------------------|
|primary              |デフォルト.全ての読み取り操作はプライマリーから行う  |
|primaryPreferred     |プライマリーが利用不能であればセカンダリーから行う   |
|secondary            |全ての読み取り操作はセカンダリーから行う            |
|secondaryPreferred   |セカンダリーが利用不能であればプライマリーから行う   |
|nearest              |ネットワークレイテンシの小さいメンバーから行う      |

[MongoDB manual - Read Preference Reference] (http://docs.mongodb.org/manual/reference/read-preference/)

#### レプリカセットの操作メソッド

|methods                             |description       |primary node|
|:-----------------------------------|:-----------------|:-----------|
|[rs.initiate()] (http://docs.mongodb.org/manual/reference/method/rs.initiate/)   |レプリカセットを作成します。| |
|[rs.reconfig()] (http://docs.mongodb.org/manual/reference/method/rs.reconfig/)   |既存のレプリカセットを再設定します。|Only Primary Node|
|[rs.add()] (http://docs.mongodb.org/manual/reference/method/rs.add/)             |レプリカセットにメンバーを追加します。|Only Primary Node|
|[rs.remove()] (http://docs.mongodb.org/manual/reference/method/rs.remove/)      |レプリカセットからメンバーを除外します。除外するメンバーは事前にシャットダウンしておく必要があります。| |
|[rs.addArb()] (http://docs.mongodb.org/manual/reference/method/rs.addArb/)       |レプリカセットにアービターを追加します。| |
|[rs.status()] (http://docs.mongodb.org/manual/reference/method/rs.status/)       |レプリカセットのステータスを出力します。| |
|[rs.conf()] (http://docs.mongodb.org/manual/reference/method/rs.conf/)           |レプリカセットの現在の設定内容を出力します。| |
|[rs.help()] (http://docs.mongodb.org/manual/reference/method/rs.help/)           |ヘルプを出力します。| |
|[db.isMaster()] (http://docs.mongodb.org/manual/reference/method/db.isMaster/)   |mongodインスタンスの役割を表示します。         | |
|[db.getMongo().getReadPrefMode()] (http://docs.mongodb.org/manual/reference/method/Mongo.getReadPrefMode/) |現在の読み取り設定を出力します。| |
|[db.getMongo().setReadPref()] (http://docs.mongodb.org/manual/reference/method/Mongo.setReadPref/)         |読み取り設定を行います。 | |

[MongoDB manual - Replication Methods] (http://docs.mongodb.org/manual/reference/method/js-replication/)

### レプリケーションの確認

#### 書き込み保証について

|option       |description|
|:------------|:----------|
|w:0          |プライマリーにデータが書き込まれたことは保証しません。（書き込み完了の確認を行いません）ネットワークエラーの情報は返します。|
|w:1          |デフォルトです。プライマリーにデータが書き込まれたことを保証します。 書き込めなかった場合はエラーを返します。|
|w:number     |numberで指定したノードにデータが書き込まれたことを保証します。(プライマリー含む）指定数に書き込めなかった場合はエラーを返します。|
|w:"majority" |レプリカセットに参加する過半数のノードにデータが書き込まれたことを保証します。指定数に書き込めなかった場合はエラーを返します。|
|j:true       |ジャーナルにデータが書き込まれたことを保証します。書き込めなかった場合はエラーを返します。|
|wtimeout     |書き込み完了のタイムリミットをミリ秒で指定します。0または未指定の場合は無期限にブロックします。wオプションが1以上のときに意味を持ちます。|

複数のノードに対して書き込みを行う場合では、一部のノードでエラーが発生してもそれまでに書き込まれたデータの取り消しはできません。

##### 書き込み保証 (Write concern)の設定例

**Unacknowledged**

```js
writeConcern: { w: 0 }
```

**Acknowledged**

```js
writeConcern: { w: 1 }
```

**Journaled**

```js
writeConcern: { w: 1, j:true }
```

**Replica Acknowledged**

```js
writeConcern: { w: 2 }
```

```js
writeConcern: { w: "majority" }
```

##### 書き込み保証の動作確認

プライマリーノード(node1)、セカンダリーノード2つ(node2,node3)の状態から、セカンダリーノード(node3)を落として下記の書き込み保証(`{writeConcern:{w:3, wtimeout:5000}}`)でドキュメントを追加してみます。
この設定は「3つのノードに5000ms以内で書き込みを完了することを保証する」という意味ですが、この例では書き込めるノードが足りないためエラーになります。

```js
var a = {no:1, name:"test"}
> db.test.insert(a, {writeConcern:{w:3, wtimeout:5000}})
WriteResult({
        "nInserted" : 1,
        "writeConcernError" : {
                "code" : 64,
                "errInfo" : {
                        "wtimeout" : true
                },
                "errmsg" : "waiting for replication timed out"
        }
})
```
:exclamation: node1、node2にデータの書き込みが完了している可能性があります。

書き込み保証を`{writeConcern:{w:2, wtimeout:5000}}`に変えてドキュメントを追加するとnode1、node2へ書き込みができるのでエラーは発生しません。

```js
> db.test.insert(a, {writeConcern:{w:2, wtimeout:5000}})
WriteResult({ "nInserted" : 1 })
```

[MongoDB manual - Write Concern] (http://docs.mongodb.org/manual/core/write-concern/)

#### ノードの除外と追加について

レプリカセット構築後もノードの除外、追加を行うことができます。

##### セカンダリーノードを除外したときの動作確認

一時的にセカンダリーノードの1つをレプリカセットから外し、その間にプリマリーノードへ追加されたデータがレプリカセット復帰後に自動的に反映されることを確認します。

この例ではnode3を一時的に除外します。
セカンダリーノード(node3)を除外する前にMongoDBをシャットダウンします。

```js:node3
> use admin
switched to db admin
> db.shutdownServer()
```

プライマリーノード(node1)でレプリカセットの状態を確認します。

```js:node1
> rs.status()
```

レプリカセットからnode3を除外します。

```js:node1
> rs.remove("localhost:30003")
{ "ok" : 1 }
```

もう一度レプリカセットの状態をみてnode3が除外されていることを確認します。

```js:node1
> rs.status()
```

プライマリーノードでデータを追加します。

```js:node1
> db.test.insert({no:3, desc:"before node3 add"})
WriteResult({ "nInserted" : 1 })
```

node3のMongoDBを起動し、その後node3をレプリカセットへ加えます。

```js:node1
> rs.add("localhost:30003")
{ "ok" : 1 }
```

レプリカセットの状態をみてmembersにnode3が追加されていることを確認します。

```js:node1
> rs.status()
```

node3へログインしデータが更新されていることを確認します。

```js:node3
> db.test.find({no:3})
{ "_id" : ObjectId("55729c1834829ba85871fcf6"), "no" : 3, "desc" : "before node3 add" }
```

##### プライマリーノードを除外したときの動作確認

プライマリーノード(node1)を除外し、セカンダリーノード(node2 or node3)がプライマリーノードに昇格することを確認します。その後除外したプライマリーノード(node1)を復帰させ昇格したセカンダリーノードと交代することを確認します。

プライマリーノード(node1)をシャットダウンします。

```js:node1
> use admin
switched to db admin
> db.shutdownServer()
```

セカンダリーノード(node2)へログインしてレプリカセットの状態を見て、残りのノードのどれかがプライマリーノードになっていることを確認します。
この例ではnode2がプライマリーノードになったとします。

```js:node2
> rs.status()
```

プライマリーノードに昇格したnode2でデータを追加します。

```js:node2
> use sample
switched to db admin
> db.test.insert({no:2, desc:"inset data from node2"})
WriteResult({ "nInserted" : 1 })
```

node1のMongoDBを起動し、レプリカセットの状態を見てnode1がプライマリーノードに復帰していることを確認します。

```js:node1
> rs.status()
```

node2で追加したデータを確認します。

```js:node1
> db.test.find()
{ "_id" : ObjectId("5572dcd134829ba85871fcfa"), "no" : 1, "desc" : "test data" }
{ "_id" : ObjectId("5572e07ab6e83d84b2e82956"), "no" : 2, "desc" : "inset data from node2" }
```

## 参考サイト

下記のサイトを参考に致しました。

* [MongoDB manual - replication](http://docs.mongodb.org/manual/replication/)
* [gihyo.jp - 第4回　MongoDBのレプリケーションを構築してみよう](http://gihyo.jp/dev/serial/01/mongodb/0004)
* [gihyo.jp - 第6回　レプリケーション＋シャーディングでMongoDBをもっと使いこなす](http://gihyo.jp/dev/serial/01/mongodb/0006)