Skip to content

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)。

子系统(节点) ​

该框架建立在一套高度固化的技术栈之上,划分为若干独立子系统:

  1. 入口层: Caddy(通配符子域名、按需 TLS、本地开发免 CORS)。
  2. 前端: Nuxt 3(TypeScript,基于 Host 请求头的 SSR 路由)。
  3. 后端: Go(轻量并发、强类型)。
  4. 持久化: PostgreSQL(隔离 schema),由 Ent ORM 管理。
  5. 鉴权: Casbin(ent-casbin 适配器、casbin-redis-watcher)。
  6. 编排: 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 逻辑:

go
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:

  1. 管理员在节点 A 上更新策略。
  2. 节点 A 将规则写入全局 public.casbin_rule PostgreSQL 表。
  3. 节点 A 发布一条 Redis Pub/Sub 通知。
  4. 节点 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 提供了可靠的基础。复杂度被留在水面之下,让开发者专注于最擅长的事:交付功能。

原文链接 ​