الضغط العكسي وقوائم الرسائل الميتة
الضغط العكسي وقوائم الرسائل الميتة
حتى أفضل خطوط الأنابيب غير المتزامنة تواجه حقيقتين تشغيليتين صعبتين: المستهلكون الذين لا يستطيعون مواكبة معدل الرسائل الواردة، والرسائل التي لا يمكن معالجتها بنجاح مهما تكررت المحاولات. الضغط العكسي هو مجموعة الأساليب التي تعالج المشكلة الأولى؛ أما قوائم الرسائل الميتة (DLQ) فهي شبكة الأمان للمشكلة الثانية. معاً يحوّلان خطوط الأنابيب الهشة إلى أنظمة مرنة وقابلة للرصد.
ما هو الضغط العكسي؟
الضغط العكسي هو إشارة تنتقل للأعلى — من المستهلك البطيء نحو المنتج — لإبطاء أو إيقاف تدفق الرسائل الجديدة حتى يتمكن المستهلك من اللحاق بالركب. يأتي المصطلح من ديناميكيات السوائل: الماء المتدفق عبر أنبوب يولّد ضغطاً عكسياً عند مواجهة مقاومة في المصب.
في أنظمة المراسلة تتجلى المشكلة بشكل مختلف تبعاً لنموذج الإرسال المستخدم — الدفع (Push) أو السحب (Pull):
- نموذج الدفع (مثل RabbitMQ بالتسليم التلقائي): يواصل الوسيط الدفع بغض النظر عن انشغال المستهلك. بدون ضغط عكسي تمتلئ ذاكرة المستهلك المؤقتة وتتصاعد استهلاك الذاكرة حتى يتعطل العملية أو تبدأ في فقدان الرسائل دون إشعار.
- نموذج السحب (مثل Kafka وSQS): يطلب المستهلك الدفعة التالية فقط حين يكون جاهزاً. الضغط العكسي ضمني — المستهلك ببساطة لا يصدر
poll()التالي حتى ينتهي من معالجة الدفعة الحالية. هذا أحد الأسباب التي تجعل Kafka أكثر تكيفاً مع الضغط العكسي مقارنةً بوسطاء الدفع.
أعراض غياب الضغط العكسي
حين يفتقر النظام إلى آليات الضغط العكسي، يكون الفشل تدريجياً وكارثياً في الغالب:
- يتزايد تأخر المستهلك. مجموعة Kafka التي تعالج 1000 رسالة/ثانية بينما تستقبل 2000/ثانية تراكم تأخراً يبلغ ~86 مليون رسالة يومياً.
- يتراكم ضغط الذاكرة. كل رسالة غير معالجة تشغل ذاكرة في عملية المستهلك أو في مخزن الوسيط المؤقت.
- ينهار المستهلك. إعادة التشغيل لا تفيد — التراكم لا يزال موجوداً والمستهلك المُعاد تشغيله يواجه التحميل الزائد نفسه فوراً.
- عواصف إعادة المحاولة تزيد الحمل سوءاً. إذا كانت الرسائل لها وقت ظهور قصير (SQS) أو أُرسلت إشعارات رفض (RabbitMQ)، فإنها تعود إلى القائمة وتتنافس مع الوافدين الجدد.
استراتيجيات الضغط العكسي
لا يوجد تطبيق موحد للضغط العكسي — يعتمد النهج الصحيح على الوسيط ومستوى الخدمة المطلوب:
- حدود الجلب المسبق / QoS: في RabbitMQ، اضبط
basic.qos(prefetch_count=N)كي يُسلّم الوسيط على الأكثر N رسالة غير مُقرّة لقناة مستهلك واحدة. يوقف الوسيط التسليم حتى تصل الإقرارات. يُعدّ الجلب المسبق بين 10–50 نقطة بداية شائعة للمهام المرتبطة بالمعالج؛ و100–500 للمهام المرتبطة بالإدخال/الإخراج. - التحكم بحجم الدفعة (نموذج السحب): يستدعي مستهلكو Kafka
poll(max.poll.records=500). تقليل هذا الرقم يُقلص العمل لكل استدعاء ويمنح المستهلك وقتاً للتعافي. اقرنه بـmax.poll.interval.msحتى لا يعدّه الوسيط معطلاً أثناء المعالجة الطويلة. - محدود الرموز / معدّل الحد في المنتج: إذا كنت تتحكم في المنتج، فأضف أداة تفقد عمق القائمة (عبر مقاييس الوسيط أو عداد في Redis) وتقيّد المنتج تلقائياً عند تجاوز عتبة معينة.
- تدفقات تفاعلية / قناة خلفية غير متزامنة: في الكود، تنشر أطر مثل Project Reactor وRxJava وتدفقات Node.js إشارات الطلب. عامل مصب يعالج 100 عنصر/ثانية يُعلن هذا الطلب؛ المصدر الأعلى لا ينتج أكثر من 100 عنصر/ثانية.
- التوسع الأفقي التلقائي مع تنبيهات التأخر: حين يتجاوز تأخر مستهلك Kafka عتبةً ما (مثلاً: > 100,000 رسالة)، أطلق حدث توسع تلقائي لإضافة pods مستهلكة. AWS SQS + Lambda يفعل هذا تلقائياً؛ Kubernetes KEDA يوفر سلوكاً مماثلاً لأي قائمة.
ما هي قائمة الرسائل الميتة؟
قائمة الرسائل الميتة (DLQ) هي قائمة مخصصة تستقبل الرسائل التي تعذّر تسليمها أو معالجتها بنجاح بعد عدد محدد من المحاولات. بدلاً من فقدان الرسالة أو تعطيل القائمة الرئيسية إلى الأبد، يوجّهها الوسيط (أو منطق المستهلك) إلى DLQ كي يمكن فحصها وإصلاحها وإعادة تشغيلها لاحقاً.
تنتهي الرسائل في DLQ لأسباب شائعة عدة:
- تجاوز الحد الأقصى للتسليم: تنقل SQS الرسالة إلى DLQ بعد
maxReceiveCount(مثلاً: 5) محاولات معالجة فاشلة. يفعل RabbitMQ الشيء ذاته عند تجاوز عدادx-deathحد السياسة. - انتهاء صلاحية TTL للرسالة: بقيت رسالة دون معالجة أطول من مدة بقائها. يُشير هذا عادةً إلى انقطاع مستهلك أطول مما كان متوقعاً.
- خطأ في التسلسل / المخطط: لا يستطيع المستهلك تحليل نص الرسالة بسبب تغيير مُكسِر في المخطط — السيناريو الكلاسيكي لرسالة التسمم.
- استثناء منطق الأعمال: يطرح كود المستهلك استثناءً غير معالج (مثلاً: سجل قاعدة البيانات المشار إليه لم يعد موجوداً).
تصميم خط أنابيب DLQ
إعداد DLQ في بيئة الإنتاج يتجاوز مجرد "قائمة منفصلة". تحتاج إلى:
- التنبيهات: أي عمق غير صفري في DLQ يجب أن يُنبّه المهندس المناوب فوراً. رسالة في DLQ تعني بيانات حقيقية لم تُعالج — عاملها كخطأ في خدمتك وليس فضولاً في الخلفية.
- بيانات وصفية للرسالة: احتفظ بسبب الفشل والقائمة الأصلية وطابع زمني لكل محاولة والمستهلك الذي فشل. AWS SQS تُضمّن هذا تلقائياً في سمات الرسالة. للوسطاء المخصصة، لفّ الحمولة الأصلية في غلاف بهذه الحقول قبل التوجيه إلى DLQ.
- أدوات إعادة التسليم: اكتب أو تبنّ أدوات لنقل الرسائل من DLQ إلى القائمة الرئيسية بعد إصلاح السبب الجذري. بدون هذا، DLQ الخاص بك هو ثقب أسود.
- مدة احتفاظ DLQ: اضبط فترة احتفاظ كافية لدورة الاستجابة للحوادث (7–14 يوماً شائع). بعد ذلك، أرشف إلى S3/تخزين الكائنات قبل الحذف حتى يبقى لديك مسار تدقيق.
رسائل التسمم وكيفية التعامل معها
رسالة التسمم هي رسالة تتسبب باستمرار في إخفاق المستهلك بغض النظر عن عدد المحاولات — الرسالة نفسها معطوبة. بدون DLQ، ستدور رسالة التسمم باستمرار في القائمة مستهلكةً المحاولات، وتعيق الرسائل السليمة (خاصةً في قوائم FIFO)، وتُعطل المستهلكين أو تشغلهم.
أمثلة ملموسة على رسائل التسمم:
- حمولة JSON يكون فيها حقل مطلوب
nullوكود المستهلك يستدعي.toString()عليه بدون فحص القيمة الفارغة. - رسالة Protobuf ثنائية مشفّرة بنسخة مخطط أحدث لا يفهمها المستهلك.
- حدث يشير إلى كيان (مثلاً: معرف الطلب 9999) حُذف بالكامل من قاعدة البيانات بين النشر والاستهلاك.
يعزل DLQ هذه الرسائل الإشكالية حتى تتمكن القائمة الرئيسية من معالجة الرسائل السليمة بينما تحقق في المشكلة. بعد إصلاح الخلل — سواء في كود المستهلك أو بتصحيح البيانات — يمكنك إعادة تشغيل رسائل DLQ عبر المستهلك المُصلح.
سياسة إعادة التوجيه في SQS — مثال ملموس
تجعل AWS SQS إعداد DLQ صريحاً عبر سياسة إعادة التوجيه (redrive policy):
حين يستقبل مستهلك رسالة ولا يحذفها ضمن وقت الظهور، يجعلها SQS مرئية مرة أخرى لمستهلك آخر. بعد maxReceiveCount استقبال دون حذف ناجح، تنقلها SQS تلقائياً إلى DLQ. مقاييس CloudWatch ApproximateNumberOfMessagesNotVisible + ApproximateNumberOfMessages على DLQ هي مُشغّلات التنبيه التي تُعدّها في الخطوة الأولى.
Kafka وموضوعات الرسائل الميتة
لا يمتلك Kafka مفهوم DLQ مدمجاً لأن المستهلك يتحكم في إزاحته الخاصة. الرسائل الفاشلة تتطلب نمطاً صريحاً من تطبيق المستهلك:
- يمسك المستهلك باستثناء المعالجة.
- يُنتج المستهلك السجل الفاشل إلى موضوع مخصص
orders.process.DLT(dead-letter topic)، مُغنّى برؤوس بيانات الفشل. - يُقدّم المستهلك إزاحته متجاوزاً السجل الفاشل حتى لا تُحجب مجموعة المستهلكين الرئيسية.
- مستهلك DLQ منفصل يراقب موضوع DLT ويُنبّه أو يؤرشف أو يُعيد تشغيل السجلات.
تُؤتمت Spring Kafka عبر DeadLetterPublishingRecoverer مع Confluent Schema Registry الخطوات 1–3 بأسطر قليلة من الإعداد.
الضغط العكسي + DLQ معاً
في خط أنابيب مُصمَّم جيداً، يُكمّل الضغط العكسي وDLQ بعضهما:
- الضغط العكسي يعالج الحمل الزائد المؤقت — يُبطئ النظام بأناقة ويمنح المستهلك وقتاً للحاق بالركب.
- DLQs تعالج الإخفاقات الدائمة — تعزل الرسائل المعطوبة حتى لا تستهلك موارد النظام إلى الأبد.
- بدون الضغط العكسي، يفيض مستهلكك بالرسائل وينهار، والانهيار يحوّل الرسائل السليمة إلى مدخلات DLQ لأنها لم تُقرَّ أبداً.
- بدون DLQs، تدور رسائل التسمم للأبد مستهلكةً المحاولات وشاغلةً فتحات الجلب المسبق ومُطبّقةً ضغطاً عكسياً غير مرغوب على الرسائل الشرعية.
قاعدة عملية: اضبط عتبة تنبيه DLQ على صفر واضبط عدد الجلب المسبق على قيمة يستطيع مستهلكك معالجتها بسهولة خلال فترة وقت ظهور واحدة. هذان القيدان وحدهما يُقضيان على أكثر كوارث قوائم الإنتاج شيوعاً.