بنية التخزين المؤقت والمراسلة

RabbitMQ وبدائل قوائم الانتظار

18 دقيقة الدرس 8 من 30

RabbitMQ وبدائل قوائم الانتظار

تهيمن Kafka على نقاشات بث الأحداث، لكنك في بيئات الإنتاج الحقيقية ستصطدم بمواقف كثيرة تكون فيها Kafka الاختيار الخاطئ. إدراك متى تلجأ إلى RabbitMQ أو Amazon SQS أو غيرها من وسطاء قوائم الانتظار — وفهم لماذا يُفرّق المهندسون المحترفون بين مفهوم القائمة ومفهوم السجل — هو ما يميز من يصمم أنظمة تدوم عن من يُركّب تعقيداً لا مبرر له.

النموذج الذهني: قائمة مقابل سجل

المقايضة الأهم في هذا المجال هي معمارية لا تشغيلية. قائمة الرسائل (RabbitMQ, SQS, ActiveMQ) تُنمذج توزيع العمل: تبقى الرسالة حتى يُقرّها مستهلك واحد بالضبط، ثم تُحذف فور ذلك. أما السجل الموزع (Kafka, Kinesis, Pulsar) فيُنمذج سجلاً مرتباً ودائماً: يقرأ كل المستهلكين نفس الإزاحات وتبقى البيانات محفوظة بغض النظر عن الإقرارات.

  • استخدم القائمة عندما: تحتاج تنفيذ المهام مرة واحدة بالضبط بواسطة عامل واحد — معالجة الطلبات، إصدار الفواتير، إرسال البريد الإلكتروني، استدعاءات API غير المتزامنة، توزيع المهام على مستهلكين متباينين.
  • استخدم السجل عندما: تحتاج أنظمة مستقلة متعددة إلى نفس الأحداث، أو تحتاج إمكانية الإعادة — قنوات التحليلات، سجلات التدقيق، التقاط تغييرات البيانات (CDC)، قنوات ميزات التعلم الآلي.
في شركات التقنية الكبرى تجد في الغالب كليهما: Kafka للعمود الفقري للأحداث، وRabbitMQ أو SQS لقوائم العمل الخاصة بكل خدمة. الخلط بينهما يُفضي إلى عناقيد Kafka تعالج ملايين رسائل المهام قصيرة الأجل التي لا يحتاجها غير مستهلك واحد — هدر في الاحتفاظ والنسخ المتماثل وسعة الأقسام.

معمارية RabbitMQ في 60 ثانية

تُطبّق RabbitMQ بروتوكول AMQP 0-9-1. يُرسل المنتجون إلى بورصة (exchange)؛ تُوجّه البورصة الرسائل إلى قائمة أو أكثر عبر روابط (bindings). يسحب المستهلكون من القوائم ويُرسلون basic.ack أو basic.nack صراحةً. تُحذف الرسائل المُقرّة من الوسيط. أربعة أنواع للبورصات تغطي معظم أنماط التوجيه:

  • direct — التوجيه بمطابقة دقيقة لمفتاح التوجيه (قوائم العمل، توزيع المهام)
  • topic — مفاتيح توجيه ذات أحرف بديلة (payments.#, *.critical)
  • fanout — بث لجميع القوائم المربوطة (الإشعارات، إبطال التخزين المؤقت)
  • headers — توجيه بناءً على خصائص ترويسة الرسالة (نادر الاستخدام ويُضيف تكلفة)
RabbitMQ Routing Model Producer publishes Exchange topic / direct fanout Queue: orders routing-key: order.* Queue: alerts routing-key: *.critical Consumer A ack / nack Consumer B ack / nack DLX / DLQ rejected / expired
نموذج توجيه RabbitMQ: تُوزّع البورصة على القوائم وفق مفتاح التوجيه؛ الرسائل المرفوضة أو منتهية الصلاحية تنتقل إلى قائمة الرسائل الميتة (DLQ).

تشغيل RabbitMQ في الإنتاج

المسار الأنسب في Kubernetes هو RabbitMQ Cluster Operator. عنقود من ثلاثة عقد بقوائم الأصوات (quorum queues) هو الحد الأدنى للإنتاج — حلّت قوائم الأصوات محل القوائم المعكوسة الكلاسيكية منذ الإصدار 3.8 وتُقدّم نسخاً متماثلاً قائماً على Raft دون فقدان بيانات عند الفشل.

# rabbitmq-cluster.yaml — عنقود ثلاثي بقوائم الأصوات عبر Operator apiVersion: rabbitmq.com/v1beta1 kind: RabbitmqCluster metadata: name: prod-rmq namespace: messaging spec: replicas: 3 image: rabbitmq:3.13-management resources: requests: cpu: "1" memory: 2Gi limits: cpu: "2" memory: 4Gi persistence: storageClassName: gp3 storage: 50Gi rabbitmq: additionalConfig: | vm_memory_high_watermark.relative = 0.6 disk_free_limit.absolute = 5GB default_consumer_prefetch = 10 consumer_timeout = 30000 management.load_definitions = /etc/rabbitmq/definitions.json affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchLabels: app.kubernetes.io/name: prod-rmq topologyKey: kubernetes.io/hostname
الحدّ الأعلى لذاكرة الوسيط هو أكثر أسباب الانهيار شيوعاً. حين يتجاوز الوسيط العتبة يُطبّق RabbitMQ تحكّماً في التدفق على جميع المنتجين — يتوقف عن قبول رسائل جديدة حتى تنخفض الذاكرة. بالإعداد الافتراضي vm_memory_high_watermark.relative = 0.4 على عقدة 4 جيجابايت، تُطلق العتبة عند 1.6 جيجابايت. اضبطها على 0.6 وتأكد من أن المستهلكين يستنزفون القائمة أسرع مما ينتج المنتجون، وإلا ستضغط الضغط على الوسيط بأكمله عند كل ارتفاع في حركة البيانات.

أعلن عن Dead-Letter Exchange (DLX) لكل قائمة تُنجز عملاً. الرسائل التي تتجاوز x-max-delivery-count أو مدة صلاحيتها تُوجَّه إليه تلقائياً، حيث يتولى مستهلك منفصل أو خط مراقبة فحص الإخفاقات دون فقدانها.

# Python (pika) — تعريف قائمة عمل بـ DLX وقوائم الأصوات import pika params = pika.URLParameters('amqps://user:pass@prod-rmq.messaging.svc:5671') conn = pika.BlockingConnection(params) ch = conn.channel() # تعريف DLX وقائمة الرسائل الميتة أولاً ch.exchange_declare('dlx', exchange_type='direct', durable=True) ch.queue_declare('orders.dead', durable=True) ch.queue_bind('orders.dead', 'dlx', routing_key='orders') # تعريف قائمة العمل الرئيسية مع ربط DLX ch.queue_declare( 'orders', durable=True, arguments={ 'x-queue-type': 'quorum', 'x-dead-letter-exchange': 'dlx', 'x-dead-letter-routing-key': 'orders', 'x-delivery-limit': 5, 'x-message-ttl': 300_000, } ) ch.basic_qos(prefetch_count=10) def on_message(ch, method, props, body): try: process(body) ch.basic_ack(method.delivery_tag) except RetryableError: ch.basic_nack(method.delivery_tag, requeue=True) except Exception: ch.basic_nack(method.delivery_tag, requeue=False) ch.basic_consume('orders', on_message) ch.start_consuming()

Amazon SQS — متى تفوز الخدمة المُدارة

تُلغي SQS كل عمليات الوسيط. لا عناقيد تُحجم، ولا نسخ متماثل بالأصوات تضبطه، ولا عتبات ذاكرة تراقبها. المقايضة هي مجموعة ميزات محدودة ونموذج تسليم "مرة واحدة على الأقل" مع حجم رسالة أقصاه 256 كيلوبايت. نوعان من القوائم يغطيان غالبية الحالات:

  • القائمة المعيارية: معدل نقل شبه لا محدود، تسليم مرة واحدة على الأقل، ترتيب تقديري. مناسبة لمعظم أعباء المهام غير المتزامنة التي صُمّمت أصلاً مع مراعاة idempotency.
  • قائمة FIFO: معالجة مرة واحدة بالضبط داخل مجموعة الرسائل، 3000 رسالة/ثانية مع التجميع (300 دونه). استخدمها حين يهم الترتيب داخل مجموعة منطقية واحدة — مثلاً انتقالات حالة متسلسلة لمعرّف طلب واحد.
في البنى السحابية الأصيلة على AWS، يُعدّ نمط SQS + Lambda هو الافتراضي لمعالجة المهام الموزعة. تتوسع Lambda تلقائياً بحسب عدد الرسائل قيد الرحلة، وتعمل SQS كمخزن مؤقت يفصل حدود التزامن في Lambda عن الانفجارات العلوية. اضبط RedrivePolicy على قائمة رسائل ميتة بعد 3-5 محاولات، وراقب ApproximateNumberOfMessagesNotVisible (قيد الرحلة) وApproximateAgeOfOldestMessage بوصفهما مؤشري أداء رئيسيين لـ SQS.

إطار اتخاذ القرار: أي الأدوات تختار؟

يفكّر المهندسون المتمرسون في هذه المحاور في آنٍ واحد عند تقييم تقنية وسيط لحمل عمل جديد:

  • دلالات التسليم: هل تحتاج مرة واحدة بحدٍّ أقصى (fire-and-forget)، أم مرة واحدة على الأقل (SQS standard, Kafka, RabbitMQ)، أم مرة واحدة فعلياً (SQS FIFO، منتج Kafka idempotent مع المعاملات)؟
  • نموذج المستهلك: المستهلكون المتنافسون (عامل واحد من مجموعة يعالج كل رسالة) يُفضّل القوائم. مجموعات مستهلكين مستقلة تحتاج نفس الأحداث تُفضّل السجلات.
  • الاحتفاظ بالرسائل: تحذف القوائم الرسائل المُقرّة؛ تحتفظ السجلات بالبيانات ساعات إلى الأبد. إن احتاج فريق التحليلات إلى أحداث الأمس التي استهلكتها خدمة الدفع بالفعل، فأنت تحتاج سجلاً.
  • السطح التشغيلي: تتطلب RabbitMQ إدارة عنقود حقيقية (نسخ بالأصوات، ضبط الذاكرة، نوافذ الترقية، تناوب الشهادات). تُلغي SQS/SNS كل ذلك مقابل الارتباط بـ AWS. تتطلب Kafka أكبر استثمار تشغيلي لكنها تُقدّم الإنتاجية الأعلى وإمكانية الإعادة.
  • الإنتاجية مقابل التعقيد: تتعامل SQS مع ملايين الرسائل في الثانية لكل قائمة دون أي عمليات. تصل RabbitMQ إلى ذروتها عند 50-100 ألف رسالة/ثانية لكل عنقود قبل أن تحتاج التجزئة. تتعامل Kafka مع الملايين في الثانية لكنها تُضيف إدارة الأقسام وإعادة التوازن لمجموعات المستهلكين وتعقيد سجل المخطط.
Pulsar والبدائل الأخرى: يحاول Apache Pulsar توحيد نموذجَي القائمة والسجل مع تخزين متدرج (BookKeeper + تخزين الكائنات). إنه جاهز للإنتاج ويُستخدم على نطاق واسع في Yahoo وTencent وSplunk، لكن تعقيده التشغيلي يتجاوز Kafka. اعتمده فقط إذا كان حمل عملك يحتاج فعلاً كلا النموذجين في وسيط واحد وكان لديك فريق SRE قادر على تشغيله. لمعظم المؤسسات، تشغيل Kafka للسجلات وRabbitMQ أو SQS لقوائم المهام أبسط وأكثر استواءً.

إشارات المراقبة التي تهم فعلاً

لـ RabbitMQ، صدّر المقاييس عبر إضافة Prometheus (rabbitmq_prometheus) وأنشئ تنبيهات على: rabbitmq_queue_messages_ready تنمو دون توقف (تأخر المستهلك)، وrabbitmq_queue_messages_unacked ترتفع (المستهلكون متوقفون)، وrabbitmq_node_mem_used تقترب من العتبة. لـ SQS، راقب ApproximateAgeOfOldestMessage — القائمة التي تستنزف ببطء هي في الغالب مشكلة مستهلك، لا مشكلة بنية تحتية.