¿Cómo funciona la desnormalización de datos con el Patrón de Microservicios?


Acabo de leer un artículo sobre Microservicios y Arquitectura PaaS. En ese artículo, alrededor de un tercio del camino hacia abajo, el autor afirma (bajo Denormalizar como loco):

Refactorice esquemas de bases de datos, y des-normalice todo, para permitir la separación y partición completa de datos. Es decir, no utilice tablas subyacentes que sirvan a varios microservicios. No debe haber uso compartido de tablas subyacentes que abarcan varios microservicios, ni datos. En su lugar, si varios servicios necesitan acceso a los mismos datos, deben compartirse a través de una API de servicio (como un REST publicado o una interfaz de servicio de mensajes).

Mientras que esto suena grande en teoría, en la práctica tiene algunos obstáculos serios que superar. El mayor de los cuales es que, a menudo, las bases de datos están estrechamente acopladas y cada tabla tiene alguna relación de clave externa con al menos otra tabla. Debido a esto podría ser imposible dividir un base de datos en n subbases controladas por n microservicios.

Así que pregunto: Dada una base de datos que consiste enteramente en tablas relacionadas, ¿cómo se desnormaliza esto en fragmentos más pequeños (grupos de tablas) para que los fragmentos puedan ser controlados por microservicios separados?

Por ejemplo, dada la siguiente base de datos (bastante pequeña, pero ejemplar):

[users] table
=============
user_id
user_first_name
user_last_name
user_email

[products] table
================
product_id
product_name
product_description
product_unit_price

[orders] table
==============
order_id
order_datetime
user_id

[products_x_orders] table (for line items in the order)
=======================================================
products_x_orders_id
product_id
order_id
quantity_ordered

No pases mucho tiempo criticando mi diseño, lo hice sobre la marcha. El el punto es que, para mí, tiene sentido lógico dividir esta base de datos en 3 microservicios:

  1. UserService - para usuarios CRUDding en el sistema; en última instancia, debe gestionar la tabla [users]; y
  2. ProductService - para CRUDding productos en el sistema; en última instancia, debe gestionar la tabla [products]; y
  3. OrderService - para órdenes CRUDding en el sistema; debe gestionar en última instancia el [orders] y [products_x_orders] tablas

Sin embargo, todas estas tablas tienen relaciones de clave externa con cada otro. Si los desnormalizamos y los tratamos como monolitos, pierden todo su significado semántico:

[users] table
=============
user_id
user_first_name
user_last_name
user_email

[products] table
================
product_id
product_name
product_description
product_unit_price

[orders] table
==============
order_id
order_datetime

[products_x_orders] table (for line items in the order)
=======================================================
products_x_orders_id
quantity_ordered

Ahora no hay forma de saber quién ordenó qué, en qué cantidad o cuándo.

Entonces, ¿este artículo es un típico alboroto académico, o hay una practicidad del mundo real en este enfoque de desnormalización, y si es así, cómo se ve (puntos de bonificación por usar mi ejemplo en la respuesta)?

Author: smeeb, 2014-11-19

4 answers

Esto es subjetivo, pero la siguiente solución funcionó para mí, mi equipo y nuestro equipo de base de datos.

  • En la capa de aplicación, los microservicios se descomponen en función semántica.
    • por ejemplo, un servicio Contact podría CRUD contactos (metadatos sobre contactos: nombres, números de teléfono, información de contacto, etc.)
    • por ejemplo, un servicio User podría CRUD usuarios con credenciales de inicio de sesión, roles de autorización, etc.
    • por ejemplo, un Payment servicio podría CRUD pagos y trabajar bajo el capó con un servicio compatible con PCI de terceros como Stripe, etc.
  • En la capa de base de datos, las tablas se pueden organizar de la manera que los devs/DBs/devops quieran que las tablas estén organizadas

El problema es con la cascada y los límites del servicio: Los pagos pueden necesitar que un Usuario sepa quién está haciendo un pago. En lugar de modelar sus servicios como este:

interface PaymentService {
    PaymentInfo makePayment(User user, Payment payment);
}

Modélalo así:

interface PaymentService {
    PaymentInfo makePayment(Long userId, Payment payment);
}

De esta manera, las entidades que pertenecen a otros microservicios solo son referenciado dentro de un servicio particular por ID, no por referencia de objeto. Esto permite que las tablas de BD tengan claves foráneas en todo el lugar, pero en la capa de la aplicación las entidades "foráneas" (es decir, las entidades que viven en otros servicios) están disponibles a través de ID. Esto evita que el objeto en cascada crezca fuera de control y delimita limpiamente los límites del servicio.

El problema en el que incurre es que requiere más llamadas a la red. Por ejemplo, si le di a cada entidad Payment una referencia User, podría obtener el usuario para un pago en particular con una sola llamada:

User user = paymentService.getUserForPayment(payment);

Pero usando lo que estoy sugiriendo aquí, necesitarás dos llamadas:

Long userId = paymentService.getPayment(payment).getUserId();
User user = userService.getUserById(userId);

Esto puede ser un factor decisivo. Pero si es inteligente e implementa almacenamiento en caché, e implementa microservicios bien diseñados que responden en 50 - 100 ms cada llamada, no tengo ninguna duda de que estas llamadas de red adicionales se pueden diseñar para no para incurrir en latencia para la aplicación.

 26
Author: smeeb,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-09-08 01:01:25

De hecho, es uno de los problemas clave en los microservicios que se omite de manera bastante conveniente en la mayoría de los artículos. Fortunatelly hay soluciones para esto. Como base para la discusión vamos a tener tablas que usted ha proporcionado en la pregunta. introduzca la descripción de la imagen aquí La imagen de arriba muestra cómo se verán las tablas en monolith. Solo unas pocas mesas con joins.


Para refactorizar esto a microservicios podemos usar algunas estrategias:

Api Join

En esta estrategia las claves foráneas entre los microservicios están rotos y el microservicio expone un punto final que imita esta clave. Por ejemplo: Product microservice expondrá findProductById endpoint. Order microservice puede usar este punto final en lugar de unirse.

introduzca la descripción de la imagen aquí Tiene una desventaja obvia. Es más lento.

Vistas de solo lectura

En la segunda solución puede crear una copia de la tabla en la segunda base de datos. La copia es de solo lectura. Cada microservicio puede usar operaciones mutables en sus tablas de lectura / escritura. Cuando viene a tablas de solo lectura que se copian de otras bases de datos que pueden (obviamente) utilizar solo lecturas introduzca la descripción de la imagen aquí

Alto rendimiento leer

Es posible lograr un alto rendimiento de lectura mediante la introducción de soluciones como redis/memcached en la parte superior de la solución read only view. Ambos lados de la unión deben copiarse en una estructura plana optimizada para la lectura. Puede introducir un microservicio sin estado completamente nuevo que se puede usar para leer desde este almacenamiento. Mientras que parece que un montón de molestia vale la pena señalar que tendrá un mayor rendimiento que la solución monolítica en la parte superior de la base de datos relacional.


Hay pocas soluciones posibles. Los que son más simples en la implementación tienen el rendimiento más bajo. Las soluciones de alto rendimiento tardarán unas semanas en implementarse.

 7
Author: Marcin Szymczak,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-11-14 14:19:47

Me doy cuenta de que esta posiblemente no sea una buena respuesta, pero qué diablos. Su pregunta era:

Dada una base de datos que consiste enteramente en tablas relacionadas, ¿cómo uno desnormaliza esto en fragmentos más pequeños (grupos de tablas)

WRT el diseño de la base de datos Yo diría "no se puede sin eliminar las claves foráneas".

Es decir, las personas que empujan Microservicios con la estricta regla de base de datos no compartida están pidiendo a los diseñadores de bases de datos que renuncien a las claves foráneas (y lo están haciendo implícita o explícitamente). Cuando no declaran explícitamente la pérdida de FK, te hace preguntarte si realmente conocen y reconocen el valor de las claves foráneas (porque con frecuencia no se menciona en absoluto).

He visto grandes sistemas divididos en grupos de tablas. En estos casos puede haber A) no se permiten FK entre los grupos o B) un grupo especial que contiene tablas "core" que pueden ser referenciadas por FK a tablas en otros grupos.

... pero en estos sistemas "grupos de tablas" a menudo son más de 50 tablas, por lo que no son lo suficientemente pequeños como para cumplir estrictamente con los microservicios.

Para mí, la otra cuestión relacionada a considerar con el enfoque de microservicios para dividir la base de datos es el impacto que esto tiene en la presentación de informes, la cuestión de cómo se reúnen todos los datos para la presentación de informes y/o la carga en un almacén de datos.

Algo relacionado está también la tendencia a ignorar las características de replicación de bases de datos integradas en favor de la mensajería (y cómo se basa en bases de datos replicación de las tablas principales / núcleo compartido DDD) afecta el diseño.

EDITAR: (el costo de UNIRSE a través de llamadas REST)

Cuando dividimos la base de datos como sugieren los microservicios y eliminamos FK, no solo perdemos la regla de negocio declarativa forzada (de FK), sino que también perdemos la capacidad de la base de datos para realizar la(s) unión (s) a través de esos límites.

En OLTP los valores FK generalmente no son "UX Friendly" y a menudo queremos unirnos a ellos.

En el ejemplo si obtenemos los últimos 100 pedidos que probablemente no queremos mostrar los valores de ID de cliente en la UX. En su lugar, necesitamos hacer una segunda llamada al cliente para obtener su nombre. Sin embargo, si también queríamos las líneas de pedido, también necesitamos hacer otra llamada al servicio de productos para mostrar el nombre del producto, sku, etc. en lugar de la identificación del producto.

En general podemos encontrar que cuando rompemos el diseño de la base de datos de esta manera necesitamos hacer muchas llamadas "JOIN via REST". Entonces, ¿cuál es el costo relativo de hacer ¿esto?

Historia real: Ejemplo de costos para' UNIRSE a través de REST ' vs DB Joins

Hay 4 microservicios y que implican una gran cantidad de "UNIRSE a través de REST". Una carga de referencia para estos 4 servicios llega a ~15 minutos. Esos 4 microservicios convertidos en 1 servicio con 4 módulos contra una base de datos compartida (que permite uniones) ejecuta la misma carga en ~20 segundos.

Desafortunadamente, esto no es una comparación directa de manzanas a manzanas para las uniones DB vs " UNIRSE a través de REST" como en este caso también cambiamos de una base de datos NoSQL a Postgres.

Es una sorpresa que "UNIRSE a través de REST" funciona relativamente mal en comparación con una base de datos que tiene un optimizador basado en costos, etc.

Hasta cierto punto cuando dividimos la base de datos de esta manera, también nos estamos alejando del 'optimizador basado en costos' y todo lo que hace con la planificación de ejecución de consultas para nosotros en favor de escribir nuestra propia lógica de unión (de alguna manera estamos escribiendo nuestro propio plan de ejecución de consultas relativamente poco sofisticado).

 2
Author: Rob Bygrave,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-10-29 09:45:24

Vería cada microservicio como un Objeto, y como cualquier OR , utilizas esos objetos para extraer los datos y luego crear uniones dentro de tus colecciones de código y consulta, los microservicios deben manejarse de manera similar. La diferencia solo aquí será que cada Microservicio representará un Objeto a la vez que un Árbol de Objetos completo. Una capa de API debe consumir estos servicios y modelar los datos de una manera que debe presentarse o almacenarse.

Hacer varias llamadas de vuelta a los servicios para cada transacción no tendrá un impacto ya que cada servicio se ejecuta en un contenedor separado y todas estas calles se pueden ejecutar en paralelo.

@ccit-spence, me gustó el enfoque de los servicios de intersección, pero ¿cómo puede ser diseñado y consumido por otros servicios? Creo que creará una especie de dependencia para otros servicios.

¿Algún comentario, por favor?

 0
Author: user1294878,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-03-27 22:40:33