So erhöhen Sie die Lesegeschwindigkeit von HBase bis zum Dreifachen und von HDFS bis zum Fünffachen

Hohe Leistung ist eine der zentralen Anforderungen bei der Arbeit mit Big Data. In der Datenladeabteilung der Sberbank pumpen wir fast alle Transaktionen in unsere Hadoop-basierte Daten-Cloud und verarbeiten daher wirklich große Informationsströme. Natürlich suchen wir immer nach Möglichkeiten, die Leistung zu verbessern, und jetzt möchten wir Ihnen erzählen, wie es uns gelungen ist, RegionServer HBase und den HDFS-Client zu patchen, wodurch wir die Geschwindigkeit der Lesevorgänge deutlich steigern konnten.
So erhöhen Sie die Lesegeschwindigkeit von HBase bis zum Dreifachen und von HDFS bis zum Fünffachen

Bevor wir uns jedoch dem Kern der Verbesserungen zuwenden, lohnt es sich, über Einschränkungen zu sprechen, die grundsätzlich nicht umgangen werden können, wenn man auf einer Festplatte sitzt.

Warum Festplatte und schnelle Lesevorgänge mit wahlfreiem Zugriff nicht kompatibel sind
Wie Sie wissen, speichern HBase und viele andere Datenbanken Daten in Blöcken mit einer Größe von mehreren zehn Kilobyte. Standardmäßig sind es etwa 64 KB. Stellen wir uns nun vor, dass wir nur 100 Byte benötigen und HBase bitten, uns diese Daten mit einem bestimmten Schlüssel zu geben. Da die Blockgröße in HFiles 64 KB beträgt, ist die Anfrage 640-mal größer (nur eine Minute!) als nötig.

Als nächstes wird die Anfrage HDFS und seinen Metadaten-Caching-Mechanismus durchlaufen ShortCircuitCache (was den direkten Zugriff auf Dateien ermöglicht), führt dies dazu, dass bereits 1 MB von der Festplatte gelesen werden. Dies kann jedoch mit dem Parameter angepasst werden dfs.client.read.shortCircuit.buffer.size und in vielen Fällen ist es sinnvoll, diesen Wert zu reduzieren, beispielsweise auf 126 KB.

Nehmen wir an, wir tun dies, aber wenn wir außerdem mit dem Lesen von Daten über die Java-API beginnen, z. B. Funktionen wie FileChannel.read, und das Betriebssystem auffordern, die angegebene Datenmenge zu lesen, liest es „nur für den Fall“ zweimal mehr , d.h. In unserem Fall 2 KB. Dies liegt daran, dass Java keine einfache Möglichkeit hat, das FADV_RANDOM-Flag zu setzen, um dieses Verhalten zu verhindern.

Um unsere 100 Bytes zu erhalten, wird daher 2600-mal mehr unter der Haube gelesen. Es scheint, dass die Lösung offensichtlich ist: Reduzieren wir die Blockgröße auf ein Kilobyte, setzen wir das erwähnte Flag und erzielen wir eine große Aufklärungsbeschleunigung. Das Problem besteht jedoch darin, dass wir durch die Reduzierung der Blockgröße um das Zweifache auch die Anzahl der pro Zeiteinheit gelesenen Bytes um das Zweifache reduzieren.

Durch das Setzen des FADV_RANDOM-Flags kann ein gewisser Gewinn erzielt werden, jedoch nur mit hohem Multithreading und einer Blockgröße von 128 KB, die jedoch maximal einige zehn Prozent beträgt:

So erhöhen Sie die Lesegeschwindigkeit von HBase bis zum Dreifachen und von HDFS bis zum Fünffachen

Die Tests wurden mit 100 Dateien durchgeführt, die jeweils 1 GB groß waren und sich auf 10 Festplatten befanden.

Berechnen wir, womit wir bei dieser Geschwindigkeit grundsätzlich rechnen können:
Nehmen wir an, wir lesen von 10 Festplatten mit einer Geschwindigkeit von 280 MB/s, d. h. 3 Millionen mal 100 Bytes. Aber wie wir uns erinnern, sind die Daten, die wir benötigen, 2600-mal kleiner als die gelesenen Daten. Wir dividieren also 3 Millionen durch 2600 und erhalten 1100 Datensätze pro Sekunde.

Deprimierend, nicht wahr? Das ist die Natur Direktzugriff Zugriff auf Daten auf der Festplatte – unabhängig von der Blockgröße. Dies ist die physische Grenze des Direktzugriffs, und unter solchen Bedingungen kann keine Datenbank mehr herausholen.

Wie erreichen Datenbanken dann viel höhere Geschwindigkeiten? Um diese Frage zu beantworten, schauen wir uns an, was im folgenden Bild passiert:

So erhöhen Sie die Lesegeschwindigkeit von HBase bis zum Dreifachen und von HDFS bis zum Fünffachen

Hier sehen wir, dass die Geschwindigkeit in den ersten Minuten tatsächlich bei etwa tausend Datensätzen pro Sekunde liegt. Da jedoch viel mehr gelesen wird, als angefordert wurde, landen die Daten im Buff/Cache des Betriebssystems (Linux) und die Geschwindigkeit steigt auf ordentlichere 60 pro Sekunde

Daher werden wir uns weiter mit der Beschleunigung des Zugriffs nur auf die Daten befassen, die sich im Betriebssystem-Cache oder auf SSD/NVMe-Speichergeräten mit vergleichbarer Zugriffsgeschwindigkeit befinden.

In unserem Fall führen wir Tests auf einer Bank mit 4 Servern durch, die jeweils wie folgt berechnet werden:

CPU: Xeon E5-2680 v4 bei 2.40 GHz 64 Threads.
Speicher: 730 GB.
Java-Version: 1.8.0_111

Und hier ist der entscheidende Punkt die Datenmenge in den Tabellen, die gelesen werden muss. Tatsache ist, dass, wenn Sie Daten aus einer Tabelle lesen, die vollständig im HBase-Cache liegt, es nicht einmal zum Lesen aus dem Buff/Cache des Betriebssystems kommt. Weil HBase standardmäßig 40 % des Speichers einer Struktur namens BlockCache zuweist. Im Wesentlichen handelt es sich hierbei um eine ConcurrentHashMap, bei der der Schlüssel der Dateiname + Offset des Blocks und der Wert die tatsächlichen Daten an diesem Offset sind.

Wenn wir also nur aus dieser Struktur lesen, haben wir Wir sehen eine hervorragende Geschwindigkeit, etwa eine Million Anfragen pro Sekunde. Aber stellen wir uns vor, dass wir nicht Hunderte von Gigabyte Speicher nur für den Datenbankbedarf zuweisen können, da auf diesen Servern noch viele andere nützliche Dinge laufen.

In unserem Fall beträgt das BlockCache-Volumen auf einem RS beispielsweise etwa 12 GB. Wir haben zwei RS auf einem Knoten gelandet, d.h. Auf allen Knoten sind 96 GB für BlockCache reserviert. Und es gibt ein Vielfaches an Daten, zum Beispiel seien es 4 Tabellen mit jeweils 130 Regionen, in denen Dateien eine Größe von 800 MB haben, komprimiert durch FAST_DIFF, d. h. insgesamt 410 GB (das sind reine Daten, also ohne Berücksichtigung des Replikationsfaktors).

Somit macht BlockCache nur etwa 23 % des gesamten Datenvolumens aus und das kommt den realen Bedingungen von BigData deutlich näher. Und hier beginnt der Spaß – denn je weniger Cache-Hits, desto schlechter die Leistung. Denn wer es verfehlt, muss viel Arbeit leisten – d.h. Gehen Sie zum Aufrufen von Systemfunktionen über. Dies lässt sich jedoch nicht vermeiden, also schauen wir uns einen ganz anderen Aspekt an: Was passiert mit den Daten im Cache?

Vereinfachen wir die Situation und gehen wir davon aus, dass wir einen Cache haben, der nur für ein Objekt geeignet ist. Hier ist ein Beispiel dafür, was passiert, wenn wir versuchen, mit einem Datenvolumen zu arbeiten, das dreimal größer als der Cache ist. Wir müssen Folgendes tun:

1. Platzieren Sie Block 1 im Cache
2. Entfernen Sie Block 1 aus dem Cache
3. Platzieren Sie Block 2 im Cache
4. Entfernen Sie Block 2 aus dem Cache
5. Platzieren Sie Block 3 im Cache

5 Aktionen abgeschlossen! Diese Situation kann jedoch nicht als normal bezeichnet werden; tatsächlich zwingen wir HBase zu einer Menge völlig nutzloser Arbeit. Es liest ständig Daten aus dem Betriebssystem-Cache, legt sie im BlockCache ab, verwirft sie jedoch fast sofort wieder, weil ein neuer Teil der Daten eingetroffen ist. Die Animation am Anfang des Beitrags zeigt den Kern des Problems: Garbage Collector gerät aus den Fugen, die Atmosphäre heizt sich auf, die kleine Greta im fernen und heißen Schweden ist unruhig. Und wir IT-Leute mögen es wirklich nicht, wenn Kinder traurig sind, also fangen wir an, darüber nachzudenken, was wir dagegen tun können.

Was wäre, wenn Sie nicht alle Blöcke, sondern nur einen bestimmten Prozentsatz davon in den Cache legen, damit der Cache nicht überläuft? Beginnen wir damit, einfach ein paar Codezeilen am Anfang der Funktion hinzuzufügen, um Daten in BlockCache einzufügen:

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

Der Punkt hier ist der Folgende: Offset ist die Position des Blocks in der Datei und seine letzten Ziffern sind zufällig und gleichmäßig von 00 bis 99 verteilt. Daher werden wir nur diejenigen überspringen, die in den von uns benötigten Bereich fallen.

Legen Sie beispielsweise „cacheDataBlockPercent = 20“ fest und sehen Sie, was passiert:

So erhöhen Sie die Lesegeschwindigkeit von HBase bis zum Dreifachen und von HDFS bis zum Fünffachen

Das Ergebnis ist offensichtlich. In den folgenden Grafiken wird deutlich, warum es zu einer solchen Beschleunigung kam – wir sparen eine Menge GC-Ressourcen, ohne die Sisyphusarbeit zu leisten, Daten in den Cache zu legen, nur um sie dann sofort in den Abfluss der Marshunde zu werfen:

So erhöhen Sie die Lesegeschwindigkeit von HBase bis zum Dreifachen und von HDFS bis zum Fünffachen

Gleichzeitig steigt die CPU-Auslastung, ist jedoch deutlich geringer als die Produktivität:

So erhöhen Sie die Lesegeschwindigkeit von HBase bis zum Dreifachen und von HDFS bis zum Fünffachen

Es ist auch erwähnenswert, dass die im BlockCache gespeicherten Blöcke unterschiedlich sind. Der größte Teil, etwa 95 %, sind Daten selbst. Und der Rest sind Metadaten, wie Bloom-Filter oder LEAF_INDEX und usw.. Diese Daten reichen nicht aus, sind aber sehr nützlich, denn bevor HBase direkt auf die Daten zugreift, greift es auf die Meta zurück, um zu verstehen, ob hier weiter gesucht werden muss und wenn ja, wo genau sich der interessierende Block befindet.

Daher sehen wir im Code eine Prüfbedingung buf.getBlockType().isData() und dank dieses Metas werden wir es auf jeden Fall im Cache belassen.

Jetzt erhöhen wir die Last und verschärfen die Funktion auf einmal etwas. Im ersten Test haben wir den Cutoff-Prozentsatz auf 20 festgelegt und BlockCache wurde leicht nicht ausgelastet. Stellen wir den Wert nun auf 23 % ein und fügen alle 100 Minuten 5 Threads hinzu, um zu sehen, an welchem ​​Punkt die Sättigung auftritt:

So erhöhen Sie die Lesegeschwindigkeit von HBase bis zum Dreifachen und von HDFS bis zum Fünffachen

Hier sehen wir, dass die Originalversion mit etwa 100 Anfragen pro Sekunde fast sofort die Obergrenze erreicht. Während der Patch eine Beschleunigung von bis zu 300 ermöglicht. Gleichzeitig ist klar, dass eine weitere Beschleunigung nicht mehr so ​​„kostenlos“ ist, auch die CPU-Auslastung steigt.

Dies ist jedoch keine sehr elegante Lösung, da wir nicht im Voraus wissen, wie viel Prozent der Blöcke zwischengespeichert werden müssen, es hängt vom Lastprofil ab. Daher wurde ein Mechanismus implementiert, um diesen Parameter abhängig von der Aktivität der Lesevorgänge automatisch anzupassen.

Um dies zu steuern, wurden drei Optionen hinzugefügt:

hbase.lru.cache.heavy.eviction.count.limit – legt fest, wie oft der Prozess des Entfernens von Daten aus dem Cache ausgeführt werden soll, bevor wir mit der Optimierung beginnen (d. h. Blöcke überspringen). Standardmäßig ist er gleich MAX_INT = 2147483647 und bedeutet faktisch, dass die Funktion mit diesem Wert niemals zu arbeiten beginnt. Denn der Räumungsvorgang beginnt alle 5 – 10 Sekunden (abhängig von der Belastung) und 2147483647 * 10 / 60 / 60 / 24 / 365 = 680 Jahre. Wir können diesen Parameter jedoch auf 0 setzen und dafür sorgen, dass die Funktion sofort nach dem Start funktioniert.

Allerdings gibt es in diesem Parameter auch eine Nutzlast. Wenn unsere Auslastung so groß ist, dass sich kurzfristige Lesevorgänge (z. B. tagsüber) und langfristige Lesevorgänge (nachts) ständig abwechseln, können wir sicherstellen, dass die Funktion nur aktiviert wird, wenn lange Lesevorgänge ausgeführt werden.

Wir wissen zum Beispiel, dass kurzfristige Messungen in der Regel etwa 1 Minute dauern. Es besteht keine Notwendigkeit, mit dem Wegwerfen von Blöcken zu beginnen, der Cache hat keine Zeit, veraltet zu sein, und dann können wir diesen Parameter beispielsweise auf 10 setzen. Dies führt dazu, dass die Optimierung erst dann zu funktionieren beginnt, wenn lange- Der Begriff des aktiven Lesens hat begonnen, d. h. in 100 Sekunden. Wenn wir also einen kurzfristigen Lesevorgang durchführen, werden alle Blöcke in den Cache verschoben und sind verfügbar (mit Ausnahme derjenigen, die vom Standardalgorithmus entfernt werden). Und wenn wir langfristige Lesevorgänge durchführen, ist die Funktion aktiviert und wir hätten eine viel höhere Leistung.

hbase.lru.cache.heavy.eviction.mb.size.limit – legt fest, wie viele Megabyte wir in 10 Sekunden in den Cache legen (und natürlich entfernen) möchten. Die Funktion wird versuchen, diesen Wert zu erreichen und beizubehalten. Der Punkt ist folgender: Wenn wir Gigabyte in den Cache schieben, müssen wir Gigabyte entfernen, und das ist, wie wir oben gesehen haben, sehr teuer. Sie sollten jedoch nicht versuchen, den Wert zu klein einzustellen, da dies dazu führt, dass der Block-Skip-Modus vorzeitig beendet wird. Für leistungsstarke Server (ca. 20–40 physische Kerne) ist es optimal, ca. 300–400 MB einzustellen. Für die Mittelklasse (~10 Kerne) 200-300 MB. Bei schwachen Systemen (2–5 Kerne) können 50–100 MB normal sein (auf diesen nicht getestet).

Schauen wir uns an, wie das funktioniert: Nehmen wir an, wir setzen hbase.lru.cache.heavy.eviction.mb.size.limit = 500, es gibt eine Art Last (Lesen) und dann berechnen wir alle ~10 Sekunden, wie viele Bytes es waren vertrieben mit der Formel:

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

Wenn tatsächlich 2000 MB entfernt wurden, beträgt der Overhead:

2000 * 100 / 500 - 100 = 300 %

Die Algorithmen versuchen, nicht mehr als ein paar zehn Prozent beizubehalten, daher reduziert die Funktion den Prozentsatz der zwischengespeicherten Blöcke und implementiert so einen Auto-Tuning-Mechanismus.

Wenn jedoch die Last sinkt, werden beispielsweise nur 200 MB entfernt und der Overhead wird negativ (das sogenannte Overshooting):

200 * 100 / 500 - 100 = -60 %

Im Gegenteil, die Funktion erhöht den Prozentsatz der zwischengespeicherten Blöcke, bis der Overhead positiv wird.

Unten sehen Sie ein Beispiel dafür, wie dies anhand realer Daten aussieht. Es besteht keine Notwendigkeit, zu versuchen, 0 % zu erreichen, es ist unmöglich. Es ist sehr gut, wenn es etwa 30–100 % beträgt. Dies hilft, ein vorzeitiges Verlassen des Optimierungsmodus bei kurzfristigen Spitzen zu vermeiden.

hbase.lru.cache.heavy.eviction.overhead.coefficient — legt fest, wie schnell wir das Ergebnis erhalten möchten. Wenn wir sicher wissen, dass unsere Lesevorgänge größtenteils lang sind und nicht warten möchten, können wir dieses Verhältnis erhöhen und schneller eine hohe Leistung erzielen.

Wir setzen diesen Koeffizienten beispielsweise auf = 0.01. Dies bedeutet, dass der Overhead (siehe oben) mit dieser Zahl mit dem resultierenden Ergebnis multipliziert wird und der Prozentsatz der zwischengespeicherten Blöcke reduziert wird. Nehmen wir an, dass der Overhead = 300 % und der Koeffizient = 0.01, dann wird der Prozentsatz der zwischengespeicherten Blöcke um 3 % reduziert.

Eine ähnliche „Gegendruck“-Logik wird auch für negative Overhead-Werte (Überschießen) implementiert. Da kurzfristige Schwankungen im Lese- und Räumungsvolumen immer möglich sind, können Sie mit diesem Mechanismus ein vorzeitiges Verlassen des Optimierungsmodus vermeiden. Gegendruck hat eine umgekehrte Logik: Je stärker das Überschießen, desto mehr Blöcke werden zwischengespeichert.

So erhöhen Sie die Lesegeschwindigkeit von HBase bis zum Dreifachen und von HDFS bis zum Fünffachen

Implementierungscode

        LruBlockCache cache = this.cache.get();
        if (cache == null) {
          break;
        }
        freedSumMb += cache.evict()/1024/1024;
        /*
        * Sometimes we are reading more data than can fit into BlockCache
        * and it is the cause a high rate of evictions.
        * This in turn leads to heavy Garbage Collector works.
        * So a lot of blocks put into BlockCache but never read,
        * but spending a lot of CPU resources.
        * Here we will analyze how many bytes were freed and decide
        * decide whether the time has come to reduce amount of caching blocks.
        * It help avoid put too many blocks into BlockCache
        * when evict() works very active and save CPU for other jobs.
        * More delails: https://issues.apache.org/jira/browse/HBASE-23887
        */

        // First of all we have to control how much time
        // has passed since previuos evict() was launched
        // This is should be almost the same time (+/- 10s)
        // because we get comparable volumes of freed bytes each time.
        // 10s because this is default period to run evict() (see above this.wait)
        long stopTime = System.currentTimeMillis();
        if ((stopTime - startTime) > 1000 * 10 - 1) {
          // Here we have to calc what situation we have got.
          // We have the limit "hbase.lru.cache.heavy.eviction.bytes.size.limit"
          // and can calculte overhead on it.
          // We will use this information to decide,
          // how to change percent of caching blocks.
          freedDataOverheadPercent =
            (int) (freedSumMb * 100 / cache.heavyEvictionMbSizeLimit) - 100;
          if (freedSumMb > cache.heavyEvictionMbSizeLimit) {
            // Now we are in the situation when we are above the limit
            // But maybe we are going to ignore it because it will end quite soon
            heavyEvictionCount++;
            if (heavyEvictionCount > cache.heavyEvictionCountLimit) {
              // It is going for a long time and we have to reduce of caching
              // blocks now. So we calculate here how many blocks we want to skip.
              // It depends on:
             // 1. Overhead - if overhead is big we could more aggressive
              // reducing amount of caching blocks.
              // 2. How fast we want to get the result. If we know that our
              // heavy reading for a long time, we don't want to wait and can
              // increase the coefficient and get good performance quite soon.
              // But if we don't sure we can do it slowly and it could prevent
              // premature exit from this mode. So, when the coefficient is
              // higher we can get better performance when heavy reading is stable.
              // But when reading is changing we can adjust to it and set
              // the coefficient to lower value.
              int change =
                (int) (freedDataOverheadPercent * cache.heavyEvictionOverheadCoefficient);
              // But practice shows that 15% of reducing is quite enough.
              // We are not greedy (it could lead to premature exit).
              change = Math.min(15, change);
              change = Math.max(0, change); // I think it will never happen but check for sure
              // So this is the key point, here we are reducing % of caching blocks
              cache.cacheDataBlockPercent -= change;
              // If we go down too deep we have to stop here, 1% any way should be.
              cache.cacheDataBlockPercent = Math.max(1, cache.cacheDataBlockPercent);
            }
          } else {
            // Well, we have got overshooting.
            // Mayby it is just short-term fluctuation and we can stay in this mode.
            // It help avoid permature exit during short-term fluctuation.
            // If overshooting less than 90%, we will try to increase the percent of
            // caching blocks and hope it is enough.
            if (freedSumMb >= cache.heavyEvictionMbSizeLimit * 0.1) {
              // Simple logic: more overshooting - more caching blocks (backpressure)
              int change = (int) (-freedDataOverheadPercent * 0.1 + 1);
              cache.cacheDataBlockPercent += change;
              // But it can't be more then 100%, so check it.
              cache.cacheDataBlockPercent = Math.min(100, cache.cacheDataBlockPercent);
            } else {
              // Looks like heavy reading is over.
              // Just exit form this mode.
              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;
       }

Schauen wir uns das alles nun anhand eines realen Beispiels an. Wir haben das folgende Testskript:

  1. Beginnen wir mit dem Scan (25 Threads, Batch = 100).
  2. Nach 5 Minuten Multi-Gets hinzufügen (25 Threads, Batch = 100)
  3. Nach 5 Minuten Multi-Gets ausschalten (es bleibt nur noch der Scan übrig)

Wir führen zwei Durchläufe durch, zuerst hbase.lru.cache.heavy.eviction.count.limit = 10000 (wodurch die Funktion tatsächlich deaktiviert wird) und dann limit = 0 setzen (aktiviert).

In den Protokollen unten sehen wir, wie die Funktion aktiviert wird und die Überschreitung auf 14–71 % zurücksetzt. Von Zeit zu Zeit nimmt die Last ab, wodurch BackPressure aktiviert wird und HBase wieder mehr Blöcke zwischenspeichert.

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

Die Scans wurden benötigt, um denselben Prozess in Form eines Diagramms der Beziehung zwischen zwei Cache-Abschnitten darzustellen – Single (wo Blöcke, die noch nie zuvor angefordert wurden) und Multi (hier werden mindestens einmal „angeforderte“ Daten gespeichert):

So erhöhen Sie die Lesegeschwindigkeit von HBase bis zum Dreifachen und von HDFS bis zum Fünffachen

Und schließlich: Wie sieht die Funktion der Parameter in Form eines Diagramms aus? Zum Vergleich: Der Cache wurde zu Beginn vollständig ausgeschaltet, dann wurde HBase mit Caching gestartet und der Beginn der Optimierungsarbeiten um 5 Minuten (30 Räumungszyklen) verzögert.

Den vollständigen Code finden Sie in Pull Request HBASE 23887 auf Github.

Allerdings sind 300 Lesevorgänge pro Sekunde nicht alles, was unter diesen Bedingungen auf dieser Hardware erreicht werden kann. Tatsache ist, dass, wenn Sie über HDFS auf Daten zugreifen müssen, der ShortCircuitCache-Mechanismus (im Folgenden als SSC bezeichnet) verwendet wird, der Ihnen den direkten Zugriff auf die Daten ermöglicht und Netzwerkinteraktionen vermeidet.

Die Profilerstellung hat gezeigt, dass dieser Mechanismus zwar einen großen Gewinn bringt, aber irgendwann auch zu einem Engpass wird, da fast alle schweren Vorgänge innerhalb einer Schleuse stattfinden, was in den meisten Fällen zu Blockaden führt.

So erhöhen Sie die Lesegeschwindigkeit von HBase bis zum Dreifachen und von HDFS bis zum Fünffachen

Nachdem wir dies erkannt hatten, wurde uns klar, dass das Problem umgangen werden kann, indem ein Array unabhängiger SSCs erstellt wird:

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

Und dann mit ihnen arbeiten, Schnittpunkte auch an der letzten Offset-Ziffer ausschließen:

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

Jetzt können Sie mit dem Testen beginnen. Dazu lesen wir Dateien aus HDFS mit einer einfachen Multithread-Anwendung. Stellen Sie die Parameter ein:

conf.set("dfs.client.read.shortcircuit", "true");
conf.set("dfs.client.read.shortcircuit.buffer.size", "65536"); // по дефолту = 1 МБ и это сильно замедляет чтение, поэтому лучше привести в соответствие к реальным нуждам
conf.set("dfs.client.short.circuit.num", num); // от 1 до 10

Und lesen Sie einfach die Dateien:

FSDataInputStream in = fileSystem.open(path);
for (int i = 0; i < count; i++) {
    position += 65536;
    if (position > 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 gelesenen Dateien (von 10 auf 200 – horizontale Achse) und die Anzahl der Caches (von 1 auf 10 – Grafiken) erhöhen. Die vertikale Achse zeigt die Beschleunigung, die sich aus einer Erhöhung des SSC im Vergleich zu dem Fall ergibt, wenn nur ein Cache vorhanden ist.

So erhöhen Sie die Lesegeschwindigkeit von HBase bis zum Dreifachen und von HDFS bis zum Fünffachen

So lesen Sie das Diagramm: Die Ausführungszeit für 100 Lesevorgänge in 64-KB-Blöcken mit einem Cache beträgt 78 Sekunden. Bei 5 Caches dauert es dagegen 16 Sekunden. Diese. Es gibt eine Beschleunigung von etwa dem Fünffachen. Wie aus der Grafik ersichtlich ist, ist der Effekt bei einer kleinen Anzahl paralleler Lesevorgänge nicht sehr spürbar; er beginnt eine spürbare Rolle zu spielen, wenn es mehr als 5 Thread-Lesevorgänge gibt. Auffällig ist auch, dass die Anzahl der SSCs von 50 erhöht wird und höher ergibt eine deutlich geringere Leistungssteigerung.

Hinweis 1: Da die Testergebnisse recht volatil sind (siehe unten), wurden 3 Durchläufe durchgeführt und die resultierenden Werte gemittelt.

Hinweis 2: Der Leistungsgewinn durch die Konfiguration des Direktzugriffs ist derselbe, obwohl der Zugriff selbst etwas langsamer ist.

Es muss jedoch klargestellt werden, dass diese Beschleunigung im Gegensatz zu HBase nicht immer kostenlos ist. Hier „schalten“ wir die Fähigkeit der CPU frei, mehr Arbeit zu leisten, anstatt an Sperren festzuhalten.

So erhöhen Sie die Lesegeschwindigkeit von HBase bis zum Dreifachen und von HDFS bis zum Fünffachen

Hier können Sie beobachten, dass eine Erhöhung der Anzahl der Caches im Allgemeinen zu einer ungefähr proportionalen Steigerung der CPU-Auslastung führt. Allerdings gibt es etwas mehr Gewinnkombinationen.

Schauen wir uns zum Beispiel die Einstellung SSC = 3 genauer an. Die Leistungssteigerung auf der Strecke beträgt etwa das 3.3-fache. Nachfolgend finden Sie die Ergebnisse aller drei separaten Läufe.

So erhöhen Sie die Lesegeschwindigkeit von HBase bis zum Dreifachen und von HDFS bis zum Fünffachen

Während der CPU-Verbrauch um etwa das 2.8-fache steigt. Der Unterschied ist nicht sehr groß, aber die kleine Greta ist schon glücklich und hat vielleicht Zeit, zur Schule zu gehen und Unterricht zu nehmen.

Dies wirkt sich also positiv auf jedes Tool aus, das Massenzugriff auf HDFS verwendet (z. B. Spark usw.), vorausgesetzt, der Anwendungscode ist leichtgewichtig (d. h. der Plug befindet sich auf der HDFS-Clientseite) und es ist freie CPU-Leistung vorhanden . Um dies zu überprüfen, testen wir, welche Auswirkungen die kombinierte Verwendung von BlockCache-Optimierung und SSC-Tuning für das Lesen aus HBase haben wird.

So erhöhen Sie die Lesegeschwindigkeit von HBase bis zum Dreifachen und von HDFS bis zum Fünffachen

Es ist zu erkennen, dass der Effekt unter solchen Bedingungen nicht so groß ist wie bei verfeinerten Tests (Lesen ohne jegliche Verarbeitung), aber es ist durchaus möglich, hier zusätzliche 80 KB herauszuquetschen. Zusammen sorgen beide Optimierungen für eine bis zu vierfache Beschleunigung.

Für diese Optimierung wurde auch eine PR erstellt [HDFS-15202], die zusammengeführt wurde und diese Funktionalität in zukünftigen Versionen verfügbar sein wird.

Und schließlich war es interessant, die Leseleistung einer ähnlichen breitspaltigen Datenbank, Cassandra und HBase, zu vergleichen.

Zu diesem Zweck haben wir Instanzen des standardmäßigen YCSB-Auslastungstestdienstprogramms von zwei Hosts aus gestartet (insgesamt 800 Threads). Auf der Serverseite – 4 Instanzen von RegionServer und Cassandra auf 4 Hosts (nicht denen, auf denen die Clients laufen, um deren Einfluss zu vermeiden). Die Messwerte stammen aus Größentabellen:

HBase – 300 GB auf HDFS (100 GB reine Daten)

Cassandra – 250 GB (Replikationsfaktor = 3)

Diese. die Lautstärke war ungefähr gleich (in HBase etwas mehr).

HBase-Parameter:

dfs.client.short.Circuit.num = 5 (HDFS-Client-Optimierung)

hbase.lru.cache.heavy.eviction.count.limit = 30 – Das bedeutet, dass der Patch nach 30 Räumungen (ca. 5 Minuten) zu funktionieren beginnt.

hbase.lru.cache.heavy.eviction.mb.size.limit = 300 – Zielvolumen für Caching und Räumung

YCSB-Protokolle wurden analysiert und in Excel-Diagrammen kompiliert:

So erhöhen Sie die Lesegeschwindigkeit von HBase bis zum Dreifachen und von HDFS bis zum Fünffachen

Wie Sie sehen, ermöglichen diese Optimierungen einen Vergleich der Leistung dieser Datenbanken unter diesen Bedingungen und erreichen 450 Lesevorgänge pro Sekunde.

Wir hoffen, dass diese Informationen jemandem im spannenden Kampf um Produktivität nützlich sein können.

Source: habr.com

Kommentar hinzufügen