Wie kann ich die Java-Garbage-Collection-Zeit um 90 % reduzieren? Jeder sollte mit GC in Java vertraut sein. Wie die GC-Optimierung durchgeführt wird, wird im Folgenden ausführlich erläutert. Der GC-Mechanismus der JVM schützt Entwickler vor den Details der Speicherverwaltung und verbessert die Entwicklungseffizienz. Apache PHP MySQL
Vor nicht allzu langer Zeit haben wir uns darauf vorbereitet, diesen allgemein anerkannten Schwachpunkt bei Ali-HBase zu überwinden. Zu diesem Zweck haben wir eine eingehende Analyse und umfassende Innovationsarbeit durchgeführt und einige relativ gute Ergebnisse erzielt. Am Beispiel des Ant-Risikokontrollszenarios wurde die Online-Young-GC-Zeit von HBase von 120 ms auf 15 ms reduziert. In Kombination mit ZenGC, einem vom Alibaba JDK-Team bereitgestellten Tool, erreichte sie in der Labor-Stresstestumgebung weitere 5 ms. In diesem Artikel werden hauptsächlich einige unserer bisherigen Arbeiten und technischen Ideen in diesem Bereich vorgestellt.
Der GC-Mechanismus von JVM schützt Entwickler vor den Details der Speicherverwaltung und verbessert die Entwicklungseffizienz. Apropos GC: Die erste Reaktion vieler Leute könnte sein, dass die JVM für eine lange Zeit pausiert oder dass FGC dazu führt, dass der Prozess stecken bleibt und unbrauchbar wird. Aber für Big-Data-Speicherdienste wie HBase sind die GC-Herausforderungen, die die JVM mit sich bringt, recht komplex und schwierig. Dafür gibt es drei Gründe:
1. Die Speichergröße ist riesig. Bei den meisten Online-HBase-Prozessen handelt es sich um große Heaps mit 96 G. Die neuen Modelle dieses Jahres haben bereits einige Heap-Konfigurationen mit mehr als 160 G eingeführt.
2. Der Objektstatus ist komplex. Der HBase-Server verwaltet intern eine große Anzahl von Lese- und Schreibcaches, die eine Größenordnung von mehreren zehn GB erreichen. HBase stellt geordnete Servicedaten in Form von Tabellen bereit und die Daten sind in einer bestimmten Struktur organisiert. Diese Datenstrukturen generieren über 100 Millionen Objekte und Referenzen
3. Die Häufigkeit junger GCs ist hoch. Je größer der Zugriffsdruck, desto schneller ist der Speicherverbrauch im jungen Bereich. Einige ausgelastete Cluster können 1 bis 2 junge GCs pro Sekunde erreichen. Ein großer junger Bereich kann die GC-Frequenz verringern, führt jedoch zu größeren Pausen bei jungen GCs Unternehmen. Echtzeitanforderungen.
Als Speichersystem verwendet HBase eine große Menge Speicher als Schreibpuffer und Lesecache, beispielsweise einen 96G großen Heap (4G jung + 92G alt). ), Schreibpuffer + Lesecache belegen mehr als 70 % des Speichers (ca. 70 G), der Speicherpegel im Heap selbst wird auf 85 % gesteuert und der verbleibende belegte Speicher liegt nur innerhalb von 10 G. Wenn wir also diesen 70G+-Speicher auf Anwendungsebene selbst verwalten können, dann entspricht für die JVM der GC-Druck eines großen 100G-Heaps dem GC-Druck eines kleinen 10G-Heaps, und er wird auch einem größeren Heap gegenüberstehen Wird die Blähungen in Zukunft nicht verschlimmern. Mit dieser Lösung wurde unsere Online-Young-GC-Zeit von 120 ms auf 15 ms optimiert.
In einem datenintensiven Servicesystem mit hohem Durchsatz wird häufig eine große Anzahl temporärer Objekte erstellt und recycelt. So verwalten Sie die Zuordnung und das Recycling dieser temporären Objekte gezielt , entwickelt vom AliJDK-Team Ein neuer mandantenbasierter GC-Algorithmus – ZenGC. Die HBase der Gruppe wurde auf der Grundlage dieses neuen ZenGC-Algorithmus transformiert. Die von uns im Labor gemessene junge GC-Zeit wurde von 15 ms auf 5 ms reduziert.
Im Folgenden werden nacheinander die Schlüsseltechnologien vorgestellt, die bei der GC-Optimierung der Ali-HBase-Version verwendet werden.
Das derzeit von HBase verwendete Speichermodell ist das LSMTree-Modell. Die geschriebenen Daten werden vorübergehend bis zu einer bestimmten Größe im Speicher gespeichert und dann auf die Festplatte übertragen eine Datei erstellen.
Wir werden ihn im Folgenden als Schreibcache bezeichnen. Der Schreibcache ist abfragbar, was erfordert, dass die Daten im Speicher geordnet werden. Um die Effizienz des gleichzeitigen Lesens und Schreibens zu verbessern und die grundlegenden Anforderungen zum Bestellen von Daten und zur Unterstützung von Suchen und Scannen zu erfüllen, ist SkipList eine weit verbreitete Datenstruktur.
Wir nehmen die mit JDK gelieferte ConcurrentSkipListMap als Beispiel für die Analyse. Es gibt die folgenden drei Probleme:
Es gibt viele interne Objekte. Jedes Mal, wenn ein Element gespeichert wird, werden durchschnittlich 4 Objekte gespeichert (Index + Knoten + Schlüssel + Wert, durchschnittliche Ebenenhöhe beträgt 1)
Neu eingefügte Objekte befinden sich im jungen Bereich und alte Objekte befinden sich im alten Bereich. Wenn Elemente kontinuierlich eingefügt werden, ändert sich die interne Referenzbeziehung häufig. Unabhängig davon, ob es sich um die CardTable-Markierung des ParNew-Algorithmus oder die RSet-Markierung des G1-Algorithmus handelt, ist es möglich, den alten Bereichsscan auszulösen.
Das vom Unternehmen geschriebene KeyValue-Element hat keine reguläre Länge. Wenn es in den alten Bereich heraufgestuft wird, wird möglicherweise eine große Anzahl von Speicherfragmenten generiert.
Problem 1 führt dazu, dass die Kosten für das Scannen von Objekten bei der GC im jungen Bereich sehr hoch sind und bei der GC im jungen Bereich mehr Objekte gefördert werden. Problem 2 führt dazu, dass sich der alte Bereich, der während der jungen GC gescannt werden muss, vergrößert. Problem 3 erhöht die Wahrscheinlichkeit einer FGC, die durch Speicherfragmentierung verursacht wird. Das Problem wird schwerwiegender, wenn die zu schreibenden Elemente kleiner sind. Wir haben Statistiken zum Online-RegionServer-Prozess erstellt und festgestellt, dass es bis zu 120 Millionen aktive Objekte gibt!
Nachdem wir den größten Feind des aktuellen jungen GC analysiert hatten, kam eine mutige Idee auf. Da die Zuweisung, der Zugriff, die Zerstörung und das Recycling des Schreibcaches alle von uns verwaltet werden, wenn die JVM „nicht sehen“ kann Beim Schreiben des Caches verwalten wir den Lebenszyklus des Schreibcaches selbst und das GC-Problem wird natürlich gelöst.
Wenn es darum geht, die JVM „unsichtbar“ zu machen, denken viele Leute vielleicht an eine Off-Heap-Lösung, aber das ist für das Schreib-Caching nicht so einfach, denn selbst wenn der KeyValue außerhalb des Heaps platziert wird, kann er keine Fragen vermeiden 1 und 2. Und 1 und 2 sind auch die größten Probleme für junge GC.
Die Frage lautet nun: Wie erstellt man eine geordnete Karte, die gleichzeitigen Zugriff unterstützt, ohne JVM-Objekte zu verwenden?
Natürlich können wir keinen Leistungsverlust akzeptieren, da die Geschwindigkeit beim Schreiben von Map eng mit dem Schreibdurchsatz von HBase zusammenhängt.
Die Nachfrage wird erneut verstärkt: Wie man eine geordnete Karte erstellt, die den gleichzeitigen Zugriff ohne Verwendung von Objekten ohne Leistungsverlust unterstützt.
Um dieses Ziel zu erreichen, haben wir eine solche Datenstruktur entworfen:
Sie verwendet kontinuierlichen Speicher (innerhalb des Heaps oder außerhalb des Heaps) und wir steuern den interne Struktur durch Code Anstatt sich auf den Objektmechanismus der JVM zu verlassen
ist es logischerweise auch eine SkipList, die sperrenfreies gleichzeitiges Schreiben und Abfragen unterstützt
Steuerzeiger und Daten werden im kontinuierlichen Speicher gespeichert
Die obige Abbildung zeigt die Speicherstruktur von CCSMap (CompactedConcurrentSkipListMap). Wir beantragen Schreib-Cache-Speicher in Form großer Speichersegmente (Chunk). Jeder Chunk enthält mehrere Knoten und jeder Knoten entspricht einem Element. Neu eingefügte Elemente werden immer am Ende des belegten Speichers platziert. Die komplexe Struktur innerhalb des Knotens speichert Wartungsinformationen und Daten wie Index/Weiter/Schlüssel/Wert. Neu eingefügte Elemente müssen in die Knotenstruktur kopiert werden. Wenn in HBase ein Schreib-Cache-Dump auftritt, werden alle Chunks der gesamten CCSMap recycelt. Wenn ein Element gelöscht wird, „werfen“ wir das Element nur logisch aus der verknüpften Liste und stellen das Element nicht tatsächlich aus dem Speicher wieder her (natürlich gibt es Möglichkeiten, eine tatsächliche Wiederherstellung durchzuführen, aber was HBase betrifft, gibt es diese). ist nicht nötig).
Obwohl beim Einfügen von KeyValue-Daten eine zusätzliche Kopie anfällt, geht das Kopieren in den meisten Fällen schneller. Da aufgrund der Struktur von CCSMap der Steuerknoten und der Schlüsselwert eines Elements in einer Map im Speicher benachbart sind, ist die Verwendung des CPU-Cache effizienter und die Suche schneller. Bei SkipList ist die Schreibgeschwindigkeit tatsächlich durch die Suchgeschwindigkeit begrenzt, und der durch das eigentliche Kopieren verursachte Overhead ist weitaus geringer als der Such-Overhead. Unseren Tests zufolge stieg der Lese- und Schreibdurchsatz im Vergleich zur ConcurrentSkipListMap, die mit dem JDK geliefert wird, im KV-Test mit einer Länge von 50 Byte um 20 bis 30 %.
Da es keine JVM-Objekte gibt, belegt jedes JVM-Objekt mindestens 16 Byte Speicherplatz und kann gespeichert werden (8 Byte sind für Tags reserviert und 8 Byte sind Typzeiger). Am Beispiel des 50 Byte langen KeyValue wird im Vergleich zur ConcurrentSkipListMap, die im JDK enthalten ist, die Speichernutzung von CCSMap um 40 % reduziert.
Nachdem CCSMap in der Produktion gestartet wurde, wurde der eigentliche Optimierungseffekt: Young GC von 120 ms+ auf 30 ms reduziert
Vor der Optimierung
Nach der Optimierung
Nach der Verwendung von CCSMap wurden die ursprünglich 120 Millionen überlebenden Objekte auf weniger als 10 Millionen reduziert, was den GC-Druck erheblich reduzierte. Aufgrund der kompakten Speicheranordnung konnte zudem der Schreibdurchsatz um 30 % verbessert werden.
HBase organisiert Daten auf der Festplatte in Form von Blöcken. Eine typische HBase-Blockgröße liegt zwischen 16 KB und 64 KB. HBase verwaltet BlockCache intern, um Festplatten-E/A zu reduzieren. BlockCache entspricht ebenso wie der Schreibcache nicht der Generationshypothese in der GC-Algorithmus-Theorie und ist von Natur aus unfreundlich für den GC-Algorithmus – es ist weder flüchtig noch dauerhaft.
Ein Teil der Blockdaten wird von der Festplatte in den JVM-Speicher geladen. Der Lebenszyklus liegt zwischen Minuten und Monaten. Die meisten Blöcke gelangen in den alten Bereich und werden von der JVM nur während des Major GC recycelt . Seine Probleme spiegeln sich hauptsächlich in Folgendem wider:
Die Größe des HBase-Blocks ist nicht festgelegt und relativ groß, und der Speicher ist leicht fragmentiert.
Im ParNew-Algorithmus ist die Förderung problematisch. Das Problem spiegelt sich nicht in den Kopierkosten wider, sondern in der Größe und den hohen Kosten für die Suche nach geeignetem Speicherplatz zum Speichern des HBase-Blocks.
Die Idee der Lese-Cache-Optimierung besteht darin, einen Teil des Speichers auf die JVM anzuwenden, der niemals als BlockCache zurückgegeben wird. Wir segmentieren den Speicher selbst in Segmente mit fester Größe Wir laden den Block in den Speicher, kopieren ihn in Segmente und markieren ihn als verwendet. Wenn dieser Block nicht mehr benötigt wird, markieren wir das Intervall als verfügbar und neue Blöcke können erneut gespeichert werden. Dies ist der BucketCache. In Bezug auf die Zuweisung und Wiederverwendung von Speicherplatz in BucketCache (das Design und die Entwicklung dieses Bereichs wurden vor vielen Jahren abgeschlossen)
BucketCache
Viele RPC-Frameworks, die auf Off-Heap-Speicher basieren, werden ebenfalls verwaltet die Zuweisung und Wiederverwendung von Off-Heap-Speicher, normalerweise durch explizite Freigabe. Für HBase gibt es jedoch einige Schwierigkeiten. Wir betrachten Blockobjekte als Speichersegmente, die selbst verwaltet werden müssen. Der Block kann von mehreren Aufgaben referenziert werden. Um das Problem der Blockwiederverwendung zu lösen, besteht die einfachste Möglichkeit darin, den Block für jede Aufgabe auf den Stapel zu kopieren (der kopierte Block wird im Allgemeinen nicht in den alten Bereich befördert) und ihn zur JVM zu übertragen Management.
Tatsächlich haben wir diese Methode schon früher verwendet, sie ist einfach zu implementieren, von JVM unterstützt, sicher und zuverlässig. Dies ist jedoch eine verlustbehaftete Speicherverwaltungsmethode. Um das GC-Problem zu lösen, werden für jede Anforderung Kopierkosten eingeführt. Da das Kopieren auf den Stapel zusätzliche CPU-Kopierkosten und Kosten für die Speicherzuweisung für junge Bereiche erfordert, scheint dieser Preis heute hoch zu sein, da CPUs und Busse immer wertvoller werden.
Also haben wir uns der Verwendung der Referenzzählung zur Speicherverwaltung zugewandt. Die Hauptschwierigkeiten in HBase sind:
In HBase gibt es mehrere Aufgaben, die auf denselben Block verweisen
Es kann mehrere Variablen geben, die auf denselben Block in derselben Aufgabe verweisen. Die Referenz kann eine temporäre Variable auf dem Stapel oder ein Objektfeld auf dem Heap sein.
Die Verarbeitungslogik von Block ist relativ komplex und wird zwischen mehreren Funktionen und Objekten in Form von Parametern, Rückgabewerten und Feldzuweisungen übergeben.
Block kann von uns verwaltet werden, oder er kann nicht von uns verwaltet werden (einige Blöcke müssen manuell freigegeben werden, andere nicht).
Block kann in einen Untertyp von Block umgewandelt werden.
Wenn man diese Punkte zusammennimmt, ist es eine Herausforderung, den richtigen Code zu schreiben. Aber in C++ ist es selbstverständlich, intelligente Zeiger zur Verwaltung des Objektlebenszyklus zu verwenden. Warum ist das in Java schwierig?
Variablenzuweisung in Java erzeugt auf Benutzercodeebene nur Referenzzuweisungsverhalten, während die Variablenzuweisung in C++ den Konstruktor und Destruktor des Objekts verwenden kann, um viele Dinge zu tun, intelligente Zeiger Das heißt, basierend darauf Implementierung (natürlich verursacht die unsachgemäße Verwendung von C++-Konstruktoren und -Destruktoren auch viele Probleme, jedes mit seinen eigenen Vor- und Nachteilen, die hier nicht besprochen werden)
Also haben wir uns auf die intelligenten Zeiger von C++ bezogen und einen Block entworfen Das Referenzverwaltungs- und Recycling-Framework ShrableHolder wird verwendet, um verschiedene oder andere Schwierigkeiten bei der Codierung zu beseitigen. Es hat das folgende Paradigma:
ShrableHolder kann Objekte mit Referenzzählung und Objekte ohne Referenzzählung verwalten
ShrableHolder wird verwendet, wenn es sich um When handelt Beim Umhängen wird das bisherige Objekt freigegeben. Wenn es sich um ein verwaltetes Objekt handelt, wird der Referenzzähler um 1 dekrementiert, andernfalls gibt es keine Änderung.
ShrableHolder muss zurückgesetzt werden, wenn die Aufgabe endet oder das Codesegment endet
ShrableHolder kann nicht direkt zugewiesen werden. Die von ShrableHolder bereitgestellte Methode muss aufgerufen werden, um Inhalte zu übertragen
Da ShrableHolder nicht direkt zugewiesen werden kann, kann ShrableHolder nicht als verwendet werden, wenn Sie einen Block mit Lebenszyklussemantik an eine Funktion übergeben müssen ein Parameter der Funktion.
Der nach diesem Paradigma geschriebene Code weist nur wenige Änderungen an der ursprünglichen Codelogik auf und es werden keine weiteren Änderungen vorgenommen. Obwohl es immer noch eine gewisse Komplexität zu geben scheint, ist der davon betroffene Bereich glücklicherweise immer noch auf eine sehr lokale untere Schicht beschränkt, was für HBase immer noch akzeptabel ist. Um auf der sicheren Seite zu sein und Speicherlecks zu vermeiden, haben wir diesem Framework einen Erkennungsmechanismus hinzugefügt, um Referenzen zu erkennen, die längere Zeit inaktiv waren. Sobald sie gefunden werden, werden sie zwangsweise zum Löschen markiert.
Nach der Verwendung von BucketCache wird der Promotion-Overhead von BlockCache reduziert und die junge GC-Zeit reduziert:
( CCSMap +BucketCache-Optimierungseffekt)
Nach den beiden oben genannten großen Optimierungen wurde die junge GC-Zeit der Ant Risk Control-Produktionsumgebung auf 15 ms reduziert. Da es in diesem Maßstab bereits schwierig ist, den ParNew+CMS-Algorithmus zu optimieren, haben wir uns an ZenGC gewandt. ZenGC hat auf der Grundlage des G1-Algorithmus tiefgreifende Verbesserungen vorgenommen. Der selbstverwaltete Speicherhaufen von HBase und ZenGC hat eine gute chemische Reaktion hervorgerufen.
ZenGC ist die Sammelbezeichnung für den GC-Algorithmus, der vom Alibaba JVM-Team auf Basis des G1-Algorithmus optimiert wurde und auf Anwendungsszenarien mit großem Heap (LargeHeap) ausgerichtet ist. Hier stellen wir hauptsächlich Multi-Tenant-GC vor.
Multi-Tenant-GC enthält drei Ebenen der Kernlogik: 1) Auf JavaHeap ist die Objektzuweisung je nach Mandant isoliert, und verschiedene Mandanten nutzen unterschiedliche Heap-Bereiche. 2) Ermöglichen, dass GC in Mandanten zu geringeren Kosten erfolgt Granularität, nicht nur die globale Anwendung; 3) Ermöglichen Sie Anwendungen der oberen Ebene, Mieter entsprechend den Geschäftsanforderungen flexibel zuzuordnen.
ZenGC unterteilt die Speicherregion in mehrere Mandanten und löst GC unabhängig in jedem Mandanten aus. Auf dieser Grundlage unterteilen wir den Speicher in normale Mieter und Mieter mit mittlerem Lebenszyklus. Mittellebige Objekte sind Objekte, die weder flüchtig noch dauerhaft sind. Aufgrund der beiden oben genannten großen Optimierungen sind die Anzahl der Lebenszyklusobjekte und die Speichernutzung im Heap jetzt sehr gering. Objekte mit mittlerem Lebenszyklus werden jedoch bei ihrer Generierung von Objekten des alten Bereichs referenziert, und jeder junge GC muss das RSet scannen, was immer noch der zeitaufwändigste Teil des jungen GC ist.
Mit Hilfe der ObjectTrace-Funktion des AJDK-Teams ermitteln wir den „größten“ Teil der Medium-Life-Cycle-Objekte und ordnen diese Objekte direkt dem alten Bereich des Medium-Life-Cycle-Mandanten zu wenn sie generiert werden, wobei die RSet-Markierung vermieden wird. Normale Mieter weisen den Speicher auf normale Weise zu.
Die GC-Frequenz normaler Mieter ist sehr hoch, aber da es nur wenige beworbene Objekte und wenige generationsübergreifende Referenzen gibt, ist die GC-Zeit in der Young-Zone gut kontrolliert. In der Laborszenensimulationsumgebung haben wir Young GC auf 5 ms optimiert.
(ZenGC-optimierter Effekt, Einheitenproblem, hier sind wir)
Ali-HBase bietet derzeit kommerzielle Dienste in der Alibaba Cloud an. Jeder bedürftige Benutzer kann den stark verbesserten HBase-Dienst aus einer Hand in der Alibaba Cloud nutzen. Im Vergleich zu selbst erstelltem HBase bietet die Cloud-HBase-Version viele Verbesserungen in Bezug auf Betrieb und Wartung, Zuverlässigkeit, Leistung, Stabilität, Sicherheit und Kosten.
Verwandte Artikel:
5 Vorschläge zur Reduzierung des Java-Garbage-Collection-Aufwands
Verwandte Videos:
Das obige ist der detaillierte Inhalt vonAuch die Java-Garbage-Collection-Zeit lässt sich leicht verkürzen. Ein Beispiel erklärt den GC von Ali-HBase.. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!