Como aumentar a velocidade de lectura desde HBase ata 3 veces e desde HDFS ata 5 veces

O alto rendemento é un dos requisitos fundamentais cando se traballa con big data. No departamento de carga de datos de Sberbank, bombeamos case todas as transaccións á nosa nube de datos baseada en Hadoop e, polo tanto, tratamos fluxos de información moi grandes. Por suposto, sempre estamos a buscar formas de mellorar o rendemento, e agora queremos contarvos como conseguimos parchear RegionServer HBase e o cliente HDFS, grazas ao cal puidemos aumentar significativamente a velocidade das operacións de lectura.
Como aumentar a velocidade de lectura desde HBase ata 3 veces e desde HDFS ata 5 veces

Non obstante, antes de pasar á esencia das melloras, paga a pena falar de restricións que, en principio, non se poden eludir se se senta nun disco duro.

Por que o disco duro e as lecturas rápidas de acceso aleatorio son incompatibles
Como sabes, HBase e moitas outras bases de datos almacenan datos en bloques de varias decenas de kilobytes de tamaño. Por defecto é duns 64 KB. Agora imaxinemos que necesitamos obter só 100 bytes e pedimos a HBase que nos proporcione estes datos usando unha determinada clave. Dado que o tamaño do bloque en HFiles é de 64 KB, a solicitude será 640 veces maior (só un minuto!) do necesario.

A continuación, xa que a solicitude pasará por HDFS e o seu mecanismo de almacenamento en caché de metadatos ShortCircuitCache (que permite o acceso directo aos ficheiros), isto leva a ler xa 1 MB do disco. Non obstante, isto pódese axustar co parámetro dfs.client.read.shortcircuit.buffer.tamaño e en moitos casos ten sentido reducir este valor, por exemplo a 126 KB.

Digamos que facemos isto, pero ademais, cando comezamos a ler datos a través da API java, como funcións como FileChannel.read e pedimos ao sistema operativo que lea a cantidade especificada de datos, le "por se acaso" 2 veces máis , é dicir. 256 KB no noso caso. Isto débese a que Java non ten un xeito sinxelo de establecer a marca FADV_RANDOM para evitar este comportamento.

Como resultado, para obter os nosos 100 bytes, léanse 2600 veces máis baixo o capó. Parece que a solución é obvia, reducimos o tamaño do bloque a un kilobyte, poñamos a bandeira mencionada e gañemos unha gran aceleración da iluminación. Pero o problema é que ao reducir o tamaño do bloque en dúas veces, tamén reducimos o número de bytes lidos por unidade de tempo en dúas veces.

Pódese obter algunha ganancia coa definición da marca FADV_RANDOM, pero só cun multi-threading alto e cun tamaño de bloque de 128 KB, pero isto é un máximo dun par de decenas de por cento:

Como aumentar a velocidade de lectura desde HBase ata 3 veces e desde HDFS ata 5 veces

Realizáronse probas en 100 ficheiros, cada un de 1 GB de tamaño e situados en 10 discos duros.

Calculemos con que podemos contar, en principio, a esta velocidade:
Digamos que lemos desde 10 discos a unha velocidade de 280 MB/seg, é dicir. 3 millóns de veces 100 bytes. Pero como lembramos, os datos que necesitamos son 2600 veces menos dos que se le. Así, dividimos 3 millóns por 2600 e obtemos 1100 rexistros por segundo.

Deprimente, non? Esa é a natureza Acceso aleatorio acceso aos datos do disco duro, independentemente do tamaño do bloque. Este é o límite físico de acceso aleatorio e ningunha base de datos pode espremer máis nesas condicións.

Como entón as bases de datos alcanzan velocidades moito máis altas? Para responder a esta pregunta, vexamos o que está a suceder na seguinte imaxe:

Como aumentar a velocidade de lectura desde HBase ata 3 veces e desde HDFS ata 5 veces

Aquí vemos que durante os primeiros minutos a velocidade é realmente duns mil rexistros por segundo. Non obstante, ademais, debido ao feito de que se le moito máis do que se solicitou, os datos acaban no buff/caché do sistema operativo (linux) e a velocidade aumenta a 60 mil por segundo máis decente.

Así, ademais trataremos de acelerar o acceso só aos datos que están na caché do SO ou que se atopan en dispositivos de almacenamento SSD/NVMe de velocidade de acceso comparable.

No noso caso, realizaremos probas nun banco de 4 servidores, cada un dos cales se cobra do seguinte xeito:

CPU: Xeon E5-2680 v4 @ 2.40GHz 64 subprocesos.
Memoria: 730 GB.
Versión de java: 1.8.0_111

E aquí o punto clave é a cantidade de datos nas táboas que hai que ler. O feito é que se le os datos dunha táboa que está completamente situada na caché de HBase, nin sequera chegará a ler desde o buff/caché do sistema operativo. Porque HBase por defecto asigna o 40% da memoria a unha estrutura chamada BlockCache. Esencialmente, trátase dun ConcurrentHashMap, onde a clave é o nome do ficheiro + a compensación do bloque, e o valor son os datos reais desta compensación.

Así, ao ler só desde esta estrutura, nós vemos unha velocidade excelente, como un millón de solicitudes por segundo. Pero imaxinemos que non podemos asignar centos de gigabytes de memoria só para as necesidades da base de datos, porque hai moitas outras cousas útiles que se executan nestes servidores.

Por exemplo, no noso caso, o volume de BlockCache nun RS é duns 12 GB. Aterramos dous RS nun nodo, é dicir. 96 GB están asignados para BlockCache en todos os nós. E hai moitas veces máis datos, por exemplo, que sexan 4 táboas, de 130 rexións cada unha, nas que os ficheiros teñen un tamaño de 800 MB, comprimidos por FAST_DIFF, é dicir. un total de 410 GB (son datos puros, é dicir, sen ter en conta o factor de replicación).

Así, BlockCache é só preto do 23% do volume total de datos e isto está moito máis preto das condicións reais do que se chama BigData. E aquí é onde comeza a diversión, porque obviamente, cantos menos accesos á caché, peor será o rendemento. Despois de todo, se perdes, terás que facer moito traballo, é dicir. baixar ás funcións do sistema de chamada. Non obstante, isto non se pode evitar, así que vexamos un aspecto completamente diferente: que pasa cos datos dentro da caché?

Simplificamos a situación e supoñemos que temos unha caché que só cabe 1 obxecto. Aquí tes un exemplo do que ocorrerá cando tratemos de traballar cun volume de datos 3 veces maior que a caché, teremos que:

1. Coloca o bloque 1 na caché
2. Elimina o bloque 1 da caché
3. Coloca o bloque 2 na caché
4. Elimina o bloque 2 da caché
5. Coloca o bloque 3 na caché

5 accións completadas! Non obstante, esta situación non se pode chamar normal; de feito, estamos obrigando a HBase a facer un montón de traballos completamente inútiles. Le constantemente os datos da caché do SO, colócaos en BlockCache, só para botalos case inmediatamente porque chegou unha nova porción de datos. A animación do comezo da publicación mostra a esencia do problema: o Garbage Collector está a saír de escala, a atmosfera está quentando, a pequena Greta na afastada e quente Suecia está a molestarse. E á xente de informática non nos gusta moito cando os nenos están tristes, así que comezamos a pensar que podemos facer ao respecto.

E se colocas non todos os bloques na caché, senón só unha determinada porcentaxe deles, para que a caché non se desborde? Comecemos simplemente engadindo unhas poucas liñas de código ao comezo da función para poñer datos en BlockCache:

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

O punto aquí é o seguinte: o desplazamento é a posición do bloque no ficheiro e os seus últimos díxitos distribúense de forma aleatoria e uniforme de 00 a 99. Polo tanto, só omitiremos aqueles que entren no rango que necesitamos.

Por exemplo, establece cacheDataBlockPercent = 20 e mira o que ocorre:

Como aumentar a velocidade de lectura desde HBase ata 3 veces e desde HDFS ata 5 veces

O resultado é evidente. Nos gráficos que aparecen a continuación, queda claro por que se produciu tal aceleración: aforramos moitos recursos de GC sen facer o traballo de Sísifo de colocar datos na caché só para botalos inmediatamente ao sumidoiro dos cans marcianos:

Como aumentar a velocidade de lectura desde HBase ata 3 veces e desde HDFS ata 5 veces

Ao mesmo tempo, a utilización da CPU aumenta, pero é moito menor que a produtividade:

Como aumentar a velocidade de lectura desde HBase ata 3 veces e desde HDFS ata 5 veces

Tamén vale a pena notar que os bloques almacenados en BlockCache son diferentes. A maioría, preto do 95%, son os propios datos. E o resto son metadatos, como filtros Bloom ou LEAF_INDEX e т.д.. Estes datos non son suficientes, pero son moi útiles, porque antes de acceder directamente aos datos, HBase recorre ao meta para comprender se é necesario buscar máis aquí e, de ser así, onde se atopa exactamente o bloque de interese.

Polo tanto, no código vemos unha condición de verificación buf.getBlockType().isData() e grazas a este meta, deixarémolo na caché en todo caso.

Agora imos aumentar a carga e reforzar lixeiramente a función dunha soa vez. Na primeira proba fixemos a porcentaxe de corte = 20 e BlockCache estaba lixeiramente infrautilizado. Agora imos configurar o 23% e engadir 100 fíos cada 5 minutos para ver en que punto se produce a saturación:

Como aumentar a velocidade de lectura desde HBase ata 3 veces e desde HDFS ata 5 veces

Aquí vemos que a versión orixinal chega case de inmediato ao teito cunhas 100 mil solicitudes por segundo. Mentres que o parche dá unha aceleración de ata 300 mil. Ao mesmo tempo, está claro que unha maior aceleración xa non é tan "gratuíta"; a utilización da CPU tamén está aumentando.

Non obstante, esta non é unha solución moi elegante, xa que non sabemos de antemán que porcentaxe de bloques hai que almacenar na caché, depende do perfil de carga. Por iso, implantouse un mecanismo para axustar automaticamente este parámetro en función da actividade das operacións de lectura.

Engadíronse tres opcións para controlar isto:

hbase.lru.cache.heavy.viction.count.limit — establece cantas veces debe executarse o proceso de expulsión de datos da caché antes de comezar a utilizar a optimización (é dicir, omitir bloques). Por defecto é igual a MAX_INT = 2147483647 e de feito significa que a función nunca comezará a funcionar con este valor. Porque o proceso de desaloxo comeza cada 5 - 10 segundos (depende da carga) e 2147483647 * 10 / 60 / 60 / 24 / 365 = 680 anos. Non obstante, podemos establecer este parámetro en 0 e facer que a función funcione inmediatamente despois do lanzamento.

Non obstante, tamén hai unha carga útil neste parámetro. Se a nosa carga é tal que as lecturas a curto prazo (por exemplo, durante o día) e as lecturas a longo prazo (pola noite) están constantemente intercaladas, entón podemos asegurarnos de que a función estea activada só cando estean en curso operacións de lectura longas.

Por exemplo, sabemos que as lecturas a curto prazo adoitan durar aproximadamente 1 minuto. Non é necesario comezar a tirar bloques, a caché non terá tempo de quedar obsoleta e entón podemos establecer este parámetro igual, por exemplo, 10. Isto levará a que a optimización comezará a funcionar só cando iniciouse a lectura activa do trimestre, é dicir. en 100 segundos. Así, se temos unha lectura a curto prazo, todos os bloques pasarán á caché e estarán dispoñibles (agás aqueles que serán desaloxados polo algoritmo estándar). E cando facemos lecturas a longo prazo, a función está activada e teriamos un rendemento moito maior.

hbase.lru.cache.heavy.eviction.mb.size.limit — establece cantos megabytes queremos colocar na caché (e, por suposto, expulsar) en 10 segundos. A función tentará alcanzar este valor e mantelo. A cuestión é esta: se metemos gigabytes na caché, entón teremos que desaloxar gigabytes, e isto, como vimos anteriormente, é moi caro. Non obstante, non debes tentar configuralo demasiado pequeno, xa que isto fará que o modo de salto de bloque saia prematuramente. Para servidores potentes (uns 20-40 núcleos físicos), o ideal é configurar uns 300-400 MB. Para a clase media (~10 núcleos) 200-300 MB. Para sistemas débiles (2-5 núcleos) 50-100 MB poden ser normais (non se probaron nestes).

Vexamos como funciona isto: digamos que establecemos hbase.lru.cache.heavy.eviction.mb.size.limit = 500, hai algún tipo de carga (lectura) e despois cada ~10 segundos calculamos cantos bytes foron expulsado usando a fórmula:

Sobrecarga = Bytes liberados Suma (MB) * 100 / Límite (MB) - 100;

Se de feito foron desaloxados 2000 MB, entón os gastos xerais equivalen a:

2000 * 100/500 - 100 = 300 %

Os algoritmos tentan manter non máis dunhas poucas decenas de por cento, polo que a función reducirá a porcentaxe de bloques almacenados na caché, implementando así un mecanismo de axuste automático.

Non obstante, se a carga cae, digamos que só se desaloxan 200 MB e que Overhead vólvese negativo (o chamado overshooting):

200 * 100/500 - 100 = -60 %

Pola contra, a función aumentará a porcentaxe de bloques almacenados na memoria caché ata que Overhead sexa positivo.

A continuación móstrase un exemplo de como se ve isto en datos reais. Non hai que tentar chegar ao 0%, é imposible. É moi bo cando se trata dun 30 - 100%, isto axuda a evitar a saída prematura do modo de optimización durante aumentos a curto prazo.

hbase.lru.cache.heavy.eviction.overhead.coeficient — establece a rapidez con que nos gustaría obter o resultado. Se sabemos con certeza que as nosas lecturas son na súa maioría longas e non queremos esperar, podemos aumentar esta proporción e obter un alto rendemento máis rápido.

Por exemplo, establecemos este coeficiente = 0.01. Isto significa que a sobrecarga (ver arriba) multiplicarase por este número polo resultado resultante e reducirase a porcentaxe de bloques en caché. Supoñamos que Overhead = 300% e coeficiente = 0.01, entón a porcentaxe de bloques almacenados en caché reducirase un 3%.

Tamén se implementa unha lóxica de "contrapresión" similar para os valores negativos de sobrecarga (superación). Dado que sempre son posibles flutuacións a curto prazo no volume de lecturas e desafiuzamentos, este mecanismo permítelle evitar a saída prematura do modo de optimización. A contrapresión ten unha lóxica invertida: canto máis forte sexa a superación, máis bloques se almacenan en caché.

Como aumentar a velocidade de lectura desde HBase ata 3 veces e desde HDFS ata 5 veces

Código de implementación

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

Vexamos agora todo isto usando un exemplo real. Temos o seguinte script de proba:

  1. Imos comezar a facer Scan (25 fíos, lote = 100)
  2. Despois de 5 minutos, engade múltiples obtencións (25 fíos, lote = 100)
  3. Despois de 5 minutos, desactiva as obtencións múltiples (só queda a exploración de novo)

Facemos dúas execucións, primeiro hbase.lru.cache.heavy.eviction.count.limit = 10000 (o que en realidade desactiva a función) e despois establecemos límite = 0 (activo).

Nos rexistros de abaixo vemos como a función está activada e restablece Overshooting ao 14-71%. De cando en vez a carga diminúe, o que activa a contrapresión e HBase almacena máis bloques de novo.

Log RegionServer
expulsados ​​(MB): 0, proporción 0.0, sobrecarga (%): -100, contador de desaloxos pesados: 0, bloque de datos de caché actual (%): 100
expulsados ​​(MB): 0, proporción 0.0, sobrecarga (%): -100, contador de desaloxos pesados: 0, bloque de datos de caché actual (%): 100
expulsados ​​(MB): 2170, proporción 1.09, sobrecarga (%): 985, contador de desafiuzamentos pesados: 1, dataBlock actual de caché (%): 91 < inicio
expulsados ​​(MB): 3763, proporción 1.08, sobrecarga (%): 1781, contador de desafiuzamentos pesados: 2, dataBlock actual de almacenamento en caché (%): 76
expulsados ​​(MB): 3306, proporción 1.07, sobrecarga (%): 1553, contador de desafiuzamentos pesados: 3, dataBlock actual de almacenamento en caché (%): 61
expulsados ​​(MB): 2508, proporción 1.06, sobrecarga (%): 1154, contador de desafiuzamentos pesados: 4, dataBlock actual de almacenamento en caché (%): 50
expulsados ​​(MB): 1824, proporción 1.04, sobrecarga (%): 812, contador de desafiuzamentos pesados: 5, dataBlock actual de almacenamento en caché (%): 42
expulsados ​​(MB): 1482, proporción 1.03, sobrecarga (%): 641, contador de desafiuzamentos pesados: 6, dataBlock actual de almacenamento en caché (%): 36
expulsados ​​(MB): 1140, proporción 1.01, sobrecarga (%): 470, contador de desafiuzamentos pesados: 7, dataBlock actual de almacenamento en caché (%): 32
expulsados ​​(MB): 913, proporción 1.0, sobrecarga (%): 356, contador de desafiuzamentos pesados: 8, dataBlock actual de almacenamento en caché (%): 29
expulsados ​​(MB): 912, proporción 0.89, sobrecarga (%): 356, contador de desafiuzamentos pesados: 9, dataBlock actual de almacenamento en caché (%): 26
expulsados ​​(MB): 684, proporción 0.76, sobrecarga (%): 242, contador de desafiuzamentos pesados: 10, dataBlock actual de almacenamento en caché (%): 24
expulsados ​​(MB): 684, proporción 0.61, sobrecarga (%): 242, contador de desafiuzamentos pesados: 11, dataBlock actual de almacenamento en caché (%): 22
expulsados ​​(MB): 456, proporción 0.51, sobrecarga (%): 128, contador de desafiuzamentos pesados: 12, dataBlock actual de almacenamento en caché (%): 21
expulsados ​​(MB): 456, proporción 0.42, sobrecarga (%): 128, contador de desafiuzamentos pesados: 13, dataBlock actual de almacenamento en caché (%): 20
expulsados ​​(MB): 456, proporción 0.33, sobrecarga (%): 128, contador de desafiuzamentos pesados: 14, dataBlock actual de almacenamento en caché (%): 19
expulsados ​​(MB): 342, proporción 0.33, sobrecarga (%): 71, contador de desafiuzamentos pesados: 15, dataBlock actual de almacenamento en caché (%): 19
expulsados ​​(MB): 342, proporción 0.32, sobrecarga (%): 71, contador de desafiuzamentos pesados: 16, dataBlock actual de almacenamento en caché (%): 19
expulsados ​​(MB): 342, proporción 0.31, sobrecarga (%): 71, contador de desafiuzamentos pesados: 17, dataBlock actual de almacenamento en caché (%): 19
expulsados ​​(MB): 228, proporción 0.3, sobrecarga (%): 14, contador de desafiuzamentos pesados: 18, dataBlock actual de almacenamento en caché (%): 19
expulsados ​​(MB): 228, proporción 0.29, sobrecarga (%): 14, contador de desafiuzamentos pesados: 19, dataBlock actual de almacenamento en caché (%): 19
expulsados ​​(MB): 228, proporción 0.27, sobrecarga (%): 14, contador de desafiuzamentos pesados: 20, dataBlock actual de almacenamento en caché (%): 19
expulsados ​​(MB): 228, proporción 0.25, sobrecarga (%): 14, contador de desafiuzamentos pesados: 21, dataBlock actual de almacenamento en caché (%): 19
expulsados ​​(MB): 228, proporción 0.24, sobrecarga (%): 14, contador de desafiuzamentos pesados: 22, dataBlock actual de almacenamento en caché (%): 19
expulsados ​​(MB): 228, proporción 0.22, sobrecarga (%): 14, contador de desafiuzamentos pesados: 23, dataBlock actual de almacenamento en caché (%): 19
expulsados ​​(MB): 228, proporción 0.21, sobrecarga (%): 14, contador de desafiuzamentos pesados: 24, dataBlock actual de almacenamento en caché (%): 19
expulsados ​​(MB): 228, proporción 0.2, sobrecarga (%): 14, contador de desafiuzamentos pesados: 25, dataBlock actual de almacenamento en caché (%): 19
expulsados ​​(MB): 228, proporción 0.17, sobrecarga (%): 14, contador de desafiuzamentos pesados: 26, dataBlock actual de almacenamento en caché (%): 19
expulsados ​​(MB): 456, proporción 0.17, sobrecarga (%): 128, contador de desafiuzamentos pesados: 27, dataBlock actual de almacenamento en caché (%): 18 < engadido gets (pero a táboa igual)
expulsados ​​(MB): 456, proporción 0.15, sobrecarga (%): 128, contador de desafiuzamentos pesados: 28, dataBlock actual de almacenamento en caché (%): 17
expulsados ​​(MB): 342, proporción 0.13, sobrecarga (%): 71, contador de desafiuzamentos pesados: 29, dataBlock actual de almacenamento en caché (%): 17
expulsados ​​(MB): 342, proporción 0.11, sobrecarga (%): 71, contador de desafiuzamentos pesados: 30, dataBlock actual de almacenamento en caché (%): 17
expulsados ​​(MB): 342, proporción 0.09, sobrecarga (%): 71, contador de desafiuzamentos pesados: 31, dataBlock actual de almacenamento en caché (%): 17
expulsados ​​(MB): 228, proporción 0.08, sobrecarga (%): 14, contador de desafiuzamentos pesados: 32, dataBlock actual de almacenamento en caché (%): 17
expulsados ​​(MB): 228, proporción 0.07, sobrecarga (%): 14, contador de desafiuzamentos pesados: 33, dataBlock actual de almacenamento en caché (%): 17
expulsados ​​(MB): 228, proporción 0.06, sobrecarga (%): 14, contador de desafiuzamentos pesados: 34, dataBlock actual de almacenamento en caché (%): 17
expulsados ​​(MB): 228, proporción 0.05, sobrecarga (%): 14, contador de desafiuzamentos pesados: 35, dataBlock actual de almacenamento en caché (%): 17
expulsados ​​(MB): 228, proporción 0.05, sobrecarga (%): 14, contador de desafiuzamentos pesados: 36, dataBlock actual de almacenamento en caché (%): 17
expulsados ​​(MB): 228, proporción 0.04, sobrecarga (%): 14, contador de desafiuzamentos pesados: 37, dataBlock actual de almacenamento en caché (%): 17
expulsados ​​(MB): 109, proporción 0.04, sobrecarga (%): -46, contador de desaloxos pesados: 37, bloqueo de datos da caché actual (%): 22 < contrapresión
expulsados ​​(MB): 798, proporción 0.24, sobrecarga (%): 299, contador de desafiuzamentos pesados: 38, dataBlock actual de almacenamento en caché (%): 20
expulsados ​​(MB): 798, proporción 0.29, sobrecarga (%): 299, contador de desafiuzamentos pesados: 39, dataBlock actual de almacenamento en caché (%): 18
expulsados ​​(MB): 570, proporción 0.27, sobrecarga (%): 185, contador de desafiuzamentos pesados: 40, dataBlock actual de almacenamento en caché (%): 17
expulsados ​​(MB): 456, proporción 0.22, sobrecarga (%): 128, contador de desafiuzamentos pesados: 41, dataBlock actual de almacenamento en caché (%): 16
expulsados ​​(MB): 342, proporción 0.16, sobrecarga (%): 71, contador de desafiuzamentos pesados: 42, dataBlock actual de almacenamento en caché (%): 16
expulsados ​​(MB): 342, proporción 0.11, sobrecarga (%): 71, contador de desafiuzamentos pesados: 43, dataBlock actual de almacenamento en caché (%): 16
expulsados ​​(MB): 228, proporción 0.09, sobrecarga (%): 14, contador de desafiuzamentos pesados: 44, dataBlock actual de almacenamento en caché (%): 16
expulsados ​​(MB): 228, proporción 0.07, sobrecarga (%): 14, contador de desafiuzamentos pesados: 45, dataBlock actual de almacenamento en caché (%): 16
expulsados ​​(MB): 228, proporción 0.05, sobrecarga (%): 14, contador de desafiuzamentos pesados: 46, dataBlock actual de almacenamento en caché (%): 16
expulsados ​​(MB): 222, proporción 0.04, sobrecarga (%): 11, contador de desafiuzamentos pesados: 47, dataBlock actual de almacenamento en caché (%): 16
expulsados ​​(MB): 104, proporción 0.03, sobrecarga (%): -48, contador de desafiuzamentos pesados: 47, bloqueo de datos da caché actual (%): 21 < interrupción obtén
expulsados ​​(MB): 684, proporción 0.2, sobrecarga (%): 242, contador de desafiuzamentos pesados: 48, dataBlock actual de almacenamento en caché (%): 19
expulsados ​​(MB): 570, proporción 0.23, sobrecarga (%): 185, contador de desafiuzamentos pesados: 49, dataBlock actual de almacenamento en caché (%): 18
expulsados ​​(MB): 342, proporción 0.22, sobrecarga (%): 71, contador de desafiuzamentos pesados: 50, dataBlock actual de almacenamento en caché (%): 18
expulsados ​​(MB): 228, proporción 0.21, sobrecarga (%): 14, contador de desafiuzamentos pesados: 51, dataBlock actual de almacenamento en caché (%): 18
expulsados ​​(MB): 228, proporción 0.2, sobrecarga (%): 14, contador de desafiuzamentos pesados: 52, dataBlock actual de almacenamento en caché (%): 18
expulsados ​​(MB): 228, proporción 0.18, sobrecarga (%): 14, contador de desafiuzamentos pesados: 53, dataBlock actual de almacenamento en caché (%): 18
expulsados ​​(MB): 228, proporción 0.16, sobrecarga (%): 14, contador de desafiuzamentos pesados: 54, dataBlock actual de almacenamento en caché (%): 18
expulsados ​​(MB): 228, proporción 0.14, sobrecarga (%): 14, contador de desafiuzamentos pesados: 55, dataBlock actual de almacenamento en caché (%): 18
expulsados ​​(MB): 112, proporción 0.14, sobrecarga (%): -44, contador de desaloxos pesados: 55, bloqueo de datos da caché actual (%): 23 < contrapresión
expulsados ​​(MB): 456, proporción 0.26, sobrecarga (%): 128, contador de desafiuzamentos pesados: 56, dataBlock actual de almacenamento en caché (%): 22
expulsados ​​(MB): 342, proporción 0.31, sobrecarga (%): 71, contador de desafiuzamentos pesados: 57, dataBlock actual de almacenamento en caché (%): 22
expulsados ​​(MB): 342, proporción 0.33, sobrecarga (%): 71, contador de desafiuzamentos pesados: 58, dataBlock actual de almacenamento en caché (%): 22
expulsados ​​(MB): 342, proporción 0.33, sobrecarga (%): 71, contador de desafiuzamentos pesados: 59, dataBlock actual de almacenamento en caché (%): 22
expulsados ​​(MB): 342, proporción 0.33, sobrecarga (%): 71, contador de desafiuzamentos pesados: 60, dataBlock actual de almacenamento en caché (%): 22
expulsados ​​(MB): 342, proporción 0.33, sobrecarga (%): 71, contador de desafiuzamentos pesados: 61, dataBlock actual de almacenamento en caché (%): 22
expulsados ​​(MB): 342, proporción 0.33, sobrecarga (%): 71, contador de desafiuzamentos pesados: 62, dataBlock actual de almacenamento en caché (%): 22
expulsados ​​(MB): 342, proporción 0.33, sobrecarga (%): 71, contador de desafiuzamentos pesados: 63, dataBlock actual de almacenamento en caché (%): 22
expulsados ​​(MB): 342, proporción 0.32, sobrecarga (%): 71, contador de desafiuzamentos pesados: 64, dataBlock actual de almacenamento en caché (%): 22
expulsados ​​(MB): 342, proporción 0.33, sobrecarga (%): 71, contador de desafiuzamentos pesados: 65, dataBlock actual de almacenamento en caché (%): 22
expulsados ​​(MB): 342, proporción 0.33, sobrecarga (%): 71, contador de desafiuzamentos pesados: 66, dataBlock actual de almacenamento en caché (%): 22
expulsados ​​(MB): 342, proporción 0.32, sobrecarga (%): 71, contador de desafiuzamentos pesados: 67, dataBlock actual de almacenamento en caché (%): 22
expulsados ​​(MB): 342, proporción 0.33, sobrecarga (%): 71, contador de desafiuzamentos pesados: 68, dataBlock actual de almacenamento en caché (%): 22
expulsados ​​(MB): 342, proporción 0.32, sobrecarga (%): 71, contador de desafiuzamentos pesados: 69, dataBlock actual de almacenamento en caché (%): 22
expulsados ​​(MB): 342, proporción 0.32, sobrecarga (%): 71, contador de desafiuzamentos pesados: 70, dataBlock actual de almacenamento en caché (%): 22
expulsados ​​(MB): 342, proporción 0.33, sobrecarga (%): 71, contador de desafiuzamentos pesados: 71, dataBlock actual de almacenamento en caché (%): 22
expulsados ​​(MB): 342, proporción 0.33, sobrecarga (%): 71, contador de desafiuzamentos pesados: 72, dataBlock actual de almacenamento en caché (%): 22
expulsados ​​(MB): 342, proporción 0.33, sobrecarga (%): 71, contador de desafiuzamentos pesados: 73, dataBlock actual de almacenamento en caché (%): 22
expulsados ​​(MB): 342, proporción 0.33, sobrecarga (%): 71, contador de desafiuzamentos pesados: 74, dataBlock actual de almacenamento en caché (%): 22
expulsados ​​(MB): 342, proporción 0.33, sobrecarga (%): 71, contador de desafiuzamentos pesados: 75, dataBlock actual de almacenamento en caché (%): 22
expulsados ​​(MB): 342, proporción 0.33, sobrecarga (%): 71, contador de desafiuzamentos pesados: 76, dataBlock actual de almacenamento en caché (%): 22
expulsados ​​(MB): 21, proporción 0.33, sobrecarga (%): -90, contador de desaloxos pesados: 76, bloque de datos de caché actual (%): 32
expulsados ​​(MB): 0, proporción 0.0, sobrecarga (%): -100, contador de desaloxos pesados: 0, bloque de datos de caché actual (%): 100
expulsados ​​(MB): 0, proporción 0.0, sobrecarga (%): -100, contador de desaloxos pesados: 0, bloque de datos de caché actual (%): 100

As exploracións foron necesarias para mostrar o mesmo proceso en forma de gráfico da relación entre dúas seccións de caché: única (onde os bloques que nunca se solicitaron antes) e multi (os datos "solicitados" polo menos unha vez almacénanse aquí):

Como aumentar a velocidade de lectura desde HBase ata 3 veces e desde HDFS ata 5 veces

E por último, como é o funcionamento dos parámetros en forma de gráfico. A modo de comparación, a caché desactivouse por completo ao principio, despois lanzouse HBase con caché e atrasando o inicio do traballo de optimización en 5 minutos (30 ciclos de expulsión).

O código completo pódese atopar en Pull Request HBASE 23887 en github.

Non obstante, 300 mil lecturas por segundo non é todo o que se pode conseguir neste hardware nestas condicións. O caso é que cando se precisa acceder aos datos a través de HDFS utilízase o mecanismo ShortCircuitCache (en diante SSC), que permite acceder aos datos directamente, evitando interaccións na rede.

O perfilado mostrou que aínda que este mecanismo dá unha gran ganancia, tamén nalgún momento convértese nun pescozo de botella, porque case todas as operacións pesadas ocorren dentro dunha pechadura, o que leva a bloquear a maioría das veces.

Como aumentar a velocidade de lectura desde HBase ata 3 veces e desde HDFS ata 5 veces

Decatámonos disto, decatámonos de que o problema pódese evitar creando unha matriz de SSC independentes:

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

E despois traballa con eles, excluíndo as interseccións tamén no último díxito de compensación:

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

Agora podes comezar a probar. Para iso, leremos ficheiros de HDFS cunha sinxela aplicación multiproceso. Establecer os parámetros:

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 só ler os ficheiros:

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

Este código execútase en fíos separados e aumentaremos o número de ficheiros lidos simultaneamente (de 10 a 200 - eixe horizontal) e o número de cachés (de 1 a 10 - gráficos). O eixe vertical mostra a aceleración que resulta dun aumento de SSC en relación ao caso en que só hai unha caché.

Como aumentar a velocidade de lectura desde HBase ata 3 veces e desde HDFS ata 5 veces

Como ler o gráfico: o tempo de execución de 100 mil lecturas en bloques de 64 KB cunha caché require 78 segundos. Mentres que con 5 cachés leva 16 segundos. Eses. hai unha aceleración de ~5 veces. Como se pode ver no gráfico, o efecto non é moi perceptible para un pequeno número de lecturas paralelas, comeza a xogar un papel notable cando hai máis de lecturas de fíos 50. Tamén se nota que aumenta o número de SSC de 6. e por riba dá un aumento de rendemento significativamente menor.

Nota 1: dado que os resultados das probas son bastante volátiles (ver a continuación), realizáronse 3 carreiras e promediaron os valores resultantes.

Nota 2: a ganancia de rendemento da configuración do acceso aleatorio é a mesma, aínda que o acceso en si é un pouco máis lento.

Non obstante, cómpre aclarar que, a diferenza do caso de HBase, esta aceleración non sempre é libre. Aquí "desbloqueamos" a capacidade da CPU para traballar máis, en lugar de colgarse nos bloqueos.

Como aumentar a velocidade de lectura desde HBase ata 3 veces e desde HDFS ata 5 veces

Aquí podes observar que, en xeral, un aumento no número de cachés dá un aumento aproximadamente proporcional na utilización da CPU. Non obstante, hai un pouco máis de combinacións gañadoras.

Por exemplo, vexamos máis de cerca a configuración SSC = 3. O aumento do rendemento no rango é de aproximadamente 3.3 veces. Abaixo amósanse os resultados das tres carreiras separadas.

Como aumentar a velocidade de lectura desde HBase ata 3 veces e desde HDFS ata 5 veces

Mentres que o consumo da CPU aumenta unhas 2.8 veces. A diferenza non é moi grande, pero a pequena Greta xa está contenta e pode ter tempo para ir ao colexio e tomar clases.

Así, isto terá un efecto positivo para calquera ferramenta que use acceso masivo a HDFS (por exemplo Spark, etc.), sempre que o código da aplicación sexa lixeiro (é dicir, o enchufe estea no lado do cliente HDFS) e haxa enerxía da CPU gratuíta. . Para comprobar, imos probar o efecto que terá o uso combinado da optimización de BlockCache e a axuste SSC para ler desde HBase.

Como aumentar a velocidade de lectura desde HBase ata 3 veces e desde HDFS ata 5 veces

Pódese ver que en tales condicións o efecto non é tan grande como nas probas refinadas (lectura sen ningún procesamento), pero é moi posible espremer aquí 80K adicionais. En conxunto, ambas optimizacións proporcionan unha velocidade de ata 4 veces.

Tamén se fixo un PR para esta optimización [HDFS-15202], que se fusionou e esta funcionalidade estará dispoñible en próximas versións.

E, finalmente, foi interesante comparar o rendemento de lectura dunha base de datos de columnas amplas semellante, Cassandra e HBase.

Para iso, lanzamos instancias da utilidade estándar de proba de carga YCSB desde dous hosts (800 fíos en total). No lado do servidor: 4 instancias de RegionServer e Cassandra en 4 hosts (non nos que se están executando os clientes, para evitar a súa influencia). As lecturas proviñan de táboas de tamaño:

HBase: 300 GB en HDFS (100 GB de datos puros)

Cassandra - 250 GB (factor de replicación = 3)

Eses. o volume era aproximadamente o mesmo (en HBase un pouco máis).

Parámetros HBase:

dfs.client.short.circuit.num = 5 (Optimización de cliente HDFS)

hbase.lru.cache.heavy.eviction.count.limit = 30 - isto significa que o parche comezará a funcionar despois de 30 desafiuzamentos (~5 minutos)

hbase.lru.cache.heavy.eviction.mb.size.limit = 300 — volume obxectivo de almacenamento en caché e expulsión

Os rexistros de YCSB foron analizados e compilados en gráficos de Excel:

Como aumentar a velocidade de lectura desde HBase ata 3 veces e desde HDFS ata 5 veces

Como podes ver, estas optimizacións permiten comparar o rendemento destas bases de datos nestas condicións e acadar 450 mil lecturas por segundo.

Agardamos que esta información poida ser útil para alguén durante a emocionante loita pola produtividade.

Fonte: www.habr.com

Engadir un comentario