Cómo aumentar la velocidad de lectura desde HBase hasta 3 veces y desde HDFS hasta 5 veces

El alto rendimiento es uno de los requisitos clave cuando se trabaja con big data. En el departamento de carga de datos de Sberbank, bombeamos casi todas las transacciones a nuestra nube de datos basada en Hadoop y, por lo tanto, manejamos flujos de información realmente grandes. Naturalmente, siempre estamos buscando formas de mejorar el rendimiento y ahora queremos contarles cómo logramos parchear RegionServer HBase y el cliente HDFS, gracias a lo cual pudimos aumentar significativamente la velocidad de las operaciones de lectura.
Cómo aumentar la velocidad de lectura desde HBase hasta 3 veces y desde HDFS hasta 5 veces

Sin embargo, antes de pasar a la esencia de las mejoras, vale la pena hablar de restricciones que, en principio, no se pueden eludir si estás sentado en un disco duro.

Por qué el disco duro y las lecturas rápidas de acceso aleatorio son incompatibles
Como sabe, HBase y muchas otras bases de datos almacenan datos en bloques de varias decenas de kilobytes de tamaño. Por defecto es de unos 64 KB. Ahora imaginemos que necesitamos obtener solo 100 bytes y le pedimos a HBase que nos proporcione estos datos usando una clave determinada. Dado que el tamaño del bloque en HFiles es de 64 KB, la solicitud será 640 veces mayor (¡sólo un minuto!) de lo necesario.

A continuación, dado que la solicitud pasará por HDFS y su mecanismo de almacenamiento en caché de metadatos caché de circuito corto (que permite el acceso directo a los archivos), esto lleva a leer ya 1 MB del disco. Sin embargo, esto se puede ajustar con el parámetro dfs.client.read.shortcircuit.buffer.size y en muchos casos tiene sentido reducir este valor, por ejemplo a 126 KB.

Digamos que hacemos esto, pero además, cuando comenzamos a leer datos a través de la API de Java, como funciones como FileChannel.read y le pedimos al sistema operativo que lea la cantidad especificada de datos, lee "por si acaso" 2 veces más. , es decir. 256 KB en nuestro caso. Esto se debe a que Java no tiene una manera fácil de configurar el indicador FADV_RANDOM para evitar este comportamiento.

Como resultado, para obtener nuestros 100 bytes, se leen 2600 veces más bajo el capó. Parecería que la solución es obvia: reduzcamos el tamaño del bloque a un kilobyte, establezcamos la bandera mencionada y obtengamos una gran aceleración de la iluminación. Pero el problema es que al reducir el tamaño del bloque 2 veces, también reducimos 2 veces el número de bytes leídos por unidad de tiempo.

Se puede obtener cierta ganancia al configurar el indicador FADV_RANDOM, pero solo con un alto nivel de subprocesos múltiples y con un tamaño de bloque de 128 KB, pero esto es un máximo de un par de decenas de por ciento:

Cómo aumentar la velocidad de lectura desde HBase hasta 3 veces y desde HDFS hasta 5 veces

Las pruebas se llevaron a cabo en 100 archivos, cada uno de 1 GB de tamaño y ubicados en 10 discos duros.

Calculemos con qué podemos, en principio, contar a esta velocidad:
Digamos que leemos desde 10 discos a una velocidad de 280 MB/seg, es decir 3 millones de veces 100 bytes. Pero como recordamos, los datos que necesitamos son 2600 veces menos de los que se leen. Por lo tanto, dividimos 3 millones entre 2600 y obtenemos 1100 registros por segundo.

Deprimente, ¿no? esa es la naturaleza Acceso aleatorio acceso a los datos del disco duro, independientemente del tamaño del bloque. Éste es el límite físico del acceso aleatorio y ninguna base de datos puede exprimir más en tales condiciones.

Entonces, ¿cómo logran las bases de datos velocidades mucho más altas? Para responder a esta pregunta, veamos lo que sucede en la siguiente imagen:

Cómo aumentar la velocidad de lectura desde HBase hasta 3 veces y desde HDFS hasta 5 veces

Aquí vemos que durante los primeros minutos la velocidad es realmente de unos mil registros por segundo. Sin embargo, además, debido al hecho de que se lee mucho más de lo solicitado, los datos terminan en el buff/cache del sistema operativo (linux) y la velocidad aumenta a unos más decentes 60 mil por segundo.

Por lo tanto, a continuación nos ocuparemos de acelerar el acceso solo a los datos que están en la memoria caché del sistema operativo o ubicados en dispositivos de almacenamiento SSD/NVMe de velocidad de acceso comparable.

En nuestro caso realizaremos pruebas en un banco de 4 servidores, cada uno de los cuales se carga de la siguiente manera:

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

Y aquí el punto clave es la cantidad de datos de las tablas que deben leerse. El hecho es que si lee datos de una tabla que está completamente ubicada en el caché de HBase, ni siquiera llegará a leer desde el buff/caché del sistema operativo. Porque HBase por defecto asigna el 40% de la memoria a una estructura llamada BlockCache. Básicamente, este es un ConcurrentHashMap, donde la clave es el nombre del archivo + desplazamiento del bloque, y el valor son los datos reales en este desplazamiento.

Por lo tanto, al leer sólo de esta estructura, vemos excelente velocidad, como un millón de solicitudes por segundo. Pero imaginemos que no podemos asignar cientos de gigabytes de memoria sólo para las necesidades de la base de datos, porque hay muchas otras cosas útiles ejecutándose en estos servidores.

Por ejemplo, en nuestro caso, el volumen de BlockCache en un RS es de aproximadamente 12 GB. Colocamos dos RS en un nodo, es decir Se asignan 96 GB para BlockCache en todos los nodos. Y hay muchas veces más datos, por ejemplo, sean 4 tablas, 130 regiones cada una, en las que los archivos tienen un tamaño de 800 MB, comprimidos por FAST_DIFF, es decir. un total de 410 GB (estos son datos puros, es decir, sin tener en cuenta el factor de replicación).

Por lo tanto, BlockCache representa solo alrededor del 23% del volumen total de datos y esto se acerca mucho más a las condiciones reales de lo que se llama BigData. Y aquí es donde comienza la diversión, porque obviamente, cuantas menos visitas al caché, peor será el rendimiento. Después de todo, si fallas, tendrás que trabajar mucho, es decir, Vaya a llamar a las funciones del sistema. Sin embargo, esto no se puede evitar, así que veamos un aspecto completamente diferente: ¿qué sucede con los datos dentro del caché?

Simplifiquemos la situación y supongamos que tenemos un caché que solo cabe en 1 objeto. Aquí un ejemplo de lo que sucederá cuando intentemos trabajar con un volumen de datos 3 veces mayor que el caché, tendremos que:

1. Coloque el bloque 1 en el caché.
2. Eliminar el bloque 1 del caché
3. Coloque el bloque 2 en el caché.
4. Eliminar el bloque 2 del caché
5. Coloque el bloque 3 en el caché.

¡5 acciones completadas! Sin embargo, esta situación no se puede llamar normal; de hecho, estamos obligando a HBase a realizar un montón de trabajo completamente inútil. Lee constantemente datos del caché del sistema operativo, los coloca en BlockCache, solo para descartarlos casi de inmediato porque ha llegado una nueva porción de datos. La animación al principio de la publicación muestra la esencia del problema: Garbage Collector se está saliendo de escala, la atmósfera se está calentando, la pequeña Greta en la lejana y calurosa Suecia se está enojando. Y a nosotros, los informáticos, realmente no nos gusta que los niños estén tristes, así que empezamos a pensar en qué podemos hacer al respecto.

¿Qué pasa si no pones todos los bloques en el caché, sino solo un cierto porcentaje de ellos, para que el caché no se desborde? Comencemos simplemente agregando unas pocas líneas de código al comienzo de la función para colocar datos en BlockCache:

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

El punto aquí es el siguiente: el desplazamiento es la posición del bloque en el archivo y sus últimos dígitos están distribuidos aleatoria y uniformemente de 00 a 99. Por lo tanto, solo omitiremos aquellos que entren en el rango que necesitamos.

Por ejemplo, establezca cacheDataBlockPercent = 20 y vea qué sucede:

Cómo aumentar la velocidad de lectura desde HBase hasta 3 veces y desde HDFS hasta 5 veces

El resultado es obvio. En los gráficos a continuación, queda claro por qué ocurrió tal aceleración: ahorramos una gran cantidad de recursos de GC sin hacer el trabajo de Sísifo de colocar datos en el caché solo para tirarlos inmediatamente por el desagüe de los perros marcianos:

Cómo aumentar la velocidad de lectura desde HBase hasta 3 veces y desde HDFS hasta 5 veces

Al mismo tiempo, la utilización de la CPU aumenta, pero es mucho menor que la productividad:

Cómo aumentar la velocidad de lectura desde HBase hasta 3 veces y desde HDFS hasta 5 veces

También vale la pena señalar que los bloques almacenados en BlockCache son diferentes. La mayoría, alrededor del 95%, son datos en sí mismos. Y el resto son metadatos, como filtros Bloom o LEAF_INDEX y т.д.. Estos datos no son suficientes, pero son muy útiles, porque antes de acceder a los datos directamente, HBase recurre al meta para comprender si es necesario buscar más aquí y, de ser así, dónde se encuentra exactamente el bloque de interés.

Por lo tanto, en el código vemos una condición de verificación. buf.getBlockType().isData() y gracias a este meta, lo dejaremos en caché en cualquier caso.

Ahora aumentemos la carga y ajustemos ligeramente la función de una sola vez. En la primera prueba establecimos el porcentaje de corte = 20 y BlockCache estaba ligeramente subutilizado. Ahora configurémoslo al 23% y agreguemos 100 subprocesos cada 5 minutos para ver en qué punto se produce la saturación:

Cómo aumentar la velocidad de lectura desde HBase hasta 3 veces y desde HDFS hasta 5 veces

Aquí vemos que la versión original casi de inmediato alcanza el techo con aproximadamente 100 mil solicitudes por segundo. Mientras que el parche da una aceleración de hasta 300 mil. Al mismo tiempo, está claro que una mayor aceleración ya no es tan “gratuita”; el uso de la CPU también está aumentando.

Sin embargo, esta no es una solución muy elegante, ya que no sabemos de antemano qué porcentaje de bloques deben almacenarse en caché, depende del perfil de carga. Por lo tanto, se implementó un mecanismo para ajustar automáticamente este parámetro dependiendo de la actividad de las operaciones de lectura.

Se han agregado tres opciones para controlar esto:

hbase.lru.cache.heavy.eviction.count.limit — establece cuántas veces debe ejecutarse el proceso de expulsión de datos del caché antes de comenzar a utilizar la optimización (es decir, omitir bloques). Por defecto es igual a MAX_INT = 2147483647 y de hecho significa que la característica nunca comenzará a funcionar con este valor. Porque el proceso de desalojo comienza cada 5 - 10 segundos (depende de la carga) y 2147483647 * 10/60/60/24/365 = 680 años. Sin embargo, podemos establecer este parámetro en 0 y hacer que la función funcione inmediatamente después del lanzamiento.

Sin embargo, también hay una carga útil en este parámetro. Si nuestra carga es tal que las lecturas a corto plazo (por ejemplo, durante el día) y las lecturas a largo plazo (por la noche) se intercalan constantemente, entonces podemos asegurarnos de que la función esté activada solo cuando se estén realizando operaciones de lectura larga.

Por ejemplo, sabemos que las lecturas de corta duración suelen durar aproximadamente 1 minuto. No es necesario comenzar a descartar bloques, el caché no tendrá tiempo de quedar obsoleto y luego podemos establecer este parámetro en, por ejemplo, 10. Esto conducirá al hecho de que la optimización comenzará a funcionar solo cuando sea largo. El semestre de lectura activa ha comenzado, es decir. en 100 segundos. Por lo tanto, si tenemos una lectura a corto plazo, todos los bloques irán a la caché y estarán disponibles (excepto aquellos que serán desalojados por el algoritmo estándar). Y cuando hacemos lecturas a largo plazo, la función se activa y tendríamos un rendimiento mucho mayor.

hbase.lru.cache.heavy.eviction.mb.size.limit — establece cuántos megabytes nos gustaría colocar en el caché (y, por supuesto, desalojar) en 10 segundos. La función intentará alcanzar este valor y mantenerlo. La cuestión es la siguiente: si metemos gigabytes en la caché, tendremos que desalojarlos, y esto, como vimos anteriormente, es muy caro. Sin embargo, no debes intentar configurarlo demasiado pequeño, ya que esto hará que el modo de salto de bloque salga prematuramente. Para servidores potentes (entre 20 y 40 núcleos físicos), lo óptimo es configurar entre 300 y 400 MB. Para la clase media (~10 núcleos) 200-300 MB. Para sistemas débiles (de 2 a 5 núcleos), 50 a 100 MB pueden ser normales (no probados en estos).

Veamos cómo funciona esto: digamos que configuramos hbase.lru.cache.heavy.eviction.mb.size.limit = 500, hay algún tipo de carga (lectura) y luego cada ~10 segundos calculamos cuántos bytes había desalojado usando la fórmula:

Gastos generales = suma de bytes liberados (MB) * 100 / límite (MB) - 100;

Si de hecho se desalojaron 2000 MB, entonces los gastos generales son iguales a:

2000 * 100 / 500 - 100 = 300%

Los algoritmos intentan mantener no más de unas pocas decenas de porcentaje, por lo que la función reducirá el porcentaje de bloques almacenados en caché, implementando así un mecanismo de ajuste automático.

Sin embargo, si la carga cae, digamos que solo se desalojan 200 MB y el Overhead se vuelve negativo (el llamado exceso):

200 * 100 / 500 - 100 = -60%

Por el contrario, la función aumentará el porcentaje de bloques almacenados en caché hasta que la sobrecarga se vuelva positiva.

A continuación se muestra un ejemplo de cómo se ve esto en datos reales. No hace falta intentar llegar al 0%, es imposible. Es muy bueno cuando está entre el 30 y el 100%, esto ayuda a evitar una salida prematura del modo de optimización durante sobretensiones a corto plazo.

hbase.lru.cache.heavy.eviction.overheadcoficiente — establece la rapidez con la que nos gustaría obtener el resultado. Si sabemos con certeza que nuestras lecturas son en su mayoría largas y no queremos esperar, podemos aumentar esta proporción y obtener un alto rendimiento más rápido.

Por ejemplo, establecemos este coeficiente = 0.01. Esto significa que la sobrecarga (ver arriba) se multiplicará por este número por el resultado resultante y se reducirá el porcentaje de bloques almacenados en caché. Supongamos que Gastos generales = 300% y coeficiente = 0.01, entonces el porcentaje de bloques almacenados en caché se reducirá en un 3%.

También se implementa una lógica de “contrapresión” similar para valores negativos de sobrecarga (sobreimpulso). Dado que siempre son posibles fluctuaciones a corto plazo en el volumen de lecturas y desalojos, este mecanismo le permite evitar una salida prematura del modo de optimización. La contrapresión tiene una lógica invertida: cuanto más fuerte es el exceso, más bloques se almacenan en caché.

Cómo aumentar la velocidad de lectura desde HBase hasta 3 veces y desde HDFS hasta 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;
       }

Veamos ahora todo esto usando un ejemplo real. Tenemos el siguiente script de prueba:

  1. Comencemos a hacer escaneo (25 subprocesos, lote = 100)
  2. Después de 5 minutos, agregue múltiples obtención (25 subprocesos, lote = 100)
  3. Después de 5 minutos, apague la obtención múltiple (solo queda nuevamente el escaneo)

Hacemos dos ejecuciones, primero hbase.lru.cache.heavy.eviction.count.limit = 10000 (que en realidad deshabilita la función) y luego establecemos limit = 0 (la habilita).

En los registros a continuación vemos cómo la función se activa y restablece el exceso al 14-71 %. De vez en cuando, la carga disminuye, lo que activa la contrapresión y HBase vuelve a almacenar en caché más bloques.

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

Los escaneos eran necesarios para mostrar el mismo proceso en forma de un gráfico de la relación entre dos secciones de caché: única (donde los bloques que nunca antes se han solicitado) y múltiples (los datos "solicitados" al menos una vez se almacenan aquí):

Cómo aumentar la velocidad de lectura desde HBase hasta 3 veces y desde HDFS hasta 5 veces

Y finalmente, cómo se ve el funcionamiento de los parámetros en forma de gráfico. A modo de comparación, el caché se desactivó por completo al principio, luego se inició HBase con el almacenamiento en caché y se retrasó el inicio del trabajo de optimización en 5 minutos (30 ciclos de desalojo).

El código completo se puede encontrar en Pull Request HBASE 23887 en github.

Sin embargo, 300 mil lecturas por segundo no es todo lo que se puede lograr con este hardware en estas condiciones. El hecho es que cuando necesita acceder a datos a través de HDFS, se utiliza el mecanismo ShortCircuitCache (en adelante SSC), que le permite acceder a los datos directamente, evitando interacciones de red.

El perfilado mostró que aunque este mecanismo proporciona una gran ganancia, en algún momento también se convierte en un cuello de botella, porque casi todas las operaciones pesadas ocurren dentro de una cerradura, lo que conduce al bloqueo la mayor parte del tiempo.

Cómo aumentar la velocidad de lectura desde HBase hasta 3 veces y desde HDFS hasta 5 veces

Una vez que nos dimos cuenta de esto, nos dimos cuenta de que el problema se puede solucionar creando una serie de SSC independientes:

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

Y luego trabaje con ellos, excluyendo las intersecciones también en el último dígito de desplazamiento:

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

Ahora puedes empezar a probar. Para ello, leeremos archivos de HDFS con una sencilla aplicación multiproceso. Establezca los 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

Y solo lee los archivos:

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 se ejecuta en subprocesos separados y aumentaremos el número de archivos leídos simultáneamente (de 10 a 200 - eje horizontal) y el número de cachés (de 1 a 10 - gráficos). El eje vertical muestra la aceleración que resulta de un aumento en SSC en relación con el caso en el que solo hay un caché.

Cómo aumentar la velocidad de lectura desde HBase hasta 3 veces y desde HDFS hasta 5 veces

Cómo leer el gráfico: el tiempo de ejecución para 100 mil lecturas en bloques de 64 KB con un caché requiere 78 segundos. Mientras que con 5 cachés se necesitan 16 segundos. Aquellos. hay una aceleración de ~5 veces. Como se puede ver en el gráfico, el efecto no es muy notable para un pequeño número de lecturas paralelas, comienza a jugar un papel notable cuando hay más de 50 lecturas de subprocesos. También es notable que aumentar el número de SSC de 6 y superiores dan un aumento de rendimiento significativamente menor.

Nota 1: dado que los resultados de las pruebas son bastante volátiles (ver más abajo), se realizaron 3 ejecuciones y se promediaron los valores resultantes.

Nota 2: La ganancia de rendimiento al configurar el acceso aleatorio es la misma, aunque el acceso en sí es ligeramente más lento.

Sin embargo, es necesario aclarar que, a diferencia del caso de HBase, esta aceleración no siempre es gratuita. Aquí "desbloqueamos" la capacidad de la CPU para trabajar más, en lugar de depender de candados.

Cómo aumentar la velocidad de lectura desde HBase hasta 3 veces y desde HDFS hasta 5 veces

Aquí puede observar que, en general, un aumento en el número de cachés produce un aumento aproximadamente proporcional en la utilización de la CPU. Sin embargo, hay un poco más de combinaciones ganadoras.

Por ejemplo, echemos un vistazo más de cerca a la configuración SSC = 3. El aumento en el rendimiento en el rango es aproximadamente 3.3 veces. A continuación se muestran los resultados de las tres ejecuciones separadas.

Cómo aumentar la velocidad de lectura desde HBase hasta 3 veces y desde HDFS hasta 5 veces

Mientras que el consumo de CPU aumenta aproximadamente 2.8 veces. La diferencia no es muy grande, pero la pequeña Greta ya está feliz y quizás tenga tiempo para ir a la escuela y tomar lecciones.

Por lo tanto, esto tendrá un efecto positivo para cualquier herramienta que utilice acceso masivo a HDFS (por ejemplo Spark, etc.), siempre que el código de la aplicación sea liviano (es decir, el complemento esté en el lado del cliente HDFS) y haya energía de CPU libre. . Para comprobarlo, probemos qué efecto tendrá el uso combinado de la optimización BlockCache y el ajuste SSC para la lectura desde HBase.

Cómo aumentar la velocidad de lectura desde HBase hasta 3 veces y desde HDFS hasta 5 veces

Se puede ver que en tales condiciones el efecto no es tan grande como en las pruebas refinadas (lectura sin ningún procesamiento), pero aquí es muy posible exprimir 80K adicionales. Juntas, ambas optimizaciones proporcionan una aceleración de hasta 4 veces.

También se realizó un PR para esta optimización. [HDFS-15202], que se ha fusionado y esta funcionalidad estará disponible en versiones futuras.

Y, finalmente, fue interesante comparar el rendimiento de lectura de una base de datos de columnas anchas similar, Cassandra y HBase.

Para hacer esto, lanzamos instancias de la utilidad de prueba de carga estándar YCSB desde dos hosts (800 subprocesos en total). En el lado del servidor: 4 instancias de RegionServer y Cassandra en 4 hosts (no aquellos donde se ejecutan los clientes, para evitar su influencia). Las lecturas provinieron de tablas de tamaño:

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

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

Aquellos. el volumen era aproximadamente el mismo (en HBase un poco más).

Parámetros de HBase:

dfs.cliente.cortocircuito.num = 5 (Optimización del cliente HDFS)

hbase.lru.cache.heavy.eviction.count.limit = 30 - esto significa que el parche comenzará a funcionar después de 30 desalojos (~5 minutos)

hbase.lru.cache.heavy.eviction.mb.size.limit = 300 — volumen objetivo de almacenamiento en caché y desalojo

Los registros de YCSB se analizaron y compilaron en gráficos de Excel:

Cómo aumentar la velocidad de lectura desde HBase hasta 3 veces y desde HDFS hasta 5 veces

Como puede ver, estas optimizaciones permiten comparar el rendimiento de estas bases de datos en estas condiciones y lograr 450 mil lecturas por segundo.

Esperamos que esta información pueda ser útil para alguien durante la apasionante lucha por la productividad.

Fuente: habr.com

Añadir un comentario