Design Walkthrough 02 — Scaling a Metadata Service (Hive Metastore-Style)
Problem statement: A Hive-style metastore (databases → tables → partitions, schemas,
stats, locations) backed by one RDBMS serves a company that grew from 10K to 5M partitions
and from 50 to 5,000 queries/minute. getPartitions calls now take 30+ seconds, planner
latency dominates short queries, and the DBA is paged weekly. Fix it without breaking the
thousands of jobs using the Thrift API.
Table of Contents
- Step 1: Why metastores melt
- Step 2: Constraints
- Step 3: Alternatives
- Step 4: Chosen design
- Step 5: Consistency details
- Step 6: Migration plan
- Interviewer follow-ups
- References
Step 1: Why metastores melt
Name the actual pathologies before designing — they determine which fixes work:
- Partition explosion: partition-per-hour × years × thousands of tables ⇒ tens of
millions of rows in
PARTITIONS+ per-partition key/value tables; ORM-generated joins over them are super-linear. - Chatty planning: a single query plan can issue thousands of metastore calls
(
getTable,getPartitionsByFilter, per-partition stats). Latency multiplies. - Listing as a primitive:
getAllPartitions(table)returns megabytes; clients call it because the filtered API is historically unreliable — an API-shape problem, not a database problem. - Write amplification on stats: per-partition stats updates after every load job.
Step 2: Constraints
- API compatibility is the hard wall: thousands of jobs speak the Thrift API; clients embed old versions for years. Anything that changes call semantics needs versioned endpoints and long deprecation (Phase 11 discipline).
- Reads ≫ writes (~100:1), and reads are partition-metadata-shaped.
- Planner correctness tolerates bounded staleness for stats, but not for schema/location (wrong location = wrong data read).
Step 3: Alternatives
A. Scale the RDBMS (bigger box, read replicas, drop the ORM for hand-written SQL,
proper indexes). Cheap, buys 5–10×, no compat risk. Rejected as the answer but accepted
as step one — hand-written SQL via directSQL was exactly Hive's own move.
B. Cache layer in front (per-instance + distributed cache, TTL + invalidation). Buys read latency; the hard part is invalidation across multiple metastore instances — needs a change journal. Accepted as step two.
C. Shard the metastore by database/table hash. Solves table-count scale, but cross-shard ops (cross-db queries, global listing) get painful, and partition explosion within one table is unsolved. Deferred.
D. Change the data model — partition metadata out of the RDBMS into the table format itself (Iceberg/Delta-style manifests in object storage; the metastore keeps only the table pointer). Solves the root cause: planning reads manifests directly, scaling with the table not the service. This is the industry's actual answer, but it is a multi-year migration, not a fix. Accepted as the destination.
The staff-level move is sequencing A → B → D with compat preserved throughout, not picking one.
Step 4: Chosen design
Phase 1 — stop the bleeding (weeks): hand-written SQL paths for the hot calls;
covering indexes for getPartitionsByFilter; server-side pagination + result-size caps on
listing calls (new API variants, old ones kept but capped with loud deprecation warnings);
connection pooling and statement caching audits.
Phase 2 — read path (months): stateless metastore instances behind LB; write-through
journal table (monotonic change id per object); per-instance caches subscribe to the
journal and invalidate; getPartitionsByFilter served from cache keyed by (table,
filter-normal-form) with change-id validity. Stats reads get TTL staleness (planner-safe);
schema/location reads validate change-id (correctness-critical).
Phase 3 — data model (quarters): new tables created as Iceberg; metastore stores the
metadata pointer only; partition APIs for those tables are served by a shim that reads
manifests — old clients keep calling getPartitionsByFilter and get correct answers
without the RDBMS holding per-partition rows. Existing tables migrate by snapshot +
catchup, heaviest first (top 1% of tables typically hold >50% of partitions).
Step 5: Consistency details
- Journal as the ordering primitive: every metadata write commits the object change and journal row in one RDBMS transaction; cache validity = "my change-id for this object ≥ journal head for this object". Avoids distributed invalidation races without distributed transactions.
- Read-your-writes for the writing client: client carries the change-id returned by
its write; reads route with
min_change_idso a stale cache either catches up or forwards to the source. - Concurrent DDL: optimistic — writes carry expected version, conflict ⇒ retry with rebase or fail to the user (matching Iceberg's optimistic commit protocol at the table layer).
Step 6: Migration plan
Compatibility-first sequencing, the part interviewers actually probe:
- Every new behavior behind a server-side flag, default off; canary instances first.
- Shadow-read validation: new path computes, old path serves, diffs logged (count and content hashes) until diff rate < 1e-6 for two weeks.
- API deprecations announced with metrics: instrument who still calls the unpaginated listing (user-agent/job tags), chase the top offenders, then cap.
- Rollback story at each step: caches can be bypassed by flag; Iceberg-pointer tables keep a frozen RDBMS snapshot of their last pre-migration state for emergency fallback (read-only).
Interviewer follow-ups
- Why not just put the metastore in a NoSQL store? — the workload is relational (filters over typed partition keys, transactional DDL); the problem is the data model keeping per-partition rows centrally, not the engine. Moving engines re-platforms the pain.
- How do you size the cache and what's the eviction story? — partition metadata is highly skewed (hot tables); LRU with per-table weight caps; measure hit rate per call type, not globally.
- What breaks first at another 10×? — the journal table write rate and the single RDBMS as the write master ⇒ that is when sharding (C) returns, by database, with the journal per shard.
- How does this compare to what actually happened in the ecosystem? — Hive directSQL + AWS Glue-style hosted metastores + the Iceberg/Delta migration; HMS remains as a catalog-of-pointers (or is replaced by REST catalogs).
References
- Hive Metastore design docs
- Apache Iceberg spec — metadata & manifests
- Iceberg REST catalog spec
- Metastore directSQL (HIVE-4051 lineage)
- Designing Data-Intensive Applications — Kleppmann, ch. 5 (replication) and 7 (transactions)