all_lessons/data_intensive_systems/12 · multitenancy & opslesson 13 / 35 · ~15 min

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.

DDIA source
Arc drawn from Martin Kleppmann, Designing Data-Intensive Applications, Chapter 6 (Partitioning) — request routing, rebalancing strategies, and partitioned secondary indexes — layered with operational practice (multitenancy isolation models, per-tenant quotas, and rebalance runbooks) that DDIA treats as engineering context. Original synthesis; the feature-store and tenant-quota framings are ours.
Linear position
Prerequisite: Lesson 11 (Partitioning, Hot Keys, and Rebalancing) — you can choose a shard key, predict which queries scatter-gather, quantify hot-key skew, and you know why the node count must never appear in the placement function. We also lean on lesson 09 (each partition is itself a small replica set).
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.
The plan
Five moves. (1) Multitenancy models — shared table with 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:

Shared table (tenant_id column)
All tenants' rows live in the same tables; every row carries a 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.
Schema / database per tenant
Each tenant gets its own schema (or logical database) inside a shared server. Middle ground: strong logical separation (a query physically cannot see another schema), per-tenant backup/restore and schema version, but still sharing the server's CPU, memory, and disk. Costs more metadata and connection management; thousands of schemas strain a single server's catalog.
Cluster / database per tenant
Each tenant gets its own database instance or cluster. Strongest isolation — separate process, separate disk, separate failure domain, independent scaling and compliance boundary, blast radius of one. Most expensive (idle capacity per tenant, N times the operational surface) and slowest to provision. Reserved for large or regulated tenants ("whales").

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.

Worked number — what one noisy tenant costs
A shared cluster has 100 tenants on a pool sized for 100,000 queries per second (qps) total. Fair share is 100,000 / 100 = 1,000 qps per tenant. Suppose 99 well-behaved tenants each run 800 qps (under fair share), so they use 99 × 800 = 79,200 qps — the cluster has 100,000 − 79,200 = 20,800 qps of headroom.

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.

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:

ML-infra tie
A multi-team feature store or vector index is multi-tenant by team or model. One team's nightly backfill is a textbook noisy neighbor that can starve online inference reads for every other team — hence per-tenant write-rate quotas and isolated ingest pools. One enormous customer's embedding corpus is a hot tenant: give it dedicated shards, or sub-partition its vectors by sub-entity so a single team's index doesn't pin one node.

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.

client: read key = "tenant42:user7" | +-------------------------+--------------------------+ | | | (A) ROUTING TIER (B) COORDINATION SVC (C) GOSSIP client -> proxy client -> ZK/etcd client -> any node | (reads the map) (node knows / forwards) v | | proxy holds map returns owner = N3 node forwards to N3 forwards to N3 | | | client -> N3 v v served by N3 served by N3 routing table (the map): hash-range -> owning node ------------------------------------------------------ 0x0000 .. 0x3fff -> N1 0x4000 .. 0x7fff -> N2 0x8000 .. 0xbfff -> N3 <- "tenant42:user7" hashes here 0xc000 .. 0xffff -> N4
(A) Routing tier / proxy
A dedicated layer in front of the cluster (e.g. a 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.
(B) Coordination service
Clients (or the routing tier) consult a coordination service — ZooKeeper or etcd — that holds the authoritative partition-to-node map and pushes updates when it changes. The service is itself a consensus system (lesson 17). Cleanest source of truth; cost: a hard dependency on the coordination cluster's availability.
(C) Gossip / any-node
No central map. Each node knows enough of the topology (spread by a gossip protocol — nodes periodically exchange state) to forward a misdirected request to the right owner (Cassandra-style). No separate tier to run; cost: convergence is eventual, so the map can be briefly inconsistent across nodes, and a request may bounce an extra hop.

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.

Worked number — throttled move time
You add 2 nodes to an 8-node cluster (8 → 10) holding 10 TB total on a fixed-partition scheme, so about 2/10 = 20% of the data — 2 TB — must migrate onto the new nodes. If you let it run unthrottled it competes for the full network and tanks live latency. So you throttle to 100 MB/s of background copy (a small slice of each node's bandwidth, chosen to leave headroom for live traffic).

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.
Rebalance runbook
A safe shard move, step by step. Every step is observable and reversible until the cut-over.
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. Decommission. Only after verification, delete the now-redundant old copies and reclaim their space. This is the irreversible step, done last and deliberately.
The shape is identical to a safe migration anywhere: copy first, cut over atomically, verify, delete last — never delete the source until the destination is proven.

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_id the 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

Try it
You run a multi-tenant online feature store, shared-table model, hash-partitioned by (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.

Where is truth?
For a partitioned cluster the system of record for any key is the shard that owns it — the node holding the partition whose hash-range covers the key, per the routing table. Copies / derived views: the cached routing maps held by every client, proxy, and gossiping node — these are derived state, not truth. The authoritative map lives in the coordination service (ZooKeeper/etcd, lesson 17) or is converged by gossip. Freshness budget: the staleness window between a cut-over and every cache learning the new owner — kept tiny and bounded by the refresh protocol. Owner: the coordination service (or the gossip consensus) owns the map; the operator owns the rebalance plan. Deletion path: an old shard copy is deleted only in the runbook's final decommission step, after the new owner is verified. Reconciliation/repair: a wrongly-addressed node redirects-and-refreshes the stale client; a mismatched copy re-syncs from the owner's log. Evidence it is correct: flat error/misroute rates and balanced per-tenant load after cut-over, plus the source copy still present until verification passes.
Takeaway
Multitenancy trades density against isolation along a spectrum — shared table with 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