Synchrone et asynchrone¶
Choisir le bon mode de communication — request-reply, fire-and-forget ou pub-sub selon le besoin réel.
Le choix fondamental¶
Quand un composant A doit communiquer avec un composant B, la première question est : A a-t-il besoin de la réponse pour continuer ? Si oui, synchrone. Si non, asynchrone. Ce choix simple conditionne le couplage, la résilience et la scalabilité du système.
| Propriété | Synchrone | Asynchrone |
|---|---|---|
| Latence | Visible par l'utilisateur | Différée, hors du chemin critique |
| Couplage | Fort (disponibilité du récepteur) | Faible (le broker absorbe les pics) |
| Fiabilité | Échec immédiat si service down | Message préservé si service down |
| Complexité | Faible (request/response) | Élevée (idempotence, ordering, retry) |
| Debugging | Stack trace lineaire | Correlation IDs, tracing distribué |
Patterns de communication¶
Trois patterns fondamentaux couvrent la majorité des besoins.
Request-reply¶
L'appelant envoie une requête et attend la réponse. C'est le modèle REST et gRPC classique. Le flux est lineaire, facile à tracer et a debugger.
sequenceDiagram
participant C as Client
participant S as Service
C->>S: POST /orders
S-->>C: 201 Created {orderId: "ORD-1"} Quand l'utiliser : lectures, actions qui doivent confirmer un résultat immédiatement (authentification, création avec identifiant retourné), appels ou la latence de réponse est acceptable.
Risques : si le service est lent, l'appelant est bloque. Si le service est down, l'appel échoué. Chaque service dans la chaîne d'appels est un point de défaillance.
Fire-and-forget¶
L'appelant envoie un message et passe à la suite sans attendre de réponse. Le message est depose dans une queue et sera traite ulterieurement.
sequenceDiagram
participant C as Client
participant A as Service A
participant Q as Queue
participant B as Service B
C->>A: POST /orders
A-->>C: 202 Accepted {jobId: "JOB-1"}
A->>Q: publish OrderCreated
Q->>B: deliver OrderCreated
B-->>Q: ack Quand l'utiliser : notifications, traitements arriere-plan, propagation d'événements entre domaines, opérations coûteuses (envoi d'email, génération de PDF, calcul de recommandations).
Risques : le client ne sait pas si le traitement a réussi. Il faut un mécanisme de suivi (polling sur le job, webhook de callback, notification push).
Pub-sub (publish-subscribe)¶
Le producteur publie un événement sur un topic. Tous les consommateurs abonnes reçoivent une copie. Le producteur ne sait ni combien de consommateurs il y a, ni qui ils sont.
graph TD
P[Producer<br/>Order Service] --"publish"--> T[Topic<br/>order.events]
T --"deliver"--> C1[Consumer 1<br/>Notification]
T --"deliver"--> C2[Consumer 2<br/>Analytics]
T --"deliver"--> C3[Consumer 3<br/>Inventory] Quand l'utiliser : événements domaines (OrderCreated, UserRegistered), propagation de changements vers plusieurs consumers, intégration inter-équipes.
Risques : difficulté a tracer le flux complet, ordering non garanti entre consumers, explosion du nombre de souscriptions si non gouverne.
Comparaison des trois patterns¶
| Critère | Request-reply | Fire-and-forget | Pub-sub |
|---|---|---|---|
| Couplage | Fort | Moyen | Faible |
| Latence client | Bloquante | Minimale | Minimale |
| Fan-out | 1-to-1 | 1-to-1 | 1-to-N |
| Suivi résultat | Immédiat | Polling/callback | Pas de retour direct |
| Complexité | Faible | Moyenne | Élevée |
Message brokers¶
Le broker de messages est l'infrastructure qui découplé producteurs et consommateurs. Il persiste les messages, géré la distribution et garantit (ou non) l'ordering.
Kafka¶
Apache Kafka est un log distribué append-only. Les messages sont écrits dans des partitions ordonnées et retenus pendant une durée configurable (par défaut 7 jours, souvent indéfiniment).
graph LR
P1[Producer 1] --"write"--> T["Topic: orders<br/>Partition 0<br/>Partition 1<br/>Partition 2"]
P2[Producer 2] --"write"--> T
T --"read"--> CG1[Consumer Group A<br/>C1 ← P0, C2 ← P1, C3 ← P2]
T --"read"--> CG2[Consumer Group B<br/>C4 ← P0+P1+P2] Concepts clés :
- Topic : flux logique de messages (un par type d'événement en général)
- Partition : unite de parallélisme. Les messages d'une même partition sont ordonnes
- Consumer group : un groupe de consumers qui se repartissent les partitions. Chaque partition est assignee a un seul consumer du groupe
- Offset : position du consumer dans la partition. Le consumer contrôle sa progression
- Rétention : les messages restent disponibles après consommation, permettant le replay
Forces : throughput très élevé (millions de messages/seconde), ordering par partition, rétention longue, replay possible, écosystème riche (Kafka Connect, Kafka Streams, ksqlDB).
Limites : complexité opérationnelle (ZooKeeper ou KRaft), latence de livraison plus élevée que RabbitMQ pour les messages individuels, pas de routing complexe.
RabbitMQ¶
RabbitMQ est un broker de messages traditionnel (AMQP). Les messages sont routes vers des queues via des exchanges et des binding keys.
graph LR
P[Producer] --"publish"--> EX[Exchange<br/>type: topic]
EX --"order.created"--> Q1[Queue: notifications]
EX --"order.*"--> Q2[Queue: audit]
EX --"order.created"--> Q3[Queue: inventory]
Q1 --"consume"--> C1[Consumer 1]
Q2 --"consume"--> C2[Consumer 2]
Q3 --"consume"--> C3[Consumer 3] Types d'exchange :
| Type | Routing | Cas d'usage |
|---|---|---|
| Direct | Routing key exacte | Queue spécifique |
| Topic | Pattern matching (*.order.created, order.#) | Routing flexible |
| Fanout | Broadcast a toutes les queues liees | Notification a tous |
| Headers | Matching sur les headers du message | Routing complexe |
Forces : latence faible, routing flexible, protocole AMQP standardise, plugins (dead letter, delayed messages, fédération).
Limites : messages supprimes après consommation (pas de replay natif), throughput inférieur à Kafka pour les gros volumes, ordering garanti uniquement au sein d'une queue.
NATS¶
NATS est un broker léger et performant. Deux modes : NATS Core (at-most-once, pas de persistence) et JetStream (at-least-once, persistence, replay).
Forces : latence ultra-faible, simplicité opérationnelle, empreinte mémoire minimale, clustering natif. Bon pour la communication inter-services en temps réel (IoT, edge computing).
Limites : écosystème moins mature que Kafka et RabbitMQ, JetStream est plus récent et moins eprouve en production à grande échelle.
Quand choisir quoi¶
| Besoin | Broker recommande |
|---|---|
| Event sourcing, replay, audit | Kafka |
| Routing complexe, priorités | RabbitMQ |
| Faible latence, simplicité | NATS |
| Intégration avec écosystème data | Kafka (Connect, Streams) |
| File d'attente de tâches (worker queue) | RabbitMQ |
| Communication IoT / edge | NATS |
Ordering et exactly-once¶
Deux problèmes recurrents dans la messagerie asynchrone.
Ordering¶
L'ordre des messages n'est garanti que dans certaines conditions :
- Kafka : ordering garanti par partition. Si deux messages doivent être ordonnes, ils doivent aller dans la même partition (même partition key).
- RabbitMQ : ordering garanti par queue, mais perdu si le consumer fait un nack + requeue.
- NATS JetStream : ordering par stream.
En pratique, l'ordering global (sur l'ensemble du topic) est impossible a garantir sans sacrifier le parallélisme. On partitionne par entité (order_id, customer_id) pour garder l'ordering là où il compte.
Exactly-once delivery¶
Exactly-once delivery au sens strict est impossible dans un système distribué (preuve similaire au problème des deux généraux). Ce qu'on obtient en pratique :
- At-most-once : le message est delivre 0 ou 1 fois. Pas de retry. Risque de perte.
- At-least-once : le message est delivre 1 ou N fois. Retry automatique. Risque de duplication.
- Exactly-once semantics : at-least-once delivery + consumer idempotent. Le message peut arriver plusieurs fois mais l'effet est le même qu'une seule livraison.
Kafka Transactions (depuis 0.11) offrent exactly-once entre producteur et consumer dans le même cluster Kafka. En dehors de Kafka, c'est au consumer de garantir l'idempotence.
Backpressure¶
Quand un producteur emet plus vite que le consumer ne peut traiter, les messages s'accumulent. Sans mécanisme de backpressure, le broker sature et le système entier se dégradé.
Stratégies de backpressure¶
| Stratégie | Mécanisme | Trade-off |
|---|---|---|
| Buffer (queue) | Accumuler dans le broker jusqu'à une limite | Latence augmente, mémoire bornee |
| Drop | Supprimer les messages les plus anciens ou récents | Perte acceptable (métriques) |
| Throttle producer | Le broker ralentit le producteur (flow control) | Le producteur est impacte |
| Scale consumers | Ajouter des instances de consumer (autoscaling) | Coût infra, délai de scaling |
| Sampling | Traiter 1 message sur N | Perte controlee (analytics) |
Implémentation pratique¶
Kafka : le producteur reçoit une erreur quand le broker est sature (buffer full). Le consumer contrôle sa vitesse via le poll() — il ne reçoit des messages que quand il les demande. Le lag (différence entre le dernier offset écrit et le dernier offset lu) est la métrique clé a monitorer.
RabbitMQ : le prefetch_count limite le nombre de messages non acquittes qu'un consumer peut recevoir. Si le consumer ne les acquitte pas assez vite, le broker cesse d'en envoyer. Le mécanisme de flow control sur les connexions ralentit le producteur quand la mémoire ou le disque du broker atteint un seuil.
Monitorer le lag
Le lag des consumers est la métrique la plus importante en messagerie asynchrone. Un lag qui augmente en continu signifie que les consumers ne suivent pas le rythme de production. La réaction peut être : ajouter des consumers, augmenter le parallélisme (partitions Kafka), optimiser le traitement, ou accepter un délai de traitement plus long.
Patterns hybrides¶
En pratique, les systèmes melangent synchrone et asynchrone selon les flux.
CQRS light avec async¶
Les commandes (écritures) passent en synchrone pour confirmer la reception, puis propagent les changements en asynchrone vers les Read Models.
sequenceDiagram
participant C as Client
participant W as Write Service
participant Q as Event Bus
participant R as Read Service
C->>W: POST /orders (sync)
W-->>C: 201 Created
W->>Q: publish OrderCreated (async)
Q->>R: deliver OrderCreated
R->>R: update Read Model Request-reply sur broker¶
Certains cas necessitent une réponse asynchrone. Le client publie un message avec un reply_to et un correlation_id, puis attend la réponse sur une queue dédiée.
RabbitMQ et NATS supportent nativement ce pattern. Kafka le supporte via des conventions (topic de réponse, header de correlation).
Webhook callback¶
Le client fournit une URL de callback. Le service traite la requête en arriere-plan et appelle le callback quand c'est termine. C'est le pattern standard des APIs de paiement (Stripe, PayPal) et des CI/CD (GitHub webhooks).
sequenceDiagram
participant C as Client
participant S as Service
participant W as Webhook URL
C->>S: POST /exports {callback_url: "..."}
S-->>C: 202 Accepted {jobId: "JOB-1"}
Note over S: traitement en arriere-plan
S->>W: POST callback {jobId: "JOB-1", status: "done", url: "..."} Règle pratique
Si l'utilisateur attend le résultat pour afficher quelque chose à l'écran : synchrone. Si le traitement peut durer plus de quelques secondes : asynchrone avec notification (polling, webhook, ou push). Ne forcez pas l'utilisateur a attendre — retournez un 202 et prevenez-le quand c'est pret.
Chapitre suivant : Contrats et gateway — définir les interfaces, gouverner les APIs et gérer le trafic.