
かなり前の話題ですが、PHPのフレームワークのパフォーマンス比較記事がありました。
これを見てわかる通り、Zend Frameworkは結構遅いフレームワークです。昔、リリースされたばかりで機能がショボかったころは速いと言われたりもしましたが、Zend_Applicationが追加されてからは多機能化を突き進んでいて、それに伴って遅くなっています。
そもそもZFのセールスポイントって、「疎結合」「高い拡張性」「品質の高さ」などで、パフォーマンスは優先されていないような気もします。
でも最低限の速さは欲しいので、パフォーマンスチューニングをやってみます。
基本
公式ドキュメントの「Zend Frameworkパフォーマンスガイド」を実践するのがすべての基本になります。
あとはPHPの基本的なパフォーマンスチューニングをやることですかね。とりあえずAPCは導入して、Xdebugで測定して、遅い部分を見つけて修正していく感じ。
ここで扱わないもの
フレームワーク自身の遅さを改善する目的なので、ページキャッシュや機能を使わなくする方向のチューニングはやりません。キャッシュはそもそも使えないケースもあるしメンテナンスが面倒になるし、豊富な機能を使いたいからフレームワークを使っているのに機能使うなってのも無茶な話です。本体側のチューニングをまずやって、どうしても改善できなくなってから、仕方なくやるのが正しい順番でしょう。
あとは、ZFの範囲を超えるもの(apache以外を使うとかリバースプロキシ使ってキャッシュするとか)も扱いません。あくまでZF固有の問題を修正する内容だけを書きます。そういうパフォーマンスチューニングの記事はほかにもたくさんあるでしょうし。
1) 初期状態
冒頭のcakephperさんの記事で使われていたソースを題材にしましょう。githubでフォークしてみました。ZFはバージョン1.11.10を使います。この記事を書いている時点の最新です。
で、個人的にこれぐらいは使うなーってところを勝手に修正しました。
- Zend_Layoutはたいてい使うだろうし有効化
- せめて正しいHTMLを吐こうぜ
- ViewHelperはdoctype()、headMeta()、headTitle()、headLink()ぐらいは使うだろ
- asp_tagsでテンプレート書いたほうがきれいだよね
- .zfproject.xmlは開発に必要なファイルなのでコミット
- デフォルトのクラス名Prefixの「Application_」は長ったらしいので削除
- Zend_Db_Adapterの設定はapplication.iniに書くだろ
- Zend_Db_Tableのクラス名は規約に従って(Application_)Model_Postsにするだろ。オートローダーで自動的に読み込まれるようになるし
ZFは初期状態で既に遅く、使う機能を増やすと更に遅くなる素敵フレームワークです。個人的にはこれでも最低限の構成だと思うんですが、これで遅いんだからたまらないです。
APC有効無効の差も見たいので、まずはAPCを無効にしてapache bencheをとります。abのパラメータは-c5 -n100とします。これでいいのかどうかわかりませんが、httpdの性能を見るわけではないので目安ぐらいにはなるでしょう。
$ ab -c 5 -n 100 http://performance/ ...(中略)... Requests per second: 6.73 [#/sec] (mean) Time per request: 742.825 [ms] (mean)
この程度の内容でレスポンスに742msってのは泣いて土下座するレベルですね。こいつをベースにチューニングしていきます。
2) APCを有効にする
まずは基本のAPCを有効にします。
Requests per second: 13.10 [#/sec] (mean) Time per request: 381.567 [ms] (mean)
コードを一切書き換えていないにも関わらず、2倍ほど速くなりました。が、381msはまだまだ遅い方です。反省文レベル。
3) プラグインロードをキャッシュする
ここから、コードに手を加えていきます。
まずはプラグインローダーのキャッシュを有効化します。ZFにはプラグインという名前の、PHPなのにmixinというのか多重継承風に書ける機構があって、これが高い柔軟性をもたらしています。クラスを継承しなくてもメソッドを追加できるとでも言えばいいのか。当然、あちこちで使われているんですが、こいつがとても遅いのです。それはもうびっくりするほど。
なので、公式に速くする手段が用意されていて、それが一度読み込んだプラグインをキャッシュしておいて次回からはpluginLoaderを使わずにincludeできるようにすること。
以下の魔法のコードをapplication/Bootstrap.phppublic/index.phpに加えると有効化できます。public/index.phpに書くとrequire_onceとかを書かないといけないので、Bootstrapに書くのが個人的な好みです。あ、もちろんですが、キャッシュ用のディレクトリは先に作って、適切なパーミッションを与えておきましょう。
2011/09/26追記
Zend_Application自体もPluginLoaderを使っているため、Zend_Applicationの起動前に書かないと効果が薄れることがわかりました。githubのコードは修正しています。
require_once 'Zend/Loader/PluginLoader.php'; $cachefile = APPLICATION_PATH . '/../data/cache/plugins.php'; if (file_exists($cachefile)) { include_once $cachefile; } Zend_Loader_PluginLoader::setIncludeFileCache($cachefile);
で、これを有効化するとこのぐらい速くなります。
Requests per second: 18.29 [#/sec] (mean) Time per request: 273.344 [ms] (mean)
少しですが確実に効果が出ています。これだけ効果があるなら、6行もプログラムを書かせず、application.iniで設定書くだけで有効になるようにしてほしいものですが…、今のところはコードを書く必要があります。
273msなら、まあ上司に舌打ちされるレベルですかね。
4) DBメタデータをキャッシュする
Zend_Db_TableはORMの一種で、簡単なSQLから結構複雑なことまで対応できる素敵モジュールです。が、デフォルトではSQLを発行するたびにdesc テーブル名を発行し、スキーマ構造を問い合わせてしまいます。DBをがっつり使う場合はこれが結構な負荷になるので、メタデータはキャッシュすることが推奨されています。
これはapplication.iniに設定を書くだけで有効にできます。以下の魔法の設定を追加します。
resources.cachemanager.db.frontend.name = "Core" resources.cachemanager.db.frontend.customFrontendNaming = false resources.cachemanager.db.frontend.options.lifetime = 7200 resources.cachemanager.db.frontend.options.automatic_serialization = true resources.cachemanager.db.backend.name = "File" resources.cachemanager.db.backend.customBackendNaming = false resources.cachemanager.db.backend.options.cache_dir = APPLICATION_PATH "/../data/cache" resources.cachemanager.db.frontendBackendAutoload = false resources.db.defaultMetadataCache = "db"
測定結果。
Requests per second: 19.16 [#/sec] (mean) Time per request: 260.907 [ms] (mean)
…ほんの少し速くなりましたが、微妙な差です。DBが遅いネットワーク越しだったり、DBへの問い合わせ回数が多かったりすれば、もっと影響が大きくなるかもしれません。
5) オートローダーを差し替える
ZFではクラスのローディングにZend_Loader_Autoloaderという名前のオートローダーを使っていますが、こいつが実は結構遅いということに最近気づきました。
ZF自体のクラスはPEAR命名規則に従っていて、もっとシンプルなオートローダーで読み込めます。Zend_Loader_Autoloaderが起動する前に割り込んで、オレオレオートローダーが起動するようにしておくと速くなります。
以下の魔法のコードをpublic/index.phpの先頭に加えます。
function myloader($classname) { return @include str_replace('_', DIRECTORY_SEPARATOR, $classname).'.php'; } spl_autoload_register('myloader');
@使うとかないわーって声が聞こえてきそうですが、読み込めない場合はZend_Loader_Autoloaderにローディングを任せればいいので、warningを出すメリットはあまりありません。@を使わずに書くと結構長くなるので、これぐらいは許容範囲だと思っています。
測定結果。
Requests per second: 21.76 [#/sec] (mean) Time per request: 229.780 [ms] (mean)
少しですが効果があることがわかります。
6)最終手段:ZF自体からrequire_onceを取り去る
マシになったとはいえ、まだまだ遅いレベルです。パフォーマンスガイドに載っている最終手段を試すことにしましょう。
require_onceは書けば書くほど遅くなる呪いのような文です。なるべくオートローダーに任せてrequire_onceは手で書かないようにするのがモダンなやり方です。
なので、ZF本体に書かれているrequire_once文を削除して、オートローダーで各クラスともに一回しか読み込まれないように保証しておくと、かなり速くなります。
「findおよびsedコマンドを使ってrequire_onceの呼び出しを取り去る」に書き換え用のコマンドが載っていますが、FreeBSDだとコマンドの仕様が違って動きませんでした。こんな感じかな。
find path/to/Zend -name '*.php' \! -path '*/Loader/Autoloader.php' \! -path '*/Application.php' -print0 | xargs -0 sed -i '' 's/require_once/\/\/ require_once/g'
本体を書き換えるとzfコマンドが動かなくなったり、いろいろ問題が起きることがあるので、library配下に一度コピーして、それを書き換えるようにした方がいいでしょう。
測定結果。
Requests per second: 32.09 [#/sec] (mean) Time per request: 155.791 [ms] (mean)
さすがに本体に手を入れただけあって、かなり速くなりました。これなら、サーバーの能力次第ではこのまま使えるレベルでしょうか。上司ににらまれなくても済むでしょう。
冒頭にも載せましたが、以上の結果をまとめてグラフにしました。
まとめとかその他
チューニングを頑張ればZFだって速くなるよ!…と書きたかったんですが、もう常識範囲として、必ずこのチューニングをやっておかないといけないレベルでZFは遅いです。(デフォルトで速くしとけよ…) ZF2に期待しますかね。
あとは、今回書かなかった点ですが、Zend_Applicationがiniファイルを読んでいる部分をapcなどにキャッシュさせると更に少し速くなります。最近追加されたZend_Config_YamlというYAML形式の設定ファイルを読めるモジュールがあるのですが、こいつはかなり遅い(標準だとYAMLのパースをpure PHPでやりだす)ので、特にyamlを使いたい場合はキャッシュを組み合わせるのが必須になると思います。
もし、もっと速くなるポイントがあれば教えてください。。。
keyword: PHP zendframework