CDN加速原理

22 分钟

一个常见的场景

你在上海访问一个部署在杭州的网站,页面秒开;你的同事在乌鲁木齐访问同一个网站,白屏了两秒。静态资源一模一样,服务器也没挂,问题出在哪?

答案是物理距离。光在光纤中的传播速度大约是 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 节点收到请求后,查找本地缓存的过程:

  1. 根据请求的 URL(通常包括查询参数)计算缓存 Key
  2. 在本地存储中查找该 Key 对应的缓存
  3. 找到了,检查是否过期
  4. 未过期 → Cache Hit,直接返回
  5. 过期或不存在 → 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-age
  • no-cache:可以缓存,但每次使用前必须向源站验证
  • no-store:完全不缓存

Expires(HTTP/1.0 遗留):

Expires: Wed, 13 May 2027 00:00:00 GMT

绝对时间,存在客户端与服务端时钟不同步的问题。当 Cache-ControlExpires 同时存在时,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.jspublic, max-age=31536000, immutable文件名变了就是新资源,可以永久缓存
HTMLno-cachemax-age=0, must-revalidateHTML 是入口文件,必须保证拿到最新版本
图片/字体public, max-age=2592000(30天)变更频率低,长缓存
API 响应private, no-cacheno-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-maxagemax-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-ages-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-maxagemax-age 有什么区别?

max-age 同时对浏览器和 CDN 生效,s-maxage 只对 CDN 等共享缓存生效且优先级更高。通过组合使用,可以实现"浏览器缓存短、CDN 缓存长"的差异化策略。

Q:什么是回源?回源多了会有什么问题?

回源是 CDN 节点从源站获取资源的过程。回源过多会导致源站压力剧增,严重时可能打垮源站,同时用户侧的响应时间也会明显变长。优化手段包括合并回源、分层回源、提升缓存命中率和配置源站保护策略。

总结

  • CDN 的本质是用空间换时间——在全球部署缓存节点,让用户就近访问
  • DNS 调度(GSLB)是 CDN 的大脑,决定用户被引导到哪个节点
  • 缓存策略是 CDN 的核心,命中率越高效果越好;Cache-Control 是最重要的控制手段
  • 回源不可避免,但可以通过合并回源、分层回源、缓存预热等手段降低回源率
  • 前端项目中,HTML 不缓存 + 静态资源带 hash 长缓存是最经典的 CDN 使用模式
  • CDN 缓存和浏览器缓存是两层独立的缓存,通过 max-ages-maxage 分别控制,配合使用效果最佳