Cluster de Failover PostgreSQL + Patroni. Experiência de implementação

No artigo, contarei como abordamos a questão da tolerância a falhas do PostgreSQL, por que ela se tornou importante para nós e o que aconteceu no final.

Temos um serviço altamente carregado: 2,5 milhões de usuários em todo o mundo, mais de 50 mil usuários ativos todos os dias. Os servidores estão localizados na Amazone em uma região da Irlanda: mais de 100 servidores diferentes estão trabalhando constantemente, dos quais quase 50 estão com bancos de dados.

Todo o back-end é um grande aplicativo Java monolítico com estado que mantém uma conexão constante de websocket com o cliente. Quando vários usuários trabalham no mesmo quadro ao mesmo tempo, todos veem as alterações em tempo real, porque cada alteração é gravada no banco de dados. Temos cerca de 10 mil solicitações por segundo em nossos bancos de dados. No pico de carga no Redis, escrevemos solicitações de 80 a 100 mil por segundo.
Cluster de Failover PostgreSQL + Patroni. Experiência de implementação

Por que mudamos de Redis para PostgreSQL

Inicialmente, nosso serviço funcionava com o Redis, um armazenamento de chave-valor que armazena todos os dados na RAM do servidor.

Vantagens do Redis:

  1. Alta velocidade de resposta, porque tudo fica guardado na memória;
  2. Facilidade de backup e replicação.

Contras do Redis para nós:

  1. Não há transações reais. Tentamos simulá-los no nível de nosso aplicativo. Infelizmente, isso nem sempre funcionava bem e exigia escrever um código muito complexo.
  2. A quantidade de dados é limitada pela quantidade de memória. À medida que a quantidade de dados aumenta, a memória vai crescendo e, no final, vamos nos deparar com as características da instância selecionada, que na AWS requer parar nosso serviço para alterar o tipo de instância.
  3. É necessário manter constantemente um baixo nível de latência, porque. temos um número muito grande de pedidos. O nível de atraso ideal para nós é de 17 a 20 ms. Em um nível de 30-40 ms, obtemos respostas longas às solicitações de nosso aplicativo e degradação do serviço. Infelizmente, isso aconteceu conosco em setembro de 2018, quando uma das instâncias com Redis por algum motivo recebeu latência 2 vezes mais do que o normal. Para resolver o problema, interrompemos o serviço no meio do dia para manutenção não programada e substituímos a instância problemática do Redis.
  4. É fácil obter inconsistência de dados mesmo com pequenos erros no código e depois gastar muito tempo escrevendo código para corrigir esses dados.

Levamos em consideração os contras e percebemos que precisávamos mudar para algo mais conveniente, com transações normais e menos dependentes da latência. Realizou pesquisas, analisou muitas opções e escolheu o PostgreSQL.

Já estamos mudando para um novo banco de dados há 1,5 anos e movemos apenas uma pequena parte dos dados, então agora estamos trabalhando simultaneamente com Redis e PostgreSQL. Mais informações sobre os estágios de movimentação e troca de dados entre bancos de dados estão escritas em artigo do meu colega.

Quando começamos a mexer, nosso aplicativo trabalhava diretamente com o banco de dados e acessava o mestre Redis e PostgreSQL. O cluster PostgreSQL consistia em um mestre e uma réplica com replicação assíncrona. É assim que o esquema do banco de dados se parecia:
Cluster de Failover PostgreSQL + Patroni. Experiência de implementação

Implementando PgBouncer

Enquanto íamos mudando, o produto também se desenvolvia: aumentava o número de usuários e o número de servidores que funcionavam com PostgreSQL, e começamos a faltar conexões. O PostgreSQL cria um processo separado para cada conexão e consome recursos. Você pode aumentar o número de conexões até certo ponto, caso contrário, há uma chance de obter um desempenho de banco de dados abaixo do ideal. A opção ideal em tal situação seria escolher um gerenciador de conexões que ficará na frente da base.

Tínhamos duas opções para o gerenciador de conexões: Pgpool e PgBouncer. Mas o primeiro não suporta o modo transacional de trabalhar com o banco de dados, então escolhemos o PgBouncer.

Definimos o seguinte esquema de trabalho: nosso aplicativo acessa um PgBouncer, atrás do qual estão os mestres PostgreSQL, e atrás de cada mestre há uma réplica com replicação assíncrona.
Cluster de Failover PostgreSQL + Patroni. Experiência de implementação

Ao mesmo tempo, não podíamos armazenar toda a quantidade de dados no PostgreSQL e a velocidade de trabalho com o banco de dados era importante para nós, então começamos a fragmentar o PostgreSQL no nível do aplicativo. O esquema descrito acima é relativamente conveniente para isso: ao adicionar um novo shard PostgreSQL, basta atualizar a configuração do PgBouncer e o aplicativo pode trabalhar imediatamente com o novo shard.

Failover do PgBouncer

Este esquema funcionou até o momento em que a única instância do PgBouncer morreu. Estamos na AWS, onde todas as instâncias são executadas em hardware que morre periodicamente. Nesses casos, a instância simplesmente muda para um novo hardware e funciona novamente. Isso aconteceu com o PgBouncer, mas ficou indisponível. O resultado dessa queda foi a indisponibilidade do nosso serviço por 25 minutos. A AWS recomenda o uso de redundância do lado do usuário para tais situações, o que não foi implementado em nosso país naquele momento.

Depois disso, pensamos seriamente na tolerância a falhas dos clusters PgBouncer e PostgreSQL, porque uma situação semelhante poderia acontecer com qualquer instância em nossa conta da AWS.

Construímos o esquema de tolerância a falhas do PgBouncer da seguinte forma: todos os servidores de aplicativos acessam o Network Load Balancer, atrás do qual existem dois PgBouncers. Cada PgBouncer examina o mesmo mestre PostgreSQL de cada fragmento. Se uma falha de instância da AWS ocorrer novamente, todo o tráfego será redirecionado por meio de outro PgBouncer. O failover do Network Load Balancer é fornecido pela AWS.

Este esquema facilita a adição de novos servidores PgBouncer.
Cluster de Failover PostgreSQL + Patroni. Experiência de implementação

Criar um cluster de failover do PostgreSQL

Ao resolver esse problema, consideramos diferentes opções: failover autogravado, repmgr, AWS RDS, Patroni.

Roteiros auto-escritos

Eles podem monitorar o trabalho do mestre e, se falhar, promover a réplica ao mestre e atualizar a configuração do PgBouncer.

As vantagens dessa abordagem são a máxima simplicidade, porque você mesmo escreve os scripts e entende exatamente como eles funcionam.

Contras:

  • O mestre pode não ter morrido, em vez disso, pode ter ocorrido uma falha de rede. O failover, sem saber disso, promoverá a réplica ao mestre, enquanto o antigo mestre continuará funcionando. Como resultado, teremos dois servidores na função de mestre e não saberemos qual deles possui os últimos dados atualizados. Essa situação também é chamada de cérebro dividido;
  • Ficamos sem resposta. Em nossa configuração, o mestre e uma réplica, após a troca, a réplica sobe para o mestre e não temos mais réplicas, então temos que adicionar manualmente uma nova réplica;
  • Precisamos de monitoramento adicional da operação de failover, enquanto temos 12 shards PostgreSQL, o que significa que temos que monitorar 12 clusters. Com o aumento do número de shards, você também deve se lembrar de atualizar o failover.

O failover autogravado parece muito complicado e requer suporte não trivial. Com um único cluster PostgreSQL, esta seria a opção mais fácil, mas não é escalável, portanto não é adequado para nós.

Repmgr

Clusters do Replication Manager for PostgreSQL, que podem gerenciar a operação de um cluster PostgreSQL. Ao mesmo tempo, ele não possui um failover automático pronto para uso, portanto, para o trabalho, você precisará escrever seu próprio “invólucro” sobre a solução finalizada. Portanto, tudo pode ficar ainda mais complicado do que com scripts escritos por nós mesmos, então nem tentamos o Repmgr.

AWS RDS

Suporta tudo o que precisamos, sabe fazer backups e mantém um pool de conexões. Possui comutação automática: quando o mestre morre, a réplica se torna o novo mestre e a AWS altera o registro dns para o novo mestre, enquanto as réplicas podem estar localizadas em diferentes AZs.

As desvantagens incluem a falta de ajustes finos. Como exemplo de ajuste fino: nossas instâncias possuem restrições para conexões tcp, que, infelizmente, não podem ser feitas em RDS:

net.ipv4.tcp_keepalive_time=10
net.ipv4.tcp_keepalive_intvl=1
net.ipv4.tcp_keepalive_probes=5
net.ipv4.tcp_retries2=3

Além disso, o AWS RDS é quase duas vezes mais caro que o preço da instância regular, o que foi o principal motivo do abandono dessa solução.

Patroni

Este é um modelo python para gerenciamento do PostgreSQL com boa documentação, failover automático e código-fonte no github.

Vantagens do Patroni:

  • Cada parâmetro de configuração é descrito, fica claro como funciona;
  • O failover automático funciona imediatamente;
  • Escrito em python, e como nós mesmos escrevemos muito em python, será mais fácil para nós lidar com problemas e, quem sabe, até ajudar no desenvolvimento do projeto;
  • Gerencia totalmente o PostgreSQL, permite alterar a configuração em todos os nós do cluster de uma só vez e, se o cluster precisar ser reiniciado para aplicar a nova configuração, isso poderá ser feito novamente usando o Patroni.

Contras:

  • Não está claro na documentação como trabalhar com o PgBouncer corretamente. Embora seja difícil chamá-lo de menos, porque a tarefa do Patroni é gerenciar o PostgreSQL, e como serão as conexões com o Patroni já é um problema nosso;
  • Existem poucos exemplos de implementação do Patroni em grandes volumes, enquanto existem muitos exemplos de implementação do zero.

Como resultado, escolhemos o Patroni para criar um cluster de failover.

Processo de Implementação do Patroni

Antes do Patroni, tínhamos 12 shards PostgreSQL em uma configuração de um mestre e uma réplica com replicação assíncrona. Os servidores de aplicação acessaram os bancos de dados através do Network Load Balancer, atrás do qual estavam duas instâncias com PgBouncer, e atrás deles estavam todos os servidores PostgreSQL.
Cluster de Failover PostgreSQL + Patroni. Experiência de implementação

Para implementar o Patroni, precisávamos selecionar uma configuração de cluster de armazenamento distribuído. Patroni trabalha com sistemas de armazenamento de configuração distribuída, como etcd, Zookeeper, Consul. Temos apenas um cluster Consul completo no mercado, que funciona em conjunto com o Vault e não o usamos mais. Um ótimo motivo para começar a usar o Consul para o propósito a que se destina.

Como a Patroni trabalha com a Consul

Temos um cluster Consul, que consiste em três nós, e um cluster Patroni, que consiste em um líder e uma réplica (no Patroni, o mestre é chamado de líder do cluster e os escravos são chamados de réplicas). Cada instância do cluster Patroni envia constantemente informações sobre o estado do cluster para o Consul. Portanto, do Consul você sempre pode descobrir a configuração atual do cluster Patroni e quem é o líder no momento.

Cluster de Failover PostgreSQL + Patroni. Experiência de implementação

Para conectar o Patroni ao Consul, basta estudar a documentação oficial, que diz que você precisa especificar um host no formato http ou https, dependendo de como trabalhamos com o Consul, e o esquema de conexão, opcionalmente:

host: the host:port for the Consul endpoint, in format: http(s)://host:port
scheme: (optional) http or https, defaults to http

Parece simples, mas aqui começam as armadilhas. Com o Consul, trabalhamos com uma conexão segura via https e nossa configuração de conexão ficará assim:

consul:
  host: https://server.production.consul:8080 
  verify: true
  cacert: {{ consul_cacert }}
  cert: {{ consul_cert }}
  key: {{ consul_key }}

Mas isso não funciona. Na inicialização, o Patroni não consegue se conectar ao Consul, porque tenta passar pelo http de qualquer maneira.

O código-fonte do Patroni ajudou a lidar com o problema. Ainda bem que está escrito em python. Acontece que o parâmetro do host não é analisado de forma alguma e o protocolo deve ser especificado no esquema. É assim que o bloco de configuração de trabalho para trabalhar com o Consul se parece para nós:

consul:
  host: server.production.consul:8080
  scheme: https
  verify: true
  cacert: {{ consul_cacert }}
  cert: {{ consul_cert }}
  key: {{ consul_key }}

modelo de consul

Então, escolhemos o armazenamento para a configuração. Agora precisamos entender como o PgBouncer mudará sua configuração ao alterar o líder no cluster Patroni. Não há resposta para essa pergunta na documentação, porque. lá, em princípio, o trabalho com PgBouncer não é descrito.

Em busca de uma solução, encontramos um artigo (infelizmente não me lembro do título) onde estava escrito que o Сonsul-template ajudou muito no emparelhamento do PgBouncer e do Patroni. Isso nos levou a investigar como o Consul-template funciona.

Descobriu-se que o Consul-template monitora constantemente a configuração do cluster PostgreSQL no Consul. Quando o líder muda, ele atualiza a configuração do PgBouncer e envia um comando para recarregá-lo.

Cluster de Failover PostgreSQL + Patroni. Experiência de implementação

Uma grande vantagem do template é que ele é armazenado como código, então ao adicionar um novo shard basta fazer um novo commit e atualizar o template automaticamente, suportando o princípio Infrastructure as code.

Nova arquitetura com Patroni

Como resultado, obtivemos o seguinte esquema de trabalho:
Cluster de Failover PostgreSQL + Patroni. Experiência de implementação

Todos os servidores de aplicativos acessam o balanceador → há duas instâncias do PgBouncer por trás dele → em cada instância, o Consul-template é iniciado, que monitora o status de cada cluster Patroni e monitora a relevância da configuração do PgBouncer, que envia solicitações ao líder atual de cada aglomerado.

teste manual

Executamos esse esquema antes de lançá-lo em um pequeno ambiente de teste e verificamos o funcionamento da comutação automática. Eles abriram o quadro, mexeram no adesivo, e nesse momento “mataram” o líder do cluster. Na AWS, isso é tão simples quanto desligar a instância por meio do console.

Cluster de Failover PostgreSQL + Patroni. Experiência de implementação

O adesivo voltou em 10 a 20 segundos e, novamente, começou a se mover normalmente. Isso significa que o cluster Patroni funcionou corretamente: mudou o líder, enviou as informações para o Сonsul e o Сonsul-template imediatamente pegou essas informações, substituiu a configuração do PgBouncer e enviou o comando para recarregar.

Como sobreviver sob alta carga e manter o tempo de inatividade mínimo?

Tudo funciona perfeitamente! Mas há novas perguntas: como funcionará sob alta carga? Como lançar tudo em produção com rapidez e segurança?

O ambiente de teste no qual conduzimos o teste de carga nos ajuda a responder à primeira pergunta. É completamente idêntico à produção em termos de arquitetura e gerou dados de teste que são aproximadamente iguais em volume à produção. Decidimos apenas “matar” um dos mestres do PostgreSQL durante o teste e ver o que acontece. Mas antes disso, é importante verificar o rolamento automático, pois neste ambiente temos vários shards PostgreSQL, assim teremos excelentes testes de scripts de configuração antes da produção.

Ambas as tarefas parecem ambiciosas, mas temos o PostgreSQL 9.6. Podemos atualizar imediatamente para 11.2?

Decidimos fazer isso em 2 etapas: primeiro atualize para 11.2 e depois inicie o Patroni.

Atualização do PostgreSQL

Para atualizar rapidamente a versão do PostgreSQL, use a opção -k, em que hard links são criados em disco e não há necessidade de copiar seus dados. Em bases de 300-400 GB, a atualização leva 1 segundo.

Temos muitos fragmentos, então a atualização precisa ser feita automaticamente. Para fazer isso, escrevemos um playbook Ansible que lida com todo o processo de atualização para nós:

/usr/lib/postgresql/11/bin/pg_upgrade 
<b>--link </b>
--old-datadir='' --new-datadir='' 
 --old-bindir=''  --new-bindir='' 
 --old-options=' -c config_file=' 
 --new-options=' -c config_file='

É importante observar aqui que antes de iniciar o upgrade, você deve realizá-lo com o parâmetro --verificarpara garantir que você pode atualizar. Nosso script também faz a substituição de configs durante o upgrade. Nosso script foi concluído em 30 segundos, o que é um excelente resultado.

Lançamento Patroni

Para resolver o segundo problema, basta olhar a configuração do Patroni. O repositório oficial tem um exemplo de configuração com initdb, que é responsável por inicializar um novo banco de dados quando você inicia o Patroni pela primeira vez. Mas como já temos um banco de dados pronto, simplesmente removemos esta seção da configuração.

Quando começamos a instalar o Patroni em um cluster PostgreSQL já existente e a executá-lo, nos deparamos com um novo problema: ambos os servidores começaram como líderes. Patroni não sabe nada sobre o estado inicial do cluster e tenta iniciar ambos os servidores como dois clusters separados com o mesmo nome. Para resolver este problema, você precisa excluir o diretório com dados no escravo:

rm -rf /var/lib/postgresql/

Isso precisa ser feito apenas no escravo!

Quando uma réplica limpa é conectada, o Patroni cria um líder de basebackup e o restaura na réplica e, em seguida, alcança o estado atual de acordo com os logs do wal.

Outra dificuldade que encontramos é que todos os clusters PostgreSQL são nomeados main por padrão. Quando cada cluster não sabe nada sobre o outro, isso é normal. Mas quando você deseja usar o Patroni, todos os clusters devem ter um nome exclusivo. A solução é alterar o nome do cluster na configuração do PostgreSQL.

teste de carga

Lançamos um teste que simula a experiência do usuário em boards. Quando a carga atingiu nosso valor médio diário, repetimos exatamente o mesmo teste, desligamos uma instância com um líder PostgreSQL. O failover automático funcionou como esperávamos: o Patroni mudou o líder, o Consul-template atualizou a configuração do PgBouncer e enviou um comando para recarregar. De acordo com nossos gráficos no Grafana, ficou claro que há atrasos de 20 a 30 segundos e uma pequena quantidade de erros dos servidores associados à conexão com o banco de dados. Esta é uma situação normal, tais valores são aceitáveis ​​para o nosso failover e são definitivamente melhores do que o tempo de inatividade do serviço.

Trazendo Patroni para a produção

Com isso, elaboramos o seguinte plano:

  • Implantar Consul-template nos servidores PgBouncer e iniciar;
  • Atualizações do PostgreSQL para a versão 11.2;
  • Altere o nome do cluster;
  • Iniciando o Cluster Patroni.

Ao mesmo tempo, nosso esquema nos permite fazer o primeiro ponto quase a qualquer momento, podemos remover cada PgBouncer do trabalho por sua vez e implantar e executar consul-template nele. Então nós fizemos.

Para implantação rápida, utilizamos o Ansible, pois já testamos todos os playbooks em um ambiente de teste, e o tempo de execução do script completo foi de 1,5 a 2 minutos para cada shard. Poderíamos distribuir tudo por vez para cada fragmento sem interromper nosso serviço, mas teríamos que desligar cada PostgreSQL por vários minutos. Neste caso, os usuários cujos dados estão neste fragmento não poderiam funcionar totalmente neste momento, e isso é inaceitável para nós.

A saída dessa situação foi a manutenção planejada, que ocorre a cada 3 meses. Esta é uma janela para trabalho agendado, quando encerramos completamente nosso serviço e atualizamos nossas instâncias de banco de dados. Faltava uma semana para a próxima janela e decidimos apenas esperar e nos preparar mais. Durante o tempo de espera, nos protegemos adicionalmente: para cada fragmento PostgreSQL, criamos uma réplica sobressalente em caso de falha em manter os dados mais recentes e adicionamos uma nova instância para cada fragmento, que deve se tornar uma nova réplica no cluster Patroni, para não executar um comando para excluir dados. Tudo isso ajudou a minimizar o risco de erro.
Cluster de Failover PostgreSQL + Patroni. Experiência de implementação

Reiniciamos nosso serviço, tudo funcionou como deveria, os usuários continuaram trabalhando, mas nos gráficos notamos uma carga anormalmente alta nos servidores Consul.
Cluster de Failover PostgreSQL + Patroni. Experiência de implementação

Por que não vimos isso no ambiente de teste? Esse problema ilustra muito bem que é preciso seguir o princípio Infraestrutura como código e refinar toda a infraestrutura, desde os ambientes de teste até a produção. Caso contrário, é muito fácil obter o problema que temos. O que aconteceu? O Consul apareceu primeiro na produção e depois nos ambientes de teste, como resultado, nos ambientes de teste, a versão do Consul era maior do que na produção. Apenas em um dos lançamentos, um vazamento de CPU foi resolvido ao trabalhar com consul-template. Portanto, simplesmente atualizamos o Consul, resolvendo assim o problema.

Reinicie o cluster Patroni

No entanto, tivemos um novo problema, do qual nem suspeitávamos. Ao atualizar o Consul, simplesmente removemos o nó Consul do cluster usando o comando consul leave → Patroni se conecta a outro servidor Consul → tudo funciona. Mas quando atingimos a última instância do cluster Consul e enviamos o comando consul leave para ela, todos os clusters Patroni simplesmente reiniciaram e nos logs vimos o seguinte erro:

ERROR: get_cluster
Traceback (most recent call last):
...
RetryFailedError: 'Exceeded retry deadline'
ERROR: Error communicating with DCS
<b>LOG: database system is shut down</b>

O cluster Patroni não conseguiu recuperar informações sobre seu cluster e foi reiniciado.

Para encontrar uma solução, contatamos os autores do Patroni por meio de um problema no github. Eles sugeriram melhorias em nossos arquivos de configuração:

consul:
 consul.checks: []
bootstrap:
 dcs:
   retry_timeout: 8

Conseguimos replicar o problema em um ambiente de teste e testamos essas opções lá, mas infelizmente não funcionaram.

O problema ainda permanece sem solução. Pretendemos tentar as seguintes soluções:

  • Use Consul-agent em cada instância de cluster Patroni;
  • Corrija o problema no código.

Entendemos onde ocorreu o erro: o problema provavelmente é o uso do timeout padrão, que não é substituído pelo arquivo de configuração. Quando o último servidor Consul é removido do cluster, todo o cluster Consul trava por mais de um segundo, por isso, o Patroni não consegue obter o status do cluster e reinicia completamente o cluster inteiro.

Felizmente, não encontramos mais erros.

Resultados do uso do Patroni

Após o lançamento bem-sucedido do Patroni, adicionamos uma réplica adicional em cada cluster. Agora, em cada cluster, há uma aparência de quorum: um líder e duas réplicas, para rede de segurança em caso de divisão do cérebro durante a troca.
Cluster de Failover PostgreSQL + Patroni. Experiência de implementação

Patroni trabalha na produção há mais de três meses. Durante esse tempo, ele já conseguiu nos ajudar. Recentemente, o líder de um dos clusters morreu na AWS, o failover automático funcionou e os usuários continuaram trabalhando. Patroni cumpriu sua principal tarefa.

Um pequeno resumo do uso do Patroni:

  • Facilidade de alterações de configuração. Basta alterar a configuração em uma instância e ela será puxada para todo o cluster. Se uma reinicialização for necessária para aplicar a nova configuração, o Patroni informará você. O Patroni pode reiniciar todo o cluster com um único comando, o que também é muito conveniente.
  • O failover automático funciona e já conseguiu nos ajudar.
  • Atualização do PostgreSQL sem tempo de inatividade do aplicativo. Você deve primeiro atualizar as réplicas para a nova versão e, em seguida, alterar o líder no cluster Patroni e atualizar o líder antigo. Nesse caso, ocorre o teste necessário de failover automático.

Fonte: habr.com

Adicionar um comentário