Runbook de Failover de Base de Datos
Un runbook paso a paso para ejecutar procedimientos de failover de base de datos de forma segura con tiempo de inactividad y pérdida de datos mínimos.
Visión General
Un failover promueve una réplica de lectura para que asuma el rol de primaria cuando la original falla o debe desconectarse; la operación inversa — devolver el tráfico al nodo original una vez reconstruido — es el failback que se cubre más adelante en este runbook. Los failovers de base de datos son eventos de alto riesgo donde minutos de retraso significan pérdida de ingresos y erosión de confianza. Un runbook manual reduce los errores impulsados por el pánico al proporcionar pasos exactos, comandos de verificación y procedimientos de rollback. Este runbook cubre la promoción de primaria a réplica, la reconfiguración de aplicaciones y la validación post-failover.
El procedimiento siguiente asume que ya ejecutas replicación en streaming; si todavía la estás configurando, empieza por la guía completa de replicación en PostgreSQL. Para los trade-offs arquitectónicos detrás de este procedimiento, consulta el patrón failover.
Cuándo Usarlo
Usa este runbook cuando:
- La base de datos primaria no responde o está severamente degradada
- Un mantenimiento planificado requiere cambiar a una réplica
- El centro de datos primario experimenta una interrupción
- El failover automático ha fallado y se requiere intervención manual
No uses este runbook cuando:
- RDS Multi-AZ o un orquestador como Patroni o Stolon ya está ejecutando un failover automático saludable — deja que la automatización termine y verifica el resultado
- La primaria está degradada pero es recuperable sin una promoción (mata las consultas bloqueantes, reinicia la instancia, añade capacidad)
- No hay réplica que promover — restaura desde backup y trátalo como un escenario de recuperación ante desastres
Antes de Empezar
Antes de comenzar:
- Acceso a dashboards de monitoreo de base de datos (lag, conexiones, estado de replicación)
- Acceso a gestión de configuración de aplicaciones (variables de entorno, archivos de config, service mesh)
- Acceso a consola de DNS o balanceador de carga
- Equipo de guardia notificado y canal de incidente abierto
- Réplica de lectura confirmada saludable y lag < 5 segundos
Plantilla
# Runbook de Failover de Base de Datos: `<Nombre del Servicio>`
## 1. Verificar la Falla (2 minutos)
### Verificar Salud del Primario
```bash
psql -h primary.db.internal -U monitor -c "SELECT pg_is_in_recovery();"
## MySQL
mysql -h primary.db.internal -u monitor -e "SHOW STATUS LIKE 'Threads_connected';"
```
| Verificación | Esperado | Acción si Falla |
|--------------|----------|-----------------|
| Ping al primario | Respuesta < 10ms | Proceder al failover |
| Conteo de conexiones | < max_connections | Verificar tormenta de conexiones |
| Lag de replicación | N/A (primario) | Confirmar que primario es fuente |
| Espacio en disco | > 10% libre | Si está lleno, failover es la única opción |
### Confirmar que la Réplica está Lista
```bash
## PostgreSQL
psql -h replica.db.internal -U monitor -c "SELECT pg_last_xact_replay_timestamp();"
## MySQL 8.0.22+ (usa SHOW SLAVE STATUS / Seconds_Behind_Master en versiones anteriores)
mysql -h replica.db.internal -u monitor -e "SHOW REPLICA STATUS\G" | grep Seconds_Behind_Source
```
**Puerta de Decisión:** Solo proceder si el lag de réplica < 5 segundos y el disco de réplica es saludable.
## 2. Detener Escrituras al Primario (1 minuto)
```bash
## Configurar aplicación a modo solo lectura (si disponible)
curl -X POST http://app.internal/admin/read-only
## O bloquear en balanceador de carga
## Bloquear puerto 5432/3306 en grupo de seguridad del primario
```
## 3. Promover Réplica a Primario (3 minutos)
### PostgreSQL
```bash
## En la réplica
sudo -u postgres pg_ctl promote -D /var/lib/postgresql/data
## Verificar promoción
psql -h replica.db.internal -U monitor -c "SELECT pg_is_in_recovery();" # Debe retornar false
```
### MySQL
```bash
## En la réplica (MySQL 8.0.22+; usa STOP SLAVE / RESET SLAVE ALL en versiones anteriores)
mysql -u root -e "STOP REPLICA; RESET REPLICA ALL;"
## Verificar
mysql -u root -e "SHOW REPLICA STATUS\G" # Debe retornar conjunto vacío
mysql -u root -e "SHOW BINARY LOG STATUS;" # Debe mostrar posición del binlog (SHOW MASTER STATUS en MySQL < 8.4)
```
### AWS RDS
```bash
aws rds promote-read-replica \
--db-instance-identifier replica-01 \
--region us-east-1
```
## 4. Actualizar Configuración de la Aplicación (2 minutos)
```bash
## Actualizar variable de entorno o config map
export DB_HOST=replica.db.internal
## Recargar aplicación (sin tiempo de inactividad si usa pool de conexiones)
sudo systemctl reload app
## O para Kubernetes
kubectl set env deployment/app DB_HOST=replica.db.internal
kubectl rollout status deployment/app
```
## 5. Cambio de DNS / Balanceador de Carga (2 minutos)
| Método | Comando | RTO |
|--------|---------|-----|
| Registro DNS A | Actualizar a IP de réplica | 5-60 segundos (depende del TTL) |
| Balanceador de carga | Cambiar target group | 10-30 segundos |
| Service mesh (Consul) | Actualizar `consul catalog services` | 5-10 segundos |
| Servicio de Kubernetes | Actualizar endpoint o selector de servicio | Inmediato |
```bash
## Ejemplo: AWS Route53
aws route53 change-resource-record-sets \
--hosted-zone-id Z123456789 \
--change-batch file://failover-dns.json
```
## 6. Verificar Funcionalidad de la Aplicación (3 minutos)
```bash
## Health check
curl -f http://app.internal/health
## Prueba de escritura
curl -X POST http://app.internal/api/test \
-H "Content-Type: application/json" \
-d '{"test": "failover-write-2026-06-26"}'
## Verificación de lectura — reemplaza <id> con el ID devuelto por la prueba de escritura
curl http://app.internal/api/test/<id>
```
| Verificación | Estado | Tiempo |
|--------------|--------|--------|
| Health checks pasando | [ ] | ___ |
| Escritura exitosa | [ ] | ___ |
| Lectura correcta | [ ] | ___ |
| Lag de replicación (nueva réplica) | < 1s | ___ |
| Tasa de error < 0.1% | [ ] | ___ |
## 7. Establecer Nueva Replicación (5 minutos)
### Opción A: Reparar el Viejo Primario (si recuperable)
```bash
## Reconfigurar viejo primario como réplica
## PostgreSQL — el directorio de datos de destino debe estar vacío (mueve o borra el viejo primero)
pg_basebackup -h new-primary.db.internal -D /var/lib/postgresql/data -Fp -Xs -P
## Editar recovery.conf o postgresql.auto.conf con primary_conninfo
sudo -u postgres pg_ctl start
```
### Opción B: Crear Nueva Réplica
```bash
## Desde snapshot o backup base
aws rds create-db-instance-read-replica \
--db-instance-identifier new-replica-01 \
--source-db-instance-identifier new-primary-01
```
## 8. Acciones Post-Incidente
- [ ] Actualizar línea de tiempo del incidente con tiempos exactos de cada paso
- [ ] Capturar logs del viejo primario para análisis de causa raíz
- [ ] Documentar pérdida de datos (si hay) con IDs de transacción exactos
- [ ] Programar postmortem dentro de 24 horas
- [ ] Actualizar este runbook con lecciones aprendidas
Explicación
El runbook separa la verificación (confirmar falla, confirmar salud de réplica) de la ejecución (promoción, cambio) y la validación (pruebas de escritura/lectura). La puerta de decisión en el paso 1 previene failovers hacia una réplica no saludable. El cambio de DNS se prefiere sobre reinicios de aplicación porque minimiza el RTO y evita retrasos por calentamiento de pools de conexión.
Dos números deciden qué tan mal puede salir un failover: el RPO, la cantidad de datos confirmados que puedes permitirte perder, y el RTO, cuánto tiempo puede estar caída la base de datos. La puerta de lag de 5 segundos en el paso 1 es el control de RPO — cada transacción que la réplica no haya replicado al momento de la promoción es un dato que la aplicación nunca volverá a ver. El método de cambio es el control de RTO: DNS es barato pero está limitado por el TTL, un balanceador de carga o service mesh converge en segundos, y un pooler de conexiones como PgBouncer puede pausar y reanudar el tráfico de clientes sin provocar una tormenta de reconexiones.
El orden de los pasos también protege contra el split-brain, el peor resultado de un failover mal ejecutado. Las escrituras se detienen (paso 2) antes de promover la réplica (paso 3) precisamente para que ambos nodos nunca acepten escrituras al mismo tiempo. Si el viejo primario vuelve inesperadamente después del cambio, no debe servir tráfico otra vez hasta que sea reconstruido como réplica — por eso existen el paso 7 y el procedimiento de failback siguiente.
Failback: Regreso al Primario Original
Un failover es solo la mitad de la operación. Una vez que el viejo primario vuelve a estar saludable, decide si mantener la réplica promovida como primaria permanente o hacer failback al original.
Mantén la réplica promovida como primaria cuando:
- Tiene capacidad igual o superior al viejo primario
- La revisión del incidente no encontró ventajas de hardware o ubicación en el nodo original
- Quieres evitar una segunda ventana de interrupción — cada cambio conlleva riesgo
Haz failback al primario original cuando:
- La réplica era capacidad de emergencia (instancia más pequeña, menos IOPS aprovisionadas)
- Las licencias, las herramientas de backup o la ubicación regional favorecen al nodo original
- El viejo primario contiene configuración fijada que la réplica no tiene
Procedimiento de failback
- Reconstruye el viejo primario como réplica del nuevo primario (paso 7 del runbook). En PostgreSQL,
pg_rewindes más rápido que unpg_basebackupcompleto cuando ambos nodos comparten un historial común de checkpoints:
## En el viejo primario, detenido y con un directorio de datos válido (no borrado)
pg_rewind --target-pgdata=/var/lib/postgresql/data \
--source-server="host=new-primary.db.internal user=replicator"
pg_rewind requiere wal_log_hints = on o checksums de datos activados — verifica SHOW data_checksums; antes de confiar en él. Si falla, vuelve a pg_basebackup sobre un directorio de datos vacío.
- Espera hasta que el lag de replicación en el nodo reconstruido sea inferior a 1 segundo.
- Programa una ventana de mantenimiento — un failback es un failover planificado, no una emergencia.
- Ejecuta el mismo runbook a la inversa: detén escrituras, promueve el primario original, revierte el DNS o balanceador de carga, verifica las escrituras.
- Restablece la replicación en la dirección final y actualiza dashboards y alertas para que coincidan con la nueva topología.
No omitas la tabla de verificación del paso 6 durante un failback. Una promoción que funciona en una dirección puede fallar en la otra por valores obsoletos de primary_conninfo o slots de replicación residuales.
Variantes
| Contexto | Enfoque | Notas |
|---|---|---|
| Replicación en streaming de PostgreSQL | pg_ctl promote | Más rápido, requiere streaming WAL saludable |
| MySQL GTID | STOP REPLICA; RESET REPLICA ALL; | GTID simplifica encontrar la posición correcta |
| AWS RDS Multi-AZ | Failover automático | Solo usar este runbook para cross-region o promoción manual |
| Kubernetes StatefulSet | Orquestador Patroni / Stolon | El operador maneja la promoción; runbook para falla del operador |
Adaptar Este Runbook
La plantilla está escrita para un setup genérico de dos nodos, así que reemplaza cada hostname *.db.internal, puerto y rol de credencial (monitor, replicator) con tus valores reales antes de que ocurra un incidente. Ajusta la puerta de lag de 5 segundos a tu objetivo de RPO real — una base de datos de pagos puede necesitar replicación síncrona y una puerta de lag cero, mientras que una réplica de analítica tolera más. Mapea cada paso a tus herramientas: Cloud SQL y Aurora tienen sus propios comandos de promoción, y los proxies gestionados reemplazan el paso de DNS en algunos stacks.
Dos detalles que los equipos olvidan hasta el primer simulacro: la deriva de versiones y los permisos. La réplica debe ejecutar la misma versión mayor que la primaria o la promoción fallará, y el ingeniero de guardia necesita las credenciales de cloud o SSH para promover antes del incidente — anota dónde viven. Por último, mantén una copia impresa de una página o fijada con los pasos 1-6 en el canal de incidentes — durante una interrupción real nadie hace scroll. Las versiones descargables (la plantilla del runbook, el script de automatización, la config de Patroni y el verificador de consistencia) están en el repositorio complementario.
Lo que funciona
- Prueba este runbook mensualmente en un entorno de staging — no durante el incidente; un plan de pruebas de recuperación ante desastres le da al simulacro una estructura de aprobado/fallido
- Automatiza los health checks en los pasos 1 y 6 con scripts, no consultas manuales
- Usa pooling de conexiones (PgBouncer, ProxySQL) para evitar retrasos de TTL DNS
- Monitorea el lag de replicación continuamente — lag > 30s debe alertar al equipo de guardia
- Documenta el ID de transacción exacto en la promoción para cálculo de pérdida de datos
- Comunica durante el evento — publica actualizaciones de estado con la plantilla de comunicación de caídas para que los stakeholders no tengan que adivinar
Errores Comunes
- Hacer failover a una réplica con lag — resulta en pérdida de datos y errores de aplicación
- No detener escrituras antes de la promoción — cerebro dividido, conjuntos de datos divergentes
- Olvidar actualizar la config de la aplicación — apps reconectan al viejo primario fallido
- No verificar escrituras post-failover — fallas silenciosas pasan desapercibidas por horas
- Omitir configurar nueva réplica — ejecutando sin redundancia después del failover
Soluciones Avanzadas
Script de failover automatizado con verificaciones pre-flight
Combina todos los pasos del runbook en un solo script ejecutable con puertas de seguridad:
#!/bin/bash
# failover.sh - Failover de base de datos automatizado con verificaciones pre-flight
# Uso: ./failover.sh [--force] [--dry-run]
set -euo pipefail
FORCE=false
DRY_RUN=false
for arg in "$@"; do
case $arg in
--force) FORCE=true ;;
--dry-run) DRY_RUN=true ;;
esac
done
PRIMARY_HOST="primary.db.internal"
REPLICA_HOST="replica.db.internal"
DB_USER="monitor"
MAX_LAG_SECONDS=5
log() { echo "[$(date -u +%H:%M:%S)] $1"; }
# Paso 1: Verificaciones pre-flight
log "Running pre-flight checks..."
# Comprobar que el primario realmente está caído
if ping -c 1 -W 2 "$PRIMARY_HOST" &>/dev/null && ! $FORCE; then
log "ERROR: Primary is reachable. Use --force to override."
exit 1
fi
# Comprobar lag de la réplica
log "Checking replica lag..."
PG_LAG=$(psql -h "$REPLICA_HOST" -U "$DB_USER" -t -c \
"SELECT EXTRACT(EPOCH FROM (now() - pg_last_xact_replay_timestamp()));" 2>/dev/null | xargs)
if (( $(echo "$PG_LAG > $MAX_LAG_SECONDS" | bc -l) )) && ! $FORCE; then
log "ERROR: Replica lag is ${PG_LAG}s (max: ${MAX_LAG_SECONDS}s). Use --force to override."
exit 1
fi
log "Pre-flight checks passed. Replica lag: ${PG_LAG}s"
if $DRY_RUN; then
log "DRY RUN: Would proceed with failover."
exit 0
fi
# Paso 2: Activar modo solo lectura
log "Enabling read-only mode..."
curl -sS -X POST http://app.internal/admin/read-only || log "WARN: Could not enable read-only mode"
# Paso 3: Promover réplica
log "Promoting replica to primary..."
sudo -u postgres pg_ctl promote -D /var/lib/postgresql/data
# Verificar promoción
IS_RECOVERY=$(psql -h "$REPLICA_HOST" -U "$DB_USER" -t -c "SELECT pg_is_in_recovery();" | xargs)
if [ "$IS_RECOVERY" != "f" ]; then
log "ERROR: Promotion failed. pg_is_in_recovery returned: $IS_RECOVERY"
exit 1
fi
log "Promotion successful."
# Paso 4: Actualizar config de la aplicación
log "Updating application configuration..."
kubectl set env deployment/app DB_HOST="$REPLICA_HOST"
kubectl rollout status deployment/app --timeout=120s
# Paso 5: Verificar
log "Running post-failover verification..."
sleep 5
HTTP_CODE=$(curl -sS -o /dev/null -w "%{http_code}" http://app.internal/health)
if [ "$HTTP_CODE" != "200" ]; then
log "ERROR: Health check failed with HTTP $HTTP_CODE"
exit 1
fi
WRITE_RESULT=$(curl -sS -X POST http://app.internal/api/test \
-H "Content-Type: application/json" \
-d "{\"test\": \"failover-$(date +%s)\"}" -w "\n%{http_code}")
WRITE_CODE=$(echo "$WRITE_RESULT" | tail -1)
if [ "$WRITE_CODE" != "200" ] && [ "$WRITE_CODE" != "201" ]; then
log "ERROR: Write test failed with HTTP $WRITE_CODE"
exit 1
fi
log "Failover complete. All verifications passed."
log "Next steps:"
log " 1. Establish new replication (Step 7 of runbook)"
log " 2. Update incident timeline"
log " 3. Schedule postmortem"
Configuración de failover automatizado con Patroni
Para PostgreSQL, Patroni proporciona failover automatizado con health checks y gestión de cluster:
# patroni.yml
name: postgres-cluster
scope: pgsql
restapi:
listen: 0.0.0.0:8008
connect_address: $(HOSTIP):8008
etcd:
hosts: etcd1.internal:2379,etcd2.internal:2379,etcd3.internal:2379
bootstrap:
dcs:
ttl: 30
loop_wait: 10
retry_timeout: 20
maximum_lag_on_failover: 1048576 # 1MB
maximum_lag_on_syncnode: 1048576
synchronous_mode: true
synchronous_mode_strict: false
postgresql:
use_pg_rewind: true
use_slots: true
parameters:
wal_level: replica
hot_standby: "on"
max_wal_senders: 10
max_replication_slots: 10
wal_keep_size: 128MB # wal_keep_segments en PostgreSQL < 13
archive_mode: "on"
archive_timeout: 1800s
recovery_conf:
restore_command: "wal-g wal-fetch %f %p"
postgresql:
listen: 0.0.0.0:5432
connect_address: $(HOSTIP):5432
data_dir: /var/lib/postgresql/data
bin_dir: /usr/lib/postgresql/bin
pg_hba:
- "replication replicator 0.0.0.0/0 md5"
- "host all all 0.0.0.0/0 md5"
replication:
username: replicator
password: "${REPLICATION_PASSWORD}"
network: 0.0.0.0/0
superuser:
username: postgres
password: "${POSTGRES_PASSWORD}"
tags:
nofailover: false
noloadbalance: false
clonefrom: false
nosync: false
Verificación de consistencia de datos post-failover
Verifica la integridad de datos después del failover comparando logs de transacciones:
import psycopg2
from dataclasses import dataclass
@dataclass
class FailoverVerification:
promoted_node: str
old_primary_lsn: str # LSN (log sequence number) en el momento de la falla
new_primary_lsn: str # LSN en el momento de la promoción
transactions_lost: int
consistency_ok: bool
def verify_failover_consistency(
new_primary_host: str,
old_primary_lsn: str,
db_user: str = "monitor",
) -> FailoverVerification:
"""Verifica la consistencia de datos después de un failover de base de datos."""
conn = psycopg2.connect(host=new_primary_host, user=db_user, dbname="postgres")
cur = conn.cursor()
# Obtener el LSN actual en el nuevo primario
cur.execute("SELECT pg_current_wal_lsn();")
new_lsn = cur.fetchone()[0]
# Contar transacciones desde el punto de promoción
cur.execute("""
SELECT count(*) FROM pg_stat_activity
WHERE state = 'active' AND xact_start > now() - interval '5 minutes';
""")
active_txns = cur.fetchone()[0]
# Comprobar salud de los slots de replicación
cur.execute("""
SELECT slot_name, active, restart_lsn
FROM pg_replication_slots;
""")
slots = cur.fetchall()
# Verificar que todos los slots están activos
all_active = all(slot[1] for slot in slots) if slots else True
# Estimar transacciones perdidas (simplificado)
lost = 0 if old_primary_lsn == "unknown" else estimate_lost(old_primary_lsn, new_lsn)
result = FailoverVerification(
promoted_node=new_primary_host,
old_primary_lsn=old_primary_lsn,
new_primary_lsn=new_lsn,
transactions_lost=lost,
consistency_ok=all_active,
)
cur.close()
conn.close()
return result
def estimate_lost(old_lsn: str, new_lsn: str) -> int:
"""Estima las transacciones perdidas entre dos LSN."""
# Parsear formato LSN (p. ej., '0/17000058')
try:
old_parts = [int(x, 16) for x in old_lsn.split("/")]
new_parts = [int(x, 16) for x in new_lsn.split("/")]
old_bytes = old_parts[0] * 0x100000000 + old_parts[1]
new_bytes = new_parts[0] * 0x100000000 + new_parts[1]
diff = new_bytes - old_bytes
# Estimación aproximada: 1 transacción ~ 200 bytes de media
return max(0, diff // 200)
except (ValueError, IndexError):
return 0
# Ejemplo de uso
result = verify_failover_consistency(
new_primary_host="replica.db.internal",
old_primary_lsn="0/17000058",
)
print(f"Promoted node: {result.promoted_node}")
print(f"Transactions lost (est.): {result.transactions_lost}")
print(f"Replication slots healthy: {result.consistency_ok}")
Árbol de Decisión de Failover
Lectura Adicional
Preguntas frecuentes
¿Cómo sé si la réplica está al día?
PostgreSQL: pg_last_xact_replay_timestamp() debe estar dentro de 5 segundos de now(). MySQL: Seconds_Behind_Source debe ser 0 (Seconds_Behind_Master en versiones anteriores a 8.0.22). Verifica siempre antes de promover.
¿Qué pasa si el viejo primario vuelve online después del failover?
Apágalo inmediatamente o configúralo como réplica. Un viejo primario que acepta escrituras crea un escenario de cerebro dividido. El enfoque más seguro: apagarlo hasta poder reconfigurarlo.
¿Cómo minimizo el RTO durante un failover?
Usa un balanceador de carga o service mesh en lugar de DNS. Pre-configura el endpoint de réplica en la aplicación con un pooler de conexiones. Automatiza el paso de promoción con un script que retorne en menos de 10 segundos.
¿Cuál es la diferencia entre failover y failback?
El failover promueve una réplica cuando la primaria falla — normalmente no es planificado. El failback es la operación inversa: una vez que el primario original está reconstruido como réplica saludable, ejecutas un failover planificado para devolverle el rol de primaria. El failback siempre se programa; el failover a menudo no.
¿Cómo se aplican el RTO y el RPO a un failover de base de datos?
El RTO es cuánto tiempo permanece caída la base de datos — los pasos cronometrados de este runbook apuntan a unos 15-20 minutos de inactividad. El RPO es cuántos datos confirmados puedes permitirte perder: con replicación asíncrona, el RPO equivale al lag de la réplica en el momento de la promoción, razón por la que existe la puerta de lag de 5 segundos. La replicación síncrona puede llevar el RPO a cero a costa de latencia de escritura.
Recursos Relacionados
Plan de Pruebas de Recuperación ante Desastres
Una plantilla para planificar y ejecutar pruebas de recuperación ante desastres incluyendo validación de failover, verificación de integridad de datos y medición del tiempo de recuperación.
DocRunbook de Rollback de Despliegues
Runbook para revertir despliegues fallidos: triggers, comandos de kubectl/Helm/ArgoCD, rollback de migraciones de base de datos y checklists de verificación.
DocPlantilla de Runbook de Migración de Datos con Rollback
Usa esta plantilla de runbook para migrar datos de forma segura. Incluye pre-chequeos, pasos de ejecución, rollback y validación post-migración.
DocPlantilla de Política de Escalamiento
Una plantilla para definir niveles de severidad de incidentes y rutas de escalamiento de guardia.
DocPlantilla de Comunicación de Tiempo de Inactividad
Plantilla para mensajes internos y externos durante interrupciones del servicio.
PatternPatrón Failover: Conmuta a Standby ante Fallo del Primario
Cómo desviar el tráfico a un sistema standby cuando el primario falla. Cubre active-passive, active-active, health checks, failover DNS y promoción de réplicas.