يا جماعة الخير، خلوني أحكيلكم قصة صارت معي قبل كم سنة، قصة علّمتني درس ما بنساه في عالم البرمجة. كنا في الشركة شغالين على نظام كبير، مجموعة خدمات مصغرة (Microservices) بتتواصل مع بعضها. في ليلة خميس، حوالي الساعة 9 بالليل، والمكتب شبه فاضي إلا من فريقي وأنا، كنا بنعمل تحديث بسيط… “بسيط” هيك كنا مفكرين.
التحديث كان في خدمة المستخدمين (Users Service)، مجرد إضافة حقل جديد في بروفايل المستخدم. ضغطنا زر النشر (Deploy) واحنا متطمنين. وما هي إلا دقايق، وإجا التلفون اللي بنكرهه كلنا… تلفون من فريق الدعم الفني. “أبو عمر، نظام الطلبات واقع! والزبائن مش قادرين يعملوا طلبات جديدة!”.
قلبي وقع بين رجليي. شو دخل خدمة المستخدمين بخدمة الطلبات (Orders Service)؟ ركضنا نفحص السجلات (Logs)، وإذ بالمصيبة: خدمة الطلبات، عشان تتأكد من بيانات المستخدم، كانت بتستدعي خدمة المستخدمين بشكل مباشر ومتزامن (Synchronous). التحديث الجديد اللي عملناه سبب بطء بسيط في الاستجابة، وهذا البطء كان كافي إنه يسبب “Timeouts” في خدمة الطلبات، وبالتالي… انهيار كامل للعملية. قضينا ليلتها للصبح واحنا بنعمل تراجع (Rollback) وبنصلّح المشكلة. هذيك الليلة، قررت إنه “خلص، بكفي! لازم نلاقي حل جذري لهي الكُبّة (الكرة المتشابكة) من التبعيات”.á>
الكابوس: الاقتران الشديد (Tight Coupling)
المشكلة اللي واجهناها كان إلها اسم علمي واضح: الاقتران الشديد (Tight Coupling). لما تكون خدماتك معتمدة على بعضها بشكل مباشر ومتزامن، إنت بتكون بتبني بيت من ورق الكوتشينة. أي تغيير في خدمة، مهما كان بسيط، ممكن يهدّ كل النظام فوق راسك.
أعراض هذا المرض الخبيث
- شلالات الفشل (Cascading Failures): زي ما صار معنا، فشل خدمة واحدة يؤدي إلى فشل خدمات أخرى تباعاً.
- بطء التطوير: كل فريق بصير يخاف يعمل أي تغيير لأنه مش عارف شو ممكن يخرب في الخدمات الثانية. الاجتماعات بتطول والتنسيق بصير كابوس.
- صعوبة التوسع (Scaling): إذا كانت خدمة الطلبات عليها ضغط عالي، فأنت مضطر تعمل توسيع (Scale out) الها ولخدمة المستخدمين اللي هي معتمدة عليها، حتى لو خدمة المستخدمين ما عليها ضغط. هذا هدر للموارد.
- رعب يوم النشر (Deployment Dread): بصير يوم نشر التحديثات يوم حزين، مليان توتر ودعاء إنه ما تصير مصايب.
الفجر الجديد: المعمارية القائمة على الأحداث (EDA)
بعد ليلتنا المشؤومة، جلسنا وبدأنا نبحث عن حلول. الحل كان واضح قدامنا، وكنا بنسمع عنه بس ما طبقناه بجدية: المعمارية القائمة على الأحداث (Event-Driven Architecture – EDA). الفكرة عبقرية في بساطتها: بدل ما الخدمات تحكي مع بعضها بشكل مباشر، خلّيها تتواصل بشكل غير مباشر.
تخيل معي إنك في ساحة عامة. بدل ما تروح لكل شخص وتخبره بالخبر الجديد (اقتران شديد)، إنت بتطلع على منصة وبتعلن الخبر بصوت عالي (هذا هو الحدث أو الـ Event). كل شخص مهتم بالخبر رح يسمعه ويتصرف بناءً عليه، واللي مش مهتم رح يكمل حياته عادي. لا إنت استنيت رد من حدا، ولا حدا تعطّل عشانك.
المكونات الأساسية للـ EDA
- منتج الحدث (Event Producer): هو الخدمة اللي “بتعلن الخبر”. في مثالنا، خدمة المستخدمين هي المنتج.
- موجّه الأحداث (Event Broker/Router): هو “المنصة” أو “لوحة الإعلانات العامة”. هو وسيط بستلم الأحداث من المنتجين وبوزعها للمستهلكين المهتمين. أمثلة عليه: RabbitMQ, Apache Kafka, AWS SQS.
- مستهلك الحدث (Event Consumer): هو الخدمة اللي “بتسمع الخبر” وبتتفاعل معه. في مثالنا، خدمة الإشعارات ممكن تكون مستهلك.
كيف حررتنا EDA؟ تطبيق عملي
خلونا نرجع لسيناريو تحديث بروفايل المستخدم، بس هالمرة باستخدام EDA.
- خدمة المستخدمين (Users Service) لم تعد تسمح لأحد باستدعائها مباشرة. بدلاً من ذلك، عندما يقوم مستخدم بتحديث ملفه الشخصي، تقوم الخدمة بإنتاج حدث (Event) اسمه
UserProfileUpdated.- يتم إرسال هذا الحدث إلى وسيط الرسائل (Message Broker) مثل RabbitMQ.
- خدمة الإشعارات (Notifications Service) تكون “مشتركة” ومستمعة لهذا النوع من الأحداث. عندما يصلها الحدث، تقوم بإرسال إيميل للمستخدم “تم تحديث ملفك الشخصي بنجاح”.
- خدمة التحليلات (Analytics Service) قد تكون مشتركة أيضاً بنفس الحدث لتسجيل نشاط المستخدم.
- والأهم: خدمة الطلبات (Orders Service)؟ لا هي منتج ولا مستهلك لهذا الحدث. هي ببساطة “مش مهتمة”. لم تعد تعرف بوجود تحديث في خدمة المستخدمين أصلاً.
النتيجة؟ استطعنا تحديث خدمة المستخدمين مئة مرة بدون ما خدمة الطلبات “تحس” علينا. كل خدمة صارت مستقلة، حرة، وقادرة على التطور والتوسع بدون ما تأثر على جيرانها. صار الشغل أسرع، والنفسية أرتاح.
مثال كود بسيط (بايثون مع RabbitMQ)
لتقريب الفكرة، هذا مثال مبسط جداً باستخدام مكتبة
pikaفي بايثون.المنتج (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') user_data = {'user_id': 123, 'new_email': 'omar.new@example.com'} message = json.dumps(user_data) # نشر الحدث channel.basic_publish(exchange='user_events', routing_key='', body=message) print(f" [x] Sent event: UserProfileUpdated for user {user_data['user_id']}") connection.close()المستهلك (Consumer – في خدمة الإشعارات)
import pika import json connection = pika.BlockingConnection(pika.ConnectionParameters('localhost')) channel = connection.channel() channel.exchange_declare(exchange='user_events', exchange_type='fanout') # إنشاء 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): user_data = json.loads(body) print(f" [!] Received UserProfileUpdated event for user {user_data['user_id']}") print(f" -> Sending notification email to {user_data['new_email']}...") # هنا يتم وضع كود إرسال الإيميل الفعلي print(" [✔] Notification sent.") channel.basic_consume(queue=queue_name, on_message_callback=callback, auto_ack=True) channel.start_consuming()لاحظ كيف أن المنتج والمستهلك لا يعرفان أي شيء عن بعضهما البعض. كل ما يجمعهما هو اسم الـ
exchangeوالاتفاق الضمني على شكل البيانات داخل الحدث.نصائح من كيس أبو عمر
بعد ما خضنا هذه التجربة، تعلمت شوية دروس بحب أشاركها معكم، “الزبدة” زي ما بنحكي:
- ابدأ صغيراً: لا تحاول تحويل كل نظامك لـ EDA مرة واحدة. اختر عملية واحدة غير حرجة (non-critical) وحولها كبداية. تعلم منها ثم توسع.
- عرّف أحداثك بوضوح (Event Schema): اتفق مع فريقك على هيكلية واضحة للأحداث. استخدموا أدوات مثل JSON Schema أو Avro لضمان التوافق وتجنب المشاكل مستقبلاً. الحدث هو عقد (Contract) بين الخدمات، فاجعله واضحاً.
- فكر في الـ Idempotency: ماذا لو استلمت خدمة الإشعارات نفس الحدث مرتين عن طريق الخطأ؟ هل سترسل إيميلين؟ يجب أن تكون خدماتك المستهلكة قادرة على التعامل مع هذا الموقف بذكاء (Idempotent Consumer). مثلاً، تتأكد من رقم الحدث قبل معالجته.
- المراقبة ثم المراقبة: في الأنظمة المتزامنة، تتبع الخطأ سهل (Call Stack). في EDA، الأمور غير مباشرة. أنت بحاجة ماسة لأدوات مراقبة (Monitoring) وتتبع (Tracing) قوية لتتمكن من تتبع رحلة الحدث عبر النظام ومعرفة أين حدثت المشكلة.
- ليست حلاً لكل شيء: الـ EDA ليست المطرقة الذهبية. إذا كنت تحتاج إلى استجابة فورية ومتزامنة (مثلاً، التحقق من توفر اسم مستخدم أثناء التسجيل)، فإن استدعاء API مباشر قد يكون أفضل وأبسط. استخدم الأداة المناسبة للمشكلة المناسبة.
الخلاصة… والزبدة
الانتقال إلى المعمارية القائمة على الأحداث كان نقلة نوعية لفريقنا ولشركتنا. حولنا من حالة الخوف والترقب عند كل تحديث، إلى حالة من الثقة والمرونة. لم نعد نبني بيوتاً من ورق، بل صرنا نبني شبكة من الخدمات المستقلة والقوية، كل واحدة تؤدي دورها بكفاءة بدون أن تعرقل الأخرى.
الرحلة لم تكن سهلة، وتطلبت تغييراً في طريقة تفكيرنا كمطورين، لكن النتيجة كانت تستحق كل التعب. إذا كنت تعاني من جحيم التبعيات، فربما حان الوقت لتفكر في “إعلان” استقلالك والبدء في رحلتك مع عالم الأحداث. الشغلة مش بس كود، الشغلة تغيير عقلية نحو بناء أنظمة أكثر نضجاً وقوة. 👍