Une panne partielle de AdGuard DNS fin novembre 2025 : le démêlage

Le 25 novembre entre 15:30 et 18:00 UTC environ, AdGuard DNS a subi une panne majeure affectant des serveurs dans plusieurs emplacements, principalement en Europe. Pendant ce temps, certains utilisateurs d’AdGuard DNS ont observé une résolution DNS lente ou échouée, y compris les délais d’expiration et les réponses SERVFAIL.

Nous nous excusons pour l’incident et aimerions fournir une explication de ce qui s’est passé, comment nous avons résolu le problème et les changements que nous mettons en œuvre pour éviter que cela ne se reproduise.

TDLR

  • Impact. Une partie importante du trafic dans plusieurs villes européennes, principalement à Amsterdam, Francfort et Londres, a connu des pannes DNS intermittentes (timeouts et SERVFAIL) pendant 2,5 heures.
  • Cause principale. Un bogue dans la logique du cache de données utilisateur a provoqué un grand nombre de synchronisations complètes avec le service business-logic, ce qui, combiné à des paramètres de mémoire et de limite de connexion sous-optimaux, a conduit à l’épuisement des ressources sur plusieurs clusters DNS.
  • Atténuation. La version avec le bug a été restaurée, les caches corrompus ont été réparés, le trafic a été temporairement redirigé et les limites de connexion/mémoire ont été ajustées ; des mesures de protection et des tests supplémentaires sont en cours de mise en œuvre pour éviter des défaillances similaires.

Qu'est-ce qui c'est passé

Les résolveurs DNS AdGuard exigent que les paramètres de filtrage des utilisateurs soient disponibles localement avant qu’ils ne puissent appliquer des filtres. Au démarrage, le résolveur charge une copie en cache de toutes les données utilisateur d’un fichier local, puis interroge périodiquement le service business-logic uniquement pour des modifications incrémentielles depuis cet instantané. Si le cache local est périmé ou corrompu, le résolveur doit demander un ensemble de données complet au service logique d’entreprise, qui est lent et génère une charge considérablement plus élevée des deux côtés.

Le 25 novembre, une nouvelle version de AdGuard DNS a été déployée dans le but d’améliorer ce mécanisme. En raison d’un bogue, au lieu de sauvegarder l’ensemble complet des données utilisateur dans le fichier de cache local, le service n’a sauvegardé que le sous-ensemble récemment modifié, qui était souvent très petit ou même vide. En conséquence, de nombreux résolveurs ont démarré soit sans aucune donnée utilisateur, soit avec seulement une petite fraction de celle-ci, déclenchant des synchronisations complètes répétées.

La première tentative de déploiement a échoué à mi-chemin en raison d’un problème de réseau, donc environ la moitié de la flotte exécutait la nouvelle version du bogue tandis que le reste était toujours sur l’ancienne. Lorsque le déploiement a été relancé, un grand nombre de résolveurs ont simultanément découvert que leurs caches locaux étaient effectivement vides ou invalides et ont commencé à synchroniser complètement les données utilisateur. Cela a provoqué un pic de trafic vers le service logique d’entreprise et une augmentation de l’utilisation des ressources sur les résolveurs DNS eux-mêmes.

Amsterdam : surcharge du matériel déjà déséquilibré

À Amsterdam, l’impact de ce pic a été amplifié par un mauvais appariement matériel. Le cluster se composait de deux groupes de machines : certaines avec 32 GiB de RAM et d’autres avec 64 GiB, tandis que la configuration DNS AdGuard (limites de connexion, tailles de cache, paramètres de mémoire) avait été réglée pour le profil 64-GiB.

Sous la charge soudaine des synchronisations complètes et une tempête de clients DNS en cours d’essai, les 32 machines Gio se sont retrouvées à court de mémoire utilisable et ont atteint leurs limites internes beaucoup plus tôt. Ils se sont fortement dégradés et ont effectivement cessé de servir le trafic, ce qui a provoqué le déplacement de la majeure partie de la charge DNS d’Amsterdam vers les machines à 64 octets. Ces nœuds restants sont ensuite également entrés dans un état de surcharge : les limites de connexion ont été atteintes, les files d’attente ont augmenté, et les requêtes DNS ont commencé à se bloquer ou à échouer avec SERVFAIL, même si les résolveurs DNS amont étaient toujours sains et répondaient rapidement.

Tout en travaillant sur le retour en arrière de la version buggy et sur la réparation du cache, le trafic d’Amsterdam a été partiellement redirigé vers d’autres emplacements européens. Cela a aidé à stabiliser Amsterdam mais a introduit une lourde charge supplémentaire sur Francfort et Londres.

Francfort et Londres : limites de connexion, pipelining et latence

À Francfort et à Londres, le matériel sous-jacent était uniforme, mais les limites mal configurées et la haute concurrence ont encore entraîné une dégradation sévère. Deux effets étaient particulièrement importants :

  1. Limites de connexion et pipelining
    Le résolveur utilise une limite interne sur le nombre de connexions actives et prend en charge le pipelining des requêtes, permettant plusieurs requêtes simultanées par connexion. Avec une profondeur de pipeline de 5, chaque connexion entrante pourrait transporter plusieurs demandes en vol, chacune gérée par des goroutines et des tampons séparés. Sous une charge lourde, cela a entraîné un grand nombre de goroutines simultanées et d’objets vivants dans la mémoire, ce qui a augmenté la taille du tas et mis la pression sur le ramasse-miettes Go.

  2. Latence élevée et SERVFAIL malgré des flux ascendants sains
    À mesure que la pression sur la mémoire augmentait, la collecte de déchets devenait plus fréquente et plus coûteuse, augmentant le temps passé par chaque requête DNS dans le résolveur. Cela a entraîné une forte augmentation de la durée des demandes et du nombre de réponses SERVFAIL, car les délais d’attente côté client et interne ont été atteints avant que les réponses ne puissent être traitées, même si les serveurs DNS en amont répondaient toujours rapidement et avec succès.

À Francfort spécifiquement, l’utilisation du processeur n’est jamais apparue complètement saturée au niveau du système, mais l’activité du GC et la contention interne étaient suffisantes pour augmenter considérablement la latence de queue. Pendant cette période, le cluster a atteint sa limite de connexion et y est resté : les connexions existantes ont été maintenues plus longtemps que d’habitude, tandis que les nouveaux clients ont eu du mal à établir de nouvelles connexions. Les clients ont répondu avec des réessais, augmentant encore plus la charge et maintenant la limite de connexion saturée.

Lorsque la profondeur du pipeline de demande a été réduite de 5 à 1 à Francfort, le système s’est rétabli très rapidement. Avec pipeline=1, chaque connexion transporte au maximum une requête DNS en vol à la fois, ce qui réduit considérablement le nombre de goroutines et d’objets vivants simultanés dans la mémoire par connexion. Cela a à son tour réduit la croissance du tas et la pression de GC, permettant au résolveur de traiter les demandes plus rapidement, de fermer les connexions en temps opportun et de libérer des créneaux de connexion. En conséquence, la limite de connexion n’était plus constamment saturée, la durée de la demande a chuté et le taux de réponses SERVFAIL est revenu à des niveaux normaux.

Problèmes clés : mémoire, limite de connexion et nœuds 32 vs 64 Gio

Un facteur important dans le comportement des différents nœuds était l'interaction entre la mémoire RAM disponible, le recycleur de mémoire de Go et le paramètre connlimit. Сonnlimit contrôle la quantité maximale de mémoire que le runtime Go utilisera pour le tas géré avant de déclencher de manière agressive le recyclage de mémoire. Sur les nœuds à forte mémoire RAM, si connlimit est réglé trop haut, le runtime permet au tas de grossir beaucoup plus avant que le GC ne devienne agressif.

Sur les machines de 64 Gio, la limite de mémoire effective était plus élevée, de sorte que dans des conditions de forte concurrence et de pipelining élevé, le processus de résolution accumulait un grand tas avec de nombreuses goroutines et tampons. Une fois que ce tas a atteint la limite configurée, le GC a dû travailler très dur pour garder l'utilisation de la mémoire sous contrôle, ce qui a entraîné des périodes prolongées d'utilisation élevée du CPU par le GC et des pauses plus fréquentes. Il s'agit d'un scénario catastrophe connu pour connlimit sur les applications à forte mémoire : la mémoire n'est pas épuisée, mais le temps CPU est de plus en plus consacré au GC, ce qui entraîne une latence plus élevée et un débit réduit.

Sur les machines de 32 Gio, la même configuration et la même charge ont conduit le processus à atteindre plus tôt les limites pratiques de la mémoire. Ces nœuds ont commencé à échouer plus tôt car leur configuration (tailles de cache, limites de connexion) n'avait pas été réduite pour correspondre à l'empreinte RAM plus petite, mais les nœuds de 64 Gio ont finalement été ralentis par l'activité GC. Combiné à un pipelining élevé et à un trafic de réessais important, cela a créé une boucle de rétroaction dans laquelle la pression du GC, la saturation des connexions et les réessais des clients se renforçaient mutuellement.

Chronologie des événements

  • 14h30 UTC : Le premier déploiement de la nouvelle version commence. En raison d'un problème réseau, le déploiement échoue environ une heure plus tard, laissant environ la moitié des serveurs fonctionner avec la nouvelle version boguée.

  • 15h30-16h00 UTC : La deuxième tentative de déploiement commence. Des alertes sont déclenchées en raison de l'augmentation des taux d'erreurs DNS et de la charge des serveurs. L'enquête identifie rapidement le bug dans la logique de mise en cache qui oblige les résolveurs à fonctionner avec des données utilisateur locales vides ou incomplètes et à effectuer des synchronisations complètes.

  • 16h00-16h30 UTC : la décision est prise de revenir simultanément à la version non boguée sur tous les serveurs. Sous la charge combinée des synchronisations complètes et des nouvelles tentatives des clients, le cluster d'Amsterdam, qui utilise un mélange de matériel 32 Gio/64 Gio, est surchargé ; les nœuds 32 Gio sont les premiers à tomber en panne, ce qui augmente le trafic sur les nœuds 64 Gio, qui commencent alors eux aussi à tomber en panne. Le trafic est partiellement redirigé d'Amsterdam vers d'autres sites européens, ce qui augmente la charge sur Francfort et Londres.

  • 16h30-17h30 UTC : Des modifications de configuration sont appliquées afin de réduire la concurrence effective des connexions et la profondeur du pipeline à Francfort et à Londres, et d'ajuster les limites à la capacité matérielle réelle. Ces modifications ramènent progressivement les clusters à un état stable. En parallèle, les fichiers cache corrompus et les instantanés des données utilisateur sont réparés afin d'éviter de nouvelles synchronisations complètes.

  • 17h30-18h00 UTC : la restauration est terminée, les caches sont rétablis dans un état cohérent, la distribution du trafic est normalisée et les taux d'erreur reviennent à leur niveau de référence.

Pourquoi les requêtes ont échoué (SERVFAIL et latence élevée)

Pendant la panne, les utilisateurs ont principalement constaté des délais d'attente DNS et des réponses SERVFAIL. Il est important de souligner que les fournisseurs DNS en amont fonctionnaient normalement et renvoyaient des réponses valides en temps opportun. Les pannes ont été causées par l'incapacité de la couche de résolution à traiter et à renvoyer les réponses avant l'expiration des délais d'attente internes ou côté client.

Plusieurs facteurs ont contribué à cette situation :

  1. La saturation des limites de connexion et la profondeur élevée du pipelining ont entraîné la mise en file d'attente ou le retard de nombreuses requêtes DNS en cours.
  2. L'augmentation de la taille du tas et de la pression GC a entraîné des temps de traitement des réponses plus longs et des pauses occasionnelles, en particulier sur les nœuds 64 GiB.
  3. Les clients et les résolveurs récursifs ont réessayé les requêtes ayant échoué, ce qui a encore augmenté la charge et maintenu le système proche de ses limites.

Mesures de suivi

Pour éviter que des incidents similaires ne se reproduisent à l'avenir, les mesures suivantes seront prises :

  1. Des améliorations du code et des tests
    1. Ajouter des tests automatisés pour vérifier que l'ensemble des données utilisateur est correctement sérialisé dans le fichier cache local et restauré à partir de celui-ci, y compris la détection des caches vides ou tronqués.
    2. Introduire des tests d'intégration qui simulent le démarrage du résolveur avec un cache vide ou corrompu sous une charge réaliste, afin de s'assurer que le système se comporte correctement et ne surcharge pas les services de logique métier.
  2. La configuration et réglage des limites
    1. Standardiser les profils matériels des serveurs par emplacement afin d'éviter les clusters mixtes avec des tailles de RAM très différentes, ou maintenir explicitement des configurations distinctes par classe de matériel.
    2. Examiner et ajuster les limites de connexion, la taille des caches et les limites de connexion pour chaque type de nœud standard afin que les nœuds 32 Gio et 64 Gio (ou leurs équivalents futurs) fonctionnent dans des limites de GC et de mémoire sûres.
    3. Réévaluer et réduire la profondeur du pipelining si nécessaire afin de maintenir la concurrence par connexion à un niveau qui ne produit pas de goroutine excessive et de croissance excessive du tas dans des conditions de pointe.
    4. Réévaluer notre approche en matière de limitation des connexions et des goroutines afin de permettre à la fois des limitations raisonnables de la consommation des ressources et un débit correct, même après une rupture de connexion.
  3. La surveillance et les alertes
    1. Élargir la surveillance afin de suivre non seulement le CPU et la mémoire globale, mais également la taille du tas Go, le temps GC, les cycles GC par seconde et les distributions de latence des requêtes par cluster.
    2. Ajouter des alertes dédiées pour les augmentations soutenues du taux SERVFAIL et de la durée des requêtes en présence d'en amonts sains, afin de détecter les conflits internes et les problèmes liés au GC avant qu'ils n'entraînent une interruption visible pour l'utilisateur.
  4. Les procédures opérationnelles
    1. Affiner les procédures de déploiement et de restauration des résolveurs DNS afin d'éviter l'invalidation simultanée du cache sur une grande partie du parc.
    2. Documenter et répéter les scénarios permettant d'ajuster rapidement la profondeur du pipeline, les limites de connexion et le connlimit lors d'incidents en direct, sur la base du comportement observé à Amsterdam et à Francfort.
Vous avez aimé cet article ?