Truyền luồng và bộ nhớ: hướng dẫn lập hồ sơ và chạy worker xử lý theo lô
Tổng quan nhanh
Phần tiêu đề “Tổng quan nhanh”NextPDF kết xuất chỉ trong một lượt và không bao giờ giữ một Document Object Model (DOM) ở cấp tài liệu; vì vậy bộ nhớ phía đầu vào bị giới hạn bởi độ sâu lồng nhau, chứ không phải số lượng phần tử. Trang này giải thích mô hình truyền luồng, các ràng buộc của Architecture Decision Record (ADR)-001, và cách chạy engine an toàn trong một worker hàng đợi chạy lâu dài.
Cài đặt
Phần tiêu đề “Cài đặt”composer require nextpdf/core:^3Tổng quan khái niệm
Phần tiêu đề “Tổng quan khái niệm”NextPDF có hai đường ghi với các hồ sơ bộ nhớ khác nhau.
Trình ghi trong bộ nhớ mặc định soạn toàn bộ tài liệu rồi mới tuần tự hóa. Bộ nhớ đỉnh tăng theo tổng kích thước đầu ra. Cách này hoạt động tốt với các tài liệu thông thường, nhưng có thể tốn kém khi tài liệu rất lớn.
Trình ghi truyền luồng tuần tự hóa từng trang ngay khi soạn xong, rồi xả trang đó trước khi bắt đầu trang tiếp theo. Engine đã phát hành — StreamingPdfWriter, StreamingCursor, DevNullWriter, và enum WriterState trong src/Writer/Streaming/ — là triển khai thực, hoàn chỉnh, đã được kiểm thử và đã phát hành kể từ 3.1.0. Nó được công khai qua các contract experimental-tier StreamingWriterInterface và CursorInterface. Các lớp của engine là nội bộ, vì vậy hãy phụ thuộc vào các contract và để Core cung cấp phần triển khai. (Một chú thích .ai/contracts-map.md trước đây đã mô tả sai truyền luồng là “chỉ có contract / không có triển khai”; lỗi trong chú thích cũ đó được theo dõi trong issue #610 và đã được sửa trong tài liệu contract B1 — engine đã phát hành kể từ 3.1.0.)
Engine truyền luồng được thiết kế sao cho bộ nhớ thường trú không tăng theo số lượng trang. Mỗi bộ đệm trang sau khi hoàn tất được chuyển cho trình ghi rồi giải phóng. Bảng tham chiếu chéo và các tham chiếu cây trang /Kids được ghi vào các luồng tạm php://temp/maxmemory:0 để tràn ngay xuống đĩa thay vì tích lũy trong heap PHP. Kết quả tuần tự hóa là một cây trang chuẩn, trong đó mục Count là số nút lá (đối tượng trang) hậu duệ của một nút (ISO 32000-2 §7.7.3.3) và mục Kids là một mảng các tham chiếu gián tiếp đến các nút con trực tiếp của nút đó (ISO 32000-2 §7.7.3.2). Hồ sơ bộ nhớ chính xác là một thuộc tính experimental-tier và có thể thay đổi giữa các bản phát hành minor, vì vậy đừng cố định một giả định từ một lần đo duy nhất.
ADR-001 chi phối mô hình bộ nhớ của pipeline kết xuất HTML. Bộ tách token tạo danh sách token trong một lượt. Bộ phân tích cú pháp tiêu thụ danh sách đó từ trái sang phải và phát ra các toán tử luồng nội dung vào một bộ đệm chuỗi. Không có cây phần tử bền vững nào được dựng: bộ phân tích cú pháp giữ tối đa một HtmlStyleState cho mỗi mức lồng nhau, bị giới hạn bởi MAX_NESTING_DEPTH = 100, đồng thời thực thi một giới hạn cứng MAX_ELEMENT_COUNT = 50_000. Hai thao tác cần nhìn trước — định kích thước cột bảng và họ bộ chọn :has() / :last-child — dùng các mảng chỉ mục quét trước có giới hạn trên danh sách token phẳng, chứ không phải một DOM được giữ lại. Phép đo chuẩn Phase 0 (docs/architecture/adr-001-memory-benchmark.md, thực thi 2026-04-06, PHP 8.5.3, memory_limit=1G) đã đo một tài liệu 50,000 phần tử ở mức đỉnh 50 MB cho đường truyền luồng so với một mô phỏng giữ lại một phần công việc ở mức 4 MB. Báo cáo quy khoảng 50 MB trong số đó cho luồng nội dung tích lũy vốn là bất biến về kiến trúc, đồng thời tách riêng một lợi thế phía đầu vào 4–5x cho mô hình truyền luồng trên đối tượng kiểm thử đó. Những con số đó được quan sát trên một dàn máy và đối tượng kiểm thử cụ thể, không phải là bảo đảm.
Lập hồ sơ bộ nhớ trước khi bạn tinh chỉnh
Phần tiêu đề “Lập hồ sơ bộ nhớ trước khi bạn tinh chỉnh”Hãy đo trước khi bạn thay đổi bất cứ điều gì. Pipeline HTML được kiểm soát bởi tools/perf-benchmark.php (chạy qua composer ai:perf-check), công cụ này báo cáo peak_memory_delta_bytes — mức tăng đỉnh theo từng mục tiêu được dùng làm trục hồi quy, chứ không phải đỉnh tuyệt đối của tiến trình. Baseline Cycle 36 (docs/architecture/PERFORMANCE-BUDGETS.md §6.3, ghi lại 2026-05-17 trên một i9-13900K, 64 GB, PHP 8.5.3, opcache tắt) đã ghi nhận mức tăng đỉnh 0 byte ở 12 trong 16 cặp target/mode. Bốn mức tăng khác 0 được quy cho các cấp phát bộ nhớ đệm phông chữ khi chạm lần đầu và bộ đệm dấu vết, vốn giữ không đổi ở các lần kết xuất sau. Hãy đọc chúng như các giá trị quan sát được cho dàn máy đó, chứ không phải như các hằng số có thể mang sang môi trường khác. Để lập hồ sơ tạm thời cho tài liệu của riêng bạn, hãy lấy mẫu memory_get_peak_usage(true) trước và sau khi kết xuất, rồi đặt lại mức đỉnh bằng memory_reset_peak_usage() giữa các lần lặp, theo cùng cách phép đo chuẩn tách riêng chi phí cho từng mục tiêu.
Chạy NextPDF trong một worker xử lý theo lô
Phần tiêu đề “Chạy NextPDF trong một worker xử lý theo lô”Một worker hàng đợi là một tiến trình PHP sống lâu: nó khởi động framework một lần, giữ thường trú, và xử lý các job trong một vòng lặp. Điều đó giúp nó nhanh, đồng thời cũng là lý do vệ sinh bộ nhớ trở nên quan trọng. Một rò rỉ chậm vốn vô hình trong một request duy nhất có thể tích lũy qua hàng nghìn job. PERFORMANCE-BUDGETS §1 nêu rõ kiểu lỗi này: một worker kết xuất nhiều PDF liên tiếp có thể cạn kiệt bộ nhớ sau nhiều giờ ngay cả khi từng lần kết xuất riêng lẻ vẫn trông ổn.
NextPDF hỗ trợ các môi trường worker. DocumentFactory cho phép một worker tạo một tài liệu mới cho mỗi job trong khi chia sẻ một FontRegistry và ImageRegistry tồn tại suốt vòng đời tiến trình, vì vậy việc phân tích cú pháp phông chữ và hình ảnh chỉ diễn ra một lần thay vì một lần cho mỗi job. ADR-001 ghi lại rằng bộ phân tích cú pháp HTML được dựng cho từng request mà không có trạng thái tĩnh có thể thay đổi, và rằng các đối tượng ngữ cảnh định dạng trong tương lai phải tuân theo cùng phạm vi từng request. Các bước sau đây giúp cấu hình một worker một cách an toàn.
Bước 1 — Chia sẻ các registry giữa các job
Phần tiêu đề “Bước 1 — Chia sẻ các registry giữa các job”Hãy tạo các registry một lần khi khởi động tiến trình và tái sử dụng chúng cho mọi job, theo examples/14-worker-factory.php:
<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Core\DocumentFactory;use NextPDF\Core\PdfFactory;use NextPDF\Graphics\ImageRegistry;use NextPDF\Typography\FontRegistry;
// Created once at process boot — not per job.$fontRegistry = new FontRegistry();$imageRegistry = new ImageRegistry(maxCacheBytes: 50 * 1024 * 1024);$documentFactory = new DocumentFactory($fontRegistry, $imageRegistry);
$factory = PdfFactory::new() ->withCompress(true) ->withDocumentFactory($documentFactory);
// Per job: a fresh document, shared registries.$doc = $factory->create();$doc->addPage();$doc->setFont('helvetica', '', 11);$doc->cell(0, 8, 'Rendered inside a worker.', newLine: true);$doc->save('/path/to/output.pdf');Thuộc tính maxCacheBytes của image registry giới hạn bộ nhớ đệm dùng chung, nên bộ nhớ đệm đó không thể tăng vô hạn qua các job.
Bước 2 — Giới hạn vòng đời của worker
Phần tiêu đề “Bước 2 — Giới hạn vòng đời của worker”Đây là thực hành kiểm soát tiến trình chung cho bất kỳ worker PHP nào, không phải bảo đảm của engine NextPDF: hãy khởi động lại các worker định kỳ để một tiến trình sống lâu không thể tích lũy bộ nhớ hoặc tiếp tục chạy mã cũ vô thời hạn. Cả hai hệ thống hàng đợi PHP lớn đều cung cấp các giới hạn tích hợp sẵn và cơ chế khởi động lại nhẹ nhàng.
Đối với hàng đợi Laravel (https://laravel.com/docs/12.x/queues), lệnh queue:work chạy worker như một tiến trình sống lâu. Các tùy chọn được ghi trong tài liệu là --memory (mặc định 128 MB; worker thoát khi bộ nhớ của nó vượt quá giới hạn), --max-jobs (thoát sau một số job), và --max-time (thoát sau một số giây). Lệnh queue:restart báo hiệu cho các worker thoát một cách nhẹ nhàng sau job hiện tại, nên một lần triển khai hoặc một bộ hẹn giờ định kỳ có thể tái chế chúng mà không làm gián đoạn một lần kết xuất đang chạy. Laravel Horizon (https://laravel.com/docs/12.x/horizon) giám sát các worker Redis với một chiến lược cân bằng auto và một php artisan horizon:terminate nhẹ nhàng; lệnh này hoàn tất các job đang chạy trước khi bộ giám sát tiến trình khởi động lại supervisor.
Đối với Symfony Messenger (https://symfony.com/doc/current/messenger.html), lệnh messenger:consume chạy vô thời hạn theo mặc định. Các tùy chọn giới hạn được ghi trong tài liệu là --limit (xử lý N thông điệp, rồi thoát), --memory-limit (ví dụ 128M; thoát khi bộ nhớ chạm tới giới hạn), và --time-limit (ví dụ 3600; thoát sau khoảng thời gian đó). Tài liệu của Symfony khuyến nghị chạy worker dưới Supervisor hoặc systemd để một tiến trình đã thoát tự động khởi động lại, và messenger:stop-workers đặt một cờ bộ nhớ đệm để yêu cầu mỗi worker hoàn tất thông điệp hiện tại rồi thoát một cách sạch sẽ.
Bước 3 — Khởi động lại khi triển khai
Phần tiêu đề “Bước 3 — Khởi động lại khi triển khai”Mỗi lần triển khai, hãy báo hiệu một lần khởi động lại nhẹ nhàng để các worker tiếp nhận mã mới: php artisan queue:restart (hoặc php artisan horizon:terminate) cho Laravel, php bin/console messenger:stop-workers cho Symfony. Trình quản lý tiến trình — Supervisor, systemd, hoặc supervisor của Horizon/Octane — sau đó khởi động một tiến trình mới trên cơ sở mã mới. Đây là thực hành triển khai chung cho các worker PHP sống lâu và độc lập với NextPDF.
Hiệu năng
Phần tiêu đề “Hiệu năng”Đường truyền luồng được thiết kế để giới hạn bộ nhớ đỉnh bằng cách xả từng trang đã hoàn tất và tràn phần ghi sổ tham chiếu chéo cùng cây trang xuống các luồng tạm có nền đĩa. Vì vậy, tập thường trú theo thiết kế không tăng theo số lượng trang. Hành vi đó được quan sát trong engine 3.1.0 đã phát hành và được ghim bởi các bài kiểm thử tái lập golden-baseline của nó, nhưng nó được nêu như hành vi thiết kế chứ không phải một con số cố định, vì hồ sơ là một thuộc tính experimental-tier. Bộ nhớ phía đầu vào của pipeline HTML bị giới hạn bởi MAX_NESTING_DEPTH = 100 chứ không phải số lượng phần tử (ADR-001). Tất cả các con số cụ thể trên trang này đều gắn với một tạo phẩm có ngày tháng — phép đo chuẩn ADR-001 ngày 2026-04-06 và baseline Cycle 36 của PERFORMANCE-BUDGETS ngày 2026-05-17 — và được quan sát trên các dàn máy mà những tài liệu đó nêu tên; hãy coi chúng là các quan sát, không phải các bảo đảm có thể mang sang môi trường khác. Giá trị performance_budget là 1500 ms / 64 MB là phạm vi của canvas, không phải một mức trần theo hợp đồng.
Ghi chú bảo mật
Phần tiêu đề “Ghi chú bảo mật”Hàm writeContent() của con trỏ truyền luồng nối thêm các byte vào luồng nội dung trang theo nguyên văn. Nó không kiểm tra cú pháp toán tử. Trong một worker kết xuất nội dung chịu ảnh hưởng từ bên gọi, đừng bao giờ truyền đầu vào không tin cậy vào writeContent(); hãy dùng writeText(), hàm mà con trỏ đã phát hành dùng để thoát ký tự theo văn phạm chuỗi ký tự PDF. Bên gọi sở hữu luồng đầu ra: engine ghi vào đó nhưng không bao giờ đóng hoặc mở lại nó, nên nó không thể chuyển hướng đầu ra. Một worker phải tự đóng handle sau khi close() của trình ghi trả về, nếu không nó sẽ rò rỉ một file descriptor qua các job. Chia sẻ các registry giữa các job là một tối ưu hóa hiệu năng, không phải một ranh giới tin cậy: một ImageRegistry dùng chung lưu đệm các hình ảnh đã phân tích, vì vậy hãy định kích thước maxCacheBytes của nó một cách có chủ đích và đừng giả định bộ nhớ đệm được cô lập giữa các tenant trong một worker đa tenant.
Tuân thủ
Phần tiêu đề “Tuân thủ”| Tuyên bố | Tiêu chuẩn | Điều khoản | Bằng chứng |
|---|---|---|---|
Trình ghi truyền luồng phát ra một cây trang có mục Kids là một mảng các tham chiếu gián tiếp đến các nút con trực tiếp của nút. | ISO 32000-2 | §7.7.3.2 | |
Trình ghi truyền luồng phát ra một mục Count bằng số đối tượng trang lá là hậu duệ của nút cây trang. | ISO 32000-2 | §7.7.3.3 |
Các điều khoản được diễn giải lại và ghim theo bảng thuật ngữ; không tái tạo văn bản quy phạm nào.
Xem thêm
Phần tiêu đề “Xem thêm”- Contracts / Streaming —
experimentalStreamingWriterInterfacevàCursorInterface, cùng máy trạng thái của chúng. - HTML / Streaming constraints (ADR-001) — quyết định một lượt, không giữ DOM, và các ngưỡng cần xem xét lại.
- Performance — cổng hồi quy độ trễ và bộ nhớ của pipeline HTML.
- Layout — các engine phụ trang không giữ trạng thái theo từng trang.
- PERFORMANCE-BUDGETS — kiểu lỗi worker rò rỉ và baseline của cổng hồi quy.