One self-hosted console to run your entire business — commerce, ERP, HRM, CRM & manufacturing

الطوابير والعمال تحت الحمل العالي: من عامل تسلسلي واحد إلى مجموعة عمليات تتوسع تلقائيًّا

تنهار المهام الخلفية أولًا عند الذروة. تتبّع إعدادًا واحدًا من عامل تسلسلي أنجز 20 إلى 26% فقط من المهام في وقتها، إلى طوابير منفصلة ومجموعة عمليات Horizon تتوسع خلال ثوانٍ.

Author

Anichur Rahaman

منذ أسبوعين12 min read
الطوابير والعمال تحت الحمل العالي: من عامل تسلسلي واحد إلى مجموعة عمليات تتوسع تلقائيًّا

أمام مهندس المناوبة في منصة امتحانات دقيقة واحدة فقط، صباح يوم الاختبار، الساعة 8:59. وفي التاسعة تمامًا يضغط 1,000 طالب زر «إرسال» في الدقيقة نفسها. خوادم الويب تردّ على كل نقرة في أجزاء من الثانية، ولوحات المراقبة خضراء. وعند 9:03 يبدأ صندوق الدعم بالامتلاء: رأى الطلاب كلمة «تم الإرسال» على الشاشة، لكن لم يصل أي تأكيد، وفريق التصحيح لا يرى سوى عدد قليل من الإجابات. هذا مشهد توضيحي، لكنني جهّزت منصة حقيقية لضغط من هذا النوع بالضبط.

لم يكن السبب في طبقة الويب. كان عامل خلفي واحد يعالج الإرسالات واحدًا بعد الآخر. الطابور يحوّل الدفعة المفاجئة إلى صفّ، ولذلك فإن جودة العمال هي التي تحدد هل يستغرق الصفّ ثلاث ثوانٍ أم ست عشرة دقيقة.

يتتبّع هذا المقال الطريق الذي سلكته من عامل تسلسلي واحد إلى مجموعة عمليات تكبر وتصغر من تلقاء نفسها، مع عادات تصميم المهام التي تجعل هذه المجموعة آمنة التشغيل.

هذا هو الجزء الثاني من سلسلة «هندسة الأنظمة عالية الحمل». يتناول الجزء الأول مسار الطلب من الحافة إلى التطبيق: التوسّع التلقائي أمام ذروات الزيارات: ما الذي يتوسّع فعلًا.

لماذا تنهار المهام الخلفية أولًا

في طلب الويب هناك إنسان ينتظر، فنلاحظ البطء فورًا. أما المهمة في الطابور فلا ينتظرها أحد في تلك اللحظة، فيختبئ البطء حتى يتضخم التراكم. كما يحوّل الطابور الدفعة المفاجئة إلى صفّ طويل: إذا وصلت 1,000 مهمة في دقيقة واحدة واستغرقت كل منها ثانية، فسيحتاج عامل واحد إلى أكثر من 16 دقيقة لإنهائها.

هذه كانت نقطة البداية عندي تمامًا. عامل واحد، يعمل بأمر مثل queue:work --queue=retakes,default، يعالج 1,000 مهمة من الدفعة واحدة تلو الأخرى. والأسوأ أن الأعمال منخفضة الأولوية كانت تقف في الصفّ نفسه مع الإرسالات الحيّة، فقد تتقدّم دفعة من المهام غير المهمة على عمل ينتظره عميل. وفي إعادة تشغيل ذلك الذروة، انتهى بين 20 و26 بالمئة فقط من المهام في وقتها. هذه أرقام إعداد واحد وليست معيارًا عامًّا، لكن شكل الإخفاق شائع جدًّا.

قاعدة البيانات كانت بخير. النسخة المُدارة لم تقترب من حدودها. العائق كان قرار تصميم: عملية واحدة، وصفّ واحد، ولا أولويات.

الخطوة 1: افصل العمال عن الويب

كان التغيير الأول ماديًّا. كان الويب والعمال يتشاركون جهازًا واحدًا بسعة 8 جيجابايت، وحين ينشغل العمال كانوا يسحبون المعالج والذاكرة من PHP-FPM. فيبطؤ الموقع بسبب المهام التي وضعها هو نفسه في الطابور.

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

مخطط قبل وبعد: عقدة مشتركة للويب والعمال بطابور واحد، مقابل عقد منفصلة للويب والعمال مع طابور لكل أولوية
قبل: جهاز واحد وصفّ واحد. بعد: الويب والعمال منفصلان، وطابور لكل نوع من العمل.

الخطوة 2: طابور لكل أولوية ونوع

التغيير الثاني كان التوقف عن خلط كل شيء في صفّ واحد. أنشأت طوابير منفصلة بحسب معنى العمل بالنسبة إلى العميل:

  • Submissions: العمل الحيّ المواجه للعميل، ويجب أن ينتهي خلال ثوانٍ.
  • Notifications: رسائل البريد والرسائل النصية وإشعارات الدفع. مهمة، لكن تأخير دقيقة مقبول.
  • Image processing: ثقيل ويستهلك ذاكرة كبيرة، وليس عاجلًا أبدًا.
  • Reports: تصديرات وملخصات بطيئة يمكنها الانتظار حتى ينتهي الضغط.

الطوابير المنفصلة تتيح لكل طابور عددًا خاصًّا من العمال ومهلة (timeout) خاصة وقواعد إعادة محاولة خاصة. لم يعد بوسع مهمة تقرير تعمل أربع دقائق أن تعطّل تأكيد دفعة. القاعدة العملية: إذا اختلفت أنواع العمل في درجة الاستعجال أو حاجة الذاكرة أو سلوك الفشل، ففي طوابير مختلفة مكانها.

ترتيب الطوابير بحسب الأولوية داخل عامل واحد (الأول يفوز دائمًا) أفضل من صفّ مختلط، لكنها تبقى عملية واحدة. أما العمليات المخصصة لكل طابور فهي التي تُنهي مشكلة تقدّم عمل على آخر نهائيًّا.

الخطوة 3: كم عاملًا تحتاج؟

أضف عمالًا يفرغ الصفّ أسرع، إلى حدّ معين. في اختباراتي، أنجزت نحو 12 عملية عامل 1,000 مهمة في نحو 3 ثوانٍ حين كانت المهام خفيفة، وفي نحو 10 ثوانٍ حين كانت ثقيلة. وبعد نحو 20 عاملًا لم تفد الإضافة: صار العمال ببساطة يصطفّون خلف بعضهم على كتابات قاعدة البيانات.

عمليات العمالما لاحظته (إعداد واحد)
1المهام تعمل واحدة تلو الأخرى؛ 20–26% فقط في الوقت
نحو 121,000 مهمة خفيفة في ~3 ثوانٍ، والثقيلة في ~10 ثوانٍ
أكثر من ~20لا فائدة؛ العمال ينتظرون كتابات قاعدة البيانات

تتماسك الأرقام بحساب بسيط. أزمنة المهام هنا مستنتجة من النتائج، فاعتبرها توضيحية: مهمة خفيفة زمنها نحو 0.03 ثانية تجعل 1,000 مهمة تساوي 30 ثانية من العمل، وتقتسمها 12 عاملًا في نحو 2.5 ثانية. ومهمة ثقيلة زمنها نحو 0.12 ثانية تجعل العمل 120 ثانية، ويُنهيها 12 عاملًا في نحو 10 ثوانٍ. أما عامل واحد فيحتاج إلى 120 ثانية كاملة، وتكون مهلة الإرسالات الحيّة قد فاتت منذ زمن.

هذا هو الدرس وراء ورقة تقدير السعة في الخاتمة: الرقم الصحيح ليس «أكبر عدد ممكن». إنه النقطة التي لا يعود فيها العامل التالي يقصّر الصفّ. اعثر عليها باختبار حمل لا بالتخمين، ولا تُرقِّ قاعدة البيانات قبل أن يُثبت الاختبار أنها مشبعة فعلًا. أتناول طبقة البيانات في الجزء الثالث.

وسّع العمليات لا الأجهزة

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

يفعل Laravel Horizon ذلك عبر إعداد balance. وبحسب وثائق Laravel الرسمية، يقدّم Horizon ثلاث استراتيجيات: simple توزّع المهام الواردة بالتساوي على عمليات العمال، وauto تضبط عدد العمليات لكل طابور بحسب حمله الحالي، وfalse توقف الموازنة. ومع auto، تحدد بضعة خيارات سرعة الاستجابة.

الإعدادما الذي يتحكم فيهالقيمة التي استخدمتها لطابور الإرسال
balanceالاستراتيجية: auto أو simple أو falseauto
minProcessesالعمليات المحفوظة لكل طابور حتى في حالة الخمول1
maxProcessesالحد الأعلى لعدد العمليات الذي يمكن أن يصل إليه Horizon10
balanceMaxShiftكم عملية تُضاف أو تُزال في تعديل واحد3
balanceCooldownالثواني بين تعديل وآخر3

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

رسم توضيحي: عمق الطابور يرتفع أثناء دفعة مفاجئة بينما يزداد عدد عمليات العمال ثم ينخفض
دفعة توضيحية: يتبع العمال عمق الطابور صعودًا خلال ثوانٍ، ويهبطون حين يفرغ الصفّ.

حجّم الذاكرة للحد الأقصى

في التوسع على مستوى العمليات فخّ واحد. يجب أن تتّسع الحاوية للحد الأقصى لا للمتوسط. إذا بلغت ذروة كل مهمة نحو 128 ميجابايت وسمحتَ بـ10 عمليات، فالمجموع نحو 1.3 جيجابايت، ولذلك ضبطت الحاوية على نحو 2 جيجابايت. وإذا حجّمتها لحالة الهدوء، فستنتهي أول دفعة حقيقية بعمليات إنهاء بسبب نفاد الذاكرة، وسينتزع النواة العمال في منتصف المهام.

يستحق الأمر قياس الذاكرة لكل مهمة لا لكل عامل، لأن نوعًا واحدًا من المهام الثقيلة قد يستأثر بالميزانية كلها.

خفّف المهمة الثقيلة قبل شراء العتاد

بلغت ذروة إحدى المهام في إعدادي 288 ميجابايت. كانت تجلب صورها بنفسها عبر HTTP وتقرأ ملفات من القرص المحلي، وتحتفظ بكل ذلك في الذاكرة دفعة واحدة. وبقراءة الملفات تدفقًا (streaming) من تخزين الكائنات انخفضت الذروة إلى نحو 128 ميجابايت. هذا التغيير وحده وفّر علينا فئة كاملة من أحجام الأجهزة.

قبل إضافة ذاكرة أو عقدة أكبر، اسأل ثلاثة أسئلة عن كل مهمة ثقيلة:

  1. هل تحمّل ملفًا كاملًا في الذاكرة بينما يمكنها قراءته تدفقًا؟
  2. هل تجلب عبر الشبكة بيانات كان يمكن أن تصلها ضمن الحمولة أو تُقرأ من مخزن قريب؟
  3. هل يمكن تقسيمها إلى مهام صغيرة كثيرة بدل مهمة كبيرة واحدة؟

والمهام الصغيرة تناسب موازنة Horizon أكثر أيضًا، لأنه يستطيع تحريك السعة بخطوات صغيرة.

قارن بين خيارات التوسع التلقائي

Horizon ليس الطريقة الوحيدة لتوسيع العمال. هكذا تتقابل الخيارات الأربعة الشائعة لطابور على نمط Laravel.

الخيارما الذي يتوسعزمن الاستجابةالتكلفة والجهد
نسخ ثابتة (Compose أو Swarm)لا شيء؛ أنت تحدد عددًا ثابتًايدويبسيط، لكنك تدفع ثمن سعة الذروة طوال الوقت
Kubernetes مع HPAالحاويات (pods) بحسب المعالج أو مقاييس مخصصةدقائقيحتاج عنقودًا وخط مقاييس
Kubernetes مع KEDAالحاويات بحسب طول الطابوردقائقيحتاج عنقودًا؛ يتوسع وفق الإشارة الحقيقية
Horizon auto-balanceالعمليات داخل حاويةثوانٍمجاني، بلا عنقود، ومحدود بجهاز واحد

KEDA مشروع جيد. فبحسب وثائقه، يفحص كل مشغّل (trigger)، مثل طول قائمة Redis، في كل فترة استطلاع (30 ثانية افتراضيًّا)، ثم يتولى Horizontal Pod Autoscaler في Kubernetes التوسع من نسخة واحدة إلى نسخ كثيرة. وإضافة الحاويات تعني أيضًا جدولتها وتشغيلها. وبالنسبة إلى ضغط مجدول يبلغ ذروته في ثوانٍ، يبقى ذلك بطيئًا، والعنقود تكلفة حقيقية على فريق صغير.

قاعدتي: استخدم Horizon auto-balance للاستجابة السريعة، ووسّع الجهاز أو الحد الأدنى مسبقًا قبل حدث تعرف أنه قادم. وفكّر في KEDA حين لا تكفي عقدة واحدة للعمل اليومي.

صمّم المهام بحيث تكون إعادة المحاولة آمنة

مجموعة من العمال السريعين تكشف كل ضعف في طريقة كتابة مهامك. هذه العادات أبقت مهامي آمنة:

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

المجدوِل (scheduler) نسخة واحدة فقط

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

اختر عقدة واحدة ووثّقها وأبقِها خارج أي مجموعة توسع تلقائي. وتحتاج الأوامر الخلفية إلى العناية نفسها التي تحتاجها طلبات الويب. ففي إحدى البيئات المستضافة ذاتيًّا، كان المجدوِل يُقلع الإطار كله لكل أمر صغير، دون OPcache لسطر الأوامر وبحدود ذاكرة ضيقة، فتسبب في مئات عمليات الإنهاء بسبب نفاد الذاكرة. وقد حُلّ ذلك بتشغيل الـrelays داخل عملية طويلة العمر وتفعيل OPcache لسطر الأوامر.

فرّغ الطوابير عند كل نشر

العامل عملية طويلة العمر، فيحتفظ بالشيفرة القديمة في الذاكرة حتى تعيد تشغيله، وإعادة التشغيل المفاجئة تقتل المهمة الجارية. الترتيب الآمن هو التوقف عن أخذ مهام جديدة، ثم ترك الجارية تنتهي، ثم تشغيل العمال الجدد.

  1. نفّذ أمر الإنهاء في Horizon (horizon:terminate) ليُنهي العمال مهمتهم الحالية ثم يخرجوا.
  2. امنح الحاوية مهلة إيقاف سخية تفوق أطول مهمة لديك، حتى لا يقتلها المُنسِّق مبكرًا.
  3. شغّل العمال الجدد على الإصدار الجديد.
  4. شغّل المجدوِل أخيرًا، حتى لا تنطلق أي مهمة مجدولة على نظام نُشر نصفه.

أستعرض تسلسل النشر كاملًا، بما فيه blue/green والرجوع إلى إصدار سابق، في الجزء الخامس.

ورقة تقدير سعة العمال

استخدم هذه القائمة القصيرة قبل ذروتك القادمة:

  1. قِس عدد المهام التي ينتجها الحدث في دقيقة ذروته، لكل طابور.
  2. قِس زمن التشغيل وذروة الذاكرة لكل نوع مهمة على عامل حقيقي.
  3. اقسم عدد المهام على زمن التشغيل لتقدّر العمليات اللازمة لتفريغ الذروة ضمن هدفك.
  4. اضبط الحد الأقصى فوق ذلك قليلًا، واضبط ذاكرة الحاوية على أقصى عدد من العمليات مضروبًا في ذروة ذاكرة المهمة.
  5. أجرِ اختبار حمل بالمزيج الحقيقي وراقب زمن الكتابة في قاعدة البيانات: توقف عن إضافة العمال حين يصبح هو الحد.
  6. تأكد أن المجدوِل يعمل مرة واحدة، وأن المهام idempotent، وأن النشر يفرّغ الطوابير بسلاسة، وحدّد أرقام عمق الطابور وزمن الانتظار التي ستراقبها (انظر الجزء الرابع).

نعود إلى التاسعة صباح يوم الامتحان. الإرسالات الـ1,000 نفسها، لكن لطابور الإرسال الآن عملياته الخاصة ولا يقف أي شيء آخر أمامه. ينتقل Horizon من عملية واحدة إلى عشر في نحو عشر ثوانٍ، وتنتهي الدفعة في الوقت الذي يستغرقه الطالب ليعيد النظر إلى الشاشة، وتخرج التأكيدات قبل أن تُكتب أول رسالة إلى الدعم. أما مهام التقارير والصور فتنتظر، كما ينبغي لها.

الجزء التالي، طبقة البيانات تحت الحمل، يتناول ما يحدث حين يصبح العمال سريعين بما يكفي للضغط على قاعدة البيانات: تجميع الاتصالات، وأدوار Redis، ومخزن منفصل لتضمينات الذكاء الاصطناعي.

أهم النقاط

  • تنهار المهام الخلفية عادةً أولًا في الدفعات المفاجئة، ويظل البطء مخفيًّا حتى يكبر التراكم.
  • انقل العمال بعيدًا عن عقد الويب، وأعطِ كل أولوية أو نوع عمل طابورًا خاصًّا به.
  • وسّع عمليات العمال داخل الجهاز بـHorizon auto-balance للاستجابة في ثوانٍ، وحجّم الذاكرة للحد الأقصى.
  • خفّف المهام الثقيلة قبل شراء العتاد، وتوقف عن إضافة العمال حين تصبح كتابات قاعدة البيانات هي الحد.
  • اجعل المهام idempotent، وأرسلها بعد الـcommit، وشفّر البيانات الشخصية في الحمولات، وشغّل المجدوِل في عقدة واحدة.
  • فرّغ الطوابير عند كل نشر مع مهلة إيقاف سخية.

Anichur Rahaman مهندس برمجيات معماري ومؤسس StoreConsole، يصمّم أنظمة التجارة وERP للشركات النامية، ويهتم خصوصًا بالبنية القائمة على الأحداث وسلامة البيانات وتشغيل الأنظمة على خوادم الشركة نفسها.

About the Author

Anichur Rahaman

Continue Reading