يا جماعة الخير، السلام عليكم ورحمة الله.
اسمحوا لي أبدأ معكم بقصة صارت معي قبل كم سنة، قصة ما بنساها لأنها علمتني درس قاسي ومهم بنفس الوقت. كنا في عز الشغل، يوم خميس بالليل، والمفروض إنه الأمور هادية والكل بيستعد لعطلة نهاية الأسبوع. أطلقنا حملة تسويقية جديدة، والضغط على النظام كان عالي، لكن كل شيء كان تحت السيطرة… أو هيك كنا مفكرين.
فجأة، بدأت التنبيهات توصل على تلفوني زي المطر. “System is slow”, “High CPU usage”, “503 Service Unavailable”. قلبي بلش يدق بسرعة. فتحت اللابتوب بسرعة البرق، ودخلت على لوحة المراقبة (Dashboard) لأشوف شو القصة. المنظر كان مرعب: كل الخوادم الرئيسية استخدام المعالج فيها 100%، والطلبات الجديدة كلها بتفشل.
أول إشي فكرت فيه إنه المشكلة في قاعدة البيانات، أو في الخدمة الرئيسية تبعتنا. لكن بعد ما فحصنا كل إشي، كانت كل المؤشرات بتحكي إنه خدماتنا الأساسية سليمة! طيب شو اللي صاير؟ بعد ساعة من التوتر والبحث المحموم، واحد من الشباب صاح فينا: “يا جماعة، خدمة الإشعارات (Notification Service) واقعة!”.
خدمة الإشعارات كانت خدمة صغيرة، وظيفتها تبعث إيميل أو إشعار للمستخدم بعد ما يعمل عملية معينة. مش إشي حرج، يعني لو ما اشتغلت لكم دقيقة مش نهاية العالم. لكن الكارثة كانت في طريقة تعامل نظامنا مع فشلها. كل طلب كان يستدعي هاي الخدمة، كان يضل معلق بستنى ردها اللي عمره ما رح يجي. كل طلب معلق يعني “thread” محجوز في الخادم. خلال دقائق، كل الـ threads انحجزت، والخادم بطل قادر يستقبل أي طلب جديد، حتى لو كان طلب بسيط ما إله علاقة بخدمة الإشعارات. النظام انهار بالكامل بسبب خدمة ثانوية. هاي الليلة، تعلمنا بالطريقة الصعبة معنى “الفشل المتتالي” أو الـ Cascading Failure.
هذه الحادثة كانت نقطة تحول في طريقة تفكيرنا ببناء الأنظمة. ومن رحم هذه المعاناة، ولد بطلنا المنقذ: نمط قاطع الدائرة (Circuit Breaker Pattern).
ما هو جحيم الفشل المتتالي (Cascading Failure)؟
قبل ما نحكي عن الحل، خلينا نفهم المشكلة بعمق. تخيل شارع سريع فيه عدة مسارات. فجأة، سيارة صغيرة بتعطل في مسار واحد. في البداية، السيارات اللي وراها بتبطئ وبتتكدس. بعد شوي، السائقين بملّوا وبحاولوا ينتقلوا للمسارات الثانية، وهيك بسببوا إرباك وبطء في كل المسارات. خلال فترة قصيرة، الشارع السريع كله بصير عبارة عن موقف سيارات كبير، وحركة السير بتنصاب بشلل تام. كله بسبب سيارة واحدة تعطلت.
هذا بالضبط ما يحدث في عالم الخدمات المصغرة (Microservices). نظامك مكون من عدة خدمات تتواصل مع بعضها. خدمة (أ) تستدعي خدمة (ب). إذا خدمة (ب) أصبحت بطيئة أو توقفت عن العمل، خدمة (أ) ستظل تنتظر الرد. خلال هذا الانتظار، هي تستهلك موارد (threads, memory, CPU). إذا كان هناك ضغط طلبات عالٍ على خدمة (أ)، فكل هذه الطلبات ستتراكم وهي تنتظر خدمة (ب). سريعاً ما تستنفد خدمة (أ) كل مواردها وتنهار هي الأخرى. والآن، أي خدمة أخرى تعتمد على خدمة (أ) ستعاني من نفس المصير. وهكذا، ينتشر الفشل كالنار في الهشيم في النظام بأكمله.
بطلنا المنقذ: نمط قاطع الدائرة (Circuit Breaker)
هنا يأتي دور نمط “قاطع الدائرة”. الفكرة مستوحاة مباشرة من قاطع الدائرة الكهربائي الموجود في كل بيت. وظيفته بسيطة: إذا حدث حمل كهربائي زائد أو ماس كهربائي، القاطع “يفصل” الدائرة لمنع حدوث حريق أو تلف في الأجهزة.
في البرمجة، قاطع الدائرة هو كائن (object) يغلف استدعاء العمليات التي قد تفشل (مثل استدعاء خدمة عبر الشبكة). هذا الكائن يراقب حالات الفشل، وإذا زاد عددها عن حد معين، “يفتح الدائرة”، أي أنه يتوقف عن محاولة استدعاء الخدمة الفاشلة لفترة زمنية محددة. بدلاً من ذلك، يقوم بإرجاع خطأ فوراً، دون انتظار.
الحالات الثلاث لقاطع الدائرة
يعمل قاطع الدائرة في واحدة من ثلاث حالات:
- مُغلق (Closed): هذه هي الحالة الطبيعية. الطلبات تمر من خلال قاطع الدائرة إلى الخدمة المستهدفة. يقوم القاطع بحساب عدد مرات الفشل المتتالية أو في فترة زمنية معينة. إذا وصل عدد مرات الفشل إلى حد معين (مثلاً، 5 مرات فشل في 10 ثوانٍ)، ينتقل القاطع إلى حالة “مفتوح”.
- مفتوح (Open): في هذه الحالة، القاطع “فصل”. أي طلب جديد يحاول المرور من خلاله يتم رفضه فوراً (fail-fast) دون محاولة استدعاء الخدمة البعيدة. هذا هو الإجراء البطولي! فهو يحمي نظامنا (الـ Caller) من استنزاف الموارد، ويعطي الخدمة الفاشلة (الـ Callee) فرصة للتعافي دون أن تغرقها الطلبات. يبقى القاطع في هذه الحالة لمدة زمنية محددة (مثلاً، 30 ثانية).
- نصف مفتوح (Half-Open): بعد انتهاء فترة الانتظار في الحالة المفتوحة، ينتقل القاطع إلى هذه الحالة الاختبارية. يسمح القاطع بمرور طلب واحد (أو عدد قليل جداً) إلى الخدمة البعيدة.
- إذا نجح هذا الطلب، فهذا مؤشر على أن الخدمة قد تعافت. يعود القاطع إلى حالة “مغلق” وتعود الأمور إلى طبيعتها.
- إذا فشل هذا الطلب، فهذا يعني أن المشكلة لا تزال قائمة. يعود القاطع إلى حالة “مفتوح” مرة أخرى ويبدأ فترة انتظار جديدة.
خلونا نوسّخ إيدينا شوي: تطبيق عملي لقاطع الدائرة
الكلام النظري جميل، لكن خلينا نشوف كيف ممكن نطبق هذا الكلام عملياً. معظم لغات البرمجة الحديثة لديها مكتبات جاهزة وممتازة لتطبيق هذا النمط. سأستخدم مثالاً بلغة C# مع مكتبة Polly الرائعة، لأنها توضح الفكرة بشكل ممتاز.
الكود قبل قاطع الدائرة (الطريق إلى الجحيم)
هذا هو الكود الذي كان لدينا، والذي سبب الكارثة. كود بسيط وبريء المظهر:
public class NotificationService
{
private readonly HttpClient _httpClient;
public NotificationService(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task SendNotificationAsync(string message)
{
// هذا الاستدعاء سيبقى معلقاً إلى الأبد (أو حتى Timeout) إذا كانت الخدمة واقعة
var response = await _httpClient.PostAsJsonAsync("http://notification-service/api/send", new { message });
response.EnsureSuccessStatusCode();
}
}
الكود بعد استخدام قاطع الدائرة مع Polly (الطريق إلى المرونة)
الآن، لنقم بإضافة قاطع الدائرة باستخدام مكتبة Polly. لاحظ كيف “نغلف” الاستدعاء بسياسة حماية.
// في ملف Startup.cs أو Program.cs
// تعريف سياسة قاطع الدائرة
var circuitBreakerPolicy = HttpPolicyExtensions
.HandleTransientHttpError() // التعامل مع أخطاء الشبكة الشائعة
.CircuitBreakerAsync(
exceptionsAllowedBeforeBreaking: 5, // اسمح بـ 5 أخطاء قبل فتح الدائرة
durationOfBreak: TimeSpan.FromSeconds(30) // ابقَ في الحالة المفتوحة لمدة 30 ثانية
);
// إضافة HttpClient مع السياسة
builder.Services.AddHttpClient<NotificationService>()
.AddPolicyHandler(circuitBreakerPolicy);
// الكود في الخدمة نفسها لا يتغير!
public class NotificationService
{
private readonly HttpClient _httpClient;
public NotificationService(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task SendNotificationAsync(string message)
{
// الآن هذا الاستدعاء محمي بسياسة قاطع الدائرة
// إذا كانت الدائرة مفتوحة، سيتم رمي BrokenCircuitException فوراً
var response = await _httpClient.PostAsJsonAsync("http://notification-service/api/send", new { message });
response.EnsureSuccessStatusCode();
}
}
بهذه الإضافة البسيطة، تغير كل شيء. الآن عندما تفشل خدمة الإشعارات 5 مرات، سيفتح قاطع الدائرة. أي استدعاء جديد خلال الـ 30 ثانية التالية سيفشل فوراً، مما يحرر الـ thread ويعيد رسالة خطأ سريعة للتطبيق الرئيسي، الذي يمكنه التعامل معها (مثلاً، تجاهل إرسال الإشعار والمتابعة). بعد 30 ثانية، سيحاول طلب واحد المرور، وهكذا.
نصائح من خبرة أبو عمر
تطبيق النمط تقنياً هو نصف القصة، النصف الآخر هو الحكمة في استخدامه.
1. ليس كل شيء يحتاج قاطع دائرة
لا تستخدمه على استدعاءات داخلية حرجة جداً لا يمكن للنظام العمل بدونها (مثل الاتصال بقاعدة البيانات الرئيسية). قاطع الدائرة مفيد جداً عند التعامل مع:
- خدمات خارجية (Third-party APIs).
- خدمات مصغرة داخلية ليست حيوية 100% لإتمام العملية الأساسية.
- أي عملية عبر الشبكة معرضة للبطء أو الفشل المؤقت.
2. أهمية البدائل (Fallbacks)
عندما يفتح قاطع الدائرة ويرجع لك خطأ فورياً، ماذا ستفعل؟ أفضل الأنظمة لا تعرض رسالة خطأ للمستخدم. بدلاً من ذلك، لديها خطة بديلة.
- بيانات من الكاش: هل يمكنك إرجاع نسخة قديمة من البيانات من ذاكرة التخزين المؤقت (Cache)؟
- قيمة افتراضية: هل يمكنك إرجاع قيمة افتراضية منطقية؟ مثلاً، إذا فشلت خدمة توصيات المنتجات، هل يمكنك عرض المنتجات الأكثر مبيعاً بدلاً منها؟
- وضع الطلب في طابور: إذا فشلت خدمة إرسال الإشعارات، هل يمكنك تخزين الإشعار في طابور (Queue) ليتم إرساله لاحقاً عندما تعود الخدمة للعمل؟
3. الضبط والمراقبة والتنبيه
قاطع الدائرة ليس حلاً “ثبته وانساه”. يجب أن تضبط إعداداته (عدد المحاولات، مدة الفتح) بعناية بناءً على طبيعة الخدمة. الأهم من ذلك، يجب أن تراقب حالة قواطع الدائرة في نظامك.
- متى يفتح القاطع؟ كم مرة؟ ولماذا؟
- يجب أن يكون لديك تنبيهات (Alerts) تخبرك عندما يكون هناك قاطع دائرة “مفتوح” لفترة طويلة. هذا مؤشر على وجود مشكلة حقيقية في الخدمة البعيدة تحتاج إلى تدخل منك.
الخلاصة: من الفوضى إلى المرونة 💡
يا جماعة، الدرس الذي تعلمناه تلك الليلة كان بسيطاً وعميقاً: في الأنظمة الموزعة، الفشل ليس احتمالاً، بل هو حتمية. مهمتنا كمطورين ومهندسين ليست بناء أنظمة لا تفشل أبداً، بل بناء أنظمة تستطيع التعامل مع الفشل بأناقة ومرونة (Resilient Systems).
نمط قاطع الدائرة هو واحد من أهم الأدوات في صندوق أدواتنا لتحقيق هذه المرونة. إنه يحول الفشل الكارثي المتتالي إلى فشل يمكن التحكم فيه، ويحمي نظامك من الانهيار، ويمنحك الوقت والمساحة لإصلاح المشاكل الحقيقية.
نصيحتي الأخيرة لكم: لا تنتظروا حتى تقع الكارثة وتنهار أنظمتكم. ابدأوا اليوم بمراجعة نقاط الضعف في تفاعل خدماتكم، وفكروا أين يمكن لـ “قاطع الدائرة” أن يكون بطلكم المنقذ. ابقوا فضوليين، استمروا في البناء، ولا تخافوا من الفشل، بل تعلموا منه. 👍