Part 5 · Distribution
Multitenancy, Request Routing, and Sharding Operations
Lesson 11 gave you the mechanics of splitting data by key: range vs hash partitioning, the shard key as a performance contract, the celebrity hot key, and why hash(key) mod N is a disaster for rebalancing. That was the algorithm. This lesson is the operations on top of it — running a real cluster that serves many customers at once without one starving the others, getting every request to the partition that owns its key while partitions are moving underneath you, and moving shards across machines in production without taking an outage. We can already split data by key; now we run a real multi-tenant cluster and move shards without an outage.
New capability: Choose a multitenancy model on its isolation/cost trade; stop a noisy neighbor from starving other tenants with quotas and resource limits; isolate or split a hot tenant; route a request to the partition owning its key through a routing tier or coordination service; and execute a safe, throttled rebalance from a runbook instead of triggering a rebalance storm.
tenant_id, schema-per-tenant, database/cluster-per-tenant — as a spectrum from cheap-and-weakly-isolated to expensive-and-strongly-isolated. (2) The noisy neighbor: a worked number for how one tenant's load starves the rest, and the quota/rate-limit/cgroup mitigations. (3) The hot tenant (the celebrity problem at tenant granularity) — dedicated shard, split, salt. (4) Request routing — how a client finds the partition owning a key (routing tier vs coordination service vs gossip) and the staleness problem during a move. (5) Rebalancing operations — manual vs automatic, the rebalance storm, throttling, and a concrete rebalance runbook.1 · Multitenancy models: the isolation/cost spectrum
A tenant is one isolated customer of a shared system — one company on your SaaS app, one team in your feature store, one project in your vector index. Multitenancy is running many tenants on the same infrastructure. The whole point is to amortize cost: a thousand small tenants sharing one cluster is far cheaper than a thousand clusters. The whole danger is that "sharing" means a fault, a load spike, or a data leak in one tenant can reach another. Every multitenancy decision trades exactly that — density (cheap) against isolation (safe).
There are three canonical models, in increasing isolation and cost:
tenant_id and every query filters on it. Densest and cheapest — one schema, one connection pool, trivial to add a tenant (insert rows). Weakest isolation: a missing WHERE tenant_id = ? leaks data across tenants, one tenant's huge table bloats shared indexes, and you cannot give one tenant a different schema version or restore one tenant alone. Make tenant_id the leading column of the shard key (lesson 11) so a tenant's rows co-locate.Most real systems are hybrid and tiered: the long tail of small tenants shares one pool (shared table), mid-size tenants get a schema each, and a handful of whales get dedicated clusters. The model is not global — it is a per-tenant tier you can promote a tenant through as it grows. The promotion path (shared row → own schema → own cluster) is exactly the hot-tenant mitigation of §3.
2 · The noisy neighbor: when one tenant starves the rest
The defining failure of dense multitenancy is the noisy neighbor: one tenant consumes a shared resource — CPU, disk I/O, connections, cache, storage — so aggressively that co-located tenants are starved, even though those tenants did nothing wrong. It is the hot-key problem of lesson 11 raised to tenant granularity, and it is invisible until it isn't: tenants A through Y see latency spikes and timeouts because tenant Z ran an unbounded query or a backfill.
Now tenant Z launches a backfill at 40,000 qps. Demand is 79,200 + 40,000 = 119,200 qps against a 100,000 ceiling — the cluster is 119% oversubscribed. Queueing collapses: every tenant's latency climbs as the pool saturates, and the well-behaved 99 are now failing their SLOs because of one neighbor. Z used 40% of the cluster while paying for 1%.
With a per-tenant quota of, say, 2,000 qps (2× fair share, to allow bursts), Z is throttled at the door: it gets 2,000 qps, total demand is 79,200 + 2,000 = 81,200 qps < 100,000, the cluster never saturates, and the other 99 tenants are unaffected. The quota converts a cluster-wide outage into one tenant's 429 Too Many Requests.
Mitigations all share one idea: turn an implicitly shared resource into an explicitly budgeted one, per tenant.
- Per-tenant quotas and rate limiting. Cap each tenant's qps, connections, query cost, and storage. A token bucket per
tenant_idat the routing tier (§4) lets bursts through but enforces a sustained ceiling. The worked number above is the whole argument. - Resource isolation at the OS / scheduler. When tenants run in their own processes or pods, pin CPU and memory with cgroups (the Linux kernel feature that caps a process group's CPU shares and memory) or Kubernetes pod limits, so a tenant physically cannot exceed its slice. A memory cap turns a runaway tenant's OOM into that pod's restart, not a shared-node crash.
- Storage quotas. Cap bytes per tenant so one tenant's unbounded growth cannot fill the shared disk and stall everyone's writes.
- Fair queueing / admission control. Schedule shared work round-robin across tenants rather than first-come-first-served, so a flood from one tenant cannot monopolize the queue.
- Promote the heavy tenant out (§3) — move it to its own schema or cluster so it has no neighbors to be noisy toward.
3 · The hot tenant: the celebrity problem at tenant granularity
Lesson 11's hot key was one shard-key value drawing a disproportionate share of traffic. A hot tenant (a "whale") is the same pathology one level up: a single tenant whose data or load dwarfs the others. If tenant_id is the leading part of the shard key, a whale's entire dataset and traffic hash to one partition — exactly the celebrity problem, and hashing alone cannot spread one tenant across partitions because all its rows share the same tenant_id.
The mitigations parallel the hot-key fixes from lesson 11, now applied to a tenant:
- Dedicated shard / cluster. Promote the whale to its own partition set or its own cluster (the cluster-per-tenant tier of §1). It now has the whole machine to itself and no neighbors — the cleanest fix, and why tiering exists. This is the tenant-scale analogue of "front the hot key with its own capacity."
- Split the tenant. If even one tenant exceeds a single partition, sub-partition within the tenant on a second key — e.g. shard key
(tenant_id, user_id)so the whale's rows spread across many partitions while small tenants still co-locate on one. This is the compound-key idea from lesson 11. - Key-salting. For a hot write target inside a tenant (a per-tenant global counter, a shared inbox), append a small random salt
0..kto spread writes acrosskpartitions and merge on read — the random-suffix trick from lesson 11, scoped to the tenant. Cost: reads of that value fan out to theksalted sub-keys.
4 · Request routing: finding the partition that owns a key
Once data is partitioned and partitions can move (rebalancing, §5), a client with a key faces a question: which node owns this key right now? This is service discovery for data, and lesson 11 named three shapes. Here is what each costs operationally.
mongos router, a Vitess vtgate) holds the partition map and forwards each request to the owning node. Clients stay dumb; you can rate-limit and apply per-tenant quotas here (§2). Cost: an extra network hop and a tier to operate and scale.Routing-table staleness is the operational hazard. The partition map is not static — a rebalance (§5) changes which node owns a range. Any client or proxy that caches the map can hold a stale entry and send a request to a node that no longer owns the key. Good systems handle this with a redirect-and-refresh protocol: the wrongly-addressed node replies "not mine, owner moved" (or forwards), and the client invalidates its cached map and refetches. The window between "partition moved" and "every cache learned" is the staleness window, and it is precisely why the cut-over step of a rebalance must be atomic and observable (§5).
Local vs global secondary indexes — the ops view
Lesson 11 introduced the two secondary-index layouts; the operational question is simply which queries stay single-shard. A local (document-partitioned) index lives beside the rows it indexes: writes touch one shard, but a query on the indexed field must scatter-gather across every shard (no single shard knows all the matches). A global (term-partitioned) index is partitioned by the indexed term: a query on that field hits one shard (single-shard read), but a write may update terms on several shards and the index lags. For multitenancy this often resolves cleanly: index by tenant_id (or make it the leading term) so a tenant's secondary lookups stay within that tenant's shard — single-shard, no cross-tenant fan-out. A query that omits tenant_id is the one that scatters, and you should know it is rare before you allow it.
5 · Rebalancing operations: don't trigger a storm
Rebalancing — moving partitions between nodes when you add or lose machines — was introduced mechanically in lesson 11 (fixed partitions, consistent hashing, dynamic splitting, so only ~1/N of data moves). The operational question is when and how fast the moves run, and who decides.
Manual vs automatic. Automatic rebalancing — the system detects imbalance and moves partitions on its own — is convenient and dangerous. The danger is the rebalance storm: an automatic rebalancer reacts to a transient signal (a node that is slow because it is busy serving traffic) by moving partitions off it, which makes the new owners busy, which the rebalancer reads as more imbalance, so it moves more — a feedback loop where rebalancing traffic starves live traffic and cascades. A node that looks overloaded during a brief spike, or one that is briefly unreachable during a failover (lesson 09), can trip a full data migration when nothing was actually wrong. We model exactly this cascade in lesson 28's drills. For this reason DDIA recommends keeping a human in the loop: the system proposes a rebalance plan, an operator approves it. And every automatic mover must throttle — cap the bytes-per-second and the number of partitions moving at once — so migration never out-competes the workload it exists to serve.
Time to move = 2 TB / 100 MB/s = 2,000,000 MB / 100 MB/s = 20,000 s ≈ 5.6 hours. That is the price of safety: the rebalance is slow on purpose. Halving the throttle to 50 MB/s doubles it to ~11 hours but halves the impact on live queries — a knob you tune against your latency SLO, not a thing to maximize.
- Add nodes. Bring the new nodes up, join them to the cluster, confirm health checks and that the coordination service (§4) sees them. They own no partitions yet.
- Mark partitions to move. Compute the plan (which partitions migrate to which new node so each ends with ~1/N of the load) and have a human review it. Do not move everything — move the minimum.
- Copy in the background. Replicate the marked partitions to their new owners as a throttled background stream (the worked number above) while the old owners keep serving all reads and writes. Live traffic is untouched; the new copy tails the old one's log to stay current.
- Cut over routing. When a partition's new copy is caught up, atomically flip its entry in the routing table / coordination service to the new owner, and signal caches to refresh (§4). This is the only moment ownership changes; keep the staleness window tiny.
- Verify. Confirm the new owner is serving the partition, error rates and latency are flat, no misroutes, and per-tenant load is balanced. Watch for the rebalance-storm signature (climbing migration traffic). If anything is wrong, the old copy still exists — roll the routing entry back.
- Decommission. Only after verification, delete the now-redundant old copies and reclaim their space. This is the irreversible step, done last and deliberately.
Failure modes
- Noisy neighbor. One tenant's load/storage saturates a shared pool; co-located tenants miss SLOs. Symptom: cluster-wide latency spike traced to a single
tenant_id's qps or bytes. - Hot tenant on one shard. A whale's data and traffic all hash to one partition (it shares one
tenant_id). Symptom: one shard at capacity while peers idle; adding shards doesn't help. - Cross-tenant leak. A query missing
WHERE tenant_id = ?returns another tenant's rows. Symptom: a tenant sees data it never wrote — a correctness/compliance incident, not just performance. - Stale routing table. A client/proxy caches an old map and hits a node that no longer owns the key. Symptom: misroutes and errors spiking right after a rebalance cut-over.
- Rebalance storm. Automatic rebalancing reacts to a transient spike or failover and cascades into mass data movement that starves live traffic. Symptom: a node addition (or a brief blip) tanks latency cluster-wide.
- Unthrottled migration. A move runs at full bandwidth and out-competes the workload it serves. Symptom: latency degrades for the whole duration of an otherwise-routine shard move.
Decision & runbook checklist
- Pick a tenancy tier per tenant: shared table (long tail), schema-per-tenant (mid), cluster-per-tenant (whales/regulated). Plan the promotion path.
- Set per-tenant quotas (qps, connections, storage) and enforce them at the routing tier; back them with cgroup/pod limits where tenants run in their own processes.
- Make
tenant_idthe leading part of the shard key so a tenant co-locates and secondary lookups stay single-shard; know which queries omit it and scatter. - Identify whales early; have a dedicated-shard / split / salt plan ready before one pins a node.
- Choose a routing shape (proxy tier / coordination service / gossip) and define the cache-refresh protocol for after a move.
- Keep rebalancing human-approved and throttled; size the throttle against your latency SLO (compute the move time first).
- Follow the runbook: add nodes → mark minimum partitions → copy in background → atomic cut-over → verify → decommission last. Never delete the source before the destination is proven.
Checkpoint exercise
(tenant_id, entity_id) across 8 partitions, serving 80,000 lookups/s across 200 tenants. (a) Fair share is how many qps per tenant, and what quota would you set to allow a 3× burst without letting any tenant exceed it? (b) One tenant launches a backfill at 30,000 qps with no quota — show the cluster goes oversubscribed and state what the 199 others experience; then show the quota from (a) prevents it. (c) That same tenant turns out to be a whale whose features won't fit one partition's slice — give two concrete fixes and the read-path cost of each. (d) You add 2 nodes (8 → 10) holding 6 TB total; with a fixed-partition scheme and a 75 MB/s throttle, how much data moves and how long does it take? Write the first three runbook steps you'd execute and the one signal that would make you abort.Where this points next
We can now run a multi-tenant cluster: data split by key, tenants isolated by quota and tier, requests routed to the right partition even as shards move, and rebalances executed from a runbook instead of a panic. But notice what we have not made safe — what happens when one logical operation must touch more than one key, on possibly different partitions, while concurrent clients and crashes claw at it. A transfer between two tenants' balances, an order plus its inventory row: these must behave as a single all-or-nothing unit even though partitioning has scattered the keys across machines. That is the next pressure — transactions and isolation (lesson 13): what atomicity and isolation actually promise, the ladder of isolation levels, and the anomalies each level does or doesn't prevent, with partition boundaries making all of it harder.
tenant_id (cheap, weak), schema-per-tenant (middle), cluster-per-tenant (expensive, strong) — and real systems tier tenants across it. The defining failure is the noisy neighbor: one tenant starving a shared pool (worked: a 40,000-qps backfill oversubscribing a 100k cluster and breaking 99 innocent tenants), fixed by per-tenant quotas, rate limits, and cgroup/pod resource isolation. A hot tenant is the celebrity hot key at tenant granularity — all its rows share one tenant_id and land on one shard — fixed by a dedicated shard, splitting on a sub-key, or salting. Request routing gets a key to its owning shard via a proxy tier, a coordination service holding the authoritative map, or gossip; the operational hazard is a stale routing table after a move, handled by redirect-and-refresh. And rebalancing in production is human-approved and throttled to avoid a rebalance storm, executed from a runbook — add nodes, mark the minimum partitions, copy in the background, cut over atomically, verify, decommission last — because the source of truth is the shard owning a key, and the routing table that names it is itself derived state with a freshness budget.Interview prompts
- Walk the three multitenancy models and their trade. (§1 — shared table with
tenant_idis densest/cheapest but weakest isolation; schema-per-tenant adds logical separation and per-tenant backup at more metadata cost; cluster-per-tenant gives the strongest isolation and smallest blast radius but is most expensive. Real systems tier tenants across the spectrum.) - What is a noisy neighbor and how do you stop it? (§2 — one tenant saturating a shared resource and starving co-located tenants; fixed by per-tenant quotas/rate limits at the routing tier and cgroup/pod resource limits, converting a cluster outage into one tenant's 429.)
- One tenant runs 40k qps on a 100k cluster of 100 tenants — what happens, and what does a 2k-qps quota change? (§2 — demand hits 119k vs a 100k ceiling, the pool saturates and all 99 others miss SLOs; a 2k quota caps that tenant so total demand stays ~81k under ceiling and no one else is affected.)
- How is a hot tenant the same problem as a hot key, and how do you fix it? (§3 — a whale's rows share one
tenant_idso they all hash to one shard, just like a celebrity key; fix with a dedicated shard/cluster, sub-partitioning on a second key, or key-salting — the tenant-scale analogues of lesson 11's hot-key fixes.) - How does a client find which node owns a key, and what breaks during a rebalance? (§4 — a routing tier/proxy, a coordination service holding the authoritative map, or gossip; the hazard is a stale cached routing table after a move, handled by the wrong node redirecting and the client refreshing its map.)
- What is a rebalance storm and how do you avoid one? (§5 — automatic rebalancing reacting to a transient spike/failover and cascading into mass data movement that starves live traffic; avoid with a human-in-the-loop approved plan and a throttle capping migration bandwidth/concurrency.)
- Give the safe rebalance runbook and the invariant behind it. (§5 — add nodes, mark the minimum partitions, copy in the background while old owners serve, cut over routing atomically, verify, decommission last; invariant: copy first and never delete the source until the destination is proven.)