Implementing Vertical Sharding: A Practical Guide to Live Table Migrations

Arpit Bhayani

Arpit Bhayani

May 25, 2022 • 8 min read

Play

Implementing Vertical Sharding: A Practical Guide to Live Table Migrations

Database sharding is the architectural practice of distributing a database across multiple physical database instances to manage compute, memory, and storage bottlenecks. While horizontal sharding divides rows of a single table across multiple servers using a partition key, vertical sharding splits a database by segregating entire tables onto different physical database servers.

+-------------------------------------------------------------+
|                      Original Database                      |
|                [ T1 ]   [ T2 ]   [ T3 ]   [ T4 ]            |
+-------------------------------------------------------------+
                               |
                               v  (Vertical Sharding)
+-------------------+ +-------------------+ +-------------------+
|      Shard 1      | |      Shard 2      | |      Shard 3      |
|      [ T2 ]       | |   [ T1 ] [ T4 ]   | |      [ T3 ]       |
+-------------------+ +-------------------+ +-------------------+

1. Motivations for Vertical Sharding

There are two primary industry drivers for vertical sharding:

  1. Monolith to Microservices Transition: When decoupling domain boundaries, specific domains require dedicated persistence stores (e.g., user profiles vs. billing records).
  2. Hotspot and Load Management: If a database instance hosts tables T1 and T4, and table T1 experiences an intense influx of writes or expensive queries, the underlying server becomes constrained on CPU, disk I/O, or connection pools. This degrades performance for unrelated tables (like T4). Vertically partitioning T1 to an isolated shard immediately relieves resource contention.

2. Core Architectural Challenges

Moving an active table handling live reads and writes from one database instance to another introduces two fundamental coordination problems:

  1. Dynamic Metadata Routing: API servers executing domain queries must know which database instance holds which table. For instance, when an authentication query arrives, the API layer must query the host designated for authentication tables.
  2. Reactive Configuration Invalidation: When a table moves from DB1 to DB2, this metadata mapping changes. If an API server retains a stale routing entry, it will execute queries against the wrong database, causing stale reads or split-brain writes.

The Metadata Hub: Apache ZooKeeper

A centralized, strongly consistent distributed configuration store like Apache ZooKeeper (or etcd) cleanly resolves this coordination problem:

  • Consistent State: ZooKeeper acts as the source of truth for table-to-host mappings (e.g., /tables/t1 -> db1:3306, /tables/t2 -> db2:3306).
  • Proactive Event Notification (Watches): Rather than having API servers constantly poll configuration stores on every request, API servers set a ZooKeeper Watch on the table mapping znodes. When a configuration path updates, ZooKeeper immediately dispatches a reactive notification to all connected API servers, which then atomically refresh their local routing caches.
graph TD
    Client[Client Request] --> LB[Load Balancer]
    LB --> API1[API Server 1]
    LB --> API2[API Server 2]
    
    subgraph Configuration Management
        ZK[Apache ZooKeeper\nTable-to-DB Mapping] 
    end
    
    API1 -.->|1. Watch & Fetch Mapping| ZK
    API2 -.->|1. Watch & Fetch Mapping| ZK
    
    subgraph Database Shards
        DB1[(DB Shard 1\nTable T1)]
        DB2[(DB Shard 2\nTable T2)]
    end
    
    API1 -->|Route Query for T1| DB1
    API2 -->|Route Query for T2| DB2

3. Step-by-Step Live Table Migration Workflow

To move an active table (e.g., T2) from DB1 to DB2 with sub-second disruption, engineers follow a multi-stage migration pipeline leveraging snapshotting, Change Data Capture (CDC), and an atomic cutover.

sequenceDiagram
    autonumber
    participant DB1 as DB1 (Source)
    participant Script as Migration Worker
    participant DB2 as DB2 (Target)
    participant ZK as ZooKeeper
    participant API as API Servers

    Note over DB1, DB2: Phase 1: Snapshot & Initial Restore
    Script->>DB1: Take consistent dump (record binlog position)
    Script->>DB2: Restore dump file (Schema + Data)

    Note over DB1, DB2: Phase 2: CDC / Continuous Catch-up
    loop Continuous Replication
        Script->>DB1: Tail binlog from recorded position
        Script->>DB2: Apply transformed writes for T2
    end

    Note over DB1, API: Phase 3: Cutover
    Script->>DB1: RENAME TABLE T2 TO T2_BAK (Atomic lock-out)
    Script->>Script: Verify zero replication lag
    Script->>ZK: Update path /tables/t2 -> DB2
    ZK-->>API: Watch triggered (Reactive notification)
    API->>API: Invalidate cache & point T2 connections to DB2
    API->>DB2: Forward live read/write queries to DB2

Step 1: Snapshotting and Binlog Position Capture

The table must be dumped without halting production traffic on the source database. Using utilities like mysqldump (or enterprise physical backup tools like Percona XtraBackup):

mysqldump --single-transaction --master-data=2 --databases db1 --tables t2 > t2_dump.sql
  • --single-transaction: Uses an InnoDB snapshot isolation level to produce an internally consistent snapshot without acquiring an exclusive table lock.
  • --master-data=2: Emits the exact binary log (binlog) coordinates (filename and byte offset, e.g., mysql-bin.000123, position 156) corresponding precisely to the point in time the snapshot was established.

Step 2: Restoring the Snapshot to the Target Shard

The generated snapshot is applied directly to the target database instance DB2:

mysql -h db2-host -u user -p db2 < t2_dump.sql

At this stage, DB2 contains an exact historical replica of T2 up to binlog offset 156. However, incoming production writes have continued landing on DB1 during the export/import window.

Step 3: Tailing the Binlog (Continuous Change Data Capture)

To bridge the divergence between DB1 and DB2, start a custom replication consumer:

  1. Connect to DB1’s binlog stream starting at the recorded offset (156).
  2. Filter events strictly for table T2.
  3. Replay these continuous INSERT, UPDATE, and DELETE operations against DB2.

Native MySQL database-level replication can be restrictive when migrating individual tables across distinct topology schemas, so engineers typically use custom CDC workers or streaming utilities (such as Debezium, Maxwell, or tailored scripts) to consume and apply events.

Step 4: The Cutover Phase

Once the replication lag between DB1 and DB2 falls close to zero (e.g., sub-millisecond, effectively real-time), perform the final switch:

  1. Stop Writes on DB1 (Atomic Invalidation): Execute an atomic table rename on DB1:
    RENAME TABLE t2 TO t2_bak;
    Because table renames in transactional engines are metadata operations, execution takes milliseconds. Any queries arriving at DB1 for t2 fail immediately with a Table 't2' doesn't exist error, preventing inconsistent writes or split-brain scenarios.
  2. Drain Remaining Changes: Ensure the CDC worker applies the final remaining buffered binlog events up to the rename event to DB2.
  3. Update Routing Metadata: Update the table pointer in ZooKeeper:
    set /tables/t2 "db2:3306"
  4. Reactive Propagation: ZooKeeper’s watch fires on all API servers. The application connection pools rebind t2 queries to DB2.
  5. Normal Operation Resumes: Live traffic flows directly to DB2. The deprecated t2_bak table on DB1 can subsequently be dropped.

4. Architectural Trade-offs: Consistency vs. Availability

During the cutover, there is a brief duration (typically 5 to 15 milliseconds) between renaming the table on DB1 and all API servers completing the ZooKeeper watch update. During this window, incoming queries targeting t2 receive table-not-found errors or connection resets.

This design represents an intentional trade-off under the CAP theorem:

AttributeStrategy ChosenAlternative Strategy
Design ChoiceConsistency over AvailabilityDual-writing / Best-effort availability
BehaviorReject writes for a brief 10ms windowAllow writes to both nodes concurrently
Failure ModeShort burst of retryable 500 errorsSilent split-brain, data loss, reconciliation nightmares

By prioritizing consistency, the system guarantees that not a single write is orphaned or accepted into a database that has lost authoritative status.


5. The “Huge Table” Migration Constraint

While this workflow operates smoothly for small to medium-large tables, migrating extremely massive, hyper-active write-heavy tables presents a significant operational hurdle:

  • CDC Starvation: If a table receives tens of thousands of writes per second, a single-threaded CDC consumer replaying events on DB2 may struggle to achieve zero lag. The target shard may never catch up to the binlog head.
  • Recommended Migration Strategy: Instead of migrating the monolithic “anchor” table (the largest, highest-throughput table in the schema), vertically shard the satellite tables around it. Move smaller and medium-sized tables off to distinct shards, leaving the massive table alone on the original, dedicated database instance.

6. Blueprint for Local Validation

Engineers can simulate this end-to-end distributed system locally using standard developer tools:

  1. Topology: Run a single-node ZooKeeper container and two MySQL containers (listening on host ports 3306 and 3307).
  2. Metadata Setup: Store table routes as ZooKeeper znodes (/shards/t1, /shards/t2).
  3. Application Layer: Write a minimal service (Node.js, Go, or Python) utilizing the official ZooKeeper client library to subscribe to zk.watch() triggers and initialize dynamic database connection pools.
  4. Migration Execution: Use mysqldump with --master-data=2, load the dump into MySQL on 3307, execute a Python script to tail the MySQL binlog using python-mysql-replication, trigger the table rename on 3306, and update the ZooKeeper znode.

Local validation confirms that the coordination mechanism works reliably under realistic cutover scenarios before taking it to production environments.

Arpit Bhayani

Principal Engineer II at Razorpay - building Agent Studio, Ex-staff engg at GCP Memorystore & Dataproc, Creator of DiceDB, ex-Amazon Fast Data, ex-Director of Engg. SRE and Data Engineering at Unacademy. I spark engineering curiosity through my no-fluff engineering videos on YouTube and my courses