Drei Arten von Speicherlecks

Ursprünglicher Autor: Nelson Elhage
  • Übersetzung
Hallo Kollegen.

Unsere lange Suche nach zeitlosen Bestsellern zur Optimierung des Codes liefert bisher nur die ersten Ergebnisse. Wir sind jedoch bereit, Ihnen zu gefallen, da die Übersetzung des legendären Ben Watson-Buches " Writing High Performance .NET Code " buchstäblich gerade fertiggestellt wurde . In den Läden - ungefähr im April, achten Sie auf Werbung.

Und heute bieten wir Ihnen an, einen rein praktischen Artikel über die dringlichsten Arten von RAM-Lecks zu lesen, den Nelson Elhage von Stripe schrieb .

Sie haben also ein Programm, dessen Ausführung umso mehr ausgegeben wird - je mehr Zeit. Es ist wahrscheinlich nicht schwer für Sie zu verstehen, dass dies ein sicheres Zeichen für ein Leck im Speicher ist.
Was genau meinen wir jedoch mit "Memory Leak"? Meiner Erfahrung nach sind offensichtliche Lecks im Gedächtnis in drei Hauptkategorien unterteilt, von denen jede durch ein bestimmtes Verhalten gekennzeichnet ist. Für das Debuggen jeder Kategorie werden spezielle Werkzeuge und Techniken benötigt. In diesem Artikel möchte ich alle drei Klassen beschreiben und vorschlagen, richtig zu erkennen, mit
welcher Klasse Sie sich befassen, und wie Sie ein Leck finden.

Typ (1): Ein nicht erreichbares Fragment des Speichers

wird zugewiesen. Dies ist ein klassischer Speicherverlust in C / C ++. Jemand hat Speicher mit newoder zugewiesen mallocund hat nie freeoder aufgerufendeleteum die Erinnerung am Ende der Arbeit mit ihr freizugeben.

voidleak_memory(){
  char *leaked = malloc(4096);
  use_a_buffer(leaked);
  /* Упс, забыл вызвать free() */
}

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

  • Wenn Sie in C oder C ++ schreiben, insbesondere in C ++, ohne die allgegenwärtige Verwendung von intelligenten Zeigern zur Steuerung der Lebensdauer von Speichersegmenten, ist dies die erste zu berücksichtigende Option.
  • 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 ausgelöst wird. Allerdings müssen Sie zunächst die Lecks der Typen (2) und (3) beseitigen.

So finden Sie ein solches Leck

  • Verwenden Sie ASAN . Verwenden Sie ASAN. Verwenden Sie ASAN.
  • Verwenden Sie einen anderen Detektor. Ich habe Valgrind- oder Heap-Tcmalloc-Tools ausprobiert. Es gibt auch andere Tools in anderen Umgebungen.
  • Einige Speicherzuordnungen ermöglichen das Ablegen eines Heap-Profils, in dem alle nicht zugewiesenen Speicherblöcke angezeigt werden. Wenn Sie ein Leck haben, werden nach einer Weile fast alle aktiven Sekrete daraus fließen, so dass es wahrscheinlich nicht schwierig ist, es zu finden.
  • Wenn nichts hilft, geben Sie den Speicherabzug aus und lernen Sie ihn so gründlich wie möglich . Aber am Anfang sollte es definitiv nicht sein.

Typ (2): ungeplante, langlebige Speicherzuordnungen

Diese Situationen sind keine "Lecks" im klassischen Sinne des Wortes, da die Verbindung zu diesem Speicherplatz immer noch erhalten bleibt und daher möglicherweise freigegeben wird (wenn das Programm Zeit hat, dorthin zu gelangen ohne all den Speicher aufzuwenden).
Situationen in dieser Kategorie können aus vielen spezifischen Gründen entstehen. Die häufigsten sind:

  • Unbeabsichtigte Zustandsakkumulation in der globalen Struktur; Der HTTP-Server schreibt beispielsweise jedes Objekt in die globale Liste Request.
  • Caches ohne durchdachte Obsoleszenz-Richtlinien. Zum Beispiel ein ORM-Cache, in dem alle hochgeladenen Objekte zwischengespeichert werden, die während des Migrationsvorgangs aktiv sind, währenddessen alle in der Tabelle vorhandenen Datensätze geladen werden.
  • Der zu volumetrische Zustand wird in der Schaltung erfasst. Ein solcher Fall tritt insbesondere in JavaScript auf, kann aber auch in anderen Umgebungen vorkommen.
  • Im weiteren Sinne die unbeabsichtigte Beibehaltung jedes der Elemente eines Arrays oder Streams, wobei davon ausgegangen wurde, dass diese Elemente online verarbeitet werden.

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

  • Wenn das Programm in einer Umgebung mit Speicherbereinigung ausgeführt wird, wird diese Option zuerst betrachtet.
  • Vergleichen Sie die in der Garbage Collector-Statistik angezeigte Heap-Größe mit der vom Betriebssystem angegebenen freien Speichergröße. Wenn das Leck in diese Kategorie fällt, werden die Zahlen vergleichbar sein und, was am wichtigsten ist, im Laufe der Zeit aufeinander folgen.

So finden Sie ein solches Leck

Verwenden Sie in Ihrer Umgebung Profiler oder Heap-Dump-Tools. Ich weiß, es gibt einen Guppy in Python oder einen memory_profiler in Ruby, und ich selbst habe ObjectSpace direkt in Ruby geschrieben.

Typ (3): freier, aber ungenutzter oder nicht verwendbarer Speicher Die

Charakterisierung dieser Kategorie ist am schwierigsten, jedoch am wichtigsten zu verstehen und zu berücksichtigen.

Diese Art von Leckagen tritt im grauen Bereich zwischen Speicher auf, der aus Sicht des Allokators innerhalb der VM oder Laufzeitumgebung als "frei" betrachtet wird, und Speicher, der aus Sicht des Betriebssystems "frei" ist. Der häufigste (aber nicht der einzige) Grund für dieses Phänomen istHaufen Fragmentierung . Einige Distributoren nehmen den Speicher nach der Zuweisung einfach an das Betriebssystem zurück und geben ihn nicht zurück.

Ein solcher Fall ist am Beispiel eines kurzen in Python geschriebenen Programms zu sehen:

import sys
from guppy import hpy
hp = hpy()
defrss():return4096 * int(open('/proc/self/stat').read().split(' ')[23])
defgcsize():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 alle nachfolgenden. Wir leiten jede Sekunde den Zustand des Gedächtnisses aus der Sicht des Betriebssystems und aus der Sicht unseres eigenen Python-Garbage Collectors ab.

Ich bekomme so etwas auf meinem Laptop: Wir können sicherstellen, dass Python tatsächlich die Hälfte der Puffer freisetzt, da gcsize fast die Hälfte des Spitzenwerts gesunken ist, aber kein einzelnes Byte an das Betriebssystem zurückgegeben werden konnte. Der freigegebene Speicher bleibt für denselben Python-Prozess verfügbar, jedoch für keinen anderen Prozess auf diesem Computer.

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




Solche freien, aber ungenutzten Erinnerungsfragmente können sowohl problematisch als auch harmlos sein. Wenn ein Python-Programm auf diese Weise agiert und dann eine Handvoll 1-kb-Fragmente zuweist, wird dieser Speicherplatz einfach wiederverwendet, und alles ist gut.

Wenn wir dies jedoch während des ersten Setups machten und später den Speicher um ein Minimum zugeteilt hatten oder wenn alle anschließend zugewiesenen Fragmente 1,5 kb betrugen und nicht in diese zuvor verbleibenden Puffer passten, würde der auf diese Weise zugewiesene Speicher immer inaktiv bleiben. wäre verschwendet.

Probleme dieser Art sind insbesondere in einer bestimmten Umgebung relevant, nämlich in Multiprozessorserversystemen für das Arbeiten mit Sprachen wie Ruby oder Python.

Nehmen wir an, wir haben ein System eingerichtet, in dem:

  • Jeder Server verwendet N Single-Threaded-Worker, die Anfragen wettbewerbsfähig bearbeiten. Nehmen wir N = 10 für die Genauigkeit.
  • In der Regel verfügt jeder Mitarbeiter über einen fast konstanten Speicherplatz. Nehmen wir für die Genauigkeit 500 MB.
  • Mit einer geringen Häufigkeit erhalten wir Anforderungen, die viel mehr Speicher erfordern als die mittlere Anforderung. Nehmen wir zur Genauigkeit an, dass wir einmal pro Minute eine Anforderung erhalten, für deren Ausführungszeit zusätzlich 1 GB zusätzlicher Speicher erforderlich ist, und nach Abschluss der Verarbeitung der Anforderung dieser Speicher freigegeben wird.

Jede Minute kommt eine „cetaceous“ Anfragen , die wir zu einem der 10 Arbeitern begehen, zum Beispiel nach dem Zufallsprinzip: ~random. Im Idealfall sollte der Mitarbeiter zum Zeitpunkt der Bearbeitung dieser Anforderung 1 GB RAM zuweisen und nach Beendigung der Arbeit diesen Speicher an das Betriebssystem zurückgeben, damit er später erneut verwendet 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 diesen Speicher aufgrund von Fragmentierung oder aus einem anderen Grund niemals an das Betriebssystem zurückgeben kann. Das heißt, die RAM-Menge, die das Betriebssystem benötigt, ist gleich der größten Speichermenge, die je zu einem Zeitpunkt zugewiesen werden muss. In diesem Fall, wenn ein bestimmter Mitarbeiter eine solche ressourcenintensive Anfrage bearbeitet, wird der von einem solchen Prozess im Speicher belegte Bereich für immer um ein Gigabyte anschwellen.

Wenn Sie den Server starten, werden Sie feststellen, dass der verwendete Speicher 10 * 500 MB = 5 GB beträgt. Sobald die erste große Anfrage eingeht, greift der erste Arbeiter 1 GB Arbeitsspeicher und gibt ihn nicht zurück. Der Gesamtspeicher springt auf 6 GB. Die folgenden eingehenden Anforderungen können von Zeit zu Zeit von dem Prozess verworfen werden, der den "Wal" zuvor verarbeitet hat, und in diesem Fall ändert sich die Menge des verwendeten Speichers nicht. Manchmal wird jedoch eine so große Anforderung an einen anderen Mitarbeiter gesendet, wodurch der Speicher um weitere 1 GB erweitert wird, usw., bis jeder Mitarbeiter Zeit hatte, eine solche große Anforderung mindestens einmal zu bearbeiten. In diesem Fall verwenden Sie diese Vorgänge mit bis zu 10 * (500 MB + 1 GB) = 15 GB RAM. Dies ist viel mehr als die idealen 6 GB! Darüber hinaus, wenn wir überlegen

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

  • Vergleichen Sie die in der Garbage Collector-Statistik angezeigte Heap-Größe mit der vom Betriebssystem angegebenen freien Speichergröße. Wenn das Leck in diese (dritte) Kategorie fällt, divergieren die Zahlen mit der Zeit.
  • Ich mag es, meine Anwendungsserver so einzurichten, dass beide dieser Zahlen in meiner Zeitreiheninfrastruktur regelmäßig abreißen, sodass es praktisch ist, Grafiken auf ihnen anzuzeigen.
  • Unter Linux können Sie den Status des Betriebssystems in Feld 24 von /proc/self/statund den Speicherzuweiser über eine sprachspezifische oder eine für eine virtuelle Maschine spezifische API anzeigen.

So finden Sie eine solche Leckage

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" arbeiten. Es gibt jedoch eine Reihe bewährter Verfahren, die dazu beitragen können, die Auswirkungen solcher „virtuellen Lecks“ zu verringern oder zu verringern:

  • Starten Sie Ihre Prozesse häufiger neu. Wenn das Problem langsam zunimmt, ist es möglicherweise nicht schwierig, alle Prozesse der Anwendung alle 15 Minuten oder einmal pro Stunde erneut zu starten.
  • Noch radikaler: Sie können allen Prozessen beibringen, selbst neu zu starten, sobald der Speicherplatz, den sie im Speicher belegen, einen bestimmten Schwellenwert überschreitet oder um einen bestimmten Betrag ansteigt. Stellen Sie jedoch sicher, dass Ihr gesamter Serverpark nicht in einem spontanen synchronen Neustart gestartet werden kann.
  • Ändern Sie den Speicherzuweiser. Auf lange Sicht meistern tcmalloc und jemalloc die Fragmentierung viel besser als der Standard-Allokator, und das Experimentieren mit ihnen ist mit einer Variablen sehr praktisch LD_PRELOAD.
  • Finden Sie heraus, ob Sie individuelle Anforderungen haben, die viel mehr Speicher benötigen als andere. In Stripe messen API-Server RSS (konstante Speicherbelegung) vor und nach der Bearbeitung jeder API-Anforderung und protokollieren das Delta. Dann können wir unsere Protokollaggregationssysteme auf einfache Weise abfragen, um festzustellen, ob es solche Terminals und Benutzer gibt (und ob Muster verfolgt werden), auf die Speicherverbrauchs-Bursts abgeschrieben werden können.
  • Passen Sie den Garbage Collector / Memory Allocationator an. Viele von ihnen verfügen über anpassbare Parameter, mit denen Sie angeben können, wie aktiv ein solcher Mechanismus Speicher an das Betriebssystem zurückgibt, wie optimiert er ist, um die Fragmentierung zu beseitigen. Es gibt andere nützliche Optionen. Auch hier ist alles sehr schwierig: Vergewissern Sie sich, dass Sie genau verstehen, was Sie messen und optimieren, und versuchen Sie auch, einen Experten für die entsprechende virtuelle Maschine zu finden, und konsultieren Sie sie.

Jetzt auch beliebt: