Patroni: Alta Disponibilidade para PostgreSQL na PME
Quando uma base de dados PostgreSQL cai, todas as aplicações que dela dependem param imediatamente. Para uma pequena ou média empresa (PME), uma indisponibilidade de uma hora pode significar milhares de euros em vendas perdidas, suporte parado e clientes frustrados. O Patroni, desenvolvido pela Zalando, é uma solução código aberto que transforma uma instalação PostgreSQL isolada num cluster de alta disponibilidade (HA) com replicação streaming, failover automático e recuperação de desastres — sem licenciamento por nó nem custos de soluções comerciais.
Neste artigo exploramos a arquitectura de três nós do Patroni, a instalação com etcd como Distributed Configuration Store (DCS), a configuração da replicação streaming, o mecanismo de failover automático e switchover, e a integração de pgBouncer e HAProxy para encaminhamento de ligações. No fim, fica com um lista de verificação de erros comuns para evitar armadilhas em produção.
1. Introdução
O Patroni é um modelo de alta disponibilidade para PostgreSQL escrito em Python. Originalmente criado na Zalando para resolver problemas reais de continuidade de serviço, tornou-se num dos projectos mais adoptados pela comunidade PostgreSQL para clustering. O Patroni não é um fork do PostgreSQL nem um substituto do motor — é uma camada de orquestração que gere instâncias PostgreSQL nativas e garante que existe sempre um primário disponível.
O conceito central é simples: vários nós PostgreSQL correm em paralelo, um como primário (leitura e escrita) e os restantes como réplicas (só leitura). Um mecanismo de consenso externo — o DCS — regista qual é o primário actual. Se o primário falha, as réplicas concorrem pela liderança e o DCS elege um novo primário em segundos, sem intervenção manual.
Para uma PME, o Patroni oferece três vantagens decisivas:
| Funcionalidade | Benefício para a PME |
|---|---|
| Failover automático | Recuperação em 5–30 segundos sem chamada nocturna ao administrador |
| Replicação streaming nativa | Cópias em tempo real para leitura distribuída e tolerância a falhas |
| Código aberto sem custos de licenciamento | Sem taxas por nó, ao contrário de soluções como EnterpriseDB ou Patroni comercial |
ℹ Nota: O Patroni suporta três DCS: etcd, Consul e Zookeeper. Este artigo foca-se em etcd por ser o mais leve e adequado para infraestruturas de PME. A lógica de failover é idêntica com qualquer um dos três.
2. Arquitectura Patroni (3 nós)
A topologia recomendada pela documentação oficial usa três nós PostgreSQL e três nós etcd para garantir quórum. O número ímpar de nós evita cenários de split-brain em que duas metades do cluster pensam ser o primário simultaneamente. Com três nós, é necessária a maioria (2 de 3) para eleger ou manter um líder.
O fluxo de dados é o seguinte: as aplicações escrevem num ponto único de entrada (HAProxy ou pgBouncer), que encaminha as ligações para o primário PostgreSQL. O primário replica os dados em streaming para as duas réplicas. O Patroni em cada nó comunica com o cluster etcd para registar o seu estado e coordenar o failover.
| Componente | Nº de nós | Função |
|---|---|---|
| PostgreSQL + Patroni | 3 | 1 primário (read/write) + 2 réplicas (read-only) |
| etcd | 3 | DCS — regista leader, estado do cluster, configuração dinâmica |
| HAProxy | 1–2 | Encaminha ligações para o primário via healthcheck REST |
| pgBouncer | 1–2 | Connection pooling — reduz ligações simultâneas ao PostgreSQL |
O etcd pode correr nos mesmos servidores que o PostgreSQL (colocalizado) ou em máquinas dedicadas. Para uma PME com recursos limitados, a colocalação é prática — cada um dos três servidores corre etcd e PostgreSQL com Patroni. Em produção de maior dimensão, separar o etcd reduz a superfície de falha.
ℹ Porquê 3 nós? Com 2 nós não há quórum se um falhar — ambos precisariam de concordar, e sem um terceiro árbitro não é possível distinguir um nó caído de uma partição de rede. Com 3 nós, 2 sobreviventes formam maioria e elegem um novo leader; o nó isolado fica em minoria e não se promove, evitando split-brain.
3. Instalar Patroni e etcd
A instalação decorre em três fases: PostgreSQL 16, etcd e Patroni. Os comandos abaixo assumem Debian/Ubuntu 24.04 LTS, a distribuição mais comum em servidores PME. Repetir em cada um dos três nós.
3.1. Pré-requisitos
Instalar o PostgreSQL 16 a partir do repositório oficial PostgreSQL (não o pacote Debian, que pode estar desactualizado):
# Repositório oficial PostgreSQL
sudo install -d /usr/share/postgresql-common/pgdg
sudo curl -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc \
--fail https://www.postgresql.org/media/keys/ACCC4CF8.asc
sudo sh -c 'echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] \
https://apt.postgresql.org/pub/repos/apt noble-pgdg main" \
> /etc/apt/sources.list.d/pgdg.list'
sudo apt update
sudo apt install -y postgresql-16 postgresql-client-16 python3-psycopg2
# etcd a partir do repositório oficial
sudo apt install -y etcd-server
# Patroni via pip (versão mais recente que os pacotes Debian)
sudo apt install -y python3-pip python3-dev
sudo pip3 install patroni[etcd]
3.2. Configurar etcd em cada nó
O etcd forma o seu próprio cluster de 3 nós. No ficheiro /etc/default/etcd (nó 1 — adaptar IPs para os nós 2 e 3):
ETCD_NAME="node1"
ETCD_DATA_DIR="/var/lib/etcd"
ETCD_LISTEN_PEER_URLS="http://192.168.1.10:2380"
ETCD_LISTEN_CLIENT_URLS="http://192.168.1.10:2379,http://127.0.0.1:2379"
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://192.168.1.10:2380"
ETCD_ADVERTISE_CLIENT_URLS="http://192.168.1.10:2379"
ETCD_INITIAL_CLUSTER="node1=http://192.168.1.10:2380,node2=http://192.168.1.11:2380,node3=http://192.168.1.12:2380"
ETCD_INITIAL_CLUSTER_TOKEN="patroni-cluster"
ETCD_INITIAL_CLUSTER_STATE="new"
Iniciar o etcd nos três nós e verificar o quórum:
sudo systemctl enable --now etcd
# Verificar quórum (deve mostrar 3 membros)
etcdctl member list --endpoints=http://192.168.1.10:2379
3.3. Configurar Patroni
O ficheiro de configuração principal do Patroni é /etc/patroni/patroni.yml. Criar a directoria e o ficheiro no nó 1:
sudo mkdir -p /etc/patroni
Criar o ficheiro /etc/patroni/patroni.yml no nó 1:
scope: &scope "prod-cluster"
name: "node1"
restapi:
listen: 192.168.1.10:8008
connect_addr: 192.168.1.10:8008
etcd:
hosts: "192.168.1.10:2379,192.168.1.11:2379,192.168.1.12: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_keep_size: 128
initdb:
- encoding: UTF8
- data-checksums
postgresql:
listen: 192.168.1.10:5432
connect_addr: 192.168.1.10:5432
data_dir: /var/lib/postgresql/16/main
bin_dir: /usr/lib/postgresql/16/bin
authentication:
replication:
username: replicator
password: "SenhaForteAqui"
superuser:
username: postgres
password: "SenhaAdminForte"
parameters:
wal_level: replica
hot_standby: "on"
tags:
nofailover: false
noloadbalance: false
clonefrom: false
Replicar este ficheiro nos nós 2 e 3 alterando name, listen e connect_addr para os IPs respectivos. Criar o utilizador de replicação no PostgreSQL do primário antes de arrancar as réplicas:
sudo -u postgres psql -c "CREATE ROLE replicator WITH REPLICATION LOGIN PASSWORD 'SenhaForteAqui';"
Criar o serviço systemd em /etc/systemd/system/patroni.service:
[Unit]
Description=Patroni PostgreSQL HA
After=etcd.service postgresql.service
Wants=etcd.service
[Service]
Type=simple
User=postgres
ExecStart=/usr/local/bin/patroni /etc/patroni/patroni.yml
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now patroni
# Verificar estado do cluster
patronictl list --config-file=/etc/patroni/patroni.yml
✓ Sucesso: O comando patronictl list deve mostrar um nó como Leader e os outros dois como Replica. Se todos aparecem como uninitialized, o etcd não atingiu quórum — verificar etcdctl member list.
4. Configurar Replicação Streaming
O Patroni gere a replicação streaming automaticamente, mas é essencial compreender os parâmetros que a controlam para optimizar desempenho e fiabilidade. A replicação streaming transfere os WAL (Write-Ahead Registos) do primário para as réplicas em tempo real, garantindo que as réplicas estejam prontas para assumir o serviço com perda mínima de dados.
4.1. Parâmetros críticos
| Parâmetro | Recomendação | Efeito |
|---|---|---|
wal_level |
replica |
Nível mínimo para replicação e recuperação point-in-time |
max_wal_senders |
10 | Máximo de processos WAL sender (réplicas + ferramentas de cópia de segurança) |
max_replication_slots |
10 | Slots de replicação — evitam perda de WAL se uma réplica ficar offline |
wal_keep_size |
128 (MB) | WAL retidos para réplicas lentas ou em reinicialização |
maximum_lag_on_failover |
1048576 (1 MB) | Lag máximo em bytes antes de bloquear failover |
Estes parâmetros estão definidos na secção bootstrap.dcs.postgresql.parameters do patroni.yml, pelo que o Patroni aplica-os automaticamente em todos os nós — não é necessário editar o postgresql.conf manualmente. Alterar um parâmetro no patroni.yml e executar patronictl reload propaga a alteração a todo o cluster.
4.2. Verificar replicação
No primário, verificar o estado das réplicas:
sudo -u postgres psql -c "SELECT application_name, state, sync_state,
pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS lag_bytes
FROM pg_stat_replication;"
O resultado mostra cada réplica, o seu estado (streaming), se é síncrona ou assíncrona, e o lag em bytes. Um lag de 0 bytes significa que a réplica está actualizada.
4.3. Replicação síncrona vs assíncrona
Por defeito, o Patroni usa replicação assíncrona — o primário confirma a escrita sem esperar pelas réplicas. Isto oferece o melhor desempenho mas pode perder os últimos commits se o primário falhar antes de os WAL chegarem à réplica. Para tolerância zero de perda de dados, activar replicação síncrona:
# Em patroni.yml, secção bootstrap.dcs.postgresql
synchronous_mode: true
synchronous_mode_strict: false
Com synchronous_mode: true, o primário espera que pelo menos uma réplica confirme a escrita do WAL antes de confirmar a transacção ao cliente. O synchronous_mode_strict: false permite que, se todas as réplicas estiverem indisponíveis, o primário continue aceitando escritas (degradando para assíncrono) em vez de bloquear.
⚠ Atenção: A replicação síncrona aumenta a latência de escrita — cada commit espera pela confirmação de rede da réplica. Para a maioria das PME, a replicação assíncrona com maximum_lag_on_failover baixo (1 MB) oferece um equilíbrio adequado entre desempenho e durabilidade.
5. Failover Automático e Switchover
O failover automático é a funcionalidade central do Patroni. Quando o primário falha (hardware, rede, falha do PostgreSQL), o Patroni detecta a indisponibilidade, elege um novo primário entre as réplicas e actualiza o DCS — tudo em segundos, sem intervenção humana.
5.1. Como funciona o failover
O processo de failover segue estes passos:
- Detecção: O TTL (Time-To-Live) no etcd para o leader expira se o primário não renovar a chave dentro do período configurado (30 segundos por defeito).
- Eleição: As réplicas com lag inferior a
maximum_lag_on_failoverconcorrem pela liderança. A primeira a adquirir o lock no etcd torna-se o novo primário. - Promoção: A réplica eleita executa
pg_ctl promote, deixando de ser read-only e aceitando escritas. - Actualização: O DCS regista o novo leader. As aplicações via HAProxy detectam a mudança e encaminham ligações para o novo primário.
- Recuperação do antigo primário: Quando o nó antigo primário regressa, o Patroni usa
pg_rewindpara sincronizar as diferenças e recriá-lo como réplica.
5.2. Switchover planeado
O switchover é um failover planeado — usado para manutenção, actualizações de versão do PostgreSQL ou migração de hardware. Ao contrário do failover, não há falha: o primário actual continua a correr até ser deliberadamente substituído por uma réplica.
# Switchover manual: promove o node2 a primário
patronictl switchover --config-file=/etc/patroni/patroni.yml
# O comando pede:
# Candidate ['node2', 'node3'] []: node2
# Cluster ['prod-cluster'] []:
# Leader ['node1'] []:
# Confirmar com "yes" para executar
O Patroni verifica que a réplica candidata tem lag aceitável, pára graciosamente o primário actual (espera que as ligações activas terminem), promove a réplica e reinicia o antigo primário como réplica. Todo o processo dura 5–15 segundos.
5.3. Simular failover
Para testar o cluster num ambiente de preparação, simular a falha do primário:
# No nó primário, parar o Patroni e o PostgreSQL
sudo systemctl stop patroni
sudo systemctl stop postgresql
# Observar noutro nó a eleição do novo leader
watch -n 2 'patronictl list --config-file=/etc/patroni/patroni.yml'
# Após o novo leader ser eleito, verificar aplicações
# O HAProxy deve encaminhar para o novo primário automaticamente
# Reiniciar o nó caído — volta como réplica
sudo systemctl start patroni
ℹ Dica: Testar o failover regularmente em preparação. Um cluster HA não testado é um cluster que falha quando mais precisa dele. Documentar o tempo de recuperação real — ajuda a definir SLAs realistas com os clientes.
6. pgBouncer e HAProxy para Encaminhamento
O Patroni garante que existe um primário disponível, mas as aplicações precisam de saber qual é o primário em cada momento. Duas ferramentas complementares resolvem este problema: o HAProxy encaminha ligações para o nó correcto via healthchecks à REST API do Patroni, e o pgBouncer faz connection pooling para reduzir o sobrecarga de ligações no PostgreSQL.
6.1. HAProxy — encaminhamento por healthcheck
O Patroni expõe uma REST API na porta 8008 de cada nó. O endpoint /primary devolve HTTP 200 se o nó for primário, 503 se for réplica. O HAProxy usa isto para encaminhar ligações automaticamente para o primário activo, sem reconfiguração manual após failover.
Configuração /etc/haproxy/haproxy.cfg:
frontend postgres_write
bind *:5000
default_backend postgres_primary
frontend postgres_read
bind *:5001
default_backend postgres_replicas
backend postgres_primary
option httpchk GET /primary
http-check expect status 200
default-server inter 3s fall 3 rise 2
server node1 192.168.1.10:5432 check port 8008
server node2 192.168.1.11:5432 check port 8008
server node3 192.168.1.12:5432 check port 8008
backend postgres_replicas
option httpchk GET /replica
http-check expect status 200
balance roundrobin
default-server inter 3s fall 3 rise 2
server node1 192.168.1.10:5432 check port 8008
server node2 192.168.1.11:5432 check port 8008
server node3 192.168.1.12:5432 check port 8008
Com esta configuração, as aplicações ligam à porta 5000 do HAProxy para escritas (sempre no primário) e à porta 5001 para leituras (distribuídas entre réplicas). Após um failover, o HAProxy detecta a mudança em 3–9 segundos (3 checks de 3s) e começa a encaminhar para o novo primário sem reconfiguração.
6.2. pgBouncer — connection pooling
O PostgreSQL cria um processo por ligação — com 200 clientes, são 200 processos, cada um consumindo 5–10 MB de memória. O pgBouncer interpõe-se entre as aplicações e o PostgreSQL, mantendo um pool pequeno de ligações reais (ex: 20) e multiplexando centenas de ligações de cliente sobre elas.
Configuração /etc/pgbouncer/pgbouncer.ini:
[databases]
prod = host=127.0.0.1 port=5000 dbname=prod
[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
pool_mode = transaction
max_client_conn = 200
default_pool_size = 20
reserve_pool_size = 5
reserve_pool_timeout = 3
server_idle_timeout = 60
query_wait_timeout = 120
O pool_mode = transaction é a forma mais eficiente — o pgBouncer atribui uma ligação real do pool apenas durante a transacção activa, libertando-a imediatamente após COMMIT ou ROLLBACK. Isto permite que 200 clientes partilhem 20 ligações PostgreSQL sem contenção.
6.3. Topologia completa
O fluxo de uma ligação de aplicação até ao PostgreSQL num cluster completo é:
| Passo | Componente | Porta | Acção |
|---|---|---|---|
| 1 | Aplicação | 6432 | Liga ao pgBouncer |
| 2 | pgBouncer | 5000 | Encaminha para HAProxy (write) |
| 3 | HAProxy | 8008 | Healthcheck REST ao Patroni |
| 4 | HAProxy | 5432 | Liga ao primário PostgreSQL |
| 5 | PostgreSQL | — | Processa consulta, replica WAL às réplicas |
7. Erros Comuns e Lista de verificação
A configuração do Patroni env vários componentes coordenados — etcd, PostgreSQL, Patroni, HAProxy e pgBouncer. Um erro numa camada pode manifestar-se noutra, tornando o diagnóstico difícil. Abaixo estão os erros mais frequentes e um lista de verificação de verificação para produção.
7.1. Erros comuns
| Problema | Causa provável | Solução |
|---|---|---|
Todos os nós aparecem como uninitialized |
etcd sem quórum | Verificar etcdctl member list — precisa de 2 de 3 nós online |
Split-brain: dois nós como Leader |
Partição de rede com TTL mal configurado | Aumentar ttl e reduzir loop_wait; usar watchdog |
| Réplica não se junta ao cluster | Utilizador replicator não criado ou senha errada |
Verificar pg_hba.conf e credenciais em patroni.yml |
| Failover não ocorre | Todas as réplicas com lag superior a maximum_lag_on_failover |
Aumentar wal_keep_size ou verificar largura de banda entre nós |
| HAProxy não encaminha para novo primário | Healthcheck a falhar — REST API não acessível | Verificar porta 8008 no restapi.listen e firewall |
| Antigo primário não volta como réplica | Timeline divergente sem pg_rewind |
Activar use_pg_rewind: true no patroni.yml |
| pgBouncer: erros de prepared statements | pool_mode = transaction não suporta prepared statements |
Migrar para pool_mode = session ou desactivar prepared statements na aplicação |
7.2. Lista de verificação de produção
- ☐ Quórum etcd: 3 nós etcd online,
etcdctl member listmostra 3 membros. - ☐ Replicação: Lag entre primário e réplicas inferior a 1 MB em condição normal.
- ☐ Watchdog: Activar watchdog hardware/software para garantir que um nó isolado se auto-termina em vez de continuar aceitando escritas.
- ☐ pg_rewind:
use_pg_rewind: trueactivado para recuperação automática do antigo primário. - ☐ Cópia de segurança: pg_basebackup ou pgBackRest configurado — o HA não substitui as cópias de segurança.
- ☐ Monitorização: Exportador postgres_exporter para Prometheus, alertas em lag > 10 MB e nós offline.
- ☐ Firewall: Portas 5432, 8008, 2379, 2380 abertas apenas entre nós do cluster; 6432 (pgBouncer) aberta apenas às aplicações.
- ☐ Teste de failover: Executar switchover mensal em preparação para validar que o cluster responde como esperado.
- ☐ Documentação: Registar IPs, portas, senhas e procedimentos de recuperação num runbook acessível à equipa.
✓ Recuperação de desastres: Para além do HA local, considerar um cluster standby noutro centro de dados ou região na nuvem. O Patroni suporta standby clusters que replicam um cluster primário para um cluster remoto, permitindo recuperação total em caso de perda do centro de dados principal.
O Patroni representa o estado da arte em alta disponibilidade código aberto para PostgreSQL. Para uma PME, a transição de um servidor único para um cluster de três nós com failover automático reduz o tempo de indisponibilidade de horas para segundos, sem custos de licenciamento. A complexidade inicial de configuração é compensada pela robustez operacional — e por saber que, quando o hardware falha, as aplicações continuam a correr.
Artigos relacionados: