symfony book 日本語ドキュメント

第18章 - パフォーマンス

あなたの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ステートメントの実行よりも下です。

symfony開発者のなかにはsymfony内部のYAMLパーサーの代わりにPHPのsyck拡張機能を使いたい人もいます。syck拡張機能は速いですが、symfonyのキャッシュシステムはYAML解析のオーバーヘッドをすでに最小にしているので、運用環境での恩恵は存在しません。syck拡張機能はまだ完全に成熟しておらず、使うときにエラーを引き起こすかもしれないことに注意してください。しかしながら、興味がありましたら、拡張機能(http://whytheluckystiff.net/syck/)をインストールすれば、symfonyは自動的にそれを利用します。

TIP 1つのサーバーだけでは十分でないとき、別のサーバーを追加すればロードバランス機能を利用できます。uploads/ディレクトリが共有され、セッションに対してデータベースストレージを利用するかぎり、symfonyプロジェクトはロードバランスされたアーキテクチャ内でシームレスに対応します。

モデルを調整する

symfonyにおいて、モデルレイヤーはもっとも遅いという評価があります。ベンチマークがこのレイヤーを最適化しなければならないことを示した場合、いくつかの改善方法を利用できます。

Propel統合を最適化する

モデルレイヤーの初期化(コアの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());

結果の変数$articlesArticleクラスのオブジェクト配列です。それぞれのオブジェクトの作成と初期化が行われるので、時間がかかります。これは大きな影響力を持ちます: データベースへの直接のクエリとは逆に、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クエリの制限を処理します。このクラスに関する詳細な情報はページャーのドキュメントを参照してください。

Joinでクエリの回数を最小にする

アプリケーションの開発期間において、それぞれのリクエストによって発行されるデータベースのクエリ数を監視すべきです。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を回避する

以前の例のように、オブジェクトが不要でさまざまなテーブルからいくつかのカラムのみが必要な場合、モデル内で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クエリは速くなりますが、INSERTUPDATE、と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>");
}

デフォルトのヘルパーを制限する

標準のヘルパーグループ(PartialCache、と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つです。キャッシュシステムを最大限活用したいのであれば、このセクションを読めば、おそらくあなたが考えていなかったいくつのトリックがわかります。

キャッシュの一部を選択してクリアする

アプリケーションの開発期間において、さまざまな状況でキャッシュをクリアしなければなりません:

キャッシュ全体のクリアに関連する問題は、コンフィギュレーションキャッシュが再生成される必要があるため、つぎのリクエストの処理時間がとても長くなることです。加えて、修正されなかったテンプレートも同じようにキャッシュからクリアされ、以前のリクエストの恩恵を失います。

このことは本当に再生成する必要のあるキャッシュファイルだけをクリアすることがよいアイディアであることを意味します。リスト18-14で示されるように、クリアするキャッシュファイルの部分集合を定義するにはclear-cacheタスクのオプションを使います。

リスト18-14 - キャッシュの選択した部分のみをクリアする

// myappアプリケーションのキャッシュのみをクリアする
> symfony clear-cache myapp

// myappアプリケーションのHTMLキャッシュのみをクリアする
> symfony clear-cache myapp template

// myappアプリケーションのコンフィギュレーションキャッシュのみをクリアする
> symfony clear-cache myapp 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]
<?php

define('SF_ROOT_DIR',    realpath(dirname(__FILE__).'/..'));
define('SF_APP',         'myapp');
define('SF_ENVIRONMENT', 'staging');
define('SF_DEBUG',       false);

require_once(SF_ROOT_DIR.DIRECTORY_SEPARATOR.'apps'.DIRECTORY_SEPARATOR.SF_APP.DIRECTORY_SEPARATOR.'config'.DIRECTORY_SEPARATOR.'config.php');

// ブラウジングする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によって再処理される必要はありません。これらのページの配信を加速するためにテンプレートキャッシュはすでに存在しますが、まだsymfonyに依存しています。

いくつかのページに関しては12章で説明されたトリックを組み合わせることでsymfonyを完全に回避できます。最初のトリックはプロキシとクライアントブラウザーがそれら自身でページをキャッシュするように求めるためにHTTP 1.1のヘッダーを利用する方法で、それらはつぎにページが必要なときにページを再リクエストしません。2番目のトリックはスーパーファーストキャッシュ(sfSuperCachePluginプラグインによって自動化される)です。Apacheがリクエストをsymfonyへ渡すまえに最初にキャッシュバージョンを探すように、これはweb/ディレクトリ内のレスポンスのコピーの保存と書き換えルールの修正から構成されます。

これらの両方の方法はとても効果的なので、静的なページに適用する場合でもページを処理する負担をsymfonyからとり除き、サーバーは複雑なリクエストを十分に対処できるようになります。

関数の呼び出し結果をキャッシュする

関数が文脈依存な値もしくはランダム性に依存しない場合、その関数を同じパラメーターで2回呼び出すと同じ値が返されます。これは最初に結果を保存していれば2番目の呼び出しは十分に回避できたことを意味します。sfFunctionCacheクラスが担う仕事はまさにこれです。このクラスのcall()メソッドは呼び出し可能なパラメーターのセットを必要とします。呼び出されたとき、このメソッドはすべての引数を用いてmd5ハッシュを作り、このハッシュによって名づけられたファイルのためにキャッシュディレクトリを探します。ファイルが見つかれば、関数はファイルに保存された結果を返します。そうではない場合、sfFunctionCacheクラスが関数を実行し、結果をキャッシュに保存し、それを返します。リスト18-16の2番目の関数の実行は最初のものより速いです。

リスト18-16 - 関数の結果をキャッシュする

[php]
$function_cache_dir = sfConfig::get('sf_cache_dir').'/function';
$fc = new sfFunctionCache($function_cache_dir);
$result1 = $fc->call('cos', M_PI);
$result2 = $fc->call('preg_replace', '/\s\s+/', ' ', $input);

sfFunctionCacheのコンストラクターは引数として絶対ディレクトリパスを必要とします(ディレクトリはオブジェクトをインスタンス化するよりまえに存在しなければなりません)。call()メソッドの最初の引数は呼び出し可能でなければならないので、指定できるのは関数名、クラス名と静的メソッド名の配列もしくはオブジェクト名とpublicなメソッド名の配列です。call()メソッドのほかの引数に関して、必要な数だけインクルードできます。それらすべてはcallableに渡されます。

このオブジェクトはCPUに負担をかける関数に対してとりわけ便利です。ファイルのI/Oのオーバーヘッドは単独の関数を処理するために必要な時間を超えるからです。このオブジェクトはsfFileCacheクラスに依存しています。このクラスはsymfonyのテンプレートキャッシュエンジンにも利用されるコンポーネントです。詳細についてはAPIドキュメントを参照してください。

CAUTION clear-cacheタスクはcache/ファイルの内容だけを削除します。どこかで関数のキャッシュを保存している場合、コマンドラインを通してキャッシュをクリアするときにこのキャッシュは自動的にクリアされません。

データをサーバーにキャッシュする

PHPアクセレータはデータをメモリに保存する特別な機能を提供するので複数のリクエストにまたがってデータを再利用できます。問題はこれらの機能が異なる構文を持ち、それぞれがこのタスクを実行するための独自の方法を持つことです。symfonyはsfProcessCacheクラスを提供します。sfProcessCacheクラスはこれらの違いを抽象化して利用しているアクセレータが何であれ連携します。リスト18-17で構文をご覧ください。

リスト18-17 - sfProcessCacheクラスの構文

[php]
// データをプロセスキャッシュに保存する
sfProcessCache::set($name, $value, $lifetime);

// データを読みとる
$value = sfProcessCache::get($name);

// プロセスキャッシュ内にデータの一部が存在するか確認する
$value_exists = sfProcessCache::has($name);

// プロセスキャッシュをクリアする
sfProcessCache::clear();

キャッシュ機能が動作しなかった場合set()メソッドはfalseを返します。キャッシュされた値は何でもなります(文字列、配列、オブジェクト); sfProcessCacheクラスはシリアル化を処理します。求められた変数がキャッシュ内に存在しなかった場合get()メソッドはnullを返します。

sfProcessCacheクラスのメソッドはアクセレータがインストールされていない場合でも動作します。それゆえ、フォールバックの値を提供しているかぎり、プロセスキャッシュからデータを読みとられるリスクは存在しません。たとえば、リスト18-18はプロセスキャッシュ内でコンフィギュレーション設定を読みとる方法を示しています。

リスト18-18 - プロセスキャッシュを安全に使う

[php]
if (sfProcessCache::has('myapp_parameters'))
{
  $params = sfProcessCache::get('myapp_parameters');
}
else
{
  $params = retrieve_parameters();
}

TIP メモリのキャッシュを詳しく研究したい場合、PHPのmemcache拡張機能のマニュアルをご覧ください。これはロードバランスが行われたアプリケーション上でデータベースのロードを減らすための助けを行い、PHP 5はそのためのOOインターフェイスを提供します。(http://jp.php.net/manual/ref.memcache.php)。

使わない機能を無効にする

symfonyのデフォルト設定ではWebアプリケーションのもっとも共通する機能を有効にしています。しかしながら、これらすべてが必要ではない場合、それぞれのリクエストごとに初期化にかかる時間を節約するためにこれらを無効にできます。

たとえば、アプリケーションがセッションのメカニズムを利用しない、もしくは手動でセッションの扱いを始めたい場合、リスト18-19のように、factories.ymlファイルのstorageキーのなかのauto_start設定をfalseに変えます。

リスト18-19 - セッションをオフにする(myapp/config/factories.yml)

all:
  storage:
    class: sfSessionStorage
    param:
      auto_start: false

同じことがデータベース(この章の前の方の"モデルを調整する"セクションで説明)と出力エスケーピング機能(7章を参照)にもあてはまります。アプリケーションがこれらの機能を利用しない場合、アプリケーションの小さなゲインのためにこれらを無効にします(リスト18-20を参照)。

リスト18-20 - 出力エスケーピング機能を無効にする(myapp/cofig/settings.yml)

all:
  .settings:
    use_database:      off    # データベースとモデルの機能
    escaping_strategy: off    # 出力エスケーピング機能

security属性とflash属性の機能(6章を参照)に関しては、リスト18-21で示されるように、filters.ymlファイルのなかで無効にできます。

リスト18-21 - 機能を無効にする(myapp/config/filters.yml)

rendering: ~
web_debug: ~
security:
  enabled: off

# 一般的に、ここの独自のフィルターを追加したい場合

cache:     ~
common:    ~
flash:
  enabled: off

execution: ~

いくつかの機能は開発環境のときだけ便利な機能なので運用環境ではこれらを有効にしないほうがいいでしょう。symfonyの運用環境のパフォーマンスは本当に最適化されているので、この方法はすでにデフォルトであてはまります。パフォーマンスが影響を与える開発機能のなかで、SF_DEBUGモードはもっとも厳しいものです。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のアプリケーションはすべてのコアクラス(sfActionssfRequestsfViewなど)を1つのファイルに統合し、コメントと二重の空白を除去し、ファイルサイズの最適化を行い、このファイルをキャッシュとconfig_core_compile.yml.phpファイルに保存します。それぞれのつぎのリクエストでは30個のファイルの代わりにこれらの内容で構成され最適化された単独のファイルだけがロードされます。

アプリケーションがつねにロードしなければならないクラスを持つ場合、とりわけ、これらのクラスが多くのコメントを持つ場合、これらのクラスをコアコンパイルファイルに追加することが有益であることがあります。そのためには、リスト18-22のように、アプリケーションのconfig/ディレクトリにcore_compile.ymlファイルを追加し、追加したいクラスの一覧を作ります。

リスト18-22 - コアコンパイレーションファイルにクラスを追加する(myapp/config/core_compile.yml)

- %SF_ROOT_DIR%/lib/myClass.class.php
- %SF_ROOT_DIR%/apps/myapp/lib/myToolkit.class.php
- %SF_ROOT_DIR%/plugins/myPlugin/lib/myPluginCore.class.php
...

sfOptimizerプラグイン

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タスクを呼び出します:

> symfony optimize myapp prod

ほかの最適化戦略をコードに適用したい場合、sfOptimizerプラグインがよい出発点になるかもしれません。

まとめ

symfonyはすでによく最適化されたフレームワークで、膨大なトラフィックを占めるWebサイトに問題なく対応できます。アプリケーションのパフォーマンスを本当に最適化する必要がある場合、設定(サーバーの設定、PHPの設定、もしくはアプリケーションの設定)を調整すれば少し速くなります。効率的なモデルメソッドを書くためによい習慣にも従うべきです。データベースはWebアプリケーションのボトルネックになることが多いので、この点にすべての注意を払うべきです。テンプレートはいくつかのトリックからも恩恵を受けますが、もっとも速いのはつねにキャッシュによるものです。最後に、既存のプラグインを探すことにためらわないでください。これらのなかにはWebページの配信をもっと加速する革新的な技術を提供するものがあるからです(sfSuperCachesfOptimizerなど)。