يا جماعة الخير، السلام عليكم.
اسمحوا لي آخذكم في رحلة قصيرة بالزمن، ليلة خميس ما بنساها بحياتي. كنت قاعد في مكتبي، الساعة كانت حوالي 11 بالليل، وفنجان القهوة الثالث بين إيديّ. فجأة، التلفون بلش يرن بدون توقف، ورسائل “سلاك” بتضوي زي شجرة عيد الميلاد. فتحت لوحة المراقبة (Dashboard) لألاقي الكارثة: النظام واقع! كل المؤشرات باللون الأحمر، والمستخدمين مش قادرين يعملوا أي إشي.
بعد شوية حفر وتحليل مع الفريق، اكتشفنا إن المشكلة كلها بلشت من خدمة صغيرة ومسكينة، خدمة الإشعارات (Notifications Service). هالخدمة صار فيها بطء شديد بسبب مشكلة في قاعدة البيانات تبعتها. لكن المصيبة إنه كل خدماتنا الثانية، من تسجيل الدخول للدفع للملف الشخصي، كانت بتحاول تحكي مع خدمة الإشعارات هاي. ولأنها بطيئة وما بترد، كل طلب كان يضل معلق، يسحب معه “ثريد” (thread) وموارد من السيرفر. شوي شوي، كل خدماتنا استنفدت مواردها ووقعت زي أحجار الدومينو. فشل واحد صغير، عمل انهيار كامل.
هذيك الليلة، واحنا بنعمل إعادة تشغيل للسيرفرات واحد ورا الثاني زي المجانين، أدركت إنه حلنا للمشكلة كان سطحي. احنا بنعالج العرض، مش المرض. ومن هون بلشت رحلتنا مع المنقذ: نمط قاطع الدائرة.
ما هو الجحيم الذي كنا نعيشه؟ مشكلة الانهيارات المتتالية (Cascading Failures)
اللي صار معنا هذيك الليلة إله اسم تقني رنان: الانهيارات المتتالية (Cascading Failures). في عالم الخدمات المصغرة (Microservices)، خدماتك بتعتمد على بعضها البعض. خدمة المستخدمين بتطلب بيانات من خدمة الطلبات، وخدمة الطلبات بتطلب تأكيد من خدمة الدفع، وهلم جرا.
تخيلها زي سلسلة من عمال المصنع على خط إنتاج. لو عامل واحد في النص توقف عن الشغل أو صار بطيء جدًا، شو بصير؟ كل العمال اللي قبله بصير عندهم تراكم في الشغل، والعمال اللي بعده بصيروا قاعدين فاضيين ما عندهم إشي يعملوه. بعد فترة قصيرة، خط الإنتاج كله بتوقف. هذا بالضبط اللي بصير في نظامك:
- الخدمة (أ) تستدعي الخدمة (ب).
- الخدمة (ب) لديها مشكلة (بطيئة أو لا تستجيب).
- الخدمة (أ) تظل منتظرة الرد، وتستهلك مواردها (اتصالات، ذاكرة، وحدات معالجة).
- تأتي طلبات جديدة إلى الخدمة (أ)، وكلها تنتظر الخدمة (ب).
- خلال دقائق، تستنفد الخدمة (أ) كل مواردها وتتوقف عن الاستجابة لأي طلب، حتى الطلبات التي لا علاقة لها بالخدمة (ب).
- الآن، إذا كانت الخدمة (ج) تعتمد على الخدمة (أ)، فإنها ستبدأ بالفشل أيضًا. وهكذا ينتشر الفشل كالنار في الهشيم.
المشكلة الأساسية ليست أن الخدمات تفشل، فالفشل حتمي في الأنظمة الموزعة. المشكلة هي كيف يتعامل نظامك مع هذا الفشل.
المنقذ: نمط قاطع الدائرة (Circuit Breaker)
هنا يأتي دور البطل، نمط قاطع الدائرة. الفكرة مستوحاة مباشرة من قاطع الكهرباء الموجود في بيتك. لما يصير في حمل كهربائي زائد أو تماس، القاطع “بيفصل” أو “بيطق” ليحمي أجهزة البيت من الاحتراق والبيت كله من الحريق. هو لا يصلح المشكلة الكهربائية، ولكنه يعزلها ويمنع وقوع كارثة أكبر.
نمط قاطع الدائرة البرمجي يعمل بنفس المبدأ. هو يغلف (wraps) استدعاءات الشبكة لخدمة أخرى ويراقب حالات الفشل. وله ثلاث حالات رئيسية:
الحالة المغلقة (Closed)
هذا هو الوضع الطبيعي. الطلبات تمر من خلال قاطع الدائرة إلى الخدمة المستهدفة بدون أي تدخل. في هذه الأثناء، يقوم القاطع بعدّ حالات الفشل (مثل انتهاء مهلة الاستدعاء، أو أخطاء السيرفر). إذا وصل عدد حالات الفشل إلى حد معين خلال فترة زمنية محددة (مثلاً، 5 أخطاء في 30 ثانية)، “يقرر” القاطع أن هناك مشكلة جدية ويفتح الدائرة.
الحالة المفتوحة (Open)
عندما تفتح الدائرة، يتوقف القاطع عن تمرير أي طلبات إلى الخدمة المتعثرة. بدلًا من ذلك، يفشل فورًا (fail fast) ويرجع خطأ مباشرًا للخدمة المستدعية. هذا هو السحر بعينه! لأنه بذلك:
- يحمي الخدمة المستدعية: لا تستهلك مواردها في انتظار رد لن يأتي أبدًا.
- يعطي الخدمة المتعثرة فرصة للتعافي: بدلًا من إغراقها بمزيد من الطلبات التي لا تستطيع معالجتها، نمنحها وقتًا لتتنفس وتصلح نفسها (ربما يتم إعادة تشغيلها تلقائيًا).
بعد فترة زمنية محددة مسبقًا (مثلاً، 60 ثانية)، ينتقل القاطع إلى الحالة التالية.
الحالة شبه المفتوحة (Half-Open)
بعد انتهاء فترة الانتظار، لا يعود القاطع مباشرة إلى الحالة المغلقة. بدلاً من ذلك، يدخل في حالة اختبار حذرة. يسمح بمرور طلب واحد فقط (أو عدد قليل من الطلبات) إلى الخدمة.
- إذا نجح هذا الطلب: يعتبر القاطع أن الخدمة قد تعافت، فيغلق الدائرة ويعود إلى الحالة الطبيعية (Closed).
- إذا فشل هذا الطلب: يعتبر القاطع أن المشكلة لا تزال قائمة، فيعود فورًا إلى الحالة المفتوحة (Open) ويبدأ فترة انتظار جديدة.
كيف طبقنا قاطع الدائرة؟ مثال عملي
الكلام النظري جميل، لكن “ورجيني الشغل”. في عالم الدوت نت (.NET)، واحدة من أروع المكتبات لتطبيق هذا النمط وغيره هي Polly. دعونا نرى مثالًا مبسطًا بلغة C# لكيفية تغليف استدعاء API باستخدام قاطع الدائرة.
// أولاً، قم بتعريف سياسة قاطع الدائرة
// يمكن تعريفها مرة واحدة عند بدء تشغيل التطبيق
var circuitBreakerPolicy = Policy
.Handle<HttpRequestException>() // حدد نوع الأخطاء التي ستؤثر على القاطع
.Or<TimeoutRejectedException>() // يمكن إضافة أنواع أخرى من الأخطاء
.CircuitBreakerAsync(
exceptionsAllowedBeforeBreaking: 5, // عدد الأخطاء المتتالية المسموح بها قبل فتح الدائرة
durationOfBreak: TimeSpan.FromSeconds(60), // مدة فتح الدائرة (60 ثانية)
onBreak: (ex, breakDelay) =>
{
// كود يتم تنفيذه عند فتح الدائرة (للتسجيل أو التنبيه)
Console.WriteLine($".قاطع الدائرة فتح! سيبقى مفتوحًا لمدة {breakDelay.TotalSeconds} ثانية");
},
onReset: () =>
{
// كود يتم تنفيذه عند إغلاق الدائرة مجددًا
Console.WriteLine("قاطع الدائرة أغلق. عادت الأمور إلى طبيعتها.");
},
onHalfOpen: () =>
{
// كود يتم تنفيذه عند الدخول في حالة شبه مفتوحة
Console.WriteLine("قاطع الدائرة في حالة شبه مفتوحة، سيتم إرسال طلب تجريبي.");
}
);
// ثانيًا، استخدم السياسة عند استدعاء الخدمة
try
{
var response = await circuitBreakerPolicy.ExecuteAsync(async () =>
{
// هذا الكود هو المحمي بواسطة قاطع الدائرة
Console.WriteLine("محاولة استدعاء الخدمة الخارجية...");
var client = new HttpClient();
return await client.GetStringAsync("http://api.example.com/data");
});
// في حال النجاح، تعامل مع الاستجابة
Console.WriteLine("تم استدعاء الخدمة بنجاح.");
}
catch (BrokenCircuitException)
{
// هذا الخطأ يظهر فورًا إذا كانت الدائرة مفتوحة
// لا يتم محاولة استدعاء الخدمة الخارجية أصلًا
Console.WriteLine("فشل سريع: الدائرة مفتوحة. لن يتم استدعاء الخدمة.");
// هنا يمكنك تطبيق منطق الـ Fallback (الخطة ب)
}
catch (Exception ex)
{
// أي أخطاء أخرى قد تحدث أثناء الاستدعاء
Console.WriteLine($"حدث خطأ أثناء استدعاء الخدمة: {ex.GetType().Name}");
}
كما ترى، الكود واضح جدًا. نحن نعرّف “سياسة” تحدد شروط فتح وإغلاق الدائرة، ثم نستخدمها لتنفيذ الكود الحساس. هذا يفصل منطق المرونة (resilience logic) عن منطق العمل (business logic) بشكل أنيق.
نصائح من مطبخ أبو عمر: ما بعد قاطع الدائرة
تطبيق النمط هو مجرد البداية. من خلال تجربتنا، تعلمت بعض الدروس التي أود مشاركتها معكم:
1. قاطع الدائرة لا يعمل وحيدًا
قاطع الدائرة هو جزء من فريق. لكي يكون فعالًا، يجب أن تدمجه مع أنماط أخرى:
- نمط إعادة المحاولة (Retry): قبل أن يعتبر القاطع أن المحاولة فاشلة، قد يكون من المفيد إعادة المحاولة مرة أو مرتين، خاصة مع الأخطاء المؤقتة (transient faults). مكتبة Polly تجعل من السهل دمج السياستين معًا.
- نمط المهلة الزمنية (Timeout): لا تجعل خدمتك تنتظر إلى الأبد. حدد مهلة زمنية قصيرة لكل استدعاء خارجي. إذا لم يأتِ الرد خلال هذه المهلة، اعتبر المحاولة فاشلة.
- نمط الخطة البديلة (Fallback): ماذا يحدث عندما تفتح الدائرة؟ هل تعرض رسالة خطأ للمستخدم؟ هذا سيء. الأفضل هو أن يكون لديك خطة بديلة، مثل عرض بيانات قديمة من الكاش (cache) أو عرض قيمة افتراضية.
2. المراقبة والإنذار (Monitoring and Alerting)
مش منطق القاطع يفصل ويفتح وإحنا مش عارفين! من الضروري جدًا أن يكون لديك نظام مراقبة يخبرك بحالة قواطع الدائرة في نظامك. يجب أن تصلك تنبيهات فورية عند فتح دائرة لخدمة حرجة. هذا يساعدك على اكتشاف المشاكل بشكل استباقي قبل أن يشعر بها المستخدم.
3. ضبط الإعدادات بحكمة (Fine-Tuning Configuration)
الأرقام التي تضعها في إعدادات قاطع الدائرة (عدد المحاولات، مدة الفتح) ليست أرقامًا مقدسة. هي تعتمد بشكل كبير على سياق الخدمة.
- خدمة حرجة (مثل الدفع): قد تحتاج إلى قاطع دائرة حساس جدًا يفتح بسرعة ولكنه يحاول التعافي بسرعة أيضًا.
- خدمة غير أساسية (مثل التوصيات): يمكن أن يكون القاطع أكثر تساهلاً، وقد تكون مدة الفتح أطول.
لا تخف من التجربة وتعديل هذه الأرقام بناءً على أداء نظامك الفعلي.
الخلاصة: من الفوضى إلى المرونة Resilience
الانتقال من نظام ينهار بالكامل بسبب فشل بسيط إلى نظام يتعامل مع الفشل برشاقة لم يكن مجرد تحديث تقني، بل كان تغييرًا في طريقة تفكيرنا كفريق. تعلمنا أن نبني أنظمة تتوقع الفشل وتتعامل معه، بدلاً من أنظمة تفترض أن كل شيء سيعمل بشكل مثالي طوال الوقت.
نمط قاطع الدائرة كان حجر الأساس في هذه الرحلة. لقد أعطانا الأداة لعزل الأخطاء، وحماية مواردنا، ومنح أنظمتنا القدرة على الشفاء الذاتي. والأهم من ذلك كله، أعاد لنا ليالي نومنا الهادئة.
نصيحتي الأخيرة لك: لا تنتظر حتى تحترق دارك لتشتري طفاية حريق. ابدأ بتطبيق أنماط المرونة مثل قاطع الدائرة اليوم، لأن الفشل ليس “إذا” حدث، بل “متى” سيحدث. ما فيه أحلى من نظام صامد ومستقر! 😉