ステップ 29: パフォーマンスを管理する

5.0 version
Maintained

パフォーマンスを管理する

早すぎる最適化は諸悪の根源です。

過去にこの引用を見たことがあるかもしれませんが、完全な形で引用したいと思います:

だいたい97%くらいのケースでは、小さな効率化に関しては忘れるべきです。早まった最適化は諸悪の根源です。しかしながら、残りのクリティカルな3%内の効率化の機会を捨てないようにしましょう。

—ドナルド・クヌース

EコマースのようなWebサイトでは、小さなパフォーマンス改善が効いてくることがあります。ゲストブックアプリケーションが、大人気になったときのこと考えて、パフォーマンスの調べ方を見ていきましょう。

パフォーマンスの最適化を調べる最善の方法は、 プロファイラー を使うことです。現時点で最も人気のあるプロファイラーは、 Blackfire です(免責事項: 私は Blackfire プロジェクトの創設者でもあります)。

Blackfire を使ってみる

Blackfire は複数のパーツから構成されています:

  • プロファイルをトリガーする クライアント (Blackfire CLI ツールもしくは Google Chrome や Firefox のブラウザ拡張);
  • blackfire.io でデータの表示をするために送る前の、データを準備して集計する エージェント;
  • PHPコードを測定するPHP 拡張( probe

Blackfire を使うには、まず ユーザー登録 する必要があります。

次のインストールスクリプトを実行してローカルマシンに Blackfire をインストールしてください:

1
$ curl https://installer.blackfire.io/ | bash

このインストーラーは、 Blackfire CLI をダウンロードし、使用可能な PHP バージョンの全てに PHP probe をインストールします(有効にはしていません)。

プロジェクトで PHP Probe を有効化してください:

patch_file
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
--- a/php.ini
+++ b/php.ini
@@ -6,3 +6,7 @@ max_execution_time=30
 session.use_strict_mode=On
 realpath_cache_ttl=3600
 zend.detect_unicode=Off
+
+[blackfire]
+# use php_blackfire.dll on Windows
+extension=blackfire.so

Web サーバーをリスタートして、PHP に Blackfire をロードさせます:

1
2
$ symfony server:stop
$ symfony server:start -d

Blackfire CLI ツールは、あなたの クライアント のクレデンシャルで設定する必要があります(あなたの個人アカウント以下のプロジェクトプロファイルに格納されます)。 Settings/Credentials ページ の上部にありますので、次のコマンド内のプレースホルダーを入れ替えて実行してください:

1
$ blackfire config --client-id=xxx --client-token=xxx

注釈

全てインストールするには、 公式のインストールガイド に沿ってください。Blackfire をサーバーにインストールする際に便利です。

Docker に Blackfire のエージェントをセットアップする

最後に、 Blackfire エージェントのサービスを Docker Compose のスタックに追加します:

patch_file
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -20,3 +20,8 @@ services:
     mailer:
         image: schickling/mailcatcher
         ports: [1025, 1080]
+
+    blackfire:
+        image: blackfire/blackfire
+        env_file: .env.local
+        ports: [8707]

サーバーと通信するために、あなた個人の サーバー クレデンシャルが必要です(これらのプロファイルを格納するクレデンシャルのIDは、各プロジェクト毎に作成することができます); Settings/Credentials ページ の最下部にあります。ローカルの .env.local ファイルに書いておきましょう:

1
2
BLACKFIRE_SERVER_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
BLACKFIRE_SERVER_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

これで新しいコンテナを起動することができます:

1
2
$ docker-compose stop
$ docker-compose up -d

Blackfire のインストール失敗を修正する

プロファイリングの際にエラーになったら、 Blackfire のログレベルを変更し、より詳細の情報が吐かれるようにします:

patch_file
1
2
3
4
5
6
7
--- a/php.ini
+++ b/php.ini
@@ -10,3 +10,4 @@ zend.detect_unicode=Off
 [blackfire]
 # use php_blackfire.dll on Windows
 extension=blackfire.so
+blackfire.log_level=4

Webサーバーをリスタートしてください:

1
2
$ symfony server:stop
$ symfony server:start -d

ログを tail してください:

1
$ symfony server:log

もう一度プロファイルして、ログの出力をチェックしてください。

本番で Blackfire を設定する

全ての SymfonyCloud のプロジェクトでは、デフォルトで Blackfire が使用できます。

サーバー のクレデンシャルを、環境変数でセットアップしてください:

1
2
$ symfony var:set BLACKFIRE_SERVER_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
$ symfony var:set BLACKFIRE_SERVER_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

PHP probe を他の PHP 拡張と同じように有効化してください:

patch_file
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
--- a/.symfony.cloud.yaml
+++ b/.symfony.cloud.yaml
@@ -4,6 +4,7 @@ type: php:7.3

 runtime:
     extensions:
+        - blackfire
         - xsl
         - amqp
         - redis

Blackfire 用に Varnish を設定する

プロファイリング開始のためのデプロイをする前に、HTTP キャッシュの Varnish へのバイパスが必要です。バイパスなしでは、 Blackfire は、 PHP アプリケーションまで到達できません。ローカルマシンからのプロファイルのリクエストだけ許可を与えます。

あなたの現在の IP アドレスを調べてください:

1
$ curl https://ifconfig.me/

そして、そのIPアドレスを使って Varnish を設定してください:

patch_file
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
--- a/.symfony/config.vcl
+++ b/.symfony/config.vcl
@@ -1,3 +1,11 @@
+acl profile {
+   # Authorize the local IP address (replace with the IP found above)
+   "a.b.c.d";
+   # Authorize Blackfire servers
+   "46.51.168.2";
+   "54.75.240.245";
+}
+
 sub vcl_recv {
     set req.backend_hint = application.backend();
     set req.http.Surrogate-Capability = "abc=ESI/1.0";
@@ -8,6 +14,16 @@ sub vcl_recv {
         }
         return (purge);
     }
+
+    # Don't profile ESI requests
+    if (req.esi_level > 0) {
+        unset req.http.X-Blackfire-Query;
+    }
+
+    # Bypass Varnish when the profile request comes from a known IP
+    if (req.http.X-Blackfire-Query && client.ip ~ profile) {
+        return (pass);
+    }
 }

 sub vcl_backend_response {

これでデプロイができます。

Webページをプロファイルする

Firefox や Google Chrome の 専用の拡張 で、従来の Web ページのプロファイルをすることができます。

ローカルマシーンでは、public/index.php 内で HTTP キャッシュを無効化するのを忘れないでください。有効なままですと、コードのプロファイルではなく、 Symfony の HTTP Cache のプロファイルをすることになってしまいます:

patch_file
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
--- a/public/index.php
+++ b/public/index.php
@@ -24,7 +24,7 @@ if ($trustedHosts = $_SERVER['TRUSTED_HOSTS'] ?? $_ENV['TRUSTED_HOSTS'] ?? false
 $kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);

 if ('dev' === $kernel->getEnvironment()) {
-    $kernel = new HttpCache($kernel);
+//    $kernel = new HttpCache($kernel);
 }

 $request = Request::createFromGlobals();

本番でアプリケーションのパフォーマンスをより詳細に測るために、 "本番" 環境でもプロファイルすべきです。デフォルトでは、ローカルの環境は、オーバーヘッドの大きい "development" 環境を使用しています(WebデバッグツールバーやSymfony Profilerでデータを集めています)。

ローカルマシンを本番環境にスイッチするには、.env.local ファイルの環境変数 APP_ENV を変更します:

1
APP_ENV=prod

もしくは、 server:prod コマンドを使用することができます:

1
$ symfony server:prod

プロファイリングのセッションが終わったら、dev へ戻すのを忘れないでください:

1
$ symfony server:prod --off

API のリソースをプロファイルする

API や SPA のプロファイリングは、先程インストールした Blackfire CLI ツールから行った方がベターです:

1
$ blackfire curl `symfony var:export SYMFONY_DEFAULT_ROUTE_URL`api

blackfire curl コマンドは、 cURL コマンドと同じ引数とオプションを受け付けます。

パフォーマンスを比較する

"キャッシュ" でのステップでは、コードのパフォーマンスを改善するためにキャッシュレイヤーを追加しましたが、それによるパフォーマンスのインパクトを調べたり、測ったりはしていませんでした。何が早くて何が遅いかを想像でしか見ていなかったので、いくつかの最適化が実際はアプリケーションを遅くしている可能性もあります。

常にプロファイラーで最適化のインパクトを測るべきです。 Blackfire の 比較機能 を使うと視覚的に見ることができます。

ブラックボックスなファンクショナルテストを書く

Symfony でファンクショナルテストを書く方法を見てきましたが、 Blackfire player を使えば、ブラウジングのシナリオを書くことができます。新しいコメントを投稿し、開発環境ではメールのリンクで、本番環境では管理者によって、バリデートするシナリオを書いてみましょう。

次の内容で .blackfire.yaml ファイルを作成してください:

.blackfire.yaml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
scenarios: |
    #!blackfire-player

    group login
        visit url('/login')
        submit button("Sign in")
            param username "admin"
            param password "admin"
            expect status_code() == 302

    scenario
        name "Submit a comment on the Amsterdam conference page"
        include login
        visit url('/fr/conference/amsterdam-2019')
            expect status_code() == 200
        submit button("Submit")
            param comment_form[author] 'Fabien'
            param comment_form[email] '[email protected]'
            param comment_form[text] 'Such a good conference!'
            param comment_form[photo] file(fake('image', '/tmp', 400, 300, 'cats'), 'awesome-cat.jpg')
            expect status_code() == 302
        follow
            expect status_code() == 200
            expect not(body() matches "/Such a good conference/")
            # Wait for the workflow to validate the submissions
            wait 5000
        when env != "prod"
            visit url(webmail_url ~ '/messages')
                expect status_code() == 200
                set message_ids json("[*].id")
            with message_id in message_ids
                visit url(webmail_url ~ '/messages/' ~ message_id ~ '.html')
                    expect status_code() == 200
                    set accept_url css("table a").first().attr("href")
                visit url(accept_url)
                    # we don't check the status code as we can deal
                    # with "old" messages which do not exist anymore
                    # in the DB (would be a 404 then)
        when env == "prod"
            visit url('/admin/?entity=Comment&action=list')
                expect status_code() == 200
                set comment_ids css('table.table tbody tr').extract('data-id')
            with id in comment_ids
                visit url('/admin/comment/review/' ~ id)
                    # we don't check the status code as we scan all comments,
                    # including the ones already reviewed
        visit url('/fr/')
            wait 5000
        visit url('/fr/conference/amsterdam-2019')
            expect body() matches "/Such a good conference/"

Blackfire プレイヤーをダウンロードして、ローカルでシナリオを実行できるようにします:

1
2
$ curl -OLsS https://get.blackfire.io/blackfire-player.phar
$ chmod +x blackfire-player.phar

開発環境でシナリオを実行してください:

1
$ ./blackfire-player.phar run --endpoint=`symfony var:export SYMFONY_DEFAULT_ROUTE_URL` .blackfire.yaml --variable "webmail_url=`symfony var:export MAILER_WEB_URL 2>/dev/null`" --variable="env=dev"

本番でも実行してください:

1
$ ./blackfire-player.phar run --endpoint=`symfony env:urls --first` .blackfire.yaml --variable "webmail_url=NONE" --variable="env=prod"

Blackfire のシナリオは、 --blackfire フラグを付けることで、各リクエストでプロファイリングをトリガーbし、パフォーマンステストを実行することもできます。

パフォーマンスチェックを自動化する

パフォーマンスを管理することは既存のコードのパフォーマンスを改善するだけではなく、パフォーマンスが悪くなっていないかチェックすることでもあります。

前のセクションで書いたシナリオは、自動的にCI ワークフローで実行され、また、本番でも定期的に実行されます。

SymfonyCloud では、新しくブランチを作った際、または、本番にデプロイされた際に、run the scenarios で、新しいコードのパフォーマンスを自動的にチェックします。


  • « Previous ステップ 28: アプリケーションをローカライズする
  • Next » ステップ 30: Symfony の内部を知る

This work, including the code samples, is licensed under a Creative Commons BY-NC-SA 4.0 license.