DEV Community

Breno Ferreira
Breno Ferreira

Posted on

Dados distribuídos - Replicação de dados

Parte da série sobre o resumo do livro Designing Data Intensive Apps.

Até aqui, falamos sobre alguns temas gerais sobre bancos de dados: Sistemas de dados, Modelos de Dados, Armazenamento de Dadose Serialização de Dados.

Agora, é hora de começarmos a falar sobre dados distribuídos e quais são os desafios inerentes à essa prática. Quando queremos garantir escalabilidade e confiabilidade do nosso sistema, é inevitável que em algum momento, nossos dados estejam distribuídos em mais de um lugar. Caso contrário, basta uma falha de acesso ao servidor de banco de dados e a aplicação para de funcionar. Se isso for inaceitável para seu negócio, aceite o fato de que algum nivel de computação distribuída vai existir.

Em uma arquitetura web típica, é comum ter um cluster de servidores web respondendo à requisições e um proxy reverso que as distribui aos nós do cluster. Essa arquitetura é fácil de implementar por que os nós do cluster não compartilham nada entre si (Shared Nothing Architecture). Como servidores web também não armazenam estado (chamados stateless web servers), caso um nó do cluster falhe, basta tirá-lo do cluster, enviar as requisições para os nós que ainda funcionam e adicionar um nó novo. Ter escalabilidade horizontal assim não é dificil.

Em um cluster de banco de dados, ainda é possível usar essa Shared-Nothing Architecture, ou seja, não compartilhamos nenhum recurso computacional. Porém, vai ser inevitável compartilhar uma coisa nos nós do cluster de banco de dados: os dados. Podemos distribuir os dados em vários servidores de um cluster de duas formas: Replicação e Particionamento. Repare que essas técnicas não são exclusivas. Pode haver dados particionados e replicados ao mesmo tempo. Nesse post iremos abordar o tema de replicação de dados. Particionamento de dados fica para um próximo post.

Implementar replicação de dados não é uma tarefa trivial que envolve alguns desafios e que existem algumas estratégias já conhecidas para resolver os problemas que podem aparecer.

Estratégias de replicação de dados

Toda vez que algum dado é escrito no banco, é necessário ter uma cópia desses dados nas outras réplicas do banco. Um cliente que envia uma requisição de escrita (ex: INSERT, UPDATE ou DELETE) para algum nó no cluster, o cluster deve seguir alguma estratégia para replicar esse dado escrito nos outros nós. As estratégias mais comuns de replicação são: Replicação com líder único, Replicação com múltiplos líderes e replicação sem líder.

Replicação de líder único

A estratégia mais comum para se garantir disponibilidade em caso de falha. Nessa estratégia, o cluster sempre elege um líder que será o único responsável por aceitar requisições de escrita. Requisições de leitura são aceitas por qualquer nó, líder ou réplica.

Em uma operação de escrita, o nó líder commita a escrita e envia uma resposta ao cliente. Em seguida, de maneira assíncrona, é enviado aos nós replicas a mesma operação para que o mesmo dado seja escrito nos outros nós e manter todas as réplicas consistentes.

A escrita nas réplicas também pode ser feita de maneira síncrona, com o cliente recebendo a resposta da requisição somente depois da escrita ter sido replicada em todos os nós. Isso evita alguns problemas que veremos depois, mas a performance é bem ruim e pode causar um enorme gargalo caso escritas sejam muito frequentes e principalmente se algum dos nós falhar. É mais comum que se a replicação for síncrona, que apenas um dos nós seja replicado sincronamente e os outros de forma assíncrona.

O líder é eleito pelo cluster usando algum tipo de algoritmo de consenso como Paxosou Raft.

Replicação com múltiplos líderes

Essa é uma estratégia mais comum em ambientes multi-datacenter. Por exemplo, caso exista versões da aplicação rodando em mais de um datacenter diferente, para acesso mais rápido em diferentes regiões (Latam e América do Norte por exemplo). É possível ter um cluster em multiplas regiões e ter um líder por região. Os líderes que commitou o dado replica a operação de escrita com os líderes das outras regiões, que por sua vez, enviam os dados para seus respectivos nós replicas.

Replicação sem líder

Nesse caso o cliente envia a requisição para varios nós. Como não há um líder coordenando que garante a escrita dos dados e a replicação dos dados escritos no banco, é necessário utilizar uma técnica chamada Quorum, que consiste basicamente em ter confirmação de uma maioria dos nós no cluster. Uma operação em um cluster com N nós, para ser considerada bem sucedida, deve ser confirmada por no mínimo K nós. O número K é o que chamamos de Quorum. Esse número K pode ser diferente para operações de leitura e escrita, ou ser igual para ambas operações, desde que esse número satisfaça a seguinte condição:

K_leitura + K_escrita > N

Voce pode definir o valor de K como sendo (N + 1) / 2, arredondado para cima. Então caso haja 4 nós no cluster, as operações teriam que ser confirmadas por um Quorum de ao menos 3 nós.

Esse tipo de replicação sem lider é conhecido como Dynamo-Style Replication. Não confundir com o Amazon DynamoDB que usa replicação com líder único

Desafios

A partir do momento que voce tem mais de uma instancia do seu servidor de banco de dados rodando, diga adeus aos confortos da computação monolítica e abrace a dor e sofrimento da computação distribuída.

Os maiores problemas que costumam aparecer quando há mais de uma réplica dos dados são relacionados a consistência dos dados e, no caso de estratégias com líder, uma falha e recuperação do líder.

Recuperação de falha de um líder se dá da seguinte forma:

  1. Detectar uma falha do líder: Geralmente detecta-se uma falha quando o servidor para de responder e ocorrem timouts. Uma falha pode acontecer por diversas razões como crash do sistema ou falta de energia por exemplo. Em alguns casos o servidor não falhou, mas houve falha da rede e ele fica incomunicável por um tempo. É impossível saber na hora qual a razão da falha, somente que ela ocorreu. Por essa razão, definir um valor de timeout pode ser um pouco complicado. Um timeout muito curto pode causar failovers desnecessários, e um timeout muito longo pode causar um tempo maior para recuperar o sistema de uma falha.
  2. Escolha de um novo líder: Para eleger um novo líder, usa-se algum algoritmo de consenso distribuído como Paxos ou Raft para que todos os nós entrem em acordo sobre qual dos nós réplicas irá tornar-se o novo líder.
  3. Reconfigurar o sistema e passar a usar o novo líder: os clientes agora precisarão se comunicar com o novo líder eleito. Caso o antigo líder volte a funcionar, ele deve detectar a existencia desse novo líder e passar a funcionar como uma réplica, caso contrário pode acontecer o que chamamos de split-brain e isso pode causar conflitos e corrupção dos dados.

Outro problema com dados distribuídos é na consistência dos dados. Como a replicação dos dados não é imediata, e os clientes lêem os dados de réplicas que podem ainda não ter os dados mais atuais que foram escritos pelo líder, pode ser bem comum haver o que chamamos de atraso de replicação, ou Replication Lag.

No exemplo da imagem acima, o usuário faz um INSERT de um dado e o líder envia as requisições de replicação. Como qualquer requisição de rede, uma pode ser mais rápida que a outra, com menor latencia e tempo de resposta menor, então a requisição de replicação no nó Replica 1 é mais rápida do que no nó réplica 2. Porém, o cliente tenta ler o dado que acabou de ser inserido enviando uma requisição para a Réplica 2 (já que leituras podem ser feitas em qualquer nó), porém a requisição chega antes da Replica 2 receber a requisição de replicação do insert, e logo, ela ainda não tem o dado e envia uma resposta com zero resultados.

Uma possível solução para esse problema é ter um controle mais fino sobre a conexão do cliente com o banco de dados para garantir que, quando o cliente ler algum dado que ele próprio tiver escrito, realizar requisição de leitura com o líder e não com réplicas. Essa técnica é conhecida como Read Your Own Writes. Isso garante que pelo menos o usuário vai ler o que ele mesmo escreveu, mas a leitura não é garantida para outros usuários. Por exemplo: em uma rede social, o usuário consegue ler seus próprios comentários, mas pode haver algumas eventuais inconsistências com os comentários dos outros.


Outro problema pode acontecer é Usuario 1 inserir um valor, e o Usuário 2 ter leituras inconsistentes por causa de conexão com réplicas diferentes que ainda não estão 100% consistentes. No caso acima, um cliente 1 insere um valor e esse dado é replicado. O cliente 2 então tenta ler o dado escrito em uma réplica que já teve esse dado replicado em sua base e retorna o valor. Num momento seguinte, o mesmo cliente tenta ler o dado de novo, mas dessa vez em uma réplica que ainda não tem o valor replicado, então ela responde um resultado vazio. Isso pode causar confusão pro usuário pois uma hora ele vê uma coisa, e logo depois não vê mais.

Monotonic Reads é uma garantia que o banco de dados pode prover para que esse tipo de anomalia não ocorra. Ela garante que o usuário ao ler um dado x no tempo t, continuará lendo esse dado no futuro e não lerá uma versão antiga de x em tempo t1. Uma possível implementação pode ser definir a replica de conexão baseado no ID do usuario. Assim, o cliente conecta sempre na mesma réplica. Caso a replica caia, a requisição é redirecionada para outra réplica.


Ambientes de replicação sem líder são otimizados para aplicações com uma tolerância maior à consistência eventual, pois a probabilidade de valores desatualizados é maior (pois não há um líder como uma fonte de consistência). Garantia de consistência geralmente requer transações distribuídas ou algoritmos de consenso distribuído, que são operações com custo alto de performance.

Em casos de falha de algum nó, após ele voltar a funcionar ele deve receber os dados que foram perdidos enquanto ele estava fora do ar. Isso pode acontecer de duas maneiras:

Read repair: o cliente, quando faz uma requisição de leitura pra varios nós, caso receba um valor desatualizado de um nó, envia uma requisição de atualização para o nó que enviou o dado desatualizado.

Processo anti-entropia: um processo que fica continuamente monitorando dados inconsistentes e realiza as atualizações necessárias copiando dados de uma réplica pra outra.

Outro problema que pode ocorrer também é quando, em cenários multi-líder ou sem líder, há escritas concorrentes e é necessário detectar e corrigir dados conflitantes.

Uma estratégia é o que é chamado de Última Escrita Vence (Last Write Wins). Porém, como veremos em posts futuros, determinar ordem de eventos no tempo em ambientes distribuídos não é tão simples. Então pode acontecer de decidir-se por um dado que não é o mais recente, ou até mesmo detectar eventos concorrentes quando na verdade não há concorrencia, mas sim problemas com relógios distribuídos.

Nas linhas de pesquisa mais recentes sobre resolução de conflitos existem alguns algoritmos que permitem uma resolução mais inteligente e automática: Conflict-free replicated datatypes (CRDTs),
Mergeable Persistent Data Structures e
Operational Transformations.

Top comments (0)