StackPractices
advanced Por Mathias Paulenko

Patró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.

Overview

Tu nodo primario va a fallar tarde o temprano — un kernel panic, un deploy que crashea, una región de AWS teniendo un mal día. El patrón failover responde a lo que pasa después: un health checker detecta que el primario está caído y redirige el tráfico a un standby que ya estaba corriendo. Bien hecho, los usuarios ven un parpadeo breve; mal hecho, ven un 502 hasta que alguien se despierta y lo arregla.

El patrón se escribe a veces “fallover” en documentación de vendors antigua, pero “failover” es el término estándar en AWS, Azure, Kubernetes y la documentación de PostgreSQL, y también lo que la gente busca.

Hay dos configuraciones principales. En active-passive, un nodo sirve todo el tráfico y el standby espera inactivo hasta ser promovido. En active-active, ambos nodos sirven tráfico y un fallo simplemente reduce la capacidad: el nodo superviviente absorbe toda la carga. El failover también puede ocurrir en cuatro capas distintas: DNS (apuntar el registro a otra IP), load balancer (dejar de enviar requests a un upstream no saludable), base de datos (promover una réplica de solo lectura a primario escribible) o nivel de aplicación (el cliente reintenta contra un endpoint de respaldo). Cada capa cambia velocidad de conmutación por complejidad, y la mayoría de los sistemas en producción combinan dos o tres.

When to Use

  • Servicios con un objetivo de disponibilidad que un solo nodo no puede cumplir (99.9%+ suele implicar failover automático)
  • Sistemas con estado como bases de datos, donde el primario puede morir a mitad de una transacción y una réplica debe tomar el relevo
  • Despliegues multi-región donde una región entera puede quedar inalcanzable
  • Llamadas a APIs de terceros que publican un endpoint de respaldo o una región secundaria documentada
  • Planes de disaster recovery que prometen un RTO de pocos minutos, que un reinicio manual casi nunca cumple

When NOT to Use

  • Aplicaciones de una sola instancia donde unos minutos de caída cuestan menos que mantener infraestructura ociosa
  • Operaciones que no deben completarse dos veces: promover automáticamente un primario ambiguo puede producir split-brain, que es peor que una caída. Los sistemas financieros suelen preferir failover manual, con un humano confirmando que el primario anterior está realmente muerto
  • Servicios stateless detrás de un load balancer con health checks, ya que el LB ya retira las instancias muertas, así que una segunda capa de failover no aporta nada
  • Cuando el standby quedaría obsoleto de todos modos (sin replicación, sin caché caliente): el failover funciona sobre el papel y luego colapsa bajo carga

Solution

Failover monitorizado por health checks (Python)

# failover/health_monitored.py — Failover active-passive con health checks
import time
import threading
import requests
from enum import Enum

class NodeStatus(Enum):
    HEALTHY = "healthy"
    UNHEALTHY = "unhealthy"
    UNKNOWN = "unknown"

class FailoverManager:
    """Monitoriza los nodos primario y standby.
    Conmuta automáticamente cuando el primario deja de estar sano."""

    def __init__(self, primary_url, standby_url, health_path="/health",
                 check_interval=5, failure_threshold=3, recovery_threshold=3):
        self.primary_url = primary_url
        self.standby_url = standby_url
        self.health_path = health_path
        self.check_interval = check_interval
        self.failure_threshold = failure_threshold
        self.recovery_threshold = recovery_threshold

        self._active_url = primary_url
        self._primary_failures = 0
        self._primary_successes = 0
        self._standby_failures = 0
        self._is_failover = False
        self._lock = threading.Lock()
        self._running = True

    @property
    def active_url(self):
        with self._lock:
            return self._active_url

    @property
    def is_failover(self):
        with self._lock:
            return self._is_failover

    def _check_health(self, url):
        """Comprueba si un nodo está sano."""
        try:
            resp = requests.get(f"{url}{self.health_path}", timeout=3)
            if resp.status_code == 200:
                return NodeStatus.HEALTHY
            return NodeStatus.UNHEALTHY
        except Exception:
            return NodeStatus.UNHEALTHY

    def _monitor_loop(self):
        """Monitoriza el primario en bucle y conmuta si es necesario."""
        while self._running:
            primary_status = self._check_health(self.primary_url)

            with self._lock:
                if not self._is_failover:
                    # Monitorizando el primario
                    if primary_status == NodeStatus.HEALTHY:
                        self._primary_failures = 0
                    else:
                        self._primary_failures += 1
                        if self._primary_failures >= self.failure_threshold:
                            print(f"Primary failed {self._primary_failures} times, "
                                  f"failing over to standby")
                            self._initiate_failover()
                else:
                    # En modo failover — comprueba si el primario se recuperó
                    if primary_status == NodeStatus.HEALTHY:
                        self._primary_successes += 1
                        if self._primary_successes >= self.recovery_threshold:
                            print(f"Primary recovered {self._primary_successes} times, "
                                  f"failing back")
                            self._initiate_failback()
                    else:
                        self._primary_successes = 0

            time.sleep(self.check_interval)

    def _initiate_failover(self):
        """Desvía el tráfico del primario al standby."""
        standby_status = self._check_health(self.standby_url)
        if standby_status == NodeStatus.HEALTHY:
            self._active_url = self.standby_url
            self._is_failover = True
            self._primary_successes = 0
            print(f"Failover complete: now serving from {self.standby_url}")
        else:
            print(f"CRITICAL: Standby is also unhealthy! Cannot fail over.")

    def _initiate_failback(self):
        """Devuelve el tráfico al primario."""
        self._active_url = self.primary_url
        self._is_failover = False
        self._primary_failures = 0
        print(f"Failback complete: now serving from {self.primary_url}")

    def start(self):
        """Arranca el hilo de monitorización."""
        t = threading.Thread(target=self._monitor_loop, daemon=True)
        t.start()

    def stop(self):
        self._running = False

    def request(self, method, path, **kwargs):
        """Hace un request al nodo actualmente activo."""
        url = f"{self.active_url}{path}"
        return requests.request(method, url, **kwargs)

    def get_status(self):
        with self._lock:
            return {
                "active_url": self._active_url,
                "is_failover": self._is_failover,
                "primary_failures": self._primary_failures,
                "primary_successes": self._primary_successes,
            }


# Uso
failover = FailoverManager(
    primary_url="https://api-primary.example.com",
    standby_url="https://api-standby.example.com",
    health_path="/health",
    check_interval=5,
    failure_threshold=3,
    recovery_threshold=3
)
failover.start()

# Todos los requests van al nodo activo (primario o standby)
response = failover.request("GET", "/api/products")

El detalle importante está en la lógica de umbrales: un solo check fallido no dispara la conmutación. Exigir tres fallos consecutivos antes de actuar, y tres éxitos consecutivos antes de volver, es lo que separa un sistema de failover de uno que oscila sin parar.

Failover de base de datos con PostgreSQL (Python)

# failover/database.py — Failover de PostgreSQL con cambio de conexión
import psycopg2
import time
import threading

class DatabaseFailover:
    """Gestiona conexiones a base de datos con failover automático.
    Primario: read-write. Standby: read-only, promovido en el failover."""

    def __init__(self, primary_config, standby_configs):
        self.primary_config = primary_config
        self.standby_configs = standby_configs
        self._active_config = primary_config
        self._is_failover = False
        self._lock = threading.Lock()
        self._connection = None

    def _create_connection(self, config):
        return psycopg2.connect(
            host=config["host"],
            port=config.get("port", 5432),
            database=config["database"],
            user=config["user"],
            password=config["password"],
            connect_timeout=5
        )

    def get_connection(self):
        """Devuelve una conexión a la base de datos activa."""
        with self._lock:
            if self._connection and not self._connection.closed:
                try:
                    # Prueba la conexión
                    self._connection.cursor().execute("SELECT 1")
                    return self._connection
                except Exception:
                    self._connection = None

            # Prueba la config activa
            try:
                self._connection = self._create_connection(self._active_config)
                return self._connection
            except Exception as e:
                print(f"Active DB unavailable: {e}")
                self._initiate_failover()
                self._connection = self._create_connection(self._active_config)
                return self._connection

    def _initiate_failover(self):
        """Prueba cada standby en orden."""
        for i, standby in enumerate(self.standby_configs):
            try:
                conn = self._create_connection(standby)
                conn.close()
                self._active_config = standby
                self._is_failover = True
                print(f"Failed over to standby {i}: {standby['host']}")
                return
            except Exception:
                continue

        raise Exception("All databases are unavailable")

    def execute(self, query, params=None):
        """Ejecuta una query en la base de datos activa."""
        conn = self.get_connection()
        cur = conn.cursor()
        cur.execute(query, params)
        result = cur.fetchall()
        conn.commit()
        return result

    @property
    def is_failover(self):
        with self._lock:
            return self._is_failover


# Uso
db = DatabaseFailover(
    primary_config={"host": "db-primary.internal", "database": "shop",
                    "user": "app", "password": "secret"},
    standby_configs=[
        {"host": "db-replica-1.internal", "database": "shop",
         "user": "app", "password": "secret"},
        {"host": "db-replica-2.internal", "database": "shop",
         "user": "app", "password": "secret"},
    ]
)

# Conmuta automáticamente si el primario está caído
users = db.execute("SELECT * FROM users LIMIT 10")

En producción no harías esto a mano. PostgreSQL tiene pg_promote(), repmgr, o failover gestionado en RDS/Cloud SQL, y la mayoría de los drivers aceptan una lista de hosts (target_session_attrs=read-write en libpq). El ejemplo existe para mostrar la semántica: sondea antes de confiar, prueba los standby en orden y falla ruidosamente cuando no queda ninguno.

Failover basado en DNS (Python)

# failover/dns.py — Failover DNS para multi-región
import dns.resolver
import time

class DNSFailover:
    """Failover DNS: actualiza los registros DNS para apuntar al standby.
    Propagación más lenta, pero funciona entre regiones."""

    def __init__(self, domain, primary_ip, standby_ip,
                 dns_server, ttl=60):
        self.domain = domain
        self.primary_ip = primary_ip
        self.standby_ip = standby_ip
        self.dns_server = dns_server
        self.ttl = ttl
        self._active_ip = primary_ip

    def check_and_failover(self, health_url):
        """Comprueba la salud del primario y actualiza el DNS si hace falta."""
        import requests
        try:
            resp = requests.get(health_url, timeout=5)
            if resp.status_code == 200:
                if self._active_ip != self.primary_ip:
                    self._update_dns(self.primary_ip)
                    self._active_ip = self.primary_ip
                    print(f"DNS failback to primary: {self.primary_ip}")
                return True
        except Exception:
            pass

        # El primario está caído — conmutar
        if self._active_ip == self.primary_ip:
            self._update_dns(self.standby_ip)
            self._active_ip = self.standby_ip
            print(f"DNS failover to standby: {self.standby_ip}")

        return False

    def _update_dns(self, ip):
        """Actualiza el registro A de DNS (depende del proveedor de DNS)."""
        # Ejemplo: llamada a la API de AWS Route 53
        # change_route53_record(self.domain, ip, self.ttl)
        print(f"Updating DNS: {self.domain} -> {ip} (TTL: {self.ttl}s)")

    def resolve_current(self):
        """Comprueba a qué IP resuelve actualmente el dominio."""
        resolver = dns.resolver.Resolver()
        answers = resolver.resolve(self.domain, "A")
        return [rdata.address for rdata in answers]

El failover DNS es la única opción que sobrevive a la caída de una región entera, porque opera por encima de la infraestructura que falló. La contrapartida es la propagación: los resolvers cachean tu registro durante el TTL, y algunos ISPs cachean más de lo que deberían. Con un TTL de 60 segundos, la mayoría de los clientes se mueven en un par de minutos, no en los segundos que te da un load balancer.

Failover de upstream en Nginx

# failover/nginx.conf — Upstream de Nginx con failover pasivo
upstream api_backend {
    # Servidor primario — recibe todo el tráfico mientras esté sano
    server api-primary:8080 max_fails=3 fail_timeout=30s;

    # Servidor standby — recibe tráfico cuando el primario falla
    server api-standby:8080 backup max_fails=3 fail_timeout=30s;

    # Ajustes de health check
    keepalive 32;
    keepalive_timeout 60s;
}

server {
    listen 80;
    server_name api.example.com;

    # Health checks activos (requiere nginx plus)
    # health_check interval=5s fails=3 passes=2 uri=/health;

    location / {
        proxy_pass http://api_backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_next_upstream error timeout http_502 http_503 http_504;
        proxy_connect_timeout 5s;
        proxy_read_timeout 30s;
    }

    # Endpoint de salud para monitorización externa
    location /health {
        access_log off;
        return 200 "healthy\n";
        add_header Content-Type text/plain;
    }
}

La directiva backup es lo que convierte al segundo servidor en standby y no en un par balanceado: Nginx solo le envía tráfico cuando el primario supera max_fails dentro del fail_timeout. El Nginx open source solo hace checks pasivos — aprende que un servidor está muerto cuando fallan requests reales, así que los primeros usuarios se comen los errores. Nginx Plus, HAProxy, Envoy y los load balancers cloud hacen sondeo activo y detectan el fallo antes que los usuarios.

Failover multi-clúster en Kubernetes

# failover/k8s-failover.yaml — Servicio de Kubernetes con failover
apiVersion: v1
kind: Service
metadata:
  name: api-service
  annotations:
    # Anotación de external-dns para failover por DNS
    external-dns.alpha.kubernetes.io/hostname: api.example.com
spec:
  type: LoadBalancer
  selector:
    app: api-server
  ports:
    - port: 80
      targetPort: 8080
---
# PodDisruptionBudget garantiza disponibilidad mínima
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: api-pdb
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: api-server
---
# Failover multi-clúster con Global Load Balancer (ej.: AWS Global Accelerator)
# Clúster primario: us-east-1
# Clúster standby: eu-west-1
# El tráfico va al primario; si falla, se desvía al standby

Dentro de un mismo clúster, kubelet ya reinicia los pods muertos y el Service mantiene los endpoints al día, que es failover que te viene gratis. El problema difícil al que apunta esta configuración es el failover entre clústeres: dos clústeres en regiones distintas detrás de un load balancer global anycast (AWS Global Accelerator, Cloudflare Load Balancing, Azure Front Door) que hace health checks a ambos y desvía el tráfico cuando uno se apaga.

Cliente de failover en JavaScript

// failover/client.js — Failover del lado del cliente para llamadas a API
class FailoverClient {
    constructor(endpoints, options = {}) {
        this.endpoints = endpoints;  // ["https://api1.com", "https://api2.com"]
        this.activeIndex = 0;
        this.healthPath = options.healthPath || "/health";
        this.checkInterval = options.checkInterval || 10000;
        this.failureThreshold = options.failureThreshold || 3;
        this.failures = 0;
        this.isChecking = false;
    }

    get activeEndpoint() {
        return this.endpoints[this.activeIndex];
    }

    async checkHealth() {
        try {
            const response = await fetch(
                `${this.activeEndpoint}${this.healthPath}`,
                { signal: AbortSignal.timeout(3000) }
            );
            if (response.ok) {
                this.failures = 0;
                return true;
            }
        } catch (error) {
            // El health check falló
        }

        this.failures++;
        if (this.failures >= this.failureThreshold) {
            this.failover();
        }
        return false;
    }

    failover() {
        const nextIndex = (this.activeIndex + 1) % this.endpoints.length;
        if (nextIndex !== this.activeIndex) {
            console.log(`Failing over from ${this.endpoints[this.activeIndex]} ` +
                        `to ${this.endpoints[nextIndex]}`);
            this.activeIndex = nextIndex;
            this.failures = 0;
        }
    }

    async request(method, path, options = {}) {
        const url = `${this.activeEndpoint}${path}`;
        try {
            const response = await fetch(url, {
                method,
                ...options,
                signal: AbortSignal.timeout(10000)
            });
            if (response.status >= 500) {
                this.failures++;
                if (this.failures >= this.failureThreshold) {
                    this.failover();
                    // Reintenta en el nuevo endpoint
                    return this.request(method, path, options);
                }
                throw new Error(`HTTP ${response.status}`);
            }
            this.failures = 0;
            return response;
        } catch (error) {
            this.failures++;
            if (this.failures >= this.failureThreshold) {
                this.failover();
                return this.request(method, path, options);
            }
            throw error;
        }
    }

    startHealthChecks() {
        this.isChecking = true;
        const check = async () => {
            if (!this.isChecking) return;
            await this.checkHealth();
            setTimeout(check, this.checkInterval);
        };
        check();
    }

    stopHealthChecks() {
        this.isChecking = false;
    }
}

// Uso
const client = new FailoverClient(
    ["https://api-primary.example.com", "https://api-standby.example.com"],
    { failureThreshold: 3, checkInterval: 10000 }
);
client.startHealthChecks();

const response = await client.request("GET", "/api/products");
const data = await response.json();

El failover del lado del cliente es el último recurso: funciona cuando nada entre tú y la API puede hacerlo por ti, pero cada cliente mantiene su propia visión de qué endpoint está vivo, así que una flota de clientes puede discrepar durante una caída parcial. Úsalo para un número pequeño de endpoints conocidos, no como estrategia general de balanceo. Combínalo con Retry with Jitter para que los reintentos no martilleen un nodo que aún se está recuperando.

How It Works

Toda implementación de failover, en cualquier capa, ejecuta el mismo bucle de cuatro pasos:

flowchart diagram: Sondear primario en intervalo

1. Detectar. Un health check golpea un endpoint como /health cada pocos segundos. Un buen endpoint de salud comprueba dependencias reales (conectividad a la base de datos, disco, lag de la cola), no solo “¿el proceso sigue vivo?”. Un nodo que devuelve 200 mientras su base de datos es inalcanzable retiene el tráfico y falla cada request.

2. Decidir. Un fallo es ruido; varios seguidos son señal. El umbral de fallos es el mando de ajuste principal: demasiado bajo y oscilas por un paquete perdido, demasiado alto y los usuarios esperan mientras cuentas. Tres fallos consecutivos con un intervalo de 5 segundos — unos 15 segundos para detectar, es un default razonable.

3. Conmutar. Redirigir el tráfico al standby. Qué significa “redirigir” depende de la capa, y la capa determina lo rápido que los usuarios perciben el cambio:

CapaMecanismoTiempo típico de conmutaciónPrincipal trade-off
Cliente de aplicaciónReintentar contra una lista de endpoints de respaldoSub-segundoCada cliente mantiene su propio estado
Load balancerRetirar el upstream no saludable (backup, health checks)SegundosSolo cubre lo que hay detrás de ese LB
DNSActualizar el registro A a la IP del standby1–5 min (ligado al TTL)El más lento, pero sobrevive a caídas regionales
Base de datosPromover la réplica a primario escribible30–120 sHay que verificar el lag de replicación antes

4. Recuperar (failback). Cuando el primario vuelve, el checker necesita verlo sano varias veces antes de volver a confiar, porque un nodo recién reiniciado suele pasar un check y morir bajo carga real. Algunos equipos se saltan el failback automático por completo: conmutan una vez, investigan el primario y vuelven manualmente en una ventana de poco tráfico. Es una decisión defendible, no pereza.

Los modos de fallo que quitan el sueño:

  • Split-brain: una partición de red hace que ambos nodos crean ser el primario y los dos aceptan escrituras. Lo previenen el fencing (STONITH), un quórum o un sistema de elección de líder como etcd.
  • Oscilación (flap): umbrales demasiado sensibles hacen que el sistema oscile primario → standby → primario cada pocos segundos. La histéresis (umbrales distintos para conmutar y para volver) lo corrige.
  • Standby frío: el standby se promueve limpiamente pero no tiene caché caliente ni conexiones abiertas, y se cae bajo la carga repentina. Mándale un 1–5% del tráfico de forma permanente para que se mantenga caliente.
  • Ventana de pérdida de datos: con replicación asíncrona, los últimos segundos de escrituras del primario muerto nunca llegaron al standby. Ese hueco es tu RPO real, y ningún ajuste de health checks lo reduce. Solo la replicación síncrona lo hace, a costa de latencia.

Variants

Active-active con load balancing

# failover/active_active.py — Ambos nodos sirven tráfico a la vez
import random
import requests

class ActiveActiveManager:
    """Tanto el primario como el standby sirven tráfico.
    Si uno falla, el otro absorbe toda la carga."""

    def __init__(self, endpoints, health_path="/health"):
        self.endpoints = {url: {"healthy": True, "failures": 0}
                          for url in endpoints}
        self.health_path = health_path

    def get_healthy_endpoints(self):
        return [url for url, info in self.endpoints.items()
                if info["healthy"]]

    def request(self, method, path, **kwargs):
        healthy = self.get_healthy_endpoints()
        if not healthy:
            raise Exception("All endpoints are unhealthy")

        # Balanceo aleatorio entre los endpoints sanos
        url = random.choice(healthy)
        try:
            resp = requests.request(method, f"{url}{path}", timeout=10, **kwargs)
            return resp
        except Exception:
            self.endpoints[url]["failures"] += 1
            if self.endpoints[url]["failures"] >= 3:
                self.endpoints[url]["healthy"] = False
                print(f"Marked {url} as unhealthy")
            # Reintenta en otro endpoint sano
            return self.request(method, path, **kwargs)

Active-active elimina el coste del standby ocioso (cada nodo que pagas hace trabajo real), y un “failover” es simplemente un nodo que sale de la rotación. El precio es el dimensionado: cada nodo debe poder con el 100% del tráfico solo, así que un par active-active de dos nodos realmente corre al 40% de utilización en estado estable. Para servicios con estado, active-active además te obliga a resolver conflictos de escritura múltiple, que es por lo que es común en APIs stateless y raro en bases de datos.

Failover en cascada (multi-nivel)

# failover/cascading.py — Failover multi-nivel: primario -> standby -> terciario
import requests

class CascadingFailover:
    """Prueba los endpoints en orden: primario, luego standby, luego terciario.
    Cada nivel se prueba solo si el anterior falla."""

    def __init__(self, tiers):
        """tiers: [{"name": "primary", "url": "...", "timeout": 5}, ...]"""
        self.tiers = tiers
        self._active_tier = 0

    @property
    def active_url(self):
        return self.tiers[self._active_tier]["url"]

    def request(self, method, path, **kwargs):
        for i, tier in enumerate(self.tiers):
            try:
                url = f"{tier['url']}{path}"
                resp = requests.request(method, url, timeout=tier["timeout"], **kwargs)
                if resp.status_code < 500:
                    if i != self._active_tier:
                        print(f"Failover: tier {self._active_tier} -> {i} ({tier['name']})")
                        self._active_tier = i
                    return resp
            except Exception:
                continue

        raise Exception("All tiers exhausted")

El failover en cascada añade un tercer nivel, útil cuando el propio standby puede estar degradado, o cuando quieres una opción barata de último recurso (una página estática de fallback, una réplica de solo lectura, un modo de funcionalidad reducida) en vez de un error total. Cada nivel extra multiplica los estados que tienes que probar, así que quédate en tres.

Best Practices

  • Automatiza la detección de punta a punta. Un failover que requiere que alguien note una alerta en un dashboard y pulse un botón es un reinicio manual con pasos extra.
  • Exige varios fallos consecutivos antes de conmutar. Tres fallos a intervalo de 5 segundos detectan una caída real en ~15 segundos sin oscilar por un paquete perdido.
  • Prueba el failover regularmente. Corre game days donde matas el primario de verdad, con tráfico realista, no en un staging vacío.
  • Alerta de cada evento de failover. Una conmutación significa que algo se rompió; el silencio es cómo un standby degradado corre durante semanas hasta que se convierte en primario y también muere.
  • Mantén el standby caliente: mándale una pequeña parte del tráfico para que cachés, conexiones y sesiones TLS ya estén establecidas. Un standby frío es un failover que funciona sobre el papel.
  • Usa TTL de DNS cortos (60 segundos o menos) en cualquier registro que esperes mover durante un incidente.
  • Verifica el lag de replicación antes de promover un standby de base de datos; promover una réplica con 30 segundos de retraso descarta silenciosamente 30 segundos de escrituras.
  • Planifica el failback antes de necesitarlo, incluyendo quién lo aprueba y en qué ventana de tráfico se ejecuta. Para reinicios limpios del nodo que vuelve, ver Graceful Shutdown: Drain In-Flight Requests Before Exit.

Common Mistakes

  • Health check que solo comprueba “el proceso vive”: el nodo devuelve 200 mientras su conexión a la base de datos está muerta, así que el tráfico sigue llegando a un zombi. Comprueba dependencias reales.
  • Umbral de fallos de uno: un solo paquete perdido dispara la conmutación, el sistema oscila y acabas con una caída peor que la que intentabas prevenir.
  • Standby que nunca se ejercita: conmuta por primera vez en meses durante un incidente real, y resulta estar mal configurado. Promuévelo deliberadamente cada pocas semanas.
  • Sin plan de failback: el standby “temporal” se convierte en primario de facto, sin documentar ni monitorizar.
  • Escrituras en split-brain: la promoción automática sin fencing deja que un primario aislado pero vivo siga aceptando escrituras junto al nuevo. Si no puedes hacer fencing, prefiere la promoción manual.
  • Olvidar la capa de dependencias: la app conmuta limpiamente pero su Redis, su cola o su almacenamiento de archivos sigue apuntando a la región muerta. El failover tiene que cubrir todo el camino del request, no solo la puerta de entrada.

See Also

Código complementario: ejemplos de failover: versiones ejecutables de los snippets de esta página.

Preguntas frecuentes

¿Qué es el patrón failover?

Un patrón de resiliencia que desvía el tráfico de un sistema primario caído a un standby sano. Un health checker detecta el fallo y el tráfico se redirige en la capa de DNS, load balancer, base de datos o aplicación, para que los usuarios vean segundos de disrupción en vez de una caída.

¿Qué es split-brain en failover?

El split-brain ocurre cuando una partición de red hace que tanto el primario como el standby crean ser el nodo activo. Ambos aceptan escrituras y las copias divergen. Hacer fencing del primario viejo (forzarlo fuera de línea), exigir un quórum o elegir el líder mediante un sistema de consenso como etcd lo previene.

¿Qué tan rápido debería ser el failover?

Depende de la capa. El failover a nivel de aplicación y de load balancer suele completarse en segundos. La promoción de base de datos tarda 30–120 segundos porque la réplica tiene que ponerse al día primero. El failover DNS está limitado por el TTL del registro — espera 1–5 minutos en la práctica, ya que algunos resolvers cachean más allá de tu TTL configurado.

¿Cuál es la diferencia entre active-passive y active-active?

En active-passive, un nodo sirve todo el tráfico y el standby espera inactivo hasta ser promovido. En active-active, ambos nodos sirven tráfico y un fallo simplemente saca a uno de la rotación. Active-active aprovecha la capacidad, pero exige que cada nodo aguante solo toda la carga, y en sistemas con estado introduce problemas de conflicto de escritura múltiple.

¿Es "fallover" lo mismo que failover?

Funcionalmente sí: "fallover" es una variante ortográfica ocasional que aparece en documentación de vendors antigua. El término estándar de la industria es "failover", y es el que usan AWS, Azure, Kubernetes y la documentación de bases de datos.

¿Cómo pruebo el failover?

Corre game days: mata el primario deliberadamente y observa qué pasa. Verifica que los clientes reintentan bien, que el standby aguanta toda la carga, que los datos están intactos y que el failback funciona. Hazlo con tráfico realista, ya que un sistema vacío esconde los problemas de standby frío y de estampida que duelen en producción.