内容提要
PostgreSQL 按租户列表分区时,若仅靠行级安全策略过滤租户,规划器无法预知租户,需扫描全部分区。实测1000租户查询从0.25毫秒增至77毫秒,3000租户达241毫秒,并产生数千锁,32并发下大量报共享内存不足。修复方法:查询中显式加tenant_id条件;添加租户改用CREATE TABLE加ATTACH PARTITION。
延伸解读
规划阶段剪枝失效是性能下降主因
文章指出,当租户过滤仅依赖行级安全策略时,规划器无法在计划阶段获知租户值,因为该值来自current_setting(),只能在执行初始化时确定。因此规划器必须为所有分区生成计划,执行器再移除无关分区。实测显示,1000个分区时规划耗时26毫秒,3000个分区时达84毫秒,而显式添加tenant_id条件后规划仅需0.05毫秒。这解释了为何分区数增加会导致查询延迟急剧上升。
锁数量随分区数线性增长,并发下易耗尽共享内存
每个查询即使只访问一个租户,也会锁定所有分区及其索引。实测1000个分区时持有2003个锁,3000个分区时达6003个锁。在32个并发客户端下,1000个分区时每轮有20到32个客户端因共享内存不足而失败;3000个分区时29个客户端在6秒内中止。显式添加tenant_id条件后,32个并发客户端完成20470 TPS且无错误。这表明锁开销是并发场景的关键瓶颈。
添加租户应避免ACCESS EXCLUSIVE锁
使用CREATE TABLE ... PARTITION OF会获取整个分区表的ACCESS EXCLUSIVE锁,可能阻塞其他租户的查询。实测中,当另一租户正在读取时,该操作等待2秒后因lock_timeout失败。替代方案是先创建独立表(CREATE TABLE ... LIKE),添加CHECK约束,再使用ATTACH PARTITION,此操作仅需SHARE UPDATE EXCLUSIVE锁,实测23毫秒完成。文章建议附加后删除CHECK约束,并保持新表无独立授权。
PostgreSQL 18未改变问题本质
在PostgreSQL 18.3上,仅策略过滤的查询延迟有所降低(1000分区55毫秒,3000分区160毫秒),但锁数量仍为2003和6003,与版本17相同。显式添加tenant_id条件后,规划时间保持在0.04-0.06毫秒,执行时间低于0.1毫秒。这表明版本升级并未解决规划器无法预知租户值导致的根本问题,显式谓词仍是必要优化。
Q&A
PostgreSQL 中按租户列表分区,如果租户过滤只写在行级安全策略里,查询性能会怎样?
规划器无法预知租户,会扫描全部分区。实测:100 租户时查询耗时约 2 毫秒,1000 租户时约 77 毫秒,3000 租户时约 241 毫秒;而显式加上 WHERE tenant_id = … 后,耗时稳定在 0.25 毫秒左右。
为什么仅靠行级安全策略过滤租户会导致查询变慢?
因为策略中的租户值来自 current_setting(),规划器在计划时不知道具体值,只能在执行初始化阶段裁剪分区。这导致规划器要处理所有分区,产生大量计划子计划,并获取每个分区的锁,同时可能触发并行计划,增加开销。
如何解决按租户分区时因 RLS 导致的性能问题?
在查询中显式添加 tenant_id 条件,让规划器能在计划阶段裁剪分区。同时,添加租户时使用 CREATE TABLE ... LIKE 加 CHECK 约束,再 ATTACH PARTITION,避免 ACCESS EXCLUSIVE 锁。
在 PostgreSQL 中为每个租户创建一个分区,添加新租户时有什么风险?如何避免?
使用 CREATE TABLE ... PARTITION OF 会获取整个分区表的 ACCESS EXCLUSIVE 锁,可能阻塞其他租户的查询甚至超时失败。应改用 CREATE TABLE ... (LIKE invoices INCLUDING ALL) 创建独立表,添加 CHECK 约束,然后 ALTER TABLE ... ATTACH PARTITION,这样只获取 SHARE UPDATE EXCLUSIVE 锁,实测耗时 23 毫秒。
高并发下,按租户分区且仅用 RLS 过滤会出现什么问题?
每个查询会获取大量锁(例如 1000 分区时约 2003 个锁),可能耗尽共享锁表,导致 'out of shared memory' 错误。实测 32 个并发客户端在 1000 分区时,每次运行有 20 到 32 个客户端失败;3000 分区时 29 个客户端在 6 秒内中止。
PostgreSQL 18 是否改善了按租户分区在 RLS 下的性能?
没有根本改善。在 PostgreSQL 18.3 上,仅策略过滤的查询在 1000 分区时耗时 55 毫秒,3000 分区时 160 毫秒,比 17 版略低,但仍然会获取所有分区的锁(2003 和 6003 个)。显式添加 tenant_id 条件后,规划时间保持在 0.04–0.06 毫秒,执行时间低于 0.1 毫秒。