Reflexiones sobre la sincronización de datos entre microservicios

Me aburro los fines de semana, vayamos a un blog sobre sincronización de datos entre servicios (principalmente sobre temas de atención). No hay ejemplos de escenarios comerciales específicos.

PD: Esto es una tontería puramente personal, si hay errores o deficiencias, indíquelos. Bueno, pongámonos manos a la obra.

Procesos de negocio

El principal proceso de negocio es el siguiente:

  • El usuario opera y guarda datos en el servicio A; después de que el servicio A guarda exitosamente, sincroniza algunos datos con el servicio B; el servicio B recibe los datos, los guarda exitosamente y el proceso finaliza.

Aquí discutimos el problema de la sincronización de datos del servicio A-> servicio B, debemos garantizar los dos puntos siguientes:

1. El primero es la exactitud de los datos (vas al banco a depositar 10,000 y tu saldo solo aumenta en 100, ¿por qué no lo haces? Por el contrario, si depositas 100 y el saldo aumenta en 10,000, ¿puede hacerlo el banco?)

Versión 1: precisión de los datos garantizada

Dibujar lleva demasiado tiempo, hagamos dibujos para explicar la versión final y explíquelo con palabras aquí.

Servicio Un pseudocódigo:

Iniciar transacciones distribuidas { 
		  // verificación de datos, requiere mucho tiempo 20 ms 
          // guardar el negocio en la base de datos, requiere mucho tiempo 10 ms 

		  // obtener los datos que deben sincronizarse, rpc llama de forma remota al método de guardado del servicio B, requiere mucho tiempo 40 ms 

     } todo tiene éxito, envíe la transacción; falle, confirme la transacción

Pseudocódigo del servicio B:

//El guardado exitoso tarda aproximadamente 40 ms 
 save método { 

 //Analiza los datos de verificación, 30 ms 

 //La verificación es exitosa, guarda los datos, 10 ms 

 //La validación falla, devuelve un error, notifica al servicio A que no pudo guardarse 

 }Guardar exitosamente y envíe la transacción, transacción de reversión de falla de operación
guardar método { 
  
  if (caso 1) { 
    
  }else if (caso 2){ 
  	
  }else if (caso 3){ 
  	//Llamar a otro de forma remota u obtener algunos datos comerciales de respaldo del caché 
  	//Verificar datos 
  	//Registrar varios registros 
  	/ /Guardar datos 
  	//Actualizar el estado de la tabla xx 
  } 
}Guardar la transacción confirmada exitosamente y revertir la transacción si la operación falla

  Al final, esta interfaz pasó de menos de 100 ms a más de 500 ms. Lo que es peor es que cuando el tráfico alcanza su punto máximo, muchos usuarios no pueden operar y el guardado sigue girando y girando, e incluso ocurren varios tiempos de espera, y la experiencia del usuario empeora cada vez más (tome Tomcat como ejemplo, su solicitud de procesamiento interno). es el grupo de subprocesos, los recursos son limitados, los recursos de solicitudes anteriores no se liberan, las solicitudes posteriores se rechazan o esperan). Se agregaron muchos servidores en el medio, pero de vez en cuando seguían ocurriendo problemas y las cartas de queja llegaban una tras otra. Un día, el jefe llamó al director técnico a la oficina: "¿Puedes hacerlo, lárgate?". . El director técnico se secó el sudor de la frente: "Está bien, está bien". Jefe: "¡Está bien, no lo resolvamos"! (muestra la gravedad del asunto)

Versión 2: middleware de mensajes integrado (aquí tome RabbitMQ como ejemplo)

El gerente técnico llamó a un grupo de desarrolladores a la oficina, discutió durante mucho tiempo y decidió usar un determinado middleware, que no solo puede resolver el problema del pico de tráfico, sino también desacoplar el código y discutir varios detalles y atención. señala claramente. Todo el equipo trabajó horas extras durante la noche durante unos días e implementó varios puntos comerciales similares uno por uno utilizando cierto middleware de mensajes. El código queda más o menos así:

Servicio Un pseudocódigo:

Iniciar la transacción { 
	 //Verificación de datos, requiere mucho tiempo 20 ms 
     //Guardar el negocio en la base de datos, consume mucho tiempo 10 ms 
	//Sincronización de datos con el middleware de mensajes, 20 ms 
	si (envío fallido){ 
		//Lanzar una excepción 
	} 
}Guardar correctamente y enviar la transacción, la operación no logra revertir la transacción

Pseudocódigo para el Servicio B:

Obtener datos del middleware de mensajes { 
   ejecutar método de guardar 
} 
guardar método { 
        //lógica de negocios 
} guardar y enviar la transacción exitosamente y revertir la transacción si la operación falla

En la versión final se mencionan algunos detalles específicos sobre el manejo de excepciones y MQ del lado del consumidor.

Los dos primeros días en línea, ¡perfecto! Volviendo a los 100 ms originales, todo el equipo está muy contento, el premio de fin de año de este año no se puede perder.

Sin embargo, un día, de repente, recibí muchas quejas, diciendo que los datos de xxx no coincidían. Xiao Wang, quien estaba a cargo de este negocio, quedó atónito. Cuando el gerente técnico fue a la consola RabbitMq, vio que la producción promedio de esta cola era de 1000/s y el consumo de 500/s. Los datos habían sido acumulados. Uno tras otro, varios otros negocios principales también tuvieron este problema, y ​​​​una gran cantidad de ellos que necesitaban sincronizarse se acumularon en mq.

¿Agregar servidor? Producción: Consumo = 2:1, ¿puede el jefe aceptar este gasto? Y no es un pico de tráfico diario, sino que el número de usuarios aumenta, según este ratio el dinero no es una cantidad pequeña. El director técnico es un director técnico e inmediatamente pensó en utilizar subprocesos múltiples para procesar mensajes y convocó a los desarrolladores responsables a una reunión. La versión final está aquí. . . .

versión definitiva

El middleware aquí es RabbitMq como ejemplo, y la base de datos para guardar datos es mysql (motor innodb) como ejemplo.

Nota 1. El servicio A se envía al middleware

Se utiliza middleware de mensajes y se adopta el modelo de producción-consumo para desacoplar el código del programa y resolver el problema del pico de tráfico, pero también aumentará la dificultad de nuestro programa y el problema de la coherencia de los datos. siguientes problemas.

Persistencia de datos de middleware

¿Qué pasa si MQ falla y se pierden datos? En este momento, debemos considerar la persistencia de los datos de la cola de configuración.

Si se debe utilizar la confirmación del editor

No es que llamemos a un método de envío de cliente RabbitMq y los datos lleguen a la cola. Solo podemos asegurarnos de que los datos se envíen al Broker cuando llamamos a la API del cliente, pero no los datos al intercambio (tenga en cuenta que el intercambio no tiene la capacidad de persistir), el intercambio a la cola y el los datos en la cola han persistido. Si mq cuelga en algún momento, es posible que se pierdan los mensajes.

En otro caso, generalmente usamos la anotación @RabbitListener para crear automáticamente una cola en el lado del consumidor y vincular la cola a un conmutador (o usar el método @Bean). En la etapa inicial de inicialización del proyecto (recién lanzada), si el consumidor no se inicia, el productor genera mensajes y el programa que llama a la API no informará ningún error. En este momento, no hay nada en la cola y el mensaje que envió se pierde. ===> He probado esto, por supuesto que podemos crearlo manualmente.

Ambas situaciones pueden provocar la pérdida de datos.

pérdida de datos del disco

Este es el extremo de los extremos, por ejemplo, el disco del servidor está dañado (es una suerte encontrarlo, pero también les ha pasado a las empresas). O el programador, operación y mantenimiento eliminó accidentalmente los datos persistentes por error.

En este caso, se puede perder una gran cantidad de datos. En términos generales, consideramos hacer una copia de seguridad de los datos primero cuando los datos se sincronizan con mq en el lado de producción.

Si utilizamos el mecanismo de compensación de tareas de tiempo, podemos utilizar el mecanismo de compensación para resolverlo.

Nota 2. El servicio B consume datos del middleware

Ya sea para firmar manualmente

De forma predeterminada, RabbitMq firma automáticamente la recepción. Es decir, si obtiene datos del consumidor, los datos se eliminarán de la cola correspondiente. Si no existe un mecanismo de compensación para las tareas programadas, se debe agregar (no puede garantizar que su servicio no se cuelgue, sus datos no se cuelguen o su código tenga otras anomalías), y si hay un problema, los datos se perdido.

La cantidad de mensajes obtenidos de mq cada vez (prefetch_count)

Cada versión del valor del cliente puede ser diferente, podemos establecer este valor en el lado del consumidor.

distribución justa

La configuración de la máquina y el rendimiento de cada servidor son diferentes y RabbitMQ utiliza el modo de sondeo de forma predeterminada. Suponiendo dos consumidores A, B, 100 mensajes, habrá 50 mensajes cada uno.

50 cada uno, justo, cincuenta para ti, cincuenta para mí, 50/50 (eres un programador junior, deja que tú y los programadores senior se dividan 50/50, ¿te sientes cómodo? Han pasado varios días desde que lo terminé). En este momento se distribuirá según el trabajo, quien termine primero de procesar se hará cargo de la tarea y los que sean capaces trabajarán más.

Repetir consumo

Existe el siguiente pseudocódigo:

//Paso 1: Insertar datos 

//Paso 2: Registrar registro 

//Paso 3: Actualizar estado 

//No se reporta ningún error, firmar manualmente

Consideremos las siguientes situaciones:

1. Active el reintento de excepción.

Supongamos que de repente ejecutamos hasta 3 en el código y, al actualizar los datos, hay un problema repentino con la base de datos actualizada. Cuando se reintenta la excepción más tarde, la base de datos se recupera nuevamente y la operación de inserción se realiza dos veces. Debemos tomar medidas para evitar garantizar que el mismo mensaje se consuma varias veces, lo que resulta en operaciones repetidas (por ejemplo: agregar una ID de mensaje, el consumo no se insertará nuevamente)

2. Recibo manual

Cuando se informa un error en el paso 3, el mensaje no se firmará manualmente, el mensaje aún existirá en la cola de mensajes y se volverá a entregar (por supuesto, aquí especificamos la operación después de que se informa el error, por ejemplo: volver a la cola para que otros consumidores lo consuman, eliminar mensajes de la cola, confirmar mensajes, etc., cómo lidiar con un error requiere procesamiento comercial de botones)

3. Compensación de tareas de tiempo

Si el estado de sincronización del servicio A es falso, el mensaje se entregará nuevamente al middleware de mensajes después de un período de tiempo. La base de datos de la operación en el paso 3 falla por primera vez y el mismo dato volverá a aparecer más adelante.

Nota 3, la tarea se entrega al grupo de subprocesos para su procesamiento.

Configuración de parámetros del grupo de subprocesos

En primer lugar, debemos establecer algunos parámetros principales del grupo de subprocesos de manera razonable, como el número de subprocesos principales, el número total de subprocesos, el tamaño de la cola (el valor máximo predeterminado de int), etc. Estos parámetros no se pueden establecer en una vez Combinamos el entorno de producción y observamos los subprocesos Establecemos un tamaño de parámetro razonable para la tasa de rechazo del grupo, la tasa de utilización de subprocesos y la cantidad de tareas acumuladas (la configuración de cada máquina puede ser diferente y se debe realizar un análisis específico para máquinas específicas ), para maximizar la tasa de utilización de las máquinas sin causar tiempo de inactividad del servicio (por ejemplo, si no configura el tamaño de la cola, se realizan demasiadas tareas a la vez, lo que genera fugas)

En segundo lugar, debe conocer un punto de conocimiento: la estrategia de rechazo del grupo de subprocesos (de forma predeterminada, es la estrategia de rechazar lanzar excepciones) (consulte la clase de implementación de java.util.concurrent.RejectedExecutionHandler). Cuando la tarea excede el límite del grupo de subprocesos, la tarea será rechazada. Si se rechaza, ¿deberíamos entregarlo al subproceso que llama para que lo procese (esto no es malo y la tarea del subproceso no se descartará), o descartar la tarea anterior o descartar la nueva tarea, si lanzamos una excepción de rechazo? ? Todos tenemos que pensarlo bien.

Nota 4. El grupo de subprocesos ejecuta guardar de forma asincrónica

Garantizar el orden de los datos

Suponiendo que el usuario guarda los datos y los sincroniza con el servicio B (supuesto: los datos del mismo usuario no solo pueden modificar el valor, sino también aumentar y eliminar la cantidad de datos), tenemos el siguiente pseudocódigo:

//Eliminar el original (eliminar) 

//Agregar uno nuevo (insertar)

Debemos considerar los datos de un usuario, si hay varios subprocesos ejecutándose al mismo tiempo y se debe garantizar el orden de los datos durante la ejecución. Por ejemplo:

1. Hay una función de guardado automático en el terminal de usuario, que se guarda cada 10 segundos, justo después del guardado automático, se modificará y guardará inmediatamente.

2. La última sincronización del usuario no fue exitosa, la tarea programada comienza a sincronizarse y el usuario está modificando los datos en este momento.

La situación anterior dará lugar a una diferencia muy pequeña entre lo nuevo y lo antiguo, o incluso se entregará al middleware de mensajes al mismo tiempo (ps: aquí usamos los dos caracteres chinos nuevo y antiguo para representar los datos nuevos y los antiguos originales). datos o los últimos datos)

situación anormal:

La máquina 1 primero se vuelve "vieja" y la máquina 2 se vuelve "nueva" (el grupo de subprocesos correspondiente a cada máquina tiene muchas otras tareas). Pero en este momento, la CPU de la máquina 1 es demasiado alta, el rendimiento de la máquina no es bueno y la ejecución es lenta, o la máquina no tiene suerte al ejecutar el subproceso "antiguo" y no ha aprovechado el intervalo de tiempo de la CPU durante mucho tiempo, lo que los lleva a ejecutar el método de guardar juntos, o incluso la ejecución "antigua" al nuevo. ¿Lo que sucederá?

Suponiendo que el método de guardar se ejecuta juntos, la eliminación e inserción de la máquina 1 y la eliminación e inserción de la máquina 2 se ejecutan juntas, el problema de la permutación y combinación, no sé en qué diablos se han convertido los datos.

Por ejemplo: 
Situación 1: 
Máquina 1 (datos antiguos) -eliminar 
máquina 2 (datos nuevos) -eliminar 
máquina 1 (datos antiguos) -insertar 
máquina 2 (datos nuevos) -insertar 
//Datos nuevos originales, eliminé una línea antigua Los datos, me los has agregado ahora 

Situación 2: 
Máquina 2 (datos nuevos) - eliminar 
máquina 2 (datos nuevos) - insertar 
máquina 1 (datos antiguos) - eliminar 
máquina 1 (datos antiguos) - insertar 
//Hacer un fantasma ¿Lo cambié por nada? Aún conservas los datos antiguos, 
otros casos no aparecerán en la lista

Suponiendo que el nuevo se ejecuta primero, es lo mismo que en el caso 2 anterior.

¿Cómo debemos afrontar esta situación? Solo puedes usar bloqueos distribuidos. La clave es el identificador de datos y el valor puede ser el tamaño de tiempo de los datos para identificar datos nuevos y antiguos. Si el antiguo agarra el candado, el nuevo simplemente espera y no hay ningún problema con los datos. Si el nuevo toma el bloqueo primero (se ejecuta primero), compare los datos antiguos con este valor de tiempo y no guarde estos datos si son demasiado pequeños.

Descripción del bloqueo: aquí solo podemos bloquear con un único ID comercial de usuario. Por ejemplo, cuando el servicio B es una máquina independiente, no podemos hacer esto:

guardado vacío sincronizado público(){....}

La granularidad del bloqueo es demasiado grande, lo que bloquea subprocesos irrelevantes y afecta la eficiencia del programa.

Asegúrese de que las operaciones de eliminación y actualización utilicen el índice mysql

En primer lugar, debemos saber que mysql bloqueará la tabla sin usar el índice al realizar operaciones de eliminación y actualización. El índice no se utiliza y el escaneo completo de la tabla es lento al eliminar o actualizar. Has bloqueado la tabla y otros hilos e incluso otros servicios te están esperando. ¿Qué pasa con jugar a las serpientes?

La optimización de consultas no se analiza aquí.

Considere usar una clave primaria de incremento automático

> Cada tabla `InnoDB` tiene un índice especial llamado índice agrupado, que almacena datos de filas. Normalmente, un índice agrupado es sinónimo de una clave principal. Para obtener el mejor rendimiento de consultas, inserciones y otras operaciones de bases de datos, es importante comprender cómo "InnoDB" utiliza índices agrupados para optimizar las búsquedas comunes y las operaciones DML. 
> 
> - Cuando se define en una tabla `PRIMARY KEY`, `InnoDB` la usa como un índice agrupado. Se debe definir una clave principal para cada tabla. Si ninguna columna o conjunto de columnas lógicamente única y no nula utiliza la clave principal, agregue una columna de incremento automático. Los valores de las columnas de incremento automático son únicos y se agregan automáticamente cuando se inserta una nueva fila. 
> - Si no se define ninguna `CLAVE PRIMARIA` para la tabla, `InnoDB` usa el primer índice `ÚNICO` y define todas las columnas clave como índices agrupados `NO NULOS`. 
> - Si la tabla no tiene una `CLAVE PRIMARIA` indexada o ningún índice `ÚNICO` adecuado, `InnoDB` genera un índice agrupado oculto que lleva el nombre de la columna sintética `GEN_CLUST_INDEX` que contiene el valor de ID de fila. Las filas se ordenan por el ID de fila asignado por `InnoDB`. El ID de fila es un campo de 6 bytes que aumenta monótonamente a medida que se insertan nuevas filas. Entonces las filas ordenadas por ID de fila están físicamente en orden de inserción

Supongamos que tiene una clave primaria desordenada. Dado que el valor de la clave primaria es aproximadamente aleatorio cada vez, cada nuevo registro debe insertarse en el medio de la página de índice existente. En este momento, MySQL tiene que insertar el nuevo registro. datos a una ubicación adecuada (los datos B + TREE de innodb de mysql se colocan en el nodo hoja), e incluso la página de destino puede haberse vuelto a escribir en el disco y borrarse del caché. En este momento, se debe volver a leer desde el disco. Esto añade mucha sobrecarga. Al mismo tiempo, las operaciones frecuentes de movimiento y paginación causan mucha fragmentación, lo que da como resultado una estructura de índice que no es lo suficientemente compacta. Después, la tabla tiene que ser reconstruida y las páginas llenas. con optimización a través de OPTIMIZE TABLE

Independientemente de si es un árbol b (los datos se colocan en el nodo correspondiente al árbol de índice agrupado) o un árbol b + (los datos solo se colocan en el árbol de índice agrupado correspondiente al nodo hoja), este problema existirá cuando utilizando una clave primaria desordenada.

Aviso:

También hay muchas bases de datos que utilizan árboles b, como mongo, postgrepSql, etc., y todas tienen este problema. Todos deberíamos considerar los problemas provocados por la reconstrucción del árbol de índice agrupado desordenado.

Optimización de eliminación de datos

Si realmente elimina los datos, volverá a haber un problema de reconstrucción del árbol. No es amigable con la base de datos ni con la eficiencia de nuestro programa. Podemos agregar un estado de eliminación y eliminar solo actualiza el estado. Si realmente es necesario eliminar los datos, podemos utilizar una tarea programada para eliminarlos cuando estemos libres en medio de la noche.

Nota 5. Actualizar el resultado de la sincronización.

En el Servicio A, el estado de sincronización se establece en falso cada vez que el usuario guarda. Una vez que hayamos sincronizado exitosamente, cambiaremos el valor del estado. Cuando este tipo de operación utiliza el índice, la actualización generalmente es muy rápida, podemos llamar directamente a la actualización de forma remota, si se utiliza middleware, la dificultad del programa aumentará.

Nota 6. Haga una copia de seguridad de los datos que deben sincronizarse

Hacer una copia de seguridad de los datos que deben sincronizarse puede resolver dos problemas:

1. Problema de pérdida de datos del disco MQ

2. Varias otras anomalías hacen que los datos no lleguen al servicio B, y el servicio de sincronización llama al servicio A para sincronizar los datos regularmente, por lo que el servicio A tiene el problema de que los datos se pueden sincronizar.

Para esta parte de los datos que se han sincronizado correctamente, si la empresa ya no es necesaria, podemos migrar la copia de seguridad a intervalos o eliminarla periódicamente.

Resumir

  • Al sincronizar datos, consideramos varias anomalías (anomalías de código, fallas de la máquina) y si las anomalías afectarán nuestros datos sincrónicos. En última instancia, es para garantizar la precisión de la sincronización de datos.Con el mecanismo de compensación de tareas programadas, podemos garantizar la coherencia final de los datos. Si bien se garantiza la exactitud de los datos, también es necesario considerar la eficiencia del programa y brindar una experiencia fácil de usar. Al utilizar nuevas tecnologías, es necesario conocer los peligros. Al igual que usar mq: puede causar pérdida de datos y consumo repetido de datos. Cuando se utilizan subprocesos múltiples, debe garantizar el orden de los datos. Cuando hay modificaciones simultáneas de los mismos datos, debe agregar un bloqueo (cas o Lock, sincronizado para una sola máquina). El bloqueo también puede controlar la granularidad. de la cerradura.

Supongo que te gusta

Origin blog.csdn.net/qq_41221596/article/details/132390578
Recomendado
Clasificación