المعالجة غير المتزامنة والمراسلة

المعمارية المدفوعة بالأحداث

18 دقيقة الدرس 7 من 10

المعمارية المدفوعة بالأحداث

في نظام الطلب/الاستجابة التقليدي، تستدعي الخدمة A الخدمة B مباشرةً: ترسل طلبًا، وتنتظر، ثم تعالج الرد. هذا الترابط بسيط في التفكير، لكنه يخلق شبكةً هشّة — إذا تباطأت B تأخرت A؛ وإذا توقفت B فشلت A؛ وإذا احتجت إلى إضافة خدمة C تتفاعل مع نفس الإجراء، فعليك تعديل كود A كي تستدعي C أيضًا.

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

ما هو الحدث؟

الحدث هو تسجيل لشيء وقع بالفعل. يُصاغ بصيغة الماضي ويحمل كل السياق الذي يحتاجه المستهلك:

  • OrderPlaced — معرف الطلب، معرف العميل، المنتجات، الإجمالي، الطابع الزمني
  • PaymentProcessed — معرف الدفع، معرف الطلب، المبلغ، طريقة الدفع، الحالة
  • InventoryReserved — رمز المنتج، الكمية، المستودع، معرف الطلب
  • EmailDelivered — معرف الرسالة، المستلم، القالب، الطابع الزمني

تختلف الأحداث عن الأوامر والاستعلامات. الأمر يقول "افعل هذا" ويتوقع نتيجةً. الاستعلام يقول "أخبرني بهذا." أما الحدث فيقول "هذا حدث" — لا يحمل أي توقع لردٍّ. هذا الفارق هو ما يقود إلى الفصل الرخو الذي تُعرف به EDA.

البنية الأساسية لنظام EDA

ثلاثة أدوار تظهر في كل نظام مدفوع بالأحداث:

  • المنتج (الناشر) — يُصدر أحداثًا عند تغيّر شيء في نطاقه. تخدمة الطلبات تُصدر OrderPlaced بعد حفظ طلب جديد.
  • وسيط الأحداث — يستقبل الأحداث ويخزّنها ويُوجّهها. Kafka وRabbitMQ وAWS EventBridge وGoogle Pub/Sub كلها تؤدي هذا الدور.
  • المستهلك (المشترك) — يستقبل الأحداث ذات الصلة ويتصرف بناءً عليها: خدمة المخزون تحجز البضاعة؛ خدمة البريد ترسل تأكيدًا؛ خدمة التحليلات تُحدّث لوحة البيانات.
Event-Driven Architecture — producers, broker, consumers Order Service Producer Payment Service Producer User Service Producer OrderPlaced PaymentProcessed UserRegistered Event Broker Kafka / Pub/Sub durable · ordered · replayable Inventory Service Consumer Email Service Consumer Analytics Service Consumer Fraud Service Consumer (new, zero code change)
ثلاثة منتجين يُصدرون أحداث النطاق؛ الوسيط يُوزّعها على أربعة مستهلكين مستقلين — إضافة مستهلك جديد لا تتطلب أي تغيير في المنتجين.

الفصل الزمني: الفائدة الجوهرية

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

قارن ذلك بسلسلة استدعاءات REST المباشرة: Order → Inventory → Email. تأخّر 500 ملّي ثانية في خدمة البريد ينعكس مباشرةً على زمن استجابة صفحة الدفع للمستخدم. مع EDA، يختفي هذا الترابط تمامًا.

إضافة مستهلكين دون لمس المنتجين

من أقوى خصائص EDA هي قابلية التوسع المفتوح/المغلق. حين تضيف المتطلبات التجارية ميزة كشف الاحتيال — "ضَع علامةً على الطلبات التي تتجاوز 5000 دولار للمراجعة" — في المعمارية التزامنية تُعدّل خدمة الطلبات لتستدعي خدمة الاحتيال الجديدة. في EDA، تنشر خدمة الاحتيال، تشترك في OrderPlaced، وانتهيت. خدمة الطلبات لا تتغير أبدًا.

تصل معمارية Uber في الوقت الفعلي إلى مئات المستهلكين على تيار حدث واحد. إضافة تجربة تحليلية جديدة تستغرق أيامًا من الهندسة لا أسابيع — لأنه لا توجد تغييرات في الخدمات المصدرة.

مصادر الأحداث مقابل المعمارية المدفوعة بالأحداث

كثيرًا ما يختلط هذان المصطلحان:

  • المعمارية المدفوعة بالأحداث تتعلق بـالتواصل — الخدمات تتكلم مع بعضها عبر الأحداث بدلًا من الاستدعاء المباشر.
  • مصادر الأحداث (Event Sourcing) تتعلق بـالتخزين الدائم — الحالة الراهنة لكيان ما تُستخلص بإعادة تشغيل سجل الأحداث بدلًا من تخزين صف قابل للتعديل في قاعدة بيانات.

يمكنك استخدام EDA دون مصادر الأحداث (معظم الأنظمة تفعل ذلك). أما مصادر الأحداث فتستلزم دائمًا منهجًا مدفوعًا بالأحداث. الاثنان متكاملان لا متطابقان.

التصميم: عقد مخطط الحدث

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

  • سجل المخططات — يُطبّق Confluent Schema Registry مخططات Avro/Protobuf؛ الإصدارات غير المتوافقة تُرفض عند النشر.
  • أنواع أحداث مُصنَّفة بالإصدار — يُصدر OrderPlaced.v1 وOrderPlaced.v2 بالتوازي خلال نافذة الانتقال.
  • التغييرات الإضافية فقط — فقط أضِف حقولًا اختيارية؛ لا تحذف أو تعيد تسمية الحقول في تيار حيّ.
EDA vs. direct call chain — coupling comparison Synchronous Call Chain Order Service Inventory Service Email Service Tight coupling — one failure cascades Adding a 4th service = code change in Order Event-Driven Order Service Broker Topic Inventory Email Fraud (new) Loose coupling — each consumer is independent
يسارًا: سلسلة تزامنية تربط كل خدمة بالتالية. يمينًا: EDA حيث يشترك كل مستهلك باستقلالية — إضافة مستهلك جديد لا تستلزم أي تغيير في المنتج.

المقايضات ومتى تستخدم EDA

EDA ليست مجانية. إنها تُضيف تعقيدًا حقيقيًا:

  • الاتساق التدريجي — بعد إصدار OrderPlaced، قد لا يُحجز المخزون لمدة 50 ملّي ثانية أخرى. قد يرى المستخدمون لحظيًا حالةً غير متسقة.
  • التصحيح أصعب — يظهر خطأ ما في الخدمة C، لكن السبب الجذري هو حدث مُشوَّه من الخدمة A، نُشر قبل ثلاث خطوات. التتبع الموزع (معرّفات الارتباط في كل حدث) ضروري.
  • لا استجابة فورية — لا يستطيع المنتج مراجعة نتيجة المستهلك داخليًا. إذا احتجت إلى عدد المخزون لعرضه في صفحة الدفع قبل تأكيد الطلب، فالاستعلام التزامني هو الأداة الصحيحة لتلك الخطوة.
  • الحمل التشغيلي — تُشغّل الآن مجموعة وسيط، وتُدير إزاحات مجموعات المستهلكين، وتُراقب التأخر، وتتعامل مع طوابير الرسائل الميتة.
القاعدة العملية: استخدم EDA حين تحتاج إلى إخطار خدمات متعددة بتغيير وحيث يمكن للإجراء تحمّل تأخير بضع ملّيثوانٍ. استخدم الاستدعاءات التزامنية حين تحتاج إلى إجابة فورية تؤثر على الاستجابة الحالية.
ابدأ بأحداث النطاق عند حدود الخدمات. لا تحتاج إلى تحويل كل استدعاء داخلي للدوال إلى حدث. حدّد التحولات الرئيسية في نطاقك — طلب وُضع، دفع مُؤكَّد، شحن مُرسَل — وأصدر أحداثًا لتلك التحولات. كل شيء آخر يمكن أن يبقى تزامنيًا داخل الخدمة.
فخ الأحداث السمينة: تضمين الحالة الكاملة للكيان في كل حدث (كائن الطلب بأكمله عند كل تحديث) يبدو مريحًا لكنه يخلق كوابيس التوافق مع الإصدارات السابقة ويُضخّم تخزين الوسيط. فضّل الأحداث النحيلة مع سياق كافٍ للتصرف، ودع المستهلكين يجلبون بيانات إضافية من API عند الحاجة.

مثال واقعي: تدفق طلب التجارة الإلكترونية

حين ينقر العميل على "إتمام الطلب" في موقع تجارة إلكترونية كبير، يبدو التسلسل تقريبًا هكذا:

  1. تحفظ خدمة الطلبات الطلب وتُصدر OrderPlaced إلى الوسيط.
  2. تستهلك خدمة الدفع OrderPlaced، تُجري عملية الدفع، وتُصدر PaymentProcessed.
  3. تستهلك خدمة المخزون OrderPlaced وPaymentProcessed، تحجز البضاعة، وتُصدر InventoryReserved.
  4. تستهلك خدمة الوفاء InventoryReserved، وتُنشئ مهمة الانتقاء والتعبئة والشحن.
  5. تستهلك خدمة الإشعارات PaymentProcessed، وترسل بريدًا إلكترونيًا ورسالة SMS للتأكيد.
  6. تستهلك خدمة التحليلات جميع الأحداث للوحات البيانات في الوقت الفعلي.

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