Blog

Comment un seul chat chargé pouvait faire tomber notre API

Le 28 septembre, environ 17 minutes après une mise en production, nos deux serveurs API de production ont manqué de mémoire au même instant, ont redémarré, et pendant environ cinq minutes le répartiteur de charge n'a eu aucun serveur sain vers lequel envoyer le trafic. Notre première réponse a été de doubler la mémoire de l'API. La vraie cause est apparue plus tard dans la journée : ouvrir un chat particulier demandait à l'API de charger 53 Mo de messages en une seule requête. L'historique des chats se charge désormais par pages limitées à la fois par taille et par nombre, et ce chat s'ouvre avec une première page de 5 Mo au lieu de planter.

Ce qui est tombé

Notre API tourne en conteneur sur chacun de deux serveurs, derrière un répartiteur de charge. Chaque conteneur avait une limite de mémoire de 384 MiB. Au repos, il utilise 130 à 140 MiB, donc la limite semblait laisser une grande marge.

Les deux conteneurs ont atteint cette limite ensemble et ont planté avec « JavaScript heap out of memory ». Le tas (heap) est la mémoire qu'un processus Node.js utilise pour les objets sur lesquels il travaille. Comme les deux serveurs ont échoué en même temps, il n'y avait plus nulle part où router les requêtes. C'est le pire type de panne pour une configuration à deux serveurs : la redondance n'aide que si les pannes ne surviennent pas simultanément.

La première correction : plus de marge

Le calendrier pointait vers le déploiement. Après une mise en production, les tours d'agent interrompus par le déploiement sont repris et leurs transcriptions sont enregistrées à nouveau, et cette rafale de travail semblait être la source évidente du pic de mémoire.

La première modification a donc porté la limite de l'API de 384 MiB à 768 MiB. Nous l'avons appliquée à chaud sur les deux serveurs le jour même, puis validée dans la configuration de déploiement pour que le prochain déploiement ne remette pas 384 en place. Une correction faite à la main sur un serveur, que le déploiement suivant annule, est une façon courante de voir la même panne revenir.

Plus de mémoire était une marge de sécurité raisonnable. Ce n'était pas une explication.

La vraie cause : une requête, 71 Mo

Plus tard ce jour-là, nous avons reproduit le plantage sur une copie d'un chat, avec l'ancienne limite de mémoire de la production. La copie a fait planter l'API à elle seule, sans aucun déploiement.

Lorsque vous ouvrez un projet, l'application web demande à l'API l'historique du chat. L'endpoint renvoyait les 30 derniers messages, entiers. Pour la plupart des chats, 30 messages restent petits. Pour celui-ci, les 30 derniers messages pesaient 53 Mo dans la base de données et une réponse de 71 Mo. Plusieurs réponses de l'agent faisaient entre 4 et 10 Mo chacune, surtout les transcriptions des sous-agents : les agents auxiliaires que l'agent principal lance pour travailler en parallèle sur une partie de la tâche, dont le travail complet est enregistré avec la réponse.

Construire cette seule réponse prenait plus de tas que l'API n'en avait. Et chaque ouverture ou rafraîchissement de ce projet renvoyait la requête. Un seul chat pouvait faire tomber le serveur qui le recevait, et comme la page pouvait être rechargée, il pouvait toucher les deux serveurs.

« 30 messages » n'avait jamais été une vraie limite de taille. C'était une limite de nombre, qui se trouvait être petite pour les chats normaux.

La correction : des pages limitées par taille et par nombre

Une page d'historique compte désormais 30 messages ou environ 4 Mo de messages, selon ce qui arrive en premier.

Cela fonctionne en deux étapes :

  1. Demander les tailles d'abord. L'API demande à la base de données les identifiants et les tailles stockées des derniers messages, sans leur contenu. Cette requête est peu coûteuse, quelle que soit la taille des messages.
  2. Ne charger que ce qui tient. En remontant du plus récent au plus ancien, elle ajoute des messages jusqu'à ce que le suivant fasse dépasser 4 Mo à la page. Seules ces lignes sont ensuite chargées en entier.

Deux règles assurent la sécurité :

  • Le message le plus récent figure toujours sur la page, quelle que soit sa taille. Une page vide laisserait le chat bloqué, sans moyen de remonter dans l'historique.
  • Rien n'est coupé. Aucun message n'est tronqué ni supprimé. Le reste du chat se charge via la commande « Scroll up for earlier messages », déjà utilisée pour les anciens messages, une page à la fois.

Nous avons trouvé un autre endroit de même nature. Lorsque vous arrêtez un tour, l'API vérifie les dernières réponses pour voir si certaines sont vides et doivent être nettoyées. Elle le faisait en chargeant huit messages entiers. Sur un chat contenant des réponses de plusieurs mégaoctets, c'est le même problème sous une forme plus petite. Elle pose maintenant à la base de données une question par oui ou par non (ce message a-t-il du contenu ?) sans charger le contenu lui-même.

Comment nous l'avons vérifié

Sur la copie du chat, avec l'ancienne limite de 384 MiB :

  • La première page fait 5 Mo. Avant, la même requête faisait planter l'API.
  • Cinq onglets du navigateur ouvrant le chat en même temps atteignent un pic de 154 Mo de mémoire.
  • Le défilement arrière sur tout le chat renvoie les 190 messages, chacun une seule fois, dans l'ordre.

La logique de choix des pages a aussi des tests unitaires, dont un construit à partir des tailles réelles des derniers messages de ce chat. La première page contient un message, indique qu'il en reste d'autres, et la page suivante commence au message qui suit.

Ce que nous en retenons

  • Un compte n'est pas une limite de taille. Tout endpoint qui renvoie « les N derniers » d'une chose pouvant croître sans limite a besoin d'un budget en octets. Les données finiront par trouver la faille.
  • Le suspect évident n'est pas toujours la cause. Le déploiement était un vrai déclencheur de travail supplémentaire, et plus de mémoire était utile. Mais le plantage s'est reproduit sans déploiement, et sans cette reproduction nous nous serions arrêtés à l'augmentation de mémoire.
  • Deux serveurs tombent ensemble quand ils reçoivent la même requête. La redondance protège contre la panne d'un serveur. Elle ne protège pas contre une requête qui fait planter tout serveur qu'elle atteint, surtout lorsqu'un utilisateur la relance naturellement en rafraîchissant la page.
  • Corrigez dans la configuration, pas seulement sur la machine. La hausse de mémoire a été inscrite dans la configuration de déploiement le jour même, pour que le déploiement suivant la conserve.

C'est notre deuxième histoire récente de mémoire liée à la sortie de l'agent. La première concernait une étiquette de statut qui mettait en file 690 Mo de chaînes. Si vous utilisez des sous-agents sur Mythex, vous pouvez lire comment ils fonctionnent dans la documentation.

Mythex · Pricing · Créez tout avec l'IA · Modèles de sites et d'applications · Mythex comparé · Glossaire de la création d'applications avec l'IA · Outils gratuits pour créer des applications avec l'IA · English

English አማርኛ العربية Български বাংলা Bosanski Català Čeština Dansk Deutsch Ελληνικά Español Español (España) Eesti فارسی Suomi Français (Canada) ગુજરાતી हिन्दी Hrvatski Magyar Հայերեն Bahasa Indonesia Íslenska Italiano 日本語 ქართული Қазақ тілі ಕನ್ನಡ 한국어 Lietuvių Latviešu Македонски മലയാളം Монгол मराठी Bahasa Melayu မြန်မာ Norsk bokmål Nederlands ਪੰਜਾਬੀ Polski Português (Brasil) Português (Portugal) Română Русский Slovenčina Slovenščina Soomaali Shqip Srpski Svenska Kiswahili தமிழ் తెలుగు ไทย Tagalog Türkçe Українська اردو Tiếng Việt 简体中文 繁體中文(香港) 繁體中文(台灣)