High Availability

Liberamos como open source um cluster PostgreSQL 17 em alta disponibilidade: Patroni, etcd, HAProxy e failover slots

PG Monitoring Team August 12, 2026 22 min de leitura

Todo mundo concorda que PostgreSQL precisa de alta disponibilidade. Bem menos times realmente têm, porque a distância entre "temos uma réplica" e "a aplicação continua gravando quando uma máquina morre" é preenchida por decisões que ninguém quer tomar às 3 da manhã: quem é o primário agora, como a aplicação encontra ele, e por que a assinatura do servidor de relatórios quebrou depois do promote. Nós construímos essa camada, rodamos ela, e hoje estamos liberando.

Gratuito e open source, licença MIT: github.com/johnvithera01/postgresql-patroni-ha — um cluster PostgreSQL 17 completo em alta disponibilidade, com failover automático, laboratório Docker para aprender e scripts Ubuntu para produção. Documentação em Português e Inglês. Clone, quebre no laboratório, use. Dúvidas? Fale com a gente.

O problema que ele resolve

O cenário é comum: uma unidade — uma planta, uma filial, um data center — precisa continuar aceitando gravações se uma máquina de banco morrer. A matriz precisa de uma cópia atualizada desses dados para consulta, sem nunca gravar no cluster da unidade.

Com dois servidores PostgreSQL, replicação nativa e promote manual, três coisas dão errado exatamente no pior momento:

  • Alguém precisa decidir quem é o primário. Uma decisão humana, sob pressão, com informação incompleta — e se duas pessoas decidirem diferente, você ganha split brain.
  • A aplicação precisa descobrir o novo endereço. Connection string fixada em um IP não faz failover; ela simplesmente falha.
  • A assinatura lógica da matriz quebra. O replication slot vivia no primário antigo. Ele não se move. Você recria a assinatura e recopia os dados, justo no dia em que menos quer.

O repositório amarra essas peças usando componentes que já existem e são bem conhecidos, ligados da forma específica que os faz funcionar como um sistema só:

NecessidadeComo é resolvida
Quem é o primário?Patroni + etcd. Só um nó consegue segurar o leader lock.
Como a aplicação acha quem grava?Keepalived (VIP) + HAProxy na porta 5000, com health check GET /primary.
E se os dois nós de banco discordarem?Um terceiro voto: a máquina witness, que roda só etcd.
A matriz precisa recriar a assinatura a cada failover?Não — se o slot lógico for um failover slot do PostgreSQL 17 e o assinante conectar pelo HAProxy, não pelo IP do nó.
Uma transação confirmada pode ser perdida?No cluster local, não. synchronous_mode_strict confirma o COMMIT só depois que a réplica síncrona tem o WAL.

A arquitetura em uma imagem

Um único modelo de HA: Patroni. Não existe caminho de promote manual, nem reconstrução artesanal com pg_basebackup, nem modo duplo "às vezes automático, às vezes não". O Patroni elege o primário, promove a réplica e reconstrói um membro quebrado.

            aplicação (leitura + escrita)            matriz (somente leitura)
              |                                  |
           VIP :5000  ── HAProxy ── :5001        |
              |            |                     |
  ┌───────────┴──┐      ┌──┴───────────┐         |
  |   db1        |◄────►|   db2        |         |
  | PG 17        | WAL  | PG 17        |         |
  | Patroni+etcd | sínc.| Patroni+etcd |         |
  └──────┬───────┘      └───────┬──────┘         |
         |    quórum etcd       |                |
         └────────┬─────────────┘                |
                  |                              |
             ┌────┴─────┐                  ┌─────┴──────┐
             | witness  |                  |  central   |
             | só etcd  |                  | PG 17 sub  |
             └──────────┘                  └────────────┘
                            replicação lógica
                            slot site_001_slot (failover = true)
PeçaOnde rodaPapel
PostgreSQL 17db1, db2, centralO banco. Na unidade, com wal_level=logical e sync_replication_slots=on.
Patroni 4.1db1, db2Supervisor do PostgreSQL: bootstrap, réplica, promote, rewind e reinit.
etcd 3.5db1, db2, witnessGuarda a configuração do cluster e o leader lock. Quórum de 2 em 3.
witnessmáquina própriaO terceiro voto. Sem PostgreSQL. Sem ele, dois nós de banco empatam.
HAProxydb1 e db2Manda escrita só para quem responde 200 em /primary.
Keepaliveddb1 e db2Anuncia um VIP, para a aplicação nunca precisar do IP de cada VM.
Failover slotscluster da unidadeO slot lógico é mantido sincronizado na standby, então a matriz sobrevive ao promote.
centralservidor separadoPostgreSQL somente leitura com CREATE SUBSCRIPTION ... failover = true.

O witness é a peça que todo mundo tenta pular, e é justamente a que separa um cluster de um cara ou coroa. Com dois votantes, uma partição de rede resulta em 1 contra 1: nenhum lado consegue provar que tem a maioria, então nenhum pode assumir como primário com segurança. O terceiro etcd — em uma VM minúscula, sem banco nenhum — desempata de forma determinística.

Duas camadas de replicação que não podem ser confundidas

É aqui que a maioria das montagens caseiras erra, então vale ser explícito. Existem dois mecanismos de replicação completamente separados em jogo, e o Patroni é responsável por apenas um deles.

Camada 1 — física, síncrona, gerenciada pelo Patroni

  • db1 e db2 formam um único escopo Patroni (pg-ha).
  • O líder aceita escritas; a standby aplica WAL por um slot físico com o nome do membro.
  • synchronous_mode: true e synchronous_mode_strict: true: o COMMIT espera a réplica síncrona.
  • Watchdog no Ubuntu: se o Patroni perde o lock e não consegue rebaixar o PostgreSQL a tempo, o nó reinicia sozinho.

RPO zero tem preço, e ele não é negociável. Com synchronous_mode_strict, se a standby síncrona estiver fora, as novas escritas param. Esse é o trade-off, não um defeito: o cluster se recusa a confirmar um commit que não consegue garantir. Se a sua prioridade é "continuar gravando mesmo sozinho", este desenho é o errado para você — e o README diz isso com essas mesmas palavras.

Camada 2 — lógica, assíncrona, PostgreSQL puro

O Patroni não replica nada para a matriz. Essa cópia é replicação lógica nativa do PostgreSQL, apontada para a porta de escrita do HAProxy:

  • A central assina uma publicação no banco da aplicação (site_001_pub em appdb).
  • A assinatura é criada com failover = true e slot_name = site_001_slot.
  • A standby mantém esse slot sincronizado (sync_replication_slots, hot_standby_feedback).
  • O primário lista o slot físico da réplica em synchronized_standby_slots, então o assinante nunca passa à frente da standby.
  • A central conecta em VIP:5000 — nunca direto em db1 ou db2.

Essa é a peça que o PostgreSQL 17 finalmente tornou possível. Antes dos failover slots, um assinante lógico apontado para um cluster que promove era uma parada programada: o slot existia só no primário antigo, então depois da promoção a assinatura não tinha o que consumir e precisava ser recriada — ou seja, recópia completa de todas as tabelas. Com failover = true, o slot já existe no novo primário, na posição certa, e o fluxo simplesmente continua.

DDL não passa pela replicação lógica. Aplique a mudança de schema na central primeiro e depois rode ALTER SUBSCRIPTION site_001_sub REFRESH PUBLICATION. Esquecer isso é a forma mais comum de um assinante lógico saudável parar silenciosamente de receber uma tabela nova.

Teste em cinco minutos: o laboratório Docker

O cluster inteiro — três nós etcd, dois nós Patroni/PostgreSQL, HAProxy e o assinante central — sobe em uma máquina só com Docker Compose. Ele existe para você quebrar as coisas de propósito antes de ser responsável por elas em produção.

git clone https://github.com/johnvithera01/postgresql-patroni-ha.git
cd postgresql-patroni-ha

./scripts/lab/reset-lab.sh        # constrói e sobe o cluster inteiro
./scripts/lab/status.sh           # quem é líder, quem é standby síncrona
./scripts/lab/setup-logical.sh    # publicação, failover slot, assinatura

Os requisitos são apenas Docker Compose v2 e Bash. O que sobe:

ServiçoPapelPorta no host
etcd1, etcd2, etcd3Quórum — etcd3 é o witnessinterna
db1, db2Patroni + PostgreSQL 17interna
haproxyescrita / leitura127.0.0.1:15000 e :15001
centralassinante lógico127.0.0.1:15002
psql "host=127.0.0.1 port=15000 user=postgres dbname=appdb"        # escrita
psql "host=127.0.0.1 port=15001 user=postgres dbname=appdb"        # leitura
psql "host=127.0.0.1 port=15002 user=postgres dbname=site_001_db"  # central

Agora quebre de propósito

Ler sobre failover não ensina nada. Ver um líder morrer com uma sessão de escrita aberta ensina muito:

./scripts/lab/test-switchover.sh          # promoção planejada e controlada
./scripts/lab/test-failover.sh           # mata o líder e acompanha a eleição
./scripts/lab/test-logical-continuity.sh # o teste completo: switchover + kill + conferir a central

Esse último é o teste interessante, e é o que a esteira de CI roda a cada commit. Ele grava no cluster, força um switchover planejado, mata o novo líder, espera a eleição, grava de novo e então verifica se a matriz recebeu todas as linhas — com a mesma assinatura, o mesmo slot e nenhuma intervenção manual. Se essa verificação falhar, o desenho está quebrado e o build avisa.

O laboratório não é produção. Não há Keepalived nem VIP (VRRP precisa de rede L2 de verdade), e todos os nós dividem o mesmo host, então uma falha do host derruba todos de uma vez. As senhas do laboratório estão em scripts/lab/lib.sh e são valores de demonstração — não leve nenhuma delas para lugar nenhum.

Produção no Ubuntu: quatro máquinas

Produção são quatro VMs, e cada script é idempotente o bastante para ser executado a partir de um checklist, não da memória:

Software
db1PostgreSQL 17, Patroni, etcd, HAProxy, Keepalived
db2o mesmo
witnesssó etcd — uma VM pequena basta
centralassinante lógico PostgreSQL 17

Decisões já embutidas nos scripts, para você não rediscutir sob pressão: PostgreSQL 17 com Patroni 4.1+, synchronous_mode_strict (RPO zero), TLS no etcd e no PostgreSQL, watchdog obrigatório, e instalação limpa com restore em vez de conversão in-place de um cluster Debian existente.

# em db1, db2 e witness
sudo ./ubuntu/install-env.sh ubuntu/env.example
sudo nano /etc/pg-patroni-ha/env
sudo bash ubuntu/patroni/setup-etcd.sh

# witness
sudo bash ubuntu/patroni/setup-witness.sh

# db1 e db2
sudo bash ubuntu/patroni/setup-patroni-node.sh
sudo bash ubuntu/patroni/bootstrap-cluster.sh   # só no primeiro nó
sudo bash ubuntu/patroni/join-replica.sh        # segundo nó
sudo bash ubuntu/patroni/setup-haproxy.sh
sudo bash ubuntu/patroni/setup-keepalived.sh
sudo bash ubuntu/patroni/setup-failover-slots.sh site_001

# central
sudo bash ubuntu/central/setup-server.sh
sudo bash ubuntu/central/setup-subscription.sh site_001

Os certificados vão em /etc/pg-patroni-ha/tls/ antes de rodar qualquer coisa. A lista SAN do certificado do PostgreSQL precisa incluir o hostname do VIP e os hostnames dos dois bancos — senão a aplicação valida bem contra um nó e falha na verificação assim que cair no outro, ou seja, durante o seu primeiro failover real.

O runbook completo é o docs/production.md; o raciocínio por trás das camadas, do quórum e do que o projeto explicitamente não faz está em docs/architecture.md.

Operação do dia a dia

sudo bash ubuntu/patroni/status.sh            # estado do cluster, lag, quem lidera
sudo bash ubuntu/patroni/switchover.sh db2   # planejado, em janela de manutenção
sudo bash ubuntu/patroni/failover.sh db2     # forçado, quando o líder já se foi
sudo bash ubuntu/patroni/reinit-member.sh db1 # reconstrói um membro a partir do líder
sudo bash ubuntu/monitor/check-all.sh        # as verificações de cron

Duas regras operacionais merecem ser decoradas, porque as duas são contraintuitivas e caras de errar.

Depois de um failover saudável, não faça DROP SUBSCRIPTION na central. O instinto é recriar, e recriar dispara uma recópia completa de todas as tabelas. Não é necessário: o failover slot já existe no novo primário e a central está conectada pelo HAProxy, então ela reconecta em quem grava e segue consumindo o mesmo site_001_slot.

Quando um nó que voltou não consegue sincronizar o failover slot, faça reinit — não outro rewind. Depois de um crash, o líder antigo pode voltar via pg_rewind. Se o catalog xmin local já avançou demais, o worker de sincronização de slots do PostgreSQL 17 se recusa a copiar o failover slot e diz isso com todas as letras:

Synchronization could lead to data loss

Essa mensagem é o worker protegendo você: copiar o slot naquele ponto permitiria que o assinante pulasse mudanças que nunca recebeu. A correção certa é um clone limpo a partir do primário atual:

sudo bash ubuntu/patroni/reinit-member.sh db1

Os limites, ditos de cara

Um desenho de HA que não diz onde quebra é propaganda. Estes são trade-offs deliberados, documentados no próprio repositório:

  • RPO zero trava as escritas quando não há standby síncrona. Troca-se disponibilidade de escrita por nunca perder um commit confirmado.
  • Perder o witness e um nó de banco ao mesmo tempo perde o quórum. O sobrevivente não tem o lock e não vai se promover. Isso é intencional — um nó sozinho que não consegue provar que está sozinho é exatamente como o split brain começa.
  • Replicação lógica não é backup. Um DELETE replica fielmente e na hora. Use pgBackRest, ou equivalente, para PITR.
  • Keepalived/VRRP precisa de rede L2 real. O laboratório Docker não exercita o VIP; valide isso nas VMs de verdade.
  • O HAProxy termina o TCP, então o PostgreSQL vê o IP do proxy, não o do cliente. Restrinja o CIDR da aplicação no firewall e no HAProxy, não só no pg_hba.conf.
  • Ele não migra um cluster de produção existente in-place. Ele sobe um cluster novo, restaura um backup validado no líder e depois junta a réplica.

Failover automático não é monitoramento

Um cluster Patroni sobrevive à falha. Ele não conta por que ela aconteceu, e vai sobreviver alegremente à mesma falha toda semana sem ninguém perceber que um disco está enchendo, que uma réplica foi reconstruída três vezes no mês ou que a standby síncrona atrasa sob a carga da tarde.

Pior: as partes desse desenho que protegem você são também as que machucam em silêncio. Uma escrita travada por synchronous_mode_strict parece "a aplicação está lenta", não "a standby sumiu". Um replication slot abandonado retém WAL até o pg_wal encher o filesystem e o cluster inteiro parar — a forma mais comum de um servidor PostgreSQL perfeitamente saudável cair. Lag de replicação que dispara às 3 da manhã e normaliza às 7 é invisível para quem só olha depois da reclamação.

Essa é a camada que o PG Monitoring cobre: acompanhamento contínuo de lag e retenção de slots, crescimento de WAL, autovacuum e risco de wraparound, regressões de query, saturação de conexões e esperas por lock — com histórico, para que depois de um failover você prove o que aconteceu em vez de adivinhar. Use o cluster open source para ficar de pé; use monitoramento para não ser o último a saber por que quase não ficou.

Use, quebre, pergunte

O repositório é licenciado em MIT. Clone, faça fork, rode o laboratório, desmonte, adapte os scripts ao seu ambiente. Issues e pull requests são bem-vindos — a CI valida Bash e ShellCheck, confere o arquivo Compose e roda o teste completo de continuidade lógica, então uma mudança que quebra o failover não entra sem alarde.

Repositório: github.com/johnvithera01/postgresql-patroni-haREADME em Português · README in English

Travou, ou quer uma segunda opinião sobre a sua topologia antes de montar? Fale direto conosco:
WhatsApp: +55 62 98156-1666
E-mail: joao.victor.32@hotmail.com

Respondemos dúvidas sobre o repositório sendo você cliente do PG Monitoring ou não. Desenhar HA errado custa caro o bastante para preferirmos que você acerte.

Referências: o manual do PostgreSQL documenta failover de replicação lógica e sincronização de slots, replicação síncrona, pg_rewind e replication slots. A documentação de modos de replicação do Patroni explica o synchronous_mode_strict em detalhe.

Related Articles

Ready to experience better PostgreSQL monitoring?

Join thousands of teams who switched from traditional tools to PG Monitoring's AI-powered platform.

Fale conosco