Dewu ZooKeeper SLA también puede ser 99,99% 丨 Mejores prácticas

Autor: bruce

fondo

El caso compartido hoy proviene de las mejores prácticas en la aplicación de MSE-ZooKeeper por parte del equipo técnico de Dewu. El SLA original de Dewu ZooKeeper también puede ser 99,99% | Tecnología Dewu .

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.

Explorar y analizar

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.

Análisis de 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 búsqueda directa]

HashMap<ZNodo, HashSet<Observador>>

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 búsqueda inversa]

HashMap<Observador, HashSet>

Escenario: Cuente a qué ZNodes se ha suscrito un determinado Vigilante.

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 hay una gran cantidad de ZNodes y Watchers, y el cliente se suscribe a una gran cantidad de ZNodes, incluso puede estar suscrito por completo. ¡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.

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, descubrimos 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.

Optimizar la exploració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.
  • El procesamiento es rápido: las operaciones de bits como AND, OR, XOR y la inversión suelen ser más rápidas 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 bytes 1 W/64/1024 = 1,2 KB . Si se reemplaza por HashSet, se requieren al menos 32 bytes 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<string, set> a ConcurrentHashMap<string,  BitHashSet >. 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.

Prueba de estrés de rendimiento

micropunto de referencia 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 relojes 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).

Prueba de estrés de rendimiento

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.

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

Resumir

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.

Árbitro:

https://issues.apache.org/jira/browse/ZOOKEEPER-1177

https://github.com/apache/zookeeper/pull/590

Linus se encargó de evitar que los desarrolladores del kernel reemplazaran las pestañas con espacios. Su padre es uno de los pocos líderes que puede escribir código, su segundo hijo es el director del departamento de tecnología de código abierto y su hijo menor es un núcleo de código abierto. Colaborador Robin Li: El lenguaje natural se convertirá en un nuevo lenguaje de programación universal. El modelo de código abierto se quedará cada vez más atrás de Huawei: tomará 1 año migrar completamente 5,000 aplicaciones móviles de uso común a Hongmeng, que es el lenguaje más propenso. Vulnerabilidades de terceros. Se lanzó el editor de texto enriquecido Quill 2.0 con características, confiabilidad y experiencia de desarrolladores que Ma Huateng y Zhou Hongyi se dieron la mano para "eliminar los rencores". La fuente de Laoxiangji no es el código, las razones detrás de esto son muy conmovedoras. Google anunció una reestructuración a gran escala.
{{o.nombre}}
{{m.nombre}}

Supongo que te gusta

Origin my.oschina.net/u/3874284/blog/11054067
Recomendado
Clasificación