Async Bulkhead
L'isolation par sémaphore plutôt que par thread-pool — l'option qui rend le Bulkhead viable sur un hub à 500 partenaires.
Problème
Le Bulkhead classique alloue un pool de threads par partenaire. Sur un hub avec 500 partenaires actifs, à 20 threads par pool = 10 000 threads. À 1 Mo de stack par thread, on est à 10 Go RAM rien que pour les stacks. La majorité des threads dorment en attente d'I/O. Le context-switching kernel devient le bottleneck. Ce n'est plus viable.
Forces
- L'isolation par partenaire reste nécessaire. Une saturation Walmart ne doit pas affecter Carrefour.
- Les threads OS sont coûteux. Stack mémoire, scheduling kernel, context-switch.
- L'I/O est presque toujours bloquant historiquement. Mais Project Reactor, Vert.x, Netty, async Python ont changé la donne.
- Une coroutine ou un Future ne coûte rien. Quelques octets de stack, pas de scheduling kernel.
Solution
Remplacer le pool de threads par un sémaphore qui
plafonne le nombre de calls concurrents. Le code applicatif
appelle semaphore.acquire() avant l'appel partenaire
(typiquement non-bloquant, retourne un CompletableFuture/Mono), exécute l'appel
sur l'event-loop partagée, et libère le permit en fin. Si tous les
permits sont pris, la requête échoue rapidement (BulkheadFullException)
ou est mise en file d'attente bornée. L'isolation est logique, pas
physique. Resilience4j propose les deux modes ; en Reactive Streams,
le pattern est intégré nativement via la backpressure.
Variante Thread-pool Bulkhead : Variante Semaphore Bulkhead : partenaire Walmart partenaire Walmart ───────────────── ───────────────── ThreadPool(20) Semaphore(20) ↓ 20 threads dédiés ↓ 20 permits ↓ ~1 Mo stack par thread ↓ quelques octets de comptage ↓ blocage I/O OK ↓ I/O non-bloquant requis ↓ context-switch coûteux ↓ ordonnancement par event-loop ↓ overhead 20 Mo/partenaire ↓ overhead négligeable Pour 500 partenaires : - thread-pool → 10 Go RAM rien que les stacks - semaphore → quelques Mo total
Implémentation EDI
Cas concret : hub EDI Spring WebFlux avec Resilience4j en mode
semaphore. Configuration resilience4j.bulkhead.instances.walmart.maxConcurrentCalls=20
et idem par partenaire. Le client AS2 utilise WebClient + Reactor Netty (event-loop
partagée). Quand Walmart est lent, ses 20 permits sont utilisés,
nouvelles requêtes vers Walmart sont rejetées avec BulkheadFullException, redirigées sur queue de retry
avec backoff. Pour Carrefour, ses propres 20 permits restent
disponibles. Coût mémoire : 500 partenaires × 20 permits =
10 000 entiers atomiques, soit ~80 ko. Vs 10 Go avec le mode
thread-pool. Sur Kafka Streams, équivalent : partitions par
clé partenaire, parallélisme borné par max.poll.records. Sur Python asyncio :
asyncio.Semaphore(20) par partenaire.
Anti-patterns
- Semaphore bulkhead sur du code bloquant. Le sémaphore ne protège pas le thread bloquant : il faut un thread-pool. Choisir le bon variant selon le code.
- Pas de queue bornée. Si les requêtes en attente
du sémaphore s'accumulent sans limite, on revient à la saturation
mémoire. Définir
maxWaitDurationetmaxQueueSize. - Mélanger semaphore et thread-pool incorrectement. Mettre un semaphore-bulkhead devant un thread-pool-bulkhead sans coordonner les tailles : deux limites qui se chevauchent et qui produisent un comportement imprévisible.
- Une seule event-loop pour tous les partenaires. Un CPU-bound dans un thread event-loop fait tomber tous les partenaires. Réserver l'event-loop à de l'I/O strict.
Patterns liés
- Bulkhead — le pattern parent (variante thread-pool).
- Back-pressure — souvent combiné en mode réactif : la backpressure régule l'amont.
- Rate Limiter — le sémaphore plafonne la concurrence, le rate limiter plafonne le débit.
- Circuit Breaker — souvent composé : si un partenaire échoue, le circuit s'ouvre avant que le bulkhead ne sature.
Sources
- Resilience4j — Bulkhead. La documentation
détaille les deux variants :
SemaphoreBulkheadetThreadPoolBulkhead. resilience4j.readme.io/docs/bulkhead - Nygard M. — Release It!, Pragmatic Bookshelf, 2e éd. 2018. Le pattern parent.
- Reactive Streams Specification. Backpressure native qui rend explicite la borne de concurrence. reactive-streams.org
- Project Reactor — Reactor Netty bulkhead. Implémentation idiomatique en Reactor. projectreactor.io
- Microsoft Architecture Center — Bulkhead pattern (mentionne les deux variants). learn.microsoft.com — Bulkhead