Consolida tu arquitectura de microservicios con Service Mesh

En el mundo de las arquitecturas de microservicios, se está llegado a un punto en el cual, manejar la complejidad del sistema se vuelve vital para poder aprovechar los beneficios que aporta.

En la actualidad, el cómo realizar la implementación de estos microservicios ya no es un problema, ya que existe un amplio abanico de frameworks disponibles (Spring Boot, Vert.X, NodeJs…).

Tampoco lo es si lo miramos desde el punto de vista de la manera de llevar a cabo el despliegue, el cual está cubierto por herramientas como Docker, Kubernetes, Openshift…  Ahora el desafío se encuentra en la propia comunicación entre estos microservicios.

En un principio la comunicación entre microservicios se realizaba punto a punto y se dotaba a cada microservicio de “inteligencia adicional”. Así se controlaban muchos aspectos importantes de la comunicación, como el enrutamiento, la tolerancia a fallos, la latencia, descubrimiento de servicios, la trazabilidad distribuida, la seguridad, etc…, dejando el canal de comunicación como una mera “tubería” por la que viajaban los mensajes.

En la actualidad algunos de estos aspectos (como por ejemplo el descubrimiento de servicios) ya son proporcionados por algunas de las plataformas de orquestación (por ejemplo Openshift), pero aún no los cubren en su totalidad.

Sin embargo, este enfoque es insuficiente e incluso se hace poco viable cuando aumenta el volumen de microservicios y la complejidad de los mismos.

Como ejemplo de todo esto, tenemos el caso de un desarrollador de un microservicio, que a la hora de realizar la implementación del mismo tiene que tener en cuenta:

  • La implementación de la lógica de negocio propia de la funcionalidad del servicio (los cálculos, la composición del servicio, etc.).
  • La implementación de las funciones de red, que se ocupan de los mecanismos de comunicación con otros servicios (aplicar patrones de resiliencia y estabilidad, descubrimiento de servicio, etc.).

Teniendo en cuenta esto, si nos planteamos la cantidad de esfuerzo dedicado a la implementación de un microservicio, podemos ver que una buena parte del tiempo se dedica a la construcción de estas funcionalidades de red orientadas a posibilitar la comunicación entre microservicios.

Pero es que además esto se acentúa si en nuestra arquitectura conviven microservicios construidos con múltiples tecnologías (y/o múltiples lenguajes de programación), para los que se necesita realizar los mismos esfuerzos, en mayor o menor medida, para cada uno de las tecnologías y/o lenguajes…

Igualmente, también es necesario tener un control más exhaustivo de todo lo que está pasando con nuestros microservicios: monitorización, métricas, trazabilidad distribuida de las peticiones, seguridad…, lo cual hace aumentar la complejidad de todo el sistema.

Ahora bien, dado que la mayoría de los requisitos de comunicación entre servicios son bastante genéricos en todas las implementaciones de microservicios, podemos pensar en descargar todas esas tareas a una capa diferente, de modo que podamos mantener el código de servicio independiente.

Aquí es cuando aparece el concepto de “Service Mesh” (o malla de servicios). Trae consigo un conjunto de características, necesarias en implementaciones pragmáticas de microservicios, que vienen a solventar esta tipología de patrones de arquitectura directamente sobre la infraestructura, en lugar de sobre el código.

Dicho de otro modo, un Service Mesh no es, en última instancia, una introducción de una nueva funcionalidad al ecosistema de microservicios, sino más bien una reubicación de una funcionalidad ya existente.

Pero… ¿Qué es Service Mesh?

En términos generales, un service mesh puede ser considerado como una infraestructura de software dedicada para manejar la comunicación entre microservicios.

La responsabilidad principal del service mesh es entregar las solicitudes del servicio A al servicio B de una manera confiable, segura y oportuna. Desde el punto de vista funcional, esto es algo similar a la función de un ESB donde se interconectan sistemas heterogéneos para la comunicación de mensajes. La diferencia aquí es que no hay un componente centralizado, sino una red distribuida de componentes.

A modo explicativo, se podría establecer una analogía entre la funcionalidad de un service mesh y la de la pila de red TCP/IP. En ésta última, los bytes (paquetes de red) se entregan de un ordenador a otro a través de la capa física subyacente, consistente en routers, switchs y cables, teniendo la capacidad de absorber fallos y de asegurarse que los mensajes se entregan correctamente.

Aunque hay similitudes entre TCP/IP y un service mesh, este último demanda mucha más funcionalidad dentro de entorno productivo real. A continuación se presenta una lista de funcionalidades que se esperan de una buena implementación de service mesh:

  • Capacidades básicas que aseguren la disponibilidad en la comunicación entre servicios: patrones de Circuit-breaking, reintentos y timeouts, gestión de errores, balanceo de carga y failover…
  • Descubrimiento de servicios: descubrimiento de los endpoints del servicio a través de un registro de servicio dedicado.
  • Capacidades de enrutamiento: enrutamiento de peticiones a versiones diferentes de servicios…
  • Observabilidad: métricas, monitorización, logging y trazabilidad distribuidas.
  • Seguridad: seguridad a nivel de transporte (TLS) y gestión de claves.
  • Autenticación/Autorización.
  • Despliegue: soporte nativo para contenedores (Docker, Kubernetes).
  • Protocolos de comunicación entre servicios: HTTP/1.1, HTTP/2, gRPC…

¿Por qué es necesario?

Cuando disponemos de una aplicación distribuida (ya sea pública o privada), basada en una arquitectura compuesta por microservicios, contenedores y una capa de orquestación (por ejemplo Openshift o Kubernetes)*; y en la que se encuentran cientos de microservicios e instancias de estos que se levantan y eliminan en base a necesidades puntuales de escalado, el camino que sigue una sola solicitud a través de la topología del servicio puede ser increíblemente complejo.

* Este sería el caso más común, pero podrían existir otras topologías de despliegue, no basadas en contenedores y/o capas de orquestación (por ejemplos, distintos servicios corriendo en distintos host, con una configuración de HA más tradicional) en los que también podría utilizarse una solución de service mesh.

Esta combinación de complejidad y criticidad motiva la necesidad de introducir una capa dedicada para la comunicación entre servicios, desacoplada del código de aplicación y capaz de capturar la naturaleza altamente dinámica del entorno subyacente, dotándolo a su vez de herramientas que permitan monitorizar, gestionar y controlar estas comunicaciones.

¿De qué está formado?

Para poder llevar a cabo toda la funcionalidad que debe aportar un service mesh, éste se basa en implementar algunos de los patrones de diseño y de aplicaciones distribuidas ya conocidos, pero en lugar de aplicarlos sobre el código (como venía siendo frecuente), lo realiza sobre la propia infraestructura.

Pero antes de hablar de cada uno de los componentes que conforman un service mesh, haremos una introducción a uno de estos patrones de los que hace uso. Estamos hablando del patrón sidecar.

En el patrón sidecar, la funcionalidad de un proceso (servicio) principal se extiende o mejora mediante un proceso “paralelo” sin apenas acoplamiento entre ambos.

El comportamiento es similar al de un proxy que proporciona al proceso principal todos los servicios “commodity” de infraestructura que necesita (por ejemplo, descubrimiento de servicios, circuit breaker, etc.).

Este patrón es particularmente útil cuando se utiliza Kubernetes como plataforma de orquestación de contenedores. En Kubernetes se utilizan los “pods”, cada uno de los cuales está compuesto por uno o más contenedores de aplicaciones.

Por tanto, un sidecar es un contenedor de “utilidad” en el pod y su propósito es dar soporte al contenedor principal. Es importante tener en cuenta que el sidecar por si sólo no sirve para nada, sino que debe combinarse con uno o más contenedores principales.

En general, el contenedor sidecar es reutilizable y puede combinarse con numerosos tipos de contenedores principales.

Plano de Datos (Data Plane)

Imaginemos un conjunto de servicios independientes en el que cada uno de ellos está desplegado junto con un proxy-sidecar en un mismo pod de Kubernetes.

Hagamos, además, que cada servicio se comunique únicamente con su propio proxy-sidecar y que, en lugar de comunicarse directamente entre ellos, sean éstos últimos (los sidecars) los que terminen siendo los responsables de la entrega fiable de las peticiones a través de una topología o arquitectura que puede ser todo lo compleja que necesitemos.

Podemos ver que cada servicio sólo se comunica sólo con su propio proxy-sidecar.

Llevemos nuestra imaginación un paso más allá y, mentalmente, eliminemos del esquema los propios servicios de negocio, dejando simplemente el conjunto de sidecars y la forma de intercomunicación entre ellos.

Ahora podemos ver con más claridad, cómo el diagrama resultante representa la “malla de conectividad” subyacente:

Si se eliminan los servicios de negocio, la propia topología de conectividad se hace patente.

Bien, pues a este conjunto de servicios y proxy-sidecars (en definitiva, a esta malla) se le asigna el nombre de Data Plane o “Plano de datos”.

A un alto nivel, la responsabilidad del “plano de datos” es asegurar que las solicitudes sean entregadas desde el microservicio A al microservicio B de una manera confiable, segura y oportuna, siendo el encargado de proporcionar las siguientes funcionalidades:

  • Descubrimiento de servicios (Service Discovery).
  • Comprobación de disponibilidad (Health check). Ya sean tanto comprobaciones activas como pasivas.
  • Enrutamiento y balanceo de carga: time-out, reintentos, circuit-breaker, fail-over…
  • Securización y control de acceso (autenticación / autorización). Securización del canal de comunicación con TLS, ACLs…
  • Observabilidad: Métricas, monitorización, logging y trazabilidad distribuidos…

Plano de Control (Control Plane)

Por otro lado, tendríamos la última pieza que forma parte de la solución, la cual recibe el nombre Control Plane o Plano de Control.

Este elemento será el encargado de gestionar y monitorizar todos las instancias de sidecars, siendo el punto ideal para implementar políticas de control, recolección de métricas, monitorización…

Es una pieza obligatoria para el correcto funcionamiento de un service mesh. Pero esta pieza no tiene porque ser un elemento que presente una interfaz de usuario (Gráfica, CLI…), sino que podría ser incluso el propio administrador el encargado de actuar como Plano de Control y realizar la configuración de los sidecar y otros parámetros de configuración de la plataforma (aunque como parece obvio, no es lo deseable).

Podemos ver la conjunción entre plano de datos y plano de control.

Otro punto importante es que un service mesh puede funcionar a nivel de capa de aplicación, no sólo de red.

Esto hace que pueda tener la capacidad de obtener información más detallada sobre las peticiones, como por ejemplo, poder distinguir entre peticiones que finalizan con un error 500 o un 404 (lo cual no es posible a nivel de capa de red).

De este modo se puede hacer uso de esta información para diferentes ámbitos (trazabilidad, health checking…).

A pesar de que las funcionalidades mencionadas anteriormente, se proporcionan dentro del plano de datos mediante los proxy-sidecar. La configuración global de estas funcionalidades se realiza dentro del plano de control. Es el plano de control quien toma todos los proxy-sidecar sin estado y los convierte en un sistema distribuido.

Si en este momento volvemos a la analogía con la pila TCP/IP, podemos decir que el plano de control es similar a la configuración de los switches y routers para que TCP/IP funcione correctamente por encima de éstos.

En un service mesh, el plano de control es por tanto, el responsable de configurar la red de proxy-sidecar. Las funcionalidades del plano de control incluyen la configuración:

  • Enrutamiento.
  • Balanceo de carga.
  • Circuit-breaker, políticas de reintentos, time-outs…
  • Despliegues.
  • Descubrimiento de servicios.

En resumen:

  • Plano de datos: toca cada paquete/petición del sistema. Responsable del descubrimiento de servicios, health check, enrutamiento, balanceo de carga, securización y observabilidad.
  • Plano de control: proporciona la política y la configuración para todos los planos de datos existentes en el service mesh. No toca ningún paquete/petición en el sistema. El plano de control convierte todos los planos de datos en un sistema distribuido.

Patrones de despliegue

Podríamos establecer dos posibles patrones de despliegue para un service mesh:

  • Patrón de despliegue basado en un proxy por host.
  • Patrón de despliegue basado en proxies-sidecar.

Patrón de despliegue basado en un proxy por host

En este tipo de despliegue, un proxy es desplegado por cada host, siendo este una máquina virtual o host físico o un nodo worker de Kubernetes.

En esta tipología, múltiples instancias de servicios de aplicación se ejecutan en un mismo host y todos ellos enrutan el tráfico a través de una misma instancia de proxy.

En el caso de Kubernetes, la instancia de proxy puede ser desplegada como un daemonset.

En este tipo de despliegue, los servicios A, B, C se comunican con los otros a través de su correspondiente instancia de proxy. En cada hosts, corren múltiples instancias de A, B, C.

Patrón de despliegue de proxy sidecar

En este patrón, un proxy sidecar es desplegado por cada instancia de cada servicio. Este modelo es muy útil para despliegues que utilizan contenedores o Kubernetes.

En este último caso, el contenedor del sidecar-proxy puede ser desplegado junto con el contenedor del servicio de aplicación como parte de un pod de Kubernetes.

Obviamente, este tipo de despliegue requiere más instancias del sidecar, por lo tanto, un perfil de recursos más pequeño para sidecar suele ser lo apropiado.

Si el perfil de recursos es un problema, se puede desplegar la instancia de service mesh como un daemoset, lo que reducirá el número de contenedores de service mesh a uno por host en lugar de uno por pod.

En este tipo de despliegue, los servicios A, B, C se comunican entre sí a través de su propia instancia de proxy sidecar.

Enrutamiento dinámico de peticiones

En una implementación de service mesh, el enrutamiento dinámico de peticiones permite encaminar una petición a una versión específica del servicio (v1, v2, v3) en el entorno concreto (dev, stag, prod) a partir de reglas de enrutamiento.

La implementación real de este tipo de reglas de enrutamiento dinámico podría variar entre diferentes frameworks de service mesh.

Aún así, la mecánica básica sigue siendo la misma: un servicio origen realiza una petición a un servicio destino; la versión exacta del servicio destino es determinada por el service mesh en base a las reglas de enrutamiento establecidas.

Este tipo de enrutamiento dinámico también ayuda con la conmutación de tráfico en escenarios de despliegue comunes como los despliegues Azul-Verde, despliegues Canario, pruebas A/B, etc.

En este caso, el service mesh determina la versión de servicio destino de forma dinámica según las reglas de enrutamiento. A continuación, controla el porcentaje de solicitudes enrutadas a dos versiones diferentes del servicio de destino, lo que permite desplazar el tráfico de forma incremental y controlada.

Implementaciones de Services Mesh

Existen diferentes tipos de soluciones de service mesh actualmente, como puede ser Nelson, que utiliza Envoy como su proxy-sidecar y construye un plano de control robusto haciendo uso del stack de Hashicorp (por ejemplo: Nomad, Consul, Vault…).

También Smartstack, el cual crea un plano de control alrededor de HAProxy o NGINX y otros elementos como Nerve, Synapse o Zookeeper, demostrando además que es posible desacoplar el plano de control y el plano de datos.

Por otro lado, están Linkerd e Istio que son dos de las implementaciones open source de service mesh más populares actualmente. Ambas siguen una arquitectura similar, pero diferentes mecanismos de implementación.

No obstante, no hay que perder de vista a Conduit (aún experimental), el cual es un service mesh ultraligero para Kubernetes de los mismos que han implementado Linkerd.

Istio vs Linkerd

Dado que Istio y Linkerd son dos de las más populares implementaciones de service mesh, en la actualidad, vamos a realizar una pequeña comparativa:

¿Cuándo se debe utilizar?

En una solución de arquitectura en la que confluyen microservicios, contenedores y una capa de orquestación (por ejemplo Kubernetes u Openshift) siempre sería muy recomendable incluir las funcionalidades aportadas por un service mesh para gestionar las comunicaciones entre microservicios.

La obligación de este uso se hace más patente aún si los microservicios, tal y como ocurre en este tipo de arquitecturas, son políglotas. Es decir, estarán implementados con diferentes frameworks y/o lenguajes de programación.

También es recomendable valorar la posibilidad de incluir un service mesh cuando, aún teniendo una arquitectura de microservicios, ésta no se encuentra desplegada sobre una plataforma de orquestación (por ejemplo, Kubernetes), sino que en este caso varios microservicios se agrupan para ejecutarse sobre una o varias máquinas (física o virtual), que no son gestionadas de un manera centralizada.

En este caso, se puede optar por un modelo de despliegue de service mesh que siga un patrón de un proxy por host.

No obstante, en soluciones en las que el número de microservicios sea relativamente pequeño, y si todos ellos estuviesen implementados con un mismo framework (por ejemplo Spring Cloud) que proporcione los mecanismos con los que hacer frente a las necesidades de comunicación entre servicios ya mencionadas, podría no tenerse en cuenta el uso de una capa de Service Mesh.

En este caso hay que ser consciente que deberán ser los desarrolladores quienes tengan que asumir la responsabilidad de incluir estas funcionalidades en los propios servicios, ya sea de manera manual o haciendo uso de librerías y/o componentes de terceros.

Por poner un ejemplo concreto, en una arquitectura de microservicios con Spring Cloud, para proporcionar la funcionalidad de descubrimiento de servicios, no sería necesario incluir las dependencias con el cliente de Eureka.

Tampoco lo sería el disponer del propio servidor Eureka, y esto mismo ocurriría con otras de funcionalidades aportadas por Spring Cloud, como Ribbon, Zuul…

Conclusión

Un service mesh aborda algunos de los desafíos clave en lo que respecta a la realización de la arquitectura de microservicio. Pero como en muchos otros casos, el uso de un nuevo tipo de solución plantea una serie de pros y contras, los cuales siempre hay que tener muy en cuenta a la hora de abordarlos:

Pros

  • Las funciones de red básicas se implementan fuera del código de microservicio y éstas son reutilizables. Descubrimiento de servicios, gestión de fallos…
  • Resuelve la mayoría de los problemas de la arquitectura de microservicios en los que se solían tener soluciones ad-hoc: trazabilidad y logging distribuido, seguridad, control de accesos, etc.
  • Proporciona más libertad cuando se trata de seleccionar un lenguaje de implementación de microservicios. No tiene que preocuparse por si un lenguaje determinado tiene o admite librerías para construir funciones de aplicación de red.
  • Aporta mecanismos de enrutamiento inteligente. Pueden ser muy útiles a la hora de incorporar nuevas estrategias de despliegue de servicios (implantación progresiva de una nueva versión, despliegues Blue/Green o Canary), y/o para realización de testing  (por ejemplo test A/B)…

Contras

  • Aumento de la complejidad. Teniendo un service mesh se incrementa drásticamente el número de instancias en ejecución que existen en una implementación de microservicios dada.
  • Incremento de los puntos de fallo potenciales, derivados de la necesidad de tener otros componentes en ejecución. Al igual que ocurre con los ya existentes (microservicios), pueden tener problemas puntuales de indisponibilidad, pero con el agravante de que estos fallos pueden afectar al funcionamiento de toda la malla o a partes de ella. Hay, por tanto, que elegir muy bien la topología de nuestra malla de servios y asegurar la resiliencia de la misma
  • Incremento de latencia por la agregación de saltos adicionales entre servicios (los realizados a través de los proxy sidecar del service mesh).
  • Inmadurez. Las tecnologías de service mesh son relativamente nuevas para declararse como una solución completa.

También hay que dejar claro que este tipo de planteamiento viene a dar solución a problemas comunes de comunicación entre servicios.

Pero no son el mecanismo para solventar otro tipo de problemas como el enrutamiento complejo de peticiones, la transformación o mapeo de tipos, o la integración con otros servicios y sistemas, los cuales deben resolverse en la lógica de negocios de un microservicio (por ejemplo mediante el uso de frameworks de integración como pudieran ser Conductor, Apache Camel o Spring Integration).

Fuentes y Referencias

Ingeniero Técnico Informático con más de una década en el mundo del desarrollo de software y de la consultoría IT. Actualmente trabajo como Arquitecto Técnico dentro del departamento de Arquitectura de Paradigma Digital, colaborando en la definición de soluciones técnicas para proyectos y en el I+D. Apasionado del mundo del desarrollo de software, entre mis áreas de interés se encuentra el desarrollo e investigación de nuevas tecnologías y el como llevarlas a cabo en el ámbito empresarial.

Ver toda la actividad de Julián Toribio

Comentarios

  1. Diego dice:

    Julián, muchas gracias por este artículo. Esta muy claro, y no deja temas sin abordar. Saludos.

Escribe un comentario