Notes
Modulo hashing
Start with the simple version. Three cache servers. To find where a key lives, hash it and take modulo three. Fast, evenly spread, and no state to keep: every client computes the same answer. This is fine, right up until you touch the number three.
server = hash(key) % N N = 3 user:4817 → 1 user:9 → 0 user:77 → 2 fast, even, stateless
Add one server
Add a fourth server. Modulo four gives different answers for about three out of four keys. Each of those keys is now looked up on a server that has never seen it. Three quarters of your cache turns into misses at once, and every miss falls through to the database. You added a server to reduce load and created a stampede. Same story when a server dies.
user:4817 → 1 (was 1) user:9 → 1 (was 0) user:77 → 1 (was 2) user:300 → 0 (was 0) about 3 of 4 keys moved every moved key = cache miss misses → the database
The goal
What do we want instead? When we add a fourth server it should take about a quarter of the keys, and the rest should stay exactly where they were. When a server dies, only its keys should move. That is the whole requirement, and a ring delivers it.
- add a server:
- only ~1/N of keys move
- remove a server:
- only its keys move
- everything else stays put
- so the cache stays warm
The ring
Picture the hash space as a circle. Zero to two to the thirty-two, or just zero to three hundred sixty degrees. Hash each server's name and place it on the ring at that angle. Four servers, four points.
ring = hash space 0 … 2^32
(think 0° … 360°)
hash("node-A") → 0°
hash("node-B") → 90°
hash("node-C") → 180°
hash("node-D") → 270° Place the key
To find a key, hash it onto the same ring. User forty-eight seventeen lands at sixty degrees. Walk clockwise until you hit a server: B, at ninety. B owns every key between A and itself. Each server owns the arc just before it.
hash("user:4817") → 60°
walk clockwise from 60°
first server you meet: B (90°)
so B owns everything
in (0°, 90°] Add a server
Now add a server. E hashes to forty-five degrees. It takes only the keys between A and itself; those used to belong to B. Our key at sixty still walks to B. C and D never noticed. Roughly one fifth of the keys moved, exactly what we asked for. Remove a server and the same thing happens in reverse: its arc goes to the next node clockwise, and no one else moves.
E now owns (0°, 45°] B now owns (45°, 90°] keys that moved: A → E arc only user:4817 at 60°: still B moved ≈ 1/N of keys C and D: untouched
The catch
One catch. Hashes are random, so the servers do not land evenly. One node might own half the ring, another five percent. Worse: when a node dies, its entire arc dumps onto a single neighbour, which may then die too. We need more points on the ring.
- hashes are random, so
- servers land unevenly
- one node might own 50%
- another 5%
- and when a node dies, its
- whole arc hits ONE neighbour
Virtual nodes
Virtual nodes. Instead of one point per server, give each one a hundred or two: hash node A hash one, node A hash two, and so on. Now every server owns many small arcs scattered round the ring, so the load averages out, and when a server dies its keys spread across all the others instead of crushing one. Bigger machine? Give it more virtual nodes.
each server gets many points:
hash("node-A#1") → 12°
hash("node-A#2") → 97°
hash("node-A#3") → 203°
… 100–200 per server
arcs average out
a dead node spreads its keys
across ALL the others Interview answer
The interview answer in four lines. Modulo hashing moves almost everything on resize. Consistent hashing puts servers and keys on a ring, so adding or removing a server moves one over N of the keys. Virtual nodes keep the ring balanced. And for fault tolerance, store each key on the next K nodes clockwise too, so a dead node's data is already on its neighbour.
- "How do you distribute keys
- across N cache servers?"
- 1. modulo breaks on resize
- 2. consistent hashing ring
- 3. virtual nodes for balance
- 4. replicate to next K nodes
- for fault tolerance
In the wild
Where it runs. Memcached and Redis client libraries, the ketama algorithm. Cassandra, Riak and DynamoDB partition data on exactly this ring with virtual nodes. Load balancers use it to keep a client on the same backend. CDNs use it to decide which edge server caches a URL. The same idea, everywhere.
- Memcached / Redis clients:
- ketama, cluster slots
- Cassandra, Riak, DynamoDB:
- partition ring + vnodes
- Load balancers:
- sticky routing by client
- CDNs: which edge caches a URL
How it fits
How does this fit with last episode? Sharding is the decision to split data by key. Consistent hashing is one way to map those keys to machines so the mapping survives machines coming and going. A sharded database might use a lookup table instead; a cache cluster almost always uses the ring.
- sharding (last episode):
- which rows live where
- key → shard, lookup or hash
- consistent hashing:
- HOW to map keys → nodes
- so the mapping survives
- nodes joining and leaving