يا جماعة الخير، السلام عليكم ورحمة الله. معكم أخوكم أبو عمر.
خليني أحكيلكم قصة صارت معي ومع فريقي قبل كم سنة، قصة علّمتنا درس قاسي عن بناء الأنظمة الكبيرة. كانت ليلة من ليالي شهر رمضان، والضغط على منصتنا للتجارة الإلكترونية في ذروته، الناس بتشتري هدايا العيد والوضع “ولّعان”. فجأة، بدأت توصلنا تنبيهات زي المطر: “النظام بطيء!”، “عمليات الدفع تفشل!”، “لا يمكن إضافة منتجات للسلة!”.
دخلنا في حالة طوارئ. أنا والشباب الطيبة قضينا ساعات طويلة نحاول نعرف أصل المشكلة. كل الخدمات الأساسية (الدفع، المنتجات، المستخدمين) شكلها شغالة تمام، لكن النظام ككل كان في حالة شلل شبه تام. بعد تحليل عميق وسهر للصبح، اكتشفنا المصيبة. خدمة صغيرة، شبه منسية، مسؤولة عن إرسال إشعارات “تم شحن طلبك” كانت بتواجه مشكلة وتأخذ وقت طويل جداً للرد (Timeout). ولكون خدمة “الطلبات” الرئيسية بتستدعي هاي الخدمة الصغيرة بعد كل عملية شراء ناجحة، كانت كل طلبات الشراء الجديدة تعلق وتنتظر الرد اللي ما بيجي. ومع آلاف الطلبات في الدقيقة، استُنزفت كل موارد الخوادم، وانهار النظام بأكمله… كله بسبب خدمة إشعارات بسيطة!
هذيك الليلة، تعلمنا بالطريقة الصعبة معنى “الفشل المتتالي” (Cascading Failure). ومن يومها، صار عنا صديق جديد في كل مشروع بنبنيه: نمط “قاطع الدائرة” أو الـ Circuit Breaker. خلونا نحكي عنه بالتفصيل.
ما هو “الفشل المتتالي” (Cascading Failure)؟ وليش هو كابوس المبرمجين؟
تخيل أنك تبني نظاماً يعتمد على معمارية الخدمات المصغرة (Microservices). عندك خدمة للمستخدمين، وخدمة للمنتجات، وخدمة للطلبات، وخدمة للدفع، وهكذا. كل خدمة تتواصل مع الأخرى عبر الشبكة لإنجاز مهمة معينة.
الفشل المتتالي هو بالضبط زي أحجار الدومينو. لما خدمة واحدة (خلينا نسميها خدمة B) تفشل أو تصير بطيئة جداً، الخدمة اللي بتستدعيها (خدمة A) بتضل تنتظر الرد. خلال فترة الانتظار هاي، خدمة A بتستهلك موارد ثمينة (مثل الـ threads في السيرفر). الآن، إذا كان عندك ضغط عالي وطلبات كثيرة جاية على خدمة A، رح تلاقي كل مواردها صارت مشغولة بانتظار خدمة B الفاشلة. والنتيجة؟ خدمة A نفسها بتصير غير قادرة على استقبال أي طلبات جديدة، وبتصير تبدو كأنها فاشلة هي الأخرى.
وهون بتبدأ الكارثة. لو في خدمة ثالثة (خدمة C) بتعتمد على خدمة A، رح تلاقيها هي كمان بتعاني وبتفشل. وهكذا، الفشل ينتشر زي النار في الهشيم من خدمة لأخرى حتى ينهار النظام بأكمله. هذا هو كابوس أي مطور يعمل على أنظمة موزعة، لأنه تتبع أصل المشكلة بصير أشبه بالبحث عن إبرة في كومة قش.
الحل السحري: نمط “قاطع الدائرة” (The Circuit Breaker Pattern)
زي ما اسمه يوحي، نمط قاطع الدائرة بيشتغل تماماً مثل قاطع الكهرباء اللي عندك في البيت. لما يصير في حمل كهربائي زائد أو تماس كهربائي، القاطع “بيفصل” تلقائياً عشان يحمي أجهزتك من التلف. بعد ما تصلح المشكلة، بترجع ترفعه عشان ترجع الكهرباء.
في عالم البرمجيات، قاطع الدائرة هو عبارة عن “بروكسي” أو “وسيط” ذكي بنحطه حول استدعاءات الشبكة الخطرة (مثل استدعاء خدمة أخرى). هذا الوسيط بيراقب حالة الطلبات اللي بتروح للخدمة البعيدة. لو لاحظ إن الخدمة هاي بتفشل بشكل متكرر، بيقوم “بفتح الدائرة”، يعني بيفصل الاتصال مؤقتاً.
لما تكون الدائرة مفتوحة، أي محاولة جديدة لاستدعاء الخدمة الفاشلة رح تفشل فوراً بدون ما نحاول نبعث الطلب أصلاً. هذا هو الجمال في الموضوع! بدل ما نضل نستهلك مواردنا في انتظار خدمة ميتة، بنفشل بسرعة (Fail Fast) وبنعطي فرصة للخدمة الفاشلة إنها تتعافى بدون ما نزيد الضغط عليها.
كيف يشتغل قاطع الدائرة؟ (مراحل التشغيل الثلاث)
قاطع الدائرة بيمر بثلاث حالات رئيسية، وفهمها هو مفتاح استخدام النمط بشكل صحيح:
- الدائرة مغلقة (Closed State): هذا هو الوضع الطبيعي. الطلبات تمر من خلال القاطع إلى الخدمة البعيدة بدون أي مشاكل. في هذه الأثناء، القاطع بيعدّ عدد مرات الفشل (مثل حالات الـ Timeout أو الأخطاء من نوع 5xx).
- الدائرة مفتوحة (Open State): إذا وصل عدد مرات الفشل لحد معين (مثلاً، 5 مرات فشل في آخر دقيقة)، القاطع “يفصل” وينتقل إلى حالة “الدائرة المفتوحة”. في هذه الحالة، ولـمدة زمنية محددة (مثلاً، 30 ثانية)، كل الطلبات الجديدة للخدمة البعيدة يتم رفضها فوراً بدون محاولة الاتصال. هذا بيحمي نظامنا وبيمنح الخدمة الأخرى وقتاً للتعافي.
- نصف مفتوحة (Half-Open State): بعد انتهاء فترة الانتظار (الـ 30 ثانية في مثالنا)، القاطع بينتقل لحالة “نصف مفتوحة”. في هذه الحالة، بيسمح لطلب واحد فقط بالمرور للخدمة البعيدة كـ “جس نبض”.
- إذا نجح هذا الطلب، القاطع بيفترض إن الخدمة رجعت للحياة، وبيقوم “بإغلاق الدائرة” (يرجع لحالة Closed).
- إذا فشل هذا الطلب، القاطع بيعرف إن المشكلة ما زالت قائمة، وبيرجع لحالة “الدائرة المفتوحة” مرة أخرى، ويبدأ فترة انتظار جديدة (ممكن تكون أطول هاي المرة).
باختصار: مغلق (كله تمام) -> فشل متكرر -> مفتوح (إيقاف الطلبات) -> انتظار -> نصف مفتوح (محاولة تجريبية) -> نجاح؟ -> مغلق. فشل؟ -> مفتوح مرة أخرى.
تطبيق عملي: خلينا نكتب كود!
الحكي النظري حلو، بس “الشغل بنعرفه من رجاله”، زي ما بنحكي. خلينا نشوف مثال بسيط باستخدام لغة Python ومكتبة ممتازة اسمها pybreaker لتوضيح الفكرة. رح نعمل محاكاة لخدمة “متقلبة المزاج” (flaky) ونشوف كيف قاطع الدائرة بيحمينا منها.
import pybreaker
import requests
import time
import random
# لنفترض أن هذه هي خدمتنا الخارجية التي تفشل أحيانًا
def call_flaky_service():
"""هذه الخدمة تفشل في 50% من الحالات لغرض المحاكاة."""
print("محاولة استدعاء الخدمة المتقلبة...")
if random.random() > 0.5:
# محاكاة لفشل في الشبكة أو خطأ في الخادم
raise requests.exceptions.RequestException("الخدمة غير متاحة حالياً")
print("... نجح استدعاء الخدمة!")
return {"data": "هذه بيانات مهمة من الخدمة"}
# إعداد قاطع الدائرة
# سيقوم بفتح الدائرة إذا حدث 3 حالات فشل
# ستبقى الدائرة مفتوحة لمدة 10 ثوانٍ قبل الانتقال إلى الحالة النصف مفتوحة
breaker = pybreaker.CircuitBreaker(fail_max=3, reset_timeout=10)
# لنقم الآن ببعض الاستدعاءات ونرى كيف يتصرف القاطع
for i in range(20):
print(f"n--- المحاولة رقم {i+1} ---")
try:
# نستخدم القاطع لحماية استدعاء الخدمة
response = breaker.call(call_flaky_service)
print(f"النتيجة: {response}")
except pybreaker.CircuitBreakerError:
print(">> فشل فوري! الدائرة مفتوحة. لن نحاول الاتصال بالخدمة.")
except requests.exceptions.RequestException as e:
# هذا هو الخطأ الفعلي القادم من الخدمة
print(f"!! حدث فشل أثناء استدعاء الخدمة: {e}")
time.sleep(1) # انتظر ثانية بين كل محاولة
لو شغّلت هذا الكود، رح تلاحظ سلوك مثير للاهتمام. في البداية، ستمر بعض الطلبات وتنجح، وبعضها سيفشل. بعد 3 حالات فشل، ستلاحظ أن الكود سيبدأ بطباعة رسالة “الدائرة مفتوحة!” فوراً، ولن يحاول حتى تنفيذ دالة call_flaky_service. بعد 10 ثوانٍ، سيحاول مرة أخرى (حالة نصف مفتوحة)، وإذا نجحت، ستعود الأمور إلى طبيعتها. هذا هو نمط قاطع الدائرة عملياً!
نصائح أبو عمر الذهبية لتطبيق قاطع الدائرة
الشغلة مش سحر، يا خبير. تطبيق النمط هذا يحتاج لحكمة وخبرة. إليك بعض النصائح من واقع التجربة:
-
لا تستخدمه لكل إشي
لا تلف كل استدعاء في نظامك بقاطع دائرة. ركز على النقاط الحرجة: استدعاءات الخدمات الخارجية (APIs لشركات أخرى)، أو الخدمات الداخلية المعروفة بأنها غير مستقرة، أو أي اتصال عبر الشبكة يمكن أن يفشل.
-
اضبط الإعدادات بحكمة (Tuning)
أهم إعدادين هما
fail_max(عدد مرات الفشل لفتح الدائرة) وreset_timeout(مدة بقاء الدائرة مفتوحة). هذه الأرقام ليست ثابتة وتعتمد على طبيعة الخدمة. خدمة حرجة جداً قد تحتاج لقاطع حساس (fail_maxقليل)، بينما خدمة أقل أهمية يمكن أن تتحمل فشلاً أكثر. ابدأ بقيم منطقية وراقب وحسّن. -
ماذا تفعل عندما تفتح الدائرة؟ (Fallback Logic)
هذه هي أهم نصيحة. عندما يرفض قاطع الدائرة الطلب، لا ترجع خطأً قبيحاً للمستخدم النهائي. جهّز “خطة بديلة” (Fallback). مثلاً:
- إذا فشلت خدمة “توصيات المنتجات”، اعرض قائمة بالمنتجات الأكثر مبيعاً (والتي يمكن أن تكون مخزنة مؤقتاً – Cached).
- إذا فشلت خدمة “الطقس”، اعرض آخر قراءة ناجحة مع رسالة توضح أنها ليست حية.
- إذا فشلت خدمة “الدفع”، اعرض رسالة لطيفة للمستخدم تطلب منه المحاولة لاحقاً، ولا تدعه يرى رسالة خطأ تقنية.
هذا المفهوم يسمى “التدهور التدريجي” (Graceful Degradation) وهو علامة على الأنظمة الناضجة.
-
المراقبة والإنذار (Monitoring and Alerting)
قاطع الدائرة ليس مجرد أداة حماية، بل هو أداة تشخيص ممتازة. يجب أن تراقب حالة قواطع الدائرة في نظامك. إذا كان قاطع معين يفتح ويغلق بكثرة، فهذه إشارة واضحة على أن الخدمة التي يحميها تعاني من مشاكل مزمنة ويجب التحقيق فيها. قم بإعداد تنبيهات (Alerts) تصلك فوراً عندما يفتح قاطع دائرة لخدمة حرجة.
الخلاصة: من الفوضى إلى المرونة 💡
في النهاية، نمط قاطع الدائرة هو أكثر من مجرد كود؛ هو تغيير في العقلية. هو الانتقال من عقلية “أتمنى ألا يفشل النظام” إلى عقلية “النظام سيفشل حتماً، وأنا مستعد لذلك”. هو الاعتراف بأن الفشل في الأنظمة الموزعة ليس استثناءً، بل هو جزء من اللعبة.
بتطبيق هذا النمط، نحن لا نمنع الفشل من الحدوث، بل نحتوي أثره ونمنعه من الانتشار. نحن نبني أنظمة قادرة على “شفاء نفسها” والتعافي من المشاكل تلقائياً، أنظمة مرنة (Resilient) تصمد في وجه العواصف بدلاً من أن تنهار عند أول هبة ريح.
فتذكر دائماً يا صديقي المبرمج: لا تبنِ بيتاً من زجاج، بل ابنِ قلعة قوية تعرف كيف تغلق أبوابها عند الخطر. 👍