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

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`も選択可能だ。

### データ圧縮率と書き込み性能を計測

３つの圧縮方式ごとに、キャッシュ制限の検証と同じクエリ(10G程度の挿入)を行ってみた。結果は以下のとおり。

表１：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);} );
```

結果は以下のとおり

表２：約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はオフ)