扒扒HTTP缓存

摘要: 本文会从理论和实战两方面描述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 缓存包含了一个用户群体中常用的页面。

结构大体像下面这样:

  • private缓存 不需要太大的空间,web浏览器中就内置了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验证缓存响应

  • 验证令牌(Validation token)是由server通过ETag的HTTP header来传递。
  • 验证令牌实现高效的资源更新检查:如果资源没有改变,没有数据传输。

让我们假设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秒!”。这样我们就没有去重新下载资源,节省了时间和带宽。

Cache-Control

  • 每个资源都可以使用Cache-Control这个HTTP header来定义自己的缓存策略。
  • Cache-Control指令控制谁可以缓存response,在什么条件下以及缓存多长时间

一个最理想的request请求就是它不需要和server进行通信:一个本地的response副本可以节省所有的网络时延,而且还可以避免因为数据传输而带来的流量花费。为了实现这个理想的状态,HTTP规范允许server返回多个Cache-Control指令,从而来控制浏览器保存response的策略。

记住:

  • Cache-Control header已经被列入HTTP/1.1规范。取代了之前的报头(比如,Expires)用来定义response的缓存策略。所有现代浏览器都将支持Cache-Control,这正是我们希望看到的!

“no-cache” and “no-store”

no-cache:必须先与服务器确认返回的响应是否被更改,然后才能使用该响应来满足后续对同一个网址的请求。因此,如果存在合适的验证令牌 (ETag),no-cache 会发起往返通信来验证缓存的响应,如果资源未被更改,可以避免下载。

相反no-store比较简单,它只是简单的不允许浏览器以及所有的中间缓存对任何版本的response进行缓存。

“public” vs. “private”

public :所有内容都将被缓存(客户端和代理服务器都可缓存)

private:内容只缓存到私有缓存中(仅客户端可以缓存,代理服务器不可缓存) - 比如一个含有用户个人隐私信息的html页面会被缓存在用户的浏览器中,但不允许被CDN缓存。

“max-age”

缓存的内容将在 xxx 秒后失效, 这个选项只在HTTP 1.1可用, 并如果和Last-Modified一起使用时, 优先级更高。比如: “max-age=60” 就表示response可以被缓存,在未来的60秒内可以一直被复用。

优化Cache-Control策略决策树

按照这个这颗决策树,你就可以根据你的实际需求来决定使用哪些指令了。

比较理想的做法是,你应该尽量把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)定制缓存策略的能力使得我们可以去定义“缓存层次结构”。我们不仅可以去控制每个缓存可以保存多长时间,而且还可以控制新版本有多快被访问者看到。现在就让我们再来分析下上面的这个图:

  • 那个html页面被标记为“no-cache”,这就意味着浏览器每次请求都会去server那边一趟,如果内容改变了,就下载;如果没变就返回。同时,我们在css和js文件名中携带了指纹:如果这些文件的内容改变了,那么这个page的html也改变了,自然一个新的html将会被下载。
  • 这个css文件允许被缓存到浏览器以及中间缓存组件(比如:CDN),并且被设置了过期时间为一年。我们甚至可以设置更长的过期时间,因为我们在文件名中携带了指纹信息:如果css被更新了,那么这个url也改变了。
  • JavaScript也被设置了1年的过期时间,但是被标记为了“private”,也许是因为这个js中包含了一些用户的私人信息,所以不允许被CDN缓存。
  • 这个图片没有设置版本或指纹。但被设置了过期时间为一天。

通过对 ETag, Cache-Control以及 unique URLs的组合,可以让我们把世界上最好的都聚在了一起:足够长的过期时间、可以控制缓存在哪里以及按需更新(on-demand updates)

long-lived expiry times, control over where the response can be cached, on-demand updates.

缓存检查清单

这个世界没有最好的缓存策略。需要根据你具体的流量模式、服务数据的类型以及应用程序对于数据新鲜度的具体需求,来为每个资源定义具体的配置,以及整体的“缓存层次结构”。

下面是某老司机奉上的缓存策略的建议和驾驶小技术(一般人他不告诉):

  1. 尽量使用一致的URL:如果通过不同的url提供相同的内容,那么这个内容将会被下载和存储很多次。tip:url大小写敏感哦。
  2. 确保server给每个response提供一个验证的token(ETag):验证tokens可以避免(当server的资源没有改变的时候)相同的bytes被下载多次。
  3. 分拣处哪些资源是可以被中间缓存组件缓存的:一些response是所有用户都访问的。这部分就可以被CDN或者其他中间缓存组件缓存起来。
  4. 为每个资源都确定合适的缓存生命周期:不同的资源有不同的新鲜度需求。给每个资源都配置一个合适的max-age。
  5. 为你的网站确定一个最好的“缓存层次结构”:给url加指纹以及缩短(或no-cache)html 文档的生命周期这可以让你任性的控制更新可以多快被客户端获取。
  6. 解耦:一些资源更新比较频繁,而且这些更新只是资源都某一部分(比如一个js的function或者css的一个set)。你可以把这这部分从现有的文件中剥离出来作为一个单独的文件。这样就可以最大化的减少下载量。

附录:

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(importsource)

原文发表时间:2016-09-12

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏张善友的专栏

使用 MDT 2010 进行可伸缩部署

最近半个月在实施学习Windows 7自动化部署过程中的一个总结分享。Microsoft Deployment Toolkit 2010是微软最新一代部署工具,...

3765
来自专栏PHP在线

在Mac下使用MAMP Pro环境

以前,我使用Windows作为自己的工作系统,后来,改用Mac作为自己的主要工作系统了。 在Windows下,快速搭建*AMP环境,使用xampp或者WAMP之...

7757
来自专栏智能大石头

[netcore]CentOS安装使用.netcore极简教程(免费提供学习服务器) 新生命团队netcore服务器免费开放计划

本文目标是指引从未使用过Linux的.Neter,如何在CentOS7上安装.Net Core环境,以及部署.Net Core应用。

2000
来自专栏coding

vagrant极简教程:快速搭建centos7前言vagrant简介基本使用小结

1264
来自专栏杨建荣的学习笔记

使用expect运行动态脚本(r6笔记第19天)

在平时的工作中,如果接手的环境多了之后,每天去尝试连接服务器,都是例行的步骤,时间长了之后就会感觉这些工作都是繁琐重复的工作,其实我们可以尝试让工作更简化,更高...

2984
来自专栏京东技术

如何实现靠谱的分布式锁?(附SharkLock的设计选择)

当前使用较多的分布式锁方案主要基于 Redis、ZooKeeper 提供的功能特性加以封装来实现的,下面我们会简要分析下这两种锁方案的处理流程以及它们各自的问题...

1693
来自专栏JAVA高级架构

Redis面试题及分布式集群

2931
来自专栏JavaEdge

操作系统之设备管理一、I/O管理概述二、I/O硬件组成三、I/O控制方式(重点)四、I/O软件组成五、I/O相关技术六、I/O设备的管理七、I/O性能问题

2.1K6
来自专栏源码之家

.htaccess重写让空间绑定多个域名到不同的目录支持多站点

6557
来自专栏木头编程 - moTzxx

Linux crontab 定时任务整理笔记

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u011415782/article/de...

1762

扫码关注云+社区

领取腾讯云代金券