إعادة المحاولات والمهلات والحواجز العازلة
إعادة المحاولات والمهلات والحواجز العازلة
قاطع الدائرة ليس إلا سلاحًا واحدًا في ترسانة المرونة. عمليًا تحتاج إلى ثلاثة أنماط أخرى تعمل معًا: إعادة المحاولات للتعافي تلقائيًا من الأعطال العابرة، والمهلات لضمان أن بطء خدمة خارجية لن يُبقي الخيط معلّقًا إلى أجل غير مسمى، والحواجز العازلة (Bulkheads) لعزل أحواض الموارد حتى لا تتسبّب موجة طلبات في منطقة واحدة في تجويع المناطق الأخرى. يتناول هذا الدرس الأنماط الثلاثة بعمق — الآليات وخيارات الإعداد وأنماط الفشل والمقايضات في الأنظمة الموزّعة التي يجب على كل مهندس إنتاج أن يفهمها.
لماذا تحدث الأعطال العابرة
تُفقَد حزم الشبكة. تُعيد عمليات البحث في DNS إدخالات قديمة لثوانٍ قليلة بعد إعادة التشغيل المتدرّجة. يخضع الخادم الرئيسي لقاعدة البيانات لانتخاب قائد يستمر 300 مللي ثانية. هذه الأحداث عابرة: لو أعدتَ الطلب نفسه بعد لحظة قصيرة لنجح. بدون منطق إعادة المحاولة تظهر هذه الاضطرابات كأخطاء صلبة للمستدعين. ومع إعادة المحاولة المُعدَّة جيدًا تصبح غير مرئية.
503 Service Unavailable، استنفاد حوض الاتصالات). أما للأعطال الدائمة (400 Bad Request، رفض منطق الأعمال) فهي ضارة. احرص دائمًا على تكوين محدّد الاستثناءات حتى تُعيد المحاولة فقط للأخطاء القابلة للتعافي.
إعداد Retry في Resilience4j
أضف مشغّل Spring Boot ووحدة AOP إلى ملف pom.xml:
اضبط نسخة retry مسمّاة في application.yml:
هذا يمنحك ثلاث محاولات مع انتظار أولي 500 مللي ثانية ثم 1000 ثم 2000، وذلك فقط لاستثناءات I/O وFeign — ولا تُعاد محاولة أخطاء الأعمال أبدًا.
تطبيق تعليق Retry
randomized-wait-factor: 0.5 في إعداد Resilience4j لتوزيع المحاولات عشوائيًا ضمن 50% من مدة الانتظار.
المهلات — شبكة الأمان الإلزامية
لا تفيد إعادة المحاولة إلا إذا عاد الاستدعاء الأصلي في نهاية المطاف. بدون مهلة، يمكن لخدمة خارجية معلّقة واحدة أن تشغل خيطًا من حوض خادم الويب إلى الأبد. في خدمة تعالج 200 طلبًا متزامنًا بحوض من 50 خيطًا، مجرّد 50 استدعاء معلّقًا يستنفد الحوض وتُراكم كل الطلبات التالية — ثم تنتهي مهلتها لدى المستدعي — حتى لو كانت بقية منطق خدمتك سليمة تمامًا.
يوفّر Resilience4j مزيّن TimeLimiter، لكن بالنسبة لمعظم خدمات Spring Boot 3، الأسلوب الأبسط هو نسخة TimeLimiter في YAML مع التعليق @TimeLimiter:
يشترط @TimeLimiter أن تُعيد الدالة CompletableFuture. إذا لم تكتمل المهمة خلال ثانيتين يلغيها المزيّن ويرمي TimeoutException. اضبط cancel-running-future: true (وهو الافتراضي) حتى تتقاطع خيط التنفيذ الأساسي أيضًا — وإلا سيواصل العمل حتى بعد أن استسلم المستدعي.
@Retry و@TimeLimiter على نفس الدالة، تسري المهلة لكل محاولة. مع 3 محاولات بمهلة 2 ثانية لكل منها إضافةً للتراجع الأسي، قد يكون أقصى وقت إجمالي أطول بكثير من ثانيتين. خطّط لاتفاقيات مستوى الخدمة وأوصل أقصى زمن استجابة محتمل للخدمات المستدعية.
الحواجز العازلة — عزل نطاقات الفشل
مصطلح الحاجز العازل (Bulkhead) مستعار من هندسة السفن: تُقسَّم السفينة إلى حجرات مانعة للماء حتى لا يتسبّب غرق قسم واحد في إغراق السفينة كلها. في البرمجيات يحدّ الحاجز العازل عدد الاستدعاءات المتزامنة لخدمة خارجية معيّنة حتى لا تستهلك تبعيةٌ بطيئة أو فاشلة كلَّ الخيوط أو الاتصالات المتاحة.
يقدّم Resilience4j نوعين من الحواجز العازلة:
- حاجز إشارة المرور (Semaphore Bulkhead) — يحدّ عدد الاستدعاءات المتزامنة. خفيف الوزن، يعمل على نفس الخيط. مناسب للعمليات غير المعيقة أو السريعة.
- حاجز حوض الخيوط (Thread-pool Bulkhead) — ينقل الاستدعاءات إلى حوض خيوط مخصّص ومحدود. يوفّر عزلًا حقيقيًا للخيوط. أفضل لـ I/O المعيق حيث تريد منع استنفاد حوض الخيوط المشترك لخادم الويب.
حاجز إشارة المرور
حاجز حوض الخيوط
ينفّذ حاجز حوض الخيوط العملية على حوضه الخاص (4–8 خيوط) مع طابور سعته 50 مهمة. إذا امتلأ الطابور يُرفض الاستدعاء فورًا برمي BulkheadFullException. هذا يحمي خيوط خادم الويب من الانسداد بسبب تقارير بطيئة.
دمج الأنماط: الترتيب الصحيح للمزيّنات
عند تكديس تعليقات Resilience4j متعددة على دالة واحدة، يهمّ ترتيب التقييم. يُطبّق Resilience4j المزيّنات بهذا الترتيب (الأخارجي أولًا):
- Bulkhead — الحاجز العازل
- TimeLimiter — محدّد المهلة
- CircuitBreaker — قاطع الدائرة
- Retry — إعادة المحاولة
- RateLimiter — محدّد المعدّل
لذلك يحصل الاستدعاء أولًا على تصريح الحاجز، ثم يبدأ المؤقّت، ثم يتحقّق من الدائرة، ثم يُعيد المحاولة عند الفشل. هذا هو الترتيب الصحيح في الغالب: تريد أن تلتفّ المهلة حول كل محاولة فردية، وأن يجمع قاطع الدائرة النتائج عبر جميع المحاولات قبل قرار الفتح.
مراقبة الأنماط الثلاثة عبر Actuator
ينشر Resilience4j المقاييس إلى Micrometer تلقائيًا. مع وجود Spring Boot Actuator في مسار الفئات يمكنك الاستعلام عن الحالة الراهنة لأي نسخة:
تتغذّى هذه المقاييس مباشرةً في لوحات Prometheus + Grafana، ممّا يتيح لفريق موثوقية الموقع ضبط تنبيهات على معدّل إعادة المحاولة — وهو مؤشّر متقدّم على تدهور خدمة خارجية — قبل أن تصل الأخطاء إلى المستخدمين النهائيين.
الخلاصة
تتعافى إعادة المحاولة من الأعطال العابرة تلقائيًا، لكنها يجب أن تقتصر على العمليات المتساوية الأثر (idempotent) وأن تُعدَّ بتراجع أسي مع عشوائية لتجنّب قطيع الرعد. تضمن المهلات تحديد زمن الاستجابة لكل استدعاء وتمنع استنفاد حوض الخيوط. تُقسّم الحواجز العازلة ميزانية التزامن حتى لا تحتكر تبعية بطيئة واحدة كل الخيوط المتاحة. معًا تشكّل هذه الأنماط الثلاثة الطوق الثاني من دفاعك في المرونة، جالسةً تحت قاطع الدائرة للتعامل مع الأعطال التي تحدث قبل أن يفتح القاطع. في الدرس القادم ستضيف تحديد المعدّل لحماية خدمتك الخاصة من الاستنفاد بسبب المستدعين.