Por qué alta disponibilidad de PostgreSQL en VPS — y no RDS
Amazon RDS Multi-AZ resuelve bien el failover, pero su modelo tarifario está diseñado para que la factura siga el crecimiento de forma no lineal: el almacenamiento, las IOPS aprovisionadas, las conexiones simultáneas y la propia replicación Multi-AZ se facturan por separado. Para un SaaS cuya base de datos crece, el costo adicional supera rápidamente el de un VPS dedicado.
En tres VPS con acceso root, Patroni presta el mismo servicio: un único líder acepta las escrituras, dos réplicas síncronas o asíncronas lo siguen, y etcd mantiene el quórum. Si el líder cae, Patroni detecta la ausencia mediante el lease (concesión) de etcd (TTL configurable, típicamente 30 segundos) y promueve la réplica más actualizada. Sin intervención humana y sin ningún DNS que actualizar manualmente si un balanceador de carga como HAProxy apunta al endpoint de salud de Patroni.
Lo que este clúster le aporta
- Failover automático en menos de 30 segundos — Patroni detecta la pérdida del líder por expiración del lease de etcd y promueve sin intervención.
- Replicación síncrona configurable —
synchronous_mode: truegarantiza que no se pierda ninguna transacción confirmada si el líder falla. - API REST integrada —
GET /leader,GET /health,POST /switchover: el clúster se consulta y se pilota sin cliente PostgreSQL. - Costo fijo y previsible — tres VPS con recursos fijos, sin sorpresas de facturación ligadas al tráfico.
- Extensiones libres —
pg_hba.conf,postgresql.conf,pgvector,PostGIS: ninguna restricción impuesta por un servicio gestionado. - Copias de seguridad centralizadas — pgBackRest 2.59 se integra de forma nativa con Patroni para copias incrementales desde la réplica.
Arquitectura del clúster: tres nodos, un quórum
El clúster se apoya en tres capas:
etcd mantiene el registro de configuración distribuido (DCS). Es quien guarda el lease del líder. Si el líder Patroni no renueva ese lease dentro del plazo TTL, etcd lo libera y los standbys se presentan a la elección. Con tres nodos etcd (uno por VPS), el quórum tolera la pérdida de un nodo sin perder la disponibilidad.
Patroni se ejecuta en cada VPS junto a PostgreSQL. Se encarga de la inicialización del clúster, de la configuración de postgresql.conf y de pg_hba.conf, del seguimiento del lag de replicación y del failover. Expone una API REST en el puerto 8008.
PostgreSQL está gestionado por completo por Patroni: no edite postgresql.conf directamente, toda modificación pasa por patronictl edit-config para mantenerse sincronizada en los tres nodos.
El flujo de replicación: el líder recibe las escrituras en WAL, las réplicas se conectan mediante pg_basebackup en el primer arranque y después siguen el flujo WAL de forma continua. En modo síncrono, el líder espera la confirmación de al menos una réplica antes de devolver COMMIT al cliente.
Requisitos previos: recursos y red
Esta guía se ha escrito con Patroni 4.1.5, etcd 3.6.6 y PostgreSQL 17 sobre Debian 12.
Recursos mínimos recomendados por nodo
- 2 vCPU / 4 GB de RAM — suficiente para empezar; prevea 8 GB de RAM en cuanto la base de datos supere unos pocos GB de
shared_buffers. - SSD NVMe — la replicación WAL es sensible a la latencia de escritura; un disco magnético degrada el lag de replicación.
- Red privada entre los tres nodos — la comunicación etcd-etcd y Patroni-PostgreSQL no debe transitar por Internet.
- IPv4 dedicada — para el acceso de clientes externos y el
pg_hba.confde las réplicas. - NTP sincronizado (chrony) — desfase < 1 s — etcd rechaza el quórum si el reloj de un nodo se desvía más de un segundo. Es la trampa más frecuente en VPS.
Instalación: de cero al clúster operativo
Sincronizar el reloj en los tres nodos
En cada nodo, instale y active
chronyantes de cualquier otra operación:apt install -y chrony systemctl enable --now chronyd chronyc trackingCompruebe que
System time offsetsea inferior a 0,1 segundos. Un desfase superior a 1 segundo provoca timeouts de etcd y elecciones en bucle.Instalar etcd 3.6 en los tres nodos
Defina las variables de entorno propias de cada nodo (sustituya
NODE1_IP,NODE2_IP,NODE3_IPpor las IP privadas):ETCD_VER=v3.6.6 curl -L https://github.com/etcd-io/etcd/releases/download/${ETCD_VER}/etcd-${ETCD_VER}-linux-amd64.tar.gz \ | tar xz -C /usr/local/bin --strip-components=1 etcd-${ETCD_VER}-linux-amd64/etcd \ etcd-${ETCD_VER}-linux-amd64/etcdctlCree
/etc/etcd/etcd.conf.ymlen cada nodo (ejemplo parapg-node1):name: pg-node1 data-dir: /var/lib/etcd listen-peer-urls: http://NODE1_IP:2380 listen-client-urls: http://NODE1_IP:2379,http://127.0.0.1:2379 initial-advertise-peer-urls: http://NODE1_IP:2380 advertise-client-urls: http://NODE1_IP:2379 initial-cluster: pg-node1=http://NODE1_IP:2380,pg-node2=http://NODE2_IP:2380,pg-node3=http://NODE3_IP:2380 initial-cluster-token: pg-cluster-token initial-cluster-state: newCree la unidad systemd, active e inicie etcd en los tres nodos antes de pasar al paso siguiente.
Verificar el quórum de etcd
En cualquier nodo:
etcdctl --endpoints=http://NODE1_IP:2379,http://NODE2_IP:2379,http://NODE3_IP:2379 \ endpoint status --write-out=tableEspere a que las tres líneas muestren
IS LEADERpara una yfalsepara las otras dos, y a queERRORSesté vacío. Si un nodo no aparece, revise el firewall en los puertos 2379 y 2380.Instalar PostgreSQL y Patroni
En los tres nodos:
# PostgreSQL desde el repositorio oficial PGDG apt install -y curl ca-certificates curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor -o /etc/apt/trusted.gpg.d/postgresql.gpg echo "deb https://apt.postgresql.org/pub/repos/apt bookworm-pgdg main" > /etc/apt/sources.list.d/pgdg.list apt update && apt install -y postgresql-17 # Patroni y el driver de etcd pip3 install patroni[etcd3] psycopg2-binaryDetenga PostgreSQL: Patroni se encarga de la inicialización del clúster:
systemctl stop postgresql systemctl disable postgresqlConfigurar Patroni en cada nodo
Cree
/etc/patroni/patroni.yml(ejemplo parapg-node1):scope: pg-cluster namespace: /service/ name: pg-node1 restapi: listen: NODE1_IP:8008 connect_address: NODE1_IP:8008 etcd3: hosts: NODE1_IP:2379,NODE2_IP:2379,NODE3_IP:2379 bootstrap: dcs: ttl: 30 loop_wait: 10 retry_timeout: 10 maximum_lag_on_failover: 1048576 synchronous_mode: true synchronous_node_count: 1 postgresql: use_pg_rewind: true use_slots: true parameters: wal_level: replica hot_standby: "on" wal_keep_size: 128MB max_wal_senders: 10 max_replication_slots: 10 initdb: - encoding: UTF8 - data-checksums pg_hba: - host replication replicator 0.0.0.0/0 scram-sha-256 - host all all 0.0.0.0/0 scram-sha-256 postgresql: listen: NODE1_IP:5432 connect_address: NODE1_IP:5432 data_dir: /var/lib/postgresql/17/main bin_dir: /usr/lib/postgresql/17/bin authentication: replication: username: replicator password: 'SU_CONTRASENA_REPLICACION' superuser: username: postgres password: 'SU_CONTRASENA_POSTGRES'Adapte
NODE1_IPen cada nodo.Iniciar Patroni e inicializar el clúster
Cree la unidad systemd
/etc/systemd/system/patroni.service:[Unit] Description=Patroni PostgreSQL HA After=network.target etcd.service Requires=etcd.service [Service] Type=simple User=postgres ExecStart=/usr/local/bin/patroni /etc/patroni/patroni.yml Restart=on-failure RestartSec=5s [Install] WantedBy=multi-user.targetInicie primero en
pg-node1(ese nodo ejecutainitdby se convierte en líder), después en los otros dos con unos segundos de intervalo:systemctl daemon-reload systemctl enable --now patroniSiga la inicialización:
patronictl -c /etc/patroni/patroni.yml listVerificar el estado inicial del clúster
Salida esperada tras la inicialización completa:
+ Cluster: pg-cluster (7234567890123456789) +---------+----+-----------+ | Member | Host | Role | State | TL | Lag in MB | +-----------+------------------+---------+---------+----+-----------+ | pg-node1 | NODE1_IP:5432 | Leader | running | 1 | | | pg-node2 | NODE2_IP:5432 | Sync Standby | running | 1 | 0 | | pg-node3 | NODE3_IP:5432 | Replica | running | 1 | 0 | +-----------+------------------+---------+---------+----+-----------+pg-node2aparece comoSync Standby: toda transacción confirmada en el líder queda garantizada en ese nodo antes de que elCOMMITse devuelva al cliente.Configurar pg_hba.conf mediante Patroni
No modifique nunca
pg_hba.confdirectamente. Utilicepatronictl edit-configpara añadir reglas de acceso en la secciónpg_hba. Patroni propaga la configuración a todos los nodos y recarga PostgreSQL automáticamente:patronictl -c /etc/patroni/patroni.yml edit-configAñada sus reglas en el bloque
pg_hbadel YAML. En VPS, la reglahost all all 0.0.0.0/0 scram-sha-256es un punto de partida que conviene afinar según su red privada.
Failover y switchover: demostración
Failover simulado — parada brusca del líder.
Antes de la parada, anote el estado del clúster:
patronictl -c /etc/patroni/patroni.yml list
# → pg-node1 es Leader, pg-node2 es Sync StandbyDetenga Patroni en el líder:
systemctl stop patroni # en pg-node1Siga la promoción en uno de los standbys:
patronictl -c /etc/patroni/patroni.yml list
# → (tras 10 a 30 segundos)
# pg-node2 : Leader | running | TL 2
# pg-node3 : Replica | running | TL 2 | 0 MB
# pg-node1 : stoppedPatroni espera la expiración del lease de etcd (TTL = 30 s), y después pg-node2 (el sync standby) adquiere el lease y se promueve. El plazo efectivo suele situarse entre 10 y 30 segundos según el valor de loop_wait.
Switchover planificado — conmutación sin corte.
Para un mantenimiento programado, prefiera switchover, que espera a que la réplica de destino esté al día antes de conmutar:
patronictl -c /etc/patroni/patroni.yml switchover pg-cluster \
--master pg-node1 --candidate pg-node2Patroni espera a que el lag sea nulo, da la señal de promoción a pg-node2, y después pg-node1 se reconecta como réplica. Duración efectiva: menos de 5 segundos en condiciones normales.
API REST de estado.
Sin cliente PostgreSQL, consulte el estado desde un balanceador de carga o un script de monitorización:
curl -s http://NODE1_IP:8008/leader # 200 = es el líder
curl -s http://NODE2_IP:8008/replica # 200 = es una réplica sana
curl -s http://NODE1_IP:8008/health # JSON: state, role, lagHAProxy puede apuntar sus health checks a /leader y /replica para enrutar las escrituras y las lecturas a los nodos correctos. Consulte la guía HAProxy en VPS para el cableado completo.
Operaciones diarias
Copias de seguridad con pgBackRest 2.59.
Instale pgBackRest en los tres nodos y designe un repositorio compartido (objeto S3, NFS o directorio local dedicado). La configuración recomendada toma las copias desde una réplica para no cargar el líder:
pgbackrest --stanza=pg-cluster --type=full backupActive la compresión y las copias incrementales diarias en pgbackrest.conf (repo1-retention-full=7). Consulte la guía copias de seguridad en VPS para las estrategias complementarias.
Monitorización del clúster.
patronictl list indica el lag en MB por réplica. Genere una alerta en cuanto el lag supere un umbral (por ejemplo: 50 MB): eso señala o bien una réplica lenta, o bien un problema de red. El endpoint GET /patroni devuelve un JSON completo que incluye xlog_location y replication_state.
Escalado vertical.
Para aumentar los recursos de un nodo: detenga Patroni en ese nodo (pasa a réplica desconectada), redimensione el VPS y reinicie. Patroni se reconecta y recupera el lag automáticamente mediante pg_rewind o pg_basebackup según la amplitud del desfase.
Compromiso de synchronous_commit.
Con synchronous_mode: true, cada COMMIT espera la confirmación del sync standby. En una red privada local, cada COMMIT espera la confirmación del standby síncrono: el plazo depende de la latencia de red entre nodos (compruebe pg_stat_replication.replay_lag). En una red más extensa (nodos en centros de datos diferentes), esa latencia puede afectar a las aplicaciones con escrituras intensivas. En ese caso, pase a synchronous_mode: false + replicación asíncrona: pierde la garantía de cero pérdida de datos si el líder falla, pero las escrituras siguen siendo rápidas. Es un arbitraje que conviene documentar explícitamente en su configuración.
Endurecimiento: TLS con autenticación mutua entre nodos
Por defecto, la comunicación de etcd y las conexiones de replicación de PostgreSQL circulan en claro por la red privada. En una red compartida o en un entorno multi-tenant, active TLS con autenticación mutua.
Para etcd, genere una CA y certificados por nodo, y después añada en etcd.conf.yml:
client-transport-security:
cert-file: /etc/etcd/tls/server.crt
key-file: /etc/etcd/tls/server.key
trusted-ca-file: /etc/etcd/tls/ca.crt
client-cert-auth: true
peer-transport-security:
cert-file: /etc/etcd/tls/peer.crt
key-file: /etc/etcd/tls/peer.key
trusted-ca-file: /etc/etcd/tls/ca.crt
peer-client-cert-auth: truePara la replicación de PostgreSQL, utilice sslmode=verify-full en los parámetros de conexión primary_conninfo de Patroni. Así, cada réplica verifica el certificado del líder.
Solución de problemas: los cinco errores más frecuentes
1. Quórum de etcd perdido: el clúster se niega a elegir un líder.
Síntoma: patronictl list muestra todos los nodos en running pero ningún Leader. Causa: un nodo etcd es inaccesible y ya no se alcanza el quórum (2 de 3). Compruébelo con etcdctl endpoint status: el nodo defectuoso aparece sin respuesta o con un error de conexión. Corrija el nodo o retírelo provisionalmente del clúster (etcdctl member remove).
2. Desfase NTP: elecciones en bucle.
Síntoma: el líder cambia cada 30 segundos y los logs de Patroni muestran failed to update leader key. Causa: el reloj de un nodo se desvía más de un segundo. Compruébelo con chronyc tracking en cada nodo y corríjalo antes de reiniciar Patroni.
3. Split-brain potencial: pg_rewind se niega a aplicarse.
Síntoma: un antiguo líder se reinicia y Patroni se niega a reintegrarlo como réplica, con el error could not connect to the target server: pg_rewind target server must be in standby mode. El servidor siguió escribiendo tras la pérdida del lease. Solución: pg_rewind --target-pgdata=/var/lib/postgresql/17/main --source-server='host=NEW_LEADER_IP ...', y después reinicie Patroni.
4. Conexión peer rechazada: falta la entrada en pg_hba.conf.
Síntoma: la replicación se inicializa pero falla con FATAL: no pg_hba.conf entry for replication connection. La regla host replication replicator 0.0.0.0/0 scram-sha-256 no está presente en la sección pg_hba del patroni.yml. Añádala mediante patronictl edit-config, no directamente en pg_hba.conf.
5. Lag persistente tras el failover: max_wal_senders insuficiente.
Síntoma: la réplica muestra un lag que no disminuye tras la promoción. Causa frecuente: max_wal_senders es demasiado bajo (valor por defecto de 10 en algunas versiones) y el slot de replicación está saturado. Auméntelo a 20 mediante patronictl edit-config (parámetro max_wal_senders) y recargue.
Un clúster que se administra, no que se improvisa
Patroni 4.1 con etcd 3.6 cubre lo esencial de lo que aporta un servicio gestionado en materia de disponibilidad: elección automática, replicación síncrona, API de pilotaje. La diferencia está en el control: acceso root, extensiones libres, costo fijo y la posibilidad de depurar el nodo que flaquea en lugar de esperar a un soporte externo.
El requisito operativo no es la complejidad de Patroni — el procedimiento anterior lo demuestra. Es la disciplina en tres puntos: NTP sincronizado, copias de seguridad verificadas con regularidad y un runbook de failover probado antes de que llegue la avería.
Para empezar, un clúster Patroni de 3 nodos requiere tres VPS con acceso root, IPv4 dedicada y red privada. Consulte la guía de instalación básica PostgreSQL en VPS y la comparativa autoalojado frente a Amazon RDS para elegir el enfoque que corresponda a su carga.