Kako povećati brzinu čitanja iz HBase do 3 puta i iz HDFS do 5 puta

Visoke performanse jedan su od ključnih zahtjeva pri radu s velikim podacima. U odjelu za učitavanje podataka u Sberbanku, gotovo sve transakcije pumpamo u naš Data Cloud temeljen na Hadoopu i stoga se nosimo sa stvarno velikim protokom informacija. Naravno, uvijek tražimo načine za poboljšanje performansi, a sada vam želimo reći kako smo uspjeli zakrpati RegionServer HBase i HDFS klijent, zahvaljujući čemu smo uspjeli značajno povećati brzinu operacija čitanja.
Kako povećati brzinu čitanja iz HBase do 3 puta i iz HDFS do 5 puta

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

Zašto HDD i brzi Random Access čitanja nisu kompatibilni
Kao što znate, HBase, kao i mnoge druge baze podataka, pohranjuju podatke u blokove veličine nekoliko desetaka kilobajta. Standardno 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č. Budući da je veličina bloka u HFiles 64 KB, zahtjev će biti 640 puta veći (samo minutu!) nego što je potrebno.

Zatim, budući da će zahtjev proći kroz HDFS i njegov mehanizam za predmemoriju metapodataka ShortCircuitCache (koji omogućuje izravan pristup datotekama), to dovodi do čitanja već 1 MB s diska. Međutim, to se može podesiti pomoću parametra dfs.client.read.shortcircuit.buffer.size au mnogim slučajevima ima smisla smanjiti ovu vrijednost, na primjer na 126 KB.

Recimo da to učinimo, ali osim toga, kada počnemo čitati podatke putem Java API-ja, kao što su funkcije poput FileChannel.read i zatražimo od operativnog sustava da pročita određenu 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 zastavice FADV_RANDOM za sprječavanje ovakvog ponašanja.

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

Određeni dobitak od postavljanja oznake FADV_RANDOM može se dobiti, ali samo s visokom višenitnošću i veličinom bloka od 128 KB, ali to je najviše nekoliko desetaka posto:

Kako povećati brzinu čitanja iz HBase do 3 puta i iz HDFS do 5 puta

Testovi su provedeni na 100 datoteka, svaka veličine 1 GB i smještenih na 10 HDD-ova.

Izračunajmo na što u principu možemo računati ovom brzinom:
Recimo da čitamo s 10 diskova brzinom od 280 MB/sec, tj. 3 milijuna puta 100 bajtova. Ali kao što se sjećamo, podataka koji su nam potrebni je 2600 puta manje od onoga što se čita. Dakle, podijelimo 3 milijuna s 2600 i dobijemo 1100 zapisa u sekundi.

Depresivno, zar ne? To je priroda Nasumični pristup pristup podacima na HDD-u - bez obzira na veličinu bloka. Ovo je fizička granica nasumičnog pristupa i nijedna baza podataka ne može istisnuti više pod takvim uvjetima.

Kako onda baze podataka postižu mnogo veće brzine? Kako bismo odgovorili na ovo pitanje, pogledajmo što se događa na sljedećoj slici:

Kako povećati brzinu čitanja iz HBase do 3 puta i iz HDFS do 5 puta

Ovdje vidimo da je prvih nekoliko minuta brzina stvarno oko tisuću zapisa u sekundi. No, nadalje, s obzirom na to da se čita puno više nego što je traženo, podaci završavaju u buff/cacheu operativnog sustava (linux) i brzina raste na pristojnijih 60 tisuća u sekundi

Stoga ćemo se dalje baviti ubrzavanjem pristupa samo onim podacima koji se nalaze u predmemorij OS-a ili se nalaze u SSD/NVMe uređajima za pohranu usporedive brzine pristupa.

U našem slučaju, provest ćemo testove na stolu od 4 poslužitelja, od kojih se svaki naplaćuje kako slijedi:

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

I ovdje je ključna točka količina podataka u tablicama koje je potrebno pročitati. Činjenica je da ako čitate podatke iz tablice koja je u cijelosti smještena u HBase cache, onda neće doći ni do čitanja iz buff/cache operativnog sustava. Budući da HBase prema zadanim postavkama dodjeljuje 40% memorije strukturi koja se zove BlockCache. U biti ovo je ConcurrentHashMap, gdje je ključ naziv datoteke + pomak bloka, a vrijednost su stvarni podaci na ovom pomaku.

Dakle, čitajući samo iz ove strukture, mi vidimo izvrsnu brzinu, poput milijun zahtjeva u sekundi. Ali zamislimo da ne možemo dodijeliti stotine gigabajta memorije samo za potrebe baze podataka, jer ima puno drugih korisnih stvari koje rade na ovim poslužiteljima.

Na primjer, u našem slučaju, volumen BlockCache-a na jednom RS-u 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, npr. neka budu 4 tablice, svaka po 130 regija, u kojima su datoteke veličine 800 MB komprimirane FAST_DIFF-om, tj. ukupno 410 GB (radi se o čistim podacima, tj. bez uzimanja u obzir faktora replikacije).

Dakle, BlockCache čini samo oko 23% ukupne količine podataka i to je puno bliže stvarnim uvjetima onoga što se naziva BigData. I tu počinje zabava - jer očito, što manje pogodaka u predmemoriju, lošija je izvedba. Uostalom, ako promašite, morat ćete puno raditi - tj. spustite se na funkcije sustava za pozivanje. No, to se ne može izbjeći, pa pogledajmo potpuno drugačiji aspekt – što se događa s podacima unutar predmemorije?

Pojednostavimo situaciju i pretpostavimo da imamo predmemoriju u koju stane samo 1 objekt. Evo primjera što će se dogoditi kada pokušamo raditi s količinom podataka 3 puta većom od predmemorije, morat ćemo:

1. Stavite blok 1 u predmemoriju
2. Uklonite blok 1 iz predmemorije
3. Stavite blok 2 u predmemoriju
4. Uklonite blok 2 iz predmemorije
5. Stavite blok 3 u predmemoriju

5 akcija završeno! Međutim, ova situacija se ne može nazvati normalnom; zapravo, tjeramo HBase da radi hrpu potpuno beskorisnog posla. Konstantno čita podatke iz predmemorije OS-a, stavlja ih u BlockCache, da bi ih gotovo odmah izbacio jer je stigla nova porcija podataka. Animacija na početku posta pokazuje srž problema - Garbage Collector ide van granica, atmosfera se zahuktava, mala Greta u dalekoj i vrućoj Švedskoj se uzrujava. A mi informatičari baš ne volimo kad su djeca tužna, pa počnemo razmišljati što možemo učiniti po tom pitanju.

Što ako ne stavite sve blokove u predmemoriju, već samo određeni postotak njih, tako da se predmemorija ne prelijeva? Započnimo jednostavnim dodavanjem samo nekoliko redaka 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;
      }
    }
...

Ovdje se radi o sljedećem: offset je položaj bloka u datoteci i njegove zadnje znamenke su nasumično i ravnomjerno raspoređene od 00 do 99. Stoga ćemo preskočiti samo one koji spadaju u raspon koji nam treba.

Na primjer, postavite cacheDataBlockPercent = 20 i pogledajte što će se dogoditi:

Kako povećati brzinu čitanja iz HBase do 3 puta i iz HDFS do 5 puta

Rezultat je očit. Na grafikonima u nastavku postaje jasno zašto je došlo do takvog ubrzanja - štedimo puno GC resursa bez obavljanja sizifovskog posla smještanja podataka u predmemoriju samo da bismo ih odmah bacili u odvod marsovskim psima:

Kako povećati brzinu čitanja iz HBase do 3 puta i iz HDFS do 5 puta

U isto vrijeme, iskorištenost CPU-a se povećava, ali je puno manja od produktivnosti:

Kako povećati brzinu čitanja iz HBase do 3 puta i iz HDFS do 5 puta

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

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

Sada povećajmo opterećenje i lagano zategnite značajku u jednom potezu. U prvom testu postavili smo granični postotak = 20, a BlockCache je malo nedovoljno iskorišten. Sada je postavimo na 23% i dodamo 100 niti svakih 5 minuta da vidimo u kojoj točki dolazi do zasićenja:

Kako povećati brzinu čitanja iz HBase do 3 puta i iz HDFS do 5 puta

Ovdje vidimo da je originalna verzija gotovo odmah dostigla plafon s oko 100 tisuća zahtjeva u sekundi. Dok zakrpa daje ubrzanje do 300 tisuća. Istovremeno, jasno je da daljnje ubrzanje više nije tako “besplatno”, raste i iskorištenje CPU-a.

Međutim, ovo nije baš elegantno rješenje, budući da ne znamo unaprijed koliki postotak blokova treba predmemorirati, to ovisi o profilu opterećenja. Stoga je implementiran mehanizam za automatsko podešavanje ovog parametra ovisno o 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 predmemorije prije nego počnemo koristiti optimizaciju (tj. preskakanje blokova). Prema zadanim postavkama jednak je MAX_INT = 2147483647 i zapravo znači da značajka nikada neće početi raditi s ovom vrijednošću. Zato što proces istjerivanja počinje svakih 5 - 10 sekundi (ovisi o opterećenju) i 2147483647 * 10 / 60 / 60 / 24 / 365 = 680 godina. Međutim, ovaj parametar možemo postaviti na 0 i učiniti da značajka radi odmah nakon pokretanja.

Međutim, postoji i korisno opterećenje u ovom parametru. Ako je naše opterećenje takvo da se kratkoročna očitavanja (recimo tijekom dana) i dugotrajna čitanja (noću) stalno isprepliću, tada možemo osigurati da je značajka uključena samo kada su u tijeku dugotrajne operacije čitanja.

Na primjer, znamo da kratkotrajna očitanja obično traju oko 1 minutu. Nema potrebe za pokretanjem izbacivanja blokova, predmemorija neće imati vremena zastarjeti i tada možemo postaviti ovaj parametar jednak, na primjer, 10. To će dovesti do činjenice da će optimizacija početi raditi tek kada dugo- započeo je termin aktivnog čitanja, tj. za 100 sekundi. Dakle, ako imamo kratkotrajno čitanje, tada će svi blokovi ići u predmemoriju i bit će dostupni (osim onih koji će biti izbačeni standardnim algoritmom). A kad vršimo dugotrajna očitavanja, značajka je uključena i imali bismo mnogo bolje performanse.

hbase.lru.cache.heavy.eviction.mb.size.limit — postavlja koliko megabajta želimo staviti u predmemoriju (i, naravno, izbaciti) u 10 sekundi. Značajka će pokušati postići tu vrijednost i održati je. Poanta je sljedeća: ako gurnemo gigabajte u predmemoriju, onda ćemo morati izbaciti gigabajte, a to je, kao što smo vidjeli gore, vrlo skupo. Međutim, ne biste ga trebali pokušati postaviti premalo, jer će to uzrokovati preuranjeni izlaz iz načina preskakanja blokova. Za moćne poslužitelje (oko 20-40 fizičkih jezgri) optimalno je postaviti oko 300-400 MB. Za srednju klasu (~10 jezgri) 200-300 MB. Za slabe sustave (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 učitavanja (čitanja) i onda svakih ~10 sekundi izračunavamo koliko je bajtova bilo iseljeni pomoću formule:

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

Ako je zapravo 2000 MB deložirano, onda je Overhead jednak:

2000 * 100 / 500 - 100 = 300%

Algoritmi pokušavaju zadržati najviše nekoliko desetaka postotaka, tako da će značajka smanjiti postotak predmemoriranih blokova, implementirajući na taj način mehanizam automatskog podešavanja.

Međutim, ako opterećenje padne, recimo da je samo 200 MB izbačeno i Overhead postaje negativan (tzv. prekoračenje):

200 * 100 / 500 - 100 = -60%

Naprotiv, značajka će povećati postotak predmemoriranih blokova sve dok Overhead ne postane pozitivan.

Ispod je primjer kako to izgleda na stvarnim podacima. Nema potrebe pokušavati doći do 0%, to je nemoguće. Vrlo je dobro kada je oko 30 - 100%, što pomaže u izbjegavanju preranog izlaska iz načina optimizacije tijekom kratkotrajnih skokova.

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

Na primjer, postavili smo ovaj koeficijent = 0.01. To znači da će se Overhead (vidi gore) pomnožiti s ovim brojem dobivenog rezultata i postotak predmemoriranih blokova će se smanjiti. Pretpostavimo da je Overhead = 300% i koeficijent = 0.01, tada će se postotak predmemoriranih blokova smanjiti za 3%.

Slična logika "povratnog tlaka" također je implementirana za negativne vrijednosti prekoračenja (prekoračenja). Budući da su kratkoročne fluktuacije u količini čitanja i izbacivanja uvijek moguće, ovaj mehanizam omogućuje izbjegavanje preranog izlaska iz načina optimizacije. Povratni pritisak ima obrnutu logiku: što je jače prekoračenje, to se više blokova sprema u predmemoriju.

Kako povećati brzinu čitanja iz HBase do 3 puta i iz HDFS do 5 puta

Implementacijski kod

        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 na stvarnom primjeru. Imamo sljedeću testnu skriptu:

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

Radimo dva pokretanja, prvo hbase.lru.cache.heavy.eviction.count.limit = 10000 (što zapravo onemogućuje značajku), a zatim postavljamo limit = 0 (omogućuje je).

U zapisima ispod vidimo kako se značajka uključuje i resetira Overshooting na 14-71%. S vremena na vrijeme opterećenje se smanjuje, što uključuje Backpressure i HBase ponovno sprema više blokova.

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

Skeniranja su bila potrebna kako bi se prikazao isti proces u obliku grafikona odnosa između dva odjeljka predmemorije - jednostruki (gdje su blokovi koji nikad prije nisu bili zatraženi) i višestruki (ovdje se pohranjuju podaci koji su "traženi" barem jednom):

Kako povećati brzinu čitanja iz HBase do 3 puta i iz HDFS do 5 puta

I za kraj, kako izgleda rad parametara u obliku grafikona. Usporedbe radi, predmemorija je na početku bila potpuno isključena, zatim je pokrenut HBase s predmemorijom i odgodom početka rada optimizacije za 5 minuta (30 ciklusa izbacivanja).

Cijeli kod možete pronaći u Pull Requestu HBASE 23887 na githubu.

No, 300 tisuća čitanja u sekundi nije sve što se može postići na ovom hardveru u ovim uvjetima. Činjenica je da kada trebate pristupiti podacima putem HDFS-a, koristi se mehanizam ShortCircuitCache (u daljnjem tekstu SSC) koji vam omogućuje izravan 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 odvijaju unutar brave, što dovodi do blokiranja većinu vremena.

Kako povećati brzinu čitanja iz HBase do 3 puta i iz HDFS do 5 puta

Nakon što smo to shvatili, shvatili smo da se problem može zaobići stvaranjem niza neovisnih SSC-ova:

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

Zatim radite s njima, isključujući sjecišta također na zadnjoj ofsetnoj znamenki:

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

Sada možete početi s testiranjem. Da bismo to učinili, čitat ćemo datoteke iz HDFS-a jednostavnom višenitnom aplikacijom. Postavite 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 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);
}

Ovaj kod se izvodi u zasebnim nitima i povećat ćemo broj datoteka koje se istovremeno čitaju (od 10 do 200 - horizontalna os) i broj predmemorija (od 1 do 10 - grafika). Okomita os pokazuje ubrzanje koje je rezultat povećanja SSC-a u odnosu na slučaj kada postoji samo jedan cache.

Kako povećati brzinu čitanja iz HBase do 3 puta i iz HDFS do 5 puta

Kako čitati graf: Vrijeme izvršenja za 100 tisuća čitanja u blokovima od 64 KB s jednom predmemorijom zahtijeva 78 sekundi. Dok s 5 predmemorija potrebno je 16 sekundi. Oni. dolazi do akceleracije ~5 puta. Kao što se može vidjeti iz grafikona, učinak nije jako primjetan za mali broj paralelnih čitanja; počinje igrati primjetnu ulogu kada ima više od 50 čitanja niti. Također je vidljivo da povećanje broja SSC-ova od 6 i iznad daje znatno manje povećanje performansi.

Napomena 1: budući da su rezultati ispitivanja prilično nestabilni (vidi dolje), provedena su 3 rada i dobivene vrijednosti su prosječne.

Napomena 2: Poboljšanje performansi od konfiguracije nasumičnog pristupa je isto, iako je sam pristup malo sporiji.

No, potrebno je pojasniti da, za razliku od slučaja s HBase-om, ovo ubrzanje nije uvijek besplatno. Ovdje "otključavamo" sposobnost CPU-a da radi više, umjesto da visi na bravi.

Kako povećati brzinu čitanja iz HBase do 3 puta i iz HDFS do 5 puta

Ovdje možete primijetiti da, općenito, povećanje broja predmemorija daje približno proporcionalno povećanje iskorištenja CPU-a. Ipak, ima nešto više dobitnih kombinacija.

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

Kako povećati brzinu čitanja iz HBase do 3 puta i iz HDFS 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 ići u školu i podučavati se.

Stoga će to imati pozitivan učinak za bilo koji alat koji koristi masovni pristup HDFS-u (na primjer Spark, itd.), pod uvjetom da je kod aplikacije lagan (tj. priključak je na strani HDFS klijenta) i da postoji slobodna CPU snaga . Da provjerimo, testirajmo kakav će učinak imati kombinirana upotreba optimizacije BlockCache i SSC podešavanja za čitanje iz HBase-a.

Kako povećati brzinu čitanja iz HBase do 3 puta i iz HDFS do 5 puta

Vidi se da pod ovakvim uvjetima učinak nije tako veliki kao u rafiniranim testovima (čitanje bez ikakve obrade), ali i ovdje je sasvim moguće izgurati dodatnih 80K. Zajedno, obje optimizacije omogućuju do 4x ubrzanje.

Za ovu optimizaciju je također napravljen PR [HDFS-15202], koji je spojen i ova će funkcionalnost biti dostupna u budućim izdanjima.

I na kraju, bilo je zanimljivo usporediti performanse čitanja slične baze podataka sa širokim stupcima, Cassandra i HBase.

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

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

Cassandra - 250 GB (faktor replikacije = 3)

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

HBase parametri:

dfs.client.short.circuit.num = 5 (optimizacija HDFS 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 — ciljani volumen predmemoriranja i izbacivanja

YCSB zapisnici su analizirani i sastavljeni u Excel grafikone:

Kako povećati brzinu čitanja iz HBase do 3 puta i iz HDFS do 5 puta

Kao što vidite, ove optimizacije omogućuju usporedbu performansi ovih baza podataka pod ovim uvjetima i postizanje 450 tisuća čitanja u sekundi.

Nadamo se da ove informacije mogu biti korisne nekome tijekom uzbudljive borbe za produktivnost.

Izvor: www.habr.com

Dodajte komentar