前端学HTTP之缓存
Web缓存是可以自动保存常见文档副本的HTTP设备。当Web请求抵达缓存时,如果本地有“已缓存的”副本,就可以从本地存储设备而不是原始服务器中提取这个文档。本文将详细介绍缓存的相关内容
功能
总的来说,缓存有以下四个功能:缓存减少了冗余的数据传输,节省了网络费用;缓解了网络瓶颈的问题,不需要更多的带宽就能够更快地加载页面;降低了对原始服务器的要求,服务器可以更快地响应,避免过载的出现;降低了距离时延,因为从较远的地方加载页面会更慢一些
【冗余的数据传输】
有很多客户端访问一个流行的原始服务器页面时,服务器会多次传输同一份文档,每次传送给一个客户端。一些相同的字节会在网络中一遍遍地传输。这些冗余的数据传输会耗尽昂贵的网络带宽,降低传输速度,加重Web服务器的负载。有了缓存,就可以保留第一条服务器响应的副本,后继请求就可以由缓存的副本来应对了,这样可以减少那些流入/流出原始服务器的,被浪费掉了的重复流量
【带宽瓶颈】
缓存还可以缓解网络的瓶颈问题。很多网络为本地网络客户端提供的带宽比为远程服务器提供的带宽要宽。客户端会以路径上最慢的网速访问服务器。如果客户端从一个快速局域网的缓存中得到了一份副本,那么缓存就可以提高性能——尤其是要传输比较大的文件时
下表中说明了在几种不同的网速下,传输几种不同大小的文档时,带宽会对传输速度产生什么样的影响。带宽会给较大的文档带来显而易见的时延,不同类型网络的速度差异会非常明显。一个54kbit/s的Modem传输一个5MB的文件需要749秒(超过12分钟),而在快速以太网LAN中,只要不到一秒的时间
【瞬间拥塞】
缓存在破坏瞬间拥塞(Flash Crowds)时显得非常重要。突发事件(比如爆炸性新闻、批量E-mail公告,或者某个名人事件)使很多人几乎同时去访问一个Web文档时,就会出现瞬间拥塞。由此造成的过多流量峰值可能会使网络和Web服务器产生灾难性的崩溃
【距离时延】
即使带宽不是问题,距离也可能成为问题。每台网络路由器都会增加因特网流量的时延。即使客户端和服务器之间没有太多的路由器,光速自身也会造成显著的时延
假设某个Web页面中包含了20个小图片,都在旧金山的一台服务器上。如果波士顿的一个客户端打开了4条到服务器的并行连接,而且保持着连接的活跃状态,光速自身就要耗费大约1/4秒(240毫秒)的下载时间。如果服务器位于(距离旧金山6700英里的)东京,时延就会变成600毫秒。中等复杂的Web页面会带来几秒钟的光速时延
将缓存放在附近的机房里可以将文件传输距离从数千英里缩短为数十米。在实际应用中,信号的传输速度会比光速低一些,因此,距离时延会更加严重
缓存命中
缓存无法保存世界上每份文档的副本。可以用已有的副本为某些到达缓存的请求提供服务,这被称为缓存命中(cache hit)。其他一些到达缓存的请求可能会由于没有副本可用,而被转发给原始服务器。这被称为缓存未命中(cache miss)
原始服务器的内容可能会发生变化,缓存要不时对其进行检测,看看它们保存的副本是否仍是服务器上最新的副本。这些“新鲜度检测”被称为HTTP再验证(revalidation)。为了有效地进行再验证,HTTP定义了一些特殊的请求,不用从服务器上获取整个对象,就可以快速检测出内容是否是最新的
缓存可以在任意时刻,以任意频率对副本进行再验证。但由于缓存通常会包含数百万的文档,而且网络带宽是很珍贵的,所以大部分缓存只有在客户端发起请求,并且副本旧得足以需要检测的时候,才会对副本进行再验证
缓存对缓存的副本进行再验证时,会向原始服务器发送一个小的再验证请求。如果内容没有变化,服务器会以一个小的304 Not Modified进行响应。只要缓存知道副本仍然有效,就会再次将副本标识为暂时新鲜的,并将副本提供给客户端,这被称作再验证命中(revalidate hit)或缓慢命中(slow hit)。这种方式确实要与原始服务器进行核对,所以会比单纯的缓存命中要慢,但它没有从服务器中获取对象数据,所以要比缓存未命中快一些
HTTP提供了几个用来对已缓存对象进行再验证的工具,但最常用的是If-Modified-Since首部。将这个首部添加到GET请求中去,就可以告诉服务器,只有在缓存了对象的副本之后,又对其进行了修改的情况下,才发送此对象
这里列出了在3种情况下(服务器内容未被修改,服务器内容已被修改,或者服务器上的对象被删除了),服务器收到GET If-Modified-Since请求时会发生的情况:再验证命中——如果服务器对象未被修改,服务器会向客户端发送一个小的HTTP 304 Not Modified响应;再验证未命中——如果服务器对象与已缓存副本不同,服务器向客户端发送一条普通的、带有完整内容的HTTP 200 0K响应;对象被删除——如果服务器对象已经被删除了,服务器就回送一个404 Not Found响应,缓存也会将其副本删除
【命中率】
由缓存提供服务的请求所占的比例被称为缓存命中率(cache hit rate,或称为缓存命中比例),有时也被称为文档命中率(document hit rate)。命中率在0到1之间,但通常是用百分数来描述的,0%表示每次请求都未命中(要通过网络来获取文档),100%表示每次请求都命中了(在缓存中有一份副本)
缓存的管理者希望缓存命中率接近100%。而实际得到的命中率则与缓存的大小、缓存用户兴趣点的相似性、缓存数据的变化或个性化频率,以及如何配置缓存有关。命中率很难预测,但对现在中等规模的Web缓存来说,40%的命中率是很合理的。缓存的好处是,即使是中等规模的缓存,其所包含的常见文档也足以显著地提高性能、减少流量了。缓存会努力确保将有用的内容保存在缓存中
【字节命中率】
由于文档并不全是同一尺寸的,所以文档命中率并不能说明一切。有些大型对象被访问的次数可能较少,但由于尺寸的原因,对整个数据流量的贡献却更大。因此,有些人更愿意使用字节命中率(byte hit rate)作为度量值(尤其那些按流量字节付费的人)
字节命中率表示的是缓存提供的字节在传输的所有字节中所占的比例。通过这种度量方式,可以得知节省流量的程度。100%的字节命中率说明每个字节都来自缓存,没有流量流到因特网上去
文档命中率和字节命中率对缓存性能的评估都是很有用的。文档命中率说明阻止了多少通往外部网络的Web事务。事务有一个通常都很大的固定时间成分(比如,建立一条到服务器的TCP连接),提高文档命中率对降低整体延迟(时延)很有好处。字节命中率说明阻止了多少字节传向因特网。提高字节命中率对节省带宽很有利
【区分命中】
不幸的是,HTTP没有为用户提供一种手段来区分响应是缓存命中的,还是访问原始服务器得到的。在这两种情况下,响应码都是200 OK,说明响应有主体部分。有些商业代理缓存会在Via首部附加一些额外信息,以描述缓存中发生的情况
客户端有一种方法可以判断响应是否来自缓存,就是使用Date首部。将响应中Date首部的值与当前时间进行比较,如果响应中的日期值比较早,客户端通常就可以认为这是一条缓存的响应。客户端也可以通过Age首部来检测缓存的响应,通过这个首部可以分辨出这条响应的使用期
拓扑结构
缓存可以是单个用户专用的,也可以是数千名用户共享的。专用缓存被称为私有缓存(private cache)。私有缓存是个人的缓存,包含了单个用户最常用的页面。共享的缓存被称为公有缓存(public cache)。公有缓存中包含了某个用户团体的常用页面
【私有缓存】
私有缓存不需要很大的动力或存储空间,这样就可以将其做的很小,很便宜。Web浏览器中有内建的私有缓存——大多数浏览器都会将常用文档缓存在个人电脑的磁盘和内存中,并且允许用户去配置缓存的大小和各种设置。还可以去看看浏览器的缓存中有些什么内容。比如,对IE来说,可以从Tools(工具)-> Internet Options(因特网选项)对话框中获取缓存内容。MSIE将缓存的文档称为“临时文件”,并将其与相关的URL和文档过期时间一起在文件列表中列出
【公有缓存】
公有缓存是特殊的共享代理服务器,被称为缓存代理服务器(caching proxy server),或者更常见地被称为代理缓存(proxy cache)。代理缓存会从本地缓存中提供文档,或者代表用户与服务器进行联系。公有缓存会接受来自多个用户的访问,所以通过它可以更好地减少冗余流量
在下图中,每个客户端都会重复地访问一个(还不在私有缓存中的)新的“热门”文档。每个私有缓存都要获取同一份文档,这样它就会多次穿过网络。而使用共享的公有缓存时,对于这个流行的对象,缓存只要取一次就行了,它会用共享的副本为所有的请求服务,以降低网络流量
【层次结构】
在实际中,实现层次化(hierarchy)的缓存是很有意义的,在这种结构中,在较小缓存中未命中的请求会被导向较大的父缓存(parent cache),由它来为剩下的那些“提炼过的”流量提供服务。下图显示了一个两级的缓存层次结构。其基本思想是在靠近客户端的地方使用小型廉价缓存,而更高层次中,则逐步采用更大、功能更强的缓存来装载多用户共享的文档
我们希望大部分用户都能在附近的第一级缓存中命中。如果没有命中,较大的父缓存可能能够处理它们的请求。在缓存层次结构很深的情况下,请求可能要穿过很长一溜缓存,但每个拦截代理都会添加一些性能损耗,当代理链路变得很长的时候,这种性能损耗会变得非常明显
【缓存类型】
有些网络结构会构建复杂的网状缓存(cache mesh),而不是简单的缓存层次结构。网状缓存中的代理缓存之间会以更加复杂的方式进行对话,做出动态的缓存通信决策,决定与哪个父缓存进行对话,或者决定彻底绕开缓存,直接连接原始服务器。这种代理缓存会决定选择何种路由对内容进行访问、管理和传送,因此可将其称为内容路由器(content router)
网状缓存中为内容路由设计的缓存(除了其他任务之外)要完成下列所有功能:根据URL在父缓存或原始服务器之间进行动态选择;根据URL动态地选择一个特定的父缓存;前往父缓存之前,在本地缓存中搜索已缓存的副本;允许其他缓存对其缓存的部分内容进行访问,但不允许因特网流量通过它们的缓存
缓存之间这些更为复杂的关系允许不同的组织互为对等(peer)实体,将它们的缓存连接起来以实现共贏。提供可选的对等支持的缓存被称为兄弟缓存(sibling cache)。HTTP并不支持兄弟缓存,所以人们通过一些协议对HTTP进行了扩展,比如因特网缓存协议(Internet Cache Protocol, ICP)和超文本缓存协议(HyperText Caching Protocol, HTCP)
处理步骤
现代的商业化代理缓存相当地复杂。这些缓存构建得非常高效,可以支持HTTP和其他一些技术的各种高级特性。但除了一些微妙的细节之外,Web缓存的基本工作原理大多很简单。对一条HTTP GET报文的基本缓存处理过程包括以下7个步骤
1、接收——缓存从网络中读取抵达的请求报文
2、解析——缓存对报文进行解析,提取出URL和各种首部
3、査询——缓存査看是否有本地副本可用,如果没有,就获取一份副本,并将其保存在本地
4、新鲜度检测——缓存査看已缓存副本是否足够新鲜,如果不是,就询问服务器是否有任何更新
5、创建响应——缓存会用新的首部和已缓存的主体来构建一条响应报文
6、发送——缓存通过网络将响应发回给客户端
7、日志——缓存可选地创建一个日志文件条目来描述这个事务
在第一步中,缓存检测到一条网络连接上的活动,读取输入数据。高性能的缓存会同时从多条输入连接上读取数据,在整条报文抵达之前开始对事务进行处理
接下来,缓存将请求报文解析为片断,将首部的各个部分放入易于操作的数据结构中。这样,缓存软件就更容易处理首部字段并修改它们
在第三步中,缓存获取了URL,査找本地副本。本地副本可能存储在内存、本地磁盘,甚至附近的另一台计算机中。专业级的缓存会使用快速算法来确定本地缓存中是否有某个对象。如果本地没有这个文档,它可以根据情形和配置,到原始服务器或父代理中去取,或者返回一条错误信息
已缓存对象中包含了服务器响应主体和原始服务器响应首部,这样就会在缓存命中时返回正确的服务器首部。已缓存对象中还包含了一些元数据(metadata),用来记录对象在缓存中停留了多长时间,以及它被用过多少次等
HTTP通过缓存将服务器文档的副本保留一段时间。在这段时间里,都认为文档是“新鲜的”,缓存可以在不联系服务器的情况下,直接提供该文档。但一旦已缓存副本停留的时间太长,超过了文档的新鲜度限值(freshness limit),就认为对象“过时”了,在提供该文档之前,缓存要再次与服务器进行确认,以査看文档是否发生了变化。客户端发送给缓存的所有请求首部自身都可以强制缓存进行再验证,或者完全避免验证,这使得事情变得更加复杂了
HTTP有一组非常复杂的新鲜度检测规则,缓存产品支持的大量配置选项,以及与非HTTP新鲜度标准进行互通的需要则使问题变得更加严重了
我们希望缓存的响应看起来就像来自原始服务器的一样,缓存将已缓存的服务器响应首部作为响应首部的起点。然后缓存对这些基础首部进行了修改和扩充
缓存负责对这些首部进行改造,以便与客户端的要求相匹配。比如,服务器返回的可能是一条HTTP/1.0响应(甚至是HTTP/0.9响应)而客户端期待的是一条HTTP1.1响应,在这种情况下,缓存必须对首部进行相应的转换。缓存还会向其中插入新鲜度信息(Cache-Control、Age以及Expires首部),而且通常会包含一个Via首部来说明请求是由一个代理缓存提供的
[注意]缓存不应该调整Date首部。Date首部表示的是原始服务器最初产生这个对象的日期
一旦响应首部准备好了,缓存就将响应回送给客户端。和所有代理服务器一样,代理缓存要管理与客户端之间的连接。高性能的缓存会尽力高效地发送数据,通常可以避免在本地缓存和网络I/O缓冲区之间进行文档内容的复制
大多数缓存都会保存日志文件以及与缓存的使用有关的一些统计数据。每个缓存事务结束之后,缓存都会更新缓存命中和未命中数目的统计数据(以及其他相关的度量值),并将条目插入一个用来显示请求类型、URL和所发生事件的日志文件
最常见的缓存日志格式为Squid日志格式和网景的可扩展通用日志格式,但很多缓存产品都允许用户创建自定义的日志文件
保持新鲜
可能不是所有的已缓存副本都与服务器上的文档一致。毕竟,这些文档会随着时间发生变化。如果缓存提供的总是老的数据,就会变得毫无用处。已 缓存数据要与服务器数据保持一致
HTTP有一些简单的机制可以在不要求服务器记住有哪些缓存拥有其文档副本的情况下,保持已缓存数据与服务器数据之间充分一致。HTTP将这些简单的机制称为文档过期(document expiration)和服务器再验证(server revalidation)
【文档过期】
通过特殊的HTTP Cache-Control首部和Expires首部,HTTP让原始服务器向每个文档附加了一个“过期日期”,这些首部说明了在多长时间内可以将这些内容视为新鲜的
在缓存文档过期之前,缓存可以以任意频率使用这些副本,而无需与服务器联系——当然,除非客户端请求中包含有阻止提供已缓存或未验证资源的首部。但一旦已缓存文档过期,缓存就必须与服务器进行核对,询问文档是否被修改过,如果被修改过,就要获取一份新鲜(带有新的过期日期)的副本
【过期日期和使用期】
服务器用HTTP/1.0+的Expires首部或HTTP/1.1的Cache-Control: max-age响应首部来指定过期日期,同时还会带有响应主体。Expires首部和Cache-Control:max-age首部所做的事情本质上是一样的,但由于Cache-Control首部使用的是相对时间而不是绝对日期,所以我们更倾向于使用比较新的Cache-Control首部
Expires: Fri, 05 Jul 2016, 05:00:00 GMT Cache-Control: max-age=484200
【服务器再验证】
仅仅是已缓存文档过期了并不意味着它和原始服务器上目前处于活跃状态的文档有实际的区別,这只是意味着到了要进行核对的时间了。这种情况被称为“服务器再验证”,说明缓存需要询问原始服务器文档是否发生了变化
如果再验证显示内容发生了变化,缓存会获取一份新的文档副本,并将其存储在旧文档的位置上,然后将文档发送给客户端。如果再验证显示内容没有发生变化,缓存只需要获取新的首部,包括一个新的过期日期,并对缓存中的首部进行更新就行了
这是个很棒的系统。缓存并不一定要为每条请求验证文档的有效性——只有在文档过期时它才需要与服务器进行再验证。这样不会提供陈旧的内容,还可以节省服务器的流量,并拥有更好的用户响应时间
HTTP协议要求行为正确的缓存返回下列内容之一:“足够新鲜”的已缓存副本;与服务器进行过再验证,确认其仍然新鲜的已缓存副本;如果需要与之进行再验证的原始服务器出故障了,就返回一条错误报文;附有警告信息说明内容可能不正确的已缓存副本
【条件方法】
HTTP的条件方法可以高效地实现再验证。HTTP允许缓存向原始服务器发送一个“条件GET”,请求服务器只有在文档与缓存中现有的副本不同时,才回送对象主体。通过这种方式,将新鲜度检测和对象获取结合成了单个条件GET。向GET请求报文中添加一些特殊的条件首部,就可以发起条件GET。只有条件为真时,Web服务器才会返回对象
HTTP定义了5个条件请求首部。对缓存再验证来说最有用的2个首部是If-Modified-Since和If-None-Match。所有的条件首部都以前缀“If-”开头。下表中列出了在缓存再验证中使用的条件请求首部
[注意]其他条件首部包括If-Unmodified-Since(在进行部分文件的传输时,获取文件的其余部分之前,要确保文件未发生变化,此时这个首部是非常有用的)、If-Range(支持对不完整文档的缓存)和If-Match(用于与Web服务器打交道时的并发控制)
最常见的缓存再验证首部是If-Modified-Since。If-Modified-Since再验证请求通常被称为IMS请求。只有自某个日期之后资源发生了变化的时候,IMS请求才会指示服务器执行请求
如果自指定日期后,文档被修改了,If-Modified-Since条件就为真,通常GET就会成功执行。携带新首部的新文档会被返回给缓存,新首部除了其他信息之外,还包含了一个新的过期日期
如果自指定日期后,文档没被修改过,条件就为假,会向客户端返回一个小的304 Not Modified响应报文,为了提高有效性,不会返回文档的主体。这些首部是放在响应中返回的,但只会返回那些需要在源端更新的首部。比如,Content-Type首部通常不会被修改,所以通常不需要发送。一般会发送一个新的过期日期
If-Modified-Since首部可以与Last-Modified服务器响应首部配合工作。原始服务器会将最后的修改日期附加到所提供的文档上去。当缓存要对已缓存文档进行再验证时,就会包含一个If-Modified-Since首部,其中携带有最后修改已缓存副本的日期
If-Modified-Since: <cached last-modified date>
如果在此期间内容被修改了,最后的修改日期就会有所不同,原始服务器就会回送新的文档。否则,服务器会注意到缓存的最后修改日期与服务器文档当前的最后修改日期相符,会返回一个304 Not Modified响应
[注意]如果有一个不认识If-Modified-Since首部的老服务器收到了条件请求,它会将其作为一个普通的GET解释。在这种情况下,系统仍然能够工作,但由于要对未修改的文档数据进行不必要的传输,所以效率会比较低
有些Web服务器并没有将lf-Modified-Since作为真正的日期来进行比对。相反,它们在IMS日期和最后修改日期之间进行了字符串匹配。这样得到的语 义就是“如果最后的修改不是在这个确定的日期进行的”,而不是“如果在这个日期之后没有被修改过”。将最后修改日期作为某种序列号使用时,这种替代语义能够很好地识别出缓存是否过期,但这会妨碍客户端将If-Modified-Since首部用于真正基于时间的一些目的
有些情况下仅使用最后修改日期进行再验证是不够的,包括以下情况:有些文档可能会被周期性地重写(比如,从一个后台进程中写入),但实际包含的数据常常是一样的。尽管内容没有变化,但修改日期会发生变化;有些文档可能被修改了,但所做修改并不重要,不需要让世界范围内的缓存都重装数据(比如对拼写或注释的修改);有些服务器无法准确地判定其页面的最后修改日期;有些服务器提供的文档会在亚称间隙发生变化(比如,实时监视器),对这些服务器来说,以一秒为粒度的修改日期可能就不够用了
为了解决这些问题,HTTP允许用户对被称为实体标签(ETag)的“版本标识符”进行比较。实体标签是附加到文档上的任意标签(引用字符串)。它们可能包含了文档的序列号或版本名,或者是文档内容的校验和及其他指纹信息
当发布者对文档进行修改时,可以修改文档的实体标签来说明这个新的版本。这样,如果实体标签被修改了,缓存就可以用If-None-Match条件首部来GET文档的新副本了
下图中,缓存中有一个实体标签为V2.6的文档。它会与原始服务器进行再验证,如果标签v2.6不再匹配,就会请求一个新对象。因为,标签仍然与之匹配,因此会返回一条304 Not Modified响应
如果服务器上的实体标签已经发生了变化(可能变成了v3.0),服务器会在一个200 OK响应中返回新的内容以及相应的新Etag
可以在If-None-Match首部包含几个实体标签,告诉服务器缓存中已经存在带有这些实体标签的对象副本
If-None-Match: "v2.6" If-None-Match: "v2.4","v2.5","v2.6" If-None-Match: "football","a123244","Profiles in Courage"
【强弱验证器】
缓存可以用实体标签来判断,与服务器相比,已缓存版本是不是最新的(与使用最近修改日期的方式很像)。从这个角度来看,实体标签和最近修改日期都是缓存验证器(cachevalidator)
有时,服务器希望在对文档进行一些非实质性或不重要的修改时,不要使所有的已缓存副本都失效。HTTP/1.1支持“弱验证器”,如果只对内容进行了少量修改,就允许服务器声明那是“足够好”的等价体
只要内容发生了变化,强验证器就会变化。弱验证器允许对一些内容进行修改,但内容的主要含义发生变化时,通常它还是会变化的。有些操作不能用弱验证器来现(比如有条件地获取部分内容),所以,服务器会用前缀“W/”来标识弱验证器
ETag: W/"v2.6" If-None-Match: W/"v2.6"
不管相关的实体值以何种方式发生了变化,强实体标签都要发生变化。而相关实体 在语义上发生了比较重要的变化时,弱实体标签也应该发生变化
原始服务器一定不能为两个不同的实体重用一个特定的强实体标签值,或者为两个语义不同的实体重用一个特定的弱实体标签值。不管过期时间是多少,缓存条目都可能会留存任意长的时间。因此,假设缓存不会再次通过它在过去某个时刻获得的验证器,对一个条目进行验证是不合适的
如果服务器回送了一个实体标签,HTTP/1.1客户端就必须使用实体标签验证器。如果服务器只回送了一个Last-Modified值,客户端就可以使用If-Modified-Since验证。如果实体标签和最后修改日期都提供了,客户端就应该使用这两种再验证方案,这样HTTP/1.0和HTTP/1.1缓存就都可以正确响应了
除非HTTP/1.1原始服务器无法生成实体标签验证器,否则就应该发送一个出去,如果使用弱实体标签有优势的话,发送的可能就是个弱实体标签,而不是强实体标签。而且,最好同时发送一个最近修改值
如果HTTP/1.1缓存或服务器收到的请求既带有If-Modified-Since,又带有实体标签条件首部,那么只有这两个条件都满足时,才能返回304 Not Modified 响应
控制缓存
服务器可以通过HTTP定义的几种方式来指定在文档过期之前可以将其缓存多长时间。按照优先级递减的顺序,服务器可以:附加一个Cache-Control: no-store首部到响应中去;附加一个Cache-Control: no-cache首部到响应中去;附加一个Cache-Control: must-revalidate 首部到响应中去;附加一个Cache-Control: max-age首部到响应中去;附加一个Expires日期首部到响应中去;不附加过期信息,让缓存确定自己的过期日期
【no-store和no-cache】
HTTP/1.1提供了几种限制对象缓存,或限制提供已缓存对象的方式,以维持对象的新鲜度。no-store首部和no-cache首部可以防止缓存提供未经证实的已缓存对象
标识为no-store的响应会禁止缓存对响应进行复制。缓存通常会像非缓存代理服务器一样,向客户端转发一条no-store响应,然后删除对象
标识为no-cache的响应实际上是可以存储在本地缓存区中的。只是在与原始服务器进行新鲜度再验证之前,缓存不能将其提供给客户端使用。这个首部使用do-not-serve-from-cache-without-revalidation这个名字会更恰当一些
HTTP/1.1中提供Pragma: no-cache首部是为了兼容HTTP/1.0+。除了与只理解Pragma: no-cache的HTTP/1.0应用程序进行交互时,HTTP1.1应用程序都应该使用Cache-Control: no-cache
[注意]从技术上来讲,pragma:no-Cache首部只能用于HTTP请求,但在实际中它作为扩展首部已被广泛地用于HTTP请求和响应之中
【max-age】
Cache-Control: max-age首部表示的是从服务器将文档传来之时起,可以认为此文档处于新鲜状态的秒数。还有一个s-maxage首部(注意maxage的中间没有连字符),其行为与tnax-age类似,但仅适用于共享(公有)缓存:
Cache-Control: max-age=3600 Cache-Control: s-maxage = 3600
服务器可以请求缓存不要缓存文档,或者将最大使用期设置为零,从而在每次访问的时候都进行刷新
Cache-Control: max-age=0 Cache-Control: s-maxage = 0
【Expires】
不推荐使用Expires首部,它指定的是实际的过期日期而不是秒数。HTTP设计者后来认为,由于很多服务器的时钟都不同步,或者不正确,所以最好还是用剩余秒数,而不是绝对时间来表示过期时间。可以通过计算过期值和日期值之间的秒数差来计算类似的新鲜生存期:
Expires: Fri, 05 Jul 2016, 05:00:00 GMT
有些服务器还会回送一个ExPires:0响应首部,试图将文档置于永远过期的状态,但这种语法是非法的,可能给某些软件带来问题。应该试着支持这种结构的输入,但不应该产生这种结构的输出
【must-revalidate】
可以配置缓存,使其提供一些陈旧(过期)的对象,以提高性能。如果原始服务器希望缓存严格遵守过期信息,可以在原始响应中附加一个Cache-Control: must-revalidate首部
Cache-Control: must-revalidate
Cache-Control: must-revalidate响应首部告诉缓存,在事先没有跟原始服务器进行再验证的情况下,不能提供这个对象的陈旧副本。缓存仍然可以随意提供新鲜的副本。如果在缓存进行must-revalidate新鲜度检査时,原始服务器不可用,缓存就必须返回一条504 Gateway Timeout错误
【试探性过期】
如果响应中没有Cache-Control: max-age首部,也没有Expires首部,缓存可以计算出一个试探性最大使用期。可以使用任意算法,但如果得到的最大使用期大于24小时,就应该向响应首部添加一个Heuristic Expiration Warning(试探性过期警告,警告13)首部。但很少有浏览器会为用户提供这种警告信息
LM-Factor算法是一种很常用的试探性过期算法,如果文档中包含了最后修改日期,就可以使用这种算法。LM-Factor算法将最后修改日期作为依据,来估计文档有多么易变。算法的逻辑如下所示:如果已缓存文档最后一次修改发生在很久以前,它可能会是一份稳定的文档,不太会突然发生变化,因此将其继续保存在缓存中会比较安全。如果已缓存文档最近被修改过,就说明它很可能会频繁地发生变化,因此在与服务器进行再验证之前,只应该将其缓存很短一段时间。实际的LM-Factor算法会计算缓存与服务器对话的时间跟服务器声明文档最后被修改的时间之间的差值
通常人们会为试探性新鲜周期设置上限,这样它们就不会变得太大了。尽管比较保守的站点会将这个值设置为一天,但通常站点会将其设置为一周。如果最后修改日期也没有的话,缓存就没什么信息可利用了。缓存通常会为没有任何新鲜周期线索的文档分配一个默认的新鲜周期(通常是一个小时或一天)。有时,比较保守的缓存会将这种试探性新鲜生存期设置为0,强制缓存在每次将其提供给客户端之前,都去验证一下这些数据仍然是新鲜的
与试探性新鲜计算有关的最后一点是——它们可能比你想象的要常见得多。很多原始服务器仍然不会产生Expires和max-age首部。选择缓存过期的默认时间时要特别小心
【客户端的新鲜度限制】
Web浏览器都有Refresh(刷新)或Reload(重载)按钮,可以强制对浏览器或代理缓存中可能过期的内容进行刷新。Refresh按钮会发布一个附加了cache- control请求首部的GET请求,这个请求会强制进行再验证,或者无条件地从服务器获取文档。Refresh的确切行为取决于特定的浏览器、文档以及拦截缓存的配置
客户端可以用Cache-Control请求首部来强化或放松对过期时间的限制。有些应用程序对文档的新鲜度要求很高(比如人工刷新按钮),对这些应用程序来说,客户端可以用Cache-Control首部使过期时间更严格。另一方面,作为提高性能、可靠性或开支的一种折衷方式,客户端可能会放松新鲜度要求。下表对Cache-control请求指令进行了总结
文档过期系统并不是一个完美的系统。如果发布者不小心分配了一个很久之后的过期日期,在文档过期之前,对文档做的所有修改都不会出现在任何缓存中
因此,很多发布者都不会使用很长的过期日期。而且,很多发布者甚至都不使用过期日期,这样缓存就很难确定文档会在多长时间内保持新鲜了
设置缓存
不同的Web服务器为HTTP Cache-Control和Expiration首部的设置提供了一 些不同的机制。Apache Web服务器提供了几种设置HTTP缓存控制首部的机制。其中很多机制在默认情况下都没有启动
通过mod_headers模块可以对单独的首部进行设置。装载了这个模块,就可以用设置单个HTTP首部的指令来扩充Apache的配置文件了。还可以将这些设置与Apache的常用表达式以及过滤器结合在一起使用,将这些首部与个别内容关联起来。这里有一个配置实例,这个例子将某目录下所有的HTML文件都标识为非缓存的
<Files * .html> Header set Cache-control no-cache </Files>
mod_expires模块提供的程序逻辑可以自动生成带有正确过期日期的Expires首部。通过这个模块,就可以在文档最后一次被访问之后,或者在它的最近修改日期之后将过期日期设置为某个时间区间。通过这个模块可以为不同的文件类型设置不同的过期日期,还可以使用便捷的冗长描述信息,比如“访问时间增加一个月”,来描述其缓存能力。这里有几个例子:
ExpiresDefault "access plus 1 week" ExpiresByType text/html "modification plus 2 days 6 hours 12 minutes"
通过mod_cern_meta模块可以将一个包含HTTP首部的文件与特定的对象联系起来。启动这个模块时,就创建了一组“元文件”,每个需要控制的文档一个,而且还会为每个元文件添加所期望的首部
【通过HTTP-EQUIV控制HTML缓存】
HTTP服务器响应首部用于回送文档的到期信息以及缓存控制信息。Web服务器与配置文件进行交互,为所提供的文档分配正确的Cache-Control首部
为了让作者在无需与Web服务器的配置文件进行交互的情况下,能够更容易地为所提供的HTML文档分配HTTP首部信息,HTML2.0定义了<META HTTP-EQUIV>标签。这个可选的标签位于HTML文档的顶部,定义了应该与文档有所关联的HTTP首部。这里有一个<META HTTP-EQUIV>标签设置的例子,它将HTML文档标记为非缓冲的
<HEAD> <TITLE>My Document</TITLE> <META HTTP-EQUIV="Cache-control" CONTENT="no-cache"> </HEAD>
最初,HTTP-EQUIV标签是给Web服务器使用的。Web服务器应该为HTML解析<META HTTP-EQUIV>标签,并将规定的首部插入HTTP 响应中:HTTP服务器可以用此信息来处理文档。特别是,它可以在为请求此文档的报文所发送的响应中包含一个首部字段:首部名称是从HTTP-EQUIV属性值中获取的,首部值是从CONTENT属性值中获取的
不幸的是,支持这个可选特性会增加服务器的额外负载,这些值也只是静态的,而且它只支持HTML,不支持很多其他的文件类型,所以很少有Web服务器和代理支持此特性。 但是,有些浏览器确实会解析并在HTML内容中使用HTTP-EQUIV标签,像对待真的HTTP首部那样来处理嵌入式首部。这样的效果并不好,因为支持HTTP-EQUIV标签的HTML浏览器使用的Cache-control规则可能会与拦截代理缓存所用的规则有所不同。这样会使缓存的过期处理行为发生混乱
总之,<META HTTP-EQUIV>标签并不是控制文档缓存特性的好方法。通过配置正确的服务器发出HTTP首部,是传送文档缓存控制请求的唯一可靠的方法
进化过程
下面对缓存的进化过程做一个简单的介绍
1、无缓存
浏览器向服务器请求资源a.jpg,服务器找到对应资源把内容返回给浏览器。当浏览器再次向服务器请求资源a.jpg时,服务器重新发送完整的数据文件给浏览器
2、有缓存无更新
浏览器第一次请求a.jpg时服务器会发送完整的文件,浏览器可以把这个文件存到本地(缓存),下次再需要这个文件时直接从本地获取
3、缓存+Expires更新
浏览器第一次请求a.jpg时服务器会发送完整的文件,服务器在发送文件时还附带发送一些额外信息——过期时间,如
Expires: Mon,10 Dec 2016 02:25:22GMT
浏览器可以把这个文件和额外信息存到本地。当再次需要a.jpg的时候浏览器用当前浏览器时间和Expires做个比较,如果当前时间在过期时间以内,就直接使用缓存文件(状态为304);如果在过期时间以外就重新向服务器发送请求要资源(200)。服务器在每次给资源的时候都会发送新的过期时间
4、缓存+max-age更新
浏览器不再使用Expires,而是使用Cache-Control:max-age作为过期时间,这个相对过期时间更方便,而且可以避免使用Expires时本地时间与服务器时间不同的误差
Cache-Control: max-age=300
浏览器把文件和附带信息保存起来。当再次需要a.jpg时,如果是在300秒以内发起的请求则直接使用缓存(200, from xx cache),否则重新发起网络请求(200)
5、缓存+max-age+ETag更新机制
这时,有一个问题就是如果过期时间到了,但图片资源a.jpg并没有被修改,这时更新资源会浪费带宽
所以,使用ETag首部就解决了这个问题。其中Etag是对a.jpg文件的编码,只有当a.jpg在服务端被修改时,这个值才会改变
Cache-Control: max-age=300 ETag:W/"e-cbxLFQW5zapn79tQwb/g6Q"
浏览器在300秒后再次获取图片资源a.jpg时,先查看ETag是否有变化,如果有变化,说明图片被更新,则向服务器发送请求重新获取a.jpg。否则,则直接读取缓存中的资源