MongoDB 2.4 の性能 徹底評価
まとめ
超長くなったのでまとめを上に持ってきた。
巷で言われているチューニングは結構嘘が多い事が解ってきた。
ツール等
workingSet Analyzer は信用ならない。(overSecondsはまあ良い)
mongoperfの値は完全に参考にならない。
insert
mongoperfの値はinsert性能と関連しない。(何を測ってるんだ?)
カラムのプリアロケーションによるUPDATE時のデータ肥大化回避($setOnInsert)はMUST。
クリティカルな時間帯にストレージファイル(2GB)の生成を避けるチューニングの効果は懐疑的。
レコードプリアロケーション・チューニングは頑張る価値が無い。(むしろ逆効果)
update
上記の通り必ずin-placeになるようにする。
paddingFactorが動くようだとお話にならない性能劣化
remove
かなり高速。
全件削除の場合はdrop()なら一瞬。(データファイルは残る)
dropDatabase()はデータファイル削除を伴うのでそれなりに時間が掛るが気にする程でもない。
access
オンメモリデータに触れば高速。
シーケンシャルアクセスならディスクアクセスでもそれ程の性能劣化は無い。
ランダムアクセスのディスクアクセスでは明らかな性能劣化。
covered indexはかなり高速。(mmap領域のスワップアプトも起こし難いので更に良い)
index
インデックス張り。
高速だが相変わらずコレクションをロックするので注意。(backgroundも意味なし)
Background Construction
Building Indexes on Secondaries
Background index operations on a replica set primary become foreground indexing operations on secondary members of the set. All indexing operations on secondaries block replication.
1.8の頃から指摘してるんだけどなぁ・・・
プライマリだけバックグラウンドになっても何も嬉しくない!
どういう発想するとこうなるの?
group
安心して使えない・・・
db.testcol.group( { key :{ } , cond : { _id:{$lt : 10000000 , $gte : 0 } }, reduce: function(o,p) { p.value0 += o.value0 }, initial: {value0:0}})
こんな事するとmongodが死ぬ。
MongoDBで集計はまだまだ無謀・・・
検証内容
環境
Sakura VPS 2G
CPU (3 core)
$ cat /proc/cpuinfo | grep -e processor -e 'model name' -e 'cpu MHz' -e 'cores' processor : 0 model name : Intel(R) Xeon(R) CPU E5645 cpu MHz : 2400.068 cpu cores : 1 processor : 1 model name : Intel(R) Xeon(R) CPU E5645 cpu MHz : 2400.068 cpu cores : 1 processor : 2 model name : Intel(R) Xeon(R) CPU E5645 cpu MHz : 2400.068 cpu cores : 1
Memory (2GB)
$ cat /proc/meminfo | head -n 2 MemTotal: 1922440 kB MemFree: 74460 kB
IO speed (write : 189 MB/s)
$ dd if=/dev/zero of=foo count=4000000 4000000+0 records in 4000000+0 records out 2048000000 bytes (2.0 GB) copied, 10.2278 s, 200 MB/s
mongoperf
MongoDB 付属のプロファイルツールでシステムの参考値を得る事ができます。
が、結論としてはなんの参考にもならない
ディスク書き込み性能 (10MB/sec)
書き込みは何スレッドで書いても一緒なのでスレッド数は1で計測
$ ./bin/mongoperf <<<'{ w:true, nThreads:1, fileSizeMB:2048 }' <略> 3121 ops/sec 12 MB/sec 3076 ops/sec 12 MB/sec 2648 ops/sec 10 MB/sec 2527 ops/sec 9 MB/sec 2546 ops/sec 9 MB/sec 2487 ops/sec 9 MB/sec 2859 ops/sec 11 MB/sec 2813 ops/sec 10 MB/sec 2531 ops/sec 9 MB/sec 2492 ops/sec 9 MB/sec
ディスク読み込み性能 (不明・・・参考値として 20MB /sec としておく)
明らかに値が変、何かの間違いな気がする。
./bin/mongoperf <<<'{ r:true, nThreads:1, fileSizeMB:2048 }' <略> 224 ops/sec 0 MB/sec 236 ops/sec 0 MB/sec 247 ops/sec 0 MB/sec 222 ops/sec 0 MB/sec 231 ops/sec 0 MB/sec 233 ops/sec 0 MB/sec
ファイルサイズを減らしたらいい感じの値になった。
プロファイラのバグじゃなかろうか?
./bin/mongoperf <<<'{ r:true, nThreads:1, fileSizeMB:128 }' <略> 5136 ops/sec 20 MB/sec 5126 ops/sec 20 MB/sec 4856 ops/sec 18 MB/sec 4830 ops/sec 18 MB/sec 4848 ops/sec 18 MB/sec
マップファイル経由で書き込む速度 (160MB/sec)
ddの速度に近い。
mmapが巧く働き、ハードの限界性能を引き出していることを示している。
./bin/mongoperf <<<'{ w:true, mmf:true, nThreads:1, fileSizeMB:1024 }' <略> 61514 ops/sec 59479 ops/sec 77352 ops/sec 87438 ops/sec 75498 ops/sec 63869 ops/sec
裏からiostat で書き込みバイトを監視する。
<略> Device: tps MB_read/s MB_wrtn/s MB_read MB_wrtn vda 20271.00 0.00 164.79 0 164 vda 19943.00 0.00 161.03 0 161 vda 20755.00 0.00 168.41 0 168
mongod
扱うデータ
- レコード数
- 1000万
- レコードサイズ
- 4kb
- カラム数
- 11
for i in {0..10000000}; do echo \{\"_id\":$i, \ \"value1\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa$i\",\ \"value2\":\"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb$i\",\ \"value3\":\"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc$i\",\ \"value4\":\"ddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddeeeeeeeeeeeeeeeeeeeeeeeeeeee$i\",\ \"value5\":`expr $i % 40`,\ \"value6\":\"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff$i\",\ \"value7\":\"gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg$i\",\ \"value8\":\"hhhhhhhhhhhh$i\",\ \"value9\":\"iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii$i\",\ \"value0\":$i\} done
- _id , value5 , value0 が数値データ
- _id と value0 は同じ値
- value5 は _id % 40 でランダムアクセスや非ユニークインデックスの検証に使う。
insert系
全件insert
- 時間
- 960 sec = 16 min
- 単位時間辺りレコード
- 10417 rec / sec
- 単位時間辺りサイズ
- 41 MB /sec
mongoperfの値は参考にならない事が解った
ファイル生成が起こらない全件insert
- 時間
- 1125 sec = 19 min
- 単位時間辺りレコード
- 8889 rec / sec
- 単位時間辺りサイズ
- 35 MB /sec
事前にファイル(storageSize)がある場合ファイル生成コスト分だけ早くなる筈がほんの少し遅くなった。
ただ、数回計測してはいるが、検証環境がVPS(非占有マシン)なので断定はできない。
インサート前の状態。
count = 0
storageSize = 43GB
db.testcol.stats() { "ns" : "test.testcol", "count" : 0, "size" : 0, "storageSize" : 43026657008, "numExtents" : 41, "nindexes" : 1, "lastExtentSize" : 2146426864, "paddingFactor" : 1, "systemFlags" : 1, "userFlags" : 0, "totalIndexSize" : 8176, "indexSizes" : { "_id_" : 8176 }, "ok" : 1 }
プリアロケート&upsert (insertの代わり)
- 時間
- 1239 sec = 20 min
- 単位時間辺りレコード
- 8071 rec / sec
- 単位時間辺りサイズ
- 31 MB / sec
schema design pitfallsの4. Preallcate decuments の手法。
インサートは事前にやっておき、レコード生成をアップデート処理に置き換えて速度を稼ぐ方法。
全然効かない。。。むしろ遅い。。
プリアロケート状態 > db.testcol.stats() { "ns" : "test.testcol", "count" : 10000001, "size" : 40800004080, "avgObjSize" : 4080, "storageSize" : 43026657008, "numExtents" : 41, "nindexes" : 1, "lastExtentSize" : 2146426864, "paddingFactor" : 1, "systemFlags" : 1, "userFlags" : 0, "totalIndexSize" : 479824912, "indexSizes" : { "_id_" : 479824912 }, "ok" : 1 } upsert後 > db.testcol.stats() { "ns" : "test.testcol", "count" : 10000001, "size" : 40800004080, "avgObjSize" : 4080, "storageSize" : 43026657008, "numExtents" : 41, "nindexes" : 1, "lastExtentSize" : 2146426864, "paddingFactor" : 1, "systemFlags" : 1, "userFlags" : 0, "totalIndexSize" : 479824912, "indexSizes" : { "_id_" : 479824912 }, "ok" : 1 }
update系
全件(in-place)1カラムupdate
- 時間
- 646 sec
- 単位時間辺りレコード
- 15480 rec / sec
- 単位時間辺りサイズ
- 60 MB / sec
db.testcol.update({},{$set:{value0:"3"}},{multi:true})
ディスクから読んで書く処理になる。
プロファイラの結果から書き込み速度(10MB /sec)+読み込み速度(20MB /sec)に近くなると予想していたが
遥かに早かった。
全件処理でシーケンシャルread & write になった事が要因か?
dd書き込み速度の1/3は優秀と言えるが、そもそもNoSQLで全件アップデートをしたら負けとも言う・・・
Device: tps MB_read/s MB_wrtn/s MB_read MB_wrtn vda 1220.00 105.62 182.12 105 182 vda 1174.00 134.38 40.00 134 40 vda 1126.00 107.88 108.00 107 108 vda 885.00 85.75 84.01 85 84 vda 916.00 88.75 88.00 88 88 vda 1176.00 96.12 196.12 96 196 vda 1178.00 136.12 36.00 136 36
- paddingFactor
- in-placeアップデートの場合は1になります。
db.testcol.stats().paddingFactor 1
- workingSet Analyzer は信用ならない・・・
- 6370207ページ = 24GB
ページを重複カウントしてるとしか思えない。
db.serverStatus({workingSet:1}).workingSet { "note" : "thisIsAnEstimate", "pagesInMemory" : 6370207, "computationTimeMicros" : 3561940, "overSeconds" : 31 }
全件(in-place)10カラムupdate
- 時間
- 737 sec
- 単位時間辺りレコード
- 13569 rec / sec
- 単位時間辺りサイズ
- 53 MB / sec
1カラムupdateより若干遅いがIO総量は変わった様に見えない。
明らかにCPU負荷が高くなるのでドキュメント更新処理の分か。
db.testcol.update( {}, { $set:{ value0:"", value1:"", value2:"", value3:"", value4:"", value5:"", value6:"", value7:"", value8:"", value9:""}}, {multi:true})'
全件(relocation)1カラムアップデート
- 時間
- 3905 sec = 65 min
- 単位時間辺りレコード
- 2564 rec / sec
- 単位時間辺りサイズ
- 22 MB / sec
レコード移動とストレージスペース肥大化でダブルパンチ。
実に6倍もの性能劣化。
これを起こしてしまったらお話にならない。
db.testcol.update({},{$set:{value0:"ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ"}},{multi:true})
update前 > db.tsetcol.stats() { "ns" : "test.tsetcol", "count" : 10000001, "size" : 40800004080, "avgObjSize" : 4080, "storageSize" : 43026657008, "numExtents" : 41, "nindexes" : 1, "lastExtentSize" : 2146426864, "paddingFactor" : 1, "systemFlags" : 1, "userFlags" : 0, "totalIndexSize" : 479824912, "indexSizes" : { "_id_" : 479824912 }, "ok" : 1 } update後 > db.tsetcol.stats() { "ns" : "test.testcol", "count" : 10000001, "size" : 90872101360, "avgObjSize" : 9087.209227279078, "storageSize" : 133176585296, "numExtents" : 84, "nindexes" : 2, "lastExtentSize" : 2146426864, "paddingFactor" : 1.9970000002930703, "systemFlags" : 1, "userFlags" : 0, "totalIndexSize" : 742560672, "indexSizes" : { "_id_" : 251583696, "value5_1" : 490976976 }, "ok" : 1 }
- paddingFactor
- ほぼ最大値(=2)
確実にリロケーションが起きている。 - storageSize
- 結果、実データ量の2倍の領域を使ってしまった。
全件削除(remove)
- 時間
- 503 sec
- 単位時間辺りレコード
- 19881 rec / sec
- 単位時間辺りサイズ
- 77 MB / sec
db.testcol.remove()
これはあくまでremoveの性能測定。
全件削除の場合はdrop()なら一瞬。
db.testcol.drop()
data access系
ディスクデータにアクセスする速度(転送無し)
- 時間
- 20 sec
- 単位時間辺りレコード
- 12500 rec / sec
- 単位時間辺りサイズ
- 48 MB / sec
まずはデータ転送に関わらない性能を測る為groupを使う
4080 * 250000件 = 972MB (約1GB) のデータを扱う。
メモリーに乗ってないデータ域を1GB程度アクセス
db.testcol.group( { key :{ } , cond : { _id:{$lt : 9500000 , $gte : 9000000 } }, reduce: function(o,p) { p.value0 += o.value0 }, initial: {value0:0}})
ちなみに件数が多いとmongodが死にます。
db.testcol.group( { key :{ } , cond : { _id:{$lt : 10000000 , $gte : 0 } }, reduce: function(o,p) { p.value0 += o.value0 }, initial: {value0:0}})
怖くて使えない・・・
ディスクデータにアクセスする速度(転送有り)
- 時間
- 21 sec
- 単位時間辺りレコード
- 11905 rec / sec
- 単位時間辺りサイズ
- 46 MB / sec
- 転送データ量
- 5.5MB
mongoシェル(クライアント側)のCPU使用率が振り切れてるので正確ではないかもしれないが
上記groupの結果と合わせて考える。
DBQuery.shellBatchSize=250000 db.testcol.find({ _id:{$lt : 9250000 , $gte : 9000000 }},{_id:0,value0:1})
iostatによると5秒程度で必要なデータを読み切っている事が解る。
(オンメモリデータアクセスと比べてのオーバヘッドが1GBで5秒程度という事)
但しこれは連続域にアクセスしているのでディスクをシーケンシャルリードしている。
ランダムアクセスすれば相当性能劣化するはず。
Device: rrqm/s wrqm/s r/s w/s rMB/s wMB/s avgrq-sz avgqu-sz await svctm %util vda 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 vda 0.00 0.00 1278.00 1.00 159.63 0.00 255.62 0.86 0.68 0.57 72.80 vda 0.00 0.00 1329.00 0.00 166.11 0.00 255.98 0.82 0.62 0.53 69.90 vda 0.00 0.00 1283.00 0.00 160.50 0.00 256.20 0.81 0.63 0.56 72.20 vda 0.00 0.00 1395.00 0.00 174.25 0.00 255.82 0.89 0.64 0.56 78.60 vda 0.00 0.00 1430.00 0.00 178.88 0.00 256.18 0.87 0.61 0.53 75.10 vda 0.00 0.00 955.00 2.00 119.25 0.01 255.21 0.51 0.54 0.46 44.10 vda 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 vda 6.00 0.00 2.00 0.00 0.03 0.00 32.00 0.01 4.00 2.00 0.40 vda 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 vda 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 vda 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 vda 0.00 1.00 0.00 2.00 0.00 0.01 12.00 0.00 0.50 0.50 0.10
オンメモリデータにアクセスする速度(転送有り)
- 時間
- 18 sec
- 単位時間辺りレコード
- 13889 rec / sec
- 単位時間辺りサイズ
- 54 MB / sec
- 転送データ量
- 5.5MB
上記スクリプトをもう一度実行
オンメモリ(coverd indexes)データにアクセスする速度
- 時間
- 15 sec
- 単位時間辺りレコード
- 16667 rec / sec
- 単位時間辺りサイズ
- データには触らない。
7. Can you use covered indexes?
_idにしか触らないので、適用される。
流石に速い。
DBQuery.shellBatchSize=250000 db.testcol.find({ _id:{$lt : 9250000 , $gte : 9000000 }},{_id:1})
ディスクデータにアクセスする速度(転送有り)(ランダムアクセス)
- 時間
- 224 sec
- 単位時間辺りレコード
- 1116 rec / sec
- 転送データ量
- 5.5MB
DBQuery.shellBatchSize=250000 db.testcol.find({ value5:0 },{value0:1,_id:0})
value5カラムは % 40 なので40行飛ばしたアクセスになり、シーケンシャルアクセスではなくなる。
同じディスクアクセスでも10倍以上の差が出てしまう。