乐于分享
好东西不私藏

一文说透 CDN:用户下载的文件到底来自哪里

一文说透 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 ,看响应头,看缓存键,看内容版本。

把这条链路看清楚,旧文件、缓存不生效、回源暴涨、不同地区内容不一致,就不再是一团雾。