Sådan øges læsehastigheden fra HBase op til 3 gange og fra HDFS op til 5 gange

Høj ydeevne er et af nøglekravene, når man arbejder med big data. I dataload-afdelingen hos Sberbank pumper vi næsten alle transaktioner ind i vores Hadoop-baserede Data Cloud og håndterer derfor virkelig store informationsstrømme. Naturligvis leder vi altid efter måder at forbedre ydeevnen på, og nu vil vi gerne fortælle dig, hvordan vi formåede at patche RegionServer HBase og HDFS-klienten, takket være hvilken vi var i stand til at øge hastigheden af ​​læseoperationer markant.
Sådan øges læsehastigheden fra HBase op til 3 gange og fra HDFS op til 5 gange

Inden du går videre til essensen af ​​forbedringerne, er det dog værd at tale om begrænsninger, som i princippet ikke kan omgås, hvis du sidder på en HDD.

Hvorfor HDD og hurtig Random Access-læsning er inkompatible
Som du ved, lagrer HBase og mange andre databaser data i blokke på flere titusinder af kilobytes i størrelse. Som standard er det omkring 64 KB. Lad os nu forestille os, at vi kun skal have 100 bytes, og vi beder HBase om at give os disse data ved hjælp af en bestemt nøgle. Da blokstørrelsen i HFiles er 64 KB, vil anmodningen være 640 gange større (kun et minut!) end nødvendigt.

Dernæst, da anmodningen vil gå gennem HDFS og dens metadata-cachemekanisme ShortCircuitCache (hvilket giver direkte adgang til filer), fører dette til, at der allerede læses 1 MB fra disken. Dette kan dog justeres med parameteren dfs.client.read.shortcircuit.buffer.størrelse og i mange tilfælde giver det mening at reducere denne værdi, for eksempel til 126 KB.

Lad os sige, at vi gør dette, men derudover, når vi begynder at læse data gennem java-api'et, såsom funktioner som FileChannel.read og beder operativsystemet om at læse den angivne mængde data, så læser det "just in case" 2 gange mere , dvs. 256 KB i vores tilfælde. Dette skyldes, at java ikke har en nem måde at indstille flaget FADV_RANDOM for at forhindre denne adfærd.

Som et resultat, for at få vores 100 bytes, læses 2600 gange mere under motorhjelmen. Det ser ud til, at løsningen er indlysende, lad os reducere blokstørrelsen til en kilobyte, sætte det nævnte flag og få stor oplysningsacceleration. Men problemet er, at ved at reducere blokstørrelsen med 2 gange, reducerer vi også antallet af læste bytes pr. tidsenhed med 2 gange.

En vis gevinst ved at indstille FADV_RANDOM-flaget kan opnås, men kun med høj multi-threading og med en blokstørrelse på 128 KB, men dette er maksimalt et par tiere af procent:

Sådan øges læsehastigheden fra HBase op til 3 gange og fra HDFS op til 5 gange

Test blev udført på 100 filer, hver 1 GB i størrelse og placeret på 10 HDD'er.

Lad os beregne, hvad vi i princippet kan regne med ved denne hastighed:
Lad os sige, at vi læser fra 10 diske med en hastighed på 280 MB/sek., dvs. 3 millioner gange 100 bytes. Men som vi husker, er de data, vi har brug for, 2600 gange mindre end det, vi læser. Således deler vi 3 millioner med 2600 og får 1100 poster i sekundet.

Deprimerende, ikke? Det er naturen Tilfældig adgang adgang til data på HDD'en - uanset blokstørrelsen. Dette er den fysiske grænse for tilfældig adgang, og ingen database kan presse mere ud under sådanne forhold.

Hvordan opnår databaser så meget højere hastigheder? For at besvare dette spørgsmål, lad os se på, hvad der sker på følgende billede:

Sådan øges læsehastigheden fra HBase op til 3 gange og fra HDFS op til 5 gange

Her ser vi, at de første par minutter er hastigheden reelt omkring tusind rekorder i sekundet. Men yderligere, på grund af det faktum, at der læses meget mere, end der blev anmodet om, ender dataene i buff/cachen af ​​operativsystemet (linux), og hastigheden stiger til mere anstændige 60 tusinde pr.

Således vil vi yderligere beskæftige os med kun at accelerere adgangen til de data, der er i OS-cachen eller placeret i SSD/NVMe-lagerenheder med sammenlignelig adgangshastighed.

I vores tilfælde vil vi udføre test på en bænk med 4 servere, som hver opkræves som følger:

CPU: Xeon E5-2680 v4 @ 2.40GHz 64 tråde.
Hukommelse: 730 GB.
java version: 1.8.0_111

Og her er nøglepunktet mængden af ​​data i tabellerne, der skal læses. Faktum er, at hvis du læser data fra en tabel, der er helt placeret i HBase-cachen, så kommer det ikke engang til at læse fra operativsystemets buff/cache. Fordi HBase som standard allokerer 40% af hukommelsen til en struktur kaldet BlockCache. I bund og grund er dette et ConcurrentHashMap, hvor nøglen er filnavnet + forskydning af blokken, og værdien er de faktiske data ved denne forskydning.

Når vi kun læser fra denne struktur, vil vi vi ser fremragende fart, som en million anmodninger i sekundet. Men lad os forestille os, at vi ikke kan allokere hundredvis af gigabyte hukommelse kun til databasebehov, fordi der er en masse andre nyttige ting, der kører på disse servere.

For eksempel, i vores tilfælde, er volumen af ​​BlockCache på en RS omkring 12 GB. Vi landede to RS på én knude, dvs. 96 GB er allokeret til BlockCache på alle noder. Og der er mange gange mere data, lad det for eksempel være 4 tabeller, 130 regioner hver, hvor filerne er 800 MB store, komprimeret af FAST_DIFF, dvs. i alt 410 GB (dette er rene data, dvs. uden hensyntagen til replikeringsfaktoren).

BlockCache er således kun omkring 23% af den samlede datamængde, og dette er meget tættere på de reelle forhold i det, der kaldes BigData. Og det er her, det sjove begynder - for selvfølgelig, jo færre cache-hits, jo dårligere præstation. Når alt kommer til alt, hvis du savner, skal du gøre en masse arbejde - dvs. gå ned til opkaldssystemfunktioner. Dette kan dog ikke undgås, så lad os se på et helt andet aspekt – hvad sker der med dataene inde i cachen?

Lad os forenkle situationen og antage, at vi har en cache, der kun passer til 1 objekt. Her er et eksempel på, hvad der vil ske, når vi forsøger at arbejde med en datavolumen 3 gange større end cachen, vi bliver nødt til:

1. Placer blok 1 i cachen
2. Fjern blok 1 fra cachen
3. Placer blok 2 i cachen
4. Fjern blok 2 fra cachen
5. Placer blok 3 i cachen

5 handlinger gennemført! Denne situation kan dog ikke kaldes normal; faktisk tvinger vi HBase til at udføre en masse fuldstændig ubrugeligt arbejde. Den læser konstant data fra OS-cachen, placerer dem i BlockCache, for kun at smide dem ud næsten med det samme, fordi en ny del af data er ankommet. Animationen i begyndelsen af ​​indlægget viser essensen af ​​problemet - Garbage Collector går ud af skala, atmosfæren varmes op, lille Greta i det fjerne og varme Sverige bliver ked af det. Og vi it-folk kan virkelig ikke lide, når børn er triste, så vi begynder at tænke på, hvad vi kan gøre ved det.

Hvad hvis du ikke lægger alle blokke i cachen, men kun en vis procentdel af dem, så cachen ikke flyder over? Lad os starte med blot at tilføje nogle få linjer kode til begyndelsen af ​​funktionen til at lægge data ind i BlockCache:

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

Pointen her er følgende: offset er placeringen af ​​blokken i filen, og dens sidste cifre er tilfældigt og jævnt fordelt fra 00 til 99. Derfor springer vi kun over dem, der falder inden for det område, vi har brug for.

Indstil for eksempel cacheDataBlockPercent = 20 og se, hvad der sker:

Sådan øges læsehastigheden fra HBase op til 3 gange og fra HDFS op til 5 gange

Resultatet er indlysende. I graferne nedenfor bliver det tydeligt, hvorfor en sådan acceleration fandt sted - vi sparer en masse GC-ressourcer uden at gøre det sisyfiske arbejde med at placere data i cachen, kun for straks at kaste dem ned i afløbet af Mars-hundene:

Sådan øges læsehastigheden fra HBase op til 3 gange og fra HDFS op til 5 gange

Samtidig øges CPU-udnyttelsen, men er meget mindre end produktiviteten:

Sådan øges læsehastigheden fra HBase op til 3 gange og fra HDFS op til 5 gange

Det er også værd at bemærke, at blokkene, der er gemt i BlockCache, er forskellige. Det meste, omkring 95 %, er selve data. Og resten er metadata, såsom Bloom-filtre eller LEAF_INDEX og т.д.. Disse data er ikke nok, men de er meget nyttige, for før HBase får direkte adgang til dataene, vender HBase sig til metaen for at forstå, om det er nødvendigt at søge her yderligere, og i givet fald, hvor præcis interesseblokken er placeret.

Derfor ser vi i koden en kontrolbetingelse buf.getBlockType().isData() og takket være denne meta vil vi under alle omstændigheder efterlade den i cachen.

Lad os nu øge belastningen og stramme funktionen lidt op på én gang. I den første test lavede vi cutoff-procenten = 20, og BlockCache var lidt underudnyttet. Lad os nu sætte den til 23 % og tilføje 100 tråde hvert 5. minut for at se, hvornår mætning sker:

Sådan øges læsehastigheden fra HBase op til 3 gange og fra HDFS op til 5 gange

Her ser vi, at den originale version næsten øjeblikkeligt rammer loftet med omkring 100 tusinde anmodninger i sekundet. Hvorimod patchen giver en acceleration på op til 300 tusind. Samtidig er det klart, at yderligere acceleration ikke længere er så "gratis", CPU-udnyttelsen er også stigende.

Dette er dog ikke en særlig elegant løsning, da vi ikke på forhånd ved hvor stor en procentdel af blokkene der skal cache, det afhænger af belastningsprofilen. Derfor blev der implementeret en mekanisme til automatisk at justere denne parameter afhængigt af aktiviteten af ​​læseoperationer.

Der er tilføjet tre muligheder for at styre dette:

hbase.lru.cache.heavy.eviction.count. limit — indstiller, hvor mange gange processen med at fjerne data fra cachen skal køre, før vi begynder at bruge optimering (dvs. springe blokke over). Som standard er den lig med MAX_INT = 2147483647 og betyder faktisk, at funktionen aldrig vil begynde at arbejde med denne værdi. Fordi udsættelsesprocessen starter hvert 5. - 10. sekund (det afhænger af belastningen) og 2147483647 * 10 / 60 / 60 / 24 / 365 = 680 år. Vi kan dog indstille denne parameter til 0 og få funktionen til at virke umiddelbart efter lanceringen.

Der er dog også en nyttelast i denne parameter. Hvis vores belastning er sådan, at korttidslæsninger (f.eks. i løbet af dagen) og langtidslæsninger (om natten) konstant er indblandet, så kan vi sørge for, at funktionen kun er tændt, når lange læseoperationer er i gang.

For eksempel ved vi, at korttidsaflæsninger normalt varer omkring 1 minut. Der er ingen grund til at begynde at smide blokke ud, cachen vil ikke nå at blive forældet, og så kan vi sætte denne parameter lig med f.eks. 10. Dette vil føre til, at optimeringen først begynder at virke, når lang- semester aktiv læsning er begyndt, dvs. på 100 sekunder. Således, hvis vi har en kortsigtet læsning, vil alle blokke gå ind i cachen og vil være tilgængelige (undtagen dem, der vil blive smidt ud af standardalgoritmen). Og når vi laver langtidslæsninger, er funktionen slået til, og vi ville have meget højere ydeevne.

hbase.lru.cache.heavy.eviction.mb.size.limit — indstiller, hvor mange megabyte vi gerne vil placere i cachen (og selvfølgelig evice) på 10 sekunder. Funktionen vil forsøge at nå denne værdi og vedligeholde den. Pointen er denne: Hvis vi skubber gigabyte ind i cachen, så bliver vi nødt til at smide gigabytes ud, og dette er, som vi så ovenfor, meget dyrt. Du bør dog ikke prøve at sætte den for lille, da dette vil få blokoverspringstilstanden til at afslutte for tidligt. For kraftige servere (ca. 20-40 fysiske kerner) er det optimalt at indstille omkring 300-400 MB. For middelklassen (~10 kerner) 200-300 MB. For svage systemer (2-5 kerner) kan 50-100 MB være normalt (ikke testet på disse).

Lad os se på, hvordan dette virker: lad os sige, at vi sætter hbase.lru.cache.heavy.eviction.mb.size.limit = 500, der er en form for belastning (læsning), og så beregner vi for hvert ~10. sekund, hvor mange bytes der var smidt ud ved hjælp af formlen:

Overhead = Frigjorte bytes sum (MB) * 100 / grænse (MB) - 100;

Hvis 2000 MB faktisk blev smidt ud, så er Overhead lig med:

2000 * 100 / 500 - 100 = 300 %

Algoritmerne forsøger ikke at vedligeholde mere end et par tiere af procent, så funktionen vil reducere procentdelen af ​​cachelagrede blokke og derved implementere en auto-tuning-mekanisme.

Men hvis belastningen falder, lad os sige, at kun 200 MB bliver smidt ud, og Overhead bliver negativ (den såkaldte overskydning):

200 * 100 / 500 - 100 = -60 %

Tværtimod vil funktionen øge procentdelen af ​​cachelagrede blokke, indtil Overhead bliver positivt.

Nedenfor er et eksempel på, hvordan dette ser ud på rigtige data. Der er ingen grund til at forsøge at nå 0%, det er umuligt. Det er meget godt, når det er omkring 30 - 100%, dette hjælper med at undgå for tidlig udgang fra optimeringstilstanden under kortvarige stigninger.

hbase.lru.cache.heavy.eviction.overhead.koefficient — angiver, hvor hurtigt vi gerne vil have resultatet. Hvis vi med sikkerhed ved, at vores læsninger for det meste er lange og ikke ønsker at vente, kan vi øge dette forhold og få høj ydeevne hurtigere.

For eksempel sætter vi denne koefficient = 0.01. Dette betyder, at Overhead (se ovenfor) vil blive ganget med dette tal med det resulterende resultat, og procentdelen af ​​cachelagrede blokke vil blive reduceret. Lad os antage, at Overhead = 300% og koefficient = 0.01, så vil procentdelen af ​​cachelagrede blokke blive reduceret med 3%.

En lignende "modtryk"-logik er også implementeret for negative overhead-værdier (overskydning). Da kortsigtede udsving i mængden af ​​aflæsninger og udsættelser altid er mulige, giver denne mekanisme dig mulighed for at undgå for tidlig udgang fra optimeringstilstanden. Modtryk har en omvendt logik: Jo stærkere overskridelse, jo flere blokke cachelagres.

Sådan øges læsehastigheden fra HBase op til 3 gange og fra HDFS op til 5 gange

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

Lad os nu se på alt dette ved at bruge et rigtigt eksempel. Vi har følgende testscript:

  1. Lad os begynde at scanne (25 tråde, batch = 100)
  2. Efter 5 minutter tilføjes multi-gets (25 tråde, batch = 100)
  3. Efter 5 minutter skal du deaktivere multi-gets (kun scanningen er tilbage igen)

Vi udfører to kørsler, først hbase.lru.cache.heavy.eviction.count.limit = 10000 (hvilket faktisk deaktiverer funktionen), og derefter sætter grænsen = 0 (aktiverer den).

I loggene nedenfor ser vi, hvordan funktionen er slået til og nulstiller Overshooting til 14-71%. Fra tid til anden falder belastningen, hvilket slår modtryk til, og HBase cacher flere blokke igen.

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

Scanningerne var nødvendige for at vise den samme proces i form af en graf over forholdet mellem to cache sektioner - enkelt (hvor blokke, der aldrig er blevet anmodet om før) og multi (data "anmodet" mindst én gang gemmes her):

Sådan øges læsehastigheden fra HBase op til 3 gange og fra HDFS op til 5 gange

Og endelig, hvordan ser driften af ​​parametrene ud i form af en graf. Til sammenligning var cachen helt slået fra i starten, derefter blev HBase lanceret med caching og forsinkelse af starten af ​​optimeringsarbejdet med 5 minutter (30 eviction-cyklusser).

Fuld kode kan findes i Pull Request HBASE 23887 på github.

Men 300 tusinde aflæsninger pr. sekund er ikke alt, der kan opnås på denne hardware under disse forhold. Faktum er, at når du skal have adgang til data via HDFS, bruges ShortCircuitCache (herefter benævnt SSC) mekanismen, som giver dig mulighed for at få direkte adgang til dataene og undgår netværksinteraktioner.

Profilering viste, at selvom denne mekanisme giver en stor gevinst, bliver den også på et tidspunkt en flaskehals, fordi næsten alle tunge operationer foregår inde i en lås, hvilket fører til blokering det meste af tiden.

Sådan øges læsehastigheden fra HBase op til 3 gange og fra HDFS op til 5 gange

Efter at have indset dette, indså vi, at problemet kan omgås ved at skabe en række uafhængige SSC'er:

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

Og arbejd så med dem, eksklusive skæringspunkter også ved det sidste offset-ciffer:

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

Nu kan du begynde at teste. For at gøre dette læser vi filer fra HDFS med et simpelt multi-threaded program. Indstil parametrene:

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 læs bare filerne:

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 kode udføres i separate tråde, og vi vil øge antallet af samtidigt læste filer (fra 10 til 200 - vandret akse) og antallet af caches (fra 1 til 10 - grafik). Den lodrette akse viser den acceleration, der er resultatet af en stigning i SSC i forhold til tilfældet, når der kun er én cache.

Sådan øges læsehastigheden fra HBase op til 3 gange og fra HDFS op til 5 gange

Sådan læser du grafen: Udførelsestiden for 100 tusind læsninger i 64 KB blokke med én cache kræver 78 sekunder. Hvorimod det med 5 caches tager 16 sekunder. De der. der er en acceleration på ~5 gange. Som det kan ses af grafen, er effekten ikke særlig mærkbar for et lille antal parallelle aflæsninger, den begynder at spille en mærkbar rolle, når der er mere end 50 trådlæsninger. Det er også bemærkelsesværdigt, at man øger antallet af SSC'er fra 6 og derover giver en væsentlig mindre ydelsesforøgelse.

Note 1: Da testresultaterne er ret flygtige (se nedenfor), blev der udført 3 kørsler og gennemsnittet af de resulterende værdier.

Note 2: Ydeevnegevinsten ved at konfigurere tilfældig adgang er den samme, selvom adgangen i sig selv er lidt langsommere.

Det er dog nødvendigt at præcisere, at i modsætning til tilfældet med HBase, er denne acceleration ikke altid gratis. Her "låser" vi op for CPU'ens evne til at udføre arbejde mere, i stedet for at hænge på låse.

Sådan øges læsehastigheden fra HBase op til 3 gange og fra HDFS op til 5 gange

Her kan man konstatere, at en stigning i antallet af caches generelt giver en tilnærmelsesvis proportional stigning i CPU-udnyttelsen. Der er dog lidt flere vindende kombinationer.

Lad os for eksempel se nærmere på indstillingen SSC = 3. Forøgelsen i ydeevne på området er omkring 3.3 gange. Nedenfor er resultaterne fra alle tre separate kørsler.

Sådan øges læsehastigheden fra HBase op til 3 gange og fra HDFS op til 5 gange

Mens CPU-forbruget stiger med omkring 2.8 gange. Forskellen er ikke særlig stor, men lille Greta er allerede glad og har måske tid til at gå i skole og tage timer.

Dette vil således have en positiv effekt for ethvert værktøj, der bruger masseadgang til HDFS (for eksempel Spark osv.), forudsat at applikationskoden er letvægts (dvs. stikket er på HDFS-klientsiden), og der er ledig CPU-strøm . Lad os for at kontrollere, hvilken effekt den kombinerede brug af BlockCache-optimering og SSC-tuning til læsning fra HBase vil have.

Sådan øges læsehastigheden fra HBase op til 3 gange og fra HDFS op til 5 gange

Det kan ses, at under sådanne forhold er effekten ikke så stor som i raffinerede tests (aflæsning uden nogen form for bearbejdning), men det er sagtens muligt at presse yderligere 80K ud her. Tilsammen giver begge optimeringer op til 4x speedup.

Der blev også lavet en PR for denne optimering [HDFS-15202], som er blevet slået sammen, og denne funktionalitet vil være tilgængelig i fremtidige udgivelser.

Og endelig var det interessant at sammenligne læseydeevnen for en lignende database med bred kolonne, Cassandra og HBase.

For at gøre dette lancerede vi forekomster af standard YCSB belastningstestværktøj fra to værter (800 tråde i alt). På serversiden - 4 forekomster af RegionServer og Cassandra på 4 værter (ikke dem, hvor klienterne kører, for at undgå deres indflydelse). Aflæsninger kom fra tabeller med størrelse:

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

Cassandra - 250 GB (replikationsfaktor = 3)

De der. volumen var omtrent den samme (i HBase lidt mere).

HBase parametre:

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

hbase.lru.cache.heavy.eviction.count.limit = 30 - det betyder, at plasteret begynder at virke efter 30 udsættelser (~5 minutter)

hbase.lru.cache.heavy.eviction.mb.size.limit = 300 — målvolumen for caching og udsættelse

YCSB-logfiler blev parset og kompileret til Excel-grafer:

Sådan øges læsehastigheden fra HBase op til 3 gange og fra HDFS op til 5 gange

Som du kan se, gør disse optimeringer det muligt at sammenligne ydeevnen af ​​disse databaser under disse forhold og opnå 450 tusinde aflæsninger i sekundet.

Vi håber, at denne information kan være nyttig for nogen under den spændende kamp for produktivitet.

Kilde: www.habr.com

Tilføj en kommentar