CDN 调教指南(四)缓存控制

0、前言

本系列从这一篇开始,将涉及到具体的配置项,配置页面各平台不同但是大同小异,可以根据配置项名称找到对应的配置页面。

关于缓存,先明确定义:

Web 缓存(或 HTTP 缓存)是用于临时存储(缓存)Web 文档(如 HTML 页面和图像),以减少服务器延迟的一种信息技术。
Web 缓存系统会保存下通过这套系统的文档的副本;如果满足某些条件,则可以由缓存满足后续请求。
Web 缓存系统既可以指设备,也可以指计算机程序。

缓存可以指服务端缓存,即在 CDN 或 反向代理 中,通过用户主动配置或使用默认配置,存储在 CDN 节点中的缓存;也可以指客户端缓存,即在一定时间内缓存在客户端,不再向服务器请求的缓存。

网站粗略分为动态网站动静网站静态网站,这不一定是最佳分类,并且最佳实践应该是动态内容原则上始终不应被缓存,静态内容应使用单独的域名配置缓存规则

1、客户端缓存

以 Nginx 为例,如果使用location字段,可以这样写:

Nginx
1
2
3
4
5
6
7
8
9
10
11
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条件:

Nginx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 伪静态规则或者反向代理规则
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 为例):

Nginx
1
2
3
4
5
6
7
8
9
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 天
}

缓存时间单位可以是mhdy

在 Cloudflare 中,Cloudflare 会自动判断一个文件是否需要缓存,如果我们需要缓存一些默认不会被缓存的文件,例如 HTML 文件,就需要在页面规则中设置“如果匹配规则……就缓存所有文件”:

页面规则

然而,这样添加完后,缓存状态仍然是DYNAMIC,即动态内容不缓存。原因是及时开启了缓存所有,如果源站不发送Cache-control头部,Cloudflare 仍然不会缓存该文件。因此只需在源站添加一行:

Nginx
1
2
3
4
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 都是在做这件事情。但是域名散列又会带来如下的问题:

  1. 建立 HTTP 连接的开销是巨大的 —— 每一个域名都需要经过一次 DNS 查询、TCP 三次握手、TLS 还要一次。
  2. 每个 HTTP/1.1 连接都要包含相同的信息比如 User-Agent,而对于非 Cookie-Free 域名还要在请求头中包含Cookie,这造成了流量浪费。
  3. 更多的并发连接和 Keep-Alive 长连接造成客户端和服务端的性能负担。
  4. 如果同一个资源在不同页面下被散列在不同的域名下,那么就没法有效利用 HTTP 缓存。

总之以本站为例,引用的前端资源只分布在两个域名下:

cdn.tsinbei.com
cdn.staticfile.org

引用 2~3 个域名的资源,DNS Lookup的开销都在可接受的范围内。

其他缓存配置文件同上,只需修改locationif的字段即可。

3、后记

关于减轻源站压力,许多人有误区:

“只要使用 CDN 就可以减轻源站压力了吧!”

然而,如果网站是动态内容,使用 CDN 仍然需要回源,并不能起到降低源站负载的作用,正确的解决方法是集群化分布式部署,或者直接升级源站服务器。

如果是静态资源,那么缓存和 CDN 就能大展身手,起到降低源站用于传输静态资源带宽、并发的压力的作用,一次缓存,在缓存过期前都通过 CDN 分发,带宽、并发瓶颈不再是源站而是 CDN。另外,使用对象存储也是不错的选择。

正确配置缓存,即使源站是1Mbps的小水管也可以变成1Gbps的大水泵。

CDN 调教指南(四)缓存控制

https://blog.tsinbei.com/archives/1363/

文章作者
Hsukqi Lee
发布于

2023-10-22

修改于

2023-11-19

许可协议

CC BY-NC-ND 4.0

评论

昵称
邮箱
网址
暂无