0、前言
本系列从这一篇开始,将涉及到具体的配置项,配置页面各平台不同但是大同小异,可以根据配置项名称找到对应的配置页面。
关于缓存,先明确定义:
Web 缓存(或 HTTP 缓存)是用于临时存储(缓存)Web 文档(如 HTML 页面和图像),以减少服务器延迟的一种信息技术。
Web 缓存系统会保存下通过这套系统的文档的副本;如果满足某些条件,则可以由缓存满足后续请求。
Web 缓存系统既可以指设备,也可以指计算机程序。
缓存可以指服务端缓存,即在 CDN 或 反向代理 中,通过用户主动配置或使用默认配置,存储在 CDN 节点中的缓存;也可以指客户端缓存,即在一定时间内缓存在客户端,不再向服务器请求的缓存。
网站粗略分为动态网站、动静网站和静态网站,这不一定是最佳分类,并且最佳实践应该是动态内容原则上始终不应被缓存,静态内容应使用单独的域名配置缓存规则。
1、客户端缓存
以 Nginx 为例,如果使用location
字段,可以这样写:
1 | location ~* \.(css|js|map|scss)$ { expires 1d; } location ~* \.(avif|bmp|gif|ico|jpeg|jpg|pjpeg|png|svg|swf|tiff|webp)$ { expires 7d; } location ~* \.(oft|ttc|ttf|woff|woff2)$ { expires 30d; } |
如果同时使用了伪静态或者反向代理,location
字段会冲突导致这些文件无法命中其它规则,因此可以改为使用if
条件:
1 | # 伪静态规则或者反向代理规则 location / { # 省略规则内容 ... if ( $uri ~* "\.(css|js|map|scss)$" ) { expires 1d; } if ( $uri ~* "\.(avif|bmp|gif|ico|jpeg|jpg|pjpeg|png|svg|swf|tiff|webp)$" ) { expires 7d; } if ( $uri ~* "\.(oft|ttc|ttf|woff|woff2)$" ) { expires 30d; } } |
这是我推荐的缓存规则:缓存时间 前端文件 > 图片 > 字体。这正是这些文件更新频率的反向排列:前端文件可能时常要微调(不会只有我有前端强迫症,看到布局没对齐之类的就会想去调它吧),图片一般不会再变动但是图标、头像可能会更改,字体文件一般而言永远都不会被改动。
另外,如果文件名带有哈希,例如:
/assets/style.6f4e6432.css
/assets/app.ee143e90.js
/assets/inter-roman-latin.2ed14f66.woff2
那么一般而言,可以大胆地把这些文件缓存非常久的时间,例如1145141919810
秒,因为这些文件在更新后文件名会替换为新的哈希值,不会命中之前的缓存。清北 CDN 正是采用此方案,每次更新 VitePress 文档后只需把页面缓存 Purge
掉,拉取的就是最新的静态资源。
然而,客户端缓存是不靠谱的,只需按下Ctrl+F5就会停用当前页面的缓存并刷新页面,各种XX卫士
、XX清理大师
也总是迫不及待地帮用户清理缓存文件:
您的上网垃圾高达 114514.1919810 Bytes!建议您立刻清理!
因此,我们同时需要使用服务端缓存,确保用户获取需要缓存的文件时随时能命中缓存。
2、服务端缓存
2.1、动态网站
动态内容(不仅是 PHP、ASP 等,也可以是 HTML 等页面文件)理论上不应被缓存,但是如果你需要缓存,可以这样配置(以 Nginx 为例):
1 | location / { # 其它内容省略 ... proxy_ignore_headers Set-Cookie Cache-Control expires; proxy_cache cache_one; proxy_cache_key $host$uri$is_args$args; proxy_cache_valid 200 304 301 302 1d; # 缓存 1 天 } |
缓存时间单位可以是m
、h
、d
或y
。
在 Cloudflare 中,Cloudflare 会自动判断一个文件是否需要缓存,如果我们需要缓存一些默认不会被缓存的文件,例如 HTML 文件,就需要在页面规则中设置“如果匹配规则……就缓存所有文件”:
然而,这样添加完后,缓存状态仍然是DYNAMIC
,即动态内容不缓存。原因是及时开启了缓存所有,如果源站不发送Cache-control
头部,Cloudflare 仍然不会缓存该文件。因此只需在源站添加一行:
1 | location ~* \.(html)$ { expires 1h; add_header Cache-Control "public, max-age=3600"; } |
即可。
2.2、动静网站
其实动静加速和动态加速几乎相同,只是前者还需要配置好什么文件命中什么规则。在这种情况下,如果 CDN 不会像 Cloudflare 一样自动判断源站是否有Cache-control
头部,缓存一切的规则是错误的,一旦在 CDN 中以高优先级启用此规则,就会导致其他规则被覆盖、所有文件都被缓存,网站更新后不会生效(甚至有人缓存没有忽略后台目录,导致访客无需登录就可以看到后台界面 XD)。
我的建议是把静态资源的域名分离,这样就可以把动静网站拆分为动态网站和静态网站单独配置,省去了很多麻烦。
2.3、静态网站
如果你使用了我上文提到的策略,那么可以把静态资源都放在同一个域名下,减少 DNS Lookup
的延迟,但是这样又会导致触发浏览器限制,这种情况也有对应的解决方法:
HTTP/1.1 的规范是 RFC2616,规定了对于同一个域名只允许同时存在 2 个连接。由于这个规定看起来就不合理、以至于后来所有的浏览器实现都无视了这一限制,最终在 RFC7230 中将这一限制去除。但是浏览器为了保证公平,每个域名下最多也只允许同时存在 6 个连接。
如果要同步下载数十甚至数百个文件(这不稀奇,下次打开 GitHub 时可以用 DevTools 看一下,HTTP 请求数量不会低于 50),压榨浏览器最常用的方法就是域名散列。
GitHub 的avatar{0..3}.githubusercontent.com
、Bilibili 的i{0..3}.hdslb.com
、京东的img{1..30}.360buyimg.com
都是在做这件事情。但是域名散列又会带来如下的问题:
- 建立 HTTP 连接的开销是巨大的 —— 每一个域名都需要经过一次 DNS 查询、TCP 三次握手、TLS 还要一次。
- 每个 HTTP/1.1 连接都要包含相同的信息比如 User-Agent,而对于非 Cookie-Free 域名还要在请求头中包含Cookie,这造成了流量浪费。
- 更多的并发连接和 Keep-Alive 长连接造成客户端和服务端的性能负担。
- 如果同一个资源在不同页面下被散列在不同的域名下,那么就没法有效利用 HTTP 缓存。
总之以本站为例,引用的前端资源只分布在两个域名下:
cdn.tsinbei.com
cdn.staticfile.org
引用 2~3 个域名的资源,DNS Lookup
的开销都在可接受的范围内。
其他缓存配置文件同上,只需修改location
或if
的字段即可。
3、后记
关于减轻源站压力,许多人有误区:
“只要使用 CDN 就可以减轻源站压力了吧!”
然而,如果网站是动态内容,使用 CDN 仍然需要回源,并不能起到降低源站负载的作用,正确的解决方法是集群化分布式部署,或者直接升级源站服务器。
如果是静态资源,那么缓存和 CDN 就能大展身手,起到降低源站用于传输静态资源的带宽、并发的压力的作用,一次缓存,在缓存过期前都通过 CDN 分发,带宽、并发瓶颈不再是源站而是 CDN。另外,使用对象存储也是不错的选择。
正确配置缓存,即使源站是1Mbps
的小水管也可以变成1Gbps
的大水泵。
CDN 调教指南(四)缓存控制
评论