Node 后端实战 · 为什么用 Cloudflare Workers + D1 扛起了整个多租户 SaaS 后端?架构决策全景复盘
Node 后端实战 · 为什么用 Cloudflare Workers + D1 扛起了整个多租户 SaaS 后端?架构决策全景复盘
各位看官,这篇聊聊我最近做的一个多租户 SaaS 后端。技术栈没有走"买服务器、装 Postgres、再配 Nginx"的传统路线,而是选了Cloudflare Workers + D1(边缘 SQLite)+ KV + R2 + Hono + Drizzle的全 serverless 边缘栈。
选型阶段我其实也犹豫过——SQLite 跑 SaaS,还是边缘的,不少人第一反应是质疑。但项目上线跑了一段时间,回过头看,当时那几个架构决策基本站得住。今天把拍板的逻辑和踩过的坑一并讲清楚,供同样想轻运维上云的同学参考。
一、为什么是 serverless 边缘栈,而不是传统服务器
最朴素的原因是:我不想长期养一台服务器。租 ECS 要一直付钱,哪怕半夜没流量;扩容得自己盯监控;数据库主从、备份、打补丁样样都要管。而这个 SaaS 的体量也就几十个租户、日活几千,犯不着为这点规模专门投入运维精力。
Cloudflare Workers 的思路正好相反:
- 按量计费、闲置不花钱:没有请求就不产生计算费用,对小项目很友好。
- 全球边缘、就近执行:Worker 跑在 Cloudflare 几百个边缘节点上,请求落到离用户最近的节点,延迟天然低。
- 冷启动几乎为零:毫秒级启动,体验上和常驻服务没有明显差别。
- 数据库跟着 Worker 走:D1 是 Cloudflare 托管的 SQLite,和 Worker 同区域部署,读写延迟极低,不需要自己维护连接池。
对中小体量、流量平稳的项目来说,"轻量、省钱、免运维"这三个诉求,一套 Workers + D1 基本都能满足。
二、多租户怎么隔离:单库 tenant_id 行级隔离
多租户 SaaS 第一个绕不开的问题是租户数据怎么隔开。我一开始也权衡过是否每个租户独立一个库,最后定了单库 + 每表tenant_id行级隔离。
核心做法:每个业务表都加一个tenant_id列,所有查询在中间件层被强制注入tenant_id过滤条件,应用层无法绕开。二三十个租户这个量级,独立库没有必要,单库反而让备份、迁移、跨租户统计都简单得多。
| 隔离方案 | 做法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 独立数据库 | 每租户一个 D1 / 实例 | 物理隔离最彻底 | 备份/迁移/统计爆炸,运维重 | 大客户、强合规 |
| Schema 隔离 | 同库不同 schema | 中等隔离 | SQLite 不支持 schema,劝退 | Postgres 多租户 |
| 单库 tenant_id 行级 | 每表加 tenant_id,查询强制过滤 | 轻量、易备份、易统计 | 靠应用层纪律保证隔离 | 中小体量 SaaS |
三、D1 没有"真事务":零物理外键 + db.batch 原子写
这是用 SQLite 系数据库最反直觉的一点。D1 不支持交互式BEGIN / COMMIT,没法开一个事务再中途查询、最后决定回滚。每条语句自带自动事务,批量写要靠别的方式。
所以我定了两条原则:
- 表之间零物理外键。Drizzle 的
extra回调里不能塞原始sql\FOREIGN KEY``(会把 schema 解析拖垮),完整性靠应用层保证。 - 需要批量原子写时,用
db.batch([...])。它把多条写打包成一个事务边界一起提交,相当于"伪事务"。比如一条线索转化时同时写leads状态变更和customers新建,就放进同一个 batch。
把传统关系型数据库"外键 + 长事务"的习惯,换成"应用层约束 + 批次提交"的 edge-native 写法,是用 D1 必须过的关。
四、架构决策全景表
把当时拍板的几个关键决策列出来,方便各位对照复盘:
| 决策 | 做法 | 为什么 |
|---|---|---|
| 多租户隔离 | 单 D1 + 每表tenant_id行级 | 几十租户级不需要独立库,备份统计都省事 |
| 原子写 | db.batch([...])替代事务 | D1 不支持交互式BEGIN/COMMIT |
| 预聚合看板 | lead_stats_daily宽表 + Cron 每日写 | 避免实时多表 GROUP BY 突破 30s 超时 |
| 禁拨合规 | 手动 / Cron 对账刷新is_blocked+ 联动取消日程 | 合规优先,标记即移出活跃池 |
| 废弃双源 | leads.nextFollowupAt仅来自 schedules | 避免两表数据不一致 |
| 审计归档 | D1 热存 365 天 → R2 NDJSON 冷归档 | 单 D1 10GB 上限控制 |
| Token 吊销 | JWT +tv版本号(KV 存) | 改密 / 全设备登出无需扫描删 RefreshToken |
| 密码存储 | PBKDF2(SHA-256, 10万迭代) + 每用户随机盐 | 符合行业密码存储规范 |
| 鉴权签名 | JWT(jose, HS256)双密钥 | 密钥轮转不中断服务 |
| 接口校验 | Zod | 运行时类型安全,挡住脏数据 |
五、最反直觉的一个决策:统计看板必须预聚合
这条值得单独说。Cloudflare Workers 单次请求有30 秒超时(实际可用 CPU 时间更短)。如果做一个实时统计接口,跨 leads / call_records / schedules 几张表 JOIN 再 GROUP BY,数据量稍大就直接超时。
我的解法是空间换时间:建一张预聚合宽表lead_stats_daily,用 Cron 每天凌晨把前一天的统计数据算好写进去,看板接口直接SELECT这张表。代价是数据有"一天延迟",但看板本来就不是实时交易场景,T+1 完全够用。这一招把看板响应从"可能超时"变成"毫秒返回"。
六、Token 吊销不扫库:tv 版本号 + KV
传统做法里,用户改密码要去数据库删掉所有 RefreshToken,才能实现"全设备登出"。在 serverless 环境里这等于一次扫库,又慢又费钱。
我改成:JWT 里带一个tv(token version)字段,版本号存在 KV 里。用户改密或管理员踢人时,只把 KV 里的tv加一,所有旧 token 因为携带的tv对不上,验证时直接拒绝——全设备登出就是一次 KV 写入,秒级生效,零扫描。配合 JWT 双密钥(JWT_SECRET/JWT_SECRET_PREV),密钥轮转也能做到不中断服务。
七、踩坑与权衡:这趟没那么顺
这套栈有几个坑是实打实绊过我的,先在这里点一下,后面会单开文章细讲:
- D1 单查询 100 绑定参数硬上限:批量写入得按"变量数"分块,宽表一张 INSERT 可能只能塞 3 行。
- 本地 D1 复位要
rm -rf .wrangler:不能用 better-sqlite3 直接开 D1 文件,否则内部状态污染,dev 各种诡异。 - workerd 端口幽灵占用:
wrangler dev退出了,真正跑 worker 的子进程还占着端口,下次启动改动"不生效",得pkill -9 workerd。 - PBKDF2 在 Workers 上限 10 万迭代:超了直接
NotSupportedError,而且 seed 脚本和运行时迭代次数必须对得上,否则登录永远失败。 - KV 写入配额反模式:限流如果每请求
kv.put一次,超过 16 QPS 就爆配额,得改成本地内存固定窗口。
这几个坑每一个都能单独成篇,今天先列在这里。
八、什么时候这套栈不适合你
这套栈有清楚的能力边界,选错场景一样会出问题。我见过有人把它硬塞进高并发事务系统,最后不得不迁回 Postgres。划条线:
| 场景 | 这套栈的短板 | 该用什么 |
|---|---|---|
| 超大量级 / 强一致事务 | D1 单库、无交互式事务,跨表强一致难保证 | 老老实实上 Postgres / MySQL |
| 长耗时计算 | Worker 30s 超时摆在那 | 丢给队列 + 独立算力(如 Cloudflare Queues + 外部 worker) |
| 复杂实时分析 | 预聚合只解决固定看板 | 专门 OLAP / 数仓 |
一句话:中小体量、请求短平快、能接受最终一致性的 CRUD 型 SaaS,这套栈合适;超出这个框,传统数据库该上还是得上。技术选型服务于项目的体量和团队的人力,不要为了 serverless 而 serverless。
小结
回头复盘:serverless 边缘栈做中小体量的 SaaS 完全够用,关键是把"传统数据库那套事务 / 实时聚合 / 扫库吊销"的习惯换掉,改成"批次 / 预聚合 / 版本号"的 edge-native 思路。
各位看官记住一点:选 Workers + D1 不是因为它比 Postgres 强,而是因为它让小团队 zero-ops 也能把多租户 SaaS 跑起来。技术选型永远服务于体量和人力,别被"最佳实践"绑架——能用最简单的栈 deliver,就是合适的架构。
如果这篇文章对你有帮助,发财的小手点个小赞。也欢迎翻翻我前面几篇实战记录:
相关阅读
- NodeJS Koa 后端用户会话管理,JWT, Session,长短Token,本文一次性讲明白
- node 后端和浏览器前端,有关 RSA 非对称加密的完整实践,前后端匹配的代码演示
- Nodejs 实现 Mysql 数据库的全量备份的代码演示
- 安装和配置 Nginx 和 Mysql —— 一步一步配置 Ubuntu Server 的 NodeJS 服务器详细实录6
- PVE 虚拟机安装 Ubuntu Server V24 系统 —— 一步一步配置 Ubuntu Server 的 NodeJS 服务器详细实录1
本文由 FungLeo 主导,Deepseek 优化校阅,转发请注明首发地址,谢谢大家!
