返回全部文章
技术·发布于 Wed Jul 22 2026 16:56:21 GMT+0800 (中国标准时间)·5 分钟

三层隔离:企业级多租户平台的边界从哪划起

多数 AI 平台只在应用层做租户过滤,一个 bug 就串数据。我们用独立 schema、行级安全和向量库分集合把隔离下沉到存储层,并讲讲路上踩过的坑。

等距 3D 多租户数据隔离示意图

把一个大模型应用做成多租户,最自然的做法是在每个查询里拼一个 where tenant_id = ?。能跑,demo 也漂亮。问题在于,这个过滤是应用层自觉:只要有一个查询忘了带条件,或者某个内部脚本用了错的连接,数据就串了。在发票、合同、报表这种错不起的场景,一次串数据就是事故。

我们做 mitrax 时定了一条原则:隔离不靠自觉,下沉到存储层强制。具体落到三层。

第一层:独立 schema,物理上分开

每个租户的业务表放在独立的数据库 schema 里,和认证系统的 schema 物理分离。这不是逻辑隔离,是 schema 级别的边界。跨租户的查询在数据库层面就不该能写出来,因为表都不在同一个 schema 下。

这一层防的是粗心和误用。开发写错连接、脚本连到错的库,第一层就拦住了。

第二层:行级安全,连表都查不到不该查的行

独立 schema 解决了跨租户,但同一个租户内部、同一张表里,不同角色该看到不同行。这层我们用 PostgreSQL 的行级安全(Row Level Security)。

RLS 的思路是在表上挂策略,策略写清楚当前会话的租户身份等于行的租户列时才放行。数据库引擎在每条查询执行前自动套用,应用代码绕不过去。

概念示意,非真实策略:

-- 在表上挂一条行级安全策略
CREATE POLICY tenant_isolation ON documents
  FOR SELECT
  USING (tenant_id = current_setting('app.tenant_id'));

-- 请求开始时,把当前租户身份绑进会话
SET app.tenant_id = 'tenant_abc';

关键不在 SQL 本身,在谁来设这个会话身份。答案是应用在每个请求开始时,从已验证的 token 里取出租户 id,绑进数据库会话变量。绑定之后所有查询自动受限,开发者不需要在每个数据访问方法里记得带条件。

RLS 会休眠

PostgreSQL 的 RLS 默认对超级用户不生效,对表的 owner 也不生效。如果应用拿一个超级用户账号连库,RLS 等于没开,安静地休眠在那里,等你某天审计才发现一行都没拦过。

这个坑我们吃过。后来在应用启动时加了一道校验:开关要求 RLS,但数据库连接身份是超级用户,直接拒启。用最笨的方式挡掉最危险的配置错误。

概念示意,非真实代码:

def check_rls_on_startup():
    if not config.require_rls:
        return
    if db_role.is_superuser:
        raise RefuseToStart("RLS 开了但连接身份是超管,等于没开")

启动失败比静默跑起来安全得多。一个连不上线的服务,比一个以为自己隔离其实没隔离的服务,问题小得多。

第三层:向量库同样按租户分集合

前两层管的是结构化数据。AI 平台还有一块容易漏:向量知识库。

RAG 检索的本质是在向量空间里找近邻。如果所有租户的文档向量混在一个集合里,检索时只靠应用层元数据过滤(先取 top 100,再筛掉别的租户的),就回到了第一段说的老问题:过滤是自觉的,漏一次就串。

我们的做法是按租户分集合(collection),检索的边界在向量库这一层就划死。一个租户的检索请求,物理上不会扫到另一个租户的向量。三层放在一起是这样:

flowchart TB
  subgraph App["应用层:每个请求绑定租户身份"]
    R["已验证 token → 租户 id"]
  end
  subgraph DB["数据库 PostgreSQL"]
    S1["租户 A 的 schema"]
    S2["租户 B 的 schema"]
    RLS["行级安全策略:自动过滤不该见的行"]
    S1 --- RLS
    S2 --- RLS
  end
  subgraph Vec["向量知识库"]
    C1["租户 A 的 collection"]
    C2["租户 B 的 collection"]
  end
  R --> S1
  R --> S2
  R --> C1
  R --> C2

为什么是三层

有人问,RLS 已经够强,为什么还要独立 schema。因为纵深防御的每一层防的是不一样的失败模式:schema 防误连,RLS 防漏条件,向量库分集合防检索越权。少一层,对应的失败模式就裸奔。

也有人问,为什么不上更重的方案,比如每个租户一个独立数据库实例。能上,但成本和运维复杂度跳一个量级,对多数企业级场景不划算。三层方案在隔离强度和工程可行性之间取了个平衡:每个租户的数据在存储层有独立边界,平台仍能统一运维和升级。

小结

多租户隔离不是在查询里加个 where 这么轻巧。真正难的是让隔离成为一种就算开发者忘了也存在、而不是依赖开发者记得的属性。把它从应用层下沉到存储层,是我们能放心把发票和审批数据放进平台的根本前提。

准备好让你的下一个员工是数字员工了吗?

从设计、运行到治理,米阵让 AI 安全地进入你的关键业务。