Warmup Guide — Production Distributed Systems
Zero-to-expert primer for Phase 07: consensus (Raft) from first principles, hash-based partitioning as used by Kafka, and multi-module build engineering — the systems substrate beneath every Apache data project.
Table of Contents
- Chapter 1: Why Distributed — and What It Costs
- Chapter 2: The Consensus Problem
- Chapter 3: Raft — Leader Election from Zero
- Chapter 4: Raft — Log Replication and the Commit Rule
- Chapter 5: Partitioning — Spreading Data with Hashing
- Chapter 6: Consumer Assignment and Rebalancing
- Chapter 7: Multi-Module Builds — Engineering the Repo Like a System
- Lab Walkthrough Guidance
- Success Criteria
- Interview Q&A
- References
Chapter 1: Why Distributed — and What It Costs
From zero: one machine bounds you in capacity (disk/RAM), throughput (CPU/NIC), and availability (it will fail). Distribution buys past all three — and the price is that the network is now inside your system: messages are lost, delayed, reordered, and duplicated; clocks disagree; and — the deep one — you cannot distinguish a crashed node from a slow one (the FLP-flavored fact behind every timeout you'll ever tune).
Two design consequences run through everything in this phase:
- Redundancy requires agreement: copies that can diverge need a protocol to decide whose version is true → consensus (Ch. 2–4).
- Scale requires partitioning: data/work split across nodes needs a stable, balanced assignment function → hashing (Ch. 5–6).
Chapter 2: The Consensus Problem
The statement: N nodes must agree on a sequence of values (a replicated log) such that — despite crashes and partitions — agreed entries are never lost or reordered (safety), and the system keeps making progress whenever a majority is alive (liveness).
Why a majority (quorum)? Any two majorities of N intersect in ≥1 node. That single intersection node is the carrier of truth between configurations — it's why ⌈(N+1)/2⌉ acknowledgments make a decision durable, and why a 5-node cluster tolerates 2 failures. Memorize the intersection argument; every quorum system reduces to it.
Why it's hard: with asynchronous networks, any protocol can be stalled by adversarial timing (FLP impossibility) — so real systems buy liveness with randomized timeouts and buy safety unconditionally. Paxos solved it first and is notoriously hard to implement; Raft (2014) re-derived equivalent guarantees optimized for understandability — which is why etcd, Kafka's KRaft, and your Lab 01 all speak Raft.
Chapter 3: Raft — Leader Election from Zero
Raft decomposes consensus into leader election + log replication, with a strong leader through whom all writes flow.
The moving parts: every node is follower / candidate / leader; time divides into
terms (monotonic integers — Raft's logical clock). Each node persists
currentTerm, votedFor, and its log.
The election algorithm (exactly what Lab 01 implements and tests):
- Followers expect heartbeats from a leader. No heartbeat within a randomized
election timeout (e.g. 150–300 ms) → become candidate: increment term, vote for
self, send
RequestVote(term, lastLogIndex, lastLogTerm)to all. - A node grants at most one vote per term (persisted — survives crashes), and only to candidates whose log is at least as up-to-date (compare last term, then last index). This election restriction is what guarantees a new leader already holds every committed entry.
- Majority of votes → leader; begin heartbeats. Tie/split vote → timeout expires → next term, retry. Randomized timeouts make repeated splits exponentially unlikely.
- Any message bearing a higher term instantly demotes a leader/candidate to follower — stale leaders can exist briefly but can't do anything: replication requires follower acks, and followers reject lower terms.
Why no two leaders per term: one-vote-per-term + majority needed → two leaders in one term would require two disjoint majorities — impossible by the intersection argument. Two leaders in different terms: the older one's appends are rejected; it steps down on first contact with the higher term.
Chapter 4: Raft — Log Replication and the Commit Rule
(Lab 01 focuses on election; replication context so the design makes sense.)
- The leader appends client commands to its log and sends
AppendEntries(prevLogIndex, prevLogTerm, entries, leaderCommit). Followers reject if their log doesn't match atprevLogIndex/Term— the consistency check that forces divergent follower logs to back up and be overwritten to match the leader's. - An entry is committed once a majority holds it — but only for entries of the leader's current term (the Figure-8 subtlety: an old-term entry on a majority can still be overwritten; committing a current-term entry on top commits everything beneath it). This is the most-asked deep Raft question; have the scenario ready.
- KRaft (Kafka), etcd, and TiKV are production Rafts; reading any of their vote-handling code after this lab is the recommended follow-up.
Chapter 5: Partitioning — Spreading Data with Hashing
The problem: assign keys (or records) to N partitions, balanced and deterministic.
- The baseline (Kafka's default, Lab 02):
partition = murmur2(key) % N(sign-bit masked). Hash quality matters: a weak hash clusters real-world keys (sequential IDs, common prefixes) onto hot partitions, capping throughput at one broker. Murmur2 is fast, non-cryptographic, and statistically uniform — Lab 02 verifies uniformity with a chi-square test over realistic key distributions, the statistically honest way to evaluate a partitioner (eyeballing histograms lies). - Keyless records: round-robin balances perfectly but kills batching; Kafka's sticky partitioner fills one partition's batch then moves on — same long-run balance, far better throughput. Batching-vs-balance is the design tension to remember.
- The modulo trap: change N and
hash % Nreassigns ~everything — key→partition affinity (ordering! local state!) breaks. Consistent hashing (ring) or rendezvous hashing moves only ~1/N of keys — that's why caches and Cassandra-style stores use them. Kafka deliberately doesn't: partition count is meant to be fixed per topic, and ordering-per-key is the contract — increasing partitions on a keyed topic is a breaking change you must plan like a migration (Phase 11 thinking). - Skew is the practical killer: one celebrity key = one hot partition no algorithm fixes. Mitigations: composite keys (key+salt with consumer-side merge), local aggregation before the shuffle (Spark's map-side combine), or two-phase aggregation.
Chapter 6: Consumer Assignment and Rebalancing
The other half of partitioning: assigning partitions to consumers in a group.
- Range (per-topic contiguous blocks): aligns co-partitioned topics (needed for joins) but skews when consumers don't divide partitions evenly across many topics.
- Round-robin: even spread, breaks co-partition alignment.
- Sticky / cooperative-sticky (modern default): minimize movement on rebalance — every moved partition aborts in-flight work and cold-starts state. Cooperative rebalancing migrates incrementally instead of stop-the-world.
- The general principle (it recurs from Kafka to YARN to storage shards): assignment quality = balance × stability × locality, and the three trade against each other. Lab 02's RangeAssignor implementation makes the skew case concrete.
Chapter 7: Multi-Module Builds — Engineering the Repo Like a System
Why a build lab sits in a distributed-systems phase: Apache projects are multi-module systems, and their build graph is the first distributed system you touch (Phase 02 navigated it; here you construct one).
- Module = dependency-graph node:
queue-api(interfaces, no deps) ←queue-core(implementation) ←queue-app(wiring + integration tests). The discipline: api modules depend on nothing, implementations depend on api, applications depend on both — cycles are a design smell Maven simply refuses. - The reactor computes topological build order;
-pl module -am(with upstream) and-amd(with downstream) are the daily flags. Version alignment via the parent POM'sdependencyManagement— one place where versions live, modules inherit. - Why split at all: independent compilation/test cycles, enforceable layering
(api can't accidentally reach into impl), separate artifact publication (clients
depend on
queue-apionly — a compatibility boundary, Phase 04's surface made physical), and parallel CI. - CI shape (the lab's
ci.yml): build once, test per module, cache the local repo keyed on the POMs — the 80% of build-engineering value in 20 lines.
Lab Walkthrough Guidance
Order: Lab 01 (Raft) → Lab 02 (Kafka partitioning) → Lab 03 (multi-module).
- Lab 01: read
RaftNode's state transitions against Chapter 3's algorithm step by step; run the tests and make them fail informatively — comment out the one-vote-per-term persistence and watch which test catches the double-vote; do the same for the up-to-date check. Negative experiments are how the safety argument becomes yours. Extension: add the AppendEntries consistency check. - Lab 02: verify murmur2 against known vectors; run the chi-square uniformity test
on (a) random keys, (b) sequential IDs, (c) prefixed keys — compare against a naive
hashCode()partitioner and watch it fail (b). Then implement RangeAssignor and construct the skew case from Chapter 6. - Lab 03: build the three-module project; deliberately introduce an api→core
dependency and read Maven's cycle error; move a version into the parent's
dependencyManagement; run-pl queue-core -amand explain what rebuilt and why.
Success Criteria
You are ready for Phase 08 when you can, from memory:
- State the quorum-intersection argument and apply it (why 5 nodes tolerate 2 failures).
- Walk Raft's election including both safety mechanisms (one vote/term persisted; up-to-date restriction) and the role of randomized timeouts.
- Explain why a leader can't commit old-term entries by counting replicas (Figure 8).
- Defend murmur2-mod-N, name the modulo trap, and say when consistent hashing is and isn't the answer.
- Compare range/round-robin/sticky assignment on balance × stability × locality.
- Draw the api/core/app dependency discipline and what each boundary buys.
Interview Q&A
Q: Design leader election without Raft/ZK — what do you need at minimum? Re-derive the essentials: terms (a monotonic tiebreaker), persisted single vote per term (else double-voting under crashes), majority quorum (intersection = uniqueness), randomized timeouts (liveness under symmetry), and a rule that winners hold all committed state (else data loss on election) — at which point you've re-invented Raft's election, which is the answer: "the minimum is Raft's election, and here's why each piece is load-bearing."
Q: A Kafka topic shows one partition at 10× the traffic. Walk through it.
First distinguish key skew from partitioner bug: sample keys per partition. Celebrity
key → composite key (salt + consumer merge) or local pre-aggregation; bad hash (custom
partitioner, hashCode() on sequential IDs) → fix the hash, chi-square it; recently
increased partition count → old keys' affinity broke and the imbalance is transitional.
Never "add partitions" reflexively — on a keyed topic that's a breaking change to
ordering and state (the modulo trap).
Q: Why does Raft persist votedFor to disk before responding?
Crash-recovery safety: grant a vote, crash, restart without the record → grant again in
the same term → two leaders become possible, violating the intersection argument. The
fsync-before-reply on currentTerm/votedFor/log entries is the price of safety under
crashes; batching/group-commit is the performance answer, never skipping the write.
References
- Ongaro & Ousterhout, In Search of an Understandable Consensus Algorithm (Raft) — raft.github.io/raft.pdf; read §5 twice, Figure 8 until it's obvious
- The Raft visualization — watch before reading the paper
- Raft implementations list — read etcd's or KRaft's vote handling after Lab 01
- Kleppmann, Designing Data-Intensive Applications, ch. 6 (partitioning), ch. 9 (consistency/consensus)
- Kafka KIP-480: Sticky Partitioner and KIP-429: Incremental Cooperative Rebalancing
- Karger et al., Consistent Hashing and Random Trees (1997) — the ring's origin
- Maven: Guide to Working with Multiple Modules