TRAJOIN is an Application to Translate symfony documents Jointly.
home > 1.2/book > 18-Performance.txt
[1] Edit ↑TOPあなたのウェブサイトが大勢の訪問者を引き寄せることを望むのであれば、パフォーマンスと最適化の問題は開発フェーズにおいて重要な要因です。ご安心下さい、パフォーマンスは常にsymfonyのコア開発者の最重要の関心事です。
開発過程の加速によって得られた利益が多少のオーバーヘッドに終わる一方で symfonyのコア開発者はパフォーマンスの要件を常に認識してきました。従って、すべてのクラスとメソッドは念入りに点検され可能な限り速く動作するように最適化されてきました。symfonyの利用の有無に関わらず「hello,world」を表示するために必要な時間を比較することで測定できる基本的なオーバーヘッドは最小です。結果として、symfonyフレームワークはスケーラブルで、負荷テストによく対応します。最高の証明として、きわめて膨大なトラフィック量を占めるいくつかのウェブサイト(つまり何百万ものアクティブな購読者とサーバに負荷を与える多くのAjaxインタラクションを抱えるウェブサイト)がsymofnyを利用しており、パフォーマンスにとても満足しています。symfonyで開発されたウェブサイトの一覧はwiki(http://trac.symfony-project.org/wiki/ApplicationsDevelopedWithSymfony)で確認して下さい。
しかし、もちろん、膨大なトラフィックを占めるウェブサイトはサーバファームを拡張し運営者が最適だと思うハードウェアにアップグレードする方法を持つことが良くあります。これを行うリソースを持たない場合、もしくはsymfonyフレームワークのフルパワーが常に思い通りに利用できることを確かめたい場合、symofny製のアプリケーションをもっと加速するために利用できる調整方法がいくつかあります。この章ではフレームワークのすべてのレベルで推奨されるパフォーマンスの最適化方法のいくつかのリストを示します。これらの大半は上級ユーザ向けです。中には既に以前の章で触れられているものもありますが、一度にこれらすべてが有用であることを理解することになります。
よく最適化されたアプリケーションはよく最適化されたサーバに依存します。symfonyの外部でボトルネックが存在しないことを確認するためにサーバのパフォーマンスの調整方法の基本を理解しておく必要があります。アプリケーションが不必要に遅くないことを確認するための項目がいくつかあります。
php.iniファイルの中でmagic_quotes_gpcディレクティブをonにしておくと、アプリケーションが遅くなります。リクエストパラメータ内のすべての引用符をエスケープするようにPHPに伝えるからですが、symfonyはこれらの引用符を後で体系的にエスケープするので、結果として、時間のロスといくつかのプラットフォームで引用符のエスケーピング問題が起きるだけです。ですので、PHPの設定にアクセスする権限があればこのディレクティブをoffにしておいて下さい。
PHPは最新のリリースであるほど、パフォーマンスが良くなります。PHP5.2はPHP5.1よりも速く、PHP5.1はPHP5.0よりも遙かに速いです。ですので、パフォーマンスの恩恵を受けるためにはPHPを最新のバージョンにアップグレードします。
PHPアクセレータ(例えばAPC、XCache、eAccelerator)の利用は本番サーバに対してはほとんど義務です。トレードオフ無しでPHPの動作速度を平均で50%速くすることができるからです。PHPの本当の速度を体感するためにはアクセレータの拡張機能の1つをインストールして下さい。
一方で、本番サーバでは、XdebugもしくはAPDエクステンションといったデバッグユーティリティは無効にして下さい。
mod_rewrite拡張機能によって引き起こされるオーバーヘッドについて困ることがあるかもしれませんが無視できます。もちろん、書き換えルールで画像を読み込むことは書き換えルール無しのときよりも遅いですが、減速の規模の桁数はPHPステートメントの実行よりも下です。
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 # 生成クラスにコメントを追加する
オブジェクトを読み取るpeerクラスのメソッドを利用するとき、クエリはハイドレイティングの処理を行います(クエリの結果の列に基づいてオブジェクトの作成と投入を行う)。例えば、Propelでarticleテーブルのすべての列を読み取るには、通常次のように行います:
$articles = ArticlePeer::doSelect(new Criteria());
結果の変数$articlesはArticleクラスのオブジェクト配列です。それぞれのオブジェクトの作成と初期化が行われるので、時間がかかります。これは大きな影響力を持ちます: データベースへの直接のクエリとは逆に、Propelのクエリは返す結果の数に直接比例します。このことはモデルメソッドが特定の数の結果のみを返すために最適化すべきであることを意味します。Criteriaオブジェクトによって返されるすべての結果が必要でなければ、setLimit()メソッドとsetOffset()メソッドで制限します。例えば、特定のクエリの10番目から20番目の列のみが必要な場合、リスト18-1のようにCriteriaオブジェクトを改良します。
リスト18-1 - Criteriaオブジェクトによって返される結果の数を制限する
$c = new Criteria();
$c->setOffset(10); // 返される最初のレコードのオフセット値
$c->setLimit(10); // 返されるレコードの数
$articles = ArticlePeer::doSelect($c);
これはページャを利用することで自動化できます。sfPropelPagerオブジェクトは任意のページに対して求められたオブジェクトだけをハイドレイトするために自動的にオフセットの値とPropelクエリの制限を処理します。このクラスに関する詳細な情報はページャのドキュメントを参照して下さい。
アプリケーションの開発期間において、それぞれのリクエストによって発行されるうデータベースクエリの回数を監視すべきです。ウェブデバッグツールバーはそれぞれのページに対してクエリの回数を示し、小さなデータベースアイコンをクリックすればこれらのクエリのSQLコードが表示されます。クエリの回数が以上に上昇するのを見かけたら、Joinの利用を考えるべきです。
Joinメソッドを説明する前に、リスト18-2で示されるように、オブジェクトの配列をループしていて、関連クラスの詳細を読み取るためにPropelのgetterを使うときに何が起きているのかを検討しましょう。この例ではスキーマがauthorテーブルへの外部キーを持つarticleテーブルを記載していることを前提にしています。
リスト18-2 - ループ内で関連クラスの詳細情報を読み取る
// アクションにおいて
$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 - 外部キーのgetterは1つのデータベースクエリを発行する
// テンプレートにおいて
$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 - 同じクエリで記事と著者の詳細情報を読み取る
// アクション内で
$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クラスの)オブジェクトの同じ配列を返します。違いが現れるのは後で外部キーのgetterがこれらのオブジェクトによって利用されるときです。doSelect()メソッドの場合、このメソッドはクエリを発行し、1つのオブジェクトは結果によってハイドレイトされます; doSelectJoinXXX()メソッドの場合、外部オブジェクトは既に存在しており、クエリが必要ないので処理速度はより速くなります。関連オブジェクトが必要であることを知っている場合、データベースクエリの回数を減らすため、そしてページのパフォーマンスを改善するためにDoSelectJoinXXX()メソッドを呼び出します。
articleテーブルとauthorテーブル間のリレーションが存在するので、doSelectJoinAuthor()メソッドはpropel-build-modelを呼び出したときに自動的に生成されます。articleテーブルの構造内において、例えばcategoryテーブルに対して他の外部キーが存在する場合、リスト18-5で示されるように、生成されたBaseArticlePeerクラスは他のJoinメソッドを持ちます。
リスト18-5 - ArticlePeerクラスに対して利用可能なdoSelectメソッド
// Articleオブジェクトを読み取る
doSelect()
// Articleオブジェクトを読み取り、関連したAuthorオブジェクトをハイドレイトする
doSelectJoinAuthor()
// Articleオブジェクトを読み取り、関連したCategoryオブジェクトをハイドレイトする
doSelectJoinCategory()
// Articleオブジェクトを読み取り、Authorオブジェクト以外の関連レコードをハイドレイトする
doSelectJoinAllExceptAuthor()
// 同義語
doSelectJoinAll()
peerクラスはdoCount()メソッドに対してJoinメソッドも含みます。国際化の対応部分(13章を参照)を持つクラスはdoSelectWithI18n()メソッドを提供します。このメソッドは国際化オブジェクト以外はJoinメソッドと同じ振る舞いをします。モデルクラス内で利用可能なJoinメソッドを見つけるためには、lib/model/om/ディレクトリ内で生成されたpeerクラスを調べて下さい。クエリに必要なJoinメソッドが見つからない場合(例えば、多対多のリレーションのために自動的に生成されたJoinメソッドが存在しない)、あなた自身でメソッドを作りモデルを拡張できます。
もちろん、doSelectJoinXXX()の呼び出しはdoSelect()の呼び出しよりも少し遅いので、ハイドレイトされたオブジェクトを後で利用する場合、これは全体のパフォーマンスを改善するだけです。
Propelを利用しているとき、オブジェクトは既にハイドレイトされており、テンプレートのために一時的な配列を用意する必要はありません。ORMに慣れていない開発者がこの罠に陥ることはよくあります。彼らは文字列もしくは整数の配列を用意したい一方で、テンプレートは既存のオブジェクトの配列に直接依存します。例えば、テンプレートがデータベース内部に存在する記事のすべてのタイトルの一覧を表示する場合を想像して下さい。オブジェクト指向のプログラミングをしない開発者はおそらくリスト18-6で示されたようなコードを書くでしょう。
リスト18-6 - 配列が既に存在する場合アクション内で配列を用意することは無駄である
// アクション内
$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 - オブジェクト配列を使えば一時的な配列を作らずに済む
// アクション内
$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 - 一時的な配列を用意するためにカスタムメソッドを使う
// アクション内
$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レイヤを完全に回避する限定的なメソッドを作成できます。例えば、PDOを利用してデータベースを直接呼び出して特製の配列を返します。リスト18-9はこのアイディアを説明しています。
リスト18-9 - lib/model/ArticlePeer.phpファイルの中で最適化されたモデルメソッドのためにPDOで直接データベースにアクセスする
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->prepare($query);
$statement->execute();
$results = array();
while ($resultset = $statement->fetch(PDO::FETCH_OBJ))
{
$results[] = array($resultset->title, $resultset->nb);
}
return $results;
}
}
この種のメソッドを作り始めるとき、それぞれのアクションに対して1つのカスタムメソッドを書くことで終わるので、階層分離の恩恵が失われます。データベースの独立性も失われることは言うまでもありません。
Propelがモデルレイヤに適していない場合、クエリを手作業で書く前に他のORMを使うことを考えて下さい。例えば、PhpDoctrineによるインタフェースのためのsfDoctrineプラグインを確認して下さい。 加えて、他のデータベース抽象化レイヤを利用できます。
symfonyを利用するか関わらず適用できるデータベース固有の最適化テクニックが多く存在します。このセクションは手短にもっとも共通のデータベース最適化戦略の要点をまとめていますが、モデルレイヤを最大限利用するにはデータベースエンジンと管理方法に関して詳しい知識が必要です。
ウェブデバッグツールバーはページ単位でそれぞれのクエリのために費やされた時間を表示し、本当にパフォーマンスが改善されたのかを判断するためにすべての調整をモニタリングされることを覚えておいて下さい。
テーブルクエリは主キーではないカラムを基にすることがよくあります。このようなクエリの速さを改善するために、データベーススキーマの中でインデックスを定義します。単独のカラムインデックスを追加するには、リスト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コマンドを呼び出せばデータベース自身が同じことを行います(テーブル構造をリビルドするだけでなく、既存のすべてのデータを削除します)。
インデックスを作成することでSELECTクエリは速くなりますが、INSERT、UPDATE、とDELETEが遅くなる傾向にあります。また、データベースエンジンは1つのクエリごとに1つのインデックスを使用し、内部の経験則に基づいてそれぞれのクエリのために使用されるインデックスを推測します。インデックスを追加するとパフォーマンスの加速に関してがっかりな結果になることも時折あるので、必ず改善結果を測定して下さい。
指定されない限り、symfonyにおいてそれぞれのリクエストは単独のデータベース接続方法を利用し、接続はリクエストの終了時点で閉じられます。リスト18-11で示されるように、databases.ymlファイルの中でpersistent: trueを設定することで、クエリの合間に開いた状態を保つデータベースの接続プールを利用するための永続的なデータベース接続を有効にできます。
リスト18-11 - config/databases.ymlファイルの中でデータベースの永続的な接続サポートを有効にする
prod:
propel:
class: sfPropelDatabase
param:
dsn: mysql:dbname=example;host=localhost
username: username
password: password
persistent: true # 永続的接続を使用する
これがデータベース全体のパフォーマンスを改善をするのかどうかは多くの要素によります。この主題に関するドキュメントはインターネット上で豊富にあります。利点を検証するためにこの設定を変更する前後でアプリケーションのパフォーマンスを必ずベンチマークして下さい。
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 echo link_to('my article', 'article/read?id='.$article->getId()) ?>
最速のバージョンを使います:
<?php echo link_to('my article', '@article_by_id?id='.$article->getId()) ?>
ページがたくさんのルーティングが行われたハイパーリンクを含むときに違いがわかるようになります。
通常、レスポンスはヘッダと内容の一式で構成されます。レスポンスの中には内容を必要としないものがあります。例えば、ページの異なる部分を更新するJavaScriptを提供するために、Ajaxインタラクションはサーバからデータの少しの部分だけ必要です。この種の短いレスポンスのために、ヘッダだけのセットを送る方が少し速いです。11章で検討したように、アクションはJSONヘッダだけを返すことができます。リスト18-12は11章からの例を再現します。
リスト18-12 - JSONヘッダを返すアクションの例
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 - テキストの内容を直接返すアクションの例
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のゲインは帯域の損失によってバランスが保たれるので、この変更によってすべての設定でパフォーマンスが向上するわけではないので注意して下さい。
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()メソッドでアクションから選択したテンプレートキャッシュをクリアすることもできます。
これらすべてのテクニックは前のリストに示された変更によるネガティブなパフォーマンスの影響を最小にします。
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ファイルの中で、テンプレートキャッシュを生成する
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を完全に回避できます。最初のトリックはプロキシとクライアントブラウザがそれら自身でページをキャッシュするように求めるためにHTTP1.1のヘッダを利用する方法で、それらは次にページが必要なときにページを再リクエストしません。2番目のトリックはスーパーファーストキャッシュ(sfSuperCachePluginプラグインによって自動化される)です。Apacheがリクエストをsymfonyへ渡す前に最初にキャッシュを探すように、これはweb/ディレクトリ内のレスポンスのコピーの保存と書き換えルールの修正から構成されます。
これらの両方の方法はとても効果的なので、静的なページに適用する場合でもページを処理する負担をsymfonyから取り除き、サーバは複雑なリクエストを十分に対処できるようになります。
関数が文脈依存な値もしくはランダム性に依存しない場合、その関数を同じパラメータで2回呼び出すと同じ値が戻ります。このことは最初に結果を保存していれば2番目の呼び出しは十分に回避できたことを意味します。sfFunctionCacheクラスが担う仕事はまさにこれです。このクラスのcall()メソッドは引数としてcallableとパラメータの配列を必要とします。呼び出されたとき、 このメソッドはすべての引数でmd5ハッシュを作りキャッシュの中でこのハッシュで名付けられたキーを探します。ファイルが見つかれば、関数はファイルに保存された結果を返します。そうでなければ、sfFunctionCacheクラスが関数を実行し、結果をキャッシュに保存し、それを返します。リスト18-16の2番目の関数の実行は最初のものより速いです。
リスト18-16 - 関数の結果をキャッシュする
$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に渡される引数の配列です。
例に関してキャッシュオブジェクトに基づいてファイルを使う場合、cache/ディレクトリの元にキャッシュディレクトリを置くのがベターです。cache:clearタスクで自動的にキャッシュが一掃されるからです。関数キャッシュを他のどこかに保存する場合、コマンドラインを通してキャッシュをクリアするときこれは自動的にクリアされません。
PHPアクセレータはデータをメモリに保存する特別な機能を提供するので複数のリクエストにまたがってデータを再利用できます。問題はこれらの機能が異なる構文を持ち、それぞれがこのタスクを実行するための独自の方法を持つことです。symfonyのキャッシュクラスはこれらすべての違いを抽出してどんなアクセレータであれ連携します。リスト18-17で構文をご覧下さい。
リスト18-17 - データをキャッシュするために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を返します。
メモリキャッシングをより研究したい場合、sfMemcacheCacheクラスを必ず調べて下さい。これは他のキャッシュクラスと同じインタフェースを提供しロードバランスされたアプリケーション上のデータベースのロードを減らす手助けを行うことができます。
symfonyのデフォルト設定ではウェブアプリケーションの最も共通する機能を有効にしています。しかしながら、これらすべてが必要ではない場合、それぞれのリクエストごとに初期化にかかる時間を節約するためにこれらを無効にできます。
例えば、アプリケーションがセッションのメカニズムを利用しない、もしくは手動でセッションの取り扱いを始めたい場合、リスト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でインストールの手引きのマニュアルを確認して下さい。
定期的にサーバエラーのログを必ず確認して下さい。これらは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パラメータの値のテストがよく見つかります:
if (sfConfig::get('sf_logging_enabled'))
{
$this->getContext()->getLogger()->info('Been there');
}
sfConfigレジストリがよく最適化されているとしても、リクエストごとの処理期間のget()メソッドの呼び出し回数は重要です。そしてこれは最後のパフォーマンスの勘定に入れられます。sfOptimizerの最適化戦略の1つは設定定数が実行時に変更されない限り、設定定数をそれらの値に置き換えることです。この場合、例えば、sf_logging_enabledパラメータに当てはまります; このパラメータがfalseに定義されていれば、sfOptimizerは以前のコードを次のように変換します:
if (0)
{
$this->getContext()->getLogger()->info('Been there');
}
そしてこれはすべてではありません。前のように明確なテストは空の文字列に対しても最適化されているからです。
最適化を適用するために、最初にhttp://www.symfony-project.org/plugins/sfOptimizerPluginプラグインをインストールし、アプリケーションと環境を指定してoptimizeタスクを呼び出します:
> php symfony optimize frontend prod
他の最適化戦略をコードに適用したい場合、sfOptimizerプラグインがよい出発点になるかもしれません。
symfonyは既によく最適化されたフレームワークで、膨大なトラフィックを占めるウェブサイトに問題無く対応できます。アプリケーションのパフォーマンスを本当に最適化する必要がある場合、設定(サーバの設定、PHPの設定、もしくはアプリケーションの設定)を調整すれば少し速くなります。効率的なモデルメソッドを書くために良い習慣にも従うべきです。データベースはウェブアプリケーションのボトルネックになることが多いので、この点にすべての注意を払うべきです。テンプレートはいくつかのトリックからも恩恵を受けますが、最も速いのは常にキャッシングによるものです。最後に、既存のプラグインを探すことにためらわないでください。これらの中にはウェブページの配信をもっと加速する革新的な技術を提供するものがあるからです(sfSuperCache、sfOptimizerなど)。
Documentation
| Symfony is already a very ... | |
| brtRiver(2009-02-11 11:06:37) | |
| all: .settings: ... | |
| brtRiver(2009-02-11 10:57:37) | |
| Symfony is already a very ... | |
| brtRiver(2009-02-11 10:56:24) | |
| Symfony is already a very ... | |
| brtRiver(2009-02-09 14:45:42) | |
| Summary ------- ... | |
| brtRiver(2009-02-09 14:45:32) | |
| If you want to apply othe ... | |
| brtRiver(2009-02-09 14:45:22) | |
| > php symfony opti ... | |
| brtRiver(2009-02-09 14:45:13) | |
| To apply the optimization ... | |
| brtRiver(2009-02-09 14:45:02) | |
| And that's not all, ... | |
| brtRiver(2009-02-09 14:44:42) | |
| [php] if (0) ... | |
| brtRiver(2009-02-09 14:44:19) | |
| Even if the `sfConfig` re ... | |
| brtRiver(2009-02-09 14:44:04) | |
| [php] if (sfConfi ... | |
| brtRiver(2009-02-09 14:43:44) | |
| The symfony code counts m ... | |
| brtRiver(2009-02-09 14:43:34) | |
| Symfony also offers anoth ... | |
| brtRiver(2009-02-09 14:41:55) | |
| ### The sfOptimizer Plug- ... | |
| brtRiver(2009-02-09 14:41:44) | |
| - %SF_ROOT_DIR%/lib/m ... | |
| brtRiver(2009-02-09 14:41:29) | |
| Listing 18-22 - Adding Yo ... | |
| brtRiver(2009-02-09 14:41:20) | |
| If your application has c ... | |
| brtRiver(2009-02-09 14:41:11) | |
| So merging a large number ... | |
| brtRiver(2009-02-09 14:40:59) | |
| Loading ten files require ... | |
| brtRiver(2009-02-09 14:40:49) |