sql >> Base de Datos >  >> NoSQL >> HBase

Ajuste de la recolección de basura de Java para HBase

Esta publicación invitada del arquitecto de rendimiento de Intel Java Eric Kaczmarek (publicada originalmente aquí) explora cómo ajustar la recolección de elementos no utilizados (GC) de Java para Apache HBase centrándose en lecturas 100 % YCSB.

Apache HBase es un proyecto de código abierto de Apache que ofrece almacenamiento de datos NoSQL. A menudo utilizado junto con HDFS, HBase se usa ampliamente en todo el mundo. Los usuarios conocidos incluyen Facebook, Twitter, Yahoo y más. Desde la perspectiva del desarrollador, HBase es una "base de datos distribuida, versionada y no relacional modelada a partir de Bigtable de Google, un sistema de almacenamiento distribuido para datos estructurados". HBase puede manejar fácilmente un rendimiento muy alto mediante la ampliación (es decir, la implementación en un servidor más grande) o la ampliación (es decir, la implementación en más servidores).

Desde el punto de vista del usuario, la latencia de cada consulta es muy importante. A medida que trabajamos con los usuarios para probar, ajustar y optimizar las cargas de trabajo de HBase, encontramos un número significativo ahora que realmente quiere latencias de operación del percentil 99. Eso significa un viaje de ida y vuelta, desde la solicitud del cliente hasta la respuesta al cliente, todo en 100 milisegundos.

Varios factores contribuyen a la variación de la latencia. Uno de los intrusos de latencia más devastadores e impredecibles son las pausas de "parar el mundo" de la máquina virtual Java (JVM) para la recolección de basura (limpieza de la memoria).

Para abordar eso, probamos algunos experimentos con el colector Oracle jdk7u21 y jdk7u60 G1 (Garbage 1st). El sistema de servidor que utilizamos se basó en procesadores Intel Xeon Ivy-bridge EP con Hyper-threading (40 procesadores lógicos). Tenía 256 GB de RAM DDR3-1600 y tres SSD de 400 GB como almacenamiento local. Esta pequeña configuración contenía un maestro y un esclavo, configurados en un solo nodo con la carga escalada adecuadamente. Utilizamos la versión 0.98.1 de HBase y un sistema de archivos local para el almacenamiento de HFile. La tabla de prueba HBase se configuró como 400 millones de filas y tenía un tamaño de 580 GB. Usamos la estrategia de montón HBase predeterminada:40 % para blockcache, 40 % para memstore. YCSB se utilizó para impulsar 600 subprocesos de trabajo que enviaban solicitudes al servidor HBase.

Los siguientes gráficos muestran jdk7u21 ejecutándose al 100 % durante una hora usando -XX:+UseG1GC -Xms100g -Xmx100g -XX:MaxGCPauseMillis=100 . Especificamos el recolector de basura a usar, el tamaño del almacenamiento dinámico y el tiempo de pausa deseado para la recolección de basura (GC) "detener el mundo".

Figura 1:Cambios bruscos en el tiempo de pausa de GC

En este caso, obtuvimos pausas de GC muy cambiantes. La pausa del GC tuvo un rango de 7 milisegundos a 5 segundos completos después de un pico inicial que alcanzó los 17,5 segundos.

El siguiente gráfico muestra más detalles, durante el estado estacionario:

Figura 2:Detalles de la pausa del GC, durante el estado estacionario

La Figura 2 nos dice que las pausas del GC en realidad se dividen en tres grupos diferentes:(1) entre 1 y 1,5 segundos; (2) entre 0,007 segundos y 0,5 segundos; (3) picos entre 1,5 segundos y 5 segundos. Esto fue muy extraño, por lo que probamos el jdk7u60 lanzado más recientemente para ver si los datos serían diferentes:

Ejecutamos las mismas pruebas de lectura al 100 % usando exactamente los mismos parámetros de JVM:-XX:+UseG1GC -Xms100g -Xmx100g -XX:MaxGCPauseMillis=100 .

Figura 3:gestión muy mejorada de los picos de tiempo de pausa

Jdk7u60 mejoró en gran medida la capacidad de G1 para manejar picos de tiempo de pausa después del pico inicial durante la etapa de asentamiento. Jdk7u60 hizo 1029 GC jóvenes y mixtos durante una carrera de una hora. GC sucedió aproximadamente cada 3,5 segundos. Jdk7u21 hizo 286 GC con cada GC cada 12,6 segundos. Jdk7u60 pudo administrar el tiempo de pausa entre 0,302 y 1 segundo sin picos importantes.

La Figura 4, a continuación, nos da una mirada más cercana a las pausas de 150 GC durante el estado estable:

Figura 4:Mejor, pero no lo suficientemente bueno

Durante el estado estable, jdk7u60 pudo mantener el tiempo de pausa promedio en alrededor de 369 milisegundos. Era mucho mejor que jdk7u21, pero aun así no cumplió con nuestro requisito de 100 milisegundos dado por –Xx:MaxGCPauseMillis=100 .

Para determinar qué más podíamos hacer para obtener nuestro tiempo de pausa de 100 millones de segundos, necesitábamos comprender más sobre el comportamiento de la administración de memoria de JVM y el recolector de basura G1 (Garbage First). Las siguientes figuras muestran cómo funciona G1 en la colección Young Gen.

Figura 5:Diapositiva de la presentación JavaOne de 2012 de Charlie Hunt y Monica Beckwith:"Ajuste del rendimiento del recolector de basura G1"

Cuando se inicia la JVM, en función de los parámetros de inicio de la JVM, le pide al sistema operativo que asigne una gran porción de memoria continua para alojar el montón de la JVM. Ese trozo de memoria es dividido por la JVM en regiones.

Figura 6:Diapositiva de la presentación JavaOne de 2012 de Charlie Hunt y Monica Beckwith:"Ajuste del rendimiento del recolector de basura G1"

Como muestra la Figura 6, cada objeto que el programa Java asigna mediante la API de Java llega primero al espacio Eden en la generación Young de la izquierda. Después de un tiempo, el Edén se llena y se activa un GC de generación joven. Los objetos a los que todavía se hace referencia (es decir, "vivos") se copian en el espacio Survivor. Cuando los objetos sobreviven a varios GC en la generación joven, se promocionan al espacio de la generación anterior.

Cuando ocurre Young GC, los subprocesos de la aplicación Java se detienen para marcar y copiar objetos en vivo de forma segura. Estas paradas son las notorias pausas de GC "parar el mundo", que hacen que las aplicaciones no respondan hasta que terminan las pausas.

Figura 7:Diapositiva de la presentación JavaOne de 2012 de Charlie Hunt y Monica Beckwith:"Ajuste del rendimiento del recolector de basura G1"

La vieja generación también puede abarrotarse. En un nivel determinado, controlado por -XX:InitiatingHeapOccupancyPercent=? donde el valor predeterminado es el 45 % del montón total:se activa un GC mixto. Recoge tanto Young gen como Old gen. Las pausas de GC mixtas se controlan según el tiempo que tarda la generación joven en limpiarse cuando ocurre una GC mixta.

Entonces, podemos ver en G1, las pausas de GC de "detener el mundo" están dominadas por la rapidez con que G1 puede marcar y copiar objetos vivos fuera del espacio de Eden. Con esto en mente, analizaremos cómo el patrón de asignación de memoria HBase nos ayudará a ajustar G1 GC para obtener nuestra pausa deseada de 100 milisegundos.

En HBase, hay dos estructuras en memoria que consumen la mayor parte de su montón:BlockCache , almacenando en caché bloques de archivos HBase para operaciones de lectura y Memstore almacenando en caché las últimas actualizaciones.

Figura 8:en HBase, dos estructuras en memoria consumen la mayor parte de su montón.

La implementación predeterminada de BlockCache de HBase es el LruBlockCache , que simplemente utiliza una gran matriz de bytes para alojar todos los bloques HBase. Cuando se "desalojan" los bloques, se elimina la referencia al objeto Java de ese bloque, lo que permite que el GC reubique la memoria.

Nuevos objetos que forman el LruBlockCache y Memstore ve primero al espacio Edén de la generación joven. Si viven lo suficiente (es decir, si no son desalojados de LruBlockCache o eliminado de Memstore), luego, después de varias generaciones jóvenes de GC, se abren paso a la generación anterior del montón de Java. Cuando el espacio libre de la generación anterior es menor que un threshOld dado (InitiatingHeapOccupancyPercent para empezar), el GC mixto se activa y elimina algunos objetos inactivos en la generación anterior, copia objetos vivos de la generación joven y vuelve a calcular el Eden de la generación joven y el HeapOccupancyPercent de la generación anterior. . Eventualmente, cuando HeapOccupancyPercent alcanza un cierto nivel, un FULL GC sucede, lo que hace que el GC de "detenga el mundo" se detenga para limpiar todos los objetos muertos dentro de la generación anterior.

Después de estudiar el registro de GC producido por “-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintAdaptiveSizePolicy “, notamos HeapOccupancyPercent nunca creció lo suficiente como para inducir un GC completo durante la lectura del 100 % de HBase. Las pausas de GC que vimos estaban dominadas por pausas de "detener el mundo" de la generación joven y el procesamiento de referencia creciente a lo largo del tiempo.

Al completar ese análisis, hicimos tres grupos de cambios en la configuración predeterminada de GC G1:

  1. Utilice -XX:+ParallelRefProcEnabled Cuando esta marca está activada, GC utiliza varios subprocesos para procesar las referencias en aumento durante la GC joven y mixta. Con esta marca para HBase, el tiempo de remarcado del GC se reduce en un 75 % y el tiempo de pausa general del GC se reduce en un 30 %.
  2. Set -XX:-ResizePLAB and -XX:ParallelGCThreads=8+(logical processors-8)(5/8) Los amortiguadores de asignación local de promoción (PLAB) se utilizan durante la recopilación de Young. Se utilizan varios hilos. Es posible que cada subproceso deba asignar espacio para los objetos que se copian en Survivor o Old space. Se requieren PLAB para evitar la competencia de subprocesos por estructuras de datos compartidas que administran la memoria libre. Cada subproceso GC tiene un PLAB para el espacio Supervivencia y otro para el espacio Antiguo. Nos gustaría dejar de cambiar el tamaño de los PLAB para evitar el gran costo de comunicación entre los subprocesos de GC, así como las variaciones durante cada GC. 5/8). Esta fórmula fue recomendada recientemente por Oracle. Con ambas configuraciones, podemos ver pausas de GC más suaves durante la ejecución.
  3. Cambiar -XX:G1NewSizePercent valor predeterminado de 5 a 1 para un montón de 100 GB Basado en la salida de -XX:+PrintGCDetails and -XX:+PrintAdaptiveSizePolicy , notamos que la razón por la que G1 no cumplió con nuestro tiempo de pausa deseado de 100GC fue el tiempo que tomó procesar Eden. En otras palabras, G1 tardó un promedio de 369 milisegundos en vaciar 5 GB de Eden durante nuestras pruebas. Luego cambiamos el tamaño de Eden usando -XX:G1NewSizePercent=
    marca de 5 a 1. Con este cambio, vimos que el tiempo de pausa del GC se redujo a 100 milisegundos.

A partir de este experimento, descubrimos que la velocidad de G1 para limpiar Eden es de aproximadamente 1 GB por 100 milisegundos, o 10 GB por segundo para la configuración de HBase que usamos.

Según esa velocidad, podemos configurar -XX:G1NewSizePercent=
por lo que el tamaño de Eden se puede mantener alrededor de 1 GB. Por ejemplo:

  • Montón de 32 GB, -XX:G1NewSizePercent=3
  • Montón de 64 GB, –XX:G1NewSizePercent=2
  • Montón de 100 GB y superior, -XX:G1NewSizePercent=1
  • Así que nuestras opciones finales de línea de comandos para HRegionserver son:
    • -XX:+UseG1GC
    • -Xms100g -Xmx100g (Tamaño de almacenamiento dinámico utilizado en nuestras pruebas)
    • -XX:MaxGCPauseMillis=100 (Tiempo de pausa deseado del GC en las pruebas)
    • XX:+ParallelRefProcEnabled
    • -XX:-ResizePLAB
    • -XX:ParallelGCThreads= 8+(40-8)(5/8)=28
    • -XX:G1NewSizePercent=1

Aquí está el gráfico de tiempo de pausa del GC para ejecutar una operación de lectura del 100 % durante 1 hora:

Figura 9:Los picos de asentamiento inicial más altos se redujeron en más de la mitad.

En este gráfico, incluso los picos de establecimiento iniciales más altos se redujeron de 3,792 segundos a 1,684 segundos. La mayoría de los picos iniciales fueron de menos de 1 segundo. Después del acuerdo, GC pudo mantener el tiempo de pausa en alrededor de 100 milisegundos.

El siguiente cuadro compara las ejecuciones de jdk7u60 con y sin ajuste, durante el estado estable:

Figura 10:jdk7u60 se ejecuta con y sin afinación, durante el estado estable.

El ajuste simple del GC que describimos anteriormente brinda tiempos ideales de pausa del GC, alrededor de 100 milisegundos, con un promedio de 106 milisegundos y una desviación estándar de 7 milisegundos.

Resumen

HBase es una aplicación crítica para el tiempo de respuesta que requiere que el tiempo de pausa del GC sea predecible y manejable. Con Oracle jdk7u60, según la información de GC notificada por -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintAdaptiveSizePolicy , podemos ajustar el tiempo de pausa del GC a los 100 milisegundos que deseemos.

Eric Kaczmarek es un arquitecto de rendimiento de Java en el Grupo de soluciones de software de Intel. Lidera el esfuerzo en Intel para habilitar y optimizar los marcos de Big Data (Hadoop, HBase, Spark, Cassandra) para las plataformas Intel.

Es posible que el software y las cargas de trabajo utilizadas en las pruebas de rendimiento se hayan optimizado para el rendimiento solo en microprocesadores Intel. Las pruebas de rendimiento, como SYSmark y MobileMark, se miden utilizando sistemas informáticos, componentes, software, operaciones y funciones específicos. Cualquier cambio en cualquiera de esos factores puede hacer que los resultados varíen. Debe consultar otra información y pruebas de rendimiento para ayudarlo a evaluar completamente sus compras contempladas, incluido el rendimiento de ese producto cuando se combina con otros productos.

Los números de procesador de Intel no son una medida de rendimiento. Los números de procesador diferencian las funciones dentro de cada familia de procesadores. No en diferentes familias de procesadores. Vaya a:http://www.intel.com/products/processor_number.

Copyright 2014 Intel Corp. Intel, el logotipo de Intel y Xeon son marcas comerciales de Intel Corporation en EE. UU. y/o en otros países.