postgresql postgres ha high availability alta disponibilidade
Um fato infeliz na computação é que falhas acontecem, sejam elas por hardware, firmware ou software, e até mesmo por erro humano. Por causa disso, precisamos trabalhar para que elas não causem perdas de dados, inconsistência, indisponibilidade ou qualquer outra consequência negativa ao nosso negócio e aplicação.
Outro tipo de evento que causa indisponibilidade é a manutenção planejada. Ela é necessária para atualizações de sistema operacional, de serviços como o próprio PostgreSQL, mudanças na topologia do ambiente e em outros momentos.
Nosso objetivo é, portanto, que nosso serviço esteja "sempre" no ar. Ou, de forma mais clara e objetiva, que a indisponibilidade seja mínima, tanto quando causadas por manutenções planejadas quanto aquelas causadas por falhas imprevistas. Ou seja, queremos atender um RTO (Recovery Time Objective) baixo.
As técnicas, recursos e ações que tomamos para lidar com as falhas são variadas e incluem monitoramento, manutenção preventiva, redundância, checksums, backups, replicações, heartbeats, proxies, EDAC, entre outras. Vamos ver como algumas podem ser encaixadas em uma topologia coerente e confiável para minimizar o tempo de indisponibilidade.
- Em um cenário simples e em condições normais, a aplicação se comunica com o PostgreSQL (primário). Qualquer ambiente importante também deve ter um mecanismo de backup configurado com arquivamento.
- Quando uma falha acontece, o serviço torna-se indisponível. A causa da falha é irrelevante neste momento, mas dois possíveis cenários emergem e precisam ser descritos. No primeiro, (a), a máquina de banco de dados não é afetada, mas sim a comunicação entre a aplicação e o PostgreSQL (como com um roteamento errado, perda de rede por qualquer uma das máquinas, falta de energia em roteadores intermediários…). No segundo, (b), a comunicação não é afetada, mas a máquina e seu respectivo serviço ficam indisponíveis (como em uma falta de energia, queda do serviço, falha de segmentação…).
- O primeiro grande problema em resolver essa situação rapidamente é que a causa da indisponibilidade pode ser tão variada e com uma solução tão igualmente variada, incerta e longa, que a melhor solução não é investigar a falha e tratá-la, mas sim descartar esse ambiente (2) e criar outro ambiente (3) através da restauração de um backup. Essa ação pode ser preparada previamente, testada e validada desde a arquiteturação do ambiente, dando garantias de tempo máximo de indisponibilidade ao seu negócio. A causa raiz da falha é, então, deixada para ser investigada em um post mortem.
(1) (2) (3) ┌─────────┐ ┌─────────┐ ┌─────────┐ ╔═╡Aplicação╞═╗ ╔═╡Aplicação╞═╗ ╔═╡Aplicação╞═╗ ║ └─────────┘ ║ ║ └─────────┘ ║ ║ └─────────┘ ║ ╚═════════════╝ ╚═════════════╝ ╚═════════════╝ ↓ ↓ ↓ │ X │ │ (a) X │ ↓ X ↓ ┌──────────┐ ┌──────────┐ ┌──────────┐ ╔═╡PostgreSQL╞═╗ ╔═╡XXX(b)XXXX╞═╗ ╔═╡PostgreSQL╞═╗ ║ └──────────┘ ╟→┈┈┐ ║ └──────────┘ ║ ┌┈┈→╢ └──────────┘ ║ ╚══════════════╝ ┊ ╚══════════════╝ ┊ ╚══════════════╝ ╏ ┊ ┊ ╏ ╏ ┊ ┊ ╏ ╏ ↓ ↑ ╏ ╏ ┏━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┷━━━┓ ╏ ╏ ┃ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┃ ╏ ╏ ┃→└┤…├┘→└┤…├┘→└┤…├┘→└┤…├┘→└┤…├┘→└┤…├┘→└┤…├┘→┃ ╏ ╏ ┠──↑─────↑─────↑────↑─────↑──↑──────↑───↑───┨ ╏ ╏ ┃ ╔═══════╗ ╔═══════╗ ╔═══════╗ ╔═══════╗ ┃ ╏ ╏ ┃ ╠══╗ ╔══╣ ╠══╗ ╔══╣ ╠══╗ ╔══╣ ╠══╗ ╔══╣ ┃ ╏ ┗╍╍╍╍╍╍╸┃……╠══╝ ╚══╣…╠══╝ ╚══╣…╠══╝ ╚══╣…╠══╝ ╚══╣……┃╺╍╍╍╍╍╍┛ ┃ ╠═════╗ ║ ╠═════╗ ║ ╠═════╗ ║ ╠═════╗ ║ ┃ ┃ ╚═════╩═╝ ╚═════╩═╝ ╚═════╩═╝ ╚═════╩═╝ ┃ ┃ ┌──────────────────┐ ┃ ┗━━━━━━━━━━━┥Servidor de backup┝━━━━━━━━━━━━┛ └──────────────────┘
Com a restauração do backup em um ambiente novo, conseguimos diminuir a indisponibilidade que poderia levar de horas a dias (por exemplo, para troca de peças no servidor) a alguns minutos no melhor caso. Contudo, o pior caso dessa mesma restauração pode também levar horas a dias, dependendo do tamanho da instância e dos recursos do novo ambiente, como a velocidade de rede e I/O dele. Então, apesar dela ser vital para tratarmos casos extremos, não podemos depender da restauração completa ser iniciada e finalizada completamente durante a indisponibilidade.
A solução é iniciar a restauração do backup antes da falha acontecer e manter essa restauração continuamente próxima do primário. Assim, quando a indisponibilidade acontecer, essa restauração pode ser finalizada instantaneamente, gerando um novo PostgreSQL pronto para aceitar conexões. Como visto antes, isso é conhecido como uma réplica.
- Em situações normais, o primário é usado para backups e arquivamento de WAL; e a réplica fica em contínua restauração do WAL através a conexão de replicação e, possivelmente, da recuperação de segmentos arquivados.
- Quando uma falha acontece, o primário fica indisponível, mas a réplica está pouco atrás do primário em replicação de WAL.
- Assim, podemos fazer a promoção da réplica como novo primário, ajustando o apontamento da aplicação, rotinas de backup, arquivamento de segmentos de WAL, monitoramento e todas as outras atividades desempenhadas pelo primário, que agora serão feitas pela réplica promovida.
(1) (2) (3) ╔═══════════╗ ╔═══════════╗ ╔═══════════╗ ║ Aplicação ║ ║ Aplicação ║ ║ Aplicação ║───────────────┐ ╚═══════════╝ ╚═══════════╝ ╚═══════════╝ │ │ X X │ │ X X │ │ X X │ ↓ ↓ ↓ ↓ ┌──────────┐ ┌─────────┐ ┌──────────┐ ┌─────────┐ ┌──────────┐ ┌──────────┐ ╔═╡ Primário ╞═╗ ╔═╡ Réplica ╞═╗ ╔═╡ XXXXXXXX ╞═╗ ╔═╡ Réplica ╞═╗ ╔═╡ XXXXXXXX ╞═╗ ╔═╡ Primário ╞═╗ ║ └──────────┘ ╟→┈┈┬┈┈→╢ └─────────┘ ║ ║ └──────────┘ ╟→XXXXX→╢ └─────────┘ ║ ║ └──────────┘ ╟→XXXXX→║ └──────────┘ ╟→┐ ╚══════════════╝ ┊ ╚═════════════╝ ╚══════════════╝ X ╚═════════════╝ ╚══════════════╝ ╚══════════════╝ ┊ ╏ ┊ X ╏ ┊ ╏ ┊ X ╏ ┊ ╏ ↓ ↓ ╏ ↓ ┏━━━━━━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━┓ ┏━━━━━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━┓ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┷━━┓ ┃ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┃ ┃ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┃ ┃ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┃ ┃→└┤…├┘→└┤…├┘→└┤…├┘→└┤…├┘→└┤…├┘→└┤…├┘→└┤…├┘→┃ ┃→└┤…├┘→└┤…├┘→└┤…├┘→└┤…├┘→└┤…├┘→└┤…├┘→└┤…├┘→┃ ┃→└┤…├┘→└┤…├┘→└┤…├┘→└┤…├┘→└┤…├┘→└┤…├┘→└┤…├┘→┃ ┠──↑─────↑─────↑────↑─────↑──↑──────↑───↑───┨ ┠──↑─────↑─────↑────↑─────↑──↑──────↑───↑───┨ ┠──↑─────↑─────↑────↑─────↑──↑──────↑───↑───┨ ┃ ╔═══════╗ ╔═══════╗ ╔═══════╗ ╔═══════╗ ┃ ┃ ╔═══════╗ ╔═══════╗ ╔═══════╗ ╔═══════╗ ┃ ┃ ╔═══════╗ ╔═══════╗ ╔═══════╗ ╔═══════╗ ┃ ┃ ╠══╗ ╔══╣ ╠══╗ ╔══╣ ╠══╗ ╔══╣ ╠══╗ ╔══╣ ┃ ┃ ╠══╗ ╔══╣ ╠══╗ ╔══╣ ╠══╗ ╔══╣ ╠══╗ ╔══╣ ┃ ┃ ╠══╗ ╔══╣ ╠══╗ ╔══╣ ╠══╗ ╔══╣ ╠══╗ ╔══╣ ┃ ┃……╠══╝ ╚══╣…╠══╝ ╚══╣…╠══╝ ╚══╣…╠══╝ ╚══╣……┃ ┃……╠══╝ ╚══╣…╠══╝ ╚══╣…╠══╝ ╚══╣…╠══╝ ╚══╣……┃ ┃……╠══╝ ╚══╣…╠══╝ ╚══╣…╠══╝ ╚══╣…╠══╝ ╚══╣……┃ ┃ ╠═════╗ ║ ╠═════╗ ║ ╠═════╗ ║ ╠═════╗ ║ ┃ ┃ ╠═════╗ ║ ╠═════╗ ║ ╠═════╗ ║ ╠═════╗ ║ ┃ ┃ ╠═════╗ ║ ╠═════╗ ║ ╠═════╗ ║ ╠═════╗ ║ ┃ ┃ ╚═════╩═╝ ╚═════╩═╝ ╚═════╩═╝ ╚═════╩═╝ ┃ ┃ ╚═════╩═╝ ╚═════╩═╝ ╚═════╩═╝ ╚═════╩═╝ ┃ ┃ ╚═════╩═╝ ╚═════╩═╝ ╚═════╩═╝ ╚═════╩═╝ ┃ ┃ ┌──────────────────┐ ┃ ┃ ┌──────────────────┐ ┃ ┃ ┌──────────────────┐ ┃ ┗━━━━━━━━━━━┥Servidor de backup┝━━━━━━━━━━━━┛ ┗━━━━━━━━━━━┥Servidor de backup┝━━━━━━━━━━━━┛ ┗━━━━━━━━━━━┥Servidor de backup┝━━━━━━━━━━━━┛ └──────────────────┘ └──────────────────┘ └──────────────────┘
Algumas falhas são claras e visíveis, como um serviço que caiu e a mensagem de erro foi automaticamente enviada por e-mail para o administrador do sistema. Outras falhas são silenciosas, como falta de energia no data center. Para o primeiro caso, podemos tomar ações reativas, ou seja, atuar quando recebemos a notificação. Mas no segundo caso, precisamos tomar ações ativas até para detectar a falha, seguidas de outras ações para mitigá-la.
- Tais atividades de detecção de falhas e indisponibilidade são chamadas de heartbeat ou health check, pois continuamente enviam mensagens e aguardam respostas de saúde. Se as mensagens (A) não chegam ao destino, não são processadas pelo serviço ou a resposta não retorna ao remetente dentro de uma janela de tempo (timeout), então considera-se que o serviço está indisponível (B).
- E nesse momento, uma série de ações é iniciada, que pode incluir notificações, fencing (isolamento dos componentes que falharam), promoções de réplica, reconfiguração de serviços de proxies, DNS, LDAP, movimentação de VIPs, montagem de sistemas de arquivos, reinício de serviços e muitas outras.
(1) (2) ╔═══════════╗ ╔═══════════╗ ║ Aplicação ║ ║ Aplicação ║───────────────┐ ╚═══════════╝ ╚═══════════╝ │ │ X │ │ X │ │ X │ ↓ ↓ ↓ ┌──────────┐ ┌─────────┐ ┌──────────┐ ┌──────────┐ ╔═╡ Primário ╞═╗ ╔═╡ Réplica ╞═╗ ╔═╡ XXXXXXXX ╞═╗ ╔═╡ Primário ╞═╗ ║ └──┤pg-1├──┘ ╟→┈┈┈┈┈→╢ └─┤pg-2├──┘ ║ ║ └──┤pg-1├──┘ ╟→XXXXX→║ └──┤pg-2├──┘ ║ ╚══════════════╝ ╚═════════════╝ ╚══════════════╝ ╚══════════════╝ ↑ (A) ↑ ↑ (B) ↑ └───────┘ └─XXXXX─┘
Após a detecção da falha, precisamos tomar ações para que o serviço esteja acessível novamente com tempo mínimo de indisponibilidade. Essa etapa é chamada de chaveamento, failover (quando é acionado pela detecção de uma falha) ou switchover (quando é acionado sem falha, por exemplo para testes ou manutenções planejadas nos servidores). O chaveamento pode ser automático ou manual.
Chaveamento automático é aquele em que o sistema detecta e tenta restabelecer a disponibilidade sem interação humana. Ele tem a vantagem de manter a indisponibilidade mínima em muitos casos, mas a desvantagem de poder causar maior dano e maior indisponibilidade em alguns casos específicos. Por exemplo, se o primário ficou indisponível por alta carga externa (ou carga normal ou erro de programação da aplicação ou ataque externo), então a réplica vai ser transformada em novo primário, que irá sofrer o mesmo problema do primário anterior, ficando também indisponível. Nesse momento, o seu ambiente não tem mais réplicas, mas tem dois primários indisponíveis, ambos com transações recentes. A recuperação nesse cenário, então, deve ser feita manualmente e com cuidado, primeiro atenuando a carga, então restabelecendo o primário com mais transações e então recriando o outro como nova réplica. Mesmo um sistema com instanciação automática de novas máquinas e criação de novas réplicas não resolveria o cenário, mas sim causaria um consumo maior de recursos (e talvez maior custo) por se manter em um ciclo de contínua criação de réplicas e promoção como novos primários, sem reestabelecer a disponibilidade.
Chaveamento manual é aquele em que o sistema detecta e avisa o administrador, que irá analisar o cenário e executar as ações de chaveamento caso decida que essa seria a melhor estratégia. Ele tem a vantagem de ser mais simples de implementar e mais confiável no geral pois envolve menos componentes, mas tem a desvantagem de que a indisponibilidade irá depender do tempo de resposta e experiência do administrador. Como a maior parte das causas de indisponibilidades reais é simples, como uma interrupção temporária de rede, o reinício de um serviço auxiliar, por exemplo, e pode ser resolvida mais rapidamente que o chaveamento, este causaria maior indisponibilidade e maior incerteza sobre a saúde final do ambiente.
A topologia de cada ambiente define quais ações precisam ser executadas. Por exemplo, um ambiente com um proxy entre a aplicação e o banco de dados precisa atualizar as regras do proxy no chaveamento; outros ambientes não precisariam. Um ambiente com backups sendo obtidos da primeira réplica precisa reconfigurar a ferramenta de backup para apontar para a nova primeira réplica, como outro exemplo. Um ambiente com VIP acompanhando o primário, precisa mover aquele VIP de uma máquina para outra.
Uma lista longa, mas sempre incompleta, de algumas das ações mais comuns que são tomadas quando uma indisponibilidade é detectada e o chaveamento é feito:
- Envio de notificação ao administrador do sistema
- Isolamento dos componentes falhos
- Promoção da réplica, tornando-a um novo primário
- Reconfiguração da aplicação
- Reconfiguração da ferramenta de backup
- Reconfiguração da ferramenta de monitoramento
- Criação de novas réplicas
- Reconfiguração do cascateamento das réplicas
- Reconfiguração de proxies entre a aplicação e o PostgreSQL
- Reconfiguração de scripts agendados
- Reconfiguração de registros de DNS que apontam para os IPs do primário e da réplica
- Movimentação de IPs virtuais, ou VIPs
- Mudanças de regras de roteamento
- Mudanças de regras de firewall
- Montagem de sistemas de arquivos
- …
Considerando o chaveamento automático, quando o primário (pg-1) sofre uma falha de rede, que pode afetar apenas o próprio host (1) ou todas as comunicações (2), uma sequência de ações será executada para promover a réplica (pg-2) e manter a disponibilidade para a aplicação.
(1) (2) ╔═══════════╗ ╔═══════════╗ ║ Aplicação ║ ║ Aplicação ║ ╚═══════════╝ ╚═══════════╝ │ │ │ │ │ │ X ↓ ↓ X ┌──────────┐ ┌─────────┐ ┌──────────┐ X ┌─────────┐ ╔═╡ XXXXXXXX ╞═╗ ╔═╡ Réplica ╞═╗ ╔═╡ Primário ╞═╗ X ╔═╡ Réplica ╞═╗ ║ └──┤pg-1├──┘ ╟→┈┈┈┈┈→╢ └─┤pg-2├──┘ ║ ║ └──┤pg-1├──┘ ╟→┈┈X┈┈→║ └──┤pg-2├─┘ ║ ╚══════════════╝ ╚═════════════╝ ╚══════════════╝ X ╚═════════════╝ ↑ ↑ ↑ X ↑ └───────┘ └───X───┘
No caso de uma falha do host (1), a comunicação de rede do host se mantém saudável. Nesse caso, as ações podem ser:
- Isolamento do primário anterior (pg-1)
- Promoção da réplica, tornando-a um novo primário
- Reconfiguração da aplicação para acessar o novo primário (pg-2)
(1) ╔═══════════╗ ╔═══════════╗ ║ Aplicação ║ ║ Aplicação ║───────────────┐ ╚═══════════╝ ╚═══════════╝ │ │ │ │ │ │ │ ↓ ↓ ↓ ┌──────────┐ ┌─────────┐ ┌──────────┐ ┌──────────┐ ╔═╡ XXXXXXXX ╞═╗ ╔═╡ Réplica ╞═╗ ╔═╡ XXXXXXXX ╞═╗ ╔═╡ Primário ╞═╗ ║ └──┤pg-1├──┘ ╟→┈┈┈┈┈→╢ └─┤pg-2├──┘ ║ ║ └──┤pg-1├──┘ ╟→ ║ └──┤pg-2├──┘ ║ ╚══════════════╝ ╚═════════════╝ ╚══════════════╝ ╚══════════════╝ ↑ ↑ ↑ ↑ └───────┘
Contudo, em (2) a comunicação entre os hosts falha, mas ambos estão no ar. Esse cenário é conhecido como particionamento de rede. Com isso, o mecanismo de heartbeat percebe a indisponibilidade mas não tem como diferenciar entre esse caso e o caso (1) de falha de hosts.
Então ele irá tomar um de dois caminhos:
- (a) OU cada host considera que o outro está disponível e, portanto, ele próprio deve se isolar para evitar inconsistências.
- (b) OU cada host considera que o outro está indisponível e, portanto, ele próprio deve tomar ações para disponibilizar o serviço.
No primeiro caso (a), o ambiente sofre a indisponibilidade que gostaríamos de evitar, já que ambos os hosts irão baixar os próprios serviços. No segundo caso (b), ambos os hosts terão o serviço como primário (pg-1 mantém o seu serviço e pg-2 promove a sua réplica) e podem notificar a aplicação dessa disponibilidade. Esse segundo cenário (b) é chamado split brain e irá causar inconsistência nos dados, já que transações serão processadas pelos dois hosts independentemente, que não poderão ser conciliadas posteriormente exceto com descarte dos dados de um host e reprocessamento das transações.
(2) (a) (b) ╔═══════════╗ ╔═══════════╗ ╔═══════════╗ ║ Aplicação ║ ║ Aplicação ║ ║ Aplicação ║───────────────┐ ╚═══════════╝ ╚═══════════╝ ╚═══════════╝ │ │ │ │ │ │ │ │ X X │ X │ ↓ X X ↓ X ↓ ┌──────────┐ X ┌─────────┐ ┌──────────┐ X ┌─────────┐ ┌──────────┐ X ┌──────────┐ ╔═╡ Primário ╞═╗ X ╔═╡ Réplica ╞═╗ ╔═╡ XXXXXXXX ╞═╗ X ╔═╡ XXXXXXX ╞═╗ ╔═╡ Primário ╞═╗ X ╔═╡ Primário ╞═╗ ║ └──┤pg-1├──┘ ╟→┈┈X┈┈→╢ └─┤pg-2├──┘ ║ ║ └──┤pg-1├──┘ ╟→┈┈X┈┈→╢ └─┤pg-2├──┘ ║ ║ └──┤pg-1├──┘ ╟→┈┈X┈┈→║ └──┤pg-2├──┘ ║ ╚══════════════╝ X ╚═════════════╝ ╚══════════════╝ X ╚═════════════╝ ╚══════════════╝ X ╚══════════════╝ ↑ X ↑ ↑ X ↑ ↑ X ↑ └───X───┘ └───X───┘ └───X───┘
A solução que evita o split brain e que permite que a disponibilidade seja mantida mesmo com failover automático para N falhas é usar um algoritmo de consenso baseado em quorum.
Para isso, é necessário ter uma quantidade ímpar de hosts participantes na rede, representada como 2N+1, sendo N o número de falhas que o sistema distribuído deve suportar. Por exemplo, um sistema simples que suporte uma falha (N=1) deve ter três nós (2*N+1 = 2*1+1 = 3).
Nesse exemplo, o primário (pg-1) replica para dois outros hosts (pg-2 e pg-a) e um heartbeat é mantido entre os três. Quando um particionamento de rede acontece separando uma das máquinas das outras duas, todas seguem um mesmo algoritmo:
- (A) Se eu estou na partição sem quorum (número de participantes é menor que a metade do total), então executo ações que baixam todos os serviços (primário ou réplica) e me isolam do resto do ambiente.
- (B) Se eu estou na partição com quorum (número de participantes é maior que a metade do total), então executo ações que sobem todos os serviços e que apontam o resto do ambiente para eles.
Primário isolado pelo particionamento da rede:
(2) (c) (d) ╔═══════════╗ ╔═══════════╗ ╔═══════════╗ ║ Aplicação ║ ║ Aplicação ║ ║ Aplicação ║───────────────┐ ╚═══════════╝ ╚═══════════╝ ╚═══════════╝ │ │ │ │ │ │ │ │ │ X X │ ↓ ↓ X X ↓ ┌──────────┐ ┌─────────┐ ┌──────────┐ X ┌─────────┐ ┌──────────┐ X ┌──────────┐ ╔═╡ Primário ╞═╗ ╔═╡ Réplica ╞═╗ ╔═╡ Primário ╞═╗ X ╔═╡ Réplica ╞═╗ ╔═╡ XXXXXXXX ╞═╗ X ╔═╡ Primário ╞═╗ ║ └──┤pg-1├──┘ ╟→┈┈┈┬┈→╢ └─┤pg-2├──┘ ║ ║ └──┤pg-1├──┘ ╟→┈X ┬┈→╢ └─┤pg-2├──┘ ║ ║ └──┤pg-1├──┘ ╟→┈X ┌┈←╢ └──┤pg-2├──┘ ║ ╚══════════════╝ ┊ ╚═════════════╝ ╚══════════════╝ X ┊ ╚═════════════╝ ╚══════════════╝ X ┊ ╚══════════════╝ ↑ ┊ ↑ ↑ X ┊ ↑ ↑ X ┊ ↑ └────┊──┤ (A) └──X ┊──┤ (B) (A) └──X ┊──┤ (B) ┊ │ X ┊ │ X ┊ │ ┊ ↓ ┌─────────┐ X ┊ ↓ ┌─────────┐ X ┊ ↓ ┌─────────┐ ┊ ╔═╡ Réplica ╞═╗ X ┊ ╔═╡ Réplica ╞═╗ X ┊ ╔═╡ Réplica ╞═╗ └┈→╢ └─┤pg-a├──┘ ║ X └┈→╢ └─┤pg-a├──┘ ║ X └┈→╢ └─┤pg-a├──┘ ║ ╚═════════════╝ X ╚═════════════╝ X ╚═════════════╝
Réplica isolada pelo particionamento da rede:
(2) (c) (d) ╔═══════════╗ ╔═══════════╗ ╔═══════════╗ ║ Aplicação ║ ║ Aplicação ║ ║ Aplicação ║ ╚═══════════╝ ╚═══════════╝ ╚═══════════╝ │ │ │ │ │ │ │ │ │ ↓ ↓ ↓ ┌──────────┐ ┌─────────┐ ┌──────────┐ (B) ┌─────────┐ ┌──────────┐ (B) ┌─────────┐ ╔═╡ Primário ╞═╗ ╔═╡ Réplica ╞═╗ ╔═╡ Primário ╞═╗ ╔═╡ Réplica ╞═╗ ╔═╡ Primário ╞═╗ ╔═╡ Réplica ╞═╗ ║ └──┤pg-1├──┘ ╟→┈┈┈┬┈→╢ └─┤pg-2├──┘ ║ ║ └──┤pg-1├──┘ ╟→┈┈┈┬┈→╢ └─┤pg-2├──┘ ║ ║ └──┤pg-1├──┘ ╟→┈┈┈┬┈←╢ └─┤pg-2├──┘ ║ ╚══════════════╝ ┊ ╚═════════════╝ ╚══════════════╝ ┊ ╚═════════════╝ ╚══════════════╝ ┊ ╚═════════════╝ ↑ ┊ ↑ ↑ ┊ ↑ ↑ ┊ ↑ └────┊──┤ └────┊──┤ └────┊──┤ ┊ │ XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX ┊ ↓ ┌─────────┐ ┊ ↓ ┌─────────┐ ┊ ↓ ┌─────────┐ ┊ ╔═╡ Réplica ╞═╗ (A) ┊ ╔═╡ Réplica ╞═╗ (A) ┊ ╔═╡ XXXXXXX ╞═╗ └┈→╢ └─┤pg-a├──┘ ║ └┈→╢ └─┤pg-a├──┘ ║ └┈→╢ └─┤pg-a├──┘ ║ ╚═════════════╝ ╚═════════════╝ ╚═════════════╝
Dessa forma, é possível manter a disponibilidade automaticamente apesar de N falhas e sem arriscar causar inconsistência nos dados.
O uso de uma quantidade ímpar de nós permite que o tipo de falha seja identificada e a decisão do novo primário seja tomada de forma confiável. Contudo, se o primário anterior estava na partição sem quorum, a decisão de todos é que ele não será mantido como primário após o failover. Nesse cenário, não é suficiente apenas promover uma réplica e reapontar a aplicação; também é necessário garantir que o antigo primário esteja definitivamente isolado, caso contrário, uma falha parcial (intermitência da rede, perdas de pacotes, alto tempo de resposta…) fará com que os dois primários estejam acessíveis ao mesmo tempo.
O conjunto de ações e atividades envolvidas em garantir o isolamento de recursos que falharam é chamado de fencing (do inglês, significando cercar). Essas ações dependem do hardware e software envolvidos na arquitetura do cluster, mas algumas das mais comuns são:
- desligamento de energia da máquina, possivelmente da PDU do rack na qual a máquina se encontra
- desligamento de todas as portas de rede, através de um comando enviado aos switches
- desligamento da máquina por IPMI, ACPI, comando de controle ao virtualizador (KVM, VMware…) ou outra forma
- baixando o serviço do PostgreSQL do servidor
- matando todos os processos do PostgreSQL do servidor
- impedimento da comunicação através da porta do PostgreSQL, por reconfiguração do firewall
- reconfiguração do DNS, LDAP, proxies e connection poolers, removendo o host da lista de possibilidades
- …
A estratégia mais comum é através do uso de todos os mecanismos de fencing disponíveis, um seguido do outro. Isso é vital pois após uma falha que afeta o serviço do primário, é altamente provável que essa mesma falha afete também uma ou outra estratéfia de fencing (por exemplo, se a comunicação pela rede de aplicação cai, é possível que o comando ssh de baixar o serviço também não consiga alcançar a máquina, então é necessário usar uma rede administrativa para controlá-la por IPMI).
É apenas com o uso de múltiplas estratégias de fencing implementadas e completamente testadas que um ambiente de failover automático pode ser usado em produção.
A solução de usar uma quantidade ímpar de nós pode se tornar custosa quando os nós também são réplicas completas dos dados, como é necessário em um sistema share-nothing, como o PostgreSQL. Contudo, para fins de determinação de presença em quorum, não é necessário que todos os nós sejam participantes do serviço principal ofertado, mas apenas que participem do heartbeat e da votação de quorum. Com isso, podemos introduzir um ator mais simples no sistema, chamado de witness, ou testemunha. A testemunha, então, pode ser um componente não relacionado ao PostgreSQL que participa do quorum.
Por exemplo, a terceira máquina (pg-a) não fornece o serviço do PostgreSQL, mas é usada para que as outras possam diferenciar uma falha de particionamento de rede de uma falha de host.
(2) (c) (d) ╔═══════════╗ ╔═══════════╗ ╔═══════════╗ ║ Aplicação ║ ║ Aplicação ║ ║ Aplicação ║ ╚═══════════╝ ╚═══════════╝ ╚═══════════╝ │ │ │ │ │ │ │ │ │ ↓ ↓ ↓ ┌──────────┐ ┌─────────┐ ┌──────────┐ (B) ┌─────────┐ ┌──────────┐ (B) ┌─────────┐ ╔═╡ Primário ╞═╗ ╔═╡ Réplica ╞═╗ ╔═╡ Primário ╞═╗ ╔═╡ Réplica ╞═╗ ╔═╡ Primário ╞═╗ ╔═╡ Réplica ╞═╗ ║ └──┤pg-1├──┘ ╟→┈┈┈┈┈→╢ └─┤pg-2├──┘ ║ ║ └──┤pg-1├──┘ ╟→┈┈┈┈┈→╢ └─┤pg-2├──┘ ║ ║ └──┤pg-1├──┘ ╟→┈┈┈┈┈←╢ └─┤pg-2├──┘ ║ ╚══════════════╝ ╚═════════════╝ ╚══════════════╝ ╚═════════════╝ ╚══════════════╝ ╚═════════════╝ ↑ ↑ ↑ ↑ ↑ ↑ └───────┤ └───────┤ └───────┤ │ XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX ↓ ┌─────────┐ ↓ ┌─────────┐ ↓ ┌─────────┐ ╔═╡ Witness ╞═╗ (A) ╔═╡ Witness ╞═╗ (A) ╔═╡ Witness ╞═╗ ║ └─┤pg-a├──┘ ║ ║ └─┤pg-a├──┘ ║ ║ └─┤pg-a├──┘ ║ ╚═════════════╝ ╚═════════════╝ ╚═════════════╝
É possível usar mais de uma testemunha, contanto que o número de nós que podem fornecer o serviço seja maior ou igual ao tamanho do quorum. Isso porque se um particionamento de rede separar todas as testemunhas na maior partição e os nós de serviço na menor, o algoritmo de alta disponibilidade por concenso irá baixar o serviço e o nosso ambiente sofrerá uma indisponibilidade.
Uma tabela com essas quantidades para clusters de 1, 2 ou até N falhas:
falhas | nós | quorum | serviço | testemunhas |
---|---|---|---|---|
1 | 3 | 2 | 2 até 3 | 0 até 1 |
2 | 5 | 3 | 3 até 5 | 0 até 2 |
N | 2*N+1 | N+1 | N+1 até 2*N+1 | 0 até N |
Diversos componentes podem ser usados para compor o cluster de alta disponibilidade. Alguns componentes trazem funcionalidades importantes, como os exemplos a seguir.
O PostgreSQL traz um driver de conexão chamado libpq com diversas funcionalidades importantes. Ele é usado por outros drivers como psycopg, ODBC, libpqxx.
Podemos fornecer uma lista de hosts na string de conexão, dessa forma o driver tentará estabelecer a conexão com cada um deles na ordem que foram informados. Hosts inacessíveis serão ignorados, o que faz com que a aplicação acesse outro host nos momentos de indisponibilidade do host inicial.
Por exemplo, se a aplicação foi configurada com a string de conexão
postgresql://pg-1.local,pg-2.local/app
, ela irá conectar no host
pg-1
em tempos de normalidade (1), mas passar automaticamente a conectar no
host pg-2
quando o primeiro estiver inacessível (2).
(1) (2) ╔═══════════╗ ╔═══════════╗ ║ Aplicação ║ ║ Aplicação ║───────────────┐ ╚═══════════╝ ╚═══════════╝ │ │ X │ │ X │ │ X │ ↓ ↓ ↓ ┌──────────┐ ┌─────────┐ ┌──────────┐ ┌──────────┐ ╔═╡ Primário ╞═╗ ╔═╡ Réplica ╞═╗ ╔═╡ XXXXXXXX ╞═╗ ╔═╡ Primário ╞═╗ ║ └──┤pg-1├──┘ ╟→┈┈┈┈┈→╢ └─┤pg-2├──┘ ║ ║ └──┤pg-1├──┘ ╟→XXXXX→║ └──┤pg-2├──┘ ║ ╚══════════════╝ ╚═════════════╝ ╚══════════════╝ ╚══════════════╝
Mesmo quando os múltiplos hosts estão acessíveis, podemos filtrar qual será
usado para cada conexão com base na capacidade dele de receber transações de
escrita. Isso pode ser consultado com SHOW transaction_read_only
a qualquer
momento, mas a libpq
faz essa filtragem automaticamente quando fornecemos o
atributo target_session_attrs
.
-
Com a string de conexão
postgresql://pg-1.local,pg-2.local/app?target_session_attrs=read-write
, a aplicação receberá apenas conexões que aceitarem transações de leitura e escrita, ou seja, o primário, independentemente da ordem dos hosts na string. Eles serão testados em ordem, mas esperamos que apenas um seja primário; portanto, é ideal configurar o host mais provável de ser o primário no início da lista para atender transações de escrita (como aquelas iniciadas para tratar requisições POST, por exemplo). -
Com a string de conexão
postgresql://pg-2.local,pg-1.local/app?target_session_attrs=any
(ou omitindotarget_session_attrs
), a aplicação receberá qualquer conexão que seja estabelecida com sucesso. Como os hosts serão testados em ordem e a aplicação pode usar hosts somente leitura (como aquelas iniciadas para tratar requisições GET), é ideal colocar o primário no final da lista, assim a carga de leitura é direcionada para a primeira réplica acessível e o primário recebe menor carga.
(1) ╔═══════════╗ ┌─────║ Aplicação ║ │ ╚═══════════╝ │ │ │ ┌────────┴────────┐ │ │ (2) │ ↓ ↓ ↓ ┌──────────┐ ┌─────────┐ ╔═╡ Primário ╞═╗ ╔═╡ Réplica ╞═╗ ║ └──┤pg-1├──┘ ╟→┈┈┈┈┈→╢ └─┤pg-2├──┘ ║ ╚══════════════╝ ╚═════════════╝
Se a libpq
foi compilada com suporte ao LDAP, é possível fazer com que ela
consulte o serviço LDAP para receber os parâmetros de conexão com o banco de
dados. Dessa forma, se um chaveamento atualizar o registro LDAP sem reconfigurar
a aplicação, conseguimos redirecionar as conexões sem impactar a aplicação.
Usando o exemplo da documentação, se criarmos um registo com o seguinte LDIF:
version:1 dn:cn=app,dc=mycompany,dc=com changetype:add objectclass:top objectclass:device cn:mydatabase description:host=pg-2.local description:port=5432 description:dbname=app description:user=app_user
-
A aplicação se conecta usando a string
ldap://ldap.mycompany.com/dc=mycompany,dc=com?description?one?(cn=app)
, que recupera o campodescription
do registrocn=app,dc=mycompany,dc=com
. -
Esse campo traz todos os parâmetros que a
libpq
usará, então para estabelecer uma conexão com o servidor PostgreSQL.
╔══════╗ ║ LDAP ║ ╚══════╝ (1) │ │ ╔═══════════╗ ║ Aplicação ║───────────────┐ ╚═══════════╝ (2) │ X │ X │ X │ ↓ ↓ ┌──────────┐ ┌──────────┐ ╔═╡ XXXXXXXX ╞═╗ ╔═╡ Primário ╞═╗ ║ └──┤pg-1├──┘ ╟→XXXXX→║ └──┤pg-2├──┘ ║ ╚══════════════╝ ╚══════════════╝
O projeto ClusterLabs desenvolve e mantém um conjunto de ferramentas de gestão de cluster de alta disponibilidade que está disponível em todas as grandes distribuições Linux. Dentre elas, o Corosync e o Pacemaker têm destaque.
Enquanto o Corosync se encarrega da comunicação das mensagens de heartbeat, o Pacemaker cuida dos recursos e serviços configurados e toma ações sobre eles quando necessário, como fencing, promoção de réplicas, movimentação de VIPs e reconfiguração de proxies.
Esse conjunto de ferramentas pode ser usado para a orquestração de clusters de alta disponibilidade de qualquer tipo de serviço, como bancos de dados, servidores DNS, servidores LDAP e outros, assim como também pode ser usado para a gestão de recursos como VIPs, rotas, NATs, LUNs, LV/VG/PV, sistemas de arquivos distribuídos e outros. Também traz regras de afinidade e anti-afinidade de recursos, multi-instanciação de serviços, ordenação de ações, tomada de decisão em quorum e diversas outras.
Especificamente, sobre essas ferramentas está implementado o PAF - PostgreSQL Automatic Failover. Com ele, é possível montar um ambiente de failover automático com heartbeat, watchdog, quorum, fencing, STONITH e todos os outros pontos importantes.
Desenvolvido pela 2ndQuadrant, o repmgr é um gerenciador de cluster de alta disponibilidade com boa integração com o barman e capacidade de failover automático e fácil criação de nós testemunha.
Proxy leve e eficiente para multiplexação de conexões com três modos de operação (statement, transaction, session) e reconfiguração online. Muito usado com registros DNS que trazem diversos IPs ou com um proxy TCP (como o haproxy).
Proxy mais complexo e pesado, com capacidade de redirecionar e replicar comandos SQL para outros backends, balanceamento de carga, multiplexação de conexões e diversas outras funcionalidades.
Proxy TCP de alto desempenho, com health check específico (pgsql-check
).
Muito usado em conjunto com outros componentes.
- Configure a máquina pg-1 como primário arquivando para pg-a.
- Configure a máquina pg-2 como réplica da pg-1 e recuperando segmentos de pg-a.
-
Configure a máquina pg-a com uma ferramenta de backup (como o Barman) recebendo o arquivamento de WAL.
- Configure a ferramenta de backup para fazer backups periódicos.
- Configure a ferramenta de backup para fazer expurgo de backups e segmentos de WAL antigos.
- Configure um cluster de alta disponibilidade com failover manual (como usando o repmgr) entre as máquinas pg-1, pg-2 e pg-a, como um primário, uma réplica e uma testemunha, respectivamente.
Gabarito: