يا أهلاً وسهلاً فيكم يا جماعة الخير. معكم أخوكم أبو عمر.
اسمحوا لي اليوم آخذكم معي في رحلة إلى الماضي القريب، لأحد أصعب الأيام التي مرت علي وعلى فريقي. كان يوم إطلاق ميزة جديدة ومهمة لأحد عملائنا الكبار في مجال التجارة الإلكترونية. الأجواء كانت مشحونة بالحماس والترقب، القهوة ما بتنزل عن الطاولة، وعيوننا كلنا مزروعة على شاشات المراقبة (Dashboards) اللي بتعرض أداء النظام لحظة بلحظة.
في الدقائق الأولى، كل شيء كان تمام التمام، والأرقام في صعود. فجأة، وبدون سابق إنذار، بدأت الإنذارات (Alerts) تصرخ زي المجنونة على قنوات “سلاك” تبعتنا. “High CPU Usage”, “High Memory Usage”, “Endpoint Unresponsive”… الدنيا ولعت!
أول ما خطر في بالنا إنه في ضغط هائل على الخدمة الرئيسية، لكن الأرقام ما كانت منطقية. بعد دقائق من البحث والتحليل السريع تحت ضغط رهيب، اكتشفنا المصيبة. المشكلة ما كانت في خدماتنا الأساسية، بل في خدمة صغيرة وثانوية جداً، خدمة “اقتراح المنتجات المشابهة”. هاي الخدمة البسيطة بدأت تواجه مشاكل داخلية وصارت ترجع أخطاء (500 Internal Server Error) بشكل متقطع.
وهون كانت الكارثة الحقيقية. الخدمة الرئيسية اللي بتعرض صفحة المنتج كانت بتستدعي خدمة الاقتراحات هاي، ولما خدمة الاقتراحات ما بترد بسرعة أو بترجع خطأ، الخدمة الرئيسية بتضلها تستنى (Wait) لحد ما يخلص وقت الانتظار (Timeout). ومع كل طلب جديد لصفحة منتج، كان في “ثريد” (Thread) جديد في الخدمة الرئيسية بتعلق وهو بستنى الرد. خلال دقائق، كل “الثريدات” المتاحة في الخدمة الرئيسية استُهلكت، وصارت غير قادرة على خدمة أي طلب جديد، حتى لو كان طلب بسيط جداً. هذا الأشي أدى إلى انهيارها، واللي بدوره سبب ضغط على خدمات ثانية… وهكذا، دخلنا في دوامة من الفشل المتتالي (Cascading Failures) اللي شلت النظام بالكامل.
في تلك اللحظة، شعرت بالعجز. خطأ واحد في مكون غير حاسم، كان كفيل بإسقاط كل شيء. بعد ما عملنا حل سريع مؤقت (عطلنا الاتصال بهذيك الخدمة يدوياً وأعدنا تشغيل كل شيء)، جلسنا كفريق وسألتهم: “يا جماعة، الغلطة مش في الخدمة اللي وقعت، الغلطة فينا إحنا. كيف نظامنا هش لهالدرجة؟ كيف ما قدرنا نعزل الخطأ ونحتويه؟”. من رحم هذه الأزمة، وُلد قرارنا بتبني واحد من أهم أنماط تصميم الأنظمة المرنة: نمط قاطع الدائرة (The Circuit Breaker Pattern).
ما هو “قاطع الدائرة” (Circuit Breaker)؟ تشبيه بسيط من عالم الكهرباء
قبل ما ندخل في التفاصيل التقنية المعقدة، خلينا نبسط المفهوم. كل واحد فينا عنده في البيت لوحة كهرباء فيها قواطع. وظيفة القاطع بسيطة: لو صار في حمل كهربائي زائد أو “شورت” في جهاز معين (مثلاً المكيف)، القاطع “بيفصل” أو “بينزل” تلقائياً. ليش؟ عشان يحمي باقي الأجهزة في البيت ويمنع حدوث حريق أو ضرر أكبر.
نمط قاطع الدائرة في البرمجة هو نفس المبدأ تماماً. هو عبارة عن “وكيل” (Proxy) بيغلف الاتصالات بين الخدمات (خصوصاً في معمارية الخدمات المصغرة – Microservices). هذا الوكيل بيراقب حالة الاتصالات مع خدمة معينة. إذا لاحظ إنها بتفشل بشكل متكرر، “بيفصل الدائرة”، يعني بيمنع أي اتصال جديد لهي الخدمة لفترة معينة، وبالتالي بيحمي الخدمة المُستَدعِية (Calling Service) من استنزاف مواردها وبيمنع انتشار الفشل لباقي النظام.
هذا النمط له ثلاث حالات رئيسية، زي إشارات المرور:
- مغلق (Closed): الحالة الطبيعية. الضوء أخضر. كل الطلبات بتمر بشكل عادي للخدمة الهدف.
- مفتوح (Open): الحالة الطارئة. الضوء أحمر. بعد عدد معين من حالات الفشل، القاطع بيفصل الدائرة. أي طلب جديد يتم رفضه فوراً بدون محاولة الاتصال بالخدمة المتعطلة.
- نصف مفتوح (Half-Open): حالة الاختبار. الضوء أصفر. بعد فترة زمنية معينة والدائرة مفتوحة، القاطع بيسمح لطلب واحد “تجريبي” بالمرور. إذا نجح، بيعتبر إنه الخدمة رجعت تشتغل وبيرجع للحالة المغلقة (أخضر). إذا فشل، بيرجع للحالة المفتوحة (أحمر) مرة أخرى.
كيف يعمل نمط قاطع الدائرة بالتفصيل؟
خلينا نفصّل الحالات الثلاثة ونشوف شو بصير خلف الكواليس في كل مرحلة.
الحالة المغلقة (Closed State): “كل شيء على ما يرام”
في هذه الحالة، يعمل قاطع الدائرة بشكل شفاف تماماً. هو مجرد وسيط يمرر الطلب من الخدمة (أ) إلى الخدمة (ب). لكنه في الخلفية، يقوم بعمل مهم:
- العدّاد السري: يحتفظ قاطع الدائرة بعدّاد لحالات الفشل (Failure Counter).
- عند النجاح: إذا تم استدعاء الخدمة (ب) بنجاح وعاد الرد سليمًا، يقوم قاطع الدائرة بإعادة تصفير عدّاد الفشل. الأمور ممتازة.
- عند الفشل: إذا فشل الاتصال بالخدمة (ب) (بسبب خطأ في الشبكة، timeout، أو رد خطأ مثل 5xx)، يقوم قاطع الدائرة بزيادة عدّاد الفشل بواحد.
- عتبة الفشل (Failure Threshold): إذا وصل عدّاد الفشل إلى حد معين قمنا بتعريفه مسبقاً (مثلاً، 5 حالات فشل متتالية في دقيقة واحدة)، هنا يتخذ قاطع الدائرة قراره الحاسم وينتقل إلى الحالة المفتوحة.
الحالة المفتوحة (Open State): “وقف عندك! الخدمة واقعة”
هذه هي حالة الحماية القصوى. عندما تفتح الدائرة، يحدث التالي:
- الفشل السريع (Fail Fast): أي طلب جديد موجه إلى الخدمة (ب) يتم رفضه على الفور من قِبل قاطع الدائرة نفسه، دون أن يحاول حتى إرسال الطلب عبر الشبكة. يتم إرجاع خطأ فوري للخدمة (أ).
- حماية الموارد: هذا الإجراء يحمي الخدمة (أ) من استنزاف مواردها (مثل الـ Threads والـ Sockets والذاكرة) في انتظار رد لن يأتي أبداً من الخدمة (ب) المتعطلة.
- مؤقت إعادة الضبط (Reset Timeout): بمجرد الدخول في هذه الحالة، يبدأ مؤقت زمني (مثلاً، 30 ثانية). خلال هذه الفترة، ستبقى الدائرة مفتوحة وكل الطلبات سيتم رفضها.
- الانتقال للاختبار: عند انتهاء مدة المؤقت، لا يعود قاطع الدائرة مباشرة إلى الحالة المغلقة (لأن الخدمة قد تكون لا تزال متعطلة)، بل ينتقل بحذر إلى الحالة نصف المفتوحة.
الحالة نصف المفتوحة (Half-Open State): “خلينا نجرب مرة، بلكي زبطت”
هذه هي مرحلة “جس النبض”. إنها الطريقة الذكية لمعرفة ما إذا كانت الخدمة المتعطلة قد تعافت أم لا، دون إغراقها بالطلبات مرة واحدة.
- الطلب الجاسوس (Probe Request): يسمح قاطع الدائرة للطلب التالي *فقط* بالمرور إلى الخدمة (ب).
- إذا نجح الطلب: يا سلام! هذا مؤشر جيد على أن الخدمة (ب) قد تعافت. يقوم قاطع الدائرة بإعادة تصفير عدّاد الفشل والانتقال فوراً إلى الحالة المغلقة (Closed). ويعود النظام للعمل بشكل طبيعي.
- إذا فشل الطلب: للأسف، يبدو أن الخدمة (ب) لا تزال تواجه مشاكل. يعود قاطع الدائرة مباشرة إلى الحالة المفتوحة (Open) ويقوم بتشغيل مؤقت إعادة الضبط من جديد، ليعطي الخدمة (ب) وقتاً إضافياً للتعافي.
مثال عملي: تطبيق قاطع الدائرة باستخدام مكتبة Polly في C#
الكلام النظري جميل، لكن خلينا نشوف كيف ممكن نطبق هذا الحكي بشكل عملي. واحدة من أشهر المكتبات لتطبيق هذا النمط وغيره من أنماط المرونة في عالم .NET هي مكتبة Polly. المثال التالي يوضح كيف يمكن تعريف سياسة قاطع دائرة وتطبيقها على استدعاء HTTP.
using Polly;
using Polly.CircuitBreaker;
using System;
using System.Net.Http;
using System.Threading.Tasks;
public class ApiClient
{
private readonly HttpClient _httpClient;
private readonly AsyncCircuitBreakerPolicy _circuitBreakerPolicy;
public ApiClient()
{
_httpClient = new HttpClient();
// تعريف سياسة قاطع الدائرة
// 1. سيتم فتح الدائرة بعد 3 محاولات فاشلة متتالية
// 2. ستبقى الدائرة مفتوحة لمدة 30 ثانية
_circuitBreakerPolicy = Policy
.Handle<HttpRequestException>() // حدد نوع الأخطاء التي يجب أن يتفاعل معها
.Or<TaskCanceledException>() // يمكن إضافة أخطاء أخرى مثل انتهاء الوقت
.CircuitBreakerAsync(
exceptionsAllowedBeforeBreaking: 3,
durationOfBreak: TimeSpan.FromSeconds(30),
onBreak: (ex, breakDelay) =>
{
// هذا الكود يتم تنفيذه عند فتح الدائرة
Console.WriteLine($".Circuit broken for {breakDelay.TotalSeconds} seconds due to: {ex.Message}");
},
onReset: () =>
{
// هذا الكود يتم تنفيذه عند إغلاق الدائرة مجدداً
Console.WriteLine(".Circuit reset and is now closed.");
},
onHalfOpen: () =>
{
// هذا الكود يتم تنفيذه عند الدخول في حالة نصف مفتوحة
Console.WriteLine(".Circuit is now half-open. Next call is a trial.");
}
);
}
public async Task<string> GetDataFromService()
{
try
{
// تنفيذ الاستدعاء الفعلي داخل سياسة قاطع الدائرة
return await _circuitBreakerPolicy.ExecuteAsync(async () =>
{
Console.WriteLine("Making call through circuit breaker...");
// هذا هو الكود الذي قد يفشل
var response = await _httpClient.GetStringAsync("http://unreliable-service/api/data");
return response;
});
}
catch (BrokenCircuitException) // هذا الخطأ يُرمى عندما تكون الدائرة مفتوحة
{
// هنا يتم التعامل مع حالة الدائرة المفتوحة - الفشل السريع
// "الخدمة حالياً غير متاحة، جرب كمان شوي"
Console.WriteLine("Circuit is open. Failing fast. Returning fallback data.");
return "Default fallback data"; // إرجاع قيمة افتراضية أو من الكاش
}
catch (Exception ex)
{
// التعامل مع أخطاء الاتصال الفعلية (عندما تكون الدائرة مغلقة أو نصف مفتوحة)
Console.WriteLine($"An actual request failed: {ex.Message}");
throw; // أو التعامل مع الخطأ حسب منطق التطبيق
}
}
}
في هذا المثال، قمنا بتغليف استدعاء HttpClient داخل سياسة _circuitBreakerPolicy. إذا فشل الاستدعاء 3 مرات متتالية، ستُفتح الدائرة، وأي استدعاء لاحق للدالة GetDataFromService خلال 30 ثانية سيرمي خطأ BrokenCircuitException فوراً، والذي نلتقطه ونرجع قيمة بديلة، مانعين بذلك استنزاف الموارد.
نصائح أبو عمر الذهبية لتطبيق قاطع الدائرة
تطبيق النمط بحد ذاته ليس كافياً، هناك بعض الحكمة المكتسبة من التجربة والخطأ التي أود مشاركتها معكم:
- لا تكن بخيلاً في الإعدادات (Configurable Settings): لا تضع أرقاماً ثابتة في الكود (مثل 3 محاولات و 30 ثانية). اجعل عتبة الفشل ومدة فتح الدائرة قيماً يمكن تعديلها من ملف الإعدادات (Configuration). ما يعمل بشكل جيد في بيئة الاختبار قد يحتاج لتعديل جذري في بيئة الإنتاج الحقيقية.
- جهز “الخطة ب” (Have a Fallback Plan): أهم سؤال يجب أن تطرحه على نفسك هو: “ماذا يجب أن يحدث عندما تكون الدائرة مفتوحة؟”. هل يجب أن تعرض للمستخدم رسالة خطأ؟ أم هل يمكنك إرجاع بيانات قديمة من ذاكرة التخزين المؤقت (Cache)؟ أم ربما قيمة افتراضية بسيطة؟ وجود خطة بديلة (Fallback) هو ما يميز النظام المرن عن النظام الفاشل.
- المراقبة والإنذارات هي عيونك: يجب أن تراقب حالة قواطع الدائرة لديك. أضف عدادات (Metrics) تسجل متى تفتح الدائرة، ومتى تغلق، وكم مرة تدخل في حالة نصف مفتوحة. قم بإعداد إنذارات (Alerts) تخبرك فوراً عند فتح دائرة مهمة. هذا الإنذار هو أول دليل على وجود مشكلة في خدمة أخرى ويساعدك على التحرك بسرعة.
- ليس فقط لأخطاء الشبكة: قاطع الدائرة ليس فقط لأخطاء 5xx أو فشل الاتصال. يمكنك تهيئته ليعتبر الاستجابات البطيئة جداً (Timeouts) كحالة فشل أيضاً. خدمة بطيئة يمكن أن تكون مدمرة تماماً مثل خدمة متوقفة.
- اختبر فوضويتك (Chaos Testing): أفضل طريقة للتأكد من أن قاطع الدائرة يعمل كما هو متوقع هي من خلال “هندسة الفوضى” (Chaos Engineering). في بيئة الاختبار، قم عمداً بإيقاف إحدى الخدمات أو اجعلها بطيئة، وراقب كيف يتصرف نظامك. هل فتح قاطع الدائرة؟ هل تم تفعيل الخطة البديلة؟ هل تعافى النظام تلقائياً عند عودة الخدمة للعمل؟ لا تثق به حتى تراه يفشل وينجح أمام عينيك.
الخلاصة: ابنِ أنظمة تتوقع الفشل
في عالم الأنظمة الموزعة والخدمات المصغرة، الفشل ليس احتمالاً، بل هو حقيقة مؤكدة ستحدث عاجلاً أم آجلاً. الدرس الذي تعلمناه بالطريقة الصعبة في ذلك اليوم هو أن بناء الأنظمة القوية لا يعني منع حدوث الأخطاء، بل يعني تصميم أنظمة قادرة على الصمود واحتواء الضرر عند حدوثها، والتعافي منها برشاقة.
نمط قاطع الدائرة ليس مجرد أداة تقنية، بل هو تغيير في العقلية. هو اعتراف بأننا لا نملك السيطرة الكاملة على كل أجزاء نظامنا، وعلينا أن نكون مستعدين للتعامل مع هذا الواقع. إنه واحد من أهم الأسلحة في ترسانة أي مطور أو مهندس معماري يسعى لبناء تطبيقات قوية، مرنة، وقادرة على تحمل ضغط العالم الحقيقي.
تذكروا يا جماعة، بناء الأنظمة القوية لا يعني أنها لا تفشل أبداً، بل يعني أنها تعرف كيف تنهض بعد كل عثرة. خليكوا مصحصحين! 😉