はじめに
今までは緊急性が高くないプロジェクトではMongoDBをStandalone(単一構成)で利用して来たが、今回はMongoDBで障害発生時にフェイルオーバー(自動でPrimaryからSecondaryに切り替える仕組み)を検証してみました。
フェイルオーバーとは?
フェイルオーバーは、現用系コンピュータサーバ/システム/ネットワークで異常事態が発生したとき、自動的に冗長な待機系コンピュータサーバ/システム/ネットワークに切り換える機能を意味する。これに対して、何らかの異常を察知して、人間が手動で切り替えを行うことをスイッチオーバーという。 ウィキペディアから引用
データベースに何らかの理由で障害が発生したら修復するまでサービスが止まることがよくありますが、MongoDBはフェイルオーバー構成になっていればPrimary(読み書き構成)に障害が発生しても自動的に降格され、SecondaryやArbiterによって投票されて投票数が多い構成がSecondaryからPrimaryに自動昇格して障害なく通常運用が可能な仕組みです。
環境構築の前提条件
docker-composeがインストールされていること
Docker Compose version v2.2.1で検証しました。
フェイルオーバーの検証流れ
docker-composeのソースコードダウンロード
Github(mongo-replicaset-docker-compose)からソースコードをクローンしてください。
クローンが終わったら$ docker-compose up -d
を実行して環境構築してください。
レプリカセット確認
- Primaryのコンテナに接続します。
$ docker exec -it mongodb-primary bash
mongoコマンドでPrimaryメンバーに接続します。
$ mongo -u root -p toor
rs.statusコマンドでレプリカセットの状況を確認します。
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に映画データを書き込みます。
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に複製されたデータがあること確認
$ 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のコンテナをダウンさせてフェイルオーバー確認
$ 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のSTATUSがExitedになってること確認します。
Secondaryでレプリカセットの状況を確認する
$ 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に戻す
$ 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に昇格しサービス停止することなく自動フェイルオーバーになることが確認できました。
本番環境ではこのまま使うことは推奨しません。マイクロサービスでそれぞれ別サーバーとして構成するのをおすすめします。