Patrones de diseño en Microservicios

Andrés Fernando Barón Sandoval
14 min readJun 20, 2022

--

En el entorno empresarial actual, las empresas deben responder a las necesidades de los clientes y a las condiciones cambiantes con mayor rapidez que nunca. Para poder cumplir con esta premisa, los equipos de desarrollo deben estar en la capacidad de implementar aplicaciones sencillas de mantener y de rápido escalamiento. Manejar un esquema dinámico para el desarrollo puede significar un factor diferenciador en términos de calidad y experiencia para el negocio.

Un enfoque muy popular para cumplir con este esquema, son los microservicios, los cuales, a diferencia de arquitecturas monolíticas, agrupan cambios de software más rápidos y sencillos. Las mejoras no solo se ven en términos de velocidad, sino también en resiliencia y visibilidad para el servicio. Para agrupar mejor estas ideas es clave entender los principios de los microservicios:

· Único propósito: Cada módulo, clase o función debería tener responsabilidad sobre una sola parte de la funcionalidad de ese programa, y ​​debería estar encapsulada.

· Bajo acoplamiento: Los servicios deberían ser agnósticos de los demás módulos, en caso en que se requiera adicionar una funcionalidad no debería afectar a las demás.

· Alta cohesión: Es la medida en la que un componente o clase realiza únicamente la tarea para la cual fue diseñada.

Figura 1. Principios de diseño en una arquitectura orientada a microservicios.

Integrar los principios mostrados en la figura 1 a una solución de un sistema, requiere de planeación y retos que pueden ser solucionados con la ayuda de patrones de diseño. A continuación se expondrán algunos usados comúnmente en la industria.

Patrones de integración

Agregador:

Cuando la lógica de negocio está dividida en varios servicios en muchas ocasiones se requerirá agregar los datos de diferentes componentes y luego enviar la respuesta final al consumidor. Una forma no muy deseable es otorgarle la responsabilidad al cliente y que este se encargue de obtener y unir la información de cada uno de los servicios para su presentación, sin embargo, una forma de no delegar tanta responsabilidad al cliente es mediante un microservicio intermedio que se encargue de realizar las llamadas a todos los componentes necesarios, consolidando los datos y transformándolos antes de devolverlos.

Figura 2. Representación de un microservicio intermedio a los demás componentes del sistema.

Proxy

Este patrón busca exponer microservicios únicamente sobre una puerta de enlace con el fin de encapsular toda la lógica en un solo punto de acceso. Cuando recibe una solicitud, la puerta de enlace API consulta un mapa de enrutamiento que especifica a qué servicio enviar la solicitud. A diferencia del agregador este proxy no compone información para entregar una respuesta al cliente.

Encadenamiento

En un sistema de microservicios siempre van a existir dependencias individuales, por ejemplo: El sistema de pagos de un E-commerce debe validar información de un cliente en un microservicio aparte, por lo cual debe solicitar los datos necesarios en una consulta externa para obtener la información. El patrón de encadenamiento ayuda a que múltiples microservicios se comuniquen entre ellos para completar sus dependencias de manera síncrona.

Figura 3. Representación del patrón de encadenamiento entre el servicio A y servicio B con el fin de completar sus dependencias de información.

Ramificación

El patrón de ramificación es una mezcla entre el agregador y el de encadenamiento, permitiendo múltiples procesamientos de comunicación entre microservicios, manejando su respectiva composición según la lógica lo requiera.

Patrones de resiliencia:

Circuit Breaker:
Este patrón implementa una funcionalidad similar a la de un switch de energía en un hogar, el flujo de corriente se puede activar o inactivar. En software este concepto nos permite manejar fallas en las cuales puede tomar una cantidad considerable de tiempo recuperarse, al conectarse a un servicio o recurso remoto. Para implementar este patrón de diseño se debe conservar el estado de la conexión a lo largo de un cierto número de solicitudes, con el fin de detectar las fallas y cambiar de estado el circuito. Los posibles estados del circuito son tres y corresponden a:

  • Cerrado:

Bajo la ausencia de errores, el circuito permanece en estado cerrado y todas las solicitudes pasan a través de los servicios como se haría normalmente. En caso que la cantidad de fallas sobrepase un rango determinado, el circuito se dispara y pasa al estado abierto.

  • Abierto:

El circuito devuelve un código de error para las solicitudes que lleguen a los servicios, sin tener una respuesta de la funcionalidad requerida.

  • Semi-abierto:

Después de un rango de tiempo, el circuito cambia a un estado semi-abierto para validar si el problema que ocasionó la apertura del circuito aún existe. Si falla al menos una vez en este estado, el circuito vuelve a pasar a modo abierto. Si la validación fue exitosa, el estado del circuito pasa a cerrado, es decir, a operación normal.

Figura 4. Estados posibles dentro de un patrón de diseño Circuit breaker

Timeout:

Lanza un error que ocurre cuando un programa no recibe una respuesta de otro microservicio. Dependiendo de cómo este escrito el programa, este error puede hacer que la aplicación se cierre o que aparezca un mensaje de error. Por ejemplo, al hacer ping a otra computadora, si esa computadora nunca responde, se recibe un error de tiempo de espera. Esta estrategia requiere un alto conocimiento del sistema para no llevar a falsos positivos [1].

Retries:

Cuando se genera un fallo en una respuesta de un microservicio, se puede manejar un cierto número de reintentos, con el fin de no devolver al servicio un error.

Deadline:

Funciona de manera similar al timeout, solo que en esta estrategia se define un rango límite de tiempo, para completar un llamado en cadena de varios microservicios, es decir si el request sobrepasa cierta hora especifica como deadline (e.i 18:54:38 UTC), el servicio retorna error.

Despliegue Blue-green:

Esta técnica reduce el tiempo de inactividad y el riesgo al ejecutar dos ambientes de producción idénticos. En cualquier momento, solo uno de los ambientes está en vivo con tráfico productivo. La implementación es una estrategia en la que se crean dos ambientes separados pero idénticos. Un entorno (azul) ejecuta la versión actual de la aplicación y un entorno (verde) ejecuta la nueva versión de la aplicación.
Al usar esta técnica sí ocurre algo inesperado con la nueva versión desplegada, se puede hacer rollback inmediatamente a la última versión funcional que fue desplegada.

Figura 5. Representación de un despliegue blue green [2].

Despliegue Canary:

Una despliegue de tipo canary, ejecuta una aplicación o servicio de forma incremental a un subconjunto de usuarios. Toda la infraestructura en un entorno de destino se actualiza en pequeñas proporciones porcentuales (por ejemplo, 2 %, 25 %, 75 %, 100 %). Las implementaciones de Canary permiten a las organizaciones realizar pruebas en producción con usuarios reales y comparar diferentes versiones de servicios una al lado de la otra. Es más económico que una implementación Blue-Green porque no requiere dos entornos de producción [3].

Despliegue Rolling-Update:

Es la estrategia de implementación predeterminada en Kubernetes. Reemplaza la versión existente de los pods con una nueva versión, actualizando los pods lentamente uno por uno, sin tiempo de inactividad del clúster.

El despliegue Rolling-Update usa una sonda de preparación para verificar si un nuevo pod está listo, antes de comenzar a reducir los pods con la versión anterior. Si hay un problema, puede detener una actualización y revertirla, sin detener todo el clúster [4].

Service Discovery:

En este patrón el cliente es responsable de determinar las ubicaciones de red de las instancias de servicio disponibles y las solicitudes de balanceo de carga entre ellas. El cliente se encarga de consultar un registro de servicios, el cual es una base de datos de instancias de servicios disponibles. Posteriormente, el cliente usa un algoritmo de balanceo de carga para seleccionar una de las instancias disponibles y realiza una solicitud.

Figura 6. Representación de un service registry entre microservicios.

La ubicación de red de una instancia de servicio se registra en el Service Registry cuando se inicia. Se elimina del Service Registry cuando finaliza la instancia. El Registry generalmente se actualiza periódicamente mediante un mecanismo de heartbeat [5].

Patrones de bases de datos

Base de datos por servicio:

Como su nombre lo indica, propone que cada servicio use su propia base de datos con el fin de desacoplar responsabilidades y escalar de forma eficiente. Esto permite acciones como cambiar el schema de una base de datos con impacto nulo en otros microservicios debido a la independencia en el acceso a datos. Adicionalmente brinda flexibilidad a la hora de escoger la naturaleza de la base de datos según se ajuste mejor a las necesidades entre NoSQL, SQL, llave valor, etc.

Bases de datos compartidas por servicio:

En este “anti” patrón una base de datos es compartida por varios servicios, al hacer esta práctica los microservicios pierden cualidades como escalabilidad y resiliencia.

CQRS (Command Query Responsibility Segregation):

Se encarga de separar las operaciones de lectura y escritura en una base de datos. Los principales beneficios en esta segregación son escalabilidad, rendimiento y seguridad.

Figura 7. Representación del patrón CQRS donde se puede observar el manejo de capa de datos en dos partes separadas, una de escritura y otra de lectura.

La flexibilidad creada al migrar a CQRS permite que un sistema evolucione mejor con el tiempo y evita que los comandos de actualización causen conflictos de fusión a nivel de dominio.

Event Sourcing:

Este patrón nos permite acumular eventos para posteriormente agregarlos en una secuencia en las bases de datos. Los eventos son preservados en un storage que actúa como sistema de registro, facilitando auditoria de logs, escalamiento horizontal y replicación del estado simplemente volviendo a aplicar los eventos.

Figura 8. Representación del patrón event sourcing para una orden de compra en una tienda.

Debido a que este patrón almacena eventos en lugar de objetos de dominio, en su mayoría evita el problema de desajuste de impedancia relacional de objetos.

Saga

El patrón de saga funciona como una secuencia de transacciones locales en la cual una transacción local es una unidad de trabajo realizada por un participante de la saga. Es decir, si una transacción falla esta debe revertirse y no le dará continuidad a la siguiente unidad de trabajo. Cada una de estas transacciones debe ser idempotente y reintentable. Se pueden manejar sagas en dos diseños de microservicios:

  • Orquestación:

Un coordinador se encarga de orquestar y gestionar las transacciones a realizar de una determinada tarea. Como se puede ver en la figura 9, un orquestador se encarga de monitorear las tareas asociadas a un servicio con el fin controlar el flujo de actividades dentro de un proceso de negocio.

Figura 9. Representación del patrón saga con diseño de orquestación, aquí el componente orquestador conoce como revertir la transacción del servicio a y b en caso de un fallo
  • Coreografía:

En este diseño de saga, no hay un coordinador como el anterior, en este se envían eventos para ser procesados por una plataforma de streaming de eventos como Kafka. Una ventaja de usar este diseño, es que es fácil de implementar y los endpoints quedan muy poco acoplados.

Figura 10. Representación del patrón Saga con diseño de coreografía

Cuando no hay una coordinación central, cada servicio produce y escucha los eventos de otro servicio y decide si se debe tomar una acción o no.

Composición del lado del cliente:

Este patrón delega la responsabilidad al cliente para consumir información de los servicios que requiera. Usualmente el cliente es un navegador cargando información de manera asincrónica por medio de llamados de Ajax. [6].

Figura 11. Representación del patrón composición del lado del cliente.

En la figura 11, se puede ver un ejemplo cuando un cliente accede a una pagina web , esta carga y renderiza solamente partes especificas de la misma, mientras que el resto se carga paulatinamente. En lugar de dejar que el usuario espere más tiempo cargando todo el contenido del sitio web a la vez, este patrón utiliza múltiples llamadas asincrónicas para obtener diferentes partes del sitio web y procesar cada fragmento cuando llega [6].

Sharding:

Este patrón ayuda al manejo de información dentro de bases de datos, ya que mediante la fragmentación o división horizontal de las mismas, permite un mayor escalamiento en sistemas distribuidos y descentralizados.

Hay dos métodos para abordar el crecimiento del sistema: escalado vertical y horizontal.

  • Escalado vertical :

Consiste en incrementar la capacidades como RAM, CPU o la capacidad de almacenamiento de un servidor. Una limitante de esto es que los proveedores basados ​​en la nube tienen límites estrictos según las configuraciones de hardware disponibles. Como resultado, existe un máximo práctico para la escala vertical.

  • Escalado horizontal:

Consiste en cargar el sistema de datos en varios servidores, con la posibilidad de agregar más maquinas de ser necesario. Si bien la velocidad o capacidad general de una sola máquina puede no ser alta, cada máquina maneja un subconjunto de la carga de trabajo general, lo que podría brindar una mayor eficiencia que un solo servidor de alta capacidad y alta velocidad. Ampliar la capacidad de la implementación solo requiere agregar servidores adicionales según sea necesario, lo que puede tener un costo general más bajo que el hardware de alta gama para una sola máquina[7].

Patrones de Observabilidad

Métricas de rendimiento:

Con el fin de poder monitorear los servicios de una aplicación, se hace menester contar con métricas de servicio para poder reportar y alertar. Herramientas como NewRelic se encargan de medir el rendimiento de una infraestructura de servicios, desde backend hasta frontend.

Figura 12. Monitor de métricas de rendimiento de un microservicio en NewRelic

Si se tuvieran muchos servicios y se necesitara registrar la información de cada uno, conviene usar un servicio de registro centralizado que agregue los registros de cada uno. El usuario debe ser capaz de buscar y analizar la información proporcionada por este servicio de registro. Adicionalmente se debe poder configurar alertas cuando algún tipo de mensaje llegue al monitor.

Seguimiento trazas distribuidas:

Es un método para monitorear aplicaciones creadas en una arquitectura de microservicios.

Los equipos de TI y DevOps usan el rastreo distribuido para seguir el curso de una solicitud o transacción a medida que viaja a través de la aplicación que se está monitoreando. Esto les permite identificar cuellos de botella, errores y otros problemas que afectan el rendimiento de la aplicación.

Figura 13. Ejemplo de conexión de multiples componentes de software que deben ser monitoreados dentro de una aplicación con el fin de tener un mayor entendimiento de la misma [8].

Esta comprensión del negocio ayuda a la resolución de problemas eficientemente, aumentando la satisfacción del cliente, garantizando ingresos constantes y ahorrando tiempo para que los equipos innoven en un ambiente construido con varios servicios interconectados.

Health Check:

Un health-check es una ruta donde se valida que el servicio está al aire y que pueda manejar transacciones o solicitudes de los clientes. De igual manera este endpoint ayuda a servicios de infraestructura a validar el status del host y de las conexiones, entre otros.

Patrones de Descomposición

Sidecar:

Despliega componentes de una aplicación en un proceso o contenedor separado para proporcionar aislamiento y encapsulación. Este patrón también permite que las aplicaciones estén compuestas por tecnologías y componentes heterogéneos.

El sidecar se adjunta a una aplicación principal proporcionando funciones de soporte como logging, configuración y proxy a servicios remotos entre otros. El sidecar también comparte el mismo ciclo de vida, se crea y se retira junto con la aplicación principal. El patrón sidecar también es conocido como patrón sidekick.[9]

Figura 14. Representación del patrón sidecar en el cual se maneja un almacenador de logs dentro del mismo pod, esto trae beneficios en latencia y red ya que comparten el mismo localhost.

Bulkhead:

Es un tipo de diseño de aplicación tolerante a fallas. En una arquitectura que implementa este patrón, los elementos de la aplicación se aíslan en grupos, de modo que si uno de ellos presenta una falla, los demás seguirán su funcionamiento normal. Su nombre se debe a las particiones seccionadas del casco de un barco. Si el casco de un barco se ve comprometido, solo la sección dañada se llena de agua, lo que evita que el barco se hunda. Un ejemplo de este patrón se puede observar con la configuración de un pod de Kubernetes, donde en el archivo env.yml se crea un contenedor aislado para ejecutar un único servicio, con sus propios recursos, límites de CPU y memoria. [10]

apiVersion: v1
kind: Pod
metadata:
name: drone-management
spec:
containers:
- name: drone-management-container
image: drone-service
resources:
requests:
memory: "64Mi"
cpu: "250m"
limits:
memory: "128Mi"
cpu: "1"

Este patrón es útil cuando se quiera escalar y aislar componentes de microservicios para disminuir el riesgo de disponibilidad y acceso a los recursos.

Strangler:

Este patrón ayuda a transformar progresivamente una aplicación monolítica en microservicios al ir reemplazando una funcionalidad particular de la aplicación con un nuevo servicio. Una vez que la nueva funcionalidad está lista, el componente anterior se "estrangula", el nuevo servicio se pone en uso y el componente anterior se retira por completo.

Cualquier nuevo desarrollo se realiza como parte del nuevo servicio y no como parte del monolito viejo. En la figura 15 se puede ver como a lo largo del tiempo mientras se vayan retirando funcionalidades, el monolito se reduce y el numero de servicios desacoplados aumenta.

Figura 15. Representación del patrón Strangler [11]

Descomposición por capa de negocio:

Un monolito se puede descomponer utilizando las capas de negocio de la empresa. Una capa empresarial es lo que hace una empresa para generar valor (por ejemplo, ventas, servicio al cliente o marketing). Por lo general, una organización tiene múltiples capacidades comerciales y estas varían según el sector o la industria [12].

Figura 16. Ejemplo de descomposición por capas de negocio dentro de una empresa.

Este patrón es de gran utilidad cuando el equipo tiene suficiente información sobre el negocio que se maneja, ya que de esta forma se puede hacer una mejor segmentación de las capas por unidad de trabajo.

Descomposición por sub-dominio:

Este patrón utiliza un sub-dominio de diseño controlado por dominio DDD (domain driven development) para descomponer monolitos. Este patrón es apropiado para sistemas monolíticos existentes que tienen límites bien definidos entre módulos relacionados con sub-dominios. Esto significa que puede descomponer el monolito al volver a empaquetar los módulos existentes como microservicios, pero sin reescribir significativamente el código existente.

Cada sub-dominio tiene un modelo y el alcance de ese modelo se denomina contexto acotado; los microservicios se desarrollan en torno a este contexto acotado.

Conclusión

Los microservicios ayuda a superar limitaciones de las arquitecturas monolíticas tradicionales, facilitando el proceso de identificación y resolución de la causa raíz de los problemas de rendimiento, aislando fallas mediante el manejo de módulos individuales, lo que significa que las aplicaciones más grandes no se ven afectadas por una sola falla. En consecuencia, se reduce el riesgo de tiempo de inactividad facilitando que los desarrolladores puedan revertir una actualización o realizar cambios en un módulo sin volver a implementar toda la aplicación. Cuando la atención se centra en un servicio específico en lugar de en toda la aplicación, es más fácil personalizar las necesidades de cada componente para mejorar la funcionalidad comercial. Esta arquitectura trae consigo varios desafíos que pueden agregar complejidad arquitectónica y carga operativa adicional, sin embargo estos pueden ser mitigados mediante el uso de patrones de diseño.

Referencias:

[1]. Forero, C., 2022. Maestria en ingeniería de software. [online] Coursera. Available at: <https://www.coursera.org/learn/desarrollo-de-aplicaciones-nativas-en-la-nube/lecture/NfOfQ/aclaracion-de-conceptos-sobre-estrategias-de-resiliencia> [Accessed 18 June 2022].

[2].Infinite Lambda. 2022. Blue/Green and Canary Deployment: A Hybrid Approach | Infinite Lambda. [online] Available at: <https://infinitelambda.com/post/canary-and-blue-green-deployments-with-helm-and-istio/> [Accessed 20 June 2022].

[3]. Harness.io. 2022. Intro to Deployment Strategies: Blue-Green, Canary, and More | Harness. [online] Available at: <https://harness.io/blog/continuous-verification/blue-green-canary-deployment-strategies/> [Accessed 20 June 2022].

[4]. 2022. [online] Available at: <https://spot.io/resources/kubernetes-autoscaling/5-kubernetes-deployment-strategies-roll-out-like-the-pros/> [Accessed 20 June 2022].

[5].Richardson, C., 2022. Service Discovery in a Microservices Architecture. [online] Available at: <https://www.nginx.com/blog/service-discovery-in-a-microservices-architecture/> [Accessed 18 June 2022].

[6]. Indrasiri, K., 2022. Design Patterns for Cloud Native Applications. [online] O’Reilly Online Learning. Available at: <https://learning.oreilly.com/library/view/design-patterns-for/9781492090700/ch04.html#data_composition_pattern> [Accessed 18 June 2022].

[7]. Mongodb.com. 2022. Sharding. [online] Available at: <https://www.mongodb.com/docs/manual/sharding/> [Accessed 18 June 2022].

[8]. Site24x7.com. 2022. What is Distributed Tracing|A guide to distributed tracing. [online] Available at: <https://www.site24x7.com/distributed-tracing.html> [Accessed 18 June 2022].

[9]. Docs.microsoft.com. 2022. Sidecar pattern — Azure Architecture Center. [online] Available at: <https://docs.microsoft.com/en-us/azure/architecture/patterns/sidecar> [Accessed 18 June 2022].

[10]. Docs.microsoft.com. 2022. Bulkhead pattern — Azure Architecture Center. [online] Available at: <https://docs.microsoft.com/en-us/azure/architecture/patterns/bulkhead> [Accessed 18 June 2022].

[11]. Livebook.manning.com. 2022. Chapter 13. Refactoring to microservices · Microservices Patterns. [online] Available at: <https://livebook.manning.com/book/microservices-patterns/chapter-13/23> [Accessed 18 June 2022].

[12]. https://docs.aws.amazon.com/. 2022. Decompose by business capability. [online] Available at: <https://docs.aws.amazon.com/prescriptive-guidance/latest/modernization-decomposing-monoliths/decompose-business-capability.html> [Accessed 18 June 2022].

--

--