El problema fundamental que Conductor resuelve es la coordinación fiable y observable de microservicios en procesos de negocio distribuidos y de larga duración. En un entorno de microservicios, la composición de tareas atómicas en workflows complejos introduce desafíos significativos en cuanto a durabilidad, observabilidad, reintentos y gestión de estado ante fallos. La necesidad de orquestar cientos de miles de definiciones de workflow y ejecutar miles de millones de instancias anualmente, como en Netflix, empuja los límites de los sistemas de orquestación tradicionales.
La evolución de Conductor es una respuesta directa a la presión de escala extrema. Inicialmente, su arquitectura se basaba en componentes in-memory como Dynomite, adecuados para cargas de trabajo menores, pero insostenibles para el crecimiento exponencial. La migración a un plano de datos distribuido y persistente, junto con un motor de evaluación asíncrono y optimizado, es una adaptación necesaria para mantener la fiabilidad y el rendimiento bajo una demanda creciente. Esto refleja un patrón común en sistemas distribuidos: a medida que la escala aumenta, las soluciones que funcionan a pequeña escala se convierten en cuellos de botella, forzando una reevaluación de los fundamentos del sistema.
La introducción de un SDK type-safe para la definición de workflows también aborda un problema de ingeniería de software: cómo gestionar la complejidad y reducir errores en la definición de lógica de negocio compleja. Al pasar de JSON declarativo a un DSL Java, Conductor mejora la experiencia del desarrollador y la robustez de los workflows, sin sacrificar la visibilidad en tiempo de ejecución que es inherente a un modelo de grafo de estado.
Arquitectura del Sistema
La arquitectura de Conductor ha evolucionado significativamente. En su primera generación, utilizaba Dynomite (un clustering layer sobre Redis) para metadatos y DynoQueues para colas, con Cassandra y Elasticsearch para datos de ejecución e indexación. Esta configuración presentaba limitaciones de escalabilidad debido a la naturaleza in-memory de Dynomite y problemas de consistencia con actualizaciones concurrentes.
La versión 2.x y 3.0 introdujeron cambios fundamentales. Cassandra se convirtió en el datastore primario para datos de ejecución, proporcionando escalabilidad horizontal y durabilidad. Los inputs y outputs de tareas grandes se descargaron a S3 para reducir la presión sobre Cassandra. DynoQueues fue reemplazado por Timestone, el sistema de colas de prioridad de alta throughput y baja latencia de Netflix, que añadió mecanismos de delayed-queuing y reparación de workflows. La indexación se desacopló del critical path, utilizando Kafka como buffer para alimentar Elasticsearch, y Iceberg se añadió para almacenamiento a largo plazo. Esta transición de un datastore in-memory a una combinación de Cassandra, S3, Timestone, Kafka y Elasticsearch/Iceberg representa una modernización completa del plano de datos.
El motor de evaluación fue reescrito para Conductor 3.0. Anteriormente, cargaba el estado completo del workflow en memoria, lo que generaba alta presión de memoria y latencia. El nuevo motor separa los metadatos del workflow de los datos de tareas y usuarios, y evalúa contra un blueprint ligero, cargando solo los datos de tarea específicos necesarios. Esto reduce drásticamente el footprint de memoria y mejora la velocidad de evaluación. Para gestionar actualizaciones concurrentes de tareas, se implementó un modelo sin bloqueo: el estado de la tarea se divide en una partición 'pending' y una 'terminal'. Un estado terminal siempre prevalece sobre uno no terminal, y entre estados del mismo tipo, se aplica 'last-write-wins', eliminando la necesidad de Cassandra Lightweight Transactions (LWT) y sus problemas de consistencia. Finalmente, la evaluación se desacopló del trigger, encolando los workflows en una cola exclusiva de Timestone para procesamiento asíncrono, eliminando la contención de locks y mejorando la latencia de cola.
Flujo de Evaluación Asíncrona de Workflow
- 1 Actualización de Workflow Llega una actualización (nueva instancia, tarea completada).
- 2 Encolar en Timestone La actualización encola el workflow en una cola exclusiva de Timestone.
- 3 Evaluador Asíncrono Un evaluador central procesa el trabajo pendiente del workflow en orden.
- 4 Actualizar Estado El evaluador actualiza el estado del workflow en Cassandra.
- 5 Notificar Indexador Los cambios se envían a Kafka para indexación asíncrona en Elasticsearch.
Flujo de Ejecución de Tareas con Asignación Dinámica
- 1 Worker Polling Instancias de worker consultan periódicamente Timestone.
- 2 Obtener Profundidad de Cola Cada instancia obtiene la profundidad actual de todas las colas de tareas que...
- 3 Asignar Hilos (Mínimo) Se asigna un número base de hilos a cada cola (asignación mínima).
- 4 Asignar Hilos (Flotante) Los hilos restantes se distribuyen proporcionalmente a la carga de cada cola.
- 5 Ejecutar Tarea Los hilos asignados extraen y ejecutan tareas de sus respectivas colas.
- 6 Reportar Resultado El worker reporta el resultado de la tarea a Conductor.
| Capa | Tecnología | Justificación |
|---|---|---|
| storage | Cassandra | Datastore primario para datos de ejecución de workflows, proporcionando escalabilidad horizontal y durabilidad. vs Dynomite (Redis cluster) |
| messaging | Timestone | Sistema de colas de prioridad de alta throughput y baja latencia, reemplazando DynoQueues para la gestión de tareas y la evaluación asíncrona de workflows. vs DynoQueues (sobre Dynomite) |
| storage | S3 | Almacenamiento de objetos para offload de inputs y outputs de tareas grandes, reduciendo la carga sobre el datastore primario. |
| messaging | Kafka | Buffer para desacoplar la indexación del critical path, alimentando el indexador de Elasticsearch. |
| storage | Elasticsearch | Motor de búsqueda e indexación para la visibilidad y consulta de workflows. |
| storage | Iceberg | Formato de tabla para almacenamiento a largo plazo de datos de ejecución de workflows. |
| compute | Titus | Plataforma de gestión de contenedores de Netflix, utilizada para tareas gestionadas que ejecutan scripts o aplicaciones en contenedores. |
| compute | Stratum | Capa de funciones serverless de Netflix, utilizada para tareas gestionadas que invocan funciones de procesamiento de medios. |
Trade-offs
Ganancias
- ▲▲ Escalabilidad horizontal
- ▲ Durabilidad de datos
- ▲ Latencia de evaluación (p99)
- ▲ Consistencia de estado de tarea
- ▲ Experiencia de desarrollador (type-safety)
- ▲ Utilización de recursos (CPU)
Costes
{
"name": "wrap_movie_workflow",
"version": 1,
"semver": "0.0.1",
"schemaVersion": 2,
"restartable": true,
"workflowStatusListenerEnabled": false,
"ownerEmail": "team@netflix.com",
"timeoutPolicy": "ALERT_ONLY",
"timeoutSeconds": 0,
"tasks": [
{
"name": "get_movie",
"taskReferenceName": "movie",
"type": "SIMPLE",
"inputParameters": {
"movieId": "${workflow.input.movieId}"
}
},
{
"name": "wrap_movie",
"taskReferenceName": "wrapMovie",
"type": "SIMPLE",
"inputParameters": {
"movie": "${movie.output}"
}
}
]
}@WorkflowStub(taskNames = {"movieapp.getMovie", "movieapp.wrapMovie"})
public interface MovieWorkflows {
@WorkflowGenerated(name = "movieapp.getMovie")
Movie getMovie(int movieId);
@WorkflowGenerated(name = "movieapp.wrapMovie")
WrappedMovie wrapMovie(Movie movie);
@WorkflowMethod(name = "wrap_movie_workflow", ownerEmail = "team@netflix.com")
default WrappedMovie wrapMovieWorkflow(MovieInput input) {
Movie movie = getMovie(input.getMovieId()); // long in, Movie out (checked)
return wrapMovie(movie); // Movie in, WrappedMovie out (checked)
}
}@WorkflowMethod(name = "process_asset_workflow", ownerEmail = "team@netflix.com")
default void processAssetWorkflow(AssetInput input) {
JobStatus status = submitJob(input);
do { // loops are do-while
status = checkStatus(input.getJobId());
} while (status.getState() == "PENDING");
if (status.getState() == "SUCCEEDED") { // branch on the result
publish(input);
} else {
flagForReview(input);
}
}@WorkflowStub(
taskNames = {"movieapp.getMovie", "movieapp.enrichMovie", "movieapp.getArtwork",
"movieapp.refreshCache", "movieapp.publishPage"},
workflowNames = {"movieapp.localize_page_workflow"})
public interface MoviePageWorkflows {
// stubs not shown for brevity
@WorkflowMethod(name = "prepare_movie_page", ownerEmail = "team@netflix.com")
default void prepareMoviePage(MoviePageInput input) {
// three independent branches, run in parallel
Fork<WrappedMovie> details = ParallelExecution.fork(() -> {
Movie movie = getMovie(input.getMovieId());
return enrichMovie(movie);
});
Fork<Artwork> art = ParallelExecution.fork(() -> getArtwork(input.getMovieId()));
Fork<Void> cache = ParallelExecution.fork(this::refreshCache);
ParallelExecution.allOf(details, art, cache); // FORK_JOIN and JOIN
WrappedMovie enriched = details.get();
Artwork artwork = art.get();
LocalizedPage localized = localizePageWorkflow(input); // sub-workflow call
publishPage(enriched, artwork, localized);
}
}Fundamentos Teóricos
La evolución de Conductor resuena con principios fundamentales de sistemas distribuidos y bases de datos. La transición de Dynomite (esencialmente un cache distribuido) a Cassandra como datastore primario para datos de ejecución es un ejemplo clásico de cómo la durabilidad y la escalabilidad horizontal se priorizan sobre la baja latencia de un sistema in-memory cuando la consistencia y la persistencia son críticas. Esto se alinea con los principios de bases de datos distribuidas que buscan el equilibrio entre consistencia, disponibilidad y tolerancia a particiones (CAP theorem), donde Cassandra es un sistema AP (Available and Partition-tolerant) que ofrece consistencia eventual.
El problema de la consistencia de estado en presencia de actualizaciones concurrentes y reordenamiento de mensajes es un desafío bien conocido en la computación distribuida, abordado por conceptos como los Conflict-free Replicated Data Types (CRDTs) o los algoritmos de consenso como Paxos o Raft. Aunque Conductor no implementa un CRDT completo, su estrategia de particiones 'pending' y 'terminal' con reglas de reconciliación en la capa de aplicación es una forma pragmática de lograr una consistencia fuerte para el estado final de una tarea, priorizando el estado terminal y aplicando 'last-write-wins' para estados intermedios. Esto es una aplicación de lógica de negocio para resolver problemas de concurrencia que de otro modo requerirían mecanismos de bloqueo más costosos o complejos algoritmos de consenso, que serían excesivos para la gestión del ciclo de vida de una tarea individual.