↓ Ir al contenido

Recuperación ante fallos de Consul en Docker Swarm: reconstrucción de quórum sin downtime

Rogelio Guerra Riverón
Autor
Rogelio Guerra Riverón
Construyendo mi propia infraestructura web desde cero. Aquí documento cada paso: servidores, redes, contenedores y lo que vaya surgiendo.

El problema
#

Hace tres semanas perdí dos de tres nodos Consul en producción por corrupción de datos. El tercero seguía en pie, pero sin quórum el servicio de descubrimiento colapsó. Swarm siguió funcionando, pero sin service discovery todo se fue al infierno.

Este artículo documenta cómo lo arreglé sin parar ni un segundo de servicio.

Entender el estado actual
#

Lo primero es saber dónde estamos:

docker exec consul-leader consul members

Con dos nodos caídos, obtendrás algo así:

Node          Address              Status  Type    Build   Protocol  DC
consul-node1  10.0.9.x:8301       alive   server  1.15.2  2         dc1
consul-node2  10.0.9.y:8301       failed  server  1.15.2  2         dc1
consul-node3  10.0.9.z:8301       failed  server  1.15.2  2         dc1

Sin quórum (necesitas mayoría: 2 de 3), Consul rechazará escrituras. Verificalo:

docker exec consul-leader consul kv put test/key "valor"

Si te dice que no hay líder elegido, confirmas que estás sin quórum.

Paso 1: Backup del nodo superviviente
#

Antes de tocar nada:

docker exec consul-leader consul snapshot save /tmp/backup.snap
docker cp consul-leader:/tmp/backup.snap ./consul-backup-$(date +%s).snap

Guarda esto en tu máquina host. Es tu única línea de defensa si algo sale mal.

Paso 2: Forzar nuevas elecciones (the nuclear option)
#

Si los nodos caídos no van a volver, necesitas decirle a Consul que olvide esos peers. Esto es irreversible:

docker exec consul-leader consul operator raft list-peers

Verás los peers caídos. Para removerlos:

docker exec consul-leader consul operator raft remove-peer -id "ID_DEL_NODO_CAIDO"

Repite para cada nodo muerto. Después:

docker exec consul-leader consul operator raft list-peers

Deberías ver solo el nodo vivo.

Paso 3: Reconstruir los nodos perdidos
#

Aquí es donde se pone interesante. No reutilices los volúmenes antiguos. Crea volúmenes nuevos:

docker volume rm consul-node2-data consul-node3-data
docker volume create consul-node2-data
docker volume create consul-node3-data

Redeploy en Swarm con la configuración correcta:

version: '3.8'
services:
  consul:
    image: consul:1.15.2
    command: agent -server -ui -node=consul-node2 -bootstrap-expect=3 
             -client=0.0.0.0 -bind=0.0.0.0
             -retry-join=consul-node1
    volumes:
      - consul-node2-data:/consul/data
    environment:
      - CONSUL_BIND_INTERFACE=eth0
    networks:
      - consul-net
    deploy:
      placement:
        constraints:
          - node.hostname == node2

volumes:
  consul-node2-data:
    driver: local

Clave: bootstrap-expect=3 sigue siendo 3, pero ahora Consul auto-descubre los peers vía retry-join. Los nodos nuevos se unirán automáticamente.

Paso 4: Validar sin downtime
#

Mientras los nodos se sincronizan, prueba que el quórum funciona:

docker exec consul-leader consul members
# Deberías ver los nuevos nodos en estado "alive"

docker exec consul-leader consul kv put test/validation "ok"
docker exec consul-leader consul kv get test/validation

Si esto funciona, tienes quórum de nuevo.

Paso 5: Migración de datos
#

Los datos clave (servicios registrados, KV) deberían replicarse automáticamente. Verifica:

docker exec consul-leader consul catalog services

¿Tus servicios están ahí? Si no, necesitas re-registrarlos. En mi caso usaba Consul DNS y el failover fue transparente porque los clientes ya tenían cached.

Lecciones aprendidas
#

  1. Nunca reutilices volúmenes Consul corrompidos. Mejor perder datos que propagar corrupción.
  2. El backup que hice me salvó de tener que investigar qué estaba registrado.
  3. bootstrap-expect debe ser consistente en todos los nodos, incluso si algunos están down.
  4. Los clientes Docker conectados a Consul en modo DNS followeaban automáticamente. El downtime percibido fue cero.

Esto me tomó 2 horas start-to-finish. Sin este proceso hubiera sido un desastre de 8 horas.