Conductor, lo nuevo de Netflix para la orquestación de microservicios

Cada vez son más las empresas que apuestan por arquitecturas basadas en microservicios. Dichos microservicios están fuertemente especializados, por lo que comúnmente se presenta la necesidad de realizar orquestaciones de los mismos para cubrir una funcionalidad de negocio. Como respuesta a esta necesidad, Netflix ha liberado recientemente Conductor, un nuevo producto dentro del ecosistema Netflix OSS. Este producto implementa un orquestador de flujos que corre en entornos cloud, implementando cada una de las tareas mediante microservicios. Hoy en el blog vamos a ver cómo funciona.

Orquestación de Microservicios con Conductor

Conductor implementa la orquestación de llamadas a múltiples microservicios, de forma que el consumidor obtenga toda la funcionalidad que necesita sin tener que realizar multitud de llamadas que no le aportan ningún valor como pueden ser insertar un acceso en la tabla de auditoría, o consultar el identificador interno de sus cuentas cuando ya se ha logado en el sistema con su usuario y contraseña y desea el saldo total para mostrarlo por pantalla.

El objetivo de Conductor es ofrecer esta funcionalidad, facilitando el control y la visualización de las interacciones entre los microservicios. Por definición, la orquestación de microservicios que se propone es asíncrona, pudiéndose realizar de forma síncrona en caso de ser necesario.

Netflix ha estado utilizando Conductor durante un año, procesando 2.6 millones de procesos de todo tipo. Además, una característica muy interesante es que se ha utilizado en flujos con una alta complejidad de procesamiento como pueden ser los procesos de ingestión de información desde proveedores de datos o de codificación de videos y el almacenamiento de los mismos.

¿Cómo lo hace?

Conductor está basado en la siguiente arquitectura:

Arquitectura de Netflix Conductor

API

  • Workflows: permite la gestión de los datos de ejecución de los flujos.
  • Metadata: permite la gestión de la definición de los flujos y las tareas.
  • Tasks: permite la gestión de los datos de ejecución de los flujos.
  • Admin: API adicional, enfocada a la administración. Permite la recuperación de los datos de configuración, así como purgar los flujos y recuperar todas las tareas encoladas.

El acceso al servidor se realiza vía balanceador HTTP o bien usando un producto de Discovery (Eureka en caso de Netflix OSS).

Servicio

La capa de servicios se basa en microservicios implementados en Java. Dado su carácter stateless se pueden desplegar múltiples servidores para escalar y garantizar la disponibilidad y respuesta del sistema.

Persistencia

Conductor está preparado para funcionar con distintos modos de persistencia:

  • In-memory: solamente recomendado para entornos de test unitarios. La configuración levanta una implementación de dynomite en local.
  • Redis: recomendado para entornos de desarrollo e integración, debido a su velocidad y sus capacidades de persistencia de fácil configuración.
  • Dynomite: implementación por defecto para su uso en producción. Dynomite es una capa de persistencia que proporciona una base de datos distribuida y autoescalable, basándose en bases de datos sin estas características. La implementación que utiliza Netflix se basa en una base de datos Redis, pero podría usarse también una MemCached.

Conductor también permite usar otros backends implementando las interfaces de acceso a base de datos (DAO) tal y como se comenta en este linkPodrían realizarse implementaciones de alguna de las capas, por ejemplo la caché o las colas, dejando la implementación por defecto de la persistencia de los datos de ejecución.

Configuración

La configuración se realiza mediante un fichero de configuración config.properties, que se pasa como parámetro al arrancar el servidor.

¿Qué aporta Conductor?

La razón de ser de Conductor es realizar la orquestación de microservicios facilitando la creación de flujos de trabajo.

Dichos flujos de trabajo se diseñan de forma sencilla en formato JSON, utilizando diferentes tipos de tareas. Si estás acostumbrado a la automatización de procesos, echarás de menos  el editor gráfico, ya que Conductor no lo tiene (ni para flujos ni para la definición de tareas), aunque sí dispone de un visor gráfico de los flujos. Esta característica hace que los flujos implementados sean fácilmente revisables por gente de negocio, siendo necesario un mínimo conocimiento técnico para su creación y modificación.

Flujo en formato JSON y ejemplo del Visor de Workflows

Tareas

Las tareas deben registrarse en el motor previamente a su uso en algún flujo. Este registro puede realizarse de forma programática o bien utilizando el API REST de gestión Metadata.

Las tareas son de dos tipos, principalmente:

  • System tasks: tareas enfocadas al control del flujo. Incluye los siguientes tipos:
  1. Fork: crea una bifurcación en paralelo.
  2. Fork_join_dynamic: similar al fork, pero realiza la paralelización en función de la expresión de entrada.
  3. Join: realiza un join de un flujo previamente bifurcado en un fork o fork_join_dynamic.
  4. Decide: condicional, de forma similar a un “case switch”.
  5. Sub_workflow: arranca un flujo como una tarea. El flujo queda detenido hasta que el subflujo termina.
  6. Wait: introduce un punto de parada asíncrono en el flujo. Se mantiene en estado in_progress hasta que se actualiza con estado completed o failed.
  7. HTTP: ejecuta la llamada de un microservicio a través de HTTP. Dicha invocación se regirá por la política de reintentos declarada al definir la tarea.
  • Simple tasks: tareas para ser implementadas en el worker. Son las tareas enfocadas al negocio en sí. Son tareas pensadas para ser implementadas en un sistema distinto a conductor. Las tareas se comunican con Conductor mediante un sistema de polling para recuperar tareas programadas del tipo configurado, y la devuelven actualizada. En ese momento es cuando Conductor continúa el flujo a partir de la tarea actualizada.

Además de las tareas mencionadas, pueden implementarse nuevas tareas de tipo system con un comportamiento personalizado según la necesidades del proyecto. Para ello habría que extender com.netflix.conductor.core.execution.tasks.WorkflowSystemTask e instanciar la nueva clase mediante patrón Eager Singleton en el startup.

Las tareas pueden tener distintas políticas de reintento en caso de fallo:

  • Marcar flujo como TIMED_OUT.
  • Reintento hasta un número máximo fijado por parámetro para cada tarea.
  • Alerta: registra un contador de reintentos.

Flujos

Los flujos de Conductor son flujos en formato JSON, que hacen referencia a las tareas definidas previamente.

Intercambio de datos

Conductor permite implementar de forma sencilla el intercambio de datos entre las distintas actividades del flujo. Existen dos contextos de datos: workflow y task. De esta forma, el flujo tendrá unos datos de entrada, que podrá propagar a las distintas actividades del flujo a través de los parámetros de entrada de las tareas. También existen datos de salida, tanto a nivel de tarea como de flujo.

El binding de los datos se realizará mediante el uso de JSONPath, que implementa la mayoría de funcionalidad de XPath para DSL JSON. La sintaxis será la siguiente:

${SOURCE.input/output.JSONPath}

Contexto de Ejecución

Conductor sigue un modelo basado en comunicaciones RPC, de forma que los workers corren en máquinas distintas a las del servidor Conductor. De esta forma, son los propios workers los que, vía HTTP, consultan al servidor las tareas pendientes de ejecución. Tras realizar la acción implementada en dicho worker, informan al servidor de la finalización de la tarea, para que continúe el flujo de trabajo.

Arquitectura de Ejecución de Netflix Conductor

Dicho modelo proporciona las siguientes características:

  • Altamente escalable, dado el modelo de persistencia y que el scheduling de las tareas se hace de forma asíncrona mediante colas.
  • Permite diversidad de implementaciones de cada tarea, de forma que, sin modificar el flujo, se puede modificar el comportamiento de los flujos tan solo modificando los workers responsables de dicha tarea.

¿Qué pasa con los servicios ya existentes o fuera de mi control?

Una pregunta que puede realizarse es qué pasa si existe la necesidad de invocar a microservicios ya existentes, o microservicios que están fuera de mi control y por tanto no puedo implementar con la funcionalidad requerida por los workers.

En este caso existen dos alternativas:

  1. Implementar un worker que se encargue de realizar la llamada al microservicio existente y devolver la información.
  2. Implementar invocación mediante una tarea de tipo HTTP. El problema de esta opción es que dicha implementación se orienta a un modo tradicional en el que el servidor pasa a ser un punto de fallo en la llamada HTTP, además de ser un posible cuello de botella ante un gran número de tareas de tipo HTTP con altos tiempos de respuesta.

Instalación

La instalación de Conductor requiere:

  1. Base de datos: Dynomite
  2. Motor de Indexación: Elasticsearch 2.X
  3. Servidor web: TomCat, Jetty o similar, corriendo sobre JDK 1.8+.

El código podemos encontrarlo aquí.

Para instalarlo se deben seguir los siguientes pasos:

  •       Clonar el directorio de conductor:
git clone git@github.com:Netflix/conductor.git
      
  • Arrancar el servidor (las APIS estarán disponibles en http//localhost:8080)
cd server
../gradlew server
  •  Arrancar la UI (estará disponible en http://localhost:3000)
cd ui
gulp watch

Docker

Para una prueba rápida de Conductor en modo test, se puede utilizar el siguiente Docker que descarga las dependencias, y levanta tanto el motor como el UI y el swagger de acceso a las APIs. Pueden levantarse tanto de forma independiente, como de forma conjunta.

Pasos para iniciar y ejecutar un flujo

  • Iniciar un flujo
POST /workflow/{nombreFlujo}
{
	... // dataInput
}
  • Crear un worker para ese tipo de tarea. Dicho worker deberá:

a) Hacer poll del tipo de tarea.

GET /tasks/poll/batch/{tipoDeTarea}

b) Realizar las acciones necesarias.

c) Actualizar el estado de la tarea a “completed”.

POST /tasks
{
…
“outputData” : { ... },
“taskStatus”: “COMPLETED”
         	}

Conclusión

Como se puede ver, Netflix sigue facilitándonos la vida al proporcionarnos componentes que dan respuesta a las necesidades que nos encontramos diariamente en el desarrollo de aplicaciones basadas en microservicios. ¡Ahora sólo queda ponernos manos a la obra e integrarlo en nuestro stack tecnológico para futuros proyectos!

Ingeniero Superior en Informática por la UCM. Apasionado por la tecnología y, sobre todo, por ponerla al servicio de las personas. He trabajado en proyectos J2EE / SOA e Integración en numerosos clientes. Actualmente trabajo como Arquitecto IT y Preventa en Paradigma Digital. Intento mantenerme al día de las novedades y aprovecharlas para construir soluciones cada vez más ágiles, robustas y eficientes.

Ver toda la actividad de Borja Gómez

Escribe un comentario