Rails内置缓存引擎与线程安全

升级到Rails 2.1之后,使用Rails内置的缓存之后,发现使用mem_cache_store总是报错,于是干脆研究了Rails内置缓存引擎的代码。阅读的结果发现,Rails的内置引擎只应该应用于进程模式,尤其是mem_cache_store,因为mem_cache_store是每进程建立一个到memcached的链接,即便是读取,也需要写入套接字,所以为了同步,必须使用锁。因此即使在读取缓存的时候,也会出现争用的情况。mem_cache_store也必须像Rails2.2中的ActiveRecord一样实现一个到memcached的线程池,或者使用异步链接,否则是发挥不出memcached的效率的。

我研究了另外几个cache_store,

  • memory_store则根本连锁机制都没有,但对应有个多线程的synchronized_memory_store,使用了Monitor
  • drb_store没有用过,可能Drb本身有一些同步机制,但估计也可能会出现与mem_cache_store一样的问题;
  • file_store应该是在多线程中应该比较理想的,因为使用了文件系统自身的同步机制,使用了File.atomic_write,无论是多线程还是多进程都能共享同一个file_store

所以,Rails的线程安全还有很长一段路要走。

PS:前面的我说的我遇到的mem_cache_store的错误是Rails.cache.fetch误加了:raw => true 参数。

Rails中取消asset id

在Rails中使用helper链接应用中的JavaScript和CSS文件的时候,会在文件的路径后面添加一个问号和一个数字,如:

问号后面的数字被称为asset id,目的是为了一些容易被缓存的目标如JavaScript和CSS文件,在文件被更改后不会被浏览器或代理服务器强制使用缓存,这也用于某些Ajax请求中。

但是这种asset id的行为比较怪异,他是一个查询字符串,但是没有键值对。我的一个部署方式是在一个已经架设了Wordpress的域名下,使用反向代理向用户暴露某个控制器。因为给Wordpress配置了mod_rewrite,并启用了.htaccess,将所有不存在的文件重写到wordpress的脚本处理。而这时我使用了Alias来将Rails的public目录下的javascripts和stylesheets目录绑定到相应的URL路径下,Apache却在处理这个asset id上有些问题,直接访问/javascripts/prototype.js可以得到,但/javascripts/prototype.js?11223344却会跑到WordPress中,并且说这个文件不存在。这可能和Apache处理URL的次序有关。当然,将DocumentRoot配置到Rails的Public目录,则不会出现这个问题。

无论如何,我要去掉这个asset id,根据Rails 2.0的代码,大家可以在actionpack/lib/action_view/helpers/asset_tag_helper.rb中找到生成asset id的方法rails_asset_id,如下:

代码的注释也说的很清楚了,asset id默认为文件的修改时间的时间戳,也可以通过环境变量RAILS_ASSET_ID来自行设置。环境变量可以在系统中设定,也可以在environment.rb或相应的环境配置的代码中修改,比如我在config/environments/production.rb中添加了:

这样最终Rails生成的代码中就没有asset id了。

小结

我认为asset id在开发环境中的意义更大,因为这些文件可能会被频繁修改,这么作就可以防止被浏览器强制缓存。但在实际的生产环境中,这些文件并不会被频繁修改,而我们正是需要这样的缓存,带有“?”的URL常常是不被缓存的,我们完全可以通过链接带有版本号的文件来确保调用了正确的版本,如”prototype-1.6.0.js”,Flickr就是这么做的:

你还能在它页面中看到更为夸张的文件名。当然,Flickr也在某些地方使用了asset id——究竟什么样的情况下用asset id,还可以继续探讨。

对于Ajax请求,POST方式是不会被缓存的,可以放心使用。

Cache_Lite的检测缓存失效

PEAR组件Cache_Lite是一个很实用的文件缓存组件,我在PHP编程中常常用到,现在我常需要检测某个id的缓存是否已经失效了,而Cache_Lite本身并未提供这样的功能,因此我写了这样一个函数:

使用也很简单,应该一眼就能看明白。当然也可以通过继承Cache_Lite,并添加一个方法来完成。

缓存友好的网页

缓存友好的网页

by Jennifer Vesperman 03/07/2002 原文地址:http://www.linuxdevcenter.com/pub/a/linux/2002/02/28/cachefriendly.html

翻译:ShiningRay @ Nirvana Studio

现在我们有很多HTTP缓存。他们存放你的页面多久呢?他们应该存放多久呢?RFC 2616(HTTP/1.1)指出了缓存必须遵循ExpiresCache-Control头——那么你的页面是否都有呢?

“在HTTP/1.1种的缓存的目的是为了在很多情况下消除发送请求的需要,同时在其他一些情况下消除发送完整的响应的需要。” RFC 2616

缓存友好页面的优点

“当缓存可以完全避免对原始服务器发送请求时,HTTP缓存的工作是最佳的。避免请求的主要机制是,针对原始服务器提供未来的一个确切的失效时间,表示一个响应可以被用来满足后续的请求。换句话说,一个缓存可以返回一个新的响应而不必先联系服务器。”RFC 2616

RFC写的时候是期望网页要包含失效期头的。如果谨慎地选择头中的失效时间,缓存可以不失去任何意义来提供存储了的页面。

当原始服务器不提供失效头时,缓存根据诸如“Last-Modified”之类的头来推断,猜测出一个合适的失效时间。与由了解页面内容以及更改频率的人设置的失效日期相比,推断的方法显然效率较低。

“由于推断的失效时间也许会降低语义透明度,应该被谨慎使用,同时我们鼓励原始服务器尽可能提供确切的失效时间。”——RFC 2616

关于缓存的注意事项

HTTP/1.1标准(第13节)关于缓存有一个强制的要求:它要求他们提供语义的透明——返回一个缓存了的响应必须提供本来从原始服务器上获取的同样的数据;同时它提倡读取原始服务器和客户端所提供的新鲜度要求。

缓存必须传递由上游缓存或者是原始服务器提供的警告,而且如果提供了一个陈旧的响应,他们也必须加入警告。一个缓存可以在特定的情况下提供一个陈旧的响应,大部分情况下是如果缓存无法连接到原始服务器同时客户端声明它可以接受一个陈旧的响应。

如果一个缓存收到了针对一个陈旧页面的请求,它发送一个验证请求询问原始服务器页面是否已经更改。最常见的验证工具便是最后的更新时间。如果在一秒钟内存储了两次更改,Last-Modified将会不正确。因此,HTTP/1.1利用Entity Tag头提供了更加严格的验证。

最简便的协助缓存的方法是保持你的HTTP服务器上的时间精确且总是发送DateLast-Modified头。

另外,要成为一个真正的缓存友好的站长,还要在你的页面中加入缓存头。

可用的缓存头

Expires头是最快捷最方便的解决方案。这个头声明了页面被认为不再可被缓存的时间,在这之后,任何保存了这个页面副本的缓存都应该联系原始服务器。语法是:

例如:

若要标记一个响应为已经“已经过期”或者“不可缓存”,头则应该设置为发送响应的时间。若要标记一个响应为“永不过期”,则应该将头设置为未来的一年。

另一个头是Cache-ControlCache-Control包括了这些元素:指明页面元素最大时限,他应该如何被缓存的,他如何被转换到另一个不同的媒介,以及他如何被存放在持久媒介中的。

本文使用Apache为例,设置头并在例子中详细讨论Cache-Control头。

在Apache中设置缓存头

#主要的方法:Expires

要使用Expires头,你需要运行在Apache 1.2或者更新的版本上,同时要启用 mod_expires 模块。去掉httpd.conf文件在“Dynamic Object Support”一节中的 expires_module 一行上的注释,然后重新编译Apache。

(如果你运行的是Apache 1.3或更新的版本,同时它已经配置为运行时加载模块,你可以编辑httpd.conf然后重新启动Apache而无需重新编译。)

mod_expires 基于三条指令来计算Expires头。这些指令可以应用于文档范围同时也可以在任何以下范围内使用:“server config”、“virutal host”、“directory”、或者“.htaccess”。

Expires指令有两种语法。其中一个有点难以阅读;它要你计算到失效为止用的秒数。幸运的是,这个模块同样可以读取一个更加人性化的语法。本文将解释较为可读的语法。

要用到的指令是:

base 可以是以下其中之一:

  • access
  • now (等同于“access”)
  • modification

num 是一个整数值,单位是 type :

  • years
  • months
  • weeks
  • days
  • hours
  • minutes
  • seconds

如果你准备对一个服务器、虚拟主机或者是目录使用Expires指令,编辑 httpd.conf 文件并在所需的范围内加入以下指令。

如果你要在 .htaccess 文件中使用Expires头,那么你要先编辑httpd.conf 设置相应的目录的AllowOverride。Apache只会读取设置了“Indexes”覆盖的目录中的 .htaccess

在相应目录里的.htaccess文件中加入Expires指令。站长可以编辑.htaccess文件而无需修改httpd.conf

.htaccess”方法的主要问题是Indexes覆盖,这样.htaccess文件将给予站长更多的配置选项而非仅仅Expires头。这也许并不是系统管理员所期望的。

#候选方法:Cache-Control

mod_cern_meta允许文件级的控制,同时它也可以使用Cache-Control头(或任何其他头)。响应头是放在原始目录的子目录中,根据原始文件名所命名的一个文件。

去掉cern_meta_module 一行的注释并重新编译,和上一节中对expires_module 的一样。

httpd.conf 文件中,打开MetaFiles on,将 MetaDirectory 设置为子目录名,同时把MetaSuffix设为头的文件的后缀名。

使用这些值的话,文件 /var/www/www.example.org/index.html 将会以 /var/www/www.example.org/.web/index.html.meta. 为元文件。

任何有效的HTTP头都可以放在这些文件中。这提供了另一种使用Expires头的方式,同时它可以加入Cache-Control头。相应的Cache-Control头如下:

修改失效机制,将覆盖Expires头。Max-age隐含了Canche-Control: public

表示对象可以被存在缓存中。这是默认值。

表示对象(或指定字段)不能被保存在一个共享的缓存中同时是针对一个单独的用户的。它可以被保存在一个私有的缓存中。

表示对象(或者指定字段)可以被缓存,但不能直接给客户除非经过原始服务器的重新验证。

表示条目不能存储在持久的存储媒介中,同时应该尽可能快地从非未定存储媒介中删除。

代理可以将数据从一个存储系统中转换到另外一个。这个指令表示(大多数)响应不能被转换。(RFC允许某些字段的转换,即使存在这个头)

强制代理重新验证该页面即使客户可以接受一个陈旧的响应。请在使用这些头之前阅读RFC,关于他们的使用有一些限制。

警告

  • HTTP/1.0有一个很小的缓存控制机制,仅能理解Pragma: no-cache头。使用HTTP/1.0的缓存将忽略ExpiresCache-Control头。
  • 任何一个Cache-Control指令都不能保证隐私性或者数据的安全性。“private”和“no-store”指令可以为隐私性和安全性方面提供一些帮助,但是他们并不能用于替代身份验证和加密。
  • 本文不能代替RFC。如果你要实现Cache-Control头,请阅读RFC来获取每个头的含义和限制的详细描述。

#最后的话

缓存是Internet的现实问题同时它能让带宽的使用更加有效。你的客户也许是通过一个缓存来浏览你的页面的,有时候还会用多个缓存。给你的页面加上缓存头可以保护你的页面内容也可以让你的客户节省他们的带宽。

#进一步阅读

Jennifer Vesperman 是《Essential CVS》一书的作者。她为O’Reilly Network、Linux Documentation Project撰稿,有时也为Linux.com。