跳到内容

第 4 卷 / 第 7 章 / 第 2 课

7.2 CDN 缓存、回源与流量导向 ​

大量远程用户下载同一份静态资源时,源站负载迅速上升。系统需要决定哪些响应可缓存、哪些副本应靠近用户,以及何时回源验证。

CDN 在多个边缘站点提供内容与请求处理。缓存命中时,它有机会缩短路径并减轻源站负载,也可终止 TLS、路由请求和执行访问策略。收益取决于可缓存性、流量映射与源站设计,接入后仍需测量。

1. 一次边缘请求的路径 ​

text
client
  |
  | DNS answer / anycast route
  v
edge point of presence
  |-- fresh cache hit -----------------> response
  |-- stale entry -> revalidate origin -> 304 / new response
  `-- miss ----------------------------> origin or origin shield

CDN 不一定先把所有文件推到全球。许多资源由第一次请求触发回源拉取,后续请求才可能命中缓存;大规模发布或视频业务也可以预热。

2. 缓存键决定响应如何共享 ​

HTTP 缓存的基本键包含请求方法与目标 URI,URI 的查询部分也属于目标。只缓存 GET 时,方法常被隐含。Vary 再按指定请求字段区分表示。CDN 可以配置查询参数等维度,但忽略或归一化参数前,必须先证明它不改变响应;不能把漏配参数当成 HTTP 的默认规则。

缓存键过窄可能把用户 A 的个性化响应发给用户 B;过宽则把无关的追踪参数、cookie 或头变体拆成大量对象,降低命中率。

假设一个公开商品列表只受 page 和已选定的语言变体影响,已确认 utm_source 不影响正文、响应头或访问权限。下面的 Python 3 模型保留有意义的查询项及其顺序,只去掉这一项追踪参数;输入已解析,不包含 URL 或 Accept-Language 解析器:

python
def cache_key(path: str, query: tuple[tuple[str, str], ...], language: str):
    if language not in {"en", "zh"}:
        raise ValueError("unsupported language variant")
    effective_query = tuple((k, v) for k, v in query if k != "utm_source")
    return ("GET", "https", "shop.example.test", path, effective_query, language)

first = cache_key("/products", (("page", "1"), ("utm_source", "mail")), "en")
second = cache_key("/products", (("page", "1"), ("utm_source", "search")), "en")
assert first == second
assert first != cache_key("/products", (("page", "2"),), "en")
assert first != cache_key("/products", (("page", "1"),), "zh")
print("tracking shares; page and language stay separate")

若商品价格还按账号变化,这个键就不够。优先禁止共享缓存这类私有响应,或设计经过审查的独立缓存;不要临时把 cookie 全拼进键就宣布隔离完成。

缓存策略应明确:

  • 哪些方法与状态可存储;
  • 共享缓存能否存储已认证或个性化响应;
  • 键包含哪些查询参数、头与 cookie 维度;
  • Cache-Control、Expires、ETag 与 Last-Modified 如何处理;
  • 源站故障或验证期间能否使用过期响应;
  • 对象大小上限,以及不存在或错误响应的缓存策略。

3. 新鲜度、验证与清除 ​

响应足够新且满足请求、Vary 与缓存指令约束时,可以直接复用。过期响应可用 ETag 或 Last-Modified 发条件请求;304 允许更新匹配缓存条目的元数据并继续使用原正文,不代表任何缓存都能复用它,也不能把 Age 一律重置为零。

Purge 或失效操作属于提供商控制面,需要传播,也可能因为键不匹配漏掉对象。HTTP 缓存语义不提供全球瞬时清除事务。

静态资源通常适合带内容摘要或版本的 URL:

text
/assets/app.4f3a91c2.js

内容变化时生成新 URL,旧 URL 可以给较长的新鲜期。HTML 入口采用较短缓存或验证策略,更新它引用的资源 URL。这样发布时需要协调的旧副本更少。

例如内容固定的公开 JS 可用 Cache-Control: public, max-age=31536000;公开 HTML 可用 Cache-Control: no-cache 配合 ETag,每次复用前验证。no-cache 允许存储,no-store 才禁止存储。旧资源应保留到旧 HTML、离线页面或回滚版本不再引用它为止。

另一种公开响应采用 Cache-Control: max-age=60, stale-while-revalidate=30。在没有其他禁止过期复用的指令时,下面用已计算好的 current_age 演示一个保守边界:满 90 秒就要求验证,不再使用过期宽限。它没有实现 HTTP 缓存或后台请求:

python
def reuse_policy(age: int) -> str:
    if age < 0:
        raise ValueError("negative age")
    if age < 60:
        return "fresh"
    if age < 90:
        return "stale: may serve while revalidating"
    return "revalidate before reuse"

for age in [45, 60, 75, 90, 95]:
    print(age, reuse_policy(age))
# 45 fresh
# 60 stale: may serve while revalidating
# 75 stale: may serve while revalidating
# 90 revalidate before reuse
# 95 revalidate before reuse

4. 缓存击穿与源站保护层 ​

热门对象过期时,多个边缘节点上的请求可能同时回源,形成缓存击穿。可考虑:

  • 请求合并:同一键在合并范围内只发起一次回源;
  • 刷新时间加抖动:避免大量对象同时需要刷新;
  • 允许后台验证或故障时过期复用的策略;
  • 源站保护层:多个边缘节点的未命中先汇集到一层共享缓存;
  • 源站容量限制与熔断。

保护层增加一跳,它通过合并未命中和减少回源保护源站,不保证每个请求延迟都更低。

请求合并有作用范围。30 个边缘节点各自合并成一次回源,源站仍可能收到 30 次;共享 shield 才有机会继续合并。同样,若用抖动错开刷新,提前刷新或缩短本地新鲜期通常更容易保证正确性,不能擅自延长源站允许的新鲜期。

5. GeoDNS 与任播的选路边界 ​

CDN 可通过 DNS 应答把解析器或客户端网段映射到边缘集群,也可由多个站点通告同一任播前缀,让 BGP 选路。实际部署经常组合使用。

GeoDNS 常看到递归解析器的地址,并非终端用户地址。EDNS Client Subnet 可以传递部分客户端前缀,但有隐私和缓存碎片化代价。应答又会被缓存,因此 DNS 导向不是逐请求的实时负载决策。

Anycast 按路由策略选择路径,不保证地理最近或延迟最低。BGP 不读取应用队列深度;运营方要把健康信号与路由通告、撤回、community 和流量工程结合,才能设计故障切换。

6. 观察缓存行为 ​

bash
curl -sS -D - -o /dev/null https://www.example.com/assets/app.js

关注:

  • Cache-Control、Expires、ETag、Last-Modified;
  • Age 是响应年龄信息,不是某 CDN 品牌的凭证;
  • Via、Server-Timing 或提供商专有缓存状态字段,语义需查相应文档;
  • 响应是命中、未命中、绕过、过期复用还是重新验证;
  • 请求 ID 或追踪信息能否关联源站日志。

只搜索 cf-、x-cache 等头名称不能可靠判断是否使用 CDN:其他代理也能生成这些字段,它们也可能被删除或修改。

这个 URL 仅示意检查命令,不保证该路径存在或已接入 CDN。实际排查要换成自己服务的资源,并比较相同键的连续请求及源站日志。Age 反映距源站生成或成功验证的估计时间,可能包含上游缓存驻留,不能直接当作当前边缘节点的驻留时间。

7. 缓存正确性与安全 ​

CDN 是共享中间设施,错误策略可能放大数据泄漏。应检查:

  • 已认证或私有响应是否被共享缓存存储;
  • 缓存键是否忽略会改变响应的头、cookie 或查询参数;
  • 不可信 Host 或其他头能否污染重定向与绝对 URL;
  • 源站是否限制入口,以及怎样认证边缘到源站的访问;
  • 签名 URL 或 cookie 的过期、范围与密钥轮换;
  • 压缩、范围请求和对象大小限制是否可能被滥用。

客户端到边缘的 TLS 若在边缘终止,边缘就能读取 HTTP 明文并缓存或路由。边缘到源站是另一段安全边界,需要独立的 TLS 认证、访问授权和证书维护。

Set-Cookie 本身不禁止 HTTP 缓存。含个人信息的响应应发出正确的 Cache-Control,并确认 CDN 规则没有覆盖它;Vary: Cookie 也不是权限检查。Authorization 请求的共享复用另有规范限制,不能一概按普通公共 GET 处理。

8. 验收问题 ​

  1. 为带版本的 JS 与 HTML 入口分别设计缓存策略。
  2. 为什么 Age 不能单独证明响应来自某 CDN?
  3. 解释缓存键过窄和过宽各会怎样失败。
  4. 为热门对象过期设计请求合并、过期复用和源站保护策略。
  5. 为什么任播不等于理解应用负载的均衡?

下一课进入7.3 负载均衡算法、健康检查与重试边界。

参考 ​

Built with VitePress | Software Systems Atlas