Catálogo de padrões de sistemas distribuídos #2

Adriano Croco
5 min readMay 14, 2024

Olá!

Esse texto faz parte de uma série. Você encontra os textos anteriores nos links abaixo:

Data Replication — Parte 1

Replicated Log

Esse termo eu achei particularmente redudante. Basicamente, é um WAL, só que distribuído. Basicamente o que ele recomenda é fazer todas as máquinas de um cluster olharem para o mesmo WAL, para que todas saibam como chegar em um consenso sobre o estado.

Como já comentei sobre WAL no artigo anterior, só tenho isso a dizer sobre esse pattern mesmo.

Singular Update Queue

Imagine que voce tenha múltiplas máquinas precisando escrever algum dado no mesmo destino. Como você garantiria que as solicitações sejam processadas em algum tipo de ordem, para que não sejam feitas de forma desodernada, causando problemas de inconsistência, por exemplo?

A Singular Update Queue serve para isso. De uma maneira geral, ela segue um príncipio muito útil em computação distribuída, que é: permita múltiplas leituras, porém, tente ao máximo limitar a escrita para pontos específicos. Isso tende a funcionar melhor na maioria das vezes.

Nesse pattern, todas as solicitações de escrita são enviadas para uma única fila, no qual as mensagens são processadas em uma determinada ordem, devido a própria natureza do mecanismo de Filas.

Representação visual

Isso garante que somente um ponto no sistema faça alterações, evitando o uso de mecanismos de sincronização como Locks e Mutexes (técnicas que em grandes volumes de processamento, impactam a performance).

Caso uma única worker thread não seja o suficiente para consumir as mensagens, é possível escalar horizontalmente o processamento adicionando mais threads. A única pegadinha aqui é que as filas tem que ser thread-safe para isso funcionar e é legal tomar cuidado com processamento duplicado também. Uma ferramenta que tem um suporte muito bom a processamentos similar a esse (já com todas as salvaguardas implementadas nas filas) é o Kafka.

Request Waiting List

Como todo o processamento em um cluster geralmente ocorre de forma assíncrona, como saber qual request de qual cliente já foi processada?

Essa estrutura serve justamente para isso: uma estrutura de dados adicional compartilhada por todo o cluster que tem uma lista de espera, que mapeia uma chave para uma funcão (callback). A chave pode ser o correlationId (técnica importante relacionada a sistemas distribuidos, porém, fora da lista do material abordado aqui).

Com isso rodando, basta que o cluster verifique se o callback associado a um determinado ID terminou de rodar e retorna a solicitação para o cliente. O callback é muito similar aos seguintes mecanismos: Promise (Javascript) , Task (C#) e Future (Java).

Apesar no nome em inglês, basicamente, é o mesmo mecanismo daqueles Pagers Wireless de restaurantes, que vibram quando é enviado um sinal para o aparelho, indicando que o pedido está pronto.

Idempotent Receiver (ou Idempotência)

Esse eu não diria nem que é um pattern e sim um pré-requisito de toda e qualquer operação em sistemas distribuídos.

O que acontece caso sejam feitas duas requests iguais para um mesmo servidor? O cliente não tinha como saber que deu certo o processamento e por via das dúvidas, fez outra chamada.

A ideia básica da Idempotência é armazenar o resultado da computação associado a algum ID único que represente um determinado cliente (ou transação).

Sendo assim, em chamadas subsequentes, primeiro o servidor verifica se aquela operação já foi feita, se sim, o resultado previamente computado é retornado.

Em casos que performance seja algo importante, a recomendação é usar Cache como mecanismo de persistência, mas um banco de dados comum funciona também, porém, será um pouco mais lento, dado que toda operação rodará a checagem de idempotência. Portanto, tenha isso em mente ao projetar um mecanismo desse.

Follower Reads

Em um cluster que se utiliza da estrutura de Leaders and Followers (mencionada no artigo anterior), pode acontecer que chamadas de leitura feitas para o Leader tenham um impacto em latência significativo.

A solução para isso é deixar o Leader responsável por operações de escrita e as máquinas Followers com a responsabilidade de atender solicitações de leitura.

Exemplo de um fluxo com Follower Reads

Faça-se a seguinte pergunta: em um sistema qualquer hipotético, qual operação é mais comum: leitura ou escrita?

Na maioria dos casos, leitura. Portanto, essas otimizações na forma que os dados são lidos é uma prática que se paga muito fácil e costuma dar muito certo. E se pareceu com técnicas de replicação presente nos bancos de dados modernos, é porque é um outro termo para a mesma coisa.

Versioned Value

Em um sistema com múltiplas máquinas, como saber se um dado requisitado pelo cliente está na última versão?

Representação visual

Para resolver isso, cada dado tem um número de versão associado, que é incrementado a cada alteração no mesmo. Isso permite que cada atualização possa ser feita sem bloquear a leitura do registro (o que ajuda na performance). Geralmente em sistemas assim, na atualização ocorre o uso de locks para garantir integridade da alteração. Como estamos gerando uma nova versão do dado, a anterior fica liberada para leitura sem problemas. Além disso, essa técnica também permite acessar o histórico de alterações, o que é uma boa prática de uma maneira geral.

É uma técnica necessária para se implementar Optimistic Locking. A título de curiosidade, o Datomic (também conhecido com o banco de dados que o só o Nubank usa) funciona todo em cima desse mecanismo, pois ele permite alterações não destrutivas nos dados, fator essencial para confiabilidade de sistemas financeiros.

Version Vector

Se múltiplos servidores permitem que uma mesma chave seja atualizada, como detectar se os dados estão sendo atualizados de forma correta entre as máquinas envolvidas?

Basicamente, é possível controlar isso como uma estrutura de chave-valor com contadores por máquina. Imagine que você tenha 3 máquinas (A, B, C), a estrutura de dados seria algo como:

{
node_a: 120,
node_b: 99,
node_c: 100
}

Caso ocorra um update na máquina B, o contador sobe para 100. Quanto as máquinas se comunicam entre si, elas sincronizam seus version vectors. Nesse processo, é possível detectar concorrência. Por exemplo: Caso a máquina A esteja com o valor de B como 101, ela sabe que está mais atualizada que o contador que veio da máquina B, logo, a versão dela é a mais atual e deve ser considerada verdade pelo resto do cluster.

Espero que o texto seja útil para você de alguma forma.

Até!

Você gostou do conteúdo e gostaria de fazer mentoria comigo? Clique aqui e descubra como.

--

--