Quando o failover não é seguro: construindo PostgreSQL de alta disponibilidade sobre Kubernetes
(datadoghq.com)- Em clusters PostgreSQL baseados em k8s, um problema estrutural fazia com que, durante falhas de rede, o atraso de replicação (replication lag) se acumulasse e tornasse impossível um failover seguro
- A arquitetura anterior priorizava disponibilidade (availability) acima de durabilidade (durability), então o primário continuava recebendo gravações enquanto as réplicas ficavam para trás, até não restar nenhum candidato que pudesse ser promovido sem perda de dados
- Como solução, foi aplicada replicação síncrona (synchronous replication) aos candidatos de failover, com coordenação do gerenciador de alta disponibilidade open source Patroni
- Em um modelo híbrido de replicação, apenas os standbys do leader pool participam da replicação síncrona, enquanto as réplicas de leitura continuam assíncronas, equilibrando durabilidade e latência
- Apesar do custo de desempenho, como aumento de 53% na latência de escrita com o modo
remote_apply, foi possível alcançar failover automático seguro após validar 5 cenários de falha
O problema revelado no GameDay
- A Datadog realiza GameDays regularmente para encontrar antecipadamente falhas em sistemas e processos, aplicando carga intencional à plataforma para aprender como ela reage em condições reais
- Em um desses GameDays, uma falha de zona de disponibilidade (AZ) foi simulada no ambiente de staging, provocando latência de rede e expondo uma fraqueza na arquitetura PostgreSQL
- Os nós primários/writers de vários clusters PostgreSQL baseados em Kubernetes estavam rodando na AZ afetada
- Com o aumento repentino da latência de rede, o primário não conseguia mais se comunicar de forma estável com as réplicas, o atraso de replicação aumentava → as escritas emperravam → a aplicação passava a servir dados antigos
- Como não havia nenhuma réplica suficientemente atualizada, o failover não era seguro e o cluster praticamente parava
- Essa arquitetura funcionava bem em condições normais, mas em certos cenários de falha de rede ela priorizava disponibilidade em vez de durabilidade, sem uma rota segura de recuperação
- O primário continuava aceitando gravações mesmo enquanto a replicação atrasava, fazendo o atraso aumentar e deixando as réplicas ainda mais para trás
- Como resultado, não era possível promover um candidato a failover sem risco de perda de dados, e a única opção era esperar a latência diminuir e as réplicas alcançarem o primário
- O objetivo passou a ser tornar o failover automático e seguro, sem degradar mais do que o necessário as características de desempenho do PostgreSQL
Arquitetura de referência: PostgreSQL sobre Kubernetes
- O cluster PostgreSQL baseado em Kubernetes é composto por dois pools: leader pool e read replica pool; o PostgreSQL é um sistema de single-writer
- Com separação entre leitura e escrita, é possível escalar leituras sem sobrecarregar o líder, enquanto a latência de escrita se mantém previsível e estável
- O leader pool é composto por 1 nó writer ativo, responsável por todas as escritas, e 2 nós standby
- Os standbys não atendem tráfego da aplicação, mas podem ser promovidos em caso de falha do líder
- O read replica pool é formado por vários nós que atendem tráfego somente de leitura, otimizados para escalabilidade de leitura e isolamento de consultas, e intencionalmente excluídos do failover
O papel do Patroni e do ZooKeeper
- O Patroni gerencia replicação, failover e eleição de líder, usando o ZooKeeper como DCS (Distributed Configuration Store)
- O ZooKeeper armazena metadados como a chave/lock do líder atual, a configuração do cluster e o estado de replicação de cada membro, incluindo o LSN mais recente
- O Patroni usa essas informações para decidir promoções e rebaixamentos de forma conservadora, priorizando consistência dos dados acima de failovers agressivos
- Quando um novo nó entra no cluster, ele primeiro verifica no ZooKeeper se já existe um líder
- Se não houver líder, tenta obter a chave de liderança criando um znode temporário; o ZooKeeper garante que apenas um nó obtenha essa chave, impedindo a formação de múltiplos primários
- Se já houver líder, o nó se configura como réplica e inicia a replicação por streaming
- Em caso de partition de rede, uma réplica que perde conexão com o líder ou com o ZooKeeper não consegue verificar o estado do cluster, então o Patroni pausa ou rebaixa temporariamente esse nó
- Se o líder perde a conexão, o Patroni trabalha com o ZooKeeper para que apenas um standby elegível obtenha o lock de líder, garantindo failover controlado mesmo em falhas parciais de rede
- Quando a conexão é restaurada, se o antigo líder não conseguir readquirir o lock, ele se rebaixa para standby, evitando split brain
Por que não era possível fazer failover seguro
- No modelo single-writer, quando ocorre uma falha, o Patroni elege um novo líder entre os standbys saudáveis
- Para evitar perda de dados, o Patroni faz verificações de segurança antes da promoção; a principal é confirmar se o atraso de replicação está dentro do limite
maximum_lag_on_failover- Se o standby estiver atrasado em relação ao líder, promovê-lo pode causar perda ou inconsistência de dados
- No GameDay, quando o primário perdeu conexão, o atraso de replicação de todos os standbys ultrapassou o limite, então o Patroni corretamente recusou o failover
- O cluster ficou sem um primário seguro para escrita não por causa do Patroni, mas porque não existia um candidato seguro para promoção
Dois modos de replicação por streaming
- Na replicação por streaming, o líder envia continuamente às réplicas o write-ahead log (WAL) com todas as mudanças, e as réplicas aplicam o WAL localmente para manter a sincronização
-
Replicação assíncrona (padrão)
- O líder não espera confirmação das réplicas antes de fazer commit da transação
- Minimiza a latência de escrita e suporta alto throughput
- Porém, se o líder falhar, transações já commitadas no primário, mas ainda não replicadas, podem se perder durante a promoção
-
Replicação síncrona
- O líder espera confirmação de pelo menos 1 réplica antes de responder ao cliente
- Isso reduz muito o risco de pelo menos uma réplica ficar atrasada demais e oferece durabilidade mais forte ao só responder depois de confirmar que a transação commitada existe em outro nó
- Assim, aumenta a chance de o candidato a failover estar atualizado e poder ser promovido sem risco de divergência de dados
Configuração híbrida de replicação
- Para equilibrar durabilidade, latência e throughput, foi adotado um modelo híbrido de replicação
- Os nós standby do leader pool participam da replicação síncrona, e o líder só confirma a escrita após o ack do standby síncrono designado
- As read replicas continuam com replicação assíncrona, pois atendem apenas tráfego de leitura e não são alvo de failover, limitando a carga de replicação no leader pool
- Assim, garantias rigorosas de durabilidade são aplicadas apenas aos candidatos de failover, sem impor o mesmo custo de latência às réplicas de leitura
Ajustes em PostgreSQL e Patroni para failover seguro
- Para ativar a replicação síncrona, foram ajustados parâmetros tanto no PostgreSQL quanto no Patroni
-
Principais parâmetros ajustados
synchronous_mode: ativa replicação síncrona no Patroni; comtrue, o commit só ocorre após a confirmação do standby síncrono conformesynchronous_node_count(padrão false → true, gerenciado pelo Patroni, obrigatório)synchronous_node_count: número de nós standby síncronos, usado para gerar a listasynchronous_standby_names(padrão 1 → 1, gerenciado pelo Patroni, opcional)synchronous_mode_strict: força o modo síncrono estrito; comtrue, se não houver réplica disponível, as escritas são bloqueadas em vez de voltar para assíncrono (padrão false → true, gerenciado pelo Patroni, opcional)synchronous_commit: configuração de durabilidade de commit no PostgreSQL (padrão on → remote_apply, gerenciado pelo PostgreSQL, opcional)
- Depois da mudança, o líder só responde a transações após o standby síncrono confirmar que recebeu e aplicou os dados
Equilíbrio entre durabilidade e latência
- A replicação síncrona melhora a durabilidade, mas aumenta a latência de escrita porque o líder espera a confirmação do standby síncrono; sob carga contínua, isso também pode afetar o throughput
- O impacto de desempenho depende do número de standbys síncronos (
synchronous_node_count) e do nível de durabilidade configurado emsynchronous_commit -
Trade-offs por nível de durabilidade de
synchronous_commitremote_apply: espera a réplica gravar, flushar e reproduzir o WAL; oferece a consistência mais forte e a maior latênciaon(internamenteremote_flush): espera a réplica flushar o WAL em disco; oferece durabilidade forte, mas os dados ainda podem não estar legíveis no standbyremote_write: espera o WAL chegar ao cache do sistema operacional da réplica, não ao disco; tem latência menor, mas é vulnerável a crash do SOlocal: faz commit após flush no disco local sem esperar standby; não garante durabilidade entre nósoff: faz commit antes do flush local do WAL; menor latência e maior risco de perda de dados
Benchmark de desempenho da replicação síncrona
- Como cada commit passa a esperar confirmação de um ou mais standbys, a replicação síncrona adiciona latência; para quantificar isso, foi feito benchmark com o pgbench, ferramenta padrão de teste de carga do PostgreSQL (Patroni versão 3.2.1)
- Foi usado o conjunto de transações TPC-B, que simula uma mistura simples de leitura e escrita, medindo duas métricas
- Latência média: tempo médio de processamento por transação (ms)
- Transações por segundo (tps): número de transações concluídas, excluindo o tempo de estabelecimento de conexão
-
Parâmetros de teste
- Foram variados o fator de escala (tamanho do banco), número de clientes (tráfego concorrente), número de threads (CPU/paralelismo) e quantidade de transações (intensidade da carga) para simular condições parecidas com produção
- O modo de replicação síncrona com commit por quórum não foi testado neste benchmark
-
Resultados do benchmark
- Aumento da latência de escrita:
remote_apply53%,on46%,remote_write38%,local32% - Queda de throughput (tps):
remote_apply34%,on31%,remote_write27%,local23% remote_applyapresentou consistentemente a maior latência e o menor throughput por esperar a reprodução e aplicação do WAL na réplica, mas é o mais adequado para failover seguro por oferecer a consistência mais forte
- Aumento da latência de escrita:
-
Aplicação em produção
- Após o benchmark,
remote_applyfoi implantado em vários clusters com alta taxa de escrita, sem impacto significativo em latência ou throughput de escrita no nível da aplicação, mesmo sob carga contínua de produção - Para mitigar riscos de desempenho, o rollout foi gradual por datacenter e por camada de workload, com período de bake-in e monitoramento contínuo entre as etapas
- Exemplo: workloads de processamento de recursos com alto throughput continuaram operando sem atraso de processamento nem backlog downstream, apesar do aumento de latência de escrita no banco após ativar a replicação síncrona
synchronous_commitpode ser ajustado imediatamente sem downtime compatronictl edit-config, oferecendo flexibilidade para reduzir rapidamente a durabilidade do commit em workloads de throughput extremamente alto
- Após o benchmark,
Validação do failover com cenários de falha
- Foi verificado se a replicação síncrona e o controle estrito de failover realmente protegem a integridade dos dados, evitam split-brain e garantem recuperação automática
-
Cenário 1: perda de 1 standby síncrono
- Quando um standby síncrono é perdido, o Patroni tenta manter a replicação síncrona atribuindo outro standby elegível
- O Patroni no nó líder detecta conexões de streaming rompidas, travadas ou atrasadas usando
pg_stat_replication, e acompanha a participação das réplicas via ZooKeeper - Ele recalcula a lista de réplicas de streaming saudáveis e elegíveis e atualiza
synchronous_standby_namesconformesynchronous_node_count, mantendo a replicação síncrona ativa
-
Cenário 2: perda de todos os standbys síncronos
- O comportamento depende do valor de
synchronous_mode_strict -
Modo não estrito: prioridade para disponibilidade de escrita
- O Patroni esvazia
synchronous_standby_namese desativa temporariamente a replicação síncrona, permitindo que o líder continue aceitando escritas em modo assíncrono até que uma réplica saudável volte
- O Patroni esvazia
-
Modo estrito: bloqueio de escrita por segurança
- O Patroni define
synchronous_standby_namescomo*; mesmo sem um standby síncrono explícito, o PostgreSQL aceita e faz commit local das transações de escrita, mas bloqueia a resposta ao cliente até que uma réplica confirme o WAL - Quando uma réplica adequada capaz de participar da replicação síncrona se reconecta, o Patroni a designa como standby síncrono
- O Patroni define
- O comportamento depende do valor de
-
Cenário 3: indisponibilidade de todos os standbys e réplicas
- Se todas as réplicas estiverem indisponíveis e
synchronous_mode_strict = true, o PostgreSQL retém a confirmação das transações até que pelo menos 1 réplica elegível retorne - A consistência dos dados é preservada, mas a aplicação sofre indisponibilidade temporária de escrita
- Se todas as réplicas estiverem indisponíveis e
-
Cenário 4: falha do líder durante commit síncrono
- Trata-se de um caso limite em que o líder falha enquanto espera a confirmação do standby síncrono, antes de recebê-la
- Causas comuns incluem cancelamento da transação pelo cliente durante o commit, crash ou encerramento do processo PostgreSQL do líder e partition de rede durante a etapa de commit
- Se o PostgreSQL flushou o WAL localmente, mas falhou em replicá-lo ao standby, a transação não fica visível na réplica por falta de confirmação
- Se o líder cair antes de replicar o WAL ao standby síncrono e esse standby for promovido, a transação pode se perder e o histórico do antigo líder divergir do novo primário
- O antigo líder usa pg_rewind para identificar o ponto de divergência da timeline e rebobinar o diretório de dados para a timeline do novo primário, descartando alterações locais não replicadas antes de voltar ao cluster como standby
- Esse comportamento decorre do funcionamento interno do commit síncrono do PostgreSQL, não do Patroni, e destaca a necessidade de ajuste e monitoramento cuidadosos da configuração de quórum e do mecanismo de confirmação
-
Cenário 5: indisponibilidade do ZooKeeper
- Quando o ZooKeeper fica indisponível, o Patroni não consegue confirmar a liderança nem eleger um novo líder e passa a operar de forma conservadora para evitar inconsistência de dados
-
Com failsafe desativado
- Mesmo sem alcançar o ZooKeeper, o líder continua aceitando escritas se estiver acessível e todos os nós estiverem saudáveis, mas só até o TTL do lock do líder expirar
- Quando o tempo do loop de renovação da chave de líder passa e o lock não pode ser renovado, o Patroni rebaixa o líder e coloca o cluster em modo somente leitura
-
Com failsafe ativado
- Se o líder perde a conexão com o ZooKeeper, o Patroni verifica continuamente pela API REST se todos os membros do cluster continuam acessíveis
- Ele só continua aceitando escritas se todos os membros estiverem acessíveis; caso contrário, rebaixa para somente leitura
Failover e switchover manuais com replicação síncrona
- Além de failover e switchover automáticos baseados em health checks e coordenação via ZooKeeper, o Patroni também permite operações manuais com o comando
patronictl; como nem todos os standbys são candidatos válidos quando a replicação síncrona está ativa, há guardrails para proteger a integridade dos dados -
Failover para um standby assíncrono
patronictl failover: falha se o nó selecionado for assíncronopatronictl switchover: falha se o nó selecionado for assíncrono- Forçar failover para um nó assíncrono com replicação síncrona ativa contornaria as garantias de durabilidade e poderia causar perda de dados
-
Ao escolher um standby síncrono
patronictl failover: funciona, e o líder é trocado por um standby síncronopatronictl switchover: funciona, realizando uma transição graciosa entre líder e standby síncrono
- Depois de validar o comportamento dos vários modos de
synchronous_commite os guardrails do Patroni, a replicação síncrona foi ativada em clusters de produção com workloads de alta escrita, alta leitura e mistos, sem impacto mensurável em latência ou throughput - Se surgir algum problema, é possível voltar com segurança para replicação assíncrona sem downtime definindo
synchronous_mode: false
Por que não escolheram DRBD
- Na avaliação de alta disponibilidade, também foi considerado o sistema de replicação em nível de bloco DRBD (Distributed Replicated Block Device), que espelha entre servidores o volume inteiro, incluindo o diretório de dados do PostgreSQL e os arquivos WAL, criando uma réplica standby quase em tempo real
- O DRBD pode oferecer latência menor que a replicação por streaming nativa do PostgreSQL, mas exigiria uma mudança arquitetural significativa, incluindo nova infraestrutura, monitoramento e playbooks operacionais
- Considerando o ambiente maduro baseado em Kubernetes e o controle refinado da replicação síncrona do PostgreSQL, foi escolhida a replicação no nível do banco, por oferecer melhor visibilidade, flexibilidade e confiança operacional
Monitoramento da replicação síncrona
- Depois de ativar a replicação síncrona, o estado da replicação e a prontidão para failover passaram a ser monitorados de perto; dois sinais em especial ajudaram a manter a estabilidade em escala
-
Evento de espera SyncRep
- Ocorre quando o primário aguarda a confirmação do standby síncrono antes de concluir o commit e retornar o status; alguma espera é normal, mas esperas longas ou frequentes indicam problemas de desempenho na réplica ou latência de rede entre nós
- Importância: esperas prolongadas aumentam a latência de escrita e reduzem o throughput
- Itens monitorados: duração e frequência dos eventos de espera
SyncRepeWalSenderWaitForReply, coletados na métrica Datadogpostgresql.activity.waitsfiltrada pela tagwait_event:SyncRepe baseada internamente em consultas à tabelapg_stat_activity
-
Ausência de standby síncrono detectado
- Se o Patroni não detectar um standby síncrono saudável por um longo período, o cluster perde a capacidade de failover seguro
- Importância: sem standby síncrono, o failover fica vulnerável a perda de dados
- Critério de alerta: se
patroni_sync_standbypermanecer vazio, é disparado um alerta de saúde de alta disponibilidade (HA); esse sinal é coletado a partir de dados OpenMetrics, já que não há integração nativa da Datadog
- A replicação síncrona melhora a durabilidade, mas pode reduzir disponibilidade e desempenho quando as réplicas estão com problema ou inacessíveis; monitorar tempos de espera e disponibilidade dos standbys é essencial para manter disponibilidade e desempenho sob carga
Failover seguro desde a fase de projeto
- A falha simulada de AZ revelou uma fraqueza crítica da arquitetura PostgreSQL: as réplicas ficavam para trás em relação ao líder, forçando a escolha entre esperar a rede se recuperar ou aceitar divergência de dados, um trade-off inaceitável em produção
- Com a adoção de replicação síncrona baseada em Patroni e ajustes finos de durabilidade e latência, o failover passou a ser viável e seguro mesmo sob degradação de rede; benchmarks e simulações repetidas de falha confirmaram recuperação previsível sem prejuízo relevante de desempenho em escala
- Ao bloquear escritas durante falhas da replicação síncrona, o sistema expõe a falha explicitamente aos serviços a montante; diferente da replicação assíncrona, em que escritas podem se perder silenciosamente, isso permite reagir com retry, enfileiramento e outras estratégias, tornando o modo de falha mais visível e recuperável
- Como próximos passos, estão sendo explorados o modo de commit baseado em quórum e uma observabilidade mais profunda do estado da replicação
Ainda não há comentários.