docker-composeでMongoDBの障害自動回復（フェイルオーバー）を検証してみた

## はじめに
今までは緊急性が高くないプロジェクトではMongoDBをStandalone（単一構成）で利用して来たが、今回はMongoDBで障害発生時に<strong>フェイルオーバー（自動でPrimaryからSecondaryに切り替える仕組み）</strong>を検証してみました。

## フェイルオーバーとは？
>フェイルオーバーは、現用系コンピュータサーバ/システム/ネットワークで異常事態が発生したとき、自動的に冗長な待機系コンピュータサーバ/システム/ネットワークに切り換える機能を意味する。これに対して、何らかの異常を察知して、人間が手動で切り替えを行うことをスイッチオーバーという。 ウィキペディアから引用

データベースに何らかの理由で障害が発生したら修復するまでサービスが止まることがよくありますが、MongoDBはフェイルオーバー構成になっていればPrimary（読み書き構成）に障害が発生しても自動的に降格され、SecondaryやArbiterによって投票されて投票数が多い構成がSecondaryからPrimaryに自動昇格して障害なく通常運用が可能な仕組みです。
![replica-set-trigger-election.bakedsvg.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/18411/53dd02ad-8f5a-195f-2ac4-be15c9206351.png)

## 環境構築の前提条件
docker-composeがインストールされていること
Docker Compose version v2.2.1で検証しました。

## フェイルオーバーの検証流れ
### docker-composeのソースコードダウンロード
[Github(mongo-replicaset-docker-compose)](https://github.com/shoridevel/mongo-replicaset-docker-compose)からソースコードをクローンしてください。
クローンが終わったら```$ docker-compose up -d```を実行して環境構築してください。
### レプリカセット確認
 - Primaryのコンテナに接続します。<br>

```bash
$ docker exec -it mongodb-primary bash
```

### mongoコマンドでPrimaryメンバーに接続します。
```bash
$ mongo -u root -p toor
```

### rs.statusコマンドでレプリカセットの状況を確認します。

```bash
replset:PRIMARY> rs.status();
{
        ...
	"members" : [
		{
			"_id" : 0,
			"name" : "mongodb-primary:27017",
			"health" : 1, -- 1/稼働, 2/停止
			"stateStr" : "PRIMARY",
                        ...
		},
		{
			"_id" : 1,
			"name" : "mongodb-secondary1:27018",
			"health" : 1,
			"stateStr" : "SECONDARY", -- データの読み書き可能なメンバー
                        ...
		},
		{
			"_id" : 2,
			"name" : "mongodb-secondary2:27019",
			"health" : 1,
			"stateStr" : "SECONDARY", -- データの読み書き可能なメンバー
                        ...
		},
		{
			"_id" : 3,
			"name" : "mongodb-arbiter:27020",
			"health" : 1,
			"stateStr" : "ARBITER", -- SECONDAYが偶数の場合に投票数を奇数にするための仕組み、データストアは持ちませんです。データは持ちません。

                        ...
		}
	],
        ...
}
```

### PrimaryからSecondaryに複製されていること検証
Primaryに映画データを書き込みます。

```bash
replset:PRIMARY> use movie;
replset:PRIMARY> db.movie.find();
replset:PRIMARY> db.movie.find({});
replset:PRIMARY> db.movie.insert({title: "千と千尋の神隠し"});
WriteResult({ "nInserted" : 1 })
replset:PRIMARY> db.movie.insert({title: "となりのトトロ"});
WriteResult({ "nInserted" : 1 })
replset:PRIMARY> db.movie.insert({title: "ハウルの動く城"});
WriteResult({ "nInserted" : 1 })
replset:PRIMARY> db.movie.insert({title: "もののけ姫"});
WriteResult({ "nInserted" : 1 })
replset:PRIMARY> db.movie.find();
{ "_id" : ObjectId("61cb1bee1484b6d46186ac15"), "title" : "千と千尋の神隠し" }
{ "_id" : ObjectId("61cb1c061484b6d46186ac16"), "title" : "となりのトトロ" }
{ "_id" : ObjectId("61cb1c141484b6d46186ac17"), "title" : "ハウルの動く城" }
{ "_id" : ObjectId("61cb1c201484b6d46186ac18"), "title" : "もののけ姫" }
replset:PRIMARY> exit
bye
root@mongodb-primary:/# exit
exit
```

### Secondaryに複製されたデータがあること確認
```bash
$ docker exec -it mongodb-secondary1 bash                                                
root@mongodb-secondary1:/# mongo -u root -p toor --port 27018
replset:SECONDARY> db.getMongo().setSecondaryOk(); <<-- ここ忘れずに実行しないとクエリが実行できません。
replset:SECONDARY> use movie;
switched to db movie
replset:SECONDARY> db.movie.find();
{ "_id" : ObjectId("61cb1bee1484b6d46186ac15"), "title" : "千と千尋の神隠し" }
{ "_id" : ObjectId("61cb1c061484b6d46186ac16"), "title" : "となりのトトロ" }
{ "_id" : ObjectId("61cb1c141484b6d46186ac17"), "title" : "ハウルの動く城" }
{ "_id" : ObjectId("61cb1c201484b6d46186ac18"), "title" : "もののけ姫" }
replset:SECONDARY> exit
bye
root@mongodb-secondary1:/# exit
exit
```
PrimaryとSecondaryのデータが同じであることが確認できました！

### Primaryのコンテナをダウンさせてフェイルオーバー確認
```bash
$ docker stop mongodb-primary     <<-- プライマリのコンテナの一時停止                                                         
mongodb-primary

$ docker ps -a
CONTAINER ID   IMAGE                                                COMMAND                  CREATED          STATUS                        PORTS                                 NAMES
bb5f739bbcb5   mongo-express:latest                                 "tini -- /docker-ent…"   43 minutes ago   Up 40 minutes                 0.0.0.0:8081->8081/tcp                mongo-express
e7f47e42ec32   mongo-replicaset-docker-compose_mongo-connector      "docker-entrypoint.s…"   43 minutes ago   Exited (0) 43 minutes ago                                           mongo-connector
1c69595e200e   mongo-replicaset-docker-compose_mongodb-arbiter      "docker-entrypoint.s…"   44 minutes ago   Up 44 minutes (healthy)       27017/tcp, 0.0.0.0:27020->27020/tcp   mongodb-arbiter
fa6683c69b79   mongo-replicaset-docker-compose_mongodb-secondary2   "docker-entrypoint.s…"   44 minutes ago   Up 44 minutes (healthy)       27017/tcp, 0.0.0.0:27019->27019/tcp   mongodb-secondary2
09fad2c160e7   mongo-replicaset-docker-compose_mongodb-secondary1   "docker-entrypoint.s…"   44 minutes ago   Up 44 minutes (healthy)       27017/tcp, 0.0.0.0:27018->27018/tcp   mongodb-secondary1
a63d3f8bf48d   mongo-replicaset-docker-compose_mongodb-primary      "docker-entrypoint.s…"   44 minutes ago   Exited (137) 24 seconds ago　　　<<-- プライマリのコンテナが停止されたこと確認
```
mongo-replicaset-docker-compose_mongodb-primaryの<strong>STATUSがExited</strong>になってること確認します。

### Secondaryでレプリカセットの状況を確認する
```bash
$ docker exec -it mongodb-secondary1 bash                                               
root@mongodb-secondary1:/# mongo -u root -p toor --port 27018
replset:PRIMARY> rs.status();
{
        ...
	"members" : [
		{
			"_id" : 0,
			"name" : "mongodb-primary:27017",
			"health" : 0,   <<--- 停止状態になってる
			"stateStr" : "(not reachable/healthy)",
                        ...
		},
		{
			"_id" : 1,
			"name" : "mongodb-secondary1:27018",
			"health" : 1,
			"stateStr" : "PRIMARY",  <<--- secondaryから投票によりPrimaryに昇格されました!!
                        ...
		},
		{
			"_id" : 2,
			"name" : "mongodb-secondary2:27019",
			"health" : 1,
			"stateStr" : "SECONDARY",
                        ...
		},
		{
			"_id" : 3,
			"name" : "mongodb-arbiter:27020",
			"health" : 1,
			"stateStr" : "ARBITER",
                        ...

		}
	],
        ...
}
replset:PRIMARY> exit
bye
root@mongodb-secondary1:/# exit
exit
```

### 元Primaryの障害対応でPrimaryに戻す
```bash
$ docker start mongodb-primary                                                           
mongodb-primary

$ docker exec -it mongodb-secondary1 bash                                                
root@mongodb-secondary1:/# mongo -u root -p toor --port 27018
replset:SECONDARY> rs.stepDown()   <<---- 自分自身を降格させる（Secondaryになる）
replset:SECONDARY> rs.status();
{
        ...
	"members" : [
		{
			"_id" : 0,
			"name" : "mongodb-primary:27017",
			"health" : 1,
			"stateStr" : "PRIMARY",
                        ...
		},
		{
			"_id" : 1,
			"name" : "mongodb-secondary1:27018",
			"health" : 1,
			"stateStr" : "SECONDARY",
                        ...
		},
		{
			"_id" : 2,
			"name" : "mongodb-secondary2:27019",
			"health" : 1,
			"stateStr" : "SECONDARY",
                        ...
		},
		{
			"_id" : 3,
			"name" : "mongodb-arbiter:27020",
			"health" : 1,
			"stateStr" : "ARBITER",
                        ...
		}
	],
        ...
}
```

## 最後に
docker-composeを利用してレプリカセットを構築しPrimaryメンバーのデータSecondaryメンバーに複製されることとPrimaryがダウン（障害）で投票によりSecondaryがPrimaryに昇格しサービス停止することなく自動フェイルオーバーになることが確認できました。
本番環境ではこのまま使うことは推奨しません。マイクロサービスでそれぞれ別サーバーとして構成するのをおすすめします。