P05 — Distributed Key-Value Store
Run it first. There is a companion page that builds this project's machinery as numbered, independently runnable blocks and then assembles them into one measured system: P05 hands-on — block by block (
handson/h*.py). Every number on it was produced by running the code. Read it alongside the milestones below.
Large · 143 hours · Weeks 55–67 · Stage 3 · Go
The hardest project in the journey, and the one most likely to overrun. The fault injector is scheduled first, before any distributed feature, specifically to bound it.
Table of Contents
- The Loop, Instantiated
- Why This Project Matters
- Prerequisites
- Duration and Size
- Central Technical Questions
- Architecture
- Showcase — Do This Before You Start
- Implementation Milestones
- Concepts To Study
- Primary-Source Readings
- Experiments
- Benchmarks and Metrics
- Correctness Tests
- Failure Tests
- Expected Difficulties
- Scope Boundaries
- Deliverables
- Exit Criteria
- Extension Ideas
- Connections
- References
The Loop, Instantiated
| Step | For this project |
|---|---|
| 1. Problem | Keep a key-value store available and correct when machines die, disks fail, networks partition, and messages arrive late, twice, or out of order |
| 2. Constraints | Asynchronous network — no bound on message delay. Nodes fail by crashing (no Byzantine). Clocks are not trustworthy |
| 3. Naive design | Yours. People invent: a primary that forwards to backups; consistent hashing with async replication; "just use a lock service" |
| 4. Predicted failure | Every naive design has a split-brain. Find yours on paper before you code it, and write down the exact interleaving |
| 5. Minimal implementation | Single-shard, three replicas, leader-based log replication |
| 6. Correctness | A linearizability checker over recorded histories |
| 7. Instrumentation | Per-RPC latency, replication lag, election counts, term numbers, log length |
| 8. Baseline | The single-node engine from P04. Distribution must justify its cost against it |
| 9. Bottleneck | Is throughput bound by fsync, by the network round trip, or by the leader's single-threaded apply loop? |
| 10. Hypothesis | Failure-detector timeout vs availability: there is an optimum, and both sides of it hurt |
| 11. Modification | Adaptive (phi-accrual) failure detection |
| 12. Experiment | Timeout sweep under injected delay distributions |
| 13. Failure analysis | Every linearizability violation gets a full interleaving diagram |
| 14. Report | Including at least one real bug the checker found in your own code |
Why This Project Matters
Almost everyone learns distributed systems as a vocabulary: quorum, consensus, CAP, eventual consistency. The vocabulary is not the skill. The skill is holding in your head the fact that you cannot distinguish a slow node from a dead one, and reasoning correctly about a system where every decision must be made without that information.
That single asymmetry generates almost everything else: why consensus needs a majority, why leases need clocks you distrust, why exactly-once delivery is impossible and exactly-once processing is not, why a failure detector's timeout is a liveness/availability trade with no correct answer.
You operate distributed systems today. This project is where you stop operating them and start being able to say what they are guaranteeing.
And the meta-lesson, which is why this is the hardest project: distributed bugs do not reproduce. The only defence is a deterministic harness that can replay a failing schedule. Building that harness before the system is the single most transferable habit in this track — it is the same reason P03 built the crash tester at milestone 2.
Prerequisites
- P04 complete — the storage engine is the per-node state
- Go: goroutines, channels,
select,context, the race detector - Comfort with the idea that a test that passes 1,000 times can still be wrong
Duration and Size
Large, 143 hours, 13 weeks.
| Tier | Contents | Hours |
|---|---|---|
| MVI | Fault injector, static partitioning by consistent hashing, single-shard leader-based replication with a replicated log, crash recovery, a linearizability checker. | 70 |
| Standard | + simplified Raft (elections, log replication, safety, persistence), membership changes, phi-accrual failure detection, idempotent retries, rebalancing, snapshots, multi-shard routing. | 143 |
| Extension | Read leases for local reads; or a Jepsen-style external test suite; or cross-shard transactions with two-phase commit. | +40–70 |
Central Technical Questions
- Why a majority? Not "because the paper says so" — derive why any two quorums must intersect and what breaks if they do not.
- What does a failure detector actually detect? Nothing about the remote node. It reports a property of your observations. Say precisely what.
- What is linearizability, and how would you check a history for it?
- Where does your system lose linearizability first? Every implementation has a weakest point. Name yours before the checker finds it.
- What happens during a leader change to in-flight writes? Trace one specific client's request across an election.
- Why is exactly-once delivery impossible and exactly-once effect achievable?
- What does the system do when the network is slow but nothing has failed? This is the common case in production and the one designs handle worst.
Architecture
Write your naive design first. Include the split-brain you predict.
client ──► router (any node) ──► shard owner
│
shard = hash(key) mod ring │ Raft group per shard, N=3
▼
┌──────── leader ────────┐
│ append to local log │
│ fsync │
│ AppendEntries ──► followers (parallel)
│ wait for majority ack │
│ commitIndex++ │
│ apply to P04 engine │
│ reply to client │
└────────────────────────┘
── fault injector sits BETWEEN every pair of nodes ──
drop · delay · duplicate · reorder · partition · pause process · corrupt disk
Why a majority — derived
A quorum system needs any two quorums to intersect, so that a decision made by one is visible to the next. With \(N\) replicas and quorum size \(Q\), two quorums of size \(Q\) intersect iff \(2Q > N\), i.e. \(Q \ge \lfloor N/2 \rfloor + 1\).
If \(2Q \le N\), two disjoint quorums can each make a decision without ever seeing the other's — that is split-brain, and it is not a bug in an implementation, it is an arithmetic consequence of the quorum size. Every real split-brain is this inequality being violated somewhere, often by an operator changing the replica count.
What majority replication buys, computed for \(N = 3\), independent node failure probability \(p\):
| p (per node) | single node available | 2-of-3 quorum available |
|---|---|---|
| 0.01 | 99.00% | 99.9702% |
| 0.05 | 95.00% | 99.2750% |
| 0.10 | 90.00% | 97.2000% |
At \(p=0.01\), majority replication turns 3.65 days of downtime a year into 2.6 hours. Note the assumption doing all the work: independence. Correlated failures — same rack, same power domain, same bad deploy, same poisoned request — collapse this entirely, and correlated failure is the normal case. Say that in your report.
The core asymmetry
You send a heartbeat. No reply arrives within \(T\). Three worlds are consistent with that observation:
- The node crashed.
- The node is alive and the network dropped the messages.
- The node is alive and just slow — GC pause, disk stall, CPU starvation.
You cannot distinguish them, ever, in an asynchronous network. This is the content of the FLP impossibility result: no deterministic consensus algorithm can guarantee termination in an asynchronous system with even one crash failure. Practical systems escape by adding a timing assumption — a failure detector that is allowed to be wrong.
So the timeout \(T\) is a trade with no correct value:
- \(T\) too small: false positives. Spurious elections, unnecessary failovers, and under load a feedback loop where slowness causes elections which cause more slowness.
- \(T\) too large: real failures go undetected. Availability gap equal to \(T\) on every genuine crash.
Phi-accrual detection (Hayashibara et al. 2004) replaces the binary with a suspicion level derived from the observed inter-arrival distribution, letting the application choose its threshold. Implementing it and measuring the false-positive/detection-time frontier is E4, and it is the best experiment in this project.
Build the fault injector first
Milestones 1–2, before any distributed feature. It must support:
| Fault | Why it is non-negotiable |
|---|---|
| Message drop (probabilistic and targeted) | The baseline network failure |
| Message delay (fixed, and drawn from a heavy-tailed distribution) | Slow is more common and more dangerous than dead |
| Message duplication | Retries make this certain, not hypothetical |
| Message reorder | TCP gives per-connection ordering only; multi-connection reorder is real |
| Network partition (arbitrary subsets, including asymmetric) | Asymmetric partitions — A hears B but B does not hear A — break more designs than symmetric ones |
| Process pause (SIGSTOP) | Simulates a GC pause; a paused leader that resumes still thinks it is leader |
| Process crash and restart | Tests persistence |
| Disk corruption and truncation | Reuses P03/P04's tooling |
| Clock skew and jumps | Ensures no correctness depends on wall time |
And it must be deterministic: seeded, with a recorded schedule that can be replayed exactly. A distributed bug you cannot replay is a distributed bug you cannot fix.
Showcase — Do This Before You Start
W3 · walkthroughs/w3_raft.py · ~60 minutes
A working miniature of this project: split-brain caused by a plurality rule and prevented by a majority one, plus the inequality brute-forced over every (N, Q).
cd walkthroughs && python3 w3_raft.py
It is 80-ish lines and it surfaces this project's central surprise in an evening rather than in week six. Run it before committing the weeks.
Implementation Milestones
| # | Milestone | Hours | Done when |
|---|---|---|---|
| 1 | Repo; in-process simulated network with deterministic seeded scheduling | 10 | Same seed → identical message order, provably |
| 2 | Fault injector: all nine faults above, replayable | 12 | A recorded failing schedule replays identically |
| 3 | Node skeleton: RPC layer, context deadlines, per-RPC metrics | 8 | Latency histogram per RPC type |
| 4 | Single-shard, static 3-replica leader-based log replication | 12 | Writes replicate; the leader is hard-coded |
| 5 | Linearizability checker over recorded histories (Wing–Gong style with pruning) | 12 | Detects a violation you injected deliberately |
| 6 | Raft: leader election, terms, votes, election timeouts | 14 | Exactly one leader per term under partition |
| 7 | Raft: log replication, matchIndex/nextIndex, commit rule | 14 | Log Matching and Leader Completeness asserted continuously |
| 8 | Raft: persistence (term, vote, log) + crash recovery | 8 | Survives crash-restart of any subset |
| 9 | Client sessions: idempotent retries via client id + sequence number | 8 | Duplicate delivery causes no duplicate effect |
| 10 | Phi-accrual failure detection | 8 | Detection-time/false-positive frontier measured |
| 11 | Membership changes (single-server at a time) | 10 | Add/remove a node with no loss of availability or safety |
| 12 | Snapshots + log compaction + install-snapshot to a lagging follower | 10 | A follower behind by a compacted prefix catches up |
| 13 | Consistent hashing, multi-shard routing, rebalancing | 12 | Ownership moves without dropping writes |
| 14 | Experiments + report | 5 | All rows filled |
Concepts To Study
- The asynchronous model and why it is the right default
- FLP impossibility: no deterministic async consensus with one crash failure
- CAP, stated precisely: during a partition, choose between linearizability and availability. Note that CAP says nothing about the non-partitioned case, which is where PACELC comes in
- Linearizability vs sequential consistency vs serializability — three different properties routinely conflated
- Quorum intersection and the derivation above
- Raft: terms, elections, log matching, leader completeness, state-machine safety
- Why Raft's commit rule excludes prior-term entries — the subtlest safety point in the paper (§5.4.2). Understand it or your implementation will have a rare, real bug
- Failure detectors: completeness and accuracy; phi-accrual
- Idempotency: client sessions, dedup tables, and their unbounded growth problem
- Leases and clocks: why a lease needs a bounded clock drift assumption
- Consistent hashing: virtual nodes and why naive consistent hashing has bad balance
- Split-brain and fencing tokens
- Read paths: read-from-leader, ReadIndex, lease reads — three different correctness/latency points
Primary-Source Readings
Budget: 20 hours, the largest in the journey.
| Reading | Why | Hours |
|---|---|---|
| Ongaro, D., Ousterhout, J. In Search of an Understandable Consensus Algorithm (Extended). USENIX ATC 2014 | Read the extended version. §5.4.2 twice | 5 |
| Lamport, L. Time, Clocks, and the Ordering of Events in a Distributed System. CACM 21(7), 1978 | The foundation. Happens-before | 2 |
| Fischer, Lynch, Paterson. Impossibility of Distributed Consensus with One Faulty Process. JACM 32(2), 1985 | Read the theorem and the intuition; the proof is optional | 2 |
| Herlihy, M., Wing, J. Linearizability: A Correctness Condition for Concurrent Objects. ACM TOPLAS 12(3), 1990 | The definition your checker implements | 2 |
| Gilbert, S., Lynch, N. Brewer's Conjecture and the Feasibility of Consistent, Available, Partition-Tolerant Web Services. SIGACT News 33(2), 2002 | CAP, stated as a theorem rather than a slogan | 1.5 |
| DeCandia, G. et al. Dynamo: Amazon's Highly Available Key-value Store. SOSP 2007 | The AP design point; vector clocks, hinted handoff, read repair | 2.5 |
| Corbett, J. C. et al. Spanner: Google's Globally-Distributed Database. OSDI 2012 | What buying a bounded clock lets you do | 2 |
| Hayashibara, N. et al. The φ Accrual Failure Detector. SRDS 2004 | E4 | 1.5 |
| Kingsbury, K. Jepsen analyses (pick three real systems) | What violations look like in shipped software | 1.5 |
Experiments
| # | Experiment | Sweep | Predict first |
|---|---|---|---|
| E1 | Replication factor | N ∈ {1,3,5,7} | Write latency vs availability; predict the latency slope |
| E2 | Write latency decomposition | fsync / network RTT / apply | Which dominates? Predict before measuring |
| E3 | Failure-detector timeout | T ∈ {50 ms … 5 s} under injected delay | The optimum, and the shape either side |
| E4 | Fixed vs phi-accrual detection | under heavy-tailed delay | Predict the false-positive reduction at equal detection time |
| E5 | Leader failure | kill leader under load | Availability gap; predict it ≈ election timeout + one RTT |
| E6 | Network partition | symmetric and asymmetric | Minority must reject writes. Assert it, do not hope |
| E7 | Replication lag | vs write rate, vs follower slowness | Where does a slow follower start hurting the leader? |
| E8 | Message loss rate | 0–30% | Throughput degradation curve; predict its shape |
| E9 | Duplicate + reorder | 10% each | Zero effect on state, if sessions are right |
| E10 | Rebalancing | move 1 shard under load | Availability and latency during the move |
| E11 | Snapshot + catch-up | follower behind by 10⁴–10⁷ entries | Catch-up time; where does install-snapshot beat log replay? |
| E12 | Read strategies | leader-read vs ReadIndex vs lease | Latency vs staleness — a three-point frontier |
| E13 | Clock skew | ±5 s | No effect. If there is one, you have a bug |
| E14 | Throughput vs shard count | 1–16 shards | Where does routing overhead cancel the parallelism? |
E6 with asymmetric partitions is the one that finds bugs. Symmetric partitions are easy to reason about. A node that can send but not receive causes a leader that believes it is still leading, and it breaks designs that symmetric tests pass.
Benchmarks and Metrics
| Metric | Notes |
|---|---|
| Write throughput and p50/p95/p99 | Under no-fault and under each injected fault |
| Read throughput and staleness | By read strategy |
| Availability gap on leader failure | Seconds with no successful writes |
| Time to detect a failure | Distribution, not mean |
| False-positive failure detections | Per hour, under load |
| Replication lag | p50/p99, in entries and in seconds |
| Elections per hour | A stable cluster should have ~0. Nonzero is a signal |
| Rebalance time and impact | Duration and p99 inflation during |
| Linearizability violations | Must be zero. Any nonzero is a stop-work bug |
| Recovery time | Crash-restart to serving |
| Bytes on the wire per client write | Amplification, distributed edition |
Correctness Tests
- Linearizability checking on every test run. Record a history of invoke/return/value events, check offline. Continuously, not once.
- Election safety: at most one leader per term. Assert globally in the simulator.
- Log Matching: if two logs contain an entry with the same index and term, all preceding entries are identical. Assert after every AppendEntries.
- Leader Completeness: a committed entry is present in the log of every future leader. This is the property §5.4.2 protects.
- State Machine Safety: no two nodes apply different commands at the same index.
- Durability: an acknowledged write survives crash-restart of a majority.
- Idempotency: replaying a client request produces one effect.
- Membership safety: no configuration change creates two disjoint majorities.
- Determinism: same seed → identical execution. Without this, nothing else is testable.
- Model-based test: whole cluster vs a single
map, under faults.
Failure Tests
Every one of these runs in CI, seeded, replayable.
| Injection | Required behaviour |
|---|---|
| Kill leader mid-write | No lost acknowledged write; new leader elected |
| Kill a majority | Cluster unavailable for writes; no data loss on recovery |
| Symmetric partition | Majority side serves; minority rejects |
| Asymmetric partition | No split-brain; the isolated leader steps down |
| SIGSTOP the leader for 2× the election timeout, then resume | Old leader must discover the new term and step down |
| Disk corruption on one follower | Detected; that follower recovers via snapshot |
| Truncate a follower's log | Repaired by the leader |
| Duplicate every message | No duplicate effects |
| Reorder all messages | Correct, possibly slower |
| Delay 10% of messages by 10 s | No spurious failover with phi-accrual |
| Clock jump +1 hour on one node | No effect |
| Restart every node in sequence | Cluster stays available throughout |
| Slow disk on the leader (10× fsync latency) | Throughput degrades; correctness does not |
Expected Difficulties
- This project will take longer than you plan. It is the one with the highest overrun risk in the journey. Mitigations, in order: the deterministic simulator (milestone 1) so bugs replay; the linearizability checker (milestone 5) so bugs are found rather than shipped; a hard scope cut at week 11 that drops milestones 11–13 and ships single-shard.
- Raft §5.4.2 will bite you. A leader may not commit an entry from a previous term by counting replicas — it must commit an entry from its own term first. The bug this prevents appears in perhaps one in 10⁵ runs and destroys linearizability. Write the test that constructs the interleaving deliberately.
- Real-network testing wastes weeks. Use the in-process simulator for everything. Run on real sockets once, at the end, to confirm nothing depended on the simulation.
- The linearizability checker is exponential in the worst case. Wing–Gong with pruning is fine for histories of a few thousand ops; keep them short.
- Debugging output is your main tool and will drown you. Structured logs with node id, term, index, and a monotonic sequence number; a script that renders a history as a space-time diagram. Build it in milestone 3.
- Go's race detector will find real bugs. Run every test under
-racefrom day one.
Scope Boundaries
In scope: crash-stop failures, asynchronous network, single-key linearizable operations, leader-based replication, static then dynamic membership, snapshots, rebalancing.
Out of scope: Byzantine faults; multi-key transactions (extension only); geo-replication and clock-bounded designs à la Spanner (read the paper, do not build it); a production RPC framework — hand-rolled is better here; an admin UI; TLS and authentication; performance work beyond what the experiments require.
Deliverables
distkv/— Go module, in-process simulator and a real-socket modefaultinjector/— the standalone, reusable artifact. This is the most portfolio-valuable thing you will build in Stage 3linchecker/— the linearizability checker, also standaloneREPORT.mdincluding at least one real bug the checker found in your own code, with the interleaving diagram- Notebook entries for E4, E6, E12
- A space-time diagram renderer for recorded histories
Exit Criteria
- Linearizability checker reports zero violations across ≥1,000 seeded fault runs
- All thirteen failure tests pass, including the asymmetric partition and the SIGSTOP-resume
- Leader election works under partition with exactly one leader per term, asserted
- E5 measured: availability gap on leader failure, compared against your prediction
- E4 measured: phi-accrual vs fixed timeout, frontier plotted
- Snapshots work: a follower behind by a compacted prefix catches up
- At least one real bug found by the checker, documented with its interleaving
- Same-seed determinism verified — every failing run replays
-
REPORT.mdwritten with a falsified prediction
Extension Ideas
- Read leases: local reads at the cost of a bounded-clock assumption. Measure the latency win and state the assumption you bought it with.
- Jepsen-style external testing against your own system.
- Two-phase commit across shards — and a measurement of how much availability it costs, which is the honest reason distributed transactions are avoided.
- Compare with an AP design: implement Dynamo-style quorum replication with vector clocks alongside, and measure the availability difference during partition. This is CAP as an experiment rather than a slogan, and it is the strongest extension here.
Connections
Backward: P04 is the per-node engine; its WAL and snapshots are reused. P03's crash harness generalises into the fault injector.
Forward:
- → P06 (MapReduce): the fault injector, membership, and failure detector are reused directly. The coordinator's fault tolerance is a simpler version of this problem
- → P07 (Streaming): the replicated log is the event log; checkpointing is snapshotting
- → P12 (Kernel): scheduling, timers, and the cost of a context switch are the micro-scale version of the same latency questions
- → P15: the storage and ingestion substrate
References
- Ongaro, D., Ousterhout, J. In Search of an Understandable Consensus Algorithm. USENIX ATC 2014 (extended version, Stanford).
- Lamport, L. Time, Clocks, and the Ordering of Events in a Distributed System. CACM 21(7), 1978.
- Lamport, L. The Part-Time Parliament. ACM TOCS 16(2), 1998. And Paxos Made Simple, 2001.
- Fischer, M. J., Lynch, N. A., Paterson, M. S. Impossibility of Distributed Consensus with One Faulty Process. JACM 32(2), 1985.
- Herlihy, M. P., Wing, J. M. Linearizability: A Correctness Condition for Concurrent Objects. ACM TOPLAS 12(3), 1990.
- Gilbert, S., Lynch, N. Brewer's Conjecture and the Feasibility of Consistent, Available, Partition-Tolerant Web Services. SIGACT News 33(2), 2002.
- Abadi, D. Consistency Tradeoffs in Modern Distributed Database System Design. IEEE Computer 45(2), 2012. PACELC.
- DeCandia, G. et al. Dynamo: Amazon's Highly Available Key-value Store. SOSP 2007.
- Corbett, J. C. et al. Spanner: Google's Globally-Distributed Database. OSDI 2012.
- Hayashibara, N., Défago, X., Yared, R., Katayama, T. The φ Accrual Failure Detector. SRDS 2004.
- Chandra, T. D., Toueg, S. Unreliable Failure Detectors for Reliable Distributed Systems. JACM 43(2), 1996.
- Karger, D. et al. Consistent Hashing and Random Trees. STOC 1997.
- Kingsbury, K. Jepsen. jepsen.io — the analyses, not just the tool.
- Alvaro, P., Rosen, J., Hellerstein, J. M. Lineage-driven Fault Injection. SIGMOD 2015. A smarter way to choose which faults to inject.