En Internet (service mesh), y aquí hay otra más. ¡Hurra! Pero, ¿por qué? Porque quiero expresar mi opinión de que hubiera sido mejor si las mallas de servicios hubieran existido hace 10 años, antes de la llegada de plataformas de contenedores como Docker y Kubernetes. No afirmo que mi perspectiva sea mejor o peor que otras, pero dado que las mallas de servicios son criaturas bastante complejas, una variedad de puntos de vista puede ayudar a comprenderlas mejor.
Les contaré sobre la plataforma dotCloud, que fue construida sobre más de un centenar de microservicios y soportó miles de aplicaciones en contenedores. Explicaré los problemas que enfrentamos durante su desarrollo y lanzamiento, y cómo las mallas de servicios podrían haber ayudado (o no).
La historia de dotCloud
Ya he escrito sobre la historia de dotCloud y la elección de la arquitectura para esta plataforma, pero he hablado poco sobre el nivel de red. Si no quieren sumergirse en la lectura sobre dotCloud, aquí va un breve resumen: es una plataforma como servicio PaaS que permite a los clientes ejecutar una amplia gama de aplicaciones (Java, PHP, Python...), con soporte para una amplia variedad de servicios de datos (MongoDB, MySQL, Redis…) y un flujo de trabajo similar al de Heroku: subes tu código a la plataforma, esta construye imágenes de contenedores y las despliega.
Voy a contar cómo se dirigió el tráfico en la plataforma dotCloud. No porque fuera especialmente genial (aunque para su época funcionaba bastante bien), sino principalmente porque con las herramientas modernas, un diseño así se puede implementar fácilmente en poco tiempo por un equipo modesto, si necesitan una manera de enrutar el tráfico entre un montón de microservicios o un montón de aplicaciones. Así, se pueden comparar las opciones: ¿qué sucede si desarrollas todo tú mismo o utilizas un servicio de malla existente? La elección estándar: hacerlo uno mismo o comprar.
Enrutamiento de tráfico para aplicaciones alojadas
Las aplicaciones en dotCloud pueden proporcionar puntos finales HTTP y TCP.
Puntos finales HTTP se agregan dinámicamente a la configuración del clúster de balanceadores de carga . Esto es similar a lo que hacen hoy los recursos en Kubernetes y un balanceador de carga como .
Los clientes se conectan a los puntos finales HTTP a través de los dominios correspondientes, siempre que el nombre de dominio apunte a los balanceadores de carga de dotCloud. Nada especial.
Puntos finales TCP están relacionados con el número de puerto, que luego se pasa a todos los contenedores de este stack a través de variables de entorno.
Los clientes pueden conectarse a los puntos finales TCP utilizando el nombre de host correspondiente (algo así como gateway-X.dotcloud.com) y el número de puerto.
Este nombre de host se resuelve en un clúster de servidores “nats“ (sin relación con ), que enrutarán las conexiones TCP entrantes al contenedor correcto (o, en el caso de servicios con balanceo de carga, a los contenedores correctos).
Si estás familiarizado con Kubernetes, probablemente esto te recuerde a los servicios .
En la plataforma dotCloud no había un equivalente a los servicios : para simplificar, el acceso a los servicios se realizaba de la misma manera tanto desde dentro como desde fuera de la plataforma.
Todo estaba organizado de manera bastante simple: las implementaciones iniciales de redes de enrutamiento HTTP y TCP consistían probablemente en solo unas pocas centenas de líneas de Python. Algoritmos simples (diría que ingenuos) que fueron refinados a medida que la plataforma crecía y surgían requisitos adicionales.
No se requería una refactorización extensa del código existente. En particular, pueden utilizar directamente la dirección obtenida a través de las variables de entorno.
¿En qué se diferencia esto de una malla de servicios moderna?
Limitada observabilidad. No teníamos métricas para la red de enrutamiento TCP en absoluto. En cuanto al enrutamiento HTTP, en versiones posteriores se añadieron métricas HTTP detalladas con códigos de error y tiempos de respuesta, pero las mallas de servicios modernas van aún más allá al proporcionar integración con sistemas de recolección de métricas, como Prometheus, por ejemplo.
La observabilidad es importante no solo desde un punto de vista operativo (para ayudar a resolver problemas), sino también al lanzar nuevas funciones. Se trata de un despliegue seguro y .
Eficiencia de enrutamiento también está limitado. En la red de enrutamiento de dotCloud, todo el tráfico debía pasar por un clúster de nodos de enrutamiento dedicados. Esto significaba un potencial cruce de varias fronteras de AZ (zonas de disponibilidad) y un aumento significativo en la latencia. Recuerdo haber solucionado problemas con un código que realizaba más de cien consultas SQL por página y para cada consulta abría una nueva conexión con el servidor SQL. En una ejecución local, la página se carga instantáneamente, pero en dotCloud la carga toma varios segundos porque cada conexión TCP (y la posterior consulta SQL) requiere decenas de milisegundos. En este caso específico, el problema se resolvió con conexiones persistentes.
Las mallas de servicio modernas manejan mejor estos problemas. Primero que nada, verifican que las conexiones se enruten en la fuente. El flujo lógico es el mismo: cliente → malla → servicio, pero ahora la malla funciona localmente, en lugar de en nodos remotos, por lo que la conexión cliente → malla es local y muy rápida (microsegundos en lugar de milisegundos).
Las mallas de servicio modernas también implementan algoritmos de balanceo de carga más inteligentes. Al monitorear la disponibilidad de los backend, pueden enviar más tráfico a los backends más rápidos, lo que resulta en un aumento del rendimiento general.
Seguridad también es mejor. La red de enrutamiento de dotCloud funcionaba completamente en EC2 Classic y no cifraba el tráfico (basándose en la suposición de que si alguien lograba poner un sniffer en el tráfico de red de EC2, ya tienes grandes problemas). Las mallas de servicio modernas protegen de forma transparente todo nuestro tráfico, por ejemplo, con autenticación mutua TLS y cifrado posterior.
Enrutamiento de tráfico para los servicios de plataforma
Bien, hemos discutido el tráfico entre aplicaciones, pero ¿qué pasa con la propia plataforma dotCloud?
La propia plataforma consistía en aproximadamente un centenar de microservicios, responsables de diversas funciones. Algunos aceptaban solicitudes de otros, mientras que algunos eran trabajadores en segundo plano que se conectaban a otros servicios, pero no aceptaban conexiones por sí mismos. De todos modos, cada servicio debe conocer las direcciones de los puntos finales a los que necesita conectarse.
Muchos servicios de alto nivel pueden utilizar la red de enrutamiento descrita anteriormente. De hecho, muchos de los más de cien microservicios de dotCloud se implementaron como aplicaciones convencionales en la propia plataforma dotCloud. Pero un pequeño número de servicios de bajo nivel (en particular, aquellos que implementan esta red de enrutamiento) necesitaban algo más simple, con menos dependencias (ya que no podían depender de sí mismos para funcionar, el viejo problema del huevo y la gallina).
Estos servicios de bajo nivel y críticos se implementaron ejecutando contenedores directamente en varios nodos clave. No se utilizaron los servicios estándar de la plataforma: ensamblador, planificador y runner. Si deseas compararlo con las plataformas de contenedores modernas, es similar a ejecutar el plano de control en docker run directamente en los nodos, en lugar de delegar la tarea a Kubernetes. Es bastante parecido al concepto de , que utiliza o al inicializar un clúster autónomo.
Estos servicios se exponían de una manera simple y cruda: se enumeraban sus nombres y direcciones en un archivo YAML; y cada cliente debía tomar una copia de este archivo YAML para el despliegue.
Por un lado, esto es extremadamente confiable, ya que no requiere el soporte de un almacenamiento externo de clave/valor, como Zookeeper (no olvides que en ese tiempo, etcd o Consul aún no existían). Por otro lado, esto complicaba el desplazamiento de los servicios. Cada vez que había un movimiento, todos los clientes debían recibir un archivo YAML actualizado (y potencialmente reiniciarse). ¡No era muy conveniente!
Posteriormente, comenzamos a implementar un nuevo esquema donde cada cliente se conectaba a un proxy local. En lugar de la dirección y el puerto, solo necesitaba conocer el número de puerto del servicio y conectarse a través de localhost. El proxy local maneja esta conexión y la dirige al servidor real. Ahora, al mover el backend a otra máquina o escalar, en lugar de actualizar a todos los clientes, solo se necesita actualizar todos estos proxys locales; y ya no es necesario reiniciar.
(También se planeaba encapsular el tráfico en conexiones TLS y colocar otro servidor proxy del lado receptor, así como verificar los certificados TLS sin la participación del servicio receptor, que está configurado para aceptar conexiones solo en localhost).
Esto es muy similar a de Airbnb, pero la diferencia fundamental es que SmartStack se ha implementado y desplegado en producción, mientras que el sistema de enrutamiento interno de dotCloud se archivó cuando dotCloud se transformó en Docker.
Personalmente, considero que SmartStack es uno de los precursores de sistemas como Istio, Linkerd y Consul Connect, porque todos siguen un mismo patrón:
- Ejecutar un proxy en cada nodo.
- Los clientes se conectan al proxy.
- La capa de control actualiza la configuración del servidor proxy cuando cambian los backend.
- … ¡Ganancia!
Implementación moderna de un service mesh
Si tuviéramos que implementar una malla similar hoy, podríamos utilizar principios análogos. Por ejemplo, configurar una zona DNS interna, asociando nombres de servicios con direcciones en el espacio 127.0.0.0/8. Luego, ejecutar HAProxy en cada nodo del clúster, aceptando conexiones en cada dirección de servicio (en esta subred 127.0.0.0/8) y redirigiendo/balanceando la carga a los backend correspondientes. La configuración de HAProxy puede ser gestionada , permitiendo almacenar información sobre el backend en etcd o Consul y actualizar automáticamente la configuración en HAProxy cuando sea necesario.
¡Así es como funciona aproximadamente Istio! Pero con algunas diferencias:
- Utiliza en lugar de HAProxy.
- Mantiene la configuración del backend a través de la API de Kubernetes en lugar de etcd o Consul.
- A los servicios se les asignan direcciones en la subred interna (direcciones Kubernetes ClusterIP) en lugar de 127.0.0.0/8.
- Tiene un componente adicional (Citadel) para agregar autenticación mutua TLS entre el cliente y los servidores.
- Soporta nuevas funciones, como circuit breaking, trazado distribuido, despliegue canario, entre otros.
Veamos brevemente algunas diferencias.
Envoy Proxy
Envoy Proxy fue escrito por la empresa Lyft [competidor de Uber en el mercado de taxis - nota del traductor]. Es muy similar a otros proxies (como HAProxy, Nginx, Traefik...), pero Lyft escribió el suyo porque necesitaban funciones que faltaban en otros proxies, y les pareció más sensato crear uno nuevo que ampliar los existentes.
Envoy se puede utilizar por sí solo. Si tengo un servicio específico que debe conectarse a otros servicios, puedo configurarlo para que se conecte a Envoy, y luego configurar y reajustar dinámicamente Envoy con la ubicación de otros servicios, obteniendo muchas características adicionales excelentes, como la observabilidad. En lugar de una biblioteca de cliente personalizada o la integración de la trazabilidad de llamadas en el código, dirigimos el tráfico a Envoy, que recopila métricas para nosotros.
Pero Envoy también puede funcionar como plano de datos (data plane) para un service mesh. Esto significa que ahora para este service mesh, Envoy se configura plano de control (control plane).
Plano de control
En el plano de control, Istio se basa en la API de Kubernetes. Esto no es muy diferente a usar confd, que se basa en etcd o Consul para mirar un conjunto de claves en el almacén de datos. Istio consulta un conjunto de recursos de Kubernetes a través de la API de Kubernetes.
Mientras tanto: personalmente, me pareció útil esta , que dice:
El servidor API de Kubernetes es un "servidor tonto" que proporciona almacenamiento, gestión de versiones, validación, actualizaciones y semántica de recursos API.
Istio está diseñado para trabajar con Kubernetes; y si deseas usarlo fuera de Kubernetes, necesitas ejecutar una instancia del servidor API de Kubernetes (y un servicio auxiliar de etcd).
Direcciones de servicios
Istio se basa en las direcciones ClusterIP que proporciona Kubernetes, por lo que los servicios de Istio reciben una dirección interna (no en el rango 127.0.0.0/8).
El tráfico a la dirección ClusterIP para un servicio específico en el clúster de Kubernetes sin Istio es interceptado por kube-proxy y enviado al backend de este proxy. Si estás interesado en los detalles técnicos, kube-proxy establece reglas de iptables (o balanceadores de carga IPVS, dependiendo de cómo se configuró), para reescribir las direcciones IP de destino de las conexiones que van hacia la dirección ClusterIP.
Después de instalar Istio en el clúster de Kubernetes, nada cambia hasta que se habilita explícitamente para un consumidor específico o incluso para todo el espacio de nombres, mediante la introducción de un contenedor sidecar en pods personalizados. Este contenedor ejecutará una instancia de Envoy y establecerá un conjunto de reglas de iptables para interceptar el tráfico que va a otros servicios y redirigir ese tráfico a Envoy.
En la integración con DNS de Kubernetes, esto significa que nuestro código puede conectarse por el nombre del servicio y todo "simplemente funciona". En otras palabras, nuestro código realiza solicitudes del tipo http://api/v1/users/4242, entonces api resuelve la solicitud en 10.97.105.48, las reglas de iptables interceptan las conexiones de 10.97.105.48 y las redirigen al proxy local Envoy, y este proxy local enviará la solicitud al backend API real. ¡Uf!
Detalles adicionales
Istio también proporciona cifrado y autenticación de extremo a extremo a través de mTLS (Transport Layer Security mutuo). Esto es gestionado por un componente llamado Citadel.
También hay un componente Mixer, que Envoy puede solicitar para cada la solicitud, para tomar una decisión especial sobre esta solicitud en función de diversos factores, como encabezados, carga del backend, etc... (no se preocupen: hay muchas formas de asegurar el funcionamiento del Mixer, y incluso si falla, Envoy seguirá funcionando correctamente como proxy).
Y, por supuesto, mencionamos la observabilidad: Envoy recopila una enorme cantidad de métricas, asegurando al mismo tiempo el seguimiento distribuido. En la arquitectura de microservicios, si una solicitud API debe pasar a través de los microservicios A, B, C y D, entonces, al ingresar al sistema, el seguimiento distribuido añadirá un identificador único a la solicitud y mantendrá ese identificador a través de las subsolicitudes a todos estos microservicios, permitiendo registrar todas las llamadas relacionadas, sus retrasos, etc.
Desarrollar o comprar
Istio tiene la reputación de ser un sistema complicado. Por otro lado, construir la malla de enrutamiento que describí al principio de este post es relativamente sencillo con las herramientas existentes. Entonces, ¿tiene sentido crear nuestra propia malla de servicios en su lugar?
Si tenemos necesidades modestas (no se necesita observabilidad, cortador de cadenas y otros detalles), surgen pensamientos sobre desarrollar nuestra propia herramienta. Pero si estamos utilizando Kubernetes, podría no ser necesario, porque Kubernetes ya proporciona herramientas básicas para descubrimiento de servicios y balanceo de carga.
Pero si tenemos requisitos avanzados, entonces "comprar" una malla de servicios parece ser una opción mucho mejor. (No siempre es exactamente "comprar", ya que Istio es de código abierto, pero aún necesitamos invertir tiempo de ingeniería para entender cómo funciona, implementarlo y gestionarlo).
¿Qué elegir: Istio, Linkerd o Consul Connect?
Hasta ahora hemos hablado solo de Istio, pero no es el único service mesh. Una alternativa popular es , y también hay .
¿Qué elegir?
Honestamente, no lo sé. Actualmente no me considero lo suficientemente competente para responder a esta pregunta. Hay varios comparaciones de estas herramientas e incluso hay .
Uno de los enfoques más prometedores es usar una herramienta como . Implementa una capa de abstracción para simplificar y unificar las API proporcionadas por los service meshes. En lugar de estudiar las API específicas (y, en mi opinión, relativamente complejas) de varios service meshes, podemos utilizar construcciones más simples de SuperGloo — y cambiar fácilmente de uno a otro, como si tuviéramos un formato intermedio de configuración que describe interfaces HTTP y backends, capaz de generar la configuración real para Nginx, HAProxy, Traefik, Apache…
He experimentado un poco con Istio y SuperGloo, y en el próximo artículo quiero mostrar cómo agregar Istio o Linkerd a un clúster existente usando SuperGloo, y qué tan bien se desempeña este último, es decir, si permite cambiar de un service mesh a otro sin reescribir configuraciones.
Fuente: habr.com
