Como projetar sistemas de cache distribuído entre regiões

1. Fundamentos e Desafios do Cache Multi-Região

Projetar um sistema de cache distribuído entre regiões geográficas é um dos desafios mais complexos em arquiteturas globais. O principal objetivo é reduzir a latência para usuários espalhados pelo mundo, mantendo dados acessíveis e consistentes.

Os desafios fundamentais incluem:

  • Latência de rede: A velocidade da luz impõe limites físicos. Uma requisição entre São Paulo e Singapura pode levar 200-300ms apenas em trânsito.
  • Custos de transferência: Provedores como AWS, GCP e Azure cobram por tráfego entre regiões. Um cache mal projetado pode gerar custos exorbitantes.
  • Consistência de dados: Em ambientes geo-distribuídos, a consistência forte é cara. A consistência eventual é mais prática, mas exige estratégias cuidadosas.
  • Workloads críticos: Aplicações de leitura intensa (catálogos de produtos, feeds) se beneficiam mais do cache regional do que sistemas de escrita frequente (logs, transações bancárias).

2. Estratégias de Roteamento e Localidade de Dados

A hierarquia de cache é essencial para equilibrar performance e custo.

Cache Local (L1) vs. Cache Regional (L2)

  • L1: Cache na memória da aplicação (ex: Redis local, HashMap). Extremamente rápido, mas volátil e limitado ao servidor.
  • L2: Cache compartilhado na região (ex: Redis Cluster, ElastiCache). Serve múltiplas instâncias da aplicação na mesma região.

Roteamento por Geolocalização

O DNS Anycast ou GeoDNS roteia o usuário para o cache mais próximo:

# Exemplo de configuração GeoDNS (Route53)
us-east-1.cache.minhaapp.com  A 203.0.113.10
eu-west-1.cache.minhaapp.com  A 203.0.113.20
ap-southeast-1.cache.minhaapp.com  A 203.0.113.30

Sharding por Região

Para evitar hot spots, fragmentamos os dados por região usando uma chave composta:

# Exemplo de chave de cache com região
chave_cache = "us-east-1:produto:12345"
valor_cache = {
  "nome": "Smartphone X",
  "preco": 2499.00,
  "estoque_regional": 150,
  "timestamp": 1699000000
}

3. Modelos de Replicação e Sincronização

Replicação Assíncrona com Filas

Usamos Apache Kafka ou RabbitMQ para propagar atualizações entre regiões:

# Publicador na região primária (us-east-1)
mensagem = {
  "evento": "CACHE_INVALIDATE",
  "chave": "produto:12345",
  "regiao_origem": "us-east-1",
  "timestamp": 1699000000
}
kafka_producer.send("cache-invalidation", mensagem)

Write-Through e Write-Behind

  • Write-through: Escreve no cache e no banco simultaneamente. Consistente, mas mais lento.
  • Write-behind: Escreve no cache primeiro e propaga para o banco assincronamente. Mais rápido, mas com risco de perda.

Estratégias de Invalidação

  • TTL (Time-to-Live): Simples, mas pode servir dados obsoletos.
  • Pub/Sub: Invalidação em tempo real via Redis Pub/Sub ou Kafka.
  • Versionamento: Cada chave carrega um número de versão. O cliente verifica se a versão local é a mais recente.
# Exemplo de chave versionada
chave = "produto:12345"
valor = {
  "dados": { "nome": "Smartphone X", "preco": 2499.00 },
  "versao": 42
}

4. Arquitetura de Dados e Escolha de Tecnologias

Redis Cluster vs. Memcached vs. DAX

Tecnologia Ideal para Multi-região nativo?
Redis Cluster Estruturas complexas, pub/sub, persistência Não (precisa de Active-Active ou replicação manual)
Memcached Cache simples, alta throughput Não
DynamoDB Accelerator (DAX) Cache para DynamoDB Sim (integrado com Global Tables)

CRDTs para Conflitos

CRDTs (Conflict-free Replicated Data Types) permitem atualizações concorrentes sem conflitos:

# Exemplo de CRDT contador
contador_regiao_a = { "valor": 10, "timestamp": 1699000000 }
contador_regiao_b = { "valor": 5, "timestamp": 1699000001 }
# Merge: valor = max(10, 5) = 10 (depende da política)
resultado = merge(contador_regiao_a, contador_regiao_b)

Cache de Borda (CDN) + Cache de Aplicação

Combine CDN (CloudFront, Cloudflare) para assets estáticos com cache de aplicação para dados dinâmicos:

# Fluxo de requisição
1. Usuário -> CDN (cache de borda)
2. CDN miss -> Cache de aplicação regional (Redis)
3. Redis miss -> Banco de dados regional (fallback)
4. Banco responde -> Redis armazena -> CDN armazena -> Usuário

5. Tolerância a Falhas e Resiliência Regional

Failover Automático

Implemente um mecanismo de failover entre regiões primária e secundária:

# Configuração de failover (exemplo com Redis Sentinel)
sentinel monitor mymaster redis-primary 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 60000
sentinel parallel-syncs mymaster 1

Degradação Graciosa

Quando o cache regional falha, o fallback deve ser para o banco de dados local, não para outra região:

def buscar_produto(produto_id, regiao_usuario):
    try:
        # Tenta cache regional primeiro
        return cache_regional.get(f"produto:{produto_id}")
    except CacheRegionFail:
        # Fallback para banco local (não cross-região)
        return banco_local.query(f"SELECT * FROM produtos WHERE id = {produto_id}")

Proteção contra Thundering Herd

Use bloqueio distribuído (lock) para evitar que múltiplas requisições recriem o mesmo cache simultaneamente:

def recriar_cache(chave, funcao_criacao):
    if redis.setnx(f"lock:{chave}", "locked", ex=10):
        try:
            dados = funcao_criacao()
            redis.set(chave, dados, ex=300)
        finally:
            redis.delete(f"lock:{chave}")
        return dados
    else:
        # Aguarda lock ser liberado
        sleep(0.1)
        return redis.get(chave)

6. Monitoramento, Métricas e Otimização de Custo

Métricas-Chave

  • Hit ratio: Percentual de requisições servidas pelo cache. Alvo: >90%.
  • Latência P99: 99% das requisições devem responder em <10ms (cache) vs <100ms (banco).
  • Throughput por região: Requisições por segundo por região.

Ferramentas de Observabilidade

  • AWS X-Ray / Google Cloud Trace: Tracing distribuído entre regiões.
  • Prometheus + Grafana: Métricas em tempo real.
  • Elasticsearch + Kibana: Logs centralizados.

Análise de Custo-Benefício

# Cálculo de economia com cache
Custo sem cache:
  10M requisições/dia * 0.0001 USD (banco) = 1000 USD/dia

Custo com cache (90% hit ratio):
  1M requisições/dia * 0.0001 USD (banco) = 100 USD/dia
  9M requisições/dia * 0.00001 USD (cache) = 90 USD/dia
  Total = 190 USD/dia
  Economia = 810 USD/dia

7. Implementação Prática e Ciclo de Vida

Configuração de Topologia Multi-Região

# Topologia recomendada para cache multi-região
Região Primária (us-east-1):
  - Redis Cluster (3 nós)
  - Aplicação (10 instâncias)
  - Kafka (3 brokers)

Região Secundária (eu-west-1):
  - Redis Cluster (3 nós)
  - Aplicação (5 instâncias)
  - Kafka (3 brokers)

Região Terciária (ap-southeast-1):
  - Redis Cluster (3 nós)
  - Aplicação (3 instâncias)
  - Kafka (3 brokers)

Comunicação entre regiões:
  - Kafka MirrorMaker para replicação assíncrona
  - DNS Anycast para roteamento de usuários
  - Health checks periódicos entre regiões

Cache Warming para Novas Regiões

Ao adicionar uma nova região, pré-popule o cache com dados críticos:

def warm_cache(nova_regiao, dados_criticos):
    for chave, valor in dados_criticos.items():
        cache_regional.set(chave, valor, ex=3600)  # TTL inicial de 1 hora
    logger.info(f"Cache warming concluído para {nova_regiao}")

Políticas de Expiração Inteligente

Use TTLs variáveis baseados na frequência de atualização:

# TTL dinâmico baseado em popularidade
def calcular_ttl(produto_id, acessos_ultima_hora):
    if acessos_ultima_hora > 1000:
        return 300  # 5 minutos para itens populares
    elif acessos_ultima_hora > 100:
        return 600  # 10 minutos
    else:
        return 1800  # 30 minutos para itens menos acessados

Referências