Kako povećati brzinu čitanja sa HBase-a do 3 puta i sa HDFS-a do 5 puta

Visoke performanse jedan je od ključnih zahtjeva za rad s velikim podacima. U odjelu za učitavanje podataka u Sberbanku, pumpamo gotovo sve transakcije u naš Data Cloud baziran na Hadoop-u i stoga se bavimo zaista velikim tokovima informacija. Naravno, uvijek tražimo načine za poboljšanje performansi, a sada želimo da vam ispričamo kako smo uspjeli zakrpiti RegionServer HBase i HDFS klijent, zahvaljujući čemu smo uspjeli značajno povećati brzinu operacija čitanja.
Kako povećati brzinu čitanja sa HBase-a do 3 puta i sa HDFS-a do 5 puta

Međutim, prije nego što pređemo na suštinu poboljšanja, vrijedi razgovarati o ograničenjima koja se u principu ne mogu zaobići ako sjedite na HDD-u.

Zašto HDD i brzo Random Access čitanje nisu kompatibilni
Kao što znate, HBase i mnoge druge baze podataka pohranjuju podatke u blokovima veličine nekoliko desetina kilobajta. Podrazumevano je oko 64 KB. Sada zamislimo da trebamo dobiti samo 100 bajtova i tražimo od HBase-a da nam da te podatke koristeći određeni ključ. Pošto je veličina bloka u HFiles-u 64 KB, zahtjev će biti 640 puta veći (samo minut!) nego što je potrebno.

Zatim, pošto će zahtjev proći kroz HDFS i njegov mehanizam za keširanje metapodataka ShortCircuitCache (što omogućava direktan pristup datotekama), to dovodi do čitanja već 1 MB sa diska. Međutim, ovo se može podesiti pomoću parametra dfs.client.read.shortcircuit.buffer.size i u mnogim slučajevima ima smisla smanjiti ovu vrijednost, na primjer na 126 KB.

Recimo da radimo ovo, ali pored toga, kada počnemo čitati podatke preko java API-ja, kao što su funkcije poput FileChannel.read i zatražimo od operativnog sistema da pročita navedenu količinu podataka, on čita "za svaki slučaj" 2 puta više , tj. 256 KB u našem slučaju. To je zato što java nema jednostavan način za postavljanje FADV_RANDOM zastavice da spriječi ovo ponašanje.

Kao rezultat, da bismo dobili naših 100 bajtova, 2600 puta više se čita ispod haube. Čini se da je rješenje očigledno, smanjimo veličinu bloka na kilobajt, postavimo spomenutu zastavu i dobijemo veliko ubrzanje prosvjetljenja. Ali problem je u tome što smanjenjem veličine bloka za 2 puta, smanjujemo i broj čitanih bajtova po jedinici vremena za 2 puta.

Neki dobitak od postavljanja zastavice FADV_RANDOM se može dobiti, ali samo uz visoku višenitost i sa veličinom bloka od 128 KB, ali to je maksimalno par desetina posto:

Kako povećati brzinu čitanja sa HBase-a do 3 puta i sa HDFS-a do 5 puta

Testovi su obavljeni na 100 datoteka, svaka veličine 1 GB i smještena na 10 tvrdih diskova.

Izračunajmo na šta u principu možemo računati pri ovoj brzini:
Recimo da čitamo sa 10 diskova brzinom od 280 MB/sec, tj. 3 miliona puta 100 bajtova. Ali kao što se sjećamo, podaci koji su nam potrebni su 2600 puta manji od onoga što se čita. Dakle, podijelimo 3 miliona sa 2600 i dobijemo 1100 zapisa u sekundi.

Depresivno, zar ne? To je priroda Slučajni pristup pristup podacima na HDD-u - bez obzira na veličinu bloka. Ovo je fizičko ograničenje slučajnog pristupa i nijedna baza podataka ne može istisnuti više pod takvim uslovima.

Kako onda baze podataka postižu mnogo veće brzine? Da bismo odgovorili na ovo pitanje, pogledajmo šta se dešava na sljedećoj slici:

Kako povećati brzinu čitanja sa HBase-a do 3 puta i sa HDFS-a do 5 puta

Ovdje vidimo da je za prvih nekoliko minuta brzina zaista oko hiljadu zapisa u sekundi. Međutim, dalje, zbog činjenice da se čita mnogo više nego što je traženo, podaci završavaju u baff/cache operativnom sistemu (linux) i brzina se povećava na pristojnijih 60 hiljada u sekundi

Dakle, dalje ćemo se baviti ubrzavanjem pristupa samo podacima koji se nalaze u kešu OS-a ili koji se nalaze u SSD/NVMe uređajima za pohranu uporedive brzine pristupa.

U našem slučaju ćemo provesti testove na klupi od 4 servera, od kojih se svaki naplaćuje na sljedeći način:

CPU: Xeon E5-2680 v4 @ 2.40GHz 64 niti.
Memorija: 730 GB.
java verzija: 1.8.0_111

I ovdje je ključna stvar količina podataka u tabelama koje treba pročitati. Činjenica je da ako čitate podatke iz tabele koja je u potpunosti smeštena u HBase keš memoriju, onda neće doći ni do čitanja iz buff/cache operativnog sistema. Zato što HBase po defaultu dodjeljuje 40% memorije strukturi koja se zove BlockCache. U suštini ovo je ConcurrentHashMap, gdje je ključ ime datoteke + pomak bloka, a vrijednost su stvarni podaci na ovom pomaku.

Dakle, kada čitamo samo iz ove strukture, mi vidimo odličnu brzinu, kao milion zahtjeva u sekundi. Ali zamislimo da ne možemo izdvojiti stotine gigabajta memorije samo za potrebe baze podataka, jer na ovim serverima radi puno drugih korisnih stvari.

Na primjer, u našem slučaju, volumen BlockCache-a na jednom RS je oko 12 GB. Spustili smo dva RS na jedan čvor, tj. 96 GB je dodijeljeno za BlockCache na svim čvorovima. A podataka ima višestruko više, na primjer, neka to budu 4 tabele, po 130 regija, u kojima su fajlovi veličine 800 MB, komprimirani sa FAST_DIFF, tj. ukupno 410 GB (ovo su čisti podaci, tj. bez uzimanja u obzir faktora replikacije).

Dakle, BlockCache čini samo oko 23% ukupne količine podataka i to je mnogo bliže stvarnim uslovima onoga što se zove BigData. I tu počinje zabava - jer očigledno, što je manje pogodaka u keš memoriji, to su performanse lošije. Na kraju krajeva, ako promašite, moraćete mnogo da radite – tj. spustite se na pozivanje sistemskih funkcija. Međutim, to se ne može izbjeći, pa pogledajmo potpuno drugačiji aspekt - šta se događa s podacima unutar keša?

Hajde da pojednostavimo situaciju i pretpostavimo da imamo keš memoriju koja stane samo za 1 objekat. Evo primjera šta će se dogoditi kada pokušamo raditi s volumenom podataka 3 puta većim od keša, morat ćemo:

1. Stavite blok 1 u keš memoriju
2. Uklonite blok 1 iz keša
3. Stavite blok 2 u keš memoriju
4. Uklonite blok 2 iz keša
5. Stavite blok 3 u keš memoriju

5 akcija završeno! Međutim, ova situacija se ne može nazvati normalnom; zapravo, tjeramo HBase da radi gomilu potpuno beskorisnog posla. Stalno čita podatke iz keša OS-a, stavlja ih u BlockCache, da bi ih skoro odmah izbacio jer je stigao novi dio podataka. Animacija na početku posta pokazuje suštinu problema - Garbage Collector se zahuktava, atmosfera se zahuktava, mala Greta u dalekoj i vrućoj Švedskoj se uznemiruje. A mi IT ljudi zaista ne volimo kada su djeca tužna, pa počnemo razmišljati šta možemo učiniti po tom pitanju.

Šta ako ne stavite sve blokove u keš memoriju, već samo određeni postotak njih, kako se keš ne bi prelio? Počnimo jednostavnim dodavanjem samo nekoliko linija koda na početak funkcije za stavljanje podataka u BlockCache:

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

Poenta je sljedeća: pomak je pozicija bloka u datoteci i njegove posljednje cifre su nasumično i ravnomjerno raspoređene od 00 do 99. Stoga ćemo preskočiti samo one koje spadaju u raspon koji nam je potreban.

Na primjer, postavite cacheDataBlockPercent = 20 i pogledajte šta se dešava:

Kako povećati brzinu čitanja sa HBase-a do 3 puta i sa HDFS-a do 5 puta

Rezultat je očigledan. Na grafikonima u nastavku postaje jasno zašto je došlo do takvog ubrzanja - štedimo mnogo GC resursa bez obavljanja sizifovskog posla stavljanja podataka u keš memoriju samo da bismo ih odmah bacili u odvod marsovskih pasa:

Kako povećati brzinu čitanja sa HBase-a do 3 puta i sa HDFS-a do 5 puta

U isto vrijeme, korištenje CPU-a se povećava, ali je mnogo manje od produktivnosti:

Kako povećati brzinu čitanja sa HBase-a do 3 puta i sa HDFS-a do 5 puta

Također je vrijedno napomenuti da su blokovi pohranjeni u BlockCache-u različiti. Većina, oko 95%, su sami podaci. A ostalo su metapodaci, kao što su Bloom filteri ili LEAF_INDEX i itd.. Ovi podaci nisu dovoljni, ali su vrlo korisni, jer prije direktnog pristupa podacima, HBase se okreće meta kako bi shvatio da li je potrebno dalje tražiti ovdje i, ako jeste, gdje se tačno nalazi blok od interesa.

Stoga, u kodu vidimo uslov provjere buf.getBlockType().isData() a zahvaljujući ovoj meta, u svakom slučaju ćemo ga ostaviti u kešu.

Sada povećajmo opterećenje i malo zategnimo funkciju u jednom potezu. U prvom testu napravili smo granični procenat = 20 i BlockCache je bio malo nedovoljno iskorišten. Sada ga postavimo na 23% i dodajmo 100 niti svakih 5 minuta da vidimo u kojoj točki dolazi do zasićenja:

Kako povećati brzinu čitanja sa HBase-a do 3 puta i sa HDFS-a do 5 puta

Ovde vidimo da originalna verzija skoro odmah dostiže plafon sa oko 100 hiljada zahteva u sekundi. Dok patch daje ubrzanje do 300 hiljada. Istovremeno, jasno je da dalje ubrzanje više nije tako „besplatno“, već se povećava i iskorištenost CPU-a.

Međutim, ovo nije baš elegantno rješenje, jer ne znamo unaprijed koji postotak blokova treba keširati, to ovisi o profilu učitavanja. Stoga je implementiran mehanizam za automatsko podešavanje ovog parametra u zavisnosti od aktivnosti operacija čitanja.

Dodane su tri opcije za kontrolu ovoga:

hbase.lru.cache.heavy.eviction.count.limit — postavlja koliko puta treba pokrenuti proces izbacivanja podataka iz keša prije nego što počnemo koristiti optimizaciju (tj. preskakanje blokova). Podrazumevano je jednako MAX_INT = 2147483647 i zapravo znači da funkcija nikada neće početi raditi s ovom vrijednošću. Jer proces deložacije počinje svakih 5 - 10 sekundi (zavisi od opterećenja) i 2147483647 * 10 / 60 / 60 / 24 / 365 = 680 godina. Međutim, možemo postaviti ovaj parametar na 0 i učiniti da funkcija radi odmah nakon pokretanja.

Međutim, u ovom parametru postoji i opterećenje. Ako je naše opterećenje takvo da se kratkoročna čitanja (recimo danju) i dugotrajna čitanja (noću) stalno izmjenjuju, onda možemo osigurati da je funkcija uključena samo kada su operacije dugog čitanja u toku.

Na primjer, znamo da kratkotrajna očitavanja obično traju oko 1 minut. Nema potrebe da počinjemo izbacivati ​​blokove, keš neće imati vremena da zastari i tada možemo postaviti ovaj parametar na, na primjer, 10. To će dovesti do činjenice da će optimizacija početi raditi tek kada dugo- počeo je termin aktivno čitanje, tj. za 100 sekundi. Dakle, ako imamo kratkotrajno čitanje, tada će svi blokovi otići u keš memoriju i bit će dostupni (osim onih koji će biti izbačeni standardnim algoritmom). A kada radimo dugotrajna čitanja, funkcija je uključena i imali bismo mnogo veće performanse.

hbase.lru.cache.heavy.eviction.mb.size.limit — postavlja koliko megabajta želimo staviti u keš memoriju (i, naravno, izbaciti) u 10 sekundi. Funkcija će pokušati postići ovu vrijednost i održati je. Poenta je sledeća: ako gurnemo gigabajte u keš memoriju, onda ćemo morati da izbacimo gigabajte, a to je, kao što smo videli gore, veoma skupo. Međutim, ne biste trebali pokušavati da ga postavite premalo, jer će to uzrokovati prerano izlazak iz moda za preskakanje bloka. Za moćne servere (oko 20-40 fizičkih jezgara) optimalno je postaviti oko 300-400 MB. Za srednju klasu (~10 jezgara) 200-300 MB. Za slabe sisteme (2-5 jezgri) 50-100 MB može biti normalno (nije testirano na ovim).

Pogledajmo kako ovo funkcionira: recimo da postavimo hbase.lru.cache.heavy.eviction.mb.size.limit = 500, postoji neka vrsta opterećenja (čitanja) i onda svakih ~10 sekundi izračunamo koliko je bajtova bilo iseljeni po formuli:

Overhead = zbir oslobođenih bajtova (MB) * 100 / ograničenje (MB) - 100;

Ako je zapravo 2000 MB izbačeno, onda su režijski troškovi jednaki:

2000 * 100 / 500 - 100 = 300%

Algoritmi pokušavaju da održe ne više od nekoliko desetina procenata, tako da će funkcija smanjiti procenat keširanih blokova, implementirajući mehanizam za automatsko podešavanje.

Međutim, ako opterećenje padne, recimo samo 200 MB se izbaci i Overhead postane negativan (tzv. prekoračenje):

200 * 100 / 500 - 100 = -60%

Naprotiv, funkcija će povećati procenat keširanih blokova sve dok Overhead ne postane pozitivan.

Ispod je primjer kako to izgleda na stvarnim podacima. Nema potrebe da pokušavate da dostignete 0%, to je nemoguće. Vrlo je dobro kada je oko 30 - 100%, ovo pomaže da se izbjegne prijevremeni izlazak iz režima optimizacije tokom kratkotrajnih prenapona.

hbase.lru.cache.heavy.eviction.overhead.coefficient — određuje koliko brzo želimo da dobijemo rezultat. Ako sa sigurnošću znamo da su naša čitanja uglavnom duga i ne želimo da čekamo, možemo povećati ovaj omjer i brže postići visoke performanse.

Na primjer, postavljamo ovaj koeficijent = 0.01. To znači da će Overhead (vidi gore) biti pomnožen ovim brojem sa rezultujućim rezultatom i postotak keširanih blokova će biti smanjen. Pretpostavimo da je Overhead = 300% i koeficijent = 0.01, tada će postotak keširanih blokova biti smanjen za 3%.

Slična logika “povratnog pritiska” je također implementirana za negativne vrijednosti nadjačavanja (prekoračivanja). Budući da su kratkoročne fluktuacije u obimu čitanja i izbacivanja uvijek moguće, ovaj mehanizam vam omogućava da izbjegnete prerano izlazak iz režima optimizacije. Povratni pritisak ima obrnutu logiku: što je jače prekoračenje, to se više blokova kešira.

Kako povećati brzinu čitanja sa HBase-a do 3 puta i sa HDFS-a do 5 puta

Kod za implementaciju

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

Pogledajmo sada sve ovo koristeći pravi primjer. Imamo sljedeću test skriptu:

  1. Počnimo sa skeniranjem (25 niti, serija = 100)
  2. Nakon 5 minuta dodajte multi-getove (25 niti, serija = 100)
  3. Nakon 5 minuta isključite multi-gets (ponovo ostaje samo skeniranje)

Radimo dva pokretanja, prvo hbase.lru.cache.heavy.eviction.count.limit = 10000 (što zapravo onemogućava funkciju), a zatim postavlja limit = 0 (omogućava).

U zapisnicima ispod vidimo kako je funkcija uključena i resetuje Overshooting na 14-71%. S vremena na vrijeme opterećenje se smanjuje, što uključuje Backpressure i HBase ponovo kešira više blokova.

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

Skeniranja su bila potrebna da bi se isti proces prikazao u obliku grafikona odnosa između dva odsječka keša - jednostrukog (gdje su blokovi koji nikada prije nisu bili traženi) i višestruki (ovdje se pohranjuju podaci koji su "zatraženi" barem jednom):

Kako povećati brzinu čitanja sa HBase-a do 3 puta i sa HDFS-a do 5 puta

I na kraju, kako izgleda rad parametara u obliku grafa. Poređenja radi, keš je na početku potpuno isključen, zatim je pokrenut HBase sa keširanjem i odlaganjem početka rada optimizacije za 5 minuta (30 ciklusa izbacivanja).

Cijeli kod se može naći u Pull Request-u HBASE 23887 na github-u.

Međutim, 300 hiljada čitanja u sekundi nije sve što se može postići na ovom hardveru u ovim uslovima. Činjenica je da kada trebate pristupiti podacima putem HDFS-a, koristi se mehanizam ShortCircuitCache (u daljem tekstu SSC), koji vam omogućava direktan pristup podacima, izbjegavajući mrežne interakcije.

Profiliranje je pokazalo da iako ovaj mehanizam daje veliki dobitak, on također u nekom trenutku postaje usko grlo, jer se gotovo sve teške operacije dešavaju unutar brave, što dovodi do blokiranja većinu vremena.

Kako povećati brzinu čitanja sa HBase-a do 3 puta i sa HDFS-a do 5 puta

Shvativši ovo, shvatili smo da se problem može zaobići stvaranjem niza nezavisnih SSC-ova:

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

I onda radite s njima, isključujući raskrsnice također na posljednjoj cifri pomaka:

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

Sada možete početi sa testiranjem. Da bismo to učinili, čitat ćemo datoteke iz HDFS-a pomoću jednostavne aplikacije s više niti. Podesite 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

I samo pročitajte fajlove:

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

Ovaj kod se izvršava u odvojenim nitima i povećaćemo broj istovremeno čitanih datoteka (sa 10 na 200 - horizontalna osa) i broj keš memorija (sa 1 ​​na 10 - grafika). Vertikalna os pokazuje ubrzanje koje je rezultat povećanja SSC-a u odnosu na slučaj kada postoji samo jedna keš memorija.

Kako povećati brzinu čitanja sa HBase-a do 3 puta i sa HDFS-a do 5 puta

Kako čitati grafikon: Vrijeme izvršenja za 100 hiljada čitanja u blokovima od 64 KB sa jednom keš memorijom zahtijeva 78 sekundi. Dok sa 5 keš memorija potrebno je 16 sekundi. One. postoji ubrzanje od ~5 puta. Kao što se može vidjeti iz grafikona, efekat nije jako primjetan za mali broj paralelnih čitanja, počinje da igra primjetnu ulogu kada ima više od 50 čitanja niti. Također je primjetno da povećanje broja SSC-ova sa 6 i iznad daje značajno manje povećanje performansi.

Napomena 1: budući da su rezultati testa prilično promjenjivi (vidi dolje), obavljena su 3 ciklusa i dobivene vrijednosti su usrednjene.

Napomena 2: Dobitak performansi od konfigurisanja slučajnog pristupa je isti, iako je sam pristup nešto sporiji.

Međutim, potrebno je pojasniti da, za razliku od slučaja sa HBase, ovo ubrzanje nije uvijek besplatno. Ovdje "otključavamo" sposobnost CPU-a da radi više, umjesto da visi na bravama.

Kako povećati brzinu čitanja sa HBase-a do 3 puta i sa HDFS-a do 5 puta

Ovdje možete primijetiti da, općenito, povećanje broja keš memorija daje približno proporcionalno povećanje iskorištenosti CPU-a. Međutim, dobitnih kombinacija ima nešto više.

Na primjer, pogledajmo bliže postavku SSC = 3. Povećanje performansi u rasponu je oko 3.3 puta. Ispod su rezultati iz sve tri odvojene vožnje.

Kako povećati brzinu čitanja sa HBase-a do 3 puta i sa HDFS-a do 5 puta

Dok se potrošnja CPU-a povećava za oko 2.8 puta. Razlika nije velika, ali mala Greta je već sretna i možda ima vremena da ide u školu i ide na časove.

Dakle, ovo će imati pozitivan učinak za svaki alat koji koristi masovni pristup HDFS-u (na primjer Spark, itd.), pod uvjetom da je kod aplikacije lagan (tj. utikač je na strani HDFS klijenta) i da postoji besplatna CPU snaga . Da provjerimo, hajde da testiramo kakav će efekat imati kombinovana upotreba optimizacije BlockCache-a i SSC podešavanja za čitanje sa HBase-a.

Kako povećati brzinu čitanja sa HBase-a do 3 puta i sa HDFS-a do 5 puta

Vidi se da u ovakvim uslovima efekat nije tako veliki kao u rafiniranim testovima (čitanje bez ikakve obrade), ali je ovde sasvim moguće istisnuti dodatnih 80K. Zajedno, obje optimizacije pružaju do 4x ubrzanje.

Napravljen je i PR za ovu optimizaciju [HDFS-15202], koji je spojen i ova funkcionalnost će biti dostupna u budućim izdanjima.

I na kraju, bilo je zanimljivo uporediti performanse čitanja slične baze podataka širokih kolona, ​​Cassandra i HBase.

Da bismo to učinili, pokrenuli smo instance standardnog YCSB uslužnog programa za testiranje opterećenja sa dva hosta (ukupno 800 niti). Na strani servera - 4 instance RegionServer i Cassandra na 4 hosta (ne na onima na kojima klijenti rade, da bi se izbjegao njihov utjecaj). Očitavanja su dolazila iz tablica veličine:

HBase – 300 GB na HDFS (100 GB čisti podaci)

Cassandra - 250 GB (faktor replikacije = 3)

One. volumen je bio približno isti (u HBase malo više).

HBase parametri:

dfs.client.short.circuit.num = 5 (HDFS optimizacija klijenta)

hbase.lru.cache.heavy.eviction.count.limit = 30 - to znači da će zakrpa početi raditi nakon 30 izbacivanja (~5 minuta)

hbase.lru.cache.heavy.eviction.mb.size.limit = 300 — ciljni obim keširanja i izbacivanja

YCSB dnevnici su raščlanjeni i kompajlirani u Excel grafikone:

Kako povećati brzinu čitanja sa HBase-a do 3 puta i sa HDFS-a do 5 puta

Kao što vidite, ove optimizacije omogućavaju upoređivanje performansi ovih baza podataka u ovim uslovima i postizanje 450 hiljada čitanja u sekundi.

Nadamo se da će ove informacije nekome biti korisne tokom uzbudljive borbe za produktivnost.

izvor: www.habr.com

Dodajte komentar