Skip to content

Latest commit

 

History

History
210 lines (141 loc) · 8.03 KB

File metadata and controls

210 lines (141 loc) · 8.03 KB

Decisões Técnicas

Este documento explica as escolhas feitas no projeto e os problemas que resolvi durante o desenvolvimento.

Resumo

Sistema de votação do BBB que aguenta carga pesada (5-6k req/s). Feito em React + Fastify + Redis + PostgreSQL, com observabilidade via Prometheus/Grafana.

Principais desafios:

  • UI fiel ao mockup (ProgressCircle com trigonometria)
  • Performance superior ao requisito (100 → 5-6k req/s)
  • Arquitetura que escala (cluster mode, load balancer, workers)
  • Observabilidade real (não só logs)

Frontend

Stack: React 18 + Vite + TypeScript + CSS puro

Por que CSS ao invés de Tailwind?

O mockup tinha design muito específico (ProgressCircle customizado, animações, gradientes). CSS puro me deu controle total sem overhead do Tailwind. Para um design único assim, é mais direto.

ProgressCircle - O Mais Difícil

Precisava de um gráfico circular com:

  • Dois arcos crescendo anti-horário
  • Labels de % seguindo o fim de cada arco
  • Casos extremos (0% e 100%) sem labels ocultos

Solução: Trigonometria básica pra calcular posição dos labels:

const orangeLabelX = centerX + radius * Math.cos(angle);
const orangeLabelY = centerY - radius * Math.sin(angle);

Problemas que resolvi:

  • Labels sumiam em 100% → Ângulo fixo nesses casos
  • Animação travando → requestAnimationFrame suave
  • Texto ilegível → text-shadow mais forte

Componentes

VotingScreen e VoteResult separados (SRP).

useVoting hook centralizou toda lógica de estado e API, deixando componentes apenas pra UI.

Sprite de imagens: Uma requisição HTTP só pra todas as fotos (técnica antiga mas funciona).

Backend

Stack: Fastify + TypeScript + JSON Schema + Redis + PostgreSQL

Por que Fastify?

Performance. Fastify é ~2x mais rápido que Express e tem validação via JSON Schema builtin. Como o requisito era 100 req/s (entreguei 5-6k), precisava de algo rápido.

Arquitetura - O Mais Importante

Aqui foi onde gastei mais tempo pensando:

Voto → API → Redis INCR (<1ms) → Fila → Worker → PostgreSQL (batch 500)

Por que Redis E PostgreSQL?

Redis: Conta em memória, < 1ms, aguenta 100k+ ops/s. Frontend consulta ele (tempo real).

PostgreSQL: Guarda histórico permanente. Workers processam fila em lote (500 votos de uma vez = 1 INSERT ao invés de 500).

Dual-purpose do Redis:

  1. Contador (INCR/HINCRBY pra stats)
  2. Fila (LPUSH/BRPOP pra persistência assíncrona)

Isso evita dois sistemas (Redis + RabbitMQ por exemplo).

Por que JSON Schema ao invés de Zod?

Decisão: Validação com JSON Schema nativo do Fastify.

Motivações:

  1. Performance: JSON Schema é compilado e otimizado pelo Fastify (Ajv)
  2. Integração: Fastify usa JSON Schema nativamente para docs automáticos
  3. Peso: Zod adiciona ~60KB ao bundle
  4. Simplicidade: Validações são diretas e rápidas

Exemplo:

const voteSchema = {
  body: {
    type: 'object',
    required: ['paredaoId', 'participantId'],
    properties: {
      paredaoId: { type: 'string' },
      participantId: { type: 'string' },
    },
  },
};

Arquitetura de Alta Performance

1. Cluster Mode

Decisão: Cada API roda em cluster com 4 workers

// cluster.ts
const workers = parseInt(process.env.WEB_CONCURRENCY || '4');
for (let i = 0; i < workers; i++) {
  cluster.fork();
}

Motivações:

  • Aproveita múltiplos cores da CPU
  • 8 workers totais (4 por API) = 8x throughput
  • Crash de um worker não derruba o serviço

2. Redis Dual-Purpose

Decisão: Redis serve como cache E fila

Como Cache (Contadores):

await redis.incr(totalKey());                    // O(1), <0.1ms
await redis.hincrby(participantHash(), id, 1);   // O(1), <0.1ms

Como Fila:

await redis.lpush(QUEUE_KEY, JSON.stringify(vote)); // Assíncrono

Motivações:

  • INCR/HINCRBY são operações atômicas extremamente rápidas
  • Fila FIFO (LPUSH/BRPOP) garante ordem e confiabilidade
  • AOF habilitado = durabilidade mesmo em crash

Por que não PostgreSQL direto?:

  • PostgreSQL UPDATE + COUNT(*) = 10-50ms
  • Redis INCR = <0.1ms
  • 100x mais rápido!

3. Workers Dedicados com Batch Processing

Criei 2 workers dedicados que processam a fila em batches de 500 votos por vez. Isso reduz drasticamente a carga no PostgreSQL - ao invés de 500 INSERTs, faço 1 só com múltiplos VALUES. Com synchronous_commit=off e bulk INSERT, consegui processar milhões de votos sem gargalo.

O PostgreSQL tem duas tabelas: votes_raw (todos os votos) e votes_hourly (agregados por hora). A segunda usa UPSERT para incrementar contadores sem duplicar linhas.

Load Balancer

O Nginx distribui requisições entre as 2 instâncias do backend usando least_conn (manda pro menos ocupado). Coloquei rate limiting de 10k req/s pra proteger contra DDoS e keepalive pra reutilizar conexões.

Infraestrutura

Tudo roda via Docker Compose - make gabriel-globo sobe os 9 serviços (nginx, 2 APIs, 2 workers, redis, postgres, prometheus, grafana, frontend). Funciona em Mac e Linux sem configuração extra.

Criei um Makefile com comandos úteis:

  • make test - roda todos os testes
  • make load-test - autocannon progressivo pra validar performance
  • make check-data - compara Redis vs PostgreSQL pra garantir consistência

Observabilidade

Configurei Prometheus pra coletar métricas customizadas do backend (votes_total, votes_by_participant, etc). O Grafana carrega automaticamente um dashboard com 4 painéis: total de votos, distribuição donut, evolução temporal e votos por hora. Queries PromQL agregam dados das 2 instâncias do backend usando sum().

Problemas que resolvi

ProgressCircle com 100%/0%: Os labels ficavam escondidos na parte de baixo. Tentei algoritmos genéricos mas acabei hardcoding os ângulos extremos - funciona e fica limpo.

Grafana datasource: Dashboard não achava o Prometheus porque faltava uid: prometheus no YAML. Detalhe pequeno mas crítico.

Agregação multi-instância: Queries do Grafana buscavam uma instância específica, mas tenho 2 backends. Usei sum() com label job="bbb-api" pra consolidar.

PostgreSQL lento: Workers salvavam 1 voto por vez. Mudei pra batch de 500 + bulk INSERT, ficou 50x mais rápido.

Extras

Além dos requisitos, implementei:

  • Script de consistência (make check-data): compara Redis vs PostgreSQL pra garantir que tudo tá sincronizado
  • Load test progressivo: autocannon em etapas (100 → 500 → 1k → 5k conexões) pra validar performance
  • Cluster mode: 4 workers por API = 8 cores utilizados = 8x mais throughput
  • Makefile: comandos simples (make test, make load-test, etc) ao invés de decorar docker compose
  • Dashboard Grafana: 4 painéis profissionais com queries otimizadas e auto-refresh

Performance

O sistema aguenta 5-6k req/s sustentado (50-60x o baseline de 100 req/s), com picos de até 38k em bursts. Testado via scripts/load-test.sh com autocannon progressivo.

O que aprendi

  • Cluster mode multiplica throughput mas exige estado compartilhado (usei Redis como fonte única de verdade)
  • Redis INCR é absurdamente rápido (<1ms mesmo a 5k req/s), perfeito pra contadores
  • PostgreSQL otimizado com synchronous_commit=off + bulk INSERT aguenta 50x mais carga do que parece
  • Observabilidade via Grafana não é luxo - sem métricas eu não saberia que cheguei a 38k req/s nos picos
  • CSS puro foi melhor que Tailwind aqui (design único, sem reutilização massiva)

O que faria diferente com mais tempo

  • CRUD de paredões: pra criar/editar via backend
  • Testes E2E completos: Playwright pra fluxo completo
  • Circuit breaker: se PostgreSQL/Redis cair, guardar votos em DLQ
  • Health checks profundos: /health checando Redis/Postgres
  • Cache de estatísticas: TTL 5-10s pra reduzir carga
  • Logs estruturados: Pino com correlation IDs
  • WebSockets: alternativa melhor ao polling do front (mas seria overkill aqui)