Probando Linkerd, el pionero de los services mesh

Siguiendo con la serie de posts dedicados a Service Mesh que hemos publicado recientemente en el blog, hoy nos centramos en uno de los productos que lleva más tiempo en el mercado: Linkerd.

¿Cómo puede ayudarnos en nuestro trabajo? ¿Qué virtudes ofrece? Además de sus características nos adentraremos en su despliegue en Kubernetes junto a una arquitectura de microservicios. ¡Empezamos!

¿Qué es Linkerd?

Buoyant es la empresa que está detrás de Linkerd. Lleva ya un largo tiempo trabajando en productos cuya finalidad es ayudar a crear sistemas robustos y resilientes.

Linkerd es un proxy de red open source diseñado para ser desplegado como Service Mesh y que está basado en finagle y netty. Su principal cometido es hacer de link, como su nombre indica, entre las diferentes piezas de sistemas distribuidos y es un buen compañero para nuestras arquitecturas de microservicios, muy en boga en estos días.

Está desarrollado en Scala y se ejecuta sobre la máquina virtual de Java. Desde febrero de 2016 existen versiones estables, por lo que puede considerarse un pionero en este área.

Quizá sea atrevido decir que Linkerd fue el primer service mesh, ya que el concepto existía antes aunque en forma de librerías y sin catalogarse con ese nombre. Lo que sí podemos asegurar es que es el “service mesh como proxy” más sólido y usado en producción, gestionando el tráfico de red en arquitecturas distribuidas de grandes compañías.

¿Qué nos ofrece?

Linkerd controla y monitoriza la comunicación entre los distintos componentes de una aplicación distribuida. Esto hace que sea más fácil para los desarrolladores crear aplicaciones políglotas y desacoplar la lógica de negocio y las tareas de “instrumentación” típicas de las arquitecturas distribuidas.

No vamos a entrar a describir todas las características que ofrece un service mesh, listamos aquí algunas de las más interesantes con las que cuenta el producto:

  • Balanceo de carga avanzado, en función de diferentes parámetros.
  • Gestión del circuit breaking y reintentos para llamadas fallidas.
  • Agrupación de conexiones.
  • Seguridad a nivel de red (TLS).
  • Enrutamiento dinámico.
  • Trazabilidad distribuida.
  • Monitorización y métricas.
  • Autodescubrimiento.

Tradicionalmente, todas estas características han estado en cada uno de los componentes de una arquitectura distribuida, diluidas y formando parte del código base de la aplicación.

Algunos ejemplos podemos verlos en servicios creados con el “archiconocido” ecosistema Netflix. En las aplicaciones generadas con este framework, el código base contiene tanto la lógica de negocio como la gestión de red (por ejemplo Hystrix y Ribbon).

El paradigma cambia con los service mesh, el código base se centrará en la lógica de negocio y los proxies gestionan el tráfico de red.

A diferencia de otros Services Mesh, Linkerd solo trabaja a nivel de “data plane” (plano de datos), no tiene “control plane” (o plano de control) completo como tienen otros service mesh*. Esto quiere decir que la malla de proxies no está gestionada o manejada por un punto común.

*NOTA: en la documentación oficial se apunta a que el servicio namerd, instalado de manera aislada, puede actuar como plano de control de Linkerd, pero en nuestra opinión se queda corto al solo cubrir funciones de enrutado y service discovery.

Otra de sus características más importantes es que Linkerd soporta diversas plataformas, puede ser instalado sobre Kubernetes, mesos y on-premise entre otros, a diferencia de sus competidores, que se han centrado en soluciones cloud específicas.

Los protocolos soportados por el proxy son gRPC, HTTP/1 y HTTP/2, siendo capaz de gestionar el cifrado de conexión mediante TLS.

Otro punto fuerte es su gran cantidad de integraciones con productos de terceros, que facilitan la vida a la hora de integrar e instalar el producto. Destacan su soporte con StatsD y Prometheus para métricas/monitorización, Zipkin como sistema de trazabilidad distribuida, o Zookeeper y Consul para descubrimiento de servicios.

¿Cómo se despliega?

Linkerd se puede desplegar en modo “proxy por host” o en modo sidecar:

  • En la instalación por host, una instancia de Linkerd proxy se despliega por cada nodo/host de nuestra arquitectura. Esto quiere decir que hay una relación n..1 entre las instancias de los servicios y el proxy. Este despliegue es menos exigente a nivel de recursos ya que solo tendremos una instancia de proxy por nodo.
  • En la instalación en modo sidecar tenemos un proxy por cada instancia de servicio. Relación 1..1 entre instancias y proxy. Esta solución es mucho más versátil pero es más “agresiva” con los recursos de la máquina.

Hay que destacar que, pese a que Buoyant ha hecho un gran trabajo optimización y parametrización de la máquina virtual de Java, la memoria usada por el proxy en tiempo de ejecución (memory footprint) es mayor que otras soluciones que usan lenguajes compilados como Go, Rust o C++.

Esto hace que la instalación en modo sidecar sea inviable en algunas situaciones y que el modo “proxy por host” esté más extendido.

Como detalle, Linkerd puede ser usado como plano de datos de Istio, otro de los service mesh del mercado que están pegando fuerte.

¿Cómo funciona?

Sea cual sea la topología de despliegue, Linkerd levantará varios puertos para gestionar el tráfico de red de los diferentes protocolos soportados por nuestra arquitectura. Se configurarán puertos distintos en función de si el tráfico es de entrada o salida.

Una arquitectura típica, en la que Linkerd hace de proxy (HTTP/1) entre varios componentes, publicará los siguientes puertos:

  • Puerto http de entrada (por ejemplo el 4141).
  • Puerto http de salida (por ejemplo el 4140).
  • Puerto para la administración y monitorización del proxy (normalmente el 9990).

El proxy recibirá los mensajes de entrada en el puerto 4141. Antes de enviar la petición a  los servicio se realizarán tareas de entrada (recolección de métricas, creación de cabeceras, etc).

El siguiente paso será trasladar la petición al servicio. El servicio ejecutará su lógica y si tiene que hacer una comunicación por http con otro componente de la arquitectura lo realiza a través del proxy, por el puerto de salida (4140), aquí el proxy realizará trabajos de salida (descubrimiento de servicio, balanceo, etc.).

¿Cómo se integra?

Antes de configurar Linkerd, debemos preparar nuestras aplicaciones. Si las comunicaciones entre los componentes de nuestra aplicación son a través del protocolo HTTP, debemos asegurarnos de que nuestros clientes http pueden usar Linkerd como proxy.

La mayoría de lenguajes procesan la variable de entorno HTTP_PROXY del sistema, aunque los lenguajes de la JVM usan variables de entorno específicas para este cometido.

Otra manera de integrar nuestras aplicaciones sería hacer que Linkerd trabaje como proxy transparente, esto lo conseguiremos haciendo uso reglas iptables que harán que la aplicación se despreocupe de la gestión de variables de entorno de proxy http. Aunque hay que matizar que esta solución no es viable para aplicaciones que usen múltiples protocolos de comunicación.

¿Cómo se configura?

Linkerd se configura con un fichero yaml/json que reside en el filesystem. Veamos un ejemplo entrando en cada sección:

admin:
  port: 9990
  ip: 0.0.0.0

namers:
- kind: io.l5d.fs
  rootDir: disco

routers:
- protocol: http
  dtab: /svc => /#/io.l5d.fs/nginx;
  servers:
  - port: 4140
    ip: 0.0.0.0

telemetry:
- kind: io.l5d.prometheus
  • Admin: establece dónde se publica la interfaz de administración.
  • Namers: define el descubrimiento de servicios. En el ejemplo, un descubrimiento básico basado en filesystem.
  • Routers: define el enrutamiento a los servicios, definiendo servers, clients y services. En el ejemplo, asocia al nombre de servicio “nginx” la IP y el puerto de destino donde reside su implementación.
  • Telemetry: configuración de métricas, monitorización, etc. En el ejemplo se activa la publicación de un endpoint de métricas adaptadas al software prometheus.

¿Cómo se integra con arquitecturas de microservicios (MSA) en Kubernetes?

Una vez sabemos lo que nos aporta y cómo podemos hacer uso de Linkerd, vamos a testearlo en una arquitectura de microservicios simple dentro de un proyecto Kubernetes.

Dentro de un cluster GKE (Google Kubernetes Engine) vamos a desplegar una MSA simple en la que un servicio “books” expone un interfaz y tiene una dependencia con otro servicio “stars” para completar la respuesta.

Todas las comunicaciones son HTTP/1 y Linkerd deberá interceptarlas. Probaremos los dos modos de despliegue: modo “proxy por host” y modo sidecar. En lo que respecta a la trazabilidad entre los servicios, usaremos zipkin para almacenar los datos.

Preparando la aplicación

Como hemos comentado antes, la integración de Linkerd en nuestros desarrollos no es del todo gratis.

Tenemos que preparar nuestro software para que haga uso de Linkerd como proxy en las llamadas http entre servicios. En nuestro caso el servicio “books” realizará una conexión con el servicio “stars”, por lo que debe lanzarse con los siguientes flags de la JVM:

… -Dhttp.proxyHost=localhost -Dhttp.proxyPort=4140

Por otro lado, y para qué Linkerd pueda gestionar la trazabilidad distribuida, tendremos que ayudar haciendo que nuestros microservicios propaguen las cabeceras específicas de Linkerd, que son las que tienen el prefijo “l5d-ctx-*”. Esto hace que la integración no sea todo lo transparente que nos gustaría.

Para no complicar nuestro ejemplo, realizaremos la propagación de cabeceras en el propio  controlador, aunque lo ideal sería crear un componente que se pudiera reutilizar en todos nuestros servicios:

 @RequestMapping(method = RequestMethod.GET)
   public List<Book> books(@RequestHeader HttpHeaders httpHeaders) {
...
       HttpHeaders forwardedHeaders = new HttpHeaders();
       Map<String,String> headerMap = httpHeaders.toSingleValueMap();
       headerMap.keySet().forEach(key -> {
           String value = headerMap.get(key);
           if (key.startsWith("l5d-ctx-")) {
               forwardedHeaders.put(key, Collections.singletonList(value));
           }
       });
…
      HttpEntity entity = new HttpEntity(forwardedHeaders);

      ResponseEntity<Star> stars = restTemplate.exchange(
               url, HttpMethod.GET, entity, Star.class, 1);

...

Modo “proxy por host”

Una vez preparado el software, comenzaremos con la instalación en modo “proxy por host” en Kubernetes. Buoyant recomienda el uso del objeto daemonset, que asegura una única instancia de pod por cada nodo en el cluster. Cada vez que se añada un nodo al cluster se desplegará este pod, pero el pod no será escalable a nivel de nodo.

Esta es una representación de la arquitectura (simplificada/un solo nodo en el cluster) y el flujo de llamadas entre nuestros servicios.

¡Manos a la obra! Con el siguiente comando, y habiendo hecho login en Google Cloud, crearemos un cluster Kubernetes básico en GKE:

gcloud container clusters create Linkerd-cluster --num-nodes 1 --machine-type n1-standard-2

Para instalar los permisos y componentes necesarios, ejecutamos estos comandos en orden:

kubectl config set-context $(kubectl config current-context) --namespace=default
kubectl create clusterrolebinding cluster-admin-binding --clusterrole cluster-admin --user $(gcloud config get-value account)

kubectl apply -f k8s/linkerd-rbac.yml
kubectl apply -f k8s/proxy-per-node/linkerd.yml
kubectl apply -f k8s/proxy-per-node/msa-linkerd.yml
kubectl apply -f k8s/zipkin.yml

Los cuatro ficheros descriptores contienen lo siguiente:

  • linkerd-rbac.yml: permisos necesarios para que Linkerd pueda descubrir servicios y hacer load balancing haciendo uso del API de Kubernetes.
  • linkerd.yml: contiene un daemonset de Kubernetes para hacer la instalación de Linkerd en modo “proxy por host”.
  • msa-linkerd.yml: contiene los microservicios “books” y “stars”.
  • zipkin.yml: contiene el software Zipkin. Lo usaremos para monitorización de trazas distribuidas. Se publicarán los servicios necesarios para recolectar y visualizar las trazas en la consola web.

Una vez desplegada la arquitectura en la consola de GKE podremos ver los artefactos generados. Se puede apreciar como Linkerd y Zipkin son los únicos servicios accesibles desde fuera del cluster:

Podemos obtener la IP pública en la que Linkerd está escuchando de dos formas:

  1. Usando  el interfaz gráfica de la consola de Google Cloud (imagen).
  2. Ejecutando el siguiente comando:
L5D_IP=$(kubectl get svc l5d -o jsonpath="{.status.loadBalancer.ingress[0].*}")

Linkerd cuenta con una consola de administración que permite configurar los niveles de log, ver peticiones recientes, o hacer uso del playground de descubrimiento de servicios. En entornos productivos, esta consola solo debería ser accesible a usuarios administradores, para ello podríamos hacer uso de port-forwarding o alguna técnica similar.

Esta consola se encuentra en el puerto 9990, para verificar que Linkerd está “up and running” abriremos en el navegador la IP obtenida junto a este puerto:

Una vez verificada la instalación de Linkerd, pasaremos a lanzar una petición a nuestro servicio “books” con el siguiente comando:

http_proxy=$L5D_IP:4141 curl -i http://books/books

Si analizamos el comando, estamos usando como proxy http la IP pública que expone Linkerd y su puerto de entrada (4141). Este es el mecanismo que permite a Linkerd redirigir la petición al objetivo.
La respuesta debería ser como la siguiente:

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Date: Thu, 03 May 2018 09:41:37 GMT
l5d-success-class: 1.0
Via: 1.1 Linkerd, 1.1 Linkerd
Transfer-Encoding: chunked

[{"id":1,"title":"Enders game","year":"1985","author":"orson scott card","stars":5}]

Entre las cabeceras de respuesta http podemos encontrar cabeceras específicas de Linkerd, por lo que podemos asegurar que la petición ha pasado por nuestro proxy.

Ahora, para conocer el flujo de llamadas y comprobar que Linkerd ha hecho su trabajo también en la comunicación entre los microservicios, abriremos la consola de visualización de Zipkin y accederemos a los detalles de la traza de la petición que acabamos de realizar.

Esta imagen muestra el tráfico de llamadas dentro de Kubernetes. Los nombres de los servicios no son muy claros, pero se puede apreciar cómo el proxy intercepta las llamadas entrantes y salientes.

Hay que destacar que ninguno de los microservicios contacta con Zipkin, la trazabilidad de la aplicación la gestiona el proxy.

Modo sidecar

En el modo sidecar desplegamos un único fichero (“sidecar-msa-linkerd.yml”) que contiene los servicios, la configuración de Linkerd y dos ReplicationController que contendrán un microservicio y su respectivo proxy. En la imagen se puede ver la arquitectura descrita y el flujo de comunicaciones.

El proxy residente en el pod “books” publica el puerto 4141 en el cluster para que sea capaz de recibir las peticiones y transmitirlas al microservicio “books”, como se puede apreciar en la consola de Google Cloud una vez instalado el sistema:

Para probar la instalación en modo sidecar, repetimos el proceso descrito en el anterior apartado, esperando la misma respuesta. En este caso debemos obtener la IP pública expuesta por el proxy Linkerd correspondiente al servicio “books”:

BOOKS_IP=$(kubectl get svc books -o jsonpath="{.status.loadBalancer.ingress[0].*}")
http_proxy=$BOOKS_IP:4141 curl -i http://books/books

Con esto, hemos verificado el objetivo de la POC y tenemos dos ejemplos de arquitectura de microservicios en Kubernetes usando Linkerd cómo service mesh, en sus dos modos de instalación.

El modo sidecar tiene la contra del mayor consumo de recursos ya que, cuando se escale un pod, se escalará tanto el microservicio como el proxy. Como ventajas, mejoramos en seguridad, en latencias y en fiabilidad al no tener un punto único de entrada a todos nuestros servicios.

El código fuente de los microservicios y los ficheros de despliegue se pueden consultar en nuestro repositorio GitHub.

Conclusiones

Después de analizar el producto y probar su instalación en Kubernetes estos son, en nuestra opinión, los puntos a favor, en contra y conclusiones:

Pros:

  • Linkerd es un producto robusto y usado ampliamente en entornos productivos.
  • La principal ventaja con sus competidores es el soporte a múltiples plataformas (ECS, on-premise, IaaS, DC-OS.).
  • Tiene soporte comercial, facilitado por la propia Buoyant.
  • Con la prueba de concepto hemos experimentado que la integración del proxy en nuestros microservicios es sencilla.
  • Linkerd ofrece mucha funcionalidad sin tener que sobrecargar nuestro código base con librerías de terceros.

Contras:

  • Su integración con Kubernetes no nos ha convencido, la instalación no es trivial, el modo “proxy por host” podría convertir a Linkerd en un punto crítico dentro de la arquitectura y aumenta las latencias.
  • La instalación en modo sidecar, aunque nos parece mejor opción, incrementa el gasto de memoria sustancialmente al escalar el sistema.
  • Al ejecutarse dentro de la JVM, Linkerd tiene un uso de memoria mayor que otros proxy como Envoy o traefik.
  • No tiene un plano de control avanzado, lo que hace que sea difícil de usar/configurar en grandes sistemas.

Linkerd es un producto interesante para aquellos que no hayan dado el paso a Kubernetes y que estén buscando un service mesh/proxy fiable y con soporte. Para aquellos que ya usen Kubernetes, existen productos que se integran mejor con el orquestador de contenedores diseñado por Google.

En el futuro, estaremos atentos a Conduit, el nuevo service mesh de Buoyant, construido para Kubernetes, que promete tener la robustez y características de Linkerd pero con una configuración simplificada y uso de memoria mucho menor.

Escribe un comentario