潜伏的缓存刺客:Next.js + OpenNext + Cloudflare KV 存储暴涨实录

潜伏的缓存刺客:Next.js + OpenNext + Cloudflare KV 存储暴涨实录

💡 原文中文,约7900字,阅读约需19分钟。
📝

内容提要

作者发现 Cloudflare KV 存储用量暴涨至 3.59GB-month,排查后定位为 OpenNext 的 KV 增量缓存缺陷:kv.put() 未设置 expirationTtl,缓存永久留存;随机 Build ID 使每次部署生成新缓存,构建期全量写入,旧版本数据成为僵尸,最终堆积 24GB。作者通过新建 KV 命名空间、自定义带 7 天 TTL 的缓存适配器、修补构建期 bulk put 三步解决。

🔎

延伸解读

问题根源:OpenNext 的 KV 缓存未设置 TTL

OpenNext 的 KV 增量缓存实现中,kv.put() 调用未传递 expirationTtl 参数,导致所有缓存条目永久留存。源码中留有 TODO 注释,表明开发者尚未解决 Next.js ISR 软过期与 Cloudflare KV 硬 TTL 之间的语义冲突。因此,即使页面代码设置了 revalidate,缓存也不会自动清理,成为存储膨胀的根本原因。

生产环境放大效应:随机 Build ID 与构建期全量写入

每次部署 Next.js 会生成新的 Build ID,缓存 Key 包含该 ID,导致新旧版本缓存完全隔离。构建时 OpenNext 通过 wrangler kv bulk put 将全站预渲染页面全量写入当前 Build ID 前缀下。随着频繁部署,旧版本缓存失去访问入口却因无 TTL 而永久堆积,最终形成数十 GB 的僵尸数据。

修复策略:自定义 TTL 适配器与构建补丁

作者通过三步修复:新建 KV 命名空间并弃用旧数据;实现自定义增量缓存适配器,在 kv.put() 时强制注入 expirationTtl(基于 revalidate 计算,上限 7 天);修补构建期 bulk put 脚本,为预渲染数据添加 expiration_ttl。这样既保留 SWR 软过期体验,又确保所有缓存条目最长 7 天后物理删除。

经验教训:Serverless 成本监控与源码排查

该案例表明,Serverless 架构虽能低成本扩展,但自动扩容也可能导致账单无上限增长。开发者应定期检查用量,不盲目信任框架默认行为,必要时深入源码排查。对于使用 OpenNext 与 Cloudflare KV 的项目,需特别注意缓存生命周期管理,避免类似存储泄漏。

❓

Q&A

为什么我的 Next.js + OpenNext + Cloudflare KV 存储用量会暴涨?

主要原因是 OpenNext 的 KV 增量缓存实现中,kv.put() 没有设置 expirationTtl,导致缓存条目永久有效。同时,每次部署生成新的随机 Build ID,构建期全量写入新缓存,旧版本数据成为无法访问的僵尸数据,持续堆积。

OpenNext 的 KV 增量缓存具体有什么缺陷?

OpenNext 在实现 KV 增量缓存时,调用 kv.put() 没有传递 expirationTtl 参数,导致所有缓存条目永久有效。源码中留有 TODO 注释,表明开发者尚未解决 SWR 语义与硬 TTL 的冲突。

如何修复 OpenNext 的 KV 缓存导致存储暴涨的问题?

作者采用三步修复:1. 新建 KV 命名空间并替换旧命名空间,删除旧数据;2. 自定义增量缓存适配器,为每个条目强制设置最长 7 天的 expirationTtl;3. 修补构建期 bulk put 逻辑,为预渲染数据注入 expiration_ttl。

为什么普通 Demo 项目不会遇到 KV 存储暴涨问题?

普通 Demo 项目通常只有少量静态页面,且很少部署,即使缓存永久不删,KV 用量也只有几十兆,很难触及每月 1GB 的免费额度上限。而生产级项目频繁部署、页面多,会快速累积。

随机 Build ID 如何导致 KV 存储堆积?

每次部署 Next.js 会生成新的随机 Build ID,缓存 Key 中包含该 ID。新版本部署后,旧版本的所有缓存条目失去访问入口,成为僵尸数据。由于没有 TTL,这些数据永久留在 KV 中,随着部署次数增加不断堆积。

自定义的 KV 增量缓存适配器是如何工作的?

适配器在写入 KV 时,根据缓存值的 revalidate 计算 TTL(取两倍 revalidate,上限 7 天),并强制传递给 kv.put() 的 expirationTtl 参数。这样既保留了 Next.js 的软过期 SWR 行为,又确保条目最长 7 天后被物理删除。

🏷️

标签

➡️

继续阅读