问题的引入
我们首先将容纳锁的数据结构称为锁管理器。 锁显然是一个共享资源,因此添加一把锁都需要将这个信息添加到锁管理器中, 无论添加的锁是什么类型, 添加的过程需要对锁管理器添加一把排他锁。这里说的锁主要是重量级锁,如 表锁。 因此锁管理器上很容易出现争用。
非常常见的 SELECT/UPDATE/DELETE/INSERT 在表锁上都不互斥。如果走上面的锁管理器的逻辑成本就很高。 我们既想对这种常见的场景进行优化, 又要保证锁的语义正确,那么我们必然要牺牲一些什么。 PostgreSQL 牺牲的是高级别锁的加锁效率。这通常是合理的,因为高级别的锁非常不频繁, 比如 DROP 需要高级别的锁,但很少有应用会高并发运行drop.
那么如何来设计这样的问题呢? 我直接粘贴chatgpt 对 lmgr/README 的部分翻译, 然后再对设计要点进行一些补充。
Fast path lock原理
快速路径锁定是一种特殊的机制,旨在减少获取和释放某些被频繁获取和释放锁的开销,并且这些很少发生冲突。
弱关系锁。SELECT、INSERT、UPDATE 和 DELETE 在操作的每个关系上必须获取锁,以及各种可以在内部使用的系统目录。许多 DML 操作可以在同一时间并行进行,而不冲突;只有如 CLUSTER、ALTER TABLE 或 DROP 这样的 DDL 操作,或者用户的显式操作如 LOCK TABLE,才会与 DML 操作所获取的“弱”锁(AccessShareLock、RowShareLock、RowExclusiveLock)产生锁冲突。
当前(早期PG 9.2)锁定机制在处理这种工作负载时表现不佳。尽管锁管理器的锁是被分区的,但任何给定关系的锁标识(locktag)仍然只属于一个分区。因此,如果许多短查询同时访问同一张表,该分区的锁管理器分区锁就会成为一个争用瓶颈。这种影响在配备 2 核处理器的服务器上就可以被测量出来,并且随着核心数量的增加,这种现象会变得更加明显。
为了减轻这个瓶颈,从 PostgreSQL 9.2 开始,每个后端进程被允许在其 PGPROC 结构中的数组内记录有限数量的针对非共享关系的锁,而不是使用主锁表。在获取锁时,只有在锁定者能够验证不存在冲突锁的情况下,才能使用该机制。
这个算法的一个关键点是,必须能够在不争用共享的 LWLock 或自旋锁的情况下验证可能存在的冲突锁的缺失。否则,这样的做法只会将争用瓶颈从一个地方移动到另一个地方。我们通过使用一个包含 1024 个整数计数器的数组来实现这一点,这实际上是对锁空间的 1024 路分区。每个计数器记录落入该分区的非共享关系上的“强”锁(即,ShareLock、ShareRowExclusiveLock、ExclusiveLock 和 AccessExclusiveLock)的数量。
1. 弱锁获取前提: 当该计数器的值非零时,快速路径机制将不能在该分区内获取新的关系锁。
2. 强锁获取过程: 一个强锁持有者将增加计数器,然后扫描每个后端的数组以匹配快速路径锁;任何找到的锁必须在尝试获取锁之前转移到主锁表,以确保正确的锁冲突和死锁检测。
理解重点:
1. 当我们需要对一张表加上弱锁的时候,我们要看一下表上是否有强锁(强锁集中管理器),如果没有,就不走Lock manager, 直接标记在一个更容易标记的地方(PGPROC)。 我们预期大部分时候都没有强锁。
2. 如果要加强锁,首先在强锁集中管理器中注册这一事件,保证新的弱锁不会出现,然后他就要检查PGPROC, 看看有没有在锁管理器之外的弱锁。 如果发现了, 要将若锁迁移到锁管理器内部, 因为死锁检测需要以来这个数据。
3. 强锁集中管理器是的大小如何控制? 强锁管理器仅仅有1024个空间,任何一把锁都仅仅存放在其中的一个分区。 这理论上可能导致hash冲突,现实中应很很少发生, 即使发生了也没有正确性问题, 影响仅仅是丢失了一次fast path 的机会。
4. 现在所有的锁都需要和 强锁管理器打交道,那么强锁管理器的访问会不会成为新的瓶颈,这个我们到下一篇文章再讨论。
Fast Path 监控:
pg_locks.fastpath 属性
Fast Path 限制
在PG17 之前, 代码实现决定了一个事务中仅有16个弱锁,在PG17 以后,这个值变成了 max_locks_per_transaction;
使用建议
PG 的表锁通常都是在事务结束时释放(并不是语句结束释放)。 所以如果一个事务中访问的锁过多,依然会触发上述的限制。 比如 有成千上百的子分区,大量的索引等。
有些query 可能最终只使用了一个索引,但在planning 阶段需要访问所有的索引去生成Plan, 这也可能浪费一些弱锁。 这个问题可以通过 Plan Cache 来缓解,因为它避免了重复生成执行计划。
同时减少索引, 减少分区个数,一个事务中的语句数量对这一主题都是有帮助的。