Performance and Cache

Enabling the Built-in HTTP Cache Invalidation System

Exposing a hypermedia API has many advantages. One of them is the ability to know exactly which resources are included in HTTP responses created by the API. We used this specificity to make API Platform apps blazing fast.

ハイパーメディア API を公開すると、多くの利点があります。その 1 つは、API によって作成された HTTP 応答にどのリソースが含まれているかを正確に知る機能です。この特異性を利用して、API プラットフォーム アプリを非常に高速にしました。

When the cache mechanism is enabled, API Platform collects identifiers of every resource included in a given HTTP response (including lists, embedded documents and subresources) and returns them in a special HTTP header called Cache-Tags.

キャッシュ メカニズムが有効になっている場合、API プラットフォームは、特定の HTTP 応答に含まれるすべてのリソース (リスト、埋め込みドキュメント、サブリソースを含む) の識別子を収集し、Cache-Tags と呼ばれる特別な HTTP ヘッダーでそれらを返します。

A caching reverse proxy supporting cache tags (e.g. Varnish, Cloudflare, Fastly) must be put in front of the web server and store all responses returned by the API with a high TTL. This means that after the first request, all subsequent requests will not hit the web server, and will be served instantly from the cache.

キャッシュ タグ (Varnish、Cloudflare、Fastly など) をサポートするキャッシング リバース プロキシを Web サーバーの前に配置し、API から返されたすべての応答を高い TTL で保存する必要があります。つまり、最初の要求の後、後続のすべての要求はWeb サーバーにヒットし、キャッシュから即座に提供されます。

When a resource is modified, API Platform takes care of purging all responses containing it in the proxy’s cache. This ensures that the content served will always be fresh, because the cache is purged in real time. Support for most specific cases such as the invalidation of collections when a document is added or removed or for relationships and inverse relations is built-in.

リソースが変更されると、API プラットフォームは、プロキシのキャッシュに含まれるすべての応答を削除します。これにより、キャッシュがリアルタイムで消去されるため、提供されるコンテンツが常に最新の状態になります。ドキュメントが追加または削除されたときのコレクションの無効化、または関係および逆関係の組み込みなど、ほとんどの特定のケースのサポートが組み込まれています。

Integration with Varnish and Doctrine ORM is shipped with the core library, and Varnish is included in the Docker setup provided with the API Platform distribution. If you use the distribution, this feature works out of the box.

Varnish および Doctrine ORM との統合はコア ライブラリに同梱されており、Varnish は API Platform ディストリビューションで提供される Docker セットアップに含まれています。ディストリビューションを使用する場合、この機能はそのまま使用できます。

If you don't use the distribution, add the following configuration to enable the cache invalidation system:

ディストリビューションを使用しない場合は、次の構成を追加して、キャッシュ無効化システムを有効にします。

api_platform:
    http_cache:
        invalidation:
            enabled: true
            varnish_urls: ['%env(VARNISH_URL)%']
        public: true
    defaults:
        cache_headers:
            max_age: 0
            shared_max_age: 3600
            vary: ['Content-Type', 'Authorization', 'Origin']

Support for reverse proxies other than Varnish can easily be added by implementing the ApiPlatform\HttpCache\PurgerInterface. Two purgers are available, the http tags (api_platform.http_cache.purger.varnish.ban) or the surrogate key implementation (api_platform.http_cache.purger.varnish.xkey). You can specify the implementation using the purger configuration node, for example to use the xkey implementation:

Varnish 以外のリバース プロキシのサポートは、ApiPlatform\HttpCache\PurgerInterface を実装することで簡単に追加できます。http タグ (api_platform.http_cache.purger.varnish.ban) または代理キー実装 (api_platform.http_cache.purger) の 2 つのパージャーが利用可能です。 .varnish.xkey)。たとえば、 xkey 実装を使用するには、パージャー構成ノードを使用して実装を指定できます。

api_platform:
    http_cache:
        invalidation:
            enabled: true
            varnish_urls: ['%env(VARNISH_URL)%']
            purger: 'api_platform.http_cache.purger.varnish.xkey'
        public: true
    defaults:
        cache_headers:
            max_age: 0
            shared_max_age: 3600
            vary: ['Content-Type', 'Authorization', 'Origin']
            invalidation:
                xkey:
                    glue: ', '

In addition to the cache invalidation mechanism, you may want to use HTTP/2 Server Push to pre-emptively send relations to the client.

キャッシュ無効化メカニズムに加えて、HTTP/2 サーバー プッシュを使用して、先制的に関係をクライアントに送信することができます。

Extending Cache-Tags for Invalidation

Sometimes you need individual resources like /me. To work properly with Varnish, the Cache-Tags header needs to be augmented with these resources. Here is an example of how this can be done:

/me のような個別のリソースが必要な場合があります。 Varnish を適切に使用するには、これらのリソースで Cache-Tags ヘッダーを強化する必要があります。これを行う方法の例を次に示します。

<?php
// api/src/EventSubscriber/UserResourcesSubscriber.php
namespace App\EventSubscriber;

use ApiPlatform\Symfony\EventListener\EventPriorities;
use App\Entity\User;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;

final class UserResourcesSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents()
    {
        return [
            KernelEvents::REQUEST => ['extendResources', EventPriorities::POST_READ]
        ];
    }

    public function extendResources(RequestEvent $event): void
    {
        $request = $event->getRequest();
        $class = $request->attributes->get('_api_resource_class');

        if ($class === User::class) {
            $resources = [
                '/me'
            ];

            $request->attributes->set('_resources', $request->attributes->get('_resources', []) + (array)$resources);
        }
    }
}

Setting Custom HTTP Cache Headers

The cacheHeaders attribute can be used to set custom HTTP cache headers:

cacheHeaders 属性を使用して、カスタム HTTP キャッシュ ヘッダーを設定できます。

use ApiPlatform\Metadata\ApiResource;

#[ApiResource(
    cacheHeaders: [
        'max_age' => 60, 
        'shared_max_age' => 120, 
        'vary' => ['Authorization', 'Accept-Language']
    ]
)]
class Book
{
    // ...
}

For all endpoints related to this resource class, the following HTTP headers will be set:

このリソース クラスに関連するすべてのエンドポイントに対して、次の HTTP ヘッダーが設定されます。

Cache-Control: max-age=60, public, s-maxage=120
Vary: Authorization, Accept-Language

It's also possible to set different cache headers per operation:

操作ごとに異なるキャッシュ ヘッダーを設定することもできます。

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;

#[ApiResource]
#[Get(
    cacheHeaders: [
        'max_age' => 60, 
        'shared_max_age' => 120
    ]
)]
class Book
{
    // ...
}

Enabling the Metadata Cache

Computing metadata used by the bundle is a costly operation. Fortunately, metadata can be computed once and then cached. API Platform internally uses a PSR-6 cache. If the Symfony Cache component is available (the default in the API Platform distribution), it automatically enables support for the best cache adapter available.

バンドルで使用されるメタデータの計算は、コストのかかる操作です。幸いなことに、メタデータは一度計算してからキャッシュすることができます。API プラットフォームは内部的に PSR-6 キャッシュを使用します。 Symfony キャッシュ コンポーネントが利用可能な場合 (API プラットフォーム ディストリビューションのデフォルト)、利用可能な最適なキャッシュ アダプターのサポートが自動的に有効になります。

Best performance is achieved using APCu. Be sure to have the APCu extension installed on your production server. API Platform will automatically use it.

APCu を使用すると、最高のパフォーマンスが得られます。本番サーバーに APCu 拡張機能がインストールされていることを確認してください。 API Platform が自動的に使用します。

Using PPM (PHP-PM)

Response time of the API can be improved up to 15x by using PHP Process Manager. If you want to use it on your project, follow the documentation dedicated to Symfony on the PPM website.

PHP Process Manager を使用すると、API の応答時間を最大 15 倍改善できます。プロジェクトで使用する場合は、PPM Web サイトの Symfony 専用のドキュメントに従ってください。

Keep in mind that PPM is still in an early stage of development and can cause issues in production.

PPM はまだ開発の初期段階にあり、本番環境で問題が発生する可能性があることに注意してください。

Doctrine Queries and Indexes

Search Filter

When using the SearchFilter and case insensitivity, Doctrine will use the LOWER SQL function. Depending on your driver, you may want to carefully index it by using a function-based index or it will impact performance with a huge collection. Here are some examples to index LIKE filters depending on your database driver.

SearchFilter と大文字と小文字を区別しない機能を使用する場合、Doctrine は LOWER SQL 関数を使用します。ドライバーによっては、関数ベースのインデックスを使用して慎重にインデックスを作成する必要がある場合があります。そうしないと、膨大なコレクションでパフォーマンスに影響を与えます。データベース ドライバに応じて、LIKEfilter をインデックス化する例を次に示します。

Eager Loading

By default Doctrine comes with lazy loading - usually a killer time-saving feature but also a performance killer with large applications.

デフォルトでは、Doctrine には遅延読み込みが付属しています。これは通常、時間を大幅に節約する機能ですが、大規模なアプリケーションではパフォーマンスを低下させます。

Fortunately, Doctrine offers another approach to solve this problem: eager loading. This can easily be enabled for a relation: #[ORM\ManyToOne(fetch: "EAGER")].

幸いなことに、Doctrine はこの問題を解決するための別のアプローチを提供しています: 熱心な読み込みです。

By default in API Platform, we made the choice to force eager loading for all relations, with or without the Doctrine fetch attribute. Thanks to the eager loading extension. The EagerLoadingExtension will join every readable association according to the serialization context. If you want to fetch an association that is not serializable, you have to bypass readable and readableLink by using the fetchEager attribute on the property declaration, for example:

API プラットフォームのデフォルトでは、Doctrinefetch 属性の有無にかかわらず、すべてのリレーションに対して熱心な読み込みを強制するように選択しました。熱心な読み込み拡張機能に感謝します。 EagerLoadingExtension は、シリアル化コンテキストに従ってすべての読み取り可能な関連付けに参加します。シリアル化できない関連付けをフェッチする場合は、プロパティ宣言で fetchEager 属性を使用して、読み取り可能および読み取り可能Link をバイパスする必要があります。次に例を示します。

...

#[ApiProperty(fetchEager: true)]
public $foo;

...

Max Joins

There is a default restriction with this feature. We allow up to 30 joins per query. Beyond that, an ApiPlatform\Exception\RuntimeException exception will be thrown but this value can easily be increased with a bit of configuration:

この機能にはデフォルトの制限があります。クエリごとに最大 30 個の結合が許可されます。それを超えると、ApiPlatform\Exception\RuntimeException 例外がスローされますが、この値は少しの構成で簡単に増やすことができます。

# api/config/packages/api_platform.yaml
api_platform:
    eager_loading:
        max_joins: 100

Be careful when you exceed this limit, it's often caused by the result of a circular reference. Serializer groups can be a good solution to fix this issue.

この制限を超える場合は注意してください。多くの場合、循環参照の結果が原因です。シリアライザー グループは、この問題を解決するための優れたソリューションとなる可能性があります。

Fetch Partial

If you want to fetch only partial data according to serialization groups, you can enable fetch_partial parameter:

シリアル化グループに従って部分的なデータのみをフェッチする場合は、fetch_partial パラメータを有効にできます。

# api/config/packages/api_platform.yaml
api_platform:
    eager_loading:
        fetch_partial: true

It is disabled by default. If enabled, Doctrine ORM entities will not work as expected if any of the other fields are used.

デフォルトでは無効になっています。有効にすると、他のフィールドが使用されている場合、Doctrine ORM エンティティは期待どおりに機能しません。

Force Eager

As mentioned above, by default we force eager loading for all relations. This behaviour can be modified in the configuration in order to apply it only on join relations having the EAGER fetch mode:

前述のように、デフォルトでは、すべてのリレーションに対して熱心な読み込みを強制します。この動作は、EAGER フェッチ モードを持つ結合関係にのみ適用するために、構成で変更できます。

# api/config/packages/api_platform.yaml
api_platform:
    eager_loading:
        force_eager: false

Override at Resource and Operation Level

When eager loading is enabled, whatever the status of the force_eager parameter, you can easily override it directly from the configuration of each resource. You can do this at the resource level, at the operation level, or both:

積極的な読み込みが有効になっている場合、force_eager パラメーターのステータスに関係なく、各リソースの構成から直接簡単にオーバーライドできます。これは、リソース レベル、工程レベル、またはその両方で行うことができます。

<?php
// api/src/Entity/Address.php
namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
#[ApiResource]
class Address
{
    // ...
}
<?php
// api/src/Entity/User.php
namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
#[ApiResource(forceEager: false)]
class User
{
    #[ORM\ManyToOne(fetch: 'EAGER')]
    public Address $address;

    /**
     * @var Group[]
     */
    #[ORM\ManyToMany(targetEntity: 'Group', inversedBy: 'users')]
    #[ORM\JoinTable(name: 'users_groups')]
    public $groups;

    // ...
}
<?php
// api/src/Entity/Group.php
namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\GetCollection;
use Doctrine\ORM\Mapping as ORM;

#[ApiResource(forceEager: false)]
#[Get(forceEager: true)]
#[Post]
#[GetCollection(forceEager: true)]
#[ORM\Entity]
class Group
{
    /**
     * @var User[]
     */
    #[ORM\ManyToMany(targetEntity: 'User', mappedBy: 'groups')] 
    public $users;

    // ...
}

Be careful, the operation level is higher priority than the resource level but both are higher priority than the global configuration.

操作レベルはリソース レベルよりも優先度が高くなりますが、どちらもグローバル構成よりも優先度が高いことに注意してください。

Disable Eager Loading

If for any reason you don't want the eager loading feature, you can turn it off in the configuration:

何らかの理由で熱心な読み込み機能が必要ない場合は、構成でオフにすることができます。

# api/config/packages/api_platform.yaml
api_platform:
    eager_loading:
        enabled: false

The whole configuration described before will no longer work and Doctrine will recover its default behavior.

前述の構成全体が機能しなくなり、Doctrine はデフォルトの動作に戻ります。

Partial Pagination

When using the default pagination, the Doctrine paginator will execute a COUNT query on the collection. The result of the COUNT query is used to compute the last page available. With big collections this can lead to quite long response times. If you don't mind not having the last page available, you can enable partial pagination and avoid the COUNT query:

デフォルトのページネーションを使用する場合、Doctrine ページネーターはコレクションに対して COUNT クエリを実行します。 COUNT クエリの結果は、利用可能な最後のページを計算するために使用されます。コレクションが大きいと、応答時間が非常に長くなる可能性があります。最後のページが表示されなくてもかまわない場合は、部分的なページネーションを有効にして COUNT クエリを回避できます。

# api/config/packages/api_platform.yaml
api_platform:
    collection:
        pagination:
            partial: true # Disabled by default

More details are available on the pagination documentation.

詳細については、ページネーションのドキュメントを参照してください。

Profiling with Blackfire.io

Blackfire.io allows you to monitor the performance of your applications. For more information, visit the Blackfire.io website.

Blackfire.io を使用すると、アプリケーションのパフォーマンスを監視できます。詳細については、Blackfire.io の Web サイトをご覧ください。

To configure Blackfire.io follow these simple steps:

Blackfire.io を構成するには、次の簡単な手順に従います。

  1. Add the following to your docker-compose.override.yml file:

    以下を docker-compose.override.yml ファイルに追加します。

    yaml blackfire: image: blackfire/blackfire:2 environment: # Exposes the host BLACKFIRE_SERVER_ID and TOKEN environment variables. - BLACKFIRE_SERVER_ID - BLACKFIRE_SERVER_TOKEN - BLACKFIRE_DISABLE_LEGACY_PORT=1

    yamlblackfire:image: blackfire/blackfire:2environment:# ホストの BLACKFIRE_SERVER_ID および TOKEN 環境変数を公開します。- BLACKFIRE_SERVER_ID- BLACKFIRE_SERVER_TOKEN- BLACKFIRE_DISABLE_LEGACY_PORT=1

  2. Add your Blackfire.io ID and server token to your .env file at the root of your project (be sure not to commit this to a public repository):

    プロジェクトのルートにある .env ファイルに Blackfire.io ID とサーバー トークンを追加します (これをパブリック リポジトリにコミットしないようにしてください)。

    shell BLACKFIRE_SERVER_ID=xxxxxxxxxx BLACKFIRE_SERVER_TOKEN=xxxxxxxxxx

    shellBLACKFIRE_SERVER_ID=xxxxxxxxxxBLACKFIRE_SERVER_TOKEN=xxxxxxxxxx

    Or set it in the console before running Docker commands:

    または、Docker コマンドを実行する前にコンソールで設定します。

    shell export BLACKFIRE_SERVER_ID=xxxxxxxxxx export BLACKFIRE_SERVER_TOKEN=xxxxxxxxxx

    shellexport BLACKFIRE_SERVER_ID=xxxxxxxxxxexport BLACKFIRE_SERVER_TOKEN=xxxxxxxxxx

  3. Install and configure the Blackfire probe in the app container, by adding the following to your ./Dockerfile:

    以下を ./Dockerfile に追加して、アプリ コンテナに Blackfire プローブをインストールして設定します。

    dockerfile RUN version=$(php -r "echo PHP_MAJOR_VERSION.PHP_MINOR_VERSION;") \ && curl -A "Docker" -o /tmp/blackfire-probe.tar.gz -D - -L -s https://blackfire.io/api/v1/releases/probe/php/alpine/amd64/$version \ && mkdir -p /tmp/blackfire \ && tar zxpf /tmp/blackfire-probe.tar.gz -C /tmp/blackfire \ && mv /tmp/blackfire/blackfire-*.so $(php -r "echo ini_get('extension_dir');")/blackfire.so \ && printf "extension=blackfire.so\nblackfire.agent_socket=tcp://blackfire:8307\n" > $PHP_INI_DIR/conf.d/blackfire.ini

    dockerfileRUN version=$(php -r "echo PHP_MAJOR_VERSION.PHP_MINOR_VERSION;") \& curl -A "Docker" -o /tmp/blackfire-probe.tar.gz -D - -L -s https://blackfire.io /api/v1/releases/probe/php/alpine/amd64/$version \&& mkdir -p /tmp/blackfire \&& tar zxpf /tmp/blackfire-probe.tar.gz -C /tmp/blackfire \&& mv / tmp/blackfire/blackfire-*.so $(php -r "echo ini_get('extension_dir');

  4. Rebuild and restart all your containers

    すべてのコンテナを再構築して再起動します

    console docker compose build docker compose up -d

    consoledocker compose builddocker compose up -d

For details on how to perform profiling, see the Blackfire.io documentation.

プロファイリングの実行方法の詳細については、Blackfire.io のドキュメントを参照してください。