Bỏ qua để đến nội dung

Tạo tài liệu khối lượng lớn

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

Tạo một PDF là một lệnh gọi hàm. Tạo một trăm nghìn PDF theo lịch trình là một bài toán hệ thống: bộ nhớ phải luôn trong giới hạn, công việc phải chạy song song, và các con số phải có ý nghĩa. Trang này trình bày kịch bản tạo tài liệu theo lô, từ câu hỏi về thông lượng đến một cách triển khai đủ vững chắc. Trang cũng nói thẳng rằng câu trả lời trung thực là “hãy tự đo trên tài liệu của bạn”, chứ không phải một con số hào nhoáng.

Việc tạo tài liệu theo lô thường thất bại theo hai cách đặc trưng. Cách thứ nhất là bộ nhớ tăng dần. Một worker chạy dài hạn tích lũy trạng thái được giữ lại theo từng tài liệu cho đến khi bị buộc dừng giữa chừng, và lượt chạy đó không hoàn tất mà cũng không thất bại một cách sạch sẽ. Cách thứ hai là một con số tự tin nhưng vô nghĩa: kết quả benchmark từ một tài liệu đơn giản được dùng để định cỡ một cụm máy chuyên kết xuất các tài liệu phức tạp, và đến khi chịu tải thực tế mới lộ rõ rằng nó sai.

Bạn có thể tránh được cả hai, nhưng chỉ khi thiết kế mô hình bộ nhớ và phương pháp đo lường ngay từ đầu, thay vì bổ sung chúng sau sự cố đầu tiên.

  • Đơn vị công việc là một tài liệu dùng một lần, không phải tài liệu dùng chung. Giữ dữ liệu tồn tại suốt vòng đời tiến trình (phông chữ, bộ đệm ảnh) trong các registry dùng chung; tạo và hủy tài liệu cho mỗi lần kết xuất.
  • Bộ nhớ gồm hai phần, và với một worker chạy dài hạn thì chỉ một phần là quan trọng. Đỉnh tạm thời trong một lần kết xuất là điều bình thường; bộ nhớ được giữ lại mà không được trả về mới là rò rỉ làm kết thúc một lô.
  • Thông lượng đến từ mức độ song song và chi phí được khống chế cho mỗi lần kết xuất. Mô hình đủ vững chắc là một hàng đợi phân phối việc cho các worker không trạng thái; mỗi worker kết xuất rồi giải phóng.
  • Một con số thiếu phương pháp đo thì không phải là một con số. NextPDF báo cáo các phép đo cho mỗi lần kết xuất dưới dạng dữ liệu để bạn thu thập, và từ chối mọi tuyên bố về tốc độ không kèm điều kiện. Con số quan trọng nhất là con số bạn tự đo trên các mẫu của chính mình (ISO 24495-1 §5.x11 — đặt thông điệp quan trọng ở nơi người đọc tìm thấy nó).

Kiến trúc xoay quanh một quyết định duy nhất: trạng thái theo vòng đời tiến trình thì được dùng chung và bất biến; trạng thái theo một lần kết xuất thì luôn mới và bị loại bỏ. Phông chữ là dữ liệu cấu trúc được phân tích một lần rồi khóa lại, nên không lần kết xuất nào có thể thay đổi chúng và làm hỏng lần kế tiếp. Bộ đệm ảnh là một kho lưu trữ theo cơ chế ít dùng gần nhất, có giới hạn và không bao giờ bị khóa, nên bộ nhớ luôn được khống chế mà không rò rỉ giữa các yêu cầu. Document factory là một singleton không trạng thái; mọi tài liệu nó tạo ra đều dùng một lần.

Chính sự tách biệt đó giúp một worker an toàn khi chạy hàng giờ trên Octane, RoadRunner hay Swoole. Nó loại bỏ kiểu lỗi “yêu cầu N làm hỏng yêu cầu N+1” ngay từ thiết kế, thay vì trông chờ từng tài liệu tự đặt lại trạng thái.

Kịch bản này gồm bốn giai đoạn.

  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.
Toàn bộ kịch bản khối lượng lớn: trạng thái dùng chung bất biến được hâm nóng một lần; mỗi tác vụ kết xuất trên một tài liệu dùng một lần rồi giải phóng; thông lượng tăng quy mô bằng cách thêm worker, chứ không phải bằng cách làm một worker lớn hơn.

Các cầu nối framework biến mô hình này thành mặc định thay vì thứ bạn phải tự lắp ráp. Service provider của Laravel đăng ký font registry dưới dạng một singleton đã hâm nóng và khóa lại, đồng thời gắn tài liệu thành một thể hiện mới cho mỗi lần resolve. Nó đi kèm một queued job có số lần thử bị giới hạn, một timeout, và backoff theo cấp số nhân. Job đó xác thực đường dẫn đầu ra ở phía worker, bởi vì một payload hàng đợi đã được serialize có thể bị can thiệp khi lưu chuyển. Các tích hợp Symfony và CodeIgniter tuân theo đúng kỷ luật tài liệu dùng một lần, registry dùng chung.

Mô hình bộ nhớ được chứng minh bằng mã nguồn. Evidence: Code-backed NextPdfServiceProvider của Laravel đăng ký FontRegistry dưới dạng một singleton được hâm nóng rồi lock(), đăng ký ImageRegistry dưới dạng một singleton LRU có giới hạn được cố tình không khóa, và đăng ký Document dưới dạng một liên kết theo từng lần resolve thông qua một factory không trạng thái. Mô hình tài liệu dùng một lần nằm ở cách đấu nối, chứ không phải ở câu chữ. GeneratePdfJob chứa tries, timeout, và backoff, đồng thời xác thực lại đường dẫn đầu ra bên trong handle().

Lớp đo lường được chứng minh bằng benchmark. Evidence: Benchmark-backed Engine phát ra một RenderReport bất biến cho mỗi lần tạo, chứa thời gian kết xuất tính bằng mili giây, bộ nhớ đỉnh tính bằng byte, số trang, số lượng cảnh báo, và số lần xảy ra fallback — chính là các đầu vào chính xác bạn cần để định cỡ một cụm máy. Một bộ phân tích phân mảnh bộ nhớ riêng phân biệt bộ nhớ đỉnh (tạm thời) với bộ nhớ được giữ lại. Sự phân biệt đó cho bạn biết một worker chạy dài hạn đang khỏe mạnh hay đang âm thầm rò rỉ. Bản thân bộ khung benchmark được cấu hình để chạy lặp lại nhiều vòng kèm khởi động làm nóng, vì một lần đo đơn lẻ chỉ là nhiễu.

Kỷ luật này là một nguyên tắc thiết kế: Evidence: Design principle NextPDF báo cáo hiệu năng kèm theo phương pháp đo của nó và từ chối mọi tuyên bố về tốc độ không kèm điều kiện. Điều đó nhất quán với cách bộ tài liệu này được viết — Spec: ISO 24495-1:2023, §5 đặt thông điệp quan trọng ở nơi người đọc sẽ tìm thấy nó. Thông điệp quan trọng ở đây là “hãy đo chính khối lượng công việc của bạn”.

Đoạn mã dưới đây là một vòng lặp dùng tài liệu dùng một lần và có đo lường. Engine tạo ra RenderReport; còn hàng đợi là hạ tầng của bạn.

<?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),
]);
}
}

Lệnh unset() không chỉ để làm đẹp mã. Trạng thái theo từng lần kết xuất cần được giải phóng ở mỗi vòng lặp để bộ nhớ được giữ lại trở về mức nền. Một worker có mức nền cứ tăng dần qua các vòng lặp chính là lỗi mà vòng lặp này được thiết kế để tránh.

Hiểu lầm nổi cộm nhất là “NextPDF tạo được bao nhiêu PDF mỗi giây?” như thể nó có một câu trả lời duy nhất. Không có câu trả lời như vậy, và việc đưa ra một con số như thế chính là cách khiến các cụm máy bị định cỡ sai. Chi phí kết xuất bị chi phối bởi tài liệu, nên con số duy nhất đáng để hành động dựa vào là con số đo được trên chính các mẫu của bạn bằng báo cáo theo từng lần kết xuất của engine. Một con số thiếu bối cảnh tài liệu, phần cứng và phương pháp phía sau nó chỉ là trang trí, không phải dữ liệu.

Hiểu lầm thứ hai là cho rằng bộ nhớ đỉnh mới là thứ cần theo dõi. Đỉnh chỉ là tạm thời và nằm trong dự kiến — nó sẽ trở về. Con số làm kết thúc một lô là bộ nhớ được giữ lại mà không trở về. Đó chính là lý do engine tách biệt hai loại này.

  • Không có con số thông lượng chung cho mọi trường hợp, và trang này cố tình không nêu ra con số nào. Chi phí kết xuất phụ thuộc vào tài liệu của bạn; hãy đo bằng báo cáo theo từng lần kết xuất.
  • Bộ nhớ trong giới hạn phụ thuộc vào việc mô hình tài liệu dùng một lần có được áp dụng hay không. Việc giữ một tài liệu qua nhiều lần kết xuất, hoặc dùng chung trạng thái có thể thay đổi theo từng lần kết xuất, sẽ làm mất đảm bảo đó. Các cầu nối framework mặc định dùng mô hình an toàn. Cách đấu nối tự viết tay phải tái tạo lại mô hình đó.
  • Bộ đệm ảnh có giới hạn, không phải vô hạn. Dưới khối lượng công việc nặng với nhiều ảnh khác nhau, cơ chế LRU sẽ loại bớt phần tử. Đó là thiết kế, không phải lỗi thoái lui.
  • Định cỡ nhóm worker, chọn hàng đợi, và tự động co giãn là những quyết định triển khai nằm ngoài engine. NextPDF cung cấp các phép đo và thành phần cơ bản có giới hạn. Nó không vận hành hàng đợi của bạn.
  • RenderReport là dữ liệu, không phải một phán quyết. Nó cho bạn biết điều gì đã diễn ra trong một lần kết xuất. Việc biến nó thành một kế hoạch năng lực là phần phân tích của bạn.
  • Trang này được chứng minh bằng benchmark cho lớp đo lường và được chứng minh bằng mã nguồn cho mô hình bộ nhớ. Trang này không khẳng định một tốc độ cụ thể nào.
Queued high-volume generation primitives — edition availability
Edition Availability
Core

Mô hình tài liệu dùng một lần, các registry dùng chung bất biến, RenderReport theo từng lần kết xuất, và bộ phân tích phân mảnh bộ nhớ đều thuộc bản Core. Việc tạo PDF khối lượng lớn thông thường không cần bậc thương mại nào.

Pro

Vẫn là những thành phần cơ bản đó; các tính năng thương mại (ký, PDF/A) làm tăng chi phí cho mỗi lần kết xuất mà bạn nên đo, chứ không nên giả định.

Enterprise

Vẫn là những thành phần cơ bản đó; công việc hóa đơn có cấu trúc và kiểm định làm tăng thêm chi phí cho mỗi lần kết xuất, tăng theo kích thước payload và bộ quy tắc.

  • Tài liệu dùng một lần — một thể hiện tài liệu được tạo cho một lần kết xuất duy nhất rồi bị loại bỏ sau đó, nên không có trạng thái nào rò rỉ sang lần kết xuất kế tiếp.
  • Registry dùng chung — trạng thái tồn tại suốt vòng đời tiến trình, bất biến sau khi hâm nóng (phông chữ, bộ đệm ảnh), được tái sử dụng qua các lần kết xuất mà không phát sinh lại chi phí cho từng lần kết xuất.
  • Bộ nhớ đỉnh — mức cao nhất tạm thời trong một lần kết xuất; nằm trong dự kiến và trở về mức nền.
  • Bộ nhớ được giữ lại — phần bộ nhớ vẫn còn được giữ sau khi một lần kết xuất hoàn tất; một mức nền giữ lại cứ tăng lên qua các lần kết xuất là dấu hiệu rò rỉ.
  • Worker — một tiến trình chạy dài hạn lấy các tác vụ kết xuất từ một hàng đợi; phải giữ bộ nhớ trong giới hạn để trụ vững qua một lô.
  • RenderReport — bản chụp số liệu bất biến của engine cho mỗi lần kết xuất (thời gian, bộ nhớ đỉnh, số trang, cảnh báo), dùng để định cỡ năng lực dựa trên dữ liệu thực.