دروس السلسلة: 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 قابلة للرمي في الاختبار.