Cloudflare 边缘灰度:HTML 在缓存前分桶,Hash 资产退出切流
内容提要
文章讲述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。