Web 缓存是可以自动保存常见文档副本的 HTTP 设备。当 Web 请求抵达缓存时,如果本地有“已缓存的”副本,就可以从本地存储设备而不是原始服务器中提取这个文档。使用缓存有下列优点。
缓存确实能够解决上面的网络问题,但缓存无法保存世界上每份文档的副本。可以用已有的副本为某些到达缓存的请求提供服务。这被称为缓存命中(cache hit),其他一些到达缓存的请求可能会由于没有副本可用,而被转发给原始服务器。这被称为缓存未命中(cache miss)。
原始服务器的内容可能会发生变化,缓存要不时对其进行检测,看看它们保存的副本是否仍是服务器上最新的副本。这被称为 HTTP 再验证(revalidation)。为了有效地进行再验证,HTTP 定义了一些特殊的请求,不用从服务器上获取整个对象,就可以快速检测出内容是否是最新的。
Web缓存对它自身缓存的副本进行再验证时,会向原始服务器发送一个小的再验证请求。如果内容没有变化,服务器会以一个小的 304 Not Modified 进行响应。只要缓存知道副本仍然有效,就会再次将副本标识为暂时新鲜的,并将副本提供给客户端这被称作再验证命中(revalidate hit)或缓慢命中(slow hit)。这种方式确实要与原始服务器进行核对,所以会比单纯的缓存命中要慢,但它没有从服务器中获取对象数据,所以要比缓存未命中快一些。
HTTP 为我们提供了几个用来对已缓存对象进行再验证的工具,但最常用的是If-Modified-Since 首部。将这个首部添加到 GET 请求中去,就可以告诉服务器,只有在缓存了对象的副本之后,又对其进行了修改的情况下,才发送此对象。
这里列出了在 3 种情况下(服务器内容未被修改,服务器内容已被修改,或者服务器上的对象被删除了)服务器收到 GET If-Modified-Since 请求时会发生的情况:
由缓存提供服务的请求所占的比例被称为缓存命中率(cache hit rate,或称为缓存命中比例),有时也被称为文档命中率(document hit rate)。命中率在 0 到 1 之间,但通常是用百分数来描述的,0% 表示每次请求都未命中(要通过网络来获取文档),100% 表示每次请求都命中了(在缓存中有一份副本)。
由于文档并不全是同一尺寸的,所以文档命中率并不能说明一切。有些大型对象被访问的次数可能较少,但由于尺寸的原因,对整个数据流量的贡献却更大。因此,有些人更愿意使用字节命中率(byte hit rate)作为度量值(尤其那些按流量字节付费的人!)。
文档命中率和字节命中率对缓存性能的评估都是很有用的。文档命中率说明阻止了多少通往外部网络的 Web 事务。事务有一个通常都很大的固定时间成分(比如,建立一条到服务器的 TCP 连接),提高文档命中率对降低整体延迟(时延)很有好处。字节命中率说明阻止了多少字节传向因特网。提高字节命中率对节省带宽很有利。
HTTP 没有为用户提供一种手段来区分响应是缓存命中的,还是访问原始服务器得到的。在这两种情况下,响应码都是 200 OK,说明响应有主体部分。有些商业代理缓存会在 Via 首部附加一些额外信息,以描述缓存中发生的情况。
客户端有一种方法可以判断响应是否来自缓存,就是使用 Date 首部。将响应中Date 首部的值与当前时间进行比较,如果响应中的日期值比较早,客户端通常就可以认为这是一条缓存的响应。客户端也可以通过 Age 首部来检测缓存的响应,通过这个首部可以分辨出这条响应的使用期。
缓存可以是单个用户专用的,也可以是数千名用户共享的。专用缓存被称为私有缓存(private cache)。私有缓存是个人的缓存,包含了单个用户最常用的页面。共享的缓存被称为公有缓存(public cache)。公有缓存中包含了某个用户团体的常用页面。
私有缓存不需要很大的动力或存储空间,这样就可以将其做得很小,很便宜。Web 浏览器中有内建的私有缓存——大多数浏览器都会将常用文档缓存在你个人电脑的磁盘和内存中,并且允许用户去配置缓存的大小和各种设置。还可以去看看浏览器的缓存中有些什么内容。
公有缓存是特殊的共享代理服务器,被称为缓存代理服务器(caching proxy server),或者更常见地被称为代理缓存(proxy cache)。代理缓存会从本地缓存中提供文档,或者代表用户与服务器进行联系。公有缓存会接受来自多个用户的访问,所以通过它可以更好地减少冗余流量。每个客户端都会重复地访问一个(不在私有缓存中的)新的“热门”文档。每个私有缓存都要获取同一份文档,这样它就会多次穿过网络。而使用共享的公有缓存时,对于这个流行的对象,缓存只要取一次就行了,它会用共享的副本为所有的请求服务,以降低网络流量。
在实际中,实现层次化(hierarchy)的缓存是很有意义的,在这种结构中,在较小缓存中未命中的请求会被导向较大的父缓存(parent cache),由它来为剩下的那些“提炼过的”流量提供服务。其基本思想是在靠近客户端的地方使用小型廉价缓存,而更高层次中,则逐步采用更大、功能更强的缓存来装载多用户共享的文档。在缓存层次结构很深的情况下,请求可能要穿过很长一溜缓存,但每个拦截代理都会添加一些性能损耗,当代理链路变得很长的时候,这种性能损耗会变得非常明显。
有些网络结构会构建复杂的网状缓存(cache mesh),而不是简单的缓存层次结构。网状缓存中的代理缓存之间会以更加复杂的方式进行对话,做出动态的缓存通信决策,决定与哪个父缓存进行对话,或者决定彻底绕开缓存,直接连接原始服务器。这种代理缓存会决定选择何种路由对内容进行访问、管理和传送,因此可将其称为内容路由器(content router)。 网状缓存中为内容路由设计的缓存(除了其他任务之外)要完成下列所有功能。
缓存之间这些更为复杂的关系允许不同的组织互为对等(peer)实体,将它们的缓存连接起来以实现共赢。提供可选的对等支持的缓存被称为兄弟缓存(sibling cache)HTTP 并不支持兄弟缓存,所以人们通过一些协议对 HTTP 进行了扩展,比如因特网缓存协议(Internet Cache Protocol,ICP)和超文本缓存协议(HyperText Caching Protocol,HTCP)。
可能不是所有的已缓存副本都与服务器上的文档一致。毕竟,这些文档会随着时间发生变化。报告可能每个月都会变化。在线报纸每天都会发生变化。财经数据可能每过几秒钟就会发生变化。如果缓存提供的总是老的数据,就会变得毫无用处。已缓存数据要与服务器数据保持一致。
通过特殊的 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 首部。绝对日期依赖于计算机时钟的正确设置。
在缓存文档过期之前,缓存可以以任意频率使用这些副本,而无需与服务器联系——当然,除非客户端请求中包含有阻止提供已缓存或未验证资源的首部。但一旦已缓存文档过期,缓存就必须与服务器进行核对,询问文档是否被修改过,如果被修改过,就要获取一份新鲜(带有新的过期日期)的副本。
仅仅是已缓存文档过期了并不意味着它和原始服务器上目前处于活跃状态的文档有实际的区别;这只是意味着到了要进行核对的时间了。这种情况被称为“服务器再验证”,说明缓存需要询问原始服务器文档是否发生了变化。
HTTP 的条件方法可以高效地实现再验证。HTTP 允许缓存向原始服务器发送一个“条件 GET”,请求服务器只有在文档与缓存中现有的副本不同时,才回送对象主体。通过这种方式,将新鲜度检测和对象获取结合成了单个条件 GET。向 GET 请求报文中添加一些特殊的条件首部,就可以发起条件 GET。只有条件为真时,Web服务器才会返回对象。
最常见的缓存再验证首部是 If-Modified-Since。If-Modified-Since 再验证请求通常被称为 IMS 请求。只有自某个日期之后资源发生了变化的时候,IMS 请求才会指示服务器执行请求:
If-Modified-Since 首部可以与 Last-Modified 服务器响应首部配合工作。原始服务器会将最后的修改日期附加到所提供的文档上去。当缓存要对已缓存文档进行再验证时,就会包含一个 If-Modified-Since 首部,其中携带有最后修改已缓存副本的日期:
If-Modified-Since: <cached last-modified date>
如果在此期间内容被修改了,最后的修改日期就会有所不同,原始服务器就会回送新的文档。否则,服务器会注意到缓存的最后修改日期与服务器文档当前的最后修改日期相符,会返回一个 304 Not Modified 响应。
有些 Web 服务器并没有将 If-Modified-Since 作为真正的日期来进行比对。相反,它们在 IMS 日期和最后修改日期之间进行了字符串匹配。这样得到的语义就是“如果最后的修改不是在这个确定的日期进行的”,而不是“如果在这个日期之后没有被修改过”。将最后修改日期作为某种序列号使用时,这种替代语义能够很好地识别出缓存是否过期,但这会妨碍客户端将If-Modified-Since 首部用于真正基于时间的一些目的。
有些情况下仅使用最后修改日期进行再验证是不够的。
为了解决这些问题,HTTP 允许用户对被称为实体标签(ETag)的“版本标识符”进行比较。实体标签是附加到文档上的任意标签(引用字符串)。它们可能包含了文档的序列号或版本名,或者是文档内容的校验和及其他指纹信息。当发布者对文档进行修改时,可以修改文档的实体标签来说明这个新的版本。这样,如果实体标签被修改了,缓存就可以用 If-None-Match 条件首部来 GET 文档的新副本了。
有时,服务器希望在对文档进行一些非实质性或不重要的修改时,不要使所有的已缓存副本都失效。HTTP/1.1 支持“弱验证器”,如果只对内容进行了少量修改,就允许服务器声明那是“足够好”的等价体。只要内容发生了变化,强验证器就会变化。弱验证器允许对一些内容进行修改,但内容的主要含义发生变化时,通常它还是会变化的。有些操作不能用弱验证器来实现(比如有条件地获取部分内容),所以,服务器会用前缀“W/”来标识弱验证器。
ETag: W/"v2.6"
If-None-Match: W/"v2.6"
不管相关的实体值以何种方式发生了变化,强实体标签都要发生变化。而相关实体在语义上发生了比较重要的变化时,弱实体标签也应该发生变化。
如果服务器回送了一个实体标签,HTTP/1.1 客户端就必须使用实体标签验证器。如果服务器只回送了一个 Last-Modified 值,客户端就可以使用 If-ModifiedSince 验证。如果实体标签和最后修改日期都提供了,客户端就应该使用这两种再验证方案,这样 HTTP/1.0 和 HTTP/1.1 缓存就都可以正确响应了。
服务器可以通过 HTTP 定义的几种方式来指定在文档过期之前可以将其缓存多长时间。按照优先级递减的顺序,服务器可以:
HTTP/1.1 提供了几种限制对象缓存,或限制提供已缓存对象的方式,以维持对象的新鲜度。no-store 首部和 no-cache 首部可以防止缓存提供未经证实的已缓存对象。
标识为 no-store 的响应会禁止缓存对响应进行复制。缓存通常会像非缓存代理服务器一样,向客户端转发一条 no-store 响应,然后删除对象。
标识为 no-cache 的响应实际上是可以存储在本地缓存区中的。只是在与原始服务器进行新鲜度再验证之前,缓存不能将其提供给客户端使用。
HTTP/1.1 中提供 Pragma: no-cache 首部是为了兼容于 HTTP/1.0+。除了与只理解 Pragma: no-cache 的 HTTP/1.0 应用程序进行交互时,HTTP 1.1 应用程序都
应该使用 Cache-Control: no-cache。
Cache-Control: max-age 首部表示的是从服务器将文档传来之时起,可以认为此文档处于新鲜状态的秒数。还有一个 s-maxage 首部(注意 maxage 的中间没有连字符),其行为与 max-age 类似,但仅适用于共享(公有)缓存。服务器可以要求缓存服务器不要进行缓存,或者将最大使用期设置为零,从而在每次访问的时候都进行刷新。例如:
Cache-Control: max-age=0
Cache-Control: s-maxage=0
不推荐使用 Expires 首部,它指定的是实际的过期日期而不是秒数。HTTP 设计者后来认为,由于很多服务器的时钟都不同步,或者不正确,所以最好还是用剩余秒数,而不是绝对时间来表示过期时间。可以通过计算过期值和日期值之间的秒数差来计算类似的新鲜生存期:
Expires: Fri, 05 Jul 2002, 05:00:00 GMT
有些服务器还会回送一个 Expires:0 响应首部,试图将文档置于永远过期的状态,但这种语法是非法的,可能给某些软件带来问题。应该试着支持这种结构的输入,但不应该产生这种结构的输出。
Cache-Control: must-revalidate 响应首部告诉缓存,在事先没有跟原始服务器进行再验证的情况下,不能提供这个对象的陈旧副本。缓存仍然可以随意提供新鲜的副本。如果在缓存进行 must-revalidate 新鲜度检查时,原始服务器不可183 用,缓存就必须返回一条 504 Gateway Timeout 错误。
Web 浏览器都有 Refresh(刷新)或 Reload(重载)按钮,可以强制对浏览器或代理缓存中可能过期的内容进行刷新。Refresh 按钮会发布一个附加了 CacheControl 请求首部的 GET 请求,这个请求会强制进行再验证,或者无条件地从服务器获取文档。Refresh 的确切行为取决于特定的浏览器、文档以及拦截缓存的配置。客户端可以用 Cache-Control 请求首部来强化或放松对过期时间的限制。有些应用程序对文档的新鲜度要求很高(比如人工刷新按钮),对这些应用程序来说,客户端可以用 Cache-Control 首部使过期时间更严格。另一方面,作为提高性能、可靠性或开支的一种折衷方式,客户端可能会放松新鲜度要求。下表展示了Cache-Control请求指令。
缓存可以提高性能并减少流量。知道缓存可以帮助用户,并为用户提供更好的使用体验,而且缓存也可以帮助网络运营商减少流量。
你可能认为内容提供商会喜欢缓存。毕竟,如果到处都是缓存的话,内容提供商就不需要购买大型的多处理器 Web 服务器来满足用户需求了——他们不需要付过高的网络服务费,一遍一遍地向用户发送同样的数据。更好的一点是,缓存可以将那些漂亮的文章和广告以更快,甚至更好看的方式显示在用户的显示器上,鼓励他们去浏览更多的内容,看更多的广告。这就是内容提供商所希望的!吸引更多的眼球和更多的广告!
但这就是困难所在。很多内容提供商的收益都是通过广告实现的——具体来说,每向用户显示一次广告内容,内容提供商就会得到相应的收益。(可能还不到一两便士,但如果一天显示数百万条广告的话,这些钱就会叠加起来!)这就是缓存的问题——它们会向原始服务器隐藏实际的访问次数。如果缓存工作得很好,原始服务器可能根本收不到任何 HTTP 访问,因为这些访问都被因特网缓存吸收了。但如果你的收益是基于访问次数的话,你就高兴不起来了。
一种解决方案就是配置缓存,每次访问时都与原始服务器进行再验证。这样,每次访问时都会将命中推向原始服务器,但通常不会传送任何主体数据。当然,这样会降低事务处理的速度。
理想的解决方案是不需要将命中传递给服务器的。毕竟,缓存就可以记录下所有的命中。缓存只要将命中日志发送给服务器就行了。实际上,为了保持内容提供商们的满意度,有些大型缓存的提供商已经在对缓存日志进行人工处理,并将其传送给受影响的内容提供商了。但是,命中日志很大,很难移动。而缓存日志并没有被标准化或被组织成独立的日志,以传送给单独的内容提供商。而且,这里面还存在着认证和隐私问题。
RFC 2227,“HTTP 的简单命中计数和使用限制”中定义了一种简单得多的方案。这个协议向 HTTP 中添加了一个称为 Meter 的首部,这个首部会周期性地将对特定URL 的命中次数回送给服务器。通过这种方式,服务器可以从缓存周期性地获取对已缓存文档命中次数的更新。而且,服务器还能控制在缓存必须向服务器汇报之前,其中的文档还可以使用多少次,或者为缓存文档设置一个时钟超时值。这种控制方式被称为使用限制;通过这种方式,服务器可以对缓存向原始服务器汇报之前,已缓存资源的使用次数进行控制。
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有