昨日、内部フラグメンテーション耐性の高いjemallocを利用することで、mod_mrubyのメモリ使用量を半分に減らす事ができました。
しかし、なお、mod_mrubyを組み込んだサーバプロセスに継続してRubyスクリプトを処理させると、メモリ消費量が単調増加してしまうという問題が残っていました。
そこで、単調増加してしまう原因を突き止め、メモリ消費量増加をさらに改善することができました。
メモリ消費量が単調増加する原因
1リクエスト処理するたびにmalloc利用で400バイト程度のメモリが増加している原因を追った所、mrb_generate_code()後にメモリ消費量が増加している事を突き止めました。
そこで、mrb_generate_code()の処理を追うと、コンパイルしたスクリプトのバイトコードに関する情報をirepテーブルに保存し、新たなスクリプトを処理するたびにirepテーブルをインクリメントしてデータを追加している所まで分かりました。そこで、以下のような方法はないかを考えました。
irepテーブルを都度綺麗にしたい
しかし、matzさんによると、残念ながら現状そういう機能は実装しておらず、将来的にirepもGCにのせる予定だということでした。
@matsumotory 今はないんです。将来的にはmrb->irep[n]に入れるんじゃなくて、irepもGCの対象にしたいんですが。
— Yukihiro Matsumotoさん (@yukihiro_matz) 12月 17, 2012
これは困ったなぁ、と思いつつcodegen_start()やscope_start()のコードを追っていると、ふと以下のような方法を思いつきました。
irepテーブルを使いまわす
そこで、matzさんにそんな手法はありなのかを尋ねた所、どうやら行けそうだったので、mod_mrubyでスクリプトを実行するmrb_run()の後に以下のようなコードを追加して、irepテーブルを使い回すように実装しました。
[program lang=’c’ escaped=’true’]
mrb_run(mrb, mrb_proc_new(mrb, mrb->irep[n]), mrb_top_self(mrb)); mrb_gc_arena_restore(mrb, ai); if (mrb->exc) ap_mrb_raise_file_error(mrb, mrb_obj_value(mrb->exc), r, mruby_code_file); (snip) mrb->irep_len = n; if (!(mrb->irep[n]->flags & MRB_ISEQ_NO_FREE)) mrb_free(mrb, mrb->irep[n]->iseq); mrb_free(mrb, mrb->irep[n]->pool); mrb_free(mrb, mrb->irep[n]->syms); mrb_free(mrb, mrb->irep[n]->lines); mrb_free(mrb, mrb->irep[n]);
[/program]
これで、都度irep[n]を開放して、そこに新しいスクリプトのバイトコード情報を上書きすることができるのではないかと考えました。その結果、メモリの単調増加はほぼ無くなるのではないかと予想されます。それを早速実験で確かめてみました
メモリ消費量の再実験
前日の実験とグラフを再利用して、再度同様の実験を行いました。
Apacheを1プロセスで起動させ、以下のスクリプトにより1000リクエストずつ処理をして、20万リクエストまでのメモリ増加量をグラフ化しました。
[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
- malloc使用でarena over flow対策をしていない(昔のmod_mruby)
- malloc+gc_restore
- malloc使用でarena over flow対策済み(先週ぐらいのmod_mruby)
- jemalloc+gc_restore
- jemalloc使用でarena over flow対策済み(昨日のエントリのmod_mruby)
- jemalloc+gc_restore+reuse_irep
- jemalloc使用でarena over flow対策済みでirepを再利用するアーキテクチャ(本エントリ)
- malloc+gc_restore+reuse_irep
- malloc使用でarena over flow対策済みでirepを再利用するアーキテクチャ(本エントリ)
このように、上記のirepテーブルを再利用するアーキテクチャを実装したことによって、上記の紫のグラフが示すようにメモリの単調増加をほぼ無くすことができました。やりました!
また、この際にはjemallocとmallocではほとんど差がありませんでした。上記のグラフでは紫のmallocとirep再利用のグラフに重なっていますが、数バイトjemallocの方がメモリ使用量が多かったです。
さいごに
以上のようなirepテーブルを再利用するアーキテクチャで、メモリ消費量の増加をほぼ無くす事ができました。さらにこれに付随して、パフォーマンスも上がっていたように思うので、それはまた次回以降に記事にしようと思います。
@matsumotory こんなことしなくてもいいように早くirepのGCを実装すべきですねえ。
— Yukihiro Matsumotoさん (@yukihiro_matz) 12月 17, 2012
ですが、将来的にirepもGCにのる予定のようなので、その際はまた実装を修正しようと思います。
とりあえず、メモリの単調増加をなくす事ができてよかったです。