El nacimiento de Xorcery - Construir microservicios altamente performantes (1ra. Parte)

 

Using dynamic DNS for service discovery - Rickard Öberg from JavaZone on Vimeo.


Hoy vengo a contarles uno de los proyectos mas prometedores que se ha empezado a cocinar en con el liderazgo de nuestro compañero Rickard Öberg y miembros del team en exoreaction.

Este proyecto se llama Xorcery. Una librería diseñada para ayudarnos a crecer nuestra solución basada en micro servicios que ya implementa clientes REST API y servidores, así como el streaming de data reactiva de forma minimalista y simple.


A continuación comparto las ideas que ha compartido con nuestro team Rickard Öberg.


Las APIs REST

Revisión del Problema del Lado del servidor 


Al revisar las implementaciones recientes de API REST, estas cumplen con devolver datos estructurados y por lo general, en formato JSON. Y si esta estructura no esta definida por un esquema JSON, esta también podría ser un texto sin formato. Lo cual trae como consecuencia que los clientes tengan que hacer mucho trabajo para lograr una buena integración.  Asi que usar un formato JSON es la clave, porque permite componentes reutilizables para no comenzar desde cero todo el tiempo. Vease la especificación JSON:API v1.1 que es en la que Xorcery se va a basar.

En la mayoría de las API REST no hay enlaces, lo que trae como consecuencia, el tener que implementar interminables estructuras de URL en los clientes, y si estos cambian se produce el caos. También hace imposible que los clientes puedan deducir que posibles acciones se pueden seguir, ya que no hay enlaces a formularios permitidos dado el estado actual del sistema.

Con lo anterior, se puede observar la falta de "descubribilidad" que es uno de los usos claves usar REST. El permitir que un cliente pueda cuando algo es posible o no basandose en la existencia de enlaces con relaciones conocidas. 

Finalmente, no tenemos formularios como consecuencia de no tener enlaces en los datos transmitidos. Si tuvieramos formularios, los clientes podrían saber que acciones son posibles, pero, al no tenerlas teerminamos en implementaciones CRUD del API simplistas, donde la unica forma de descubrir que una acción no fue posible es enviar una solicitud POST y rechazarla. 

Tener formularios vinculados a la estructura de datos permitiría a los clientes no solo saber cuándo son posibles ciertas acciones, sino que también facilitaría la realización de acciones que actualicen solo unos pocos campos en un recurso, en lugar de tener que depender de solicitudes PATCH donde el cliente "sabe” qué hacer.

Conclusión: El lado del servidor de las API REST es un completo desastre en este momento. Esto se podría solucionar simplemente usando un formato multimedia que incluya todas estas características de forma nativa, como el formato JSON:API, combinado con el esquema JSON para definiciones de las partes personalizadas de la API.

Solución: Crear un cliente sandbox totalmente automatizado que traduzca una API REST a HTML y permita a un desarrollador interactuar con ella en un navegador. 


Revisión del Problema del Lado del cliente 

La mayoría de los clientes de API REST estan orientados a solicitudes. Usted construye una URL, hace un GET o POST basado en la documentación del API y luego la envía. Y como cada API tiene su estructura JSON cada cliente es único y debe adaptarse al servicio que está creando. Este estilo de clientes ignora por completo lo que aprendimos de la web cuando se trata de diseño de clientes. 

REST hace que el servidor sea sin estado, en el sentido de sesiones de conexión, pero lo hace poniendo esta responsabilidad en manos del cliente. Pero si todo lo que tiene es un cliente REST basado en solicitudes, ¿dónde está el estado? Esta olvidado.

La forma correcta de hacerlo es ver cada interacción de una API REST como una sesión, con estado. Tal como lo hace un navegador web. Una sesión de cliente REST debería permitir tres acciones principales:

  • Extraer información de los datos estructurados
  • Seguir enlaces
  • Enviar Formularios
Esto hace que sea más fácil manejar la recuperación de errores, realizar múltiples pasos, encadenar flujos de procesos y manejar el caso en el que es posible que la interacción nunca termine, en caso de que el servidor no esté disponible durante toda la vida útil del cliente.

Estos son algunos de los problemas de las API REST, tanto en el cliente como en el servidor, que queremos solucionar con Xorcery, facilitando la creación de API REST basadas en los principios de la web y facilitando en crear clientes REST que tengan sesiones con estado que puedan abordar las falacias de la computación distribuida de una manera repetible y natural.


Descubrimiento distribuido, orquestación y conectividad


Al comienzo de este post encontrará un vídeo de la charla de Rickard en el JavaZone del 2023 sobre como Using dynamic DNS for service discovery - Rickard Öberg que explica este punto.

Una segunda area que necesita ayuda es la colaboración de servicios. Los servicios deben poder descubrirse entre sí y descubrir donde está disponible un servicio en particular que se necesita, mediante las relaciones definidas en la descripción del servicio y JSON;API y JSON Schema para luego interactuar con ellos. 

Si queremos escalar un servidor para arriba o para abajo, debemos poder hacerlo sin un mecanismo externo. Debe estar intergrado en cada servidor para que los servicios puedan detectarse entre sí, decidir como organizar las colaboraciones y gestionar las conexiones entre servicios. 

Es importante, hacer la distinción entre servidores, contenedores y servicios que realizar una tarea particular. Un servidor puede tener uno o veinte servicios, y la razón para hacer uno u otro debe basarse en las necesidades, no en un diagrama de diseño preconstruido que puede o no ser aplicable al sistema en ejecución. Si un servidor puede ejecutar todos los servicios, normalmente lo llamamos "monolito". En Xorcery tal vez sea mejor considerarlo como “un sistema que aún no ha tenido suficiente presión como para requerir una división”.


Siempre que los servicios estén diseñados de tal manera que no estén codificados con la dependencia de que otros servicios se ubiquen o no en el mismo servidor, tenemos la libertad de elegir cuándo realizar dichas divisiones. 
Si bien es posible ejecutar siempre servidores con un solo servicio cada uno, pero, cada uno con su propio contenedor Docker ejecutándose en la misma máquina virtual, esto no es particularmente rentable ni práctico. Es una complejidad innecesaria.

Al crear cuidadosamente métodos de descubrimiento y orquestación, que utilizan los principios de REST para permitir ignorar las ubicaciones físicas de los servidores, podemos hacer que sea más fácil comenzar poco a poco y crecer cuando sea necesario, a medida que aumentan las presiones sobre el sistema, en lugar de tener que diseñar por adelantado estas delineaciones.

Finalmente, analizando que el principal medio de comunicación entre servicios actualmente es, a traves, de API REST, en algunas situaciones, no son tan útiles. En particular cuando se trata de enviar flujos de datos entre sí. Ahí es mejor utilizar una forma de abstracción de transmisión y con el descubrimiento del patrón reactivo en especial los flujos reactivos. Entonces, concluimos que se debe permitir a los servicios publicar en flujos de datos reactivos y se suscriban y consuman estos flujos facilmente. Esto para nosotros es la herramienta que nos falta. 

En Xorcery estamos abordando esto proporcionando una función incorporada de flujo reactivo de procesamiento por lotes, que se implementa mediante websockets y la biblioteca Disruptor, para que sea más fácil posible la creación de flujos de datos que eventualmente progresen, que puedan procesarse por lotes y sean recuperables, en caso de errores. 

Esta es una mejora importante con respecto al uso de endpoints de API REST para transferir flujos continuos de datos de un servicio a otro. Como se trata de una característica nativa, combina bien con las características de descubrimiento y orquestación para permitir que los servicios se encuentren fácilmente entre sí y decidan quién debe conectarse a qué. Al utilizar websockets y, posteriormente, aprovechar el HTTP/3, estamos reutilizando toda la infraestructura existente que necesitamos para estas conexiones, incluido el cifrado, la autenticación y la autorización SSL.

Service meshes (Mallas de Servicios)


Cuando un sistema de microservicios crece hasta "un cierto tamaño", inevitablemente llegará la pregunta de si se debe utilizar una malla de servicios o no. Esto se debe en parte a los problemas mencionados anteriormente con el descubrimiento, la orquestación y la conectividad, pero si los manejamos permitiendo que los servidores tengan esas características de forma nativa, ¿qué nos podría ofrecer?

Nos quedan problemas operativos, principalmente de registro, métricas y gestión de certificados para SSL. Dado que estamos tratando con el registro y las métricas del ecosistema Java, ambos tienen herramientas que podemos integrar con el servidor mismo y, utilizando las funciones de flujos reactivos, estos se pueden enviar de manera eficiente a servidores centralizados para su almacenamiento y análisis.

El problema restante relacionado con la administración de certificados lo podemos manejar creando un servicio que se ejecute en cada servidor Xorcery y que actualice periódicamente el certificado instalado localmente para habilitar SSL, tanto para nuestras API REST como para conexiones websocket, además de manejar algunos escenarios de autenticación y autorización donde los certificados de cliente son suficientes.

Con esto esperamos que no haya necesidad de los engorrosos service meshes que ya no serían necesarios. Menos piezas móviles, menos cosas que actualizar, menos cosas que puedan fallar y menos conexiones de red dan como resultado menos dolores de cabeza relacionados con las falacias de la computación distribuida.



El crecimiento de un sistema

La parte más compleja de la creación de sistemas de microservicios tiene que ver con afrontar el crecimiento. O más específicamente, lidiar con la presión.

¿Qué presiones tenemos en un sistema distribuido que podría ser útil analizar? Aquí hay algunas opciones posibles:

  • Tamaño de la data
  • Tamaño de la característica del sistema
  • Operaciones por segundo
  • Tamaño del equipo

Por ejemplo, con tamaños de datos pequeños podría ser suficiente tener un único servidor con los datos. A medida que crezca es posible que queramos replicarlo. A medida que crezca aún más, es posible que queramos fragmentarlo.

Con un equipo pequeño, es posible que queramos que todas las funciones se reúnan en un solo servicio. A medida que el equipo crece o se divide, es posible que deseemos dividir los servicios en consecuencia para facilitar la gestión del control de cambios.

Todas estas cosas son presiones que pueden cambiar la forma en que crecen nuestros sistemas. Pero, por definición, no podemos conocer el futuro. Entonces, ¿cómo diseñaría un sistema para manejar estas presiones sin saber cuáles serán? Cómo será todo de aquí a un año?, en cinco años, en diez años, etc. 

Al leer artículos de ingenieros de Facebook y Twitter sobre cómo han evolucionado sus sistemas, queda claro que lidiar con los cambios en las presiones es el principal dolor de cabeza con el que todos tienen que lidiar.

Y así, la conclusión a la que llegamos, resulta tan obvia para mencionarla. ¿Por qué no diseñar el sistema de manera que pueda hacer frente a estas presiones cuando se produzcan? En lugar de intentar diseñar desde el principio lo que creemos que serán en el futuro, podríamos implementar mecanismos que nos permitan cambiar la estructura del sistema sobre la marcha, como reacción a estas presiones y no de manera proactiva. Aunque no sepamos lo que nos deparará el futuro, entonces sabremos que tenemos opciones sobre cómo lidiar con las diversas posibilidades cuando se presenten frente a nosotros.

Tengo la esperanza de que, al abordar todas las cuestiones antes mencionadas de manera inteligente, la capacidad de reaccionar ante las presiones se convierta en un resultado natural y no en algo que deba abordarse específicamente. Este es el objetivo de diseño de todas las demás partes: facilitar el uso de Xorcery desde el principio hasta el final del ciclo de vida de una arquitectura de microservicios.


En los siguientes articulos ire compartiendo con todos ustedes detalles de la implementación y tutoriales de como usar Xorcery.


Enjoy!


José Díaz



Share:

2 comentarios: