Diagnosing IST Failures & gcache Eviction

This diagnostic guide extends gcache Sizing for IST & Fast Recovery and answers the question every operator asks after a rejoin drags on for an hour: why did this node fall back to a full State Snapshot Transfer when it should have caught up with a quick Incremental State Transfer? The answer is almost always that the sequence number the joiner needed was no longer in the donor’s write-set ring — but “the ring was too small” is only one of four distinct causes, and they demand different fixes. This page shows you the exact log lines to read on both the donor and the joiner, how to confirm eviction from the ring, how page-store eviction and gcache.keep_pages_size interact, and a decision tree that maps each symptom to its remedy.

Context: Why IST Silently Degrades to SST in Multi-Master

A rejoining node in a synchronous multi-master cluster asks a donor for every write-set it missed, identified by seqno. IST succeeds only if the donor still holds an unbroken chain of write-sets from the joiner’s last applied seqno forward. The failure is quiet: nothing errors at the moment of eviction: write-sets simply age out of the ring in the normal course of operation, and the shortfall becomes visible only later, when a node happens to rejoin and its required seqno has already scrolled off the bottom. Because the provider cannot replay a gap, it abandons IST and starts SST — the expensive full-datadir path described in Initial Data Synchronization Methods. Diagnosing this well means proving which of the several failure modes occurred, because raising gcache.size fixes eviction but does nothing for an unclean-shutdown seqno of -1 or a page-size mismatch.

Decision tree for why a Galera rejoin fell back to full SST Starting from a rejoin that took a full SST, four checks are made in order. First, if the joiner's last seqno is below the donor's wsrep_local_cached_downto, the write-set was evicted from the ring and the fix is to raise gcache.size and gcache.keep_pages_size. If not, second, if grastate.dat shows seqno minus one, an unclean shutdown lost the position and the fix is to run mysqld with wsrep-recover. If not, third, if the log shows a page size mismatch, gcache.page_size differs across nodes and must be realigned with a coordinated restart. If none of these apply, the cause is donor selection or network, so verify ist.recv_addr and reachability on port 4568. START · REJOIN TOOK FULL SST 1 · joiner seqno < donor wsrep_local_cached_downto ? Seqno evicted from ring Raise gcache.size & gcache.keep_pages_size YES no 2 · grastate.dat shows seqno: -1 ? Unclean shutdown lost position Run mysqld --wsrep-recover, then rejoin YES no 3 · log says "gcache page size mismatch" ? gcache.page_size differs Realign value, coordinated restart YES no 4 · Donor selection or network Verify ist.recv_addr is set and port 4568 is reachable from the joiner to the chosen donor.
Work the checks top to bottom: only the first branch is fixed by enlarging gcache. The other three need a seqno recovery, a page-size realignment, or a network fix.

Solution: Read the Logs and Prove the Cause

Every IST failure leaves a paired trail — one line on the joiner explaining what it asked for, one on the donor explaining why it could not serve it. Read both.

Step 1 — Confirm which path the rejoin actually took

On the joiner, the two outcomes are unmistakable in the error log:

# IST success looks like this:
sudo grep -E "Receiving IST|IST received" /var/log/mysql/error.log
#   WSREP: Receiving IST: 40217 writesets, seqnos 918342-958559

# Fallback to SST looks like this:
sudo grep -E "IST failed|Failed to prepare for incremental|Running: .*sst" /var/log/mysql/error.log
#   WSREP: Failed to prepare for incremental state transfer ... falling back to SST
#   WSREP: Running: 'wsrep_sst_mariabackup --role joiner ...'

The phrase Failed to prepare for incremental state transfer followed by a fallback to wsrep_sst_* is the definitive signature that IST was attempted and rejected.

Step 2 — Prove or rule out ring eviction

Eviction is the first and most common cause. Compare the seqno the joiner needed against the oldest seqno the donor still holds. On the donor, read the low-water mark:

-- On the donor: the oldest seqno still replayable from gcache
SHOW GLOBAL STATUS LIKE 'wsrep_local_cached_downto';

On the joiner, its last applied seqno is in grastate.dat (or the recovered value from Step 3). If the joiner’s seqno is less than the donor’s wsrep_local_cached_downto, the write-set chain the joiner needs has been overwritten in the ring — this is textbook eviction, and the fix is to enlarge the ring per the gcache sizing worksheet. The donor log states it plainly:

WSREP: IST first seqno 918342 not found from cache, falling back to SST

Step 3 — Rule out an unclean-shutdown seqno of -1

If eviction is not the cause, inspect the joiner’s saved position. A node that crashed or was killed rather than shut down cleanly writes seqno: -1 and safe_to_bootstrap: 0 into grastate.dat, meaning it does not know how far it had applied — and a node with an unknown position cannot request IST, so it is forced to SST regardless of ring size:

sudo cat /var/lib/mysql/grastate.dat
# seqno:   -1        <- position unknown; IST impossible
# safe_to_bootstrap: 0

# Recover the real seqno from the InnoDB redo log without joining:
sudo -u mysql mariadbd --wsrep-recover
#   WSREP: Recovered position: 5f3a...:958517

mariadbd --wsrep-recover reads the committed position out of InnoDB and writes it back, restoring the node’s ability to request IST. This mechanism and the surrounding startup diagnostics are covered in Handling Galera Startup Errors & Logs.

Step 4 — Understand page-store eviction and keep_pages_size

The gcache is not purely in-memory. When the ring or an individual write-set exceeds the RAM allocation, the provider spills to on-disk page-store files sized by gcache.page_size. As write-sets are consumed, the provider frees drained page files — this is page eviction, and it is normal. The problem arises under spiky write loads: the provider repeatedly allocates and deletes page files, and in the churn it can free pages that still held write-sets a slow joiner needed. gcache.keep_pages_size counters this by instructing the provider to keep at least that many bytes of page-store files allocated rather than deleting them the instant they drain:

[mysqld]
# Keep 4G of drained page files allocated so spiky writes don't evict
# write-sets a lagging joiner still needs.
wsrep_provider_options = "gcache.size=14G; gcache.page_size=1G; gcache.keep_pages_size=4G"

Setting gcache.keep_pages_size to a few gigabytes stabilizes the effective retained window on bursty workloads, where the nominal gcache.size alone can be misleading because page churn evicts history faster than the byte budget implies.

Parameter Reference

Parameter / signal Type Default Diagnostic meaning
wsrep_local_cached_downto status n/a Oldest seqno the donor can still serve via IST; the eviction threshold
gcache.keep_pages_size size 0 Bytes of drained page-store files kept allocated; raise to stop churn-eviction
gcache.page_size size 128M On-disk page file size; a mismatch across nodes blocks IST entirely
gcache.size size 128M Total ring budget; too small evicts needed write-sets
grastate.dat seqno file value n/a -1 means position unknown, forcing SST until --wsrep-recover runs
ist.recv_addr provider opt node addr Address/port the joiner listens on for IST; wrong value defeats IST

Verification

After applying a fix, force a controlled rejoin and confirm it now takes IST. Stop one node cleanly, wait a couple of minutes so it genuinely lags, then restart it and watch the log:

# On the rejoining node, in one terminal:
sudo tail -f /var/log/mysql/error.log | grep -E "IST|SST|Recovered position"
# Expect: 'Receiving IST: N writesets, seqnos A-B'  (NOT 'Running: wsrep_sst_...')

Then confirm from status that the join completed cleanly and the node is back in the primary component:

SHOW GLOBAL STATUS WHERE Variable_name IN (
  'wsrep_local_state_comment',  -- expect 'Synced'
  'wsrep_cluster_status'        -- expect 'Primary'
);

If it still fell back to SST, re-walk the decision tree — the cause you fixed was not the operative one.

Edge Cases & Gotchas

  • A donor under load can evict mid-IST. If the donor keeps taking heavy writes while serving a slow joiner, its ring can advance past the seqno it is still streaming, aborting the IST partway through. On write-heavy clusters, prefer a less-loaded donor via wsrep_sst_donor, or raise gcache.keep_pages_size so in-flight history is protected.
  • gcache.recover=no turns every restart-then-rejoin into SST. Even a correctly sized ring is discarded at restart unless recovery is enabled, so a node that reboots and immediately needs to donate has an empty cache. Confirm gcache.recover=yes is set, as detailed in gcache Sizing for IST & Fast Recovery.
  • A page-size mismatch masquerades as eviction. If gcache.page_size differs between donor and joiner, the log may show an IST failure that looks like a cache miss but is really a provider-level incompatibility. Confirm the value is identical across the group with SHOW GLOBAL VARIABLES LIKE 'wsrep_provider_options' before enlarging the ring, since more space will not fix a mismatch.