Catálogo de padrões de sistemas distribuídos #1
Olá!
No texto de hoje irei comentar alguns pontos encontrados no livro Patterns of Distributed Systems, de Unmesh Joshi. Você também encontra uma versão resumida dos padrões aqui senão quiser ir atrás da obra original.
O livro usa as seguintes categorias: Data Replication, Data Partitioning, Distributed Time, Cluster Management, Communication Between Nodes como opções de classificação. O texto de hoje é sobre Data Replication.
Data Replication
Write-Ahead Log (ou Commit Log)
O problema que esse pattern resolve é quando é necessário manter o estado consistente de um servidor mesmo em caso de falhas. Ao invés de usar armazenamento externo, é possível assegurar a consistência do estado armazenando os comandos executados em um log append-only. Caso a máquina sofra uma interrupção, o estado armazenado na memória é perdido, porém, é possível replicar o estado re-executando os comandos do WAL.
Um uso prático disso é em banco de dados. Boa parte das soluções modernas (como o Postgres), usam o WAL. O mecanismo básico envolve armazenar todos os comandos SQL executados no banco. Em caso de falhas, basta executar os comandos novamente a partir do WAL. Já comentei sobre essa técnica anteriormente aqui.
Segmented Log
Imagine que um determinado sistema use um arquivo de log único. Uma boa prática relacionada a log é deletar registros mais antigos, para manter a saúde da busca e uma boa performance.
Porém, como fazer esse tipo de manipulação quando todos os logs são armazenados em um arquivo só?
Esse pattern é justamente para isso: divida o log em segmentos menores baseado em algum critério, geralmente por data. Qual o recorte de tempo vai variar de acordo com o volume, porém, uma regra geral simples de memorizar é: meses para volumes pequenos, dias para volumes médios e horas para volumes grandes.
Low-Water Mark
Como gerenciar o tamanho do WAL, para que não cresça indefinidamente? Afinal, é um arquivo que será escrito a cada comando executado no cluster.
Mesmo usando o pattern anterior, o WAL pode consumir todo o espaço em disco disponível. Para evitar isso, a sugestão é utilizar uma estrutura de dados adicional com um índice que aponta a partir de qual registro o log pode ser descartado.
Uma forma que eu vejo isso funcionando na prática é também em um banco de dados: uma thread em background pode ficar checando quais registros do WAL foram registrados em disco e marca quais podem ser removidos de forma segura após a checagem.
Leader and Followers
Antigamente chamado de Master/Slave. Por esse termo ter conotação com a escravidão, felizmente está caindo em desuso.
O problema aqui é: é necessário replicar dados entre múltiplas máquinas. Porém, ao mesmo tempo, é importante garantir uma certa consistência para os clientes. Quando os dados são atualizados em uma máquina, é necessário decidir como replicar isso corretamente, para que o cliente não lide com inconsistências.
Esse pattern é o jeito mais simples de fazer isso: eleja uma máquina como líder. Todas as operações de escrita são feitas por ela. Ao mesmo tempo, ela também é responsável por replicar esses dados para os demais seguidores.
Se pareceu o mecanismo de replicação de banco de dados, é porque é.
O grande trade-off desse mecanismo é: ele possui um ponto único de falha, pois, caso o líder caia, nenhum seguidor consegue operar com dados atualizados até eleger um novo líder (mais sobre isso no pattern Majority Quorum).
Heartbeat
Essa técnica resolve o seguinte problema: como saber dentro de um conjunto de máquinas em um cluster se todas estão vivas?
Caso seja necessário replicações ou ações corretivas para substituir uma máquina defeituosa, quanto antes ocorrer a detecção da falha, melhor.
É aqui que entra o Heartbeat. Ele funciona como um pulso cíclico (como uma batida de coração, daí o nome). O funcionamento se dá da seguinte forma: avisos de tempos em tempos (pode ser um comando simples como ping) que são enviados para a máquina controladora do cluster, para que a mesma tenha o controle de qual máquina está funcionando. Caso o pulso falhe, uma ação corretiva pode ser tomada.
O único ponto de atenção é que o delay de envio dos pulsos precisa ser maior que o tempo de latência entre as round-trips. O motivo disso é para não informar falsos negativos da vida da máquina para a máquina controladora em caso da comunicação estar demorando muito.
Majority Quorum
Em um sistema distribuído, mesmo em caso de falhas de algumas máquinas, os resultados da computação precisam ser disponibilizados. Porém, como saber qual dado é de fato válido? Como autorizar a replicação de um dado entre máquinas?
Uma das formas de se validar isso é criar um mecanismo de Quorum. Que é uma quantidade de máquinas que precisam concordar entre si. No caso de dentro de um conjunto de n máquinas, o Quorum é n / 2 + 1 (basicamente, a é uma fórmula para calcular o conceito de maioria). No caso de um cluster de 3 máquinas, o valor mínimo de máquinas que precisam concordar entre si é 2.
Ou seja, se essa quantidade de máquinas receber o mesmo dado igual e concordarem com o resultado, então será esse o dado que será considerado verdade e replicado para todos os servidores.
O MongoDB usa esse mecanismo para decidir como replicar dados, por exemplo.
High-Water Mark (ou Commit Index)
Utilizando um WAL, um único log na máquina não é o suficiente para garantir disponibilidade em um cluster. A solução então se torna replicar o WAL para as outras máquinas do conjunto de servidores e considerá-lo como fonte da verdade para o estado dos dados. Aqui podem ser usados os patterns já mencionados para controlar onde e como será feita a escrita nesse log (Líderes e Quorum).
Porém, o seguinte pode ocorrer:
- O líder pode falhar antes de escrever no WAL dos seguidores;
- A replicação do WAL aconteceu apenas para alguns seguidores e não para todos;
Devido a isso, o problema se torna: como saber qual é o registro recente correto dentro do WAL distribuído?
È aqui que entra o High-Water Mark. Usando um índice no arquivo de log do WAL que aponta qual o último registro que foi de fato replicado e que passou pelo Quorum. Essa marcação/data de corte tem que ser replicado pelo líder para todos os followers em todas as replicações para manter a consistência do cluster.
Só serão considerados dados válidos e retornados para os clientes os que estiverem antes da data de corte.
Generation Clock (aka Epoch)
O que acontece caso um líder que usa a estrutura de seguidores fique fora do ar por algum tempo? Pode ser por qualquer motivo, desde uma execução de um garbage collector até mesmo falhas pontuais de rede.
Após a recuperação da falha, o líder vai enviar solicitações de sincronização para os seguidores. Porém, o cluster já decidiu eleger um novo líder durante a falha do líder anterior. Isso pode acarretar em vários problemas, como detectar qual dado é válido e tudo mais.
A solução que esse pattern propõe é adicionar no WAL um valor atômico incremental que indica a geração (versão) do cluster. Esse número será incrementado a cada eleição de um novo líder.
Se um follower receber uma mensagem de um líder antigo, ele pode detectar isso verificando se o valor recebido é menor que o último que recebeu. Se for, ele pode ignorar aquela solicitação com segurança, pois agora a fonte da verdade é o líder novo.
Um ponto de atenção aqui é que a geração do valor que representa a versão precisa ser extremamente confiável dentro do cluster, como ser thread-safe para evitar race conditions.
Agora eu paro por aqui para que esse texto não fique muito grande.
Até!
Você gostou do conteúdo e gostaria de fazer mentoria comigo? Clique aqui e descubra como.