Patterns

Move tables between clusters: GitHub

GitHub split one MySQL cluster into schema domains, then moved tables to separate clusters with Vitess and a write cutover.

GitHub.com ran on one MySQL cluster called mysql1 for about a decade. By 2019 the cluster answered close to a million queries a second, and every feature shared the same tables. The team split the code first: tables went into named schema domains, and a linter failed a build when a query crossed a domain boundary. Only then did the tables move.

The system#

The move used two independent methods. Vitess put a proxy in front of MySQL and copied whole tables between clusters in the background. A custom write cutover rode on ordinary MySQL replication and ProxySQL, and the read-only window lasted tens of milliseconds. By 2021 the same tables answered 1.2 million queries a second across several clusters, and the average load on each host had halved.

The board#

Drag to pan. Hold Ctrl or Cmd and scroll to zoom.

At 4.00k Users the Simulation solves this Design to Apdex 1.00, p99 211 ms, 0.0% errors and $31/s Burn. The board is the split at game scale. Vitess and the schema linters that made the split safe are not on it. The caption is Quorum's result, not the company's numbers.
WhereSettingValueWhy
Load BalancerPolicyleast-connThe load balancer sends each request to the cluster that owns its data.
App ServerInstance sizelargeLarge, because the core application keeps the busiest tables.
App ServerCopies3Three copies stand for the app tier over the core cluster.
App ServerInstance sizemediumMedium, because the feature domain is smaller than the core.
App ServerCopies2The feature domain scales on its own.
Connection PoolerPooler modetransactionA connection is lent for one transaction, so many app connections share a few. ProxySQL does this at GitHub.
Connection PoolerPooler modetransactionEach cluster gets its own pool, as each domain gets its own cluster.
DatabaseInstance sizelargeLarge, because the core tables still take most queries.
DatabaseEnginemysqlMySQL, the engine GitHub runs.
DatabaseInstance sizemediumMedium, sized for the feature domain alone.
DatabaseEnginemysqlMySQL, the engine GitHub runs.
Read ReplicaInstance sizelargeMatch the primary, so the copy takes an equal share of reads.
Link Load Balancer → App ServerKinds carriedwrite,loginWrites and logins go to the cluster that owns the core tables.
Link Load Balancer → App ServerKinds carriedpage,readPages and reads go to the feature domain, which the split made independent.

Research this Design needs: Load balancing, Connection pooling, Read replicas, Connection pooling, Read replicas, Sharding.

The two App Servers are the two schema domains, core and feature. Each has its own Connection Pooler, which stands for ProxySQL, and its own Database. The Read Replica is one of the read copies that took load off the primary. The Load Balancer's kinds stand for the domain split, not a real read/write split.

Why it works#

The split is virtual before it is physical. Once the code cannot join across a domain, the tables can move one domain at a time, and a mistake stays inside one cluster. Split the database by feature and expand and contract are the two steps that make the move safe.

Where the game stops#

The game's Connection Pooler is PgBouncer, not Vitess or ProxySQL, and it cannot route a query by table. The board shows two domains and two databases, not the linters, the VTGate processes, or the 130 tables that moved in one cutover. The caption is Quorum's result, not GitHub's query rate.

Sources#