Hotdry.

Article

PgDog 多租户查询路由与资源隔离实战

在多租户 SaaS 架构中,PgDog 通过物理分片与虚拟 Schema 两种模式实现租户级查询路由,结合 AST 解析与缓存机制强制校验 tenant_id,提供从配置到运维的完整隔离方案。

2026-06-10systems

多租户架构是 B2B SaaS 的核心基础设施,但数据隔离与查询路由始终是最棘手的工程问题。传统方案要么在应用层硬编码路由逻辑,要么为每个租户维护独立数据库实例,前者容易出错,后者成本高昂。PgDog 作为 PostgreSQL 的代理层方案,将多租户能力下沉到数据库访问层,通过声明式配置实现租户识别、路由分发与查询校验,让应用代码保持无感知。

物理多租户:基于分片的硬隔离

物理多租户是 PgDog 最成熟的隔离模式,每个租户的数据存储在独立的数据库实例或分片中。这种模式的优势在于故障域隔离彻底 —— 单个租户的慢查询或数据损坏不会影响其他租户,同时也便于针对不同租户实施差异化的资源配置。

实现物理多租户需要两个核心配置。首先是定义分片表结构,指定 tenant_id 作为路由键:

[[sharded_tables]]
database = "prod"
column = "tenant_id"
data_type = "bigint"

其次是建立租户到分片的映射关系。PgDog 支持列表映射和范围映射两种策略。列表映射适合租户分布不均的场景,比如将大客户单独分配到独立分片:

[[sharded_mappings]]
database = "prod"
column = "tenant_id"
kind = "list"
values = [1, 5, 1000]
shard = 0

范围映射则更适合租户 ID 连续分布的情况,例如按 ID 段划分:

[[sharded_mappings]]
database = "prod"
column = "tenant_id"
kind = "range"
start = 1
end = 100
shard = 0

PgDog 使用高效的查找算法处理映射关系,列表映射采用哈希查找,范围映射目前为线性扫描但即将升级为二叉树结构以达到 O (log n) 复杂度,足以支撑百万级租户的映射需求。

虚拟多租户:Schema 级别的轻量隔离

对于租户规模较小或数据量不大的场景,物理分片可能显得过于沉重。PgDog 提供的虚拟多租户模式允许所有租户共享同一个 PostgreSQL 实例,但通过 Schema 和角色权限实现逻辑隔离。

虚拟多租户的工作原理是在建立连接时自动执行两条会话级命令:

SET search_path TO '<tenant_schema>', public;
SET ROLE TO '<tenant_role>';

search_path 确保查询无需使用完全限定名即可访问租户专属表,SET ROLE 则通过 PostgreSQL 的原生权限系统限制该连接只能访问特定 Schema 下的对象。这种方式的好处是迁移成本低 —— 当租户规模增长需要升级到物理分片时,只需修改 PgDog 配置即可,应用代码完全无感知。

需要注意的是,虚拟多租户目前仍处于开发阶段,生产环境使用前建议充分测试边界场景。

查询层防护:AST 解析与租户校验

无论采用物理还是虚拟多租户,查询层面的安全校验都是最后一道防线。PgDog 内置了基于 PostgreSQL 原生解析器(通过 pg_query 项目)的 AST 分析能力,能够识别所有经过代理的 SQL 语句。

校验的核心逻辑是确保每个查询的 WHERE 子句中包含有效的 tenant_id 过滤条件。简单的等值查询如 WHERE tenant_id = 5 很容易识别,但真实业务中的查询往往更复杂。PgDog 能够处理多表 JOIN 场景,通过遍历语法树分析字段间的传递关系。例如以下查询:

SELECT * FROM personal_notes
INNER JOIN emails ON emails.tenant_id = personal_notes.tenant_id
INNER JOIN customers ON customers.id = emails.tenant_id
WHERE customers.id = ?

尽管 tenant_id 没有直接出现在 WHERE 子句中,但 PgDog 能够识别出 customers.id 与 personal_notes.tenant_id 之间的传递等价关系,从而判定这是一个单租户查询。

对于 CTE(公用表表达式),PgDog 采用递归遍历的方式,将相同的校验规则应用于子查询。UPDATE 和 DELETE 操作同样受到严格管控 ——PgDog 可以强制要求这类操作必须包含 tenant_id 过滤条件,防止误执行 UPDATE users SET banned = true 这类灾难性语句。

性能方面,PgDog 对解析结果进行缓存。首次遇到的查询会被解析为 AST 并存储在内存哈希表中,后续相同的查询(包括参数化查询)直接命中缓存,只需一次互斥锁操作和哈希查找即可获取解析结果,开销极低。

配置实践与运维要点

完整的 PgDog 多租户配置需要协调多个配置段。以下是一个生产环境可用的配置框架:

[general]
default_pool_size = 100

# 定义分片数据库
[[databases]]
name = "prod"
host = "10.0.0.1"
shard = 0

[[databases]]
name = "prod"
host = "10.0.0.2"
shard = 1

# 定义分片键
[[sharded_tables]]
database = "prod"
column = "tenant_id"
data_type = "bigint"

# 定义租户映射(示例:大客户独立分片,其余共享)
[[sharded_mappings]]
database = "prod"
column = "tenant_id"
kind = "list"
values = [10001, 10002]  # VIP 租户
shard = 0

[[sharded_mappings]]
database = "prod"
column = "tenant_id"
kind = "range"
start = 1
end = 10000
shard = 1

运维层面需要关注几个关键指标:首先是查询路由命中率,确保绝大多数查询能够被正确路由到目标分片;其次是 AST 缓存命中率,低命中率可能意味着查询模式过于发散或缓存容量不足;最后是跨分片查询的比例,虽然 PgDog 支持跨分片事务(基于两阶段提交),但这类操作性能开销较大,应当控制在合理范围内。

对于租户迁移场景,PgDog 支持通过 UPDATE 语句修改 tenant_id 来实现数据在分片间的移动。配合复制表(在所有分片上存储相同数据的表)可以实现分片本地 JOIN,避免跨分片查询。

总结

PgDog 的多租户方案将路由逻辑从应用层下沉到代理层,通过声明式配置替代硬编码逻辑,显著降低了多租户系统的维护复杂度。物理多租户提供最强的隔离性,适合对数据安全要求极高的场景;虚拟多租户则以更低的资源开销满足中小型租户的需求。结合 AST 级别的查询校验,PgDog 在便利性与安全性之间找到了平衡点,为 PostgreSQL 生态的多租户架构提供了新的工程选择。


参考来源

systems

内容声明:本文无广告投放、无付费植入。

如有事实性问题,欢迎发送勘误至 i@hotdrydog.com