Para pôr em cache uma API de alto tráfego, meça primeiro e depois coloque uma camada cache-aside em Redis à frente dos endpoints mais lentos e mais lidos, com TTL explícitos, invalidação na escrita e proteção contra avalanches de pedidos (cache stampede). O Elasticsearch serve para a pesquisa e as listagens filtradas que a base de dados principal trata mal. Dados específicos de cada utilizador ou que exigem consistência estrita nunca devem ir para a cache sem um desenho pensado para isso.
Medir a latência P95 antes de acrescentar qualquer cache
As médias escondem os pedidos de que os utilizadores se queixam. Acompanhe a latência P95 e P99 por endpoint, a par do volume de pedidos, para ver que rotas são ao mesmo tempo lentas e muito usadas.
Depois, descubra onde se perde o tempo. Um trace ou uma simples decomposição dos tempos por pedido mostra normalmente se o custo vem de uma consulta lenta, de consultas repetidas, de uma chamada a uma API externa ou da serialização. Pôr em cache uma resposta cujo verdadeiro problema é um índice em falta só esconde esse problema até à próxima falha de cache.
- Registar P50, P95 e P99 por endpoint, e não apenas um número global
- Ordenar os endpoints pelo volume de pedidos multiplicado pela latência, para ver onde a cache compensa mais
- Verificar a relação entre leituras e escritas, porque os dados lidos muito mais vezes do que mudam são os melhores candidatos
- Corrigir índices em falta e consultas N+1 antes de pôr uma cache à volta delas
Usar cache-aside como padrão por omissão
No padrão cache-aside, a aplicação consulta primeiro o Redis. Se o valor lá estiver, devolve-o. Se não estiver, lê da base de dados, escreve o resultado no Redis com um TTL e devolve-o.
O padrão mantém a base de dados como fonte de verdade e falha de forma segura. Se o Redis estiver lento ou indisponível, a aplicação recorre à base de dados, com um timeout curto e um circuit breaker, para que uma cache em dificuldades não atrase todos os pedidos.
As chaves devem ser desenhadas com cuidado. Incluem o tipo de recurso, o identificador, a versão da API e todos os parâmetros que alteram a resposta, como o idioma ou a página. Um esquema de chaves previsível é o que torna possível, mais tarde, uma invalidação dirigida.
Definir o TTL pela desatualização aceitável e invalidar quando os dados mudam
Um TTL é uma decisão de negócio escrita como um número. A pergunta é até que ponto os dados podem estar desatualizados antes de prejudicarem um utilizador ou um sistema a jusante, e o TTL define-se a partir dessa resposta. O resultado de um jogo em direto e um artigo arquivado toleram graus de desatualização muito diferentes.
Depender apenas da expiração significa servir dados desatualizados até o TTL acabar. Para os dados que a própria aplicação altera, apague ou reescreva a chave depois de a escrita ser confirmada, idealmente a partir de um evento emitido após a transação, para que a cache nunca guarde dados que a base de dados reverteu.
Acrescente uma pequena variação aleatória (jitter) aos TTL, para que as chaves escritas ao mesmo tempo não expirem todas no mesmo instante.
- TTL curto, de alguns segundos, para dados que mudam depressa e em que uma ligeira desatualização é aceitável
- TTL mais longo com invalidação na escrita para dados controlados pela aplicação que raramente mudam
- Chaves com versão, em que subir um número de versão invalida de uma só vez um grupo inteiro de chaves
Proteger a base de dados contra avalanches de pedidos na cache
Uma avalanche (cache stampede) acontece quando uma chave popular expira e muitos pedidos simultâneos falham a cache ao mesmo tempo, enviando todos a mesma consulta pesada à base de dados. Numa API de alto tráfego, isto pode sobrecarregar a base de dados exatamente no pico de tráfego.
Nas chaves mais solicitadas, combine duas ou mais destas defesas.
- Agregação de pedidos: um só pedido reconstrói a chave sob um lock curto no Redis, criado com NX e uma expiração, enquanto os outros esperam um pouco ou servem o valor anterior
- Stale-while-revalidate: guardar uma expiração flexível dentro do valor, continuar a servir a cópia desatualizada depois dela e atualizar em segundo plano
- Atualização antecipada probabilística: reconstruir de vez em quando uma chave muito solicitada antes de expirar, com uma probabilidade que aumenta à medida que a expiração se aproxima
- Pré-aquecimento: preencher as chaves mais solicitadas antes de um pico de tráfego previsto, como um grande evento em direto
Usar o Elasticsearch para pesquisa e listagens, não como cache genérica
O Redis é o melhor para consultas chave-valor: um único objeto, um fragmento calculado, um contador de limite de pedidos. O Elasticsearch serve outra finalidade: pesquisa em texto integral, filtros por facetas e listagens ordenadas que são caras de calcular numa base de dados relacional.
O índice Elasticsearch deve ser tratado como um modelo de leitura alimentado pela base de dados principal, através de eventos de alteração ou de uma sincronização agendada, aceitando que tem consistência eventual. A base de dados continua a fazer fé para as escritas e para tudo o que tenha de ser exato.
Na plataforma de media desportivos da NorthStar Network, que serve 50M+ utilizadores por mês, os nossos engenheiros reestruturaram APIs críticas e redesenharam a cache em Redis e Elasticsearch, o que reduziu os tempos de resposta P95.
Saber o que não pôr em cache
O erro de cache mais caro é servir os dados de um utilizador a outro. Reveja cada endpoint em cache para confirmar que a identidade está na chave, e teste-o com duas contas diferentes antes do lançamento.
- Respostas que dependem da identidade ou das permissões de quem chama, a menos que a chave inclua o utilizador ou a função
- Dados que têm de ser estritamente consistentes, como saldos, stock no checkout ou tudo o que seja usado em decisões de autorização
- Endpoints de escrita, tokens de utilização única e tudo o que tenha efeitos secundários
- Endpoints com pouco tráfego, onde uma cache acrescenta complexidade e um novo modo de falha para pouco ganho
- Payloads muito grandes que expulsam muitas chaves mais pequenas e mais solicitadas
Tornar a cache observável, ou não será possível confiar nela
Acompanhe a taxa de acerto por prefixo de chave, a memória usada e as expulsões no Redis, a latência dos comandos Redis e a carga da base de dados, ao lado do P95 da API, no mesmo painel. Quando o P95 se mexe, deve ser possível perceber em minutos se a causa foi a cache, a base de dados ou um serviço a montante.
Configure alertas para uma queda súbita da taxa de acerto, que muitas vezes significa que um deploy mudou o formato de uma chave, e para um aumento das expulsões, que indica que a cache é pequena demais para o seu conjunto de trabalho.
Assumimos o trabalho de desempenho de APIs como um bloco definido: medição, redesenho da camada de cache e entrega de painéis e runbooks à equipa que a opera.
Pontos essenciais
- Medir o P95 por endpoint e corrigir os problemas de consultas antes de acrescentar uma cache.
- O cache-aside em Redis é a opção por omissão mais segura, porque a base de dados continua a ser a fonte de verdade.
- Definir os TTL pela desatualização aceitável dos dados e invalidar na escrita os dados que a aplicação controla.
- Proteger as chaves mais solicitadas contra avalanches com locks, stale-while-revalidate ou pré-aquecimento.
- Usar o Elasticsearch como modelo de leitura com consistência eventual para pesquisa e listagens, não como cache genérica.
Perguntas frequentes
Redis ou Elasticsearch para pôr respostas de API em cache?
O Redis serve para a cache chave-valor de objetos, fragmentos calculados e contadores, porque as consultas por chave são rápidas e simples de invalidar. O Elasticsearch serve quando a parte cara é a pesquisa, a filtragem ou a ordenação sobre muitos registos, e o seu índice deve ser tratado como um modelo de leitura e não como uma cache.
Qual é um bom TTL para respostas de API?
Não há um valor universal. Defina cada TTL em função de quão desatualizados esses dados podem estar sem prejudicar um utilizador, use alguns segundos para dados que mudam depressa e combine TTL mais longos com invalidação na escrita. Acrescente uma variação aleatória para que chaves relacionadas não expirem em conjunto.
Como evitar uma avalanche de pedidos (cache stampede) no Redis?
Deixe apenas um pedido reconstruir uma chave expirada, com um lock curto obtido por SET com a opção NX, e faça os outros pedidos esperar um pouco ou servir o valor anterior. O stale-while-revalidate e a atualização antecipada das chaves mais solicitadas reduzem, à partida, o número de expirações bruscas.