المستوى: متوسط — يفترض هذا المقال أنك تعرف أساسيات الـ microservices واستدعاءات HTTP بين الخدمات. لو لسه مبتدئ تماماً، المثال الأول هيوصّلك الفكرة قبل الجزء التقني.
Circuit Breaker: امنع خدمة واحدة بطيئة من إسقاط نظامك بالكامل
لو خدمة دفع واحدة بطيئة قدرت توقّع 8 خدمات معاها في أقل من 30 ثانية، المشكلة مش في الخدمة البطيئة نفسها. المشكلة إن مفيش حاجة بتقطع النزيف. نمط Circuit Breaker بيعزل الخدمة العاطلة في ملي ثانية، ويحافظ على باقي النظام شغّال بدل ما ينهار بالكامل.
المشكلة باختصار
تخيل عندك خدمة checkout بتنادي خدمة payment عبر HTTP. الـ timeout متظبّط على 5 ثوانٍ. فجأة خدمة الدفع بقت بطيئة وبتاخد 5 ثوانٍ في كل طلب قبل ما تفشل.
اللي بيحصل فعلاً: كل request جاي على checkout بيفتح thread ويستنى 5 ثوانٍ. لو عندك thread pool بـ 200 thread وبيوصلك 100 طلب/ثانية، الـ pool بيتملي في تانيتين. بعد كده checkout نفسها بقت مش بترد، رغم إن المشكلة الأصلية في خدمة تانية. ده اسمه cascading failure — الفشل بيتنقّل من خدمة لخدمة لحد ما النظام كله يقع.
الفكرة بمثال قبل التقنية
في بيتك فيه قاطع كهرباء (الطبلون). لو حصل short circuit في جهاز، القاطع بيفصل الدايرة دي فوراً. ليه؟ عشان ميسيبش التيار الزايد يحرق أسلاك البيت كله ويوصل لحريق. القاطع بيضحّي بجهاز واحد عشان ينقذ البيت.
الفصل ده مش دائم. انت بتصلّح العطل، وبعدين بترفع القاطع تاني. ولو رفعته وفصل تاني على طول، معناه العطل لسه موجود فبتسيبه مفصول.
دلوقتي الكلام العلمي: نمط Circuit Breaker، اللي وثّقه Michael Nygard في كتاب Release It! ونشره Martin Fowler، بيعمل بالظبط نفس الفكرة حوالين أي استدعاء خارجي. بيلفّ الاستدعاء في كائن (proxy) بيراقب نسبة الفشل. لو النسبة عدّت حد معين، الـ breaker "بيفتح" ويرفض الطلبات فوراً بدل ما يستنى timeout كل مرة.
الحالات الثلاث: Closed و Open و Half-Open
الـ Circuit Breaker عنده 3 حالات، وده قلب الموضوع:
- Closed (مغلق): الوضع الطبيعي. كل الطلبات بتعدّي للخدمة. الـ breaker بيعدّ نسبة الفشل في الخلفية.
- Open (مفتوح): لما نسبة الفشل تعدّي الحد (مثلاً 50%)، الدايرة بتفتح. أي طلب جديد بيترفض فوراً (fail fast) من غير ما يلمس الخدمة العاطلة أصلاً. هنا بنرجّع fallback أو خطأ سريع.
- Half-Open (نصف مفتوح): بعد فترة انتظار (مثلاً 30 ثانية)، الـ breaker بيسمح بعدد محدود من الطلبات التجريبية. لو نجحت، بيرجع Closed. لو فشلت، بيرجع Open تاني ويستنى من جديد.
الفرق العملي ضخم: من غير breaker، كل طلب فاشل بياخد 5 ثوانٍ. مع breaker مفتوح، الطلب بيترفض في أقل من 1 ملي ثانية. يعني الـ P99 latency للطلبات دي بينزل من 5,000ms لـ أقل من 5ms، والـ threads بتفضل فاضية لباقي الشغل.
كود شغّال: Node.js بمكتبة opossum
أشهر مكتبة Circuit Breaker في عالم Node اسمها opossum. ده مثال كامل قابل للنسخ:
const CircuitBreaker = require('opossum');
const axios = require('axios');
async function callPayment(orderId) {
const res = await axios.post('http://payment:8080/charge',
{ orderId }, { timeout: 2000 });
return res.data;
}
const options = {
timeout: 2000, // اعتبر الطلب فاشل لو عدّى 2 ثانية
errorThresholdPercentage: 50, // افتح الدايرة عند 50% فشل
resetTimeout: 30000, // استنى 30 ثانية قبل Half-Open
volumeThreshold: 20 // متفتحش قبل 20 طلب على الأقل
};
const breaker = new CircuitBreaker(callPayment, options);
// fallback لما الدايرة مفتوحة — قرار عمل واضح بدل ما الطلب يعلّق
breaker.fallback(() => ({ status: 'queued', message: 'الدفع هيتعالج بعد لحظات' }));
breaker.on('open', () => console.warn('Circuit OPEN: payment معزولة'));
breaker.on('halfOpen', () => console.info('Circuit HALF-OPEN: بنجرّب تاني'));
breaker.on('close', () => console.info('Circuit CLOSED: رجعت طبيعية'));
module.exports = (orderId) => breaker.fire(orderId);النقطة المهمة هنا هي الـ volumeThreshold. من غيره، أول طلبين يفشلوا ممكن يفتحوا الدايرة (100% فشل من طلبين)، وده false positive. الـ breaker لازم يستنى عيّنة كافية قبل ما ياخد قرار.
نفس الفكرة في Spring بـ resilience4j
لو شغّال Java، مكتبة resilience4j هي البديل القياسي بعد ما Hystrix دخلت maintenance mode. الإعداد بيتعمل declaratively:
resilience4j:
circuitbreaker:
instances:
paymentService:
failureRateThreshold: 50
slowCallRateThreshold: 80
slowCallDurationThreshold: 2s
waitDurationInOpenState: 30s
permittedNumberOfCallsInHalfOpenState: 5
minimumNumberOfCalls: 20
slidingWindowSize: 100@CircuitBreaker(name = "paymentService", fallbackMethod = "paymentFallback")
public PaymentResult charge(Order order) {
return paymentClient.charge(order); // الاستدعاء الخارجي
}
public PaymentResult paymentFallback(Order order, Throwable t) {
return PaymentResult.queued(order.getId()); // قرار بديل واضح
}لاحظ slowCallRateThreshold: ده بيفتح الدايرة مش بس على الأخطاء، لكن كمان لو 80% من الطلبات بقت "بطيئة" (أبطأ من 2 ثانية). ده مهم لأن الخدمة البطيئة بتأذي أكتر من الخدمة اللي بترجّع error بسرعة.
المراقبة: من غيرها الـ breaker أعمى
الـ Circuit Breaker لازم يصدّر metrics. اربط الـ events (open/close) على Prometheus، واعمل alert لما الدايرة تفتح. السبب: فتح الدايرة مش هو المشكلة — هو عَرَض لمشكلة في الـ dependency. لو خدمة الدفع بتاعتك بتفتح الدايرة 12 مرة في الساعة، ده مؤشر إنها محتاجة تدخّل، مش إن الـ breaker شغّال كويس وخلاص.
الـ trade-offs اللي محدش بيقولك عليها
- بتكسب عزل الفشل، بتخسر بساطة. كل استدعاء خارجي بقى محتاج إعداد threshold و fallback. ده كود زيادة لازم يتصان ويتاختبر.
- الـ tuning صعب. threshold واطي جداً بيفتح الدايرة على spikes مؤقتة بريئة (false positive). threshold عالي جداً بيخلّي الـ breaker يتأخر في الحماية. الافتراض إن عندك trafic كفاية تقيس عليه نسبة فشل ذات معنى.
- ممكن يخبّي مشاكل حقيقية. لو الـ fallback بيرجّع بيانات قديمة (cache) بصمت، المستخدم ممكن ياخد قرار غلط مبني على بيانات بايتة. الـ fallback لازم يكون واضح إنه fallback.
- مش بديل عن الـ retry ولا الـ timeout. هو طبقة فوقهم. الترتيب الصح: timeout قصير → retry محدود → circuit breaker → fallback.
متى لا تستخدم هذه الطريقة
الـ Circuit Breaker مش مجاني، ومش دايماً مناسب:
- monolith من غير استدعاءات خارجية: لو كودك كله في عملية واحدة وفي نفس الذاكرة، مفيش دايرة تتقطع. استخدمه بس حوالين الـ network calls و الـ I/O الخارجي (DB, API, queue).
- عمليات كتابة حرجة وغير idempotent من غير fallback منطقي: لو الطلب لازم ينجح ومفيش بديل مقبول، الـ fail fast هيخسّرك المعاملة. هنا الأفضل retry مع backoff + dead letter queue.
- traffic واطي جداً: أداة داخلية بتتنفذ 10 مرات في اليوم مش هتجمّع عيّنة كافية عشان نسبة الفشل تبقى ذات معنى. التعقيد هنا overhead بدون عائد.
الخطوة التالية
افتح أبطأ استدعاء خارجي في خدمتك (غالباً نداء على API تابع لفريق تاني أو بوابة دفع). لُفّه بـ opossum أو resilience4j بإعداد بسيط: errorThresholdPercentage: 50 و resetTimeout: 30s و volumeThreshold: 20. ضيف fallback يرجّع قرار واضح، واربط event الـ open على alert. شغّل load test بسيط يخلّي الـ dependency تفشل، واتأكد إن الـ P99 نزل وإن باقي الخدمة فضلت بترد. لو فضلت بترد وانت بتقتل الـ dependency، يبقى الـ breaker شغّال صح.
المصادر
- Martin Fowler — CircuitBreaker (الوصف الأصلي للنمط والحالات الثلاث).
- Michael Nygard — كتاب Release It! (المصدر الأساسي لنمط Circuit Breaker و Stability Patterns).
- resilience4j — CircuitBreaker Documentation (الإعدادات و sliding window و slow call rate).
- Nodeshift — opossum Circuit Breaker for Node.js (التوثيق والخيارات).
- Microsoft Azure Architecture Center — Circuit Breaker pattern (سياق المعمارية و cascading failures).