Adding a Patroni node that catches up from the WAL archive (`nostream`)
Overview
Section titled “Overview”Use this runbook when you add a node to an existing Patroni cluster and that node will spend a long time replaying WAL from the wal-g archive before it can stream from the leader. Typical cases:
- a new backup / LVM node whose data volume is restored with
wal-g backup-fetch(many hours for the main and ci clusters) - a replacement replica seeded from an old disk snapshot that has a lot of WAL to replay before it is current
For a plain read replica seeded from a fresh snapshot, Scale Up Patroni is enough. DR archive/delayed replicas are their own Patroni scope and do not create slots on the production leader, so this runbook does not apply to them (see build_cluster_from_snapshot.md).
By default Patroni creates a physical replication slot on the leader for every
member as soon as the member registers in DCS. While the new node is replaying
from the archive it never connects to that slot, so the slot is inactive and the
leader keeps every WAL segment for it. During
INC-12794 the slot for
patroni-main-v17-102 retained about 4 TB of WAL during a 17 h backup-fetch
and pushed the gprd main leader’s disk towards capacity.
The <env>-base-db-patroni-nostream overlay role fixes this by setting the
Patroni nostream tag on the node from its very first chef run. Patroni skips
nostream members when it reconciles slots, so no slot exists on the leader
until the node is caught up and the role is removed.
The behaviour was validated end to end on patroni-pmtest-v17 (db-benchmarking);
evidence is on production-engineering#29507.
What the role does
Section titled “What the role does”roles/<env>-base-db-patroni-nostream.json in chef-repo (exists for
db-benchmarking, gstg and gprd) sets three Patroni tags:
| Tag | Effect while the role is applied |
|---|---|
nostream: true | The leader creates no replication slot for this member. The node recovers through restore_command (wal-g wal-fetch) instead of streaming. |
nofailover: true | The node cannot be promoted while it is behind. |
noloadbalance: true | The Patroni /replica endpoint answers 503, so Consul keeps the node out of the read pool. |
The tags are reload-safe. Removing the role and running chef-client rewrites
patroni.yml and reloads Patroni (no restart). Patroni then creates the slot,
PostgreSQL switches from restore_command to streaming, and the noloadbalance
tag goes away with the role.
Hard rules
Section titled “Hard rules”- The role has to be in the run_list from the first chef run. Chef starts
Patroni on first boot. If the role is added afterwards with
knife, Patroni comes up without the tag and the slot appears on the leader immediately. The role therefore goes into the node’schef_run_list_extrain the config-mgmt MR that creates the node, never as a follow-up. - Never pause the cluster while a member is away. A paused leader does not
reconcile slots. During validation a slot on a paused leader kept growing after
its member was stopped and was only dropped 12 s after
patronictl resume. CheckMaintenance modebefore you start and before go-live. - Keep
chef-clientdisabled while Patroni is stopped on the node. Chef’s systemd reload starts an inactivepatroniunit. With an empty data directory that means Patroni bootstraps the node withpg_basebackupfrom the leader.
Pre-checks
Section titled “Pre-checks”Run on the current leader of the target cluster.
-
The cluster is not paused.
Terminal window sudo gitlab-patronictl listYou should see the member table and no
Maintenance mode: online. -
No slot already exists for the node you are about to add. Slot names are the node FQDN with
.and-replaced by_, cut to 63 characters, for examplepatroni_main_v17_103_db_gprd_c_gitlab_production_internal.Terminal window sudo gitlab-psql -c "SELECT slot_name, slot_type, active, wal_status, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained FROM pg_replication_slots ORDER BY 1;"You should see the existing members and Siphon slots only. If a stale slot for the new node is there (for example from an earlier attempt), drop it before continuing:
Terminal window sudo gitlab-psql -c "SELECT pg_drop_replication_slot('<slot_name>');"sudo gitlab-psql -c "CHECKPOINT;" -
Confirm which PostgreSQL major the archive backups were taken with, so the role,
data_directoryandbin_directoryon the new node match the backup and not thevNNin a role filename:Terminal window sudo -u gitlab-psql /usr/bin/envdir /etc/wal-g.d/env /opt/wal-g/bin/wal-g backup-list --detail | tail -3
Step 1: Terraform MR, add the node with the role attached
Section titled “Step 1: Terraform MR, add the node with the role attached”In config-mgmt, add the node to the cluster’s nodes map in
environments/database-<env>/patroni-<cluster>.tf and put the nostream role in
its chef_run_list_extra. Overlay roles are comma separated inside the quoted
string.
Backup / LVM node (keeps the cluster’s -backup-replica overlay as well):
103 = { machine_type = var.machine_types["patroni-c4-highmem-96"] chef_run_list_extra = "\"role[${var.environment}-base-db-patroni-main-v17-backup-replica],role[${var.environment}-base-db-patroni-nostream]\"" additional_labels = { shard = "backup" }}Plain replica that will replay a long backlog:
12 = { chef_run_list_extra = "\"role[${var.environment}-base-db-patroni-nostream]\"" zone = "us-east1-b"}Merge and apply through Atlantis as part of the CR. The plan must show exactly one instance added and nothing replaced.
Step 2: After bootstrap, confirm the tags landed
Section titled “Step 2: After bootstrap, confirm the tags landed”Wait for the node to register in Chef and for the first chef run to finish
(gcloud compute instances tail-serial-port-output <instance> --zone <zone> --port 1
shows the chef run).
-
On your workstation, the run_list carries the role:
Terminal window knife node show <node-fqdn> -rYou should see
role[<env>-base-db-patroni-nostream]in the run_list. -
On the new node, the tags are in the Patroni config:
Terminal window sudo grep -A5 '^tags:' /var/opt/gitlab/patroni/patroni.ymlYou should see
nostream: true,nofailover: trueandnoloadbalance: true. -
On the leader, still no slot for the node:
Terminal window sudo gitlab-psql -c "SELECT slot_name, active FROM pg_replication_slots WHERE slot_name LIKE 'patroni_%' ORDER BY 1;"
If the tags are missing, stop here and fix the run_list before Patroni runs without them (see Troubleshooting).
Step 3: Get the data directory in place
Section titled “Step 3: Get the data directory in place”Which variant applies depends on how the node was provisioned.
Variant A: data disk seeded from a snapshot
Section titled “Variant A: data disk seeded from a snapshot”Patroni started on first boot, found a data directory and is replaying WAL from the archive. Nothing to do here, continue with Step 4.
Variant B: empty data volume (LVM backup nodes)
Section titled “Variant B: empty data volume (LVM backup nodes)”The data volume is empty, so the base backup has to be fetched by hand before Patroni is allowed to run. Do all of this on the new node.
-
Stop chef and Patroni:
Terminal window sudo chef-client-disable "<CR or issue link>"sudo systemctl stop patronisudo systemctl is-active patroniYou should see
inactive. -
Make sure the data directory is empty. The path follows the PG major, for example
/var/opt/gitlab/postgresql/data17.Terminal window sudo -u gitlab-psql bash -c 'ls -A /var/opt/gitlab/postgresql/data17 | wc -l'You should see
0. If Patroni already started apg_basebackupand left files behind, clear them. Run the removal insidebash -casgitlab-psql; a plainsudo -u gitlab-psql rm -rf <dir>/*expands the glob as your own user, which cannot read the directory, and silently removes nothing.Terminal window sudo -u gitlab-psql bash -c 'rm -rf /var/opt/gitlab/postgresql/data17/*'sudo -u gitlab-psql bash -c 'ls -A /var/opt/gitlab/postgresql/data17 | wc -l' -
Fetch the base backup. This takes hours (about 17 h for the 60 TiB main cluster), so run it in
tmuxand log the output.Terminal window sudo -u gitlab-psql /usr/bin/envdir /etc/wal-g.d/env /opt/wal-g/bin/wal-g backup-listTerminal window sudo -u gitlab-psql /usr/bin/envdir /etc/wal-g.d/env /opt/wal-g/bin/wal-g backup-fetch /var/opt/gitlab/postgresql/data17 LATEST 2>&1 | tee /tmp/wal-g-backup-fetch.logYou should see
Backup extraction complete(or the wal-g equivalent for the installed version) at the end of the log and a populatedPG_VERSIONfile in the data directory. -
Start Patroni and re-enable chef. The
nostreamtag is already inpatroni.yml, so Patroni starts PostgreSQL in archive recovery and the leader creates no slot.Terminal window sudo systemctl start patronisudo chef-client-enablesudo gitlab-patronictl listYou should see the new member with state
in archive recovery(Patroni 3.x) and its tags in theTagscolumn.
Step 4: Monitor the catch-up
Section titled “Step 4: Monitor the catch-up”Expect this to take from minutes (snapshot seeded) to many hours (main/ci backup nodes). Silence the replication-lag alerts for the new node for the expected window, see database_alert_management.md.
On the new node:
sudo gitlab-patronictl listLag in MB should be falling. The state stays in archive recovery the whole
time; that is expected with nostream.
sudo tail -n 20 /var/log/gitlab/postgresql/postgresql.csv | grep -E 'restored log file|started streaming'You should see restored log file "..." from archive lines and no
started streaming WAL line yet.
On the leader, every hour or so:
sudo gitlab-psql -c "SELECT slot_name, active, wal_status, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained FROM pg_replication_slots ORDER BY 1;"sudo gitlab-psql -c "SELECT application_name, state FROM pg_stat_replication ORDER BY 1;"The new node must appear in neither result. If a slot for it shows up, go to Troubleshooting.
Step 5: Go-live
Section titled “Step 5: Go-live”Gate: Lag in MB on the new node is in the low single-digit GB or less and the
leader is not paused (sudo gitlab-patronictl list shows no Maintenance mode
line). Going live with a large lag is safe for the leader but the node will hold
the slot open while it finishes catching up over streaming.
-
Note the Patroni PID so you can confirm the switch was a reload. On the new node:
Terminal window systemctl show patroni -p MainPID -
Remove the role. On your workstation:
Terminal window knife node run_list remove <node-fqdn> "role[<env>-base-db-patroni-nostream]"knife node show <node-fqdn> -rYou should no longer see the
nostreamrole. Any other overlays (-backup-replica,-nofailover) stay. -
Apply it. On the new node:
Terminal window sudo chef-clientsudo grep -A5 '^tags:' /var/opt/gitlab/patroni/patroni.ymlsystemctl show patroni -p MainPIDYou should see
nostreamgone from the tags and the sameMainPIDas in step 1 (reload, not restart). For a backup nodenofailoverandnoloadbalanceremain because the-backup-replicaoverlay sets them. -
Confirm streaming started. On the new node:
Terminal window sudo grep -E 'started streaming WAL' /var/log/gitlab/postgresql/postgresql.csv | tail -1sudo gitlab-patronictl listYou should see one
started streaming WAL from primary at ...line with a timestamp after the chef run, and the member state changing fromin archive recoverytostreamingwithLag in MBdropping to 0. During validation the switch happened within one Patroni loop (about 10 s) and the remaining lag cleared in about 2 minutes. -
Confirm the slot. On the leader:
Terminal window sudo gitlab-psql -c "SELECT slot_name, active, wal_status FROM pg_replication_slots WHERE slot_name LIKE 'patroni_%' ORDER BY 1;"sudo gitlab-psql -c "SELECT application_name, state, sync_state FROM pg_stat_replication ORDER BY 1;"You should see the node’s slot with
active = tandwal_status = reserved, and the node inpg_stat_replicationwithstate = streaming. -
Confirm read-pool membership matches the intent. On the new node:
Terminal window curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8009/replicadig @127.0.0.1 -p 8600 <read-service>.service.consul. SRVFor a plain replica you should see
200and the node in the SRV answer. For a backup node you should see503and the node absent, because-backup-replicakeepsnoloadbalance. The read service name is in the base role’sconsul.services(for exampledb-replicafor main,ci-db-replica-v18for ci).
Step 6: Terraform clean-up MR
Section titled “Step 6: Terraform clean-up MR”chef_run_list_extra is only read at bootstrap, so the node is already correct.
Remove role[${var.environment}-base-db-patroni-nostream] from that node’s entry
in config-mgmt so Terraform matches the live run_list, the same way the
long-term maintenance procedure in patroni-management.md
reverts its role. The Atlantis plan must show an in-place metadata update only;
if it shows a replace, stop and ask in #g_database_operations.
Troubleshooting
Section titled “Troubleshooting”A slot for the node appeared on the leader while it was still recovering
Section titled “A slot for the node appeared on the leader while it was still recovering”The tags did not make it into patroni.yml before Patroni started (rule 1). Fix
the run_list with knife node run_list add <node-fqdn> "role[<env>-base-db-patroni-nostream]"
and run sudo chef-client on the node. Patroni reloads with nostream and the
leader drops the slot within one loop; during validation the drop took under 10 s.
If the node is going to be down for a long time and the slot is already large,
drop it by hand on the leader:
sudo gitlab-psql -c "SELECT pg_drop_replication_slot('<slot_name>');"sudo gitlab-psql -c "CHECKPOINT;"No slot appeared after go-live
Section titled “No slot appeared after go-live”Patroni caches the slots it manages. If a slot for this member was dropped by hand earlier while the leader’s Patroni still had it in its cache, Patroni may not recreate it. Create it on the leader; Patroni adopts it on the next loop:
sudo gitlab-psql -c "SELECT pg_create_physical_replication_slot('<slot_name>');"The cluster is paused
Section titled “The cluster is paused”Resume it before doing anything else (sudo gitlab-patronictl resume --wait <scope>).
A paused leader does not create or drop member slots.
rm -rf on the data directory removed nothing
Section titled “rm -rf on the data directory removed nothing”The glob expanded as your user. Use sudo -u gitlab-psql bash -c '...' as shown
in Step 3.
References
Section titled “References”- Patroni tags: https://patroni.readthedocs.io/en/latest/yaml_configuration.html#tags
- Related runbooks: Scale Up Patroni, Patroni management, wal-g backups