Docs

Running a cluster

The same binary runs on a laptop and on a cluster. On a cluster it takes its role from the environment: one head, any number of workers, and drivers that connect to the head. Dista starts no machines or pods itself; Kubernetes, Orca or a script of yours does.

Locally

Runtime[...](workers=n) with nothing in the environment forks a head and n workers (default 4) and drives them from the same process, which is how tests and laptops run. The forked head is the same code a cluster runs.

Roles

EnvironmentThe process is
DISTA_ROLE=headruns the head on DISTA_PORT and never returns
DISTA_ROLE=workerserves the head at DISTA_HEAD=host:port and never returns
DISTA_HEAD=host:porta driver that submits to that head
neitherlocal mode: forks a head and workers workers, then drives them

Processes start in any order: workers and drivers retry the head for DISTA_CONNECT_TIMEOUT seconds. A worker gives its peers the address it reaches the head from, or DISTA_ADVERTISE. Each hello carries a hash of the program's tasks, so a cluster can't mix builds.

The head doesn't run your code, so it can be the generic one, dista head, from the dista command. It serves drivers and workers built from any program, several at once, and gives each driver's tasks and actors only to workers built from the same program. A head built into your program, DISTA_ROLE=head, serves only that program.

dista head --port 6390 &                            # the head, for any program
DISTA_ROLE=worker DISTA_HEAD=headhost:6390 ./app    # once per worker
DISTA_HEAD=headhost:6390 ./app                      # the driver

Kubernetes

The repository's deploy/ folder builds one image for every role, with your program at /opt/dista/bin/app, its entrypoint, and the command at /opt/dista/bin/dista. deploy/k8s/cluster.yaml runs the head as dista head in a StatefulSet behind the headless Service dista-head, and the workers as a Deployment, one per node, with DISTA_ADVERTISE set to the pod IP. A driver is a Job with DISTA_HEAD=dista-head:6390 (deploy/k8s/driver-job.yaml). tests/kind.sh runs it on a kind cluster of three nodes, in CI too.

Each pod has its own /dev/shm, so large results cross pods over sockets. Give the workers' /dev/shm room for the results they keep, and set DISTA_STORE_BYTES below its size limit: Kubernetes evicts a pod that passes it rather than failing the write. The AWS manifests keep 1 GiB per worker. A pod that's deleted gets SIGTERM, and its worker drains.

EC2

The repository's bench/cloud runs Dista on EC2 machines with k3s, for the performance numbers. Orca's ec2 runner starts a run's head and workers as instances of their own, and with orca register --workers MIN..MAX it scales them while the run goes; Orca's docs say how.

Draining a worker

A worker sent SIGTERM, as Kubernetes stops a pod, drains instead of dying. The head sends it no more tasks, it finishes what it was sent, the other workers adopt its results, and an actor it hosts moves, restored from a checkpoint. Then it exits 0: nothing fails or runs twice. DISTA_DRAIN_SECONDS (default 25) bounds the drain; past it the worker leaves anyway, and what it still holds is lost. A second SIGTERM ends it at once. The head itself exits at SIGTERM.

dista drain <ip:port> drains a worker by its address, as dista status shows it, for a provider that can't signal the worker's process. The head refuses to drain the last worker while tasks are queued or running.

Autoscaling

A job's workers can grow while tasks wait and shrink once they're idle. Dista says how many workers it wants and which ones may go, and whatever owns the machines starts and stops them. Three pieces, each usable alone:

  • Demand. dista status --json prints the head's tasks ready, waiting for inputs and running, its actors waiting for a worker, and per worker its address, node, tasks in flight, seconds idle, bytes held, actor and whether it's draining. /metrics has dista_tasks_ready, dista_tasks_waiting, dista_tasks_running and dista_workers_idle.
  • A policy. decide(demand, policy, now, pending, short_since), from dista.scale, says how many workers to start and which to drain, from Demand.parse(status_json) and a Policy. It has no clock or I/O of its own.
  • Draining by name. dista drain, above.

Policy holds min_workers (default 1, and it may be 0), max_workers (4), tasks_per_worker (0, the head's pipeline depth), up_after (5 s: tasks ready that long with no free slot start workers), idle_before_down (60 s) and max_step (4, the most workers started at once). Idle workers drain fewest held bytes first, never below min_workers; a worker hosting an actor drains only with a checkpoint store, and the last worker only once no task is left and it holds nothing. Workers asked for but not yet joined are passed in as pending, so a slow boot isn't asked for twice.

dista scale --once prints the decision for the head's current demand, a line each: want N, start N, drain ADDRESS per worker to drain, and short_since T to pass back next time. A provider loop runs it, starts workers and drains the ones it names; tests/autoscale.sh does that in shell, from 0 workers to 6 and back to 0. For a long-lived cluster on Kubernetes, a HorizontalPodAutoscaler on the head's dista_queue_depth works too.

The environment

Every variable Dista reads. The other pages say more about each: Failure and recovery for the timeouts and retries, Tuning for the store and the limits.

VariableWhat it setsDefault
DISTA_ROLEhead or worker; unset, the process is a driverdriver
DISTA_HEADhost:port of the head, for a driver or a worker in a cluster, and for dista's observer commandslocal mode
DISTA_PORTthe port a head built into the program listens on; 0 lets the OS pick, and the head prints it6390
DISTA_ADVERTISEthe address a worker gives peers and drivers to fetch its results fromthe address it reaches the head from
DISTA_NODEthe node a worker says it's on, for placementits host name
DISTA_CONNECT_TIMEOUTseconds a driver or worker keeps retrying the head, and an observer waits for it60
DISTA_PEER_CONNECT_TIMEOUTseconds, fractions allowed, a worker or driver waits to connect to a worker holding a result5
DISTA_KEEPALIVE_SECONDSseconds of silence before a connection is probed; it fails after 3 unanswered probes. 0 turns keepalive off10
DISTA_TASK_TIMEOUTseconds a task may run after it was sent before it failsno limit
DISTA_TASK_RETRIEShow many times a task whose worker died runs again3
DISTA_ORPHAN_SECONDSseconds a head keeps the results workers report for a driver that hasn't come back, after a head restart120
DISTA_DRAIN_SECONDShow long a draining worker may take before it leaves anyway25
DISTA_CHECKPOINT_URIwhere actor checkpoints go: a directory, file://, or s3://bucket/prefixnone: checkpoints fail
DISTA_STORE_DIRwhere a worker writes large results for its host to map/dev/shm on Linux, else $TMPDIR
DISTA_SHM0 keeps large results in the worker's memory insteadon
DISTA_STORE_BYTESbytes of results a worker keeps in the store before it spillsno limit
DISTA_SPILL_DIRwhere results past the store go, on disk$TMPDIR, else /tmp
DISTA_MAX_QUEUEDtasks queued or waiting at the head before remote blocks; 0, no limit100,000
DISTA_MAX_QUEUED_BYTESbytes of their arguments before remote blocks; 0, no limit1 GiB
DISTA_MAX_DRIVER_BUFFERbytes waiting for a driver that isn't reading before its tasks wait; 0, no limit256 MiB
DISTA_WARN_HELDobjects a driver holds before the head logs it, and again at each doubling; 0, never100,000
DISTA_METRICS_PORTthe port a head or worker serves Prometheus metrics on, at /metricsoff
DISTA_DEBUG1 logs each task's inputs as a worker assembles themoff