あなたのWebサイトが大勢の訪問者を引き寄せることを望むのであれば、パフォーマンスと最適化の問題は開発フェーズにおいて重要な要因です。ご安心ください、パフォーマンスはつねにsymfonyのコア開発者の最重要の関心事です。
開発過程の加速によって得られた利益が多少のオーバーヘッドに終わる一方で symfonyのコア開発者はパフォーマンスの要件をつねに認識してきました。したがって、すべてのクラスとメソッドは念入りに点検され可能なかぎり速く動作するように最適化されてきました。symfonyの利用の有無にかかわらず「hello,world」を表示するために必要な時間を比較することで測定できる基本的なオーバーヘッドは最小です。結果として、symfonyフレームワークはスケーラブルで、負荷テストによく対応します。最高の証明として、きわめて膨大なトラフィック量を占めるいくつかのWebサイト(つまり何百万ものアクティブな購読者とサーバーに負荷を与える多くのAjaxインタラクションをかかえるWebサイト)がsymfonyを利用しており、パフォーマンスにとても満足しています。symfonyで開発されたWebサイトの一覧はwiki(http://trac.symfony-project.org/wiki/ApplicationsDevelopedWithSymfony)で確認してください。
しかし、もちろん、膨大なトラフィックを占めるWebサイトはサーバーファームを拡張し運営者が最適だと思うハードウェアにアップグレードする方法を持つことがよくあります。これを行うリソースを持たない場合、もしくはsymfonyフレームワークのフルパワーがつねに思いどおりに利用できることを確かめたい場合、symfony製のアプリケーションをもっと加速するために利用できる調整方法がいくつかあります。この章ではフレームワークのすべてのレベルで推奨されるパフォーマンスの最適化方法のいくつかのリストを示します。これらの大半は上級ユーザー向けです。中にはすでに以前の章で触れられているものもありますが、一度にこれらすべてが有用であることを理解することになります。
よく最適化されたアプリケーションはよく最適化されたサーバーに依存します。symfonyの外部でボトルネックが存在しないことを確認するためにサーバーのパフォーマンスの調整方法の基本を理解しておく必要があります。アプリケーションが不必要に遅くないことを確認するための項目がいくつかあります。
php.ini
ファイルのなかでmagic_quotes_gpc
ディレクティブをon
にしておくと、アプリケーションが遅くなります。リクエストパラメーター内のすべての引用符をエスケープするようにPHPに伝えるからですが、symfonyはこれらの引用符をあとで体系的にエスケープするので、結果として、時間のロスといくつかのプラットフォームで引用符のエスケーピング問題が起きるだけです。ですので、PHPの設定にアクセスする権限があればこのディレクティブをoff
にしておいてください。
PHPは最新のリリースであるほど、パフォーマンスがよくなります。PHP 5.2はPHP 5.1よりも速く、PHP 5.1はPHP 5.0よりもはるかに速いです。ですので、パフォーマンスの恩恵を受けるにはPHPを最新のバージョンにアップグレードします。
PHPアクセレータ(たとえばAPC、XCache、eAccelerator)の利用は運用サーバーに対してはほとんど義務です。トレードオフなしでPHPの動作速度を平均で50%速くすることができるからです。PHPの本当の速度を体感するにはアクセレータの拡張機能の1つをインストールしてください。
一方で、運用サーバーでは、XdebugもしくはAPDエクステンションといったデバッグユーティリティは無効にしてください。
NOTE
mod_rewrite
拡張機能によって引き起こされるオーバーヘッドについて困ることがあるかもしれませんが無視できます。もちろん、書き換えルールで画像を読み込むことは書き換えルールなしのときよりも遅いですが、減速の規模の桁数はPHPステートメントの実行よりも下です。
-
TIP 1つのサーバーだけでは十分でないとき、別のサーバーを追加すればロードバランス機能を利用できます。
uploads/
ディレクトリが共有され、セッションに対してデータベースストレージを利用するかぎり、symfonyプロジェクトはロードバランスされたアーキテクチャ内でシームレスに対応します。
symfonyにおいて、モデルレイヤーはもっとも遅いという評価があります。ベンチマークがこのレイヤーを最適化しなければならないことを示した場合、いくつかの改善方法を利用できます。
モデルレイヤーの初期化(コアのPropelクラス)は幾分か時間がかかります。いくつかのクラスをロードしてさまざまなオブジェクトをコンストラクトするからです。しかしながら、symfonyがPropelを統合する方法のため、これらの初期化タスクはアクションが実際にモデルを必要とするときのみに起こり、しかもできるかぎり直前に行われます。Propelのクラスは生成モデルのオブジェクトがオートロードされたときのみに初期化されます。このことはモデルを使わないページはモデルレイヤーによるペナルティが課されないことを意味します。
アプリケーション全体がモデルレイヤーの使用を必要としなければ、settings.yml
ファイルのなかでレイヤー全体をオフに切り替えることでsfDatabaseManager
を初期化しないですみます:
all:
.settings:
use_database: off
生成されたモデルクラス(lib/model/om/
)はすでに最適化されています。これらはコメントを含まず、オートロードシステムから恩恵を受けます。ファイルを手動でインクルードする代わりにオートロードに頼ることはクラスが本当に必要な場合だけロードされることを意味します。この場合において、モデルクラスは不要なので、クラスをオートロードすれば実行時間の節約になります。一方でinclude
ステートメントを使う代わりの方法はそうではありません。コメントに関しては、これらは生成されたメソッドの使いかたをドキュメントにしますが、モデルファイルを長くします。結果として遅いディスク上では少々のオーバーヘッドになります。生成されたメソッドの名前はとても明快なので、デフォルトでコメントはオフに切り替えられます。
これら2つの強化方法はsymfony固有のものですが、つぎのようにpropel.ini
ファイルのなかで2つの設定を変更することでPropelのデフォルト設定に戻すことができます:
propel.builder.addIncludes = true # オートロードシステムに依存する代わりに
# 生成クラスにincludeステートメントを追加する
propel.builder.addComments = true # 生成クラスにコメントを追加する
オブジェクトを検索するピアクラスのメソッドを利用するとき、クエリはハイドレイティングの処理を行います(クエリの結果の列に基づいてオブジェクトの作成と投入を行う)。たとえば、Propelでarticle
テーブルのすべての列を検索するには、通常つぎのように行います:
[php]
$articles = ArticlePeer::doSelect(new Criteria());
結果の変数$articles
はArticle
クラスのオブジェクト配列です。それぞれのオブジェクトの作成と初期化が行われるので、時間がかかります。これは大きな影響力を持ちます: データベースへの直接のクエリとは逆に、Propel
のクエリは返す結果の数に直接比例します。このことはモデルメソッドが特定の数の結果のみを返すために最適化すべきであることを意味します。Criteria
オブジェクトによって返されるすべての結果が必要でなければ、setLimit()
メソッドとsetOffset()
メソッドで制限します。たとえば、特定のクエリの10番目から20番目の列のみが必要な場合、リスト18-1のようにCriteria
オブジェクトを改良します。
リスト18-1 - Criteria
オブジェクトによって返される結果の数を制限する
[php]
$c = new Criteria();
$c->setOffset(10); // 返される最初のレコードのオフセット値
$c->setLimit(10); // 返されるレコードの数
$articles = ArticlePeer::doSelect($c);
これはページャーを利用することで自動化できます。sfPropelPager
オブジェクトは任意のページに対して求められたオブジェクトだけをハイドレイトするために自動的にオフセットの値とPropelクエリの制限を処理します。このクラスに関する詳細な情報はページャーのドキュメントを参照してください。
アプリケーションの開発期間において、それぞれのリクエストによって発行されるうデータベースクエリの回数を監視すべきです。Webデバッグツールバーはそれぞれのページに対してクエリの回数を示し、小さなデータベースアイコンをクリックすればこれらのクエリのSQLコードが表示されます。クエリの回数が以上に上昇するのを見かけたら、Joinの利用を考えるべきです。
Joinメソッドを説明するまえに、リスト18-2で示されるように、オブジェクトの配列をループしていて、関連クラスの詳細を検索するためにPropelのゲッターを使うときに何が起きているのかを検討しましょう。この例ではスキーマがauthor
テーブルへの外部キーを持つarticle
テーブルを記載していることを前提にしています。
リスト18-2 - ループ内で関連クラスの詳細情報を検索する
[php]
// アクションにおいて
$this->articles = ArticlePeer::doSelect(new Criteria());
// doSelect()によって発行されたデータベースクエリ
SELECT article.id, article.title, article.author_id, ...
FROM article
// テンプレートにおいて
<ul>
<?php foreach ($articles as $article): ?>
<li><?php echo $article->getTitle() ?>,
written by <?php echo $article->getAuthor()->getName() ?></li>
<?php endforeach; ?>
</ul>
$articles
配列が10のオブジェクトを格納する場合、getAuthor()
メソッドは10回呼び出されます。リスト18-3のように、Author
クラスの1つのオブジェクトをハイドレイトするためにこのメソッドが呼び出されるたびに、1つのデータベースクエリが順番に実行されます。
リスト18-3 - 外部キーのゲッターは1つのデータベースクエリを発行する
[php]
// テンプレートにおいて
$article->getAuthor()
// getAuthor()によって発行されたデータベースクエリ
SELECT author.id, author.name, ...
FROM author
WHERE author.id = ? // ? は article.author_id
リスト18-2のページは合計で11のクエリを必要とします: 1つのクエリはArticle
オブジェクトの配列を作るために、残りの10のクエリは一度に1つのAuthor
オブジェクトを作るために必要です。これは記事と著者の一覧だけを表示するためのたくさんのクエリになります。
SQL文を使っているのであれば、同じクエリでarticle
テーブルとauthor
テーブルのカラムを検索することで多くのクエリの回数を1つだけに減らす方法をご存じでしょう。これがまさにArticlePeer
クラスのdoSlectJoinAuthor()
メソッドが行うことです。このメソッドは単純なdoSelect()
呼び出しよりもわずかに複雑なクエリを発行しますが、結果セット内の追加カラムによってPropelはArticle
オブジェクトと関連するAuthor
オブジェクトの両方をハイドレイトできます。リスト18-4のコードはリスト18-2とまったく同じ結果を示しますが、データベースへの必要なクエリの回数は11回ではなく1回なので速くなります。
リスト18-4 - 同じクエリで記事と著者の詳細情報を検索する
[php]
// アクション内で
$this->articles = ArticlePeer::doSelectJoinAuthor(new Criteria());
// doSelectJoinAuthor()によって発行されたデータベースへのクエリ
SELECT article.id, article.title, article.author_id, ...
author.id, author.name, ...
FROM article, author
WHERE article.author_id = author.id
// テンプレートにおいて(変わらず)
<ul>
<?php foreach ($articles as $article): ?>
<li><?php echo $article->getTitle() ?>,
written by <?php echo $article->getAuthor()->getName() ?></li>
<?php endforeach; ?>
</ul>
doSelect()
呼び出しとdoSelectJoinXXX()
メソッドによって返された結果には違いはありません: これらは両方とも(この例ではArticle
クラスの)オブジェクトの同じ配列を返します。違いが現れるのはあとで外部キーのゲッターがこれらのオブジェクトによって利用されるときです。doSelect()
メソッドの場合、このメソッドはクエリを発行し、1つのオブジェクトは結果によってハイドレイトされます; doSelectJoinXXX()
メソッドの場合、外部オブジェクトはすでに存在しており、クエリが必要ないので処理速度はより速くなります。関連オブジェクトが必要であることを知っている場合、データベースクエリの回数を減らすため、そしてページのパフォーマンスを改善するためにDoSelectJoinXXX()
メソッドを呼び出します。
article
テーブルとauthor
テーブル間のリレーションが存在するので、doSelectJoinAuthor()
メソッドはpropel-build-model
を呼び出したときに自動的に生成されます。article
テーブルの構造内において、たとえばcategory
テーブルに対してほかの外部キーが存在する場合、リスト18-5で示されるように、生成されたBaseArticlePeer
クラスはほかのJoinメソッドを持ちます。
リスト18-5 - ArticlePeer
クラスに対して利用可能なdoSelect
メソッド
[php]
// Articleオブジェクトを検索する
doSelect()
// Articleオブジェクトを検索し、関連するAuthorオブジェクトをハイドレイトする
doSelectJoinAuthor()
// Articleオブジェクトを検索し、関連するCategoryオブジェクトをハイドレイトする
doSelectJoinCategory()
// Articleオブジェクトを検索し、Authorオブジェクト以外の関連レコードをハイドレイトする
doSelectJoinAllExceptAuthor()
// 同義語
doSelectJoinAll()
ピアクラスはdoCount()
メソッドに対してJoinメソッドも含みます。国際化の対応部分(13章を参照)を持つクラスはdoSelectWithI18n()
メソッドを提供します。このメソッドは国際化オブジェクト以外はJoinメソッドと同じふるまいをします。モデルクラス内で利用可能なJoinメソッドを見つけるには、lib/model/om/
ディレクトリ内で生成されたピアクラスを調べてください。クエリに必要なJoinメソッドが見つからない場合(たとえば、多対多のリレーションのために自動的に生成されたJoinメソッドが存在しない)、あなた自身でメソッドを作りモデルを拡張できます。
TIP もちろん、
doSelectJoinXXX()
の呼び出しはdoSelect()
の呼び出しよりも少し遅いので、ハイドレイトされたオブジェクトをあとで利用する場合、これは全体のパフォーマンスを改善するだけです。
Propelを利用しているとき、オブジェクトはすでにハイドレイトされており、テンプレートのために一時的な配列を用意する必要はありません。ORMに慣れていない開発者がこの罠に陥ることはよくあります。彼らは文字列もしくは整数の配列を用意したい一方で、テンプレートは既存のオブジェクトの配列に直接依存します。たとえば、テンプレートがデータベース内部に存在する記事のすべてのタイトルの一覧を表示する場合を想像してください。オブジェクト指向のプログラミングをしない開発者はおそらくリスト18-6で示されたようなコードを書くでしょう。
リスト18-6 - 配列がすでに存在する場合アクション内で配列を用意することは無駄である
[php]
// アクション内
$articles = ArticlePeer::doSelect(new Criteria());
$titles = array();
foreach ($articles as $article)
{
$titles[] = $article->getTitle();
}
$this->titles = $titles;
// テンプレート内
<ul>
<?php foreach ($titles as $title): ?>
<li><?php echo $title ?></li>
<?php endforeach; ?>
</ul>
このコードの問題はハイドレイティングがすでにdoSelect()
の呼び出しによって行われているので(時間がかかります)、配列$titles
が余計なものになっていることです。代わりにリスト18-7のようなコードを書けます。配列$titles
を作るために費やされた時間が節約されアプリケーションのパフォーマンスが改善されます。
リスト18-7 - オブジェクト配列を使えば一時的な配列を作らずにすむ
[php]
// アクション内
$this->articles = ArticlePeer::doSelect(new Criteria());
// テンプレート内
<ul>
<?php foreach ($articles as $article): ?>
<li><?php echo $article->getTitle() ?></li>
<?php endforeach; ?>
</ul>
オブジェクト上でいくつかの処理作業が必要なので一時的な配列を本当に用意する必要があると感じたら、それを行うための正しい方法はこの配列を直接返すモデルクラス内で新しいメソッドを作ることです。たとえば、それぞれの記事に対して記事のタイトルの配列とコメント数が必要な場合、アクションとテンプレートはリスト18-8のようになります。
リスト18-8 - 一時的な配列を用意するためにカスタムメソッドを使う
[php]
// アクション内
$this->articles = ArticlePeer::getArticleTitlesWithNbComments();
// テンプレート内
<ul>
<?php foreach ($articles as $article): ?>
<li><?php echo $article[0] ?> (<?php echo $article[1] ?> comments)</li>
<?php endforeach; ?>
</ul>
モデル内で速い処理であるgetArticleTitlesWithNbComments()
メソッドを作るのはあなた次第です。たとえば、オブジェクトリレーショナルマッピングとデータベース抽象レイヤー全体を回避することによって行われます。
以前の例のように、オブジェクトが不要でさまざまなテーブルからいくつかのカラムのみが必要な場合、モデル内でORMレイヤーを完全に回避する限定的なメソッドを作成できます。たとえば、Creoleを利用してデータベースを直接呼び出して特製の配列を返します。リスト18-9はこのアイディアを説明しています。
リスト18-9 - 最適化されたモデルメソッドのためにCreoleで直接データベースにアクセスする(lib/model/ArticlePeer.php
)
[php]
class ArticlePeer extends BaseArticlePeer
{
public static function getArticleTitlesWithNbComments()
{
$connection = Propel::getConnection();
$query = 'SELECT %s as title, COUNT(%s) AS nb FROM %s LEFT JOIN %s ON %s = %sGROUP BY %s';
$query = sprintf($query,
ArticlePeer::TITLE, CommentPeer::ID,
ArticlePeer::TABLE_NAME, CommentPeer::TABLE_NAME,
ArticlePeer::ID, CommentPeer::ARTICLE_ID,
ArticlePeer::ID
);
$statement = $connection->prepareStatement($query);
$resultset = $statement->executeQuery();
$results = array();
while ($resultset->next())
{
$results[] = array($resultset->getString('title'), $resultset->getInt('nb'));
}
return $results;
}
}
この種のメソッドを作り始めるとき、それぞれのアクションに対して1つのカスタムメソッドを書くことで終わるので、階層分離の恩恵が失われます。データベースの独立性も失われることは言うまでもありません。
TIP Propelがモデルレイヤーに適していない場合、クエリを手作業で書くまえにほかのORMを使うことを考えてください。たとえば、PhpDoctrineによるインターフェイスのための
sfDoctrine
プラグインを確認してください。加えて、Creole以外にもデータベースに直接アクセスするほかのデータベース抽象化レイヤーも利用できます。PHP 5.1において、PDOがPHPにバンドルされ、Creoleより速い代替機能を提供します。
symfonyを利用するかかかわらず適用できるデータベース固有の最適化テクニックが多く存在します。このセクションは手短にもっとも共通のデータベース最適化戦略の要点をまとめていますが、モデルレイヤーを最大限利用するにはデータベースエンジンと管理方法に関して詳しい知識が必要です。
TIP Webデバッグツールバーはページ単位でそれぞれのクエリのために費やされた時間を表示し、本当にパフォーマンスが改善されたのかを判断するためにすべての調整をモニタリングされることを覚えておいてください。
テーブルクエリは主キーではないカラムを基にすることがよくあります。このようなクエリの速さを改善するために、データベーススキーマのなかでインデックスを定義します。単独のカラムインデックスを追加するには、リスト18-10のように、index: true
プロパティをカラムの定義に追加します。
リスト18-10 - 単独のカラムインデックスを追加する(config/schema.yml
)
propel:
article:
id:
author_id:
title: { type: varchar(100), index: true }
古典的なインデックスの代わりにユニークインデックスを定義するために代替のindex: unique
構文を利用できます。schema.yml
ファイルで複数のカラムインデックスを定義することもできます(インデックスの構文に関する詳細な情報は8章を参照)。この方法はしばし複雑なクエリを加速するのによいのでよく熟慮すべきです。
インデックスをスキーマに追加したあとで、ADD INDEX
クエリを直接データベースに発行するか、propel-build-all
コマンドを呼び出せばデータベース自身が同じことを行います(テーブル構造をリビルドするだけでなく、既存のすべてのデータを削除します)。
TIP インデックスを作成することで
SELECT
クエリは速くなりますが、INSERT
、UPDATE
、とDELETE
が遅くなる傾向にあります。また、データベースエンジンは1つのクエリごとに1つのインデックスを使用し、内部の経験則に基づいてそれぞれのクエリのために使われるインデックスを推測します。インデックスを追加するとパフォーマンスの加速に関してがっかりな結果になることも時折あるので、かならず改善結果を測定してください。
指定されないかぎり、symfonyにおいてそれぞれのリクエストは単独のデータベース接続方法を利用し、接続はリクエストの終了時点で閉じられます。リスト18-11で示されるように、databases.yml
ファイルのなかでpersistent: true
を設定することで、クエリの間に開いた状態を保つデータベースの接続プールを利用するための永続的なデータベース接続を有効にできます。
リスト18-11 - データベースの永続的な接続サポートを有効にする(config/databases.yml
)
prod:
propel:
class: sfPropelDatabase
param:
persistent: true
dsn: mysql://login:passwd@localhost/blog
これがデータベース全体のパフォーマンスを改善をするのかどうかは多くの要素によります。この主題に関するドキュメントはインターネット上で豊富にあります。利点を検証するためにこの設定を変更する前あとでアプリケーションのパフォーマンスをかならずベンチマークしてください。
SIDEBAR MySQL固有のティップス
my.cnf
ファイルのなかで見つかる、MySQLのコンフィギュレーションの多くの設定は、データベースパフォーマンスを変えることがあります。この主題についてはオンラインドキュメント(http://dev.mysql.com/doc/refman/5.1/ja/option-files.html)をかならず読んでください。MySQLによって提供されたツールの1つはスロークエリログです。実行するのに
long_query_time
秒よりも時間がかかるすべてのSQLステートメント(my.cnf
で変更できる設定)は手作業で構文解析するのがとても難しいファイルに記録されますが、mysqldumpslow
コマンドはわかりやすいようにまとめします。これは最適化が必要なクエリを検出するためのすばらしいツールです。
ビューレイヤーを設計し実装する方法によって、小さな減速もしくは加速が起きることにお気づきかもしれません。このセクションは代わりの方法とトレードオフについて説明します。
キャッシュシステムを利用しない場合、include_component()
ヘルパーがinclude_partial()
ヘルパーよりも遅く、include_partial()
ヘルパーは単純なPHPのinclude
ステートメントよりも遅いことは認識すべきです。symfonyはコンポーネントをインクルードするために部分テンプレートとsfComponent
クラスのオブジェクトを含むビューをインスタンス化するからです。ファイルをインクルードするために必要なもの以上の小さなオーバーヘッドは累積されます。
しかしながら、多くの部分テンプレートもしくはコンポーネントをテンプレート内部に含まないかぎり、このオーバーヘッドは重要ではありません。リストもしくはテーブル内、foreach
ステートメント内でinclude_partial()
ヘルパーを呼び出すたびにオーバーヘッドが起こる可能性があります。膨大な数の部分テンプレートもしくはコンポーネントのインクルードがパフォーマンスに重大な影響を与えるとき、キャッシュを考えるか(12章を参照)、キャッシュが選択肢になければ、単純なinclude
ステートメントに切り替えます。
スロットとコンポーネントスロットに関して、パフォーマンスの違いを知覚できます。スロットを設定してインクルードするために必要な処理時間は無視できます。これは変数のインスタンス化と同じことです。コンポーネントスロットはビューの設定に依存し、これらを機能させるためにインスタンス化される必要があります。しかしながら、コンポーネントスロットはテンプレートから呼び出すことから個別にキャッシュできるのに対して、スロットはそれらを含むテンプレート内でつねにキャッシュされます。
9章で説明されたように、テンプレート内部でlink
ヘルパーへのすべての呼び出しはルーティングシステムに内部URIを外部URLに処理することを求めます。これはURIとrouting.yml
ファイルのパターンの間のマッチを見つけることによって行われます。symfonyはこれを簡単に実行します: 任意のURIが最初のルールにマッチするか試し、マッチしない場合、つぎのルールで試すことを行います。すべてのテストは正規表現を含むので、これはとても時間のかかる処理です。
簡単な次善策があります: モジュール/アクションの組み合わせの代わりにルール名を使います。これはどのルールを使うのかsymfonyに伝えるので、ルーティングシステムは以前のすべてのルールにマッチさせる処理を行わずにすみます。
具体的には、routing.yml
ファイルで定義されたつぎのルーティングルールを考えてください:
article_by_id:
url: /article/:id
param: { module: article, action: read }
ハイパーリンクの出力の代わりにつぎの方法で:
[php]
<?php echo link_to('my article', 'article/read?id='.$article->getId()) ?>
最速のバージョンを使います:
[php]
<?php echo link_to('my article', '@article_by_id?id='.$article->getId()) ?>
ページがたくさんのルーティングが行われたハイパーリンクを含むときに違いがわかるようになります。
通常、レスポンスはヘッダーと内容の一式で構成されます。レスポンスのなかには内容を必要としないものがあります。たとえば、ページの異なる部分を更新するJavaScriptを提供するために、Ajaxインタラクションはサーバーからデータの少しの部分だけ必要です。この種の短いレスポンスのために、ヘッダーだけのセットを送るほうが少し速いです。11章で検討したように、アクションはJSONヘッダーだけを返すことができます。リスト18-12は11章からの例を再現します。
リスト18-12 - JSONヘッダーを返すアクションの例
[php]
public function executeRefresh()
{
$output = '{"title":"My basic letter","name":"Mr Brown"}';
$this->getResponse()->setHttpHeader("X-JSON", '('.$output.')');
return sfView::HEADER_ONLY;
}
このコードはテンプレートとレイアウト、そして一度だけ送信されるレスポンスをスキップします。これはヘッダーだけを含むので、もっと軽量でユーザーに送信するために必要な時間はより短くなります。
6章ではテキストの内容をアクションから直接返すことでテンプレートをスキップする別の方法を説明しました。これはMVC分離の原則を破ることになりますが、アクションの反応がとても速くなります。例としてリスト18-13をご覧ください。
リスト18-13 - テキストの内容を直接返すアクションの例
[php]
public function executeFastAction()
{
return $this->renderText("<html><body>Hello, World!</body></html>");
}
標準のヘルパーグループ(Partial
、Cache
、とForm
)はすべてのリクエストごとにロードされます。これらのいくつかを使わないことがわかっているのであれば、標準のヘルパーグループのリストから1つのヘルパーグループを除外すればヘルパーファイルの解析を行わずにすむようになります。とりわけ、Form
ヘルパーグループはデフォルトで含まれていますが、サイズが大きいのでフォームなしのページの表示が重くなります。Form
ヘルパーを除外するためにsettings.yml
ファイルのなかでstandard_helpers
設定を編集するのはよいアイディアかもしれません:
all:
.settings:
standard_helpers: [Partial, Cache] # Formが除外された
トレードオフはuse_helper('Form')
ヘルパーでForm
ヘルパーグループを利用するテンプレートごとにこのヘルパーグループを宣言しなければならないことです。
symfonyはユーザーにレスポンスを送るまえにレスポンスを圧縮します。この機能はPHPのzlibモジュールによるものです。settings.yml
ファイルでこの機能を無効にすればそれぞれのリクエストに対するCPUの時間を少し節約できます:
all:
.settings:
compressed: off
CPUのゲインは帯域の損失によってバランスが保たれるので、この変更によってすべての設定でパフォーマンスが向上するわけではないので注意してください。
TIP PHPでzip圧縮を無効にする場合、サーバーレベルで有効にできます。Apacheは圧縮のための独自の拡張機能を持ちます。
12章でレスポンスの部分もしくはそのすべてをキャッシュする方法を説明しました。レスポンスのキャッシュは主要なパフォーマンス改善につながるので、最適化のには最初に考慮すべきことの1つです。キャッシュシステムを最大限活用したいのであれば、このセクションを読めば、おそらくあなたが考えていなかったいくつのトリックがわかります。
アプリケーションの開発期間において、さまざまな状況でキャッシュをクリアしなければなりません:
lib/
フォルダーの1つ)に追加するだけではsymfonyはそれを見つけられません。symfonyがautoload.yml
ファイルのディレクトリのすべてを再び閲覧して新しいクラスを含めてオートロード可能なクラスの位置を参照できるように、オートロードのコンフィギュレーションキャッシュをクリアしなければなりません。project:deploy
コマンドでアプリケーションを更新するとき: この場合通常は3つの以前の修正をカバーします。キャッシュ全体のクリアに関連する問題は、コンフィギュレーションキャッシュが再生成される必要があるため、つぎのリクエストの処理時間がとても長くなることです。加えて、修正されなかったテンプレートも同じようにキャッシュからクリアされ、以前のリクエストの恩恵を失います。
このことは本当に再生成する必要のあるキャッシュファイルだけをクリアすることがよいアイディアであることを意味します。リスト18-14で示されるように、クリアするキャッシュファイルの部分集合を定義するにはcache:clear
タスクのオプションを使います。
リスト18-14 - キャッシュの選択した部分のみをクリアする
// frontendアプリケーションのキャッシュのみをクリアする
> php symfony cache:clear frontend
// frontendアプリケーションのHTMLキャッシュのみをクリアする
> php symfony cache:clear frontend template
// frontendアプリケーションのコンフィギュレーションキャッシュのみをクリアする
> php symfony cache:clear frontend config
12章で説明されたように、cache/
ディレクトリのファイルを手作業で削除する、もしくは$cacheManger->remove()メソッド
でアクションから選択したテンプレートキャッシュをクリアすることもできます。
これらすべてのテクニックは前のリストに示された変更によるネガティブなパフォーマンスの影響を最小にします。
TIP symfonyをアップグレードするとき、手動による介入を行わなくても、キャッシュは自動的にクリアされます(
settings.yml
のなかでcheck_symfony_version
パラメーターをtrue
に設定している場合)。
新しいアプリケーションを運用サーバーにデプロイしたとき、テンプレートキャッシュは空です。キャッシュに設置されたページを一度訪問するユーザーを待たなければなりません。クリティカルな開発において、ページ処理のオーバーヘッドは受け入れられるものではなく、最初のリクエストが発行されると同時にキャッシュの利点を利用できなければなりません。
解決方法はテンプレートキャッシュを生成するためにステージング(staging)環境(設定は運用環境と似ている)でアプリケーションのページを自動的にブラウジングして、キャッシュを持つアプリケーションを運用サーバーに転送することです。
ページを自動的にブラウジングするための選択肢の1つは ブラウザーで外部URLのリストを通して見るシェルスクリプト(たとえばcurl)を作成することです。しかし、より速く優れた解決方法があります: sfTestBrowser
オブジェクトを利用するsymfonyバッチです。これはすでに15章で検討されました。これはPHPで書かれた内部ブラウザーです(そして機能テストのためにsfTestBrowser
によって使われます)。これは外部URLを取得しレスポンスを返しますが、興味深いことは通常のブラウザーのようにテンプレートキャッシュの生成機能を実行させることです。これはsymfonyを一度だけ初期化してHTTP転送レイヤーを通さないので、この方法ははるかに速いです。
リスト18-15はステージング環境においてテンプレートキャッシュファイルを生成するために使われるバッチスクリプトの例を示しています。このバッチはphp batch/generate_cache.php
を呼び出すことで実行されます。
リスト18-15 - テンプレートキャッシュを生成する(batch/generate_cache.php
)
[php]
require_once(dirname(__FILE__).'/../config/ProjectConfiguration.class.php');
$configuration = ProjectConfiguration::getApplicationConfiguration('frontend', 'staging', false);
sfContext::createInstance($configuration);
// ブラウジングするURLの配列
$uris = array(
'/foo/index',
'/foo/bar/id/1',
'/foo/bar/id/2',
...
);
$b = new sfBrowser();
foreach ($uris as $uri)
{
$b->get($uri);
}
symfonyにおいてテンプレートキャッシュ用のデフォルトストレージシステムはファイルシステムです: HTMLのフラグメントもしくはシリアライズされたレスポンスオブジェクトはプロジェクトのcache/
ディレクトリに保存されます。symfonyはキャッシュを保存するための代わりの方法を提案します: SQLiteデータベースです。このデータベースはPHPがネイティブでとても効果的にクエリを行う方法を知っているシンプルなファイルです。
テンプレートキャッシュに対してファイルシステムストレージの代わりにSQLite
ストレージを使うようにsymfonyに伝えるには、factories.yml
ファイルを開き、view_cache
エントリーをつぎのように編集します:
view_cache:
class: sfSQLiteCache
param:
database: %SF_TEMPLATE_CACHE_DIR%/cache.db
テンプレートキャッシュのためにSQLiteストレージを利用する利点はキャッシュ要素の数が多いときに読み込みと書き込みのオペレーションが速くなることです。アプリケーションがキャッシュを大量に使うとき、テンプレートのキャッシュファイルがファイル構造の深い部分に散乱してしまいます; この場合、SQLiteストレージに切り替えることでパフォーマンスが増加します。加えて、ファイルシステムストレージ上のキャッシュをクリアすると大量のファイルをディスクから削除することが必要になることがあります; このオペレーションは数秒続くので、この間はオペレーションを利用できません。SQLiteストレージシステムによって、キャッシュのクリア処理は単独のファイルオペレーション、SQLiteデータベースファイルの削除ですみます。現在保存されているキャッシュ要素の数がなんであれ、オペレーションは即座に行われます。
おそらくsymfonyを加速するベストな方法はsymfony自身を完全に回避することです・・・これは一部冗談が入っています。ページのなかには変更されないものがあり、これらはリクエストごとにsymfonyによって再処理される必要はありません。これらのページの配信を加速するためにテンプレートキャッシュはすでに存在しますが、まだsymfonyに依存しています。
いくつかのページに関しては12章で説明されたトリックを組み合わせることでsymfonyを完全に回避できます。最初のトリックはプロキシとクライアントブラウザーがそれら自身でページをキャッシュするように求めるためにHTTP 1.1のヘッダーを利用する方法で、それらはつぎにページが必要なときにページを再リクエストしません。2番目のトリックはスーパーファーストキャッシュ(sfSuperCachePlugin
プラグインによって自動化される)です。Apacheがリクエストをsymfonyへ渡すまえに最初にキャッシュを探すように、これはweb/
ディレクトリ内のレスポンスのコピーの保存と書き換えルールの修正から構成されます。
これらの両方の方法はとても効果的なので、静的なページに適用する場合でもページを処理する負担をsymfonyからとり除き、サーバーは複雑なリクエストを十分に対処できるようになります。
関数が文脈依存な値もしくはランダム性に依存しない場合、その関数を同じパラメーターで2回呼び出すと同じ値が戻ります。このことは最初に結果を保存していれば2番目の呼び出しは十分に回避できたことを意味します。sfFunctionCache
クラスが担う仕事はまさにこれです。このクラスのcall()
メソッドは引数としてcallableとパラメーターの配列を必要とします。呼び出されたとき、 このメソッドはすべての引数でmd5ハッシュを作りキャッシュのなかでこのハッシュで名づけられたキーを探します。ファイルが見つかれば、関数はファイルに保存された結果を返します。そうでなければ、sfFunctionCache
クラスが関数を実行し、結果をキャッシュに保存し、それを返します。リスト18-16の2番目の関数の実行は最初のものより速いです。
リスト18-16 - 関数の結果をキャッシュする
[php]
$cache = new sfFileCache(array('cache_dir' => sfConfig::get('sf_cache_dir').'/function'));
$fc = new sfFunctionCache($cache);
$result1 = $fc->call('cos', array(M_PI));
$result2 = $fc->call('preg_replace', array('/\s\s+/', ' ', $input));
sfFunctionCache
のコンストラクターはキャッシュオブジェクトを必要とします。call()
メソッドの最初の引数は呼び出し可能でなければならないので、関数名、クラス名と静的メソッド名の配列、もしくはオブジェクト名とpublicなメソッド名の配列でなければなりません。call()
メソッドの別の引数に関して、これはcallableに渡される引数の配列です。
CAUTION 例に関してキャッシュオブジェクトに基づいてファイルを使う場合、
cache/
ディレクトリの元にキャッシュディレクトリを置くのがベターです。cache:clear
タスクで自動的にキャッシュが一掃されるからです。関数キャッシュをほかのどこかに保存する場合、コマンドラインを通してキャッシュをクリアするときこれは自動的にクリアされません。
PHPアクセレータはデータをメモリに保存する特別な機能を提供するので複数のリクエストにまたがってデータを再利用できます。問題はこれらの機能が異なる構文を持ち、それぞれがこのタスクを実行するための独自の方法を持つことです。symfonyのキャッシュクラスはこれらすべての違いを抽出してどんなアクセレータであれ連携します。リスト18-17で構文をご覧ください。
リスト18-17 - データをキャッシュするためにPHPアクセレータを利用する
[php]
$cache = new sfAPCCache();
// データをキャッシュに保存する
$cache->set($name, $value, $lifetime);
// データを読みとる
$value = $cache->get($name);
// データのピースがキャッシュのなかに存在するかチェックする
$value_exists = $cache->has($name);
// キャッシュをクリアする
$cache->clear();
キャッシュ機能が動作しなかった場合set()
メソッドはfalse
を返します。キャッシュされた値は何でもなります(文字列、配列、オブジェクト); sfProcessCache
クラスはシリアル化を処理します。求められた変数がキャッシュ内に存在しなかった場合get()
メソッドはnull
を返します。
TIP メモリキャッシュをより研究したい場合、
sfMemcacheCache
クラスをかならず調べてください。これはほかのキャッシュクラスと同じインターフェイスを提供しロードバランスされたアプリケーション上のデータベースのロードを減らす助けを行うことができます。
symfonyのデフォルト設定ではWebアプリケーションのもっとも共通する機能を有効にしています。しかしながら、これらすべてが必要ではない場合、それぞれのリクエストごとに初期化にかかる時間を節約するためにこれらを無効にできます。
たとえば、アプリケーションがセッションのメカニズムを利用しない、もしくは手動でセッションの扱いを始めたい場合、リスト18-19のように、factories.yml
ファイルのstorage
キーのなかのauto_start
設定をfalse
に変えます。
リスト18-19 - セッションをオフにする(frontend/config/factories.yml
)
all:
storage:
class: sfSessionStorage
param:
auto_start: false
同じことがデータベース機能にあてはまります(この章の前の方の"モデルを調整する"のセクションで説明されています)。アプリケーションがデータベースを利用しないのであれば、小さなパフォーマンスのゲインを得るために、今回はsettings.yml
ファイルでこの機能を無効にします(リスト18-20を参照)。
リスト18-20 - データベースの機能を無効にする(frontend/cofig/settings.yml
)
all:
.settings:
use_database: off # データベースとモデルの機能
セキュリティ機能(6章を参照)に関しては、リスト18-21で示されるように、filters.yml
ファイル内で無効にできます。
リスト18-21 - セキュリティ機能を無効にする(frontend/config/filters.yml
)
rendering: ~
security:
enabled: off
# 一般的に、ここの独自のフィルターを追加したい場合
cache: ~
common: ~
execution: ~
いくつかの機能は開発環境のときだけ便利な機能なので運用環境ではこれらを有効にしないほうがいいでしょう。symfonyの運用環境のパフォーマンスは本当に最適化されているので、この方法はすでにデフォルトであてはまります。パフォーマンスが影響を与える開発機能のなかで、デバッグモードはもっとも厳しいものです。symfonyのロギング機能に関して、運用環境ではこの機能もデフォルトでオフにされます。
ロギング機能を無効にしていて、開発環境だけで起きるわけでない問題を議論する場合、運用環境で失敗したリクエストに関する情報を取得する方法に悩んでいる人がいるかもしれません。幸いにして、symfonyはsfErrorLoggerPlugin
プラグインを利用できます。sfErrorLoggerPlugin
プラグインは運用環境でバックグラウンドで動作し、データベースに404エラーと500エラーの詳細を記録します。ファイルのロギング機能よりもずっと速いです。いったんロギングメカニズムがオンになると、レベルが何であれ無視できないオーバーヘッドを追加するのに対して、プラグインのメソッドはリクエストが失敗したときのみ呼び出されるからです。http://trac.symfony-project.org/wiki/sfErrorLoggerPluginでインストールの手引きのマニュアルを確認してください。
TIP 定期的にサーバーエラーのログをかならず確認してください。これらは404エラーと500エラーに関するとても価値のある情報も含みます。
コード自身を最適化することでアプリケーションを加速することも可能です。このセクションはこれを行う方法に関するいくつかの洞察を提供します。
10個のファイルをロードすることは1個の長いファイルをロードするよりも多くのI/Oオペレーションが必要です。とても長いファイルのロードは小さなファイルのロードよりも多くのリソースを必要とします。とりわけファイルの内容の大部分がPHPパーサーに無意味な場合、これはコメントがあてはまります。
多くのファイルを統合してこれらに含まれるコメントをとり除けばパフォーマンスの改善につながります。symfonyはその最適化の機能をすでに持ちます; この機能はコアコンパイレーション(core compilation)と呼ばれます。最初のリクエストの始めに(もしくはキャッシュがクリアされた後に)、symfonyのアプリケーションはすべてのコアクラス(sfActions
、sfRequest
、sfView
など)を1つのファイルに統合し、コメントと二重の空白を除去し、ファイルサイズの最適化を行い、このファイルをキャッシュとconfig_core_compile.yml.php
ファイルに保存します。それぞれのつぎのリクエストでは30個のファイルの代わりにこれらの内容で構成され最適化された単独のファイルだけがロードされます。
アプリケーションがつねにロードしなければならないクラスを持つ場合、とりわけ、これらのクラスが多くのコメントを持つ場合、これらのクラスをコアコンパイルファイルに追加することが有益であることがあります。そのためには、リスト18-22のように、アプリケーションのconfig/
ディレクトリにcore_compile.yml
ファイルを追加し、追加したいクラスの一覧を作ります。
リスト18-22 - コアコンパイレーションファイルにクラスを追加する(frontend/config/core_compile.yml
)
- %SF_ROOT_DIR%/lib/myClass.class.php
- %SF_ROOT_DIR%/apps/frontend/lib/myToolkit.class.php
- %SF_ROOT_DIR%/plugins/myPlugin/lib/myPluginCore.class.php
...
symfonyはsfOptimizer
という別の最適化ツールも提供します。このプラグインはsymfonyとアプリケーションのコードに最適化戦略を適用し、実行速度をさらに加速します。
symfonyのコードは設定パラメーターに依存する多くのチェック作業を含みます。アプリケーションもこの作業を行うことがあります。たとえば、symfonyのクラスを見てみると、sfLogger
オブジェクトを呼び出すまえにsf_logging_enabled
パラメーターの値のテストがよく見つかります:
[php]
if (sfConfig::get('sf_logging_enabled'))
{
$this->getContext()->getLogger()->info('Been there');
}
sfConfig
レジストリがよく最適化されているとしても、リクエストごとの処理期間のget()
メソッドの呼び出し回数は重要です。そしてこれは最後のパフォーマンスの勘定に入れられます。sfOptimizer
の最適化戦略の1つは設定定数が実行時に変更されないかぎり、設定定数をそれらの値に置き換えることです。この場合、たとえば、sf_logging_enabled
パラメーターにあてはまります; このパラメーターがfalse
に定義されていれば、sfOptimizer
は以前のコードをつぎのように変換します:
[php]
if (0)
{
$this->getContext()->getLogger()->info('Been there');
}
そしてこれはすべてではありません。前のように明確なテストは空の文字列に対しても最適化されているからです。
最適化を適用するために、最初にhttp://www.symfony-project.org/plugins/sfOptimizerPluginプラグインをインストールし、アプリケーションと環境を指定してoptimize
タスクを呼び出します:
> php symfony optimize frontend prod
ほかの最適化戦略をコードに適用したい場合、sfOptimizer
プラグインがよい出発点になるかもしれません。
symfonyはすでによく最適化されたフレームワークで、膨大なトラフィックを占めるWebサイトに問題なく対応できます。アプリケーションのパフォーマンスを本当に最適化する必要がある場合、設定(サーバーの設定、PHPの設定、もしくはアプリケーションの設定)を調整すれば少し速くなります。効率的なモデルメソッドを書くためによい習慣にも従うべきです。データベースはWebアプリケーションのボトルネックになることが多いので、この点にすべての注意を払うべきです。テンプレートはいくつかのトリックからも恩恵を受けますが、もっとも速いのはつねにキャッシュによるものです。最後に、既存のプラグインを探すことにためらわないでください。これらのなかにはWebページの配信をもっと加速する革新的な技術を提供するものがあるからです(sfSuperCache
、sfOptimizer
など)。