Help us understand the problem. What is going on with this article?

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

More than 3 years have passed since last update.

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

概要

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

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

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

環境

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

ミドルウェア バージョン
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への昇格投票のみを行う

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

構築手順

MongoDBをインストール

CentOS6.5にMongoDBをインストールする」の記事を参考に「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のプログラム制御と動作確認
」の記事をご覧ください。

まとめ

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

megadreams14
平成元年生,兵庫県出身,スタートアップ企業(介護×IT)でCTOやってます。 AWS Summit 2014Tokyo,Jenkins Conference 2015, Developers Summit 2015で発表!! 介護×ITという分野に興味ある方、お気軽にご連絡下さい!!
https://brightvie.me/
brightvie
「あなたの“困った・できたらいいな“をカタチに」 ブライト・ヴィーは手作りのICTシステムをお届けするエンジニアチームです。
https://brightvie.me/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away