独立开发者最容易被“服务器”套牢——买VPS、配Nginx、盯证书、半夜爬起来处理告警,看起来是在做产品,实际上大半时间在当运维。这几年我越来越倾向另一种玩法:把Cloudflare当“操作系统”用,也就是圈子里常说的cloudflare-os风格项目。它不是一个Linux发行版,而是把Workers、KV、R2、D1、Tunnel这些边缘服务组合成一套统一底座,让个人应用像跑在“系统”上一样:代码上传即部署,全球节点就近响应,证书自动续,攻击流量被挡在最外层。这套东西能解决的核心痛点非常具体:零服务器运维成本、免费额度覆盖大多数个人项目、自带全球分发能力。适合受够了续费账单的折腾型开发者,也适合想把自托管应用推到“全球可达”又想控制预算的人。这篇文章我会从架构思路讲到代码落地,再把实际部署中踩过的坑完整摊开。
1. 先拆解一下“cloudflare-os”到底在做什么
1.1 “操作系统”这个词,在边缘场景下是什么意思
传统操作系统管理的是CPU、内存、磁盘这些硬件资源,向上提供进程、文件、网络等抽象接口。cloudflare-os没有真实硬件,它的“硬件”是分布在全球数百个城市边缘节点上的基础服务:Workers的V8 Isolate就是CPU,R2和KV就是磁盘,D1是关系型数据库,Zero Trust Tunnel是内网入口,Cron Triggers是定时调度器。我们写代码的方式不变,但不再关心“这台机器跑在哪个机房”“磁盘满了没有”“证书过期怎么办”,这些都被边缘平台接管了。
用一个生活化的类比:租VPS等于在一栋楼里租一间房,家具、水电、物业、安保全要自己搞定;而cloudflare-os像是在一家全球连锁酒店里长住,你只管向前台提需求,客房在哪个城市、水电是否正常、门锁是否安全,都是酒店的事。对个人项目来说,这种“退租式”的省心非常值钱——你不用再因为一台最低配服务器流量被打满就整个服务不可用。
这里需要强调一个关键区别:cloudflare-os不是让你抛弃代码能力,而是把注意力从“基础设施运维”转移到“业务逻辑本身”。我见过很多朋友第一次上手时很不适应,总觉得不装个Linux、不连个SSH就不踏实。事实上你完全可以在本地用Wrangler调试,代码跑在边缘之后,日志、监控、灰度发布都有现成工具,比传统服务器那套体验顺滑很多。
1.2 组件全览:这些服务在系统里各扮演什么角色
在具体动手之前,先搞清楚每个组件在这个“操作系统”里对应什么位置,否则后面很容易把KV当成数据库用、把R2当成图床工具用,架构很快就糊了。
| 组件 | 传统系统概念 | 在个人云底座里的职责 | 免费计划的大致边界 |
|---|---|---|---|
| Workers | 应用进程/后端逻辑 | API、页面渲染、中间件、业务核心 | 约10万请求/天,单请求CPU时间10ms |
| Pages | 静态前端托管 | 仪表盘UI、文档站、前端产物 | 静态请求不限量,存储5GB |
| KV | 全局键值缓存 | 配置项、会话、短小数据 | 约10万读/天,1GB存储 |
| R2 | 对象存储 | 文件、图片、备份、静态资产 | 10GB存储,低频操作免费额度 |
| D1 | 关系型数据库 | 结构化数据、任务记录、用户信息 | 5GB存储,读行数每日限额 |
| Zero Trust Tunnel | 内网穿透通道 | 把家中NAS、树莓派服务安全暴露到公网 | 免费版可用 |
| Cron Triggers | 定时任务 | 每日统计、提醒、缓存刷新 | 按创建数量与最小间隔受限 |
这个表格不是让你背参数,而是建立一个映射关系:哪个“器官”管什么。实际项目中我的经验是,KV适合做“系统注册表”式的轻量信息,D1适合做“业务台账”,R2适合做“文件柜”,三者配合而不是互相替代。
1.3 为什么我不建议直接租VPS“省心”
很多人的第一反应是:VPS一个月几十块不贵,为什么要折腾边缘那一套?我原来也这么想,直到对比过两个方案的总拥有成本和体验才改观。
| 对比维度 | 传统VPS自建 | cloudflare-os方案 |
|---|---|---|
| 成本 | 年付几百上千,到期忘续费服务直接挂 | 个人量级基本0元,靠免费额度覆盖 |
| 全球延迟 | 单机房,跨洲访问延迟高 | 边缘节点就近响应,静态资源打满缓存 |
| 运维投入 | 系统更新、安全补丁、证书续期、监控告警 | 平台托管,证书自动化,账单一目了然 |
| 弹性伸缩 | 加配置要迁移或重启 | 请求量增加自动分摊到边缘 |
| 安全防护 | 依赖自己配WAF和防火墙 | 边缘层自带DDoS缓解与WAF规则 |
当然,VPS也有不可替代的场景:跑长驻进程、需要随意安装系统包、依赖gRPC长连接等。但如果你要做的是个人导航页、博客、图床、短链、API聚合、家里NAS透传这类典型需求,cloudflare-os这套底座确实比VPS舒服太多。我自己的项目就是从一台2C2G的VPS迁过来的,迁完最大感受是:不用再凌晨三点被“磁盘使用率超过90%”的告警短信吵醒。
2. 动手前的规划:账号、域名与本地工程化准备
2.1 域名接入的两种姿势与推荐
cloudflare-os的地基是域名,所有绑定服务都围绕域名展开。接入Cloudflare有两条路:把主域名的NS记录整体交给Cloudflare,这是官方推荐的“Full Setup”;或者只在Cloudflare侧保留子域名的CNAME接入记录,这是“Partial Setup”。绝大多数情况下我建议直接NS接入,理由很简单:Tunnel绑定、Access鉴权、WAF规则这些高级能力只对通过Cloudflare DNS解析的域名完整开放。换NS最难受的是等待生效,通常从几分钟到24小时不等,期间老解析记录还在跑,不会造成中断,只是新记录暂时不生效。
接入后在DNS页面确认一下根域和子域的代理状态。Cloudflare控制台里灰色云朵是“仅DNS”,橙色云朵是“已代理”。cloudflare-os项目里务必要让核心域名走橙色代理,否则请求不会经过边缘网络,后续所有WAF、缓存、Tunnel能力都用不上。
2.2 本地环境搭建:Node与Wrangler
官方最新的命令行工具是Wrangler,底层是Rust写的,安装体验比早期版本好很多。前置条件是Node.js 18以上,然后全局安装:
npm i -g wrangler wrangler --version接着登录账号:
wrangler login这个命令会拉起浏览器并跳转到Cloudflare的OAuth授权页,同意之后本地会持久化一份token。对CI环境或不想走浏览器的场景,也可以创建一个API Token并写入环境变量:
export CLOUDFLARE_API_TOKEN=xxxxxxxx wrangler whoamiwrangler whoami能快速确认当前身份是否可用,部署时很多“明明登录了却说认证失败”的问题都是因为这里没配置对。这里有个实用技巧:尽量用wrangler login生成的临时token,而不是创建一个永不过期的API Token放在全局配置里,免得哪天电脑丢失或token泄露,顺手就把你全部资源暴露了。
2.3 项目初始化:从官方模板起步
创建项目最直接的方式是用官方脚手架:
npm create cloudflare@latest cloudflare-os交互式向导会问你选择什么模板,对本文场景我建议选“Workers + Assets”类别,它会生成一个同时包含静态资源服务和Worker逻辑的骨架项目。目录结构长这样:
cloudflare-os/ ├── src/ │ ├── worker.ts # 主入口,类似“系统内核” │ ├── routes/ │ │ ├── dashboard.ts │ │ └── api.ts │ ├── middleware/ │ │ ├── auth.ts │ │ └── log.ts │ └── utils/ ├── public/ # 静态前端产物 ├── migrations/ # D1数据库迁移SQL ├── tests/ └── wrangler.jsonc # 项目配置也许有朋友觉得一个个人项目没必要分这么细,但我强烈建议从第一天就保持这个分层:worker.ts只做路由分发和中间件串联,业务逻辑全部拆出去。原因很现实——Workers单个脚本体积有限制,如果所有代码堆在一个文件里,后期加依赖时很容易撞上“脚本体积超限”,那时再造结构就痛苦了。
2.4 把“环境边界”设计清楚,避免组件滥用
这一步是在写代码之前就要做的决策:每个数据该放在哪里,哪些逻辑跑在边缘,哪些一次性算完。我总结了一套简单判断规则:
- 能静态生成的,不要动态渲染:比如个人导航页的图标配置、博客文章列表,构建期生成静态文件扔给Pages,远比每个请求都查数据库快。
- 能进KV的,不上D1:只读配置项、用户的登录态、临时令牌,KV读延迟极低,但它是最终一致性的,写入后可能有一两秒的延迟,不适合做强一致的业务数据。
- 能Workers算的,不引入数据库:比如RSS聚合、Webhook转发、图片裁切这类无状态逻辑,全在Workers里算完,D1只留真正需要查和统计的数据。
- 文件永远去R2,不要塞KV或D1:KV单值上限是25MB,D1不适合存二进制,文件类数据进R2是唯一正确的选择。
3. 核心实现:把“个人云操作系统”跑起来
3.1 系统入口:用Workers做统一路由接入
一切请求的入口都是Worker脚本,它像内核里的调度器:先经中间件,再分发给具体路由。下面是一个最小可运行的入口实现:
// src/worker.ts export default { async fetch(request: Request, env: Env, ctx: ExecutionContext) { const url = new URL(request.url); const start = Date.now(); // 中间件:简单请求日志 ctx.waitUntil( fetch(`https://api.cloudflare.com/client/v4/accounts/${env.ACCOUNT_ID}/...`, { method: 'POST', headers: { 'Authorization': `Bearer ${env.API_TOKEN}` }, body: JSON.stringify({ url: url.pathname, ts: new Date().toISOString() }), }).catch(() => {}) ); // 核心路由 if (url.pathname.startsWith('/api/')) { return handleApi(request, env, ctx); } if (url.pathname.startsWith('/healthz')) { return new Response('ok', { status: 200 }); } // 静态资源回退到 Assets 托管 return env.ASSETS.fetch(request); } }这段代码的核心意图是把“动态逻辑”和“静态资源”分开:/api/*走接口处理,其余路径全部交给env.ASSETS.fetch去取静态文件,这样浏览器加载HTML、JS、CSS时完全不消耗Worker的CPU时间。如果嫌日志上报太占资源,也可以只把关键事件记录到D1,后面查询时再做分析。
3.2 登录鉴权:优先用Cloudflare Access做前置校验
自托管应用最怕裸奔。自己写一套用户体系和JWT成本不低,还容易出现安全漏洞。Cloudflare Access可以直接在边缘层做身份校验:用户访问你的域名时,未认证请求会被重定向到Cloudflare的登录页,认证通过后才会把请求转发到后端Worker。
在控制台的Zero Trust -> Access -> Applications里创建一个Self-hosted应用,填上你的域名,再配置一条Access Policy(指定允许的邮箱或身份提供商即可)。个人项目我一般只允许自己的邮箱,用起来跟“单用户操作系统”一样干净。
这样做了之后,Worker里甚至不再需要自己写登录校验。如果还需要识别当前用户,可以从请求头里读cf-access-authenticated-user-email,这个头是Cloudflare在认证通过后注入的。实际部署中这个方案比自己写Session安全得多,而且完全省掉了“忘记密码”“token过期”这类破事。
3.3 用KV做全局配置与轻量会话
KV适合放“变化不频繁、读取频繁”的数据,比如系统里的功能开关、用户偏好、短时token。绑定KV很简单,先在控制台创建Namespace,再到wrangler.jsonc里声明:
{ "kv_namespaces": [ { "binding": "STORE", "id": "xxxx" } ] }然后代码里直接读写:
// 读取配置 const config = await env.STORE.get('app:config', 'json'); // 写入配置 await env.STORE.put('app:config', JSON.stringify({ theme: 'dark' })); // 设置过期时间 await env.STORE.put('session:abc', 'user_001', { expirationTtl: 3600 });这里要特别记住KV是最终一致性的:刚写完立刻去读,可能读不到新值。所以我会把KV当成“缓存层”而不是“事实来源”。比如用户改完主题色,页面先显示旧值也没关系,几秒后刷新就是新配置了。如果你要做“写入后立即查询并展示”的强一致功能,直接上D1,千万别在KV上较劲。
3.4 用D1存结构化数据
D1是基于SQLite的关系型数据库,API兼容SQLite语法,对个人项目来说足够强大。创建和绑定过程如下:
# 创建数据库 wrangler d1 create cloudflare-os-db # 本地创建迁移文件 wrangler d1 migrations create cloudflare-os-db create_tasks_table迁移文件一般长这样:
-- migrations/0001_create_tasks_table.sql CREATE TABLE IF NOT EXISTS tasks ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'pending', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );然后应用到本地与远程:
wrangler d1 migrations apply cloudflare-os-db --local wrangler d1 migrations apply cloudflare-os-db --remote在Worker里通过env.DB直接执行SQL:
const result = await env.DB.prepare( 'SELECT * FROM tasks WHERE status = ? ORDER BY created_at DESC' ).bind('pending').all(); const inserted = await env.DB.prepare( 'INSERT INTO tasks (title) VALUES (?)' ).bind('写一篇cloudflare-os总结').run();用D1最舒服的是它自带迁移工具,本地开发、线上发布的schema冲突能被较早地发现。数据库初始化时我推荐把迁移文件全部放到migrations目录统一管理,不要把创建表的SQL散落在代码注释里,否则两周之后你自己都记不清线上有哪些表。
3.5 用R2管文件:个人云里的“磁盘”
R2是S3兼容的对象存储,最大的优势是免出口流量费。如果要把图片、备份、媒体文件分发到全球用户,R2很省钱。在控制台创建Bucket之后,在wrangler.jsonc里加绑定:
{ "r2_buckets": [ { "binding": "FILES", "bucket_name": "personal-files" } ] }上传下载的代码非常直观:
// 上传 const file = await request.arrayBuffer(); await env.FILES.put(`uploads/${Date.now()}.png`, file, { httpMetadata: { contentType: 'image/png' }, customMetadata: { source: 'dashboard' } }); // 下载 const obj = await env.FILES.get('uploads/1700000000.png'); if (obj) { return new Response(obj.body, { headers: { 'Content-Type': obj.httpMetadata?.contentType || 'application/octet-stream' } }); } // 删除 await env.FILES.delete('uploads/1700000000.png'); // 带过期时间的临时访问URL,适合分享场景 const url = await env.FILES.createPresignedUrl('uploads/1700000000.png', 3600);R2与S3兼容意味着你本地的其他工具也可以直接通过S3 API访问同一个Bucket,比如批量脚本、Rclone备份等。我的图片处理流程是:前端上传到Worker,Worker校验格式后先压缩再写R2,然后返回CDN地址。整个链路没有一台自己的服务器参与,但体验上跟传统文件服务器没有任何区别。
3.6 用Tunnel把家里NAS变成私有云
cloudflare-os如果只能跑“云端函数”还差点意思,真正让它像“操作系统”的是Zero Trust Tunnel:你可以把家里树莓派上跑的Home Assistant、NAS的管理界面、内网里的Docker服务全部映射到公网域名下,而且不暴露家里IP、不需要路由器端口映射。
先在本机安装cloudflared并登录:
# macOS 安装 brew install cloudflared # Linux 安装 curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -o /usr/local/bin/cloudflared chmod +x /usr/local/bin/cloudflared # 登录并创建隧道 cloudflared tunnel login cloudflared tunnel create homeserver创建后会生成一个隧道ID和对应的凭证文件,接着编写配置文件:
# ~/.cloudflared/config.yml tunnel: homeserver credentials-file: /home/user/.cloudflared/homeserver.json ingress: - hostname: nas.example.com service: http://192.168.1.100:5000 - hostname: home.example.com service: http://192.168.1.100:8123 - service: http_status:404最后启动:
cloudflared tunnel run homeserver然后在Zero Trust控制台的Networks -> Tunnels里把刚才的隧道和主机名关联起来。到这里,你在外网打开nas.example.com就能直接访问家里的NAS界面,全程加密且不需要公网IP。
很多朋友纠结“要不要把Tunnel部署成systemd服务”,我的建议是:如果你只是偶尔用,直接前台跑就行;如果想让服务7x24在线,用systemd或等价进程守护工具把它托底。另一个常见坑是Tunnel所在的主机休眠后隧道会断,恢复后要等几十秒重连,所以最好把宿主机电源策略改成“永不休眠”。
3.7 用Cron Triggers做定时任务
操作系统不能没有计划任务。Cloudflare Worker内置Cron Triggers,最细粒度可以做到每分钟触发一次(免费档有最小间隔限制,付费档更灵活)。在wrangler.jsonc里这样配置:
{ "triggers": { "crons": [ "0 * * * *", // 每小时整点 "*/30 * * * *", // 每30分钟 "0 2 * * *" // 每天凌晨2点,注意是UTC时间 ] } }Worker里通过scheduled处理器响应:
export default { async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext) { switch (event.cron) { case '0 * * * *': await refreshRssFeed(env); break; case '0 2 * * *': await generateDailyReport(env); break; } } }这里必须着重提示一个坑:Cron Triggers用的是UTC时间,不是你的本地时区。中国用户如果想让任务在“每天早上8点”执行,需要写成0 0 * * *(UTC凌晨零点对应北京时间早上八点),而且要考虑夏令时国家和UTC的差异。排错时建议在任务日志里直接输出new Date().toISOString(),方便核对实际触发时刻。
3.8 静态资源与Worker融合:选择Assets模式还是自建路由
现在的官方推荐路线是“Workers + Assets”:把前端构建产物放在public/目录,由Cloudflare边缘直接托管,Worker只处理动态请求。好处是静态资源不走Worker CPU配额,前端改版发布也很简单。
在wrangler.jsonc里做如下配置:
{ "assets": { "directory": "./public", "binding": "ASSETS" } }这种做法相当于把“系统盘”和“程序文件”分开:静态页面是壳,Worker是可执行文件,数据在D1/R2里。对个人项目来说,这种分工让前端迭代可以独立部署,后端逻辑升级也不会影响静态资源的缓存命中率。
4. 从“能跑”到“能稳”:容量规划与性能瓶颈
4.1 免费额度边界:哪些墙不能碰
很多项目挂在“免费”二字上,但免费额度不是无限量。以下几个常见边界必须心里有数:
| 资源 | 免费档额度 | 超出后的表现 |
|---|---|---|
| Workers请求数 | 约10万/天 | 请求直接返回429,无法再处理 |
| Worker CPU时间 | 单请求10ms | 超过会被强制中断并报错 |
| KV读 | 约10万/天 | 报读超限,降级频繁 |
| KV写 | 约1000/天 | 写入失败,配置更新不生效 |
| R2存储 | 10GB | 超出后写入被拒绝,只能删除或升级 |
| D1读 | 500万行/天 | 查询报错,后台被限流 |
不要觉得“个人项目一定不会超”。有一个用户量不大的RSS聚合服务,因为每个订阅源更新都触发一次全量扫描,一天就把KV写入额度耗光了。后来我把配置缓存、内容缓存全部迁移到R2,KV只剩高频访问的小片段,才稳定下来。上线前我建议先做一次“最坏情况估算”:假设每个用户操作会触发几次KV读、几次D1查、几次R2写,再乘上预估日活,看看离限额还有多少余量。
4.2 冷启动与依赖体积:为什么我坚持“瘦Worker”
Cloudflare对单个Worker脚本的体积限制是3MB(压缩后)。加上node_modules里的依赖,小项目很容易突破这个限制。我踩过一次:引入一个包含了完整图像处理库的SDK后,脚本体积直接到4.2MB,部署直接报Worker exceeded size limits,整了半个小时才定位是哪个包的问题。
解决思路有三层:
- 第一层,压缩依赖:优先选体积小的库,比如用
date-fns替代moment,用picomatch替代glob。 - 第二层,动态导入:把非核心模块放在远程或R2里,运行时按需拉取。
- 第三层,拆分服务:把重活交给独立的Worker或Pages Functions,通过
fetch内部调用,而不是把所有东西塞在一个Worker里。
我的经验是,从一开始就给依赖“设预算”:每个包先看一眼npm体积,超过200KB的就要思考它是否值得。这不是强迫症,而是边缘函数这个形态下“体积就是性能”,脚本越小,冷启动越快,命中率越高。
4.3 缓存策略:边缘缓存开了不等于自动优化
把图片放在R2后,浏览器每次请求还要走一遍Worker吗?不一定。Cloudflare的缓存层能自动缓存静态资源的响应,前提是你设置了正确的Cache-Control头。比如R2对象返回时加上:
const headers = new Headers(); headers.set('Cache-Control', 'public, max-age=31536000, immutable'); return new Response(obj.body, { headers });这时你可以用浏览器的Network面板或命令行确认缓存是否生效:
curl -I https://your-domain.com/uploads/image.png # 期望看到 cf-cache-status: HITcf-cache-status可能出现的值有MISS、HIT、DYNAMIC。DYNAMIC表示响应没有被缓存,通常是动态API或缺少可缓存标记。排查问题时先看这个头,比瞎猜快得多。对于API响应,如果数据可以忍受一定延迟,也可以加s-maxage=60让边缘缓存60秒,同一天的重复请求就不会每次都打到D1上。
5. 我在实际部署中踩过的坑(排查记录)
5.1 Worker exceeded size limits:体积超限
症状:wrangler deploy时报错,提示脚本体积超过限制。
排查步骤:
- 执行
npx wrangler deploy --dry-run --outdir=dist查看产物大小。 - 用工具分析各依赖体积,找出占比最高的包。
- 看是否能把重型依赖拆到独立Worker或改为运行时动态加载。
- 如果卡在某个npm包上,尝试用动态
import()在函数内部加载,而不是顶层引入。
这个坑最容易在“项目顺利上线很久后加新功能”时出现,因为依赖是在持续累积的。建议每次部署前习惯性跑一次wrangler deploy --dry-run,把“意外超限”变成“提前发现”。
5.2 D1迁移不一致:本地成功、线上失败
症状:wrangler d1 migrations apply --remote报错,提示表已存在或约束冲突,但本地一切正常。
原因:本地和远程的迁移历史记录不一致,可能之前手改过数据库,或者某个迁移文件在远程执行过但又被修改过。
解决办法:
- 用
wrangler d1 migrations list cloudflare-os-db --remote查看远程迁移状态。 - 与本地
--local状态对比,找出差异。 - 不要直接删远程表,宁可再写一个“补偿迁移”来调整schema,也不要手动改表结构导致后续迁移全部错乱。
这条经验来自一次“图省事直接在线上库执行了ALTER TABLE”的操作,后来所有新迁移都报错。诚实地说,手动改库一时爽,后续迁移火葬场。
5.3 Tunnel掉线后访问报错
症状:通过隧道访问的域名偶尔返回521或530,有时刷新就恢复。
排查思路:
- 先检查
cloudflared进程是否存活,本地日志里有没有断连记录。 - 看隧道另一端服务是否可用,比如家里NAS的Web服务本身有没有挂。
- 检查是不是宿主机休眠导致的隧道断开。
- 尽量让cloudflared以守护进程方式运行,并配置重连退避参数,例如在
config.yml中加入:retries: 10 grace-period: 10s - 不要随便设置
noTLSVerify: true,那是用于自签名证书调试的,生产环境开了等于裸奔。
5.4 Cron时区陷阱
症状:每天定时任务总是差8小时执行,日志里的时间戳看着像“凌晨”,但本地逻辑判断又是白天。
原因:Cron Triggers按UTC执行,没有时区配置项。
解决:
- 在你自己的代码里不要依赖服务器本地时区,统一用
new Date()和时间戳比较。 - 定时任务执行体第一时间记录
new Date().toISOString(),方便对照。 - 如果多个任务都需要按北京时间的“凌晨”执行,可以把
0 0 * * *这种UTC时间写成一个常量表注释在配置旁,减少误解。
5.5 KV写入后立刻读不到
症状:刚在后台更新了一个配置项,随后测试访问时还是旧值。
原因:KV是最终一致性模型,写入后可能需要几秒才能全局生效。
解决:
- 对“改完立即生效”有要求的场景,把配置改写到D1,让业务读取时查D1,KV只做加速。
- 对KV写入,尽量设置
expirationTtl,避免留下永远不清理的脏数据。 - 在日志里避免对KV写入和读取做“强顺序”假设,否则在测试时会看到很多假故障。
说实话,KV这个特性我在项目前两周反复“误判”成Bug,后来想明白后就把架构调过来了。现在KV只缓存那些“晚几秒生效没关系”的数据,真正需要强一致的读写全部走D1。
5.6 免费额度突增被限流
症状:某天某个接口突然大量返回429,查看Workers实时日志发现有密密麻麻的请求。
原因:可能是某个页面在浏览器端做了高频轮询,或者外部服务回调地址配置错误导致死循环。
解决:
- 在Worker入口给关键API加频率限制,最简单的做法是记录IP加时间窗口,超过阈值直接返回429。
- 给静态资源引用加上版本参数,避免浏览器缓存过期后大量重复回源。
- 用Cloudflare的Rate Limiting规则在边缘层做前置拦截,比业务代码里判断更省额度。
这个坑比较隐蔽,因为免费额度看着够用,但一旦代码不小心写了个setInterval(1000)轮询接口,一天的请求配额可能一小时就烧完了。上线前在浏览器开发者工具里检查一下网络请求频率,能提前发现这种自杀式循环。
写在最后:一个扩展方向
如果你已经跑通了上面这套底座,可以顺手再加一个很实用的小功能:用R2做个人图床,再配合Workers的/api/upload接口实现截图即分享。我把这个功能挂在cloudflare-os的“文件管理”模块里,配合Access鉴权,等于有了一个自带全球CDN、私有权限、零成本的个人文件服务。这套东西折腾下来,最大的感受不是省钱,而是心态变了——以前加个功能先纠结服务器资源够不够,现在只关心代码逻辑对不对,推倒重建的成本极低,这大概就是边缘“操作系统”带来的正向副作用。