Postgresql中无锁修改buffer状态

2023-03-01 13:32:23 浏览数 (1)

概述

Postgresql中缓冲块的状态操作是非常频繁的,尤其是pin/unpin的操作。

这类操作类似于引用计数,具体由int类型记录:

  • pin:buf_state = BUF_USAGECOUNT_ONE
  • unpin:buf_state -= BUF_USAGECOUNT_ONE

这类 1的动作是concurrency unsafe的,需要使用锁或原子操作来保护。

(背景知识)

  • PG的缓存页面每一个有8kB空间;对应的每一个页面都对应一个状态描述符BufferDesc。
  • BufferDesc中记录缓存页面的信息,包括锁、引用计数、状态等等。
  • PG对引用计数为0的buffer采用时钟算法淘汰

PinBuffer早期实现:spin lock

早期PG使用spin lock实现pin的自增操作,并发性能比较差。

代码语言:javascript复制
PinBuffer
    ...
    ...
    LockBufHdr(buf);
	buf->refcount  ;
	...
	...
	if (buf->usage_count < BM_MAX_USAGE_COUNT)
		buf->usage_count  ;

	result = (buf->flags & BM_VALID) != 0;
	UnlockBufHdr(buf);

PinBuffer优化实现:CAS

使用CAS函数pg_atomic_compare_exchange_u32来做check&swap,兼顾原子性与性能(高并发readonly场景有8倍的性能提升)。

代码语言:javascript复制
		old_buf_state = pg_atomic_read_u32(&buf->state);
		for (;;)
		{
			if (old_buf_state & BM_LOCKED)
				old_buf_state = WaitBufHdrUnlocked(buf);

			buf_state = old_buf_state;

			/* increase refcount */
			buf_state  = BUF_REFCOUNT_ONE;

			/* increase usagecount unless already max */
			if (BUF_STATE_GET_USAGECOUNT(buf_state) != BM_MAX_USAGE_COUNT)
				buf_state  = BUF_USAGECOUNT_ONE;

			if (pg_atomic_compare_exchange_u32(&buf->state, &old_buf_state,
											   buf_state))
			{
				result = (buf_state & BM_VALID) != 0;
				break;
			}
		}
  1. 整体流程是:
    • 拿到state(包含引用计数)
    • 增加引用计数、
    • CAS检查拿到的state是否被并发修改
      • 修改了:old_buf_state更新为最新的state,在最新的state基础上,再去增加引用计数。
      • 没修改:old_buf_state更新为最新的state,buf->state更新为 后的state。
  2. 可以直观总结CAS的用法:
    • 参数2总会更新为参数一的值,也就是拿到共享变量最新的状态。
    • 参数1会将 检查和更新 合并为原子动作,如果检查参1==参2,则更新,更新后函数返回true。
    • 参数1会将 检查和更新 合并为原子动作,如果检查参1!=参2,说明参1被并发修改,函数返回false。

PG的CAS函数惯用法:

代码语言:javascript复制
原子函数拿到OLD
while(1)
    用拿到的OLD做一些更新,记录到NEW,但不能修改OLD
    if (pg_atomic_compare_exchange_u32(共享变量,OLD,NEW))
		break;

0 人点赞