Splitting data across nodes without splitting correctness: partition strategies, consistent hashing and why mod-N is a trap, rebalancing, the hot-partition (celebrity) problem, and what sharding does to your queries.
~3h study~2h exercise2 interactive simsFast track ~80 min
Fast track — sharded before?
Compresses to ~80 min. Strategy definitions collapse; consistent hashing mechanics, rebalancing, and hot partitions stay mandatory — hot keys during live events are your platform's specific nightmare.
§04 hot partitions + sim, in full · 20 min · MANDATORY
§05–§06 takeaways; Exercise step 2 · 25 min
STEP 01
Partitioning strategies
SKIM
Fast-track takeawayRange: sorted key ranges per shard — efficient range scans, but sequential keys (timestamps!) write to one shard, creating a rolling hot spot. Hash: uniform spread, range queries destroyed. Directory: lookup service maps key→shard — maximal flexibility, one more thing to keep consistent and available. Secondary indexes: local (per-shard; reads scatter-gather every shard) vs global (index itself sharded by indexed value; reads targeted, writes now touch multiple shards). The partition key decision is the schema decision — everything else is downstream of it.
Strategy
Wins
Loses
Used by
Range
range scans, sorted iteration
hot spots on sequential keys (time!)
HBase, Bigtable, Mongo range
Hash
uniform distribution
range queries → scatter-gather
Cassandra, DynamoDB, Redis Cluster
Directory
arbitrary placement, easy migration
the directory is a dependency + consistency burden
Vitess-style, custom estates
Secondary indexes under partitioning: local indexes make every index read a fan-out to all shards (tail amplification, Module 02); global indexes make reads targeted but turn one logical write into multi-shard writes. Pick per access pattern.
STEP 02
Consistent hashing — why mod-N is a trap
MANDATORY
shard = hash(key) % N looks obviously right and fails catastrophically at the worst moment: change N and almost every key remaps — a self-inflicted cache avalanche or a full-cluster data migration, triggered by adding capacity. Consistent hashing fixes it: nodes and keys hash onto a ring; a key belongs to the next node clockwise; adding a node steals only ~1/N of the keyspace, from its neighbors alone. Virtual nodes (100–256 tokens per physical node) smooth the statistical imbalance and let heterogeneous hardware take proportional load.
Resharding cost · 100 keys, add a 5th node
KEYS REMAPPED —
OF 100 —
Each square is a key, colored by owning node (4 nodes). Add a 5th node with each scheme and watch how many keys move (highlighted).
The number to remembermod-N remaps ~N/(N+1) of all keys (~80% going 4→5 nodes). Consistent hashing remaps ~1/(N+1) (~20%). At cluster scale that's the difference between a non-event and an outage. Every serious system — Cassandra, DynamoDB, Redis Cluster (via its slot layer), Memcached clients — uses ring-or-slots for exactly this reason.
STEP 03
Rebalancing in real systems
MANDATORY
Production systems mostly don't hash straight to nodes — they hash to a fixed pool of partitions, then map partitions → nodes. Rebalancing becomes moving whole partitions, and the key→partition mapping never changes:
Redis Cluster: 16,384 hash slots; CRC16(key) % 16384 → slot; slots assigned to nodes; resharding = migrating slots live. Hash tags{user42}:profile / {user42}:sessions force related keys into one slot — the prerequisite for multi-key ops and Lua across them.
Kafka: partitions are the parallelism unit; consumers rebalance partition ownership. Partition count is quasi-permanent — changing it breaks key→partition affinity and thus per-key ordering, so over-provision partitions on day one.
Dynamic splitting (HBase/Mongo/Dynamo): partitions split when hot or large; handles growth automatically, but a split of a hot partition happens during the heat.
Rules regardless of system: rebalancing must be throttled (moving data competes with serving it — untuned rebalancing is a self-inflicted incident), resumable, and never automatic on mere node-unreachability — a GC pause misread as node death triggers a data-movement storm that causes the real outage.
STEP 04
Hot partitions — the celebrity problem
MANDATORY
Perfect key distribution ≠ perfect load distribution: keys aren't equally popular. One live-final manifest, one trending series — Zipfian reality means one shard melts while others idle. Salting spreads a hot key across shards at the cost of read-side aggregation:
Hot shard · Zipfian load, key "match:final" on shard 3
Shard 3 at ~62% of total load — it saturates while five shards nap. Salt the hot key (match:final:{0..7}) and watch it flatten.
Write salting: append rand(0,K) to the hot key; reads fan to all K and merge. Trades one hot shard for K-way read amplification — apply only to identified hot keys.
Read replication: copy the hot value to K suffixed keys; read one at random. For read-hot/write-rare data (manifests) this is usually the right first move.
The layered answer (from Module 01): the best fix for a read-hot key is not sharding at all — L1/CDN absorbs it before the shard sees it. Salting is for write-hot or uncacheable keys.
Detection first: per-key traffic metrics (Redis --hotkeys, per-slot QPS). You can't salt what you haven't spotted, and the World Cup won't wait while you add instrumentation.
Staff expectationChoosing the partition key against a written list of the top access patterns and known skew, and pre-declaring the hot-event playbook: which keys get pre-salted, what per-slot QPS triggers it, who owns the read-merge. "user_id is a natural key" is a mid-level answer; "user_id, except device-session data co-located via hash tags, plus a pre-salting playbook for event-day content keys" is the staff one.
STEP 05
What sharding does to queries
SKIM
Fast-track takeawaySingle-key ops stay O(1); everything else gets a tax. Cross-shard queries → scatter-gather (latency = slowest shard: tail amplification again); cross-shard JOINs → don't — denormalize at write time or co-locate by shared partition key; cross-shard transactions → 2PC is slow and fragile, the microservices answer is the Saga (Week 3); cross-shard uniqueness → dedicated keyspace or ID-generation scheme (Snowflake IDs). Design rule: choose the partition key so your top-N queries are single-shard, and treat every scatter-gather as a design smell to justify.
Scatter-gather: query all shards, merge. p99 = max over shards → Module 02's 1−(1−p)ᴺ applies to your own database.
Joins: co-locate rows that join (same partition key) or denormalize at write time. Cross-shard joins in the request path are how sharded systems die slowly.
Transactions: single-shard ACID survives; cross-shard needs 2PC (blocking, coordinator SPOF) or Sagas with compensation — full module in Week 3.
IDs: auto-increment dies with sharding; use Snowflake-style (timestamp | node | sequence) or UUIDv7 for index-friendly ordering.
STEP 06
Code: a ring you can reason about
SKIM
Fast-track takeawayThe whole algorithm is a TreeMap and two methods: hash each node to many ring positions (vnodes), and lookup = ceilingEntry(hash(key)) with wraparound. Skim it once so "consistent hashing" is 25 lines in your head, not a paper. Note the Redis hash-tag snippet below it — co-location you'll actually use this quarter.
public classConsistentHashRing<N> {
private finalTreeMap<Long, N> ring = newTreeMap<>();
private final int vnodes; // 100–256 in practicepublic voidaddNode(N node) {
for (int v = 0; v < vnodes; v++)
ring.put(hash(node + "#" + v), node); // murmur3/xxHash — NOT hashCode()
}
public voidremoveNode(N node) {
for (int v = 0; v < vnodes; v++) ring.remove(hash(node + "#" + v));
}
publicNnodeFor(String key) {
var e = ring.ceilingEntry(hash(key)); // next node clockwise…return (e != null ? e : ring.firstEntry()).getValue(); // …wrapping the ring
}
}
// Redis Cluster co-location: hash tags force one slot → multi-key ops legal
redis.opsForValue().multiSet(Map.of(
"{user:42}:profile", profileJson,
"{user:42}:sessions", sessionsJson)); // same slot: only {user:42} is hashed
STEP 07
Exercise: measure the remap, melt a shard
MANDATORY
Fast trackStep 2 only (~40 min): building the ring and measuring 80% vs 20% remap with your own code makes the argument permanent.
1
Build the ring. Implement ConsistentHashRing with pluggable vnode count, using Guava's murmur3. Unit-test distribution: 1M keys over 5 nodes should land within ±5% of 200k each at 200 vnodes.
2
Measure the remap. Assign 1M keys via mod-5, then mod-6: count movers. Repeat with the ring 5→6 nodes. Produce the two percentages (~83% vs ~17%) from your own run, and additionally chart imbalance vs vnodes ∈ {1, 10, 100, 500}.
3
Melt and salt. Local 3-node Redis Cluster (docker). Zipfian load-test with one dominant key; watch per-node QPS. Apply read-replication salting ×8 in your client; show per-node QPS flatten. Keep before/after screenshots.
4
Design on paper. Shard your watch-history store: pick a partition key against these queries — (a) resume point by (user, content), (b) continue-watching rail by user, (c) analytics "views per content per day". State which query becomes scatter-gather and what you denormalize to avoid it.
Self-check
Why does adding a node to a mod-N cluster cause a cache avalanche?
~N/(N+1) of keys change owner instantly, so nearly every lookup goes to a node that's never seen that key — cluster-wide hit ratio collapses toward zero at once, and the full miss load lands on the origin: Module 01's avalanche, triggered by adding capacity.
What do vnodes buy beyond smoothing randomness?
(1) A leaving node's load spreads across many successors instead of one neighbor eating it all. (2) Heterogeneous hardware: give a 2× machine 2× vnodes. (3) Rebalancing granularity — move small slices, throttled, instead of a monolithic range.
Why is Kafka partition count effectively permanent for keyed topics?
Key→partition is hash(key) % partitions. Changing the count remaps keys to different partitions, so a key's events straddle two partitions across the change — per-key ordering, the property keyed topics exist for, silently breaks. Hence day-one over-provisioning.
Timestamp-prefixed keys on range partitioning — what happens and what are two fixes?
All current writes hit the shard owning "now" — a rolling hot spot that moves but never spreads. Fixes: prefix with a hash/entity component (hash(sensor)‖ts) to spread writes while keeping per-entity time locality; or bucket time into salted windows and merge on read. Pure timestamp keys are the classic range-sharding own-goal.