تطوير الويب

Resilience4j 3 مع Spring Boot 4: circuit breaker، retry، bulkhead

6 دقائق للقراءة

دروس السلسلة: Virtual threads Java 25 · Spring Boot 4 + GraalVM Native · JPA Hibernate المتقدم · Spring Cloud Gateway 4.3

كل استدعاء شبكي قد يفشل. في الإنتاج، التحدّي ليس منع كل الفشل — مستحيل — بل تجنّب انتشاره وإسقاطه للنظام كاملًا. نمط Circuit Breaker لـ Michael Nygard، مع retries وtimeouts وbulkheads وrate limiters، يُشكّل صندوق الأدوات القياسي. Resilience4j هي مكتبة Java المرجع لهذه الأنماط منذ تراجع Hystrix (Netflix، نهاية الدعم 2020). Resilience4j 3 (2025) تتطلّب Java 21 كحد أدنى. في 18 مايو 2026، الـ starter الرسمي resilience4j-spring-boot3 يعمل مع Spring Boot 4 (Spring Framework 7) لكن دعم SB4 الرسمي لا يزال مُناقَشًا من جانب المشروع. تكميليًا، Spring Boot 4 نفسه يكشف الآن @Retryable و@ConcurrencyLimit built-in عبر Spring Framework 7.

المتطلبات

  • Java 21+ (مثاليًا Java 25 LTS لـ virtual threads)
  • Spring Boot 4.0+ أو 3.5+
  • فهم استدعاءات HTTP بين microservices
  • اختياري: Prometheus + Grafana
  • الوقت المُقدَّر: 90 دقيقة

الخطوة 1 — إضافة Resilience4j إلى Spring Boot

<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-spring-boot3</artifactId>
    <version>3.0.0</version>
</dependency>
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-reactor</artifactId>
    <version>3.0.0</version>
</dependency>

resilience4j:
  circuitbreaker:
    configs:
      default:
        register-health-indicator: true
        sliding-window-type: COUNT_BASED
        sliding-window-size: 100
        minimum-number-of-calls: 20
        failure-rate-threshold: 50
        slow-call-rate-threshold: 100
        slow-call-duration-threshold: 2s
        wait-duration-in-open-state: 30s
        permitted-number-of-calls-in-half-open-state: 5
        automatic-transition-from-open-to-half-open-enabled: true
    instances:
      catalogueService:
        base-config: default
      paiementService:
        base-config: default
        failure-rate-threshold: 30   # plus strict pour le paiement

Spring Boot 4 يكشف تلقائيًا metrics عبر Actuator (/actuator/circuitbreakers) وhealth checks (/actuator/health). كل instance مُسماة وقابلة للضبط مستقلة، مع وراثة من default. الوضع COUNT_BASED أكثر قابلية للتنبؤ من TIME_BASED.

الخطوة 2 — تعليق الـ methods بـ @CircuitBreaker

@Service
@RequiredArgsConstructor
public class CatalogueClient {
    private final RestClient restClient;

    @CircuitBreaker(name = "catalogueService", fallbackMethod = "obtenirProduitFallback")
    public Produit obtenirProduit(String id) {
        return restClient.get()
            .uri("/produits/{id}", id)
            .retrieve()
            .body(Produit.class);
    }

    // Méthode fallback : même signature + Throwable en dernier param
    public Produit obtenirProduitFallback(String id, Throwable ex) {
        log.warn("CB ouvert pour {} : {}", id, ex.getMessage());
        return ProduitCache.parDefaut(id);
    }
}

ثلاثة انضباطات حرجة. الـ fallback يجب أن يكون سريعًا وحتميًا (لا استدعاء شبكي ثانٍ قد يفشل أيضًا). الـ fallback لا يجب أن يُخفي الأخطاء على جانب المستخدم: سجّل، زِد عدّاد Prometheus، أنذِر.

الخطوة 3 — فهم حالات Circuit Breaker

# États du Circuit Breaker
#
# CLOSED (normal) : appels passent, on compte succès/échecs sur la fenêtre glissante
#   ↓ failure-rate-threshold dépassé
# OPEN (rejet) : tous les appels échouent immédiatement (fail-fast)
#   ↓ wait-duration-in-open-state écoulé (30s par défaut)
# HALF_OPEN (test) : N appels-test sont autorisés (5 par défaut)
#   ↓ majorité réussit ou échoue
# CLOSED ou OPEN
#
# États spéciaux :
# - DISABLED : CB désactivé
# - FORCED_OPEN : forcé ouvert (incident manuel)
# - METRICS_ONLY : collecte sans bloquer

automatic-transition-from-open-to-half-open-enabled ينتقل تلقائيًا OPEN → HALF_OPEN دون استدعاء مستخدم — مفيد لاكتشاف الشفاء بسرعة.

الخطوة 4 — Retry بـ backoff تصاعدي

resilience4j:
  retry:
    configs:
      default:
        max-attempts: 3
        wait-duration: 500ms
        enable-exponential-backoff: true
        exponential-backoff-multiplier: 2
        enable-randomized-wait: true
        randomized-wait-factor: 0.5
        retry-exceptions:
          - org.springframework.web.client.ResourceAccessException
          - java.net.SocketTimeoutException
          - java.util.concurrent.TimeoutException
        ignore-exceptions:
          - com.app.NotFoundException
          - com.app.ValidationException
    instances:
      catalogueService:
        base-config: default

@CircuitBreaker(name = "catalogueService")
@Retry(name = "catalogueService", fallbackMethod = "obtenirProduitFallback")
public Produit obtenirProduit(String id) { /* ... */ }

الـ backoff التصاعدي 500ms × 2^attempt مع jitter ±50% يُعطي: المحاولة 2 عند ~500ms، المحاولة 3 عند ~1s. هذا الانضباط يتجنّب thundering herd.

الخطوة 5 — Timeout لكل استدعاء

resilience4j:
  timelimiter:
    configs:
      default:
        timeout-duration: 3s
        cancel-running-future: true
    instances:
      catalogueService:
        base-config: default

@Service
public class CatalogueAsyncClient {
    @TimeLimiter(name = "catalogueService")
    @CircuitBreaker(name = "catalogueService", fallbackMethod = "fallback")
    public CompletableFuture<Produit> obtenirProduit(String id) {
        return CompletableFuture.supplyAsync(() -> restClient.get()
            .uri("/produits/{id}", id)
            .retrieve()
            .body(Produit.class));
    }

    public CompletableFuture<Produit> fallback(String id, Throwable ex) {
        return CompletableFuture.completedFuture(ProduitCache.parDefaut(id));
    }
}

انضباط timeout: المدة يجب أن تكون أقل من timeout HTTP للعميل وأقل من timeout CB. تسلسل هرمي نموذجي: HTTP client 5s، CB slow threshold 2s، TL 3s.

الخطوة 6 — Bulkhead: عزل thread pools

resilience4j:
  bulkhead:
    configs:
      default:
        max-concurrent-calls: 25
        max-wait-duration: 100ms
    instances:
      catalogueService:
        base-config: default

  thread-pool-bulkhead:
    configs:
      default:
        max-thread-pool-size: 10
        core-thread-pool-size: 5
        queue-capacity: 20
        keep-alive-duration: 20ms
    instances:
      catalogueService:
        base-config: default

@Bulkhead(name = "catalogueService", type = Bulkhead.Type.SEMAPHORE)
@CircuitBreaker(name = "catalogueService", fallbackMethod = "fallback")
public Produit obtenirProduit(String id) { /* ... */ }

نوعان من bulkhead. SEMAPHORE (افتراضي): حد بسيط على عدّاد الاستدعاءات الجارية. THREADPOOL: pool مخصص للخدمة. لغالبية الحالات، semaphore يكفي.

الخطوة 7 — Rate Limiter لـ APIs خارجية

resilience4j:
  ratelimiter:
    configs:
      default:
        limit-for-period: 50    # 50 appels...
        limit-refresh-period: 1s  # ...par seconde
        timeout-duration: 200ms   # attente max si limit atteint
    instances:
      stripeApi:
        limit-for-period: 100
        limit-refresh-period: 1s
      twilioApi:
        limit-for-period: 50

@RateLimiter(name = "stripeApi")
@Retry(name = "stripeApi")
public PaiementResponse creerPaiement(PaiementRequest req) {
    return stripeClient.payments().create(req);
}

عند الوصول للحد، الاستدعاءات الإضافية تنتظر حتى 200ms. ما بعد ذلك، يُرفع RequestNotPermitted. لاحترام عقد Stripe وتجنّب 429s، هذا الانضباط في المنبع يتجنّب العقوبة التجارية.

الخطوة 8 — الملاحظة: metrics + alertes

# Métriques exposées
# - resilience4j.circuitbreaker.state{name="catalogueService",state="open|closed|half_open"}
# - resilience4j.circuitbreaker.calls{name=...,kind="successful|failed|not_permitted|ignored"}
# - resilience4j.retry.calls{name=...,kind="successful|failed_with_retry|failed_without_retry"}
# - resilience4j.bulkhead.available.concurrent.calls{name=...}
# - resilience4j.ratelimiter.available.permissions{name=...}

# Alertes Prometheus
- alert: CircuitBreakerOpen
  expr: resilience4j_circuitbreaker_state{state="open"} == 1
  for: 1m
  labels: { severity: critical }
  annotations:
    summary: "CB {{ \$labels.name }} ouvert depuis 1 minute"
    description: "Vérifier la santé du service en aval"

- alert: HighFailureRate
  expr: rate(resilience4j_circuitbreaker_calls{kind="failed"}[5m]) / rate(resilience4j_circuitbreaker_calls[5m]) > 0.1
  for: 5m
  labels: { severity: warning }

نمط التنبيه CircuitBreakerOpen هو الكناري: CB مفتوح > دقيقة واحدة يُشير لمشكلة كامنة. اقترنه بحدث Slack/PagerDuty لإيقاظ فريق الاستعداد ليلًا.

أخطاء شائعة

العَرَض السبب الحل
CB دائمًا مغلق رغم أخطاء ظاهرة minimum-number-of-calls عالٍ جدًا خفّض إلى 5-10 للخدمات قليلة الاستدعاء
CB يفتح على 4xx مشروعة recordExceptions عريضة جدًا تجاهل NotFoundException، ValidationException
Retry على POST غير idempotent إعداد retry افتراضي عطّل retry على POST/PUT دون idempotency-key
Fallback لا يُستدعى أبدًا Exception مرفوعة من fallback أيضًا fallback يجب أن يُرجع قيمة افتراضية، أبدًا throw
تعليقات Resilience4j متجاهَلة method خاصة أو self-invocation method عامة مُستدعاة من bean آخر
ذاكرة تنمو مع ratelimiter كثير من instances تُنشأ ديناميكيًا أعد استخدام instances عبر Registry

الأسئلة الشائعة

Resilience4j أم Failsafe؟
Resilience4j أكثر نضجًا في منظومة Spring (تكامل أصلي). Failsafe يبقى صالحًا خارج Spring، API أبسط.

كم CB لخدمة؟
واحد لكل تبعية خارجية (catalogue، paiement، notifications). ليس واحدًا لكل method. granularité على مستوى « خدمة مُستدعاة »، لا « endpoint فردي ».

أي عتبات failure-rate؟
30% للخدمات الحرجة (paiement، auth). 50% للخدمات المتسامحة (analytics). كيّف حسب profil الحركة وكلفة false positive.

متوافق WebFlux / Reactor؟
نعم عبر resilience4j-reactor. operators transformDeferred تُغلّف Mono/Flux. مثله لـ Kotlin coroutines مع resilience4j-kotlin.

اختبار Circuit Breaker؟
حقن mock يرفع منهجيًا، تحقّق من انتقال CLOSED → OPEN. CircuitBreakerRegistry يُتيح إنشاء CBs قابلة للرمي في الاختبار.

مقالات ذات صلة

Sponsoriser ce contenu

Cet emplacement est à vous

Position premium en fin d'article — c'est l'instant où les lecteurs sont le plus engagés. Réservez cet espace pour votre marque, votre formation ou votre offre.

Recevoir nos tarifs
Publicité