{"id":158,"date":"2009-09-05T13:56:46","date_gmt":"2009-09-05T16:56:46","guid":{"rendered":"http:\/\/blog.plataformatec.com.br\/?p=158"},"modified":"2009-12-30T13:28:46","modified_gmt":"2009-12-30T15:28:46","slug":"como-evitar-dog-pile-effect-no-rails","status":"publish","type":"post","link":"https:\/\/blog.plataformatec.com.br\/2009\/09\/como-evitar-dog-pile-effect-no-rails\/","title":{"rendered":"Como evitar que a sua aplica\u00e7\u00e3o Rails seja derrubada pelo dog-pile effect"},"content":{"rendered":"

Todo mundo j\u00e1 ouviu falar de escalabilidade. Todo mundo tamb\u00e9m j\u00e1 ouviu falar de Memcached<\/a>. O que nem todos ouviram falar ainda \u00e9 do dog-pile effect e de como evitar esse problema. Mas antes de discutirmos sobre o que \u00e9 o dog-pile effect, vamos primeiro dar uma olhada em como \u00e9 simples usar o Memcached com o Rails.<\/p>\n

Rails com Memcached<\/h3>\n

Se voc\u00ea nunca usou o Memcached com Raills ou ainda n\u00e3o conhece muito sobre escalabilidade com Rails, os epis\u00f3dios do Scaling Rails<\/a> feitos pelo Gregg Pollack<\/a> s\u00e3o extremamente recomendados, especialmente o epis\u00f3dio<\/a> sobre Memcached.<\/p>\n

Dado que voc\u00ea tem o Memcached instalado e quer us\u00e1-lo dentro do seu aplicativo Rails, basta voc\u00ea ir em um dos arquivos de configura\u00e7\u00e3o, por exemplo o production.rb e colocar:<\/p>\n

\r\nconfig.cache_store = :mem_cache_store\r\n<\/pre>\n

Por padr\u00e3o, o Rails vai procurar por um processo do Memcached rodando no localhost na porta 11211.<\/p>\n

Mas pra qu\u00ea eu vou querer usar o Memcached? Bem, digamos que o seu aplicativo possui alguma p\u00e1gina que precise rodar uma query lenta no banco de dados, tal como uma query para gerar um ranking de blog posts baseados na influencia dos autores, que demora em m\u00e9dia 5 segundos. Nesse caso, toda vez que um usu\u00e1rio entrar nessa p\u00e1gina do seu aplicativo, essa query ser\u00e1 executada e o aplicativo ter\u00e1 um tempo de resposta muito alto.<\/p>\n

Como voc\u00ea n\u00e3o vai querer que todo usu\u00e1rio sofra esperando 5 segundos para ver o ranking, o que voc\u00ea faz? Armazena o resultado dessa query no Memcached. Assim, uma vez que o resultado da query estiver cacheado, seus usu\u00e1rios n\u00e3o ter\u00e3o que esperar pelos malditos 5 segundos!<\/p>\n

O que \u00e9 o dog-pile effect?<\/h3>\n

Legal, voc\u00ea est\u00e1 cacheando o resultado da query do ranking e voc\u00ea finalmente vai poder dormir a noite. Ser\u00e1 mesmo?<\/p>\n

Vamos supor que voc\u00ea est\u00e1 expirando o cache baseado no tempo, ou seja, digamos que a query cacheada deva expirar ap\u00f3s 5 minutos. Vejamos como esse processo funciona diante de dois cen\u00e1rios:<\/p>\n

Um \u00fanico usu\u00e1rio acessando depois do cache ficar obsoleto (stale):<\/b><\/p>\n

Nesse primeiro cen\u00e1rio, quando um usu\u00e1rio acessar a p\u00e1gina do ranking, a query deve ser executada novamente, fazendo com que a CPU do seu sistema tenha um pico de consumo para conseguir process\u00e1-la. Depois dos 5 segundos, seu usu\u00e1rio, poder\u00e1 visualizar o ranking. At\u00e9 aqui, ok, demorou, mas o usu\u00e1rio conseguiu ver o ranking e seu sistema (ainda) est\u00e1 de p\u00e9.<\/p>\n

N usu\u00e1rios acessando depois do cache ficar obsoleto (stale):<\/b><\/p>\n

Digamos que, em um dado hor\u00e1rio, essa p\u00e1gina do seu aplicativo tenha em m\u00e9dia 4 requests\/segundo. Nesse cen\u00e1rio, entre a chegada do primeiro request e o banco de dados retornar o resultado da query, passar\u00e3o 5 segundos e ocorrer\u00e1 algo em torno de 20 requests. O problema \u00e9, todos esses 20 requests ir\u00e3o dar cache miss e seu aplicativo executar\u00e1 a query em todos os casos, consumindo muitos recursos de CPU e mem\u00f3ria. Esse \u00e9 o dog-pile effect.<\/p>\n

Dependendo de quantas requests resultarem em cache miss, do qu\u00e3o pesado for o processamento de sua query e dos recursos de infra dispon\u00edveis, o dog-pile effect pode acabar derrubando sua aplica\u00e7\u00e3o!<\/p>\n

Caramba!!! Quer dizer que o dog-pile effect \u00e9 t\u00e3o s\u00e9rio assim e pode chegar a derrubar minha aplica\u00e7\u00e3o!? Sim, isso pode acontecer. Mas existem algumas solu\u00e7\u00f5es para evitarmos esse tipo de problema. Vamos dar uma olhada em uma delas.<\/p>\n

\"O

O dog-pile effect pode fazer isso com seu aplicativo!<\/p><\/div>\n

Como resolver o problema do dog-pile?<\/h3>\n

Bem, o problema do dog-pile effect \u00e9 que deixamos mais de um request executar o custoso c\u00e1lculo da query. Hum… e se isolarmos esse c\u00e1lculo para apenas o primeiro request e servir o antigo cache para os pr\u00f3ximos requests at\u00e9 que o resultado cacheado da nova query esteja dispon\u00edvel? Parece uma boa id\u00e9ia… Ent\u00e3o vamos implement\u00e1-la!<\/p>\n

Desde o Rails 2.1, temos uma API padr\u00e3o para acessar o cache dentro do Rails definida por uma classe abstrata chamada ActiveSupport::Cache::Store<\/a>. Voc\u00ea pode ver mais dessa API nesse post<\/a> ou nesse \u00f3timo epis\u00f3dio do railscast<\/a>.<\/p>\n

O c\u00f3digo abaixo simplesmente cria um novo cache store baseado no j\u00e1 existente MemCacheStore<\/a>:<\/p>\n

\r\nmodule ActiveSupport\r\n  module Cache\r\n    class SmartMemCacheStore < MemCacheStore\r\n\r\n      alias_method :orig_read, :read\r\n      alias_method :orig_write, :write\r\n\r\n      def read(key, options = nil)\r\n        lock_expires_in = options.delete(:lock_expires_in) if !options.nil?\r\n        lock_expires_in ||= 10\r\n\r\n        response = orig_read(key, options)\r\n        return nil if response.nil?\r\n\r\n        data, expires_at = response\r\n        if Time.now > expires_at && !exist?(\"lock_#{key}\")\r\n          orig_write(\"lock_#{key}\", true, :expires_in => lock_expires_in)\r\n          return nil\r\n        else\r\n          data\r\n        end\r\n      end\r\n\r\n      def write(key, value, options = nil)\r\n        expires_delta = options.delete(:expires_delta) if !options.nil?\r\n        expires_delta ||= 300\r\n\r\n        expires_at = Time.now + expires_delta\r\n        package = [value, expires_at]\r\n        orig_write(key, package, options)\r\n        delete(\"lock_#{key}\")\r\n      end\r\n    end\r\n  end\r\nend\r\n<\/pre>\n

A id\u00e9ia do c\u00f3digo acima \u00e9 a seguinte:<\/p>\n

    \n
  1. Digamos que o resultado da sua query j\u00e1 est\u00e1 cacheada;<\/li>\n
  2. Durante os 5 minutos seguintes, todas as requests resultar\u00e3o em cache hit;<\/li>\n
  3. Depois dos 5 minutos, o primeiro request que chegar vai ver que o resultado cacheado j\u00e1 est\u00e1 stale, ou seja, obsoleto (linha 17). Esse request vai criar um lock para apenas ele ter direto de calcular o novo valor a ser cacheado;<\/li>\n
  4. Durante os pr\u00f3ximos 5 segundos, estar\u00e1 sendo feito o c\u00e1lculo da query, e todos os requests que chegarem, ao inv\u00e9s de resultarem em cache miss, v\u00e3o resultar em cache hit (devido ao lock), devolvendo o \u00faltimo valor cacheado (linhas 17 e 21);<\/li>\n
  5. Ap\u00f3s o primeiro request acabar de fazer o c\u00e1lculo da query, ele vai cachear o novo valor e apagar o lock que tinha criado (linhas 31 e 32);<\/li>\n
  6. Todas os novos requests que chegarem nos pr\u00f3ximos 5 minutos ver\u00e3o que o valor cacheado n\u00e3o est\u00e1 “stale”, resultar\u00e3o em cache hit e retornar\u00e3o o novo valor cacheado (linhas 17 e 21).<\/li>\n<\/ol>\n

    Armadilhas e algumas coisas para se lembrar<\/h3>\n

    Primeiramente, n\u00e3o \u00e9 recomendado setar o valor :expires_in do seu objeto cacheado:<\/p>\n

    \r\nRails.cache.write('my_key', 'my_value', :expires_in => 300)\r\n<\/pre>\n

    Com a nossa solu\u00e7\u00e3o, basta que voc\u00ea sete o valor do :expires_delta. Isso porque a aplica\u00e7\u00e3o ser\u00e1 respons\u00e1vel por expirar o cache, e n\u00e3o mais o Memcached.<\/p>\n

    \r\nRails.cache.write('my_key', 'my_value', :expires_delta => 300)\r\n<\/pre>\n

    Entretanto, existem alguns casos em que o Memcached ainda pode expirar o cache. Quando voc\u00ea inicializa o Memcached, ele aloca por default 64 MB de mem\u00f3ria. Digamos que os 64 MB est\u00e3o cheios, o que ele faz quando voc\u00ea tenta salvar o pr\u00f3ximo objeto em mem\u00f3ria? Ele usa o algor\u00edtmo de LRU<\/a> e deleta o objeto cacheado menos acessado.<\/p>\n

    Nos casos em que o Memcached expira o cache por conta pr\u00f3pria, h\u00e1 uma possibilidade de termos o dog pile effect novamente. Digamos que o ranking n\u00e3o \u00e9 acessado por muito tempo e o objeto cacheado \u00e9 descartado devido ao LRU. Se, de repente, muitas pessoas voltam a acessar a p\u00e1gina e ocorrem muitos acessos durante os cincos segundos iniciais em que o ranking \u00e9 gerado, o efeito pode ocorrer novamente.<\/p>\n

    Vale a pena ter essa situa\u00e7\u00e3o em mente na hora que voc\u00ea for dimensionar por exemplo o quanto de mem\u00f3ria vai ficar alocado para o seu Memcached.<\/p>\n

    J\u00e1 consigo lidar com o dog-pile, posso voltar a dormir!<\/h3>\n

    Resumindo, quando voc\u00ea estiver usando uma estrat\u00e9gia de caching, voc\u00ea provavelmente ir\u00e1 precisar expirar o seu cache. Com o processo de expira\u00e7\u00e3o, pode surgir o problema de dog-pile effect. Agora voc\u00ea j\u00e1 tem (mais) uma ferramenta na sua toolbox para resolver esse problema.<\/p>\n

    Basta colocar o c\u00f3digo acima do SmartMemCacheStore na sua aplica\u00e7\u00e3o Rails (no diret\u00f3rio lib\/ por exemplo), setar no seu production.rb (ou em outro environment.rb que voc\u00ea queira) e configurar a cache store para :smart_mem_cache_store (config.cache_store = :smart_mem_cache_store<\/b>). Se voc\u00ea usa a interface padr\u00e3o do Rails para acessar o cache (Rail.cache.read, Rails.cache.write) e arquitetou bem sua estrutura com o memcached, voc\u00ea estar\u00e1 protegido do dog-pile effect.<\/p>\n

    Curiosidade:<\/b> de onde vem o termo dog-pile effect? Dog-pile \u00e9 um termo comum em ingl\u00eas para se referir a um monte de coisas empilhadas. A id\u00e9ia da met\u00e1fora ent\u00e3o seria comparar uma pilha de requests uma em cima da outra, esperando pelo processamento da query custosa.<\/i><\/p>\n

    \"Um

    Um dog-pile de verdade =p<\/p><\/div>\n","protected":false},"excerpt":{"rendered":"

    Todo mundo j\u00e1 ouviu falar de escalabilidade. Todo mundo tamb\u00e9m j\u00e1 ouviu falar de Memcached. O que nem todos ouviram falar ainda \u00e9 do dog-pile effect e de como evitar esse problema. Mas antes de discutirmos sobre o que \u00e9 o dog-pile effect, vamos primeiro dar uma olhada em como \u00e9 simples usar o Memcached … \u00bb<\/a><\/p>\n","protected":false},"author":5,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"ngg_post_thumbnail":0,"footnotes":""},"categories":[3],"tags":[],"aioseo_notices":[],"jetpack_sharing_enabled":true,"jetpack_featured_media_url":"","_links":{"self":[{"href":"https:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/posts\/158"}],"collection":[{"href":"https:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/users\/5"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/comments?post=158"}],"version-history":[{"count":87,"href":"https:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/posts\/158\/revisions"}],"predecessor-version":[{"id":562,"href":"https:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/posts\/158\/revisions\/562"}],"wp:attachment":[{"href":"https:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/media?parent=158"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/categories?post=158"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/tags?post=158"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}