Aller au contenu

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.

# RabbitMQ : prefetch de 10 messages
channel.basic_qos(prefetch_count=10)

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.