4.1 CDN 接入:缓存策略与刷新

站已经托管上云了。第三篇我们走通了 COS、域名和 SSL,页面能通过 HTTPS 正常打开。但上线第一天合伙人就发来截图:首页首屏要三秒多,轮播图转半天。本地调试时明明是秒开的,这差距从哪来的?

场景:源站在广州,用户在全国

我们的站部署在腾讯云网站托管上,源站资源在华南。本地访问秒开,是因为我们本来就在同城;但跨地域访问时,每个 JS、CSS、图片都要回源拉取,一来一回延迟叠加,首屏自然就慢。创业页是要发给投资人看的,他们在各个城市,加载速度就是第一印象,慢三秒可能页面就被直接关掉了。

解决方案是 CDN:把静态资源缓存到全国各地的边缘节点,用户就近取内容,不用每次都回源。CDN 的官方文档在 cloud.tencent.com/document/product/228,接入流程和接口说明都很全。网站托管服务本身已经内置了 CDN 加速能力,但在自定义域名、缓存策略、刷新时机这些环节,还有不少可以调优的空间。这一篇记下我们接入和调优 CDN 的全过程,核心就两件事:缓存策略怎么配、内容更新后怎么刷。

想理解为什么这两件事最重要,先看一个心智模型。

方案:缓存是钥匙,刷新是锁

CDN 的缓存是整个加速方案的钥匙。节点上没有缓存时,用户请求会回源一次,拿到的资源按缓存规则存在节点上;之后其他用户再访问同一资源,直接从节点返回,不再回源。所以缓存策略决定两个指标——命中率和新鲜度。缓存时间越久命中率越高,但内容更新后旧缓存残留的时间也越久。

我们的资源分三类,策略各不相同:

资源类型 路径特征 建议缓存时长 更新方式
带哈希的打包产物 /assets/xxxx.js 365 天 文件名带哈希,内容变名字就变
首页入口 HTML /index.html 不缓存或 60 秒 每次部署后主动刷新
图片与字体 /img、/fonts 30 天 覆盖上传后刷新目录

关键点在打包产物:一定要用内容哈希命名。Vite 默认就是这么做的,build 出来的 JS 文件名带八位哈希,内容一变名字就变。这样 JS 和 CSS 可以放心缓存一年,而 HTML 里引用的是新文件名,用户刷新首页时自然就拿到新资源。这个约定在第二篇构建脚本里已经打好基础,到这里正好接上,这也是我们把缓存策略和构建脚本放在一起规划的原因。

配置入口在 CDN 控制台的域名管理里,找到缓存配置,按上表一条条加规则。注意规则有优先级,先匹配先生效,要把 HTML 的规则放在最前面,否则后面的通配规则可能先截获请求。保存后规则一般一两分钟内生效,不放心的话可以用响应头里的缓存标记验证:请求一个文件,看返回头里是节点缓存命中还是回源拉取。调规则当天我们反复用这个办法确认,比凭感觉猜要靠谱得多。

配置:把刷新写进部署脚本

缓存配好只是第一步。更实际的问题是:每次部署后,旧 HTML 还躺在节点上,新内容如何立刻生效?答案是刷新。刷新的意思是主动把节点上的缓存标记为失效,下一次请求会回源拉最新内容。

控制台可以手动刷,但每次发包都去点一下太容易忘。我们的做法是把刷新写进部署脚本,部署完自动调用 CDN 接口,这一步在后面的 CI/CD 篇会串进完整流水线。核心代码如下:

// scripts/refresh-cdn.ts:构建部署完成后刷新 CDN 缓存
import CdnClient from "tencentcloud-sdk-nodejs-cdn";

const client = new CdnClient({
  credential: {
    secretId: process.env.TC_SECRET_ID!,
    secretKey: process.env.TC_SECRET_KEY!,
  },
  region: "ap-guangzhou",
  profile: { httpProfile: { endpoint: "cdn.tencentcloudapi.com" } },
});

async function refreshCdn() {
  const urls = [
    "https://example.com/",
    "https://example.com/index.html",
    "https://example.com/about",
  ];
  const res = await client.FlushCdnUrl({
    Urls: urls,
    FlushType: "flush", // flush 清除并立即回源,delete 只删缓存不回源
  });
  console.log("刷新任务已提交,TaskId:", res.TaskId);
}

refreshCdn().catch((e) => {
  console.error("CDN 刷新失败:", e.message);
  process.exit(1);
});

这段脚本只做一件事:把几个入口页面加入刷新队列。接口是异步的,提交后会返回一个 TaskId,可以用它查询刷新进度。这在排查问题时很有用——有时候页面还是旧的,先确认任务到底成功了没有,而不是反复怀疑缓存策略没生效。官方接口文档在 CDN 文档中心(cloud.tencent.com/document/product/228)里写得很清楚。

除了刷新,还有一个方向相反的操作:预热(Push)。刷新是清空,预热是提前把资源填充到节点上。新站刚上线或发大版本时,与其等第一个用户触发回源,不如主动把首屏资源推到节点,调用的接口是 PushUrlsCache,参数几乎一样,场景正好互补。

预热适合两种时刻:一是站刚上线,所有节点都没有缓存;二是做了改版,首屏资源换了新文件名,一次预热能让全国用户都秒开。我们固定在每次大版本发布时跑一次预热脚本,小迭代只刷入口页。预热同样占用每日额度,不过配额和刷新分开计算,不用担心互相挤压。

还要注意控制台里的一个开关:是否遵循源站缓存头。如果源站返回了不缓存的响应头,CDN 节点就不会缓存,命中率会低到没法看。我们的做法是在控制台强制使用自定义缓存规则,忽略源站的响应头,这样行为完全可控。

另外说一个回源协议的细节。CDN 节点回源拉资源时,用 HTTPS 还是 HTTP 取决于控制台里的回源协议配置。我们一开始图快用了 HTTP 回源,虽然用户侧是 HTTPS,但节点到源站这段是明文,安全上差点意思。后来改成协议跟随,用户用 HTTPS 访问就 HTTPS 回源,一次配置就没有短板了。证书是第三篇申请的免费 DV 证书,配上之后此处一路加密到底。

踩坑与排查

接入过程踩过几个坑,记下来给你省时间。

第一个是刷新额度。CDN 的 URL 刷新有每日额度上限,默认每天不超过 1000 个 URL、100 个目录。我们的 CI 一开始每次部署都刷全站,包不大但次数多,很快触到限额,接口报错时部署脚本还以为是代码出了问题。解决办法是只刷变化的入口页,带上哈希的资源根本不用刷,名字变了自然就是新请求。后来在监控告警里加了刷新失败告警,才算彻底闭环。

第二个是命中率怎么看。CDN 控制台有命中率曲线,正常应该在九成以上。如果某天命中率突然掉到一半以下,先别怀疑策略,按这个顺序查:先看是不是有大流量回源的新资源(比如刚发布了新版本),再看源站响应头是不是变了,最后才去翻缓存规则。命中率下降往往是回源变多造成的,而不是规则失效。

第三个是刷新目录还是刷新 URL。刷新目录开销大、生效慢,但适合图片这种整体替换的场景;刷新 URL 精准、额度占用多,适合 HTML 这类入口页。我们最后定的规矩:HTML 走 URL 刷新,图片目录整体替换时走目录刷新,两者结合着用。命中率的长期观察可以参考云监控实践,思路是一致的。具体到排查,控制台还有几个现成工具:回源流量明细能看出是哪些 URL 在频繁回源,状态码统计能一眼发现有没有大量 404 被缓存。有一次我们某个旧图标路径删了但外站还在引用,节点上反复缓存 404,就是靠状态码统计抓住的。顺手把这类路径写进刷新清单,清一次干净一次。

小结

CDN 接入本身不复杂,控制台点点就能通。真正难的是把缓存策略、构建产物命名、部署后的刷新串成一条线:构建用内容哈希让静态资源敢长期缓存,部署后自动刷新入口页让新内容立刻可见,再用命中率曲线验证效果。这套组合拳打完,全国访问首屏从三秒多降到 300 毫秒以内,这就是创业页发给投资人之前的底气。还有一个意外收获:因为缓存命中率稳定在九成以上,源站的流量和费用也跟着降了下来,托管账单几乎只剩基础部分。投入产出比一算,CDN 调优可能是整个项目里最划算的一件事。下一篇我们给静态站补上后端,先从留言表单开始。

延伸阅读