
Le jour où notre app s'est DDoS elle-même
Un backlog d'uploads en attente, relâché d'un seul coup au démarrage, a transformé chaque client GeekBye en une petite attaque par déni de service contre nos propres serveurs. Le correctif — et l'échelle de vérification de vitalité de la connexion qu'il nous a forcés à construire — est l'une des choses les plus utiles que la v2 nous ait apprises.
Tout ingénieur de systèmes distribués finit par rencontrer la ruée (la thundering herd) : une masse de clients faisant tous la même chose au même instant, et la ressource partagée derrière eux qui cède. D'habitude ce sont les clients de quelqu'un d'autre. Dans GeekBye v2.0.1 c'étaient les nôtres, et l'attaquant était notre propre app.
Comment un notetaker s'attaque lui-même
GeekBye peut uploader des enregistrements sur votre Google Drive. Si un enregistrement ne peut pas s'uploader tout de suite — vous étiez hors ligne, l'app s'est fermée, Drive a eu un hoquet — il part dans un backlog pour réessayer plus tard. Raisonnable.
La panne était dans le quand de ce « plus tard ». Au démarrage suivant, l'app tentait de vider tout le backlog d'un seul coup. Un utilisateur avec une semaine d'enregistrements en attente devenait une rafale de requêtes d'upload simultanées à l'instant même où il ouvrait l'app. Multipliez par chaque utilisateur qui démarre le matin, et on demandait à notre backend d'authentifier, de vérifier les limites et de traiter un mur de trafic dans les mêmes quelques secondes — de la part de clients qui, techniquement, se comportaient tous bien.
Notre backend a fait ce qu'il fallait et a limité le flot. Et c'est là qu'ont commencé les dégâts de second ordre. Une réponse de limite de débit est un HTTP 429, et un 429 ressemble beaucoup à d'autres échecs si on n'y prend pas garde :
- Le chargement du profil au démarrage recevait un
429et l'app le traitait comme un échec d'authentification — déconnectant brièvement les utilisateurs d'une session parfaitement valide. - La connexion de transcription recevait un
429générique et affichait une invite de mise à niveau pour limite audio atteinte — disant à des utilisateurs payants qu'ils avaient touché un quota qu'ils n'avaient pas atteint.
Ainsi une seule cause racine — un backlog non cadencé — produisait trois symptômes visibles : pression sur le serveur, déconnexions fausses et upsells faux. La forme classique d'un auto-DoS : la charge est mauvaise, mais ce que les utilisateurs ressentent vraiment, c'est la mauvaise interprétation des symptômes de cette charge.
Le correctif, en deux couches
Arrêter la ruée. Le backlog d'uploads est désormais cadencé — les requêtes s'étalent avec backoff, et il y a un temps de repos après tout 429 pour que le client s'écarte d'un serveur sollicité au lieu de s'appuyer plus fort dessus. Une ruée devient une file ordonnée.
Arrêter la mauvaise interprétation. Un 429 ou 5xx transitoire pendant le démarrage ne vous déconnecte plus — le client distingue « le serveur est brièvement occupé » de « votre session est invalide ». Et un 429 générique de limite de débit sur le chemin de transcription ne se fait plus passer pour une erreur de quota audio ; seule une vraie réponse de quota affiche l'invite de mise à niveau. La leçon qui est restée : un code d'erreur n'est pas un sens d'erreur. 429 signifie « ralentis », pas « déconnecte-toi » ni « tu es à court de quota » — et le client doit connaître la différence.
L'échelle de vitalité qui a suivi
Cadencer la ruée a exposé un problème plus discret : quand une connexion devenait vraiment mauvaise sous charge, à quelle vitesse le remarquions-nous ? Plus lentement qu'on ne le voulait. Alors la v2.0.4 a bâti une échelle de vérification de vitalité de la connexion pour la transcription en temps réel :
- Heartbeats — le socket de transcription est pingé à intervalle fixe, pour qu'une connexion morte en silence soit détectée en secondes au lieu d'attendre que le prochain fragment d'audio échoue.
- Un relancement de reconnexion au retour du réseau — quand le système d'exploitation signale que le réseau est revenu, l'app rétablit la connexion de façon proactive au lieu d'attendre de trébucher sur la découverte.
- Vitalité par ping/pong de contrôle — un aller-retour au niveau applicatif qui confirme non seulement que « le socket est ouvert » mais que « l'autre bout répond réellement ».
- Une vanne de reconnexion gérée par le service — un seul endroit décide s'il faut reconnecter, au lieu de plusieurs parties de l'app se disputant la tâche et se marchant dessus.
Rien de tout cela n'est glamour. Tout cela est la raison pour laquelle une session v2 donne l'impression de simplement... rester connectée.
C'est arrivé encore cette semaine — un cran plus bas
Voici la partie qui fait de ceci plus qu'une histoire de guerre. Cette même classe de panne a resurgi il y a quelques jours, un niveau en dessous de nous. Un seul client a déclenché 21 démarrages de session de transcription en moins de 400 millisecondes — et a fait sauter la limite à l'échelle du compte de notre fournisseur de reconnaissance vocale de 15 requêtes concurrentes. Une ruée contre une infrastructure partagée, exactement comme le backlog d'uploads, juste visant une dépendance au lieu de notre propre backend.
La forme est identique, et le correctif aussi : le client qui déferle a besoin d'un garde-fou single-flight pour ne pas pouvoir déclencher vingt démarrages pour une seule intention, et la ressource partagée a besoin d'un plafond par utilisateur pour qu'un client ne puisse pas consommer tout le pool. On a déjà construit la version côté client de ça — le cadenceur d'uploads est ce motif. Maintenant on l'applique aux démarrages de session. La ruée n'est pas un bug qu'on corrige une fois ; c'est une forme qu'on apprend à reconnaître partout où des clients rencontrent une limite partagée.
Trois choses à retenir
- Vos propres clients sont un test de charge que vous n'avez pas programmé. Tout ce qui groupe-au-démarrage, réessaie-au-lancement ou se-reconnecte-au-réveil est une ruée qui attend d'avoir assez d'utilisateurs. Cadencez-la avant de les avoir.
- Distinguez le code du sens.
429est le statut le plus mal lu qui existe. « Ralentis » n'est pas « déconnecte-toi » ni « paie-nous ». Aiguillez chaque échec vers ce qu'il signifie réellement. - La vitalité est une échelle, pas un flag. « La connexion est-elle vivante ? » a plusieurs réponses honnêtes à différentes couches — socket ouvert, octets qui circulent, l'autre bout qui répond, réseau présent. Une app robuste en vérifie plus d'une.
Voici la machinerie sans gloire sous le calme de la v2 de GeekBye. Pour les fonctionnalités de fiabilité qu'elle a rendues possibles, voyez pourquoi votre notetaker IA s'arrête sur un mauvais Wi-Fi et transcription en direct quand le pare-feu bloque les WebSockets (v2.0.8). Pour les versions voisines de cette série, pourquoi votre notetaker IA arrête d'enregistrer en pleine réunion (v2.0.9).