软件工厂 PostgreSQL 多租户隔离方案

讨论日期:2026-07-07 · 状态:方案讨论

背景

软件工厂计划从系统层面提供一个共享的 PostgreSQL 实例,供两类场景使用:

核心诉求:

  1. 系统数据不能被用户破坏
  2. 不同用户/项目之间的数据完全不可见

方案选型

采用 PostgreSQL Schema 级隔离

方案 优点 缺点 结论
Schema 隔离 隔离边界清晰、运维简单、单实例即可 schema 数量极多时需注意 PG catalog 膨胀 采用
Database 隔离 最强隔离 运维复杂,跨库查询困难 过重
RLS 行级隔离 最省资源 一个 policy 写漏就泄露;用户能看到其他人的表名 风险高

架构总览

PostgreSQL 实例
│
├─ system schema        ← 系统表,用户角色无任何权限
│   ├─ projects
│   ├─ users
│   └─ ...
│
├─ tenant_proj_001     ← 项目 A 的独立空间
│   ├─ (用户自建的表)
│   └─ ...
│
├─ tenant_proj_002     ← 项目 B 的独立空间
│   └─ ...
│
└─ public schema        ← 可选:放共享扩展(如 uuid-ossp)

隔离机制

① 系统 vs 用户隔离

  • 系统使用高权限 role(如 blade_system),拥有 system schema 的全部权限
  • 用户项目的 role 对 system schema 没有任何权限,连 SELECT 都不行

② 用户之间隔离

  • 每个项目创建独立的 PG role(如 tenant_proj_001
  • 该 role 只被授予对应 schema 的权限
  • 连接时设置 search_path = tenant_proj_xxx, public,用户应用直接写 SQL 即可,无需感知 schema 前缀

③ 资源限制(防止单用户拖垮整个实例)

ALTER ROLE tenant_proj_001 SET statement_timeout = '30s';
ALTER ROLE tenant_proj_001 CONNECTION LIMIT 5;

磁盘配额可通过 tablespace + 文件系统 quota 实现,但开发环境一般不需要。

用户在自己 Schema 内的权限

用户在自己的 schema 内拥有完整的 DDL 和 DML 权限,包括:

可以做

  • CREATE / ALTER / DROP TABLE
  • INSERT / UPDATE / DELETE
  • CREATE INDEX / VIEW / FUNCTION

不能做

  • 访问 system schema
  • 访问其他项目的 schema
  • CREATE DATABASE / ROLE
  • 修改全局配置

这是必须的——否则用户开发应用时连建表都要审批,体验不可接受。

Schema 生命周期

1

项目创建时

自动执行 CREATE SCHEMA + CREATE ROLE + GRANT

2

沙盒启动时

在沙盒环境变量中注入带对应 role 凭据的 DATABASE_URL

3

项目删除时

DROP SCHEMA ... CASCADE + DROP ROLE 彻底清理

数据丢失与恢复策略

定位:软件工厂是开发环境,不是生产环境

用户 DROP TABLE 或写 bug 删了数据,和在本地删了 SQLite 文件一样——是用户的责任。不需要提供企业级备份恢复。

兜底措施:定时 pg_dump

  • 按项目 schema 粒度定时备份(cron job)
  • 保留最近几份快照即可
  • 恢复时 pg_restore 回对应 schema
  • 不需要 point-in-time recovery 那一套

参考:初始化 SQL

-- 1. 系统 schema(仅 blade_system 可访问)
CREATE SCHEMA IF NOT EXISTS system;
CREATE ROLE blade_system WITH LOGIN PASSWORD '...';
GRANT ALL ON SCHEMA system TO blade_system;

-- 2. 创建租户(项目创建时自动执行)
CREATE SCHEMA tenant_proj_001;
CREATE ROLE tenant_proj_001 WITH LOGIN PASSWORD '...';

-- 授予 schema 权限
GRANT USAGE, CREATE ON SCHEMA tenant_proj_001 TO tenant_proj_001;
GRANT ALL ON ALL TABLES IN SCHEMA tenant_proj_001 TO tenant_proj_001;
ALTER DEFAULT PRIVILEGES IN SCHEMA tenant_proj_001
  GRANT ALL ON TABLES TO tenant_proj_001;

-- 设置 search_path(用户无需写 schema 前缀)
ALTER ROLE tenant_proj_001 SET search_path = tenant_proj_001, public;

-- 资源限制
ALTER ROLE tenant_proj_001 SET statement_timeout = '30s';
ALTER ROLE tenant_proj_001 CONNECTION LIMIT 5;

-- 撤销对 public schema 的默认 CREATE 权限(PG 15+ 默认已撤销)
REVOKE CREATE ON SCHEMA public FROM tenant_proj_001;

-- 3. 删除租户(项目删除时执行)
DROP SCHEMA tenant_proj_001 CASCADE;
DROP ROLE tenant_proj_001;