さて、昨日は SSIとの組み合わせでPageキャッシュの適用範囲を広げる話 をした。 なぜSSIかというと、これは組込みの手軽なフィルタ機構だからだ。Apache 1系統ではSSIはハンドラとして実装されているけれども、2系統では新たにフィルタ機構が加わって、SSIはこちらで再実装されている。 フィルタ機構ならmongrelからの出力にも加工できる。Pageキャッシュとキャッシュでないものを透過的に扱えてうれしい訳だ。
ただ、確かにちょっとDRYさに欠ける。どうせならRailsのレイアウトファイルにPHPコード片を直接書きたいではないか。で、これを出力するとPHPとして処理してその結果がクライアントに伝わる、と。 id:yamazさんが「 rhtmlで直接phpを吐き出して処理する方法を模索したいのです。 」と言ってるのはたぶんそういうことだ。私もそれが理想だと思う。今日はそれに挑戦してみた。
Apache ━ (mod_php5 filter) ┳ (mod_rewrite) ━ (mod_proxy_balancer) ━ mongrel_cluster │ ┃ │ │ ┗ キャッシュファイル │ │ │ └───→ [Memcached] (session情報) ←──────────────────────┘
方針
要は、PHPコードを受け付けて実行結果を出力するApacheフィルタがあればいいわけですよ。普通のmod_php5の使いかただとPHPプログラムはHandlerで処理されている。フィルタではない。これをちょこっといじってフィルタ版を作ればいいんだろうと思って、ソースを見てみた。
と、sapi/apache2filterというディレクトリがある。中を見てみるとap_register_output_filterを呼んでるから、これってApacheフィルタそのものじゃないのさ。どうやら、私が知らないだけでフィルタ版は存在したらしい。 ただし、いくつか問題があった。
- Experimentalと書いてある
- Debianとかの標準設定だとフィルタモードは有効にならない
- Proxyに対しては有効にならない模様。
Experimentalでも、私が0から書くよりましでしょう。人柱上等。標準設定の問題は--with-apxs2filterを付けて再コンパイルすればOK。残る問題はProxyに対しては効かない仕様になってること。だからキャッシュに対しては有効だけれども、初回アクセス時は処理してくれない。コードを見てみると、わざわざProxy経由からの出力に対しては無効化するようにしてある。
sapi/apache2filter/sapi_apache2.c:450: php_output_filter
if (f->r->proxyreq) { zend_try { zend_ini_deactivate(TSRMLS_C); } zend_end_try(); return ap_pass_brigade(f->next, bb); }
まあ、そうかもしれない。一般的には他のサーバーから送出されてきたPHPコードを処理するのは恐すぎるし、使いどころが少なそうだ。ただ、今回は必要なわけなので上のコードをコメントアウトする。
方法
実験環境はUbuntu Linux 7.04で、次のようにした。
- apt-get source php5
- debian/rulesの"configure-apache2-stamp"のルールの中、"--with-apxs2=/usr/bin/apxs2"を"--with-apxs2filter=/usr/bin/apxs2"に置き換え
- debian/changelogを適当に。
- sapi/apache2filter/sapi_apache2.cの前述部分をコメントアウト
- fakeroot dpkg-buildpackage
できあがったモジュールをインストールしたら、次のように設定する。基本的には昨日のPageキャッシュ版と同じだけれども、フィルタの設定とRailsのlayouts/application.rhtmlだけ違う。
apacheの設定
PHPという名前のフィルタが導入されているのでこれを使う。
<Proxy balancer://mongrel> BalancerMember http://localhost:8000 BalancerMember http://localhost:8001 BalancerMember http://localhost:8002 Allow from all SetOutputFilter PHP </Proxy> NameVirtualHost * <VirtualHost *> DocumentRoot /path/to/app/public # 以下、普通のvirtual hostの設定 # (略)... <Directory /> Options FollowSymLinks AllowOverride None </Directory> <Directory /path/to/app/public> Options Indexes FollowSymLinks MultiViews AllowOverride None Order allow,deny allow from all </Directory> RewriteEngine on RewriteRule ^/?$ index.html [QSA] RewriteRule ^([^.]+?)/?$ $1.html [QSA] RewriteCond %{DOCUMENT_ROOT}%{REQUEST_FILENAME} !-f RewriteRule ^(.*)$ balancer://mongrel%{REQUEST_URI} [P,QSA] AddOutputFilter PHP .html </VirtualHost>
app/views/layouts/application.rhtml
helperメソッドが出力したダブルクォーテーションがPHPのsyntaxエラーを生じたりして一瞬戸惑った。
<html> <head> <title>test</title> </head> <body> <p> <?php $memcache = new Memcache; $memcache->pconnect('localhost', 11211) or die('cannot connect'); $json = $memcache->get("session:" . $_COOKIE['_simple_layout_session_id']); $hash = json_decode($json); $user_name = $hash->user_name; if ($user_name) { echo '<h1>ようこそ' . $user_name .'さん</h1>'; } else { echo '<p><%= link_to 'ログイン', :action => 'login' %></p>'; } ?> </p> <hr /> <%= @content_for_layout %> </body> </html>
ベンチマーク
昨日とおなじ条件で測ったらこうなった。
Concurrency Level: 100 Time taken for tests: 6.951062 seconds Complete requests: 10000 Failed requests: 0 Write errors: 0 Total transferred: 23683594 bytes HTML transferred: 21641211 bytes Requests per second: 1438.63 [#/sec] (mean) Time per request: 69.511 [ms] (mean) Time per request: 0.695 [ms] (mean, across all concurrent requests) Transfer rate: 3327.26 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 3 32 11.5 32 69 Processing: 15 36 11.6 36 72 Waiting: 2 31 11.9 31 69 Total: 57 68 7.2 66 133 Percentage of the requests served within a certain time (ms) 50% 66 66% 67 75% 68 80% 69 90% 81 95% 83 98% 85 99% 100 100% 133 (longest request)
理論上はSSIの解析プロセスが無くなった分だけ速くなるはずだけれども、結果は誤差の範囲だ。まぁ、ファイルサイズとリクエスト数がこの程度なら、今時のマシンはSSIぐらい気にすることはないということなんだろうか。 とにかく、設定がシンプルになってview templateがDRYになったのは嬉しい。
補遺または蛇足
- 今回作成したフィルタは外部からPHPコードを受け取って処理するという危険極まりない代物になる。こちらで指定したmongrel以外を受け付けないように設定には注意が必要だ。
- SetOutputFilterしたりしているのはいささか乱暴でJSONを出力する場合なんかに対応できない。Apache 2.2なら本当はmod_filterを使うべき。
- キャッシュファイルの拡張子は私は.htmlのままで満足。.phpに変えたい場合はActionController::Base:: page_cache_extensionを設定せよと 本家のドキュメント に書いてある。
- 「可能だから」といってRHTML内PHPを肥大化させるのはおすすめしない。何のためにRailsを使ってるのか分からなくなるし、はっきり言ってきつい。昔、TeXをプリプロセスするJavaプログラムを生成するPerlプログラムを書いたけれども異言語のネストはきつい。