文中の中で「MMAP」と記載した場合、これはMongoDB 2.xもしくはMongoDB 3.0のMMAPv1ストレージエンジンのことを指します。また「WiredTiger」と記載した場合は、MongoDB 3.0のWiredTigerストレージエンジンのことを指します。
コンピュータが遅いとはどういうことなのか
MongoDBに限らずコンピュータシステムが遅いというのはどういうことなのでしょうか?
1つ目は、コンピュータのリソースが限界に達して何かを待っている状態です。コンピュータにはCPU、メモリ、ディスク、ネットワークといったリソースがあります。例えば、多くのプロセスで大量の計算をした時には、CPUの使用が限界に達して、結果としてCPUを使えずに待たされる処理が出てきます。メモリであれば、メモリ量が限界に達してディスクからデータを読むことになり、ディスクのIOを待つという事になります。
MongoDBの場合はディスクIOを待つ場合が多く、CPUネックになることは複雑な集計の時を除いてほとんどありません。それはJOINや副問い合わせなどの複雑な参照クエリが提供されていないためです。また、単体構成であればネットワークがネックになることもほとんどありません。
2つ目は、非効率な処理をしているです。MongoDBの場合は、「本来応答を待たなくてよいのに応答を待っている」や「必要以上にロックを取って他の計算が待たされる」といった非効率な処理により期待どおりの性能が出ない事が多いです。
では具体的にMongoDBが遅くなる原因を参照と更新で分けて説明していきましょう。
[参照の場合] データがメモリに載っていない
一般的にデータをメモリから読む場合とディスクから読む場合では100~1万倍速度が違うと言われています。参照クエリではメモリにデータあればメモリを読み込むだけでよいですが、メモリになければディスクIOを待つことになります。
どれだけメモリにデータが載っているかは、mongostatを見ればわかります。resの項目が実際に利用している物理メモリ量です。この値とデータのワーキングセット(インデックスとクエリで必要となるデータ)のサイズを比較して、ワーキングセットのほうが大きければメモリに載りきっていないことになります。特にインデックスがメモリに載らない場合は読み込み性能は著しく低下しますので、最低でもインデックスはメモリに載るようにしましょう。インデックスのサイズは db.コレクション.stats()の出力結果を見ればわかります。
この問題が発生しているかどうかは、MMAPあればmongostatのfaultsの項目を見ればわかります。この項目には、秒間のページフォルト、つまりメモリに無くディスクを読みにいった回数が表示されます。
十分にメモリがあってもMongoDBの起動直後は注意が必要です。MongoDBは起動した状態ではメモリには一切データは載っておらず、クエリでデータが読み込まれていくと次第にメモリに載っていきます。これを回避したければtouchコマンドを用いてコレクションをメモリに載せることができます。
上記の問題はデータファイルにフラグメンテーションが発生している時により顕著になります。MongoDBではドキュメントを消したり肥大化により移動が発生すると、データファイルに穴ができます。これがフラグメンテーションです。これが発生すると「穴ごと」メモリに乗せてしまうため、実際のデータサイズよりも大きなメモリを使うことになり、よりメモリからあふれる可能性がでてきます。これはdb.コレクション.stats()や db.stats()のdataSizeとstorageSizeデータサイズを比較するとわかります。またこの問題はMMAPでのみ発生します。
[参照の場合] インデックスがない
これは多くの他のDBMSと同じですが、インデックスを使わない検索は、すべてのドキュメントの中を開けて調べることになりディスクIOが多発します。これを「フルスキャン」といいます。
クエリがフルスキャンしているかどうかは、explain()でわかります。
explain()の実行例は以下の通りです。
db.mycol.find({mykey:98}).explain()
explain()はそのクエリの実行計画を示します。MongoDBの実行計画はどのインデックスを選ぶかを決めることです。以下がexplain()の実行結果例です。今回はMongoDB 3.0の結果を抜粋して記載します。
{ "queryPlanner" : { ... "winningPlan" : { # ←選択された実行計画 "stage" : "COLLSCAN", # ←フルスキャンしている "filter" : { "mykey" : { "$eq" : 98 }
上記の例では、winningPlanという箇所に、最終的に選ばれた実行計画が表示され、その中身に"stage" : "COLLSCAN" とあることからフルスキャンしていることがわかります。
このクエリを速くしたければ、mykeyというキーにインデックを貼ることでディスクIOを最小にすることができます。
MongoDB 3.0からexplain()の結果が大きく変わりました。MongoDB 3.0から実行計画の計画フェーズと実行状況を明確に区別して表示できるようになり、表示項目も非常に増えています。詳しくは公式マニュアルのcursor.explainを確認して下さい。