Siembra de bases de datos de microservicios

10

Dado el servicio A (CMS) que controla un modelo (producto, supongamos que los únicos campos que tiene son id, título, precio) y los servicios B (envío) y C (correos electrónicos) que tienen que mostrar el modelo dado cuál debería ser el enfoque. sincronizar la información del modelo dado en esos servicios en el enfoque de abastecimiento de eventos? Supongamos que el catálogo de productos rara vez cambia (pero cambia) y que hay administradores que pueden acceder a los datos de envíos y correos electrónicos con mucha frecuencia (las funcionalidades de ejemplo son: B: display titles of products the order containedy C:) display content of email about shipping that is going to be sent. Cada uno de los servicios tiene su propia base de datos.

Solución 1

Enviar toda la información requerida sobre el Producto dentro del evento; esto significa la siguiente estructura para order_placed:

{
    order_id: [guid],
    product: {
        id: [guid],
        title: 'Foo',
        price: 1000
    }
}

En el servicio B y C, la información del producto se almacena en el productatributo JSON en la orderstabla

Como tal, para mostrar la información necesaria solo se utilizan los datos recuperados del evento

Problemas : dependiendo de qué otra información se deba presentar en B y C, la cantidad de datos en el evento puede crecer. B y C pueden no requerir la misma información sobre el Producto, pero el evento tendrá que contener ambos (a menos que separemos los eventos en dos). Si los datos dados no están presentes dentro del evento dado, el código no puede usarlo; si agregaremos una opción de color al Producto dado, para los pedidos existentes en B y C, el producto dado será incoloro a menos que actualicemos los eventos y luego los volvamos a ejecutar .

Solución 2

Enviar solo el guid del producto dentro del evento; esto significa la siguiente estructura para order_placed:

{
    order_id: [guid],
    product_id: [guid]
}

En los servicios B y C, la información del producto se almacena en el product_idatributo de la orderstabla

La información del producto es recuperada por los servicios B y C cuando es necesario al realizar una llamada API al A/product/[guid]punto final

Problemas : esto hace que B y C dependan de A (en todo momento). Si el esquema del Producto cambia en A, los cambios deben hacerse en todos los servicios que dependen de ellos (de repente)

Solución 3

Enviar solo el guid del producto dentro del evento; esto significa la siguiente estructura para order_placed:

{
    order_id: [guid],
    product_id: [guid]
}

En los servicios B y C, la información del producto se almacena en la productstabla; todavía hay product_iden la ordersmesa, pero hay replicación de productsdatos entre A, B y C; B y C pueden contener información diferente sobre el Producto que A

La información del producto se siembra cuando se crean los servicios B y C y se actualizan cada vez que la información sobre los Productos cambia haciendo una llamada al A/productpunto final (que muestra la información requerida de todos los productos) o realizando un acceso directo de base de datos a A y copiando la información necesaria del producto requerida para Servicio.

Problemas : esto hace que B y C dependan de A (cuando se siembra). Si el esquema de Producto cambia en A, los cambios deben hacerse en todos los servicios que dependen de ellos (cuando se inicia)


Según tengo entendido, el enfoque correcto sería ir con la solución 1, y actualizar el historial de eventos según cierta lógica (si el catálogo del Producto no ha cambiado y queremos agregar color para mostrar, podemos actualizar el historial de forma segura para obtener el estado actual de productos y rellenar datos faltantes dentro de los eventos) o atender la inexistencia de datos dados (si el catálogo de productos ha cambiado y queremos agregar el color que se mostrará, no podemos estar seguros si en ese momento en el pasado producto dado tenía un color o no, podemos suponer que todos los productos en el catálogo anterior eran negros y se atienden al actualizar eventos o código)

eithed
fuente
En lo que respecta a updating event history: en el evento de abastecimiento, el historial de eventos es su fuente de verdad y nunca debe modificarse, sino que solo debe avanzar. Si los eventos cambian, puede usar versiones de eventos o soluciones similares, pero al reproducir sus eventos hasta un punto específico en el tiempo, el estado de los datos debe ser como era en ese punto.
No
En lo que respecta al almacenamiento de datos (esquemas, etc.) para consultas y campos que se agregan / eliminan, etc., nosotros mismos utilizamos cosmosDB para almacenar los datos en JSON tal como están en ese momento. Lo único que necesita versionarse son los eventos y / o comandos. También debe actualizar los contratos de punto final y los objetos de valor que contienen los datos que responden a las consultas de un cliente (web, móvil, etc.). Los datos más antiguos que no tienen un campo tendrán un valor predeterminado o en blanco, lo que se adapta a la empresa, pero el historial de eventos permanece intacto y solo avanza.
No
@Nope, updating event historyquiero decir: revisa todos los eventos, copiándolos de un flujo (v1) a otro flujo (v2) para mantener un esquema de eventos consistente.
eithed
Como comentario aparte, en el ámbito del comercio electrónico, es posible que desee capturar el precio como se indica dado que los precios cambian con frecuencia. El precio que se muestra al usuario puede ser diferente en el momento en que se captura el pedido real. Hay varias formas de resolver el problema, pero es una que debe considerarse.
CPerson
@CPerson yup: el precio podría ser uno de los atributos que se pasa dentro del evento en sí. Por otro lado, la URL de la imagen puede existir dentro del evento (que representa la intención de display image at the point when purchase was made) o no puede (representa la intención de display current image as it within catalog)
publicó el

Respuestas:

3

La solución n. ° 3 se acerca mucho a la idea correcta.

Una forma de pensar en esto: B y C son copias en caché "locales" de los datos que necesitan. Los mensajes procesados ​​en B (y también en C) usan la información almacenada localmente en caché. Asimismo, los informes se producen utilizando la información almacenada localmente en caché.

Los datos se replican desde el origen a los cachés a través de una API estable. B y C ni siquiera necesitan usar la misma API: usan el protocolo de búsqueda apropiado para sus necesidades. En efecto, definimos un contrato (protocolo y esquema de mensaje) que restringe al proveedor y al consumidor. Entonces, cualquier consumidor para ese contrato puede conectarse a cualquier proveedor. Los cambios incompatibles hacia atrás requieren un nuevo contrato.

Los servicios eligen la estrategia de invalidación de caché adecuada para sus necesidades. Esto puede significar extraer cambios de la fuente en un horario regular, o en respuesta a una notificación de que las cosas pueden haber cambiado, o incluso "a pedido", actuando como una lectura a través de la memoria caché, volviendo a la copia almacenada de los datos cuando La fuente no está disponible.

Esto le brinda "autonomía", en el sentido de que B y C pueden continuar entregando valor comercial cuando A no está disponible temporalmente.

Lectura recomendada: Datos externos, Datos internos , Pat Helland 2005.

VoiceOfUnreason
fuente
Sí, estoy completamente de acuerdo con lo que has escrito aquí y la solución 3 es la solución Goto que he aplicado, sin embargo, no es el enfoque de abastecimiento de eventos, ya que, si repetimos los eventos, no necesariamente queremos utilizar el estado actual del producto; queremos usar el estado tal como estaba en el punto del evento. Por supuesto, esto podría estar bien (depende del requisito comercial). Sin embargo, si queremos realizar un seguimiento de los cambios en el catálogo, eso también requiere el abastecimiento de eventos y, dependiendo de la cantidad de datos que sean, podríamos
recurrir
1
Creo que lo tienes con la solución n. ° 3. Si necesita volver a reproducir la coherencia con el catálogo, también puede hacerlo. Solo necesita volver a reproducir cuando reinicia, lo que probablemente sea al inicio; una vez que esté listo, solo necesita mirar nuevos eventos, por lo que la cantidad de datos probablemente no sea un problema real. Sin embargo, incluso entonces tiene la opción (si es necesario) de usar puntos de control, es decir, "aquí está el estado del evento 1,000", por lo que toma eso y ahora solo tiene que volver a reproducir el evento 1.001 al actual en lugar del historial completo .
Mike B.
2

Hay dos cosas difíciles en informática, y una de ellas es la invalidación de caché.

La solución 2 es absolutamente mi posición predeterminada, y generalmente solo debería considerar implementar el almacenamiento en caché si se encuentra con uno de los siguientes escenarios:

  1. La llamada API al Servicio A está causando problemas de rendimiento.
  2. El costo de que el Servicio A esté inactivo y no pueda recuperar los datos es significativo para el negocio.

Los problemas de rendimiento son realmente el principal impulsor. Hay muchas formas de resolver el n. ° 2 que no implican el almacenamiento en caché, como garantizar que el Servicio A esté altamente disponible.

El almacenamiento en caché agrega una complejidad significativa a un sistema y puede crear casos extremos que son difíciles de razonar y errores que son muy difíciles de replicar. También debe mitigar el riesgo de proporcionar datos obsoletos cuando existen datos más nuevos, que pueden ser mucho peor desde una perspectiva comercial que (por ejemplo) mostrar un mensaje que dice "El servicio A está inactivo; intente nuevamente más tarde".

De este excelente artículo de Udi Dahan:

Estas dependencias se arrastran lentamente hacia usted, atando los cordones de los zapatos, disminuyendo gradualmente el ritmo de desarrollo, socavando la estabilidad de su base de código donde los cambios en una parte del sistema rompen otras partes. Es una muerte lenta por mil recortes, y como resultado nadie está exactamente seguro de qué gran decisión tomamos que hizo que todo saliera tan mal.

Además, si necesita consultar en un momento dado los datos del producto, esto debe manejarse de la manera en que los datos se almacenan en la base de datos del producto (por ejemplo, fechas de inicio / finalización), debe exponerse claramente en la API (la fecha efectiva debe ser una entrada para la llamada API para consultar los datos).

Phil Sandler
fuente
1
@SavvasKleanthous "La red es confiable" es una de las falacias de la informática distribuida. Pero la respuesta a esa falacia no debería ser "almacenar en caché cada bit de datos de cada servicio en cualquier otro servicio" (me doy cuenta de que es un poco hiperbólico). Espere que un servicio no esté disponible y lidie con eso como una condición de error. Si tiene una situación poco común en la que la caída del Servicio A tiene un impacto comercial importante, entonces (¡con cuidado!) Considere otras opciones.
Phil Sandler
1
@SavvasKleanthous también considera (como mencioné en mi respuesta) que devolver datos obsoletos en muchos casos puede ser mucho peor que arrojar un error.
Phil Sandler
1
@eithed Me estaba refiriendo a este comentario: "Si, sin embargo, queremos hacer un seguimiento de los cambios en el catálogo, eso también requiere el abastecimiento de eventos". En cualquier caso, tiene la idea correcta: el servicio del Producto debe ser responsable de rastrear los cambios a lo largo del tiempo, no los servicios posteriores.
Phil Sandler
1
Además, al almacenar datos que observa, aunque tiene algunas similitudes con el almacenamiento en caché, no presenta los mismos problemas. Más específicamente, la invalidación no es necesaria; obtienes la nueva versión de los datos cuando sucede. Lo que experimenta es una consistencia retardada. Sin embargo, incluso usando la solicitud web hay una ventana de inconsistencia (aunque pequeña).
Savvas Kleanthous
1
@SavvasKleanthous En cualquier caso, mi punto principal es no tratar de resolver problemas que aún no existen, especialmente con soluciones que traen sus propios problemas y riesgos. La opción 2 es la solución más simple y debería ser la opción predeterminada hasta el momento en que no cumpla con los requisitos comerciales . Si cree que elegir la solución más simple que puede funcionar es (como usted lo dice) "realmente malo", entonces creo que simplemente no estamos de acuerdo.
Phil Sandler
2

Es muy difícil simplemente decir que una solución es mejor que la otra. Elegir uno entre la Solución # 2 y # 3 depende de otros factores (duración de caché, tolerancia de consistencia, ...)

Mis 2 centavos:

La invalidación de caché puede ser difícil, pero la declaración del problema menciona que el catálogo de productos rara vez cambia. Este hecho hace que los datos del producto sean un buen candidato para el almacenamiento en caché

Solución # 1 (NOK)

  • Los datos se duplican en múltiples sistemas.

Solución # 2 (OK)

  • Ofrece una fuerte consistencia
  • Funciona solo cuando el servicio del producto está altamente disponible y ofrece un buen rendimiento
  • Si el servicio de correo electrónico prepara un resumen (con muchos productos), el tiempo de respuesta general podría ser más largo

Solución n. ° 3 (Complejo pero preferido)

  • Prefiera el enfoque API en lugar del acceso directo a la base de datos para recuperar información del producto
  • Los servicios que consumen resilientes no se ven afectados cuando el servicio del producto está inactivo
  • Las aplicaciones consumidoras (servicios de envío y correo electrónico) recuperan los detalles del producto inmediatamente después de que se publica un evento. La posibilidad de que el servicio del producto disminuya en estos pocos milisegundos es muy remota.
Sudhir
fuente
1

En términos generales, recomendaría encarecidamente la opción 2 debido al acoplamiento temporal entre esos dos servicios (a menos que la comunicación entre estos servicios sea súper estable y no sea muy frecuente). El acoplamiento temporal es lo que usted describe this makes B and C dependant upon A (at all times), y significa que si A está inactivo o inalcanzable desde B o C, B y C no pueden cumplir su función.

Personalmente, creo que ambas opciones 1 y 3 tienen situaciones en las que son opciones válidas.

Si la comunicación entre A y B y C es tan alta, o la cantidad de datos necesarios para ingresar al evento es lo suficientemente grande como para preocuparlo, entonces la opción 3 es la mejor opción, porque la carga en la red es mucho menor , y la latencia de las operaciones disminuirá a medida que disminuya el tamaño del mensaje. Otras preocupaciones a considerar aquí son:

  1. Estabilidad del contrato: si el contrato del mensaje que sale de A cambia con frecuencia, colocar muchas propiedades en el mensaje daría lugar a muchos cambios en los consumidores. Sin embargo, en este caso, creo que esto no es un gran problema porque:
    1. Usted mencionó que el sistema A es un CMS. Esto significa que está trabajando en un dominio estable y, como tal, no creo que vea cambios frecuentes
    2. Dado que los B y C son envío y correo electrónico, y está recibiendo datos de A, creo que experimentará cambios aditivos en lugar de romperlos, que es seguro agregar cada vez que los descubra sin reprocesos.
  2. Acoplamiento: aquí hay muy poco o ningún acoplamiento. Primero, dado que la comunicación se realiza a través de mensajes, no existe un acoplamiento entre los servicios que no sea uno temporal corto durante la transmisión de los datos, y el contrato de esa operación (que no es un acoplamiento que puede o debe intentar evitar)

Sin embargo, la opción 1 no es algo que descartaría. Hay la misma cantidad de acoplamiento, pero en cuanto al desarrollo, debería ser fácil de hacer (sin necesidad de acciones especiales), y la estabilidad del dominio debería significar que estos no cambiarán con frecuencia (como ya mencioné).

Otra opción que sugeriría es una ligera variación a 3, que no es ejecutar el proceso durante el inicio, sino observar un evento "ProductAdded y" ProductDetailsChanged "en B y C, cuando hay un cambio en el catálogo de productos en A. Esto haría que sus implementaciones sean más rápidas (y más fáciles de solucionar un problema / error si encuentra alguno).


Editar 2020-03-03

Tengo un orden específico de prioridades al determinar el enfoque de integración:

  1. ¿Cuál es el costo de la consistencia? ¿Podemos aceptar algunos milisegundos de inconsistencia entre las cosas cambiadas en A y las que se reflejan en B y C?
  2. ¿Necesita consultas de punto en el tiempo (también llamadas consultas temporales)?
  3. ¿Hay alguna fuente de verdad para los datos? ¿Un servicio que los posee y se considera aguas arriba?
  4. Si hay un propietario / fuente única de verdad, ¿es estable? ¿O esperamos ver cambios frecuentes de ruptura?

Si el costo de la inconsistencia es alto (básicamente, los datos del producto en A deben ser consistentes lo antes posible con el producto almacenado en caché en B y C), entonces no puede evitar tener que aceptar la no disponibilidad y realizar una solicitud sincrónica (como una web / solicitud de descanso) de B & C a A para obtener los datos. ¡Ten cuidado! Esto todavía no significa transaccionalmente consistente, sino que minimiza las ventanas por inconsistencia. Si absolutamente, positivamente, tiene que ser inmediatamente consistente, necesita retomar los límites de su servicio. Sin embargo, yo muy firmemente que esto no debería ser un problema. Por experiencia, en realidad es extremadamente raro que la compañía no pueda aceptar algunos segundos de inconsistencia, por lo que ni siquiera debería necesitar hacer solicitudes sincrónicas.

Si necesita consultas puntuales (que no noté en su pregunta y, por lo tanto, no incluí anteriormente, tal vez erróneamente), el costo de mantener esto en los servicios posteriores es muy alto (necesitaría duplicar lógica de proyección de eventos internos en todos los servicios posteriores) que deja en claro la decisión: debe dejar la propiedad a A, y consultar A ad-hoc sobre solicitud web (o similar), y A debe usar el abastecimiento de eventos para recuperar todos los eventos que conoce en el momento de proyectar al estado y devolverlo. Supongo que esta puede ser la opción 2 (si lo entendí correctamente), pero los costos son tales que, si bien el acoplamiento temporal es mejor que el costo de mantenimiento de eventos duplicados y la lógica de proyección.

Si no necesita un punto en el tiempo, y no hay un propietario claro y único de los datos (que en mi respuesta inicial supuse esto en función de su pregunta), entonces un patrón muy razonable sería mantener representaciones del producto en cada servicio por separado. Cuando actualiza los datos de los productos, actualiza A, B y C en paralelo al hacer solicitudes web paralelas a cada uno, o tiene una API de comandos que envía múltiples comandos a cada uno de A, B y C. B y C usan su versión local de los datos para hacer su trabajo, que puede o no estar obsoleta. Esta no es ninguna de las opciones anteriores (aunque podría hacerse para estar cerca de la opción 3), ya que los datos en A, B y C pueden diferir, y el "todo" del producto puede ser una composición de los tres datos fuentes.

Saber si la fuente de la verdad es un contrato estable es útil porque puede usarlo para usar el dominio / eventos internos (o eventos que almacena en su fuente de eventos como patrón de almacenamiento en A) para la integración entre A y los servicios B y C. Si el contrato es estable, puede integrarse a través de los eventos de dominio. Sin embargo, entonces tiene una preocupación adicional en el caso de que los cambios sean frecuentes, o que el contrato de mensaje sea lo suficientemente grande como para que el transporte sea una preocupación.

Si tiene un propietario claro, con un contrato que se espera que sea estable, las mejores opciones serían la opción 1; una orden contendría toda la información necesaria y luego B y C cumplirían su función utilizando los datos del evento.

Si es probable que el contrato cambie o se rompa con frecuencia, siguiendo su opción 3, recurrir a las solicitudes web para obtener datos del producto es en realidad una mejor opción, ya que es mucho más fácil mantener múltiples versiones. Entonces B haría una solicitud en la v3 del producto.

Savvas Kleanthous
fuente
Sí, estoy de acuerdo. Si bien ProductAddedo ProductDetailsChangedagregar complejidad al seguimiento de los cambios en el catálogo de productos, necesitamos mantener esa información sincronizada entre las bases de datos de alguna manera, en caso de que los eventos se reproduzcan y necesitemos acceder a los datos del catálogo del pasado.
Eithed
@eithed Actualicé la respuesta para ampliar algunos supuestos que hice.
Savvas Kleanthous