يا جماعة الخير، السلام عليكم ورحمة الله. اسمحوا لي اليوم أحكي لكم قصة صارت معي قبل كم سنة، قصة فيها عرق وتوتر وسهر لآخر الليل، بس نهايتها كانت درس معماري مهم غير طريقة تفكيرنا في بناء الأنظمة.
كنا وقتها شغالين على منصة تجارة إلكترونية جديدة، والحماس كان واصل للسما. قرب موعد الإطلاق، وكل الفريق كان على أعصابه. قسمنا النظام لخدمات مصغرة (Microservices) زي ما الكتاب بيقول: خدمة للمنتجات، خدمة للطلبات، خدمة للدفع، وخدمة للمخزون، وخدمة للإشعارات (عشان نبعث إيميل ورسالة للمستخدم لما يشتري). كل إشي كان تمام التمام في بيئة التطوير.
وجاء يوم الإطلاق. “يلا يا شباب، توكلنا على الله”. أول ساعة، الأمور ماشية زي الحلاوة. فجأة، بدأت توصلنا تنبيهات: “الطلبات بتعلّق!”، “المستخدمين بشتكوا إنه عملية الشراء ما بتكمل!”. دخلنا على السيرفرات، لقينا الدنيا مولعة. المعالجات (CPU) في السما والذاكرة شبه مليانة.
بعد حفر وتحليل على السريع، اكتشفنا الكارثة. خدمة الإشعارات اللي بتبعت الإيميلات كانت بتعتمد على طرف ثالث (Third-party service)، وهذا الطرف الثالث صار بطيء جدًا تحت الضغط. المصيبة إنه تصميمنا كان غبي شوي: خدمة الطلبات لما يجيها طلب جديد، كانت بتتصل بشكل مباشر ومتزامن (Synchronously) بخدمة المخزون عشان تخصم الكمية، وبعدها بتتصل بخدمة الدفع، وبعدها بتتصل بخدمة الإشعارات. ولأنه خدمة الإشعارات بطيئة ومش راضية ترد بسرعة، خدمة الطلبات كانت بتضلها تستنى… وتستنى… وتستنى. كل طلب جديد كان بيفتح اتصال جديد ويضل معلق، لحد ما النظام كله “فرط” ووقع الفاس بالراس.
هذيك الليلة، بعد ما طفينا الحريقة يدويًا (وعملنا disable لخدمة الإشعارات مؤقتًا)، قعدت مع الفريق وحكيتلهم: “يا عمي، خدماتنا بتحب بعض زيادة عن اللزوم. لازم نفرّقهم، لازم كل خدمة تشتغل لحالها وما تستنى حدا”. ومن هان، بدأت رحلتنا مع المعمارية القائمة على الأحداث (Event-Driven Architecture).
ما هي المعمارية القائمة على الأحداث (EDA)؟
ببساطة شديدة، بدل ما الخدمات تكلم بعضها مباشرة وتستنى رد، صارت تتواصل بطريقة غير مباشرة. تخيلها زي لوحة إعلانات عامة في شركة. بدل ما قسم الطلبات يروح لمكتب قسم المخزون ويحكيله “اخصم لي حبة من هذا المنتج”، وبعدها يروح لمكتب قسم الإشعارات ويحكيله “ابعث إيميل”، بيصير إشي مختلف تمامًا.
قسم الطلبات بكل بساطة بروح على لوحة الإعلانات هاي وبحط ورقة مكتوب عليها: “يا جماعة، تم إنشاء طلب جديد برقم 123”. وبس، هيك مهمته انتهت وبرجع لمكتبه يكمل شغل.
قسم المخزون، وقسم الإشعارات، وأي قسم ثاني بهمه الموضوع، بكونوا قاعدين بيراقبوا لوحة الإعلانات. لما قسم المخزون يشوف الإعلان، بروح لحاله وبخصم الكمية من المخزون. ولما قسم الإشعارات يشوف نفس الإعلان، بروح بيبعت الإيميل للزبون. لاحظوا إنه قسم المخزون ما بيعرف أصلًا بوجود قسم الإشعارات، والعكس صحيح. كلهم بيعرفوا “لوحة الإعلانات” وبس. هذا هو جوهر “الاقتران المنخفض” (Loose Coupling).
المكونات الأساسية للـ EDA
هذا النظام الأنيق له ثلاث مكونات رئيسية:
- الحدث (Event): هو الإعلان نفسه. معلومة عن شيء مهم حصل في النظام (مثل:
OrderPlaced,UserRegistered,PaymentFailed). الحدث هو حقيقة وقعت في الماضي ولا يمكن تغييرها. - منتج الحدث (Event Producer): هو الخدمة اللي بتنشر الحدث على لوحة الإعلانات (مثل خدمة الطلبات في مثالنا).
- مستهلك الحدث (Event Consumer): هو أي خدمة مهتمة بهذا الحدث وبتقوم بفعل معين بناءً عليه (مثل خدمة المخزون وخدمة الإشعارات).
- ناقل الأحداث (Event Broker/Bus): هاي هي “لوحة الإعلانات” السحرية. هي عبارة عن برنامج وسيط (Middleware) مثل RabbitMQ, Apache Kafka, أو خدمات سحابية مثل AWS SQS/SNS. وظيفته يستلم الأحداث من المنتجين ويوصلها للمستهلكين المهتمين بشكل موثوق.
ليش وجع هالراس؟ فوائد تستحق العناء
قد تبدو هذه المعمارية معقدة في البداية، لكن الفوائد اللي بتجنيها على المدى الطويل هائلة، وخصوصًا في الأنظمة الكبيرة والمعقدة.
1. الاقتران المنخفض (Loose Coupling)
هذه هي الفائدة الكبرى اللي حلت مشكلتنا. خدمة الطلبات لم تعد بحاجة لمعرفة أي شيء عن خدمة الإشعارات أو المخزون. هي فقط تطلق حدث OrderPlaced. إذا كانت خدمة الإشعارات معطلة أو بطيئة، لا مشكلة على الإطلاق! خدمة الطلبات تستمر في العمل بشكل طبيعي، وناقل الأحداث سيحتفظ بالحدث حتى تعود خدمة الإشعارات للعمل وتستهلكه لاحقًا.
2. قابلية التوسع والمرونة (Scalability & Resilience)
لأن الخدمات مستقلة، يمكنك توسيع كل خدمة على حدة. هل تتلقى الكثير من الطلبات؟ يمكنك زيادة عدد نسخ (instances) خدمة الطلبات. هل عملية معالجة الصور بطيئة؟ يمكنك زيادة عدد مستهلكي حدث ImageUploaded. هذا يمنحك مرونة هائلة في التعامل مع ضغط العمل ويجعل نظامك أكثر صمودًا أمام الأخطاء. فشل خدمة واحدة لم يعد يعني فشل النظام بأكمله.
3. سهولة إضافة خدمات جديدة
تخيل بعد فترة قررنا نضيف نظام نقاط ولاء (Loyalty Points) بحيث يحصل المستخدم على نقاط مع كل طلب. في النظام القديم، كنا سنضطر لتعديل كود خدمة الطلبات لتقوم باستدعاء خدمة الولاء الجديدة (مما يزيد الطين بلة). أما مع EDA، فالأمر في غاية السهولة: ننشئ خدمة ولاء جديدة، ونجعلها “تستمع” لحدث OrderPlaced. هذا كل شيء! لم نلمس أي خدمة من الخدمات القائمة. النظام يتوسع ويتطور بأقل قدر من المخاطر.
يلا نشتغل: من النظرية إلى التطبيق
خلينا نشوف كيف تحول تدفق العمل (workflow) في مثالنا.
الطريقة القديمة (الاقتران المحكم – Synchronous)
المستخدم يضغط “شراء” ➔
- خدمة الطلبات تستقبل الطلب.
- خدمة الطلبات تستدعي خدمة المخزون (وتنتظر الرد).
- خدمة الطلبات تستدعي خدمة الدفع (وتنتظر الرد).
- خدمة الطلبات تستدعي خدمة الإشعارات (وتنتظر الرد). ← نقطة الفشل!
- خدمة الطلبات ترد على المستخدم بالنجاح.
الطريقة الجديدة (EDA – Asynchronous)
المستخدم يضغط “شراء” ➔
- خدمة الطلبات تستقبل الطلب، تحفظه في قاعدة البيانات بحالة “قيد المعالجة” (Processing).
- خدمة الطلبات تنشر حدث
OrderPlacedإلى ناقل الأحداث.- خدمة الطلبات ترد فورًا على المستخدم: “شكرًا لك، طلبك قيد المعالجة وسيتم تأكيده قريبًا”.
في الخلفية، وبشكل متوازٍ وغير متزامن:
- مستهلك المخزون يسمع حدث
OrderPlaced➔ يخصم الكمية من المخزون ➔ ينشر حدثStockReserved.- مستهلك الدفع يسمع حدث
StockReserved➔ يعالج الدفع ➔ ينشر حدثPaymentSuccessful.- مستهلك الإشعارات يسمع حدث
PaymentSuccessful➔ يرسل إيميل التأكيد للمستخدم.
لاحظ كيف أن تجربة المستخدم أصبحت أسرع بكثير، والنظام أصبح أكثر قوة. حتى لو كانت خدمة الإشعارات بطيئة، هذا لن يؤثر على عملية الشراء نفسها.
شوية كود عشان الصورة توضح (مثال بـ Node.js)
هذا مثال مبسط جدًا يوضح الفكرة. تخيل أن لدينا كائن eventBroker يمثل ناقل الأحداث.
// في خدمة الطلبات (Order Service) - المنتج
//
// const eventBroker = require('./eventBroker');
async function placeOrder(orderData) {
// 1. حفظ الطلب في قاعدة البيانات بحالة "PENDING"
const order = await db.orders.save(orderData);
// 2. نشر الحدث
const event = {
name: 'order.placed',
payload: {
orderId: order.id,
userId: order.userId,
items: order.items,
totalAmount: order.totalAmount
}
};
await eventBroker.publish(event.name, event.payload);
// 3. الرد فورًا على المستخدم
return { success: true, message: 'Your order is being processed.' };
}
// في خدمة الإشعارات (Notification Service) - المستهلك
//
// const eventBroker = require('./eventBroker');
async function setupListeners() {
eventBroker.subscribe('order.placed', async (payload) => {
try {
console.log(`Received order.placed event for order ID: ${payload.orderId}`);
// منطق إرسال الإيميل أو الرسالة
const user = await db.users.findById(payload.userId);
await emailService.sendConfirmation(user.email, payload);
console.log(`Confirmation email sent for order ID: ${payload.orderId}`);
} catch (error) {
console.error('Failed to process order.placed event:', error);
// هنا يجب التعامل مع الخطأ، مثلاً إعادة المحاولة أو إرساله إلى "طابور الرسائل الميتة"
}
});
}
setupListeners();
مش كل إشي وردي: تحديات لازم تعرفها
صحيح أن EDA قوية جدًا، لكنها ليست حلًا سحريًا لكل المشاكل، وتأتي مع مجموعة من التحديات الخاصة بها.
- التعقيد: أنت تستبدل تعقيد الاقتران في وقت التشغيل (runtime) بتعقيد في التصميم. تتبع تدفق الطلب عبر عدة خدمات غير متزامنة يصبح أصعب. تحتاج إلى أدوات مراقبة (Monitoring) وتتبع (Tracing) قوية.
- الاتساق النهائي (Eventual Consistency): في النظام المتزامن، البيانات تكون متسقة فورًا. هنا، البيانات تصبح متسقة “في النهاية”. قد يرى المستخدم طلبه “قيد المعالجة” لبضع ثوان. هذا يتطلب تغييرًا في طريقة التفكير وتصميم واجهات المستخدم لتتعامل مع هذه الحالة.
- إدارة الأحداث: ماذا لو احتجت إلى تغيير حقول حدث معين؟ هذا يتطلب استراتيجية لإدارة إصدارات الأحداث (Event Versioning). وماذا لو وصلت الأحداث بترتيب خاطئ؟ هذه مشاكل حقيقية تحتاج إلى حلول.
- الوسيط (Broker): ناقل الأحداث نفسه يصبح مكونًا حاسمًا. إذا توقف عن العمل، توقف النظام بأكمله. لذلك يجب أن يكون عالي التوفر (Highly Available) وموثوقًا.
نصائح من أبو عمر (من الكيس)
بعد ما خضنا هذه التجربة، تعلمت كم درس على الطريق، بحب أشارككم فيها:
- ابدأ بسيطًا: “ما تبدأ بالقانون قبل ما تعرف الحاجة”. ليس بالضرورة أن تبدأ مع وحش مثل Kafka. أحيانًا يكون Redis Pub/Sub أو حتى طابور بسيط في قاعدة البيانات كافيًا لبدء رحلتك مع EDA وفهم مفاهيمها.
- اجعل أحداثك غنية بالمعلومات: لا ترسل فقط
orderIdفي الحدث. أرسل كل المعلومات التي قد يحتاجها المستهلك (مثلuserId,totalAmount). هذا يمنع المستهلك من الاضطرار إلى الاتصال بخدمة المنتج مرة أخرى للحصول على تفاصيل، مما يقلل من الاقتران. - فكر في الفشل دائمًا: ماذا يحدث إذا فشل المستهلك في معالجة حدث؟ استخدم أنماطًا مثل “طابور الرسائل الميتة” (Dead Letter Queue – DLQ) لعزل الرسائل الفاشلة وتحليلها لاحقًا دون إيقاف بقية النظام.
- التوثيق هو مفتاح النجاة: وثّق كل حدث: ما هو، ما هي حقوله (Schema)، من ينتجه، ومن يستهلكه. استخدام “سجل مخططات الأحداث” (Event Schema Registry) هو استثمار ممتاز في الأنظمة الكبيرة.
الخلاصة: من الفوضى إلى التناغم 🎶
التحول إلى المعمارية القائمة على الأحداث كان نقلة نوعية لنا. نعم، الرحلة كانت صعبة في البداية وتطلبت تغييرًا في طريقة تفكيرنا، لكنها في النهاية أعطتنا نظامًا مرنًا، قويًا، وقابلًا للنمو دون خوف. لقد حولت فوضى الخدمات التي تصرخ في وجه بعضها البعض إلى أوركسترا متناغمة، حيث تعزف كل خدمة لحنها في الوقت المناسب دون أن تزعج الأخرى.
إذا كنت تشعر أن نظامك بدأ يصبح هشًا ومعقدًا، وأن أي تغيير صغير يهدد بكارثة، فربما حان الوقت لتفكر في “فك الاقتران” ومنح خدماتك مساحة لتتنفس. صدقني، مستقبلك ومستقبل فريقك سيشكرانك على هذا القرار. يلا يا شباب، فكوا هالاقتران! 😉