Tema representativo de entrevista

¿Cómo diseñarías un programador de tareas distribuido?

Diseño de sistemasDifícil
Equipo editorial de Offer.ccPublicado Actualizado

Pregunta

Diseña un programador de tareas distribuido multiinquilino que admita tareas únicas y tareas cron con husos horarios de IANA. Almacena 20 millones de programaciones activas, crea 200 millones de ocurrencias por día y puede tener 500,000 disparadores en el cambio de hora. Bajo carga máxima normal, las tareas estrictas deben llegar a una cola dentro de los 5 segundos en el p99; las tareas flexibles se pueden distribuir en una ventana de 5 minutos. La entrega es de al menos una vez y las programaciones se pueden pausar, reanudar, actualizar y cancelar.

Consigna y alcance

Diseña un programador de tareas distribuido multiinquilino. Admite tareas únicas y tareas cron con husos horarios de IANA, almacena 20 millones de programaciones activas, crea 200 millones de ocurrencias por día y puede tener 500,000 disparadores en el cambio de hora. Bajo carga máxima normal, las tareas estrictas deben llegar a una cola duradera dentro de los 5 segundos de su hora programada en el p99. Las tareas que no requieren una hora exacta se pueden distribuir en una ventana flexible de 5 minutos.

La API admite crear, pausar, reanudar, actualizar y cancelar programaciones. La entrega es de al menos una vez. El programador es responsable de convertir una hora programada en una ocurrencia y colocarla en una cola de destino. La ubicación de contenedores, las dependencias de DAG y la orquestación de flujos de trabajo arbitrarios están fuera del alcance. El tiempo real de finalización de la tarea no forma parte del SLO de programación de 5 segundos.

Esta pregunta se adapta a roles de backend, plataforma, infraestructura y diseño de sistemas de nivel intermedio a sénior. El material público de diseño de sistemas de 2026 todavía trata a un programador de tareas como un problema de entrevista independiente, mientras que la documentación actual de AWS y Kubernetes expone límites prácticos en torno a ventanas flexibles, creación duplicada o faltante, fallas de disparo, husos horarios y reintentos. El desafío central no es analizar expresiones cron. Es materializar de manera confiable una hora programada en una ejecución rastreable, reintentable e idempotente.

Qué evalúa el entrevistador

La primera señal es separar Schedule, Occurrence y Attempt. Una programación describe la temporización futura. Una ocurrencia representa una hora programada específica. Un intento representa una prueba de entrega o procesamiento para esa ocurrencia. Una sola fila con next_run_at y status mezcla reintentos, historial, actualizaciones y el próximo disparador.

La segunda señal es negarse a prometer exactamente una vez a la ligera. Un programador puede fallar después de encolar pero antes de registrar el éxito, y un worker puede completar un efecto secundario antes de perder su confirmación. Kubernetes señala explícitamente que un CronJob a veces puede crear dos Jobs o ningún Job y, por lo tanto, recomienda tareas idempotentes. El arrendamiento de visibilidad de una cola estándar tampoco puede evitar de forma absoluta la entrega duplicada. Una respuesta sólida elige la entrega de al menos una vez y hace que el efecto de negocio sea idempotente con un ID de ocurrencia estable.

La tercera señal es el manejo de picos sincronizados. El promedio diario es de solo unas 2,315 ocurrencias por segundo, pero colocar 500,000 disparadores estrictos en las colas dentro de 5 segundos requiere aproximadamente 100,000 despachos por segundo. La capacidad basada en el promedio falla en el cambio de hora. Un solo heap global ordenado por tiempo también crea un punto caliente de capacidad y disponibilidad.

Finalmente, el comportamiento del tiempo y de las fallas debe ser un contrato del producto: si cron significa hora local de reloj de pared o un intervalo fijo, qué sucede con los horarios de verano inexistentes y repetidos, si una interrupción de diez minutos causa recuperación acumulada o descartes, si las ejecuciones pueden superponerse y qué significa la actualización o cancelación después de que una ocurrencia ha sido encolada.

Preguntas para aclarar

  • ¿Qué miden los cinco segundos? Defínelo como enqueued_at - scheduled_for, cubriendo únicamente la programación y el encolado. Si se requiere la finalización, el modelo de capacidad cambia por completo.
  • ¿Se requieren programaciones únicas, cron y de tasa fija? Este diseño admite programaciones únicas y cron. Cada 24 horas difiere de las 09:00 hora local a través de los cambios de horario de verano y no debe usar el mismo cálculo.
  • ¿Son preferibles los duplicados a las omisiones? Este diseño elige la entrega de al menos una vez y maneja los duplicados con idempotencia. Un efecto externo no repetible necesita una clave de idempotencia o un riesgo residual explícito.
  • ¿Qué sucede con las ocurrencias omitidas durante el tiempo de inactividad? Cada programación necesita una misfire_policy, un retraso máximo y un límite de recuperación acumulada. De lo contrario, la recuperación puede crear repentinamente millones de ocurrencias obsoletas.
  • ¿Puede una programación superponerse a sí misma? El valor predeterminado es ALLOW, con SKIP_IF_RUNNING como otra opción. Reemplazar trabajo en curso arbitrario es inseguro cuando los efectos secundarios no se pueden deshacer.
  • ¿Qué tan fuertes son las garantías de actualización y cancelación? Las ocurrencias pendientes de versiones antiguas deben quedar invalidadas. Eliminar una programación no puede retirar el trabajo que un worker ya reclamó o completó, por lo que la API debe informar el estado real.
  • ¿Cuál es el destino? Este diseño coloca las ocurrencias en colas duraderas para controladores registrados. Se excluye el código de usuario arbitrario para que el sandboxing y la ubicación del cómputo no oscurezcan el problema de programación.
  • ¿Qué aislamiento de inquilinos se requiere? Aplica cuotas separadas para el conteo de programaciones, la tasa de disparos, la concurrencia en ejecución y la tasa de recuperación acumulada. Un lote en el cambio de hora no puede consumir cada partición y cola.

Respuesta de 30 segundos

“Separaría programaciones, ocurrencias e intentos. Una programación almacena la expresión, el huso horario de IANA, la versión y el próximo disparador. Los nodos del programador se particionan por intervalo de tiempo y hash de ID de programación, y materializan solo un horizonte acotado. El ID de ocurrencia se deriva del ID de programación, la versión y la hora programada original; una restricción única evita la materialización duplicada durante la conmutación por error. La ocurrencia, el próximo disparador y la outbox se confirman en una sola transacción, luego un relay escribe en una cola duradera al menos una vez. Los workers procesan bajo arrendamientos de visibilidad renovables, mientras que los controladores de negocio deduplican por ID de ocurrencia. Las tareas estrictas se aprovisionan para la tasa máxima y las tareas flexibles usan fluctuación (jitter) determinista. El comportamiento ante fallas de disparo, superposiciones, husos horarios y cancelaciones son políticas explícitas. Lo validaría con 500,000 disparadores simultáneos, caídas en cada transferencia, límites de horario de verano y carreras de cancelación”.

Análisis detallado paso a paso

Calcula el plano de datos antes de dibujar los componentes:

text
average = 200,000,000 / 86,400 ≈ 2,315 occurrences/s
strict_peak = 500,000 / 5 = 100,000 dispatches/s

El pico estricto es más de 43 veces el promedio diario. Si una programación activa y una ocurrencia usan cada una aproximadamente 1 KB de almacenamiento lógico, los metadatos de programación son de aproximadamente 20 GB y las ocurrencias agregan aproximadamente 200 GB por día. Los índices, las réplicas, los mensajes en cola y la retención del historial agregan más. Estas son suposiciones de dimensionamiento para particionamiento y niveles de almacenamiento; los campos e índices reales deben probarse bajo carga.

El plano de control valida expresiones, autoriza inquilinos, aplica cuotas, crea de forma idempotente y versiona actualizaciones. Un modelo simplificado es:

text
Schedule(schedule_id, tenant_id, expression, time_zone, version,
         next_run_at, state, misfire_policy, max_lateness,
         overlap_policy, flexible_window)

Occurrence(occurrence_id, schedule_id, schedule_version,
           scheduled_for, available_at, state, attempt_count)

Attempt(attempt_id, occurrence_id, lease_token, started_at,
        finished_at, result)

UNIQUE(schedule_id, schedule_version, scheduled_for)

La API de creación acepta una clave de idempotencia del emisor. Valida la expresión cron y el huso horario y previsualiza varios disparadores futuros para que una expresión sintácticamente válida pero no intencionada se detecte temprano. Almacena next_run_at en UTC mientras conserva la expresión original y la zona IANA. Una tarea diaria en hora local debe derivar su próxima hora a partir de las reglas de la zona en lugar de sumar 24 horas a la marca de tiempo UTC anterior.

Los casos límite de husos horarios requieren un contrato estable. Este diseño omite una hora local inexistente durante el adelanto de hora en primavera y se ejecuta una vez cuando el retraso de hora en otoño repite una hora de reloj de pared. Una actualización de huso horario afecta solo a ocurrencias futuras bajo una nueva versión de programación. AWS Scheduler documenta el mismo comportamiento de omitir y ejecutar una vez, pero aquí sigue siendo una elección de producto en lugar de una regla universal de programador. Un tipo futuro de tasa fija usaría la duración transcurrida y no cambiaría con el horario de verano.

El almacén de programaciones tiene un índice equivalente a (time_bucket, shard, next_run_at), con shard = hash(schedule_id) mod N. Los nodos del programador arriendan múltiples particiones lógicas y escanean un horizonte de planificación acotado, como los próximos minutos. Un único líder global limita la capacidad y la recuperación. Los arrendamientos de particiones reducen los escaneos duplicados, mientras que la unicidad de la base de datos y las escrituras condicionales proporcionan el límite final de corrección.

Para cada programación vencida, una transacción inserta un Occurrence determinista, avanza condicionalmente next_run_at desde la versión de programación actual y escribe una fila de outbox de despacho con available_at. Si dos nodos se superponen durante una transición de arrendamiento, la unicidad deja una sola ocurrencia (schedule_id, version, scheduled_for). La actualización condicional evita que un nodo antiguo mueva el próximo disparador hacia atrás.

No generes un historial cron infinito por adelantado. Un horizonte de planificación demasiado corto traslada la fluctuación del almacenamiento directamente al SLO de 5 segundos. Uno demasiado largo hace que las actualizaciones y cancelaciones invaliden un gran conjunto de ocurrencias de versiones antiguas. Elige un horizonte que cubra la conmutación por error del programador y los presupuestos de encolado, y luego ajústalo a partir del retraso medido. Una programación única se vuelve terminal; una programación recurrente mantiene solo un cursor de próximo disparador mientras las ocurrencias antiguas pasan a un historial por niveles.

Cerca de available_at, un relay de despacho escribe eventos de outbox en colas duraderas particionadas por inquilino y prioridad. Si el relay falla después de que la cola acepta un mensaje pero antes de la confirmación en la base de datos, envía el mismo occurrence_id nuevamente. Ese duplicado es intencional. Marcar como enviado antes de encolar puede omitir trabajo, mientras que encolar antes de marcar puede duplicarlo. Una outbox transaccional hace que la compensación sea una ruta explícita, reintentable de forma idempotente y de al menos una vez.

Quinientas mil tareas en el cambio de hora requieren más que hilos de sondeo adicionales. Divide cada intervalo de tiempo en suficientes particiones hash de ID de programación. Si las pruebas de carga muestran que una partición realiza de forma segura Q despachos por segundo con transacciones reales, índices y escrituras en cola, el plano estricto necesita al menos ceil(100,000 / Q) particiones disponibles simultáneamente más margen para fallas. La cola aplica equidad ponderada por inquilino y cuotas de tasa para que un inquilino no pueda consumir el carril estricto.

Las tareas con una ventana de 5 minutos utilizan fluctuación (jitter) determinista: available_at = scheduled_for + hash(occurrence_id) mod 300s. La misma ocurrencia obtiene la misma hora después de reintentos y conmutaciones por error, lo que hace que el comportamiento sea reproducible mientras distribuye disparadores correlacionados. Las tareas estrictas conservan la hora original y la capacidad reservada. La Builders' Library de Amazon también identifica el jitter en tareas periódicas de mantenimiento como una forma de reducir fallas correlacionadas.

Un worker recibe una ocurrencia bajo un arrendamiento de visibilidad acotado en el tiempo y lo renueva con latidos (heartbeats) para trabajos largos. La expiración del arrendamiento hace que el mensaje vuelva a ser visible, y la entrega duplicada no se puede excluir incluso durante la ventana. Por lo tanto, la máquina de estados no puede tratar un arrendamiento como exactamente una vez:

text
SCHEDULED -> ENQUEUED -> RUNNING -> SUCCEEDED
                         |   |
                         |   +-> RETRY_WAIT -> ENQUEUED
                         +------> DEAD

Side states: MISSED, CANCELED, SKIPPED_OVERLAP

Cada reclamo obtiene un nuevo lease_token o una generación de intento incremental. Las actualizaciones de finalización deben coincidir con el token actual para que un worker que agotó su tiempo de espera no pueda sobrescribir el estado de un intento más reciente. Esta esgrima (fencing) protege el estado del programador, no un efecto externo ya realizado por el worker antiguo. La idempotencia del negocio utiliza occurrence_id como una clave única en la misma transacción que el efecto, o como la clave de idempotencia de la API aguas abajo. Si un destino externo carece de idempotencia y no se puede consultar, el riesgo de duplicación permanece.

Reintenta según la clase de error. Los errores permanentes de parámetros van a DEAD. Las fallas de red, la limitación de tasa (throttling) y las respuestas 5xx temporales usan retroceso exponencial con fluctuación completa, acotado por los intentos máximos, el plazo límite de la ocurrencia y el presupuesto del inquilino. Una DLQ de la cola retiene las ocurrencias agotadas. La reproducción manual mantiene el mismo occurrence_id; asignar un nuevo ID eludiría la deduplicación.

La recuperación del plano de control necesita políticas explícitas ante fallas de disparo. SKIP marca las ocurrencias que superan el retraso máximo como MISSED. FIRE_ONCE emite solo la última ocurrencia omitida. CATCH_UP emite como máximo K, sujeto a una tasa de recuperación del inquilino. El plazo de inicio y los límites de programaciones omitidas de Kubernetes exponen la misma decisión: una recuperación sin límites pierde trabajo necesario o crea una tormenta de recuperación.

SKIP_IF_RUNNING verifica atómicamente si existe una ocurrencia RUNNING actual de la misma programación antes de marcar la nueva como SKIPPED_OVERLAP. Sin una transición de estado atómica, dos ocurrencias pueden observar ambas la ausencia de predecesores. Cancelar o actualizar incrementa la versión de la programación e invalida las ocurrencias de versiones antiguas no reclamadas. Los workers vuelven a verificar la versión y el marcador de cancelación antes del efecto secundario. El trabajo iniciado requiere cancelación cooperativa, y los efectos externos completados no pueden ser revertidos por el programador.

La observabilidad sigue a las garantías: p50/p95/p99 para schedule_lag = enqueued_at - scheduled_for; filas pendientes por intervalo de tiempo; retraso de materialización; conflictos de unicidad; acumulación en outbox; antigüedad en cola; reclamos duplicados; reintentos y DLQ; descartes por MISSED y superposición; limitación de inquilinos; desfase de reloj; y tiempo empleado en cada estado de ocurrencia. La disponibilidad de la API no prueba que las ocurrencias esperadas hayan llegado a las colas a tiempo.

Las pruebas de aceptación primero inyectan 500,000 ocurrencias estrictas en el cambio de hora y verifican el p99 de 5 segundos más la equidad entre inquilinos. Luego, provocan caídas en los programadores antes de la inserción, después de la confirmación de la transacción y después del envío a la cola; afirman que no haya omisiones silenciosas y que exista deduplicación mediante el mismo ID de ocurrencia. También cubren a un worker que pierde la confirmación después de su efecto, la expiración del arrendamiento, las tres políticas ante fallas de disparo tras una interrupción de diez minutos en el plano de control, ambas transiciones de horario de verano, carreras de actualización y cancelación, superposición de ejecuciones largas y la tormenta de recuperación de un solo inquilino.

Respuesta de ejemplo sólida

“Definiría el SLO como el tiempo transcurrido desde la hora programada hasta la aceptación en la cola duradera, excluyendo la ejecución. Doscientos millones de disparadores diarios promedian unos 2,315 por segundo, pero 500,000 disparadores en el cambio de hora dentro de cinco segundos requieren un plano estricto dimensionado para 100,000 por segundo. El trabajo flexible se puede distribuir de forma determinista a lo largo de cinco minutos.

El modelo separa programaciones, ocurrencias e intentos. Una programación almacena su expresión cron, la zona IANA, la versión y next_run_at. Un ID de ocurrencia se deriva del ID de programación, la versión y scheduled_for y tiene una restricción única. Los nodos del programador se particionan por intervalo de tiempo y hash de ID de programación y arriendan particiones para escanear. Una transacción inserta la ocurrencia, avanza el próximo disparador y escribe una outbox de despacho, de modo que la conmutación por error no omita trabajo silenciosamente y un relay repetido envíe solo el mismo ID de ocurrencia.

La cola y los workers utilizan semántica de al menos una vez. Los workers tienen arrendamientos de visibilidad renovables y la finalización debe coincidir con el token de arrendamiento actual. El controlador de negocio utiliza el ID de ocurrencia como una clave única de base de datos o una clave de idempotencia aguas abajo. La expiración del arrendamiento puede agregar intentos sin agregar efectos de negocio; un destino externo sin idempotencia retiene un riesgo explícito de duplicación.

El comportamiento ante fallas de disparo, superposiciones, husos horarios y cancelaciones son políticas de la API. Tras un tiempo de inactividad, una programación puede omitirse, dispararse una vez o recuperarse como máximo K veces. Se omite una hora cron inexistente y una hora de reloj de pared repetida se ejecuta una vez. Las actualizaciones incrementan la versión y los workers vuelven a verificar antes de los efectos secundarios. Probaría el pico en el cambio de hora, cada ventana de caída, mensajes duplicados, límites de horario de verano y carreras de cancelación, mientras monitoreo el retraso de programación, la acumulación pendiente, los reclamos duplicados, las ocurrencias omitidas, la DLQ y la limitación de inquilinos”.

Errores comunes

  • Combinar programación, ocurrencia e intento en una sola fila → Los reintentos sobrescriben el historial y las actualizaciones no pueden identificar ocurrencias antiguas → Separa Schedule, Occurrence y Attempt.
  • Dimensionar para el promedio diario de 2,315 por segundo → Quinientos mil disparadores en el cambio de hora causan un retraso severo → Prueba particiones bajo carga en el pico estricto y distribuye el trabajo flexible de forma determinista.
  • Usar un único líder global con un min-heap en memoria → El líder se convierte en un cuello de botella de capacidad y recuperación, y el reinicio reconstruye todo el trabajo futuro → Usa un índice de tiempo duradero, particiones lógicas y un horizonte acotado.
  • Marcar como enviado antes de escribir en la cola → Una falla en la cola omite silenciosamente una ocurrencia → Escribe una outbox transaccionalmente y transmite (relay) al menos una vez.
  • Afirmar que los bloqueos y los tiempos de espera de visibilidad garantizan exactamente una vez → El tiempo de espera y la confirmación perdida aún superponen intentos → Combina IDs de ocurrencia estables, tokens de esgrima (fencing) e idempotencia de negocio.
  • Reproducir cada ocurrencia histórica omitida después de la recuperación → Millones de tareas obsoletas crean una segunda interrupción → Establece retraso máximo, límites de recuperación acumulada y tasas por inquilino.
  • Sumar 24 horas en UTC para un cron diario local → La hora del reloj de pared se desvía a través del horario de verano → Conserva la zona IANA y calcula la próxima ocurrencia local.
  • Eliminar una fila para pausar o cancelar → El trabajo materializado o encolado aún puede ejecutarse y el historial de auditoría desaparece → Invalida por versión y vuelve a verificar el estado antes de los efectos secundarios.
  • Poner a cada inquilino en una cola FIFO → Un lote grande en el cambio de hora bloquea a otros inquilinos → Usa una programación justa consciente del inquilino y cuotas separadas de disparo y concurrencia.
  • Monitorear solo el éxito de la API → Una programación se puede crear con éxito y nunca dispararse a tiempo → Rastrea el retraso de programación, la acumulación pendiente, las ocurrencias omitidas y la reconciliación de estados.

Preguntas de seguimiento

Pregunta de seguimiento 1: ¿Qué pasa si un programador falla después de enviar a la cola pero antes de actualizar la outbox?

El relay envía el mismo occurrence_id nuevamente. El consumidor de la cola y el destino de negocio deduplican sobre ese ID, y la confirmación de la outbox es condicional. Se elige la duplicación sobre la omisión porque la base de datos y la cola no comparten una transacción. Un registro transaccional puede estrechar la ventana, pero los efectos externos aún requieren idempotencia.

Pregunta de seguimiento 2: ¿Cómo admitirías regiones activo-activo?

Asigna a cada programación una región principal y una generación incremental. Solo la región que posee la generación actual puede materializar ocurrencias, mientras que las colas de ejecución pueden enrutar a las regiones de destino. La conmutación por error avanza la generación antes de que la nueva región se reanude desde el cursor duradero. Una clave de ocurrencia globalmente única o una capa de agregación sigue siendo el límite final contra duplicados. Un plano de control de un solo escritor con conmutación por error regional es más simple cuando las escrituras entre regiones son innecesarias.

Pregunta de seguimiento 3: En el retraso de hora de otoño, la 01:30 local ocurre dos veces. ¿Cuántas ejecuciones deberían ocurrir?

No hay una respuesta universal; es parte del contrato de programación. Este diseño se ejecuta una vez al colapsar los dos candidatos UTC con la fecha local, la hora de reloj de pared y la zona. Una conciliación financiera que requiera ambos instantes físicos elegiría dos ocurrencias con diferentes valores de scheduled_for. La API debe previsualizar las ejecuciones futuras antes de guardar.

Pregunta de seguimiento 4: Una tarea tarda dos horas en ejecutarse pero se repite cada hora. ¿Qué sucede?

ALLOW las ejecuta concurrentemente. SKIP_IF_RUNNING marca atómicamente la nueva ocurrencia como SKIPPED_OVERLAP. Si el producto requiere esperar, agrega QUEUE_ONE y retén como máximo una ocurrencia pendiente. Reemplazar la ejecución antigua es seguro solo cuando los controladores admiten cancelación cooperativa, ya que los efectos pueden existir previamente.

Pregunta de seguimiento 5: El programador está caído durante un día. ¿Cómo te recuperas sin abrumar a los destinos?

Aplica SKIP, FIRE_ONCE o CATCH_UP acotado por programación, luego limita por inquilino y capacidad de destino. Reanuda el escaneo desde el next_run_at duradero en lugar de reemplazar el cursor con la hora actual. Rastrea la hora pendiente más antigua y la duración estimada de drenaje, y aísla el trabajo nuevo estricto de la capacidad de recuperación.

Pregunta de seguimiento 6: ¿Cómo demuestras que no hubo una omisión silenciosa?

Un reconciliador fuera de línea recalcula de forma independiente el conjunto de ocurrencias esperadas a partir de las versiones de programación y las reglas de tiempo, y luego lo compara con el conjunto de claves únicas de Occurrence. Cada elemento esperado debe haber tenido éxito, fallado, haber sido cancelado, omitido o estar ausente. Las métricas en línea detectan retrasos; la reconciliación detecta brechas donde el sistema no reportó ningún error porque nunca se creó una ocurrencia.

Fuentes públicas

Preguntas relacionadas

Herramienta de entrevista relacionada

Usa Resolver para una respuesta de diseño de sistemas

Aclara primero los requisitos y luego avanza a través de la escala, la arquitectura, la elección de componentes y las compensaciones (trade-offs).

Ver la herramienta