DataARTIGO

Arquitetura Cassandra e anatomia Write Path

O Cassandra é um banco de dados NoSQL que pertence à categoria de banco de dados da família da coluna NoSQL. Ele é um projeto Apache e possui uma versão Enterprise mantida pelo DataStax. O Cassandra é escrito em Java e é usado principalmente para dados de séries temporais, como métricas, IoT (Internet of Things), logs, mensagens de bate-papo, e assim por diante. Ele é capaz de lidar com uma enorme quantidade de gravações e lê e escala para milhares de nós. Vamos listar aqui algumas características importantes do Cassandra. Basicamente, o Cassandra …

  • mistura ideias do Big Table do Google e do Dynamo da Amazon.
  • é baseado em arquitetura peer-to-peer. Cada nó é igual (pode executar leituras e gravações), portanto, não há nó mestre ou escravo, ou seja, não há um único ponto mestre de falha.
  • faz partição e replicação automáticas.
  • tem gravação sintonizável e consistência de leitura para operações de leitura e gravação.
  • é capaz de escalar horizontalmente mantendo a escalabilidade linear para leitura e gravação.
  • lida com a comunicação inter-nó através do protocolo Gossip.
  • lida com a comunicação do cliente através do CQL (Cassandra Query Language), que é muito semelhante ao SQL.

Coordenador

Quando uma solicitação é enviada para qualquer nó Cassandra, esse nó age como um proxy entre a aplicação (na verdade, o driver Cassandra) e os nós envolvidos no fluxo de solicitação. Esse nó proxy é chamado de coordenador. Ele é responsável por gerenciar todo o caminho da solicitação e responder de volta para o cliente.

 

Figura 1 – Coordenador

 

Além disso, às vezes, quando o coordenador encaminha uma solicitação de gravação para os nós de réplica, eles podem estar indisponíveis naquele exato momento. Nesse caso, o coordenador desempenha um papel importante, implementando um mecanismo chamado Hinted Handoff, que será descrito em detalhes mais tarde.

Particionador

Basicamente, para cada nó no cluster Cassandra (anel Cassandra), é atribuída uma gama de tokens como mostrado na figura 2 para um cluster de 6 nós (com tokens imaginários, é claro).

 

Figura 2 – Token Ranges

 

O Cassandra distribui dados no cluster usando um algoritmo consistente de Hashing e, a partir da versão 1.2, implementa também o conceito de nós virtuais (vnodes), em que cada nó possui uma grande quantidade de pequenos intervalos de token para melhorar a reorganização de token e evitar hotspots no cluster, isto é, alguns nós armazenando muito mais dados do que outros. Os nós virtuais também permitem adicionar e remover nós no cluster com mais facilidade e gerenciam automaticamente a atribuição de token para você, para que você possa desfrutar de um bom café ao adicionar ou remover um nó em vez de calcular e atribuir novos intervalos de token para cada nó (o que, por sinal, é uma operação muito propensa a erros).

Bem, dito isso, o particionador é o componente responsável por determinar como distribuir os dados através dos nós no cluster, dada a chave de partição de uma linha. Basicamente, é uma função hash para computar um token com a chave de partição.

Quando o particionador aplica a função hash à chave de partição e obtém o token, ele sabe exatamente qual nó irá lidar com a solicitação.

Consideremos um exemplo simples: suponha que uma solicitação seja emitida para node6 (ou seja, node6 é o coordenador para essa solicitação) com uma linha contendo a chave de partição “jorge_acetozi”. Suponha que o particionador aplique a função hash à chave de partição “jorge_acetozi” e obtenha o token -17. Como mostra a figura 3, os intervalos do token node2 incluem -17, então esse nó será aquele que lida com a solicitação.

 

Figura 3: Particionador

 

O Cassandra oferece três tipos de particionadores: Murmur3Partitioner (que é o padrão), RandomPartitioner e ByteOrderedPartitioner.

Replicação

A vida seria muito mais fácil se…

  • Nós nunca falhassem;
  • Redes não tivessem latência;
  • Pessoas não tropeçassem em cabos;
  • A Amazon não tivesse reiniciado suas instâncias;
  • Full GC significasse “Full Guitar Concert”.

E assim por diante. Infelizmente, essas coisas acontecem o tempo todo, e você já escolheu uma carreira de engenheiro de software (sua mãe costumava aconselhá-lo a estudar muito e se tornar um médico, mas você escolheu continuar jogando Counter-Strike em vez disso. Agora, você é um engenheiro de software, sabe o que significa AK-47 e tem que se preocupar com coisas assim).

Felizmente, o Cassandra oferece replicação automática de dados e mantém seus dados redundantes em diferentes nós no cluster. Isso significa que (em certos níveis) você pode até resistir a cenários de falha de nó, e seus dados ainda estarão seguros e disponíveis. Mas tudo tem um preço, e o preço de replicação é a consistência.

 

Estratégia de replicação

Basicamente, o coordenador usa a estratégia de replicação para descobrir quais nós serão os nós de réplica para uma determinada solicitação.

Há duas estratégias de replicação disponíveis:

SimpleStrategy: usada para a implementação um único de data center (não recomendada para ambientes de produção). Ela não considera a topologia da rede. Basicamente, apenas toma a decisão do particionador (ou seja, o nó que lida com a solicitação primeiro, com base no intervalo de token) e coloca as réplicas restantes no sentido horário em relação a esse nó. Por exemplo, na figura 3, se o fator de replicação da tabela fosse 3, quais nós teriam sido escolhidos pelo SimpleStrategy para atuar como réplicas (além do node2, que já havia sido escolhido pelo particionador)? Isso, node3 e node4! E se o fator de replicação fosse 4? Bem, então o node5 também seria incluído.

NetworkTopologyStrategy: usada para a implementação de múltiplos data centers (recomendada para ambientes de produção). Ela também toma a decisão do particionador e coloca as réplicas restantes no sentido horário, mas também leva em consideração a configuração do rack e dos data centers.

 

Fator de replicação

Quando você cria uma tabela (Column Family) no Cassandra, você especifica o fator de replicação. O fator de replicação é o número de réplicas que o Cassandra irá armazenar para essa tabela em nós diferentes. Se você especificar REPLICATION_FACTOR=3, seus dados serão replicados para 3 nós diferentes em todo o cluster. Isso fornece tolerância a falhas e resiliência, porque, mesmo que alguns nós falhem, seus dados ainda estarão seguros e disponíveis.

 

Nível de consistência de gravação

Você ainda lembra que, quando o cliente envia uma solicitação para um nó Cassandra, esse nó é chamado de coordenador e atua como um proxy entre o cliente e os nós de réplica?

Bem, quando você escreve uma tabela no Cassandra (inserindo dados, por exemplo), você pode especificar o nível de consistência de gravação. Trata-se do número de nós de réplica que devem reconhecer o coordenador que a inserção local foi bem sucedida (o sucesso aqui significa que os dados foram anexados ao log de commit e escritos no memtable). Assim que o coordenador obtiver confirmações de sucesso WRITE_CONSISTENCY_LEVEL dos nós de réplica, ele retorna com sucesso para o cliente e não espera que as réplicas restantes reconheçam o sucesso.

Por exemplo, se um aplicativo emitir uma solicitação de inserção com WRITE_CONSISTENCY_LEVEL =TWO para uma tabela configurada com REPLICATION_FACTOR=3, o coordenador somente retornará sucesso para a aplicação quando duas das três réplicas reconhecerem o sucesso. Claro, isso não significa que a terceira réplica também não vai gravar os dados; ela vai, mas, neste momento, o coordenador já teria enviado o sucesso de volta para cliente.

Existem muitos tipos diferentes de níveis de consistência de gravação que você pode especificar em sua solicitação de gravação. Do menos consistente para a consistência total: ANY, ONE, TWO, THREE, QUORUM, LOCAL_QUORUM, EACH_QUORUM, ALL.

 

Exemplo de fluxo de gravação

Por questões de simplicidade, suponha que uma solicitação de gravação seja emitida para um cluster Cassandra de 6 nós com as seguintes características:

WRITE_CONSISTENCY_LEVEL=TWO
TABLE_REPLICATION_FACTOR=3
REPLICATION_STRATEGY=SimpleStrategy

Primeiro, o cliente envia a solicitação de gravação para o cluster Cassandra usando o driver. Não discutimos o papel do driver neste artigo (talvez em outro), mas ele é muito importante. O driver é responsável por muitos recursos, como IO assíncrono, execução paralela, pipelining de solicitação, agrupamento de conexões, descoberta de nó automático, reconexão automática, reconhecimento de token, e assim por diante. Por exemplo, usando um driver que implementa uma política de reconhecimento de token, o driver reduz saltos de rede enviando solicitações diretamente para o nó que possui os dados em vez de enviá-lo para um coordenador “aleatório”.

Assim que o coordenador obtiver a solicitação de gravação, ele aplica a função hash do particionador à chave de partição e usa a estratégia de replicação configurada para determinar os nós de réplica TABLE_REPLICATION_FACTOR que realmente gravarão os dados (nesta frase, substitua TABLE_REPLICATION_FACTOR pelo número 3). A figura 4 mostra os nós de réplica (em verde) que irão lidar com a solicitação de gravação.

 

Figura 4 – Nós de réplica

 

Agora, antes que o coordenador encaminhe o pedido de gravação para todos os 3 nós de réplica, ele perguntará ao componente Failure Detector quantos desses nós de réplica estão realmente disponíveis e vai compará-los com WRITE_CONSISTENCY_LEVEL fornecido na solicitação. Se o número de nós de réplica disponíveis for menor do que o WRITE_CONSISTENCY_LEVEL fornecido, o Failure Detector lançará imediatamente uma Exceção.

Para o nosso exemplo, suponha que os 3 nós de réplica estejam disponíveis (ou seja, o Failure Detector permitirá que a solicitação continue) como mostrado na figura 5. Agora, o coordenador encaminhará de forma assíncrona a solicitação de gravação para todos os nós de réplica (nesses casos, os 3 nós de réplica que foram calculados na primeira etapa). Assim que os nós de réplica WRITE_CONSISTENCY_LEVEL reconhecem o sucesso (node2 e node4), o coordenador retorna sucesso para o driver.

 

Figura 5 – Write Sucess

 

Se o WRITE_CONSISTENCY_LEVEL para esse pedido fosse TRÊS (ou TODOS), o coordenador teria que esperar até que o nó3 reconhecesse o sucesso também e, claro, essa solicitação de gravação seria mais lenta.

Então, basicamente…

  • Você precisa de tolerância a falhas e alta disponibilidade? Use replicação;
  • Tenha em mente que usar replicação significa que você pagará com consistência (para a maioria dos casos, isso não é um problema. A disponibilidade geralmente é mais importante do que a consistência);
  • Se a consistência não é um problema para o seu domínio, perfeito. Se for, apenas aumente o nível de consistência, mas você pagará com maior latência;
  • Se você quer tolerância a falhas e alta disponibilidade, consistência forte e baixa latência, então você deve ser o cliente, e não o engenheiro de software (Lol).

 

Hinted Handoff

Suponha que, no último exemplo, apenas 2 dos 3 nós de réplica estivessem disponíveis. Nesse caso, o Failure Detector ainda permitiria que a solicitação continuasse, pois o número de nós de réplica disponíveis não era menor que o WRITE_CONSISTENCY_LEVEL fornecido. Nesse caso, o coordenador se comportaria exatamente como descrito anteriormente, mas haveria uma etapa adicional. O coordenador gravaria a dica localmente (o blob de solicitação de gravação junto com alguns metadados) no disco (diretório de dicas) e manteria a dica por 3 horas (por padrão), esperando que o nó de réplica ficasse disponível novamente. Se o nó de réplica se recuperar dentro desse período, o coordenador enviará a dica ao nó de réplica para que ele possa se atualizar e se tornar consistente com as outras réplicas. Se o nó de réplica ficar offline por mais de 3 horas, será necessário um reparo de leitura. Esse processo é chamado de Hinted Handoff.

 

Write Internals – Últimas considerações?

Em suma, quando uma solicitação de escrita atinge um nó, duas coisas principalmente acontecem:

  1. A solicitação de gravação é anexada ao log de commit no disco. Isso garante a durabilidade dos dados (os dados da solicitação de gravação sobreviveriam permanentemente mesmo em um cenário de falha de nó);
  2. A solicitação de gravação é enviada para o memtable (uma estrutura armazenada na memória). Quando o memtable está cheio, os dados são descarregados para um SSTable no disco usando I/O sequencial, e os dados no log de confirmação são purgados.

 

Figura 6 – Nós internos do Cassandra

Eu realmente espero que este artigo tenha sido útil para você. Se você gostou de lê-lo, avise-me se você gostaria de ler outro artigo sobre Read Request Path – ele é um pouco mais complicado, pois envolve Snitches, Bloom Filter, Indexes, e assim por diante, mas também é muito interessante.

Se você é um desenvolvedor de software interessado em usar Cassandra em um cenário realista, codificando um aplicativo de bate-papo em tempo real a partir do zero, veja o meu livro: Pro Java Clustering and Scalability: Building Real-Time Apps with Spring, Cassandra, Redis, WebSocket and RabbitMQ.

Muito obrigado.

É um engenheiro de software que passa quase todos os dias se divertindo com coisas como AWS, CoreOS, Kubernetes, Docker, Terraform, Cassandra, Redis, Elasticsearch, Logstash, Graylog, Sensu, RabbitMQ, Kafka etc. Ele adora coisas como a implementação de aplicativos em produção enquanto milhares de usuários estão online, monitorando a infraestrutura e agindo rapidamente quando as ferramentas de monitoramento decidem desafiar a saúde do seu coração!

Ver perfil