Come aumentare la velocità di lettura da HBase fino a 3 volte e da HDFS fino a 5 volte

Le prestazioni elevate sono uno dei requisiti chiave quando si lavora con i big data. Nel reparto caricamento dati di Sberbank, pompiamo quasi tutte le transazioni nel nostro Data Cloud basato su Hadoop e quindi gestiamo flussi di informazioni molto grandi. Naturalmente siamo sempre alla ricerca di modi per migliorare le prestazioni e ora vogliamo raccontarvi come siamo riusciti ad applicare le patch a RegionServer HBase e al client HDFS, grazie ai quali siamo riusciti ad aumentare notevolmente la velocità delle operazioni di lettura.
Come aumentare la velocità di lettura da HBase fino a 3 volte e da HDFS fino a 5 volte

Tuttavia, prima di passare all'essenza dei miglioramenti, vale la pena parlare delle restrizioni che, in linea di principio, non possono essere aggirate se ci si siede su un HDD.

Perché l'HDD e le letture veloci ad accesso casuale sono incompatibili
Come sai, HBase e molti altri database memorizzano i dati in blocchi di diverse decine di kilobyte. Per impostazione predefinita è circa 64 KB. Ora immaginiamo di dover ottenere solo 100 byte e chiediamo a HBase di fornirci questi dati utilizzando una determinata chiave. Poiché la dimensione del blocco in HFiles è 64 KB, la richiesta sarà 640 volte più grande (solo un minuto!) del necessario.

Successivamente, poiché la richiesta passerà attraverso HDFS e il suo meccanismo di memorizzazione nella cache dei metadati ShortCircuitCache (che consente l'accesso diretto ai file), questo porta a leggere già 1 MB dal disco. Tuttavia, questo può essere regolato con il parametro dfs.client.read.shortcircuit.buffer.size e in molti casi ha senso ridurre questo valore, ad esempio a 126 KB.

Diciamo di farlo, ma in più, quando iniziamo a leggere i dati tramite l'API Java, come funzioni come FileChannel.read e chiediamo al sistema operativo di leggere la quantità di dati specificata, legge "per ogni evenienza" 2 volte di più , cioè. 256 KB nel nostro caso. Questo perché Java non dispone di un modo semplice per impostare il flag FADV_RANDOM per impedire questo comportamento.

Di conseguenza, per ottenere i nostri 100 byte, ne vengono letti 2600 volte di più. Sembrerebbe che la soluzione sia ovvia, riduciamo la dimensione del blocco a un kilobyte, impostiamo il flag menzionato e otteniamo una grande accelerazione dell'illuminazione. Ma il problema è che riducendo la dimensione del blocco di 2 volte, riduciamo anche il numero di byte letti per unità di tempo di 2 volte.

Si può ottenere un certo guadagno dall'impostazione del flag FADV_RANDOM, ma solo con multithreading elevato e con una dimensione del blocco di 128 KB, ma questo è un massimo di un paio di decine di percentuali:

Come aumentare la velocità di lettura da HBase fino a 3 volte e da HDFS fino a 5 volte

I test sono stati effettuati su 100 file, ciascuno della dimensione di 1 GB e posizionati su 10 HDD.

Calcoliamo su cosa possiamo, in linea di principio, contare a questa velocità:
Diciamo che leggiamo da 10 dischi ad una velocità di 280 MB/sec, cioè 3 milioni di volte 100 byte. Ma come ricordiamo, i dati di cui abbiamo bisogno sono 2600 volte inferiori a quelli letti. Pertanto, dividiamo 3 milioni per 2600 e otteniamo 1100 registrazioni al secondo.

Deprimente, non è vero? Questa è la natura Accesso casuale accesso ai dati sull'HDD, indipendentemente dalla dimensione del blocco. Questo è il limite fisico dell'accesso casuale e nessun database può spremerne di più in tali condizioni.

In che modo allora i database raggiungono velocità molto più elevate? Per rispondere a questa domanda, diamo un'occhiata a cosa sta succedendo nella seguente immagine:

Come aumentare la velocità di lettura da HBase fino a 3 volte e da HDFS fino a 5 volte

Qui vediamo che per i primi minuti la velocità è veramente di circa mille registrazioni al secondo. Inoltre, poiché viene letto molto di più di quanto richiesto, i dati finiscono nel buff/cache del sistema operativo (linux) e la velocità aumenta fino a un più dignitoso 60mila al secondo

Pertanto, ci occuperemo ulteriormente di accelerare l'accesso solo ai dati che si trovano nella cache del sistema operativo o che si trovano in dispositivi di archiviazione SSD/NVMe con velocità di accesso comparabile.

Nel nostro caso effettueremo i test su un banco di 4 server, ciascuno dei quali verrà addebitato come segue:

CPU: Xeon E5-2680 v4 a 2.40 GHz 64 thread.
Memoria: 730 GB.
versione Java: 1.8.0_111

E qui il punto chiave è la quantità di dati nelle tabelle che devono essere letti. Il fatto è che se leggi i dati da una tabella che è interamente posizionata nella cache HBase, non arriverà nemmeno a leggere dal buff/cache del sistema operativo. Perché HBase per impostazione predefinita alloca il 40% della memoria a una struttura chiamata BlockCache. Essenzialmente si tratta di una ConcurrentHashMap, dove la chiave è il nome del file + offset del blocco e il valore sono i dati effettivi a questo offset.

Pertanto, leggendo solo da questa struttura, noi vediamo un'eccellente velocità, come un milione di richieste al secondo. Ma immaginiamo di non poter allocare centinaia di gigabyte di memoria solo per le esigenze del database, perché su questi server sono in esecuzione molte altre cose utili.

Ad esempio, nel nostro caso, il volume di BlockCache su una RS è di circa 12 GB. Abbiamo atterrato due RS su un nodo, cioè 96 GB sono allocati per BlockCache su tutti i nodi. E ci sono molte volte più dati, ad esempio, lascia che siano 4 tabelle, 130 regioni ciascuna, in cui i file hanno una dimensione di 800 MB, compressi da FAST_DIFF, ad es. un totale di 410 GB (si tratta di dati puri, cioè senza tenere conto del fattore di replica).

BlockCache rappresenta quindi solo il 23% circa del volume totale di dati ed è molto più vicino alle condizioni reali di ciò che viene chiamato BigData. Ed è qui che inizia il divertimento, perché ovviamente, minore è il numero di accessi alla cache, peggiori sono le prestazioni. Dopotutto, se sbagli, dovrai lavorare molto, ad es. vai alla chiamata delle funzioni di sistema. Tuttavia, questo non può essere evitato, quindi consideriamo un aspetto completamente diverso: cosa succede ai dati all'interno della cache?

Semplifichiamo la situazione e assumiamo di avere una cache che può contenere solo 1 oggetto. Ecco un esempio di cosa accadrà quando proveremo a lavorare con un volume di dati 3 volte più grande della cache, dovremo:

1. Posiziona il blocco 1 nella cache
2. Rimuovere il blocco 1 dalla cache
3. Posiziona il blocco 2 nella cache
4. Rimuovere il blocco 2 dalla cache
5. Posiziona il blocco 3 nella cache

5 azioni completate! Tuttavia, questa situazione non può essere definita normale; infatti, stiamo costringendo HBase a fare un sacco di lavoro completamente inutile. Legge costantemente i dati dalla cache del sistema operativo, li inserisce in BlockCache, solo per eliminarli quasi immediatamente perché è arrivata una nuova porzione di dati. L'animazione all'inizio del post mostra l'essenza del problema: Garbage Collector sta andando fuori scala, l'atmosfera si sta surriscaldando, la piccola Greta nella lontana e calda Svezia si sta arrabbiando. E a noi IT non piace davvero quando i bambini sono tristi, quindi iniziamo a pensare a cosa possiamo fare al riguardo.

Cosa succede se non metti tutti i blocchi nella cache, ma solo una certa percentuale di essi, in modo che la cache non trabocchi? Iniziamo semplicemente aggiungendo poche righe di codice all'inizio della funzione per inserire i dati in BlockCache:

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

Il punto qui è il seguente: l'offset è la posizione del blocco nel file e le sue ultime cifre sono distribuite in modo casuale e uniforme da 00 a 99. Pertanto, salteremo solo quelli che rientrano nell'intervallo di cui abbiamo bisogno.

Ad esempio, imposta cacheDataBlockPercent = 20 e guarda cosa succede:

Come aumentare la velocità di lettura da HBase fino a 3 volte e da HDFS fino a 5 volte

Il risultato è ovvio. Nei grafici sottostanti, diventa chiaro il motivo per cui si è verificata una tale accelerazione: risparmiamo molte risorse GC senza fare il lavoro di Sisifo di inserire i dati nella cache solo per buttarli immediatamente nello scarico dei cani marziani:

Come aumentare la velocità di lettura da HBase fino a 3 volte e da HDFS fino a 5 volte

Allo stesso tempo, l’utilizzo della CPU aumenta, ma è molto inferiore alla produttività:

Come aumentare la velocità di lettura da HBase fino a 3 volte e da HDFS fino a 5 volte

Vale anche la pena notare che i blocchi archiviati in BlockCache sono diversi. La maggior parte, circa il 95%, sono dati stessi. E il resto sono metadati, come i filtri Bloom o LEAF_INDEX e т.д.. Questi dati non bastano, ma sono molto utili, perché prima di accedere direttamente ai dati, HBase si rivolge al meta per capire se è necessario cercare ulteriormente qui e, in caso affermativo, dove si trova esattamente il blocco di interesse.

Pertanto nel codice vediamo una condizione di controllo buf.getBlockType().isData() e grazie a questo meta lo lasceremo comunque nella cache.

Ora aumentiamo il carico e rafforziamo leggermente la funzione in una volta sola. Nel primo test abbiamo impostato la percentuale di cutoff = 20 e BlockCache era leggermente sottoutilizzata. Ora impostiamolo al 23% e aggiungiamo 100 thread ogni 5 minuti per vedere a che punto avviene la saturazione:

Come aumentare la velocità di lettura da HBase fino a 3 volte e da HDFS fino a 5 volte

Qui vediamo che la versione originale raggiunge quasi immediatamente il tetto con circa 100mila richieste al secondo. Mentre la patch fornisce un'accelerazione fino a 300mila. Allo stesso tempo è chiaro che un’ulteriore accelerazione non è più così “gratuita”; aumenta anche l’utilizzo della CPU.

Tuttavia, questa non è una soluzione molto elegante, poiché non sappiamo in anticipo quale percentuale di blocchi deve essere memorizzata nella cache, dipende dal profilo di carico. Pertanto, è stato implementato un meccanismo per regolare automaticamente questo parametro in base all'attività delle operazioni di lettura.

Sono state aggiunte tre opzioni per controllarlo:

hbase.lru.cache.heavy.eviction.count.limit — imposta quante volte deve essere eseguito il processo di eliminazione dei dati dalla cache prima di iniziare a utilizzare l'ottimizzazione (ovvero saltare i blocchi). Di default è uguale a MAX_INT = 2147483647 e di fatto significa che la funzionalità non inizierà mai a funzionare con questo valore. Perché il processo di sfratto inizia ogni 5 - 10 secondi (dipende dal carico) e 2147483647 * 10 / 60 / 60 / 24 / 365 = 680 anni. Possiamo però impostare questo parametro su 0 e far funzionare la funzionalità subito dopo il lancio.

Tuttavia, c'è anche un carico utile in questo parametro. Se il nostro carico è tale che le letture a breve termine (ad esempio durante il giorno) e le letture a lungo termine (di notte) sono costantemente intervallate, allora possiamo assicurarci che la funzionalità sia attivata solo quando sono in corso operazioni di lettura a lungo termine.

Ad esempio, sappiamo che le letture a breve termine durano solitamente circa 1 minuto. Non è necessario iniziare a eliminare blocchi, la cache non avrà il tempo di diventare obsoleta e quindi possiamo impostare questo parametro uguale, ad esempio, a 10. Ciò porterà al fatto che l'ottimizzazione inizierà a funzionare solo quando è iniziata la lettura attiva del semestre, vale a dire tra 100 secondi. Pertanto, se abbiamo una lettura a breve termine, tutti i blocchi andranno nella cache e saranno disponibili (ad eccezione di quelli che verranno eliminati dall'algoritmo standard). E quando eseguiamo letture a lungo termine, la funzionalità è attivata e avremmo prestazioni molto più elevate.

hbase.lru.cache.heavy.eviction.mb.size.limit — imposta quanti megabyte vorremmo inserire nella cache (e, ovviamente, eliminare) in 10 secondi. La funzione proverà a raggiungere questo valore e a mantenerlo. Il punto è questo: se inseriamo gigabyte nella cache, dovremo sfrattare gigabyte e questo, come abbiamo visto sopra, è molto costoso. Tuttavia, non dovresti provare a impostarlo su un valore troppo piccolo, poiché ciò causerebbe l'uscita prematura della modalità di salto del blocco. Per server potenti (circa 20-40 core fisici), è ottimale impostare circa 300-400 MB. Per la classe media (~10 core) 200-300 MB. Per sistemi deboli (2-5 core) 50-100 MB potrebbero essere normali (non testati su questi).

Diamo un'occhiata a come funziona: diciamo che impostiamo hbase.lru.cache.heavy.eviction.mb.size.limit = 500, c'è una sorta di caricamento (lettura) e poi ogni ~ 10 secondi calcoliamo quanti byte sono stati sfrattato utilizzando la formula:

Overhead = Somma byte liberati (MB) * 100 / Limite (MB) - 100;

Se infatti vengono eliminati 2000 MB, il sovraccarico è pari a:

2000 * 100 / 500 - 100 = 300%

Gli algoritmi cercano di mantenere non più di qualche decina di punti percentuali, quindi la funzionalità ridurrà la percentuale di blocchi memorizzati nella cache, implementando così un meccanismo di auto-tuning.

Se però il carico cala, diciamo che vengono eliminati solo 200 MB e l’Overhead diventa negativo (il cosiddetto overshooting):

200 * 100 / 500 - 100 = -60%

Al contrario, la funzionalità aumenterà la percentuale di blocchi memorizzati nella cache finché l'Overhead non diventerà positivo.

Di seguito è riportato un esempio di come appare sui dati reali. Non è necessario cercare di raggiungere lo 0%, è impossibile. È molto buono quando è intorno al 30 - 100%, questo aiuta ad evitare l'uscita prematura dalla modalità di ottimizzazione durante i picchi a breve termine.

hbase.lru.cache.heavy.eviction.overheadcoefficient — imposta la velocità con cui vorremmo ottenere il risultato. Se sappiamo per certo che le nostre letture sono per lo più lunghe e non vogliamo aspettare, possiamo aumentare questo rapporto e ottenere prestazioni elevate più velocemente.

Ad esempio, impostiamo questo coefficiente = 0.01. Ciò significa che il sovraccarico (vedi sopra) verrà moltiplicato per questo numero in base al risultato risultante e la percentuale di blocchi memorizzati nella cache verrà ridotta. Supponiamo che Overhead = 300% e coefficiente = 0.01, quindi la percentuale di blocchi memorizzati nella cache verrà ridotta del 3%.

Una logica simile di “contropressione” è implementata anche per valori negativi di Overhead (overshooting). Poiché sono sempre possibili fluttuazioni a breve termine nel volume delle letture e delle eliminazioni, questo meccanismo consente di evitare l'uscita prematura dalla modalità di ottimizzazione. La contropressione ha una logica invertita: più forte è il superamento, più blocchi vengono memorizzati nella cache.

Come aumentare la velocità di lettura da HBase fino a 3 volte e da HDFS fino a 5 volte

Codice di implementazione

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

Vediamo ora tutto questo utilizzando un esempio reale. Abbiamo il seguente script di test:

  1. Iniziamo a eseguire la scansione (25 thread, batch = 100)
  2. Dopo 5 minuti, aggiungi multi-get (25 thread, batch = 100)
  3. Dopo 5 minuti, disattiva i multi-get (rimane solo la scansione)

Eseguiamo due esecuzioni, prima hbase.lru.cache.heavy.eviction.count.limit = 10000 (che effettivamente disabilita la funzione), quindi impostiamo limit = 0 (la abilita).

Nei log seguenti vediamo come viene attivata la funzione e reimposta l'Overshooting al 14-71%. Di tanto in tanto il carico diminuisce, il che attiva Backpression e HBase memorizza nuovamente nella cache più blocchi.

Registra regioneServer
sfrattati (MB): 0, rapporto 0.0, sovraccarico (%): -100, contatore di sfrattamenti pesanti: 0, memorizzazione nella cache corrente DataBlock (%): 100
sfrattati (MB): 0, rapporto 0.0, sovraccarico (%): -100, contatore di sfrattamenti pesanti: 0, memorizzazione nella cache corrente DataBlock (%): 100
sfrattati (MB): 2170, rapporto 1.09, sovraccarico (%): 985, contatore di sfratti pesanti: 1, caching corrente DataBlock (%): 91 < inizio
sfrattati (MB): 3763, rapporto 1.08, sovraccarico (%): 1781, contatore di sfratti pesanti: 2, caching corrente DataBlock (%): 76
sfrattati (MB): 3306, rapporto 1.07, sovraccarico (%): 1553, contatore di sfratti pesanti: 3, caching corrente DataBlock (%): 61
sfrattati (MB): 2508, rapporto 1.06, sovraccarico (%): 1154, contatore di sfratti pesanti: 4, caching corrente DataBlock (%): 50
sfrattati (MB): 1824, rapporto 1.04, sovraccarico (%): 812, contatore di sfratti pesanti: 5, caching corrente DataBlock (%): 42
sfrattati (MB): 1482, rapporto 1.03, sovraccarico (%): 641, contatore di sfratti pesanti: 6, caching corrente DataBlock (%): 36
sfrattati (MB): 1140, rapporto 1.01, sovraccarico (%): 470, contatore di sfratti pesanti: 7, caching corrente DataBlock (%): 32
sfrattati (MB): 913, rapporto 1.0, sovraccarico (%): 356, contatore di sfratti pesanti: 8, caching corrente DataBlock (%): 29
sfrattati (MB): 912, rapporto 0.89, sovraccarico (%): 356, contatore di sfratti pesanti: 9, caching corrente DataBlock (%): 26
sfrattati (MB): 684, rapporto 0.76, sovraccarico (%): 242, contatore di sfratti pesanti: 10, caching corrente DataBlock (%): 24
sfrattati (MB): 684, rapporto 0.61, sovraccarico (%): 242, contatore di sfratti pesanti: 11, caching corrente DataBlock (%): 22
sfrattati (MB): 456, rapporto 0.51, sovraccarico (%): 128, contatore di sfratti pesanti: 12, caching corrente DataBlock (%): 21
sfrattati (MB): 456, rapporto 0.42, sovraccarico (%): 128, contatore di sfratti pesanti: 13, caching corrente DataBlock (%): 20
sfrattati (MB): 456, rapporto 0.33, sovraccarico (%): 128, contatore di sfratti pesanti: 14, caching corrente DataBlock (%): 19
sfrattati (MB): 342, rapporto 0.33, sovraccarico (%): 71, contatore di sfratti pesanti: 15, caching corrente DataBlock (%): 19
sfrattati (MB): 342, rapporto 0.32, sovraccarico (%): 71, contatore di sfratti pesanti: 16, caching corrente DataBlock (%): 19
sfrattati (MB): 342, rapporto 0.31, sovraccarico (%): 71, contatore di sfratti pesanti: 17, caching corrente DataBlock (%): 19
sfrattati (MB): 228, rapporto 0.3, sovraccarico (%): 14, contatore di sfratti pesanti: 18, caching corrente DataBlock (%): 19
sfrattati (MB): 228, rapporto 0.29, sovraccarico (%): 14, contatore di sfratti pesanti: 19, caching corrente DataBlock (%): 19
sfrattati (MB): 228, rapporto 0.27, sovraccarico (%): 14, contatore di sfratti pesanti: 20, caching corrente DataBlock (%): 19
sfrattati (MB): 228, rapporto 0.25, sovraccarico (%): 14, contatore di sfratti pesanti: 21, caching corrente DataBlock (%): 19
sfrattati (MB): 228, rapporto 0.24, sovraccarico (%): 14, contatore di sfratti pesanti: 22, caching corrente DataBlock (%): 19
sfrattati (MB): 228, rapporto 0.22, sovraccarico (%): 14, contatore di sfratti pesanti: 23, caching corrente DataBlock (%): 19
sfrattati (MB): 228, rapporto 0.21, sovraccarico (%): 14, contatore di sfratti pesanti: 24, caching corrente DataBlock (%): 19
sfrattati (MB): 228, rapporto 0.2, sovraccarico (%): 14, contatore di sfratti pesanti: 25, caching corrente DataBlock (%): 19
sfrattati (MB): 228, rapporto 0.17, sovraccarico (%): 14, contatore di sfratti pesanti: 26, caching corrente DataBlock (%): 19
sfrattati (MB): 456, rapporto 0.17, sovraccarico (%): 128, contatore di sfratti pesanti: 27, caching corrente DataBlock (%): 18 < getti aggiunti (ma la tabella è la stessa)
sfrattati (MB): 456, rapporto 0.15, sovraccarico (%): 128, contatore di sfratti pesanti: 28, caching corrente DataBlock (%): 17
sfrattati (MB): 342, rapporto 0.13, sovraccarico (%): 71, contatore di sfratti pesanti: 29, caching corrente DataBlock (%): 17
sfrattati (MB): 342, rapporto 0.11, sovraccarico (%): 71, contatore di sfratti pesanti: 30, caching corrente DataBlock (%): 17
sfrattati (MB): 342, rapporto 0.09, sovraccarico (%): 71, contatore di sfratti pesanti: 31, caching corrente DataBlock (%): 17
sfrattati (MB): 228, rapporto 0.08, sovraccarico (%): 14, contatore di sfratti pesanti: 32, caching corrente DataBlock (%): 17
sfrattati (MB): 228, rapporto 0.07, sovraccarico (%): 14, contatore di sfratti pesanti: 33, caching corrente DataBlock (%): 17
sfrattati (MB): 228, rapporto 0.06, sovraccarico (%): 14, contatore di sfratti pesanti: 34, caching corrente DataBlock (%): 17
sfrattati (MB): 228, rapporto 0.05, sovraccarico (%): 14, contatore di sfratti pesanti: 35, caching corrente DataBlock (%): 17
sfrattati (MB): 228, rapporto 0.05, sovraccarico (%): 14, contatore di sfratti pesanti: 36, caching corrente DataBlock (%): 17
sfrattati (MB): 228, rapporto 0.04, sovraccarico (%): 14, contatore di sfratti pesanti: 37, caching corrente DataBlock (%): 17
sfrattati (MB): 109, rapporto 0.04, sovraccarico (%): -46, contatore di sfratti pesanti: 37, caching corrente DataBlock (%): 22 < contropressione
sfrattati (MB): 798, rapporto 0.24, sovraccarico (%): 299, contatore di sfratti pesanti: 38, caching corrente DataBlock (%): 20
sfrattati (MB): 798, rapporto 0.29, sovraccarico (%): 299, contatore di sfratti pesanti: 39, caching corrente DataBlock (%): 18
sfrattati (MB): 570, rapporto 0.27, sovraccarico (%): 185, contatore di sfratti pesanti: 40, caching corrente DataBlock (%): 17
sfrattati (MB): 456, rapporto 0.22, sovraccarico (%): 128, contatore di sfratti pesanti: 41, caching corrente DataBlock (%): 16
sfrattati (MB): 342, rapporto 0.16, sovraccarico (%): 71, contatore di sfratti pesanti: 42, caching corrente DataBlock (%): 16
sfrattati (MB): 342, rapporto 0.11, sovraccarico (%): 71, contatore di sfratti pesanti: 43, caching corrente DataBlock (%): 16
sfrattati (MB): 228, rapporto 0.09, sovraccarico (%): 14, contatore di sfratti pesanti: 44, caching corrente DataBlock (%): 16
sfrattati (MB): 228, rapporto 0.07, sovraccarico (%): 14, contatore di sfratti pesanti: 45, caching corrente DataBlock (%): 16
sfrattati (MB): 228, rapporto 0.05, sovraccarico (%): 14, contatore di sfratti pesanti: 46, caching corrente DataBlock (%): 16
sfrattati (MB): 222, rapporto 0.04, sovraccarico (%): 11, contatore di sfratti pesanti: 47, caching corrente DataBlock (%): 16
sfrattati (MB): 104, rapporto 0.03, sovraccarico (%): -48, contatore di sfrattamenti pesanti: 47, caching corrente DataBlock (%): 21 <interrupt ottiene
sfrattati (MB): 684, rapporto 0.2, sovraccarico (%): 242, contatore di sfratti pesanti: 48, caching corrente DataBlock (%): 19
sfrattati (MB): 570, rapporto 0.23, sovraccarico (%): 185, contatore di sfratti pesanti: 49, caching corrente DataBlock (%): 18
sfrattati (MB): 342, rapporto 0.22, sovraccarico (%): 71, contatore di sfratti pesanti: 50, caching corrente DataBlock (%): 18
sfrattati (MB): 228, rapporto 0.21, sovraccarico (%): 14, contatore di sfratti pesanti: 51, caching corrente DataBlock (%): 18
sfrattati (MB): 228, rapporto 0.2, sovraccarico (%): 14, contatore di sfratti pesanti: 52, caching corrente DataBlock (%): 18
sfrattati (MB): 228, rapporto 0.18, sovraccarico (%): 14, contatore di sfratti pesanti: 53, caching corrente DataBlock (%): 18
sfrattati (MB): 228, rapporto 0.16, sovraccarico (%): 14, contatore di sfratti pesanti: 54, caching corrente DataBlock (%): 18
sfrattati (MB): 228, rapporto 0.14, sovraccarico (%): 14, contatore di sfratti pesanti: 55, caching corrente DataBlock (%): 18
sfrattati (MB): 112, rapporto 0.14, sovraccarico (%): -44, contatore di sfratti pesanti: 55, caching corrente DataBlock (%): 23 < contropressione
sfrattati (MB): 456, rapporto 0.26, sovraccarico (%): 128, contatore di sfratti pesanti: 56, caching corrente DataBlock (%): 22
sfrattati (MB): 342, rapporto 0.31, sovraccarico (%): 71, contatore di sfratti pesanti: 57, caching corrente DataBlock (%): 22
sfrattati (MB): 342, rapporto 0.33, sovraccarico (%): 71, contatore di sfratti pesanti: 58, caching corrente DataBlock (%): 22
sfrattati (MB): 342, rapporto 0.33, sovraccarico (%): 71, contatore di sfratti pesanti: 59, caching corrente DataBlock (%): 22
sfrattati (MB): 342, rapporto 0.33, sovraccarico (%): 71, contatore di sfratti pesanti: 60, caching corrente DataBlock (%): 22
sfrattati (MB): 342, rapporto 0.33, sovraccarico (%): 71, contatore di sfratti pesanti: 61, caching corrente DataBlock (%): 22
sfrattati (MB): 342, rapporto 0.33, sovraccarico (%): 71, contatore di sfratti pesanti: 62, caching corrente DataBlock (%): 22
sfrattati (MB): 342, rapporto 0.33, sovraccarico (%): 71, contatore di sfratti pesanti: 63, caching corrente DataBlock (%): 22
sfrattati (MB): 342, rapporto 0.32, sovraccarico (%): 71, contatore di sfratti pesanti: 64, caching corrente DataBlock (%): 22
sfrattati (MB): 342, rapporto 0.33, sovraccarico (%): 71, contatore di sfratti pesanti: 65, caching corrente DataBlock (%): 22
sfrattati (MB): 342, rapporto 0.33, sovraccarico (%): 71, contatore di sfratti pesanti: 66, caching corrente DataBlock (%): 22
sfrattati (MB): 342, rapporto 0.32, sovraccarico (%): 71, contatore di sfratti pesanti: 67, caching corrente DataBlock (%): 22
sfrattati (MB): 342, rapporto 0.33, sovraccarico (%): 71, contatore di sfratti pesanti: 68, caching corrente DataBlock (%): 22
sfrattati (MB): 342, rapporto 0.32, sovraccarico (%): 71, contatore di sfratti pesanti: 69, caching corrente DataBlock (%): 22
sfrattati (MB): 342, rapporto 0.32, sovraccarico (%): 71, contatore di sfratti pesanti: 70, caching corrente DataBlock (%): 22
sfrattati (MB): 342, rapporto 0.33, sovraccarico (%): 71, contatore di sfratti pesanti: 71, caching corrente DataBlock (%): 22
sfrattati (MB): 342, rapporto 0.33, sovraccarico (%): 71, contatore di sfratti pesanti: 72, caching corrente DataBlock (%): 22
sfrattati (MB): 342, rapporto 0.33, sovraccarico (%): 71, contatore di sfratti pesanti: 73, caching corrente DataBlock (%): 22
sfrattati (MB): 342, rapporto 0.33, sovraccarico (%): 71, contatore di sfratti pesanti: 74, caching corrente DataBlock (%): 22
sfrattati (MB): 342, rapporto 0.33, sovraccarico (%): 71, contatore di sfratti pesanti: 75, caching corrente DataBlock (%): 22
sfrattati (MB): 342, rapporto 0.33, sovraccarico (%): 71, contatore di sfratti pesanti: 76, caching corrente DataBlock (%): 22
sfrattati (MB): 21, rapporto 0.33, sovraccarico (%): -90, contatore di sfrattamenti pesanti: 76, memorizzazione nella cache corrente DataBlock (%): 32
sfrattati (MB): 0, rapporto 0.0, sovraccarico (%): -100, contatore di sfrattamenti pesanti: 0, memorizzazione nella cache corrente DataBlock (%): 100
sfrattati (MB): 0, rapporto 0.0, sovraccarico (%): -100, contatore di sfrattamenti pesanti: 0, memorizzazione nella cache corrente DataBlock (%): 100

Le scansioni erano necessarie per mostrare lo stesso processo sotto forma di grafico della relazione tra due sezioni della cache: singola (dove i blocchi che non sono mai stati richiesti prima) e multi (i dati “richiesti” almeno una volta vengono archiviati qui):

Come aumentare la velocità di lettura da HBase fino a 3 volte e da HDFS fino a 5 volte

E infine, come appare il funzionamento dei parametri sotto forma di grafico. Per fare un confronto, all'inizio la cache è stata completamente disattivata, quindi è stato avviato HBase con la memorizzazione nella cache e ritardando l'inizio del lavoro di ottimizzazione di 5 minuti (30 cicli di eliminazione).

Il codice completo è disponibile in Pull Request HBASE23887 su github.

Tuttavia, 300mila letture al secondo non sono tutto ciò che si può ottenere su questo hardware in queste condizioni. Il fatto è che quando è necessario accedere ai dati tramite HDFS, viene utilizzato il meccanismo ShortCircuitCache (di seguito denominato SSC), che consente di accedere direttamente ai dati, evitando interazioni di rete.

La profilazione ha dimostrato che, sebbene questo meccanismo offra un grande vantaggio, a un certo punto diventa anche un collo di bottiglia, perché quasi tutte le operazioni pesanti avvengono all'interno di una serratura, il che porta per la maggior parte del tempo al blocco.

Come aumentare la velocità di lettura da HBase fino a 3 volte e da HDFS fino a 5 volte

Avendo capito questo, ci siamo resi conto che il problema può essere aggirato creando una serie di SSC indipendenti:

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

E poi lavorare con loro, escludendo le intersezioni anche all'ultima cifra di offset:

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

Ora puoi iniziare a testare. Per fare ciò, leggeremo i file da HDFS con una semplice applicazione multi-thread. Imposta i parametri:

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

E basta leggere i file:

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

Questo codice viene eseguito in thread separati e aumenteremo il numero di file letti contemporaneamente (da 10 a 200 - asse orizzontale) e il numero di cache (da 1 a 10 - grafica). L'asse verticale mostra l'accelerazione che risulta da un aumento di SSC rispetto al caso in cui è presente una sola cache.

Come aumentare la velocità di lettura da HBase fino a 3 volte e da HDFS fino a 5 volte

Come leggere il grafico: Il tempo di esecuzione per 100mila letture in blocchi da 64 KB con una cache richiede 78 secondi. Mentre con 5 cache ci vogliono 16 secondi. Quelli. c'è un'accelerazione di ~ 5 volte. Come si può vedere dal grafico, l'effetto non è molto evidente per un numero limitato di letture parallele; inizia a svolgere un ruolo notevole quando ci sono più di 50 letture di thread. È anche evidente che aumentando il numero di SSC da 6 e superiori danno un aumento delle prestazioni significativamente inferiore.

Nota 1: poiché i risultati del test sono piuttosto volatili (vedi sotto), sono state effettuate 3 analisi ed è stata calcolata la media dei valori risultanti.

Nota 2: il miglioramento delle prestazioni derivante dalla configurazione dell'accesso casuale è lo stesso, sebbene l'accesso stesso sia leggermente più lento.

È necessario però chiarire che, a differenza di HBase, questa accelerazione non è sempre gratuita. Qui “sblocchiamo” la capacità della CPU di lavorare di più, invece di restare bloccata.

Come aumentare la velocità di lettura da HBase fino a 3 volte e da HDFS fino a 5 volte

Qui puoi osservare che, in generale, un aumento del numero di cache dà un aumento approssimativamente proporzionale nell'utilizzo della CPU. Tuttavia, ci sono leggermente più combinazioni vincenti.

Ad esempio, diamo uno sguardo più da vicino all'impostazione SSC = 3. L'aumento delle prestazioni sull'intervallo è di circa 3.3 volte. Di seguito sono riportati i risultati di tutte e tre le prove separate.

Come aumentare la velocità di lettura da HBase fino a 3 volte e da HDFS fino a 5 volte

Mentre il consumo della CPU aumenta di circa 2.8 volte. La differenza non è molto grande, ma la piccola Greta è già felice e forse avrà tempo per frequentare la scuola e prendere lezioni.

Pertanto, ciò avrà un effetto positivo per qualsiasi strumento che utilizza l'accesso in blocco a HDFS (ad esempio Spark, ecc.), a condizione che il codice dell'applicazione sia leggero (ovvero la presa sia sul lato client HDFS) e ci sia potenza della CPU libera . Per verificare, testiamo quale effetto avrà l'uso combinato dell'ottimizzazione BlockCache e dell'ottimizzazione SSC per la lettura da HBase.

Come aumentare la velocità di lettura da HBase fino a 3 volte e da HDFS fino a 5 volte

Si può vedere che in tali condizioni l'effetto non è così eccezionale come nei test raffinati (lettura senza alcuna elaborazione), ma qui è del tutto possibile spremere altri 80K. Insieme, entrambe le ottimizzazioni forniscono una velocità fino a 4 volte superiore.

Per questa ottimizzazione è stata realizzata anche una PR [HDFS-15202], che è stato unito e questa funzionalità sarà disponibile nelle versioni future.

Infine, è stato interessante confrontare le prestazioni di lettura di un database simile a colonne larghe, Cassandra e HBase.

Per fare ciò, abbiamo lanciato istanze dell'utilità di test di carico YCSB standard da due host (800 thread in totale). Lato server: 4 istanze di RegionServer e Cassandra su 4 host (non quelli su cui sono in esecuzione i client, per evitare la loro influenza). Le letture provengono da tabelle di dimensioni:

HBase – 300 GB su HDFS (100 GB dati puri)

Cassandra - 250 GB (fattore di replica = 3)

Quelli. il volume era più o meno lo stesso (in HBase un po' di più).

Parametri HBase:

dfs.client.corto.circuito.num = 5 (Ottimizzazione client HDFS)

hbase.lru.cache.heavy.eviction.count.limit = 30 - ciò significa che la patch inizierà a funzionare dopo 30 eliminazioni (~5 minuti)

hbase.lru.cache.heavy.eviction.mb.size.limit = 300 — volume target di memorizzazione nella cache ed eliminazione

I registri YCSB sono stati analizzati e compilati in grafici Excel:

Come aumentare la velocità di lettura da HBase fino a 3 volte e da HDFS fino a 5 volte

Come puoi vedere, queste ottimizzazioni consentono di confrontare le prestazioni di questi database in queste condizioni e di raggiungere 450mila letture al secondo.

Ci auguriamo che queste informazioni possano essere utili a qualcuno durante l'entusiasmante lotta per la produttività.

Fonte: habr.com

Aggiungi un commento