Edited at

MongoDB 3.0のWired Tigerのデータ圧縮とメモリ使用量制限を検証してみた

More than 3 years have passed since last update.

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はオフ)