การเรนเดอร์ PDF อย่างปลอดภัยในเวิร์กเกอร์ที่ทำงานระยะยาว
ภาพรวมโดยย่อ
หัวข้อที่มีชื่อว่า “ภาพรวมโดยย่อ”เวิร์กเกอร์ PHP (PHP: Hypertext Preprocessor) ที่ทำงานระยะยาว (RoadRunner, Swoole, Laravel Octane) คงโปรเซสเดิมให้ทำงานต่อเนื่องระหว่างคำขอจำนวนมาก หากต้องแยกวิเคราะห์ฟอนต์ชุดเดิมและถอดรหัสรูปภาพชุดเดิมในทุกคำขอ จะสิ้นเปลืองเวลาของหน่วยประมวลผลและเพิ่มหน่วยความจำที่ใช้งานอยู่ NextPDF หลีกเลี่ยงต้นทุนดังกล่าวด้วยการแยกอายุการใช้งานสองแบบออกจากกัน
- อายุการใช้งานระดับโปรเซสสำหรับใช้ร่วมกัน:
FontRegistryและImageRegistryเก็บตารางฟอนต์ที่แยกวิเคราะห์แล้วและแคชรูปภาพที่ถอดรหัสแล้ว สร้าง registry เหล่านี้เพียงครั้งเดียวเมื่อเวิร์กเกอร์เริ่มทำงาน - อายุการใช้งานระดับคำขอที่ใช้แล้วทิ้ง:
Documentที่ส่งคืนโดยDocumentFactory::create()ให้สร้าง เขียนเอาต์พุต แล้วปล่อยให้พ้นขอบเขต จากนั้นตัวเก็บขยะของ PHP จะสามารถคืนกราฟอ็อบเจ็กต์ทั้งหมดได้
สูตรนี้แสดงลำดับการบูตเวิร์กเกอร์ งานต่อคำขอ และการรีเซ็ตตามรอบที่ช่วยรักษาระดับการใช้หน่วยความจำสูงสุดให้คงที่
การติดตั้ง
หัวข้อที่มีชื่อว่า “การติดตั้ง”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
หัวข้อที่มีชื่อว่า “API surface”สูตรนี้ใช้ 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) บวกกับการเปรียบเทียบเฉพาะเมตาดาตา) การอ้างความสอดคล้องในระดับบิตหรือระดับโครงสร้างจึงไม่ถูกต้องสำหรับเอาต์พุตของเวิร์กเกอร์