تخطَّ إلى المحتوى

توليد المستندات بكميات كبيرة

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

توليد ملف ⁨PDF⁩ واحد لا يتعدّى استدعاء دالّة. أما توليد مئة ألف ملف وفق جدول زمني فهو مشكلة أنظمة: ذاكرة يجب أن تبقى ضمن حدودها، وعمل يجب أن يجري بالتوازي، وأرقام يجب أن تكون ذات معنى. تستعرض هذه الصفحة سيناريو التوليد الدُّفعي بدءًا من سؤال معدّل الإنتاجية وصولًا إلى نشر يصمد. وتقول صراحةً إن الجواب الصادق هو “قِسْه على مستنداتك أنت”، لا رقمًا دعائيًا.

يفشل التوليد الدُّفعي غالبًا بطريقتين واضحتين. الأولى هي تسلُّل الذاكرة. يراكم العامل طويل العمر حالةً محتجَزة، مستندًا بعد آخر، إلى أن يُقتَل في منتصف الدُّفعة، فلا يكتمل التشغيل ولا يفشل بصورة نظيفة. والثانية رقم يبدو واثقًا لكنه عديم المعنى: قياس مرجعي مأخوذ من مستند بسيط يُستخدَم لتحديد حجم أسطول يعرض مستندات معقّدة، ولا يتبيّن خطؤه إلا تحت حمل الإنتاج.

يمكنك تجنُّب الحالتين، لكن فقط إذا صمّمت شكل الذاكرة وطريقة القياس منذ البداية، بدلًا من إضافتهما بعد أول حادثة.

  • وحدة العمل مستند قابل للتخلّص منه، لا مستند مشترك. احتفظ ببيانات عمر العملية (الخطوط، ذاكرة الصور المؤقتة) في سجلات مشتركة؛ وأنشئ المستند وتخلّص منه لكل عملية عرض.
  • للذاكرة جانبان، ولا يهمّ العامل طويل العمر إلا أحدهما. الذروة العابرة أثناء العرض متوقَّعة؛ أما الذاكرة المحتجَزة التي لا تعود فهي التسرُّب الذي يُنهي الدُّفعة.
  • معدّل الإنتاجية هو التوازي مع كلفة محدودة لكل عملية عرض. الشكل الذي يصمد هو طابور يُغذّي عمّالًا عديمي الحالة، كلٌّ منهم يعرض ثم يُحرِّر.
  • الرقم دون طريقته ليس رقمًا. يُبلّغ ⁨NextPDF⁩ عن قياسات كل عملية عرض كبيانات تجمعها أنت، ويرفض ادّعاءات السرعة غير المشروطة. أهمّ رقم هو الذي تقيسه على قوالبك أنت (⁨ISO 24495-1⁩ §5.⁨x11⁩ — ضَع الرسالة التي تهمّ حيث يجدها القارئ).

بُنيت المعمارية حول قرار واحد: الحالة التي تعيش طوال عمر العملية مشتركة وغير قابلة للتغيير؛ والحالة التي تعيش طوال عملية عرض واحدة جديدة ويُتخلَّص منها. الخطوط بيانات بنيوية تُحلَّل مرة واحدة ثم تُقفَل، فلا تستطيع أي عملية عرض تغييرها أو تلويث العملية التالية. ذاكرة الصور المؤقتة مخزن محدود من نوع الأقل استخدامًا مؤخرًا لا يُقفَل أبدًا، فتبقى الذاكرة عند حدّها الأقصى دون أن تتسرّب عبر الطلبات. مصنع المستندات مفردة عديمة الحالة؛ وكل مستند يُنشئه قابل للتخلّص منه.

هذا الفصل هو ما يجعل تشغيل العامل لساعات تحت ⁨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.
سيناريو الأحجام الكبيرة من طرف إلى طرف: تُهيَّأ الحالة المشتركة غير القابلة للتغيير مرة واحدة؛ وتعرض كل مهمة على مستند قابل للتخلّص منه ثم تُحرِّر؛ ويتوسّع معدّل الإنتاجية بإضافة عمّال، لا بتكبير عامل واحد.

تجعل جسور أُطر العمل هذا الشكل هو الافتراضي بدلًا من أن يكون شيئًا تجمعه بنفسك. يُسجّل مزوِّد خدمة ⁨Laravel⁩ سجلّ الخطوط بوصفه مفردة مهيَّأة ومقفَلة، ويربط المستند بوصفه نسخة جديدة لكل عملية حلّ. ويأتي مزوَّدًا بمهمة مُدرَجة في الطابور بعدد محاولات محدود ومهلة وتراجع أُسّي. وتتحقّق هذه المهمة من مسار الإخراج على جانب العامل، لأن حمولة الطابور المُسلسَلة يمكن العبث بها أثناء النقل. ويتّبع تكاملا ⁨Symfony⁩ و⁨CodeIgniter⁩ الانضباط نفسه: مستند قابل للتخلّص منه وسجلّ مشترك.

نموذج الذاكرة مدعوم بالشيفرة. Evidence: Code-backed يُسجّل NextPdfServiceProvider الخاص بـ Laravel سجلّ FontRegistry بوصفه مفردة تُهيَّأ ثم تُقفَل بـ lock()، وImageRegistry بوصفه مفردة محدودة من نوع LRU لا تُقفَل عمدًا، وDocument بوصفه ربطًا لكل عملية حلّ عبر مصنع عديم الحالة. نموذج المستند القابل للتخلّص منه موجود في الربط، لا في النصّ. تحمل GeneratePdfJob الخصائص tries وtimeout وbackoff وتُعيد التحقّق من مسار الإخراج داخل handle().

سطح القياس مدعوم بالقياس المرجعي. Evidence: Benchmark-backed يُصدر المحرك تقرير RenderReport غير قابل للتغيير لكل عملية توليد يحمل زمن العرض بالميلي ثانية، وذروة الذاكرة بالبايت، وعدد الصفحات، وأعداد التحذيرات، وحالات الرجوع البديل — أي المدخلات الدقيقة التي تحتاجها لتحديد حجم أسطول. ويُميّز محلّل تجزُّؤ الذاكرة المنفصل بين الذاكرة الذروة (العابرة) والذاكرة المحتجَزة. وهذا التمييز يوضح لك ما إذا كان العامل طويل العمر سليمًا أم يتسرّب ببطء. وإطار القياس المرجعي نفسه مُهيَّأ لدورات متكرّرة مع تهيئة مسبقة، لأن قياسًا زمنيًا واحدًا مجرّد ضجيج.

هذا الانضباط مبدأ تصميمي: 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() ليست إضافة تجميلية. المقصود أن تُحرَّر حالة كل عملية عرض في كل تكرار كي تعود الذاكرة المحتجَزة إلى خطّ الأساس. إن العامل الذي يرتفع خطّ أساسه عبر التكرارات هو الفشل الذي صُمّمت هذه الحلقة لتجنُّبه.

المفهوم الخاطئ الأبرز هو “كم عدد ملفات ⁨PDF⁩ في الثانية التي يستطيع ⁨NextPDF⁩ توليدها؟” وكأن له إجابة واحدة. ليست له إجابة واحدة، واقتباس إجابة واحدة هو ما يؤدّي إلى تحديد أحجام الأساطيل خطأً. كلفة العرض يحكمها المستند، لذا فالرقم الوحيد الجدير بالعمل بموجبه هو الرقم المقيس على قوالبك أنت بتقرير المحرك الخاص بكل عملية عرض. إن رقمًا دون المستند والعتاد والطريقة التي وراءه زخرفة، لا بيانات.

المفهوم الخاطئ الثاني هو أن ذروة الذاكرة هي ما ينبغي مراقبته. الذروة عابرة ومتوقَّعة؛ فهي تعود. أما الرقم الذي يُنهي الدُّفعة فهو الذاكرة المحتجَزة التي لا تعود. وهذا بالضبط سبب فصل المحرك بينهما.

  • لا يوجد رقم معدّل إنتاجية شامل، وهذه الصفحة لا تذكر أي رقم عمدًا. تعتمد كلفة العرض على مستنداتك؛ فقِسها بتقرير كل عملية عرض.
  • الذاكرة المحدودة تعتمد على استخدام نموذج المستند القابل للتخلّص منه. إن الاحتفاظ بمستند عبر عمليات عرض كثيرة، أو مشاركة حالة قابلة للتغيير خاصة بكل عملية عرض، يُلغي الضمان. تتبنّى جسور أُطر العمل الشكل الآمن افتراضيًا. أما الربط اليدوي فيجب أن يُحاكيه.
  • ذاكرة الصور المؤقتة محدودة، لا غير محدودة. تحت أحمال ثقيلة من الصور الفريدة، يُخرِج ⁨LRU⁩ عناصر منها. هذا هو التصميم، وليس تراجعًا في الأداء.
  • تحديد حجم تجمُّع العمّال، واختيار الطابور، والتوسيع التلقائي قرارات نشر خارج المحرك. يوفّر ⁨NextPDF⁩ القياسات والبدائية المحدودة. لكنه لا يُشغّل طابورك.
  • RenderReport بيانات، لا حُكم. يخبرك بما حدث في عملية عرض. أما تحويل ذلك إلى خطة سعة فهو تحليلك أنت.
  • هذه الصفحة مدعومة بالقياس المرجعي فيما يخصّ سطح القياس، ومدعومة بالشيفرة فيما يخصّ نموذج الذاكرة. وهي لا تؤكّد أي معدّل محدّد.
Queued high-volume generation primitives — edition availability
Edition Availability
Core

نموذج المستند القابل للتخلّص منه، والسجلات المشتركة غير القابلة للتغيير، وRenderReport لكل عملية عرض، ومحلّل تجزُّؤ الذاكرة جزء من إصدار ⁨Core.⁩ توليد ملفات ⁨PDF⁩ العادي بأحجام كبيرة لا يحتاج إلى أي مستوى تجاري.

Pro

البدائيات نفسها؛ والميزات التجارية (التوقيع، ⁨PDF/A⁩) تُضيف كلفة لكل عملية عرض ينبغي أن تقيسها، لا أن تفترضها.

Enterprise

البدائيات نفسها؛ وعمل الفواتير المهيكلة والتحقّق يُضيف مزيدًا من الكلفة لكل عملية عرض التي تتوسّع مع حجم الحمولة ومجموعة القواعد.

  • المستند القابل للتخلّص منه — نسخة مستند تُنشَأ لعملية عرض واحدة ويُتخلَّص منها بعدها، فلا تتسرّب أي حالة إلى عملية العرض التالية.
  • السجلّ المشترك — حالة بعمر العملية وغير قابلة للتغيير بعد التهيئة (الخطوط، ذاكرة الصور المؤقتة) يُعاد استخدامها عبر عمليات العرض دون كلفة لكل عملية عرض.
  • ذروة الذاكرة — أعلى مستوى عابر أثناء عملية العرض؛ متوقَّع ويعود إلى خطّ الأساس.
  • الذاكرة المحتجَزة — الذاكرة التي تبقى محتجَزة بعد اكتمال عملية العرض؛ وارتفاع خطّ الأساس المحتجَز عبر عمليات العرض تسرُّب.
  • العامل — عملية طويلة العمر تسحب مهام العرض من طابور؛ يجب أن تبقى محدودة الذاكرة لتصمد طوال الدُّفعة.
  • ⁨RenderReport⁩ — لقطة مقاييس المحرك غير القابلة للتغيير لكل عملية عرض (الزمن، ذروة الذاكرة، عدد الصفحات، التحذيرات) تُستخدَم لتحديد السعة انطلاقًا من بيانات حقيقية.