Consultas en una arquitectura de microservicios

Cuando migramos a una arquitectura de microservicios, hacer consultas es un reto, en este blog te muestro 2 patrones para hacer consultas a información distribuida.

Diseño web
8 minutos
Hace 4 años
Consultas en una arquitectura de microservicios

¡Accede a cientos de cursos
con solo un pago al año!

Blog Microbanner

Cuando nuestra información está centralizada en una base de datos, no tenemos que preocuparnos por como consultar la información, esto cambia cuando distribuimos nuestra información en diferentes bases de datos en una arquitectura de microservicios. En este blog te explicaré 2 patrones con los cuales podremos consultar nuestra información.

Primero hay que entender, por que guardaríamos nuestra información en diferentes base de datos.

Podemos llegar a este punto cuando estamos migrando una aplicación monolítica a una de microservicios. La aplicación monolítica la cual se encarga de manejar todos los procesos del negocio suele manejar una sola base de datos donde centraliza toda la información, por ejemplo, en el caso de EDteam:

Pero cuando llevamos esa aplicación a una arquitectura de microservicios, se pasa a tener una base de datos por cada servicio:

Aquí es donde entra el problema. Antes, en la aplicación monolítica, si yo quisiera consultar el detalle de las facturas de un usuario, se tendría que seguir en siguiente flujo:

Como ven, el flujo es sencillo, al tener la información en una sola base de datos, yo puedo hacer uso de los JOIN para unir la información de diferentes tablas y así responderle al cliente de una forma rápida, pero esto es algo que no podríamos si tenemos nuestra información en diferentes bases de datos, es por eso, que les voy a mostrar 2 patrones que nos ayudarán a consultar nuestra información.

API Composition

Este patrón lo que nos dice es que creemos un Composer que es el que implementa la consulta y que este, consulte a los Provider Services necesarios para unir la información que se requiere para la consulta.

Aquí podemos ver el flujo de este patrón:

El Composer podría ser una de las siguientes opciones:

  • Client (Web, mobile, etc)
  • API Gateway
  • Service Standalone

Cliente Composer

Esta no es una buena idea si el cliente no esta dentro del mismo firewall que nuestros servicios, ya que las consultas demorarían más en ser completadas, el flujo seria como el de antes:

API Gateway Composer

Una API Gateway es un servicio que sirve como puerta de enlace para nuestros clientes, es decir, los clientes no hacen la consulta directamente a un microservicio, sino que este hace la petición al Gateway y este consulta a los servicios:

Este es responsable de enrutar las peticiones, API composition y autenticación. Un API Gateway también nos permite usar otros protocolos de comunicación como gRPC, haciendo que las peticiones sean mucho más rápidas.

Service Standalone Composer

Este es parecido al API Gateway, con la diferencia de que este es un servicio independiente que se encarga de manejar peticiones relacionadas, por ejemplo, un servicio que maneje peticiones que tengan que ver con facturas, podría haber otro, que se encargue de peticiones relacionadas con los usuarios.

Problemas

Independientemente la manera en que implementemos el patrón nos vamos a encontrar con los siguientes problemas:

Tiempo de respuesta

Actualmente, el flujo de este patrón es el siguiente:

Este flujo es muy lento, ya que las consultas a los provider services están siendo de forma síncrona, es decir, para que el composer haga la petición al Service B, primero tiene que terminar con la petición del Service A

Una solución a ese problema es usar peticiones de forma paralela:

Ahora el composer, no necesita esperar a que se termine de realizar la petición al Service A, él puede ir a los siguientes servicios y hacer las peticiones, aunque habrá servicios que van a depender de la respuesta de otro, como es el caso del Course Service, primero nosotros necesitamos saber que cursos consultar, y eso lo sabemos al consultar el Invoice Service, pero aun así, hemos logrado mejorar el tiempo de respuesta al cliente.

Más gastos en la transferencia de datos

Antes, con la arquitectura monolítica, se hacían normalmente 2 peticiones, del cliente al backend, y del backend a la base de datos:

Pero ahora, con la arquitectura de microservicios e implementando el patrón API Composition hay muchas más peticiones:

Ahora el cliente hace una petición al composer, el composer a cada uno de los provider services necesarios, y cada uno de los provider services, hace una petición a su base de datos.

Disponibilidad

Van a haber momentos, en los que un servicio no va a estar disponible por X razón, y esto nos trae un problema, nosotros le estaríamos devolviendo un error al cliente.

Esto lo podríamos resolver de las siguientes maneras:

Cachear la información

Al composer ver que el Service A no está disponible, podría responder con información de la caché

Devolver información incompleta

Talvez la información del Service A no es importante, por lo que se podría omitir, y responder con la información del Service B y Service C

Consultas complejas

Hasta ahora hemos manejado una consulta relativamente fácil, traernos el detalle de las facturas de un usuario, pero que sucede si volvemos más compleja esta consulta al agregarle filtros, ahora queremos el detalle de las facturas de un usuario, si este es estudiante universitario, y si la factura tiene algún curso con el tag de programación.

Al agregarle esos filtros, nuestro flujo anterior se rompe, ya no podríamos hacer consultas paralelas

Ahora, el Invoice Service depende de la respuesta del User Service porque ahora hay que verificar que el usuario sea estudiante universitario, si este no lo es, no me interesa hacer la petición a los demás servicios, ya que estaría gastando recursos innecesariamente. Pero no es solo eso, también debemos filtrar las facturas en las que se haya comprado un curso con el tag de programación, pero ese dato no lo sabemos en Invoice Service por lo que primero, tendríamos que traernos todas las facturas y luego los cursos con el filtro aplicado, y el composer se encargaría de eliminar las facturas en las que no se haya comprado un curso con el tag de programación y darle una respuesta al cliente, pero para esto tuvimos que traernos consultas que no eran necesarias y luego encargarle al composer de hacer un filtro lo cual es muy ineficiente.

Como vemos, el patrón es fácil de entender e implementar, pero solo nos funciona cuando las consultas son sencillas, es por eso que ahora les hablaré del patrón CQRS que nos ayudará a realizar esas consultas donde API Composition se vuelve ineficiente.

CQRS

Este patrón el cual extiende de Command Query Responsability Segregation es un patrón que busca separar las responsabilidades en 2 partes:

  • Commands
    • Los comandos son aquellas operaciones donde se altera la información:
      • CREATE
      • UPDATE
      • DELETE
  • Queries
    • Son las operaciones en las que se lee la información, pero no se altera
      • READ

Si se fijan es separar las operaciones del CRUD, pero, ¿por qué hacer esta separación?

En un enfoque tradicional, nosotros manejamos un modelo para tratar todo lo que tenga que ver con un usuario, por ejemplo, ese modelo lo usamos en todas las operaciones: CREATE, READ, UPDATE, DELETE, entonces, cuando queremos registrar un usuario, nosotros transportamos la información en el User Model, el cual aplica validaciones a la información, por ejemplo, valida que el correo tenga una estructura válida, y si todo está bien, se guarda en la base de datos

El problema es que, cuando tocamos lógica que se utiliza en las consultas (Query), nos arriesgamos a que rompamos lógica que se utiliza en la creación, actualización, eliminación (Command), ya que el User Model tiene 2 responsabilidades.

Cuando aplicamos CQRS pasamos a tener más de un Modelo, ahora podemos tener un modelo para las Query y otro para los Command:

Y con esa separación, yo puedo hacer modificaciones en el Query User Model sin afectar a los demás Models

Pero, nosotros podemos llevar este patrón a otro nivel, hasta ahora hemos manejado una sola base de datos para las Query y Command, pero nosotros también podríamos separar las responsabilidades de la base de datos, teniendo ahora una base datos para la consulta de información y otra para la persistencia, que sería nuestra base de datos principal:

Al hacer esta separación, podríamos tener una base de datos que esté optimizada para consultar información, haciendo que nuestras peticiones se realicen de una forma más rápida, podríamos usar una base de datos NoSQL como mongodb o elasticsearch.

El problema es que al hacer esta separación, agregamos complejidad a la arquitectura, y es que ahora, al tener 2 bases de datos, tenemos que tener sincronizada la información de ambas, tiene que haber consistencia, para esto podríamos usar patrones como Event Sourcing y Saga Pattern.

Y otro nivel al que podríamos llevar el patrón, es a nivel de servicios

Nosotros podríamos tener un servidor exclusivo para consumir información de los servicios A, B y C, cuando se realice una operación de Command en cualquiera de los servicios A, B o C, esa información se envía al Query Service y este la almacena en su base de datos que está optimizada para consultar información.

Esta es la forma en la que en EDteam implementamos el buscador. Como saben, en EDteam contamos con un buscador que busca entre los blogs, publicaciones de la comunidad, cursos y especialidades:

Lo que nosotros hacemos, es que cuando se hace un Command en los cursos, especialidades, blog o publicaciones de la comunidad, enviamos esa información al Query Service, el cual usa Elasticsearch que es un motor de búsqueda que está optimizado para estas operaciones.

Por lo cual, cuando tú haces una búsqueda en EDteam, internamente se está haciendo una consulta al Query Service que a su vez consulta Elasticsearch para hacer una búsqueda eficiente y poder responder rápido.

Y no sé si lo notaron, pero con CQRS volvemos al flujo que teníamos en nuestra arquitectura monolítica

Pero hay 2 grandes cambios, ahora nuestra base de datos está optimizada para las consultas del servidor y nosotros almacenamos la información de forma de normalizada, es decir, ya no tendríamos que hacer un JOIN de la información y una ventaja frente al patrón API Composition, es que ya no tenemos que hacer una gran cantidad de peticiones, y podemos realizar cualquier tipo de consulta como lo hacíamos antes, ya que nuestra información está centralizada, pero todo esto, al agregar más complejidad a la arquitectura.

Conclusión

En resumen podemos ver que el patrón API Composition nos servirá para aquellas consultas que son sencillas pero, cuando haya consultas más complejas, lo mejor será utilizar el patrón CQRS.

Al final lo ideal es que usemos los 2 patrones y aprovechar sus beneficios, al usar API Composition nos evitamos agregar complejidad innecesaria por consultas que no lo valen y con CQRS logramos hacer aquellas consultas en las que API Composition se vuelve ineficiente.

Eso ha sido todo por mi parte, ¡nos vemos en la próxima 👋 !

Comentarios de los usuarios