Bases de datos

MySQL en 2026: replicación, GTID y failover automático en producción con Kubernetes

Ruslan Ismailov Publicado 14 min de lectura
M

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 a mysql_replica_status_seconds_behind_source en 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=NONE para MySQL 8.x, o la herramienta gh-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_Source antes de ejecutar un ALTER. Si el lag supera el umbral, la migración se pospone.
  • Compatibilidad hacia atrás: añade columnas como NULL o 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_workers y LOGICAL_CLOCK son 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í →