「旧文」关于 DalliStore 的 Namespace 的问题

「旧文」关于 DalliStore 的 Namespace 的问题

徐峥徐峥

这两天在 Tower 里遇到个古怪的问题,处理以后发现是 DalliStore 的 Namespace 配置不能使用 Symbol 造成的,把 DEBUG 过程记录下来,给大家参考。

两天前,有个家伙在 Tower 上提交了一条 bug,说是 Tower 里一个项目的 “查看全部文档” 页面会报错,尝试以后发现,当一个项目里的在线文档数超过 7 篇的时候,这个错误才会出现,在日志里发现输出的报错信息如下:

[views] documents#index: 17 - NoMethodError: undefined method `size' for :tower:Symbol

输出的报错位置是模板文件 documents/index.html.erb 的第 17 行:

<% cache @docs do %>

这个 @docs 实例变量是在 index 接口中通过 project.documents.active 被赋值的,但如果把上面的缓存片写法换成下面这样:

<% cache [@docs] do %>

页面就能正常显示。

为了搞清楚究竟是怎么回事,我们得看看到底这两种写法对 Rails 来说有什么不同。Rails 里关于 Fragment Cache 的源码在 ActionPack 的 action_controller/caching/fragments.rb 源文件中,其中使用 read_fragment 方法来获取模板缓存内容:

def read_fragment(key, options = nil)
  return unless cache_configured?
  key = fragment_cache_key(key)
 
  instrument_fragment_cache :read_fragment, key do
    result = cache_store.read(key, options)
    result.respond_to?(:html_safe) ? result.html_safe : result
  end
end

其中使用了 fragment_cache_key 方法,对传入的页面模板上的 key 进行转换,获取实际缓存片对应的 key。那么在我们上面的例子中,模板 key 为 @docs 时,获取的缓存片的 key 是:

views/documents/6-20140109185707/documents/7-20140109190809/documents/8-20140124122519/documents/9-20140122160257/documents/13-20140124113341/documents/14-20140124113354/documents/15-20140124113406/documents/16-20140124122444/documents/17-20140124114552/cb24a858585fcec8c

实际上是把查询出来的所有 9 篇文档的 key 组合起来了。而如果模板的 key 使用 [@docs],那么得到的缓存片的 key 是:

views/#/cb24a858585fcec8c

进行到这儿,会觉得理论上写 @docs 应该是正确的才对,那么继续深入下去,在 read_fragment 方法中,接下来使用这个缓存片的 key 去缓存系统中获取实际存储的结果:

result = cache_store.read(key, options)

对应 @docs 和 [@docs] 两个不同的模板 key,[@docs] 返回的值是 nil,而 @docs 如我们所愿的报了最开始的错误。

从这个结果我们可以知道,[@docs] 这种写法,实际上并没有真正让问题得到解决,这样的写法只会造成每次请求页面时,生成的模板缓存 key 不一样,因为<ActiveRecord::Relation:0x007fbfad35aa60> 是每次请求的数据在内存中的地址,所以实际上每次都不会取到缓存片,导致页面重新渲染。那么到底是什么原因使得使用 @docs 作为缓存 key 会报错的呢?

接下来我进入了 console,使用 Rails.cache.read 方法,传入转换后的那个实际的缓存 key,这个时候输出了真正的报错点:

NoMethodError: undefined method `size' for :tower:Symbol
 
from /Users/xuzheng/.rbenv/versions/1.9.3-p484/lib/ruby/gems/1.9.1/gems/dalli-2.6.2/lib/dalli/client.rb:343:in `validate_key

DalliStore 是我们使用的 memcache 的缓存库,于是去看对应的 client.rb 源文件,validate_key 这个方法的定义如下:

def validate_key(key)
  raise ArgumentError, "key cannot be blank" if !key || key.length == 0
  key = key_with_namespace(key)
 
  if key.length > 250
    max_length_before_namespace = 212 - (namespace || '').size
    key = "#{key[0, max_length_before_namespace]}:md5:#{Digest::MD5.hexdigest(key)}"
  end
 
  return key
end

这里首先使用 key_with_namespace,把 Rails 中生成的 key 做了一次转换,所以 @docs 的 key:

views/documents/6-20140109185707/documents/7-20140109190809/documents/8-20140124122519/documents/9-20140122160257/documents/13-20140124113341/documents/14-20140124113354/documents/15-20140124113406/documents/16-20140124122444/documents/17-20140124114552/cb24a858585fcec8c670822415d8d306

转换后的结果是:

tower:views/documents/6-20140109185707/documents/7-20140109190809/documents/8-20140124122519/documents/9-20140122160257/documents/13-20140124113341/documents/14-20140124113354/documents/15-20140124113406/documents/16-20140124122444/documents/17-20140124114552/cb24a858585fcec8c670822415d8d306

前面增加的 “tower”,是我们在 environment 配置文件里设置的 dalli_store 的 namespace:

config.cache_store = :dalli_store, { raise_errors: true, namespace: :tower }

获取了这个 key 以后,这个 key 的长度明显超过了 250,所以进入 if 分支进行处理,出错代码就在这一行:

max_length_before_namespace = 212 - (namespace || '').size

按照我们的设置,namespace 的值是 :tower,所以 name || ” 以后取 size,是对一个 Symbol 做 size 操作,于是出现了最开始的那个报错。

从这个错误可以知道两点,一是 DalliStore 配置 namesapce 如果使用 Symbol,会有潜在的隐患,我们已经在 Github 上给作者提交了 PR,看看作者的回复吧 并且已经合入Master分支了。

另外更重要的一点是,模板缓存的 key 使用数组,本身不是个好的实践原则,数组里的元素越多,生成的缓存片的长度越长,耗费的资源也越多,更好的作法,应该是在 index 接口中获取最后更新的文档的时间作为模板的缓存 key,像这样:

<% cache @docs.maximum('updated_at') do %>

或者,更进一步的,在 Document 模型中直接定义 cache_key 方法:

require 'digest/md5'
 
class Document < ActiveRecord::Base
  def self.cache_key
    Digest::MD5.hexdigest "#{maximum(:updated_at)}.try(:to_i)-#{count}"
  end
end

这样 Document 也能有自己的 cache_key 了。注:Tower 里删除文档实际上并不会真正 delete 这条记录,如果是真正会删除的资源,在 cache_key 中加入 “count” 数量是必要的。

这次 DEUBG 的过程实际上最大的收获是对 Rails Fragment Cache 的更深入的理解,在 Tower里,我们大量的使用了 Fragment Cache 来提高页面访问时的感知速度,今后我们会写更多的关于开发 Tower 时的一些经验分享给大家,希望对大家有所帮助。

还没有评论