导航菜单

CDN 原理

广州用户的 3.2 秒

“光影”的 OSS 源站在北京。有一天我闲着没事,用 curl 测试了一下从不同地区访问同一张图片的延迟:

# 从北京访问(OSS 源站在北京)
curl -o /dev/null -s -w "DNS: %{time_namelookup}s\nConnect: %{time_connect}s\nTTFB: %{time_starttransfer}s\nTotal: %{time_total}s\n" \
  "https://guangying-images.oss-cn-beijing.aliyuncs.com/thumbs/medium_webp/2024/06/a3/abc.webp"

# 结果:
# DNS:        0.005s
# Connect:    0.008s
# TTFB:       0.045s
# Total:      0.062s   ← 62ms,北京用户

# 从广州访问(通过朋友帮忙测试)
# DNS:        0.035s
# Connect:    0.045s
# TTFB:       0.380s
# Total:      3.200s   ← 3.2s,广州用户!

# 从纽约访问(通过海外 VPS 测试)
# DNS:        0.120s
# Connect:    0.185s
# TTFB:       0.820s
# Total:      8.500s   ← 8.5s,海外用户!

为什么差距这么大?因为光速

# 物理延迟的计算
speed_of_light = 299_792  # km/s(光速)
fiber_refraction = 1.5     # 光纤折射率(光在光纤中速度约为真空的 2/3)
effective_speed = speed_of_light / fiber_refraction  # ≈ 200,000 km/s

# 北京到广州的距离
distance_beijing_guangzhou = 2000  # km(直线距离约 1900km,光纤路径约 2200km)

# 单程延迟(RTT 的一半)
one_way_delay = distance_beijing_guangzhou / effective_speed
# = 2000 / 200000 = 0.01s = 10ms

# 但实际网络不是直线,经过多个路由器,每跳都有处理延迟
# 实际 RTT 通常 = 物理延迟 × 3~5
actual_rtt = one_way_delay * 2 * 4  # RTT,考虑路由跳数
# = 80ms

# TCP 三次握手 + HTTP 请求 = 至少 2 个 RTT
# 80ms × 2 = 160ms(纯网络延迟)

# 再加上:
# - 服务器处理时间:50ms
# - 图片传输时间:50KB / (5Mbps / 8) = 80ms
# - TCP 慢启动(小文件影响不大)
# 总计:约 300~400ms

# 对于 3.2s 的结果,可能是:
# - 网络拥堵导致 RTT 更高
# - TCP 慢启动的多次往返
# - DNS 解析延迟
# - 网络中间链路的队列延迟

物理距离 + 网络跳数 + TCP 协议开销 = 远距离用户不可避免的延迟。

CDN 的核心思路很简单:既然不能缩短距离,就把数据搬到离用户更近的地方。

CDN 是怎么工作的?

一次完整的 CDN 访问流程:

用户在广州,输入 cdn.guangying.com


① DNS 解析
    用户 → Local DNS → 权威 DNS
    权威 DNS 返回:CNAME 到 CDN 的 DNS 域名
    CDN DNS 根据"广州"返回广州边缘节点的 IP


② 建立连接
    用户 → 广州边缘节点(物理距离 ~10km,RTT ~2ms)


③ 缓存查找
    广州边缘节点检查:这个 URL 有缓存吗?

    ├─ 有缓存 ──→ 直接返回 ✅(~5ms)

    └─ 无缓存 ──→ 回源到北京源站

                    ④ 回源请求
                    广州节点 → 北京源站(RTT ~40ms)
                    获取图片 → 缓存到广州节点 → 返回用户
                    总耗时:~100ms(第一次)
                    


⑤ 后续请求
    广州用户再次访问同一张图 → 广州边缘节点直接返回(~5ms)
    深圳用户访问同一张图 → 可能命中广州节点的缓存(~10ms)

DNS 解析的魔法

CDN 的关键在于 DNS 解析——它怎么知道用户在广州?

# 没有 CDN 的 DNS 解析
"""
用户访问 guangying-images.oss-cn-beijing.aliyuncs.com
  → DNS 返回北京 OSS 的 IP: 47.95.xxx.xxx
  → 用户直连北京服务器
  → 延迟 300ms+
"""

# 使用 CDN 后的 DNS 解析
"""
用户访问 cdn.guangying.com
  → CNAME 到 guangying.com.cdn30.org(CDN 厂商的域名)
  → CDN 厂商的智能 DNS(GSLB)介入
  → GSLB 根据"用户 Local DNS 的 IP"判断用户位置
  → 返回最近的边缘节点 IP

  广州用户的 Local DNS 是 119.29.xx.xx(广州电信)
  → GSLB 判断:这是广州电信的用户
  → 返回广州电信节点的 IP: 119.29.yy.yy
"""

智能 DNS(GSLB)的判断依据:

判断"用户在哪里"的数据来源:

1. 用户 Local DNS 的 IP 地址
   → 精确到运营商 + 城市
   → 例如:119.29.xx.xx = 广东电信

2. EDNS Client Subnet(ECS)
   → 新版 DNS 协议,把用户真实 IP 传给权威 DNS
   → 比依赖 Local DNS IP 更准确

3. Anycast 路由
   → 同一个 IP 在全球多节点宣告
   → BGP 路由自动选择最近的节点

动手实验:接入 CDN

我在阿里云上开了一个 CDN 服务,把 cdn.guangying.com 指向 OSS 源站:

# CDN 配置(通过阿里云 SDK)
from aliyunsdkcdn.request.v20180510 import AddCdnDomainRequest

def setup_cdn():
    """配置 CDN 域名"""
    request = AddCdnDomainRequest.AddCdnDomainRequest()
    
    request.set_CdnType('web')                    # web = 网页/下载加速
    request.set_DomainName('cdn.guangying.com')   # CDN 域名
    request.set_Sources([
        {
            'type': 'oss',                         # 源站类型:OSS
            'content': 'guangying-images.oss-cn-beijing.aliyuncs.com',
            'priority': '20',
            'weight': '10',
        }
    ])
    
    # 缓存配置
    request.set_CacheConfig({
        'CacheContent': [
            # 原图缓存 30 天
            {'Path': '/originals/', 'TTL': 2592000, 'Type': 'directory'},
            # 缩略图缓存 7 天(可能重新生成)
            {'Path': '/thumbs/', 'TTL': 604800, 'Type': 'directory'},
        ]
    })
    
    response = client.do_action_with_exception(request)
    return json.loads(response)

CDN 配好后,我重新测试了延迟:

# 配置 CDN 后的测试

# 广州用户访问 CDN
curl -o /dev/null -s -w "DNS: %{time_namelookup}s\nConnect: %{time_connect}s\nTTFB: %{time_starttransfer}s\nTotal: %{time_total}s\n" \
  "https://cdn.guangying.com/thumbs/medium_webp/2024/06/a3/abc.webp"

# 第一次请求(缓存未命中,回源):
# DNS:    0.025s
# Connect:0.008s   ← 连接到广州边缘节点,很近
# TTFB:   0.095s   ← 回源到北京 + 缓存
# Total:  0.120s   ← 比 OSS 直连的 3.2s 快 26 倍

# 第二次请求(缓存命中):
# DNS:    0.005s
# Connect:0.003s
# TTFB:   0.006s   ← 从广州节点直接返回
# Total:  0.010s   ← 10ms!

效果对比

图片加载延迟对比(50KB WebP 缩略图)

地区     OSS 直连      CDN(首次)    CDN(命中)   提升
────────────────────────────────────────────────────────
北京      62ms          55ms          5ms          12倍
上海      180ms         90ms          8ms          22倍
广州      3200ms        120ms         10ms         320倍
成都      2800ms        100ms         8ms          350倍
纽约      8500ms        500ms         30ms         283倍

缓存命中后,所有地区都在 10~30ms 内完成。

回源策略

缓存不会永远存在。当缓存过期或被淘汰时,CDN 节点需要回到源站获取图片。

# CDN 回源策略配置
class CDNOriginConfig:
    """CDN 回源配置"""
    
    def configure(self):
        return {
            # 回源协议
            'origin_protocol': 'https',    # 回源走 HTTPS
            
            # 回源超时
            'origin_connect_timeout': 10,   # 连接超时 10s
            'origin_read_timeout': 30,      # 读取超时 30s
            
            # 回源重试
            'retry_count': 2,               # 失败重试 2 次
            'retry_delay': 1,               # 重试间隔 1s
            
            # 回源 HTTP 头(传给源站的额外信息)
            'origin_headers': {
                'X-CDN-Node': '$remote_addr',   # 告诉源站哪个节点在回源
                'X-Client-IP': '$http_x_forwarded_for',  # 传递用户真实 IP
            },
            
            # 回源限速(防止突发回源压垮源站)
            'origin_rate_limit': '100Mbps',
        }

回源优化:回源跟随 301

# 优化:CDN 回源时,如果源站返回 301/302 重定向,CDN 会自动跟随
# 而不是把 301 返回给用户(节省一次往返)

"""
没有回源跟随:
用户 → CDN → OSS → CDN → 用户
                   ↑ 301 → 用户 → CDN → 新地址 → 用户
                   3 次 RTT

有回源跟随:
用户 → CDN → OSS → CDN 跟随 301 → 获取图片 → 缓存 → 返回用户
                   1 次 RTT(对用户透明)
"""

CDN 缓存命中率

缓存命中率是 CDN 的核心指标:

# 缓存命中率监控
class CDNMonitor:
    """CDN 缓存命中率监控"""
    
    def get_cache_stats(self, period='24h'):
        """获取缓存命中率统计"""
        # 从 CDN API 获取统计数据
        stats = cdn_client.describe_domain_usage_data(
            domain='cdn.guangying.com',
            start_time=datetime.now() - timedelta(hours=24),
            end_time=datetime.now(),
        )
        
        total_requests = stats['TotalRequests']
        hit_requests = stats['HitRequests']
        
        hit_rate = hit_requests / total_requests * 100 if total_requests > 0 else 0
        
        return {
            'total_requests': total_requests,
            'hit_requests': hit_requests,
            'miss_requests': total_requests - hit_requests,
            'hit_rate': round(hit_rate, 2),
            'bandwidth_saved_gb': stats['HitTraffic'] / 1024 / 1024 / 1024,
        }

# 理想的缓存命中率
"""
目标:缓存命中率 ≥ 95%

分析"光影"的访问模式:
- 热门图片(日访问 > 1000 次):命中率 99.9%
  → 几乎永远在 CDN 缓存中
  
- 中等图片(日访问 10~1000 次):命中率 98%
  → 偶尔被淘汰,回源一次后重新缓存
  
- 冷门图片(日访问 < 10 次):命中率 70%
  → 经常被淘汰,回源频繁

提高命中率的策略:
1. 合理设置缓存过期时间(图片不常变,可以设置较长 TTL)
2. 合理设计缓存 key(避免相同内容有多个 key)
3. CDN 节点容量足够(避免频繁淘汰)
"""

CDN 的成本

# CDN 成本分析
class CDNCostAnalyzer:
    """CDN 成本分析"""
    
    def monthly_cost(self, monthly_traffic_tb: float):
        """计算月度 CDN 成本"""
        # 阿里云 CDN 阶梯计价
        tiers = [
            (0,     10,    0.24),    # 0~10TB,0.24 元/GB
            (10,    50,    0.22),    # 10~50TB,0.22 元/GB
            (50,    100,   0.20),    # 50~100TB,0.20 元/GB
            (100,   1000,  0.18),    # 100TB~1PB,0.18 元/GB
        ]
        
        traffic_gb = monthly_traffic_tb * 1024
        total_cost = 0
        remaining = traffic_gb
        
        for low, high, price in tiers:
            tier_traffic = min(remaining, (high - low) * 1024)
            if tier_traffic <= 0:
                continue
            total_cost += tier_traffic * price
            remaining -= tier_traffic
            if remaining <= 0:
                break
        
        return round(total_cost, 2)

# 月度流量估算
daily_page_views = 10000          # 每天 1 万次页面访问
images_per_page = 10              # 每页 10 张图
avg_image_size_kb = 50            # 平均 50KB/张
cache_hit_rate = 0.95             # 缓存命中率 95%

daily_cdn_traffic_gb = (daily_page_views * images_per_page * avg_image_size_kb) / 1024 / 1024
# = 4.77 GB/天

# 只有不命中的 5% 才产生回源流量
daily_origin_traffic_gb = daily_cdn_traffic_gb * (1 - cache_hit_rate)
# = 0.24 GB/天

monthly_cdn_traffic_tb = daily_cdn_traffic_gb * 30 / 1024
# = 0.14 TB/月

analyzer = CDNCostAnalyzer()
cdn_cost = analyzer.monthly_cost(monthly_cdn_traffic_tb)
# ≈ 34 元/月

# 对比直连 OSS 的流量费
oss_traffic_cost = daily_origin_traffic_gb * 30 * 0.50  # OSS 外网流量 0.50 元/GB
# ≈ 3.6 元/月(回源流量远小于直连流量)

print(f"CDN 流量费: {cdn_cost} 元/月")
print(f"OSS 回源费: {oss_traffic_cost:.1f} 元/月")
print(f"如果不用 CDN,OSS 直连: {daily_cdn_traffic_gb * 30 * 0.50:.1f} 元/月")
print(f"节省: {daily_cdn_traffic_gb * 30 * 0.50 - cdn_cost - oss_traffic_cost:.1f} 元/月")

等等——CDN 流量费比 OSS 直连还贵?是的,CDN 本身不省钱,它省的是用户体验

但 CDN 间接省钱的地方在于:

  1. 回源流量减少 95%——OSS 外网流量费大幅降低
  2. 源站压力减少——不需要高配置服务器
  3. 用户留存提升——加载速度影响转化率

本节小结

我学到了什么

  • CDN 的核心原理是 DNS 智能解析 + 边缘缓存 + 回源
  • 广州用户从 3.2 秒降到 10 毫秒——CDN 的效果是数量级的
  • 缓存命中率是 CDN 的核心指标,目标 ≥ 95%
  • CDN 不直接省钱,但通过减少回源流量和提升用户体验间接降低成本

⚠️ 踩过的坑

  • CDN DNS 解析有时不准确(如果用户用了 8.8.8.8 等 Public DNS,可能被解析到错误节点)
  • 首次回源仍然有延迟——需要做缓存预热
  • CDN 缓存刷新有延迟(通常 5~10 分钟全节点生效)

🎯 下一步:CDN 接入了,但如何让 CDN 更智能?URL 参数化实时裁剪——用户请求 ?w=800&q=80 就返回对应尺寸的图片。

我的思考

思考 1

如果 CDN 缓存了违规图片(审核通过后被判定为违规),如何快速从所有 CDN 节点清除?

参考答案

这叫做”缓存刷新”(Cache Purge),是 CDN 运维的关键操作。

紧急刷新流程

class UrgentCachePurge:
    """紧急缓存刷新"""
    
    def purge_image(self, object_key: str):
        """从所有 CDN 节点清除指定图片"""
        url = f'https://cdn.guangying.com/{object_key}'
        
        # 方法 1:URL 刷新(精确,推荐)
        cdn_client.refresh_object_caches(
            object_path=url,
            object_type='File',   # File = 单个 URL
        )
        # 通常 5 分钟内全网生效
        
        # 方法 2:目录刷新(范围更大)
        # 如果不确定有哪些 URL 变体(?w=800, ?w=400 等)
        cdn_client.refresh_object_caches(
            object_path=f'https://cdn.guangying.com/thumbs/{object_key.split("/")[1]}/',
            object_type='Directory',
        )
        
        # 方法 3:URL 改写(最安全)
        # 更新图片 URL(加版本号),旧 URL 自然过期
        # 新 URL: https://cdn.guangying.com/thumbs/...?v=2
        # 旧 URL 仍然缓存着违规图片,但没人会访问了

更安全的方案:版本化 URL

# 每次更新图片时,改变 URL 中的版本号
def get_image_url(object_key, version):
    return f'https://cdn.guangying.com/{object_key}?v={version}'

# 违规图片不需要刷新缓存——只要数据库里的 version 变了,
# 用户就不会访问旧 URL,旧缓存自然过期后会被清除。

紧急情况的处理优先级

1 分钟内:标记图片为不可访问(API 层面拦截)
5 分钟内:CDN 缓存刷新完成
1 小时内:确认全网节点已刷新
24 小时内:Review 原因,防止再次发生

思考 2

如果用户的 Local DNS 配置错误(比如广州用户用了北京的 DNS 服务器),CDN 还能正确就近分发吗?

参考答案

这是 CDN 的经典问题——DNS 定位不准

问题场景:
用户在广州,但 Local DNS 设为了 114.114.114.114(江苏)
→ CDN 的 GSLB 根据 Local DNS IP 判断用户在江苏
→ 分配南京节点(而不是广州节点)
→ 延迟比最优路径高 20~30ms

解决方案

1. EDNS Client Subnet(ECS)

RFC 7871 定义的 DNS 扩展
Local DNS 在查询时附加用户真实 IP 的前 24 位
CDN 的 GSLB 根据用户真实 IP 而不是 Local DNS IP 做判断

用户 IP:119.29.xx.xx(广州)
Local DNS:114.114.114.114(江苏)
ECS:119.29.0.0/24

GSLB 看到 119.29.0.0/24 → 判断用户在广州 → 分配广州节点 ✅

2. Anycast + BGP 路由

同一个 IP 在全球多个节点宣告
BGP 协议自动选择最短路径
不依赖 DNS 定位

用户发出请求 → BGP 路由自动走向最近的节点
完全不受 Local DNS 影响

3. HTTP 重定向

1. 用户访问 CDN
2. CDN 返回一个探测 URL
3. 浏览器同时请求多个节点的探测 URL
4. 哪个最快就用哪个节点
5. 后续请求直接走最快节点

Cloudflare 的"Argo Smart Routing"就用了类似策略。

对于”光影”的规模,使用支持 ECS 的 CDN 服务商就能解决大部分问题。

搜索