Apacheのmod_rewriteをmod_mrubyによって無くす事ができるか

Apacheのmod_rewriteをmod_mrubyによって無くすことを考えた時に、現状mod_rewriteでやれることをmod_mrubyに置き換えるとどうなるのかを検証・考察してみました。


なぜかというと、mod_rewriteで困っているような呟きをいくつか見て、以下のように思ったからです。

以前こんな記事「Apache 2.4.1のmod_luaでApacheに介入する(mod_rewriteの終焉?)」を書いたのですが、これも深く関連しています。現状、いったいmod_mrubyはどこまでmod_rewriteの代替手段としてなり得るのかをみていきましょう。

mod_rewriteのルール記述をmod_mrubyのRubyで書く

例えば、「/hoge/を/fuga/にリダイレクトしたい」場合、mod_rewriteでは以下のようなルールを書きます。

RewriteEngine On
RewriteRule ^/hoge/(.*) /fuga/$1

一方で、これをmod_mrubyで書くと以下のようになります。

# /var/www/html/rewrite/rewrite.rb
r = Apache::Request.new()
s = Apache::Server.new()

match_hoge = Regexp.new("^/hoge/(.*)").match(r.uri)

if match_hoge
  redirect   = "/fuga/" + match_hoge[1]
  r.filename = s.document_root + redirect

  Apache.return(Apache::OK)
else
  Apache.return(Apache::DECLINED)
end

このような短い処理では、rewriteの方が可読性が良いように思えます。

この場合のパフォーマンスがどの程度か、以下のabコマンドでベンチをとってみました。テスト環境としては、mod_rewriteは.htaccessに記述し、mod_mrubyはRubyスクリプトに記述して、それを都度フックする設定方法をとりました。

  • httpd.confの設定
mrubyTranslateNameMiddle /var/www/html/rewrite/rewrite.rb
  • abコマンド(同時接続数100、総接続数10万)
ab -c 100 -n 100000 http://example.com/hoge/index.html

例えば、hoge/index.htmlの中には「hoge」とかいて、fuga/index.htmlの中には「fuga」と書いておくと、curlなどで上記URLにアクセスすると以下のようになりますね。

curl http://example.com/hoge/index.html
fuga

実際のベンチ結果は以下のようになりました。

  • mod_rewrite
Requests per second: 8874.57 [#/sec] (mean)
  • mod_mruby
Requests per second: 5624.03 [#/sec] (mean)

mod_rewriteの方がやはり早いですね。

次に、「/hoge/ 以下を /fuga/ 以下にリダイレクトした上に /fuga/hage/ 以下のものは /foo/ 以下にリダイレクトする」というような、複数行に渡るrewriteルールはmod_rewriteの場合、以下のように書きます。

RewriteEngine On
RewriteRule ^/hoge/(.*) /fuga/$1
RewriteRule ^/fuga/hage/(.*) /foo/$1

少しずつですが、カオスな雰囲気が漂ってきました。でも、これぐらいだったらまだまだ全然可読性はあります。しかし、複数行に渡って相互に関連したルールを書き出すと、僕の場合はこれ以上書くと読み間違えたりと、結構危ういです。

これをmod_mrubyでRubyのコードで書くと以下のようになります。

# /var/www/html/rewrite/rewrite.rb
r = Apache::Request.new()
s = Apache::Server.new()

match1 = Regexp.new("^/hoge/(.*)").match(r.uri)

if match1
  redirect = "/fuga/" + match1[1]

  match2   = Regexp.new("^/fuga/hage/(.*)").match(redirect)

  if match2
    redirect = "/foo/" + match2[1]
  end

  r.filename = s.document_root + redirect

  Apache.return(Apache::OK)
else
  Apache.return(Apache::DECLINED)
end

rewriteルールと比べると少し冗長になりますが、読み間違いをすることはないような気がします。

この場合でも、abコマンドでベンチマークを行いました。/hoge/hage/index.htmlにアクセスすると、/foo/index.htmlにリダイレクトされますね。

  • abコマンド(同時接続数100、総接続数10万)
ab -c 100 -n 100000 http://example.com/hoge/hage/index.html

実際のベンチ結果は以下のようになりました。

  • mod_rewrite
Requests per second:    7895.43 [#/sec] (mean)
  • mod_mruby
Requests per second: 5240.75 [#/sec] (mean)

mod_rewrite側のパフォーマンスが少し落ち気味のように見えますが、大体は以上のようになりました。

mod_rewriteとmod_mrubyに関する結論

mod_rewriteのルールで簡単に書けるような単一のルールの場合は、やはりmod_mrubyのコード量のコストとパフォーマンス劣化が目立ってしまう事は否めません。しかし、この程度のパフォーマンス劣化で抑えられているのは褒めるべき箇所なのかもしれません。

ただ、mod_rewriteではどんどんとリダイレクトやそれに付随する処理が増えるにつれ、可読性が劣りミスが発生しやすくなるという話もよく聞いています。恐らく、複数行に渡るルールを羅列し、されにそれぞれのルールが相互に関連しているような場合は、非常に可読性が悪くなり危険と言えるでしょう。

その場合は、mod_mrubyによるある程度のパフォーマンス劣化は受け入れるとして、Rubyでルールの記述や、それ以上の処理が想定で切る場合はmod_mrubyを使うというのも一つの手だと思います。

また、livedoor Tech Blodの「mod_rewrite マニアックス」エントリにもあるように、

mod_rewrite では独自のプログラムで URL の変換を行う仕組みもあります。RewriteMap の MapType で prg を指定する事で、外部プログラムを使用する事が出来ます。任意の言語でプログラミングが出来るため、一見便利に見えますが、RewriteLock ディレクティブによるロックが必要だったり、外部プロセスなので遅いというような問題があり、あまり使われていないのが実際のところです。

とされています。例えば、以下のような処理をmod_rewriteでやろうとすると、ルールがとんでもなく複雑になったり、外部プログラムで解決することでmod_mrubyよりも大幅にパフォーマンスが低下するでしょう。

  • バックエンドのサーバの振り分けルールを変更する実装
backends = [
    "http://192.168.0.101:8888/",
    "http://192.168.0.102:8888/",
    "http://192.168.0.103:8888/",
    "http://192.168.0.104:8888/",
]

# この辺に色々条件を入れたり、backends配列から取り出すルールを別途定義したりするとmod_mrubyのうまみがでる?

r = Apache::Request.new()

r.handler  = "proxy-server"
r.proxyreq = Apache::PROXYREQ_REVERSE
r.filename = "proxy:" + backends[rand(backends.length)] + r.uri

Apache::return(Apache::OK)
  • 例えば、Apacheのスレッドの状態でバックエンドのサーバの振り分けルールを変更する実装
backends = [
    "http://192.168.0.101:8888/",
    "http://192.168.0.102:8888/",
    "http://192.168.0.103:8888/",
    "http://192.168.0.104:8888/",
]

sb = Apache::Scoreboard.new()
busyrate = sb.busy_worker / (sb.process_limit * sb.worker_limit)

if busyrate * 100 > 90
    proxy_rate = backends.length
else if busyrate * 100 > 70
    proxy_rate = backends.length - 1
else if busyrate * 100 > 50
    proxy_rate = backends.length - 2
else
    proxy_rate = backends.length - 3
end

r = Apache::Request.new()

r.handler  = "proxy-server"
r.proxyreq = Apache::PROXYREQ_REVERSE
r.filename = "proxy:" + backends[rand(proxy_late)] + r.uri

Apache::return(Apache::OK)

また、livedoor Tech Blodの「mod_rewrite マニアックス」エントリにもあるように、

mod_rewrite に独自の RewriteMap 用の関数を追加するためには mod_rewrite から公開されている ap_register_rewrite_mapfunc という関数を使用して Apache モジュールを書きます。
外部プログラムと比較しての利点としては、Apache のライフサイクルの中で実行されるため、外部プログラムと比べて高速であり、request_rec 構造体を扱えるのでHTTPヘッダや Cookie のような HTTP 固有の情報を容易に扱う事が出来ます。値の書き換えだけを実装し、肝心の URL 変換部分は mod_rewrite にまかせてしまうため、Apache モジュールとしては非常に少ない行数で済むのも利点です。

という、Cでの拡張も可能ですが、これはまた別の意味で敷居が高くなります。

以上をまとめると、大体以下になるでしょう。

  • rewriteルールが増える見込みもなく、シンプルなもを使うならmod_rewrite
  • rewriteルールが増えるくらいなら、最初からmod_mruby
  • rewriteルールで書けないような処理も、同一の実装手法で統一的に扱いたい場合はmod_mruby
  • どうせやるならRuby力を上げるためにもmod_mruby
  • mruby面白そうだし普及しそうだしどうせならmod_mruby
  • mod_rewriteマスターでどんなルールでも読めるぜ、ならmod_rewrite
  • とにかく速度重視ならmod_rewrite
  • rewriteルール書いても技術的に何も得られてない気がするならmod_mruby

これが、個人的なmod_rewriteとmod_mrubyに関する結論です。

最近はmrubyに関する記事として、以下の日経Linuxにまつもとゆきひろさんが毎月記事を書いてくれているので、日経Linuxが手っ取り早くmrubyについて知ることのできる最も信頼のある情報だと思われます。

  • 日経 Linux (リナックス) 2012年 11月号 [雑誌]
  • 日経 Linux (リナックス) 2012年 10月号 [雑誌]
  • mrubyの基本から学びたい方は是非上記の記事をご覧ください。10月号から記事が開始しており、ページ数は6ページ程度なので、十分追いつける分量だと思います。

    Leave a Comment