يا جماعة الخير، السلام عليكم ورحمة الله. اسمحوا لي اليوم أن أروي لكم قصة حدثت معي قبل بضع سنوات، قصة لا تزال تفاصيلها محفورة في ذاكرتي، ليس لأنها كانت كابوساً تقنياً، بل لأنها علمتني درساً من أهم الدروس في عالم بناء الأنظمة الموزعة والخدمات المصغرة.
كنا في خضم إطلاق منصة تجارة إلكترونية جديدة، والكل يعمل على قدم وساق. “الدنيا قايمة قاعدة” كما نقول في فلسطين. الساعات الأخيرة قبل الإطلاق الرسمي كانت مليئة بالتوتر والترقب. ضغطنا على زر الإطلاق، وبدأت الطلبات تتدفق على النظام. في الدقائق الأولى، كان كل شيء يبدو مثالياً. لوحات المراقبة (Dashboards) تضيء بالأخضر، والكل يتبادل نظرات الرضا والفخر.
وفجأة، بعد حوالي نصف ساعة، بدأت الكارثة. أولاً، تنبيه بأن خدمة “توصيات المنتجات” لا تستجيب. قلنا “بسيطة”، هذه خدمة ثانوية، يمكن للمستخدمين تصفح الموقع بدونها. لكن ما لم نكن نتوقعه هو ما حدث بعد ذلك. مثل أحجار الدومينو المتساقطة، بدأت الخدمات الأخرى تنهار واحدة تلو الأخرى: خدمة سلة المشتريات، ثم خدمة ملفات المستخدمين، وأخيراً الواجهة الرئيسية للموقع أصبحت بطيئة جداً ثم توقفت عن العمل تماماً. النظام كله انهار!
في غرفة العمليات، كان الصراخ يعلو: “شو القصة؟”، “لماذا كل شيء توقف؟”، “أعيدوا تشغيل الخوادم!”. لكن إعادة التشغيل لم تكن تحل المشكلة، فالنظام كان ينهار مجدداً بعد دقائق. بعد تحليل مرهق تحت ضغط هائل، اكتشفنا السبب: خدمة “التوصيات” البسيطة تلك كانت تعاني من خطأ أدى لبطء شديد في استجابتها. كل خدمة أخرى كانت تحاول استدعاءها كانت تنتظر الرد، مستهلكةً أحد “الخيوط” (Threads) المتاحة لديها. ومع تزايد الطلبات، استُهلكت كل الخيوط في الانتظار، مما أدى إلى شلل تلك الخدمات، والتي بدورها تسببت في شلل خدمات أخرى تعتمد عليها. كان انهياراً متسلسلاً كارثياً.
هنا، في قلب هذه الفوضى، تذكرت نمطاً تصميمياً كنت قد قرأت عنه اسمه “قاطع الدائرة” (Circuit Breaker). كان هذا هو اليوم الذي انتقل فيه هذا المفهوم من مجرد نظرية في رأسي إلى أداة لا غنى عنها في صندوق أدواتي. دعوني أشرح لكم كيف يعمل هذا المنقذ.
ما هو الانهيار المتسلسل (Cascading Failure) في الخدمات المصغرة؟
قبل أن نتعمق في الحل، يجب أن نفهم المشكلة جيداً. عندما تبني نظامك باستخدام معمارية الخدمات المصغرة (Microservices)، فأنت تقوم بتقسيم تطبيقك الكبير إلى خدمات صغيرة ومستقلة تتواصل مع بعضها عبر الشبكة. هذا رائع من أجل التوسع والتطوير المستقل، لكنه يخلق نقطة ضعف جديدة: الاعتماد على الشبكة وعلى استقرار الخدمات الأخرى.
تخيل السيناريو التالي:
- الخدمة أ (مثلاً: واجهة المتجر) تحتاج إلى معلومات من الخدمة ب (مثلاً: توصيات المنتجات).
- تقوم الخدمة أ بإرسال طلب إلى الخدمة ب وتنتظر الرد.
- لسبب ما، الخدمة ب أصبحت بطيئة جداً أو توقفت عن العمل.
- الخدمة أ لا تزال تنتظر. الخيط (Thread) الذي أرسل الطلب يبقى “محجوزاً” ومنتظراً.
- يأتي طلب مستخدم جديد إلى الخدمة أ، فتستخدم خيطاً جديداً لاستدعاء الخدمة ب، وهذا الخيط أيضاً يُحجز.
- مع تزايد الطلبات، يتم حجز جميع الخيوط المتاحة في الخدمة أ، كلها تنتظر الخدمة ب التي لا ترد.
- الآن، الخدمة أ نفسها تصبح غير قادرة على الاستجابة لأي طلبات جديدة، حتى لو كانت لا تعتمد على الخدمة ب. لقد استنفدت مواردها.
- إذا كانت هناك خدمة ج تعتمد على الخدمة أ، فإنها ستبدأ بالمعاناة أيضاً. وهكذا، ينتشر الفشل كالنار في الهشيم عبر النظام بأكمله.
هذا هو الانهيار المتسلسل. خطأ صغير في مكان واحد يؤدي إلى كارثة شاملة، تماماً مثل قاطع كهربائي صغير يتسبب في إطفاء مدينة بأكملها لو لم تكن هناك حماية.
البطل الصامت: نمط قاطع الدائرة (The Circuit Breaker Pattern)
فكرة هذا النمط مستوحاة مباشرة من قاطع الدائرة الكهربائي في منزلك. وظيفته ليست إصلاح الجهاز المعطوب، بل حماية بقية المنزل من الضرر عن طريق “فصل” الدائرة عند حدوث مشكلة. نمط قاطع الدائرة البرمجي يفعل الشيء نفسه بالضبط: إنه يراقب الاتصالات بين الخدمات.
يعمل قاطع الدائرة في ثلاث حالات:
الحالة الأولى: مُغلق (Closed)
هذا هو الوضع الطبيعي. تكون الدائرة “مغلقة”، والطلبات تتدفق من الخدمة أ إلى الخدمة ب بشكل عادي. في هذه الأثناء، يقوم قاطع الدائرة بمراقبة هذه الطلبات وعدّ حالات الفشل (مثل انتهاء مهلة الانتظار “Timeout” أو أخطاء الشبكة).
الحالة الثانية: مفتوح (Open)
إذا تجاوز عدد حالات الفشل حداً معيناً خلال فترة زمنية محددة (مثلاً، 5 حالات فشل في 30 ثانية)، “يقفز” قاطع الدائرة ويغير حالته إلى “مفتوح”.
الآن، وهذا هو الجزء السحري، أي طلب جديد من الخدمة أ إلى الخدمة ب سيتم رفضه فوراً دون حتى محاولة إرساله عبر الشبكة. سيعيد قاطع الدائرة خطأً فورياً (مثل BrokenCircuitException). هذا الإجراء له فائدتان عظيمتان:
- حماية الخدمة الطالبة (أ): لم تعد تهدر مواردها وخيوطها في انتظار خدمة لا تستجيب. يمكنها الآن التعامل مع الفشل بسرعة (مثلاً، عن طريق عرض رسالة للمستخدم أو استخدام بيانات بديلة).
- إعطاء فرصة للخدمة المتعثرة (ب): عن طريق إيقاف سيل الطلبات الموجهة إليها، نعطيها فرصة لتتعافى. ربما كانت تعاني من ضغط عالٍ وتحتاج فقط لبعض الوقت لتفريغ قائمة المهام المتراكمة لديها.
الحالة الثالثة: نصف مفتوح (Half-Open)
بعد فترة زمنية محددة والدائرة في حالة “مفتوح” (مثلاً، بعد دقيقة واحدة)، ينتقل قاطع الدائرة إلى حالة “نصف مفتوح”. في هذه الحالة، يسمح بمرور طلب “تجريبي” واحد فقط إلى الخدمة ب.
- إذا نجح الطلب التجريبي: هذا مؤشر على أن الخدمة ب قد تعافت. يعود قاطع الدائرة إلى حالة “مغلق” وتعود الأمور إلى طبيعتها.
- إذا فشل الطلب التجريبي: هذا يعني أن المشكلة لا تزال قائمة. يعود قاطع الدائرة إلى حالة “مفتوح” مرة أخرى ويبدأ مؤقت جديد، مانعاً المزيد من الطلبات.
لنطبق الأمر عملياً: مثال برمجي
لحسن الحظ، لست بحاجة لبناء هذا المنطق من الصفر. هناك مكتبات رائعة توفر تطبيقات جاهزة لنمط قاطع الدائرة في معظم لغات البرمجة. في عالم .NET، أشهر مكتبة لهذا الغرض هي Polly. دعونا نرى مثالاً بسيطاً باستخدام C#.
لنفترض أن لديك خدمة تقوم باستدعاء خدمة توصيات المنتجات:
// الكود قبل استخدام قاطع الدائرة (الطريقة الخطرة)
public async Task<List<Product>> GetRecommendations()
{
// هذا الاستدعاء قد يستغرق وقتاً طويلاً أو يفشل، مما يحجز الخيط
var response = await _httpClient.GetAsync("http://recommendation-service/api/products");
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsAsync<List<Product>>();
}
الآن، دعنا نضف “بوليصة” حماية باستخدام Polly:
الخطوة 1: تعريف سياسة قاطع الدائرة
في مكان ما عند بدء تشغيل تطبيقك، ستقوم بتعريف سياستك.
// تعريف سياسة قاطع الدائرة
// ستفتح الدائرة بعد 5 محاولات فاشلة متتالية
// وستبقى مفتوحة لمدة 30 ثانية
var circuitBreakerPolicy = Policy
.Handle<HttpRequestException>()
.CircuitBreakerAsync(
exceptionsAllowedBeforeBreaking: 5,
durationOfBreak: TimeSpan.FromSeconds(30)
);
الخطوة 2: استخدام السياسة عند استدعاء الخدمة
الآن، سنقوم “بتغليف” استدعاء الشبكة بهذه السياسة.
public async Task<List<Product>> GetRecommendationsWithBreaker()
{
try
{
// ExecuteAsync سيقوم بتطبيق منطق قاطع الدائرة على الكود بداخله
return await circuitBreakerPolicy.ExecuteAsync(async () =>
{
var response = await _httpClient.GetAsync("http://recommendation-service/api/products");
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsAsync<List<Product>>();
});
}
catch (BrokenCircuitException)
{
// الدائرة مفتوحة! لا تحاول مجدداً.
// هنا يجب أن نطبق منطق "الخطة البديلة" (Fallback)
Console.WriteLine("Circuit is open! Returning fallback recommendations.");
return GetFallbackRecommendations();
}
catch (HttpRequestException)
{
// فشل آخر (مثلاً، أول فشل قبل فتح الدائرة)
Console.WriteLine("Request failed. Returning fallback recommendations.");
return GetFallbackRecommendations();
}
}
private List<Product> GetFallbackRecommendations()
{
// أعد قائمة منتجات عامة أو من الذاكرة المؤقتة (Cache)
return new List<Product> { /* ... قائمة منتجات الأكثر مبيعاً ... */ };
}
لاحظوا جمال هذا الحل. الكود الرئيسي الذي يستدعي الخدمة لم يتغير كثيراً. كل ما فعلناه هو تغليفه بسياسة حماية. الأهم من ذلك، عندما تكون الدائرة مفتوحة، يتم تنفيذ منطق GetFallbackRecommendations فوراً، مما يعيد استجابة سريعة ومفيدة للمستخدم (ولو كانت غير مخصصة) ويحمي نظامنا من الانهيار.
نصائح من خبرة أبو عمر 🧔
بعد استخدام هذا النمط لسنوات، اسمحوا لي أن أقدم لكم بعض النصائح العملية:
- لا تستخدم قاطعاً واحداً للجميع: لكل خدمة خارجية تستدعيها خصائصها. خدمة الدفع الإلكتروني حرجة جداً وتتطلب عتبات فشل مختلفة عن خدمة جلب أسعار العملات. قم بإنشاء وتخصيص سياسة قاطع دائرة لكل اعتمادية (Dependency) على حدة.
- راقب قواطعك: أهم شيء هو مراقبة حالة قواطع الدائرة لديك. يجب أن تعرض لوحة المراقبة (Dashboard) الخاصة بك بوضوح أي الدوائر مفتوحة، وكم مرة فُتحت. هذه المعلومة لا تقدر بثمن لتشخيص المشاكل بشكل استباقي. عندما ترى دائرة تفتح وتغلق بشكل متكرر، فهذا مؤشر قوي على وجود مشكلة في الخدمة التي تستدعيها.
- اضبط إعداداتك بحكمة: عدد مرات الفشل المسموح بها ومدة فتح الدائرة هي إعدادات حساسة. مدة قصيرة جداً قد لا تعطي الخدمة الأخرى وقتاً للتعافي، ومدة طويلة جداً قد تؤثر سلباً على تجربة المستخدم. ابدأ بقيم منطقية (مثل 5 محاولات و 30-60 ثانية) ثم قم بضبطها بناءً على المراقبة.
- اجمع بين الأنماط: قاطع الدائرة يعمل بشكل أفضل عند دمجه مع أنماط مرونة أخرى مثل:
- نمط إعادة المحاولة (Retry): قبل أن تحسب محاولة فاشلة، يمكنك إعادة المحاولة مرة أو مرتين مع فاصل زمني بسيط. قد يكون الفشل عشوائياً ومؤقتاً.
- نمط المهلة (Timeout): لا تنتظر إلى الأبد! حدد مهلة زمنية قصيرة ومعقولة لكل طلب شبكة.
- نمط العزل (Bulkhead): يخصص مجموعة موارد (مثل الخيوط) لكل اعتمادية، بحيث لو فشلت إحداها، لا تؤثر على موارد المخصصة للاعتماديات الأخرى.
الخلاصة: لا تبنِ بيتاً من زجاج 🏠
في عالم الخدمات المصغرة والأنظمة الموزعة، الفشل ليس احتمالاً، بل هو حتمية. الشبكات غير موثوقة، والخدمات قد تفشل لألف سبب وسبب. مهمتك كمطور ليست منع الفشل، بل بناء أنظمة قادرة على الصمود والتعافي بأناقة عند حدوثه.
الانهيارات المتسلسلة هي العدو الأول لهذه الأنظمة، ونمط قاطع الدائرة هو سلاحك الأكثر فعالية ضده. إنه يحول الفشل الكارثي إلى تدهور بسيط ومتحكم به في الخدمة، مما يمنح نظامك فرصة للنجاة والتنفس والتعافي.
تذكروا قصتي جيداً في المرة القادمة التي تبنون فيها خدمة تستدعي خدمة أخرى. لا تفترضوا أن كل شيء سيكون على ما يرام. افترضوا الأسوأ، واستعدوا له. طبقوا نمط قاطع الدائرة، وستشكرونني لاحقاً. 👍