Greenplum资源队列相关的死锁问题

一、概述

这个主题似乎有点令人懵逼,数据库的资源管理不外乎是并发、CPU、内存、IO等资源的分配,和死锁这种往往是业务逻辑引发的问题有什么关系。而对于熟悉资源队列的人往往咬牙切齿,深受其害,好不容易业务上的逻辑捋顺了,上线跑着跑着突然就报“deadlock detected”了

这TM是啥?多个session同时获取相同的几个表锁,或者同时更新一个表的几行造成的业务死锁我改下逻辑就好了;但是这诡异的报错是谁和谁锁了,咋改?

deadlock detected
Process 12316 waits for ExclusiveLock on resource queue 6055; blocked by process 23280;
Process 23280 waits for ExclusiveLock on resource queue 6055; blocked by process 25260;
Process 25260 waits for ExclusiveLock on resource queue 6055; blocked by process 37721;

Greenplum资源队列导致死锁的问题,是一类在实际生产中出现频率比较高,但资料比较少的问题。本文会按照PG/GP相关的背景知识、PostgreSQL的锁、Greenplum的资源队列与锁、死锁、资源队列常见死锁原因与解决办法的顺序,尽量深入完整地介绍这个问题。对PostgreSQL和Greenplum底层比较感兴趣的读者可以全部阅读,如果只对死锁问题的解决感兴趣,可以只阅读最后一章“资源队列常见死锁原因与解决办法”部分。

二、背景知识

2.1、PostgreSQL

PostgreSQL是一种功能强大、稳定可靠的关系型数据库管理系统。它以其开源性和丰富的功能而闻名,用户可以免费获取和使用。作为一种关系型数据库,PostgreSQL支持SQL语言,可以进行常见的数据操作和查询。它具有ACID特性,确保数据的完整性和一致性。PostgreSQL还支持多种数据类型,包括整数、字符串、日期/时间、JSON等。它具有良好的可扩展性,可以处理大规模和高并发的数据工作负载,并提供了多用户和权限管理功能,是应用最广泛的关系型数据库系统之一。

2.2、GreenPlum

Greenplum是一个高性能、可扩展的数据仓库系统,它是基于PostgreSQL开发的。Greenplum的设计目标是处理大规模数据分析和数据仓库应用。它通过将数据分布在多个节点上并并行处理来实现高度的并发性和处理能力。Greenplum具有强大的分布式查询优化器和并行执行引擎,可以快速处理复杂的分析查询和大规模数据集。它还提供了一系列的数据加载和提取工具,以及与其他大数据工具和生态系统的集成。Greenplum具有完整的事务支持,是应用最广泛的数据分析系统之一。

2.3、OS层面的进程同步

谈到锁与并发控制,大多数技术人员都并不陌生,很多编程语言都提供了并发控制的方法库,比如C的mutex,Java的ReentrantLock、Go的RWMutex等,这些大多数都是线程间同步的库,由于多个线程之前天然能够共享内存空间,线程同步相对比较简单轻量。而PostgreSQL是进程模型,对于每个用户连接,数据库Server会fork出新的进程来处理。对于进程的并发控制的实现方式,往往需要通过基于进程间通信(IPC)来实现。

IPC(inter-process communication,进程间通信)是指在不同进程之间进行数据交换的方式。Linux提供了多种IPC方式,常用的包括:

  1. 管道(Pipe):管道是一种半双工的通信方式,用于具有亲缘关系的进程间通信。它可以在父进程和子进程之间传递数据,但只能在具有公共祖先的进程之间使用。

  2. 信号量(Semaphore):信号量是一种计数器,用于多个进程之间的进程同步和互斥。它可以用来控制对共享资源的访问,通过对计数器的操作实现进程间的同步。

  3. 共享内存(Shared Memory):共享内存是一种将内存段映射到多个进程地址空间的机制。通过共享内存,进程可以直接读写共享内存区域,从而实现高效的数据交换。

  4. 信号(Signal):信号是一种异步通信方式,用于向进程发送通知。可以通过系统调用发送信号,进程收到信号后可以采取相应的动作。

  5. 消息队列(Message Queue):消息队列是一种按照消息类型进行通信的方式。进程可以将消息放入队列,并由其他进程接收和处理。

  6. 套接字(Socket):套接字是通过网络进行进程间通信的一种方式。它可以在不同的主机之间进行通信,提供了可靠的、面向连接的通信机制。

  7. 文件(File):文件锁是一类应用很广泛的进程间同步的方式。

在PostgreSQL中的一种锁实现机制就是基于信号量Semaphore来在多个进程间进行同步,保证同一时间内只有一个进程获取到锁。

三、PostgreSQL的锁

现代数据库管理系统都支持多个用户同时向系统提交修改以及发送查询。为了保持保证多个数据库请求之间的并发安全性,PostgreSQL基于锁机制对多个会话请求的资源进行管理,以保证数据的一致性、实现并发控制以及维护数据完整性。

3.1、PostgreSQL的几类锁

对于数据库的使用者来说,接触更多的是加在表或者元组上面的常规锁,比如我们执行ALTER TABLE类的TTL操作的时候,会在整表上加最高级别的AccessExclusiveLock锁,阻塞所有操作,包括SELECT。

对于数据库开发者来说,除了上述Regular Lock,为了在数据库中维护并发安全性,PostgreSQL还实现了内部使用的多种类型的锁。具体来说,PostgreSQL使用四种类型的进程间锁:

自旋锁(SpinLock)。这些锁用于非常短期的锁定。如果一个锁需要持有超过几十条指令,或跨越任何内核调用(甚至是对非平凡子程序的调用),请不要使用自旋锁。自旋锁主要用作轻量级锁的基础设施。如果可用,它们使用硬件原子测试和设置指令来实现。等待进程会忙等待直到获取到锁。自旋锁没有提供死锁检测、出错时的自动释放或其他优点。如果在大约一分钟内无法获取到锁,则会超时。

对于PG中获取和释放SpinLock是一种非常常用、且非常基础的操作,无论是更高级别的LWLOCK、Regular Lock还是存储层实现,都大量使用了SpinLock,所以其具体的实现方式与实现效率对数据库的效率还是影响很大的。PG对于SpinLock有两种实现方式,一种是基于OS层的信号量(Semaphore)来实现,另外一种方式是基于TAS硬件指令来实现的。

在代码层面,在检测到平台提供了#ifdef HAVE_SPINLOCKS的宏指令之后,则会进一步根据平台调用具体的汇编指令:

#ifdef HAVE_SPINLOCKS


......
#ifdef __i386__		/* 32-bit i386 */
.......
#endif	 /* __i386__ */
.....


#ifdef __x86_64__		/* AMD Opteron, Intel EM64T */
#define HAS_TEST_AND_SET

typedef unsigned char slock_t;

#define TAS(lock) tas(lock)

/*
 * On Intel EM64T, it's a win to use a non-locking test before the xchg proper,
 * but only when spinning.
 *
 * See also Implementing Scalable Atomic Locks for Multi-Core Intel(tm) EM64T
 * and IA32, by Michael Chynoweth and Mary R. Lee. As of this writing, it is
 * available at:
 * http://software.intel.com/en-us/articles/implementing-scalable-atomic-locks-for-multi-core-intel-em64t-and-ia32-architectures
 */
#define TAS_SPIN(lock)    (*(lock) ? 1 : TAS(lock))

static __inline__ int
tas(volatile slock_t *lock)
{
   register slock_t _res = 1;

   __asm__ __volatile__(
      "  lock         \n"
      "  xchgb  %0,%1  \n"
:     "+q"(_res), "+m"(*lock)
:
:     "memory", "cc");
   return (int) _res;
}
#define S_UNLOCK(lock)		(*((volatile slock_t *) (lock)) = 0)
#else	/* !HAVE_SPINLOCKS */


/*
 * Fake spinlock implementation using semaphores --- slow and prone
 * to fall foul of kernel limits on number of semaphores, so don't use this
 * unless you must!  The subroutines appear in spin.c.
 */
typedef int slock_t;

extern bool s_lock_free_sema(volatile slock_t *lock);
extern void s_unlock_sema(volatile slock_t *lock);
extern void s_init_lock_sema(volatile slock_t *lock);
extern int	tas_sema(volatile slock_t *lock);

#define S_LOCK_FREE(lock)	s_lock_free_sema(lock)
#define S_UNLOCK(lock)	 s_unlock_sema(lock)
#define S_INIT_LOCK(lock)	s_init_lock_sema(lock)
#define TAS(lock)	tas_sema(lock)


#endif	/* HAVE_SPINLOCKS */

如果并不提供TAS指令,则会对应调用信号量实现的s_init_lock_sema/s_lock_free_sema/s_unlock_sema来实现锁的初始化、加锁和释放锁,以最大程度提高效率。PG中用上述TAS函数实现单次的尝试加锁。

在基于TAS指令实现的SpinLock中:

我们以__x86_64__平台的指令为例:

static __inline__ int

tas(volatile slock_t *lock)

{

   register slock_t _res = 1;

   __asm__ __volatile__(

      "  lock         \n"

      "  xchgb  %0,%1  \n"

:     "+q"(_res), "+m"(*lock)

:

:     "memory", "cc");

   return (int) _res;

}

lock指令是一种常用于同步指令,用于锁总线或者缓存,以及防止指令重排序,常搭配INC, XCHG, CMPXCHG等指令使用。

而 xchgb则是原子性地交换寄存器与内存中的值。一旦当前进程获取到锁,就会原子性将锁的内容更新到寄存器中,而且将*lock中的值置1。

两个指令结合能够保证compare and set过程中的原子性和一致性,不会读到*lock过期的值,更新是个原子的过程。

在基于Semaphore实现的SpinLock中:

则是在共享内存中初始化128个信号量。

void
SpinlockSemaInit(void)
{
    PGSemaphore *spinsemas;
    int         nsemas = SpinlockSemas();
    int         i;

    /*
     * We must use ShmemAllocUnlocked(), since the spinlock protecting
     * ShmemAlloc() obviously can't be ready yet.
     */
    spinsemas = (PGSemaphore *) ShmemAllocUnlocked(SpinlockSemaSize());
    for (i = 0; i < nsemas; ++i)
        spinsemas[i] = PGSemaphoreCreate();
    SpinlockSemaArray = spinsemas;
}

基于上述的单次尝试加锁,PG进一步实现自旋加锁。

while (TAS_SPIN(lock))
{
   /* CPU-specific delay each time through the loop */
   SPIN_DELAY();
  ......
}

通过底层实现的CAS不断的判断锁是否被获取(*lock==0),保证判断过程中的原子性和一致性,然后在lock未被获取时。

轻量级锁(LWLocks)。这些锁通常用于在共享内存中对数据结构进行交互锁定。LWLocks支持独占和共享锁模式(用于对共享对象进行读/写和只读访问)。没有提供死锁检测,但在elog()恢复期间,LWLock管理器会自动释放持有的LWLocks,所以在持有LWLocks时引发错误是安全的。当没有争用锁时,获取或释放LWLocks非常快(几十条指令)。当进程必须等待LWLock时,它会在SysV信号量上阻塞,以避免占用CPU时间。等待的进程将按到达顺序获得锁。没有超时。

常规锁(Regular Locks 也称为重量级锁)。常规锁就是用户使用数据库经常接受到的锁的概念,比如在简单SELECT的时候会获取ACCESS SHARE Lock以阻塞并发的DDL,以保证查询的并发安全,常规锁具有完全的死锁检测和事务结束时的自动释放。

常见查询获取的锁类型

#define AccessShareLock         1   /* SELECT 最低级别的锁 */
#define RowShareLock            2   /* SELECT FOR UPDATE/FOR SHARE */
#define RowExclusiveLock        3   /* INSERT, UPDATE, DELETE */
#define ShareUpdateExclusiveLock 4  /* VACUUM (non-FULL),ANALYZE, CREATE INDEX
                                     * CONCURRENTLY */
#define ShareLock               5   /* CREATE INDEX (WITHOUT CONCURRENTLY) */
#define ShareRowExclusiveLock   6   /* like EXCLUSIVE MODE, but allows ROW
                                     * SHARE */
#define ExclusiveLock           7   /* blocks ROW SHARE/SELECT...FOR UPDATE */
#define AccessExclusiveLock     8   /* ALTER TABLE, DROP TABLE, VACUUM FULL,
                                     * and unqualified LOCK TABLE ,对系统表进行操作时会申请该锁*/

常规锁之间的冲突关系

Requested Lock Mode

Current Lock Mode

ACCESS SHARE

ROW SHARE

ROW EXCLUSIVE

SHARE UPDATE EXCLUSIVE

SHARE

SHARE ROW EXCLUSIVE

EXCLUSIVE

ACCESS EXCLUSIVE

ACCESS SHARE

X

ROW SHARE

X

X

ROW EXCLUSIVE

X

X

X

X

SHARE UPDATE EXCLUSIVE

X

X

X

X

X

SHARE

X

X

 X

X

X

X

SHARE ROW EXCLUSIVE

X

X

X

X

X

X

EXCLUSIVE

X

X

X

X

X

X

X

ACCESS EXCLUSIVE

X

X

X

X

X

X

X

X

SIReadLock(谓词锁)。这种锁仅在SSI隔离级别下被使用到。

在实际的数据库开发中,我们注意到获取自旋锁或轻量级锁会导致取消查询和die()中断被推迟,直到所有这些锁都被释放。然而,对于常规锁不存在这种限制。还要注意的是,在等待常规锁时,我们可以接受取消查询和die()中断,但在等待自旋锁或LW锁时我们将不接受它们。因此,在等待时间可能超过几秒钟时,不建议使用LW锁。

3.2、PostgreSQL的锁管理

在具体实现上,PostgreSQL基于以下的数据结构和方法当时实现对锁的管理:

1)LOCK结构,实例级别,存储于共享内存中,存储实例中各种锁的信息;

typedef struct LOCK
{
	/* hash key */
	LOCKTAG		tag;			/* unique identifier of lockable object */

	/* data */
	LOCKMASK	grantMask;		/* bitmask for lock types already granted */
	LOCKMASK	waitMask;		/* bitmask for lock types awaited */
	SHM_QUEUE	procLocks;		/* list of PROCLOCK objects assoc. with lock */
	PROC_QUEUE	waitProcs;		/* list of PGPROC objects waiting on lock */
	int			requested[MAX_LOCKMODES];		/* counts of requested locks */
	int			nRequested;		/* total of requested[] array */
	int			granted[MAX_LOCKMODES]; /* counts of granted locks */
	int			nGranted;		/* total of granted[] array */
	bool		holdTillEndXact;     /* flag for global deadlock detector */
} LOCK;

procLocks代表这个锁相关的procLock信息,双向链表;waitProcs代表等待这个锁的进程列表。

2)PROCLOCK结构,实例级别,存储于共享内存中,存储每个锁的持有和等待信息,也就是锁与进程之间的关系;

typedef struct PROCLOCK
{
	/* tag */
	PROCLOCKTAG tag;			/* unique identifier of proclock object */

	/* data */
	LOCKMASK	holdMask;		/* bitmask for lock types currently held */
	LOCKMASK	releaseMask;	/* bitmask for lock types to be released */
	SHM_QUEUE	lockLink;		/* list link in LOCK's list of proclocks */
	SHM_QUEUE	procLink;		/* list link in PGPROC's list of proclocks */
	int			nLocks;			/* total number of times lock is held by 
								   this process, used by resource scheduler */
	SHM_QUEUE	portalLinks;	/* list of ResPortalIncrements for this 
								   proclock, used by resource scheduler */
} PROCLOCK;

3)LOCALLOCK结构,backend级别,存储于单个进程本地,对于某种锁,存储当前进程对锁的持有和等待信息;对其他进程不可见。可以避免多次加同一种锁的时候频繁访问共享内存中的LOCK和PROCLOCK影响性能。

typedef struct LOCALLOCK
{
	/* tag */
	LOCALLOCKTAG tag;			/* unique identifier of locallock entry */

	/* data */
	LOCK	   *lock;			/* associated LOCK object, if any */
	PROCLOCK   *proclock;		/* associated PROCLOCK object, if any */
	uint32		hashcode;		/* copy of LOCKTAG's hash value */
	bool		istemptable;	/* MPP: During prepare we set this if the lock is on a temp table, to avoid MPP-1094 */
	int64		nLocks;			/* total number of times lock is held */
	int			numLockOwners;	/* # of relevant ResourceOwners */
	int			maxLockOwners;	/* allocated size of array */
	bool		holdsStrongLockCount;	/* bumped FastPathStrongRelationLocks */
	bool		lockCleared;	/* we read all sinval msgs for lock */
	LOCALLOCKOWNER *lockOwners; /* dynamically resizable array */
} LOCALLOCK;

4)几类锁表(hash table):

LockMethodLockHash,实例级别,为Lock 数据结构创建的hash表,hash key 用LOCKTAG通过hash函数生成,整个hash表会存储到共享内存中。

LockMethodProcLockHash, 实例级别,为 ProcLock 数据结构创建的hash 表,hash key用 PROCLOCKTAG 通过hash函数生成,同样会存储到共享内存中。

LockMethodLocalHash 本地锁表,为 LocalLock 数据结构的创建的hash表,LOCALLOCKTAG 通过hash 函数生成hash-key, 存储到本地。

5)LockAcquire加锁方法:

5-1)基于所申请的锁类型构建logtag,在本地锁表对应的hash表(LockMethodLocalHash)中查找,没有找到则新建一个Lock结构,存储到hash表中。

        5-2)如果当前锁对象加锁次数nLocks 大于 0,则这个锁已经被当前进程持有了,则更新计数,然后返回 LOCKACQUIRE_ALREADY_HELD,否则进入下一步。

        5-3)如果所申请的锁类型是 AccessExclusiveLock 且 锁对象是 Relation,则会尝试分配一个 transactionid 来在后续加锁成功之后写一条 WAL record。

        5-4)检查并且尝试进入快速锁模式。快速锁模式是针对会话加弱锁的时候减少对主锁表和进程锁表等共享内存信息的访问,弱锁之间是互相兼容的,所以不需要做死锁检测。以提升加锁效率。如果需要加强锁,则无法进入快速锁模式,进入下一步。

        5-5)如果申请的是强锁,则会在本地更新锁计数,并调用SetupLockInTable在主锁表中查找或者建立强锁项,如果存在锁冲突,则会调用WaitOnLock进行锁等待,在锁等待的堆栈中会进行死锁检测,即建立有向图来检查是否存在环装等待。

ProcSleep
  CheckDeadLock
    DeadLockCheckRecurse 
    FindLockCycle 

四、死锁

4.1、什么是死锁

PostgreSQL中的死锁是指两个或多个事务互相等待对方持有的锁,导致它们无法继续执行并进入无限等待状态的情况。当发生死锁时,系统必须通过中断一个或多个事务来解决死锁,并释放资源。PostgreSQL会对死锁进行自动的检测和解除以系统资源空耗。

我们举个例子:

BEGIN;
SELECT * FROM table1 WHERE id = 1;
-- 事务 T1 获取了表 table1 中 id 为 1 行的锁

BEGIN;
UPDATE table2 SET value = 2 WHERE id = 1;
-- 事务 T2 获取了表 table2 中 id 为 1 行的锁

SELECT * FROM table2 WHERE id = 1;
-- 事务 T1 尝试获取表 table2 中 id 为 1 行的锁,但该行已经被事务 T2 锁定,因此事务 T1 必须等待事务 T2 释放锁

UPDATE table1 SET value = 2 WHERE id = 1;
-- 事务 T2 尝试获取表 table1 中 id 为 1 行的锁,但该行已经被事务 T1 锁定,因此事务 T2 必须等待事务 T1 释放锁

-- 此时,事务 T1 和事务 T2 都处于等待状态,无法继续执行,导致死锁

PostgreSQL没有显式的死锁避免机制,相反它使用死锁超时机制。当一个事务被阻塞时,它将等待一个预定义的时间(指定为死锁超时时间)。如果超过这个时间仍然无法获取所需的锁,系统会中断一个事务以解除死锁。PostgreSQL通过使用等待图(wait-for graph)来检测死锁。当一个事务请求一个其他事务持有的锁时,它将被阻塞,并在等待图上创建一个边表示等待关系。当等待图中出现闭环时,系统检测到死锁并选择一个事务进行回滚以解除死锁。

4.2、如何避免死锁

为了避免死锁,以下是一些最佳实践:

  • 对数据库对象(表、行等)以一致的顺序进行访问。

  • 尽量减少事务的持有锁的时间,尽快释放锁。

  • 使用较小的事务和更短的数据库事务块。

  • 尽量避免在事务中嵌套其他事务。

  • 使用合适的事务隔离级别,避免不必要的锁冲突。

如果想要了解死锁的更多详细信息可以参考PostgreSQL官网:PostgreSQL: Documentation: 16: LOCK

五、Greenplum资源队列与锁

5.1、Greenplum中的锁

Greenplum采用了一个称为全局死锁检测(global deadlock detector,简称GDD)的机制来解决分布式环境下的死锁问题。

在Greenplum中,每个节点(segment)都有自己的本地死锁检测器,负责检测和解决本地节点上的死锁情况。然而,在分布式环境中,由于跨节点的事务操作可能会导致全局死锁,因此需要引入全局死锁检测器来处理这种情况。GDD是Greenplum的一个系统进程,负责协调各个节点上本地死锁检测器的工作。它定期从各个节点收集本地死锁检测器的信息,并分析是否存在全局死锁的情况。具体步骤如下:

1)GDD首先会进行本地死锁检测器的信息收集阶段,它会向每个节点发送请求,要求节点上的本地死锁检测器提供当前的锁信息。节点上的本地死锁检测器会将自己的锁信息发送回全局死锁检测器。

2)全局死锁检测器收集到所有节点的锁信息后,会进行死锁检测的分析阶段。它会使用死锁检测算法,分析各个节点的锁信息,判断是否存在全局死锁的情况。

3)处理阶段,如果存在全局死锁,全局死锁检测器会采取相应的措施解决死锁,通常是中断一个或多个事务,以解除死锁。

需要注意的是,全局死锁检测器的工作是一个周期性的过程,它会定期触发死锁检测,并解决任何发现的全局死锁情况。这样可以保证系统在分布式环境中的高可用性和高性能。

假设有两个节点(Node A和Node B)和两个事务(Transaction 1和Transaction 2)在Greenplum数据库中运行。它们涉及到相同的表并且需要获取互斥的锁。

  1. Transaction 1在Node A上运行,并持有一个排它锁(X锁)。

  2. Transaction 2在Node B上运行,并持有一个共享锁(S锁)。

  3. Transaction 1想要获取Node B上的一个S锁。

  4. Transaction 2想要获取Node A上的一个X锁。

在这种情况下,由于Transaction 1和Transaction 2互相等待对方的锁而形成了一个死锁。

如果没有全局死锁检测器,每个节点上的本地死锁检测器只能检测到本地的死锁情况,无法发现全局死锁。因此,事务可能会一直等待对方的锁,导致系统无法继续进行。

然而,通过全局死锁检测器的协调,它会收集到Node A和Node B上的锁信息,并分析出存在全局死锁的情况。全局死锁检测器可以中断其中一个事务(例如Transaction 1),以解除死锁并允许系统继续正常运行。

5.2、资源队列

Resource Queue是ADBPG最早的资源管理方式,支持通过SQL配置。Resource Queue支持进行四种类型的资源限制:并发限制、CPU限制、内存限制和查询计划限制。

超级用户可以通过SQL在数据库内定义多个资源队列,并设置每个资源队列的资源限制。在一个数据库中,每个资源队列可以关联一个或多个数据库用户,而每个数据库用户只能归属于单个资源队列。资源队列支持的资源限制配置如下:

配置名

配置说明

MEMORY_LIMIT

队列中所有查询所使用的的内存的量。例如,对ETL查询设置2GB的MEMORY_LIMIT, 这样每个Segment里的ETL查询最多使用2GB的内存。

ACTIVE_STATEMENTS

队列中槽位的数量;一个队列中最大可并行数,当所有槽位都占用时,新的查询必须等待。默认每个查询使用等量的内存。

PRIORITY

查询使用的相对CPU使用量,可以设置为以下级别:LOW、MEDIUM、HIGH 和MAX。默认级别是MEDIUM,查询优先权的机制会监控系统中所有正在运行的查询的CPU使用量, 并根据其优先权别来调整其CPU使用量。例如,你可以为执行资源队列设置MAX优先权,为其他 查询设置MEDIUM优先权,确保执行查询可以获得比较多的CPU资源。

MAX_COST

查询计划消耗限制。

数据库优化器会为每个查询计算消耗,如果该消耗超过了资源队列所设定的的MAX_COST的值,该查询就会被拒绝。

六、资源队列常见死锁原因与解决办法

6.1、资源队列锁与业务查询产生死锁

我们观察这个例子来了解资源队列锁与业务查询产生死锁

session1 and session2 connect server with role test1; test1 is associated with a queue with active_statements=1;
session1: accquire resource lock;
session2: vaccum full t1; accquire lock on t1;
session1: insert into t1 values ..;      --hold resource queue lock; wait for table lock on t1;
session2: select * from t1;                --hold table lock on t1; wait for resource queue lock; 
deadlock detected.

为了限制并发,资源队列需要每个查询获取数据库中的对应资源队列的锁,另外业务查询本身又需要获取表锁或者行锁,当多个session进入互相等待都无法获取锁的状态后,就会产生死锁现象。当资源队列锁与业务查询产生死锁之后,数据库的全局死锁检测器GDD就会取消掉某个session的查询,以解除死锁。

业务锁与资源队列锁混合等待导致的死锁问题如何处理:

0)重试大概率可以解决单个SQL的死锁问题;

1)增大资源队列并发上限可以缓解死锁问题;

2)换用资源组(resource group)可以解决这个问题。

6.2、资源队列锁之间产生死锁

6.2.1、单个Session内部的资源队列死锁

0:CREATE RESOURCE QUEUE rq_multi_portal WITH (active_statements = 2);
0:CREATE ROLE role_multi_portal RESOURCE QUEUE rq_multi_portal;

1:SET ROLE role_multi_portal;
2:SET ROLE role_multi_portal;

-- Scenario 1:
-- Multiple explicit cursors active in the same session with a self deadlock.
--
1:BEGIN;
1:DECLARE c1 CURSOR FOR SELECT 1;
1:DECLARE c2 CURSOR FOR SELECT 1;

-- There should be 2 active statements.
0:SELECT rsqcountlimit, rsqcountvalue FROM pg_resqueue_status WHERE rsqname = 'rq_multi_portal';
 rsqcountlimit | rsqcountvalue 
---------------+---------------
 2.0           | 2.0           
(1 row)

-- This should cause a self-deadlock and session 1 should error out, aborting
-- its transaction.
1:DECLARE c3 CURSOR FOR SELECT 1;
ERROR:  deadlock detected, locking against self

6.2.2、多个Session之间的资源队列死锁

0:CREATE RESOURCE QUEUE rq_multi_portal WITH (active_statements = 2);
0:CREATE ROLE role_multi_portal RESOURCE QUEUE rq_multi_portal;

1:SET ROLE role_multi_portal;
2:SET ROLE role_multi_portal;

--
-- Scenario 2:
-- Multiple explicit cursors active in the same session with a deadlock.
--

1:BEGIN;
1:DECLARE c1 CURSOR FOR SELECT 1;

2:BEGIN;
2:DECLARE c2 CURSOR FOR SELECT 1;

-- This should block as it will exceed the active statements limit.
1&:DECLARE c3 CURSOR FOR SELECT 1;  <waiting ...>

-- This should cause a deadlock.
2:DECLARE c4 CURSOR FOR SELECT 1;

1<:  <... completed>
ERROR:  deadlock detected
DETAIL:  Process 738539 waits for ExclusiveLock on resource queue 90366; blocked by process 738548.
Process 738548 waits for ExclusiveLock on resource queue 90366; blocked by process 738539.
HINT:  See server log for query details.

资源队列锁之间产生死锁问题如何解决:

0)重试大概率可以解决单个SQL的死锁问题;

1)增大资源队列并发上限可以缓解死锁问题;

2)对于使用JDBC extend导致隐式开启cursor的客户,可以换用simple协议,或者将fetchSize参数设为0,来规避问题。

3)另外GP中的pl/pgSQL会隐式调用cursor,也会导致占用多个resource queue或者死锁问题,可以检查相关业务逻辑。

3)换用资源组(Resource Group)可以解决这个问题。

七、总结

在前面的篇幅我们说明了为什么数据库要有锁,数据库为什么会产生死锁,PostgreSQL怎么实现的锁,Greenplum怎么实现的资源队列,死锁的现象和产生原因,从理论层面说明了Greenplum常见资源队列死锁的底层原因,并阐述了解决办法。

不知不觉已经聊了这么多,受篇幅限制,这里无法更深入的聊Greenplum资源队列锁的更多底层细节,欢迎大家随时找我讨论更多的细节。

对Greenplum资源管理相关的技术感兴趣的读者可以我之前的几篇文章,欢迎大家批评指正。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值