摘要: 本文会从理论和实战两方面描述http缓存。理论层面会介绍:缓存命中、缓存丢失、Revalidations(重新验证)、命中率(Hit Rate)、字节命中率(Byte Hit Rate)、如何区分命中和丢失、缓存拓扑、代理缓存分层、网状缓存、缓存处理过程。实战方面会介绍如何使用ETags验证缓存响应 、Cache-Control、优化Cache-Control用到的策略决策树以及如何使缓存失效并及时更新缓存的response,最后会列出实现http缓存的一些最佳实践。
开始吧。全文分为两个部分:理论和实战。
理论
先上理论:
web的缓存其实就是http协议的那些设备会对经常访问的document自动的保存一份副本而已。
当一个web request 到达了一个cache,如果这个local 缓存的副本是可用的,那么就直接返回这个副本就可以了,而不用去origin server上去取了。
缓存有以下的好处:
1、减少数据传输。在网络开销上为你节省金钱。
2、降低网络瓶颈。在带宽不是很理想的情况下网页依然飞速加载。
3、减少对origin server的访问。这样的话你的server就可以响应更快而且避免了过载。
4、降低了因为距离的时延。因为你去访问一个很远的地方的网页总是会比较慢。
接下来我们会解释缓存是如何改善我们的性能及降低成本的,以及如何来评判这些成效以及把缓存放在哪里来最大化缓存的效果。我们也会解释http如何保证缓存备份的新鲜以及缓存是如何与其他缓存交互的,以及如何与server们交互的等等。最后我们会送上一些最佳实践。
下面先来说几个概念:
缓存命中
缓存是很有用。但缓存不可能把世界上每份document都存一份。
有的请求能够获得一个可用的备份。我们叫这叫“缓存命中”,cache hit。
缓存丢失
还有的请求到达一个cache后被转发到了origin server,因为cache里没有可用的备份。我们叫这种情况叫“缓存丢失”,cache miss。
Revalidations(重新验证)
因为origin server上的内容会改变,所以缓存们就不得不时不时的去检查这些改变,来确保他们的备份是最新的。这些“新鲜度检查”我们一般叫:HTTP revalidations。为了保证这种新鲜,一个缓存可以在任何时候去更新他的备份。但因为缓存们通常有上百万个文档,而且由于带宽的限制,大部分的缓存都只是在有客户端请求的时候才会更新备份。
当一个缓存需要更新自己的缓存备份,他只需要向origin server发送一个很小字节的更新请求。如果内容没有改变,那么server就会返回304,表示原始数据没有被改变。缓存得到304后,知道自己还是合法的数据,于是又开心的把备份更新状态为refresh,然后把备份返回给了客户端。像下面这样:
我们叫这种带有检查的命中叫做“慢命中”,英文叫slow hit。虽然慢, 但好歹命中了。这种命中虽然比纯粹不需要检查的命中要慢点,但还是要比“缓存丢失”要快点儿,至少这种检查不用去server上获取原始的数据。
说了这么多,那么在http中怎么使用这些技能的呢?http给了我们一些工具,
可以让我们去更新缓存数据。最常用的做法就是If-Modified-Since 这个header。当我们把这个加在一个GET请求上以后,这个header就会告诉server说如果数据发生了变更就把缓存里的备份更新到最新。
下面就展示下当你把一个设置了“If-Modified-Since”header的http GET请求发送到server以后,三种情况下分别会发生什么事情:
当server 的内容没有改变的时候;当server的内容已经改变的时候;以及当在server上的数据被删除的时候。
1、Revalidate hit
如果server上的数据没有被改变的话,那么server就会返回给客户端一个很小的HTTP 304 Not Modified的状态。就像下面一样:
2、Revalidate miss
如果server的原始数据和缓存中的拷贝不一样的话,server就会发送给client一个普通的 HTTP 200 OK,以及数据内容。
3、Object deleted
如果server上的数据已经被删除了,那么server就会发送给客户端一个404 Not Found,并且缓存会把备份数据删掉。
命中率(Hit Rate)
缓存的命中率。这个很容易理解。就是只要某次请求的数据是从缓存中获取的,那么就算作缓存被命中。一般是个百分比。如果命中率为0%,那就意思说每次请求都不是去缓存拿的数据,而是通过网络去拿到的数据。如果命中率是100%就表示所有的数据都是从缓存中拿到的。
缓存的管理员总是希望自己的缓存命中率是100%。而实际的命中率肯定没有这么高。这要取决于你的缓存的容量大小、用户请求的相似性、要被缓存的数据的变化频率以及你对缓存的配置策略等。
命中率这事是比较难预测的。有数据显示说一般一个中等规模的网站缓存的命中率是40%是一个比较合理的范围。这个数据可能有点过时。
缓存中存储足够多的常被访问的那些document,就可以显著的改善性能和减少流量了。
字节命中率(Byte Hit Rate)
接下来说说字节命中率。有的人觉得上面说的缓存命中率并不能很好的说明问题。他们认为通过统计字节,最后把流量占比算出来比较好。于是有人发明了“字节命中率”,byte hit rate。
比如,有的文档只有1kb。而有的文档20kb。虽然20kb的文档也许被访问的次数并不多。但每次命中都会节省20倍的流量。
100%的字节命中表示每个byte都是从缓存中拿到的,没有一个byte是通过网络取的。
通过字节缓存命中率我们就可以知道我们节省了多少流量。
总之这两种命中率都很有用。第一种让我们知道有多少次请求没有通过网络。
第二种让我们知道我们节省了多少流量。
两种命中率是两个维度。提高缓存命中率可以让我们访问网页有更短的时延,提高字节命中率可以让我们节省更多的流量。
如何区分命中和丢失
http并没有为我们提供一个方法,让我们知道 每次响应是从缓存中拿到的还是从server中拿到的。这两种情况,response code都是 200 OK,只是告诉你本次响应是含有body的。有的商业代理缓存会在header里附加一些信息来描述在缓存中究竟发生了什么。不过我们很机智,有种方法可以让让我们可以知道这个细节。就是 Date header。就是我们通过把响应的时间header和现在的时间对比,如果响应的时间是一个较早的时间,那么这个响应是从缓存中拿到。还有一种方式就是判断Age header。
缓存拓扑
缓存分为private、public缓存。private缓存是个人缓存,包含一个用户经常访问的一些页面。public 缓存包含了一个用户群体中常用的页面。
结构大体像下面这样:
通过公共代理缓存,让每个用户的私有缓存不用再去server获取某个新的热门文档。而是由代理缓存代表用户们去获取这个文档。这样避免了每个用户重复获取同一份文档。像下面图里说的,a是没有使用public缓存策略的情况,b是使用了public缓存的情况。
代理缓存分层
在实际中,提供缓存分级是非常有用的。比如在较小的缓存中没有命中的请求,继续往上由父一级的缓存来继续命中提供。
下面这个图就展示了一个两级缓存。
靠近客户端的由一些廉价而且小巧的缓存方案来提供支持,更上层则使用更强大、更昂贵的缓存来为更多的用户共享文档。
当然不是分层越多越好。每分一层意味着过滤和分析。所以层多了反倒会让性能下降。
网状缓存
有些网络结构会构建复杂的网状缓存(cache mesh),而不是简单的缓存层次结构。网状缓存中的代理缓存之间会以更加复杂的方式进行对话,做出动态的缓存通信决策,决定与哪个父缓存进行对话,或者决定彻底绕开缓存,直接连接原始服务器。
这种代理缓存会决定选择何种路由对内容进行访问、管理和传送,因此可将其称为内容路由器(content router)。
网状缓存中为内容路由设计的缓存(除了其他任务之外)要完成下列所有功能。
1、根据URL在父缓存或原始服务器之间进行动态选择。
2、根据URL动态地选择一个特定的父缓存。
3、前往父缓存之前,在本地缓存中搜索已缓存的副本。
4、允许其他缓存对其缓存的部分内容进行访问,但不允许因特网流量通过它们的缓存。
缓存之间这些更为复杂的关系允许不同的组织互为对等(peer)实体,将它们的缓存连接起来以实现共赢。提供可选的对等支持的缓存被称为兄弟缓存(sibling cache),如下图。HTTP 并不支持兄弟缓存,所以人们通过一些协议对 HTTP进行了扩展,比如因特网缓存协议(Internet Cache Protocol,ICP)和超文本缓存协议(HyperText Caching Protocol,HTCP)。
缓存处理过程
现在很多的商业缓存都非常的复杂。他们的性能非常好,支持各种先进而高端的http上的一些功能以及一些其他黑科技。但是,抛开这些细节。web缓存的工作步骤其实很简单。就那么几步。比如一个http GET请求就是由下面的几步组成的:
1、接收(Receiving)-缓存读取从网络上的过来的请求message。
2、解析(Parsing)-缓存解析这个message,把url和headers挖掘出来。
3、查找(Lookup)-缓存检查有没有一个可用的备份,如果没有,就去fetch一个,然后把这个数据备份到本地。
4、新鲜度检查(Freshness check)-缓存检查自己里边存的文档们是不是足够新鲜。如果陈旧了过时了,那就向server发送更新请求。
5、创建一个Response。-缓存构建一个response的message。这个message包含一个新的headers以及一个body。(cached body)
6、发送-缓存通过网络把这个response返回给client。
7、日志(可选)-缓存创建一条日志用来描述这次事务(transaction)。这一步可以有,也可以没有。
实战
上面说了这么多理论。是该说点和动手沾边的事情了。下面这个图,展示了一个普通http request以及response。
server返回response中携带了一个http headers的集合。描述了content-type、长度、缓存指令、验证token等等。比如上面的这个交互中,server返回了一个1024byte的response,里边告诉客户端说把我发给你的内容保存120秒,并且提供了一个validation token(“x234dff”),这个token可以在稍后被用到,就是当缓存过期,我们可以通过这个token来去检查server上的资源是不是被修改了。
使用ETags验证缓存响应
让我们假设120秒已经过去了,这会浏览器对相同的资源进行了一次新的请求。
这时候,浏览器会首先检查本地缓存并查找之前的那个response,不幸的是,这会这个response已经不能被用了,因为已经“expired”(过期了)。这时候,浏览器完全可以对server进行一次重新请求,然后重新下载一次资源,但这样做显然效率很低。因为server上的资源并没有更新,我们要无缘无故的把和还在缓存里一摸一样的资源再重新下载一遍,傻啊。
这时候 validation tokens就派上用场了,就是那个ETag头部,这个ETag专门被设计用来解决这种问题的。说说ETag吧:server会生成并返回一个随机的token。一般是一个hash码或者file内容的一个指纹(fingerprint)。客户端并不关心你这个指纹是怎么生成的。在一次新的请求中,客户端只需要把这个指纹发送给server:如果指纹依然和服务端端一样,那么就表示资源没有被修改,于是就跳过“下载”这一步了。
上面这个图中。就是client自动提供了一个ETag token在“If-None-Match” HTTP 请求头中,server就会把这个token和当前的资源进行对比检查。如果没有改变,那么就返回一个“304 Not Modified”的response,意思告诉浏览器说:“在你缓存里呆着的那个之前的response还没有改变,恭喜你,你可以继续给这个资源续命120秒!”。这样我们就没有去重新下载资源,节省了时间和带宽。
一个最理想的request请求就是它不需要和server进行通信:一个本地的response副本可以节省所有的网络时延,而且还可以避免因为数据传输而带来的流量花费。为了实现这个理想的状态,HTTP规范允许server返回多个Cache-Control指令,从而来控制浏览器保存response的策略。
记住:
“no-cache” and “no-store”
no-cache:必须先与服务器确认返回的响应是否被更改,然后才能使用该响应来满足后续对同一个网址的请求。因此,如果存在合适的验证令牌 (ETag),no-cache 会发起往返通信来验证缓存的响应,如果资源未被更改,可以避免下载。
相反no-store比较简单,它只是简单的不允许浏览器以及所有的中间缓存对任何版本的response进行缓存。
“public” vs. “private”
public :所有内容都将被缓存(客户端和代理服务器都可缓存)
private:内容只缓存到私有缓存中(仅客户端可以缓存,代理服务器不可缓存) - 比如一个含有用户个人隐私信息的html页面会被缓存在用户的浏览器中,但不允许被CDN缓存。
缓存的内容将在 xxx 秒后失效, 这个选项只在HTTP 1.1可用, 并如果和Last-Modified一起使用时, 优先级更高。比如: “max-age=60” 就表示response可以被缓存,在未来的60秒内可以一直被复用。
按照这个这颗决策树,你就可以根据你的实际需求来决定使用哪些指令了。
比较理想的做法是,你应该尽量把response们缓存在客户端尽可能长的时间,而且要给每个response携带token来实现有效的revalidation(重验)。
Cache-Control directives | 解释 |
---|---|
max-age=86400 | Response可以被浏览器或者任何的中间缓存提供者(要是“public”)进行缓存1天时间(60 seconds x 60 minutes x 24 hours) |
private, max-age=600 | Response可以只能被被客户端的浏览器进行缓存,中间缓存(如,CDN)不能缓存。保存10分钟(60 seconds x 10 minutes) |
no-store | Response不允许被缓存!并且每次请求都必须要全量下载资源。 |
根据HTTP存档,跻身前30万的网站(由Alexa排名),近一半的下载的response都可以被浏览器缓存,这节约了多少成本!节省了多少次pageview和visit!但这并不意味着你的应用程序将有50%的资源可以被缓存:一些网站可以缓存其资源的90%以上,而其他人可能有很多私人或对时间敏感的数据,这种情况下你可能根本不敢缓存任何东西。
要仔细审查你的页面,然后挑出哪些资源可以被缓存,然后确保给这些资源携带合适的Cache-Control 和 ETag headers!
使缓存失效并及时更新缓存的response
由浏览器发起的所有的http request 都会首先被路由到浏览器的缓存处,去检查在浏览器的缓存里是否有合法的 缓存的response。如果有,本次请求的response就会从这个缓存里拿到数据然后返回。这样我们就不用通过网络去server上下载了。然而,如果我们现在有这样一个需求:现在我们想要更新缓存里的response,这个怎么搞呢?
你比方说,我们告诉我们的访问者们说:“你们要把这个CSS保存24小时(max-age=86400)”。但我们的前端设计美眉们在没过一个小时的时候,就提交了一次对css的修改。这时候,我们总不能等待23个小时吧,我们希望让用户尽快看到我们新的样式。这时候怎么办呢?是不是没办法了?我们通过什么办法来通知所有的客户端说“现在服务器上有个最新的css,你们来下载吧”?显然,这是个棘手的问题,我们没什么办法,除非你去修改这个资源的url!
一旦一个response被缓存在浏览器那里,这个缓存版本将会一直被使用,直到过期(max-age 或 expires)或者浏览器缓存被清空。比如用户手动把浏览器缓存清理掉。 于是,就会导致不同的用户看到的是不同的样式,他们在使用着不同版本的css样式。
那么,怎样才能得到两全呢?既要具备缓存能力同时又想要随时更新资源通知到每个用户。很简单,就是用上面那种粗暴的做法,改url!这样就会强制用户去下载最新的资源了。比较常见的做法是,给一个文件名上加个一个指纹或者一个版本号。比如index.x234dff.css
这种为每个resource( per-resource)定制缓存策略的能力使得我们可以去定义“缓存层次结构”。我们不仅可以去控制每个缓存可以保存多长时间,而且还可以控制新版本有多快被访问者看到。现在就让我们再来分析下上面的这个图:
通过对 ETag, Cache-Control以及 unique URLs的组合,可以让我们把世界上最好的都聚在了一起:足够长的过期时间、可以控制缓存在哪里以及按需更新(on-demand updates)
long-lived expiry times, control over where the response can be cached, on-demand updates.
这个世界没有最好的缓存策略。需要根据你具体的流量模式、服务数据的类型以及应用程序对于数据新鲜度的具体需求,来为每个资源定义具体的配置,以及整体的“缓存层次结构”。
下面是某老司机奉上的缓存策略的建议和驾驶小技术(一般人他不告诉):
附录:
public:所有内容都将被缓存(客户端和代理服务器都可缓存)。
private:内容只缓存到私有缓存中(仅客户端可以缓存,代理服务器不可缓存)。
no-cache:必须先与服务器确认返回的响应是否被更改,然后才能使用该响应来满足后续对同一个网址的请求。因此,如果存在合适的验证令牌 (ETag),no-cache 会发起往返通信来验证缓存的响应,如果资源未被更改,可以避免下载。
no-store:所有内容都不会被缓存到缓存或 Internet 临时文件中。
must-revalidation/proxy-revalidation:如果缓存的内容失效,请求必须发送到服务器/代理以进行重新验证。
max-age=xxx (xxx is numeric):缓存的内容将在 xxx 秒后失效, 这个选项只在HTTP 1.1可用, 并如果和Last-Modified一起使用时, 优先级较高。
本文分享自 ImportSource 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!