用Cloudflare Workers搭建个人云OS:从VPS到边缘计算的实践指南 我一直觉得操作系统这个词不该只属于电脑和手机。去年我给自己定了个挺折腾的目标把我日常依赖的各种零散服务——笔记、书签、定时任务、临时文件、个人API——全部拼成一个能随取随用的个人云OS。调研了一圈之后我把底座定在了Cloudflare上项目代号就叫cloudflare-os。这个项目说白了就是用Cloudflare的边缘网络做内核用Workers当进程用KV、R2、D1这些存储当内存和硬盘。它解决的核心问题是不想再为几个小应用维护一台服务器也不想被各家免费服务割裂数据。如果你也受够了为了一个博客、一个接口、一个定时任务各租一台机器的状态那这篇分享应该能给你一些实在的思路。这个系列我会讲清楚三件事为什么选Cloudflare而不是传统VPS整个系统的架构怎么搭以及上线后我踩过的坑。内容不追求大而全就按我实际搭建的过程来。1. 从自建服务器转向边缘个人OS的判断过程1.1 自建服务器最难受的点不是钱是养很多人一想到个人云服务第一反应是买台VPS。国内国外厂商都行价格也确实不贵一年几百块。但问题从来不在买而在养。我前前后后折腾过好几台VPS最崩溃的一次是某天凌晨一个运行了半年的服务突然挂了。查日志发现是磁盘满了但明明没存什么东西一排查是日志本身滚了十几个G。再往后又遇到内核升级后服务起不来的问题那会儿我才意识到维护一台服务器的隐性成本根本不是那点租金的钱而是你得持续投入时间去处理系统更新、安全补丁、进程守护、磁盘告警这些脏活。有人会说现在有各种PaaS平台直接push代码就能跑。这话没错但多数PaaS平台的免费层级要么有很严格的内存限制要么会在30秒无请求后进入冷启动。真正用起来你会发现一个尴尬的现实很多个人应用其实是低频但需要随时可用的比如凌晨的定时任务、偶尔打开的书签页、一个月用几次的文件转存。1.2 Cloudflare对我的三个决定性价值我对Cloudflare本身并不陌生大多数用过博客的人都知道它是个CDN厂商但真正让它变成个人OS底座的是几个不太起眼的能力。第一个是Workers的全球部署。你写一个JavaScript或TypeScript函数它能自动分布在全球几百个数据中心。这对我这种没精力做多区域部署的开发者来说直接省掉了一整块工程量。冷启动虽然存在但在绝大多数个人场景下基本感知不到。第二个是存储生态的完整度。KV、R2、D1、Durable Objects每个都有明确的适用场景。不需要像自建服务器那样先在数据库选型里纠结半天一套API下来全打通。第三个是Cron Triggers。Workers支持定时触发免费的额度对个人项目来说基本够用。你那些每天早上拉一次天气数据每周备份一次书签之类的需求不需要再去单独搭一套定时任务系统。当然Cloudflare也不是没有缺点。比如Workers的运行时长限制免费版CPU时间在10毫秒内付费版可以放宽这决定了你不可能在Worker里跑重型计算。还有它的运行环境对包体积有要求超过1MB就得自己想办法拆逻辑。我的做法是能拆的拆成多个Worker不能拆的推到R2里懒加载。1.3 传统VPS和Free Plan的对比测试在正式动手前我花了一个周末做了次简单的对比测试。左边是我原来的一台1核1G的VPS右边是Cloudflare的免费套餐。我部署了一个内容差不多的API服务连续请求1000次测延迟。结果很有意思单次请求的响应速度Cloudflare的免费套餐响应速度在某些区域反而比我的小型VPS更快尤其是跨地区访问时。主要原因在于边缘CDN请求直接就近命中了而我的VPS只有单点。但在持续高负载的场景下VPS的固定资源反而是稳定的Cloudflare的免费层级如果触发了CPU时间限制并发一高就开始报错。这个测试让我彻底想明白了Cloudflare适合的是请求频率低、算力要求小、但必须全球快的服务VPS适合的是持续重负载、需要完整环境的服务。个人云OS绝大多数组件属于前者。2. 账号、域名与路由整个系统最先要稳住的底盘2.1 一个账号打底免费套餐的边界在哪注册Cloudflare账号这里不多说了一个邮箱就行。我用的免费套餐没有单独开付费计划。很多人问免费的够不够用我的回答是够用但要知道边界。免费套餐里Workers有每天10万次请求的额度KV有每天10万次读、1000次写R2的存储免费额度是10GBD1数据库是每天500万行读。这些数字对个人项目来说绝大多数时候没法触顶。真正需要留意的反而是Worker脚本的CPU时间限制免费用户每次运行CPU时间不能超过10ms免费层这里是10毫秒CPU时间不是请求时长。如果你有复杂的运算干脆把任务拆成多个步骤或者用Queue异步处理。2.2 把域名接入Cloudflare的全流程域名接入这部分看起来简单但有几个细节我一直提醒自己注意。如果你是在Cloudflare新注册的域名跳过添加站点的步骤直接就能在DNS面板配置。如果是外部域名流程是先把域名的NS记录改到Cloudflare指定的两个NS地址然后等生效。生效之后你有两种方式让Worker跑在你的域名下一是给Worker绑定自定义域名Custom Domain二是通过Route路由把某个子域名的请求转发到指定Worker。我自己用的是Bound Custom Domain方式。原因是直观且不容易和DNS记录冲突。比如我的笔记服务绑定的是note.mycloud.dev书签服务绑定的是bookmark.mycloud.dev直接在Worker的域名配置里加上就能用Cloudflare会自动创建对应的DNS记录。2.3 路由规划决定了你后面运维省不省心路由规划是整个项目里我觉得最值得花时间想的事。很多人的习惯是建一个Worker然后把所有业务逻辑塞进同一个脚本里路由靠URL判断。这在项目初期看起来挺顺但一旦业务多起来你会发现改一个功能必须重新部署整个脚本一个模块出bug可能导致所有服务不可用。我的做法是一业务一Worker。每个Worker只管一件具体的事比如feed-parser只负责抓RSSimage-resizer只处理图片缩放todo-api只暴露待办接口。然后通过Cloudflare的路由规则把不同子域名的请求分发到对应Worker。这样每个Worker都是独立部署、独立回滚出了问题也只会影响一个功能。这里还有一个经验把子域名的命名规则定清楚。我用的格式是功能名.主域名一眼就能看出是干什么用的。像auth.mycloud.dev这种做登录鉴权file.mycloud.dev做文件上传下载。后面接第三方回落机制的时候也比一个裸域到处转发清晰得多。3. 个人OS的存储层选型KV、R2、D1、Durable Objects怎么配合3.1 四种存储的定位差异Cloudflare的生态里最唬人的就是存储产品多KV、R2、D1、Durable Objects。很多人一开始会问到底用哪个我直接给了自己一张对照表按场景选型不纠结存储产品定位典型场景个人OS里的角色KV全球分布低延迟读最终一致性配置项、短链接、用户Session相当于高速缓存和标记位R2对象存储兼容S3 API图片、文件、备份包相当于硬盘D1SQL数据库基于SQLite需要关系查询的结构化数据相当于关系型数据库Durable Objects单点强一致可持久化状态实时协作、WebSocket、计数器相当于有状态的微服务我最早犯的错误是把KV当数据库用。往里塞了一堆结构化数据结果要做列表查询时完全无从下手。KV的价值是用key直接拿value如果业务逻辑里有按条件筛选这种需求KV不是合适的选择。3.2 各存储在小项目里的真实提质经验先说KV。我拿它做了两件事一是存全局配置比如不同服务的开关状态和API Key的加密串二是做短链接服务里的URL映射。效果非常好读延迟几乎可以忽略。KV的注意事项是写入后的生效延迟。虽然是最终一致性但实测通常几十毫秒内就能读到新值对个人项目不影响。D1是最让我惊喜的一个。之前我一直没想到Cloudflare会推出SQLite兼容的关系型数据库。我现在把待办清单、书签的分类和标签关系、以及RSS阅读状态都存在D1里。因为SQLite的语法很接近传统数据库我几乎没花学习成本。唯一要注意的是D1的连接方式如果你在Worker里每次请求都新建连接很容易把连接数打满。官方推荐的是在Fetcher里复用连接。R2的定位就简单了存文件。我主要存两类图片资源和定时备份的压缩包。R2支持S3 API所以如果你熟悉AWS S3的SDK直接把endpoint换成R2的地址就能用。这里有必须注意的一点R2的公开访问默认是关闭的如果你直接用URL去访问Bucket里的资源会得到AccessDenied。要么通过Worker做一层鉴权和转发要么在R2的Bucket设置里绑定自定义域名。Durable Objects我目前只在一个场景用了多人协作的Markdown白板。因为它支持WebSocket的持久连接和单点状态比用KV轮询舒服得多。但它也是最容易被过度使用的存储类型官网文档明确说它适合有状态的服务如果只是存个数据别用它。3.3 一个完整的存储方案配置示例我直接贴一段我项目的初始化代码帮大家把概念落到实践。比如我要在Worker里同时用D1和KVwrangler.toml是这么配置的name cloudflare-os main src/index.js compatibility_date 2025-01-01 [[d1_databases]] binding DB database_name my-os-db database_id xxxxxxxxxxxx [[kv_namespaces]] binding MY_KV id xxxxxxxxxxxx然后在Worker脚本里export default { async fetch(request, env, ctx) { // KV读配置 const theme await env.MY_KV.get(theme); // D1查询 const { results } await env.DB.prepare( SELECT * FROM bookmarks WHERE status ? ).bind(active).all(); return new Response(JSON.stringify({ theme, results }), { headers: { content-type: application/json } }); } }这样就同时接入了两个存储后面扩展R2也类似只需要再加一个binding声明。4. 端到端搭建从域名到第一个Worker应用跑通全流程4.1 本地环境准备Wrangler CLI的安装Cloudflare官方提供的命令行工具叫Wrangler所有项目的本地开发、上传、日志查看都靠它。安装很简单Node环境前提下用npm:npm install -g wrangler装完之后先登录wrangler login它会自动打开浏览器让你授权。这一步必须做不然后面没法部署。如果是在无浏览器环境可以用wrangler login --api-key方式手动填Token但我不太推荐容易把Token留在shell历史里。4.2 创建一个Worker并绑定额外资源执行wrangler init cloudflare-os它会帮你生成一个基本的Worker项目结构包括wrangler.toml和src/index.js。我习惯在index.js里先用一个最简单的响应测试连通性export default { async fetch(request, env, ctx) { return new Response(cloudflare-os running); } }然后直接部署wrangler deploy部署完成后你会得到一个xxx.workers.dev的地址。打开浏览器看一下如果能看到那行文字说明环境OK。接着把之前配置的D1和KV绑定好重新部署一次。到这一步你的Worker已经具备读写数据库和缓存的能力了。4.3 绑定自定义域名与HTTPS配置我个人不建议长期使用*.workers.dev域名一是因为部分网络环境对这个域名有屏蔽风险二是不符合个人OS的统一感。绑定自定义域名的方式分两种第一种在Cloudflare控制台的Workers页面选择你的Worker进入Settings - Domains - Add然后输入你想绑定的子域名。系统会自动创建一条DNS记录生效时间通常在几分钟内。第二种在wrangler.toml里指定route配置例如routes [ { pattern note.mycloud.dev, custom_domain true } ]custom_domain true表示它是直接绑定而不是通过代理路由配置完执行wrangler deploy就行。HTTPS方面绑定到Cloudflare域名下的Worker会自动启用HTTPS不需要自己配置证书。这是我特别喜欢的一点在其他平台光是一个证书续期就能消磨掉人的耐心。4.4 跑通定时任务Cron Triggers的完整配置个人OS里定时任务是最能体现自动运行魅力的部分。Cron Triggers在wrangler.toml里配置[triggers] crons [0 0 * * *, 30 8 * * *]第一行表示每天零点触发第二行表示每天早上8点30分触发。时间默认是UTC如果要按北京时间需要换算北京时间比UTC快8小时所以每天早上9点对应的cron应该是0 1 * * *。在Worker脚本里你要监听scheduled事件export default { async scheduled(event, env, ctx) { // 根据event.cron判断当前执行的是哪个任务 if (event.cron 0 0 * * *) { await runDailyBackup(env); } if (event.cron 30 8 * * *) { await fetchMorningNews(env); } } }一个Worker可以绑定多个cron表达式你在代码里通过event.cron区分具体执行哪段逻辑。跑完之后可以到Cloudflare的日志面板看一眼输出确认没有异常状态。5. 上线后的真实体验与常见坑5.1 免费套餐里最容易触碰的隐形限制项目上线一个月后我开始遇到一些奇怪的现象某个接口突然返回523或者524。排查发现不是代码问题而是我在处理图片时开了ctx.waitUntil()执行一些不紧急的后台任务导致Worker实例占用的总时长超过了免费额度。这里需要说清楚区别Worker的免费套餐是每天10万次请求不假但每次请求你可以跑的时间不是无限。CPU时间限制是一个杀手锏是实例内存限制。如果你的Worker实例运行时间超过30秒不管你是在等待上游接口还是处理大文件都会被杀掉。对个人项目来说建议把凡是可能超过10秒的任务都改成异步队列。另外KV免费额度里每天1000次写听着少但对你个人使用还好。如果你一时想不开用KV记录每条用户日志的递增计数那很快就会爆表。我当时就因为这个把一天的写额度耗尽过整体心路历程十分崩溃。建议所有写操作推量级时先在配置里加一层开关或采样率。5.2 日志监控的几个实用命令每次部署完我都习惯用两条命令看运行情况wrangler tail --format pretty这个是实时打印线上日志排查问题非常有用。它会一直挂着监听直到你CtrlC退出。适合在部署后立刻跑一遍确认没有报错。如果你想看历史日志使用wrangler tail --status会有点不太够用。我一般直接去Cloudflare后台的Workers - 对应Worker - Logs页面查看。注意免费套餐的日志只保留一小段时间所以出问题时尽快看。5.3 缓存与数据不一致的两个实测场景由于有CDN这个中间层Cloudflare的缓存策略很多情况下能帮你大幅加速但也容易踩坑。第一个坑静态资源的更新滞后。我一开始把图片存在R2然后通过Worker返回。当时给响应设了Cache-Control: public, max-age3600结果换了一张头像但一小时内所有访问者看到的都是旧图。后来我把需要即时更新的资源改成no-cache或者上传时把文件名加上版本号彻底解决。第二个坑D1数据的跨区域读。D1默认在主区域写入其他区域需要复制和同步。有一次我在某地中心机房附近调试写入一条待办立刻从另一个区域请求结果没读到。后来又过一个几秒再请求才出现。这个现象符合预期毕竟D1的强一致是多区域副本同步后才能做到。解决方案是关键数据立即回读时直接强制请求写入区域或者应用里做一个短时间的本地缓存补偿。个人场景不用太纠结正常使用基本无感知。5.4 免费套餐部署环境的兜底思路有人会担心万一Cloudflare某个服务出问题个人OS是不是就全瘫了我现在的做法是把最核心的几个Worker比如保存了重要数据的API另加了一个备份方案。每天定时通过Cron Triggers把D1的表导出成JSON存到R2。然后R2里的备份文件再用Cron推到另一家对象存储随便选个免费额度够用的就行实现离岛备份。这样就算Cloudflare后台临时不可用至少数据不会丢。这套方案的代价是每天多了两次API调用几乎可以忽略。但对个人OS来说数据安全永远比功能多寡优先级更高。6. 实际项目的完整代码布局6.1 多Worker项目结构参考整个cloudflare-os项目我最终调整成了monorepo结构一个仓库管理所有Worker每个子目录独立部署。结构是这样cloudflare-os/ ├── workers/ │ ├── auth-api/ # 登录鉴权 │ ├── bookmark-api/ # 书签管理 │ ├── note-api/ # 笔记管理 │ ├── file-api/ # 文件上传下载 │ ├── cron-tasks/ # 定时任务集合 │ └── short-link/ # 短链接服务 ├── packages/ │ └── shared-utils/ # 公共方法比如加密解密、日期格式化 ├── scripts/ │ ├── deploy-all.sh # 批量部署脚本 │ └── backup-db.sh # 数据库备份脚本 └── wrangler.toml # 各Worker的配置可以用include的方式拆开每个Worker的入口文件都保持极简核心逻辑抽到共享包或独立模块里。这样做的最大好处是以后要引入测试改造某个模块时不会影响其他服务。6.2 核心Worker的代码片段这里贴一段我书签API里最有代表性的代码完整展示如何同时用D1和R2。书签的数据放在D1书签缩略图的截图文件放在R2。export default { async fetch(request, env, ctx) { const url new URL(request.url); const path url.pathname; // 增删改查的REST路由 if (request.method GET path.startsWith(/api/bookmarks)) { const { results } await env.DB.prepare( SELECT * FROM bookmarks ORDER BY created_at DESC LIMIT 100 ).all(); return Response.json(results); } if (request.method POST path /api/bookmarks) { const body await request.json(); const key bookmark-${Date.now()}; const imgKey thumb-${key}.png; // 将截图存入R2 if (body.screenshotBase64) { const imgBuffer Buffer.from(body.screenshotBase64, base64); await env.FILES.put(imgKey, imgBuffer, { httpMetadata: { contentType: image/png } }); } await env.DB.prepare( INSERT INTO bookmarks (title, url, thumb_key) VALUES (?, ?, ?) ).bind(body.title, body.url, imgKey).run(); return Response.json({ ok: true, key, imgKey }); } return new Response(Not Found, { status: 404 }); } }通过这段代码你会发现整个流程并没有比传统服务器开发复杂多少但部署和运维的体验完全不在一个量级没有SSH没有systemd没有Nginx配置。6.3 用Queue处理重任务的优雅方案前面提到Worker的执行时长限制这让我开始用Queue来解耦重任务。Cloudflare的Queues产品可以当作一个简单的消息队列Worker把耗时操作的消息放进去另一个Worker作为消费者来处理。图示就不画了用文字描述流程用户上传图片 - 入口Worker把图片处理的消息塞进Queue - Queue触发图片处理器Worker - 处理器从R2拉原图 - 生成缩略图 - 存回R2 - 更新D1记录。整个过程中入口Worker只负责快速响应已收到处理在后台异步完成。这个设计让我的文件服务不再受30秒限制实测上传一个5MB的图片接口响应时间稳定在200毫秒左右而之前直接在Worker里做缩略图时经常超过1秒。7. 一套没有服务器却无比顺手的个人服务形态项目上线到现在已经跑了几个月给我最大的感受倒不是省了多少钱而是心理负担轻了很多。以前用VPS时我总有一种后院需要浇水的焦虑每次系统发安全公告都要盘算要不要升级。现在升级这种事Cloudflare在后端就处理完了我要做的只是重新部署一下Worker代码。日常使用中我的所有数据入口都收敛在这套系统里手机上的快捷指令通过Webhook写D1电脑上的浏览器插件直接调API平时写文章用的Markdown图片全部传到R2再通过Worker统一输出成可访问链接。最奇妙的一点是这套架构天然就是分布式缓存在各地机房都有响应点比我以前任何一台VPS都快。回到cloudflare-os这个名字它本质上一个比喻把我所有的数字生活挂靠在一套统一的运行时环境上。如果你也想折腾我给的建议很朴素从小做起先跑通一个API再逐步加存储和定时任务。千万不要一上来就想着把全部业务搬上去那只会让你在第一天就被配置项淹没。每天推进一点点OS会在周末前悄悄长成。