CDN 可以缩短读者与可复用响应之间的路径,也能减少源站请求量。但这些收益取决于实际路由、缓存里已有的内容和生效策略。域名指向 CDN,不等于某个对象已经缓存,也不证明服务它的是地理上最近的机器,更不能保证每次请求都会绕开源站。应检查真正发生的请求链路,而不是把网络示意图当作验证结果。

沿着真实请求检查

访客会到达服务商路由选出的边缘。如果那里存在可以复用的响应,就能直接返回;否则请求可能经过另一层缓存,也可能抵达源站。因此某个位置的第一次访问,常常与紧接着的第二次不同。发布文件也不代表它会立刻完整复制到所有边缘,不能把“源站可读取”和“所有节点已预热”混为一谈。

即使不能缓存,流量仍可能受益于连接处理或网络路由优化,但它不会取得缓存命中带来的同等回源减免。测量时要分开看建连、首字节等待和正文传输。一次距离源站很近的快速请求,不能推断远方读者、冷节点或源站故障时的表现;需要按真实访问位置和缓存状态分别取证。

明确哪些请求可以共享字节

缓存键决定哪些请求允许共享同一份响应。路径、选定的查询参数、请求头和 Cookie 都可能影响这个身份。凡是会改变所选表示的输入,都必须在复用决策中得到区分;只有确实不改变响应的追踪参数,才适合排除。维度太多会碎片化缓存,遗漏必要维度则可能返回错误内容,甚至跨越敏感数据边界。

“转发给源站”不等于“纳入缓存键”。CloudFront 区分缓存策略和源站请求策略,后者可以转发不进入缓存键的值。如果币种会传给源站、源站也据此生成金额,但缓存键忽略币种,那么一份美元响应就可能用于欧元请求。此时命中率上升并不是优化成果,而是在掩盖内容正确性缺陷。

检查最终生效的策略

源站的 Cache-Control、验证器和 Vary 都很重要,但服务商规则决定它们最终如何处理。必须确认实际绑定的缓存行为保留了每个必要变体。CloudFront 明确警告:最小 TTL 大于零时,即使源站发送 no-cache、no-store 或 private,也可能仍按最小 TTL 缓存内容。敏感路由需要同时检查源站和分发配置,不能只截图一个响应头就认定安全。

在遵守标准的共享缓存中,s-maxage 可以给出不同于浏览器 max-age 的寿命。新鲜度与重新验证仍是两个问题:ETag 能减少验证时的正文传输,却不能修复错误的缓存键。版本化资产需要配套更新引用文档,而边缘清理也不能召回浏览器自己保存、仍被认为新鲜的响应。这些层次要分别验收。

坏掉的样子

遗漏语言或币种会混用公开变体,遗漏访问授权边界则可能更严重。相反,把无关的唯一追踪标识放进缓存键,会让每次访问都变成冷请求。验证时需要对比不同输入下的实际正文,不能只检查“命中”标记。单独缺少 Age 也不足以证明未命中,不同服务商的缓存状态字段不能当作完全通用的标准来解释。

还要考虑发布清理和冷启动的回源压力。多个边缘位置可以各自正确地独立回填,却同时增加数据源流量。应确认源站能承受这类负载,并同时观察错误和回源请求计数。只在一个地点重复请求成功,既不证明全球所有节点已预热,也不证明清理已经传播完成,不能据此扩大性能或一致性结论。

快速验证

需要 Node.js 与 npm;本次使用 Node 26.0.0 和 http-cache-semantics 4.2.0。安装固定版本后,用 NODE_PATH="$cdn_lab/node_modules" node 执行 JavaScript。实验以固定时钟检查一份已存响应的合成请求元数据,安装后不进行网络请求。它不是 CDN 实现,也不模拟任何服务商的完整行为,目的仅是把缓存键和策略边界变成可断言的结果。

cdn_lab=$(mktemp -d)
npm install --prefix "$cdn_lab" --ignore-scripts --no-audit --no-fund --package-lock=false http-cache-semantics@4.2.0
const assert = require("node:assert/strict");
const CachePolicy = require("http-cache-semantics");

class FixedClockPolicy extends CachePolicy {
  now() { return 0; }
}

function request(currency, campaign, language = "en") {
  const url = new URL("https://edge.example.invalid/prices");
  url.searchParams.set("currency", currency);
  url.searchParams.set("utm_source", campaign);
  return {url: url.href, method: "GET", headers: {"accept-language": language}};
}

function project(req, omitCurrency = false) {
  const url = new URL(req.url);
  url.searchParams.delete("utm_source");
  if (omitCurrency) url.searchParams.delete("currency");
  return {...req, url: url.href};
}

const original = request("USD", "alpha");
const trackingChange = request("USD", "beta");
const currencyChange = request("EUR", "beta");
const languageChange = request("USD", "alpha", "zh");
const response = {
  status: 200,
  headers: {
    "cache-control": "public, max-age=0, s-maxage=60",
    "vary": "accept-language",
  },
};
const body = {currency: "USD", language: "en", price: 10};
const policy = new FixedClockPolicy(project(original), response, {shared: true});
assert(policy.storable());
assert(policy.satisfiesWithoutRevalidation(project(trackingChange)));
assert(!policy.satisfiesWithoutRevalidation(project(currencyChange)));
assert(!policy.satisfiesWithoutRevalidation(project(languageChange)));
console.log("safe projection: tracking change hits; currency/language changes miss");

const privateCache = new FixedClockPolicy(project(original), response, {shared: false});
assert(!privateCache.satisfiesWithoutRevalidation(project(original)));
console.log("s-maxage=60: fresh for shared cache; max-age=0: private cache must validate");

const broken = new FixedClockPolicy(project(original, true), response, {shared: true});
assert(broken.satisfiesWithoutRevalidation(project(currencyChange, true)));
assert.notEqual(body.currency, new URL(currencyChange.url).searchParams.get("currency"));
console.log("broken projection: EUR request can reuse the stored USD body");

for (const directive of ["private, max-age=60", "no-store, max-age=60"]) {
  const prohibited = new FixedClockPolicy(original, {
    status: 200, headers: {"cache-control": directive},
  }, {shared: true});
  assert(!prohibited.storable());
}
console.log("standards-based shared policy rejects private and no-store responses");

正确的请求投影只移除追踪参数,所以改变活动来源仍可复用响应,改变币种或语言则不行。其中语言检查使用这个库对 Vary 的处理,真实服务商是否实现等价行为,仍须单独验证。共享策略按照 s-maxage 判断副本新鲜,私有策略则受 max-age 为零的约束,需要先验证,不能直接复用。

故意错误的投影会从策略看到的 URL 中移除币种。库因此允许复用,但正文仍标明 USD,目标请求却要求 EUR。这里的错误是调用方把必要信息藏掉,并非缓存策略库出现故障。最后两项断言证明标准共享策略拒绝 private 和 no-store 响应,并不模拟 CloudFront 的最小 TTL 覆盖行为。

这些断言支持的是明确的策略边界,不涉及全球延迟、路由、清理传播或真实账户隔离。文章给出的上线检查方向与本机实验覆盖面必须分开:只有在真实配置和请求链路上获得证据之后,才能对某个 CDN 部署作出更大的正确性或性能承诺。

延伸阅读

来源