本記事はmod_mruby ngx_mruby advent calendar 2014 20日目の記事です。
昨日は、 @inokappa さんによる「ngx_mruby で Nginx への接続数等の内部情報を取得して InfluxDB と Tasseo で可視化してみる」でした。
僕が担当した17日のアドベントカレンダー「mod_mrubyとngx_mrubyの設計思想とスクリプト間でオブジェクトを共有するためのアーキテクチャ概論」では、オブジェクトを共有しつつも、利用者が効率良く利用できるように最適化したアーキテクチャがどういうものかを説明しました。
そこで言及しつつも保留した以下の点について、今回は紹介したいと思います。
インタプリタを共有することのデメリット
ただし、やはりインタプリタを共有するというアーキテクチャにはデメリットもあって、
- グローバル変数や例外フラグ、クラス等のグローバルな状態を同一のインタプリタ上で実行するスクリプト間で共有してしまう
- インタプリタの開放処理を行わないためメモリが増加傾向にある
というデメリットがあげられます。
このデメリットをそれなりに担保しないことには、単にメリットをとっただけに過ぎず、両立できているとは言えません。
ということで、このデメリットのお話については、 次回の担当のアドベントカレンダーで紹介したい と思います。
上記のように、単一のインタプリタを複数のスクリプトで共有すると、高速に動作させられるものの、デメリットもあるというお話でした。
本日のアドベントカレンダーでは、それらについて詳細を説明したいと思います。
グローバル変数等を共有してしまう
これは、インタプリタにグローバル変数テーブルやクラスのバイトコードを保持しているために、スクリプトを実行する際にこれらの値を参照できてしまう、という問題です。
これらに対しては、概ね以下のように対策しました。
- 例外フラグのみをスクリプト実行後に自動で初期化するようにした
- グローバル変数を手動で未定義にできるメソッドを用意した
- クラスはこれまで通り未定義にできる
では、これらの使い方をみていきましょう。
例外フラグのみをスクリプト実行後に初期化するようにした
これは、インタプリタの主要構造体であるmrb_stateに例外フラグ(mrb->ext)が存在するので、これをスクリプト実行後に0で初期化するようにしました。これをしないと、一旦とあるスクリプトで例外が発生してしまうと、別のスクリプトを同一のインタプリタで実行した場合に、例外をあげないコードであっても例外が発生してしまいます。
はい、ここはただそれだけですね。簡単です。
グローバル変数を手動で未定義にできるメソッドを容易した
続いて、グローバル変数をどうしても共有したくない場合に備えて、グローバル変数を未定義にするAPIをmrubyのC側には以前から実装していて、このようなメソッドはCRubyには存在しない(多分)ので、mod_mrubyやngx_mrubyでは、このグローバル変数を未定義にできるmruby APIを叩ける特別なメソッドを実装しました。
各スクリプトが完全にグローバル変数を互いに共有しないようにするためには、各スクリプトで宣言したグローバル変数を全て最後に未定義にする必要があります。
また、mod_mrubyやngx_mrubyにおいては、プロセス単位でのmrubyの処理は排他処理されているので、グローバル変数のレースコンディション問題は起きないはずです。
ということで、もしグローバル変数を使った上で、それを他のスクリプトに見られたくない場合は、以下のようにグローバル変数を未定義にしましょう。
$a = 1
Apache.echo global_variables #=> [:$stdout, :$$, :$/, :$stdin, :$?, :$a, :$stderr, :$1, :$2, :$3, :$4, :$5, :$6, :$7, :$8, :$9]
Apache.remove_global_variable :$a
Apache.echo global_variables #=> [:$stdout, :$$, :$/, :$stdin, :$?, :$stderr, :$1, :$2, :$3, :$4, :$5, :$6, :$7, :$8, :$9]
クラスはこれまで通り未定義にできる
これは、今までのRubyでもできるはずなので、定義したクラスを他のスクリプトでは未定義にしたい場合に、以下のようにクラスを未定義にしましょう。
class Hoge
def self.fuga
Apache::rputs "hello"
end
end
Hoge::fuga
Apache::rputs self.class.constants.to_s
self.class.remove_const :Hoge
Apache::rputs self.class.constants.to_s
簡単ですね。
以上のように、現状はこの対処がどれほど必要とされているのかはまだ分かっていないので、一旦はこのように手動で対応できるようにしています。
今後、自動でグローバル変数やクラスを未定義にする処理やメソッドを用意したり、mod_mrubyやngx_mrubyの設定で、グローバル状態を共有する方式かそうでない方式かを選択できるようにするのもありかな、などと考えていたりはします。
いずれにせよ、それらの内部で行う事はこういう類の処理になります。
インタプリタの開放処理を行わないためメモリが増加傾向にある
続いて、この問題を解決するためには、以下の2つのポイントがあります。
- バイトコードをスクリプト実行毎に開放する
- C側の関数で作られたオブジェクトを登録しておくarenaテーブルを考慮した実装
では、これらをそれぞれ見て行きましょう。
バイトコードをスクリプト実行毎に開放する
当初は、バイトコードが保存されるテーブルをスクリプト実行毎にどれだけテーブルが増えたかを記録しておいて、実行後に増えた分だけ開放するという処理をしていました。
これによって、メモリは単調増加せずに処理を継続することができます。
そこからmruby自体が改善されていき、現在はmrubyのバイトコードをGCする機能が追加されているので、mod_mrubyやngx_mrubyではバイトコードの管理をmrubyのバイトコードGCにまかせています。
また、それに伴い、以前触る事のできたバイトコードテーブルはさわれなくなっています。
C側の関数で作られたオブジェクトを登録しておくarenaテーブルを考慮した実装
これも、mrubyがリリース後しばらくは、テーブル数が100程度の小さなarenaテーブル上で、C関数側で作ったオブジェクトが無駄に増え続けないように、arenaテーブルを都度restoreしてオブジェクトを手動で回収していました。
arenaテーブルについては、書き出すと1つのエントリになってしまうので、Matzさんが以前書いた記事を読むのが良いでしょう。(なんか日記がめちゃくちゃ重かったのでとりあえずtoggerのリンクを紹介しておきます)
mrubyのmrb_gc_arena_save()/mrb_gc_arena_restore()の使い方
当時は、これをきちんとやらないと、mod_mrubyでリクエストをちょっとさばくと、すぐに、
arena over flow!!!!!
みたいな例外と共にabortしていました。まさに、__突然のarena over flow!!__ですね。
ですが、ある時期からarenaテーブルが上限に近づいてきたら自動でテーブルサイズが拡張される機能が追加されたので、それ以来arenaテーブルをあまり気にしなくてもmrbgemやmrubyのCアプリケーション組込みが実装できるようになりました。
その結果、面倒な事を考える必要なく実装が非常にやりやすくなった反面、arenaテーブルの無駄な増加によるメモリ使用量の肥大化や、arenaテーブルのサイズを自動拡張ではなく固定にしている場合(mrubyのビルド時に設定できる)等に、テーブルをoverflowしてしまうというデメリットもあります。
現在、mod_mrubyやngx_mrubyで、ある条件下においてはメモリが単調増加するケースがあるらしく、僕の予想ではarenaテーブルが肥大化しているんじゃないかと考えています。
ですので、自分の作ったmgemも含めて、mod_mrubyやngx_mrubyで使われるmgemのarenaテーブルが適切に開放されているかをチェックする必要がありそうです。
まとめ
ということで、今回はmrubyインタプリタを共有することによるデメリットについて、様々な視点から言及してみました。
mrubyの良い所の一つとして、一つのプロセスに複数のインタプリタを独立で動かせる、というのがあると思うのですが、サーバアプリケーション組込みや性能を要求される場所でのアプリケーション組込みにおいて、インタプリタを共有する戦略を取った場合には、前回のadvent calendarの記事と、今回のデメリットに関する記事はそれなりに参考になるのではないかと思います。
是非、これらを考慮してmrubyのアプリケーション組込みに挑戦してみてください。
明日のmod_mruby ngx_mruby advent calendar 2014 21日目は、まだいらっしゃいません!
もしギリギリまで待っていなかったら何か書こうと思いますが、まだまだまだ!募集中ですので、是非ともよろしくお願いします!!!!