Hvordan øke lesehastigheten fra HBase opptil 3 ganger og fra HDFS opptil 5 ganger

Høy ytelse er et av nøkkelkravene når du arbeider med big data. I datainnlastingsavdelingen hos Sberbank pumper vi nesten alle transaksjoner inn i vår Hadoop-baserte Data Cloud og håndterer derfor virkelig store informasjonsflyter. Naturligvis leter vi alltid etter måter å forbedre ytelsen på, og nå vil vi fortelle deg hvordan vi klarte å lappe RegionServer HBase og HDFS-klienten, takket være at vi klarte å øke hastigheten på leseoperasjonene betydelig.
Hvordan øke lesehastigheten fra HBase opptil 3 ganger og fra HDFS opptil 5 ganger

Men før du går videre til essensen av forbedringene, er det verdt å snakke om begrensninger som i prinsippet ikke kan omgås hvis du sitter på en HDD.

Hvorfor HDD og raske Random Access-lesninger er inkompatible
Som du vet, lagrer HBase og mange andre databaser data i blokker på flere titalls kilobyte store. Som standard er den omtrent 64 KB. La oss nå forestille oss at vi bare trenger å få 100 byte, og vi ber HBase om å gi oss disse dataene ved hjelp av en bestemt nøkkel. Siden blokkstørrelsen i HFiles er 64 KB, vil forespørselen være 640 ganger større (bare et minutt!) enn nødvendig.

Neste, siden forespørselen vil gå gjennom HDFS og dens metadatabufringsmekanisme ShortCircuitCache (som gir direkte tilgang til filer), fører dette til å lese allerede 1 MB fra disken. Dette kan imidlertid justeres med parameteren dfs.client.read.shortcircuit.bufferstørrelse og i mange tilfeller er det fornuftig å redusere denne verdien, for eksempel til 126 KB.

La oss si at vi gjør dette, men i tillegg, når vi begynner å lese data gjennom java-api, for eksempel funksjoner som FileChannel.read og ber operativsystemet om å lese den spesifiserte mengden data, står det "i tilfelle" 2 ganger mer , dvs. 256 KB i vårt tilfelle. Dette er fordi java ikke har en enkel måte å sette FADV_RANDOM-flagget for å forhindre denne oppførselen.

Som et resultat, for å få våre 100 byte, leses 2600 ganger mer under panseret. Det ser ut til at løsningen er åpenbar, la oss redusere blokkstørrelsen til en kilobyte, sette det nevnte flagget og få stor opplysningsakselerasjon. Men problemet er at ved å redusere blokkstørrelsen med 2 ganger, reduserer vi også antall leste byte per tidsenhet med 2 ganger.

Noe gevinst ved å sette FADV_RANDOM-flagget kan oppnås, men bare med høy flertråding og med en blokkstørrelse på 128 KB, men dette er maksimalt et par titalls prosent:

Hvordan øke lesehastigheten fra HBase opptil 3 ganger og fra HDFS opptil 5 ganger

Tester ble utført på 100 filer, hver 1 GB stor og plassert på 10 harddisker.

La oss beregne hva vi i prinsippet kan regne med ved denne hastigheten:
La oss si at vi leser fra 10 disker med en hastighet på 280 MB/sek, dvs. 3 millioner ganger 100 byte. Men som vi husker, er dataene vi trenger 2600 ganger mindre enn det som leses. Dermed deler vi 3 millioner på 2600 og får 1100 poster per sekund.

Deprimerende, ikke sant? Det er naturen Tilfeldig tilgang tilgang til data på HDD - uavhengig av blokkstørrelse. Dette er den fysiske grensen for tilfeldig tilgang, og ingen database kan presse ut mer under slike forhold.

Hvordan oppnår da databaser mye høyere hastigheter? For å svare på dette spørsmålet, la oss se på hva som skjer i følgende bilde:

Hvordan øke lesehastigheten fra HBase opptil 3 ganger og fra HDFS opptil 5 ganger

Her ser vi at de første minuttene er hastigheten egentlig rundt tusen rekorder i sekundet. Men videre, på grunn av det faktum at mye mer leses enn det som ble forespurt, havner dataene i buff/cachen til operativsystemet (linux) og hastigheten øker til mer anstendige 60 tusen per sekund

Derfor vil vi videre behandle akselererende tilgang bare til dataene som er i OS-bufferen eller plassert i SSD/NVMe-lagringsenheter med sammenlignbar tilgangshastighet.

I vårt tilfelle vil vi gjennomføre tester på en benk med 4 servere, som hver belastes som følger:

CPU: Xeon E5-2680 v4 @ 2.40GHz 64 tråder.
Minne: 730 GB.
java-versjon: 1.8.0_111

Og her er nøkkelpunktet mengden data i tabellene som må leses. Faktum er at hvis du leser data fra en tabell som er helt plassert i HBase-cachen, vil det ikke engang komme til å lese fra operativsystemets buff/cache. Fordi HBase som standard allokerer 40% av minnet til en struktur kalt BlockCache. I hovedsak er dette et ConcurrentHashMap, der nøkkelen er filnavnet + forskyvning av blokken, og verdien er de faktiske dataene ved denne forskyvningen.

Når vi kun leser fra denne strukturen, vil vi vi ser utmerket fart, som en million forespørsler per sekund. Men la oss forestille oss at vi ikke kan tildele hundrevis av gigabyte minne bare for databasebehov, fordi det er mange andre nyttige ting som kjører på disse serverne.

For eksempel, i vårt tilfelle, er volumet av BlockCache på en RS omtrent 12 GB. Vi landet to RS på en node, dvs. 96 GB er tildelt for BlockCache på alle noder. Og det er mange ganger mer data, for eksempel, la det være 4 tabeller, 130 regioner hver, der filene er 800 MB store, komprimert av FAST_DIFF, dvs. totalt 410 GB (dette er rene data, dvs. uten å ta hensyn til replikeringsfaktoren).

Dermed utgjør BlockCache bare omtrent 23 % av det totale datavolumet og dette er mye nærmere de reelle forholdene til det som kalles BigData. Og det er her moroa begynner - for selvsagt, jo færre cache-treff, jo dårligere ytelse. Tross alt, hvis du savner, må du gjøre mye arbeid - d.v.s. gå ned til ringesystemfunksjoner. Dette kan imidlertid ikke unngås, så la oss se på et helt annet aspekt - hva skjer med dataene inne i cachen?

La oss forenkle situasjonen og anta at vi har en cache som bare passer til 1 objekt. Her er et eksempel på hva som vil skje når vi prøver å jobbe med et datavolum 3 ganger større enn cachen, vi må:

1. Plasser blokk 1 i cachen
2. Fjern blokk 1 fra hurtigbufferen
3. Plasser blokk 2 i cachen
4. Fjern blokk 2 fra hurtigbufferen
5. Plasser blokk 3 i cachen

5 handlinger fullført! Denne situasjonen kan imidlertid ikke kalles normal; faktisk tvinger vi HBase til å gjøre en haug med helt ubrukelig arbeid. Den leser konstant data fra OS-cachen, plasserer den i BlockCache, bare for å kaste den ut nesten umiddelbart fordi en ny del av data har kommet. Animasjonen i begynnelsen av innlegget viser essensen av problemet – Garbage Collector går av skala, atmosfæren varmes opp, lille Greta i det fjerne og varme Sverige blir opprørt. Og vi IT-folk liker virkelig ikke det når barn er triste, så vi begynner å tenke på hva vi kan gjøre med det.

Hva om du legger ikke alle blokkene i cachen, men bare en viss prosentandel av dem, slik at cachen ikke renner over? La oss starte med å bare legge til noen få linjer med kode i begynnelsen av funksjonen for å legge data inn i BlockCache:

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

Poenget her er følgende: offset er posisjonen til blokken i filen og dens siste sifre er tilfeldig og jevnt fordelt fra 00 til 99. Derfor vil vi bare hoppe over de som faller innenfor området vi trenger.

Sett for eksempel cacheDataBlockPercent = 20 og se hva som skjer:

Hvordan øke lesehastigheten fra HBase opptil 3 ganger og fra HDFS opptil 5 ganger

Resultatet er åpenbart. I grafene nedenfor blir det klart hvorfor en slik akselerasjon skjedde - vi sparer mye GC-ressurser uten å gjøre det sisyfiske arbeidet med å plassere data i hurtigbufferen, bare for å umiddelbart kaste dem ned i avløpet til marshundene:

Hvordan øke lesehastigheten fra HBase opptil 3 ganger og fra HDFS opptil 5 ganger

Samtidig øker CPU-utnyttelsen, men er mye mindre enn produktiviteten:

Hvordan øke lesehastigheten fra HBase opptil 3 ganger og fra HDFS opptil 5 ganger

Det er også verdt å merke seg at blokkene som er lagret i BlockCache er forskjellige. Det meste, omtrent 95 %, er selve data. Og resten er metadata, for eksempel Bloom-filtre eller LEAF_INDEX og т.д.. Disse dataene er ikke nok, men de er veldig nyttige, for før HBase får tilgang til dataene direkte, vender HBase seg til metaen for å forstå om det er nødvendig å søke videre her og i så fall hvor nøyaktig interesseblokken befinner seg.

Derfor ser vi i koden en kontrollbetingelse buf.getBlockType().isData() og takket være denne metaen vil vi uansett la den ligge i cachen.

La oss nå øke belastningen og stramme opp funksjonen litt på én gang. I den første testen gjorde vi cutoff-prosenten = 20 og BlockCache var litt underutnyttet. La oss nå sette den til 23 % og legge til 100 tråder hvert 5. minutt for å se på hvilket tidspunkt metning skjer:

Hvordan øke lesehastigheten fra HBase opptil 3 ganger og fra HDFS opptil 5 ganger

Her ser vi at originalversjonen nesten umiddelbart treffer taket med rundt 100 tusen forespørsler per sekund. Mens lappen gir en akselerasjon på opptil 300 tusen. Samtidig er det klart at ytterligere akselerasjon ikke lenger er så "gratis"; CPU-utnyttelsen øker også.

Dette er imidlertid ikke en veldig elegant løsning, siden vi ikke på forhånd vet hvor mange prosent av blokkene som må bufres, det avhenger av lastprofilen. Derfor ble en mekanisme implementert for å automatisk justere denne parameteren avhengig av aktiviteten til leseoperasjoner.

Tre alternativer er lagt til for å kontrollere dette:

hbase.lru.cache.heavy.eviction.count.limit — angir hvor mange ganger prosessen med å kaste ut data fra cachen skal kjøre før vi begynner å bruke optimalisering (dvs. hoppe over blokker). Som standard er den lik MAX_INT = 2147483647 og betyr faktisk at funksjonen aldri vil begynne å fungere med denne verdien. Fordi utkastelsesprosessen starter hvert 5. - 10. sekund (det avhenger av belastningen) og 2147483647 * 10 / 60 / 60 / 24 / 365 = 680 år. Vi kan imidlertid sette denne parameteren til 0 og få funksjonen til å fungere umiddelbart etter lansering.

Imidlertid er det også en nyttelast i denne parameteren. Hvis belastningen vår er slik at korttidsavlesninger (f.eks. på dagtid) og langtidsavlesninger (om natten) hele tiden er ispedd, så kan vi sørge for at funksjonen bare slås på når lange leseoperasjoner pågår.

For eksempel vet vi at korttidsavlesninger vanligvis varer i omtrent 1 minutt. Det er ikke nødvendig å begynne å kaste ut blokker, cachen vil ikke ha tid til å bli utdatert og da kan vi sette denne parameteren lik for eksempel 10. Dette vil føre til at optimaliseringen vil begynne å virke først når lang- termin aktiv lesing har begynt, dvs. på 100 sekunder. Derfor, hvis vi har en kortsiktig lesing, vil alle blokker gå inn i hurtigbufferen og vil være tilgjengelige (bortsett fra de som vil bli kastet ut av standardalgoritmen). Og når vi gjør langtidslesninger, er funksjonen slått på, og vi vil ha mye høyere ytelse.

hbase.lru.cache.heavy.eviction.mb.size.limit — angir hvor mange megabyte vi ønsker å plassere i hurtigbufferen (og, selvfølgelig, kaste ut) på 10 sekunder. Funksjonen vil prøve å nå denne verdien og opprettholde den. Poenget er dette: hvis vi skyver gigabyte inn i cachen, må vi kaste ut gigabyte, og dette, som vi så ovenfor, er veldig dyrt. Du bør imidlertid ikke prøve å sette den for liten, da dette vil føre til at blokkhoppmodusen avsluttes for tidlig. For kraftige servere (ca. 20-40 fysiske kjerner) er det optimalt å sette ca. 300-400 MB. For middelklassen (~10 kjerner) 200-300 MB. For svake systemer (2-5 kjerner) kan 50-100 MB være normalt (ikke testet på disse).

La oss se på hvordan dette fungerer: la oss si at vi setter hbase.lru.cache.heavy.eviction.mb.size.limit = 500, det er en slags belastning (lesing) og deretter hvert ~10. sekund beregner vi hvor mange byte som var kastet ut ved hjelp av formelen:

Overhead = Frigjorte bytes sum (MB) * 100 / grense (MB) - 100;

Hvis faktisk 2000 MB ble kastet ut, er Overhead lik:

2000 * 100 / 500 - 100 = 300 %

Algoritmene prøver å opprettholde ikke mer enn noen få titalls prosent, så funksjonen vil redusere prosentandelen av hurtigbufrede blokker, og dermed implementere en auto-tuning-mekanisme.

Men hvis belastningen synker, la oss si at bare 200 MB blir kastet ut og Overhead blir negativ (den såkalte overshooting):

200 * 100 / 500 - 100 = -60 %

Tvert imot vil funksjonen øke prosentandelen av bufrede blokker til Overhead blir positiv.

Nedenfor er et eksempel på hvordan dette ser ut på ekte data. Det er ikke nødvendig å prøve å nå 0%, det er umulig. Det er veldig bra når det er ca. 30 - 100 %, dette bidrar til å unngå for tidlig utgang fra optimaliseringsmodus under kortvarige overspenninger.

hbase.lru.cache.heavy.eviction.overhead.coefficient — angir hvor raskt vi ønsker å få resultatet. Hvis vi vet med sikkerhet at lesingene våre for det meste er lange og ikke ønsker å vente, kan vi øke dette forholdet og få høy ytelse raskere.

For eksempel setter vi denne koeffisienten = 0.01. Dette betyr at Overhead (se ovenfor) vil bli multiplisert med dette tallet med det resulterende resultatet og prosentandelen av hurtigbufrede blokker vil bli redusert. La oss anta at Overhead = 300 % og koeffisient = 0.01, så vil prosentandelen av bufrede blokker reduseres med 3 %.

En lignende "mottrykk"-logikk er også implementert for negative overhead-verdier (overskyting). Siden kortsiktige svingninger i volumet av lesninger og utkastelser alltid er mulig, lar denne mekanismen deg unngå for tidlig utgang fra optimaliseringsmodusen. Mottrykk har en invertert logikk: jo sterkere oversving, jo flere blokker bufres.

Hvordan øke lesehastigheten fra HBase opptil 3 ganger og fra HDFS opptil 5 ganger

Implementeringskode

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

La oss nå se på alt dette ved å bruke et ekte eksempel. Vi har følgende testskript:

  1. La oss begynne å skanne (25 tråder, batch = 100)
  2. Etter 5 minutter, legg til multi-gets (25 tråder, batch = 100)
  3. Etter 5 minutter, slå av multi-gets (bare skanningen gjenstår igjen)

Vi kjører to ganger, først hbase.lru.cache.heavy.eviction.count.limit = 10000 (som faktisk deaktiverer funksjonen), og deretter setter vi grense = 0 (aktiverer den).

I loggene nedenfor ser vi hvordan funksjonen slås på og tilbakestiller Overshooting til 14-71%. Fra tid til annen avtar belastningen, noe som slår på mottrykk og HBase cacher flere blokker igjen.

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

Skanningene var nødvendig for å vise den samme prosessen i form av en graf over forholdet mellom to cache-seksjoner - enkelt (hvor blokker som aldri har blitt forespurt før) og multi (data "forespurt" minst én gang lagres her):

Hvordan øke lesehastigheten fra HBase opptil 3 ganger og fra HDFS opptil 5 ganger

Og til slutt, hvordan ser operasjonen av parameterne ut i form av en graf. Til sammenligning ble cachen helt slått av i begynnelsen, deretter ble HBase lansert med caching og forsinkelse av starten av optimaliseringsarbeidet med 5 minutter (30 utkastingssykluser).

Full kode finner du i Pull Request HBASE 23887 på github.

Imidlertid er 300 tusen avlesninger per sekund ikke alt som kan oppnås på denne maskinvaren under disse forholdene. Faktum er at når du trenger å få tilgang til data via HDFS, brukes ShortCircuitCache (heretter kalt SSC) mekanismen, som lar deg få tilgang til dataene direkte, og unngår nettverksinteraksjoner.

Profilering viste at selv om denne mekanismen gir en stor gevinst, blir den også på et tidspunkt en flaskehals, fordi nesten alle tunge operasjoner skjer inne i en lås, noe som fører til blokkering mesteparten av tiden.

Hvordan øke lesehastigheten fra HBase opptil 3 ganger og fra HDFS opptil 5 ganger

Etter å ha innsett dette, innså vi at problemet kan omgås ved å lage en rekke uavhengige SSCer:

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

Og jobb deretter med dem, unntatt kryss også ved det siste forskyvningssifferet:

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

Nå kan du begynne å teste. For å gjøre dette vil vi lese filer fra HDFS med en enkel flertrådsapplikasjon. Still inn parameterne:

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

Og bare les filene:

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

Denne koden kjøres i separate tråder, og vi vil øke antall samtidig leste filer (fra 10 til 200 - horisontal akse) og antall cacher (fra 1 til 10 - grafikk). Den vertikale aksen viser akselerasjonen som følger av en økning i SSC i forhold til tilfellet når det bare er én cache.

Hvordan øke lesehastigheten fra HBase opptil 3 ganger og fra HDFS opptil 5 ganger

Slik leser du grafen: Utførelsestiden for 100 tusen avlesninger i 64 KB-blokker med én cache krever 78 sekunder. Mens med 5 cacher tar det 16 sekunder. De. det er en akselerasjon på ~5 ganger. Som man kan se av grafen er effekten lite merkbar for et lite antall parallelle avlesninger, den begynner å spille en merkbar rolle når det er mer enn 50 trådavlesninger. Det er også merkbart at man øker antallet SSC-er fra 6 og over gir en betydelig mindre ytelsesøkning.

Merknad 1: siden testresultatene er ganske flyktige (se nedenfor), ble det utført 3 kjøringer og gjennomsnittet av de resulterende verdiene.

Merknad 2: Ytelsesgevinsten ved å konfigurere tilfeldig tilgang er den samme, selv om tilgangen i seg selv er litt tregere.

Det er imidlertid nødvendig å klargjøre at, i motsetning til tilfellet med HBase, er ikke denne akselerasjonen alltid gratis. Her "låser" vi opp CPUens evne til å jobbe mer, i stedet for å henge på låser.

Hvordan øke lesehastigheten fra HBase opptil 3 ganger og fra HDFS opptil 5 ganger

Her kan du observere at generelt sett gir en økning i antall cacher en tilnærmet proporsjonal økning i CPU-utnyttelsen. Det er imidlertid litt flere vinnende kombinasjoner.

La oss for eksempel se nærmere på innstillingen SSC = 3. Ytelsesøkningen på området er omtrent 3.3 ganger. Nedenfor er resultatene fra alle tre separate løpene.

Hvordan øke lesehastigheten fra HBase opptil 3 ganger og fra HDFS opptil 5 ganger

Mens CPU-forbruket øker med omtrent 2.8 ganger. Forskjellen er ikke veldig stor, men lille Greta er allerede fornøyd og har kanskje tid til å gå på skolen og ta timer.

Dermed vil dette ha en positiv effekt for ethvert verktøy som bruker bulktilgang til HDFS (for eksempel Spark, etc.), forutsatt at applikasjonskoden er lett (dvs. pluggen er på HDFS-klientsiden) og det er ledig CPU-strøm . For å sjekke, la oss teste hvilken effekt den kombinerte bruken av BlockCache-optimalisering og SSC-innstilling for lesing fra HBase vil ha.

Hvordan øke lesehastigheten fra HBase opptil 3 ganger og fra HDFS opptil 5 ganger

Man kan se at under slike forhold er ikke effekten like stor som i raffinerte tester (lesing uten noen form for prosessering), men det er fullt mulig å presse ut ytterligere 80K her. Sammen gir begge optimaliseringene opptil 4x hastighetsøkning.

Det ble også laget en PR for denne optimaliseringen [HDFS-15202], som er slått sammen og denne funksjonaliteten vil være tilgjengelig i fremtidige utgivelser.

Og til slutt var det interessant å sammenligne leseytelsen til en lignende database med bred kolonne, Cassandra og HBase.

For å gjøre dette lanserte vi forekomster av standard YCSB-lasttestingsverktøy fra to verter (800 tråder totalt). På serversiden - 4 forekomster av RegionServer og Cassandra på 4 verter (ikke de der klientene kjører, for å unngå deres innflytelse). Avlesningene kom fra tabeller med størrelse:

HBase – 300 GB på HDFS (100 GB rene data)

Cassandra - 250 GB (replikeringsfaktor = 3)

De. volumet var omtrent det samme (i HBase litt mer).

HBase parametere:

dfs.client.short.circuit.num = 5 (HDFS-klientoptimalisering)

hbase.lru.cache.heavy.eviction.count.limit = 30 - dette betyr at lappen vil begynne å virke etter 30 utkastelser (~5 minutter)

hbase.lru.cache.heavy.eviction.mb.size.limit = 300 — målvolum for caching og utkastelse

YCSB-logger ble analysert og kompilert til Excel-grafer:

Hvordan øke lesehastigheten fra HBase opptil 3 ganger og fra HDFS opptil 5 ganger

Som du kan se, gjør disse optimaliseringene det mulig å sammenligne ytelsen til disse databasene under disse forholdene og oppnå 450 tusen avlesninger per sekund.

Vi håper denne informasjonen kan være nyttig for noen under den spennende kampen for produktivitet.

Kilde: www.habr.com

Legg til en kommentar