By the end of this lesson
  • Compare Range-Based vs. Hash-Based partitioning topologies and calculate write skew under monotonic key workloads
  • Contrast Document-Partitioned (Local) vs. Term-Partitioned (Global) secondary index architectures
  • Prove that adding a node to an N-node Consistent Hashing ring migrates strictly 1/(N + 1) fraction of keys
  • Design an automated, zero-downtime shard rebalancing algorithm using background snapshot streaming and atomic route cutover

When an application dataset grows from gigabytes to terabytes and petabytes, it inevitably exceeds the physical storage capacity, memory bandwidth, and I/O throughput of a single machine. The only path forward is horizontal partitioning (also termed sharding).

Partitioning divides a large dataset into smaller, disjoint subsets called partitions. Each partition acts as an independent mini-database, allowing a cluster to scale read and write throughput linearly by adding commodity servers.

However, partitioning fundamentally alters application architecture: cross-partition transactions become expensive distributed operations, secondary index queries risk cluster-wide scatter-gather storms, and adding or removing machines requires non-disruptive rebalancing.


1. Problem and Motivation

Consider an analytical metrics platform storing events.

If partitioned naively:

  1. The Range Hotspot: The team partitions events by timestamp: Shard-1: [Jan 1 - Jan 7], Shard-2: [Jan 8 - Jan 14]. Because all incoming writes contain the current timestamp, of writes hammer the single latest shard, melting its CPU while older shards sit idle.
  2. The Rehash Catastrophe: The team hashes user IDs using shard = hash(user_id) % N. When the cluster expands from to servers, of all keys map to new shard IDs. The entire database must pause operations to execute a massive, multi-terabyte data migration across the network.
  3. Scatter-Gather Amplification: An application partitions users by user_id, but frequently queries WHERE email = 'user@example.com'. Because the email index is local to each shard, every single login query must be broadcast to all shards, bottlenecking overall latency on the single slowest node.

To avoid these traps, architects must master Range Partitioning, Consistent Hashing with Virtual Nodes, and Secondary Indexing Topologies.


2. Mental Model & Architectural Taxonomy

SYSTEM ARCHITECTURE BLUEPRINTDISTRIBUTED SYSTEMS
Partitioning Topologies: Range vs. Hash, Secondary Indexes & Rebalancing
ARCHITECTURAL FLOW · STEP 1 OF 3

1. Baseline: 3 Active Nodes & Node A

Establishes the ingress and structural boundary across Baseline: 3 Active Nodes & Node A: P0, P1, P2; P3, P4, P5; HOT! >95%.

Architectural Invariant: Maintains deterministic distributed systems invariants across Baseline: 3 Active Nodes & Node A.
ARCHITECTURAL FLOW · STEP 2 OF 3

2. Add Node D & Catch-Up Sync & Node C (Origin)

Coordinates core data-plane execution and state transitions through Add Node D & Catch-Up Sync & Node C (Origin): Streaming P8..; Catch-up WAL; P8 routed to Node D • Lock: <5ms.

Architectural Invariant: P8 routed to Node D • Lock: <5ms.
ARCHITECTURAL FLOW · STEP 3 OF 3

3. Range-Based (Bigtable/HBase) & Local (Document-Partitioned)

Enforces durability, state convergence, and output verification across Range-Based (Bigtable/HBase) & Local (Document-Partitioned): Shard 1: [A - G) • Shard 2: [G - N); Shard 3: [N - Z); Advantage.

Architectural Invariant: Latency bounded by slowest node.

Comparison of Partitioning Schemes

DimensionRange-Based PartitioningHash-Based Partitioning
Mapping MechanismContiguous key slices: Mathematical hash:
Range Queries (BETWEEN)Optimal: Scans a single partition or contiguous rangePoor: Keys scattered randomly across all shards
Hotspot SusceptibilityHigh: Sequential/timestamp keys create single-shard write bottlenecksLow: Hashes distribute writes uniformly across the ring
Rebalancing MechanismDynamic Region / Tablet splitting (e.g. Bigtable, TiKV)Consistent hashing token migration (Cassandra, DynamoDB)
Representative SystemsGoogle Bigtable, Apache HBase, TiDB/TiKV, CockroachDBApache Cassandra, Amazon DynamoDB, Couchbase, Redis Cluster

3. Core Terminology & Sharding Primitives

  • Partition Key (Sharding Key): The attribute (or composite attributes) in a record that dictates which physical partition owns the data.
  • Hotspot (Key Skew): An uneven distribution where a disproportionate volume of reads or writes targets a single partition, creating a bottleneck.
  • Scatter-Gather: A distributed query pattern where the coordinator broadcasts a request to every partition in the cluster and merges the independent responses.
  • Virtual Nodes (vnodes): An optimization in consistent hashing where each physical server is mapped to multiple distinct points on the token ring, evening out variance and simplifying rebalancing.
  • Consistent Hashing: A hashing technique where adding or removing a node requires remapping only fraction of keys on average.

4. Deep Dive: Secondary Index Architectures

Partitioning by a primary key is straightforward. The architectural challenge arises when queries need to filter by a secondary attribute (e.g., finding orders by customer_email when data is partitioned by order_id).

Architecture A: Document-Partitioned (Local Index)

  • Mechanics: Each shard maintains its own secondary index covering only the documents stored within that specific shard.
  • Write Path: Extremely fast. Updating a record requires modifying only the local shard. No distributed locks or cross-network 2PC required.
  • Read Path: Expensive (Scatter-Gather). The client must query every single shard in the cluster. If the cluster has shards, one query becomes internal queries. The overall response time is bounded by the percentile latency of the slowest shard.

Architecture B: Term-Partitioned (Global Index)

  • Mechanics: The secondary index itself is partitioned independently across the cluster, keyed by the indexed term (e.g., terms A - M on Node 1, N - Z on Node 2).
  • Write Path: Complex. Writing a single document requires a distributed transaction or asynchronous pipeline between the data partition and the index partition.
  • Read Path: Extremely fast. Looking up user@example.com queries exactly the single shard responsible for that email hash, returning in a single point query.

5. Mathematical Foundations & Formal Proofs

5.1 Consistent Hashing Key Migration Theorem

Let a cluster have nodes distributed uniformly on a continuous hash ring . When node is added:

  1. Node claims ownership of an interval of size of the total ring space.
  2. The keys falling inside this newly allocated interval are transferred to node from its immediate predecessor/successor.
  3. All other keys on the ring remain completely unaffected.

Theorem: Under consistent hashing, the expected fraction of total keys migrated upon adding the node is:

Contrast this with naive modular hashing : When the modulus changes to , the fraction of keys that must change nodes is:

For a 10-node cluster scaling to 11 nodes:

  • Consistent hashing migrates of keys.
  • Modulo hashing forces an eviction and network migration of of the entire database!

6. Production TypeScript Simulation: Consistent Hashing Ring with Vnodes

Below is a production-grade Consistent Hashing Ring featuring Virtual Nodes (vnodes), deterministic token placement, and keyspace migration tracking:

import { createHash } from "node:crypto";

export class ConsistentHashRing {
  private ring = new Map<bigint, string>(); // Token -> NodeId
  private sortedTokens: bigint[] = [];

  constructor(
    public readonly vnodesPerNode: number = 100
  ) {}

  private hash(value: string): bigint {
    const hex = createHash("md5").update(value).digest("hex");
    return BigInt(`0x${hex.substring(0, 16)}`); // 64-bit token
  }

  public addNode(nodeId: string): void {
    for (let i = 0; i < this.vnodesPerNode; i++) {
      const token = this.hash(`${nodeId}#vnode-${i}`);
      this.ring.set(token, nodeId);
      this.sortedTokens.push(token);
    }
    this.sortedTokens.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
  }

  public removeNode(nodeId: string): void {
    this.sortedTokens = this.sortedTokens.filter((token) => {
      if (this.ring.get(token) === nodeId) {
        this.ring.delete(token);
        return false;
      }
      return true;
    });
  }

  public getNode(key: string): string {
    if (this.sortedTokens.length === 0) {
      throw new Error("ClusterEmpty: No nodes registered on hash ring");
    }

    const keyToken = this.hash(key);

    // Binary search for the first token >= keyToken
    let low = 0;
    let high = this.sortedTokens.length - 1;
    let targetIdx = 0;

    while (low <= high) {
      const mid = Math.floor((low + high) / 2);
      if (this.sortedTokens[mid] >= keyToken) {
        targetIdx = mid;
        high = mid - 1; // Look for earlier match
      } else {
        low = mid + 1;
      }
    }

    // Wrap around ring if keyToken > all tokens
    if (keyToken > this.sortedTokens[this.sortedTokens.length - 1]) {
      targetIdx = 0;
    }

    const assignedToken = this.sortedTokens[targetIdx];
    return this.ring.get(assignedToken)!;
  }

  public measureKeyDistribution(keys: string[]): Record<string, number> {
    const distribution: Record<string, number> = {};
    for (const key of keys) {
      const node = this.getNode(key);
      distribution[node] = (distribution[node] || 0) + 1;
    }
    return distribution;
  }
}

function runDemo(): void {
  console.log("=== Partitioning & Sharding Rebalancing: Consistent Hash Ring ===");
  const ring = new ConsistentHashRing(64);
  ring.addNode("shard-1");
  ring.addNode("shard-2");
  ring.addNode("shard-3");
  console.log("Consistent hash ring initialized with 3 shards (64 vnodes/shard)");
  for (const k of ["order-1", "order-2", "order-3", "order-4"]) {
    console.log(`  Key '${k}' maps to shard: ${ring.getNode(k)}`);
  }

  // Verify 1/(N+1) migration fraction when scaling from N=3 to N+1=4 shards
  const sampleKeys = Array.from({ length: 10000 }, (_, i) => `key-${i}`);
  const beforeMap = new Map<string, string>(sampleKeys.map((k) => [k, ring.getNode(k)]));
  ring.addNode("shard-4");
  let migrated = 0;
  for (const k of sampleKeys) {
    if (ring.getNode(k) !== beforeMap.get(k)) migrated++;
  }
  console.log(`Added shard-4: migrated ${migrated}/10000 keys (${((migrated / 10000) * 100).toFixed(1)}%, theoretical 1/(N+1) = 25.0%)`);
}

runDemo();

Simulation Execution Output & State Transitions

=== Partitioning & Sharding Rebalancing: Consistent Hash Ring ===
Consistent hash ring initialized with 3 shards (64 vnodes/shard)
  Key 'order-1' maps to shard: shard-2
  Key 'order-2' maps to shard: shard-1
  Key 'order-3' maps to shard: shard-1
  Key 'order-4' maps to shard: shard-1
Added shard-4: migrated 2460/10000 keys (24.6%, theoretical 1/(N+1) = 25.0%)

7. Open-Source Implementation Analysis

A. TiDB / TiKV: Continuous Dynamic Region Splitting

TiKV divides the raw Byte keyspace into contiguous Regions (default each).

  • Each Region is a Raft consensus group replicating across 3 or 5 nodes.
  • When a Region reaches due to insertions, it executes an atomic Region Split:
    [ Region 1: "a" -> "m" ]  ──(Split at "g")──>  [ Region 1: "a" -> "g" ] + [ Region 2: "g" -> "m" ]
  • The Placement Driver (PD) continuously monitors cluster disk and QPS metrics, rebalancing Regions across physical nodes by scheduling Raft peer additions and leader transfers.

B. Vitess: Declarative VSchema Keyspace Partitioning

Vitess shards massive MySQL clusters transparently:

  • The VSchema defines the sharding key (e.g. user_id using hash or reverse_bits).
  • VTGate proxies incoming SQL queries:
    • If a query filters by WHERE user_id = 42, VTGate computes the keyspace ID and routes directly to the single target VTTablet.
    • If a query cannot resolve a shard key, VTGate issues scatter-gather queries with stream-merging.

8. Failure Modes & Operational War Stories

War Story: The Celebrity Shard Meltdown

  • Incident: A social media application partitioned user activity feeds by user_id.
  • Failure: A celebrity with 80 million followers posted a live update. The partition responsible for that celebrity’s user_id received , maxing out CPU, while remaining shards experienced normal load ().
  • Outcome: The database partition crashed. The automated orchestrator kept restarting the pod, which immediately crashed again due to the unbuffered TCP backlog.
  • Mitigation: Implemented Salted Keys: For celebrity accounts, the primary key was salted with a random suffix (celebrity_id#1, celebrity_id#2, …, celebrity_id#16), distributing the write load across 16 shards. Reading queries aggregate across the 16 salted keys.

9. Operational Decision Guide

Workload CharacteristicRecommended Partitioning TopologyRationale
Range scans required (BETWEEN timestamp1 AND timestamp2)Range-Based (TiKV / CockroachDB)Contiguous keys are co-located on disk, minimizing I/O hops.
Key-value lookups, high write throughputHash-Based Consistent Hashing (Cassandra / DynamoDB)Uniform random distribution prevents write skew and hot spots.
Frequent secondary attribute lookups with low write volumeTerm-Partitioned (Global Index)High read speed; avoids cluster-wide scatter-gather storms.
Massive write volume with ad-hoc search queriesDocument-Partitioned (Local Index)Zero cross-shard coordination on writes; scatter-gather acceptable for low-QPS admin queries.

10. Hands-on Operational Drills

  1. Verify Consistent Hashing Key Migration: Run ConsistentHashRing with 4 nodes (node-A, node-B, node-C, node-D) and generate random keys. Add node-E. Count how many keys changed ownership and verify that the migration fraction is approximately .
  2. Benchmark Vnode Variance: Test key distribution across 3 nodes with vnodesPerNode = 1 vs vnodesPerNode = 200. Observe how virtual nodes reduce standard deviation from down to .
View Reference Architecture & Worked Solutions (CORE-M19-001)

1. Worked Solution — Drill 1: Verify Consistent Hashing Key Migration ()

  • Mathematical Proof & Expected Migration Bound:
    • With nodes (node-A through node-D) and virtual nodes per physical server ( tokens uniformly interleaved on ), each node initially owns an expected ( of keys).
    • Adding node-E inserts new virtual-node tokens into the ring. Each new token splits an existing interval and steals keys solely from the single successor token immediately clockwise on the ring, leaving all keys that continue to land on existing nodes untouched.
    • Expected keys migrated: Every moved key transitions exclusively from an existing node to node-E (roughly keys donated by each of node-A, node-B, node-C, and node-D), whereas zero keys are exchanged between node-A..D. By contrast, naive hash(k) % 4 hash(k) % 5 remaps .

2. Worked Solution — Drill 2: Benchmark Virtual Node (Vnode) Variance ( vs. )

  • Why Suffers Severe Key Skew:
    • Placing only random token per physical node on the ring partitions the unit circle into random exponential spacings (Dirichlet distribution ). The theoretical relative standard deviation of arc length for vnodes per node is:
    • At , of the mean share (), meaning one node routinely receives of all keys while another receives .
  • Variance Reduction at :
    • Increasing to vnodes per node yields of the per-node share (or absolute cluster keyspace deviation across keys):
      • V = 1: e.g., node-A: 6,120 (61.2%), node-B: 2,740 (27.4%), node-C: 1,140 (11.4%)
      • V = 200: e.g., node-A: 3,385 (33.9%), node-B: 3,290 (32.9%), node-C: 3,325 (33.3%)
    • Furthermore, if node-B crashes under , its token ranges are absorbed evenly across all surviving nodes rather than dumping of node-B’s load onto a single clockwise neighbor.

11. Authoritative References & Version Context

  • David Karger et al. (1997), “Consistent Hashing and Random Trees: Distributed Caching Protocols for Relieving Hot Spots on the World Wide Web”, ACM STOC ‘97 — doi.org/10.1145/258533.258660
  • Giuseppe DeCandia et al. (2007), “Dynamo: Amazon’s Highly Available Key-value Store”, ACM SOSP ‘07 (Partitioning and Virtual Nodes) — doi.org/10.1145/1294261.1294281
  • Apache Cassandra 5.0 Documentation — Architecture: Token Allocation and Virtual Nodes (vnodes) — cassandra.apache.org/doc/latest/cassandra/architecture/dynamo.html
  • Version Context: Token-ring partitioning models apply to Apache Cassandra 4.x/5.0, Amazon DynamoDB, and ScyllaDB 5.x.
MASTERY GATE · STEP 35 OF 94

Knowledge check & lesson completion

Score 4 / 4 (100%) on this check to mark this lesson Completed ✓, advance your position on the Master Spine, and add these concepts to your Spaced-Repetition Review Deck.

1. Why does Range-Based partitioning (e.g. Bigtable or HBase) on a timestamp primary key (e.g., event_timestamp, user_id) cause a catastrophic write bottleneck?
2. What is the primary operational trade-off of a Document-Partitioned (Local) secondary index compared to a Term-Partitioned (Global) secondary index?
3. When scaling a cluster from N to N + 1 nodes using Consistent Hashing, what fraction of the total keyspace must be migrated across the network?
4. How does TiKV handle shard growth when a single Region exceeds its configured size threshold (e.g., 96 MB)?
PERSONAL NOTEBOOK

Chapter Notes

Personal engineering insights and observations saved to this browser.

0 notes
Tip: You can also highlight text in the article above to quote directly.

Loading personal notes...