Quem mexeu no meu cache?

mauricioabreu

Maurício Antunes

Posted on October 16, 2022

Quem mexeu no meu cache?

O objetivo desse texto é mostrar como um erro de configuração no cache da sua aplicação pode trazer problemas sérios que vão desde uma dor de cabeça até a perda de dinheiro.

O que é caching?

Caching é uma estratégia utilizada para obter dados de forma mais rápida em uma aplicação. Nossa memória recente é uma espécie de cache, pois não precisamos fazer um grande esforço mental pra lembrar.

Se você está pensando em usar alguma forma de cache na sua aplicação, provavelmente você está com alguma parte da sua aplicação executando devagar, por exemplo. Cache pode ajudar sua aplicação de diversas maneiras, até como mecanismo de resiliência.

Errando na configuração do cache

É possível degradar de forma severa uma aplicação ao configurar o cache de maneira errada. Alguns tipos de aplicação são extremamente dependentes de cache, como serviços de storage e multimídia, como transmissão de áudio e vídeo pela internet.

Para o exemplo deste artigo, vamos usar exemplos usando Ruby e um servidor web famoso, o NGINX.

Vamos falar de dois tipos de ataque que podem acontecer nos seus servidores.

Minha aplicação caiu porque o cache não funcionou!

Cache que não funciona é muito comum, principalmente quando sites usam CDNs externas e as pessoas envolvidas não têm conhecimento de como um erro de configuração pode derrubar a aplicação.

Vamos supor que você seja dev de um site que contenha várias imagens de produtos, com boa resolução e você adicione caching para que o seu serviço de imagens não seja acionado toda requisição. Essa é uma ótima ideia e há expectativas de que ela vai melhorar a performance da aplicação. Porém, a configuração que você fez coloca os parâmetros da query string como parte da chave de cache.

Chave de cache (cache key) é um hash formado a partir de variáveis especificadas pela configuração das diretivas do servidor. Essa chave é uma maneira de mapear um valor para um hash md5 que vai ser alocado em um grande mapa, de rápido acesso, no seu servidor

O que pode dar errado? Essa imagem abaixo exemplifica requests que furam o cache ao usar parâmetros quaisquer na query string.

dois fluxos de request mostrando o erro ao usar a query string como chave de cache

Podemos representar isso com código Ruby, criando funções que definem as chaves de cache, similares às configurações que podemos fazer no NGINX:

require 'digest'

# Esse é o nosso cache
cache = {}

# Uma lista de requests a serem feitos
reqs = [
  '/airfy.jpg',
  '/airfy.jpg',
  '/airfy.jpg',
]

# Nossa função que define a cache key de acordo com 
# o valor passado
def good_cache_key_hash(value)
    # Divide a string por "?" e remove o "/"
    key = value.split('?')[0].slice(1, value.size)
    Digest::MD5.hexdigest key
end

# Valida que todos os paths vão usar a mesma chave de cache
puts(reqs.map { |req| good_cache_key_hash(req) })

# Agora vamos ter uma nova lista de requests maliciosos (ou não)
reqs = [
  '/airfy.jpg?foo=1',
  '/airfy.jpg?foo=2',
  '/airfy.jpg?foo=3',
]

# Nossa função que define a cache key de acordo com 
# o valor passado mas dessa vez ela não foi pensada corretamente
def bad_cache_key_hash(value)
    key = value
    Digest::MD5.hexdigest key
end

# Valida que todos os paths vão usar chaves diferentes, 
# mesmo que as imagens sejam exatamente as mesmas
puts(reqs.map { |req| bad_cache_key_hash(req) })
Enter fullscreen mode Exit fullscreen mode

O resultado fica como a seguir:

43eb6cb76f885b82fbfef92cbee4acd1
43eb6cb76f885b82fbfef92cbee4acd1
43eb6cb76f885b82fbfef92cbee4acd1
9a03b0f4730f70964c01f97355823f20
b0affdcb74f872918bb795937f98feaa
0082e52c9b5e1613888e1a26bbc96f37
Enter fullscreen mode Exit fullscreen mode

Observe que os 3 primeiros valores são iguais, provando que a chave será a mesma. Entretanto, os próximos três requests estão com os valores diferentes, mesmo que a imagem seja a mesma.

O que podemos concluir? Que nosso cache vai ficar furado por uma má definição do hash computado que é usado para mapear requests aos nossos valores.

Às vezes vejo uma air fry, às vezes um liquidificador

Você já entrou em um site que a imagem ficava trocando, sem ter um motivo muito óbvio? Provavelmente muitos objetos estão apontando pra mesma cache key.

cache key apontando sempre para o mesmo objeto

Esse caso acima está relacionado a um erro de configuração que vai fazer todas as URLs de imagem do seu site apontarem pro mesmo arquivo. Como?

Lembra que a cache key é o hash md5 que aponta qual lugar do mapa de objetos vai ser consultado? Então, nesse caso a cache key apontou qualquer URL para o mesmo lugar. A primeira requisição trouxe airfry.jpg e fez cache. As requisições subsequentes vão sempre trazer a imagem da airfry, até que o tempo de cache termine e faça a imagem expirar.

O exemplo abaixo um Ruby mostra que uma chave de cache mal definida pode fazer todos os objetos apontarem pro mesmo hash:

# Esse é o nosso cache
cache = {}

# Agora vamos supor que nós deixamos passar que nossa cache key
# foi definida usando a extensão da imagem. Isso é totalmente
# possível quando você usa variáveis erradas ou define
# uma função de cache e não testa ela corretamente
def wrong_cache_key(value)
  # pega a extensão do caminho passado
  key = value.split(".").last
  Digest::MD5.hexdigest key
end

# Uma lista de requests a serem feitos
reqs = [
  '/airfy.jpg',
  '/liquidificador.jpg',
  '/airfy.jpg',
]

# Valida que todos as chaves são a mesma
puts(reqs.map { |req| wrong_cache_key(req) })
Enter fullscreen mode Exit fullscreen mode

O resultado fica como a seguir:

c36bbd258b7ee694eb987221b2b197b0
c36bbd258b7ee694eb987221b2b197b0
c36bbd258b7ee694eb987221b2b197b0
Enter fullscreen mode Exit fullscreen mode

Testando as requisições em um servidor web

Foi criado o projeto https://github.com/mauricioabreu/my-cache-haz-a-problem que contém o código do web server e os exemplos didáticos criados para o texto.

Cache key com query string

Aqui demonstramos o caso em que usar a query string como cache pode degradar sua aplicação:

curl -v "http://localhost:8080/items/airfry.jpg?foo=1"

< HTTP/1.1 200 OK
< Content-Length: 101321
< X-Cache-Status: MISS
Enter fullscreen mode Exit fullscreen mode

Uma boa estratégia de cache ignoraria esses parâmetros na chave de cache. Um segundo request com os parâmetros foo=2 deveria retornar HIT

HIT - objeto encontrado no cache
MISS - objeto não encontrado no cache. Uma requisição ao upstream será realizada, o objeto será baixado e cacheado.

curl -v "http://localhost:8080/items/airfry.jpg?foo=2"

< HTTP/1.1 200 OK
< Content-Length: 101321
< X-Cache-Status: MISS
Enter fullscreen mode Exit fullscreen mode

Imagine essa informação nas mãos erradas?

Não é um erro formar a cache key com parâmetros da query string. Um exemplo bom é se o seu site renderiza imagens com tamanhos predefinidos, como /products/airfry.jpg?width=300&height=250. Esses parâmetros podem ser usados e vão aumentar a performance do seu cache.

Cache key errada

Usar a cache key errada pode deixar as pessoas usuárias do seu site infelizes ao receber sempre a mesma imagem:

curl -v http://localhost:8080/products/airfry.jpg

< HTTP/1.1 200 OK
< Content-Length: 101321
< X-Cache-Status: MISS
Enter fullscreen mode Exit fullscreen mode

O primeiro request está OK. O estado do cache é MISS, ainda não existia no cache e foi resgatado do nosso storage.

Vamos requisitar a imagem de um liquidificador.

curl -v http://localhost:8080/products/liquidificador.jpg

< HTTP/1.1 200 OK
< Content-Length: 101321
< X-Cache-Status: HIT
Enter fullscreen mode Exit fullscreen mode

Aqui observamos duas coisas estranhas: o tamanho e o X-Cache-Status. Muita coincidência o tamanho da imagem liquidificador ter o exato mesmo tamanho da imagem da air fry, não é? Além disso, como uma imagem que nunca requisitamos trouxe um estado de HIT? Isso aconteceu porque a chave de cache foi baseada, erroneamente, na extensão da requisição, jpg

Solucionando os problemas

Uma das maneiras mais simples de resolver os problemas apresentados é usar a cache key padrão do NGINX, que é $scheme$proxy_host$request_uri. Em detalhe:

  • Scheme - HTTP/HTTPS
  • Proxy host - host e porta do endereço do storage (ou do upstream usado)
  • Request URI - URI original com os parâmetros da query string

Algumas dicas:

  • Use a cache key default quando puder;
  • Verifique suas cache keys antes de colocar seu site em produção;
  • Colete métricas de uso do cache. Quanto mais requests com HIT, melhor vai ser seu cache hit ratio.

Evil icons created by Smashicons - Flaticon

💖 💪 🙅 🚩
mauricioabreu
Maurício Antunes

Posted on October 16, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related