banner

Noticias

Mar 07, 2023

Construyendo e implementando MySQL Raft en Meta

En Meta, ejecutamos una de las mayores implementaciones de MySQL en el mundo. La implementación impulsa el gráfico social junto con muchos otros servicios, como Mensajería, Anuncios y Feed. En los últimos años, implementamos MySQL Raft, un motor de consenso de Raft que se integró con MySQL para construir una máquina de estado replicada. Hemos migrado una gran parte de nuestra implementación a MySQL Raft y planeamos reemplazar por completo las bases de datos semisincrónicas de MySQL actuales. El proyecto ha brindado beneficios significativos a la implementación de MySQL en Meta, incluida una mayor confiabilidad, seguridad comprobable, mejoras significativas en el tiempo de conmutación por error y simplicidad operativa, todo con un rendimiento de escritura igual o comparable.

Para permitir una alta disponibilidad, tolerancia a fallas y lecturas escalables, el almacén de datos MySQL de Meta es una implementación replicada geográficamente fragmentada masivamente con millones de fragmentos, que contiene petabytes de datos. La implementación incluye miles de máquinas que se ejecutan en varias regiones y centros de datos en varios continentes.

Anteriormente, nuestra solución de replicación usaba el protocolo de replicación semisincrónica (semisync) de MySQL. Este era un protocolo de solo ruta de datos. MySQL primario usaría la replicación semisincrónica en dos réplicas de solo registro (logtailers) dentro de la región primaria pero fuera del dominio de falla del primario. Estos dos logtailers actuarían como ACKer semisincrónico (un ACK es un reconocimiento para el primario de que la transacción se ha escrito localmente). Esto permitiría que la ruta de datos tenga confirmaciones de muy baja latencia (submilisegundos) y proporcionaría alta disponibilidad/durabilidad para las escrituras. Se usó la replicación asincrónica regular de MySQL primario a réplica para una distribución más amplia a otras regiones.

Las operaciones del plano de control (p. ej., promociones, conmutación por error y cambio de membresía) estarían a cargo de un conjunto de demonios de Python (de ahora en adelante llamados automatización). La automatización haría la orquestación necesaria para promocionar un nuevo servidor MySQL en una ubicación de conmutación por error como principal. La automatización también señalaría el principal anterior y las réplicas restantes para replicar desde el nuevo principal. Las operaciones de cambio de membresía serían orquestadas por otra pieza de automatización llamada MySQL pool scanner (MPS). Para agregar un nuevo miembro, MPS apuntaría la nueva réplica al principal y la agregaría al almacén de detección de servicios. Una conmutación por error sería una operación más compleja en la que los subprocesos de seguimiento de los logtailers (ACKers semisincrónicos) se cerrarían para cercar el primario inactivo anterior.

En el pasado, para ayudar a garantizar la seguridad y evitar la pérdida de datos durante las complejas operaciones de promoción y conmutación por error, varios scripts y demonios de automatización usaban bloqueo, pasos de orquestación, un mecanismo de vallado y SMC, un sistema de detección de servicios. Era una configuración distribuida y era difícil lograr esto de forma atómica. La automatización se volvió más compleja y más difícil de mantener con el tiempo, ya que era necesario reparar más y más casos de esquina.

Decidimos tomar un enfoque completamente diferente. Mejoramos MySQL y lo convertimos en un verdadero sistema distribuido. Al darnos cuenta de que las operaciones del plano de control, como las promociones y los cambios de membresía, eran el desencadenante de la mayoría de los problemas, queríamos que las operaciones del plano de control y del plano de datos fueran parte del mismo registro replicado. Para esto, utilizamos el bien entendido protocolo de consenso Raft. Esto también significó que la fuente de la verdad sobre membresía y liderazgo se movió dentro del servidor (mysqld). Esta fue la mayor contribución individual de traer Raft porque permitió la corrección demostrable (propiedad de seguridad) en las promociones y los cambios de membresía en el servidor MySQL.

Nuestra implementación de Raft para MySQL se basa en Apache Kudu. Lo mejoramos significativamente para las necesidades de MySQL y nuestra implementación. Publicamos esta bifurcación como un proyecto de código abierto, kuduraft.

Algunas de las características clave que agregamos a kuduraft son:

También tuvimos que hacer cambios relativamente grandes en la replicación de MySQL para interactuar con Raft. Para esto, creamos un nuevo complemento MySQL de código cerrado llamado MyRaft. MySQL se conectaría con MyRaft a través de las API del complemento (también se habían usado API similares para semisync), mientras que creamos una API separada para MyRaft para interactuar con el servidor MySQL (devoluciones de llamada).

Un anillo de Raft consistiría en varias instancias de MySQL (cuatro en el diagrama) en diferentes regiones. El tiempo de ida y vuelta de la comunicación (RTT) entre estas regiones oscilaría entre 10 y 100 milisegundos. A algunos de estos MySQL (generalmente tres) se les permitió convertirse en primarios, mientras que al resto solo se les permitió ser réplicas de lectura pura (sin capacidad primaria). La implementación de MySQL en Meta también tiene un requisito de larga data para confirmaciones de latencia extremadamente baja. Los servicios que usan MySQL como almacén (por ejemplo, el gráfico social) necesitan o han sido diseñados para escrituras tan extremadamente rápidas.

Para cumplir con este requisito, la configuración de FlexiRaft usaría solo confirmaciones en la región (modo dinámico de una sola región). Para habilitar esto, cada región con capacidad principal tendría dos logtailers adicionales (testigos o entidades de solo registro). El quórum de datos para escrituras sería 2/3 (2 ACK de 1 MySQL + 2 logtailers). Raft aún administraría y ejecutaría un registro replicado en todas las entidades (1 MySQL con capacidad primaria + 2 logtailers) * 3 regiones + (MySQL sin capacidad primaria) * 3 regiones = 12 entidades.

Roles de la balsa: El líder, como sugiere el nombre, es el líder en un término del registro replicado. Un líder en Raft también sería el principal en MySQL y el que acepta las escrituras del cliente. El seguidor es un miembro con derecho a voto del anillo y recibe pasivamente mensajes (AppendEntries) del líder. Un seguidor sería una réplica desde el punto de vista de MySQL y estaría aplicando las transacciones a su motor. No permitiría escrituras directas desde conexiones de usuario (read_only=1 está configurado). Un alumno sería un miembro sin derecho a voto del anillo, por ejemplo, los tres MySQL en regiones sin capacidad primaria (arriba). Sería una réplica desde el punto de vista de MySQL.

Para la replicación, MySQL ha utilizado históricamente el formato de registro binario. Este formato es fundamental para la replicación de MySQL y decidimos conservarlo. Desde la perspectiva de Raft, el registro binario se convirtió en el registro replicado. Esto se hizo a través de la mejora de la abstracción de registros en kuduraft. Las transacciones de MySQL se codificarían como una serie de eventos (p. ej., el evento Actualizar filas) con un inicio y un final para cada transacción. El registro binario también tendría encabezados apropiados y normalmente terminaría con un evento de finalización (evento Rotar).

Tuvimos que modificar cómo MySQL administra sus registros internamente. En un primario, Raft escribiría en un binlog. Esto no es diferente de lo que sucede en MySQL estándar. En una réplica, Raft también escribiría en un binlog en lugar de en un registro de retransmisión separado en MySQL estándar. Esto creó simplicidad para Raft, ya que solo había un espacio de nombres de archivos de registro que preocuparían a Raft. Si un seguidor fuera ascendido a líder, podría volver sin problemas a su historial de registros para enviar transacciones a los miembros rezagados. Los subprocesos del aplicador de la réplica recogerían las transacciones del binlog y luego las aplicarían al motor. Durante este proceso, se crearía un nuevo archivo de registro, el registro de aplicación. Este registro de aplicación desempeñaría un papel importante en la recuperación de fallas de las réplicas, pero por lo demás es un archivo de registro no replicado.

Entonces, en resumen:

En MySQL estándar:

En Balsa MySQL:

La transacción se prepararía primero en el motor. Esto sucedería en el hilo de la conexión del usuario. El acto de preparar la transacción implicaría interacciones con el motor de almacenamiento (p. ej., InnoDB o MyRocks) y generaría una carga útil de binlog en memoria para la transacción. En el momento de la confirmación, la escritura pasaría a través del flujo de confirmación del grupo/commit_ordered. Se asignarían GTID y luego Raft asignaría un OpId (término: índice) a la transacción. En este punto, Raft comprimiría la transacción, la almacenaría en su LogCache y escribiría la transacción en un archivo binlog. Comenzaría a enviar la transacción de forma asíncrona a otros seguidores para obtener ACK y llegar a un consenso.

El subproceso del usuario, que está en "confirmación" de la transacción, se bloquearía, esperando el consenso de Raft. Cuando Raft obtuviera dos de tres votos en la región, se alcanzaría un compromiso por consenso. Raft también enviaría la transacción a todos los miembros fuera de la región, pero ignoraría sus votos debido a un algoritmo llamado FlexiRaft (descrito a continuación). En el compromiso de consenso, el subproceso del usuario se desbloquearía y la transacción continuaría y se confirmaría en el motor. Después de la confirmación del motor, la consulta de escritura finalizaría y regresaría al cliente. Poco después, Raft también enviaría de forma asincrónica un marcador de compromiso (OpId del compromiso actual) a los seguidores posteriores para que también puedan aplicar las transacciones a su base de datos.

Se tuvieron que hacer cambios en la recuperación de fallas para que funcione sin problemas con Raft. Los bloqueos pueden ocurrir en cualquier momento durante la vida de una transacción y, por lo tanto, el protocolo debe garantizar la coherencia de los miembros. Aquí hay algunas ideas clave sobre cómo lo hicimos funcionar.

Las operaciones de conmutación por error y mantenimiento regular pueden desencadenar cambios de liderazgo en Raft. Después de elegir a un líder, el complemento MyRaft intentaría hacer la transición del MySQL que lo acompaña al modo principal. Para esto, el complemento orquestaría un conjunto de pasos. Estas devoluciones de llamada de Raft → MySQL anularían las transacciones en curso, revertirían los GTID en uso, harían la transición del registro del lado del motor de apply-log a binlog y, finalmente, establecerían la configuración de solo lectura adecuada. Este mecanismo es complejo y actualmente no es de código abierto.

Dado que el documento de Raft y Apache Kudu admitían solo un único quórum global, no funcionaría bien en Meta, donde los anillos eran grandes pero el quórum de la ruta de datos debía ser pequeño.

Para sortear este problema, innovamos en FlexiRaft, tomando prestadas ideas de Flexible Paxos.

En un nivel alto, FlexiRaft le permite a Raft tener un quórum de compromiso de datos diferente (pequeño) pero recibir un impacto correspondiente en el quórum de elección de líder (grande). Al seguir garantías comprobables de intersección de quórum, FlexiRaft garantiza que las reglas de registro más largas de Raft y la intersección de quórum adecuada garantizarán una seguridad comprobable.

FlexiRaft admite el modo dinámico de región única. En este modo, los miembros se agrupan por su región geográfica. El quórum actual de Raft depende de quién sea el líder actual (de ahí el nombre de "dinámica de región única"). El quórum de datos es la mayoría de los votantes en la región del líder. Durante las promociones, si los términos son continuos, el Candidato se cruzará con la región del último líder conocido. FlexiRaft también garantizaría que se logre el quórum de la región del Candidato, de lo contrario, el mensaje No-Op subsiguiente podría atascarse. Si en el raro caso de que los términos no sean continuos, Flexi Raft intentará descubrir un conjunto creciente de regiones con las que es necesario intersectar por seguridad o, en el peor de los casos, recurrirá al caso de intersección de la región N de Flexible Paxos. . Gracias a las preelecciones y las elecciones simuladas, las incidencias de brechas de mandato son raras.

Con el fin de serializar los eventos de promoción y cambio de membresía en el binlog, secuestramos el evento Rotate Event y Metadata del formato de registro binario de MySQL. Estos eventos llevarían el equivalente de los mensajes No-Op y las operaciones de agregar/eliminar miembros de Raft. Apache Kudu no admitía el consenso conjunto, por lo tanto, solo permitimos cambios de membresía de uno en uno (puede cambiar la membresía por una sola entidad en una ronda para seguir las reglas de intersección de quórum implícito).

Con la implementación de MySQL Raft, logramos una separación muy clara de preocupaciones para la implementación de MySQL. El servidor MySQL sería responsable de la seguridad a través de la máquina de estado replicada de Raft. La garantía de no pérdida de datos probablemente estaría consagrada en el propio servidor. La automatización (scripts de Python, demonios) iniciaría las operaciones del plano de control y monitorearía la salud de la flota. También reemplazaría a los miembros o haría promociones a través de Raft durante el mantenimiento o cuando se detectara una falla del host. De vez en cuando, la automatización también podría cambiar la ubicación regional de la topología de MySQL. Cambiar la automatización para adaptarla a Raft fue una tarea enorme, que abarcó varios años de esfuerzo de desarrollo e implementación.

Durante eventos de mantenimiento prolongados, la automatización establecería información de prohibición de liderazgo en Raft. Raft impediría que esas entidades prohibidas se convirtieran en líderes o las evacuaría rápidamente en caso de elección inadvertida. La automatización también promovería lejos de esas regiones a otras regiones.

La implementación de Raft en la flota fue un gran aprendizaje para el equipo. Inicialmente desarrollamos Raft en MySQL 5.6 y tuvimos que migrar a MySQL 8.0.

Uno de los aprendizajes clave fue que, si bien la corrección era más fácil de razonar con Raft, el protocolo Raft en sí mismo no ayuda mucho en lo que respecta a la disponibilidad. Dado que nuestro quórum de datos de MySQL era muy pequeño (dos de los tres miembros de la región), dos entidades malas en la región podrían destruir el quórum y reducir la disponibilidad. La flota de MySQL sufre una buena cantidad de rotación todos los días (debido al mantenimiento, fallas del host, operaciones de reequilibrio), por lo que iniciar y realizar cambios de membresía de manera rápida y correcta fueron requisitos clave para una disponibilidad constante. Una gran parte del esfuerzo de implementación se centró en realizar reemplazos de logtailer y MySQL con prontitud para que los quórumes de Raft fueran saludables.

Tuvimos que mejorar kuduraft para hacerlo más robusto en cuanto a disponibilidad. Estas mejoras no formaban parte del protocolo central, pero pueden considerarse complementos de ingeniería. Kuduraft tiene soporte para elecciones previas, pero las elecciones previas se realizan solo durante una conmutación por error. Durante una elegante transferencia de liderazgo, el Candidato designado pasa directamente a una elección real, adelantando el mandato. Esto conduce a líderes atascados (kuduraft no realiza una reducción automática). Para abordar este problema, agregamos una función de elecciones simuladas, que era similar a las preelecciones pero solo ocurría con una elegante transferencia de liderazgo. Dado que se trataba de una operación asíncrona, no aumentó los tiempos de inactividad de la promoción. Una elección simulada eliminaría los casos en los que una elección real tendría un éxito parcial y se estancaría.

Manejo de fallas bizantinas: se considera que la lista de miembros de Raft está bendecida por Raft. Pero durante el aprovisionamiento de nuevos miembros, o debido a las carreras en la automatización, podría haber casos extraños de dos anillos de Raft diferentes que se cruzan. Estos nodos de membresía zombi tuvieron que eliminarse y no deberían poder comunicarse entre sí. Implementamos una función para bloquear los RPC de dichos miembros zombis al ring. Esto fue, en cierto modo, un manejo de un actor bizantino. Mejoramos la implementación de Raft después de notar estos incidentes raros que ocurrieron en nuestro despliegue.

Al lanzar MySQL Raft, uno de los objetivos era reducir la complejidad operativa de las guardias, de modo que los ingenieros pudieran encontrar la causa raíz y mitigar los problemas. Construimos varios tableros, herramientas CLI y tablas de buceo para monitorear Raft. Agregamos una gran cantidad de registros a MySQL, especialmente en el área de promociones y cambios de membresía. Creamos CLI para informes de quórum y votación en un anillo, que nos ayudan a identificar rápidamente cuándo y por qué un anillo no está disponible (quórum destrozado). La inversión en la infraestructura de herramientas y automatización fue de la mano y podría haber sido una inversión mayor que los cambios de servidor. Esta inversión dio sus frutos a lo grande y redujo el dolor operativo y de incorporación.

Aunque no es deseable, los quórumes se rompen de vez en cuando, lo que lleva a la pérdida de disponibilidad. El caso típico es cuando la automatización no detecta instancias/logtailers en mal estado en el ring y no los reemplaza rápidamente. Esto puede suceder debido a una mala detección, sobrecarga de la cola de trabajo o falta de capacidad de host adicional. Las fallas correlacionadas, cuando múltiples entidades en el quórum fallan al mismo tiempo, son menos típicas. Esto no sucede con frecuencia, porque las implementaciones intentan aislar los dominios de error en las entidades críticas del quórum a través de decisiones de ubicación adecuadas. Para resumir: a escala, suceden cosas inesperadas, a pesar de las salvaguardas existentes. Las herramientas deben estar disponibles para mitigar tales situaciones en la producción. Creamos Quorum Fixer anticipándonos a esto.

Quorum Fixer es una herramienta de reparación manual creada en Python que silencia las escrituras en el anillo. Realiza comprobaciones fuera de banda para determinar la entidad de registro más larga. Cambia por la fuerza las expectativas de quórum para la elección de un líder dentro de Raft, de modo que la entidad elegida se convierta en líder. Después de una promoción exitosa, restablecemos la expectativa de quórum y, por lo general, el anillo se vuelve saludable.

Fue una decisión consciente no ejecutar esta herramienta automáticamente, porque queremos identificar la causa principal e identificar todos los casos de pérdida de quórum y corregir errores en el camino (no dejar que la automatización los corrija silenciosamente).

La transición de semisíncrono a MySQL Raft en una implementación masiva es difícil. Para ello creamos una herramienta (en Python) llamada enable-raft. Enable-raft organiza la transición de semisynchronous a Raft cargando el complemento y configurando las configuraciones apropiadas (mysql sys-vars) en cada una de las entidades. Este proceso implica un pequeño tiempo de inactividad para el anillo. La herramienta se hizo robusta con el tiempo y puede implementar Raft a escala muy rápidamente. Lo hemos usado para desplegar Raft de forma segura.

No hace falta decir que hacer un cambio en la tubería de replicación central de MySQL es un proyecto muy difícil. Dado que la seguridad de los datos está en juego, las pruebas fueron clave para la confianza. Aprovechamos significativamente las pruebas de sombra y la inyección de fallas durante el proyecto. Inyectaríamos miles de conmutaciones por error y elecciones en los anillos de prueba antes de cada implementación del administrador de paquetes RPM. Desencadenaríamos reemplazos y cambios de membresía en los activos de prueba para activar las rutas de código críticas.

Las pruebas de larga duración con verificaciones de corrección de datos también fueron clave. Contamos con automatización que se ejecuta todas las noches en los fragmentos, lo que garantiza la consistencia de las primarias y las réplicas. Somos alertados de cualquier discrepancia y lo depuramos.

El rendimiento de la latencia de la ruta de escritura para Raft fue equivalente a semisync. La maquinaria semisincronizada es un poco más simple y, por lo tanto, se espera que sea más eficiente; sin embargo, optimizamos Raft para obtener las mismas latencias que la semisincronización. Optimizamos kuduraft para no agregar más CPU a la flota a pesar de incorporar muchas más responsabilidades que anteriormente estaban fuera del binario del servidor.

Raft realizó mejoras de orden de magnitud en las promociones y los tiempos de conmutación por error. Las promociones elegantes, que son la mayor parte de los cambios de liderazgo en la flota, mejoraron significativamente y, por lo general, podemos finalizar una promoción en 300 milisegundos. En las configuraciones de semisincronización, dado que el almacén de detección de servicios sería la fuente de la verdad, los clientes notarían el final de la promoción durante mucho más tiempo, lo que generaría tiempos de inactividad más elevados para el usuario final en un fragmento.

Raft normalmente realiza una conmutación por error en 2 segundos. Esto se debe a que pulsamos para la salud de Raft cada 500 milisegundos y comenzamos una elección cuando fallan tres latidos consecutivos. En el mundo de la semisincronización, este paso requería mucha orquestación y demoraba de 20 a 40 segundos. Por lo tanto, Raft brindó una mejora de 10 veces en los tiempos de inactividad para los casos de conmutación por error.

Raft ha ayudado a resolver problemas con la gestión operativa de MySQL en Meta al brindar seguridad y simplicidad demostrables. Nuestros objetivos de tener una gestión directa de la consistencia de MySQL y tener herramientas para los raros casos de pérdida de disponibilidad, se cumplen en su mayoría. Raft ahora abre importantes oportunidades en el futuro, ya que podemos centrarnos en mejorar la oferta de los servicios que utilizan MySQL. Una de las peticiones de nuestros propietarios de servicios es tener una consistencia configurable. La coherencia configurable permitirá a los propietarios, en el momento de la incorporación, seleccionar si el servicio necesita quórums de región X o quórumes que solicitan copias en algunas geografías específicas (por ejemplo, Europa y Estados Unidos). FlexiRaft tiene un soporte perfecto para tales quórums configurables y planeamos comenzar a implementar este soporte en el futuro. Dichos quórumes conducirán correspondientemente a latencias de compromiso más altas, pero los casos de uso deben poder compensar entre consistencia y latencia (p. ej., el teorema de PACELC).

Debido a la función de proxy (capacidad de enviar mensajes utilizando una topología de distribución multisalto), Raft también puede ahorrar ancho de banda de red a través del Atlántico. Planeamos usar Raft para replicar desde los Estados Unidos a Europa solo una vez, y luego usar la función de proxy de Raft para distribuir dentro de Europa. Esto aumentará la latencia, pero será nominal dado que la mayor parte de la latencia está en la transferencia a través del Atlántico y el salto adicional es mucho más corto.

Algunas de las ideas más especulativas en las implementaciones de bases de datos de Meta y el espacio de consenso distribuido se refieren a la exploración de protocolos sin líderes, como Epaxos. Nuestras implementaciones y servicios actuales han funcionado con las suposiciones que vienen con protocolos líderes fuertes, pero estamos comenzando a ver un goteo de requisitos donde los servicios se beneficiarían de una latencia de escritura más uniforme en la WAN. Otra idea que estamos considerando es separar el registro de la máquina de estado (la base de datos) en una configuración de registro desagregada. Esto permitirá que el equipo administre las inquietudes del registro y la replicación por separado de las inquietudes del almacenamiento de la base de datos y el motor de ejecución de SQL.

La creación e implementación de MySQL Raft a escala Meta requería un importante trabajo en equipo y apoyo administrativo. Nos gustaría reconocer a las siguientes personas por su papel en hacer de este proyecto un éxito. Shrikanth Shankar, Tobias Asplund, Jim Carrig, Affan Dar y David Nagle por apoyar a los miembros del equipo durante este viaje. También nos gustaría dar las gracias a los responsables de programa de este proyecto, Dan O y Karthik Chidambaram, que nos mantuvieron en el buen camino.

El esfuerzo de ingeniería involucró contribuciones clave de varios miembros actuales y pasados ​​del equipo, incluidos Vinaykumar Bhat, Xi Wang, Bartholomew Pelc, Chi Li, Yash Botadra, Alan Liang, Michael Percy, Yoshinori Matsunobu, Ritwik Yadav, Luqun Lou, Pushap Goyal y Anatoly Karp Igor. Pozgaj.

COMPARTIR