Cum să crești viteza de citire de la HBase de până la 3 ori și de la HDFS de până la 5 ori

Performanța ridicată este una dintre cerințele cheie atunci când lucrați cu date mari. În departamentul de încărcare a datelor de la Sberbank, pompăm aproape toate tranzacțiile în Data Cloud-ul nostru bazat pe Hadoop și, prin urmare, ne ocupăm de fluxuri foarte mari de informații. Bineînțeles, căutăm mereu modalități de îmbunătățire a performanței, iar acum vrem să vă spunem cum am reușit să corectăm RegionServer HBase și clientul HDFS, datorită căruia am reușit să creștem semnificativ viteza operațiunilor de citire.
Cum să crești viteza de citire de la HBase de până la 3 ori și de la HDFS de până la 5 ori

Cu toate acestea, înainte de a trece la esența îmbunătățirilor, merită să vorbim despre restricții care, în principiu, nu pot fi ocolite dacă stai pe un HDD.

De ce HDD-ul și citirile rapide cu acces aleatoriu sunt incompatibile
După cum știți, HBase și multe alte baze de date stochează date în blocuri de câteva zeci de kiloocteți. În mod implicit, este de aproximativ 64 KB. Acum să ne imaginăm că trebuie să obținem doar 100 de octeți și cerem HBase să ne dea aceste date folosind o anumită cheie. Deoarece dimensiunea blocului în HFiles este de 64 KB, cererea va fi de 640 de ori mai mare (doar un minut!) decât este necesar.

Apoi, deoarece cererea va trece prin HDFS și prin mecanismul său de stocare în cache a metadatelor ShortCircuitCache (care permite accesul direct la fișiere), acest lucru duce la citirea a 1 MB deja de pe disc. Cu toate acestea, acest lucru poate fi ajustat cu parametrul dfs.client.read.scurtcircuit.buffer.dimensiune și în multe cazuri are sens să reduceți această valoare, de exemplu la 126 KB.

Să presupunem că facem acest lucru, dar în plus, când începem să citim date prin API-ul java, cum ar fi funcții precum FileChannel.read și cerem sistemului de operare să citească cantitatea specificată de date, se citește „pentru orice eventualitate” de 2 ori mai mult , adică 256 KB în cazul nostru. Acest lucru se datorează faptului că Java nu are o modalitate ușoară de a seta steag-ul FADV_RANDOM pentru a preveni acest comportament.

Ca rezultat, pentru a obține cei 100 de octeți ai noștri, se citesc de 2600 de ori mai mult sub capotă. S-ar părea că soluția este evidentă, să reducem dimensiunea blocului la un kilooctet, să setăm steagul menționat și să obținem o accelerație mare a iluminării. Dar problema este că prin reducerea dimensiunii blocului de 2 ori, reducem și numărul de octeți citiți pe unitatea de timp de 2 ori.

Se poate obține un câștig din setarea steagului FADV_RANDOM, dar numai cu multi-threading mare și cu o dimensiune a blocului de 128 KB, dar acesta este un maxim de câteva zeci de procente:

Cum să crești viteza de citire de la HBase de până la 3 ori și de la HDFS de până la 5 ori

Testele au fost efectuate pe 100 de fișiere, fiecare cu dimensiunea de 1 GB și situate pe 10 HDD-uri.

Să calculăm pe ce ne putem baza, în principiu, la această viteză:
Să presupunem că citim de pe 10 discuri cu o viteză de 280 MB/sec, adică. de 3 milioane de ori 100 de octeți. Dar după cum ne amintim, datele de care avem nevoie sunt de 2600 de ori mai puține decât cele citite. Astfel, împărțim 3 milioane la 2600 și obținem 1100 de înregistrări pe secundă.

Deprimant, nu-i așa? Asta e natura Acces aleatoriu acces la datele de pe HDD - indiferent de dimensiunea blocului. Aceasta este limita fizică a accesului aleatoriu și nicio bază de date nu poate strânge mai mult în astfel de condiții.

Atunci cum ating bazele de date viteze mult mai mari? Pentru a răspunde la această întrebare, să ne uităm la ce se întâmplă în imaginea următoare:

Cum să crești viteza de citire de la HBase de până la 3 ori și de la HDFS de până la 5 ori

Aici vedem că în primele minute viteza este într-adevăr de aproximativ o mie de înregistrări pe secundă. Totuși, în continuare, datorită faptului că se citește mult mai mult decât s-a cerut, datele ajung în buff-ul/cache-ul sistemului de operare (linux) și viteza crește la 60 de mii pe secundă mai decentă.

Astfel, în continuare ne vom ocupa de accelerarea accesului doar la datele care se află în cache-ul sistemului de operare sau aflate în dispozitivele de stocare SSD/NVMe cu viteză de acces comparabilă.

În cazul nostru, vom efectua teste pe un banc de 4 servere, fiecare dintre ele taxat după cum urmează:

CPU: Xeon E5-2680 v4 @ 2.40GHz 64 fire.
Memorie: 730 GB.
versiunea java: 1.8.0_111

Și aici punctul cheie este cantitatea de date din tabele care trebuie citită. Faptul este că, dacă citiți date dintr-un tabel care este plasat în întregime în memoria cache HBase, atunci nici măcar nu va ajunge la citirea din buff-ul/cache-ul sistemului de operare. Deoarece HBase alocă implicit 40% din memorie unei structuri numite BlockCache. În esență, acesta este un ConcurrentHashMap, unde cheia este numele fișierului + offset-ul blocului, iar valoarea sunt datele reale la acest offset.

Astfel, când citim doar din această structură, noi vedem o viteza excelenta, ca un milion de cereri pe secundă. Dar să ne imaginăm că nu putem aloca sute de gigaocteți de memorie doar pentru nevoile bazei de date, deoarece există o mulțime de alte lucruri utile care rulează pe aceste servere.

De exemplu, în cazul nostru, volumul BlockCache pe un RS este de aproximativ 12 GB. Am aterizat două RS pe un singur nod, de exemplu. 96 GB sunt alocați pentru BlockCache pe toate nodurile. Și există de multe ori mai multe date, de exemplu, să fie 4 tabele, 130 de regiuni fiecare, în care fișierele au o dimensiune de 800 MB, comprimate de FAST_DIFF, adică. un total de 410 GB (aceasta sunt date pure, adică fără a lua în considerare factorul de replicare).

Astfel, BlockCache reprezintă doar aproximativ 23% din volumul total de date și acesta este mult mai aproape de condițiile reale ale ceea ce se numește BigData. Și aici începe distracția - pentru că, evident, cu cât mai puține accesări în cache, cu atât performanța este mai proastă. La urma urmei, dacă ratezi, va trebui să faci multă muncă - adică. mergeți la apelarea funcțiilor sistemului. Cu toate acestea, acest lucru nu poate fi evitat, așa că să ne uităm la un aspect complet diferit - ce se întâmplă cu datele din interiorul cache-ului?

Să simplificăm situația și să presupunem că avem un cache care se potrivește doar pentru 1 obiect. Iată un exemplu despre ceea ce se va întâmpla când vom încerca să lucrăm cu un volum de date de 3 ori mai mare decât memoria cache, va trebui să:

1. Puneți blocul 1 în cache
2. Scoateți blocul 1 din cache
3. Puneți blocul 2 în cache
4. Scoateți blocul 2 din cache
5. Puneți blocul 3 în cache

5 acțiuni finalizate! Cu toate acestea, această situație nu poate fi numită normală; de fapt, forțăm HBase să facă o grămadă de muncă complet inutile. Citește constant date din memoria cache a sistemului de operare, le plasează în BlockCache, doar pentru a le arunca aproape imediat, deoarece a sosit o nouă porțiune de date. Animația de la începutul postării arată esența problemei - Garbage Collector iese la scară, atmosfera se încălzește, micuța Greta din îndepărtata și fierbinte Suedia se supără. Și nouă, IT-ului, chiar nu ne place când copiii sunt triști, așa că începem să ne gândim la ce putem face în privința asta.

Ce se întâmplă dacă nu puneți toate blocurile în cache, ci doar un anumit procent din ele, astfel încât cache-ul să nu depășească? Să începem prin a adăuga doar câteva linii de cod la începutul funcției pentru introducerea datelor în BlockCache:

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

Ideea aici este următoarea: offset este poziția blocului în fișier și ultimele sale cifre sunt distribuite aleatoriu și uniform de la 00 la 99. Prin urmare, vom sări peste cele care se încadrează în intervalul de care avem nevoie.

De exemplu, setați cacheDataBlockPercent = 20 și vedeți ce se întâmplă:

Cum să crești viteza de citire de la HBase de până la 3 ori și de la HDFS de până la 5 ori

Rezultatul este evident. În graficele de mai jos, devine clar de ce a avut loc o astfel de accelerare - economisim o mulțime de resurse GC fără a face munca Sisyphean de a plasa date în cache doar pentru a le arunca imediat în scurgerea câinilor marțieni:

Cum să crești viteza de citire de la HBase de până la 3 ori și de la HDFS de până la 5 ori

În același timp, utilizarea procesorului crește, dar este mult mai mică decât productivitatea:

Cum să crești viteza de citire de la HBase de până la 3 ori și de la HDFS de până la 5 ori

De asemenea, merită remarcat faptul că blocurile stocate în BlockCache sunt diferite. Majoritatea, aproximativ 95%, sunt date în sine. Iar restul sunt metadate, cum ar fi filtrele Bloom sau LEAF_INDEX și т.д.. Aceste date nu sunt suficiente, dar sunt foarte utile, pentru că înainte de a accesa datele direct, HBase apelează la meta pentru a înțelege dacă este necesar să căutați mai departe aici și, dacă da, unde se află exact blocul de interes.

Prin urmare, în cod vedem o condiție de verificare buf.getBlockType().isData() și datorită acestui meta, îl vom lăsa în cache în orice caz.

Acum, să creștem sarcina și să întărim ușor funcția dintr-o singură mișcare. În primul test am făcut procentul de limită = 20 și BlockCache a fost ușor subutilizat. Acum să-l setăm la 23% și să adăugăm 100 de fire la fiecare 5 minute pentru a vedea în ce moment are loc saturația:

Cum să crești viteza de citire de la HBase de până la 3 ori și de la HDFS de până la 5 ori

Aici vedem că versiunea originală atinge aproape imediat plafonul la aproximativ 100 de mii de solicitări pe secundă. În timp ce patch-ul oferă o accelerație de până la 300 de mii. În același timp, este clar că accelerarea ulterioară nu mai este atât de „gratuită”; utilizarea CPU este, de asemenea, în creștere.

Cu toate acestea, aceasta nu este o soluție foarte elegantă, deoarece nu știm dinainte ce procent de blocuri trebuie să fie stocate în cache, depinde de profilul de încărcare. Prin urmare, a fost implementat un mecanism de reglare automată a acestui parametru în funcție de activitatea operațiilor de citire.

Au fost adăugate trei opțiuni pentru a controla acest lucru:

hbase.lru.cache.heavy.eviction.count.limit — stabilește de câte ori ar trebui să ruleze procesul de evacuare a datelor din cache înainte de a începe să folosim optimizarea (adică să omitem blocuri). În mod implicit, este egal cu MAX_INT = 2147483647 și, de fapt, înseamnă că caracteristica nu va începe niciodată să funcționeze cu această valoare. Pentru că procesul de evacuare începe la fiecare 5 - 10 secunde (depinde de sarcină) și 2147483647 * 10 / 60 / 60 / 24 / 365 = 680 de ani. Cu toate acestea, putem seta acest parametru la 0 și face ca funcția să funcționeze imediat după lansare.

Cu toate acestea, există și o sarcină utilă în acest parametru. Dacă sarcina noastră este de așa natură încât citirile pe termen scurt (să zicem în timpul zilei) și citirile pe termen lung (noaptea) sunt în mod constant intercalate, atunci ne putem asigura că funcția este activată numai atunci când sunt în desfășurare operațiuni de citire lungă.

De exemplu, știm că citirile pe termen scurt durează de obicei aproximativ 1 minut. Nu este nevoie să începeți să aruncați blocuri, memoria cache nu va avea timp să devină învechită și apoi putem seta acest parametru egal cu, de exemplu, 10. Acest lucru va duce la faptul că optimizarea va începe să funcționeze numai atunci când a început lectura activă a termenului, adică în 100 de secunde. Astfel, dacă avem o citire pe termen scurt, atunci toate blocurile vor intra în cache și vor fi disponibile (cu excepția celor care vor fi evacuate de algoritmul standard). Și când facem citiri pe termen lung, funcția este activată și am avea performanțe mult mai mari.

hbase.lru.cache.heavy.eviction.mb.size.limit — setează câți megaocteți am dori să plasăm în cache (și, bineînțeles, să scoatem) în 10 secunde. Caracteristica va încerca să atingă această valoare și să o mențină. Ideea este aceasta: dacă introducem gigabytes în cache, atunci va trebui să scoatem gigabytes, iar acest lucru, așa cum am văzut mai sus, este foarte scump. Cu toate acestea, nu ar trebui să încercați să îl setați prea mic, deoarece acest lucru va duce la ieșirea prematură a modului de ignorare a blocurilor. Pentru serverele puternice (aproximativ 20-40 de nuclee fizice), este optim să setați aproximativ 300-400 MB. Pentru clasa de mijloc (~10 nuclee) 200-300 MB. Pentru sisteme slabe (2-5 nuclee) 50-100 MB pot fi normale (nu sunt testate pe acestea).

Să ne uităm la cum funcționează: să presupunem că setăm hbase.lru.cache.heavy.eviction.mb.size.limit = 500, există un fel de încărcare (citire) și apoi la fiecare ~10 secunde calculăm câți octeți au fost evacuat folosind formula:

Overhead = Suma octeților eliberați (MB) * 100 / Limită (MB) - 100;

Dacă de fapt 2000 MB au fost evacuați, atunci Overhead este egal cu:

2000 * 100 / 500 - 100 = 300%

Algoritmii încearcă să nu mențină mai mult de câteva zeci de procente, astfel încât caracteristica va reduce procentul de blocuri stocate în cache, implementând astfel un mecanism de reglare automată.

Cu toate acestea, dacă sarcina scade, să presupunem că doar 200 MB sunt evacuați și Overhead devine negativ (așa-numita depășire):

200 * 100 / 500 - 100 = -60%

Dimpotrivă, caracteristica va crește procentul de blocuri stocate în cache până când Overhead devine pozitiv.

Mai jos este un exemplu despre cum arată acest lucru pe datele reale. Nu este nevoie să încerci să ajungi la 0%, este imposibil. Este foarte bun când este de aproximativ 30 - 100%, acest lucru ajută la evitarea ieșirii premature din modul de optimizare în timpul creșterilor pe termen scurt.

hbase.lru.cache.heavy.eviction.overhead.coeficient — stabilește cât de repede dorim să obținem rezultatul. Dacă știm cu siguranță că citirile noastre sunt în mare parte lungi și nu vrem să așteptăm, putem crește acest raport și obține performanțe ridicate mai repede.

De exemplu, setăm acest coeficient = 0.01. Aceasta înseamnă că Overhead (vezi mai sus) va fi înmulțit cu acest număr cu rezultatul rezultat și procentul de blocuri din cache va fi redus. Să presupunem că Overhead = 300% și coeficient = 0.01, atunci procentul de blocuri din cache va fi redus cu 3%.

O logică similară „Contropresiune” este, de asemenea, implementată pentru valori negative Overhead (depășire). Deoarece fluctuațiile pe termen scurt ale volumului de citiri și evacuări sunt întotdeauna posibile, acest mecanism vă permite să evitați ieșirea prematură din modul de optimizare. Contrapresiunea are o logică inversată: cu cât depășirea este mai puternică, cu atât mai multe blocuri sunt stocate în cache.

Cum să crești viteza de citire de la HBase de până la 3 ori și de la HDFS de până la 5 ori

Cod de implementare

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

Să ne uităm acum la toate acestea folosind un exemplu real. Avem următorul script de testare:

  1. Să începem să facem Scanare (25 fire, lot = 100)
  2. După 5 minute, adăugați mai multe opțiuni (25 fire, lot = 100)
  3. După 5 minute, dezactivați opțiunile multiple (rămâne din nou doar scanarea)

Facem două rulări, mai întâi hbase.lru.cache.heavy.eviction.count.limit = 10000 (care dezactivează caracteristica), apoi setăm limit = 0 (o activează).

În jurnalele de mai jos vedem cum funcția este activată și resetează Overshooting la 14-71%. Din când în când, sarcina scade, ceea ce pornește Backpressure și HBase memorează din nou mai multe blocuri.

Log RegionServer
evacuat (MB): 0, raport 0.0, supraîncărcare (%): -100, contor de evacuare grea: 0, bloc de date curent în cache (%): 100
evacuat (MB): 0, raport 0.0, supraîncărcare (%): -100, contor de evacuare grea: 0, bloc de date curent în cache (%): 100
evacuat (MB): 2170, raport 1.09, overhead (%): 985, contor de evacuare grea: 1, cache curent DataBlock (%): 91 < start
evacuate (MB): 3763, raport 1.08, supraîncărcare (%): 1781, contor de evacuare grea: 2, cache curent DataBlock (%): 76
evacuate (MB): 3306, raport 1.07, supraîncărcare (%): 1553, contor de evacuare grea: 3, cache curent DataBlock (%): 61
evacuate (MB): 2508, raport 1.06, supraîncărcare (%): 1154, contor de evacuare grea: 4, cache curent DataBlock (%): 50
evacuate (MB): 1824, raport 1.04, supraîncărcare (%): 812, contor de evacuare grea: 5, cache curent DataBlock (%): 42
evacuate (MB): 1482, raport 1.03, supraîncărcare (%): 641, contor de evacuare grea: 6, cache curent DataBlock (%): 36
evacuate (MB): 1140, raport 1.01, supraîncărcare (%): 470, contor de evacuare grea: 7, cache curent DataBlock (%): 32
evacuate (MB): 913, raport 1.0, supraîncărcare (%): 356, contor de evacuare grea: 8, cache curent DataBlock (%): 29
evacuate (MB): 912, raport 0.89, supraîncărcare (%): 356, contor de evacuare grea: 9, cache curent DataBlock (%): 26
evacuate (MB): 684, raport 0.76, supraîncărcare (%): 242, contor de evacuare grea: 10, cache curent DataBlock (%): 24
evacuate (MB): 684, raport 0.61, supraîncărcare (%): 242, contor de evacuare grea: 11, cache curent DataBlock (%): 22
evacuate (MB): 456, raport 0.51, supraîncărcare (%): 128, contor de evacuare grea: 12, cache curent DataBlock (%): 21
evacuate (MB): 456, raport 0.42, supraîncărcare (%): 128, contor de evacuare grea: 13, cache curent DataBlock (%): 20
evacuate (MB): 456, raport 0.33, supraîncărcare (%): 128, contor de evacuare grea: 14, cache curent DataBlock (%): 19
evacuate (MB): 342, raport 0.33, supraîncărcare (%): 71, contor de evacuare grea: 15, cache curent DataBlock (%): 19
evacuate (MB): 342, raport 0.32, supraîncărcare (%): 71, contor de evacuare grea: 16, cache curent DataBlock (%): 19
evacuate (MB): 342, raport 0.31, supraîncărcare (%): 71, contor de evacuare grea: 17, cache curent DataBlock (%): 19
evacuate (MB): 228, raport 0.3, supraîncărcare (%): 14, contor de evacuare grea: 18, cache curent DataBlock (%): 19
evacuate (MB): 228, raport 0.29, supraîncărcare (%): 14, contor de evacuare grea: 19, cache curent DataBlock (%): 19
evacuate (MB): 228, raport 0.27, supraîncărcare (%): 14, contor de evacuare grea: 20, cache curent DataBlock (%): 19
evacuate (MB): 228, raport 0.25, supraîncărcare (%): 14, contor de evacuare grea: 21, cache curent DataBlock (%): 19
evacuate (MB): 228, raport 0.24, supraîncărcare (%): 14, contor de evacuare grea: 22, cache curent DataBlock (%): 19
evacuate (MB): 228, raport 0.22, supraîncărcare (%): 14, contor de evacuare grea: 23, cache curent DataBlock (%): 19
evacuate (MB): 228, raport 0.21, supraîncărcare (%): 14, contor de evacuare grea: 24, cache curent DataBlock (%): 19
evacuate (MB): 228, raport 0.2, supraîncărcare (%): 14, contor de evacuare grea: 25, cache curent DataBlock (%): 19
evacuate (MB): 228, raport 0.17, supraîncărcare (%): 14, contor de evacuare grea: 26, cache curent DataBlock (%): 19
evacuat (MB): 456, raport 0.17, overhead (%): 128, contor de evacuare grea: 27, cache curent DataBlock (%): 18 < obținute adăugate (dar tabelul la fel)
evacuate (MB): 456, raport 0.15, supraîncărcare (%): 128, contor de evacuare grea: 28, cache curent DataBlock (%): 17
evacuate (MB): 342, raport 0.13, supraîncărcare (%): 71, contor de evacuare grea: 29, cache curent DataBlock (%): 17
evacuate (MB): 342, raport 0.11, supraîncărcare (%): 71, contor de evacuare grea: 30, cache curent DataBlock (%): 17
evacuate (MB): 342, raport 0.09, supraîncărcare (%): 71, contor de evacuare grea: 31, cache curent DataBlock (%): 17
evacuate (MB): 228, raport 0.08, supraîncărcare (%): 14, contor de evacuare grea: 32, cache curent DataBlock (%): 17
evacuate (MB): 228, raport 0.07, supraîncărcare (%): 14, contor de evacuare grea: 33, cache curent DataBlock (%): 17
evacuate (MB): 228, raport 0.06, supraîncărcare (%): 14, contor de evacuare grea: 34, cache curent DataBlock (%): 17
evacuate (MB): 228, raport 0.05, supraîncărcare (%): 14, contor de evacuare grea: 35, cache curent DataBlock (%): 17
evacuate (MB): 228, raport 0.05, supraîncărcare (%): 14, contor de evacuare grea: 36, cache curent DataBlock (%): 17
evacuate (MB): 228, raport 0.04, supraîncărcare (%): 14, contor de evacuare grea: 37, cache curent DataBlock (%): 17
evacuate (MB): 109, raport 0.04, supraîncărcare (%): -46, contor de evacuare grea: 37, cache curent DataBlock (%): 22 < presiune inversă
evacuate (MB): 798, raport 0.24, supraîncărcare (%): 299, contor de evacuare grea: 38, cache curent DataBlock (%): 20
evacuate (MB): 798, raport 0.29, supraîncărcare (%): 299, contor de evacuare grea: 39, cache curent DataBlock (%): 18
evacuate (MB): 570, raport 0.27, supraîncărcare (%): 185, contor de evacuare grea: 40, cache curent DataBlock (%): 17
evacuate (MB): 456, raport 0.22, supraîncărcare (%): 128, contor de evacuare grea: 41, cache curent DataBlock (%): 16
evacuate (MB): 342, raport 0.16, supraîncărcare (%): 71, contor de evacuare grea: 42, cache curent DataBlock (%): 16
evacuate (MB): 342, raport 0.11, supraîncărcare (%): 71, contor de evacuare grea: 43, cache curent DataBlock (%): 16
evacuate (MB): 228, raport 0.09, supraîncărcare (%): 14, contor de evacuare grea: 44, cache curent DataBlock (%): 16
evacuate (MB): 228, raport 0.07, supraîncărcare (%): 14, contor de evacuare grea: 45, cache curent DataBlock (%): 16
evacuate (MB): 228, raport 0.05, supraîncărcare (%): 14, contor de evacuare grea: 46, cache curent DataBlock (%): 16
evacuate (MB): 222, raport 0.04, supraîncărcare (%): 11, contor de evacuare grea: 47, cache curent DataBlock (%): 16
evacuat (MB): 104, raport 0.03, supraîncărcare (%): -48, contor de evacuare grea: 47, cache curent DataBlock (%): 21 < întreruperea devine
evacuate (MB): 684, raport 0.2, supraîncărcare (%): 242, contor de evacuare grea: 48, cache curent DataBlock (%): 19
evacuate (MB): 570, raport 0.23, supraîncărcare (%): 185, contor de evacuare grea: 49, cache curent DataBlock (%): 18
evacuate (MB): 342, raport 0.22, supraîncărcare (%): 71, contor de evacuare grea: 50, cache curent DataBlock (%): 18
evacuate (MB): 228, raport 0.21, supraîncărcare (%): 14, contor de evacuare grea: 51, cache curent DataBlock (%): 18
evacuate (MB): 228, raport 0.2, supraîncărcare (%): 14, contor de evacuare grea: 52, cache curent DataBlock (%): 18
evacuate (MB): 228, raport 0.18, supraîncărcare (%): 14, contor de evacuare grea: 53, cache curent DataBlock (%): 18
evacuate (MB): 228, raport 0.16, supraîncărcare (%): 14, contor de evacuare grea: 54, cache curent DataBlock (%): 18
evacuate (MB): 228, raport 0.14, supraîncărcare (%): 14, contor de evacuare grea: 55, cache curent DataBlock (%): 18
evacuate (MB): 112, raport 0.14, supraîncărcare (%): -44, contor de evacuare grea: 55, cache curent DataBlock (%): 23 < presiune inversă
evacuate (MB): 456, raport 0.26, supraîncărcare (%): 128, contor de evacuare grea: 56, cache curent DataBlock (%): 22
evacuate (MB): 342, raport 0.31, supraîncărcare (%): 71, contor de evacuare grea: 57, cache curent DataBlock (%): 22
evacuate (MB): 342, raport 0.33, supraîncărcare (%): 71, contor de evacuare grea: 58, cache curent DataBlock (%): 22
evacuate (MB): 342, raport 0.33, supraîncărcare (%): 71, contor de evacuare grea: 59, cache curent DataBlock (%): 22
evacuate (MB): 342, raport 0.33, supraîncărcare (%): 71, contor de evacuare grea: 60, cache curent DataBlock (%): 22
evacuate (MB): 342, raport 0.33, supraîncărcare (%): 71, contor de evacuare grea: 61, cache curent DataBlock (%): 22
evacuate (MB): 342, raport 0.33, supraîncărcare (%): 71, contor de evacuare grea: 62, cache curent DataBlock (%): 22
evacuate (MB): 342, raport 0.33, supraîncărcare (%): 71, contor de evacuare grea: 63, cache curent DataBlock (%): 22
evacuate (MB): 342, raport 0.32, supraîncărcare (%): 71, contor de evacuare grea: 64, cache curent DataBlock (%): 22
evacuate (MB): 342, raport 0.33, supraîncărcare (%): 71, contor de evacuare grea: 65, cache curent DataBlock (%): 22
evacuate (MB): 342, raport 0.33, supraîncărcare (%): 71, contor de evacuare grea: 66, cache curent DataBlock (%): 22
evacuate (MB): 342, raport 0.32, supraîncărcare (%): 71, contor de evacuare grea: 67, cache curent DataBlock (%): 22
evacuate (MB): 342, raport 0.33, supraîncărcare (%): 71, contor de evacuare grea: 68, cache curent DataBlock (%): 22
evacuate (MB): 342, raport 0.32, supraîncărcare (%): 71, contor de evacuare grea: 69, cache curent DataBlock (%): 22
evacuate (MB): 342, raport 0.32, supraîncărcare (%): 71, contor de evacuare grea: 70, cache curent DataBlock (%): 22
evacuate (MB): 342, raport 0.33, supraîncărcare (%): 71, contor de evacuare grea: 71, cache curent DataBlock (%): 22
evacuate (MB): 342, raport 0.33, supraîncărcare (%): 71, contor de evacuare grea: 72, cache curent DataBlock (%): 22
evacuate (MB): 342, raport 0.33, supraîncărcare (%): 71, contor de evacuare grea: 73, cache curent DataBlock (%): 22
evacuate (MB): 342, raport 0.33, supraîncărcare (%): 71, contor de evacuare grea: 74, cache curent DataBlock (%): 22
evacuate (MB): 342, raport 0.33, supraîncărcare (%): 71, contor de evacuare grea: 75, cache curent DataBlock (%): 22
evacuate (MB): 342, raport 0.33, supraîncărcare (%): 71, contor de evacuare grea: 76, cache curent DataBlock (%): 22
evacuat (MB): 21, raport 0.33, supraîncărcare (%): -90, contor de evacuare grea: 76, bloc de date curent în cache (%): 32
evacuat (MB): 0, raport 0.0, supraîncărcare (%): -100, contor de evacuare grea: 0, bloc de date curent în cache (%): 100
evacuat (MB): 0, raport 0.0, supraîncărcare (%): -100, contor de evacuare grea: 0, bloc de date curent în cache (%): 100

Scanările au fost necesare pentru a arăta același proces sub forma unui grafic al relației dintre două secțiuni de cache - single (unde blocuri care nu au mai fost solicitate până acum) și multi (datele „solicitate” cel puțin o dată sunt stocate aici):

Cum să crești viteza de citire de la HBase de până la 3 ori și de la HDFS de până la 5 ori

Și, în sfârșit, cum arată funcționarea parametrilor sub forma unui grafic. Pentru comparație, cache-ul a fost complet dezactivat la început, apoi a fost lansat HBase cu memorarea în cache și amânarea începerii lucrărilor de optimizare cu 5 minute (30 de cicluri de evacuare).

Codul complet poate fi găsit în Pull Request HBASE 23887 pe github.

Cu toate acestea, 300 de mii de citiri pe secundă nu este tot ceea ce se poate realiza pe acest hardware în aceste condiții. Faptul este că atunci când trebuie să accesați date prin HDFS, se folosește mecanismul ShortCircuitCache (denumit în continuare SSC), care vă permite să accesați datele direct, evitând interacțiunile în rețea.

Profilarea a arătat că, deși acest mecanism oferă un câștig mare, el devine și la un moment dat un blocaj, deoarece aproape toate operațiunile grele au loc în interiorul unei lacăte, ceea ce duce la blocare de cele mai multe ori.

Cum să crești viteza de citire de la HBase de până la 3 ori și de la HDFS de până la 5 ori

După ce am realizat acest lucru, ne-am dat seama că problema poate fi ocolită prin crearea unei serii de SSC independente:

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

Și apoi lucrați cu ele, excluzând intersecțiile și la ultima cifră de compensare:

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

Acum puteți începe testarea. Pentru a face acest lucru, vom citi fișiere din HDFS cu o aplicație simplă multi-threaded. Setați parametrii:

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 doar citește fișierele:

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

Acest cod este executat în fire separate și vom crește numărul de fișiere citite simultan (de la 10 la 200 - axa orizontală) și numărul de cache (de la 1 la 10 - grafică). Axa verticală arată accelerația care rezultă dintr-o creștere a SSC față de cazul în care există un singur cache.

Cum să crești viteza de citire de la HBase de până la 3 ori și de la HDFS de până la 5 ori

Cum se citește graficul: Timpul de execuție pentru 100 de mii de citiri în blocuri de 64 KB cu un singur cache necesită 78 de secunde. În timp ce cu 5 cache durează 16 secunde. Acestea. există o accelerație de ~5 ori. După cum se poate observa din grafic, efectul nu este foarte vizibil pentru un număr mic de citiri paralele, începe să joace un rol vizibil atunci când sunt mai mult de 50 de citiri de fire. De asemenea, se observă că creșterea numărului de SSC-uri de la 6 și mai sus oferă o creștere semnificativ mai mică a performanței.

Nota 1: deoarece rezultatele testului sunt destul de volatile (vezi mai jos), au fost efectuate 3 rulări și valorile rezultate au fost mediate.

Nota 2: Câștigul de performanță din configurarea accesului aleatoriu este același, deși accesul în sine este puțin mai lent.

Cu toate acestea, este necesar să lămurim că, spre deosebire de cazul HBase, această accelerație nu este întotdeauna gratuită. Aici „deblochăm” capacitatea procesorului de a lucra mai mult, în loc să ne agățăm de încuietori.

Cum să crești viteza de citire de la HBase de până la 3 ori și de la HDFS de până la 5 ori

Aici puteți observa că, în general, o creștere a numărului de cache dă o creștere aproximativ proporțională a utilizării CPU. Cu toate acestea, există puțin mai multe combinații câștigătoare.

De exemplu, să aruncăm o privire mai atentă la setarea SSC = 3. Creșterea performanței pe interval este de aproximativ 3.3 ori. Mai jos sunt rezultatele din toate cele trei runde separate.

Cum să crești viteza de citire de la HBase de până la 3 ori și de la HDFS de până la 5 ori

În timp ce consumul procesorului crește de aproximativ 2.8 ori. Diferența nu este foarte mare, dar micuța Greta este deja fericită și poate avea timp să meargă la școală și să ia lecții.

Astfel, acest lucru va avea un efect pozitiv pentru orice instrument care utilizează acces în bloc la HDFS (de exemplu Spark, etc.), cu condiția ca codul aplicației să fie ușor (adică mufa să fie pe partea clientului HDFS) și să existe putere CPU liberă. . Pentru a verifica, să testăm ce efect va avea utilizarea combinată a optimizării BlockCache și a reglajului SSC pentru citirea din HBase.

Cum să crești viteza de citire de la HBase de până la 3 ori și de la HDFS de până la 5 ori

Se poate observa că în astfel de condiții efectul nu este la fel de mare ca în testele rafinate (citire fără nicio prelucrare), dar este foarte posibil să stoarceți încă 80K aici. Împreună, ambele optimizări oferă o accelerare de până la 4x.

S-a făcut și un PR pentru această optimizare [HDFS-15202], care a fost fuzionat și această funcționalitate va fi disponibilă în versiunile viitoare.

Și, în sfârșit, a fost interesant să comparăm performanța de citire a unei baze de date similare cu coloane largi, Cassandra și HBase.

Pentru a face acest lucru, am lansat instanțe ale utilitarului standard de testare a încărcării YCSB de la două gazde (800 de fire în total). Pe partea de server - 4 instanțe de RegionServer și Cassandra pe 4 gazde (nu cele în care rulează clienții, pentru a evita influența lor). Citirile au venit din tabele de dimensiuni:

HBase – 300 GB pe HDFS (100 GB date pure)

Cassandra - 250 GB (factor de replicare = 3)

Acestea. volumul a fost aproximativ același (în HBase puțin mai mult).

Parametrii HBase:

dfs.client.short.circuit.num = 5 (Optimizare client HDFS)

hbase.lru.cache.heavy.eviction.count.limit = 30 - asta înseamnă că plasturele va începe să funcționeze după 30 de evacuari (~5 minute)

hbase.lru.cache.heavy.eviction.mb.size.limit = 300 — volumul țintă de stocare în cache și evacuare

Jurnalele YCSB au fost analizate și compilate în grafice Excel:

Cum să crești viteza de citire de la HBase de până la 3 ori și de la HDFS de până la 5 ori

După cum puteți vedea, aceste optimizări fac posibilă compararea performanței acestor baze de date în aceste condiții și obținerea a 450 de mii de citiri pe secundă.

Sperăm că aceste informații pot fi utile cuiva în timpul luptei incitante pentru productivitate.

Sursa: www.habr.com

Adauga un comentariu