أذكرها وكأنها البارحة، ليلة خميس هادئة، وكنا نراقب إطلاق تحديث جديد لمنصة تجارة إلكترونية ضخمة. الأمور كانت تسير بسلاسة، والفريق كان متفائلاً. كنا قاعدين في أمان الله، نشرب القهوة ونتبادل النكات، وفجأة… بدأت التنبيهات تنهال علينا كالمطر. “System is down!”, “Checkout failing!”, “API Unresponsive!”.
في لحظات، تحول الهدوء إلى فوضى عارمة. “شو القصة يا جماعة؟” صرخ مدير المشروع. كلنا كنا في حيرة، فالتحديث الذي أطلقناه كان بسيطاً ولا يمس الخدمات الأساسية. بدأنا رحلة البحث المحمومة في سجلات الأخطاء (Logs). كل شيء كان يصرخ “Timeout”. الخدمات لا تستجيب لبعضها البعض. الأسوأ من ذلك، أن النظام بأكمله كان يحتضر، حتى الصفحات التي لا علاقة لها بعملية الشراء أصبحت بطيئة جداً أو لا تفتح على الإطلاق.
بعد ساعات من التوتر والضغط، اكتشفنا السبب. خدمة طرف ثالث (Third-party) صغيرة، بالكاد نلتفت لوجودها، مسؤولة عن حساب تكلفة الشحن… توقفت عن العمل. هذا الخطأ الصغير كان كحجر الدومينو الأول. كل طلب شراء كان ينتظر هذه الخدمة، مما أدى إلى استهلاك كل الـ Threads المتاحة في خدمة الطلبات (Orders Service). بدورها، خدمة الطلبات أصبحت لا تستجيب، مما أثر على خدمة المستخدمين (Users Service) التي تنتظر تأكيداً منها، وهكذا دواليك. لقد كان انهياراً متتالياً (Cascading Failure) بكل ما تحمله الكلمة من معنى، والسبب؟ خطأ واحد بسيط.
في تلك الليلة، لم ننم. لكننا خرجنا بدرس غيّر طريقتنا في بناء الأنظمة إلى الأبد. تعلمنا بالطريقة الصعبة أهمية بناء “جدران حماية” بين خدماتنا، وهنا كان بطل قصتنا: نمط قاطع الدائرة (Circuit Breaker).
ما هو جحيم الانهيارات المتتالية (Cascading Failures)؟
قبل أن نغوص في الحل، دعونا نفهم الوحش الذي واجهناه. في عالم الخدمات المصغرة (Microservices)، نظامك لم يعد كتلة واحدة، بل هو عبارة عن شبكة من الخدمات المستقلة التي تتواصل مع بعضها. خدمة الدفع تكلم خدمة الطلبات، التي تكلم خدمة المخزون، التي تكلم خدمة الشحن. هذا جميل ورائع ويعطينا مرونة كبيرة، لكنه يخلق نقطة ضعف جديدة.
تخيلها كصف من أحجار الدومينو. إذا سقطت خدمة واحدة (حجر واحد)، فإنها قد تتسبب في إسقاط الخدمة التي تعتمد عليها، والتي بدورها تسقط خدمة أخرى، وهكذا في سلسلة لا تنتهي حتى ينهار النظام بأكمله. هذا هو الانهيار المتتالي.
لماذا يحدث هذا؟
- استنزاف الموارد: عندما تفشل خدمة ما في الاستجابة، فإن الخدمة التي تطلبها تظل تنتظر، محتجزةً موارد ثمينة مثل الاتصالات (Connections) وخيوط المعالجة (Threads). مع تزايد الطلبات، يتم استنزاف كل الموارد المتاحة، وتتوقف الخدمة عن العمل.
- المهلة الزمنية (Timeouts): قد تظن أن وضع مهلة زمنية للطلب (Request Timeout) يحل المشكلة. لكنه ليس كافياً. إذا كانت كل الطلبات تفشل بعد 30 ثانية مثلاً، فأنت لا تزال تهدر 30 ثانية من الموارد ووقت المستخدم في كل مرة، مما يؤدي إلى تباطؤ النظام بأكمله.
المشكلة أن النظام يستمر في محاولة “ضرب رأسه بالحائط”، أي استدعاء الخدمة الفاشلة مراراً وتكراراً، مما يزيد الطين بلة ويمنع الخدمة الفاشلة من التعافي (لأنها تتعرض لوابل من الطلبات التي لا تستطيع معالجتها).
المنقذ: نمط قاطع الدائرة (Circuit Breaker Pattern)
هنا يأتي دور “أبو العُمر”، بطل قصتنا التقني. فكرة نمط قاطع الدائرة مستوحاة مباشرة من قاطع الدائرة الكهربائي الموجود في منزلك. وظيفته بسيطة: عندما يحدث حمل زائد أو تماس كهربائي، “يفصل” القاطع التيار ليحمي أجهزتك والمنزل من الحريق. لا يستمر في محاولة تمرير الكهرباء.
في عالم البرمجيات، قاطع الدائرة هو كائن (Object) يراقب استدعاءات عملية معينة (عادةً استدعاء خدمة بعيدة). وله ثلاث حالات رئيسية:
1. الحالة المغلقة (Closed)
هذا هو الوضع الطبيعي. يكون القاطع “مغلقاً”، مما يعني أن الدائرة مكتملة، والطلبات تتدفق بحرية إلى الخدمة البعيدة. في هذه الحالة، يقوم القاطع بحساب عدد مرات الفشل. إذا ظل عدد مرات الفشل تحت حد معين (مثلاً، 5 أخطاء متتالية)، يبقى الوضع على ما هو عليه.
2. الحالة المفتوحة (Open)
إذا تجاوز عدد مرات الفشل الحد المسموح به، “يفتح” القاطع الدائرة. يا عمي، خلص، بكفي محاولات! في هذه الحالة، أي استدعاء جديد للخدمة يفشل فوراً (Fail Fast) دون حتى محاولة الاتصال بالخدمة البعيدة. يتم إرجاع خطأ فوري للعميل (المستدعي). هذا هو السحر! بهذه الطريقة، نحن:
- نحمي نظامنا من استنزاف الموارد.
- نعطي الخدمة الفاشلة فرصة “لتلتقط أنفاسها” وتتعافى، لأننا توقفنا عن إغراقها بالطلبات.
يبقى القاطع في هذه الحالة لمدة زمنية محددة (مثلاً، 30 ثانية).
3. الحالة نصف المفتوحة (Half-Open)
بعد انقضاء مدة الانتظار في الحالة المفتوحة، ينتقل القاطع إلى حالة “نصف مفتوحة”. إنها مرحلة جس النبض. يسمح القاطع بمرور طلب واحد فقط (طلب تجريبي) إلى الخدمة البعيدة.
- إذا نجح هذا الطلب: يفترض القاطع أن الخدمة قد تعافت، فيعود إلى الحالة المغلقة (Closed)، وتعود الحياة إلى طبيعتها.
- إذا فشل هذا الطلب: يستنتج القاطع أن المشكلة لا تزال قائمة، فيعود فوراً إلى الحالة المفتوحة (Open) ويبدأ فترة انتظار جديدة.
ببساطة، قاطع الدائرة يمنع تطبيقك من القيام بشيء يعرف أنه سيفشل على الأرجح، مما يمنحه المرونة والقدرة على الصمود.
كيف طبقنا قاطع الدائرة؟ (مثال عملي)
الحكي سهل، خلينا نشوف الكود. سأستخدم مثالاً بلغة C# مع مكتبة Polly الرائعة، وهي مكتبة متخصصة في بناء تطبيقات مرنة ومقاومة للأخطاء. المبدأ نفسه ينطبق على أي لغة أخرى مع مكتبات مثل Resilience4j في Java أو Hystrix (الذي أصبح في وضع الصيانة) أو libraries مشابهة في Python و JavaScript.
المرحلة الأولى: قبل قاطع الدائرة (الكود الكارثي)
هذا هو الكود الذي كان لدينا تقريباً، والذي كان يسبب الكارثة. مجرد استدعاء مباشر للخدمة.
// C# Example with HttpClient
public class ShippingServiceClient
{
private readonly HttpClient _httpClient;
public ShippingServiceClient(HttpClient httpClient)
{
_httpClient = httpClient;
_httpClient.BaseAddress = new Uri("http://unstable-shipping-service.com/");
}
public async Task<decimal> GetShippingCost(string address)
{
// This call will hang or throw an exception if the service is down
var response = await _httpClient.GetAsync($"/calculate?address={address}");
if (!response.IsSuccessStatusCode)
{
// This will keep happening, exhausting resources
throw new Exception("Shipping service is unavailable.");
}
var cost = await response.Content.ReadAsStringAsync();
return decimal.Parse(cost);
}
}
المشكلة هنا أنه مع كل طلب فاشل، ننتظر المهلة الزمنية كاملة، ونحجز الموارد، ونساهم في انهيار النظام.
المرحلة الثانية: تطبيق قاطع الدائرة باستخدام مكتبة Polly
الآن، لنقم بإضافة قاطع الدائرة باستخدام Polly. الكود يصبح أكثر ذكاءً.
أولاً، نقوم بتعريف “سياسة” قاطع الدائرة عند إعداد خدماتنا (في ملف Startup.cs أو Program.cs في .NET Core):
// C# Example with Polly
services.AddHttpClient<ShippingServiceClient>()
.AddPolicyHandler(
HttpPolicyExtensions
.HandleTransientHttpError() // Handles typical HTTP errors (5xx, 408)
.CircuitBreakerAsync(
handledEventsAllowedBeforeBreaking: 5, // Break after 5 consecutive failures
durationOfBreak: TimeSpan.FromSeconds(30) // Stay open for 30 seconds
)
);
ماذا فعلنا هنا؟
HandleTransientHttpError(): أخبرنا Polly أننا نعتبر أخطاء HTTP المؤقتة (مثل 503 Service Unavailable أو 408 Request Timeout) بمثابة فشل.CircuitBreakerAsync(...): طبقنا سياسة قاطع الدائرة.handledEventsAllowedBeforeBreaking: 5: اسمح بـ 5 محاولات فاشلة متتالية. بعد الفشل الخامس، افتح الدائرة.durationOfBreak: TimeSpan.FromSeconds(30): عند فتح الدائرة، ابقَ في الحالة المفتوحة لمدة 30 ثانية.
كود العميل (ShippingServiceClient) نفسه لا يتغير! هذا هو جمال استخدام هذه المكتبات. السياسة تُطبّق بشفافية تامة.
الآن، عندما تتوقف خدمة الشحن عن العمل:
- أول 5 طلبات ستفشل (ربما بعد timeout).
- الطلب السادس وما يليه (خلال الـ 30 ثانية القادمة) سيفشلون فوراً مع إلقاء استثناء من نوع
BrokenCircuitException. لن يتم إرسال أي طلب HTTP. - بعد 30 ثانية، ينتقل القاطع إلى حالة Half-Open ويسمح بمرور طلب واحد. إذا نجح، تُغلق الدائرة. إذا فشل، تُفتح مجدداً لـ 30 ثانية أخرى.
نصائح من خبرة أبو عمر (مش بس كود)
تطبيق النمط تقنياً هو الجزء السهل. الجزء الأصعب هو استخدامه بحكمة. الحكي بينا، رأيت الكثير من المطورين يطبقونه بشكل خاطئ. إليك بعض النصائح من القلب:
نصيحة 1: لا تضع قاطع دائرة على كل شيء!
قاطع الدائرة مصمم لحماية نظامك من أخطاء العمليات التي قد تكون طويلة الأمد أو تتطلب موارد، مثل استدعاءات الشبكة (APIs, Databases) أو عمليات I/O. لا تستخدمه لاستدعاء دالة داخل نفس العملية (in-process call) إلا في حالات نادرة جداً. قد يؤدي ذلك إلى تعقيد لا مبرر له.
نصيحة 2: اضبط الإعدادات بحكمة (Configure Wisely)
الأرقام التي وضعتها في المثال (5 محاولات، 30 ثانية) ليست مقدسة. يجب أن تضبطها بناءً على طبيعة الخدمة:
- حساسية الخطأ: كم عدد الأخطاء المتتالية التي يمكنك تحملها قبل أن تقرر أن الخدمة “ميتة”؟ خدمة حيوية قد تحتاج لرقم أقل (2-3)، بينما خدمة أقل أهمية قد تتحمل رقماً أعلى.
- مدة الفتح: ما هو الوقت المتوقع لتعافي الخدمة؟ 30 ثانية هي بداية جيدة، لكن إذا كانت الخدمة تحتاج عادةً لدقائق لتعيد تشغيل نفسها، فاجعل المدة أطول. مدة قصيرة جداً قد لا تعطي الخدمة فرصة للتعافي.
نصيحة 3: ماذا تفعل عندما يكون القاطع مفتوحاً؟ (Fallback)
هذه هي أهم نصيحة. أن يفشل طلبك بسرعة هذا جيد، لكن الأفضل هو أن يكون لديك خطة بديلة (Fallback). عندما يفتح القاطع، بدلاً من إرجاع خطأ للمستخدم، حاول أن تقدم شيئاً مفيداً.
يمكنك توسيع سياسة Polly لتشمل الـ Fallback:
// C# Fallback Example
var circuitBreakerPolicy = HttpPolicyExtensions
.HandleTransientHttpError()
.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30));
// Define what to do when the circuit is open
var fallbackPolicy = Policy<HttpResponseMessage>
.Handle<BrokenCircuitException>()
.FallbackAsync(
fallbackAction: (cancellationToken) =>
{
// Return a default/cached response
var fallbackResponse = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent("10.00") // Default shipping cost
};
return Task.FromResult(fallbackResponse);
}
);
// Wrap the circuit breaker with the fallback
var policyWrap = Policy.WrapAsync(fallbackPolicy, circuitBreakerPolicy);
// Apply the wrapped policy
services.AddHttpClient<ShippingServiceClient>().AddPolicyHandler(policyWrap);
في هذا المثال، إذا كانت الدائرة مفتوحة، بدلاً من رمي استثناء، سنقوم بإرجاع تكلفة شحن افتراضية (10.00). قد يكون هذا أفضل من فشل عملية الشراء بأكملها. خيارات الـ Fallback الأخرى تشمل: إرجاع بيانات من ذاكرة التخزين المؤقت (Cache)، أو إرجاع رسالة للمستخدم “لا يمكن حساب تكلفة الشحن حالياً، يرجى المحاولة لاحقاً”.
نصيحة 4: المراقبة والإنذار (Monitoring & Alerting)
قاطع الدائرة ليس مجرد آلية حماية، بل هو أيضاً أداة تشخيص قوية. عندما “يفتح” قاطع، فهذا مؤشر قوي على وجود مشكلة في خدمة ما. يجب أن تقوم بتسجيل هذه الأحداث (Open, Close, Half-Open) وإرسالها إلى نظام المراقبة الخاص بك (مثل Prometheus, Datadog, Azure Monitor). قم بإعداد تنبيهات لإعلام فريقك فوراً عند فتح دائرة لخدمة حيوية. هذا يجعلك استباقياً في حل المشاكل قبل أن يلاحظها المستخدم.
الخلاصة: من الفوضى إلى المرونة 🧘
في تلك الليلة المشؤومة، تعلمنا أن بناء الأنظمة الموزعة لا يتعلق فقط بكتابة كود يعمل في الظروف المثالية، بل يتعلق ببناء أنظمة تستطيع الصمود والتعافي عند حدوث ما هو غير متوقع. الفشل ليس استثناءً، بل هو جزء طبيعي من حياة هذه الأنظمة.
نمط قاطع الدائرة هو واحد من أهم الأدوات في جعبتنا كمهندسي برمجيات لبناء تطبيقات مرنة (Resilient). إنه يحول الفشل الكارثي المتتالي إلى فشل محلي يمكن التحكم فيه والتعامل معه بأناقة.
نصيحتي الأخيرة لك: لا تنتظر حتى ينهار نظامك لتتعلم هذا الدرس. كن استباقياً. فكر في نقاط الضعف في نظامك، خاصةً عند الاعتماد على خدمات خارجية. طبق نمط قاطع الدائرة، جهز خططاً بديلة (Fallbacks)، وراقب أدائه. عندها فقط، يمكنك أن تنام ليلتك بهدوء، حتى لو قررت إحدى الخدمات أن تأخذ إجازة مفاجئة!