4.3 CDN 加速:静态资源分发
场景:外地同事说报告下载要等半分钟
COS 接入之后文件确实规整了,但抱怨并没消失。深圳的同事还好,哈尔滨出差的同事说每次下载分析报告要等半分钟,前端页面第一次打开也要三四秒。原因很直白:COS 单点在广州,跨地区公网回源,距离越远越慢;而且我们前端那些 JS、CSS、图标,每个用户来都回源拉一遍,纯属浪费。
解法是 CDN。内容分发网络的核心思路是把内容缓存到全国各地的边缘节点,用户就近取,回源只发生在缓存失效时。腾讯云 CDN 接入成本极低——一个域名、几分钟配置、按流量计费。这一节记录我接入 CDN 的全过程,以及缓存策略、刷新机制和费用控制三件最容易出事的地方。
方案:接入与缓存策略
接入步骤本身不复杂:控制台进入 CDN,添加域名 static.toolkit.example.com,源站类型选之前建好的 COS 桶私有域名,回源鉴权打开(私有桶必须配,否则 CDN 拿不到内容),加速类型选静态加速。域名 CNAME 解析配好之后等几分钟生效,就有了全国分发能力。这部分流程与轻量服务器建站指南里的域名与解析章节是同一套体系,不清楚 CNAME 怎么配的可以对照着看。
真正的技术含量在缓存规则。CDN 默认按源站 Cache-Control 头走,但 COS 默认的缓存策略对前端资源不合理:带 hash 的静态文件(app.3f2a1b.js)内容永变,适合长缓存;用户上传的导出报告天天换,缓存几分钟即可;API 接口绝不能缓存。我在控制台按目录配了三套规则:
| 路径匹配 | 缓存时长 | 优先级 | 适用内容 | 不遵循缓存的后果 |
|---|---|---|---|---|
/assets/* |
365 天 | 高 | 带 hash 的前端静态文件 | 用户拿到旧版页面,版本错乱 |
/reports/* |
5 分钟 | 中 | 分析报告、导出文件 | 旧数据多缓存一会儿,无伤大雅 |
/api/* |
不缓存 | 最高 | 接口响应 | 缓存了动态数据,用户看到别人的聊天记录 |
第一行配合前端构建的 hash 文件名是天作之合:文件名带内容指纹,发新版本自动换名,旧缓存自然失效,根本不需要手动刷。第二行的 5 分钟是我与产品反复掰扯后的折中——业务方希望报告下下来就是最新的,但对延迟又敏感,5 分钟的窗口既能扛住同部门几十人的并发下载,又保证最多五分钟数据新鲜度。
第三个规则背后是一次真实事故。某次同事把 /api/chat 的响应通过 CDN 暴露给了测试域名,结果一个用户看到了另一个用户的问答记录。 panicked 了一上午,最后定位到是某处把接口路径写进了 CDN 域名引用。安全红线:动态接口一律不经 CDN,CDN 域名只服务静态路径。这条教训后来写进了团队规范,腾讯云实践里也有类似的缓存与数据隔离案例分析。
缓存配好了,改内容的善后工作就是刷新。CDN 提供刷新(清缓存)和预热(主动推送)两个 API,发布新版本时我会调用刷新:
from tencentcloud.cdn.v20180606 import cdn_client, models
import json, os
def purge_css_url():
client = cdn_client.CdnClient(
credential={"secretId": os.getenv("TENCENT_SECRET_ID"),
"secretKey": os.getenv("TENCENT_SECRET_KEY")},
region="ap-guangzhou",
)
req = models.PurgePathCacheRequest()
req.from_dict({
"Paths": ["https://static.toolkit.example.com/assets/"],
"FlushType": "flush",
})
resp = client.PurgePathCache(req)
print(json.dumps(resp.to_json(), ensure_ascii=False))
这段脚本配进发布流水线,每次发版自动执行。注意刷目录有每日额度限制(默认 100 个左右路径),别把它当常规操作;真正的日常更新应该靠 hash 文件名自动绕过缓存,刷新只用于紧急修复。
预热是与刷新相反的操作:在用户访问之前主动把内容拉到节点上。我们的分析报告每天凌晨生成,第二天全部门上班同时打开,如果不预热,第一批用户等于集体帮 CDN 回源。所以报告生成流水线里加了一段:
def push_warm_report(date_str):
# client 构造与上文 purge_css_url 相同
client = get_cdn_client()
for region_report in report_urls(date_str):
req = models.PushUrlsCacheRequest()
req.from_dict({"Urls": [region_report["url"]]})
client.PushUrlsCache(req)
预热的额度比刷新更紧(每天几十条),所以只对确定会被集中访问的内容用,不能无脑全推。上预估实际命中率最直观的办法是看控制台的回源率指标:回源率连续几天走高,说明缓存规则配得不合理,得回头检查匹配路径是不是太宽或者过期时间太短。
踩坑与排查
坑一:改了资源,页面还是旧的。 经典问题,排查顺序:浏览器本地缓存、CDN 节点缓存、源站。先无痕模式打开排除浏览器;再 curl -I 一个 CDN 节点看 X-Cache-Lookup 和 Age 头,Age 为 0 说明节点刚回源过,命中旧缓存的概率低;最后直连源站确认内容确实是新的。我遇到过一次源站文件就没传上去,却查了两小时 CDN——排查顺序反了,先信 CDN 再信源站,时间全浪费。从那以后哪怕再急,也按这三步走,十分钟内必见分晓。
坑二:费用突然飙高。 某月账单是平时的八倍,一查是有个部门把产品演示视频放到了 CDN 域名下,被全公司反复播。应急处理:视频移回 COS 私有桶走签名链接,给 CDN 加带宽上限告警。长期措施是给域名配流量包抵扣,高频大文件走对象存储 COS加回源,CDN 只服务小文件。
坑三:跨域配置丢失。 前端切到 CDN 域名后所有接口请求开始报 CORS 错误。原因是浏览器的跨域校验针对的是响应头,CDN 的跨域配置在控制台的回头 Headers 模块单独设置,与源站 CORS 是两套。配好 Access-Control-Allow-Origin 后还要注意 CDN 会对 OPTIONS 预检请求也缓存,缓存了预检结果 sometimes 会返回错误的头,排查时记得带上 Cache-Control: no-cache。
坑四:HTTPS 证书配置在 CDN 侧。 CDN 域名的证书不会自动继承源站,要在 CDN 控制台单独上传或一键申请。有次我以为配好了其实传错了密钥文件,用户报 TLS 错误才发现。之后所有证书统一用脚本管理、到期监控统一告警,相关实现在下一节监控与日志展开。
挑选加速类型也有讲究。控制台里静态加速、流媒体加速、下载加速是不同的计费与缓存策略模版:我们的报告属于典型的下载场景,选下载加速后大文件的分块传输明显更稳;如果只图省事选默认静态加速,遇到几十兆的报告就会频繁回源、首包时间拉长。配置前先把自己的内容结构列清楚——多少比例是小文件、多少是大文件、有没有视频,模版选对比后面所有调优都重要。
小结
CDN 的核心是就近缓存与回源的最小化,难点不在接入而在缓存策略与费用治理:静态文件靠 hash 长缓存、动态内容坚决不进 CDN、刷新 API 留给紧急场景、带宽设告警防账单爆。接入完成并不代表万事大吉,运行状态怎么观测是下一节的内容。如果你的站点的流量结构偏 Serverless 应用,Serverless 部署入门里也介绍了边缘函数加静态托管的组合方案,可以对比选型。第一次接入 CDN 的成本顾虑可以通过AI 办公助手活动页的流量包减免消除。
