SD System Design In Depth
0%
lesson system-designobject-storage

Partition manager and map table

Module 2.3: Storage Engine Internals & Blob Systems Updated Sep 17, 2026
📄 Cheat Sheet
BigTable - One Database to Rule Them All?. | Distributed Systems Deep Dives With Ex-Google SWE
Jordan has no life BigTable - One Database to Rule Them All?. | Distributed Systems Deep Dives With Ex-Google SWE Watch on YouTube ↗

Jordan's Bigtable paper deep dive covers the master, tablet servers, the three-level METADATA location hierarchy and tablet splits, which is exactly this unit's model.

0:00 / --:--

Partition manager and map table

A partition map tells the frontend where to send an object request. When ownership changes, that answer must stay connected to two facts: the new owner has the accepted data, and the former owner can no longer change it.

We'll separate the manager's responsibilities from normal request routing, then follow one planned transfer through its final write and a stale-client retry.

Who maintains the assignment?

ComponentResponsibility
Partition mapDurably record each key range, its serving owner and ownership generation.
Partition managerAssign and move ranges, split busy ranges, merge cold neighbors, and arrange recovery after failures.
Partition serverServe its assigned ranges; one server can own several.
Coordination authoritySerialize manager decisions and establish valid ownership.
The frontend caches map entries and sends requests directly to the partition server. The manager stays off this ordinary request path. It examines load and health to decide when assignments should change.

An assignment carries an epoch, a generation that changes on handoff. The receiver must enforce that authority where a write takes effect. A cached address is only a routing hint; publishing a new map cannot stop an old process by itself.

For this design, a range has at most one active write owner. A transfer may briefly leave it with none. Allowing an unavailable interval is safer than accepting independent changes at two owners without a reconciliation protocol.

A planned move

A owns epoch 1. Its append-only file contains r1: first and a newline, six bytes. We copy those bytes to B while A remains active. Then A accepts r2: second and a newline, another seven bytes.

B is now behind. Before switching owners, pause new writes at A, wait for any admitted effects to finish, copy the missing tail, and verify the destination. Only then activate B at epoch 2.

sequenceDiagram accTitle: Copying the accepted writes before cutover accDescr: A first copies r1 to B, then accepts r2. The manager pauses A, copies the r2 tail and verifies equality before activating B at epoch 2. participant A as Owner A participant M as Manager participant B as Owner B A->>B: Copy r1: 6 bytes Note over A: Accept r2: 7 bytes M->>A: Pause and drain writes A->>B: Copy r2 tail M->>M: Verify complete files match M->>B: Activate epoch 2 Note over A: Epoch 1 can no longer write

In the object-storage example, the append and cutover use the same Python lock. A request cannot pass its authority check, wait through cutover, and then append with stale permission. The check and effect occur inside one guarded section.

python3 m13-object-store.py transfer
copy snapshot: 6 bytes; A epoch=1 remains owner
r2 during transfer: accepted
pause, copy tail: 7 bytes; equal=True; publish B epoch=2
delayed r3 at A epoch=1: stale or paused
refreshed r3 at B epoch=2: accepted
accepted: ["r1","r2","r3"] ; B bytes: b'first\nsecond\nthird\n'
authority and cutover lock are in memory; files are real

The delayed r3 attempt adds nothing at A. Retrying against B with epoch 2 appends the third record, bringing B to 19 bytes. If the copied files differ before cutover, the example raises an error and leaves the assignment paused.

This runs a chosen sequence of calls, not concurrent threads. The files are real, but authority and accepted-operation history live in memory. It has no durable transfer journal or power-loss recovery protocol.

How much must we pause?

Pausing for the whole transfer is simpler: finish earlier writes, copy a fixed source, verify it, then switch. The cost is an unavailable write interval covering the entire copy.

Copying while writes continue needs a snapshot boundary and a retained change history. Our append-only file uses a byte offset; a mutable database needs an equivalent agreement between snapshot and log. The final pause accounts for everything accepted after the snapshot. Measure that pause rather than assuming a small example proves acceptable downtime.

Not every assignment change requires copying object bytes. With a separate durable storage layer, a new partition server can load the existing checkpoint and replay its log. The requirement is still to recover the accepted state and establish exclusive authority before serving writes.

Failure is different from a planned move

A failed health check means the owner is suspected, not that its writes are impossible. The manager must prevent the old authority from taking effect even if that process resumes. Fencing supplies this boundary only when the resource receiving the writes enforces it.

The replacement also needs recoverable state outside the failed server. If A held the only copy, changing the map to B cannot recover its data. When replicas or shared storage exist, recover from them and the retained log before making B active.

Persist transfer progress and assignments through the coordination authority. After a manager restart, it must determine whether B already accepted writes before deciding to resume or abandon a move. Reopening A's older file after B became active would discard accepted history. Electing one manager helps serialize decisions; it does not replace durable state or write enforcement.

Handling an old map

Return an explicit stale-assignment response so the frontend can refresh and retry within its original deadline. Keep the operation identity. If the first attempt succeeded but its reply was lost, the new owner needs the corresponding receipt or another deduplication rule; the rejected-r3 example does not cover that case.

Those receipts belong with the transferred metadata. The next lesson makes the object metadata and operation results persistent together.


Under the Hood: Coordinating Range-Partitioned Tablets

In range-partitioned storage engines (like Google Bigtable, CockroachDB, and Apache HBase), data is divided into contiguous dynamic ranges called Tablets or Ranges. As tablets grow through writes, they must split and migrate across the server fleet. The Partition Manager and Routing Map Table orchestrate this distributed metadata.

Three-Level Routing Hierarchy (Bigtable Model)

flowchart TD Client["Client SDK"] -->|"1. Fetch Root Location"| Chubby["Chubby / Etcd Consensus Lock Service"] Chubby -->|"Points to"| RootTablet["Root Tablet (Metadata Level 1)"] RootTablet -->|"Points to"| MetaTablets["Metadata Tablets (Metadata Level 2)"] MetaTablets -->|"Points to"| UserTablets["User Data Tablets (Thousands of Nodes)"] Client -->|"2. Cache Routing Entries Locally"| LocalCache["Client Route Cache"]

Under the Hood: The Tablet Split

When a user tablet exceeds a size threshold (e.g. 256MB):

  1. The tablet server hosting the range picks a split row key (typically near the middle of its data) and splits the tablet into two child ranges locally.
  2. Because underlying SSTable data files are immutable, the split is instantaneous: child tablets reference the parent's SSTables with restricted start/end key boundaries (zero data copying).
  3. The tablet server commits the split by recording the new tablet in the METADATA table, then notifies the master (Bigtable describes no two-phase transaction here).
  4. Clients with stale cached locations discover them on a miss (the server no longer serves that row range), then invalidate the entry and walk back up the METADATA hierarchy for a fresh location.

Worked example: Bigtable's location hierarchy capacity

The Bigtable paper stores each tablet's location as a METADATA row of about 1 KB, and caps METADATA tablets at 128 MB.

  • One METADATA tablet holds 128 MB / 1 KB, about 2^17 (131,072) locations.
  • The root tablet (never split) points at up to 2^17 METADATA tablets, which together point at about 2^17 x 2^17 = 2^34 user tablets.
  • At 128 MB per user tablet, that addresses 2^61 bytes, far beyond any deployment.

Lookup cost from the paper: with an empty client cache, locating a tablet takes three network round trips (Chubby file, root tablet, METADATA tablet). With a stale cache, it can take up to six, because stale entries are discovered only on misses.

How systems implement the map

SystemRange unit and default sizeMap locationWho decides placement
BigtableTablet, roughly 100-200 MB in the paperChubby file, root tablet, METADATA tableMaster (single, with Chubby lock)
HBaseRegion, split around 10 GB by defaulthbase:meta table, location in ZooKeeperHMaster
CockroachDBRange, up to 512 MiB by defaultTwo-level meta1 and meta2 rangesDistributed per-node allocators and replicate queues
TiKVRegionPlacement Driver (PD)PD cluster

A split, step by step (CockroachDB style)

  1. A range exceeds its size threshold; the leaseholder picks a split key.
  2. A distributed transaction updates the range descriptors and the meta2 addressing records for both halves atomically.
  3. Both halves initially live on the same replicas; the rebalancer later moves one half elsewhere.
  4. Clients sending to the old descriptor get a range-key-mismatch error, refresh their cache from meta2 and retry.
Check yourself Question 1 of 3
Why splits are cheap recall
From: Under the Hood: The Tablet Split

Why can a Bigtable-style tablet split finish almost instantly even when the tablet holds hundreds of megabytes?

Key Takeaways

A 3-level hierarchical metadata table maps hundreds of millions of user ranges with zero central bottleneck.
Clients cache range-to-node routing tables locally; stale entries are discovered on misses and refreshed lazily (up to six round trips in Bigtable).
Tablet splits are instant metadata-only operations because immutable SSTables are partitioned by key boundaries without copying files.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue