「第3回 MongoDB 勉強会 in Tokyo」 : ATND
先週土曜日(2011年5月14日)にフューチャーアーキテクトさんのセミナールームにて行われたMongoDBの勉強会に行ってきました。
MongoDBの仕様・機能などの体系的な話からチューニングの話、開発に利用する話、プロダクションとして運用している話と、バランスよくまとまっていてとても勉強になりました。
RDBとの違いや特有の挙動をきちんと把握して利用すれば、十分プロダクションで利用出来るというビジョンが見えました。
詳細は各発表者の方のスライドを見るとわかると思いますが、当日会場でメモしたものでダイジェスト的にまとめました。
MongoDB勉強会は毎月開催(予定)しているそうなので、興味を持たれた方は是非参加して見てください。
MongoDBコミュニティー:MongoDB JP | Google グループ
【発表1】MongoDB全機能解説1
MongoDB特有の仕様や機能など、プロダクションで使う場合には特に注意が必要な部分を中心に説明されていました。
ロック
Read/Writeの各オペレーション時にDBに対してロックがかかる。
MongoDBをフロントエンドで使う場合、DBがロックして応答が返って来なくなるなどの問題が起きる可能性があるので、時間がかかる処理には注意が必要とのこと。
実際、単体のRead/Writeは十分高速でロックの待ち時間で問題が起こることはないが、複数のWrite処理をアトミックで行なったり({atomic: ture})、MapReduceで重い処理をさせるケース(ロックはかからないが処理が重いので)が問題になる。
mongos
mongosサーバーは協調する複数のmongodサーバーがある場合、それを一つのクラスターとして扱うためのルーティングサーバーで、それ自体ではストレージする機能はない。
アプリケーションは複数あるmongodサーバーの中でどれががマスターでどれがスレーブなのかを知らなくてもmongosサーバに問い合わせればいい。
database
32bit環境ではDBサイズが2.5GBまで扱えないので、要64bit環境とのこと。
参考:32-bit limitations
BSON
データはBSONという形式で保存されている。データ型が複数あり、どのデータ型で保存されるのかはドライバーに依存する。
→ 例えば、PHPの場合はMongoInt32やMongoInt64などのデータ型用のクラスが用意されてる。
オブジェクト内の順序もみているので、順番が入れ替わるだけで別のオブジェクトととされてしまう。
→ { ‘key1’: ‘value1’, ‘key2’: ‘value2’ } ≠ { ‘key2’: ‘value2’, ‘key1’: ‘value1’ }
Capped Collection
サイズを指定してCollectionを作成するとCapped Collectionになる。
サイズが固定なのでサイズを越えた場合は古いものから自動的に削除される。
とにかく書き込みが速い。でも削除できない。
_idにすらindexがつかない。indexを張ることもできるが遅くなる(それでも速い方)。
shardingすることはできない。
用途 → ロギング(ディスクサイズを圧迫しない)
→ キャッシング
→ MapReduceの一時的なCollectinとして
Insert周り
Bulk Insert
複数のdocumentを配列に入れて一括でインサートする「Bulk Insert」が速い。
ただ、どのくらいのサイズまでが最適なのかはスペック依存で、経験上16MBを超えるとエラーが出るそう。
fire and forget
インサートはデフォルトではデータベースからの応答を待たない。
応答を確認するには、各種ドライバーでインサート時に応答を待つ処理を明示的にオプションとして指定する必要がある。
→ PHPではarray(‘safe’=>true)
トランザクション
RDBのように複数のオペレーションをトランザクション処理することは出来ないので、連続インサートする場合、途中でエラーになっても一連のインサートをロールバックすることは出来ない。
getLastErrorメソッド
db.getLastError();
でオペレーションの最後に起こったエラーがわかるので、このメソッドは重要。
fsyncコマンド
メモリ上のデータを強制的にストレージにフラッシュするコマンド。
db.runCommand({fsync:1});
fsyncは自動的に60秒に1回走るようになっていて、mongod起動時に、–syncdelayオプションで変更可能だが10genとしては非推奨とのこと。
つまり、最長60秒間に行われたデータの更新は喪失する可能性がある。
Journaling(v1.7.5)
オペレーション前にJournalingファイルに保存し、オペレーションに失敗した場合、次回実行される。
100ms毎にジャーナリングされる。つまり、100ms間のオペレーションは喪失する可能性がある仕様。
Query
printjson
シェル(mongo)でデータを確認するときには
db.collection.find().forEach(printjson);
とするとJSON形式にフォーマットされ見やすくなる。
入れ子
入れ子になったオブジェクトをサブオブジェクト形式でfindする場合は、完全に一致したオブジェクトでないと取得できない。
サブオブジェクト内のあるキーだけを検索対象としたい場合は、dot notation形式({author.name: ‘Jane’})で問い合わせる。
var doc = { title: 'タイトル', body: '本文', author: { first_name: '太郎', family_name: '山田'} }; db.collection.insert(doc); db.collection.find({author.first_name: '太郎'}); //◯ヒットする db.collection.find({author: { first_name: '太郎'} }); //×ヒットしない
Update
Update
デフォルトでは最初にヒットしたオブジェクトのみを更新する。
→ ヒットした全てを更新対象としたい場合は{ ‘multi’: true } オプションを付ける。
→ 複数の更新をアトミックに行うには { ‘$atomic’: true } オプションを付ける。
Save
同じ_idのオブジェクトがあれば自動的にドキュメント全体を更新する。
modifier オペレーション
アップデートのオプションとして$inc/$set/$push/$popなどのオペレーションがある。
modifierオペレーションを指定しないでアップデートを行うとオブジェクト(ドキュメント)をまるごと上書きするのでとても怖い。
in-place update
データサイズが変わらない更新の場合は高速。(例えば、{ ‘$inc’; { key: 1 } } など。)
また、データの更新を高速に行うため、インサートされた際に実データサイズよりも大きめにストレージを使って保存している。
→ 多少のサイズの増加も高速に更新出来る。逆に言うと無駄にストレージを使う。
Snapshot
MongoDBはデータサイズが大きく増えた場合、データベースファイルの新しい領域にデータを格納し直す。この瞬間にfind()すると同じドキュメントが2個取れたりする可能性がある。
find()で取り出した複数のドキュメントをupdate()するようなケースでは問題になるので、重複を排除して結果を返すsnapshot()を使う。
db.collection.find({name:'Jane'}).snapshot();
Indexについて
B-Tree indexes
インデックスを生成するオペレーションensureIndex()はデフォルトではDBをブロックする。
{ background: true } オプションを付けるとロックしないので、基本的にこれを必ず付けること。
db.collection.ensureIndex({name: 1}, {background:true});
参考:Indexing as a Background Operation
Sparse Index(v 1.7.4)
インデックスの設定されたフィールドを持っていないドキュメントは無視される。
→ コレクション全体的の中でSparse Indexに設定されたフィールドに値を持っているドキュメントがが少ない場合にパフォーマンスが出るインデックス。
Unique Index
インデックスを張ったキーが存在しないドキュメントを登録するとnullが挿入され、nullがユニークの対象となる。
→ インデックスを張ったキーが存在しないドキュメントをもう一つ登録すると重複エラーになる。
ただし、sharding環境では重複する可能性がある。
Replication
- Master-Slave:一般的なM/S構成
- Replica Sets:自動フェイルオーバ機能などを提供
Master-Slave
Master->Slaveは非同期でコピーされる。
mongodをそれぞれ、–master、 –slaveを付けて起動するだけ。
→ slaveには直接書き込みできない。(mongos経由)
同期は各データベースがそれぞれローカルに自動的に作成するoplog(operation log)コレクションを介して行われる。
- スレーブ(セカンダリ)はマスターのoplogを監視している。
- マスター(プライマリ)は更新系のオペレーションをoplogに書き込み、自分のデータベースに実行する。
- スレーブはマスターとの差分のoplogを自分のoplogへコピーし、そのオペレーションを自分のデータベースに実行する。
MySQLのバイナリログと同じようなイメージ。
Replica Sets
3台〜12台で構成でき、自動フェイルオーバー機能/自動リカバリ機能を持つ。
ノードタイプには、Standard(データあり。Primaryになれる。)、Passive(データあり。Primaryになれない。)、Arbiter(データなし。投票権はある。)がある。
フェイルオーバーは、起動時に設定するprimary値の高いもの > 最も新鮮なデータを持っているもの > 投票数の多いもの の順で順番付けされ、決定したセカンダリーがプライマリになる。
ダウンしているサーバーが多かったり、最終更新データが古すぎたりするとどのセカンダリーもプライマリにならないことがある。→ 手動で対応。
【発表2】ソーシャルアプリのプロトタイプ制作にMongoDBを活用
Sleepy.Mongoose
アプリケーションとデータベースの通信はSleepy.Mongoose(pythonで実装されたRestインターフェース)を利用して、curlで実装している。
→ Sleepy.Mongooseにはcount()が実装されていないのでforkしたものをGitHubに置いてあるとのこと。
bibrost / sleepy.mongoose
Cray Model
自動車開発のプロセスで粘土細工(Cray Model)で何度も何度も型を作ってから金型を作るように、プロトタイプの開発がしたい。
RDBのようにスキーマを定義して、チーム内で変更を共有して、不整合が起きないようにCREATE TABLEしたりALTER TABLEしたりするのは面倒。
MongoDBはアプリケーション上でいくらでもデータ構造を変更できるので、ソースさえ共有すればよく、途中でドキュメント構造が変わっても、手を止めることなくcollection名を変えるだけでいい。→まさに粘土細工のように失敗したら壊してまた作ればいい。
なぜ最終的にRDBにするのか
- MongoDBを保守できるエンジニアがいない。
- MongoDBをクリティカルなプロダクションで使うことが難しい(トランザクションがない)。
【発表3】MongoDBチューニング 〜スロークエリ撲滅までの道筋〜
オプティマイザ
RDBで多く採用されている一般的なオプティマイザは統計情報を元にしたコストベースで、本番環境と検証環境で統計情報が異なっていると実行計画が違ってきたり、悪い実行計画も行われる。
MongoDBのオプティマイザはコストベースではなく、新しいクエリー実行がある度に計算する仕様。(オーバーヘッド・CPUコストが高いが、最適な計画が行われる)
- 新しいクエリー実行時に複数の実行計画を並行に実施して一番速いのを採用・学習し、他のものは途中で止める。
- 100回データ更新が行われ時やindexが変更された時、最初の評価時からスキャンするドキュメントが大きく増えた場合(nscannedの値が10倍になったら)再評価を実施する。
explainメッソド
explain()で実行計画を確認できる。
db.collection.find(query).explain();
勘所としてはBasicCursorは全件スキャンなので×、millisが長いと処理時間が遅いので×、nscannedが多いとスキャン対象のドキュメント数が多いので×、など。
スロークエリの特定
→ ログファイルに保存される。
→ db.system.profileコレクションに保存されていく。(デフォルト100ms)
→ 現在実行中のクエリが見られる。
複合インデックス
順序が大事。
db.collection.find({x: 10}).sort({ y: 1});でも { x: 1, y: 1 }のインデックスが使われる。
hintメソッド
hintメソッドを使うと特定のインデックスを使わせることができる。
インデックス設定されていれば、
db.collection.find().hint({uid:1}); より db.collection.find().hint({uid:1, path:1}); の方がより絞られて高速になる。
mongostat
mongoやmongodなどと一緒にインストールされるmongostatコマンド。
ロックされている割合やインデックスが利用されなかった割合などの統計情報が確認できる。
killメソッド
db.killOp(opId)で実行中のクエリをkillできる。
【発表4】MongoDBを使用したモバイルゲーム開発
東京ガールズスナップの構成
Java(appサーバー2台)+ MongoDB(Replica Setで3台)で運用している。
MongoDBに対応したSpring Data Documentを利用して開発した。
MongoDBで良かったこと
階層型にデータが持てるのでkey-value的に使えた。
→ ドキュメント内に配列を持つことができるので、RDBのようなリレーションや連結などしなくても直接$pushや$popで複数のデータを出し入れできる。
バイナリデータが保存しやすい
当初の想定していた利用法ではなかったが画像データのキャッシュストレージとして利用できた。
→ 4MBまでだったらバイナリデータをそのまま格納できる。
MongoDBが問題になったこと
- オペレーションミスが大変なことになる(update時に$setで指定しないと全部上書きする)
- アプリ経由ではなくコンソールでデータを更新したらデータ型が変わってしまった(intがdoubleに)
- ドキュメント設計の最適化に時間がかかった(ベストプラクティスがわからない)