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:

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:

¿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.

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:

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:

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).

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:

En resumen:

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

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.

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.

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.

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

Contras

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

Cuéntanos qué te parece.

Enviar.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.