Failure and Time¶
What makes a system distributed¶
A program on one machine either works or crashes. A system spread over several machines has partial failure: one node is down, a link drops packets, a process is paused for seconds by GC or a VM migration — while everything else keeps running. Worse, a node cannot tell these apart. A request that gets no reply might mean:
- the request was lost;
- the remote node crashed before handling it;
- the remote node handled it, and the reply was lost;
- the remote node is just slow (overloaded, paused, swapping);
- the network is partitioned and the remote node is fine on its side.
The only tool a node has is a timeout, and a timeout cannot
distinguish "dead" from "slow". Every protocol on the following pages —
Raft, leases, linearizable reads — exists to stay correct despite this
ambiguity. The background is in
Replication,
CAP Theorem, and
Raft Consensus; the
runnable code for this section is in labs/raft, labs/raftkv,
labs/lincheck, labs/hlc, and labs/lease.
Failure models¶
| Model | What a faulty node may do | Typical assumption in |
|---|---|---|
| Crash-stop | Stop forever | Simple textbook protocols |
| Crash-recovery | Stop, lose volatile state, restart from stable storage | Raft, Paxos, databases — the realistic model for most infrastructure |
| Omission | Drop some messages | Covered by retries and idempotency |
| Byzantine | Anything, including lying | Blockchains, some aerospace systems; not assumed inside one organization's cluster |
Raft assumes crash-recovery: whatever a node promised before crashing (its vote, its log) must be on disk before the promise is sent. That is why the node saves to storage before a vote or an append is acknowledged (Raft: Leader Election).
Network model: messages can be lost, duplicated, delayed arbitrarily, and reordered, but not corrupted undetectably (checksums handle that). Protocols must be safe under all of these, and only need to make progress when the network behaves well enough for long enough (partial synchrony). Raft's safety never depends on timing; only its liveness (electing a leader) does.
Physical clocks can't order events¶
Each machine's clock drifts (quartz oscillators are off by tens of parts per million — roughly a second per day) and is corrected by NTP, which can step it backwards. Typical skew between machines in one datacenter is from under a millisecond to tens of milliseconds; across regions or with misconfigured NTP, far more.
Consequences:
- Timestamps from different machines don't give causal order. If A sends a message and B receives it, B's clock may say the receive happened before the send.
- "Last write wins" by timestamp loses writes: a write from a node with a slow clock is silently discarded in favour of an older write from a node with a fast clock.
- Time-based leases assume clocks advance at roughly the same rate; a paused process breaks that assumption completely (Leases and Fencing).
- Use the monotonic clock for durations. Go's
time.Now()carries a monotonic reading, andtime.Since/Subuse it, so durations measured within one process are immune to wall-clock steps. Wall time is only for display and cross-machine coordination.
Logical clocks¶
Lamport clocks: every node keeps a counter; increment it on each
event, attach it to messages, and on receive set it to max(local,
received) + 1. If A causally precedes B, then L(A) < L(B). The
converse does not hold, and the counter has no relation to real time.
Vector clocks: one counter per node; comparing two vectors tells whether events are ordered or concurrent. Used by Dynamo-style stores to detect conflicting writes. Cost: a vector per value, growing with the number of writers.
Hybrid logical clocks¶
An HLC timestamp is a pair (wall, logical): wall tracks the
largest physical time seen anywhere, logical breaks ties. It
preserves causality like a Lamport clock and stays within the clock
skew of real time, so it can be used where physical timestamps are
expected (MVCC versions, TTLs, "as of" queries). CockroachDB and
YugabyteDB use HLCs for transaction timestamps.
type Timestamp struct {
Wall int64 // nanoseconds: max physical time seen
Logical uint32 // breaks ties among events with the same Wall
}
// Now timestamps a local event or a message send.
func (c *Clock) Now() Timestamp {
c.mu.Lock()
defer c.mu.Unlock()
if pt := c.physical(); pt > c.last.Wall {
c.last = Timestamp{Wall: pt}
} else {
c.last.Logical++ // physical clock hasn't advanced (or went backwards)
}
return c.last
}
// Update timestamps the receipt of a message stamped remote; the result
// is greater than both remote and every earlier local timestamp.
func (c *Clock) Update(remote Timestamp) Timestamp {
c.mu.Lock()
defer c.mu.Unlock()
pt := c.physical()
wall := max(c.last.Wall, remote.Wall, pt)
var logical uint32
switch {
case wall == c.last.Wall && wall == remote.Wall:
logical = max(c.last.Logical, remote.Logical) + 1
case wall == c.last.Wall:
logical = c.last.Logical + 1
case wall == remote.Wall:
logical = remote.Logical + 1
default: // our physical clock is ahead of everything seen
logical = 0
}
c.last = Timestamp{Wall: wall, Logical: logical}
return c.last
}
Measured¶
Two nodes exchange 10,000 messages A→B (and replies B→A), with 1–5 ms of network delay. B's clock is behind A's by a fixed skew. For each message: is the receive stamped strictly after the send?
| Skew | Receive stamped at or before send — physical clocks | HLC | Max HLC lead over B's own clock |
|---|---|---|---|
| 3 ms | 5,928 (59.3%) | 0 | 2 ms |
| 100 ms | 10,000 (100%) | 0 | 99 ms |
With 3 ms of skew and 1–5 ms delays, every message delivered in 3 ms or less appears to arrive before it was sent — 3 out of 5 equally likely delays, so the expected rate is 60%, and 59.3% was measured. Skew comparable to network latency is entirely normal, so ordering events across machines by raw timestamps is wrong most of the time, not occasionally. HLC never violated causality; the price is that B's HLC runs ahead of its own clock by up to the skew. A clock that steps backwards (an NTP correction) is also handled: the test sets the physical clock from 1000 back to 500 and the next timestamp still increases.
HLC does not give you real-time ordering between unrelated events on different nodes. For that, systems either bound the skew and wait it out (Spanner's TrueTime "commit wait", CockroachDB's uncertainty intervals) or route everything through one sequencer (a Raft leader).
What this means for designs¶
| Need | Use |
|---|---|
| Durations, timeouts | Monotonic clock (time.Since) |
| Order of operations on one piece of state | A single leader assigning a sequence (Raft log index) |
| Causal order across nodes | Lamport clock or HLC carried on messages |
| Detect concurrent conflicting writes | Vector clocks / version vectors |
| Timestamps that look like real time and respect causality | HLC |
| Mutual exclusion across nodes | A lease plus fencing tokens (Leases and Fencing) |
| Linearizability | Consensus (Replicated Key-Value Store) |
Common pitfall¶
Resolving conflicts with wall-clock "last write wins". Two services
update a user's profile; the store keeps whichever write has the later
time.Now(). One host's clock is 2 seconds ahead after an NTP glitch.
For as long as the skew lasts, every update from the other host that
lands within 2 seconds of one from the fast host is silently discarded
— no error, no conflict, just data that never existed as far as
readers can tell. Nothing in testing shows it, because test machines
have synchronized clocks. Use a sequence assigned by a single owner
(a leader, a database's version column with compare-and-set), or
version vectors that surface the conflict, and keep wall-clock
timestamps for display.
Review questions¶
- List four reasons a request can get no reply, and explain why a timeout cannot distinguish them.
- What does the crash-recovery model require of a node before it sends a vote or acknowledges a write?
- Why did 59.3% of messages appear to arrive before they were sent with only 3 ms of skew?
- How does
Updateguarantee the receive timestamp is greater than the send timestamp, even when the receiver's clock is behind? - What can HLC not give you, and how do Spanner and CockroachDB deal with that?