Diagrama de replicação e failover de banco de dados: primário, réplicas e proteção contra split-brain

Um diagrama de replicação deve mostrar para onde vão as escritas, quão atrasadas as leituras podem estar, quem decide um failover e o que impede dois primários.

· Fundador e desenvolvedor

Publicado em

Diagrama de replicação e failover de banco de dados: primário, réplicas e proteção contra split-brain

A maioria dos diagramas de replicação mostra um primário, duas réplicas e setas entre eles. A imagem é verdadeira e quase inútil durante um incidente, porque não diz qual nó aceita escritas agora, qual réplica pode servir dados atrasados nem o que acontece quando o primário para de responder.

Um diagrama de replicação útil responde a quatro perguntas. Este guia trabalha essas perguntas com PostgreSQL, Patroni e etcd e depois desenha o resultado. As mesmas perguntas valem para MySQL, para outros gerenciadores de failover e para serviços gerenciados.

Quatro perguntas que todo diagrama de replicação precisa responder

  1. Para onde vão as escritas? Precisa haver exatamente um primário, e os clientes precisam de um jeito de encontrá-lo depois de um failover.
  2. Quão atrasadas as leituras podem estar? Cada réplica é síncrona ou assíncrona, e as assíncronas ficam para trás.
  3. Quem decide um failover? Alguma coisa precisa detectar a falha, escolher uma réplica e promovê-la.
  4. O que impede dois primários? Um primário isolado precisa parar de aceitar escritas antes que outro nó seja promovido.

Replicação síncrona e assíncrona

A replicação por streaming do PostgreSQL é assíncrona por padrão. O primário confirma um commit antes de o standby recebê-lo, então uma queda pode perder as transações que ainda não tinham chegado. A documentação do PostgreSQL diz isso com todas as letras: a quantidade de dados perdidos é proporcional ao atraso de replicação no momento do failover.

A replicação síncrona faz os commits esperarem por um ou mais standbys listados em synchronous_standby_names. Quanto eles esperam depende de synchronous_commit:

synchronous_commit Um commit retorna depois que o standby síncrono o tiver
remote_apply Aplicado, então consultas no standby já o veem
on (padrão) Gravado em armazenamento durável
remote_write Entregue ao sistema operacional, ainda sem flush
local Sem esperar o standby; só o flush local
off Sem esperar nada, nem o disco local

O preço é a latência: todo commit inclui uma ida e volta até o standby, então mantenha os standbys síncronos em uma zona próxima, não em outra região. E se o único standby síncrono sumir, os commits podem esperar para sempre. Defina mais de um candidato ou deixe um gerenciador de failover mudar a configuração por você.

Réplicas de leitura e roteamento de leituras

Uma réplica assíncrona é um bom lugar para relatórios e endpoints com muitas leituras, desde que o código saiba que os dados podem estar atrasados. No primário, a view pg_stat_replication mostra write_lag, flush_lag e replay_lag de cada standby; replay_lag aproxima quanto tempo leva até um commit ficar visível na réplica.

Decida por consulta quais podem ler dados atrasados. Um usuário que acabou de salvar um formulário e recarrega a página deve ler do primário. Um dashboard pode ler da réplica.

Failover: quem decide

O PostgreSQL não detecta sozinho um primário com falha nem promove um standby; a documentação deixa isso para ferramentas externas. O Patroni é uma escolha comum. Cada nó roda um agente do Patroni, e os agentes compartilham estado por um armazenamento de configuração distribuído como o etcd:

  • O primário mantém uma chave de líder com TTL e a renova a cada ciclo (loop_wait, 10 segundos por padrão).
  • Se não conseguir renovar a chave por ttl segundos (30 por padrão), a chave expira e as réplicas elegíveis começam uma corrida pela liderança.
  • O melhor candidato pega a chave e se promove; os outros passam a segui-lo.

Os clientes encontram o novo primário por meio de um proxy. A API REST do Patroni responde a GET /primary com HTTP 200 só no nó que tem a chave de líder, então o HAProxy pode mandar as escritas para o nó que disser sim:

listen primary
    bind *:5000
    option httpchk OPTIONS /primary
    http-check expect status 200
    default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
    server pg-a 10.0.1.10:5432 check port 8008
    server pg-b 10.0.2.10:5432 check port 8008
    server pg-c 10.0.3.10:5432 check port 8008

Um segundo listener na porta 5001 verifica /replica?lag=16MB, o que tira de cena uma réplica com mais de 16 MB de WAL de atraso. Rode o HAProxy em mais de uma máquina, ou ele vira o ponto único de falha que você estava tentando eliminar.

Split-brain: o que impede dois primários

Dois primários aceitando escritas significam dois históricos que divergem, e um deles vai para o lixo. A documentação do PostgreSQL chama o remédio de STONITH: é preciso avisar ao primário antigo que ele não é mais o primário. O Patroni empilha várias defesas:

  • O lease. Um primário que não consegue atualizar a chave de líder para de aceitar escritas: o Patroni para ou rebaixa o PostgreSQL. Se o próprio armazenamento ficar inacessível por mais tempo que retry_timeout, o Patroni rebaixa o primário, a menos que o modo failsafe do DCS esteja ativado.
  • Um watchdog. Se o processo do Patroni travar, um watchdog do Linux reinicia a máquina. O Patroni o configura para disparar 5 segundos antes de o TTL expirar.
  • Um quórum. O etcd precisa da maioria dos membros, dois de três, para aceitar uma escrita. Três membros em três zonas sobrevivem à perda de uma zona, e uma minoria isolada não consegue eleger ninguém.
  • Um único processo no comando. Só o Patroni pode iniciar o PostgreSQL. Um serviço do sistema que reinicia o Postgres sozinho depois de uma queda pode trazer de volta um segundo primário.

Quando o primário antigo volta, ele precisa entrar de novo como réplica. O pg_rewind ressincroniza o diretório de dados dele com o novo primário sem copiar blocos que não mudaram; o Patroni o executa quando use_pg_rewind está ativado.

# Patroni dynamic configuration (patronictl edit-config)
ttl: 30
loop_wait: 10
retry_timeout: 10
synchronous_mode: on        # promote only a node that has every acknowledged commit
postgresql:
  use_pg_rewind: true
  parameters:
    wal_log_hints: "on"

# Local patroni.yml tags
# pg-a: noloadbalance: true            (failover target, no reads)
# pg-c: nosync: true, nofailover: true (reads only)

Com synchronous_mode ativado, o Patroni só promove um standby que ele sabe ter todos os commits informados como bem-sucedidos. A contrapartida é a disponibilidade: se o primário cair enquanto nenhum standby síncrono estiver conectado, o Patroni não promove ninguém, e as escritas ficam paradas até alguém intervir.

Replicação não é backup

Um DROP TABLE chega a todas as réplicas em milissegundos. Mantenha o arquivamento contínuo de WAL e backups base regulares, que permitem a recuperação point-in-time para qualquer momento depois do backup base, e teste as restaurações com regularidade.

O diagrama

Três zonas de disponibilidade com nós PostgreSQL. O HAProxy envia as escritas do serviço de pedidos ao pg-b, o primário, e as leituras ao pg-c, uma réplica assíncrona. O pg-b envia WAL ao pg-a de forma síncrona e ao pg-c de forma assíncrona e guarda a chave de líder no etcd.

11 componentes · 8 conexões

Abrir no Archboard
Conteúdo do diagrama
  • Order service
  • HAProxy
  • AZ a
  • AZ b
  • AZ c
  • pg-a · sync standby
  • pg-b · primary
  • pg-c · async replica
  • etcd
  • WAL archive + base backups
  • SQL
  • writes :5000
  • reads :5001
  • WAL (sync)
  • WAL (async)
  • leader key, TTL 30 s
  • races if key expires
  • archived WAL
Um cluster PostgreSQL em três zonas gerenciado pelo Patroni: escritas para o primário pelo HAProxy, um standby síncrono para failover, uma réplica assíncrona para leituras, uma chave de líder no etcd e um arquivo de WAL para recuperação point-in-time.

Como ler:

  • Escritas. O serviço de pedidos se conecta ao HAProxy. A porta 5000 vai para o pg-b, o único nó que responde 200 a /primary.
  • Leituras. A porta 5001 vai para o pg-c, uma réplica assíncrona em outra zona. O pg-a não atende leituras.
  • Replicação. O pg-b envia WAL ao pg-a de forma síncrona (linha contínua) e ao pg-c de forma assíncrona (tracejada).
  • Failover. O pg-b renova a chave de líder no etcd. Se a chave expirar, o pg-a, o standby síncrono, disputa a liderança. O pg-c tem a tag nofailover.
  • Recuperação. O pg-b arquiva o WAL e os backups base em um armazenamento de objetos.
  • Metas. A nota no canto superior esquerdo traz RPO, RTO, o orçamento de atraso e a retenção de backups.

O que rotular em um diagrama de replicação

  • O modo em cada seta de replicação, síncrona ou assíncrona, e o orçamento de atraso das réplicas assíncronas.
  • O papel de cada réplica: candidata a failover, réplica de leitura ou as duas coisas.
  • Onde fica o lock de líder e o TTL dele, ou o serviço gerenciado que faz esse papel.
  • Como os clientes encontram o primário: um proxy, um nome DNS ou um IP virtual.
  • Domínios de falha: zonas de disponibilidade como molduras, incluindo onde rodam os membros do consenso.
  • Metas de RPO e RTO, para que quem revisa possa conferir o design contra elas.

O que os serviços gerenciados fazem por você

Bancos de dados gerenciados respondem às mesmas perguntas, mas algumas respostas são fixas. Em uma instância Amazon RDS Multi-AZ, o standby é síncrono, fica em outra zona e não atende leituras. O failover aponta o registro DNS para o standby e normalmente leva de 60 a 120 segundos. Um cluster Multi-AZ tem dois standbys legíveis, usa replicação semissíncrona e normalmente faz o failover em menos de 35 segundos. No diagrama, o endpoint substitui o HAProxy e a AWS substitui o Patroni e o etcd. Mesmo assim, você rotula o atraso, o roteamento de leituras e as metas.

Teste o failover

Troque o primário de propósito, por exemplo em toda janela de manutenção com patronictl switchover. A documentação do PostgreSQL recomenda alternar com regularidade, porque isso também testa o mecanismo de failover. Meça por quanto tempo as escritas falham, confira se o HAProxy acompanha e confirme que nenhum commit confirmado ficou faltando. Para failover entre regiões, veja o diagrama de arquitetura multirregião da AWS.

Monte no Archboard

  1. Entre com Google ou GitHub e crie um diagrama.
  2. Adicione três nós Moldura do pack Básico para as zonas de disponibilidade e, em cada um, um nó Postgres do pack Dados: pg-a (standby síncrono), pg-b (primário) e pg-c (réplica assíncrona).
  3. Acima das zonas, adicione um Contêiner do pack Modelo C4 para o serviço de pedidos e um Balanceador de carga do pack Rede para o HAProxy.
  4. Abaixo delas, adicione um Contêiner do pack Modelo C4 para o etcd e um Repositório de objetos do pack Dados para o arquivo de WAL.
  5. Rotule as conexões: "writes :5000", "reads :5001", "WAL (sync)" contínua, "WAL (async)" tracejada, "leader key, TTL 30 s" do pg-b para o etcd e uma conexão tracejada do pg-a para o etcd para a corrida pela liderança.
  6. Adicione um bloco Markdown do pack Básico com RPO, RTO, orçamento de atraso e retenção de backups.

O exemplo usa 11 nós de diagrama e fica dentro do plano Free. Para manter o diagrama atualizado conforme o cluster muda, veja Diagramas de arquitetura que não envelhecem e, para o método geral, o exemplo de diagrama de design de sistemas.

Perguntas frequentes

Uma réplica de leitura é um backup? Não. Exclusões e migrations com erro também são replicadas. Backups com arquivamento de WAL permitem voltar a um momento anterior ao erro.

Replicação síncrona significa zero perda de dados? Para commits que um standby síncrono confirmou, sim, desde que o failover promova só esse standby. Commits aceitos enquanto nenhum standby síncrono estava conectado existem apenas no primário.

Por que três membros no etcd e não dois? O etcd precisa de maioria para aceitar escritas. Dois membros não aguentam a perda de nenhum; três aguentam a perda de um e mantêm a maioria de um lado de uma partição de rede.

Posso usar DNS em vez de um proxy para encontrar o primário? Sim, é o que os serviços gerenciados fazem. Mantenha TTLs de DNS curtos e garanta que os clientes não guardem as resoluções em cache por muito tempo; a AWS recomenda um TTL de cache de DNS da JVM de no máximo 60 segundos.

Com que frequência devemos testar o failover? Com regularidade suficiente para que o time confie nele, por exemplo um switchover planejado em toda janela de manutenção, mais um teste de falha não planejada em staging.

Comece grátis — um assento, sem cartão de crédito.

Artigos relacionados

← Voltar ao blog

Abra um canvas em um minuto

Entre com Google ou GitHub. Grátis para um assento. Faça upgrade quando a equipe entrar.