〜うまく動かす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を駆使して解析したい方へ↑