OASIS:用系统论重构多租户 SaaS 架构
译注:本文编译自 dev.to 文章《Building OASIS: A Systems Theory Approach to Multi-Tenant SaaS Architecture》,作者介绍了其设计的开源框架 OASIS。原文链接见文末。
多租户 SaaS 的构建通常会给开发团队带来沉重的认知负担。一旦把多个客户的数据混进同一个数据库,系统的安全性就依赖于行为纪律——你只能寄希望于初级开发者永远不会忘记写 WHERE tenant_id = ? 这个条件。
为解决这一问题,作者设计了 OASIS(Opinionated Architecture for Secure Isolated SaaS)。OASIS 将数据隔离从一种行为要求转变为一种结构性约束。它借用系统论(具体来说是 Donella Meadows 的存量、流量与反馈回路框架)来分析:如何在提供隔离式多租户安全性的同时,保留单租户 CRUD 应用的开发者体验(DevEx)。
子系统(节点)
该框架建立在一套高度固化的技术栈之上,划分为若干独立子系统:
- 入口层: Caddy(通配符子域名、按需 TLS、本地开发免 CORS)。
- 前端: Nuxt 3(TypeScript,基于 Host 请求头的 SSR 路由)。
- 后端: Go(轻量并发、强类型)。
- 持久化: PostgreSQL(隔离 schema),由 Ent ORM 管理。
- 鉴权: Casbin(
ent-casbin适配器、casbin-redis-watcher)。 - 编排: OpenTofu、LXD 与 Docker。
追踪存量与流量
要理解 OASIS 如何抽象复杂度,需要梳理资源与信息在系统中的流动方式。
核心存量
- 租户数据(无形): 动态隔离。OASIS 不采用行级隔离(共享表),而是使用隔离 schema(
tenant_01h45...)。每个租户都是单一 PostgreSQL 实例内完全独立的命名空间。 - 数据库连接(有形): Go 维护一个全局的
database/sql连接池。高效管理这一存量,是防止「吵闹邻居」耗尽资源的关键。
上下文流量
OASIS 最具代表性的流量是 Go 的 context.Context,它充当身份的不可变载体。当请求到达 Go 后端时,中间件提取 JWT 与租户子域名,将身份注入 context.Context 并沿调用链向下传递。处理器的业务逻辑无需直接检查 HTTP 请求头。
跨系统协同与摩擦削减
交叉审视这些子系统,可以发现消除开发者摩擦、防止系统性故障的有效协同。
摩擦削减:事务级 schema 护栏
多租户的最大风险是数据泄漏。OASIS 的缓解方式,是将一个绑定到特定 search_path 的数据库事务直接注入请求流。
请求到达时,Go 中间件从全局连接池获取连接,执行 SET LOCAL search_path TO tenant_x,开启一个 Ent ORM 事务(*ent.Tx),并将其挂载到 context.Context 上。
对初级开发者而言,代码看起来就是标准的单租户 CRUD 逻辑:
func GetUsersHandler(w http.ResponseWriter, r *http.Request) {
// rbac.Can 从上下文流中原生读取 Casbin 身份
if !rbac.Can(r.Context(), "GET", "/api/users") {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
// fetchTx 提取已隔离的 Ent 事务
tx := fetchTx(r.Context())
users, _ := tx.User.Query().All(r.Context())
renderJSON(w, users)
}由于 search_path 与事务绑定,它会在 commit 或 rollback 时自动重置。连接池污染不可能发生,开发者在结构上就无法查询到错误的租户。
资源级联:Nuxt 3 的 Host 路由
OASIS 无需为营销站点、全局管理后台和租户应用维护三个独立仓库,而是在 SSR 层利用资源级联。
单个 Nuxt 3 应用解析传入的 Host 请求头。Caddy 动态路由 *.saasdomain.tld,Nuxt 的服务端中间件进行拦截。Host 身份级联至 Vue 组件,动态挂载正确的布局,并执行经 Zod 校验的状态注入(useAuth 与 useRBAC),无需在 URL 参数中堆砌信息。
强化回路:分布式鉴权
鉴权方面,OASIS 使用 Casbin 的域作用域角色 g(user, role, domain)。但部署在多个 Docker 容器中的无状态 Go 后端会产生缓存陈旧问题。
为建立强化同步回路,OASIS 集成了 casbin-redis-watcher:
- 管理员在节点 A 上更新策略。
- 节点 A 将规则写入全局
public.casbin_rulePostgreSQL 表。 - 节点 A 发布一条 Redis Pub/Sub 通知。
- 节点 B、C、D 拦截该流量,立即从数据库重新加载内存中的 Casbin 图。
系统在毫秒级内完成整个集群安全态势的自愈。
许可作为系统边界(BUSL-1.1)
最后,保护框架本身需要法律边界。OASIS 采用 Business Source Licence(BUSL-1.1),常被称为 Fair Source。
在 BUSL-1.1 下,代码公开,对爱好者、学习者和初创公司完全免费。附加使用授权明确允许商业生产使用,前提是该实体总营收低于 1,000,000 英镑。
这形成了一个互利回路:初级开发者可以不受限制地接触企业级架构工具来学习,而大型企业则无法在不购买商业许可的情况下利用该框架。四年后,每个版本自动转为宽松的 Apache 2.0 许可,确保代码最终永久惠及更广泛的开源生态。
通过将多租户视为系统性的架构问题而非行为编码规范,OASIS 提供了可靠的基础。复杂度被留在水面之下,让开发者专注于最擅长的事:交付功能。