AnalyticDB for PostgreSQL 4.3版本(以下简称ADBPG 4.3)存在着严重的并发瓶颈,很多操作都需要加互斥排它锁。这导致ADBPG 4.3在高并发情况下,TP 性能不太理想,TPC-C最高只能达到5000 tpmC。AnalyticDB for PostgreSQL 6.0版本(以下简称ADBPG6.0)进行了很多高并发执行优化,解决了很多不必要的锁竞争,极大的提升并发吞吐,将TPC-C的性能提升到了20W+ tpmC,性能相比ADBPG 4.3提升了几十倍。本文详细介绍ADBPG 6.0实现TP性能提升所采用的优化方案,主要包括:全局死锁检测机制、事务优化、表锁的fastpatch机制。
1、全局死锁检测机制
在早期 ADBPG 版本中, 由于没有全局级别的死锁检测,为了避免出现跨分区 segment 的死锁场景, 默认会将 UPDATE/DELETE 所加锁由行锁提升至表锁, 即单表上的 UPDATE/DELETE 只能串行执行. 另外在 PRIMARY KEY/UNIQUE INDEX 存在的场景下, 由纯粹 INSERT 语句组成的事务并发执行时也有可能会导致死锁,故 insert from select 语句也会提升为表锁。
为了解决死锁问题以,并提升高并发 OLTP 能力, ADBPG 在4.3版本 及 6.0版本中均引入了全局死锁检测机制(Global Deallock Detector,以下简称 GDD)。该机制能检测到跨越多个 segment 出现的死锁场景, 并按照一定规则来打破死锁循环. 简单来说, ADBPG 6.0全局死锁检测分为如下几步:
- Build lock waits-for graph。GDD 会执行 SELECT * FROM pg_locks 来获取锁等待信息, 之后以 session id 作为图顶点, 遍历扫描锁等待信息. 期间若发现 session A 在等待着 session B 持有着的某个锁, 则认为 A 在等待 B, 此时会在图中建立一个从 A 到 B 的边.
- Reduce lock waits-for graph。GDD 会反复遍历第一步建立生成的锁等待图。当发现一个顶点没有出边或者入边, 即表明指定会话没有在等待其他会话, 或者没有其他会话在等待该会话时, 便会将顶点以及关联的边从图中移除. 因为这时可以证明该顶点一定处于一个环中。
- Break deadlock cycle。在 Reduce 完成之后, 若 GDD 发现此时锁等待图中不再包含任何顶点, 那么则说明本轮检测没有发现死锁。仍包含有顶点, 那么此时变说明存在死锁情况。此时 GDD 会反复尝试移除具有最大 session 值的顶点, 然后 Reduce 这一步, 直至锁等待图变为空。之后对于被移除的 session, 调用 pg_cancel_backend 来取消这些 session 中 SQL 的执行。
有了GDD后,ADBPG 4.3 和 6.0的UPDATE/DELETE 可以只需要加行锁,而不再需要加表锁,因此单表上的UPDATE/DELETE可以高并发执行,不会因加表锁而阻塞其他该表的并发操作执行。即便出现死锁,GDD可以有效地检测到并且破除死锁。另外,在GDD框架下,ADBPG 6.0对行存表的select for update操作,也不再需要加表锁,而是降级到加行锁。select for update是TPC-C标准测试集的占比较高的SQL语句之一,避免加表锁无疑会大大提升该语句的并发执行能力。
注意:考虑到GDD带来的并发能力的大幅提升,目前ADBPG4.3和6.0版本均已经合入了GDD,以将UPDATE/DELETE的锁从表锁降为行锁。ADB PG 4.3版本的新实例默认引入GDD,UPDATE/DELETE为行锁,但SELECT FOR UPDATE仍然维持着加表锁。 ADB PG 6.0所有实例均引入GDD,且SELECT FOR UPDATE 也为行锁。
2、事务优化
ADBPG 4.3的开始事务(StartTransaction)和结束事务(CommitTransaction)都存在着大量的临界区竞争,针对临界区的加锁行为,严重制约着ADBPG 4.3的并发性能。而ADBPG 4.3在开始事务和结束事务时的加锁行为,是由如下设计逻辑导致的。
2.1、开始事务
分配事务ID。在ADBPG 4.3中,无论是在QD端开始一个分布式事务,还是在QE端开始一个本地事务,ADBPG 4.3都会默认首先去获取一个本地事务ID(xid)。获取xid时,实质是对共享变量进行自增,因此需要持有XidGenLock的排它锁(LW_EXCLUSIVE)。同时,QD端还会去额外获取一个分布式事务ID(gxid),以保证分布式事务的正确执行。同理,获取gxid时,需要持有ProcArrayLock的排它锁。当并发较大时,排它锁的持有会成为性能瓶颈。
分布式事务映射。ADBPG 4.3的事务设计逻辑是分布式事务和本地事务共存:即在执行时,QD会开启一个分布式事务,以保证用户请求在执行时的跨节点强一致性;同时,QD和每个QE还会开始一个本地事务,作为分布式事务在每个节点上的执行单元。这里会存在分布式事务和本地事务的关联映射问题。在ADBPG 4.3中,是通过gxid和xid的映射,来将分布式事务和其对应的各个本地事务关联在一起的。
ADBPG 4.3通过LocalDistribXactData结构体来完成gxid和xid之间的映射,无论在QD上还是在QE上,在每次映射时,都需要从空闲链表上摘掉一个空闲的LocalDistribXactData的实例,将gxid和xid赋给它后,再将它加入到非空闲链表中。由于空闲链表和非空闲链表都是临界资源,对其的操作需要以只有ProcArrayLock的排它锁为前提,相关示例代码如下:
typedef LocalDistribXactData* LocalDistribXact;
LocalDistribXact ele;
LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE);
ele = SharedDoublyLinkedHead_RemoveFirst(&LocalDistribXactShared->sortedLocalBase, &LocalDistribXactShared->freeList);
ele->distribXid = gxid;
ele->localXid = xid;
SharedDoublyLinkedHead_AddLast(&LocalDistribXactShared->sortedLocalBase,&LocalDistribXactShared->sortedLocalList,ele);
LWLockRelease(ProcArrayLock);
同时,每次开启一个事务,QD会从一个全局TMGXACT数组中取一个空闲的项来记录分布式事务的状态。对该全局数组的操作依然需要持有ProcArrayLock的排它锁,相关示例代码如下:
TMGXACT *gxact;
LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE);
gxact = shmGxactArray[(*shmNumGxacts)++];
LWLockRelease(ProcArrayLock);
从以上分析可以看出,对ProcArrayLock排它锁的持有,会很大程度上影响高并发下的性能。尤其是ProcArrayLock,其在事务处理的很多阶段(比如事务提交、获取Snapshot等)都会被持有,因此,过多持有其排他锁会带来严重的竞争,限制事务的并发。
2.2、结束事务
由于开始事务时,从全局链表和全局数组中获取了资源,那么在结束事务时,就需要将资源清空后,再返还给全局资源。在将空闲资源插入到全局链表和全局数组中时,牵涉到对全局共享资源的改动,因此,仍然需要持有ProcArrayLock的排它锁来保护相关操作。和开始事务同理,对ProcArrayLock的排它锁的持有,会限制系统的并发性能。
ADBPG6.0对开始事务和结束事务存在的大量加锁行为进行了优化,消除了不必要的临界区竞争,将全局共享资源的操作,改成了私有变量的操作。具体优化逻辑如下。
2.3、事务id延迟分配
ADBPG6.0中,当开始事务时,QD和QE并不会首先去获取一个本地事务ID(xid)。对于一个事务来说,如果该事务只处理读操作而不处理写操作,那么该事务是不需要去获取本地事务ID的。因此,ADBPG6.0将获取本地事务ID的操作,一直推迟到在事务中遇到写操作时才执行,而不是在事务一开始就去获取。如果事务中没有写操作,那么就不会再获取本地事务ID了。
但是QD上仍然会获取一个分布式事务ID(gxid),以确保分布式事务的正确执行。ADBPG6.0对获取分布式事务ID也进行了优化,通过pg_atomic_add_fetch_u32的原子自增操作,来对全局共享变量进行自增和取值,不再需要持有ProcArrayLock的排它锁,提升了事务ID获取的并发性。
2.4、共享资源变为私有变量
对于存在写操作的事务,ADBPG6.0仍然会维护分布式事务ID和本地事务ID的映射。在优化中,ADBPG6.0存储分布式事务ID和本地事务ID映射关系的结构体资源(LocalDistribXactData),不再从全局共享资源中去进行分配和回收,而是各自进程维护自己独立的资源,在进程创建的时候就分配好,在进程结束时就销毁。这种情况下,在进行事务映射时,只需要对自己的私有变量赋值即可,不要再去持有ProcArrayLock。结束事务时,也只需要清空自己私有变量的相关赋值。这种优化消除了临界区的持锁竞争,提升高并发事务下的性能。
同时,QD为了记录分布式事务状态而维护的TMGXACT结构体资源,也不再从全局共享数组去分配和回收,也是每个进程自己在创建的时候就提前分配好资源,在分布式事务创建/提交的时候,对自己的私有变量进行赋值/清空。这也同样避免了对ProcArrayLock的持有申请,提升系统执行的并发性。
2.5、分布式事务优化
ADBPG 4.3的分布式事务也存在着严重瓶颈,主要体现在:如果一个分布式事务的相关操作,只涉及一个segment,那么在该分布式事务提交时,ADBPG 4.3仍然会走两阶段提交,并将其他不相关的segment也涉及进来。两阶段提交是分布式事务性能的“杀手”。 ADBPG6.0对此进行了很好的优化。在分布式事务执行的过程中,ADBPG6.0会记录该事务在执行过程中涉及到的segment。如果整个执行过程中只涉及到一个segment,那么在事务提交时,ADBPG6.0就不再需要走两阶段提交,而是通过一阶段提交即可完成。如果整个执行过程涉及到了多个segment,那么ADBPG6.0只会在这些涉及到的segment上走两阶段提交,不会将无关的segment涉及进来。
3、表锁的fastpatch机制
ADBPG6.0引入了事务优化的一个重大特性 -- 表锁的fastpath机制。ADBPG 4.3没有fastpath机制,对于DML操作SELECT/INSERT/UPDATE/DELETE,在加锁时,需要走到主表的加锁逻辑。这时需要在主表(LockMethodLockHash和LockMethodProcLockHash)中记录加锁信息(LOCK和PROCLOCK),所以要在主表上加上LWLock的排它锁。尽管在主表加锁时,对主表进行了分片,每个分片对应一把排它锁,这样对于不同表的加锁,可能会映射到不同的分片,从而可以减少对主表操作的锁竞争。但是在TP场景下,很多的负载是对同一张表的大并发操作,此时,分片就没有任何效果,所有的并发在加表锁时,都是串行的,严重地影响了TP性能。
Fastpath机制可以有效地减少加锁的开销和大并发下的阻塞问题,从而提高TP的性能。Fastpath适用的场景为DML操作(SELECT/INSERT/UPDATE/DELETE)在加对应表的表锁,对应的锁类型为AccessShareLock、RowShareLock、RowExclusiveLock。ADBPG6.0当前表锁的类型有如下8种:AccessShareLock、RowShareLock、RowExclusiveLock、ShareUpdateExclusiveLock、ShareLock、ShareRowExclusiveLock、ExclusiveLock、AccessExclusiveLock。他们之间的冲突矩阵如下所示(X代表有冲突):
锁类型 | AccessShareLock | RowShareLock | RowExclusiveLock | ShareUpdateExclusiveLock | ShareLock | ShareRowExclusiveLock | ExclusiveLock | AccessExclusiveLock |
---|---|---|---|---|---|---|---|---|
AccessShareLock | X | |||||||
RowShareLock | X | X | X | |||||
ShareUpdateExclusiveLock | X | X | X | X | ||||
ShareLock | X | X | X | X | X | |||
ShareRowExclusiveLock | X | X | X | X | X | X | ||
ExclusiveLock | X | X | X | X | X | X | X | |
AccessExclusiveLock | X | X | X | X | X | X | X | X |
从上面的冲突矩阵中可以看出,AccessShareLock、RowShareLock、RowExclusiveLock这三种类型的锁之间互不冲突,他们对应的操作为SELECT/INSERT/UPDATE/DELETE等DML操作。Fastpath机制利用了这个特点,如果一个表上只会加三类表锁,那么加锁请求可以直接加锁无需判断他们之间是否冲突。在这里我们将这三种类型的锁称为weak relation lock。而对于ShareLock、ShareRowExclusiveLock、ExclusiveLock、AccessExclusiveLock这几种类型的锁,我们称为strong relation lock。
Fastpath实现机制。每个backend在PGPROC结构体之中记录了一定数量(默认16个)的表锁(只能是非共享表的表锁)的加锁情况,当请求的表锁类型为weak lock时,并且通过FastPathStrongRelationLocks判断出当前表没有其它backend持有strong relation lock时,那么此次加锁请求则直接通过fastpath加锁,将加锁信息记录在PGPROC和locallock中,而无需操作主表进行加锁。当请求strong relation lock时,不能使用fastpath进行加锁,并且会将、FastPathStrongRelationLocks对应加锁表的分区计数+1,表示对应表的锁有strong lock,用于其它backend加weak relation lock时来判断当前这个表锁有没有加strong relation lock;然后需要访问其它backend的PGPROC中的fastpath加锁信息,将这些backend在这个表上weak relation lock的加锁信息同步到当前的backend中;最后,走主表加锁逻辑进行加锁。对ShareUpdateExclusiveLock类型的加锁请求,不能走fastpath加锁逻辑,而是直接走主表加锁逻辑,并且无需更新FastPathStrongRelationLocks和同步其它backend的fastpath加锁信息,因为它与weak relation lock不冲突。
从上面fastpath加锁机制来看,对于普通DML的加锁如果走fastpath,首先不需要操作主表减少了单个加锁操作的开销,其次是,如果有大并发存在,由于fastpath操作的数据结果在本地backend的PGPROC之中,因此并发之间基本没有锁竞争,在大并发对同一个表的操作负载下能够显著提高性能。
4. 参数配置
在ADBPG6.0中,需要获取极致的TP性能要对以下参数进行检查:
参数 | TP友好型参数值 | 说明 | 操作 |
---|---|---|---|
optimizer | off | 关闭orca优化器 | 如果为on, 用户可以在本session内进行设置 |
gp_enable_global_deadlock_detector | on | 打开全局死锁检测,去掉表锁 | 需要重启集群,如果为off,用户需要联系ADBPG值班同学进行设置 |
resource_scheduler/gp_enable_resqueue_priority | off/off | 关闭resource queue限制,以便跑出更高的并发,跑正常业务不建议关闭 | 需要重启集群,如果任意为off,用户需要联系ADBPG值班同学进行设置 |
rds_enable_custom_plan | on | 使用新生成计划,避免计划广播 | 如果为off,用户可以在本sesion内进行设置 |
random_page_cost | 10 | 如果表某列上建有索引,explain查看计划时没有走index scan,需要设置此参数,来减少随机访问的代价,使得查询走index scan | 用户可以在本sesion内进行设置 |
log_statement | none | 关闭日志输出,跑正常业务不建议关闭 | 如果不为none,用户需要联系ADBPG值班同学进行设置 |
max_prepared_transactions | 不建议超过1500 | 用户可以并发执行的总的事务数 | 需要重启集群,如果过低,用户需要联系ADBPG值班同学进行设置 |
rds_max_non_super_conns | 不建议超过500 | 用户总的连接数限制 | 如果过低,用户需要联系ADBPG值班同学进行设置 |