本文是 CDN 系列的原理篇,关注请求如何被调度、响应何时可以复用,以及未命中和故障如何传播。架构取舍见《内容分发体系设计:CDN 架构决策与全球化落地》,接入操作见《CDN 接入与运营实战》。
CDN 优化的是请求路径与重复工作
用户与源站相距很远时,传播、建连和回源处理都可能增加延迟。光纤传播速度可以用来估算物理下限,但实际线路长度、路由和连接复用决定最终体验。现代浏览器会并发请求与复用连接,不能把 50 个资源的完整握手时间简单相加。
CDN 将可复用响应放到靠近用户的网络位置,并为未命中或动态请求提供代理路径。这里的“近”首先是网络路径合适,不一定是地图上距离最短。效果取决于命中状态、内容大小、源站处理和用户网络,不能承诺统一的延迟降幅。
下面以一个内容社区的公开文章、版本化前端资源和登录接口为例,区分 CDN 的三个职责:调度请求、复用响应、控制回源。
边缘与中间层分别减少什么
边缘节点直接面向用户。中间层或 Origin Shield 聚合多个边缘的未命中流量,源站提供权威内容。这是一种常见结构,并非所有产品都固定包含三层。
假设 200 个边缘同时请求同一个新对象,且请求能按相同缓存键合并、对象允许缓存、没有失败重试,四个独立中间层可以将回源收敛到各一次。若希望再合成一次,还需要共同的上层缓存或协调点,四个独立节点不会自动彼此合并。
边缘数量增加可以改善覆盖,也可能让每个节点的内容访问变得更稀疏。中间层通过聚合需求改善复用机会,但也增加路径与费用。需要比较未命中延迟、源站负载和实际成本。
源站容量不能只按平时平均命中率设计。新内容发布、缓存清除、绕过缓存的接口以及区域故障,都可能产生更高的回源压力。
调度:DNS、重定向与 Anycast 可以组合
DNS 调度通常通过 CNAME 或服务商支持的别名机制,将业务域名连接到 CDN 的解析体系。权威 DNS 通常看到递归解析器地址,支持 ECS 时还可能获得客户端网络前缀;不能默认拿到精确用户 IP。缓存与 TTL 也限制了调整速度。
HTTP 重定向在应用层选择另一个 URL,可以利用请求信息,但会增加请求步骤。还要保持签名、跨域、证书与缓存语义,不能只比较一次往返成本。
Anycast 则让多个位置通告同一地址前缀,由网络路由选择路径。它仍可能需要 DNS 来得到这个 IP,BGP 的选择也不是严格按物理距离或实时延迟排序。路由变化影响连接的方式,取决于协议和服务实现。
| 机制 | 能观察或控制的范围 | 不能默认保证 |
|---|---|---|
| DNS 调度 | 解析响应、地域或健康策略 | 每个用户的精确位置、即时切换 |
| HTTP 重定向 | 请求级 URL 与业务规则 | 零额外延迟、签名自动兼容 |
| Anycast | 网络前缀与路由路径 | 无需 DNS、所有流量均匀分散 |
健康检测与调度还需要防抖和容量约束。把流量从异常节点切走,只在目标节点有容量且资源已准备时才有帮助。
缓存键决定哪些响应可以共享
两次请求能否共享响应,不由文件后缀单独决定,而由身份、请求参数、内容协商和响应缓存策略共同决定。
公开文章可能按语言生成不同版本;私有图片即使以 .jpg 结尾,也不能未经鉴权共享。若源站响应受某个参数影响,缓存键却忽略它,就可能返回错误内容。反过来,追踪参数不影响内容时,可以在验证这一前提后从缓存键中移除。
Nginx 的缓存键示例可以明确分隔字段:
proxy_cache_key "$scheme|$host|$request_uri";
这只是键的定义,不是完整缓存配置。若改为只保留部分参数,需要同时限制参数语法、处理重复参数和缺省值,并确保源站看到的请求与归一化规则一致。拼接 $uri$arg_id 而没有边界,还可能让不同字段组合形成同一个字符串。Nginx 缓存模块
Vary 表达响应依赖哪些请求头;CDN 是否按相应头区分,需要核对产品策略。过宽的 User-Agent 变化会增加变体数量,但也不能简单删除它而混用不同内容。设备类别应由可信逻辑归一化,并与源站生成逻辑保持一致。
新鲜度、验证与存储是不同动作
| 指令 | 作用 |
|---|---|
max-age |
定义响应的新鲜期 |
s-maxage |
定义共享缓存的新鲜期,优先于其适用的 max-age |
no-cache |
可保存,但重用前需要成功验证 |
no-store |
不允许缓存存储本次响应 |
private |
不允许共享缓存保存完整响应,可用于私有缓存 |
验证可能得到 304,也可能得到新响应;no-cache 不等于“不回源”。Age 是响应当前年龄的估计,并不是某个边缘节点保存该对象的精确时长。带 Authorization 的请求存在明确的共享缓存条件,Cookie 本身也不自动定义所有产品的行为;业务默认绕过登录响应的共享缓存更便于控制,但这是策略选择。HTTP 缓存规范 RFC 9111
控制台规则、最小 TTL 和边缘代码可能改变源站头的效果。应同时核对产品文档和实际响应,例如 Cloudflare 有自己的默认可缓存类型与响应头处理规则,不能把一家厂商的行为推广到全部 CDN。Cloudflare 默认缓存行为
内容更新:版本化与失效分别承担什么
对构建产物使用内容哈希 URL,可以让不同内容拥有不同地址。新 HTML 引用新地址后,客户端获取新文件;旧地址仍指向旧版本,并没有“自然立即失效”。
因此发布顺序应该是先准备新资源、验证可获取,再更新入口。旧资源应保留足够时间,让尚未更新的入口和在途会话继续工作。HTML、Service Worker 与浏览器缓存仍有自己的更新路径。
Purge 用于失效 CDN 保存的内容,其覆盖范围和传播时间依产品而异。它通常不能远程删除浏览器已保存的响应,也不能让正在运行的页面自动更新。紧急修复需要同时处理入口、资源版本及已经暴露的服务行为。
允许短暂陈旧的公开内容可以使用 stale-while-revalidate;它允许在指定陈旧窗口内复用旧响应并尝试后台验证,不保证每次都无等待,也不保证无请求时会主动刷新。stale-if-error 允许在指定错误条件和时间范围内使用旧响应。RFC 5861
Soft Purge 通常将对象标记为陈旧,后续是否能继续返回旧值还取决于策略。撤销访问权限、下架敏感内容等场景不能用“继续提供旧响应”满足即时撤销要求。
回源合并:共享任务,也要共享失败结果
同一个节点中,相同可共享资源的并发未命中,可以等待一次回源。它降低重复工作,但必须处理回源异常和等待者取消;只在成功时完成 Future,会让失败时的等待者一直挂起。
下面是 Python 3.9+ 单事件循环中的教学实现,只演示在途任务合并,不实现缓存、鉴权或完整 CDN:
import asyncio
class SingleFlight:
def __init__(self, fetch, timeout=5.0, max_pending=100):
if timeout <= 0 or max_pending <= 0:
raise ValueError("limits must be positive")
self.fetch = fetch
self.timeout = timeout
self.max_pending = max_pending
self.pending = {}
async def get(self, key):
task = self.pending.get(key)
if task is None:
if len(self.pending) >= self.max_pending:
raise RuntimeError("origin concurrency exhausted")
task = asyncio.create_task(
asyncio.wait_for(self.fetch(key), timeout=self.timeout)
)
self.pending[key] = task
def done(completed):
if self.pending.get(key) is completed:
self.pending.pop(key)
if not completed.cancelled():
completed.exception()
task.add_done_callback(done)
return await asyncio.shield(task)
shield 防止单个等待者取消时顺带取消共享回源;回源本身受超时约束。完成回调清理在途表并取出异常,即使所有等待者都离开也不会留下未处理异常。真实系统仍需限制等待者数量与响应体大小,选择哪些错误可重试,并保证被合并请求确实有相同权限和表示。
多源站重试还要考虑方法语义。GET 获取不可变资源与 POST 创建订单不是同一类操作,CDN 不能在结果未知时任意向另一源站重放写请求。
大文件、动态请求与安全需要额外检查
大文件可以通过流式响应尽早发送,也可以使用 Range 获取字节范围,不必等完整文件下载完才开始返回。分片缓存应结合版本、强验证器与范围信息,避免把不同版本的字节拼接成一个文件。
动态加速主要优化代理路径、连接复用与传输;它不必然使用专线,也不保证消除公网拥塞。TLS 1.3 的首次握手与恢复连接不同,0-RTT 需要满足恢复条件且存在重放风险。QUIC 减少跨流的传输层队头阻塞,不等于不存在拥塞、应用依赖或任何队头阻塞。
边缘计算适合部分改写、鉴权和就近计算,但不同产品的运行时、网络访问、时限和触发阶段不同,不能把 CloudFront Functions、Lambda@Edge 与 Workers 当作可互换的 SSR 平台。
CDN 上的 WAF、限速和 DDoS 防护可以减少恶意流量到达源站,但无法保证所有攻击被识别。源站暴露、绕过 CDN 的路径和错误鉴权也需要处理。HTTPS 回源应校验证书与名称;私有 CA 应建立受信任链,不以关闭验证作为常规方案。
验证原理是否落实在实际链路
同一个 URL 的两次请求可能命中不同节点,也可能因策略不可缓存;“第二次必须 HIT”不是通用判据。应结合厂商缓存状态、Age、对象版本、实际正文和源站日志观察。
请求命中率与字节命中率要分开。一个小图片命中和一个大视频命中,减少的字节量不同;回源请求也可能只传 304,不能用请求命中率直接计算带宽费用。
定位问题时,低命中率只提示检查内容访问分布、缓存键、过期和发布事件。回源错误也可能来自 DNS、TLS、网络或 CDN 到源站的配置,不能直接宣布源站故障。正确的排查应提出能够区分这些解释的检查,再根据结果缩小范围。
CDN 的核心能力,是让可共享的响应在合适的位置复用,并让不能复用的请求走可控路径。缓存键、新鲜度、失效和回源共同决定这件事是否成立;节点数量和命中率数字只能说明其中一部分。