MySQL en 2026: replicación, GTID y failover automático en producción con Kubernetes
Introducción: por qué la replicación MySQL sigue siendo relevante en 2026
A pesar del crecimiento explosivo de las soluciones NoSQL y las bases de datos NewSQL, MySQL mantiene posiciones de liderazgo en los stacks de producción de grandes empresas. Según los datos de DB-Engines para 2025–2026, MySQL se mantiene de forma estable en el top 3 de los SGBD relacionales. Las razones son simples: un ecosistema maduro, un comportamiento predecible bajo carga y una comunidad enorme.
En el contexto de Kubernetes, la replicación MySQL resuelve varias tareas críticas a la vez:
- Alta disponibilidad (HA) — ante la caída del Primary, la conmutación automática a la Replica minimiza el downtime.
- Escalado horizontal de lecturas — las réplicas de lectura aceptan consultas SELECT, reduciendo la carga sobre el Primary.
- Copias de seguridad sin bloqueos — realizar un dump desde una réplica no afecta al tráfico de producción.
- Recuperación ante desastres — las réplicas distribuidas geográficamente protegen contra fallos de datacenter.
En este artículo recorreremos el camino completo: desde la teoría de GTID hasta un clúster funcional en Kubernetes con failover automático e integración en CI/CD.
Fundamentos de la replicación GTID: qué ha cambiado y por qué importa
GTID (Global Transaction Identifier) es un identificador único asignado a cada transacción en MySQL. El formato es: source_uuid:transaction_id, por ejemplo 3E11FA47-71CA-11E1-9E33-C80AA9429562:1-100.
Diferencias respecto a la replicación clásica por binlog
En la replicación clásica, la réplica rastrea la posición en el log binario (archivo + offset). Ante un failover, el administrador debe determinar manualmente la posición correcta en el nuevo Primary, lo que es fuente de errores humanos. GTID elimina este problema: cada transacción tiene un identificador único global, y la réplica determina por sí misma qué transacciones no ha aplicado aún.
Ventajas clave de la replicación GTID:
- Determinación automática del punto de replicación al cambiar el Primary.
- Simplicidad en la configuración de Orchestrator y MySQL Operator para failover automático.
- Garantía de ausencia de transacciones duplicadas en la réplica.
- Soporte nativo desde MySQL 5.6+ y madurez completa en MySQL 8.0/8.4.
En MySQL 8.4 (LTS, 2024–2026), la replicación GTID se ha convertido en el estándar de facto: varios parámetros obsoletos master_* han sido completamente reemplazados por source_*.
Configuración de MySQL Primary/Replica con GTID
Configuración de my.cnf para el Primary
[mysqld]
# Identificador del servidor — único en el clúster
server-id = 1
# Log binario
log_bin = /var/log/mysql/mysql-bin.log
binlog_format = ROW
binlog_row_image = FULL
# GTID
gtid_mode = ON
enforce_gtid_consistency = ON
# Rendimiento y fiabilidad
innodb_flush_log_at_trx_commit = 1
sync_binlog = 1
# Replicación
binlog_expire_logs_seconds = 604800
max_binlog_size = 100M
# Replicación paralela en réplicas (MySQL 8.0+)
binlog_transaction_dependency_tracking = WRITESET
transaction_write_set_extraction = XXHASH64
Configuración de my.cnf para la Replica
[mysqld]
server-id = 2
# Réplica de solo lectura
read_only = ON
super_read_only = ON
# GTID
gtid_mode = ON
enforce_gtid_consistency = ON
# Logging en la réplica (necesario para cadenas de replicación)
log_bin = /var/log/mysql/mysql-bin.log
log_replica_updates = ON
binlog_format = ROW
# Aplicación paralela de transacciones
replica_parallel_workers = 4
replica_parallel_type = LOGICAL_CLOCK
replica_preserve_commit_order = ON
# Reconexión automática de la replicación ante error de conexión
replica_net_timeout = 60
Inicialización de la réplica
Creamos el usuario de replicación en el Primary:
-- En el Primary
CREATE USER 'replicator'@'%' IDENTIFIED WITH caching_sha2_password BY 'StrongPass!2026';
GRANT REPLICATION SLAVE ON *.* TO 'replicator'@'%';
FLUSH PRIVILEGES;
Generamos un dump del Primary para inicializar la réplica (sin bloqueo de datos):
mysqldump \
--single-transaction \
--master-data=2 \
--set-gtid-purged=ON \
--all-databases \
-u root -p > full_backup.sql
Restauramos en la réplica e iniciamos la replicación:
-- En la Replica tras restaurar el 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;
-- Verificamos el estado
SHOW REPLICA STATUS\G
Campos clave en la salida de SHOW REPLICA STATUS: Replica_IO_Running: Yes, Replica_SQL_Running: Yes, Seconds_Behind_Source: 0; los campos Retrieved_Gtid_Set y Executed_Gtid_Set deben coincidir cuando el retraso es cero.
Despliegue del clúster MySQL en Kubernetes
Componentes de la arquitectura
Para aplicaciones con estado en Kubernetes se utilizan StatefulSets: garantizan identidades de red estables (mysql-0, mysql-1) y vinculación a Persistent Volumes específicos. Esto es crítico para MySQL, donde cada nodo debe tener un server-id único y un hostname estable.
Headless Service
apiVersion: v1
kind: Service
metadata:
name: mysql
namespace: mysql
labels:
app: mysql
spec:
clusterIP: None # Headless — sin IP virtual
selector:
app: mysql
ports:
- name: mysql
port: 3306
targetPort: 3306
ConfigMap con la configuración de MySQL
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
# Generamos server-id basado en el número ordinal del pod
[[ $(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
# Copiamos la configuración de Primary o Replica
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
Failover automático: herramientas y configuración
MySQL Operator for Kubernetes (Oracle)
MySQL Operator for Kubernetes es el operador oficial de Oracle que implementa la gestión del clúster InnoDB Cluster (MySQL Group Replication). En 2026, este es el enfoque recomendado para entornos de producción.
Instalación mediante 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
Creación del clúster mediante 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 realiza automáticamente:
- Configuración de Group Replication con selección automática de Primary.
- Despliegue de MySQL Router para enrutamiento transparente de consultas.
- Failover ante la no disponibilidad del Primary (habitualmente en 5–30 segundos).
- Gestión de certificados TLS entre los nodos.
Orchestrator — alternativa para la replicación clásica
Si utilizas la replicación clásica Primary/Replica (no Group Replication), Orchestrator de GitHub/Pinterest es una herramienta probada para el failover automático. Construye la topología de replicación, detecta los nodos no disponibles y promueve automáticamente la mejor réplica a Primary.
Despliegue de Orchestrator en 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
Parámetros clave de configuración de Orchestrator para la replicación GTID:
{
"AutomatedRecoveryMasterDetachLostReplicas": true,
"RecoverMasterClusterFilters": ["*"],
"RecoveryPeriodBlockSeconds": 3600,
"FailMasterPromotionOnLagMinutes": 0,
"DetachLostReplicasAfterMasterFailover": true,
"MasterFailoverLostInstancesDowntimeMinutes": 0,
"PostMasterFailoverProcesses": [
"update-dns.sh --old={failedHost} --new={successorHost}"
]
}
Monitoreo de la replicación: métricas, Prometheus y alertas
mysqld_exporter para Prometheus
Para monitorear MySQL en Kubernetes utilizamos mysqld_exporter. Lo desplegamos como contenedor sidecar o como Deployment independiente.
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
Métricas clave de replicación
mysql_slave_status_seconds_behind_master— retraso de la réplica en segundos (renombrada amysql_replica_status_seconds_behind_sourceen versiones nuevas).mysql_slave_status_slave_io_running— estado del hilo IO de replicación (1 = activo).mysql_slave_status_slave_sql_running— estado del hilo SQL de replicación.mysql_global_status_binlog_cache_disk_use— uso de la caché en disco del binlog.mysql_global_status_threads_running— hilos activos.
Reglas de alerting para Prometheus Alertmanager
groups:
- name: mysql-replication
rules:
- alert: MySQLReplicationLag
expr: mysql_slave_status_seconds_behind_master > 30
for: 5m
labels:
severity: warning
annotations:
summary: "La replicación MySQL acumula más de 30 segundos de retraso"
description: "El pod {{ $labels.pod }} tiene un retraso de {{ $value }}s"
- alert: MySQLReplicationIOThreadDown
expr: mysql_slave_status_slave_io_running == 0
for: 1m
labels:
severity: critical
annotations:
summary: "El hilo IO de replicación de MySQL está detenido"
- alert: MySQLReplicationSQLThreadDown
expr: mysql_slave_status_slave_sql_running == 0
for: 1m
labels:
severity: critical
annotations:
summary: "El hilo SQL de replicación de MySQL está detenido"
- 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: "CRÍTICO: la replicación MySQL no está funcionando"
Problemas típicos de replicación y su solución
1. Error 1062: Duplicate entry
Ocurre cuando se viola una clave única en la réplica. La causa es que los datos de la réplica difieren del Primary (por ejemplo, debido a escrituras directas en la réplica). Solución: nunca escribir directamente en la réplica (super_read_only=ON). Si ocurre, omite la transacción problemática mediante GTID:
-- Localizamos el GTID problemático en SHOW REPLICA STATUS\G
-- Lo omitimos:
STOP REPLICA;
SET GTID_NEXT='3E11FA47-71CA-11E1-9E33-C80AA9429562:101';
BEGIN; COMMIT;
SET GTID_NEXT='AUTOMATIC';
START REPLICA;
2. Gran retraso de replicación bajo carga
Con alto tráfico de escritura, la aplicación monohilo de transacciones en la réplica genera un cuello de botella. La solución es la replicación multihilo:
SET GLOBAL replica_parallel_workers = 8;
SET GLOBAL replica_parallel_type = 'LOGICAL_CLOCK';
SET GLOBAL replica_preserve_commit_order = ON;
3. Pérdida de conexión entre Primary y Replica
Aumentamos el timeout y habilitamos la reconexión automática:
CHANGE REPLICATION SOURCE TO
SOURCE_CONNECT_RETRY=10,
SOURCE_RETRY_COUNT=86400,
SOURCE_HEARTBEAT_PERIOD=5;
4. Split-brain durante el failover
Situación peligrosa en la que el antiguo Primary "resucita" y comienza a aceptar escrituras en paralelo con el nuevo. Protección: usar STONITH/fencing a nivel de Kubernetes (PodDisruptionBudget), así como habilitar super_read_only=ON en todos los nodos por defecto — Orchestrator/Operator solo lo desactiva en el Primary activo.
5. Problemas con GTID al restaurar desde una copia de seguridad
Al restaurar un dump creado sin --set-gtid-purged=ON se producen conflictos. Utiliza siempre este flag al crear dumps para replicación. Si los conjuntos GTID entran en conflicto, puedes restablecerlos:
RESET MASTER;
SET @@GLOBAL.gtid_purged='3E11FA47-71CA-11E1-9E33-C80AA9429562:1-500';
Integración CI/CD: migraciones automáticas de esquema con replicación
Las migraciones de esquema en un entorno con replicación requieren especial atención. Un error típico es ejecutar ALTER TABLE sin considerar el impacto sobre las réplicas y el lag de replicación.
Principios para migraciones seguras
- Online DDL: utiliza
ALTER TABLE ... ALGORITHM=INPLACE, LOCK=NONEpara MySQL 8.x, o la herramientagh-ost, que realiza la migración mediante una tabla sombra y no bloquea el entorno de producción. - Monitoreo del lag antes de la migración: el pipeline CI/CD debe verificar
Seconds_Behind_Sourceantes de ejecutar un ALTER. Si el lag supera el umbral, la migración se pospone. - Compatibilidad hacia atrás: añade columnas como
NULLo con valor por defecto, de modo que el nuevo código funcione con el esquema antiguo y viceversa.
Ejemplo de paso CI/CD con migración mediante gh-ost en GitLab
migrate-schema:
stage: deploy
image: github/gh-ost:latest
script:
- |
# Verificamos el lag de replicación
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 "El lag de replicación es $LAG segundos, abortando la migración"
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
El flag --max-lag-millis hace que gh-ost reduzca automáticamente su velocidad o se detenga si el retraso de replicación supera el umbral, protegiendo el entorno de producción de la degradación durante las migraciones.
Integración con Flyway / Liquibase
Al usar Flyway o Liquibase en Kubernetes, ejecuta las migraciones como un Kubernetes Job con restartPolicy: Never para garantizar una única ejecución. Utiliza la tabla de bloqueos (flyway_schema_history) — se replica en todos los nodos y evita la doble ejecución.
Conclusiones y recomendaciones prácticas
La configuración de la replicación MySQL con GTID en Kubernetes en 2026 es una solución madura, bien documentada y con un amplio conjunto de herramientas. Resumamos los puntos clave:
- GTID es obligatorio: no uses replicación posicional en proyectos nuevos. GTID simplifica el failover, la administración y la integración con operadores.
- MySQL Operator for Kubernetes — la opción recomendada para nuevos despliegues en producción. Group Replication con failover automático "de serie" ahorra meses de trabajo de automatización.
- StatefulSets + headless services — la abstracción correcta para aplicaciones con estado en Kubernetes. No intentes ejecutar MySQL en Deployments convencionales.
- Replicación multihilo:
replica_parallel_workersyLOGICAL_CLOCKson críticos bajo alta carga. - El monitoreo no es opcional: configura alertas sobre el lag de replicación y la detención de hilos antes de que el problema afecte a los usuarios.
- gh-ost para DDL: los ALTER TABLE bloqueantes en producción con replicación son el camino hacia un incidente. Usa siempre herramientas de migración online.
- Prueba el failover regularmente: el Chaos Engineering (por ejemplo, Chaos Mesh en Kubernetes) debe incluir escenarios de eliminación del Primary y verificación del tiempo de recuperación.
Un clúster MySQL correctamente configurado en Kubernetes proporciona una fiabilidad del 99,99% con un overhead operativo mínimo, lo que lo convierte en una elección vigente para entornos de producción en 2026 y más allá.
Tecnologías
Etiquetas
Ruslan Ismailov
Desarrollador Senior Web / Backend. Desarrollador senior web/backend con 9 años de experiencia. Stack: PHP, Laravel, PostgreSQL, Redis, Docker, Kubernetes, REST, microservicios, CI/CD. Más sobre mí →