مقدمة: ليلة إطلاق لن أنساها
يا جماعة الخير، اسمحولي أرجع بالذاكرة كم سنة لورا. كنا فريق صغير ومتحمس، وبنشتغل ليل نهار على إطلاق منصة تجارة إلكترونية جديدة مبنية بالكامل على معمارية الخدمات المصغرة (Microservices). كل شي كان جاهز، والكل متفائل. أطلقنا المنصة، وبدأنا أول حملة تخفيضات كبيرة.
في البداية، كانت الأجواء احتفالية. الطلبات بتوصل، والمبيعات بترتفع. لكن بعد حوالي ساعة، بدأت الكارثة. هواتف الدعم الفني لم تتوقف عن الرنين: “دفعت ثمن الطلبية بس ما إجاني تأكيد!”، “انخصم المبلغ من حسابي والطلبية ملغية!”، “المنتج اللي طلبته رجع ظهر إنه متوفر بعد ما دفعت حقه!”.
دخلت على قواعد البيانات لأفهم شو اللي بصير. كانت فوضى عارمة. خدمة الطلبات (Order Service) سجلت الطلب، وخدمة الدفع (Payment Service) خصمت المبلغ بنجاح، لكن خدمة المخزون (Inventory Service) فشلت في حجز المنتج لأنه نفد في آخر لحظة. النتيجة؟ بيانات غير متسقة بالمرة. زبون دفع مصاري على منتج مش موجود، وسجل الطلب في حالة غريبة عالق بين النجاح والفشل. قضينا هذيك الليلة والليالي اللي بعدها نصلّح البيانات يدوياً، وكان شعور أشبه بالكابوس. وقتها أدركت إن الطريقة التقليدية في التعامل مع المعاملات (Transactions) ما بتنفع في عالم الخدمات المصغرة الموزع. ومن هنا بدأت رحلتي مع منقذي، نمط “الساجا” (Saga Pattern).
المشكلة: لماذا تفشل المعاملات التقليدية (ACID) في الخدمات المصغرة؟
في الأنظمة التقليدية المتجانسة (Monolithic)، نعيش في نعيم معاملات الـ ACID (Atomicity, Consistency, Isolation, Durability). لو بدك تعمل طلبية، بتفتح معاملة واحدة في قاعدة البيانات، بتحدث جدول الطلبات، وبتحدث جدول المخزون، وبتحدث جدول المدفوعات، وبعدين بتعمل COMMIT. لو أي خطوة فشلت، بتعمل ROLLBACK وبيرجع كل شي زي ما كان. بسيطة وسهلة.
لكن في عالم الخدمات المصغرة، كل خدمة إلها قاعدة بياناتها الخاصة. خدمة الطلبات ما بتقدر مباشرة تحدث قاعدة بيانات خدمة المخزون. هذا ضد المبدأ الأساسي للخدمات المصغرة (Loose Coupling & Encapsulation). إذن، كيف نضمن إن سلسلة من العمليات الموزعة على خدمات مختلفة تتم كلها بنجاح أو تفشل كلها معًا؟ هذا هو التحدي الأكبر: إدارة المعاملات الموزعة (Distributed Transactions).
نصيحة من أبو عمر: محاولة تطبيق معاملات الـ (2PC – Two-phase commit) التقليدية في بيئة الخدمات المصغرة غالبًا ما تؤدي إلى كوارث. فهي تخلق اقترانًا شديدًا (Tight Coupling) وتقلل من توافرية النظام (Availability)، لأن كل الخدمات المشاركة يجب أن تكون متاحة ومتجاوبة في نفس اللحظة.
الحل الساحر: نمط الساجا (The Saga Pattern)
نمط الساجا ليس حلاً سحريًا، بل هو نهج عملي ومنطقي لإدارة فشل المعاملات الموزعة. الفكرة بسيطة جدًا في جوهرها:
الساجا هي سلسلة من المعاملات المحلية (Local Transactions) المترابطة. كل معاملة في السلسلة تقوم بتحديث قاعدة البيانات الخاصة بخدمتها، ثم تنشر حدثًا (Event) أو ترسل أمرًا (Command) لتشغيل المعاملة المحلية التالية في الخدمة التالية.
طيب، ماذا لو فشلت إحدى الخطوات في المنتصف؟ هنا يأتي دور المعاملات التعويضية (Compensating Transactions). لكل خطوة في الساجا، يجب أن يكون هناك خطوة معاكسة يمكنها التراجع عن تأثير الخطوة الأولى. فإذا فشلت الخطوة رقم 3، تقوم الساجا بتشغيل المعاملات التعويضية للخطوتين 2 و 1 بالترتيب العكسي لإعادة النظام إلى حالة متسقة.
هذا يقودنا إلى مفهوم الاتساق النهائي (Eventual Consistency). النظام قد يمر بحالة غير متسقة مؤقتًا، لكن الساجا تضمن أنه في النهاية سيصل إلى حالة مستقرة ومتسقة.
أنواع تطبيق نمط الساجا
هناك طريقتان رئيسيتان لتطبيق هذا النمط، ولكل منهما مزاياه وعيوبه.
1. التنسيق الراقص (Choreography)
في هذا النهج، لا يوجد منسق مركزي. كل خدمة تعرف ما يجب أن تفعله وتتفاعل مع أحداث الخدمات الأخرى. الأمر يشبه مجموعة من الراقصين المحترفين، كل منهم يعرف حركاته ومتى يؤديها بناءً على حركات الآخرين، دون وجود قائد يوجههم مباشرة.
- كيف يعمل: الخدمة الأولى تنفذ معاملتها المحلية ثم تنشر حدثًا (e.g.,
OrderCreated) عبر وسيط رسائل (Message Broker) مثل RabbitMQ أو Kafka. الخدمات الأخرى المهتمة بهذا الحدث (مثل خدمة الدفع) تستمع له، وتنفذ معاملاتها الخاصة، ثم تنشر أحداثها الخاصة (e.g.,PaymentProcessed)، وهكذا. - المزايا: بسيط في المفاهيم، لا يوجد نقطة فشل مركزية (No Single Point of Failure)، الخدمات منفصلة تمامًا (Loosely Coupled).
- العيوب: يصعب تتبع سير عمل الساجا ككل، خاصة مع زيادة عدد الخدمات. يمكن أن يؤدي إلى تبعيات دورية (Cyclic Dependencies) إذا لم يتم تصميمه بحذر.
مثال كود (Choreography – باستخدام Node.js و RabbitMQ كمثال توضيحي)
// OrderService.js
async function createOrder(orderData) {
// 1. Save order to local DB in 'PENDING' state
const order = await db.orders.save({ ...orderData, status: 'PENDING' });
// 2. Publish an event
const event = { type: 'OrderCreated', payload: { orderId: order.id, amount: order.total } };
await messageBroker.publish('order_events', event);
return order;
}
// PaymentService.js - Listens to 'order_events'
messageBroker.subscribe('order_events', async (event) => {
if (event.type === 'OrderCreated') {
try {
// 1. Process payment
await paymentGateway.charge(event.payload.amount);
// 2. Publish success event
const newEvent = { type: 'PaymentSuccessful', payload: { orderId: event.payload.orderId } };
await messageBroker.publish('payment_events', newEvent);
} catch (error) {
// Payment failed, publish a failure event to trigger compensation
const newEvent = { type: 'PaymentFailed', payload: { orderId: event.payload.orderId } };
await messageBroker.publish('payment_events', newEvent);
}
}
});
2. التنسيق المركزي (Orchestration)
في هذا النهج، يوجد “مايسترو” أو منسق مركزي (Orchestrator) يدير العملية بأكملها. المنسق هو المسؤول عن إخبار كل خدمة بما يجب أن تفعله ومتى. إذا فشلت إحدى الخطوات، فإن المنسق هو المسؤول عن استدعاء المعاملات التعويضية بالترتيب الصحيح.
- كيف يعمل: العميل يرسل طلبًا إلى المنسق. المنسق بدوره يرسل أوامر (Commands) بشكل متسلسل إلى الخدمات المشاركة. ينتظر المنسق ردًا من كل خدمة قبل أن يرسل الأمر التالي.
- المزايا: منطق سير العمل مركزي وواضح، أسهل في الفهم والتصحيح (Debugging)، لا توجد تبعيات دورية بين الخدمات.
- العيوب: المنسق يمكن أن يصبح نقطة فشل مركزية (Single Point of Failure)، ويضيف درجة من الاقتران (Coupling) حيث يجب على الخدمات أن تتوافق مع واجهة برمجة تطبيقات المنسق.
مثال كود (Orchestration – Pseudo-code)
class OrderSagaOrchestrator {
constructor(orderService, paymentService, inventoryService) {
this.orderService = orderService;
this.paymentService = paymentService;
this.inventoryService = inventoryService;
}
async execute(orderData) {
const executedSteps = [];
try {
// Step 1: Create Order
const order = await this.orderService.createOrder(orderData);
executedSteps.push('CreateOrder');
// Step 2: Process Payment
await this.paymentService.processPayment(order.id, order.amount);
executedSteps.push('ProcessPayment');
// Step 3: Reserve Inventory
await this.inventoryService.reserveInventory(order.id, order.items);
executedSteps.push('ReserveInventory');
// All successful, finalize the order
await this.orderService.approveOrder(order.id);
} catch (error) {
// Something went wrong, start compensation
this.compensate(executedSteps, orderData);
}
}
async compensate(steps, orderData) {
// Reverse the steps and compensate
for (const step of steps.reverse()) {
switch (step) {
case 'ReserveInventory':
await this.inventoryService.releaseInventory(orderData.id, orderData.items);
break;
case 'ProcessPayment':
await this.paymentService.refundPayment(orderData.id);
break;
case 'CreateOrder':
await this.orderService.cancelOrder(orderData.id);
break;
}
}
}
}
نصائح من خبرة أبو عمر
بعد سنوات من التعامل مع الساجا، تعلمت كم درس “على جلدي” كما نقول. إليكم الزبدة:
- اجعل عملياتك قابلة للتكرار (Idempotent): هذه أهم نصيحة. في الأنظمة الموزعة، قد تصل الرسالة أو يستدعى الإجراء أكثر من مرة. يجب أن تصمم خدماتك ومعاملاتك التعويضية بحيث لو تم استدعاؤها 5 مرات يكون لها نفس تأثير استدعائها مرة واحدة. تخيل لو تم خصم المبلغ من الزبون مرتين! استخدم معرفات فريدة (Unique IDs) لكل عملية لتجنب هذا.
- المعاملة التعويضية ليست مجرد “تراجع”: التراجع عن عملية دفع ليس حذف سجل الدفع، بل هو إنشاء معاملة “استرداد” (Refund). التراجع عن حجز مخزون ليس حذف الحجز، بل هو “تحرير” المخزون ليعود متاحًا. فكر جيدًا في المنطق التجاري لكل خطوة تعويضية.
- متى تختار Choreography ومتى Orchestration؟ قاعدتي الشخصية:
- استخدم Choreography للعمليات البسيطة التي تشمل 2-3 خدمات. هي أسرع في التطوير وأكثر مرونة.
- استخدم Orchestration عندما يتجاوز عدد الخدمات 4 أو عندما يكون سير العمل معقدًا ويحتاج إلى منطق شروطي (Conditional Logic) واضح. الرؤية المركزية هنا لا تقدر بثمن.
- الرصد والتتبع هما عيناك في الظلام: بدون وجود معرف تتبع موحد (Correlation ID) يمر عبر كل الخدمات المشاركة في الساجا، فإن تصحيح الأخطاء سيكون جحيمًا. استثمر في أدوات الرصد (Monitoring) والتتبع الموزع (Distributed Tracing) مثل Jaeger أو Zipkin.
الخلاصة: الساجا ليست رفاهية، بل ضرورة 🚀
التحول إلى معمارية الخدمات المصغرة يفتح آفاقًا واسعة من المرونة والقابلية للتوسع، لكنه يأتي مع تحدياته الخاصة، وعلى رأسها اتساق البيانات. نمط الساجا ليس مجرد نمط تصميمي جميل، بل هو أداة أساسية وضرورية في صندوق أدوات أي مطور يتعامل مع الأنظمة الموزعة.
قد يبدو الأمر معقدًا في البداية، ولكنه يحول الفوضى المحتملة للمعاملات الفاشلة إلى عملية منظمة يمكن التنبؤ بها والتعامل معها. تذكر دائمًا القصة التي بدأت بها: كل طلبية كانت مغامرة محفوفة بالمخاطر. الآن، مع تطبيق نمط الساجا، كل طلبية هي مجرد قصة (Saga) لها بداية ووسط ونهاية واضحة، حتى لو تخللتها بعض المنعطفات غير المتوقعة.
نصيحتي الأخيرة لك: لا تخف من الفشل، بل خطط له. هذا هو جوهر الهندسة البرمجية القوية والمرنة.