
我用个人项目这几年有个很深的感触业务逻辑本身往往不难真正消耗精力的全是那些“围绕在代码旁边的事”。服务器要不要续费、证书是不是快过期了、配置又改崩了、半夜被报警叫醒去查日志……有一段时间我甚至觉得独立开发者的宿命就是半个运维。直到我把“服务”这个概念从一台机器上彻底挪开用 Cloudflare 的全家桶重新组装了一遍自己的业务还顺手给这套方法起了个代号叫 cloudflare-os。我说的 os 不是传统意义的操作系统而是一个组合出来的“边界系统”用 Workers 跑逻辑用 Pages 托管前端用 D1 存关系数据用 KV 做高速缓存用 R2 放文件再用一套统一的路由规则把它们全部挂载到同一个域名下。整个过程不需要买服务器、不需要手动维护镜像、不需要配 Nginx甚至 SSL 证书和 DDOS 防护都不用关心。听起来像不是有点耳熟很多人觉得这不就是个 Serverless 网站吗能有多特别。但当你真的把所有服务都抽象成“OS 里的进程、磁盘、内存”之后开发体验是会被抬到另一个高度的。这篇文章是我从零把 cloudflare-os 这套方法落地之后的实操笔记。适合那种手头有几个小项目、不想再伺候物理机器的开发者也适合想用低成本跑一个小型生产环境的小团队。我会把设计思路、组件选型、完整搭建过程、踩过的坑全部拆开给你看你照着走就能把之前的 VPS 项目迁过来。1. 项目整体设计与思路拆解1.1 cloudflare-os 究竟是一个什么东西很多人听到 Cloudflare 的第一反应是 CDN第二反应是“那是不是只能加速静态页面”。早几年确实是这样但现在已经完全变了。现在的 Cloudflare 更像是一个覆盖全球 300 多个城市的分布式运行时你上传一段代码它会被部署到距离用户最近的数据中心里执行用户访问你的域名时背后不是一台固定的服务器而是一整套分布式的边缘计算网络。cloudflare-os 做的事情是把这个分布式网络按照操作系统的逻辑重新组织一遍Workers 就是 CPU 里的计算进程代码跑在它里面。Pages 就是文件系统里的常规目录承载前端静态文件和构建产物。KV 是内存缓存层适合高频读、低频写。D1 是磁盘上的关系数据库事务、索引、SQL 全都能用。R2 是对象存储类似外接硬盘。Tunnel 是网络接口把本地运行的开发服务安全地暴露到公网。这样的抽象有个非常大的好处以往你要搞一台服务器要担心的东西是 CPU 够不够、磁盘满没满、内核有没有漏洞、SSH 是不是又被扫了。而在 cloudflare-os 里单个组件可以被单独替换和横向扩展你不需要关心硬件只需要关心你的代码和配置。所以我的看法是它不是某个具体的软件安装包而是一种资源组织方式。1.2 为什么我放弃了传统的 VPS 和容器方案我做技术选型的时候很少只看“能不能跑通”。我更喜欢先算一笔账我要花多少精力去维护这件事。以前我有一台 2 核 4G 的 VPS只要项目开始稳定运转运维工作就自动冒出来了。首先是系统更新半个月不补丁心里就发慌其次是安全加固改 SSH 端口、装 Fail2ban、配 firewalld再往后是体检日志发现磁盘占用要清理、发现进程异常要排查。最让我崩溃的一次是某个深夜数据库突然把磁盘写满服务直接挂掉我在被窝里爬起来翻监控。后来我就想我需要的不是一个“可以控制一切的服务器”而是一个“能让我专心写业务逻辑的地方”。Cloudflare 免费版和 Workers 免费计划组合起来能承受的流量对小项目来讲完全够用。更关键的是分布在边缘节点上的代码天然具备容灾和负载均衡你不需要考虑单点故障。对比传统 VPS节省的不只是那几十块钱月租而是每周两三次的“例行维护”时间。对我这种个人开发者和几个人的小团队来说时间是比机器更值钱的资源。1.3 核心组件选型一览第一次接触 Cloudflare 页面的时候你会发现产品非常多容易脸盲。我在这儿先整理一张我在 cloudflare-os 里的组件分工表后面章节的实操都是基于这套分工来做的组件在 OS 中的类比主要用途选用理由Workers进程跑 API、业务逻辑、中间件部署简单按请求计费天然边缘执行Pages文件目录托管前端 SPA、静态站点支持 Git 集成自动构建自带 CDNKV内存缓存配置信息、临时会话、高频读取数据全球分布读取低延迟D1关系型数据库用户表、订单表、任何需要事务的数据兼容 SQLite免费额度内足够用R2对象存储图片、文件、备份数据零出口流量费和 S3 类似Tunnel网络接口让本地服务通过 Cloudflare 节点对外访问不暴露本机端口自带加密和防护Access访问控制网关给你的后台页面加统一登录认证不需要自己写复杂的鉴权逻辑Rules路由规则把不同路径分配到不同服务一个域名下统一编排所有子服务这张表基本上就是整个架构的地图。不要一次性把这些全塞进一个项目里而是按需引入。项目第一版我只用了 Workers、Pages、D1 和 KV后来才加了 R2 和 Tunnel。2. 核心细节解析与实操要点2.1 计算与静态资源分离为什么要把 Workders 和 Pages 拆开我在设计接口时有一个习惯动态和静态永远分开。Pages 管静态资源Workers 管动态逻辑两者通过路由挂到同一个域名之下。为什么要这么拆分最重要的原因是部署节奏不同。前端可能一天要改好几次样式后端接口则要尽量保持稳定。如果它们混在一个服务里每次前端改动都得连带把整个 Worker 重新部署一次风险隔离做不好。Pages 的构建机制绑定 GitHub 或 GitLab你每次 push 代码云端都会自动拉取、安装依赖、构建、部署。前端开发流程简直可以简化成“合并分支即发布”。Workers 的部署则完全走命令行提交代码后执行wrangler deploy src/index.ts服务就更新了。这样前端和后端各自有各自的发布节奏互不阻塞。路由层面我非常推荐用子域名而不是路径来区分。比如myapp.com指向 Pages承载首页和前端资源。api.myapp.com指向 Worker承载所有后端接口。files.myapp.com指向另一个 Worker 并绑定 R2承载上传和文件访问。admin.myapp.com指向 Pages并且在前面加一道 Access 登录认证。这种方式在 Cloudflare 的仪表盘里配起来很快。点击对应资源选“自定义域”输入子域名然后等几秒钟 SSL 证书自动签发。相比以前要手动配置 DNS 记录 Caddy/Nginx 反代这里已经把流程压缩到近乎“填表即生效”。2.2 D1 和 KV数据层里两种存储的边界在哪里数据是任何系统的底座cloudflare-os 里最容易被混淆的就是 KV 和 D1。KV 是“全局键值存储”它背后的传播模型是最终一致性。也就是说你在一条边缘节点写入的数据不会立刻同步到全球所有节点可能需要一两秒才能让其他节点看到。KV 特别适合那些“多读少写、偶尔一致也没关系”的数据比如网站的主题配置、功能开关、菜单列表。D1 则完全不同。它基于 SQLite进行一次查询时会去主库拿数据所以它是强一致的。你写入一条记录立刻就能查出来而且支持多行事务、外键、索引这些传统数据库的能力。代价是 D1 的查询速度比 KV 要慢一些毕竟 SQLite 引擎的解析和索引查询都需要时间。我一般通过这几个维度来决策数据会被频繁修改吗会就用 D1。数据需要跨表关联吗需要就用 D1。数据很少变化但每秒钟要被读几千次用 KV。数据能容忍几秒钟的迟到吗能用 KV不能用 D1。我曾经做过一个 JSON 接口原本放 D1后来查了日志平均响应 30ms。我把数据迁到 KV 之后响应降到了 10ms 以内。但反过来有个项目我直接用 KV 存了用户订单结果用户刚下单就去查详情偶尔出现查不到因为 KV 还没同步。这就是没分清边界的反面教材。现在 cloudflare-os 里的通用做法是“写操作走 D1读多改少的聚合数据再主动同步一份到 KV”。这样两条数据链路的优势都能利用起来。2.3 身份认证一个较轻量但完整的 Token 方案边缘计算环境下服务是多节点并行的没有任何一台机器能保存“会话状态”。所以你千万不能按照传统思路在进程内存里存 Session。cloudflare-os 里我最常用的认证方案有两种第一种适合个人项目或内部接口直接在 Worker 环境变量里存一个静态 token每个请求进来先比对Authorization头。这个办法零依赖、执行起来飞快适合做 cron 定时任务用的内部触发接口或者你只想挡住绝大多少无关调用。第二种适合真实用户体系使用 JWT。用户登录后签发一个带签名的 token之后的每次请求 Worker 只需要验签不需要查询会话表。关键代码如下function createAuthMiddleware(env: Env) { return async (request: Request): PromiseResponse | null { const header request.headers.get(Authorization); if (!header || !header.startsWith(Bearer )) { return new Response(Unauthorized, { status: 401 }); } const token header.slice(7); // 用 Web Crypto 做 HMAC 验签 const encoder new TextEncoder(); const keyBytes encoder.encode(env.AUTH_SECRET); const cryptoKey await crypto.subtle.importKey( raw, keyBytes, { name: HMAC, hash: SHA-256 }, false, [verify] ); // 解析 payload const parts token.split(.); const valid await crypto.subtle.verify( HMAC, cryptoKey, // 这里的 signature 需要 base64url 解码 base64UrlDecode(parts[2]), encoder.encode(${parts[0]}.${parts[1]}) ); return valid ? null : new Response(Unauthorized, { status: 401 }); }; }这里的关键点是验签用的是标准 Web Crypto API不需要引入任何第三方 JWT 库Workers 原生支持。你可以把这种 middleware 放到 worker 请求处理的最前面凡是需要鉴权的接口统一过一层校验。如果某个页面是后台管理界面也可以直接配上 Cloudflare Access 在边缘网关层做拦截那样你的业务代码里连鉴权都不用写了。2.4 路由与域名一个域名调度全部子服务我经常把 cloudflare-os 比作一个“城市交通系统”域名就是主干道路由规则就是交通指示牌。Cloudflare 的路由能力可以精确到“哪个域名 哪个路径”匹配到哪个 Worker 或 Pages 项目。比如routes [ { pattern api.example.com/*, zone_name example.com }, { pattern files.example.com/*, zone_name example.com }, ]这里的pattern支持通配符也能写详细的路径比如api.example.com/admin/*。当用户访问api.example.com时Cloudflare 会在边缘层直接把请求转发给你指定的 Worker。没有 Nginx没有负载均衡器没有端口监听所有调度全部由 Cloudflare 的全局代理执行。你甚至可以在同一个域名的不同路径上挂不同的 Worker。有些版本不支持重叠路由但大部分情况只要路径前缀不冲突就直接可用。要注意的一点是路由规则在“Workers Routes”里面配置时如果同时有一个 Pages 项目也匹配了这个域名那么两者之间会存在竞争优先级标准和坑我会在第 4 章详细说。总之先记住一个原则域名路径规划在项目开始时就做好别等到服务多了再改否则 DNS 缓存和 Worker 预热过程会让你排查起来很崩溃。3. 实操过程与核心环节实现3.1 初始化项目与 Wrangler 配置首先要做的就是安装官方命令行工具 Wrangler。它是操作整个 cloudflare-os 的“终端入口”所有的部署、日志、数据库配置都通过它完成。npm install -g wrangler wrangler login登录之后运行wrangler whoami确认账号已经关联。接下来创建项目npm create cloudflarelatest cloudflare-os -- --type workers --lang ts cd cloudflare-os这个命令会生成一个 TypeScript 类型的 Workers 项目模板自带wrangler.toml配置文件和最小入口文件。我的完整配置文件长这样name cloudflare-os main src/index.ts compatibility_date 2024-11-01 workers_dev true [[d1_databases]] binding DB database_name os_db database_id 你没有创建的id可以先留空 [[kv_namespaces]] binding KV_STORE id 留空首次绑定后再填 routes [ { pattern api.example.com/*, zone_name example.com } ]可以看到d1_databases和kv_namespaces的 ID 都还没确定。你需要先创建对应的资源wrangler d1 create os_db wrangler kv namespace create KV_STORE命令会把生成的 id 输出在终端里你复制回填到wrangler.toml中即可。这段是在项目里非常有价值的一步因为很多教程只让你在 Dashboard 界面点点完之后没有把 ID 填到本地配置导致部署完全失败。3.2 编写核心 Worker认证、接口和数据库访问现在来写真正跑在边缘逻辑。为了方便拆解我设计一个最小可用的“笔记接口系统”包含三个接口健康检查、创建笔记、列出笔记。export interface Env { DB: D1Database; KV_STORE: KVNamespace; AUTH_SECRET: string; } export default { async fetch(request: Request, env: Env): PromiseResponse { const url new URL(request.url); const authMiddleware createAuthMiddleware(env); const authResult authMiddleware(request); if (authResult ! null) return authResult; if (url.pathname /health) { return Response.json({ status: ok }); } if (url.pathname /notes request.method GET) { const result await env.DB.prepare( SELECT * FROM notes ORDER BY created_at DESC LIMIT 20 ).all(); return Response.json(result.results); } if (url.pathname /notes request.method POST) { const body await request.json{ content: string }(); const result await env.DB.prepare( INSERT INTO notes (content, created_at) VALUES (?, ?) ) .bind(body.content, new Date().toISOString()) .run(); return Response.json({ id: result.meta.last_row_id }); } return Response.json({ error: Not Found }, { status: 404 }); }, };这里面有几个细节值得单独说authMiddleware会普通所有请求所以健康检查也被保护了。如果你的健康检查需要被外部监控系统访问可以单独放一个不做认证的路径或者把 AUTH_SECRET 设成监控系统相同的 token。D1 的查询语法就是 SQLite 的语法几乎零学习成本。?是绑定参数的占位符用.bind()传入值在边缘环境下这也能有效规避 SQL 注入。返回 JSON 我没有手动设置Content-Type直接用的Response.json()省掉一截样板代码。写完之后本地运行一下wrangler dev --remote wrangler d1 execute os_db --remote --file./schema.sqlschema.sql里就是建表语句我这里创建一个notes表CREATE TABLE IF NOT EXISTS notes ( id INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT NOT NULL, created_at TEXT NOT NULL );本地验证通过后直接部署wrangler deploy命令输出里会给出workers.dev下的临时域名以及你自定义路由的最终地址。这时候去访问api.example.com/notes如果返回 401说明认证中间件起作用了带上 Header 之后就能正常读写数据。3.3 部署 Pages 前端并联调路由后端能跑了前端也得配上。我用 Vite 搭一个极简的页面负责展示笔记列表。npm create vitelatest frontend -- --template react-ts cd frontend npm install npm run build构建完成之后最直接的方式是登录 Cloudflare Dashboard在 Pages 里“创建项目”然后选择“直接上传资产”把你dist目录上传上去。不过我更推荐直接把仓库连到 GitHub这样每次 push 后 Pages 自动构建省到人工上传。上传完成后为 Pages 项目绑定一个自定义域名比如example.com。此时你的主页应该可以正常访问了。接着让前端代码请求 API 时带上 token。为了避免把 token 暴露在浏览器里面可以把这个复用的 token 放在一个只存在于 Worker 端的环境变量里前端代码通过一个代理接口来取。更常见的做法是让 Worker 直接作为 API 网关前端页面通过AUTHORIZATION请求头把用户登录态的 token 带上。具体贴代码就不展开了核心思路是Pages 只负责静态资源渲染所有动态请求都打到api.example.com这个 Worker 上跨域问题通过 Worker 里的 CORS 中间件解决。一个简单 CORS 中间件可以这样写export function corsHeaders(request: Request): Recordstring, string { const origin request.headers.get(Origin) || *; return { Access-Control-Allow-Origin: origin, Access-Control-Allow-Methods: GET,POST,PUT,DELETE,OPTIONS, Access-Control-Allow-Headers: Content-Type, Authorization, }; }在 Worker 的 fetch 处理逻辑里遇到 OPTIONS 请求直接返回 204。前端就可以在example.com的页面安全地调用api.example.com的接口了不需要在网关层做任何额外代理。3.4 打通本地联调与自动化部署项目还没结束工程化到这一步接下来要考虑的是“怎么让团队协作和部署变成流水线”。wrangler dev --local会在本地用 Miniflare 模拟一套完整的运行时包括 D1、KV 和 Workers 的绑定关系。你不用担心改了代码会影响线上本地模拟器就是最佳的联调环境。我推荐在项目根目录放一个scripts/init-db.sh用来执行迁移#!/bin/bash wrangler d1 execute os_db --remote --file./schema.sql自动化发布我会用一个 GitHub Actions 工作流。每次 push 到 main 分支自动跑测试、构建、部署name: Deploy cloudflare-os on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - uses: actions/setup-nodev4 with: node-version: 20 - run: npm install - run: npm run build - name: Deploy Worker run: npx wrangler deploy env: CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }}这里的关键是要在 Cloudflare 后台生成一个 API Token并添加到 GitHub Secrets 中。Actions 部署时就不用每次手工登录速度大概只需要二三十秒。这套流程下来主分支更新代码等于自动发布我一度觉得我的“操作系统”已经开始自我维护了。4. 常见问题与排查技巧实录4.1 免费额度边界别把免费套餐当作无限资源cloudflare-os 用起来很爽但免费额度必须心里有数。我刚开始的时候因为一个小脚本疯狂刷新 KV 导致额度超限整个 Worker 被暂停来一上午。Cloudflare 给的免费额度大约是资源免费额度大概值我的建议Workers 请求每天 10 万次对个人项目足够但别跑无人值守的高频轮询KV 读写读 10 万次/天写 1 千次/天写入量是最大瓶颈批量写要注意D1 读行数每天 500 万行查询条件要带 LIMIT避免全表扫描D1 写行数每天 10 万行事务里不要做无谓的 UPDATER2 存储10GB 存储图片类应用很容易超需定期清理Pages 构建每月 500 次不要频繁触发无意义的重构我的建议是进 Dashboard 设置用量提醒或者每周手动看一次用量曲线。万万不能等到被限流了才发现某个接口遍历了全表。4.2 KV 写入后立刻读到旧数据怎么办有一次我做了个“公告开关”管理端写入 KV 后立马调接口验证结果发现页面还是旧状态。排查了很久才发现 KV 是全球分布式存储写入的是某个边缘节点而读请求被路由到了另一个节点那边还没有同步完成。想解决就要分场景如果只是标题、状态这种可以容忍秒级延迟的数据直接在响应头加上Cache-Control: no-cache让页面不要在浏览器端缓存。如果这个数据必须强一致那就改存 D1或者写入时顺带在请求里拿到 KV 写入确认等待几十毫秒后再返回。如果你的业务对一致性要求极高那么从一开始就不该考虑 KV就像我前面讲的用 D1。后面我再做类似的“开关”类功能直接写 D1没有再出现这个问题。4.3 路由绑定和自定义域名里的坑汇总workers_dev true时每个 Worker 都会有一个xxx.workers.dev的免费子域名。但如果你想用自己的域名必须保证域名已经在 Cloudflare 接入。接入通常要求你的域名 DNS 服务器改成 Cloudflare 提供的两个地址。改完之后解析记录通常会保留原来的值但由 Cloudflare 作为权威 DNS这个变更可能需要一两个小时生效。另一个我踩过的坑是给 Pages 项目绑定自定义域时只有在 DNS 选择“CNAME”别名的时候才支持根域名老用户有时候默认生成的是 A 记录需要手动改成 CNAME。如果绑定后一直显示待验证不用太慌等半个小时再看可能是 DNS 传播没完成。最后是路由优先级问题如果你同时给example.com/notes/*创建了一个 Worker 路由又给 Pages 绑定example.com作为自定义域两者可能存在冲突。Cloudflare 对 Pages 自定义域的解析优先级有时候会高于 Workers Routes。操作上我的建议是尽量用子域名区分避免路径级路由和 Pages 自定义域打架。4.4 冷启动与响应延迟的实测优化很多人对 Serverless 的抱怨是冷启动耗时。Workers 本身冷启动控制得比较好通常只有几毫秒到几十毫秒远好于普通的云函数。但如果你在代码里引用了体积巨大的 npm 包那冷启动时依赖解析和模块加载都会被拖慢。我实测过几个优化点效果非常明显部署时加--minify压缩代码体积。把node_modules中不用的模块删干净能用原生 API 的情况下不要引入第三方库。尽量把“配置”放在环境变量或 KV 里不要在模块初始化时做大量计算。响应时能缓存就缓存用caches.default把重复请求的响应直接打回去。const cache caches.default; const cached await cache.match(request.url); if (cached) return cached; // 业务逻辑 const response new Response(body, { headers: { Cache-Control: max-age300 } }); await cache.put(request.url, response.clone());加缓存后接口平均响应从 20ms 降到了个位数毫秒。对于爬到边缘节点上的流量这个改善非常可观。5. 进阶扩展能力与阶段性心得分享5.1 给 cloudflare-os 挂上文件磁盘R2 的接入系统跑顺之后自然会想要文件能力。R2 的接入比 S3 简单太多它兼容 S3 API但你不需要操心密钥轮换和桶权限。我用一个 Worker 把 R2 包成了简单的“图床接口”async handleUpload(request: Request, env: Env): PromiseResponse { const formData await request.formData(); const file formData.get(file) as File; const fileName ${Date.now()}-${file.name}; await env.MY_BUCKET.put(fileName, file.stream(), { httpMetadata: { contentType: file.type }, }); return Response.json({ path: /files/${fileName} }); }然后创建另一个 Worker 负责读取文件const object await env.MY_BUCKET.get(name); return new Response(object.body, { headers: { Content-Type: object.httpMetadata.contentType } });R2 最让人安心的一点是没有出口流量费。对图片、视频这类高频率的读请求成本结构比传统对象存储清晰得多。把 R2 的域名也挂到files.example.com下整套系统从“能跑 API”升级成了“能存文件”。5.2 同步请求改异步接入 Queues 与 Cron Triggers如果你做过数据导入、导出、定时抓取这类任务就会知道边缘函数不适合跑超过 30 秒的长耗时操作。Cloudflare Queues 可以把任务推给后台队列由 Worker 按批消费。Cron Triggers 则能帮你做定时轮询。这两样的组合对 cloudflare-os 的扩展能力非常关键。我引入 Cron 之后做了个“每天早上 8 点汇总前一天笔记并发送邮件”的小工具。只需要在wrangler.toml里配置[[triggers]] crons [0 8 * * *]然后 Worker 里监听scheduled事件在指定的时间执行逻辑。这不像传统 Cron 要保证服务器开机边缘平台天然让定时任务分布在世界各地按 UTC 时间触发。5.3 关于这套方法的几条真实体会把项目跑到现在我最深的感受是cloudflare-os 与其说是一种技术选型不如说是一种思维模式。以前写代码的时候潜意识里总有一台“中心机器”的概念所有的状态都往这台机器上堆。后来我学会把系统拆成“请求处理、数据存储、外部服务”三个维度每个维度都能独立部署和扩展思路就清楚太多了。另外一点是不要一次性把所有 Cloudflare 产品全都用上。我很长一段时间只用 Pages Workers D1直到需要文件上传了才加 R2需要定时任务了才去开 Cron。每个组件都等到业务明确需要的时候再引入系统一直保持足够简洁。如果一上来就把所有产品都堆上去调试一个跨模块故障会非常痛苦。至于这个项目未来还要怎么走我目前正在尝试把 Workers AI 接进来做一个小型的语义搜索把笔记内容自动做向量化。Cloudflare 的 Workers AI 支持本地跑模型不需要自己管理 GPU对个人项目来说又是一个非常友好的入口。等我把这块跑通了再写一篇专门的文章分享。现在这套 cloudflare-os 已经稳定运行了半个自然年我没有半夜运维过没有手动清过磁盘甚至没有担心过证书过期这种体验就是我会持续用它的理由。