Apache Kafka para desenvolvedores web: conceitos e casos de uso práticos

1. Por que desenvolvedores web precisam entender Kafka?

1.1. A arquitetura monolítica vs. microsserviços: o papel da mensageria assíncrona

Desenvolvedores web acostumados com aplicações monolíticas muitas vezes enfrentam gargalos quando precisam escalar. Em um monólito, uma requisição HTTP geralmente executa tudo em um único processo — desde a validação até o banco de dados. Com microsserviços, surge a necessidade de comunicação entre componentes. Kafka entra como um backbone assíncrono, desacoplando produtores e consumidores sem bloqueios.

# Exemplo: Monólito vs. Kafka em microsserviços
# Monólito: requisição síncrona bloqueante
POST /api/pedido HTTP/1.1
Response: 200 OK (pedido processado, email enviado, estoque atualizado)

# Com Kafka: fluxo assíncrono
POST /api/pedido HTTP/1.1 → Produz evento "pedido_criado" no Kafka
Consumidor 1: Serviço de Email → processa em paralelo
Consumidor 2: Serviço de Estoque → processa em paralelo

1.2. Kafka como cola entre frontend, backend e sistemas externos

Kafka atua como um barramento de eventos que conecta frontends (via WebSocket ou proxies reversos), backends (APIs REST/GraphQL) e sistemas legados. Um evento gerado no frontend pode ser consumido por múltiplos serviços sem acoplamento.

1.3. Diferenças cruciais: filas tradicionais (RabbitMQ) vs. log distribuído (Kafka)

Característica RabbitMQ Kafka
Modelo Fila (consume e remove) Log (retenção por tempo/tamanho)
Persistência Opcional Por padrão, durável
Replay de mensagens Não suporta nativamente Sim, via offsets
Throughput Centenas de milhares/seg Milhões/seg

Kafka é ideal para streams de eventos e replay histórico; RabbitMQ é melhor para roteamento complexo de mensagens pontuais.

2. Conceitos fundamentais do Kafka para quem vem do mundo web

2.1. Tópicos, partições e offsets: a estrutura de dados que parece um banco

Um tópico é como uma tabela em um banco de dados. As partições são os "shards" físicos. Cada mensagem recebe um offset sequencial — similar a um ID auto-incrementado.

# Estrutura conceitual
Tópico: "cliques_usuario"
Partição 0: [offset 0] [offset 1] [offset 2] ...
Partição 1: [offset 0] [offset 1] [offset 2] ...

2.2. Produtores, consumidores e grupos de consumidores: paralelo com requisições HTTP

  • Produtor: equivalente a um POST que envia dados ao Kafka.
  • Consumidor: equivalente a um GET contínuo (polling) que lê mensagens.
  • Grupo de consumidores: balanceamento de carga — cada partição é atribuída a um consumidor do grupo.
# Exemplo: Grupo de consumidores
Grupo "processadores_pedidos":
  Consumidor A → Partição 0
  Consumidor B → Partição 1
  Consumidor C → Partição 2

2.3. Garantias de entrega (at-least-once, exactly-once) e implicações para aplicações web

  • At-least-once: padrão — mensagem pode ser entregue mais de uma vez (idempotência necessária no consumidor).
  • Exactly-once: suportado com transações Kafka e idempotência do produtor, mas com custo de performance.
  • At-most-once: mensagens podem ser perdidas se o consumidor falhar após o recebimento.

Para aplicações web financeiras, prefira exactly-once; para analytics, at-least-once é suficiente.

3. Configurando Kafka no ambiente de desenvolvimento web

3.1. Subindo um cluster Kafka local com Docker Compose (Zookeeper + Broker)

# docker-compose.yml
version: '3'
services:
  zookeeper:
    image: confluentinc/cp-zookeeper:latest
    environment:
      ZOOKEEPER_CLIENT_PORT: 2181
  kafka:
    image: confluentinc/cp-kafka:latest
    depends_on:
      - zookeeper
    ports:
      - "9092:9092"
    environment:
      KAFKA_BROKER_ID: 1
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092

Execute: docker-compose up -d

3.2. Conectando uma aplicação Node.js ao Kafka com bibliotecas oficiais

// Instalação: npm install kafkajs

const { Kafka } = require('kafkajs')

const kafka = new Kafka({
  clientId: 'web-app',
  brokers: ['localhost:9092']
})

// Produtor
const producer = kafka.producer()
await producer.connect()
await producer.send({
  topic: 'eventos-web',
  messages: [
    { key: 'user123', value: JSON.stringify({ acao: 'clique', pagina: '/home' }) }
  ]
})

// Consumidor
const consumer = kafka.consumer({ groupId: 'web-group' })
await consumer.connect()
await consumer.subscribe({ topic: 'eventos-web', fromBeginning: true })
await consumer.run({
  eachMessage: async ({ topic, partition, message }) => {
    console.log(`Offset ${message.offset}: ${message.value.toString()}`)
  }
})

3.3. Comandos essenciais do CLI

# Criar tópico
kafka-topics --create --topic eventos-web --bootstrap-server localhost:9092 --partitions 3 --replication-factor 1

# Listar tópicos
kafka-topics --list --bootstrap-server localhost:9092

# Produzir mensagens
kafka-console-producer --topic eventos-web --bootstrap-server localhost:9092
> {"acao": "teste"}

# Consumir mensagens
kafka-console-consumer --topic eventos-web --bootstrap-server localhost:9092 --from-beginning

4. Padrões de integração entre Kafka e aplicações web

4.1. Event sourcing: registrando ações do usuário como eventos imutáveis

Em vez de salvar o estado atual do usuário, armazene cada ação como um evento no Kafka. O estado é reconstruído reprocessando os eventos.

# Evento de alteração de email
Tópico: "eventos_usuario"
Partição 0: 
  offset 0: { tipo: "USUARIO_CRIADO", email: "a@b.com", id: 1 }
  offset 1: { tipo: "EMAIL_ALTERADO", novo_email: "c@d.com", id: 1 }

4.2. CQRS com Kafka: separando comandos de escrita e consultas de leitura

Comandos (escrita) vão para um tópico Kafka. Um serviço projeta os dados em um banco de leitura otimizado (ex: Elasticsearch, Redis).

# Fluxo CQRS
POST /api/comando → Kafka (tópico "comandos") → Serviço de Comando → Banco de Escrita
Serviço de Projeção → Kafka (tópico "eventos") → Banco de Leitura
GET /api/consulta → Banco de Leitura

4.3. Webhooks vs. Kafka: quando usar cada abordagem

  • Webhooks: ideais para notificações simples e externas (ex: pagamento confirmado → chamar URL do parceiro).
  • Kafka: melhor para fluxos internos de alta frequência, replay e múltiplos consumidores.

Use webhooks para integrações externas; Kafka para streams internos e event sourcing.

5. Casos de uso práticos no ecossistema web

5.1. Rastreamento de cliques e analytics em tempo real (frontend → Kafka → dashboard)

// Frontend envia evento via fetch ou WebSocket
fetch('/api/track', {
  method: 'POST',
  body: JSON.stringify({ evento: 'clique', elemento: 'botao-comprar', timestamp: Date.now() })
})

// Backend produz para Kafka
app.post('/api/track', async (req, res) => {
  await producer.send({ topic: 'cliques', messages: [{ value: JSON.stringify(req.body) }] })
  res.status(202).send()
})

// Consumidor alimenta dashboard em tempo real (ex: Spark Streaming ou Kafka Streams)

5.2. Sincronização de cache (Redis) e invalidação via eventos do Kafka

Quando um dado é atualizado no banco, publique um evento no Kafka. Consumidores invalidam ou atualizam o cache Redis.

# Evento de invalidação
Tópico: "cache_invalidation"
{ chave: "produto:123", acao: "atualizar" }

# Consumidor Redis
consumer.run({
  eachMessage: async ({ message }) => {
    const { chave } = JSON.parse(message.value)
    await redis.del(chave) // invalida cache
  }
})

5.3. Orquestração de jobs assíncronos: envio de e-mails, processamento de imagens, webhooks

# Produtor: job de envio de email
producer.send({
  topic: 'jobs-email',
  messages: [{ value: JSON.stringify({ to: 'user@example.com', template: 'boas-vindas' }) }]
})

# Consumidor: processa e envia email
consumer.run({
  eachMessage: async ({ message }) => {
    const job = JSON.parse(message.value)
    await emailService.send(job.to, job.template)
  }
})

6. Tratamento de falhas e resiliência em aplicações web com Kafka

6.1. Estratégias de retry e dead letter queues (DLQ) para mensagens com falha

Crie um tópico separado para mensagens que falharam após N tentativas.

# Estrutura de retry
Tópico: "jobs-email"
Tópico: "jobs-email-retry" (com TTL maior)
Tópico: "jobs-email-dlq" (mensagens mortas)

# Lógica no consumidor
try {
  await processarMensagem(message)
} catch (error) {
  if (tentativas < 3) {
    await producer.send({ topic: 'jobs-email-retry', messages: [message] })
  } else {
    await producer.send({ topic: 'jobs-email-dlq', messages: [message] })
  }
}

6.2. Lidando com partições offline e rebalanceamento de consumidores

Quando um consumidor cai, o grupo rebalanceia — as partições do consumidor falho são reassignadas. Para minimizar impacto, use session.timeout.ms adequado e processamento idempotente.

6.3. Monitoramento básico: lag de consumidores e throughput

# Verificar lag de consumidores via CLI
kafka-consumer-groups --bootstrap-server localhost:9092 --group web-group --describe

# Saída esperada:
# GROUP           TOPIC           PARTITION  CURRENT-OFFSET  LOG-END-OFFSET  LAG
# web-group       eventos-web     0          150             200             50

Lag alto indica que consumidores não estão acompanhando a produção. Aumente partições ou consumidores.

7. Boas práticas e armadilhas comuns para desenvolvedores web

7.1. Serialização e evolução de schemas com Avro/Protobuf vs. JSON simples

JSON é fácil, mas não possui schema rígido. Use Avro com Schema Registry para evolução segura:

# Vantagens do Avro
- Schemas versionados
- Tamanho menor que JSON
- Compatibilidade direta/retroativa

# Exemplo de schema Avro
{
  "type": "record",
  "name": "Clique",
  "fields": [
    {"name": "usuario_id", "type": "string"},
    {"name": "pagina", "type": "string"}
  ]
}

7.2. Tamanho de mensagens e tuning de produtores/consumidores para latência web

  • Mensagens grandes (>1MB): aumente max.message.bytes no broker e no produtor.
  • Latência: use linger.ms (tempo de espera para batch) e batch.size — trade-off entre latência e throughput.
  • Compressão: ative compression.type: snappy para reduzir tráfego.
# Configuração de produtor para baixa latência
await producer.send({
  topic: 'eventos-rapidos',
  messages: [message],
  acks: 1,       # apenas líder confirma
  compression: 'snappy'
})

7.3. Segurança básica: autenticação SASL e criptografia TLS em ambientes web

Em produção, nunca use PLAINTEXT. Configure SASL/SCRAM e TLS:

# Exemplo de configuração Kafka com SASL
security.protocol=SASL_SSL
sasl.mechanism=SCRAM-SHA-256
sasl.jaas.config=org.apache.kafka.common.security.scram.ScramLoginModule required \
  username="user" password="pass";

Referências