Your profiles fit in a hash table on one server. The table fills up and you add eight servers. Then one server stops hearing from the others.
Key points
Objectives
A single server key-value store is a hash table in memory. Access is fast. Fit is limited.
Two optimizations help a little. Compress data. Keep only hot data in memory and the rest on disk. Even with both, a large data set fills one server quickly.
That breaks (1). The fix is a distributed hash table. Split data across many servers. Replicate each key on N servers. The rest of this lesson is how to place, copy, read, and repair that data.
Mental picture: think of servers as stops on a clock face. Keys are riders. Each rider walks clockwise to the first stop that will take them.
Try it: grow the data set. Toggle the two small fixes. Watch one server fill even with both on.
one box, memory plus disk budget
raw data with no help
needs distributed hash table
Consistent hashing solves placement. Place servers on a hash ring. Hash each key onto the same ring. Store the key on the first server clockwise from its hash.
Example from the chapter: s0 through s7 sit on the ring. Key0 hashes between s0 and s1. It is stored on s1.
Two properties matter. Load spreads evenly. Add or remove of a server moves only nearby keys. Virtual nodes improve this. A big server gets more virtual spots on the ring.
Try it: slide the key hash around the clock. Press the key0 preset to land between s0 and s1. Toggle virtual nodes to see a big server claim extra spots.
Key hashes at 30 deg. First clockwise server is s1. Stored on s1.
Add or remove of a server moves only nearby keys because only the arc before the changed server reassigns. Virtual nodes split a big server into several arcs so load spreads evenly.
CAP theorem frames the tradeoff. Consistency means all clients see the same data. Availability means every request gets a response. Partition tolerance means the system works despite a split.
You can only pick two. In practice splits happen. So real stores pick CP or AP. CA cannot exist in a real distributed system.
Concrete split: replicas n1, n2, n3 hold the same key. Network drops n3. Writes to n1 cannot reach n3. Now choose. CP blocks writes to n1 and n2. That protects (2) but breaks (3). AP keeps accepting reads and writes. That protects (3) but breaks (2) until repair.
Try it: cut n3, then pick a side. This is a before and after contrast, not a sequence.
No split. n1, n2, n3 agree. Pick isolate n3 to force the CAP choice.
Replication means N copies on N distinct servers. Logic: hash the key. Walk clockwise. Pick the first N unique servers. Unique matters when virtual nodes put two spots from one physical server in a row.
Example: N equals 3. Key0 lands before s1. Copies go to s1, s2, s3. For reliability, put replicas in distinct data centers. Connect centers with high speed networks. This protects (5).
Reads and writes go through a coordinator. The coordinator is a node acting as proxy for the client. Define three numbers. N is replica count. W is write quorum. The write succeeds after W acknowledgements. R is read quorum. The read succeeds after R responses.
W equals 1 does not mean one copy. Copies still go to N nodes. It means the coordinator waits for one acknowledgement. That is fast. It risks stale data.
Tuning:
Latency versus consistency is the price. W or R greater than 1 waits for the slowest replica in the set. W or R equals 1 returns after any replica answers.
Consistency models in the chapter:
Try it: pick a preset, then click W and R dots to resize quorums. Overlap in the middle is what gives strong consistency.
Replication plus eventual consistency creates versions. Example: n1 and n2 both hold name john. Server 1 writes johnSanFrancisco as v1. Server 2 writes johnNewYork as v2 at the same time. Neither saw the other. There is no obvious winner.
Versioning treats each write as a new immutable version. A vector clock is a list of server and counter pairs per data item. Write D as D with entries like Sx comma 1.
Rules: if Si already has an entry, increment it. If not, add Si comma 1.
Walk from the chapter:
Detection rule: X is an ancestor of Y if every counter in X is less than or equal to the matching counter in Y. Example: D with s0 comma 1, s1 comma 1 is ancestor of D with s0 comma 1, s1 comma 2. No conflict.
X conflicts with Y if some counter in Y is less than the matching counter in X. Example: D with s0 comma 1, s1 comma 2 conflicts with D with s0 comma 2, s1 comma 1.
Weakness stated honestly: clients must run reconciliation. Clocks can grow. Fix is a length cap that drops oldest pairs. That can blur ancestry. Dynamo in production had not hit this limit per the chapter. Price is client complexity for high availability.
Try it: click a version to see its clock. Then test any two clocks for ancestor or conflict. Follow D1 to D5 end to end.
Select a version. D2 descends from D1. D3 and D4 are siblings from D2.
| version | clock | story |
|---|
Pick X and Y, then compare. Ancestor needs every counter in X at or below Y. Otherwise siblings conflict.
Failure is normal at scale. One report is not enough to mark a node down. All to all multicast works but wastes messages.
Gossip protocol is decentralized detection. Each node keeps a membership list with member ID and heartbeat counter. Each node increments its own counter. Each node sends heartbeats to random nodes. Those nodes spread them further. If a counter stops rising past a timeout, the member is marked offline and the news spreads.
Example: s0 sees member 2 heartbeat stuck at 8066 while others are past 10220. s0 shares that info. Others confirm. s2 is marked down.
Watch it live: counters tick by gossip. Kill s2 and watch s0 notice the stuck heartbeat, then recover it.
| member | heartbeat | status as seen by s0 |
|---|
All members ticking. One stuck report is not enough to mark a node down.
Repair depends on duration.
Temporary failure: strict quorum would block. Sloppy quorum relaxes placement. Use first W healthy servers for writes and first R healthy for reads on the ring. Skip offline nodes. Hinted handoff stores the write elsewhere and pushes it back later. Example: s2 is down. s3 takes writes for s2. When s2 returns, s3 hands data back. This protects (3) and later restores (2).
Try it: take s2 down, send a write for key0, then bring s2 back to hand off the hint.
Healthy ring. Writes go to s1, s2, s3 for N equals 3.
Permanent failure: use anti-entropy. Compare all data and update to newest. A Merkle tree makes this cheap. Each non leaf node holds the hash of its children. Compare roots first. If equal, data matches. If not, compare left child then right child. Sync only bad buckets.
Build with key space 1 to 12:
Sync cost tracks difference size, not total size. That is the win.
Try it: dirty one bucket, then compare from the root down. Only bad buckets sync.
Roots match. Data matches. Sync cost 0 buckets.
Data center outage: power or network or disaster can kill a site. Fix is copies across data centers. Users reach other centers. This protects (5) at the cost of cross center links.
Nodes share the same duties. No single point of failure. Client API is get with key and put with key and value.
Write path, based on Cassandra:
This breaks (2) briefly for speed, then converges. Commit log guards against memory loss.
Read path:
System view: clients hit any node. Coordinator routes by ring. Ring gives decentralization. Replication gives durability. Quorums give tunable correctness. Gossip plus handoff plus Merkle gives repair. Storage engine gives speed plus persistence.
Step it: this is the one stepper in the lesson. Play the full trace for key0 from client to disk and back. Press Next to start.
# Simulates N replicas, W/R quorum, and vector clock comparison.
# Run with: python3 kv_demo.py
replicas = {"s0": {}, "s1": {}, "s2": {}}
N = 3
def vector_dominates(a, b):
# Returns True if a >= b on all entries. Ancestor check.
keys = set(a) | set(b)
return all(a.get(k, 0) >= b.get(k, 0) for k in keys)
def clocks_conflict(a, b):
# Conflict if neither dominates. Sibling check.
return not vector_dominates(a, b) and not vector_dominates(b, a)
def quorum_write(key, value, clock, W, down=set()):
# Line that matters: only count healthy nodes, sloppy quorum style.
healthy = [s for s in replicas if s not in down]
if len(healthy) < W:
return False, "blocked: not enough healthy nodes"
for s in healthy[:W]:
replicas[s][key] = (value, dict(clock))
return True, f"acked by {healthy[:W]}"
def quorum_read(key, R, down=set()):
# Line that matters: caller must compare versions, eventual consistency.
healthy = [s for s in replicas if s not in down]
results = [replicas[s].get(key) for s in healthy[:R]]
return [r for r in results if r is not None]
if __name__ == "__main__":
ok, msg = quorum_write("name", "johnSF", {"Sx": 2, "Sy": 1}, W=2)
print("write:", ok, msg)
print("read R=2:", quorum_read("name", R=2))
# Illustrative clocks only to show conflict logic:
c1 = {"s0": 1, "s1": 2}
c2 = {"s0": 2, "s1": 1}
print("conflict?", clocks_conflict(c1, c2))
print("ancestor?", vector_dominates({"s0": 1, "s1": 2}, {"s0": 1, "s1": 1}))
# Expected output:
write: True acked by ['s0', 's1']
read R=2: [('johnSF', {'Sx': 2, 'Sy': 1})]
conflict? True
ancestor? True
1Predict: N equals 3, W equals 2, R equals 2. Partition isolates one replica. Can a write still succeed. Which requirement is stressed.
Two healthy nodes still form a W equals 2 quorum, so (3) holds on the majority side while the isolated copy lags on (2).
2Compare: CP block versus AP accept during n3 isolation. What breaks in each.
Blocking keeps one view of data but refuses answers. Accepting keeps answers but lets n3 diverge until repair.
3Choose: reads are 10 times writes and must be fast. Pick R and W with N equals 3 and defend the price.
R equals 1 minimizes read latency for a read heavy mix. The price is W equals 3 writes that wait for the slowest replica and stall if any replica is down, unless sloppy quorum takes the hint.