يا جماعة الخير، السلام عليكم ورحمة الله وبركاته. معكم أخوكم أبو عمر.
بتذكر هذيك الليلة، كانت الساعة حوالي 2 بعد منتصف الليل، وكنا بنحضر لإطلاق ميزة جديدة في منصة تجارة إلكترونية كنا شغالين عليها. الأجواء كانت متوترة ومผสมة بالحماس، قهوة ورا قهوة، والكل مركز في شاشته. الميزة كانت عبارة عن نظام طلبات متكامل: المستخدم بيعمل طلب، الدفع بيتم، المخزون بيتحدث، وشركة الشحن بتاخد إشعار. كل خطوة من هدول كانت عبارة عن “خدمة مصغرة” (Microservice) منفصلة.
أطلقنا الميزة… وفي الدقائق الأولى، كل شيء كان يبدو تمام. لكن فجأة، بدأت توصلنا بلاغات غريبة. مستخدم بحكي “دفعت الفلوس وانسحبت من حسابي، بس الطلب عندي حالته ‘ملغي’!”. ومستخدم ثاني بحكي “وصلني إيميل تأكيد شحن لمنتج أنا ما دفعته أصلاً!”.
دخلنا على قواعد البيانات، وهون كانت الصدمة. خدمة الدفع (Payment Service) سجلت عملية دفع ناجحة، لكن خدمة المخزون (Inventory Service) فشلت في حجز المنتج لأنه كان آخر قطعة وخدمة تانية حجزته بنفس الثانية. النتيجة؟ بيانات غير متسقة بالمرة. خدمة بتحكي “تم”، وخدمة تانية بتحكي “لأ فشل”. كانت فوضى عارمة، وشغلة بتجلط بمعنى الكلمة. قضينا باقي الليلة بنصلح البيانات يدوياً ونعتذر للعملاء. في هذيك اللحظة أدركت إننا وقعنا في الفخ الكلاسيكي للأنظمة الموزعة: فشل المعاملات الموزعة (Distributed Transactions). ومن هنا بدأت رحلتنا مع نمط الساجا.
ما هي مشكلة المعاملات في الخدمات المصغرة؟
في النظام التقليدي المتجانس (Monolith)، لما بدك تعمل عملية مكونة من عدة خطوات (زي تسجيل مستخدم جديد وإرسال إيميل ترحيبي)، كنت بتحطهم كلهم داخل “معاملة” (Transaction) واحدة في قاعدة البيانات. لو أي خطوة فشلت، قاعدة البيانات بتعمل “Rollback” تلقائياً وبترجع كل شيء زي ما كان. سهلة وبسيطة.
لكن في عالم الخدمات المصغرة، كل خدمة إلها قاعدة بياناتها الخاصة. خدمة الطلبات (Orders) ما بتقدر تبدأ معاملة تشمل قاعدة بيانات خدمة الدفع (Payments) وقاعدة بيانات خدمة المخزون (Inventory). هذا مستحيل تقنياً، لأنه بخالف مبدأ استقلالية الخدمات. طيب شو الحل؟
الحل التقليدي القديم كان اسمه “Two-Phase Commit (2PC)”، لكنه حل معقد جداً وبخلق ترابط قوي (tight coupling) بين الخدمات، وهذا عكس الهدف من الخدمات المصغرة أساساً. لهيك، أغلب المعماريين اليوم بتجنبوه.
بطل الحكاية: نمط الساجا (The Saga Pattern)
نمط الساجا هو ببساطة أسلوب لإدارة اتساق البيانات عبر خدمات متعددة بدون الحاجة لمعاملات موزعة. الفكرة بسيطة: بدل ما نحاول نعمل معاملة واحدة كبيرة، بنقسم العملية لسلسلة من المعاملات المحلية (Local Transactions) الصغيرة، كل واحدة بتصير داخل خدمة واحدة.
الفكرة الجوهرية في الساجا هي: لكل خطوة (معاملة محلية) تنجح، لازم يكون في خطوة عكسية (Compensating Transaction) تقدر تلغي أثرها في حال فشلت خطوة لاحقة في السلسلة.
خلونا نرجع لمثالنا، عملية الطلب:
- خدمة الطلبات: تنشئ طلب بحالة “قيد الانتظار” (Pending). (معاملة محلية 1)
- خدمة الدفع: تعالج الدفع. (معاملة محلية 2)
- خدمة المخزون: تحجز المنتج من المخزون. (معاملة محلية 3)
- خدمة الشحن: تنشئ طلب شحن. (معاملة محلية 4)
طيب، ماذا لو فشلت الخطوة 3 (خدمة المخزون)؟ هنا يأتي دور المعاملات العكسية:
- خدمة المخزون فشلت، فما في داعي لعملية عكسية لها.
- الساجا تبدأ بتشغيل العمليات العكسية للخطوات الناجحة السابقة:
- خدمة الدفع: تنفذ معاملة عكسية وهي “إرجاع المبلغ” (Refund).
- خدمة الطلبات: تنفذ معاملة عكسية وهي “تغيير حالة الطلب إلى ملغي” (Cancel Order).
بهذه الطريقة، حتى لو فشلت العملية في المنتصف، النظام برجع لحالة متسقة وصحيحة. ما في فلوس مسحوبة بدون طلب، ولا طلب مؤكد بدون منتج محجوز.
طرق تطبيق نمط الساجا
الحكي بينا، في طريقتين مشهورات لتطبيق الساجا، وكل وحدة إلها ميزاتها وعيوبها.
1. الكوريغرافيا (Choreography)
في هذا النموذج، ما في قائد أو منسق مركزي. كل خدمة بعد ما تخلص شغلها، بتنشر “حدث” (Event) معين. والخدمات الأخرى المهتمة بتسمع لهذا الحدث وبتنفذ شغلها بناءً عليه.
مثال (سيناريو الطلب):
OrderServiceتنشئ الطلب وتنشر حدثOrderCreated.PaymentServiceتستمع لحدثOrderCreated، تعالج الدفع، ثم تنشر حدثPaymentProcessed.InventoryServiceتستمع لحدثPaymentProcessed، تحجز المخزون، ثم تنشر حدثInventoryUpdated.- … وهكذا.
ماذا لو فشل الدفع؟
PaymentService ستنشر حدث PaymentFailed. خدمة OrderService (اللي بدأت كل شيء) رح تكون بتستمع لهذا الحدث، ولما يوصلها، بتنفذ المعاملة العكسية وبتلغي الطلب.
// OrderService
function createOrder(data) {
const order = db.orders.create({ ...data, status: 'PENDING' });
eventBus.publish('OrderCreated', { orderId: order.id, amount: data.amount });
return order;
}
// PaymentService
eventBus.subscribe('OrderCreated', async (event) => {
try {
await paymentGateway.charge(event.amount);
eventBus.publish('PaymentProcessed', { orderId: event.orderId });
} catch (error) {
eventBus.publish('PaymentFailed', { orderId: event.orderId, reason: 'Insufficient funds' });
}
});
// OrderService (again, listening for failure)
eventBus.subscribe('PaymentFailed', async (event) => {
const order = db.orders.findById(event.orderId);
order.status = 'CANCELLED';
await order.save();
// Maybe notify the user
});
- الميزات: بسيط، لا يوجد نقطة فشل مركزية (no single point of failure)، الخدمات منفصلة تماماً (loosely coupled).
- العيوب: صعب تتبع سير العملية ككل. لما يزيد عدد الخدمات، بتصير زي حفلة فوضوية ما بتعرف مين بحكي مع مين.
2. الأوركسترا (Orchestration)
هنا، يوجد “مايسترو” أو منسق (Orchestrator). هو عبارة عن خدمة مخصصة وظيفتها الوحيدة هي تنسيق سير عمل الساجا. هو اللي بحكي لكل خدمة شو تعمل ومتى.
مثال (سيناريو الطلب):
الـ OrderOrchestrator يبدأ العمل:
- يرسل أمر لـ
OrderService: “أنشئ طلب”. ينتظر الرد. - إذا نجح، يرسل أمر لـ
PaymentService: “اخصم المبلغ”. ينتظر الرد. - إذا نجح، يرسل أمر لـ
InventoryService: “احجز المنتج”. ينتظر الرد. - وهكذا…
ماذا لو فشل الدفع؟
الـ Orchestrator سيكتشف الفشل (لأن PaymentService سترجع خطأ). عندها، سيبدأ هو بإرسال أوامر العمليات العكسية بالترتيب المعاكس:
- يرسل أمر لـ
OrderService: “ألغِ الطلب رقم X”.
// OrderOrchestrator
async function executeOrderSaga(data) {
let orderId;
try {
// Step 1: Create Order
const orderResponse = await orderService.createOrder(data);
orderId = orderResponse.id;
// Step 2: Process Payment
await paymentService.processPayment({ orderId, amount: data.amount });
// Step 3: Update Inventory
await inventoryService.updateInventory({ orderId, items: data.items });
// If all successful, mark saga as complete
await sagaRepo.update(orderId, { status: 'COMPLETED' });
} catch (error) {
// Saga failed, start compensation
console.error(`Saga for order ${orderId} failed. Starting compensation.`);
await compensate(orderId, error.step);
}
}
async function compensate(orderId, failedStep) {
if (failedStep === 'Payment' || failedStep === 'Inventory') {
// Compensate order creation
await orderService.cancelOrder({ orderId });
}
if (failedStep === 'Inventory') {
// Compensate payment
await paymentService.refundPayment({ orderId });
}
}
- الميزات: منطق العملية كله مركزي وواضح، أسهل في التتبع والمراقبة، إدارة الأخطاء أبسط.
- العيوب: المنسق نفسه ممكن يصير نقطة فشل مركزية. خطر أن يصبح هذا المنسق “خدمة إله” (God Service) تعرف كل شيء عن كل الخدمات الأخرى، مما يزيد الترابط.
نصائح أبو عمر العملية من قلب المعركة
بعد ما تبنينا نمط الساجا، تعلمنا كم درس مهم “بالطريقة الصعبة”. خذوا هالنصائح من الآخر:
- اجعل عملياتك العكسية بسيطة ومضمونة النجاح: عملية “إرجاع المبلغ” (Refund) يجب أن تكون مصممة بحيث لا تفشل أبداً (مثلاً، لا تعتمد على خدمة خارجية قد تكون معطلة). يجب أن تكون عملية موثوقة جداً.
- الـ Idempotency هي مفتاح النجاة: تأكد أن عملياتك (العادية والعكسية) تكون “Idempotent”. يعني لو تم استدعاؤها أكثر من مرة بنفس المدخلات، النتيجة تكون واحدة. مثلاً، لو وصل أمر “اخصم 100 دولار للطلب رقم 123” مرتين بسبب مشكلة في الشبكة، يجب أن يتم الخصم مرة واحدة فقط. هذا يمنع الكوارث.
- المراقبة والتتبع (Observability) ليست رفاهية: بدون أدوات تتبع (Tracing) وسجلات (Logs) واضحة، تتبع فشل ساجا مكونة من 10 خطوات هو كابوس حقيقي. لازم يكون عندك طريقة تشوف فيها كل خطوة في الساجا، وين نجحت ووين فشلت.
- متى تختار كوريغرافيا ومتى أوركسترا؟ نصيحتي: إذا كانت العملية بسيطة (2-4 خدمات)، ابدأ بالكوريغرافيا (Choreography) فهي أسرع وأبسط. إذا كانت العملية معقدة، أو تحتاج منطق تفريع (if/else)، أو تشمل عدداً كبيراً من الخدمات، فالأوركسترا (Orchestration) هي خيارك الأفضل لأنها تمنحك تحكماً ورؤية أوضح.
الخلاصة… ومن الآخر 💡
الانتقال إلى معمارية الخدمات المصغرة رحلة مثيرة ومليئة بالفوائد، لكنها تأتي مع تحدياتها الخاصة، وأكبرها هو ضمان اتساق البيانات. تجاهل هذه المشكلة في البداية سيكلفك الكثير من الليالي الطوال وعملاء غاضبين.
نمط الساجا (Saga Pattern) ليس حلاً سحرياً، بل هو استراتيجية قوية ومنطقية للتعامل مع هذه الفوضى. سواء اخترت بساطة الكوريغرافيا أو تحكم الأوركسترا، فإن تبني هذا النمط سيمنحك الثقة لبناء أنظمة موزعة قوية وقادرة على التعافي من الأخطاء بشكل تلقائي.
لا تخافوا من الأنظمة الموزعة، فقط افهموا تحدياتها جيداً، واستخدموا الأنماط الصحيحة للتغلب عليها. والله ولي التوفيق.