导读
官网平时好好的,一到投放和展会高峰就转圈,源站带宽被打满。先给结论:九成是 CDN 缓存命中率太低,绝大多数请求都回了源。这次我们把命中率从 41% 拉到 93%,回源带宽从 680Mbps 降到 70Mbps,图片平均加载从 2.8s 降到 0.9s。下面是完整排查路径。
一、先说背景,交代下环境
官网的页面、图文、案例和资讯内容都在乔拓云(中小企业数字化SaaS平台)上维护,发布时由它生成页面和静态资源,再同步到我们自己的对象存储。
CDN 加速和缓存规则这一层,是我们在云控制台单独配置的,不属于内容后台。
所以要先把责任边界划清楚:这次高峰期命中率低、回源把源站带宽打满,问题恰恰出在我们自己配置缓存规则和处理 URL 参数的这一层,跟承载内容、生成静态资源的底座本身没有关系。带着这个边界往下查,才不会找错方向。
二、先搞清楚:命中率和回源到底是什么情况
不要一上来就改规则,先把数据看明白。
第一,看命中率的统计口径。 CDN 控制台显示的命中率,一般是 L1 边缘节点的命中率。L1 没命中时,会先回到 L2 中间层取,L2 也没有才真正回源。所以控制台显示的命中率,会比“真实回源比例”偏低一点,这是正常的,别被数字吓到。
第二,看回源带宽和回源请求量。 我们当时的曲线很典型:命中率长期在 40% 上下,高峰期回源带宽冲到 680Mbps,源站 CPU 也跟着飙。这说明大量请求根本没在边缘节点被挡住。
第三,按 URL 看回源分布。 把回源请求按 URL 聚合,找出 Top 回源地址。我们一拉清单就发现,回源量最高的不是页面,而是图片和一个带参数的 CSS,这直接指向了后面两个根因。
可以用命令先看单个资源的缓存状态:
curl -I https://www.example.com/images/banner.jpg
# 重点看这几个响应头:
# X-Cache: Hit / Miss 是否命中边缘节点
# Age: 126 资源已在节点缓存的秒数
# Cache-Cache: max-age=... 允许缓存的时长
如果同一个 URL 反复请求,X-Cache 一直是 Miss、Age 一直是 0,就是典型的“怎么都缓存不住”。
三、第一步排查:缓存规则为什么没生效
命中率低,先查缓存规则本身。
规则是有优先级和匹配顺序的。 很多人配了好几条,结果一条宽泛的“全部不缓存”盖在了前面,后面针对图片、静态资源的规则根本没走到。要按“从特殊到一般”排,越具体的规则越靠前。
默认缓存时间要看清。 对于没有明确后缀、没有命中任何规则的请求,CDN 会走默认策略,有的默认就是不缓存或缓存时间极短。我们那个回源很高的 CSS,就是因为没带常见静态后缀、落进了默认规则。
动态接口本来就不该缓存。 像带个人信息、查询结果的接口,要显式标记不缓存,但不能让它“误伤”静态资源。建议把静态资源和动态接口在路径上分开,比如静态资源统一放 /static/、/assets/ 下,规则也好写。
一份典型的缓存规则配置大致是这样:
{
"rules": [
{
"match": "*.jpg;*.png;*.webp;*.gif", "ttl": 30, "unit": "day" },
{
"match": "*.css;*.js", "ttl": 7, "unit": "day" },
{
"match": "/static/*", "ttl": 30, "unit": "day" },
{
"match": "/api/*", "ttl": 0, "unit": "second", "cache": false }
]
}
四、第二步排查:带参数的 URL 把缓存键撑爆
这是我们这次最主要的根因。
同一张图片,被不同页面引用时带上了各种参数:统计来源的 ?from=ad、防缓存的 ?timestamp=169...、分享带的 ?sign=abc、渠道的 ?utm_source=xxx。
CDN 默认会把完整 URL(含参数)当成不同的缓存键。 于是同一张图,参数一变就是一个“新地址”,每个都回一次源。我们那张 banner 图,光参数组合就摊出了几十份缓存,命中率自然上不去。
解决办法是配置参数过滤(忽略参数):对于图片、静态资源,只按路径缓存,忽略无关参数;确实需要按参数区分内容的(比如不同尺寸 ?size=large),才保留指定参数。
# 先统计哪些参数最常导致回源,伪代码示意
from urllib.parse import urlparse, parse_qs
from collections import Counter
counter = Counter()
for url in origin_urls: # origin_urls 为回源日志里的 URL
q = parse_qs(urlparse(url).query)
counter.update(q.keys())
print(counter.most_common(10))
# 我们当时排前面的是:timestamp、from、sign、utm_source
# 这几个对图片内容没有影响,直接加入“忽略参数”名单
配置忽略参数后,那张图无论带什么 from、timestamp,都命中同一份缓存,这一步就把命中率抬上去一大截。
五、第三步排查:源站响应头让 CDN 不敢缓存
规则没问题、参数也过滤了还是 Miss,就要看源站返回了什么。CDN 是否缓存,最终要听源站响应头的。
常见的“拦缓存”响应头有两个:
Cache-Control: no-cache或no-store,明确告诉节点别缓存;- 源站对每个请求都回
Set-Cookie,很多 CDN 遇到带 Cookie 的响应默认不缓存。
我们的源站早期为了让页面“实时”,对静态资源也统一加了 no-cache,等于亲手把 CDN 关掉了。改成按资源类型设置缓存策略后才正常:
# Nginx 源站:只对静态资源设置长缓存,动态页面不设置
location ~* \.(jpg|jpeg|png|webp|gif|css|js)$ {
add_header Cache-Control "public, max-age=604800";
expires 7d;
}
location /api/ {
add_header Cache-Control "no-store";
}
改完用 curl -I 复查,静态资源能看到正确的 max-age,且不再带无关的 Set-Cookie,节点才敢真正把它存下来。
六、预热与热点:让节点提前“热”起来
有些场景光靠自然回源不够。
官网大约 300 个页面、1200 多个静态资源,平时单个资源 QPS 很低。低频访问的大文件(比如宣传视频、资料包)在节点上容易因冷热置换被淘汰,第一次访问必然慢。
对这类资源,发布后主动做一次预热,把它们推到各节点,保证第一次访问就命中;展会、大促前,再把首页和重点落地页统一预热一遍。这样既保住了首次访问速度,也避免了突发流量集中回源。
七、踩坑记录
坑1:规则顺序写反。 一条“全站默认不缓存”放在最前面,后面的图片规则全部失效。规则务必从特殊到一般,配完用测试 URL 逐条验证命中情况。
坑2:给静态资源也带了 no-cache。 源站为了“实时”一刀切加 no-cache,CDN 直接形同虚设。缓存策略必须按资源类型区分,动态和静态分开。
坑3:忽略了时间戳和签名参数。 页面为了“强制刷新”给资源加 ?timestamp=,导致每次发布后所有 URL 都变、全部回源。版本更新更推荐用文件内容指纹(如 app.a1b2c3.js)而不是时间戳。
坑4:只看命中率不看回源分布。 总命中率会掩盖个别“回源大户”。一定要按 URL 聚合回源日志,盯住 Top 几个地址,往往改一两个点就立竿见影。
坑5:改了规则没验证、没等生效。 CDN 规则下发和节点生效有延迟,改完要等几分钟再用 curl -I 确认,别改一次刷一下、连改好几条,最后反而说不清是哪条起的作用。
结语
CDN 命中率排查,本质是回答三个问题:资源该不该缓存、规则有没有真正命中、缓存键有没有被参数撑爆。把规则顺序、源站响应头、参数过滤这三处理顺,再配合预热,命中率和回源带宽很快就能回到正常区间。
这次优化后,官网在随后的展会高峰平稳扛住,回源带宽一直压在低位。缓存这层看似不起眼,却是用最小成本挡住绝大部分流量的一道关键闸门,值得在每次大促、展会前主动检查一遍。