mod_mrubyに高速化機能を追加

Apacheやnginxで動くLuaやその周辺を見ていると、Luaのコードをhttpd.confに直接書くような設定の方法があります。その結果、高速にLuaが動いて、「やっぱLuaはやいー、最高ー」とかそういう話を目にしたので、それだったらmod_mrubyでもやってやろうじゃないかと思いやってみました。

そもそもconfに直接書くとなぜ速いのか

もともとmod_mrubyの売りの一つは、Apacheを起動させている状態で再起動することなく自由にスクリプトを変更して、Apacheの内部処理をリアルタイムに変更できる事です。mod_mrubyの基本的な考え方は以下を参考にして下さい。

しかし、これを実現するためには、Apacheのリクエストの処理毎に、mrubyのステートの初期化処理を行い、rubyスクリプトファイルをopenして構文解析し、バイトコードにコンパイルして実行する必要がありました。これまでのスクリプト言語ではこれはかなりのコストなのですが、mod_mrubyではステートの初期化処理をApacheの起動時に行って、起動後、Apacheにフックされる複数のコードはそのステートを共有するアーキテクチャを取りました。

その結果、mod_mrubyではmrubyの高速軽量性と相まって、スクリプトを高速かつ軽量に動作させる事が可能になり、利便性とその性能劣化とのバランスが非常に良く取れていると考えています。Luaはどうなの?と思った方は昔の以下のエントリを読むと分かると思います。

で、confにコードを直接書く方式がなぜ速いのかというと、Apacheの起動時にそのコードを事前にコンパイルしてメモリ上に保存しておき、リクエスト処理時にはそのバイトコードのみを実行するため、ファイルオープンからコンパイルに至るまでのコストを無視できるからです。一方で、Apacheを再起動することなくコードを変更することが不可能になり、利便性が低下します。

mod_mrubyの本気を見せようか(高速性を優先)

しかし、使い方は人それぞれで、頻繁にApacheの内部処理を弄る必要のない実装もあるだろうという事と、mod_mrubyの本気を見せてやろうという事で、confに直接rubyのコードを書く高速化機能を実装して、どれほどの速度が出るかを実験してみました。

とりあえずはテストということで、SetHandlerでmruby-native-scriptを指定しておいて、このHandlerがセットされているリクエストはmrubyHandlerCodeで指定しているrubyコードを実行するようにしました。具体的な実装としては、Apacheの起動時の子プロセスの初期化時に、mrubyHandlerCodeにコードが指定してあった場合は、構文解析からバイトコードまでのコンパイルをしておいて、リクエストがあったらそのバイトコードを実行します。

以下のようにhttpd.confに設定を書きます。

SetHandler mruby-native-script
mrubyhandlerCode "Apache.rputs('hello native mruby world')"

これで、mruby-native-scriptハンドラーがセットされたリクエストはブラウザにhello native mruby worldの文字列を出力します。複数行書く場合は、例えば以下のように書く必要があります。

SetHandler mruby-native-script
mrubyhandlerCode ' \
Apache.rputs("Test Start.
"); \ Apache.rputs("
"); \ Apache.rputs("Request Class Test
"); \ r = Apache::Request.new(); \ r.content_type = "text/html"; \ Apache.rputs("filename = "+r.filename+"
"); \ Apache.rputs("uri = "+r.uri+"
"); \ Apache.rputs("---- request_rec changed ----
"); \ r.filename = "/var/www/html/index.html"; \ r.uri = "/index.html"; \ Apache.rputs("filename = "+r.filename+"
"); \ Apache.rputs("uri = "+r.uri+"
"); \ Apache.rputs("
"); \ '

パフォーマンス比較

どれほど処理が速くなったのか例のごとくパフォーマンス比較を行いました。比較のためのコードは、ブラウザに文字列を返すだけの処理です。このように軽量な処理にしているのは、なるべくスクリプト内の処理がmod_のアーキテクチャに影響を与えないようにするためです。以下の4つの手法で実装しました。

  • C言語でかかれたApacheモジュール
  • mod_mrubyの高速化機能
  • mod_mrubyの利便性を優先した都度ファイルからスクリプトを読み込む手法
  • htmlで同程度の文字列を出力する静的htmlファイル

また、なるべくApacheのサーバプロセスの生成破棄が影響を与えないように、以下のようなMPM設定にしました。

StartServers 128
MinSpareServers 128
MaxSpareServers 128
ServerLimit 128
MaxClients 128
MaxRequestsPerChild 0

このそれぞれが実装させたApacheに対して、abコマンドで同時接続数100、総接続数100000でベンチを行い、request/secを比較しました。

結果は以下になります。数回行って、値にあまり差が無い事も確認し、一応2回分を以下に示します

abコマンドに対するrequest/secの値
c言語のモジュール mod_mruby高速化 従来のmod_mruby 静的htmlファイル
1回目 10690.39 10388.20 8985.97 10949.71
2回目 10921.63 10366.62 8646.35 10921.28

その他のmod_の比較記事は以下を参考にして下さい。

このように、利便性を放棄して高速化機能を使えば、文字列出力程度の処理においては、C言語で実装したものとほとんど変わらない程のパフォーマンスが出ていました。また、htmlでかかれた静的ファイルともほとんど差が無い事がわかりました。これは個人的にも驚きの結果です。

もはや単なるhtml出力は静的ファイルで用意する必要がない?と思ったりしました。自分でもここまで早くなるとは思っていなったのでびっくりです。しかし、利便性を優先してもこの程度のパフォーマンスが出るというのも、やはりmod_mrubyおよびmrubyの良い所だと言えそうです。

最後に

今回の高速化を実装する上で、元々あったその他の問題点を解決することができました。例えば、mod_mrubyはプロセスを再利用するため、コードを処理するたびにどんどんオブジェクト数が肥大化し、プロセスが太ったり、arena oveflow errorというオブジェクト数が多過ぎると生じるmruby特有のエラーが発生していました。

しかし、matzさんに教えて頂き、オブジェクトが肥大化するけど一時的なオブジェクトである場合は、そのルーチンの前後でmrb_gc_arena_saveとmrb_gc_arena_restoreで囲む事で、一時的なオブジェクトが大量に生成される前の状態にrestoreすることができるようになりました。これによって、上記のような問題が綺麗さっぱり解決しました。こういう所まで工夫がされているmrubyはとても良い言語だと改めて思いました。

これまでは、利便性を意識しつつも高速化するためにはどうしたらよいかと実装をしていましたが、今回は純粋に高速化を行ってみて、C言語でかかれたApacheモジュールに近いパフォーマンスが出せてうれしく思います。

引き続き、mod_mrubyを良くするために色々勉強していきたいと思っています。

「mod_mrubyに高速化機能を追加」への1件のフィードバック

コメントは受け付けていません。