Overview
We conducted 19 chaos experiments on MySQL 8.4.8 InnoDB Cluster with MySQL Router running on a KubeDB-managed 3-node cluster. The goal: validate that KubeDB MySQL InnoDB Cluster delivers zero data loss, automatic failover, transparent connection re-routing, and self-healing recovery under realistic failure conditions with production-level write loads.
The result: every experiment passed with zero data loss, zero split-brain, and zero errant GTIDs.
This post summarizes the methodology, results, and key findings from comprehensive chaos testing of KubeDB MySQL InnoDB Cluster with MySQL Router.
Why Chaos Testing?
Running databases on Kubernetes introduces failure modes that traditional infrastructure does not have — pods can be evicted, nodes can go down, network policies can partition traffic, and resource limits can trigger OOMKills at any time. Chaos engineering deliberately injects these failures to verify that the system recovers correctly before they happen in production.
For a MySQL InnoDB Cluster managed by KubeDB, we needed to answer:
- Does the cluster lose data when a primary is killed mid-transaction?
- Does MySQL Router automatically re-route traffic to the new primary after failover?
- Can the cluster self-heal after a full outage with no manual intervention?
- Are GTIDs consistent across all nodes after recovery?
- Does the RW-Split port (6450) work correctly under chaos conditions?
What is InnoDB Cluster?
InnoDB Cluster is MySQL’s integrated high-availability solution that combines Group Replication with MySQL Router for automatic connection routing. KubeDB exposes a single primary service (mysql-ha-cluster) that routes traffic to the right place:
- Port 3306 (Read-Write) — Kubernetes service follows
kubedb.com/role=primary, so writes always reach the current PRIMARY without any Router round-trip. - Port 6447 (Read-Only via Router) — round-robin across SECONDARYs.
- Port 6450 (Read-Write Split via Router) — single port with automatic routing: writes go to PRIMARY, reads distributed across all members (MySQL 8.2+; requires TLS because of
connection_sharing=1+caching_sha2_password).
The key advantage is transparent failover — applications don’t need to know which pod is the primary. They connect to the primary service, and either the k8s service (port 3306) or the Router (port 6450) handles routing automatically.
Test Environment
| Component | Details |
|---|---|
| Kubernetes | kind (local cluster) |
| KubeDB Version | 2026.2.26 |
| Cluster Topology | 3-node InnoDB Cluster + 1 MySQL Router |
| MySQL Version | 8.4.8 |
| Storage | 2Gi PVC per node (Durable, ReadWriteOnce) |
| Memory Limit | 1.5Gi per MySQL pod |
| CPU Request | 500m per pod |
| Chaos Engine | Chaos Mesh |
| Load Generator | sysbench oltp_write_only via primary service (port 3306), 4 tables × 50k rows, 8 threads |
| Baseline TPS | ~1,312 (8 threads, p95 9.22 ms) |
All experiments were run under sustained sysbench write load through the MySQL Router to simulate production traffic during failures.
Setup Guide
Step 1: Create a kind Cluster
We used kind (Kubernetes IN Docker) as our local Kubernetes cluster. Follow the kind installation guide to install it, then create a cluster:
kind create cluster --name chaos-test
Step 2: Install KubeDB
Install KubeDB operator using Helm:
helm install kubedb oci://ghcr.io/appscode-charts/kubedb \
--version v2026.2.26 \
--namespace kubedb --create-namespace \
--set-file global.license=/path/to/license.txt \
--wait --burst-limit=10000 --debug
Step 3: Install Chaos Mesh
Install Chaos Mesh for fault injection:
helm repo add chaos-mesh https://charts.chaos-mesh.org
helm repo update chaos-mesh
helm upgrade -i chaos-mesh chaos-mesh/chaos-mesh \
-n chaos-mesh --create-namespace \
--set dashboard.create=true \
--set dashboard.securityMode=false \
--set chaosDaemon.runtime=containerd \
--set chaosDaemon.socketPath=/run/containerd/containerd.sock \
--set chaosDaemon.privileged=true
Step 4: Deploy InnoDB Cluster
Create the namespace:
kubectl create namespace demo
Deploy the InnoDB Cluster:
apiVersion: kubedb.com/v1
kind: MySQL
metadata:
name: mysql-ha-cluster
namespace: demo
spec:
deletionPolicy: Delete
podTemplate:
spec:
containers:
- name: mysql
resources:
limits:
memory: 1.5Gi
requests:
cpu: 500m
memory: 1.5Gi
replicas: 3
storage:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi
storageType: Durable
topology:
mode: InnoDBCluster
innoDBCluster:
router:
replicas: 1
version: 8.4.8
This deploys 3 MySQL pods + 1 MySQL Router pod. The Router automatically discovers the cluster topology and exposes 3 ports via the Kubernetes Service: 6446 (RW), 6447 (RO), and 6450 (RW-Split).
Deploy and wait for Ready:
kubectl apply -f mysql-innodb-cluster.yaml
kubectl wait --for=jsonpath='{.status.phase}'=Ready mysql/mysql-ha-cluster -n demo --timeout=5m
Step 5: Verify Primary Service
The Router ports (6446 RW, 6447 RO, 6450 RW-Split) are exposed directly on the main primary service mysql-ha-cluster — there is no separate *-router service to manage.
➤ kubectl get svc -n demo
NAME TYPE CLUSTER-IP PORT(S)
mysql-ha-cluster ClusterIP 10.96.140.10 3306/TCP,6446/TCP,6447/TCP,6450/TCP
mysql-ha-cluster-pods ClusterIP None 3306/TCP
mysql-ha-cluster-standby ClusterIP 10.96.221.45 3306/TCP
Test each port:
➤ # Port 6446 (RW) → routes to PRIMARY
mysql -h mysql-ha-cluster -P 6446 -e "SELECT @@hostname;"
@@hostname
mysql-ha-cluster-0
➤ # Port 6447 (RO) → routes to SECONDARY
mysql -h mysql-ha-cluster -P 6447 -e "SELECT @@hostname;"
@@hostname
mysql-ha-cluster-1
➤ # Port 6450 (RW-Split) → round-robin across all members (requires --mysql-ssl)
mysql -h mysql-ha-cluster -P 6450 --ssl-mode=REQUIRED -e "SELECT @@hostname;"
@@hostname
mysql-ha-cluster-2
Step 6: Deploy sysbench Load Generator
apiVersion: apps/v1
kind: Deployment
metadata:
name: sysbench-load
namespace: demo
labels:
app: sysbench
spec:
replicas: 1
selector:
matchLabels:
app: sysbench
template:
metadata:
labels:
app: sysbench
spec:
containers:
- name: sysbench
image: perconalab/sysbench:latest
command: ["/bin/sleep", "infinity"]
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "2"
memory: "2Gi"
env:
- name: MYSQL_HOST
value: "mysql-ha-cluster.demo.svc.cluster.local"
- name: MYSQL_PORT
value: "6446"
- name: MYSQL_USER
value: "root"
- name: MYSQL_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-ha-cluster-auth
key: password
- name: MYSQL_DB
value: "sbtest"
kubectl apply -f sysbench.yaml
Step 7: Prepare sysbench Tables
#!/bin/bash
# Get the MySQL root password
PASS=$(kubectl get secret mysql-ha-cluster-auth -n demo -o jsonpath='{.data.password}' | base64 -d)
# Create the sbtest database via the Router RW port
kubectl exec -n demo mysql-ha-cluster-0 -c mysql -- \
mysql -uroot -p"$PASS" -e "CREATE DATABASE IF NOT EXISTS sbtest;"
# Get the sysbench pod name
SBPOD=$(kubectl get pods -n demo -l app=sysbench -o jsonpath='{.items[0].metadata.name}')
# Prepare tables via Router (4 tables × 50k rows)
kubectl exec -n demo $SBPOD -- sysbench oltp_write_only \
--mysql-host=mysql-ha-cluster.demo.svc.cluster.local \
--mysql-port=6446 --mysql-user=root --mysql-password="$PASS" \
--mysql-db=sbtest --tables=4 --table-size=50000 \
--threads=8 prepare
Step 8: Run sysbench via Router During Chaos
# Write load via RW port (6446)
kubectl exec -n demo $SBPOD -- sysbench oltp_write_only \
--mysql-host=mysql-ha-cluster.demo.svc.cluster.local \
--mysql-port=6446 --mysql-user=root --mysql-password="$PASS" \
--mysql-db=sbtest --tables=4 --table-size=50000 \
--threads=8 --time=60 --report-interval=10 run
# Read-write split load via RW-Split port (6450) — requires SSL
kubectl exec -n demo $SBPOD -- sysbench oltp_read_write \
--mysql-host=mysql-ha-cluster.demo.svc.cluster.local \
--mysql-port=6450 --mysql-user=root --mysql-password="$PASS" \
--mysql-db=sbtest --tables=4 --table-size=50000 \
--threads=8 --time=60 --report-interval=10 --mysql-ssl=REQUIRED run
Note: Port 6450 (RW-Split) requires
--mysql-ssl=REQUIREDbecause MySQL Router’sconnection_sharing=1withcaching_sha2_passwordneeds a secure connection. Ports 6446 and 6447 do not require this.
Chaos Testing
We will run chaos experiments to see how our InnoDB Cluster behaves under failure scenarios like pod kill, OOM kill, network partition, network latency, IO latency, and more. We will use sysbench via the MySQL Router to simulate high write load on the cluster during each experiment.
Important Notes on Database Status:
Ready— Database is fully operational. All pods are ONLINE.Critical— Primary is accepting connections and operational, but one or more replicas may be down.NotReady— Primary is not available. No writes can be accepted.You can read/write in your database in both
ReadyandCriticalstates. So even if your db is inCriticalstate, your uptime is not compromised.
Verify Cluster is Ready
Before starting chaos experiments, verify the cluster is healthy:
➤ kubectl get mysql,pods -n demo -L kubedb.com/role
NAME VERSION STATUS AGE ROLE
mysql.kubedb.com/mysql-ha-cluster 8.4.8 Ready 63m
NAME READY STATUS RESTARTS AGE ROLE
pod/mysql-ha-cluster-0 2/2 Running 0 63m primary
pod/mysql-ha-cluster-1 2/2 Running 0 63m standby
pod/mysql-ha-cluster-2 2/2 Running 0 63m standby
pod/mysql-ha-cluster-router-0 1/1 Running 0 63m
pod/sysbench-load-849bdc4cdc-h2zpx 1/1 Running 0 8d
Inspect the GR members:
➤ kubectl exec -n demo mysql-ha-cluster-0 -c mysql -- \
mysql -uroot -p"$PASS" -e "SELECT MEMBER_HOST, MEMBER_PORT, MEMBER_STATE, MEMBER_ROLE \
FROM performance_schema.replication_group_members;"
MEMBER_HOST MEMBER_PORT MEMBER_STATE MEMBER_ROLE
mysql-ha-cluster-2.mysql-ha-cluster-pods.demo 3306 ONLINE SECONDARY
mysql-ha-cluster-1.mysql-ha-cluster-pods.demo 3306 ONLINE SECONDARY
mysql-ha-cluster-0.mysql-ha-cluster-pods.demo 3306 ONLINE PRIMARY
The pod with kubedb.com/role=primary is the PRIMARY; the rest are SECONDARY. With the cluster ready, sysbench tables prepared, and the Router exposing 6446/6447/6450, we are ready to run chaos experiments.
InnoDB Chaos#1: Kill the Primary Pod
We are about to kill the primary pod and see how fast MySQL Router detects the new primary and re-routes RW traffic on port 6446.
Save this yaml as tests/01-pod-kill.yaml:
apiVersion: chaos-mesh.org/v1alpha1
kind: PodChaos
metadata:
name: mysql-primary-pod-kill
namespace: chaos-mesh
spec:
action: pod-kill
mode: one
selector:
namespaces:
- demo
labelSelectors:
"app.kubernetes.io/instance": "mysql-ha-cluster"
"kubedb.com/role": "primary"
gracePeriod: 0
What this chaos does: Terminates the primary pod abruptly with gracePeriod=0, forcing an immediate failover and Router re-route to a standby replica.
-
Expected behavior: Primary killed → GR elects new primary → Router (metadata-cache TTL 0.5s) detects topology change and re-routes RW traffic on port 6446 within seconds → killed pod rejoins as secondary → cluster returns to
Ready. Zero data loss, GTIDs and checksums consistent across all 3 nodes. -
Actual result: Pod-0 killed → pod-2 elected new primary → Router re-routed RW to pod-2 within seconds (confirmed via
SELECT @@hostnameon port 6446) → pod-0 rejoined as secondary. All 3 membersONLINE, GTIDs and checksums match. PASS.
Before kill — pod-0 is PRIMARY:
➤ SELECT MEMBER_HOST, MEMBER_PORT, MEMBER_STATE, MEMBER_ROLE
FROM performance_schema.replication_group_members;
+-----------------------------------------------+-------------+--------------+-------------+
| MEMBER_HOST | MEMBER_PORT | MEMBER_STATE | MEMBER_ROLE |
+-----------------------------------------------+-------------+--------------+-------------+
| mysql-ha-cluster-2.mysql-ha-cluster-pods.demo | 3306 | ONLINE | SECONDARY |
| mysql-ha-cluster-1.mysql-ha-cluster-pods.demo | 3306 | ONLINE | SECONDARY |
| mysql-ha-cluster-0.mysql-ha-cluster-pods.demo | 3306 | ONLINE | PRIMARY |
+-----------------------------------------------+-------------+--------------+-------------+
Run watch kubectl get mysql,pods -n demo -L kubedb.com/role in one terminal, then apply the chaos:
➤ kubectl apply -f tests/01-pod-kill.yaml
podchaos.chaos-mesh.org/mysql-primary-pod-kill created
After kill — pod-2 elected as new PRIMARY, Router automatically re-routes:
➤ SELECT MEMBER_HOST, MEMBER_PORT, MEMBER_STATE, MEMBER_ROLE
FROM performance_schema.replication_group_members;
+-----------------------------------------------+-------------+--------------+-------------+
| MEMBER_HOST | MEMBER_PORT | MEMBER_STATE | MEMBER_ROLE |
+-----------------------------------------------+-------------+--------------+-------------+
| mysql-ha-cluster-2.mysql-ha-cluster-pods.demo | 3306 | ONLINE | PRIMARY |
| mysql-ha-cluster-1.mysql-ha-cluster-pods.demo | 3306 | ONLINE | SECONDARY |
+-----------------------------------------------+-------------+--------------+-------------+
➤ # Router automatically re-routed RW traffic to the new primary
mysql -h mysql-ha-cluster -P 6446 -e "SELECT @@hostname;"
@@hostname
mysql-ha-cluster-2
After recovery — all 3 members ONLINE, GTIDs match, checksums match:
➤ SELECT MEMBER_HOST, MEMBER_PORT, MEMBER_STATE, MEMBER_ROLE
FROM performance_schema.replication_group_members;
+-----------------------------------------------+-------------+--------------+-------------+
| MEMBER_HOST | MEMBER_PORT | MEMBER_STATE | MEMBER_ROLE |
+-----------------------------------------------+-------------+--------------+-------------+
| mysql-ha-cluster-2.mysql-ha-cluster-pods.demo | 3306 | ONLINE | PRIMARY |
| mysql-ha-cluster-1.mysql-ha-cluster-pods.demo | 3306 | ONLINE | SECONDARY |
| mysql-ha-cluster-0.mysql-ha-cluster-pods.demo | 3306 | ONLINE | SECONDARY |
+-----------------------------------------------+-------------+--------------+-------------+
➤ # GTIDs — all match ✅
pod-0: dcb462a5-...:1-4, e8ac711c-...:1-563:1000093-1000107
pod-1: dcb462a5-...:1-4, e8ac711c-...:1-563:1000093-1000107
pod-2: dcb462a5-...:1-4, e8ac711c-...:1-563:1000093-1000107
➤ # Checksums — all match ✅
pod-0: sbtest1=309802666
pod-1: sbtest1=309802666
pod-2: sbtest1=309802666
Result: PASS — Router detected the failover and re-routed RW traffic within seconds. Zero data loss. All GTIDs and checksums match across all 3 nodes.
Clean up:
➤ kubectl delete -f tests/01-pod-kill.yaml
podchaos.chaos-mesh.org "mysql-primary-pod-kill" deleted
InnoDB Chaos#2: OOMKill the Primary Pod (Memory Stress)
Apply 1200MB memory stress on the primary via StressChaos. Combined with MySQL’s working set, the pod exceeds its 1.5Gi limit and gets OOMKilled. Unlike standalone Group Replication where 8.4.8 sometimes survives the stress, InnoDB Cluster with 1200MB consistently triggers an OOMKill.
Save this yaml as tests/02-oomkill.yaml:
apiVersion: chaos-mesh.org/v1alpha1
kind: StressChaos
metadata:
name: mysql-primary-memory-stress
namespace: chaos-mesh
spec:
mode: one
selector:
namespaces:
- demo
labelSelectors:
"app.kubernetes.io/instance": "mysql-ha-cluster"
"kubedb.com/role": "primary"
stressors:
memory:
workers: 2
size: "1200MB"
duration: "10m"
What this chaos does: Allocates 1200MB of extra memory on the primary pod. Combined with MySQL’s memory usage (~500MB), this exceeds the 1.5Gi limit and triggers an OOMKill.
-
Expected behavior: Memory stress pushes primary past 1.5Gi limit → OOMKill → GR expels the unreachable member, new primary elected → Router re-routes RW traffic on port 6446 → killed pod restarts and rejoins → cluster returns to
Ready. Zero data loss. -
Actual result: Primary pod-2 OOMKilled (
Restarts: 1). Pod-1 elected as new primary. Router re-routed automatically. Pod-2 rejoined as secondary within ~60s. All 3 membersONLINE, GTIDs and checksums match. PASS.
Apply the chaos while sysbench is running through the Router:
➤ kubectl apply -f tests/02-oomkill.yaml
stresschaos.chaos-mesh.org/mysql-primary-memory-stress created
The primary pod gets OOMKilled. Sysbench loses the connection on port 6446 momentarily before the Router routes new connections to the new primary:
FATAL: mysql_stmt_execute() returned error 2013 (Lost connection to MySQL server during query)
➤ kubectl get pods -n demo -l app.kubernetes.io/instance=mysql-ha-cluster
NAME READY STATUS RESTARTS AGE
mysql-ha-cluster-0 2/2 Running 0 89s
mysql-ha-cluster-1 2/2 Running 0 17m
mysql-ha-cluster-2 2/2 Running 1 (3s ago) 17m # OOMKill restart
mysql-ha-cluster-router-0 1/1 Running 0 17m
After recovery, pod-1 is the new PRIMARY:
➤ SELECT MEMBER_HOST, MEMBER_PORT, MEMBER_STATE, MEMBER_ROLE
FROM performance_schema.replication_group_members;
+-----------------------------------------------+-------------+--------------+-------------+
| MEMBER_HOST | MEMBER_PORT | MEMBER_STATE | MEMBER_ROLE |
+-----------------------------------------------+-------------+--------------+-------------+
| mysql-ha-cluster-2.mysql-ha-cluster-pods.demo | 3306 | ONLINE | SECONDARY |
| mysql-ha-cluster-1.mysql-ha-cluster-pods.demo | 3306 | ONLINE | PRIMARY |
| mysql-ha-cluster-0.mysql-ha-cluster-pods.demo | 3306 | ONLINE | SECONDARY |
+-----------------------------------------------+-------------+--------------+-------------+
➤ # Router now routes RW to the new primary
mysql -h mysql-ha-cluster -P 6446 -e "SELECT @@hostname;"
@@hostname
mysql-ha-cluster-1
Result: PASS — OOMKill triggered failover. Router re-routed RW traffic automatically. Zero data loss, GTIDs and checksums match across all 3 nodes.
Clean up:
➤ kubectl delete -f tests/02-oomkill.yaml
stresschaos.chaos-mesh.org "mysql-primary-memory-stress" deleted
InnoDB Chaos#3: Network Partition the Primary
Isolate the primary from all standby replicas for 2 minutes. The standbys lose contact with the primary and elect a new one; the Router follows the new topology.
Save this yaml as tests/03-network-partition.yaml:
apiVersion: chaos-mesh.org/v1alpha1
kind: NetworkChaos
metadata:
name: mysql-primary-network-partition
namespace: chaos-mesh
spec:
action: partition
mode: one
selector:
namespaces:
- demo
labelSelectors:
"app.kubernetes.io/instance": "mysql-ha-cluster"
"kubedb.com/role": "primary"
target:
mode: all
selector:
namespaces:
- demo
labelSelectors:
"kubedb.com/role": "standby"
direction: both
duration: "2m"
What this chaos does: Creates a complete network partition between the primary and all standby replicas for 2 minutes. The primary loses quorum and is expelled; the standbys form a new quorum and elect a new primary.
-
Expected behavior: Primary isolated for 2 min → GR marks it
UNREACHABLE→ afterunreachable_majority_timeout(~20s) it is expelled and a new primary is elected from the remaining 2 nodes → Router re-routes RW to the new primary → after partition lifts, expelled node rejoins via the coordinator → cluster returns toReady. Zero data loss. -
Actual result: Pod-1 (old primary) shown
UNREACHABLE, expelled. Pod-2 elected as new primary. Router re-routed. Pod-1 auto-rejoined ~90s after partition lifted. GTIDs and checksums match. PASS.
Apply the chaos:
➤ kubectl apply -f tests/03-network-partition.yaml
networkchaos.chaos-mesh.org/mysql-primary-network-partition created
During partition — primary shown as UNREACHABLE:
➤ SELECT MEMBER_HOST, MEMBER_PORT, MEMBER_STATE, MEMBER_ROLE
FROM performance_schema.replication_group_members;
+-----------------------------------------------+-------------+--------------+-------------+
| MEMBER_HOST | MEMBER_PORT | MEMBER_STATE | MEMBER_ROLE |
+-----------------------------------------------+-------------+--------------+-------------+
| mysql-ha-cluster-2.mysql-ha-cluster-pods.demo | 3306 | ONLINE | SECONDARY |
| mysql-ha-cluster-1.mysql-ha-cluster-pods.demo | 3306 | UNREACHABLE | PRIMARY |
| mysql-ha-cluster-0.mysql-ha-cluster-pods.demo | 3306 | ONLINE | SECONDARY |
+-----------------------------------------------+-------------+--------------+-------------+
After GR timeout — pod-2 elected as new PRIMARY, pod-1 expelled. Router re-routes:
➤ SELECT MEMBER_HOST, MEMBER_PORT, MEMBER_STATE, MEMBER_ROLE
FROM performance_schema.replication_group_members;
+-----------------------------------------------+-------------+--------------+-------------+
| MEMBER_HOST | MEMBER_PORT | MEMBER_STATE | MEMBER_ROLE |
+-----------------------------------------------+-------------+--------------+-------------+
| mysql-ha-cluster-2.mysql-ha-cluster-pods.demo | 3306 | ONLINE | PRIMARY |
| mysql-ha-cluster-0.mysql-ha-cluster-pods.demo | 3306 | ONLINE | SECONDARY |
+-----------------------------------------------+-------------+--------------+-------------+
➤ mysql -h mysql-ha-cluster -P 6446 -e "SELECT @@hostname;"
@@hostname
mysql-ha-cluster-2
After the 2-minute partition lifted, the coordinator rejoined pod-1 automatically (~90s later). All 3 members ONLINE, GTIDs and checksums match.
Result: PASS — Partition caused failover in ~20 seconds. Router re-routed RW traffic. Expelled member auto-rejoined. Zero data loss.
Clean up:
➤ kubectl delete -f tests/03-network-partition.yaml
networkchaos.chaos-mesh.org "mysql-primary-network-partition" deleted
InnoDB Chaos#4: IO Latency (100ms) on Primary
Inject 100ms latency on every disk I/O operation on the primary’s data volume. This simulates a slow storage backend — a common issue in cloud environments with noisy neighbours.
Save this yaml as tests/04-io-latency.yaml:
apiVersion: chaos-mesh.org/v1alpha1
kind: IOChaos
metadata:
name: mysql-primary-io-latency
namespace: chaos-mesh
spec:
action: latency
mode: one
selector:
namespaces:
- demo
labelSelectors:
"app.kubernetes.io/instance": "mysql-ha-cluster"
"kubedb.com/role": "primary"
volumePath: "/var/lib/mysql"
path: "/**"
delay: "100ms"
percent: 100
duration: "30s"
What this chaos does: Adds 100ms delay to every read/write on the primary’s MySQL data directory. Every fsync, write, and read is slowed down.
-
Expected behavior: Primary’s disk I/O slowed by 100ms → TPS collapses (disk-bound writes) but cluster stays
Ready→ Router keeps the connection to the degraded primary (it is still alive, just slow) → when chaos ends, TPS recovers. Zero data loss. -
Actual result: TPS dropped to ~0 during the 30s latency window, then recovered to 1242 TPS after chaos expired. No failover, no errors. GTIDs and checksums match. PASS.
Apply the chaos while running 8-thread sysbench write load through the Router (port 6446):
➤ kubectl apply -f tests/04-io-latency.yaml
iochaos.chaos-mesh.org/mysql-primary-io-latency created
➤ kubectl exec -n demo $SBPOD -- sysbench oltp_write_only \
--mysql-host=mysql-ha-cluster.demo.svc.cluster.local \
--mysql-port=6446 --mysql-user=root --mysql-password="$PASS" \
--mysql-db=sbtest --tables=4 --table-size=50000 \
--threads=8 --time=60 --report-interval=10 run
[ 10s ] thds: 8 tps: 0.10 qps: 1.90 lat (ms,95%): 1678.14 err/s: 0.00
[ 20s ] thds: 8 tps: 0.00 qps: 0.00 lat (ms,95%): 0.00 err/s: 0.00
[ 30s ] thds: 8 tps: 0.20 qps: 1.50 lat (ms,95%): 26861.48 err/s: 0.00
[ 40s ] thds: 8 tps: 0.00 qps: 0.00 lat (ms,95%): 0.00 err/s: 0.00
[ 50s ] thds: 8 tps: 254.30 qps: 1528.22 lat (ms,95%): 13.22 err/s: 0.00 # chaos expired
[ 60s ] thds: 8 tps: 1242.00 qps: 7452.01 lat (ms,95%): 11.24 err/s: 0.00 # full recovery
The Router maintained the connection to the degraded primary throughout — no failover triggered. TPS recovered to ~1242 once the chaos cleared.
➤ # GTIDs — all match ✅
pod-0: dcb462a5-...:1-4, e8ac711c-...:1-580:1000093-1000115
pod-1: dcb462a5-...:1-4, e8ac711c-...:1-580:1000093-1000115
pod-2: dcb462a5-...:1-4, e8ac711c-...:1-580:1000093-1000115
Result: PASS — Severe performance degradation but no data loss, no failover, zero errors.
Clean up:
➤ kubectl delete -f tests/04-io-latency.yaml
iochaos.chaos-mesh.org "mysql-primary-io-latency" deleted
InnoDB Chaos#5: Network Latency (1s) Between Primary and Replicas
Inject 1-second network delay between the primary and all standby replicas. Group Replication uses Paxos consensus — every write must be acknowledged by the majority. With 1s latency on every packet, writes become extremely slow but the cluster stays operational.
Save this yaml as tests/05-network-latency.yaml:
apiVersion: chaos-mesh.org/v1alpha1
kind: NetworkChaos
metadata:
name: mysql-replication-latency
namespace: chaos-mesh
spec:
action: delay
mode: one
selector:
namespaces:
- demo
labelSelectors:
"app.kubernetes.io/instance": "mysql-ha-cluster"
"kubedb.com/role": "primary"
target:
mode: all
selector:
namespaces:
- demo
labelSelectors:
"app.kubernetes.io/instance": "mysql-ha-cluster"
"kubedb.com/role": "standby"
delay:
latency: "1s"
jitter: "50ms"
duration: "1m"
direction: both
What this chaos does: Adds 1-second delay (with 50ms jitter) to all GR traffic between the primary and standbys. Every Paxos round-trip waits ≥1s.
-
Expected behavior: 1s delay on GR traffic → each Paxos commit waits ≥1s → TPS near zero but writes still succeed → no failover (delay is within
unreachable_majority_timeout) → recovers after chaos. Zero data loss. -
Actual result: Average TPS 1.26 (99.9% reduction), 95p latency 7.6s. Zero errors, no failover. All 3 members stayed
ONLINE. GTIDs and checksums match. PASS.
➤ kubectl apply -f tests/05-network-latency.yaml
networkchaos.chaos-mesh.org/mysql-replication-latency created
➤ kubectl exec -n demo $SBPOD -- sysbench oltp_write_only \
--mysql-host=mysql-ha-cluster.demo.svc.cluster.local \
--mysql-port=6446 --mysql-user=root --mysql-password="$PASS" \
--mysql-db=sbtest --tables=4 --table-size=50000 \
--threads=8 --time=60 --report-interval=10 run
[ 10s ] thds: 8 tps: 0.80 qps: 8.80 lat (ms,95%): 7615.89 err/s: 0.00
[ 30s ] thds: 8 tps: 1.10 qps: 6.60 lat (ms,95%): 6476.48 err/s: 0.00
[ 60s ] thds: 8 tps: 1.20 qps: 7.20 lat (ms,95%): 6476.48 err/s: 0.00
All 3 members stayed ONLINE throughout the latency window. Router did not change route.
Result: PASS — GR tolerated extreme replication latency. Zero data loss, zero errors.
Clean up:
➤ kubectl delete -f tests/05-network-latency.yaml
networkchaos.chaos-mesh.org "mysql-replication-latency" deleted
InnoDB Chaos#6: CPU Stress (98%) on Primary
Apply 98% CPU stress on the primary pod to test how MySQL handles extreme CPU pressure with the Router actively routing traffic.
Save this yaml as tests/06-cpu-stress.yaml:
apiVersion: chaos-mesh.org/v1alpha1
kind: StressChaos
metadata:
name: mysql-primary-cpu-stress
namespace: chaos-mesh
spec:
mode: one
selector:
namespaces:
- demo
labelSelectors:
"app.kubernetes.io/instance": "mysql-ha-cluster"
"kubedb.com/role": "primary"
stressors:
cpu:
workers: 2
load: 98
duration: "1m"
What this chaos does: Consumes 98% of the CPU on the primary pod, leaving minimal CPU for MySQL query processing and Paxos consensus.
-
Expected behavior: Primary CPU saturated at 98% → TPS drops significantly → cluster stays
Ready(no failover — CPU contention slows MySQL but doesn’t make it unresponsive to GR heartbeats) → recovers when chaos ends. Zero data loss. -
Actual result: Baseline 1113 TPS dropped to 188 at peak stress, then stabilized around 810 as MySQL adapted. Average 763 TPS. No failover, zero errors. GTIDs and checksums match. PASS.
➤ kubectl apply -f tests/06-cpu-stress.yaml
stresschaos.chaos-mesh.org/mysql-primary-cpu-stress created
➤ kubectl exec -n demo $SBPOD -- sysbench oltp_write_only \
--mysql-host=mysql-ha-cluster.demo.svc.cluster.local \
--mysql-port=6446 --mysql-user=root --mysql-password="$PASS" \
--mysql-db=sbtest --tables=4 --table-size=50000 \
--threads=8 --time=60 --report-interval=10 run
[ 10s ] thds: 8 tps: 1113.75 qps: 6686.48 lat (ms,95%): 13.22 err/s: 0.00
[ 30s ] thds: 8 tps: 187.50 qps: 1125.00 lat (ms,95%): 137.35 err/s: 0.00 # stress kicks in
[ 50s ] thds: 8 tps: 877.90 qps: 5267.80 lat (ms,95%): 29.19 err/s: 0.00 # adapting
[ 60s ] thds: 8 tps: 810.70 qps: 4863.39 lat (ms,95%): 31.37 err/s: 0.00
Cluster stays Ready — no failover. All 3 members remain ONLINE. Router does not switch routes.
Result: PASS — CPU stress caused temporary TPS drop. Zero data loss, zero errors.
Clean up:
➤ kubectl delete -f tests/06-cpu-stress.yaml
stresschaos.chaos-mesh.org "mysql-primary-cpu-stress" deleted
InnoDB Chaos#7: Packet Loss (30%) Across Cluster
Inject 30% packet loss on all MySQL pods in the cluster. This simulates a degraded network — common in cloud environments with noisy switches or cross-AZ communication issues. Affects both Router-to-MySQL traffic and GR member-to-member traffic.
Save this yaml as tests/07-packet-loss.yaml:
apiVersion: chaos-mesh.org/v1alpha1
kind: NetworkChaos
metadata:
name: mysql-cluster-packet-loss
namespace: chaos-mesh
spec:
action: loss
mode: all
selector:
namespaces:
- demo
labelSelectors:
"app.kubernetes.io/instance": "mysql-ha-cluster"
loss:
loss: "30"
correlation: "25"
duration: "3m"
What this chaos does: Drops 30% of all network packets on every MySQL pod (with 25% correlation — dropped packets cluster together). Affects Router connections and GR inter-node communication.
-
Expected behavior: Packet drops disrupt Router-to-MySQL handshakes and GR heartbeats → some members may go
UNREACHABLE(transient) → TPS collapses but writes still eventually commit → either cluster staysReadyor sees a failover. Zero data loss either way. -
Actual result: Sysbench could not establish new connections through the Router (error 2003), but cluster internals stayed stable — all 3 members
ONLINE, no failover triggered. Notable contrast with standalone GR mode where 30% loss did trigger a failover. GTIDs and checksums match. PASS.
➤ kubectl apply -f tests/07-packet-loss.yaml
networkchaos.chaos-mesh.org/mysql-cluster-packet-loss created
➤ # Sysbench through Router fails to connect
kubectl exec -n demo $SBPOD -- sysbench oltp_write_only \
--mysql-host=mysql-ha-cluster.demo.svc.cluster.local \
--mysql-port=6446 --mysql-user=root --mysql-password="$PASS" \
--mysql-db=sbtest --tables=4 --table-size=50000 \
--threads=8 --time=60 run
FATAL: unable to connect to MySQL server (error 2003)
Inside the cluster, GR members stayed ONLINE — no failover:
➤ SELECT MEMBER_HOST, MEMBER_PORT, MEMBER_STATE, MEMBER_ROLE
FROM performance_schema.replication_group_members;
+-----------------------------------------------+-------------+--------------+-------------+
| MEMBER_HOST | MEMBER_PORT | MEMBER_STATE | MEMBER_ROLE |
+-----------------------------------------------+-------------+--------------+-------------+
| mysql-ha-cluster-2.mysql-ha-cluster-pods.demo | 3306 | ONLINE | PRIMARY |
| mysql-ha-cluster-1.mysql-ha-cluster-pods.demo | 3306 | ONLINE | SECONDARY |
| mysql-ha-cluster-0.mysql-ha-cluster-pods.demo | 3306 | ONLINE | SECONDARY |
+-----------------------------------------------+-------------+--------------+-------------+
After packet loss removed, sysbench reconnects through the Router and TPS returns to baseline.
Result: PASS — Cluster remained stable despite packet loss. Router connections degraded but no data loss, no split brain, no errant GTIDs.
Clean up:
➤ kubectl delete -f tests/07-packet-loss.yaml
networkchaos.chaos-mesh.org "mysql-cluster-packet-loss" deleted
InnoDB Chaos#8: Full Cluster Kill (All 3 Pods)
The ultimate stress test for the InnoDB Cluster path: force-delete all 3 MySQL pods simultaneously. There is no surviving primary; the coordinator must detect the complete outage, pick the pod with the highest GTID, and invoke dba.rebootClusterFromCompleteOutage() to restore the cluster.
-
Expected behavior: All 3 pods deleted → no primary → coordinator detects complete outage, identifies pod with highest GTID → invokes
dba.rebootClusterFromCompleteOutage()from that pod → other pods rejoin → Router re-discovers topology and resumes routing → cluster returns toReady. Zero data loss. -
Actual result: Pod-0 elected as bootstrap candidate (max-transacted pod).
rebootClusterFromCompleteOutagesucceeded, pod-1 and pod-2 rejoined. Recovery in ~60s. All 3 membersONLINE, GTIDs and checksums match. PASS.
Force-delete all 3 MySQL pods simultaneously:
➤ kubectl delete pod -n demo mysql-ha-cluster-0 mysql-ha-cluster-1 mysql-ha-cluster-2 \
--force --grace-period=0
Warning: Immediate deletion does not wait for confirmation that the running resource has been terminated.
pod "mysql-ha-cluster-0" force deleted
pod "mysql-ha-cluster-1" force deleted
pod "mysql-ha-cluster-2" force deleted
The coordinator detected the complete outage and initiated rebootClusterFromCompleteOutage:
➤ kubectl logs -n demo mysql-ha-cluster-0 -c mysql-coordinator | tail
I0413 07:47:51 mysql.go:563] pod mysql-ha-cluster-0 is a superset of all other pods
I0413 07:47:51 mysql.go:622] max transacted pod: mysql-ha-cluster-0
I0413 07:47:51 mysql.go:435] all peers acknowledged bootstrap
I0413 07:47:51 mysql.go:364] cluster rebooting from complete outage
Pod-0 was elected as the bootstrap candidate (highest GTID), bootstrapped the cluster, and pod-1 and pod-2 rejoined. Full recovery in ~60 seconds:
➤ kubectl get mysql,pods -n demo -L kubedb.com/role
NAME VERSION STATUS AGE ROLE
mysql.kubedb.com/mysql-ha-cluster 8.4.8 Ready 3h42m
NAME READY STATUS RESTARTS AGE ROLE
pod/mysql-ha-cluster-0 2/2 Running 0 4m primary
pod/mysql-ha-cluster-1 2/2 Running 0 3m standby
pod/mysql-ha-cluster-2 2/2 Running 0 3m standby
pod/mysql-ha-cluster-router-0 1/1 Running 0 63m
➤ SELECT MEMBER_HOST, MEMBER_PORT, MEMBER_STATE, MEMBER_ROLE
FROM performance_schema.replication_group_members;
+-----------------------------------------------+-------------+--------------+-------------+
| MEMBER_HOST | MEMBER_PORT | MEMBER_STATE | MEMBER_ROLE |
+-----------------------------------------------+-------------+--------------+-------------+
| mysql-ha-cluster-2.mysql-ha-cluster-pods.demo | 3306 | ONLINE | SECONDARY |
| mysql-ha-cluster-1.mysql-ha-cluster-pods.demo | 3306 | ONLINE | SECONDARY |
| mysql-ha-cluster-0.mysql-ha-cluster-pods.demo | 3306 | ONLINE | PRIMARY |
+-----------------------------------------------+-------------+--------------+-------------+
The Router pod kept running through the outage and re-discovered the new topology automatically as the cluster came back up.
Result: PASS — Cluster rebooted from complete outage automatically. Zero data loss, GTIDs and checksums match across all 3 nodes.
InnoDB Chaos#9: Double Primary Kill
Kill the primary, wait for a new primary to be elected and the Router to re-route, then immediately kill the new primary. Tests whether the InnoDB Cluster path survives two consecutive leader failures with the Router in the middle.
-
Expected behavior: First primary killed → new primary elected → Router re-routes (1st time) → second primary killed → third primary elected → Router re-routes (2nd time) → both killed pods rejoin → cluster returns to
Ready. Zero data loss despite rapid leader churn. -
Actual result: Pod-0 killed → pod-2 elected → pod-2 killed → pod-1 elected as third primary. Both killed pods rejoined as secondary. Router re-routed correctly after each failover. GTIDs and checksums match across all 3 nodes. PASS.
➤ # First kill — pod-0 (PRIMARY)
kubectl delete pod mysql-ha-cluster-0 -n demo --force --grace-period=0
pod "mysql-ha-cluster-0" force deleted
➤ # Wait for failover
mysql -h mysql-ha-cluster -P 6446 -e "SELECT @@hostname;"
@@hostname
mysql-ha-cluster-2 # new primary
➤ # Second kill — pod-2 (new PRIMARY)
kubectl delete pod mysql-ha-cluster-2 -n demo --force --grace-period=0
pod "mysql-ha-cluster-2" force deleted
➤ # Router re-routes again
mysql -h mysql-ha-cluster -P 6446 -e "SELECT @@hostname;"
@@hostname
mysql-ha-cluster-1 # third primary
After both kills, both pods rejoined as SECONDARY and the cluster returned to Ready:
➤ SELECT MEMBER_HOST, MEMBER_PORT, MEMBER_STATE, MEMBER_ROLE
FROM performance_schema.replication_group_members;
+-----------------------------------------------+-------------+--------------+-------------+
| MEMBER_HOST | MEMBER_PORT | MEMBER_STATE | MEMBER_ROLE |
+-----------------------------------------------+-------------+--------------+-------------+
| mysql-ha-cluster-2.mysql-ha-cluster-pods.demo | 3306 | ONLINE | SECONDARY |
| mysql-ha-cluster-1.mysql-ha-cluster-pods.demo | 3306 | ONLINE | PRIMARY |
| mysql-ha-cluster-0.mysql-ha-cluster-pods.demo | 3306 | ONLINE | SECONDARY |
+-----------------------------------------------+-------------+--------------+-------------+
Result: PASS — Survived two consecutive primary failures. Router re-routed twice. Zero data loss.
InnoDB Chaos#10: Rolling Restart (0→1→2)
Sequentially force-delete pod-0, pod-1, and pod-2, simulating a rolling upgrade. Each pod should rejoin before the next is killed; the Router follows the topology throughout.
-
Expected behavior: Pods deleted sequentially (0 → 1 → 2) → each rejoins before the next is killed → primary is maintained throughout (unless the current primary is hit, in which case Router re-routes) → cluster returns to
Readybetween steps. Zero data loss. -
Actual result: Pod-1 maintained PRIMARY throughout. Pod-2 needed the coordinator’s 10-attempt restart cycle before rejoining (known behaviour for rapid rejoin), then joined cleanly. All 3 members
ONLINE, GTIDs and checksums match. PASS.
➤ kubectl delete pod mysql-ha-cluster-0 -n demo --force --grace-period=0
➤ # wait ~40s for rejoin
➤ kubectl delete pod mysql-ha-cluster-1 -n demo --force --grace-period=0
➤ # wait ~40s for rejoin
➤ kubectl delete pod mysql-ha-cluster-2 -n demo --force --grace-period=0
Final state — all 3 members ONLINE, pod-1 still PRIMARY:
➤ SELECT MEMBER_HOST, MEMBER_PORT, MEMBER_STATE, MEMBER_ROLE
FROM performance_schema.replication_group_members;
+-----------------------------------------------+-------------+--------------+-------------+
| MEMBER_HOST | MEMBER_PORT | MEMBER_STATE | MEMBER_ROLE |
+-----------------------------------------------+-------------+--------------+-------------+
| mysql-ha-cluster-2.mysql-ha-cluster-pods.demo | 3306 | ONLINE | SECONDARY |
| mysql-ha-cluster-1.mysql-ha-cluster-pods.demo | 3306 | ONLINE | PRIMARY |
| mysql-ha-cluster-0.mysql-ha-cluster-pods.demo | 3306 | ONLINE | SECONDARY |
+-----------------------------------------------+-------------+--------------+-------------+
Result: PASS — Rolling restart completed with zero data loss. Router never lost the primary.
InnoDB Chaos#11: DNS Failure on Primary
Block all DNS resolution on the primary for 3 minutes. GR uses IP addresses internally for inter-node communication, so existing TCP sockets stay up.
Save this yaml as tests/11-dns-error.yaml:
apiVersion: chaos-mesh.org/v1alpha1
kind: DNSChaos
metadata:
name: mysql-dns-error-primary
namespace: chaos-mesh
spec:
action: error
mode: one
selector:
namespaces:
- demo
labelSelectors:
"app.kubernetes.io/instance": "mysql-ha-cluster"
"kubedb.com/role": "primary"
duration: "3m"
What this chaos does: Blocks all DNS resolution on the primary pod for 3 minutes.
-
Expected behavior: DNS resolution blocked on primary → no impact on existing TCP connections (GR uses IP addresses internally) → cluster stays
Ready, no failover. Zero data loss. -
Actual result: Cluster stayed
Readythroughout the 3-minute DNS chaos. No failover, no errors. GTIDs and checksums match. PASS.
➤ kubectl apply -f tests/11-dns-error.yaml
dnschaos.chaos-mesh.org/mysql-dns-error-primary created
➤ kubectl get mysql -n demo
NAME VERSION STATUS AGE
mysql-ha-cluster 8.4.8 Ready 78m
GR members stayed ONLINE throughout:
➤ SELECT MEMBER_HOST, MEMBER_PORT, MEMBER_STATE, MEMBER_ROLE
FROM performance_schema.replication_group_members;
+-----------------------------------------------+-------------+--------------+-------------+
| MEMBER_HOST | MEMBER_PORT | MEMBER_STATE | MEMBER_ROLE |
+-----------------------------------------------+-------------+--------------+-------------+
| mysql-ha-cluster-2.mysql-ha-cluster-pods.demo | 3306 | ONLINE | PRIMARY |
| mysql-ha-cluster-1.mysql-ha-cluster-pods.demo | 3306 | ONLINE | SECONDARY |
| mysql-ha-cluster-0.mysql-ha-cluster-pods.demo | 3306 | ONLINE | SECONDARY |
+-----------------------------------------------+-------------+--------------+-------------+
Result: PASS — DNS failure has no impact on InnoDB Cluster. Zero data loss.
Clean up:
➤ kubectl delete -f tests/11-dns-error.yaml
dnschaos.chaos-mesh.org "mysql-dns-error-primary" deleted
InnoDB Chaos#12: Clock Skew (-5 min)
Shift the primary’s wall clock back by 5 minutes. GR’s Paxos uses logical clocks, so consensus is unaffected; this test confirms that wall-clock drift doesn’t break the cluster or the Router.
Save this yaml as tests/12-clock-skew.yaml:
apiVersion: chaos-mesh.org/v1alpha1
kind: TimeChaos
metadata:
name: mysql-primary-clock-skew
namespace: chaos-mesh
spec:
mode: one
selector:
namespaces:
- demo
labelSelectors:
"app.kubernetes.io/instance": "mysql-ha-cluster"
"kubedb.com/role": "primary"
timeOffset: "-5m"
duration: "3m"
What this chaos does: Shifts the primary’s CLOCK_REALTIME back by 5 minutes for 3 minutes.
-
Expected behavior: Primary wall clock shifted -5 min → GR’s Paxos uses logical clocks, so consensus unaffected → no failover → cluster stays
Ready. Zero data loss. -
Actual result: Primary showed time 5 minutes behind secondaries; cluster stayed
Readythroughout, no failover, no errors. GTIDs and checksums match. PASS — confirms logical-clock-based Paxos.
➤ kubectl apply -f tests/12-clock-skew.yaml
timechaos.chaos-mesh.org/mysql-primary-clock-skew created
➤ # During chaos — primary shows time 5 min behind secondaries
kubectl exec -n demo mysql-ha-cluster-1 -c mysql -- date
# Tue Apr 13 08:24:44 UTC 2026
kubectl exec -n demo mysql-ha-cluster-0 -c mysql -- date
# Tue Apr 13 08:29:44 UTC 2026
No impact — GR’s Paxos uses logical clocks for consensus, not wall-clock time:
➤ kubectl get mysql -n demo
NAME VERSION STATUS AGE
mysql-ha-cluster 8.4.8 Ready 82m
➤ SELECT MEMBER_HOST, MEMBER_PORT, MEMBER_STATE, MEMBER_ROLE
FROM performance_schema.replication_group_members;
+-----------------------------------------------+-------------+--------------+-------------+
| MEMBER_HOST | MEMBER_PORT | MEMBER_STATE | MEMBER_ROLE |
+-----------------------------------------------+-------------+--------------+-------------+
| mysql-ha-cluster-2.mysql-ha-cluster-pods.demo | 3306 | ONLINE | PRIMARY |
| mysql-ha-cluster-1.mysql-ha-cluster-pods.demo | 3306 | ONLINE | SECONDARY |
| mysql-ha-cluster-0.mysql-ha-cluster-pods.demo | 3306 | ONLINE | SECONDARY |
+-----------------------------------------------+-------------+--------------+-------------+
Result: PASS — Clock skew tolerated. Zero data loss, no failover.
Clean up:
➤ kubectl delete -f tests/12-clock-skew.yaml
timechaos.chaos-mesh.org "mysql-primary-clock-skew" deleted
InnoDB Chaos#13: Pod Failure on Primary (5 min)
Inject a 5-minute pod-failure fault into the current primary pod — Chaos Mesh keeps the primary’s MySQL container in a failed state for the full duration. Unlike Chaos#1 (pod-kill), the pod is not deleted and rescheduled; it stays in place but its container is unavailable. This exercises long-duration primary unreachability and the operator’s clone-vs-incremental rejoin logic, with the primary service following the failover throughout.
Save this yaml as tests/13-pod-failure.yaml:
apiVersion: chaos-mesh.org/v1alpha1
kind: PodChaos
metadata:
name: test-kubedb-primary-pod-failure
namespace: demo
spec:
selector:
namespaces:
- demo
labelSelectors:
app.kubernetes.io/component: database
app.kubernetes.io/managed-by: kubedb.com
app.kubernetes.io/name: mysqls.kubedb.com
kubedb.com/role: primary
mode: all
action: pod-failure
duration: 5m
What this chaos does: Holds the primary pod’s MySQL container in a failed state for 5 minutes (the pod stays in place; its container restarts repeatedly during the window).
-
Expected behavior: Primary container becomes unavailable → cluster transitions
Ready→NotReady→ GR elects a new primary, primary service follows the new role label → state moves toCritical. When the 5-minute fault clears, the original primary’s container restarts, rejoins asSECONDARY, and the cluster returns toReady. Zero data loss. -
Actual result: Cluster transitioned
Ready→NotReady(+5s) →Critical(+20s) after pod-2 was elected new PRIMARY. Sysbench (no auto-reconnect) dropped at +5s with error 2013; a fresh sysbench session against the primary service ran cleanly at ~1400 TPS within seconds of the failover. Chaos auto-cleared at +5m; pod-0 restarted 4× during the window, rejoined asSECONDARY, and the cluster returned toReadyat +8m. GTIDs match exactly on all 3 nodes (87aa5caa-…:1-42972:1000103-1000312). PASS.
Pre-chaos baseline:
➤ kubectl get mysql.kubedb.com/mysql-ha-cluster -n demo
NAME VERSION STATUS AGE
mysql-ha-cluster 8.4.8 Ready 17m
➤ kubectl get pods -n demo -l app.kubernetes.io/instance=mysql-ha-cluster -L kubedb.com/role
NAME READY STATUS RESTARTS AGE ROLE
mysql-ha-cluster-0 2/2 Running 0 17m primary
mysql-ha-cluster-1 2/2 Running 0 16m standby
mysql-ha-cluster-2 2/2 Running 0 16m standby
mysql-ha-cluster-router-0 1/1 Running 0 16m
mysql-ha-cluster-router-1 1/1 Running 0 16m
➤ SELECT MEMBER_HOST, MEMBER_PORT, MEMBER_STATE, MEMBER_ROLE
FROM performance_schema.replication_group_members;
+-----------------------------------------------+-------------+--------------+-------------+
| MEMBER_HOST | MEMBER_PORT | MEMBER_STATE | MEMBER_ROLE |
+-----------------------------------------------+-------------+--------------+-------------+
| mysql-ha-cluster-2.mysql-ha-cluster-pods.demo | 3306 | ONLINE | SECONDARY |
| mysql-ha-cluster-1.mysql-ha-cluster-pods.demo | 3306 | ONLINE | SECONDARY |
| mysql-ha-cluster-0.mysql-ha-cluster-pods.demo | 3306 | ONLINE | PRIMARY |
+-----------------------------------------------+-------------+--------------+-------------+
Apply the chaos and observe — sysbench was running through the primary service when chaos hit:
➤ kubectl apply -f tests/13-pod-failure.yaml
podchaos.chaos-mesh.org/test-kubedb-primary-pod-failure created
➤ # sysbench through primary service — connection died at +5s, no auto-reconnect
[ 5s ] thds: 8 tps: 263.97 qps: 1591.85 lat (ms,95%): 101.13 err/s: 0.00 reconn/s: 0.00
FATAL: mysql_stmt_execute() returned error 2013 (Lost connection to MySQL server during query) for query 'COMMIT'
FATAL: `thread_run' function failed: SQL error, errno = 2013, state = 'HY000': Lost connection
Note: sysbench’s default behaviour is to abort on connection loss. Production applications using a connection pool with retry logic experience a brief blip and reconnect to the primary service automatically (port 3306 follows the new primary).
During chaos — pod-2 elected as new PRIMARY, cluster Critical:
➤ kubectl get mysql.kubedb.com/mysql-ha-cluster -n demo
NAME VERSION STATUS AGE
mysql-ha-cluster 8.4.8 Critical 17m
➤ kubectl get pods -n demo -l app.kubernetes.io/instance=mysql-ha-cluster -L kubedb.com/role
NAME READY STATUS RESTARTS AGE ROLE
mysql-ha-cluster-0 2/2 Running 0 17m standby # under chaos, container failing
mysql-ha-cluster-1 2/2 Running 0 17m standby
mysql-ha-cluster-2 2/2 Running 0 17m primary # promoted
➤ # Fresh sysbench session through the primary service — full TPS on new primary
kubectl exec -n demo $SBPOD -- sysbench oltp_write_only \
--mysql-host=mysql-ha-cluster.demo.svc.cluster.local \
--mysql-port=3306 --mysql-user=root --mysql-password="$PASS" \
--mysql-db=sbtest --tables=4 --table-size=50000 \
--threads=8 --time=15 --report-interval=5 run
events (avg/stddev): 2633.3750/65.35
execution time (avg/stddev): 14.9955/0.00 # ~1404 TPS aggregate, no errors
After chaos cleared (+5m) — pod-0 restarted (Restarts: 4) and rejoined as SECONDARY:
➤ kubectl get mysql.kubedb.com/mysql-ha-cluster -n demo
NAME VERSION STATUS AGE
mysql-ha-cluster 8.4.8 Ready 25m
➤ kubectl get pods -n demo -l app.kubernetes.io/instance=mysql-ha-cluster -L kubedb.com/role
NAME READY STATUS RESTARTS AGE ROLE
mysql-ha-cluster-0 2/2 Running 4 (2m44s ago) 25m standby
mysql-ha-cluster-1 2/2 Running 0 25m standby
mysql-ha-cluster-2 2/2 Running 0 25m primary
➤ SELECT MEMBER_HOST, MEMBER_PORT, MEMBER_STATE, MEMBER_ROLE
FROM performance_schema.replication_group_members;
+-----------------------------------------------+-------------+--------------+-------------+
| MEMBER_HOST | MEMBER_PORT | MEMBER_STATE | MEMBER_ROLE |
+-----------------------------------------------+-------------+--------------+-------------+
| mysql-ha-cluster-2.mysql-ha-cluster-pods.demo | 3306 | ONLINE | PRIMARY |
| mysql-ha-cluster-1.mysql-ha-cluster-pods.demo | 3306 | ONLINE | SECONDARY |
| mysql-ha-cluster-0.mysql-ha-cluster-pods.demo | 3306 | ONLINE | SECONDARY |
+-----------------------------------------------+-------------+--------------+-------------+
➤ # GTIDs — all match ✅
pod-0: 7b919383-...:1-4, 87aa5caa-...:1-42972:1000103-1000312
pod-1: 7b919383-...:1-4, 87aa5caa-...:1-42972:1000103-1000312
pod-2: 7b919383-...:1-4, 87aa5caa-...:1-42972:1000103-1000312
Observed timeline:
| Wall-clock | Δ from chaos | Event | DB Status |
|---|---|---|---|
| 17:10:03 | — | Pre-chaos baseline (pod-0 = primary, all 3 ONLINE) | Ready |
| 17:10:22 | 0s | PodChaos applied (pod-0 targeted) |
Ready |
| 17:10:27 | +5s | Primary unreachable detected | NotReady |
| 17:10:42 | +20s | pod-2 promoted PRIMARY, primary service follows | Critical |
| 17:15:22 | +5m00s | Chaos auto-recovered, pod-0 container restarts | Critical |
| ~17:15–18 | +5–8m | pod-0 in RECOVERING (incremental rejoin) |
Critical |
| 17:18:36 | +8m14s | pod-0 reaches ONLINE SECONDARY, GTIDs match |
Ready |
Result: PASS — Failover completed in 20s, the primary service routed RW to the new primary transparently, and the chaos-impacted pod rejoined automatically once the fault expired. Zero data loss. Note: applications without auto-reconnect (like default sysbench) will need to retry the connection — KubeDB drives the cluster recovery, not the client.
Clean up:
➤ kubectl delete -f tests/13-pod-failure.yaml
podchaos.chaos-mesh.org "test-kubedb-primary-pod-failure" deleted
InnoDB Chaos#14: Continuous OOM Loop on Primary (10× 30s)
Inject a tight loop of memory-stress chaos against the current primary to mimic a runaway query / leak that keeps OOM-killing mysqld while sysbench keeps writing through the primary service. Each iteration allocates 100GB with oomScoreAdj=-1000 so the kernel kills the MySQL container almost immediately. Ten iterations are applied back-to-back (≈ 5 minutes of effective OOM pressure). This surfaces the InnoDB Cluster coordinator’s errant-GTID handling and the cluster’s behaviour when the same pod cycles through CrashLoopBackOff repeatedly.
Save this yaml as tests/14-oom-loop.yaml (replace <primary-pod-name> with the actual current primary):
apiVersion: chaos-mesh.org/v1alpha1
kind: StressChaos
metadata:
name: test-kubedb-primary-pod-oom
namespace: demo
spec:
selector:
namespaces: [demo]
labelSelectors:
statefulset.kubernetes.io/pod-name: <primary-pod-name>
mode: all
stressors:
memory:
workers: 1
size: "100GB"
oomScoreAdj: -1000
duration: 30s
Run a tight loop so the OOM keeps recurring (a single 30 s pulse may be absorbed before sysbench notices the failover):
PRIMARY=$(kubectl get pods -n demo \
-l app.kubernetes.io/name=mysqls.kubedb.com,kubedb.com/role=primary \
-o jsonpath='{.items[0].metadata.name}')
for i in $(seq 1 10); do
sed "s/test-kubedb-primary-pod-oom/test-kubedb-primary-pod-oom-${i}/; s/<primary-pod-name>/${PRIMARY}/" \
tests/14-oom-loop.yaml | kubectl apply -f -
sleep 2
done
What this chaos does: Repeatedly OOM-kills the primary’s mysqld container by scheduling 10 back-to-back 30-second 100GB stressors with oomScoreAdj=-1000.
-
Expected behavior: Primary container OOMKilled repeatedly → cluster transitions
Ready→NotReadyonce the primary becomes unreachable → GR fails over to a healthy secondary, primary service follows the new role label → original primary cyclesCrashLoopBackOffuntil OOM pressure clears → afterwards the pod rejoins via incremental recovery, GTIDs reconcile, and the cluster returns toReady. Zero data loss expected. -
Actual result: pod-2 (primary at start) was OOMKilled repeatedly —
Restarts: 3over the 5-minute window. pod-1 was promotedPRIMARY; the primary service routed RW to pod-1 within seconds. Sysbench (no auto-reconnect) lost connection at the first OOM and the test thread aborted. By +90 seconds the cluster was alreadyReadywith all 3 membersONLINE— pod-2 rejoined asSECONDARYcleanly. No errant GTIDs were reported by the coordinator (the OOM hit before any locally-committed-but-unreplicated transactions accumulated), so no manual approval was required. GTIDs match exactly on all 3 nodes (87aa5caa-…:1-49173:1000103-1000354). PASS.
➤ # During chaos — pod-1 promoted, pod-2 cycling restarts
➤ kubectl get mysql.kubedb.com/mysql-ha-cluster -n demo
NAME VERSION STATUS AGE
mysql-ha-cluster 8.4.8 Ready 30m
➤ kubectl get pods -n demo -l app.kubernetes.io/instance=mysql-ha-cluster -L kubedb.com/role
NAME READY STATUS RESTARTS AGE ROLE
mysql-ha-cluster-0 2/2 Running 4 (7m ago) 29m standby
mysql-ha-cluster-1 2/2 Running 0 29m primary # promoted
mysql-ha-cluster-2 2/2 Running 3 (99s ago) 29m standby # OOM-cycled, rejoined
mysql-ha-cluster-router-0 1/1 Running 0 29m
mysql-ha-cluster-router-1 1/1 Running 0 29m
➤ # GR view (consistent across all 3 nodes)
SELECT MEMBER_HOST, MEMBER_PORT, MEMBER_STATE, MEMBER_ROLE
FROM performance_schema.replication_group_members;
+-----------------------------------------------+-------------+--------------+-------------+
| MEMBER_HOST | MEMBER_PORT | MEMBER_STATE | MEMBER_ROLE |
+-----------------------------------------------+-------------+--------------+-------------+
| mysql-ha-cluster-2.mysql-ha-cluster-pods.demo | 3306 | ONLINE | SECONDARY |
| mysql-ha-cluster-1.mysql-ha-cluster-pods.demo | 3306 | ONLINE | PRIMARY |
| mysql-ha-cluster-0.mysql-ha-cluster-pods.demo | 3306 | ONLINE | SECONDARY |
+-----------------------------------------------+-------------+--------------+-------------+
➤ # Coordinator did not detect any errant GTIDs — clean rejoin
kubectl logs -n demo mysql-ha-cluster-2 -c mysql-coordinator | grep -i errant
(no output)
➤ # GTIDs — all match ✅
pod-0: 7b919383-...:1-4, 87aa5caa-...:1-49173:1000103-1000354
pod-1: 7b919383-...:1-4, 87aa5caa-...:1-49173:1000103-1000354
pod-2: 7b919383-...:1-4, 87aa5caa-...:1-49173:1000103-1000354
➤ # Post-chaos sysbench — full TPS restored on the new primary
kubectl exec -n demo $SBPOD -- sysbench oltp_write_only \
--mysql-host=mysql-ha-cluster.demo.svc.cluster.local \
--mysql-port=3306 --mysql-user=root --mysql-password="$PASS" \
--mysql-db=sbtest --tables=4 --table-size=50000 \
--threads=8 --time=10 --report-interval=5 run
events (avg/stddev): 1820.1250/58.58 # ~1456 TPS aggregate
95th percentile: 8.43 ms
ignored errors: 0
Observed timeline:
| Wall-clock | Δ from chaos | Event | DB Status |
|---|---|---|---|
| 17:20:56 | — | Pre-chaos baseline (pod-2 = primary, all 3 ONLINE) | Ready |
| 17:20:59 | 0s | First OOM iteration applied to pod-2 | Ready |
| 17:21:21 | +22s | All 10 iterations queued (covers ~5 min of OOM pressure) | Critical |
| ~17:21–22 | +1–2m | pod-1 promoted PRIMARY, pod-2 cycling restarts (×3) | Critical |
| 17:23:00 | +2m01s | pod-2 reached ONLINE SECONDARY, all 3 ONLINE |
Ready |
Notable safety behaviour — the KubeDB coordinator includes an errant-GTID gate that refuses to auto-clone a recovering pod when extra GTIDs (locally committed transactions that never replicated) are present. In this test no errant GTIDs surfaced because the OOM struck before such transactions could accumulate, so the gate stayed quiet and pod-2 rejoined incrementally. If the OOM had landed after a partial commit, you would see the coordinator log:
WARNING: instance mysql-ha-cluster-X has extra GTIDs not on primary:
<uuid>:N-M (these will be lost if clone proceeds)
to approve clone, create the file:
kubectl exec -n demo mysql-ha-cluster-X -c mysql -- touch /scripts/approve-clone
That deliberate refusal is the right behaviour: it surfaces the choice between data preservation (manual reconciliation) and rejoining via clone (which would discard the extra GTIDs).
Result: PASS — Group Replication failed over to a healthy secondary, the primary service routed transparently, and the cluster fully reconverged in ~2 minutes after OOM pressure cleared. Zero data loss, GTIDs perfectly aligned across all 3 nodes.
Clean up:
➤ kubectl delete stresschaos -n demo --all
InnoDB Chaos#15: 100% Outbound Packet Loss on Primary (2 min)
Drop 100% of outbound packets from the primary for 2 minutes — the primary is alive and accepting reads locally, but cannot ship anything to the secondaries (heartbeats, binlog events, GR Paxos messages all dropped). This is the network equivalent of a one-way mute and is one of the harshest single-node faults you can inject without killing the process.
Save this yaml as tests/15-packet-loss.yaml:
apiVersion: chaos-mesh.org/v1alpha1
kind: NetworkChaos
metadata:
name: test-kubedb-primary-network-loss
namespace: demo
spec:
selector:
namespaces: [demo]
labelSelectors:
app.kubernetes.io/component: database
app.kubernetes.io/managed-by: kubedb.com
app.kubernetes.io/name: mysqls.kubedb.com
kubedb.com/role: primary
mode: all
action: loss
loss:
loss: '100'
correlation: '100'
direction: to
duration: 2m
What this chaos does: Drops every outbound packet from the primary pod for 2 minutes. The primary’s local socket still works (mysqld stays up), but it cannot send GR Paxos messages or binlog events to its peers.
-
Expected behavior: Primary cannot communicate with the group → secondaries notice the primary timed out → cluster transitions
Ready→NotReady→Critical→ secondaries form a quorum and elect a new primary, primary service follows the new role label → after the loss clears, the old primary rejoins asSECONDARYand the cluster returns toReady. Zero data loss. -
Actual result: Cluster transitioned
Ready→NotReady(+30s) →Critical(+60s) after pod-2 was elected new PRIMARY. Dual-primarykubedb.com/rolelabel transient observed for ~150s — pod-1 (old primary, isolated) and pod-2 (new primary) both carriedkubedb.com/role=primaryin Kubernetes labels because each pod’s coordinator reported its own local view ofperformance_schema.replication_group_members. GR itself remained Single-Primary (only the majority partition could commit) — this is a label-layer transient, not a data-layer split. Once chaos auto-cleared at +2m, pod-1’s local view converged and the label corrected tostandby. ClusterReadyat +3m49s. Final GTIDs match exactly on all 3 nodes (87aa5caa-…:1-68238:1000103-1000444). PASS.
➤ kubectl apply -f tests/15-packet-loss.yaml
networkchaos.chaos-mesh.org/test-kubedb-primary-network-loss created
➤ # +30s — DUAL PRIMARY LABEL transient (pod-1 was primary, pod-2 newly promoted)
kubectl get pods -n demo -l app.kubernetes.io/instance=mysql-ha-cluster -L kubedb.com/role
NAME READY STATUS RESTARTS AGE ROLE
mysql-ha-cluster-0 2/2 Running 4 62m standby
mysql-ha-cluster-1 2/2 Running 0 62m primary # stale local view
mysql-ha-cluster-2 2/2 Running 3 62m primary # majority view
mysql-ha-cluster-router-0 1/1 Running 0 62m
mysql-ha-cluster-router-1 1/1 Running 0 62m
➤ kubectl get mysql.kubedb.com/mysql-ha-cluster -n demo
NAME VERSION STATUS AGE
mysql-ha-cluster 8.4.8 Critical 62m
➤ # GR view from a healthy member (correct majority view)
SELECT MEMBER_HOST, MEMBER_PORT, MEMBER_STATE, MEMBER_ROLE
FROM performance_schema.replication_group_members;
+-----------------------------------------------+-------------+--------------+-------------+
| MEMBER_HOST | MEMBER_PORT | MEMBER_STATE | MEMBER_ROLE |
+-----------------------------------------------+-------------+--------------+-------------+
| mysql-ha-cluster-2.mysql-ha-cluster-pods.demo | 3306 | ONLINE | PRIMARY |
| mysql-ha-cluster-1.mysql-ha-cluster-pods.demo | 3306 | ONLINE | SECONDARY |
| mysql-ha-cluster-0.mysql-ha-cluster-pods.demo | 3306 | ONLINE | SECONDARY |
+-----------------------------------------------+-------------+--------------+-------------+
After chaos auto-cleared (+2 min) — pod-1’s local view converged, label corrected, cluster returned to Ready:
➤ kubectl get mysql.kubedb.com/mysql-ha-cluster -n demo
NAME VERSION STATUS AGE
mysql-ha-cluster 8.4.8 Ready 65m
➤ kubectl get pods -n demo -l app.kubernetes.io/instance=mysql-ha-cluster -L kubedb.com/role
NAME READY STATUS RESTARTS AGE ROLE
mysql-ha-cluster-0 2/2 Running 4 65m standby
mysql-ha-cluster-1 2/2 Running 0 65m standby # corrected
mysql-ha-cluster-2 2/2 Running 3 65m primary
➤ # GTIDs — all match ✅
pod-0: 7b919383-...:1-4, 87aa5caa-...:1-68238:1000103-1000444
pod-1: 7b919383-...:1-4, 87aa5caa-...:1-68238:1000103-1000444
pod-2: 7b919383-...:1-4, 87aa5caa-...:1-68238:1000103-1000444
Observed timeline:
| Wall-clock | Δ from chaos | Event | DB Status |
|---|---|---|---|
| 17:54:50 | — | Pre-chaos baseline (pod-1 = primary) | Ready |
| 17:54:53 | 0s | 100% outbound loss applied to pod-1 | Ready |
| 17:55:23 | +30s | Operator probe times out; dual-primary label appears | NotReady |
| 17:55:53 | +60s | pod-2 promoted PRIMARY, primary service follows | Critical |
| 17:56:53 | +2m00s | Chaos auto-recovered, packet flow restored | Critical |
| 17:57:23 | +2m30s | pod-1’s local GR view converges; label corrected to standby |
Critical |
| 17:58:42 | +3m49s | pod-1 rejoined as ONLINE SECONDARY, all 3 ONLINE |
Ready |
Note on the dual-primary label — this is a known InnoDB Cluster behaviour during one-way network isolation: each pod’s mysql-coordinator sidecar updates its own pod’s kubedb.com/role label from its own local copy of replication_group_members. While the network is one-way down, the isolated pod still sees itself as PRIMARY in its own view, even though the majority partition has elected a new primary. No split-brain at the data layer — the isolated pod cannot replicate any commits because its outbound traffic is dropped. Once the network heals, the isolated pod’s view reconciles within seconds and the label corrects automatically.
Result: PASS — failover and recovery completed in 3m49s end-to-end. Zero data loss, GTIDs perfectly aligned across all 3 nodes. The dual-label transient is a UI artifact, not a correctness issue, and resolves automatically.
Clean up:
➤ kubectl delete -f tests/15-packet-loss.yaml
networkchaos.chaos-mesh.org "test-kubedb-primary-network-loss" deleted
InnoDB Chaos#16: 100% Outbound Packet Corruption on Primary (2 min)
Flip random bits in 100% of outbound packets from the primary for 2 minutes. Unlike packet loss, every corrupted packet fails its TCP checksum on receipt — the receiver discards it and waits for a retransmit, but the retransmit is also corrupted, and so on. Effectively the primary cannot deliver any meaningful traffic to the secondaries: a slower-burning version of 100% packet loss.
Save this yaml as tests/16-packet-corrupt.yaml:
apiVersion: chaos-mesh.org/v1alpha1
kind: NetworkChaos
metadata:
name: test-kubedb-primary-network-corrupt
namespace: demo
spec:
selector:
namespaces: [demo]
labelSelectors:
app.kubernetes.io/component: database
app.kubernetes.io/managed-by: kubedb.com
app.kubernetes.io/name: mysqls.kubedb.com
kubedb.com/role: primary
mode: all
action: corrupt
corrupt:
corrupt: '100'
correlation: '100'
direction: to
duration: 2m
What this chaos does: Bit-flips every outbound packet from the primary for 2 minutes. TCP checksum validation rejects each packet at the receiver, which is functionally equivalent to 100% packet loss but stresses TCP’s retransmission machinery rather than its drop detection.
-
Expected behavior: Primary’s outbound traffic is undeliverable → secondaries notice the primary is unresponsive → cluster transitions
Ready→NotReady→Critical→ secondaries elect a new primary, primary service follows → after corruption clears, the old primary rejoins asSECONDARY. Zero data loss. -
Actual result: Cluster transitioned
Ready→NotReady(+30s) → dual-primary label transient (pod-1 + pod-2) at +30–120s →Critical(+60s) after pod-1 elected new PRIMARY → chaos auto-cleared at +2m, pod-2’s label corrected tostandbyshortly after →Ready(+3m01s). Final GTIDs match exactly on all 3 nodes (87aa5caa-…:1-70911:1000103-1000531). PASS.
➤ kubectl apply -f tests/16-packet-corrupt.yaml
networkchaos.chaos-mesh.org/test-kubedb-primary-network-corrupt created
➤ # +30s — dual-primary label transient (same as Chaos#15)
kubectl get pods -n demo -l app.kubernetes.io/instance=mysql-ha-cluster -L kubedb.com/role
NAME READY STATUS RESTARTS ROLE
mysql-ha-cluster-0 2/2 Running 4 standby
mysql-ha-cluster-1 2/2 Running 0 primary # newly promoted
mysql-ha-cluster-2 2/2 Running 3 primary # was primary, corrupted, stale view
➤ kubectl get mysql.kubedb.com/mysql-ha-cluster -n demo
NAME VERSION STATUS AGE
mysql-ha-cluster 8.4.8 Critical 142m
After corruption cleared (+2 min) and pod-2 rejoined:
➤ kubectl get mysql.kubedb.com/mysql-ha-cluster -n demo
NAME VERSION STATUS AGE
mysql-ha-cluster 8.4.8 Ready 144m
➤ kubectl get pods -n demo -l app.kubernetes.io/instance=mysql-ha-cluster -L kubedb.com/role
NAME READY STATUS RESTARTS ROLE
mysql-ha-cluster-0 2/2 Running 4 standby
mysql-ha-cluster-1 2/2 Running 0 primary
mysql-ha-cluster-2 2/2 Running 3 standby # corrected
➤ SELECT MEMBER_HOST, MEMBER_PORT, MEMBER_STATE, MEMBER_ROLE
FROM performance_schema.replication_group_members;
+-----------------------------------------------+-------------+--------------+-------------+
| MEMBER_HOST | MEMBER_PORT | MEMBER_STATE | MEMBER_ROLE |
+-----------------------------------------------+-------------+--------------+-------------+
| mysql-ha-cluster-2.mysql-ha-cluster-pods.demo | 3306 | ONLINE | SECONDARY |
| mysql-ha-cluster-1.mysql-ha-cluster-pods.demo | 3306 | ONLINE | PRIMARY |
| mysql-ha-cluster-0.mysql-ha-cluster-pods.demo | 3306 | ONLINE | SECONDARY |
+-----------------------------------------------+-------------+--------------+-------------+
➤ # GTIDs — all match ✅
pod-0: 7b919383-...:1-4, 87aa5caa-...:1-70911:1000103-1000531
pod-1: 7b919383-...:1-4, 87aa5caa-...:1-70911:1000103-1000531
pod-2: 7b919383-...:1-4, 87aa5caa-...:1-70911:1000103-1000531
Observed timeline:
| Wall-clock | Δ from chaos | Event | DB Status |
|---|---|---|---|
| 19:14:09 | — | Pre-chaos baseline (pod-2 = primary) | Ready |
| 19:14:09 | 0s | 100% corruption applied to pod-2 outbound | Ready |
| 19:14:39 | +30s | Operator probe times out; dual-primary label appears | NotReady |
| 19:15:09 | +60s | pod-1 promoted PRIMARY, primary service follows | Critical |
| 19:16:09 | +2m00s | Chaos auto-recovered, packet flow restored | Critical |
| 19:16:10 | +2m01s | pod-2 label corrected to standby |
Critical |
| 19:17:10 | +3m01s | pod-2 rejoined as ONLINE SECONDARY |
Ready |
Result: PASS — failover handled cleanly even when the primary’s outbound traffic was completely garbled, no SQL-level errors leaked through (TCP layer rejected every corrupted segment). Recovery completed in 3m01s with zero data loss. The dual-primary label transient is the same artifact as Chaos#15 and resolves automatically.
Clean up:
➤ kubectl delete -f tests/16-packet-corrupt.yaml
networkchaos.chaos-mesh.org "test-kubedb-primary-network-corrupt" deleted
InnoDB Chaos#17: Extreme Bandwidth Throttle on Primary (1 bps, 2 min)
Push the bandwidth throttle to its absolute limit — 1 bit per second on the primary’s outbound traffic for 2 minutes. At this rate Group Replication can transmit no useful data, effectively isolating the primary from its quorum partners while leaving the pod itself responsive on the local socket. This stresses how the cluster behaves when a primary is “alive but useless.”
Save this yaml as tests/17-bandwidth-1bps.yaml:
apiVersion: chaos-mesh.org/v1alpha1
kind: NetworkChaos
metadata:
name: test-kubedb-primary-network-bandwidth
namespace: demo
spec:
selector:
namespaces: [demo]
labelSelectors:
app.kubernetes.io/component: database
app.kubernetes.io/managed-by: kubedb.com
app.kubernetes.io/name: mysqls.kubedb.com
kubedb.com/role: primary
action: bandwidth
mode: all
bandwidth:
rate: '1bps'
limit: 20971520
buffer: 10000
duration: 2m
What this chaos does: Throttles the primary’s outbound bandwidth to 1 bps for 2 minutes — too low for even GR heartbeats to fit, but the pod itself stays responsive on the local socket.
-
Expected behavior: Primary unable to ship binlog events or send GR heartbeats → cluster transitions
Ready→NotReadyonce the secondaries notice → secondaries form a quorum and elect a new primary, primary service follows → state may briefly becomeCriticalduring the role flip → after the throttle clears, the old primary rejoins asSECONDARYand the cluster returns toReady. Zero data loss. -
Actual result: Cluster transitioned
Ready→NotReady(+30s) → dual-primary label transient (pod-1 + pod-2) at +30–180s →Critical(+2m, after chaos cleared) →Ready(+3m01s). Failover to pod-2 occurred while the throttle was active. After the throttle cleared at +2 min, pod-1’s local view converged and the label corrected tostandby. Final GTIDs match exactly on all 3 nodes (87aa5caa-…:1-71032:1000103-1000579). PASS.
➤ kubectl apply -f tests/17-bandwidth-1bps.yaml
networkchaos.chaos-mesh.org/test-kubedb-primary-network-bandwidth created
➤ # +30s — failover triggered, dual-primary label transient
kubectl get mysql.kubedb.com/mysql-ha-cluster -n demo
NAME VERSION STATUS AGE
mysql-ha-cluster 8.4.8 NotReady 149m
➤ kubectl get pods -n demo -l app.kubernetes.io/instance=mysql-ha-cluster -L kubedb.com/role
NAME READY STATUS RESTARTS ROLE
mysql-ha-cluster-0 2/2 Running 4 standby
mysql-ha-cluster-1 2/2 Running 0 primary # was primary, throttled, stale view
mysql-ha-cluster-2 2/2 Running 3 primary # newly promoted
➤ # +180s — throttle cleared, pod-1 label corrected to standby
kubectl get mysql.kubedb.com/mysql-ha-cluster -n demo
NAME VERSION STATUS AGE
mysql-ha-cluster 8.4.8 Ready 151m
➤ kubectl get pods -n demo -l app.kubernetes.io/instance=mysql-ha-cluster -L kubedb.com/role
NAME READY STATUS RESTARTS ROLE
mysql-ha-cluster-0 2/2 Running 4 standby
mysql-ha-cluster-1 2/2 Running 0 standby # corrected
mysql-ha-cluster-2 2/2 Running 3 primary
➤ SELECT MEMBER_HOST, MEMBER_PORT, MEMBER_STATE, MEMBER_ROLE
FROM performance_schema.replication_group_members;
+-----------------------------------------------+-------------+--------------+-------------+
| MEMBER_HOST | MEMBER_PORT | MEMBER_STATE | MEMBER_ROLE |
+-----------------------------------------------+-------------+--------------+-------------+
| mysql-ha-cluster-2.mysql-ha-cluster-pods.demo | 3306 | ONLINE | PRIMARY |
| mysql-ha-cluster-1.mysql-ha-cluster-pods.demo | 3306 | ONLINE | SECONDARY |
| mysql-ha-cluster-0.mysql-ha-cluster-pods.demo | 3306 | ONLINE | SECONDARY |
+-----------------------------------------------+-------------+--------------+-------------+
➤ # GTIDs — all match ✅
pod-0: 7b919383-...:1-4, 87aa5caa-...:1-71032:1000103-1000579
pod-1: 7b919383-...:1-4, 87aa5caa-...:1-71032:1000103-1000579
pod-2: 7b919383-...:1-4, 87aa5caa-...:1-71032:1000103-1000579
Observed timeline:
| Wall-clock | Δ from chaos | Event | DB Status |
|---|---|---|---|
| 19:21:45 | — | Pre-chaos baseline (pod-1 = primary) | Ready |
| 19:21:45 | 0s | 1 bps bandwidth applied to pod-1 | Ready |
| 19:22:15 | +30s | Operator probe times out; dual-primary label appears | NotReady |
| 19:22:46 | +60s | pod-2 promoted PRIMARY | NotReady |
| 19:23:45 | +2m00s | Chaos auto-recovered, throttle cleared | Critical |
| 19:24:46 | +3m01s | pod-1 label corrected, all 3 ONLINE | Ready |
Result: PASS — failover completed even under the harshest bandwidth condition; no writes accepted on the throttled side (no split-brain at the data layer); the cluster fully reconverged after the throttle cleared. Zero data loss. Same dual-primary label transient as Chaos#15/#16.
Clean up:
➤ kubectl delete -f tests/17-bandwidth-1bps.yaml
networkchaos.chaos-mesh.org "test-kubedb-primary-network-bandwidth" deleted
InnoDB Chaos#18: IO Latency 2s on /var/lib/mysql (Primary, 2 min)
Inject a 2-second per-operation latency on every file read and write under /var/lib/mysql on the primary pod, for 2 minutes. Compared to Chaos#4 (100ms IO latency), this is 20× more severe — every InnoDB redo log flush, every binlog write, every checkpoint stalls for 2 seconds.
Save this yaml as tests/18-io-latency-2s.yaml:
apiVersion: chaos-mesh.org/v1alpha1
kind: IOChaos
metadata:
name: test-kubedb-primary-io-latency
namespace: demo
spec:
action: latency
mode: all
selector:
namespaces: [demo]
labelSelectors:
app.kubernetes.io/component: database
app.kubernetes.io/managed-by: kubedb.com
app.kubernetes.io/name: mysqls.kubedb.com
kubedb.com/role: primary
volumePath: /var/lib/mysql
path: '/var/lib/mysql/**/*'
delay: '2000ms'
percent: 100
duration: 2m
What this chaos does: Adds 2000ms delay to every read/write under the primary’s data directory. Every InnoDB IO operation stalls for 2 seconds; mysqld is alive but cannot make progress.
-
Expected behavior: Primary’s storage IO becomes effectively unusable → operator probes time out → cluster transitions
Ready→NotReady. GR’s failure detector may or may not trip — Paxos heartbeats use the network, not local IO, so the primary may still appear “alive” to peers. After IO returns to normal, the primary catches up and the cluster returns toReady. Zero data loss. -
Actual result: Cluster transitioned
Ready→NotReady(+30s, operator probe timed out) → no failover (pod-2 retainedPRIMARYthroughout). Returned toReadyimmediately when chaos auto-cleared at +2 min. Group Replication tolerated the IO stall because GR-to-GR communication is over TCP and the pod’s network stack stayed responsive. Final GTIDs match exactly on all 3 nodes (87aa5caa-…:1-71281:1000103-1000579). PASS.
Comparison with Chaos#4 (100 ms IO latency): the milder version slowed TPS without flipping cluster status. The 2-second variant flipped status to
NotReadybecause the operator’s TCP probe to mysqld timed out, but GR itself never triggered failover — exactly the right behaviour.
➤ kubectl apply -f tests/18-io-latency-2s.yaml
iochaos.chaos-mesh.org/test-kubedb-primary-io-latency created
➤ # During chaos — primary still PRIMARY but operator probe times out
➤ kubectl get mysql.kubedb.com/mysql-ha-cluster -n demo
NAME VERSION STATUS AGE
mysql-ha-cluster 8.4.8 NotReady 157m
➤ kubectl get pods -n demo -l app.kubernetes.io/instance=mysql-ha-cluster -L kubedb.com/role
NAME READY STATUS RESTARTS ROLE
mysql-ha-cluster-0 2/2 Running 4 standby
mysql-ha-cluster-1 2/2 Running 0 standby
mysql-ha-cluster-2 2/2 Running 3 primary # unchanged
➤ # GR view from a peer — all members still ONLINE, no role flip
SELECT MEMBER_HOST, MEMBER_PORT, MEMBER_STATE, MEMBER_ROLE
FROM performance_schema.replication_group_members;
+-----------------------------------------------+-------------+--------------+-------------+
| MEMBER_HOST | MEMBER_PORT | MEMBER_STATE | MEMBER_ROLE |
+-----------------------------------------------+-------------+--------------+-------------+
| mysql-ha-cluster-2.mysql-ha-cluster-pods.demo | 3306 | ONLINE | PRIMARY |
| mysql-ha-cluster-1.mysql-ha-cluster-pods.demo | 3306 | ONLINE | SECONDARY |
| mysql-ha-cluster-0.mysql-ha-cluster-pods.demo | 3306 | ONLINE | SECONDARY |
+-----------------------------------------------+-------------+--------------+-------------+
➤ # After chaos cleared (+2 min) — back to Ready immediately
➤ kubectl get mysql.kubedb.com/mysql-ha-cluster -n demo
NAME VERSION STATUS AGE
mysql-ha-cluster 8.4.8 Ready 158m
➤ # GTIDs — all match ✅
pod-0: 7b919383-...:1-4, 87aa5caa-...:1-71281:1000103-1000579
pod-1: 7b919383-...:1-4, 87aa5caa-...:1-71281:1000103-1000579
pod-2: 7b919383-...:1-4, 87aa5caa-...:1-71281:1000103-1000579
Observed timeline:
| Wall-clock | Δ from chaos | Event | DB Status |
|---|---|---|---|
| 19:29:25 | — | Pre-chaos baseline (pod-2 = primary) | Ready |
| 19:29:25 | 0s | IO latency 2s applied to pod-2 | Ready |
| 19:29:55 | +30s | Operator probe times out | NotReady |
| 19:31:25 | +2m00s | Chaos auto-recovered, IO restored | Ready |
Result: PASS — no failover required, no data loss, no role change. pod-2 retained PRIMARY and resumed normal IO immediately after chaos cleared. Demonstrates that catastrophic local-disk slowness is contained at the operator-status layer without escalating to a GR membership change — exactly the right behaviour for transient storage glitches.
Clean up:
➤ kubectl delete -f tests/18-io-latency-2s.yaml
iochaos.chaos-mesh.org "test-kubedb-primary-io-latency" deleted
InnoDB Chaos#19: 100% IO Fault (errno=5 / EIO) on Primary’s Data Directory (2 min)
Force every read/write under /var/lib/mysql on the primary to return EIO (errno 5) for 2 minutes — simulating a complete underlying disk failure. Compared to Chaos#18 (slow but successful IO), every InnoDB read, log write, and binlog flush now fails immediately.
Save this yaml as tests/19-io-fault.yaml:
apiVersion: chaos-mesh.org/v1alpha1
kind: IOChaos
metadata:
name: test-kubedb-primary-io-fault
namespace: demo
spec:
action: fault
mode: all
selector:
namespaces: [demo]
labelSelectors:
app.kubernetes.io/component: database
app.kubernetes.io/managed-by: kubedb.com
app.kubernetes.io/name: mysqls.kubedb.com
kubedb.com/role: primary
volumePath: /var/lib/mysql
path: '/var/lib/mysql/**/*'
errno: 5
percent: 100
duration: 2m
What this chaos does: Returns EIO (errno 5) on 100% of disk operations under the primary’s data directory for 2 minutes. InnoDB cannot read or write any data file.
-
Expected behavior: Primary cannot service IO → mysqld either aborts or stops accepting writes → cluster transitions
Ready→NotReady→Critical→ secondaries elect a new primary → after IO returns, the failed primary rejoins asSECONDARY(likely incrementally, since data files weren’t corrupted, only blocked). Zero data loss. -
Actual result: Cluster transitioned
Ready→Critical(+30s) — pod-1 promoted PRIMARY cleanly (no dual-primary label transient because EIO crashes mysqld’s IO thread immediately rather than leaving it stale). After chaos auto-cleared at +2 min, pod-2 entered incremental recovery and reachedONLINE SECONDARYby +3 min. ClusterReadyat +3m. GTIDs converged on all 3 nodes (87aa5caa-…:1-71507:1000103-1000662; pod-0 briefly 1 tx behind during a live-write window, caught up immediately). PASS.
Comparison with Chaos#18 (2s IO latency): the latency variant kept mysqld alive but slow, so no failover. The 100% EIO variant produces a hard failure — failover happens cleanly in 30 s.
➤ kubectl apply -f tests/19-io-fault.yaml
iochaos.chaos-mesh.org/test-kubedb-primary-io-fault created
➤ # +30s — failover already complete, cluster Critical
➤ kubectl get mysql.kubedb.com/mysql-ha-cluster -n demo
NAME VERSION STATUS AGE
mysql-ha-cluster 8.4.8 Critical 165m
➤ kubectl get pods -n demo -l app.kubernetes.io/instance=mysql-ha-cluster -L kubedb.com/role
NAME READY STATUS RESTARTS ROLE
mysql-ha-cluster-0 2/2 Running 4 standby
mysql-ha-cluster-1 2/2 Running 0 primary # promoted cleanly
mysql-ha-cluster-2 2/2 Running 3 standby # IO-faulted
After chaos cleared (+2 min) — pod-2 rejoined as SECONDARY:
➤ kubectl get mysql.kubedb.com/mysql-ha-cluster -n demo
NAME VERSION STATUS AGE
mysql-ha-cluster 8.4.8 Ready 168m
➤ SELECT MEMBER_HOST, MEMBER_PORT, MEMBER_STATE, MEMBER_ROLE
FROM performance_schema.replication_group_members;
+-----------------------------------------------+-------------+--------------+-------------+
| MEMBER_HOST | MEMBER_PORT | MEMBER_STATE | MEMBER_ROLE |
+-----------------------------------------------+-------------+--------------+-------------+
| mysql-ha-cluster-2.mysql-ha-cluster-pods.demo | 3306 | ONLINE | SECONDARY |
| mysql-ha-cluster-1.mysql-ha-cluster-pods.demo | 3306 | ONLINE | PRIMARY |
| mysql-ha-cluster-0.mysql-ha-cluster-pods.demo | 3306 | ONLINE | SECONDARY |
+-----------------------------------------------+-------------+--------------+-------------+
➤ # GTIDs — all converged ✅
pod-0: 7b919383-...:1-4, 87aa5caa-...:1-71506:1000103-1000662 # 1 tx behind (live-write window)
pod-1: 7b919383-...:1-4, 87aa5caa-...:1-71507:1000103-1000662
pod-2: 7b919383-...:1-4, 87aa5caa-...:1-71507:1000103-1000662
Observed timeline:
| Wall-clock | Δ from chaos | Event | DB Status |
|---|---|---|---|
| 19:37:58 | — | Pre-chaos baseline (pod-2 = primary) | Ready |
| 19:37:58 | 0s | 100% IO fault (EIO) applied to pod-2 | Ready |
| 19:38:28 | +30s | pod-1 promoted PRIMARY, primary service follows | Critical |
| 19:39:58 | +2m00s | Chaos auto-recovered, IO restored | Critical |
| 19:40:58 | +3m00s | pod-2 reached ONLINE SECONDARY |
Ready |
Result: PASS — even with every disk operation failing on the primary, GR’s failover and KubeDB’s incremental rejoin completed in 3 minutes without any human intervention. Zero data loss across all 3 nodes. Notably cleaner than the network-side faults (Chaos#15–17) because EIO immediately drops the primary out of the GR view rather than leaving a stale local view that the label updater can read.
Clean up:
➤ kubectl delete -f tests/19-io-fault.yaml
iochaos.chaos-mesh.org "test-kubedb-primary-io-fault" deleted
Router & Service-Specific Observations
- Primary service (port 3306) follows the new primary automatically — the Kubernetes service uses the
kubedb.com/role=primarylabel selector, so it routes to whichever pod GR has elected. RW traffic switches over within seconds of a failover with no MySQL-Router round-trip needed. - Existing connections during failover get
error 2013(Lost connection) — in-flight transactions are aborted; new connections route to the new primary as soon as the role label updates. Production applications need a connection pool with retry logic; default sysbench has none, so its threads die on the first failover. - Port 6450 (RW-Split via Router) — auto-enabled by MySQL Router 8.4+ bootstrap. No custom configuration required.
connection_sharing=1means clients must use TLS (--mysql-ssl=REQUIRED). - Dual-primary label transient under one-way isolation — when the primary is one-way isolated (Chaos#15–17), each pod’s
mysql-coordinatorreports its own local view, so two pods can carrykubedb.com/role=primaryfor 1–3 minutes. This is purely a label-layer artifact; GR itself never accepts writes on the isolated side. Labels self-heal once the network recovers. - Router pod resilience — both Router replicas were untouched by every database-side chaos in this suite. They re-discover topology automatically as MySQL pods come back.
Chaos Testing Results Summary
| # | Experiment | Failover | Recovery Time | Data Loss | GTIDs | Verdict |
|---|---|---|---|---|---|---|
| 1 | Pod Kill Primary | Yes | ~30s | Zero | MATCH | PASS |
| 2 | OOMKill Primary (1200MB stress) | Yes (OOMKill) | ~60s | Zero | MATCH | PASS |
| 3 | Network Partition (2 min) | Yes | ~90s | Zero | MATCH | PASS |
| 4 | IO Latency (100ms) | No | Auto | Zero | MATCH | PASS |
| 5 | Network Latency (1s) | No | Auto | Zero | MATCH | PASS |
| 6 | CPU Stress (98%) | No | Auto | Zero | MATCH | PASS |
| 7 | Packet Loss (30%) | No | Auto | Zero | MATCH | PASS |
| 8 | Full Cluster Kill | Yes (reboot) | ~60s | Zero | MATCH | PASS |
| 9 | Double Primary Kill | Yes (×2) | ~120s | Zero | MATCH | PASS |
| 10 | Rolling Restart (0→1→2) | No | Auto | Zero | MATCH | PASS |
| 11 | DNS Failure on Primary | No | Auto | Zero | MATCH | PASS |
| 12 | Clock Skew (-5 min) | No | Auto | Zero | MATCH | PASS |
| 13 | Pod Failure (5 min) | Yes (20s) | ~8m (after fault) | Zero | MATCH | PASS |
| 14 | Continuous OOM Loop (10× 30s) | Yes | ~2m | Zero | MATCH | PASS |
| 15 | 100% Packet Loss on Primary | Yes | ~3m49s | Zero | MATCH | PASS † |
| 16 | 100% Packet Corruption on Primary | Yes | ~3m01s | Zero | MATCH | PASS † |
| 17 | Bandwidth Throttle (1 bps) | Yes | ~3m01s | Zero | MATCH | PASS † |
| 18 | IO Latency (2s) | No | Auto | Zero | MATCH | PASS |
| 19 | 100% IO Fault (EIO) | Yes | ~3m | Zero | MATCH | PASS |
† Network-isolation tests (#15–17) exhibited a transient dual-primary kubedb.com/role Kubernetes label for 1–3 minutes — a UI-layer artifact caused by each pod’s mysql-coordinator reading its own local view of replication_group_members while the network was one-way down. No split-brain at the data layer: the isolated pod cannot replicate any commits, and the label corrects automatically once the network heals.
All 19 InnoDB Cluster experiments passed with zero data loss and zero errant GTIDs.
Data Integrity Validation
Every experiment verified data integrity through 4 checks across all 3 nodes:
- GTID Consistency —
SELECT @@gtid_executedmust match on all nodes after recovery. - Checksum Verification —
CHECKSUM TABLEon every sysbench table must match across nodes. - Row-count Validation — Cumulative tracking-table row counts must be preserved.
- Errant GTID Detection — No local
server_uuidGTIDs outside the group UUID.
Failover Performance (Single-Primary InnoDB Cluster)
| Scenario | Failover Time | Full Recovery Time |
|---|---|---|
| Pod Kill Primary | ~3 seconds | ~30 seconds |
| OOMKill Primary | ~3 seconds | ~60 seconds |
| Network Partition (2 min) | ~5 seconds | ~90 seconds |
| Full Cluster Kill | ~10 seconds (after reboot) | ~60 seconds |
| Double Primary Kill | ~3 seconds (×2) | ~120 seconds |
| Pod Failure (5 min) | ~20 seconds | ~8 minutes (bound by chaos duration) |
| Continuous OOM Loop (10× 30s) | ~30 seconds | ~2 minutes |
| 100% Packet Loss on Primary (2 min) | ~30 seconds | ~3m49s |
| 100% Packet Corruption on Primary (2 min) | ~30 seconds | ~3m01s |
| Bandwidth Throttle 1 bps (2 min) | ~30 seconds | ~3m01s |
| 100% IO Fault EIO (2 min) | ~30 seconds | ~3 minutes |
Performance Impact Under Chaos
| Chaos Type | TPS During Chaos | Notes |
|---|---|---|
| Baseline (no chaos) | ~1,312 (8 threads via primary service) | p95 latency 9.22 ms |
| IO Latency (100ms) | 0–254 | ~100% drop during chaos, full recovery on clear |
| IO Latency (2s) | Stalled | Operator probe times out → NotReady, no failover |
| Network Latency (1s) | 1.26 | 99.9% drop, no failover |
| CPU Stress (98%) | 187–877 | ~30–80% drop, no failover |
| Packet Loss (30%) | 0 (new conn refused) | Cluster stable, no failover |
| 100% Packet Loss / Corrupt / 1 bps | sysbench dies on first txn | Failover within 30s, dual-primary label transient |
| Clock Skew (-5 min) | Normal | No impact |
| DNS Failure | Normal | No impact |
| 100% IO Fault (EIO) | sysbench dies on first txn | Hard mysqld failure, clean failover |
InnoDB Cluster vs Group Replication
| Aspect | InnoDB Cluster | Group Replication |
|---|---|---|
| Connection routing | Via MySQL Router (automatic) | Direct to pods (manual or service-side) |
| Failover handling | Transparent (Router re-routes) | Application must reconnect |
| Read-Write Split | Built-in (port 6450) | Application logic required |
| Additional component | MySQL Router pod | None |
| Best for | Applications needing transparent failover | Applications with custom routing |
Key Takeaways
-
KubeDB MySQL InnoDB Cluster achieves zero data loss across all 19 chaos experiments. Every recovered cluster ended with byte-for-byte identical
@@gtid_executedon all 3 nodes. -
The primary service routes RW transparently — applications connecting to
mysql-ha-cluster.demo.svc.cluster.local:3306see automatic re-routing when the primary fails. The k8s service followskubedb.com/role=primary, so applications with retry logic (any production connection pool) reconnect to the new primary within seconds. The MySQL Router pods continue to provide read-only routing on 6447 and RW-split on 6450. -
Full cluster recovery is fully automatic — even after all 3 pods are killed simultaneously, the coordinator detects the complete outage, identifies the pod with the highest GTID, and invokes
dba.rebootClusterFromCompleteOutage()to restore the cluster. -
GR’s failure detector tolerates everything that isn’t actual unreachability — clock skew, DNS failure, IO latency (even 2s), packet duplication, packet loss up to 30%, and CPU stress at 98% all leave the cluster stable. Only outright unreachability — pod kill, OOMKill, partition, 100% packet loss/corrupt, EIO disk failure, severe bandwidth throttle — triggers failover.
-
Network-isolation tests expose a label-layer transient (#15–17) — when the primary is one-way isolated (packet loss, corruption, severe bandwidth throttle), each pod’s
mysql-coordinatorupdates its ownkubedb.com/rolelabel from its own local GR view. The isolated pod still sees itself asPRIMARYuntil the network heals, so for 1–3 minutes Kubernetes labels show two pods asprimary. GR itself never goes split-brain — only the majority partition can commit, and the labels self-heal once connectivity returns. The behaviour is a UI artifact, not a correctness issue. -
Coordinator’s errant-GTID gate prevents silent data loss — when an OOMKilled pod has locally committed transactions that never replicated, the coordinator refuses to auto-clone (which would discard them) until an operator explicitly approves via
touch /scripts/approve-clone. Chaos#14 did not surface this in our run because the OOM hit before any partial commits accumulated, but the gate is in place and visible in the coordinator logs when it fires. -
Sysbench is not representative of a real application — sysbench has no auto-reconnect, so any failover-induced connection drop kills its threads. Real applications with connection pools (HikariCP, mysql-connector-python’s
pool_reset_session, etc.) experience a brief blip and reconnect to the new primary automatically. Tests #13, #15–17, and #19 all reproduced this: the cluster recovers cleanly while sysbench reports fatal connection errors. -
Recovery times are short and predictable — failover completes in 20–30 seconds for any chaos that triggers it; full re-Ready (with the chaos-impacted pod rejoined) takes 1.5–4 minutes for everything except Pod Failure (#13), which is bound by the chaos duration itself.
What’s Next
- InnoDB Cluster Multi-Primary testing — test Multi-Primary topology with MySQL Router.
- MySQL 9.x InnoDB Cluster — validate all experiments on the 9.x line.
- Long-duration soak testing — extended chaos runs (hours/days) to validate stability under sustained failure injection.
Support
To speak with us, please leave a message on our website .
To receive product announcements, follow us on X .
To watch tutorials of various Production-Grade Kubernetes Tools Subscribe our YouTube channel.
If you have found a bug with KubeDB or want to request for new features, please file an issue .
