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などバリエーションを増やしていきたいとも考えていますが、まずはブログ書く頻度を増やしていかないといけませんね…

foursquareの11時間にも及ぶサービスダウンの原因を詳細に調査してみた。<a href="http://b.hatena.ne.jp/entry/http://d.hatena.ne.jp/doryokujin/20101014/1287000278" class="bookmark-count"><img src="http://b.hatena.ne.jp/entry/image/http://d.hatena.ne.jp/doryokujin/20101014/1287000278" tit

こんにちは、@です。前回に引き続き、MongoDBに関するエントリーです。今回は10月4日にMongoDBが原因で起きた、foursquareのサービスダウンに関して、その原因や復旧に至る経緯を詳細に調査しました。TechCrunchJapanの記事、Foursquare:「対策を講じたはずなのですが、また6時間もダウンしてしまいました」にも紹介されていたのでご存知の方も多いと思います。MongoDBが原因で引き起こったとするならば、企業で実際に運用している僕にとっては放っておけない問題になります。実は5月にも長いサービスダウンがあったのですが、それはAmazonEC2の停電によるものでした。
本日のアジェンダです:

  • foursquareにおけるMongoDB
  • サービスダウン時の状況
  • 本当にMongoDB自体の問題だったのだろうか
  • サービスダウンの引き金となった出来事
  • 本質的な原因はどこにあったのか
  • 復旧後の状況
  • foursquareの行なった対策について

foursquareにおけるMongoDB

foursquareのサービスにはScalaの言語が用いられ、WebフレームワークであるLiftを使用して作られています。現在130万人のユーザーを抱え、1日に 61.5万回のチェックインが行われています。これまでに5000万回のチェックインが行われました(2010年5月時点)。なお、8月末の時点で会員数は300万人を超えたとロサンゼルス・タイムズのインタビューの中で報じられています。
そして、チェックインの情報を保存しておくデータベースとして現在はMongoDBが使われています。元は1台のPostgreSQLインスタンスをデータストアとして使用していましたが、急速のサービス拡大に伴ってMongoDBに切り替えた経緯があります。切り替えた理由は主にスケールアップ(レプリケーションとシャーディング)の観点で、MongoDBの方がSQLデータベースよりもはるかに容易に導入・管理できるからと考えたためです。この機能が使えるMongoDBのバージョンは1.6からなので、foursquareはバージョン1.6(最新)を使用しています。チェックイン・Tips、そして現在地の施設などにつける、Venueと呼ばれるマーカ情報(とそれに関するデータ)がMongoDBに書き込まれていきます。

MongoDBはロケーション(+/- 180 degrees in each of 2 axes)に関するデータに対して特殊なインデックスを作成することができます。インデックスとして「"2d" index type」というタイプを使用することができ、例えば latlng: [40, -72]というロケーションを持ったvenuesというコレクション(SQLでいうテーブルに当たる用語です)に対して、

db.venues.ensureIndex({latlng: "2d"})

と、インデックスを作成することで以下のようなクエリーでの検索が可能になります。

  • 与えられたロケーション[40,-72]に近いデータ20件を取得する:

db.venues.find({latlng: {$near: [40.72, -73.99]}).limit(20)

  • 与えられたロケーションから各軸で1度だけ異なる範囲にあるデータを20件取得する:

db.venues.find({latlng: {$near: [40.72, -73.99, 1]}).limit(20)

さらに多くの情報は公式ドキュメント日本語訳の地理空間のインデックスを参照してください。

参考:Foursquare & MongoDB

サービスダウン時の状況

  • 10/04 11:00am EST - 片方のシャードにチェックインが集中していたために、パフォーマンスが著しく低下していることに気づく
  • 10/04 12:30pm EST - この1時間半の間に偏りをなくすためのバランシングを試みたがうまく機能しなかった。そして新しいシャードを追加することでこの偏りを軽減させようとした
  • 10/04 6:30pm EST - シャードの追加によっても1台のシャードのパフォーマンス低下が解決されなかったために、インデックスの再構築を行ない、メモリの断片化を修復することを決めた。この再構築にはかなりの時間と、データの損失や破壊の可能性があるためにバックアップ作業が必要になる
  • 10/04 11:30pm EST - 5時間以上におよぶ再構築とバックアップ作業の後、サービスが復旧。幸いにもデータの損失などの障害は避けられた。

この約11時間の間、foursquareのすべてのサービスは完全にダウンしてしまっていました。

参考:So, that was a bummer.

本当にMongoDB自体の問題だったのだろうか

foursquareのダウン時はまだデータベースの記録容量はまだ120GB程度でしかありませんでした。また直近のレポートでは1日約100万チェックインが行われ、これは1秒当たり60チェックイン(=書き込み数)が行われたことになります。この数値は大きいのかというとそれ程でもなく、例えばBasho on NoSQL performanceによれば、RiakとよばれるNoSQLで8ノード(16,000 TPS with 14 nodes)の構成で秒間10,000トランザクションをさばけたという報告があります。(Riakについてはこちらのスライドが参考になります。)なので、今回の問題はMongoDBの本質的な性能不足によるもの以上に、foursquareのMongoDBのデザインやトラブル対応に少し不備があったのではと考えるほうが自然になります。

参考:MongoDB, FourSquare and the Sharding problem

サービスダウンの引き金となった出来事

foursqareではサービスダウン当時、AmazonEC2上で66GBのメモリを積んだ2台のマシンでシャーディング(データベースを分割することで負荷やディスク・メモリ容量を分散させる方法)を行なっていました。シャーディングはユーザーIDを元に行われていました。それまでの2ヶ月間の運用では、データは2台のサーバーに均等にうまく振り分けられていてどちらのシャードも約33GBのメモリを消費する程度で安定していました。しかし今回はこのバランスが崩れてしまったようで、サービスダウンの原因は

  • 1. 片方のシャード(shard0とする)のメモリが物理限界の66GBをオーバーし、著しい性能低下を引き起こした

であったと報告されています。さらにその対策もうまくいかなかったわけで、

  • 2. 第3のシャードを追加することによってshard0から5%のチャンク(データの塊)を移行しようとしたが、その後もshard0のメモリ容量が減らず、状況が解決されなかった

ために、復旧に11時間もかかってしまいました。しかし、幸いにも一部のデータが失われるといった状況にはなりませんでした。

参考:Foursquare outage post mortem

本質的な原因はどこにあったのか

それではこれに関する本質的な原因をMongoDBの特徴と絡めて詳しく見ていくことにします。まず前者に関しては、

ユーザーIDをキーにした分割方法が適切でなかった

が考えられます。ちなみにもう片方のサーバーのメモリ消費量は50GBでした。ユーザーIDを分割のキーとしていたために、片方のシャードに何度もチェックインを繰り返すヘビーユーザーが偏ってしまう振り分けになってしまっていたとしたら、このような状況になってしまう可能性があります。また、後者の原因にはMongoDBが密接に絡んでおり、

インデックスに使用したメモリの断片化によってshard0の負荷を低減させることに手間取った

ことが考えられます。foursquareが記録していたチェックインに関するレコードは1レコード当たり約300バイトでしかありませんでした。実はMongoDBでは4KB以下のレコードの集合に関しては、それが別のシャードに移動したり削除されたとしてもインデックス作成に使用された一部のメモリは開放ずに残ってしまう(断片化)状況が起こってしまうのです。foursquareの場合は、データが連続しているチャンクから効率よくデータを移行することができずにこの状況が起こってしまいました。user-groupのDetails on FourSquare bug?において、新しいシャードの追加が素早く・効果的に機能するための条件として、Dwight Merriman (10gen)は以下のように述べています:

  1. シャードの条件に設定されたキーの振り分けが挿入順序で行われる場合
  2. ソースとなるシャード(ここではshard0)に高負荷がまだかかっていない状況時にシャードを追加した場合
  3. 1つ1つのレコードが4kBより少し上回る程度の容量を持っている場合
  • もしこれらの条件のすべてを満たしていない場合は、速やかにデフラグを行い、断片化の進行を防ぐ必要がある

同サイトに、4kBより遥かに小さい大量のレコードで構成されたコレクションを作成して検証が行われています。このコレクションはインデックスに約300MBの容量を消費していましたが、このコレクションを削除したあとも2.3MBの容量を消費していることがわかります。さらにこのコレクションを作成しては削除するといった動作を繰り返すたびに、どんどん消費量が増えていっています。今回のケースでもこの断片化が引き起こされていたことが考えられます。さらに、他の2条件も満たしていなかったので、なかなか追加シャードへのデータの移行が進みませんでした。

参考:Details on FourSquare bug?

復旧後の状況

その後shard0の復旧が進み、また3番目のシャードに一部のデータの移行が完了したために、現在はデータが均等に振り分けられ安定に動作していると報告されています。MongoDBには全てのインデックスを再構築するためのrepairDatabase()コマンドがあり、これを実行したようです。このコマンドは、すべてのデータが壊れていなかチェックし、壊れているものがあった場合取り除きます。またデータファイルを少し縮小します。しかし、容量の小さなチェックインレコードを大量に持ったコレクションによって引き起こされた断片化の問題に関しては本質的な解決には至らず、スレーブサーバーに repairDatabase() というコマンド(すべてのインデックスの再生成)を実行して最適化した後、このスレーブをマスターに昇格させることで約20GBの消費に抑えたと報告されています。また、今回明確となったMongoDBのボトルネックは、現在10genの技術チームが解決に向けて取り組んでいるとのことです。

参考(MongoDBのrepairコマンドについて):耐障害性と修復(公式ドキュメント邦訳)
参考(MongoDBのインデックスについて):インデックス(公式ドキュメント邦訳)

foursquareの行なった対策について

今回11時間にも及ぶサービスダウンはユーザーに対しての信頼を損ねてしまう結果になってしまいましたが、やはり新しい技術を使用する以上はこのようなリスクは避けられないと思います。しかし、foursqareはこのダウンの間、

  • サーバーダウン中やバックアップ中である旨のメッセージをTwitterアカウント、 @4sqsupport と @foursquare が伝えた
  • また、 @4sqsupport は毎時間ごとに定期的に状況報告を続けた

ときちんと状況報告に努め、かつこの事例から学んだことを、

  • サービスの稼働状況を表示するサイト、 status.foursquare.comを新たに作成した
  • より意味のあるエラーページを作成することにした。現在どのような状況でそれが起きているのかを詳細に記述するようにした

としてすぐにサービスに反映しました。僕はこの一連のきちんとした対応には好感を持っています。また、この大きな出来事を元にMongoDBがより良いものに、そして他の企業この事例を学んで同じ状況が起こるのを回避できるようになっていくことを願っています。

終わりに

今回、様々な海外サイトを参考にさせていただきましたが、英語面・技術面ともにまだまだ未熟ですので訳や解釈の違いが多々あると思います、その場合はご指摘していただけると幸いです。今回はMongoDBの少しネガティブな部分を書きましたが、他にも色々とネガティブな部分があることがわかっています。MongoDBのメリット/デメリットという軸でそのうちエントリーが書けたらなと思っています。

追記(10/21更新)

後日InfoQより詳細な報告があげられていましたので追記してお伝えします:

InfoQ Foursquare's MongoDB Outage

また、この日本語訳も公開されていますので参考にしてください。

FoursquareのMongoDBが機能停止

MongoDBのちょっと詳しいチュートリアル<a href="http://b.hatena.ne.jp/entry/http://d.hatena.ne.jp/doryokujin/20101010/1286668402" class="bookmark-count"><img src="http://b.hatena.ne.jp/entry/image/http://d.hatena.ne.jp/doryokujin/20101010/1286668402" title="はてなブックマーク - Mo

@です。本エントリーから数回にわけてMongoDBの紹介をつらつら書いていきたいと思います。日々、MongoDBの魅力にどっぷりな僕でして、それを少しでも多くの方に共有できたらというモチベーションで書いています。今回はチュートリアルとして主要な機能を少し詳しめに紹介していきます。アジェンダは以下の通りです:

  • はじめに
  • ちょっと詳しいチュートリアル
    • オープンソース
    • NoSQL・ドキュメント指向データベース
    • ドライバとして多くの言語サポート
    • 完全なインデックスサポート
    • リッチなクエリー
    • MySQLに類似した機能群
    • レプリケーション機能
    • オートシャーディング
    • 巨大ファイルを扱うGridFS
  • 今後の予定
    • 本家ドキュメントの翻訳
    • より深い機能説明
    • 勉強会での発表
    • Production Deploymentsとして弊社の名前を掲載する

はじめに

僕は現在MongoDBをソーシャルアプリのログ解析の中間集計データの格納・集約場所としてMongoDBを活用させてもらっています。MongoDB採用に至る経緯や使用目的については前々回のエントリースライドをご覧頂ければと思います。現在過去3ヶ月の行動ログや課金データなどの集計データのMongoDBへの格納がほぼ完了したところでして、現在約2億件のレコードが、シャーディングされた3台のマシンで管理しています。今後は実際のデータ解析を行いながら経営や企画の方々にデータの側から様々な提言をしていきたいと思っています。と、同時に誰でも簡単にデータを閲覧できるようなフロント部分の実装(Webアプリ)も進めて行く予定です。MongoDBにはmongooseというJavaScriptからMongoDBにアクセスできる非常に便利なライブラリーがありますので、これとテーブルや描画関連のAjaxを利用しながら効率よく作業を進めていく予定です。相関を求めたりモデル式を作成したりといった少し突っ込んだ解析にはRに全部任せてしまおうと考えています。このように、芸者東京エンターテインメント GTEにおいて、MongoDBはソーシャルアプリのログ解析のバックエンドとフロントエンドをつなぐ非常に重要な役割を担う存在となっています。

f:id:doryokujin:20101010053256p:image
おみせやさんのログ解析

業務でのアプリケーションや実際のパフォーマンスにつきましては次回のデータマイニング+WEB 勉強会@東京 #TokyoWebMiningで発表させて頂きたいと思います。(次回は11月14日予定です。是非ともご参加下さい!アジェンダはこちらです。)

ちょっと詳しいチュートリアル

それではMongoDBとは一体どのようなDBなのか、今回は主に公式ドキュメントを参照しながら少し突っ込んだ形のチュートリアルとてして見ていきたいと思います。参考にした資料、詳しく書かれた資料などは随時リンクを張っていきます。

オープンソース

現在MongoDBは開発・サポート元である10genからオープンソースとして公開されています。彼らによって商用サポートも行われているみたいです。また、素晴らしいことに公式ドキュメントが非常に充実しています。公式ドキュメントの日本語訳もかなり進んでいます。この日本語ドキュメントは全て@さん1人の手によって運営されています。(ブログはこちら)@さんの日本語訳によってたくさんの方がMongoDBを知り、活用する機会が生まれたと思います。本当にありがとうございます。

NoSQL・ドキュメント指向データベース(JSON-like data schemas)

MongoDBは話題のNoSQL(Not Only SQL)の代表として今や多くの記事に取り上げられるようになりました。内部はC++で記述され、SQLと違ってスキーマレスであり、データはBSONと呼ばれるJSONに似た形式で格納されています。ユーザー側では完全なJSON形式として扱えます。弊社のソーシャルアプリ、おみせやさんのログ解析において格納しているユーザー課金データを例にしますと、

{
 "_id" : "2010-06-28+000000+Charge",
 "lastUpdate" : "2010-09-20",
 "userId" : "000000",
 "date" : "2010-06-28",
 "actionType" : "Charge",
 "totalCharge" : 1210,
 "boughtItem" : { "おもちゃの素EX 5個" : 1,
         "おもちゃの素 5個" : 1,
         "ヌイグルミの素 5個" : 1,
         "おすすめ看板" : 1,
         "梅の湯" : 2 }
}

のような形になっています。

ドキュメント指向データベースは他にもCouchDB等が有名です。CouchDBとの機能比較については

に表があってとてもわかりやすいと思います。他にも、

が参考になります。

NoSQLにつきましてはたくさんの情報が公開されていますので各自お調べ頂けると幸いです。ここではとても詳しく各種NoSQLについて述べられたHPを紹介しておきます。比較表などもあってとても素晴らしい資料だと思います。これが無料で公開されていることは驚きです。

ドライバとして多くの言語サポート

◆各種言語向けドライバ

現在MySQLと同じくらい豊富にドライバが用意されています。公式にサポートされている言語は、

C, C++, Java, Javascript, Perl, PHP, Python, Ruby

ですが、コミュニティサポートとして

C# and .NET, Clojure, ColdFusion, Erlang, Go, Groovy, Haskell, Javascript, Lua, Objective C, Scala, etc...

など、非常に充実しています。

Rails

さらにWebアプリケーションフレームワークのModelとしてMongoDBを扱うためのライブラリも用意されています。例えばRuby On Railsここに詳しく書かれていたり、企業のRails+MongoDB活用事例としては、

が参考になると思います。

Python+Django

僕はPythonを利用してMongoDBの操作を行っています。ドライバは2種類、

を確認しています。僕は前者のPymongoを使用しています。後者はまだ試した事がありませんので比較はしていませんが併せてDjango-MongoKitも用意されているので、Djangoまでの使用を考えるとこちらを使う方が一貫性があって良さそうです。その他のDjangoのサポートとして

があるみたいです。僕もDjangoの使用を検討していましたが、まずはmongooseをつかってWebアプリを実装しようと考えています。Python+Djangoに関してはこちらの記事が色々を参考になると思います:

その他のPythonでのMongoDB支援ライブラリとして、

などがあります。

完全なインデックスサポート

スキーマレスでありながら、完全にインデックスをサポートしているのがMongoDBの大きな強みであると思っています。MongoDBのインデックスは、MySQLのようなRDBMSのインデックスと概念的に似てて、MySQLでインデックスを付けたいような場所に、MongoDBでもインデックスを付ける事ができます。

先ほど例で示したおみせやさんの課金データに対しては、キーである、"_id" ・ "lastUpdate"・"userId"・"date"・"actionType"・"totalCharge"・"boughtItem"
の全てに対してインデックスを付けることが可能です。例えば"userId"に対してインデックスを作成する場合、以下のようなコマンドを指定します。

db.things.ensureIndex({"userId": 1})

スキーマレスですので、例えば"userId"が存在しないレコードがあるかもしれませんが、その場合は値がnullであるものとしてインデックスが付けらることになります。数字の1(-1)は昇順か降順を指定します。

さらにキー"boughtItem"の(連想)配列の要素に対してもインデックスを貼ることができ、

db.things.ensureIndex({"boughtItem.梅の湯": 1})

とすれば良く、"boughtItem"に"梅の湯"を含むレコードを高速に検索・ソートすることが可能になります。

他にも複合インデックスなど、非常に柔軟にインデックスを利用することができます。日本語ドキュメントに詳しく書かれていますのでぜひ一読してみてください。また、インデックスの情報は全てメモリ上に保存されることになりますので、大量レコードのDBに対して複数のインデックスを用いる場合は、十分なメモリ容量が必要になりますので注意して下さい。

リッチなクエリー

◆柔軟な検索条件指定

MongoDBにはダイナミックなクエリーのサポートがあります。様々な条件で柔軟な検索が行えます。HBaseやCassandraといった他のNoSQLでは不可能な検索条件もMongoDBでは可能です。こちらに様々な条件での検索方法が記載されています。例えば、

  • 範囲指定
  • 存在指定
  • 配列のサイズ指定
  • 正規表現

といった条件で検索可能になります。

◆集約関数

また、MySQLの "COUNT" や "DISTINCT"、"GROUP BY" といった集約関数も利用することができます。さらに大量レコードに対する集約関数の適用の際には"Map/Reduce"を使って、シャーディングされた複数のマシンのパワーを利用して処理を行う事ができます。詳しくはクエリーに関しては日本語ドキュメントのクエリー項目の、高度なクエリー"集約関数"を参照して下さい。

そして、以下のMySQLとMongoDBのクエリー比較表が面白いと思います。

MySQLに類似した機能群

今までの機能を見て頂けた通り、MongoDBはNoSQLでありながら非常にSQLに近い機能を持っていることが伺えます。こちらのチャートSQLとMongoDBのコマンド比較が載っています。

さらに現在ではMySQLのデータをそのままMongoDBへ移行した事例が多くの企業から報告されています。

レプリケーション機能

MongoDBにはMySQLと同じくレプリケーション機能を備えており、かつ非常に簡単な設定でそれを実現することができます。現在レプリケーションに関する機能として3種類用意されております。各々を簡単に説明していきます。ここではいきなりmongodコマンドを紹介していますが、これだけで簡単にレプリケーションが行えるという事を理解してもらいと思ったからです。

  1. Master/Slave
  2. Replica Pairs
  3. Replica Sets

◆Master/Slave

あるMasterに対して、そのSlaveを構成する設定です。MasterとSlaveは基本的には入れ替わることはありません。まず、Masterにしたいサーバーに対して、MongoDBのプロセス起動時に

$ bin/mongod --master [--dbpath /data/masterdb/]

として起動します。--dbpathは明示的にデータの格納場所を指定するオプションです。Slave側ではこのMasterの場所を指定すればよく、

$ bin/mongod --slave --source [:] [--dbpath /data/slavedb/]

としてやれば自動的にMasterと同期を行ってくれます。その他のオプションとして同期間隔やMaster/Slave間のやりとりを記録するoplogのサイズを指定することができます。このoplogをより扱いやすくするライブラリも存在します。

さらに2つのMasterに対して1つのSlaveを構成するといったことも可能です。

◆Replica Pairs

Replica Pairsは状況に応じて、MasterとSlaveが自動的に入れ替える事ができるものです。例えばMaster側に障害があった場合には自動的にSlaveをMasterに切り替えて運用を続けることができます。さらにSlaveがMasterに切り替わった場合、新しいSlaveとして使用するサーバーもあらかじめ設定しておくことができます。こちらも設定は非常に簡単で、それぞれのサーバーで

$ ./mongod --pairwith --arbiter

としてプロセスを起動するだけです。remoteserver は、pariの相手のサーバのホスト名です。 標準的でないポートで動かしたい場合には、 :port を付けてください。arbiterserverはある時点で、どのMongoデータベースがMasterとなるかを決定するのを手伝うMongoのデータベースサーバで、独立なサーバーとして起動します。

◆Replica Sets

Replica setsは"Replica Pairs version 2" として最新のバージョン1.6から導入された先進的な機能です。3台以上のサーバーのレプリケーションを行い、障害時には自動でMasterを切り替えることができます。こちらも設定方法はそれほど難しくありませんので、
公式ドキュメントSetting up replica sets with MongoDB 1.6、またはpdf資料や下のスライドを参考してみてください。

オートシャーディング

MongoDBはCassandraやHBaseと同様に容易にスケールアウトさせる事が可能で、また複数台のマシンにDBを分割して配置することができます。これによって1台のマシンの負荷を低減させたり、ディスク容量も低減させることができます。MongoDBはこのDB分割自身を自動で行ってくれる機能を備えています。例えばユーザーIDが1000番台のレコードはこのサーバーに、2000番台は…みたいなことをこちらが考える必要がありません。(もちろんマニュアルに指定することもできます。)Auto Partithiningといわれるこの機能はYahooの[http://research.yahoo.com/project/212:title=PNUTS:やGoogleのBigTableと同じ思想で設計されています。

また、どれかのマシンがダウンしてしまった時も、そのマシンが担っていたデータは自動的に他のマシンに振り分けてくれます。Dynamoと同じConsistent Hashingを採用しています。ConsistentHashingについてはこちらがわかりやすいです

またクエリーの所で紹介しましたが、シャーディングした複数のマシンパワーを利用してMapReduceを行うこともできます。

参考資料は本家ドキュメントと、以下の資料がわかりやすいと思います。

巨大ファイルを扱うGridFS

MongoDBは、バイナリデータの格納をBSONでサポートしています。しかし、MondoDBでのBSONオブジェクトは4MBのサイズに制限されているために、通常ではそれ以上のデータを格納することは不可能です。GridFSの仕様は、それ以上の大きいファイルを複数のドキュメントに透過的に分割する方法を提供します。これは、巨大なファイルを効率的に格納したり、ビデオなどの巨大なファイルを様々な方法で(例えばファイルの最初のNバイトを取得とか)効率的に扱うことができます。

今後の予定

さて、MongoDBに関しては今後とも積極的に情報を書いて行けたらと思っています。

より深い機能説明

今回はチュートリアルとして駆け足で紹介しました。今後はもっと内容を掘り下げて行きたいと思います。例えばシャーディングやレプリケーションに機能ついての詳細説明や、メモリーの消費量や32bitOSでの制約などを紹介していければと思っています。まずはMongoDBの特徴について今回よりもより詳しく書かれたNotes on MongoDBという記事を翻訳してみたいと思っています。

本家ドキュメントの翻訳

現在の日本語ドキュメントを作成していただいている@さんに協力する形で、今後僕も公式ドキュメントのAdmin Zoneの部分の残りの訳を担当させて頂くことになりました。現在のような質の高いドキュメントを維持できるかは不安ですが、皆さんのお役に立てる事を信じて、しっかりと訳を進めていきたいと思います。進捗はブログやTwitter等で告知していきたいと思っています。

勉強会での発表

今後とも勉強会には積極的に参加して、発表を通じて多くの方と議論をしていければと思っています。こちらは主に、現在仕事で行っている、「ソーシャルアプリのログ解析での(アプリケーションとしての)MongoDB」という位置づけで、前回に引き続き、リアルな話をしていきたいと思っています。

Production Deploymentsとして弊社の名前を掲載する

これは実は自分の中で大きな目標にしているのですが、MongoDBの公式サイトのProduction Deploymentsにソーシャルアプリでの導入事例として日本企業初として芸者東京エンターテインメントの名前を是非とも連ねたいですね…はい。

また、このサイトは他の企業がどのような用途でMongoDBを導入しているかという、非常に参考になる部分が多いページですので、ぜひともこのサイトの翻訳と各社の事例紹介を行っていきたいと思っています。