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

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

بتذكر هذاك اليوم زي كأنه مبارح. كانت ليلة خميس، والكل بستنى الويكند على نار. فجأة، بدأت توصلني التنبيهات على الموبايل زي المطر… “System is down!”، “Users can’t register!”، “API is throwing 500 errors!”. نزلت ركض على المكتب، وفنجان القهوة بإيدي اللي كانت بترجف، وبديت رحلة البحث عن السبب.

بعد ساعات من الحفر في السجلات (Logs) والبحث المضني، اكتشفنا المصيبة. فريق خدمة الإشعارات كان قد نشر تحديثًا بسيطًا يحتوي على خطأ صغير (bug) أدى إلى بطء شديد في استجابة الخدمة. لكن، كيف يمكن لخدمة إشعارات “ثانوية” أن تشلّ حركة النظام بأكمله؟

الجواب كان بسيطًا ومُرعبًا في نفس الوقت: خدماتنا كانت متشابكة ببعضها زي كُبّة الصوف. عند تسجيل مستخدم جديد، كانت “خدمة المستخدمين” تستدعي “خدمة الإشعارات” بشكل مباشر ومتزامن (Synchronous) لإرسال بريد ترحيبي، ثم تنتظر الرد لتكمل العملية. عندما تباطأت خدمة الإشعارات، تباطأت معها عملية التسجيل بأكملها، ومع تراكم الطلبات، انهار كل شيء. في تلك الليلة، أدركت أننا لا نبني نظامًا، بل نبني برجًا من أوراق اللعب سينهار مع أي نسمة هواء. كان لا بد من التغيير، وهنا بدأت رحلتي مع المعمارية الموجهة بالأحداث (EDA).

ما هي المعمارية المتشابكة (Tightly Coupled)؟ أو “كُبّة الصوف” كما أسميها

قبل أن نغوص في الحل، دعونا نفهم المشكلة جيدًا. المعمارية المتشابكة، أو “كُبّة الصوف”، هي عندما تعتمد الخدمات المصغرة (Microservices) على بعضها البعض بشكل مباشر وقوي. تخيلها كخط إنتاج في مصنع، حيث يقوم كل عامل بتسليم القطعة للعامل الذي يليه مباشرة. إذا أخذ أحد العمال استراحة أو تباطأ، يتوقف خط الإنتاج بأكمله.

في عالم البرمجيات، هذا يعني أن الخدمة (أ) تستدعي الخدمة (ب) عبر طلب HTTP API مباشر وتنتظر الرد. هذا الارتباط المباشر يخلق عدة مشاكل قاتلة:

  • هشاشة النظام (Fragility): كما حدث في قصتي، أي فشل أو بطء في خدمة واحدة (مثل خدمة الإشعارات) يمكن أن يتسبب في سلسلة من الأعطال (Cascading Failures) تشل النظام بأكمله.
  • صعوبة التطوير (Development Difficulty): عندما تريد تعديل شيء في الخدمة (ب)، عليك أن تكون حذرًا جدًا حتى لا تكسر شيئًا في الخدمة (أ) التي تعتمد عليها. بدك تغير شغلة صغيرة، بتلاقي حالك بتفحص كل النظام. هذا يقتل سرعة التطوير والابتكار.
  • محدودية التوسع (Scalability Issues): لنفترض أن خدمة المستخدمين تتلقى 1000 طلب في الثانية، بينما خدمة الإشعارات لا تستطيع معالجة أكثر من 100 طلب في الثانية. في النموذج المتشابك، ستصبح خدمة الإشعارات عنق الزجاجة (Bottleneck) الذي يحد من قدرة النظام بأكمله على التوسع.

الحل السحري: المعمارية الموجهة بالأحداث (Event-Driven Architecture – EDA)

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

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

الخدمات الأخرى المهتمة بهذا الحدث (مثل خدمة الإشعارات، خدمة التحليلات، خدمة التوصيات) تراقب لوحة الإعلانات هذه. عندما ترى إعلانًا يهمها، تأخذ نسخة منه وتنفذ مهمتها في الوقت الذي يناسبها وبالسرعة التي تناسبها. خدمة المستخدمين لا تعرف حتى بوجود هذه الخدمات الأخرى! هذا ما نسميه “الفصل” (Decoupling).

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

  1. الحدث (Event): هو سجل يوثق حقيقة وقوع شيء ما في الماضي. على سبيل المثال، UserRegistered, OrderPlaced, PaymentProcessed. يحتوي الحدث على البيانات اللازمة لوصف ما حدث (مثل هوية المستخدم، بريده الإلكتروني، قيمة الطلب، إلخ).
  2. المنتِج (Producer/Publisher): هو الخدمة التي تكتشف الحدث وتنشئه وتنشره. في مثالنا، “خدمة المستخدمين” هي المنتِج لحدث UserRegistered.
  3. المستهلِك (Consumer/Subscriber): هو الخدمة التي تستمع إلى الأحداث وتتفاعل معها. في مثالنا، “خدمة الإشعارات” و”خدمة التحليلات” هما مستهلكان.
  4. ناقل الأحداث (Event Broker/Bus): هو الوسيط أو “لوحة الإعلانات”. إنه البنية التحتية التي تستقبل الأحداث من المنتجين وتوجهها إلى المستهلكين المهتمين. من أشهر الأمثلة: RabbitMQ, Apache Kafka, AWS SQS/SNS, Google Pub/Sub.

تطبيق عملي: كيف أعدنا بناء نظامنا باستخدام EDA

دعونا نعود إلى سيناريو تسجيل المستخدم الذي كاد أن يدمر شركتنا ونرى كيف أصلحناه باستخدام EDA.

السيناريو القديم (كُبّة الصوف)

1. المستخدم يرسل طلب تسجيل إلى خدمة المستخدمين.

2. خدمة المستخدمين تحفظ بياناته في قاعدة البيانات.

3. خدمة المستخدمين تستدعي خدمة الإشعارات مباشرة (API Call) وتنتظر…

4. (هنا حدثت المشكلة: خدمة الإشعارات بطيئة جدًا أو معطلة).

5. خدمة المستخدمين تبقى معلقة، مما يؤدي إلى انتهاء مهلة الطلب (Timeout) وفشل عملية التسجيل بأكملها.

السيناريو الجديد (سيمفونية EDA)

1. المستخدم يرسل طلب تسجيل إلى خدمة المستخدمين.

2. خدمة المستخدمين تحفظ بياناته في قاعدة البيانات.

3. خدمة المستخدمين تنشئ حدثًا اسمه UserRegistered يحتوي على تفاصيل المستخدم (ID, email, name).

4. خدمة المستخدمين ترسل هذا الحدث إلى ناقل الأحداث (Event Broker) وتنسى أمره تمامًا.

5. خدمة المستخدمين ترد فورًا على المستخدم بنجاح العملية “تم التسجيل بنجاح!”. (تجربة مستخدم أسرع وأفضل بكثير!)

— في الخلفية، وبشكل غير متزامن —

6. خدمة الإشعارات، المشتركة في حدث UserRegistered، تستلم الحدث من الناقل وترسل البريد الترحيبي.

7. خدمة التحليلات، المشتركة أيضًا في نفس الحدث، تستلمه وتحدث مؤشراتها.

8. خدمة جديدة (مثلاً خدمة التوصيات) يمكن إضافتها للاستماع لنفس الحدث دون الحاجة لتعديل سطر واحد في “خدمة المستخدمين”.

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

مثال بالكود: لنكتب شيئًا بسيطًا (Python و RabbitMQ)

الكلام النظري جميل، لكن دعونا نرى كيف يبدو هذا على أرض الواقع. سنستخدم Python و RabbitMQ كمثال.

المنتج (Producer): خدمة المستخدمين

هذا الكود يمثل الجزء الذي ينشر الحدث بعد تسجيل المستخدم.


import pika
import json

# الاتصال بـ RabbitMQ
connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()

# تعريف 'exchange' وهو المسؤول عن توجيه الرسائل
# النوع 'fanout' يرسل الرسالة لكل الـ queues المرتبطة به
channel.exchange_declare(exchange='user_events', exchange_type='fanout')

# بيانات الحدث
event_data = {
    'event_type': 'UserRegistered',
    'user_id': 'usr_12345',
    'email': 'ahlan@example.com',
    'name': 'أبو عمر'
}

# نشر الحدث
channel.basic_publish(
    exchange='user_events',
    routing_key='',  # routing_key لا يهم في نوع fanout
    body=json.dumps(event_data)
)

print(" [x] Sent 'UserRegistered' event")
connection.close()

المستهلك (Consumer): خدمة الإشعارات

وهذا الكود يمثل خدمة الإشعارات التي تستمع للأحداث وتتصرف بناءً عليها.


import pika
import json
import time

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()

channel.exchange_declare(exchange='user_events', exchange_type='fanout')

# تعريف queue مؤقتة (اسمها يحدده RabbitMQ)
# exclusive=True تعني أن الـ queue ستحذف عند انقطاع الاتصال
result = channel.queue_declare(queue='', exclusive=True)
queue_name = result.method.queue

# ربط الـ queue بالـ exchange
channel.queue_bind(exchange='user_events', queue=queue_name)

print(' [*] Waiting for user events. To exit press CTRL+C')

def callback(ch, method, properties, body):
    event_data = json.loads(body)
    if event_data.get('event_type') == 'UserRegistered':
        print(f" [->] Received UserRegistered event for user: {event_data.get('email')}")
        print(" [!] Simulating sending a welcome email...")
        time.sleep(2) # محاكاة لعملية إرسال البريد
        print(" [✔] Email sent successfully.")
    
    # إعلام RabbitMQ بأن الرسالة تمت معالجتها
    ch.basic_ack(delivery_tag=method.delivery_tag)

channel.basic_consume(
    queue=queue_name,
    on_message_callback=callback
)

channel.start_consuming()

نصائح أبو عمر الذهبية للانتقال إلى EDA

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

  • ابدأ صغيرًا: لا تهدم كل الدار وتبنيها من جديد. اختر عملية واحدة غير حرجة في نظامك (مثل تحديث صورة الملف الشخصي) وحولها إلى EDA. تعلم من التجربة ثم توسع تدريجيًا. خذها حبة حبة.
  • صمم أحداثك بعناية: عقد الحدث (Event Schema) هو واجهة برمجة التطبيقات الجديدة بين خدماتك. اجعله غنيًا بالمعلومات الكافية للمستهلكين، وفكر في كيفية إدارة الإصدارات المختلفة منه منذ اليوم الأول (Schema Versioning).
  • اختر ناقل الأحداث المناسب: مش كل إشي بده مدفع يا جماعة. Kafka ممتاز للتدفقات الضخمة والتحليلات الفورية. RabbitMQ رائع للتوجيه المعقد وقوائم المهام. خدمات سحابية مثل AWS SQS/SNS بسيطة وممتازة لفك الارتباط الأولي بين الخدمات. اختر الأداة التي تناسب مشكلتك.
  • فكر في قابلية المراقبة (Observability): تصحيح الأخطاء في نظام موزع قد يكون كابوسًا. أنت بحاجة ماسة إلى أدوات تتبع (Tracing) مثل OpenTelemetry لترى رحلة الحدث عبر الخدمات المختلفة، بالإضافة إلى سجلات (Logging) ومراقبة (Monitoring) جيدة.
  • تعامل مع الفشل بأناقة: ماذا لو فشل المستهلك في معالجة حدث؟ لا تدعه يختفي. استخدم نمط “قائمة انتظار الرسائل الميتة” (Dead Letter Queue – DLQ) لإرسال الأحداث الفاشلة إلى مكان آمن لتحليلها وإعادة معالجتها لاحقًا.
  • تقبّل الاتساق النهائي (Eventual Consistency): في عالم EDA، البيانات لا تتحدث فورًا في كل مكان. لوحة التحليلات لن تُظهر المستخدم الجديد في نفس الثانية التي يسجل فيها. ستكون “متسقة في النهاية”. هذا هو الثمن الذي تدفعه مقابل المرونة وقابلية التوسع، وعليك أن تكون واعيًا به وتصمم نظامك بناءً عليه.

الخلاصة: من فوضى كُبّة الصوف إلى سيمفونية متناغمة 🎼

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

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

نصيحتي الأخيرة لك: لا تخف من التغيير. ابدأ بالتعلم، جرب على نطاق صغير، وستكتشف بنفسك القوة الهائلة التي تمنحها لك المعمارية الموجهة بالأحداث. يلا شدوا حيلكم! 💪

أبو عمر

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

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

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

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

آخر المدونات

التكنلوجيا المالية Fintech

عمليات الاحتيال كانت تستنزف أرباحنا بصمت: كيف أنقذني ‘نموذج كشف الاحتيال’ القائم على الذكاء الاصطناعي من خسارة ثقة العملاء؟

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

26 مارس، 2026 قراءة المزيد
أتمتة العمليات

كل سيرفر جديد كان قصة رعب: كيف أنقذتني ‘البنية التحتية كشيفرة’ (IaC) من فوضى الإعدادات اليدوية؟

أشارككم قصة من قلب المعاناة مع إعداد السيرفرات يدوياً، وكيف كانت "البنية التحتية كشيفرة" (IaC) وتحديداً أداة Terraform هي طوق النجاة. مقالة عملية للمبرمجين ومديري...

26 مارس، 2026 قراءة المزيد
نصائح برمجية

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

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

26 مارس، 2026 قراءة المزيد
خوارزميات

ذاكرة التخزين المؤقت كانت بلا فائدة: كيف أنقذتني خوارزمية ‘الأقل استخدامًا مؤخرًا’ (LRU) من بطء قاعدة البيانات؟

أشارككم قصة حقيقية عن مشروع كاد أن يفشل بسبب بطء قاعدة البيانات رغم استخدامي للتخزين المؤقت. اكتشفوا كيف كانت خوارزمية بسيطة مثل LRU هي طوق...

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

ألواني الزاهية كانت فخاً: كيف أنقذني ‘تباين الألوان’ من تصميم واجهات كارثية؟

أشارككم قصة حقيقية من بداياتي، عندما كاد حبي للألوان الزاهية أن يدمر مشروعاً كاملاً. اكتشفوا معي كيف تعلمت بالطريقة الصعبة أهمية تباين الألوان (Color Contrast)...

26 مارس، 2026 قراءة المزيد
الشبكات والـ APIs

واجهاتي البرمجية كانت دعوة مفتوحة للمخترقين: كيف أنقذتني ‘بوابة الواجهات البرمجية’ (API Gateway) من كابوس الاستغلال؟

أروي لكم قصتي مع مشروع كاد أن ينهار بسبب ثغرات أمنية في واجهاته البرمجية، وكيف كانت "بوابة الواجهات البرمجية" (API Gateway) هي طوق النجاة. اكتشفوا...

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