Optimizando la comunicación entre microservicios: Thrift, gRPC y HTTP/2

Publicado por Jordi Hernández Sellés el

MicroserviciosThriftHTTP2gRPC

En este post vamos a hablar de uno de los aspectos más relevantes en el mundo de los servicios y la integración: los protocolos de comunicación entre componentes software. Es este un tópico que dista mucho de ser nuevo: está entre nosotros desde el primer día en que alguien decidió que dos máquinas deberían ser capaz de comunicarse entre ellas.

Una vez decidimos que queremos invocar métodos de forma remota en una máquina, inevitablemente lo primero que hay que resolver es la definición de un protocolo de comunicaciones que sea manejable tanto desde el punto de invocación como desde el punto de ejecución.

En la historia (casi) reciente, los protocolos de integración se han llamado CORBA, EJB, SOAP... y muchos otros. Algunos muy populares y abiertos, otros de nicho y propietarios. En realidad, nunca hablamos de un solo protocolo concreto, sino de stacks que incluyen desde la base de comunicación de red (TCP, UDP...) hasta el formato de los mensajes que se transfieren (binario, cifrado, contenido textual...)

En el presente, el stack más popular puede definirse a muy grandes rasgos como JSON sobre REST sobre HTTPS (versión 1.1). No es objeto de este artículo entrar a definir este stack en detalle. Básicamente tenemos un formato de texto legible para humanos, algo más frugal que el XML, una convención de uso racional de los métodos que define el protocolo HTTP, y este último protocolo en su versión cifrada.

Sin entrar a valorar las indudables ventajas de este stack, una cosa está clara: no está enfocado a requisitos de rendimiento críticos. Si bien nos permite la ejecución paralela de múltiples peticiones, nos encontramos con varios puntos de contención:

  • El protocolo HTTP solo admite una petición por conexión. Esto implica que la negociación entre las máquinas implicadas se debe realizar una vez por invocación remota.
  • El formato preferido, JSON, se codifica como texto, habitualmente como UTF-8. El tamaño del mensaje sería hasta diez veces menor en un formato binario, ya que como sabemos el texto plano es muy comprimible.

Estos inconvenientes no suponen un gran problema en aplicaciones monolíticas, ya que generalmente el receptor del mensaje no necesita a su vez realizar peticiones a servicios remotos, más allá de la habitual base de datos. Sin embargo, en esta era de microservicios en plataformas de virtualización, nos encontramos con que una sola petición a un servicio puede ramificarse y extenderse a cierta cantidad de servicios auxiliares. De ahí que empiece a notarse una acumulación de latencia que puede resultar lesiva a la hora de cumplir con un determinado KPI, especialmente si se trata de componentes en los que la velocidad en la respuesta es crítica.

Por tanto, ya hace años que se han ido desarrollando metodologías, componentes y protocolos que se orientan a minimizar la latencia inherente a un stack de microservicios distribuidos. El objeto de este artículo es presentar el estado del arte en la optimización de los protocolos de comunicación en este ámbito.

Las alternativas

Cuando busquemos opciones para optimizar la comunicación entre nuestros microservicios, nos encontraremos con que existen al menos tres opciones que comparten las deseables características de ser abiertas, tener una buena base de usuarios e incluso en algún caso basarse en estándares. Vamos a analizar las alternativas, que son las siguientes:

  • Apache Thrift.
  • gRPC.
  • JSON sobre HTTP/2.

Spoiler masivo: Thrift seguramente no nos interese salvo en el caso de aplicaciones legacy que ya lo estén usando; gRPC nos servirá cuando el rendimiento sea crítico, y JSON sobre HTTP/2 será nuestra opción menú del día para proyectos que, siendo más o menos complejos, no nos exijan exprimir hasta el último microsegundo de latencia.

¿Qué factores vamos a analizar? A fin de cuentas, no es oro todo lo que reluce y la eficiencia pura y dura que obtengamos al final no compensa si para llegar al objetivo tenemos que programar un protocolo en ensamblador, escribir nuestro propio módulo para el servidor HTTP y pasarnos el Doom 2 con una escopeta en menos de 3 minutos.

Jugando a Doom 2

esto va a ser difícil...

Por tanto, además de la eficiencia pura y dura, nos interesa evaluar los siguientes aspectos del stack antes de adoptarlo:

  • Lenguajes soportados: ¿Sabremos programar en alguno de los lenguajes que son compatibles, o tendremos que contratar expertos solo para poder usarlo?
  • Versionado/compatibilidad con evolutivos: ¿Cómo de facil será evolucionar el producto resultante, una vez surjan nuevos requisitos?
  • Costes de adaptación/formación/implementación: ¿Cuánto nos cuesta formarnos en el uso del nuevo stack? ¿La curva de aprendizaje es suave o elevada?
  • Costes de migración: ¿Cuál es el coste de adaptar una aplicación ya existente para optimizarla con esta nueva tecnología?
  • Herramientas/Documentación: ¿Cuál es el tooling que me va a hacer la vida más fácil a la hora de implementar la solución? ¿Tengo una documentación completa y precisa que me ayude a aprender más fácilmente? ¿Stack Overflow será mi amigo cuando tenga un problema?
  • Integración con frameworks existentes: ¿Dispongo de APIs de integración que funcionen con la tecnología que quiero adoptar dentro de mi framework, ya sea Spring o cualquier otro?

Vamos a ello.

Apache Thrift

Thrift es un proyecto que nació dentro de Facebook en la primera década del siglo. En 2007 Facebook decidió convertirlo en código abierto bajo la fundación Apache. Desde entonces se ha desarrollado de acuerdo con el modus operandi habitual en Apache: Open Source descentralizado, gestión basada en la meritocracia. Como pasa en tantas ocasiones, el desarrollo del código tiene prevalencia sobre la documentación del mismo. En consecuencia es usual ver a usuarios del producto quejandose amargamente de la baja calidad de su documentación (¡hola, Maven!).

Thrift presenta similitudes de concepto con gRPC, en el sentido de que es un stack completo de Remote Procedure Call, con lo cual encontraremos en ambos los ingredientes habituales del RPC: lenguaje IDL (interface definition language) para definir interfaces de servicio y stubs de comunicación.

Eficiencia

Ofrece varios formatos para los mensajes, incluyendo los de tipo binario y comprimido.
También tiene varios formatos de transporte (socket, por frames, ...). Son ad hoc, esto es, no corresponden a un protocolo de alto nivel como HTTP. Es dudoso que soporten transporte vía proxy. Los benchmark publicados le dan algo de ventaja sobre gRPC en cuanto a eficiencia.

No ofrece ninguna solución para cachear respuestas idempotentes.
En cuanto a monitorización de carga, se puede usar Elastic Packetbeat, una herramienta de análisis de paquetes de red.
En lo referente al balanceo de carga, no está documentado per se. Requiere que usemos herramientas externas para implementarlo.

Lenguajes soportados

Este es un punto fuerte de Thrift: la lista de lenguajes que soporta incluye desde lo más habitual a lo decididamente exótico:
C, Common Lisp, C++, C#, D, Delphi, Erlang, Go, Haxe, Haskell, Java, JavaScript, node.js, OCaml, Perl, PHP, Python, Ruby y SmallTalk.

Jugando a Doom 2

¿Y habla con colectores de humedad? ¡Ja!

En iOS el desarrollo parece que no es excesivamente complicado pero mi desconocimiento personal no me permite juzgar si merece la pena intentarlo. Se puede consultar la documentación aquí: https://wiki.apache.org/thrift/ThriftUsageObjectiveC

En Android, Microsoft lo usa en Outlook y han creado un generador de código que reduce el peso de los clientes https://github.com/Microsoft/thrifty.

Documentación

https://thrift.apache.org/tutorial/
La documentación es comparativamente escasa y está bastante desorganizada. Partir desde cero seguramente resultará una tarea ardua e ingrata. Por suerte, al llevar mucho tiempo liberado (desde 2007), hay bastantes artículos de consulta al respecto en blogs técnicos.

Versionado/compatibilidad con evolutivos

Parece que la compatibilidad se rompe en las mismas circunstancias que en cualquier servicio RPC (número y tipo de parámetros, métodos, tipos de retorno con menos datos). Esto le sonará a cualquiera que haya usado EJBs o SOAP.

Costes de adaptación/formación

Para empezar a usar Thrift tendremos que aprender el lenguaje de definición de servicios o IDL. Como con cualquier framework de RPC, existe un lenguaje de dominio específico que deberemos conocer para saber cómo crear las interfaces remotas y sus clientes. Nos tendremos que aprender las buenas prácticas, que en general están documentadas. Finalmente, habrá que familiarizarse con las APIs para el lenguaje en que se vaya a usar.

Costes de implementación

Al usar un protocolo binario, es más difícil de depurar las peticiones, comparado a la comodidad de usar postman contra APIs JSON.
No hay un 'soapui' o 'postman' para thrift que esté maduro aún. Sí se están desarrollando varios en distintos lenguajes. Eso sí, para ser RPC, es mucho más simple que SOAP.

Costes de migración

Si queremos usar Thrift en un proyecto preexistente, los servicios y sus clientes deben ser implementados de nuevo. La lógica de negocio se mantendría como tal.
El compilador crea objetos en el lenguaje objetivo, que deberían ser análogos a los que ya existen, suavizando hasta cierto punto la implementación.

Costes de mantenimiento

En cuanto a evolutivos, se añade cierta complejidad respecto a JSON, pero si se conocen las buenas prácticas, no parece que sea un incremento determinante.
Es de esperar que no haya demasiada gente que conozca la tecnología, así que si hay mucha rotación de personal, habrá que incurrir en más gastos de formación en el tiempo. Este es el clásico dilema al que nos enfrentamos siempre que decidimos utilizar una nueva tecnología para nuestros proyectos.

Herramientas/Integración con frameworks existentes

A nivel de herramientas, tenemos un plugin de maven para compilar los esquemas.
En Istio está soportado en vía el proxy de servicios Envoy.
En cuanto a herramientas de prueba, hay un CLI para probar llamadas, pero poco más.

gRPC

Este RPC tiene su origen en Google, que lo desarrolló para consumo interno. Se ha ido haciendo Open Source por fases: en 2004 se abrió al público el subcomponente de Protocol Buffers, y más adelante se liberó el resto. A diferencia de Facebook, que abandonó la gestión de su proyecto Thrift cuando lo hizo Open Source, Google sigue gestionando el desarrollo de su tecnología, lo cual siempre inspira cierta confianza.

Al igual que Apache Thrift, se compone de un IDL y un formato binario de mensajes serializados. Los protocol buffers son los mensajes que el IDL gestionará para realizar las invocaciones remotas. Además hace uso del protocolo HTTP/2 para el transporte, con las ventajas que nos da el basarse en un estándar global que muchos servidores de aplicaciones ya soportan nativamente.

Vamos a ver que en términos generales, hay bastantes similitudes con Thrift ya que en muchos aspectos da igual el RPC que escojamos: nos va a costar lo mismo en términos de aprendizaje, los problemas de versionado serán los mismos y la ganancia en rendimiento será alta.

Eficiencia

Los mensajes se transportan en el formato ad hoc protocol buffer, en binario y comprimidos, de modo que el tamaño es muy pequeño. El transporte de los mensajes se hace mediante el protocolo estándar HTTP/2.

Se pueden definir métodos idempotentes para cachear las respuestas a peticiones repetidas, aunque la caché está en el cliente, no en el servicio.
Para la monitorización de carga se utilizan interceptores, y existe por ejemplo una integración con Istio (https://aspenmesh.io/blog/2018/04/tracing-grpc-with-istio/)

El balanceo de carga ofrece varias opciones que podemos ver en este enlace:
https://grpc.io/blog/loadbalancing
El cliente puede balancear carga por sí mismo o podemos optar por un balanceo externo que se puede hacer con Envoy, Ribbon, Linkerd, o Istio. Las opciones disponibles son bastantes.

Lenguajes soportados

El compilador es un ejecutable en C++. El runtime que se genera soporta una selección de los lenguajes más populares:
Java (6+, Android API 14+), Python, Objective-C, C++, Go, Ruby,JavaScript, PHP, Dart y C#.

De hecho, soporta mensajes en formato Thrift.

Documentación

En términos de API es suficiente, y la documentación está bastante bien organizada. Hay un montón de proyectos de ejemplo en java, Android, Node...

Versionado/compatibilidad con evolutivos

Al igual que ocurría en Thrift, la compatibilidad de un API existente se rompe en las condiciones habituales para cualquier RPC: añadir parámetros nuevos a métodos existentes, cambiar el tipo de retorno del método, etc.

Costes de adaptación/formación

En este apartado nos encontramos con la misma situación que para aprender a usar Thrift (o, para el caso, cualquier RPC): aprender el IDL con su lenguaje de definición de interfaces, manejar el generador de stubs, conocer el API de estos últimos, etc.

La documentación nos da toda la información necesaria y nos guiará para usar las mejores prácticas. Además, la literatura online es extensa y no deja de crecer.

Costes de implementación

He podido leer algún comentario de personas que usaron gRPC en sus proyectos y afirmaban que el proceso de definir las interfaces es mucho más fluido si se compara con una API RESTful. Aún así, seguimos teniendo el problema de depurar las peticiones cuando éstas están formateadas en binario comprimido. Sin embargo, van surgiendo herramientas como grpc-tools que nos facilitarán la vida.

Costes de migración

Si decidimos adaptar una aplicación existente a gRPC, tendremos que reimplementar de nuevo la capa de transporte de aquellos servicios que necesitemos optimizar, sin necesidad de modificar la lógica de negocio. Los data transfer objects se regenerarán, por lo que posiblemente haya que realizar alguna adaptación del código que los utiliza. Esto último puede ser más o menos costoso en función de la complejidad de la capa de negocio.

Costes de mantenimiento

Este es otro aspecto en el que la similitud con Thrift o cualquier otro RPC salta a la vista. Un poco más complejo de evolucionar que JSON-REST de toda la vida, más la dificultad que pueda plantear el que no haya profesionales en el mercado que conozcan la tecnología. La excelente documentación del proyecto suaviza estos problemas pero siempre hay opciones que no siendo óptimas, nos hacen la vida más fácil en este sentido.

Herramientas/Integración con frameworks existentes

En este apartado disponemos de un buen número de opciones:

  • Plugins de Maven y de Gradle para compilar los esquemas.
  • Otro plugin de análisis estático de código para detectar posibles malas prácticas: grpc-java-api-checker.
  • El lenguaje soporta transcodificación de JSON a protocol buffer, lo que permite mantener un API con endpoints JSON y protobuff simultáneos.
  • grpc-gateway: plugin para generar endpoints RESTful automáticamente. CLI para pruebas a mano.
  • No parece haber problemas para usar Istio y otros proveedores de Service Mesh.
  • Spring tiene una integración pendiente en su backlog. Hay un par de boot-starter open source que podrían integrarse en Spring en un futuro próximo.

REST sobre HTTP/2

Aquí vamos a hacer trampa. Thrift y gRPC son frameworks RPC end to end, con su IDL y formato binario específico. Sin embargo, la opción de usar JSON-REST sobre HTTP/2 es una opción muy atractiva que merece la pena analizar, ya que nos proporciona una optimización bastante buena sin tener que tocar apenas el código que conocemos y amamos.

La idea es muy simple, en principio: se trata de desarrollar o actualizar nuestra aplicación usando las prácticas ya conocidas para generar servicios REST (seguramente con JSON como formato). La diferencia estará en el protocolo de transporte, que será HTTP/2, lo cual aportará significativas ventajas en el rendimiento de modo casi del todo transparente.

Eficiencia

HTTP/2 es un transporte que convierte los mensajes a binario comprimido, de modo que el JSON que generamos como petición/respuesta reduce su tamaño en gran medida antes de viajar por la red. La mejora es evidente, aunque ciertamente el formato de mensajes de Thrift y gRPC estén aún más optimizados.

El protocolo también soporta mantener un socket abierto con el servidor que enruta todos los mensajes, minimizando el overhead de negociación HTTP. Esto es otro big win a la hora de interconectar microservicios e incluso para gestionar la conexión desde el cliente al servidor.

Otra ventaja es el server push: se puede anticipar la siguiente petición e ir enviando la respuesta antes de que ésta se produzca. Esta optimización no sale gratis como las otras, y solo aplicará a casos muy específicos (generalmente para peticiones desde el cliente usuario).

La monitorización, cacheo y balanceo de carga están ya resueltas desde hace años para los servidores HTTP, así que no faltarán soluciones para estas necesidades.

Documentación

Como no estamos hablando de un framework RPC per se, no existe un único punto de referencia para obtener documentación. En función del servidor y lenguaje de programación que manejemos, tendremos que referirnos a la documentación de los mismos para implementar soluciones con HTTP/2.

En general, el soporte y documentación a todos los niveles no hace más que crecer. No en vano, es algo tan importante como la evolución de uno de los protocolos fundamentales de internet. Por tanto, es previsible que no tengamos problemas a la hora de encontrar información, tanto para configurar nuestro servidor como para implementar la solución en el lenguaje que estemos usando.

Costes de adaptación/formación/implementación

Migrar un servicio a HTTP/2 es cuestión de pequeños retoques en el código y de configurar el servidor de aplicaciones. El coste es muy inferior a la adopción de Thrift o gRPC.

Si el servicio es de nueva creación, el overhead sobre los costes sería mínimo. Si sabemos de antemano que tenemos un KPI exigente pero no asfixiante, esta es una buena opción para saltarse la ley de no optimizar prematuramente.

La inquisición española

¿Optimización prematura has dicho?

Costes de migración

Se requiere un servidor compatible con HTTP/2. Evidentemente las últimas versiones de los sospechosos habituales lo soportan ya: NginX, Apache, Jetty, Tomcat... El problema lo podemos tener si el cliente para el que estamos trabajando, como suele ser habitual, esté usando una versión antigua que no podamos actualizar. En ese caso puede ser que perdamos la guerra antes de empezarla.

A nivel software, por ejemplo Spring Boot necesita utilizar OkHTTPClient, es un cambio relativamente sencillo y este cliente viene incluido de serie a partir de la versión 4.3. Para otros lenguajes seguramente necesitemos también clientes específicos que es de esperar que ya estén disponibles a estas alturas, teniendo en cuenta que el protocolo HTTP/2 se hizo público en 2015.

Por lo demás, adaptar una aplicación JSON-REST a HTTP/2 debería tener un coste bastante contenido, siempre que no sea un monolito de spaguetti code. Esto en cuanto a código; la infraestructura puede necesitar actualizaciones o reconfiguración. Estos costes variarán en función de lo profundos que sean los cambios (no es lo mismo habilitar un módulo HTTP/2 en NginX que actualizar el servidor de aplicaciones entero y probar que no se rompe nada).

Herramientas/Integración con frameworks existentes

Spring 4.3 o superior incluye el cliente OkHTTP, librería que además está soportada por Android. Para otros lenguajes o frameworks, nos tendremos que buscar soluciones que seguramente ya existan o estén en proceso de crearse. De nuevo, HTTP/2 será muy pronto ubicuo y los lenguajes y el tooling necesitarán soportarlo para no quedarse atrás.

Postman no soporta HTTP/2 en el momento de escribir este artículo, pero esto no impediría probar un API siempre que se autorice acceso HTTP/1.1 a la misma en los entornos de desarrollo e integración. De este modo podemos depurar con la misma facilidad que antes. Si todo va bien, los desarrolladores del Postman harán caso a las numerosas peticiones de este issue y darán soporte nativo.

Otras consideraciones

En lo que se refiere a JSON sobre HTTP/2, no he incluido algunos apartados que sí estaban presentes en las secciones sobre Thrift y gRPC:

  • Costes de mantenimiento.
  • Lenguajes soportados.
  • Versionado/compatibilidad con evolutivos.

El motivo es que, en todos estos aspectos, tenemos una correspondencia casi idéntica con un desarrollo normal y corriente de JSON-REST sobre HTTP/1.1. Por tanto, no hay mucho que decir al respecto. Como mucho cabe destacar que, a diferencia de las otras opciones, con HTTP/2 la curva de aprendizaje es casi plana y el número de profesionales capaces de ponerse a trabajar casi de inmediato será ordenes de magnitud mayor.

Conclusiones

Por lo visto hasta ahora, antes de optimizar los tiempos de respuesta de nuestros servicios, nos tendríamos que preguntar:

  • ¿Necesito optimizar hasta el último milisegundo o me vale una reducción razonable y notable?
  • ¿Tengo la posibilidad de actualizar la infraestructura de servicio del cliente, si esta es antigua y no dispongo de virtualización para usar lo que me haga falta?
  • ¿Me puedo permitir aprender una serie de temas como es un IDL y un lenguaje de dominio específico para gestionar las llamadas y respuestas en mis servicios?

En función de las respuestas, tendremos uno de dos escenarios: o bien usamos un framework de RPC completo, como son Thrift y gRPC, o bien nos limitamos a usar REST sobre HTTP/2. En el primer caso alcanzaremos la máxima optimización a un coste entre medio y alto. En el segundo caso tendremos una optimización muy buena a un coste mínimo.

Si estamos en el primer caso, gRPC parece tener la ventaja sobre Thrift. El único escenario que se me ocurre para usar el segundo es un entorno en el que no se pueda implementar HTTP/2 (requisito para gRPC). O, por supuesto, si el cliente ya está usando Thrift y no le compensa hacer un evolutivo.

En el segundo caso, mucho más habitual, tanto para nuevos proyectos como especialmente para proyectos existentes que requieren optimización, aplicar HTTP/2 es una opción muy atractiva. Bajo coste, basada en estándares de la industria y por tanto con soporte y documentación creciente: no hay muchos peros que ponerle.

Por tanto, como ya decíamos en el spoiler, HTTP/2 es la solución sencilla y barata para optimizar un stack de microservicios. Y gRPC es la opción para una optimización máxima. No es difícil pensar, incluso, en soluciones mixtas en las que los servicios menos críticos usen HTTP/2, y se utilice gRPC para los puntos en los que sea necesario exprimir al máximo los tiempos de respuesta. Thrift nos queda como opción en ciertos casos en los que las alternativas no nos sirven por uno u otro motivo.

Si te ha gustado, ¡síguenos en Twitter para estar al día de nuevas entregas!

Autor

Jordi Hernández Sellés

Licenciado en Filosofía, baterista aficionado. Casi veinte años de entrega al oficio de la programación, ya sea en front o back end. Neutral respecto a poner piña en la pizza o cebolla en la torti