نظامك ليس مونوليث، لكنه يتصرف كواحد: تفكيك التبعيات الخفية بالمعمارية الموجهة بالأحداث (EDA)

يا جماعة الخير، السلام عليكم ورحمة الله.

خليني أحكيلكم قصة صارت معي قبل كم سنة، قصة فيها قهوة باردة، وعيون حمرا من السهر، ودرس تعلمته بالطريقة الصعبة. كنا بنشتغل على نظام كبير، منصة تعليمية ضخمة، وكنا “مودرن” و”عصريين”، وبنبني كل شي بنظام المايكروسيرفس (الخدمات المصغرة). كان عنا خدمة للمستخدمين (Auth Service)، وخدمة للمساقات (Courses Service)، وخدمة للدفع والاشتراكات (Billing Service)، وغيرهم كثير. كنا مبسوطين على حالنا، وفاكرين إنه إحنا هيك فككنا النظام ومورنا تمام.

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

بعد ساعة من البحث والتحليل والتوتر اللي وصل لركبنا، اكتشفنا المصيبة. خدمة الفوترة (Billing Service) الصغيرة “المسكينة” اللي ما حدا معبرها، كان فيها bug صغير ووقعت. لكن ليش كل النظام وقع معها؟ اكتشفنا إنه خدمة المصادقة (Auth Service) عند كل عملية تسجيل دخول، كانت بتتصل بشكل مباشر ومتزامن (Synchronous API Call) بخدمة الفوترة عشان تتأكد إذا اشتراك المستخدم فعال ولا لأ. ولما خدمة الفوترة وقعت، خدمة المصادقة صارت تستنى الرد وما يجيها، فبتعلق وبتفشل، وبالتالي فشلت كل عمليات تسجيل الدخول. وهكذا، خدمة صغيرة غير أساسية قدرت توقع النظام كله على رأسه. وقتها أدركت، نظامنا ما كان مونوليث، بس كان يتصرف زي أكبر مونوليث في التاريخ.

لما المايكروسيرفس بتتصرف زي المونوليث: أعراض الوحش الموزع

المشكلة اللي واجهناها إلها اسم تقني فخم: “المونوليث الموزع” (Distributed Monolith). وهو أسوأ نوع ممكن تتعامل معه. عندك كل تعقيدات وصعوبات إدارة المايكروسيرفس (النشر، المراقبة، الشبكات)، وبنفس الوقت عندك كل مساوئ المونوليث (الاقتران الشديد، فشل خدمة يؤثر على الأخرى). كيف بتعرف إنه نظامك مصاب بهذا المرض؟

أنا بكلمك، وإنت بترد… ولو ما رديت، خربت الدنيا! (مشكلة الاقتران المتزامن)

هذا هو العرض الرئيسي. لما تكون الخدمة A تحتاج معلومة من الخدمة B، فتقوم بعمل طلب HTTP مباشر لها وتنتظر الرد. هذا اسمه اقتران متزامن (Synchronous Coupling). تخيلها زي مكالمة تلفون: لو الشخص اللي بتتصل فيه ما رد، أو خطه مشغول، أو شبكته واقعة، إنت بتضلك معلق على الخط ومش قادر تعمل إشي ثاني. هذا بالضبط ما حدث معنا. خدمة المصادقة “اتصلت” بخدمة الفوترة، ولما الأخيرة ما ردت، الدنيا كلها خربت.

سلسلة الفشل المتتالية (Cascading Failures)

هذا نتيجة طبيعية للاقتران المتزامن. فشل في خدمة واحدة غير حرجة يمكن أن ينتشر كالنار في الهشيم ويُسقط خدمات أخرى تبدو غير مرتبطة بها، مما يؤدي في النهاية إلى انهيار النظام بأكمله. الخدمة B تقع، فتتسبب في تعليق الخدمة A التي تعتمد عليها، والخدمة C التي تعتمد على A تعلق هي الأخرى، وهكذا دواليك.

صعوبة التطوير والنشر

من المفترض أن المايكروسيرفس تمنحك حرية تطوير ونشر كل خدمة بشكل مستقل. لكن في عالم المونوليث الموزع، هذا حلم. إذا أردت تغيير شيء في الخدمة B، قد تكسر شيئًا في الخدمة A التي تعتمد عليها. فتجد نفسك مضطراً لتنسيق عمليات النشر بين الفرق المختلفة، واختبار كل شيء معًا، وكأنك تنشر مونوليث واحد كبير. راحت الفائدة كلها!

الحل في “الحدث”: مقدمة إلى المعمارية الموجهة بالأحداث (EDA)

بعد ليلة الإطلاق الكارثية هذيك، جلسنا كفريق وأعدنا التفكير في كل شيء. الحل لم يكن في إضافة المزيد من الـ “try/catch” أو زيادة الـ “timeout”. الحل كان في تغيير طريقة تفكيرنا، في تغيير طريقة تواصل الخدمات مع بعضها. وهنا يأتي دور البطل الحقيقي: المعمارية الموجهة بالأحداث (Event-Driven Architecture – EDA).

شو هو “الحدث” أصلاً؟

بكل بساطة، “الحدث” (Event) هو إشعار بحدوث شيء مهم في النظام. إنه حقيقة وقعت في الماضي ولا يمكن تغييرها. أمثلة:

  • UserRegistered (مستخدم جديد سجل)
  • OrderPlaced (تم إنشاء طلب جديد)
  • PaymentSucceeded (نجحت عملية الدفع)
  • SubscriptionStatusUpdated (تم تحديث حالة الاشتراك)

لاحظ أن أسماء الأحداث دائمًا بصيغة الماضي. الخدمة لا تطلب شيئًا، بل تُخبر العالم “لقد حدث هذا الأمر”.

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

تتكون المعمارية الموجهة بالأحداث من ثلاثة أجزاء رئيسية:

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

الفكرة عبقرية في بساطتها: المنتج لا يعرف شيئًا عن المستهلك، والمستهلك لا يعرف شيئًا عن المنتج. كلاهما يعرف فقط وسيط الأحداث. هذا ما نسميه “الفصل” أو “الاستقلالية” (Decoupling) الحقيقية.

يلا نصلحها سوا: تفكيك نظام تسجيل الدخول والفوترة

دعونا نعود لمثالنا الكارثي ونرى كيف يمكن لـ EDA أن تنقذ الموقف.

الوضع القديم: جحيم الطلب والاستجابة (Request/Response Hell)

كان التدفق كالتالي:

  1. المستخدم يحاول تسجيل الدخول عبر Auth Service.
  2. Auth Service يرسل طلب HTTP مباشر إلى Billing Service: “يا خدمة الفوترة، هل اشتراك هذا المستخدم فعال؟”.
  3. Auth Service ينتظر الرد.
  4. إذا كانت Billing Service معطلة، ينتظر Auth Service حتى انتهاء المهلة (timeout) ثم يفشل ويعيد خطأ للمستخدم.

وهذا هو الاقتران القاتل الذي تحدثنا عنه.

الوضع الجديد: عالم الأحداث السعيد

باستخدام EDA، يتغير التدفق تمامًا:

  1. عندما يتغير أي شيء يتعلق باشتراك المستخدم في Billing Service (مثلاً، تجديد الاشتراك أو انتهاؤه)، تقوم الخدمة فورًا بنشر حدث، وليكن اسمه SubscriptionStatusUpdated، إلى وسيط الأحداث (مثل RabbitMQ). محتوى الحدث يكون شيئًا كهذا: { userId: '123', newStatus: 'active', expiryDate: '2024-12-31' }.
  2. Auth Service تكون “مشتركة” (subscribed) في هذا النوع من الأحداث.
  3. عندما يصل الحدث إلى Auth Service، تقوم بتحديث نسخة محلية من بيانات حالة الاشتراك لديها (في قاعدة بياناتها أو في ذاكرة تخزين مؤقت cache).
  4. الآن، وهذا هو الجزء الأهم: عندما يحاول المستخدم تسجيل الدخول، كل ما تحتاجه Auth Service هو التحقق من هذه البيانات المحلية! لا يوجد أي اتصال مباشر بخدمة الفوترة على الإطلاق أثناء عملية تسجيل الدخول.

النتيجة؟ لو وقعت خدمة الفوترة لمدة ساعة، لن يتأثر تسجيل الدخول أبدًا! النظام أصبح مرنًا (resilient) وقويًا.

مثال كود بسيط (باستخدام Node.js و RabbitMQ)

هذا مجرد توضيح للفكرة، وليس كودًا جاهزًا للإنتاج.

المنتج (BillingService)


// billing-service.js
const amqp = require('amqplib');

// هذه الدالة تُستدعى عندما يتغير اشتراك المستخدم
async function publishSubscriptionUpdate(userId, status) {
  const connection = await amqp.connect('amqp://localhost');
  const channel = await connection.createChannel();
  const exchange = 'user_events';

  await channel.assertExchange(exchange, 'fanout', { durable: false });
  
  const event = { userId, status, timestamp: new Date() };
  
  // انشر الحدث للجميع! لا يهمنا من يستمع.
  channel.publish(exchange, '', Buffer.from(JSON.stringify(event)));
  console.log(" [x] Sent Event: Subscription Updated for user %s", userId);
  
  // ... إغلاق الاتصال
}

المستهلك (AuthService)


// auth-service.js
const amqp = require('amqplib');

// هذه الدالة تعمل باستمرار في الخلفية
async function listenForSubscriptionUpdates() {
  const connection = await amqp.connect('amqp://localhost');
  const channel = await connection.createChannel();
  const exchange = 'user_events';

  await channel.assertExchange(exchange, 'fanout', { durable: false });
  
  // أنشئ طابورًا خاصًا لهذه الخدمة
  const q = await channel.assertQueue('', { exclusive: true });
  console.log(" [*] AuthService is waiting for events in %s.", q.queue);

  // اربط الطابور بالـ exchange
  channel.bindQueue(q.queue, exchange, '');

  channel.consume(q.queue, (msg) => {
    if (msg.content) {
      const event = JSON.parse(msg.content.toString());
      console.log(" [.] Received Event: ", event);
      // !!! هنا السحر !!!
      // قم بتحديث قاعدة البيانات المحلية أو الكاش بمعلومات الاشتراك الجديدة
      // updateUserSubscriptionInLocalDB(event.userId, event.status);
    }
  }, { noAck: true });
}

listenForSubscriptionUpdates();

نصائح من مطبخ أبو عمر البرمجي

الانتقال إلى EDA ليس مجرد كتابة كود مختلف، بل هو تغيير في طريقة التفكير. من خلال خبرتي، هذه بعض النصائح العملية:

ابدأ صغيرًا

لا تحاول تحويل نظامك بأكمله إلى EDA دفعة واحدة. اختر أكثر جزء مؤلم في نظامك، أكثر تدفق يسبب مشاكل بسبب الاقتران، وابدأ به. أثبت الفكرة، تعلم من التجربة، ثم توسع تدريجيًا.

اختر وسيط الأحداث المناسب

مش كل إشي بده دبابة. Kafka قوي جدًا وممتاز للتعامل مع كميات هائلة من البيانات والتسلسل (ordering) مهم. RabbitMQ مرن ومتعدد الاستخدامات وسهل البدء به. خدمات سحابية مثل AWS SQS/SNS ممتازة للبدء بسرعة وبدون إدارة. افهم متطلباتك واختر الأداة المناسبة للمهمة.

تقبل “الاتساق النهائي” (Eventual Consistency)

في مثالنا، قد يكون هناك تأخير لجزء من الثانية بين تحديث الاشتراك في Billing Service ووصول الحدث إلى Auth Service. هذا يعني أن النظام ليس متسقًا بشكل فوري، بل “في النهاية” سيصبح متسقًا. في 99% من الحالات، هذا مقبول تمامًا. تحدث مع أصحاب المصلحة وتأكد أن هذا التأخير البسيط لا يكسر منطق العمل.

راقب ثم راقب ثم راقب

تصحيح الأخطاء في نظام EDA يمكن أن يكون أصعب. “وين راح هالحدث؟” هو سؤال ستطرحه كثيرًا. استخدم أدوات مراقبة قوية (Monitoring & Tracing)، وأضف دائمًا “معرف ارتباط” (Correlation ID) لكل حدث لتتمكن من تتبع رحلته عبر الخدمات المختلفة.

صمم “عقود الأحداث” بحكمة

هيكل الحدث (Event Schema) هو عقد بين المنتج والمستهلكين. تغييره بشكل عشوائي يمكن أن يكسر الخدمات المستهلكة. فكر في إصدار الأحداث (Event Versioning) منذ اليوم الأول. مثلاً، بدلاً من `SubscriptionStatusUpdated`، يمكن أن يكون `SubscriptionStatusUpdated_v2`.

الخلاصة: حرر خدماتك من أغلالها ⛓️

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

الانتقال إلى EDA هو نقلة نوعية في التفكير قبل أن يكون نقلة تقنية. إنه التحول من بناء خدمات “متطلبة” تتصل ببعضها البعض باستمرار، إلى بناء خدمات “مستقلة” و”واعية” تصرخ للعالم بما فعلته وتترك للآخرين حرية الاستماع والتصرف.

قد يبدو الأمر معقدًا في البداية، ولكنه استثمار يؤتي ثماره على المدى الطويل في شكل أنظمة أكثر مرونة وقوة وقابلية للتوسع. يلا، شدوا حيلكم يا جماعة، وخلينا نبني أنظمة بتستاهل جهدنا وتعبنا. 🚀

أبو عمر

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

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

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

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

آخر المدونات

نصائح برمجية

كان منطقنا البرمجي هرماً من الشروط المتداخلة: كيف أنقذتنا ‘الجمل الحارسة’ (Guard Clauses) من جحيم التعقيد؟

أتذكر مشروعاً قديماً كاد أن يصيبني بالجنون بسبب شفرة مليئة بـ if-else المتداخلة، حتى تعلمت تقنية "الجمل الحارسة" (Guard Clauses). في هذا المقال، أشارككم كيف...

31 مايو، 2026 قراءة المزيد
ذكاء اصطناعي

كانت نماذجنا العملاقة تلتهم الذاكرة: كيف أنقذنا ‘التكميم’ (Quantization) من جحيم فواتير الـ GPU؟

أشارككم قصة حقيقية من قلب المعركة مع نماذج الذكاء الاصطناعي العملاقة، وكيف أنقذنا تقنية "التكميم" (Quantization) من فواتير الخوادم الباهظة. سنتعمق في هذه التقنية السحرية،...

31 مايو، 2026 قراءة المزيد
تجربة المستخدم والابداع البصري

كانت تصاميمنا تتحطم عند التسليم: كيف أنقذتنا ‘رموز التصميم’ (Design Tokens) من جحيم الهوة بين المصمم والمطور؟

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

31 مايو، 2026 قراءة المزيد
برمجة وقواعد بيانات

كان تحديث قاعدة البيانات يوقف خدماتنا: كيف أنقذتنا استراتيجيات الترحيل بدون توقف (Zero-Downtime Migration) من جحيم نافذة الصيانة؟

أشارككم قصة ليلة طويلة تعلمت فيها بالطريقة الصعبة أن "نافذة الصيانة" هي عدو للمستخدمين والشركات. نستكشف معاً استراتيجيات الترحيل بدون توقف (Zero-Downtime Migration) التي تحافظ...

31 مايو، 2026 قراءة المزيد
التوظيف وبناء الهوية التقنية

كان حسابي على GitHub مقبرة للمشاريع الميتة: كيف أنقذتني ‘المساهمات المفتوحة المصدر’ من جحيم السيرة الذاتية الفارغة؟

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

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