一个典型的HTTP应用拓扑如下

user-agent — forward proxy … proxy server n … reverse proxy — origin server

user-agent通常是浏览器,也可以是第三方工具如curl,或HTTP相关模块,如python的request库。
forward proxy是正向代理,逻辑上靠近客户端,可以在浏览器的设置中指定。
proxy server n是处于中间位置,提供HTTP代理功能的代理服务器。
reverse proxy是反向代理,位于origin server服务端的边缘,由它代理提供WEB服务。
origin server真实提供WEB服务的源WEB服务器。

HTTP Cache是一个本地存储,保存了可缓存的响应消息。它可以分为private cache和public cache。比如浏览器上的cache就是一个private cache,仅为一个终端用户服务;而share cache就是为多个用户提供缓存服务的cache服务器,它可以在user-agent到origin server上的任何proxy上实现。

HTTP Cache的目的是降低WEB服务响应的时延,减少带宽消耗。它的基本工作逻辑是,保存上一次用户请求的结果到HTTP cache,以后有相同请求发往origin server时,则直接从cache中返回结果,比如响应消息直接从浏览器本地缓存中取,显然响应更快,而且没有带宽消耗。当然上述只是理想情况,具体到现实世界,还有很多因素需要考虑。

HTTP Caching原理图:

可缓存的响应
并不是所有响应都是可以缓存的,其中既有安全性的考虑,也有管理策略的需求,还有现实的实现情况。
安全性的考虑:对于HTTP请求中包含Authentication头字段的响应,或包含Set-Cookie头字段的响应,缺省不会保存在public cache中,因为它们包含个人信息。如果确实需要保存,需要显示的设置Cache-Control: public, s-maxage,或must-revalidate中的一个。
什么样的响应会被缓存呢?RFC7234上写的很复杂,一般理解为GET方法,状态码是200,206,301,404的,并在响应中指定了缓存的新鲜度的响应就可以被缓存。
什么样的响应不会被缓存?其它方法,除非构造了合适的cache key,并被cache理解和支持;明确设置了Cache-Control: no-store的也不会被缓存。

响应的新鲜度:
网络上的资源并不是一成不变的,并且变化频率也不一样,对于某些资源可能实时在变化,某些资源有固定的更新周期,而另外一些资源可能10年8年也不会改变。按照上面缓存的工作逻辑,只有第一次请求发送到了origin server,后继的请求/响应都直接由cache返回,显然不能满足需求。为了解决这个问题,HTTP cache中引入了新鲜度的概念,为不同更新频度的资源指定不同的过期时间,新鲜度从响应生成时刻算起Date,到指定的过期时间Expiration time。即fressless _lifetime = Expiration time - Date,【注:如果是s-maxage或max-age这样的相对时间,则它们的值就是fressless _lieftime】相当于是给响应贴了一个保质期的标签,资源的状态在保质期内的称为fresh,否则称为stale。Expiration time可以有几个来源,分别是s-maxage,max-age,Expires,优先级也是从高到低,其中s-maxage仅针对share cache。另外,响应可能保存在某个中间的proxy cache上,当用户发请求获取时,响应已经保存在proxy cache一段时间,HTTP Cache引入了一个头字段Age, 标记自响应产生以来,到proxy cache发送回应时,响应经历的时间。所以判断一个响应是否新鲜的公式是fressness_lifetime>current_age。current_age的计算有比较复杂的逻辑,可以参考RFC7234,此处略过。
注:还有一个所谓启发式的过期时间,即如果上述决定新鲜度的时间origin server都没有提供,cache可以自行依据Last-Modifed字段和Date,计算一个新鲜度时间,通常是(Date - Last_modified)/10。

控制缓存的头字段:
Expires 响应保质期的绝对时间
Cache-Control 目前控制缓存策略的主要头字段
其它,如Pragma, 等效于发送方向的Cache-Control: no-cache,浏览器中强制刷新时可以看到,纯粹是为了兼容HTTP/1.0,可理解为已经废弃。

Cache-Control头字段:
有3点需要说明:

  1. 对于缓存的控制是单向的,对一个方向施加的指令,并不代表对另外一个方向施加了相同的指令,尽管某些指令在发送和接收方同时存在,但它们表达的语义和作用是不同的。
  2. 指令的施加是对请求/应答链上所有的HTTP Cache施加的,并不能指定对某个特定的HTTP cache施加某个指令。
  3. 某些指令仅针对share cache生效,对于private cache则忽略。如s-maxage, proxy-revalidate。

接收方向的Cache-Control directive
max-age: 指定资源【即术语representations】的过期时间。
s-maxage: 指定资源的过期时间,仅针对share cache。
no-store: 指示cache不缓存
no-cache: 缓存,但使用前无论缓存是否fresh都需要去origin server验证。所以可以保证客服始终获取新鲜的资源。
must-revalidate: 可以保存响应,过期则需去origin server验证,所以must-revalidate通常和max-age一起使用,如Cache-Control: must-revalidate, max-age=3600。
private/public: 决定缓存保存的位置。private仅private cache可以保存,反之,public可以保存在private和public cache。
其它还有一些头字段相对没那么重要,就略过了。

发送方向的Cache-Control directive:
max-age: 不接受age>指定时间的缓存。【注1】
max-stale: 可以接收过期的资源stale,但不能超过指定的时间。【注2】
min-fresh: 仅接收新鲜度不小于指定时间的资源
no-restore: 通知cache不要保存请求和缓存响应,并应该删除已存在的缓存。
no-cache: 缓存使用前,需要去origin server验证。
注1,注2 在MDN的表述中,并不清晰,为此博主还给他提了1个PR,并在最近一次的更新修正了,你敢信。

验证和验证器
首先要澄清的是,验证应由origin server进行,而不能由cache进行。因为验证涉及2类验证器:Last-Modified和ETage,二者都是由origin server提供。
验证的逻辑是,如果cache上的资源已经过期,当收到客户端请求,则由cache发请求询问origin server,询问是否资源已经改变,是则重发新响应返回200,否则继续使用缓存,返回304,并更新age。
如何判断资源是否改变?依据是资源的某种标识,就是上文说到的两种验证器:Last-Modified和ETag。
验证流程是:

  1. origin server回应响应时携带了验证器。
  2. 需要验证时,如资源已经过期,或指定缓存指令是Cache-Control: no-cache,强制要求验证,则请求中通过条件请求字段:If-Modified-Since或If-None-Match携带1中的验证器。
  3. 验证通过则返回304,不返回包体,并更新age,节省了带宽,否则返回完整的包体和200。

HTTP cache验证原理图:

Vary和cache
cache是一个本地存储,同时提供了机制检索缓存,检索使用的是URI+请求方式,或省略为URI,称为cache key,或primary cache key。在有内容协商的情况下,单用URL并不能区分缓存。此时通过Vary字段标记,哪些字段会影响到缓存,把Vary字段标记的请求字段作为缓存的secordary key[注3]
注3:MDN举例的图中这块错了,其中Content-Encoding应该是Accept-Encoding,博主指出,并被确认,但现在还没改过来。

Varying response原理图

网页开发的Revved策略
前面说过,对于某些资源,如CSS,JS可能10年8年不会变,我们可以为它指定一个超长的过期时间。但万一这个拥有超长缓存时间的资源发生了变化怎么办?此时可以从资源命名着手,采取所谓cache busting的技术,给文件名添加版本号hash值的方式命名,然后给引用它们的HTML文件设置一个较短的缓存时间,这样就可以解决静态文件的更新问题。

Nginx中的对于Cache-Control和Expires的控制:
Nginx中的ngx_http_headers_module,可以控制返回的Expires和Cache-Control,下面是官方给出的例子

expires    24h;
expires    modified +24h;
expires    @24h;
expires    0;
expires    -1;
expires    epoch;
expires    $expires;
add_header Cache-Control private;

用REDbot解读某些网站的Cache设置:
REDbot是一个开源工具,可以用来解读网站返回的HTTP信息。比如下面是解读百度首页的信息,我们重点关注Cache相关的信息。


运用我们所学的知识,可以对百度首页Cache相关的设置做一个解读:
Cache-Control: private #表示不允许share cache存储该页面。
Date和Expires,可以看到Expires的时间早于Date,即这个页面创建时就已经stale了,不能被缓存使用。
Vary: Accept-Encoding #相当于指定了Cache key的secondary key为Accept-Encoding,表达缓存可以按压缩协商结果的不同,分别存储。
上述百度首页的缓存设置逻辑是:“我是百度的首页,页面内容会实时变化的,所以不要试图缓存我。”

这个截图是针对百度首页调用的JS文件的分析


我们来分析一下它的Cache相关信息
文件名:jquery-1-edb203c114.10.2.js #显然是带版本号的,使用了cache busting的命名方式。
Cache-Control: max-age #设置了一个很大的值,大约可以在cache中保存一个月。
Age: 记录了它在proxy cache上存在的时间。
Last-Modified和Etag:是两个validator。
其中没有关于public/private的设置,REDbot提示说可以被所有cache缓存,看来public是缺省值。

这个文件的缓存信息表达的语义是:我是一个JS静态文件,可以在cache中保存很长时间,需要验证是否更新,可以携带Validator。

结合上面的HTML设置,符合Revved的缓存更新逻辑,你学废了吗?

MDN上关于HTTP Caching的几个小问题:

实践和验证:
为进一步验证上述HTTP caching相关的理论知识,博主搭建了一个真实的实验环境如下:

工具:
user-agent: curl 7.6.1
origin server: nginx 1.20.1
proxy cache: openresty 1.19.9.1

拓扑:
client — proxy cache — origin server

由Nginx充当origin server, 有关HTTP caching的头字段在此设置,核心就是前面提到过的add_header和expires指令。

origin server 配置:

    server {listen  127.0.0.1:8080;access_log logs/103_access.log main;error_log logs/103_error.log debug;#注:同时设置Cache-Control,X-Accel-Expires,X-Accel-Expires优先级高#add_header Cache-Control 'max-age=30,stale-while-revalidate=3';#缓存30秒#add_header Vary *;#有Vary *,不缓存#add_header X-Accel-Expires 180;#通知代理缓存保存时间,尽管HTTP返回消息头部看不到该设置,由于它优先级高,设置以它为准#expires 120s;#expires的优先级高于proxy_cache_valid中设定的时间                                                      #add_header Cache-Control no-store;#add_header Cache-Control no-cache;add_header Cache-Control 'max-age=120,must-revalidate';#add_header Cache-Control 'max-age=120,proxy-revalidate';#add_header Cache-Control private;#add_header Cache-Control public;location / {}}Openresty充当proxy cache, 开启缓存功能,核心是添加X-Cache-Status和打开revalidation.

proxy cache配置:

proxy_cache_path 103_proxy_cache levels=2:2 keys_zone=103_proxy_cache:100m max_size=200m inactive=5m loader_threshold=300 loader_files=200;server {listen 80;server_name test;access_log logs/103_access.log main;error_log logs/103_error.log debug;location ~ /purge(/.*) {proxy_cache_purge 103_proxy_cache $scheme$1;}location / {proxy_cache 103_proxy_cache;proxy_cache_valid 200 1m;#优先级低于Expires                                                                                add_header X-Cache-Status $upstream_cache_status;proxy_cache_key $scheme$uri;proxy_http_version 1.1;proxy_cache_revalidate on;proxy_pass http://127.0.0.1:8080;}   }

测试方法:用curl 发起请求,观察返回结果和相关日志。

以下以验证must-revalidate为例,分析测试的结果和日志
关键设置:
(1)在origin server中
add_header Cache-Control ‘max-age=120,must-revalidate’;
即缓存120秒,过期需要回源验证。
(2)在proxy cache中
proxy_cache_revalidate on;
注:缺省proxy_cache_revalidate off,需要显示设置为on, 否则不会向origin server 发条件请求。
辅助设置:
add_header X-Cache-Status #返回缓存的状态,包括MISS,HIT,EXPIRED,REVALIDATED等。

执行4次curl test -I,由于缓存120s过期,所以2-3次之间间隔2分钟,捕捉到缓存过期状态,又因为设置了proxy_cache_revalidate on,所以不会显示EXPIRED,直接显示REVALIDATED

[root@test01 ~]# curl test -I
HTTP/1.1 200 OK
Server: openresty/1.19.9.1
Date: Thu, 31 Mar 2022 04:12:24 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 620
Connection: keep-alive
Last-Modified: Wed, 23 Jun 2021 06:31:52 GMT
ETag: "60d2d558-26c"
Cache-Control: max-age=120,must-revalidate
X-Cache-Status: MISS
Accept-Ranges: bytes[root@test01 ~]# curl test -I
HTTP/1.1 200 OK
Server: openresty/1.19.9.1
Date: Thu, 31 Mar 2022 04:12:27 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 620
Connection: keep-alive
Last-Modified: Wed, 23 Jun 2021 06:31:52 GMT
ETag: "60d2d558-26c"
Cache-Control: max-age=120,must-revalidate
X-Cache-Status: HIT
Accept-Ranges: bytes[root@test01 ~]# curl test -I
HTTP/1.1 200 OK
Server: openresty/1.19.9.1
Date: Thu, 31 Mar 2022 04:14:33 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 620
Connection: keep-alive
Last-Modified: Wed, 23 Jun 2021 06:31:52 GMT
ETag: "60d2d558-26c"
Cache-Control: max-age=120,must-revalidate
X-Cache-Status: REVALIDATED
Accept-Ranges: bytes[root@test01 ~]# curl test -I
HTTP/1.1 200 OK
Server: openresty/1.19.9.1
Date: Thu, 31 Mar 2022 04:14:36 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 620
Connection: keep-alive
Last-Modified: Wed, 23 Jun 2021 06:31:52 GMT
ETag: "60d2d558-26c"
Cache-Control: max-age=120,must-revalidate
X-Cache-Status: HIT
Accept-Ranges: bytes

日志文件

proxy-cache
192.168.31.133 - - [31/Mar/2022:12:12:24 +0800] "HEAD / HTTP/1.1" 200 0 "-" "curl/7.61.1" "-"
192.168.31.133 - - [31/Mar/2022:12:12:27 +0800] "HEAD / HTTP/1.1" 200 0 "-" "curl/7.61.1" "-"
192.168.31.133 - - [31/Mar/2022:12:14:33 +0800] "HEAD / HTTP/1.1" 200 0 "-" "curl/7.61.1" "-"
192.168.31.133 - - [31/Mar/2022:12:14:36 +0800] "HEAD / HTTP/1.1" 200 0 "-" "curl/7.61.1" "-"origin-cache
127.0.0.1 - - [31/Mar/2022:12:12:24 +0800] "GET / HTTP/1.1" 200 620 "-" "curl/7.61.1" "-"
127.0.0.1 - - [31/Mar/2022:12:14:33 +0800] "GET / HTTP/1.1" 304 0 "-" "curl/7.61.1" "-"

结果分析和解读:
一共执行4次请求,
从curl返回结果看,
X-Cache-Status的状态是MISS,HIT,REVALIDATED,HIT,符合预期
从log记录看:
proxy cache 4次HEAD请求都收到并响应
origin server 响应2次,分别是第一和第三次,符合预期。
第一次没有缓存,所以origin server有回应。
第二次缓存命中,请求被proxy cache截获,未发送到origin server。
第三次因为验证缓存所以要回源验证缓存,所以请求打到origin server。
第四次命中缓存,请求被proxy cache截获,未发送到origin server。

origin server收到的proxy cache发送的条件请求日志:

2022/03/31 12:14:33 [debug] 19220#0: *2 http process request header line
2022/03/31 12:14:33 [debug] 19220#0: *2 http header: "Host: 127.0.0.1:8080"
2022/03/31 12:14:33 [debug] 19220#0: *2 http header: "Connection: close"
2022/03/31 12:14:33 [debug] 19220#0: *2 http header: "If-Modified-Since: Wed, 23 Jun 2021 06:31:52 GMT"
2022/03/31 12:14:33 [debug] 19220#0: *2 http header: "If-None-Match: "60d2d558-26c""
2022/03/31 12:14:33 [debug] 19220#0: *2 http header: "User-Agent: curl/7.61.1"
2022/03/31 12:14:33 [debug] 19220#0: *2 http header: "Accept: */*"
2022/03/31 12:14:33 [debug] 19220#0: *2 http header done

至此验证了must-revalidate的行为,即缓存过期时回源验证,否则直接用缓存。

其它cache相关的头字段,验证方式类似,不再赘述。

几点未解决的问题:
(1)测试Cache-Control: no-cache时,效果等同于Cache-Control: no-store,不知何故?—一个猜想如果expires设置为负数,即缓存过期,不缓存,Nginx会同时添加Cache-Control: no-cache,貌似Nginx中no-cache就是no-store的意思,和Nginx的实现有关。
(2)不知如何添加动态的Age头字段,add_header可以添加Age,但是静态的。
希望有高人指点~~~

BTW: Nginx对于HTTP caching有大量的可配置参数,需要另外研读。

学习HTTP Caching的常见误区:
(1)5个条件请求都可以用于HTTP Caching。其实只有If-None-Match和If-Modified-Since和HTTP Caching有关。其它3个条件请求针对其它的应用场景,不能想当然。关于HTTP 的条件请求可以参考一文读懂4个HTTP条件请求头部
(2)缓存是否过期是max-age倒计时到0,其实不是,是Age的值在增长,当max-age >= Age时,缓存是fresh的,没有age_value,即上游获取的age值为0,current_age是currected_initial_age+resident time,要计算驻留时间。

补充:
current_age的计算
所谓current_age是指从当前cache发送响应时Age字段的值,而计算中用到的age_value是从上游cache获得的current_age。Age的值本质是HTTP消息传输路径上消耗的传输时间+路径上各个cache累积的驻留时间。

一点题外话:
博主学习HTTP Caching过程中,阅读了若干资料,说点体会:
RFC是最权威的,错误最少的,你敢质疑RFC,你飘了啊~~~但学习曲线最陡,比如某些细节和实现有关,它说得很模糊【比如启发式过期时间】,某些细节又考虑得过于细致,实现不一定实现【比如如果回应时碎片时,怎么合并,怎么缓存】,适合有一定基础的同学看。
MDN比较亲民,基本说的人话,但居然错误不少~,其实都是小错误,不存在概念性的重大错误,还是要相信MDN专家的水平。
网上的教程,多数太浅显,碎片化,错误也多,优点是先入个门,普及个概念,建立基本的知识体系,建议先看,再和MDN印证。
最后,是实践,搭建环境,上手实操。实践实践再实践,是学好一切计算机技术的终极奥义。

参考:
RFC7234
MDN
Nginx HTTP Proxy

一文读懂HTTP Caching相关推荐

  1. 从实验室走向大众,一文读懂Nanopore测序技术的发展及应用

    关键词/Nanopore测序技术    文/基因慧 随着基因测序技术不断突破,二代测序的发展也将基因检测成本大幅降低.理想的测序方法,是对原始DNA模板进行直接.准确的测序,消除PCR扩增带来的偏差, ...

  2. 一文读懂Faster RCNN

    来源:信息网络工程研究中心本文约7500字,建议阅读10+分钟 本文从四个切入点为你介绍Faster R-CNN网络. 经过R-CNN和Fast RCNN的积淀,Ross B. Girshick在20 ...

  3. 福利 | 一文读懂系列文章精选集发布啦!

    大数据时代已经悄然到来,越来越多的人希望学习一定的数据思维和技能来武装自己,虽然各种介绍大数据技术的文章每天都扑面而来,但纷繁又零散的知识常常让我们不知该从何入手:同时,为了感谢和回馈读者朋友对数据派 ...

  4. ​一文读懂EfficientDet

    一文读懂EfficientDet. 今年年初Google Brain团队在 CVPR 2020 上发布了 EfficientDet目标检测模型, EfficientDet是一系列可扩展的高效的目标检测 ...

  5. 一文读懂序列建模(deeplearning.ai)之序列模型与注意力机制

    https://www.toutiao.com/a6663809864260649485/ 作者:Pulkit Sharma,2019年1月21日 翻译:陈之炎 校对:丁楠雅 本文约11000字,建议 ...

  6. AI洞观 | 一文读懂英特尔的AI之路

    AI洞观 | 一文读懂英特尔的AI之路 https://mp.weixin.qq.com/s/E9NqeywzQ4H2XCFFOFcKXw 11月13日-14日,英特尔人工智能大会(AIDC)在北京召 ...

  7. 一文读懂机器学习中的模型偏差

    一文读懂机器学习中的模型偏差 http://blog.sina.com.cn/s/blog_cfa68e330102yz2c.html 在人工智能(AI)和机器学习(ML)领域,将预测模型参与决策过程 ...

  8. 一文读懂AI简史:当年各国烧钱许下的愿,有些至今仍未实现

    一文读懂AI简史:当年各国烧钱许下的愿,有些至今仍未实现 导读:近日,马云.马化腾.李彦宏等互联网大佬纷纷亮相2018世界人工智能大会,并登台演讲.关于人工智能的现状与未来,他们提出了各自的观点,也引 ...

  9. 一文读懂你该了解的5G知识:现在别买5G手机

    来源: 腾讯科技 2019年是中国全力布局5G的一年:三大运营商纷纷搭建基站,手机厂商发布5G手机,部分城市已经开启了5G测试--在电信日这天,腾讯科技联合知乎推出重磅策划,聚焦和5G相关的小知识,精 ...

最新文章

  1. 开发商微信选房后不退认筹金_网曝!青岛恒大文化旅游城1400余名购房者欲退认筹金,开发商表示.........
  2. Adobe Flash Player(Flash播放器)下载地址
  3. android显示绘图动画,Android自定义View绘图实现渐隐动画
  4. Spring Boot 2.x基础教程:配置文件详解
  5. Re-attention机制Transformer,实现强大性能
  6. php 租房子(练习题)
  7. Linux三种修改打开文件数量限制的方法
  8. Go语言的复合数据类型struct,array,slice,map
  9. Linux怎么修改用户密码
  10. 数学 计算机类论文题目,数学计算机论文题目范文 数学计算机论文标题如何定...
  11. Adobe Premiere基础-声音调整(音量矫正,降噪,电话音,音高换挡器,参数均衡器)(十八)
  12. Android 获取手机号码
  13. Android 长按3Dtouch快捷方式
  14. oracle11g_R2 exp imp 用法
  15. 海南信用社计算机试题,2015年海南农村信用社考试试题——计算机基础知识一...
  16. GTX1650Super和GTX1060哪个好?
  17. 需求文档(PRD文档)应该怎么写?
  18. 完全教程 Aircrack-ng破解WEP、WPA-PSK加密利器 [MARK]
  19. 手机无线网络 dns服务器设置,iPhone手机网速慢?1分钟教你设置DNS,网速立马翻一番...
  20. 学python怎么样

热门文章

  1. Ajax 服务器软件安装、以及Ajax介绍
  2. 为什么美团点评系统架构这么牛?
  3. 印度最大本土电商卖身给沃尔玛,阿里为何没有行动?
  4. 推荐 10 个 GitHub 上最火的程序员简历项目,少说加薪 3K 的简历技巧!
  5. SD-WAN架构的主要因素:优势和选择
  6. 联想a668t各种root方法汇集
  7. [C#] StringBuilder简介及使用方法
  8. SEM推广营销远比你想象得更丰富
  9. PHP——PHP的数据类型
  10. CEF3 添加mp4播放功能