第18章 - パフォーマンス

大勢の訪問客をあなたの Web サイトに引き寄せたいのであれば、パフォーマンスと最適化の問題は開発フェーズにおいて重要な要素です。ご安心ください、パフォーマンスはつねに symfony のコア開発者の最重要の関心事です。

開発過程の加速によって得られた利益が多少のオーバーヘッドに終わる一方で symfony のコア開発者はパフォーマンスの要件をつねに意識してきました。したがって、すべてのクラスとメソッドは念入りに点検され可能なかぎり速く動作するように最適化されてきました。symfony の利用の有無にかかわらず「hello,world」を表示するために必要な時間を比較することで測定できる基本的なオーバーヘッドは最小です。結果として、symfony フレームワークはスケーラブルで、負荷テストによく対応します。最高の証明として、きわめて 膨大な トラフィック量を占めるいくつかの Web サイト(つまり何百万ものアクティブな購読者とサーバーに負荷を与える多くの Ajax インタラクションをかかえる Web サイト)が symfony を利用しており、パフォーマンスにとても満足しています。

しかし、もちろん、膨大なトラフィックを占める Web サイトはサーバーファームを拡張し運営者が最適だと思うハードウェアにアップグレードする方法を持つことがよくあります。これを実現するリソースが持たない場合、もしくは symfony フレームワークのフルパワーがつねに思いどおりに利用できることを確かめたい場合、symfony 製のアプリケーションをもっと加速するために利用できる調整方法がいくつかあります。この章ではフレームワークのすべてのレベルで推奨されるパフォーマンスの最適化方法のいくつかのリストを示します。これらの大半は上級ユーザー向けです。中にはすでに以前の章で触れられているものもありますが、一度にこれらすべてが有用であることを理解することになります。

サーバーを調整する

よく最適化されたアプリケーションはよく最適化されたサーバーに依存します。symfony の外部でボトルネックが存在しないことを確認するためにサーバーのパフォーマンスの調整方法の基本を理解しておく必要があります。アプリケーションが不必要に遅くないことを確認するための項目がいくつかあります。

php.ini ファイルのなかで magic_quotes_gpc ディレクティブを true にしておくと、アプリケーションが遅くなります。リクエストパラメーターのすべての引用符をエスケープするように PHP に伝えるからですが、symfony はこれらの引用符をあとで体系的にエスケープするので、結果として、時間のロスといくつかのプラットフォームで引用符のエスケープ問題が起きるだけです。ですので、PHP の設定にアクセスする権限があればこのディレクティブを off にしておいてください。

PHP は最新のリリースであるほど、パフォーマンスがよくなります(PHP 5.3 は PHP 5.2 よりも速いです)。ですので、パフォーマンスの恩恵を受けるには PHP を最新バージョンにアップグレードします。

運用サーバーでの PHP アクセラレータ (たとえば APC、XCache、eAccelerator)の利用はほぼ義務です。トレードオフなしで PHP の動作速度を平均で50%速くすることができるからです。PHPの本当の速度を体感するにはアクセラレータの拡張機能の1つをインストールしてください。

一方で、運用サーバーでは、Xdebug もしくは APD エクステンションといったデバッグユーティリティは無効にしてください。

mod_rewrite 拡張機能によって引き起こされるオーバーヘッドについて困ることがあるかもしれませんが無視できます。もちろん、書き換えルールで画像を読み込むことは書き換えルールなしのときよりも遅いですが、減速の規模の桁数はPHPステートメントの実行よりも下です。

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

モデルを調整する

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

Propel やDoctrine 統合を最適化する

モデルレイヤーの初期化 (コアの ORM クラス) は幾分か時間がかかります。いくつかのクラスをロードしてさまざまなオブジェクトをコンストラクトするからです。しかしながら、symfony が ORM を統合する方法のため、これらの初期化タスクはアクションが実際にモデルを必要とするときのみに起こり、しかもできるかぎり直前に行われます。ORM のクラスは生成モデルのオブジェクトがオートロードされたときのみに初期化されます。このことはモデルを使わないページはモデルレイヤーによるペナルティが課されないことを意味します。

アプリケーション全体でモデルレイヤーを使う必要がなければ、settings.ymlファイルのなかでレイヤー全体をオフに切り替えることで sfDatabaseManager を初期化しないですみます:

---
all:
  .settings:
    use_database: false

Propel の向上

生成されるモデルクラス (lib/model/om/) はすでに最適化されています。これらはコメントを含まず、オートロードシステムから恩恵を受けます。ファイルを手動でインクルードする代わりにオートロードに頼ることはクラスが本当に必要な場合だけロードされることを意味します。この場合において、モデルクラスは不要なので、クラスをオートロードすれば実行時間の節約になります。一方で include ステートメントを使う代わりの方法はそうではありません。コメントに関しては、これらは生成されるメソッドの使いかたをドキュメントにしますが、モデルファイルを長くします。結果として遅いディスク上では少々のオーバーヘッドになります。生成されるメソッドの名前はとても明快なので、デフォルトでコメントはオフに切り替えられます。

これら2つの強化方法は symfony 固有のものですが、つぎのように propel.ini ファイルのなかで2つの設定を変更することで Propel のデフォルト設定に戻すことができます:

propel.builder.addIncludes = true   # オートロードシステムに依存する代わりに
                                    # 生成クラスに include ステートメントを追加する
propel.builder.addComments = true   # 生成クラスにコメントを追加する

ハイドレイトするオブジェクトの数を制限する

オブジェクトを検索するピアクラスのメソッドを利用するとき、クエリはハイドレイト処理を行います (クエリの結果の列に基づいてオブジェクトの作成と投入を行う)。たとえば、Propel で article テーブルのすべての列を検索するには、通常つぎのように書きます:

$articles = ArticlePeer::doSelect(new Criteria());
 

結果の変数 $articlesArticle クラスのオブジェクト配列です。それぞれのオブジェクトの作成と初期化が行われるので、時間がかかります。これは大きな影響力を持ちます: データベースへの直接のクエリとは逆に、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 クエリの制限を処理します。

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

アプリケーションの開発期間において、それぞれのリクエストによって発行されるデータベースクエリの回数を監視すべきです。Web デバッグツールバーはそれぞれのページに対してクエリの回数を示し、小さなデータベースアイコンをクリックすればこれらのクエリの SQL コードが表示されます。クエリの回数が異つねに上昇するのを見かけたら Join の利用を考えるべきです。

Join メソッドを説明するまえに、リスト18-2で示されるように、オブジェクトの配列をループしていて、関連クラスの詳細を検索するために Propel のゲッターを使うときに何が起きているのかを検討しましょう。この例ではスキーマが author テーブルへの外部キーを持つ article テーブルを記載していることを前提にしています。

リスト18-2 - ループ内で関連クラスの詳細情報を検索する

// アクションのなか(Propel の場合)
$this->articles = ArticlePeer::doSelect(new Criteria());
// Doctrine の場合
$this->articles = Doctrine::getTable('Article')->findAll();
 
// 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つのデータベースクエリを発行する

// テンプレートのなか
$article->getAuthor()
 
// getAuthor() によって発行されるデータベースクエリ
SELECT author.id, author.name, ...
FROM   author
WHERE  author.id = ?                // ? は article.author_id
 

リスト18-2のページは合計で11のクエリを必要とします: 1つのクエリは Article オブジェクトの配列を作るために、残りの10のクエリは一度に1つの Author オブジェクトを作るために必要です。これは記事と著者の一覧だけを表示するためのたくさんのクエリになります。

Propelのクエリ最適化方法

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クラスの) オブジェクトの同じ配列を返します。違いが現れるのはあとで外部キーのゲッターがこれらのオブジェクトによって利用されるときです。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()
 

ピアクラスは doCount() メソッドのための Join メソッドも備えます。国際化の対応部分(13章を参照)を持つクラスは doSelectWithI18n()メソッドを提供します。このメソッドは国際化オブジェクト以外は Join メソッドと同じふるまいをします。モデルクラス内で利用可能な Join メソッドを見つけるには、lib/model/om/ディレクトリ内で生成されるピアクラスを調べてください。クエリに必要な Join メソッドが見つからない場合(たとえば、多対多のリレーションのために自動生成される Join メソッドが存在しない)、あなた自身でメソッドを作りモデルを拡張できます。

もちろん、doSelectJoinXXX() の呼び出しは doSelect() の呼び出しよりも少し遅いので、ハイドレイトされるオブジェクトをあとで利用する場合、これは全体のパフォーマンスを改善するだけです。

Doctrine の最適化

Doctrine には DQL(Doctrine Query Language) と呼ばれる独自のクエリ言語をがあります。構文は SQL にとても似ていますが、結果セットに変わってオブジェクトを取得します。SQL で、articleauthor テーブルのカラムを取得したいとします。DQLでは、クエリに JOIN 句をつけることにより、Doctrine が適切なオブジェクトにハイドレーションします。次は2つのテーブルを結合する方法を示したコードです:

// アクションのなか
Doctrine::getTable('Article')
  ->createQuery('a')
  ->innerJoin('a.Author') // "a.Author" は "Author" へのリレーション
  ->execute();
 
// テンプレートのなか(変わらず)
<ul>
<?php foreach ($articles as $article): ?>
  <li><?php echo $article->getTitle() ?>,
    written by <?php echo $article->getAuthor()->getName() ?></li>
<?php endforeach; ?>
</ul>
 

一時的な配列の利用を避ける

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());
// Doctrineの場合
$this->articles = Doctrine::getTable('Article')->findAll();
 
// テンプレートのなか
<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['title'] ?> (<?php echo $article['nb_comments'] ?> comments)</li>
<?php endforeach; ?>
</ul>
 

モデル内で速い処理である getArticleTitlesWithNbComments() メソッドを作るのはあなた次第です。たとえば、オブジェクトリレーショナルマッピングとデータベース抽象レイヤー全体を回避することによって行われます。

ORM を回避する

以前の例のように、オブジェクトが不要でさまざまなテーブルからいくつかのカラムのみが必要な場合、モデル内で ORM レイヤーを完全に回避する限定的なメソッドを作ることができます。たとえば、PDO を利用してデータベースを直接呼び出して特製の配列を返します。リスト18-9はこのアイディアを説明しています。

リスト18-9 - 最適化されたモデルメソッドのために PDO で直接データベースにアクセスする(lib/model/ArticlePeer.php)

// Propel の場合
class ArticlePeer extends BaseArticlePeer
{
  public static function getArticleTitlesWithNbComments()
  {
    $connection = Propel::getConnection();
    $query = 'SELECT %s as title, COUNT(%s) AS nb_comments 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('title' => $resultset->title, 'nb_comments' => $resultset->nb_comments);
    }
 
    return $results;
  }
}
 
// Doctrine の場合
class ArticleTable extends Doctrine_Table
{
  public function getArticleTitlesWithNbComments()
  {
    return $this->createQuery('a')
        ->select('a.title, count(*) as nb_comments')
        ->leftJoin('a.Comments')
        ->groupBy('a.id')
        ->fetchArray();
  }
}
 

この種のメソッドを作り始めるとき、それぞれのアクションに対して1つのカスタムメソッドを書くことで終わるので、階層分離の恩恵が失われます。データベースの独立性も失われることは言うまでもありません。

データベースを高速化する

symfony を利用するかかかわらず適用できるデータベース固有の最適化テクニックが多く存在します。このセクションは手短にもっとも共通のデータベース最適化戦略の要点をまとめていますが、モデルレイヤーを最大限利用するにはデータベースエンジンと管理方法に関して詳しい知識が必要です。

Web デバッグツールバーはそれぞれのクエリのために費やされる時間をページ単位で表示し、本当にパフォーマンスが改善されたのかを判断するためにすべての調整をモニタリングされることを覚えておいてください。

テーブルクエリは主キーではないカラムを基にすることがよくあります。このようなクエリの速さを改善するには、データベーススキーマのなかでインデックスを定義します。単独のカラムインデックスを追加するには、リスト18-10のように、index: true プロパティをカラムの定義に追加します。

リスト18-10 - 単独のカラムインデックスを追加する (config/schema.yml)

#

古典的なインデックスの代わりにユニークインデックスを定義するために代替の index: unique 構文を利用できます。schema.yml ファイルで複数のカラムインデックスを定義することもできます (インデックスの構文に関する詳細な情報は8章を参照)。この方法はしばし複雑なクエリを加速するのによいのでよく熟慮すべきです。

スキーマにインデックスを追加したあとで、ADD INDEX クエリを直接データベースに発行するか、propel-build-all コマンドを呼び出せばデータベース自身が同じことを行います (テーブル構造をリビルドするだけでなく、既存のすべてのデータを削除します)。

インデックスを作成することで SELECT クエリは速くなりますが、INSERTUPDATEDELETE が遅くなる傾向にあります。また、データベースエンジンは1つのクエリごとに1つのインデックスを使い、内部の経験則に基づいてそれぞれのクエリのために使われるインデックスを推測します。インデックスを追加するとパフォーマンスの加速に関してがっかりな結果になることも時折あるので、かならず改善結果を測定してください。

指定されないかぎり、symfony においてそれぞれのリクエストは単独のデータベース接続方法を利用し、接続はリクエストの終了時点で閉じられます。リスト18-11で示されるように、databases.yml ファイルのなかで persistent: true を設定することで、クエリのあいだに開いた状態を保つデータベースのコネクションプールを利用するための永続的なデータベース接続を有効にできます。

リスト18-11 - データベースの永続的な接続サポートを有効にする (config/databases.yml)

---
prod:
  propel:
    class:         sfPropelDatabase
    param:
      dsn:         mysql
      username:    username
      password:    password
      persistent:  true      # 永続的接続を使う

これがデータベース全体のパフォーマンスを改善をするのかどうかは多くの要素によります。この主題に関するドキュメントはインターネット上で豊富にあります。利点を検証するためにこの設定を変更する前あとでアプリケーションのパフォーマンスをかならずベンチマークしてください。

ビューを調整する

ビューレイヤーの設計と実装によって小さな減速もしくは加速が起きることにお気づきかもしれません。このセクションは代わりの方法とトレードオフについて説明します。

最速のコードフラグメントを使う

キャッシュシステムを利用しない場合、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', array('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>");
}
 

キャッシュを調整する

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 オブジェクトを利用する PHP スクリプトです。すでにこれは15章で検討しました。これは PHP で書かれた内部ブラウザーです(そして機能テストのために sfTestFunctional によって使われます)。これは外部 URL を取得しレスポンスを返しますが、興味深いことは通常のブラウザーのようにテンプレートキャッシュの生成機能を実行させることです。これは symfony を一度だけ初期化して HTTP 転送レイヤーを通さないので、この方法ははるかに速いです。

リスト18-15はステージング環境においてテンプレートキャッシュファイルを生成するために使われるスクリプトの例を示しています。このバッチは php generate_cache.php を呼び出すことで実行されます。

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

いくつかのページに関しては12章で説明したトリックを組み合わせることで symfony を完全に回避できます。最初のトリックはプロキシとクライアントブラウザーがそれら自身でページをキャッシュするように求めるために HTTP 1.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() メソッドの最初の引数は callable でなければならないので、関数名、クラス名とスタティックメソッド名の配列、もしくはオブジェクト名とパブリックメソッド名の配列でなければなりません。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 を返します。キャッシュされる値は何でもなります (文字列、配列、オブジェクト); sfApcCache クラスはシリアライズ処理を行います。キャッシュのなかで求められる変数が存在しなかった場合 get() メソッドは null を返します。

メモリキャッシュを詳しく研究したい場合、かならず sfMemcacheCache クラスを調べてください。これはほかのキャッシュクラスと同じインターフェイスを提供しロードバランスされるアプリケーション上のデータベースのロードを減らす助けを行うことができます。

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

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

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

リスト18-18 - セッションを無効にする (frontend/config/factories.yml)

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

同じことがデータベース機能にあてはまります (この章の前の方の"モデルを調整する"のセクションで説明されています)。アプリケーションがデータベースを利用しないのであれば、小さなパフォーマンスのゲインを得るために、今回は settings.yml ファイルでこの機能を無効にします(リスト18-19を参照)。

リスト18-19 - データベース機能を無効にする (frontend/cofig/settings.yml)

---
all:
  .settings:
    use_database:      false    # データベースとモデルの機能

セキュリティ機能(6章を参照)に関しては、リスト18-20で示されるように、filters.yml ファイルのなかで無効にできます。

リスト18-20 - セキュリティ機能を無効にする (frontend/config/filters.yml)

---
rendering:
security:
  enabled: false

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

cache:
execution:

いくつかの機能は開発環境のときだけ便利な機能なので運用環境ではこれらを有効にしないほうがいいでしょう。symfony の運用環境のパフォーマンスは本当に最適化されているので、すでにこの方法はデフォルトであてはまります。パフォーマンスに影響を与える開発機能のなかで、デバッグモードはもっとも厳しいものです。symfony のロギング機能に関して、運用環境ではこの機能もデフォルトでオフにされます。

ロギング機能を無効にしていて、開発環境だけで起きるわけでない問題を検討する場合、運用環境で失敗したリクエストに関する情報を取得する方法に悩んでいる人がいるかもしれません。幸いにして、symfony は sfErrorLoggerPlugin プラグインを利用できます。運用環境で sfErrorLoggerPluginプラグインはバックグラウンドで動作し、データベースに404エラーと500エラーの詳細を記録します。ファイルのロギング機能よりもずっと速いです。いったんロギングメカニズムがオンになると、レベルが何であれ無視できないオーバーヘッドを追加するのに対して、プラグインのメソッドはリクエストが失敗したときのみ呼び出されるからです。マニュアル でインストールの手引きを確認してください。

定期的にサーバーエラーのログをかならず確認してください。これらは404エラーと500エラーに関するとても価値のある情報も含みます。

コードを最適化する

コード自身を最適化することでアプリケーションを加速することも可能です。このセクションはこれを行う方法に関するいくつかの洞察を提供します。

コアコンパイレーション

10個のファイルをロードするには1個の長いファイルをロードするよりも多くの I/O オペレーションが必要です。とても長いファイルのロードは小さなファイルのロードよりも多くのリソースを必要とします。とりわけファイルの内容の大部分が PHP パーサーに無意味な場合、これはコメントがあてはまります。

多くのファイルを統合してこれらに含まれるコメントをとり除けばパフォーマンスの改善につながります。すでに symfony はその最適化の機能を持ちます; この機能はコアコンパイレーション (core compilation) と呼ばれます。最初のリクエストの始めに (もしくはキャッシュがクリアされた後に)、symfonyのアプリケーションはすべてのコアクラス (sfActionssfRequestsfViewなど)を1つのファイルに統合し、コメントと二重の空白を除去し、ファイルサイズの最適化を行い、このファイルをキャッシュとconfig_core_compile.yml.phpファイルに保存します。それぞれのつぎのリクエストでは30個のファイルの代わりにこれらの内容で構成され最適化された単独のファイルだけがロードされます。

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

リスト18-21 - クラスをコアコンパイレーションファイルに追加する (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
...

project:optimize タスク

symfony の最適化を行うもう1つのツールに、project:optimize タスクがあります。これは様々な最適化手法を symfony やアプリケーションのコードに適応し、実行速度のさらなる向上に繋げます。

$ php symfony optimize frontend prod

このタスクによる最適化手法の実装について知りたい場合は、タスクのソースコードを見てください。

まとめ

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

インデックス

Document Index

関連ページリスト

Related Pages

日本語ドキュメント

Japanese Documents

リリース情報
Release Information

Symfony2 に関する情報(公式) Books on symfony