Reference · quick sheet
The Partitioning Playbook
Choosing a partition key, and recognising the outage when it was the wrong one.
Vocabulary
- Partitioning (sharding)
- Splitting data so each machine holds a slice. The only move that buys write capacity. Orthogonal to replication, which copies and buys reads.
- Partition key
- Hashed to pick a position on the node ring; that position and its neighbours hold the row. Rows sharing it are stored together and contiguously. Every query must supply it or become a scatter-gather that gets worse as the cluster grows. Changing it means rewriting the dataset.
- Clustering key
- The physical sort order of rows inside one partition — "an ordered dictionary". Makes a range of rows a contiguous read rather than a query plus a sort. One ordering per partition, fixed at write time; a different order is a different table.
- Snowflake id
- An id with its creation time in the high bits, so sorting by id is sorting by time. Lets one column serve as both identity and clustering order.
- Hot partition
- One partition receiving disproportionate traffic. Adding nodes does not help: the key still routes to the same replicas. You can have an idle cluster and an outage simultaneously.
- p99
- The latency the slowest 1% of requests see. The only metric that moves when a small number of requests fail badly — which is exactly the shape of a hot partition.
- Request coalescing
- Collapsing N concurrent identical requests into one: the first spawns the work, the rest subscribe to its result. Does the most work where load is heaviest.
- Tombstone
- A marker that a row was deleted, kept until compaction removes it. Until then it still has to be read in order to be skipped.
Discord's requirements list (2017) — a template worth stealing
- Linear scalability — no manual re-sharding later
- Automatic failover — "we love sleeping at night"
- Low maintenance — growth should mean adding nodes, nothing else
- Proven to work — "new technology, but not too new". Write your conservatism down.
- Predictable performance — alert on the 95th percentile, not the mean. And: no cache in front, the store must be fast enough by itself.
- Not a blob store — the write pattern must suit the engine
- Open source — "controlling our own destiny"; the line that made the 2023 migration possible
Choosing a key
- Name the access pattern first. The key must contain whatever every read filters by, or every read becomes a scatter-gather across the cluster.
- Bound the partition's size. Anything that grows forever needs a second component — usually a time bucket. Discord targeted <100 MB per partition.
- Now bound its traffic. Size and heat are different problems, and bucketing only fixes the first.
- Assume a power law. Any key derived from a human grouping — tenant, customer, account, channel, video, celebrity user — is skewed. The uniform key is usually the one nobody queries by.
- Ask what happens when the biggest one doubles. If the answer is "we add nodes", check that adding nodes actually helps. For a hot partition it does not.
A cache is not an answer to a percentile
Caching lifts the mean; cold reads still pay full price, so the tail barely moves. If a p95/p99 target needs a cache in front to be met, the store is wrong. Caches earn their keep when a hot working set is hit repeatedly — check first whether the OS page cache is already holding that set for free, as Discord found it was.
Spotting it
- Latency quoted as a range ("40–125 ms") rather than a number. Unpredictability is the symptom.
- p99 climbing while the mean is flat. Alert on percentiles; a mean over a skewed population reports health during a partial outage.
- One node hot, the rest idle. Not a capacity problem — a routing problem.
- Rituals with names ("the gossip dance"). A recurring manual remedy is a design defect with a nickname.
Fixes, and what they cost
| Fix | What it does | Price |
|---|---|---|
| Add a bucket to the key | Bounds partition size | Reads may span buckets; does nothing for heat |
| Request coalescing | Collapses concurrent identical reads into one | A service layer to own; only helps concurrent, identical reads |
| Consistent-hash routing to that layer | Makes coalescing possible by sending one key's traffic to one instance | Deliberately creates a hotspot upstream — worth it only if merging wins more |
| Change the runtime | Removes a whole failure class (e.g. JVM GC pauses → C++ shard-per-core) | A migration, and a new operational model to learn |
| Salt the key | Spreads one hot key across N partitions | Every read on every key fans out to N and merges — the whole workload pays to fix the tail. Discord considered the key correct and did not do this. |
| Accept it | Some skew is a property of the domain, not a schema mistake | Say so explicitly; mitigate the blast radius instead of promising a fix that cannot exist |
Discord, in numbers
| Cassandra | ScyllaDB | |
|---|---|---|
| Historical read p99 | 40–125 ms | 15 ms |
| Insert p99 | 5–70 ms | 5 ms |
| Nodes | 177 | 72 |
| Disk per node | ~4 TB | 9 TB |
| Backfill estimate | 3 months (Spark) → 9 days (bespoke Rust, 3.2M rows/sec) | |