概述
本文将分析在发布前后端未分离项目(freemaker
)时遇到的CDN
缓存问题,主要有以下两个问题:
- 页面请求获取的
html
里面却是旧版本号的script
链接 script
脚本链接是新版本号但拉取到的却是旧脚本代码
CDN
CDN
全称是Content Delivery Network
,即内容分发网络,也称为内容传送网络。CDN
是构建在现有网络基础之上的智能虚拟网络,依靠部署在各地的边缘服务器,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。
如上图CDN
的逻辑主要分为两步:DNS
解析和请求边缘节点。
用dig
看下DNS
解析结果:
$ dig juejin.cn
; <<>> DiG 9.10.6 <<>> juejin.cn
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 63296
;; flags: qr rd ra; QUERY: 1, ANSWER: 9, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;juejin.cn. IN A
;; ANSWER SECTION:
juejin.cn. 412 IN CNAME juejin.cn.w.cdngslb.com.
juejin.cn.w.cdngslb.com. 60 IN A 112.85.251.229
juejin.cn.w.cdngslb.com. 60 IN A 112.85.251.227
juejin.cn.w.cdngslb.com. 60 IN A 112.85.251.231
juejin.cn.w.cdngslb.com. 60 IN A 112.85.251.224
juejin.cn.w.cdngslb.com. 60 IN A 112.85.251.225
juejin.cn.w.cdngslb.com. 60 IN A 112.85.251.230
juejin.cn.w.cdngslb.com. 60 IN A 112.85.251.226
juejin.cn.w.cdngslb.com. 60 IN A 112.85.251.228
;; Query time: 9 msec
;; SERVER: 192.168.3.1#53(192.168.3.1)
;; WHEN: Sat May 15 14:26:26 CST 2021
;; MSG SIZE rcvd: 203
在ANSWER SECTION
列表可以看出
juejin.cn
为cname
记录指向juejin.cn.w.cdngslb.com
juejin.cn.w.cdngslb.com
返回了7条A
记录,这7个ip
信息是江苏 徐州 联通,我所在地是上海,联通,可以看出返回的都是就近节点。实际上CDN
是有非常多的边缘节点。
问题分析
1、页面请求获取的html
里面却是旧版本号的script
链接
问题分析前首先我们要知道以下知识点:
(1)freemaker
项目的页面是后端服务将ftl
处理成html
返回的
(2)部署时会遍历ftl
文件,对所有的script
链接打上版本号
// 构建前 supplierQuoteDetailPaging.ftl
<!DOCTYPE html>
<head>
<script type="text/javascript" src="${Global.getConfig("web.app.static.url")}/js/supplierQuoteDetail.js"></script>
</head>
</html>
// 在`Jenkins`构建后会对请求静态脚本的`url`加上版本号 supplierQuoteDetailPaging.ftl
<!DOCTYPE html>
<head>
<script type="text/javascript" src="${Global.getConfig("web.app.static.url")}/js/supplierQuoteDetail.js?version=1638706227856"></script>
</head>
</html>
(3)后端是集群服务,采用滚动部署,也就是说部署时节点服务是一批一批来更新的,直到集群中所有的实例都更新成新版本,而不是一次性全量更新
因为采用滚动部署,因此在部署期间新服务和旧服务会同时存在,如果在这期间访问页面,页面接口可能命中旧服务,也可能命中新服务。当命中旧服务时,请求得到的html
里面script
链接打上的是旧版本号;当部署完成时,集群中所有的实例都更新成新版本,页面请求命中新服务,请求得到的html
里面script
链接打上的是新版本号。
解决方案:待项目部署完成后刷新页面就可以了
2、script
链接是新版本号但拉取到的却是旧脚本代码
正常来说,部署项目后,浏览器根据新版本号去请求CDN
上的静态脚本文件,如果CDN
缓存中没有对应新版本号对应的脚本文件,则会向后端服务拉取新脚本,然后CDN
在做一次缓存,后面的脚本请求直接由CDN
返回。
但是,如果部署还未完成浏览器就去访问了,此时这个阶段新服务和旧服务是同时存在的,当新版本号对应的脚本在CDN
上找不到时,就会去服务请求,恰恰请求命中的是旧服务(服务响应跟版本号无关),旧服务返回旧的脚本,然后CDN
缓存新版号对应的旧脚本,这样后续每次请求拉取到的都是CDN
上缓存的就脚本,因此就出现了上述问题。
我们实际的解决方案是对此类项目维护两个发布任务(重复批),也就是部署两次。我们先假设三次部署过程版本号和脚本的映射关系:
版本号 | 脚本 | |
---|---|---|
上一次部署 | V1.0 | J1.0 |
重复批次1 | V1.1 | J1.1 |
重复批次2 | V1.2 | J1.2 |
注:重复批次1与重复批次2代码没有任何改变,J1.1
与J1.2
代码一模一样
假如重复批次1部署未完成时页面访问命中新服务,新服务返回的html
中script
版本号为V1.1
,在浏览器加载html
时回去请求版本号V1.1
对应的脚本代码,而CDN
没有V1.1
对应的脚本代码,需要向服务请求,恰恰请求命中旧服务,返回J1.0
脚本代码,然后CDN
缓存版本号与脚本关系<V1.1,J1.0>
,当重复批次1部署完成后,服务实例都更新了,脚本都是J1.1
,页面再怎么刷新访问,都是拉取CDN
缓存的<V1.1,J1.0>
。
如果此时再部署一次(重复批次2),在部署未完成时页面又访问了,假如请求命中旧服务,那么依旧拉取CDN
缓存的<V1.1,J1.0>
;假如请求命中新服务,新服务返回的html
中script
版本号为V1.2
,浏览器加载html
时回去请求版本号V1.2
对应的脚本代码,而CDN
没有V1.2
对应的脚本代码,需要向服务请求,如果请求命中旧服务则拉取到<V1.2,J1.1>
,并缓存到CDN
;如果请求命中新服务,则拉取到<V1.2,J1.2>
,并缓存到CDN
,因为J1.1
与J1.2
代码一模一样,所以部署两次是可以解决上述CDN
缓存问题的。
另外,还有一些其他的解决方案,比如CDN
刷新和预热
CDN
刷新和预热
CDN
提供资源的刷新和预热功能。通过刷新功能,您可以强制CDN
节点回源并获取最新文件;通过预热功能您可以在业务高峰期预热热门资源,提高资源访问效率。缓存刷新和缓存预热的区别如下所示:
- 缓存刷新:提交缓存刷新请求后,
CDN
节点的缓存内容将会被强制过期。当用户向CDN
节点请求资源时,CDN
会直接回源站请求对应的资源返回给用户,并将其缓存。 - 缓存预热:提交缓存预热请求后,源站将会主动将对应的资源缓存到
CDN
节点。当用户首次请求时,就能直接从CDN
节点缓存中获取到最新的资源,无需再回源站请求。
刷新分为两种:URL
刷新和目录刷新。
URL
刷新:通过提供目录下文件的方式,强制CDN
节点回源获取最新文件,生效时间5分钟内,API
接口RefreshObjectCaches。- 目录刷新:通过提供目录及目录下所有文件的方式,强制
CDN
节点回源获取最新文件。生效时间为5分钟内,API
接口同上。
调用该接口前,请注意:
- 支持
post
请求,参数用form
表单。 - 刷新预热类接口包含
RefreshObjectCaches
刷新接口和PushObjectCache
预热接口。 - 同一个
ID
每天最多可提交2000条URL
刷新和100个目录刷新。 - 每次请求最多只能提交1000条
URL
刷新。 - 每秒最多50次请求。
除了上图所示外,还有一个参数Action
,值为RefreshObjectCaches
,返回值与预热接口一致,如下所示:
示例代码:
https://cdn.aliyuncs.com?Action=RefreshObjectCaches
&ObjectPath=abc.com/image/1.png\nabc.com/image/2.png
&ObjectType=File
&<公共请求参数>
预热仅支持完整URL预热,不支持目录预热,将指定的资源主动预热到CDN
的L2
二级节点上,用户首次访问即可直接命中缓存。生效时间为5分钟内,API
接口PushObjectCache。
调用PushObjectCache
将源站的内容主动预热到L2 Cache
节点上,您首次访问可直接命中缓存,缓解源站压力。调用该接口前,请注意:
- 支持
post
请求,参数用form
表单。 - 刷新预热类接口包含
RefreshObjectCaches
刷新接口和PushObjectCache
预热接口。 - 同一个ID每天最多可提交500条
URL
预热。 - 每次请求最多只能提交100条
URL
预热。 - 每秒最多50次请求。
- 单个
ID
的预热队列最大限制为100条,根据提交的先后顺序来预热。如果队列任务堆积到100条,则需要等提交的预热请求完成之后才能提交新的,以此来保持队列大小始终不超过100。 CDN
的L2 Cache
节点架设在L1 Cache
节点和源站之间,帮助您缓解源站压力。
http(s)://cdn.aliyuncs.com/?Action=PushObjectCache
&ObjectPath=abc.com/image/1.png\nabc.com/image/2.png
&<公共请求参数>
Q&A
Q:缓存预热失败怎么办?
A:缓存预热失败的可能原因是:
- 执行大批量文件的集中预热时,可能会导致您的源站带宽资源被占满。预热时请尽量分批次执行,您也可以通过扩充源站带宽来提升预热效率。
- 检查资源对应的缓存过期时间是否为0,如果为0,不允许缓存会导致预热失败;
- 排查源站的
cache-control
配置,配置private、no-cache、no-store
将导致CDN
不能缓存引起预热失败,如果不配置,默认为private。 - 目前不支持预热目录、动态文件和缓存过期时间为0的
url
。 - 当您预热多个
URL
且以“;”进行分隔时,需切换到英文状态,中文状态下的“;”会导致预热失败
Q:做了刷新和预热操作,为什么访问的文件还是旧的?
A:可能是您缓存刷新和预热的时间间隔太近,导致刷新失败,建议您刷新和预热的间隔时间为五分钟以上。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。