Hur man ökar läshastigheten från HBase upp till 3 gånger och från HDFS upp till 5 gånger

Hög prestanda är ett av nyckelkraven när man arbetar med big data. På dataladdningsavdelningen på Sberbank pumpar vi nästan alla transaktioner in i vårt Hadoop-baserade Datamoln och hanterar därför riktigt stora informationsflöden. Naturligtvis letar vi alltid efter sätt att förbättra prestandan, och nu vill vi berätta hur vi lyckades patcha RegionServer HBase och HDFS-klienten, tack vare vilken vi avsevärt kunde öka hastigheten på läsoperationerna.
Hur man ökar läshastigheten från HBase upp till 3 gånger och från HDFS upp till 5 gånger

Men innan du går vidare till kärnan i förbättringarna är det värt att prata om begränsningar som i princip inte kan kringgås om du sitter på en hårddisk.

Varför hårddisk och snabb slumpmässig läsning är inkompatibla
Som ni vet lagrar HBase, och många andra databaser, data i block om flera tiotals kilobyte stora. Som standard är den cirka 64 KB. Låt oss nu föreställa oss att vi bara behöver få 100 byte och vi ber HBase att ge oss dessa data med en viss nyckel. Eftersom blockstorleken i HFiles är 64 KB, kommer begäran att vara 640 gånger större (bara en minut!) än nödvändigt.

Därefter, eftersom begäran kommer att gå igenom HDFS och dess metadata-cachemekanism ShortCircuitCache (vilket tillåter direkt åtkomst till filer), leder detta till att man redan läser 1 MB från disken. Detta kan dock justeras med parametern dfs.client.read.shortcircuit.buffer.storlek och i många fall är det vettigt att minska detta värde, till exempel till 126 KB.

Låt oss säga att vi gör detta, men dessutom, när vi börjar läsa data genom java-api, såsom funktioner som FileChannel.read och ber operativsystemet att läsa den angivna mängden data, läses det "för säkerhets skull" 2 gånger mer , dvs. 256 KB i vårt fall. Detta beror på att java inte har ett enkelt sätt att ställa in flaggan FADV_RANDOM för att förhindra detta beteende.

Som ett resultat, för att få våra 100 byte, läses 2600 gånger mer under huven. Det verkar som att lösningen är uppenbar, låt oss minska blockstorleken till en kilobyte, sätta den nämnda flaggan och få stor upplysningsacceleration. Men problemet är att genom att minska blockstorleken med 2 gånger, minskar vi också antalet byte som läses per tidsenhet med 2 gånger.

Viss vinst från att ställa in FADV_RANDOM-flaggan kan erhållas, men bara med hög flertrådning och med en blockstorlek på 128 KB, men detta är max ett par tiotals procent:

Hur man ökar läshastigheten från HBase upp till 3 gånger och från HDFS upp till 5 gånger

Tester utfördes på 100 filer, vardera 1 GB i storlek och placerade på 10 hårddiskar.

Låt oss räkna ut vad vi i princip kan räkna med vid denna hastighet:
Låt oss säga att vi läser från 10 diskar med en hastighet av 280 MB/sek, d.v.s. 3 miljoner gånger 100 byte. Men som vi minns är den data vi behöver 2600 gånger mindre än vad som läses. Alltså delar vi 3 miljoner med 2600 och får 1100 rekord per sekund.

Deprimerande, eller hur? Det är naturen Slumpmässig tillgång tillgång till data på hårddisken - oavsett blockstorlek. Detta är den fysiska gränsen för slumpmässig åtkomst och ingen databas kan pressa ut mer under sådana förhållanden.

Hur uppnår då databaser mycket högre hastigheter? För att svara på denna fråga, låt oss titta på vad som händer i följande bild:

Hur man ökar läshastigheten från HBase upp till 3 gånger och från HDFS upp till 5 gånger

Här ser vi att under de första minuterna är hastigheten egentligen cirka tusen rekord per sekund. Men vidare, på grund av det faktum att mycket mer läses än vad som efterfrågades, hamnar data i buff/cache i operativsystemet (linux) och hastigheten ökar till mer anständiga 60 tusen per sekund

Därför kommer vi vidare att ta itu med att accelerera åtkomsten endast till data som finns i OS-cachen eller finns i SSD/NVMe-lagringsenheter med jämförbar åtkomsthastighet.

I vårt fall kommer vi att utföra tester på en bänk med fyra servrar, som var och en debiteras enligt följande:

CPU: Xeon E5-2680 v4 @ 2.40GHz 64 trådar.
Minne: 730 GB.
java version: 1.8.0_111

Och här är nyckeln mängden data i tabellerna som behöver läsas. Faktum är att om du läser data från en tabell som är helt placerad i HBase-cachen, kommer det inte ens att komma till läsning från operativsystemets buff/cache. Eftersom HBase som standard allokerar 40% av minnet till en struktur som kallas BlockCache. I huvudsak är detta en ConcurrentHashMap, där nyckeln är filnamnet + förskjutning av blocket, och värdet är den faktiska data vid denna förskjutning.

Således, när vi bara läser från denna struktur, vi vi ser utmärkt fart, som en miljon förfrågningar per sekund. Men låt oss föreställa oss att vi inte kan allokera hundratals gigabyte minne bara för databasbehov, eftersom det finns många andra användbara saker som körs på dessa servrar.

Till exempel, i vårt fall är volymen BlockCache på en RS cirka 12 GB. Vi landade två RS på en nod, d.v.s. 96 GB tilldelas för BlockCache på alla noder. Och det finns många gånger mer data, låt det till exempel vara 4 tabeller, 130 regioner vardera, där filerna är 800 MB stora, komprimerade av FAST_DIFF, dvs. totalt 410 GB (detta är ren data, d.v.s. utan hänsyn till replikeringsfaktorn).

BlockCache är alltså bara cirka 23% av den totala datavolymen och detta är mycket närmare de verkliga förhållandena för det som kallas BigData. Och det är här det roliga börjar - för uppenbarligen, ju färre cacheträffar, desto sämre prestanda. När allt kommer omkring, om du missar, kommer du att behöva göra mycket arbete - d.v.s. gå ner till anropssystemets funktioner. Detta kan dock inte undvikas, så låt oss titta på en helt annan aspekt - vad händer med datan inuti cachen?

Låt oss förenkla situationen och anta att vi har en cache som bara passar 1 objekt. Här är ett exempel på vad som kommer att hända när vi försöker arbeta med en datavolym som är 3 gånger större än cachen, vi måste:

1. Placera block 1 i cachen
2. Ta bort block 1 från cachen
3. Placera block 2 i cachen
4. Ta bort block 2 från cachen
5. Placera block 3 i cachen

5 åtgärder slutförda! Den här situationen kan dock inte kallas normal, i själva verket tvingar vi HBase att göra ett gäng helt värdelöst arbete. Den läser hela tiden data från OS-cachen, placerar den i BlockCache, bara för att kasta ut den nästan omedelbart eftersom en ny del av data har anlänt. Animationen i början av inlägget visar kärnan i problemet – Garbage Collector går ur skala, atmosfären hettar upp, lilla Greta i det avlägsna och varma Sverige blir upprörd. Och vi IT-folk gillar verkligen inte när barn är ledsna, så vi börjar fundera på vad vi kan göra åt det.

Vad händer om du inte lägger alla block i cachen, utan bara en viss procent av dem, så att cachen inte svämmar över? Låt oss börja med att helt enkelt lägga till några rader kod i början av funktionen för att lägga in data i BlockCache:

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

Poängen här är följande: offset är positionen för blocket i filen och dess sista siffror är slumpmässigt och jämnt fördelade från 00 till 99. Därför kommer vi bara att hoppa över de som faller inom det intervall vi behöver.

Ange till exempel cacheDataBlockPercent = 20 och se vad som händer:

Hur man ökar läshastigheten från HBase upp till 3 gånger och från HDFS upp till 5 gånger

Resultatet är uppenbart. I graferna nedan blir det tydligt varför en sådan acceleration inträffade - vi sparar en hel del GC-resurser utan att göra det sisyfiska arbetet med att placera data i cachen bara för att omedelbart kasta ner det i avloppet för marshundarna:

Hur man ökar läshastigheten från HBase upp till 3 gånger och från HDFS upp till 5 gånger

Samtidigt ökar CPU-användningen, men är mycket mindre än produktiviteten:

Hur man ökar läshastigheten från HBase upp till 3 gånger och från HDFS upp till 5 gånger

Det är också värt att notera att blocken som lagras i BlockCache är olika. Det mesta, cirka 95 %, är själva data. Och resten är metadata, som Bloom-filter eller LEAF_INDEX och т.д.. Denna data räcker inte, men den är väldigt användbar, för innan HBase får tillgång till datan direkt, vänder sig HBase till metan för att förstå om det är nödvändigt att söka här ytterligare och, i så fall, var exakt det intressanta blocket finns.

Därför ser vi ett kontrollvillkor i koden buf.getBlockType().isData() och tack vare denna meta kommer vi att lämna den i cachen i alla fall.

Låt oss nu öka belastningen och dra åt funktionen något på en gång. I det första testet gjorde vi cutoff-procenten = 20 och BlockCache var något underutnyttjad. Låt oss nu ställa in den till 23 % och lägga till 100 trådar var 5:e minut för att se vid vilken tidpunkt mättnad inträffar:

Hur man ökar läshastigheten från HBase upp till 3 gånger och från HDFS upp till 5 gånger

Här ser vi att originalversionen nästan omedelbart slår i taket med cirka 100 tusen förfrågningar per sekund. Medan patchen ger en acceleration på upp till 300 tusen. Samtidigt är det tydligt att ytterligare acceleration inte längre är så "gratis", CPU-utnyttjandet ökar också.

Detta är dock inte en särskilt elegant lösning, eftersom vi inte i förväg vet hur stor andel av blocken som behöver cachelagras, det beror på belastningsprofilen. Därför implementerades en mekanism för att automatiskt justera denna parameter beroende på aktiviteten av läsoperationer.

Tre alternativ har lagts till för att kontrollera detta:

hbase.lru.cache.heavy.eviction.count.limit — ställer in hur många gånger processen att avhysa data från cachen ska köras innan vi börjar använda optimering (dvs. hoppa över block). Som standard är det lika med MAX_INT = 2147483647 och betyder faktiskt att funktionen aldrig kommer att börja arbeta med detta värde. Eftersom vräkningsprocessen startar var 5 - 10:e sekund (det beror på belastningen) och 2147483647 * 10 / 60 / 60 / 24 / 365 = 680 år. Vi kan dock ställa in den här parametern till 0 och få funktionen att fungera direkt efter lanseringen.

Det finns dock också en nyttolast i denna parameter. Om vår belastning är sådan att korttidsavläsningar (säg under dagen) och långtidsavläsningar (på natten) ständigt varvas, då kan vi se till att funktionen är aktiverad endast när långa läsoperationer pågår.

Vi vet till exempel att korttidsavläsningar vanligtvis varar cirka 1 minut. Det finns ingen anledning att börja kasta ut block, cachen kommer inte att hinna bli föråldrad och då kan vi ställa in denna parameter lika med till exempel 10. Detta kommer att leda till att optimeringen kommer att börja fungera först när lång- terminsaktiv läsning har påbörjats, d.v.s. på 100 sekunder. Således, om vi har en korttidsläsning, kommer alla block att gå in i cachen och vara tillgängliga (förutom de som kommer att vräkas av standardalgoritmen). Och när vi gör långtidsläsningar är funktionen påslagen och vi skulle ha mycket högre prestanda.

hbase.lru.cache.heavy.eviction.mb.size.limit — ställer in hur många megabyte vi vill placera i cachen (och, naturligtvis, vräka) på 10 sekunder. Funktionen kommer att försöka nå detta värde och behålla det. Poängen är denna: om vi stoppar in gigabyte i cachen, måste vi vräka gigabyte, och detta, som vi såg ovan, är väldigt dyrt. Du bör dock inte försöka ställa in den för liten, eftersom detta kommer att göra att blockhoppningsläget avslutas i förtid. För kraftfulla servrar (ca 20-40 fysiska kärnor) är det optimalt att ställa in ca 300-400 MB. För medelklassen (~10 kärnor) 200-300 MB. För svaga system (2-5 kärnor) kan 50-100 MB vara normalt (ej testat på dessa).

Låt oss titta på hur detta fungerar: låt oss säga att vi ställer in hbase.lru.cache.heavy.eviction.mb.size.limit = 500, det finns någon form av belastning (läsning) och sedan beräknar vi var ~10:e sekund hur många byte som var vräkt med formeln:

Overhead = Frigjorda byte Summa (MB) * 100 / Limit (MB) - 100;

Om 2000 MB faktiskt vräkts är Overhead lika med:

2000 * 100 / 500 - 100 = 300 %

Algoritmerna försöker inte upprätthålla mer än några tiotals procent, så funktionen kommer att minska andelen cachade block och därigenom implementera en automatisk inställningsmekanism.

Men om belastningen sjunker, låt oss säga att bara 200 MB vräkas och Overhead blir negativ (den så kallade överskjutningen):

200 * 100 / 500 - 100 = -60 %

Tvärtom kommer funktionen att öka andelen cachade block tills Overhead blir positivt.

Nedan är ett exempel på hur detta ser ut på verkliga data. Det finns ingen anledning att försöka nå 0%, det är omöjligt. Det är mycket bra när det är cirka 30 - 100%, detta hjälper till att undvika för tidig utgång från optimeringsläget under kortvariga överspänningar.

hbase.lru.cache.heavy.eviction.overhead.koefficient — anger hur snabbt vi vill få resultatet. Om vi ​​med säkerhet vet att våra läsningar för det mesta är långa och inte vill vänta, kan vi öka detta förhållande och få hög prestanda snabbare.

Till exempel sätter vi denna koefficient = 0.01. Detta innebär att Overhead (se ovan) kommer att multipliceras med detta tal med det resulterande resultatet och andelen cachade block kommer att minskas. Låt oss anta att Overhead = 300% och koefficient = 0.01, då kommer andelen cachade block att minska med 3%.

En liknande "mottrycks"-logik är också implementerad för negativa Overhead-värden (överskridande). Eftersom kortsiktiga fluktuationer i volymen av läsningar och vräkningar alltid är möjliga, tillåter denna mekanism dig att undvika för tidig utgång från optimeringsläget. Mottryck har en inverterad logik: ju starkare överskridandet är, desto fler block cachelagras.

Hur man ökar läshastigheten från HBase upp till 3 gånger och från HDFS upp till 5 gånger

Implementeringskod

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

Låt oss nu titta på allt detta med ett verkligt exempel. Vi har följande testskript:

  1. Låt oss börja göra Scan (25 trådar, batch = 100)
  2. Efter 5 minuter, lägg till multi-gets (25 trådar, batch = 100)
  3. Efter 5 minuter, stäng av multi-gets (endast skanningen återstår)

Vi kör två körningar, först hbase.lru.cache.heavy.eviction.count.limit = 10000 0 (vilket faktiskt inaktiverar funktionen), och sedan sätter vi limit = XNUMX (aktiverar den).

I loggarna nedan ser vi hur funktionen är påslagen och återställer Overshooting till 14-71%. Då och då minskar belastningen, vilket sätter på mottryck och HBase cachar fler block igen.

Logga RegionServer
vräkt (MB): 0, förhållande 0.0, overhead (%): -100, tung vräkningsräknare: 0, aktuell cachning DataBlock (%): 100
vräkt (MB): 0, förhållande 0.0, overhead (%): -100, tung vräkningsräknare: 0, aktuell cachning DataBlock (%): 100
vräkt (MB): 2170, förhållande 1.09, overhead (%): 985, tung vräkningsräknare: 1, aktuell cachning DataBlock (%): 91 < start
vräkt (MB): 3763, förhållande 1.08, overhead (%): 1781, tung vräkningsräknare: 2, nuvarande cachning DataBlock (%): 76
vräkt (MB): 3306, förhållande 1.07, overhead (%): 1553, tung vräkningsräknare: 3, nuvarande cachning DataBlock (%): 61
vräkt (MB): 2508, förhållande 1.06, overhead (%): 1154, tung vräkningsräknare: 4, nuvarande cachning DataBlock (%): 50
vräkt (MB): 1824, förhållande 1.04, overhead (%): 812, tung vräkningsräknare: 5, nuvarande cachning DataBlock (%): 42
vräkt (MB): 1482, förhållande 1.03, overhead (%): 641, tung vräkningsräknare: 6, nuvarande cachning DataBlock (%): 36
vräkt (MB): 1140, förhållande 1.01, overhead (%): 470, tung vräkningsräknare: 7, nuvarande cachning DataBlock (%): 32
vräkt (MB): 913, förhållande 1.0, overhead (%): 356, tung vräkningsräknare: 8, nuvarande cachning DataBlock (%): 29
vräkt (MB): 912, förhållande 0.89, overhead (%): 356, tung vräkningsräknare: 9, nuvarande cachning DataBlock (%): 26
vräkt (MB): 684, förhållande 0.76, overhead (%): 242, tung vräkningsräknare: 10, nuvarande cachning DataBlock (%): 24
vräkt (MB): 684, förhållande 0.61, overhead (%): 242, tung vräkningsräknare: 11, nuvarande cachning DataBlock (%): 22
vräkt (MB): 456, förhållande 0.51, overhead (%): 128, tung vräkningsräknare: 12, nuvarande cachning DataBlock (%): 21
vräkt (MB): 456, förhållande 0.42, overhead (%): 128, tung vräkningsräknare: 13, nuvarande cachning DataBlock (%): 20
vräkt (MB): 456, förhållande 0.33, overhead (%): 128, tung vräkningsräknare: 14, nuvarande cachning DataBlock (%): 19
vräkt (MB): 342, förhållande 0.33, overhead (%): 71, tung vräkningsräknare: 15, nuvarande cachning DataBlock (%): 19
vräkt (MB): 342, förhållande 0.32, overhead (%): 71, tung vräkningsräknare: 16, nuvarande cachning DataBlock (%): 19
vräkt (MB): 342, förhållande 0.31, overhead (%): 71, tung vräkningsräknare: 17, nuvarande cachning DataBlock (%): 19
vräkt (MB): 228, förhållande 0.3, overhead (%): 14, tung vräkningsräknare: 18, nuvarande cachning DataBlock (%): 19
vräkt (MB): 228, förhållande 0.29, overhead (%): 14, tung vräkningsräknare: 19, nuvarande cachning DataBlock (%): 19
vräkt (MB): 228, förhållande 0.27, overhead (%): 14, tung vräkningsräknare: 20, nuvarande cachning DataBlock (%): 19
vräkt (MB): 228, förhållande 0.25, overhead (%): 14, tung vräkningsräknare: 21, nuvarande cachning DataBlock (%): 19
vräkt (MB): 228, förhållande 0.24, overhead (%): 14, tung vräkningsräknare: 22, nuvarande cachning DataBlock (%): 19
vräkt (MB): 228, förhållande 0.22, overhead (%): 14, tung vräkningsräknare: 23, nuvarande cachning DataBlock (%): 19
vräkt (MB): 228, förhållande 0.21, overhead (%): 14, tung vräkningsräknare: 24, nuvarande cachning DataBlock (%): 19
vräkt (MB): 228, förhållande 0.2, overhead (%): 14, tung vräkningsräknare: 25, nuvarande cachning DataBlock (%): 19
vräkt (MB): 228, förhållande 0.17, overhead (%): 14, tung vräkningsräknare: 26, nuvarande cachning DataBlock (%): 19
vräkt (MB): 456, förhållande 0.17, overhead (%): 128, tung vräkningsräknare: 27, nuvarande cachning DataBlock (%): 18 < tillagda får (men tabell samma)
vräkt (MB): 456, förhållande 0.15, overhead (%): 128, tung vräkningsräknare: 28, nuvarande cachning DataBlock (%): 17
vräkt (MB): 342, förhållande 0.13, overhead (%): 71, tung vräkningsräknare: 29, nuvarande cachning DataBlock (%): 17
vräkt (MB): 342, förhållande 0.11, overhead (%): 71, tung vräkningsräknare: 30, nuvarande cachning DataBlock (%): 17
vräkt (MB): 342, förhållande 0.09, overhead (%): 71, tung vräkningsräknare: 31, nuvarande cachning DataBlock (%): 17
vräkt (MB): 228, förhållande 0.08, overhead (%): 14, tung vräkningsräknare: 32, nuvarande cachning DataBlock (%): 17
vräkt (MB): 228, förhållande 0.07, overhead (%): 14, tung vräkningsräknare: 33, nuvarande cachning DataBlock (%): 17
vräkt (MB): 228, förhållande 0.06, overhead (%): 14, tung vräkningsräknare: 34, nuvarande cachning DataBlock (%): 17
vräkt (MB): 228, förhållande 0.05, overhead (%): 14, tung vräkningsräknare: 35, nuvarande cachning DataBlock (%): 17
vräkt (MB): 228, förhållande 0.05, overhead (%): 14, tung vräkningsräknare: 36, nuvarande cachning DataBlock (%): 17
vräkt (MB): 228, förhållande 0.04, overhead (%): 14, tung vräkningsräknare: 37, nuvarande cachning DataBlock (%): 17
vräkt (MB): 109, förhållande 0.04, overhead (%): -46, tung vräkningsräknare: 37, aktuell cachning DataBlock (%): 22 < mottryck
vräkt (MB): 798, förhållande 0.24, overhead (%): 299, tung vräkningsräknare: 38, nuvarande cachning DataBlock (%): 20
vräkt (MB): 798, förhållande 0.29, overhead (%): 299, tung vräkningsräknare: 39, nuvarande cachning DataBlock (%): 18
vräkt (MB): 570, förhållande 0.27, overhead (%): 185, tung vräkningsräknare: 40, nuvarande cachning DataBlock (%): 17
vräkt (MB): 456, förhållande 0.22, overhead (%): 128, tung vräkningsräknare: 41, nuvarande cachning DataBlock (%): 16
vräkt (MB): 342, förhållande 0.16, overhead (%): 71, tung vräkningsräknare: 42, nuvarande cachning DataBlock (%): 16
vräkt (MB): 342, förhållande 0.11, overhead (%): 71, tung vräkningsräknare: 43, nuvarande cachning DataBlock (%): 16
vräkt (MB): 228, förhållande 0.09, overhead (%): 14, tung vräkningsräknare: 44, nuvarande cachning DataBlock (%): 16
vräkt (MB): 228, förhållande 0.07, overhead (%): 14, tung vräkningsräknare: 45, nuvarande cachning DataBlock (%): 16
vräkt (MB): 228, förhållande 0.05, overhead (%): 14, tung vräkningsräknare: 46, nuvarande cachning DataBlock (%): 16
vräkt (MB): 222, förhållande 0.04, overhead (%): 11, tung vräkningsräknare: 47, nuvarande cachning DataBlock (%): 16
vräkt (MB): 104, förhållande 0.03, overhead (%): -48, tung vräkningsräknare: 47, aktuell cachning DataBlock (%): 21 < avbrott får
vräkt (MB): 684, förhållande 0.2, overhead (%): 242, tung vräkningsräknare: 48, nuvarande cachning DataBlock (%): 19
vräkt (MB): 570, förhållande 0.23, overhead (%): 185, tung vräkningsräknare: 49, nuvarande cachning DataBlock (%): 18
vräkt (MB): 342, förhållande 0.22, overhead (%): 71, tung vräkningsräknare: 50, nuvarande cachning DataBlock (%): 18
vräkt (MB): 228, förhållande 0.21, overhead (%): 14, tung vräkningsräknare: 51, nuvarande cachning DataBlock (%): 18
vräkt (MB): 228, förhållande 0.2, overhead (%): 14, tung vräkningsräknare: 52, nuvarande cachning DataBlock (%): 18
vräkt (MB): 228, förhållande 0.18, overhead (%): 14, tung vräkningsräknare: 53, nuvarande cachning DataBlock (%): 18
vräkt (MB): 228, förhållande 0.16, overhead (%): 14, tung vräkningsräknare: 54, nuvarande cachning DataBlock (%): 18
vräkt (MB): 228, förhållande 0.14, overhead (%): 14, tung vräkningsräknare: 55, nuvarande cachning DataBlock (%): 18
vräkt (MB): 112, förhållande 0.14, overhead (%): -44, tung vräkningsräknare: 55, aktuell cachning DataBlock (%): 23 < mottryck
vräkt (MB): 456, förhållande 0.26, overhead (%): 128, tung vräkningsräknare: 56, nuvarande cachning DataBlock (%): 22
vräkt (MB): 342, förhållande 0.31, overhead (%): 71, tung vräkningsräknare: 57, nuvarande cachning DataBlock (%): 22
vräkt (MB): 342, förhållande 0.33, overhead (%): 71, tung vräkningsräknare: 58, nuvarande cachning DataBlock (%): 22
vräkt (MB): 342, förhållande 0.33, overhead (%): 71, tung vräkningsräknare: 59, nuvarande cachning DataBlock (%): 22
vräkt (MB): 342, förhållande 0.33, overhead (%): 71, tung vräkningsräknare: 60, nuvarande cachning DataBlock (%): 22
vräkt (MB): 342, förhållande 0.33, overhead (%): 71, tung vräkningsräknare: 61, nuvarande cachning DataBlock (%): 22
vräkt (MB): 342, förhållande 0.33, overhead (%): 71, tung vräkningsräknare: 62, nuvarande cachning DataBlock (%): 22
vräkt (MB): 342, förhållande 0.33, overhead (%): 71, tung vräkningsräknare: 63, nuvarande cachning DataBlock (%): 22
vräkt (MB): 342, förhållande 0.32, overhead (%): 71, tung vräkningsräknare: 64, nuvarande cachning DataBlock (%): 22
vräkt (MB): 342, förhållande 0.33, overhead (%): 71, tung vräkningsräknare: 65, nuvarande cachning DataBlock (%): 22
vräkt (MB): 342, förhållande 0.33, overhead (%): 71, tung vräkningsräknare: 66, nuvarande cachning DataBlock (%): 22
vräkt (MB): 342, förhållande 0.32, overhead (%): 71, tung vräkningsräknare: 67, nuvarande cachning DataBlock (%): 22
vräkt (MB): 342, förhållande 0.33, overhead (%): 71, tung vräkningsräknare: 68, nuvarande cachning DataBlock (%): 22
vräkt (MB): 342, förhållande 0.32, overhead (%): 71, tung vräkningsräknare: 69, nuvarande cachning DataBlock (%): 22
vräkt (MB): 342, förhållande 0.32, overhead (%): 71, tung vräkningsräknare: 70, nuvarande cachning DataBlock (%): 22
vräkt (MB): 342, förhållande 0.33, overhead (%): 71, tung vräkningsräknare: 71, nuvarande cachning DataBlock (%): 22
vräkt (MB): 342, förhållande 0.33, overhead (%): 71, tung vräkningsräknare: 72, nuvarande cachning DataBlock (%): 22
vräkt (MB): 342, förhållande 0.33, overhead (%): 71, tung vräkningsräknare: 73, nuvarande cachning DataBlock (%): 22
vräkt (MB): 342, förhållande 0.33, overhead (%): 71, tung vräkningsräknare: 74, nuvarande cachning DataBlock (%): 22
vräkt (MB): 342, förhållande 0.33, overhead (%): 71, tung vräkningsräknare: 75, nuvarande cachning DataBlock (%): 22
vräkt (MB): 342, förhållande 0.33, overhead (%): 71, tung vräkningsräknare: 76, nuvarande cachning DataBlock (%): 22
vräkt (MB): 21, förhållande 0.33, overhead (%): -90, tung vräkningsräknare: 76, aktuell cachning DataBlock (%): 32
vräkt (MB): 0, förhållande 0.0, overhead (%): -100, tung vräkningsräknare: 0, aktuell cachning DataBlock (%): 100
vräkt (MB): 0, förhållande 0.0, overhead (%): -100, tung vräkningsräknare: 0, aktuell cachning DataBlock (%): 100

Skanningarna behövdes för att visa samma process i form av en graf över förhållandet mellan två cache-sektioner - singel (där block som aldrig har begärts tidigare) och multi (data "begärd" minst en gång lagras här):

Hur man ökar läshastigheten från HBase upp till 3 gånger och från HDFS upp till 5 gånger

Och slutligen, hur ser operationen av parametrarna ut i form av en graf. Som jämförelse var cachen helt avstängd i början, sedan lanserades HBase med cachning och fördröjde starten av optimeringsarbetet med 5 minuter (30 vräkningscykler).

Fullständig kod finns i Pull Request HBASE 23887 på github.

Men 300 tusen läsningar per sekund är inte allt som kan uppnås på denna hårdvara under dessa förhållanden. Faktum är att när du behöver komma åt data via HDFS, används ShortCircuitCache (nedan kallad SSC) mekanismen, som gör att du kan komma åt data direkt och undvika nätverksinteraktioner.

Profilering visade att även om denna mekanism ger en stor vinst så blir den också någon gång en flaskhals, eftersom nästan alla tunga operationer sker inuti ett lås, vilket leder till blockering för det mesta.

Hur man ökar läshastigheten från HBase upp till 3 gånger och från HDFS upp till 5 gånger

Efter att ha insett detta insåg vi att problemet kan kringgås genom att skapa en rad oberoende SSC:er:

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

Och arbeta sedan med dem, exklusive korsningar även vid den sista offsetsiffran:

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

Nu kan du börja testa. För att göra detta kommer vi att läsa filer från HDFS med en enkel flertrådad applikation. Ställ in parametrarna:

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

Och läs bara filerna:

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

Denna kod exekveras i separata trådar och vi kommer att öka antalet samtidigt lästa filer (från 10 till 200 - horisontell axel) och antalet cacher (från 1 till 10 - grafik). Den vertikala axeln visar accelerationen som är resultatet av en ökning av SSC i förhållande till fallet när det bara finns en cache.

Hur man ökar läshastigheten från HBase upp till 3 gånger och från HDFS upp till 5 gånger

Så här läser du grafen: Exekveringstiden för 100 tusen läsningar i 64 KB-block med en cache kräver 78 sekunder. Medan det tar 5 sekunder med 16 cacher. De där. det finns en acceleration på ~5 gånger. Som framgår av grafen är effekten inte särskilt märkbar för ett litet antal parallella läsningar, den börjar spela en märkbar roll när det finns fler än 50 trådläsningar. Det märks också att man ökar antalet SSC från 6 och ovan ger en betydligt mindre prestandaökning.

Anmärkning 1: eftersom testresultaten är ganska flyktiga (se nedan), genomfördes 3 körningar och medelvärdena beräknades.

Note 2: Prestandavinsten från att konfigurera direktåtkomst är densamma, även om åtkomsten i sig är något långsammare.

Det är dock nödvändigt att klargöra att, till skillnad från fallet med HBase, är denna acceleration inte alltid gratis. Här "låser" vi upp CPU:s förmåga att arbeta mer, istället för att hänga på lås.

Hur man ökar läshastigheten från HBase upp till 3 gånger och från HDFS upp till 5 gånger

Här kan du observera att en ökning av antalet cachar generellt ger en ungefär proportionell ökning av CPU-utnyttjandet. Det finns dock lite fler vinnande kombinationer.

Låt oss till exempel titta närmare på inställningen SSC = 3. Ökningen av prestanda på intervallet är cirka 3.3 gånger. Nedan är resultaten från alla tre separata körningar.

Hur man ökar läshastigheten från HBase upp till 3 gånger och från HDFS upp till 5 gånger

Medan CPU-förbrukningen ökar med cirka 2.8 gånger. Skillnaden är inte särskilt stor, men lilla Greta är redan nöjd och kanske hinner gå i skolan och ta lektioner.

Detta kommer alltså att ha en positiv effekt för alla verktyg som använder bulkåtkomst till HDFS (till exempel Spark, etc.), förutsatt att applikationskoden är lätt (dvs. kontakten är på HDFS-klienten) och det finns ledig CPU-kraft . För att kontrollera, låt oss testa vilken effekt den kombinerade användningen av BlockCache-optimering och SSC-inställning för läsning från HBase kommer att ha.

Hur man ökar läshastigheten från HBase upp till 3 gånger och från HDFS upp till 5 gånger

Man kan se att under sådana förhållanden är effekten inte lika stor som i förfinade tester (avläsning utan någon bearbetning), men det är fullt möjligt att pressa ut ytterligare 80K här. Tillsammans ger båda optimeringarna upp till 4x snabbhet.

En PR gjordes också för denna optimering [HDFS-15202], som har slagits samman och den här funktionen kommer att finnas tillgänglig i framtida utgåvor.

Och slutligen var det intressant att jämföra läsprestandan för en liknande databas med breda spalter, Cassandra och HBase.

För att göra detta lanserade vi instanser av standardverktyget YCSB belastningstestning från två värdar (totalt 800 trådar). På serversidan - 4 instanser av RegionServer och Cassandra på 4 värdar (inte de där klienterna körs, för att undvika deras inflytande). Avläsningar kom från tabeller av storlek:

HBase – 300 GB på HDFS (100 GB ren data)

Cassandra - 250 GB (replikeringsfaktor = 3)

De där. volymen var ungefär densamma (i HBase lite mer).

HBase parametrar:

dfs.client.short.circuit.num = 5 (HDFS-klientoptimering)

hbase.lru.cache.heavy.eviction.count.limit = 30 - detta betyder att plåstret kommer att börja fungera efter 30 vräkningar (~5 minuter)

hbase.lru.cache.heavy.eviction.mb.size.limit = 300 — Målvolym för cachning och vräkning

YCSB-loggar analyserades och kompilerades till Excel-diagram:

Hur man ökar läshastigheten från HBase upp till 3 gånger och från HDFS upp till 5 gånger

Som du kan se gör dessa optimeringar det möjligt att jämföra prestandan för dessa databaser under dessa förhållanden och uppnå 450 tusen läsningar per sekund.

Vi hoppas att denna information kan vara användbar för någon under den spännande kampen för produktivitet.

Källa: will.com

Lägg en kommentar