MongoDB JPの一年を振り返り、そして今後についてお話しします

MongoDB JPが発足してちょうど一年が経とうとしています。発足当時まだ学生だった頃の話です。初めは小さなコミュニティだったMongoDB JPも、現在では600名近いのGoogleグループのメンバーを抱え、毎回の勉強会でも100名以上の参加者に集まって頂くような規模にまで成長してきました。
この一年でMongoDBを実運用で使われているWeb企業さんもだいぶ増えてきました。多くの方の支えと協力があっての今もなお成長し続けている事に関しては感謝の気持ちでいっぱいです。

今回は技術の話しでは無く、以下の内容でMongoDB JPのこれまでの活動と現状を振り返り、そして今後について述べていきたいと思います。

アジェンダ

  1. MongoDB JPのこれまでの活動
    • MongoDBと共に成長した一年間を簡単に振り返ります。
  2. MongoDB JPの現状の問題
    • 金銭的な問題・ドキュメント整備の問題などについて。
  3. MongoDB JPのこれからについて
    • 10genと共に今後は日本での本格展開に向けて動いていきます。
  4. 今後のMongoDBイベント
    • 今年から来年にかけて、Mongoイベントは盛りだくさんです。

1. MongoDB JPのこれまでの活動

MongoDB JPはこれまでに6回の定例勉強会(最近の開催風景第5回第6回)と、10gen主催の公式MongoDB Conferenceを1回、そしてソースコードリーディング分科会やその他の勉強会に招かれての発表など、たくさんの活動を行ってきました。僕がこれまでに書いたMongoDB関連の資料も20近くになりました。これまでの経緯を少し振り返っていきます。

第1回MongoDB勉強会

記念すべき12月の第1回勉強会CouchDB JPの方と合同で開催しました。初めての主催側の勉強会という事で段取りなどに非常に苦労したことを覚えています。リラックスなCouchな皆さんには非常に助けられました。スタッフを募り・発表資料を作り・当日はあれこれ裏方で動く…当時はそんな大変な勉強会をこれから続けていけるのかと不安でした。

MongoDB Conference in Japan

3月には10gen主催のMongoDB Conference in Japan(通称 mongotokyo)を開催しました。10genの方と密にやり取りしながら、英文メールの書き方や簡単な会話のしかたを学びました。楽天さんに会場をお借りし、技評さんに告知をお願いし、スタッフと発表者を募集し…、これらも非常に大変でしたが、世界公式のカンファレンスを日本で行える嬉しさと誇りはその苦労を遙かに上回るものでした。例え個人であっても、OSSを通じて世界のトップエンジニアとつながれるチャンスはいくらでもあるんだなぁとその時強く感じました。今思えば蒼々たる方々に発表して頂け、カンファレンス自身も成功に終わったことはMongoDBの今後の可能性を確信させてくれ、大きな自信となるものでした。
そしてまた、この時に当時スタッフとして参加してくださっていた @ さんに出会えたことは、以後のMongoDB JPの安定かつ継続的な活動ができているという意味で非常に大きな事だったと思います。

MongoDB 勉強会@フューチャーアーキテクト

それ以降の勉強会は @ さんの協力のおかげでフューチャーアーキテクトさんで開催できることになりました。常に10名近くのフューチャー社内・そして今では社外の多くのスタッフの方が勉強会の進行を支えてくれることになり、僕は運営や金銭管理のオペレーションを全て任せてしまって発表と会の進行に専念することができるようになりました。会場誘導をはじめUstream、そして懇親会の集金や準備まで全てが彼らの完璧なオペレーションの元で遂行されています。僕自身、勉強会で発表することで誰よりも自分自身が勉強になっていますので、僕は当日の司会と発表をするだけで良いというのは本当にありがたい状況です。いつもMongoDB勉強会をさせてくださっているスタッフの皆さん、本当にありがとうございます。そしてそれからも宜しくお願いします。

2. MongoDB JPの現状の問題

さて、皆さんに支えられて成長してきたMongoDB JPですが、もちろん色々と問題を抱える事にもなります。

運営資金の問題

基本的にオープンなコミュニティでありますので、勉強会などは必要な最低限の経費のみ参加者から頂くことにしています。主におやつ代と懇親会費ですね。ただ100名を超える規模になりますと当日キャンセルの方も多く、予定していた懇親会参加人数を10-20名下回るということもありました。そういった場合は数万円の赤字となってしまい、コミュニティで負担することになります。現状で僕たちは5万円近くの累積赤字を抱えています。金銭管理をうまくやれていないことに関してはきちんと反省してアクションを起こしていかないと日々反省するばかりです。

会場の問題

いつもフューチャーさんのおかげで100名を超える規模で安定してやらせてもらっていますが、フューチャーさんの会場が借りれなくなった時点で色々と問題が発生します。100名を超える人数を収容する会場を見つける事、その会場をお借りするのに必要なお金、そしてそのお金をどう捻出していくのか、など色々と大変になってしまいます。
特に来年1月に開催する第2回MongoDBカンファレンスでは、多数の参加者を見込めることから300名以上を収容できる会場が良いわけですが、まともに借りれば百万円はかかりますし、それを実現するためにたくさんのスポンサーを集めないといけません。そのために日々の仕事の合間を縫ってスポンサーを探す営業活動などをしていかないといけない、そういった所に時間をとられる懸念がありました。今回は前回に引き続き、楽天さんのご厚意によって無料で大きな会場を貸して頂く事ができました。本当にありがとうございます。他にも同時通訳を採用するのか、もしそうならその費用をどう捻出するかなどなど、コミュニティがこれだけ大きくなるとお金と時間が個人レベルではまかなえないくらいたくさんかかってしまい、もはや今までのままでは存続維持ができない状況になりつつあります。

日本語ドキュメント整備の問題

これも非常にしんどい問題です。本家のドキュメントの更新が非常に早くて翻訳が追いつかない、常に古い情報しか提供できていないという状況にあります。また、現状でドキュメント翻訳に参加している人もほぼいない状況ですので現在はほとんど行われていない状況にあります。僕も最近ほとんど訳していません…すいません…
これはきちんと呼びかけを行っていないという僕のさぼりの問題なのですが、前述した更新の速さもあってなかなかしんどいです。元々英語のドキュメントを読んでもらうようにしてもらえばありがたいというのが本音だったりします(すいません…)。

3. MongoDB JPのこれからについて

本の窓口・エンタープライズ

さて、このようにコミュニティが大きく活発で、またそれ故の問題点を多数かかえるMongoDB JPですが、現在一つの転換期を迎えようとしています。10genによるMongoDBの本格的な日本展開です。具体的には日本の中でMongoDBをサポートする窓口を設けること、また強力なパートナー企業さんを見つけ、スポンサーやエンタープライズへのMongoDBの普及を後押ししてもらう、などなどです。
現在はそのファーストステップであり、それらを実現するための活動を10genと一緒に積極的に行っていく、といった状況です。特にエンタープライズ向けへのMongoDB本格導入のために動いてくれ、将来的にMongoDBの日本の窓口の中心なってくれるパートナーさんとも手を組んでいます。彼らが10gen側と密にやりとりをしてくれ、既にエンタープライズ方面で活発に動いてくれています。僕も今後はエンタープライズの方々向けに資料を用意し、導入メリットや技術的な話しを紹介しに行くといった活動を増やしていこうと思っています。
なんにせよ、10genというアメリカのテクノロジーベンチャーが世界の中の日本という国に可能性を見いだしてくれたことはまずは非常に嬉しいことです。

ですので今後MongoDBの導入を検討されている企業さんがいらっしゃいましたら是非とも声をおかけ下さい。どのようなシーンでMongoDBが活用できるのか、そもそもMongoDBで何ができるのか、色々ご提案させて頂きます。僕としても新しいMongoDB活用事例に出会えることに関して非常にわくわくしています。

MongoDBとオープンコミュニティ

ただ僕自身、エンタープライズの領域やサポート窓口の部分は今の所は深く介入するつもりはなく、これからもMongoDBを愛する1ユーザーとして、オープンなコミュニティ活動を中心となって勢力的に行っていこうと考えています。
オープンソースの恩恵を享受している身として、その恩返しのためにもこの活動を継続的に行っていくことは僕たちエンジニアに課せられた使命でもあります。また今まで一緒にコミュニティを育ててきたMongoな面々、 @ さん、 @ さん、 @ さんを代表するフューチャーのMongoスタッフの皆さん、ソースレベルでMongoDBを語れるPFIの @ さん・ @ さん、ソースコードリーディング主催者の @ さん、そして人生の師である @ 師、またMongoDB立ち上げを後押ししてくれ、コミュニティにいつも参加してくださっている @ さん、宴会部長 @ さん、その他多数のメンバー(書ききれなくてすいません…)と共に今後ともMongoDBを通じた交流と発信を行っていくつもりです。今後とも宜しくお願いします。

4. 今後のMongoDBイベント

今後もMongoDBイベント盛りだくさんです。特に来年1月18日のmongotokyo2012は10gen側からPresidentを迎え、300名を超える規模で開催したいと考えております。ふるってご参加下さい!

mongotokyo2012について

既に募集の方が始まっています。開催一ヶ月前まではEarly Birdという通常より安い参加費で登録できます。一ヶ月を切りますと、General Admissionの価格になりますのでご注意下さい。また楽天の社員の方・発表者およびスタッフの方の参加日は無料です。ひとまずここで登録せずにお待ち下さい。本カンファレンスでは懇親会の費用をすべてこちらで負担します、カンファレンス後の懇親会まで、是非ともご参加下さい!

日付 タイトル
2011/11/15(火) 「第7回 MongoDB 勉強会 in Tokyo」〜今年最後のMongoイベント〜
2011/11/19(土)-20(日) オープンソースカンファレンス2011
2011/11/19(土) 楽天テクノロジーカンファレンス2011
2011/12/7-9 MongoDB Leader Summit & MongoSV *海外に行ってきます!
2012/1/18(水) Mongo Tokyo 2012
2012/1/27(金) エンジニアサポート新年会

今後ともMongoDB JP をどうぞ宜しくお願いします!

MongoDBの新機能:ジャーナリングについて詳しく

v1.8でMongoDBはジャーナリングと呼ばれる機能が新たに加わりました。今日はMongoDBのジャーナリングについて、実際にどのような処理が行われているのかを確認しながら、丁寧に見ていくことにしましょう。※なお、ジャーナリングという言葉自身、Mongoにこの機能が実装されるまで深く意識するようなことはありませんでした。解釈の部分で誤りなどがあるかもしれません、その際はご指摘していただけると幸いです。

ジャーナリングによってデータの堅牢性が格段に高まった

v1.8でジャーナリング機能が追加されたことによって、シングルサーバーにおけるデータの堅牢性がさらに高まりました。ジャーナリングという言葉は主にファイルシステムの分野においてかなり前から議論され、改善が進められてきた機能です。この意味におけるジャーナリングの目的はファイルシステム全体を保護することであり、そのためにメタデータの整合性を保持するために処理が施されるのが一般的なものです。一方、データベースで用いられるジャーナリングの目的は主にデータの保全であり、サーバーの障害からデータの損失をいかに最小限に抑えられるか、という目的で処理が施されます。もちろんMongoDBにおけるジャーナリングの目的も後者に該当します。

また、サーバーが完全に再起不能な状態に陥ってしまう状況に備えたデータ保全対策も重要で、その場合の対策は物理的に異なるサーバーにReplica Setを(またはMaster/Slave)構成するといった事が必要です。MongoDBでは従来のReplica Setによるデータ保全に加えて、ジャーナリング機能が今回新たに加わった形でシングルサーバーに対しての強力なデータ保全機能が備わりました。これによりサーバーダウンによるデータの損失を最小限にとどめることができ、またスムーズなリカバリが可能になりました。

実はジャーナリングが導入される前のバージョンまでは、シングルサーバーに対してはそれほど強力なデータの保全機能を備えているわけではありませんでした。次の章では、まずジャーナリングが追加される前のv1.6が抱えていた問題点を指摘し、それがジャーナリングによってどのように改善されるのかを示すことにします。

insertなどのデータ変更のディスクへの反映は60秒に1回

MongoDBを使用する多くの場合、各種言語ドライバを介してMongoDBへデータ処理を行うはずです。その際に注意して欲しいのは、多くのドライバのデータのinsertは"fire-and-forget"と呼ばれ、毎回のinsert実行後、それが実際にディスクに書き込まれたかを確認せずに次の処理を次々に実行します。基本的にはupdate等のクエリに対しても同じです、これらのオペレーション併せて「データ変更処理」と呼ぶことにします。
この理由の1つに、MongoDBではinsert処理実行後すぐにデータがディスクに書き込まれるわけではないというのが挙げられます。まずはメモリに書き込まれ、(デフォルトでは)60秒に1回、メモリ上の全てのデータがディスクに書き込まれるという処理が行われています。これによって大量のデータを効率良くかつ高速に書き込めるようになっているのですが、これはメモリからディスクへの書き込みが行われる前にサーバーのダウンなどの何らかの問題が生じた場合にはまだディスクに書き込まれていなかったデータは完全に消失することになってしまうというデメリットも持っています。つまりデフォルトでは最悪60秒の間にinsertされたデータが丸々失われてしまうリスクを背負っていることになります。
ただ、この問題への対応策は既に2点用意されています。
1つは起動時のコマンドラインオプション"--syncdelay"によってディスクへのフラッシュ間隔を任意の秒数に変更することです。ただしこの設定を短い秒数に変更することで、Replica SetsやMaster/Slave環境を構成している場合はメンバーとの同期のための通信量が増えてしまい、大きな負荷となりえます。シングルサーバーで稼働している場合以外にはこの値を減少させることは推奨されていません。
2つ目は毎回のinsertの実行結果を確認するオプションがドライバに備わっているはず("safe"モード、または "set write concern"と呼ばれているコマンドです。)ですのでそれを使用する事です。このオプションを使用する事で、毎回のinsert()コマンドの後にgetLastError()コマンドを実行するようになります。このgetLastError()コマンドは、直前のオペレーションが成功したか否かを確認するコマンドです。またこのコマンドを実行することでデータに変更が加えられるオペレーションに対しては即座にディスクに書き込まれるようになります。ただしこのモードを使用すると(当たり前ですが)明らかに処理速度が低下します。MongoDBを利用する際には"critical"なデータと"non-critical"なデータという区別を持ち続け、それに併せた適切なデータ変更処理を行うようにしてください。

以上をまとめますと、MongoDBのデフォルトの振る舞いは"non-critical"なデータを想定したパフォーマンス重視に触れた設定になっています。安全なデータ変更処理を行う場合は、パフォーマンスを多少犠牲にしてでも上に述べた2つの施策(特に後者)を必ず行うようにして下さい。

ジャーナリングがデータの保全性を格段に高める第3の手段を与えてくれた

そしてv1.8で第3の手段となるジャーナリング機能によって、先ほど述べたデータ損失のリスクを大幅に低減されることになりました。
ジャーナリング機能を有効にして起動されたMongoDBは、実際にオペレーションを実行してデータに変更が加えられる前にこのオペレーション自身を追記専用の先行書き込みログ、ジャーナルファイルという特殊なファイルに書き込みます。より厳密に言えば100ミリ秒に1回、グループコミットという手法を用いてジャーナルファイルにオペレーションを書き込んでいきます。
このジャーナルファイルへの先行書き込みによって、インデックス作成などのデータの変更処理の実行途中、あるいはいくつかのデータがディスクにフラッシュされていない状況でサーバーがダウンしてしまったとしても、再起動時にそのオペレーションを再実行することでデータのリカバリができるようになります。ジャーナル機能をオンにしたMongoDBでは再起動時にクライアントから処理を受け付ける状態に入る前に、ジャーナルファイルに記録されたオペレーションを再実行してデータのリカバリが行うようになります。これによって今まで最悪60秒間のデータが全て失われてしまう可能性のあったものが、100ミリ秒間のデータの損失のみで済むようになりました。また、今まで必要だったmongod.lockの削除と、起動後のdb.repairDatabase()の実行も不要になりました。
このようにジャーナリングはデータの保全性に対して非常に大きな改善をもたらしてくれました。ただし、毎回データに変更を与えるオペレーションが先行してログに書き込まれる事になるので、それなりにパフォーマンスが落ちてしまう事はさけられません。記事、MongoDB Journaling Performance - Single Server Durabilityでは、ジャーナリングによって30%程度パフォーマンスが落ちてしまったという報告がされています。まだこの機能が登場して間もないことやこのようなパフォーマンスの問題もあり、v1.8においてはジャーナリングはデフォルトの機能にはなりませんでした。jounalingを使用するかどうかは、MongoDBをどのような目的や環境で利用しているかによって判断すべきでしょう。ただし今後ジャーナリングのグループコミットを行う間隔の改善や種々のパフォーマンスの改善がある程度進んだ段階でデフォルトの機能になる事でしょう。

従来のダウンからの復帰に必要な手順

さて、実際にジャーナリングの振る舞いを確認する前に、先ほど触れた言葉、mongod.lock、db.repairDatabase()に関する従来のサーバーダウンから復帰の手順についてお話ししておきます。
mongodが正常に終了されなかった場合、手動で再起動して正常な状態に復帰させるには以下の2つの手順が必要になってきます:

起動前に mongod.lock ファイルを削除する

mongodの起動時にはデータディレクトリの中にはmongod.lockというファイルが作成されます。また、正常終了時にはこのファイルは削除されます。mongod.lockの主な役割は正常終了されたか否かをファイルの存在によって確認することと、1つのデータディレクトリに1つのmongodしかアクセスできないようにブロックする役割です。異常終了の場合はこのファイルが残ることになり、再起動する場合には警告が出てこのファイルを削除しない限り起動することができません。この手順の詳細は後ほど具体的に説明しますのでここでは割愛させて頂きます。

起動後、db.repairDatabase()を実行して全データが壊れていないかのチェックを行う

また、いくつかのデータが破損している可能性がありますので起動後はまずdb.repairDatabase()というコマンドを実行して全データのスキャンを行う必要があります。このdb.repairDatabase()コマンドは全データが壊れていないかを確認するとともにデフラグのような役割も担います。ただ、このコマンドはデータベースの規模が大きいと(ロックはしないものの)パフォーマンスの大幅な低下と、非常に時間がかかってしまう事には注意しないといけません。また、実行のためにテンポラリファイルをdbpath内に新たに作成することを知っておかないといけません。

実際にジャーナリングの動作を確認してみる

それではここからは実際に手を動かし、mondodのサーバーログを確認しながらジャーナリングの動作に対する理解を深めることにしましょう。

journalサブディレクトリが作成され、ジャーナルファイルが作成されていることを確認する

mongodの起動時に--journalオプションをつけて起動してみましょう、ここではdbpathはdataディレクトリを指定しています。そして早速mongodのサーバーログを確認してみましょう。[1]で"journal dir=data/journal" として、なにやら新しいディレクトリがdbpath以下に作成されたのがわかります。[2]では"recover : no journal files present, no recovery needed"と書かれていますね。"recover"を行おうとしたけれども、ジャーナルファイルが見つからなかったのでその必要がない事が書かれています。その後、[3]でmongodはクライアントからの接続待ちの状態になります。(今回は必要な部分しか抜粋していません。)

$ mongod --journal --dbpath data
Sun Jun  5 22:33:13 [initandlisten] MongoDB starting : pid=50557 port=27017 dbpath=data 64-bit
Sun Jun  5 22:33:13 [initandlisten] db version v1.8.0, pdfile version 4.5
[1]Sun Jun  5 22:33:13 [initandlisten] journal dir=data/journal
[2]Sun Jun  5 22:33:13 [initandlisten] recover : no journal files present, no recovery needed
[3]Sun Jun  5 22:33:13 [initandlisten] waiting for connections on port 27017
Sun Jun  5 22:33:13 [websvr] web admin interface listening on port 28017
...

まず[1]で実際にdbpathにjournalサブディレクトリが作成された事を確認してみましょう:

$ ls data   
journal    mongod.lock    test.0    test.1    test.ns

確かに作成されていることが確認できました。さらにjournalサブディレクトリの中を確認してみます:

$ ls -lh data/journal/

total 24
-rw-------  1 doryokujin  staff   8.0K  6  5 22:55 j._0
-rw-------  1 doryokujin  staff    88B  6  5 22:56 lsn

j._0 というファイルとlsnというファイルが作成されているのがわかります。どちらもバイナリファイルですので中身を確認することはできません。それではここでtestデータベースのmycollコレクションに対して100,000件の挿入を行ってみます:

$ mongo localhost:27017/test  #<host>:<port>/<dbname>

> for(var n=0; n<100000; n++){ db.mycoll.insert( { "n": n } ); }

ここでもう一度journalサブディレクトリを確認してみます:

$ ls -lh data/journal/

total 23696
-rw-------  1 doryokujin  staff    12M  6  5 23:19 j._0
-rw-------  1 doryokujin  staff    88B  6  5 23:19 lsn

j._0ファイルサイズが増えているのが確認できました。この j._0ファイルは前述したジャーナルファイルと呼ばれるもので、データに対して更新が行われた際のオペレーションが全て記録されています。インデックスの作成もジャーナルファイルに記録されます:

> db.mycoll.ensureIndex({"n":1})

このオペレーションに対してもジャーナルファイルに記録されているのが確認できます:

$ ls -lh data/journal/

total 30280
-rw-------  1 doryokujin  staff    15M  6  5 23:29 j._0
-rw-------  1 doryokujin  staff    88B  6  5 23:29 lsn

ジャーナルファイルはそのファイルサイズが1GBに達した時点でローテートされj._0, j._1,...という名前で保存されていきます。また、古くなったジャーナルファイルは自然に削除されるため、通常運用においては2つから3つのジャーナルファイルが存在している状況になります。ただし、念のためにジャーナルファイル用に10GB程度の空き容量を確保するようにしておいて下さい。また、データの読み取りに関するオペレーションは何回行ってもジャーナルファイルには記述されません:

> var ary=[]; 
> for(var n=0; n<1000; n++){ ary[n]=db.mycoll.findOne( { "n": n } ); }
> ary

[
        {
                "_id" : ObjectId("4deb904e79d55a3e071f21a9"),
                "n" : 0
        },
        {
                "_id" : ObjectId("4deb904e79d55a3e071f21aa"),
                "n" : 1
        },
...
$ ls -lh data/journal/

total 30280
-rw-------  1 doryokujin  staff    15M  6  5 23:29 j._0
-rw-------  1 doryokujin  staff    88B  6  5 23:37 lsn
正常終了時から再起動までの動作を確認する

ここからはmongodの正常終了・異常終了の際のジャーナリング機能周辺の挙動を確認してみることにしましょう。まずは正常終了させるために、起動している27017のmondodに対してCTRL+Cを押します。するとmongodのサーバーログは正常に終了処理を開始します。[1]に"shutdown: journalCleanup..."という表示が出ていますね、ここでジャーナルファイルのクリーンアップが行われているようです。[2]でジャーナルファイルが削除されているのが確認できます。[3]でmongod.lockファイルが削除され、[4]で正常な終了処理が完了したという通知とともに、mongodサーバーが終了します。

...
Sun Jun  5 22:45:30 [interruptThread] shutdown: closing all files...
Sun Jun  5 22:45:30 closeAllFiles() finished
[1]Sun Jun  5 22:45:30 [interruptThread] shutdown: journalCleanup...
[2]Sun Jun  5 22:45:30 [interruptThread] removeJournalFiles
[3]Sun Jun  5 22:45:30 [interruptThread] shutdown: removing fs lock...
[4]Sun Jun  5 22:45:30 dbexit: really exiting now

このように、正常な終了処理が行われるとjournalサブディレクト内のジャーナルファイルは全て削除されます。正常に終了したのですから、終了前の全てのデータ変更オペレーションはディスクに反映されていることが保証されていますので、次の起動の際にジャーナルファイルを保存しておく必要もないわけです。

異常終了時から再起動までの動作を確認する(ジャーナリング機能を有効にしていない場合)

それではここでmongodを意図的に異常終了(ダウン)させてみますが、ここではさらにジャーナリング機能を有効にした場合とそうでない場合でその挙動の違いを確認することにします。まずは--journalオプションなしで起動し、newcollコレクションに対して100,000件のデータ挿入してみましょう。

$ mongod --dbpath data

> for(var n=0; n<100000; n++){ db.newcoll.insert( { "n": n } ); }

この操作の後にmongodを強制的にシャットダウンさせます。

$  ps aux | grep mongod | grep -v grep
doryokujin 35401  16.4  0.5  2675212  22240 s015  S+   11:54PM   0:00.61 mongod --dbpath data

$  sudo kill -KILL 35401

強制終了されたmongodに対して再起動しようとすると前述したとおり、以下の様な警告が出て起動できません。

mongod --dbpath data
Sun Jun  5 23:56:48 [initandlisten] MongoDB starting : pid=42647 port=27017 dbpath=data 64-bit 
Sun Jun  5 23:56:48 [initandlisten] db version v1.8.0, pdfile version 4.5
Sun Jun  5 23:56:48 [initandlisten] git version: 9c28b1d608df0ed6ebe791f63682370082da41c0
Sun Jun  5 23:56:48 [initandlisten] build sys info: Darwin erh2.10gen.cc 9.6.0 Darwin Kernel Version 9.6.0: Mon Nov 24 17:37:00 PST 2008; root:xnu-1228.9.59~1/RELEASE_I386 i386 BOOST_LIB_VERSION=1_40
************** 
[1]old lock file: data/mongod.lock.  probably means unclean shutdown
[2]recommend removing file and running --repair
see: http://dochub.mongodb.org/core/repair for more information
*************
Sun Jun  5 23:56:48 [initandlisten] exception in initAndListen std::exception: old lock file, terminating
Sun Jun  5 23:56:48 dbexit: 
Sun Jun  5 23:56:48 [initandlisten] shutdown: going to close listening sockets...
Sun Jun  5 23:56:48 [initandlisten] shutdown: going to flush diaglog...
Sun Jun  5 23:56:48 [initandlisten] shutdown: going to close sockets...
Sun Jun  5 23:56:48 [initandlisten] shutdown: waiting for fs preallocator...
Sun Jun  5 23:56:48 [initandlisten] shutdown: closing all files...
Sun Jun  5 23:56:48 closeAllFiles() finished
Sun Jun  5 23:56:48 dbexit: really exiting now

[1]で正常終了時には削除されるはずのdata/mongod.lock が残っているとの警告が出されていますね。そして[2]でこのファイルを削除して再起動し、reparコマンドを実行する事を勧めてきています。先ほどは記述しませんでしたが、repairコマンドは、起動後にdb.repairDatabase()を実行する以外に、起動の際に--repair コマンドラインオプションをつけることでも同じことが行えます。また、mongod.lockの削除の際はくれぐれも他のファイルやディレクトリを削除しないように注意して下さい。

$ rm data/mongod.lock 
$ mongod --repair --dbpath data

このように--journalオプションをつけずに復帰した場合は強制終了するまでにディスクに書き込まれていたデータだけが反映されており、実行途中のもの、実行されたもののまだディスクにフラッシュされていないデータはディスクに反映しないまま消失してしまいます。

異常終了時から再起動までの動作を確認する(ジャーナリング機能を有効にしている場合)

それでは次に--journalオプションをつけて起動し、前回使用したnewcollコレクションを一度ドロップしてから同じ処理を行ってみます。

$ mongod --journal --dbpath data

> db.newcoll.drop()
true
> for(var n=0; n<100000; n++){ db.newcoll.insert( { "n": n } ); }
$ ps aux | grep mongod
doryokujin 71504  17.2  0.2  2889368   6684 s015  R+   12:05AM   0:01.34 mongod --journal --dbpath data
$ sudo kill -KILL 71504

それでは--journalオプションをつけたままmongodを再起動してみましょう。そうすると今度は警告も出ずに再起動することができます。サーバーログを見てみましょう:

$ mongod --journal --dbpath data
Mon Jun  6 00:08:24 [initandlisten] MongoDB starting : pid=83165 port=27017 dbpath=data 64-bit 
Mon Jun  6 00:08:24 [initandlisten] db version v1.8.0, pdfile version 4.5
Mon Jun  6 00:08:24 [initandlisten] git version: 9c28b1d608df0ed6ebe791f63682370082da41c0
Mon Jun  6 00:08:24 [initandlisten] build sys info: Darwin erh2.10gen.cc 9.6.0 Darwin Kernel Version 9.6.0: Mon Nov 24 17:37:00 PST 2008; root:xnu-1228.9.59~1/RELEASE_I386 i386 BOOST_LIB_VERSION=1_40
[1]Mon Jun  6 00:08:24 [initandlisten] journal dir=data/journal
[2]Mon Jun  6 00:08:24 [initandlisten] recover begin
[3]Mon Jun  6 00:08:24 [initandlisten] recover lsn: 0
[4]Mon Jun  6 00:08:24 [initandlisten] recover data/journal/j._0
[5]Mon Jun  6 00:08:26 [initandlisten] recover cleaning up
[6]Mon Jun  6 00:08:26 [initandlisten] removeJournalFiles
[7]Mon Jun  6 00:08:26 [initandlisten] recover done
Mon Jun  6 00:08:26 [dur] lsn set 0
Mon Jun  6 00:08:26 [initandlisten] waiting for connections on port 27017
Mon Jun  6 00:08:26 [websvr] web admin interface listening on port 28017

[1]でジャーナルディレクトリの存在を確認し、[2]でrecover(リカバリ)を開始したことを述べています。[3]でlsnファイルから必要なジャーナルファイルを確認し、[4]でジャーナルファイル: j._0 に記述されたオペレーションを実行してリカバリを行っています。これにより、実行されたものの、ディスクに書き込まれていないデータをDBに反映することができます。これにより実際に実行されたオペレーションに対しては、リカバリ時に再実行されることによってデータの変更が保証されることになります。そして[5]で正常にリカバリが行われ、[6]でジャーナルファイルを削除し、[7]でリカバリが終了したことが述べられています。このように、サーバーログを見ればジャーナリング有効時の挙動を理解することが出来ます。

まとめ

さて、いかがでしたでしょうか?途中でも触れましたが、ジャーナリングはまだ導入されたばかりの機能であり、今後はグループコミットのタイミングの短縮化など、より精錬され、デフォルトの機能になることは間違いありませんので、その仕組みがどんなものでどれだけ重要な機能であるかを理解しておくことは非常に重要です。この記事によってその助けになれば幸いです。それではまた。

Sharding を使いこなすための5つのTips

@です。今日も相変わらずMongoDBの、そしてShardingに関する記事を書こうと思います。

…と、その前にお知らせです!6月は2つのMongoDB勉強会を予定しております、是非ご参加下さい!

・2011年6月11日(土) 「第4回 MongoDB 勉強会 in Tokyo」@フューチャーアーキテクト 
・2011年6月28日(火)「第1回 MongoDB ソースコードリーディング」@PFI

さて、それでは本題に入りたいと思います。
MongoDBのShardingといえば、

・初期設定やShardの追加・削除といった導入の容易さ
・Shardの面倒をMongo側がずっと見てくれるという管理の容易さ

を備えていると言うことで興味を持っておられる方も多数いると思います。
しかしその一方で実際にSharding環境を導入している方々の中の多くは、遭遇する様々な不都合や不整合に頭を悩まし、初期設定のミスで取り戻しのつかない状況に陥っていたりと、想定していた以上の苦労を強いられているように思われます。確かに現バージョン(v1.8)のMongoDBのShardingはまだまだ扱いが難しく、出来る限り「Shardingを使わない」方向で検討を進める方が得策である場合も多くあるような気がします。しかしきちんと対処法を身につけておきさえすれば、そういった起こりうるShardingの種々の問題に立ち向かうことができるはずです。
今日は少しマニアックな内容ですがShardingについて、知っておくと将来きっと役立つであろう5つのTipsを紹介したいと思います。
MongoDBのShardingはその余りある欠点を補うほどに強力です。大規模なSharding環境では、TBを超えるデータをあたかもシングルサーバーのMongoDBを相手にしているように簡単に扱え(若干言い過ぎですが…)、Replicationと連携した「大規模データストレージ」としての役割と、Mongo MapReduceやRedisやHadoopとの連携した「大規模データ解析基盤」としての役割を高い次元で両立させることができます。
この事はログ解析の観点で言えばScribeやFlume等で多数のサーバーに分散された大規模ログを定常的にMongoDBに集約させつつ、Mongo ShellからMap Reduceをさらっと記述して実行して、非バッチな処理やごく一部の領域にしか存在しない対象の解析でもインデックスなどを活用して高速かつ手軽に実行することができるようになる事を意味しています。Webアプリケーションの観点で言えば、あらゆるユーザーデータをMongoDBに格納し、一方バックグラウンドではリアルタイムに集計やクラスタリングアルゴリズムなどを走らせ、ユーザー単位でのパラメータの最適化やデータに基づいたアクションを即座にアプリケーションに反映することができる事を意味しています。

Sharding 5 Tips

0. 準備 〜自動Shardingと自動Balancing機能、そしてChunkという概念を知る〜
1. Primary Shardを活用する 〜Primary Shardの存在を知る〜
2. Chunkの事前分割 〜運用開始からデータを分散させる〜
3. Chunkの手動マイグレーション 〜安全に効率的にChunkを移動する〜
4. Chunkの手動分割&マイグレーション 〜動かないChunkを移動する〜
5. Shardの状態を監視する 〜有用な2つのコマンド〜

0. 準備 〜自動Shardingと自動Balancing機能、そしてChunkという概念を知ろう〜

ここではMongo Shardingの基本的な概念について簡単に解説したいと思います。「自動Sharding」とは挿入されるデータに対して、そのデータがどのShardに入るべきかを決定して振り分けてくれる機能です。より具体的に言えば、Sharding環境においてMongoDBはInsertされたドキュメントを(最初に設定した)ShardKeyの値に基づいて振り分けます。Sharding環境ではドキュメントの集合である"Chunk"という単位を持ち、このChunk単位で分割や移動が行われます。それぞれのChunkは他と重複しないShardKeyのレンジを持っています。例えばChunkAは [ "a", "k" ), ChunkBが [ "k", "{" ) のレンジを持っていたとすると、ShardKeyのイニシャルが"a"から"j"までの値を持つドキュメントはChunkAに、"k"から"z"までを持つドキュメントはChunkBに属します。"{" は "z" の次の順序を持つ値です。例えば"inoue" の値を持つドキュメントは ChunkA に、"takahiro" を持つドキュメントは ChunkB に属する事になります。繰り返しますが、このChunkがShard間を移動したり分割されたりします。1つ1つのドキュメント単位でそれらが行われることが無いことにはくれぐれも注意して下さい。

各Chunkはそのレンジが重複することは無く、またカバーしていないレンジもありません。先ほどの例では実は最低でもさらに後2つのChunkが存在します。それは [ -∞, "a" ) と [ "{", ∞ ] です。そして1つのChunkがデフォルトで200MB(Sharding開始時は64MB)以上のサイズに膨れあがると、Chunkはそれを等分割するキーによって分割されます。例えば ChunkA は [ "a", "g" ) と [ "g", "k" ) に分割されたりします。データ流入量が増え、Chunkの分割があちこちで行われるようになると、Shard間でChunkの数にばらつきが起こってきます。ドキュメントのShardKeyが特定のレンジ、つまり特定のChunkに集中する場合はそのShard内でたくさん分割が行われる事になります。これはある程度避けられない事であり、またデータローカリティを強く意識するならこの現象は必ずしも悪いものとも言い切れません。
MongoDBでは現在アクセスされているShardのChunkの数とクラスタ内で最小のChunk数を持つShardとの数の差を計算しており、その差が10以上になったときに、「Shard間に偏りが起こっている」と認識され、Chunk数が大のところから小のところへChunkの移動(マイグレーション)が行われます。これを自動Balancing機能と呼び、バックグラウンドでShard間のデータサイズを均質に保ち続けようとしてくれる親切な機能です。

このように、MongoDBのShardingは自動Sharding機能と自動Balancing(とReplica Sets の自動フェイルオーバー機能)によって容易にスケールさせる事が可能です。その他のShardingの機能についてや、ここで取り上げなかったSharding環境で起こる様々な問題については過去の資料をご覧下さい。

1. Primary Shardを活用する 〜Primary Shardの存在を知ろう〜

MongoDBには"Primary Shard"という概念があります。これは運用開始直後の数個しかないChunkが保存される初期Shardであり、MapReduceのOutputCollectionが作成される場所であり、その他様々なケースで優先的に使われるShardです。このPrimary Shardの存在を知り、適切な設定を行っておく事は以下の意味で非常に有用です。

・メモリやCPUの優秀なマシンを割り降ることで初期段階のデータの局所集中に耐える
・Primary ShardをSSD上に配置し、Map Reduceなどの出力を高速に行う

この2つの説明に入る前に、まずはShardingの構成について簡単にお話しておきます。

Sharding の初期構成

※ここでは簡単なShardingを用意する時間が無かったので、現在使用しているSharding環境を流用させて頂きます。

# 25のShard ( With Replica Sets ) を追加(24HDD + 1SSD)
# HDD
> db.adminCommand( { addshard: "shard00/delta1:2400,delta2:2400", name: "shard00" } )
> db.adminCommand( { addshard: "shard01/delta1:2401,delta2:2401", name: "shard01" } )
...
> db.adminCommand( { addshard: "shard22/delta6:2422,delta5:2422", name: "shard22" } )
> db.adminCommand( { addshard: "shard23/delta6:2423,delta5:2423", name: "shard23" } )
# SSD
> db.adminCommand( { addshard: "delta5:2424",name: "shard24" } )  
# Enable Sharding
> db.adminCommand( { enablesharding : "mydb" } ) 
> db.adminCommand( { shardcollection : "mydb.log", key : { hour : 1 } } )

"addshard" コマンドによってshardを構成していきます。"name"キーはそのshardの名前を決定し、"addshard"キーでshardの所在をhost名とport名により指定します。上記の例はReplica Setsを各Shardで構成しているためにReplica Setsメンバー全員の所在とSet名を指定しています。例えば"shard00"は"shard00"というSet名を持ったReplica Setsメンバー、delta1:2400とdelta2:2400(と後1つのarbiter server)を指定しています。またこのような24台のHDD上のReplica Setsを持つShardを構成するのに加えて、"shard0024"というReplicationを行っていない、かつSSD上に配置されたShardを登録しています。これは他のShardとは別の目的で用意しています。そして"enablesharding"コマンドによってデータベース"mydb"をSharding対象に加え、"mydb.log"コレクションをShard Key: "hour"で分割することを指定しています。実際に挿入されるレコード例を挙げておきます(一部加工済):

{
        "_id" : ObjectId("4de3d6f573a4bc264f002619"),
        "protocol" : "HTTP/1.1",
        "hour" : 23,
        "referer" : "-",
        "ipaddr" : "000.00.000.000",
        "responseBodySize" : 6052,
        "userId" : "777777",
        "timeMilliSec" : 83396000,
        "options" : {

        },
        "userAgent" : "DoCoMo/2.0 F706i(c100;TB;W24H17)",
        "params" : {
                "count" : "10",
                "opensocial_viewer_id" : "777777",
                "material" : "1",
                "item" : "61",
                "opensocial_owner_id" : "777777",
                "opensocial_app_id" : "10"
        },
        "time" : "23:09:56",
        "date" : 20110530,
        "path" : "/hoge/foo/bar",
        "method" : "GET",
        "statusCode" : "200"
}
メモリやCPUの優秀なマシンを割り降ることで初期段階のデータの局所集中に耐える

Sharding開始直後は1つのChunkからスタートします。これは1つのShardにデータが集中することを意味しています。特に初動から多数のアクセスが見込まれる場合、またはShard間でサーバースペックが異なる場合は、高性能マシンをPrimary Shardに割り当て、高い負荷に備える準備が必要です。Primary Shardの設定は上記のコマンドに続いて以下の様に行います。

> db.adminCommand( { moveprimary :     "mydb", to : "shard24" } );
   { "_id" : "mydb", "partitioned" : true, "primary" : "shard24" }

これによりPrimary Shardを意図的に"shard0024"に移動することができます。この設定を行わない場合は、自動でどこかのShardがPrimaryに設定されています。Primary Shardの設定はデータの挿入後にも行う事ができますが、エラーが出る可能性があること、事前に行うことに多くの意義を持つことに注意しておいて下さい。

Primary ShardはSSD上に配置し、Map Reduceなどの出力を高速に行う

今回の例の場合では、Primary Shard: shard24をこちらの目的で活用しています。shard24は解析専用サーバーのSSD上に配置されています。また、意図的な設定によって、このshard上には普通のデータは入って来ないようにしており、Map Reduceを使用した際の出力先のコレクションとしてのみ活用することにしています。複雑な集計作業においては、複数のMap Reduceの結果を用いてさらにMap Reduceを行うような場合がありますので、今回はその目的で高速な出力と、SSD上で多段MapReduceを高速に行うためのShardとして活用しています。例えば以下の様なUUを求めるMapReduceの結果はshard24上の"mr"コレクションに格納されます:

> m = function() { emit(this.userId, 1); }
> r = function(k,vals) { return 1; }
> res = db.log.mapReduce(m, r, { out : {replace:'mr'} });
{        "result" : "mr",
        "shardCounts" : {
                "shard00/delta1:2400,delta2:2400" : {
                        "input" : 11349174, 
                         "emit" : 11349174,
                         "output" : 167265 },
                "shard01/delta1:2401,delta2:2401" : {
                        "input" : 7582207,
                        "emit" : 7582207,
                        "output" : 139164 },
                ...
                "shard23/delta5:2423,delta6:2423" : {
                        "input" : 7508581,
                        "emit" : 7508581,
                        "output" : 144319
                }
        },
        "counts" : {
                "emit" : NumberLong(174721895),
                "input" : NumberLong(174721895),
                "output" : NumberLong(3317670)
        },
        "ok" : 1,
        "timeMillis" : 681401,
        "timing" : {
                "shards" : 653462,
                "final" : 27938
        },
}

以上の結果の統計情報を見ても"shard24"はデータが存在せずに、集計対象から外れていることがわかります。それではmongosからではなく、shard24上のmongodに直接アクセスして、そこに"mr"コレクションがあるか覗いてみます:

$ mongo delta5:2424/mydb
  MongoDB shell version: 1.8.1
  connecting to: delta5:2424/mydb
> show collections
log
mr                     # mr コレクションが確かにshard24に存在
system.indexes
> db.mr.count()
500000
> db.mr.find() 
{ "_id" : "u100001", "value" : 1 }
{ "_id" : "u100002", "value" : 1 }
{ "_id" : "u100003", "value" : 1 }
has more

大量の出力がある場合や保存されたコレクションからさらに何らかの集計を行う場合には、Primary Shardを利用して特定の用途だけのShardを作るような方法は有効な手段となることでしょう。

2. Chunkの事前分割 〜運用開始からデータを分散させる〜

Primary Shardの話の中で、「初期段階では1つのShardにデータが集中する」問題を挙げましたが、これに対してはより効果的な対策を施す事が出来ます。設定したShardKeyに対してデータを挿入する前にChunkの分割を行ってしまおうというものです。これは事前にShardKeyの分布が予測できている場合には始めから均質なデータの分散を可能にすることができます。今回の例では(そこまで均質性を求める事はできませんが、)入ってくるデータ(ログレコード)が記録された時間(0〜23)をshardKeyにすることで、初動からデータを分散させるように意図しています。この作業はデータが挿入される前に行わなければならないことに注意して下さい。

db.adminCommand( { split : "mydb.log" , middle : { hour:   0 } )
db.adminCommand( { split : "mydb.log" , middle : { hour:   1 } )
...
db.adminCommand( { split : "mydb.log" , middle : { hour:   23 } )

このsplitコマンドを実行する事によって、"middle"で定めたShardKeyの値を中心にしてChunkの分割を行ってくれます。今回はこれを0,..23のmiddle値でsplitコマンドを実行する事によって異なる時間のログレコードは異なるchunkに入るように設定しています。このコマンドを実行した結果を確認してみます:

> db.printShardingStatus()
        { "_id" : "mydb", "partitioned" : true, "primary" : "shard24" }
                mydb.log chunks:
                                shard24       25
                        { "hour" : { $minKey : 1 } } -->> { "hour" : 0 } on : shard0025 { "t" : 1000, "i" : 1 }
                        { "hour" : 0 } -->> { "hour" : 1 } on : shard24 { "t" : 1000, "i" : 3 }
                        { "hour" : 1 } -->> { "hour" : 2 } on : shard24 { "t" : 1000, "i" : 5 }
                        { "hour" : 2 } -->> { "hour" : 3 } on : shard24 { "t" : 1000, "i" : 7 }
                        { "hour" : 3 } -->> { "hour" : 4 } on : shard24 { "t" : 1000, "i" : 9 }
                        ...
                        { "hour" : 22 } -->> { "hour" : 23 } on : shard24 { "t" : 1000, "i" : 47 }
                        { "hour" : 23 } -->> { "hour" : { $maxKey : 1 } } on : shard24 { "t" : 1000, "i" : 48 }

この結果より、Chunkのレンジが (-∞, 0 ), [0, 1 ), [1, 2 ),..., [22, 23 ), [23, ∞ ) に分割されていることが確認できます。ここまででChunkの事前分割は実現しましたが、また異なるshardに分散させるところまでは実現していません。全ての行で"on : shard24"とあるように、全てのChunkは自動Balancing機能が働くまで、依然としてshard24上に集中して存在してしまうことになります。

3. Chunkの手動マイグレーション 〜安全に効率的にChunkを移動する〜

そこで次にChunkを意図的に移動(マイグレーション)させることにします。これを手動で行う事によって初動からデータを分散させることができ、かつChunkを置きたい場所に自由に移動させておくことで、データの意図的なルーティングも可能になります。

db.adminCommand({moveChunk : "mydb.log", find : {hour :  0}, to : "shard00"})
db.adminCommand({moveChunk : "mydb.log", find : {hour :  1}, to : "shard01"})
db.adminCommand({moveChunk : "mydb.log", find : {hour :  2}, to : "shard02"})
...
db.adminCommand({moveChunk : "mydb.log", find : {hour :  22}, to : "shard22"})
db.adminCommand({moveChunk : "mydb.log", find : {hour :  23}, to : "shard23"})

この"moveChunk"コマンドは、findキーで指定したShardKey条件に該当するドキュメントを含むChunkをtoキーで指定したShardに移動させます。今回の例では、Chunk [ 0, 1 ) (つまりhour=0のデータ)をshard00に移動させています。この結果を見てみますと今度は、各Chunkが異なるShardに存在している事が確認できます。

> db.printShardingStatus()
        { "_id" : "mydb", "partitioned" : true, "primary" : "shard0025" }
                mydb.log chunks:
                                shard24       1
                                shard00       1
                                shard01       1
                                shard02       1
                                ...
                                shard22       1
                                shard23       1
                        { "hour" : { $minKey : 1 } } -->> { "hour" : 0 } on : shard24 { "t" : 26000, "i" : 1 }
                        { "hour" : 0 } -->> { "hour" : 1 } on : shard00 { "t" : 3000, "i" : 0 }
                        { "hour" : 1 } -->> { "hour" : 2 } on : shard01 { "t" : 4000, "i" : 0 }
                        { "hour" : 2 } -->> { "hour" : 3 } on : shard02 { "t" : 5000, "i" : 0 }
                        ...
                        { "hour" : 22 } -->> { "hour" : 23 } on : shard22 { "t" : 25000, "i" : 0 }
                        { "hour" : 23 } -->> { "hour" : { $maxKey : 1 } } on : shard23 { "t" : 26000, "i" : 0 }

この設定の元でデータの挿入を行った場合、データのhour値によってそれぞれのShardに始めから分散させて挿入することができます。実際に挿入して確かめてみましょう:

$ mongo delta1:2600/mydb #connect to mongos
> for( var n=0; n<24; n++ ){
...   db.log.insert({"hour": n})
... }

$ mongo delta1:2400/mydb
> db.log.distinct("hour")
[ 0 ]

$ mongo delta1:2401/mydb
> db.log.distinct("hour")
[ 1 ]
...
$ mongo delta1:2423/mydb
> db.log.distinct("hour")
[ 23 ]

意図したとおりの結果が得られている事が確認できました。この設定は非常に効果的ですので、一度検討してみて下さい。

4. Chunkの手動分割&マイグレーション 〜動かないChunkを移動する〜

すでにSharding環境を構築して運用されている場合でも、2.と3.でお話したことは実践が可能です。つまりあるShard内の、既にデータが詰まっているChunkを意図的に分割して軽くし、特定のShardへ移動させることが可能です。これらの作業は次の意味で非常に有用です:

・Shard間の偏りを自力で補正する
・類似のChunkを同じShardへ集合させることでデータローカリティを実現する

shard間の偏りを自力で補正する

MongoDBの自動Balancing機能ではShard間の偏りがどうしても生じてきてしまいます。実際のChunkのマイグレーションには、そのChunkサイズと同じ以上のメモリ領域と、configサーバーへの大きな負担をかけるというリスクによって、必ずしも成功するとは限りません。メモリが足りなければChunkの移動が必要な状況でも決して起こりません。これらの状況は避けることができませんので、手動でのChunkの移動を定期的に行うようにします。また、Chunkサイズが大きかったりメモリが足りなくて移動に失敗する場合は、Chunkの手動分割によってより細かいサイズまで分割してから移動させることにします。まずはShardごとのデータサイズを確認して、Chunkの手動分割・移動を行ってみます:

> printShardingSizes()
        ...
        { "_id" : "mydb", "partitioned" : true, "primary" : "shard24" }
                mydb.log chunks:
                        { "hour" : { $minKey : 1 } } -->> { "hour" : 0 } on : shard24 { "size" : 0, "numObjects" : 0 }
                        { "hour" : 0 } -->> { "hour" : 1 } on : shard00 { "estimate" : false, "size" : 89590388, "numObjects" : 198518  }
                        { "hour" : 1 } -->> { "hour" : 2 } on : shard01 { "estimate" : false, "size" : 75265412, "numObjects" : 165859 }
                        { "hour" : 2 } -->> { "hour" : 3 } on : shard02 { "estimate" : false, "size" : 163306000, "numObjects" : 360413 }
                        ...
                        { "hour" : 22 } -->> { "hour" : 23 } on : shard22 {"estimate" : false, "size" : 254483548, "numObjects" : 576126 }
                        { "hour" : 23 } -->> { "hour" : { $maxKey : 1 } } on : shard23 { "estimate" : false, "size" : 247971184, "numObjects" : 561015 }

このコマンドによって各Shardの各データベース/コレクションのデータサイズ(の推定値)とオブジェクト数を確認することができます。他にも各Shard情報を確認するコマンドがあります。実際の分割・移動は前回までにお話したことと同じですが、分割に関しては、既にデータが存在しているので、対象のChunkをfindキーで特定して等分割を行う事にします(前回はmiddleキーだったことに注意して下さい)。今までの例はhourという24種類しかないキーをShardKeyに設定してしまっているため、分割などをしにくい状態になってしまっています。ここでは00:00:00を0としたtimeMilliSecをShardKeyとしている前提で分割することにします。

> db.adminCommand( { split : "mydb.log" , find : { timeMilliSec : 60000 } } ) 

このコマンドによってtimeMilliSecキー: 60000 を含むChunkが等分割になるようなキーで分割が行われます。Chunkの移動の方は前回お話したコマンドと全く同じです:

db.adminCommand({moveChunk : "mydb.log", find : {timeMillSec :  30000}, to : "shard10"})

これによって分割した片方の、timeMillSec=30000を含む方のChunkをsahrd10に移動させる事ができました。

類似のchunkを同じShardへ集合させることでデータローカリティを実現する

後の解析を見据えた場合のデータストレージにおいては、データの分散を意識しつつ、データローカリティ、つまり類似のデータは同じShardに存在させておいて検索や集計の効率性を上げることも意識しておく必要があります。完全にデータローカリティに特化したストレージを行うには、類似のChunkを特定のShardに集合させるようにします。この場合はShardの偏りはあまり気にしないでしょう。

5. Shardの状態を監視する 〜有用な2つのコマンド〜

さて、最後になりましたが、ここまでに登場したSharding環境で頻繁に使うことになるコマンドを紹介しておきます。

printShardingStatus(undefined,true)

機能的には db.printShardingStatus() と同じなのですが、 db.printShardingStatus() で肝心のChunkの分割情報を表示してくれない状況が発生してきます:

> db.printShardingStatus()
                                ...
                                shard0022       1
                                shard0023       1
                        too many chunksn to print, use verbose if you want to force print #肝心な情報が見れない…orz

その場合に printShardingStatus(undefined,true) を使います:

> printShardingStatus(undefined,true)
                                ...
                                shard0022       1
                                shard0023       1
                        { "hour" : { $minKey : 1 } } -->> { "hour" : 0 } on : shard0025 { "t" : 26000, "i" : 1 }
                        { "hour" : 0 } -->> { "hour" : 1 } on : shard0000 { "t" : 3000, "i" : 0 }
                        { "hour" : 1 } -->> { "hour" : 2 } on : shard0001 { "t" : 4000, "i" : 0 } 
                        ...

また、Chunkの情報はconfigデータベースからも確認することができます:

>use config
> db.chunks.find().forEach(printjson)
...
{
        "_id" : "mydb.log-hour_22.0",
        "lastmod" : {
                "t" : 25000,
                "i" : 0
        },
        "ns" : "mydb.log",
        "min" : {
                "hour" : 22
        },
        "max" : {
                "hour" : 23
        },
        "shard" : "shard22"
}
{
        "_id" : "mydb.log-hour_23.0",
        "lastmod" : {
                "t" : 26000,
                "i" : 0
        },
        "ns" : "mydb.log",
        "min" : {
                "hour" : 23
        },
        "max" : {
                "hour" : { $maxKey : 1 }
        },
        "shard" : "shard23"
}

configデータベースにはその他Shardingに必要なメタ情報を管理していますのでここから様々な情報を取得することが可能です。ただし、このメタ情報をupdateしたりremoveしたりすると大変な事になるので注意して下さい。configサーバーで手を加えて良いのは自動Balancing機能をオフにする場合だけです:

use config
db.settings.update( { _id: "balancer" }, { $set : { stopped: true } } , true )
printShardingSizes()

こちらも既出ですが、各Shardのデータサイズとオブジェクト数をコレクションごとに表示してくれる便利なコマンドです。ただし、このコマンドがデータ数が多くなると返ってくるのが非常に遅くなってしまいます。その場合には他のコマンドで確認するようにしてください。

> printShardingSizes()
Wed Jun  1 00:30:46 updated set (shard00) to: shard00/delta1:2400,delta2:2400
Wed Jun  1 00:30:46 [ReplicaSetMonitorWatcher] starting
...
--- Sharding Status ---
  sharding version: { "_id" : 1, "version" : 3 }
  shards:
      { "_id" : "shard0000", "host" : "shard00/delta1:2400,delta2:2400" }
      { "_id" : "shard0001", "host" : "shard01/delta1:2401,delta2:2401" }
      { "_id" : "shard0002", "host" : "shard02/delta1:2402,delta2:2402" }
      ...
               playshop-gree-access.log chunks:
                        { "hour" : { $minKey : 1 }, "date" : { $minKey : 1 } } -->> { "hour" : 0, "date" : { "$minKey" : 1 } } on : shard0012 { "estimate" : false, "size" : 1854702464, "numObjects" : 3765808 }
                        { "hour" : 0, "date" : { "$minKey" : 1 } } -->> { "hour" : 1, "date" : { "$minKey" : 1 } } on : shard0013 { "estimate" : false, "size" : 1384094100, "numObjects" : 2806727 }
                        { "hour" : 1, "date" : { "$minKey" : 1 } } -->> { "hour" : 2, "date" : { "$minKey" : 1 } } on : shard0014 { "estimate" : false, "size" : 882130544, "numObjects" : 1780488 }
                        { "hour" : 2, "date" : { "$minKey" : 1 } } -->> { "hour" : 3, "date" : { "$minKey" : 1 } } on : shard0015 { "estimate" : false, "size" : 539366856, "numObjects" : 1089989 }
                        { "hour" : 3, "date" : { "$minKey" : 1 } } -->> { "hour" : 4, "date" : { "$minKey" : 1 } } on : shard0016 { "estimate" : false, "size" : 419811240, "numObjects" : 848770 }
                        { "hour" : 4, "date" : { "$minKey" : 1 } } -->> { "hour" : 5, "date" : { "$minKey" : 1 } } on : shard0017 { "estimate" : false, "size" : 726579776, "numObjects" : 1471823 }
                        { "hour" : 5, "date" : { "$minKey" : 1 } } -->> { "hour" : 6, "date" : { "$minKey" : 1 } } on : shard0018 { "estimate" : false, "size" : 1152177872, "numObjects" : 2347418 }
                        { "hour" : 6, "date" : { "$minKey" : 1 } } -->> { "hour" : 7, "date" : { "$minKey" : 1 } } on : shard0019 { "estimate" : false, "size" : 2630525884, "numObjects" : 5485197 }
                        { "hour" : 7, "date" : { "$minKey" : 1 } } -->> { "hour" : 8, "date" : { "$minKey" : 1 } } on : shard0020 { "estimate" : false, "size" : 5387886924, "numObjects" : 11132989 }
                        { "hour" : 8, "date" : { "$minKey" : 1 } } -->> { "hour" : 9, "date" : { "$minKey" : 1 } } on : shard0021 { "estimate" : false, "size" : 2379915544, "numObjects" : 4840048 }
                        { "hour" : 9, "date" : { "$minKey" : 1 } } -->> { "hour" : 10, "date" : { "$minKey" : 1 } } on : shard0022 { "estimate" : false, "size" : 2308302612, "numObjects" : 4689784 }
                        { "hour" : 10, "date" : { "$minKey" : 1 } } -->> { "hour" : 11, "date" : { "$minKey" : 1 } } on : shard0023 { "estimate" : false, "size" : 2558705640, "numObjects" : 5189979 }
                        { "hour" : 11, "date" : { "$minKey" : 1 } } -->> { "hour" : 12, "date" : { "$minKey" : 1 } } on : shard0000 { "estimate" : false, "size" : 1881322140, "numObjects" : 3818884 }
                        { "hour" : 12, "date" : { "$minKey" : 1 } } -->> { "hour" : 13, "date" : { "$minKey" : 1 } } on : shard0001 { "estimate" : false, "size" : 2264673264, "numObjects" : 4605082 }
                        { "hour" : 13, "date" : { "$minKey" : 1 } } -->> { "hour" : 14, "date" : { "$minKey" : 1 } } on : shard0002 { "estimate" : false, "size" : 2303920996, "numObjects" : 4686648 }
                        { "hour" : 14, "date" : { "$minKey" : 1 } } -->> { "hour" : 15, "date" : { "$minKey" : 1 } } on : shard0003 { "estimate" : false, "size" : 3298773252, "numObjects" : 6840413 }
                        { "hour" : 15, "date" : { "$minKey" : 1 } } -->> { "hour" : 16, "date" : { "$minKey" : 1 } } on : shard0004 { "estimate" : false, "size" : 3157248456, "numObjects" : 6542235 }
                        { "hour" : 16, "date" : { "$minKey" : 1 } } -->> { "hour" : 17, "date" : { "$minKey" : 1 } } on : shard0005 { "estimate" : false, "size" : 2521432232, "numObjects" : 5130008 }
                        { "hour" : 17, "date" : { "$minKey" : 1 } } -->> { "hour" : 18, "date" : { "$minKey" : 1 } } on : shard0006 { "estimate" : false, "size" : 2551743308, "numObjects" : 5197201 }
                        { "hour" : 18, "date" : { "$minKey" : 1 } } -->> { "hour" : 19, "date" : { "$minKey" : 1 } } on : shard0007 { "estimate" : false, "size" : 1840064336, "numObjects" : 3695385 }
                        { "hour" : 19, "date" : { "$minKey" : 1 } } -->> { "hour" : 20, "date" : { "$minKey" : 1 } } on : shard0008 { "estimate" : false, "size" : 2250083536, "numObjects" : 4526531 }
                        { "hour" : 20, "date" : { "$minKey" : 1 } } -->> { "hour" : 21, "date" : { "$minKey" : 1 } } on : shard0009 { "estimate" : false, "size" : 2556930488, "numObjects" : 5139566 }
                        { "hour" : 21, "date" : { "$minKey" : 1 } } -->> { "hour" : 22, "date" : { "$minKey" : 1 } } on : shard0010 { "estimate" : false, "size" : 2567556160, "numObjects" : 5295961 }
                        { "hour" : 22, "date" : { "$minKey" : 1 } } -->> { "hour" : 23, "date" : { "$minKey" : 1 } } on : shard0011 { "estimate" : false, "size" : 2323384656, "numObjects" : 4784284 }
                        { "hour" : 23, "date" : { "$minKey" : 1 } } -->> { "hour" : { $maxKey : 1 }, "date" : { $maxKey : 1 } } on : shard0023 { "estimate" : false, "size" : 0, "numObjects" : 0 }
                        ...

まとめ

いかがでしたでしょうか?今回は内容がマニアックすぎたので何を言っているのかわからなかった方も多いと思います。実際にSharding環境を構築されて、同じような問題や必要に迫られたときに、もう一度この記事を思い返して頂ければ幸いです。

弊社の新解析基盤も今日お話した設定を反映しながら、26Shard+Replica Sets、データサイズ18TB×2 の規模で運用を開始しています。ここに述べたことだけでなく、自動Shardingや自動Balancingを行わないマニュアルのChunk分割ルールやShardの世代管理、SlaveからもMap Reduceを同時に行ったりHadoopやRedisと連携する集計方法など、色々やっています。またその辺の話も安定して運用できるようになればお話できればと思っています。

今後ともMongoDBを宜しくお願いします。

「ニフティクラウドでMongoDBは使えるか?」を読んで、僕なりの考察を書いてみた

@です。2011年5月19日付けのニフティクラウドさんのブログエントリーが「ニフティクラウドでMongoDBは使えるか?」と称してニフティクラウド上でMongoDBのパフォーマンス比較を行うという、非常に興味深いものでしたので、このエントリーを読んで僕なりの考察をまとめてみました。完全な検証環境がわからないので一部推測を含み、誤解などもあるかもしれませんが、それらのフィードバックも期待して、ブログにすることにしました。ですのでまずはこのエントリーを読んで下さいね。

ニフティクラウドでMongoDBは使えるか? : NIFTY Cloud ユーザーブログ

それではエントリーの中盤、「Benchmark」以降を部分的に引用しながら考察を述べていきたいと思います。

shardKeyを考慮に入れる事の重要性

■ Benchmark 下記のような条件およびプロセスでベンチマークを行い、プロセス完了までの時間を計測しました。
データ twitterのtweetを1.4GB (delete含めて707,259tweet)
条件
・shardingの有無(1台=無 vs. 3台)
・stripingの有無(有 vs. 無)
ニフティクラウドスペック(mini vs large16)

注:stripingについてはニフティクラウドにMySQLは載せられるのか?パフォーマンス大検証!をご覧ください

まず提示された条件については、以下の情報が不足しているように感じました。

shardingを構成する際に、shardKeyとして何を設定したか?

MongoDB のshardingでは、1番始めに行う「shardKeyの設定」が非常に重要です。このshardKeyの設定次第では、shard間の偏りやデータの非効率な取得が生じてしまう原因になります。また、find()やinsert()などの処理に関してもそのshardKeyを条件に含めて実行するかどうかで効率性が大きく異なってきます。shard環境の有無でfind()やinsert()の性能比較を行う場合には、shardKeyを条件に含めた場合と含めない場合の両方でさらに比較する必要があります。このshardKeyが不明ですとこの切り分けができません。

ここでは前提としてstatus_idをshardKeyにすることにします

今回のインプットはTwitterデータということで、最も一般的なstatus_idをshardKeyにした場合で考えることにします。実はstatus_idはMongoDBのshardKeyを考える上で非常に良い例となってくれます(最後のスライドの2枚目をご覧下さい)。
ここでのstatus_idは時間増加する性質をもったものと仮定しています。つまり時間が進みにつれてstatus_idが単調増加する性質です。一時期Cassandraを採用するためにこのstatus_idが時間増加にならないようにするといった話題が出ましたが、Cassandraを採用しなかった今、結局どうなったのでしょうか…(^_^;)

find() の条件にshardKeyを含むか否か、キーにインデックスを作成しているか否かの切り分けは比較としてとても重要なファクターです

プロセス
...
find
 コレクションからデータを取得するコマンドです。MongoDBの場合は、ドキュメントを縦横無尽に条件付けてフィルタリングができるのでこの性能も無視できない重要な性能です。

・1条件 find({source:'web'})
・ネスト条件 find({'user.lang':'ja'})
・否定 find({'geo':{$ne:null}})
・サイズ find({'entities.hashtags:{$size:2}})
・比較 find({'retweet_cnt':{$gt:1}})

MongoDBのパフォーマンス比較においては、これらのfindの条件が

  1. shardKey を含むかどうか
  2. インデックスを作成しているかどうか

を明示的に考慮に入れた方が良いと感じます。

検索条件にshardKeyが含まれている場合とそうでない場合

まず前者について考えていきます。sharding環境のfind()においては、その条件にshardKeyを含ませるか否かでデータ取得の方法が異なってきます。shardKeyが含まれている場合、mongosは該当のshardKeyを持つshardがどこにあるのかを知っていますので、そのshardにだけクエリを実行することができます。

逆に、shardKeyを含まない場合にはmongosは全てのshardに対してクエリを実行して取得したデータをマージしてクライアントに返してくれます。

つまりsharding環境において効率の良いクエリが実行できるのは、find()の条件にshardKeyが含まれていた場合です。ですので性能比較の際にはshardKeyを含ませるかどうかの違いも検証することが重要です。

インデックスを作成しているか、smallインスタンスにインデックスを作り過ぎていないか

後者に関しては、もちろんインデックスを作成した方が速いのは明らかですが、インデックスサイズが大きくなりがちなMongoDBに関しては、元のメモリ量が少ないsmallインスタンスに大量のインデックスを作成した場合に、逆にパフォーマンスが落ちてしまう可能性もあるのではないかと考えたからです。

(余談)mapreduce はcount()やdistinct()との比較もあれば

mapreduce
 MongoDBを十二分に活用するのであれば、避けられないコマンドです。簡単な解析に利用することが出来ます(簡単、と書きましたがかなり使えます)。


・単純なグループ集計(source集計)
・単純なグループ集計(5分ごとのtweet数集計)
・2条件のグループ集計(1時間当たり・1人当たりのtweet数集計)
・少し複雑な集計(1時間当たりのhashtag集計)
・少し複雑な集計(共起するhashtag集計)

count() や distinct() は高速

余談ですが、sharding環境でのMongoDBにおけるmapreduceの比較については、単純なmapreduceを記述する場合にはcount()やdistinct()といった組み込み関数との比較を行うのも面白いと思います。例えばtweet数集計や、sourceの集計、あるいはUUの計算などはsharding環境でもcount()やdinstinct()コマンドが全shardを対象に行ってくれます(正しい値を返さない場合もありますが…)。そしてこれらは実はmapreduceよりも高速に結果を返してくれます。例えば24shard環境で130GB、1億2千万レコードのuseridが特定できているアクセスログに対してUU計算を行った場合、mapreduceで記述した場合が350secに対してdistinct()を用いた場合には200secでした。これは後者がインデックスを利用して効率良く集計くれているからだと思います。

結果に対する考察

それではエントリーに記述されたベンチマーク条件での比較結果について、今まで述べた性質を考慮しながら考察してみたいと思います。



























スペックminilarge16
sharding
striping
mongoimport549.201 520.438 630.101 690.042 259.671 104.757 263.050 193.803
find38.378 42.293 13.618 14.998 1.044 1.030 1.124 1.132
mapreduce226.64 229.46 107.39 106.85 52.53 52.10 26.08 26.08

全体

全体的にざっくりと見ると、当然といえば当然ですが、VMのスペックがimport/find/mapreduceに大きなパフォーマンスの差に影響を及ぼしています。
 また、IO性能に関わるstripingに関しては、import、特にlarge16以外ではほとんど意味のある効果を発揮していませんでした。これはminiの場合はIOよりもmemory/cpuがボトルネックになっているためだと考えられます。

 データサイズが1.4GBであるため、mini(memory=512MB)においてはfindやmapreduceにおいてIO性能の及ぼす影響が強いと予想していましたが、ほとんど見られませんでした。これは今後追求していければと思っています。

findに関しては検索に使用したキーにインデックスを使用していたかがポイントになってくるような気がしました。mapreduceに関しては基本的にドキュメント全件走査しますが、findの場合はインデックスが使用できますので作成しているキーが条件に指定された場合には全件走査をする必要がありません。インデックスを作成した場合とそうで無い場合でもボトルネックはmemory/cpuであったのかも試されると良かったのかもしれません。(恐らく同じボトルネックであったと思いますが。)
また、インデックスを作成した場合、MongoDBはインデックスサイズが大きくなりやすい傾向があります。今回の場合、miniインスタンスでさらにインデックス作成の有無でさらに比較をしたとすると、findにインデックスを作成することの高速化する一方で、大量のメモリ消費の影響がどのくらいありそうなのかが気になりました。

mongos

 上記と重複しますが、スペックにおける影響が非常に強く、また、次に、large16のときにstripingの影響が出ていました。このimport処理では書き込み性能に影響が出るため、shardingの影響を予測していましたが、shardingによる性能の改善は見られないばかりか、1台で行った場合よりも劣化するという結果でした。

sharding環境の有無でmongoimportに差が出なかったのは恐らくshardKeyの問題ではないでしょうか。前述しましたが、shardingは事前に設定したshardKeyに従ってデータを各shardに分割しています。例えばこのshardKeyの値が "_id" を始めとした時間軸に対して増加するキーであった場合には、連続して挿入されるデータは分散されずに同じchunk、つまり同じshardに入ってしまいます。この場合にはshardingしていながら、実際にはシングルサーバーへ書き込みを行っているのと同じ、さらにはmongosサーバーを介することによってシングルよりも無駄なステップを消費しています。今回の結果の原因は、status_idをshardKeyとして使用しているような場合にはこれによるものと考えられそうです。

 この結果は、mongosまたはconfigサーバがボトルネックとなりうるという要因が予測されます。今後、例えばmongosを分割し、それぞれに対して同時にimportを行い、全体としてのパフォーマンスの改善が見られるかを検討する必要があるかと思います。

mongosもconfigサーバーもそれほど負荷のかかる役割を担う処理を行いませんし、複数用意するのは主にレプリケーション目的であって、スケール目的ではありません。ですのでこれらはボトルネックにはなりにくいはずです。
ただ、一度に大量なインサートを行う場合には、mongosを複数用意してデータを分割して、各mongosに並列insertすることによって多少の負荷を軽減することができそうですし、書き込みにゆとりがある場合には並列insertの方が高速に処理してくれそうです。
また、クライアントとのデータ入出力の窓口を増やすことで1つのmongosがダウンしてしまった際にも安定稼働させる上で有用だと思います。

find

IO性能に関わるstripingはほぼ、findにおいては影響を見せませんでした。またshardingによるfindの性能は、miniでは、およそ2倍?3倍程度の改善を示していましたが、large16ではほとんど効果が見られませんでした。同時に、stripingも影響があまり見られませんでした。

 今回の程度のデータサイズであれば、cpu/memory性能が十分に高ければ分散させてIO性能を稼ぐことにあまり意義が無いと考えられます。

find処理においては、検索条件にshardKeyが含まれていたかどうかが重要になります。もしshardKeyが検索条件に含まれていた場合には、mongosはどのshardに該当するデータが存在するかを把握していますので必要なshardに対してのみ検索を行うので効率良く高速に実行できます。しかし、shardKeyが検索条件に含まれていない場合は、全てのshardにfind()を実行し、結果をマージするということを行いますので効率があまりよくありません。
今回差が出なかったのも検索条件にshardKeyが含まれていなかったので全部のshardを確認していったからでは無いでしょうか?
(もちろん、全てのクエリがいつもshardKeyを含んでいるとは限りません、MongoDBはメモリをうまく利用することによって、この場合でもそれなりに速く実行してくれます。)

あるいはsharding環境においては、バックグラウンドでchunkのマイグレーションが行われているかどうかによってパフォーマンスが変わってきます。chunkのマイグレーション中はデータの移動がバックグラウンドで行われており、その際にchunkは丸々メモリにコピーされています。マイグレーションが行われている場合にはこのメモリの消費とサーバーへの負担のせいでパフォーマンスが一時的に下がってしまう可能性があります。
ただ、今回はその状況は起きていないと考えられます。デフォルトのchunkサイズは200MBであり、balancing機能によってマイグレーションが発動するタイミングは、現在アクセスされているshardのchunk数と、最もchunk数の少ないshardとの数の差違で10以上になったときです。すなわち2GB以上の差違が生じた時に発動します。今回のデータは1.4GB、MongoDBに格納した場合に2.5GB程度になると考えられますのでそもそもマイグレーションが発生していたかどうか、またそれが起きたとしても少ないマイグレーション回数でbalancingできるので、検証実行時には既にそのマイグレーションが完了しており、その状況に出くわさなかったことが考えられます。

それでは、なぜminiではshard環境の有無で結果に差が出たかというと、はやりシングルサーバーの場合はmemory/cpuがボトルネックになっていて、それが3つのmemory/cpuにデータを分散できたことで解消されたので、shardKeyが条件指定されておらず、全部のshardを見に行くという処理になっても高速に実行できたのではないでしょうか?

mapreduce

 今回、一番ベンチマークを取った甲斐があるプロセスでした。stripingについては性能にほとんど影響が見られませんでしたが、shardingの効果がよく出ており、簡単とはいえ集計や計算がcpu/memory依存で行われ、mapreduceを複数台で実現することの意味が見出せたものと考えられます。

map/reduceに関してはsharding環境を活用できますので高速化が最も期待できるところです。ただし、ここでもshardKeyの設定によって、集計対象とするデータが1つのshardに偏っている場合などは、その効果があまり期待できません。
また、map/reduceの実行は基本的に1shardあたり1スレッドしか使えません。このことは例えば8coreと1coreのCPUをそれぞれ積んだサーバーで比較しても、1サーバーごとに1shardしか配置されていない場合には大きな差違が出ないということです。
MongoDBのmapreduceはshuffle機能が無いなど、Hadoop等のmapreduceに比べてやや簡素化された仕組みでありますが、この1core/shard制約は最も大きな違いであり、MongoDBのmapreduceがスケールしない、遅いといわれているゆえんであります。
これらを考慮して、データの均等分散に気を遣い、かつコア数だけshardを同居させるといった(これは非推奨です)環境で実行した場合にはより大きな差を生むことが出来るはずです。

mapreduceの特性を理解して環境を構築する重要性

今回、すべてのデータについては、分散はほとんど見られなかったこともあり、ざっくりとした考察だけを行っていますが、いかがでしたでしょうか。実際に使う上でshardingを適切に配置し「Largeシリーズ」を使っていただければ。「Largeシリーズ」を使っていただければ、性能がスケールすることも含め、比較的性能的にもよいパフォーマンスが得られたのではないかと思っております。

1つのサーバーに1shardしか組まないという安全性を重視した設計を行う場合は、例えばメモリが4MBと同じでCPUのコア数のみが異なる「large」と「small4」ならばsmall4を選択してもそれほど問題が無いと思われます。しかしその金額の差違は、月額プランにして1サーバーあたり¥48,300-¥25,410=¥22,980。仮に10台のshardとそのreplicationとしてさらに10台を使用する場合には、月額で ¥22,980 × 20台 = ¥457,800 になります。このようにMongoDBのmapreduceの特性を理解せずに高スペックマシンの方が高速に動作すると思い込んで毎月数十万を無駄する可能性があります。
ニフティクラウドのインスタンス毎の金額設定は以下を参照して下さい:
クラウド 料金 | ニフティクラウド

逆に、あくまで試験的ですが、フルコアを使いきるためにlargeの4コアサーバーに擬似的に4shard構成にすることも可能です。この場合には必要なサーバー台数が1/4にできる一方で、月額は2倍高いだけなので、結果的にsmall4に比べて1/2の金額で済む事になります。ただ、これはmapreduceのみを考慮した話であり、同サーバーでその他の処理も行われていたりする場合などで状況は変わってくるので注意して下さい。

感想

まず、ニフティさん、エントリーに対して少し批判的な内容を含む記事になってしまったことをお詫びいたします。また、誤解や間違いもあるかもしれませんので、その際は指摘して頂けると幸いです。
今回の感想ですが、サービス提供側が、今回のような比較記事を積極的に示してくれることによって、そこから僕たちは非常にたくさんの情報を読み取ることができますので、非常に良いエントリーだと思いました。是非とも今後の継続的なレポートも期待しています。
また、ニフティクラウドを代表するクラウドサービスでMongoDBをメインに使用する環境を構築する場合には、MongoDBの特性を十分に考慮した上で構築しないと、無駄に高い構成になってしまったり、きちんとMongoDBを設定・チューニングできていないことによる低パフォーマンスをスペックのせいにして高スペックプランに乗り換えてしまうといった事態が生じてしまいがちです。十分に注意して下さい。
僕はクラウド上でMongoDBを活用してサービスや解析を展開される方を心より応援しています。気軽に声を掛けて下さい。そしてニフティさん、僕にクラウド環境を貸して頂ければMongoDBの性能検証などを詳細に行うことが可能です。いかがでしょうか?

※最後の最後に、shardKeyは重要ですという話をしましたので、悪いshardKey3例と良いshardKey1例を挙げておきます。

〜うまく動かすMongoDB〜仕組みや挙動を理解する

@です。この業界で非常に強い影響力を持つ@氏が某勉強会でMongoDBについてdisられており、このままではMongoDB自身の存続が危ういと思い、急遽ブログ書きました。(冗談ですよ)

MongoDBを使っているときに出会うトラブルをうまくまとめてくださった「MongoDBあるある」的な良い資料だと思います。今日はここで書かれているトラブルの解決方法を提示したいと思います。恐らく@氏は全ての解決方法を知っていながら、同じトラブルへ悩む人のためにあえてdisったのだと思います。

MongoDB はデータベースもコレクションも存在しなければ自動作成してくれる

f:id:doryokujin:20110518222510p:image

mongoシェルを起動する場合、たいていは

$ mongo :
# mongo localhost:27017

のようにして現在稼働しているmongodのホスト名とポート名を指定してmongoシェル起動するかと思います。この時僕たちは既に「test」データベースの下にいることになります。そしてMongoDBは存在しないコレクション名に対してinsertを実行した場合はコレクションを作成してその中にドキュメントをinsertしてくれるのです。以下のコマンドを試して見てください。

$ mongo localhost:27017
 MongoDB shell version: 1.6.4
 connecting to: name1:27017/test

> show collections # 現在のデータベースに存在するコレクションを確認
 system.indexes

> db.test.insert( {test: 1} ) # 存在しないはずのtestコレクションにドキュメント {test: 1} を挿入!
> show collections  # testコレクションが作成されている
 system.indexes
 test
> db.test.find()    # testコレクション内のデータ(ドキュメント)を確認
 { "_id" : ObjectId("4dd3ca8e0901112f237b9495"), "test" : 1 }

さらにMongoDBは存在しないデータベースに対しても処理を行うことができます。

> show dbs # 現在存在するデータベースを確認
 admin
 test

> use newdatabase # 存在しないnewdatabaseデータベースに移ってみます
 switched to db newdatabase

> db.test.insert( {test: 1} ) # さらに存在しないはずのtestコレクションにドキュメント {test: 1} を挿入!
> show dbs #newdatabaseデータベースができている!
 admin
 newdatabase
 test

> show collections #testコレクションもできている!
 test
 system.indexes

> db.test.find() # そしてデータも入っている
 { "_id" : ObjectId("4dd3cc1c0901112f237b9496"), "test" : 1 }

MongoDBは非常におせっかいなDBなのでこちらの怠惰な操作に関しても自動で色々と面倒を見てくれるのです。ここでのまとめは、

・mongo : で起動した場合、testデータベースにいる。始めから移動しておきたいときは mongo :/ とする
・存在しないデータベース、コレクションに対しても自動作成してinsertなどの処理が行える。エラーや警告は基本的に返さない

注意してください。

Replication時の親切な自動リカバリ機能

MongoDBのReplicationは非常にステキな機能を備えています。この機能の完成度は(他の機能に比べて)高い方だと思います。よく自動フェイルオーバーしないという話を聞きますが、きちんと設定とフェイルオーバーの仕組みを理解しておけば問題なく動作する(はず)です。今回@氏に起こった問題を考えてみましょう。


f:id:doryokujin:20110518225829p:image
f:id:doryokujin:20110518225830p:image

MongoDBのReplica SetはSetメンバーを他のReplica Setと区別をつけるためのと各メンバーのなどで管理しています。ここでは全てのメンバーがPrimaryになる可能性がある設定にしたとします。上記の例では今Primaryであるメンバーがダウンしてしまった状況です。この場合はMongoDBの自動フェイルオーバー機能によって他のSecondaryメンバーから次期Primaryが選択され、(その間に30秒〜のダウンタイムがありますが)引き続き処理を受け付ける状態を保ち続けてくれます。
その後、ダウンしていた元Primaryサーバーを修復して、データを全部消して再起動したのが2枚目の図です。ここでのポイントは前回と同じホスト名・ポート名で再起動したことです。そうするとmongodを再起動した時点ですぐに現Primaryとの同期が始まり、できるだけ速く完全に同期がとれるように"Catch Up"してくれます。mongoを立ち上げてデータベースを確認したときには既に同期が完了していたということです。これをMongoDBの「自動リカバリ機能」と呼びます。自動フェイルオーバー時の挙動や自動リカバリについては以下の図を参考にしてください。


f:id:doryokujin:20110518231814p:image
f:id:doryokujin:20110518231815p:image
f:id:doryokujin:20110518231816p:image
f:id:doryokujin:20110518231817p:image
f:id:doryokujin:20110518231818p:image

ちなみに異なるホスト名やポート名でmongodを起動した場合はこの自動リカバリ機能は発動しません。まずは手動でReplica Setsのメンバーに新しく加える作業を行わなければなりません。ここでの注意は、あまりにもデータが大きい場合や、oplogと呼ばれるコレクションのサイズを小さく設定した場合には、完全な同期を行うことができないので注意してください。MongoDBの同期はPrimrayのoplogに保存されている過去のオペレーションリストをSecondaryメンバーが自身のoplogにコピーして実行することによって行われます。また、このoplogは容量以上のオペレーションを保存しなければならなくなった場合には古いオペレーションをどんどん削除していきます。まっさらなメンバーをReplica Setsのメンバーに加えようとした場合、もしPrimary(または他のSecondaryメンバー)に全てのオペレーションが残っていないと、完全な同期が不可能になりますのでエラーになります。この場合はmongodumpなどの他の手段をとって同期を行うしかありません。

チャンクが移動しまくる・デフォルトのチャンクが大きすぎる


f:id:doryokujin:20110518233215p:image

MongoDBにはShardingとよばれるデータを複数のサーバーに分割する機能を備えています。その機能と特徴は下の図を見てください。


f:id:doryokujin:20110518233216p:image
f:id:doryokujin:20110518233217p:image

MongoDBのShardingは2つのおせっかいな便利な自動機能をもっています。「自動Sharding」と「自動Balancing」機能です。前者はデータの振り分けルールを自動で行ってくれる機能で、始めに「Shard Key」と呼ばれる”どのキーの値によって分割ルールを定めるのか”を指定してさえおけば、後はMongoDBの方で自動的にそのキーで振り分けルールを決めてくれます。しかもデータが入ってくるごとにどんどんとそのルールを変更(細かくしていく)していってくれます。後者はShard間でのデータ偏りが大きくなった場合に、データの移動(マイグレーション)をバックグラウンドで自動で行ってくれる機能です。移動の単位はChunk単位です。この機能によってShard間でデータが均質になるようにMongoDBが頑張ってくれているのです。

自動Sharding

例えばShard Keyに"name"を指定したとします。すると始めはざっくりと「ア行はShard0」に「カ行はShard1」といった具合に振り分けルールを決定します。この「ア行に属するデータ集合」のことをChunk呼びます。各ChunkはShard Keyの値に対して他とかぶらない範囲をもっておりその範囲に属するデータはそのChunkの中に入っていきます。そしてChunkの中にデータが詰まりすぎた場合はそのChunkを等分割してChunkサイズを均等に保とうとします。先ほどの例でいうと、[あ,い,う,え,お]の範囲を持っていたChunkが[あ,い,う]と[え,お]というChunkに分割されます。@氏が触れているデフォルトのChunkサイズは200MBです。つまり200MB以上のデータがそのChunk内に入ってきた場合に分割が行われることになります。データが大量に入ってきている状態の裏で、Chunkの細胞分裂が絶えず行われているのです。

実はデフォルトのChunkサイズは200MBなのですが、Sharding開始時では64MBに下げられています。そしてある程度のデータサイズとなった場合に200MBに変更されます(変更されないという話も聞きますが…)。もちろんこのデフォルトのChunkサイズは変更を行うことができます。mongosを起動するときにオプションとして --chunkSize [MB] を設定してやれば良いのです。

mongos --port 10000 --configdb host1:10001 --chunkSize 500

ここで --chunkSizeを1[MB]に設定してやるとchunkはデフォルトよりも遙かに速いペースで分割されていきます。ただ分割されるといっても、物理的な分割が行われているわけではないことに注意してください。しかしchunkSizeを1に設定すると後述するChunkの移動が絶えず行われるような状態に陥り、様々な問題を引き起こすので注意してください。




f:id:doryokujin:20110518233218p:image

自動Balancing

ShardKeyを適切に設定しなかったり、大量のデータ挿入で振り分けルールの設定が追いつかなかった場合にはShard間でデータの偏りが生じてしまいます。これはどう頑張っても避けられない問題でもあります。しかしMongoDBはデータの偏りがある程度大きくなった時点で偏りの大きいShardから少ないShardへChunkの移動を行うことによってそれに立ち向かってくれます。しかも自動で。


f:id:doryokujin:20110518233219p:image
f:id:doryokujin:20110518233220p:image

Sharding の種々の問題

しかしこれらの機能が完璧に働いてくれることを期待してはいけません。MongoDBは多機能ですが、高性能ではありません。おせっかいですがたくさんドジをします。以下に起こりうる問題の一部をあげました。





@氏の嘆かれている「チャンクが移動しまくる」という問題は、「チャンクのデフォルトサイズがでかい」と矛盾するのですが、チャンクサイズを大きくすれば移動しにくくなります。ただし、chunkの移動時にはそのchunkをまるまるメモリに乗っけるので、そもそもメモリに移動させるだけの空き領域がなければ移動が行われないので注意してください。また、移動させること自身をやめたい(自動Balancing機能をオフにする)場合はmongosのコンソールから以下のコマンドを実行します。

> use config
> db.settings.update( { _id: "balancer" }, { $set : { stopped: true } } , true )

chunkの移動はマニュアルで行うことが出来ますので printShardingSizes() で各shardのサイズ情報を参照にして行うようにしてください。またマニュアルでchunkの分割も行えます。

http://www.mongodb.org/display/DOCS/Moving+Chunks
http://www.mongodb.org/display/DOCS/Splitting+Chunks

余談ですが、このconfigデータベースにはShardingに関する情報が入っています。そしてこのsettingコレクションにはChunkSizeに関する情報も格納されています。ここの値をアップデートすることでchunkSizeの変更も可能であるように見えますが、ここで変更してしまうと様々な問題が生じるらしいのでやめましょう。そして自動ShardingがイヤになったらマニュアルShardingを行えば良いのです。しかしこの設定と以降の管理を行うのはかなり大変です。興味のある人は僕に声をかけてください。

メモリの状況を確認する

完璧なメモリの状況を確認するのは難しいですが、どれくらいのメモリ容量があれば高速にMongoDBが動いてくれるかは以下のコマンドで確認できます。

> db.stats()
{
"collections" : 3,
"objects" : 379970142,
"avgObjSize" : 146.4554114991488,
"dataSize" : 55648683504,
"storageSize" : 61795435008,
"numExtents" : 64,
"indexes" : 1,
"indexSize" : 21354514128,
"fileSize" : 100816388096,
"ok" : 1
}

ここの"indexSize"はインデックスが占める容量になっています。上記の例だと19GBメモリ容量が必要であることがわかります。ただ、メモリがこの容量になくてもMongoDBはもちろん動作します。あくまでこれだけのサイズがあればインデックスは全てメモリ上に載せることができるという指標です。その他にも rs.status() や db.serverStatus() などの情報を参照して種々の状態を監視するようにしてください。(2011年05月19日12時追記)例えば db.sertverStatus() を利用してメモリリークが起きていないかを確認することができます。

> db.serverStatus().mem
{
"bits" : 64,
"resident" : 4074,
"virtual" : 619051,
"supported" : true,
"mapped" : 618515
}
> db.serverStatus().extra_info
{
"note" : "fields vary by platform",
"heap_usage_bytes" : 379296,
"page_faults" : 6322555
}

例えば上の方のコマンドの出力結果で "virtual" と "mapped" の差が実装メモリサイズに比べて大きくなっていればメモリリークの可能性がありますので、より詳細な情報を確認しにいかなければなりません。上記の例では問題なさそうですね。

まとめ

上記のように、MongoDBの挙動を理解して、それに対する対策を適切に行うことによって生じる多くの問題には対応することができます。まだ日本語の情報が整備できていなくて申し訳ないですが、海外ドキュメントや本やブログを細かく読めばいろいろな気づきがあります。僕に聞いてくれればいつでもお答えしますので気軽に質問してください。

といっても、仕様的に解決不可能な問題もたくさんあります。しかしそれらの問題は開発元の10genの人たちが将来的に解決してくれると信じていますし、それを補うツールを自分で作れば良いことです。全ての役割をMongoDBが担ってくれるという期待はせずに、MongoDBが得意な機能だけを使うようにして、MongoDBが苦手な部分はあえて他のツールを使うようにしてください。繰り返しますが、MongoDBは多機能ですが高性能では無いです、様々なトレードオフがあります。

最後に問題定義をして頂いた@氏、どうもありがとうございました。いつしか毎月開催しているMongoDB勉強会で発表されるのを心待ちにしています。

参考資料

文章中で記載した図は次の3つのスライドからの抜粋になります。



↑Shardingを仕組みから理解したい方、Sharding環境で起こりうる問題について知っておきたい方へ↑



↑MongoDBの持つ機能について、仕組みから詳しく理解したい方へ↑



↑Manual ShardingやSlave MapReduceを駆使して解析したい方へ↑

MongoTokyo:10gen エンジニア講演時に行われたQ & A メモ

03月01日(火)に開催されたMongoDB Conference (通称 #mongotokyo) は盛会の内に終了することができました。詳細なレポートは後日アップしていきます。今回はカンファレンスにおいて10genの方々の発表時の質問タイムに議論された内容についてのメモを公開します。
このメモは#mongotokyoに通訳スタッフとして参加していただいた @ さんからいただいたものを少し修正したものです。本当にありがとうございます。あくまでメモですので、文章としてきちんと書いていませんので、そこはご了承下さい。

Q) Complex transactionsは実現しますか?

普通は1つのドキュメントに対するtransactionだけサポートしています。
リレーショナルデーターベースのような複数のドキュメントに対するtransactionは
現在のところサポートしていません。ただし将来的には複数ドキュメントtransactionを
サポートする可能性はあります。

Q) 4MB/16MB以下のバイナリーデータはDBに入れても良いでしょうか?

DBにJPGアバターを入れたりする人はいます。
v1.8前のドキュメント当たりの最大サイズは4MBですが、v1.8後は16MBに拡張されます。
しかしまだv1.8に対応してないドライバーは依然として4MBの制限が残ります。
また、インデックスを作成していれば重いバイナリーデータであっても検索時間に影響がありません。

Q) 現在MongoDBを利用しているサービスの中で最大の運用規模はどれくらいでしょうか?

数TBと数Nodeのデータ規模での運用があります。ただし、数千台のサーバーによる
運用事例はあまりありません。MongoDBはCPUをあまり使わないので。
普通の4コアで20,000-40,000 transactions/sec、70% read, 30% write
なので1日に数億オペレーションは可能です。

Q) ロックについて。どのようなコマンドに対してMongoDBはブロックされますか?

現在グローバルロックはあります。DBは編集時にロックされます。
しかし編集における時間は早いので、10−15%の時間しかロックされていません。
MongoStatのツールはこれを見るために役に立ちます。
今はデータを読むコマンドをロックしないように、グローバルロックをなくすように
MongoDBをアップデートしてます。いつかグローバルロックをないようにしたいです。

Q) リレーショナルデーターベースの世界から来るとジョインなしの世界に慣れるのは難しい。ドキュメントIDでジョインみたいなリンクは可能ですが、リンクはプログラミング言語で手動で書かないといけません。将来にこれを自動でできるようになりますか?またlazy loadingとかも実現しないでしょうか?

そうするつもりはありません。今のドライバはそれをサポートしていません。けれどもドライバ上のマッパーがあります。例えばRubyではMongoidとMongoMapperがあり、オブジェクト指向インターフェースを備えています。

Q) Indices。いつインデックスを作れますか?リアルタイムで?production serverで?

2つのオプションがあります。DBをオフラインにして、ブロックするリアルタイムインデックス構築と、オンラインのままで、低いpriorityでのインデックス構築。後者はそれなりに時間がかかります。比較として、インデックスを作る時間はリレーショナルデーターベースと同じぐらいです。しかしNoSQLのDBは大体非常に大きいので時間が結構かかります。

「第1回 MongoDB JP & CouchDB JP 合同勉強会 in Tokyo」を開催してきました!

こんにちは、@です。本日は12月12日(日)行われました「第1回 MongoDB JP & CouchDB JP 合同勉強会 in Tokyo」の報告をします。当日は忙しい時期にも関わらずたくさんの人に参加していただきました。会場準備で色々を手間取ってしまいましたが、会場提供者のニフティさんと参加者の皆様の暖かいご協力もあって無事に勉強会を終えることができました。どうもありがとうございました!また、当日はgihyo.jpさんの協力のもと、Ustream配信も行いました。このUstream動画と共に勉強会報告の方をgihyo.jpさんの方にも書かせていただく予定ですのでぜひともそちらの方も宜しくお願いします。今回の勉強会のツイートがまとめられたTogetter:第1回 MongoDB JP & CouchDB JP 合同勉強会 in Tokyoもなかなかが臨場感があって面白いですね。
当日の発表内容はこちらになります:

  • 「MongoDB_JP 今後の活動計画」:@
  • 「MongoDBを用いたソーシャルアプリのログ解析 〜解析基盤構築からフロントUIまで、MongoDBを最大限に活用する〜」: @
  • 「Ameba PicoとMongoDB」:@
  • 「Big Couch & CouchDBからのお知らせ」:@
  • 「PostgresSQLからMongoDBへの以降 -アシストオン通販サイトでの実際-」: @
  • 「センサーデータストアとしてのCouchDB 〜そうだ、Couchに入れよう。〜」:@
  • 「MongoDBと位置情報 〜地理空間インデックスの紹介〜」:@
  • CouchDB on Androidでスタンドアローンアプリ。」:@

MongoDB_JP 今後の活動計画

僕の方からは前説としてMongoDB JPの今後の活動計画についてお話しさせて頂きました。ここでの本題は、直近の大きな目標として2011年3月に「MongoDB Conference in Japan」を開催したいという宣言です。(数百名規模を収容する)会場提供者・発表者・スポンサー・協力者を広く募っております。どうぞ宜しくお願いします。

MongoDBを用いたソーシャルアプリのログ解析 〜解析基盤構築からフロントUIまで、MongoDBを最大限に活用する〜

今回も相変わらずログ解析におけるMongoDBの活用事例をお話しさせて頂きました。3ヶ月以上MongoDBを解析データサーバーとして使ってきて改めて感じるのは、検索クエリの豊富さ、コンソールの扱いやすさ、node.jsやRESTといった機能によるデータの取り出しやすさに非常に助けられているということです。解析者に優しいデータベース、それがMongoDBです。また最近データ量が増えてHadoopとの連携がより重要になってきています。MongoDBの開発元である10genもHadoopとの連携を積極的に進めているようですので今後の動向に注目しつつ、それらの機能は積極的に検証していきたいと思います。

Ameba PicoとMongoDB

@さんからは、累計登録ユーザー数 3,000,000、MAU 560,00を誇る大人気サービスAmeba Pico(アメーバピグの海外版)のMongoDBでの運用について、その具体的な方法と注意点などについてお話していただきました。日本でのMongoDBでの運用事例は非常に稀少ですし、ましてやShardingやReplicationをいったスケーラビリティを考慮しないといけない大規模についての事例はとても貴重です。AmazonEC2での具体的なサーバー構成をお話ししていただいた後、遭遇した様々な問題点とその対象方法を具体的に述べていただきました。例えばShardingが偏ってしまう問題はfoursquareでも発生していて、それにどう対処するのかなどは非常に参考になりました。さらにサーバー負荷のモニタリング方法やJava Asynchronou Driverの開発など、ハイレベルなMongoDBの活用事例に見習うところはたくさんありました。本当にありがとうございました!

Big Couch & CouchDBからのお知らせ

@さんからは CouchDBCouchDB JPの2010年の活動を振り返えったあと、Big Couchについてお話ししていただきました。CouchDB JPの皆様は非常に精力的な活動を続けられていますので、MongoDB JPの方も負けじと頑張っていきたいですね。
CouchDBとは何か、Big Couchとは何かの紹介を面白くわかりやすく紹介していただいたあと、Big Couchの仕組みと動かし方を詳細に発表していただきました。特に実際の動かし方の手順をわかりやすく述べて頂いたあと、様々な実験検証を行い、q: シャード数・n: 複製するシャード数・w: 書き込み保証数・r: 読み込み保証数のトレードオフと、それによる運用の難しさをお話ししていただきました。素晴らしい内容でした。ありがとうございました。また、@さんはnode.jsに関して色々と活動をされているみたいなので、そちらの方にも注目・期待しております。

PostgresSQLからMongoDBへの移行 -アシストオン通販サイトでの実際-

@さんからは、構造がしっかりしていないといけないECサイトながら、様々なオーナーの要望に応えないといけないという状況の中でその双方の制約を叶えるのがMongoDB、というステキなイントダクションの後に、原宿の人気セレクトショップのECサイトアシストオン通販サイトを事例にしたMongoDBを使った環境への移行についてお話ししていただきました。元々の構成(PostgresSQL + PHP)から新しい構成(MongoDB + Lithium)への移行について、LithiumでどうMongoDBからデータを取得するのか、その時にどういった所に注意する必要があるのか、その方法と注意点が非常に具体的で非常に参考になりました。さすが@さん、非常に話し上手でわかりやすい、最後まで魅力的な発表でした。ありがとうございました。

センサーデータストアとしてのCouchDB 〜そうだ、Couchに入れよう。〜

@さんからは、CouchAppを使ってとにかくあらゆるデータをCouchDBに入れていこうという話。なんとそのデータの対象が家の中のセンサーデータという斬新な題材であったので、会場は大いに盛り上がりました。「Arduino + ethernet shield + センサー」をCouchDBでモニタリングすれば数千円でオール電化が実現しますとのこと(笑)。SDカードにCouchDBを入れて、ACアダプタサイズのサーバに挿入すれば簡単にホームサーバーのできあがり。構築なんて必要無い、Relax。CouchDBの良さを活かした面白いプレゼンでした。ありがとうございました。sensmonは商標登録した方がいいですね。

MongoDBと位置情報 〜地理空間インデックスの紹介〜

@さんからはMongoDBの特徴的な機能でありながら実はあまり知られていない(かもしれない)地理空間インデックスについて、丁寧にお話ししていただけました。2次元の地理空間情報インデックスして検索が行える機能がver.1.3.3以上で使えるようになりました。Indexの作成の仕方から$near、$maxDistanceなどのクエリ使った検索の仕方について、具体例とともに紹介していただきました。しかし「地球は丸かった」(笑)ということで平面上での近傍検索しか行えない先ほどのクエリに対して、$Sphere という球体情報を考慮した画期的なクエリ(ver.1.7)についてお話し頂きました。非常にエキサイティングで面白かったです。ありがとうございました。

CouchDB on Androidでスタンドアローンアプリ。

@さんからは最後の締めを飾るにふさわしく、まさに漢の家計簿管理方法:CouchDBJQueryとCouchAppを用いた家計簿管理アプリ」についてデモともにわかりやすくご紹介いただきました。ポイントはJavaScriptのみで書かれたスタンドアローンアプリであることで、それだけで簡単に実務に役立つアプリが作れてしまうということです。CouchDBの面白いところですね。最後にCouchDBjquery-mobileで使用する際の注意点などをお話ししていただきました。CouchDBとモバイル端末との連携はアツいですね、今後に期待です。ありがとうございました。

最後に

ユーザーグループの主催、勉強会の主催というのを今回初めて経験させてもらっていますが、非常に楽しく刺激的でもあり、一方で色々と大変だなぁというのが正直な印象です。しかしそこに非常にたくさんの方々の協力支援があって、着々と良い方向に向かって前進していることを確信しています。これからも皆様のお力を借りながら、これらの活動がもっとたくさんの人達を巻き込んで、ハッピーにしていけたらなぁと思っています。いつも助けていただいている皆さん、本当にありがとうございます。今後ともどうぞ宜しくお願いします。

MongoDBドキュメントチュートリアル

こんにちは@です。まずは皆さんのおかげでMongoDB JPを立ち上げることができ、かつ150名以上の方に参加していただいている事に感謝したいと思っています。今後積極的な活動を行っていきますのでどうぞ宜しくお願いします。

さて、本エントリーはMongoDB JPの方で日本語ドキュメントを手伝ってくれる方を募集し、かつどの部分を翻訳したいですかと尋ねたところ、「ドキュメントの構成がどうなっているのかわからない」という貴重なご意見を頂きました。確かにおっしゃるとおりでどのようなドキュメントの構成なんてわかりませんよね、しかもそれをすぐに把握するのも大変ですし。そこで今回はドキュメントがどのような構成になっているのか、ドキュメントに沿ってMongoDBの機能を簡単に紹介するようなチュートリアルをやってみたいと思います。そういう意味で全て本家ドキュメントからの引用になります。

なお、前エントリー、MongoDBのちょっと詳しいチュートリアルも参考になると思いますので宜しくお願いします。

※日本語ドキュメント訳には現在10名程度の方が手を挙げて下さっています。本当にありがとうございます。実際の進め方や管理方法、用語の統一など、スムーズな作業ができるようにそちらの環境を整えていきますので宜しくお願いします。もし経験者の方で複数人での訳の進め方について何かアドバイスがございましたらコメントいただけると幸いです。

ドキュメントインデックス

現在のドキュメントの目次は以下のようになっています。本日のアジェンダはこの目次に沿って行きたいと思います。フライングしますが、Aboutのページはあまり読まれなかったりしますが、かなりの情報が詰まっていますので是非とも見落とさずに読んでみて下さいね。

  1. Introduction
  2. Quickstart
  3. Downloads
  4. Drivers
  5. Developer Zone
  6. Admin Zone
  7. Contributors
  8. Community
  9. About
  10. International Docs(省略)
  11. Books
  12. Doc Index(省略)

1. Introduction

まず一番初めに見るべきイントロダクションです。10gen CTOであるEliot Horowitz氏の言葉に始まり、MongoDBの特徴、

  • Document-oriented
  • High performance
  • High availability
  • Rich query language

が簡単に書かれています。箇条書きで詳しく書かれていないのであまり読む必要は無いような気がします。

2. Quickstart

  1. Quickstart OS X
  2. Quickstart Unix
  3. Quickstart Windows

Windows、Mac、Linuxでのインストール手順が記載されています。各種32bit/64bitが選べますが、オンメモリーデータベースにおける32bitの利用はデータが2GBに限られるというかなり厳しい制約を受けるため、できる限り64bitをインストールするようにして下さい。

3. Downloads

本ブログ執筆時点(2010年11月22日)の最新安定バージョンは 1.6.4 です。このバージョンでかなり多くの改善がなされたので、今から導入される方はこのバージョンをお薦めします。1.6 Release Notesより、主な変更点を記しますと、

Sharding

Sharding がこのバージョンで初めて正式にサポートされました。スケーラブルで単一障害点を持たず、設定・管理が非常に簡単で、かつ現在の単体構成から容易にスケールさせることのできるMongoDBの魅力的な機能です。

Replica Sets

従来のMaster/Slave構成・Replica Pair構成に加えて新たにReplica Sets構成が加わりました。正しく言えばReplica Pairの拡張で、Pairの2台構成からSetsは7台構成まで可能になりました。ではなぜ7台なのか、その台数で必要十分であるということをMongoDBの代表的なプロダクションカンパニーであるBoxed IceのブログWhen would ever need more than 7 replica set members?で書かれています。Boxed IceのMongoDBに関するブログ記事はとても参考になります。

その他 - クエリの強化・64bit

その他、いくつかの機能が改善されています。例えば$or、$slice オペレーターが増え、1コレクション当たり64インデックス(以前は40)を作成できるようになりました。

4. Drivers

MongoDBは非常に多くの言語でドライバをサポートしています。ここに掲載されている以外でも多数のドライバが存在します。また、RubyOnRailsやDjango、@さんの作られたCakePHPといったフレームワークのORMとしてのライブラリも各種充実しています。現在サポートされている言語は、

になっています。さらに最近ではRからMongoDBを呼び出せるRmongomongo-hadoopといったHadoopとの連携をサポートするツールも公開されるようになってきました。特にHadoopとの連携は今後サポートを強化していくという正式アナウンスがありました。今後が楽しみですね。

5. Developer Zone

さて、MongoDBの機能を説明してあるページはこのDeveloper Zoneと次のAdmin Zoneになります。Developer Zoneでは、機能説明であるManualに入るまでにその他紹介されている事項を先に記入しておきます:

cookbook.mongodb.org

MongoDB Cook Bookです。といってもそこまで豊富なTipsが掲載されているわけではありませんが、あるコレクションからデータを一様ランダムに抽出する方法、MapReduceのサンプルなどが掲載されており、今後Tipsが増えていくことを期待しています。

Tutorial

こちらのチュートリアルは具体的に手を動かしながらMongoプロセスを立ち上げ、基本的な挿入・検索コマンドからどんな値がどのような形式で返ってくるのかを具体的に指示してくれています。こちらは有用なので一読をお勧めします。

mongo - The Interactive Shell

MongoDBはシェル上でCRUDの操作やReplicationやShardingの管理構築ができます。実はMongoDBのコマンドはJavaScriptで書かれており、わかりやすい形式でコマンドが表現できます。また、ユーザーが関数を定義することもできますし、定義関数が書かれたJSファイルをシェル起動時に読み込ませることもできますので非常に便利です。このシェルこそ最もMongoDBらしい部分かもしれません。

Developer FAQ

ネームスペースとは何か?どのRAIDで構成すべきか?などの興味深い点についての説明がなされています。最後の質問、「Why are my data files so large?」MongoDBは保存するデータサイズが巨大になりがちなのですが、これについての説明がされています。実際運用するに当たってはディスク・メモリ使用量やフラグメンテーションといった部分が気になると思います。前エントリーの「foursquareの11時間にも及ぶサービスダウンの原因を詳細に調査してみた」もこの辺りに少しだけ関係してきそうです。

Manual

Developper Zone、主に使用者(反対は管理者)が扱うための主要な機能が網羅されているManualになります:

Databases

シェル上でMongoDBを扱うためのガイドです。シェル上のコマンドは管理者向け、CRUDを行うため、サーバーステータスを確認するため、といった様々な用途で使用する事ができます。前述しましたようにJSで記述されています。API Documentationでソースを読むこともできます。

Collections

MySQLのテーブルのような概念はMongoDBではcollectionと呼びます。MongoDBのcollectionはまたBSONドキュメントの集合になっています。MongoDBから取得したデータはたいていJSONとして扱うのですが、内部的にはJSONよりも(多くの場合で)効率的なBSON形式でデータ格納されています。

  • Capped Collections

Capped collectonsはあらかじめサイズの固定されたcollectionで、サイズがいっぱいになった場合は古いデータが消されて追記されて行きます。Capped collectionsは書き込みが非常に多く発生する場面(ロギングなど)において非常に高いパフォーマンスを発揮することが期待されます。現バージョンではオブジェクトの削除ができないなどのいくつかの制約がありますが、それが問題にならない特定の用途に対しては最適な選択になります。MongoDB自身のオペレーションログ(oplog)はこのCapped collectionで構成されています。ですのでoplogは初めに大きめのサイズを指定しておくことで障害時の復旧時などのロールバック時に役立ちます。このページは今後MongoDBをよりうまく活用したい場合などに有用なページになりますし、色々丁寧に書かれていますので一度読んでおくことをお薦めします。

Data Types and Conventions

MongoDBに格納されたオブジェクトのDocument IDである "_id" キーについて、BSONについて、MongoDBのデータタイプについてやや詳しく記述されています。また、MongoDBはUTF-8での正規表現をサポートしています。

GridFS

他のDBが持たない希有な機能の代表としてこのGridFSがあります。これはMongoDBで大容量のファイルを扱うための仕組みを提供してくれます。BSONでのオブジェクトサイズの上限は4MBですが、GridFSはそれ以上の例えばビデオや音楽ファイルを扱うことができます。大きなファイルを複数のファイルに分割して、それがあたかも一つのデータであるように扱っています。各種言語向けにDridFS APIが用意されています。

Indexes

MongoDBは完全なインデックスをサポートしており、あらゆるキーに対してそれを作成することができます。ここではインデックスの詳細な利用方法が記述されています。運用上重要な項目ですが、しっかりと書かれていると思います。最後にここで特筆すべき事として、Geospatial Indexing(地理情報インデックス)をサポートしていまして、例えばfoursquareは積極的にこのインデックスを活用しています。

Querying

MongoDBでのクエリーの作り方について書かれています。MongoDBが豊富なクエリを持つことは、Advanced Queryから確認できると思います。このページはとても読んでいて楽しいです。また、countやdistinctといった集約関数も使えます。

Optimization

最適化の説明になります。MongoDBの性能を落とさない/性能を導き出すための様々な提案が書かれています。インデックスを張る、必要な項目のみを指定して取り出す…などの重要な情報がたくさん書かれています。

Inserting

データの挿入方法についての説明です。MongoDBへの挿入データはJSON形式で記述します。挿入オブジェクトにはかならずDOCUMENT IDである一意なキー "_id" が無いといけません。キーに含まれていない場合は自動で作成されます。Schema DesignではMongoDBにおけるデータのスキーマデザインについて詳しく書かれています。また bulk insert という高速にinsertを行うための機構を備えています。

Removing

MongoDBのオブジェクトを削除します。removeコマンドは基本的には検索と同じように条件を指定し、それに合致したオブジェクトを削除します。条件を指定しない場合は始めの1件だけが削除されます。

Updating

MongoDBには基本的なupdateコマンドに併せて、Modifier Operationsという非常に高速・効率の良い更新方法を備えています。オブジェクトの特定のキーの値のみをインクリメントするする場合などで用いることができますが、他にも$inc、$set、$push、$addToSet、$pop、$pull、$renameなど、なかなか多くのModifierコマンドがありますので、updateの際はまずはModifier Operationを検討してみて下さい。

MapReduce

MongoDBはShardingを行っているサーバー間でMapReduceを実行することができます。また、大規模のコレクションに対するgroupなどの一部の集約関数はMapReduceで代用することが推奨されています。サンプルコードたくさんあってそれなりに丁寧な説明がなされています。

6. Admin Zone

続いてAdmin Zoneの紹介です。こちらはMongoDBの監視からReplication、Sharding、バックアップの方法などの機能が紹介されています。

Production Notes

基本のTCP port番号は、

  • Standalone mongod : 27017
  • mongos : 27017
  • shard server (mongod --shardsvr) : 27018
  • config server (mongod --configsvr) : 27019
  • web stats page for mongod : add 1000 to port number (28017, by default)

で、推奨ファイルシステムLinux)はext4かxfsとなっています。

Replication

Replicationの主要な2つの構成、Master/SlaveとReplica setsについて、詳細に記述されています。Replica sets は現在のPrimaryに障害があってダウンした場合、自動的にメンバー内で次のPrimaryを選択するフェイルオーバー機能を祖備えています。MongoDBのレプリケーションとバックアップ機能の紹介に少し詳しく書いています。この部分のドキュメントは多数のサンプルコードが使われており、実際に自分で手を動かしながら動作の確認を行う事ができます。

Sharding

MongoDBのShardingはこちらがキーの分割の仕方を示してやらなくても、自動で適切に配備した複数のサーバーに割り振ってくれます。また、Shardingしているサーバー間でMapReduceを行うこともできます。まずSharding IntroductionにてMongoDBにおけるShardingを丁寧に解説してもらった後、Configuring Shardingで実際に設定方法が細かく記載されています。前述のReplicationとこのShardingの部分はかなり丁寧なドキュメント構成になっている印象を受けます。

Hosting Center

非常にホットな話題です。MongoHQなどのホスティングサービスの紹介から、AmazonEC2上で動かす方法まで、簡単ですが述べられています。

Monitoring and Diagnostics

現在のMongoDBの状態の監視・診断といって、管理者にとっては重要な項目です。muminやgangliaで状況を確認するアダプターも存在しています。

Backups

データファイルをコピーしてバックアップする方法、Slaveの同期によって行う方法、mongodumpコマンドを利用してエクスポートする方法など、いくつかの方法が提案されています。2番目のReplicationのメンバーを追加することによってバックアップを行う方法は、データのが大きいと非常に多くの時間がかかる場合があるので注意が必要です。

Admin UIs

MySQLphpmyadminのような、UIから直感的にMongoDBを扱えるツールが多数スクリーンショット付きで紹介されています。僕も一通り試してみましたが、結局はシェルで操作するのが一番柔軟で楽なので使わずに終わっています。ただ、管理者以外の複数のメンバーがデータを閲覧できる方法として考えると非常に便利かもしれません。

その他

その他細かい項目がありますが、割愛させて頂きます。

7. Contributors

Contributer向けのルールや加入方法などについて記載されています。肝心のドキュメント翻訳のContributing to the Documentationになっていますが、日本語ドキュメント訳を手伝って下さる方を募集しています。MongoDB JAの募集スレッドからの返信か、僕@に連絡いただければ申請を行いますので是非という方は宜しくお願いします。

8. Community

はなかなか重要な情報が満載です。まず本家ユーザーグループ・IRC・Blog・バグトラッカーへのアクセス方法が紹介されています。そして現在の開発陣の紹介、そしてジョブボード。非常に個人的な意見ですが、Looking for Data Scientistsという役職があるのが海外らしく、かつ大変興味深いですね−。QualificationsにHadoopやPigやMapReduceと書かれていたりします。

9. About

完全に見落としがちなAboutページですが、MongoDBを全体を俯瞰して理解を深めるにはここが最適です。なぜならば、Slide Galleryにて今まで紹介してきたMongoDBの機能がスライドでわかりやすく紹介されており、またUse Casesではロギングやテキストサーチといった具体的なMongoDBの利用例が紹介されています。Production DeploymentsではMongoDBを活用する企業の名前が資料とともにリストアップされています。この資料がとてもわかりやすいので、色々読まれてみる事をお薦めします。 BenchmarksではMongoDBとMySQL、あるいはCouchDBなどといった他のDBとの比較を行った記事のリンクがあります。こちらも非常に参考になります。

11. Books

現在出版されているMongoDBに関する書籍の情報です。MongoDB The Definitive Guideはやや薄めの書籍ですが、ReplicationやShardingといったAdmin Zoneの内容について、その原理から具体的な構成方法まで記述してくれていますのでお薦めです。 The Definitive Guide to MongoDB: The NoSQL Database for Cloud and Desktop Computingの方は時が少し読みにくいのと、ドキュメントとかぶるところが多いのですが、こちらもしっかり書かれているので勉強になると思います。この2冊は買って持っていますが、どちらかと言えば前者がお薦めです。日本語訳本が待たれるところです。というより訳をしたいですよね。残りの2冊はまだ発売されていませんが、MongoDB in Actionは筆者のブログで色々と情報が公開されています。というよりとても貴重な情報源です。

最後に

いかがでしたでしょうか?以前のエントリー、MongoDBのちょっと詳しいチュートリアルはそれなりにまとめて書いていますので、こちらも併せて見て頂けると幸いです。今後はMongoDBの各機能にもっとフォーカスして具体例を出しながら紹介していきたいと思っています。今後とも宜しくお願いします。

MongoDB JP を立ち上げました!

こんにちは、@です。本日はMongoDB JPの立ち上げに伴って、その告知と本グループにかける想いを少し書かせて頂きたいと思います。まずは改めて、

MongoDB日本ユーザー会:
MongoDB JP
を立ち上げました!興味のある方、ご登録宜しくお願いします!

記念すべき第1回勉強会&懇親会はCouchDB JPの皆様と合同で開催させて頂こうかと思っています。

2010年12月12日(日)
第1回 MongoDB & CouchDB 合同勉強会
※ 開催場所は規模などを見て後日ATNDとメーリングリストで告知します。
※ 現在MongoDBに関する発表者を募集中です。是非とも、という方がいらっしゃれば@ 宛かメーリングリストに連絡宜しくお願いします。

今回の立ち上げに当たってはCouchDB JPの@さんと@さんの温かい支援も頂きました。今後とも情報交換として、また良きライバル(!?)として交流を深めていきたいと思っています。

さて、この告知で大方の目的は果たしましたが、時間のある方は以下のアジェンダでのお話に付き合っていただければ嬉しいです。

  • 設立に至る経緯
  • 目的
  • 活動内容
  • (個人的な)目標

設立に至る経緯

業務でMongoDBを活用してみて

僕自身、MongoDBに出会ったのはつい2ヶ月程前の事です。現在アルバイトをさせてもらっている芸者東京エンターテインメントでの業務、ソーシャルアプリのログ解析(資料1資料2)において散在する各種ログを集計・集積し、かつ解析側が自由にアクセスできるような解析データストレージを検討していたところ、MongoDBに出会い、現在はソーシャルアプリ2種・1TB近くのデータ容量・10億レコード以上を保持し、解析基盤の中枢を担う役割として日々活躍してもらっています。
運用を進めるに当たって感じたことは2点ありまして、

  1. 非常に扱いやすく・高機能で様々なシーンでもっと活用できるはず
  2. 日本ではMongoDBに関する情報が圧倒的に少ない

そしてこのどちらもが今回の設立における原動力となっています。

  • 非常に扱いやすく・高機能で様々なシーンでもっと活用できるはず

実際にログ解析という用途でのMongoDBの運用でも、非常に多くの機能を最大限活用させてもらっています。スキーマレスでありながら豊富なクエリをもち、完全なインデックスをサポートし、かつレプリケーションシャーディングが非常に容易に行え、コンソールから何でも操作できて…そういった機能はログ解析に限らず様々な用途での活用にもベストな選択になりうると感じるようになりました。例えば、

のようなものがMongoDBを使うことでかなり楽にできる気がします。比較的新しいサービスが日本からもドンドンと生まれて来て欲しいなと考えています。

  • 日本ではMongoDBに関する情報が圧倒的に少ない

海外ではfoursquareの利用を始めとして、MongoDBを活用したサービスが多数存在し、かつ本家ユーザーグループでの議論がとても盛んに進んでいます。ただ、現在のところまだ日本ではMongoDBという名前はあまり知られていない現状であり、それ故に日本語の情報がとても少ないです。こういった部分がネックになってさらに利用が遠ざかっていってしまうのはとても残念なことだと思います。
日本からもMongoDBに関する情報をどんどん増やしていって、企業から個人利用に至るまで、誰でももっと気軽にMongoDBに触れるようになったらないいなと思います。さらにこちらからどんどん技術のフィードバックを出していって、MongoDBの開発自身にも貢献していければと思っています。

後は思い切りですよね

日頃業務でも社員の方々には色々MongoDBの話を聞いてもらっていたり、個人的に仲良くしていただいている@さんや@さん、 @さんとMongoな話をしていたりして、ユーザー会を立ち上げてもっとMongoDBの事を知ってほしいな、色んな人に会いたいなと思っていました。
そんな想いが募りに募ってきた頃、ちょうど先日、@さん、@さん、@さんという、PHP界の第一線を走られかつとてもMongoなアツイ方々とお酒を飲む場がありまして、「じゃあやろう!」という話でその場で盛り上がりました。そして@さんのこの強烈な後押しによって、思い切ってその日にTwitterで告知したという経緯になります。ありがたいことに非常に多くの方の反応があり・温かいお言葉をたくさん頂いて、今はなんでもっと早くやらなかったのかと思っています。まあ、本当に大切なのはこれからですからね、頑張ります^^。

目的と活動内容

何よりの目的はMongoDBの日本での普及です。普及という言葉にかける想いは、たくさんのエンジニアが門戸を叩いてくれるような「広がり」という側面と、熱心なエンジニアが議論を交わしあえたり勉強会で交流できるような「深み」を持った側面を持っています。

  • [勉強会]:定期的な勉強会を通じて技術・情報交流を進めていきます。
  • [交流]:CouchDB JPとの積極的な技術・情報交流を行ないます。
  • [ドキュメント]:MongoDB日本語ドキュメントの整備を行ないます。
  • [Q&A]:MongoDB活用に伴う問題・課題をメンバーで議論・解決していきます。
  • [イベント]:MongoDBカンファレンスの日本開催に向けての働きかけを行います。
勉強会

勉強会は積極的に開催していきたいと考えています。また他のNoSQLとの比較や、MongoDBのサービスへの活用事例、Node.jsやWebOSとの連携の話など、他の技術やコミュニティとのコラボレーションなども積極的に行って行きたいと思います。

交流

本家ユーザーグループでは常に積極的な様々議論が行われていますが、こちらでもそういった議論やQ&A、情報交流が活発になり、それが多くの方の有用な情報源となっていく場を目指します。

ドキュメント

MongoDBドキュメントの日本語訳を整備していきたいと思っています。現在の日本語ドキュメント訳は最近僕も参加させて頂いていますが、今のほとんどの部分は@さん1人の貢献に寄るものです。より多くの方々に関わって頂き、ドキュメントの整備を勧めていきたいと思っています。他にも現在出版されているMongoDBに関する本の日本語訳などもできたらと思います。

イベント

これはあくまで個人的な想いですが大きなイベント、特に全国各地で開催されているMongo Conferencesを是非とも日本で開催させたいと思っています。
foursquarebit.lyの中の人を呼んで是非ともお話を聞いてみたい!そのために10genの方々に積極的に働きかけて行きたいと思っています。それは個人的にも英語の勉強の良いモチベーションにもなりますし、頑張ります!あ、協力者も募集してます!

目標

最後に個人的な目標ですが、まずは海外で行われるMongo Conferencesに参加してきて、発表してきたいですねー。そこで色んな人と交流できればもっと色々な事ができそうな気がします!後は外国の方々にも注目されるようなサービスと技術を持った日本企業がProduction Deploymentsとして大々的に掲載されることです。
今まで色んな想いを述べさせて頂きましたが、どれも決して実現不可能なことで無いと確信しています。大事なのは”率先して手を上げること”、まずはその渦の中に飛び込んでいけば案外なんとかなるものですし、本当に困難な事があったときはきっと誰かが助けてくれると信じています。インターネット業界って、そんなステキな世界だと僕は思っています。今後とも率先してMongoDB JPの活動に取り組み、皆様に助けられながら大きくしていければと思っています。どうぞ、宜しくお願いします。

第8回データマイニング+WEB勉強会@東京で発表してきました。「MongoDBとAjaxで作る解析フロントエンド&GraphDBを用いたソーシャルデータ解析」

お久しぶりです。@です。11/14(日)に行われました、第8回 データマイニング+WEB 勉強会@東京−大規模解析・ウェブ・クオンツ 祭り−で発表してきました。Togetterも参考にして下さい。

発表者・参加者双方の議論を重視するこの勉強会、今回もアツイ議論が絶えず巻起こって、とてもエキサイティングで有意義な勉強会でした。僕は前回に引き続き、今回も発表側として参加させていただきました。その時の資料は以下になります。

前回のログ解析バックエンドの続編として、散在する各種ログを集計してMongoDBに入っているデータを表・グラフとして可視化するためのフロントエンドのお話と、ソーシャルデータの解析をGraphDBであるNeo4jを用いて解析した事例を紹介させていただきました。解析方法の詳細はスライドの方に盛り込ませて頂いたので、今回は今までの解析の総括として以下のアジェンダでお話させていただきます:

  1. 解析用DBとしてのMongoDBの役割とそのメリット
  2. 3ヶ月の仕事を通して感じたこと

GraphDBに関しては次のエントリーでがっつりと書ける程のネタがありますので今回はスルーさせていただきます。

解析用DBとしてのMongoDBの役割とそのメリット

発表や懇親会で「なぜ解析用のDBにMongoDBを選んだのか?」という質問をいつも頂きます。今までのエントリーなどでもその理由をちょくちょく書いてきましたが、今回改めて書こうと思います。まず結論として、MongoDBを採用して間違いなかったことを確信しています。もしこれで無ければもっと基盤構築に時間がかかっていたでしょうし、もっとごちゃごちゃした構成になっていたと思います。といってもそれは作業者のスキル・サービス規模・社内環境/風土・リソース…といった様々な制約条件によって変わってくるものであると思います。なので僕はどのような制約条件の元で仕事をしているか、そこからお話をさせていただきます。
ここでは「データ」を様々なログの1つのレコード、あるいはそれを軽く集計した形でMongoDBに格納されたものと定義します。対して「指標」とはその(複数の)データから計算された最終出力としての数値と定義しておきます。

解析環境・ノウハウ、何も無いところからスタートした(ただ、ログはきちんと残しておいてくれていた)

もともと解析環境やノウハウが無いところからスタートしました。ソーシャルアプリの「おみせやさん」が大ヒットして、弊社が「お客さんにもっとこのゲームを楽しんでもらいたい」→「そのためにログ解析をきちんとやって科学的に仮説検証・実行していこう」というフェーズにあったところに僕が運良くジョインさせてもらったというのがそもそもの流れです。ちょうど8月のことです。ですのでこの課題のゴールとしては、

「とにかく色んなデータを採って検証してみたい」

という、なかなかふわっとしたものになります。例えば他のゴールとしては「PV、UU、ARPU…といった決められた基本指標を出して欲しい」といった場合もあると思います。数TBの手に負えない規模のログデータを持っていたり、そこまで解析を重視しない(そんな余裕がない)といった状況では、基本的な指標算出のみにとどまるでしょう。そういった取るべき指標が明確に決まっている場合はまた違った、もっと効率的な解析手法があったと思います。僕の場合はとにかく色んなデータを採取して見る必要がありましたので、まずログの情報がほとんど”生”の状態で保管しておく必要がありました。まずはどんなデータがあって、どんな数値・特性を持っているのか、そしてそれが今後の意思決定に必要なものになりうるか取捨選択、ということを解析者である僕自身が細かく確認していく作業をしていかないといけません。ですので、

「解析者が必要な時に必要なデータを簡単に眺められて、その特性を見れる」

環境が必要でした。これは、

  • 常にデータが手元にある(社内から簡単にアクセスできる)
  • 散在するログから抽出したデータは一つの場所に集約しておく
  • データの特質を簡単に確認できるデータ保存方法である

ようにしなければならないという事です。今採れているおみせやさんのログは4種類、

  1. MySQLに入っているユーザーの登録情報、課金情報
  2. Cassandraに入っているユーザーのゲームステータス
  3. AmazonS3にバックアップされているゲームのアクセスログ
  4. AmazonS3にバックアップされているユーザーの行動ログ

で、これらを社内から解析者が簡単にアクセスできる1つの場所に集約しておく必要がありました。MySQLのデータは解析専用のレプリケーションを作って頂いて社内からアクセスできる状況にしてもらいました。Cassandraのデータは可動サーバー上にデータを取得するPHPスクリプトを書いて置いておき、社内からそのPHPを叩いてデータを引っ張ってくるAPIの形で取得するようにしました。(実はここはThrift+PHPの形から、Pigを用いてより効率よく取得していく形を考えて実装しようとしています。ですので資料内のバックエンドの構成図では「Cassandra」→「Hadoop」→「MongoDB」の流れになっています…)残りの行動ログ、アクセスログはS3とデータ同期する社内データストレージを構成して、社内から簡単にアクセスできるようになっています。

これは今から考えるとなかなか難しい要請であったと思います。MySQLやCassandraに関しては、そもそもアクセスする権限を解析者に与えてくれること・整備されたネットワーク環境・それなりの規模であったからそれができたのであり、ファイル形式のログも、今まで全てをきちんとS3にバックアップしてくれていたこと・それを社内で同期できる程のデータ量・ネットワーク環境・リソースがあったからこそです。そして何よりもインフラエンジニアの快い協力があったからです。今回は社内にデータ解析サーバーを構成していますが、例えばサービスを全てAmazonEC2上で構成していれば、解析サーバーも手法も全てAmazonEC2・S3上に集約して解析していたと思います。そうなればもっとAmazonクラウド上でのメリット・デメリットを考慮した構成になっていたと思います。MongoDBも使っていなかったかもしれません。

次は集約してきた各種ログをどのような形で保管していくかの問題になります。それぞれファイルの形で管理しておくことも考えましたし、MySQLで管理、Hadoopを活用して(HIVE、Pig、HBase)HDFS上で管理することも考えました。その中でなぜNoSQLのMongoDBで管理するようになったのかというと、

(管理上の理由)

  • スキーマレスであったこと
  • 他のサービスのログに対しても1つの解析基盤で管理する
  • もっともわかりやすくシンプルで管理しやすい方法であったこと

(後の処理を考慮しての理由)

  • 他の解析者が裏側の構成を把握しなくてもデータをとってくることができる
  • 色んな検索条件でデータを引っ張ってこれること
  • データ増大に備えてスケールしやすいものであること

などが挙げられます。まず初めの2項目、

  • スキーマレスであること
  • 他のサービスのログに対しても1つの解析基盤で管理する

ですが、これはログの情報をできるだけ落とさずに保管しておきかったのと、そもそも1つの行動ログの中でも項目全てが統一された形式で記録されていなかったことが原因になります。さらに今後のイベントや解析手法確立、新サービスリリースなどによってログの項目、とり方が増えたり変わったりする可能性が大きくあることを考慮しての事です。全てのデータにスキーマを定義するのはそもそもデータの種類や性質を把握することから始める時点で困難でありましたし、何かあるたびにスキーマに頭を悩ますのを避けたかったというのもあります。

  • もっともわかりやすくシンプルで管理しやすい方法であったこと

これは結構大きな理由です。なにせ僕ひとりで全ての解析をバックからフロントまでを管理しないといけない、そして僕は駆け出しのへっぽこエンジニアかつアルバイトの身ですのでできるだけシンプルでわかりやすく、かつ特にバックエンドの構成に関しては一度構築したあとはできるだけ手を加えることのない、また僕がいなくても他のエンジニアが構成を容易に把握できて、何かあったときには対策を替わりにしてもらえるようなものにしたかったからです。

  • 他の解析者が裏側の構成を把握しなくてもデータをとってくることができる
  • 色んな検索条件でデータを引っ張ってこれること

これは実際に様々な軸で解析を行うときにどれだけそれがやりやすいか、という次のステップでの処理を考慮した話です。もし今後、解析者が入ってこられるとしたならば、いつでも欲しいデータにアクセスできるという前提のもとで解析自身に専念して欲しいという想いがありました。そもそも純粋な解析者でバックエンドの作業が好きな人はいませんでしょうし、もっとも経営クリティカルなのはデータから何を導き出すかですので、今後増員を考えるならフロントよりの方を集めて欲しいという考えもありました。そういうところではファイルとしての管理方法よりも、DBの一つの大きなストレージの中で管理しておき、かつHadoopなどの特別な知識がなくても、簡単な検索クエリーで好きにデータをとってこれるものが望ましかったです。最後に

  • データ増大に備えてスケールしやすいものであること

これも重要ですね。サービスは今後絶対に増えていきますし、それに伴ってデータは増えていきます。データ量が10倍になることもありうると考えると、今から簡単にスケールする仕組みを考えておかなければなりません。

MongoDBの何が嬉しいのか

上のような環境や想定を踏まえて選択したのがMongoDBでした。ではなぜMongoDBかというとその性質

  • スキーマレスであったこと
  • ドキュメント志向であったこと
  • クエリの種類が非常に豊富であったこと
  • 更新性能より検索性能が優れていること
  • (Replication、Shardingの2つの観点で)スケールしやすいこと
  • コンソール上で簡単にデータ操作ができ、かつユーザー定義関数が作れること
  • Pythonなどの各種言語のドライバが整備されていたこと
  • ORM、REST Interface、node.jsに対応するドライバも整備されていたこと

であったからです。

  • ドキュメント志向であったこと

は人に読みやすい形式JSONでレコードを眺められるからです。なにせ格納されているデータの種類や特性を把握しないといけないので、その度に何らか人が読める形に整形して出力するのは効率が悪かったからです。また、フロント側のWebUIにはjQueryを使ってテーブル作成やグラフ描画を考えていました。そこにはJSONの形式で投げるので、もともとJSON(内部的にはBSON)の形式でデータを持っていれば、データを取り出して可視化するまでの処理が非常にシームレスです。

  • クエリの種類が非常に豊富であったこと
  • 更新性能より検索性能が優れていること

これは非常に重要な要素でした。解析は様々な角度から多角的に行う必要があります。そういった用途では時間による範囲検索や、特定の項目での絞り込み、や複数条件での検索、集約関数の充実などの機能が備わっていないといけません。かつ、高速に検索できるよう、インデックス機能を備えているかも重要になります。たいていのNoSQLではSQLでは当然だったクエリの種類が少なかったり、そもそも各所にインデックスを貼れることもできないものが多いです。逆にその部分を犠牲にして更新機能などの他の機能を充実させています。MongoDBは更新機能はそこそこですが、検索クエリがずば抜けて豊富で全ての項目にインデックスを作成することができます。これは解析用のDBとしては非常にありがたいことです。

  • (Replication、Shardingの2つの観点で)スケールしやすいこと

MongoDBの一つの大きなウリはスケールアウトが容易なことにあります。Replicationに関してはReplica setを構成すれば3台以上ののサーバーでデータを同期でき、かつMasterに障害があった場合でも自動的に次のMasterを昇格してくれるフェイルオーバー機能を備えています。Shardingに関してはオートSharding機能があり、指定したキーに対して分散したサーバーに自動的にデータを振り分けてくれます。しかもこれらの機能を非常は簡単に扱うことができます。

  • コンソール上で簡単にデータ操作ができ、かつユーザー定義関数が作れること

MongoDBはコンソール上でデータの操作が簡単にできます。かつコマンドはJavaScriptで書かれていて、独自の定義関数をJavaScriptで書けばそれが使えるようになります。例えば毎回実行するけれども、長くなってしまうようなコマンドはこのユーザー定義関数にしておけば非常に楽です。また、コンソールで色んな操作ができることは作業効率の大幅なアップにつながります。

  • ORM、REST Interface、node.jsに対応するドライバも整備されていたこと

これは後のフロント側の処理に関連するところです。WebUIではMongoDBに入っているデータを取得→可視化しないといけませんので、そのための何らかのドライバが必要です。MongoDBはそのための豊富なドライバが各種言語で用意されています。

以上を見ると、MongoDBは今回の僕の解析に求められる条件をほとんど満たしていることがわかります。他の方法ではここまで条件を満たしてはいません。HBaseやCassandraはそこまで検索クエリが備わっていませんし、HiveやPigはデータの可視性や操作性などで面倒な部分があります。CouchDBやRiakなどの他のドキュメント志向DBと比べましても検索クエリが一番豊富でありますし、各種ドライバも豊富です。かつ大容量のレコードを扱えるGridFSやパフォーマンスの非常に高いCappedCollectionといった独自の便利な機能は後に使用することになりそうだとも考えていました。それでは逆にMongoDBの弱点を挙げてみますと、

  • Replicationが遅い
  • メモリ・HDDを大量に消費する
  • インデックスがメモリに乗らなくなった時点でパフォーマンスが著しく低下する
  • 管理者向け機能がプアである
  • map/reduceがシングルスレッドで行われるため、その間DBがロックされる

ことが挙げられますが、これらは解析用として使うにはそれ程クリティカルな要素ではありません。瞬時に大量の書き込みを絶対に裁かないといけないワケでもありませんし、社内向けDBなのでオープンにする必要もありませんし、基本的に毎日1回のバッチ処理ですので速度が多少遅くても問題ありません。メモリやHDDも急激に消耗することも少ないですので、ある程度事前に対策ができます。Shardingも簡単にできますからね。

技術は適材適所、その場に応じたものを選択することが重要

MongoDBを採用した理由はだいたいこんなところです。今回の自分の制約条件を満足する最も最適な解がMongoDBだったのです。前述しましたように条件が変われば解はいくらでも変わってきます。大事なことはその条件に応じて常に最適な技術を選んでこれる能力だと思っています。ただ、プロバイダー側のソーシャルアプリのログ解析においてはだいたい同じようなことが当てはまるような気がします。もし似た環境におられる方は、一度MongoDBを検討してみては良いかと思います。あと、一つの技術だけで全ての要請を満たすことはできないとも感じています。今回の資料にもありますが、ソーシャルデータの解析にはGraphDBを用いた方が便利なような気がします。GraphDBの中でNeo4jはその中で最も発達したDBで、かつ今まで使用してきたPythonのドライバがあって、REST Interfaceも備えています。そういったMongoDBと共通に扱える要素が多いという意味でMongoDBとGraphDBを現在は共存させています。

3ヶ月の仕事を通して感じたこと

芸者東京エンターテインメントにアルバイトに入って、ゼロからログ解析を初めて早くも3ヶ月が経ちました。今少し感じていることを書いてみます。

解析という仕事にはバックエンドとフロントエンドの2種類があって、求められる技術が全く異なる

今までのおはなしの中でもわかって頂けると思いますが、解析という仕事は大きく2つに分類できます。1つは散在するログを集積し、何らかの形式で保管・管理するための仕組み(基盤)を作るバックエンドの部分と、もう1つは欲しいデータが常に手に入るという前提の下でRやEXCELなどのツールを駆使して実際の解析を行ない、経営や企画の意思決定に貢献する部分です。前者で重要なのは、インフラやDBの知識、あるいはHadoopやNoSQLなどの大規模データ処理技術を使いこなせる能力や各種制約の下で適切なアーキテクチャを構築できる能力で、逆に前者はユーザー支点や多角的に物事を観察できる能力、各種ツールを使いこなせる能力です。ですので解析者がいない企業では、前者はインフラエンジニアが兼任し、後者はマーケティング担当や企画側が兼任することになると思います。もちろんどちらの知識も併せ持つことが重要ですが、もし解析者としてエキスパートになるのならば、どちらの方でそれを目指すのかということを考えていかないといけないかもしれません。解析者というと後者の方をイメージされる方、目指している方が多いかもしれません。僕は前者のバックエンドのほうでエキスパートを目指していきたいなと思っています。そこら辺の話はまた別の機会にしたいなと思っています。

とにかく今回の勉強会もとても有意義な素晴らしいものでした。運営者のみなさん、会場を貸していただいたニフティのみなさん、参加されたみなさん、本当にありがとうございました。今後とも宜しくお願いします。