Hoe u de leessnelheid van HBase tot 3 keer kunt verhogen en van HDFS tot 5 keer

Hoge prestaties zijn een van de belangrijkste vereisten bij het werken met big data. Op de afdeling voor het laden van gegevens bij Sberbank pompen we vrijwel alle transacties in onze op Hadoop gebaseerde Data Cloud en verwerken daardoor echt grote informatiestromen. Uiteraard zijn we altijd op zoek naar manieren om de prestaties te verbeteren, en nu willen we u vertellen hoe we RegionServer HBase en de HDFS-client hebben kunnen patchen, waardoor we de snelheid van leesbewerkingen aanzienlijk hebben kunnen verhogen.
Hoe u de leessnelheid van HBase tot 3 keer kunt verhogen en van HDFS tot 5 keer

Voordat we echter verder gaan met de essentie van de verbeteringen, is het de moeite waard om te praten over beperkingen die in principe niet kunnen worden omzeild als je op een harde schijf zit.

Waarom HDD en snelle Random Access-lezingen incompatibel zijn
Zoals u weet, slaan HBase en vele andere databases gegevens op in blokken van enkele tientallen kilobytes groot. Standaard is dit ongeveer 64 KB. Laten we ons nu voorstellen dat we slechts 100 bytes nodig hebben en we vragen HBase om ons deze gegevens te geven met behulp van een bepaalde sleutel. Omdat de blokgrootte in HFiles 64 KB is, zal het verzoek 640 keer groter zijn (slechts een minuut!) dan nodig.

Vervolgens gaat het verzoek via HDFS en het bijbehorende caching-mechanisme voor metagegevens ShortCircuitCache (wat directe toegang tot bestanden mogelijk maakt), dit leidt ertoe dat er al 1 MB van de schijf wordt gelezen. Dit kan echter worden aangepast met de parameter dfs.client.read.shortcircuit.buffer.size en in veel gevallen is het zinvol om deze waarde te verlagen, bijvoorbeeld naar 126 KB.

Laten we zeggen dat we dit doen, maar bovendien, wanneer we gegevens gaan lezen via de Java-api, zoals functies als FileChannel.read en het besturingssysteem vragen om de opgegeven hoeveelheid gegevens te lezen, staat er twee keer meer "voor het geval dat" , d.w.z. 2 KB in ons geval. Dit komt omdat Java geen gemakkelijke manier heeft om de vlag FADV_RANDOM in te stellen om dit gedrag te voorkomen.

Als gevolg hiervan wordt er, om onze 100 bytes te halen, 2600 keer meer onder de motorkap gelezen. Het lijkt erop dat de oplossing voor de hand ligt: ​​laten we de blokgrootte terugbrengen tot een kilobyte, de genoemde vlag instellen en een grote verlichtingsversnelling bereiken. Maar het probleem is dat door de blokgrootte twee keer te verkleinen, we ook het aantal gelezen bytes per tijdseenheid met twee keer verminderen.

Er kan enige winst worden behaald door het instellen van de vlag FADV_RANDOM, maar alleen met hoge multi-threading en met een blokgrootte van 128 KB, maar dit is maximaal enkele tientallen procenten:

Hoe u de leessnelheid van HBase tot 3 keer kunt verhogen en van HDFS tot 5 keer

Er zijn tests uitgevoerd op 100 bestanden, elk 1 GB groot en opgeslagen op 10 harde schijven.

Laten we berekenen waar we in principe op kunnen rekenen met deze snelheid:
Laten we zeggen dat we van 10 schijven lezen met een snelheid van 280 MB/sec, d.w.z. 3 miljoen keer 100 bytes. Maar zoals we ons herinneren, zijn de gegevens die we nodig hebben 2600 keer minder dan wat er wordt gelezen. We delen dus 3 miljoen door 2600 en krijgen 1100 records per seconde.

Deprimerend, nietwaar? Dat is de natuur Willekeurige toegang toegang tot gegevens op de harde schijf - ongeacht de blokgrootte. Dit is de fysieke limiet van willekeurige toegang en geen enkele database kan onder dergelijke omstandigheden meer uitpersen.

Hoe bereiken databases dan veel hogere snelheden? Laten we, om deze vraag te beantwoorden, eens kijken naar wat er gebeurt in de volgende afbeelding:

Hoe u de leessnelheid van HBase tot 3 keer kunt verhogen en van HDFS tot 5 keer

Hier zien we dat de snelheid de eerste paar minuten echt zo'n duizend records per seconde bedraagt. Maar doordat er veel meer wordt gelezen dan gevraagd, komen de gegevens echter in de buff/cache van het besturingssysteem (Linux) terecht en stijgt de snelheid naar een fatsoenlijkere 60 per seconde.

We zullen ons dus verder bezighouden met het versnellen van de toegang tot alleen de gegevens die zich in de cache van het besturingssysteem bevinden of zich bevinden op SSD/NVMe-opslagapparaten met een vergelijkbare toegangssnelheid.

In ons geval voeren we tests uit op een bank van 4 servers, die elk als volgt in rekening worden gebracht:

CPU: Xeon E5-2680 v4 @ 2.40 GHz 64 threads.
Geheugen: 730 GB.
Java-versie: 1.8.0_111

En hier is het belangrijkste punt de hoeveelheid gegevens in de tabellen die moeten worden gelezen. Het is een feit dat als je gegevens leest uit een tabel die volledig in de HBase-cache is geplaatst, het niet eens zover komt dat je de buff/cache van het besturingssysteem leest. Omdat HBase standaard 40% van het geheugen toewijst aan een structuur genaamd BlockCache. In wezen is dit een ConcurrentHashMap, waarbij de sleutel de bestandsnaam + offset van het blok is, en de waarde de feitelijke gegevens bij deze offset zijn.

Dus als we alleen vanuit deze structuur lezen, kunnen we we zien een uitstekende snelheid, zoals een miljoen verzoeken per seconde. Maar laten we ons voorstellen dat we niet honderden gigabytes aan geheugen kunnen toewijzen alleen voor databasebehoeften, omdat er nog een heleboel andere nuttige dingen op deze servers draaien.

In ons geval is het volume van BlockCache op één RS bijvoorbeeld ongeveer 12 GB. We hebben twee RS op één knooppunt geland, d.w.z. Er wordt 96 GB toegewezen voor BlockCache op alle knooppunten. En er zijn vele malen meer gegevens, laat het bijvoorbeeld 4 tabellen zijn, elk 130 regio's, waarin bestanden 800 MB groot zijn, gecomprimeerd door FAST_DIFF, d.w.z. in totaal 410 GB (dit zijn pure data, dus zonder rekening te houden met de replicatiefactor).

BlockCache maakt dus slechts ongeveer 23% uit van het totale datavolume en dit ligt veel dichter bij de werkelijke omstandigheden van wat BigData wordt genoemd. En dit is waar het plezier begint - want uiteraard geldt: hoe minder cachehits, hoe slechter de prestaties. Als je mist, zul je immers veel werk moeten doen, d.w.z. ga naar het oproepen van systeemfuncties. Dit kan echter niet worden vermeden, dus laten we eens naar een heel ander aspect kijken: wat gebeurt er met de gegevens in de cache?

Laten we de situatie vereenvoudigen en aannemen dat we een cache hebben waar slechts 1 object in past. Hier is een voorbeeld van wat er zal gebeuren als we proberen te werken met een gegevensvolume dat drie keer groter is dan de cache. We zullen het volgende moeten doen:

1. Plaats blok 1 in de cache
2. Verwijder blok 1 uit de cache
3. Plaats blok 2 in de cache
4. Verwijder blok 2 uit de cache
5. Plaats blok 3 in de cache

5 acties voltooid! Deze situatie kan echter niet normaal worden genoemd; in feite dwingen we HBase een hoop volkomen nutteloos werk te doen. Het leest voortdurend gegevens uit de cache van het besturingssysteem, plaatst deze in BlockCache en gooit deze vrijwel onmiddellijk weg omdat er een nieuw deel van de gegevens is binnengekomen. De animatie aan het begin van het bericht laat de essentie van het probleem zien: Garbage Collector raakt van de schaal, de atmosfeer warmt op, de kleine Greta in het verre en hete Zweden raakt van streek. En wij IT-mensen houden er echt niet van als kinderen verdrietig zijn, dus we gaan nadenken over wat we eraan kunnen doen.

Wat als je niet alle blokken in de cache plaatst, maar slechts een bepaald percentage ervan, zodat de cache niet overloopt? Laten we beginnen door simpelweg een paar regels code toe te voegen aan het begin van de functie voor het plaatsen van gegevens in BlockCache:

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

Het punt hier is het volgende: offset is de positie van het blok in het bestand en de laatste cijfers zijn willekeurig en gelijkmatig verdeeld van 00 tot 99. Daarom slaan we alleen de cijfers over die binnen het bereik vallen dat we nodig hebben.

Stel bijvoorbeeld cacheDataBlockPercent = 20 in en kijk wat er gebeurt:

Hoe u de leessnelheid van HBase tot 3 keer kunt verhogen en van HDFS tot 5 keer

Het resultaat is duidelijk. In de onderstaande grafieken wordt duidelijk waarom zo'n versnelling plaatsvond - we besparen veel GC-bronnen zonder het Sisyphean-werk te doen: gegevens in de cache plaatsen om deze vervolgens onmiddellijk in de afvoer van de Mars-honden te gooien:

Hoe u de leessnelheid van HBase tot 3 keer kunt verhogen en van HDFS tot 5 keer

Tegelijkertijd neemt het CPU-gebruik toe, maar dit is veel minder dan de productiviteit:

Hoe u de leessnelheid van HBase tot 3 keer kunt verhogen en van HDFS tot 5 keer

Het is ook vermeldenswaard dat de blokken die in BlockCache zijn opgeslagen verschillend zijn. Het merendeel, ongeveer 95%, bestaat uit data zelf. En de rest is metadata, zoals Bloom-filters of LEAF_INDEX en т.д.. Deze gegevens zijn niet voldoende, maar wel erg nuttig, want voordat HBase rechtstreeks toegang krijgt tot de gegevens, wendt HBase zich tot de meta om te begrijpen of het nodig is om hier verder te zoeken en, zo ja, waar het interessante blok zich precies bevindt.

Daarom zien we in de code een controlevoorwaarde buf.getBlockType().isData() en dankzij deze meta laten we hem in ieder geval in de cache staan.

Laten we nu de belasting verhogen en de functie in één keer iets strakker maken. In de eerste test hebben we het cutoff-percentage = 20 gemaakt en werd BlockCache enigszins onderbenut. Laten we het nu instellen op 23% en elke 100 minuten 5 threads toevoegen om te zien op welk punt verzadiging optreedt:

Hoe u de leessnelheid van HBase tot 3 keer kunt verhogen en van HDFS tot 5 keer

Hier zien we dat de originele versie vrijwel onmiddellijk het plafond bereikt met ongeveer 100 verzoeken per seconde. Terwijl de patch een versnelling tot 300 duizend geeft. Tegelijkertijd is het duidelijk dat verdere acceleratie niet langer zo “gratis” is; het CPU-gebruik neemt ook toe.

Dit is echter geen erg elegante oplossing, aangezien we van tevoren niet weten welk percentage blokken in de cache moet worden opgeslagen, dit is afhankelijk van het laadprofiel. Daarom werd een mechanisme geïmplementeerd om deze parameter automatisch aan te passen, afhankelijk van de activiteit van de leesbewerkingen.

Er zijn drie opties toegevoegd om dit te regelen:

hbase.lru.cache.heavy.uitzetting.count.limit — stelt in hoe vaak het proces van het verwijderen van gegevens uit de cache moet worden uitgevoerd voordat we optimalisatie gaan gebruiken (dat wil zeggen blokken overslaan). Standaard is dit gelijk aan MAX_INT = 2147483647, wat in feite betekent dat de feature nooit met deze waarde zal gaan werken. Omdat het uitzettingsproces elke 5 - 10 seconden begint (het hangt af van de belasting) en 2147483647 * 10 / 60 / 60 / 24 / 365 = 680 jaar. We kunnen deze parameter echter op 0 instellen en de functie onmiddellijk na de lancering laten werken.

Er zit echter ook een payload in deze parameter. Als onze belasting zodanig is dat leesbewerkingen op korte termijn (bijvoorbeeld overdag) en leesbewerkingen op lange termijn ('s nachts) voortdurend worden afgewisseld, kunnen we ervoor zorgen dat de functie alleen wordt ingeschakeld wanneer lange leesbewerkingen worden uitgevoerd.

We weten bijvoorbeeld dat kortetermijnmetingen meestal ongeveer 1 minuut duren. Het is niet nodig om blokken weg te gooien, de cache heeft geen tijd om verouderd te raken en dan kunnen we deze parameter gelijkstellen aan bijvoorbeeld 10. Dit zal ertoe leiden dat de optimalisatie pas gaat werken als het lang duurt. termijn actief lezen is begonnen, d.w.z. binnen 100 seconden. Als we dus een kortetermijnuitlezing hebben, gaan alle blokken naar de cache en zijn ze beschikbaar (behalve de blokken die door het standaardalgoritme worden verwijderd). En als we langetermijnlezingen uitvoeren, wordt de functie ingeschakeld en behalen we veel betere prestaties.

hbase.lru.cache.heavy.uitzetting.mb.size.limit — stelt in hoeveel megabytes we binnen 10 seconden in de cache willen plaatsen (en natuurlijk willen verwijderen). De functie zal proberen deze waarde te bereiken en te behouden. Het punt is dit: als we gigabytes in de cache stoppen, zullen we gigabytes moeten verwijderen, en dit is, zoals we hierboven zagen, erg duur. Probeer het echter niet te klein in te stellen, omdat hierdoor de blokskipmodus voortijdig wordt beëindigd. Voor krachtige servers (ongeveer 20-40 fysieke kernen) is het optimaal om ongeveer 300-400 MB in te stellen. Voor de middenklasse (~10 cores) 200-300 MB. Voor zwakke systemen (2-5 cores) kan 50-100 MB normaal zijn (hierop niet getest).

Laten we eens kijken hoe dit werkt: laten we zeggen dat we hbase.lru.cache.heavy.eviction.mb.size.limit = 500 instellen, er is een soort belasting (lezen) en vervolgens berekenen we elke ~10 seconden hoeveel bytes er zijn uitgezet met behulp van de formule:

Overhead = Totaal aantal vrije bytes (MB) * 100 / Limiet (MB) - 100;

Als er feitelijk 2000 MB wordt uitgezet, dan is Overhead gelijk aan:

2000 * 100 / 500 - 100 = 300%

De algoritmen proberen niet meer dan enkele tientallen procenten te behouden, dus de functie zal het percentage in de cache opgeslagen blokken verminderen, waardoor een automatisch afstemmingsmechanisme wordt geïmplementeerd.

Als de belasting echter afneemt, wordt er bijvoorbeeld slechts 200 MB verwijderd en wordt de overhead negatief (de zogenaamde overshooting):

200 * 100 / 500 - 100 = -60%

Integendeel, de functie zal het percentage in de cache opgeslagen blokken verhogen totdat Overhead positief wordt.

Hieronder ziet u een voorbeeld van hoe dit eruit ziet op echte gegevens. Het is niet nodig om te proberen de 0% te bereiken, dat is onmogelijk. Het is erg goed als het ongeveer 30 - 100% is, dit helpt om voortijdig verlaten van de optimalisatiemodus tijdens kortetermijnpieken te voorkomen.

hbase.lru.cache.heavy.uitzetting.overhead.coëfficiënt — stelt in hoe snel we het resultaat willen krijgen. Als we zeker weten dat onze leesbewerkingen meestal lang zijn en niet willen wachten, kunnen we deze verhouding verhogen en sneller hoge prestaties behalen.

We stellen deze coëfficiënt bijvoorbeeld = 0.01 in. Dit betekent dat Overhead (zie hierboven) met dit getal wordt vermenigvuldigd door het resulterende resultaat en dat het percentage in de cache opgeslagen blokken wordt verminderd. Laten we aannemen dat Overhead = 300% en coëfficiënt = 0.01, dan wordt het percentage in de cache opgeslagen blokken met 3% verlaagd.

Een soortgelijke “Tegendruk”-logica wordt ook geïmplementeerd voor negatieve Overhead-waarden (overschrijding). Omdat kortetermijnschommelingen in het aantal lees- en uitzettingen altijd mogelijk zijn, kunt u met dit mechanisme voortijdig verlaten van de optimalisatiemodus voorkomen. Tegendruk heeft een omgekeerde logica: hoe sterker de overschrijding, hoe meer blokken er in de cache worden opgeslagen.

Hoe u de leessnelheid van HBase tot 3 keer kunt verhogen en van HDFS tot 5 keer

Implementatiecode

        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;
       }

Laten we dit nu allemaal bekijken aan de hand van een echt voorbeeld. We hebben het volgende testscript:

  1. Laten we beginnen met scannen (25 threads, batch = 100)
  2. Voeg na 5 minuten multi-gets toe (25 threads, batch = 100)
  3. Schakel na 5 minuten multi-gets uit (alleen scan blijft weer over)

We voeren twee runs uit, eerst hbase.lru.cache.heavy.eviction.count.limit = 10000 (waardoor de functie daadwerkelijk wordt uitgeschakeld) en vervolgens limit = 0 (inschakelen).

In de onderstaande logs zien we hoe de functie wordt ingeschakeld en Overshooting wordt gereset naar 14-71%. Van tijd tot tijd neemt de belasting af, waardoor Back Pressure wordt ingeschakeld en HBase weer meer blokken in de cache opslaat.

Log RegioServer
uitgezet (MB): 0, ratio 0.0, overhead (%): -100, teller voor zware uitzettingen: 0, huidige caching DataBlock (%): 100
uitgezet (MB): 0, ratio 0.0, overhead (%): -100, teller voor zware uitzettingen: 0, huidige caching DataBlock (%): 100
uitgezet (MB): 2170, ratio 1.09, overhead (%): 985, teller voor zware uitzettingen: 1, huidige caching DataBlock (%): 91 < start
uitgezet (MB): 3763, ratio 1.08, overhead (%): 1781, teller voor zware uitzettingen: 2, huidige caching DataBlock (%): 76
uitgezet (MB): 3306, ratio 1.07, overhead (%): 1553, teller voor zware uitzettingen: 3, huidige caching DataBlock (%): 61
uitgezet (MB): 2508, ratio 1.06, overhead (%): 1154, teller voor zware uitzettingen: 4, huidige caching DataBlock (%): 50
uitgezet (MB): 1824, ratio 1.04, overhead (%): 812, teller voor zware uitzettingen: 5, huidige caching DataBlock (%): 42
uitgezet (MB): 1482, ratio 1.03, overhead (%): 641, teller voor zware uitzettingen: 6, huidige caching DataBlock (%): 36
uitgezet (MB): 1140, ratio 1.01, overhead (%): 470, teller voor zware uitzettingen: 7, huidige caching DataBlock (%): 32
uitgezet (MB): 913, ratio 1.0, overhead (%): 356, teller voor zware uitzettingen: 8, huidige caching DataBlock (%): 29
uitgezet (MB): 912, ratio 0.89, overhead (%): 356, teller voor zware uitzettingen: 9, huidige caching DataBlock (%): 26
uitgezet (MB): 684, ratio 0.76, overhead (%): 242, teller voor zware uitzettingen: 10, huidige caching DataBlock (%): 24
uitgezet (MB): 684, ratio 0.61, overhead (%): 242, teller voor zware uitzettingen: 11, huidige caching DataBlock (%): 22
uitgezet (MB): 456, ratio 0.51, overhead (%): 128, teller voor zware uitzettingen: 12, huidige caching DataBlock (%): 21
uitgezet (MB): 456, ratio 0.42, overhead (%): 128, teller voor zware uitzettingen: 13, huidige caching DataBlock (%): 20
uitgezet (MB): 456, ratio 0.33, overhead (%): 128, teller voor zware uitzettingen: 14, huidige caching DataBlock (%): 19
uitgezet (MB): 342, ratio 0.33, overhead (%): 71, teller voor zware uitzettingen: 15, huidige caching DataBlock (%): 19
uitgezet (MB): 342, ratio 0.32, overhead (%): 71, teller voor zware uitzettingen: 16, huidige caching DataBlock (%): 19
uitgezet (MB): 342, ratio 0.31, overhead (%): 71, teller voor zware uitzettingen: 17, huidige caching DataBlock (%): 19
uitgezet (MB): 228, ratio 0.3, overhead (%): 14, teller voor zware uitzettingen: 18, huidige caching DataBlock (%): 19
uitgezet (MB): 228, ratio 0.29, overhead (%): 14, teller voor zware uitzettingen: 19, huidige caching DataBlock (%): 19
uitgezet (MB): 228, ratio 0.27, overhead (%): 14, teller voor zware uitzettingen: 20, huidige caching DataBlock (%): 19
uitgezet (MB): 228, ratio 0.25, overhead (%): 14, teller voor zware uitzettingen: 21, huidige caching DataBlock (%): 19
uitgezet (MB): 228, ratio 0.24, overhead (%): 14, teller voor zware uitzettingen: 22, huidige caching DataBlock (%): 19
uitgezet (MB): 228, ratio 0.22, overhead (%): 14, teller voor zware uitzettingen: 23, huidige caching DataBlock (%): 19
uitgezet (MB): 228, ratio 0.21, overhead (%): 14, teller voor zware uitzettingen: 24, huidige caching DataBlock (%): 19
uitgezet (MB): 228, ratio 0.2, overhead (%): 14, teller voor zware uitzettingen: 25, huidige caching DataBlock (%): 19
uitgezet (MB): 228, ratio 0.17, overhead (%): 14, teller voor zware uitzettingen: 26, huidige caching DataBlock (%): 19
uitgezet (MB): 456, ratio 0.17, overhead (%): 128, teller voor zware uitzettingen: 27, huidige caching DataBlock (%): 18 < toegevoegd krijgt (maar tabel hetzelfde)
uitgezet (MB): 456, ratio 0.15, overhead (%): 128, teller voor zware uitzettingen: 28, huidige caching DataBlock (%): 17
uitgezet (MB): 342, ratio 0.13, overhead (%): 71, teller voor zware uitzettingen: 29, huidige caching DataBlock (%): 17
uitgezet (MB): 342, ratio 0.11, overhead (%): 71, teller voor zware uitzettingen: 30, huidige caching DataBlock (%): 17
uitgezet (MB): 342, ratio 0.09, overhead (%): 71, teller voor zware uitzettingen: 31, huidige caching DataBlock (%): 17
uitgezet (MB): 228, ratio 0.08, overhead (%): 14, teller voor zware uitzettingen: 32, huidige caching DataBlock (%): 17
uitgezet (MB): 228, ratio 0.07, overhead (%): 14, teller voor zware uitzettingen: 33, huidige caching DataBlock (%): 17
uitgezet (MB): 228, ratio 0.06, overhead (%): 14, teller voor zware uitzettingen: 34, huidige caching DataBlock (%): 17
uitgezet (MB): 228, ratio 0.05, overhead (%): 14, teller voor zware uitzettingen: 35, huidige caching DataBlock (%): 17
uitgezet (MB): 228, ratio 0.05, overhead (%): 14, teller voor zware uitzettingen: 36, huidige caching DataBlock (%): 17
uitgezet (MB): 228, ratio 0.04, overhead (%): 14, teller voor zware uitzettingen: 37, huidige caching DataBlock (%): 17
uitgezet (MB): 109, ratio 0.04, overhead (%): -46, teller zware uitzetting: 37, huidige caching DataBlock (%): 22 < tegendruk
uitgezet (MB): 798, ratio 0.24, overhead (%): 299, teller voor zware uitzettingen: 38, huidige caching DataBlock (%): 20
uitgezet (MB): 798, ratio 0.29, overhead (%): 299, teller voor zware uitzettingen: 39, huidige caching DataBlock (%): 18
uitgezet (MB): 570, ratio 0.27, overhead (%): 185, teller voor zware uitzettingen: 40, huidige caching DataBlock (%): 17
uitgezet (MB): 456, ratio 0.22, overhead (%): 128, teller voor zware uitzettingen: 41, huidige caching DataBlock (%): 16
uitgezet (MB): 342, ratio 0.16, overhead (%): 71, teller voor zware uitzettingen: 42, huidige caching DataBlock (%): 16
uitgezet (MB): 342, ratio 0.11, overhead (%): 71, teller voor zware uitzettingen: 43, huidige caching DataBlock (%): 16
uitgezet (MB): 228, ratio 0.09, overhead (%): 14, teller voor zware uitzettingen: 44, huidige caching DataBlock (%): 16
uitgezet (MB): 228, ratio 0.07, overhead (%): 14, teller voor zware uitzettingen: 45, huidige caching DataBlock (%): 16
uitgezet (MB): 228, ratio 0.05, overhead (%): 14, teller voor zware uitzettingen: 46, huidige caching DataBlock (%): 16
uitgezet (MB): 222, ratio 0.04, overhead (%): 11, teller voor zware uitzettingen: 47, huidige caching DataBlock (%): 16
uitgezet (MB): 104, ratio 0.03, overhead (%): -48, teller voor zware uitzettingen: 47, huidige caching DataBlock (%): 21 < interrupt krijgt
uitgezet (MB): 684, ratio 0.2, overhead (%): 242, teller voor zware uitzettingen: 48, huidige caching DataBlock (%): 19
uitgezet (MB): 570, ratio 0.23, overhead (%): 185, teller voor zware uitzettingen: 49, huidige caching DataBlock (%): 18
uitgezet (MB): 342, ratio 0.22, overhead (%): 71, teller voor zware uitzettingen: 50, huidige caching DataBlock (%): 18
uitgezet (MB): 228, ratio 0.21, overhead (%): 14, teller voor zware uitzettingen: 51, huidige caching DataBlock (%): 18
uitgezet (MB): 228, ratio 0.2, overhead (%): 14, teller voor zware uitzettingen: 52, huidige caching DataBlock (%): 18
uitgezet (MB): 228, ratio 0.18, overhead (%): 14, teller voor zware uitzettingen: 53, huidige caching DataBlock (%): 18
uitgezet (MB): 228, ratio 0.16, overhead (%): 14, teller voor zware uitzettingen: 54, huidige caching DataBlock (%): 18
uitgezet (MB): 228, ratio 0.14, overhead (%): 14, teller voor zware uitzettingen: 55, huidige caching DataBlock (%): 18
uitgezet (MB): 112, ratio 0.14, overhead (%): -44, teller zware uitzetting: 55, huidige caching DataBlock (%): 23 < tegendruk
uitgezet (MB): 456, ratio 0.26, overhead (%): 128, teller voor zware uitzettingen: 56, huidige caching DataBlock (%): 22
uitgezet (MB): 342, ratio 0.31, overhead (%): 71, teller voor zware uitzettingen: 57, huidige caching DataBlock (%): 22
uitgezet (MB): 342, ratio 0.33, overhead (%): 71, teller voor zware uitzettingen: 58, huidige caching DataBlock (%): 22
uitgezet (MB): 342, ratio 0.33, overhead (%): 71, teller voor zware uitzettingen: 59, huidige caching DataBlock (%): 22
uitgezet (MB): 342, ratio 0.33, overhead (%): 71, teller voor zware uitzettingen: 60, huidige caching DataBlock (%): 22
uitgezet (MB): 342, ratio 0.33, overhead (%): 71, teller voor zware uitzettingen: 61, huidige caching DataBlock (%): 22
uitgezet (MB): 342, ratio 0.33, overhead (%): 71, teller voor zware uitzettingen: 62, huidige caching DataBlock (%): 22
uitgezet (MB): 342, ratio 0.33, overhead (%): 71, teller voor zware uitzettingen: 63, huidige caching DataBlock (%): 22
uitgezet (MB): 342, ratio 0.32, overhead (%): 71, teller voor zware uitzettingen: 64, huidige caching DataBlock (%): 22
uitgezet (MB): 342, ratio 0.33, overhead (%): 71, teller voor zware uitzettingen: 65, huidige caching DataBlock (%): 22
uitgezet (MB): 342, ratio 0.33, overhead (%): 71, teller voor zware uitzettingen: 66, huidige caching DataBlock (%): 22
uitgezet (MB): 342, ratio 0.32, overhead (%): 71, teller voor zware uitzettingen: 67, huidige caching DataBlock (%): 22
uitgezet (MB): 342, ratio 0.33, overhead (%): 71, teller voor zware uitzettingen: 68, huidige caching DataBlock (%): 22
uitgezet (MB): 342, ratio 0.32, overhead (%): 71, teller voor zware uitzettingen: 69, huidige caching DataBlock (%): 22
uitgezet (MB): 342, ratio 0.32, overhead (%): 71, teller voor zware uitzettingen: 70, huidige caching DataBlock (%): 22
uitgezet (MB): 342, ratio 0.33, overhead (%): 71, teller voor zware uitzettingen: 71, huidige caching DataBlock (%): 22
uitgezet (MB): 342, ratio 0.33, overhead (%): 71, teller voor zware uitzettingen: 72, huidige caching DataBlock (%): 22
uitgezet (MB): 342, ratio 0.33, overhead (%): 71, teller voor zware uitzettingen: 73, huidige caching DataBlock (%): 22
uitgezet (MB): 342, ratio 0.33, overhead (%): 71, teller voor zware uitzettingen: 74, huidige caching DataBlock (%): 22
uitgezet (MB): 342, ratio 0.33, overhead (%): 71, teller voor zware uitzettingen: 75, huidige caching DataBlock (%): 22
uitgezet (MB): 342, ratio 0.33, overhead (%): 71, teller voor zware uitzettingen: 76, huidige caching DataBlock (%): 22
uitgezet (MB): 21, ratio 0.33, overhead (%): -90, teller voor zware uitzettingen: 76, huidige caching DataBlock (%): 32
uitgezet (MB): 0, ratio 0.0, overhead (%): -100, teller voor zware uitzettingen: 0, huidige caching DataBlock (%): 100
uitgezet (MB): 0, ratio 0.0, overhead (%): -100, teller voor zware uitzettingen: 0, huidige caching DataBlock (%): 100

De scans waren nodig om hetzelfde proces te laten zien in de vorm van een grafiek van de relatie tussen twee cachesecties - single (waar blokken die nog nooit eerder zijn opgevraagd) en multi (gegevens die minstens één keer zijn “opgevraagd” worden hier opgeslagen):

Hoe u de leessnelheid van HBase tot 3 keer kunt verhogen en van HDFS tot 5 keer

En tot slot, hoe ziet de werking van de parameters eruit in de vorm van een grafiek. Ter vergelijking: de cache was in het begin volledig uitgeschakeld, daarna werd HBase gelanceerd met caching en werd de start van het optimalisatiewerk met 5 minuten uitgesteld (30 uitzettingscycli).

De volledige code is te vinden in Pull Request HBASE 23887 op github.

300 leesbewerkingen per seconde is echter niet het enige dat onder deze omstandigheden op deze hardware kan worden bereikt. Het is een feit dat wanneer u toegang wilt krijgen tot gegevens via HDFS, het ShortCircuitCache-mechanisme (hierna SSC genoemd) wordt gebruikt, waarmee u rechtstreeks toegang hebt tot de gegevens en netwerkinteracties vermijdt.

Uit profilering is gebleken dat dit mechanisme weliswaar een grote winst oplevert, maar op een gegeven moment ook een knelpunt wordt, omdat bijna alle zware handelingen binnen een sluis plaatsvinden, wat meestal tot blokkering leidt.

Hoe u de leessnelheid van HBase tot 3 keer kunt verhogen en van HDFS tot 5 keer

Nu we dit beseften, realiseerden we ons dat het probleem kan worden omzeild door een reeks onafhankelijke SSC's te creëren:

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

En werk er dan mee, waarbij u ook de snijpunten bij het laatste offsetcijfer uitsluit:

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

Nu kunt u beginnen met testen. Om dit te doen, zullen we bestanden van HDFS lezen met een eenvoudige multi-threaded applicatie. Stel de parameters in:

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

En lees gewoon de bestanden:

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);
}

Deze code wordt uitgevoerd in afzonderlijke threads en we zullen het aantal gelijktijdig gelezen bestanden (van 10 naar 200 - horizontale as) en het aantal caches (van 1 naar 10 - afbeeldingen) verhogen. Op de verticale as wordt de versnelling weergegeven die het gevolg is van een toename van de SSC ten opzichte van het geval waarin er slechts één cache is.

Hoe u de leessnelheid van HBase tot 3 keer kunt verhogen en van HDFS tot 5 keer

Hoe de grafiek te lezen: De uitvoeringstijd voor 100 leesbewerkingen in blokken van 64 KB met één cache vereist 78 seconden. Terwijl het bij 5 caches 16 seconden duurt. Die. er is een versnelling van ~ 5 keer. Zoals uit de grafiek blijkt, is het effect niet erg merkbaar bij een klein aantal parallelle lezingen; het begint een merkbare rol te spelen wanneer er meer dan 50 threadlezingen zijn. Het valt ook op dat het verhogen van het aantal SSC's van 6 en hoger geeft een aanzienlijk kleinere prestatieverbetering.

Opmerking 1: aangezien de testresultaten behoorlijk volatiel zijn (zie hieronder), zijn er 3 runs uitgevoerd en zijn de resulterende waarden gemiddeld.

Opmerking 2: De prestatiewinst bij het configureren van willekeurige toegang is hetzelfde, hoewel de toegang zelf iets langzamer is.

Het is echter noodzakelijk om duidelijk te maken dat deze versnelling, anders dan bij HBase, niet altijd gratis is. Hier ‘ontgrendelen’ we het vermogen van de CPU om meer werk te doen, in plaats van vast te houden aan sloten.

Hoe u de leessnelheid van HBase tot 3 keer kunt verhogen en van HDFS tot 5 keer

Hier kunt u zien dat een toename van het aantal caches over het algemeen een ongeveer evenredige toename van het CPU-gebruik oplevert. Er zijn echter iets meer winnende combinaties.

Laten we bijvoorbeeld de instelling SSC = 3 eens nader bekijken. De prestatieverbetering op het bereik is ongeveer 3.3 keer. Hieronder staan ​​de resultaten van alle drie de afzonderlijke runs.

Hoe u de leessnelheid van HBase tot 3 keer kunt verhogen en van HDFS tot 5 keer

Terwijl het CPU-verbruik ongeveer 2.8 keer toeneemt. Het verschil is niet heel groot, maar de kleine Greta is al blij en heeft misschien tijd om naar school te gaan en lessen te volgen.

Dit zal dus een positief effect hebben voor elke tool die bulktoegang tot HDFS gebruikt (bijvoorbeeld Spark, enz.), op voorwaarde dat de applicatiecode licht van gewicht is (dat wil zeggen dat de plug zich aan de HDFS-clientzijde bevindt) en er vrije CPU-kracht is. . Laten we, om dit te controleren, testen welk effect het gecombineerde gebruik van BlockCache-optimalisatie en SSC-afstemming voor het lezen van HBase zal hebben.

Hoe u de leessnelheid van HBase tot 3 keer kunt verhogen en van HDFS tot 5 keer

Het is duidelijk dat onder dergelijke omstandigheden het effect niet zo groot is als bij verfijnde tests (lezen zonder enige verwerking), maar het is heel goed mogelijk om hier nog eens 80K uit te persen. Samen zorgen beide optimalisaties voor een snelheidsverbetering van maximaal 4x.

Ook voor deze optimalisatie is een PR gemaakt [HDFS-15202], dat is samengevoegd en deze functionaliteit zal in toekomstige releases beschikbaar zijn.

En tot slot was het interessant om de leesprestaties van een vergelijkbare database met brede kolommen, Cassandra en HBase, te vergelijken.

Om dit te doen, hebben we exemplaren van het standaard YCSB-hulpprogramma voor belastingtests gelanceerd vanaf twee hosts (in totaal 800 threads). Aan de serverzijde: 4 instances van RegionServer en Cassandra op 4 hosts (niet degene waarop de clients draaien, om hun invloed te vermijden). De metingen kwamen uit tabellen met afmetingen:

HBase – 300 GB op HDFS (100 GB pure data)

Cassandra - 250 GB (replicatiefactor = 3)

Die. het volume was ongeveer hetzelfde (in HBase iets meer).

HBase-parameters:

dfs.client.kortsluiting.num = 5 (HDFS-clientoptimalisatie)

hbase.lru.cache.heavy.eviction.count.limit = 30 - dit betekent dat de patch begint te werken na 30 uitzettingen (~5 minuten)

hbase.lru.cache.heavy.uitzetting.mb.size.limit = 300 — doelvolume van caching en uitzetting

YCSB-logboeken werden geparseerd en gecompileerd in Excel-grafieken:

Hoe u de leessnelheid van HBase tot 3 keer kunt verhogen en van HDFS tot 5 keer

Zoals u kunt zien, maken deze optimalisaties het mogelijk om de prestaties van deze databases onder deze omstandigheden te vergelijken en 450 leesbewerkingen per seconde te bereiken.

We hopen dat deze informatie nuttig kan zijn voor iemand tijdens de spannende strijd om productiviteit.

Bron: www.habr.com

Voeg een reactie