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 间接省钱的地方在于:
- 回源流量减少 95%——OSS 外网流量费大幅降低
- 源站压力减少——不需要高配置服务器
- 用户留存提升——加载速度影响转化率
本节小结
✅ 我学到了什么:
- 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 服务商就能解决大部分问题。
