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

Flow Control, Certification Conflicts, Auto-Increment, Hot Rows, and Workload Constraints

Diagnose MariaDB Galera certification conflicts and flow-control backpressure, observe multi-primary AUTO_INCREMENT behavior, and connect hot-row/transaction design to sustainable cluster throughput and safe retry semantics.

Advanced155–200 minutesConflict + flow-control workload 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

ServiceHub now has three healthy writable nodes, but the workload is not automatically three times faster. Two dispatch workers update the same queue counters from different nodes; bulk imports create large write sets; one reporting node sometimes applies changes more slowly. Galera exposes these problems as certification conflicts, receive queues, and flow control rather than hiding them behind replication lag.

01

Create a controlled cross-node write conflict and identify certification/high-priority abort evidence.

02

Explain flow control as cluster backpressure from a slow apply queue and interpret paused/queue metrics.

03

Observe wsrep_auto_increment_control and explain why unique AUTO_INCREMENT values can contain gaps.

04

Relate hot rows, transaction size, network latency, and applier concurrency to practical Galera throughput.

05

Design safe retry behavior and reject blind “increase threads/queue limits” tuning.

Measurement discipline

Local container timings are demonstrations, not production performance claims. Flow control may not trigger on a fast laptop with a tiny dataset. This lesson treats status counters as the result: if the workload does not produce pressure, that is an observation. Do not invent latency or throughput numbers.

1. Start a clean three-node lab and create the hot-row fixture

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 · bootstrap then 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 workload tables
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 (501,'open',1),(502,'open',1);
sql · all nodes — capture baseline conflict/flow counters
SHOW GLOBAL STATUS WHERE Variable_name IN ( 'wsrep_local_cert_failures','wsrep_local_bf_aborts', 'wsrep_local_recv_queue_avg','wsrep_flow_control_paused', 'wsrep_flow_control_sent','wsrep_cert_deps_distance');

2. A deterministic mental model for certification conflicts

Open two clients: Session A on n1 and Session B on n2. Both transactions update the same primary-key row while neither has yet committed. There is no single distributed row lock that prevents both sessions from executing. The write sets meet at certification. One ordering wins; the conflicting transaction must roll back/retry.

sql · Session A on n1 — hold an uncommitted change
START TRANSACTION;UPDATE servicehub_galera_lab.countersSET counter_value=counter_value+1WHERE counter_name='dispatch';-- do not COMMIT yet
sql · Session B on n2 — touch the same key
START TRANSACTION;UPDATE servicehub_galera_lab.countersSET counter_value=counter_value+10WHERE counter_name='dispatch';-- do not COMMIT yet

Commit Session A, then commit Session B. Depending on exact scheduling, either side can be the victim, but the important result is that the two conflicting commits cannot both be accepted as independent histories.

sql · commit both sessions and inspect counters
-- Session ACOMMIT;-- Session BCOMMIT;-- Then on both nodesSHOW GLOBAL STATUS WHERE Variable_name IN ('wsrep_local_cert_failures','wsrep_local_bf_aborts');
Expected failure class

The losing client can receive a deadlock-style/retryable transaction error such as error 1213. Do not hard-code a single message string as your detector; versions and conflict paths differ. Check the SQL error class plus wsrep conflict counters and confirm the final business state.

3. Retry belongs to the application contract

wsrep_retry_autocommit can retry autocommitted statements a limited number of times, but multi-statement transactions still require application reasoning. A safe retry repeats a business operation whose inputs and external side effects are controlled. Retrying “charge card, write database, send email” as a single blind loop can duplicate non-database effects.

Operation Retry posture
idempotent UPDATE by stable business key often safe with bounded retry + backoff
INSERT with generated request/idempotency key safe when duplicate detection is designed
external side effect before COMMIT unsafe to blindly replay
large transaction touching hot shared rows redesign/chunk before adding retries
sql · inspect retry setting rather than assuming a default
SHOW GLOBAL VARIABLES LIKE 'wsrep_retry_autocommit';

4. AUTO_INCREMENT is coordinated for uniqueness, not gaplessness

With wsrep_auto_increment_control=ON, Galera adjusts auto_increment_increment and auto_increment_offset as membership changes so simultaneous inserts from several nodes are less likely to collide. The consequence is expected gaps. Business logic must never interpret consecutive IDs as “no missing tickets.”

sql · run on n1, n2, n3
SHOW VARIABLES WHERE Variable_name IN ('wsrep_auto_increment_control','auto_increment_increment','auto_increment_offset');
sql · insert once on each node
INSERT INTO servicehub_galera_lab.tickets(customer_id,state,priority) VALUES (601,'open',1);SELECT LAST_INSERT_ID();
sql · any node — inspect generated identifiers
SELECT ticket_id,customer_idFROM servicehub_galera_lab.ticketsWHERE customer_id>=600ORDER BY ticket_id;

Do not predict exact IDs in the lesson; offsets depend on membership and prior allocation. Verify uniqueness and replication, not consecutiveness.

5. Flow control is deliberate cluster-wide backpressure

Every node receives ordered write sets, but local apply can lag. If a receive queue grows beyond provider thresholds, that node sends flow-control pause messages. Writers across the Primary Component slow down so the lagging node can catch up. That protects bounded queues and consistency at the cost of cluster throughput.

Metric Interpretation
wsrep_local_recv_queue_avg average pending receive/apply queue; sustained growth points to local apply pressure
wsrep_flow_control_paused fraction of observation time replication was paused by flow control
wsrep_flow_control_sent pause events sent by this node
wsrep_cert_deps_distance potential parallel apply distance; not a worker-count prescription

Counters are cumulative/interval-sensitive. Record a start point or use FLUSH STATUS only when you understand which counters it resets. Correlate with CPU, storage latency, lock waits, query workload, and network evidence.

6. Create pressure without claiming a universal result

The following workload creates many small commits from n1 while n3 executes a local CPU/I/O-heavy query. On some machines it will raise queues/flow-control metrics; on others the cluster remains fast enough. Both outcomes are valid local observations.

sql · n3 — create local work that can compete with apply
SELECT BENCHMARK(2000000, SHA2('servicehub',256));
shell · host — generate many small writes on n1
for i in $(seq 1 500); do docker exec mdb15-n1 mariadb -uroot -plabroot -e "UPDATE servicehub_galera_lab.counters SET counter_value=counter_value+1 WHERE counter_name='dispatch';"; done
Windows note

The loop above is POSIX shell syntax. On PowerShell, use 1..500 | ForEach-Object { docker exec ... }, or simply repeat a smaller batch manually. The database observations are the same.

sql · all nodes — measure after workload
SHOW GLOBAL STATUS WHERE Variable_name IN ('wsrep_local_recv_queue_avg','wsrep_flow_control_paused','wsrep_flow_control_sent','wsrep_cert_deps_distance');

If flow control stays near zero, do not tune anything. If one node consistently sends flow control, find why that node cannot apply at the cluster rate before increasing provider queue thresholds.

7. Wrong tuning approach: hide the slow node with larger queues

Provider options such as gcs.fc_limit change when flow control engages. Raising them can postpone backpressure but increases queued work and memory requirements; it does not make the slow disk, hot row, or blocked applier faster. Likewise, blindly increasing wsrep_slave_threads/wsrep_applier_threads can stop helping when dependencies or storage contention dominate.

Use a controlled baseline: one workload, one variable, same dataset, same cluster membership, same cache/warm-up conditions. Accept a change only if throughput/latency improves without unacceptable conflict, queue, memory, or durability consequences.

8. Transaction size and hot-row design

  • Prefer moderate transactions. Very large write sets consume memory, increase certification/apply work, and can amplify recovery/SST consequences.
  • Partition business contention logically: avoid one global “next number” row or single mutable aggregate when independent shards/counters can exist.
  • Use primary keys on replicated InnoDB tables. Galera's row identity/certification depends on stable keys.
  • Multi-primary capability is optional. Many teams route writes to one preferred node to reduce conflict while retaining fast failover to another node.

9. Production judgment

Galera is strongest when transactions are short, keyed, and mostly independent. Treat certification aborts as a workload signal, not merely an exception to suppress. Treat flow control as a safety mechanism telling you the cluster's sustainable write rate is bounded by the slowest relevant member.

Next, separate those database guarantees from the client-routing problem: a node can be running and reachable over TCP while being Non-Primary, Joining, Donor/Desynced, or otherwise unsuitable for application traffic.

Check your understanding

  1. What makes a Galera certification conflict different from a local InnoDB row-lock wait?
  2. Why can AUTO_INCREMENT values have gaps in a healthy cluster?
  3. What does wsrep_flow_control_paused tell you?
  4. Why is raising gcs.fc_limit not a first-line performance fix?
  5. When is writing to only one preferred Galera node a reasonable design?
Review the answers

Certification compares globally ordered write sets from different nodes, so a transaction can be aborted at commit even without a prior distributed lock. Galera adjusts AUTO_INCREMENT increment/offset to avoid multi-primary collisions, so gaps are normal. Flow-control pause fraction shows how much of an observation interval replication was throttled. Raising queue limits hides backpressure without fixing the slow component. A preferred-writer design is sensible when conflict reduction and simpler application behavior matter more than distributing writes across every 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.