CDN 技术体系:从分发原理到工程落地

目录

本文是 CDN 系列的原理篇,关注请求如何被调度、响应何时可以复用,以及未命中和故障如何传播。架构取舍见《内容分发体系设计:CDN 架构决策与全球化落地》,接入操作见《CDN 接入与运营实战》。

CDN 优化的是请求路径与重复工作

用户与源站相距很远时,传播、建连和回源处理都可能增加延迟。光纤传播速度可以用来估算物理下限,但实际线路长度、路由和连接复用决定最终体验。现代浏览器会并发请求与复用连接,不能把 50 个资源的完整握手时间简单相加。

CDN 将可复用响应放到靠近用户的网络位置,并为未命中或动态请求提供代理路径。这里的“近”首先是网络路径合适,不一定是地图上距离最短。效果取决于命中状态、内容大小、源站处理和用户网络,不能承诺统一的延迟降幅。

下面以一个内容社区的公开文章、版本化前端资源和登录接口为例,区分 CDN 的三个职责:调度请求、复用响应、控制回源。

边缘与中间层分别减少什么

请求逐层查找缓存,命中时返回,未命中才继续访问源站

边缘节点直接面向用户。中间层或 Origin Shield 聚合多个边缘的未命中流量,源站提供权威内容。这是一种常见结构,并非所有产品都固定包含三层。

假设 200 个边缘同时请求同一个新对象,且请求能按相同缓存键合并、对象允许缓存、没有失败重试,四个独立中间层可以将回源收敛到各一次。若希望再合成一次,还需要共同的上层缓存或协调点,四个独立节点不会自动彼此合并。

边缘数量增加可以改善覆盖,也可能让每个节点的内容访问变得更稀疏。中间层通过聚合需求改善复用机会,但也增加路径与费用。需要比较未命中延迟、源站负载和实际成本。

源站容量不能只按平时平均命中率设计。新内容发布、缓存清除、绕过缓存的接口以及区域故障,都可能产生更高的回源压力。

调度:DNS、重定向与 Anycast 可以组合

DNS 返回可用入口后,客户端再向 CDN 建立内容请求

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 的核心能力,是让可共享的响应在合适的位置复用,并让不能复用的请求走可控路径。缓存键、新鲜度、失效和回源共同决定这件事是否成立;节点数量和命中率数字只能说明其中一部分。

评论