コンテンツにスキップ
getnextpdf.com

トラブルシューティング:メモリとパフォーマンス

これらのエントリーは、負荷下で遭遇する 2 つの失敗のファミリーを扱います。レンダリング中に PHP がメモリを使い果たすことと、プロセスがウォームになったり飽和したりするとスループットが崖から落ちることです。各エントリーは、症状、もっともありそうな原因、 そして実際の NextPDF の API または標準の PHP-FPM コントロールを使う修正を名指しします。 背後のストリーミングモデルとワーカーのチュートリアルについては、ストリーミングとメモリ を読んでください。このページは、 その障害対応側の伴侶です。

まず計測します。レンダリングの前後で memory_get_peak_usage(true) をサンプリングし、 反復の間に memory_reset_peak_usage() を呼び出します。エンジンのベンチマークがレンダリングごとのコストを分離するのと同じやり方です。ベースラインなしのチューニングは、 崖を取り除くのではなく動かすだけです。

  • 症状。 レンダリングが、しばしば大きな、または画像の多いドキュメントで、PHP ランタイムからの致命的な Allowed memory size of <n> bytes exhausted で中断します。
  • ありそうな原因。 デフォルトの書き込みパスはドキュメント全体を構成してからシリアライズするため、ピークメモリは出力サイズの合計を追います。大きなドキュメント、 大きな埋め込み画像、または大きな埋め込みフォントフェイスが、リクエストを memory_limit を超えて押し上げる可能性があります。
  • 解決。
    1. 画像キャッシュを境界付ける。 NextPDF\Core\ConfigimageCacheBytes (デフォルト 52428800、つまり 50 MB)を公開します。インスタンスの wither $config->withImageCacheBytes($bytes)(シグネチャ withImageCacheBytes(int $bytes): self)でそれを下げて、多くの画像を埋め込むビルドが、スワップする代わりに既知の上限で速やかに失敗するようにします。これはメモリ内の画像キャッシュを上限で抑えます。画像自体を再サンプリングや再エンコードはしません。
    2. 埋め込む前に入力を縮小する。 Core は画像をダウンスケールや再エンコードしません。 過大なラスターアートを埋め込む 前に リサイズして再エンコードし、実際に使うフォントを埋め込んで、サブセット化が保持すべきグリフセットを小さく保ちます (PDF ファイルサイズを削減する を参照)。
    3. 圧縮を有効に保つ。 新しい Configcompresstrue に設定されています。 通常のビルドではそのままにしてください。withCompress(false) はサイズ最適化ではありません(通常は出力が増えます)。パイプラインをデバッグまたはプロファイルするために手を伸ばしてください――それはメモリを減らすのではなく、CPU/メモリのトレードオフをシフトします(圧縮ステップをスキップする)。
    4. memory_limit をワーカーごとに意図的に上げる。 これは NextPDF のキーではなく、標準の PHP 設定です。プール構成で、または CLI/キュープロセスに対して ini_set('memory_limit', '256M') で設定し、推測ではなくプロファイルされたピークに合わせてサイズを決めてください。
  • 関連。 ストリーミングとメモリ

エントリー: 非常に大きなドキュメントでメモリがページ数とともに増える

「エントリー: 非常に大きなドキュメントでメモリがページ数とともに増える」という見出しのセクション
  • 症状。 各ページが小さくても数千ページのドキュメントがメモリを使い果たし、ピークがページ数とおおよそ歩調を合わせて上昇します。
  • ありそうな原因。 バッファリングするライターは、シリアライズされたドキュメント全体をヒープに保持します。非常に大きなドキュメントでは、それが支配的なコストです。
  • 解決。
    1. ストリーミングの書き込みパスを優先します。ストリーミングとメモリ に記述された、文書化されたストリーミング書き込みパスを使用してください。それは各ページを構成されるそばからシリアライズしてバッファを解放するため、ページバッファ/出力の増加を抑えます。小さなオブジェクトごとのメタデータ(オフセット、 ページツリー)は、依然としてページ/オブジェクト数とともにスケールしうります。 内部クラスをコピーするのではなく、文書化されたエントリーポイントに従ってください ――背後のストリーミングエンジンは experimental 階層で、そのシンボルは安定した公開面ではありません。
    2. ネイティブの writeHtml() パーサーについては、入力側のメモリがネスト深さと要素数の両方のガードで境界付けられていることを覚えておいてください。ADR-001 はネストを MAX_NESTING_DEPTH = 100 で上限を設け、MAX_ELEMENT_COUNT = 50000 を超えるドキュメントを拒否します。要素の上限に達したドキュメントは、静かにメモリを使い果たすのではなく、明示的にそう告げられます。これらの ADR-001 の上限はネイティブパーサーのみを統制します。オプションの Chrome ブリッジ (writeHtmlChrome())はプロセス外でレンダリングし、これらの上限ではない独自の別個のメモリ/入力制限を持ちます。
  • 関連。 ストリーミングとメモリ

エントリー: 長命のワーカーが多くのジョブの後にメモリを使い果たす

「エントリー: 長命のワーカーが多くのジョブの後にメモリを使い果たす」という見出しのセクション
  • 症状。 単一のレンダリングは成功するが、多くの PDF を連続してレンダリングするキューワーカーが、数分または数時間後にメモリを使い果たす。
  • ありそうな原因。 長命の PHP プロセスは、ジョブをまたいで割り当てを蓄積します。 1 つのリクエストでは見えない緩やかな増加が、数千にわたって積み重なります。
  • 解決。
    1. レジストリを共有し、ドキュメントを再作成します。FontRegistryImageRegistry を起動時に一度構築して DocumentFactory に渡し、ジョブごとに $factory->create($config) で新しい Document を作成します。フォントと画像の解析は、ジョブごとに一度ではなくプロセスごとに一度起こり、ジョブごとのドキュメントツリーはスコープを外れたときに回収されます。 examples/14-worker-factory.php に従ってください。
    2. 共有画像キャッシュを new ImageRegistry(maxCacheBytes: ...) で境界付けて、 ジョブをまたいで際限なく増えないようにします。
    3. ワーカーをリサイクルする ――エンジンの保証ではなく、プロセス制御です。 PHP-FPM では、各子プロセスが固定数のリクエスト後に再生成されるよう pm.max_requests を設定します。Laravel キューでは queue:work --max-jobs / --max-time / --memory を、Symfony Messenger では messenger:consume --limit / --time-limit / --memory-limit を使用します。
  • 関連。 ストリーミングとメモリ

エントリー: コールドまたは不十分にウォームアップされたプロセスでのスループットの崖

「エントリー: コールドまたは不十分にウォームアップされたプロセスでのスループットの崖」という見出しのセクション
  • 症状。 新しいプロセスでの最初のレンダリングが遅い、またはすべてのリクエストが、 ウォームなリクエストが払うべきでない解析コストを払う。
  • ありそうな原因。 2 つのコールドスタートコストが積み重なります。opcache のない PHP はリクエストごとにすべてのファイルを再コンパイルし、ウォームアップされていない FontRegistry は各フォントフェイスを最初に使われたときに解析します。
  • 解決。
    1. opcache を有効にする(そして役立つ場合は JIT も)。 opcache.enable=1 と寛大な opcache.memory_consumption を設定し、本番では opcache.validate_timestamps=0 を設定して、キャッシュがリクエストごとに再チェックされないようにします。その設定は、すべてのリリースで PHP-FPM を再起動またはリロードする(あるいは別の方法で opcache をリセットする、例えば opcache_reset() / cachetool)デプロイプロセスを必要とします――そうでなければ opcache は古いバイトコードを提供し続け、デプロイ後に古いコードが走ります。これらは標準の PHP ini 設定で、NextPDF のキーではありません。
    2. 起動時にフォントレジストリをウォームアップしてロックする。 FontRegistry インスタンスで、$fontRegistry->warmup($fontFiles) は起動時にフェイスを一度解析し、$fontRegistry->lock() はレジストリを凍結してリクエスト時のコードが共有状態を変更できないようにします。$fontRegistry->isLocked() が状態を報告します。本物の長命のワーカーやアプリケーションサーバー――同じ PHP プロセスを多くのリクエストにわたって生かし続けるキューコンシューマーや RoadRunner/Swoole/Octane ワーカー――では、ウォームアップされロックされたレジストリは解析済みフェイスをオブジェクト状態に保持し、リクエストごとのフォント解析をプロセス起動時の一度きりのコストに変えます。標準の PHP-FPM リクエストモデルの下では、そのウォームなオブジェクト状態はリクエストをまたいで生き残り ません。opcache はコンパイル済みクラスとバイトコードをキャッシュするのであって、ウォームなユーザーランドのオブジェクト状態をキャッシュするのではないため、ウォームアップされた FontRegistryリクエストごとに 再構築されます(子の起動時から各リクエストで再実行される)。子プロセス内でリクエストをまたいでウォームに保たれるのではありません。素の PHP-FPM では、opcache は主にバイトコードの再コンパイルコストを償却します。フォント解析が リクエストごとに 払われ、解消されはしないことを受け入れてください。リクエストをまたぐ償却――各フェイスをプロセスの寿命にわたって一度だけ解析する――は、RoadRunner/Swoole/Octane ワーカーや、 同じ PHP プロセスを多くのリクエストにわたって生かし続けるキューコンシューマーのような、 本物の長命のプロセスでのみ当てはまります。
    3. 同じテンプレートをリクエストごとに再解析しない。 フォントと再利用可能なリソースを、共有レジストリを通じて起動時に一度解決します。リクエストで作成すべきなのは、ジョブごとの Document だけです。
  • 関連。 ストリーミングとメモリ

エントリー: 並行下でサーバーが飽和してレイテンシが急増する

「エントリー: 並行下でサーバーが飽和してレイテンシが急増する」という見出しのセクション
  • 症状。 レンダリングごとのレイテンシは単独では問題ないが、負荷下でマシンがスワップしたり、CPU が飽和したり、リクエストがキューに溜まってタイムアウトしたりする。
  • ありそうな原因。 利用可能な RAM に対して PHP-FPM ワーカーが多すぎて、ワーカーピークの合計が物理メモリを超えてホストがスワップする。または、ワーカーが少なすぎて、 リクエストが小さなプールの後ろで直列化する。
  • 解決。
    1. pm.max_children をプロファイルされたピークから決める。 標準の式を使用します。

      pm.max_children = (total RAM - OS/other overhead) / per-worker peak memory

      代表的なドキュメントでワーカーの実際のピークを計測し(範囲のプロファイリング注記を参照)、OS とあらゆる同居サービスのためのヘッドルームを確保して、除算します。マージンを残してください。RAM の 100% にサイズを合わせないでください。

    2. 予算で圧縮コストを見積もります。Flate 圧縮はストリームを書き込む際の大きな CPU コストになりえ、圧縮可能なストリームバイトの量とともにスケールするため、ページ数と埋め込みフォントの量がレンダリングごとの CPU に影響します。画像処理、フォントサブセット化、入力解析も支配的になりえます。代表的なドキュメントで計測し、ワーカー数と CPU を選ぶときに本当のドライバーを考慮してください。

    3. pm.max_requestspm.max_children と並べて設定し、上のワーカーエントリーのように子プロセスがリサイクルしてあらゆる緩やかな増加を回収するようにします。

  • 関連。 ストリーミングとメモリ

エントリー: 大きな信頼できない入力が解析に遅い、または高コスト

「エントリー: 大きな信頼できない入力が解析に遅い、または高コスト」という見出しのセクション
  • 症状。 大きな、または深くネストされた入力――特に HTML やあなたが生成しなかったフォント――で、レンダリングが遅い、またはメモリを多く使う。
  • ありそうな原因。 解析コストは入力のサイズと構造とともにスケールします。病的な入力(深いネスト、巨大な要素数、または不正なフォント)が予算を支配する可能性があります。
  • 解決。
    1. エンジンの境界に頼ります。ネイティブの writeHtml() HTML パーサーは MAX_NESTING_DEPTH = 100MAX_ELEMENT_COUNT = 50000(ADR-001)を強制します。 それらの上限を超える入力は、プロセスを使い果たすことを許される代わりに拒否されます。(オプションの Chrome ブリッジ、writeHtmlChrome() は、これらの ADR-001 の上限の範囲外で、独自の別個のメモリ/入力制限を強制します。)
    2. 呼び出し元が提供するフォントを信頼できないものとして扱います。不正なフォントは、 出力を破損させる代わりに NextPDF\Exception\FontParsingException を送出するため、 特定の例外をキャッチして、再試行する代わりに入力を拒否してください。
    3. 入力を境界で検証してサイズを決め、呼び出し元が影響するコンテンツにはドキュメントサイズにリクエストレベルの制限を適用してください。
  • 関連。 トラブルシューティング: フォントとタグ付け
症状もっともありそうなレバー
単一レンダリングでの Allowed memory size … exhausted$config->withImageCacheBytes() を下げる。埋め込み前に画像を縮小する。ワーカーごとの memory_limit を上げる
ピークメモリがページ数とともに上昇する文書化されたストリーミング書き込みパス を使う
ワーカーメモリが多くのジョブにわたって増えるDocumentFactory を介して FontRegistry/ImageRegistry を共有する。pm.max_requests / --max-jobs を設定する
最初のリクエストが遅い、リクエストごとの解析コストopcache を有効にする。起動時に $fontRegistry->warmup() の後 ->lock()
負荷下でホストがスワップ / レイテンシ急増pm.max_children = (RAM − overhead) / per-worker peak をサイズ決め
大きな/信頼できない入力で遅い、または重いADR-001 の上限に頼る。FontParsingException で不正なフォントを拒否する
  • imageCacheBytesメモリの上限であり、サイズのつまみではありません。 それを下げるとキャッシュが上限で抑えられてビルドが速やかに失敗します。埋め込む画像を再サンプリングや再エンコードすることは決してありません。Core には画質の制御はありません。
  • withCompress(false) はファイルを 大きく し、デバッグ/プロファイリングの補助です。サイズ最適化ではありません。メモリを減らすのではなく、CPU/メモリのトレードオフをシフトします(圧縮ステップをスキップする)。
  • ストリーミングエンジンの正確なメモリプロファイルは experimental 階層のプロパティであり、マイナーリリース間でシフトする可能性があります。あらゆる単一の計測を、移植可能な定数ではなく観測として扱ってください。
  • memory_limitopcache.*pm.max_childrenpm.max_requests は標準の PHP / PHP-FPM 設定です。NextPDF はそれらのための独自のキーを公開しません。Config ではなく、ランタイムで構成してください。

用語集: ストリーミングライター · フォントサブセット化