中年engineerの独り言 - crumbjp

LinuxとApacheの憂鬱

Scheme design in MongoDB.

非常に参考になるMongoDBのノウハウ集を和訳(&少々所感)しました。

内容はデータベースの基本に忠実です。
データサイズを圧縮し、レコードの移動を防ぎ、効率の良いクエリーを発行しろという事です。
突飛な手法では無いので理解し易い。

One of things that makes MongoDB easy to get started with is you don’t have to think about schema design -- just shove data in and it’ll let you query it. That helps initial development and has benefits down the line when you want to change your document structure. That said…

MongoDBを簡単に始められる理由の一つは”スキーマ設計を考えなくて良い”という事だ。単にデータを突っ込めば後はクエリーすれば良い。
これは開発初期の助けとなり、将来ドキュメントの構造を変更する時にも利点になる。だが

Remember, "schemaless" doesn't mean you don't need to design your schema! #mongodb

ただし、スキーマレスとはスキーマ設計をしなくて良いという意味では無い!

…so just like any database, to improve performance and make things scale, you still have to think about schema design. This has been well covered elsewhere (here, here and here) so here are some more in depth considerations to avoid the pitfalls of MongoDB schema design:

そう。普通のデータベースと同様、パフォーマンス向上やスケーラブルにする為にはやはりスキーマ設計を考えなければならない。
この事は他の記事でも言及されている(ここここここ)ので、本記事ではMongoDBにおけるスキーマ設計の落とし穴に関してより深く触れる。

1. Avoid growing documents

If you add new fields or the size of the document (field names + field values) grows past the allocated space, the document will be written elsewhere in the data file. This has a hit on performance because the data has to be rewritten. If this happens a lot then Mongo will adjust its padding factor so documents will be given more space by default. But in-place updates are faster.

You can find out if your documents are being moved by using the profiler output and looking at the moved field. If this is true then the document has been rewritten and you can get a performance improvement by fixing that (see below).

1. ドキュメントを成長させない。

既存のドキュメントに新たなフィールドを追加したり、サイズを大きくしたり(フィールド名+値)すると元の領域に収まらなくなり、ドキュメントをデータファイルの空き領域に移動しなければならなくなる。この処理はドキュメントを書き直す為パフォーマンスに影響する。更にこの処理が大量に走るとMongoDBはドキュメントに対する余白(パディング)割合を調整する。よってドキュメントはより多くの領域を消費するようになってしまう。しかし元領域内に収まるアップデートは高速である。

プロファイラを使う事でドキュメントが移動された事を検知できる。もしmovedフィールドがtureの場合ドキュメントは書き直(移動)されたという事だ。これを是正する事でパフォーマンス改善の余地がある。(後述)

MongoDBはドキュメントをディスクに格納する際ある程度のパディングをする。これにより将来のドキュメント更新によって若干のサイズ増加があったとしてもin-place updateにする事ができる。
パディング量はドキュメントサイズに対する割合で例えば1.001の様に設定されている。
上記の通りアップデートによるドキュメント移動が多いとこの値が大きくなっていく(1.0〜2.0,default:1.0 =nopad)。もしこの値が2.0であるとドキュメントを格納する際はその2倍の領域を必要とするようになる。
当然こうなるとディスクやメモリーの消費が効率的ではなくなりパフォーマンスが落ちる。

以前はこの値は単調増加で全く制御できなかったが、2.2からはcompact,各種ツールなどで指定できるようだ。また2.4のソースからはアップデートの際に縮小方向にも調整が入ってるように見える。
(あまり気にしなくて良くなったかもしれない。→要検証)

#FF0000">プロファイルの例:
// このDBのプロファイル(レベル2)を有効にする
PRIMARY> db.setProfilingLevel(2);
// フィールド追加
PRIMARY>db.testcol.update({_id:ObjectId("514ac4666bff1b5721ca1bc1")},{$set :{ value3: "a" }})
// プロファイル結果はsystem.profileに格納されている
PRIMARY> db.system.profile.find({op:'update'})
{
    "op" : "update", 
    "ns" : "testdb.testcol",
    "query" : { "_id" : ObjectId("514ac4666bff1b5721ca1bc1") },
    "updateobj" : { "$set" : { "value3" : "a" } },
    "idhack" : true, 
    "moved" : true,
    "nmoved" : 1,
    "nupdated" : 1,
    "keyUpdates" : 0,
    "numYield" : 0,
    "lockStats" : { "timeLockedMicros" : { "r" : NumberLong(0), "w" : NumberLong(394) }, "timeAcquiringMicros" : { "r" : NumberLong(0), "w" : NumberLong(8) } },
    "millis" : 0,
    "ts" : ISODate("2013-03-22T04:30:35.504Z"),
    "client" : "192.168.159.142", "allUsers" : [ { "user" : "crumb", "userSource" : "admin" } ], "user" : "crumb@admin" 
}
#FF0000">paddingFactorの増減:確定。毎回のupdate毎に評価されちょうど良い値に調整されている模様。
またcompactで指定できるpaddingFactorはcompact用の一時的なもので、上記のpaddingFactorとは関係なくpaddingを行う。また範囲も1.0〜4.0である。
頭が良くなった反面、予測が難しくなったかな?
2. Use field modifiers

One way to avoid rewriting a whole document and modifying fields in place is to specify only those fields you wish to change and use modifiers where possible. Instead of sending a whole new document to update an existing one, you can set or remove specific fields. And if you’re doing certain operations like increment, you can use their modifiers. These are more efficient on the actual communication between the database as well as the operation on the data file itself.

2.フィールド修飾子(オペレーター)を使え。

ドキュメント全体の更新をせずにフィールド指定を使い必要なフィールドだけを更新する事ができる。新しいドキュメント全体を転送する代わりに更新したいフィールドにsetremoveの修飾子を指定できる。更にincrementの様な特定の処理を行うオペレーターも幾つかある。これらはデータベース通信だけでなくデータファイル自体の処理の面でも非常に効率的である。

3. Pay attention to BSON data types

A document could be moved even by changing a field data type. Consider what format you want to store your data in e.g. if you rewrite (float)0.0 to (int)0 then this is actually a different BSON data type, and can cause a document to be moved.

3.BSONデータ型に留意する。

ドキュメントはフィールドのデータ型を変更しただけでも移動される場合がある。データを保存する際にはそのフォーマットも考慮する必要がある。例えば(float)0.0を(int)0とした場合、BSON data typeは異なりドキュメントの移動を引き起こす場合がある。

4. Preallocate documents

If you know you are going to add fields later, preallocate the document with placeholder values, then use the $set field modifier to change the actual value later. As noted above, be sure to preallocate the correct data type -- beware: null is a different type!

However, trigger the preallocation randomly because if you’re suddenly creating a huge number of new documents, that too will have an impact e.g. if you create a document for each hour, you want to do them in advance of that hour balanced over a period of time rather than creating them all on the hour.

4.ドキュメントをプリアロケート(事前確保)する。

もしドキュメントのフィールドを後から追加する事が解っているなら、仮の値でプリアロケートしておき、後から$setオペレーターで実際の値に更新する事でドキュメントサイズ増加を防げる。この時、以上の様にデータ型も留意しなければならない。特にnullは別データ型である!

しかし、プリアロケートのタイミングはランダムに行わなければならない。突然大量のドキュメントinsertが起きるとシステムの負担になるだろう。毎時に分けるとか、時間的にバランスを取りながら行うとよいでしょう。

ここでは1ドキュメントのフィールドをプリアロケートするだけでなく
これから使うであろうレコードもある程度プリアロケートしておけ。
と言っているようだ
確かにピーク時間帯に書き込みが限界だ。とかデータファイルの追加(2GB)の突然のI/Oが苦しい。
とかは考えられるが、、正直あまり困った事は無いな。。
ただ(私の環境でも)新規insertより(in-place)updateの方が(全フィールドを更新したとしても)倍近く早いので困ってる方は試す価値がありそう。

5. Field names take up space

This is less important if you only have a few million documents but when you get up to billions of records, they have a meaningful impact on your index size. Disk space is cheap but RAM isn’t, and you want as much in memory as possible.

5.フィールド名も容量を食う

数百万程度のレコード数の場合、これはあまり重要ではない。しかし数十億のレコード数を扱う場合はインデックスサイズに考慮すべきインパクトがある。ディスク容量は良いとしてもメモリーは違う。データは出来る限りメモリー上に載る様にしたい。

6. Consider using _id for your own purposes

Every collection gets _id indexed by default so you could make use of this by creating your own unique index. For example if you have a structure based on date, account ID and server ID like we do with our server monitoring metrics storage for Server Density, you can use that as the index content rather than having them each as separate fields. You can then query by _id with the single index instead of using a compound index across multiple fields.

6._id を有効活用する事

全てのコレクションは_id のインデックスを持っている。よってコレをアプリケーション用途のユニークインデックスとして活用出来る。例えばServer Density用のサーバ監視情報として日付、アカウントID、サーバIDの様な構造を持つコレクションを考えた場合、ドキュメントを特定する為にはこれらのフィールドの複合インデックス(日付,アカウントID,サーバID)の代わりに_idを使ってクエリーできる。

当たり前っちゃ当たり前なのだが、アプリ用途にも_idを積極的に使いなさい。という事です。
耳が痛い・・・

7. Can you use covered indexes?

If you create an index which contains all the fields you would query and all the fields that will be returned by that query, MongoDB will never need to read the data because it’s all contained within the index. This significantly reduces the need to fit all data into memory for maximum performance. These are called covered queries. The explain output will show indexOnly as true if you are using a covered query.

7.coverd indexesを使ってますか?

もしクエリーを発行した時、利用したインデックスに全ての返却フィールドが含まれていた場合、クエリーはインデックス参照で完結しMongoDBはデータファイルを読む必要が無い。これはオンメモリーに載せるデータ量を劇的に減らすのでパフォーマンスを最大化できる。これはcovered queriesと呼ばれる。covered queriesが使われた場合、explainの結果がindexOnly=trueになる。

一般的なRDBMSでも同様の機能があるのでMongoDBが特別な訳ではなくむしろ一般的な手法。
積極的に使うべき

8. Use collections and databases to your advantage

You can split data up across multiple collections and databases:

Dropping a whole collection is significantly faster than doing a remove() on the documents within it. This can be useful for handling retention e.g. you could split collections by day. A large number of collections usually makes little difference to normal operations, but does have a few considerations such as namespace limits.
Database level locking lets you split up workloads across databases to avoid contention e.g. you could separate high throughput logging from an authentication database.

8.コレクションやデータベースを上手に使う。

複数のコレクションやデータベースにデータを分割することを考慮する。

コレクションのdropは全ドキュメントremoveより非常に早い。これはデータ保持期間のコントロールに役立つ。例えば日毎にコレクションを分け、大量のコレクションを扱うとしても、普通、処理内容はあまり変わらない。ただ名前空間の制限など幾つか注意が必要な項目はある。
データベースレベルロックを考慮しデータベースを分ける事で、ワークロードの競合を避ける事ができる。例えばハイ・スループットのロギングを認証DBと分ける事ができる。

#FF0000">全体的にやや解り難く乱暴な説明:日毎にコレクションを分けておくと、保持期間が切れたドキュメントを削除する処理はコレクションdropで済み高速化できる。コレクションが増えて複雑化する分は大した事ないからアプリでやりなさいと。。。
#FF0000">名前空間の制限:--nssizeオプションで名前空間のデータファイルサイズが指定できる。このサイズはデータベース内に保持できるコレクション数に直結する。
普通初期値の16Mで充分(私は4Mで使ってる)だが数万のコレクションを作りたいならチューニングが必要
#FF0000">データベースレベルロック:本文の認証DBの例は解り辛い。。認証DB(admin)をロックしたら全部のクエリーが止まるだろ!と言いたいのか?

MongoDB2.2からはグローバルロック(mongod全体ロック)からデータベースレベルロック(特定DBの処理をロック)に変更された。
解りやすい例だと、compactコマンドやrepairDatabaseなどのコマンドでディスク領域の掃除をしたい場合、発行したデータベースは掃除が終わるまでロックされる。
しかし別データベースの処理はロックの影響を受けない。
他にもデータファイルの追加の際のロックなど色々考慮すべきロックのタイミングはある。
#FF0000">compact:あれ?いつの間にかDBロックしなくなった模様。。

Test everything

People misinterpret @mongodb scalability. It's not easy per se, it's just easier. Still requires thought, understanding and testing

Make good use of the system profiler and explain output to test you are doing what you think you are doing. And run benchmarks of your code in production, over a period of time. There are some great examples of problems uncovered with this in this schema design post.

全部テストしなさい。

多くの人がMongoDBのスケーラビリティを誤解している。それ程簡単なものではない。よく考え、理解し、テストしなければわかるまい。

プロファイラやexplanを活用し、思った通りに動くかテストしなさい。それと本番で一定期間自分のコードのベンチマークを取ること。
ココにはとても参考になる問題やスキーマ設計の例があります。

[原文MongoDB schema design pitfalls