缓存

通过JavaScript等方式显示动态页面, 这是现代的主流做法.
SSI指令是一种在HTML注释里设置的服务器脚本语言,
作为CGI/动态网页生成技术的补充, 非常古老, 可以追溯至1990年代.
SSI使用 .shtml 扩展名.
SSI被主流Web服务器支持, Web服务器在读取到HTML页面里的SSI指令时,
会将SSI指令替换为它代表的内容.
SSI已经过时, 因为现代技术更成熟.
基本上可以视作SSI的现代版本: 不使用注释指令, 而是直接嵌入XML.
ESI在2001年提交给W3C, 但从未成为正式标准.
Varnish实现了ESI的子集, 可将其用作缓存内容的组装.
https://datatracker.ietf.org/doc/html/rfc5861
访问没有被缓存的资源时, 用户需要等待内容生成, 直到缓存成功, 至此跟普通的缓存策略一致.
当缓存过期时, 会在一定宽限期内立即返回旧缓存, 在后台刷新缓存(revalidate).
如果刷新缓存总是失败或刷新的时间过长, 导致缓存没有被及时更新, 则缓存会在设定的宽限期之后被删除,
下一次访问该资源时会恢复到第一次访问的情况.
除非将宽限期设置得很长, 否则SWR对那些被访问的频率较低的资源没有什么帮助, 因为缓存总是会超过宽限期,
然后被删除, 进而导致大多数访问都需要等待内容生成.
访问没有被缓存的资源时, 用户需要等待内容生成, 直到缓存成功, 至此跟普通的缓存策略一致.
当缓存过期时, 用户需要等待内容生成, 当生成的新内容是错误时, 会在一定宽限期内用旧缓存作为替代.
如果刷新缓存总是失败, 则缓存会在宽限期之后被删除, 下一次访问该资源时会恢复到第一次访问时的情况.
由于它的行为与SWR冲突, 所以在一同使用时, SIE的行为只会发生在SWR的宽限期以后(参考Fastly的处理方式).
SWR和SIE同样具有在刷新缓存失败的情况下返回旧缓存(stale)的行为.
二者在缓存过期后的缓存刷新(revalidate)行为上有根本区别: SWR的缓存刷新是异步的, 而SIE是同步的.
缓存清除是提前使缓存失效的机制, 它通常是一种主动调用的接口.
由于缓存层往往位于应用层之上, 在应用层里嵌入主动清除缓存的代码会制造对缓存层的依赖, 导致不良的设计.
正确的做法是基于事件驱动, 将"缓存清除"作为一个事件广播出去, 由与缓存层相关的第三方接受事件, 从而间接调用缓存层的清除接口.
Chrome有两种缓存, 内存缓存和磁盘缓存, Chrome使用哪种缓存是由它自己决定的, 不受HTTP头的影响.
由于已经基本不存在只支持HTTP/1.0的程序, 此项可以被忽略
该header控制缓存策略, 是最常用和最普遍的缓存控制手段.
CDN服务商, 缓存服务器, 浏览器都可能根据Cache-Control的值决定自身的缓存行为.
该头大多数情况下是响应头, 在不提供此头的情况下, 客户端会采取它自己默认的缓存策略.
当作为请求头使用时, 最常见的用法是使用 no-store 指令强制向服务器发送请求,
它同时也是fetch API的cache选项候选值.
Cache-Control可以同时使用多个指令, 像 no-store 这样的指令在优先级上高于其他指令.
参考: https://csswizardry.com/2019/03/cache-control-for-civilians/
禁用缓存.
最佳实践是 对所有不应该被缓存的资源设置此值.
出于兼容性目的, 可以设置为以下值:
private, no-cache, no-store, max-age=0, must-revalidate
该资源可以被任何中间人缓存, 因为资源内容是公开的.
该指令的名称具有误导性.
该资源 可以 被中间人缓存, 但是缓存在使用前必须发送请求到服务器进行验证.
与无缓存的区别在于如果验证成功, 就不会下载资源正文.
该指令等同于 max-age=0, must-revalidate.
最适合作为Cache-Control的默认指令.
该资源只能由带有私密性的中间人缓存, 例如浏览器.
该资源的带有按秒计的缓存, 它的优先级比 Expires 头要高.
max-ageExpires 的优先级高, 但只对公共缓存(中间人)有效.
该指令的名称具有误导性.
一旦资源过期, 则需要重新验证.
该指令应该与非零的 max-age 一同使用.
实验性指令, 采取先返回过期缓存, 然后在后台更新缓存的策略.
该指令的秒数是此行为(返回过期缓存)的宽限期(可以被容忍的时间).
适合可以容忍过期内容的资源.
建议配合 max-agemust-revalidate 使用.
实验性指令, 如果重新验证失败, 则使用过期的缓存.
该指令的秒数是此行为(返回过期缓存)的宽限期(可以被容忍的时间).
由于该指令与stale-while-revalidate的行为冲突, 所以在一同使用时, stale-if-error的行为只会发生在stale-while-revalidate的宽限期以后.
请求的是 不可变资源, 因此不会过期, 客户端不需要进行重新验证.
一些客户端不支持此指令, 因此应该配合一个超长的 max-age 使用,
例如 public, max-age=31536000, immutable.
对于那些会通过改变文件名或查询字符串来实现强制更新的静态资源(例如样式表和脚本), 也十分推荐使用此指令.
该指令要求中间人不得转换响应内容(例如优化/压缩图片).
在使用HTTPS时, 此指令是无意义的.
重新验证是一种缓存刷新行为:
客户端请求一个已经过期的本地缓存时, 会为相关的请求加上验证头发送到服务器, 以确定缓存的资源是否过期.
服务器如果返回304(不带有正文), 则客户端会继续使用本地缓存.
If-None-Match 与If-Match相反, 仅当ETag不符合时返回信息, 否则返回304
If-Modified-Since 用于为服务器提供时间以确定是否返回新信息, 仅当有修改时返回信息, 否则返回304
  • If-Match: 用于为服务器提供ETag以确定是否返回新信息, 仅当ETag符合时返回新信息, 否则返回412
  • If-Unmodified-Since: 与 If-Modified-Since 相反, 仅当不修改时返回新信息, 否则返回412
当服务器的响应不包含过期时间(Cache-Control: max-ageExpires)时,
客户端会采取启发式方法根据 Last-Moidfied 来推算出一个过期时间.
如需避免客户端使用启发式方法, 则应该通过相关标头设置一个过期时间.
缓存破坏可以通过两种形式实现:
  • 改变资源的路径/文件名(推荐)
  • 改变资源的查询字符串
更推荐改变资源的路径/文件名是因为查询字符串可能被一些中间人标准化为移除了查询字符串的路径.
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Clear-Site-Data
为使客户端数据失效而专门设计的HTTP头, 可以用来清除缓存.
除了 "cache" 以外还支持 "cookies", "storage", "executionContexts", "*" 等值.
  • ETag 资源的唯一性标识
  • Expires 告知资源的过期时间
  • Last-Modified 告知资源的上次修改时间
告知相关服务器资源已经经过的生存时间, 通常由缓存服务器发出,
例如CDN服务器, 意为该资源已经在此服务器上缓存了这么长时间.
配合Cache-Control或Expires, 可以将max-age减去此Age值, 得到此资源在缓存服务器上的过期时间,
从而让客户端能够在正确的时间使本地缓存过期.
换言之, 如果没有Age, 则会出现以下现象:
客户端向服务器请求资源, 服务器已经缓存了从上游得到的资源, 该资源的max-age值为100秒,
已经在此缓存服务器上存活30秒.
在服务器不发送Age的情况下, 服务器如果将资源连带max-age原样发送给客户端,
则客户端会错误的认为该资源在100秒后才会过期, 而实际上70秒后就会过期.
HTTP缓存的最佳实践是尽可能为每个资源设置准确的Cache-Control.
为每个HTTP资源在 Surrogate-Key 响应头里声明一个或多个key.
在清除缓存时, 指定需要清除的key, 涉及相关key的HTTP资源的缓存会被清除.
这种做法的好处是清除缓存的一方不需要关注具体的URL, 只需要关注key就行了.
此外, 由key对HTTP资源进行标记也有很大的灵活性, 因为key与HTTP资源之间形成的是多对多关系.
参考: https://docs.fastly.com/en/guides/getting-started-with-surrogate-keys
Varnish专注于提供缓存功能, 它不是一个负载均衡软件, 不支持SSL, 通常需要与其他负载均衡软件配合使用.
Varnish具有被称为VCL的具有很强表达力的配置语言, 由于VCL语言会被编译, 它也具有极高的性能.
支持ESI, SWR.
提供基于Surrogate Keys的缓存失效机制.
Varnish有名为Varnish Cache Plus的付费版本.
CDN服务商Fastly使用Varnish作为缓存核心, 所以Fastly也支持VCL和基于Surrogate Key的缓存失效.
VCL是一种DSL.
编写VCL代码本质上是在定义对Varnish缓存处理过程(有限状态机)中各个环节(子例程)的钩子.
参考: https://book.varnish-software.com/4.0/chapters/VCL_Basics.html
多项不同来源的基准测试表明, Varnish的性能比Nginx稍差.
实际进行压力测试后发现, 默认配置的Varnish(v7.0)和简单配置过的Nginx(v1.21)在不同的负载下互有胜负,
但在精心配置的Nginx面前缺乏竞争力.
在高并发场景下, Varnish需要的计算资源也远远大于Nginx.
对过期内容进行异步验证, 发生在TTL到期之后.
对过期内容进行同步验证, 发生在Grace到期之后.
缓存src的内容并展示它.
<esi:include src="inc/sidebar.php"/>
作为 esi:include 的回退使用, 在Varnish未配置ESI的情况下, 会显示其中的内容.
<esi:include src="inc/sidebar.php"/>
<esi:remove>
<?php include('inc/sidebar.php'); ?>
<p>Not ESI!</p>
</esi:remove>
缓存是Nginx的功能之一.
支持SWR.
Nginx相比Varnish缺少很多功能, 但性能表现较好, 也不需要使用者专门学习VCL语言, 适用于简单的缓存需求.
参考: https://docs.nginx.com/nginx/admin-guide/content-cache/content-caching/
通过向URL发送HTTP PURGE方法, 可以清除对应URL的缓存.
map $request_method $purge_method {
PURGE 1;
default 0;
}
location / {
proxy_cache_key $my_cache_key;
proxy_cache_purge $purge_method;
}
这要求:
  • URL对申请缓存清除的一方来说是可知的.
  • PURGE方法需要有ACL, 以避免受外部攻击清除缓存.
nuster是基于HAProxy的高性能缓存服务器,
官方的基准测试表明它在多核心服务器上的性能是Nginx和Varnish的数倍.
支持SWR.
不推荐使用它的原因在于, nuster并未在足够多的生产环境中使用, 不应该贸然使用它.
原因:
查询的目标数据同时不存在于Redis和数据库里.
先查询Redis, 发现找不到目标数据, 再查询数据库, 发现也找不到目标数据.
这种不存在的目标数据导致大量的数据库查询请求, 进而增加了数据库的运行压力.
应对方法:
  1. 1.
    想办法提前预知该数据是否在数据库里存在.
    例如使用自增id, 那么就可以过滤掉最大id以外的查询
    (仍然有弱点, 因为可以利用已被删除的旧id进行攻击).
  2. 2.
    将全部的数据库记录id作为Set保存进Redis.
  3. 3.
    对客户端IP进行访问限制, 进而增加攻击成本.
原因:
被频繁访问的缓存数据到期, 导致大量数据库查询请求.
应对方法:
  1. 1.
    取消目标数据在Redis里的生存时间, 一种类似的做法是使用stale-while-revalidate机制.
  2. 2.
    当查询不到目标数据时, 使用编程语言的锁机制, 限制访问数据库的请求数量.
原因:
大量缓存同时到期, 导致大量数据库查询请求.
应对方法:
  1. 1.
    为目标数据在Redis里设置随机的生存时间.