Pular para o conteúdo
Voltar para o acervo
Engenharia5 min de leituraPasso 2 de 8

Cache: as quatro armadilhas que ninguém antecipa

Colocar cache é fácil. O difícil é conviver com as quatro consequências que ele cria, e todas as quatro têm solução conhecida.

· Gabriel Dias
cacheperformancebackend

Phil Karlton disse que existem apenas duas coisas difíceis em computação: invalidação de cache e dar nome às coisas. A piada envelheceu bem porque a primeira metade continua verdadeira.

Cache é guardar o resultado caro para não recalcular. A ideia é trivial. O que não é trivial é decidir quando aquele resultado deixou de valer, e o que acontece com o seu sistema no instante em que muitos deles deixam de valer ao mesmo tempo.

Onde cachear

Do mais perto do usuário para o mais longe:

  1. NavegadorgrátisControlado por cabeçalho HTTP. A camada mais subutilizada.
  2. CDNbordaServe perto do usuário, com a diretiva s-maxage.
  3. Local da aplicaçãonanossegundosEm memória, no processo. Cada instância tem o seu, e eles divergem.
  4. Distribuídouma ida à redeRedis, Memcached. Compartilhado e consistente entre instâncias.
  5. Do banconão controladoBuffer pool e page cache. Explica a segunda execução ser mais rápida.
Quanto mais alto na pilha, mais barato o acerto e mais difícil a invalidação.

Navegador. Grátis, rápido, e você controla por cabeçalho HTTP. É a camada mais subutilizada.

CDN. Serve conteúdo da borda, perto do usuário. Também controlada por cabeçalho, com a diretiva s-maxage.

Cache local da aplicação. Em memória, no próprio processo. Latência de nanossegundos. Problema: cada instância tem o seu, e eles divergem.

Cache distribuído. Redis, Memcached. Compartilhado entre instâncias, consistente entre elas, com o custo de uma ida à rede.

Cache do banco. Buffer pool, page cache do sistema operacional. Você não controla diretamente, mas ele explica por que a segunda execução da mesma query é muito mais rápida.

As três estratégias

Cache-aside é a mais comum: a aplicação procura no cache; se não achar, busca no banco e grava no cache. Simples, você controla tudo, e o cache pode cair sem derrubar a aplicação.

Write-through grava no cache e no banco ao mesmo tempo. Mais consistente, escrita mais lenta.

Write-behind grava no cache e no banco depois, em lote. Rápido e arriscado: se cair antes de descarregar, você perdeu escrita.

Para a maioria dos casos, cache-aside é a resposta certa. As outras duas resolvem problemas específicos e trazem modos de falha próprios.

Armadilha 1: dado velho

O TTL é a expressão numérica da sua tolerância a estar errado.

Não existe TTL certo em abstrato. Existe a resposta para: "quanto tempo esse dado pode estar desatualizado sem alguém se importar?".

Preço de produto: segundos. Nome de categoria: horas. Configuração de feature: minutos. Perfil do usuário logado: essa é a difícil, porque o próprio usuário percebe na hora, e aí a resposta é invalidar na escrita, não confiar no TTL.

O erro comum é escolher um TTL único para tudo, normalmente cinco minutos, porque foi o que estava no exemplo da documentação.

Armadilha 2: estouro de cache

Você populou o cache em um deploy, com TTL de uma hora. Uma hora depois, tudo expira no mesmo instante, e todo o tráfego bate no banco de uma vez.

A causa é a sincronização. A correção é o jitter: adicione uma variação aleatória ao TTL, tipo entre 50 e 70 minutos em vez de 60 exatos. Isso espalha as expirações.

É a mesma ideia do jitter em retry, e pelo mesmo motivo: eventos sincronizados criam picos que o sistema não aguenta, mesmo tendo capacidade média de sobra.

Armadilha 3: debandada

Uma chave popular expira. Mil requisições simultâneas descobrem isso no mesmo milissegundo e todas vão recalcular a mesma coisa.

O nome é thundering herd, e a solução é deixar apenas uma recalcular:

  • Um lock por chave: quem pegar o lock recalcula; os outros esperam ou servem o valor antigo.
  • Ou recomputação antecipada: quando faltar pouco para expirar, uma requisição sorteada renova em segundo plano enquanto as outras continuam servindo o valor atual.

A segunda é melhor, porque ninguém espera.

Armadilha 4: cache negativo

Alguém consulta um id que não existe. Você não encontra e não guarda nada. A próxima consulta ao mesmo id inexistente bate no banco de novo.

Se isso vier em volume (e vem, porque é assim que raspadores e testes automatizados se comportam), você tem uma rota que ignora completamente o cache.

Guarde o negativo também, com TTL curto. E se o volume for realmente grande, um Bloom filter na frente responde "definitivamente não existe" sem nem tocar no cache.

O cache como dependência crítica

Cache-aside bem implementado degrada: sem cache, tudo fica mais lento, e continua funcionando. Isso exige que o caminho para o banco continue existindo e que o seu banco aguente a carga sem o cache, nem que seja com descarte de carga.

O número que importa

Monitore a taxa de acerto por chave ou por prefixo, não só a global.

Abaixo de oitenta por cento em cache de leitura, uma das duas coisas está errada: a chave está granular demais, ou o TTL está curto demais para o padrão de acesso. Nos dois casos, você está pagando a complexidade e a inconsistência sem receber o benefício.

E monitore também a taxa de invalidação. Se ela for alta, você está gastando mais tempo invalidando do que servindo.

Resumo prático

Cache troca frescor por velocidade. O trabalho de engenharia não é ligar o cache; é decidir explicitamente quanto de frescor você topa perder, por chave, e depois defender o sistema contra as quatro formas conhecidas de isso dar errado.

Leia depois disto

Fala comigo

Dúvida sobre o artigo? Me chama no WhatsApp

Sem formulário e sem lista de e-mail. Se você discorda de alguma coisa que eu escrevi, ou quer contar como resolveu aí, a conversa é direta comigo.

Abrir conversa