MySQL in 2026: Replication, GTID, and Automatic Failover in a Production Kubernetes Environment
Introduction: Why MySQL Replication Remains Relevant in 2026
Despite the rapid growth of NoSQL solutions and NewSQL databases, MySQL continues to hold a leading position in the production stacks of major companies. According to DB-Engines data for 2025–2026, MySQL consistently ranks in the top 3 relational databases. The reasons are straightforward: a mature ecosystem, predictable behavior under load, and a massive community.
In the context of Kubernetes, MySQL replication addresses several critical challenges:
- High Availability (HA) — when the Primary fails, automatic failover to a Replica minimizes downtime.
- Horizontal read scaling — read replicas handle SELECT queries, reducing load on the Primary.
- Lock-free backups — taking a dump from a replica does not affect production traffic.
- Disaster Recovery — geographically distributed replicas protect against datacenter failures.
In this article, we will walk through the full journey: from GTID theory to a working Kubernetes cluster with automatic failover and CI/CD integration.
GTID Replication Fundamentals: What Has Changed and Why It Matters
GTID (Global Transaction Identifier) is a unique identifier assigned to every transaction in MySQL. The format is: source_uuid:transaction_id, for example 3E11FA47-71CA-11E1-9E33-C80AA9429562:1-100.
Differences from Classic Binlog Replication
In classic replication, the replica tracks a position in the binary log (file + offset). During failover, an administrator must manually determine the correct position on the new Primary — a common source of human error. GTID eliminates this problem: every transaction has a globally unique identifier, and the replica automatically determines which transactions it has not yet applied.
Key advantages of GTID replication:
- Automatic determination of the replication point when the Primary changes.
- Simplified configuration of Orchestrator and MySQL Operator for automatic failover.
- Guaranteed absence of duplicate transactions on the replica.
- Built-in support since MySQL 5.6, with full maturity in MySQL 8.0/8.4.
In MySQL 8.4 (LTS, 2024–2026), GTID replication has become the de facto standard: several deprecated master_* parameters have been fully replaced by source_*.
Configuring MySQL Primary/Replica with GTID
my.cnf Configuration for Primary
[mysqld]
# Server identifier — must be unique within the cluster
server-id = 1
# Binary log
log_bin = /var/log/mysql/mysql-bin.log
binlog_format = ROW
binlog_row_image = FULL
# GTID
gtid_mode = ON
enforce_gtid_consistency = ON
# Performance and reliability
innodb_flush_log_at_trx_commit = 1
sync_binlog = 1
# Replication
binlog_expire_logs_seconds = 604800
max_binlog_size = 100M
# Parallel replication on replicas (MySQL 8.0+)
binlog_transaction_dependency_tracking = WRITESET
transaction_write_set_extraction = XXHASH64
my.cnf Configuration for Replica
[mysqld]
server-id = 2
# Replica is read-only
read_only = ON
super_read_only = ON
# GTID
gtid_mode = ON
enforce_gtid_consistency = ON
# Logging on replica (required for replication chaining)
log_bin = /var/log/mysql/mysql-bin.log
log_replica_updates = ON
binlog_format = ROW
# Parallel transaction application
replica_parallel_workers = 4
replica_parallel_type = LOGICAL_CLOCK
replica_preserve_commit_order = ON
# Automatic replication restart on connection error
replica_net_timeout = 60
Initializing the Replica
Create the replication user on the Primary:
-- On Primary
CREATE USER 'replicator'@'%' IDENTIFIED WITH caching_sha2_password BY 'StrongPass!2026';
GRANT REPLICATION SLAVE ON *.* TO 'replicator'@'%';
FLUSH PRIVILEGES;
Take a dump from the Primary to initialize the replica (without data locking):
mysqldump \
--single-transaction \
--master-data=2 \
--set-gtid-purged=ON \
--all-databases \
-u root -p > full_backup.sql
Restore on the replica and start replication:
-- On Replica after restoring the dump
CHANGE REPLICATION SOURCE TO
SOURCE_HOST='mysql-primary.mysql.svc.cluster.local',
SOURCE_PORT=3306,
SOURCE_USER='replicator',
SOURCE_PASSWORD='StrongPass!2026',
SOURCE_AUTO_POSITION=1;
START REPLICA;
-- Check status
SHOW REPLICA STATUS\G
Key fields in the SHOW REPLICA STATUS output: Replica_IO_Running: Yes, Replica_SQL_Running: Yes, Seconds_Behind_Source: 0; Retrieved_Gtid_Set and Executed_Gtid_Set should match when lag is zero.
Running a MySQL Cluster in Kubernetes
Architectural Components
For stateful applications in Kubernetes, StatefulSets are used: they guarantee stable network identifiers (mysql-0, mysql-1) and binding to specific Persistent Volumes. This is critical for MySQL, where each node must have a unique server-id and a stable hostname.
Headless Service
apiVersion: v1
kind: Service
metadata:
name: mysql
namespace: mysql
labels:
app: mysql
spec:
clusterIP: None # Headless — no virtual IP
selector:
app: mysql
ports:
- name: mysql
port: 3306
targetPort: 3306
ConfigMap with MySQL Configuration
apiVersion: v1
kind: ConfigMap
metadata:
name: mysql-config
namespace: mysql
data:
primary.cnf: |
[mysqld]
server-id=1
log_bin=/var/log/mysql/mysql-bin.log
binlog_format=ROW
gtid_mode=ON
enforce_gtid_consistency=ON
log_replica_updates=ON
binlog_transaction_dependency_tracking=WRITESET
replica.cnf: |
[mysqld]
log_bin=/var/log/mysql/mysql-bin.log
binlog_format=ROW
gtid_mode=ON
enforce_gtid_consistency=ON
read_only=ON
super_read_only=ON
log_replica_updates=ON
replica_parallel_workers=4
replica_parallel_type=LOGICAL_CLOCK
StatefulSet
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
namespace: mysql
spec:
selector:
matchLabels:
app: mysql
serviceName: mysql
replicas: 3
template:
metadata:
labels:
app: mysql
spec:
initContainers:
- name: init-mysql
image: mysql:8.4
command:
- bash
- "-c"
- |
set -ex
# Generate server-id based on pod ordinal index
[[ $(hostname) =~ -([0-9]+)$ ]] || exit 1
ordinal=${BASH_REMATCH[1]}
echo [mysqld] > /mnt/conf.d/server-id.cnf
echo server-id=$((100 + $ordinal)) >> /mnt/conf.d/server-id.cnf
# Copy Primary or Replica config
if [[ $ordinal -eq 0 ]]; then
cp /mnt/config-map/primary.cnf /mnt/conf.d/
else
cp /mnt/config-map/replica.cnf /mnt/conf.d/
fi
volumeMounts:
- name: conf
mountPath: /mnt/conf.d
- name: config-map
mountPath: /mnt/config-map
containers:
- name: mysql
image: mysql:8.4
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-secret
key: root-password
ports:
- name: mysql
containerPort: 3306
volumeMounts:
- name: data
mountPath: /var/lib/mysql
- name: conf
mountPath: /etc/mysql/conf.d
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "2"
memory: "4Gi"
readinessProbe:
exec:
command: ["mysqladmin", "ping", "-u", "root", "-p$(MYSQL_ROOT_PASSWORD)"]
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
livenessProbe:
exec:
command: ["mysqladmin", "ping", "-u", "root", "-p$(MYSQL_ROOT_PASSWORD)"]
initialDelaySeconds: 60
periodSeconds: 20
volumes:
- name: conf
emptyDir: {}
- name: config-map
configMap:
name: mysql-config
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: fast-ssd
resources:
requests:
storage: 100Gi
Automatic Failover: Tools and Configuration
MySQL Operator for Kubernetes (Oracle)
MySQL Operator for Kubernetes is Oracle's official operator for managing InnoDB Cluster (MySQL Group Replication). In 2026, this is the recommended approach for production environments.
Installation via Helm:
helm repo add mysql-operator https://mysql.github.io/mysql-operator/
helm repo update
helm install mysql-operator mysql-operator/mysql-operator \
--namespace mysql-operator \
--create-namespace \
--set image.tag=8.4.0
Creating a cluster via CRD:
apiVersion: mysql.oracle.com/v2
kind: InnoDBCluster
metadata:
name: mycluster
namespace: mysql
spec:
secretName: mysql-secret
tlsUseSelfSigned: true
instances: 3
router:
instances: 2
datadirVolumeClaimTemplate:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 100Gi
storageClassName: fast-ssd
MySQL Operator automatically:
- Configures Group Replication with automatic Primary election.
- Deploys MySQL Router for transparent request routing.
- Performs failover when the Primary becomes unavailable (typically within 5–30 seconds).
- Manages TLS certificates between nodes.
Orchestrator — An Alternative for Classic Replication
If you use classic Primary/Replica replication (not Group Replication), Orchestrator from GitHub/Pinterest is a battle-tested tool for automatic failover. It maps the replication topology, detects unavailable nodes, and automatically promotes the best replica to Primary.
Deploying Orchestrator in Kubernetes:
apiVersion: apps/v1
kind: Deployment
metadata:
name: orchestrator
namespace: mysql
spec:
replicas: 1
selector:
matchLabels:
app: orchestrator
template:
metadata:
labels:
app: orchestrator
spec:
containers:
- name: orchestrator
image: openarkcode/orchestrator:latest
ports:
- containerPort: 3000
env:
- name: ORC_TOPOLOGY_USER
value: orchestrator
- name: ORC_TOPOLOGY_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-secret
key: orc-password
volumeMounts:
- name: orc-config
mountPath: /etc/orchestrator
volumes:
- name: orc-config
configMap:
name: orchestrator-config
Key Orchestrator configuration parameters for GTID replication:
{
"AutomatedRecoveryMasterDetachLostReplicas": true,
"RecoverMasterClusterFilters": ["*"],
"RecoveryPeriodBlockSeconds": 3600,
"FailMasterPromotionOnLagMinutes": 0,
"DetachLostReplicasAfterMasterFailover": true,
"MasterFailoverLostInstancesDowntimeMinutes": 0,
"PostMasterFailoverProcesses": [
"update-dns.sh --old={failedHost} --new={successorHost}"
]
}
Replication Monitoring: Metrics, Prometheus, and Alerts
mysqld_exporter for Prometheus
To monitor MySQL in Kubernetes, we use mysqld_exporter, deployed as a sidecar container or a standalone Deployment.
apiVersion: apps/v1
kind: Deployment
metadata:
name: mysql-exporter
namespace: mysql
spec:
replicas: 1
selector:
matchLabels:
app: mysql-exporter
template:
metadata:
labels:
app: mysql-exporter
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "9104"
spec:
containers:
- name: mysqld-exporter
image: prom/mysqld-exporter:v0.15.1
args:
- --collect.slave_status
- --collect.slave_hosts
- --collect.info_schema.innodb_metrics
- --collect.global_status
env:
- name: DATA_SOURCE_NAME
valueFrom:
secretKeyRef:
name: mysql-secret
key: exporter-dsn
ports:
- containerPort: 9104
Key Replication Metrics
mysql_slave_status_seconds_behind_master— replica lag in seconds (renamed tomysql_replica_status_seconds_behind_sourcein newer versions).mysql_slave_status_slave_io_running— replication IO thread status (1 = running).mysql_slave_status_slave_sql_running— replication SQL thread status.mysql_global_status_binlog_cache_disk_use— binlog disk cache usage.mysql_global_status_threads_running— active threads.
Alerting Rules for Prometheus Alertmanager
groups:
- name: mysql-replication
rules:
- alert: MySQLReplicationLag
expr: mysql_slave_status_seconds_behind_master > 30
for: 5m
labels:
severity: warning
annotations:
summary: "MySQL replication is lagging by more than 30 seconds"
description: "Pod {{ $labels.pod }} has a lag of {{ $value }}s"
- alert: MySQLReplicationIOThreadDown
expr: mysql_slave_status_slave_io_running == 0
for: 1m
labels:
severity: critical
annotations:
summary: "MySQL replication IO thread is stopped"
- alert: MySQLReplicationSQLThreadDown
expr: mysql_slave_status_slave_sql_running == 0
for: 1m
labels:
severity: critical
annotations:
summary: "MySQL replication SQL thread is stopped"
- alert: MySQLReplicationNotRunning
expr: mysql_slave_status_slave_io_running == 0 OR mysql_slave_status_slave_sql_running == 0
for: 2m
labels:
severity: page
annotations:
summary: "CRITICAL: MySQL replication is not running"
Common Replication Issues and How to Resolve Them
1. Error 1062: Duplicate Entry
This occurs when a unique key constraint is violated on the replica. The cause is data divergence between the replica and the Primary (e.g., due to direct writes to the replica). Solution: never write directly to a replica (super_read_only=ON). If it occurs, skip the problematic transaction using GTID:
-- Find the problematic GTID in SHOW REPLICA STATUS\G
-- Skip it:
STOP REPLICA;
SET GTID_NEXT='3E11FA47-71CA-11E1-9E33-C80AA9429562:101';
BEGIN; COMMIT;
SET GTID_NEXT='AUTOMATIC';
START REPLICA;
2. High Replication Lag Under Load
Under heavy write traffic, single-threaded transaction application on the replica becomes a bottleneck. The solution is multi-threaded replication:
SET GLOBAL replica_parallel_workers = 8;
SET GLOBAL replica_parallel_type = 'LOGICAL_CLOCK';
SET GLOBAL replica_preserve_commit_order = ON;
3. Lost Connection Between Primary and Replica
Increase the timeout and enable automatic reconnection:
CHANGE REPLICATION SOURCE TO
SOURCE_CONNECT_RETRY=10,
SOURCE_RETRY_COUNT=86400,
SOURCE_HEARTBEAT_PERIOD=5;
4. Split-Brain During Failover
A dangerous situation where the old Primary "comes back to life" and starts accepting writes alongside the new Primary. Protection: use STONITH/fencing at the Kubernetes level (PodDisruptionBudget), and enable super_read_only=ON on all nodes by default — Orchestrator/Operator removes it only from the active Primary.
5. GTID Issues When Restoring from Backup
Restoring a dump created without --set-gtid-purged=ON causes conflicts. Always use this flag when creating dumps for replication. If GTID sets conflict, you can reset them:
RESET MASTER;
SET @@GLOBAL.gtid_purged='3E11FA47-71CA-11E1-9E33-C80AA9429562:1-500';
CI/CD Integration: Automated Schema Migrations with Replication in Mind
Schema migrations in a replicated environment require special attention. A common mistake is running ALTER TABLE without considering the impact on replicas and replication lag.
Principles of Safe Migrations
- Online DDL: use
ALTER TABLE ... ALGORITHM=INPLACE, LOCK=NONEfor MySQL 8.x, or thegh-osttool, which performs migrations via a shadow table without blocking production. - Lag monitoring before migration: the CI/CD pipeline should check
Seconds_Behind_Sourcebefore running ALTER. If the lag exceeds the threshold, the migration is postponed. - Backward compatibility: add columns as
NULLor with a default value so that new code works with the old schema and vice versa.
Example GitLab CI/CD Step with gh-ost Migration
migrate-schema:
stage: deploy
image: github/gh-ost:latest
script:
- |
# Check replication lag
LAG=$(mysql -h $MYSQL_REPLICA_HOST -u root -p$MYSQL_ROOT_PASSWORD \
-e "SHOW REPLICA STATUS\G" | grep Seconds_Behind_Source | awk '{print $2}')
if [ "$LAG" -gt 10 ]; then
echo "Replication lag is $LAG seconds, aborting migration"
exit 1
fi
- |
gh-ost \
--host=$MYSQL_PRIMARY_HOST \
--port=3306 \
--user=root \
--password=$MYSQL_ROOT_PASSWORD \
--database=myapp \
--table=orders \
--alter="ADD COLUMN status_v2 TINYINT NOT NULL DEFAULT 0" \
--execute \
--max-lag-millis=1500 \
--chunk-size=1000 \
--ok-to-drop-table
only:
- main
The --max-lag-millis flag causes gh-ost to automatically slow down or pause if replication lag exceeds the threshold — this protects production from degradation during migrations.
Integration with Flyway / Liquibase
When using Flyway or Liquibase in Kubernetes, run migrations as a Kubernetes Job with restartPolicy: Never to guarantee single execution. Use the lock table (flyway_schema_history) — it is replicated to all nodes and prevents double execution.
Conclusion and Practical Recommendations
Setting up MySQL replication with GTID in Kubernetes in 2026 is a mature, well-documented solution with a rich toolset. Let's summarize:
- GTID is mandatory: do not use positional replication in new projects. GTID simplifies failover, administration, and integration with operators.
- MySQL Operator for Kubernetes is the recommended choice for new production deployments. Group Replication with out-of-the-box automatic failover saves months of automation work.
- StatefulSets + headless services are the right abstraction for stateful applications in Kubernetes. Do not attempt to run MySQL in regular Deployments.
- Multi-threaded replication:
replica_parallel_workersandLOGICAL_CLOCKare critical under high load. - Monitoring is not optional: set up alerts for replication lag and thread failures before the issue affects users.
- gh-ost for DDL: blocking ALTER TABLE in production with replication is a path to an incident. Always use online migration tools.
- Test failover regularly: Chaos Engineering (e.g., Chaos Mesh in Kubernetes) should include scenarios for killing the Primary and verifying recovery time.
A properly configured MySQL cluster in Kubernetes delivers 99.99% reliability with minimal operational overhead — which is exactly what makes it a relevant choice for production environments in 2026 and beyond.
Technologies
Tags
Ruslan Ismailov
Senior Web / Backend Developer. Senior web/backend developer with 9 years of experience. Stack: PHP, Laravel, PostgreSQL, Redis, Docker, Kubernetes, REST, microservices, CI/CD. More about me →