MongoDBを真面目に使ってみた
MongoDB Advent Calendar 2013の10日目の記事です。
2つほどNode.js + MongoDBで作ったサービスを最近リリースする機会があったので、そこで得られた個人的な知見や感想についてつらつら書きまとめようと思います。
前提
今回はアプリ側の開発を担当していて、インフラ周りはインフラチームやMongoDBのプロバイダーに御任せでしたので、そちらの設定などの話はノータッチです。
元々の業務ではMySQLを使っていて、この2つのプロジェクトで初めてMongoDBを勉強しました。
永続化にMongoDBを使う理由
MySQLでなくMongoDBを使う理由はMongoDB自体の利便性などもあげることができるかもしれませんが、個人的には運用面での利点が強いと思っています。(特にサンフランシスコのような人材採用が激しい地域の場合)
シャーディングやフェイルオーバーが大規模な運用では必須になりますが、MySQLの場合はMySQL自体が提供している機能ではないので、運用する会社ごとに独自の運用方法を持っています。運用方法を標準化できないと、運用メンバーの流動性が高いと学習コストが馬鹿にならなかったり、サービスの規模や数が増える際に運用メンバーの数がボトルネックになってくる可能性があります。 MongoDBはこの辺りが標準の機能として提供されているのは、運用メンバーのスケールアウトを考えると一つ大きな利点だと思っています。
とはいえ、実際にMongoDBを運用した話をググったことがある方はわかると思うのですが、この辺の機能を使おうとして失敗した話や苦労した話は無数に転がっています。残念ながら、気軽に使うには少し難易度が高く、ある程度の知識や経験が求められています。
そこを踏まえてのもう一つの理由は、有力なMongoDBプロバイダ(DaaSを提供する会社)がいくつか存在することです(MongoHQ, MongoLab, ObjectRocketなど)。現在の人材採用の戦線でMongoDBのエキスパートを会社に何人も雇うことは大変なことですが、こういったプロバイダを利用することで、そういった人材不足をカバーでき、運用面のコストや人的リソースの削減を行うことができます。
日本にもこういった有力なプロバイダが出てくると、もっとMongoDBの利用事例が増えていくのではないかと思います。
MongoDBが適するケース、適さないケース
個人的に、適するケース、適さないケースと思ったことについて述べます。
適するケース
複数のコレクション同士の関連が薄いもの、または多少の不整合が許容されるものの場合にMongoDBは適しています。
ブログや掲示板などの場合、同時に複数のコレクションに挿入や更新を行うことが少なく、また失敗しても多少の不整合は許容される場合が多いため、適した例の一つです。
FoursquareがMongoDBを使っていることは有名かと思いますが、実際の機能から推測するしかないですが、この場合もコレクションの関連が薄く、多少の不整合が許されるものの良い例だと思います。
その他にも、不整合がクリティカルになりにくい社内ツールやコレクションの独立性の高いちょっとしたログデータのような場合も適した例と言えます。
適さないケース
逆に複数のコレクションに跨いで更新が必要なものの場合、MongoDBは利用が難しくなります。
例えばPVP対戦機能を持ったMMO RPGなどで使うことを考えると、複数のコレクションに更新、挿入をすることが予想されます。あるコレクションの更新に失敗した後に別のコレクションの更新に失敗した場合に不整合が起きる可能性があります。開発者は、自前で不整合を解消する方法や複数のコレクションにまたがらないようにデータ設計の改善などの工夫が必要となります。
また、不整合を避けるために1つのドキュメントに複数のドキュメントをまとめることがありますが、最大で1ドキュメント16MBまでは許容はされますが、成長を続けるドキュメントはパフォーマンスの低下やデータの断片化を招くため、あまり良い状態とは言えません。
ドキュメント設計について
ドキュメント設計で考えるべきポイントは、ドキュメントを成長させない形に設計することです。
最も効率の良いMongoDBのドキュメントの使い方としては、ドキュメントに必要なフィールドがプリアロケートできるケースです。
データが変わると、MongoDBは新しい領域を確保してそこにデータを移動します。この処理はコストが高く、頻発するとMongoDBのパフォーマンスの劣化を招きます。MongoDBではこれを避けるために、データの移動が起きるとpaddingFactorの値を増やし、そのコレクションに新しいドキュメントを保存する際に少し大きめの領域を確保するようになります。これはパフォーマンスの観点からは良いことなのですが、実際に必要なデータのサイズ以上にデータ領域を確保してしまうため、データサイズの肥大化を招きます。
あるコレクションが持つドキュメントの構造があらかじめわかっている場合には、仮の値を入れておくとドキュメントのために必要な領域が最初に確保することができ、データの移動も起きないので、最も効率よくMongoDBを使うことができます。(この辺りの話については「Scheme design in MongoDB.」という記事の「ドキュメントを成長させない」を読むとよく理解できます)
ただ、実際のユースケースを考えると、ある程度のドキュメントの成長は避けられない場合も多いです。この場合、1つのドキュメントが大きくなりすぎるとデータの移動のコストも高くなるので、1ドキュメントが大きくなりすぎないように適切にコレクションを分割するように設計をするのが良いと思います。
MongoDBの使用サイズを減らす
MongoDBを使っていてアプリ開発側が考慮すべき一つのポイントはドキュメントやコレクションのサイズです。
MongoDBはMySQLに比べるとデータのサイズが肥大化しがちで、サービスを運営する側から見るとコストの増加につながっていきます。 ここはアプリ側の設計や工夫の余地がある所になります。
実サイズを確認する
既に開発中や運用中のデータがある場合、以下のやり方でデータベースやコレクションやドキュメントのサイズを確認可能です。
データベースとコレクションのサイズを確認するのは以下を実行します。1024*1024はMBで表示をするために指定しています。
# データベースのサイズを確認 > db.stats(1024*1024); { "collections" : 2, "objects" : 227397, "avgObjSize" : 183.91343280251579, "dataSize" : 110, "storageSize" : 154, "numExtents" : 27, "indexes" : 18, "indexSize" : 47, "fileSize" : 496, "nsSizeMB" : 16, "dataFileVersion" : { "major" : 4, "minor" : 5 }, "ok" : 1 } # コレクションのサイズを確認 > db.user.stats(1024*1024) { "ns" : "test.user", "count" : 4408, "size" : 85, "avgObjSize" : 0.00009644043132718926, "storageSize" : 118, "numExtents" : 11, "nindexes" : 6, "lastExtentSize" : 35, "paddingFactor" : 1, "systemFlags" : 1, "userFlags" : 0, "totalIndexSize" : 62, "indexSizes" : { "_id_" : 5, "id_1" : 0 }, "ok" : 1 }
読み方は以下を参照。
http://docs.mongodb.org/manual/reference/command/dbStats/
http://docs.mongodb.org/manual/reference/command/collStats/
変なインデックスを作っていてインデックスサイズが大きくなりすぎていないかや、paddingFactorが1になっていない場合はドキュメントの移動(前述)が起きていることの確認ができます。
では、実際にドキュメントがどのくらいサイズを利用しているのかはBSONのサイズで確認します。
# 渡したオブジェクトのサイズを確認 Object.bsonsize({"_id" : ObjectId("11271c0c3b6cd10db106eeef"), name: "John"}) # findしてきたドキュメントのサイズを確認 Object.bsonsize(db.collection.findOne( {"_id" : ObjectId("52242092732483cb06a444fc")}));
サイズを減らす方法
実際にドキュメントのサイズを減らす方法ですが、データ構造やインデックスの見直しなどもありますが、一番手っ取り早いのはドキュメントのフィールド名を短くすることです。MongoDBはドキュメントベースのデータベースであるため、フィールド名も各ドキュメントで持っているため、フィールド名を数文字減らすだけでもドキュメントの数が数千万とかになってくると差が出てきます。
ドキュメントを設計する時点で意識的に短い名前にする(userIdをuidにするとか)のも良いですし、MongoDBのライブラリでフィールド名にエイリアスを付ける(実際にはuidという名前だけどuserIdとしてアクセスできる)ような機能を持つものを利用するのも良いかもしれません。
このあたりはMySQLとは事情が異なるのでRaisの慣習でフィールド名を決めると(テーブル名+IDとか)後々になって後悔するかもしれません。
以下の記事がわかりやすかったです。
終わりに
ということで、色々書きましたが、この記事が今後MongoDBを使う方の参考になれば幸いです。