Apache Cassandra
Quando sistemas alcançam a escala de petabytes e milhões de requisições por segundo, a arquitetura tradicional de bancos de dados relacionais (RDBMS) começa a apresentar fissuras. O gargalo geralmente não está na CPU ou na memória, mas na própria natureza centralizada do armazenamento. O Apache Cassandra nasceu para resolver exatamente este cenário: como escalar horizontalmente de forma linear sem nunca sacrificar a disponibilidade.
O Limite da Centralização
Em bancos de dados relacionais, escalar geralmente significa “comprar um servidor maior” (escala vertical) ou implementar sharding manual e complexas topologias de replicação Master-Slave. O problema é que, mesmo com réplicas de leitura, a escrita continua sendo um ponto único de falha ou um gargalo de contenção. Se o Master cai, o sistema entra em modo de recuperação, gerando downtime ou latência.
O Apache Cassandra subverte essa lógica através de uma arquitetura baseada no paper do Amazon Dynamo e no Google BigTable. Ele não possui um “nó mestre”. Todos os nós são iguais, o que chamamos de arquitetura Masterless. Maiores detalhes podem ser encontrados na Documentação de Arquitetura Oficial.
A Arquitetura em Anel e Particionamento
No Cassandra, os dados são distribuídos em um anel de nós. Cada dado é atribuído a um nó com base em um hash da sua Partition Key.
flowchart TD
subgraph Ring [Cluster Cassandra]
NodeA((Nó A)) --- NodeB((Nó B))
NodeB --- NodeC((Nó C))
NodeC --- NodeD((Nó D))
NodeD --- NodeA
end
Client[Cliente] -->|Hash Partition Key| NodeB
NodeB -->|Replica| NodeC
NodeB -->|Replica| NodeD
Quando um cliente envia uma escrita, qualquer nó pode atuar como coordenador. Ele calcula para quais nós aquela partição pertence e replica os dados conforme o Replication Factor configurado. Isso garante que, mesmo que dois nós morram simultaneamente, os dados ainda estejam disponíveis em outros pontos do anel.
Vantagens e Desvantagens
A escolha pelo Cassandra nunca é gratuita; ela é baseada no Teorema CAP, onde o Cassandra prioriza Disponibilidade e Tolerância a Partição (AP).
Vantagens
- Escalabilidade Linear: Se você precisa do dobro de throughput, basta dobrar o número de nós.
- Escrita de Alta Performance: O Cassandra utiliza uma estrutura de log (LSM Trees) que transforma escritas aleatórias em sequenciais no disco, tornando-o incrivelmente rápido para ingestão de dados.
- Sem Ponto Único de Falha: A arquitetura masterless garante que o cluster continue operando enquanto a maioria dos nós estiver saudável.
- Multi-Data Center: Suporte nativo para replicação entre diferentes regiões geográficas com latência mínima.
Desvantagens
- Sem Joins ou Integridade Referencial: Esqueça o
JOIN. Se você precisa relacionar dados, deve fazer isso na camada de aplicação ou desnormalizar. - Modelagem Rígida: Você deve modelar suas tabelas estritamente de acordo com as queries que irá executar. Alterar uma forma de busca muitas vezes exige criar uma nova tabela.
- Consistência Eventual: Por padrão, os dados podem demorar alguns milisegundos para estarem idênticos em todos os nós (embora isso seja configurável via Consistency Levels).
Buscas Rápidas: O Segredo da Modelagem
Para obter buscas em milisegundos no Cassandra, você deve evitar a todo custo o Full Table Scan. A regra de ouro é: Uma tabela por query.
O segredo está na Primary Key, que é composta por:
- Partition Key: Define em qual nó o dado reside.
- Clustering Columns: Define a ordenação dos dados dentro da partição no disco.
Se você quer buscar as últimas transações de um usuário de forma ultra rápida, sua modelagem deve ser:
1
2
3
4
5
6
7
8
9
10
11
12
13
-- scripts/init.cql
CREATE KEYSPACE IF NOT EXISTS wallet_system
WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 3};
USE wallet_system;
CREATE TABLE IF NOT EXISTS transactions_by_user (
user_id uuid,
transaction_id timeuuid,
amount decimal,
status text,
PRIMARY KEY (user_id, transaction_id)
) WITH CLUSTERING ORDER BY (transaction_id DESC);
Neste exemplo, todas as transações de um user_id específico são armazenadas fisicamente juntas e já ordenadas. A busca se torna um acesso sequencial simples, o que é extremamente eficiente.
Simulando um Cluster Local com Docker
Para entender o Cassandra, nada melhor do que ver os nós conversando. Vamos criar um ambiente com 3 nós utilizando o Docker Oficial e Docker Compose, incluindo limites de memória para simular um ambiente mais restrito e uma interface visual para facilitar a consulta.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
services:
cassandra-1:
image: cassandra:latest
container_name: cassandra-1
ports:
- "9042:9042"
mem_limit: 1g
environment:
- CASSANDRA_CLUSTER_NAME=DevCluster
- CASSANDRA_DC=DC1
- CASSANDRA_ENDPOINT_SNITCH=GossipingPropertyFileSnitch
- MAX_HEAP_SIZE=512M
- HEAP_NEWSIZE=128M
ulimits:
memlock:
soft: -1
hard: -1
volumes:
- cassandra-data-1:/var/lib/cassandra
healthcheck:
test: ["CMD-SHELL", "nodetool status | grep UN"]
interval: 15s
timeout: 10s
retries: 10
cassandra-2:
image: cassandra:latest
container_name: cassandra-2
mem_limit: 1g
environment:
- CASSANDRA_CLUSTER_NAME=DevCluster
- CASSANDRA_DC=DC1
- CASSANDRA_SEEDS=cassandra-1
- CASSANDRA_ENDPOINT_SNITCH=GossipingPropertyFileSnitch
- MAX_HEAP_SIZE=512M
- HEAP_NEWSIZE=128M
ulimits:
memlock:
soft: -1
hard: -1
volumes:
- cassandra-data-2:/var/lib/cassandra
depends_on:
cassandra-1:
condition: service_healthy
cassandra-3:
image: cassandra:latest
container_name: cassandra-3
mem_limit: 1g
environment:
- CASSANDRA_CLUSTER_NAME=DevCluster
- CASSANDRA_DC=DC1
- CASSANDRA_SEEDS=cassandra-1
- CASSANDRA_ENDPOINT_SNITCH=GossipingPropertyFileSnitch
- MAX_HEAP_SIZE=512M
- HEAP_NEWSIZE=128M
ulimits:
memlock:
soft: -1
hard: -1
volumes:
- cassandra-data-3:/var/lib/cassandra
depends_on:
- cassandra-2
cassandra-web:
image: ipushc/cassandra-web
container_name: cassandra-web
restart: always
ports:
- "3000:80"
environment:
- CASSANDRA_HOST=cassandra-1
- CASSANDRA_PORT=9042
- HOST_PORT=:80
depends_on:
cassandra-1:
condition: service_healthy
volumes:
cassandra-data-1:
cassandra-data-2:
cassandra-data-3:
ENVs
CASSANDRA_CLUSTER_NAME: Define o nome lógico do cluster. Apenas nós com o mesmo nome conseguem se comunicar e formar um anel.CASSANDRA_DC: Define o nome do Datacenter. No Cassandra, o DC é uma divisão lógica usada para replicação inteligente e proximidade de rede.CASSANDRA_ENDPOINT_SNITCH: O Snitch é o componente que ensina ao Cassandra a topologia da rede. OGossipingPropertyFileSnitché o padrão recomendado, pois utiliza o protocolo Gossip para propagar informações de DC e Rack.MAX_HEAP_SIZEeHEAP_NEWSIZE: Configurações críticas da JVM. Em containers, é essencial limitar o Heap para evitar que o processo seja morto pelo OOM Killer do Docker, garantindo que o Cassandra opere dentro dos limites demem_limit.CASSANDRA_SEEDS: Lista de nós “semente”. Quando um novo nó sobe, ele entra em contato com os seeds para descobrir quem são os outros membros do cluster e obter o Token Map atualizado.
Automação e Massa de Dados
Em sistemas distribuídos, configurar o schema e popular dados manualmente é ineficiente. Podemos automatizar o processo aguardando a saúde do cluster e injetando dados via script Bash.
1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash
# scripts/setup.sh
echo "Aguardando o Cassandra subir..."
until docker exec cassandra-1 cqlsh -e "describe cluster" > /dev/null 2>&1; do
printf "."
sleep 5
done
echo -e "\nCluster pronto! Aplicando schema..."
docker exec -i cassandra-1 cqlsh < scripts/init.cql
echo "Gerando dados iniciais..."
./scripts/generate_data.sh | docker exec -i cassandra-1 cqlsh
O uso do cassandra-web permite visualizar as transações geradas acessando http://localhost:3000. Essa visibilidade é crucial para entender como a Partition Key (user_id) realmente distribui os dados entre os nós físicos do cluster.
Validando o Cluster
Após subir o ambiente com docker-compose up -d, você pode verificar a saúde do anel utilizando a ferramenta nodetool.
1
docker exec -it cassandra-1 nodetool status
O output esperado deve mostrar os três nós com o status UN (Up/Normal):
1
2
3
4
5
6
7
8
Datacenter: DC1
===============
Status=Up/Down
|/ State=Normal/Leaving/Joining/Moving
-- Address Load Tokens Owns (effective) Host ID Rack
UN 172.18.0.2 105.24 KiB 256 66.7% ...-9ce0-f86997787050 rack1
UN 172.18.0.3 105.12 KiB 256 66.7% ...-8b21-a12b43521321 rack1
UN 172.18.0.4 105.43 KiB 256 66.7% ...-7c32-b23c54632432 rack1
Elasticidade: Adicionando Nós e Rebalanceamento
Uma das maiores forças do Cassandra é a capacidade de expandir o cluster sem downtime. Diferente de bancos tradicionais onde adicionar um nó exige particionamento manual, no Cassandra esse processo é automatizado através de vnodes (Virtual Nodes).
Consistent Hashing: O Elo entre Partition Key e Nodes
A grande dúvida na expansão é: se a Partition Key define o nó onde o dado reside, como o Cassandra não “se perde” ao adicionar novos servidores?
O segredo está no fato de que a Partition Key não define o nó diretamente, mas sim a sua posição lógica no anel. O Cassandra utiliza uma camada de indireção chamada Consistent Hashing:
- O Token é Imutável: Quando um dado é inserido, a Partition Key passa por uma função de hash (Murmur3) que gera um Token. Esse valor é fixo e nunca muda para aquela chave.
- O Nó é Dinâmico: O que muda é a responsabilidade sobre as fatias de tokens. O cluster mantém um Token Map (distribuído via Gossip) que diz: “Atualmente, tokens de 0 a 1000 pertencem ao Nó A”.
- Redefinição de Faixas: Ao adicionar o Nó D, o Token Map é atualizado para que o Nó D assuma, por exemplo, os tokens de 500 a 1000.
Dessa forma, o dado com user_id: 123 (que sempre gera o mesmo token) simplesmente “descobre” que seu novo endereço físico é o Nó D. O cliente (driver) recebe essa atualização do Token Map e passa a rotear as requisições para o novo responsável sem precisar re-hashar nada.
O Processo de Bootstrapping e Coordenção
Quando o cassandra-4 entra no cluster, ele inicia o Bootstrapping:
- Streaming Seletivo: O novo nó olha para as faixas de tokens que ele agora possui e solicita aos donos antigos apenas os dados que se encaixam naquelas faixas.
- Escritas em Duplicidade: Durante o processo de entrada, o nó coordenador do cluster é inteligente o suficiente para enviar escritas tanto para os donos antigos quanto para o novo nó que está entrando (pendente), garantindo que nenhum dado seja perdido durante a transição.
- Finalização: Uma vez que o streaming termina, o nó muda seu estado para
Normale passa a ser o dono oficial daquelas fatias.
flowchart TD
subgraph Ring [Rebalanceamento com vnodes]
NodeA((Nó A)) --- NodeB((Nó B))
NodeB --- NodeNew((Novo Nó D))
NodeNew --- NodeC((Nó C))
NodeC --- NodeA
end
NodeA -.->|Streaming de Sub-faixas| NodeNew
NodeB -.->|Streaming de Sub-faixas| NodeNew
NodeC -.->|Streaming de Sub-faixas| NodeNew
Rebalanceamento e Cleanup
Embora o novo nó já comece a responder por suas novas faixas de dados assim que o bootstrap termina, os nós antigos ainda mantêm as cópias dos dados que foram transferidos. O Cassandra não deleta esses dados automaticamente para evitar riscos de perda durante a expansão.
Para finalizar o rebalanceamento e liberar espaço em disco nos nós antigos, você deve executar manualmente:
1
nodetool cleanup
Este comando percorre os dados locais e remove tudo o que não pertence mais àquele nó de acordo com a nova distribuição do anel. É uma operação intensiva de I/O, por isso recomenda-se executá-la em um nó por vez.
Como o Cassandra funciona por baixo do capô?
O segredo da velocidade de escrita reside no caminho que o dado percorre, conforme detalhado na Arquitetura de Motor de Armazenamento:
- Commit Log: O dado é escrito imediatamente em um arquivo de log sequencial no disco (segurança contra quedas).
- Memtable: O dado é armazenado em uma estrutura na memória (rápido).
- SSTable (Sorted String Table): Quando a Memtable enche, o Cassandra faz um flush dos dados para o disco em um arquivo imutável chamado SSTable.
Como as SSTables são imutáveis, o Cassandra não precisa lidar com locks complexos de atualização. Periodicamente, ele executa um processo chamado Compaction, que mescla diferentes SSTables e remove dados antigos ou deletados (marcados com tombstones).
Conclusão
O Apache Cassandra não é um substituto para o MySQL ou PostgreSQL em todos os cenários. Ele é uma arma de precisão para casos onde o volume de escrita e a necessidade de alta disponibilidade superam a necessidade de flexibilidade relacional. Se o seu sistema precisa crescer sem medo de limites de hardware e você pode abraçar a desnormalização, o Cassandra é, sem dúvida, um dos alicerces mais robustos para a sua infraestrutura.