postgresql postgres ha high availability alta disponibilidade

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.

Problema 1: Alto tempo de investigação da causa raiz

  1. 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.
  2. 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…).
  3. 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┝━━━━━━━━━━━━┛
                           └──────────────────┘

Problema 2: Alto tempo de restauração 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.

  1. 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.
  2. 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.
  3. 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┝━━━━━━━━━━━━┛
            └──────────────────┘                          └──────────────────┘                          └──────────────────┘

Problema 3: Detecção da indisponibilidade

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.

  1. 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).
  2. 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─┘

Problema 4: Chaveamento do serviço

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:

Problema 5: Split brain

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:

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

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:

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.

Problema 6: Fencing

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:

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.

Problema 7: Custo de 2N+1 nós

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

Ferramentas

Diversos componentes podem ser usados para compor o cluster de alta disponibilidade. Alguns componentes trazem funcionalidades importantes, como os exemplos a seguir.

libpq

O PostgreSQL traz um driver de conexão chamado libpq com diversas funcionalidades importantes. Ele é usado por outros drivers como psycopg, ODBC, libpqxx.

1 Múltiplos hosts

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├──┘ ║
    ╚══════════════╝       ╚═════════════╝      ╚══════════════╝       ╚══════════════╝

2 Conexões de escrita ou apenas leitura

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.

  1. 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).
  2. Com a string de conexão postgresql://pg-2.local,pg-1.local/app?target_session_attrs=any (ou omitindo target_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├──┘ ║
    ╚══════════════╝       ╚═════════════╝

3 LDAP

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
  1. A aplicação se conecta usando a string ldap://ldap.mycompany.com/dc=mycompany,dc=com?description?one?(cn=app), que recupera o campo description do registro cn=app,dc=mycompany,dc=com.
  2. 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├──┘ ║
╚══════════════╝       ╚══════════════╝

PostgreSQL Automatic Failover (PAF)

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.

repmgr

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.

PgBouncer

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

Pgpool-II

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.

haproxy

Proxy TCP de alto desempenho, com health check específico (pgsql-check). Muito usado em conjunto com outros componentes.

Prática

Gabarito: