--- title: "Alta disponibilidad de PostgreSQL con Patroni y etcd" description: "Monta un clúster PostgreSQL 16 de alta disponibilidad con Patroni, etcd y HAProxy en Ubuntu 24.04, con failover automático y un único punto de conexión." category: "Base de Datos Avanzada" language: es source: https://cubepath.com/es/docs/base-de-datos-avanzada/alta-disponibilidad-de-postgresql-con-patroni updated: 2026-09-25 --- # Alta disponibilidad de PostgreSQL con Patroni y etcd Patroni es un gestor de alta disponibilidad para PostgreSQL. Cada nodo ejecuta un agente de Patroni que arranca PostgreSQL, configura la replicación en streaming y compite por un bloqueo de líder guardado en un almacén distribuido (etcd). Si el primario cae, el bloqueo expira y Patroni promociona automáticamente a la réplica más actualizada, sin intervención manual. En este tutorial montarás en Ubuntu 24.04 un clúster de tres nodos con PostgreSQL 16, etcd y Patroni, todo desde los repositorios de Ubuntu, y pondrás HAProxy delante para que las aplicaciones siempre se conecten al primario actual. Al final provocarás un failover y un switchover para ver el clúster en acción. ## Requisitos previos - Tres servidores con Ubuntu 24.04 LTS para la base de datos, por ejemplo VPS de CubePath, con al menos 2 GB de RAM y un usuario no root con `sudo`. - Un cuarto servidor pequeño para HAProxy (también puede ir en el servidor de la aplicación). - Red privada entre todos ellos. En esta guía etcd y Patroni hablan por HTTP sin cifrar, así que no expongas sus puertos a Internet. Valores usados en los ejemplos: | Nodo | IP privada | Función | |---|---|---| | `pg1` | `10.0.0.11` | PostgreSQL + Patroni + etcd | | `pg2` | `10.0.0.12` | PostgreSQL + Patroni + etcd | | `pg3` | `10.0.0.13` | PostgreSQL + Patroni + etcd | | `haproxy` | `10.0.0.20` | HAProxy | Puertos necesarios en la red privada: | Puerto | Servicio | |---|---| | 2379/tcp | etcd, clientes | | 2380/tcp | etcd, comunicación entre miembros | | 5432/tcp | PostgreSQL | | 8008/tcp | API REST de Patroni | ## Paso 1: Instalar los paquetes en los nodos de base de datos Ejecuta en **pg1, pg2 y pg3**: ```bash sudo apt update sudo apt install postgresql-16 patroni etcd-server etcd-client ``` El paquete de PostgreSQL crea y arranca un clúster por defecto (`16/main`). Patroni necesita gestionar PostgreSQL él mismo y crear el directorio de datos desde cero, así que elimina ese clúster y desactiva el servicio de Ubuntu. En **cada nodo**: ```bash sudo pg_dropcluster 16 main --stop sudo systemctl disable --now postgresql ``` Comprueba que no queda ningún clúster: ```bash pg_lsclusters ``` ```output Ver Cluster Port Status Owner Data directory Log file ``` El paquete de etcd también arranca un miembro independiente. Páralo; lo reconfigurarás en el siguiente paso: ```bash sudo systemctl stop etcd ``` Abre los puertos para la red privada: ```bash sudo ufw allow from 10.0.0.0/24 to any port 2379,2380,5432,8008 proto tcp ``` ## Paso 2: Configurar el clúster etcd etcd guarda el estado del clúster y el bloqueo de líder. Con tres miembros tolera la caída de uno. En Ubuntu el servicio lee su configuración como variables de entorno desde `/etc/default/etcd`. En **pg1**, sustituye el contenido del fichero: ```bash sudo nano /etc/default/etcd ``` ```ini ETCD_NAME="pg1" ETCD_DATA_DIR="/var/lib/etcd/patroni" ETCD_LISTEN_PEER_URLS="http://10.0.0.11:2380" ETCD_LISTEN_CLIENT_URLS="http://10.0.0.11:2379,http://127.0.0.1:2379" ETCD_INITIAL_ADVERTISE_PEER_URLS="http://10.0.0.11:2380" ETCD_ADVERTISE_CLIENT_URLS="http://10.0.0.11:2379" ETCD_INITIAL_CLUSTER="pg1=http://10.0.0.11:2380,pg2=http://10.0.0.12:2380,pg3=http://10.0.0.13:2380" ETCD_INITIAL_CLUSTER_STATE="new" ETCD_INITIAL_CLUSTER_TOKEN="patroni-etcd" ``` Se usa un directorio de datos nuevo (`/var/lib/etcd/patroni`) para no reutilizar el del miembro independiente que arrancó el paquete, que pertenece a otro clúster. En **pg2** y **pg3** usa el mismo fichero cambiando `ETCD_NAME` y las IP de las cuatro líneas `LISTEN`/`ADVERTISE` por las del propio nodo. Por ejemplo, en pg2: ```ini ETCD_NAME="pg2" ETCD_DATA_DIR="/var/lib/etcd/patroni" ETCD_LISTEN_PEER_URLS="http://10.0.0.12:2380" ETCD_LISTEN_CLIENT_URLS="http://10.0.0.12:2379,http://127.0.0.1:2379" ETCD_INITIAL_ADVERTISE_PEER_URLS="http://10.0.0.12:2380" ETCD_ADVERTISE_CLIENT_URLS="http://10.0.0.12:2379" ETCD_INITIAL_CLUSTER="pg1=http://10.0.0.11:2380,pg2=http://10.0.0.12:2380,pg3=http://10.0.0.13:2380" ETCD_INITIAL_CLUSTER_STATE="new" ETCD_INITIAL_CLUSTER_TOKEN="patroni-etcd" ``` Arranca etcd en los **tres nodos** con poca diferencia de tiempo. El primero esperará a que haya quórum antes de responder: ```bash sudo systemctl enable --now etcd ``` Comprueba desde cualquier nodo que los tres miembros están sanos: ```bash etcdctl --endpoints=http://10.0.0.11:2379,http://10.0.0.12:2379,http://10.0.0.13:2379 endpoint health ``` ```output http://10.0.0.11:2379 is healthy: successfully committed proposal: took = 2.10ms http://10.0.0.12:2379 is healthy: successfully committed proposal: took = 2.43ms http://10.0.0.13:2379 is healthy: successfully committed proposal: took = 2.87ms ``` ```bash etcdctl --endpoints=http://127.0.0.1:2379 member list -w table ``` La tabla debe mostrar los tres miembros en estado `started`. ## Paso 3: Configurar Patroni El paquete de Ubuntu incluye un servicio `patroni.service` que lee `/etc/patroni/config.yml`. Crea ese fichero en **pg1**: ```bash sudo nano /etc/patroni/config.yml ``` ```yaml scope: pg-cluster namespace: /service/ name: pg1 restapi: listen: 10.0.0.11:8008 connect_address: 10.0.0.11:8008 etcd3: hosts: 10.0.0.11:2379,10.0.0.12:2379,10.0.0.13:2379 bootstrap: dcs: ttl: 30 loop_wait: 10 retry_timeout: 10 maximum_lag_on_failover: 1048576 postgresql: use_pg_rewind: true use_slots: true parameters: wal_level: replica hot_standby: "on" max_wal_senders: 10 max_replication_slots: 10 wal_log_hints: "on" initdb: - encoding: UTF8 - data-checksums postgresql: listen: 0.0.0.0:5432 connect_address: 10.0.0.11:5432 data_dir: /var/lib/postgresql/16/main bin_dir: /usr/lib/postgresql/16/bin pgpass: /var/lib/postgresql/.pgpass_patroni authentication: superuser: username: postgres password: your_superuser_password replication: username: replicator password: your_replication_password parameters: unix_socket_directories: /var/run/postgresql pg_hba: - local all all peer - host all all 127.0.0.1/32 scram-sha-256 - host replication replicator 10.0.0.0/24 scram-sha-256 - host all all 10.0.0.0/24 scram-sha-256 tags: nofailover: false noloadbalance: false clonefrom: false nosync: false ``` Las partes importantes: - `scope` es el nombre del clúster y debe ser igual en los tres nodos. `name` es único por nodo. - `etcd3` usa la API v3 de etcd, la única activa por defecto en etcd 3.4. - `bootstrap.dcs` se escribe en etcd solo al crear el clúster. `ttl: 30` es el tiempo que tarda en expirar el bloqueo del líder, y por tanto el tiempo máximo aproximado hasta un failover. `maximum_lag_on_failover` (en bytes) impide promocionar una réplica demasiado retrasada. - `use_pg_rewind` permite que un antiguo primario vuelva como réplica sin copiar todos los datos; necesita `wal_log_hints` o checksums, y aquí están activos ambos. - `authentication` define el superusuario y el usuario de replicación. Patroni crea `replicator` al inicializar el clúster. Sustituye ambas contraseñas por valores seguros. - `pg_hba` es el contenido completo de `pg_hba.conf`, que Patroni escribe en cada nodo. La línea de `127.0.0.1` es necesaria porque Patroni se conecta a su PostgreSQL local por TCP. En **pg2** y **pg3** usa el mismo fichero cambiando `name`, `restapi.listen`, `restapi.connect_address` y `postgresql.connect_address` a los valores del nodo. Por ejemplo, en pg2: ```yaml name: pg2 restapi: listen: 10.0.0.12:8008 connect_address: 10.0.0.12:8008 ``` ```yaml postgresql: listen: 0.0.0.0:5432 connect_address: 10.0.0.12:5432 ``` El fichero contiene contraseñas, así que solo debe poder leerlo el usuario `postgres`. En **cada nodo**: ```bash sudo chown postgres:postgres /etc/patroni/config.yml sudo chmod 600 /etc/patroni/config.yml ``` Valida la sintaxis antes de arrancar: ```bash sudo -u postgres patroni --validate-config /etc/patroni/config.yml ``` Si no muestra ningún error, la configuración es válida. ## Paso 4: Arrancar Patroni Arranca primero en **pg1**. Al no encontrar ningún clúster en etcd, tomará el bloqueo de líder y ejecutará `initdb`: ```bash sudo systemctl enable --now patroni ``` Sigue el arranque en el log: ```bash sudo journalctl -u patroni -f ``` Cuando veas `no action. I am (pg1), the leader with the lock`, pulsa `Ctrl+C` y arranca Patroni en **pg2** y **pg3**: ```bash sudo systemctl enable --now patroni ``` Estos nodos encontrarán un líder, clonarán sus datos con `pg_basebackup` y arrancarán como réplicas. Comprueba el estado del clúster desde cualquier nodo con `patronictl`: ```bash sudo -u postgres patronictl -c /etc/patroni/config.yml list ``` ```output + Cluster: pg-cluster (7419263518471032371) -+-----------+----+-----------+ | Member | Host | Role | State | TL | Lag in MB | +--------+-----------+---------+-----------+----+-----------+ | pg1 | 10.0.0.11 | Leader | running | 1 | | | pg2 | 10.0.0.12 | Replica | streaming | 1 | 0 | | pg3 | 10.0.0.13 | Replica | streaming | 1 | 0 | +--------+-----------+---------+-----------+----+-----------+ ``` Un líder `running`, dos réplicas `streaming` y lag 0 indican que la replicación funciona. ## Paso 5: Probar la replicación Crea una tabla en el primario (pg1): ```bash sudo -u postgres psql -c "CREATE TABLE ha_test (id serial PRIMARY KEY, creado timestamptz DEFAULT now());" sudo -u postgres psql -c "INSERT INTO ha_test DEFAULT VALUES;" ``` Léela en una réplica (pg2): ```bash sudo -u postgres psql -c "SELECT * FROM ha_test;" ``` ```output id | creado ----+------------------------------- 1 | 2026-09-25 10:42:11.52131+00 (1 row) ``` Las réplicas son de solo lectura. Un `INSERT` en pg2 falla con `cannot execute INSERT in a read-only transaction`. ## Paso 6: Poner HAProxy delante Las aplicaciones no deberían saber qué nodo es el primario. La API REST de Patroni responde en `/primary` con código 200 solo en el líder, y en `/replica` con 200 solo en réplicas sanas. HAProxy usa esas respuestas como health check para enviar cada conexión al nodo adecuado. En el servidor **haproxy**: ```bash sudo apt update sudo apt install haproxy ``` Añade al final de la configuración: ```bash sudo nano /etc/haproxy/haproxy.cfg ``` ```haproxy listen postgres_primary bind *:5000 mode tcp option httpchk GET /primary http-check expect status 200 default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions server pg1 10.0.0.11:5432 check port 8008 server pg2 10.0.0.12:5432 check port 8008 server pg3 10.0.0.13:5432 check port 8008 listen postgres_replicas bind *:5001 mode tcp balance roundrobin option httpchk GET /replica http-check expect status 200 default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions server pg1 10.0.0.11:5432 check port 8008 server pg2 10.0.0.12:5432 check port 8008 server pg3 10.0.0.13:5432 check port 8008 ``` - El puerto 5000 siempre lleva al primario (lectura y escritura). - El puerto 5001 reparte las conexiones entre las réplicas (solo lectura). - `on-marked-down shutdown-sessions` corta las conexiones abiertas contra un nodo que deja de ser primario, para que los clientes reconecten al nuevo. Valida y recarga: ```bash sudo haproxy -c -f /etc/haproxy/haproxy.cfg sudo systemctl reload haproxy ``` ```output Configuration file is valid ``` Permite el acceso a los puertos 5000 y 5001 solo desde la red de tus aplicaciones: ```bash sudo ufw allow from 10.0.0.0/24 to any port 5000,5001 proto tcp ``` Comprueba desde el servidor de HAProxy (instala el cliente con `sudo apt install postgresql-client`) que el puerto 5000 llega al primario y el 5001 a una réplica. `pg_is_in_recovery()` devuelve `f` en el primario y `t` en las réplicas: ```bash psql -h 10.0.0.20 -p 5000 -U postgres -c "SELECT inet_server_addr(), pg_is_in_recovery();" psql -h 10.0.0.20 -p 5001 -U postgres -c "SELECT inet_server_addr(), pg_is_in_recovery();" ``` ```output inet_server_addr | pg_is_in_recovery ------------------+------------------- 10.0.0.11 | f (1 row) inet_server_addr | pg_is_in_recovery ------------------+------------------- 10.0.0.12 | t (1 row) ``` Te pedirá la contraseña del superusuario. En producción, crea usuarios propios para las aplicaciones en lugar de usar `postgres`. ## Paso 7: Probar el failover automático Simula la caída del primario parando Patroni en **pg1**. Patroni detiene PostgreSQL de forma ordenada y libera el bloqueo: ```bash sudo systemctl stop patroni ``` Desde **pg2**, observa el clúster. En unos segundos otra réplica pasa a líder: ```bash sudo -u postgres patronictl -c /etc/patroni/config.yml list ``` ```output + Cluster: pg-cluster (7419263518471032371) -+-----------+----+-----------+ | Member | Host | Role | State | TL | Lag in MB | +--------+-----------+---------+-----------+----+-----------+ | pg2 | 10.0.0.12 | Leader | running | 2 | | | pg3 | 10.0.0.13 | Replica | streaming | 2 | 0 | +--------+-----------+---------+-----------+----+-----------+ ``` La línea temporal (`TL`) sube a 2 porque ha habido una promoción. Si repites la consulta por el puerto 5000 de HAProxy, ahora responderá `10.0.0.12`. Vuelve a arrancar Patroni en **pg1**: ```bash sudo systemctl start patroni ``` Patroni detecta que ya no es el líder, usa `pg_rewind` para alinear sus datos con el nuevo primario y se une como réplica. `patronictl list` mostrará de nuevo tres miembros, con pg1 como `Replica`. Si en lugar de parar el servicio el servidor se cae de golpe, el failover tarda algo más: hay que esperar a que expire el bloqueo del líder (`ttl`, 30 segundos). ## Paso 8: Hacer un switchover planificado Para tareas de mantenimiento en el primario, cambia de líder de forma controlada con `switchover`, que no pierde transacciones: ```bash sudo -u postgres patronictl -c /etc/patroni/config.yml switchover pg-cluster --leader pg2 --candidate pg1 --force ``` Comprueba el resultado con `patronictl list`: pg1 vuelve a ser `Leader`. Sin `--force`, `patronictl` pide confirmación y permite programar el cambio para una hora concreta. Para reiniciar todos los nodos de forma ordenada (por ejemplo, tras cambiar un parámetro que lo requiera), usa: ```bash sudo -u postgres patronictl -c /etc/patroni/config.yml restart pg-cluster ``` Los parámetros de PostgreSQL comunes a todo el clúster se cambian con `patronictl edit-config`, que los guarda en etcd y los aplica en todos los nodos; no edites `postgresql.conf` a mano, porque Patroni lo sobrescribe. ## Solución de problemas **Patroni no arranca y el log dice que el directorio de datos no está vacío**: queda un clúster previo en `/var/lib/postgresql/16/main`. Asegúrate de haber ejecutado `pg_dropcluster 16 main --stop` en el paso 1. **`patronictl list` muestra un nodo en estado `start failed` o sin lag**: revisa `sudo journalctl -u patroni` en ese nodo. Las causas habituales son una línea de `pg_hba` que no permite la conexión de replicación desde su IP o una contraseña de `replicator` distinta entre nodos. **`Failed to get list of machines from ...` en el log de Patroni**: Patroni no llega a etcd. Comprueba `etcdctl endpoint health`, el puerto 2379 y los valores de `etcd3.hosts`. **etcd no arranca con `member ... has already been bootstrapped`**: el directorio de datos contiene un clúster anterior. Para el servicio, borra `/var/lib/etcd/patroni` en ese nodo y vuelve a arrancarlo junto a los demás (solo durante la creación inicial; en un clúster en marcha, sustituye el miembro con `etcdctl member remove` y `member add`). ## Conclusión Tienes un clúster PostgreSQL 16 de alta disponibilidad en Ubuntu 24.04: Patroni gestiona la replicación y el failover, etcd guarda el estado y el bloqueo de líder, y HAProxy ofrece a las aplicaciones un punto de conexión fijo hacia el primario y otro hacia las réplicas. Como siguientes pasos puedes: - Activar TLS en etcd, en la API REST de Patroni y en PostgreSQL si el tráfico sale de una red privada. - Configurar copias de seguridad continuas con pgBackRest o WAL-G para poder recuperar a un punto en el tiempo. - Duplicar HAProxy con keepalived y una IP virtual para que el balanceador no sea un punto único de fallo.