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: trueProceso de failover#
Cuando ejecuto docker-compose up, Patroni:
- Inicializa PostgreSQL en ambos nodos
- Designa al primero como primario
- El standby se conecta y comienza a replicar
- 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-primaryVerifico el estado en Etcd:
curl http://localhost:2379/v3/kv/range \
-X POST \
-H "Content-Type: application/json" \
-d '{"key":"cG9zdGdyZXMtY2x1c3Rlcg=="}' | jqEl 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.