ข้ามไปยังเนื้อหา

การเรนเดอร์ PDF อย่างปลอดภัยในเวิร์กเกอร์ที่ทำงานระยะยาว

เวิร์กเกอร์ PHP (PHP: Hypertext Preprocessor) ที่ทำงานระยะยาว (RoadRunner, Swoole, Laravel Octane) คงโปรเซสเดิมให้ทำงานต่อเนื่องระหว่างคำขอจำนวนมาก หากต้องแยกวิเคราะห์ฟอนต์ชุดเดิมและถอดรหัสรูปภาพชุดเดิมในทุกคำขอ จะสิ้นเปลืองเวลาของหน่วยประมวลผลและเพิ่มหน่วยความจำที่ใช้งานอยู่ NextPDF หลีกเลี่ยงต้นทุนดังกล่าวด้วยการแยกอายุการใช้งานสองแบบออกจากกัน

  • อายุการใช้งานระดับโปรเซสสำหรับใช้ร่วมกัน: FontRegistry และ ImageRegistry เก็บตารางฟอนต์ที่แยกวิเคราะห์แล้วและแคชรูปภาพที่ถอดรหัสแล้ว สร้าง registry เหล่านี้เพียงครั้งเดียวเมื่อเวิร์กเกอร์เริ่มทำงาน
  • อายุการใช้งานระดับคำขอที่ใช้แล้วทิ้ง: Document ที่ส่งคืนโดย DocumentFactory::create() ให้สร้าง เขียนเอาต์พุต แล้วปล่อยให้พ้นขอบเขต จากนั้นตัวเก็บขยะของ PHP จะสามารถคืนกราฟอ็อบเจ็กต์ทั้งหมดได้

สูตรนี้แสดงลำดับการบูตเวิร์กเกอร์ งานต่อคำขอ และการรีเซ็ตตามรอบที่ช่วยรักษาระดับการใช้หน่วยความจำสูงสุดให้คงที่

Terminal window
composer require nextpdf/core:^3

รูปแบบเวิร์กเกอร์นี้ไม่ต้องใช้ส่วนขยายเพิ่มเติม และรันไทม์ของเวิร์กเกอร์ (RoadRunner / Swoole / Octane) เป็นทางเลือก คุณสามารถรันรูปแบบ factory เดียวกันนี้ในลูป for ของ command-line interface (CLI) ตามที่ฮาร์เนสใช้ในการทดสอบ

ในโค้ดของเวิร์กเกอร์ ให้เริ่มด้วย DocumentFactory ที่สร้างขึ้นเพียงครั้งเดียวด้วย FontRegistry และ ImageRegistry ที่ใช้ร่วมกัน:

  • FontRegistry::warmup() แยกวิเคราะห์ไฟล์ฟอนต์ที่ระบุและแคชตารางที่แยกวิเคราะห์แล้ว FontRegistry::lock() ตรึง registry ไว้เพื่อไม่ให้โค้ดระดับคำขอเปลี่ยนแปลงชุดฟอนต์ที่ใช้ร่วมกันได้ isLocked() รายงานสถานะปัจจุบัน หลังจากล็อก registry แล้ว จะสามารถใช้ร่วมกันข้ามคอรูทีนที่ทำงานพร้อมกันได้อย่างปลอดภัย
  • สร้าง ImageRegistry ด้วยงบประมาณ maxCacheBytes เมื่อการใช้งานเกินงบประมาณ ระบบจะขับไล่รายการที่ใช้งานล่าสุดน้อยที่สุดออก รูปภาพที่ใหญ่กว่างบประมาณจะข้ามแคชแทนที่จะทำให้แคชปั่นป่วน
  • ImageRegistry::reset() ขับไล่รูปภาพที่แคชไว้ทุกรายการออก โดยที่ registry ยังคงพร้อมใช้งาน คำขอถัดไปจะเติมข้อมูลกลับเข้าไปตามที่ต้องการ เรียกใช้ตามรอบ (ทุก N คำขอ หรือเมื่อ memoryUsage() เกินเกณฑ์) เพื่อนำระดับสูงสุดกลับสู่เส้นฐาน

เอกสารแต่ละฉบับที่ factory สร้างขึ้นเป็นไฟล์ Portable Document Format (PDF) ที่เป็นอิสระต่อกัน ISO 32000-2 §7.5.5 กำหนดว่า trailer ของไฟล์ที่ไม่เคยถูกอัปเดตจะไม่มีรายการ Prev และคำขอของเวิร์กเกอร์แต่ละครั้งจะสร้างไฟล์รุ่นแรกในลักษณะนั้น ดังนั้นคำขอจึงไม่ใช้สถานะเอกสารร่วมกัน แม้ว่าจะใช้แคชฟอนต์และรูปภาพร่วมกัน แท็ก BaseFont ของฟอนต์แบบ subset (ISO 32000-2 §9.6.4) คงที่ข้ามคำขอเพราะฟอนต์ที่แยกวิเคราะห์แล้วอยู่ใน registry ที่ใช้ร่วมกัน

สูตรนี้ใช้ API surface ที่สร้างจาก PHPDoc ของ NextPDF\Core\DocumentFactory, NextPDF\Typography\FontRegistry, NextPDF\Graphics\ImageRegistry และ NextPDF\Support\MemoryReport เมธอดและพร็อพเพอร์ตีหลักได้แก่ DocumentFactory::create(), FontRegistry::warmup() / lock() / isLocked() / memoryUsage(), ImageRegistry::reset() / memoryUsage() และ MemoryReport::$currentBytes / $peakBytes / $entryCount / utilizationPercent()

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\DocumentFactory;
use NextPDF\Graphics\ImageRegistry;
use NextPDF\Typography\FontRegistry;
// --- Worker boot (run ONCE, before the request loop) ---------------------
$fonts = new FontRegistry();
$fonts->lock(); // freeze the shared font set
$images = new ImageRegistry(maxCacheBytes: 50 * 1024 * 1024);
$factory = new DocumentFactory($fonts, $images);
// --- Per request ---------------------------------------------------------
$doc = $factory->create();
$doc->setTitle('Worker output');
$doc->addPage();
$doc->setFont('helvetica', 'B', 16);
$doc->cell(0, 12, 'Generated in a shared-registry worker', newLine: true);
$doc->save(getenv('NEXTPDF_COOKBOOK_OUTPUT') ?: __DIR__ . '/out.pdf');
// $doc leaves scope here → GC reclaims the whole document tree.

ตัวอย่างฉบับสมบูรณ์ทำตามช่องทางเอาต์พุตของฮาร์เนส แสดงลำดับการบูต ลูปคำขอที่มีขอบเขตจำกัด การ reset() ตามรอบ และการยืนยันระดับสูงสุดของหน่วยความจำ ฮาร์เนสจะรันสคริปต์นี้สองครั้งเพื่อตรวจสอบความสามารถในการทำซ้ำ

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\DocumentFactory;
use NextPDF\Graphics\ImageRegistry;
use NextPDF\Typography\FontRegistry;
// --- Worker boot: shared, process-lifetime registries --------------------
$fonts = new FontRegistry();
$fonts->lock(); // share-safe once locked
$images = new ImageRegistry(maxCacheBytes: 50 * 1024 * 1024);
$factory = new DocumentFactory($fonts, $images);
$resetEvery = 4; // reset cadence in requests
$peakAfterReset = 0;
// --- Simulated request loop ---------------------------------------------
for ($request = 1; $request <= 12; $request++) {
$doc = $factory->create();
$doc->setTitle("Worker Request #{$request}");
$doc->addPage();
$doc->setFont('helvetica', 'B', 16);
$doc->cell(0, 12, "Worker Request #{$request}", newLine: true);
$doc->setFont('helvetica', '', 11);
$doc->cell(0, 8, 'Shared FontRegistry / ImageRegistry across requests.', newLine: true);
// The harness captures the LAST request's PDF via the side channel.
if ($request === 12) {
$doc->save(getenv('NEXTPDF_COOKBOOK_OUTPUT') ?: __DIR__ . '/out.pdf');
} else {
$doc->getPdfData(); // force render, then drop
}
unset($doc); // explicit end-of-request
// Bound the cache high-water mark on a fixed cadence.
if ($request % $resetEvery === 0) {
$images->reset();
\gc_collect_cycles();
$report = $images->memoryUsage();
$peakAfterReset = \max($peakAfterReset, $report->currentBytes);
}
}
$final = $images->memoryUsage();
fwrite(STDERR, \sprintf(
"fonts.locked=%s images.entries=%d images.current=%dB peak_after_reset=%dB\n",
$fonts->isLocked() ? 'yes' : 'no',
$final->entryCount,
$final->currentBytes,
$peakAfterReset,
));

เว้น STDOUT ไว้ให้ว่างสำหรับฮาร์เนส ข้อความแสดงความคืบหน้าจะส่งไปยัง STDERR ส่วน PDF จะถูกเขียนไปยัง NEXTPDF_COOKBOOK_OUTPUT เท่านั้น และจะไม่ถูกส่งออกทางเอาต์พุตเลย

  • ล็อกก่อนใช้ร่วมกัน เรียก FontRegistry::lock() ตอนบูต registry ที่ยังเปลี่ยนแปลงได้และถูกคอรูทีนสองตัวเข้าถึงพร้อมกันจะเกิด data race ใช้ isLocked() เป็นการยืนยันใน health check
  • reset() ไม่ใช่ unset() ImageRegistry::reset() ขับไล่ข้อมูลไบนารีที่แคชไว้ออกและคง registry ให้ใช้งานได้ จึงเหมาะสำหรับการเรียกใช้เป็นระยะ หากทำลายและสร้าง registry ขึ้นใหม่ในทุกคำขอ จะสูญเสียประโยชน์ของแคชที่ใช้ร่วมกัน
  • การข้ามรูปภาพที่มีขนาดเกิน รูปภาพที่ใหญ่กว่า maxCacheBytes จะถูกถอดรหัสในแต่ละครั้งที่ใช้และไม่ถูกแคช จึงไม่ไปขับไล่ working set ออก พฤติกรรมนี้ตั้งใจให้เป็นเช่นนั้น กำหนดขนาดงบประมาณให้เหมาะกับรูปภาพทั่วไปที่ใช้งาน ไม่ใช่สำหรับรูปภาพขนาดใหญ่ที่พบได้ยาก
  • เอกสารต้องพ้นขอบเขต หากเก็บ Document ไว้ในตัวแปร static การผูก container ที่มีอายุยาว หรือ closure ที่เวิร์กเกอร์จับไว้ กราฟอ็อบเจ็กต์ทั้งหมดจะคงมีชีวิตอยู่และการเก็บขยะต่อคำขอจะทำงานไม่ได้ การเรียก unset() หรือการออกจากขอบเขตเป็นสิ่งจำเป็น
  • ตำแหน่งการวาง gc_collect_cycles() ตัวเก็บวงรอบของ PHP ไม่รับรู้ขอบเขตของคำขอ เรียกใช้หลังรอบการรีเซ็ต ไม่ใช่ในทุกคำขอ การทำเช่นนี้จำกัดระดับสูงสุดโดยไม่เพิ่มต้นทุนการเก็บขยะให้กับเส้นทางที่ถูกเรียกบ่อย
  • ข้อควรระวังเรื่องความสามารถในการทำซ้ำ ไทม์สแตมป์ของเอกสารและ /ID ใน trailer จะถูกสร้างใหม่ในการบันทึกแต่ละครั้ง (ISO 32000-2 §14.3) ดังนั้น PDF ที่จับผลลัพธ์ได้จึงถูกเปรียบเทียบด้วยโปรไฟล์ เชิงความหมาย (โครงสร้าง abstract syntax tree (AST) บวกกับเมตาดาตา ไม่ใช่ไบต์ที่เปลี่ยนไปได้) ดู “ความสอดคล้อง”
  • registry ที่ใช้ร่วมกันทำให้การแยกวิเคราะห์ฟอนต์และถอดรหัสรูปภาพซ้ำ ๆ กลายเป็นต้นทุนการบูตเพียงครั้งเดียว จากนั้นงานต่อคำขอจึงเหลือเพียงการจัดเค้าโครงและ serialization
  • หน่วยความจำที่ใช้งานอยู่สูงสุดถูกจำกัดด้วย maxCacheBytes บวกกับ working set ของเอกสารที่กำลังดำเนินการอยู่หนึ่งฉบับ การ reset() ตามรอบจะคืนแคชสู่เส้นฐาน ดังนั้นเวิร์กเกอร์ที่ทำงานระยะยาวจึงไม่เกิดรูปแบบฟันเลื่อยที่ไต่สูงขึ้นเรื่อย ๆ
  • frontmatter performance_budget (wall_ms: 4000, peak_mb: 192) จำกัดการรันลูป 12 คำขอของฮาร์เนส ฮาร์เนสบังคับใช้งบประมาณนี้ ไม่ใช่การรับประกันสำหรับเอกสารฉบับใดฉบับหนึ่ง
  • สูตรนี้ให้ความครอบคลุม “memory/GC” ของรายการช่องว่าง §4.3 สำหรับ #31 examples/14-worker-factory.php ที่อยู่เบื้องหลังมีอยู่จริง และ tests/Cookbook/Php/WorkerSafeBatchRenderingRecipeTest.php เพิ่มการยืนยัน memory/GC ที่ขาดหายไป (ระดับสูงสุดไม่เพิ่มขึ้นข้ามรอบหลังการรีเซ็ต)
  • รูปแบบเวิร์กเกอร์ประมวลผลเอกสารหนึ่งฉบับต่อคำขอ และใช้ร่วมกันเฉพาะแคชฟอนต์ที่แยกวิเคราะห์แล้วกับรูปภาพที่ถอดรหัสแล้วเท่านั้น เนื้อหาเอกสารไม่ข้ามขอบเขตของคำขอ คำขอหนึ่งไม่สามารถอ่านข้อมูลเอกสารของอีกคำขอหนึ่งผ่าน registry ที่ใช้ร่วมกันได้
  • อินพุตที่ไม่น่าเชื่อถือยังคงผ่านขอบเขตอินพุตปกติของ NextPDF และรูปแบบเวิร์กเกอร์ไม่ผ่อนปรนการตรวจสอบความถูกต้อง จงปฏิบัติต่ออินพุต HyperText Markup Language (HTML) และแอสเซ็ตของแต่ละคำขอว่าไม่น่าเชื่อถือ เช่นเดียวกับในโปรเซสแบบต่อคำขอ
ข้อความระบุข้อกำหนดข้อreference_id (รหัสอ้างอิง)
วันที่แก้ไขเอกสารถูกสร้างใหม่ในการบันทึกแต่ละครั้ง ดังนั้นเอาต์พุตต่อคำขอจึงไม่เสถียรในระดับไบต์ISO 32000-2§14.3
เอกสารของเวิร์กเกอร์แต่ละฉบับเป็นไฟล์ที่ยังไม่เคยถูกอัปเดต (ไม่มี Prev ใน trailer) คำขอต่าง ๆ ไม่ใช้สถานะเอกสารร่วมกันISO 32000-2§7.5.5
คำนำหน้าแท็กของฟอนต์แบบ subset คงที่ข้ามคำขอเพราะฟอนต์ที่แยกวิเคราะห์แล้วอยู่ใน registry ที่ใช้ร่วมกันISO 32000-2§9.6.4

เนื่องจาก /ID ใน trailer และวันที่แก้ไขถูกสร้างใหม่ในการบันทึกแต่ละครั้ง สูตรนี้จึงถูกตรวจสอบด้วยโปรไฟล์ความสามารถในการทำซ้ำ เชิงความหมาย (ความเท่ากันของโครงสร้าง abstract syntax tree (AST) บวกกับการเปรียบเทียบเฉพาะเมตาดาตา) การอ้างความสอดคล้องในระดับบิตหรือระดับโครงสร้างจึงไม่ถูกต้องสำหรับเอาต์พุตของเวิร์กเกอร์