Distributed Systems

Distributed Scheduling

Assigning work to nodes across a cluster — centralised schedulers, work stealing, distributed queues, fair-share algorithms, Kubernetes scheduler, distributed cron, and job deduplication.

⏱ 12 min read

What it is

Distributed scheduling is the problem of assigning units of work (tasks, jobs, containers) to compute resources (nodes, workers, threads) across a cluster, optimising for utilisation, fairness, latency, priority, and resource constraints. Schedulers range from simple round-robin work queues to sophisticated constraint-based bin-packers like the Kubernetes scheduler. The fundamental challenge is making assignment decisions with incomplete and stale information about resource availability while maintaining high throughput and low assignment latency.

Why it exists

As workloads outgrow a single machine, work must be distributed across multiple nodes. Without a scheduler, work distribution is manual — engineers assign tasks to specific machines, which doesn't scale and cannot adapt to failures. A distributed scheduler automates placement decisions, handles node failures by rescheduling work, enforces resource limits to prevent noisy neighbours, and optimises cluster utilisation. Modern schedulers also manage priorities, preemption, quotas, and affinity rules, making them central to multi-tenant cluster management.

When to use

  • Running heterogeneous workloads (batch, streaming, services) on shared infrastructure.
  • Auto-scaling where the number of workers changes dynamically based on queue depth or load.
  • Multi-tenant environments where fair resource allocation between teams or applications is required.
  • Recurring jobs (cron-like) that must run at scheduled intervals across a distributed cluster.

When not to use

  • Single-machine workloads where OS process scheduling suffices.
  • Purely event-driven architectures where a message queue directly drives workers — the queue IS the scheduler.
  • When work units are not decomposable or must run on specific dedicated hardware.

Typical architecture

Scheduling Architectures
════════════════════════════
Centralised Scheduler (Kubernetes-style):
  Jobs/Pods ──► Scheduler (single process, HA replica)
  Scheduler reads: node capacity, pod resource requests,
                   affinity/anti-affinity, taints/tolerations
  Scheduler writes: Pod.Spec.NodeName = "node-4"
  Kubelet on node-4 reads binding, starts container.

Work Queue (Pull-based):
  Producers ──► Queue (SQS, Kafka, Redis)
               ▲   ▲   ▲
              W1   W2   W3  (workers pull and process)
  Worker polls, claims task, processes, acks.
  Dead-letter queue for failed tasks after N retries.

Work Stealing:
  Each worker has a local deque of tasks.
  Worker with empty deque steals from the back
  of a random peer's deque.
  Minimises contention vs. shared central queue.

Distributed Cron (HA):
  Multiple cron-runner instances + leader election (etcd/ZK).
  Only elected leader fires scheduled jobs.
  On leader failure: follower acquires lock → resumes cron.
  Job deduplication: idempotency key or DB unique constraint
  prevents double-execution during leader failover window.

Pros and cons

Pros

  • Centralised schedulers make globally optimal placement decisions with full cluster visibility.
  • Work stealing minimises coordination overhead while maintaining high utilisation.
  • Queue-based scheduling decouples producers from consumers and provides natural backpressure.
  • Kubernetes scheduler extensibility (custom plugins, scheduler profiles) supports diverse workloads on shared infrastructure.

Cons

  • Centralised scheduler is a bottleneck at very high job submission rates — Google Borg addresses this with cell-based scheduling.
  • Optimistic concurrency in two-level schedulers (Mesos) can cause contention when multiple frameworks compete for the same resources.
  • Distributed cron requires careful deduplication logic — the failover window can cause double-execution without it.
  • Scheduler complexity grows with constraint types (affinity, topology spread, custom resources) creating scheduling latency for large clusters.

Implementation notes

Kubernetes scheduler architecture uses a two-phase approach: filtering removes nodes that don't satisfy hard constraints (insufficient CPU/memory, taints, affinity rules), then scoring ranks remaining nodes by soft preferences (spread, balanced resource utilisation, image locality). The highest-scoring node wins. The scheduler is a single elected leader but processes bindings asynchronously. Scheduling throughput is ~100 pods/sec for a typical cluster; at very large scale, multiple scheduler profiles or virtual clusters can parallelize scheduling.

Fair-share scheduling (used in Hadoop YARN, Spark, Kubernetes ResourceQuotas + LimitRanges) ensures each tenant receives a fair share of cluster resources proportional to their configured weight. Dominant Resource Fairness (DRF) extends this to multi-dimensional resources (CPU, memory, GPU) by maximising the minimum dominant resource share across tenants. Preemption allows high-priority workloads to evict lower-priority pods when cluster capacity is exhausted.

Distributed cron in a microservices environment is commonly implemented using leader election: all instances of a cron service compete for a distributed lock (etcd Lease, ZooKeeper ephemeral node); only the leader fires jobs. The key challenge is the failover window — if the leader fires a job and then loses leadership before recording completion, the new leader may fire the same job again. Solutions include: writing a "job started" record with an idempotency key before firing, checking for in-flight jobs on leader acquisition, and making jobs themselves idempotent.

Apache Nomad provides a two-level scheduler (plan queue + eval broker) with support for services, batch jobs, system jobs, and parameterised jobs across heterogeneous workloads including VMs, containers, and raw executables — without requiring all workloads to be containerised as Kubernetes does.

Common failure modes

  • Scheduler backlog: high job submission rates overwhelm the scheduler's processing capacity, causing scheduling latency spikes and job starvation.
  • Resource fragmentation: many nodes with small resource remnants that don't fit any pending job — the cluster appears full but is underutilised. Bin-packing heuristics and descheduler components address this.
  • Distributed cron double-execution: leader failover window fires the same job twice if deduplication is not implemented at the application level.
  • Priority inversion: high-priority jobs waiting for preemption of lower-priority jobs that are themselves waiting for external resources (e.g., database I/O), causing high-priority work to be effectively blocked.
  • Thundering herd on worker pool: all workers simultaneously becoming available and contending on a shared task queue, causing lock contention. Jitter and exponential backoff in polling reduces this.

Decision checklist

  • Are workloads containerised? If so, Kubernetes is the de facto standard scheduler.
  • Do you need to schedule non-container workloads (VMs, binaries)? Consider Nomad.
  • Is fair resource sharing between teams required? Configure ResourceQuotas and LimitRanges.
  • Do recurring jobs need exactly-once semantics? Implement idempotency keys and deduplication.
  • What is the expected job submission rate? Validate scheduler throughput against your peak rate.
  • Is work stealing appropriate for CPU-bound parallelism? Consider Golang, Java fork-join, or Rayon (Rust) thread pool implementations.

Example use cases

  • Kubernetes scheduling microservice Pods across a cluster with node affinity for data-locality-sensitive workloads.
  • Celery with Redis as a work queue for distributed background job processing in a Python web application.
  • Apache Spark using its internal task scheduler to assign map/reduce tasks to executor slots with data locality preferences.
  • Temporal.io as a distributed workflow orchestrator, scheduling activity tasks to workers with durable state and exactly-once execution guarantees.
  • Kubernetes CronJob + job deduplication for daily ETL pipelines that must not run twice even during scheduler failover.
  • Leader Election — distributed cron requires a leader election mechanism to ensure only one scheduler fires jobs at a given time.
  • Distributed Locking — job deduplication and exclusive job execution use distributed locks to prevent concurrent runs.
  • Idempotency — distributed jobs must be idempotent to be safely retried or executed at-least-once by a distributed scheduler.

Further reading