Introdução ao Apache Cassandra: quando um banco wide-column faz sentido

1. Fundamentos do Modelo Wide-Column no Cassandra

O Apache Cassandra é um banco de dados NoSQL do tipo wide-column, projetado para lidar com grandes volumes de dados distribuídos em múltiplos nós. Diferente dos bancos relacionais tradicionais, o Cassandra organiza dados em famílias de colunas (column families), onde cada linha pode ter um conjunto diferente de colunas — um conceito conhecido como tabelas esparsas.

No modelo relacional (SQL), você define um esquema fixo com colunas predefinidas. No MongoDB (documentos), você armazena JSON flexível. O Cassandra combina o melhor dos dois: oferece estrutura com tabelas, mas permite que cada linha tenha colunas dinâmicas. Isso é particularmente útil quando você não sabe antecipadamente todos os atributos que um registro terá.

A estrutura hierárquica do Cassandra é:
- Keyspace: equivalente a um "banco de dados" no SQL
- Table: família de colunas
- Partition: unidade de distribuição de dados no cluster
- Row: conjunto de colunas dentro de uma partição
- Clustering columns: definem a ordem de armazenamento dentro de uma partição

Keyspace: sistema_logs
  Table: logs_iot
    Partition Key: data_hora (dia/hora)
      Clustering Key: timestamp
        Row: { sensor_id: "A1", temperatura: 25.3, umidade: 60 }

2. Arquitetura Distribuída e Sem Mestre (Masterless)

O Cassandra opera em um modelo masterless (sem mestre), onde todos os nós são iguais. O cluster forma um anel (ring) lógico, e cada nó é responsável por uma faixa de dados determinada pelo particionador (partitioner) — geralmente o Murmur3Partitioner.

A replicação é controlada pelo replication factor (RF). Se RF=3, cada dado é armazenado em três nós diferentes. A consistência é ajustável por consulta:

  • ONE: resposta do primeiro nó disponível (baixa latência, menor consistência)
  • QUORUM: maioria dos nós réplicas (equilíbrio entre latência e consistência)
  • ALL: todos os nós réplicas (alta consistência, maior latência)

O Gossip protocol permite que cada nó descubra o estado dos outros nós a cada segundo, sem necessidade de um coordenador central. Se um nó falha, o cluster continua operando normalmente.

Exemplo de configuração de consistência:
  INSERT INTO logs (id, mensagem) VALUES (1, 'erro') 
  USING CONSISTENCY QUORUM;

3. Modelagem de Dados para Cassandra: Pensando em Consultas Primeiro

A modelagem no Cassandra segue o princípio query-first: você primeiro define quais consultas serão feitas e depois cria as tabelas para atendê-las. Isso é oposto ao SQL, onde você normaliza os dados primeiro.

A chave primária composta é essencial:
- Partition key: determina onde os dados são armazenados (distribuição)
- Clustering columns: ordenam os dados dentro da partição

Exemplo: Sensores IoT (séries temporais)

CREATE TABLE sensores.temperatura (
  sensor_id text,
  data_hora timestamp,
  temperatura float,
  PRIMARY KEY ((sensor_id), data_hora)
) WITH CLUSTERING ORDER BY (data_hora DESC);

Aqui, sensor_id é a partition key, e data_hora é a clustering column ordenada decrescentemente. Isso permite consultas eficientes como "últimas 100 leituras do sensor A1".

Exemplo: Catálogo de Produtos

CREATE TABLE catalogo.produtos_por_categoria (
  categoria text,
  produto_id uuid,
  nome text,
  preco decimal,
  PRIMARY KEY ((categoria), preco, produto_id)
) WITH CLUSTERING ORDER BY (preco ASC);

4. Quando o Cassandra é a Escolha Certa (e Quando Não)

Casos de uso ideais:

  • Escrita intensa: milhares de escritas por segundo (logs, métricas IoT)
  • Escalabilidade horizontal: adicionar nós sem downtime
  • Alta disponibilidade: sem ponto único de falha, failover automático
  • Dados geo-distribuídos: replicação entre data centers

Cenários inadequados:

  • Joins complexos: o Cassandra não suporta JOINs nativos
  • Transações ACID: apenas consistência eventual ou ajustável
  • Consultas ad-hoc: você precisa modelar para consultas específicas
  • Dados pequenos e relacionais: overhead desnecessário

Comparado com PostgreSQL e CockroachDB:
- PostgreSQL: consistência forte, ideal para dados relacionais, mas escala verticalmente
- CockroachDB: consistência forte distribuída, mas mais complexo e maior latência
- Cassandra: consistência eventual, menor latência em escritas, melhor para cargas massivas

5. Operações Essenciais: Inserção, Leitura e Compaction

CQL (Cassandra Query Language)

-- Criação
CREATE KEYSPACE exemplo WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 3};

USE exemplo;

CREATE TABLE usuarios (
  id uuid PRIMARY KEY,
  nome text,
  email text
);

-- Inserção
INSERT INTO usuarios (id, nome, email) 
VALUES (uuid(), 'João Silva', 'joao@email.com');

-- Leitura
SELECT * FROM usuarios WHERE id = ?;

-- Atualização
UPDATE usuarios SET email = 'novo@email.com' WHERE id = ?;

-- Exclusão
DELETE FROM usuarios WHERE id = ?;

Como o Cassandra lê:

  1. Verifica o memtable (cache em RAM)
  2. Verifica SSTables (arquivos em disco) usando bloom filters para evitar leituras desnecessárias
  3. Índices secundários podem ser usados, mas são menos eficientes que consultas por partition key

Compaction:

  • SizeTieredCompactionStrategy (STCS): mescla SSTables de tamanho similar (bom para escritas pesadas)
  • LeveledCompactionStrategy (LCS): mantém SSTables em níveis (melhor para leituras)
ALTER TABLE logs WITH compaction = {'class': 'LeveledCompactionStrategy'};

6. Estratégias de Particionamento e Hot/Cold Data

A escolha da partition key é crítica para evitar hot spots — partições que recebem carga desproporcional. Uma partition key mal escolhida pode sobrecarregar um nó enquanto outros ficam ociosos.

Exemplo de hot spot (ruim):

PRIMARY KEY ((data_hora))  -- Uma partição por hora, carga concentrada

Distribuição uniforme (bom):

PRIMARY KEY ((sensor_id, data_hora))  -- Combinação única distribui carga

TTL (time-to-live) permite expirar dados automaticamente:

INSERT INTO logs (id, mensagem) VALUES (1, 'erro') USING TTL 86400;  -- 24 horas

Para dados históricos, use compressão:

CREATE TABLE logs_historicos WITH compression = {'class': 'LZ4Compressor'};

7. Monitoramento e Manutenção do Cluster

Métricas essenciais:
- Latência de leitura/escrita: p95, p99
- Pending compactions: número de compactações pendentes
- Hinted handoffs: entregas atrasadas de dados para nós temporariamente offline
- Bloom filter hit rate: eficiência na filtragem de SSTables

Ferramentas:

nodetool status              # Ver estado do cluster
nodetool repair              # Reparar consistência entre réplicas
nodetool cfstats             # Estatísticas de tabelas
nodetool compactionstats     # Status de compactações

Para monitoramento contínuo, integre com Prometheus e visualize no Grafana.

nodetool repair é crucial para garantir consistência quando nós ficam offline temporariamente. Execute regularmente em clusters de produção.

8. Exemplo Prático: Construindo um Sistema de Logs Distribuído

Modelagem da tabela

CREATE KEYSPACE logs_distribuidos 
WITH replication = {'class': 'NetworkTopologyStrategy', 'datacenter1': 3};

USE logs_distribuidos;

CREATE TABLE logs_por_dia (
  dia text,                    -- Partition key: "2025-03-15"
  timestamp timeuuid,          -- Clustering column (ordenado)
  servico text,
  severidade text,             -- "INFO", "WARN", "ERROR"
  mensagem text,
  PRIMARY KEY ((dia), timestamp, servico)
) WITH CLUSTERING ORDER BY (timestamp DESC);

Inserção em lote vs. assíncrona

Para alta throughput, use inserção assíncrona com prepared statements:

-- Inserção assíncrona (recomendada para alta throughput)
INSERT INTO logs_por_dia (dia, timestamp, servico, severidade, mensagem) 
VALUES ('2025-03-15', now(), 'api-gateway', 'ERROR', 'Timeout na conexão');

-- Batch (apenas para operações atômicas relacionadas)
BEGIN BATCH
  INSERT INTO logs_por_dia (dia, timestamp, servico, severidade, mensagem) 
  VALUES ('2025-03-15', now(), 'auth', 'INFO', 'Login bem-sucedido');
  INSERT INTO logs_por_dia (dia, timestamp, servico, severidade, mensagem) 
  VALUES ('2025-03-15', now(), 'auth', 'INFO', 'Token gerado');
APPLY BATCH;

Consulta por intervalo de tempo e filtro por severidade

-- Últimas 100 entradas do dia
SELECT * FROM logs_por_dia 
WHERE dia = '2025-03-15' 
ORDER BY timestamp DESC 
LIMIT 100;

-- Filtro por severidade (usando índice secundário)
CREATE INDEX idx_severidade ON logs_por_dia (severidade);

SELECT * FROM logs_por_dia 
WHERE dia = '2025-03-15' 
  AND severidade = 'ERROR';

Para consultas frequentes por severidade, crie uma tabela separada:

CREATE TABLE logs_por_severidade (
  severidade text,
  dia text,
  timestamp timeuuid,
  servico text,
  mensagem text,
  PRIMARY KEY ((severidade, dia), timestamp)
) WITH CLUSTERING ORDER BY (timestamp DESC);

Referências