Dieser Blogbeitrag wurde vor der Fusion mit Cloudera auf Hortonworks.com veröffentlicht. Einige Links, Ressourcen oder Referenzen sind möglicherweise nicht mehr korrekt.
Dieser Blogbeitrag erschien ursprünglich hier und wird hier vollständig wiedergegeben.
HBase ist eine verteilte Datenbank, die auf den Kernkonzepten eines geordneten Schreibprotokolls und eines protokollstrukturierten Zusammenführungsbaums basiert. Wie bei jeder Datenbank ist optimierte E/A ein kritisches Anliegen für HBase. Wenn möglich, besteht die Priorität darin, überhaupt keine E/A auszuführen. Das bedeutet, dass Speicherauslastung und Caching-Strukturen von größter Bedeutung sind. Zu diesem Zweck unterhält HBase zwei Cache-Strukturen:den „Speicher“ und den „Block-Cache“. Speicher, implementiert als MemStore
, sammelt Datenbearbeitungen, sobald sie empfangen werden, und puffert sie im Speicher (1). Der Block-Cache, eine Implementierung von BlockCache
Schnittstelle, hält Datenblöcke im Speicher, nachdem sie gelesen wurden.
Der MemStore
ist wichtig für den Zugriff auf die letzten Änderungen. Ohne MemStore
, würde der Zugriff auf diese Daten, während sie in das Schreibprotokoll geschrieben wurden, das Lesen und Deserialisieren von Einträgen wieder aus dieser Datei erfordern, mindestens ein O(n)
Betrieb. Stattdessen MemStore
unterhält eine Skiplist-Struktur, die ein O(log n)
genießt Zugriffskosten und erfordert keine Platten-I/O. Der MemStore
enthält jedoch nur einen winzigen Teil der in HBase gespeicherten Daten.
Serviert liest aus dem BlockCache
ist der primäre Mechanismus, durch den HBase zufällige Lesevorgänge mit Millisekunden-Latenz bereitstellen kann. Wenn ein Datenblock aus HDFS gelesen wird, wird er im BlockCache
zwischengespeichert . Nachfolgende Lesevorgänge benachbarter Daten – Daten aus demselben Block – erleiden nicht die E/A-Einbuße, diese Daten erneut von der Festplatte abzurufen (2). Es ist der BlockCache
das wird der verbleibende Schwerpunkt dieses Beitrags sein.
Zwischenzuspeichernde Blöcke
Bevor Sie den BlockCache
verstehen , hilft es zu verstehen, was genau ein HBase-„Block“ ist. Im HBase-Kontext ist ein Block eine einzelne E/A-Einheit. Beim Schreiben von Daten in eine HFile ist der Block die kleinste geschriebene Dateneinheit. Ebenso ist ein einzelner Block die kleinste Datenmenge, die HBase aus einer HFile zurücklesen kann. Achten Sie darauf, einen HBase-Block nicht mit einem HDFS-Block oder mit den Blöcken des zugrunde liegenden Dateisystems zu verwechseln – diese sind alle unterschiedlich (3).
HBase-Blöcke gibt es in 4 Varianten: DATA
, META
, INDEX
, und BLOOM
.
DATA
Blöcke speichern Benutzerdaten. Wenn die BLOCKSIZE
für eine Stützenfamilie angegeben ist, ist dies ein Hinweis für diese Art von Block. Wohlgemerkt, es ist nur ein Hinweis. Beim Leeren des MemStore
, HBase wird sein Bestes tun, um diese Richtlinie einzuhalten. Nach jeder Cell
geschrieben wird, prüft der Schreiber, ob der geschriebene Betrag>=der Zielgröße BLOCKSIZE
ist . Wenn dies der Fall ist, wird der aktuelle Block geschlossen und der nächste gestartet (4).
INDEX
und BLOOM
Blöcke dienen demselben Ziel; beide werden verwendet, um den Lesepfad zu beschleunigen. INDEX
Blöcke stellen einen Index über die Cell
bereit s in den DATA
enthalten Blöcke. BLOOM
Blöcke enthalten einen Bloom-Filter über dieselben Daten. Der Index ermöglicht es dem Leser, schnell zu erkennen, wo sich eine Cell
befindet gespeichert werden soll. Der Filter teilt dem Leser mit, wenn eine Cell
fehlt definitiv in den Daten.
Schließlich META
Blöcke speichern Informationen über die HFile selbst und andere verschiedene Informationen – Metadaten, wie Sie vielleicht erwarten. Eine umfassendere Übersicht über die HFile-Formate und die Rollen verschiedener Blocktypen finden Sie in Apache HBase I/O – HFile.
HBase BlockCache und seine Implementierungen
Es gibt einen einzigen BlockCache
Instanz auf einem Regionsserver, was bedeutet, dass alle Daten aus allen Regionen, die von diesem Server gehostet werden, denselben Cache-Pool (5) teilen. Der BlockCache
wird beim Start des Regionsservers instanziiert und bleibt für die gesamte Lebensdauer des Prozesses erhalten. Herkömmlicherweise stellte HBase nur einen einzigen BlockCache
bereit Implementierung:der LruBlockCache
. Die Version 0.92 führte die erste Alternative in HBASE-4027 ein:den SlabCache
. HBase 0.96 führte eine weitere Option über HBASE-7404 ein, die als BucketCache
bezeichnet wird .
Der Hauptunterschied zwischen dem bewährten LruBlockCache
und diese Alternativen sind die Art und Weise, wie sie den Speicher verwalten. Insbesondere LruBlockCache
ist eine Datenstruktur, die sich vollständig auf dem JVM-Heap befindet, während die anderen beiden Speicher von außerhalb des JVM-Heaps nutzen können. Dies ist ein wichtiger Unterschied, da der JVM-Heap-Speicher vom JVM Garbage Collector verwaltet wird, während dies bei den anderen nicht der Fall ist. Im Fall von SlabCache
und BucketCache
, besteht die Idee darin, den GC-Druck zu verringern, dem der Regionsserverprozess ausgesetzt ist, indem die Anzahl der auf dem Heap aufbewahrten Objekte verringert wird.
LruBlockCache
Dies ist die Standardimplementierung. Datenblöcke werden mit dieser Implementierung im JVM-Heap zwischengespeichert. Es ist in drei Bereiche unterteilt:Single-Access, Multi-Access und In-Memory. Die Bereiche haben eine Größe von 25 %, 50 %, 25 % des gesamten BlockCache
Größe (6). Ein anfänglich aus HDFS gelesener Block wird in den Einzelzugriffsbereich gefüllt. Aufeinanderfolgende Zugriffe befördern diesen Block in den Mehrfachzugriffsbereich. Der In-Memory-Bereich ist für Blöcke reserviert, die aus Spaltenfamilien geladen werden, die als IN_MEMORY
gekennzeichnet sind . Unabhängig vom Bereich werden alte Blöcke entfernt, um Platz für neue Blöcke zu schaffen, indem ein Least-Recently-Used-Algorithmus verwendet wird, daher das „Lru“ in „LruBlockCache“.
SlabCache
Diese Implementierung weist mithilfe von DirectByteBuffer
Speicherbereiche außerhalb des JVM-Heaps zu s. Diese Bereiche bilden den Hauptteil dieses BlockCache
. Der genaue Bereich, in dem ein bestimmter Block platziert wird, basiert auf der Größe des Blocks. Standardmäßig werden zwei Bereiche zugewiesen, die 80 % bzw. 20 % der gesamten konfigurierten Off-Heap-Cachegröße verbrauchen. Ersteres wird verwendet, um Blöcke zwischenzuspeichern, die ungefähr der Zielblockgröße (7) entsprechen. Letzteres enthält Blöcke, die ungefähr das Zweifache der Zielblockgröße haben. Ein Block wird in den kleinsten Bereich platziert, in den er passen kann. Wenn der Cache auf einen Block stößt, der größer ist, als in einen der beiden Bereiche passt, wird dieser Block nicht zwischengespeichert. Wie LruBlockCache
, Blockbereinigung wird mithilfe eines LRU-Algorithmus verwaltet.
BucketCache
Diese Implementierung kann so konfiguriert werden, dass sie in einem von drei verschiedenen Modi arbeitet: heap
, offheap
und file
. Unabhängig vom Betriebsmodus wird der BucketCache
verwaltet Speicherbereiche, die als „Buckets“ bezeichnet werden, um zwischengespeicherte Blöcke zu halten. Jeder Bucket wird mit einer Zielblockgröße erstellt. Der heap
die Implementierung erstellt diese Buckets auf dem JVM-Heap; offheap
Implementierung verwendet DirectByteByffers
um Buckets außerhalb des JVM-Heaps zu verwalten; file
mode erwartet einen Pfad zu einer Datei auf dem Dateisystem, in dem die Buckets erstellt werden. file
Der Modus ist für die Verwendung mit einem Sicherungsspeicher mit niedriger Latenz vorgesehen – einem In-Memory-Dateisystem oder vielleicht einer Datei, die sich auf einem SSD-Speicher befindet (8). Unabhängig vom Modus BucketCache
schafft 14 Eimer unterschiedlicher Größe. Es verwendet die Häufigkeit des Blockzugriffs, um die Nutzung zu informieren, genau wie LruBlockCache
, und hat die gleiche Single-Access-, Multi-Access- und In-Memory-Aufteilung von 25 %, 50 %, 25 %. Ebenso wie der Standard-Cache wird die Blockentfernung mit einem LRU-Algorithmus verwaltet.
Mehrstufiges Caching
Sowohl der SlabCache
und BucketCache
wurden entwickelt, um als Teil einer mehrstufigen Caching-Strategie verwendet zu werden. Daher ein Teil des gesamten BlockCache
Größe wird einem LruBlockCache
zugewiesen Beispiel. Diese Instanz fungiert als First-Level-Cache „L1“, während die andere Cache-Instanz als Second-Level-Cache „L2“ behandelt wird. Die Interaktion zwischen LruBlockCache
und SlabCache
unterscheidet sich vom LruBlockCache
und der BucketCache
interagieren.
Der SlabCache
Strategie namens DoubleBlockCache
, besteht darin, Blöcke immer sowohl im L1- als auch im L2-Cache zwischenzuspeichern. Die beiden Cache-Ebenen arbeiten unabhängig voneinander:Beide werden beim Abrufen eines Blocks überprüft und jeder räumt Blöcke ohne Rücksicht auf den anderen. Der BucketCache
Strategie namens CombinedBlockCache
, verwendet den L1-Cache ausschließlich für Bloom- und Index-Blöcke. Datenblöcke werden direkt an den L2-Cache gesendet. Im Falle einer L1-Blockräumung wird dieser Block nicht vollständig verworfen, sondern in den L2-Cache herabgestuft.
Was soll ich wählen?
Es gibt zwei Gründe, einen der alternativen BlockCache
zu aktivieren Implementierungen. Die erste ist einfach die Menge an RAM, die Sie dem Regionsserver zuweisen können. Die Community-Weisheit erkennt an, dass die Obergrenze des JVM-Heaps, soweit es den Regionsserver betrifft, irgendwo zwischen 14 GB und 31 GB liegt (9). Die genaue Grenze hängt normalerweise von einer Kombination aus Hardwareprofil, Clusterkonfiguration, der Form von Datentabellen und Anwendungszugriffsmustern ab. Sie wissen, dass Sie die Gefahrenzone betreten haben, wenn GC pausiert und RegionTooBusyException
s Fangen Sie an, Ihre Protokolle zu überfluten.
Der andere Zeitpunkt, an dem ein alternativer Cache in Betracht gezogen werden sollte, ist, wenn die Antwortlatenz wirklich auftritt Angelegenheiten. Wenn der Heap auf etwa 8-12 GB niedrig gehalten wird, kann der CMS-Sammler sehr reibungslos laufen (10), was sich messbar auf das 99. Perzentil der Antwortzeiten auswirkt. Angesichts dieser Einschränkung besteht die einzige Wahl darin, einen alternativen Garbage Collector zu erkunden oder eine dieser Off-Heap-Implementierungen auszuprobieren.
Diese zweite Option ist genau das, was ich getan habe. In meinem nächsten Beitrag teile ich einige unwissenschaftliche, aber informative Experimentergebnisse, in denen ich die Antwortzeiten für verschiedene BlockCache
vergleiche Implementierungen.
Bleiben Sie wie immer dran und machen Sie weiter mit HBase!
1:Der MemStore
sammelt Datenbearbeitungen, sobald sie empfangen werden, und puffert sie im Speicher. Dies dient zwei Zwecken:Es erhöht die Gesamtmenge der Daten, die in einem einzigen Vorgang auf die Festplatte geschrieben werden, und behält diese letzten Änderungen im Speicher für den späteren Zugriff in Form von Lesevorgängen mit geringer Latenz. Ersteres ist wichtig, da es die HBase-Schreibblöcke ungefähr synchron mit den HDFS-Blockgrößen hält und die HBase-Zugriffsmuster mit dem zugrunde liegenden HDFS-Speicher abgleicht. Letzteres ist selbsterklärend und erleichtert Leseanfragen auf kürzlich geschriebene Daten. Es sei darauf hingewiesen, dass diese Struktur nicht an der Datenbeständigkeit beteiligt ist. Änderungen werden auch in das geordnete Schreibprotokoll, das HLog
, geschrieben , die einen HDFS-Anfügevorgang in einem konfigurierbaren Intervall umfasst, normalerweise sofort.
2:Das erneute Lesen von Daten aus dem lokalen Dateisystem ist das beste Szenario. HDFS ist schließlich ein verteiltes Dateisystem, sodass im schlimmsten Fall dieser Block über das Netzwerk gelesen werden muss. HBase tut sein Bestes, um die Datenlokalität aufrechtzuerhalten. Diese zwei Artikel bieten einen detaillierten Einblick, was Datenlokalität für HBase bedeutet und wie sie verwaltet wird.
3:Dateisystem, HDFS und HBase-Blöcke sind alle unterschiedlich, aber verwandt. Das moderne E/A-Subsystem besteht aus vielen Abstraktionsschichten über der Abstraktion. Kernstück dieser Abstraktion ist das Konzept einer einzelnen Dateneinheit, die als „Block“ bezeichnet wird. Daher definieren alle drei dieser Speicherschichten ihren eigenen Block, jeder mit seiner eigenen Größe. Im Allgemeinen bedeutet eine größere Blockgröße einen erhöhten sequentiellen Zugriffsdurchsatz. Eine kleinere Blockgröße ermöglicht einen schnelleren Direktzugriff.
4:Platzierung der BLOCKSIZE
Überprüfung nach dem Schreiben von Daten hat zwei Auswirkungen. Eine einzelne Cell
ist die kleinste Dateneinheit, die in einen DATA
geschrieben wird Block. Es bedeutet auch eine Cell
kann sich nicht über mehrere Blöcke erstrecken.
5:Dies unterscheidet sich vom MemStore
, für die es eine separate Instanz für jede Region gibt, die vom Regionsserver gehostet wird.
6:Bis vor kurzem waren diese Speicherpartitionen statisch definiert; Es gab keine Möglichkeit, die Aufteilung 25/50/25 zu überschreiben. Ein bestimmtes Segment, beispielsweise der Multi-Access-Bereich, könnte größer werden als seine 50-prozentige Zuteilung, solange die anderen Bereiche nicht ausgelastet sind. Erhöhte Auslastung in den anderen Bereichen wird Einträge aus dem Multi-Access-Bereich vertreiben, bis das 25/50/25-Gleichgewicht erreicht ist. Der Bediener konnte diese Standardgrößen nicht ändern. HBASE-10263, ausgeliefert in HBase 0.98.0, führt Konfigurationsparameter für diese Größen ein. Das flexible Verhalten bleibt erhalten.
7:Das „ungefähr“-Geschäft besteht darin, etwas Spielraum in Blockgrößen zuzulassen. Die HBase-Blockgröße ist ein grobes Ziel oder ein Hinweis, keine streng erzwungene Einschränkung. Die genaue Größe eines bestimmten Datenblocks hängt von der Zielblockgröße und der Größe der Cell
ab darin enthaltenen Werte. Der Blockgrößenhinweis ist als Standardblockgröße von 64 KB angegeben.
8:Verwendung des BucketCache
in file
Der Modus mit einem persistenten Sicherungsspeicher hat einen weiteren Vorteil:Persistenz. Beim Start sucht es nach vorhandenen Daten im Cache und überprüft deren Gültigkeit.
9:So wie ich es verstehe, gibt es zwei Komponenten, die die Obergrenze für diesen Bereich empfehlen. Das erste ist eine Beschränkung der Adressierbarkeit von JVM-Objekten. Die JVM kann auf ein Objekt auf dem Heap mit einer relativen 32-Bit-Adresse anstelle der vollständigen nativen 64-Bit-Adresse verweisen. Diese Optimierung ist nur möglich, wenn die gesamte Heap-Größe weniger als 32 GB beträgt. Weitere Informationen finden Sie unter Komprimierte Oops . Die zweite ist die Fähigkeit des Garbage Collectors, mit der Menge der Objektänderung im System Schritt zu halten. Soweit ich das beurteilen kann, sind die drei Quellen der Objektabwanderung MemStore
, BlockCache
, und Netzwerkbetrieb. Der erste wird durch MemSlab
gemildert Funktion, standardmäßig aktiviert. Die zweite wird durch die Größe Ihres Datensatzes im Vergleich zur Größe des Caches beeinflusst. Dem dritten kann nicht geholfen werden, solange HBase einen Netzwerkstapel verwendet, der auf Datenkopien angewiesen ist.
10:Genau wie bei 8 setzt dies „moderne Hardware“ voraus. Die Interaktionen hier sind ziemlich komplex und gehen weit über den Rahmen eines einzelnen Blogposts hinaus.