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:

  1. 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).
  2. Eleição: As réplicas com lag inferior a maximum_lag_on_failover concorrem pela liderança. A primeira a adquirir o lock no etcd torna-se o novo primário.
  3. Promoção: A réplica eleita executa pg_ctl promote, deixando de ser read-only e aceitando escritas.
  4. 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.
  5. Recuperação do antigo primário: Quando o nó antigo primário regressa, o Patroni usa pg_rewind para 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 list mostra 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: true activado 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: