俺でもわかるシリーズ: MongoDBのレプリケーション

この記事は[MongoDB Advent Calendar 2015](http://qiita.com/advent-calendar/2015/mongodb)の17日目です。

# 概要

何を今更感満載のおさらい記事です。

意図せず嘘を含む可能性がゼロではないので、ドキュメント [Replication — MongoDB Manual 3.0](https://docs.mongodb.org/v3.0/replication/) 併読推奨です。

# Replica Setとは？

- [Replication Introduction — MongoDB Manual 3.0](https://docs.mongodb.org/v3.0/core/replication-introduction/)

Replica Setとは、MongoDBが備える強力なレプリケーションと自動フェイルオーバーの仕組みです。

複数台のMongoDBサーバーで構築されたReplica Setは、もう一つのMongoDBの強力な機能であるSharding (水平分割)においてShardとして追加することが可能です。

どちらも大規模で高可用なMongoDBデータベースを構築する際に欠かせないものであり、これを標準機能として持っていることはMongoDBの大きな強みと言えます。

# Replica Setメンバー

- [Replica Set Members — MongoDB Manual 3.0](https://docs.mongodb.org/v3.0/core/replica-set-members/)

Replica Setがその機能を果たすためには、最低3台のMongoDBサーバーが必要です。これは1台のPrimary、1台以上のSecondary、0台または1台のArbiterから構成されます。

## Primary

レプリケーションの親となるメンバーです。

原理的にDBへの書き込みはこの1台に集約されるため、後述のRead Preferenceを使っていかに読み出しを分散させるかがReplica Setの性能を引き出すことのカギになるでしょう。

## Secondary

レプリケーションの子となり、Primaryに追随するようデータの複製を行うメンバーです。

Primary死亡時には基本的に全メンバーによる投票が行われ、Secondaryのうち1台がPrimaryに昇格します。データの読み出しが可能なため、Read HeavyなDBの場合はShardingによる水平分割をせず、Secondaryの数を増やし読み出しの分散だけで済む場合もあります。また、Secondaryを増やすことにはメンバー同時死亡への耐性を高める効果もあります。

### Hidden

参照されず、Primaryへ昇格することもないように設定されたSecondaryです。参照を受けることがなく、Primaryのデータに追随するだけのため、他のSecondaryに比べると負荷は低いです。

定期的に全DBのバックアップを取る運用が必要になる場合、静止点を作る必要があるため、ファイルシステムへの同期を停止しても問題ないようにこのようなメンバーが必要になります。

この他、Delayedメンバーという概念もあり、意図的に時間を遅らせたデータを保持することが可能です。

## Arbiter

DBのデータは保持せず、単にPrimary死亡時の昇格投票のみを行います。実データの読み書きが一切なく、負荷は非常に低いです。Arbiterは必須ではなく、通常PrimaryとSecondaryのメンバー数の合計が偶数になってしまう場合に数合わせとして参加させるメンバーです。

# Replica Setの構成

- [Replica Set Deployment Architectures — MongoDB Manual 3.0](https://docs.mongodb.org/v3.0/core/replica-set-architectures/)

要求性能としては、

Primary = Secondary >> Hidden >> Arbiter

となるため、次のように様々な組み合わせが考えられます（コストが低くなるであろう順番で並べています）。

| Primary | Secondary | Secondary (Hidden) | Arbiter | 合計台数 | 同時死亡許容数 |
|---------|-----------|--------------------|---------|----------|----------------|
|       1 |         1 |                  0 |       1 |        3 |              1 |
|       1 |         1 |                  1 |       0 |        3 |              1 |
|       1 |         2 |                  0 |       0 |        3 |              1 |
|       1 |         2 |                  1 |       1 |        5 |              2 |
|       1 |         3 |                  0 |       1 |        5 |              2 |
|       1 |         3 |                  1 |       0 |        5 |              2 |
|       1 |         4 |                  0 |       0 |        5 |              2 |
|       1 |         4 |                  1 |       1 |        7 |              3 |
|       1 |         5 |                  0 |       1 |        7 |              3 |
|       1 |         5 |                  1 |       0 |        7 |              3 |
|       1 |         6 |                  0 |       0 |        7 |              3 |
|       1 |       ... |                ... |     ... |        n |    (n - 1) / 2 |

偶数台での構成も可能なのですが、そのような場合はArbiterを加えて必ず奇数にするように推奨されています。

ここに書いていない、例えば遅らせる時間を変えた複数台のDelayedメンバーを加えるという構成も考えられます（その場合、合計台数と同時死亡許容数の計算はこの表の通りではありません）。

# Read Preference

- [Read Preference — MongoDB Manual 3.0](https://docs.mongodb.org/v3.0/core/read-preference/)

Read Preferenceとは、クライアントからMongoDBの読み出しを行う際、どのメンバーに対してアクセスを行うか選択できる機能です。

| モード             | 内容                                                                                                 |
|--------------------|------------------------------------------------------------------------------------------------------|
| primary            | Primaryのみから読み出しを行います。                                                                  |
| primaryPreferred   | 基本的にPrimaryのみから読み出しを行いますが、もしそれができない場合Secondaryから読み出しを行います。 |
| secondary          | Secondaryのみから読み出しを行います。                                                                |
| secondaryPreferred | 基本的にSecondaryのみから読み出しを行いますが、もしそれができない場合Primaryから読み出しを行います。 |
| nearest            | ネットワークレイテンシーが最も低いメンバーから読み出しを行います。                                   |

この機能はクライアント側で行うので、サーバー側に特別な設定はありません。

通常は`secondaryPreferred`を指定しておき、常に最新のデータを読み出したい（レプリケーション遅延が一切許されない）場合のみ`primaryPreferred`を使う、というのが普通かと思います。

そのように使ったらSecondaryばかりCPUが上がる！という場合には、`nearest`を検討するのもよいかもしれません。

## Mongooseでの指定方法

[Mongoose](http://mongoosejs.com/)はNode.js用ODMです。リレーショナルではないからORMではなく、Object Document Mapperなのです。以下のように[Schemaオブジェクトを作る際にRead Preferenceを指定](http://mongoosejs.com/docs/guide.html#read)します。

```js
var schema = new Schema({ name: String, inventory: {} }, { minimize: false, read: 'secondaryPreferred' });
var Character = mongoose.model('Character', schema);

// will store `inventory` if empty
var sam = new Character({ name: 'Sam', inventory: {}});
Character.findOne({ name: 'Sam' }, function(err, character) {
  console.log(character); // { name: 'Sam', inventory: {}}
});
```

# Write Concern

- [Write Concern Reference — MongoDB Manual 3.2](https://docs.mongodb.org/manual/reference/write-concern/)

こちらもクライアントの機能で、「どこまで書き込めたら書き込み成功と見なすか」を明示的に指定することができます。重要な書き込みは厳重にチェックするような設定がクライアント側で可能です。

`{w: 'majority', j: true, wtimeout: 2000}`のように、以下のオプションを組み合わせて指定します。

## wオプション

数字または`majority`を指定します。

数字を指定した場合、その台数分のReplica Setメンバーへの書き込みが完了したら書き込み成功と判断されます。

`majority`を指定した場合、投票権を持つReplica Setメンバーの過半数に書き込みが完了したら書き込み成功と判断されます。

デフォルトは`w: 1`です（Primaryに書き込み完了したら書き込み成功）。

## jオプション

trueまたはfalseを指定します。

trueの場合、journal（ストレージ）に書き込みが完了したら書き込み成功と判断されます。

## wtimeoutオプション

書き込み完了までのタイムアウト指定です。単位はミリ秒です。

## Mongooseでの指定方法

こちらもRead Preferenceと同様、Schemaオブジェクト生成時にオプションでWrite Concernを表すオブジェクトを渡すことで指定可能です。

```js
var safe = { w: "majority", wtimeout: 10000 };
new Schema({ .. }, { safe: safe });
```

# Replica SetまわりのMongoDBコマンド

キリがないので、かいつまんで紹介。

## rs.initiate()

- [rs.initiate() — MongoDB Manual 3.0](https://docs.mongodb.org/v3.0/reference/method/rs.initiate/)

最初のPrimaryとなるメンバーでこれを実行することでReplica Setが初期化されます。

## rs.add()

- [rs.add() — MongoDB Manual 3.0](https://docs.mongodb.org/v3.0/reference/method/rs.add/)

Primaryで実行し、Replica Setにメンバーを追加します。

以下の例は、上が通常のSecondaryとして追加する場合、下がHiddenメンバーとして追加する場合です。

```js
rs.add({host: "oreno-mongodb-1.internal:27017"});
rs.add({host: "oreno-mongodb-backup.internal:27017", priority: 0, hidden: true});
```

引数はオブジェクトまたは文字列なので、以下のように指定することも可能なのですが、

```js
rs.add("oreno-mongodb-1.internal:27017");
```

**本番運用中のReplica Setに新規Secondaryを素で参加させるのは事故**なので、気をつけましょう。

## rs.status()

- [rs.status() — MongoDB Manual 3.0](https://docs.mongodb.org/v3.0/reference/method/rs.status/)
- [replSetGetStatus — MongoDB Manual 3.0](https://docs.mongodb.org/v3.0/reference/command/replSetGetStatus/#dbcmd.replSetGetStatus)

Replica Setの状態を表示します。調査の際に非常に多用します。

今、どんなメンバーがいるのか、どのメンバーがPrimaryなのか、疎通は取れているか、などなど。

## rs.stepDown()

- [rs.stepDown() — MongoDB Manual 3.0](https://docs.mongodb.org/v3.0/reference/method/rs.stepDown/)

Primaryで実行し、自身をSecondaryに降格させます。自動フェイルオーバーが発動し、他のSecondaryのうち1台が新たなPrimaryになるでしょう。

## rs.conf()

- [rs.conf() — MongoDB Manual 3.0](https://docs.mongodb.org/v3.0/reference/method/rs.conf/)

現在のReplica Setの設定を表示します。以下のようなオブジェクトが帰ってきます。

```js
{
	"_id" : "oreno-mongodb0-sh00",
	"version" : 10,
	"members" : [
		{
			"_id" : 0,
			"host" : "oreno-mongodb0-sh00-000:27017",
			"arbiterOnly" : false,
			"buildIndexes" : true,
			"hidden" : false,
			"priority" : 1,
			"tags" : {

			},
			"slaveDelay" : 0,
			"votes" : 1
		},
		{
			"_id" : 2,
			"host" : "oreno-mongodb0-sh00-backup:27017",
			"arbiterOnly" : false,
			"buildIndexes" : true,
			"hidden" : true,
			"priority" : 0,
			"tags" : {

			},
			"slaveDelay" : 0,
			"votes" : 1
		},
		{
			"_id" : 3,
			"host" : "oreno-mongodb0-sh00-001:27017",
			"arbiterOnly" : false,
			"buildIndexes" : true,
			"hidden" : false,
			"priority" : 1,
			"tags" : {

			},
			"slaveDelay" : 0,
			"votes" : 1
		}
	],
	"settings" : {
		"chainingAllowed" : true,
		"heartbeatTimeoutSecs" : 10,
		"getLastErrorModes" : {

		},
		"getLastErrorDefaults" : {
			"w" : 1,
			"wtimeout" : 0
		}
	}
}
```

単体でも使用しますが、後述の`rs.reconfig()`と併せて使うことが多いです。

## rs.reconfig()

- [rs.reconfig() — MongoDB Manual 3.0](https://docs.mongodb.org/v3.0/reference/method/rs.reconfig/)

Primaryで実行し、Replica Setの設定を書き換えます。

通常このコマンドの引数に与えるオブジェクトは上の`rs.conf()`から取得したものをベースに、変更したい部分だけ変更したものを使います。

例として、上の`rs.conf()`の結果の中から`oreno-mongodb0-sh00-001`をHiddenメンバーとする場合は以下のようになります。MongoDBのシェルはJavaScriptなので、こういう操作も少しだけプログラムちっくです。

```js
var conf = rs.conf();
conf.members[2].priority = 0;
conf.members[2].hidden = true;
rs.reconfig(conf);
```

`"_id" : 3,`なのに`members[2]`？？

と気付いた人は勘がいいです。`conf.members`はただの配列なので、これで良いのです。間違えて他メンバーの設定を操作しないように！

また、現在のPrimaryの`priority`より高い値をSecondaryの`priority`に設定すると、`rs.reconfig()`を実行したタイミングで即座に`rs.stepDown()`と同様の挙動になります。

## rs.remove()

- [rs.remove() — MongoDB Manual 3.0](https://docs.mongodb.org/v3.0/reference/method/rs.remove/)

Primaryで実行し、Replica Setからメンバーを排除します。社会は厳しい。

引数に指定するhostnameは`rs.status()`から`members[n].name`をコピペしましょう。

## db.printSlaveReplicationInfo()

- [db.printSlaveReplicationInfo() — MongoDB Manual 3.2](https://docs.mongodb.org/manual/reference/method/db.printSlaveReplicationInfo/)

`rs.status()`のoptimeを比較することでもレプリケーション遅延の確認はできるのですが、Unix Timestampを肉眼でパースするのは人類には早いです。こちらを使うと一目でわかります。

# トリビア

## なぜPrimary/SecondaryであってMaster/Slaveでないのか

Replica Set機能が登場する前（[MongoDB 1.6より前](https://docs.mongodb.org/v3.0/release-notes/1.6/)です。遠い昔ですね）のMongoDBには、自動フェイルオーバーの仕組みを含まないMaster/Slaveレプリケーションの機能がありました。

その後、2台のみで構成されるReplica Pairという機能が追加され、それが発展して任意台でのレプリケーションが可能なReplica Setとなりました。そこでいつでも切り替わりが可能という意味を込め、Master/SlaveではなくPrimary/Secondaryという名前が付けられたと思われます。