Cloudflare 边缘灰度:HTML 在缓存前分桶,Hash 资产退出切流

💡 原文中文,约2500字,阅读约需6分钟。
📝

内容提要

文章讲述lobehub.com从Next.js迁移到React Router时,如何设计灰度发布系统。系统通过Cloudflare Worker根据cookie选择不同origin,形成缓存分区,确保stable和canary版本不混用。文章详细分析了缓存键、分流位置、资产处理等关键问题,并总结了三次故障教训,最终实现快速回退和高效缓存。

🔎

延伸解读

缓存键设计:灰度与缓存共存的基石

文章强调,任何缓存层只要缓存随桶变化的响应,其键就必须包含 canary_bucket 或等价分区。通过使用不同 origin host 作为缓存键,stable 和 canary 的 HTML 自然隔离,无需额外 Vary 头。这避免了因缓存键缺失导致的串桶问题,是灰度系统能够同时保持缓存效率和版本隔离的关键。

分流位置决定缓存能力

将灰度逻辑从应用 middleware 移到 Cloudflare Worker,使请求在到达应用前就能根据 cookie 选择 origin,从而让边缘缓存生效。若在应用层分流,则必须返回 no-store,导致缓存失效。Worker 位于 zone 缓存之前,子请求与直达流量共用缓存规则,既实现了分流,又保留了缓存能力。

资产与 HTML 分离:避免桶间漂移

hash 资产若跟随应用 origin,用户在不同版本间漂移时可能请求到不存在的文件。将构建产物上传到共享 R2 桶,并通过无 cookie 的独立域名提供,使资产请求不进入 Worker route,彻底解耦资产与版本。这保证了无论用户处于哪个桶,都能稳定加载资源。

故障教训:验证需贴近真实用户

三次故障均源于测试与真实用户行为的差异:HEAD 与 GET 缓存行为不同、裸 UA 触发爬虫 bypass、浏览器本地缓存干扰。最终采用浏览器 UA 的 GET 并禁用本地缓存,才准确反映真实情况。此外,生产环境的 scheme 和 CORS 问题也需在验证矩阵中覆盖所有可能承载页面的 origin。

Q&A

lobehub.com 在从 Next.js 迁移到 React Router 时,为什么第一版灰度逻辑放在应用 middleware 中会导致生产站无法缓存?

因为每个请求都必须到达应用才能读取 cookie 和选择版本,所以生产站统一返回 cdn-cache-control: no-store,cf-cache-status 始终为 DYNAMIC,导致边缘缓存无法生效。

Cloudflare Worker 是如何实现 stable 和 canary 版本缓存分区的?

Worker 根据 cookie 选择不同的 origin host(stable 或 canary),子请求 URL 的 host 不同,从而形成两个缓存分区。cookie 只决定请求进入哪个分区,无需额外 Vary 或 cf 选项。

灰度比例存放在哪里?为什么 origin 和 route 不放在 KV 中?

灰度比例存放在 KV 的 percent 键中,并设置 cacheTtl: 60。origin 和 route 保留在部署配置中,因为比例属于运行时控制面,而 origin 属于需要部署记录和回滚锚点的结构配置。

文章提到的三次故障分别是什么?它们暴露了哪些缓存边界问题?

三次故障分别暴露了浏览器缓存、Worker 前置缓存和 zone 规则中的边界问题。浏览器缓存导致调整灰度比例后回访用户仍读取旧 HTML;Workers Cache 默认缓存键不包含 cookie,导致 stable HTML 可能返回给 canary 用户;zone 规则只匹配公网 host,无法命中 vercel.app 子请求,导致 canary 文档显示 DYNAMIC。

为什么 hash 资产要使用共享域,不参与粘桶?

因为两个部署的 HTML 会引用不同的 chunk hash,如果资产跟随应用 origin,用户在桶间漂移时可能请求到另一个部署不存在的文件。将构建产物上传到同一个 R2 桶,并通过无 cookie 的 web-assets 域名提供,资产请求就不再进入 Worker route,避免了这个问题。

R2 存储中,preview 和 prod 资产是如何分别回收的?

按部署分目录(project/{prod,preview}/),每次构建全量重传,使对象上传时间表示“最后一次被构建引用的时间”。R2 lifecycle 可以分别回收 30 天未引用的 preview 资产和 180 天未引用的 prod 资产。

为什么验证灰度系统时不能使用 curl -I 或裸 curl UA?

因为 HEAD 与 GET 的缓存行为不同,裸 UA 会命中爬虫 bypass,浏览器则可能从本地缓存回放切流前的 HTML。验证应使用浏览器 UA 的 GET,并用 fetch(url, { cache: 'no-store' }) 排除本地缓存。

灰度放量操作具体是什么?生效时间多长?

放量操作是 wrangler kv key put percent 1,KV 在 60 秒内生效,同一条写操作可以降低比例或回到 stable。

🏷️

标签

➡️

继续阅读