↓ Ir al contenido

Recuperación ante fallos de PostgreSQL con Patroni en Docker

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
#

Tenía PostgreSQL corriendo en un contenedor Docker en mi servidor, pero sin redundancia. Si el servicio fallaba o la máquina se reiniciaba, perdía acceso a los datos. Necesitaba alta disponibilidad con un mecanismo automático de failover.

Patroni es la solución que estaba buscando. Es un template de Python que gestiona clusters PostgreSQL con replicación primaria-standby y failover automático usando etcd, Consul o Kubernetes como backend de estado distribuido.

Arquitectura que implementé
#

  • PostgreSQL primario: nodo principal que recibe escrituras
  • PostgreSQL standby: replica datos en tiempo real
  • Etcd: almacén de estado distribuido para coordinar el failover
  • Patroni: supervisor que monitorea salud y ejecuta el cambio automático

Todo en Docker Compose para facilitar el despliegue en mi servidor doméstico.

Docker Compose setup
#

version: '3.8'

services:
  etcd:
    image: quay.io/coreos/etcd:v3.5.5
    environment:
      - ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:2379
      - ETCD_ADVERTISE_CLIENT_URLS=http://etcd:2379
    ports:
      - "2379:2379"
    networks:
      - postgres-net

  patroni-primary:
    image: patroni:latest
    environment:
      - PATRONI_POSTGRESQL_DATA_DIR=/var/lib/postgresql/data
      - PATRONI_POSTGRESQL_PGPASS=/var/lib/postgresql/pgpass
      - PATRONI_POSTGRESQL_PARAMETERS_MAX_CONNECTIONS=200
      - PATRONI_ETCD3_HOSTS=etcd:2379
      - PATRONI_SCOPE=postgres-cluster
      - PATRONI_NODE_NAME=primary
      - PATRONI_REST_LISTEN=0.0.0.0:8008
    volumes:
      - pg-primary:/var/lib/postgresql/data
      - ./patroni.yml:/etc/patroni/patroni.yml
    depends_on:
      - etcd
    ports:
      - "5432:5432"
      - "8008:8008"
    networks:
      - postgres-net

  patroni-standby:
    image: patroni:latest
    environment:
      - PATRONI_POSTGRESQL_DATA_DIR=/var/lib/postgresql/data
      - PATRONI_ETCD3_HOSTS=etcd:2379
      - PATRONI_SCOPE=postgres-cluster
      - PATRONI_NODE_NAME=standby
      - PATRONI_REST_LISTEN=0.0.0.0:8008
    volumes:
      - pg-standby:/var/lib/postgresql/data
      - ./patroni.yml:/etc/patroni/patroni.yml
    depends_on:
      - etcd
    ports:
      - "5433:5432"
      - "8009:8008"
    networks:
      - postgres-net

networks:
  postgres-net:
    driver: bridge

volumes:
  pg-primary:
  pg-standby:

Configuración de Patroni
#

Archivo patroni.yml simplificado:

scope: postgres-cluster
name: primary
restapi:
  listen: 0.0.0.0:8008
  connect_address: patroni-primary:8008

etcd3:
  hosts:
    - etcd:2379

postgresql:
  data_dir: /var/lib/postgresql/data
  pgpass: /var/lib/postgresql/pgpass
  parameters:
    max_connections: 200
    shared_buffers: 256MB
    effective_cache_size: 1GB
    wal_level: replica
    max_wal_senders: 3
    max_replication_slots: 3

bootstrap:
  dcs:
    ttl: 30
    loop_wait: 10
    retry_timeout: 10
    maximum_lag_on_failover: 1048576
    master_start_timeout: 300
    synchronous_mode: false
    postgresql:
      use_pg_rewind: true
      use_slots: true

Proceso de failover
#

Cuando ejecuto docker-compose up, Patroni:

  1. Inicializa PostgreSQL en ambos nodos
  2. Designa al primero como primario
  3. El standby se conecta y comienza a replicar
  4. Etcd almacena el estado del cluster

Si el primario falla, Patroni detecta que no responde en 30 segundos (ttl) y promove automáticamente el standby. La aplicación se reconecta al nuevo primario.

Probando el failover
#

Para simular una caída:

docker-compose pause patroni-primary

Verifico el estado en Etcd:

curl http://localhost:2379/v3/kv/range \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"key":"cG9zdGdyZXMtY2x1c3Rlcg=="}' | jq

El standby se promueve en segundos. Las conexiones activas se desconectan, pero los clientes pueden reintentar automáticamente.

Lo que aprendí
#

  • Etcd es crítico: es el coordinador del cluster. Si cae, el failover automático no ocurre.
  • Rewind vs resync: con use_pg_rewind: true, el antiguo primario se reinicia más rápido.
  • Slots de replicación: previenen la pérdida de WAL si el standby se atrasa.
  • VIP no es opción en Docker: uso conexiones directas o un proxy TCP.

Siguiente paso
#

Estoy considerando agregar un proxy (PgBouncer) para manejar mejor las reconexiones automáticas de la aplicación durante failover.