The problem: exactly one, but not zero
Several Snip jobs must run in exactly one place at a time:
- The certificate renewer, which renews TLS certificates for custom domains before they expire. Two renewers at once could hit the certificate authority's rate limits or overwrite each other's work.
- The live-count aggregator for a viral link (07.09 Fan-out patterns): two would double the numbers.
- The weekly report scheduler (07.07 Delayed and scheduled jobs), if it isn't using the database trick.
Each runs on three servers for resilience. One should be active, the leader, and if it dies another should take over within seconds. Picking one is easy. Making sure there's never more than one, while never being stuck with none, is the hard part.
Leases
A lease is a lock with an expiry. The leader holds it only as long as it keeps renewing it. If it crashes, it stops renewing, the lease expires, and another instance can take it. No one has to detect the crash; time does it.
In etcd, a client is granted a lease with a time to live, attaches a key to it ("/leaders/cert-renewer = server-2"),
and keeps it alive with a stream of keep-alive messages. In etcd's words, a lease expires if the cluster doesn't
receive a keep-alive within the TTL, and when a lease expires or is revoked, all keys attached to it are deleted. The
election is then just: whoever creates the key first, with a transaction that only succeeds if the key doesn't exist,
is the leader. The others watch the key and try again when it disappears.
Because etcd runs Raft (08.06 Consensus and Raft, intuitively), only one client can hold the key at a time, as far as etcd is concerned.
Kubernetes works the same way. Its control-plane components, such as the controller manager and the scheduler, run several copies for high availability, but only one should be active; they coordinate through Lease objects, which are also how each node's kubelet sends its heartbeat.
4 messages. Use Next and Back to follow them in order.
Text version
- Renewer 1 (leader) → etcd: keep-alive (TTL 10 s) — The leader renews its lease every few seconds.
- Renewer 1 (leader) → etcd: keep-alive (fails) — Renewer 1 crashes. No more keep-alives.
- Note over etcd and Renewer 2: 10 s pass with no keep-alive: the lease expires and the leader key is deleted
- etcd → Renewer 2: watch: key deleted (reply) — Renewer 2 was watching the key, and sees it go.
- Renewer 2 → etcd: create key if absent — It creates the leader key with its own lease. It's the leader now.
The catch: a leader that doesn't know it's been replaced
Leases fix crashed leaders. They don't fix paused ones. Suppose Renewer 1 checks "I still hold the lease", then the process freezes for 15 seconds: a long garbage-collection pause, the virtual machine being suspended, a slow disk. Meanwhile its 10-second lease expires and Renewer 2 becomes leader. Then Renewer 1 wakes up and carries on with the work it had already decided to do, believing it's still leader. For a moment there are two.
Nothing in Renewer 1 can prevent this, because the pause can happen between any two lines of code, including between "check the lease" and "do the work". Ways to limit the damage:
- Stop early. A leader stops acting some margin before its lease expires (say at 7 seconds of a 10-second lease), measured on a monotonic clock, never wall-clock time (05.06 Clocks, skew and ordering IDs).
- Make the work safe to repeat. If both renewers request a certificate, the result should be one valid certificate.
- Let the resource reject the old leader. Each new leader gets a higher number, and whatever it writes to checks it. That's a fencing token, and it's the only complete answer (08.08 Distributed locks and fencing tokens).
Choosing the TTL
| Shorter lease (2 s) | Longer lease (30 s) |
|---|---|
| Fast takeover after a crash | Slow takeover: up to 30 s with no leader |
| Leaders lose it during brief network blips or pauses | Rides out short blips |
| Frequent keep-alives | Fewer keep-alives |
Snip uses 10 seconds for the certificate renewer, where a minute without a leader doesn't matter, and fencing tokens on its writes.
Seen in production
Chubby
Chubby provides coarse-grained locking and reliable, low-volume storage for loosely coupled distributed systems. The Google File System uses a Chubby lock to appoint its master server, and Bigtable uses Chubby to elect a master, to let the master discover the servers it controls, and to let clients find the master. Chubby favours locks held for long periods, not fine-grained ones, which impose far less load on the lock service.
What we foundA coarse-grained lock service, built on consensus, that other systems use to elect their masters.
The Chubby lock service for loosely-coupled distributed systems (Mike Burrows, OSDI 2006)static.googleusercontent.com ↗etcd
DocsLeases
etcd describes leases as a mechanism for detecting client liveness. The cluster grants a lease with a time to live; a lease expires if the cluster doesn't receive a keep-alive within that period, and when it expires or is revoked, all keys attached to it are deleted. Keep-alives are sent over a bidirectional stream.
What we foundA lease detects liveness: keys attached to it vanish when the holder stops renewing.
etcd API: Lease APIetcd.io ↗Snip's renewer checks its etcd lease, sees it's valid, then calls the certificate authority, which takes 20 seconds. The lease TTL is 10 seconds and keep-alives run on the same thread, which is blocked during the call. What can happen?
Recap
Interview questions
How would you make sure only one instance of a background job runs at a time across several servers?