2024年11月05日8 分钟

CDN 原理与实践:从用户请求到内容分发的全过程

深入理解 CDN 的工作原理,涵盖 DNS 调度、边缘节点、缓存策略和前端项目实战配置

什么是 CDN

CDN(Content Delivery Network,内容分发网络)是一组分布在全球各地的服务器,它们缓存你网站的静态资源,让用户从最近的节点获取内容,而不是从遥远的源服务器获取。

没有 CDN:
  北京用户 ──────────── 跨太平洋 ──────────── 美国源服务器
  延迟: 200-400ms

有 CDN:
  北京用户 ──── 北京 CDN 节点(缓存副本)
  延迟: 5-20ms

CDN 请求的完整流程

用户输入 https://cdn.example.com/bundle.js
           │
     ┌─────▼──────┐
     │  浏览器 DNS  │
     │  解析请求    │
     └─────┬──────┘
           │
     ┌─────▼──────┐
     │ 本地 DNS    │  查询 cdn.example.com
     │ (ISP递归)   │
     └─────┬──────┘
           │
     ┌─────▼──────────────┐
     │ 权威 DNS 返回 CNAME │  cdn.example.com → cdn.provider.net
     └─────┬──────────────┘
           │
     ┌─────▼──────────────┐
     │ CDN 智能 DNS 调度   │  根据用户 IP 判断地理位置
     │                     │  选择最优节点
     │ 考虑因素:           │
     │ - 地理距离           │
     │ - 节点负载           │
     │ - 网络质量           │
     │ - 节点健康状态        │
     └─────┬──────────────┘
           │  返回最优节点 IP: 1.2.3.4
           ▼
     ┌──────────────┐
     │ 浏览器请求     │  GET /bundle.js → 1.2.3.4(北京边缘节点)
     │ 边缘节点       │
     └──────┬───────┘
            │
      ┌─────▼─────┐
      │ 缓存命中?  │
      ├─── 是 ────▶ 直接返回缓存(5ms)✅
      │             │
      └─── 否 ────▶ 回源请求
                    │
              ┌─────▼────────┐
              │  中间层缓存    │  区域中心节点
              │  命中?       │
              ├── 是 ────────▶ 返回 + 缓存到边缘
              │               │
              └── 否 ────────▶ 请求源服务器
                              │
                        ┌─────▼────┐
                        │ 源服务器   │
                        │ 返回资源   │
                        └─────┬────┘
                              │
                    逐级缓存回填
                    源 → 中间层 → 边缘节点
                              │
                              ▼
                    返回给用户 ✅

CDN 的核心机制

1. DNS 调度(智能选路)

用户在北京请求 static.example.com:

CDN DNS 系统收到请求:
  用户 IP: 202.106.x.x(北京联通)
  │
  ├─ 查地理数据库 → 北京
  ├─ 查北京节点列表:
  │   节点A(联通): 负载 40%,延迟 5ms  ← 最优
  │   节点B(电信): 负载 20%,延迟 15ms
  │   节点C(移动): 负载 60%,延迟 12ms
  │
  └─ 返回节点A 的 IP

结果:北京联通用户 → 北京联通 CDN 节点(同运营商,最快)

2. 缓存策略

HTTP 缓存头控制 CDN 行为:

Cache-Control: public, max-age=31536000
  │              │              │
  │              │              └── 缓存 1 年(365 天)
  │              └── 可以被 CDN 缓存
  └── 缓存控制头

Cache-Control: no-cache
  └── 每次都要向源服务器验证(但可以缓存)

Cache-Control: no-store
  └── 完全不缓存(CDN 直接透传)

3. 缓存层级

                 ┌─────────────┐
                 │   源服务器    │  存储原始文件
                 └──────┬──────┘
                        │
              ┌─────────┼─────────┐
              │         │         │
        ┌─────▼───┐ ┌───▼────┐ ┌──▼──────┐
        │ 华北中心  │ │华东中心 │ │ 华南中心  │  L2 缓存(区域)
        └────┬────┘ └───┬────┘ └────┬────┘
             │          │           │
       ┌─────┼────┐     │     ┌────┼─────┐
       │     │    │     │     │    │     │
     ┌─▼─┐ ┌▼─┐ ┌▼─┐ ┌▼─┐ ┌▼─┐ ┌▼─┐ ┌▼──┐
     │北京│ │天│ │济│ │上│ │广│ │深│ │成都│  L1 边缘节点
     └───┘ │津│ │南│ │海│ │州│ │圳│ └───┘
           └──┘ └──┘ └──┘ └──┘ └──┘

边缘节点没有 → 找区域中心
区域中心没有 → 找源服务器
找到后逐级缓存回填

前端项目的 CDN 配置

Next.js + Vercel

// next.config.js
module.exports = {
  // Vercel 自动为静态资源启用 CDN
  // _next/static/ 下的文件自动缓存 1 年

  // 自定义 CDN 域名
  assetPrefix: process.env.NODE_ENV === 'production'
    ? 'https://cdn.example.com'
    : '',

  // 图片 CDN
  images: {
    domains: ['cdn.example.com'],
    loader: 'custom',
    loaderFile: './lib/image-loader.js',
  },
}

缓存策略配置

// next.config.js — headers 配置
module.exports = {
  async headers() {
    return [
      {
        // 带 hash 的静态资源:缓存 1 年
        source: '/_next/static/:path*',
        headers: [
          {
            key: 'Cache-Control',
            value: 'public, max-age=31536000, immutable',
          },
        ],
      },
      {
        // HTML 页面:每次检查更新
        source: '/:path*',
        headers: [
          {
            key: 'Cache-Control',
            value: 'public, max-age=0, must-revalidate',
          },
        ],
      },
      {
        // API 接口:不缓存
        source: '/api/:path*',
        headers: [
          {
            key: 'Cache-Control',
            value: 'no-store',
          },
        ],
      },
    ]
  },
}

文件名 Hash 策略

为什么静态资源可以缓存 1 年?
因为文件名包含内容 hash:

bundle.a1b2c3d4.js ← 内容变化 → hash 变化 → 文件名变化 → 新的 URL

旧版本:bundle.a1b2c3d4.js(缓存 1 年,不影响)
新版本:bundle.e5f6g7h8.js(全新 URL,CDN 重新获取)

这就是为什么 Next.js 的 _next/static 文件可以设置超长缓存
HTML 引用的是新文件名,自然加载新版本

CDN 的缓存失效

主动刷新

# 使用 CDN 提供商的 API 主动清除缓存
# Cloudflare 示例
curl -X POST "https://api.cloudflare.com/client/v4/zones/{zone_id}/purge_cache" \
  -H "Authorization: Bearer {api_token}" \
  -d '{"files":["https://example.com/styles.css"]}'

# 全量刷新(谨慎使用,会导致所有节点回源)
curl -X POST ... -d '{"purge_everything":true}'

版本化 URL

最佳实践:不清除缓存,而是使用新 URL

v1: https://cdn.example.com/app.v1.js
v2: https://cdn.example.com/app.v2.js

旧版本继续缓存(不影响还在使用的用户)
新版本自动从源站获取

CDN 常见问题

缓存不一致

问题:不同节点缓存了不同版本
  北京节点:bundle.js(v2,刚刷新)
  上海节点:bundle.js(v1,还没过期)

解决:
  1. 使用文件名 hash → 不同版本就是不同文件
  2. 缓存 purge API → 全节点刷新
  3. 合理设置 TTL → 短的缓存时间用于频繁变更的资源

回源风暴

问题:缓存同时过期 → 大量请求涌向源服务器

  1000 个请求同时到达 → 缓存都过期
  → 1000 个请求全部回源 → 源服务器崩溃

解决:
  1. 缓存预热:上线前主动推送到 CDN 节点
  2. 回源合并:相同 URL 的回源请求只发一个
  3. 过期时间随机化:避免同时过期

CDN 选型对比

CDN 服务特点适用场景
Cloudflare免费额度大,全球节点个人网站、中小项目
Vercel EdgeNext.js 原生集成Next.js 项目
AWS CloudFront与 AWS 生态集成已用 AWS 的项目
阿里云 CDN国内节点多国内用户为主
腾讯云 CDN国内节点多国内用户为主

总结

CDN 的核心价值:

  1. 智能 DNS 调度:把用户导向最近、最快的节点
  2. 分级缓存:边缘 → 区域 → 源站,减少回源
  3. 缓存策略:Hash 文件名 + 长期缓存 = 最佳方案
  4. 前端实践:静态资源长缓存 + HTML 短缓存/不缓存