1 Punkte von GN⁺ 2024-05-12 | 1 Kommentare | Auf WhatsApp teilen

Zusammenfassung des Lösungsprozesses für einen Memory Leak im Zusammenhang mit ActiveSupport::Notifications

  • Situation, in der der Memory Leak auftrat

    • Ab einem bestimmten Zeitpunkt begann die Speichernutzung des web-Dynos ungewöhnlich stark anzusteigen
    • Der Pager schlug Alarm, und es entstand eine Situation, die wie ein Memory Leak aussah
  • Sofortige Reaktion

    • Wenn auf Heroku ein Memory Leak vermutet wird, kann ein Neustart des Dynos vorübergehend Abhilfe schaffen
    • Neustart passend zum normalen Deploy-Zyklus oder manuelles Neustarten von Dynos, die sich dem Memory-Limit nähern
  • Überprüfung verdächtiger Änderungen zur Ursachenfindung

    • Prüfung der direkt vor dem Memory Spike deployten Codeänderungen
    • Einige verdächtige Codeabschnitte wurden nacheinander deployt, um zu prüfen, ob der Memory Leak auftritt
    • Da kein eindeutiger Verursacher im Code zu finden war, wurden auch Änderungen an den Tools zurückgerollt. Der Memory Leak blieb jedoch bestehen
  • Analyse des Musters des Speicheranstiegs

    • Der Leak trat nur auf web-Dynos auf. Sidekiq- und Delayed::Job-Dynos verhielten sich normal
    • Nicht alle web-Dynos leakten ständig. Nach einigen Stunden normaler Nutzung begann der Leak bei ein oder zwei oder auch allen Dynos
    • Es entstand der Verdacht, dass nicht das Traffic-Volumen, sondern bestimmter Traffic der Auslöser ist
    • Nicht alle Puma-Worker innerhalb eines Dynos leakten; stattdessen verbrauchten wenige Worker den Großteil des gesamten Speichers
  • Sammlung und Analyse von Heap Dumps

    • Mit rbtrace wurden Heap Dumps von Ruby-Prozessen gesammelt, bei denen der Leak gerade auftrat
      • Per heroku ps:exec SSH-Verbindung zu einem leakkenden Dyno aufgebaut
      • Mit dem Befehl ps den Ruby-Worker-Prozess mit dem höchsten Speicherverbrauch ausgewählt
      • Mit rbtrace an die betreffende PID angehängt und die Verfolgung der Speicherallokationen gestartet (ObjectSpace.trace_object_allocations_start)
      • Mit ObjectSpace.dump_all einen Heap Dump gesammelt, bei großer Dateigröße per gzip komprimiert
      • Mit heroku ps:copy die Dump-Datei lokal heruntergeladen
    • Mit reap wurde der Heap Dump als Flamegraph visualisiert
      • Es wurde ein Thread gefunden, der 1,9 GB Speicher referenzierte, und darunter ein Array, das 32.067 Objekte referenzierte
    • Mit sheap wurden verdächtige Objekte untersucht
      • Es stellte sich heraus, dass der betreffende Thread ein Worker-Thread von Puma war
      • Ein Objekt vom Typ ActiveSupport::SubscriberQueueRegistry referenzierte ein Hash, darunter befanden sich String- und Array-Objekte
      • In dem problematischen Array hatten sich mehr als 32.000 Objekte vom Typ ActiveSupport::Notifications::Event angesammelt
  • Herleitung der Ursache

    • Es wurde vermutet, dass Event-Objekte von ActiveSupport::Notifications fälschlich im Array #children angesammelt werden
    • Wenn innerhalb des Blocks von ActiveSupport::Notifications.instrument ein Fehler auftritt, wird das betreffende Event offenbar nicht aus #children entfernt und verursacht so vermutlich den Memory Leak
  • Lokale Reproduktion

    • Lokal wurde mit dem in der Produktion gefundenen verdächtigen Request-Pfad und den entsprechenden Parametern eine Anfrage gesendet
    • Dabei trat zusammen mit 500 Internal Server Error ein URI::InvalidURIError auf
    • Es wurde bestätigt, dass die Speichernutzung des Production-Dynos, der diesen Request verarbeitet hatte, sprunghaft anstieg
  • Analyse der konkreten Ursache

    • In Rails 7.1 gab es einen behobenen Bug im Zusammenhang mit Event#children in ActiveSupport::Notifications
    • Dazu kam ein Bug im Bugsnag-Gem, bei dem beim Bereinigen der Request-URL während URI.parse ein URI::InvalidURIError ausgelöst wurde, was zusammen den Memory Leak verursachte
    • Da ein im Block von ActiveSupport::Notifications.subscribe ausgelöster Fehler nicht abgefangen wurde, wurde das betreffende Event nicht aus dem Array #children entfernt und sammelte sich weiter an, was zum Memory Leak führte
  • Lösungsansatz

    • Kurzfristig: Upgrade des Bugsnag-Gems auf eine Version, die bei URI::InvalidURIError keinen Fehler mehr auslöst
    • Langfristig: Upgrade auf Rails 7.x, in dem der Bug in ActiveSupport::Notifications behoben ist

Meinung von GN⁺

  • Beeindruckend ist vor allem, wie das Problem entdeckt und die Ursache systematisch eingegrenzt wurde. Der Beitrag fasst einen sinnvollen grundlegenden Analyseprozess zusammen, den man bei Verdacht auf einen Memory Leak ausprobieren kann
  • Für das Sammeln, Visualisieren und Analysieren von Heap Dumps in Ruby scheint es eine Reihe aktiv entwickelter Open-Source-Tools zu geben (rbtrace, reap, sheap usw.). Auch außerhalb von Ruby ist es wichtig, nützliche Memory-Analyse-Tools je nach Sprache zu kennen und in Problemen anwenden zu können
  • Tatsächlich liegen die Ursachen von Memory Leaks oft in Bugs bestimmter Bibliotheken oder Frameworks. Da man solche Bugs jedoch nicht immer direkt selbst analysieren und fixen sowie deployen kann, ist es wichtig, möglichst schnell praktikable Workarounds anzuwenden. Sinnvoll ist auch, zusammen mit einem Bug Report mögliche Alternativen bereitzustellen
  • Positiv ist außerdem, dass es nicht beim Beheben des Memory Leaks blieb, sondern tief in die Root Cause eingestiegen wurde. Diese analytische Haltung, internen Framework-Code sorgfältig zu untersuchen und bis zur eigentlichen Ursache vorzudringen, wirkt für Entwickler sehr wertvoll
  • Letztlich lag die Ursache des Memory Leaks in einem auf den ersten Blick völlig unbedeutenden Library-Versionsupgrade. Das zeigt, wie wichtig Dependency-Management und die Nachverfolgung von Änderungen sind. Selbst kleine Änderungen sollten hinsichtlich ihrer Auswirkungen sorgfältig analysiert und nach dem Deploy weiter überwacht werden

1 Kommentare

 
GN⁺ 2024-05-12
Hacker-News-Kommentar

Lässt sich ohne Angst vor manueller Speicherverwaltung durch technische Disziplin lösen

  • Mit RAII und klaren Ownership-Regeln ist Speicherverwaltung eine einfache technische Aufgabe
  • Frameworks, die stattdessen auf Referenzzählung und Shared Pointers beharren, machen Ownership eher unklar und dadurch schwieriger
  • Wer etwas erzeugt, gibt es wieder frei; wer es weitergibt, muss sich nicht mehr darum kümmern — das gehört zur technischen Disziplin
  • Speicherfehler unterscheiden sich letztlich nicht von Logikfehlern, also ist es selbstverständlich, sie zu beheben
  • Auch OS-Ressourcen (Handles, Sockets usw.) werden ohne automatische Ressourcenmanager manuell verwaltet; an Speicher kann man daher genauso herangehen

Beispiel für einen Verlust von 5 Millionen Dollar durch ein Memory Leak

  • Anekdote über einen Memory-Leak-Bug in einem Solaris-Druckertreiber aus den 90ern
  • Damals wurden in Banken Transaktionen per Fax bestätigt, ausgedruckt und dem Gegenüber am Telefon vorgelesen und aufgezeichnet, um eine rechtliche Bestätigung zu erhalten
  • Wegen des Memory Leaks stürzte der Druckertreiber ab, die Bestätigung wurde nicht ausgedruckt, die Transaktion wurde storniert und es entstand ein Verlust von 5 Millionen Dollar
  • Am Ende beheben die Entwickler den Bug, nachdem sich der Sun-CEO beschwert hatte

Debugging-Tools und Lösungsansätze für Memory Leaks

  • Mit Valgrind lassen sich Leaks in C leicht finden
  • Wenn das Design sauber ist, passieren Allokation und Freigabe meist in derselben Funktion, sodass sich das Problem leicht beheben lässt
  • Beispiel eines Memory Leaks in einem Yahoo-Werbeserver und einer pragmatischen Übergangslösung
  • Ein scherzhaftes Zitat des PHP-Designers zeigt eine Haltung, die eher Pragmatismus als Perfektionismus wählt
  • Bei Rails ist es offenbar üblich, Produktivitätsprobleme lieber mit Hardware zu lösen

Lob für den Schreibstil

  • Kommentar, dass die Schreibweise des Autors — vielleicht wegen der Emoticons oder des Formattings — angenehm zu lesen ist