mod_mrubyはmrubyのmrb_stateをApacheのサーバプロセス上で使いまわすアーキテクチャをとっています。基本的にプロセスは起動しっぱなしなのですが、リクエストを大量に処理すると、プロセスのメモリがどんどん太っていく問題がありました。
これは、mruby-uv等でも報告されており、どうにか改善したいと思っていました。そこで、twitter上でいくつかmatzさんとやり取りしながら、valgrindでメモリのLEAKとHEAPを調査の上、mallocのフラグメントを疑い、jemallocでmallocをフックするとどのようにメモリ使用量が改善するかを実験しました。
valgrindでApacheをデバッグ
valgrindでApacheをデバッグするためには、Apacheそのものをデバッグモードでコンパイルする必要があります。
また、最近のApacheはaprやapr-utilを自前でコンパイルする必要があるので、それらもデバッグモードでコンパイルした上でApacheをコンパイルするようにしましょう。以下のようにApacheのGENERAL DEBUGを定義しておきます。
[program lang=’bash’ escaped=’true’]
export CPPFLAGS=-DAPR_POOL_DEBUG=0x01
[/program]
上記のFLAGで、apr、apr-util、apacheをコンパイル後、mod_mrubyを-gオプションでコンパイルし組み込みます。
その後、以下のようにvalgrindでmod_mrubyがメモリリークを起こしていないかを調査しました。
[program lang=’bash’ escaped=’true’]
valgrind -v --leak-check=yes --show-reachable=yes --error-limit=no --tool=memcheck --log-file=valgrind.log /usr/sbin/httpd -X
[/program]
上記のようにApacheを起動した後、Apacheに対してabで負荷をかけます。以下のような結果が得られました。
[program lang=’text’ escaped=’true’]
==20358== LEAK SUMMARY: ==20358== definitely lost: 0 bytes in 0 blocks ==20358== indirectly lost: 0 bytes in 0 blocks ==20358== possibly lost: 536 bytes in 2 blocks ==20358== still reachable: 583,620 bytes in 14,042 blocks ==20358== suppressed: 0 bytes in 0 blocks
[/program]
どうやらメモリリークは発生していないようです。
以上から、mallocのフラグメントでプロセスの消費するメモリが増加してしまっているのではないかと推測できます。
jemallocでmallocを置き換えてmod_mrubyを動作させる
そこで、LD_PRELOADを使ってmallocをjeamallocで置き換えて、メモリ使用量をチェックしました。jemallocでシステムコールをフックする簡単な方法として、以下のようにLD_PRELOADを使います。
[program lang=’bash’ escaped=’true’]
LD_PRELOAD=/usr/lib/libjemalloc.so /usr/sbin/httpd -X
[/program]
わかりやすいように、サーバプロセスを-Xオプションにより1プロセスで起動し、以下のようなチェックスクリプトを走らせ、jemallocで置き換えた場合とmallocの場合で10万リクエストまで処理した場合のメモリの増加量をグラフ化しました。
[program lang=’bash’ escaped=’true’]
for count in `seq 1000 1000 200000` do ab -c 10 -n 1000 http://127.0.0.1/hello.mrb rss=`ps auwx ¦ grep httpd ¦ grep apache ¦ awk "{print $ 6}"` echo "$count: $rss" >> reslt.txt sleep 5 done
[/program]
以下、結果のグラフです。
上記のように、mallocを使った場合と比べ、リクエストが増えるとともに消費されるメモリ使用量が半分程度に激減しました。
jemallocに差し替えるとメモリ使用量が半分になる理由
jemallocは内部フラグメント問題を改善しており、mrubyのVALUEが6バイトであるのに対し、jemallocの最小割当サイズは8バイト(さらに小さい割当も可能?)mallocは16バイトです。mrubyでは16バイト及び8バイト以下の割当が多発するため、内部フラグメントが頻発します。その結果、最小割当サイズが半分以下のjemallocによって内部フラグメントが軽減され、メモリ消費量が半分程度に激減したと予想されます。
詳しくはこの論文「A Scalable Concurrent malloc(3) Implementation for FreeBSD」に書いており、この論文だと最小のchunkサイズが2バイトから可能な表になっています。一方でこの論文を書いたJason EvansさんがFaceBookに書いた記述では、最小のchunkサイズが8バイトであるようにも読み取れます。いずれにせよ、mallocよりも内部フラグメンテーションに強い事は間違いないようです。
さらにはマルチスレッドやマルチプロセスに最適化されているようなので、マルチスレッドやマルチプロセスで起動させると、よりよい効果が得られるかもしれません。
さいごに
mod_mrubyプロセスのメモリが太って行く問題は、メモリリークではなく、mallocの内部フラグメントであろう事がこれでわかりました。また、jemallocを使う事で、この内部フラグメントによるメモリ増加を低減し、問題を改善できました。
ただ、このように単調増加していく問題はまだ残っているため、今後はこれをどう対応するか考えようと思います。(MaxRequestPerChildのようなもので逃げるのもありですが、それは最終手段ということで)
「mod_mrubyのメモリ問題をvalgrindで調査の上jemallocで改善」への1件のフィードバック
コメントは受け付けていません。