paint-brush
Wie eine Datenbank OOM-Abstürze beseitigtvon@wydfy111
719 Lesungen
719 Lesungen

Wie eine Datenbank OOM-Abstürze beseitigt

von Jiafeng Zhang11m2023/06/12
Read on Terminal Reader
Read this story w/o Javascript

Zu lang; Lesen

Eine robustere und flexiblere Speicherverwaltungslösung mit Optimierungen bei der Speicherzuweisung, Speicherverfolgung und Speicherbegrenzung.

People Mentioned

Mention Thumbnail
featured image - Wie eine Datenbank OOM-Abstürze beseitigt
Jiafeng Zhang HackerNoon profile picture

Was garantiert die Systemstabilität bei großen Datenabfrageaufgaben? Es handelt sich um einen effektiven Speicherzuweisungs- und Überwachungsmechanismus. Auf diese Weise beschleunigen Sie die Berechnung, vermeiden Speicher-Hotspots, reagieren umgehend auf unzureichenden Speicher und minimieren OOM-Fehler.




Wie leiden Datenbankbenutzer aus Sicht eines Datenbankbenutzers unter einer schlechten Speicherverwaltung? Dies ist eine Liste von Dingen, die unsere Benutzer früher gestört haben:


  • OOM-Fehler führen zum Absturz von Backend-Prozessen. Um eines unserer Community-Mitglieder zu zitieren: Hallo, Apache Doris, es ist in Ordnung, Dinge zu verlangsamen oder ein paar Aufgaben fehlzuschlagen, wenn Ihnen der Speicher fehlt, aber eine Ausfallzeit herbeizuführen ist einfach nicht cool.


  • Backend-Prozesse verbrauchen zu viel Speicherplatz, es gibt jedoch keine Möglichkeit, die genaue Aufgabe zu finden oder die Speichernutzung für eine einzelne Abfrage zu begrenzen.


  • Es ist schwierig, für jede Abfrage die richtige Speichergröße festzulegen. Daher besteht die Möglichkeit, dass eine Abfrage abgebrochen wird, selbst wenn genügend Speicherplatz vorhanden ist.


  • Abfragen mit hoher Parallelität sind unverhältnismäßig langsam und Speicher-Hotspots sind schwer zu finden.


  • Zwischendaten während der HashTable-Erstellung können nicht auf Festplatten geleert werden, sodass Join-Abfragen zwischen zwei großen Tabellen aufgrund von OOM häufig fehlschlagen.


Glücklicherweise liegen diese dunklen Tage hinter uns, denn wir haben unseren Speicherverwaltungsmechanismus von Grund auf verbessert. Jetzt machen Sie sich bereit; Es wird intensiv.

Speicherzuweisung

In Apache Doris haben wir eine einzige Schnittstelle für die Speicherzuweisung: Allocator . Es werden nach eigenem Ermessen Anpassungen vorgenommen, um die Speichernutzung effizient und unter Kontrolle zu halten.


Außerdem sind MemTracker vorhanden, um die zugewiesene oder freigegebene Speichergröße zu verfolgen, und drei verschiedene Datenstrukturen sind für die große Speicherzuweisung bei der Operatorausführung verantwortlich (wir werden gleich darauf zurückkommen).




Datenstrukturen im Speicher

Da verschiedene Abfragen unterschiedliche Speicher-Hotspot-Muster bei der Ausführung haben, stellt Apache Doris drei verschiedene In-Memory-Datenstrukturen bereit: Arena , HashTable und PODArray . Sie stehen alle unter der Herrschaft des Allokators.



  1. Arena

Die Arena ist ein Speicherpool, der eine Liste von Chunks verwaltet, die auf Anfrage des Allokators zugewiesen werden sollen. Die Chunks unterstützen die Speicherausrichtung. Sie bleiben während der gesamten Lebensdauer der Arena bestehen und werden bei der Zerstörung freigegeben (normalerweise, wenn die Abfrage abgeschlossen ist).


Chunks werden hauptsächlich zum Speichern der serialisierten oder deserialisierten Daten während Shuffle oder der serialisierten Schlüssel in HashTables verwendet.


Die anfängliche Größe eines Blocks beträgt 4096 Bytes. Wenn der aktuelle Block kleiner als der angeforderte Speicher ist, wird ein neuer Block zur Liste hinzugefügt.


Wenn der aktuelle Block kleiner als 128 MB ist, verdoppelt sich die Größe des neuen Blocks. Wenn er größer als 128 MB ist, ist der neue Block höchstens 128 MB größer als erforderlich.


Der alte kleine Block wird nicht für neue Anfragen zugewiesen. Es gibt einen Cursor, der die Trennlinie zwischen zugewiesenen und nicht zugewiesenen Blöcken markiert.


  1. Hash-tabelle

HashTables sind für Hash-Joins, Aggregationen, Mengenoperationen und Fensterfunktionen anwendbar. Die PartitionedHashTable-Struktur unterstützt nicht mehr als 16 Sub-HashTables. Es unterstützt auch die parallele Zusammenführung von HashTables und jeder Sub-Hash-Join kann unabhängig skaliert werden.


Dadurch kann die Gesamtspeichernutzung und die durch die Skalierung verursachte Latenz reduziert werden.


Wenn die aktuelle HashTable kleiner als 8 MB ist, wird sie um den Faktor 4 skaliert.

Wenn es größer als 8 MB ist, wird es um den Faktor 2 skaliert.

Wenn es kleiner als 2G ist, wird es skaliert, wenn es zu 50 % voll ist;

und wenn es größer als 2G ist, wird es skaliert, wenn es zu 75 % ausgelastet ist.


Die neu erstellten HashTables werden basierend auf der Menge der Daten, die sie enthalten werden, vorskaliert. Wir bieten auch verschiedene Arten von HashTables für verschiedene Szenarien an. Für Aggregationen können Sie beispielsweise PHmap anwenden.


  1. PODArray

PODArray ist, wie der Name schon sagt, ein dynamisches POD-Array. Der Unterschied zu std::vector besteht darin, dass PODArray keine Elemente initialisiert. Es unterstützt die Speicherausrichtung und einige Schnittstellen von std::vector .


Es wird um den Faktor 2 skaliert. Bei der Zerstörung wird der Speicher des gesamten PODArrays freigegeben, anstatt die Destruktorfunktion für jedes Element aufzurufen. PODArray wird hauptsächlich zum Speichern von Zeichenfolgen in Spalten verwendet und ist in vielen Funktionsberechnungen und Ausdrucksfilterungen anwendbar.

Speicherschnittstelle

Als einzige Schnittstelle, die Arena, PODArray und HashTable koordiniert, führt der Allocator die Speicherzuordnung (MMAP) für Anfragen mit mehr als 64 MB aus.


Diejenigen, die kleiner als 4K sind, werden direkt vom System über malloc/free zugewiesen; und die dazwischen liegenden werden durch einen Allzweck-Caching-ChunkAllocator beschleunigt, der unseren Benchmarking-Ergebnissen zufolge eine Leistungssteigerung von 10 % mit sich bringt.


Der ChunkAllocator versucht, einen Block der angegebenen Größe sperrenfrei aus der FreeList des aktuellen Kerns abzurufen. Wenn ein solcher Block nicht vorhanden ist, wird er es auf sperrenbasierte Weise von anderen Kernen aus versuchen. Wenn dies immer noch fehlschlägt, fordert es die angegebene Speichergröße vom System an und kapselt sie in einen Block.


Nachdem wir beide erlebt hatten, entschieden wir uns für Jemalloc gegenüber TCMalloc. Wir haben TCMalloc in unseren Tests mit hoher Parallelität ausprobiert und festgestellt, dass Spin Lock in CentralFreeList 40 % der gesamten Abfragezeit in Anspruch nahm.


Das Deaktivieren der „aggressiven Speicherfreigabe“ brachte eine Verbesserung, führte jedoch zu einer deutlich höheren Speicherauslastung, sodass wir einen einzelnen Thread verwenden mussten, um den Cache regelmäßig wiederzuverwenden. Jemalloc hingegen war bei Abfragen mit hoher Parallelität leistungsfähiger und stabiler.


Nach der Feinabstimmung für andere Szenarien lieferte es die gleiche Leistung wie TCMalloc, verbrauchte jedoch weniger Speicher.

Speicherwiederverwendung

Die Wiederverwendung von Speicher erfolgt weitgehend auf der Ausführungsebene von Apache Doris. Beispielsweise werden Datenblöcke während der Ausführung einer Abfrage wiederverwendet. Während des Shuffle gibt es auf der Senderseite zwei Blöcke, die abwechselnd arbeiten, wobei einer Daten empfängt und der andere im RPC-Transport.


Beim Lesen eines Tablets verwendet Doris die Prädikatspalte wieder, implementiert zyklisches Lesen, Filtern, kopiert gefilterte Daten in den oberen Block und löscht sie dann.


Wenn Sie Daten in eine Aggregate Key-Tabelle aufnehmen, werden diese vorab aggregiert, sobald die MemTable, die Daten zwischenspeichert, eine bestimmte Größe erreicht, und dann werden weitere Daten hineingeschrieben.


Die Wiederverwendung des Speichers erfolgt auch beim Datenscan. Bevor der Scanvorgang beginnt, werden der Scanaufgabe eine Reihe freier Blöcke (abhängig von der Anzahl der Scanner und Threads) zugewiesen.


Bei jeder Scannerplanung wird einer der freien Blöcke zum Datenlesen an die Speicherschicht übergeben.


Nach dem Lesen der Daten wird der Block in die Produzentenwarteschlange gestellt, damit er von den oberen Operatoren in der nachfolgenden Berechnung verwendet wird. Sobald ein oberer Bediener die Berechnungsdaten aus dem Block kopiert hat, wird der Block für die nächste Scannerplanung wieder in die freien Blöcke verschoben.


Der Thread, der die freien Blöcke vorab zuweist, ist auch dafür verantwortlich, sie nach dem Datenscannen freizugeben, sodass kein zusätzlicher Overhead entsteht. Die Anzahl der freien Blöcke bestimmt in gewisser Weise die Parallelität beim Datenscannen.

Gedächtnisverfolgung

Apache Doris verwendet MemTracker, um die Zuweisung und Freigabe von Speicher zu verfolgen und gleichzeitig Speicher-Hotspots zu analysieren. Die MemTracker zeichnen Aufzeichnungen über jede Datenabfrage, Datenaufnahme, Datenkomprimierungsaufgabe und die Speichergröße jedes globalen Objekts wie Cache und TabletMeta auf.


Es unterstützt sowohl manuelles Zählen als auch automatisches MemHook-Tracking. Benutzer können die Speichernutzung in Echtzeit im Doris-Backend auf einer Webseite anzeigen.

Struktur von MemTrackern

Das MemTracker-System vor Apache Doris 1.2.0 befand sich in einer hierarchischen Baumstruktur, bestehend aus Process_mem_tracker, query_pool_mem_tracker, query_mem_tracker, Instanz_mem_tracker, ExecNode_mem_tracker usw.


MemTracker zweier benachbarter Schichten stehen in einer Eltern-Kind-Beziehung. Daher werden alle Berechnungsfehler in einem untergeordneten MemTracker bis zum Ende akkumuliert und führen zu einem größeren Ausmaß an Unglaubwürdigkeit.



In Apache Doris 1.2.0 und neuer haben wir die Struktur von MemTrackers viel einfacher gemacht. MemTracker werden aufgrund ihrer Rolle nur in zwei Typen unterteilt: MemTracker Limiter und die anderen.


Der MemTracker Limiter überwacht die Speichernutzung und ist in jeder Abfrage-/Aufnahme-/Komprimierungsaufgabe und jedem globalen Objekt einzigartig. während die anderen MemTracker die Speicher-Hotspots bei der Abfrageausführung verfolgen, wie z. B. HashTables in Join-/Aggregations-/Sort-/Fensterfunktionen und Zwischendaten bei der Serialisierung, um ein Bild davon zu geben, wie Speicher in verschiedenen Operatoren verwendet wird, oder eine Referenz für die Speichersteuerung bereitzustellen Datenlöschung.


Die Eltern-Kind-Beziehung zwischen MemTracker Limiter und anderen MemTrackern zeigt sich nur beim Snapshot-Drucken. Sie können sich eine solche Beziehung als symbolische Verbindung vorstellen. Sie werden nicht gleichzeitig verbraucht und der Lebenszyklus des einen hat keinen Einfluss auf den des anderen.


Dies macht es für Entwickler viel einfacher, sie zu verstehen und zu verwenden.


MemTracker (einschließlich MemTracker Limiter und die anderen) werden in eine Gruppe von Karten eingefügt. Sie ermöglichen es Benutzern, allgemeine Snapshots vom Typ MemTracker und Snapshots von Abfrage-/Lade-/Komprimierungsaufgaben zu drucken und die Abfrage/das Laden mit der höchsten Speichernutzung oder der größten Speicherübernutzung herauszufinden.



So funktioniert MemTracker

Um die Speichernutzung einer bestimmten Ausführung zu berechnen, wird ein MemTracker zu einem Stapel in Thread Local des aktuellen Threads hinzugefügt. Durch Neuladen von malloc/free/realloc in Jemalloc oder TCMalloc erhält MemHook die tatsächliche Größe des zugewiesenen oder freigegebenen Speichers und zeichnet sie in Thread Local des aktuellen Threads auf.


Wenn eine Ausführung abgeschlossen ist, wird der entsprechende MemTracker vom Stapel entfernt. Am Ende des Stapels befindet sich der MemTracker, der die Speichernutzung während des gesamten Abfrage-/Ladevorgangs aufzeichnet.


Lassen Sie es mich nun anhand eines vereinfachten Abfrageausführungsprozesses erklären.


  • Nach dem Start eines Doris-Backend-Knotens wird die Speichernutzung aller Threads im Process MemTracker aufgezeichnet.


  • Wenn eine Abfrage übermittelt wird, wird ein Query MemTracker zum Thread Local Storage (TLS) Stack im Fragmentausführungsthread hinzugefügt.


  • Sobald ein ScanNode geplant ist, wird ein ScanNode MemTracker zum Thread Local Storage (TLS) Stack im Fragmentausführungsthread hinzugefügt. Anschließend wird der in diesem Thread zugewiesene oder freigegebene Speicher sowohl im Query MemTracker als auch im ScanNode MemTracker aufgezeichnet.


  • Nachdem ein Scanner geplant wurde, werden ein Query MemTracker und ein Scanner MemTracker zum TLS-Stack des Scanner-Threads hinzugefügt.


  • Wenn der Scanvorgang abgeschlossen ist, werden alle MemTracker im Scanner-Thread-TLS-Stack entfernt. Wenn die ScanNode-Planung abgeschlossen ist, wird der ScanNode MemTracker aus dem Fragmentausführungsthread entfernt. Wenn dann ein Aggregationsknoten geplant wird, wird auf ähnliche Weise ein AggregationNode MemTracker zum TLS-Stack des Fragmentausführungsthreads hinzugefügt und nach Abschluss der Planung entfernt.


  • Wenn die Abfrage abgeschlossen ist, wird der Query MemTracker aus dem TLS-Stack des Fragmentausführungsthreads entfernt. Zu diesem Zeitpunkt sollte dieser Stapel leer sein. Anschließend können Sie im QueryProfile die maximale Speichernutzung während der gesamten Abfrageausführung sowie in jeder Phase (Scannen, Aggregation usw.) anzeigen.



So verwenden Sie MemTracker

Die Doris-Backend-Webseite demonstriert die Speichernutzung in Echtzeit, die in folgende Typen unterteilt ist: Abfrage/Laden/Komprimierung/Global. Der aktuelle Speicherverbrauch und der Spitzenverbrauch werden angezeigt.



Zu den globalen Typen gehören MemTracker von Cache und TabletMeta.



Anhand der Abfragetypen können Sie den aktuellen Speicherverbrauch und den Spitzenverbrauch der aktuellen Abfrage und der beteiligten Operatoren sehen (anhand der Beschriftungen können Sie erkennen, wie diese zusammenhängen). Für Speicherstatistiken historischer Abfragen können Sie die Doris FE-Überwachungsprotokolle oder BE INFO-Protokolle überprüfen.



Speicherlimit

Mit der weithin implementierten Speicherverfolgung in Doris-Backends sind wir der Eliminierung von OOM, der Ursache für Backend-Ausfallzeiten und großflächige Abfragefehler, einen Schritt näher gekommen. Der nächste Schritt besteht darin, das Speicherlimit für Abfragen und Prozesse zu optimieren, um die Speichernutzung unter Kontrolle zu halten.

Speicherlimit bei Abfrage

Benutzer können für jede Abfrage ein Speicherlimit festlegen. Wenn dieses Limit während der Ausführung überschritten wird, wird die Abfrage abgebrochen. Aber seit Version 1.2 haben wir Memory Overcommit zugelassen, eine flexiblere Steuerung der Speicherbegrenzung.


Wenn genügend Speicherressourcen vorhanden sind, kann eine Abfrage ohne Abbruch mehr Speicher als das Limit verbrauchen, sodass Benutzer nicht besonders auf die Speichernutzung achten müssen. Ist dies nicht der Fall, wartet die Abfrage, bis neuer Speicherplatz zugewiesen wird. Die Abfrage wird nur dann abgebrochen, wenn der neu freigewordene Speicher nicht für die Abfrage ausreicht.


In Apache Doris 2.0 haben wir Ausnahmesicherheit für Abfragen realisiert. Das bedeutet, dass eine unzureichende Speicherzuweisung sofort zum Abbruch der Abfrage führt, was die mühsame Überprüfung des Status „Abbrechen“ in nachfolgenden Schritten erspart.

Speicherlimit für den Prozess

Das Doris-Backend ruft regelmäßig den physischen Speicher von Prozessen und die aktuell verfügbare Speichergröße vom System ab. In der Zwischenzeit werden MemTracker-Snapshots aller Abfrage-/Lade-/Komprimierungsaufgaben gesammelt.


Wenn ein Backend-Prozess sein Speicherlimit überschreitet oder nicht genügend Speicher vorhanden ist, gibt Doris Speicherplatz frei, indem sie den Cache löscht und eine Reihe von Abfragen oder Datenaufnahmeaufgaben abbricht. Diese werden regelmäßig von einem einzelnen GC-Thread ausgeführt.



Wenn der verbrauchte Prozessspeicher das SoftMemLimit überschreitet (standardmäßig 81 % des gesamten Systemspeichers) oder der verfügbare Systemspeicher unter die Warnwassermarke (weniger als 3,2 GB) fällt, wird Minor GC ausgelöst.


In diesem Moment wird die Abfrageausführung beim Speicherzuweisungsschritt angehalten, die zwischengespeicherten Daten in Datenaufnahmeaufgaben werden zwangsweise geleert und ein Teil des Datenseitencaches und des veralteten Segmentcaches wird freigegeben.


Wenn der neu freigegebene Speicher nicht 10 % des Prozessspeichers abdeckt, beginnt Doris bei aktivierter Speicherüberbelegung damit, die Abfragen abzubrechen, die die größten „Überbelegungen“ darstellen, bis das Ziel von 10 % erreicht ist oder alle Abfragen abgebrochen werden.


Dann wird Doris das Systemspeicher-Überprüfungsintervall und das GC-Intervall verkürzen. Die Abfragen werden fortgesetzt, sobald mehr Speicher verfügbar ist.


Wenn der verbrauchte Prozessspeicher das MemLimit überschreitet (standardmäßig 90 % des gesamten Systemspeichers) oder der verfügbare Systemspeicher unter die Low Water Mark (weniger als 1,6 GB) fällt, wird die vollständige GC ausgelöst.


Zu diesem Zeitpunkt werden Datenaufnahmeaufgaben gestoppt und der gesamte Datenseiten-Cache und die meisten anderen Caches werden freigegeben.


Wenn nach all diesen Schritten der neu freigegebene Speicher nicht 20 % des Prozessspeichers abdeckt, wird Doris alle MemTracker durchsuchen, die speicherintensivsten Abfragen und Aufnahmeaufgaben finden und diese nacheinander abbrechen.


Erst wenn das 20-Prozent-Ziel erreicht ist, werden das Systemspeicherüberprüfungsintervall und das GC-Intervall verlängert und die Abfragen und Aufnahmeaufgaben werden fortgesetzt. (Ein Garbage-Collection-Vorgang dauert normalerweise Hunderte von μs bis Dutzende ms.)

Einflüsse und Ergebnisse

Nach Optimierungen bei der Speicherzuweisung, Speicherverfolgung und Speicherbegrenzung haben wir die Stabilität und die Leistung bei hoher Parallelität von Apache Doris als Echtzeit-Analyse-Data-Warehouse-Plattform erheblich erhöht. OOM-Abstürze im Backend sind mittlerweile eine seltene Szene.


Selbst wenn ein OOM vorhanden ist, können Benutzer anhand der Protokolle die Ursache des Problems lokalisieren und es dann beheben. Darüber hinaus müssen Benutzer dank flexiblerer Speichergrenzen für Abfragen und Datenaufnahme keinen zusätzlichen Aufwand für die Speicherpflege aufwenden, wenn ausreichend Speicherplatz vorhanden ist.


In der nächsten Phase wollen wir sicherstellen, dass Abfragen bei Speicherüberbelegung abgeschlossen werden, was bedeutet, dass weniger Abfragen aufgrund von Speichermangel abgebrochen werden müssen.


Wir haben dieses Ziel in spezifische Arbeitsrichtungen unterteilt: Ausnahmesicherheit, Speicherisolation zwischen Ressourcengruppen und den Flush-Mechanismus von Zwischendaten.


Wenn Sie unsere Entwickler kennenlernen möchten, finden Sie uns hier .