CDN加速原理
一个常见的场景
你在上海访问一个部署在杭州的网站,页面秒开;你的同事在乌鲁木齐访问同一个网站,白屏了两秒。静态资源一模一样,服务器也没挂,问题出在哪?
答案是物理距离。光在光纤中的传播速度大约是 20 万公里/秒,上海到杭州约 200 公里,往返延迟不到 2ms;上海到乌鲁木齐约 3700 公里,往返延迟可以到 37ms。一个页面加载几十个资源,每个资源都多 30ms,累积起来差距非常明显。
CDN 要解决的核心问题就是:把内容放到离用户最近的地方,减少网络传输的物理距离和中间跳数。
什么是 CDN
CDN(Content Delivery Network,内容分发网络)是一组分布在不同地理位置的服务器集群。它的职责不是生成内容,而是缓存和分发源站的内容。
用更直白的话说:CDN 就是在全国(甚至全球)各地部署了一堆"副本服务器",用户请求资源时,由离他最近的那台服务器响应,而不是每次都跑去源站拿。
CDN 解决的核心问题:
- 降低延迟:缩短用户到服务器的物理距离
- 减轻源站压力:大量请求被边缘节点消化,不会全部打到源站
- 提升可用性:某个节点故障,调度系统可以将流量切到其他节点
- 应对突发流量:CDN 节点的带宽储备远大于单台源站
CDN 的工作原理
一次完整的 CDN 请求流程是这样的:
用户发起请求 → DNS 解析 → 智能调度 → 边缘节点响应(命中缓存直接返回 / 未命中则回源)
展开来看每一步:
1. 用户发起请求
用户在浏览器访问 https://static.example.com/app.js,浏览器需要先把域名解析成 IP 地址。
2. DNS 解析与 CNAME 跳转
这是 CDN 调度的入口。源站在 DNS 配置中,会将 static.example.com 设置一条 CNAME 记录,指向 CDN 厂商提供的调度域名,比如 static.example.com.cdn.cloudflare.net。
DNS 解析过程:
用户请求 static.example.com
→ 本地 DNS 递归查询
→ 拿到 CNAME: static.example.com.cdn.cloudflare.net
→ 继续解析 CNAME,请求到达 CDN 的智能 DNS
→ CDN 智能 DNS 返回最优边缘节点 IP
→ 用户拿到 IP,发起 HTTP 请求
3. 智能调度(GSLB)
CDN 的智能 DNS 背后是一套 GSLB(Global Server Load Balancing,全局负载均衡) 系统。它根据多个维度决定把用户引导到哪个边缘节点:
- 地理位置:用户的 IP 对应哪个地区,就近分配节点
- 网络状况:各节点的实时延迟、丢包率
- 节点负载:CPU、带宽、连接数等指标
- 运营商:用户是电信还是联通,尽量分配同运营商节点,避免跨网延迟
- 节点健康状态:故障节点会被自动剔除
GSLB 的调度决策通常在毫秒级完成,用户完全无感知。
4. 边缘节点响应
用户的请求到达边缘节点后,会出现两种情况:
缓存命中(Cache Hit):节点本地有这个资源的缓存副本,且未过期,直接返回。这是最理想的情况,响应速度极快。
缓存未命中(Cache Miss):节点本地没有缓存,或缓存已过期。此时节点需要去源站(或上层父节点)拉取资源,这个过程叫回源。
大多数 CDN 采用两级架构:边缘节点 → 中心节点(父节点)→ 源站。边缘节点未命中时,先问中心节点;中心节点也没有,才回源站。这样可以进一步降低回源频率。
DNS 调度机制详解
DNS 调度是 CDN 的"交通指挥系统",直接决定用户被分配到哪个节点。
CNAME 配置
接入 CDN 的第一步就是配置 CNAME。以阿里云 CDN 为例:
# 在域名管理控制台添加 CNAME 记录
# 主机记录: static
# 记录类型: CNAME
# 记录值: static.example.com.w.cdngslb.com
配置完成后,所有对 static.example.com 的 DNS 查询都会被引导到 CDN 的调度系统。
智能 DNS 的工作方式
普通 DNS 对同一个域名返回固定 IP,CDN 的智能 DNS 则会根据请求来源动态返回不同 IP。
一个简化的判断逻辑:
if 用户IP属于华东地区 && 电信网络:
return 上海电信边缘节点IP
elif 用户IP属于华南地区 && 联通网络:
return 广州联通边缘节点IP
else:
return 距离最近且负载最低的节点IP
实际的 GSLB 系统远比这复杂,会综合考虑实时探测数据、历史统计数据和预设策略。
GSLB 与普通负载均衡的区别
| 维度 | 普通负载均衡(如 Nginx) | GSLB |
|---|---|---|
| 作用范围 | 单机房内的多台服务器 | 全球多个数据中心 |
| 调度层级 | 四层/七层转发 | DNS 层调度 |
| 调度依据 | 连接数、权重、轮询 | 地理位置、网络质量、节点健康 |
| 典型场景 | 应用服务器集群 | CDN 节点选择 |
缓存策略
CDN 的缓存策略直接决定了缓存命中率,命中率越高,用户体验越好,回源越少。
缓存命中与未命中
CDN 节点收到请求后,查找本地缓存的过程:
- 根据请求的 URL(通常包括查询参数)计算缓存 Key
- 在本地存储中查找该 Key 对应的缓存
- 找到了,检查是否过期
- 未过期 → Cache Hit,直接返回
- 过期或不存在 → Cache Miss,触发回源
你可以通过响应头判断是否命中了 CDN 缓存:
# 命中缓存
X-Cache: HIT
Via: 1.1 cache1.cdn.example.com
# 未命中缓存
X-Cache: MISS
Via: 1.1 cache1.cdn.example.com
缓存过期机制
CDN 节点判断缓存是否过期,主要依赖源站返回的 HTTP 缓存头:
Cache-Control(优先级最高):
# 资源可被缓存 365 天
Cache-Control: public, max-age=31536000
# 资源不缓存
Cache-Control: no-cache, no-store
# 仅浏览器可缓存,CDN 不缓存
Cache-Control: private, max-age=3600
public:允许 CDN 和浏览器都缓存private:只允许浏览器缓存,CDN 不缓存max-age:缓存有效期(秒),相对时间s-maxage:专门给 CDN 等共享缓存使用,优先级高于max-ageno-cache:可以缓存,但每次使用前必须向源站验证no-store:完全不缓存
Expires(HTTP/1.0 遗留):
Expires: Wed, 13 May 2027 00:00:00 GMT
绝对时间,存在客户端与服务端时钟不同步的问题。当 Cache-Control 和 Expires 同时存在时,Cache-Control 优先。
ETag 和 Last-Modified(用于缓存验证):
当缓存过期后,CDN 节点不一定要重新下载完整资源。它可以带上条件请求头去验证:
# CDN 节点发送条件请求到源站
If-None-Match: "abc123" # 对应 ETag
If-Modified-Since: Tue, 12 May 2026 10:00:00 GMT # 对应 Last-Modified
# 源站返回 304:资源没变,继续用缓存
HTTP/1.1 304 Not Modified
# 源站返回 200:资源变了,返回新内容
HTTP/1.1 200 OK
ETag: "def456"
这个机制可以大幅减少回源时的带宽消耗——资源没变的情况下,304 响应几乎没有 body。
CDN 缓存时间的推荐配置
| 资源类型 | 建议 Cache-Control | 原因 |
|---|---|---|
带 hash 的 JS/CSS(如 app.3a7b.js) | public, max-age=31536000, immutable | 文件名变了就是新资源,可以永久缓存 |
| HTML | no-cache 或 max-age=0, must-revalidate | HTML 是入口文件,必须保证拿到最新版本 |
| 图片/字体 | public, max-age=2592000(30天) | 变更频率低,长缓存 |
| API 响应 | private, no-cache 或 no-store | 动态数据,不应被 CDN 缓存 |
回源逻辑
回源是 CDN 节点从源站获取内容的过程。虽然 CDN 的目标是尽量减少回源,但在以下场景中回源不可避免:
回源触发条件
- 首次请求:资源从未被缓存过
- 缓存过期:
max-age到期,需要重新验证或获取 - 缓存刷新:运维手动清除了 CDN 缓存
- 不可缓存的请求:POST 请求、带 Authorization 头的请求、
Cache-Control: no-store的资源
回源请求优化
CDN 厂商在回源环节做了大量优化:
合并回源(Request Collapsing):当多个用户同时请求同一个未缓存的资源时,CDN 节点只向源站发起一次请求,其他请求排队等待结果。这在突发流量场景下尤其重要。
用户A请求 app.js → 边缘节点无缓存 → 向源站发起回源
用户B请求 app.js → 排队等待用户A的回源结果
用户C请求 app.js → 排队等待
...
源站返回 → 边缘节点缓存 → 同时响应 A、B、C
分层回源:边缘节点不直接回源站,而是先向区域中心节点请求。中心节点缓存了更多资源,能拦截大部分回源请求。
回源连接复用:CDN 节点与源站之间保持长连接,避免每次回源都建立 TCP 连接和 TLS 握手。
源站保护
回源流量过大会直接打垮源站。CDN 提供了多种源站保护机制:
- 回源限速:限制每秒回源请求数
- 回源超时控制:设置回源连接超时和传输超时,避免慢请求拖垮源站
- 源站健康检查:主动探测源站健康状态,源站不可用时返回缓存的过期内容(stale-while-revalidate)
- 多源站容灾:配置主备源站,主源站故障时自动切换
CDN 缓存刷新与预热
缓存刷新(Purge)
当源站内容更新后,CDN 节点上的旧缓存不会自动失效(除非到期)。这时需要主动刷新:
URL 刷新:精确刷新某个资源的缓存
# 以阿里云 CDN 为例
aliyun cdn RefreshObjectCaches \
--ObjectPath "https://static.example.com/app.js" \
--ObjectType File
目录刷新:刷新某个路径下所有资源的缓存
aliyun cdn RefreshObjectCaches \
--ObjectPath "https://static.example.com/assets/" \
--ObjectType Directory
刷新操作有额度限制(如每天 10000 条 URL、100 个目录),使用文件名 hash 策略可以大幅减少刷新需求。
缓存预热(Prefetch)
预热是主动把资源推送到 CDN 边缘节点,让第一个用户也能命中缓存:
aliyun cdn PushObjectCache \
--ObjectPath "https://static.example.com/new-feature.js"
典型使用场景:
- 版本发布:新版本上线前,提前预热关键资源
- 运营活动:大促开始前,预热活动页面的静态资源
- 新地区上线:在新地区的 CDN 节点预热热门内容
前端项目中 CDN 的应用
静态资源部署
现代前端项目的典型部署架构:
源站(OSS/S3)
├── index.html ← 不走 CDN 长缓存,或走 CDN 但 no-cache
├── assets/
│ ├── app.3a7b2c.js ← CDN 长缓存(带 hash)
│ ├── vendor.8f2d.js ← CDN 长缓存(带 hash)
│ └── style.a1b2.css ← CDN 长缓存(带 hash)
└── images/
└── logo.png ← CDN 中等缓存
核心策略:HTML 不缓存(或短缓存),静态资源带 hash 长缓存。这样每次发版只需要更新 HTML 中的资源引用路径,用户就能拿到新版本,同时未变更的资源继续命中缓存。
Webpack/Vite 的 hash 配置:
// vite.config.ts
export default defineConfig({
build: {
rollupOptions: {
output: {
// 入口文件使用 contenthash
entryFileNames: 'assets/[name].[hash].js',
// chunk 文件使用 contenthash
chunkFileNames: 'assets/[name].[hash].js',
// 静态资源使用 contenthash
assetFileNames: 'assets/[name].[hash].[ext]',
},
},
},
});
域名哈希(Domain Sharding)
HTTP/1.1 时代,浏览器对同一域名的并发连接数有限制(通常 6 个)。为了突破这个限制,会把静态资源分散到多个 CDN 域名:
<img src="https://img1.cdn.example.com/a.png" />
<img src="https://img2.cdn.example.com/b.png" />
<script src="https://js1.cdn.example.com/app.js"></script>
注意:在 HTTP/2 环境下,域名哈希反而是负优化。 HTTP/2 支持多路复用,一个连接可以并行传输多个资源。多域名意味着多次 DNS 解析和 TLS 握手,增加了额外开销。
现代项目的建议:
- HTTP/2 环境下使用单一 CDN 域名
- 仅在需要隔离 Cookie 时使用额外的静态资源域名
缓存版本管理
除了文件名 hash,还有其他版本管理方式:
查询参数版本号(不推荐):
<!-- 某些 CDN 默认忽略查询参数,导致缓存未更新 -->
<script src="https://cdn.example.com/app.js?v=1.2.3"></script>
文件名 hash(推荐):
<!-- 文件内容变了,hash 变了,天然是新 URL,CDN 必然回源 -->
<script src="https://cdn.example.com/app.3a7b2c.js"></script>
路径版本号(适合公共库):
<!-- 显式控制版本,常见于 CDN 托管的开源库 -->
<script src="https://cdn.jsdelivr.net/npm/vue@3.3.4/dist/vue.global.prod.js"></script>
多 CDN 容灾
大型项目通常不会只依赖单一 CDN 厂商,常见的容灾方案:
DNS 层面切换:通过 DNS 的权重配置,将流量分配到多个 CDN:
static.example.com CNAME cdn-a.example.com (权重 70%)
static.example.com CNAME cdn-b.example.com (权重 30%)
前端 fallback 机制:资源加载失败时,切换 CDN 域名重试:
function loadScript(primaryUrl, fallbackUrl) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = primaryUrl;
script.onload = resolve;
script.onerror = () => {
script.src = fallbackUrl;
script.onload = resolve;
script.onerror = reject;
};
document.head.appendChild(script);
});
}
loadScript(
'https://cdn-a.example.com/vendor.js',
'https://cdn-b.example.com/vendor.js'
);
Service Worker 拦截:更精细的容灾控制,可以在 SW 层统一处理资源加载失败后的 CDN 切换逻辑。
CDN 与浏览器缓存的配合
CDN 缓存和浏览器缓存是两层独立的缓存,它们通过 HTTP 缓存头协同工作:
用户请求
→ 浏览器缓存(强缓存:Cache-Control/Expires)
→ 命中:直接用,不发请求(200 from cache)
→ 未命中/过期:发请求到 CDN
→ CDN 缓存
→ 命中:CDN 直接返回(200)
→ 未命中:CDN 回源站拿,缓存后返回
关键配合点:
s-maxage 与 max-age 分离控制:
Cache-Control: public, max-age=600, s-maxage=86400
这个配置的含义:浏览器缓存 10 分钟,CDN 缓存 24 小时。浏览器缓存过期后请求 CDN,大概率还能命中 CDN 缓存。这在"内容更新不频繁,但希望用户能在合理时间内看到新内容"的场景下非常实用。
stale-while-revalidate:
Cache-Control: public, max-age=600, stale-while-revalidate=86400
缓存过期后,先返回过期内容给用户(保证速度),同时在后台异步回源更新缓存。用户下次请求就能拿到新内容。
配合策略总结:
| 场景 | 浏览器缓存 | CDN 缓存 | 说明 |
|---|---|---|---|
| 带 hash 的 JS/CSS | 长期(1年) | 长期(1年) | 内容变 = URL 变,双层长缓存 |
| HTML 入口 | 不缓存 | 短缓存或不缓存 | 保证用户拿到最新入口 |
| 图片资源 | 中等(7天) | 较长(30天) | CDN 缓存比浏览器久 |
| API 数据 | 不缓存 | 不缓存 | 动态数据不适合 CDN |
面试高频追问
Q:CDN 是怎么知道该把用户引导到哪个节点的?
通过 GSLB(全局负载均衡)系统。用户请求域名时,DNS 解析会经过 CDN 的智能 DNS,它根据用户 IP 的地理位置、运营商、各节点的实时负载和健康状态,选出最优节点的 IP 返回给用户。
Q:CDN 缓存和浏览器缓存有什么区别?
浏览器缓存是终端用户设备上的缓存,只服务当前用户;CDN 缓存是边缘节点上的共享缓存,服务该节点覆盖区域内的所有用户。两者通过 HTTP 缓存头(Cache-Control 的 max-age 和 s-maxage)分别控制。
Q:如果 CDN 节点缓存了旧资源,怎么让用户拿到新资源?
两种思路:一是使用文件名 hash 策略(推荐),内容变了文件名就变,天然绕过旧缓存;二是通过 CDN 控制台或 API 执行缓存刷新操作,手动使旧缓存失效。
Q:为什么 HTML 不建议走 CDN 长缓存?
HTML 是所有资源的入口文件,它引用了带 hash 的 JS/CSS 路径。如果 HTML 被长缓存,用户拿到的是旧 HTML,里面引用的还是旧资源路径,发版就不生效了。HTML 应设置 no-cache 或极短的 max-age,保证每次都能拿到最新版本。
Q:HTTP/2 下还需要域名哈希吗?
不需要。HTTP/2 支持多路复用,单连接可以并行传输大量资源。域名哈希反而会增加 DNS 解析和 TLS 握手的开销,在 HTTP/2 下是负优化。
Q:CDN 的 s-maxage 和 max-age 有什么区别?
max-age 同时对浏览器和 CDN 生效,s-maxage 只对 CDN 等共享缓存生效且优先级更高。通过组合使用,可以实现"浏览器缓存短、CDN 缓存长"的差异化策略。
Q:什么是回源?回源多了会有什么问题?
回源是 CDN 节点从源站获取资源的过程。回源过多会导致源站压力剧增,严重时可能打垮源站,同时用户侧的响应时间也会明显变长。优化手段包括合并回源、分层回源、提升缓存命中率和配置源站保护策略。
总结
- CDN 的本质是用空间换时间——在全球部署缓存节点,让用户就近访问
- DNS 调度(GSLB)是 CDN 的大脑,决定用户被引导到哪个节点
- 缓存策略是 CDN 的核心,命中率越高效果越好;
Cache-Control是最重要的控制手段 - 回源不可避免,但可以通过合并回源、分层回源、缓存预热等手段降低回源率
- 前端项目中,HTML 不缓存 + 静态资源带 hash 长缓存是最经典的 CDN 使用模式
- CDN 缓存和浏览器缓存是两层独立的缓存,通过
max-age和s-maxage分别控制,配合使用效果最佳