Plusieurs systèmes qui doivent fonctionner ensemble, y compris lorsqu’un d’eux tombe.
Nous concevons et exploitons des plateformes composées de plusieurs services, avec bus de messages, tentatives plafonnées, clés d’idempotence, disjoncteurs et publication avec retour automatique. Chaque modèle ci-dessous s’exécute dans un système propre, pas dans un diagramme.
Déjà construitOperăm patru platforme proprii cu arhitecturi diferite și le putem deschide pe toate. Cifrele sunt măsurate azi cu comenzi, nu preluate din documentație: aichat are 28 de directoare de serviciu, 235 de modele de date pe patru scheme și o magistrală de mesaje cu 297 de cozi și 17.525 de legături; Kallina are 472 de funcții edge, 559 de migrări și 28 de sarcini programate în bază; MEGA CRM are un backend de 9.516 linii cu 99 de rute și 179 de politici de acces pe rând; Taskin rulează 21 de sarcini programate fără niciun Docker. Rezerva pe care o spunem noi: în două locuri documentația proprie a rămas în urma codului — o afirmație despre numărul de linii era depășită cu 16%, iar o diagramă de infrastructură descria un server pe care nu mai rulăm nimic. Am folosit măsurătoarea, nu documentul.
« Architecture complexe » ne signifie pas beaucoup de boîtes sur un dessin. Cela signifie que vous savez ce qui se passe lorsqu’une d’elles ne répond pas. Un système avec trois services qui s’appellent de manière synchrone, sans plafond de réessais et sans idempotence, est plus fragile qu’un monolithe — car chaque nouveau lien est un nouveau mode de défaillance, et les modes de défaillance se multiplient plus vite que les fonctionnalités.
Notre travail commence par la séparation des responsabilités et se termine à ce que l’on voit quand quelque chose cède. Sur l’une des plateformes propriétaires, les messages ne transitent pas directement entre services, mais par un bus avec 297 files, 9 échanges et 17.525 liens, dont 278 files sont générées par client, et non écrites à la main. Huit files ont un échange de lettres mortes et huit ont un temps de vie du message de cinq minutes — c’est-à-dire qu’un message qui ne peut pas être traité arrive quelque part où il peut être vu, il ne disparaît pas.
Les modèles que nous mettons en place sont peu nombreux et se répètent : des réessais avec un plafond écrit dans le code, des clés d’idempotence qui rendent inoffensive une deuxième livraison du même événement, un interrupteur qui coupe les appels vers un service qui tombe de façon répétée, une gate de consommation avant les actions qui coûtent de l’argent, et des alertes avec un seuil de répétition pour qu’une panne intermittente n’enterre pas le reste.
La dernière partie, celle qu’on oublie : la publication. L’une des plateformes est publiée par synchronisation de fichiers, sans Docker, avec vérification que la page servie contient exactement l’identifiant du paquet tout juste publié — pas seulement que le serveur répond 200 — et avec retour automatique à la version précédente si la vérification échoue. La règle est apparue après un incident réel dans lequel une vérification superficielle a annulé une bonne publication.
Ce que cela comprend
Le travail, par composantes
Nous séparons les services selon ce qui peut se casser indépendamment
Dans une plateforme propriétaire, il y a 28 répertoires de service, dont 16 ont leur propre point d’entrée, et en production cinq processus tournent sous un gestionnaire de processus. Chaque canal de communication a son propre service et son propre port, justement pour qu’une panne sur l’un n’arrête pas le reste. Les données sont modélisées sur quatre schémas séparés, avec 235 modèles au total — la séparation n’est pas seulement au niveau du processus, elle l’est aussi au niveau du schéma.
Nous mettons un bus de messages, pas une chaîne d’appels synchrones
297 files, 9 échanges, 17.525 liens. Le nom des files est par client et, dans certains cas, par thread — c’est-à-dire que la topologie est générée, et non écrite à la main. Il existe aussi un échange avec livraison différée, pour les choses qui doivent se produire plus tard, pas maintenant. Huit files ont un échange de lettres mortes configuré et huit ont un temps de vie du message de cinq minutes.
Réessais avec plafond, pas à l’infini
Trois plafonds différents pour trois situations différentes, tous écrits dans le code : cinq réessais pour les messages planifiés, avec le compteur persisté, et non gardé en mémoire ; trois réessais au niveau de la file, comptés dans un en-tête du message ; et trois réessais avec augmentation exponentielle de la pause pour l’envoi d’e-mail, sur deux voies de fournisseur. Un réessai sans plafond n’est pas de la résilience, c’est une boucle.
Clés d’idempotence, pour que la deuxième livraison ne casse rien
Pour les événements de tiers, nous utilisons une insertion qui échoue si l’événement a déjà été vu : la violation d’unicité de la base de données est traitée comme « duplicate », et non comme une erreur, en plus d’une vérification de fraîcheur du moment. Pour les paiements, la clé est un marqueur sur la transaction de crédit, donc le renvoi du même webhook ne crédite pas deux fois. Pour les messages, la déduplication se fait sur l’identifiant, avec durée de vie et nettoyage.
Un interrupteur de circuit sur l’intégration fragile
L’intégration qui dépend de la session d’un site tiers a un interrupteur réel, pas seulement une note dans la documentation : après dix échecs consécutifs, les appels s’arrêtent pendant deux minutes. Sans lui, un site qui ne répond pas transforme un canal tombé en plateforme lente, parce que tout le monde attend dans la requête.
Gate de consommation avant les actions qui coûtent
Dans l’une des plateformes, toute action payante passe par une fonction de vérification en quatre étapes, chacune avec son propre motif renvoyé en clair : compte suspendu, limite mensuelle de conversations atteinte, plafond quotidien de dépense dépassé, solde insuffisant. En cas d’exception, elle renvoie « non autorisé », pas « autorisé » — le gate se ferme, il ne s’ouvre pas, quand quelque chose ne fonctionne pas. Derrière se trouve un registre des événements de consommation avec coût par événement, ventilé par fournisseur, modèle et modalité.
Limitation de débit qui survit au redémarrage
Fenêtre glissante conservée en base de données, avec une carte en mémoire uniquement comme raccourci à l’intérieur d’une invocation. La raison est écrite dès l’en-tête du fichier : les processus qui servent les requêtes sont de courte durée, donc leur mémoire ne peut pas être la source de vérité. Quinze fonctions l’utilisent, et le quota d’un fournisseur externe a son propre limiteur, séparé.
Observabilité qui distingue « a répondu » de « a fonctionné »
Une réponse 200 ne signifie pas que la tâche a réussi. L’enveloppe avec laquelle nous exécutons les tâches programmées parcourt récursivement la réponse à la recherche de listes d’échecs ou d’erreurs et traite « 200 avec des échecs » comme un résultat distinct. Les alertes ont un seuil de répétition — dix minutes dans un système, deux heures dans un autre — et le marquage est supprimé au premier succès, afin qu’une défaillance revenue puisse alerter à nouveau immédiatement.
Publication avec vérification et retour automatique
La publication vérifie non seulement que l’adresse répond 200, mais aussi que la page servie contient exactement l’identifiant du paquet tout juste publié ; sinon, elle restaure la version précédente. Pour le service d’arrière-plan, la publication compare l’arborescence par sommes de contrôle et saute le redémarrage si rien n’a changé — un redémarrage inutile tue une boucle en cours de travail.
À quoi cela ressemble
Le parcours, étape par étape.
01
Nous dessinons la carte des modes d’échec, pas celle des boîtes
Pour chaque liaison entre deux systèmes : que se passe-t-il si l’autre est lent, s’il est en panne, s’il répond deux fois, s’il répond mal. Nous livrons : la liste des liaisons avec le comportement attendu dans chacun des quatre cas, ainsi que la décision synchrone/asynchrone pour chacune.
02
Nous mettons en place le bus et les contrats de message
Files, échanges, messages morts, durée de vie par message, et la clé d’idempotence pour chaque type d’événement. Nous livrons : la topologie, les contrats de message et le comportement documenté lors de la retransmission.
03
Nous ajoutons les portes : débit, consommation, interrupteur
La limitation de débit sur fenêtre glissante dans la base, la porte de consommation avant les actions qui coûtent et l’interrupteur sur les intégrations hors de notre contrôle. Nous livrons : les seuils configurés, les motifs renvoyés en clair et les tests pour chaque étape de refus.
04
Nous construisons la publication et le retour en arrière
Publication avec vérification du contenu, pas seulement du code de réponse, version précédente conservée à côté et retour en arrière automatique. Nous livrons : la procédure de publication, la procédure de retour en arrière exercée au moins une fois, et les sondes de santé — une superficielle pour « vit », une profonde pour « fonctionne vraiment ».
05
Nous remettons avec la documentation vérifiée par rapport au code
Le document est vérifié par rapport à la mesure avant la remise, parce qu’une documentation qui reste en arrière est plus dangereuse que son absence. Nous livrons : le diagramme, les procédures et la liste explicite des endroits où le document et le code ont été trouvés en désaccord, avec ce que nous avons corrigé.
Sistemele nu se apelează în lanț: între ele stă o magistrală cu cozi per client, schimburi separate pe tip de trafic și scrisori moarte pentru ce nu se poate procesa. În jurul ei, porțile — limitare de rată, poartă de consum, întrerupător de circuit — și sondele de sănătate, una superficială și una adâncă.
Les données
Ce que nous touchons, où elles sont et combien de temps elles restent
Les questions que pose toute personne ayant un délégué à la protection des données — posées ici avant qu'il ne les pose.
Où se trouvent les données, par plateforme
Il n’existe pas de réponse unique, et c’est bien ainsi : une plateforme utilise MySQL sur son propre serveur, deux utilisent PostgreSQL via Supabase — l’une hébergée, l’autre installée sur notre infrastructure — et une conserve le contenu sous forme de fichiers statiques, sans base de données du tout. Le choix se fait selon les exigences, pas par habitude.
L’accès s’applique à la base, pas seulement à l’application
Dans un de nos systèmes internes, la sécurité au niveau des lignes est activée sur 43 tables, au moyen de 90 instructions, avec 179 politiques écrites. La règle que nous suivons : si un appel contourne l’application et interroge directement la base, il ne doit pas voir les lignes d’un autre.
Le registre de consommation
Une ligne par événement avec le type, le fournisseur, les quantités séparées par modalité — secondes d’appel, jetons d’entrée, jetons de sortie, caractères — plus le coût en devise avec six décimales et les crédits débités. On en tire le plafond quotidien de dépense, le coût par appel et la projection de consommation.
Les migrations sont l’historique, pas la documentation
559 migrations dans une plateforme, 80 dans une autre. Le schéma change par migrations versionnées, donc l’état de la base peut être reconstruit et lu chronologiquement. Quand le document et la migration ne sont pas d’accord, la migration a raison.
Les tâches planifiées restent dans la base ou dans cron, pas dans le processus
28 tâches planifiées s’exécutent à l’intérieur de la base de données dans une plateforme ; dans une autre, 21 tâches s’exécutent via le cron système. La raison est écrite dans le fichier : les minuteurs à l’intérieur d’un service se réinitialisent à chaque redémarrage, donc une boucle de trois heures dans un service qui est redéployé ne se déclenche jamais.
Un cas
Une topologie de messages qui se génère toute seule pour chaque client
La situation
Une plateforme avec plusieurs clients, chacun avec ses propres canaux de communication, ses propres fils de discussion et ses propres notifications. La version naïve — une file commune et un filtre sur l’identifiant du client — fait qu’un client à fort volume bloque les autres, et qu’une erreur sur un fil arrête la file pour tous.
Ce que nous avons construit
La topologie se génère par client, elle n’est pas écrite à la main : sur 297 files, 278 sont du type « notifications pour un compte donné », et certaines descendent jusqu’au niveau d’un seul fil de discussion. Au-dessus, neuf échanges séparent les types de trafic — notifications, jetons, webhooks, canaux — et il existe un échange à livraison différée pour ce qui doit se produire plus tard. Huit files ont un échange de lettres mortes, huit ont un temps de vie par message de cinq minutes. Les règles d’orchestration peuvent être rechargées à chaud, via un canal de publication, sans redémarrage des consommateurs.
Ce qui en est sorti
Un client à fort volume ne retarde pas les autres clients, et un message qui ne peut pas être traité arrive dans un endroit où il peut être vu et repris, au lieu de disparaître ou de bloquer la file. Le nombre de liaisons dans le bus — 17.525 — montre exactement pourquoi la topologie doit être générée : personne ne maintient manuellement quelque chose comme ça.
Ce que le cas ne dit pas
Le prix est opérationnel : un bus de cette taille a besoin de sa propre surveillance et d’un plan pour les files abandonnées, sinon il grossit sans fin. Et la génération par client suppose que la suppression d’un client supprime aussi sa topologie — si cette étape manque, des files mortes s’accumulent.
Questions
Ce que les gens nous demandent avant d'appeler
Comment savoir que vous ne me vendez pas une complexité dont je n’ai pas besoin ?
Parce que la première recommandation que nous donnons souvent est de ne pas séparer. Chaque nouveau service est un nouveau mode de défaillance, et les modes de défaillance se multiplient plus vite que les fonctionnalités. Un exemple tiré de nos propres systèmes : le backend de l’un d’eux est un seul fichier de 9.516 lignes avec 99 routes. Ce n’est pas élégant et nous le disons, mais il est publié en une étape et dépanné en un seul endroit. La séparation se fait lorsqu’il existe une raison mesurable — un service qui doit être mis à l’échelle séparément, une équipe séparée, un rythme de livraison séparé.
Que se passe-t-il lorsqu’un système de la chaîne ne répond pas ?
Cela dépend de ce nous avons décidé ensemble à l’étape de cartographie, et c’est bien l’idée. Dans nos systèmes : le message entre dans la file d’attente et est retenté un nombre plafonné de fois, puis arrive dans l’échange de lettres mortes, où il peut être vu ; l’intégration instable a un disjoncteur qui, après dix échecs consécutifs, s’arrête deux minutes au lieu de laisser les requêtes en attente ; et les actions qui coûtent de l’argent sont arrêtées par une gate qui, en cas d’exception, refuse, n’autorise pas.
Comment évitez-vous qu’un même événement soit traité deux fois ?
Avec une clé d’idempotence, pas avec une vérification « je l’ai déjà vu ? » qui comporte une course de données. Concrètement : nous insérons une ligne avec l’identifiant de l’événement et, si la base refuse pour violation d’unicité, nous traitons ce code d’erreur comme un signal de doublon, pas comme une défaillance. Pour les paiements, le marquage se trouve sur la transaction de crédit, donc le renvoi du même webhook ne crédite pas deux fois.
Utilisez-vous Docker ou non ?
Les deux, et nous justifions le choix à chaque fois. Un système tourne dans un container, mais sans volumes montés, ce qui signifie que la publication est une copie dans le container plus un redémarrage — un `docker rm` là-bas fait perdre l’état, et c’est écrit exactement ainsi dans le document d’exploitation. Une autre plateforme n’a aucun Docker : la publication est une synchronisation de fichiers, et les scripts de publication sont installés manuellement avec des droits d’administrateur et exposés au flux automatisé par exactement deux voies autorisées. La raison est écrite dans le code : un script que le flux peut réécrire est un script par lequel le flux peut escalader.
Comment savez-vous qu’une tâche programmée a vraiment tourné ?
Par mauvaise expérience. Un de nos rapports quotidiens est resté mort onze jours, entre le 7 et le 17 août, alors que la tâche programmée se déclenchait chaque jour — la commande utilisée sortait silencieusement en cas d’erreur et n’écrivait rien. Depuis, chaque tâche s’exécute dans une enveloppe qui parcourt la réponse selon des listes d’échec, traite « 200 avec des échecs » comme un résultat distinct, a un temps maximal d’exécution et écrit une ligne structurée par exécution. Les alertes ont un seuil de répétition, et le marquage est supprimé au premier succès.
Dans quelle mesure l’abstraction par fournisseur compte-t-elle ?
Beaucoup, si le fournisseur se trouve dans un domaine qui évolue rapidement. Pour la voix, nous avons trois ponts séparés — un pour chaque fournisseur de speech en temps réel — qui implémentent la même interface. Le pont effectue la conversion audio entre le central téléphonique et le fournisseur, sur un port dédié, avec le serveur de santé relié uniquement à l’interface locale, non exposée. Le changement de fournisseur devient une décision, pas une réécriture.
Votre documentation est-elle à jour ?
Pas partout, et nous préférons le dire là où ce n’est pas le cas. Lors de la préparation de cette page, nous avons mesuré deux systèmes et avons constaté que notre propre document était en retard : une affirmation sur la taille d’un fichier était inférieure d’environ 16% à la réalité, et un schéma d’infrastructure décrivait un serveur dont nous avions déjà déménagé. La règle que nous appliquons et que nous demandons dans les projets : lorsque le document et la mesure ne sont pas d’accord, la mesure a raison, et le document est corrigé dans le même mouvement.
Que ne faites-vous pas ?
Nous ne promettons pas d’objectifs de disponibilité sans mesure — un pourcentage du type « 99,9% » exige des données de terrain sur une période, et lorsque nous ne les avons pas, nous ne l’affirmons pas. Nous ne concevons pas de systèmes que nous ne pouvons pas exploiter ou transmettre : si le résultat est une architecture que votre équipe ne peut pas maintenir, c’est une mauvaise architecture. Et nous ne faisons pas de certifications de conformité — nous pouvons construire les contrôles, nous ne pouvons pas délivrer le certificat.
Sur quoi reposent les affirmations ci-dessus (23 sources)
23 d'entre elles sont du code et des fichiers de nos dépôts. Nous n'en publions ni le nom ni la ligne : réunies sur une seule page, elles décriraient trop précisément comment sont construits des systèmes qui ne sont pas seulement les nôtres. Nous les parcourons avec vous, dans le dépôt, sur demande — la vérification reste possible, elle se fait simplement lors d'un échange.
Qu'est-ce que vous voudriez voir mieux fonctionner ?
Racontez-nous votre processus. Ensemble, nous décidons ce qui vaut la peine d'être construit, ce que nous pouvons connecter et comment nous vérifions le résultat.