يا جماعة الخير، السلام عليكم ورحمة الله وبركاته. معكم أخوكم أبو عمر.
قبل كم سنة، كنا في خضم حماس الانتقال من تطبيقنا المونوليثي (Monolith) الضخم إلى معمارية الخدمات المصغرة (Microservices). الكل كان مبسوط، الفريق متحمس للتقنيات الجديدة، والسرعة، والمرونة اللي وعدتنا فيها هالمعمارية. أطلقنا أول أجزاء النظام الجديد، وكان نظام الطلبات في متجرنا الإلكتروني.
في ليلة من الليالي، وأنا قاعد بشرب كاسة الشاي بالنعناع وبراقب لوحات المراقبة (Dashboards)، رن جوالي. كان صوت مدير قسم خدمة العملاء على الطرف الثاني متوتر: “أبو عمر، الحقنا! في عنا طلبية مكتملة، والبضاعة انخصمت من المستودع، بس الفلوس ما وصلت! والزبون معصّب وبحكي انه الدفع فشل عنده”.
قلبي وقتها “نقزني نقزة”. كيف يعني الطلب مكتمل والدفع فاشل؟ فتحت سجلات النظام (Logs) بسرعة، ولقيت الكارثة اللي كنت خايف منها. القصة كانت كالتالي:
- خدمة الطلبات (Order Service) استلمت الطلب بنجاح وسجلته في قاعدة بياناتها.
- خدمة الطلبات أرسلت أمر لخدمة المستودع (Inventory Service) عشان تخصم القطع المطلوبة. ونجحت العملية.
- خدمة الطلبات أرسلت أمر لخدمة الدفع (Payment Service) عشان تسحب المبلغ من بطاقة العميل. وهنا، “علّق النظام”! خدمة الدفع فشلت في إتمام العملية بسبب مشكلة مؤقتة في بوابة الدفع الخارجية.
النتيجة؟ خدمة الطلبات والمستودع بتعتقد إن الطلب ناجح، بينما خدمة الدفع (والعميل) بيعرفوا إنه فاشل. بياناتنا صارت في حالة فوضى وغير متناسقة. قضينا ساعات طويلة في هذاك اليوم نصلّح البيانات يدوياً ونتواصل مع العميل عشان نعتذر. وقتها قلت لحالي: “لهون وبس، لازم نلاقي حل جذري. شغل الترقيع هاد ما بزبط”. ومن هنا بدأت رحلتي الحقيقية مع نمط تصميم اسمه “الساجا” (Saga Pattern).
لماذا فشلت الطريقة التقليدية؟ (مشكلة المعاملات الموزعة)
في عالم التطبيقات المونوليثية القديمة، كانت الحياة أسهل. كل شيء موجود في مكان واحد وقاعدة بيانات واحدة. لما كنا نعمل عملية زي عملية الطلب، كنا نستخدم إشي اسمه “المعاملات” أو Transactions (وتحديداً ACID Transactions).
ببساطة، المعاملة بتضمن إنه مجموعة من العمليات يا إما بتنجح كلها مع بعض، أو بتفشل كلها مع بعض. لو فشلت خطوة واحدة، النظام تلقائياً بعمل “Rollback” أو تراجع عن كل الخطوات السابقة. زي كأنك بتقول للنظام: “يا كل إشي بتم، يا ولا إشي بتم”.
لكن في عالم الخدمات المصغرة، كل خدمة إلها قاعدة بياناتها الخاصة. خدمة الطلبات إلها قاعدة بياناتها، وخدمة الدفع إلها قاعدة بياناتها، وهلم جرا. محاولة تطبيق معاملة واحدة (Transaction) تشمل كل قواعد البيانات هاي هو كابوس تقني بنسميه “المعاملات الموزعة” (Distributed Transactions) باستخدام بروتوكول مثل (Two-Phase Commit – 2PC).
هاي الطريقة معقدة جداً، وبتبطئ النظام، والأهم من هيك، بتخلق اعتمادية (Coupling) قوية بين الخدمات، وهذا بعكس الهدف الأساسي من الخدمات المصغرة اللي هو الاستقلالية. باختصار، هالطريقة مش عملية بالمرة في الأنظمة الحديثة.
الحل السحري: نمط الساجا (Saga Pattern)
هنا يأتي دور نمط الساجا. الساجا مش معاملة (Transaction) بالمعنى التقليدي، بل هي طريقة لإدارة تناسق البيانات عبر خدمات متعددة بدون الحاجة لقفل قواعد البيانات.
الفكرة الأساسية للساجا هي: “سلسلة من المعاملات المحلية (Local Transactions)”.
كل خطوة في عمليتنا الموزعة (مثل إنشاء طلب، معالجة الدفع، تحديث المخزون) هي معاملة محلية بتصير داخل خدمة واحدة فقط. لما تنجح هاي المعاملة المحلية، الخدمة بتطلق حدث (Event) أو أمر (Command) عشان تبدأ المعاملة المحلية التالية في الخدمة اللي بعدها.
السر الأهم في الساجا: لكل إجراء (Action) تقوم به، يجب أن يكون هناك “إجراء تعويضي” (Compensating Action) مقابل له.
فإذا فشلت أي خطوة في منتصف السلسلة، يقوم نمط الساجا بتشغيل الإجراءات التعويضية بالترتيب العكسي لكل الخطوات السابقة التي نجحت. هذا الإجراء التعويضي وظيفته “إلغاء” أثر الإجراء الأصلي. خلينا نطبق هاد الحكي على مثالنا:
- الإجراء: خدمة المستودع تخصم قطعة من المخزون.
- الإجراء التعويضي: خدمة المستودع تضيف قطعة إلى المخزون (إلغاء الخصم).
- الإجراء: خدمة الدفع تسحب المبلغ.
- الإجراء التعويضي: خدمة الدفع تعيد المبلغ (Refund).
بهذه الطريقة، إذا فشل الدفع، ستقوم الساجا تلقائياً بتشغيل الإجراء التعويضي لإلغاء حجز القطعة في المستودع، وإلغاء الطلب في خدمة الطلبات، وبالتالي تعود حالة النظام إلى وضع متناسق. هذا ما يسمى بـ “التناسق النهائي” (Eventual Consistency).
أنواع الساجا: كيف ننسق الأوركسترا؟
هناك طريقتان أساسيتان لتطبيق نمط الساجا، وكل طريقة إلها حسناتها وسيئاتها.
1. التنسيق الكوريغرافي (Choreography)
في هذا النوع، لا يوجد منسق مركزي. كل خدمة بتعرف شو لازم تعمل لما تسمع حدث معين من خدمة أخرى. الأمر أشبه برقصة جماعية مدرب عليها جيداً، كل راقص بيعرف حركته التالية لما يشوف الراقص اللي قبله أنهى حركته.
كيف يعمل:
- خدمة الطلبات: تنشئ الطلب بحالة “قيد الإنشاء”، ثم تنشر حدث
OrderCreatedEvent. - خدمة الدفع: تستمع لهذا الحدث. عند استلامه، تحاول معالجة الدفع.
- إذا نجح الدفع، تنشر حدث
PaymentProcessedEvent. - إذا فشل الدفع، تنشر حدث
PaymentFailedEvent.
- إذا نجح الدفع، تنشر حدث
- خدمة المستودع: تستمع لحدث
PaymentProcessedEvent. عند استلامه، تخصم المخزون وتنشر حدثInventoryUpdatedEvent. - خدمة الطلبات: تستمع لحدث
InventoryUpdatedEventلتغيير حالة الطلب إلى “مكتمل”، وتستمع أيضاً لحدثPaymentFailedEventلتغيير حالة الطلب إلى “ملغي”.
الميزات: بسيطة في العمليات القصيرة، لا توجد نقطة فشل مركزية (No Single Point of Failure)، الخدمات منفصلة تماماً عن بعضها.
العيوب: يصعب تتبع سير العملية، خصوصاً مع زيادة عدد الخدمات. ممكن تصير فوضى “مين بيسمع لمين؟”. تصبح الصورة الكبيرة للعملية موزعة ومخفية داخل كل خدمة.
2. التنسيق الأوركسترالي (Orchestration)
في هذا النوع، يوجد “مايسترو” أو منسق (Orchestrator). هذا المنسق هو خدمة أو مكون مسؤول عن إدارة سير عمل الساجا بأكمله. هو الذي يخبر كل خدمة ماذا تفعل ومتى.
كيف يعمل:
لدينا خدمة جديدة اسمها OrderOrchestrator.
- العميل يرسل طلب إنشاء طلبية إلى المنسق.
- المنسق: يطلب من خدمة الطلبات إنشاء طلب.
- إذا نجح، المنسق: يطلب من خدمة الدفع معالجة الدفع.
- إذا نجح، المنسق: يطلب من خدمة المستودع تحديث المخزون.
- إذا نجح، المنسق: يطلب من خدمة الطلبات تحديث حالة الطلب إلى “مكتمل”.
وإذا فشلت خطوة؟ لنفترض أن خطوة الدفع (رقم 3) فشلت. يقوم المنسق بتشغيل الإجراءات التعويضية بالترتيب العكسي:
- المنسق: يطلب من خدمة الطلبات إلغاء الطلب (الإجراء التعويضي للخطوة 2).
الميزات: منطق العملية مركزي وواضح، أسهل في الفهم والتعديل والمراقبة، يمكن تتبع حالة الساجا بسهولة.
العيوب: المنسق قد يصبح نقطة فشل مركزية، هناك خطر إضافة منطق أعمال (Business Logic) زائد داخل المنسق بدلاً من إبقائه في الخدمات المختصة.
كمثال بسيط جداً على منطق المنسق بلغة تشبه الجافاسكريبت:
class OrderOrchestrator {
async createOrder(orderData) {
let orderId;
let paymentId;
try {
// Step 1: Create Order
const order = await orderService.create(orderData);
orderId = order.id;
// Step 2: Process Payment
const payment = await paymentService.process(orderId, orderData.paymentDetails);
paymentId = payment.id;
// Step 3: Update Inventory
await inventoryService.deduct(orderData.items);
// Step 4: Mark order as complete
await orderService.complete(orderId);
return { success: true, orderId: orderId };
} catch (error) {
console.error("Saga failed! Starting compensation...", error);
// Compensation logic (runs in reverse)
if (paymentId) {
// Compensate for successful payment
await paymentService.refund(paymentId);
}
if (orderId) {
// Compensate for created order
await orderService.cancel(orderId);
}
return { success: false, error: "Order could not be processed." };
}
}
}
نصائح من “أبو عمر” لتطبيق الساجا بنجاح
بعد التجربة والخطأ، تعلمت شوية دروس “عالصعب”، وحابب أشارككم إياها:
- ابدأ بالبساطة: مش كل إشي بده تعقيد يا جماعة. إذا كانت عمليتك تشمل خدمتين أو ثلاث فقط، قد يكون نمط الكوريغرافيا (Choreography) كافياً وأسرع في التطبيق. الأوركسترا (Orchestration) رائعة للعمليات المعقدة والطويلة.
- اجعل الإجراءات قابلة للتكرار (Idempotent): تخيل لو أن خدمة الدفع استلمت نفس الأمر مرتين بسبب خطأ في الشبكة، هل ستخصم المبلغ مرتين؟ يجب أن تصمم خدماتك بحيث لو استلمت نفس الطلب أكثر من مرة، تكون النتيجة واحدة. هذا مبدأ مهم جداً في الأنظمة الموزعة.
- صمم الإجراءات التعويضية بعناية فائقة: الإجراء التعويضي يجب أن يكون موثوقاً قدر الإمكان. ماذا لو فشل إجراء “إعادة المبلغ” (Refund)؟ هذه كارثة بحد ذاتها. يجب أن تكون هذه الإجراءات بسيطة جداً ولديها آليات إعادة محاولة قوية.
- فكر في العزل (Isolation): خلال تنفيذ الساجا، بياناتك ستكون في حالة “غير متناسقة مؤقتاً”. هذا طبيعي. لكن يجب أن تفكر كيف ستتعامل واجهة المستخدم مع هذا. مثلاً، بدلاً من إظهار “تمت الطلبية” فوراً، أظهر “جاري معالجة الطلب…” حتى تكتمل الساجا بنجاح.
- المراقبة والتسجيل (Logging & Monitoring): في الأنظمة الموزعة، بدون سجلات (Logs) واضحة ومراقبة مركزية، أنت كالأعمى في الظلام. يجب أن يكون لديك نظام يمكنك من تتبع كل خطوة في الساجا عبر جميع الخدمات لتعرف بالضبط أين ومتى حدث الخطأ.
الخلاصة: الساجا ليست عصا سحرية، بل أداة قوية 🧠
نمط الساجا لم يحل مشكلتي بضغطة زر، بل تطلب مني ومن فريقي إعادة التفكير في طريقة تصميمنا للعمليات. هو ليس حلاً لكل المشاكل، بل هو أداة قوية في صندوق أدوات مهندس البرمجيات للتعامل مع واقع الأنظمة الموزعة.
الانتقال للخدمات المصغرة يعني تبني عقلية “التناسق النهائي” بدلاً من “التناسق الفوري” الذي اعتدنا عليه. الساجا هي الجسر الذي يساعدنا على عبور هذه الفجوة بأمان وثقة.
نصيحتي الأخيرة لكل مطور أو معماري برمجيات يفكر في هذا الطريق: ادرس نمط الساجا جيداً، افهم أنواعه، وجرّبه في مشروع صغير قبل تطبيقه على نظامك الأساسي. الاستثمار في فهم هذه الأنماط اليوم سيوفر عليك الكثير من ليالي “الشاي بالنعناع” المتوترة في المستقبل. 💪
ودمتم سالمين.