Transcript de ma conférence « Bienvenue dans le Monde Merveilleux des Systèmes Distribués »
16 septembre 2025 —En 2022-2023, j’ai présenté plusieurs fois ma conférence « Bienvenue dans le Monde Merveilleux des Systèmes Distribués », basée sur un peu de théorie et sur plusieurs années de pratique.
Mon intervention a été enregistrée lors de mon passage à MixIT 2023 :
Comme tout le monde n’accroche pas avec le format vidéo et que beaucoup préfèrent du texte, voici une tentative de transcript de cette conférence.
Vous trouverez en-dessous de chaque slide le texte qui lui correspond, parfois enrichi d’informations que je n’ai pas données lors de la présentation ou qui ne sont pas visibles dans les slides – parce qu’elles sont passées par ma gestuelles ou par des animations / transitions invisibles avec ces images figées. Le style est volontairement assez proche de l’oral.

Bonjour à toutes, bonjour à tous. Bienvenue ! Aujourd’hui, j’ai le plaisir d’être ici avec vous pour parler d’un sujet qui nous concerne toutes, qui nous concerne tous. Les systèmes distribués.

Je m’appelle Pascal MARTIN, je suis Principal Engineer et AWS Hero, je bosse beaucoup sur de l’infra, du back, Kubernetes, AWS, des sujets de performance, de scalabilité, de résilience et de couts.
Je travaille chez Bedrock, nous développons une plateforme de VOD et de replay que nous vendons en marque blanche à nos clients, des broadcasteurs européens. En France, notre plateforme propulse notamment 6play/M6+.

Bizarrement, ce n’est qu’assez récemment que j’ai réalisé que je bossais avec des systèmes distribués.

Mon premier projet, c’était du PHP. J’étais au lycée, ça ressemblait à ça : un serveur, PHP, MySQL, tout sur la même machine.
Ce n’était pas vraiment un système distribué.

Mais assez rapidement, des problématiques de charge, et je me suis retrouvé à mettre PHP sur une machine et MySQL sur une autre machine, avec une communication entre les deux.
J’avais deux serveurs, j’avais une communication via le réseau, j’avais mon premier système distribué 🎉.

Je suis arrivé en entreprise. Et dans le monde pro, rapidement, je me suis retrouvé sur un projet où un seul serveur ne suffisait pas.
Peut-être qu’on ne codait pas très bien, peut-être que PHP était moins performant maintenant, mais on a ajouté plein de serveurs, plein de machines.
Et puis devant on a mis un load balancer.

Du temps est passé et on s’est rendu compte que notre application n’était pas très rapide à nouveau. Peut-être qu’on codait mal en SQL aussi ?
Mais ce qu’on a fait, c’est sortir un serveur pour les écritures, et puis brancher d’autres serveurs pour les lectures. Plutôt qu’optimiser les requêtes, on a juste payé plus de serveurs avec un mécanisme de réplication entre tout ça.
Et là on a découvert des problématiques qu’on n’avait jamais vues. Genre on écrit sur un serveur, on fait une lecture sur un autre serveur et la donnée n’est pas encore là. Ça fait des surprises dans les back-office.

Et puis au bout d’un moment on a réalisé que nos serveurs passaient beaucoup de temps, beaucoup de ressources, à servir des JS, des CSS, des images.
Et on s’est dit qu’est-ce qu’on va faire ? On va sortir ça sur un autre serveur, un serveur d’assets, qui fera du service statique.

Je garde l’idée dans un coin, imaginez qu’elle est en dehors de l’écran. Et on revient un petit peu plus haut, sur ce schéma.
On a réalisé qu’on générait encore et encore et encore les mêmes pages HTML. C’est un peu dommage de bouffer plein de ressources à générer les mêmes pages HTML.
Donc on a mis un reverse proxy, une machine là pour faire du cache. On génère les pages, elles sont stockées en cache par le proxy, qui sert aux utilisateurs suivants.

Et puis quelque chose dont je n’ai pas parlé encore, bien sûr : on en a mis un deuxième.
Et c’est quelque chose qu’on avait déjà fait pour tous les autres serveurs précédemment. Si un serveur s’écroule, on va en avoir un autre pour prendre le relai.

Puis petit à petit on garde le load balancer, il y a au moins un truc qui reste, et là on arrive à un schéma avec un reverse proxy cache qui parle à une application qui a sa base de données, qui a son Redis, qui parle aussi à une autre application et même à une seconde autre application.
On a une seconde application avec son Elastic Search, une troisième application avec son MongoDB, une quatrième avec une application, avec sa base de données, un mécanisme de synchronisation entre les deux, application 7, application 8… Enfin bon bref, plein d’autres applications, vous l’aurez compris.
Et on se rend compte qu’on a commencé à distribuer de deux façons :
- On distribue l’hébergement, les services, les serveurs.
- Et on distribue aussi les applications elles-mêmes. On commence à les découper en plusieurs services, peut-être en microservices, en API.
Et si vous cherchez application numéro 5, on l’a encore oubliée 🤷♂️.

Et le titre de ce talk, ça aurait peut-être pu être celui-là, mais c’était moins vendeur… Et puis, c’est quand même aussi très fun les systèmes distribués… Donc, j’ai gardé le premier.

Aujourd’hui chez Bedrock, on travaille avec un cluster Kubernetes. Avec plusieurs clusters Kubernetes, même. On déploie nos applications sous forme de pods, avec de l’autoscaling de folie dans tous les sens, c’est plutôt cool.
On se repose beaucoup sur les services managés de notre hébergeur, pour tout ce qui est stockage et bases de données notamment, et on continue bien sûr d’appeler des services externes.
Donc on a déployé un service distribué, notre application, dans un autre service distribué, Kubernetes. On utilise aussi les systèmes distribués de notre hébergeur. On a plein de systèmes distribués qui constituent un système distribué. Fun.

Finalement, qu’est-ce que c’est un système distribué ?
Si j’essaye de résumer, c’est un ensemble de services qui communiquent les uns avec les autres à travers le réseau.
Et donc, dès qu’on travaille dans le web, c’est nous.

Pourquoi on fait ça ?

On fait ça parce que mon serveur tout seul, le jour où il se casse la gueule, le jour où il s’écroule, je suis content d’en avoir un deuxième qui prenne le relais.
Et puis pareil pour la base de données. J’ai un serveur de base de données, je mets en place de la réplication. Le jour où mon premier serveur de base de données s’écroule, je bascule sur le second.
Extrêmement pratique.

Mais aussi, dimensionner ou scaler un gros composant ou un gros logiciel monolithique, ça peut être assez difficile, on va assez rapidement atteindre des limites. On ne peut pas pousser les murs que la machine.

Et il est peut-être plus facile de dimensionner ou de scaler plusieurs petits composants… Pour peu qu’ils soient indépendants.
Donc oui, on crée un système distribué pour la scalabilité, et on va devoir gérer la scalabilité du système distribué. On se crée un peu nos propres problèmes, en fait 😅.

Et puis, si vous hébergez votre système en France, votre application répond rapidement aux utilisateurs qui sont en France, quelques millisecondes.
Par contre, il faut plusieurs centaines de millisecondes pour répondre aux utilisateurs qui sont en Amérique ou en Asie.
Et les millisecondes de latence ont un impact majeur sur votre chiffre d’affaires.

Donc, qu’est-ce qu’on fait ?
On héberge notre application, sur d’autres continents, au plus proche de nos utilisateurs.

Et puis encore sur d’autres continents.
Sauf que maintenant, on a plusieurs centaines de millisecondes entre nos serveurs et on met plusieurs centaines de millisecondes à répliquer des données ou à effectuer des transactions distribuées.
Mieux pour nos utilisateurs, plus compliqué pour nous.

Et puis construire un système distribué, ça peut aussi être pour se faciliter la tâche, peut-être ? Ou, plutôt, pour répartir la charge de travail, la création, la maintenance de services et d’applications.
On peut avoir plusieurs équipes qui vont travailler chacune à leur rythme. Chaque équipe va pouvoir bosser avec sa techno, du PHP, du JS, du Rust, du Go, plein d’autres choses. Théoriquement, on aura moins de dépendance, moins de couplage entre nos services et nos équipes, pour peu qu’on définisse nos contrats d’API en amont.
Il y a beaucoup de théorie, à nous de faire en sorte que ça marche en pratique.

Et puis un autre intérêt, c’est de pouvoir utiliser le bon outil pour répondre aux bons besoins.
MySQL, ça marche très bien dans certains cas, mais dans d’autres cas, on voudra peut-être de la recherche full text avec Elasticsearch, ou du stockage clé-valeur, ou du stockage orienté documents.
Avec des microservices et un système distribué, on peut faire ça.

Et on peut même pousser plus loin et utiliser des services externes.
Genre un service développé par une autre boîte : on va signer un contrat avec cette autre boîte et on va payer cette autre boîte. Mais on ne va pas réinventer la roue.
Pensez prestataire de paiement, prestataire d’expédition de colis, régie publicitaire, plein de services comme ça, ou hébergeurs de cloud.

Maintenant on le sait, dans nos métiers, communiquer, c’est difficile, on le voit tous les jours, on fait des morning meetings tous les matins pour communiquer.
Eh ben c’est pareil sur le réseau : des services qui communiquent entre eux, c’est difficile.

Et quand on a un problème en informatique, on ajoute des couches d’abstraction.
Dans le réseau, il devait y avoir beaucoup de problèmes.

On commence avec la couche liaison de données, tout en bas, je n’en parlerai pas plus.
Et par-dessus, on met la couche IP, la couche niveau 3 du modèle OSI.
La couche IP est réputée non fiable. Il y a une petite garantie sur la transmission des entêtes de paquets et des checksums pour vérifier qu’on n’a pas d’erreur, mais tout le reste il n’y a pas de garantie.
Corruption de données, ordre d’arrivée de paquets, perte de paquets, duplication de paquets, tout ça, non, pas de garantie. C’est géré par les couches de niveau supérieur.
Donc il va y en avoir d’autres sur le schéma. Par contre, l’avantage c’est que les routeurs vont vite.

Mais qui dit couche de niveau supérieur dit TCP, couche 4. Et TCP fournit un protocole de transport fiable en mode connecté.
Et surtout, TCP gère la perte de segment, en attendant et en réessayant.
Donc on envoie paquet, on attend un accusé de réception, si on n’a pas eu l’accusé de réception, on renvoie le paquet.
Ok, tout va bien. Mais n’importe qui peut lire ce qu’on fait sur le réseau.

Donc on ajoute de la sécurité avec une couche supplémentaire, le protocole TLS.
Trois objectifs de sécurité, l’authentification du serveur, la confidentialité des données (= du chiffrement) et l’intégrité des données échangées.
Et j’ai dit trois parce que le quatrième, on l’utilise extrêmement rarement : on peut aussi authentifier les clients
On n’est toujours pas en haut du schéma.

Au-dessus de TCP vient le protocole HTTP, celui avec lequel on travaille quasiment tous les jours.
Ou plutôt vient HTTPS, au-dessus de TLS, quand on veut de la sécurité des échanges.

Et enfin, on arrive en haut du schéma et on arrive sur nos applications, nos API, nos conventions.
Et on décide, est-ce qu’on va faire du REST ? Est-ce qu’on va faire du GraphQL ? Est-ce qu’on va faire du GRPC ?
Et là on doit aussi définir comment versionner nos API. Comment est-ce qu’on fait pour ne pas tout casser chez les utilisateurs de nos API à chaque fois qu’on fait une évolution ? Très bonne question.

Si je résume : un utilisateur veut parler à un serveur.
- On commence par établir une connexion TCP, ça se fait en trois temps, puis on a la connexion.
- TLS, c’est plein d’échanges de données dans tous les sens, vous regarderez sur Wikipédia si vous voulez les détails sur les flèches, il n’y avait pas la place.
- Et enfin, l’utilisateur commence à parler à notre serveur, commence à parler à notre application.
- Notre application met un petit peu de temps à faire son truc et répond à l’utilisateur.
Le nombre de flèches ici, ça montre bien qu’on a intérêt à être proche de nos utilisateurs. Plus on réduit la durée de chacune de ces flèches, plus notre application est jointe rapidement.

Et j’ai oublié la résolution DNS qu’on fait avant tout ça !

Un système distribué, ça a des très bons côtés. Par exemple, chaque brique peut être dimensionnée en fonction des besoins.
Mais ça a aussi des côtés un petit peu plus challengants, puisque chaque brique doit être dimensionnée en fonction des besoins.

Souvent, quand on a un serveur qui n’est pas assez puissant, qu’est-ce qu’on fait ?
On prend un serveur plus puissant.
Donc globalement, on va ajouter des CPU, on va ajouter de la RAM, on va ajouter du disque, on va ajouter des CPU, on va ajouter de la RAM, on va ajouter du disque. Puis encore des CPU, encore de la RAM, encore du disque. On atteint quand même les limites de nos machines à quelques centaines de CPU et quelques dizaines de Teraoctets de RAM.
Puis quand on a une machine qui a des centaines de CPU, elle coûte cher et on n’en a qu’une. Donc quand elle s’écroule, on est un petit peu embêté.

On peut faire des choses un petit peu autrement.
On peut faire ce qu’on a commencé à faire tout à l’heure : on peut découper une plateforme en composants séparés, en composants techniques séparés, qui aient chacun sa responsabilité. C’est ce qu’on fait quand on a PHP sur un serveur, MySQL sur un autre. Et c’est valable avec autre chose que PHP bien sûr.
On répartit la charge sur plusieurs serveurs qui peuvent être moins puissants.

Ou alors on découpe notre application en composants fonctionnels : chaque composant a sa responsabilité bien définie.
Et qui a dit microservices ? Peut-être on peut arriver à des microservices.
Là aussi on répartit la charge sur plusieurs services, sur plusieurs serveurs et peut-être sur plusieurs équipes.

Mais au bout d’un moment, quand même, nos serveurs, ils sont plus assez gros.
Et la seule solution c’est d’ajouter des serveurs. Plus de serveurs, plus de machines, donc plus de CPU, plus de RAM.
Et ça marche très bien notamment sur des systèmes stateless, quasiment tout ce qu’on fait dans le web : on ajoute plein de machines.

C’est un petit peu plus compliqué pour les données : on va souvent les partitionner, les données, les répartir sur plusieurs serveurs. Et on doit réfléchir à une clé de partitionnement.
Imaginons par exemple sur un site e-commerce, et c’est une mauvaise clé de partitionnement, mais sur un site e-commerce, je peux dire je mets sur un serveur les commandes de l’an dernier, sur un autre serveur les commandes de cette année.

Ou pour une base de données d’utilisateurs, je peux prendre trois serveurs.
Tous les utilisateurs dont le nom commence par A
à H
vont sur le premier serveur. Tous les utilisateurs dont le nom commence par I
à P
vont sur le second, et ainsi de suite pour Q
à Z
sur le troisième.

Et là, qui dit solution dit aussi problématique, nouvelle problématique.
Oui, on va se retrouver avec une partition plus chaude que les autres. Prenons “Martin”, le nom le plus commun en France. Eh bien “Martin” fait que la partition en bas à droite, elle va être plus sollicitée que les autres. Plus de stockage de données ou plus de volume de requêtes peut-être.
Et on va avoir besoin de repartitionner, découper une partition en deux partitions plus petites. Ou dans l’autre sens, peut-être la nuit quand nos serveurs sont peu sollicités, refusionner des partitions.
Et on ne veut pas requêter plusieurs partitions à la fois. Donc on doit réfléchir à notre schéma de partitionnement avant de faire nos requêtes. Si on veut requêter plusieurs partitions en parallèle, c’est coûteux, ça prend du temps, c’est complexe, donc on ne veut pas faire ça. Sauf pour de l’analytics, à la limite, et, souvent, on mettrait les données ailleurs.

Et puis si la charge est trop lourde pour un seul serveur, qu’est-ce qu’on fait ? Potentiellement, on ajoute des serveurs de lecture.
Donc les serveurs en rouge sont les serveurs d’écriture, les serveurs en orange de lecture. Et puis si beaucoup de gens essayent d’accéder à “Martin”, on peut mettre plus de serveurs de lecture sur une partition que sur les autres.
Et à nouveau, on va rencontrer des problématiques de réplication de données, où on écrit sur un serveur d’écriture, et un moment plus tard, les serveurs de lecture seront à jour. J’en reparlerai tout à l’heure.

Quand on a plein de serveurs, on veut étaler les requêtes sur ces serveurs, on met en place un load balancer.

L’approche la plus simple, la moins coûteuse du load balancing, c’est : les utilisateurs font des requêtes, on les balance sur les différents serveurs à tour de rôle ou au hasard, ça marche très bien.
Du load balancing avec plusieurs serveurs, ça demande de pouvoir ajouter ou supprimer des serveurs dynamiquement pour répondre à la charge. Et ça demande aussi d’avoir des sondes qui vont détecter qu’un serveur ne répond plus pour qu’on arrête de lui balancer du trafic.

Et souvent quand je parle d’élasticité, de scalabilité, j’ai un développeur qui pointe la tête dans le bureau et qui dit “Hey, et si on mettait du cache ?”

L’application regarde si une donnée est en cache. Si elle est en cache, elle l’utilise. Sinon on va la chercher dans base de données ou sur un service distant : une opération un peu plus coûteuse.
Et dans beaucoup de cas, le cache va nous aider.

On a deux approches possibles au cache.
L’approche de gauche, où on a un cache dans les processus ou sur chacun des serveurs applicatifs.
C’est cool, le cache scale comme l’application. On ajoute des serveurs, on ajoute du cache, on a de la scalabilité, c’est très bien.
Par contre, chaque serveur peut avoir une version différente de la même donnée. On a un compromis à faire.

L’autre approche, l’approche de droite, on a un serveur de cache centralisé, un gros serveur de cache, ce qui garantit que tous les serveurs applicatifs verront la même version de la donnée en cache. C’est extrêmement pratique.
Par contre, si le serveur de cache s’écroule, on n’a plus de cache.

Et qu’est-ce qui se passe quand le cache n’est pas là ? Que ce soit parce qu’un serveur de cache s’est écroulé ou parce qu’on a une nouvelle donnée qui n’est pas encore en cache ?
Eh bien on a tous nos serveurs applicatifs qui font leur requête sur la base de données ou le service distant, qui n’est peut-être pas dimensionné pour.
Quand on met du cache, on doit quand même réfléchir au dimensionnement de notre base de données.

Et depuis tout à l’heure, on met en cache des données, on régénère des réponses encore et encore.
Mais je vous ai dit tout à l’heure, les réponses, on peut peut-être les garder en entier et les renvoyer à plusieurs utilisateurs ?

Faisons du cache HTTP, mettons en cache des réponses HTTP entières.
Le chemin un peu traditionnel sur Internet, c’est nos utilisateurs passent à travers Internet pour parler à notre application.
Imaginons à droite c’est notre application, qui se prend plein de requêtes. Peut-être ça rame un peu ou alors ça nous coûte cher en hébergement.

Et si on mettait un serveur de cache avant notre application ?
Les utilisateurs requêtent le serveur de cache. Et quand il n’a pas la donnée en cache, il va la chercher sur notre origine.

Et ensuite, il répond aux utilisateurs directement depuis le serveur de cache.
Nos serveurs à droite vont être beaucoup moins chargés.

Mais si on est sur plusieurs continents, on peut voir les choses un peu différemment : on peut envisager de mettre des serveurs de cache plus proches de nos utilisateurs pour essayer de réduire la latence.
Les utilisateurs font une requête au serveur de cache le plus proche de chez eux, qui lui fait une requête à notre origine, stocke la donnée en cache et répond aux utilisateurs suivants. Et ainsi de suite pour le troisième continent.
Au bout d’un moment, toutes les requêtes sont servies depuis les serveurs de cache. On y gagne en latence pour les utilisateurs. Par contre, on fait un petit peu plus de requêtes sur notre origine qu’avec le schéma précédent.
Combinons les deux, faisons le meilleur des deux mondes : des serveurs de cache proches de nos utilisateurs sur chaque continent et un serveur intermédiaire qui va absorber les requêtes et protéger notre origine. C’est ce qu’on fait quand on met en place un CDN à deux niveaux. Et ça marche plutôt bien.

Vous le savez, quand on travaille à plusieurs personnes, ou même plusieurs équipes, on a parfois besoin de nous coordonner. Par exemple, on ne sort pas un front si on n’a pas développé l’API derrière qui fournit les données.
Et ce n’est pas toujours facile de se coordonner. Pour nos applications, c’est pareil, c’est pas facile.

Déjà, dans nos systèmes, on a souvent besoin de savoir dans quel ordre les opérations s’exécutent.

Je prends un exemple un petit peu tiré par les cheveux, mais admettons : un utilisateur qui fait des requêtes sur une API, sur deux machines. Notre API, les API, écrivent des trucs en log, des trucs qui marchent, des trucs qui ne marchent pas, enfin bref, tout ça.

Et au bout d’un moment, on veut comprendre ce qu’il s’est passé. Il y a longtemps, on se serait connecté en SSH sur les différents serveurs, on aurait regardé les logs. Mais, maintenant, on ne fait plus ça.

Maintenant on se dit que ces logs, je veux les ramener sur un serveur centralisé.
Et surtout, j’ai besoin de les parcourir dans l’ordre : je veux savoir quelle opération s’est produite en premier.

Et c’est quelque chose qui est difficile quand les serveurs ne sont pas à l’heure, ne sont pas à la même heure. Sauf que les serveurs ne sont jamais à la même heure. En fait, les horloges n’avancent pas à la même vitesse.
Et c’est valable dans des services, dans des systèmes informatiques, mais vous prenez des montres, des montres mécaniques avec une trotteuse, vous en posez deux sur une table… Au bout d’un mois, si elles sont de bonne qualité, il peut y avoir 15 secondes d’écart entre les deux.
C’est pareil dans nos machines, on a du drift entre les horloges des différentes machines. Et même avec une seule horloge, les circuits électroniques ne vont pas toujours à la même vitesse. On peut avoir une petite différence entre deux circuits électroniques. Avoir du Skew.

Donc quand on veut ordonner des événements, on arrête de regarder l’heure et on se base sur d’autres approches, on se base par exemple sur des horloges logiques, qui fournissent des informations sur l’ordre d’arrivée des événements.
Qu’est-ce qui est arrivé avant, qu’est-ce qui est arrivé après. En fait, c’est de ça dont on a besoin. Rarement de l’heure.

Et puis je prends un autre cas, un cas que j’aime bien, il est marrant. Problématique, peut-être, mais marrant.

Imaginons, on a une application, avec sa base de données. En base de données, A
vaut 23.

J’ai deux utilisateurs.
Le premier dit A
vaut désormais 36
et le deuxième dit A
vaut désormais 42
. Question, combien vaut A
?
Eh bien, je remontre l’animation : le premier utilisateur envoie 36
, le deuxième envoie 42
, mais le serveur reçoit 42
en premier. Combien vaut A
?
- Est-ce que je prend la première requête qui est arrivée ?
- Est-ce que je prends la première requête qui a été émise ?
Et bien, si je ne me suis pas posé la question, je ne le sais pas… Et vous non plus.

On peut parfois résoudre ça en ajoutant une condition comme ici :
- Mets
A
à36
siA
valait23
. - Mets
A
à42
siA
valait23
.
23
c’était la valeur d’origine.
Avec ça, une seule des deux requêtes va réussir, l’autre va échouer, et on saura pourquoi : A
ne valait plus 23
.
Ça peut être une bonne approche pour résoudre cette problématique et savoir quelle requête réussit.

Ou alors on a de la chance, on est dans un cas business qui nous permet de faire une autre opération.
Par exemple, au lieu de renseigner A
à une valeur, on fait cette opération d’incrémentation : A
vaut A + 19
ou A
vaut A + 13
. Et peu importe l’ordre dans lequel ces deux opérations s’exécutent, la valeur finale de A
est la même.
C’est un cas d’école un peu tiré par les cheveux mais, dans certaines applications, ça peut marcher.

Je parlais de réplication de données tout à l’heure. Vous savez, un serveur central qui reçoit les écritures, et des serveurs de lecture à côté.

Le cas assez classique de la réplication, c’est celui-ci : l’utilisateur effectue une opération d’écriture sur un serveur, la donnée est répliquée sur tous les serveurs de lecture qui sont autour.
Maintenant la grande question, c’est quand est-ce qu’on dit à l’utilisateur “Oui, nous avons pris en compte ton écriture” ?
On a trois cas possibles.

Premier cas, l’utilisateur écrit et tout de suite on lui dit “oui c’est bon, on a pris en compte ton écriture”.
C’est cool pour l’utilisateur, c’est rapide, on lui répond vite.

Par contre, les autres utilisateurs qui viennent lire la donnée voient l’ancienne version.
Et si on n’a vraiment pas de chance et que notre serveur d’écriture s’écroule, il a une panne, une coupure de courant ou un bug, nous perdons totalement la nouvelle valeur. Alors qu’on a dit à l’utilisateur qu’on l’avait pris en compte.
Donc c’est rapide, c’est bien, mais c’est peut-être pas terrible.

Deuxième approche : l’utilisateur fait son écriture et on attend d’avoir répliqué la donnée sur la majorité des serveurs de lecture. Et ensuite, on répond OK à l’utilisateur.
Ça met plus de temps : on a attendu que plusieurs serveurs aient répliqué la donnée.

Et pour les gens qui viennent lire la donnée, c’est un peu fun : certains ont l’ancienne, d’autres ont la nouvelle, mais au bout d’un moment ils devraient tous avoir la nouvelle.
Si notre serveur d’écriture se casse la gueule, puisque la majorité des réplicas ont la nouvelle version de la donnée, on peut déterminer quelle est la nouvelle version de la donnée : celle qui est présente sur le plus de réplicas.

Ou alors troisième approche : on recommence le schéma, on attend d’avoir répliqué la donnée sur tous les réplicas avant de répondre OK à l’utilisateur.
Et là, on va attendre longtemps. On a des réplicas peut-être sur d’autres continents, donc plusieurs centaines de millisecondes. Ou on n’a vraiment pas de chance et un des réplicas est éteint. Et on va donc attendre éternellement. Donc on ne répondra jamais à l’utilisateur.

Par contre, les utilisateurs qui viennent lire la donnée, ils ont tous la nouvelle version.
Donc on fait un compromis entre la latence et la cohérence. Et c’est quelque chose qu’on doit faire assez souvent.
Quelle approche est choisie par votre moteur de base de données, par votre système de stockage ? Ça dépend du système que vous utilisez, ça dépend de son paramétrage. Et si vous ne vous êtes pas posé la question, quelqu’un a choisi à votre place.

Et maintenant, passons à un autre sujet, qui sert assez souvent dans les microservices.

La problématique qu’on a, c’est l’utilisateur en haut à gauche fait une requête sur une API, l’API écrit une donnée, mettons dans une base de données.
Et ensuite, on veut transmettre une information à nos autres microservices pour dire “coucou, il y a une nouvelle version de la donnée”.

Pour ça, on pousse un message sur un bus d’événements. Architecture très très classique sur des microservices.

L’écriture en base de données, c’est une opération transactionnelle (begin
+ commit
ou rollback
), qui garantit du tout ou rien.
Par contre, ici, l’écriture dans le bus de messages n’est pas transactionnelle : on écrit en base de données, puis on envoie un message. Si on n’a vraiment pas de chance, on écrit en base de données, notre application crashe. Ça arrive, et on n’envoie jamais le message disant au reste de notre système qu’on a une nouvelle version de la donnée.
C’est souvent inacceptable. Ça mène à une désynchronisation entre nos systèmes.

Comment on en réchappe ? En revoyant un petit peu notre approche.
On déplace notre bus d’événement ici, et maintenant l’utilisateur appelle l’API, l’API effectue ses enregistrements en base de données, et en plus écrit en base de données une information disant que, à un moment, il faudra envoyer un événement dans le bus de message.

Ensuite, en asynchrone, on a un worker, un processus en arrière-plan qui tourne, qui vient voir ce qu’il y a en base de données.
Et en base de données, il voit qu’il va falloir envoyer un événement dans le bus de messages. Il envoie l’événement dans le bus de messages. Et une fois qu’il a envoyé cet événement dans le bus de messages, il efface l’information en disant qu’il va falloir envoyer un événement dans le bus de messages.
Ici, ça peut toujours planter. Mais le reste, la base de données, c’est transactionnel. Et on écrit dans le bus de messages avant d’effacer l’enregistrement en base de données qui dit qu’il faut écrire dans le bus de messages.
Le pire des cas c’est quoi ? On écrit dans le bus de messages, notre worker plante. Quand il se relance, l’information en base de données est toujours là, il réécrit dans le bus de messages. On peut délivrer un message plusieurs fois.
Mais très souvent, dans nos systèmes, il vaut mieux distribuer un message plusieurs fois, dire plusieurs fois “hey, il y a une nouvelle version de la donnée” plutôt que de ne jamais transmettre qu’on a une nouvelle version de la donnée.
Ça s’appelle le Pattern Outbox, et c’est assez pratique.

Dans cet exemple, nous allons prendre un train, nous faisons un voyage.
Pour aller en conférence, on prend un train, l’aller, le retour. On prend un hôtel pour dormir et on fait un paiement. Et tout ça, je veux du mode tout ou rien. Je veux avoir mon train, mon hôtel, mon paiement. Et je ne veux pas avoir le train mais pas l’hôtel ou le paiement sans le train. Enfin bref, ce genre de choses.
Et autant quand on travaille avec un seul système, une seule base de données, on fait begin
puis commit
ou rollback
, autant quand on est sur plusieurs systèmes, c’est plus compliqué.

Faire une approche où on envoie des begin
sur chacun des services de train, d’hôtel et de paiement, puis on envoie un commit
ou un rollback
sur chacun de ces services, ce n’est pas toujours possible.
C’est pas toujours possible parce que c’est lent… Et aussi parce que ce n’est pas supporté par beaucoup de systèmes.

Donc à la place, quand on veut travailler avec plusieurs services, quand on veut effectuer une transaction distribuée, on met en place le Pattern Saga.

On est quelqu’un d’optimiste : depuis notre système, on dit “prends le train”, “prends l’hôtel”, “prends le paiement”.
Et on sait qu’en général ça marche, donc on se permet d’être optimiste.

Mais si par malchance, à un moment, la réservation de l’hôtel échoue, ou que nous n’obtenons pas l’information en disant qu’elle a réussi (et ça arrive de temps en temps), nous allons annuler la réservation du train, nous allons annuler le paiement et nous allons annuler la réservation de l’hôtel.
Et j’ai bien dit qu’on annule la réservation de l’hôtel : on ne sait pas si elle avait échoué la première fois, ou si on n’avait pas reçu l’information nous disant qu’elle avait réussi.
Et ces transactions d’annulation, on va les réessayer jusqu’à ce qu’on reçoive confirmation qu’elles ont été prises en compte.
Ça demande au système distant d’implémenter un mécanisme de réservation et un mécanisme d’annulation, et ça demande que le mécanisme d’annulation puisse être appelé plusieurs fois.

Puis deux ou trois termes de plus avant de terminer cette section sur la coordination.

Difficile de parler de coordination sans parler du théorème CAP qui, dans les très grandes lignes, vous dit qu’entre la cohérence, la disponibilité et la tolérance aux partitions, vous ne pouvez en choisir que deux.
Et assez souvent dans le système, quand une partition réseau se produit, il faut choisir entre annuler une opération ou continuer avec l’opération.
- Si on annule l’opération, on diminue la disponibilité de notre système, mais on assure la cohérence entre les différents logiciels.
- Si on continue avec l’opération, c’est cool, on est disponible. Par contre, les données sont un peu désynchronisées ici et là.

Et comme le théorème CAP s’applique uniquement en cas de problème réseau, de partitions réseau, qui sont assez rares, il est étendu par le théorème PACELC qui ajoute la deuxième moitié, ce qui est en bas de l’écran, qui dit que quand tout va bien, on doit surtout choisir entre la latence et la cohérence.
Et c’est ce qu’on a fait jusqu’à présent.

Maintenant n’oublions pas que quand on développe des applications, le premier but de ces applications, c’est qu’elles marchent. Et je ne parle pas sur nos laptops de développeurs, je parle qu’elles marchent avec des vrais utilisateurs.
Et je vais prendre une petite histoire inspirée de faits réels, un peu simplifiée.

Un utilisateur qui parle à une API. L’API interroge une autre API, un truc qui s’appelle le catalogue. Et les données du catalogue sont mises en cache dans un Redis. C’est quelque chose qui est beaucoup interrogé, qui est lourd. Donc on fait beaucoup de cache dans le Redis.
Les développeurs du projet ont pensé aux utilisateurs : ils ont mis du cache pour protéger le catalogue et pour répondre rapidement.

Mais manque de chance, à un moment, le cache s’écroule.
Imaginez par exemple que les entrées de cache sont tellement grosses qu’on sature la bande passante réseau des machines. Et oui, ça peut arriver, même en 2022.

Dans ce cas-là, les développeurs ont prévu le truc : ils se sont dit que les informations du catalogue sont super importantes donc, si on n’arrive pas à les charger depuis le cache, on va réinterroger le catalogue. Donc on fait beaucoup de requêtes sur le catalogue.
Le catalogue fait beaucoup de requêtes sur son Elasticsearch et Elasticsearch commence à ramer un petit peu, il a un petit peu du mal.
Donc le catalogue a un petit peu du mal.

Les développeurs de l’API ont prévu le truc.
Ils se sont dit : si le catalogue ne répond pas ou ne répond pas suffisamment vite, nous réessayons nos requêtes.

Manque de chance, toutes les requêtes étaient lentes, donc toutes les requêtes sont réessayées, donc on écroule complètement le catalogue et en conséquence notre API s’écroule et les utilisateurs ne sont plus satisfaits.

On est dans un cas où les devs, les architectes du projet, ont pris en compte plusieurs cas d’échecs :
- Ils ont pris en compte le cas où Redis ne répond pas.
- Ils ont pris en compte le cas où le catalogue est lent.
Mais la plateforme s’est quand même écroulée et nous avons atteint un joli système distributé.

En fait, quand une application explose, son impact sur les autres applications autour s’appelle son “Blast Radius”. Et plus le Blast Radius – plus le rayon d’explosion – d’une application est réduit, moins une panne impactera les autres applications.
Dans l’exemple qu’on a vu juste avant, le Redis de cache a un Blast Radius qui correspond à l’ensemble de notre plateforme.
Et un jour, quelque chose va casser. C’est comme ça, c’est la vie, faites avec. Si on veut répondre aux attentes de nos utilisateurs, le Blast Radius de chacun des composants de notre application ne peut pas être l’ensemble de notre plateforme.

Et quand je dis que tout va casser, je n’ai pas eu besoin de réfléchir beaucoup, et je suis sûr qu’en réfléchissant, on peut doubler, tripler, quadrupler ce slide.
- On peut se taper des pannes matériels, des pannes réseaux, des pics de charges, des bugs applicatifs. Surprise !
- Ou des bugs applicatifs un peu plus prévisibles, après un déploiement, parce qu’on n’a pas assez de test.
- On peut avoir des changements de configuration à effet retardé, vous savez la configuration qui est mise en cache, le cache qui expire au milieu de la nuit et boum !
- Ou alors on peut interroger un service distant qui lui-même est cassé.
Et en général, quand un truc commence à casser, il y en a d’autres qui vont casser en cascade.

En fait, Werner Vogels, le CTO d’Amazon (Amazon, un gros site), dit très souvent que “Everything fails all the time”.
Tout casse tout le temps. C’est comme ça, faites avec, c’est la vie.

Et c’est le moment de parler de deux définitions :
- La reliability, c’est la capacité à éviter les échecs.
- Et la resiliency, c’est la capacité à résister aux échecs quand ils se produisent.
Si on veut que notre application soit le mot de gauche, on a besoin de savoir résister aux échecs et d’être résilient.

Pour notre application, on va chercher à être résilient aux problèmes de nos dépendances, aux problèmes des services qu’on appelle, qu’on consomme.

Et reprenons un cas qui ressemble un petit peu à ça : notre utilisateur appelle notre application et notre application interroge un service distant.

Le service distant, pour une raison ou pour, une autre met du temps à répondre ou ne répond pas…
L’utilisateur est parti !
Pourquoi est-ce qu’on continue à consommer des ressources sur notre application pour attendre une réponse qui ne viendra peut-être jamais ?

L’utilisateur est parti, arrêtons d’attendre !
Mettons en place un time-out court.

Et ça va nous permettre de répondre à l’utilisateur avant qu’il ne soit parti en lui disant “on est désolé, on a un petit problème, mais voilà au moins la moitié de la réponse”.
Ça sera mieux que rien. Et on consommera moins de ressources sur nos machines à attendre une réponse qui ne viendra peut-être jamais.

Et puis en cas d’échec, si une requête échoue, réessayons :
- Je balance une requête, elle échoue.
- Je renvoie la requête, ça va peut-être marcher.
Quand je demande un café, qu’on m’entend mal, je redemande un café. Et la seconde fois, j’ai un café.

Mais au risque de me contredire : les retries, les réessais, sont égoïstes. Un client qui réessaye dépense plus de temps serveur pour augmenter ses chances à lui d’obtenir une réponse.
Et ça marche très bien quand les erreurs sont rares, quand les erreurs sont temporaires, quand une fois de temps en temps il y a un truc qui ne marche pas.
Par contre, quand une erreur est due à une surcharge, vous ne faites qu’empirer la surcharge.
Le cas qu’on a vu tout à l’heure où les développeurs interrogeaient le catalogue quand Redis ne répondait pas, ça marche très bien si une fois de temps en temps on n’arrive pas à se connecter à Redis. Par contre, en cas de surcharge, ce qui était le cas sur mon schéma, boum.

Et si vous réessayez, attendez de plus en plus longtemps entre vos retentatives :
- Vous envoyez une requête, elle échoue.
- Vous renvoyez la requête, elle échoue.
- Si vous la renvoyez juste derrière, elle va rééchouer. Attendez 10 millisecondes, 50 millisecondes, 100 millisecondes.
Puis au bout d’un moment, arrêtez d’attendre.

Et on peut faire des choses un petit peu plus extrêmes, même.
Notre utilisateur qui interroge notre application. Notre application interroge son service distant. Et le service distant juste ne répond pas, ne répond jamais.Soit il répond en erreur, soit il time out.

Arrêtons d’appeler ce pauvre service ! Pourquoi est-ce qu’on le bourrine encore plus ?
Non, arrêtons, arrêtons de l’appeler, et peut-être que ça va lui permettre de revenir à la vie.

De temps en temps, envoyons une petite requête sur ce service pour voir s’il est revenu, mais dans tous les cas, répondons à l’utilisateur en lui disant “bah voilà la moitié de la réponse”. Encore une fois, c’est mieux que rien.
Et si au bout d’un moment le service distant revient à la vie, on réouvre les vannes progressivement.
On ne le rebourrine pas d’un seul coup, sinon il va s’écrouler à nouveau. Garanti.

Et puis on parlait de cache tout à l’heure.
Si vous avez du cache sur vos applications, utilisez votre cache : si vous ne parvenez pas à charger une donnée depuis une base de données ou depuis un service distant, et que vous avez une vieille version de la donnée en cache.
Dans 90% des cas, vous pouvez utiliser cette vieille version, c’est mieux que rien. Quasiment tout le temps, c’est possible.

Et puis quand même, attention, le cache ne réduit pas la variabilité. Au contraire, du cache, ça ajoute de la variabilité parce qu’un cache introduit deux modes :
- Un mode nominal, les données sont en cache, l’application répond vite.
- Et un autre mode, les données ne sont pas en cache et l’application répond moins vite.
Et si le mécanisme de cache tombe en panne, et on l’a vu il y a quelques minutes, ça peut poser des pannes en cascade, et ça peut tout casser.
Donc un cache introduit de la fragilité, notamment au moment où il est le plus utilisé, au moment où peut-être notre application ne survit pas sans.

Et maintenant dans l’autre sens, notre application veut aussi se protéger des applications qui l’appellent, de ses utilisateurs.

Le cas un petit peu basique, c’est qu’on a des utilisateurs qui interrogent notre application, on leur autorise deux requêtes par unité de temps, ça permet de répondre à beaucoup d’utilisateurs.
Et si on a un utilisateur malveillant ou irrespectueux qui essaye de faire d’autres requêtes, sur les requêtes qui dépassent les deux requêtes par unité de temps, on lui dit “non”. Et ça permet de répondre convenablement aux autres utilisateurs.

Ou alors, un petit peu plus extrême, au niveau de notre application, on voit qu’on se prend une tempête de requêtes. Et au bout d’un moment, notre application arrive au maximum de sa capacité.
Eh bien rejetons les requêtes supplémentaires. Il vaut mieux rejeter brutalement 10% du trafic que s’écrouler et répondre à personne.
Et c’est le genre de choses qu’on fait typiquement sur un load balancer ou un autre composant en amont.

Puis quelques principes généraux encore…
Mettons de l’aléatoire dans nos vies !

Amusons-nous un petit peu. Et je détaille uniquement le premier exemple, celui avec des durées de cache.
- Vous démarrez une application, elle va chercher des données en base de données ou sur un service distant, et elle les stocke en cache pendant 60 minutes.
- 60 minutes après, les entrées de cache expirent toutes et vous bourrinez à nouveau la base de données, qui a un petit peu de mal à vous répondre.
- Et après, ça va bien à nouveau pendant 60 minutes.
- Et 60 minutes après, vous commencez à connaître la musique.
Mais à la place, mettez de l’aléatoire ! Stockez en cache pendant 55, 56, 58, 61, 64 minutes. Vous n’avez peut-être, vous n’avez généralement, pas besoin de stocker vos données en cache pendant exactement 60 minutes. Étalez les expirations de cache.
Et puis ne faites pas comme moi il y a longtemps : je me suis dit, “hey, mon serveur ne fait rien à 4h du matin, je vais mettre mon premier cron à 4h du matin”. Et puis après on a copié-collé la ligne de CronTab et tous nos crons étaient à 4h du matin. Et on s’est dit, mais pourquoi nos serveurs s’enflamment tous les jours à 4h du matin ? Donc étalez les crons sur la nuit…

Et puis idéalement, un client qui a un problème ne doit pas impacter les autres clients. Une application qui a un problème ne doit pas impacter les autres applications. Découpons, isolons.
Isolons dans plusieurs régions, dans plusieurs data centres, peut-être déployons notre plateforme une fois par client, si vous avez un nombre réduit de clients.
Ou alors utilisons des sous-ensembles de serveurs dédiés à certaines applications : si une application devient folle, les serveurs de cette application vont s’enflammer, mais les autres serveurs des autres applications seront toujours là.

Une fois que vous avez admis que tout allait foirer un jour ou l’autre, vous devez le gérer et pour ça vous devez mettre en place de la dégradation gracieuse. Et c’est indispensable pour continuer à fonctionner au moment où quelque chose va mal.

Je prends l’exemple d’un site de presse – j’ai bossé dans la presse il y a longtemps – et c’est peut-être le moment où vous comprenez pourquoi mes collègues m’interdisent systématiquement de faire quoi que ce soit de visible sur nos applications, et ça fait longtemps que ça dure. Je comprends pas 🤷♂️.

Imaginons, l’API qui nous sert les données météo ne répond pas.
Eh bien, n’affichons pas le bloc météo !

Ou alors notre serveur commence à ramer un petit peu parce qu’on a beaucoup d’utilisateurs.
Eh bien, n’affichons pas le blog de publicité en bas de l’écran !

Puis si on veut être un petit peu extrême, on continue comme ça à désactiver des fonctionnalités jusqu’à ce qu’il reste ça, qui est le contenu textuel de l’article, qui est ce que les utilisateurs venaient voir sur votre site.
L’expérience utilisateur est un peu dégradée, quoique, il n’y a pas une guirlande de Noël qui clignote à côté.
Bon, OK, vous ne faites pas de CA puisqu’il n’y a pas de publicité, c’est un peu plus gênant.

Mais vos utilisateurs sont vachement plus satisfaits que si leur navigateur affiche ce truc dégueulasse… Qui est à peu près toujours aussi moche depuis la nuit des temps.

Au niveau technique, comment ça se traduit ?
En général on a un truc un peu comme ça, avec des services qui appellent d’autres services, qui appellent d’autres services, et ainsi de suite, j’ai beaucoup simplifié.

Et dans le cas où la base de données tout en bas à droite s’écroule, certainement notre service D
va s’écrouler en cascade : il ne peut pas bosser sans sa base de données.

Mais nous ce qu’on veut c’est que A
fonctionne : A
qui est user facing, qui est appelé par nos utilisateurs.
Pour que A
fonctionne, on a peut-être besoin que C
fonctionne, et peut-être que C
peut fonctionner, mais du coup c’est au niveau de B
qu’il va falloir faire quelque chose.
Au niveau de B
, on va mettre en place un fonctionnement dégradé. On va renvoyer peut-être une réponse partielle. La moitié des données, mieux que rien. Ça veut dire que B
doit savoir d’une certaine façon fonctionner sans D
pour que notre plateforme, dans son ensemble, continue à répondre.

Et ici, le Blast Radius de la base de données en bas à droite est beaucoup plus réduit, il est maîtrisé : ce n’est pas l’ensemble de notre plateforme. Et on continue à répondre aux utilisateurs.

Et j’en viens à cette citation que j’aime vraiment beaucoup.
Les systèmes distribués ne sont jamais opérationnels, ils existent dans un état permanent de service partiellement dégradé.
Et si on ne l’admet pas, c’est totalement dégradé.

On approche petit à petit du temps qui m’est alloué et il y a plein de choses dont j’ai pas parlé. Et je pense qu’on pourrait faire une conférence entière, toute une journée sur ce sujet, je crois que ça existe.
Les systèmes distribués, c’est passionnant.

On n’a pas parlé d’environnement de développement.
- Genre, avec tous ces services, qu’est-ce qu’on fait tourner en local ?
- Qu’est-ce qu’on fait tourner ailleurs ?
- Est-ce qu’on fait du Docker ?
- Est-ce qu’on fait autre chose ?
- Un hébergement centralisé pour le reste, comment est-ce qu’on déploie tout ça ?
- Comment est-ce qu’on l’orchestre ?

Mais aussi :
- Comment est-ce qu’on déploie aussi notre chaîne de CI, de CD ?
- Comment est-ce qu’on teste ?
- Comment est-ce qu’on détecte des problèmes automatiquement ?
- Comment est-ce qu’on roll back ? Automatiquement ? Bonne question.
- Est-ce qu’on fait du canary, du blue-green ?
Ce genre de choses.

On n’a pas non plus parlé de choisir nos objectifs.
En fait, pour choisir entre deux solutions techniques, pour choisir entre deux approches, pour savoir où placer votre curseur à compromis, vous avez besoin de connaître vos objectifs.
Et vos objectifs techniques dépendent de vos objectifs business.

Et puis pour savoir si tout va bien, ou plus réalistement, pour savoir ce qui va mal, vous avez besoin d’observabilité.
Et suivre le chemin d’opérations à travers plusieurs services, ce n’est pas facile quand on est mal outillé.

Et au bout d’un moment, remplacer un monolithe par un ensemble de microservices, en cas d’incident, c’est une enquête assez sympathique.

Et sous les yeux, vous avez huit idées fausses que les développeurs croient trop souvent, d’après une publication de Peter Deutsch et d’autres quand ils bossaient chez Sun Microsystems.
- Genre le réseau est fiable. Non.
- Le temps de latence est nul. Non plus.
- La bande passante est infinie. Pas du tout, on l’a vu tout à l’heure avec Redis.
- La topologie ne change pas.
- Le coût de transport est nul. Vous verriez les factures… Non.
Enfin bref, tout ça, ça date de 1994. Voilà. Presque 30 ans après, on en est encore là.

En conclusion, on a de la chance : dans nos métiers, on ne s’ennuie jamais.
Et c’est super cool.

On travaille avec des systèmes distribués pour gagner en scalabilité, en performance, peut-être pour gagner en résilience si on fait ce qu’il faut, pour exploiter le meilleur outil pour répondre à chacun de nos besoins.
Et puis peut-être aussi pour en permettre à chaque équipe de travailler avec un petit peu plus d’indépendance, à son rythme, avec ses propres outils.

En 2023, si on essaye de voir les choses en face, impossible de travailler sans système distribué.
Donc nous d’en tenir compte quand on conçoit nos architectures, à nous d’y penser.

On fait des compromis, on fait des choix et on ne peut pas tout avoir mais on peut avoir plein de choses bien.

Pour ça il faut savoir un petit peu ce qu’on veut.
Donc pour faire du bon boulot, on a besoin de définir nos besoins avant de commencer à architecturer. C’est comme ça qu’on fait une bonne architecture qui n’est pas non plus trop architecturée.

Puis basez-vous sur le travail des autres.
Genre l’expérience, la recherche, ne réinventez pas Raft, par exemple. Ou même, utilisez des services qui existent, qui marchent, qui marchent depuis longtemps.
Et n’allez pas réimplémenter Cassandra, DynamoDB, ETCD. Non, utilisez ces trucs-là ! La valeur ajoutée dans vos entreprises, elle est ailleurs.

Et quoi qu’il en soit, si on s’en donne les moyens, tout est possible :-)

Je m’appelle Pascal MARTIN, j’ai pendant longtemps été développeur avec PHP et son écosystème, j’ai été Lead DevOps, je suis désormais Principal Engineer chez Bedrock et AWS Hero.
Vous avez mes infos de contact sur pascal-martin.fr et on n’a pas beaucoup de temps pour les questions, mais vous pouvez me trouver à proximité d’une machine à café toute la journée et demain. C’est un petit peu mon péché, le café.
Merci beaucoup !
J’ai donné cette conférence plusieurs fois – et je serais très heureux de la redonner, c’est un sujet qui me passionne… Vous savez où me trouver ;-)
Je n’apprends jamais mon discours par cœur et j’ai toujours quelques variations mineures entre deux déroulés, mais ce que j’ai noté ici doit être assez proche de ce que j’ai dit à MixIT en 2023.