Olá a todos. Aqui é Vladislav Rodin. Neste momento, estou ensinando no portal OTUS cursos dedicados à arquitetura de software e arquitetura de software sob alta carga. Na véspera do início de um novo ciclo do curso decidi escrever um pequeno material autoral que gostaria de compartilhar com vocês.

Introdução
Devido ao fato de que um HDD pode executar apenas cerca de 400-700 operações por segundo (o que é incomparável com os rps típicos encontrados em um sistema sob alta carga), um banco de dados em disco clássico se torna um gargalo na arquitetura. Portanto, é necessário prestar atenção especial aos padrões de escalabilidade deste armazenamento.
Atualmente, existem 2 padrões de escalabilidade para o banco de dados: replicação e sharding. O sharding permite escalar a operação de gravação e, como consequência, reduzir o rps de gravação por servidor do seu cluster. A replicação faz o mesmo, mas com operações de leitura. Este padrão está dedicado a este artigo.
Replicação
Se olharmos para a replicação de maneira bem superficial, é algo simples: você tinha um servidor, onde os dados estavam armazenados, e então esse servidor não conseguia mais lidar com a carga de leitura desses dados. Você adiciona mais alguns servidores, sincroniza os dados em todos os servidores e o usuário pode ler de qualquer servidor do seu cluster.
Apesar da aparente simplicidade, existem várias maneiras de classificar diferentes implementações deste esquema:
- Por papéis no cluster (master-master ou master-slave)
- Por objetos transmitidos (baseado em linha, baseado em declaração ou misto)
- Pelo mecanismo de sincronização dos nós
Hoje, vamos nos concentrar exatamente no 3º ponto.
Como ocorre o commit da transação
Este tema não está diretamente relacionado à replicação; um artigo separado pode ser escrito sobre isso, porém, como a compreensão do mecanismo de commit da transação é essencial para a leitura posterior, permitam-me lembrar os pontos mais básicos. O commit da transação ocorre em 3 etapas:
- Gravação da transação no log do banco de dados.
- Aplicação da transação no mecanismo do banco de dados.
- Retorno da confirmação ao cliente sobre a aplicação bem-sucedida da transação.
Em diferentes bancos de dados, nuances podem surgir neste algoritmo: por exemplo, no mecanismo InnoDB do MySQL, existem 2 logs: um para replicação (binary log) e outro para manter ACID (undo/redolog), enquanto no PostgreSQL há um único log que desempenha ambas as funções (write ahead log = WAL). No entanto, o que foi apresentado é a concepção geral que permite ignorar essas nuances.
Replicação síncrona (sync)
Vamos adicionar ao algoritmo de commit da transação a lógica para replicar as mudanças recebidas:
- Gravação da transação no log do banco de dados.
- Aplicação da transação no mecanismo do banco de dados.
- Envio de dados para todas as réplicas.
- Recebendo a confirmação de todas as réplicas sobre a execução da transação nelas.
- Retorno da confirmação ao cliente sobre a aplicação bem-sucedida da transação.
Com essa abordagem, obtivemos uma série de desvantagens:
- o cliente espera pela aplicação das mudanças em todas as réplicas.
- à medida que aumentamos o número de nós no cluster, diminuímos a probabilidade de que a operação de gravação seja bem-sucedida.
Se com o 1º ponto tudo está mais ou menos claro, a razão do 2º ponto merece uma explicação. Se na replicação síncrona não obtermos resposta de pelo menos um nó, revertamos a transação. Assim, ao aumentar o número de nós no cluster, você aumenta a probabilidade de que a operação de gravação falhe.
Podemos esperar confirmação apenas de uma parte dos nós, por exemplo, de 51% (quorum)? Sim, podemos, no entanto, na versão clássica, é necessário receber a confirmação de todos os nós, pois é assim que podemos garantir a consistência total dos dados no cluster, o que é uma indiscutível vantagem desse tipo de replicação.
Replicação assíncrona (async)
Vamos modificar o algoritmo anterior. Enviaremos os dados para as réplicas "algum tempo depois", e "algum tempo depois" as mudanças serão aplicadas nas réplicas:
- Gravação da transação no log do banco de dados.
- Aplicação da transação no mecanismo do banco de dados.
- Retorno da confirmação ao cliente sobre a aplicação bem-sucedida da transação.
- Envio de dados para as réplicas e aplicação das mudanças por elas.
Essa abordagem leva ao resultado de que o cluster opera rapidamente, pois não mantemos o cliente esperando até que os dados cheguem às réplicas e sejam confirmados.
Mas a condição de enviar os dados para as réplicas "algum tempo depois" pode levar à perda de transações, especialmente de transações já confirmadas ao usuário; pois, se os dados não forem replicados a tempo, e a confirmação ao cliente sobre o sucesso da operação for enviada, mas o HDD do nó que recebeu as mudanças falhar, perderemos a transação, o que pode trazer consequências muito desagradáveis.
Replicação semi-síncrona (semisync)
Finalmente, chegamos à replicação semissíncrona. Esse tipo de replicação não é muito conhecido e não é tão comum, mas desperta um interesse considerável, pois pode combinar as vantagens da replicação síncrona e assíncrona.
Vamos tentar combinar as duas abordagens anteriores. Não vamos manter o cliente esperando, mas exigiremos que os dados sejam replicados:
- Gravação da transação no log do banco de dados.
- Aplicação da transação no mecanismo do banco de dados.
- Envio de dados para as réplicas.
- Recebimento de confirmação da réplica sobre a recepção das alterações (aplicadas elas serão "algum momento depois").
- Retorno da confirmação ao cliente sobre a aplicação bem-sucedida da transação.
Observe que, com esse algoritmo, a perda de uma transação ocorre apenas no caso de falha tanto do nó que realiza as alterações quanto do nó réplica. A probabilidade dessa falha é considerada baixa, e esses riscos são aceitos.
Mas, com essa abordagem, existe o risco de leituras fantasmas. Vamos imaginar o seguinte cenário: no passo 4, não recebemos confirmação de nenhuma réplica. Precisamos reverter essa transação, mas não retornamos a confirmação ao cliente. Como os dados foram aplicados no passo 2, entre o término do passo 2 e a reversão da transação, há uma lacuna de tempo, durante a qual transações paralelas podem ver essas alterações que não deveriam estar no banco de dados.
Replicação semissíncrona sem perda
Se pensarmos um pouco, podemos corrigir o problema das leituras fantasmas neste cenário, simplesmente trocando a ordem dos passos do algoritmo:
- Gravação da transação no log do banco de dados.
- Envio de dados da réplica.
- Recebimento de confirmação da réplica sobre a recepção das alterações (aplicadas elas serão "algum momento depois").
- Aplicação da transação no mecanismo do banco de dados.
- Retorno da confirmação ao cliente sobre a aplicação bem-sucedida da transação.
Agora, só comitamos as alterações se elas foram replicadas.
Saída
Como sempre, não existem soluções perfeitas, existe um conjunto de soluções, cada uma com suas vantagens e desvantagens e adequada para resolver diferentes classes de problemas. Isso é verdade também para a escolha do mecanismo de sincronização de dados de um banco de dados replicado. O conjunto de vantagens que a replicação semissíncrona oferece é sólido e interessante o suficiente para ser reconhecido como digna de atenção, apesar de sua baixa disseminação.
Isso é tudo. Até breve em !
Fonte: habr.com
