硬核内容: PostgreSQL fast-path lock 分析
本期播客
硬核内容: PostgreSQL fast-path lock 分析
PG 19 引入了 pg_stat_lock 视图, 用于统计锁等待增量信息, 其中有一项 fastpath_exceeded 指标, 这个指标,值得单独讲一下,因为它不是“锁冲突次数”,而是“锁获取路径退化次数”。
先说结论:
它升高,不等于一定发生了锁等待;但它往往意味着你的锁管理成本在变贵。
什么是 fast-path lock
PostgreSQL 为一部分常见、低冲突的锁设计了 fast-path 快速路径。
核心目标只有一个:别什么锁都进主锁表。
因为主锁表是共享结构,访问它要付出更高的并发协调成本。
如果某些锁本来就很常见、冲突概率又低,那就没必要每次都走“重路径”。
所以 fast-path 本质上是一种优化机制:
能在 backend 自身的 fast-path 槽位里解决,就尽量别进全局主锁表。
fastpath_exceeded 统计的到底是什么
它统计的是:
某类锁本来尝试走 fast-path,但因为 fast-path 槽位容量不够,没走成,只能退回普通路径的次数。
注意两个边界:
它不是“锁冲突次数” 它也不是“锁等待时长”
它只说明一件事:
快速路径装不下了。
所以这个指标更像“锁子系统的容量压力信号”,而不是“阻塞事故计数器”。
为什么会 exceed
最典型的原因,就是单个事务或单条语句需要同时接触很多对象,导致 fast-path 槽位被打满。
这也是为什么官方测试特意用了“分区表”场景:
建一个分区表 创建超过 max_locks_per_transaction的大量分区执行一次会访问这些分区的查询 观察 pg_stat_lock里relation的fastpath_exceeded上升
这背后的原理很直接:
查询分区表时,执行器往往需要接触多个子分区 每个关系对象都可能涉及 relation lock 当一个 backend 需要持有的 relation 级锁太多时,fast-path 槽位不够 多出来的那部分就只能回退到主锁表
所以它暴露的不是“你被谁堵住了”,而是:
你的 workload 正在把轻量锁路径挤爆。
为什么 DBA 和架构师要重视它
因为 fastpath_exceeded 高,通常意味着这几类风险之一正在发生:
分区数过多,单次查询接触对象太多 事务过宽,同时持有大量 relation 级锁 某些 DDL / 元数据操作让锁对象数急剧膨胀 应用访问模式导致 backend 频繁超出 fast-path 容量
这类问题的可怕之处在于:
它不一定马上表现成明显阻塞,但会先表现成锁管理开销上升、并发效率下降、抖动加剧。
也就是说,fastpath_exceeded 更像“事故前兆”,不是“事故现场”。
什么时候它最有解释力
这几个场景里,它特别有价值:
大分区表系统 多租户 schema 很多、对象很多的系统 宽事务、批处理事务 一条 SQL 会触达大量分区或大量关系对象的场景 并发高但锁等待日志又不明显的系统
如果你发现:
waits没有特别夸张但系统并发一高就发抖 CPU、LWLock、锁相关开销看起来怪怪的 同时 relation.fastpath_exceeded持续增长
那就要警惕了。
问题可能不是“锁冲突严重”,而是“锁路径退化严重”。
它不说明什么
也要防止误判。
fastpath_exceeded 升高,不自动推出以下结论:
不代表一定有长时间阻塞 不代表一定有死锁风险 不代表一定是 SQL 写错了 不代表单纯调大 max_locks_per_transaction就万事大吉
因为如果根因是分区设计过碎、事务范围过宽、访问模式过散,那么调参数只是延后症状,不是消灭原因。
正确的排查思路
看到它升高后,建议按这个顺序想:
是哪类锁升高了
通常重点先看relation。workload 最近有没有变化
比如新增大量分区、批任务、DDL、复杂报表。单事务接触对象数是不是过多
尤其是分区表、继承表、复杂查询计划。是否需要评估 max_locks_per_transaction
但这一步是容量兜底,不是默认答案。能不能从设计上减少对象接触数
这通常比单纯调参数更根治。
一句话概括
fastpath_exceeded 不是“锁冲突计数器”,而是锁获取从便宜路径退化到昂贵路径的压力指标。
它最适合回答的问题不是“谁阻塞了谁”,而是:
我的并发访问模式,是不是已经把 PostgreSQL 的轻量锁优化打穿了?
下面继续展开讲讲
fast-path lock在 PostgreSQL 内部到底是怎么工作的fastpath_exceeded高了以后,DBA 该怎么调参数、改表设计、改 SQL 访问路径
1. fast-path lock 在 PostgreSQL 内部怎么工作
简单说:
它是 PostgreSQL 给一部分常见 relation 锁准备的“快捷通道”。
正常情况下,锁要进全局主锁表;但主锁表是共享结构,并发高时更贵。
所以 PostgreSQL 做了优化:
对一部分低冲突、常见的 relation 级锁 先尝试放进 backend 自己的 fast-path 槽位 放得下,就不走全局主锁表 放不下,或者不满足 fast-path 条件,就回退到普通锁路径
所以它的本质是:
fast-path= 便宜路径普通主锁表 = 更重路径
fastpath_exceeded 变高,表示:
本来想走快捷通道 但槽位不够 只能退回普通路径的次数变多了
它不直接等于锁冲突,但通常意味着锁管理成本上升。
2. fastpath_exceeded 高了以后,DBA 怎么做
先记住原则:
先找“为什么一个事务要碰这么多对象”,再考虑调参数。
优先排查这几类问题:
分区太多,一条查询要碰很多分区 事务太宽,同时持有很多 relation 锁 批处理/报表 SQL 一次扫太多对象 DDL 频繁,元数据相关锁很多
3. 参数怎么调
最直接的是看 max_locks_per_transaction。
它可以考虑调大,但要谨慎:
适合:确实有大量对象需要同时加锁的场景 风险:这只是扩容,不是根治;会增加共享内存消耗
所以建议是:
如果业务模型合理,只是对象数量确实多,可以适度调大 如果根因是设计失控,别指望只靠参数解决
4. 表设计怎么改
重点是减少单次操作接触的对象数。
常见做法:
控制分区数量,别把分区切得过碎 优化分区策略,让查询更容易分区裁剪 避免一个事务跨太多表、分区、索引对象 把超大批处理拆小,缩短事务生命周期
一句话:
对象越多,锁越多,fast-path 越容易被打穿。
5. SQL 访问路径怎么改
核心目标也是一个:
让单条 SQL 少碰对象。
可以优先做这些:
确保谓词能触发 partition pruning 避免无条件扫大量分区 把超大查询拆成更小、更聚焦的批次 减少一个事务里无必要的多表/多分区访问 避免长事务一直占着大量 relation 锁
6. 实战判断标准
如果你看到:
pg_stat_lock.fastpath_exceeded持续升高主要集中在 relation同时系统高并发时抖动明显
那优先顺序应该是:
看分区和对象数量 看事务宽度 看 SQL 是否访问过多分区/表 最后再评估 max_locks_per_transaction
一句话总结
fast-path lock= PostgreSQL 为常见 relation 锁准备的低成本快捷通道fastpath_exceeded高 = 快捷通道容量不够,越来越多锁回退到重路径DBA 的正确动作 = 先减对象数、减事务宽度、优化分区裁剪,再考虑调大 max_locks_per_transaction