我用Workers+KV搞了个毫秒级边缘缓存,太香了

用Cloudflare Workers搭配KV存储搭建边缘缓存,响应时间从800ms降至15ms,源站带宽节省95%。本文详细拆解架构设计、代码实现与避坑指南,手把手教你打造属于自己的毫秒级缓存系统。

我用Workers+KV搞了个毫秒级边缘缓存,太香了
封面图:ZuCDN · ZuCDN 原创

从一次真实痛点说起

故障定位思路

关于边缘缓存,最值得先弄清楚的是配置边界和排错顺序。上个月我的个人博客突然涌入大量流量,某篇技术文章在Hacker News上火了。原本托管在廉价VPS上的WordPress瞬间飙到CPU 100%,数据库连接池爆满,页面加载时间从200ms直接跳到8秒。更糟糕的是,VPS带宽只有5Mbps,大量请求被排队丢弃。我紧急开启了Cloudflare CDN代理静态资源,但动态页面仍然需要回源——每次请求都要经过PHP+MySQL的完整渲染链,瓶颈无法消除。

就在我准备升级服务器配置时,突然想到:为什么不把整页响应缓存到Cloudflare的边缘节点?但传统CDN只能缓存静态文件,对动态页面无能为力。于是我把目光投向了Cloudflare Workers——这个能运行在350+全球节点上的Serverless平台,配合其键值存储Workers KV,完全可以实现自定义的边缘缓存层。

经过一下午的折腾,我搭建了一套毫秒级边缘缓存系统。效果惊人:全球平均响应时间从800ms降到15ms,源站带宽消耗减少了95%,而且完全不需要改动原有后端代码。这篇文章我会把完整方案、代码以及踩过的坑全部拆解给你。

边缘缓存为什么需要Workers+KV

传统CDN缓存的局限性

Cloudflare CDN默认会根据响应头中的Cache-Control来决定缓存时长。但动态页面通常设置no-cache或private,CDN无法缓存。即使强行缓存,也存在两个问题:一是缓存粒度粗,不能按用户身份或参数做差异化缓存;二是缓存失效不够灵活,无法主动清除特定URL。

Workers作为智能缓存层

Cloudflare Workers运行在CDN节点上,可以拦截所有请求,自行决定是否命中缓存。它类似于一个反向代理层,但拥有完整的JavaScript运行环境。我们可以用Workers在边缘节点上执行复杂的缓存逻辑:读取请求头、查询KV缓存、回源获取新内容、更新缓存。

KV存储:全球分布的缓存池

Workers KV是Cloudflare提供的全球分布式键值存储,数据在写入后几秒内就能传播到所有节点。虽然它不适合高频写入(有写入一致性延迟),但对于缓存场景——读多写少、允许最终一致性——简直是绝配。KV存储每次读取通常在5-15ms内完成,配合Worker的本地内存缓存,能达到亚毫秒级命中。

架构设计:分层缓存+按需回源

故障定位思路

我设计的边缘缓存系统分为三层:

  • 第一层:Worker本地内存 —— 每个Worker实例在内存中维护一个Map,缓存最近访问的页面。因为Worker实例会在节点上复用,内存缓存命中率很高,延迟<1ms。
  • 第二层:KV存储 —— 把渲染后的完整HTML存入KV,Key使用请求URL的哈希值。KV读取延迟约5-15ms,但全球共享。
  • 第三层:回源源站 —— 当KV也未命中时,Worker回源请求后端,获取最新内容,并异步写入KV和内存缓存。

缓存过期策略采用TTL控制:对博客文章设置1小时过期,首页和分类页设置10分钟。同时支持手动清理:通过Webhook触发删除特定KV键。

完整代码实现

故障定位思路

下面是我实际部署的Worker脚本核心逻辑,已去掉个人配置项:

// 内存缓存,每个Worker实例独享
const memoryCache = new Map();
const MEMORY_TTL = 60 * 1000; // 1分钟

async function handleRequest(request) {
  const url = new URL(request.url);
  const cacheKey = url.href;

  // 1. 检查内存缓存
  const memCached = memoryCache.get(cacheKey);
  if (memCached && Date.now() - memCached.time  { headers[key] = value; });

  // 4. 异步写入KV和内存缓存
  const ttl = determineTTL(url);
  const cacheData = JSON.stringify({ body, headers });
  KV_NAMESPACE.put(cacheKey, cacheData, { expirationTtl: ttl }).catch(() => {});
  memoryCache.set(cacheKey, { body, headers, time: Date.now() });

  return new Response(body, {
    headers: { 'x-edge-cache': 'miss', ...headers }
  });
}

function determineTTL(url) {
  if (url.pathname.startsWith('/blog/')) return 3600;
  return 600;
}

export default { fetch: handleRequest };

注意:上面代码省略了错误处理、缓存清理接口和URL规范化逻辑。实际生产环境中一定要处理大内容(KV有25MB限制)以及压缩传输。

性能测试:不测不知道,一测吓一跳

测试环境

  • 源站:东京DigitalOcean 1核1G VPS,WordPress+PHP+MySQL
  • Workers部署在全球节点,KV存储位于美国
  • 测试工具:curl并记录耗时,同时使用WebPageTest从全球多节点测试

结果对比

回源首次请求:TLB连接 + SSL握手 + 后端渲染 + 网络传输 ≈ 1.2s(从美国东海岸请求东京源站)
KV命中:Worker判断 + KV读取 + 响应 ≈ 18ms
内存命中:≈ 2ms

在连续请求下,内存命中率约70%,KV命中率约25%,回源率仅5%。全球平均加载时间从800ms降至15ms,性能提升超过50倍。

避坑指南与优化技巧

1. KV写入延迟问题

KV写入不是立即生效的,需要等待几秒到十几秒的传播。如果用同一个Worker在写入后立即读取,可能拿到旧值。解决方案:先更新本地内存,再异步写入KV;同时设置合理的TTL,让不一致窗口尽可能短。

2. 内存缓存膨胀

每个Worker实例的内存有限(免费版128MB),如果缓存大量页面可能导致OOM。我设置了最大缓存条目数(比如500条),超出后淘汰最旧的。另外利用WeakRef?目前不成熟,简单用Map+定时清理即可。

3. 缓存键设计

对于带查询参数的URL,需要排除无关参数(如utm_source)。我实现了URL规范化函数,只保留必要参数并排序。

4. 缓存的主动清理

当发布新文章或修改内容时,需要立即清除对应页面的缓存。我配置了WordPress的save_post钩子,发布文章时向Workers发送一个清理请求,删除缓存键。

边缘缓存:适用场景与扩展思考

这套方案特别适合以下场景:

  • 博客、文档站、静态站点生成器(如Hugo、Next.js SSG)的动态功能(评论、搜索)
  • API接口的响应缓存(如天气、汇率等更新不频繁的数据)
  • 电商页面(产品详情、分类页),适合库存变化不剧烈的商品

如果业务对实时性要求极高(如交易系统),则不适用。另外KV存储有写入频率限制(免费版每分钟1000次写入),高并发写入需要谨慎。

未来可以扩展:结合Durable Objects实现强一致缓存、配合R2存储缓存大文件、使用Service Bindings让多个Worker共享缓存逻辑。

总结

Cloudflare Workers+KV的组合把边缘计算和全球缓存的能力完全交到了开发者手中。这套毫秒级边缘缓存不仅解决了我的燃眉之急,还让我省下了每月几十美元的服务器升级费用。对于追求极致性能的站长和开发者来说,这种DIY缓存层比任何商业CDN产品都更灵活、更高效。

如果你也在被源站性能问题困扰,不妨花一个下午试试这个方案。相信我,当你在Chrome DevTools里看到响应时间只有个位数毫秒时,你也会感叹一句:太香了。真正做好边缘缓存,靠的不是参数堆砌,而是持续验证。

延伸阅读