3.15

View in English

3.15 Cache e distribuição de conteúdo

Visão geral e motivação

Um cache é uma cópia de dados guardada em algum lugar mais rápido ou mais próximo que o original, para que vocês respondam a uma requisição sem refazer todo o trabalho caro. Quase todo sistema que parece rápido é rápido por causa do cache. A consulta ao banco de dados que levaria 40 milissegundos retorna em menos de um quando o resultado já está na memória. A imagem que atravessaria um oceano é servida de uma máquina na mesma cidade. O cache é a técnica de desempenho de maior alavancagem que vocês têm, e também a mais propensa a lhes entregar um bug sutil e enlouquecedor.

Este capítulo vai fundo na estratégia de cache. O capítulo 3.4 (arquitetura de dados e armazenamento) introduz os caches e as redes de distribuição de conteúdo como uma preocupação de armazenamento entre muitas, e o capítulo 3.13 (redes e conectividade) cobre o caminho de rede por onde eles viajam. Aqui vocês ficam com as decisões: onde pôr um cache, como dar chave a ele, quando invalidá-lo, como protegê-lo sob carga e como raciocinar sobre a obsolescência que vocês trocam por velocidade. O cache toca a engenharia de desempenho (capítulo 2.16), a escalabilidade e a resiliência (capítulo 3.5), as realidades de falha parcial dos sistemas distribuídos (capítulo 3.3) e, porque um cache envenenado pode servir um ataque a milhares de usuários, a segurança de aplicações (capítulo 4.2).

A motivação se resume a três alavancas. O cache corta a latência, então os usuários esperam menos. Corta a carga, então a sua origem atende mais tráfego no mesmo hardware. E corta o custo, porque uma requisição respondida na borda nunca toca o seu banco de dados, a sua computação nem a sua conta de saída. Para grandes equipes, uma estratégia de cache compartilhada é a diferença entre uma plataforma que escala de forma previsível e uma em que cada serviço reinventa a invalidação e erra. Em sistemas corporativos e governamentais, onde o tráfego dispara em prazos de declaração e dias de lançamento, um cache bem projetado costuma ser o que fica entre um portal funcionando e uma falha pública.

Princípios fundamentais

  • Façam cache para cortar a latência, a carga e o custo, e saibam qual dos três estão comprando.
  • Ponham os caches na camada certa da hierarquia, o mais perto de onde ajudam mais.
  • Tratem a invalidação como a parte difícil. Projetem chaves e tempos de vida antes de guardar em cache.
  • Protejam o cache sob carga com coalescência, jitter e defesas contra estouros de manada.
  • Escolham um padrão de escrita deliberadamente: a consistência e a velocidade puxam em sentidos opostos.
  • Meçam a taxa de acerto, a obsolescência e a carga na origem. Um cache sem medição é um passivo.
  • Tratem o conteúdo em cache como superfície de ataque. Um cache envenenado serve a todos.

Recomendações

Entenda a hierarquia de cache

O cache não é uma coisa só em um lugar só. É uma hierarquia de cópias, cada uma mais perto do usuário que a anterior, e vocês projetam ao longo de toda ela. Mais perto do usuário fica o cache do cliente: o cache HTTP do navegador, o armazenamento local de um aplicativo móvel, um cache de memória no processo. Em seguida vem a rede de distribuição de conteúdo (CDN), uma frota de servidores distribuídos pelo mundo que guardam cópias do conteúdo na borda da rede, perto dos usuários. Atrás dela fica o cache de proxy reverso ou de gateway, um cache compartilhado na frente dos seus servidores. Depois o cache da aplicação: um armazenamento rápido de chave e valor, como uma grade de dados em memória, guardando resultados calculados, sessões e fragmentos renderizados. Por fim, o próprio cache de consultas e de buffer do banco de dados, que mantém páginas quentes na memória para que o disco seja tocado menos.

Cada camada cumpre um trabalho distinto: o cache do cliente elimina a requisição por completo, a CDN absorve o tráfego global de leitura, o proxy reverso protege a origem do trabalho idêntico repetido, o cache da aplicação poupa recomputação e o cache do banco mantém o repositório responsivo. Uma requisição que erra todas as camadas e chega ao banco de dados é o caminho mais lento e mais caro que vocês têm, então o ponto da hierarquia é responder o mais acima e o mais fora que vocês puderem com segurança. Projetem-na como um sistema, porque um fragmento em cache na camada da aplicação e uma cópia obsoleta da CDN acima dele podem discordar de modos que confundem os usuários.

Trate a invalidação como o problema difícil

Há uma piada antiga de que os dois problemas mais difíceis da ciência da computação são nomear coisas, a invalidação de cache e os erros de off-by-one. A piada perdura porque a invalidação é genuinamente difícil: um cache é uma cópia, e no instante em que o original muda, toda cópia é uma mentira em potencial. Vocês têm três estratégias amplas. A expiração por tempo, com um tempo de vida (TTL), a duração em que uma entrada permanece válida antes de ser considerada obsoleta, é a mais simples: aceitam obsolescência limitada e deixam as entradas envelhecerem. A invalidação explícita expurga ou atualiza as entradas quando os dados subjacentes mudam, o que é preciso mas exige que vocês saibam todo lugar onde vive uma cópia. A invalidação orientada a eventos assina os caches a eventos de mudança para que se atualizem sozinhos, o que escala melhor entre muitos caches mas acrescenta uma dependência de mensageria.

A maioria dos sistemas reais mistura essas estratégias: TTLs curtos para dados que mudam com frequência e toleram segundos de obsolescência, TTLs mais longos com expurgo explícito para dados que mudam raramente mas precisam estar corretos quando mudam e chaves de cache versionadas para conteúdo imutável depois de publicado. O truque da chave versionada vale ser internalizado: em vez de invalidar, vocês mudam a chave. Uma folha de estilo servida como app.v187.css nunca precisa de expurgo, porque uma nova versão é uma nova chave e a antiga simplesmente deixa de ser pedida. Sempre que puderem transformar um problema de invalidação num problema de nomeação, façam isso.

Projete chaves de cache e TTLs de propósito

Um cache é só tão bom quanto a sua chave. A chave de cache é o identificador sob o qual um valor é guardado e consultado, e errá-la causa duas falhas opostas. Grossa demais, e vocês servem os dados de um usuário a outro: uma página personalizada em cache sob uma URL que ignora a identidade do usuário é um vazamento de dados. Fina demais, e a taxa de acerto desaba porque duas requisições nunca compartilham uma chave. Decidam deliberadamente o que pertence à chave: a identidade do recurso mais o que legitimamente varia a resposta (idioma, moeda, classe de dispositivo) e nada que não varie. Normalizem as chaves para que diferenças triviais, como a ordem dos parâmetros de consulta, não fragmentem o cache.

Os TTLs merecem o mesmo cuidado. Um TTL é uma promessa sobre a obsolescência máxima que vocês servirão, então definam-no a partir da tolerância real dos dados, não de um número redondo que alguém chutou: um painel de cotações tolera segundos, um catálogo de produtos minutos, uma regulamentação publicada horas ou uma chave versionada e nenhuma expiração. Acrescentem uma pequena dispersão aleatória, chamada jitter, para que um lote de entradas gravadas juntas não expire todo no mesmo instante e estoure a origem. Escrevam essas escolhas, porque um TTL sem justificativa é um número que o próximo engenheiro terá medo de mudar.

Proteja-se contra estouros de manada e coalesça as requisições

Quando uma entrada popular em cache expira, toda requisição que a queria erra ao mesmo tempo e corre junta para a origem. Esse é o estouro de manada do cache (cache stampede), também chamado de thundering herd, e pode derrubar o próprio banco de dados que o cache protegia. Construam as defesas uma vez e reutilizem-nas em toda parte. A coalescência de requisições (single-flight) deixa só a primeira requisição por uma chave ausente recalcular o valor enquanto as outras esperam pelo resultado, de modo que mil erros simultâneos causam uma chamada à origem. A recomputação antecipada probabilística atualiza uma entrada quente de forma aleatória um pouco antes de expirar, para que uma requisição em segundo plano a renove antes de a multidão ver um erro. Uma política stale-while-revalidate serve de imediato a cópia ligeiramente obsoleta e a atualiza de forma assíncrona, para que os usuários nunca esperem num erro.

Esses padrões importam mais exatamente quando vocês mais precisam do cache, sob carga de pico, então validem-nos em escala realista: uma defesa que funciona com dez usuários ainda pode falhar com dez mil. Combinem-nos com os padrões de resiliência do capítulo 3.5, em especial tempos limite e disjuntores, para que, quando a origem está genuinamente lenta, a camada de cache a proteja em vez de sobrecarregá-la. O objetivo é um cache que se comporte melhor sob pressão, não um que amplifique um pico numa queda.

Escolha um padrão de escrita deliberadamente

Como vocês tratam as escritas decide quão fresco o cache permanece e quanto vocês arriscam numa falha. Há quatro padrões comuns. No cache-aside (carregamento preguiçoso), a aplicação consulta o cache e, num erro, lê a origem, popula o cache e devolve o valor. As escritas vão para a origem e invalidam a entrada. É o padrão por um bom motivo: simples, e o cache guarda só o que é pedido. No write-through, toda escrita vai ao cache e à origem juntos, então o cache está sempre atual, ao custo da latência de escrita e de guardar dados que talvez nunca sejam lidos. No write-back (write-behind), as escritas acertam primeiro o cache e são descarregadas para a origem de forma assíncrona, o que torna as escritas rápidas mas arrisca perda se o cache morrer antes do descarregamento. No write-around, as escritas vão direto à origem e pulam o cache, evitando a agitação de dados de muita escrita raramente lidos, ao custo de um erro garantido na primeira leitura.

Escolham por carga de trabalho, não uma vez para o sistema inteiro. Um catálogo de muita leitura combina com cache-aside ou write-through. Um fluxo de log ou de métricas de muita escrita combina com write-around, para que o cache não seja agitado por dados que ninguém relê. O write-back combina com escritas de alta vazão em que um pequeno risco entendido de perda é aceitável e a durabilidade é tratada em outro lugar. Declarem explicitamente o padrão de cada cache, porque um leitor que presume cache-aside quando o código faz write-back julgará mal tanto o frescor quanto o comportamento em falhas.

Combine a política de remoção com o seu padrão de acesso

Um cache tem tamanho fixo, então quando enche, algo precisa sair. A política de remoção decide o quê. O menos recentemente usado (LRU) remove a entrada intocada há mais tempo, apostando que o uso recente prevê o uso futuro, e é um padrão sensato. O menos frequentemente usado (LFU) remove a entrada com menos acertos, o que combina com conjuntos quentes estáveis em que poucos itens são perenemente populares mas pode se agarrar a entradas que foram quentes uma vez e não se adaptar nunca. Variantes como LRU segmentado e políticas adaptativas misturam recência e frequência. O primeiro a entrar, primeiro a sair e a simples expiração por tempo são mais baratos mas mais toscos.

Combinem a política com o modo como os dados são acessados: LFU ou uma política consciente de frequência para um conjunto quente pequeno que raramente muda, LRU onde a popularidade se move com o tempo, como em notícias ou conteúdo em alta. Qualquer que seja a escolha, dimensionem o cache para que o conjunto quente caiba, porque um cache pequeno demais para guardar o conjunto de trabalho se debate, removendo entradas logo antes de serem necessárias de novo. Observem a taxa de remoção como métrica de primeira classe, já que uma subida repentina costuma significar que o cache está subdimensionado ou que uma explosão de chaves o está fragmentando.

Use corretamente a semântica de cache do HTTP

A web tem um modelo de cache maduro e padronizado embutido no HTTP, e usá-lo bem dá cache de cliente e de CDN de graça. O cabeçalho Cache-Control é a superfície de controle: max-age define o tempo de vida de frescor, public e private dizem se os caches compartilhados podem guardar a resposta, no-store proíbe o cache e stale-while-revalidate permite servir uma cópia obsoleta enquanto se atualiza. A validação deixa um cache conferir o frescor de forma barata sem rebuscar o corpo. Um ETag (entity tag) é um identificador opaco de versão que o servidor anexa a uma resposta. O cliente o devolve num cabeçalho If-None-Match, e o servidor responde 304 Not Modified sem corpo se nada mudou. O Last-Modified com If-Modified-Since faz o mesmo usando carimbos de data e hora.

A disciplina prática é ser explícito. Definam Cache-Control em toda resposta em vez de deixar os caches adivinharem por heurísticas. Marquem as respostas privadas, por usuário, como private ou no-store para que um proxy compartilhado nunca as guarde, um erro comum e perigoso. Usem URLs versionadas com max-age longo e a diretiva immutable para ativos estáticos, e validação com ETags para conteúdo que muda de modo imprevisível. Acertar esses cabeçalhos transforma toda a camada de cliente e de CDN num cache correto e baseado em padrões que vocês não precisaram construir.

Leve o trabalho à borda com CDNs e computação de borda

A CDN começou como um modo de guardar arquivos estáticos perto dos usuários, e ainda faz isso de forma soberba: imagens, scripts, vídeo e downloads servidos de um local de borda a milissegundos em vez de uma origem distante. As CDNs modernas vão além. Guardam conteúdo dinâmico e personalizado com chaves refinadas, terminam o TLS na borda, absorvem picos de tráfego e ataques distribuídos de negação de serviço e cada vez mais rodam o seu código. A computação de borda executa lógica nos próprios locais de borda, para que vocês possam personalizar uma resposta, verificar a autorização ou montar um fragmento de página sem uma ida e volta a uma região central.

Apoiem-se nisso para as leituras que dominam a maioria dos sistemas. Ponham os ativos estáticos atrás da CDN com URLs versionadas e longa duração, guardem em cache respostas de API na borda onde o frescor permitir (com chaves cuidadosas para que a personalização não vaze) e usem computação de borda para lógica leve e sensível à latência perto dos usuários. A troca é alcance versus controle: a borda é rápida e próxima mas longe dos seus dados e mais difícil de depurar, então mantenham na origem tudo que exige consistência forte ou estado autoritativo fresco e deixem a borda tratar o vasto tráfego de leitura que pode ir para o cache.

Trate o cache como superfície de ataque

Um cache serve a mesma resposta guardada a muitos usuários, o que o torna um alvo. O envenenamento de cache é um ataque em que uma requisição é forjada para que o cache guarde uma resposta nociva ou controlada pelo atacante e depois a sirva a todos que vêm em seguida. Costuma explorar uma entrada fora da chave: um cabeçalho que a aplicação reflete na resposta mas que o cache ignora ao montar a chave. O ataque relacionado de decepção de cache web engana um cache para guardar a resposta privada de uma vítima sob uma URL pública. Os dois são falhas de chaveamento e de confiança nas entradas, cobertas de modo mais amplo no capítulo 4.2.

Defendam-se deliberadamente. Incluam na chave de cache toda entrada que possa mudar a resposta e recusem-se a refletir cabeçalhos fora da chave em corpos em cache. Nunca deixem um cache compartilhado guardar respostas autenticadas, por usuário, sob uma chave compartilhada. Normalizem e validem os caminhos e parâmetros das requisições antes de guardar em cache. Definam Vary corretamente para que os caches particionem as respostas pelos cabeçalhos que realmente importam, como a codificação de conteúdo ou o idioma. Como uma única entrada envenenada prejudica todo usuário a jusante, tratem a configuração do cache como código sensível à segurança e revisem-na como tal.

Torne observável o comportamento do cache

Vocês não conseguem gerenciar um cache que não enxergam. A métrica principal é a taxa de acerto: a fração das requisições servidas do cache e não da origem. Uma taxa de acerto que cai em silêncio de 95 para 70 por cento pode multiplicar várias vezes a carga na origem e preceder uma queda, e vocês só a pegarão cedo se a observarem. Instrumentem cada camada separadamente, já que uma taxa de acerto saudável na CDN pode esconder uma taxa de acerto em colapso no cache da aplicação logo abaixo. Esta é a face específica de cache das práticas de observabilidade do capítulo 9.2.

Acompanhem mais que acertos: a taxa de remoção e a pressão de memória para pegar o subdimensionamento, a latência em cada camada para confirmar que o cache é de fato mais rápido, a taxa de requisições à origem para ver quanta carga o cache absorve e a obsolescência (quão antigas são as entradas servidas) para confirmar que vocês honram suas promessas de frescor. Alertem sobre as razões que predizem problemas, em especial uma taxa de acerto em queda ou uma taxa de remoção em alta, para saberem de um cache em degradação por um painel e não pelos usuários. Um cache observado é um ativo que vocês conseguem ajustar. Um não observado é uma dependência oculta esperando para surpreender.

Compromissos: prós e contras

O cache compra velocidade e escala com a moeda do frescor e da complexidade. Todo cache é uma aposta de que obsoleto-mas-rápido vence fresco-mas-lento para aquele dado em particular, e a arte é fazer essa aposta de forma consciente e não por padrão. A tabela abaixo resume as principais escolhas.

EscolhaPrósContras
Cache-asideSimples. Guarda só o que é lidoA primeira leitura sempre erra. Risco de breve obsolescência após escritas
Write-throughCache sempre atual na escritaEscritas mais lentas. Guarda dados que talvez nunca sejam lidos
Write-backEscritas muito rápidas. Absorve rajadasRisco de perda de dados se o cache falhar antes do descarregamento
Write-aroundEvita agitar o cache com dados de muita escritaErro garantido na primeira leitura
TTL curtoObsolescência limitada e pequenaMenor taxa de acerto. Mais carga na origem
TTL longo / chaves versionadasAlta taxa de acerto. Baixa carga na origemObsolescência a menos que invalidado. Exige chaves disciplinadas
CDN e bordaBaixa latência global. Absorve picosLonge dos dados. Mais difícil de depurar e invalidar
Remoção LRUAdapta-se à popularidade que mudaPode remover um conjunto quente estável sob carga de varredura
Remoção LFUProtege um conjunto quente estávelLenta para se adaptar. Agarra-se a entradas antes quentes

A tensão recorrente é consistência versus desempenho. Um cache com TTL longo e alta taxa de acerto é rápido e barato e pode servir dados obsoletos. Um cache com TTL curto e invalidação agressiva é fresco e correto e faz a origem trabalhar mais. Não há resposta universal certa, apenas uma resposta certa por dado, definida pela tolerância real dele à obsolescência. A segunda tensão é simplicidade versus alcance: um cache de aplicação fica perto dos seus dados e é fácil de raciocinar, enquanto a borda é distante, rápida e mais difícil de invalidar. Resolvam as duas classificando os dados por necessidade de frescor e volume de leitura e depois situando e configurando cada classe de propósito.

Perguntas para discutir com sua equipe

  1. Qual é a tolerância real de obsolescência de cada tipo de dado que guardamos em cache, e definimos os TTLs e a invalidação a partir dessa tolerância e não do hábito? A maioria das equipes faz cache com um TTL que alguém escolheu uma vez e nunca revisitou, então alguns dados são servidos mais obsoletos do que o negócio aceita enquanto outros expiram de modo tão agressivo que o cache quase não ajuda. Levem os dez principais recursos em cache e, para cada um, perguntem às pessoas donas daquele dado quão obsoleto ele pode ficar com segurança: segundos, minutos, horas ou nunca depois de publicado. Vocês normalmente verão que as respostas variam muito e que os TTLs atuais não as acompanham. O resultado que vocês querem é uma curta classificação de frescor, cada classe mapeada para uma abordagem (TTL curto, TTL longo com expurgo ou chaves imutáveis versionadas), para que as decisões de cache decorram da semântica dos dados e não do palpite.

  2. Se a nossa entrada de cache mais popular expirasse agora, sob tráfego de pico, o que aconteceria com a origem? Esta pergunta expõe se vocês têm proteção real contra estouro de manada ou apenas esperança. Muitos sistemas funcionam bem até uma chave quente expirar num pico de tráfego e toda requisição estourar o banco de dados de uma vez, transformando o cache de escudo em gatilho. Percorram o caminho concretamente para o seu endpoint mais movimentado: há coalescência de requisições para que só um erro chegue à origem, há jitter para que as entradas não expirem em sincronia, há uma política stale-while-revalidate para que os usuários nunca esperem uma reposição? Levem evidência de teste de carga, não intuição, porque uma defesa contra estouro que se sustenta com dez usuários ainda pode desmoronar com dez mil. Se vocês não conseguem responder com confiança, acabaram de achar o seu próximo investimento em resiliência.

  3. Temos certeza de que nenhum cache compartilhado jamais guarda os dados privados de um usuário sob uma chave que outro usuário possa acertar? Este é o erro de cache que vira incidente de segurança e manchete. Acontece quando uma resposta personalizada ou autenticada é guardada sob uma chave que omite a identidade do usuário, ou quando falta um cabeçalho Cache-Control destinado a manter privada uma resposta, de modo que um proxy compartilhado ou uma CDN a guarda e a serve à próxima pessoa. Auditem quais respostas são passíveis de cache nas camadas compartilhadas, confirmem que toda resposta por usuário está marcada private ou no-store e confirmem que toda chave de cache inclui toda entrada que muda a resposta. Tratem isso como revisão de segurança, porque o raio de impacto é todo usuário a jusante, e liguem às práticas do capítulo 4.2.

  4. Que padrão de escrita cada um dos nossos caches de fato usa, e alguém o escolheu de propósito? Cache-aside, write-through, write-back e write-around fazem promessas opostas sobre o frescor e sobre o que se perde quando o cache falha, e ainda assim na maioria das bases de código o padrão é o que o primeiro autor por acaso copiou. Para uma grande equipe isso importa porque um serviço presumindo o frescor do cache-aside enquanto outro roda write-back em silêncio pode produzir dados que parecem corrompidos mas só estão obsoletos, e o engenheiro de sobreaviso perde horas perseguindo um fantasma. Levem um inventário por cache: o padrão de escrita, o frescor que ele garante e o que acontece com as escritas não descarregadas se o processo morrer. Onde algum cache usa write-back, levem a história de durabilidade que o sustenta. Em sistemas corporativos e governamentais que tratam dados financeiros ou de registros, um cache write-back sem garantia de apoio é um achado de auditoria esperando para acontecer, então a discussão deve terminar com o padrão de cada cache nomeado, justificado e escrito.

  5. Quando implantamos ou mudamos dados, toda camada de cache relevante invalida corretamente, ou dependemos de alguém se lembrar de expurgar? A invalidação é a parte difícil, e o modo de falha é silencioso: um valor corrigido que continua errado por horas porque uma camada da hierarquia, uma CDN, um proxy reverso ou um cache de aplicação, nunca recebeu a mensagem. Uma grande organização multiplica esse risco, porque uma única mudança lógica pode precisar se propagar por muitos caches em muitas regiões pertencentes a equipes diferentes. Levem um rastro concreto de uma mudança recente de dados e sigam-na por toda camada de cache, perguntando em cada uma: o que disparou a invalidação aqui, e quanto tempo levou? Favoreçam designs que transformem a invalidação em nomeação (chaves versionadas) ou em eventos (uma mudança publica um expurgo) em vez de runbooks manuais. Em sistemas do setor público em que um número publicado errado, uma alíquota ou o valor de um benefício, pode ter peso legal, uma lacuna de invalidação não é um incômodo, mas uma exposição de conformidade, então o resultado deve ser um caminho de invalidação mapeado para toda classe de dado em cache.

  6. Estamos tratando o cache como infraestrutura compartilhada da plataforma, ou cada equipe está reinventando chaves, invalidação e proteção contra estouro por conta própria? O cache bem feito é um pequeno conjunto de problemas difíceis resolvidos uma vez: chaves normalizadas, invalidação orientada a eventos, coalescência de requisições, semântica HTTP correta e observabilidade por camada. Quando cada equipe improvisa isso, uma grande organização paga pelos mesmos erros repetidamente, e um bug de envenenamento ou um vazamento de dados privados corrigido num serviço persiste em silêncio em dez outros. Levem um mapa honesto de quem é dono das convenções de cache hoje e de quanto código de cache duplicado existe entre os serviços. A consideração concorrente é a autonomia: as equipes resistem a uma biblioteca compartilhada imposta, então pesem um padrão de caminho pavimentado fácil de adotar contra um padrão rígido que é imposto. Para um grupo de plataforma corporativo ou governamental, uma capacidade de cache compartilhada e bem testada também é a maneira mais barata de fazer as exigências de segurança e de auditoria valerem uniformemente, então a discussão deve decidir o que vira infraestrutura compartilhada e quem a financia.

Perspectiva por setor

Startup. O cache é o seu caminho mais barato para sobreviver a um pico de tráfego para o qual vocês ainda não podem bancar a escala, então gastem o pouco tempo que têm em algumas posições de alta alavancagem: uma CDN com URLs versionadas para ativos estáticos e uma única camada de cache-aside com TTLs curtos e jitter na frente da sua consulta mais quente. Apoiem-se em serviços gerenciados de CDN e de cache em vez de operar os seus e acrescentem cedo a coalescência de requisições, porque um estouro de manada no dia do lançamento contra um banco pequeno é a falha com maior probabilidade de acabar mal um bom dia. Pulem esquemas elaborados de invalidação até terem dados dizendo que importam.

Pequena empresa. Sem especialista em cache e com orçamento apertado, prefiram comprar o cache que vem de graça nas ferramentas que vocês já rodam: uma CDN incluída na hospedagem, cabeçalhos HTTP Cache-Control nas respostas do framework web e o cache de consultas embutido do banco de dados. A escolha entre construir e comprar quase sempre favorece comprar aqui, já que um cache com chave errada que vaza os dados de um cliente a outro custa muito mais que o serviço gerenciado que vocês evitaram. Acertem as duas vitórias baratas, cabeçalhos HTTP corretos e nunca guardar páginas autenticadas nas camadas compartilhadas, e deixem os padrões exóticos em paz.

Grande empresa. Em escala, entre muitas equipes, o risco passa de qualquer cache isolado para a inconsistência entre eles: esquemas divergentes de chaves, invalidação desigual e vazamentos de dados privados que aparecem num serviço e não em outro. Ofereçam o cache como infraestrutura compartilhada da plataforma, com padrões de caminho pavimentado para chaves, invalidação, proteção contra estouro e observabilidade por camada, para que a taxa de acerto, a remoção e a obsolescência fiquem visíveis num só lugar e governadas de modo uniforme. Tornem a configuração de cache revisável como código sensível à segurança e tratem a invalidação entre regiões como problema de design de primeira classe e não um runbook de cada equipe.

Governo. As restrições de contratação e de transparência moldam o que vocês podem guardar em cache e como provam que é seguro. Guardem em cache o conteúdo público de forma agressiva, orientações, formulários e tabelas de alíquotas atrás de uma CDN com TTLs longos, para que uma onda de prazo de declaração seja absorvida longe da origem, e documentem essa configuração para a auditoria. As páginas autenticadas que mostram os registros do próprio cidadão nunca devem tocar um cache compartilhado, e essa regra deve ser verificável e não apenas afirmada. Onde uma CDN ou serviço de cache é contratado de um fornecedor, exijam que o contrato exponha os controles de que vocês precisam (chaveamento, expurgo e registro) e evitem o aprisionamento que prenderia dados públicos atrás de formatos proprietários de cache.

Exemplos

Startup. Um pequeno aplicativo de consumo passa seu catálogo de produtos por uma camada cache-aside apoiada num armazenamento em memória, com TTL de 60 segundos e jitter para que as entradas não expirem juntas. Os ativos estáticos vão para uma CDN com nomes de arquivo versionados e max-age de um ano, de modo que uma implantação que muda uma folha de estilo serve uma nova URL e nunca precisa de expurgo. Quando um lançamento num podcast popular manda um pico de tráfego, a coalescência single-flight faz os milhares de erros simultâneos da página inicial causarem uma leitura do banco de dados e não milhares. Os fundadores gastam quase nada com cache e ainda assim tratam um pico que teria derretido seu pequeno banco, porque puseram de propósito alguns caches bem escolhidos.

Grande empresa. Uma varejista global atende milhões de compradores por um cache em camadas: uma CDN para imagens e respostas de API passíveis de cache, um cache de proxy reverso compartilhado em cada região e um cache de aplicação para fragmentos calculados de preço e de estoque. As chaves de cache são normalizadas e incluem moeda, idioma e classe de dispositivo, de modo que a personalização nunca vaza e as taxas de acerto permanecem altas. Os dados de produto usam TTLs curtos com invalidação orientada a eventos, de modo que uma mudança de preço publica num barramento de mensagens que expurga as chaves afetadas em todas as regiões em segundos. Toda camada reporta taxa de acerto, taxa de remoção e obsolescência à plataforma de observabilidade do capítulo 9.2, e um alerta sobre uma taxa de acerto em queda uma vez pegou um cache subdimensionado antes que virasse uma queda no checkout.

Governo. Uma autoridade tributária nacional opera um portal de declaração que fica quieto na maior parte do ano e sobrecarregado perto do prazo. A equipe guarda em cache de modo agressivo onde é seguro e nunca onde não é. O conteúdo público (páginas de orientação, formulários, tabelas de alíquotas) é servido de uma CDN com TTLs longos e URLs versionadas, absorvendo a onda de leitura do dia do prazo longe da origem. As páginas autenticadas que mostram a declaração do próprio cidadão são marcadas no-store e nunca tocam um cache compartilhado, de modo que nenhum contribuinte jamais recebe os dados de outro. A configuração de cache é revisada como código sensível à segurança contra as práticas do capítulo 4.2, e a proteção contra estouro é testada sob carga na escala do prazo com meses de antecedência, de modo que o portal que costumava ceder no dia mais movimentado do ano agora aguenta.

Justificativa de negócio: motivações, ROI e TCO

O retorno do cache é incomumente direto e fácil de quantificar. Um cache que eleva a taxa de acerto de 80 para 95 por cento corta o tráfego na origem em três quartos, o que pode significar adiar uma atualização de banco de dados, rodar menos servidores de aplicação ou sobreviver a um pico de tráfego que de outro modo teria exigido um escalonamento emergencial. As melhorias de latência se convertem em receita no comércio e em satisfação e taxas de conclusão nos serviços públicos, onde a pesquisa há muito liga páginas mais rápidas a maior conversão e menor abandono. Os custos de saída e de computação caem porque uma requisição servida da borda nunca paga pela largura de banda nem pelo processamento da origem. Para sistemas de muita leitura, que são a maioria, o cache costuma ser o desempenho mais barato que vocês podem comprar.

Pesem o custo total de propriedade com honestidade. Os custos diretos são modestos: a infraestrutura de CDN e de cache é barata em relação à capacidade de origem que poupa. O custo real é a disciplina de engenharia, porque um cache errado é pior que nenhum cache. Os bugs de obsolescência, os erros de invalidação e as vulnerabilidades de envenenamento de cache carregam custo real, e crescem quando o cache é improvisado por equipe em vez de fornecido como capacidade compartilhada e bem testada. O argumento de negócio mais forte financia uma pequena quantidade de infraestrutura e de convenção de cache compartilhadas (chaves padrão, invalidação, proteção contra estouro e observabilidade) para que toda equipe obtenha o benefício sem repetir os erros. Enquadrado para a liderança, o cache se conecta a métricas que ela já acompanha: custo de infraestrutura, latência de página, taxas de conversão e de conclusão e frequência de incidentes durante eventos de pico.

Antipadrões e armadilhas

  • Cache sem invalidação: definir um TTL longo sem modo de expurgar, de modo que um valor corrigido continua errado por horas.
  • Chaves grossas demais: guardar respostas personalizadas sob uma chave compartilhada, vazando os dados de um usuário a outro.
  • Chaves finas demais: incluir entradas voláteis na chave de modo que duas requisições nunca coincidam e a taxa de acerto desabe.
  • Sem proteção contra estouro de manada: uma chave quente expira sob carga e toda requisição corre para a origem de uma vez.
  • Expiração sincronizada: um lote de entradas gravadas juntas expira no mesmo instante, sem jitter, causando uma manada estourando periodicamente.
  • Guardar dados privados em camadas compartilhadas: faltar Cache-Control: private ou no-store, de modo que um proxy ou CDN guarda respostas autenticadas.
  • Ignorar entradas fora da chave: refletir um cabeçalho no corpo da resposta mas omiti-lo da chave, abrindo a porta ao envenenamento de cache.
  • Cache subdimensionado: um cache pequeno demais para guardar o conjunto de trabalho se debate e remove entradas logo antes de serem necessárias.
  • Cache sem medição: sem métrica de taxa de acerto nem de remoção, de modo que um cache em degradação fica invisível até virar uma queda.
  • Write-back sem durabilidade: escritas rápidas que somem quando o cache morre antes do descarregamento, sem garantia de apoio.

Modelo de maturidade

  • Nível 1, Iniciar: O cache é ad hoc e por desenvolvedor, acrescentado de forma reativa quando algo parece lento. Os TTLs são chutados, as chaves são inconsistentes, a invalidação é manual ou ausente, e dados obsoletos e bugs misteriosos são comuns. Ninguém acompanha a taxa de acerto, e um pico de tráfego que um cache deveria ter absorvido causa uma queda.
  • Nível 2, Desenvolver: As equipes fazem cache nos lugares óbvios e usam uma CDN para ativos estáticos. Aparecem TTLs básicos e cache-aside, mas as convenções variam de serviço para serviço, a invalidação é inconsistente, falta proteção contra estouro, as regras de cache privado versus compartilhado são informais e a observabilidade se limita a verificações pontuais ocasionais.
  • Nível 3, Padronizar: Uma estratégia de cache é documentada e imposta em toda a organização. As chaves de cache são normalizadas, os TTLs seguem uma classificação de frescor compartilhada, a invalidação é orientada a eventos onde importa, a proteção contra estouro e a semântica HTTP correta são padrão, dados privados nunca são guardados em camadas compartilhadas e toda camada reporta taxa de acerto e de remoção a um pipeline comum de observabilidade.
  • Nível 4, Gerenciar: O cache é medido e controlado em relação a linhas de base. Cada camada tem taxas de acerto-alvo, orçamentos de obsolescência e limiares de remoção, e os painéis alertam quando uma taxa de acerto cai ou uma taxa de remoção sobe além da linha de base. As defesas contra estouro são testadas sob carga na escala de pico, a redução de carga na origem é quantificada por cache, os TTLs e as políticas de remoção são ajustados a partir de padrões de acesso medidos e a configuração de cache é revisada como código sensível à segurança antes do lançamento.
  • Nível 5, Orquestrar: O cache é continuamente melhorado e integrado em toda a organização. A posição, as chaves e os TTLs se adaptam ao tráfego que muda, a computação de borda é usada onde compensa, o planejamento de capacidade e os modelos de custo se alimentam de métricas de cache e as lições dos incidentes de uma equipe alimentam as convenções compartilhadas. A organização trata o cache como uma capacidade projetada, medida e adaptativa e não uma coleção de remendos locais.

Ideias para discussão

  1. Qual cache único do seu sistema, se esfriasse agora, mais poria em perigo a sua origem, e o que o protege?
  2. Para cada camada da sua hierarquia de cache, vocês conseguem dizer a taxa de acerto atual de memória e, se não, o que isso diz?
  3. Onde vocês transformaram um problema de invalidação num problema de nomeação com chaves versionadas, e onde ainda poderiam fazê-lo?
  4. Qual dos seus caminhos de escrita usa cache-aside, write-through, write-back ou write-around, e cada um foi escolhido de propósito?
  5. Se um atacante controlasse um cabeçalho de requisição, ele poderia envenenar alguma resposta em cache que os seus usuários compartilham?
  6. Como vocês saberiam, em minutos, que a taxa de acerto caiu em silêncio vinte pontos?

Principais conclusões

  • O cache corta a latência, a carga e o custo, e a hierarquia de cache (cliente, CDN e borda, proxy reverso, aplicação, banco de dados) permite responder o mais acima e o mais fora que vocês puderem com segurança.
  • A invalidação é a parte difícil. Projetem as chaves de cache e os TTLs deliberadamente e transformem problemas de invalidação em problemas de nomeação com chaves versionadas sempre que puderem.
  • Protejam o cache sob carga com coalescência de requisições, jitter e stale-while-revalidate, porque um cache é mais necessário exatamente quando um estouro de manada poderia quebrá-lo.
  • Escolham os padrões de escrita e as políticas de remoção por carga de trabalho e usem a semântica de cache do HTTP (Cache-Control, ETags, validação) explicitamente em vez de deixar os caches adivinharem.
  • Tratem o conteúdo em cache como superfície de ataque e meçam a taxa de acerto, a remoção e a obsolescência, porque um cache não observado ou com chave errada é um passivo oculto, não um ativo.

Referências e leitura complementar

  • Martin Kleppmann, Designing Data-Intensive Applications
  • Andrew S. Tanenbaum and Herbert Bos, Modern Operating Systems
  • John L. Hennessy and David A. Patterson, Computer Architecture: A Quantitative Approach
  • Roy T. Fielding and Julian Reschke, “Hypertext Transfer Protocol (HTTP/1.1): Caching,” RFC 7234, IETF
  • Mark Nottingham, “Caching Tutorial for Web Authors and Webmasters”
  • James Kettle, “Practical Web Cache Poisoning,” PortSwigger Research
  • Betsy Beyer, Chris Jones, Jennifer Petoff, and Niall Richard Murphy (eds.), Site Reliability Engineering: How Google Runs Production Systems
  • Michael T. Nygard, Release It! Design and Deploy Production-Ready Software