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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章