المعمارية المدفوعة بالأحداث
المعمارية المدفوعة بالأحداث
في نظام الطلب/الاستجابة التقليدي، تستدعي الخدمة A الخدمة B مباشرةً: ترسل طلبًا، وتنتظر، ثم تعالج الرد. هذا الترابط بسيط في التفكير، لكنه يخلق شبكةً هشّة — إذا تباطأت B تأخرت A؛ وإذا توقفت B فشلت A؛ وإذا احتجت إلى إضافة خدمة C تتفاعل مع نفس الإجراء، فعليك تعديل كود A كي تستدعي C أيضًا.
تقلب المعمارية المدفوعة بالأحداث (EDA) هذا النموذج رأسًا على عقب. بدلًا من استدعاء الخدمات بعضها البعض، تُعلن كل خدمة عمّا حدث بإرسال حدث — حقيقة غير قابلة للتغيير تُسجّل تغيّرًا في الحالة مع طابع زمني. أي خدمة تهتم بهذا الحدث تشترك فيه وتتفاعل معه باستقلالية. المُرسِل لا يعرف شيئًا عن المستمعين، والمستمعون لا يعرفون شيئًا عن بعضهم.
ما هو الحدث؟
الحدث هو تسجيل لشيء وقع بالفعل. يُصاغ بصيغة الماضي ويحمل كل السياق الذي يحتاجه المستهلك:
OrderPlaced— معرف الطلب، معرف العميل، المنتجات، الإجمالي، الطابع الزمنيPaymentProcessed— معرف الدفع، معرف الطلب، المبلغ، طريقة الدفع، الحالةInventoryReserved— رمز المنتج، الكمية، المستودع، معرف الطلبEmailDelivered— معرف الرسالة، المستلم، القالب، الطابع الزمني
تختلف الأحداث عن الأوامر والاستعلامات. الأمر يقول "افعل هذا" ويتوقع نتيجةً. الاستعلام يقول "أخبرني بهذا." أما الحدث فيقول "هذا حدث" — لا يحمل أي توقع لردٍّ. هذا الفارق هو ما يقود إلى الفصل الرخو الذي تُعرف به EDA.
البنية الأساسية لنظام EDA
ثلاثة أدوار تظهر في كل نظام مدفوع بالأحداث:
- المنتج (الناشر) — يُصدر أحداثًا عند تغيّر شيء في نطاقه. تخدمة الطلبات تُصدر
OrderPlacedبعد حفظ طلب جديد. - وسيط الأحداث — يستقبل الأحداث ويخزّنها ويُوجّهها. Kafka وRabbitMQ وAWS EventBridge وGoogle Pub/Sub كلها تؤدي هذا الدور.
- المستهلك (المشترك) — يستقبل الأحداث ذات الصلة ويتصرف بناءً عليها: خدمة المخزون تحجز البضاعة؛ خدمة البريد ترسل تأكيدًا؛ خدمة التحليلات تُحدّث لوحة البيانات.
الفصل الزمني: الفائدة الجوهرية
في الرسم أعلاه، لا تنتظر خدمة الطلبات انتهاء خدمتَي المخزون والبريد. تُصدر 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
EDA ليست مجانية. إنها تُضيف تعقيدًا حقيقيًا:
- الاتساق التدريجي — بعد إصدار
OrderPlaced، قد لا يُحجز المخزون لمدة 50 ملّي ثانية أخرى. قد يرى المستخدمون لحظيًا حالةً غير متسقة. - التصحيح أصعب — يظهر خطأ ما في الخدمة C، لكن السبب الجذري هو حدث مُشوَّه من الخدمة A، نُشر قبل ثلاث خطوات. التتبع الموزع (معرّفات الارتباط في كل حدث) ضروري.
- لا استجابة فورية — لا يستطيع المنتج مراجعة نتيجة المستهلك داخليًا. إذا احتجت إلى عدد المخزون لعرضه في صفحة الدفع قبل تأكيد الطلب، فالاستعلام التزامني هو الأداة الصحيحة لتلك الخطوة.
- الحمل التشغيلي — تُشغّل الآن مجموعة وسيط، وتُدير إزاحات مجموعات المستهلكين، وتُراقب التأخر، وتتعامل مع طوابير الرسائل الميتة.
مثال واقعي: تدفق طلب التجارة الإلكترونية
حين ينقر العميل على "إتمام الطلب" في موقع تجارة إلكترونية كبير، يبدو التسلسل تقريبًا هكذا:
- تحفظ خدمة الطلبات الطلب وتُصدر
OrderPlacedإلى الوسيط. - تستهلك خدمة الدفع
OrderPlaced، تُجري عملية الدفع، وتُصدرPaymentProcessed. - تستهلك خدمة المخزون
OrderPlacedوPaymentProcessed، تحجز البضاعة، وتُصدرInventoryReserved. - تستهلك خدمة الوفاء
InventoryReserved، وتُنشئ مهمة الانتقاء والتعبئة والشحن. - تستهلك خدمة الإشعارات
PaymentProcessed، وترسل بريدًا إلكترونيًا ورسالة SMS للتأكيد. - تستهلك خدمة التحليلات جميع الأحداث للوحات البيانات في الوقت الفعلي.
تعمل كل خطوة من هذه الخطوات الست بالتوازي فور وصول الحدث ذي الصلة. تعود واجهة برمجة الدفع للمستخدم بعد الخطوة الأولى — والباقي يحدث بشكل غير متزامن في غضون ثوانٍ، مما يوفر خطوط أنابيب سريعة ومرنة وقابلة للتطوير باستقلالية.