HBase
 sql >> Datenbank >  >> NoSQL >> HBase

Optimieren der Java Garbage Collection für HBase

Dieser Gastbeitrag von Intel Java Performance Architect Eric Kaczmarek (ursprünglich hier veröffentlicht) untersucht, wie die Java Garbage Collection (GC) für Apache HBase mit Fokus auf 100 % YCSB-Lesevorgänge optimiert werden kann.

Apache HBase ist ein Open-Source-Projekt von Apache, das NoSQL-Datenspeicherung anbietet. HBase wird häufig zusammen mit HDFS verwendet und ist weltweit weit verbreitet. Bekannte Nutzer sind Facebook, Twitter, Yahoo und mehr. Aus Entwicklersicht ist HBase eine „verteilte, versionierte, nicht-relationale Datenbank nach dem Vorbild von Googles Bigtable, einem verteilten Speichersystem für strukturierte Daten“. HBase kann einen sehr hohen Durchsatz problemlos handhaben, indem es entweder hochskaliert (d. h. Bereitstellung auf einem größeren Server) oder horizontal skaliert wird (d. h. Bereitstellung auf mehr Servern).

Aus Benutzersicht ist die Latenz für jede einzelne Abfrage sehr wichtig. Während wir mit Benutzern zusammenarbeiten, um HBase-Workloads zu testen, abzustimmen und zu optimieren, treffen wir jetzt auf eine beträchtliche Anzahl, die wirklich Betriebslatenzen im 99. Perzentil wollen. Das bedeutet einen Roundtrip, von der Client-Anfrage bis zur Antwort zurück an den Client, alles innerhalb von 100 Millisekunden.

Mehrere Faktoren tragen zu Schwankungen in der Latenz bei. Einer der verheerendsten und unvorhersehbarsten Latenzeindringlinge sind die „Stop the World“-Pausen der Java Virtual Machine (JVM) für die Garbage Collection (Speicherbereinigung).

Um dem entgegenzuwirken, haben wir einige Experimente mit Oracle jdk7u21 und jdk7u60 G1 (Garbage 1st) Collector durchgeführt. Das von uns verwendete Serversystem basierte auf Intel Xeon Ivy-Bridge EP-Prozessoren mit Hyperthreading (40 logische Prozessoren). Es hatte 256 GB DDR3-1600 RAM und drei 400 GB SSDs als lokalen Speicher. Dieses kleine Setup enthielt einen Master und einen Slave, die auf einem einzigen Knoten mit entsprechend skalierter Last konfiguriert waren. Wir haben HBase Version 0.98.1 und ein lokales Dateisystem für die HFile-Speicherung verwendet. Die HBase-Testtabelle wurde mit 400 Millionen Zeilen konfiguriert und hatte eine Größe von 580 GB. Wir haben die standardmäßige HBase-Heap-Strategie verwendet:40 % für Blockcache, 40 % für Memstore. YCSB wurde verwendet, um 600 Arbeitsthreads zu steuern, die Anforderungen an den HBase-Server senden.

Die folgenden Diagramme zeigen, dass jdk7u21 mit -XX:+UseG1GC -Xms100g -Xmx100g -XX:MaxGCPauseMillis=100 eine Stunde lang zu 100 % gelesen wird . Wir haben den zu verwendenden Garbage Collector, die Heap-Größe und die gewünschte Garbage-Collection (GC)-„Stopp-die-Welt“-Pausenzeit angegeben.

Abbildung 1:Wilde Schwankungen in der GC-Pausenzeit

In diesem Fall bekamen wir wild schwingende GC-Pausen. Die GC-Pause hatte einen Bereich von 7 Millisekunden bis 5 vollen Sekunden nach einer anfänglichen Spitze, die bis zu 17,5 Sekunden erreichte.

Das folgende Diagramm zeigt weitere Details im stationären Zustand:

Abbildung 2:GC-Pausendetails im stabilen Zustand

Abbildung 2 zeigt uns, dass die GC-Pausen tatsächlich in drei verschiedenen Gruppen auftreten:(1) zwischen 1 und 1,5 Sekunden; (2) zwischen 0,007 Sekunden und 0,5 Sekunden; (3) Spitzen zwischen 1,5 Sekunden und 5 Sekunden. Das war sehr seltsam, also haben wir das zuletzt veröffentlichte jdk7u60 getestet, um zu sehen, ob die Daten anders wären:

Wir haben dieselben 100 %-Lesetests mit genau denselben JVM-Parametern durchgeführt:-XX:+UseG1GC -Xms100g -Xmx100g -XX:MaxGCPauseMillis=100 .

Abbildung 3:Deutlich verbesserte Handhabung von Pausenzeitspitzen

Jdk7u60 hat die Fähigkeit von G1, Pausenzeitspitzen nach der anfänglichen Spitze während der Einschwingphase zu bewältigen, erheblich verbessert. Jdk7u60 machte 1029 Young und Mixed GCs während eines einstündigen Laufs. GC passiert etwa alle 3,5 Sekunden. Jdk7u21 machte 286 GCs, wobei jede GC etwa alle 12,6 Sekunden stattfand. Jdk7u60 konnte eine Pausenzeit zwischen 0,302 und 1 Sekunde ohne größere Spitzen bewältigen.

Abbildung 4 unten gibt uns einen genaueren Blick auf 150 GC-Pausen im stationären Zustand:

Abbildung 4:Besser, aber nicht gut genug

Im stationären Zustand konnte jdk7u60 die durchschnittliche Pausenzeit bei etwa 369 Millisekunden halten. Es war viel besser als jdk7u21, aber es erfüllte immer noch nicht unsere Anforderung von 100 Millisekunden, die durch –Xx:MaxGCPauseMillis=100 gegeben ist .

Um zu bestimmen, was wir sonst noch tun könnten, um unsere Pausenzeit von 100 Millionen Sekunden zu erreichen, mussten wir mehr über das Verhalten der Speicherverwaltung der JVM und des Garbage Collectors G1 (Garbage First) verstehen. Die folgenden Abbildungen zeigen, wie G1 bei der Sammlung von Young Gen arbeitet.

Abbildung 5:Folie aus der JavaOne-Präsentation 2012 von Charlie Hunt und Monica Beckwith:„G1 Garbage Collector Performance Tuning“

Wenn die JVM startet, fordert sie das Betriebssystem basierend auf den JVM-Startparametern auf, einen großen kontinuierlichen Speicherblock zum Hosten des JVM-Heaps zuzuweisen. Dieser Speicherblock wird von der JVM in Regionen partitioniert.

Abbildung 6:Folie aus der JavaOne-Präsentation 2012 von Charlie Hunt und Monica Beckwith:„G1 Garbage Collector Performance Tuning“

Wie Abbildung 6 zeigt, gelangt jedes Objekt, das das Java-Programm mithilfe der Java-API zuordnet, zuerst in den Eden-Bereich in der Young-Generation auf der linken Seite. Nach einer Weile wird das Eden voll und ein GC der jungen Generation wird ausgelöst. Objekte, auf die noch verwiesen wird (d. h. „lebendig“), werden in den Survivor-Raum kopiert. Wenn Objekte mehrere GCs in der jungen Generation überleben, werden sie in den Bereich der alten Generation befördert.

Wenn Young GC passiert, werden die Threads der Java-Anwendung gestoppt, um Live-Objekte sicher zu markieren und zu kopieren. Diese Stopps sind die berüchtigten „Stop-the-World“-GC-Pausen, die dazu führen, dass die Anwendungen nicht mehr reagieren, bis die Pausen vorbei sind.

Abbildung 7:Folie aus der JavaOne-Präsentation 2012 von Charlie Hunt und Monica Beckwith:„G1 Garbage Collector Performance Tuning“

Die alte Generation kann auch überfüllt werden. Auf einer bestimmten Ebene – gesteuert durch -XX:InitiatingHeapOccupancyPercent=? wobei der Standardwert 45 % des gesamten Heaps beträgt – ein gemischter GC wird ausgelöst. Es sammelt sowohl Young Gen als auch Old Gen. Die gemischten GC-Pausen werden dadurch gesteuert, wie lange die Young-Gen braucht, um aufzuräumen, wenn gemischte GC auftritt.

Wir können also in G1 sehen, dass die „Stop the World“-GC-Pausen davon dominiert werden, wie schnell G1 lebende Objekte aus dem Eden-Raum markieren und kopieren kann. Vor diesem Hintergrund werden wir analysieren, wie uns das HBase-Speicherzuweisungsmuster helfen wird, G1 GC so einzustellen, dass wir unsere gewünschte Pause von 100 Millisekunden erhalten.

In HBase gibt es zwei In-Memory-Strukturen, die den größten Teil des Heaps verbrauchen:Der BlockCache , Zwischenspeichern von HBase-Dateiblöcken für Lesevorgänge und Zwischenspeichern der neuesten Aktualisierungen durch den Memstore.

Abbildung 8:In HBase verbrauchen zwei In-Memory-Strukturen den größten Teil des Heaps.

Die Standardimplementierung von BlockCache von HBase ist der LruBlockCache , das einfach ein großes Byte-Array verwendet, um alle HBase-Blöcke zu hosten. Wenn Blöcke „geräumt“ werden, wird der Verweis auf das Java-Objekt dieses Blocks entfernt, sodass der GC den Speicher verschieben kann.

Neue Objekte, die den LruBlockCache bilden und Memstore Gehen Sie zuerst in den Eden-Raum der jungen Generation. Wenn sie lange genug leben (d. h. wenn sie nicht aus LruBlockCache entfernt werden oder aus Memstore gespült), dann gelangen sie nach mehreren Young-Generationen von GCs zur Old-Generation des Java-Haufens. Wenn der freie Speicherplatz der alten Generation kleiner als ein bestimmter threshOld ist (InitiatingHeapOccupancyPercent zu Beginn) tritt gemischte GC ein und löscht einige tote Objekte in der alten Generation, kopiert lebende Objekte aus der jungen Generation und berechnet das Eden der jungen Generation und den HeapOccupancyPercent der alten Generation neu . Schließlich, wenn HeapOccupancyPercent ein bestimmtes Level erreicht, ein FULL GC passiert, was dazu führt, dass riesige „Stop the World“-GC-Pausen angehalten werden, um alle toten Objekte in der alten Generation zu beseitigen.

Nach dem Studium des GC-Protokolls, das von „-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintAdaptiveSizePolicy “, haben wir HeapOccupancyPercent bemerkt nie groß genug geworden, um eine vollständige GC während des 100 %-HBase-Lesens zu induzieren. Die GC-Pausen, die wir gesehen haben, wurden von Young Gen „Stop the World“-Pausen und der zunehmenden Referenzverarbeitung im Laufe der Zeit dominiert.

Nach Abschluss dieser Analyse haben wir drei Gruppen von Änderungen an der G1-GC-Standardeinstellung vorgenommen:

  1. Verwenden Sie -XX:+ParallelRefProcEnabled Wenn dieses Flag aktiviert ist, verwendet GC mehrere Threads, um die zunehmenden Referenzen während Young und Mixed GC zu verarbeiten. Mit diesem Flag für HBase wird die GC-Anmerkungszeit um 75 % und die gesamte GC-Pausenzeit um 30 % reduziert.
  2. Set -XX:-ResizePLAB and -XX:ParallelGCThreads=8+(logical processors-8)(5/8) Promotion Local Allocation Buffers (PLABs) werden während der Young-Sammlung verwendet. Es werden mehrere Threads verwendet. Jeder Thread muss möglicherweise Speicherplatz für Objekte zuweisen, die entweder im Survivor- oder im alten Speicherplatz kopiert werden. PLABs sind erforderlich, um eine Konkurrenz von Threads um gemeinsam genutzte Datenstrukturen zu vermeiden, die freien Speicher verwalten. Jeder GC-Thread hat ein PLAB für Survival Space und eines für Old Space. Wir möchten die Größenänderung von PLABs stoppen, um die hohen Kommunikationskosten zwischen GC-Threads sowie Variationen während jeder GC zu vermeiden. Wir möchten die Anzahl der GC-Threads auf die Größe festlegen, die durch 8+(logische Prozessoren-8) 5/8). Diese Formel wurde kürzlich von Oracle empfohlen. Mit beiden Einstellungen können wir glattere GC-Pausen während des Laufs sehen.
  3. Ändern Sie -XX:G1NewSizePercent Standardwert von 5 bis 1 für 100-GB-HeapBasierend auf der Ausgabe von -XX:+PrintGCDetails and -XX:+PrintAdaptiveSizePolicy , bemerkten wir, dass der Grund dafür, dass G1 unsere gewünschte Pausenzeit von 100 GC nicht einhalten konnte, die Zeit war, die für die Verarbeitung von Eden benötigt wurde. Mit anderen Worten, G1 brauchte während unserer Tests durchschnittlich 369 Millisekunden, um 5 GB Eden zu leeren. Wir haben dann die Eden-Größe mit -XX:G1NewSizePercent=
    geändert Flag von 5 auf 1 heruntergesetzt. Mit dieser Änderung haben wir festgestellt, dass die GC-Pausenzeit auf 100 Millisekunden reduziert wurde.

Aus diesem Experiment haben wir herausgefunden, dass die G1-Geschwindigkeit zum Bereinigen von Eden etwa 1 GB pro 100 Millisekunden oder 10 GB pro Sekunde für das von uns verwendete HBase-Setup beträgt.

Basierend auf dieser Geschwindigkeit können wir -XX:G1NewSizePercent=
festlegen Die Eden-Größe kann also bei etwa 1 GB gehalten werden. Zum Beispiel:

  • 32 GB Heap, -XX:G1NewSizePercent=3
  • 64 GB Heap, –XX:G1NewSizePercent=2
  • 100 GB und mehr Heap, -XX:G1NewSizePercent=1
  • Unsere letzten Befehlszeilenoptionen für den HRegionserver sind also:
    • -XX:+UseG1GC
    • -Xms100g -Xmx100g (Heap-Größe, die in unseren Tests verwendet wurde)
    • -XX:MaxGCPauseMillis=100 (Gewünschte GC-Pausenzeit in Tests)
    • XX:+ParallelRefProcEnabled
    • -XX:-ResizePLAB
    • -XX:ParallelGCThreads= 8+(40-8)(5/8)=28
    • -XX:G1NewSizePercent=1

Hier ist das GC-Pausenzeitdiagramm für einen 1-stündigen Lesebetrieb mit 100 %:

Abbildung 9:Die höchsten anfänglichen Setzungsspitzen wurden um mehr als die Hälfte reduziert.

In diesem Diagramm wurden selbst die höchsten anfänglichen Einschwingspitzen von 3,792 Sekunden auf 1,684 Sekunden reduziert. Die meisten anfänglichen Spitzen waren weniger als 1 Sekunde. Nach der Abrechnung konnte GC die Pausenzeit bei etwa 100 Millisekunden halten.

Das folgende Diagramm vergleicht jdk7u60-Läufe mit und ohne Tuning im stabilen Zustand:

Abbildung 10:jdk7u60 läuft mit und ohne Tuning im stabilen Zustand.

Die oben beschriebene einfache GC-Abstimmung ergibt ideale GC-Pausenzeiten von etwa 100 Millisekunden mit durchschnittlich 106 Millisekunden und 7 Millisekunden Standardabweichung.

Zusammenfassung

HBase ist eine reaktionszeitkritische Anwendung, bei der die GC-Pausenzeit vorhersehbar und verwaltbar sein muss. Mit Oracle jdk7u60, basierend auf den von -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintAdaptiveSizePolicy gemeldeten GC-Informationen , können wir die GC-Pausenzeit auf die gewünschten 100 Millisekunden reduzieren.

Eric Kaczmarek ist ein Java Performance Architect in Intels Software Solution Group. Er leitet die Bemühungen bei Intel, Big-Data-Frameworks (Hadoop, HBase, Spark, Cassandra) für Intel-Plattformen zu ermöglichen und zu optimieren.

Software und Workloads, die in Leistungstests verwendet werden, wurden möglicherweise nur für die Leistung auf Intel-Mikroprozessoren optimiert. Leistungstests wie SYSmark und MobileMark werden anhand bestimmter Computersysteme, Komponenten, Software, Operationen und Funktionen gemessen. Jede Änderung an einem dieser Faktoren kann dazu führen, dass die Ergebnisse variieren. Sie sollten andere Informationen und Leistungstests konsultieren, um Ihre beabsichtigten Käufe umfassend zu bewerten, einschließlich der Leistung dieses Produkts in Kombination mit anderen Produkten.

Intel-Prozessorzahlen sind kein Maß für die Leistung. Prozessornummern unterscheiden Funktionen innerhalb jeder Prozessorfamilie. Nicht über verschiedene Prozessorfamilien hinweg. Gehen Sie zu:http://www.intel.com/products/processor_number.

Copyright 2014 Intel Corp. Intel, das Intel-Logo und Xeon sind Marken der Intel Corporation in den USA und/oder anderen Ländern.