Whitepapers / MySQL on Kubernetes

Running MySQL on Kubernetes

A complete architecture guide

MySQL is one of the most widely deployed databases in the world. Running it on Kubernetes is no longer a question of whether, but of how, starting with which replication model to use.

For DBAs, SREs, and platform engineers. Covers MySQL 8.4 LTS and later.

1. Introduction: MySQL meets Kubernetes

MySQL has been in development since 1995 and runs a large part of the web. Running it on Kubernetes is no longer experimental. It is a production-proven approach for organizations that want portability, automation, and freedom from managed-service lock-in.

Is MySQL on Kubernetes production-ready?

Yes. The Percona Operator for MySQL based on Percona XtraDB Cluster has been generally available since 2019. Oracle ships an official operator for InnoDB Cluster, and Percona's Group Replication operator reached general availability in 2025. MySQL's own high-availability features, Group Replication and semi-synchronous replication, are mature and well understood.

Compared with VM-based deployments or a managed service, running MySQL on Kubernetes has three main advantages:

  1. It avoids vendor lock-in. The same cluster runs on AWS, GCP, Azure, or bare metal, so your data and configuration stay portable.
  2. Operations are automated. Operators handle failover, scaling, backups, and upgrades through Kubernetes resources instead of runbooks.
  3. It costs less. You can consolidate workloads, right-size resources, and avoid the premium of managed services like RDS or Cloud SQL.

The real decisions are which replication model, which storage, and which operator to use. Unlike PostgreSQL, where every operator builds on the same streaming replication, MySQL operators differ in the replication model itself, so that choice comes first.

2. MySQL on Kubernetes architectures

A MySQL deployment on Kubernetes has two layers: a routing layer that sends each connection to the right instance, and a database layer that replicates data between instances.

The routing layer (HAProxy, ProxySQL, MySQL Router)

Applications need to reach the current primary for writes, and ideally the replicas for reads, without knowing which pod is which. Most operators put a proxy in front of MySQL for this:

RouterWhat it doesUsed by
HAProxyFast TCP load balancing with health checks. Separate ports for writes and reads.Both Percona operators
ProxySQLMySQL-aware: query routing rules, read/write splitting, and connection multiplexingPercona XtraDB Cluster operator
MySQL RouterFollows Group Replication membership and routes to the primary or replicasOracle operator, Percona Server operator
Kubernetes ServicesOne Service that always points to the primary, one for the replicasMOCO

Run at least two proxy pods, on different nodes from each other, so the routing layer has no single point of failure.

Asynchronous and semi-synchronous replication

This is MySQL's classic replication model. The primary writes changes to its binary log, and replicas copy and apply them. With GTIDs, every transaction has a global ID, which makes promoting a replica and repointing the others safe and automatic.

Asynchronous replication can lose the last transactions if the primary fails before replicas receive them. Semi-synchronous replication closes that gap: in its loss-less mode, the primary waits until at least one replica has received each transaction before it commits. The cost is one network round trip per commit. Semi-sync is fully supported in current MySQL versions. Only the old master and slave names of its plugins and settings were deprecated, in favour of source and replica.

Group Replication (InnoDB Cluster)

Group Replication is MySQL's built-in consensus replication. Before a transaction commits, a majority of the group must agree on it, so a committed transaction survives the loss of any minority of members. If the primary fails, the group elects a new one, and MySQL Router or a proxy follows it. InnoDB Cluster is Oracle's name for Group Replication managed with MySQL Shell and MySQL Router.

Single-primary mode is the standard. Group Replication supports up to nine members, and it has limits worth knowing: every table needs a primary key, and very large transactions are rejected above group_replication_transaction_size_limit, about 143 MB by default.

Galera (Percona XtraDB Cluster)

Galera, used by Percona XtraDB Cluster and MariaDB, replicates each transaction to every node at commit and certifies it there. Every node can accept writes, though in practice sending writes to one node avoids certification conflicts, and that is what the Percona operator's HAProxy does by default. There is no primary to elect: if a node fails, the others keep serving as long as they have quorum.

The trade-offs are flow control, which slows the whole cluster down to its slowest node, and limits on large transactions. Like Group Replication, Galera requires primary keys and only works with InnoDB.

Semi-sync replicationGroup ReplicationGalera
WritesOne primaryOne primary (single-primary mode)Any node, usually one in practice
Commit waits forOne replica to receive itA majority of membersCertification on all nodes
FailoverOperator promotes a replicaGroup elects a primaryNo election needed
RestrictionsFewestPrimary keys, transaction size limitPrimary keys, transaction size limit, flow control
OperatorsMOCOOracle, Percona ServerPercona XtraDB Cluster
Kubernetes cluster: 3 nodes across 3 availability zonesZone A · node-1Zone B · node-2Zone C · node-3HAProxy or RouterHAProxy or RouterHAProxy or RouterPrimary (read/write)Replica (read-only)Replica (read-only)Persistent volumePersistent volumePersistent volumemysql-0mysql-1mysql-2replication: semi-sync, Group Replication, or Galera
A three-instance cluster across three zones. Each zone runs a proxy pod, one MySQL instance, and its own persistent volume. With Group Replication or Galera, losing one zone still leaves a majority, and strict pod anti-affinity keeps the instances on separate nodes.

Pod anti-affinity

A three-instance cluster needs at least three Kubernetes nodes, with each MySQL pod on a different one, so a node failure takes out at most one instance. Most operators set this for you. Check that it is required, not preferred, in production:

affinity:
  podAntiAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
    - topologyKey: kubernetes.io/hostname
      labelSelector:
        matchLabels:
          app: mysql

Multi-AZ deployments

Spread the instances across three availability zones with topologySpreadConstraints, so the cluster keeps its quorum if a whole zone fails. With only two zones, the zone holding the majority takes the cluster down with it.

topologySpreadConstraints:
- maxSkew: 1
  topologyKey: topology.kubernetes.io/zone
  whenUnsatisfiable: DoNotSchedule
  labelSelector:
    matchLabels:
      app: mysql

3. Compute and storage strategy

Getting resources right is the difference between a solid database and one that the Linux OOM killer stops at 3 AM. For MySQL, the InnoDB buffer pool decides most of the memory picture.

Guaranteed QoS (requests equal limits)

For stateless apps, low requests and high limits are a standard way to pack more pods onto each node. For databases, this causes problems. If the memory limit is higher than the request, the Linux OOM killer targets the database pod first when the node runs low on memory.

For MySQL on Kubernetes, set requests equal to limits for both CPU and memory, in every container of the pod. This puts the pod in the Guaranteed QoS class, the last to be killed or evicted when a node is under pressure.

resources:
  requests:
    memory: 2Gi
    cpu: 500m
  limits:
    memory: 8Gi
    cpu: 4000m

Burstable: killed before Guaranteed pods under memory pressure.

resources:
  requests:
    memory: 8Gi
    cpu: 4000m
  limits:
    memory: 8Gi
    cpu: 4000m

Guaranteed: the last to be killed.

QoS classWhen a pod gets it
GuaranteedRequests equal limits
BurstableRequests lower than limits
BestEffortNo requests or limits set

InnoDB buffer pool sizing

innodb_buffer_pool_size sets how much memory InnoDB uses to cache data and indexes. On a dedicated server it is often 70–80% of RAM. In a container, leave more headroom: about 50–75% of the memory limit, because per-connection buffers, temporary tables, and sidecars share the same limit, and going over it gets the pod OOM-killed.

Some operators derive the buffer pool from the container's memory limit if you don't set it. Check what yours does, and check it again whenever you change the limit.

Network-attached storage (EBS, Ceph)

The standard approach is to separate storage from compute with network-attached volumes such as AWS EBS, GCP Persistent Disk, or Ceph. When a node fails, the pod is rescheduled on another node and the same PersistentVolume is re-attached with all its data, so the instance only has to catch up on the changes it missed.

The trade-off is I/O latency, which is higher than local disk because every read and write goes over the network. For most MySQL workloads, network storage with provisioned IOPS is the right default.

Local NVMe SSDs

For latency-sensitive workloads, local NVMe SSDs give the best raw performance, with much lower I/O latency and higher throughput than network-attached storage. They also avoid paying for provisioned IOPS.

The trade-off is durability. If the node fails, the local volume is lost, and the replacement instance has to copy the full dataset from another member, with the clone plugin, XtraBackup, or a Galera state transfer. That takes time and loads the donor. Use local NVMe only with at least three instances and current backups.

Network-attached (EBS)Local NVMe SSD
LatencyAbout 1 msAbout 0.1 ms
ThroughputMediumHigh
Node failureData survives, and the volume re-attaches to the new pod right awayData on the node is lost, and the instance needs a full copy
CostProvisioned IOPS add costBetter cost per IOPS
Best forGeneral workloads and simplicityWrite-heavy, latency-sensitive workloads

4. Monitoring MySQL on Kubernetes

Kubernetes changes how monitoring works. Pods come and go and their IPs change, so the monitoring stack has to discover targets dynamically. Prometheus with mysqld_exporter, or Percona Monitoring and Management (PMM), is the usual choice.

Prometheus, Grafana, and PMM

mysqld_exporter reads MySQL's status variables and Performance Schema and exposes them as Prometheus metrics. Operators usually run it as a sidecar next to each MySQL pod. Prometheus Operator then finds it through a ServiceMonitor or PodMonitor, so you never hardcode scrape targets.

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
spec:
  selector:
    matchLabels:
      app: mysql
  endpoints:
  - port: metrics
    path: /metrics

With the Percona operators, enabling PMM adds a client sidecar that ships metrics and query data to a PMM server, with ready-made MySQL dashboards and Query Analytics.

MySQL-specific metrics

Generic Kubernetes metrics such as CPU, memory, and network tell you the pod is alive. MySQL metrics tell you whether the database is healthy. Instrument these five first:

MetricSourceWhat to watch
Replication lagSeconds_Behind_Source in SHOW REPLICA STATUS; the applier queue in replication_group_member_stats for Group Replication; wsrep_local_recv_queue and wsrep_flow_control_paused for GaleraAlert if lag or queues keep growing. For Galera, time spent in flow control means the cluster is waiting for its slowest node.
Buffer pool hit ratioInnodb_buffer_pool_reads and Innodb_buffer_pool_read_requestsShould stay above 99%. Lower means the working set doesn't fit in memory.
ConnectionsThreads_connected vs max_connections, and Aborted_connectsAlert well before you reach the limit. Running out causes hard application errors.
Threads runningThreads_runningQueries executing right now. Spikes point to lock contention or saturation.
Slow queriesSlow_queries, with long_query_timeThe trend matters more than the absolute number. Query Analytics shows which queries.

Adapting monitoring for Kubernetes

  • Use ServiceMonitor or PodMonitor and never hardcode Prometheus scrape targets.
  • Alert on replication health, not just pod restarts. A Running pod can have stopped replicating.
  • For Group Replication, alert when a member leaves the ONLINE state or the group loses its majority.
  • Alert on unexpected primary changes, which point to evictions, node pressure, or network problems.
  • Track PVC capacity. Binary logs can fill a volume faster than you expect if they aren't purged.
  • Monitor the proxy layer too: HAProxy backend health and ProxySQL connection pools.

5. Backup strategies

Backups are the last line of defence. In cloud-native environments, the standard approach is S3-compatible object storage combined with continuous binary log archiving, which lets you recover to any second in your retention window.

Physical and logical backups

ToolTypeNotes
Percona XtraBackupPhysical, hotOpen source. Copies InnoDB files while the database runs. Fast to back up and restore large databases. Used by both Percona operators.
MySQL Shell dump utilitiesLogicalParallel and much faster than mysqldump, but restores still rebuild every table and index. Used by the Oracle operator and MOCO.
MySQL Enterprise BackupPhysical, hotOracle's commercial tool, with incremental backups and point-in-time recovery in the Oracle operator.

For databases larger than a few hundred gigabytes, physical backups make the difference between restoring in minutes and restoring in hours.

S3-compatible backup storage

The cloud-native standard for backup storage is any S3-compatible endpoint: AWS S3, Google Cloud Storage, Azure Blob, or self-hosted options such as MinIO. These give you durability, versioning, and lifecycle management.

Keep backups outside the Kubernetes cluster: in a separate account or region, or at the very least a separate namespace. If the cluster is compromised or destroyed, your backups survive.

Point-in-time recovery (binary logs)

A full backup is a snapshot of one moment. Real incidents often need a restore to an exact second, such as just before a DELETE without a WHERE clause. This is point-in-time recovery (PITR). It works by uploading binary logs to object storage continuously and replaying them on top of the last full backup, up to the chosen moment or GTID.

Keep binary logs on the server at least until they are uploaded, with binlog_expire_logs_seconds, and check that the upload process keeps up. A gap in the binary logs breaks recovery for everything after it.

Binary logs uploaded continuously to S3Full backupFull backupMon 00:00Thu 00:00Restore to Wed 14:31DELETE without WHERE at 14:32
Recovery restores Monday's full backup, replays the archived binary logs, and stops one minute before the bad DELETE.

With the Percona XtraDB Cluster operator, binary logs are uploaded every 60 seconds by default, and a point-in-time restore is a Kubernetes resource:

apiVersion: pxc.percona.com/v1
kind: PerconaXtraDBClusterRestore
metadata:
  name: before-bad-delete
spec:
  pxcCluster: production
  backupName: daily-backup-mon
  pitr:
    type: date
    date: "2026-10-21 14:31:00"
    backupSource:
      storageName: s3-binlogs

Backup architecture decisions

ApproachSetupWhy
RecommendedPhysical backups with continuous binary log archiving, stored in S3 in a separate cloud account or regionFast restores, recovery to any second, and it survives the loss of the cluster. Test restores regularly, because an untested backup is not a backup.
Acceptable for smaller databasesLogical backups with MySQL Shell, plus binary logsSimple and portable, but restores get slow as data grows.
Acceptable for dev and stagingS3-compatible storage such as MinIO, RustFS, or Ceph running in the same Kubernetes clusterConvenient, but it shares the cluster's fate. If you lose the cluster, you lose the backups.
Not recommendedA mysqldump cron job writing to a local PVCNo point-in-time recovery, slow restores, and the same blast radius as the database.

6. MySQL operators compared

The Kubernetes ecosystem has several MySQL operators, and they differ most in the replication model they use. For scores, a feature-by-feature deep dive, and a picker, see our MySQL operators comparison.

OperatorMaintainerLicenseReplicationBackups
Percona Operator for MySQL (XtraDB Cluster)PerconaApache 2.0GaleraXtraBackup, with PITR
Percona Operator for MySQL (Percona Server)PerconaApache 2.0Group Replication; async in tech previewXtraBackup. PITR in tech preview
Oracle MySQL OperatorOracleUPL 1.0Group Replication (InnoDB Cluster)MySQL Shell dumps. PITR needs Enterprise Backup
MOCOCybozuApache 2.0Semi-synchronousMySQL Shell dumps, with PITR
Vitess OperatorPlanetScaleApache 2.0Sharding on top of MySQL replicationBuilt into Vitess
KubeDBAppsCodeProprietaryGroup Replication or semi-syncKubeStash
OpenEverestCNCF Sandbox projectApache 2.0Galera or Group Replication, through the Percona operatorsXtraBackup, with PITR. A control plane with a UI and API

Percona Operator for MySQL based on Percona XtraDB Cluster

The longest-running production MySQL operator, generally available since 2019. It runs Galera-based Percona XtraDB Cluster with HAProxy or ProxySQL in front, backs up with XtraBackup, and has supported point-in-time recovery since 2021. PMM integration and cross-site replication are included.

Percona Operator for MySQL based on Percona Server

Percona's newer operator, built on upstream Group Replication with HAProxy or MySQL Router. It became generally available for Group Replication in November 2025. Asynchronous replication with Orchestrator, incremental backups, and point-in-time recovery are still tech previews.

Oracle MySQL Operator

The official operator for InnoDB Cluster, using Group Replication, MySQL Router, and MySQL Shell. The free version backs up with MySQL Shell dumps. Physical backups, incremental backups, and point-in-time recovery need MySQL Enterprise Backup, which is part of Oracle's commercial offering.

MOCO

Built by Cybozu around GTID-based, loss-less semi-synchronous replication, with no proxy layer and no Group Replication restrictions. It backs up with MySQL Shell dumps plus binary logs, including point-in-time recovery. A good fit if you want standard MySQL behaviour and a small operator.

Vitess

A sharding layer for MySQL that started at YouTube and graduated from the CNCF. Use it when one MySQL cluster can no longer hold your data or write load. It is not a replacement for the operators above when you just need highly available MySQL.

KubeDB

Part of a multi-database operator suite (PostgreSQL, MongoDB, Redis, and others) with one management plane. It is not open source, and enterprise features need a license.

The Bitpoke (formerly Presslabs) MySQL Operator, once popular for WordPress hosting, hasn't had a release since 2023. Avoid it for new deployments.

OpenEverest

OpenEverest is a CNCF Sandbox project for running databases on Kubernetes through one control plane. For MySQL it offers two providers, one for each Percona operator: Percona XtraDB Cluster for Galera, and Percona Server for Group Replication. It also has a MariaDB provider based on mariadb-operator. Either way you get high availability, backups to S3-compatible storage, and monitoring from the same UI and API.

OpenEverest is modular, so the operator underneath is pluggable. The same approach covers PostgreSQL, MongoDB, and a growing list of other databases. You learn one API and one way of operating, whichever operator or database runs underneath.

Related whitepapers

Run MySQL on Kubernetes with Solanica

Solanica builds on OpenEverest to give you high availability, point-in-time recovery, and monitoring for MySQL through one open source control plane, on infrastructure you own. Talk to us about your setup, or try OpenEverest yourself.