背景
He3DB for PostgreSQL是受Aurora论文启发,基于开源数据库PostgreSQL 改造的数据库产品。架构上实现计算存储分离,并进一步支持数据的冷热分层,大幅提升产品的性价比。
He3DB for PostgreSQL中存在多个会话试图同时访问同一数据的情况,并发控制的目标就是保证所有会话高效地访问,同时维护数据完整性,并发访问控制的常用方式为两种:锁机制和多版本并发控制(MVCC)。因为 MVCC 并不能解决所有的并发控制情况,所以还需要使用传统的锁机制来保证那些通常不需要完整事务隔离并且想要显式管理特定冲突点的应用。
整体概述
按照功能划分,锁管理分为锁功能模块,锁级别管理模块,死锁处理模块。
锁功能模块:针对三种类型的锁功能,自旋锁,轻量级锁,事务锁。
锁级别管理模块:针对四种不同级别的锁管理器,表级别、页级别、元组级别、事务级别。
死锁处理模块:包括死锁检测功能和死锁处理功能。
数据结构
- EDGE
EDGE 有向边结构
typedef struct
{
PGPROC *waiter; /* the leader of the waiting lock group */
PGPROC *blocker; /* the leader of the group it is waiting for */
LOCK *lock; /* the lock being waited for */
int pred; /* workspace for TopoSort */
int link; /* workspace for TopoSort */
} EDGE;
- WAIT_ORDER
WAIT_ORDER 锁等待队列重排序
typedef struct
{
LOCK *lock; /* the lock whose wait queue is described */
PGPROC **procs; /* array of PGPROC *'s in new wait order */
int nProcs;
} WAIT_ORDER;
- DEADLOCK_INFO
DEADLOCK_INFO 死锁环中的边的信息
typedef struct
{
LOCKTAG locktag; /* ID of awaited lock object */
LOCKMODE lockmode; /* type of lock we're waiting for */
int pid; /* PID of blocked backend */
} DEADLOCK_INFO;
- DeadLockState
DeadLockState 死锁检测的记录结果
typedef enum
{
DS_NOT_YET_CHECKED, /* no deadlock check has run yet */
DS_NO_DEADLOCK, /* no deadlock detected */
DS_SOFT_DEADLOCK, /* deadlock avoided by queue rearrangement */
DS_HARD_DEADLOCK, /* deadlock, no way out but ERROR */
DS_BLOCKED_BY_AUTOVACUUM /* no deadlock; queue blocked by autovacuum
* worker */
} DeadLockState;
死锁处理设计
设计原理
在He3DB中,事务可以按照任意顺序申请锁,所以必须考虑死锁的处理机制。
He3DB对于死锁的预防分为两步:
1)当进程请求加锁时,如果失败,会进入等待队列。如果在队列中已经存在一些进程要获取本进程中已经持有的锁,那么为了尽量避免死锁,可以简单地把本进程插入到它们的前面。
2)当一个锁被释放时,将会试图唤醒等待队列中的进程。如果其中的某个进程的要求与排在它前面但由于某些原因不能被唤醒的进程冲突,这个进程将不被唤醒。这么做可以保证互相冲突的加锁请求按照到达的先后被处理。
当然,仅仅依靠上述死锁预防方法还不能够完全消除死锁。He3DB还提供一套死锁检测机制。死锁检测的触发条件如图2所示。
He3DB使用等待图(WFG)来进行死锁检测。WFG是一个有向图,图中的顶点表示申请加锁的进程,而图中的有向边表示依赖关系(等待关系),如图3所示。
如果进程A在等待进程B(进程A申请的锁a与进程B持有的锁b冲突,或者进程A申请的锁a与进程B申请的锁b冲突并且进程B比进程A申请锁的时间更早),那么从顶点A到顶点B就有条有向边。系统出现死锁当且仅当WFG中出现环。
SoftEdge和Hard Edge的定义
1)进程A和进程B都在某个锁的等待队列中,进程A在进程B的后面。如果进程A和进程B的加锁要求冲突,即进程A在等待进程B,这时候顶点A到顶点B会有一条有向边。我们称这样的有向边为 Soft Edge。
2)如果进程A的加锁要求和进程B已持有的锁冲突,这时候顶点A到顶点B也有一条有向边。我们称这样的有向边为 Hard Edge。
如果WFG中的环是有Soft Edge的环,可以通过拓扑排序对队列进行重排,尝试消除死锁。
He3DB死锁检测与消除算法如下:
a. 从每一个顶点开始出发,沿WFG中的有向边走,看能不能回到此顶点。如果找到一条路径满足此条件,则说明出现死锁。
b. 如果此路径中没有 Soft Edge,直接终止这个事务。
c. 否则记录所有出现的 Soft Edge。对于这个集合,递归地枚举它的所有子集,尝试进行调整。
d. 对于上述每个子集,使用拓扑排序来排出一个调整方案,并逐一加以测试(一旦找到一个可行的调整方案,这个测试过程就结束了,He3DB并没有要求这个调整方案一定是最优的)。如果找不到一个可以消除死锁的方案,则死锁消除失败,然后终止这个事务。
假设有两个资源X和Y。进程C持有X的共享锁,进程B请求X的排他锁,先进入等待队列:进程A请求X的共享锁,在进程B之后进入等待队列。进程A持有资源Y的排他锁,进程C请求Y的排他锁,进入等待队列,其WFG如图8所示。
He3DB对进程A的检测过程如下:
a. 调用DeadLockCheck试图检测和消除死锁,DeadLockCheck将调用DeadLockCheckRecurse,递归进行死锁检测。
b. DeadlockCheckRecurse调用TestConfiguration测试当前的队列状态会不会发生死锁。TestConfiguration调用ExpandConstrains进行约束性检査,若不满足,则死锁。其中可对Soft Edge进行调整,并调用TopoSort判断调整后的序列是否合法。
c. 最后调用FindlockCycle判断是否出现环,如果出现并且不能调整,则死锁。
主要流程
在He3DB中,当进程不能获得锁进入等待队列时,就会触发死锁检测的操作。如果发现了“死锁”,进而尝试进行死锁的解除操作,死锁解除采用枚举的方法去尝试调整等待队列中进程的等待先后拓扑顺序,试图找到一种打破进程之间循环等待的状态。
(1)死锁处理相关数据结构的初始化
每个服务进程启动的时候都需要初始化死锁检测器,即给死锁检测时需要用到的数据结构分配内存空间。
这个初始化过程由函数InitDeadLockChecking完成。每个服务进程都需要进行死锁检测器的初始化过程,因为服务进程不可能从Postmaster那里继承其工作空间的内容。另一方面,之所以在每个服务进程启动的时候就马上进行这个初始化过程,有两个原因:
其一,如果等到死锁检测器被启动的时候才进行初始化,可能已经没有空余的内存可以分配;
其二,死锁检测一般在SignalHandler中运行,而这部分的内存空间如果分配给死锁检测器的话不够安全。
(2)死锁检测入口函数
死锁检测入口函数为DeadlockCheck,用于在一个给定进程中检查死锁。如果发现了死锁,该函数会重新排序锁的等待队列以消除这个死锁。如果这个死锁无法消除,则返回DS_HARD_DEADLOCK。该返回值提醒调用者中断正在处理的事务。
在执行该函数之前,调用者必须已经获取整个锁表结构的LWLock。如果该函数执行失败,死锁详细信息将保存在DeadlockDetails中。
记录在DeadlockDetails中的死锁详细信息将由DeadLockReport函数打印出来。之所以将死锁报告和死锁检测函数分开实现,有两个原因:
其一,死锁检测函数执行时需要持有整个锁表结构上的LWLock,而在执行死锁报告函数的时候并不需要这些锁。如果在执行DeadLockReport的时候还持有这些LWIock,会影响系统的并发性能。
其二,DeadLockCheck函数执行时处于SignalHandler关键内存中。
(3)死锁检测调整函数
死锁调整定义在函数DeadLockCheckRecurse中,它递归地调整等待队列的排序,寻找无死锁的队列。其中最主要的操作就是检测调整后的等待队列是否满足“约束”条件。如果等待队列中进程A一定得在进程B之前才能消除死锁,我们称这是一个约束。如果对等待队列重新排序后,进程B在进程A前面,那么称这个重新排序后的等待队列不满足约束条件。如果等待队列的任何拓扑序列都无法消除死锁,则该函数返回TRUE。如果发现可用的新等待序列,则保存这个新序列,并返回FALSE。
(4)等待图的环检测函数
该函数为FindLockCycle,它调用FindockCycleRecurse函数检测等待图中是否有死锁环。如果发现了死锁环,则通过调用参数返回一个Soft Edge的链表。
主要接口
对外接口函数 | 接口说明 |
---|---|
void InitDeadLockChecking(void) | 初始化相关结构 |
DeadLockState DeadLockCheck(PGPROC *proc) | 死锁检测 |
void DeadLockReport(void) | 死锁结果状态上报 |
内部接口函数 | 接口说明 |
bool DeadLockCheckRecurse(PGPROC *proc) | 死锁检测递归 |
bool FindLockCycle(PGPROC *checkProc, EDGE *softEdges, int *nSoftEdges) | 死锁环检测 |
bool FindLockCycleRecurse(PGPROC *checkProc, int depth, EDGE *softEdges, int *nSoftEdges) | 死锁环递归检测 |
bool TopoSort(LOCK *lock, EDGE *constraints, int nConstraints, PGPROC **ordering) | 序列顺序检查 |
作者介绍
徐元慧,移动云数据库高级系统架构师,负责云原生数据库He3DB的架构设计与研发。