一文说透 CDN:用户下载的文件到底来自哪里
你刚把一张图片上传到服务器。
自己访问已经是新图。
同事打开还是旧图。
你刷新浏览器、重启服务、确认文件路径,甚至登录源站反复 cat,都没问题。
但用户看到的就是没变。
这时候通常有人说:
是不是 CDN 缓存了?
这句话经常说对。
但继续问下去,很多人就说不清了:
用户访问的到底是不是源站?文件缓存在哪里?为什么有人看到新内容,有人还是旧内容?刷新 CDN 为什么不是立刻全球生效?
先给结论:
用了 CDN 以后,用户大多数时候拿到的不是源站文件,而是边缘节点缓存的副本。
CDN 做的事情,就是把内容提前放到离用户更近的地方。
没有 CDN 时,请求怎么走
假设你的源站在上海。
一个北京用户访问图片:
北京用户
↓
公网链路
↓
上海源站
每次请求都要跑到源站。
如果用户在更远的地区,网络链路会更长,经过的运营商和中间设备也更多。
问题很直接:
延迟更高
跨运营商链路可能不稳定
源站出口压力大
大量重复请求浪费带宽
突发流量容易把源站打满
一张热门图片被请求一百万次,源站就得重复发送一百万次。
挺笨的。
CDN 的思路是:既然大家要的是同一份文件,就把副本放到各地节点,让用户就近拿。

加上 CDN 后,请求去了哪里
使用 CDN 后,链路变成:
用户
↓
附近的 CDN 边缘节点
↓
命中缓存:直接返回
↓
未命中缓存:向源站请求
CDN 节点通常分布在不同城市、地区和运营商网络中。
用户访问域名时, DNS 或调度系统会尝试把请求引导到一个合适的边缘节点。
“合适”不一定只是地理距离最近。
还可能考虑:
网络运营商
节点负载
链路质量
节点健康状态
内容是否可用
调度策略
所以北京用户和广州用户访问同一个 URL ,可能落到不同 CDN 节点。
他们看到的缓存状态也可能不同。
这就是“我这里已经更新,你那里还是旧的”的来源之一。
缓存命中和回源是什么
边缘节点收到请求后,会先查本地缓存。
如果节点已经有这份文件,而且缓存仍然有效:
缓存命中
节点直接把内容返回给用户,不需要访问源站。
如果节点没有缓存,或者缓存已经过期:
缓存未命中
节点会去源站获取内容。
这个过程叫回源。
完整链路是:
用户请求文件
↓
边缘节点查缓存
↓ 未命中
请求源站
↓
源站返回文件
↓
边缘节点保存副本
↓
返回给用户
下一个用户再访问同一份文件,就可能直接命中边缘缓存。
于是源站只传一次,节点可以返回很多次。

CDN 怎么判断是不是同一份内容
缓存通常依赖缓存键,也就是 cache key 。
最基础的缓存键可能包含:
协议
域名
路径
查询参数
比如:
https://static.example.com/app.js
https://static.example.com/app.js?v=2
是否被视为同一个缓存对象,要看 CDN 的缓存键配置。
有的配置会保留所有查询参数。
有的会忽略部分参数。
有的还会把请求头、 Cookie 、设备类型加入缓存键。
缓存键配错,会出现两个极端。
第一,本该共用缓存的请求被拆得很碎:
app.js?a=1
app.js?a=2
app.js?a=3
每个参数都生成一份缓存,命中率瘦得可怜。
第二,不该共用的内容被当成同一份:
不同用户
不同权限
不同语言
不同设备
如果这些个性化内容被错误缓存,用户可能看到别人的数据。
这就不是性能问题了。
是事故。
缓存多久由谁决定
CDN 缓存时间可能来自多层规则:
源站 Cache-Control
源站 Expires
CDN 控制台缓存规则
节点默认策略
特定文件类型策略
源站常见响应头:
Cache-Control: public, max-age=3600
表示内容可以被公共缓存保存 3600 秒。
如果是带版本号的静态资源:
app.8f3a21.js
logo.a18c9d.png
通常可以设置很长的缓存时间。
因为文件内容变化时,文件名也会变化。
比如:
Cache-Control: public, max-age=31536000, immutable
一年。
听起来很猛,但只要文件名带内容哈希,没问题。
如果你更新文件内容,却继续使用原文件名:
app.js
logo.png
那就要面对旧缓存。
所以前端构建工具喜欢给静态资源加 hash ,不是为了让文件名难看。
是为了把“更新缓存”变成“请求一个新 URL”。
为什么刷新 CDN 后还有人看到旧文件
CDN 不是一台服务器。
它是一张分布式节点网络。
刷新或预热操作,需要把指令传播到不同节点。
而且“旧内容”不一定只在 CDN :
浏览器缓存
公司代理缓存
运营商缓存
CDN 边缘节点
CDN 中间层
源站反向代理缓存
应用自身缓存
你刷新了 CDN 边缘缓存,但用户浏览器还在用本地缓存,看到的仍然可能是旧文件。
所以排查时先看响应头:
curl-Ihttps://static.example.com/app.js
重点观察:
Age
Cache-Control
ETag
Last-Modified
Via
X-Cache
CDN 厂商自定义命中头
不同 CDN 的响应头名称不一样,但通常会提供命中、回源或节点信息。
不要只按刷新键。
那只是浏览器动作,不代表 CDN 缓存被清了。
预热和刷新有什么区别
刷新是让节点里的旧缓存失效。
下一个用户访问时,节点再去源站获取新内容。
流程是:
删除或标记旧缓存
↓
用户请求
↓
节点回源
↓
缓存新内容
预热是主动让 CDN 节点提前去源站拉取内容。
流程是:
提交预热 URL
↓
节点提前回源
↓
内容进入缓存
↓
用户访问时直接命中
大文件发布、热门活动、软件下载,常用预热避免第一批用户一起打源站。
但预热也会产生回源流量。
一次提交几万个大文件,把源站带宽直接打满——这种操作挺有节目效果,但不推荐。
动态接口能不能用 CDN
CDN 不只缓存图片、 JS 、 CSS 。
动态 API 也可以经过 CDN ,但是否缓存要谨慎。
适合缓存的接口:
公开内容列表
商品公共信息
天气和行情快照
不频繁变化的配置
匿名用户共享数据
不适合随便缓存:
用户个人信息
订单详情
支付结果
权限相关接口
实时写操作
包含敏感 Cookie 的响应
如果接口响应因用户而异,缓存键必须把身份和变化维度考虑进去。
但把用户 token 放进缓存键,又可能让缓存碎成渣。
所以动态加速往往不是简单开一个开关。
它需要先回答:
哪些响应可以共享
缓存多久
怎样失效
怎样区分用户
源站异常时能否返回旧内容
想不清楚就别硬缓存。
CDN 还能做什么
CDN 的核心是分发和缓存,但现代 CDN 往往还承担:
HTTPS 证书
DDoS 防护
WAF
访问控制
图片处理
视频分发
边缘计算
请求重写
流量调度
源站隐藏
用户先到 CDN ,源站就不必直接暴露给所有人。
这能减少攻击面。
但别误会:接了 CDN 不代表源站天然安全。
如果源站 IP 仍然公开,攻击者完全可以绕过 CDN 直接打源站。
常见做法是:
源站只允许 CDN 回源 IP
使用回源鉴权
隐藏源站地址
回源走 HTTPS
限制源站暴露端口
CDN 是入口层。
入口层后面仍然要守门。
CDN 出问题怎么排查
常见问题有几类:
文件一直是旧的
部分地区访问异常
命中率很低
回源流量突然升高
HTTPS 证书错误
大文件下载中断
接口被错误缓存
源站正常但 CDN 访问 502
排查顺序可以这样走。
第一,直接访问源站,确认源内容:
curl-Ihttps://origin.example.com/app.js
第二,访问 CDN 域名:
curl-Ihttps://static.example.com/app.js
第三,对比:
状态码
Content-Length
ETag
Last-Modified
Cache-Control
响应内容哈希
第四,检查 DNS 是否已经指向 CDN 。
第五,查看 CDN 命中状态和节点信息。
第六,换地区、换网络验证,避免只测一个节点。
如果带查询参数:
curl-I'https://static.example.com/app.js?v=20260715'
看看是否生成了新的缓存对象。
先确认是哪一层的问题。
别一着急就全量刷新。

怎么理解 CDN
CDN 不是简单的“下载加速器”。
它在源站和用户之间增加了一层分布式内容副本。
用户访问一个 URL 时,真正发生的是:
DNS 或调度选择节点
节点查找缓存
命中则直接返回
未命中则回源获取
节点保存副本
后续用户复用缓存
所以用户下载的文件到底来自哪里?
答案是:
可能来自源站
也可能来自某个边缘节点
还可能来自浏览器自己的缓存
判断不能靠猜。
看 DNS ,看响应头,看缓存键,看内容版本。
把这条链路看清楚,旧文件、缓存不生效、回源暴涨、不同地区内容不一致,就不再是一团雾。
夜雨聆风