Apache网页服务器缓存的深入分析
2010-03-13 10:55:09 阿炯

Expires、Cache-Control、Last-Modified、ETag是RFC 2616(HTTP/1.1)协议中和网页缓存相关的几个字段。前两个用来控制缓存的失效日期,后两个用来验证网页的有效性。要注意的是,HTTP/1.0有一个功能比较弱的缓存控制机制:Pragma,使用HTTP/1.0的缓存将忽略Expires和Cache-Control头。我们这里以Apache2.x服务器为例,只讨论HTTP/1.1协议。

Expires
Expires字段声明了一个网页或URL地址不再被浏览器缓存的时间,一旦超过了这个时间,浏览器都应该联系原始服务器。RFC告诉我们:“由于推断的失效时间也许会降低语义透明度,应该被谨慎使用,同时我们鼓励原始服务器尽可能提供确切的失效时间。”

对于一般的纯静态页面,如html、gif、jpg、css、js,默认安装的Apache服务器,不会在响应头添加这个字段。Firefox浏览器接受到相应后,如果发现没有Expires字段,浏览器根据文件的类型和“Last-Modified”字段来推断出一个合适的失效时间,并存储在客户端。推测出的时间一般是接受到响应时间后的三天左右。

Apache的expires_module模块可以在Http响应头部自动加上Expires字段。在Apache的httpd.conf文件中进行如下配置:

#启用expires_module模块
LoadModule expires_module modules/mod_expires.so
# 启用有效期控制
ExpiresActive On
# GIF有效期为1个月
ExpiresByType image/gif A2592000
# HTML文档的有效期是最后修改时刻后的一星期
ExpiresByType text/html M604800
#以下的含义类似
ExpiresByType text/css “now plus 2 month”
ExpiresByType text/js “now plus 2 day”
ExpiresByType image/jpeg “access plus 2 month”
ExpiresByType image/bmp “access plus 2 month”
ExpiresByType image/x-icon “access plus 2 month”
ExpiresByType image/png “access plus 2 month”

对于动态页面,如果在页面内部没有通过函数强制加上Expires,例如header(”Expires:”。 gmdate(”D, d M Y H:i:s”) 。 ”GMT”),Apache服务器会把'Wed, 11 Jan 1984 05:00:00 GMT'作为Expires字段内容,返回给浏览器。即认为动态页面总是失效的,而浏览器仍然会保存已经失效的动态页面。
可以发现Firefox浏览器总是缓存所有页面,不管失效、不失效还是没有声明失效时间。即使缓存中声明了一个网页的失效日期是1970-01- 01 08:00:00,浏览器仍然会发送该文件在缓存中的Last-Modified和ETag字段。如果在服务器端验证通过,返回304状态,浏览器就还会使用此缓存。

Cache-Control
Cache-Control字段中可以声明多些元素,例如no-cache, must-revalidate, max-age=0等。这些元素用来指明页面被缓存最大时限,如何被缓存的,如何被转换到另一个不同的媒介,以及如何被存放在持久媒介中的。但是任何一个 Cache-Control指令都不能保证隐私性或者数据的安全性。“private”和“no-store”指令可以为隐私性和安全性方面提供一些帮助,但是他们并不能用于替代身份验证和加密。

Apache的mod_cern_meta模块允许文件级Http响应头部的控制,同时它也可以配置Cache-Control头(或任何其他头)。响应头文件是放在原始目录的子目录中,根据原始文件名所命名的一个文件。具体用法请参阅Apache的官方文档。

其中Cache-Control : max-age表示失效日期。如果没有启动mod_cern_meta模块,Apache服务器会把Expires字段中的日期换算成以秒为单位的一个 delta值,赋值给max-age。如果启动mod_cern_meta模块,并且配置了max-age值,Apache会将这个覆盖Expires字段。同时,max-age隐含了Canche-Control: public。这样浏览器接受到的Cache-Control : max-age和Expires值就是一致的。

如果失效日期Cache-Control : max-ag=0或者是负值,浏览器会在对应的缓存中把Expires设置为1970-01-01 08:00:00。

Last-Modified
Last-Modified和ETag是条件请求(Conditional Request)相关的两个字段。如果一个缓存收到了针对一个页面的请求,它发送一个验证请求询问服务器页面是否已经更改,在HTTP头里面带上” ETag”和”If Modify Since”头。服务器根据这些信息判断是否有更新信息,如果没有,就返回HTTP 304(Not Modify);如果有更新,返回HTTP 200和更新的页面内容,并且携带新的”ETag”和”Last-Modified”。

使用这个机制,能够避免重复发送文件给浏览器,不过仍然会产生一个HTTP请求。一般纯静态页面本身都会有Last-Modified信息,Apache服务器会读取页面文件中的Last-Modified信息,并添加到http响应头部。

对于动态页面,如果在页面内部没有通过函数强制加上Last-Modified,例如header(”Last-Modified: ” . gmdate(”D, d M Y H:i:s”) . ”GMT”),Apache服务器会把当前时间作为Last-Modified,返回给浏览器。

无论是纯静态页面还是动态页面,Firefox浏览器巧妙地按照接受到服务器响应的时间设置缓存页面的Last-Modified,而不是按照http响应头部中的Last-Modified字段。

ETag
既然有了Last-Modified,为什么还要用ETag字段呢?因为如果在一秒钟之内对一个文件进行两次更改,Last-Modified就会不正确。因此,HTTP/1.1利用Entity Tag头提供了更加严格的验证。

Apache服务器默认情况下,会对所有的静态、动态文件的响应头添加ETag字段。

在Apache的配置文件中可以通过FileETag指令配置该选项。FileETag指令配置了当文档是基于一个文件时用以创建 Etag(entity tag)响应头的文件的属性。在1.3.22及以前,ETag的值是对文件的索引节(INode),大小(Size)和最后修改时间(MTime)进行Hash后得到的。如果一个目录的配置包含了‘FileETag INode MTime Size’而其一个子目录包含了‘FileETag -INode’那么这个子目录的设置(并会被其下任何没有进行覆盖的子目录继承)将等价于‘FileETag MTime Size’。

在多台负载平衡的服务器环境下,同一个文件会有不同的etag或者文件修改日期,浏览器每次都会重新下载。设置‘FileETag None’可以使响应头不再包含ETag字段。

Etag在HTTP1.1中有介绍,主要的作用就是在(css file, image, javascript file)文件后面添加一个唯一的参数(相当于查询参数字符串),Etag有服务器端生成,并且随着文件的改变而改变,这样浏览器端就会只重新请求获取 Etag发生变化的文件,减少浏览器端数据的流量,加快浏览器的反应速度,重要的是减轻服务器端的压力,所以服务器端Etag的实现就比较重要了。

下面分别说下在Apache和Lighttpd中怎样配置Etag

Apache Etag
在Apache中设置Etag的支持比较简单,只用在含有静态文件的目录中建立一个文件.htaccess, 里面加入:
FileETag MTime Size

这样就行了,详细的可以参考Apache的文档。

Lighttpd Etag
在Lighttpd中设置Etag支持:
etag.use-inode: 是否使用inode作为Etag
etag.use-mtime: 是否使用文件修改时间作为Etag
etag.use-size: 是否使用文件大小作为Etag
static-file。etags: 是否启用Etag的功能
第四个参数肯定是要enable的, 前面三个就看实际的需要来选吧,推荐使用修改时间

Expires-过期时间是HTTP响应(response)的头部控制信息,浏览器会在指定过期时间内使用本地缓存,不用重新加载,对应的缓存对象主要是静态文件,如css,image,javascript;可以在服务器端进行全局设置,也可以应用端进行设置,这里我会说下服务器端的设置。

Apache首先要确定Apache是否支持了mod_expired模块,然后在Apache的配置文件中加入:
ExpiresActive on
ExpiresDefault "access plus 1 year"
可以看出,配置时可以指定设置Expire对象的文件类型,以及过期时间,这里是一年后。

Lighttpd
Lighttpd设置expire也要先查看是否支持了mod_expire模块,查看vlighttpd.conf文件中是否开启了该模块,然后就可以设置了。

下面的设置是让URI中所有images目录下的文件1小时后过期:
expire.url = ( "/images/" => "access 1 hours" )
下面是让作用于images目录及其子目录的文件;
$HTTP["url"] =~ "^/images/" {
expire.url = ( "" => "access 1 hours" )
}

也可以指定文件的类型:
$HTTP["url"] =~ "\。(jpg|gif|png|css|js)$" {
expire.url = ( "" => "access 1 hours" )
}

其他
设置较长的过期时间后如果想要客户端重新下载文件怎么办呢,比如当你修改了javascript或者是css文件后,这时可以改变文件的文件名,最合适的做法是给文件加上一个版本号,比如main_1.0.1.js, 这也是yahoo的做法。

当服务器的环境是cluster时,Etag的使用可能就要考虑的更多了,因为每个服务器生成的Etag不一样,所以最终的浏览器每次都检测到文件的Etag不一样而去重新请求,这里就不多说了,注意到这个问题就行了。

Etag在HTTP1.1中有介绍,主要的作用就是在(css file, image, javascript file)文件后面添加一个唯一的参数(相当于查询参数字符串),Etag有服务器端生成,并且随着文件的改变而改变,这样浏览器端就会只重新请求获取Etag发生变化的文件,减少浏览器端数据的流量,加快浏览器的反应速度,重要的是减轻服务器端的压力,所以服务器端Etag的实现就比较重要了。

现在我们有个问题为什么要使用Etag呢?
Etag主要为了解决Last-Modified无法解决的一些问题。他能比Last_Modified更加精确的知道文件是否被修改过。如果有个文件修改非常频繁,比如在秒以下的时间内进行修改,比如1秒内修改了10次,If-Modified-Since能检查只能秒级的修改,所以这种修改无法判断。原因是UNIX记录MTIME只能精确到秒。所以我们选择生成Etag,因为Etag可以综合Inode,MTime和Size,可以避免这个问题。

Etag的工作原理
Etag在服务器上生成后,客户端通过If-Match或者说If-None-Match这个条件判断请求来验证资源是否修改,我们常见的是使用If-None-Match,请求一个文件的流程可能如下:

新的请求
客户端发起HTTP GET请求一个文件(css ,image,js);服务器处理请求,返回文件内容和一堆Header(包括Etag,例如"2e681a-6-5d044840"),http头状态码为为200。

同一个用户第二次这个文件的请求
客户端在一次发起HTTP GET请求一个文件,注意这个时候客户端同时发送一个If-None-Match头,这个头中会包括上次这个文件的Etag(例如"2e681a- 6-5d044840"),这时服务器判断发送过来的Etag和自己计算出来的Etag,因此If-None-Match为False,不返回200,返回304,客户端继续使用本地缓存。

注意:服务器又设置了Cache-Control:max-age和Expires时,会同时使用,也就是说在完全匹配If-Modified-Since和If-None-Match即检查完修改时间和Etag之后,服务器才能返回304。

下面是在Apache中的Etag的配置
在Apache中设置Etag的支持比较简单,只需要在apache的配置中加入下面的内容就可以了:
FileETag MTime Size

注意:FileETag指令配置了当文档是基于一个文件时用以创建ETag(实体标签)应答头的文件的属性(ETag的值用于进行缓冲管理以节约网络带宽)。ETag的值由文件的inode(索引节点)、大小、最后修改时间决定。FileETag指令可以让您选择(如果您想进行选择)这其中哪些要素将被使用。主要关键字如下:

INode--文件的索引节点(inode)数
MTime--文件的最后修改日期及时间
Size--文件的字节数
All--所有存在的域,等价于:FileETag INode MTime Size
None--如果一个文档是基于文件的,则不在应答中包含任何ETag头


在大型多WEB集群时,使用ETag时有问题,所以有人建议使用WEB集群时不要使用ETag,其实很好解决,因为多服务器时,INode不一样,所以不同的服务器生成的ETag不一样,所以用户有可能重复下载(这时ETag就会不准),明白了上面的原理和设置后,解决方法也很容易,让ETag后面二个参数,MTime和Size就好了。只要ETag的计算没有INode参于计算,就会很准了。

关闭具体属性

Etag关掉的方法如下,加一个'none':
FileETag none

要关掉Last-Modified的方法要麻烦些,先想好哪些内容需要去掉Last-Modified 的标签,然后用header模块来控制:
LoadModule headers_module modules/mod_headers.so
<FilesMatch "\.(gif|jpg|png)">
 Header unset Last-Modified
</FilesMatch>

关于代理服与Web服之间的缓存关系,简单总结如下:
'ETag'默认是需要向源网站确认的,而'Last-Modified'默认是不向源服务器确认的。

HTTP 协议本身设计的优先级顺序如下,最上面优先级最高,到下面最小:
Cache-Control: no-store
Cache-Control: no-cache
Cache-Control: must-revalidate
Cache-Control: max-age
Expires:

关于它们的简单讲解:
1、不缓存控制
Cache-Control: no-store -- 禁止中间的缓存服务器存储这个对象,并给 header 转发给用户。
Cache-Control: no-cache -- 缓存服务器可以给文件缓存在本地缓存区,只是在和源站进行新鲜验证前,不能提供给客户端使用。
Pragma: no-cache -- 这是兼容 HTTP/1.0 时使用,原则上只能用于 HTTP 请求,与'Cache-Control: no-cache'的用途一样。

2、指定过期时间控制
Cache-Control: max-age -- 表示如果缓存服务器拿到这个文件后,这个对象多久之内是可用的,可以发给客户端使用的。
Cache-Control: s-maxage -- 行为和上面一样,只是只能使用于 public 地时候缓存。
Cache-Control: must-revalidate -- 默认的情况下,缓存代理是可以提供给用户一些旧的对象的内容,以提高性能;但如果原始服务器不希望这样,就可以配置这个选项,进行严格检查。比如源站不可用时,回源验证过程会失败,默认会吐旧的数据,但配置了这个以后会多报'504 Gateway Timeout'此类的错误。
Expires:  这个作用和 max-age 是一样。但这是指定一个过期的日期,但不是秒数,所以不建议使用。因为很多缓存服务器和源服务器常常时间不同步,所以基于 max-age 是使用相对的时间来表示还剩下多少秒可用,不要使用 Expires 来使用绝对时间。


该文章最后由 阿炯 于 2013-04-18 17:29:35 更新,目前是第 3 版。