多租户 SaaS 架构:在你构建之前没人告诉你的事

发布: (2026年3月30日 GMT+8 03:57)
8 分钟阅读
原文: Dev.to

Source: Dev.to

抱歉,我目前无法直接访问外部链接。请您把需要翻译的正文内容粘贴到这里,我会按照要求保留源链接、格式和技术术语,将其翻译成简体中文。

三种规范模式

模式描述优点缺点
1. 为每个租户单独数据库每个租户拥有自己的数据库实例。• 完全的数据隔离
• 没有跨租户泄漏的风险
• 简单的下线流程
• 每租户的备份/恢复非常容易
• 供应时间 ↑
• 连接池管理变得复杂
• 模式迁移必须在 N 个数据库上执行
• 成本随租户数量线性增长
2. 为每个租户单独模式,共享数据库一个数据库服务器,每个租户拥有自己的 schema(PostgreSQL 原生)。• 在不增加独立实例开销的情况下实现逻辑分离• 连接池(如 PgBouncer)在连接层面工作,而不是在 schema 层面
• 必须为每个请求设置 search_path
• 部分 ORM 能优雅处理,其他则不行
• 迁移仍需在所有租户之间协调
3. 共享 schema,共享数据库(行级租户)所有租户共享同一套表,每行都有 tenant_id 列。• 运营成本最低
• 迁移最简单
• 入驻速度最快
• 如果遗漏 tenant_id 过滤,数据泄漏风险极高
• 必须使用 Row‑Level Security (RLS) 作为硬防线

3️⃣ 共享‑Schema 方法:实施行级安全

如果你选择共享‑schema 模型,PostgreSQL RLS 不是可选的——它是最后一道防线。

-- Enable RLS on the projects table
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;

-- Create a policy that restricts reads to the current tenant
CREATE POLICY tenant_isolation ON projects
  USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

为每个请求设置租户上下文

SET app.current_tenant_id = '{{tenant_uuid}}';

即使应用代码忘记了 tenant_id 过滤,数据库仍会强制边界(深度防御)。

成本: current_setting() 会为每个查询增加一点开销——对大多数工作负载来说可以忽略不计,但如果你的查询率非常高,请进行基准测试。

您的应用程序如何识别租户?

方法示例含义
基于子域acme.yourapp.com → 将 acme 解析为租户需要 DNS 通配符或每个租户的记录;增加路由复杂度
自定义域名app.acmecorp.com必须在边缘将任意域名映射到租户 ID
基于路径yourapp.com/t/acme/dashboardDNS 更简单,但需要解析 URL
基于令牌在 JWT 或会话令牌中编码租户 ID对 API 很友好;需要安全的令牌处理

大多数团队会混合使用:主 UI 使用子域,API 使用基于令牌的方式。

迁移策略

  • 单租户应用: 迁移运行一次。
  • 多租户(共享模式): 迁移运行一次。
  • 多租户(每租户独立模式或每租户独立数据库): 迁移运行 N 次。

多租户迁移运行器的需求

  1. 按租户跟踪迁移状态
  2. 并行运行迁移(可配置并发数)
  3. 优雅地处理失败 – 避免部分发布(例如,某些租户在版本 7,其他租户在版本 8)

成熟模式

在管理数据库中维护 tenant_migrations 表:

tenant_idmigration_versionlast_run_at

您的部署流水线:

  1. 查询 tenant_migrations,找出未达到当前版本的租户。
  2. 按批次运行迁移(并行或顺序,依据并发限制)。
  3. 成功后更新表,或记录失败以便重试。

新租户入驻

模型入驻步骤典型延迟
共享‑schematenants 表中插入一行;使用生成的 ID 作为所有写入的 tenant_id几乎即时(原子)
Schema‑per‑tenant创建新 schema → 运行基线迁移。小型 schema 几秒;大型 schema 几分钟(通常以异步方式,并显示“工作区正在准备中”界面)
Database‑per‑tenant配置新数据库实例 → 设置访问权限 → 运行迁移 → 更新路由表。几分钟(后台任务)

围绕供应模型设计用户体验;不要等发布后才发现不匹配。

关键要点

多租户是数据架构和应用设计层面的关注点,而非基础设施层面的关注点。
您可以在单台服务器上运行多租户应用,也可以跨数百台服务器运行;同样,您也可以在 Kubernetes 中使用 50 个副本运行单租户应用。隔离模型存在于数据层和应用逻辑中;扩展性、可用性和部署是独立(但同样重要)的决策。

进一步阅读

  • Actinode guide on multi‑tenant SaaS architecture – a comprehensive decision matrix covering compliance, pattern trade‑offs, and migration strategies.

租户隔离选项

ModelScopeCostMigration ComplexityBest for
Separate databasesFullHighHighEnterprise, regulated industries
Separate schemasLogicalMediumMediumMid‑market SaaS
Shared schema + RLSRow‑levelLowLowHigh‑volume B2B, most startups

注意: 您在此做出的选择将伴随多年。请慎重决定。

测试您的租户隔离

无论选择哪种模型,都要编写明确的测试来验证租户隔离是否有效。

  • 不仅仅是查询逻辑的单元测试——集成测试,模拟跨租户访问尝试并验证它们在数据库层被阻止。

示例测试套件

1. Create two test tenants with separate datasets
2. Authenticate as tenant A
3. Attempt to read tenant B's records via your application's own API routes
4. Assert the response contains zero tenant B records
  • 该测试应在每次合并时于 CI 流水线中运行。
  • 租户隔离失效是会引起媒体关注的严重漏洞,在 CI 中捕获的成本相对于在生产环境中发现的成本微乎其微。

RLS 策略验证

-- Connect with the RLS policy active
SET app.current_tenant_id = '<tenant_uuid>';

-- Attempt to SELECT tenant B's rows directly
SELECT * FROM sensitive_table WHERE tenant_id = '<other_tenant_uuid>';
  • 如果 RLS 正常工作,查询将返回零行
  • 如果返回任何行,则说明您的策略存在漏洞。

Bottom Line

隔离是一种 正确性属性,而不仅仅是设计偏好。
像对待正确性属性一样进行测试。

0 浏览
Back to Blog

相关文章

阅读更多 »