| title | Backing up a Patroni cluster | |||
|---|---|---|---|---|
| description | Configure pg_hardstorage against a 3-node Patroni cluster — slot continuity across failover, leader-aware backup routing. | |||
| tags |
|
Wires
pg_hardstorageto a 3-node Patroni cluster: one primary, two replicas, leader changes happen, the WAL stream continues without gaps. The agent watches Patroni's REST API, follows the leader, and recreates its replication slot on the new primary at the right LSN. About 25 minutes against a Docker-based Patroni stack.
The contract: the agent talks to one Patroni-managed PG cluster as a single logical deployment. WAL streaming routes to the current leader; on a leader change the streamer reconnects against the new primary's slot at the saved LSN, and any cross-timeline gap is recorded as a structured audit event so you can reason about RPO afterwards.
- A 3-node Patroni cluster reachable from the operator host. The
patroni/patroniDocker examples or any of the published Compose stacks work; for a quick local stand-up, use Patroni's reference stack. - The Patroni REST URL — usually
http://patroni-leader:8008for the reference Compose. Read endpoints (/cluster,/leader,/history) need no auth by default. - A replication user (
CREATE ROLE pgbackup REPLICATION LOGIN ...) with apg_hba.confline that admits the operator host on every member. Patroni'sbootstrap.pg_hbablock applies this fleet-wide.
Before any backup config, prove you can talk to Patroni:
# RUNNABLE skip-in-ci="needs Patroni cluster"
pg_hardstorage patroni status \
--url http://patroni-leader:8008patroni cluster "patroni-tutorial"
Leader: patroni-1 (TLI 3)
NAME ROLE STATE HOST PORT TLI LAG
patroni-1 Leader running 10.0.0.11 5432 3 0
patroni-2 Replica streaming 10.0.0.12 5432 3 0
patroni-3 Replica streaming 10.0.0.13 5432 3 0patroni history prints the timeline-history events that record
every promotion. The agent's leader-follow coordinator consumes those
events when it reconnects after a failover.
# RUNNABLE skip-in-ci="needs Patroni cluster"
pg_hardstorage repo init file:///srv/hs-patroni-repoFor Patroni clusters, write the connection string against the Patroni REST URL's leader endpoint rather than a single member — the agent rewrites the host every time the leader moves. The simplest form just points at the leader at config-time and lets the follow-loop take over:
# RUNNABLE skip-in-ci="needs Patroni cluster"
pg_hardstorage init \
--pg-connection 'postgres://pgbackup@patroni-leader.example.com/postgres' \
--repo file:///srv/hs-patroni-repo \
--deployment pg-tutorial \
--skip-backup \
--yespg_hardstorage init writes the deployment without Patroni
awareness. Either run
pg_hardstorage deployment edit pg-tutorial \
--patroni-url http://patroni-leader:8008 \
--patroni-slot pg_hardstorage_pg_tutorial \
--patroni-interval 5s…or hand-edit pg_hardstorage.yaml (path printed by
pg_hardstorage doctor) and add a patroni: block:
deployments:
pg-tutorial:
pg_connection: postgres://pgbackup@patroni-leader.example.com/postgres
repo: file:///srv/hs-patroni-repo
patroni:
url: http://patroni-leader:8008
slot: pg_hardstorage_pg_tutorial # explicit; default is auto-derived
interval: 5s # poll cadence; matches Patroni's default leader TTL
# user: opspg # optional HTTP basic auth
# password: ...pg_hardstorage deployment list shows the configured patroni-url,
slot, and interval so you can verify the block landed.
The patroni.url activates the agent's leader-follow coordinator.
With this block in place, every WAL streaming or backup run begins
with a Patroni leader probe and routes to the current primary —
not the host baked into pg_connection.
For a long-lived setup you would run the agent under systemd:
pg_hardstorage agentThe agent spawns one Patroni follower coordinator per Patroni-enabled
deployment. A misconfigured Patroni URL on one deployment is
non-fatal — the follower for that deployment is dropped, the agent
emits patroni.startup_partial, and the rest of the fleet keeps
running.
For the tutorial, run the streamer directly so you can see the events:
pg_hardstorage wal stream pg-tutorial \
--pg-connection 'postgres://pgbackup@patroni-leader.example.com/postgres' \
--repo file:///srv/hs-patroni-repo# RUNNABLE skip-in-ci="needs Patroni cluster"
pg_hardstorage backup pg-tutorial \
--pg-connection 'postgres://pgbackup@patroni-leader.example.com/postgres' \
--repo file:///srv/hs-patroni-repoThe base backup connects to the endpoint you give it. To take it from
a replica (to keep primary I/O free), point --pg-connection at a
replica's host — there is no automatic replica routing.
In a Patroni-shell terminal, force a manual failover:
patronictl -c /etc/patroni.yml failover --master patroni-1 --candidate patroni-2In a third terminal, follow the Patroni leader stream:
pg_hardstorage patroni follow \
--url http://patroni-leader:8008patroni follow streams a patroni.leader_change event each time the
leader changes (and patroni.poll_error on a transient REST error);
each event carries the old and new leader's name and host:port. In
text mode it renders through the generic event renderer:
11:03:42 [NOTICE] patroni.leader_changeIn the wal stream terminal you will see the follower coordinator
react. It emits events under the wal.follower namespace — the ones
that matter on a failover:
wal.follower.leader_change— the coordinator saw the new leader;wal.follower.slot_reconciled— the physical slot was found (or recreated) on the new leader;wal.follower.wal_gap_detected— critical, emitted only if the reconnect could not bridge the WAL (e.g. the slot was lost and recreated past the last archived LSN).
11:03:42 [NOTICE] wal.follower.leader_change
11:03:43 [NOTICE] wal.follower.slot_reconciledThe agent recreated its replication slot on the new primary at the saved LSN (or the lowest available LSN, whichever is more conservative), and recorded the cross-timeline gap as a structured audit event. You can list gaps later with:
pg_hardstorage wal gaps pg-tutorial \
--repo file:///srv/hs-patroni-repopg_hardstorage status rolls up the picture:
pg_hardstorage status pg-tutorialpg-tutorial PG 17.x primary @ 10.0.0.12:5432 (Patroni leader)
Last backup 8m ago · full · 1.2 GB · ✓ verified
WAL streaming active · lag 4s · slot pg_hardstorage_pg_tutorial
Patroni 3 members · TLI 4 · last failover 2m ago
RPO / RTO 8m / ~3m (estimate)
Health ✓ all clearYou configured one logical deployment against a real 3-node Patroni cluster, exercised a failover, and watched the agent rebind to the new primary without operator intervention. The two non-trivial pieces of the design that earned their keep:
- Patroni reuse, not duplication. The agent reads Patroni's REST
state and writes its own coordination keys under
/pg_hardstorage/<deployment>/...in the existing DCS (etcd, Consul, ZooKeeper — whatever Patroni already runs). No second DCS, no coordination tax. - The slot is the source of truth. PG retains WAL until the slot ACKs; the agent only ACKs after a segment commits in the repo. Combine that with the leader-follow coordinator and the gap window shrinks to "the seconds between the old primary's last commit and the new primary's promotion" — which is precisely the cross- timeline gap event the audit log records.
- Kubernetes & CloudNativePG — same agent, per-Pod sidecar, CNPG-I provider.
- R7 — Patroni split-brain — what to do when two members both think they are primary.
- R6 — Slot dropped, gap detected — diagnosing and repairing a WAL gap.
- Operator guide — Restore —
restoring from a Patroni-sourced backup is identical to a
single-host one; PITR uses the same
--tosemantics.