パフォーマンス・チューニング

あなたのウェブ・アプリケーションのパフォーマンスに影響を及ぼす要因は数多くあります。 環境の要因もありますし、あなたのコードに関係する要因もあります。また、Yii そのものに関係する要因もあります。 このセクションでは要因のほとんどを列挙して、どのようにそれらを修正すればあなたのアプリケーションのパフォーマンスを向上させることが出来るかを説明します。

PHP 環境を最適化する

PHP 環境を正しく構成することは非常に重要です。最大のパフォーマンスを得るためには、

  • 最新の安定した PHP バージョンを使うこと。使用する PHP のメジャー・リリースを上げると、顕著なパフォーマンスの改善がもたらされることがあります。
  • Opcache (PHP 5.5 以降) または APC (PHP 5.4) を使って、 バイト・コード・キャッシュを有効にすること。 バイト・コード・キャッシュによって、リクエストが入ってくるたびに PHP スクリプトを解析してインクルードする時間の浪費を避けることが出来ます。
  • realpath() キャッシュをチューニングする.

デバッグ・モードを無効にする

本番環境でアプリケーションを実行するときには、デバッグ・モードを無効にしなければなりません。 Yii は、YII_DEBUG という名前の定数の値を使って、デバッグ・モードを有効にすべきか否かを示します。 デバッグ・モードが有効になっているときは、Yii はデバッグ情報の生成と記録のために時間を余計に費やします。

エントリ・スクリプト の冒頭に次のコード行を置くことによってデ バッグ・モードを無効にすることが出来ます。

defined('YII_DEBUG') or define('YII_DEBUG', false);

Info: YII_DEBUG のデフォルト値は false です。 従って、アプリケーション・コードの他のどこかでこのデフォルト値を変更していないと確信できるなら、単に上記の行を削除してデバッグ・モードを無効にしても構いません。

キャッシュのテクニックを使う

さまざまなキャッシュのテクニックを使うと、あなたのアプリケーションのパフォーマンスを目に見えて改善することが出来ます。 たとえば、あなたのアプリケーションが Markdown 形式のテキスト入力をユーザに許可している場合、解析済みの Markdown のコンテントをキャッシュすることを考慮してください。 そうすれば、リクエストごとに毎回同じ Markdown テキストの解析を繰り返すことを回避できるでしょう。 Yii によって提供されているキャッシュのサポートについて学ぶためには キャッシュ のセクションを参照してください。

スキーマ・キャッシュを有効にする

スキーマ・キャッシュは、アクティブ・レコード を使おうとする場合には、いつでも有効にすべき特別なキャッシュ機能です。 ご存じのように、アクティブ・レコードは、賢いことに、あなたがわざわざ記述しなくても、 DB テーブルに関するスキーマ情報 (カラムの名前、カラムのタイプ、外部キー制約など) を自動的に検出します。 アクティブ・レコードはこの情報を取得するために追加の SQL クエリを実行しています。 スキーマ・キャッシュを有効にすると、取得されたスキーマ情報はキャッシュに保存されて将来のクエリで再利用されるようになります。

スキーマ・キャッシュを有効にするためには、アプリケーションの構成情報 の中で、 cache アプリケーション・コンポーネント にスキーマ情報を保存するように構成し、yii\db\Connection::$enableSchemaCachetrue に設定します。

return [
    // ...
    'components' => [
        // ...
        'cache' => [
            'class' => 'yii\caching\FileCache',
        ],
        'db' => [
            'class' => 'yii\db\Connection',
            'dsn' => 'mysql:host=localhost;dbname=mydatabase',
            'username' => 'root',
            'password' => '',
            'enableSchemaCache' => true,

            // スキーマ・キャッシュの持続時間
            'schemaCacheDuration' => 3600,

            // スキーマ情報を保存するのに使用されるキャッシュ・コンポーネントの名前
            'schemaCache' => 'cache',
        ],
    ],
];

アセットを結合して最小化する

複雑なウェブ・ページでは、多数の CSS や JavaScript のアセット・ファイルをインクルードすることがよくあります。 HTTP リクエストの回数、および、これらのアセットの全体としてのダウンロード・サイズを削減するために、アセットを単一のファイルに結合して、それを圧縮することを考慮すべきです。 これによって、ページのロードにかかる時間とサーバの負荷を大きく削減することが出来ます。 詳細については、アセット のセクションを参照してください。

セッションのストレージを最適化する

デフォルトでは、セッションのデータはファイルに保存されます。 これは、session_write_close() が呼ばれる (Yii では Yii::$app->session->close() によって呼び出されます) か、あるいはリクエストの処理が終了して、セッションが閉じられる時点まで、ファイルが開かれるのをロックするという実装になっています。 セッション・ファイルがロックされている間は、同じセッションを使用しようとする全てのリクエストはブロックされ、最初のリクエストがセッション・ファイルを解放するのを待たなければなりません。 開発時はこれでも構いません。おそらく、小さなプロジェクトでも、これで大丈夫でしょう。 しかし、大量のリクエストを並列処理するとなると、データベースのような、もっと洗練されたストレージを使う方が良いでしょう。 Yii はさまざまなセッション・ストレージのサポートを内蔵しています。 これらのストレージは、アプリケーションの構成情報 の中で session コンポーネントを次のように構成することによって使用することが出来ます。

return [
    // ...
    'components' => [
        'session' => [
            'class' => 'yii\web\DbSession',

            // デフォルトの 'db' 以外の DB コンポーネントを使用したい場合は
            // 以下を設定する
            // 'db' => 'mydb',

            // デフォルトの session テーブルをオーバーライドするためには以下を設定する
            // 'sessionTable' => 'my_session',
        ],
    ],
];

上記の構成は、セッション・データの保存にデータベース・テーブルを使用するものです。 デフォルトでは、db アプリケーション・コンポーネントをデータベース接続として使用し、セッション・データを session テーブルに保存します。 ただし、前もって session テーブルを次のように作っておく必要があります。

CREATE TABLE session (
    id CHAR(40) NOT NULL PRIMARY KEY,
    expire INTEGER,
    data BLOB
)

yii\web\CacheSession を使って、セッションをキャッシュに保存することも出来ます。 理論上、サポートされている キャッシュ・ストレージ のどれでも使うことが出来ます。 ただし、キャッシュ・ストレージの中には、容量の上限に達したときにキャッシュされたデータをフラッシュするものがあることに注意してください。 この理由により、主として容量の上限が無い種類のキャッシュ・ストレージを使用すべきです。

あなたのサーバに Redis がある場合は、yii\redis\Session によって redis をセッション・ストレージとして使用することを強く推奨します。

データベースを最適化する

DB クエリの実行とデータベースからのデータ取得がウェブ・アプリケーションのパフォーマンスの主たるボトルネックになることがよくあります。 データ・キャッシュ の使用によってパフォーマンスの劣化を緩和することは出来ますが、問題を完全に解決することは出来ません。 データベースが膨大なデータを抱えている場合、キャッシュされたデータが無効化されたときに最新のデータを取得するためのコストは、 データベースとクエリが適切に設計されていないと、法外なものになり得ます。

DB クエリのパフォーマンスを向上させるための一般的なテクニックは、フィルタの対象になるテーブル・カラムにインデックスを作成することです。 例えば、username によってユーザのレコードを検索する必要があるなら、username に対してインデックスを作成するべきです。 ただし、インデックスを付けると SELECT クエリを非常に速くすることが出来る代りに、INSERT、UPDATE、または DELTE のクエリが遅くなることに注意してください。

複雑な DB クエリについては、クエリの解析と準備の時間を節約するために、データベース・ビューを作成することが推奨されます。

最後にもう一つ大事なことですが、SELECT クエリで LIMIT を使ってください。 こうすることで、大量のデータが返されて、PHP のために確保されたメモリを使い尽くすということがなくなります。

プレーンな配列を使う

アクティブ・レコード は非常に使い勝手のよいものですが、データベースから大量のデータを取得する必要がある場合は、プレーンな配列を使うほどには効率的ではありません。 そういう場合は、アクティブ・レコードを使ってデータを取得する際に asArray() を呼んで、 取得したデータがかさばるアクティブ・レコードのオブジェクトではなく配列として表現されるようにすることを考慮するのが良いでしょう。 例えば、

class PostController extends Controller
{
    public function actionIndex()
    {
        $posts = Post::find()->limit(100)->asArray()->all();
        
        return $this->render('index', ['posts' => $posts]);
    }
}

上記において、$posts は、テーブル行の配列としてデータを代入されることになります。各行はプレーンな配列になります。 $i 番目の行の title カラムにアクセスするためには、$posts[$i]['title'] という式を使うことが出来ます。

クエリを構築するのに DAO を使って、データをプレーンな配列に取得することも出来ます。

Composer オートローダを最適化する

Composer のオートローダは、ほとんどのサードパーティのクラス・ファイルをインクルードするのに使われますので、 次のコマンドを実行して Composer のオートローダを最適化することを考慮すべきです。

composer dumpautoload -o

さらに加えて、 authoritative class maps および APCu cache の使用を検討して下さい。 ただし、この二つの最適化があなたの特定のケースに適切である場合もあれば、そうでない場合もあります。

オフラインでデータを処理する

リクエストが何らかのリソース集約的な操作を必要とするものである場合は、そういう操作が終るまでユーザを待たせずに、 オフラインモードで操作を実行する方策を考えるべきです。

オフラインでデータを処理するための方法が二つあります。すなわち、プルとプッシュです。

プルの方法では、リクエストが何らかの複雑な操作を必要とするたびに、タスクを作成してデータベースなどの持続的ストレージに保存します。 そうしておいて、別の独立したプロセス (例えばクロンジョブ) を使い、タスクを引き出して処理します。 この方法は、実装は容易ですが、いくつかの欠点があります。 例えば、タスクのプロセスはストレージから定期的にタスクを引き出さなければなりません。 引き出す間隔が長すぎると、タスクの処理に大きな遅延が生じます。しかし、間隔が短すぎると、オーバーヘッドが大きくなります。

プッシュの方法では、タスクを管理するのにメッセージ・キュー (例えば、RabbitMQ、ActiveMQ、Amazon SQS など) を使用します。 新しいタスクがキューに入れられるたびに、タスクを処理するプロセスが起動されたり通知を受けたりして、タスク処理がトリガされます。

パフォーマンス・プロファイリング

あなたは、あなたのコードをプロファイルして、パフォーマンスのボトルネックを発見し、それに応じた適切な手段を講じるべきです。 次のプロファイリング・ツールが役に立つでしょう。

アプリケーションをスケーラブルなものにする覚悟を決める

何をやっても助けにならないときは、あなたのアプリケーションをスケーラブルにすることを試みましょう。良い導入記事が Configuring a Yii 2 Application for an Autoscaling Stack (Yii 2 アプリケーションを自動スケール環境のために構成する) の中で提供されています。