DevOpsIndex

MySQL on Kubernetes

Architecture with MySQL Operator (Oracle)

graph TD
    subgraph "MySQL InnoDB Cluster (3 nodes)"
        P3["mysql-0 PRIMARY<br/>R/W<br/>Group Replication source"]
        S1_3["mysql-1 SECONDARY<br/>R/O<br/>Group Replication member"]
        S2_3["mysql-2 SECONDARY<br/>R/O<br/>Group Replication member"]
        MGR["MySQL Group Replication<br/>built-in HA + automatic failover<br/>uses Paxos for consensus"]
        P3 & S1_3 & S2_3 --> MGR
    end
    ROUTER["MySQL Router<br/>connection routing<br/>:3306 --> primary<br/>:3307 --> secondaries"]
    ROUTER --> P3
    ROUTER --> S1_3 & S2_3
    APP3["Application<br/>connects to MySQL Router"] --> ROUTER

MySQL Group Replication uses a Paxos-based consensus protocol — every transaction is certified by a majority of members before committing. This gives virtually synchronous replication — no data loss on failover.


Replication Types

graph LR
    subgraph ASYNC["Async Replication (classic)"]
        A_P["Primary<br/>commits locally"] -->|"binlog event<br/>(no wait)"| A_R["Replica<br/>may be seconds behind"]
    end
    subgraph SEMI["Semi-Sync Replication"]
        S_P["Primary<br/>waits for 1 replica ACK"] -->|"binlog ACK"| S_R["At least 1 replica<br/>has the data"]
        S_P --> S_R2["Other replicas<br/>(async)"]
    end
    subgraph GR["Group Replication (InnoDB Cluster)"]
        G_P["All members certify<br/>transaction before commit"] --> G_R["All members<br/>have data before client sees commit"]
    end
Mode Data loss on failover Write latency Use case
Async Up to replication lag Lowest Read replicas, reporting
Semi-sync Zero (for 1 replica) +1 RTT to replica Production HA
Group Replication Zero +1-2ms (consensus) Production HA, automatic failover

MySQL Operator YAML

apiVersion: mysql.oracle.com/v2
kind: InnoDBCluster
metadata:
  name: mysql
spec:
  secretName: mysql-secret   # root password
  tlsUseSelfSigned: true
  instances: 3
  router:
    instances: 2             # MySQL Router pods for HA routing
  datadirVolumeClaimTemplate:
    accessModes: [ReadWriteOnce]
    resources:
      requests:
        storage: 100Gi
    storageClassName: premium-rwo
  mycnf: |
    [mysqld]
    max_connections=500
    innodb_buffer_pool_size=4G
    binlog_expire_logs_seconds=604800  # 7 days binlog retention
    slow_query_log=ON
    long_query_time=1

Failover

sequenceDiagram
    participant M4 as mysql-0 (Primary)
    participant GR as Group Replication
    participant S1_4 as mysql-1 (Secondary)
    participant ROUTER2 as MySQL Router

    Note over M4: mysql-0 crashes
    GR->>GR: Primary failure detected (5s timeout)
    GR->>S1_4: Elect mysql-1 as new primary (Paxos vote)
    S1_4->>S1_4: Promoted to primary
    ROUTER2->>S1_4: Route writes to mysql-1

    Note over M4: mysql-0 recovers
    M4->>GR: Rejoin group as secondary
    M4->>S1_4: Catch up using binary log

Backups with XtraBackup

# Physical hot backup (no locks, consistent)
xtrabackup --backup \
  --user=backup_user \
  --password=$PASS \
  --target-dir=/backup/$(date +%Y%m%d)

# Upload to GCS
gsutil -m rsync -r /backup/$(date +%Y%m%d) gs://my-mysql-backups/$(date +%Y%m%d)/

# Prepare backup for restore
xtrabackup --prepare --target-dir=/backup/20240115

# Restore
xtrabackup --copy-back --target-dir=/backup/20240115
chown -R mysql:mysql /var/lib/mysql

Monitoring

# Replication lag in seconds
mysql_slave_status_seconds_behind_master > 30

# Connection usage
mysql_global_status_threads_connected / mysql_global_variables_max_connections > 0.8

# InnoDB buffer pool hit rate (alert < 95%)
mysql_global_status_innodb_buffer_pool_read_requests /
(mysql_global_status_innodb_buffer_pool_read_requests +
 mysql_global_status_innodb_buffer_pool_reads) < 0.95

# Slow queries per second
rate(mysql_global_status_slow_queries[5m]) > 0

MySQL on Kubernetes — Oracle MySQL Operator

# Install MySQL Operator
helm repo add mysql-operator https://mysql.github.io/mysql-operator/
helm install mysql-operator mysql-operator/mysql-operator \
  --namespace mysql-operator --create-namespace
# Secret
apiVersion: v1
kind: Secret
metadata:
  name: mysql-secret
  namespace: mysql
type: Opaque
stringData:
  rootUser: root
  rootPassword: "changeme"
---
# 3-node InnoDB Cluster (1 primary + 2 secondary, Group Replication)
apiVersion: mysql.oracle.com/v2
kind: InnoDBCluster
metadata:
  name: mysql
  namespace: mysql
spec:
  secretName: mysql-secret
  tlsUseSelfSigned: true
  instances: 3
  router:
    instances: 2        # MySQL Router pods — route :6446 to primary, :6447 to replicas
  mycnf: |
    [mysqld]
    max_connections = 500
    innodb_buffer_pool_size = 4G
    slow_query_log = ON
    long_query_time = 1
  datadirVolumeClaimTemplate:
    accessModes: [ReadWriteOnce]
    storageClassName: gp3
    resources:
      requests:
        storage: 100Gi
  podSpec:
    containers:
    - name: mysql
      resources:
        requests:
          cpu: "2"
          memory: 8Gi
        limits:
          cpu: "4"
          memory: 16Gi
# Check cluster status
kubectl get innodbcluster mysql -n mysql
# NAME    STATUS   ONLINE   INSTANCES   ROUTERS
# mysql   ONLINE   3        3           2

# Connect via Router (auto-routes to primary)
kubectl port-forward svc/mysql-router 6446:6446 -n mysql
mysql -h 127.0.0.1 -P 6446 -u root -p

# Check Group Replication status
kubectl exec -it mysql-0 -n mysql -- \
  mysql -u root -p -e "SELECT * FROM performance_schema.replication_group_members;"
# Headless service: direct pod DNS (mysql-0.mysql.mysql.svc.cluster.local)
apiVersion: v1
kind: Service
metadata:
  name: mysql
  namespace: mysql
spec:
  clusterIP: None
  selector:
    app: mysql
  ports:
  - port: 3306