خدماتنا كانت كخيوط العنكبوت: كيف حررتنا ‘المعمارية الموجهة بالأحداث’ (EDA) من جحيم الفشل المتتالي؟

يا أهلاً وسهلاً بالجميع، معكم أبو عمر.

خليني أحكيلكم قصة صارت معي قبل كم سنة، قصة “يوم الجمعة الأسود” اللي ما بنساه. كنا في الشركة شغالين على منصة تجارة إلكترونية كبيرة، وكنا متحمسين جداً لموسم التخفيضات. الأنظمة جاهزة، السيرفرات “مشدشدة” على الآخر، والفريق كله على أهبة الاستعداد.

بدأ اليوم، وبدأت الطلبات تنهال علينا زي المطر. الأرقام في لوحة المراقبة (Dashboard) بترتفع بشكل جنوني، والفرحة مش سايعانا. وفجأة، وبدون سابق إنذار… كل شي توقف. لوحة المراقبة صارت حمرا بالكامل، ورسائل الخطأ بدأت توصلنا على Slack كأنها رشاش آلي. “Order failed”, “Cannot process request”, “Service unavailable”.

شو اللي صار؟ بعد ساعات من التوتر والضغط وتحليل السجلات (Logs)، اكتشفنا المصيبة. خدمة الدفع الخارجية (Third-party payment gateway) اللي بنتعامل معها صار عليها ضغط عالي وتوقفت عن الاستجابة. لكن المشكلة ما كانت هون، المشكلة كانت في تصميمنا إحنا. كان نظامنا عبارة عن سلسلة متصلة: لما يجي طلب جديد، خدمة الطلبات (Orders Service) بتستدعي مباشرة خدمة المخزون (Inventory Service) عشان تتأكد من توفر المنتج، وبعدها بتستدعي خدمة الدفع (Payment Service) عشان تتمم العملية، وبعدها بتستدعي خدمة الإشعارات (Notifications Service) عشان تبعت إيميل للزبون. كله ورا بعضه، زي قطار البضائع.

فلما “عطّلت” خدمة الدفع، صارت خدمة الطلبات تنتظر الرد منها بدون فايدة، وهذا الأشي سبب تراكم للطلبات المعلقة، واستهلك كل موارد السيرفر، وبالنهاية… انهار كل شي. فشل خدمة واحدة تسبب في “فشل متتالي” أو ما يعرف بالـ Cascading Failure. وقتها حسيت حالي محبوس في ورطة كبيرة، وكل خدماتنا مقيدة ببعضها بسلاسل من حديد. كان لازم نلاقي حل جذري، وهون بدأت رحلتنا مع المعمارية الموجهة بالأحداث.

ما هي المعمارية الموجهة بالأحداث (EDA) على بلاطة؟

ببساطة شديدة، بدل ما الخدمات “تتصل” ببعضها البعض بشكل مباشر وتطلب منها تسوي شغل، في معمارية EDA الخدمات بتصير “تعلن” عن الأحداث اللي بتصير فيها، والخدمات الثانية المهتمة بهذا الحدث “بتسمع” وبتتصرف بناءً عليه. خلينا نبسطها أكتر.

تخيل الفرق بين أمرين:

  1. النمط التقليدي (Request/Response): أنا (خدمة الطلبات) بتصل فيك (يا خدمة الإشعارات) وبقلك: “يا خوي، ابعت هالإيميل للزبون الفلاني هلأ”. أنا رح أظل ماسك الخط ومستني منك تأكيد إنك بعت الإيميل. لو خطك مشغول أو عندك مشكلة، أنا رح أتعطل معك.
  2. نمط الأحداث (Event-Driven): أنا (خدمة الطلبات) بطلع على ساحة عامة وبصرخ بأعلى صوتي: “يا جماعة الخير، تم إنشاء طلب جديد برقم 123!”. أنا هيك خلصت مهمتي ومشيت. إنت (يا خدمة الإشعارات) كنت واقف بالساحة وسمعتني، فأخذت رقم الطلب ورحت تبعت الإيميل. خدمة المخزون كمان كانت بتسمع، فأخذت المعلومة وراحت خصمت المنتج من المستودع. لو خدمة الإشعارات كانت “نايمة” أو مش موجودة، مش مشكلتي، أنا أعلنت عن الحدث والباقي على المهتمين.

هاي “الساحة العامة” في عالم البرمجة بنسميها ناقل الأحداث (Event Bus) أو وسيط الرسائل (Message Broker). وهو القلب النابض لهي المعمارية.

المكونات الأساسية لمعمارية EDA

عشان نفهم الموضوع تقنياً أكثر، معمارية EDA بتتكون من 3 أجزاء رئيسية:

  • منتج الحدث (Event Producer): هو أي خدمة أو جزء من النظام يقوم بإنشاء “حدث” ونشره. في مثالنا، “خدمة الطلبات” هي منتج لحدث OrderCreated.
  • مستهلك الحدث (Event Consumer): هو أي خدمة “تستمع” لنوع معين من الأحداث وتقوم بمعالجته. في مثالنا، “خدمة الإشعارات” و “خدمة المخزون” هم مستهلكون لحدث OrderCreated.
  • ناقل الأحداث (Event Broker/Router): هو الوسيط اللي بستقبل الأحداث من المنتجين وبوصلها للمستهلكين المهتمين. من أشهر الأمثلة عليه: RabbitMQ, Apache Kafka, AWS SQS, Google Pub/Sub.

كيف أنقذتنا EDA من “جحيم الجمعة الأسود”؟

بعد الكارثة اللي صارت، قررنا نعيد التفكير في تصميمنا. قعدنا ورسمنا المخطط الجديد باستخدام EDA. شوفوا كيف صار تدفق عملية الطلب الجديد:

  1. الزبون يضغط على “إتمام الطلب”.
  2. “خدمة الطلبات” تستقبل الطلب، تتحقق منه بشكل مبدئي، تحفظه في قاعدة البيانات بحالة “قيد المعالجة”، ثم تقوم فوراً بنشر حدث اسمه OrderCreated على ناقل الأحداث (استخدمنا وقتها RabbitMQ).
  3. بمجرد نشر الحدث، “خدمة الطلبات” بترجع استجابة سريعة للزبون “شكراً لك، طلبك قيد المعالجة”. لاحظ السرعة! ما استنينا أي خدمة ثانية.
  4. الآن، هناك ثلاث خدمات “مشتركة” ومستعدة للاستماع لحدث OrderCreated:
    • خدمة الدفع (Payment Service): تستقبل الحدث، وتحاول معالجة الدفع. إذا نجحت، تنشر حدث جديد اسمه PaymentSuccessful. إذا فشلت، تنشر حدث PaymentFailed.
    • خدمة المخزون (Inventory Service): تستقبل الحدث، وتقوم بخصم المنتج من المخزون.
    • خدمة الإشعارات (Notifications Service): تستقبل الحدث، وتقوم بإرسال إيميل “تم استلام طلبك” للزبون.

طيب، شو استفدنا من هالشغلانة كلها؟

  • فك الاقتران (Decoupling): صارت خدمة الطلبات ما بتعرف أي شي عن خدمة الدفع أو المخزون. هي فقط تعلن عن حدث. لو أضفنا خدمة جديدة بكرة (مثلاً خدمة تحليل بيانات)، كل اللي علينا نعمله هو نخليها “تستمع” لنفس الحدث بدون ما نلمس خدمة الطلبات أبداً.
  • الصمود والمرونة (Resilience): لو فرضنا إن خدمة الإشعارات توقفت عن العمل (زي ما بصير دايماً 😂)، شو بصير؟ ولا شي! ناقل الأحداث بحفظ الحدث عنده، ولما ترجع خدمة الإشعارات تشتغل، بتلاقي الحدث بستناها وبتقوم بمعالجته. النظام ككل بضل شغال والزبون ما بحس بأي مشكلة. هاد هو الجمال الحقيقي!
  • قابلية التوسع (Scalability): لو صار عنا ضغط كبير على معالجة المدفوعات، بنقدر ببساطة نشغل نسخ إضافية من “خدمة الدفع” فقط، وكلهم بصيروا يسحبوا ويعالجوا الأحداث من الطابور (Queue) بالتوازي.

مثال كود بسيط (Pseudocode)

عشان الصورة تكون أوضح، خلينا نشوف كيف ممكن يكون شكل الكود.

نصيحة من أبو عمر: الأمثلة التالية هي للتوضيح فقط. في الواقع، رح تستخدم مكتبات متخصصة للتعامل مع Kafka أو RabbitMQ بتسهل عليك هاي العمليات.

كود منتج الحدث (في خدمة الطلبات)


// Using a hypothetical event broker library in JavaScript

// This function is called when a new order is submitted
async function createOrder(orderData) {
  // 1. Save order to our own database with "PENDING" status
  const newOrder = await db.orders.save(orderData);

  // 2. Create the event payload
  const event = {
    eventName: "OrderCreated",
    payload: {
      orderId: newOrder.id,
      userId: newOrder.userId,
      items: newOrder.items,
      timestamp: new Date().toISOString()
    }
  };

  // 3. Publish the event to the event broker
  await eventBroker.publish("orders_topic", event);

  // 4. Immediately return a response to the user
  return { success: true, message: "Your order is being processed." };
}

كود مستهلك الحدث (في خدمة الإشعارات)


// Using a hypothetical event broker library in JavaScript

// Subscribe to the "orders_topic" and listen for "OrderCreated" events
eventBroker.subscribe("orders_topic", async (event) => {
  if (event.eventName === "OrderCreated") {
    // We got an order! Let's process it.
    const { userId, orderId } = event.payload;

    try {
      const user = await db.users.find(userId);
      await emailService.send(
        user.email,
        "Order Received!",
        `Hi ${user.name}, we have received your order #${orderId}.`
      );
      console.log(`Confirmation email sent for order ${orderId}`);
    } catch (error) {
      // If sending fails, the broker might be configured to retry
      console.error(`Failed to send email for order ${orderId}:`, error);
      // We might need to publish another event like "NotificationFailed"
    }
  }
});

لاحظ كيف الكودين منفصلين تماماً. خدمة الإشعارات ما بتعرف مين اللي نشر الحدث، ولا بهما. هي فقط مهتمة بحدث OrderCreated.

نصائح عملية من خبرتي

الانتقال لـ EDA مش وردي بالكامل، وفي كم شغلة لازم تنتبه إلها:

  • ابدأ صغيراً: ما في داعي تحول كل نظامك مرة واحدة. اختار جزء معين من النظام بعاني من مشاكل الاعتمادية (زي عملية تسجيل مستخدم جديد أو معالجة طلب) وحوله كبداية.
  • تصميم الأحداث هو الملك: قبل ما تكتب أي كود، اقعد مع فريقك وصمموا شكل الأحداث (Event Schema). شو اسم الحدث؟ شو المعلومات اللي لازم يحتويها؟ اتفقوا على صيغة موحدة (مثلاً استخدام JSON Schema) عشان ما تتغلبوا بعدين.
  • التعامل مع الفشل مهم: شو بصير لو “مستهلك الحدث” فشل في معالجة الحدث؟ لازم يكون عندك استراتيجية. هل رح يعيد المحاولة (Retry)؟ كم مرة؟ هل رح ينقل الحدث لطابور خاص بالأخطاء (Dead-letter Queue) عشان المطورين يحللوا المشكلة يدوياً؟
  • المراقبة والتتبع (Monitoring & Tracing): تتبع تدفق طلب واحد بصير أصعب في EDA لأنه بمر عبر خدمات كثيرة بشكل غير متزامن. لازم تستخدم أدوات مثل OpenTelemetry أو Jaeger عشان تقدر تشوف رحلة الحدث كاملة من المنتج للمستهلك.
  • فكر في الـ Idempotency: هاي كلمة مهمة جداً. معناها إنك لو شغّلت نفس العملية بنفس المدخلات أكتر من مرة، النتيجة النهائية لازم تكون نفسها. في عالم الأحداث، ممكن المستهلك يستقبل نفس الحدث مرتين (بسبب مشكلة في الشبكة مثلاً). لازم يكون الكود تبعك قادر يتعامل مع هاي الحالة بدون ما يسبب مشاكل (مثلاً، ما يخصم نفس المنتج مرتين من المخزون).

الخلاصة: من سلاسل إلى شبكة مرنة 🚀

الانتقال إلى المعمارية الموجهة بالأحداث كان نقلة نوعية في طريقة تفكيرنا وتصميمنا للأنظمة. حولنا نظامنا من سلسلة هشة ممكن تنقطع من أي حلقة ضعيفة، إلى شبكة مرنة وقوية من الخدمات المستقلة. صحيح إنها بتتطلب تخطيط وأدوات مختلفة، لكن النتيجة كانت نظام قادر على الصمود في وجه الأخطاء، وقابل للتوسع بسهولة، والأهم من كل هاد… فريق تطوير مرتاح البال أكتر بكتير.

نصيحتي الأخيرة إلك: فكّر في الأحداث، لا في الأوامر. لا تفكر بس في “مين بستدعي مين”، بل فكر في “شو الأحداث المهمة اللي بتصير في نظامي، ومين بهمه يعرف عنها؟”. هذا التغيير البسيط في طريقة التفكير هو مفتاح بناء أنظمة قوية ومستدامة للمستقبل.

أبو عمر

سجل دخولك لعمل نقاش تفاعلي

كافة المحادثات خاصة ولا يتم عرضها على الموقع نهائياً

آراء من النقاشات

لا توجد آراء منشورة بعد. كن أول من يشارك رأيه!

آخر المدونات

تجربة المستخدم والابداع البصري

كان كل مصمم يغني على ليلاه: كيف أنقذنا ‘نظام التصميم’ (Design System) من جحيم الفوضى البصرية؟

أشارككم حكايتي من قلب المعركة مع الفوضى البصرية في أحد المشاريع الكبرى، وكيف كان "نظام التصميم" (Design System) هو طوق النجاة الذي أعاد التناغم والاتساق....

27 أبريل، 2026 قراءة المزيد
البودكاست