Una de las razones de la popularidad de los microservicios es la posibilidad de un desarrollo autónomo e independiente. En esencia, la arquitectura de microservicios es el intercambio de la posibilidad de desarrollo autónomo para una implementación, prueba, depuración y monitoreo más complejo (en comparación con un monolito). Pero tenga en cuenta que los microservicios no perdonan la separación de responsabilidades. Si la separación de funciones es incorrecta, se producen cambios dependientes frecuentes en diferentes servicios. Y esto es mucho más doloroso y más complicado que los cambios coordinados dentro del marco de diferentes módulos o paquetes dentro del monolito. Los cambios consistentes en los microservicios se complican por el diseño, implementación, prueba, etc.
Y me gustaría hablar sobre los diversos patrones y antipatrones de la división de responsabilidades en microservicios.
Entidad de servicio como antipatrón
La "entidad de servicio" es uno de los posibles patrones (anti) del diseño de la arquitectura de microservicios, que conduce a un código altamente dependiente en diferentes servicios y acoplado libremente dentro de los servicios.
Para la mayoría de los desarrolladores, parece que al seleccionar servicios de acuerdo con la esencia del área temática: "trato", "persona", "cliente", "orden", "imagen", sigue los principios de responsabilidad exclusiva y, además, a menudo esto parece lógico. Pero el enfoque de entidad de servicio puede convertirse en un antipatrón. Esto sucede porque la mayoría de las características o cambios afectan a varias entidades, y no a una. Como resultado, cada uno de estos servicios combina la lógica de diferentes procesos comerciales.
Por ejemplo, tome una tienda en línea. Decidimos destacar los servicios "producto", "pedido", "cliente".
¿Qué cambios y servicios debo hacer para agregar la entrega a domicilio?
Por ejemplo, puedes hacer esto:
- en el servicio "pedido" agregue la dirección de entrega, el tiempo deseado y el repartidor
- en el servicio al cliente, agregue una lista de direcciones de entrega seleccionadas para el cliente
- en el servicio "producto" agregue una lista de entidades de bienes
Para la interfaz del proveedor, será necesario hacer un método API separado en el servicio de "pedido", que proporcionará una lista de los pedidos asignados a este proveedor en particular. Además, se necesitarán métodos para eliminar productos del pedido que no se ajustaban o que el cliente rechazó en el momento de la entrega.
¿O qué cambios y en qué servicios debo hacer para agregar descuentos en el código promocional?
Como mínimo necesitas:
- agregar un código promocional al servicio de "pedido"
- en el servicio "producto" agregue si se aplican descuentos en el código promocional de este producto
- en el servicio al cliente, agregue una lista de códigos promocionales que se emitieron al cliente
En la interfaz del administrador, agregar un código promocional personalizado al cliente es un método separado en el servicio al cliente, que está disponible solo para los gerentes de tienda, pero no está disponible para el cliente mismo. Y en el servicio de "producto", cree un método que proporcione una lista de productos que están afectados por el código promocional, para que sea más fácil para el cliente elegir en su interfaz.
Las fuentes de los cambios en el servicio pueden ser varios procesos comerciales: selección y diseño, pago y facturación, entrega. Cada una de las áreas problemáticas tiene sus propias limitaciones, invariantes y requisitos para el pedido. Como resultado, resulta que en el servicio "producto" almacenamos información sobre el producto, sobre descuentos y saldos de productos en almacenes. Y en el "pedido" se almacena la lógica del repartidor.
En otras palabras, un cambio en la lógica de negocios que se extiende a varios servicios conduce a cambios dependientes en varios servicios. Y al mismo tiempo en un servicio hay un código que no está conectado entre sí.
Servicios de almacenaje
Parece que este problema puede resolverse si se crea un servicio de "capa" separado sobre los servicios de la entidad, que encapsulan toda la lógica. Pero generalmente esto también termina mal. Porque entonces los servicios de la entidad se convierten en servicios de almacenamiento, es decir toda la lógica de negocios se elimina de ellos, excepto el almacenamiento.
Si los datos se almacenan en diferentes bases de datos, en diferentes máquinas, entonces
- perdemos rendimiento porque no proporcionamos datos directamente desde la base de datos, sino a través de la capa de servicio
- perdemos flexibilidad porque la API del servicio suele ser mucho menos flexible que SQL o cualquier otro lenguaje de consulta
- perdemos flexibilidad, porque es difícil hacer fusiones de datos de diferentes servicios
Si diferentes servicios de la entidad tienen acceso a otras bases de datos, entonces la comunicación entre los servicios se produce implícitamente: a través de una base de datos común, entonces, para realizar cualquier cambio que afecte un cambio de esquema de datos, es posible solo después de verificar que este cambio no interrumpirá todos los demás servicios que usan esta base de datos o tableta .
Además del desarrollo complejo, dichos servicios se vuelven excesivamente críticos y muy cargados: con casi todas las solicitudes de un servicio de nivel superior, debe realizar varias solicitudes a diferentes entidades de servicio, lo que significa que editarlas se vuelve aún más difícil para satisfacer los mayores requisitos de confiabilidad y rendimiento.
Debido a tales dificultades con el desarrollo y el soporte de los servicios de la entidad en su forma pura, rara vez se ve un patrón, por lo general, los servicios de la entidad se convierten en uno o dos "monolitos de microservicios" centrales, que a menudo cambian y contienen la lógica comercial principal y los colocadores de microservicios pequeños, generalmente infraestructura y pequeños que rara vez cambian.
Separación por áreas problemáticas.
Los cambios en sí mismos no nacen, provienen de algún área problemática. Un área problemática es un área de tareas dentro de la cual los problemas que requieren cambios en el código se formulan en un idioma, utilizando un conjunto de conceptos o interconectados por la lógica empresarial. En consecuencia, dentro del marco de un área problemática, lo más probable es que haya un conjunto de restricciones, invariantes en las que puede confiar al escribir código.
La separación de la responsabilidad de los servicios por áreas problemáticas, en lugar de por entidades, generalmente conduce a una arquitectura más compatible y comprensible. Las áreas problemáticas con mayor frecuencia corresponden a procesos comerciales. Para la tienda en línea, las áreas problemáticas más probables serán "pago y facturación", "entrega", "proceso de pedido".
Los cambios que afectarían varias áreas problemáticas al mismo tiempo son menores que los cambios que afectarían a varias entidades.
Además, los servicios desglosados por procesos comerciales pueden reutilizarse en el futuro. Por ejemplo, si al lado de la tienda en línea quisiéramos hacer otra venta de boletos de avión, podríamos reutilizar el servicio general "Facturación y pago". Y no haga otro similar, sino específico para la venta de entradas.
Por ejemplo, podemos dividirnos en servicios:
- Un servicio o grupo de servicios de “Entrega”, que almacenará la lógica del trabajo con la entrega de un pedido específico, la organización del trabajo de los proveedores, la evaluación de la calidad de su trabajo, la aplicación móvil del proveedor, etc.
- Un servicio o un grupo de servicios de "Facturación y pago", que almacenará la lógica de trabajo con el pago, las cuentas de pago para las personas jurídicas, la generación de contratos y los documentos de cierre.
- Servicio o grupo de servicios "Proceso de pedido", que almacena la lógica de la elección del cliente de productos, catalogación, marcas, lógica de la cesta, etc.
- Servicio de "autorización y autenticación".
- Incluso puede tener sentido separar el servicio de descuento.
Para interactuar entre sí, los servicios pueden usar el modelo de eventos o intercambiar objetos simples entre sí (api relajante, grpc, etc.). Es cierto que vale la pena señalar que no es fácil organizar la interacción entre dichos servicios correctamente. Como mínimo, la descentralización de datos tiene problemas de consistencia en algún momento (consistencia eventual) y transaccionalidad (en el caso cuando es importante).
La descentralización de datos, el intercambio de objetos simples tiene sus ventajas, desventajas y desventajas. Por un lado, la descentralización permite desarrollar y operar de manera independiente varios servicios. Por otro lado, el costo de almacenar dos o tres copias de datos y mantener la consistencia en diferentes sistemas.
En la vida real, a menudo ocurre algo intermedio. Entidad de servicio con un conjunto mínimo de atributos que utilizan todos los servicios por parte de los consumidores. Y una capa mínima de lógica, por ejemplo, un modelo de estado y eventos en la cola con la notificación de todos los cambios en la entidad. Al mismo tiempo, los servicios al consumidor a menudo mantienen un "caché" de datos. Se está haciendo todo lo posible para que haya tan pocos cambios como sea posible en dicho servicio, y esto, en principio, es difícil de hacer debido al hecho de que hay muchos consumidores.
Al mismo tiempo, es importante comprender que cualquier partición, tanto por entidad como por área problemática, no es una bala de plata, siempre habrá características que requerirán cambios dependientes en varios servicios. Es solo que con un desglose habrá muchos más cambios de este tipo que con otro. Y la tarea del desarrollo es minimizar el número de cambios dependientes.
Una división ideal solo es posible si tiene dos productos completamente independientes. En cualquier negocio tiene todo conectado con todo, la única pregunta es cuánto está conectado.
Y la pregunta está en la separación de responsabilidades y en la altura de las barreras a las abstracciones.
API de servicio de diseño
El diseño de interfaces dentro del servicio repite el historial con el desglose en servicios, solo en una escala más pequeña. Cambiar la interfaz (no solo una extensión) es complejo y requiere mucho tiempo. En aplicaciones complejas, la interfaz debe ser lo suficientemente universal como para no causar cambios constantes, y debe ser lo suficientemente específica y específica como para no provocar la difusión de la responsabilidad y la semántica.
Por lo tanto, las interfaces de servicio deben diseñarse de modo que su semántica sea resistente a los cambios. Y esto es posible si la semántica o el área de responsabilidad de la interfaz se basa en las limitaciones del área del problema.
Interfaces CRUD para servicios con lógica empresarial compleja
Una interfaz que es demasiado amplia e inespecífica contribuye a la erosión de la responsabilidad o la complejidad excesiva.
Por ejemplo, API CRUD para servicios con lógica empresarial compleja, tales interfaces no encapsulan el comportamiento. No solo permiten que la lógica empresarial se filtre a otros servicios y erosionan la responsabilidad del servicio, sino que provocan la difusión de la lógica empresarial: las restricciones, los invariantes y los métodos para trabajar con datos ahora se encuentran en otros servicios. Los servicios de usuario de interfaz (API) deben implementar la lógica ellos mismos.
Si intentamos, sin cambiar significativamente la interfaz, transferir la lógica empresarial al servicio, obtendremos un método demasiado universal y demasiado complicado.
Por ejemplo, hay un servicio de entradas. Un boleto puede ser de diferentes tipos. Cada tipo tiene un conjunto diferente de campos y una validación ligeramente diferente. El ticket también tiene un modelo de estado: una máquina de estado para la transición de un estado a otro.
Deje que la API se vea así: métodos POST / PATCH / GET, url /api/v1/tickets/{ticket_idasket.json
Entonces, puedes actualizar el ticket
PATCH /api/v1/tickets/{ticket_id}.json { "type": "bug", "status": "closed", "description": " " }
Si el modelo de estado dependerá del ticket, entonces son posibles conflictos de lógica comercial. Primero, cambie el estado de acuerdo con el modelo de estado anterior y luego cambie el tipo de ticket. O viceversa?
Resulta que dentro del método API habrá un código que no está conectado entre sí: campos de entidad cambiantes, una lista de campos disponibles, según el tipo de ticket, y un modelo de estado. Cambian por varias razones y tiene sentido distribuirlos de acuerdo con diferentes métodos e interfaces API.
Si cambiar un campo dentro del marco de los métodos API CRUD no es solo un cambio de datos, sino una operación relacionada con un cambio coordinado en el estado de una entidad, entonces esta operación debe llevarse a un método separado y no debe permitirse que se cambie directamente. Si cambiar una API sin compatibilidad con versiones anteriores es muy malo (para API públicas), es mejor pensar de inmediato al diseñar la API.
Por lo tanto, para evitar tales problemas, es mejor hacer las interfaces pequeñas, específicas y tan orientadas a los problemas como sea posible, en lugar de las universales centradas en datos.
Este patrón (anti) suele ser característico de las interfaces RESTful, debido al hecho de que, de manera predeterminada, solo hay unos pocos "verbos" centrados en datos de acciones para crear, eliminar, actualizar, leer. No hay operaciones de entidad específicas del negocio
¿Qué se puede hacer para que RESTful esté más orientado a los problemas?
Primero, puede agregar métodos a las entidades. La interfaz se está volviendo menos tranquila. Pero hay tal oportunidad. Todavía no luchamos por la pureza de la carrera, sino que resolvemos problemas prácticos.
En lugar del recurso universal
/api/v1/tickets.json
agregue más recursos:
/api/v1/tickets/{ticket_id}/migrate.json
: migra de un tipo a otro
/api/v1/tickets/{ticket_id}/status.json
: si hay un modelo de estado
En segundo lugar, puede imaginar cualquier operación como un recurso dentro del marco de REST. ¿Existe una operación de migración de tickets de un tipo a otro (o de un proyecto a otro?). Ok, entonces habrá un recurso
/api/v1/tickets/migration.json
¿Existe una operación comercial para crear una suscripción de prueba?
/api/v1/subscriptions/trial.json
¿Hay una operación de transferencia de dinero?
/api/v1/money_transfers.json
Etc.
El antipatrón con la API centrada en datos también se refiere a la interacción rpc. Por ejemplo, la presencia de métodos demasiado generales como editAccount () o editTicket (). "Modificar un objeto" no lleva la carga semántica asociada con el área del problema. Esto significa que se llamará a este método por varias razones, por varias razones para cambiar.
Cabe señalar que las interfaces centradas en datos están bastante bien, si el área del problema involucra solo almacenar, recibir y modificar datos.
Modelo de evento
Una forma de desatar fragmentos de código es organizar la interacción entre los servicios a través de una cola de mensajes.
Por ejemplo, si en el servicio, al registrar un usuario, debemos enviarle una carta de bienvenida, crear una solicitud en CRM para un administrador de clientes, etc., entonces es lógico no hacer una llamada de servicio externa, sino poner el mensaje "el usuario 123 está registrado" en el servicio de registro ", Y todos los servicios necesarios leerán este mensaje y tomarán las medidas necesarias. Al mismo tiempo, cambiar la lógica de negocios no requerirá cambiar el servicio de registro.
En la mayoría de los casos, no solo se arrojan mensajes a la cola, sino también eventos. Como la cola es solo un protocolo de transporte, se aplican las mismas restricciones a la interfaz de datos que a la interfaz síncrona normal. Por lo tanto, para evitar problemas al cambiar la interfaz y las ediciones posteriores en otros servicios, es mejor hacer que los eventos estén lo más orientados a los problemas posibles. Aún así, tales eventos a menudo se denominan eventos de dominio. Al mismo tiempo, el uso del modelo de evento generalmente no afecta en gran medida los límites en los que los (micro) servicios luchan.
Dado que los eventos de dominio se traducen prácticamente 1 en 1 a métodos API síncronos, a veces incluso sugieren usar una secuencia de eventos en lugar de una secuencia de eventos en lugar de una llamada API (Fuente de eventos). Por el flujo de eventos, siempre puede restaurar el estado de los objetos, pero también tiene un historial libre. De hecho, generalmente este enfoque no es muy flexible: debe admitir todos los eventos y, a menudo, es más fácil mantener una historia junto con la API habitual.
Microservicios y rendimiento. Cqrs
En principio, el área del problema implica cambios en el código asociado no solo con los requisitos comerciales funcionales, sino también con los no funcionales, por ejemplo, el rendimiento. Si hay dos partes de código con diferentes requisitos de rendimiento, esto significa que estas dos partes de código pueden tener sentido para separarse. Y generalmente se dividen en servicios separados para poder utilizar diferentes idiomas y tecnologías que son más adecuados para la tarea.
Por ejemplo, hay un método de calculadora enlazado a la CPU en un servicio escrito en PHP que realiza cálculos complejos. Con un aumento en la carga y la cantidad de datos, dejó de hacer frente. Y, por supuesto, como una de las opciones, tiene sentido hacer cálculos no en código php, sino en un demonio de sistema de alto rendimiento separado.
Como uno de los ejemplos de la división de servicios por el principio de productividad: la separación de servicios en lectura y modificación (CQRS). Esta separación a menudo se ofrece porque los requisitos de rendimiento de los servicios de lectura y escritura son diferentes. La carga de lectura es a menudo un orden de magnitud mayor que la carga de escritura. Y los requisitos para la velocidad de respuesta de las solicitudes de lectura son mucho más altos que para la escritura.
El cliente pasa el 99% del tiempo en la búsqueda de bienes, y solo el 1% del tiempo en el proceso de pedido. Para un cliente en un estado de búsqueda, la velocidad de visualización es importante y las características relacionadas con los filtros, varias opciones para mostrar productos, etc. Por lo tanto, tiene sentido resaltar un servicio separado que es responsable de la búsqueda, el filtrado y la exhibición de productos. Tal servicio probablemente funcionará en algún tipo de ELK, una base de datos orientada a documentos con datos desnormalizados.
Obviamente, una división ingenua en servicios de lectura y modificación no siempre es buena.
Un ejemplo Para un gerente que trabaja con el llenado de la gama de productos, las características principales serán la capacidad de agregar productos, eliminarlos, cambiarlos y verlos convenientemente. No hay mucha carga, si separamos la lectura y el cambio a servicios separados, no obtendremos nada de dicha separación, excepto por problemas cuando sea necesario hacer cambios coordinados en los servicios.