AWS Certified CloudOps Engineer - Associate
Cache com CloudFront e ElastiCache
Onde colocar um cache para que ele tire carga de verdade: as regras de cache key e TTL que decidem o que o CloudFront guarda na borda, e as estratégias de lazy loading, write-through e TTL que decidem o que o ElastiCache guarda na frente do seu banco.
- Decidir se uma carga de trabalho precisa de cache na borda, cache de banco de dados ou os dois
- Prever por quanto tempo o CloudFront guarda um objeto dados os TTLs da cache policy e os headers Cache-Control da origem
- Elevar a taxa de cache hit do CloudFront encolhendo a cache key, e escolher entre invalidation e nomes de arquivo versionados
- Comparar lazy loading, write-through e TTL como estratégias de população do ElastiCache e nomear a falha que cada uma carrega
- Ler as métricas do ElastiCache no CloudWatch para escolher entre escalar verticalmente, adicionar réplicas e adicionar shards
Sua página de catálogo roda uma consulta que leva 300 ms e devolve as mesmas 200 linhas para todo visitante. A 400 requisições por segundo, o banco roda essa consulta 400 vezes por segundo e queima CPU produzindo uma resposta que ele já produziu. A consulta não tem defeito nenhum. O problema é que você está recalculando uma constante, e o conserto é guardar a resposta em algum lugar mais perto do que a coisa que a calculou.
A AWS te dá dois lugares bem diferentes para guardar, e a Skill 2.1.2 do SOA-C03 cita os dois: Amazon CloudFront na borda e Amazon ElastiCache ao lado da sua aplicação. Eles não são intercambiáveis, e escolher o errado significa adicionar um serviço que não tira carga nenhuma.
Dois caches, duas distâncias
| CloudFront | ElastiCache | |
|---|---|---|
| Onde fica | Edge locations da AWS, perto do visitante | Dentro da sua VPC, perto da sua aplicação |
| O que guarda | Respostas HTTP inteiras, indexadas por uma cache key | O que seu código colocar lá, indexado por uma string |
| Quem escreve nele | O CloudFront, automaticamente, num cache miss | Seu código de aplicação, explicitamente |
| O que ele protege | A origem e o caminho de rede até ela | O banco de dados |
| Um miss custa | Uma requisição à origem | Uma consulta ao banco mais uma escrita no cache |
| Você ajusta com | Cache policies, TTLs, a cache key | Estratégia, TTL, node type, contagem de shards |
A linha que decide qual dos dois você pega: o CloudFront é um cache que você configura, o ElastiCache é um cache que você programa. Se os mesmos bytes saem para muitos visitantes por HTTP, o CloudFront absorve esse tráfego sem mudança de código. Se a coisa cara é um resultado de consulta, um objeto de sessão ou um ranking calculado que só a sua aplicação entende, nenhum cache de borda ajuda, porque o CloudFront não consegue olhar dentro da resposta para saber que ela é reutilizável.
Sistemas grandes rodam os dois. O CloudFront pega as respostas públicas repetidas, o ElastiCache pega as leituras internas repetidas, e o banco só vê o que varia de verdade.
A cache key do CloudFront decide o que é um hit
O CloudFront guarda cada objeto sob uma cache key. Uma requisição de visitante é um hit só se ela produzir a mesma cache key de uma requisição anterior e aquele objeto ainda estiver válido naquela edge location. Todo o ajuste de cache do CloudFront volta a uma regra: menos valores na cache key significa mais hits.
Você controla a key com uma cache policy anexada a um cache behavior. A policy diz quais headers, cookies e query strings entram na key, e carrega os ajustes de TTL.
As managed policies valem a memorização porque marcam as duas pontas da faixa:
| Managed cache policy | Min TTL | Max TTL | Default TTL | Conteúdo da cache key |
|---|---|---|---|---|
CachingOptimized | 1 s | 31.536.000 s (365 dias) | 86.400 s (24 h) | Nada além do header Accept-Encoding normalizado |
CachingDisabled | 0 s | 0 s | 0 s | Nada |
A CachingOptimized é a resposta padrão para assets estáticos. A CachingDisabled é a resposta certa para qualquer coisa genuinamente por usuário, e ela funciona porque os três TTLs são 0, não porque ela não encaminha nada.
Três erros de cache key custam taxa de hit de verdade, e cada um tem seu conserto:
- Caixa e ordem nos query strings.
?parameter1=Ae?parameter1=asão duas keys diferentes, e?parameter1=a¶meter2=be?parameter2=b¶meter1=atambém. O mesmo objeto, até quatro entradas de cache. Padronize uma caixa e uma ordem na aplicação que monta as URLs. - Encaminhar todos os cookies. Para cada cookie encaminhado, o CloudFront guarda uma cópia separada por combinação de nome e valor. Dois cookies com três valores possíveis cada dão até 9 cópias do mesmo arquivo
.css. Separe conteúdo estático e dinâmico em cache behaviors diferentes e encaminhe cookies só no dinâmico. - Cachear por
User-Agent. Ele tem uma quantidade enorme de valores distintos, então na prática desliga o cache parecendo uma configuração que funciona. Se você precisa de resposta por dispositivo, use os headers de dispositivo do CloudFront:CloudFront-Is-Desktop-Viewer,CloudFront-Is-Mobile-Viewer,CloudFront-Is-SmartTV-ViewereCloudFront-Is-Tablet-Viewer. Quatro valores em vez de milhares, e você encaminha só os que realmente mudam a resposta.
Uma alavanca fica fora da key. Se não há compressão em jogo, anexe um custom origin header chamado Accept-Encoding com valor em branco. O CloudFront então tira esse header da cache key em vez de partir cada objeto em variantes Gzip, Brotli e sem compressão.
Quanto tempo o objeto fica: a origem propõe, a policy decide
Esta é a parte que os operadores erram, e vale percorrer devagar.
Três ajustes vivem na cache policy: Minimum TTL, Maximum TTL e Default TTL. A origem também pode mandar Cache-Control: max-age, Cache-Control: s-maxage ou um header Expires. Nenhum dos lados vence sozinho. A origem propõe uma duração e a policy a encaixa dentro da faixa.
Percorra com uma policy de Minimum TTL 60, Maximum TTL 86400, Default TTL 3600:
| Resposta da origem | O CloudFront guarda por | Por quê |
|---|---|---|
Cache-Control: max-age=10 | 60 s | Abaixo do mínimo, então sobe para o Minimum TTL |
Cache-Control: max-age=7200 | 7200 s | Dentro da faixa, então é respeitado exatamente |
Cache-Control: max-age=31536000 | 86400 s | Acima do máximo, então desce para o Maximum TTL |
Sem header Cache-Control | 3600 s | Nada foi proposto, então vale o Default TTL |
Três detalhes nas bordas dessa tabela:
s-maxageganha domax-agena borda. Quando a origem manda os dois, o CloudFront encaixa os-maxagee o navegador usa omax-age. É assim que você guarda um objeto por uma hora na borda e por um minuto no navegador.Expiresé a opção mais fraca. Se a origem mandaCache-Control: max-ageeExpires, o CloudFront usa só omax-age. A AWS recomenda omax-age.- O visitante não consegue forçar um refresh. O CloudFront ignora
Cache-ControlePragmanas requisições de visitante, então um reload forçado no navegador não limpa a borda.
Agora o erro de entendimento que gera bug em produção. É tentador supor que Cache-Control: no-store vindo da origem sempre impede o CloudFront de guardar. Não impede, quando o Minimum TTL da cache policy é maior que 0. Nesse caso o CloudFront guarda o objeto pelo TTL mínimo mesmo com a origem tendo dito que não, e é assim que uma página de conta personalizada é servida para o próximo visitante que cair na mesma edge location. Tanto a CachingOptimized (Minimum TTL de 1 segundo) quanto a Amplify (Minimum TTL de 2 segundos) carregam esse aviso na documentação da AWS. Se o conteúdo nunca pode ser guardado, a resposta é uma policy com TTL mínimo 0.
Servir conteúdo velho de propósito
Duas diretivas deixam você trocar um pouco de frescor por latência e por sobreviver a uma queda da origem:
Cache-Control: max-age=3600, stale-while-revalidate=600, stale-if-error=86400
- Na primeira hora, o CloudFront serve do cache normalmente.
- Depois disso,
stale-while-revalidate=600deixa o CloudFront entregar a cópia velha na hora enquanto busca uma nova em segundo plano, por até 10 minutos. stale-if-error=86400deixa o CloudFront continuar servindo a cópia velha por até 24 horas se a origem estiver inacessível ou devolver um 5xx.
As duas são limitadas pelo Maximum TTL da cache policy, o que for menor. Passado o TTL máximo, o objeto some da borda independentemente do que as diretivas pediram.
Invalidation ou nomes de arquivo versionados
Quando você precisa tirar conteúdo do cache antes da expiração, existem duas opções, e a AWS recomenda aquela que as pessoas pegam em segundo lugar.
Invalidation remove arquivos dos caches de borda agora. Os primeiros 1.000 caminhos de invalidation por mês são gratuitos por conta AWS em todas as distributions, e você paga por caminho depois disso. Um caminho com o wildcard * conta como um caminho, não importa quantos arquivos ele remova, então /* é uma unidade cobrada só. O wildcard precisa ser o último caractere; um asterisco em qualquer outra posição é tratado literalmente.
Duas armadilhas de invalidation que estragam um deploy:
- Query strings fazem parte do caminho. Se a cache key inclui query strings,
/images/logo.jpgnão invalida/images/logo.jpg?v=2. Use/images/logo.jpg*. - Caminhos de diretório precisam das duas formas. Se suas URLs não são consistentes com a barra final, invalide
/imagese/images/.
Nomes de arquivo versionados significam publicar app.a3f91c.js em vez de sobrescrever app.js. A AWS recomenda essa como a abordagem principal para conteúdo que muda com frequência, e o motivo não é só custo. Uma invalidation limpa o CloudFront e não faz nada com a cópia que está no navegador do visitante ou num proxy corporativo; um nome novo muda a URL, então todo cache da cadeia dá miss. Ela também torna o rollback trivial e deixa os access logs legíveis, porque a linha de log nomeia a versão que foi servida.
Origin Shield é a outra alavanca estrutural. Ele coloca mais uma camada de cache na frente da origem para que todas as camadas do CloudFront, edge locations e regional edge caches, passem por uma única localização. A origem então serve uma requisição por objeto em vez de uma por regional cache.
ElastiCache: o cache que você precisa programar
O CloudFront se popula sozinho. O ElastiCache não: seu código decide o que entra, quando e por quanto tempo. A AWS documenta três estratégias, e a prova testa o modo de falha de cada uma, não a definição.
Lazy loading escreve no cache só depois de um miss.
get_customer(customer_id)
customer_record = cache.get(customer_id)
if (customer_record == null)
customer_record = db.query("SELECT * FROM Customers WHERE id = {0}", customer_id)
cache.set(customer_id, customer_record)
return customer_record
Só dado que foi pedido entra no cache, e uma falha de node é sobrevivível: um node vazio novo ainda devolve respostas corretas, só mais devagar, enquanto os misses o reenchem. Os custos são uma penalidade de três viagens no miss (ler o cache, consultar o banco, escrever no cache) e dado desatualizado, porque nada atualiza o cache quando o banco muda por baixo.
Write-through escreve no cache a cada escrita no banco.
save_customer(customer_id, values)
customer_record = db.query("UPDATE Customers WHERE id = {0}", customer_id, values)
cache.set(customer_id, customer_record)
return success
O dado guardado nunca fica velho, e a latência extra cai nas escritas, onde os usuários toleram melhor. Os custos são a imagem espelhada do lazy loading: dado faltando em qualquer node novo, porque um node fresco não guarda nada até as linhas correspondentes serem escritas de novo, e churn de cache, porque você está guardando escritas que talvez nunca sejam lidas.
Adicionar um TTL a cada escrita é o que faz as duas trabalharem juntas. Um TTL limita quão velha uma entrada carregada por lazy loading pode ficar e expulsa entradas write-through que ninguém lê. O padrão de produção usa os três: write-through pela correção, lazy loading pela resiliência, TTL para limitar as duas.
cache.set(customer_id, customer_record, 300) # expira em 5 minutos
TTL nunca garante frescor. Ele garante um teto de quão velho o dado pode estar, o que é uma promessa diferente e bem mais viável. Diga isso na revisão de arquitetura em vez de deixar "a gente guarda por 5 minutos" ser ouvido como "o cache está correto".
Escolher o engine, e por que isso muda suas opções de escala
O ElastiCache oferece Memcached, Valkey e Redis OSS. O engine que você escolhe determina quais ações de escala sequer existem, então isso não é questão de preferência.
| Memcached | Valkey / Redis OSS (cluster mode disabled) | Valkey / Redis OSS (cluster mode enabled) | |
|---|---|---|---|
| Tipos de dado | Strings e objetos simples | Complexos (listas, hashes, sets, sorted sets) | Complexos |
| Multi-threaded | Sim | Não | Não |
| Replicação (read replicas) | Não | Sim | Sim |
| Automatic failover | Não | Opcional | Obrigatório |
| Particionamento de dados entre shards | Sim, no cliente | Não | Sim |
| Online resharding | Não | Não | Sim |
| Backup e restore | Clusters baseados em nodes: não | Sim | Sim |
Escolha Memcached quando quiser o modelo mais simples possível, nodes grandes multi-core e cache de objetos puro. Escolha Valkey ou Redis OSS quando precisar de replicação, failover, persistência, sorted sets ou pub/sub. A consequência prática: um cluster Memcached não pode ser consertado adicionando réplicas, porque ele não tem réplicas. As únicas alavancas dele são um node type maior ou mais nodes.
Ler as métricas e escolher a ação de escala
Questões de escala do ElastiCache normalmente são de diagnóstico. Uma métrica diz qual recurso está curto, e o recurso mais o engine dizem a ação.
| Métrica | O que significa | Para onde ela aponta |
|---|---|---|
CPUUtilization | Percentual de CPU no nível do host | Em nodes pequenos, o teto da carga |
EngineCPUUtilization | Uso do único core do engine | O sinal real em nodes com 4 vCPUs ou mais |
Evictions | Chaves removidas para abrir espaço | Memória insuficiente para o working set |
SwapUsage e FreeableMemory | Pressão de memória | FreeableMemory abaixo de 100 MB, ou SwapUsage acima do FreeableMemory, significa node em apuros |
ReplicationLag | Quanto uma réplica está atrás do primary | Leituras na réplica estão devolvendo dado antigo |
TrafficManagementActive | Valor 1 significa que o ElastiCache está fazendo throttle dos comandos | O node está subdimensionado para a carga |
O limiar de CPU pega as pessoas, então trabalhe ele. Valkey e Redis OSS rodam o engine numa única thread, então um node pode estar completamente saturado com a CPU do host bem abaixo de 100 por cento. A AWS dá a conta: coloque o limiar em 90 dividido pelo número de cores. Num node de 2 cores isso dá 45 por cento. Um cluster parado em 46 por cento de CPU com latência subindo não está saudável e ocioso, está no talo. Em node types com 4 vCPUs ou mais, use EngineCPUUtilization e a divisão some. O Memcached é multi-threaded, então o limiar dele fica mesmo perto de 90 por cento.
Sabendo o recurso, a ação vem do engine e da carga:
- Leitura pesada e acima do limiar, Valkey ou Redis OSS: adicione read replicas.
- Escrita pesada, cluster mode disabled: escale para um node type maior. Só existe um primary, então mais escritas pedem um primary maior.
- Escrita pesada, cluster mode enabled: adicione shards, o que espalha as escritas por mais nodes primary.
- Qualquer pressão, Memcached: node type maior, ou mais nodes.
Como a escala acontece de fato
Para cluster mode enabled, o online resharding muda a contagem de shards com o cluster servindo requisições. Adicionar shards eleva capacidade de leitura e escrita, remover reduz custo, e o rebalance nivela o keyspace entre os shards existentes. Três limites para conhecer: shards novos recebem a mesma contagem de nodes do menor shard existente, você não define keyspaces por shard online (isso exige o caminho offline de backup e restore), e chaves com itens maiores que 256 MB depois da serialização não são migradas, o que pode deixar shards desbalanceados. Antes de remover shards, o ElastiCache confere se os shards restantes comportam o dado e cancela a operação em vez de perder chaves.
Escala vertical por node type também é operação online para Valkey e Redis OSS. O resharding offline, o caminho de backup e restore, é o que fica indisponível, e você aceita essa parada só quando precisa do que só ele permite: mudar node type, versão do engine, contagem de réplicas por shard e keyspaces num movimento só.
O ElastiCache Serverless tira a decisão de shard da sua mão. Ele acompanha CPU, memória e rede continuamente e adiciona shards sozinho, e você observa duas métricas em vez de uma lista de nodes: BytesUsedForCache para armazenamento e ElastiCacheProcessingUnits (ECPUs) para computação. Você pode limitar as duas para conter custo, mas entenda o que um limite faz na borda: bater no máximo de armazenamento faz o ElastiCache expulsar por LRU as chaves que têm TTL e depois devolver erros de falta de memória, e bater no máximo de ECPU faz ele fazer throttle das requisições. A AWS recomenda um alarme do CloudWatch em 75 por cento de qualquer máximo que você definir, para você descobrir antes dos seus usuários.
Dicas para a prova
- "Mesma resposta para muitos visitantes por HTTP" é CloudFront. "Resultado de consulta caro reutilizado pela aplicação" é ElastiCache. Um enunciado que descreve banco sob pressão de leitura não está perguntando da borda.
- Dados os números de TTL, faça o encaixe:
max-ageabaixo do Minimum TTL sobe para o mínimo, acima do Maximum TTL desce para o máximo, e o Default TTL só vale quando a origem não mandamax-age. As alternativas erradas pegam o número citado por último. - "A origem manda no-store mas conteúdo velho continua sendo servido" aponta para uma cache policy com Minimum TTL maior que 0, não para um defeito do CloudFront.
- 1.000 caminhos de invalidation gratuitos por mês por conta, e um caminho com wildcard conta como um. Se o enunciado insiste em atualizações frequentes, a resposta pretendida costuma ser nomes de arquivo versionados, não mais invalidations.
- Qualquer alternativa que aumente a taxa de hit adicionando headers, cookies ou query strings à cache key está errada. Hits vêm de uma key menor.
- Lazy loading permite dado velho e sobrevive a node vazio. Write-through é sempre fresco e falha em node vazio. TTL é o que torna viável rodar os dois. Case o sintoma com a estratégia, não a definição.
- Valkey e Redis OSS são single-threaded: o limiar de alarme de CPU é 90 dividido pela contagem de cores, e o
EngineCPUUtilizationé o sinal mais limpo em nodes maiores. - Memcached não tem réplicas, não tem failover e não tem cluster mode. Qualquer alternativa que ofereça isso para um cluster Memcached já está errada só pela fronteira de engine.
- Leitura pesada pede réplicas, escrita pesada com cluster mode disabled pede um node maior, escrita pesada com cluster mode enabled pede mais shards.
A regra para levar desta lição: nomeie a coisa que está se repetindo antes de nomear o serviço. Respostas HTTP repetidas pertencem à borda, resultados de consulta repetidos pertencem à memória ao lado da aplicação, e o que varia de verdade a cada requisição não pertence a lugar nenhum além do banco. Essa última categoria é a que cache nenhum resgata, e é onde a próxima lição começa, escalando o próprio banco relacional.
