MongoDBのレプリケーションとバックアップ機能の紹介

久々の更新です。MongoDBのドキュメントのReplicationの部分の訳が一応完了しましたので、それに合わせてこのブログでもReplication機能について書いていきたいと思います。まだ解釈の甘い部分も残っていますので、今後もこの部分の勉強を続け修正を行っていきます。また、引き続きAdmin Zoneの訳を進めていくつもりです。
本日のアジェンダです:

  1. MongoDBのReplication機能について
    1. Master/Slave
    2. Replica pair
    3. Replica set
  2. MongoDBのバックアップ機能について
    1. ファイルのバックアップ
    2. mongodumpによるエクスポート

MongoDBのReplication機能について

MongoDBのReplicationは細かく分けると3種類あります。

Master/Slave

典型的なMasterとSlaveの構成です。Masterが常に書き込み処理を行い、Masterに書き込まれたデータがSlaveに同期されていきます。ただ1つのMasterのみが書き込みに対してアクティブなことにより、強い一貫性(consistency)を実現します。ただ、それ以外のSlaveに対してもオプションで読み込み権限を与えることができます。これは結果整合性(eventual consistency)を採用しているためです。
MongoDBでMaster/Slaveの構成を実現するのはとても簡単です。Masterサーバーは自身がMasterであることを起動時のオプションで明示してやります。一方、Slaveサーバーには自身がSlaveであることと、Masterサーバーの居場所(source)を起動オプションで明示してやるだけです:

  • master


//port: 27020でmasterサーバーを起動します。
mongod --master --port 27020 --dbpath /data/mongo/master


//--fork オプションとログの保存先:--logpath [log_path] を指定してやることでバックグラウンド起動もできます
mongod --master --port 27020 --fork --logpath /var/log/mongodb/mongodb.log --dbpath /data/mongo/master

  • slave

今、Masterサーバーが同じlocalhost内で起動しており、Slaveサーバーをlocalhostの27021で、データの保存先を /data/mongo/slave として起動する場合を考えます。ここで必要なのは --slave オプションと --source [host (:port)] のみです。


mongod --slave --port 27021 --source localhost:27020 --dbpath /mongo/slave

また、1つのslaveで、2つの別々のmasterからデータを持ってくるといった事も可能です。こちらにその例があります。

Replica pair

Replica pairはMongoDBのバージョン1.6でReplica setに置き換わるまで、フェイルオーバー機能を備えたもう1つのレプリケーションでした。Master/Slaveとの大きな違いは、フェイルオーバーと呼ばれる、現在書き込みがアクティブなサーバー(primary)がダウンしたときに自動的にその他のサーバー群(secondaries)から1台をprimaryに昇格させて処理を継続させる機能を持っていることです。そしてReplica pairではその名の通り2台のサーバーで構成されるのに対し、Replica setは3台以上のサーバーで構成できるより柔軟な仕様になっています。現在Replica pairの採用は推奨されていませんが、こちらも非常に簡単に構成することができますので紹介しておきます。

  • 起動

Replica pairとしてMongoDBを起動する場合、必要なのはペアとなるサーバー情報(host、port)と、arbiterと呼ばれるペアのどちらをprimaryにするかの決定を支援する独立したサーバーの情報です。今、同じlocalhost内でポート27020(pair1)と27021(pair2)と27022(arbiter)を使ってreplica pairを構成してみます。これはあくまで例であって、本来はすべてのサーバーは違うマシンで稼働させるべきです。得にarbiterサーバーは独立したマシンで起動させないと意味がありません。pair1を起動させる際に、--pairwith コマンドで相方のサーバー情報を、--arbiter コマンドでarbiterサーバー情報を明示します:


mongod --port 27020 --pairwith localhost:27021 --arbiter localhost:27022 --dbpath /data/mongo/pair1

同様にpair2も起動します:

mongod --port 27021 --pairwith localhost:27020 --arbiter localhost:27022 --dbpath /data/mongo/pair2

arbiterサーバーは特別なオプションをつけずに起動しておきます:

mongod --port 27022 --dbpath /data/mongo/arbiter

  • arbiter

ここでarbiterについて少し説明を加えておきます。独立したマシンで起動されたarbiterサーバーは、replica pairのメンバーがネットワーク障害でお互いに通信できなくなった場合、どのデータベースをprimaryにするのかの決定を効果的に行う役目を持っています。もし --arbiter オプションを省略することもでき、この設定では通信ができなくなった場合、両方のサーバがprimaryとして動作することになります。しかし書き込みが双方のサーバーで継続して可能な場合、この状況ではデータの競合が起こってしまいます。


f:id:doryokujin:20101102053703p:image
arbiterサーバーが必要となる場合。通信障害の後もClientsからのリクエストは双方のサーバーへ届いてしまいます。
これを防止するためにarbiterサーバーがただ1つのprimaryを決定します。

Replica set

Replica setはReplica pairに置き換わる機能としてバージョン1.6より追加されました。複数台以上のサーバーでのフェイルオーバーをサポートできるのが大きな特徴です。Replica setsにおいては以下の3種類のノードが存在します:

  • standardノード

標準的なreplica setのノードです。データはこのノード間で完全に同期され、この内のただ1台だけがprimaryとなりその他はSecondaryとして動作します。どのノードもprimaryになる可能性を持っています。

  • passiveノード

このノードは完全なデータの複製を保持しますが、決してprimaryになることはありません。

  • arbiterノード

データの複製も保持せず、primaryノードにもなり得ません。このノードは現primaryノードに障害があったときに次のprimaryをsecondariesから選択する際の補助役としてのみ動作します。

Replica setとしてMongoDBを起動する場合には、--replSet オプションでsetメンバーに共通の名称を起動時に明示してやる必要があります。各サーバーを起動させた後、1つのサーバーでsetメンバーの登録設定を行います。その後、initiateコマンドによってすべてのsetにその設定が伝搬・反映され、Replica setとして機能するようになります。今回も簡単な例として、localhostのポート27020(set1)、27021(set2)、27022(set3)、27023(arbiter)を使用した構成例を示します。


mongod --rest --replSet myRepl --port 27020 --dbpath /data/mongo/set1
mongod --rest --replSet myRepl --port 27021 --dbpath /data/mongo/set2
mongod --rest --replSet myRepl --port 27022 --dbpath /data/mongo/set3
mongod --rest --replSet myRepl --port 27023 --dbpath /data/mongo/arbiter

続いてset1をシェルから呼び出してReplica setの設定を行います:


// シェルを起動
mongo localhost:27020


//config オブジェクトの作成
> config = {
_id: 'myRepl', //_idは--replSetで指定したset名
members: [ //メンバーの登録、_idはユニーク
{_id: 0, host: 'localhost:27020'},
{_id: 1, host: 'localhost:27021'},
{_id: 2, host: 'localhost:27022'},
{_id: 2, host: 'localhost:27023',arbiterOnly : true}] //arbiterノードに指定
}
//設定の反映
> rs.initiate(config);
{
"info" : "Config now saved locally. Should come online in about a minute.",
"ok" : 1
}
//数秒後にReplica setはオンラインになって使用可能な状態になる
// > rs.status() で状態の確認ができる

  • フェイルオーバー時の動作

それでは現primaryがダウンしてしまった場合、どのようにして時期primaryが選択されるのでしょうか?ここではオプションである優先度(priority)がすべてのsecondaryで等しいものとして話を進めていきます。現在のprimaryがダウンした際には最も直近で同期を完了したサーバーが優先的に次のprimaryとして選択されるようになっています。新しいprimaryとして選ばれたサーバーは他の健常なサーバーと同期を始めます。実は各サーバーにはデフォルトで1つの投票権(vote)を持っており、時期primaryの決定にはこのvoteによる投票が行われ、過半数のvoteを得たサーバーが晴れてprimaryとなります。各サーバーは現在通信可能なサーバーの中で最新のデータを保持しているサーバーへ投票します。過半数を得るためには少なくとも半数以上のサーバーが通信可能な状態でないといけません。そうでない場合は過半数票を得ることができませんのでprimaryが選ばれず、書き込み不可能な状態が継続します。これは2台のstandardノードで構成されるReplica setでも生じ、primaryがダウンした場合、voteは自身の持つ1つのみであるため過半数とみなされず、primaryを決定できません。よってこれにarbiterサーバーを加えた少なくとも3台以上のサーバーでReplica setを構成しないといけません。

※時期primaryの選ばれ方についてはまだ理解が浅く、もしかしたら上の記述は間違っているかもしれません(すいません)。その際はご指摘していただけると幸いです。


f:id:doryokujin:20101102070754p:image
primaryがダウン。この時secondary2が1分前に同期を終えたばかりで最新のデータを有している。


f:id:doryokujin:20101102070755p:image
この時secondary2がprimaryとして選択され、他のノードとの同期を再会する。

MongoDBのバックアップ機能について

ほとんどのデータベースにはデータのバックアップ機能を備えています。MongoDBにも大きく分けて2種類のバックアップ機能を備えています。

  • ファイルのバックアップ
  • mongodumpによるエキスポート

この2つの違いは主に前者は書き込み処理を止めて実行しないといけない(すなわちダウンタイムが生じる)のに対して、後者はサーバーを停止させずにバックアップを行うことができる点です。それでは詳しく見ていきましょう。

ファイルのバックアップ

ファイルのバックアップとは、直接MongoDBのデータファイルをコピーすることです。Linuxの場合、データパスは /data/db/ に設定されていますので、このディレクトリ以下のファイルをコピーすれば基本的にバックアップは完了します。また、このデータパスはデータベース(mongod プロセス)の起動の際にオプションとして指定することができます。例えば27020ポートを利用してmongodプロセスを起動し、かつ /data/mongo/master/ 以下にデータを保存したい場合は


mongod --port 27020 --dbpath /data/mongo/master

とします。このバックアップは非常に簡単ですが、いくつかの注意点があります:

  • 書き込みが行われている途中でのバックアップは安全ではない

データベースが稼働中で書き込み処理が行われている際中にmongodのデータを直接バックアップするのは安全ではありません。データベースを一度停止させてバックアップをし、その後再起動する方法もありますが、mongoDBではコマンドライン上でfsyncコマンドを利用することでデータベースを停止させることなく、一時的な書き込みのロックを行って、その間にバックアップを行います。

  • fsync コマンドを利用する

fsyncコマンドは元々まだ書き込みが行われていない処理を全て強制的に行うコマンドですが、安全にデータベースのデータファイルのスナップショットを取るためのロックオプションをサポートしています。ロックオプションが指定されたfsyncコマンドが実行されている間はすべての書き込み操作がブロックされます。この間にバックアップを行い、完了後にunlockコマンドでロックを解除して書き込み処理を再開するようにします。


> use admin //adminデータベースを使用します
switched to db admin
> db.runCommand({fsync:1,lock:1})
{
"info" : "now locked against writes",
"ok" : 1
}

>// この間にデータのバックアップを行います…
>// バックアップ終了後は以下のロック解除コマンドを実行します。

> db.$cmd.sys.unlock.findOne();
{ "ok" : 1, "info" : "unlock requested" }
> // アンロックが実行されましたが、これには少し時間がかかるかもしれません…

  • バックアップしたデータからmongodを起動する際の注意

バックアップしたデータを例えば /data/mongo/slave/ に保存して、これをdbpathとして、


mongod --dbpath /data/mongo/slave

と実行使用としたときに、

old lock file: /data/db/mongod.lock. probably means unclean shutdown
recommend removing file and running --repair
see: http://dochub.mongodb.org/core/repair for more information

と表示されて起動に失敗することがあります。これは不意にmongodbがシャットダウンされた場合に、ロックされたままの状態になっているような際に起こります。このときは mongod.lock ファイルを削除して起動してみてください。

mongodump によるエクスポート
  • mongodump

上述の方法では、一時的にMongoDBの書き込み処理を停止させてしますことになります。一方、このmongodumpによる方法はデータベースが稼働中でもデータベース全体をdumpすることができます。下記の例では、--outで指定したパスに--host で稼働しているMongodから--dbで指定したdbをバックアップしています。


mongodump --host localhost:27020 --db mydb --out /data/mongo/dump

  • mongorestore

mongodumpでダンプされたファイルは、mongorestoreコマンドによってリストアすることができます。dumpされたディレクトリ以下には[collection名].bsonというファイル名でデータが格納されています。今、以下のmongodサーバーでリストアを行うことにします。先ほどdumpしたファイルが /data/mongo/dump にあるとします。このdumpファイルを /data/mongo/restore というディレクトリ以下にリストアするとします。まずはmongodプロセスを起動します:


mongod --port 27021 --dbpath /data/mongo/restore

そしてコンソール上で以下のコマンドでデータベース:mydbのリストアが実行されます。--dropコマンドは既に同じコレクション名が存在している場合には先にコレクションのデータをを削除してからリストアするオプションです。

mongorestore --port 27021 --db mydb --drop --dbpath /data/mongo/dump

終わりに

これまでに紹介した機能はMongoDBの持つ多彩なレプリケーション機能のほんの一部でしかありません。より細かな設定や他の機能は公式ドキュメント(日本語英語)を参考にしてください。また、まだMongoDBのレプリケーション機能は万能ではありません。以前エントリーで紹介したFoursquareの事例のように、断片化の問題や、同期の遅さ、フェイルオーバーの失敗などが報告されています。しかしそのような問題も、提供元の10genとコミュニティの方々の活発な活動によってどんどん解消されていくだろうと思います。そして、僕もその活動に少しでも関わって行けたらと感じています。今後とも本ブログにおいてMongoDBに関するエントリーを続けていきますのでよろしくお願いします。他にもデータマイニング、勉強会報告、GraphDBなどバリエーションを増やしていきたいとも考えていますが、まずはブログ書く頻度を増やしていかないといけませんね…