Hogyan növelhető az olvasási sebesség a HBase-ről akár 3-szorosra és a HDFS-ről akár ötszörösére

A nagy teljesítmény az egyik kulcsfontosságú követelmény a nagy adatokkal végzett munka során. A Sberbank adatbetöltési osztályán szinte minden tranzakciót a Hadoop-alapú adatfelhőnkbe pumpálunk, és ezért igazán nagy információáramlással foglalkozunk. Természetesen mindig keressük a lehetőségeket a teljesítmény javítására, most pedig szeretnénk elmondani, hogyan sikerült a RegionServer HBase és a HDFS kliens foltozása, aminek köszönhetően jelentősen meg tudtuk növelni az olvasási műveletek sebességét.
Hogyan növelhető az olvasási sebesség a HBase-ről akár 3-szorosra és a HDFS-ről akár ötszörösére

Mielőtt azonban rátérnénk a fejlesztések lényegére, érdemes beszélni azokról a korlátozásokról, amelyeket elvileg nem lehet megkerülni, ha HDD-n ülünk.

Miért nem kompatibilis a HDD és a gyors Random Access olvasás?
Mint tudják, a HBase és sok más adatbázis több tíz kilobájt méretű blokkokban tárolja az adatokat. Alapértelmezés szerint körülbelül 64 KB. Most képzeljük el, hogy csak 100 bájtot kell kapnunk, és megkérjük a HBase-t, hogy adja meg ezeket az adatokat egy bizonyos kulccsal. Mivel a HFiles blokkmérete 64 KB, a kérés 640-szer nagyobb lesz (csak egy perccel!) a szükségesnél.

Következő, mivel a kérés a HDFS-en és a metaadat-gyorsítótárazási mechanizmuson keresztül megy át ShortCircuitCache (ami lehetővé teszi a fájlok közvetlen elérését), ez már 1 MB kiolvasásához vezet a lemezről. Ez azonban a paraméterrel állítható dfs.client.read.shortcircuit.buffer.size és sok esetben van értelme ezt az értéket csökkenteni, például 126 KB-ra.

Tegyük fel, hogy ezt tesszük, de ezen felül, amikor elkezdünk adatokat olvasni a java API-n keresztül, például a FileChannel.read függvényeket, és megkérjük az operációs rendszert, hogy olvassa be a megadott mennyiségű adatot, akkor kétszer többet olvas „csak abban az esetben” , azaz esetünkben 2 KB. Ennek az az oka, hogy a java nem rendelkezik egyszerű módszerrel a FADV_RANDOM jelző beállítására, hogy megakadályozza ezt a viselkedést.

Ennek eredményeként, hogy megkapjuk a 100 bájtunkat, 2600-szor több olvasható a motorháztető alatt. Úgy tűnik, a megoldás kézenfekvő, csökkentsük a blokk méretét kilobyte-ra, állítsuk be az említett zászlót, és kapjunk nagy megvilágosodási gyorsulást. De az a baj, hogy a blokk méretének 2-szeres csökkentésével az időegységenként kiolvasott bájtok számát is 2-szeresére csökkentjük.

Némi haszon a FADV_RANDOM jelző beállításával érhető el, de csak magas többszálú és 128 KB-os blokkmérettel, de ez legfeljebb néhány tíz százalék:

Hogyan növelhető az olvasási sebesség a HBase-ről akár 3-szorosra és a HDFS-ről akár ötszörösére

A teszteket 100 fájlon végezték el, mindegyik 1 GB méretű és 10 HDD-n található.

Számítsuk ki, mire számíthatunk elvileg ennél a sebességnél:
Tegyük fel, hogy 10 lemezről olvasunk 280 MB/sec sebességgel, pl. 3 milliószor 100 bájt. De mint emlékszünk, a szükséges adat 2600-szor kevesebb, mint amit olvasunk. Így 3 milliót elosztunk 2600-zal, és megkapjuk 1100 rekord másodpercenként.

Nyomasztó, nem? Ilyen a természet Véletlen hozzáférés hozzáférés a HDD-n lévő adatokhoz – a blokk méretétől függetlenül. Ez a véletlen hozzáférés fizikai határa, és ilyen feltételek mellett egyetlen adatbázis sem tud többet kipréselni.

Hogyan érhetnek el sokkal nagyobb sebességet az adatbázisok? A kérdés megválaszolásához nézzük meg, mi történik a következő képen:

Hogyan növelhető az olvasási sebesség a HBase-ről akár 3-szorosra és a HDFS-ről akár ötszörösére

Itt azt látjuk, hogy az első percekben a sebesség valóban körülbelül ezer rekord/másodperc. Azonban, mivel sokkal többet olvasnak, mint amennyit kértek, az adatok az operációs rendszer (linux) buffjában/cache-jében végzik, és a sebesség tisztességesebb másodpercenként 60 ezerre nő.

Így a továbbiakban csak az operációs rendszer gyorsítótárában vagy a hasonló sebességű SSD/NVMe tárolóeszközökön található adatokhoz való hozzáférés felgyorsításával fogunk foglalkozni.

Esetünkben 4 szerverből álló padon végezzük el a teszteket, amelyek mindegyike az alábbiak szerint kerül felszámolásra:

CPU: Xeon E5-2680 v4 @ 2.40 GHz, 64 szál.
Memória: 730 GB.
java verzió: 1.8.0_111

És itt a kulcsfontosságú pont a táblázatokban található adatok mennyisége, amelyet el kell olvasni. A helyzet az, hogy ha egy olyan táblából olvassa be az adatokat, amely teljes egészében a HBase gyorsítótárban van, akkor az nem is fog kiolvasni az operációs rendszer buff/cache-éből. Mivel a HBase alapértelmezés szerint a memória 40%-át a BlockCache nevű struktúrához rendeli. Lényegében ez egy ConcurrentHashMap, ahol a kulcs a fájlnév + a blokk eltolása, az érték pedig a tényleges adat ennél az eltolásnál.

Így ha csak ebből a szerkezetből olvasunk, mi kiváló sebességet látunk, mint egy millió kérés másodpercenként. De képzeljük el, hogy nem tudunk több száz gigabájt memóriát lefoglalni pusztán adatbázis-szükségletekre, mert sok más hasznos dolog is fut ezeken a szervereken.

Például esetünkben a BlockCache mennyisége egy RS-en körülbelül 12 GB. Egy csomóponton két RS-t landoltunk, i.e. 96 GB van lefoglalva a BlockCache számára az összes csomóponton. És sokszor több adat van, például legyen 4 tábla, egyenként 130 régió, amiben a fájlok 800 MB méretűek, FAST_DIFF-el tömörítve, pl. összesen 410 GB (ez tiszta adat, vagyis a replikációs tényező figyelembevétele nélkül).

Így a BlockCache a teljes adatmennyiségnek csak körülbelül 23%-át teszi ki, és ez sokkal közelebb áll az úgynevezett BigData valós feltételeihez. És itt kezdődik a móka – mert nyilvánvalóan minél kevesebb a gyorsítótár találata, annál rosszabb a teljesítmény. Hiszen ha hiányzik, rengeteget kell dolgoznia – pl. menjen le a rendszerfunkciók hívásához. Ezt azonban nem lehet elkerülni, ezért nézzünk egy teljesen más szempontot – mi történik a gyorsítótáron belüli adatokkal?

Egyszerűsítsük le a helyzetet, és tegyük fel, hogy van egy gyorsítótárunk, amely csak 1 objektumra fér el. Íme egy példa arra, hogy mi fog történni, ha a gyorsítótárnál háromszor nagyobb adatmennyiséggel próbálunk dolgozni, a következőket kell tenni:

1. Helyezze az 1. blokkot a gyorsítótárba
2. Távolítsa el az 1. blokkot a gyorsítótárból
3. Helyezze az 2. blokkot a gyorsítótárba
4. Távolítsa el az 2. blokkot a gyorsítótárból
5. Helyezze az 3. blokkot a gyorsítótárba

5 akció befejeződött! Ez a helyzet azonban nem nevezhető normálisnak, sőt, egy csomó teljesen haszontalan munkára kényszerítjük a HBase-t. Folyamatosan beolvassa az adatokat az operációs rendszer gyorsítótárából, elhelyezi a BlockCache-ben, de szinte azonnal kidobja, mert új adatrész érkezett. A poszt elején található animáció megmutatja a probléma lényegét - a Garbage Collector leáll a méretéről, a légkör felforrósodik, a kis Gréta a távoli és forró Svédországban ideges lesz. És mi informatikusok nagyon nem szeretjük, ha a gyerekek szomorúak, ezért elkezdünk gondolkodni azon, hogy mit tehetünk ellene.

Mi van, ha nem az összes blokkot helyezi a gyorsítótárba, hanem csak egy bizonyos százalékát, hogy a gyorsítótár ne csorduljon túl? Kezdjük azzal, hogy egyszerűen adjunk hozzá néhány kódsort az adatok BlockCache-be helyezéséhez szükséges függvény elejéhez:

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

A lényeg itt a következő: az offset a blokk helyzete a fájlban, és utolsó számjegyei véletlenszerűen és egyenletesen vannak elosztva 00 és 99 között. Ezért csak azokat hagyjuk ki, amelyek a szükséges tartományba esnek.

Például állítsa be a cacheDataBlockPercent = 20 értéket, és nézze meg, mi történik:

Hogyan növelhető az olvasási sebesség a HBase-ről akár 3-szorosra és a HDFS-ről akár ötszörösére

Az eredmény nyilvánvaló. Az alábbi grafikonokon világossá válik, hogy miért történt ilyen gyorsulás – rengeteg GC-erőforrást takarítunk meg anélkül, hogy elvégeznénk a sziszifuszi munkát, hogy a gyorsítótárba helyezzük az adatokat, csak hogy azonnal a marsi kutyák csatornájába dobjuk:

Hogyan növelhető az olvasási sebesség a HBase-ről akár 3-szorosra és a HDFS-ről akár ötszörösére

Ugyanakkor a CPU kihasználtsága nő, de jóval kisebb, mint a termelékenység:

Hogyan növelhető az olvasási sebesség a HBase-ről akár 3-szorosra és a HDFS-ről akár ötszörösére

Azt is érdemes megjegyezni, hogy a BlockCache-ben tárolt blokkok eltérőek. A legtöbb, körülbelül 95%-a maga az adat. A többi pedig metaadatok, például Bloom-szűrők vagy LEAF_INDEX és т.д.. Ezek az adatok nem elégségesek, de nagyon hasznosak, mert az adatok közvetlen elérése előtt a HBase a meta felé fordul, hogy megértse, kell-e itt tovább keresgélni, és ha igen, akkor pontosan hol található az érdeklődési kör.

Ezért a kódban egy ellenőrzési feltételt látunk buf.getBlockType().isData() és ennek a metának köszönhetően mindenképpen a gyorsítótárban hagyjuk.

Most növeljük a terhelést, és enyhén erősítsük meg a funkciót egy mozdulattal. Az első tesztben 20-at tettünk ki, és a BlockCache kissé alul volt kihasználva. Most állítsuk 23%-ra, és 100 percenként adjunk hozzá 5 szálat, hogy lássuk, mikor következik be a telítettség:

Hogyan növelhető az olvasási sebesség a HBase-ről akár 3-szorosra és a HDFS-ről akár ötszörösére

Itt azt látjuk, hogy az eredeti verzió szinte azonnal eléri a plafont körülbelül 100 ezer kérés/másodpercnél. Míg a patch akár 300 ezres gyorsulást ad. Ugyanakkor jól látható, hogy a további gyorsítás már nem annyira „ingyen”, a CPU kihasználtsága is növekszik.

Ez azonban nem túl elegáns megoldás, hiszen nem tudjuk előre, hogy a blokkok hány százalékát kell gyorsítótárazni, ez a terhelési profiltól függ. Ezért egy olyan mechanizmust vezettek be, amely automatikusan beállítja ezt a paramétert az olvasási műveletek aktivitásától függően.

Három lehetőség került hozzáadásra ennek szabályozására:

hbase.lru.cache.heavy.eviction.count.limit — beállítja, hogy az adatok gyorsítótárból való kiürítésének folyamata hányszor fusson le, mielőtt elkezdjük az optimalizálást (vagyis a blokkok átugrását). Alapértelmezés szerint ez egyenlő a MAX_INT = 2147483647 értékkel, és valójában azt jelenti, hogy a szolgáltatás soha nem fog működni ezzel az értékkel. Mivel a kilakoltatási folyamat 5-10 másodpercenként indul (a terheléstől függ), és 2147483647 * 10 / 60 / 60 / 24 / 365 = 680 év. Ezt a paramétert azonban 0-ra állíthatjuk, és azonnal működésbe hozhatjuk a funkciót az indítás után.

Ebben a paraméterben azonban van egy hasznos terhelés is. Ha a terhelésünk olyan, hogy a rövid távú (mondjuk nappal) és a hosszú távú (éjszakai) olvasások állandóan szóba kerülnek, akkor biztos lehetünk benne, hogy a funkció csak hosszú olvasási műveletek esetén kapcsol be.

Például tudjuk, hogy a rövid távú leolvasások általában körülbelül 1 percig tartanak. Nem kell elkezdeni a blokkokat kidobni, a gyorsítótárnak nem lesz ideje elavulni, és akkor ezt a paramétert beállíthatjuk például 10-re. Ez azt eredményezi, hogy az optimalizálás csak akkor fog működni, ha hosszú terminus aktív olvasás megkezdődött, i.e. 100 másodperc alatt. Így, ha van egy rövid távú olvasásunk, akkor minden blokk a gyorsítótárba kerül, és elérhető lesz (kivéve azokat, amelyeket a szabványos algoritmus kiürít). És amikor hosszú távú olvasást végzünk, a funkció be van kapcsolva, és sokkal nagyobb teljesítményt érnénk el.

hbase.lru.cache.heavy.eviction.mb.size.limit — beállítja, hogy 10 másodperc alatt hány megabájtot szeretnénk elhelyezni a gyorsítótárban (és természetesen kidobni). A funkció megpróbálja elérni ezt az értéket és fenntartani. A lényeg a következő: ha gigabájtokat tolunk a gyorsítótárba, akkor gigabájtokat kell kilakoltatnunk, és ez, mint fentebb láttuk, nagyon drága. Azonban ne próbálja meg túl kicsire állítani, mert ez a blokkhagyás mód idő előtti kilépését okozza. Erőteljes szervereknél (kb. 20-40 fizikai mag) az optimális körülbelül 300-400 MB beállítása. Középosztálynak (~10 mag) 200-300 MB. Gyenge rendszereknél (2-5 mag) 50-100 MB lehet normális (ezeken nem tesztelték).

Nézzük meg, hogyan működik ez: tegyük fel, hogy beállítjuk a hbase.lru.cache.heavy.eviction.mb.size.limit = 500-at, van valami terhelés (olvasás), majd ~10 másodpercenként kiszámoljuk, hogy hány bájt volt kilakoltatás a következő képlet alapján:

Overhead = felszabadított bájtok összege (MB) * 100 / korlát (MB) - 100;

Ha valóban 2000 MB-ot kilakoltattak volna, akkor a rezsi egyenlő:

2000 * 100 / 500 - 100 = 300%

Az algoritmusok legfeljebb néhány tíz százalékot próbálnak fenntartani, így a funkció csökkenti a gyorsítótárazott blokkok százalékos arányát, ezáltal egy automatikus hangolási mechanizmust valósít meg.

Ha azonban csökken a terhelés, mondjuk csak 200 MB kerül kiürítésre, és az Overhead negatív lesz (ún. túllövés):

200 * 100 / 500 - 100 = -60%

Éppen ellenkezőleg, a szolgáltatás növeli a gyorsítótárazott blokkok százalékos arányát, amíg az Overhead pozitív lesz.

Az alábbiakban egy példa látható, hogyan néz ki ez valós adatokon. Nem kell megpróbálni elérni a 0%-ot, ez lehetetlen. Nagyon jó, ha ez körülbelül 30-100%, ez segít elkerülni az optimalizálási módból való idő előtti kilépést rövid távú túlfeszültség esetén.

hbase.lru.cache.heavy.eviction.overhead.coefficient — beállítja, hogy milyen gyorsan szeretnénk elérni az eredményt. Ha biztosan tudjuk, hogy olvasmányaink többnyire hosszúak, és nem akarunk várni, akkor ezt az arányt növelhetjük, és gyorsabban érhetjük el a nagy teljesítményt.

Például ezt az együtthatót 0.01-re állítjuk. Ez azt jelenti, hogy az általános költségeket (lásd fent) megszorozzuk ezzel a számmal a kapott eredménnyel, és a gyorsítótárazott blokkok százalékos aránya csökken. Tegyük fel, hogy Overhead = 300% és együttható = 0.01, akkor a gyorsítótárazott blokkok százalékos aránya 3%-kal csökken.

Hasonló „Hátnyomás” logika van megvalósítva a negatív Overhead (túllövés) értékek esetében is. Mivel a leolvasások és a kilakoltatások mennyiségének rövid távú ingadozása mindig lehetséges, ez a mechanizmus lehetővé teszi az optimalizálási módból való idő előtti kilépés elkerülését. Az ellennyomásnak fordított logikája van: minél erősebb a túllépés, annál több blokk kerül gyorsítótárba.

Hogyan növelhető az olvasási sebesség a HBase-ről akár 3-szorosra és a HDFS-ről akár ötszörösére

Megvalósítási kód

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

Nézzük most mindezt egy valós példa segítségével. A következő tesztszkriptünk van:

  1. Kezdjük el a beolvasást (25 szál, köteg = 100)
  2. 5 perc elteltével adjon hozzá multi-geteket (25 szál, köteg = 100)
  3. 5 perc elteltével kapcsolja ki a multi-geteket (csak a szkennelés marad újra)

Két futtatást végzünk, először a hbase.lru.cache.heavy.eviction.count.limit = 10000 (ami valójában letiltja a szolgáltatást), majd a limit = 0 értéket állítja be (engedélyezi).

Az alábbi naplókban láthatjuk, hogy a funkció hogyan van bekapcsolva, és visszaállítja a túllövést 14-71%-ra. A terhelés időről időre csökken, ami bekapcsolja a Backpressure-t, és a HBase ismét több blokkot gyorsítótáraz.

Log RegionServer
kilakoltatás (MB): 0, arány 0.0, rezsi (%): -100, nehéz kilakoltatási számláló: 0, aktuális gyorsítótár DataBlock (%): 100
kilakoltatás (MB): 0, arány 0.0, rezsi (%): -100, nehéz kilakoltatási számláló: 0, aktuális gyorsítótár DataBlock (%): 100
kilakoltatás (MB): 2170, arány 1.09, rezsi (%): 985, nehéz kilakoltatási számláló: 1, aktuális gyorsítótár DataBlock (%): 91 < start
kilakoltatás (MB): 3763, arány 1.08, rezsi (%): 1781, nehéz kilakoltatási számláló: 2, aktuális gyorsítótár DataBlock (%): 76
kilakoltatás (MB): 3306, arány 1.07, rezsi (%): 1553, nehéz kilakoltatási számláló: 3, aktuális gyorsítótár DataBlock (%): 61
kilakoltatás (MB): 2508, arány 1.06, rezsi (%): 1154, nehéz kilakoltatási számláló: 4, aktuális gyorsítótár DataBlock (%): 50
kilakoltatás (MB): 1824, arány 1.04, rezsi (%): 812, nehéz kilakoltatási számláló: 5, aktuális gyorsítótár DataBlock (%): 42
kilakoltatás (MB): 1482, arány 1.03, rezsi (%): 641, nehéz kilakoltatási számláló: 6, aktuális gyorsítótár DataBlock (%): 36
kilakoltatás (MB): 1140, arány 1.01, rezsi (%): 470, nehéz kilakoltatási számláló: 7, aktuális gyorsítótár DataBlock (%): 32
kilakoltatás (MB): 913, arány 1.0, rezsi (%): 356, nehéz kilakoltatási számláló: 8, aktuális gyorsítótár DataBlock (%): 29
kilakoltatás (MB): 912, arány 0.89, rezsi (%): 356, nehéz kilakoltatási számláló: 9, aktuális gyorsítótár DataBlock (%): 26
kilakoltatás (MB): 684, arány 0.76, rezsi (%): 242, nehéz kilakoltatási számláló: 10, aktuális gyorsítótár DataBlock (%): 24
kilakoltatás (MB): 684, arány 0.61, rezsi (%): 242, nehéz kilakoltatási számláló: 11, aktuális gyorsítótár DataBlock (%): 22
kilakoltatás (MB): 456, arány 0.51, rezsi (%): 128, nehéz kilakoltatási számláló: 12, aktuális gyorsítótár DataBlock (%): 21
kilakoltatás (MB): 456, arány 0.42, rezsi (%): 128, nehéz kilakoltatási számláló: 13, aktuális gyorsítótár DataBlock (%): 20
kilakoltatás (MB): 456, arány 0.33, rezsi (%): 128, nehéz kilakoltatási számláló: 14, aktuális gyorsítótár DataBlock (%): 19
kilakoltatás (MB): 342, arány 0.33, rezsi (%): 71, nehéz kilakoltatási számláló: 15, aktuális gyorsítótár DataBlock (%): 19
kilakoltatás (MB): 342, arány 0.32, rezsi (%): 71, nehéz kilakoltatási számláló: 16, aktuális gyorsítótár DataBlock (%): 19
kilakoltatás (MB): 342, arány 0.31, rezsi (%): 71, nehéz kilakoltatási számláló: 17, aktuális gyorsítótár DataBlock (%): 19
kilakoltatás (MB): 228, arány 0.3, rezsi (%): 14, nehéz kilakoltatási számláló: 18, aktuális gyorsítótár DataBlock (%): 19
kilakoltatás (MB): 228, arány 0.29, rezsi (%): 14, nehéz kilakoltatási számláló: 19, aktuális gyorsítótár DataBlock (%): 19
kilakoltatás (MB): 228, arány 0.27, rezsi (%): 14, nehéz kilakoltatási számláló: 20, aktuális gyorsítótár DataBlock (%): 19
kilakoltatás (MB): 228, arány 0.25, rezsi (%): 14, nehéz kilakoltatási számláló: 21, aktuális gyorsítótár DataBlock (%): 19
kilakoltatás (MB): 228, arány 0.24, rezsi (%): 14, nehéz kilakoltatási számláló: 22, aktuális gyorsítótár DataBlock (%): 19
kilakoltatás (MB): 228, arány 0.22, rezsi (%): 14, nehéz kilakoltatási számláló: 23, aktuális gyorsítótár DataBlock (%): 19
kilakoltatás (MB): 228, arány 0.21, rezsi (%): 14, nehéz kilakoltatási számláló: 24, aktuális gyorsítótár DataBlock (%): 19
kilakoltatás (MB): 228, arány 0.2, rezsi (%): 14, nehéz kilakoltatási számláló: 25, aktuális gyorsítótár DataBlock (%): 19
kilakoltatás (MB): 228, arány 0.17, rezsi (%): 14, nehéz kilakoltatási számláló: 26, aktuális gyorsítótár DataBlock (%): 19
kilakoltatás (MB): 456, arány 0.17, rezsi (%): 128, nehéz kilakoltatási számláló: 27, aktuális gyorsítótár DataBlock (%): 18 < hozzáadott kap (de ugyanaz a táblázat)
kilakoltatás (MB): 456, arány 0.15, rezsi (%): 128, nehéz kilakoltatási számláló: 28, aktuális gyorsítótár DataBlock (%): 17
kilakoltatás (MB): 342, arány 0.13, rezsi (%): 71, nehéz kilakoltatási számláló: 29, aktuális gyorsítótár DataBlock (%): 17
kilakoltatás (MB): 342, arány 0.11, rezsi (%): 71, nehéz kilakoltatási számláló: 30, aktuális gyorsítótár DataBlock (%): 17
kilakoltatás (MB): 342, arány 0.09, rezsi (%): 71, nehéz kilakoltatási számláló: 31, aktuális gyorsítótár DataBlock (%): 17
kilakoltatás (MB): 228, arány 0.08, rezsi (%): 14, nehéz kilakoltatási számláló: 32, aktuális gyorsítótár DataBlock (%): 17
kilakoltatás (MB): 228, arány 0.07, rezsi (%): 14, nehéz kilakoltatási számláló: 33, aktuális gyorsítótár DataBlock (%): 17
kilakoltatás (MB): 228, arány 0.06, rezsi (%): 14, nehéz kilakoltatási számláló: 34, aktuális gyorsítótár DataBlock (%): 17
kilakoltatás (MB): 228, arány 0.05, rezsi (%): 14, nehéz kilakoltatási számláló: 35, aktuális gyorsítótár DataBlock (%): 17
kilakoltatás (MB): 228, arány 0.05, rezsi (%): 14, nehéz kilakoltatási számláló: 36, aktuális gyorsítótár DataBlock (%): 17
kilakoltatás (MB): 228, arány 0.04, rezsi (%): 14, nehéz kilakoltatási számláló: 37, aktuális gyorsítótár DataBlock (%): 17
kilakoltatás (MB): 109, arány 0.04, rezsi (%): -46, nehéz kilakoltatási számláló: 37, aktuális gyorsítótár DataBlock (%): 22 < ellennyomás
kilakoltatás (MB): 798, arány 0.24, rezsi (%): 299, nehéz kilakoltatási számláló: 38, aktuális gyorsítótár DataBlock (%): 20
kilakoltatás (MB): 798, arány 0.29, rezsi (%): 299, nehéz kilakoltatási számláló: 39, aktuális gyorsítótár DataBlock (%): 18
kilakoltatás (MB): 570, arány 0.27, rezsi (%): 185, nehéz kilakoltatási számláló: 40, aktuális gyorsítótár DataBlock (%): 17
kilakoltatás (MB): 456, arány 0.22, rezsi (%): 128, nehéz kilakoltatási számláló: 41, aktuális gyorsítótár DataBlock (%): 16
kilakoltatás (MB): 342, arány 0.16, rezsi (%): 71, nehéz kilakoltatási számláló: 42, aktuális gyorsítótár DataBlock (%): 16
kilakoltatás (MB): 342, arány 0.11, rezsi (%): 71, nehéz kilakoltatási számláló: 43, aktuális gyorsítótár DataBlock (%): 16
kilakoltatás (MB): 228, arány 0.09, rezsi (%): 14, nehéz kilakoltatási számláló: 44, aktuális gyorsítótár DataBlock (%): 16
kilakoltatás (MB): 228, arány 0.07, rezsi (%): 14, nehéz kilakoltatási számláló: 45, aktuális gyorsítótár DataBlock (%): 16
kilakoltatás (MB): 228, arány 0.05, rezsi (%): 14, nehéz kilakoltatási számláló: 46, aktuális gyorsítótár DataBlock (%): 16
kilakoltatás (MB): 222, arány 0.04, rezsi (%): 11, nehéz kilakoltatási számláló: 47, aktuális gyorsítótár DataBlock (%): 16
kilakoltatás (MB): 104, arány 0.03, általános költségek (%): -48, nehéz kilakoltatási számláló: 47, aktuális gyorsítótár DataBlock (%): 21 < megszakítások
kilakoltatás (MB): 684, arány 0.2, rezsi (%): 242, nehéz kilakoltatási számláló: 48, aktuális gyorsítótár DataBlock (%): 19
kilakoltatás (MB): 570, arány 0.23, rezsi (%): 185, nehéz kilakoltatási számláló: 49, aktuális gyorsítótár DataBlock (%): 18
kilakoltatás (MB): 342, arány 0.22, rezsi (%): 71, nehéz kilakoltatási számláló: 50, aktuális gyorsítótár DataBlock (%): 18
kilakoltatás (MB): 228, arány 0.21, rezsi (%): 14, nehéz kilakoltatási számláló: 51, aktuális gyorsítótár DataBlock (%): 18
kilakoltatás (MB): 228, arány 0.2, rezsi (%): 14, nehéz kilakoltatási számláló: 52, aktuális gyorsítótár DataBlock (%): 18
kilakoltatás (MB): 228, arány 0.18, rezsi (%): 14, nehéz kilakoltatási számláló: 53, aktuális gyorsítótár DataBlock (%): 18
kilakoltatás (MB): 228, arány 0.16, rezsi (%): 14, nehéz kilakoltatási számláló: 54, aktuális gyorsítótár DataBlock (%): 18
kilakoltatás (MB): 228, arány 0.14, rezsi (%): 14, nehéz kilakoltatási számláló: 55, aktuális gyorsítótár DataBlock (%): 18
kilakoltatás (MB): 112, arány 0.14, rezsi (%): -44, nehéz kilakoltatási számláló: 55, aktuális gyorsítótár DataBlock (%): 23 < ellennyomás
kilakoltatás (MB): 456, arány 0.26, rezsi (%): 128, nehéz kilakoltatási számláló: 56, aktuális gyorsítótár DataBlock (%): 22
kilakoltatás (MB): 342, arány 0.31, rezsi (%): 71, nehéz kilakoltatási számláló: 57, aktuális gyorsítótár DataBlock (%): 22
kilakoltatás (MB): 342, arány 0.33, rezsi (%): 71, nehéz kilakoltatási számláló: 58, aktuális gyorsítótár DataBlock (%): 22
kilakoltatás (MB): 342, arány 0.33, rezsi (%): 71, nehéz kilakoltatási számláló: 59, aktuális gyorsítótár DataBlock (%): 22
kilakoltatás (MB): 342, arány 0.33, rezsi (%): 71, nehéz kilakoltatási számláló: 60, aktuális gyorsítótár DataBlock (%): 22
kilakoltatás (MB): 342, arány 0.33, rezsi (%): 71, nehéz kilakoltatási számláló: 61, aktuális gyorsítótár DataBlock (%): 22
kilakoltatás (MB): 342, arány 0.33, rezsi (%): 71, nehéz kilakoltatási számláló: 62, aktuális gyorsítótár DataBlock (%): 22
kilakoltatás (MB): 342, arány 0.33, rezsi (%): 71, nehéz kilakoltatási számláló: 63, aktuális gyorsítótár DataBlock (%): 22
kilakoltatás (MB): 342, arány 0.32, rezsi (%): 71, nehéz kilakoltatási számláló: 64, aktuális gyorsítótár DataBlock (%): 22
kilakoltatás (MB): 342, arány 0.33, rezsi (%): 71, nehéz kilakoltatási számláló: 65, aktuális gyorsítótár DataBlock (%): 22
kilakoltatás (MB): 342, arány 0.33, rezsi (%): 71, nehéz kilakoltatási számláló: 66, aktuális gyorsítótár DataBlock (%): 22
kilakoltatás (MB): 342, arány 0.32, rezsi (%): 71, nehéz kilakoltatási számláló: 67, aktuális gyorsítótár DataBlock (%): 22
kilakoltatás (MB): 342, arány 0.33, rezsi (%): 71, nehéz kilakoltatási számláló: 68, aktuális gyorsítótár DataBlock (%): 22
kilakoltatás (MB): 342, arány 0.32, rezsi (%): 71, nehéz kilakoltatási számláló: 69, aktuális gyorsítótár DataBlock (%): 22
kilakoltatás (MB): 342, arány 0.32, rezsi (%): 71, nehéz kilakoltatási számláló: 70, aktuális gyorsítótár DataBlock (%): 22
kilakoltatás (MB): 342, arány 0.33, rezsi (%): 71, nehéz kilakoltatási számláló: 71, aktuális gyorsítótár DataBlock (%): 22
kilakoltatás (MB): 342, arány 0.33, rezsi (%): 71, nehéz kilakoltatási számláló: 72, aktuális gyorsítótár DataBlock (%): 22
kilakoltatás (MB): 342, arány 0.33, rezsi (%): 71, nehéz kilakoltatási számláló: 73, aktuális gyorsítótár DataBlock (%): 22
kilakoltatás (MB): 342, arány 0.33, rezsi (%): 71, nehéz kilakoltatási számláló: 74, aktuális gyorsítótár DataBlock (%): 22
kilakoltatás (MB): 342, arány 0.33, rezsi (%): 71, nehéz kilakoltatási számláló: 75, aktuális gyorsítótár DataBlock (%): 22
kilakoltatás (MB): 342, arány 0.33, rezsi (%): 71, nehéz kilakoltatási számláló: 76, aktuális gyorsítótár DataBlock (%): 22
kilakoltatás (MB): 21, arány 0.33, rezsi (%): -90, nehéz kilakoltatási számláló: 76, aktuális gyorsítótár DataBlock (%): 32
kilakoltatás (MB): 0, arány 0.0, rezsi (%): -100, nehéz kilakoltatási számláló: 0, aktuális gyorsítótár DataBlock (%): 100
kilakoltatás (MB): 0, arány 0.0, rezsi (%): -100, nehéz kilakoltatási számláló: 0, aktuális gyorsítótár DataBlock (%): 100

A vizsgálatokra azért volt szükség, hogy ugyanazt a folyamatot grafikonon mutassák be két gyorsítótár-szakasz közötti kapcsolatról - egy (ahol korábban soha nem kért blokkok) és több (a legalább egyszer „lekért” adatok itt tárolódnak):

Hogyan növelhető az olvasási sebesség a HBase-ről akár 3-szorosra és a HDFS-ről akár ötszörösére

És végül, hogyan néz ki a paraméterek működése grafikon formájában. Összehasonlításképpen a gyorsítótárat az elején teljesen kikapcsolták, majd a HBase gyorsítótárazással és 5 perccel késlelteti az optimalizálási munka megkezdését (30 kilakoltatási ciklus).

A teljes kód a Pull Request-ben található HBASE 23887 a githubon.

A másodpercenkénti 300 ezer olvasás azonban nem minden, amit ezen a hardveren ilyen körülmények között el lehet érni. A helyzet az, hogy amikor HDFS-en keresztül kell hozzáférnie az adatokhoz, akkor a ShortCircuitCache (a továbbiakban: SSC) mechanizmus kerül alkalmazásra, amely lehetővé teszi az adatok közvetlen elérését, elkerülve a hálózati interakciókat.

A profilozás azt mutatta, hogy bár ez a mechanizmus nagy nyereséget ad, egy ponton szűk keresztmetszetté is válik, mert szinte minden nehéz művelet a záron belül történik, ami legtöbbször blokkoláshoz vezet.

Hogyan növelhető az olvasási sebesség a HBase-ről akár 3-szorosra és a HDFS-ről akár ötszörösére

Ennek felismerése után rájöttünk, hogy a probléma megkerülhető független SSC-k tömbjének létrehozásával:

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

Ezután dolgozzon velük, kivéve a kereszteződéseket az utolsó eltolási számjegynél is:

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

Most elkezdheti a tesztelést. Ehhez egy egyszerű többszálas alkalmazással HDFS-ből olvasunk be fájlokat. Állítsa be a paramétereket:

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

És csak olvassa el a fájlokat:

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

Ez a kód külön szálban fut, és növelni fogjuk az egyidejűleg olvasott fájlok számát (10-ről 200-ra - vízszintes tengely) és a gyorsítótárak számát (1-ről 10-re - grafika). A függőleges tengely azt a gyorsulást mutatja, amely az SSC növekedéséből adódik ahhoz az esethez képest, amikor csak egy gyorsítótár van.

Hogyan növelhető az olvasási sebesség a HBase-ről akár 3-szorosra és a HDFS-ről akár ötszörösére

A grafikon olvasása: A 100 ezer olvasás végrehajtása 64 KB-os blokkokban egy gyorsítótárral 78 másodpercet vesz igénybe. Míg 5 gyorsítótárral ez 16 másodpercet vesz igénybe. Azok. ~5-szörös gyorsulás van. Amint az a grafikonon is látható, a hatás kevés párhuzamos olvasásnál nem túl észrevehető, 50-nél több szál olvasásakor kezd észrevehető szerepet játszani. Az is észrevehető, hogy az SSC-k számát 6-ról növelik. és a fentiek lényegesen kisebb teljesítménynövekedést adnak.

1. megjegyzés: mivel a teszteredmények meglehetősen ingadozóak (lásd alább), 3 futtatást végeztünk, és a kapott értékeket átlagoltuk.

2. megjegyzés: A véletlen hozzáférés konfigurálásából származó teljesítménynövekedés ugyanaz, bár maga a hozzáférés valamivel lassabb.

Azonban tisztázni kell, hogy a HBase esetével ellentétben ez a gyorsulás nem mindig ingyenes. Itt „feloldjuk” a CPU azon képességét, hogy több munkát végezzen, ahelyett, hogy a zárakon lógna.

Hogyan növelhető az olvasási sebesség a HBase-ről akár 3-szorosra és a HDFS-ről akár ötszörösére

Itt megfigyelhető, hogy általában a gyorsítótárak számának növekedése hozzávetőlegesen arányosan növeli a CPU kihasználtságot. Vannak azonban valamivel több nyerő kombináció is.

Például nézzük meg közelebbről az SSC = 3 beállítást. A teljesítmény növekedése a tartományon körülbelül 3.3-szoros. Az alábbiakban mindhárom külön futam eredményeit közöljük.

Hogyan növelhető az olvasási sebesség a HBase-ről akár 3-szorosra és a HDFS-ről akár ötszörösére

Miközben a CPU-fogyasztás körülbelül 2.8-szorosára nő. A különbség nem túl nagy, de a kis Gréta már boldog, és lehet, hogy lesz ideje iskolába járni és leckéket venni.

Így ez pozitív hatással lesz minden olyan eszközre, amely tömeges hozzáférést használ a HDFS-hez (például Spark stb.), feltéve, hogy az alkalmazás kódja könnyű (azaz a csatlakozó a HDFS kliens oldalon van), és van szabad CPU-táp. . Az ellenőrzéshez teszteljük, milyen hatással lesz a BlockCache optimalizálás és az SSC hangolás együttes használata a HBase-ről történő olvasáshoz.

Hogyan növelhető az olvasási sebesség a HBase-ről akár 3-szorosra és a HDFS-ről akár ötszörösére

Látható, hogy ilyen körülmények között nem akkora a hatás, mint a finomított teszteknél (mindenféle feldolgozás nélkül leolvasva), de itt teljesen ki lehet préselni további 80K-t. A két optimalizálás együtt akár 4-szeres gyorsulást biztosít.

Ehhez az optimalizáláshoz PR is készült [HDFS-15202], amelyet egyesítettek, és ez a funkció a jövőbeni kiadásokban lesz elérhető.

És végül érdekes volt összehasonlítani egy hasonló széles oszlopú adatbázis, a Cassandra és a HBase olvasási teljesítményét.

Ennek érdekében elindítottuk a szabványos YCSB terheléstesztelő segédprogram példányait két gazdagépről (összesen 800 szál). Szerver oldalon – a RegionServer és a Cassandra 4 példánya 4 gazdagépen (nem azokon, ahol a kliensek futnak, hogy elkerüljük a befolyásukat). Az adatok a következő méretű táblázatokból származnak:

HBase – 300 GB HDFS-en (100 GB tiszta adat)

Cassandra - 250 GB (replikációs tényező = 3)

Azok. a hangerő megközelítőleg azonos volt (HBase-ben kicsit több).

HBase paraméterek:

dfs.client.short.circuit.num = 5 (HDFS kliens optimalizálás)

hbase.lru.cache.heavy.eviction.count.limit = 30 - ez azt jelenti, hogy a tapasz 30 kilakoltatás után (~5 perc) kezd működni.

hbase.lru.cache.heavy.eviction.mb.size.limit = 300 — a gyorsítótárazás és a kilakoltatás célmennyisége

Az YCSB naplókat elemezte és Excel grafikonokká fordította:

Hogyan növelhető az olvasási sebesség a HBase-ről akár 3-szorosra és a HDFS-ről akár ötszörösére

Amint látja, ezek az optimalizálások lehetővé teszik ezen adatbázisok teljesítményének összehasonlítását ilyen körülmények között, és 450 ezer olvasást érünk el másodpercenként.

Reméljük, hogy ez az információ hasznos lehet valakinek a termelékenységért folytatott izgalmas küzdelem során.

Forrás: will.com

Hozzászólás