Rails 4俄罗斯娃娃缓存如何防止踩踏事件?

时间:2013-11-26 16:30:08

标签: ruby-on-rails ruby caching memcached

我希望找到有关Rails 4中的缓存机制如何防止多个用户同时尝试重新生成缓存密钥的信息,即缓存踩踏:http://en.wikipedia.org/wiki/Cache_stampede

我无法通过谷歌搜索找到更多信息。如果我查看其他系统(例如Drupal),则通过数据库中的semaphores表实现缓存标记预防。

6 个答案:

答案 0 :(得分:6)

Rails没有内置机制来阻止缓存标记。

根据atomic_mem_cache_store的自述文件(替代ActiveSupport::Cache::MemCacheStore减轻缓存踩踏事件):

  

Rails(以及依赖于活动支持缓存存储的任何框架)都可以   不提供任何内置的解决方案

不幸的是,我猜这个宝石也无法解决你的问题。它支持片段缓存,但它仅适用于基于时间的过期。

在这里阅读更多相关信息: https://github.com/nel/atomic_mem_cache_store

更新和可能的解决方案:

我想到了这一点,并提出了我认为合理的解决方案。我没有证实这是有效的,并且可能有更好的方法来做到这一点,但我试图想到可以缓解大部分问题的最小变化。

我假设您在模板中执行类似cache model do的操作,如DHH所述(http://37signals.com/svn/posts/3113-how-key-based-cache-expiration-works)。问题是,当模型的updated_at列发生更改时,cache_key同样会发生更改,并且所有服务器都会尝试同时重新创建模板。为了防止服务器加盖,您需要在短时间内保留旧的cache_key。

您可以通过(dum da dum)缓存对象的cache_key,并使用短暂的到期时间(例如,1秒)和race_condition_ttl

您可以创建这样的模块并将其包含在您的模型中:

module StampedeAvoider
  def cache_key
    orig_cache_key = super
    Rails.cache.fetch("/cache-keys/#{self.class.table_name}/#{self.id}", expires_in: 1, race_condition_ttl: 2) { orig_cache_key }
  end
end

让我们回顾一下会发生什么。有许多服务器正在调用cache model。如果您的模型包含StampedeAvoider,则其cache_key现在将提取/cache-keys/models/1,并返回/models/1-111(其中111是时间戳)等内容,cache将用于获取已编译的模板片段。

更新模型后,model.cache_key将开始返回/models/1-222(假设222是新的时间戳),但在此之后的第一秒,cache将继续/models/1-111因为这是cache_key返回的内容。一旦通过,所有服务器将在/cache-keys/models/1上获得缓存未命中并将尝试重新生成它。如果它们都立即重新创建它,它将击败覆盖cache_key的点。但是因为我们将race_condition_ttl设置为2,所以除了第一个服务器之外的所有服务器都将延迟2秒,在此期间它们将继续根据旧的缓存密钥获取旧的缓存模板。一旦2秒过去,fetch将开始返回新的缓存密钥(它将由尝试读取/更新/cache-keys/models/1的第一个线程更新)并且它们将获得缓存命中,返回由第一个线程编译的模板。

钽哒!踩踏踩踏。

请注意,如果你这样做,你会做两倍的缓存读取,但是根据常见的踩踏事件,它可能是值得的。

我没有测试过这个。如果你试试,请告诉我它是怎么回事:))

答案 1 :(得分:5)

:race_condition_ttl中的ActiveSupport::Cache::Store#fetch设置应有助于避免此问题。正如documentation所说:

  

设置:race_condition_ttl在非常频繁使用缓存条目且负载很重的情况下非常有用。如果缓存过期并且由于负载过重,七个不同的进程将尝试本机读取数据,然后它们都会尝试写入缓存。为避免这种情况,查找过期缓存条目的第一个进程将使缓存过期时间超过:race_condition_ttl中设置的值。是的,此过程将陈旧值的时间延长了几秒钟。由于前一个缓存的使用寿命延长,其他进程将继续使用稍微过时的数据。与此同时,第一个进程将继续,并将写入缓存新值。之后,所有流程都将开始获得新价值。关键是保持:race_condition_ttl小。

答案 2 :(得分:0)

好问题。适用于单个多线程Rails服务器但不适用于多进程(或)环境的部分答案(感谢Nick Urban绘制此区别)是ActionView模板编译代码在每个模板的互斥锁上阻塞。见line 230 in template.rb here。请注意,在获取锁定之前和之后都会检查已完成的编译。

效果是序列化编译同一模板的尝试,其中只有第一个实际上会进行编译,其余的将获得已经完成的结果。

答案 3 :(得分:0)

非常有趣的问题。我搜索谷歌(如果你搜索“狗堆”而不是“踩踏”,你会得到更多的结果)但是像你一样,我没有得到任何答案,除了这篇博文:protecting from dogpile using memcache

基本上它会将片段存储在两个键中:key:timestamp(其中时间戳为活动记录对象的updated_at)和key:last

def custom_write_dogpile(key, timestamp, fragment, options)
  Rails.cache.write(key + ':' + timestamp.to_s, fragment)
  Rails.cache.write(key + ':last', fragment)
  Rails.cache.delete(key + ':refresh-thread')
  fragment
end

现在,当从缓存中读取并尝试获取不存在的缓存时,它会尝试改为使用key:last片段:

def custom_read_dogpile(key, timestamp, options)
  result = Rails.cache.read(timestamp_key(name, timestamp))

  if result.blank?
    Rails.cache.write(name + ':refresh-thread', 0, raw: true, unless_exist: true, expires_in: 5.seconds)
    if Rails.cache.increment(name + ':refresh-thread') == 1
      # The cache didn't exists
      result = nil
    else
      # Fetch the last cache, as the new one has not been created yet
      result = Rails.cache.read(name + ':last')
    end
  end
  result
end

这是我以前链接过的Moshe Bergman的简要摘要,或者你可以找到here

答案 4 :(得分:0)

没有针对memcache踩踏事件的保护措施。当涉及多台机器并且在这些多台机器上有多个进程时,这是一个真正的问题。 -Ouch -

当其中一个关键进程“死亡”而任何“锁定”......被锁定时,问题就更加复杂了。

为了防止踩踏事件,您必须在数据到期之前重新计算数据。因此,如果您的数据有效期为10分钟,则需要在第5分钟再次重新生成,并在新的到期时间内重新设置数据10分钟。因此,您不要等到数据到期才能再次设置它。

也不应允许您的数据在10分钟后过期,但每5分钟重新计算一次,并且永远不会过期。 :)

你可以使用wget& cron定期调用代码。

我建议使用redis,它可以保存数据并在崩溃发生时重新加载。

-daniel

答案 5 :(得分:0)

合理的策略是:

  • 使用:race_condition_ttl 至少刷新资源所需的预期时间。将它设置为比预期更少的时间进行刷新是不可取的,因为愤怒的暴徒最终会试图刷新它,导致踩踏事件。
  • 使用:expires_in时间计算为最长可接受的到期时间 减去 :race_condition_ttl,以允许单个工作人员刷新资源,避免踩踏事件。

使用上述策略将确保您不会超过到期/过期期限并避免踩踏事件。它的工作原理是因为只有一个工作人员通过刷新,而愤怒的暴徒使用缓存值延迟,race_condition_ttl延长时间直到最初预期的到期时间。

相关问题