技術者ブログ
クラウド型WAF「Scutum(スキュータム)」の開発者/エンジニアによるブログです。
金床“Kanatoko”をはじめとする株式会社ビットフォレストの技術チームが、“WAFを支える技術”をテーマに幅広く、不定期に更新中!

MongoDBとメモリ使用量

はじめに
WAF「Scutum(スキュータム)」ではサービス開始時より、データストアとしてmemcachedとpgpool II+PostgreSQLを利用しています。これらはどれも安定して動いており満足しているのですが、最近になってより柔軟にデータを取っていきたいというニーズが高くなってきたため、MongoDBの導入を行いました。まだ完全なリプレースまでは至っていませんが、元々のデータベースのスキーマ構造がシンプルであることもあり、数ヶ月以内にはpgpool II+PostgreSQLの部分をMongoDB(Replica Sets)で置き換えることができるのではないかと思っています。
MongoDBにとっての「メモリ使用量」
MongoDBを導入するにあたり、Linux(X86_64)上でMongoDBを動作させたときのメモリの消費について、簡単にですが調べてみました。まず参考にしたのは本家のドキュメントです(英語、日本語訳)。読んでみたところ、非常にあっさりしているな、という印象を受けました。例えば私が慣れているPostgreSQLでは、メモリをどのくらい割り当てるかについてはいくつかの設定項目があり、サーバ上でpostgresプロセス間で共有するメモリや、ソート時にワークスペースとして使うメモリの量などを指定することができます。MongoDBでは基本的にそのような項目が存在せず、デフォルトの状態におまかせ、とするのが作法のようです。しかしそれでは巨大な(インデックスの張られていない)データをソートしようとしたりした場合に何が起こるのかが心配です。まさかサーバのメモリを圧迫してOOM Killerが発動するのでは?という不安が浮かんだのですが、なんとMongoDBはそのような場合にはソートをあっさりあきらめるという動作をするようです。「ちゃんとインデックスを張ってね」あるいは「メモリが少なくてもソートできるように結果の件数を絞ってね」というようなエラーメッセージが出されることになり、メモリを無駄に消費することにはなりません。NoSQL時代を感じさせる、割り切った仕様だと感じます。
そのような背景もあり、MongoDBでは、メモリ使用量が話題になる(問題になる)のは「サイズの大きなデータベースを総なめするようなアクセスが発生した場合に、メモリをどれだけ圧迫するか?」といった視点となることが多くなっています。
Memory Mapped File
ここで登場するのが先述した本家のドキュメントにも記述されている、Memory Mapped File(以下mmap)という概念です。mmapを使うと、メモリにアクセスするようなコードを書くだけで、それが自動的にファイルへのアクセスとなります。「あ...ありのまま 今 起こった事を話すぜ!...オレはメモリに書き込んでいると思っていたら、いつのまにかファイルに書き込んでいた」となり、恐ろしいものの片鱗を味わうことができます。OS全体でメモリが足りなくなった場合には、OSが自動的にmmapで使われているメモリを回収して他の用途に使ってくれるため、大きなファイルに対してmmapを行っても、基本的には大丈夫ということになっています(ただし、MAP_LOCKEDを指定してmmapを呼び出した場合には回収されません)。
私はLinuxのmmapではVMware Serverで過去に一度痛い目に遭っており、今回はそのときの経験が生きることになりそうです。まず気をつけるべきこととして、「mmapで消費しているメモリはfreeコマンドでは見えない」ということがあります。実際にmongodプロセスのmmapがどれだけのメモリを消費しているかを調べるためには、/proc/PID/statusのVmRSS項目を見るのが確実です。また、OS全体でmmapなどによって消費されているメモリの量は、/proc/meminfoのMapped項目で知ることができます。
straceを使ってmongodプロセスを追ってみると、以下のようにmmapを呼び出していることが確認できます。
open("/data/db/test1.0", O_RDWR|O_NOATIME) = 6 lseek(6, 0, SEEK_END) = 67108864 lseek(6, 0, SEEK_SET) = 0 mmap(NULL, 67108864, PROT_READ|PROT_WRITE, MAP_SHARED, 6, 0) = 0x7facc0000000
呼び出しの際の引数として、先述したMAP_LOCKEDは使用されていません。そのため、ここで割り当てられたメモリはOSが必要に応じて自動的に回収してくれることになります。
ディスクキャッシュとmmapの違い
私がMongoDBのメモリ使用量についてググっていた中で最も興味深かったのがこちらのスレッドです(以下、かなり意訳なので、時間がある方は英文をお読みください)。
「開発環境でMongoDBを使っていると、だんだん(おそらくOS全体が)重くなってしまい、そのうちどうしようもなくなる。mongodがメモリを消費しすぎているからだ。mongodを再起動すると解決する。その繰り返しでうんざりする」
というような質問者に対して、
「それはMongoDB固有の問題ではない。あらゆるデータベースにおいて、大きなデータを総なめするケースで常に発生する問題だ。」
という回答がなされています。MySQLやPostgreSQLなどでも、データが大きければディスクアクセスが発生し、その際にページキャッシュという形でメモリを消費します。これがMongoDBにおけるmmapによるメモリ消費と同じだろうという主張です。私はこれが本当なのかどうか非常に気になったため、手元の環境でテストしてみることにしました。
2通りのメモリ消費を比較テスト
テストの目的は以下になります。
- 大量にmmapが行われた場合、メモリが圧迫されるが、そのとき他のプロセスにどのような影響が出るか
- 大量にディスクアクセスが行われた場合、メモリはページキャッシュによって圧迫されるが、そのとき他のプロセスにどのような影響が出るか
前者のテストは2通りの方法で行いました。まず、単にmmapを呼び出すだけの簡単なプログラムをCで作成し、それを使う方法。次に、実際にmongodプロセスに対して、インデックスを使わない大量のデータアクセスを行わせる方法です。後者のテストについては、MongoDBのデータファイルをcatして/dev/nullに流すという方法で行いました。
結果わかったことは以下の通りです。
- mmapによるメモリ圧迫の方が、ページキャッシュによる圧迫よりも、他のプロセスに与える影響(=スワップアウトさせる)が大きい
- 特に、/proc/sys/vm/swappinessの値が60などのように大きな場合、顕著となる
具体的な数値は次のようになります。まず、実験を行ったサーバは4GBのメモリを載せたX86_64のUbuntuで、カーネルは2.6.32です。ここでmallocによってヒープに2GBのメモリを確保するだけの無駄なプロセスを起動しておきます(ヒープには書き込みを行っておき、実際にメモリが消費されることを確認します)。
mmapのテストを行います。あらかじめ用意しておいた10GBほどのデータを持つデータベースに対してmongodを起動し、mongoコマンドで接続した後に、先述したとおりにインデックスが効かないデータ総なめのアクセスを行わせます。すると、
- swappinessが60の場合、ヒープにメモリを持っていたプロセスを中心に、1.3GBのスワップが発生します。
- swappinessが10の場合には、わずか80MBのスワップで済みました。
次にページキャッシュのテストです。/data/db/にあるMongoDBのデータファイルをcatして/dev/nullにリダイレクトします。すると、
- swappinessが60の場合、2MBのスワップが発生しました。
- swappinessが10の場合、スワップは一切発生しません。
結論
以上のように、MongoDBが派手なデータアクセスを行う場合、他のプロセスをスワップアウトさせる可能性が高いです。そのための対策として、以下のようなことが考えられます。
- メモリを可能な限りたっぷり載せる
- MongoDBが効率的なアクセスを行うよう、インデックスの使用が正しくなされているか日々チェックしておく
- MongoDBのメモリ使用量(/proc/PID/statusのVmRSS)を日々監視しておく
- MongoDBのデータベースのサイズをできるだけ低く保つ
- /proc/sys/vm/swappinessを低い値にする
スワップ領域を用意しなければそもそもスワップアウトされることもないので、それでもよいかもしれませんが、万が一でもOOM Killerが発動すると怖いので、私はswappinessを10にした上でスワップ領域を割り当てておくつもりです。