Blog

  • Bonnes pratiques de journalisation et d’observabilite

    Bonnes pratiques de journalisation et d’observabilite

    Vous decouvrez la qualite de votre observabilite au pire moment possible : quand quelque chose est casse, que les clients le remarquent, et que vous ne savez pas pourquoi. Tout ce que je fais ici vise ce moment. Le but est de repondre a « que se passe-t-il et pourquoi » en minutes, pas en heures. Une bonne observabilite, c’est la difference entre un incident calme et un incident frenetique.

    Journalisez des donnees structurees, pas des phrases

    Les lignes de log lisibles par l’humain semblent sympathiques jusqu’a ce que vous deviez en chercher dix millions. Alors vous ecrivez des regex fragiles contre de la prose. Je journalise des enregistrements structures, cle-valeur ou JSON, pour que les logs soient interrogeables comme une base plutot que fouilles comme un journal intime.

    // Pas ca :
    log.info("Utilisateur " + userId + " echec connexion depuis " + ip)
    
    // Ca :
    log.info("login_failed", {
      user_id: userId,
      ip: ip,
      reason: "bad_password",
      attempt: 3
    })

    Maintenant « montre-moi toutes les connexions echouees de cet utilisateur dans la derniere heure » est un filtre, pas un projet d’archeologie. Choisissez des noms de champ coherents entre services pour que le meme concept ait la meme cle partout, et une requete ecrite une fois fonctionne sur tout le systeme.

    Utilisez les niveaux avec discipline

    Les niveaux de log n’aident que s’ils veulent dire quelque chose de coherent. Quand tout est journalise en INFO, le niveau n’est que du bruit. Ma regle empirique :

    • ERROR, c’est quelque chose de casse qu’un humain doit regarder. Si ca ne merite pas d’attention, ce n’est pas une erreur.
    • WARN, c’est inattendu mais gere, le genre de chose qu’il vaut la peine de surveiller pour un motif.
    • INFO, ce sont des evenements metier importants : une commande passee, un travail termine.
    • DEBUG, c’est du detail pour le developpement local, en general coupe en production.

    Le test pour ERROR est simple : si une alerte se declenchait pour chacun, seriez-vous en colere ? Si oui, ce n’est pas vraiment une erreur, et vous venez de vous entrainer a ignorer le niveau cense vous reveiller.

    Faites circuler un identifiant de requete partout

    Dans tout systeme a plus d’un service, une seule action utilisateur devient une douzaine de lignes de log eparpillees sur plusieurs machines. Sans un fil qui les relie, vous devinez. Je genere un identifiant de correlation a la frontiere et le passe a chaque appel en aval et dans chaque ligne de log. Alors un seul identifiant reconstitue tout le chemin d’une requete, pour la meme raison qui me pousse a garder des formes d’erreur coherentes dans le guide de conception d’API REST : quand autre chose doit suivre la piste, la structure gagne.

    Mesurez les trois choses qui parlent de sante

    Les logs parlent d’evenements precis. Les metriques parlent du systeme dans son ensemble, et ce sont elles qui font tourner vos tableaux de bord et vos alertes. Pour tout service qui traite des requetes, je suis le debit, les erreurs et la duree : combien de requetes, combien ont echoue, et combien de temps elles ont pris. Regarder la distribution de latence plutot que la moyenne compte, car la moyenne cache la queue lente ou les vrais utilisateurs souffrent.

    • Suivez la latence au 95e et au 99e percentile, pas seulement la moyenne.
    • Suivez le taux d’erreur en pourcentage pour qu’il ait du sens a tout niveau de trafic.
    • Suivez la saturation, le taux de remplissage de vos ressources, pour voir les ennuis avant qu’ils deviennent une panne.

    Alertez sur les symptomes, pas sur les causes

    Une alerte devrait signifier qu’un humain doit agir maintenant. Sinon, ce devrait etre un tableau de bord, pas une alerte. La facon la plus rapide de faire detester l’astreinte, ce sont des alertes qui se declenchent sans cesse et ne veulent rien dire, car les gens apprennent a les balayer puis ratent celle qui comptait. J’alerte sur les symptomes vus par l’utilisateur, comme un taux d’erreur qui franchit un seuil ou une latence qui explose son budget, plutot que sur des causes internes comme un CPU eleve, qui peut etre parfaitement normal.

    Une derniere chose qui paie : ne journalisez jamais de secrets, mots de passe, jetons ou details de paiement complets. Il est facile de les laisser fuiter dans les logs par accident, et les logs s’etalent sur des systemes aux controles d’acces plus faibles que votre base. Le meme soin guide par les contraintes que j’ai decrit dans les bonnes pratiques de schema de base de donnees vaut ici aussi. Decidez ce qui est sensible, puis assurez-vous que ca n’atteigne jamais une ligne de log.

  • Gestion d etat Flutter avec Provider, Riverpod et Bloc

    Gestion d etat Flutter avec Provider, Riverpod et Bloc

    La gestion d etat est l endroit ou les projets Flutter restent sains ou pourrissent. Le framework fournit setState, qui convient pour un seul widget, mais des que l etat doit etre partage entre ecrans il faut mieux. J ai livre des applications avec Provider, Riverpod et Bloc, et chacun gagne sa place dans des situations differentes. Voici comment je choisis.

    Pourquoi setState ne suffit plus

    setState reconstruit le widget dans lequel il vit. Cela marche jusqu a ce que deux ecrans aient besoin des memes donnees, ou qu un enfant profond ait besoin d une valeur tenue pres de la racine. Vous finissez par passer des callbacks et des valeurs vers le bas a travers les constructeurs, couche apres couche, et le terme pour cette misere est le prop drilling. Chaque bibliotheque d etat existe pour resoudre ce meme probleme: amener les donnees la ou elles sont necessaires sans les enfiler a travers tout le reste.

    Provider, le depart en douceur

    Provider est longtemps la recommandation officielle et reste un choix par defaut raisonnable. Il s appuie sur les inherited widgets et offre une facon propre d exposer une valeur a l arbre en dessous. Un ChangeNotifier tient votre etat et appelle notifyListeners quand quelque chose change, et les widgets qui ecoutent se reconstruisent. C est simple a raisonner et facile a enseigner a un nouveau membre.

    import 'package:flutter/material.dart';
    
    class CartModel extends ChangeNotifier {
      final List<String> _items = [];
      List<String> get items => _items;
    
      void add(String item) {
        _items.add(item);
        notifyListeners();
      }
    }

    La faiblesse de Provider apparait a grande echelle. Il est lie a l arbre de widgets, donc tester la logique isolement demande des efforts, et il est facile de reconstruire plus de l arbre que prevu. Pour les petites et moyennes applications je n ai aucune plainte.

    Riverpod, ce que je choisis maintenant

    Riverpod vient du meme auteur que Provider et corrige la plupart de ses douleurs. L etat vit en dehors de l arbre de widgets, donc vous pouvez le lire sans BuildContext, le tester sans monter de widgets, et attraper les erreurs a la compilation plutot qu a l execution. Les providers sont declares comme variables de haut niveau et vous les observez la ou vous en avez besoin.

    import 'package:flutter_riverpod/flutter_riverpod.dart';
    
    final counterProvider = StateProvider<int>((ref) => 0);
    
    class CounterText extends ConsumerWidget {
      const CounterText({super.key});
    
      @override
      Widget build(BuildContext context, WidgetRef ref) {
        final count = ref.watch(counterProvider);
        return Text('Compteur a ' + count.toString());
      }
    }

    Ce que j aime le plus, c est sa gestion de l async. Un FutureProvider vous donne les etats de chargement, d erreur et de donnees sans ecrire le boilerplate vous meme, ce qui se relie bien au travail reseau que je decris dans debuter avec Flutter. Pour la plupart des nouveaux projets c est mon choix par defaut.

    Bloc, quand la discipline compte

    Bloc est plus lourd et plus dogmatique. Vous modelisez votre application comme des evenements qui entrent et des etats qui sortent, et la separation stricte rend les grandes equipes previsibles. Chaque changement d etat est un evenement explicite avec un gestionnaire clair, ce qui rend l application facile a tracer et a tester. Le cout est la ceremonie. Les fonctionnalites simples demandent beaucoup de code.

    • Provider: peu de ceremonie, lie a l arbre, ideal pour les petites applications
    • Riverpod: testable, sur a la compilation, fort support async, mon defaut
    • Bloc: verbeux mais previsible, brille sur les grandes equipes et les flux complexes

    Comment je decide vraiment

    J adapte l outil a l equipe et a l application. Un projet solo ou un prototype recoit Provider ou Riverpod parce que je veux avancer vite. Une grande application avec de nombreux contributeurs et des regles metier complexes recoit Bloc parce que la structure se rentabilise en moins de surprises. La mauvaise decision est de choisir l outil le plus lourd pour le plus petit travail parce qu un article de blog vous a dit que c etait la meilleure pratique.

    Gardez l etat hors de la methode build

    Quoi que vous choisissiez, une regle tient pour toutes. Ne creez jamais et ne modifiez jamais l etat dans une methode build, car build peut s executer plusieurs fois par seconde et vous creerez des dechets ou declencherez des boucles. Gardez l etat au bon endroit, ecoutez le, et laissez le framework reconstruire. Bien faire cela garde aussi votre application fluide, ce qui rejoint l optimisation des performances Flutter. Reglez bien l etat et la plupart des autres problemes deviennent plus petits.

  • Bonnes pratiques de revue de code

    Bonnes pratiques de revue de code

    La revue de code est l’habitude au plus fort levier qu’une equipe puisse avoir, et c’est aussi celle qu’on rate le plus souvent. J’ai recu des revues qui ressemblaient a un interrogatoire et des revues qui approuvaient 800 lignes avec un pouce leve en quatre secondes. Les deux sont des echecs. Une bonne revue attrape de vrais problemes, diffuse la connaissance et laisse l’auteur soutenu plutot que juge.

    Relisez ce que les humains font bien

    Ne gaspillez pas votre attention sur le formatage, l’ordre des imports ou la longueur d’une ligne. Un linter et un formateur automatique s’en chargent, et ils ne se fatiguent jamais. Si votre equipe se dispute sur le style dans les commentaires de PR, vous avez un manque d’outillage, pas un probleme de discipline. Configurez le formateur, validez la config, et passez a autre chose.

    Ce qu’une machine ne peut pas verifier, c’est si le code est correct, s’il resout le vrai probleme, et si quelqu’un dans six mois le comprendra. C’est la que va mon attention :

    • Est-ce que ca fait ce que la description annonce ?
    • Que se passe-t-il aux bords : entree vide, valeurs nulles, acces concurrent, un appel reseau qui pend ?
    • Y a-t-il une approche plus simple cachee derriere celle-ci ?
    • Le nommage aura-t-il du sens pour quelqu’un qui n’etait pas dans la piece ?

    Gardez des changements assez petits pour etre vraiment relus

    La limite dure de la qualite de relecture, c’est la taille. La recherche et mon experience disent la meme chose : au-dela de quelques centaines de lignes, la detection de defauts s’effondre parce que les relecteurs survolent. Quand je recois une PR enorme, je demande a l’auteur de la decouper. Une serie de petites PR ciblees recoit un vrai examen sur chacune. C’est aussi pourquoi je rebase et ecrase avec soin avant d’ouvrir une PR, une habitude que j’ai decrite dans les bonnes pratiques Git.

    Commentez comme un collegue, pas comme un compilateur

    Le ton fait l’essentiel du travail. Le meme point passe completement differemment selon la formulation. Je pose des questions au lieu de rendre des verdicts, et je clarifie quels commentaires sont bloquants et lesquels sont optionnels.

    # Au lieu de :
    C'est faux.
    
    # Essayez :
    Que se passe-t-il ici si items est vide ? Je crois
    qu'on depasserait la fin. On pourrait proteger avec
    une verification de longueur, ou ce cas est-il
    impossible en amont ?
    
    # Et etiquetez les details :
    detail : on pourrait inliner, non bloquant

    Marquer les details comme non bloquants est une petite chose qui retire enormement de friction. L’auteur sait ce qu’il doit corriger pour fusionner par rapport a ce qui n’est que mon gout. Je prends aussi soin de dire quand quelque chose est vraiment bien. Une revue qui n’est que critique entraine les gens a redouter le processus.

    Relisez vite et finissez ce que vous commencez

    Une PR qui reste sans relecture deux jours bloque une personne et pourrit a mesure que main avance dessous. Je traite les demandes de relecture comme une priorite quasi en haut de ma file, idealement le jour meme. Le cout d’une PR a l’arret s’accumule : l’auteur change de contexte, puis doit tout recharger quand le retour arrive enfin.

    Quand je relis, j’essaie de donner tous mes retours en une passe plutot que de distiller des commentaires sur trois tours. Rien n’est plus demoralisant que tout corriger, etre relu, et decouvrir cinq nouveaux commentaires qui etaient visibles depuis le debut.

    L’auteur a aussi des devoirs

    Les revues vont plus vite quand l’auteur les rend faciles. Avant de demander une relecture, j’ecris une description qui explique ce qui a change et pourquoi, je laisse des commentaires en ligne sur mon propre diff pour pointer ce qui n’est pas evident, et je m’assure que la CI est au vert. Une bonne description peut diviser par deux le temps de relecture, car le relecteur ne reconstitue pas l’intention a partir du diff.

    • Enoncez le probleme, pas seulement la solution.
    • Signalez tout ce dont vous n’etes pas sur et sur quoi vous voulez un regard.
    • Incluez des captures ou un exemple de sortie pour tout ce qui touche l’utilisateur.

    Etre en desaccord, puis s’engager

    Parfois l’auteur et le relecteur voient simplement les choses autrement. Quand le desaccord porte sur le gout plutot que sur la justesse, je m’en remets a la decision de l’auteur apres avoir exprime mon avis une fois. Trainer une PR sur cinq tours pour une preference stylistique brule un capital de bonne volonte dont vous aurez besoin plus tard. Gardez les positions fermes pour ce qui compte vraiment : la justesse, la securite, et les contrats de donnees qui me tiennent tant a coeur dans le guide de conception d’API REST. Reussissez ceux-la et laissez filer les petites choses.

  • Comment mettre en place du CI/CD avec GitHub Actions

    Comment mettre en place du CI/CD avec GitHub Actions

    GitHub Actions a mauvaise réputation parce que les gens copient un énorme fichier YAML trouvé sur un blog, ça marche à moitié, et ils n’y retouchent plus jusqu’à ce que ça casse. Je veux vous montrer la version petite et compréhensible que j’utilise vraiment sur de vrais projets.

    Où vivent les workflows

    Chaque workflow est un fichier YAML dans .github/workflows. Le nom du répertoire n’est pas optionnel. Chaque fichier décrit un ou plusieurs jobs, et chaque job tourne sur une machine virtuelle neuve. Le modèle mental qui m’a le plus aidé : un job est un ordinateur portable propre qui démarre, fait exactement ce que vous lui dites, puis s’évapore. Rien ne persiste entre les jobs sauf si vous le sauvegardez explicitement.

    Un pipeline minimal mais réel

    Voici un workflow qui tourne à chaque push et chaque pull request. Il installe les dépendances, lance la suite de tests et construit le site. Remarquez comme le déclencheur, le runner et les étapes se traduisent en français simple :

    name: CI
    on:
      push:
        branches: [main]
      pull_request:
    
    jobs:
      build:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          - uses: actions/setup-node@v4
            with:
              node-version: 20
              cache: npm
          - run: npm ci
          - run: npm test
          - run: npm run build

    Deux choses ici valent leur place. La ligne cache: npm restaure le cache des dépendances, ce qui fait passer l’installation d’une minute à quelques secondes. Et npm ci au lieu de npm install respecte exactement votre lockfile, ce qui garantit que la CI installe les mêmes versions à chaque fois. La reproductibilité est tout l’enjeu.

    Faites tourner les jobs en parallèle quand c’est possible

    Si vos étapes de lint, de test et de vérification de types ne dépendent pas les unes des autres, ne les enchaînez pas. Séparez-les en jobs distincts et elles tourneront en même temps sur des machines différentes. Votre boucle de retour raccourcit, et le coût est le même puisque vous payez les minutes de calcul de toute façon. Je n’impose une séquence avec needs que lorsqu’un job ultérieur a vraiment besoin qu’un précédent se termine, comme le déploiement qui attend les tests.

    Ajouter le déploiement sans danger

    C’est là que je vois le plus d’erreurs. Le déploiement ne doit tourner que sur la branche main, jamais sur les pull requests, et il doit dépendre de la réussite des tests. La forme ressemble à ceci :

      deploy:
        needs: build
        if: github.ref == 'refs/heads/main'
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          - run: npm ci
          - run: npm run build
          - name: Publish
            run: npx wrangler pages deploy dist --project-name my-site

    Wrangler a besoin d’un jeton d’API Cloudflare pour publier. Stockez-le comme secret de dépôt chiffré dans Settings, Secrets and variables, et exposez-le à l’étape via le bloc d’environnement du job. Ne collez jamais un jeton dans le YAML lui-même, car le fichier reste dans votre historique pour toujours. Si vous déployez spécifiquement sur Cloudflare, la partie configuration manuelle est traitée dans le déploiement d’un site statique sur Cloudflare Pages.

    Les secrets, comme il faut

    Les secrets de dépôt sont chiffrés et seulement déchiffrés à l’exécution dans le job. Ils sont masqués dans les journaux, donc si un jeton s’affiche par accident, GitHub le caviarde. Référencez-les via le contexte des secrets dans le mappage d’environnement de votre workflow plutôt que de les afficher. La règle d’or : si cela permet à quelqu’un de déployer ou de dépenser de l’argent, c’est un secret, et il vit dans le coffre de GitHub, pas dans votre code.

    Du cache au-delà des dépendances

    Vous pouvez mettre en cache plus que node_modules. Les artefacts de build, les binaires compilés, les ressources téléchargées, tout ce qui est coûteux à recréer est candidat. L’étape actions/cache prend une clé, généralement un hachage d’un lockfile ou d’un répertoire source, et restaure le cache correspondant s’il existe. Trouvez la bonne clé et vos étapes lentes deviennent instantanées. Trompez-vous et vous publiez des artefacts périmés, alors incluez toujours le hachage du fichier pertinent dans la clé.

    Restez ennuyeux

    Mon conseil le plus fort est de résister à l’envie d’être malin. Un workflow que n’importe qui dans l’équipe peut lire en trente secondes vaut mieux qu’un workflow brillant que vous seul comprenez. Ajoutez des étapes quand vous avez une raison concrète, supprimez-les dès qu’elles ne servent plus, et figez les versions de vos actions pour qu’une mise à jour surprise ne casse jamais un déploiement du vendredi. Si votre pipeline publie aussi du contenu qui doit être consultable, vous pouvez y intégrer cette étape, ce qui se marie bien avec l’ajout d’une recherche plein texte à un site statique. Bien fait, le CI/CD s’efface en arrière-plan et garde simplement votre branche main déployable.

  • Écrire un allocateur mémoire simple

    Écrire un allocateur mémoire simple

    La première fois que j’ai écrit mon propre malloc, le concept de mémoire dynamique a cessé de ressembler à une boîte noire. Un allocateur n’est qu’un programme qui gère un gros bloc de mémoire et en distribue des morceaux à la demande. Les parties difficiles sont la comptabilité et la fragmentation, rien de mystique. Voici une version minimale.

    D’où vient la mémoire

    Votre allocateur a besoin de mémoire brute à découper. Sur Unix, vous l’obtenez du noyau avec sbrk, qui déplace le sommet du tas, ou avec mmap pour de plus grandes régions. sbrk est le plus simple à raisonner : appelez-le avec un nombre positif et le tas grandit, en retournant un pointeur vers le nouvel espace.

    void *region = sbrk(4096);   // demande une page au noyau
    if (region == (void *) -1) {
        // plus de mémoire
    }

    Tout le jeu consiste maintenant à prendre cette région et à la distribuer, en se souvenant des parties utilisées et des parties libres.

    Les en-têtes de bloc : l’astuce centrale

    L’idée fondamentale est de stocker des métadonnées juste avant chaque bloc distribué. Quand l’appelant demande N octets, vous réservez en réalité N octets plus la taille d’un petit en-tête. Vous retournez un pointeur situé après l’en-tête, donc l’appelant ne le voit jamais. Quand il appelle free avec ce pointeur, vous reculez pour retrouver l’en-tête.

    typedef struct block {
        size_t size;          // taille utile en octets
        int free;             // 1 si disponible, 0 si utilisé
        struct block *next;   // bloc suivant dans la liste
    } block_t;
    
    #define HEADER_SIZE sizeof(block_t)

    Je tiens une liste chaînée de ces en-têtes. Pour allouer, je parcours la liste à la recherche d’un bloc libre assez grand. C’est la stratégie first-fit : prendre le premier bloc qui convient. Le best-fit (le plus petit bloc qui rentre) gaspille moins d’espace mais est plus lent à chercher. Les deux conviennent pour apprendre.

    Le chemin d’allocation

    static block_t *head = NULL;
    
    void *my_malloc(size_t size) {
        block_t *cur = head;
        while (cur) {
            if (cur->free && cur->size >= size) {
                cur->free = 0;
                return (void *)(cur + 1);   // mémoire juste après l'en-tête
            }
            cur = cur->next;
        }
        // rien de libre, agrandir le tas
        block_t *blk = sbrk(HEADER_SIZE + size);
        if (blk == (void *) -1) return NULL;
        blk->size = size;
        blk->free = 0;
        blk->next = head;
        head = blk;
        return (void *)(blk + 1);
    }

    L’expression cur + 1 est de l’arithmétique de pointeurs sur un pointeur block_t, donc elle saute exactement après l’en-tête. Si cette ligne semble étrange, mon article sur comment fonctionnent vraiment les pointeurs explique pourquoi ajouter un déplace d’une structure entière, pas d’un octet.

    Libérer et le problème de la fragmentation

    Libérer est presque trop facile : trouver l’en-tête et basculer le drapeau free. La mémoire n’est pas rendue au noyau, elle est simplement marquée réutilisable par la prochaine allocation.

    void my_free(void *ptr) {
        if (!ptr) return;
        block_t *blk = (block_t *)ptr - 1;   // recule jusqu'à l'en-tête
        blk->free = 1;
    }

    Cette version naïve a un vrai défaut : la fragmentation. Libérez un bloc de 100 octets et un autre de 100 octets côte à côte, puis demandez 150 octets, et mon allocateur échoue alors même que 200 octets contigus sont libres. La correction est le coalescing : à la libération, vérifier si les blocs voisins sont aussi libres et les fusionner en un bloc plus grand. Les vrais allocateurs scindent aussi les blocs surdimensionnés pour qu’une demande de 16 octets ne consomme pas un morceau de 4 Ko.

    L’alignement, le détail qui mord plus tard

    Il y a un problème de correction que ma version jouet escamote : l’alignement. Le processeur attend qu’une valeur de huit octets se trouve à une adresse divisible par huit, et sur certaines architectures un accès mal aligné provoque une faute pure et simple. Un vrai allocateur arrondit chaque demande vers le haut pour que la zone utile commence toujours sur une frontière correctement alignée, généralement 16 octets sur un système 64 bits. Ma version marche par hasard parce que l’en-tête est déjà un multiple de l’alignement, mais dès que vous commencez à scinder des blocs, il faut arrondir les tailles, sinon vous rendez des adresses qui plantent sur certaines lectures. C’est le genre de bug qui se cache des mois puis ne surgit que sur une seule plateforme.

    Pourquoi ça vaut la peine de le construire

    • Vous cessez de craindre malloc et le voyez comme un composant réglable.
    • Vous comprenez pourquoi les schémas d’allocation comptent pour la performance, ce qui rejoint directement conception orientée données et caches du processeur.
    • Vous voyez exactement pourquoi le double-free et le use-after-free corrompent la mémoire : ils gribouillent sur ces en-têtes.

    Les allocateurs de production comme jemalloc et tcmalloc ajoutent des classes de taille, des caches par thread et des structures astucieuses, mais le squelette est celui que je viens de décrire. Connaître le squelette, c’est la différence entre utiliser la mémoire et la comprendre.

  • Cybersécurité pour développeurs : la checklist que j’utilise vraiment

    Cybersécurité pour développeurs : la checklist que j’utilise vraiment

    Les guides de sécurité pour développeurs échouent souvent de deux façons. Soit un mur de jargon de conformité que personne ne lit, soit une liste de mots effrayants sans instructions. Voici la checklist que je lance vraiment avant de livrer, écrite comme je l’expliquerais à un collègue.

    Authentification : arrêtez de réinventer la roue

    Si vous hachez encore des mots de passe à la main en 2026, arrêtez. Utilisez une bibliothèque qui fait de l’argon2id ou du bcrypt avec des réglages sains. Les façons de se tromper subtilement sont nombreuses, et aucune n’apparaît aux tests, car un hash faible connecte quand même l’utilisateur.

    Des sessions plutôt que des JWT pour la plupart des applications web. Une session côté serveur que vous pouvez révoquer vaut mieux qu’un jeton sans état que vous ne pouvez pas. Si vous utilisez des jetons, gardez-les à courte durée et prévoyez un vrai flux de rafraîchissement. Le confort du « je n’interroge jamais la base » devient un problème le jour où vous devez expulser quelqu’un immédiatement.

    L’autorisation, c’est là que vivent les vrais bugs

    L’authentification demande qui vous êtes. L’autorisation demande ce à quoi vous avez le droit de toucher, et c’est là que surviennent la plupart des fuites graves. Le grand classique : un endpoint lit l’identifiant utilisateur depuis le corps de la requête au lieu de la session, donc je modifie mon profil en envoyant votre identifiant. Ça s’appelle IDOR et c’est partout.

    Le correctif est une habitude, pas un outil. Chaque fois que vous chargez un enregistrement, demandez : « l’utilisateur courant en est-il propriétaire, et l’ai-je vérifié ? » Écrivez ce contrôle au niveau des données pour qu’il ne s’oublie pas dans un contrôleur. Le même soin vaut pour les fonctionnalités IA : un agent agissant au nom d’un utilisateur a besoin des permissions de cet utilisateur, pas du compte de service, un point que j’aborde dans l’IA agentique en cybersécurité.

    Toute entrée est hostile jusqu’à preuve du contraire

    L’injection SQL est ancienne et marche encore parce que quelqu’un, quelque part, construit toujours des requêtes par concaténation de chaînes. Utilisez des requêtes paramétrées. Toujours. Votre ORM le fait probablement pour vous, jusqu’au moment où vous passez en requête brute pour la performance et l’oubliez.

    Pour tout ce qui finit dans du HTML, l’échappement par défaut du framework est votre ami. Le danger, c’est l’instant où vous appelez la fonction « rendre ce HTML brut ». Chaque bug XSS que j’ai corrigé vivait à quelques lignes d’un de ces appels.

    Les secrets n’ont rien à faire dans le dépôt

    Clés API, mots de passe de base, secrets de signature : rien de tout ça ne va dans git, même un dépôt privé, même « temporairement ». Utilisez des variables d’environnement ou un gestionnaire de secrets. Ajoutez un scanner pre-commit pour qu’une version fatiguée de vous-même ne puisse pas en divulguer un à minuit.

    Et faites-les tourner quand quelqu’un part ou quand une clé traîne depuis un an. Un secret dont vous ne vous souvenez pas de la création est un secret à retirer.

    Les en-têtes que la plupart oublient

    Une poignée d’en-têtes de réponse HTTP achète beaucoup de sécurité pour presque aucun effort. Une Content-Security-Policy stricte est la plus importante ; pénible à régler et payante. Ajoutez HSTS pour que les navigateurs refusent le HTTP en clair, et posez des drapeaux de cookies sensés (HttpOnly, Secure, SameSite). Une demi-heure de travail qui ferme des catégories entières d’attaques.

    Les dépendances font partie de votre surface d’attaque

    L’essentiel de votre code n’est pas le vôtre. Lancez un audit de vos dépendances, activez les PR de mise à jour automatiques, et lisez-les vraiment au lieu de les valider d’un tampon. Un paquet compromis dans votre pipeline de build peut faire tout ce que votre build peut faire, c’est-à-dire beaucoup. C’est une raison pour laquelle je garde les frontières build/exécution propres, un sujet que j’aborde dans l’architecture full-stack moderne.

    Testez-la avant de la livrer

    Pointez un scanner sur votre propre application avant qu’un attaquant le fasse. Même gratuit, il attrapera les trous évidents. Associez ça à l’habitude de tester les chemins malheureux : que se passe-t-il avec un mauvais type, une charge énorme, l’identifiant d’un autre, un jeton manquant. Les bugs se cachent dans les cas que vous n’aviez pas prévus.

    Rien de tout cela n’est exotique. C’est les mêmes dix choses, faites à chaque fois, qui séparent les applications piratées de celles qui ne le sont pas. Pour aller plus loin dans la construction de systèmes sécurisés avec l’IA dans la boucle, les pratiques de l’ingénierie IA pratique sont la suite logique.

  • Sûreté mémoire avec l’ownership et l’emprunt de Rust

    Sûreté mémoire avec l’ownership et l’emprunt de Rust

    Après des années à courir après des pointeurs pendouillants et des double-frees en C, Rust m’a donné l’impression que quelqu’un avait enfin écrit les règles que je gardais en tête et les avait fait appliquer par le compilateur. Rust offre un contrôle manuel de la mémoire sans ramasse-miettes, et pourtant il empêche statiquement toute la classe de bugs mémoire qui ronge le C. Le mécanisme est l’ownership et l’emprunt, et c’est plus simple que sa réputation ne le laisse croire.

    Les trois règles de l’ownership

    Tout en Rust part de trois règles que le compilateur applique :

    • Chaque valeur a exactement un propriétaire, une seule variable responsable d’elle.
    • Il ne peut y avoir qu’un propriétaire à la fois.
    • Quand le propriétaire sort de portée, la valeur est détruite et sa mémoire libérée.

    Cette dernière règle est le génie discret. Il n’y a aucun free à appeler ni ramasse-miettes à exécuter. Le compilateur sait exactement où finit la portée de chaque valeur et insère le nettoyage pour vous. C’est la même durée de vie liée à la portée que celle de la pile, décrite dans pile contre tas : comment la mémoire fonctionne vraiment, sauf que Rust l’étend aussi aux données du tas.

    Des déplacements, pas des copies

    Parce qu’il n’y a qu’un propriétaire, affecter une valeur de tas à une autre variable déplace l’ownership au lieu de copier les données. L’ancienne variable devient invalide, et le compilateur rejettera toute utilisation de celle-ci.

    let s1 = String::from("bonjour");
    let s2 = s1;            // l'ownership passe de s1 à s2
    // println!("{}", s1);  // erreur de compilation : s1 a été déplacée
    println!("{}", s2);     // correct, s2 possède les données maintenant

    Cette seule règle élimine le double-free à la compilation. En C, deux pointeurs vers le même bloc de tas pensent chacun devoir le libérer. En Rust, une seule variable possède les données, donc elles sont libérées exactement une fois. Toute la catégorie de bug disparaît avant même que le programme tourne.

    Emprunter au lieu de déplacer

    Déplacer partout serait pénible, donc Rust vous laisse emprunter une valeur en prenant une référence. Une référence est un pointeur qui ne possède pas ce vers quoi il pointe. Le vérificateur d’emprunt applique un jeu de règles supplémentaire pour garder les références sûres :

    • Vous pouvez avoir autant de références immuables que vous voulez en même temps.
    • Ou exactement une référence mutable.
    • Mais jamais les deux à la fois.
    fn main() {
        let mut data = vec![1, 2, 3];
        let r1 = &data;        // emprunt immuable
        let r2 = &data;        // un autre, correct
        println!("{} {}", r1[0], r2[0]);
    
        let m = &mut data;     // emprunt mutable, autorisé maintenant que r1/r2 sont finis
        m.push(4);
    }

    Cette règle « un écrivain ou plusieurs lecteurs » est ce qui empêche les courses de données et le use-after-free. Vous ne pouvez pas tenir une référence dans un vecteur pendant qu’un autre bout de code le réorganise, car cela exigerait un emprunt mutable et un emprunt immuable simultanés. Le compilateur refuse de le construire.

    Les durées de vie rendent le pendouillant impossible

    Le vérificateur d’emprunt suit aussi combien de temps vit chaque référence et garantit qu’une référence ne survit jamais aux données qu’elle pointe. Le classique pointeur pendouillant du C, retourner une référence vers une locale, ne compile tout simplement pas.

    fn dangle() -> &String {
        let s = String::from("oups");
        &s    // erreur : s est détruite ici, la référence pendouillerait
    }

    Si vous avez lu comment fonctionnent vraiment les pointeurs, vous savez que c’est exactement le bug qui produit la corruption silencieuse en C. Rust le transforme en erreur de compilation avec un message clair.

    Ce qu’on abandonne et ce qu’on gagne

    Le coût est réel : le vérificateur d’emprunt rejette des programmes qui seraient en fait corrects, et vous passez du temps à restructurer le code pour le satisfaire. Cette courbe d’apprentissage est la fameuse phase de « lutte contre le borrow checker ». Ce que vous gagnez en retour, c’est une performance et un contrôle de niveau C sans aucune insécurité mémoire, vérifiés avant que le programme tourne. Après avoir vécu dans les deux mondes, je pense que le compromis vaut la peine pour tout ce où la correction compte. Rust n’a pas inventé ces règles. Il a juste fait du compilateur celui qui s’en souvient, pour que je n’aie pas à le faire.

  • Comment ajouter une recherche plein texte à un site statique

    Comment ajouter une recherche plein texte à un site statique

    La première objection que j’entends, c’est qu’un site statique ne peut pas avoir de recherche puisqu’il n’y a pas de serveur à interroger. C’est faux. Vous pouvez construire un index de recherche au moment de la génération du site et le publier comme un simple fichier, puis le parcourir entièrement dans le navigateur. Pour quelques milliers de documents au plus, c’est assez rapide pour que les utilisateurs croient à un backend.

    Pourquoi la recherche côté client fonctionne

    L’astuce est de déplacer le travail au moment du build. Pendant que mon générateur parcourt déjà chaque page pour produire le HTML, il est trivial d’y collecter aussi le titre, l’URL et le texte de chacune dans une liste. Cette liste devient un fichier JSON. Le navigateur le télécharge une fois, construit un index en mémoire, et chaque frappe ensuite est instantanée car rien ne quitte l’appareil. Pas de base de données, pas d’API, aucun coût par requête.

    Construisez l’index au moment de la génération

    Pendant le build, je retire les balises HTML de chaque page et j’ajoute un petit enregistrement à un tableau. Gardez le texte du corps réduit ; vous n’avez pas besoin de chaque mot, et un index plus léger se télécharge plus vite. Voici l’essentiel :

    const index = pages.map(page => ({
      title: page.title,
      url: page.url,
      excerpt: page.excerpt,
      body: page.text.slice(0, 2000)
    }));
    
    fs.writeFileSync('dist/search-index.json', JSON.stringify(index));

    Écrire ce fichier n’est qu’une étape de plus dans le même pipeline qui produit vos pages, donc cela s’insère naturellement dans un build que vous lancez peut-être déjà via le CI/CD avec GitHub Actions. L’index est publié à côté de votre HTML sur le même réseau de périphérie.

    Choisissez une bibliothèque, ou écrivez la version bête

    Pour les petits sites, vous pouvez vraiment écrire votre propre comparateur en une douzaine de lignes. Mettez tout en minuscules, découpez la requête en mots, et notez les documents selon le nombre de mots qu’ils contiennent. Ça marche. Mais dès que vous voulez la tolérance aux fautes, la correspondance par préfixe ou un classement par pertinence, prenez une bibliothèque. J’aime Fuse.js pour la correspondance approximative et MiniSearch quand je veux un vrai score plein texte. Les deux sont minuscules et tournent dans le navigateur sans étape de build.

    import MiniSearch from 'minisearch';
    
    const res = await fetch('/search-index.json');
    const docs = await res.json();
    
    const mini = new MiniSearch({
      fields: ['title', 'body'],
      storeFields: ['title', 'url', 'excerpt']
    });
    mini.addAll(docs);
    
    const results = mini.search('cloudflare deploy', { fuzzy: 0.2 });

    Reliez-le au champ de saisie

    Branchez un écouteur sur votre champ de recherche, mais ne lancez pas une recherche à chaque frappe. Temporisez de 150 millisecondes environ, sinon une personne qui tape vite déclenche une douzaine de recherches pour un seul mot. À chaque événement temporisé, lancez la requête, prenez les meilleurs résultats, et affichez-les comme une liste de liens. Montrez le titre et l’extrait pour que les gens sachent quel résultat ils veulent avant de cliquer.

    • Temporisez la saisie pour chercher sur une pause, pas à chaque lettre.
    • Limitez aux huit ou dix premiers résultats ; personne ne fait défiler un menu de recherche.
    • Surlignez le terme correspondant dans le résultat pour que la pertinence saute aux yeux.
    • Gérez explicitement l’état vide et l’état sans résultat.

    Chargez l’index paresseusement

    Ne téléchargez pas l’index de recherche au chargement de la page. La plupart des visiteurs ne cherchent jamais, donc payer ce coût d’avance est du gaspillage. Je récupère le JSON la première fois que quelqu’un place le curseur dans le champ de recherche, je le mets en cache dans une variable, et je le réutilise. La première recherche a un minuscule délai pendant l’arrivée du fichier, toutes les suivantes sont instantanées, et ceux qui ne cherchent jamais ne paient pas un octet. Cela garde votre chargement initial léger, ce qui compte pour les mêmes raisons qui m’obsèdent dans l’optimisation des images pour le web.

    Quand s’arrêter et passer à un service

    La recherche côté client a un plafond. Au-delà d’environ dix mille documents, l’index devient assez gros pour que le télécharger et l’analyser fasse mal, surtout sur téléphone. À cette échelle, je passe à un service de recherche hébergé qui expose une API. Mais honnêtement, la plupart des blogs et des sites de doc n’approchent jamais cette limite. Construisez l’index, publiez le JSON, cherchez dans le navigateur, et vous obtenez une fonctionnalité qui semble coûteuse pour presque rien et zéro serveur à maintenir.

  • Comment construire une API REST avec limitation de débit

    Comment construire une API REST avec limitation de débit

    Toute API exposée au public finira par se faire marteler, que ce soit par un client buggé qui boucle sur une erreur, un scraper, ou quelqu’un qui en abuse franchement. La limitation de débit est la façon de protéger votre service et de le garder équitable pour tout le monde. J’en ai ajouté à beaucoup d’API, et les concepts sont plus simples que le jargon le laisse croire.

    Choisissez un algorithme

    Il existe quelques approches classiques qui arbitrent entre précision et coût. La fenêtre fixe compte les requêtes par tranche de temps, disons 100 par minute, et se réinitialise à la frontière. C’est très simple mais cela autorise des rafales aux bords, puisqu’un client peut tirer 100 requêtes à la fin d’une minute et 100 au début de la suivante. La fenêtre glissante lisse cela en pondérant la fenêtre précédente. Le seau à jetons remplit des jetons à un rythme constant et laisse les clients les dépenser en rafales jusqu’à un plafond, ce qui colle le mieux au trafic réel.

    • Fenêtre fixe : la plus facile à construire, autorise des rafales aux bords.
    • Fenêtre glissante : plus précise, un peu plus de comptabilité.
    • Seau à jetons : gère les rafales avec souplesse, mon choix habituel.

    Un seau à jetons simple

    L’idée est que chaque client a un seau de jetons. Chaque requête coûte un jeton, et les jetons se rechargent avec le temps. Si le seau est vide, la requête est rejetée. Voici la logique de base, en ignorant le stockage un instant :

    function allow(bucket, now, rate, capacity) {
      const elapsed = (now - bucket.last) / 1000;
      bucket.tokens = Math.min(capacity, bucket.tokens + elapsed * rate);
      bucket.last = now;
      if (bucket.tokens >= 1) {
        bucket.tokens -= 1;
        return true;
      }
      return false;
    }

    Cette fonction recharge selon le temps écoulé, plafonne le seau pour que les jetons ne s’accumulent pas indéfiniment, et dépense un jeton par requête autorisée. C’est peut-être quinze lignes et cela couvre la grande majorité des besoins réels.

    Identifiez correctement le client

    La limitation de débit ne vaut que ce que vaut votre idée de ce qu’est un client. Pour le trafic authentifié, indexez la limite sur la clé d’API ou l’identifiant utilisateur, ce qui est fiable. Pour le trafic anonyme, vous vous rabattez sur l’adresse IP, qui est imparfaite car des utilisateurs derrière le même réseau partagent une IP et les proxys peuvent l’usurper. Si vous êtes derrière un proxy ou un CDN, lisez l’en-tête transmis, mais ne lui faites confiance que lorsque la requête est vraiment passée par votre infrastructure. Mal choisir la clé revient soit à punir des utilisateurs innocents, soit à ne pas arrêter les abuseurs.

    Répondez de la bonne façon

    Quand vous rejetez une requête, faites-le avec le bon statut HTTP et des en-têtes utiles pour que les clients bien élevés s’adaptent. Le statut est 429 Too Many Requests. Incluez toujours un en-tête Retry-After indiquant au client combien de temps attendre, et les en-têtes standard de limitation pour qu’il s’auto-régule avant de heurter le mur :

    HTTP/1.1 429 Too Many Requests
    Retry-After: 30
    RateLimit-Limit: 100
    RateLimit-Remaining: 0
    RateLimit-Reset: 30
    Content-Type: application/json
    
    { "error": "limite de debit depassee, reessayez dans 30 secondes" }

    Un bon client lit ces en-têtes et lève le pied poliment. Un mauvais les ignore, mais au moins vous lui avez laissé sa chance, et vous avez une trace propre de la raison de votre refus.

    Où stocker les compteurs

    Un compteur en mémoire fonctionne pour un seul serveur et s’effondre dès que vous passez à deux, car chaque instance a sa propre vue. Pour quoi que ce soit de distribué, il vous faut un état partagé. Redis est le choix classique ; il est rapide et possède des opérations d’incrément atomiques qui rendent cela facile. À la périphérie, un magasin clé-valeur comme le KV de Cloudflare ou les Durable Objects fait le même travail près de l’utilisateur. Quoi que vous choisissiez, la mise à jour du compteur doit être atomique, sinon deux requêtes simultanées peuvent lire le même compte et passer toutes les deux.

    Combinez-la avec d’autres défenses

    La limitation de débit est une couche, pas tout le mur. Associez-la à l’authentification, à la validation des entrées et à des délais d’attente raisonnables. Placez-la le plus tôt possible dans le cycle de vie de la requête, idéalement avant tout travail coûteux, pour qu’un flot de requêtes bloquées ne vous coûte presque rien. Si vous déployez ce genre de service vous-même, la même plateforme de périphérie que je décris dans le déploiement sur Cloudflare Pages peut faire tourner l’API et son magasin de limitation ensemble, et vous pouvez la publier en confiance via un pipeline GitHub Actions. Construisez le limiteur une fois, gardez-le ennuyeux, et il fait tranquillement son travail sous charge.

  • Les patterns de scalabilite backend qui tiennent vraiment sous la charge

    Les patterns de scalabilite backend qui tiennent vraiment sous la charge

    La plupart des backends ne s effondrent pas à cause d un seul gros problème. Ils s effondrent à cause d une douzaine de petites hypothèses qui tenaient très bien à mille requêtes par jour et qui cessent discrètement de tenir à un million. J ai passé une bonne partie de ma carrière à traquer ces hypothèses, souvent à trois heures du matin, et les patterns ci dessous sont ceux vers lesquels je reviens toujours. Aucun n est exotique. La compétence, c est de savoir lequel appliquer et quand s arrêter.

    Scaler verticalement avant de scaler horizontalement

    La première question que je pose quand un service souffre, c est de savoir si je peux simplement lui donner une machine plus grosse. La scalabilité verticale a mauvaise réputation parce qu elle a un plafond, mais ce plafond est bien plus haut qu on ne le croit. Une instance moderne avec 64 cœurs et 256 gigaoctets de mémoire encaisse une quantité énorme de trafic, et vous l obtenez sans toucher à votre code, à votre déploiement ni à votre modèle mental du système.

    La scalabilité horizontale, c est quand vous ajoutez des machines et répartissez le travail entre elles. Elle va plus loin, mais elle vous impose des décisions. L état doit vivre quelque part de partagé. Les requêtes doivent être routées. Les pannes se multiplient parce que vous avez désormais dix choses qui peuvent casser au lieu d une. Ma règle est simple. Je scale verticalement jusqu à ce que ça devienne cher ou que j atteigne le plafond de l instance, et seulement alors je scale horizontalement. Déployer une flotte de minuscules nœuds dès le premier jour, c est la meilleure façon de déboguer des problèmes de systèmes distribués que vous n aviez pas encore besoin d avoir.

    La statelessness rend tout le reste possible

    Vous ne pouvez pas répartir le trafic sur plusieurs serveurs si l un d eux est secrètement spécial. Dès qu une requête ne fonctionne que parce qu elle est tombée sur la même machine que la requête précédente, la scalabilité horizontale est morte. C est pour ça que je traite la statelessness comme une fondation plutôt qu une optimisation.

    En pratique, cela veut dire pas de données de session en mémoire locale, pas de fichiers uploadés posés sur le disque local, pas de compteurs en mémoire qui comptent. Poussez l état de session dans Redis ou dans un token signé. Poussez les fichiers dans du stockage objet. Poussez tout ce qui est durable dans une base de données. Quand chaque nœud applicatif est interchangeable, un répartiteur de charge peut envoyer une requête à n importe lequel, vous pouvez ajouter et retirer des nœuds librement, et un nœud planté ne vous coûte que les requêtes en cours qu il portait.

    Si vous voulez le contexte plus large de la place de tout ça dans un système, j en ai parlé dans l architecture fullstack moderne, où la statelessness réapparaît comme une condition pour des déploiements propres.

    La répartition de charge et comment une requête trouve un foyer

    Une fois que vous avez plusieurs nœuds interchangeables, quelque chose doit décider où va chaque requête. Un répartiteur de charge se place devant et distribue le trafic. Le round robin est le point de départ évident et il convient pour des charges uniformes, mais il devient vite bête quand le coût des requêtes varie. Least connections est généralement un meilleur choix par défaut parce qu il envoie le nouveau travail au nœud le moins occupé à l instant présent.

    Ce que les gens oublient, ce sont les health checks. Un répartiteur ne vaut que par sa capacité à repérer un nœud malade et à cesser de lui envoyer du trafic. Je configure toujours des health checks actifs avec un vrai endpoint qui touche les dépendances critiques, pas une route qui renvoie 200 quoi qu il arrive.

    upstream api_backend {
        least_conn;
        server 10.0.1.10:8080 max_fails=3 fail_timeout=30s;
        server 10.0.1.11:8080 max_fails=3 fail_timeout=30s;
        server 10.0.1.12:8080 max_fails=3 fail_timeout=30s;
    }
    
    server {
        listen 443 ssl;
        location / {
            proxy_pass http://api_backend;
            proxy_next_upstream error timeout http_502 http_503;
        }
        location /healthz {
            access_log off;
            proxy_pass http://api_backend;
        }
    }

    Le cache, le levier au plus fort rendement

    Rien ne vous offre de la marge plus vite que de ne pas faire le travail deux fois. Le cache est l outil au plus fort rendement de toute cette liste, et il opère à plusieurs niveaux. Il y a le CDN en bordure pour les ressources statiques et les réponses cacheables. Il y a un cache applicatif comme Redis ou Memcached pour les résultats calculés et les lignes chaudes. Il y a le cache de requêtes de la base et le cache de pages du système d exploitation en dessous. Chaque niveau depuis lequel vous pouvez servir est un niveau de travail que les niveaux inférieurs ne voient jamais.

    Le difficile n est jamais de lire depuis un cache. C est l invalidation. Un cache périmé est pire que pas de cache parce qu il ment avec aplomb. Je m appuie beaucoup sur l expiration par durée parce qu elle est prévisible, et je ne passe à l invalidation par événement que quand la péremption fait vraiment mal. Le cache-aside est mon pattern par défaut : on vérifie le cache, en cas de miss on va à la source, puis on réécrit le résultat avec un TTL.

    def get_user(user_id):
        key = "user:" + str(user_id)
        cached = redis.get(key)
        if cached is not None:
            return deserialize(cached)
        user = db.query("SELECT * FROM users WHERE id = %s", user_id)
        redis.setex(key, 300, serialize(user))
        return user

    Deux modes de défaillance méritent d être nommés. Une ruée sur le cache survient quand une clé populaire expire et que mille requêtes ratent en même temps et frappent la base ensemble. On la combat avec des TTL jitterés ou un court verrou pour qu une seule requête reconstruise la valeur. Le second, c est la croissance non bornée, que l on gère avec une politique d éviction comme LRU et un plafond mémoire.

    Les files de messages, ou comment arrêter le travail lent dans le chemin de la requête

    Une grande partie de ce qui rend les requêtes lentes n a pas besoin de se produire pendant que l utilisateur attend. Envoyer un email, redimensionner une image, générer un rapport, synchroniser avec un tiers. Si l appelant n a pas besoin du résultat tout de suite, sortez le du chemin de la requête et mettez le dans une file. Le tier web accepte le job, le confie à un broker comme RabbitMQ ou SQS, répond immédiatement, et un pool de workers avale le backlog à son propre rythme.

    Cela fait deux choses. Ça rend votre latence prévisible parce que la requête n attend plus le travail lent en aval. Et ça absorbe les pics, parce qu une file est un tampon. Quand le trafic triple pendant une heure, la file grossit et les workers rattrapent ensuite au lieu que tout le système fonde. Ce qu il faut absolument réussir, c est l idempotence. Les files redélivrent. Un message finira par être traité plus d une fois, donc chaque worker doit pouvoir tourner deux fois sur la même entrée sans danger.

    • Rendez les handlers idempotents avec une clé de déduplication ou un upsert pour qu une redélivrance soit sans effet.
    • Configurez une dead letter queue pour que les messages empoisonnés cessent de bloquer la file et atterrissent quelque part où vous pouvez les inspecter.
    • Surveillez la profondeur de la file comme une métrique de premier plan. Une file qui grossit est le tout premier signe que les workers ne suivent plus.

    Backpressure et dégradation gracieuse

    Scaler, ce n est pas seulement encaisser plus. C est échouer correctement quand on ne peut pas. Un système en vraie surcharge devrait délester volontairement plutôt que s effondrer. Je place des limites de débit aux bords, des timeouts sur chaque appel sortant, et des circuit breakers autour des dépendances instables pour qu un service aval lent n empile pas les threads et n emporte pas tout le processus avec lui.

    La dégradation gracieuse, c est décider à l avance de ce qu on lâche en premier. Si le service de recommandation est en panne, montrez une liste générique au lieu d une page d erreur. Si le cache est froid, servez des données un peu périmées plutôt que rien. Les utilisateurs qui ont une expérience dégradée sont bien plus contents que ceux qui reçoivent une 500.

    Où se place la base de données

    Tout ce qui précède achète du temps, mais finit par buter sur la base de données, qui est presque toujours le vrai goulot. Le cache réduit les lectures, les files lissent les écritures, mais passé un certain point la couche de données elle même doit scaler. C est un sujet assez vaste pour que je lui aie consacré un article dédié sur scaler les bases de données avec la réplication et le sharding, et si votre schéma vous résiste avant même d en arriver là, corrigez ça d abord avec une conception de schéma solide.

    L ordre dans lequel je les applique vraiment

    Si je devais compresser tout ça en une séquence, ce serait celle ci. Rendre le service stateless pour qu il puisse scaler tout court. Scaler verticalement jusqu à ce que ce ne soit plus rentable. Mettre un répartiteur de charge et plus de nœuds devant. Ajouter du cache aux niveaux qui font le plus mal. Déplacer le travail lent vers des files. Ajouter du backpressure pour que la surcharge dégrade au lieu de planter. Et ensuite, en général seulement, faire le travail plus dur de scaler la base de données. Mesurez à chaque étape, parce que le goulot est rarement là où votre intuition le situe, et ajouter de la capacité à la mauvaise couche ne fait que déplacer la file.