第七章、缓存
1 缓存的优点
为什么要使用缓存呢,因为使用缓存具有如下几个优点:
- 可以减少冗余数据的传输,节省资源
- 环节网络带宽的问题,可以达到用较少的带宽便能很快的加载页面
- 降低原始服务器的负载
- 降低距离时延,加载的页面距离我们越远需要的时间也就越长
1.1 冗余数据的传输
如果不使用缓存,所有的客户端每次需要的数据都从服务器获取。如果每次我们获取的内容和都不一样,这没问题,但是实际情况是,我们可能很多时候获取的都是同一份文档或者同一些数据,这个时候如果我们将这些数据或者文档缓存起来,后面客户端再想需要的时候,就可以直接从缓存中获取,面一遍一遍的传输这些冗余信息。
1.2 带宽瓶颈
一般情况网络服务商为我们提供的本地网络带宽都会比真正远程连接的带宽要宽。如果所有的请求都需要经过远程请求回来,这样可能出现由于远程带宽的限制,导致获取速度非常缓慢,所以如果我们能在本地网络获取到数据,就能避免这样的问题。
1.3 瞬间拥塞
当所有的客户端每次都从原始服务器请求数据,那么服务器很容出现过载的情况,因为访问量非常大,而且如果再出现一些突发事件,使得大家几乎处于同一时间去访问同一资源。这样对原始服务器的性能要求就非常高,否则很容易就会造成服务器死掉,所以通过缓存,将部分请求通过缓存进行处理,而不需要真正将请求交给服务器处理,就能大大减少服务器的承载量。
1.4 距离时延
网络传输速度非常快,好的传输介质可以接近光速(具体传输速度和传输介质也有非常大关系)。但是毕竟还是有速度限制,既然有速度限制,意味着距离越长肯定所用的时间也就越长。所以当原始服务器离我们很远的时候,请求时延就会比较严重,如果在本地有缓存,那就会好很多了。
1.5 命中和未命中
虽然面描述中缓存有非常多的好处,但是任何缓存也不肯讲所有Web上的资源进行缓存。首先要装下所有的资源的容量就不敢想象,而且我们服务器张的资源并不是一层不变的,随着时间的推移,部分数据或者文档是有更新的。所以缓存一般都是缓存部分资源。这个时候,如果客户端的请求能够被缓存满足,那么称为缓存命中(cache hit),如果缓存无法满足客户端的请求,需要转发给服务器进行处理,就称为缓存未命中(cache miss)
1.5.1 再验证
当客户端请求达到缓存的时候,缓存有所请求资源的副本。但是并不是有副本就能满足客户端要求的,因为缓存所保存的副本并不一定还有效,可能副本是很早以前的了,但是服务器上对应的该资源已经更新。所以还要求缓存要有能够对我们缓存的资源副本能够进行“新鲜度”检测。而这种检测就称为HTTP再验证。
HTTP协议也为这种再验证机制提供了很好的支持,当需要验证的时候,缓存可以发送一条较小的验证请求给服务器(HTTP提供了几个验证机制,后面会展开说明,这里以常用的If-Modified-Since为例),服务器会对该请求进行处理,如果缓存的资源还是新鲜可用的,就只需要返回304 Not Modified进行响应即可,无需传送整个资源。这种情况被称为再验证命中或者缓慢命中。这种情况请求速度比单纯的缓存命中会慢一些,但是由于没有传输整个资源,所以相对于从服务器请求资源会快一些。再验证结果一般有如下三种:
- 再验证命中:缓存中的资源副本有效,服务器返回HTTP 304 Not Modified响应;
- 再验证未命中-如果服务器还有存在该资源,但是缓存的资源副本失效,服务器会向客户端发送一条普通的HTTP 200 OK且带有完整内容的响应
- 对象被删除:服务器对象已经被删除了,这时候发送一个 404 Not Found响应,缓存也会将其副本删除。
1.5.2 命中率
由缓存提供的服务的请求所占的比例称为缓存命中率(cache hit rate),也称为缓存命中比例(文档命中率)。所以该值取值区间为0~1,一般使用百分比表示。缓存命中率的统计,有些是包含了再验证命中,有些没有包含,这个看自己需要什么了。一般来说,该值在40%左右是比较合理的一个命中率。
上面的缓存命中率是以请求次数为统计基础,这样可能存在一个问题,如果某些资源非常大,但是却很少命中,这个时候出现的结果是:缓存命中率相对较高,但是实际流量的消耗也非常大。这个时候,使用字节命中率进行统计可能更为准确。其表示缓存提供的字节,在传输的所有字节中所占的比例。
文档命中率说明阻止了多少通往外部网路的web事务
字节命中率水命阻止了多少字节传向因特网
最后这里补充一点,HTTP规范里面并没有规定怎么区分响应是来自缓存命中的还是访问原始服务器得到的。某些代理缓存的响应会包含Via首部添加一些信息说明用于判断,很多时候都是没有的,这时候我们可以通过Date、或者Age等首部信息进行判断。也有可能没有想先信息用于判断。
2 缓存的拓扑结构
2.1 私有缓存和公有缓存
一般来说,缓存可以分为私有缓存和公有缓存。
私有缓存一般为某个用户专享缓存,如大部分流量的缓存功能,一般浏览器会将资源缓存在本地电脑中。
公有缓存是一个群体或者某个用户团体共同享有,比如一个企业中,都使用同一个代理缓存,这个时候,再进行资源缓存的时候,不用每个用户都缓存一份资源,只需要在公有的代理缓存上缓存资源即可。
2.2 层次结构的代理缓存
实际应用当中,大部分的缓存都会呈现出一种层次结构。在这种结构中,较小缓存未命中的请求会被导向给较大缓存。基本四线是,越靠近客户端的地方使用较小的、廉价的缓存,在更高的层次中使用更大、更强的缓存,如下图所示:
上述图中,我们可以看到有两级缓存结构(这里暂时认为浏览器中没有缓存功能),当第一级缓存命中的时候,直接返回,如果未命中,继续转发到第二级缓存,以此类推。
这里还要注意点的是,缓存层级结构不能无限拉长,如果太长的缓存结构,因为每个缓存都会有部分性能和时间消耗,如果太长了,这种消耗就会变的很明显。至于多少层级比较合适,这个就要看具体环境和缓存的性能等综合考虑。
2.3 网状缓存
上面说了一种层次结构的缓存结构,实际当中还有很多呈现为一种网状结构的网状缓存。这种缓存结构,缓存在寻找下一个节点的时候要复杂一些,需要判断具体与哪个缓存或者是直接与服务器进行对话。这种代理缓存会决定选择何种路由对内容进行访问、管理和传送,因此可将其称为内容路由器(content router)。
一般网状缓存要具有以下几个基本功能:
- 能够根据URL在父缓存或者服务器之间进行动态选择
- 选择父缓存的时候,能动态选择一个较优父缓存
- 前往父缓存之前,在本地缓存中搜索已缓存的副本
- 允许其他缓存节点对本缓存节点内容的访问,但是不允许英特网流量通过他们的缓存
3 缓存的处理步骤
一般缓存的处理会经过下面几个步骤:
- 接收——缓存从网络中读取抵达的请求报文。
- 解析——缓存对报文进行解析,提取出 URL 和各种首部。
- 查询——缓存查看是否有本地副本可用,如果没有,就获取一份副本(并将其保存在本地)。
- 新鲜度检测——缓存查看已缓存副本是否足够新鲜,如果不是,就询问服务器是否有任何更新(后面会专门有一小节对此进行说明)。
- 创建响应——缓存会用新的首部和已缓存的主体来构建一条响应报文。
- 发送——缓存通过网络将响应发回给客户端。
- 日志——缓存可选地创建一个日志文件条目来描述这个事务。
缓存GET请求流程如下图所示:
4 缓存管理控制
4.1 新鲜度的保持
服务器上的资源并不是一层不变的,这些资源随着时间的推移可能会被修改,更新、或者删除。如果服务器上的内容已经更改,但是缓存依然给客户端的是之前旧的数据,那这个数据就是没用的。所以我们的缓存,必须要保证其资源的新鲜度。HTTP规范中,将这些保持一致的机制称为文档过期(document expiration)和服务器再验证(server revalidation)
4.1.1 文档过期
HTTP协议中,我们可以通过Cache-Control和expires首部来控制资源的过期,就好比在超时里面购买的东西,有一个过期时间一样。示例如下:
在缓存文档过期之前,缓存可以以任意频率使用这些副本,而无需与服务器联系
有了文档过期时间,缓存就知道什么时候可以直接使用缓存资源,什么时候该项服务器验证或者请求最新的资源。
Cache-Control和expires都可以用于文档过期,区别主要是expires使用的是一个绝对时间,而Cache-Control使用的是一个相对时间,可以避免服务器和缓存时间不同步的时候发生问题,所以优先推荐使用Cache-Control,具体秒速如下表所示:
4.1.2 服务器再验证方法
当我们的资源达到过期时间后,并不是一定就需要重新从服务器获取新的资源,而是会发起前面所说的服务器再验证。说明缓存需要询问原始服务器文档是否发生变化。
HTTP规范中定义了5个条件首部,但对于再验证来说,最常用的就是如下两个:
除了这两个,另外三个包括 If-Unmodified-Since(在进行部分文件的传输时,获取文件的其余部分之前要确保文件未发生变化,此时这个首部是非常有用的)、If-Range(支持对不完整文档的缓存)和 If-Match(用于与 Web 服务器打交道时的并发控制)。
这里重点关注前面两个即可。
If-Modified-Since: Date再验证(重点)
通过该首部进行再验证的时候,缓存想服务器发起一个携带该头部的GET请求,该请求一般被称为IMS请求,服务器收到请求后,一般会做如下处理:
- 如果能够识别该首部,就会判断所请求的资源在指定的日期后是否有更改,如果有更改,那么该条件为真,就会返回一个携带全新资源的响应给缓存,一般还包含一个新的过期时间;如果在该时间后资源没有修改,条件为false,服务器一般会返回一个304 Not Modifed的响应(不会携带文档资源)给缓存,同时根据需要也会更新部分相应头的信息,比如新的过期时间。
- 如果服务器不能识别该头部,一般就会把其当初普通的GET请求,直接返回所请求资源的内容。
- 一般If-Modified-Since会和Last-Modified配合使用,Last-Modified一般是服务器告诉缓存,该资源最新一次的修改时间是什么时候,缓存下次使用If-Modified-Since进行验证的时候,即可使用该时间。
- 最后再使用该方式进行验证的时候,还有一点需要注意的是,有些服务器在处理该请求头的时候,不是按照时间进行先后对比,而是按照字符串进行匹配对比。
If-None-Match: Tag 实体标签再验证(重点)
有些时候,上面的If-Modified-Since: Date
并不一定能满足我们的需求。考虑下面几种情况:
- 服务器上有一份文档,会被定期进行数据写入,但是写入的数据不一定发生变化。这个时候,如果写入的数据本身没有变化,但是使用If-Modified-Since: Date进行判断,结果就是服务器会重新返回一份相同内容的文档给缓存。
- 服务器不能准确地判断资源最后修改日期
- 如果文档的修改时间和验证的时间间隔小于1s,可能会由于精度不够,造成判断错误
- 上面集中情况下,If-Modified-Since: Date首部并不能很好的工作,为了解决这些问题,HTTP又引入了If-None-Match: Tag的方式进行比较。工作原理就是服务器给一份资源加上一个标签(比如一个序列号,版本名称等),当对资源修改的时候,同时修改该标签,然后当缓存使用If-None-Match: Tag进行验证的时候,就可以对比标签,如果标签不匹配,就说明有修改了,返回新的资源内容,并且携带一个Etag首部,用于告知缓存本次资源的标签值。如果匹配则一样返回304 Not Modified就行了。
- 缓存在使用该请求头的时候,标签的值可以包含多个,用于告诉服务器,这几个标签对应的副本,我本地都有缓存了。如:
If-None-Match: "v2.6" If-None-Match: "v2.4","v2.5","v2.6" If-None-Match: "foobar","A34FAC0095","Profiles in Courage
上面两种验证方式都是可用的,而且可以同时使用,那在使用过程中,他们的优先级是怎么样的呢,原则总结如下:
- 如果服务只返回了一种验证方式Last-Modified或者Etag,那么就直接使用一种即可,如果两者都提供了,那么下次验证的时候,就需要两者都用上。
- 服务在接收验证请求的时候,如果请求里面只有一种验证,按照之前的介绍的逻辑处理,入股有包含两种验证方式,则需要在两种方式的条件都满足的时候,才能返回304 Not Modified。
介绍完上面两个比较重要的验证方式后,这里HTTP/1.1开始还支持了一种“弱验证器”的特性。主要是用于-----有些时候,虽然我们资源有细微修改,但是缓存的内容还是可以继续使用的。这个时候也还是希望验证通过,而不是重新返回新的资源内容。弱验证器用“W/”前缀进行标识,如下:
ETag: W/"v2.6" If-None-Match: W/"v2.6”
不管相关的实体值以何种方式发生了变化,强实体标签都要发生变化。而相关实体在语义上发生了比较重要的变化时,弱实体标签也应该发生变化。
注:原始服务器一定不能为两个不同的实体重用一个特定的强实体标签值,或者为两个语义不同的实体重用一个特定的弱实体标签值。
4.2 缓存控制
一般该验证的前提是缓存的资源过期了的时候才进行。所以这一小节,我们就来介绍怎么控制缓存的过期时间。
HTTP规范提供了一下几种方式来帮助服务器控制缓存:
- 附加一个 Cache-Control: no-store 首部到响应中去;
- 附加一个 Cache-Control: no-cache 首部到响应中去;
- 附加一个 Cache-Control: must-revalidate 首部到响应中去;
- 附加一个 Cache-Control: max-age 首部到响应中去;
- 附加一个 Expires 日期首部到响应中去;
- 不附加过期信息,让缓存确定自己的过期日期。
4.2.1 no-store和no-cache
这两个响应头都能防止缓存提供未经验证的资源缓存给客户端。使用格式如下:
Pragma: no-cache Cache-Control: no-store Cache-Control: no-cache
其中,Cache-Control是HTTTP/1.1中的规范,而Pragma是为了兼容HTTP/1.0+所保留的使用方式,优先使用Cache-Control的方式。
虽然no-store和no-cache都能防止缓存提供未经验证的资源给客户端,但是两者还是有一定区别:
- no-store:表示禁止缓存对响应的内容进行复制保存,及该条响应不能进行缓存
- no-cache:缓存可以保存一份副本在本地,但是每次使用之前都必须同服务器进行验证
4.2.2 max-age和expires
Cache-Control: max-age 表示的是从服务器将文档传来之时起,可以认为此文档处于新鲜状态的秒数。还有一个 s-maxage 首部(注意 maxage 的中间没有连字符),其行为与 max-age 类似,但仅适用于共享(公有)缓存,使用格式如下:
Cache-Control: max-age=3600 Cache-Control: s-maxage=3600
如果我们不想该响应被缓存,可以设置其值为0即可。
expires的作用和max-age一样,都是设置缓存资源的新鲜时间,但是其值为绝对值
如果服务器这两个值都没有提供的情况下,缓存可以根据一定的算法自己确定一个有效期,一般会根据文档修改间隔进行处理。这种情况的参考意义不大,
4.2.3 must-revalidate
某些时候,我们为了节省资源,可以配置缓存使用一些过期对象。但是如果服务器希望某些对象严格按照过期信息来提供新鲜的对象。这个时候可以在响应中添加Cache-Control:must-revalidate头部进行限制。
如果对象过了有效期,进行再验证的时候,服务器出现问题,不可用的时候,不能使用之前过期缓存,应该返回504 GateWay TimeOut进行说明。
4.2.4 客户端对缓存的控制
除了服务器可以通过Cache-Control首部来对缓存进行控制,客户端也可以使用该头部进行缓存控制,如:可以让缓存必须从服务器获取资源,或者必须进行新鲜度验证等。具体使用如下表所示:
5 缓存当中一些相关时间的计算(了解)
在确定缓存是否新鲜的时候,只需要确定两个时间即可,一个是该缓存的新鲜生存期(freshness lefttime),一个是该缓存副本已经使用的试用期(age)。如果age<freshness left time
。那则说明该缓存还是有效的,新鲜的。
5.1 使用期的计算
使用期是指从服务器发出响应那一刻起后面所经过的总时间。所以这里包含了响应从服务器到缓存中间的传输时间,资源达到缓存后,缓存对其进行处理的时间,再加上资源被保存好之后真正在缓存中保留时间。
计算规则如下:
响应传输延迟时间=max(0, 收到响应的时间-响应头Date时间) 不考虑传输延迟的使用时间 = max(响应传输延迟时间,响应中age );//这里的响应age是指缓存代理自己发出响应时候的age头部的值(表示资源已经产生多长时间),这里取两者大的一个是为了保守计算 传输延迟时间 = 响应时间 - 请求时间; // 使用期中至少不低于这个值,也是为了保守计算 考虑延迟的使用时间=不考虑传输延迟的使用时间+传输延迟时间 停留缓存时间 = 当前时间 - 响应时间;//计算缓存一级停留的时间 最终保守使用期 = 考虑延迟的使用时间 + 停留缓存时间 // 这个值不是精准,是保存估计的值
使用伪代码计算规则如下:
/* * age_value 当代理服务器用自己的头部去响应请求时,Age标明实体产生到现在多长时间了。 * date_value HTTP 服务器应答中的Date字段 原始服务器 * request_time 缓存的请求时间 * response_time 缓存获取应答的时间 * now 当前时间 */ apparent_age = max(0, response_time - date_value); //缓存收到响应时响应的年龄 处理时钟偏差存在时,可能为负的情况 corrected_received_age = max(apparent_age, age_value); // 容忍Age首部的错误 response_delay = response_time - request_time; // 处理网络时延,导致结果保守 corrected_initial_age = corrected_received_age + response_delay; resident_time = now - response_time; // 本地的停留时间,即收到响应到现在的时间间隔 current_age = corrected_initial_age + resident_time;
通过上面的计算,我们最终就可以获得当前资源缓存的使用期了,该值相对来来说是一种保守估计值,比如同时存在Date和Age响应头的时候,我们会取其中使用期较长的时间,计算网络延迟的时候,我们也是直接结算从发起请求到相应达到整个网络延迟,所以最终得到的结果也是一个保守值。
5.2 新鲜生存期的计算
前面提到,要判断一份缓存是否新鲜可用的,除了使用期外,还需要确定一个新鲜生存期,需要比较两者才能得出结论,所以这一小节我们看下怎么计算新鲜生存期。
这里我们先不考虑客户端对缓存控制的情况,但从服务器来看新鲜生存期。
在服务器我们可以通过多种方式确定其新鲜生存期,一般遵循如下优先级进行确定:
max-age 》 expires - date_header 》 factor * max(0,date_header - last_modified_date)》default_cache_min_date
获取到最终的值后,还需要检查结果是否超过缓存的最大或者最小新鲜度。伪代码如下:
/** * heuristic 启发式过期值应不大于从那个时间开始到现在这段时间间隔的某个分数 * Max_Age_value_set 是否存在Max_Age值 Cache-Control字段中“max-age”控制指令的值 * Max_Age_value Max_Age值 * Expires_value_set 是否存在Expires值 * Expires_value Expires值 * Date_value Date头部 * default_cache_min_lifetime * default_cache_max_lifetime */ public int server_freshness_limit() { int factor = 0.1; //典型设置为10% int heuristic = false; // 启发式 默认为false if (Max_Age_value_set) { // 优先级一为 Max_Age freshness_lifetime = Max_Age_value; }elseif(Expires_value_set) { // 优先级二为Expires freshness_lifetime = Expires_value - Date_value; }elseif(Last_Modified_value_set) { // 优先级三为Last_Modified freshness_lifetime = (int)(factor * max(0, Date_value - Last_Modified_value )); heuristic = true; // 启发式 }else{ freshness_lifetime = default_cache_min_lifetime; heuristic = true; // 启发式 } if (heuristic) { freshness_lifetime = freshness_lifetime > default_cache_max_lifetime ? default_cache_max_lifetime : freshness_lifetime; freshness_lifetime = freshness_lifetime < default_cache_min_lifetime ? default_cache_min_lifetime : freshness_lifetime; } return freshness_lifetime; }
通过上面的计算,我们得到了服务器指定的新鲜生存期,但是实际应用当中,除了服务器,客户端也可以通过一些首部对缓存进行控制,这里我们将客户端的控制考虑进来,再进行修正计算,伪代码如下:
/** * Max_Stale_value_set 是否存在Max_Stal值 Cache-Control字段中“max-stale”的值 * Min_Fresh_value_set 是否存在Min_Fresh值 Cache-Control字段中“min-fresh”的值 * Max_Age_value_set 是否存在Max-Age值 Cache-Control字段中“max-age”的值 */ int sub client_modified_freshness_limit { int age_limit = server_freshness_limit( ); // 获取服务器设置的新鲜生存期 if (Max_Stale_value_set) { if (Max_Stale_value == INT_MAX) { age_limit = INT_MAX; } else { age_limit = server_freshness_limit( ) + Max_Stale_value; } } if (Min_Fresh_value_set) { age_limit = min(age_limit, server_freshness_limit( ) - Min_Fresh_value); } if (Max_Age_value_set) { age_limit = min(age_limit, Max_Age_value); } }
最终通过上面的修正计算,我们得到了最终的新鲜生存期的值。最后通过比较新鲜生存期和使用期的值,就能确定该缓存资源是否有效了。
HTTP缓存机制:https://www.cnblogs.com/ranyonsue/p/8918908.html