Drei Arten von Speicherlecks

Hallo Kollegen.

Unsere lange Suche nach zeitlos meistverkauften Büchern zur Codeoptimierung hat nur zu ersten Ergebnissen geführt. Wir sind jedoch bereit, Ihnen zu gefallen, dass die Übersetzung von Ben Watsons legendärem Buch " Writing High Performance .NET Code " buchstäblich gerade abgeschlossen wurde. In den Läden - vorläufig im April, achten Sie auf Werbung.

Und heute bieten wir Ihnen an, einen rein praktischen Artikel über die dringendsten Arten von Speicherlecks zu lesen, der von Nelson Ilheidzhe (Strike) geschrieben wurde.

Sie haben also ein Programm, dessen Fertigstellung umso länger dauert, je länger es dauert. Wahrscheinlich wird es für Sie nicht schwer zu verstehen sein, dass dies ein sicheres Zeichen für einen Speicherverlust ist.
Was genau meinen wir jedoch mit „Speicherverlust“? Nach meiner Erfahrung werden explizite Speicherverluste in drei Hauptkategorien unterteilt, von denen jede durch ein spezielles Verhalten gekennzeichnet ist. Zum Debuggen jeder Kategorie werden spezielle Tools und Techniken benötigt. In diesem Artikel möchte ich alle drei Klassen beschreiben und vorschlagen, wie man mit richtig erkennt
mit welcher Klasse Sie es zu tun haben und wie Sie ein Leck finden.

Typ (1): Nicht erreichbares Speicherfragment zugewiesen

Dies ist ein klassischer Speicherverlust in C / C ++. Jemand hat Speicher mit new oder malloc zugewiesen und nicht free oder delete aufgerufen, free Speicher malloc , nachdem die Arbeit damit beendet wurde.

 void leak_memory() { char *leaked = malloc(4096); use_a_buffer(leaked); /* ,   free() */ } 

So stellen Sie fest, ob ein Leck zu dieser Kategorie gehört

  • Wenn Sie in C oder C ++ schreiben, insbesondere in C ++, ohne dass häufig intelligente Zeiger zur Steuerung der Lebensdauer von Speichersegmenten verwendet werden, ist dies die Option, die wir zuerst in Betracht ziehen.
  • Wenn das Programm in einer Umgebung mit Garbage Collection ausgeführt wird, ist es möglich, dass ein Leck dieses Typs durch eine native Code-Erweiterung hervorgerufen wird. Leckagen der Typen (2) und (3) müssen jedoch zuerst beseitigt werden.

Wie man ein solches Leck findet

  • Verwenden Sie ASAN . Verwenden Sie ASAN. Verwenden Sie ASAN.
  • Verwenden Sie einen anderen Detektor. Ich habe Valgrind- oder tcmalloc-Tools für die Arbeit mit einer Reihe von Tools ausprobiert. Es gibt auch andere Tools in anderen Umgebungen.
  • Einige Speicherzuordnungen ermöglichen das Speichern des Heap-Profils, in dem alle nicht zugewiesenen Speicherbereiche angezeigt werden. Wenn Sie ein Leck haben, fließen nach einiger Zeit fast alle aktiven Entladungen daraus, sodass es wahrscheinlich nicht schwierig ist, es zu finden.
  • Wenn alles andere fehlschlägt, sichern Sie einen Speicherauszug und überprüfen Sie ihn so sorgfältig wie möglich . Aber sollte auf keinen Fall damit beginnen.

Typ (2): ungeplante langlebige Speicherzuordnungen

Solche Situationen sind keine „Lecks“ im klassischen Sinne des Wortes, da eine Verbindung von irgendwo zu diesem Speicherstück immer noch erhalten bleibt, sodass sie am Ende freigegeben werden kann (wenn das Programm es schafft, dorthin zu gelangen, ohne den gesamten Speicher zu verbrauchen).
Situationen in dieser Kategorie können aus vielen spezifischen Gründen auftreten. Die häufigsten sind:

  • Unbeabsichtigte Anhäufung von Staat in einer globalen Struktur; Beispielsweise schreibt der HTTP-Server jedes empfangene Request in die globale Liste.
  • Caches ohne durchdachte Veralterungspolitik. Beispiel: Ein ORM-Cache, der jedes einzelne geladene Objekt zwischenspeichert, das während der Migration aktiv ist und in dem alle in der Tabelle vorhandenen Datensätze ausnahmslos geladen werden.
  • Ein zu voluminöser Zustand wird in der Schaltung erfasst. Dieser Fall tritt besonders häufig in Java Script auf, kann aber auch in anderen Umgebungen auftreten.
  • Im weiteren Sinne die unbeabsichtigte Beibehaltung jedes Elements eines Arrays oder Streams, während angenommen wurde, dass diese Elemente beim Online-Streaming verarbeitet werden.

So stellen Sie fest, ob ein Leck zu dieser Kategorie gehört

  • Wenn das Programm in einer Umgebung mit Garbage Collection ausgeführt wird, ist dies die Option, die wir zuerst in Betracht ziehen.
  • Vergleichen Sie die in der Garbage Collector-Statistik angezeigte Heap-Größe mit der Größe des vom Betriebssystem generierten freien Speichers. Wenn ein Leck in diese Kategorie fällt, sind die Zahlen vergleichbar und folgen vor allem im Laufe der Zeit aufeinander.

Wie man ein solches Leck findet

Verwenden Sie die in Ihrer Umgebung verfügbaren Profiler oder Heap-Dump-Tools. Ich weiß, dass es in Python Guppy oder in Ruby memory_profiler gibt , und ich habe ObjectSpace auch direkt in Ruby geschrieben.

Typ (3): freier, aber nicht verwendeter oder unbrauchbarer Speicher

Diese Kategorie ist am schwierigsten zu charakterisieren, aber es ist genau am wichtigsten, sie zu verstehen und zu berücksichtigen.

Leckagen dieses Typs treten in der Grauzone zwischen dem Speicher, der aus Sicht des Allokators in der VM oder der Laufzeitumgebung als "frei" betrachtet wird, und dem Speicher auf, der aus Sicht des Betriebssystems "frei" ist. Der häufigste (aber nicht der einzige) Grund für dieses Phänomen ist die Haufenfragmentierung . Einige Allokatoren nehmen einfach Speicher und geben ihn nicht an das Betriebssystem zurück, nachdem er zugewiesen wurde.

Ein Fall dieser Art kann anhand eines Beispiels eines in Python geschriebenen Kurzprogramms betrachtet werden:

 import sys from guppy import hpy hp = hpy() def rss(): return 4096 * int(open('/proc/self/stat').read().split(' ')[23]) def gcsize(): return hp.heap().size rss0, gc0 = (rss(), gcsize()) buf = [bytearray(1024) for i in range(200*1024)] print("start rss={} gcsize={}".format(rss()-rss0, gcsize()-gc0)) buf = buf[::2] print("end rss={} gcsize={}".format(rss()-rss0, gcsize()-gc0)) 

Wir weisen 200.000 1-kb-Puffer zu und speichern dann jeden weiteren. Jede Sekunde zeigen wir den Speicherstatus aus Sicht des Betriebssystems und aus Sicht unseres eigenen Python-Garbage-Collectors an.

Auf meinem Laptop bekomme ich so etwas:

start rss=232222720 gcsize=11667592
end rss=232222720 gcsize=5769520


Wir können sicherstellen, dass Python tatsächlich die Hälfte der Puffer freigegeben hat, da der gcsize-Pegel fast um die Hälfte vom Spitzenwert abfiel, aber kein Byte dieses Speichers an das Betriebssystem zurückgeben konnte. Der freigegebene Speicher bleibt für denselben Python-Prozess zugänglich, jedoch nicht für andere Prozesse auf diesem Computer.

Solche freien, aber nicht verwendeten Erinnerungsfragmente können sowohl problematisch als auch harmlos sein. Wenn ein Python-Programm so handelt und dann eine Handvoll 1-KB-Fragmente zuweist, wird dieser Speicherplatz einfach wiederverwendet, und alles ist in Ordnung.

Wenn wir dies jedoch während der Ersteinrichtung tun und anschließend den Speicher auf ein Minimum beschränken oder wenn alle anschließend zugewiesenen Fragmente jeweils 1,5 KB groß sind und nicht in diese im Voraus verbleibenden Puffer passen, bleibt der gesamte auf diese Weise zugewiesene Speicher immer inaktiv wäre verschwendet.

Probleme dieser Art sind besonders relevant in einer bestimmten Umgebung, nämlich in Multiprozess-Serversystemen für die Arbeit mit Sprachen wie Ruby oder Python.

Nehmen wir an, wir richten ein System ein, in dem:

  • Auf jedem Server werden N Single-Threaded-Worker verwendet, um Anforderungen kompetent zu bearbeiten. Nehmen wir zur Genauigkeit N = 10.
  • In der Regel verfügt jeder Mitarbeiter über eine nahezu konstante Speicherkapazität. Nehmen wir für die Genauigkeit 500 MB.
  • Mit einer geringen Häufigkeit erhalten wir Anforderungen, die viel mehr Speicher benötigen als die mittlere Anforderung. Nehmen wir aus Gründen der Genauigkeit an, dass wir einmal pro Minute eine Anforderung erhalten, deren Ausführungszeit zusätzlich 1 GB zusätzlichen Speicher erfordert. Wenn die Anforderung verarbeitet wird, wird dieser Speicher freigegeben.

Einmal pro Minute kommt eine solche "Cetacean" -Anforderung an, deren Verarbeitung wir einem der 10 Arbeiter anvertrauen, zum Beispiel zufällig: ~random . Idealerweise sollte dieser Mitarbeiter während der Verarbeitung dieser Anforderung 1 GB RAM zuweisen und diesen Speicher nach Beendigung der Arbeit an das Betriebssystem zurückgeben, damit er später wiederverwendet werden kann. Um Anfragen nach diesem Prinzip unbegrenzt zu verarbeiten, benötigt der Server nur 10 * 500 MB + 1 GB = 6 GB RAM.

Nehmen wir jedoch an, dass die virtuelle Maschine aufgrund von Fragmentierung oder aus einem anderen Grund diesen Speicher niemals an das Betriebssystem zurückgeben kann. Das heißt, die vom Betriebssystem benötigte RAM-Größe entspricht der größten Speichermenge, die Sie jemals gleichzeitig zuweisen müssen. In diesem Fall schwillt der Bereich, der von einem solchen Prozess im Speicher belegt wird, für immer um ein ganzes Gigabyte an, wenn ein bestimmter Mitarbeiter eine solche ressourcenintensive Anforderung bearbeitet.

Wenn Sie den Server starten, sehen Sie, dass 10 * 500 MB = 5 GB verwendet werden. Sobald die erste große Anfrage eintrifft, belegt der erste Mitarbeiter 1 GB Speicher und gibt ihn nicht zurück. Die Gesamtspeichermenge springt auf 6 GB. Die folgenden eingehenden Anforderungen werden gelegentlich an den Prozess weitergeleitet, der zuvor den „Wal“ verarbeitet hat. In diesem Fall ändert sich die verwendete Speichermenge nicht. Aber manchmal wird eine so große Anfrage an einen anderen Mitarbeiter gesendet, wodurch der Speicher um weitere 1 GB aufgeblasen wird, und so weiter, bis jeder Mitarbeiter eine so große Anfrage mindestens einmal bearbeiten kann. In diesem Fall benötigen Sie mit diesen Vorgängen bis zu 10 * (500 MB + 1 GB) = 15 GB RAM, was viel mehr als die idealen 6 GB ist! Wenn Sie sich außerdem ansehen, wie die Serverflotte im Laufe der Zeit verwendet wird, können Sie sehen, wie die Menge des verwendeten Speichers allmählich von 5 GB auf 15 GB ansteigt, was sehr an ein "echtes" Leck erinnert.

So stellen Sie fest, ob ein Leck zu dieser Kategorie gehört

  • Vergleichen Sie die in der Garbage Collector-Statistik angezeigte Heap-Größe mit der Größe des vom Betriebssystem generierten freien Speichers. Wenn das Leck zu dieser (dritten) Kategorie gehört, weichen die Zahlen mit der Zeit voneinander ab.
  • Ich möchte meine Anwendungsserver so konfigurieren, dass diese beiden Zahlen in meiner Zeitreiheninfrastruktur regelmäßig abwehren, sodass es bequem ist, Diagramme darauf anzuzeigen.
  • Zeigen Sie unter Linux den Betriebssystemstatus in Feld 24 von /proc/self/stat an und zeigen Sie den Speicherzuweiser über eine sprach- oder virtuelle Maschinenspezifische API an.

Wie man ein solches Leck findet

Wie bereits erwähnt, ist diese Kategorie etwas heimtückischer als die vorherigen, da das Problem häufig auftritt, selbst wenn alle Komponenten „wie beabsichtigt“ funktionieren. Es gibt jedoch eine Reihe nützlicher Tricks, mit denen die Auswirkungen solcher „virtuellen Lecks“ gemindert oder verringert werden können:

  • Starten Sie Ihre Prozesse häufiger neu. Wenn das Problem langsam auftritt, ist es möglicherweise nicht schwierig, alle Anwendungsprozesse alle 15 Minuten oder einmal pro Stunde neu zu starten.
  • Noch radikaler: Sie können allen Prozessen beibringen, unabhängig voneinander neu zu starten, sobald der Speicherplatz im Speicher einen bestimmten Schwellenwert überschreitet oder um einen vorgegebenen Wert wächst. Versuchen Sie jedoch vorauszusehen, dass Ihre gesamte Serverflotte keinen spontanen synchronen Neustart starten kann.
  • Ändern Sie die Speicherzuordnung. Auf lange Sicht handhaben tcmalloc und jemalloc die Fragmentierung normalerweise viel besser als der Standardzuweiser, und das Experimentieren mit ihnen ist mit der Variablen LD_PRELOAD sehr praktisch.
  • Finden Sie heraus, ob Sie einzelne Abfragen haben, die viel mehr Speicher belegen als die anderen. Bei Stripe messen unsere API-Server RSS (konstanter Speicherverbrauch) vor und nach der Bearbeitung jeder API-Anforderung und protokollieren das Delta. Anschließend fragen wir unsere Protokollaggregationssysteme einfach ab, um festzustellen, ob es solche Terminals und Benutzer (und Muster) gibt, mit denen Bursts des Speicherverbrauchs abgeschrieben werden können.
  • Passen Sie den Garbage Collector / Memory Allokator an. Viele von ihnen verfügen über anpassbare Parameter, mit denen Sie festlegen können, wie aktiv ein solcher Mechanismus Speicher an das Betriebssystem zurückgibt und wie optimiert er ist, um Fragmentierung zu beseitigen. Es gibt andere nützliche Optionen. Alles hier ist auch ziemlich kompliziert: Stellen Sie sicher, dass Sie genau verstehen, was Sie messen und optimieren, und versuchen Sie auch, einen Experten für die entsprechende virtuelle Maschine zu finden und ihn zu konsultieren.

Source: https://habr.com/ru/post/de432072/


All Articles