El SLA de Dewu ZooKeeper también puede ser del 99,99%

1. Antecedentes

ZooKeeper (ZK) es un servicio de coordinación de aplicaciones distribuidas nacido en 2007. Aunque por algunas razones históricas especiales, muchos escenarios empresariales todavía tienen que depender de él. Por ejemplo, Kafka, programación de tareas, etc. Especialmente cuando la implementación mixta de Flink y el desacoplamiento de ETCD, el lado comercial requería estabilidad absoluta y recomendaba encarecidamente no utilizar ZooKeeper de construcción propia. Por consideraciones de estabilidad, se utiliza MSE-ZK de Alibaba. Desde su uso en septiembre de 2022, el equipo técnico de Dewu no ha encontrado ningún problema de estabilidad y, de hecho, la confiabilidad del SLA ha alcanzado el 99,99%.

En 2023, algunas empresas utilizaron clústeres ZooKeeper (ZK) de construcción propia, y luego ZK experimentó varias fluctuaciones durante el uso. Luego, Dewu SRE comenzó a hacerse cargo de algunos clústeres de construcción propia e hizo varias rondas de intentos de refuerzo de estabilidad. Durante el proceso de adquisición, se descubrió que después de que ZooKeeper se haya estado ejecutando durante un período de tiempo, el uso de memoria seguirá aumentando, lo que puede provocar fácilmente problemas de falta de memoria (OOM). El equipo técnico de Dewu sentía mucha curiosidad por este fenómeno y por ello participó en el proceso de exploración para solucionar este problema.

2. Exploración y análisis

2.1 Determinar la dirección

Al solucionar el problema, tuve mucha suerte de encontrar un sitio de falla en un entorno de prueba. Dos nodos en el clúster estaban en un estado de borde de OOM.

En el caso de la escena del fallo, normalmente sólo queda el 50% antes del punto final exitoso. La memoria es alta Según la experiencia pasada, o no es del montón o hay un problema en el montón. Se puede confirmar a partir del gráfico de llamas y jstat que se trata de un problema en el montón.

 
 

Como se muestra en la figura: significa que un determinado recurso en el montón de JVM ocupa una gran cantidad de memoria y FGC no puede liberarlo.

2.2 Análisis de la memoria

Para explorar la distribución del uso de memoria en el montón de JVM, el equipo técnico de Dewu realizó inmediatamente un volcado del montón de JVM. El análisis encontró que la memoria JVM está muy ocupada por childWatches y dataWatches.

 

 

dataWatches: realiza un seguimiento de los cambios en los datos del nodo znode.
childWatches: realiza un seguimiento de los cambios en la estructura del nodo znode (árbol).

childWatches y dataWatches se originan en WatcherManager.

Después de la investigación de datos, se descubrió que WatcherManager es el principal responsable de administrar los Watchers. El cliente ZooKeeper (ZK) primero registra los Vigilantes en el servidor ZooKeeper y luego el servidor ZooKeeper utiliza WatcherManager para administrar todos los Vigilantes. Cuando los datos de un Znode cambian, WatchManager activará el Watcher correspondiente y se comunicará con el socket del cliente ZooKeeper suscrito a Znode. Posteriormente, el administrador de Watch del cliente activará la devolución de llamada de Watcher relevante para ejecutar la lógica de procesamiento correspondiente, completando así todo el proceso de publicación/suscripción de datos.

Un análisis más detallado de WatchManager muestra que la proporción de memoria de las variables miembro Watch2Path y WatchTables es tan alta como (18,88 + 9,47)/31,82 = 90%.

WatchTables y Watch2Path almacenan la relación de mapeo exacta entre ZNode y Watcher, como se muestra en el diagrama de estructura de almacenamiento:

WatchTables [Tabla de consultas directas] HashMap>
Escenario: cuando un ZNode cambia, el observador suscrito al ZNode recibirá una notificación.
Lógica: use este ZNode para encontrar todas las listas de Vigilantes correspondientes a través de WatchTables y luego envíe notificaciones una por una.
Watch2Paths [Tabla de consulta inversa]
Escenario HashMap
: cuente a qué ZNodes se ha suscrito un determinado Watcher.
Lógica: utilice este Watcher para encontrar todas las listas de ZNode correspondientes a través de Watch2Paths.
Watcher es esencialmente NIOServerCnxn, que puede entenderse como una sesión de conexión.

Si la cantidad de ZNodes y Watchers es relativamente grande y el cliente se suscribe a más ZNodes, incluso la suscripción completa. ¡La relación registrada en estas dos tablas Hash crecerá exponencialmente y eventualmente alcanzará un volumen altísimo!

Cuando esté completamente suscrito, como se muestra en la figura:

Cuando el número de ZNodes: 3, el número de Watchers: 2, WatchTables y Watch2Paths tendrán cada uno 6 relaciones.

Cuando el número de ZNodes: 4, el número de Watchers: 3, WatchTables y Watch2Paths tendrán cada uno 12 relaciones.

Se descubrió un nodo ZK anormal mediante el monitoreo. La cantidad de ZNodes es de aproximadamente 20 W y la cantidad de Watchers es de 5000. El número de relaciones entre Watcher y ZNode ha alcanzado los 100 millones.

Si se necesita un HashMap&Node (32Byte) para almacenar cada relación, dado que hay dos tablas de relaciones, duplíquelo. Entonces no calcule nada más. Este "shell" por sí solo requiere 2*10000^2*32/1024^3 = 5,9 GB de sobrecarga de memoria no válida.

2.3 Descubrimiento inesperado

Del análisis anterior, podemos saber que es necesario evitar que el cliente se suscriba por completo a todos los ZNodes. Sin embargo, la realidad es que muchos códigos comerciales tienen esa lógica para atravesar todos los ZNodes comenzando desde el nodo raíz de ZTree y suscribirse completamente a ellos.

Puede ser posible persuadir a algunas partes comerciales para que realicen mejoras, pero no se puede obligar a restringir el uso de todas las partes comerciales. Por tanto, la solución a este problema pasa por el seguimiento y la prevención. Sin embargo, desafortunadamente, ZK no admite dicha función, lo que requiere la modificación del código fuente de ZK.

Mediante el seguimiento y análisis del código fuente, se descubrió que la raíz del problema apuntaba a WatchManager y se estudiaron cuidadosamente los detalles lógicos de esta clase. Después de una comprensión profunda, descubrí que la calidad de este código parecía haber sido escrita por un recién graduado y que había muchos usos inapropiados de subprocesos y bloqueos. Al observar los registros de Git, encontramos que este problema se remonta a 2007. Sin embargo, lo interesante es que durante este período de tiempo, apareció WatchManagerOptimized (2018). Al buscar en la información de la comunidad ZK, se descubrió [ZOOKEEPER-1177]. Es decir, en 2011, la comunidad ZK se dio cuenta de que había una gran cantidad de. Los relojes causaron problemas de uso de memoria y finalmente proporcionaron una solución en 2018. Es precisamente por este WatchManagerOptimized  que parece que la comunidad ZK ya lo ha optimizado.

Curiosamente, ZK no habilita esta clase de forma predeterminada, incluso en la última versión 3.9.X, WatchManager todavía se usa de forma predeterminada. Quizás porque ZK es tan antiguo, la gente poco a poco le presta menos atención. Al preguntar a los colegas de Alibaba, se confirmó que MSE-ZK también habilitó WatchManagerOptimized, lo que confirmó aún más que el enfoque del equipo técnico de Dewu estaba en la dirección correcta.

2.4 Exploración de optimización

Optimización de bloqueo

En la versión predeterminada, el HashSet utilizado no es seguro para subprocesos. En esta versión, los métodos de operación relacionados, como addWatch, removeWatcher y triggerWatch, se implementan agregando bloqueos pesados ​​sincronizados a los métodos. En la versión optimizada, se utiliza una combinación de ConcurrentHashMap y ReadWriteLock para utilizar el mecanismo de bloqueo de una manera más refinada. De esta manera, se pueden lograr operaciones más eficientes durante el proceso de agregar Watch y activar Watch.

Optimización del almacenamiento

Este es el enfoque. Del análisis de WatchManager, podemos ver que la eficiencia de almacenamiento al usar WatchTables y Watch2Paths no es alta. Si ZNode tiene muchas relaciones de suscripción, se consumirá una gran cantidad de memoria adicional no válida.

Sorprendentemente, WatchManagerOptimized utiliza "tecnología negra" -> mapa de bits aquí.

El almacenamiento relacional está fuertemente comprimido utilizando mapas de bits para lograr la optimización de la reducción de dimensionalidad.

Características principales de Java BitSet:

  • Ahorro de espacio: BitSet utiliza matrices de bits para almacenar datos, lo que requiere menos espacio que las matrices booleanas estándar.
  • Procesamiento rápido: realizar operaciones bit a bit (como AND, OR, XOR, voltear) suele ser más rápido que las operaciones lógicas booleanas correspondientes.
  • Expansión dinámica: el tamaño de un BitSet puede crecer dinámicamente según sea necesario para acomodar más bits.

BitSet utiliza palabras largas [] para almacenar datos. El tipo largo ocupa  8 bytes y tiene 64 bits . Cada elemento de la matriz puede almacenar  64  datos. El orden de almacenamiento de los datos en la matriz es de izquierda a derecha, de menor a mayor. Por ejemplo, la capacidad de palabras del BitSet en la figura siguiente es 4, las palabras [0] de menor a mayor indican si existen los datos 0 ~ 63, las palabras [1] de menor a mayor indican si existen los datos 64 ~ 127, y así en. Entre ellos, palabras [1] = 8, y el bit binario correspondiente 8 es 1, lo que indica que hay datos {67} almacenados en BitSet en este momento.

WatchManagerOptimized usa BitMap para almacenar todos los Watchers. De esta forma, incluso si hay un Vigilante de 1W. El consumo de memoria del mapa de bits es de solo 8 Byte*1W/64/1024= 1,2 KB . Si se reemplaza por HashSet, se requieren al menos 32 Byte * 10000/1024 = 305 KB y la eficiencia del almacenamiento es casi 300 veces diferente.

WatchManager.java:
private final Map<String, Set<Watcher>> watchTable = new HashMap<>();
private final Map<Watcher, Set<String>> watch2Paths = new HashMap<>();

 

WatchManagerOptimized.java:
private final ConcurrentHashMap<String, BitHashSet> pathWatches = new ConcurrentHashMap<String, BitHashSet>();
private final BitMap<Watcher> watcherBitIdMap = new BitMap<Watcher>();

El almacenamiento de mapeo de ZNode a Watcher se cambia de Map a ConcurrentHashMapBitHashSet>. Es decir, el conjunto ya no se almacena, pero el mapa de bits se utiliza para almacenar el valor del índice del mapa de bits.

Utilice 1W ZNode, 1W Watcher y vaya al punto extremo de suscripción completa (todos los Watchers se suscriben a todos los ZNodes) para realizar PK de eficiencia de almacenamiento:

Puede ver que  11,7 MB PK 5,9 GB , la diferencia en la eficiencia del almacenamiento de memoria es: 516 veces .

Optimización lógica

Agregar un monitor: ambas versiones pueden completar operaciones en tiempo constante, pero la versión optimizada  proporciona un mejor rendimiento de concurrencia mediante el uso de ConcurrentHashMap  .

Eliminación de un monitor: es posible que la versión predeterminada deba recorrer toda la colección de monitores para buscar y eliminar el monitor, lo que genera una complejidad temporal de O(n). La versión optimizada utiliza  BitSet y ConcurrentHashMap para localizar y eliminar rápidamente monitores en O(1) en la mayoría de los casos.

Activación de monitores: la versión predeterminada es más compleja porque requiere operaciones en cada monitor en cada ruta. La versión optimizada optimiza el rendimiento de los monitores de activación a través de estructuras de datos más eficientes y un uso reducido de bloqueos.

3. Prueba de estrés de desempeño

3.1 Microbenchmark JMH

Compilación del código fuente de ZooKeeper 3.6.4, prueba de estrés del micrófono JMH WatchBench.

pathCount: indica el número de rutas de ZNode utilizadas en la prueba. watchManagerClass: representa la clase de implementación WatchManager utilizada en la prueba.
watcherCount: Indica el número de observadores (Watchers) utilizados en la prueba.
Modo: indica el modo de prueba, aquí está avgt, que indica el tiempo de ejecución promedio.
Cnt: Indica el número de ejecuciones de prueba.
Puntuación: Indica la puntuación de la prueba, es decir, el tiempo medio de ejecución.
Error: Indica el rango de error de la puntuación.
Unidades: la unidad que representa la puntuación, aquí es milisegundos/operación (ms/op).
  • Hay 1 millón de suscripciones entre ZNode y Watcher. La versión predeterminada usa 50 MB y la versión optimizada solo requiere 0,2 MB y no aumentará linealmente.
  • Al agregar Watch, la versión optimizada (0,406 ms/op) es 6,5 veces más rápida que la versión predeterminada (2,669 ms/op).
  • Se activa una gran cantidad de vigilancias y la versión optimizada (17,833 ms/op) es 5 veces más rápida que la versión predeterminada (84,455 ms/op).

3.2 Prueba de estrés de desempeño

A continuación, se construyó un conjunto de ZooKeeper 3.6.4 de 3 nodos en una máquina (32C 60G) y se utilizaron la versión optimizada y la versión predeterminada para comparar las pruebas de estrés de capacidad.

Escenario 1: ruta corta de znode de 20 W

Ruta corta de Znode: /demo/znode1

Escenario 2: ruta larga de znode de 20 W

Ruta larga de Znode: /sentinel-cluster/dev/xx-admin-interfaces/lock/_c_bb0832d5-67a5-48ab-8fe0-040b9ddea-lock/12

  • El uso de la memoria de vigilancia está relacionado con la longitud de la ruta de ZNode.
  • La cantidad de relojes aumenta linealmente en la versión predeterminada y funciona muy bien en la versión optimizada, lo cual es una mejora muy obvia para la optimización del uso de la memoria.

3.3 Prueba de escala de grises

Según las pruebas comparativas y de capacidad anteriores, la versión optimizada tiene una optimización de memoria obvia en una gran cantidad de escenarios de Watch. A continuación, comenzamos a realizar observaciones de prueba de actualización en escala de grises en el clúster ZK en el entorno de prueba.

El primer clúster de ZooKeeper y sus beneficios

Versión predeterminada

Versión optimizada

 

Efecto ingreso:

  • elección_time (tiempo de elección): reducido en un 60%
  • fsync_time (tiempo de sincronización de transacciones): reducido en un 75%
  • Uso de memoria: reducido en un 91%

Segundo grupo de ZooKeeper y beneficios

 
 
 

Efecto ingreso:

  • Memoria: antes del cambio, la respuesta JVM Attach no respondió y la recopilación de datos falló.
  • elección_time (tiempo de elecciones): reducido en un 64%.
  • max_latency (latencia de lectura): reducida en un 53%.
  • propuesta_latencia (retraso de la propuesta de procesamiento electoral): 1400000 ms --> 43 ms.
  • propagation_latency (retardo de propagación de datos): 1400000 ms --> 43 ms.

El tercer conjunto de clústeres y beneficios de ZooKeeper

Versión predeterminada

Versión optimizada

 
 

Efecto ingreso:

  • Memoria: Ahorre 89%
  • elección_time (tiempo de elecciones): reducido en un 42%
  • max_latency (latencia de lectura): reducida en un 95%
  • propuesta_latencia (retraso de la propuesta de procesamiento electoral): 679999 ms --> 0,3 ms
  • propagation_latency (retardo de propagación de datos): 928000 ms--> 5 ms

4. Resumen

A través de pruebas comparativas anteriores, pruebas de estrés de rendimiento y pruebas de escala de grises, se descubrió WatchManagerOptimized de ZooKeeper. Esta optimización no solo ahorra memoria, sino que también mejora significativamente indicadores como la elección y la sincronización de datos entre nodos mediante la optimización de bloqueo, mejorando así la coherencia de ZooKeeper. También tuvimos intercambios en profundidad con estudiantes de Alibaba MSE, cada uno de ellos simuló pruebas de estrés en escenarios extremos y llegamos a un consenso: WatchManagerOptimized mejora significativamente la estabilidad de ZooKeeper. En general, esta optimización mejora el SLA de ZooKeeper en un orden de magnitud.

ZooKeeper tiene muchas opciones de configuración, pero en la mayoría de los casos no es necesario realizar ningún ajuste. Para mejorar la estabilidad del sistema, se recomienda realizar las siguientes optimizaciones de configuración:

  • Monte dataDir (directorio de datos) y dataLogDir (directorio de registro de transacciones) en diferentes discos respectivamente y utilice almacenamiento en bloque de alto rendimiento.
  • Para ZooKeeper versión 3.8, se recomienda usar JDK 17 y habilitar el recolector de basura ZGC; para las versiones 3.5 y 3.6, se recomienda usar JDK 8 y habilitar el recolector de basura G1. Para estas versiones, simplemente configure -Xms y -Xmx.
  • Ajuste el valor predeterminado del parámetro SnapshotCount de 100.000 a 500.000, lo que puede reducir significativamente la presión del disco cuando ZNode cambia a altas frecuencias.
  • Utilice la versión optimizada de Watch Manager WatchManagerOptimized.

Enlace original

Este artículo es contenido original de Alibaba Cloud y no puede reproducirse sin permiso.

Los estudiantes de secundaria crean su propio lenguaje de programación de código abierto como una ceremonia de mayoría de edad: comentarios agudos de los internautas: confiando en la defensa, Apple lanzó el chip M4 RustDesk Los servicios nacionales fueron suspendidos debido al fraude desenfrenado Yunfeng renunció a Alibaba. En el futuro, planea producir un juego independiente en la plataforma Windows Taobao (taobao.com). Reiniciar el trabajo de optimización de la versión web, destino de los programadores, Visual Studio Code 1.89 lanza Java 17, la versión Java LTS más utilizada, Windows 10 tiene un cuota de mercado del 70%, Windows 11 continúa disminuyendo Open Source Daily | Google apoya a Hongmeng para que se haga cargo; Rabbit R1 de código abierto respalda los teléfonos Android; Haier Electric ha cerrado la plataforma abierta;
{{o.nombre}}
{{m.nombre}}

Supongo que te gusta

Origin my.oschina.net/yunqi/blog/11105634
Recomendado
Clasificación