High Availability

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

PG Monitoring Team August 12, 2026 20 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 como evitar terminar com dois. Nós construímos essa camada, rodamos, quebramos de propósito, 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 banco precisa continuar aceitando gravações se uma máquina morrer, sem acordar ninguém. 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.
  • A aplicação precisa descobrir o novo endereço. Connection string fixada em um IP não faz failover; ela simplesmente falha.
  • Uma partição de rede pode deixar você com dois primários. Os dois aceitam escrita, os dois se acham certos, e reconciliar a divergência depois é trabalho manual e com perda.

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.
Quem promove a standby às 3 da manhã?O Patroni, sozinho, assim que o leader lock expira.
E se os dois nós de banco discordarem?Um terceiro voto: a máquina witness, que roda só etcd.
Uma transação confirmada pode ser perdida?Não. synchronous_mode_strict confirma o COMMIT só depois que a standby 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 standby e reconstrói um membro quebrado.

Arquitetura de alta disponibilidade PostgreSQL 17: a aplicação grava por um VIP com Keepalived no HAProxy porta 5000, que roteia para db1 ou db2 conforme quem segura o leader lock do Patroni, enquanto a porta 5001 serve consultas somente leitura da standby; os dois nós trocam WAL síncrono para RPO zero, e um quórum etcd de dois em três — etcd em db1, etcd em db2 e uma máquina witness rodando só etcd — decide quem segura o lock.
Uma camada de replicação: streaming físico síncrono entre db1 e db2, com o leader lock decidido por um quórum etcd de três votos.
PeçaOnde rodaPapel
PostgreSQL 17db1, db2O banco, com wal_level=replica.
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.
watchdogdb1, db2Reinicia o nó que perde o lock e não consegue se rebaixar a tempo.

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.

Uma camada de replicação, de propósito

  • 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 standby síncrona.
  • A standby atende consultas somente leitura na porta :5001 do HAProxy (hot_standby=on).
  • 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.

Por que removemos a camada de replicação lógica

A primeira versão carregava uma segunda preocupação: um servidor central somente leitura alimentado por failover slots do PostgreSQL 17, com publication, subscription e callbacks de troca de papel. Funcionava. Removemos mesmo assim, e o raciocínio vale mais do que o código valia.

Alta disponibilidade e replicação lógica são problemas diferentes que por acaso dividiam um repositório. Carregar os dois custava mais do que arquivos: forçava wal_level=logical em produção, somava apply workers e — o ponto que realmente importava — os testes do laboratório mediam continuidade lógica em vez de medir failover. O projeto dizia ser um cluster HA sem testar a única coisa que um cluster HA precisa provar.

A remoção levou junto oito scripts e cerca de 1.400 linhas, e deixou a configuração voltar ao que o streaming físico realmente precisa:

ParâmetroAntesAgoraMotivo
wal_levellogicalreplicaninguém mais decodifica WAL logicamente
max_replication_slots4020só os slots físicos dos membros
sync_replication_slotsonremovidosincronizava o slot lógico na standby
max_logical_replication_workers10removidosem apply workers
max_worker_processes16removidovolta ao padrão
callbackson_role_changeremovidoexistia para limpar slot órfão

A standby continua respondendo consultas somente leitura na :5001. Se você precisa de uma cópia analítica separada, acrescente replicação lógica por conta própria — de forma deliberada, com parâmetros e testes próprios — em vez de herdá-la de um cluster cujo trabalho é sobreviver a uma máquina morta.

Teste em cinco minutos: o laboratório Docker

O cluster inteiro — três nós etcd, dois nós Patroni/PostgreSQL e o HAProxy — 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-app.sh    # um banco e uma tabela para gravar

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
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

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-ha.sh           # o teste completo: reset + switchover + kill

O test-failover.sh mata o container do líder e então verifica três coisas: que o Patroni promoveu a standby sozinho, que a linha já confirmada sobreviveu à promoção, e que o HAProxy :5000 voltou a aceitar escritas. Toda escrita dos testes passa pelo HAProxy — nenhum teste fixa qual nó é o primário, porque num failover de verdade você também não sabe.

== Killing leader db2; db1 must take over with no operator action ==
 Container postgresql-patroni-ha-db2-1  Killed
automatic promotion succeeded: leader=db1
committed row before-kill-1786624502 survived the promotion (RPO zero)
== Restarting db2 to restore RPO-zero writes ==
INSERT 0 1
Automatic failover completed. The cluster writes again through HAProxy :5000.

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.

O bug que só apareceu rodando

Vale contar, porque é o tipo de falha que nenhuma checagem estática pega. Os testes reescritos passaram no bash -n e no ShellCheck. Na primeira execução real do laboratório, quebraram assim:

== Creating appdb through the HAProxy writer (leader=db2) ==
CREATE DATABASE
ERROR:  relation "public.ha_events" does not exist
LINE 1: INSERT INTO public.ha_events (marker) VALUES ('bootstrap')

O CREATE TABLE nunca chegou ao psql. A causa: docker compose exec -T herda o stdin do chamador. A função que descobre qual container está vivo roda dentro de uma substituição de comando — e essa substituição herdava o mesmo stdin, comendo o heredoc destinado ao psql antes que ele o lesse.

A correção é um redirecionamento, aplicado a toda sonda que pode ser chamada de dentro de um $(…):

# scripts/lab/lib.sh
- if dc exec -T "$service" true             >/dev/null 2>&1; then
+ if dc exec -T "$service" true </dev/null  >/dev/null 2>&1; then

Nenhum linter pega isso. Só rodar pega — que é exatamente por que o laboratório existe.

Produção no Ubuntu: três máquinas

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

NóSoftware
db1PostgreSQL 17, Patroni, etcd, HAProxy, Keepalived
db2o mesmo
witnesssó etcd — uma VM pequena basta

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/apply-dcs-tuning.sh    # só no líder atual
sudo bash ubuntu/patroni/setup-haproxy.sh
sudo bash ubuntu/patroni/setup-keepalived.sh
sudo bash ubuntu/monitor/install-cron.sh

O apply-dcs-tuning.sh grava configuração no etcd para o cluster inteiro, então ele pertence ao líder atual — rodar nos dois nós não é "mais seguro", é redundante.

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 do modelo de replicação, 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

Use o switchover.sh quando o líder atual está saudável e você escolhe a hora — o Patroni só troca se o candidato já for a standby síncrona no DCS, então ele recusa as movimentações que perderiam dados. Use o failover.sh apenas quando o líder já se foi e o Patroni não agiu sozinho; se o Patroni está funcionando, você nunca vai precisar dele.

Depois de um crash, o nó antigo normalmente volta via pg_rewind. Quando o rewind não é possível, o reinit-member.sh clona aquele membro a partir do líder atual — mais lento, mas sempre correto.

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 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.
  • Isto é HA local, em uma rede L2. Sobrevive a uma máquina morta, não a um site morto. Disaster recovery entre regiões é outro desenho.
  • 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 standby.

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 um membro foi reinicializado 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 deixado para trás por um membro que nunca voltou 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 test-ha.sh completo, 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 replicação síncrona, hot standby, pg_rewind e replication slots. A documentação de modos de replicação do Patroni explica o synchronous_mode_strict em detalhe.

Perguntas frequentes

Preciso mesmo de um nó witness?
Sim, se você só tem dois servidores de banco. Com dois votantes no etcd, uma partição de rede termina em 1 contra 1: nenhum lado consegue provar que tem a maioria, então nenhum pode assumir como primário com segurança. O witness é um terceiro voto etcd, em uma VM pequena e sem PostgreSQL, e ele desempata de forma determinística.
Alguém precisa promover a standby às 3 da manhã?
Não. O Patroni promove sozinho assim que o leader lock expira, e o HAProxy acompanha a promoção porque faz health check em GET /primary, não em um endereço fixo. O script manual failover.sh existe só para o caso em que o líder já se foi e o Patroni não agiu por conta própria.
O que acontece com as escritas se a standby síncrona cair?
Elas param. Com synchronous_mode_strict o cluster se recusa a confirmar um COMMIT que não consegue garantir, e é isso que torna o RPO zero real. Se a sua prioridade é continuar gravando com um único nó vivo, este desenho é o errado para você.
Ele também alimenta um servidor de relatórios por replicação lógica?
Não mais, e essa remoção foi deliberada. Alta disponibilidade e replicação lógica são problemas diferentes, e carregar os dois forçava wal_level=logical e workers extras em um cluster cujo trabalho é fazer failover. A standby já responde consultas somente leitura na porta 5001 do HAProxy; uma cópia analítica separada é algo que você acrescenta por fora deste repositório.
Dá para usar isso para deixar um cluster PostgreSQL existente em alta disponibilidade?
Não in-place. Os scripts sobem um cluster PostgreSQL 17 novo, restauram um backup validado no líder e depois juntam a standby. Converter um cluster Debian já em execução está explicitamente fora do escopo.

Related Articles

Precisa resolver isso no seu ambiente PostgreSQL?

Fale diretamente com um especialista para avaliar o cenário, priorizar riscos e definir um plano de ação.

Fale conosco