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

การสร้างเอกสารปริมาณสูง

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

การสร้าง PDF หนึ่งไฟล์เป็นการเรียกฟังก์ชันหนึ่งครั้ง แต่การสร้างหนึ่งแสนไฟล์ตามกำหนดเวลาเป็นปัญหาเชิงระบบ ทั้งหน่วยความจำที่ต้องอยู่ในขอบเขตจำกัด งานที่ต้องทำแบบขนาน และตัวเลขที่ต้องมีความหมาย หน้านี้อธิบายสถานการณ์การสร้างแบบ batch ตั้งแต่คำถามด้าน throughput ไปจนถึงการ deploy ที่ใช้งานได้จริง และระบุอย่างชัดเจนว่าคำตอบที่ตรงไปตรงมาคือ “วัดผลกับเอกสารของคุณเอง” ไม่ใช่ตัวเลขพาดหัว

การสร้างแบบ batch มักล้มเหลวได้สองรูปแบบ รูปแบบแรกคือหน่วยความจำค่อยๆไต่สูงขึ้น worker ที่ทำงานยาวนานจะสะสมสถานะที่ถูกเก็บค้างไว้ทีละเอกสารจนกระทั่งถูกหยุดระหว่าง batch ทำให้การรันนั้นไม่เสร็จสมบูรณ์และไม่ล้มเหลวอย่างเป็นระเบียบ รูปแบบที่สองคือตัวเลขที่ดูน่าเชื่อถือแต่ไร้ความหมาย กล่าวคือ ผลการทดสอบประสิทธิภาพจากเอกสารธรรมดาๆถูกนำมาใช้กำหนดขนาดกลุ่มเครื่องที่ต้องเรนเดอร์เอกสารที่ซับซ้อน และกว่าจะรู้ว่าตัวเลขนั้นผิดก็เมื่ออยู่ภายใต้ภาระงานจริงในระบบ production

คุณสามารถหลีกเลี่ยงความล้มเหลวทั้งสองรูปแบบนี้ได้ แต่ต้องออกแบบรูปแบบหน่วยความจำและวิธีการวัดผลไว้ตั้งแต่ต้น ไม่ใช่เพิ่มเข้ามาภายหลังจากเกิดเหตุการณ์ครั้งแรก

  • หน่วยของงานคือเอกสารแบบใช้แล้วทิ้ง ไม่ใช่เอกสารที่ใช้ร่วมกัน เก็บข้อมูลที่มีอายุเท่ากับกระบวนการ (ฟอนต์ แคชรูปภาพ) ไว้ใน registry ที่ใช้ร่วมกัน สร้างและทิ้งเอกสารในแต่ละการเรนเดอร์
  • หน่วยความจำมีสองส่วน และมีเพียงส่วนเดียวที่สำคัญต่อ worker ที่ทำงานยาวนาน ค่า peak ชั่วคราวระหว่างการเรนเดอร์เป็นสิ่งที่คาดหมายได้ ส่วนหน่วยความจำที่ retained ซึ่งไม่คืนกลับมาคือการรั่วไหลที่ทำให้ batch ล้มเหลว
  • Throughput คือการทำงานแบบขนานบวกกับต้นทุนต่อการเรนเดอร์ที่อยู่ในขอบเขตจำกัด รูปแบบที่ใช้งานได้จริงคือคิวที่ป้อนงานให้ worker แบบไร้สถานะ โดยแต่ละตัวจะเรนเดอร์แล้วปล่อยทรัพยากรคืน
  • ตัวเลขที่ปราศจากวิธีการได้มาไม่ถือเป็นตัวเลข NextPDF รายงานผลการวัดต่อการเรนเดอร์ในฐานะข้อมูลที่คุณเก็บรวบรวมเอง และปฏิเสธการกล่าวอ้างเรื่องความเร็วที่ไม่มีเงื่อนไขกำกับ ตัวเลขที่สำคัญที่สุดคือตัวเลขที่คุณวัดจากเทมเพลตของคุณเอง (ISO 24495-1 §5.x11 — วางข้อความสำคัญไว้ในจุดที่ผู้อ่านจะพบ)

สถาปัตยกรรมถูกสร้างขึ้นรอบการตัดสินใจเพียงข้อเดียว นั่นคือ สถานะที่มีอายุเท่ากับกระบวนการจะถูกใช้ร่วมกันและเปลี่ยนแปลงไม่ได้ ส่วนสถานะที่มีอายุเท่ากับการเรนเดอร์จะสร้างใหม่และถูกทิ้งไป ฟอนต์เป็นข้อมูลเชิงโครงสร้างที่ถูกแยกวิเคราะห์ครั้งเดียวแล้วล็อกไว้ ดังนั้นจึงไม่มีการเรนเดอร์ใดเปลี่ยนแปลงฟอนต์แล้วส่งผลปนเปื้อนไปยังการเรนเดอร์ถัดไปได้ แคชรูปภาพเป็นที่เก็บแบบ least-recently-used ที่มีขอบเขตจำกัดและไม่ถูกล็อกเลย หน่วยความจำจึงมีเพดานกำกับโดยไม่รั่วไหลข้ามคำขอ ส่วน document factory เป็น singleton แบบไร้สถานะ ทุกเอกสารที่ factory สร้างขึ้นล้วนเป็นแบบใช้แล้วทิ้ง

การแยกส่วนนี้คือสิ่งที่ทำให้ worker ปลอดภัยพอที่จะรันได้นานหลายชั่วโมงภายใต้ Octane, RoadRunner หรือ Swoole และกำจัดรูปแบบความล้มเหลวที่ “คำขอ N ทำให้คำขอ N+1 เสียหาย” ด้วยการออกแบบโครงสร้าง แทนที่จะหวังว่าเอกสารจะรีเซ็ตตัวเอง

สถานการณ์นี้มีสี่ขั้นตอน

  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.
สถานการณ์ปริมาณสูงตั้งแต่ต้นจนจบ สถานะที่ใช้ร่วมกันและเปลี่ยนแปลงไม่ได้จะถูก warm หนึ่งครั้ง แต่ละงานเรนเดอร์บนเอกสารแบบใช้แล้วทิ้งแล้วปล่อยทรัพยากรคืน throughput ขยายขนาดด้วยการเพิ่มจำนวน worker ไม่ใช่ด้วยการขยาย worker ตัวเดียวให้ใหญ่ขึ้น

framework bridge ทำให้รูปแบบนี้เป็นค่าเริ่มต้น แทนที่จะเป็นสิ่งที่คุณต้องประกอบเอง service provider ของ Laravel ลงทะเบียน font registry เป็น singleton ที่ถูก warm และล็อกไว้ และ bind เอกสารเป็น instance ใหม่ในแต่ละการ resolve พร้อม queued job ที่จำกัดจำนวนครั้งของการลองใหม่ มี timeout และมี exponential backoff โดย job ดังกล่าวตรวจสอบ output path ที่ฝั่ง worker เนื่องจาก payload ของคิวที่ถูก serialize อาจถูกแก้ไขดัดแปลงระหว่างการส่งได้ การผสานรวมกับ Symfony และ CodeIgniter ยึดแนวทางเดียวกัน คือเอกสารแบบใช้แล้วทิ้งและ registry ที่ใช้ร่วมกัน

โมเดลหน่วยความจำเป็นแบบ code-backed Evidence: Code-backed NextPdfServiceProvider ของ Laravel ลงทะเบียน FontRegistry เป็น singleton ที่ถูก warm แล้วจึง lock() ลงทะเบียน ImageRegistry เป็น singleton แบบ bounded-LRU ที่จงใจ ไม่ ล็อก และลงทะเบียน Document เป็น binding แบบต่อการ resolve ผ่าน factory ที่ไร้สถานะ โมเดลเอกสารแบบใช้แล้วทิ้งอยู่ในการเดินสาย (wiring) ไม่ใช่ในเนื้อความ GeneratePdfJob มี tries, timeout และ backoff และตรวจสอบ output path ภายใน handle() อีกครั้ง

ส่วนการวัดผลเป็นแบบ benchmark-backed Evidence: Benchmark-backed เอนจินจะส่งออก RenderReport ที่เปลี่ยนแปลงไม่ได้ในแต่ละการสร้าง ซึ่งมีเวลาการเรนเดอร์เป็นมิลลิวินาที หน่วยความจำ peak เป็นไบต์ จำนวนหน้า จำนวนคำเตือน และจำนวนครั้งของ fallback อันเป็น ข้อมูลนำเข้าที่แม่นยำสำหรับการกำหนดขนาดกลุ่มเครื่อง ตัววิเคราะห์ memory-fragmentation แยกต่างหากจะแยกแยะหน่วยความจำ peak (ชั่วคราว) ออกจากหน่วยความจำ retained การ แยกแยะนี้ช่วยบอกว่า worker ที่ทำงานยาวนานนั้นสมบูรณ์ดีหรือกำลังค่อยๆ รั่วไหล ตัว benchmark harness เองถูกกำหนดค่าให้ทำซ้ำ หลายรอบพร้อมการ warmup เนื่องจากการจับเวลาเพียงครั้งเดียวคือสัญญาณรบกวน

วินัยนี้เป็น หลักการออกแบบ 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() ไม่ได้มีไว้เพียงเพื่อความเรียบร้อย สถานะต่อการเรนเดอร์มีจุดประสงค์ให้ถูกปล่อยคืนในแต่ละรอบ เพื่อให้หน่วยความจำที่ retained คืนกลับสู่ค่าพื้นฐาน worker ที่มีค่าพื้นฐานไต่สูงขึ้นเรื่อยๆในแต่ละรอบคือความล้มเหลวที่ลูปนี้ถูกออกแบบมาเพื่อหลีกเลี่ยง

ความเข้าใจผิดหลักคือ “NextPDF สร้าง PDF ได้กี่ไฟล์ต่อวินาที?” ราวกับว่ามีคำตอบเพียงคำตอบเดียว ความจริงไม่ได้เป็นเช่นนั้น และการอ้างตัวเลขเพียงค่าเดียวคือสาเหตุที่ทำให้กลุ่มเครื่องถูกกำหนดขนาดผิด ต้นทุนการเรนเดอร์ถูกกำหนดเป็นหลักโดยตัวเอกสาร ดังนั้นตัวเลขเดียวที่ควรนำไปใช้คือตัวเลขที่วัดบนเทมเพลตของคุณเองด้วยรายงานต่อการเรนเดอร์ของเอนจินเอง ตัวเลขที่ปราศจากเอกสาร ฮาร์ดแวร์ และวิธีการที่อยู่เบื้องหลังเป็นเพียงเครื่องประดับ ไม่ใช่ข้อมูล

ความเข้าใจผิดประการที่สองคือการคิดว่าหน่วยความจำ peak คือสิ่งที่ต้องเฝ้าระวัง ค่า peak เป็นแบบชั่วคราวและคาดหมายได้ ค่านี้จะคืนกลับมา ตัวเลขที่ทำให้ batch ล้มเหลวคือหน่วยความจำ retained ที่ไม่คืนกลับมา นั่นคือเหตุผลที่เอนจินแยกทั้งสองค่าออกจากกัน

  • ไม่มีตัวเลข throughput ที่ใช้ได้ทั่วไป และหน้านี้จงใจไม่ระบุตัวเลขใดๆ ต้นทุนการเรนเดอร์ขึ้นอยู่กับเอกสารของคุณ จงวัดผลด้วยรายงานต่อการเรนเดอร์
  • หน่วยความจำที่อยู่ในขอบเขตจำกัดขึ้นอยู่กับการใช้โมเดลเอกสารแบบใช้แล้วทิ้ง การถือเอกสารไว้ตลอดหลายการเรนเดอร์ หรือการใช้สถานะต่อการเรนเดอร์ที่เปลี่ยนแปลงได้ร่วมกัน จะทำให้การรับประกันนี้เป็นโมฆะ framework bridge ใช้รูปแบบที่ปลอดภัยเป็นค่าเริ่มต้น การเดินสายที่ทำขึ้นเองต้องจำลองรูปแบบนี้ให้เหมือนกัน
  • แคชรูปภาพมีขอบเขตจำกัด ไม่ใช่ไม่จำกัด ภายใต้ภาระงานที่มีรูปภาพไม่ซ้ำกันจำนวนมาก LRU จะ evict ข้อมูลออก นั่นคือการออกแบบ ไม่ใช่การถดถอย
  • การกำหนดขนาด worker pool การเลือกคิว และการ autoscaling เป็นการตัดสินใจด้านการ deploy ที่อยู่นอกเหนือเอนจิน NextPDF จัดเตรียมผลการวัดและ primitive ที่อยู่ในขอบเขตจำกัดให้ NextPDF ไม่ได้รันคิวของคุณ
  • RenderReport คือข้อมูล ไม่ใช่คำตัดสิน รายงานนี้บอกคุณว่าเกิดอะไรขึ้นในการเรนเดอร์ การแปลงข้อมูลนั้นให้เป็นแผนความจุคือการวิเคราะห์ของคุณเอง
  • หน้านี้เป็นแบบ benchmark-backed สำหรับพื้นผิวการวัดผล และเป็นแบบ code-backed สำหรับโมเดลหน่วยความจำ หน้านี้ไม่ได้ยืนยันอัตราที่เจาะจงใดๆ
primitive การสร้างเอกสารปริมาณสูงแบบเข้าคิว — edition availability
Edition Availability
Core

โมเดลเอกสารแบบใช้แล้วทิ้ง registry ที่ใช้ร่วมกันและเปลี่ยนแปลงไม่ได้ RenderReport ต่อการเรนเดอร์ และตัววิเคราะห์ memory-fragmentation ล้วนเป็น Core การสร้าง PDF ปริมาณสูงทั่วไปไม่จำเป็นต้องใช้ระดับเชิงพาณิชย์

Pro

primitive เดียวกัน คุณสมบัติเชิงพาณิชย์ (การลงลายเซ็น PDF/A) เพิ่มต้นทุน ต่อการเรนเดอร์ที่คุณควรวัด ไม่ใช่สันนิษฐานเอง

Enterprise

primitive เดียวกัน งานใบแจ้งหนี้แบบมีโครงสร้างและงานตรวจสอบความถูกต้องเพิ่มต้นทุน ต่อการเรนเดอร์เพิ่มเติมที่ขยายตามขนาด payload และขนาดชุดกฎ

  • เอกสารแบบใช้แล้วทิ้ง (disposable document) — instance ของเอกสารที่ถูกสร้างขึ้นสำหรับการเรนเดอร์เพียงครั้งเดียวและถูกทิ้งหลังจากนั้น ดังนั้นจึงไม่มีสถานะรั่วไหลไปยังการเรนเดอร์ถัดไป
  • registry ที่ใช้ร่วมกัน (shared registry) — สถานะที่มีอายุเท่ากับกระบวนการและเปลี่ยนแปลงไม่ได้หลังการ warm (ฟอนต์ แคชรูปภาพ) ซึ่งถูกนำกลับมาใช้ซ้ำตลอดหลายการเรนเดอร์โดยไม่มีต้นทุนต่อการเรนเดอร์
  • หน่วยความจำ peak (peak memory) — จุดสูงสุดชั่วคราวระหว่างการเรนเดอร์ เป็นสิ่งที่คาดหมายได้และจะคืนกลับสู่ค่าพื้นฐาน
  • หน่วยความจำ retained (retained memory) — หน่วยความจำที่ยังถูกถือไว้หลังการเรนเดอร์เสร็จสมบูรณ์ การที่ค่าพื้นฐานของ retained ไต่สูงขึ้นตลอดหลายการเรนเดอร์คือการรั่วไหล
  • Worker — กระบวนการที่ทำงานยาวนานซึ่งดึง render job จากคิว ต้องคงสภาพหน่วยความจำให้อยู่ในขอบเขตจำกัดเพื่อให้รอดผ่าน batch ได้
  • RenderReport — สแนปช็อตเมตริกต่อการเรนเดอร์ของเอนจินที่เปลี่ยนแปลงไม่ได้ (เวลา หน่วยความจำ peak จำนวนหน้า คำเตือน) ซึ่งใช้กำหนดความจุจากข้อมูลจริง