MongoDBでReplica Setをつくる

Replica Setとは、MongoDBでのレプリケーションの仕組みです。実際に設定を実施しながら、MongoDBでのレプリケーションの仕組みを確認します。

環境

  • CentOS 8.1.1911
  • MongoDB Community Edition v4.2.7

構成

各MongoDBサーバーの構成です。

MongoDB IPアドレス
Primary 192.168.0.10
Secondary 192.168.0.20
Arbiter 192.168.0.30

MongoDBのReplica Setは、3台以上の奇数にて構成する必要があり、PrimaryとSecondary、(必要あらば)Arbiterがメンバーとなります。PrimaryはMasterサーバー、SecondaryはReplicaサーバーです。Arbiterとは、

An arbiter does not have a copy of data set and cannot become a primary. However, an arbiter participates in elections for primary. An arbiter has exactly 1 election vote.

Replica Set Arbiter

データは持たないけど、レプリケーション環境のPrimary選出の投票権(Replica Set Elections)はもつ、というやつです。

Replica Setの作成

Replica Set環境を作成します。各MongoDBサーバーは、既にMongoDBインストール済として作業を進めます。公式tutorialを参考に作業していきます。

Deploy a Replica Set

コンフィグファイルの編集

/etc/mongod.conf ファイルを編集して、 bindIp パラメーターと、 replication セクションに設定をします。PrimaryとSeecondaryサーバー共に、同じコンフィグ設定にした後、mongodプロセスを再起動しておきます。

...

# network interfaces
net:
  port: 27017
  bindIp: 0.0.0.0  # Enter 0.0.0.0,:: to bind to all IPv4 and IPv6 addresses or, alternatively, use the net.bindIpAll setting.

...

# replication Options
replication:
    oplogSizeMB: 30
    replSetName: "rs0"
    enableMajorityReadConcern: false

...

Replicationセクションについては、マニュアルの記述を参考に、調べた内容をまとめておきます。

replication Options

補足:oplogについて

oplogSizeMB パラメーターは、oplogサイズの指定をしています。oplogとは、

The oplog (operations log) is a special capped collection that keeps a rolling record of all operations that modify the data stored in your databases.

MongoDB applies database operations on the primary and then records the operations on the primary’s oplog. The secondary members then copy and apply these operations in an asynchronous process. All replica set members contain a copy of the oplog, in the local.oplog.rs collection, which allows them to maintain the current state of the database.

Replica Set Oplog

RDBのアクティブログ的なものをイメージしています。それをlocal.oplog.rsというcapped collectionに格納してくれています。capped collectionとは、

Capped collections are fixed-size collections that support high-throughput operations that insert and retrieve documents based on insertion order. Capped collections work in a way similar to circular buffers: once a collection fills its allocated space, it makes room for new documents by overwriting the oldest documents in the collection.

Capped Collections

固定サイズの代わりに高速で動作してくれる、上書きローテーションされるコレクションということでしょうか。

oplogSizeMBパラメーターを設定する際に注意しないといけないことは、上書きされていくという事ですね。Secondary側のMongoDBが停止している間に、oplog内のデータが上書きされてしまった場合、レプリケーションされるべきデータがなくなってしまうので、Secondary側が復旧した後に同期できなくなってしまう。この点は、下記Qiitaの投稿が参考になりました。

MongoDBの同期の仕組みと知っておくべき注意点

補足:Read Concernについて

enableMajorityReadConcern パラメーターは、defaultのRead Concern LevelをMajorityにすべきか定義しています。Read Concernとは、

The readConcern option allows you to control the consistency and isolation properties of the data read from replica sets and replica set shards.

Read Concern

MongoDB分散環境下での、読み取り一貫性を保証してくれるものですね。例えばfindクエリを実行する時に、以下のように指定して利用します。

> db.zips.find(
... { city: /NEW YORK/ }
... ).readConcern("majority")
{ "_id" : "10001", "city" : "NEW YORK", "loc" : [ -73.996705, 40.74838 ], "pop" : 18913, "state" : "NY" }
{ "_id" : "10002", "city" : "NEW YORK", "loc" : [ -73.987681, 40.715231 ], "pop" : 84143, "state" : "NY" }

Majorityレベルの動作は下記となります。多数のMemberが承認したデータを返す、とのこと。

The query returns the data that has been acknowledged by a majority of the replica set members. The documents returned by the read operation are durable, even in the event of failure.

その他のレベルについてこちら。

Read Concern Levels

基本的にMajorityを利用すべきとのことですが、今回の構成(Primary, Secondary, Arbiterがそれぞれ1台構成)の場合、避けるべきとのことでパラメーター値をfalseにしています。

In general, avoid disabling "majority" read concern unless necessary. However, if you have a three-member replica set with a primary-secondary-arbiter (PSA) architecture or a sharded cluster with a three-member PSA shard, disable to prevent the storage cache pressure from immobilizing the deployment.

Disable Read Concern Majority

Replica Setのimit処理

コマンドを実行して、初期化処理を実施します。

PrimaryとしたいMongoDBに接続して、以下コマンドを実行します。

> rs.initiate( {
... _id: "rs0",
... members: [
...   { _id: 0, host: "192.168.0.10:27017" },
...   { _id: 1, host: "192.168.0.20:27017" }
... ]
... } )
{
        "ok" : 1,
        "$clusterTime" : {
                "clusterTime" : Timestamp(1592259434, 1),
                "signature" : {
                        "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
                        "keyId" : NumberLong(0)
                }
        },
        "operationTime" : Timestamp(1592259434, 1)
}

rs.initiate

接続しているMongoDBサーバーがPrimaryとなります。以下コマンドにて設定情報の確認ができます。

rs0:PRIMARY> rs.conf()
{
        "_id" : "rs0",
        "version" : 1,
        "protocolVersion" : NumberLong(1),
        "writeConcernMajorityJournalDefault" : true,
        "members" : [
                {
                        "_id" : 0,
                        "host" : "192.168.0.10:27017",
                        "arbiterOnly" : false,
                        "buildIndexes" : true,
                        "hidden" : false,
                        "priority" : 1,
                        "tags" : {

                        },
                        "slaveDelay" : NumberLong(0),
                        "votes" : 1
                },
                {
                        "_id" : 1,
                        "host" : "192.168.0.20:27017",
                        "arbiterOnly" : false,
                        "buildIndexes" : true,
                        "hidden" : false,
                        "priority" : 1,
                        "tags" : {

                        },
                        "slaveDelay" : NumberLong(0),
                        "votes" : 1
                }
        ],
        "settings" : {
                "chainingAllowed" : true,
                "heartbeatIntervalMillis" : 2000,
                "heartbeatTimeoutSecs" : 10,
                "electionTimeoutMillis" : 10000,
                "catchUpTimeoutMillis" : -1,
                "catchUpTakeoverDelayMillis" : 30000,
                "getLastErrorModes" : {

                },
                "getLastErrorDefaults" : {
                        "w" : 1,
                        "wtimeout" : 0
                },
                "replicaSetId" : ObjectId("5ee7f36a3bd8152beb3b7f36")
        }
}

rs.conf()

ステータスの確認。

rs0:PRIMARY> rs.status()
{
        "set" : "rs0",
        "date" : ISODate("2020-06-15T22:19:12.527Z"),
        "myState" : 1,
        "term" : NumberLong(1),
        "syncingTo" : "",
        "syncSourceHost" : "",
        "syncSourceId" : -1,
        "heartbeatIntervalMillis" : NumberLong(2000),
        "majorityVoteCount" : 2,
        "writeMajorityCount" : 2,
        "optimes" : {
                "lastCommittedOpTime" : {
                        "ts" : Timestamp(1592259545, 1),
                        "t" : NumberLong(1)
                },
                "lastCommittedWallTime" : ISODate("2020-06-15T22:19:05.957Z"),
                "readConcernMajorityOpTime" : {
                        "ts" : Timestamp(1592259545, 1),
                        "t" : NumberLong(1)
                },
                "readConcernMajorityWallTime" : ISODate("2020-06-15T22:19:05.957Z"),
                "appliedOpTime" : {
                        "ts" : Timestamp(1592259545, 1),
                        "t" : NumberLong(1)
                },
                "durableOpTime" : {
                        "ts" : Timestamp(1592259545, 1),
                        "t" : NumberLong(1)
                },
                "lastAppliedWallTime" : ISODate("2020-06-15T22:19:05.957Z"),
                "lastDurableWallTime" : ISODate("2020-06-15T22:19:05.957Z")
        },
        "lastStableRecoveryTimestamp" : Timestamp(1592259505, 1),
        "lastStableCheckpointTimestamp" : Timestamp(1592259505, 1),
        "electionCandidateMetrics" : {
                "lastElectionReason" : "electionTimeout",
                "lastElectionDate" : ISODate("2020-06-15T22:17:25.879Z"),
                "electionTerm" : NumberLong(1),
                "lastCommittedOpTimeAtElection" : {
                        "ts" : Timestamp(0, 0),
                        "t" : NumberLong(-1)
                },
                "lastSeenOpTimeAtElection" : {
                        "ts" : Timestamp(1592259434, 1),
                        "t" : NumberLong(-1)
                },
                "numVotesNeeded" : 2,
                "priorityAtElection" : 1,
                "electionTimeoutMillis" : NumberLong(10000),
                "numCatchUpOps" : NumberLong(0),
                "newTermStartDate" : ISODate("2020-06-15T22:17:25.931Z"),
                "wMajorityWriteAvailabilityDate" : ISODate("2020-06-15T22:17:26.797Z")
        },
        "members" : [
                {
                        "_id" : 0,
                        "name" : "192.168.0.10:27017",
                        "health" : 1,
                        "state" : 1,
                        "stateStr" : "PRIMARY",
                        "uptime" : 238,
                        "optime" : {
                                "ts" : Timestamp(1592259545, 1),
                                "t" : NumberLong(1)
                        },
                        "optimeDate" : ISODate("2020-06-15T22:19:05Z"),
                        "syncingTo" : "",
                        "syncSourceHost" : "",
                        "syncSourceId" : -1,
                        "infoMessage" : "could not find member to sync from",
                        "electionTime" : Timestamp(1592259445, 1),
                        "electionDate" : ISODate("2020-06-15T22:17:25Z"),
                        "configVersion" : 1,
                        "self" : true,
                        "lastHeartbeatMessage" : ""
                },
                {
                        "_id" : 1,
                        "name" : "192.168.0.20:27017",
                        "health" : 1,
                        "state" : 2,
                        "stateStr" : "SECONDARY",
                        "uptime" : 117,
                        "optime" : {
                                "ts" : Timestamp(1592259545, 1),
                                "t" : NumberLong(1)
                        },
                        "optimeDurable" : {
                                "ts" : Timestamp(1592259545, 1),
                                "t" : NumberLong(1)
                        },
                        "optimeDate" : ISODate("2020-06-15T22:19:05Z"),
                        "optimeDurableDate" : ISODate("2020-06-15T22:19:05Z"),
                        "lastHeartbeat" : ISODate("2020-06-15T22:19:12.384Z"),
                        "lastHeartbeatRecv" : ISODate("2020-06-15T22:19:11.337Z"),
                        "pingMs" : NumberLong(7),
                        "lastHeartbeatMessage" : "",
                        "syncingTo" : "192.168.0.10:27017",
                        "syncSourceHost" : "192.168.0.10:27017",
                        "syncSourceId" : 0,
                        "infoMessage" : "",
                        "configVersion" : 1
                }
        ],
        "ok" : 1,
        "$clusterTime" : {
                "clusterTime" : Timestamp(1592259545, 1),
                "signature" : {
                        "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
                        "keyId" : NumberLong(0)
                }
        },
        "operationTime" : Timestamp(1592259545, 1)
}

rs.status()

Arbiterの追加

作成したReplica SetにArbiterサーバーを追加します。これもマニュアルを用意してくれています。

Add an Arbiter to Replica Set

Arbiter用のDB Pathを作成

ArbiterとしたいMongoDBサーバーにて、Arbiter用のDB領域として利用するディレクトリーを作成しておきます。

$ sudo mkdir /var/lib/mongo/arb
$ sudo chown mongod:mongod /var/lib/mongo/arb
$ sudo chmod 700 /var/lib/mongo/arb

コンフィグファイルの編集

ArbiterとしたいMongoDBサーバーの /etc/mongod.conf ファイルを編集します。

# Where and how to store data.
storage:
  dbPath: /var/lib/mongo/arb
  journal:
    enabled: true
#  engine:
#  wiredTiger:

...

# network interfaces
net:
  port: 27017
  bindIp: 0.0.0.0  # Enter 0.0.0.0,:: to bind to all IPv4 and IPv6 addresses or, alternatively, use the net.bindIpAll setting.

...

# replication Options
replication:
    replSetName: "rs0"

コンフィグを編集した後、mongodプロセスを再起動しておきます。

Arbiter追加コマンドの実行

PrimaryサーバーのMongoDBに接続して、以下のコマンドを実行します。

rs0:PRIMARY> rs.addArb("192.168.0.30:27017")
{
        "ok" : 1,
        "$clusterTime" : {
                "clusterTime" : Timestamp(1592262280, 1),
                "signature" : {
                        "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
                        "keyId" : NumberLong(0)
                }
        },
        "operationTime" : Timestamp(1592262280, 1)
}

Arbiterサーバーが追加されています。

rs0:PRIMARY> rs.status().members[2]
{
        "_id" : 2,
        "name" : "192.168.0.30:27017",
        "health" : 1,
        "state" : 7,
        "stateStr" : "ARBITER",
        "uptime" : 3022,
        "lastHeartbeat" : ISODate("2020-06-15T23:55:00.796Z"),
        "lastHeartbeatRecv" : ISODate("2020-06-15T23:55:00.779Z"),
        "pingMs" : NumberLong(5),
        "lastHeartbeatMessage" : "",
        "syncingTo" : "",
        "syncSourceHost" : "",
        "syncSourceId" : -1,
        "infoMessage" : "",
        "configVersion" : 2
}

アクセスコントロールの設定

Replica Setの環境に、アクセスコントロールの設定を有効にする方法です。マニュアルはこちら。

Deploy Replica Set With Keyfile Authentication

rootユーザーの作成

PrimaryサーバーのMongoDBに接続して、rootユーザーを作成しておきます。

> use admin
> db.createUser(
...   {
...     user: "root",
...     pwd: "password",
...     roles: [ { role: "root", db: "admin" } ]
...   }
... )

keyFileの作成

分散環境下のMongoDBでは、作成された鍵ファイルを利用して内部通信をしているらしいです。 A key’s length must be between 6 and 1024 characters and may only contain characters in the base64 set. とあるので、マニュアルを参考に必要となる鍵ファイルを作成します。

opensslコマンドにて、乱数を作成しています。

$ openssl rand -base64 756 | sudo tee /var/lib/mongo/keyFile
$ sudo chown mongod:mongod /var/lib/mongo/keyFile
$ sudo chmod 400 /var/lib/mongo/keyFile

作成した鍵ファイルを、Replica Set環境の全MongoDBサーバーに配置します。

コンフィグファイルの編集

コンフィグファイルを編集して、作成したkeyFileを利用して、Access ControlモードでMongoDBを起動するよう設定します。これも全MongoDBサーバーに設定します。

...

security:
  authorization: enabled
  clusterAuthMode: keyFile
  keyFile: /var/lib/mongo/keyFile

...

security Options

コンフィグファイルを編集した後、MongoDBを再起動します。

検証

挿入と検索

データを挿入してみます。

rs0:PRIMARY> use test
switched to db test
rs0:PRIMARY> db.zunda.insertOne(
...   { name: "Tohoku Zunko" },
...   { writeConcern: { w: 1, j: true, wtimeout: 1000 } }
... )
{
        "acknowledged" : true,
        "insertedId" : ObjectId("5ee923f8c9f1bdf73095d5da")
}

writeConcern オプションは、分散環境下での書き込み一貫性を保証するオプションです。各パラメーターの説明のざっくり説明はこちら。

The value specified to w determines the number of replica set members that must acknowledge the write before returning success. For each eligible replica set member, the j option determines whether the member acknowledges writes after applying the write operation in memory or after writing to the on-disk journal.

上記クエリの場合、1台以上のサーバーのディスク(journal)に書き込みされることで処理成功となります。

Write Concern

Write処理はPrimaryのMongoDBにしかできませんが、Read処理はSecondaryでも実施できます。DefaultではPrimaryに読み込みしにいくそうで、明示的にSecondaryを指定する場合、 readPref オプションを利用するとのこと。

rs0:PRIMARY> db.zunda.find().readPref( "secondary" )
{ "_id" : ObjectId("5ee923f8c9f1bdf73095d5da"), "name" : "Tohoku Zunko" }

Read Preference

failoverを試してみる

実際にfailoverさせてみます。PrimaryであるMongoDBを停止します。

$ sudo systemctl stop mongod

SecondaryであったMongoDBに接続して、ステータスを確認します。Primaryのサーバーが移動していますね。

> rs.status().members
[
        {
                "_id" : 0,
                "name" : "192.168.0.10:27017",
                "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("2020-06-16T20:12:06.845Z"),
                "lastHeartbeatRecv" : ISODate("1970-01-01T00:00:00Z"),
                "pingMs" : NumberLong(1),
                "lastHeartbeatMessage" : "Error connecting to 192.168.0.10:27017 :: caused by :: Connection refused",
                "syncingTo" : "",
                "syncSourceHost" : "",
                "syncSourceId" : -1,
                "infoMessage" : "",
                "configVersion" : -1
        },
        {
                "_id" : 1,
                "name" : "192.168.0.20:27017",
                "health" : 1,
                "state" : 1,
                "stateStr" : "PRIMARY",
                "uptime" : 3104,
                "optime" : {
                        "ts" : Timestamp(1592338326, 1),
                        "t" : NumberLong(9)
                },
                "optimeDate" : ISODate("2020-06-16T20:12:06Z"),
                "syncingTo" : "",
                "syncSourceHost" : "",
                "syncSourceId" : -1,
                "infoMessage" : "could not find member to sync from",
                "electionTime" : Timestamp(1592338276, 1),
                "electionDate" : ISODate("2020-06-16T20:11:16Z"),
                "configVersion" : 2,
                "self" : true,
                "lastHeartbeatMessage" : ""
        },
        {
                "_id" : 2,
                "name" : "192.168.0.30:27017",
                "health" : 1,
                "state" : 7,
                "stateStr" : "ARBITER",
                "uptime" : 3065,
                "lastHeartbeat" : ISODate("2020-06-16T20:12:06.674Z"),
                "lastHeartbeatRecv" : ISODate("1970-01-01T00:00:00Z"),
                "pingMs" : NumberLong(2),
                "lastHeartbeatMessage" : "",
                "syncingTo" : "",
                "syncSourceHost" : "",
                "syncSourceId" : -1,
                "infoMessage" : "",
                "configVersion" : 2
        }
]

停止したMongoDBサーバーを起動し直すと、Secondaryとなります。