ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Cloudflare全家桶实战:基于边缘网络构建无服务器全栈应用

Cloudflare全家桶实战:基于边缘网络构建无服务器全栈应用

1. 项目定位与整体架构思路

1.1 这个“cloudflare-os”到底做了什么

先聊清楚标题的含义。很多朋友看到“cloudflare-os”这个词,第一反应是“Cloudflare 是不是出了个操作系统”,其实不是。它更像是一种工程实践的代号:把一套完整的、可用的业务系统,全部跑在 Cloudflare 的边缘网络生态上,让整个应用从静态资源托管、后端 API 执行、对象存储、数据库、鉴权认证到安全防护,都不再依赖传统服务器或云主机。

我最初产生这个想法,是因为做一个中小型项目时实在受够了“买服务器、配 Nginx、折腾 HTTPS、维护环境依赖”这一套流程。尤其是个人项目或小团队项目,流量不大但又不希望基础设施占据太多精力。Cloudflare 的 Workers、Pages、R2、KV、D1、Tunnels 这些产品和能力组合在一起,恰好能把“一套业务系统”的所有环节都搬到全球边缘网络上。你不需要在深夜爬起来处理服务器宕机,也不用考虑流量突增时扩容,因为边缘网络本身就是分布式的。

这个方案适合谁?适合那些已经能熟练写业务代码、但不想花太多时间运维基础设施的开发者,也适合做原型、做小工具、做个人作品集的程序员,甚至适合小团队跑一个体量不大的生产级应用。如果你做的业务对合规、数据本地化有严格要求,那么这套方案还需要谨慎评估,但如果你做的是互联网工具类产品、内容站、个人 SaaS,那么 cloudflare-os 这套思路真的可以把基础设施复杂度降到极低。

1.2 为什么选 Cloudflare 全家桶而不是单独混搭

通常我们去搭建一个全栈应用,比较常见的路径是:前端静态资源放在 Vercel 或 Netlify,后端接口部署在 Render 或 Fly.io,数据库用 MongoDB Atlas 或 Supabase,文件存储用 AWS S3,域名解析和 CDN 再切到 Cloudflare。听起来每个环节都选了最流行的工具,但实际维护时会发现——需要管理五六个控制台、处理五六个不同的 Bun 包、记住每个平台的部署配置和限制,集成时还要关注 API 兼容性、CORS 规则、数据迁移等一堆问题。

而 Cloudflare 全家桶的好处是,整套系统的每个环节都长在同一片生态里,运行时模型、鉴权方式、绑定关系都是同一套语言。比如 Workers 里可以直接绑定 R2、绑定 D1、绑定 KV,配置几行就好,不需要去维护一套复杂的 SDK 初始化逻辑。前端发布到 Pages 后,通过自定义域名就能无缝接入 WAF、Bot 管理、缓存规则等全套能力。这种“统一”带来的维护成本下降,在项目跑起来之后体会特别深。

另外,Cloudflare 的全球边缘节点覆盖非常广,任何地方访问都走最近的边缘节点,接口延迟很低。这点跟传统“服务器在上海,美国访问要跨太平洋”的体验完全是两回事。我对 cloudflare-os 的整体盘算是:把数据面(静态资源、接口响应)放到边缘,把状态面(数据库、缓存、队列)也尽量放到边缘产品上,最后只保留必须临时的任务或重型计算不走 Cloudflare。这个架构思路贯穿了后面所有选型和实现细节。

2. 核心组件选型与原理拆解

2.1 Workers 与 Hono 框架:为什么不用原生 Fetch

Workers 是这套系统的运行时核心。它本质上是在边缘节点上运行的 V8 isolate,不像传统服务器的进程常驻,而是“请求到来时启动、执行完成即释放”。这种模型带来的好处是天然缩容到零,没有流量时你也不花一分钱。但它也带来一些约束,比如执行时长限制、CPU 时间限制、包体积限制,所以选框架时不能像写 Node.js 后端那样随便堆依赖。

我选的是 Hono。它就是一个适配边缘运行时的极简 Web 框架,API 风格类似 Express,路由、中间件、参数校验都有,但整体体积非常小巧,压缩后只有几十 KB。你当然也可以用原生 Fetch 去写路由,写几个接口还行,但一旦涉及路由分组、中间件、鉴权钩子,代码会很快变得杂乱。

另外,Workers 的代码是直接运行在你自己的域名下的,绑定 KV、R2、D1 时不需要在代码里写 accessKey,而是在 wrangler.toml 里声明 binding,运行时由 Cloudflare 注入。这样代码里就不会出现密钥泄露的问题,也不需要单独去管理和轮换密钥。

2.2 R2 对象存储:图片与文件上传的可靠落点

R2 是这套系统里的“硬盘”。它兼容 S3 API,这意味着你之前写过的 S3 SDK 代码可以直接改个 endpoint 迁移过来,而且最大的亮点是——它不收出口流量费。做内容类应用最怕什么?怕图片被人访问量大,S3 的流量费能让你喝西北风。R2 免费额度给得也可以,存储 10GB、读操作 100 万次、写操作 100 万次,对小项目来说非常宽裕。

实际使用时,我建议不要把公开资源直接暴露成“公开读”,而是通过 Workers 做一个简单的鉴权代理,或者用 R2 的预签名 URL 功能生成临时直传链接。上传文件时,前端先请求 Workers 获取一个带时效的上传凭证,然后前端直传 R2,这样既绕过了 Workers 的执行时长限制,也能避免大量文件数据从 Worker 中转发造成流量成本。

2.3 KV 与 D1 的分工逻辑:谁管缓存,谁管业务数据

KV 是一个全球复制的键值存储,读的延迟很低,但它是“最终一致性”的,写入后需要大约 60 秒才能在全球所有节点完全生效。所以它适合存放配置项、会话状态、临时缓存、以及不要求强一致的数据。我用 KV 来存 JWT 的临时黑名单、用户偏好设置、热榜缓存。注意——不要把 KV 当成业务数据库来用,尤其是涉及账户余额、订单状态这类数据,最终一致性会给你带来事故。

D1 则是 Cloudflare 基于 SQLite 打造的 serverless 关系型数据库。它是强一致性的,支持标准的 SQL 语法,适合放用户表、文章表、评论表这些结构化数据。不过 D1 毕竟是 SQLite 内核扩展来的,自动增量备份、并发写能力都不如传统 PostgreSQL,所以设计时要主动规避高并发写入的场景。比如点赞计数,我不会直接 UPDATE 数据库,而是先写 KV 再异步批量回刷 D1,这样既保住了接口响应速度,也不会把 D1 的并发读写打满。

2.4 Tunnel 与安全边界:把内网服务藏起来

cloudflare-os 里边还有一个容易忽略但特别好用的组件:Cloudflare Tunnel。它可以让一台没有任何公网 IP 的本地机器,通过隧道直接映射到你自己的域名下。等于把 localhost 安全地暴露在公网上,但请求必须先经过 Cloudflare 的边缘节点,源站不会暴露 IP。

这个能力在开发调试和交付验证场景中非常有用。我做外部接口联调时,直接把本地开发环境起一个 Tunnel,对方访问我的临时域名就和访问线上接口一样,省去了反复部署的流程。更关键的是,如果你有一些必须在传统服务器上跑的服务(比如需要长连接、需要特定系统依赖),你依然可以通过 Tunnel 把它接入 cloudflare-os 的域名体系里,在安全层面完全收口在边缘节点。

3. 从零搭建的整体实操过程

3.1 工程结构初始化:先理清自定义域名的映射关系

我建议直接把项目按 Monorepo 组织,分成 apps/web 和 apps/api 两大部分,再加上一个 packages/shared 共享类型定义。虽然 cloudflare-os 的部署是前后端分离的,但代码放在一个仓库里,类型和工具函数可以共享,联调时不用复制定义。

先用 pnpm 初始化工作区,然后分别创建 web 和 api 两个子目录。web 端用 Vite + React 走 Pages 部署,api 用 Hono 走 Workers 部署。这里要特别提醒一下域名映射关系的规划:Pages 负责承载 www 主域名和静态资源,Workers 则绑定一个带 /api 前缀的子路径,比如 api.yourdomain.com 或者 customdomain.com/api/*。你可以在 Cloudflare Dashboard 的 Workers 路由配置里把接口路径都指向同一个 Worker,这样前后端域名的 cookie 作用域也比跨域更友好。

3.2 Workers API 的具体实现:绑定 R2、D1、KV 的配置写法

有一个容易踩坑的点:本地开发和线上环境对 binding 的处理方式不一样。线上可以在 wrangler.toml 里直接配置,本地开发则建议用 wrangler dev 的 --remote 参数去连线上资源,避免本地还得搭一套模拟环境。我的 wrangler.toml 看起来像这样:

name = "cloudflare-os-api" main = "src/index.ts" compatibility_date = "2024-11-01" workers_dev = false routes = [ { pattern = "api.yourdomain.com", zone_name = "yourdomain.com" } ] [[kv_namespaces]] binding = "SESSION_KV" id = "<你的KV命名空间ID>" [[d1_databases]] binding = "DB" database_name = "cloudflare_os_db" database_id = "<你的D1数据库ID>" [[r2_buckets]] binding = "FILE_BUCKET" bucket_name = "cloudflare-os-files"

注意 routes 这一段,如果你希望 Workers 只处理 api 子域名的所有请求,就按上面的写法。如果希望让某个主域名下的所有 /api/* 都走 Worker,那就用 pattern = "yourdomain.com/api/*"。如果是纯 Pages 项目通过 next-on-pages 之类的方式集成,那还需要额外处理函数的编译,我这次没有走那条路线,而是保持前后端彻底分离,职责更清晰。

然后看接口代码。我用了 Hono 的 R2 绑定,实现一个最简单的文件上传获取签名 URL 的接口:

import { Hono } from 'hono' import { getSignedUrl } from './r2-helpers' type Bindings = { FILE_BUCKET: R2Bucket SESSION_KV: KVNamespace DB: D1Database } const app = new Hono<{ Bindings: Bindings }>() app.post('/upload/ticket', async (c) => { const { fileName, contentType, size } = await c.req.json() // 这里可以做用户鉴权,校验通过后签发一个5分钟有效的直传链接 const url = await getSignedUrl(c.env.FILE_BUCKET, fileName, contentType, size) return c.json({ ok: true, url }) }) export default app

生成预签名 URL 时需要用到 S3 兼容签名算法,代码里我封装了 crypto 签名逻辑,这块网上有现成的库可以做,也可以用@aws-sdk/s3-request-presigner配合 R2 的 S3 endpoint。实测下来直接传 R2 真的很稳,文件不上 Worker,请求体不用在边缘节点和 R2 之间做二次转发。

业务数据方面,D1 的初始化就是建表:

CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY, email TEXT UNIQUE NOT NULL, nickname TEXT NOT NULL, created_at INTEGER NOT NULL );

Workers 里执行 D1 查询非常简单:

const { results } = await c.env.DB.prepare('SELECT * FROM users WHERE email = ?') .bind(email) .all()

如果你习惯了 ORM,可以直接上 Drizzle ORM,它支持 D1 的 driver,类型安全体验很好。但如果查询不复杂,我建议直接用 SQL 写,少一层抽象,排查问题更直接。

3.3 前端 Pages 与 API 的对接细节

Pages 部署静态资源没什么好讲的,构建命令配好就行。我要重点说的是前端请求 API 时的鉴权和域名策略。推荐用同一个主域名,让 Web 端调用/api/*走 Workers 路由,这样不需要跨域配置,cookie 也能自动带上,省掉 CORS 的很多麻烦。

如果你就是不喜欢同域,非要前后端分属不同子域名,那就要在 Workers 里显式配置 CORS 头,同时处理 OPTIONS 预检请求。还有一个坑:无论你配置 Access-Control-Allow-Origin 为*还是具体域名,cookie 跨域的时候都不能用*,必须指明具体来源,并且要求前端请求带上credentials: 'include'。

实际项目里我还会在 Workers 中间件里统一注入安全头:HSTS、X-Content-Type-Options、X-Frame-Options 这几个是基础的。然后公共 API 加一个简单的速率限制,Cloudflare 默认有 DNS 级别的防护,但应用层面我依然自己写了一个基于 KV 计数的限流中间件,确保单个 IP 每秒最多打 10 次接口。代码如下:

app.use('/api/*', async (c, next) => { const id = c.req.header('cf-connecting-ip') || 'unknown' const key = `rate:${id}:${Math.floor(Date.now() / 1000)}` const count = await c.env.SESSION_KV.get(key) if (count && Number(count) >= 10) { return c.json({ error: 'too many requests' }, 429) } await c.env.SESSION_KV.put(key, String(Number(count || 0) + 1), { expirationTtl: 5 }) await next() })

这里cf-connecting-ip是 Cloudflare 在边缘注入的真实客户端 IP 头,不要用 request 里默认的 IP 字段,那个在套了 CDN 之后一定是 CF 节点的地址。

3.4 部署发布与持续集成的配置思路

发布这件事,在 cloudflare-os 里简单得有点不真实。后端只需要在项目根目录执行:

wrangler deploy

前端构建产物推到 Pages 仓库,Cloudflare 检测到分支更新就会自动构建发布。我实际工作中的流程是:GitHub 上建两个 workflow,一个在 push 到 main 时自动部署 API,另一个把最新 web 代码推送到 Pages 绑定的仓库分支,实现前端自动上线。

为了部署过程更顺畅,我强烈建议本地安装 wrangler 并预先登录,因为第一次跑wrangler deploy时会引导你去浏览器授权,CI 环境里没有浏览器权限时容易卡住。CI 里直接通过环境变量配置CLOUDFLARE_API_TOKEN和CLOUDFLARE_ACCOUNT_ID就能无感部署了。

此外,Pages 绑定自定义域名之后,可以顺手把 SSL/TLS 模式调成“灵活”或“完全(严格)”。我推荐用“完全(严格)”,因为 Pages 会自动签发证书,选择这个模式能保证边缘到源站之间的连接也走加密,避免中间人截取风险。Cloudflare Proxy 开启状态下,所有访客的真实 IP 都由 CF 处理,安全层面相当省心。

4. 常见问题与排查技巧实录

4.1 KV 取不到最新值:最终一致性带来的坑

这个坑几乎人人都会遇到。你刚用 KV 写入了一个新值,紧接着读取,发现拿到的还是旧值。这是因为 KV 数据从写入边缘节点到全球节点同步需要时间,不是写后立即可见。解决方式很简单:区分业务属性。对一致性要求高的数据放在 D1 里,KV 只放“容忍几十秒延迟”的数据。

我自己的做法是把 KV 的 key 设计成带版本号的格式,比如user:profile:{userId}:{updatedAt},读取时先从 KV 拿,没拿到再回源 D1 并把结果回填到 KV。这样即使 KV 延迟生效,接口也能通过 D1 兜底拿到最新值。还有个小技巧:配置cacheTtl区别对待冷热数据,热数据 TTL 设短一点,避免缓存堆积。

4.2 R2 的 CORS 直传问题和公开桶的坑

用 R2 做直传时,最容易遇到的问题就是浏览器侧的 CORS 失效。即使你通过 Workers 生成了预签名 URL,如果 R2 桶没有配置正确的 CORS 规则,浏览器直传时依然会被拦截。配置方法是在 R2 管理界面的“设置”里添加 CORS 策略,AllowedOrigin 填你的前端域名,AllowedMethod 填 PUT、GET,AllowedHeader 填 Content-Type 和 Authorization。

另一个需要注意的坑是证书和自定义域名的绑定。R2 的默认访问域名是r2.dev结尾,体验不太好且有限速。我建议通过“自定义域名”把 R2 桶映射到自己的子域名,比如files.yourdomain.com,这样既可以进一步挂到 Cloudflare 的缓存加速下面,还能统一用主站域名体系来维护证书。绑定好之后,预签名 URL 的主机名也会变成你自己的域名,体验会很统一。

4.3 D1 的 SQL 限制和并发写问题

D1 虽然以 SQLite 为内核,但并不是无限能力。它的本地复制节点主要在主区域,跨区域访问会稍慢。所以在建库时要指定主数据中心,而且尽量把你业务的主要用户群体所在区域作为主区域。如果全球用户都需要低延迟访问,D1 不如传统集中式数据库主从架构灵话。

写并发是另一个要重视的问题。D1 对单条记录的写并发有限制,我碰到过一次评论服务被瞬时刷爆的情况,大量 UPDATE 操作打到同一条记录上,出现持续的错误。最后改成“先更新 KV 计数,再用队列慢慢把计数回写到 D1”,高峰期问题立刻消失了。所以做这类系统时要早早在设计层面区分“热路径”和“最终路径”,不能让边缘上所有实时请求都打到最底层的数据库。

4.4 免费额度与限流:怎么让项目跑得长久

Cloudflare 的免费额度对个人项目非常友好,但并不是无限详情。Workers 免费版每天有 10 万次请求,KV 每天读写各 1000 次免费,超出后按量计费或直接限流。R2 的流量免费是亮点,但操作次数依然有上限。我个人的经验是:在架构设计里加一层“缓存优先”策略,所有可缓存的接口都设置合理的 CDN cache TTL,静态资源的缓存时间直接设为一个月以上。

还有一点容易被忽略:Workers 的 CPU 时间限制。免费版单个请求的 CPU 时间为 10 毫秒。如果你的接口有复杂的计算或大 JSON 解析,很容易超时。解决方案是把重计算挪到前端做,或者把结果提前预计算存入 KV/R2。我在实际项目中遇到过用 Workers 做 OpenGraph 图片合成,这个操作太吃 CPU,超时概率很高。后来我把合成任务放到 R2 队列异步处理,请求端只拿最终 URL,体验完全不同。

5. 一些实用扩展:Analytics 与访问日志处理

很多人在搭建完系统后,最容易忽略的一环是日志和用户行为分析。传统方案是接第三方统计 SDK,但数据要发给别人,总有点不放心。Cloudflare 有一套边缘侧的 Web Analytics,只需在 Pages 里加一小段代码就能无侵入地统计页面访问量和来源,不需要把数据发到第三方。

接口层面的埋点和日志,我推荐用 Workers Logs 加一个自定义的 JSON 日志采集函数,把关键时刻的耗时、请求体大小、错误码统一聚合到 R2 里。每天凌晨用一个定时 Worker 把前一天的 JSON 日志聚合成摘要,写到 D1 的报表表里。这样整个系统自包含,没有一个环节依赖外部的数据管道,数据这块的隐私问题也被最小化了。

关于成本控制再提一句:R2 的存储成本极低,适合长期存放日志和静态文件;但 KV 和 D1 的数据量增长需要注意,尤其是 D1 的存储计费是按字节数来的。为了避免无限膨胀,养成给 D1 表加“归档过期”的习惯,定期把老数据导出到 R2 后删掉,保持核心数据库一直轻快。

6. 最后再分享一点个人心得

跑 cloudflare-os 这套体系到现在,最直观的感受是“运维基本不存在了”。以前做一个小项目,最焦虑的是半夜收到服务器报警短信,现在这个焦虑彻底消失了。Cloudflare 的稳定性很强,边缘网络的容错能力远超我自己搭一台单机服务器。

但我也要说清楚,它并不是万能的。如果你的业务依赖 WebSocket 长连接,Workers 目前的状态支持不那么理想,你还是要靠 Tunnel 去映射一个传统服务;如果你的数据量级到了几十 GB 且有复杂聚合查询,D1 也会吃力,还是要引入专门数据库。cloudflare-os 最适合的状态是:业务逻辑清晰、数据量可控、希望用最少的运维成本跑出全球低延迟体验的中小型应用。

最后分享一个我踩过几次坑后学到的习惯:每次修改 wrangler.toml 里的绑定配置,先跑一遍wrangler dev --remote确认线上资源都能连得通,再执行wrangler deploy。一次部署失败往往不是业务代码的问题,而是绑定 ID 填错、区域不匹配、或者变量名不一致。本地的tsc --noEmit也要每次跑,Workers 运行时对类型定义比较严格,运行时才发现错误会浪费好几轮排查时间。

如果你也准备把业务搬到边缘网络上,我建议先拿一个非核心的小工具练手,跑通全流程之后再逐步迁移核心服务。这样踩坑的成本低,而且能快速建立起对整个体系的信任感。等你好几个月没有打开过“维护”页面的时候,回头再来看 cloudflare-os 这套方案,会觉得当初的折腾非常值得。

返回列表