Chapter 15 · Galera Cluster: Synchronous Multi-Primary Replication and High Availability

Bootstrap, Join, State Snapshot Transfer, Incremental State Transfer, and Donor Selection

Operate MariaDB Galera bootstrap and node rejoin safely, distinguish SST from IST using GCache evidence, control donor selection, and recover a full outage from the most advanced state instead of arbitrarily declaring a node authoritative.

Advanced155–195 minutesBootstrap + IST/SST + donor labMariaDB Community 12.3.2 + Galera 4 baselineCurriculum anchor: 11.8 LTS · verify server/provider/SST versionsLinux nodes/containers · Last reviewed: August 2026

Learning outcomes

A Galera node that restarts is not useful merely because mariadbd started. It must recover the exact cluster state it missed. If a donor still has the missing write sets in its GCache (Galera write-set cache), the node can perform an Incremental State Transfer (IST). If the gap is not recoverable from GCache—or the joiner has no compatible state—it needs a full State Snapshot Transfer (SST). Choosing the wrong node to bootstrap after a complete outage is more dangerous than either transfer: it can intentionally declare stale data authoritative.

01

Bootstrap a brand-new cluster exactly once, then distinguish ordinary join/rejoin from cluster creation.

02

Explain donor, joiner, SST, IST, GCache, grastate.dat, safe_to_bootstrap, and recovered sequence position.

03

Observe an IST after a short outage and an SST after recreating an empty joiner.

04

Use wsrep_sst_donor and status/log evidence to reason about donor choice without assuming a fixed donor.

05

Recover the operating model from a full outage by selecting the most advanced safe node rather than bootstrapping arbitrarily.

Lab continuity

This lesson recreates the three-node Community lab from scratch so it does not depend on Lesson 1 state. It uses the free rsync SST method to keep credentials simple. Production environments often choose mariabackup because donor behavior and operational characteristics differ; verify the exact SST method, package, authentication privileges, TLS, and major-version compatibility before deployment.

1. Bootstrap, join, and rejoin are three different operations

Operation What the node assumes Correct mechanism
brand-new cluster bootstrap no authoritative component exists yet one node starts with --wsrep-new-cluster / galera_new_cluster
join an authoritative Primary Component already exists normal startup with wsrep_cluster_address pointing at seed members
rejoin node has prior state and must catch up normal startup; Galera selects IST or SST
full-outage recovery no Primary Component exists, but old data exists identify most advanced node, then bootstrap only that node
Wrong approach

Do not “try bootstrap on each node until one works.” Each bootstrap is a declaration of authority. The safe choice after a full outage comes from state evidence—grastate.dat, safe_to_bootstrap, and when necessary --wsrep-recover—not from which service starts fastest.

2. Build the disposable cluster

yaml · compose.yaml
services:  n1:    image: mariadb:12.3.2    container_name: mdb15-n1    environment:      MARIADB_ROOT_PASSWORD: labroot    command:      - --bind-address=0.0.0.0      - --binlog-format=ROW      - --default-storage-engine=InnoDB      - --innodb-autoinc-lock-mode=2      - --wsrep-on=ON      - --wsrep-provider=/usr/lib/galera/libgalera_smm.so      - --wsrep-cluster-name=servicehub15      - --wsrep-cluster-address=gcomm://n2,n3      - --wsrep-node-name=n1      - --wsrep-sst-method=rsync      - --wsrep-new-cluster    ports:      - "33151:3306"  n2:    image: mariadb:12.3.2    container_name: mdb15-n2    environment:      MARIADB_ROOT_PASSWORD: labroot    command:      - --bind-address=0.0.0.0      - --binlog-format=ROW      - --default-storage-engine=InnoDB      - --innodb-autoinc-lock-mode=2      - --wsrep-on=ON      - --wsrep-provider=/usr/lib/galera/libgalera_smm.so      - --wsrep-cluster-name=servicehub15      - --wsrep-cluster-address=gcomm://n1      - --wsrep-node-name=n2      - --wsrep-sst-method=rsync    ports:      - "33152:3306"  n3:    image: mariadb:12.3.2    container_name: mdb15-n3    environment:      MARIADB_ROOT_PASSWORD: labroot    command:      - --bind-address=0.0.0.0      - --binlog-format=ROW      - --default-storage-engine=InnoDB      - --innodb-autoinc-lock-mode=2      - --wsrep-on=ON      - --wsrep-provider=/usr/lib/galera/libgalera_smm.so      - --wsrep-cluster-name=servicehub15      - --wsrep-cluster-address=gcomm://n1      - --wsrep-node-name=n3      - --wsrep-sst-method=rsync    ports:      - "33153:3306"
shell · fresh bootstrap and join
docker compose down -v --remove-orphans# Bootstrap exactly one node for a brand-new disposable cluster.docker compose up -d n1# Wait until n1 reports wsrep_ready=ON, then join the other nodes.docker compose up -d n2 n3# Verify all three nodes before creating application data.docker exec mdb15-n1 mariadb -uroot -plabroot -e "SHOW GLOBAL STATUS LIKE 'wsrep_cluster_size'; SHOW GLOBAL STATUS LIKE 'wsrep_cluster_status';"docker exec mdb15-n2 mariadb -uroot -plabroot -e "SHOW GLOBAL STATUS LIKE 'wsrep_local_state_comment';"docker exec mdb15-n3 mariadb -uroot -plabroot -e "SHOW GLOBAL STATUS LIKE 'wsrep_local_state_comment';"
sql · n1 — create a small replicated workload
CREATE DATABASE IF NOT EXISTS servicehub_galera_lab;CREATE TABLE IF NOT EXISTS servicehub_galera_lab.tickets (  ticket_id BIGINT PRIMARY KEY AUTO_INCREMENT,  customer_id BIGINT NOT NULL,  state VARCHAR(24) NOT NULL,  priority INT NOT NULL DEFAULT 0,  updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)             ON UPDATE CURRENT_TIMESTAMP(6)) ENGINE=InnoDB;CREATE TABLE IF NOT EXISTS servicehub_galera_lab.counters (  counter_name VARCHAR(64) PRIMARY KEY,  counter_value BIGINT NOT NULL) ENGINE=InnoDB;INSERT INTO servicehub_galera_lab.counters(counter_name,counter_value)VALUES ('dispatch',0)ON DUPLICATE KEY UPDATE counter_value=VALUES(counter_value);INSERT INTO servicehub_galera_lab.tickets(customer_id,state,priority) VALUES (301,'open',1),(302,'open',2);
sql · all nodes — baseline transfer evidence
SHOW GLOBAL STATUS WHERE Variable_name IN ('wsrep_local_state_comment','wsrep_local_cached_downto','wsrep_last_committed','wsrep_cluster_size');

wsrep_local_cached_downto is the lowest sequence number the local GCache can currently serve for IST. GCache sizing is workload/time-window dependent; do not convert a default size into a universal “hours of outage” promise.

3. Short outage: observe IST

Stop n3 but keep its container filesystem. Advance the cluster from n1 with a modest number of transactions so the donor's GCache still contains the gap.

shell · temporarily stop n3
docker stop mdb15-n3
sql · n1 — create a recoverable gap
INSERT INTO servicehub_galera_lab.tickets(customer_id,state,priority)SELECT 400+n, 'queued', MOD(n,5)FROM (SELECT 1 n UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5) AS x;SHOW GLOBAL STATUS LIKE 'wsrep_last_committed';
shell · restart n3 and inspect transfer path
docker start mdb15-n3docker logs --since 2m mdb15-n3 2>&1 | grep -Ei "IST|SST|state transfer|ready" || true
sql · n3 — verify synchronized state
SHOW GLOBAL STATUS WHERE Variable_name IN ('wsrep_local_state_comment','wsrep_ready','wsrep_last_committed');SELECT COUNT(*) AS ticket_count FROM servicehub_galera_lab.tickets;

The expected path is IST if the missing write sets are still in a donor GCache and the cluster state UUID is compatible. The exact log wording is version/provider dependent; the acceptance criterion is that n3 returns to Synced with the same application state.

4. Empty joiner: force the need for SST

An empty node has no prior state to incrementally extend. Recreate n3 without its old container filesystem and start it normally. Galera must obtain a full state before it can become Synced.

shell · recreate n3 as an empty joiner
docker rm -f mdb15-n3docker compose up -d n3docker logs --since 3m mdb15-n3 2>&1 | grep -Ei "SST|state transfer|ready" || true
sql · n3 — prove full-state convergence
SHOW GLOBAL STATUS WHERE Variable_name IN ('wsrep_local_state_comment','wsrep_ready','wsrep_cluster_size');SELECT COUNT(*) AS ticket_count FROM servicehub_galera_lab.tickets;

With rsync, donor availability behavior differs from mariabackup. Production selection must account for database size, donor blocking, transfer bandwidth, encryption, backup-tool version compatibility, and the possibility that a joiner restart triggers a full transfer during peak load.

5. Donor selection is a capacity decision

Galera can choose a donor automatically. wsrep_sst_donor lets a joiner express preferred node names or lists. A preferred donor is not automatically the safest donor: it may be slow, already serving heavy traffic, behind in local apply, or separated by a weak network path.

sql · inspect node identity and transfer readiness
SHOW GLOBAL VARIABLES WHERE Variable_name IN ('wsrep_node_name','wsrep_sst_method','wsrep_sst_donor');SHOW GLOBAL STATUS WHERE Variable_name IN ('wsrep_local_state_comment','wsrep_ready','wsrep_local_recv_queue_avg');

When using mariabackup SST, current MariaDB documentation requires local donor authentication with backup privileges unless the exact product/version provides an alternative automatic mechanism. Community labs should not silently copy Enterprise-only account-management behavior.

6. GCache: outage window, not backup

GCache stores recent write sets so a returning node can request only what it missed. It is operational acceleration, not durable business recovery. It can be too small for a long outage, can be lost or become unavailable, and does not protect against logical mistakes replicated to every node.

Evidence Question answered
wsrep_local_cached_downto how far back can this node potentially serve IST?
wsrep_last_committed what sequence has this node committed most recently?
joiner recovered state UUID/seqno where does this node need to resume?
error log transfer decision did the provider choose IST or SST and why?

7. Full-cluster outage: choose authority from state evidence

After a graceful full shutdown, Galera writes saved state in grastate.dat. The node marked safe_to_bootstrap: 1 is the intended starting point. After a hard crash, all nodes can show unsafe/unknown positions. Then run mariadbd --wsrep-recover on each stopped node and compare recovered sequence positions before selecting the most advanced node.

shell · inspect saved state after stopping the disposable cluster
docker stop mdb15-n1 mdb15-n2 mdb15-n3docker exec mdb15-n1 sh -lc "cat /var/lib/mysql/grastate.dat"docker exec mdb15-n2 sh -lc "cat /var/lib/mysql/grastate.dat"docker exec mdb15-n3 sh -lc "cat /var/lib/mysql/grastate.dat"

If you intentionally practice a full restart, bootstrap only the chosen most-advanced node, then start the remaining nodes normally. Never edit safe_to_bootstrap merely to silence an error without proving why that node is authoritative.

8. Production judgment

  • Provision enough GCache for your measured write-set rate and expected maintenance/outage window, then monitor actual cached range.
  • Prefer IST for routine short outages; plan SST as a normal, resource-heavy fallback rather than an exceptional surprise.
  • Select SST method against exact version/engine/encryption requirements; mariabackup SST has backup-tool compatibility and credential considerations.
  • Document bootstrap authority and rehearse full-cluster recovery. The safest node is determined from state, not hostname conventions such as “node1.”
  • Keep tested Chapter 13 backups even with Galera. A cluster synchronizes state; it does not preserve every historical state you may need.

Next, stress the cluster with conflicting writers and slower apply so you can see certification failures, auto-increment spacing, and flow-control backpressure rather than memorizing them.

Check your understanding

  1. When can a joiner use IST instead of SST?
  2. What does GCache contain, and why is it not a backup?
  3. Why is wsrep_sst_donor a preference rather than proof of a safe donor?
  4. What does safe_to_bootstrap mean after a graceful full shutdown?
  5. What should you do when every node has an uncertain sequence after a hard crash?
Review the answers

IST is possible when the joiner has compatible prior state and a donor still caches every missing write set. GCache is a recent write-set cache for catch-up, not a point-in-time backup. Donor preference does not prove capacity or health. safe_to_bootstrap marks the node Galera considers safe to use to recreate the Primary Component after a clean full shutdown. After a hard crash, recover and compare the most advanced positions before bootstrapping exactly one node.

shell · cleanup
docker compose down -v --remove-orphans

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only assets compatible with the Ethereum/ERC-20 network. Do not send TRC-20/TRON assets.