プロダクション

BEAR.Sundayの既定のprod束縛をベースに、アプリケーション側で各デプロイ環境に応じたモジュールを追加・上書きしてカスタマイズします。

既定のProdModule

既定のprod束縛では、以下のインターフェイスが束縛されています。

  • エラーページ生成ファクトリー
  • PSRロガーインターフェース
  • ローカルキャッシュ
  • 分散キャッシュ

詳細はBEAR.PackageのProdModule.phpを参照してください。

アプリケーションのProdModule

既定のProdModuleに対してアプリケーションのProdModulesrc/Module/ProdModule.phpに設置してカスタマイズします。特にエラーページと分散キャッシュは重要です。

<?php
namespace MyVendor\Todo\Module;

use BEAR\Package\Context\ProdModule as PackageProdModule;
use BEAR\QueryRepository\CacheVersionModule;
use BEAR\Resource\Module\OptionsMethodModule;
use BEAR\Package\AbstractAppModule;

class ProdModule extends AbstractModule
{
    /**
     * {@inheritdoc}
     */
    protected function configure()
    {
        $this->install(new PackageProdModule);       // デフォルトのprod設定
        $this->override(new OptionsMethodModule);    // OPTIONSメソッドをプロダクションでも有効に
        $this->install(new CacheVersionModule('1')); // リソースキャッシュのバージョン指定
        
        // 独自のエラーページ
        $this->bind(ErrorPageFactoryInterface::class)->to(MyErrorPageFactory::class);
    }
}

キャッシュ

キャッシュには、ローカルキャッシュと複数のWebサーバー間で共有する分散キャッシュの2種類があります。どちらのキャッシュもデフォルトはPhpFileCacheです。

ローカルキャッシュ

ローカルキャッシュはデプロイ後に変更されないアノテーションなどのキャッシュに使われ、分散キャッシュはリソース状態の保存に使われます。

分散キャッシュ

2つ以上のWebサーバーでサービスを提供するには分散キャッシュの構成が必要です。代表的なキャッシュエンジンであるmemcachedRedis向けのモジュールが用意されています。

Memcached

<?php
namespace BEAR\HelloWorld\Module;

use BEAR\QueryRepository\StorageMemcachedModule;
use BEAR\Resource\Module\ProdLoggerModule;
use BEAR\Package\Context\ProdModule as PackageProdModule;
use BEAR\Package\AbstractAppModule;
use Ray\Di\Scope;

class ProdModule extends AbstractModule
{
    protected function configure()
    {
        // memcache
        // {host}:{port}:{weight},...
        $memcachedServers = 'mem1.domain.com:11211:33,mem2.domain.com:11211:67';
        $this->install(new StorageMemcachedModule($memcachedServers));
        
        // Prodロガーのインストール
        $this->install(new ProdLoggerModule);
        
        // デフォルトのProdModuleのインストール
        $this->install(new PackageProdModule);
    }
}

Redis

// redis
$redisServer = 'localhost:6379'; // {host}:{port}
$this->install(new StorageRedisModule($redisServer));

リソースの状態保存は単にTTLによる時間更新のキャッシュとの他に、TTL時間では消えない永続的なストレージとして(CQRS)の運用も可能です。その場合にはRedisで永続処理を行うか、Cassandraなどの他KVSのストレージアダプターを独自で用意する必要があります。

キャッシュ時間の指定

デフォルトのTTLを変更する場合StorageExpiryModuleをインストールします。

// Cache time
$short = 60;
$medium = 3600;
$long = 24 * 3600;
$this->install(new StorageExpiryModule($short, $medium, $long));

キャッシュバージョンの指定

リソースのスキーマが変わり、互換性が失われる時にはキャッシュバージョンを変更します。特にTTL時間で消えないCQRS運用の場合に重要です。

$this->install(new CacheVersionModule($cacheVersion));

ディプロイの度にリソースキャッシュを破棄するためには$cacheVersionに時刻や乱数の値を割り当てると変更が不要で便利です。

ログ

ProdLoggerModuleはプロダクション用のリソース実行ログモジュールです。インストールするとGET以外のリクエストをPsr\Log\LoggerInterfaceにバインドされているロガーでログします。

特定のリソースや特定の状態でログしたい場合は、カスタムのログをBEAR\Resource\LoggerInterfaceにバインドします。

use BEAR\Resource\LoggerInterface;
use Ray\Di\AbstractModule;

final class MyProdLoggerModule extends AbstractModule
{
    protected function configure(): void
    {
        $this->bind(LoggerInterface::class)->to(MyProdLogger::class);
    }
}

LoggerInterface__invokeメソッドでリソースのURIとリソース状態がResourceObjectオブジェクトとして渡されるのでその内容で必要な部分をログします。作成には既存の実装 ProdLoggerを参考にしてください。

デプロイ

⚠️ 上書き更新を避ける

サーバーにディプロイする場合

  • 駆動中のプロジェクトフォルダをrsyncなどで上書きするのはキャッシュやオンデマンドで生成されるファイルの不一致や、高負荷のサイトではキャパシティを超えるリスクがあります。安全のために別のディレクトリでセットアップを行い、そのセットアップが成功すれば切り替えるようにします。
  • DeployerBEAR.Sundayレシピを利用することができます。

クラウドにディプロイする時には

  • コンパイルが成功すると0、依存関係の問題を見つけるとコンパイラはexitコード1を出力します。それを利用してCIにコンパイルを組み込むことを推奨します。

コンパイル

セットアップ時にプロジェクトをウォームアップできます。DI/AOP 用の動的ファイルやアノテーションなどの静的キャッシュを事前に作成し、最適化された autoload.phppreload.php を出力します。

推奨は、ランタイムと同じアプリ Injector を使う Compiler::fromInjector() です(BEAR.Package 1.21+。スケルトンは bin/compile.php)。

// bin/compile.php
use BEAR\Package\Compiler;
use MyVendor\MyProject\Injector;

$context = $argv[1] ?? 'prod-app';

exit(Compiler::fromInjector(Injector::getInstance($context), $context)());
"scripts": {
    "compile": "php bin/compile.php prod-app"
}
  • コンパイルをすれば全てのクラスでインジェクションを行うのでランタイムでDIのエラーが出る可能性が極めて低くなります。
  • .envに含まれた内容はPHPファイルに取り込まれるのでコンパイル後に.envを消去可能です。コンテントネゴシエーションを行う場合など(例:api-app, html-app)1つのアプリケーションで複数コンテキストのコンパイルを行うときには、コンテキストごとに bin/compile.php を呼び、プロジェクト直下に出る autoload.php / preload.php を退避します(後続コンパイルで上書きされないようにします)。
php bin/compile.php prod-hal-api-app
mv autoload.php api.autoload.php
mv preload.php api.preload.php
php bin/compile.php prod-html-app

DI スクリプトの出力先は {tmpDir}/di です(既定の tmpDirvar/tmp/{context})。ほとんどのデプロイではこの既定のままで問題ありません。

vendor/bin/bear.compile は非推奨です。移行手順は BEAR.Package#482 を参照してください。

書き込み先を変える場合 {: #writable-paths }

プロジェクトツリー外に一時ファイルやログを置きたいときだけ使います(任意)。例としては、アプリ本体が読み取り専用のホスト(一部の serverless / コンテナ構成)や、ビルド時と実行時で書き込みパスが分かれる場合です。通常の VPS や共有ホストでは不要です。

Meta のコンストラクタに解決済みのパスを渡せます(環境変数の解釈はアプリ側の都合です)。Compiler::fromInjector() ならコンパイルも同じ Meta を使います。

new Meta($name, $context, $appDir, '/var/tmp/my-app', '/var/log/my-app');

autoload.php

{project_path}/autoload.phpに最適化されたautoload.phpファイルが出力されます。composer dump-autoload --optimizeで出力されるvendor/autoload.phpよりずっと高速です。

注意:preload.phpを利用する場合、ほとんどの利用クラスが読み込まれた状態で起動するのでコンパイルされたautoload.phpは不要です。composerが生成するvendor/autoload.phpをご利用ください。

preload.php

{project_path}/preload.phpに最適化されたpreload.phpファイルが出力されます。preloadを有効にするためにはphp.iniでopcache.preloadopcache.preload_userを指定する必要があります。

PHP 7.4でサポートされた機能ですが、7.4初期のバージョンでは不安定です。7.4.4以上の最新版を使いましょう。

例)

opcache.preload=/path/to/project/preload.php
opcache.preload_user=www-data

Note: パフォーマンスベンチマークはbenchmarkを参考にしてください。(2020年)

.compile.php

実環境ではないと生成ができないクラス(例えば認証が成功しないとインジェクトが完了しないResourceObject)がある場合には、コンパイル時にのみ読み込まれるダミークラス読み込みをルートの.compile.phpに記述することによってコンパイルをすることができます。

.compile.php

例) 例えばコンストラクタで認証が得られない場合に例外を出してしまうAuthProviderがあったとしたら以下のように空のクラスを作っておいて、.compile.phpに読み込ませます。

/tests/Null/AuthProvider.php

<?php
class AuthProvider 
{  // newをするだけのdummyなので実装は不要
}

.compile.php

<?php
require __DIR__ . '/tests/Null/AuthProvider.php'; // 常に生成可能なNullオブジェクト
$_SERVER[__REQUIRED_KEY__] = 'fake'; // 特定の環境変数がないとエラーになる場合

こうする事で例外を避けてコンパイルを行うことができます。他にもSymfonyのキャッシュコンポーネントはコンストラクタでキャッシュエンジンに接続を行うので、コンパイル時にはこのようにダミーのアダプターを読み込むようにしておくと良いでしょう。

tests/Null/RedisAdapter.php

namespace Ray\PsrCacheModule;

use Ray\Di\ProviderInterface;
use Serializable;
use Symfony\Component\Cache\Adapter\RedisAdapter as OriginAdapter;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;

class RedisAdapter extends OriginAdapter implements Serializable
{
    use SerializableTrait;

    public function __construct(ProviderInterface $redisProvider, string $namespace = '', int $defaultLifetime = 0, ?MarshallerInterface $marshaller = null)
    {
        // do nothing
    }
}

module.dot

コンパイルをすると”dotファイル”が出力されるのでgraphvizで画像ファイルに変換するか、GraphvizOnlineを利用すればオブジェクトグラフを表示することができます。スケルトンのオブジェクトグラフもご覧ください。

dot -T svg module.dot > module.svg

ブートストラップのパフォーマンスチューニング

immutable_cacheは、不変の値を共有メモリにキャッシュするためのPECLパッケージです。APCuをベースにしていますが、PHPのオブジェクトや配列などの不変の値を共有メモリに保存するため、APCuよりも高速です。また、APCuでもimmutable_cacheでも、PECLのIgbinaryをインストールすることでメモリ使用量が減り、さらなる高速化が期待できます。

現在、専用のキャッシュアダプターなどは用意されていません。ImmutableBootstrapを参考に、専用のBootstrapを作成し呼び出してください。初期化コストを最小限に抑え、最大のパフォーマンスを得ることができます。

php.ini

// エクステンション
extension="apcu.so"
extension="immutable_cache.so"
extension="igbinary.so"

// シリアライザーの指定
apc.serializer=igbinary
immutable_cache.serializer=igbinary