PostgreSQL中的LWLock

PostgreSQL中的LWLock

上一篇文章介绍了PostgreSQL中的SpinLock,本文将介绍的LWLock是基于SpinLock实现的一种轻量级锁( Lightweight Lock)。

1. What is LWLock?

从PG 10.5的注释来看,LWLock主要提供对共享内存变量的互斥访问,比如Clog buffer(事务提交状态缓存)、Shared buffers(数据页缓存)、Substran buffer(子事务缓存)等等。

LWLock的数据结构定义如下:

typedef struct LWLock
{
	uint16		tranche;		/* tranche ID */
	pg_atomic_uint32 state;		/* state of exclusive/nonexclusive lockers */
	proclist_head waiters;		/* list of waiting PGPROCs */
#ifdef LOCK_DEBUG
	pg_atomic_uint32 nwaiters;	/* number of waiters */
	struct PGPROC *owner;		/* last exclusive owner of the lock */
#endif
} LWLock;

从代码上看,LWLock的互斥访问依赖于pg_atomic_uint32 state这个变量,这个变量是PG内部实现的原子变量,在上篇文章中也提过一句,PG中的原子操作也是依赖于SpinLock来实现的。tranche:相当于LWLock的id,唯一标记一个LWLock,主要用于查找某个LWLock,以观察其状态。waiters:记录等待获取LWLog的进程号。nwaiters:等待的进程数量,调试相关。owner:上一次获取LWLock的进程,调试相关。

我们知道,锁的应用场景是多并发的控制访问,也就是不同的进程/线程会并发地访问这把锁。当代的CPU架构对于访存采用的基本都是多级的构架:L1 Cache -> L2 Cache -> L3 Cache -> Memory -> 磁盘。其中访问L1 Cache速度最快,L2 Cache次之(数据不再L1中,去L2找,后面依次类推),后面的存储访问速度依次类推。关于CPU cache的更多信息,读者可以阅读这篇wiki。另外,不同的CPU拥有的自己本地的L1 Cache和L2 Cache,因此,当某个CPU上的线程更新了L1 Cache上的数据时,就需要在CPU之间进行通信,更新其它CPU上的Cache,否则就会出现“脏读”。

因此,考虑到当代计算机的这种访存体系结构,PG对LWLock的数据结构做了优化,如下述代码所示:

#define LWLOCK_PADDED_SIZE	PG_CACHE_LINE_SIZE  // 128
#define LWLOCK_MINIMAL_SIZE (sizeof(LWLock) <= 32 ? 32 : 64)

/* LWLock, padded to a full cache line size */
typedef union LWLockPadded
{
	LWLock		lock;
	char		pad[LWLOCK_PADDED_SIZE];
} LWLockPadded;

/* LWLock, minimally padded */
typedef union LWLockMinimallyPadded
{
	LWLock		lock;
	char		pad[LWLOCK_MINIMAL_SIZE];
} LWLockMinimallyPadded;

其实也没有什么高大上的优化==,就是在分配LWLock的时候,做了padding。其中,LWLockPadded保证LWLock的分配大小占满一个或者多个cache line,防止存在false sharing,而影响性能。现在PG中大部分情况下,分配LWLock时,采用的是LWLockPadded数据类型。LWLockMinimallyPadded(当前,只有shared buffer会使用此类型的LWLock),保证LWLock分配不会跨cache line,而只会在一个cache line上,因为如果跨两个cache line了,读取/更新的时候,就需要2个cache line,从而影响性能。

false sharing,是两个不同CPU上的线程t1 ,t2。其中t1改了本地cache中一个cache line上的一个数据x,而t2改了本地相同cache line上的另外一个数据y。而当t1需要访问这个cache line上的另一个数据z(也可以是x)的时候,根据当前CPU Cache失效的逻辑,需要重新从共享的内存中重新把这个cache line的数据load进来(这个过程是很慢的),而实际上这个cache line上的z并没有被任何其它线程修改过。
为什么保证LWLock不跨cache line是32或者64?因为LWLock的实际大小在16字节左右,而PG中认为Cache line大小为128字节,实际的cache line大小也为32字节/64字节/128字节,因此如果不padding,按照其本身大小对齐,可能会跨cache line的。而按32或者64字节对齐分配,则可以保证在一个cache line内。

2. Some details

在PG内部按模块(叫做named tranches)来划分LWLock,每个模块有自己独立的LWLock(一个或多个),对某个模块的共享变量的访问,则使用对应模块的LWLock。这样做的的好处:1,可以减少加锁的冲突,每个模块用自己的锁访问本模块的共享变量;2、便于追踪调试加锁的状态,这样便于开发和用户观察到那个模块加锁比较严重,从而优化代码逻辑或者业务逻辑。

查看锁等待命令:SELECT pid, wait_event_type, wait_event FROM pg_stat_activity; 其中wait_event_type的为WAIT_LWLOCK_NAMED和WAIT_LWLOCK_TRANCHE时,就是LWLock的加锁类型等待。其中WAIT_LWLOCK_NAMED为The backend is waiting for a specific named lightweight lock. Each such lock protects a particular data structure in shared memory.wait_eventwill contain the name of the lightweight lock.,一个ID只会有1个LWLock,共有45个,如ShmemIndexLock,OidGenLock等。所以在下面的子模块的LWLock中,起始ID从NUM_INDIVIDUAL_LWLOCKS(46)开始。LWLockTranche: The backend is waiting for one of a group of related lightweight locks. All locks in the group perform a similar function;wait_eventwill identify the general purpose of locks in that group. 这种ID对应的Lock通常有多个。
目前LWLock的加锁子模块有:

/*
 * Every tranche ID less than NUM_INDIVIDUAL_LWLOCKS is reserved; also,
 * we reserve additional tranche IDs for builtin tranches not included in
 * the set of individual LWLocks.  A call to LWLockNewTrancheId will never
 * return a value less than LWTRANCHE_FIRST_USER_DEFINED.
 */
typedef enum BuiltinTrancheIds
{
	LWTRANCHE_CLOG_BUFFERS = NUM_INDIVIDUAL_LWLOCKS,
	LWTRANCHE_COMMITTS_BUFFERS,
	LWTRANCHE_SUBTRANS_BUFFERS,
	LWTRANCHE_MXACTOFFSET_BUFFERS,
	LWTRANCHE_MXACTMEMBER_BUFFERS,
	LWTRANCHE_ASYNC_BUFFERS,
	LWTRANCHE_OLDSERXID_BUFFERS,
	LWTRANCHE_WAL_INSERT,
	LWTRANCHE_BUFFER_CONTENT,
	LWTRANCHE_BUFFER_IO_IN_PROGRESS,
	LWTRANCHE_REPLICATION_ORIGIN,
	LWTRANCHE_REPLICATION_SLOT_IO_IN_PROGRESS,
	LWTRANCHE_PROC,
	LWTRANCHE_BUFFER_MAPPING,
	LWTRANCHE_LOCK_MANAGER,
	LWTRANCHE_PREDICATE_LOCK_MANAGER,
	LWTRANCHE_PARALLEL_QUERY_DSA,
	LWTRANCHE_TBM,
	LWTRANCHE_FIRST_USER_DEFINED
}			BuiltinTrancheIds;

2.1 LWLock的初始化

在PG初始化shared memory和信号量时,会初始化LWLock array(CreateLWLocks)。具体做的事情就是:1. 算出LWLock需要占用的shared memory的内存空间:算出固定的和每个子模块(requested named tranches)LWLock的个数(固定在系统初始化的时候,就需要分配的LWLock有:buffer_mapping,lock_manager,predicate_lock_manager,parallel_query_dsa,tbm),每个LWLock的大小(LWLOCK_PADDED_SIZE+counter,couter为锁的计算器,用于记录有多少个加了share锁),子模块的信息占用大小。2. 分配内存空间,与cache line对齐。3. 依次对每个LWLock调用LWLockInitialize进行初始化,将LWLock的状态置为LW_FLAG_RELEASE_OK。4. 使用LWLockRegisterTranche函数注册所有的初始化了LWLock的子模块,包括系统预先定义(BuiltinTrancheIds)的和用户自定义的。

用户自定义的LWLock,需要使用 RequestNamedLWLockTranche函数,目前PG10.5的代码中,自定义了pg_stat_statements模块的LWLock。该模块用于监控SQL的执行时的统计信息。自定义的LWLock一般用于PG extension,需要在_PG_init函数中调用RequestNamedLWLockTranche,否则一旦shared memory分配完毕(上面的LWLock化阶段肯定也执行完毕了),那么自定义的锁将不会被分配出来。

/*
 * LWLockInitialize - initialize a new lwlock; it's initially unlocked
 */
void
LWLockInitialize(LWLock *lock, int tranche_id)
{
	pg_atomic_init_u32(&lock->state, LW_FLAG_RELEASE_OK);
#ifdef LOCK_DEBUG
	pg_atomic_init_u32(&lock->nwaiters, 0);
#endif
	lock->tranche = tranche_id;
	proclist_init(&lock->waiters);
}

/*
 * Register a tranche ID in the lookup table for the current process.  This
 * routine will save a pointer to the tranche name passed as an argument,
 * so the name should be allocated in a backend-lifetime context
 * (TopMemoryContext, static variable, or similar).
 */
void
LWLockRegisterTranche(int tranche_id, char *tranche_name)
{
	Assert(LWLockTrancheArray != NULL);

	if (tranche_id >= LWLockTranchesAllocated)
	{
		int			i = LWLockTranchesAllocated;
		int			j = LWLockTranchesAllocated;

		while (i <= tranche_id)
			i *= 2;

		LWLockTrancheArray = (char **)
			repalloc(LWLockTrancheArray, i * sizeof(char *));
		LWLockTranchesAllocated = i;
		while (j < LWLockTranchesAllocated)
			LWLockTrancheArray[j++] = NULL;
	}

	LWLockTrancheArray[tranche_id] = tranche_name;
}

/*
 * RequestNamedLWLockTranche
 *		Request that extra LWLocks be allocated during postmaster
 *		startup.
 *
 * This is only useful for extensions if called from the _PG_init hook
 * of a library that is loaded into the postmaster via
 * shared_preload_libraries.  Once shared memory has been allocated, calls
 * will be ignored.  (We could raise an error, but it seems better to make
 * it a no-op, so that libraries containing such calls can be reloaded if
 * needed.)
 */
void
RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)
{
	NamedLWLockTrancheRequest *request;

	if (IsUnderPostmaster || !lock_named_request_allowed)
		return;					/* too late */

	if (NamedLWLockTrancheRequestArray == NULL)
	{
		NamedLWLockTrancheRequestsAllocated = 16;
		NamedLWLockTrancheRequestArray = (NamedLWLockTrancheRequest *)
			MemoryContextAlloc(TopMemoryContext,
							   NamedLWLockTrancheRequestsAllocated
							   * sizeof(NamedLWLockTrancheRequest));
	}

	if (NamedLWLockTrancheRequests >= NamedLWLockTrancheRequestsAllocated)
	{
		int			i = NamedLWLockTrancheRequestsAllocated;

		while (i <= NamedLWLockTrancheRequests)
			i *= 2;

		NamedLWLockTrancheRequestArray = (NamedLWLockTrancheRequest *)
			repalloc(NamedLWLockTrancheRequestArray,
					 i * sizeof(NamedLWLockTrancheRequest));
		NamedLWLockTrancheRequestsAllocated = i;
	}

	request = &NamedLWLockTrancheRequestArray[NamedLWLockTrancheRequests];
	Assert(strlen(tranche_name) + 1 < NAMEDATALEN);
	StrNCpy(request->tranche_name, tranche_name, NAMEDATALEN);
	request->num_lwlocks = num_lwlocks;
	NamedLWLockTrancheRequests++;
}

2.2 LWLock的使用

LWLock的使用跟其它锁一样,主要分为加锁,放锁和等锁三个行为。

加锁:使用LWLockAcquire(LWLock *lock, LWLockMode mode)来进行加锁,其中mode可以为LW_SHARED(共享)和LW_EXCLUSIVE(排他)。加锁时,首先把需要加的锁放入等待队列,然后通过LWLock中的state状态判断是否可以加锁成功,如果可以加锁成功,使用原子操作campare and set来修改LWLock的状态,把锁从等待队列中删除。否则,需要等锁。还可以使用LWLockConditionalAcquire(LWLock *lock, LWLockMode mode)来获取锁,与LWLockAcquire不同的是,如果获取不到直接返回,不会休眠等待。LWLockAcquireOrWait函数,如果加锁不成功,会一直等待,但是如果锁变成了free之后,不会再加锁而是直接返回;当前这个函数在WALWriteLock中被使用,当一个backend需要flush WAL时,会加上WALWriteLock,然后会顺带把其它backend产生的WAL也flush了,因此,其它等锁去flush WAL的backend其实也并不需要再去flush WAL了。
放锁:使用LWLockRelease(LWLock *lock)来释放给定的一把锁。从当前proc的获取的LWLockRelease(LWLock *lock)LWLock中找到给定的锁,判断这把锁是否有其它进程在等待,如果是,则调用LWLockWakeup来唤醒等待这把锁的backend。
等锁:等锁逻辑也在LWLockAcquire函数中。当没有加上锁时,会等待一个信号量proc->sem(此时会休眠,不会消耗CPU),由于该信号量regular lock manager and ProcWaitForSignal都会使用到,当在此时获取到信号量时,并不一定是LWLockRelease发出来的,因此,如果不是LWLockRelease发出来的则需要在等信号量,如果是,则重新进行上述的加锁。
锁的mode除了LW_SHARED和LW_EXCLUSIVE外,还有一种模式为LW_WAIT_UNTIL_FREE,此模式仅通过LWLockWaitForVar(LWLock *lock, uint64 *valptr, uint64 oldval, uint64 *newval)
来使用,其场景是:如果锁被其它backend持有(模式需为LW_EXCLUSIVE。若为LW_SHARED模式不会阻塞,直接返回),那么会等待对应的backend释放锁,或者持有该锁的backend通过LWLockUpdateVar函数(不会释放锁)更新了valptr(如果更新了valptr,会将这个值赋给newval)。在PG10.5中,LWLockWaitForVar仅被使用在了XLog的插入中:在flush WAL到磁盘时,需要调用WaitXLogInsertionsToFinish将某个点的XLOG flush磁盘,此时要等待当前可能正在进行的insertion完成,因此,在这里面会调用LWLockWaitForVar,如果锁处于free状态,LWLockWaitForVar返回true,那么表示没有等锁即没有insert在进行,所以insert的位置也没有变,将上次Xlog写入的位置取出来即可。如果锁在exclusive状态,说明有backend正在进行插入,LWLockWaitForVar会或取得xlog插入后新的位置,然后返回。

3. LWLock和SpinLock比较

从上面的介绍中可以看出,LWLock中使用了原子操作(基于SpinLock实现)来进行互斥访问,因此PG中的LWLock是基于SpinLock来实现的。

LWLock提供了share和exclusive两种模式,而SpinLock只有一种模式,那就是exclusive。显然,如果某个变量需要在share模式下频繁被访问,那么使用LWLock是更好的选择。
LWLock是wait-free的,也就是说LWLock当需要等锁的时候基本不会消耗CPU资源,为此,LWLock实现了一个等待队列可以减少判断状态的原子操作,从而降低了原子操作时产生的竞争开销。而SpinLock在等锁状态时,会消耗大量的CPU资源。
基于第2点,LWLock可以应用于会被较长时间锁定的共享变量上,而SpinLock锁操作的变量,一定需要非常短的,否则会造成很大的开销。

参考资料

https://www.postgresql.org/docs/9.6/monitoring-stats.html

转自

PostgreSQL中的LWLock

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值