MongoDBのレプリケーションとフェイルオーバーの設定

MongoDBを本番運用で利用する機会があったので、
レプリケーション及びフェイルオーバーの仕組みを導入しておきたく、そのときに実施した対応のメモ。


# 概要

今回の検証は下記内容を確認する目的で実施。

1. MongoDBのレプリケーション及びフェイルオーバーの設定
2. MongoDBのフェイルオーバーの動作確認
3. フェイルオーバー時のプログラム側の処理や設定
4. コネクション数の管理

この記事では、1と2にあたるMongoDB周りの設定や動きについて記載する。
なお、プログラム面の設定や処理については、下記記事を参考。

- 参考
   - [MongoDBのフェイルオーバー時のNode.jsのプログラム制御と動作確認
](http://qiita.com/megadreams14/items/f4f226d33d65f5a88e32)


# 環境

今回は、ローカル環境において検証を実施。

|ミドルウェア| バージョン |
|:--:|:--:|
| PC OS   | Mac |
| Virtual Box | 5.1.26 |
| Vagrant | 1.9.7 |
| 仮想OS | CentOS 6.7 |
| MongoDB | v3.4.8 |

# 構成

MongoDBのレプリケーション（Replica Set）を設定するには、最低でも3台は必要になるため、
今回の検証では、1台のサーバの中にportを分けて3台のDBサーバを稼働させる。

| DB名 | ポート | 役割 | 備考 |
|:--:|:--:|:--:|:--:
| DB01 | 50000 | Primary | レプリケーションの親　|
| DB02 | 50001 | Secondary | レプリケーションの子　|
| DB03 | 50002 | Arbiter | データは保持せず、Primaryへの昇格投票のみを行う |

それぞれの説明は、下記記事に詳しく記載されている。

- 参考
   - [Replication — MongoDB Manual 3.4](https://docs.mongodb.com/v3.4/replication/)
   - [俺でもわかるシリーズ: MongoDBのレプリケーション](http://qiita.com/y13i/items/78c9f45acd07a9478d2d)

# 構築手順

## MongoDBをインストール

「[CentOS6.5にMongoDBをインストールする](http://qiita.com/nownabe/items/123a8fd04ff5252b3036)」の記事を参考に「MongoDBレポジトリ追加」と「インストール」を行う。

### MongoDBレポジトリ追加

```
$ sudo vi /etc/yum.repos.d/mongodb.repo
```

```/etc/yum.repos.d/mongodb.repo
[mongodb]
name=MongoDB Repository
baseurl=http://downloads-distro.mongodb.org/repo/redhat/os/x86_64/
gpgcheck=0
enabled=1
```

### インストール

```
$ sudo yum install -y mongodb-org
```


## MongoDBのレプリカセットを構築する

### データ保存領域の作成

1台のサーバ内で3台分のMongoDBを動かすため、まずはMongoDBのデータ保存領域を作成する
（なお、以降はrootユーザで実行）

```
mkdir -p /var/lib/mongodb/db01/
mkdir -p /var/lib/mongodb/db02/
mkdir -p /var/lib/mongodb/db03/
```

### 3台分のMongoDBのプロセスを起動する

```
# db01をport: 50000で起動する
$ mongod --port=50000 --dbpath=/var/lib/mongodb/db01 --logpath=/var/log/mongodb/db01.log --replSet=LocalRep --fork

# db02をport: 50001で起動する
$ mongod --port=50001 --dbpath=/var/lib/mongodb/db02 --logpath=/var/log/mongodb/db02.log --replSet=LocalRep --fork

# db03をport: 50002で起動する
$ mongod --port=50002 --dbpath=/var/lib/mongodb/db03 --logpath=/var/log/mongodb/db03.log --replSet=LocalRep --fork
```

### プロセスの確認

```
$ ps aux | grep mongod
root      8475  2.4  7.1 1061980 45048 ?       Sl   10:42   0:00 mongod --port=50000 --dbpath=/var/lib/mongodb/db01 --logpath=/var/log/mongodb/db01.log --replSet=LocalRep --fork
root      8502  2.4  5.9 1061980 37504 ?       Sl   10:43   0:00 mongod --port=50001 --dbpath=/var/lib/mongodb/db02 --logpath=/var/log/mongodb/db02.log --replSet=LocalRep --fork
root      8529  3.7  6.2 1061984 39528 ?       Sl   10:43   0:00 mongod --port=50002 --dbpath=/var/lib/mongodb/db03 --logpath=/var/log/mongodb/db03.log --replSet=LocalRep --fork
root      8555  0.0  0.1 103304   884 pts/2    R+   10:43   0:00 grep mongod
```

### レプリカセットの設定を行う

現状では起動したのみで、レプリカセットの設定はされていないため、
プライマリのDBにてレプリカセットの設定を行う。


まずは、プライマリのDB01サーバにログイン後、statusを確認する。

```
$ mongo --port 50000
> rs.status()
{
	"info" : "run rs.initiate(...) if not yet done for the set",
	"ok" : 0,
	"errmsg" : "no replset config has been received",
	"code" : 94,
	"codeName" : "NotYetInitialized"
}
```

上記のように初期化されていないよとのメッセージが表示されるため、
「rs.initiate()」コマンドを実行して初期化を行う。

```
> rs.initiate()  
{
	"info2" : "no configuration specified. Using a default configuration for the set",
	"me" : "localhost.localdomain:50000",
	"ok" : 1
}
LocalRep:OTHER>
```

デフォルトの設定ではあるがレプリカセットのPrimaryとしての初期化が完了。
しばらくしてからrs.status()コマンドを打つと、
「"name": "localhost.localdomain:50000"」のサーバが"Primary"になっているのが分かる。

```
LocalRep:OTHER> rs.status()
{
	"set" : "LocalRep",
	"date" : ISODate("2017-09-08T09:44:50.159Z"),
	"myState" : 1,
	"term" : NumberLong(1),
	"heartbeatIntervalMillis" : NumberLong(2000),
	"optimes" : {
		"lastCommittedOpTime" : {
			"ts" : Timestamp(1504863885, 1),
			"t" : NumberLong(1)
		},
		"appliedOpTime" : {
			"ts" : Timestamp(1504863885, 1),
			"t" : NumberLong(1)
		},
		"durableOpTime" : {
			"ts" : Timestamp(1504863885, 1),
			"t" : NumberLong(1)
		}
	},
	"members" : [
		{
			"_id" : 0,
			"name" : "localhost.localdomain:50000",
			"health" : 1,
			"state" : 1,
			"stateStr" : "PRIMARY",
			"uptime" : 111,
			"optime" : {
				"ts" : Timestamp(1504863885, 1),
				"t" : NumberLong(1)
			},
			"optimeDate" : ISODate("2017-09-08T09:44:45Z"),
			"infoMessage" : "could not find member to sync from",
			"electionTime" : Timestamp(1504863883, 2),
			"electionDate" : ISODate("2017-09-08T09:44:43Z"),
			"configVersion" : 1,
			"self" : true
		}
	],
	"ok" : 1
}
LocalRep:PRIMARY> 
```

### SecondaryとArbiterの追加

次にSecondaryとしてDB02を追加する。PRIMARYサーバ上でrs.add()コマンドを実行。

```
LocalRep:PRIMARY> rs.add('localhost.localdomain:50001')
{ "ok" : 1 }
```

さらにArbiterとしてDB03を追加する。

```
LocalRep:PRIMARY> rs.addArb('localhost.localdomain:50002');
{ "ok" : 1 }
```

上記コマンドは下記のようにしてもOK

```
LocalRep:PRIMARY> rs.add({host: 'localhost.localdomain:50002', arbiterOnly: true);
```

この状態でrs.status()コマンドを実行すると正常にレプリカセットされていることが分かる。

```
LocalRep:PRIMARY> rs.status()
{
	"set" : "LocalRep",
	"date" : ISODate("2017-09-08T09:49:10.158Z"),
	"myState" : 1,
	"term" : NumberLong(1),
	"heartbeatIntervalMillis" : NumberLong(2000),
	"optimes" : {
		"lastCommittedOpTime" : {
			"ts" : Timestamp(1504864145, 1),
			"t" : NumberLong(1)
		},
		"appliedOpTime" : {
			"ts" : Timestamp(1504864145, 1),
			"t" : NumberLong(1)
		},
		"durableOpTime" : {
			"ts" : Timestamp(1504864145, 1),
			"t" : NumberLong(1)
		}
	},
	"members" : [
		{
			"_id" : 0,
			"name" : "localhost.localdomain:50000",
			"health" : 1,
			"state" : 1,
			"stateStr" : "PRIMARY",
			"uptime" : 371,
			"optime" : {
				"ts" : Timestamp(1504864145, 1),
				"t" : NumberLong(1)
			},
			"optimeDate" : ISODate("2017-09-08T09:49:05Z"),
			"electionTime" : Timestamp(1504863883, 2),
			"electionDate" : ISODate("2017-09-08T09:44:43Z"),
			"configVersion" : 3,
			"self" : true
		},
		{
			"_id" : 1,
			"name" : "localhost.localdomain:50001",
			"health" : 1,
			"state" : 2,
			"stateStr" : "SECONDARY",
			"uptime" : 112,
			"optime" : {
				"ts" : Timestamp(1504864145, 1),
				"t" : NumberLong(1)
			},
			"optimeDurable" : {
				"ts" : Timestamp(1504864145, 1),
				"t" : NumberLong(1)
			},
			"optimeDate" : ISODate("2017-09-08T09:49:05Z"),
			"optimeDurableDate" : ISODate("2017-09-08T09:49:05Z"),
			"lastHeartbeat" : ISODate("2017-09-08T09:49:09.258Z"),
			"lastHeartbeatRecv" : ISODate("2017-09-08T09:49:09.249Z"),
			"pingMs" : NumberLong(0),
			"syncingTo" : "localhost.localdomain:50000",
			"configVersion" : 3
		},
		{
			"_id" : 2,
			"name" : "localhost.localdomain:50002",
			"health" : 1,
			"state" : 7,
			"stateStr" : "ARBITER",
			"uptime" : 74,
			"lastHeartbeat" : ISODate("2017-09-08T09:49:09.258Z"),
			"lastHeartbeatRecv" : ISODate("2017-09-08T09:49:05.277Z"),
			"pingMs" : NumberLong(0),
			"configVersion" : 3
		}
	],
	"ok" : 1
}
```

### フェイルオーバー時の優先順位の設定

DB01が落ちてフェイルオーバーが発生した際にDB02がPrimaryになるが、
DB01が復活した際にまたDB01をPrimaryとする際には、各サーバにpriorityを設定しておけば良い。
今回は、DB01のpriorityを100、DB02のpriority10として、
DB01が起動中は必ずDB01がPrimaryとして稼働するような設定を行っておく。

```
LocalRep:PRIMARY> var conf = rs.conf();
LocalRep:PRIMARY> conf.members[0].priority = 100;
100
LocalRep:PRIMARY> conf.members[1].priority = 10;
10
LocalRep:PRIMARY> rs.reconfig(conf);
{ "ok" : 1 }
```

membersは、rs.status()を実行したときのmembersキーの配列のことを表しており、
db01はindex:0、db02はindex:1になるので上記のような指定の方法を行っている。

これで各種サーバの設定が完了。

## 各MongoDBの状態確認

各サーバにログインして状況を確認する。

- DB01の状態

```
$ mongo --port 50000
LocalRep:PRIMARY> 
```

- DB02の状態

```
$ mongo --port 50001
LocalRep:SECONDARY>  
```

- DB03の状態

```
$ mongo --port 50002
LocalRep:ARBITER> 
```

それぞれの役割に応じた設定になっていることを確認

## フェイルオーバーの実施

## DB01を落としてみる

とりあえずフェイルオーバーが正しく実施されるのかを確認する。
まずは、DB01のプロセスを切ってみる

```
$ sudo ps aux | grep db01 | awk '{print $2}' | xargs kill -9
```

いなくなったことを確認

```
$ ps aux | grep mongod
root      8502  1.7  7.2 1561832 45804 ?       Sl   10:43   0:13 mongod --port=50001 --dbpath=/var/lib/mongodb/db02 --logpath=/var/log/mongodb/db02.log --replSet=LocalRep --fork
root      8529  1.6  7.0 1078444 44748 ?       Sl   10:43   0:12 mongod --port=50002 --dbpath=/var/lib/mongodb/db03 --logpath=/var/log/mongodb/db03.log --replSet=LocalRep --fork
root      8714  0.0  0.1 103304   888 pts/1    R+   10:56   0:00 grep mongod
```

ArbiterのDB03でステータス状況を見てみると、DB01がConnection refusedのため「"stateStr" : "(not reachable/healthy)"」となっており、
DB02が「"stateStr" : "PRIMARY"」とPrimaryとして稼働していることが分かる。

```
LocalRep:ARBITER> rs.status()
{
	"set" : "LocalRep",
	"date" : ISODate("2017-09-08T09:56:34.742Z"),
	"myState" : 7,
	"term" : NumberLong(2),
	"heartbeatIntervalMillis" : NumberLong(2000),
	"optimes" : {
		"lastCommittedOpTime" : {
			"ts" : Timestamp(1504864535, 1),
			"t" : NumberLong(1)
		},
		"appliedOpTime" : {
			"ts" : Timestamp(1504864535, 1),
			"t" : NumberLong(1)
		},
		"durableOpTime" : {
			"ts" : Timestamp(0, 0),
			"t" : NumberLong(-1)
		}
	},
	"members" : [
		{
			"_id" : 0,
			"name" : "localhost.localdomain:50000",
			"health" : 0,
			"state" : 8,
			"stateStr" : "(not reachable/healthy)",
			"uptime" : 0,
			"optime" : {
				"ts" : Timestamp(0, 0),
				"t" : NumberLong(-1)
			},
			"optimeDurable" : {
				"ts" : Timestamp(0, 0),
				"t" : NumberLong(-1)
			},
			"optimeDate" : ISODate("1970-01-01T00:00:00Z"),
			"optimeDurableDate" : ISODate("1970-01-01T00:00:00Z"),
			"lastHeartbeat" : ISODate("2017-09-08T09:56:34.275Z"),
			"lastHeartbeatRecv" : ISODate("2017-09-08T09:55:43.336Z"),
			"pingMs" : NumberLong(0),
			"lastHeartbeatMessage" : "Connection refused",
			"configVersion" : -1
		},
		{
			"_id" : 1,
			"name" : "localhost.localdomain:50001",
			"health" : 1,
			"state" : 1,
			"stateStr" : "PRIMARY",
			"uptime" : 519,
			"optime" : {
				"ts" : Timestamp(1504864584, 1),
				"t" : NumberLong(2)
			},
			"optimeDurable" : {
				"ts" : Timestamp(1504864584, 1),
				"t" : NumberLong(2)
			},
			"optimeDate" : ISODate("2017-09-08T09:56:24Z"),
			"optimeDurableDate" : ISODate("2017-09-08T09:56:24Z"),
			"lastHeartbeat" : ISODate("2017-09-08T09:56:34.261Z"),
			"lastHeartbeatRecv" : ISODate("2017-09-08T09:56:32.791Z"),
			"pingMs" : NumberLong(0),
			"electionTime" : Timestamp(1504864552, 1),
			"electionDate" : ISODate("2017-09-08T09:55:52Z"),
			"configVersion" : 4
		},
		{
			"_id" : 2,
			"name" : "localhost.localdomain:50002",
			"health" : 1,
			"state" : 7,
			"stateStr" : "ARBITER",
			"uptime" : 806,
			"configVersion" : 4,
			"self" : true
		}
	],
	"ok" : 1
}
```

## DB01を再起動する

```
$ mongod --port=50000 --dbpath=/var/lib/mongodb/db01 --logpath=/var/log/mongodb/db01.log --replSet=LocalRep --fork
```

数秒立つと

- DB01の状態

```
$ mongo --port 50000
LocalRep:SECONDARY> 
LocalRep:PRIMARY> 

```

- DB02の状態

```
$ mongo --port 50001
LocalRep:PRIMARY> 
2017-09-08T11:00:03.401+0100 I NETWORK  [thread1] trying reconnect to 127.0.0.1:50001 (127.0.0.1) failed
2017-09-08T11:00:03.402+0100 I NETWORK  [thread1] reconnect 127.0.0.1:50001 (127.0.0.1) ok
LocalRep:SECONDARY> 

```

- DB03でステータスを確認

```
LocalRep:ARBITER> rs.status()
{
	"set" : "LocalRep",
	"date" : ISODate("2017-09-08T10:05:11.992Z"),
	"myState" : 7,
	"term" : NumberLong(3),
	"heartbeatIntervalMillis" : NumberLong(2000),
	"optimes" : {
		"lastCommittedOpTime" : {
			"ts" : Timestamp(1504865102, 1),
			"t" : NumberLong(3)
		},
		"appliedOpTime" : {
			"ts" : Timestamp(1504865102, 1),
			"t" : NumberLong(3)
		},
		"durableOpTime" : {
			"ts" : Timestamp(0, 0),
			"t" : NumberLong(-1)
		}
	},
	"members" : [
		{
			"_id" : 0,
			"name" : "localhost.localdomain:50000",
			"health" : 1,
			"state" : 1,
			"stateStr" : "PRIMARY",
			"uptime" : 317,
			"optime" : {
				"ts" : Timestamp(1504865102, 1),
				"t" : NumberLong(3)
			},
			"optimeDurable" : {
				"ts" : Timestamp(1504865102, 1),
				"t" : NumberLong(3)
			},
			"optimeDate" : ISODate("2017-09-08T10:05:02Z"),
			"optimeDurableDate" : ISODate("2017-09-08T10:05:02Z"),
			"lastHeartbeat" : ISODate("2017-09-08T10:05:09.553Z"),
			"lastHeartbeatRecv" : ISODate("2017-09-08T10:05:11.725Z"),
			"pingMs" : NumberLong(0),
			"electionTime" : Timestamp(1504864801, 1),
			"electionDate" : ISODate("2017-09-08T10:00:01Z"),
			"configVersion" : 4
		},
		{
			"_id" : 1,
			"name" : "localhost.localdomain:50001",
			"health" : 1,
			"state" : 2,
			"stateStr" : "SECONDARY",
			"uptime" : 1036,
			"optime" : {
				"ts" : Timestamp(1504865102, 1),
				"t" : NumberLong(3)
			},
			"optimeDurable" : {
				"ts" : Timestamp(1504865102, 1),
				"t" : NumberLong(3)
			},
			"optimeDate" : ISODate("2017-09-08T10:05:02Z"),
			"optimeDurableDate" : ISODate("2017-09-08T10:05:02Z"),
			"lastHeartbeat" : ISODate("2017-09-08T10:05:09.524Z"),
			"lastHeartbeatRecv" : ISODate("2017-09-08T10:05:10.254Z"),
			"pingMs" : NumberLong(0),
			"syncingTo" : "localhost.localdomain:50000",
			"configVersion" : 4
		},
		{
			"_id" : 2,
			"name" : "localhost.localdomain:50002",
			"health" : 1,
			"state" : 7,
			"stateStr" : "ARBITER",
			"uptime" : 1323,
			"configVersion" : 4,
			"self" : true
		}
	],
	"ok" : 1
}
```

DB01が復活すると数秒後には、きちんと認識されてPrimaryとして稼働、
DB02もPrimaryからSecondaryに降格する形で正常に稼働しており、
フェイルオーバーがうまくいったことを確認！


次に、プログラム側の設定とコネクション周りの調査検証であるが、
詳細は「[MongoDBのフェイルオーバー時のNode.jsのプログラム制御と動作確認
](http://qiita.com/megadreams14/items/f4f226d33d65f5a88e32)」の記事をご覧ください。


# まとめ

すごく簡単な設定で「Primary」「Secondary」「Arbiter」構成でのレプリケーション及びフェイルオーバーが行えるのはとてもありがたい！
とはいえ、実際の運用では、「Primary」「Secondary」「Secondary」構成や昇格させない「Secondary」の運用など、サービスの性質に応じた設定や運用方針を決める必要があるので、追求すると奥が深そうだ...
次は、実際のフェイルオーバーにかかる時間やレプリケーション遅延など負荷試験を行いながら試してみたい！