يا جماعة الخير، السلام عليكم ورحمة الله.
اسمحوا لي أن أرجع بالذاكرة ليلة لن أنساها ما حييت. كانت الساعة تقارب الثانية صباحًا، وأنا في سريري أحاول أن أغفو بعد يوم طويل من البرمجة. فجأة، بدأ هاتفي يهتز بلا توقف… رسائل على Slack، تنبيهات من نظام المراقبة، واتصالات من مدير المشروع. “أبو عمر، الحقنا! النظام واقع!”.
قفزت من سريري، فتحت اللابتوب، والقلب يدق بسرعة. لوحة المراقبة (Dashboard) كانت حمراء بالكامل، كأنها شجرة عيد ميلاد لكن بألوان الجحيم. كل الخدمات تصرخ وتشتكي من أخطاء “Timeout” و “Service Unavailable”. شو القصة؟ ما الذي يمكن أن يسقط نظامًا ضخمًا مبنيًا على معمارية الخدمات المصغرة (Microservices) بهذا الشكل الكارثي؟
بعد نصف ساعة من التحليل المحموم، اكتشفنا الجاني. لم يكن خدمة الدفع، ولا خدمة المصادقة، بل كانت خدمة صغيرة وبسيطة مسؤولة عن توليد صور مصغرة (Thumbnails) للمنتجات. هذه الخدمة كانت تواجه بطئًا شديدًا بسبب مشكلة في مساحة التخزين. لكن كيف لخدمة “هامشية” كهذه أن تسقط النظام بأكمله؟ هنا بدأت رحلتي الحقيقية مع فهم قوة وأهمية نمط تصميم يُدعى “قاطع الدائرة”.
ما هو جحيم الفشل المتتالي (Cascading Failures)؟
لنفهم ما حدث تلك الليلة، يجب أن نفهم طبيعة الأنظمة الموزعة. في معمارية الخدمات المصغرة، تتواصل الخدمات مع بعضها البعض عبر الشبكة. تخيل السيناريو التالي:
- خدمة “عرض المنتجات” (Product Service) تريد عرض صفحة منتج.
- للقيام بذلك، تحتاج لمعلومات من خدمة “المخزون” (Inventory Service) وصورة مصغرة من خدمة “الصور” (Image Service).
- خدمة “الصور” كانت هي المشكلة، كانت بطيئة جدًا ولا تستجيب.
ماذا حدث بالضبط؟
- طلب جديد يصل إلى “خدمة المنتجات”.
- “خدمة المنتجات” تطلب صورة من “خدمة الصور” وتنتظر… وتنتظر… وتنتظر.
- أثناء انتظارها، هي تحجز موارد (Thread, Connection).
- يصل طلب ثانٍ، وثالث، ورابع… كل طلب جديد يفتح اتصالاً جديدًا بـ “خدمة الصور” البطيئة ويحجز موارد إضافية.
- في غضون ثوانٍ، استُهلكت كل الموارد المتاحة في “خدمة المنتجات”. لم يعد بإمكانها خدمة أي طلبات جديدة، حتى تلك التي لا تحتاج إلى صور!
- الآن، “خدمة المنتجات” نفسها أصبحت لا تستجيب.
- أي خدمة أخرى تعتمد على “خدمة المنتجات” (مثل خدمة “عربة التسوق”) تبدأ بالفشل هي الأخرى.
وهكذا، مثل أحجار الدومينو، سقط النظام بأكمله بسبب مشكلة في خدمة واحدة. هذا هو “الفشل المتتالي”، وهو الكابوس الأكبر لكل مطور يعمل على أنظمة موزعة.
المُنقذ: نمط قاطع الدائرة (Circuit Breaker Pattern)
الفكرة عبقرية وبسيطة، ومستوحاة من قاطع الدائرة الكهربائي الموجود في منزلك. عندما يحدث تماس كهربائي، يفصل القاطع التيار ليحمي أجهزتك والمنزل من الحريق. نمط قاطع الدائرة البرمجي يفعل الشيء نفسه بالضبط: إنه يحمي نظامك من “الحريق” البرمجي.
بدلاً من السماح لخدمتك بالاتصال بشكل أعمى بخدمة أخرى قد تكون فاشلة، نضع “قاطع دائرة” بينهما. هذا القاطع يراقب حالة الاتصالات، وله ثلاث حالات رئيسية.
كيف يعمل قاطع الدائرة؟ الحالات الثلاث
لفهم الآلية، تخيل أن قاطع الدائرة هو حارس ذكي يقف على بوابة الاتصال بين خدمة (أ) وخدمة (ب).
-
الحالة المغلقة (Closed):
هذا هو الوضع الطبيعي. الحارس يسمح لجميع الطلبات بالمرور من (أ) إلى (ب). في نفس الوقت، هو يعدّ عدد المرات التي يفشل فيها الطلب (مثلاً، بسبب Timeout). إذا وصل عدد الأخطاء المتتالية إلى حد معين (مثلاً، 5 أخطاء في 30 ثانية)، يقرر الحارس أن هناك مشكلة جدية في الخدمة (ب).
-
الحالة المفتوحة (Open):
هنا يقوم الحارس “بفصل” الدائرة. لأي طلب جديد يحاول الوصول إلى الخدمة (ب)، يقوم الحارس برفضه فورًا دون حتى محاولة الاتصال. هذا هو السحر! بدلاً من أن تنتظر خدمتك (أ) وتستهلك مواردها، تحصل على رد فوري بالفشل. هذا الإجراء يحقق هدفين:
- حماية الخدمة الطالبة (أ): بمنعها من استهلاك مواردها في انتظار خدمة ميتة.
- إعطاء فرصة للخدمة الفاشلة (ب): بمنع إغراقها بطلبات إضافية وهي تحاول التعافي.
يبقى القاطع في هذه الحالة لمدة زمنية محددة (مثلاً، 60 ثانية).
-
الحالة نصف المفتوحة (Half-Open):
بعد انتهاء مدة “الفتح”، لا يعود القاطع مباشرة إلى الحالة “المغلقة”. بدلاً من ذلك، يدخل في حالة اختبار حذرة. يسمح الحارس بمرور طلب واحد فقط إلى الخدمة (ب).
- إذا نجح هذا الطلب: يعتبر القاطع أن الخدمة (ب) قد تعافت، فيعود إلى الحالة المغلقة (Closed) ويسمح بعودة حركة المرور الطبيعية.
- إذا فشل هذا الطلب: يستنتج القاطع أن المشكلة لا تزال قائمة، فيعود إلى الحالة المفتوحة (Open) ويبدأ فترة انتظار جديدة.
“ورجينا الشغل يا أبو عمر”: تطبيق عملي لقاطع الدائرة
هالحكي النظري جميل، لكن كيف نطبقه عمليًا؟ لحسن الحظ، لست بحاجة إلى بناء هذا المنطق من الصفر. هناك مكتبات رائعة جاهزة للاستخدام في معظم لغات البرمجة. من أشهرها:
- في عالم .NET/C#: مكتبة Polly هي المعيار الذهبي.
- في عالم Java: مكتبة Resilience4j هي الخيار الشائع.
- في عالم Go: مكتبة gobreaker ممتازة.
دعنا نأخذ مثالاً بسيطًا باستخدام مكتبة Polly في C#، لنرى كيف كنا سنحمي “خدمة المنتجات” من فشل “خدمة الصور”.
مثال باستخدام مكتبة Polly في C#
أولاً، نضيف مكتبة Polly إلى مشروعنا. ثم، بدلاً من استدعاء خدمة الصور مباشرة، سنقوم بتغليف الاستدعاء بسياسة قاطع الدائرة.
// في مكان ما عند تهيئة الخدمات (Startup.cs أو Program.cs)
// 1. تعريف سياسة قاطع الدائرة
var circuitBreakerPolicy = Policy
.Handle<HttpRequestException>() // حدد نوع الأخطاء التي ستؤثر على القاطع
.CircuitBreakerAsync(
exceptionsAllowedBeforeBreaking: 5, // اسمح بـ 5 أخطاء قبل فتح الدائرة
durationOfBreak: TimeSpan.FromSeconds(60) // اترك الدائرة مفتوحة لمدة 60 ثانية
);
// 2. تسجيل الـ HttpClient مع السياسة
services.AddHttpClient<IImageService, ImageService>()
.AddPolicyHandler(circuitBreakerPolicy);
// ... في داخل خدمة المنتجات ...
public class ProductService
{
private readonly IImageService _imageService;
public ProductService(IImageService imageService)
{
_imageService = imageService;
}
public async Task<ProductViewModel> GetProductDetails(int productId)
{
// ... (كود جلب تفاصيل المنتج) ...
string imageUrl;
try
{
// هذا الاستدعاء الآن محمي بواسطة قاطع الدائرة
imageUrl = await _imageService.GetThumbnailUrl(productId);
}
catch (BrokenCircuitException) // هذا الخطأ يُلقى عندما تكون الدائرة مفتوحة
{
// هنا يحدث السحر! لا ننتظر، بل نحصل على خطأ فوري
// يمكننا الآن التعامل مع الموقف بأناقة
imageUrl = "/images/default-thumbnail.png"; // استخدام صورة افتراضية
}
catch (HttpRequestException)
{
// هذا الخطأ يحدث عند فشل الطلب والدائرة لا تزال مغلقة
imageUrl = "/images/default-thumbnail.png";
}
// ... (إكمال بناء الـ ViewModel) ...
return productViewModel;
}
}
لاحظ كيف تغير المنطق. عندما تكون الدائرة مفتوحة، لن يتم إجراء استدعاء الشبكة أصلاً. بدلاً من ذلك، ستلقي Polly استثناءً من نوع BrokenCircuitException فورًا، والذي نلتقطه ونوفر قيمة احتياطية (صورة افتراضية). النظام يظل يعمل، وتجربة المستخدم لا تتأثر بشكل كارثي.
نصائح من الخبير: كيف تستخدم قاطع الدائرة كالمحترفين
تطبيق النمط هو البداية فقط. الخبرة علمتني بعض الدروس التي أود مشاركتها معكم.
نصيحة أبو عمر الأولى: لكل خدمة قاطعها الخاص.
لا تستخدم نفس إعدادات قاطع الدائرة لجميع الخدمات. خدمة الدفع الحرجة تحتاج إلى إعدادات مختلفة (مثلاً، حساسية أقل للفشل) عن خدمة إرسال الإشعارات غير الحرجة. خصص الأرقام (عدد الأخطاء، مدة الفتح) بناءً على أهمية الخدمة وطبيعتها.
نصيحة أبو عمر الثانية: التراجع الأنيق (Graceful Degradation) هو الأهم.
قاطع الدائرة يمنع الانهيار، لكن ماذا تعرض للمستخدم؟ خطأ؟ لا! أفضل شيء هو توفير “بديل”. هذا ما يسمى بالتراجع الأنيق. أمثلة:
- بيانات من الكاش: إذا فشلت في جلب بيانات حية، اعرض آخر بيانات ناجحة قمت بتخزينها مؤقتًا.
- قيمة افتراضية: كما في مثالنا، عرضنا صورة افتراضية.
- تجربة مستخدم مبسطة: ربما تخفي الجزء الذي يعتمد على الخدمة الفاشلة من الواجهة مؤقتًا.
دائمًا اسأل نفسك: “ماذا سيحدث عندما يفتح هذا القاطع؟” وجهز خطة بديلة.
نصيحة أبو عمر الثالثة: راقب قواطعك.
قاطع الدائرة الذي يفتح هو إنذار مبكر بوجود مشكلة. يجب أن يكون لديك نظام مراقبة (Monitoring) يسجل هذه الأحداث ويرسل لك تنبيهات. عندما ترى أن قاطع دائرة خدمة معينة بدأ يفتح بشكل متكرر، فهذه إشارة لك للتحرك وإصلاح المشكلة قبل أن يلاحظها المستخدمون.
الخلاصة: من الفوضى إلى المرونة 🚀
في تلك الليلة المشؤومة، تعلمنا بالطريقة الصعبة أن بناء نظام خدمات مصغرة دون التفكير في المرونة (Resilience) يشبه بناء منزل من ورق. نمط قاطع الدائرة لم يكن مجرد إصلاح تقني، بل كان تحولاً في طريقة تفكيرنا. لقد حول نظامنا من سلسلة دومينو هشة إلى شبكة مرنة يمكنها امتصاص الصدمات والتعافي بأناقة.
نصيحتي الأخيرة لك: لا تنتظر حتى يشتعل الحريق في نظامك لتركيب أجهزة الإنذار. ابدأ اليوم، راجع نقاط الاتصال بين خدماتك، وطبق نمط قاطع الدائرة. إنه استثمار صغير في الكود، ولكنه عائد ضخم في استقرار النظام وراحة بالك كمطور. وتذكر دائمًا، النظام القوي ليس الذي لا يفشل أبدًا، بل هو الذي يعرف كيف يفشل بأمان. والله ولي التوفيق.