1 points par GN⁺ 2024-05-12 | 1 commentaires | Partager sur WhatsApp

Récapitulatif du processus de résolution d’une fuite mémoire liée à ActiveSupport::Notifications

  • Situation dans laquelle la fuite mémoire s’est produite

    • À partir d’un certain moment, l’utilisation mémoire du Dyno web a commencé à augmenter anormalement
    • Le pager s’est mis à sonner, laissant penser à une fuite mémoire
  • Réponse immédiate

    • Sur Heroku, lorsqu’une fuite mémoire est suspectée, redémarrer le Dyno permet une résolution temporaire
    • Redémarrer selon le cycle de déploiement habituel ou redémarrer manuellement les Dyno proches de la limite mémoire
  • Examen du code suspect pour identifier la cause

    • Revue des changements de code déployés juste avant le pic de mémoire
    • Déploiement un par un de plusieurs morceaux de code suspectés afin de vérifier si la fuite mémoire se reproduisait
    • Comme aucun code ne semblait en cause, les changements d’outillage ont aussi été annulés et vérifiés. Mais la fuite mémoire persistait
  • Analyse du schéma d’augmentation mémoire

    • La fuite ne se produisait que sur les Dyno web. Les Dyno Sidekiq et Delayed::Job étaient normaux
    • Tous les Dyno web ne fuyaient pas en permanence. Après quelques heures d’usage normal, une ou deux instances, ou parfois toutes, commençaient à fuir
    • Il a été suspecté qu’elle était déclenchée par un certain type de trafic plutôt que par le volume de trafic
    • Tous les workers Puma d’un Dyno n’étaient pas touchés : un petit nombre de workers utilisait l’essentiel de la mémoire totale
  • Collecte et analyse d’un heap dump

    • Utilisation de rbtrace pour collecter un heap dump du processus Ruby en cours de fuite
      • Connexion en SSH au dyno en fuite avec heroku ps:exec
      • Sélection, via la commande ps, du processus worker Ruby consommant le plus de mémoire
      • Attachement à ce pid avec rbtrace, puis démarrage du traçage des allocations mémoire (ObjectSpace.trace_object_allocations_start)
      • Collecte du heap dump avec ObjectSpace.dump_all. Compression en gzip si la taille est importante
      • Récupération locale du fichier dump avec heroku ps:copy
    • Utilisation de reap pour visualiser le heap dump sous forme de flamegraph
      • Découverte d’un Thread référençant 1,9 Go de mémoire et, en dessous, d’un Array référençant 32 067 objets
    • Utilisation de sheap pour explorer les objets suspects
      • Il s’est avéré que ce Thread était un worker thread de Puma
      • Un objet ActiveSupport::SubscriberQueueRegistry référençait un Hash, sous lequel se trouvaient des objets String et Array
      • Dans l’Array problématique, plus de 32 000 objets ActiveSupport::Notifications::Event s’étaient accumulés
  • Déduction sur la cause

    • Il a été supposé que des objets Event de ActiveSupport::Notifications s’accumulaient à tort dans le tableau #children
    • Si une erreur se produit dans le bloc ActiveSupport::Notifications.instrument, l’Event correspondant ne serait pas retiré de #children, provoquant ainsi la fuite mémoire
  • Reproduction en local

    • Envoi en local d’une requête avec le path et les paramètres suspects observés en production
    • Confirmation de la survenue de URI::InvalidURIError avec une 500 Internal Server Error
    • Confirmation que l’utilisation mémoire du dyno de production ayant reçu cette requête augmentait brutalement
  • Analyse détaillée de la cause

    • Il existait dans Rails 7.1 un bug corrigé lié à Event#children de ActiveSupport::Notifications
    • À cela s’ajoutait un bug du gem Bugsnag : lors du nettoyage de l’URL de requête, URI.parse levait URI::InvalidURIError, ce qui déclenchait la fuite mémoire
    • Comme l’erreur levée dans le bloc ActiveSupport::Notifications.subscribe n’était pas interceptée, l’Event concerné n’était pas retiré du tableau #children et continuait à s’y accumuler
  • Solution

    • Court terme : mettre à niveau la version du gem Bugsnag afin qu’il ne lève pas d’erreur même si URI::InvalidURIError se produit
    • Long terme : mettre à niveau vers Rails 7.x, où le bug de ActiveSupport::Notifications est corrigé

L’avis de GN⁺

  • Le processus de détection du problème puis d’identification méthodique de sa cause est impressionnant. L’article résume très bien les analyses de base à tenter lorsqu’on soupçonne une fuite mémoire
  • Il semble qu’un grand nombre d’outils open source pour collecter, visualiser et analyser les heap dumps Ruby (rbtrace, reap, sheap, etc.) soient activement développés. Même en dehors de Ruby, il est important de connaître les outils d’analyse mémoire utiles à chaque langage et de savoir les appliquer aux problèmes rencontrés
  • En pratique, la cause d’une fuite mémoire est souvent un bug dans une bibliothèque ou un framework utilisé, mais on n’a pas toujours les moyens d’analyser puis de corriger ce bug soi-même avant de déployer. Il est donc important d’appliquer au plus vite une solution de contournement. Fournir une alternative praticable avec un bug report est aussi une bonne approche
  • Il est appréciable que l’auteur ne se soit pas contenté de corriger la fuite mémoire, mais ait cherché en profondeur la root cause du problème. Cette rigueur d’analyse, qui consiste à examiner attentivement le code interne du framework pour remonter jusqu’à la cause fondamentale, est une qualité importante pour les développeurs
  • Au final, la cause de la fuite mémoire provenait d’une simple mise à jour de bibliothèque qui semblait au départ sans rapport. C’est un bon exemple de l’importance de la gestion des dépendances et du suivi des changements. Même une modification mineure mérite une analyse d’impact attentive et une surveillance après déploiement

1 commentaires

 
GN⁺ 2024-05-12
Commentaire Hacker News

Cela peut être résolu par une formation d’ingénierie, sans peur de la gestion manuelle de la mémoire

  • Avec seulement le RAII et des règles de propriété claires, la gestion mémoire est une tâche d’ingénierie simple
  • À l’inverse, les frameworks qui s’obstinent à utiliser le comptage de références et les pointeurs partagés rendent la propriété plus ambiguë et donc plus difficile
  • Allouer puis libérer, transférer puis ne plus s’en soucier, cela fait partie de la discipline d’ingénierie
  • Les bugs mémoire ne sont pas différents des bugs logiques, il est donc naturel de les corriger
  • Les ressources de l’OS (handles, sockets, etc.) sont aussi gérées manuellement sans gestionnaire automatique de ressources, donc on peut aborder la mémoire de la même façon

Cas d’une perte de 5 millions de dollars causée par une fuite mémoire

  • Présentation d’une anecdote sur un bug de fuite mémoire dans un pilote d’imprimante Solaris dans les années 90
  • À l’époque, dans les banques, on validait les transactions légalement en confirmant par fax, en imprimant le document, puis en le lisant à voix haute à l’interlocuteur au téléphone tout en enregistrant l’appel
  • À cause de la fuite mémoire, le pilote d’imprimante a planté, l’accusé de confirmation n’a pas été imprimé, la transaction a été annulée et cela a entraîné une perte de 5 millions de dollars
  • Finalement, c’est après les plaintes du CEO de Sun que les développeurs ont corrigé le bug

Outils de débogage des fuites mémoire et pistes de résolution

  • Avec Valgrind, on peut facilement trouver des fuites en C
  • Si la conception est correcte, l’allocation et la libération se trouvent généralement dans la même fonction, ce qui rend la correction facile
  • Présentation d’un cas de fuite mémoire sur un serveur publicitaire de Yahoo et d’une solution de contournement temporaire
  • Une citation humoristique du concepteur de PHP illustre une attitude plus pragmatique que perfectionniste
  • Dans Rails, il serait courant de résoudre ce type de problème avec du matériel pour privilégier la productivité

Éloge du style d’écriture

  • Un commentaire dit que la manière d’écrire de l’auteur est agréable, peut-être grâce aux émoticônes ou à la mise en forme