Kako povečati hitrost branja iz HBase do 3-krat in iz HDFS do 5-krat

Visoka zmogljivost je ena ključnih zahtev pri delu z velikimi podatki. V oddelku za nalaganje podatkov pri Sberbank črpamo skoraj vse transakcije v naš podatkovni oblak, ki temelji na Hadoopu, in se zato ukvarjamo z res velikimi tokovi informacij. Seveda vedno iščemo načine za izboljšanje zmogljivosti, zdaj pa vam želimo povedati, kako nam je uspelo popraviti RegionServer HBase in odjemalca HDFS, zaradi česar smo lahko znatno povečali hitrost branja.
Kako povečati hitrost branja iz HBase do 3-krat in iz HDFS do 5-krat

Preden pa preidemo na bistvo izboljšav, je vredno govoriti o omejitvah, ki jih načeloma ni mogoče zaobiti, če sedite na trdem disku.

Zakaj HDD in hitro branje z naključnim dostopom nista združljiva
Kot veste, HBase in številne druge zbirke podatkov shranjujejo podatke v blokih, velikih nekaj deset kilobajtov. Privzeto je približno 64 KB. Zdaj pa si predstavljajmo, da potrebujemo samo 100 bajtov in prosimo HBase, da nam te podatke posreduje z uporabo določenega ključa. Ker je velikost bloka v HFiles 64 KB, bo zahteva 640-krat večja (samo minuto!), kot je potrebno.

Nato, ker bo zahteva šla skozi HDFS in njegov mehanizem za predpomnjenje metapodatkov ShortCircuitCache (ki omogoča neposreden dostop do datotek), to vodi do branja že 1 MB z diska. Vendar pa je to mogoče prilagoditi s parametrom dfs.client.read.shortcircuit.buffer.size in v mnogih primerih je smiselno to vrednost zmanjšati, na primer na 126 KB.

Recimo, da to storimo, vendar poleg tega, ko začnemo brati podatke prek API-ja Java, kot so funkcije, kot je FileChannel.read, in od operacijskega sistema zahtevamo, da prebere določeno količino podatkov, prebere »za vsak slučaj« 2-krat več , tj. 256 KB v našem primeru. To je zato, ker java nima enostavnega načina za nastavitev zastavice FADV_RANDOM, da bi preprečila to vedenje.

Kot rezultat, da bi dobili naših 100 bajtov, se pod pokrovom prebere 2600-krat več. Zdi se, da je rešitev očitna, zmanjšajmo velikost bloka na kilobajt, nastavimo omenjeno zastavico in pridobimo velik pospešek razsvetljenja. Toda težava je v tem, da z zmanjšanjem velikosti bloka za 2-krat zmanjšamo tudi število prebranih bajtov na časovno enoto za 2-krat.

Nekaj ​​dobička z nastavitvijo zastavice FADV_RANDOM je mogoče doseči, vendar le z visoko večnitnostjo in z velikostjo bloka 128 KB, vendar je to največ nekaj deset odstotkov:

Kako povečati hitrost branja iz HBase do 3-krat in iz HDFS do 5-krat

Preizkusi so bili izvedeni na 100 datotekah, velika 1 GB in na 10 trdih diskih.

Izračunajmo, na kaj lahko načeloma računamo pri tej hitrosti:
Recimo, da beremo z 10 diskov s hitrostjo 280 MB/sek, tj. 3 milijone krat 100 bajtov. Toda kot se spomnimo, je podatkov, ki jih potrebujemo, 2600-krat manj od prebranih. Tako 3 milijone delimo s 2600 in dobimo 1100 zapisov na sekundo.

Depresivno, kajne? To je narava Naključni dostop dostop do podatkov na trdem disku - ne glede na velikost bloka. To je fizična omejitev naključnega dostopa in nobena baza podatkov pod takimi pogoji ne more iztisniti več.

Kako potem baze podatkov dosegajo veliko višje hitrosti? Za odgovor na to vprašanje si poglejmo, kaj se dogaja na naslednji sliki:

Kako povečati hitrost branja iz HBase do 3-krat in iz HDFS do 5-krat

Tukaj vidimo, da je prvih nekaj minut hitrost res približno tisoč zapisov na sekundo. Dalje pa zaradi dejstva, da se prebere veliko več kot je bilo zahtevano, podatki končajo v buff/cache operacijskega sistema (linux) in hitrost se poveča na bolj spodobnih 60 tisoč na sekundo

Tako se bomo v nadaljevanju ukvarjali s pospeševanjem dostopa le do podatkov, ki so v predpomnilniku OS ali v SSD/NVMe pomnilniških napravah primerljive hitrosti dostopa.

V našem primeru bomo izvedli teste na klopi 4 strežnikov, od katerih se vsak zaračuna na naslednji način:

CPU: Xeon E5-2680 v4 @ 2.40 GHz 64 niti.
Pomnilnik: 730 GB.
različica java: 1.8.0_111

In tukaj je ključna točka količina podatkov v tabelah, ki jih je treba prebrati. Dejstvo je, da če berete podatke iz tabele, ki je v celoti nameščena v predpomnilniku HBase, potem sploh ne bo prišlo do branja iz buff/cache operacijskega sistema. Ker HBase privzeto dodeli 40 % pomnilnika strukturi, imenovani BlockCache. V bistvu je to ConcurrentHashMap, kjer je ključ ime datoteke + odmik bloka, vrednost pa so dejanski podatki pri tem odmiku.

Tako pri branju le iz te strukture mi vidimo odlično hitrost, kot milijon zahtev na sekundo. Toda predstavljajmo si, da ne moremo dodeliti na stotine gigabajtov pomnilnika samo za potrebe baze podatkov, ker se na teh strežnikih izvaja veliko drugih uporabnih stvari.

Na primer, v našem primeru je prostornina BlockCache na enem RS približno 12 GB. Na enem vozlišču smo pristali dva RS, tj. 96 GB je dodeljenih za BlockCache na vseh vozliščih. In podatkov je velikokrat več, na primer, naj bodo to 4 tabele, vsaka po 130 regij, v katerih so datoteke velike 800 MB, stisnjene s FAST_DIFF, tj. skupaj 410 GB (gre za čiste podatke, tj. brez upoštevanja faktorja podvajanja).

Tako BlockCache predstavlja le približno 23 % skupne količine podatkov in to je veliko bližje dejanskim pogojem tako imenovanega BigData. In tukaj se začne zabava - kajti očitno je, da manj zadetkov v predpomnilniku slabša je zmogljivost. Konec koncev, če zamudite, boste morali opraviti veliko dela - tj. pojdite na klicne sistemske funkcije. Vendar se temu ne moremo izogniti, zato poglejmo povsem drugačen vidik – kaj se zgodi s podatki v predpomnilniku?

Poenostavimo situacijo in predpostavimo, da imamo predpomnilnik, ki ustreza samo 1 predmetu. Tukaj je primer, kaj se bo zgodilo, ko bomo poskušali delati s količino podatkov, ki je 3-krat večja od predpomnilnika, bomo morali:

1. Postavite blok 1 v predpomnilnik
2. Odstranite blok 1 iz predpomnilnika
3. Postavite blok 2 v predpomnilnik
4. Odstranite blok 2 iz predpomnilnika
5. Postavite blok 3 v predpomnilnik

5 dejanj končanih! Vendar te situacije ne moremo imenovati normalne; pravzaprav prisilimo HBase, da opravi kup popolnoma neuporabnega dela. Nenehno bere podatke iz predpomnilnika operacijskega sistema, jih postavlja v BlockCache in jih skoraj takoj vrže ven, ker je prispel nov del podatkov. Animacija na začetku prispevka prikazuje bistvo problema - Garbage Collector gre čez mejo, ozračje se segreva, mala Greta na daljni in vroči Švedski se razburja. In IT-jevci res ne maramo, ko so otroci žalostni, zato začnemo razmišljati, kaj lahko storimo glede tega.

Kaj pa, če v predpomnilnik ne postavite vseh blokov, ampak le določen odstotek, da se predpomnilnik ne prepolni? Začnimo tako, da preprosto dodamo le nekaj vrstic kode na začetek funkcije za vnos podatkov v BlockCache:

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

Tukaj gre za naslednje: offset je položaj bloka v datoteki in njegove zadnje števke so naključno in enakomerno porazdeljene od 00 do 99. Zato bomo preskočili samo tiste, ki spadajo v obseg, ki ga potrebujemo.

Na primer, nastavite cacheDataBlockPercent = 20 in poglejte, kaj se zgodi:

Kako povečati hitrost branja iz HBase do 3-krat in iz HDFS do 5-krat

Rezultat je očiten. V spodnjih grafih postane jasno, zakaj je prišlo do takšnega pospeška - prihranimo veliko virov GC, ne da bi opravili sizifovsko delo postavljanja podatkov v predpomnilnik, da bi jih takoj vrgli v odtok marsovskih psov:

Kako povečati hitrost branja iz HBase do 3-krat in iz HDFS do 5-krat

Hkrati se izkoriščenost procesorja poveča, vendar je veliko manjša od produktivnosti:

Kako povečati hitrost branja iz HBase do 3-krat in iz HDFS do 5-krat

Omeniti velja tudi, da so bloki, shranjeni v BlockCache, različni. Večino, približno 95 %, predstavljajo podatki sami. In ostalo so metapodatki, kot so Bloomovi filtri ali LEAF_INDEX in т.д.. Ti podatki niso dovolj, vendar so zelo uporabni, saj se HBase pred neposrednim dostopom do podatkov obrne na meta, da bi razumel, ali je tukaj potrebno iskati naprej in, če je tako, kje točno se nahaja blok, ki nas zanima.

Zato v kodi vidimo pogoj preverjanja buf.getBlockType().isData() in zahvaljujoč tej meta jo bomo v vsakem primeru pustili v predpomnilniku.

Zdaj pa povečajmo obremenitev in rahlo zategnite funkcijo naenkrat. V prvem preizkusu smo dosegli mejni odstotek = 20 in BlockCache je bil rahlo premalo izkoriščen. Zdaj ga nastavimo na 23 % in dodamo 100 niti vsakih 5 minut, da vidimo, na kateri točki pride do nasičenosti:

Kako povečati hitrost branja iz HBase do 3-krat in iz HDFS do 5-krat

Tukaj vidimo, da izvirna različica skoraj takoj doseže zgornjo mejo pri približno 100 tisoč zahtevah na sekundo. Medtem ko obliž omogoča pospešek do 300 tisoč. Ob tem je jasno, da nadaljnje pospeševanje ni več tako »zastonj«, povečuje se tudi izkoriščenost procesorja.

Vendar to ni zelo elegantna rešitev, saj ne vemo vnaprej, koliko odstotkov blokov je treba predpomniti, odvisno je od profila obremenitve. Zato je bil implementiran mehanizem za samodejno prilagajanje tega parametra glede na aktivnost bralnih operacij.

Za nadzor nad tem so bile dodane tri možnosti:

hbase.lru.cache.heavy.eviction.count.limit — nastavi, kolikokrat naj se izvede postopek izrivanja podatkov iz predpomnilnika, preden začnemo uporabljati optimizacijo (tj. preskakovanje blokov). Privzeto je enak MAX_INT = 2147483647 in dejansko pomeni, da funkcija nikoli ne bo začela delovati s to vrednostjo. Ker se proces izselitve začne vsakih 5 - 10 sekund (odvisno od obremenitve) in 2147483647 * 10 / 60 / 60 / 24 / 365 = 680 let. Vendar pa lahko ta parameter nastavimo na 0 in omogočimo, da funkcija deluje takoj po zagonu.

Vendar pa je v tem parametru tudi koristna obremenitev. Če je naša obremenitev tolikšna, da se kratkotrajna branja (recimo čez dan) in dolgotrajna branja (ponoči) nenehno prepletajo, potem lahko zagotovimo, da je funkcija vklopljena le, ko potekajo operacije dolgotrajnega branja.

Na primer, vemo, da kratkotrajni odčitki običajno trajajo približno 1 minuto. Ni vam treba začeti izmetavati blokov, predpomnilnik ne bo imel časa zastareti in potem lahko ta parameter nastavimo na, na primer, 10. To bo privedlo do dejstva, da bo optimizacija začela delovati šele, ko bo dolgo se je začel rok aktivnega branja, t.j. v 100 sekundah. Torej, če imamo kratkotrajno branje, bodo vsi bloki šli v predpomnilnik in bodo na voljo (razen tistih, ki jih bo izločil standardni algoritem). In ko izvajamo dolgoročna branja, je funkcija vklopljena in imeli bi veliko večjo zmogljivost.

hbase.lru.cache.heavy.eviction.mb.size.limit — nastavi, koliko megabajtov želimo shraniti v predpomnilnik (in seveda izseliti) v 10 sekundah. Funkcija bo poskušala doseči to vrednost in jo ohraniti. Bistvo je naslednje: če v predpomnilnik potisnemo gigabajte, bomo morali gigabajte izločiti, to pa je, kot smo videli zgoraj, zelo drago. Vendar ga ne poskušajte nastaviti premajhnega, saj bo to povzročilo prezgodnji izhod iz načina preskakovanja blokov. Za močne strežnike (približno 20-40 fizičnih jeder) je optimalno nastaviti približno 300-400 MB. Za srednji razred (~10 jeder) 200-300 MB. Za šibke sisteme (2-5 jeder) je lahko 50-100 MB normalno (ni testirano na teh).

Poglejmo, kako to deluje: recimo, da nastavimo hbase.lru.cache.heavy.eviction.mb.size.limit = 500, pride do neke vrste obremenitve (branje) in nato vsakih ~10 sekund izračunamo, koliko bajtov je bilo izseljen z uporabo formule:

Overhead = vsota prostih bajtov (MB) * 100 / omejitev (MB) - 100;

Če je bilo dejansko izločenih 2000 MB, so režijski stroški enaki:

2000 * 100 / 500 - 100 = 300 %

Algoritmi poskušajo ohraniti največ nekaj deset odstotkov, zato bo funkcija zmanjšala odstotek predpomnjenih blokov in s tem uvedla mehanizem samodejnega prilagajanja.

Če pa obremenitev pade, je recimo izločenih samo 200 MB in Overhead postane negativen (tako imenovano prekoračitev):

200 * 100 / 500 - 100 = -60 %

Nasprotno, funkcija bo povečala odstotek predpomnjenih blokov, dokler Overhead ne postane pozitiven.

Spodaj je primer, kako je to videti na resničnih podatkih. Ni potrebe, da poskušate doseči 0%, to je nemogoče. Zelo dobro je, če je približno 30 - 100%, kar pomaga preprečiti prezgodnji izhod iz načina optimizacije med kratkotrajnimi sunki.

hbase.lru.cache.heavy.eviction.overhead.coefficient — nastavi, kako hitro želimo dobiti rezultat. Če zagotovo vemo, da so naša branja večinoma dolga in ne želimo čakati, lahko povečamo to razmerje in hitreje dosežemo visoko zmogljivost.

Na primer, ta koeficient nastavimo = 0.01. To pomeni, da bodo režijski stroški (glejte zgoraj) pomnoženi s tem številom z dobljenim rezultatom in odstotek predpomnjenih blokov bo zmanjšan. Predpostavimo, da je Overhead = 300 % in koeficient = 0.01, potem se bo odstotek predpomnjenih blokov zmanjšal za 3 %.

Podobna logika "protitlaka" je implementirana tudi za negativne vrednosti režijskih stroškov (prekoračitev). Ker so vedno možna kratkotrajna nihanja v obsegu branja in izločitve, vam ta mehanizem omogoča, da se izognete prezgodnjemu izhodu iz načina optimizacije. Povratni pritisk ima obrnjeno logiko: močnejše kot je prekoračitev, več blokov je predpomnjenih.

Kako povečati hitrost branja iz HBase do 3-krat in iz HDFS do 5-krat

Izvedbena koda

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

Poglejmo zdaj vse to na resničnem primeru. Imamo naslednji testni skript:

  1. Začnimo s skeniranjem (25 niti, serija = 100)
  2. Po 5 minutah dodajte multi-gets (25 niti, serija = 100)
  3. Po 5 minutah izklopi multi-gets (zopet ostane samo skeniranje)

Izvedemo dva zagona, najprej hbase.lru.cache.heavy.eviction.count.limit = 10000 (kar dejansko onemogoči funkcijo) in nato nastavimo limit = 0 (jo omogočimo).

V spodnjih dnevnikih vidimo, kako se funkcija vklopi in ponastavi Overshooting na 14–71 %. Občasno se obremenitev zmanjša, kar vklopi Backpressure in HBase znova predpomni več blokov.

Log RegionServer
izločeno (MB): 0, razmerje 0.0, režijski stroški (%): -100, števec težkih izrinitev: 0, trenutno predpomnjenje DataBlock (%): 100
izločeno (MB): 0, razmerje 0.0, režijski stroški (%): -100, števec težkih izrinitev: 0, trenutno predpomnjenje DataBlock (%): 100
izločeno (MB): 2170, razmerje 1.09, režijski stroški (%): 985, števec velikih izrinitev: 1, trenutno predpomnjenje DataBlock (%): 91 < začetek
izločeno (MB): 3763, razmerje 1.08, režijski stroški (%): 1781, števec velikih izrinitev: 2, trenutno predpomnjenje DataBlock (%): 76
izločeno (MB): 3306, razmerje 1.07, režijski stroški (%): 1553, števec velikih izrinitev: 3, trenutno predpomnjenje DataBlock (%): 61
izločeno (MB): 2508, razmerje 1.06, režijski stroški (%): 1154, števec velikih izrinitev: 4, trenutno predpomnjenje DataBlock (%): 50
izločeno (MB): 1824, razmerje 1.04, režijski stroški (%): 812, števec velikih izrinitev: 5, trenutno predpomnjenje DataBlock (%): 42
izločeno (MB): 1482, razmerje 1.03, režijski stroški (%): 641, števec velikih izrinitev: 6, trenutno predpomnjenje DataBlock (%): 36
izločeno (MB): 1140, razmerje 1.01, režijski stroški (%): 470, števec velikih izrinitev: 7, trenutno predpomnjenje DataBlock (%): 32
izločeno (MB): 913, razmerje 1.0, režijski stroški (%): 356, števec velikih izrinitev: 8, trenutno predpomnjenje DataBlock (%): 29
izločeno (MB): 912, razmerje 0.89, režijski stroški (%): 356, števec velikih izrinitev: 9, trenutno predpomnjenje DataBlock (%): 26
izločeno (MB): 684, razmerje 0.76, režijski stroški (%): 242, števec velikih izrinitev: 10, trenutno predpomnjenje DataBlock (%): 24
izločeno (MB): 684, razmerje 0.61, režijski stroški (%): 242, števec velikih izrinitev: 11, trenutno predpomnjenje DataBlock (%): 22
izločeno (MB): 456, razmerje 0.51, režijski stroški (%): 128, števec velikih izrinitev: 12, trenutno predpomnjenje DataBlock (%): 21
izločeno (MB): 456, razmerje 0.42, režijski stroški (%): 128, števec velikih izrinitev: 13, trenutno predpomnjenje DataBlock (%): 20
izločeno (MB): 456, razmerje 0.33, režijski stroški (%): 128, števec velikih izrinitev: 14, trenutno predpomnjenje DataBlock (%): 19
izločeno (MB): 342, razmerje 0.33, režijski stroški (%): 71, števec velikih izrinitev: 15, trenutno predpomnjenje DataBlock (%): 19
izločeno (MB): 342, razmerje 0.32, režijski stroški (%): 71, števec velikih izrinitev: 16, trenutno predpomnjenje DataBlock (%): 19
izločeno (MB): 342, razmerje 0.31, režijski stroški (%): 71, števec velikih izrinitev: 17, trenutno predpomnjenje DataBlock (%): 19
izločeno (MB): 228, razmerje 0.3, režijski stroški (%): 14, števec velikih izrinitev: 18, trenutno predpomnjenje DataBlock (%): 19
izločeno (MB): 228, razmerje 0.29, režijski stroški (%): 14, števec velikih izrinitev: 19, trenutno predpomnjenje DataBlock (%): 19
izločeno (MB): 228, razmerje 0.27, režijski stroški (%): 14, števec velikih izrinitev: 20, trenutno predpomnjenje DataBlock (%): 19
izločeno (MB): 228, razmerje 0.25, režijski stroški (%): 14, števec velikih izrinitev: 21, trenutno predpomnjenje DataBlock (%): 19
izločeno (MB): 228, razmerje 0.24, režijski stroški (%): 14, števec velikih izrinitev: 22, trenutno predpomnjenje DataBlock (%): 19
izločeno (MB): 228, razmerje 0.22, režijski stroški (%): 14, števec velikih izrinitev: 23, trenutno predpomnjenje DataBlock (%): 19
izločeno (MB): 228, razmerje 0.21, režijski stroški (%): 14, števec velikih izrinitev: 24, trenutno predpomnjenje DataBlock (%): 19
izločeno (MB): 228, razmerje 0.2, režijski stroški (%): 14, števec velikih izrinitev: 25, trenutno predpomnjenje DataBlock (%): 19
izločeno (MB): 228, razmerje 0.17, režijski stroški (%): 14, števec velikih izrinitev: 26, trenutno predpomnjenje DataBlock (%): 19
izločeno (MB): 456, razmerje 0.17, režijski stroški (%): 128, števec težkih izselitev: 27, trenutni predpomnjeni podatkovni blok (%): 18 < dodani dobi (toda tabela ista)
izločeno (MB): 456, razmerje 0.15, režijski stroški (%): 128, števec velikih izrinitev: 28, trenutno predpomnjenje DataBlock (%): 17
izločeno (MB): 342, razmerje 0.13, režijski stroški (%): 71, števec velikih izrinitev: 29, trenutno predpomnjenje DataBlock (%): 17
izločeno (MB): 342, razmerje 0.11, režijski stroški (%): 71, števec velikih izrinitev: 30, trenutno predpomnjenje DataBlock (%): 17
izločeno (MB): 342, razmerje 0.09, režijski stroški (%): 71, števec velikih izrinitev: 31, trenutno predpomnjenje DataBlock (%): 17
izločeno (MB): 228, razmerje 0.08, režijski stroški (%): 14, števec velikih izrinitev: 32, trenutno predpomnjenje DataBlock (%): 17
izločeno (MB): 228, razmerje 0.07, režijski stroški (%): 14, števec velikih izrinitev: 33, trenutno predpomnjenje DataBlock (%): 17
izločeno (MB): 228, razmerje 0.06, režijski stroški (%): 14, števec velikih izrinitev: 34, trenutno predpomnjenje DataBlock (%): 17
izločeno (MB): 228, razmerje 0.05, režijski stroški (%): 14, števec velikih izrinitev: 35, trenutno predpomnjenje DataBlock (%): 17
izločeno (MB): 228, razmerje 0.05, režijski stroški (%): 14, števec velikih izrinitev: 36, trenutno predpomnjenje DataBlock (%): 17
izločeno (MB): 228, razmerje 0.04, režijski stroški (%): 14, števec velikih izrinitev: 37, trenutno predpomnjenje DataBlock (%): 17
izločeno (MB): 109, razmerje 0.04, režijski stroški (%): -46, števec težkih izrinitev: 37, trenutno predpomnjenje DataBlock (%): 22 < povratni pritisk
izločeno (MB): 798, razmerje 0.24, režijski stroški (%): 299, števec velikih izrinitev: 38, trenutno predpomnjenje DataBlock (%): 20
izločeno (MB): 798, razmerje 0.29, režijski stroški (%): 299, števec velikih izrinitev: 39, trenutno predpomnjenje DataBlock (%): 18
izločeno (MB): 570, razmerje 0.27, režijski stroški (%): 185, števec velikih izrinitev: 40, trenutno predpomnjenje DataBlock (%): 17
izločeno (MB): 456, razmerje 0.22, režijski stroški (%): 128, števec velikih izrinitev: 41, trenutno predpomnjenje DataBlock (%): 16
izločeno (MB): 342, razmerje 0.16, režijski stroški (%): 71, števec velikih izrinitev: 42, trenutno predpomnjenje DataBlock (%): 16
izločeno (MB): 342, razmerje 0.11, režijski stroški (%): 71, števec velikih izrinitev: 43, trenutno predpomnjenje DataBlock (%): 16
izločeno (MB): 228, razmerje 0.09, režijski stroški (%): 14, števec velikih izrinitev: 44, trenutno predpomnjenje DataBlock (%): 16
izločeno (MB): 228, razmerje 0.07, režijski stroški (%): 14, števec velikih izrinitev: 45, trenutno predpomnjenje DataBlock (%): 16
izločeno (MB): 228, razmerje 0.05, režijski stroški (%): 14, števec velikih izrinitev: 46, trenutno predpomnjenje DataBlock (%): 16
izločeno (MB): 222, razmerje 0.04, režijski stroški (%): 11, števec velikih izrinitev: 47, trenutno predpomnjenje DataBlock (%): 16
izločeno (MB): 104, razmerje 0.03, režijski stroški (%): -48, števec težkih izrinitev: 47, trenutni predpomnilnik DataBlock (%): 21 <prekinitev dobi
izločeno (MB): 684, razmerje 0.2, režijski stroški (%): 242, števec velikih izrinitev: 48, trenutno predpomnjenje DataBlock (%): 19
izločeno (MB): 570, razmerje 0.23, režijski stroški (%): 185, števec velikih izrinitev: 49, trenutno predpomnjenje DataBlock (%): 18
izločeno (MB): 342, razmerje 0.22, režijski stroški (%): 71, števec velikih izrinitev: 50, trenutno predpomnjenje DataBlock (%): 18
izločeno (MB): 228, razmerje 0.21, režijski stroški (%): 14, števec velikih izrinitev: 51, trenutno predpomnjenje DataBlock (%): 18
izločeno (MB): 228, razmerje 0.2, režijski stroški (%): 14, števec velikih izrinitev: 52, trenutno predpomnjenje DataBlock (%): 18
izločeno (MB): 228, razmerje 0.18, režijski stroški (%): 14, števec velikih izrinitev: 53, trenutno predpomnjenje DataBlock (%): 18
izločeno (MB): 228, razmerje 0.16, režijski stroški (%): 14, števec velikih izrinitev: 54, trenutno predpomnjenje DataBlock (%): 18
izločeno (MB): 228, razmerje 0.14, režijski stroški (%): 14, števec velikih izrinitev: 55, trenutno predpomnjenje DataBlock (%): 18
izločeno (MB): 112, razmerje 0.14, režijski stroški (%): -44, števec težkih izrinitev: 55, trenutno predpomnjenje DataBlock (%): 23 < povratni pritisk
izločeno (MB): 456, razmerje 0.26, režijski stroški (%): 128, števec velikih izrinitev: 56, trenutno predpomnjenje DataBlock (%): 22
izločeno (MB): 342, razmerje 0.31, režijski stroški (%): 71, števec velikih izrinitev: 57, trenutno predpomnjenje DataBlock (%): 22
izločeno (MB): 342, razmerje 0.33, režijski stroški (%): 71, števec velikih izrinitev: 58, trenutno predpomnjenje DataBlock (%): 22
izločeno (MB): 342, razmerje 0.33, režijski stroški (%): 71, števec velikih izrinitev: 59, trenutno predpomnjenje DataBlock (%): 22
izločeno (MB): 342, razmerje 0.33, režijski stroški (%): 71, števec velikih izrinitev: 60, trenutno predpomnjenje DataBlock (%): 22
izločeno (MB): 342, razmerje 0.33, režijski stroški (%): 71, števec velikih izrinitev: 61, trenutno predpomnjenje DataBlock (%): 22
izločeno (MB): 342, razmerje 0.33, režijski stroški (%): 71, števec velikih izrinitev: 62, trenutno predpomnjenje DataBlock (%): 22
izločeno (MB): 342, razmerje 0.33, režijski stroški (%): 71, števec velikih izrinitev: 63, trenutno predpomnjenje DataBlock (%): 22
izločeno (MB): 342, razmerje 0.32, režijski stroški (%): 71, števec velikih izrinitev: 64, trenutno predpomnjenje DataBlock (%): 22
izločeno (MB): 342, razmerje 0.33, režijski stroški (%): 71, števec velikih izrinitev: 65, trenutno predpomnjenje DataBlock (%): 22
izločeno (MB): 342, razmerje 0.33, režijski stroški (%): 71, števec velikih izrinitev: 66, trenutno predpomnjenje DataBlock (%): 22
izločeno (MB): 342, razmerje 0.32, režijski stroški (%): 71, števec velikih izrinitev: 67, trenutno predpomnjenje DataBlock (%): 22
izločeno (MB): 342, razmerje 0.33, režijski stroški (%): 71, števec velikih izrinitev: 68, trenutno predpomnjenje DataBlock (%): 22
izločeno (MB): 342, razmerje 0.32, režijski stroški (%): 71, števec velikih izrinitev: 69, trenutno predpomnjenje DataBlock (%): 22
izločeno (MB): 342, razmerje 0.32, režijski stroški (%): 71, števec velikih izrinitev: 70, trenutno predpomnjenje DataBlock (%): 22
izločeno (MB): 342, razmerje 0.33, režijski stroški (%): 71, števec velikih izrinitev: 71, trenutno predpomnjenje DataBlock (%): 22
izločeno (MB): 342, razmerje 0.33, režijski stroški (%): 71, števec velikih izrinitev: 72, trenutno predpomnjenje DataBlock (%): 22
izločeno (MB): 342, razmerje 0.33, režijski stroški (%): 71, števec velikih izrinitev: 73, trenutno predpomnjenje DataBlock (%): 22
izločeno (MB): 342, razmerje 0.33, režijski stroški (%): 71, števec velikih izrinitev: 74, trenutno predpomnjenje DataBlock (%): 22
izločeno (MB): 342, razmerje 0.33, režijski stroški (%): 71, števec velikih izrinitev: 75, trenutno predpomnjenje DataBlock (%): 22
izločeno (MB): 342, razmerje 0.33, režijski stroški (%): 71, števec velikih izrinitev: 76, trenutno predpomnjenje DataBlock (%): 22
izločeno (MB): 21, razmerje 0.33, režijski stroški (%): -90, števec težkih izrinitev: 76, trenutno predpomnjenje DataBlock (%): 32
izločeno (MB): 0, razmerje 0.0, režijski stroški (%): -100, števec težkih izrinitev: 0, trenutno predpomnjenje DataBlock (%): 100
izločeno (MB): 0, razmerje 0.0, režijski stroški (%): -100, števec težkih izrinitev: 0, trenutno predpomnjenje DataBlock (%): 100

Skeniranja so bila potrebna za prikaz istega procesa v obliki grafa razmerja med dvema odsekoma predpomnilnika - enojnim (kjer so bloki, ki še nikoli niso bili zahtevani) in več (tu so shranjeni vsaj enkrat "zahtevani" podatki):

Kako povečati hitrost branja iz HBase do 3-krat in iz HDFS do 5-krat

In za konec še kako izgleda delovanje parametrov v obliki grafa. Za primerjavo, predpomnilnik je bil na začetku popolnoma izklopljen, nato pa je bil zagnan HBase s predpomnjenjem in zakasnitvijo začetka optimizacijskega dela za 5 minut (30 ciklov izselitve).

Celotno kodo najdete v Pull Request HBASE 23887 na githubu.

Vendar 300 tisoč branj na sekundo ni vse, kar je v teh pogojih mogoče doseči na tej strojni opremi. Dejstvo je, da ko potrebujete dostop do podatkov prek HDFS, se uporablja mehanizem ShortCircuitCache (v nadaljevanju SSC), ki vam omogoča neposreden dostop do podatkov in se izognete omrežnim interakcijam.

Profiliranje je pokazalo, da čeprav ta mehanizem daje velik dobiček, na neki točki postane tudi ozko grlo, saj se skoraj vse težke operacije zgodijo znotraj ključavnice, kar večino časa vodi do blokade.

Kako povečati hitrost branja iz HBase do 3-krat in iz HDFS do 5-krat

Ko smo to ugotovili, smo ugotovili, da se težavi lahko izognemo z ustvarjanjem niza neodvisnih SSC:

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

In nato delajte z njimi, pri čemer izključite presečišča tudi pri zadnji številki odmika:

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

Zdaj lahko začnete s testiranjem. Da bi to naredili, bomo brali datoteke iz HDFS s preprosto večnitno aplikacijo. Nastavite parametre:

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

In samo preberite datoteke:

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

Ta koda se izvaja v ločenih nitih in povečali bomo število sočasno prebranih datotek (z 10 na 200 - vodoravna os) in število predpomnilnikov (z 1 na 10 - grafika). Navpična os prikazuje pospešek, ki je posledica povečanja SSC glede na primer, ko obstaja samo en predpomnilnik.

Kako povečati hitrost branja iz HBase do 3-krat in iz HDFS do 5-krat

Kako brati graf: Čas izvedbe za 100 tisoč branj v 64 KB blokih z enim predpomnilnikom zahteva 78 sekund. Medtem ko s 5 predpomnilniki traja 16 sekund. Tisti. obstaja ~5-kratni pospešek. Kot je razvidno iz grafa, učinek ni zelo opazen pri majhnem številu vzporednih branj, opaznejšo vlogo začne igrati, ko je branj niti več kot 50. Opazno je tudi, da povečanje števila SSC s 6 in višje daje bistveno manjše povečanje zmogljivosti.

Opomba 1: ker so rezultati testa precej nestanovitni (glejte spodaj), so bili izvedeni 3 postopki in dobljene vrednosti so bile povprečne.

Opomba 2: Povečanje zmogljivosti zaradi konfiguracije naključnega dostopa je enako, čeprav je sam dostop nekoliko počasnejši.

Vendar je treba pojasniti, da za razliko od primera s HBase to pospeševanje ni vedno brezplačno. Tukaj »odklenemo« sposobnost CPE-ja, da opravi več dela, namesto da visi na ključavnicah.

Kako povečati hitrost branja iz HBase do 3-krat in iz HDFS do 5-krat

Tukaj lahko opazite, da na splošno povečanje števila predpomnilnikov povzroči približno sorazmerno povečanje izkoriščenosti procesorja. Je pa dobitnih kombinacij nekoliko več.

Na primer, poglejmo podrobneje nastavitev SSC = 3. Povečanje zmogljivosti na območju je približno 3.3-krat. Spodaj so rezultati vseh treh ločenih voženj.

Kako povečati hitrost branja iz HBase do 3-krat in iz HDFS do 5-krat

Medtem ko se poraba procesorja poveča za približno 2.8-krat. Razlika ni zelo velika, vendar je mala Greta že srečna in bo morda imela čas za obiskovanje šole in pouk.

Tako bo imelo to pozitiven učinek za katero koli orodje, ki uporablja množični dostop do HDFS (na primer Spark itd.), pod pogojem, da je koda aplikacije lahka (tj. vtič je na strani odjemalca HDFS) in je na voljo prosta moč procesorja . Če želite preveriti, preizkusimo, kakšen učinek bo imela kombinirana uporaba optimizacije BlockCache in nastavitve SSC za branje iz HBase.

Kako povečati hitrost branja iz HBase do 3-krat in iz HDFS do 5-krat

Vidi se, da v takšnih pogojih učinek ni tako velik kot pri izpopolnjenih testih (branje brez kakršne koli obdelave), vendar je tukaj povsem mogoče iztisniti dodatnih 80K. Obe optimizaciji skupaj zagotavljata do 4-kratno pospešitev.

Za to optimizacijo je bil narejen tudi PR [HDFS-15202], ki je bil združen in ta funkcionalnost bo na voljo v prihodnjih izdajah.

In končno, zanimivo je bilo primerjati uspešnost branja podobne baze podatkov s širokimi stolpci, Cassandra in HBase.

Da bi to naredili, smo zagnali primerke standardnega pripomočka za testiranje obremenitve YCSB z dveh gostiteljev (skupaj 800 niti). Na strani strežnika - 4 primerki RegionServerja in Cassandre na 4 gostiteljih (ne tistih, kjer se izvajajo odjemalci, da se izognemo njihovemu vplivu). Odčitki so izhajali iz tabel velikosti:

HBase – 300 GB na HDFS (100 GB čistih podatkov)

Cassandra - 250 GB (replikacijski faktor = 3)

Tisti. glasnost je bila približno enaka (v HBase malo več).

Parametri HBase:

dfs.client.short.circuit.num = 5 (Optimizacija odjemalca HDFS)

hbase.lru.cache.heavy.eviction.count.limit = 30 - to pomeni, da bo popravek začel delovati po 30 izselitvah (~5 minut)

hbase.lru.cache.heavy.eviction.mb.size.limit = 300 — ciljni obseg predpomnilnika in izselitve

Dnevniki YCSB so bili razčlenjeni in zbrani v Excelove grafe:

Kako povečati hitrost branja iz HBase do 3-krat in iz HDFS do 5-krat

Kot lahko vidite, te optimizacije omogočajo primerjavo delovanja teh baz podatkov pod temi pogoji in doseganje 450 tisoč branj na sekundo.

Upamo, da bodo te informacije komu koristile med razburljivim bojem za produktivnost.

Vir: www.habr.com

Dodaj komentar