Report
During full-cluster-crash recovery, the GR bootstrap startup probe (/opt/percona/bootstrap, failureThreshold: 1) races the recovery process itself. bootstrap deliberately os.Exit(1)s on transient recovery states (e.g. "Can't connect to any of the peers"), which makes the kubelet kill the mysql container — including a clone donor mid-addInstance. On clusters with large datasets (clone takes minutes) the recovery repeatedly kills its own members and the cluster loops through full-cluster-crash recoveries indefinitely.
The full-cluster-crash marker bypass added for the readiness/liveness probes (the #1392 / K8SPS fix, cmd/healthcheck/main.go) does not cover the startup probe, so bootstrap's exit-1 paths still terminate containers during recovery.
More about the problem
Observed on a 3-node GR cluster (~70 GB dataset, communication_stack=MYSQL) after a full cluster crash:
- Operator crash recovery works as designed at first:
Crash recovery Attempting to reboot cluster from complete outage pod=mysql-2
Crash recovery Cluster was successfully rebooted
Crash recovery Primary pod is mysql-0
Crash recovery Deleting secondary pod pod=mysql-1
Crash recovery Deleting secondary pod pod=mysql-2
- Deleted secondaries return and start CLONE from the primary (
addInstance ... waitRecovery: 2) — for a ~70 GB dataset this takes minutes. During this window a clone recipient also restarts its own mysqld (clone plugin behavior), and GR is still forming.
- Meanwhile, whichever member's startup probe fires during a moment when no peer answers
dba.getCluster() takes this path in cmd/bootstrap/gr/group_replication.go (v1.2.0):
log.Printf("Can't connect to any of the peers, we need to reboot")
...
os.Exit(1) // force restart container
With startupProbe.failureThreshold: 1, one non-zero exit = immediate container kill. mysqld logs show:
[System] [MY-013172] [Server] Received SHUTDOWN from user <via user signal>. Shutting down mysqld
~27s after boot (15s initialDelaySeconds + one probe run).
- When the killed member is the primary/clone donor, the in-flight clone aborts, the joiner fails, the group is dead again → operator declares
FullClusterCrashDetected → reboot → GOTO 1. We observed this loop for ~90 minutes with zero client load until we intervened (paused the CR).
v1.2.0 cmd/bootstrap/gr/group_replication.go has five such os.Exit(1) paths (lines 503, 521, 546, 592, 600). Note the probe's timeoutSeconds: 43200 shows long-running bootstrap is expected and tolerated — it's the early deliberate exits interacting with failureThreshold: 1 that cause the kills.
Steps to reproduce
- 3-node GR cluster with a dataset large enough that CLONE takes several minutes (tens of GB).
- Induce a full cluster crash (e.g. make all members leave the group / kill mysqld on all pods).
- Let the operator run
rebootClusterFromCompleteOutage and delete the secondaries.
- While the secondaries clone, the members' startup probes keep running
bootstrap; observe container SIGTERMs shortly after boots ("Received SHUTDOWN from user "), aborted clones, and repeated Full cluster crash detected cycles.
Small datasets rarely hit this (the vulnerable window is seconds); large datasets make the window minutes wide and the loop quasi-deterministic.
Versions
- Kubernetes: k3s v1.35.4 (bare metal)
- Operator: 1.2.0
- Database: Percona Server 8.0.46-37, group replication,
communication_stack=MYSQL
Anything else?
Related: #1392 (state-monitor race — fixed in 1.2.0; this is a different race in the same recovery area; the 1.2.0 crash-marker bypass covers readiness/liveness probes but not the startup probe).
Possible directions:
- Make
bootstrap marker-aware: while /var/lib/mysql/full-cluster-crash exists (or operator-driven recovery is in progress), return success / keep waiting instead of os.Exit(1) on transient "no peers" states.
- Or retry/tolerate transient states inside
bootstrap (it already has a 12h probe timeout to work with) instead of forcing a container restart on the first failure.
- Or
failureThreshold > 1 on the startup probe so a single transient failure isn't fatal.
- Never exit-1 on the member currently acting as a clone donor.
Report
During full-cluster-crash recovery, the GR bootstrap startup probe (
/opt/percona/bootstrap,failureThreshold: 1) races the recovery process itself.bootstrapdeliberatelyos.Exit(1)s on transient recovery states (e.g. "Can't connect to any of the peers"), which makes the kubelet kill the mysql container — including a clone donor mid-addInstance. On clusters with large datasets (clone takes minutes) the recovery repeatedly kills its own members and the cluster loops through full-cluster-crash recoveries indefinitely.The
full-cluster-crashmarker bypass added for the readiness/liveness probes (the #1392 / K8SPS fix,cmd/healthcheck/main.go) does not cover the startup probe, sobootstrap's exit-1 paths still terminate containers during recovery.More about the problem
Observed on a 3-node GR cluster (~70 GB dataset,
communication_stack=MYSQL) after a full cluster crash:addInstance ... waitRecovery: 2) — for a ~70 GB dataset this takes minutes. During this window a clone recipient also restarts its own mysqld (clone plugin behavior), and GR is still forming.dba.getCluster()takes this path incmd/bootstrap/gr/group_replication.go(v1.2.0):startupProbe.failureThreshold: 1, one non-zero exit = immediate container kill. mysqld logs show:initialDelaySeconds+ one probe run).FullClusterCrashDetected→ reboot → GOTO 1. We observed this loop for ~90 minutes with zero client load until we intervened (paused the CR).v1.2.0
cmd/bootstrap/gr/group_replication.gohas five suchos.Exit(1)paths (lines 503, 521, 546, 592, 600). Note the probe'stimeoutSeconds: 43200shows long-running bootstrap is expected and tolerated — it's the early deliberate exits interacting withfailureThreshold: 1that cause the kills.Steps to reproduce
rebootClusterFromCompleteOutageand delete the secondaries.bootstrap; observe container SIGTERMs shortly after boots ("Received SHUTDOWN from user "), aborted clones, and repeatedFull cluster crash detectedcycles.Small datasets rarely hit this (the vulnerable window is seconds); large datasets make the window minutes wide and the loop quasi-deterministic.
Versions
communication_stack=MYSQLAnything else?
Related: #1392 (state-monitor race — fixed in 1.2.0; this is a different race in the same recovery area; the 1.2.0 crash-marker bypass covers readiness/liveness probes but not the startup probe).
Possible directions:
bootstrapmarker-aware: while/var/lib/mysql/full-cluster-crashexists (or operator-driven recovery is in progress), return success / keep waiting instead ofos.Exit(1)on transient "no peers" states.bootstrap(it already has a 12h probe timeout to work with) instead of forcing a container restart on the first failure.failureThreshold > 1on the startup probe so a single transient failure isn't fatal.