Blogue

Des alertes qui se déclenchent une seule fois, et non une fois par serveur

L'une de nos alertes opérationnelles, « crédits faibles » pour l'application publiée d'un créateur, doit nous parvenir au plus une fois par jour. Un après-midi, elle est arrivée cinq fois. La cause : chaque copie de notre API retenait en mémoire les alertes qu'elle avait déjà envoyées. Deux serveurs en envoyaient donc une chacun, et chaque déploiement effaçait cette mémoire et permettait de les renvoyer. Le même schéma faisait aussi exécuter deux fois un travail en arrière-plan. Les deux se coordonnent maintenant par un magasin partagé, et les deux échouent du côté sûr si ce magasin est indisponible.

Le symptôme

Mythex envoie à son équipe des alertes lorsqu'il faut jeter un coup d'œil : un service est en panne, une tâche est bloquée, ou le solde de crédits d'un créateur baisse pendant que son application est publiée. Chaque alerte a une fenêtre de déduplication, c'est-à-dire une période pendant laquelle la même alerte n'est envoyée qu'une fois. Pour l'alerte de crédits faibles, cette fenêtre est de 24 heures.

Cinq copies de la même alerte dans un seul après-midi signifiaient que la fenêtre ne tenait pas. Pire encore, elles arrivaient par paires, à environ 11 minutes d'intervalle.

Pourquoi c'est arrivé

Notre paquet d'alertes gardait sa déduplication en mémoire : une table « clé d'alerte → moment de son dernier envoi », à l'intérieur du processus en cours. Cela fonctionne lorsqu'il n'y a qu'un seul processus qui ne redémarre jamais. La production n'a ni l'un ni l'autre :

  • Deux réplicas de l'API. La production fait tourner deux copies de l'API. Chacune avait sa propre table, donc chacune envoyait l'alerte une fois. Ce sont les paires.
  • Les déploiements. Chaque déploiement remplace les deux réplicas par des processus neufs et des tables vides. Après un déploiement, l'alerte repartait, comme si elle n'avait jamais été envoyée.

L'intervalle d'environ 11 minutes entre les deux alertes de chaque paire venait de la tâche qui déclenche cette alerte. Elle tourne sur les deux réplicas et prend un verrou, de sorte que les réplicas l'obtiennent à tour de rôle, et chacun déclenchait l'alerte à son tour.

Il y avait un second bogue, plus discret, dans le même code. Pour éviter que la table grossisse indéfiniment, elle se nettoyait elle-même dès qu'elle contenait 500 clés, en supprimant chaque entrée vieille de plus de 15 minutes, quelle que soit sa fenêtre. Sous charge, une déduplication de 24 heures pouvait ne durer qu'un quart d'heure.

La correction

Un enregistrement partagé par alerte

Avant d'envoyer une alerte, l'API réserve maintenant la clé de déduplication de l'alerte dans Redis, un magasin en mémoire partagé auquel les deux réplicas se connectent et qui ne redémarre pas quand l'API redémarre. La réservation est une seule opération : « définis cette clé seulement si elle n'existe pas, et fais-la expirer après la fenêtre ». Le réplica qui réserve en premier envoie l'alerte; l'autre trouve la clé prise et reste silencieux. Un nouveau processus après un déploiement retrouve toujours la clé.

La clé et la fenêtre viennent de la même règle que le paquet d'alertes, désormais partagée entre les deux couches, de sorte qu'elles ne peuvent pas être en désaccord sur ce qui compte comme « la même alerte ».

Échouer ouvert, pas fermé

La décision la plus importante a été de savoir quoi faire lorsque Redis est injoignable. Nous avons choisi d'envoyer quand même l'alerte. Si Redis est en panne, ou ne répond pas dans un délai de 1,5 seconde, l'alerte part. Un doublon est du bruit. Une alerte perdue, c'est quelqu'un qui n'est pas prévenu.

Les alertes les plus graves, P0, ne sont jamais retenues par la déduplication.

Chaque entrée garde sa propre fenêtre

Dans la déduplication en mémoire du paquet d'alertes, chaque entrée enregistre maintenant le moment où sa fenêtre se termine, et le nettoyage ne supprime que les entrées dont la fenêtre est échue. Une clé de 24 heures reste 24 heures, quel que soit le nombre d'autres alertes qui arrivent.

Le même bogue, ailleurs

Le même schéma avait aussi doublé un travail en arrière-plan : une tâche de réconciliation qui garde notre registre de l'état de chaque espace de travail aligné sur celui de notre fournisseur de bacs à sable. Chaque passage liste chaque bac à sable du compte, des milliers en production, en environ cinq secondes, ainsi que chaque projet en marche ou en pause. Les deux réplicas l'exécutaient toutes les deux minutes.

Ses écritures sont idempotentes, c'est-à-dire que les exécuter deux fois donne le même résultat qu'une seule fois, donc rien ne cassait. Il s'agissait simplement de deux fois plus de travail. Maintenant, chaque fenêtre de deux minutes est réservée dans Redis de la même façon que nos compteurs d'utilisation réservaient déjà les leurs, et seul le réplica qui obtient la réservation exécute le passage. Si Redis est injoignable, le passage s'exécute quand même : faire le travail deux fois est sans danger, ne pas le faire ne l'est pas.

Comment nous l'avons testé

Les tests reproduisent la configuration réelle. Ils chargent le module d'alertes deux fois, comme deux « réplicas » distincts, les pointent vers un même magasin partagé, déclenchent la même alerte depuis chacun, puis chargent une troisième copie neuve pour représenter « après un déploiement », et la déclenchent de nouveau. Une seule alerte est envoyée. D'autres tests vérifient qu'une alerte d'un autre créateur part toujours, qu'une P0 est envoyée depuis les deux réplicas, et que l'alerte part encore lorsque le magasin est en panne ou dépasse le délai. La correction du nettoyage a son propre test : une clé de 24 heures survit à 600 autres alertes arrivant 20 minutes plus tard.

Pour la tâche de réconciliation, deux réplicas qui se déclenchent en même temps, plus un troisième déclenchement dans la même fenêtre, ne produisent qu'un seul listage.

Ce que nous en retenons

  • L'état en mémoire appartient à un processus. Tout ce qui doit être vrai entre les serveurs, « envoyé une fois » ou « exécuté une fois », doit vivre quelque part qu'ils partagent.
  • Un déploiement est un redémarrage. Un état qui ne survit que jusqu'au prochain redémarrage ne survit pas à une journée de travail normale.
  • Décidez dans quel sens échouer. Pour les alertes, un magasin partagé en panne doit signifier plus d'alertes, pas moins.
  • Cherchez le schéma, pas seulement le cas. L'alerte était le cas bruyant; la tâche de réconciliation doublée était le même bogue, qui coûtait discrètement du travail.

Rien de tout cela n'est visible depuis l'intérieur de Mythex. Pour en savoir plus sur la façon dont nous gardons la plateforme stable, consultez comment nous avons ramené les déploiements à zéro erreur 502 et le fichier d'échange manquant. Si l'idée de faire tourner plusieurs copies d'un service est nouvelle pour vous, notre guide sur ce qu'est un serveur couvre les bases.

Mythex · Pricing · Créez n'importe quoi avec l'IA · Modèles de sites web 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 ગુજરાતી हिन्दी 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 简体中文 繁體中文(香港) 繁體中文(台灣)