MongoDB 3.0から従来のmmapベースのストレージエンジンから、Wired Tigerというストレージエンジンが使えるようになった。
主な変更点は以下のとおり。
- mmapベース
- MongoDB ver 3にはMongoDB ver 2にあったmmapベースに改良を加えたものが乗っている
- MongoDBはOSのmmap()システムコールを読んで、OSにデータファイルをメモリに載せてもらう。
- MongoDBはメモリを管理しない。OS任せ。
- メモリ使用量を制限できない。OSから与えられるだけ使う。
- メモリがいっぱいになるとOSが適宜スワップさせる
- データ圧縮できない
- コレクションレベルロック(ver 2のころはデータベースレベルロック)
- インデックスとデータは分離できない(同一ファイル内に混在)
- Wired Tiger
- MongoDB ver 3からの新規
- Wired TigerというストレージエンジンのOSSを組み込んでいる。
- 自分でメモリを管理する
- メモリ使用量制限もできる
- メモリがいっぱいになるとWired Tigerが適宜スワップせせる。
- データ圧縮できる
- ドキュメントレベルロック
- インデックスとデータを分離できる
今回は、メモリ使用量制限とデータ圧縮について調べる。
WiredTigerを指定して起動する方法
簡単に立ち上げるのであれば、mongodにオプションを渡せばOK
# ./bin/mongod --storageEngine wiredTiger
細かく設定したいのであれば、以下の様な設定ファイル(yaml)を作りmongod
に-f
で食わせて起動。
設定ファイル例
storage:
dbPath: data //データディレクトリのパス
engine: wiredTiger //ストレージエンジンの名称
journal:
enabled: false //journal(先行書き込みログ)を有効にするかどうか
wiredTiger:
engineConfig:
cacheSizeGB: 1 //キャッシュサイズの上限
statisticsLogDelaySecs: 0 //稼働統計ログをどれ位遅延して出すか
journalCompressor: none //journalの圧縮有無
directoryForIndexes: false //インデックスごとにディレクトリを分けるか
collectionConfig:
blockCompressor: none //コレクションの圧縮 snappy と zlibが指定可能
indexConfig:
prefixCompression: false //インデックスのプレフィックスの圧縮
メモリ使用量制限の検証
storage.wiredTiger.engineConfig.cacheSizeGB
によりキャッシュサイズを制限できる。これ以上はメモリを使わなくなるということだろう。
storage.wiredTiger.engineConfig.cacheSizeGB
を1GByteに設定して、MongoDBに適当にラッシュをかけてみる。
//100万文字(≒1M)の文字列を生成
> longstr = ""
> for(var i=0;i<100000;i++){longstr+="0123456789"}
//それを1万回(トータル10Gぐらい)挿入
> for(var i=0;i<10000;i++){db.hoge.insert({a:longstr})}
> db.stats()["dataSize"]
10000300000 //10Gぐらい入った
この時点では物理メモリ使用量は172Mでしかなく、設定値が有効かどうかのテストにはなっていないため、適当に中身を検索してメモリに載せていく。
メモリ載せるためにドキュメントを片っ端から検索していく。find().forEach()を使って中身のvalueのサイズを表示させる。
> db.hoge.find().forEach(function(doc){ print(doc["a"].length);} );
以下はmongostatの抜粋だが、ご覧のとおり、途中でメモリ使用率が頭打ちになっていることがわかる。これは今まではなかった動きだ。
(mongostat抜粋)
getmore command % dirty % used flushes vsize res
0 1|0 0.0 1.3 0 476.0M 172.0M
9 2|0 0.0 5.3 0 476.0M 177.0M //クエリ開始
12 1|0 0.0 11.3 0 492.0M 232.0M //物理メモリ使用量が増えていく
21 2|0 0.0 21.9 0 596.0M 332.0M
20 1|0 0.0 32.0 0 692.0M 428.0M
32 1|0 0.0 48.2 0 844.0M 583.0M
24 2|0 0.0 60.3 0 956.0M 697.0M
19 1|0 0.0 69.9 0 1.0G 791.0M
17 2|0 0.0 78.5 0 1.1G 873.0M
19 1|0 0.0 76.6 0 1.1G 893.0M
23 1|0 0.0 78.7 0 1.1G 903.0M //900Mで頭打ちになった。
24 2|0 0.0 70.6 1 1.1G 903.0M
24 1|0 0.0 72.6 0 1.1G 902.0M
23 2|0 0.0 74.6 0 1.1G 902.0M
ちなみに、設定値を2Gにしてみた結果は以下のとおり
(mongostat抜粋)
getmore command % dirty % used flushes vsize res
4 2|0 0.0 1.2 0 297.0M 73.0M //クエリ開始
24 1|0 0.0 7.3 0 417.0M 189.0M
26 2|0 0.0 13.8 0 537.0M 313.0M
29 1|0 0.0 21.2 0 681.0M 453.0M
22 1|0 0.0 26.7 0 785.0M 559.0M
22 2|0 0.0 32.3 0 889.0M 665.0M
27 1|0 0.0 39.1 0 1017.0M 795.0M
21 2|0 0.0 44.4 0 1.1G 895.0M
19 1|0 0.0 49.2 0 1.2G 987.0M
23 1|0 0.0 54.8 0 1.3G 1.1G
27 2|0 0.0 61.8 0 1.4G 1.2G
28 1|0 0.0 68.9 0 1.6G 1.3G
16 2|0 0.0 72.8 0 1.6G 1.4G
31 1|0 0.0 70.7 0 1.8G 1.6G //1.6Gで頭打ちになった
40 1|0 0.0 78.1 0 1.8G 1.6G
38 3|0 0.0 75.5 0 1.8G 1.6G
38 1|0 0.0 79.9 0 1.8G 1.6G
ご覧のとおり、1.6Gで頭打ちになった。
今回の実験結果をまとめると
- 1Gに設定すると、メモリ使用量は900Mで頭打ち
- 2Gと設定すると、メモリ使用量は1.6Gで頭打ち
となる。設定値と頭打ちの値が微妙にずれている理由はわからずでした。。。
データ圧縮の検証
storage.wiredTiger.collectionConfig.blockCompressor
のオプションで、コレクションの圧縮ができる。デフォルト値はsnappy
で、snappyアルゴリズムによる圧縮・伸長が行われる。このアルゴリズムはgoogleが開発したもので、圧縮率はそこそこだが圧縮・伸長速度が速いらしい。他にもおなじみのzlib
が選択できる。また、圧縮しないnone
も選択可能だ。
データ圧縮率と書き込み性能を計測
3つの圧縮方式ごとに、キャッシュ制限の検証と同じクエリ(10G程度の挿入)を行ってみた。結果は以下のとおり。
表1:10G程度挿入した時のデータ容量と挿入時間
圧縮方式 | データ容量(byte) | 挿入時間(sec) |
---|---|---|
none | 10,038,214,656 | 170 |
snappy | 491,556,864 | 109 |
zlib | 41,123,840 | 286 |
ご覧のとおり何も圧縮しないnone
は10Gであるのに対し、snappy
は490Mに圧縮されている。挿入にかかった時間も短い。一方zlib
は41Mとかなり圧縮されているが、挿入にかかる時間は長い。
よってまとめると、
-
none
: 圧縮せずに、挿入は普通 -
snappy
: それなりに圧縮して、挿入が速い※ -
zlib
: かなり圧縮して、挿入が遅い
となる。前評判通りだ。
※(2015/9/12追記) 「挿入が速い」と記載したが、必ずしもそうではない。当然圧縮する方が余分に計算するため、計算時間はかかる。しかし、その分データ量が減ってディスクIOは減ることによりIO時間は減る。この二つのバランスによって挿入の速さが決まる。今回は文字列という圧縮しやすいデータでありかなりデータ圧縮ができたため、ディスクIO削減による時間削減が圧縮の計算時間よりも大きかったため、トータルで速かったと思われる。
読み込み性能を計測
また、読み込みについても調査してみた。MongoDBを止めて一旦メモリを空にしてから、下記のクエリを用いて約5Gのデータをメモリに載せるまでの時間を計測した。
//1Mのドキュメントを5000個メモリに載せる
db.hoge.find().limit(5000).forEach(function(doc){ print(doc["a"].length);} );
結果は以下のとおり
表2:約5Gのデータをメモリに載せるまでの時間
圧縮方式 | 読み込み時間 |
---|---|
none | 62秒 |
snappy | 53秒 |
zlib | 59秒 |
予想通り、snappyが最も高速となった。しかし大きな差ではないため、データ伸長の時間が支配的ではないのかもしれない。
ちょっと自信はないが、まとめると
-
none
: ディスクからメモリへの読み込みは普通 -
snappy
: ディスクからメモリへの読み込みがやや早い -
zlib
: ディスクからメモリへの読み込みがやや遅い
ということなのだろう。
また、上記の実験では、どの方法でも利用するメモリ量は等しかった。
つまり、 ディスク上のデータが圧縮されたからといって、メモリ使用量が減るわけではない のでご注意。
テストした環境
- OS: CentOS release 6.2 (on CentOS 7.0 KVM)
- RAM: 14G 割り当て
- CPU: Core i5 660 3.33GHz 4 Core割り当て
- FS: ext4
- MongoDB 3.0.4 (journalはオフ)