コンテンツにスキップ

大量ドキュメント生成

Spec: ISO 24495-1:2023, §5 Spec: ISO 9241-112:2025, §6.1.2.3 Evidence: Benchmark-backed

PDF を 1 つ生成するのは関数呼び出しです。スケジュールに沿って 10 万件を生成するのはシステムの問題です。メモリは上限内に保たれなければならず、処理は並列でなければならず、数値には意味がなければなりません。このページでは、バッチ生成シナリオを、スループットに関する問いから破綻しないデプロイまで順を追って解説します。率直な答えは、目を引く数値ではなく「自分のドキュメントで計測すること」だと、はっきりと述べます。

バッチ生成は、特徴的な 2 つの形で失敗します。1 つ目はメモリの漸増です。長時間稼働するワーカーは、ドキュメントごとに保持状態を蓄積していき、バッチの途中で強制終了されます。その結果、実行は完了もせず、明確に失敗もしません。2 つ目は、もっともらしいが無意味な数値です。簡素なドキュメントから得たベンチマークを、複雑なドキュメントをレンダリングするフリートのサイジングに用いてしまい、本番負荷にさらされて初めてそれが誤りだと判明します。

どちらも回避できますが、それは最初のインシデントが起きてから付け足すのではなく、メモリ構成と計測手法を最初から設計に組み込んだ場合に限られます。

  • 作業単位は、共有されるドキュメントではなく、使い捨てのドキュメントです。 プロセス寿命のデータ(フォント、画像キャッシュ)は共有レジストリに保持し、ドキュメントはレンダリングごとに作成して破棄します。
  • メモリには 2 つの側面があり、長時間稼働するワーカーにとって重要なのは一方だけです。 レンダリング中の一時的なピークは想定内です。一方、戻ってこない保持メモリこそが、バッチを停止に追い込むリークです。
  • スループットとは、並列性に、レンダリングごとの上限付きコストを加えたものです。 破綻しない構成は、ステートレスなワーカー群にキューが供給し、各ワーカーがレンダリングして解放するという形です。
  • 手法を伴わない数値は、数値とは言えません。 NextPDF は、レンダリングごとの計測値を、あなたが収集するデータとして報告し、条件を明示しない速度の主張を拒みます。最も重要な数値は、自分自身のテンプレートで計測した数値です(ISO 24495-1 §5.x11 — 重要なメッセージは、読み手が見つけられる場所に置く)。

このアーキテクチャは、たった 1 つの決定を軸に構築されています。プロセスの寿命に属する状態は共有かつイミュータブルであり、レンダリングの寿命に属する状態は新規に作られて破棄される、というものです。 フォントは一度パースしてからロックされる構造データなので、どのレンダリングもそれを変更して次のレンダリングを汚染することはできません。画像キャッシュは、決してロックされない上限付きの LRU(最も最近使われていないものから破棄する)ストアであり、メモリはリクエストをまたいでリークすることなく上限内に保たれます。ドキュメントファクトリはステートレスなシングルトンであり、それが作成するすべてのドキュメントは使い捨てです。

この分離こそが、Octane、RoadRunner、Swoole の下でワーカーを何時間も安全に稼働させられる理由です。これは「リクエスト N がリクエスト N+1 を破壊する」という障害モードを、ドキュメントが自らリセットされることに期待するのではなく、構造的に取り除きます。

このシナリオには 4 つの段階があります。

  1. Warm the shared state once On worker boot, parse and lock the font registry and size the image cache. This cost is paid once, not per document.
  2. Enqueue the work A queue holds the render jobs. The queue is the throughput dial — workers scale horizontally behind it.
  3. Render on a disposable document Each worker creates a fresh document from the factory, renders, emits the bytes, and lets the document go.
  4. Measure, then size Collect per-render time and peak memory. Size the fleet from measurements on your own templates, not a generic figure.
大量処理シナリオを端から端まで解説します。共有のイミュータブルな状態は一度だけウォームアップされ、各ジョブは使い捨てのドキュメント上でレンダリングして解放し、スループットは 1 つを拡大するのではなくワーカーを追加することでスケールします。

フレームワークブリッジは、この構成を自分で組み立てる対象ではなく、デフォルトとして提供します。Laravel のサービスプロバイダは、フォントレジストリをウォームアップ済みでロックされたシングルトンとして登録し、ドキュメントを解決のたびに新規インスタンスとしてバインドします。上限付きのリトライ回数、タイムアウト、指数バックオフを備えたキューイング型ジョブを同梱しています。このジョブは出力パスをワーカー側で検証します。シリアライズされたキューのペイロードは、転送中に改ざんされる可能性があるためです。Symfony および CodeIgniter の統合も、同じ使い捨てドキュメント・共有レジストリの規律に従います。

メモリモデルはコード裏付けです。 Evidence: Code-backed Laravel のNextPdfServiceProviderは、FontRegistryをウォームアップ後にlock()されるシングルトンとして、ImageRegistryを意図的にロックしない上限付き LRU シングルトンとして、そしてDocumentをステートレスなファクトリ経由で解決ごとのバインディングとして登録します。使い捨てドキュメントモデルは、説明文ではなく配線(ワイヤリング)に組み込まれています。GeneratePdfJobtriestimeoutbackoffを備え、handle()内で出力パスを再検証します。

計測の側面はベンチマーク裏付けです。 Evidence: Benchmark-backed エンジンは、生成ごとにイミュータブルな RenderReportを発行します。これにはミリ秒単位のレンダリング時間、ピークメモリ(バイト単位)、ページ数、警告数、フォールバックの発生回数が含まれます。これらはフリートをサイジングするために必要となる、まさにその入力データです。さらに、メモリ断片化アナライザーが、ピーク(一時的)メモリと保持メモリを区別します。この区別によって、長時間稼働するワーカーが健全なのか、それとも徐々にリークしているのかが分かります。ベンチマークハーネス自体は、ウォームアップを伴う反復実行を行うよう設定されています。1 回だけの計測はノイズだからです。

この規律は設計原則です。 Evidence: Design principle NextPDF は、パフォーマンスをその手法とともに報告し、条件を明示しない速度の主張を拒みます。これは、このドキュメントの書き方とも一貫しています。— Spec: ISO 24495-1:2023, §5 重要なメッセージを、 読み手が見つけられる場所に置いています。ここで重要なメッセージは「自分自身のワークロードを計測すること」です。

以下のコードは、計測を伴う使い捨てドキュメントのループであり、このモデルを示しています。エンジンはRenderReportを生成します。キューは利用者側のインフラです。

<?php
declare(strict_types=1);
use NextPDF\Contracts\DocumentFactoryInterface;
use NextPDF\Observability\RenderReport;
use Psr\Log\LoggerInterface;
/**
* One batch worker iteration: render, emit, release, measure.
*
* The factory and its registries are process-lifetime singletons; the
* document is disposable. Retained memory must return to baseline between
* iterations or the worker is leaking.
*
* @param iterable<int, callable(\NextPDF\Core\Document): \NextPDF\Core\Document> $jobs
*/
function runBatch(
DocumentFactoryInterface $factory,
LoggerInterface $logger,
iterable $jobs,
): void {
foreach ($jobs as $jobId => $build) {
$startedAt = hrtime(true);
// Fresh, disposable document — shares the warmed registries.
$doc = $factory->create();
$doc = $build($doc);
$bytes = $doc->getPdfData();
// Hand the bytes off to your sink (object store, response, etc.).
unset($doc, $bytes); // let the per-render state go
$elapsedMs = (hrtime(true) - $startedAt) / 1_000_000;
$logger->info('pdf.render.complete', [
'job_id' => $jobId,
'render_time_ms' => round($elapsedMs, 2),
'peak_memory_mb' => round(memory_get_peak_usage(true) / 1_048_576, 2),
]);
}
}

unset()は見せかけではありません。レンダリングごとの状態は、保持メモリがベースラインに戻るよう、各反復で解放されることを意図しています。反復をまたいでベースラインが上昇していくワーカーこそ、このループが回避するために設計された障害です。

最大の誤解は、まるで唯一の答えがあるかのように問われる*「NextPDF は毎秒何件の PDF を処理できるのか?」*です。唯一の答えはありません。むしろ、何らかの単一の数値を引き合いに出すことが、フリートのサイジングを誤らせる原因です。レンダリングコストを支配するのはドキュメントです。したがって、行動の根拠とする価値がある唯一の数値は、エンジン自身のレンダリングごとのレポートを用いて、自分自身のテンプレートで計測した数値です。前提としてドキュメント、ハードウェア、手法を伴わない数値は、データではなく飾りです。

2 つ目の誤解は、ピークメモリこそ注視すべきものだという考えです。ピークは一時的で想定内です。元に戻ります。バッチを停止に追い込む数値は、戻ってこない保持メモリです。エンジンがこの 2 つを分離しているのは、まさにそのためです。

  • 普遍的なスループット数値は存在せず、このページは意図的にそれを一切示しません。 レンダリングコストはあなたのドキュメントに依存します。レンダリングごとのレポートで計測してください。
  • 上限付きメモリは、使い捨てドキュメントモデルが使われていることに依存します。 1 つのドキュメントを多数のレンダリングにまたがって保持したり、ミュータブルなレンダリングごとの状態を共有したりすると、その保証は失われます。フレームワークブリッジは、安全な構成をデフォルトとします。手作業の配線(ワイヤリング)では、これを再現しなければなりません。
  • 画像キャッシュは上限付きであり、無制限ではありません。 ユニークな画像が多い高負荷のワークロードでは、LRU が追い出しを行います。これは設計であり、退行(リグレッション)ではありません。
  • ワーカープールのサイジング、キューの選定、オートスケーリングは、エンジンの外側にあるデプロイ上の判断です。 NextPDF は、計測値と上限付きのプリミティブを提供します。あなたのキューそのものを実行するわけではありません。
  • RenderReportは、判定ではなくデータです。 これは、あるレンダリングで何が起きたかを教えてくれます。それをキャパシティプランに変えるのは、あなたの分析です。
  • このページは、計測の側面についてはベンチマーク裏付けであり、メモリモデルについてはコード裏付けです。特定のレートを主張することはありません。
Queued high-volume generation primitives — edition availability
Edition Availability
Core

使い捨てドキュメントモデル、共有のイミュータブルなレジストリ、 レンダリングごとのRenderReport、そしてメモリ断片化アナライザーは Core です。単純な大量 PDF 生成に、商用ティアは必要ありません。

Pro

同じプリミティブです。商用機能(署名、PDF/A)は、計測すべきであって仮定すべきではないレンダリングごとのコストを追加します。

Enterprise

同じプリミティブです。構造化請求書と検証の処理は、ペイロードとルールセットの規模に応じてスケールするレンダリングごとのコストをさらに追加します。

  • メモリとストリーミング — エンジンが大きなドキュメントでメモリをどのように上限内に保ち、どこでストリーミングするか。
  • 誠実なベンチマーク — 手法を伴わないベンチマーク数値にどれほどの価値があるか、そして NextPDF がパフォーマンスをどのように報告するか。
  • 本番環境での NextPDF の運用 — バッチが実際に稼働し始めたら、レンダリングごとのレポートをヘルスシグナルへと変えること。
  • 使い捨てドキュメント — 単一のレンダリングのために作成され、その後に破棄されるドキュメントインスタンス。状態が次のレンダリングへ漏れることはない。
  • 共有レジストリ — プロセス寿命に属し、ウォームアップ後はイミュータブルな状態(フォント、画像キャッシュ)。レンダリングごとの追加コストなしにレンダリングをまたいで再利用される。
  • ピークメモリ — レンダリング中の一時的な最高到達点。想定内であり、ベースラインへ戻る。
  • 保持メモリ — レンダリング完了後もなお保持されているメモリ。レンダリングをまたいで保持ベースラインが上昇するのはリーク。
  • ワーカー — キューからレンダリングジョブを取得する長時間稼働プロセス。バッチを生き延びるには、メモリを上限内に保たなければならない。
  • RenderReport — エンジンのイミュータブルなレンダリングごとのメトリクススナップショット(時間、ピークメモリ、ページ数、警告)。実データに基づいてキャパシティをサイジングするために用いる。