Les fluides passent avant les cloisons
Sur Le Chantier, une fois le gros œuvre monté, arrivent les lots techniques : l'eau, l'électricité, la ventilation. Personne ne les verra dans le bâtiment fini, et ce sont eux qui décident si on y vit bien.
Ils décident aussi du calendrier. Une gaine de ventilation qui traverse un plancher demande une réservation dans la dalle, et cette réservation se prend avant le coulage. Percer après coup un plancher porteur coûte bien plus cher, et affaiblit l'ouvrage.
Puis vient le local technique. Une pièce, souvent au sous-sol, où arrivent les armoires et les compteurs, et où trône l'écran de la GTB, la gestion technique du bâtiment. Sur cet écran, un exploitant voit la température des étages, l'état des pompes, les alarmes de la nuit.
Deux sujets, un seul billet, parce que c'est la même faute qu'on commet sur les deux : les traiter à la fin.
La façon dont les services se parlent et la façon dont on les observe se décident avec l'architecture. Après la mise en production, c'est une reprise en sous-œuvre.
Ce billet ferme l'architecture système ouverte au billet précédent sur le couplage lâche, le stateless et le cache. On y a vu comment ne pas souder les services les uns aux autres. Reste à dire comment ils se parlent, et comment on sait ce qu'ils se sont dit quand la nuit tourne mal.
Vue d'ensemble : quatre modes, cinq critères
Le réflexe, aujourd'hui, tient en trois lettres : deux services doivent échanger, donc on expose une API REST. C'est souvent le bon choix. Ce n'est pas le seul, et surtout ce n'est pas un choix quand on ne l'a pas comparé aux autres.
Les familles, elles, n'ont pas bougé depuis plus de vingt ans. Gregor Hohpe et Bobby Woolf en recensaient quatre dans Enterprise Integration Patterns en 2003 : le transfert de fichiers, la base de données partagée, l'appel de procédure distant, la messagerie. Le découpage retenu ici est le leur, à deux ajustements près : fichiers et base partagée tiennent dans un même mode, et l'appel distant se lit dans les deux sens, l'appel REST et la notification poussée.
L'appel REST synchrone. Un service en appelle un autre en HTTP et attend sa réponse pour continuer.
La notification poussée en HTTP. On inverse le sens : c'est le producteur de l'événement qui appelle une adresse exposée par le consommateur, au moment où il se passe quelque chose.
La file de messages. Un intermédiaire s'intercale. Le producteur dépose, le courtier conserve, le consommateur retire quand il peut.
L'échange par fichiers ou par base partagée. Le plus ancien, le plus décrié, et toujours le seul praticable pour certains volumes.
Le choix entre les quatre se fait sur cinq critères. Ils se posent dans cet ordre, et aucun ne se déduit des autres.
| Critère | La question qu'il pose | Ce qu'on paie si on se trompe |
|---|---|---|
| Performance | Combien de temps entre l'émission et la prise en compte, et combien de messages par seconde ? | Un appelant qui attend, ou une file qui prend du retard sans que personne le voie |
| Taille des messages | Quelques centaines d'octets, ou un fichier de deux gigaoctets ? | Des délais d'attente dépassés, un courtier saturé, des messages rejetés |
| Modèle d'exécution | L'appelant a-t-il besoin de la réponse pour continuer son travail ? | Une chaîne synchrone où la panne du dernier maillon remonte jusqu'au premier |
| Fiabilité et retour | Que se passe-t-il si le destinataire est arrêté, et par où revient le résultat ? | Des messages perdus en silence, ou traités deux fois |
| Complexité | Combien de composants en plus à déployer, sécuriser et superviser ? | Un courtier de messages que personne dans l'équipe ne sait exploiter à 3 h du matin |
Le dernier critère est celui qu'on oublie, et c'est souvent celui qui tranche. Une file de messages qu'on n'a pas les moyens d'exploiter est moins fiable qu'une API REST bien faite.
expose une adresse"] end subgraph queue["3. File de messages"] A3["Service A"] --> Q(["Courtier"]) Q --> B3["Service B"] end subgraph files["4. Fichiers ou base partagée"] A4["Service A"] --> D[("Dépôt commun")] D -. "scrutation" .-> B4["Service B"] end classDef svc fill:#d89253,stroke:#8a4d16,color:#1a1a1a classDef infra fill:#b5651d,stroke:#8a4d16,color:#f5f2ec class A1,B1,A2,B2,A3,B3,A4,B4 svc class Q,D infra
Les quatre modes, un par un
L'appel REST synchrone
C'est le raccordement direct, en attente de réponse. Le service A appelle le service B, se bloque, reçoit un code de retour et un corps JSON, puis continue.
Sur les cinq critères, il s'en tire bien sur trois. La performance est bonne tant que le destinataire répond vite. Le retour est immédiat et typé : un code 200 ou une erreur 4xx ou 5xx, exploitable directement dans le code appelant. Et la complexité d'exploitation est nulle, puisque toute plateforme sait faire une requête HTTP.
Les deux autres critères sont ses angles morts. La taille des messages doit rester modeste, quelques dizaines ou centaines de kilo-octets, parce qu'un corps HTTP volumineux transforme chaque appel en transfert. Et la fiabilité dépend entièrement de la disponibilité du destinataire : s'il est arrêté, l'appel échoue, point.
Le vrai danger est plus insidieux. Trois appels synchrones en cascade additionnent leurs latences et leurs probabilités de panne. Si chaque service répond en 200 ms et tient 99,9 % du temps, la chaîne répond en 600 ms et tient 99,7 %. À dix maillons, la disponibilité perçue tombe à 99,0 %, alors qu'aucun service n'est en faute.
Le bon usage tient en une phrase : l'appelant a besoin de la réponse pour continuer. Consulter un solde, valider un code postal, autoriser un paiement.
La notification poussée en HTTP
Ici, on retourne l'appel. Le consommateur expose une adresse, le producteur l'appelle quand un événement se produit. C'est ce que font les plateformes de paiement, les forges Git et les outils d'intégration continue, sous le nom de webhook.
L'intérêt principal est d'éliminer la scrutation. Sans notification, un service qui veut savoir si un paiement a abouti interroge son fournisseur toutes les cinq secondes, indéfiniment, pour un événement qui survient une fois. Le coût est absurde et la latence reste mauvaise.
Sur les critères, le mode ressemble à REST avec ses défauts inversés. Le producteur n'attend plus rien d'utile, il pousse. Mais le consommateur, lui, doit être debout au moment exact où on l'appelle. Un consommateur en cours de redéploiement rate la notification.
Trois obligations en découlent, et elles sont non négociables. Le producteur doit réessayer avec un délai croissant. Le consommateur doit être idempotent, parce que ces réessais provoqueront des doublons. Et l'appel doit être authentifié, typiquement par une signature HMAC du corps, faute de quoi n'importe qui peut poster de faux événements sur une adresse publique.
La file de messages
Un intermédiaire s'intercale entre les deux services : un courtier, comme RabbitMQ, Apache Kafka, NATS ou une file gérée par un fournisseur cloud. Le producteur dépose un message et repart. Le message attend. Le consommateur le retire quand il est prêt.
Le gain n'est pas la performance brute, c'est le découplage temporel. Le consommateur peut être arrêté trois heures pour une migration : les messages s'accumulent, rien n'est perdu, tout est rattrapé au redémarrage. Ni l'appel REST ni la notification poussée n'offrent ça, puisque tous deux exigent un destinataire debout à l'instant de l'appel.
Le courtier apporte aussi de quoi survivre aux erreurs : persistance sur disque, accusé de réception, réessai automatique, et file de rebut (dead letter queue) pour ce qui échoue de façon répétée. C'est le mode le plus fiable des quatre, de loin.
Il coûte deux choses. D'abord la complexité : un composant supplémentaire à déployer, sécuriser, dimensionner, superviser et redonder, ce qui n'est pas un détail de fin de projet. Ensuite l'absence de retour direct : le producteur ne sait pas ce que son message est devenu. S'il a besoin d'une réponse, il faut une seconde file en sens inverse et un identifiant de corrélation pour raccrocher la réponse à la demande. Ce point se découvre souvent après coup, une fois le mode déjà retenu.
Dernier détail pratique : les courtiers plafonnent la taille des messages. Pour une pièce jointe volumineuse, on dépose le fichier dans un stockage objet et on ne fait circuler que sa référence.
L'échange par fichiers ou par base partagée
Un service écrit un fichier dans un dossier commun, un autre le scrute et le traite. Variante : un service insère une ligne dans une table, un autre la lit. C'est le mode qu'on présente comme dépassé, et qu'on retrouve pourtant au cœur des échanges bancaires, logistiques et de facturation, sous forme de lots nocturnes.
Il a une qualité qu'aucun autre n'a : la taille. Un export de quatre gigaoctets passe par un fichier, jamais par un corps HTTP ni par un message. Et il est le seul terrain d'entente possible avec un partenaire dont le système date de 1998.
Le reste est à son désavantage. La latence dépend de l'intervalle de scrutation, donc se compte en minutes. Il n'y a ni accusé de réception, ni réessai, ni ordre garanti : tout est à construire, conventions de nommage, fichier témoin de fin d'écriture, dossier de quarantaine pour les rejets.
Sa complexité est trompeuse. Elle est très basse le premier jour, et très haute en exploitation, parce que chaque garantie que le courtier offrait gratuitement devient du code maison que personne ne teste.
Un mode de communication ne se choisit pas sur ce qu'il coûte à écrire, mais sur ce qu'il coûte à exploiter pendant cinq ans.
Choisir un mode, et le piège du dossier partagé
Le tableau qui suit résume les quatre modes sur les cinq critères. Il ne remplace pas la réflexion, il évite de l'oublier.
| Mode | Performance | Taille des messages | Modèle d'exécution | Fiabilité et retour | Complexité |
|---|---|---|---|---|---|
| Appel REST | Bonne, latence directe | Faible à moyenne | Synchrone, bloquant | Retour immédiat, échoue si la cible est arrêtée | Minimale |
| Notification poussée | Bonne, pas de scrutation | Faible | Asynchrone côté producteur | Réessais et idempotence à la charge des deux parties | Moyenne |
| File de messages | Débit élevé, latence indirecte | Moyenne, plafonnée par le courtier | Asynchrone, découplé dans le temps | La meilleure : persistance, réessai, file de rebut. Pas de retour direct | Haute |
| Fichiers ou base partagée | Latence en minutes | Illimitée en pratique | Asynchrone, par lots | Tout est à construire | Basse à l'écriture, haute à l'exploitation |
Le même tableau, repris par l'usage : c'est sous cette forme que la question se pose en réunion.
| Mode | Utile pour | À écarter quand |
|---|---|---|
| Appel REST | Une réponse nécessaire pour continuer : solde, validation, autorisation | La chaîne d'appels dépasse trois maillons, ou la charge arrive par pics |
| Notification poussée | Prévenir un tiers d'un événement ponctuel : paiement abouti, build terminé | Le consommateur ne peut pas garantir sa disponibilité ni son idempotence |
| File de messages | Traitement différé, lissage de charge, diffusion à plusieurs consommateurs | L'équipe n'a ni astreinte ni supervision pour tenir un courtier |
| Fichiers ou base partagée | Gros volumes, lots nocturnes, interopérabilité avec un système ancien | Deux services actifs écrivent dans le même jeu de données au fil de l'eau |
Reste le piège classique, celui qui se déclenche le jour où on passe à deux instances.
Un service scrute un dossier et traite les fichiers qu'il y trouve. Ça marche parfaitement. On double le service pour absorber la charge, et les deux instances voient le même fichier au même moment. Les deux le traitent. La facture part deux fois.
La rustine du fichier verrou est une fausse solution : entre le test d'existence et la création, il reste une fenêtre où les deux instances passent. Ce qu'il faut, c'est une opération atomique. Sur un système de fichiers POSIX local, le déplacement d'un fichier vers un dossier de travail propre à l'instance en est une : celui qui perd la course reçoit une erreur, et il a sa réponse. Sur un partage réseau, la garantie est plus faible et dépend du protocole et des options de montage, ce qui se vérifie avant de s'y fier.
Le même problème sur une table partagée a une solution nette, prévue par les moteurs.
-- Chaque instance prend un lot distinct, sans attendre les autres
BEGIN;
SELECT id, charge_utile
FROM file_de_travail
WHERE statut = 'A_TRAITER'
ORDER BY id
LIMIT 10
FOR UPDATE SKIP LOCKED;
-- Le traitement, la mise à jour du statut et le COMMIT suivent dans la même transaction
La clause SKIP LOCKED fait passer une transaction par-dessus les lignes déjà verrouillées par une autre au lieu d'attendre. C'est le minimum vital pour transformer une table en file de travail multi-consommateurs.
Dès qu'il y a deux instances, un dossier ou une table partagés deviennent une ressource en concurrence. Sans opération atomique, ce n'est pas une intégration, c'est un tirage au sort.
Journalisation et supervision centralisées
Retour au local technique. Chaque lot du bâtiment a ses propres appareils de mesure : un compteur d'eau au sous-sol, des sondes de température par étage, un tableau électrique par cage. Tant que chacun reste dans son coin, diagnostiquer une fuite consiste à parcourir douze niveaux avec une lampe.
La GTB ne change rien aux capteurs. Elle les ramène tous sur un seul écran, avec un horodatage commun et des seuils d'alarme. C'est ce déplacement du point d'observation qui fait la différence, pas la qualité des sondes.
En informatique, la situation par défaut est celle d'avant la GTB. Chaque service écrit son journal sur sa machine. Huit services en trois exemplaires, cela fait vingt-quatre endroits où regarder, sur des machines dont certaines auront disparu au prochain redéploiement. Une nuit d'astreinte ne suffit pas à ouvrir vingt-quatre sessions.
La centralisation répond à deux besoins distincts, et il faut les nommer séparément parce qu'ils n'ont pas le même public. Tracer les erreurs sert au diagnostic, en urgence. Collecter la donnée d'usage sert au produit, à froid : quelles fonctions sont réellement utilisées, quels parcours sont abandonnés.
Deux implémentations existent pour ramener les journaux au même endroit.
| Critère | Le service appelle une API de journalisation | Un agent surveille les fichiers et la sortie standard |
|---|---|---|
| Couplage | Le service dépend du service de journalisation | Aucun, le service écrit et oublie |
| Panne du collecteur | Perte de lignes, ou pire, appelant ralenti | Les fichiers restent sur l'hôte, rattrapés au retour |
| Latence | Immédiate | Quelques secondes |
| Format | Structuré dès l'émission | À produire proprement, sinon il faut l'analyser après coup |
| Exploitation | Un service à redonder | Un agent à déployer sur chaque hôte |
La seconde forme s'est imposée, pour une raison simple : elle ne couple pas l'application à son observabilité. Le service écrit du JSON sur sa sortie standard, un agent embarqué sur l'hôte ramasse et expédie, un magasin indexe, une console interroge. C'est ce que recommande le facteur XI des douze facteurs : traiter les journaux comme un flux d'événements, et laisser l'environnement d'exécution s'occuper du routage.
génère l'identifiant"] s1["Service Commandes"] s2["Service Facturation"] s3["Service Notifications"] ag["Agent de collecte
sur chaque hôte"] st[("Magasin de journaux
indexé")] ui["Console de recherche
et alertes"] gw --> s1 s1 --> s2 s1 --> s3 s1 -. "flux structuré" .-> ag s2 -. "flux structuré" .-> ag s3 -. "flux structuré" .-> ag ag --> st st --> ui classDef io fill:#ede9e1,stroke:#b5651d,color:#1a1a1a classDef svc fill:#d89253,stroke:#8a4d16,color:#1a1a1a classDef infra fill:#b5651d,stroke:#8a4d16,color:#f5f2ec classDef store fill:#f5f2ec,stroke:#5f5e5a,color:#1a1a1a class gw,ui io class s1,s2,s3 svc class ag infra class st store
Une ligne de journal utile est une ligne qu'une machine sait filtrer. Le texte libre oblige à écrire des expressions régulières en pleine crise.
{
"timestamp": "2026-02-11T22:41:07.482Z",
"level": "ERROR",
"service": "facturation",
"version": "3.14.2",
"instance": "facturation-7d9c4f8b6-x2ktp",
"correlation_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"message": "Échec de création de la facture",
"commande_id": 4711,
"cause": "délai dépassé sur le service TVA"
}
Ce qui n'y figure pas compte autant : pas de mot de passe, pas de jeton, pas de numéro de carte, pas de donnée personnelle inutile au diagnostic. Un magasin de journaux centralisé est consultable par beaucoup de monde et conservé longtemps, ce qui en fait une base de données personnelles quand on n'y prend pas garde.
Le bon test de la supervision : combien de temps entre « un client se plaint » et « je vois la ligne de journal correspondante » ? Si la réponse se compte en dizaines de minutes, la GTB n'est pas branchée.
L'identifiant de corrélation
Une commande traverse la passerelle, le service commandes, le service stock, le service facturation, le service notifications. Cinq services, cinq séries de lignes de journal, cinq horloges qui ne sont jamais parfaitement d'accord.
Sans identifiant commun, la reconstitution se fait à l'horodatage. On cherche les lignes de facturation à plus ou moins deux secondes de l'appel de commandes, on tombe sur les quarante autres commandes traitées dans la même seconde, et on recommence. Ce travail se fait à 3 h du matin, sous pression, avec le support client en ligne.
L'identifiant de corrélation supprime ce travail. Il est généré une fois au point d'entrée, propagé dans chaque appel sortant, et écrit dans chaque ligne de journal. Une requête sur la console, et le parcours complet apparaît dans l'ordre.
Le standard existe et il vaut mieux l'utiliser que d'inventer un en-tête maison. La recommandation W3C Trace Context définit l'en-tête traceparent, qui porte l'identifiant de trace et celui du parent.
curl -s https://api.interne/commandes/4711 \
-H "traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
C'est aussi le format que propagent les bibliothèques OpenTelemetry, qui instrumentent la plupart des clients HTTP et des courtiers de messages sans qu'on ait à écrire la propagation à la main.
Le point de rupture est presque toujours au même endroit : la file de messages. Un identifiant se propage naturellement d'appel HTTP en appel HTTP, et se perd dès qu'un service dépose un message, parce que personne n'a pensé à le recopier dans les en-têtes du message. La trace s'arrête net au courtier, et la moitié du système redevient opaque.
L'identifiant de corrélation ne coûte presque rien à mettre en place le premier jour, et devient très pénible à généraliser sur un système en production. C'est exactement le profil d'une réservation dans la dalle.
Scénario B : Monzo, le 29 juillet 2019
La banque britannique Monzo a publié sur son blog un post-mortem public de l'incident du lundi 29 juillet 2019. C'est un texte rare : détaillé, chronologique, écrit sans arrondir les angles, et publié alors que rien n'y obligeait l'entreprise.
Le contexte, tel que la banque le décrit. Sa plateforme repose sur un très grand nombre de services adossés à un unique cluster Cassandra de vingt et un serveurs. Ce jour-là, une opération planifiée d'agrandissement était en cours : six serveurs de plus, pour absorber la croissance à venir. L'opération avait déjà été menée, la dernière fois en octobre 2018.
Ce qui a suivi n'a pas ressemblé à une panne franche. Une partie des requêtes de la plateforme s'est mise à échouer, et les clients concernés ne pouvaient plus se servir de l'application. Une panne partielle est le pire cas pour la supervision : sur des indicateurs agrégés, une fraction d'échecs se fond dans le bruit, et le système continue de paraître globalement sain pendant que des gens sont bloqués.
Le récit publié par Monzo se lit alors comme un exercice de corrélation. La piste de l'agrandissement est envisagée tôt, puis écartée parce que les indicateurs du cluster paraissent normaux, et d'autres hypothèses occupent l'équipe avant que le lien soit établi. La banque chiffre elle-même ce délai : près d'une heure entre la première alerte et le moment où les éléments se sont assemblés en une image cohérente désignant Cassandra.
Le retour en arrière n'a pas non plus été instantané. Les serveurs ajoutés se retirent un par un, à raison de huit à dix minutes chacun, parce qu'annuler une modification de topologie sur un cluster de données n'est pas une bascule mais une opération longue.
Les correctifs annoncés sont de trois ordres. Le réglage mal compris a été corrigé et les décisions de configuration consignées dans des procédures d'exploitation, l'essai ayant été mené sur un seul serveur là où la production en recevait six. De nouveaux indicateurs et alertes sont prévus, notamment sur les lectures qui ne trouvent pas leur donnée, l'angle mort exact de cet incident. Et le cluster unique doit être découpé en plusieurs clusters plus petits, pour qu'un seul changement ne puisse plus atteindre tout le système.
Une journalisation centralisée ne sert pas à rédiger le rapport après coup. Elle sert pendant, à répondre à une seule question : est-ce que ce que voit le client et ce que je viens de changer sont la même chose ?
Trois enseignements transposables tels quels. Une panne partielle ne se voit pas sur une moyenne, il faut découper par service, par version et par client. Tout changement d'infrastructure doit apparaître sur la même chronologie que les indicateurs applicatifs, sinon la corrélation reste manuelle. Et le retour en arrière se conçoit à l'avance, avec sa durée réelle, parce qu'on ne la mesure jamais au bon moment.
Points clés
- REST n'est pas le mode par défaut, c'est un mode parmi quatre. Notification poussée, file de messages et échange de fichiers résolvent des problèmes que l'appel synchrone ne sait pas traiter.
- Le choix se fait sur cinq critères : performance, taille des messages, modèle d'exécution, fiabilité et retour, complexité d'exploitation. Le dernier est celui qui tranche le plus souvent.
- Une file de messages découple dans le temps sans rien lâcher sur la remise, et c'est le seul mode qui ajoute un composant critique à exploiter. Elle ne rend pas de réponse directe, ce qui se conçoit avant de la choisir.
- Un dossier ou une table partagés deviennent une ressource en concurrence dès la deuxième instance. Déplacement atomique ou
SKIP LOCKED, jamais un fichier verrou naïf. - La journalisation centralisée et l'identifiant de corrélation se posent avec l'architecture. L'incident Monzo du 29 juillet 2019 montre ce que coûte une panne partielle qu'aucun tableau de bord ne désigne du doigt.
Dans la série
Palier 3 : Conduire le chantier. Domaine : Integrations.
- Précédent : Couplage lâche, stateless et cache : tenir la charge
- Suivant : Microservices, Event Sourcing et CQRS : quand et pourquoi
- Vue d'ensemble : Architecture logicielle : par où commencer
Pour aller plus loin
- Enterprise Integration Patterns, le catalogue de référence des styles d'intégration et des motifs de messagerie
- W3C Trace Context, la spécification de l'en-tête
traceparent - OpenTelemetry, pour l'instrumentation et la propagation du contexte de trace
- The Twelve-Factor App, facteur XI : les journaux comme flux d'événements
- Documentation d'Apache Kafka et de PostgreSQL sur
SELECTpour la clauseSKIP LOCKED - Post-mortem public de Monzo sur l'incident du 29 juillet 2019
- Couplage lâche, stateless et cache : tenir la charge, la première moitié de l'architecture système
- Microservices, Event Sourcing et CQRS : quand et pourquoi, pour ce que ces modes de communication permettent de construire une fois le système découpé