feat: ADD SHARD command plus lookup_query for fixed shard mapping - #1300
Draft
rlittlefield wants to merge 35 commits into
Draft
feat: ADD SHARD command plus lookup_query for fixed shard mapping#1300rlittlefield wants to merge 35 commits into
rlittlefield wants to merge 35 commits into
Conversation
…atency metric in ms
…-sharding omni fix, bigger debug-build stacks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
ADD SHARD: orchestrated shard provisioning with WAL catch-up
Base: fixed shard placement
This branch includes the work for the lookup_query (hashed), and also adds support
for fixed shard mapping based on lookup_query:
lookup_queryon[[sharded_tables]]routes queries by asking thedatabase. pgdog runs the query with the sharding key as
$1, lazily andper key, and caches the result. The cache and query timeout are tuned
with
sharding_lookup_cache_sizeandsharding_lookup_timeoutin[general].lookup_result = "shard"means the query returns the 0-based shardnumber itself. The application owns placement, for example a permanent
shard_idcolumn on its tenants table. Placement is stored, not derivedfrom a hash, so the shard count can change without moving any rows.
Stored placement is what makes growing a cluster in place safe. This PR
automates the growing.
What this PR adds
An
ADD SHARDadmin command (and apgdog add-shardCLI) that grows acluster by one shard end to end while production traffic keeps flowing.
You declare each future shard once, in its final shape. The
provisioningflag keeps it out of the serving topology until its cutoveractivates it:
You create an empty database on the new server, deploy the entry, and
run the command. The database's existing
schema_adminuser covers thenew shard. pgdog does the rest:
placement-stable, meaning
lookup_result = "shard"or an explicitmapping. Hash-routed tables need a real reshard, because adding a shard
would move their rows. The task also refuses when the named shard has
no provisioning entry, when it isn't the next shard number (declared
shards are added in order, one ADD SHARD each), when the new shard
isn't empty, when another topology change for the database is already
running, and when another pgdog instance holds the provisioning lock.
That lock is a session-scoped advisory lock taken on the new shard
itself, so a crashed holder releases it automatically.
DDL onto the new shard.
using the resharding data-sync machinery, then WAL-streamed until
caught up. A publication scoped to the omnisharded tables is created
automatically and dropped when the task ends. Pass one explicitly to
manage it yourself. A database with no omnisharded tables skips this
phase and the cutover's write pause entirely: the task syncs schema,
parks, and activates.
reads flow throughout. The task drains replication to zero, flips the
provisioningflag off in the running config, reloads, refreshespgdog.configon every shard, persists the files whencutover_save_configis on, and resumes omni writes. The pause is thedrain plus coordination, typically around a second.
Progress is visible in
SHOW TASKS(validating, syncing schema, syncingdata, replicating, awaiting cutover, draining, swapping topology).
STOP_TASKaborts cleanly any time before the swap: slots and theauto-created publication are dropped, and the topology never changed. The
swap is the point of no return. Everything after it is best-effort, and
each failure warns with its recovery step.
Sequence
sequenceDiagram autonumber actor Op as You participant App as Application participant PD as pgdog running ADD SHARD participant Fleet as Other pgdog instances participant S0 as Shard 0 participant NEW as New shard participant ALL as All shards Note over Op,NEW: MANUAL - create empty DB, deploy the provisioning entry, RELOAD Op->>PD: ADD SHARD prod 2 PD->>PD: guards - placement stable, shard 2 is next, new shard empty PD->>NEW: take the cross-instance advisory lock PD->>S0: create publication for omni tables PD->>S0: pg_dump schema only PD->>NEW: apply all DDL with idempotent restore PD->>S0: create durable replication slot at L0 loop each omni table PD->>S0: temp slot with snapshot, COPY PD->>NEW: rows consistent at Lt after L0 end PD->>S0: start replication from L0 loop until lag under threshold S0-->>PD: WAL omni changes PD->>NEW: apply with upsert and per table LSN dedup App->>ALL: traffic flows normally throughout end Note over PD: status awaiting cutover - WAL retained on shard 0 while parked Op->>PD: CUTOVER SHARD prod 2 PD->>PD: pause local omni writes PD->>NEW: publish armed state Fleet-->>NEW: see armed, pause omni writes, ack Note over App,Fleet: omni writes park everywhere - reads and sharded traffic keep flowing S0-->>PD: in flight omni writes drain to zero lag PD->>S0: stop stream and drop slots PD->>PD: POINT OF NO RETURN - provisioning flag flips off, registry reloads with 3 shards PD->>PD: resume local omni writes, persist config if enabled PD->>ALL: refresh pgdog.config rows on every shard PD->>NEW: publish activated state Fleet-->>NEW: see activated, activate shard 2, resume omni writes, ack PD->>S0: drop publication, also on new shard App->>ALL: omni writes now reach all three shards Note over Op,App: MANUAL - remove the provisioning flag from the config source, app assigns tenants to shard 2Config as state
The
provisioning = trueflag makes the config declarative rather thanstateful. The entry already describes the end state, and activation only
deletes one line. When the config comes from a manifest or template
(GitOps), you remove the flag at the source at your leisure.
cutover_save_configcovers file-managed setups.The database records the truth either way: the cutover writes
(shard = N, shards = N + 1)into the new shard's ownpgdog.config, andconvergence reads it back at startup (before the listener opens) and after
every RELOAD. A restart with a stale manifest re-activates the shard
before serving a single query.
Multiple pgdog instances
The cutover coordinates a fleet, using the new shard itself as the shared
medium. Every instance that has the entry can reach it, and it serves
nothing else.
pgdog.instanceson shard 0. That table isthe fleet registry, installed by
SETUP SCHEMAand by the topologytasks. Graceful shutdowns (including SIGTERM) deregister the instance,
so rolling restarts don't disturb a cutover.
next declared shard. The agent registers on that shard and polls a
state row there once a second.
shard, naming it in the error. That instance is running a config without
the entry, and the fix is deploying the config everywhere first.
the omni-write barrier locally, publishes
armed, and waits for everypeer's ack. It drains to zero, swaps, resumes its own writes, and
publishes
activated. Peers activate, resume writes, and ack. Abortspublish
released.5 seconds for as long as it holds the fleet's barriers, and agents
resume writes only when that heartbeat goes silent for 30 seconds, which
means the coordinator actually died. A dead but recently seen peer times
out the arm phase and the task re-parks for a retry. An instance that
missed the activation keeps its omni writes parked until it learns the
outcome, and a crashed one converges at restart before serving. There is
no path that writes with a stale topology.
Failure and crash behavior
Everything before the swap aborts cleanly and reruns from scratch.
Per-table copy slots are temporary. The durable slot and the auto-created
publication are dropped by cleanup guards. The omni-write barrier and the
provisioning cluster (whose session holds the advisory lock) are drop
guards, so even a hard-aborted cancellation releases them. After the swap,
a crash before the
pgdog.configrefresh is recovered withSETUP SCHEMA.Once the marker is written, restarts converge on their own. There is no
rollback after the swap, because once lookups assign a tenant to the new
shard, removing it would strand data.
Notes for review
cutover_timeoutandcutover_timeout_action. Withabort, the task releases the barriers and re-parks at "awaitingcutover" instead of failing, so the cutover can be retried.
SHOW REPLICATION_SLOTS, so don't park indefinitely.liveness window 15s, heartbeat 5s) are code constants for now. Promoting
them to config is a reasonable follow-up if fleets need it.
listener to replace the agents' poll, and a
SHOW INSTANCEScommandover the registry.
integration/add_shard/run.shcovers a parkedcutover with a live omni writer, auto cutover, abort mid-task, the
cross-instance lock, convergence after a restart with a stale flag, a
two-instance coordinated cutover, a dead-peer refusal, and a
schema-only run without omnisharded tables.