Wie man die Lesegeschwindigkeit von HBase um das Dreifache und von HDFS um das FĂŒnffache erhöht

Hohe Leistung ist eine der wichtigsten Anforderungen bei der Arbeit mit Big Data. Bei der Datenladeverwaltung in Sber beschĂ€ftigen wir uns mit der Verarbeitung nahezu aller Transaktionen in unsere Datenwolke auf der Basis von Hadoop und haben daher mit wirklich großen Informationsströmen zu tun. NatĂŒrlich sind wir stĂ€ndig auf der Suche nach Möglichkeiten, die Leistung zu steigern, und jetzt möchten wir erzĂ€hlen, wie es gelungen ist, den RegionServer von HBase und den HDFS-Client zu patchen, wodurch die Leseoperation erheblich beschleunigt werden konnte.
Wie man die Lesegeschwindigkeit von HBase um das Dreifache und von HDFS um das FĂŒnffache erhöht

Bevor wir jedoch zu den Details der Verbesserungen kommen, sollten wir ĂŒber die EinschrĂ€nkungen sprechen, die grundsĂ€tzlich nicht umgangen werden können, wenn man auf HDD sitzt.

Warum sind HDDs und schnelle Random Access LesevorgÀnge unvereinbar?
Wie bekannt ist, speichern HBase und viele andere Datenbanken Daten in Blöcken, deren GrĂ¶ĂŸe mehrere Dutzend Kilobyte betrĂ€gt. StandardmĂ€ĂŸig liegt dies bei etwa 64 KB. Stellen wir uns nun vor, dass wir nur 100 Byte abrufen mĂŒssen und wir HBase bitten, uns diese Daten anhand eines bestimmten SchlĂŒssels bereitzustellen. Da die BlockgrĂ¶ĂŸe in HFiles 64 KB betrĂ€gt, wird das Abgerufene 640 mal mehr sein (nur mal so zur Info!), als benötigt wird.

Da die Anfrage ĂŒber HDFS und dessen Mechanismus zur Zwischenspeicherung von Metadaten erfolgt, ShortCircuitCache (der direkten Zugriff auf Dateien ermöglicht), fĂŒhrt dies dazu, dass bereits 1 MB vom Speicher gelesen wird. Das kann jedoch durch den Parameter dfs.client.read.shortcircuit.buffer.size reguliert werden und in vielen FĂ€llen macht es Sinn, diesen Wert zu verringern, zum Beispiel auf 126 KB.

Angenommen, wir tun dies, aber darĂŒber hinaus, wenn wir beginnen, Daten ĂŒber die Java-API zu lesen, beispielsweise mit Funktionen wie FileChannel.read und die Betriebssystem anfordern, die angegebene Datenmenge zu lesen, liest die Betriebssystem 'vorsichtshalber' das doppelte, also 256 KB in unserem Fall. Das passiert, weil es in Java keine einfache Möglichkeit gibt, das Flag FADV_RANDOM zu setzen, das dieses Verhalten verhindert.

In der Folge wird, um unsere 100 Byte zu erhalten, unter der Haube 2600 mal mehr gelesen. Es scheint offensichtlich — die Lösung ist klar, lassen Sie uns die BlockgrĂ¶ĂŸe auf ein Kilobyte reduzieren, das erwĂ€hnte Flag setzen und eine grandiose Beschleunigung erzielen. Aber das Problem ist, dass wir beim Halbieren der BlockgrĂ¶ĂŸe auch die Anzahl der in einer bestimmten Zeit gelesenen Bytes ebenfalls halbieren.

Ein gewisser Gewinn aus dem Setzen der FADV_RANDOM-Flagge kann erzielt werden, jedoch nur bei hoher Multithreading und einer BlockgrĂ¶ĂŸe von 128 KB; dies sind jedoch maximal ein paar Dutzend Prozent:

Wie man die Lesegeschwindigkeit von HBase um das Dreifache und von HDFS um das FĂŒnffache erhöht

Die Tests wurden mit 100 Dateien durchgefĂŒhrt, jede mit einer GrĂ¶ĂŸe von 1 GB und auf 10 HDDs platziert.

Lassen Sie uns berechnen, worauf wir mit dieser Geschwindigkeit grundsÀtzlich hoffen können:
Angenommen, wir lesen von 10 Disks mit einer Geschwindigkeit von 280 MB/s, d.h. 3 Millionen Mal 100 Bytes. Aber wie wir uns erinnern, benötigen wir Daten, die 2600 Mal seltener auftreten als das, was gelesen wird. Daher teilen wir 3 Millionen durch 2600 und erhalten 1100 DatensÀtze pro Sekunde.

Erschreckend, oder? So ist die Natur des Random Access auf HDD — unabhĂ€ngig von der BlockgrĂ¶ĂŸe. Das ist die physikalische Grenze des zufĂ€lligen Zugriffs, und unter diesen Bedingungen kann keine Datenbank mehr extrahieren.

Wie schaffen es die Datenbanken dann, viel höhere Geschwindigkeiten zu erreichen? Um diese Frage zu beantworten, lassen Sie uns anschauen, was im nÀchsten Bild passiert:

Wie man die Lesegeschwindigkeit von HBase um das Dreifache und von HDFS um das FĂŒnffache erhöht

Hier sehen wir, dass die ersten paar Minuten die Geschwindigkeit tatsÀchlich bei etwa tausend DatensÀtzen pro Sekunde liegt. Danach, weil viel mehr gelesen wird als angefordert, gelangen die Daten in den Buff/cache des Betriebssystems (Linux) und die Geschwindigkeit steigt auf annehmbarere 60.000 pro Sekunde.

Daher werden wir uns kĂŒnftig mit der Beschleunigung des Zugriffs nur auf die Daten beschĂ€ftigen, die im Cache des Betriebssystems vorhanden sind oder sich in vergleichbaren schnellen Speichern wie SSD/NVMe befinden.

In unserem Fall werden wir Tests an einem Stand von 4 Servern durchfĂŒhren, die wie folgt konfiguriert sind:

CPU: Xeon E5-2680 v4 @ 2,40 GHz, 64 Threads.
Speicher: 730 GB.
Java-Version: 1.8.0_111

Und hier ist der SchlĂŒsselpunkt — das Volumen der Daten in den Tabellen, die gelesen werden mĂŒssen. Der Punkt ist, dass, wenn Daten aus einer Tabelle gelesen werden, die vollstĂ€ndig in den HBase-Cache passt, es nicht einmal zum Lesen aus dem Buff/cache des Betriebssystems kommt. Denn HBase reserviert standardmĂ€ĂŸig 40% des Speichers fĂŒr eine Struktur, die als BlockCache bezeichnet wird. Im Wesentlichen handelt es sich um ein ConcurrentHashMap, wobei der SchlĂŒssel der Dateiname + Offset des Blocks ist und der Wert die Daten fĂŒr dieses Offset.

Daher, wenn das Lesen nur aus dieser Struktur erfolgt, erleben wir eine hervorragende Geschwindigkeit., etwa eine Million Anfragen pro Sekunde. Aber lassen Sie uns vorstellen, dass wir nicht Hunderte von Gigabyte Speicherplatz nur fĂŒr die Datenbank bereitstellen können, weil auf diesen Servern auch viele andere nĂŒtzliche Anwendungen laufen.

In unserem Fall betrĂ€gt das Volumen des BlockCache auf einem RS etwa 12 GB. Wir haben zwei RS auf einer Node bereitgestellt, d.h. im BlockCache sind insgesamt 96 GB auf allen Nodes reserviert. Die Daten sind jedoch um ein Vielfaches grĂ¶ĂŸer, nehmen wir an, es gibt 4 Tabellen mit 130 Regionen, in denen die Dateien jeweils 800 MB groß sind, komprimiert mit FAST_DIFF, d.h. insgesamt 410 GB (das sind reine Daten, ohne BerĂŒcksichtigung der Replikationsfaktoren).

So macht der BlockCache nur etwa 23% des gesamten Datenvolumens aus, und das ist viel nĂ€her an den realen Bedingungen dessen, was Big Data genannt wird. Und hier wird es interessant – denn offensichtlich, je weniger Cache-Treffer, desto schlechter die Leistung. Im Falle eines Missrats mĂŒssen wir eine Menge Arbeit durchfĂŒhren – d.h. wir mĂŒssen auf systemeigenen Funktionen zugreifen. Doch dem können wir nicht entkommen, schauen wir uns also einen ganz anderen Aspekt an – was passiert mit den Daten innerhalb des Caches?

Vereinfachen wir die Situation und nehmen wir an, wir haben einen Cache, in den nur 1 Objekt passt. Hier ist ein Beispiel dafĂŒr, was passiert, wenn wir mit einem Datenvolumen arbeiten, das dreimal grĂ¶ĂŸer ist als der Cache, wir mĂŒssen:

1. Block 1 in den Cache einfĂŒgen
2. Block 1 aus dem Cache entfernen
3. Block 2 in den Cache einfĂŒgen
4. Block 2 aus dem Cache entfernen
5. Block 3 in den Cache einfĂŒgen

FĂŒnf Aktionen durchgefĂŒhrt! Man kann diese Situation jedoch nicht als normal bezeichnen; im Grunde zwingen wir HBase dazu, eine Menge völlig nutzloser Arbeit zu verrichten. Es liest stĂ€ndig Daten aus dem Betriebssystem-Cache, fĂŒgt sie in den BlockCache ein, nur um sie fast sofort wieder herauszuwerfen, weil eine neue Menge Daten angekommen ist. Die Animation am Anfang des Posts zeigt das Problem – der Garbage Collector ĂŒberlastet, die AtmosphĂ€re erwĂ€rmt sich, die kleine Greta in einem fernen und heißen Schweden ist enttĂ€uscht. Und wir ITler mögen es ĂŒberhaupt nicht, wenn Kinder traurig sind, daher beginnen wir zu ĂŒberlegen, was wir dagegen tun können.

Was wĂ€re, wenn wir nicht alle Blöcke in den Cache einfĂŒgen, sondern nur einen bestimmten Prozentsatz davon, sodass der Cache nicht ĂŒberlĂ€uft? Lassen Sie uns zu Beginn einfach ein paar Zeilen Code zu Beginn der Funktion hinzufĂŒgen, um Daten in den BlockCache einzufĂŒgen:

  public void cacheBlock(BlockCacheKey cacheKey, Cacheable buf, boolean inMemory) {
    if (cacheDataBlockPercent != 100 && buf.getBlockType().isData()) {
      if (cacheKey.getOffset() % 100 >= cacheDataBlockPercent) {
        return;
      }
    }
...

Hier geht es darum, dass der Offset die Position des Blocks in der Datei ist und die letzten Zahlen zufĂ€llig und gleichmĂ€ĂŸig von 00 bis 99 verteilt sind. Daher werden wir nur die ĂŒberspringen, die in den benötigten Bereich fallen.

Nehmen wir an, wir setzen cacheDataBlockPercent = 20 und sehen uns an, was passiert:

Wie man die Lesegeschwindigkeit von HBase um das Dreifache und von HDFS um das FĂŒnffache erhöht

Das Ergebnis ist offensichtlich. In den folgenden Grafiken wird deutlich, wodurch dieser Geschwindigkeitszuwachs zustande kam – wir sparen eine Menge Ressourcen des GC, indem wir uns nicht mit der sinnlosen Arbeit beschĂ€ftigen, Daten in den Cache zu legen, nur um sie sofort wieder an die Mars-RĂŒden zu verschwenden:

Wie man die Lesegeschwindigkeit von HBase um das Dreifache und von HDFS um das FĂŒnffache erhöht

Die CPU-Auslastung steigt dabei, jedoch deutlich weniger als die Leistung:

Wie man die Lesegeschwindigkeit von HBase um das Dreifache und von HDFS um das FĂŒnffache erhöht

Es ist auch erwĂ€hnenswert, dass die in BlockCache gespeicherten Blöcke unterschiedlich sein können. Der Großteil, etwa 95%, sind tatsĂ€chlich Daten. Der Rest sind Metadaten, wie Bloom-Filter oder LEAF_INDEX und usw.. Diese Daten sind zwar gering, aber Ă€ußerst nĂŒtzlich, da HBase, bevor es direkt auf die Daten zugreift, zuerst auf die Metadaten schaut, um festzustellen, ob es weiter suchen muss und wenn ja, wo sich der gesuchte Block genau befindet.

Deshalb sehen wir im Code die Bedingung zur ÜberprĂŒfung buf.getBlockType().isData() und dank dieser Metadaten werden wir auf jeden Fall im Cache bleiben.

Jetzt lassen Sie uns die Last erhöhen und gleichzeitig die Funktion leicht optimieren. Im ersten Test haben wir den Schwellenwert bei 20 gesetzt und der BlockCache war etwas unterausgelastet. Nun setzen wir 23% und fĂŒgen alle 5 Minuten 100 Threads hinzu, um zu sehen, wann die SĂ€ttigung eintritt:

Wie man die Lesegeschwindigkeit von HBase um das Dreifache und von HDFS um das FĂŒnffache erhöht

Hier sehen wir, dass die ursprĂŒngliche Version fast sofort an die Decke bei etwa 100.000 Anfragen pro Sekunde stĂ¶ĂŸt. WĂ€hrend der Patch eine Beschleunigung auf 300.000 ermöglicht. Es ist klar, dass eine weitere Beschleunigung nicht so „kostenlos“ ist, und auch die CPU-Auslastung steigt.

Das ist jedoch keine sehr elegante Lösung, da wir im Voraus nicht wissen, wie viel Prozent der Blöcke zwischengespeichert werden mĂŒssen, das hĂ€ngt vom Lastprofil ab. Daher wurde ein Mechanismus zur automatischen Anpassung dieses Parameters in AbhĂ€ngigkeit von der AktivitĂ€t der LesevorgĂ€nge implementiert.

Um dies zu steuern, wurden drei Parameter hinzugefĂŒgt:

hbase.lru.cache.heavy.eviction.count.limit — legt fest, wie oft der Prozess des Auslagerns von Daten aus dem Cache gestartet werden soll, bevor wir mit der Optimierung beginnen (d.h. Blocke ĂŒberspringen). StandardmĂ€ĂŸig betrĂ€gt es MAX_INT = 2147483647 und bedeutet faktisch, dass die Funktion bei diesem Wert niemals aktiv wird. Der Prozess des Auslagerns wird alle 5 bis 10 Sekunden gestartet (abhĂ€ngig von der Last), und 2147483647 * 10 / 60 / 60 / 24 / 365 = 680 Jahre. Wir können diesen Parameter jedoch auf 0 setzen und die Funktion sofort nach dem Start aktivieren.

Es gibt jedoch auch einen Nutzen in diesem Parameter. Wenn unsere Last so beschaffen ist, dass sich kurzfristige LesevorgĂ€nge (nehmen wir tagsĂŒber) und langfristige (nachts) stĂ€ndig abwechseln, können wir dafĂŒr sorgen, dass die Funktion nur aktiviert wird, wenn lĂ€ngere LesevorgĂ€nge stattfinden.

Zum Beispiel wissen wir, dass kurzfristige LesevorgĂ€nge normalerweise etwa 1 Minute dauern. Wir mĂŒssen also nicht sofort mit dem Auslagern von Blocken beginnen, der Cache wird nicht veraltet und wir können diesen Parameter beispielsweise auf 10 setzen. Das fĂŒhrt dazu, dass die Optimierung nur dann aktiv wird, wenn ein lĂ€ngerer aktiver Lesevorgang begonnen hat, also nach 100 Sekunden. Auf diese Weise gelangen alle Blocke in den Cache und sind verfĂŒgbar (außer denjenigen, die durch den Standardalgorithmus ausgelagert werden). Wenn wir jedoch langfristige LesevorgĂ€nge durchfĂŒhren, wird die Funktion aktiviert und wir haben eine deutlich höhere Leistung.

hbase.lru.cache.heavy.eviction.mb.size.limit — legt fest, wie viele Megabyte wir in den Cache (und natĂŒrlich auslagern) möchten, alle 10 Sekunden. Die Funktion wird versuchen, diesen Wert zu erreichen und aufrechtzuerhalten. Der Sinn ist folgender: Wenn wir Gigabytes in den Cache schaufeln, mĂŒssen wir auch Gigabytes auslagern, was, wie wir oben gesehen haben, ziemlich kostspielig ist. Es ist jedoch nicht ratsam, ihn zu klein einzustellen, da dies zu einem vorzeitigen Verlassen des Block-Überspringmodus fĂŒhren wĂŒrde. FĂŒr leistungsstarke Server (ungefĂ€hr 20-40 physische Kerne) ist es optimal, etwa 300-400 MB einzustellen. FĂŒr Server der Mittelklasse (~10 Kerne) 200-300 MB. FĂŒr schwache Systeme (2-5 Kerne) können 50-100 MB in Ordnung sein (darauf wurde nicht getestet).

Lassen Sie uns ansehen, wie das funktioniert: Angenommen, wir haben hbase.lru.cache.heavy.eviction.mb.size.limit = 500 gesetzt, es gibt eine gewisse Last (Lesen) und dann berechnen wir alle ~10 Sekunden, wie viele Bytes durch die Formel ausgeschlossen wurden:

Overhead = Summe der freigegebenen Bytes (MB) * 100 / Limit (MB) — 100;

Wenn tatsÀchlich 2000 MB ausgeschlossen wurden, ergibt sich folgender Overhead:

2000 * 100 / 500 — 100 = 300%

Die Algorithmen versuchen, nicht mehr als einige Dutzend Prozent zu unterstĂŒtzen, sodass die Funktion den Prozentsatz der zwischengespeicherten Blöcke verringert und somit einen Auto-Tuning-Mechanismus implementiert.

Wenn jedoch die Last gesunken ist und beispielsweise nur 200 MB ausgeschlossen wurden und der Overhead negativ geworden ist (das sogenannte Overshooting):

200 * 100 / 500 — 100 = -60%

Dann wird die Funktion im Gegenteil den Prozentsatz der zwischengespeicherten Blöcke erhöhen, bis der Overhead positiv wird.

Im Folgenden sehen Sie ein Beispiel, wie das mit echten Daten aussieht. Versuchen Sie nicht, 0% zu erreichen, das ist unmöglich. Es ist sehr gut, wenn es etwa 30 — 100% betrĂ€gt, das hilft, einen vorzeitigen Austritt aus dem Optimierungsmodus bei kurzfristigen Spitzen zu vermeiden.

hbase.lru.cache.heavy.eviction.overhead.coefficient — legt fest, wie schnell wir ein Ergebnis erhalten möchten. Wenn wir fest davon ĂŒberzeugt sind, dass unsere LesevorgĂ€nge hauptsĂ€chlich langwierig sind und wir nicht warten möchten, können wir diesen Koeffizienten erhöhen und schneller eine hohe Leistung erzielen.

Wenn wir beispielsweise diesen Koeffizienten auf 0,01 setzen. Das bedeutet, dass der Overhead (siehe oben) mit dieser Zahl multipliziert wird und der Prozentsatz der zwischengespeicherten Blöcke verringert wird. Angenommen, der Overhead betrÀgt 300% und der Koeffizient ist 0,01, dann wird der Prozentsatz der zwischengespeicherten Blöcke um 3% reduziert.

Eine Ă€hnliche Logik von „Backpressure“ ist auch fĂŒr negative Overhead-Werte (Overshooting) implementiert. Da kurzfristige Schwankungen im Volumen von Lese- und AusschlussvorgĂ€ngen immer möglich sind, ermöglicht dieser Mechanismus, einen vorzeitigen Austritt aus dem Optimierungsmodus zu vermeiden. Backpressure hat eine umgekehrte Logik: Je stĂ€rker das Overshooting ist, desto mehr Blöcke werden zwischengespeichert.

Wie man die Lesegeschwindigkeit von HBase um das Dreifache und von HDFS um das FĂŒnffache erhöht

Implementierungscode

        LruBlockCache cache = this.cache.get();
        if (cache == null) {
          break;
        }
        freedSumMb += cache.evict() / 1024 / 1024;
        /*
        * Manchmal lesen wir mehr Daten, als in den BlockCache passen.
        * Das ist die Ursache fĂŒr eine hohe Anzahl von Eviction-Ereignissen.
        * Dies wiederum fĂŒhrt zu einem intensiven Einsatz des Garbage Collectors.
        * Viele Blöcke werden in den BlockCache eingefĂŒgt, aber nie gelesen,
        * wÀhrend sie viele CPU-Ressourcen verbrauchen.
        * Hier analysieren wir, wie viele Bytes freigegeben wurden und entscheiden,
        * ob es an der Zeit ist, die Anzahl der cachen Blöcke zu reduzieren.
        * Das hilft, zu viele Blöcke in den BlockCache zu stecken,
        * wenn evict() sehr aktiv arbeitet und CPU fĂŒr andere Aufgaben zu sparen.
        * Weitere Informationen: https://issues.apache.org/jira/browse/HBASE-23887
        */

        // ZunĂ€chst mĂŒssen wir kontrollieren, wie viel Zeit
        // seit der letzten evict() vergangen ist.
        // Das sollte fast dieselbe Zeit sein (+/- 10s)
        // da wir jedes Mal vergleichbare Volumina an freigegebenen Bytes erhalten.
        // 10s, weil dies der Standardzeitraum fĂŒr die AusfĂŒhrung von evict() ist (siehe oben this.wait)
        long stopTime = System.currentTimeMillis();
        if ((stopTime - startTime) > 1000 * 10 - 1) {
          // Hier mĂŒssen wir berechnen, welche Situation wir haben.
          // Wir haben das Limit "hbase.lru.cache.heavy.eviction.bytes.size.limit"
          // und können die Überlastung darauf berechnen.
          // Wir werden diese Informationen verwenden, um zu entscheiden,
          // wie sich der Prozentsatz der cachen Blöcke Àndern lÀsst.
          freedDataOverheadPercent =
            (int) (freedSumMb * 100 / cache.heavyEvictionMbSizeLimit) - 100;
          if (freedSumMb > cache.heavyEvictionMbSizeLimit) {
            // Jetzt sind wir in einer Situation, in der wir ĂŒber das Limit hinaus sind
            // Aber vielleicht ignorieren wir das, weil es bald enden wird
            heavyEvictionCount++;
            if (heavyEvictionCount > cache.heavyEvictionCountLimit) {
              // Es geht schon lange so und wir mĂŒssen die Caching-
              // Blöcke jetzt reduzieren. Hier berechnen wir, wie viele Blöcke wir ĂŒberspringen möchten.
              // Es hÀngt ab von:
              // 1. Überlastung - wenn die Überlastung groß ist, könnten wir aggressiver
              // die Anzahl der cachen Blöcke reduzieren.
              // 2. Wie schnell wir das Ergebnis möchten. Wenn wir wissen, dass unser
              // schweres Lesen lange dauert, wollen wir nicht warten und können
              // den Koeffizienten erhöhen und eine gute Leistung relativ schnell erhalten.
              // Aber wenn wir uns nicht sicher sind, können wir es langsam angehen und es könnte
              // helfen, einen vorzeitigen Austritt aus diesem Modus zu verhindern. Wenn also der Koeffizient
              // höher ist, erhalten wir eine bessere Leistung, wenn das schwere Lesen stabil ist.
              // Wenn sich das Lesen Àndert, können wir uns anpassen und den
              // Koeffizienten auf einen niedrigeren Wert setzen.
              int change =
                (int) (freedDataOverheadPercent * cache.heavyEvictionOverheadCoefficient);
              // Aber die Praxis hat gezeigt, dass eine Reduzierung um 15% ziemlich ausreichend ist.
              // Wir sind nicht gierig (das könnte zu einem vorzeitigen Austritt fĂŒhren).
              change = Math.min(15, change);
              change = Math.max(0, change); // Ich denke, das wird niemals passieren, aber sicherheitshalber ĂŒberprĂŒfen.
              // Das ist der kritische Punkt, hier reduzieren wir % der cachen Blöcke.
              cache.cacheDataBlockPercent -= change;
              // Wenn wir zu tief sinken, mĂŒssen wir hier aufhören, 1% sollten es auf jeden Fall sein.
              cache.cacheDataBlockPercent = Math.max(1, cache.cacheDataBlockPercent);
            }
          } else {
            // Nun, wir haben eine Überschreitung.
            // Vielleicht ist es nur eine kurzfristige Schwankung und wir können in diesem Modus bleiben.
            // Das hilft, einen vorzeitigen Austritt wÀhrend kurzfristiger Schwankungen zu vermeiden.
            // Wenn die Überschreitung weniger als 90% betrĂ€gt, versuchen wir, den Prozentsatz der
            // cachen Blöcke zu erhöhen und hoffen, dass es genug ist.
            if (freedSumMb >= cache.heavyEvictionMbSizeLimit * 0.1) {
              // Einfache Logik: mehr Überschreitung - mehr cachen Blöcke (Gegenwind)
              int change = (int) (-freedDataOverheadPercent * 0.1 + 1);
              cache.cacheDataBlockPercent += change;
              // Aber es kann nicht mehr als 100% sein, also ĂŒberprĂŒfen wir das.
              cache.cacheDataBlockPercent = Math.min(100, cache.cacheDataBlockPercent);
            } else {
              // Es sieht so aus, als wÀre das schwere Lesen vorbei.
              // Einfach aus diesem Modus aussteigen.
              heavyEvictionCount = 0;
              cache.cacheDataBlockPercent = 100;
            }
          }
          LOG.info("BlockCache evicted (MB): {}, overhead (%): {}, " +
            "heavy eviction counter: {}, " +
            "current caching DataBlock (%): {}",
            freedSumMb, freedDataOverheadPercent,
            heavyEvictionCount, cache.cacheDataBlockPercent);

          freedSumMb = 0;
          startTime = stopTime;
       }

Lassen Sie uns dies nun an einem realen Beispiel betrachten. Wir haben folgendes Testszenarium:

  1. Wir beginnen mit dem Scannen (25 Threads, Batch = 100)
  2. Nach 5 Minuten fĂŒgen wir Multi-Gets hinzu (25 Threads, Batch = 100)
  3. Nach 5 Minuten schalten wir die Multi-Gets aus (es bleibt wieder nur das Scannen)

Wir fĂŒhren zwei DurchlĂ€ufe durch, zuerst hbase.lru.cache.heavy.eviction.count.limit = 10000 (was die Funktion effektiv ausschaltet), danach setzen wir limit = 0 (was sie aktiviert).

In den folgenden Protokollen sehen wir, wie die Funktion aktiviert wird, und das Overshooting auf 14-71% zurĂŒckgesetzt wird. Gelegentlich sinkt die Last, was Backpressure auslöst und HBase erneut mehr Blöcke zwischenspeichert.

RegionServer-Log
evicted (MB): 0, VerhÀltnis 0.0, Overhead (%): -100, schwerer AuslagerungszÀhler: 0, aktuelle Zwischenspeicherung DataBlock (%): 100
evicted (MB): 0, VerhÀltnis 0.0, Overhead (%): -100, schwerer AuslagerungszÀhler: 0, aktuelle Zwischenspeicherung DataBlock (%): 100
evicted (MB): 2170, VerhÀltnis 1.09, Overhead (%): 985, schwerer AuslagerungszÀhler: 1, aktuelle Zwischenspeicherung DataBlock (%): 91 < start
evicted (MB): 3763, VerhÀltnis 1.08, Overhead (%): 1781, schwerer AuslagerungszÀhler: 2, aktuelle Zwischenspeicherung DataBlock (%): 76
evicted (MB): 3306, VerhÀltnis 1.07, Overhead (%): 1553, schwerer AuslagerungszÀhler: 3, aktuelle Zwischenspeicherung DataBlock (%): 61
evicted (MB): 2508, VerhÀltnis 1.06, Overhead (%): 1154, schwerer AuslagerungszÀhler: 4, aktuelle Zwischenspeicherung DataBlock (%): 50
evicted (MB): 1824, VerhÀltnis 1.04, Overhead (%): 812, schwerer AuslagerungszÀhler: 5, aktuelle Zwischenspeicherung DataBlock (%): 42
evicted (MB): 1482, VerhÀltnis 1.03, Overhead (%): 641, schwerer AuslagerungszÀhler: 6, aktuelle Zwischenspeicherung DataBlock (%): 36
evicted (MB): 1140, VerhÀltnis 1.01, Overhead (%): 470, schwerer AuslagerungszÀhler: 7, aktuelle Zwischenspeicherung DataBlock (%): 32
evicted (MB): 913, VerhÀltnis 1.0, Overhead (%): 356, schwerer AuslagerungszÀhler: 8, aktuelle Zwischenspeicherung DataBlock (%): 29
evicted (MB): 912, VerhÀltnis 0.89, Overhead (%): 356, schwerer AuslagerungszÀhler: 9, aktuelle Zwischenspeicherung DataBlock (%): 26
evicted (MB): 684, VerhÀltnis 0.76, Overhead (%): 242, schwerer AuslagerungszÀhler: 10, aktuelle Zwischenspeicherung DataBlock (%): 24
evicted (MB): 684, VerhÀltnis 0.61, Overhead (%): 242, schwerer AuslagerungszÀhler: 11, aktuelle Zwischenspeicherung DataBlock (%): 22
evicted (MB): 456, VerhÀltnis 0.51, Overhead (%): 128, schwerer AuslagerungszÀhler: 12, aktuelle Zwischenspeicherung DataBlock (%): 21
evicted (MB): 456, VerhÀltnis 0.42, Overhead (%): 128, schwerer AuslagerungszÀhler: 13, aktuelle Zwischenspeicherung DataBlock (%): 20
evicted (MB): 456, VerhÀltnis 0.33, Overhead (%): 128, schwerer AuslagerungszÀhler: 14, aktuelle Zwischenspeicherung DataBlock (%): 19
evicted (MB): 342, VerhÀltnis 0.33, Overhead (%): 71, schwerer AuslagerungszÀhler: 15, aktuelle Zwischenspeicherung DataBlock (%): 19
evicted (MB): 342, VerhÀltnis 0.32, Overhead (%): 71, schwerer AuslagerungszÀhler: 16, aktuelle Zwischenspeicherung DataBlock (%): 19
evicted (MB): 342, VerhÀltnis 0.31, Overhead (%): 71, schwerer AuslagerungszÀhler: 17, aktuelle Zwischenspeicherung DataBlock (%): 19
evicted (MB): 228, VerhÀltnis 0.3, Overhead (%): 14, schwerer AuslagerungszÀhler: 18, aktuelle Zwischenspeicherung DataBlock (%): 19
evicted (MB): 228, VerhÀltnis 0.29, Overhead (%): 14, schwerer AuslagerungszÀhler: 19, aktuelle Zwischenspeicherung DataBlock (%): 19
evicted (MB): 228, VerhÀltnis 0.27, Overhead (%): 14, schwerer AuslagerungszÀhler: 20, aktuelle Zwischenspeicherung DataBlock (%): 19
evicted (MB): 228, VerhÀltnis 0.25, Overhead (%): 14, schwerer AuslagerungszÀhler: 21, aktuelle Zwischenspeicherung DataBlock (%): 19
evicted (MB): 228, VerhÀltnis 0.24, Overhead (%): 14, schwerer AuslagerungszÀhler: 22, aktuelle Zwischenspeicherung DataBlock (%): 19
evicted (MB): 228, VerhÀltnis 0.22, Overhead (%): 14, schwerer AuslagerungszÀhler: 23, aktuelle Zwischenspeicherung DataBlock (%): 19
evicted (MB): 228, VerhÀltnis 0.21, Overhead (%): 14, schwerer AuslagerungszÀhler: 24, aktuelle Zwischenspeicherung DataBlock (%): 19
evicted (MB): 228, VerhÀltnis 0.2, Overhead (%): 14, schwerer AuslagerungszÀhler: 25, aktuelle Zwischenspeicherung DataBlock (%): 19
evicted (MB): 228, VerhÀltnis 0.17, Overhead (%): 14, schwerer AuslagerungszÀhler: 26, aktuelle Zwischenspeicherung DataBlock (%): 19
gerĂ€umt (MB): 456, VerhĂ€ltnis 0.17, Overhead (%): 128, schwerer RĂ€umungszĂ€hler: 27, aktueller Cache-Datenblock (%): 18 < hinzugefĂŒgte Abrufe (aber Tabelle gleich)
gerÀumt (MB): 456, VerhÀltnis 0.15, Overhead (%): 128, schwerer RÀumungszÀhler: 28, aktueller Cache-Datenblock (%): 17
gerÀumt (MB): 342, VerhÀltnis 0.13, Overhead (%): 71, schwerer RÀumungszÀhler: 29, aktueller Cache-Datenblock (%): 17
gerÀumt (MB): 342, VerhÀltnis 0.11, Overhead (%): 71, schwerer RÀumungszÀhler: 30, aktueller Cache-Datenblock (%): 17
gerÀumt (MB): 342, VerhÀltnis 0.09, Overhead (%): 71, schwerer RÀumungszÀhler: 31, aktueller Cache-Datenblock (%): 17
gerÀumt (MB): 228, VerhÀltnis 0.08, Overhead (%): 14, schwerer RÀumungszÀhler: 32, aktueller Cache-Datenblock (%): 17
gerÀumt (MB): 228, VerhÀltnis 0.07, Overhead (%): 14, schwerer RÀumungszÀhler: 33, aktueller Cache-Datenblock (%): 17
gerÀumt (MB): 228, VerhÀltnis 0.06, Overhead (%): 14, schwerer RÀumungszÀhler: 34, aktueller Cache-Datenblock (%): 17
gerÀumt (MB): 228, VerhÀltnis 0.05, Overhead (%): 14, schwerer RÀumungszÀhler: 35, aktueller Cache-Datenblock (%): 17
gerÀumt (MB): 228, VerhÀltnis 0.05, Overhead (%): 14, schwerer RÀumungszÀhler: 36, aktueller Cache-Datenblock (%): 17
gerÀumt (MB): 228, VerhÀltnis 0.04, Overhead (%): 14, schwerer RÀumungszÀhler: 37, aktueller Cache-Datenblock (%): 17
gerĂ€umt (MB): 109, VerhĂ€ltnis 0.04, Overhead (%): -46, schwerer RĂ€umungszĂ€hler: 37, aktueller Cache-Datenblock (%): 22 < RĂŒckdruck
gerÀumt (MB): 798, VerhÀltnis 0.24, Overhead (%): 299, schwerer RÀumungszÀhler: 38, aktueller Cache-Datenblock (%): 20
gerÀumt (MB): 798, VerhÀltnis 0.29, Overhead (%): 299, schwerer RÀumungszÀhler: 39, aktueller Cache-Datenblock (%): 18
gerÀumt (MB): 570, VerhÀltnis 0.27, Overhead (%): 185, schwerer RÀumungszÀhler: 40, aktueller Cache-Datenblock (%): 17
gerÀumt (MB): 456, VerhÀltnis 0.22, Overhead (%): 128, schwerer RÀumungszÀhler: 41, aktueller Cache-Datenblock (%): 16
gerÀumt (MB): 342, VerhÀltnis 0.16, Overhead (%): 71, schwerer RÀumungszÀhler: 42, aktueller Cache-Datenblock (%): 16
gerÀumt (MB): 342, VerhÀltnis 0.11, Overhead (%): 71, schwerer RÀumungszÀhler: 43, aktueller Cache-Datenblock (%): 16
gerÀumt (MB): 228, VerhÀltnis 0.09, Overhead (%): 14, schwerer RÀumungszÀhler: 44, aktueller Cache-Datenblock (%): 16
gerÀumt (MB): 228, VerhÀltnis 0.07, Overhead (%): 14, schwerer RÀumungszÀhler: 45, aktueller Cache-Datenblock (%): 16
gerÀumt (MB): 228, VerhÀltnis 0.05, Overhead (%): 14, schwerer RÀumungszÀhler: 46, aktueller Cache-Datenblock (%): 16
gerÀumt (MB): 222, VerhÀltnis 0.04, Overhead (%): 11, schwerer RÀumungszÀhler: 47, aktueller Cache-Datenblock (%): 16
gerÀumt (MB): 104, VerhÀltnis 0.03, Overhead (%): -48, schwerer RÀumungszÀhler: 47, aktueller Cache-Datenblock (%): 21 < unterbrochene Abrufe
gerÀumt (MB): 684, VerhÀltnis 0.2, Overhead (%): 242, schwerer RÀumungszÀhler: 48, aktueller Cache-Datenblock (%): 19
gerÀumt (MB): 570, VerhÀltnis 0.23, Overhead (%): 185, schwerer RÀumungszÀhler: 49, aktueller Cache-Datenblock (%): 18
gerÀumt (MB): 342, VerhÀltnis 0.22, Overhead (%): 71, schwerer RÀumungszÀhler: 50, aktueller Cache-Datenblock (%): 18
gerÀumt (MB): 228, VerhÀltnis 0.21, Overhead (%): 14, schwerer RÀumungszÀhler: 51, aktueller Cache-Datenblock (%): 18
gerÀumt (MB): 228, VerhÀltnis 0.2, Overhead (%): 14, schwerer RÀumungszÀhler: 52, aktueller Cache-Datenblock (%): 18
gerÀumt (MB): 228, VerhÀltnis 0.18, Overhead (%): 14, schwerer RÀumungszÀhler: 53, aktueller Cache-Datenblock (%): 18
gerÀumt (MB): 228, VerhÀltnis 0.16, Overhead (%): 14, schwerer RÀumungszÀhler: 54, aktueller Cache-Datenblock (%): 18
gerÀumt (MB): 228, VerhÀltnis 0.14, Overhead (%): 14, schwerer RÀumungszÀhler: 55, aktueller Cache-Datenblock (%): 18
gerĂ€umt (MB): 112, VerhĂ€ltnis 0.14, Overhead (%): -44, schwerer RĂ€umungszĂ€hler: 55, aktueller Cache-Datenblock (%): 23 < RĂŒckdruck
gerÀumt (MB): 456, VerhÀltnis 0.26, Overhead (%): 128, schwerer RÀumungszÀhler: 56, aktueller Cache-Datenblock (%): 22
gerÀumt (MB): 342, VerhÀltnis 0.31, Overhead (%): 71, schwerer RÀumungszÀhler: 57, aktueller Cache-Datenblock (%): 22
ausgegliedert (MB): 342, VerhÀltnis 0.33, Overhead (%): 71, schwerer AusgliederungszÀhler: 58, aktueller Cache-Datenblock (%): 22
ausgegliedert (MB): 342, VerhÀltnis 0.33, Overhead (%): 71, schwerer AusgliederungszÀhler: 59, aktueller Cache-Datenblock (%): 22
ausgegliedert (MB): 342, VerhÀltnis 0.33, Overhead (%): 71, schwerer AusgliederungszÀhler: 60, aktueller Cache-Datenblock (%): 22
ausgegliedert (MB): 342, VerhÀltnis 0.33, Overhead (%): 71, schwerer AusgliederungszÀhler: 61, aktueller Cache-Datenblock (%): 22
ausgegliedert (MB): 342, VerhÀltnis 0.33, Overhead (%): 71, schwerer AusgliederungszÀhler: 62, aktueller Cache-Datenblock (%): 22
ausgegliedert (MB): 342, VerhÀltnis 0.33, Overhead (%): 71, schwerer AusgliederungszÀhler: 63, aktueller Cache-Datenblock (%): 22
ausgegliedert (MB): 342, VerhÀltnis 0.32, Overhead (%): 71, schwerer AusgliederungszÀhler: 64, aktueller Cache-Datenblock (%): 22
ausgegliedert (MB): 342, VerhÀltnis 0.33, Overhead (%): 71, schwerer AusgliederungszÀhler: 65, aktueller Cache-Datenblock (%): 22
ausgegliedert (MB): 342, VerhÀltnis 0.33, Overhead (%): 71, schwerer AusgliederungszÀhler: 66, aktueller Cache-Datenblock (%): 22
ausgegliedert (MB): 342, VerhÀltnis 0.32, Overhead (%): 71, schwerer AusgliederungszÀhler: 67, aktueller Cache-Datenblock (%): 22
ausgegliedert (MB): 342, VerhÀltnis 0.33, Overhead (%): 71, schwerer AusgliederungszÀhler: 68, aktueller Cache-Datenblock (%): 22
ausgegliedert (MB): 342, VerhÀltnis 0.32, Overhead (%): 71, schwerer AusgliederungszÀhler: 69, aktueller Cache-Datenblock (%): 22
ausgegliedert (MB): 342, VerhÀltnis 0.32, Overhead (%): 71, schwerer AusgliederungszÀhler: 70, aktueller Cache-Datenblock (%): 22
ausgegliedert (MB): 342, VerhÀltnis 0.33, Overhead (%): 71, schwerer AusgliederungszÀhler: 71, aktueller Cache-Datenblock (%): 22
ausgegliedert (MB): 342, VerhÀltnis 0.33, Overhead (%): 71, schwerer AusgliederungszÀhler: 72, aktueller Cache-Datenblock (%): 22
ausgegliedert (MB): 342, VerhÀltnis 0.33, Overhead (%): 71, schwerer AusgliederungszÀhler: 73, aktueller Cache-Datenblock (%): 22
ausgegliedert (MB): 342, VerhÀltnis 0.33, Overhead (%): 71, schwerer AusgliederungszÀhler: 74, aktueller Cache-Datenblock (%): 22
ausgegliedert (MB): 342, VerhÀltnis 0.33, Overhead (%): 71, schwerer AusgliederungszÀhler: 75, aktueller Cache-Datenblock (%): 22
ausgegliedert (MB): 342, VerhÀltnis 0.33, Overhead (%): 71, schwerer AusgliederungszÀhler: 76, aktueller Cache-Datenblock (%): 22
ausgegliedert (MB): 21, VerhÀltnis 0.33, Overhead (%): -90, schwerer AusgliederungszÀhler: 76, aktueller Cache-Datenblock (%): 32
evicted (MB): 0, VerhÀltnis 0.0, Overhead (%): -100, schwerer AuslagerungszÀhler: 0, aktuelle Zwischenspeicherung DataBlock (%): 100
evicted (MB): 0, VerhÀltnis 0.0, Overhead (%): -100, schwerer AuslagerungszÀhler: 0, aktuelle Zwischenspeicherung DataBlock (%): 100

Die Scans waren notwendig, um denselben Prozess als Diagramm des VerhĂ€ltnisses zwischen zwei Cache-Bereichen darzustellen – single (wohin Blöcke gehen, die noch niemand angefordert hat) und multi (wo Daten gespeichert werden, die mindestens einmal „angefordert“ wurden):

Wie man die Lesegeschwindigkeit von HBase um das Dreifache und von HDFS um das FĂŒnffache erhöht

Und schließlich, wie die Parameter in Form eines Diagramms aussehen. Zum Vergleich wurde der Cache zu Beginn ganz deaktiviert, dann wurde HBase mit Caching und einer Verzögerung des Optimierungsbeginns um 5 Minuten (30 Ausgliederungszyklen) gestartet.

Der vollstÀndige Code ist im Pull Request zu finden. HBASE 23887 auf github.

Jedoch sind 300.000 LesevorgĂ€nge pro Sekunde nicht alles, was man auf dieser Hardware unter diesen Bedingungen herausholen kann. Das liegt daran, dass bei einem Datenzugriff ĂŒber HDFS der ShortCircuitCache (im Folgenden SSC) verwendet wird, der einen direkten Zugriff auf die Daten ermöglicht, um Netzwerkinteraktionen zu vermeiden.

Die Profilierung hat gezeigt, dass dieser Mechanismus zwar große Vorteile bietet, aber auch irgendwann zum Engpass wird, da praktisch alle schweren Operationen innerhalb des Locks stattfinden, was zu Blockierungen fĂŒhrt, die die meiste Zeit in Anspruch nehmen.

Wie man die Lesegeschwindigkeit von HBase um das Dreifache und von HDFS um das FĂŒnffache erhöht

Das haben wir erkannt und verstanden, dass man das Problem umgehen kann, wenn man ein Array unabhÀngiger SSCs erstellt:

private final ShortCircuitCache[] shortCircuitCache;
...
shortCircuitCache = new ShortCircuitCache[this.clientShortCircuitNum];
for (int i = 0; i < this.clientShortCircuitNum; i++)
  this.shortCircuitCache[i] = new ShortCircuitCache(
);

Und anschließend mit ihnen arbeiten, dabei auch Überschneidungen nach der letzten Ziffer des Offsets auszuschließen:

public ShortCircuitCache getShortCircuitCache(long idx) {
    return shortCircuitCache[(int) (idx % clientShortCircuitNum)];
}

Jetzt können wir mit den Tests beginnen. DafĂŒr werden wir Dateien aus HDFS mit einer einfachen Multithread-Anwendung lesen. Wir setzen die Parameter:

conf.set("dfs.client.read.shortcircuit", "true");
conf.set("dfs.client.read.shortcircuit.buffer.size", "65536"); // standardmĂ€ĂŸig = 1 MB und das verlangsamt das Lesen erheblich, daher ist es besser, es an die tatsĂ€chlichen BedĂŒrfnisse anzupassen
conf.set("dfs.client.short.circuit.num", num); // von 1 bis 10

Und einfach die Dateien lesen:

FSDataInputStream in = fileSystem.open(path);
for (int i = 0; i  900000000)
        position = 0L;
    int res = in.read(position, byteBuffer, 0, 65536);
}

Dieser Code wird in separaten Threads ausgefĂŒhrt und wir werden die Anzahl der gleichzeitig zu lesenden Dateien (von 10 bis 200 — horizontale Achse) und die Anzahl der Caches (von 1 bis 10 — Grafiken) erhöhen. Die vertikale Achse zeigt die Beschleunigung, die durch die Erhöhung der SSC im Vergleich zu dem Fall erzielt wird, wenn der Cache nur einmal vorhanden ist.

Wie man die Lesegeschwindigkeit von HBase um das Dreifache und von HDFS um das FĂŒnffache erhöht

So liest man das Diagramm: Die AusfĂŒhrungszeit von 100.000 LesevorgĂ€ngen in Blöcken von 64 KB mit einem Cache benötigt 78 Sekunden. WĂ€hrend dies mit 5 Caches in 16 Sekunden ausgefĂŒhrt wird. Das heißt, es gibt eine Beschleunigung von ~5 mal. Wie aus dem Diagramm ersichtlich ist, ist die Wirkung bei einer kleinen Anzahl paralleler LesevorgĂ€nge nicht sehr ausgeprĂ€gt; sie spielt eine spĂŒrbare Rolle, wenn die Anzahl der LesevorgĂ€nge ĂŒber 50 liegt. Außerdem ist klar, dass eine Erhöhung der Anzahl der SSC von 6 und höher zu einem deutlich geringeren Leistungszuwachs fĂŒhrt.

Anmerkung 1: Da die Testergebnisse ziemlich volatil sind (siehe unten), wurden 3 DurchlĂ€ufe durchgefĂŒhrt und die erhaltenen Werte wurden gemittelt.

Anmerkung 2: Der Leistungszuwachs durch Einstellungen fĂŒr den zufĂ€lligen Zugriff ist gleich, obwohl der Zugriff selbst etwas langsamer ist.

Es ist jedoch wichtig zu klÀren, dass im Gegensatz zum Fall von HBase diese Beschleunigung nicht immer kostenlos ist. Hier "entsperren" wir eher die Möglichkeiten des CPUs, um die Arbeit zu erledigen, anstatt ihn bei Locks hÀngen zu lassen.

Wie man die Lesegeschwindigkeit von HBase um das Dreifache und von HDFS um das FĂŒnffache erhöht

Hier kann man beobachten, dass die Erhöhung der Anzahl an Caches insgesamt zu einem proportionalen Anstieg der CPU-Auslastung fĂŒhrt. Es gibt jedoch mehrere vorteilhaftere Kombinationen.

Schauen wir uns zum Beispiel die Einstellung SSC = 3 genauer an. Der Leistungsschub im Bereich betrÀgt etwa das 3,3-fache. Unten sind die Ergebnisse aller drei einzelnen DurchlÀufe.

Wie man die Lesegeschwindigkeit von HBase um das Dreifache und von HDFS um das FĂŒnffache erhöht

WĂ€hrend der CPU-Verbrauch um etwa das 2,8-fache steigt. Der Unterschied ist nicht sehr groß, aber die kleine Greta hat bereits Freude daran und es könnte Zeit fĂŒr den Besuch der Schule und den Unterricht entstehen.

Somit wird dies einen positiven Effekt fĂŒr jedes Werkzeug haben, das massiven Zugriff auf HDFS nutzt (zum Beispiel Spark usw.), vorausgesetzt, der Anwendungscode ist leicht (d.h. der Flaschenhals liegt tatsĂ€chlich auf der Client-Seite von HDFS) und es sind freie CPU-Ressourcen vorhanden. Zur ÜberprĂŒfung lassen Sie uns testen, welchen Effekt die gemeinsame Anwendung der Optimierung von BlockCache und des Tuning von SSC beim Lesen aus HBase haben wird.

Wie man die Lesegeschwindigkeit von HBase um das Dreifache und von HDFS um das FĂŒnffache erhöht

Hier ist zu sehen, dass in solchen Bedingung der Effekt nicht so groß ist wie bei verfeinerten Tests (Lesen ohne jegliche Verarbeitung), jedoch können hier zusĂ€tzliche 80K durchaus herausgeholt werden. Gemeinsam fĂŒhren beide Optimierungen zu einer Beschleunigung von bis zu 4-fach.

Zur Optimierung wurde auch ein PR erstellt [HDFS-15202], der integriert wurde und diese FunktionalitĂ€t wird in den kommenden Veröffentlichungen verfĂŒgbar sein.

Und schließlich war es interessant, die Leseleistung einer Ă€hnlichen Wide-Column-Datenbank Cassandra mit HBase zu vergleichen.

DafĂŒr wurden Instanzen des Standard-Stresstests YCSB von zwei Hosts aus gestartet (insgesamt 800 Threads). Auf der Serverseite liefen jeweils 4 Instanzen von RegionServer und Cassandra auf 4 Hosts (nicht die, auf denen die Clients laufen, um deren Einfluss zu vermeiden). Die LesevorgĂ€nge erfolgten aus Tabellen mit folgenden GrĂ¶ĂŸen:

HBase — 300 GB auf HDFS (100 GB reiner Daten)

Cassandra — 250 GB (Replikationsfaktor = 3)

Das Volumen war also ziemlich Àhnlich (bei HBase etwas mehr).

Parameter von HBase:

dfs.client.short.circuit.num = 5 (Optimierung des HDFS-Clients)

hbase.lru.cache.heavy.eviction.count.limit = 30 — das bedeutet, dass der Patch nach 30 Auslagerungen (~5 Minuten) wirksam wird.

hbase.lru.cache.heavy.eviction.mb.size.limit = 300 — ZielgrĂ¶ĂŸe fĂŒr Cache und Auslagerung.

Die Logs von YCSB wurden geparsed und in Excel-Diagramme ĂŒberfĂŒhrt:

Wie man die Lesegeschwindigkeit von HBase um das Dreifache und von HDFS um das FĂŒnffache erhöht

Wie ersichtlich ist, ermöglichen die Optimierungsdaten einen Vergleich der LeistungsfÀhigkeit dieser Datenbanken unter diesen Bedingungen und erreichen 450.000 LesevorgÀnge pro Sekunde.

Wir hoffen, dass diese Informationen fĂŒr jemanden wĂ€hrend des spannenden Wettkampfs um die Leistung nĂŒtzlich sein können.

Quelle: habr.com

60GB SSD 8Gb DDR4