Databases

MySQL in 2026: Replication, GTID, and Automatic Failover in a Production Kubernetes Environment

Ruslan Ismailov Published 14 min read
M

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 to mysql_replica_status_seconds_behind_source in 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=NONE for MySQL 8.x, or the gh-ost tool, which performs migrations via a shadow table without blocking production.
  • Lag monitoring before migration: the CI/CD pipeline should check Seconds_Behind_Source before running ALTER. If the lag exceeds the threshold, the migration is postponed.
  • Backward compatibility: add columns as NULL or 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_workers and LOGICAL_CLOCK are 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 →