Rails 4俄罗斯套娃缓存如何防止缓存失效风暴?

20

我想找到有关Rails 4缓存机制如何防止多个用户同时尝试重新生成缓存密钥的信息,即缓存失效的解决方案:http://en.wikipedia.org/wiki/Cache_stampede

通过谷歌搜索,我没有找到太多相关信息。如果看其他系统(例如Drupal)的话,缓存失效的预防是通过数据库中的semaphores表来实现的。


1
谁说它能防止这种情况? - Sergio Tulentsev
你说得对,这是我假设的结果。Rails有很多大型用户,我相信一定有人遇到过这个问题。 - Morgan Tocker
嗯,只有一个重要的Rails应用程序,那就是Basecamp :) 对于他们来说,缓存失效可能不是问题。 - Sergio Tulentsev
看起来没有内置的功能。在谷歌上搜索“rails cache dogpile”会带来更多结果,似乎有几个实现方法。但它们是否与cache_digests兼容呢?这是问题。 - Anthony Alberto
6个回答

7
Rails没有内置防止缓存流行的机制。
根据atomic_mem_cache_store的README(这是一个替代ActiveSupport::Cache::MemCacheStore且减轻了缓存流行的缓存机制):
“Rails(以及任何依赖于活动支持缓存存储的框架)没有提供任何内置解决此问题的解决方案。”
不幸的是,我猜想该gem也无法解决您的问题。它支持片段缓存,但仅限于基于时间的过期。
在此处阅读更多:https://github.com/nel/atomic_mem_cache_store 更新和可能的解决方法:
我考虑了一下,并想出了一个对我来说似乎是可行解决方案。我还没有验证过这是否有效,而且可能有更好的做法,但我正在尝试思考最小的更改,以减轻大部分问题。
我假设你正在像DHH所描述的那样在模板中执行cache model dohttp://37signals.com/svn/posts/3113-how-key-based-cache-expiration-works)。问题是,当模型的updated_at列更改时,cache_key也会更改,所有服务器都会尝试同时重新创建模板。为了防止服务器发生拥堵,您需要保留旧的cache_key一段时间。
您可以通过使用短期(例如1秒)过期和race_condition_ttl缓存对象的cache_key来实现这一点。
您可以创建一个像这样的模块,并将其包含在您的模型中:
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

让我们回顾一下会发生什么。有一堆服务器在调用“缓存模型”。如果您的模型包括“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返回的内容。一旦过去了1秒,所有服务器都会在/cache-keys/models/1上获得缓存未命中,并尝试重新生成它。如果它们都立即重新创建,那么就会破坏覆盖cache_key的目的。但是因为我们将race_condition_ttl设置为2,除第一个服务器外,所有服务器都会延迟2秒钟,这段时间内它们将继续根据旧的缓存键获取旧的缓存模板。一旦过去了2秒钟,fetch将开始返回新的缓存键(这将由试图读取/更新/cache-keys/models/1的第一个线程更新),它们将获得缓存命中,返回由该第一个线程编译的模板。

哒哒!避免了奔跑的情况。

请注意,如果您这样做,将会读取缓存两次,但根据挤压的常见程度,这可能是值得的。
我没有测试过这个。如果您尝试,请告诉我它的效果如何 :)

5
:race_condition_ttl设置在ActiveSupport::Cache::Store#fetch中,有助于避免这个问题。正如文档所述:

在缓存条目非常频繁地使用并且负载很重的情况下,设置:race_condition_ttl非常有用。如果一个缓存过期,并且由于负载过重,七个不同的进程将尝试本地读取数据,然后它们都将尝试写入缓存。为了避免这种情况,发现过期缓存条目的第一个进程将通过:race_condition_ttl设置来增加缓存过期时间。是的,这个进程正在将陈旧值的时间延长几秒钟。由于上一个缓存的寿命延长了,其他进程将继续使用略微陈旧的数据更长一点时间。同时,该第一个进程将继续向缓存中写入新值。此后,所有进程都将开始获取新值。关键是保持:race_condition_ttl小。


1
我能看出这对于常规缓存过期会有帮助,但是在俄罗斯套娃缓存中,键实际上会在更新时更改..所以增加到期时间不会有帮助吗? - Morgan Tocker
@MorganTocker 你是正确的。这种机制只适用于过期,而不适用于由ActiveRecord更新引起的缓存键更改。 - Gene
我添加了一个关于为什么保持race_condition_ttl小是不好的答案。实际上,它需要至少预期的刷新时间才能避免在超过race_condition_ttl截止日期时导致的踩踏事件。 - Andrew Hacking

0
非常有趣的问题。我在谷歌上搜索了一下(如果你搜索“dog pile”而不是“stampede”,你会得到更多结果),但像你一样,我没有得到任何答案,除了这篇博客文章:使用memcache保护免受dogpile攻击
基本上,它会将你的片段存储在两个键中: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 的简化摘要,或者你可以在 这里 找到。

0

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

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


2
Mutex.synchronize只能在同一个进程内同步线程,无法解决进程间(或服务器间)的同步问题。 - Nick Urban

0

Memcache的stampede问题是无法避免的。当多台机器和多个进程参与时,这是一个真正的问题。-痛苦-

当其中一个关键进程“死亡”并且任何“锁定”都被锁定时,问题会变得更加复杂。

为了防止stampede,您必须在数据过期之前重新计算数据。因此,如果您的数据有效期为10分钟,则需要在第5分钟重新生成数据,并使用新的过期时间重新设置数据,以便不必等到数据过期才重新设置它。

还应该不允许数据在10分钟标记处过期,而应该每5分钟重新计算一次,这样它就永远不会过期。 :)

您可以使用wget和cron定期调用代码。

我建议使用redis,它将允许您在崩溃时保存数据并重新加载数据。

-丹尼尔


这与phillbaker的建议类似。踩踏事件的问题不仅仅在于到期。使用俄罗斯套娃缓存时,更新会导致许多客户端在新的键名下重新生成缓存,从而引发踩踏事件。 - Morgan Tocker

0

一个合理的策略是:

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

使用上述策略将确保您不会超过到期/陈旧截止日期,并避免踩踏事件。这有效的原因是只有一个工作人员可以通过刷新,而愤怒的群众则使用带有race_condition_ttl扩展时间的缓存值一直保持在最初预定的到期时间。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接