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ó:
| Necessidade | Como é 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ça | Onde roda | Papel |
|---|---|---|
| PostgreSQL 17 | db1, db2, central | O banco. Na unidade, com wal_level=logical e sync_replication_slots=on. |
| Patroni 4.1 | db1, db2 | Supervisor do PostgreSQL: bootstrap, réplica, promote, rewind e reinit. |
| etcd 3.5 | db1, db2, witness | Guarda a configuração do cluster e o leader lock. Quórum de 2 em 3. |
| witness | máquina própria | O terceiro voto. Sem PostgreSQL. Sem ele, dois nós de banco empatam. |
| HAProxy | db1 e db2 | Manda escrita só para quem responde 200 em /primary. |
| Keepalived | db1 e db2 | Anuncia um VIP, para a aplicação nunca precisar do IP de cada VM. |
| Failover slots | cluster da unidade | O slot lógico é mantido sincronizado na standby, então a matriz sobrevive ao promote. |
| central | servidor separado | PostgreSQL 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
db1edb2formam 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: trueesynchronous_mode_strict: true: oCOMMITespera 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_pubemappdb). - A assinatura é criada com
failover = trueeslot_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 emdb1oudb2.
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ço | Papel | Porta no host |
|---|---|---|
etcd1, etcd2, etcd3 | Quórum — etcd3 é o witness | interna |
db1, db2 | Patroni + PostgreSQL 17 | interna |
haproxy | escrita / leitura | 127.0.0.1:15000 e :15001 |
central | assinante lógico | 127.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:
| Nó | Software |
|---|---|
db1 | PostgreSQL 17, Patroni, etcd, HAProxy, Keepalived |
db2 | o mesmo |
witness | só etcd — uma VM pequena basta |
central | assinante 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
DELETEreplica 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-ha — README 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.