我是如何用 Cloudflare Worker 实现 HTML 灰度发布与两级缓存的

在用 Cloudflare Worker 做 HTML 灰度发布和缓存优化的过程中,响应头里会出现下面几个字段:

plain 复制代码
CF-Cache-Status: HIT
X-Cache: HIT

有时候又会看到:

plain 复制代码
CF-Cache-Status: HIT
X-Cache: MISS

而我访问的明明是:

plain 复制代码
http://localhost:8787

这到底意味着什么?

是不是缓存逻辑有问题?

Worker 到底有没有真的执行?

这篇文章就是我对这套方案的完整复盘。

一、这套 Worker 我想解决什么问题?

先说目标,而不是代码。

我这套 Worker 主要做了几件事:

  1. HTML 支持 灰度发布
  2. 正式 / 灰度 缓存完全隔离
  3. 用 KV 控制版本,实现 软失效
  4. Query 参数归一化,提高缓存命中率
  5. 在 Cloudflare CDN 之上,再加一层业务级缓存

重点是最后一条:

👉 我不是"用 CDN 缓存",而是"叠加了一层缓存"

二、完整 Worker 实现(示意)

下面是我当前使用的一份 Worker 代码。

plain 复制代码
export default {
  async fetch(
    request: Request,
    env: { config: KVNamespace },
    ctx: ExecutionContext
  ) {
    if (request.method !== "GET") {
      return fetch(request);
    }

    const url = new URL(request.url);

    /* 1. 灰度判断 */
    const cookie = request.headers.get("Cookie") || "";
    const isGray = /(?:^|;\s*)env_code=gray(?:;|$)/.test(cookie);

    /* 2. Query 归一化 */
    const IGNORE_PARAMS = [
      "utm_source",
      "utm_medium",
      "utm_campaign",
      "utm_term",
      "utm_content",
      "_t",
      "timestamp",
    ];
    IGNORE_PARAMS.forEach((p) => url.searchParams.delete(p));

    /* 3. KV 版本控制(软失效) */
    const versionKey = isGray ? "gray" : "online";
    const version = (await env.config.get(versionKey)) || "v1";

    /* 4. 构造 cache key */
    const cacheKey = new Request(`${url.toString()}|v=${version}`, {
      method: "GET",
      headers: { Accept: "text/html" },
    });

    const cache = caches.default;

    /* 5. Worker 内部缓存 */
    const cached = await cache.match(cacheKey);
    if (cached) {
      const response = new Response(cached.body, cached);
      response.headers.set("X-Cache", "HIT");
      response.headers.set("X-Cache-Version", version);
      response.headers.set("X-Cache-Env", isGray ? "gray" : "online");
      return response;
    }

    /* 6. 源站选择 */
    const origin = isGray
      ? "https://gray.example.com"
      : "https://www.example.com";

    const targetUrl = origin + url.pathname + url.search;

    const originResp = await fetch(targetUrl, {
      headers: {
        "User-Agent": request.headers.get("User-Agent") || "",
        Accept: "text/html",
      },
    });

    const response = new Response(originResp.body, originResp);
    response.headers.set("X-Cache", "MISS");
    response.headers.set("X-Cache-Version", version);
    response.headers.set("X-Cache-Env", isGray ? "gray" : "online");

    /* 7. 写入 Worker 缓存 */
    if (originResp.ok) {
      response.headers.set(
        "Cache-Control",
        "public, s-maxage=86400, stale-while-revalidate=300"
      );

      ctx.waitUntil(cache.put(cacheKey, response.clone()));
    }

    return response;
  },
};

三、CF-Cache-Status 是什么?(不是我控制的)

CF-Cache-StatusCloudflare CDN 自动加的响应头,它只回答一个问题:

这一次请求,是否被 Cloudflare 边缘节点直接命中?

常见值:

  • HIT
  • MISS
  • BYPASS
  • REVALIDATED

👉 这是 平台层缓存视角

👉 和我写的 Worker 代码 没有直接逻辑关系

四、X-Cache 是什么?(这是我自己定义的)

我在 Worker 里手动加了这些 Header:

plain 复制代码
X-Cache: HIT | MISS
X-Cache-Version: v1
X-Cache-Env: gray | online

重点说明:

X-Cache 不是 Cloudflare 内置字段

是我为了观察 Worker 内部缓存命中情况,自己定义的业务缓存标记

它反映的是:

  • caches.default 是否命中
  • 当前 HTML 属于哪个版本
  • 当前请求落在哪个环境

👉 这是 业务层缓存视角

五、为什么会有「双层缓存」?

真实的请求路径其实是:

plain 复制代码
浏览器
  ↓
Cloudflare CDN
  ↓
Cloudflare Worker
  ↓
Worker 内部 caches.default
  ↓
源站

所以本质上我有 两层缓存

层级 标识
CDN 边缘缓存 CF-Cache-Status
Worker 业务缓存 X-Cache

六、三种常见组合,一次讲清楚

1️⃣ CF-Cache-Status: MISS + X-Cache: MISS(冷启动)

plain 复制代码
CF-Cache-Status: MISS
X-Cache: MISS

含义:

  • CDN 没缓存
  • Worker 内部缓存也没命中
  • Worker 回源
  • 写入 Worker 缓存
  • 同时被 CDN 缓存

👉 这是第一次请求,完全正常

2️⃣ CF-Cache-Status: HIT + X-Cache: HIT(理想态)

plain 复制代码
CF-Cache-Status: HIT
X-Cache: HIT

这是我最常看到、也最喜欢看到的状态。

真实情况是:

  • 某一次 Worker 执行时:
    • 命中了 Worker 缓存
    • 返回 X-Cache: HIT
  • 这个 完整 Response 被 CDN 缓存
  • 后续请求:
    • CDN 直接返回
    • Header 被完整复用

⚠️ 关键点:

X-Cache: HIT
不一定是这一次实时算出来的

而是"历史某一次 Worker 执行的结果"。

3️⃣ CF-Cache-Status: HIT + X-Cache: MISS(最容易被误解)

plain 复制代码
CF-Cache-Status: HIT
X-Cache: MISS

这个组合 完全真实存在,而且非常重要

它通常出现在什么时候?
  • Worker 内部缓存 MISS
  • Worker 回源
  • 返回:
plain 复制代码
X-Cache: MISS
  • 这个 Response 被 CDN 缓存
  • 后续请求:
    • CDN 命中
    • 直接返回这个「MISS 状态的响应」

也就是说:

CDN 命中了

但命中的,是一个「Worker 当时 MISS 的结果」

👉 这不是矛盾

👉 而是缓存时间线不同步导致的状态保留

七、为什么我现在几乎只看到 HIT + HIT?

原因很简单:

  • Worker 缓存已经热了
  • CDN 缓存也已经热了
  • 你访问的路径、Query、Cookie 都很稳定

在这种情况下:

Worker 不再执行,CDN 直接兜底返回

所以你会"长期看到同一个 Header 组合"。

八、那我访问的是 http://localhost:8787 啊?

这是很多人第一次用 wrangler dev 时都会误解的点。

当我运行:

plain 复制代码
wrangler dev

并访问:

plain 复制代码
http://localhost:8787

真实链路是:

plain 复制代码
浏览器
  ↓
localhost(本地代理)
  ↓
Cloudflare 边缘 Worker(真实执行)
  ↓
Cloudflare CDN

localhost 只是入口,不是执行环境。

所以:

  • CF-Cache-Status 真实存在
  • caches.default 是线上行为
  • KV / Cache / 灰度逻辑都是真的

九、我什么时候会特别关注这些状态?

我一般只在这些场景下「刻意观察 Header」:

  • 切换 KV 版本号
  • 调整 cacheKey 规则
  • 灰度 Cookie 切换
  • 验证是否真的生效

只要状态变化 符合我的预期,哪怕是:

plain 复制代码
CF-Cache-Status: MISS
X-Cache: HIT

我都认为系统是健康的。

十、最后总结一句话

**CF-Cache-Status 描述的是 CDN 是否命中

****X-Cache 描述的是 Worker 内部缓存当时的状态

**两者本来就不要求一致

理解了这一点:

  • 双 HIT 不会慌
  • HIT + MISS 不会慌
  • 本地 localhost 也不会再怀疑人生

缓存这件事,

不是"有没有",

而是"你知不知道它在哪一层"。

相关推荐
独行soc2 小时前
2026年渗透测试面试题总结-2(题目+回答)
android·java·网络·python·安全·web安全·渗透测试
爱学java的ptt2 小时前
AQS简单源码思路和手撕实现
java·网络
写代码的【黑咖啡】2 小时前
Python中的BeautifulSoup:强大的HTML/XML解析库
python·html·beautifulsoup
短剑重铸之日2 小时前
《7天学会Redis》特别篇:Redis十大经典面试题2
数据库·redis·后端·缓存·架构
忍冬行者2 小时前
Elasticsearch 介绍及集群部署
java·大数据·elasticsearch·云原生·云计算
罗小爬EX2 小时前
升级IDEA 2025.3+后 Spring Boot 配置文件自动提示插件推荐
java·spring boot·intellij-idea
曹轲恒10 小时前
Java中断
java·开发语言
xxxmine10 小时前
Java并发wait(timeout)
java
冰冰菜的扣jio11 小时前
Redis缓存问题——一致性问题、事务、持久化
java·spring·mybatis