【PGCCC】postgresql 缓存池并发设计

前言

postgresql 对于缓存池的并发控制,比较复杂。下面通过一步步的优化,来讲述 postgresql 的设计思路。它为了提升锁的性能,大量使用了 CAS 操作,只有在操作比较慢的时候,才会加入读写锁。在继续阅读之前,需要熟悉它的存储设计,可以参考这篇文章 {% post_link postgresql-storage-architecture postgresql 存储设计 %} 。

设计思路推演

首先考虑下单线程的情况,我们先要查找缓存池是否已经包含了要读取的page,如果包含了则直接返回该 buffer。如果没有,需要从磁盘加载到缓存池中,然后返回。下面是通过一步步的加锁,来实现并发的控制。

Hash 表锁

hash 表保存了 page 到 buffer 的对应关系,很明显所有的读取都需要从 hash 表查找开始,我们可以从 控制 hash 表的锁来开始。假设现在只有 hash 分区锁,我们写的程序应该是这样的

// 获取分区 id,page_number 表示要获取的 page 标识
partition_id = get_hash_parition(page_number);
// 对此分区加读锁
lock_partition_read(hash_table, partition_id);
// 查找该 page 是否存在缓存
buffer_id, found = lookup(hash_table, page_number);if (found) {// 如果找到该缓存,则返回ublock(hash_table, partition);return buffer_id;
}// 如果没有对应的缓存,则需要从缓存池中,找到替换的位置
free_buffer_id = find_victim_buffer();
// 获取旧有位置的缓存对应哪个 page number
free_page_number = get_page_number(free_buffer_id);
// 对其所在的分区加写锁
free_partition_id = get_hash_parition(free_page_number);
lock_partition_write(hash_table, free_partition_id);
lock_partition_write(hash_table, partition_id);
// 删除旧有的缓存
delete(hash_table, free_page_number);
// 从磁盘读取数据到缓存,并且添加到 hash 表
read_from_disk(page_number, free_buffer_id);
insert(hash_table, pageNumber, free_buffer_id);// 释放分区锁
unlock_partition(hash_table, free_buffer_id);
unlock_partition(hash_table, partition_id);
return free_buffer_id;

Pin Buffer

其中 find_victim_buffer 函数负责挑选出可被替换的 buffer,它的原理是找到 pin count 等于 0 的 buffer。所以这里面会有个问题,当我们查找到数据后,需要及时的 pin 操作,不然有可能会被替换掉,造成读取的数据不是想要的 page number。

// 获取分区 id,page_number 表示要获取的 page 标识
partition_id = get_hash_parition(page_number);
// 对此分区加读锁
lock_partition_read(hash_table, partition_id);
// 查找该 page 是否存在缓存
buffer_id, found = lookup(hash_table, page_number);if (found) {// 需要及时 pin 操作pin_buffer(buffer_id)ublock(hash_table, partition);return buffer_id;
}// 如果没有对应的缓存,则需要从缓存池中,找到替换的位置
free_buffer_id = find_victim_buffer();
// 获取旧有位置的缓存对应哪个 page number
free_page_number = get_page_number(free_buffer_id);
// 及时 pin 操作
pin_buffer(free_buffer_id);// 对其所在的分区加写锁
free_partition_id = get_hash_parition(free_page_number);
lock_partition_write(hash_table, free_partition_id);
lock_partition_write(hash_table, partition_id);
// 删除旧有的缓存
delete(hash_table, free_page_number);
// 从磁盘读取数据到缓存,并且添加到 hash 表
read_from_disk(page_number, free_buffer_id);
insert(hash_table, pageNumber, free_buffer_id);// 释放分区锁
unlock_partition(hash_table, free_buffer_id);
unlock_partition(hash_table, partition_id);
return free_buffer_id;

查找空闲Buffer优化

我们观察上述代码,如果我们没有找到对应的 buffer,那么需要从buffer数组中挑选出一个合适的,这个步骤是很花费时间的。所以会造成长时间的加锁,并且hash表的一个分区会包含了多个 buffer,这样会造成性能影响。所以 在执行这一步之前,我们可以将分区锁释放。不过这样会遇到一个问题,如下所示

// 获取分区 id,page_number 表示要获取的 page 标识
partition_id = get_hash_parition(page_number);
// 对此分区加读锁
lock_partition_read(hash_table, partition_id);
// 查找该 page 是否存在缓存
buffer_id, found = lookup(hash_table, page_number);if (found) {// 需要及时 pin 操作pin_buffer(buffer_id)ublock(hash_table, partition);return buffer_id;
}// 释放锁,防止长时间占住分区锁
unlock_partition(hash_table, partition_id);// 如果没有对应的缓存,则需要从缓存池中,找到替换的位置
free_buffer_id = find_victim_buffer();
// 获取旧有位置的缓存对应哪个 page number
free_page_number = get_page_number(free_buffer_id);
// 及时 pin 操作
pin_buffer(free_buffer_id);// 对其所在的分区加写锁
free_partition_id = get_hash_parition(free_page_number);// 步骤1
lock_partition_write(hash_table, free_partition_id);
lock_partition_write(hash_table, partition_id);// 删除旧有的缓存
delete(hash_table, free_page_number);
// 从磁盘读取数据到缓存,并且添加到 hash 表
read_from_disk(page_number, free_buffer_id);
insert(hash_table, pageNumber, free_buffer_id);// 释放分区锁
unlock_partition(hash_table, free_buffer_id);
unlock_partition(hash_table, partition_id);
return free_buffer_id;

避免重复读取

我们假设有两个线程都跑到了 步骤1,那么就会产生两个 buffer 都是保存了同一份 Page 的数据。为了避免这一情况,我们在执行步骤1之后,需要检查一下。

// 获取分区 id,page_number 表示要获取的 page 标识
partition_id = get_hash_parition(page_number);
// 对此分区加读锁
lock_partition_read(hash_table, partition_id);
// 查找该 page 是否存在缓存
buffer_id, found = lookup(hash_table, page_number);if (found) {// 需要及时 pin 操作pin_buffer(buffer_id)ublock(hash_table, partition);return buffer_id;
}// 释放锁,防止长时间占住分区锁
unlock_partition(hash_table, partition_id);// 如果没有对应的缓存,则需要从缓存池中,找到替换的位置
free_buffer_id = find_victim_buffer();
// 获取旧有位置的缓存对应哪个 page number
free_page_number = get_page_number(free_buffer_id);
// 及时 pin 操作
pin_buffer(free_buffer_id);// 对其所在的分区加写锁
free_partition_id = get_hash_parition(free_page_number);// 步骤1
lock_partition_write(hash_table, free_partition_id);
lock_partition_write(hash_table, partition_id);// 增加查看是否有别的进程已经在读取磁盘数据了
buffer_id, found = lookup(hash_table, page_number);
if (found) {return buffer_id;
}// 删除旧有的缓存
delete(hash_table, free_page_number);
// 从磁盘读取数据到缓存,并且添加到 hash 表
read_from_disk(page_number, free_buffer_id);
insert(hash_table, pageNumber, free_buffer_id);// 释放分区锁
unlock_partition(hash_table, free_buffer_id);
unlock_partition(hash_table, partition_id);
return free_buffer_id;

磁盘读取优化

同样从磁盘读取数据的操作也会很花费时间,为了避免长时间占用锁,我们需要在执行之前释放相关的锁。如果要实现这一步,需要设置一个 valid 标记位,表示数据是否成功的被磁盘读取。所以我们在哈希表中找到缓存后,还需要分辨数据是否完成磁盘读取,并且我们还需要额外的锁(称为 io_lock),来保证同步等待。

// 获取分区 id,page_number 表示要获取的 page 标识
partition_id = get_hash_parition(page_number);
// 对此分区加读锁
lock_partition_read(hash_table, partition_id);
// 查找该 page 是否存在缓存
buffer_id, found = lookup(hash_table, page_number);if (found) {pin_buffer(buffer_id);unlock_partition(hash_table, partition_id);while(! is_valid(buffer_id)) {wait_io_lock(buffer_id)}return buffer_id;
}// 释放锁,防止长时间占住分区锁
unlock_partition(hash_table, partition_id);// 如果没有对应的缓存,则需要从缓存池中,找到替换的位置
free_buffer_id = find_victim_buffer();
// 获取旧有位置的缓存对应哪个 page number
free_page_number = get_page_number(free_buffer_id);
// 及时 pin 操作
pin_buffer(free_buffer_id);// 对其所在的分区加写锁
free_partition_id = get_hash_parition(free_page_number);// 步骤1
lock_partition_write(hash_table, free_partition_id);
lock_partition_write(hash_table, partition_id);// 增加查看是否有别的进程已经在读取磁盘数据了
buffer_id, found = lookup(hash_table, page_number);
if (found) {return buffer_id;
}// 删除旧有的缓存
delete(hash_table, free_page_number);// 设置该缓存无效
set_buffer_invalid(free_buffer_id);// 并且添加到 hash 表
insert(hash_table, pageNumber, free_buffer_id);// 释放分区锁
unlock_partition(hash_table, free_buffer_id);
unlock_partition(hash_table, partition_id);// 加锁
lock_io(free_buffer_id);
// 如果该缓存已经成功读取完了,那么直接返回。
while (! is_valid(free_buffer_id)) {// 否则从磁盘读取read_from_disk(page_number, free_buffer_id);set_buffer_valid(free_buffer_id);
}
unlock_io(free_buffer_id)
return free_buffer_id;

Valid标记位

现在基本的流程确定好了,不过 postgresql 认为加载磁盘是很慢的操作,是可以被终止的。所以当检测到一个 buffer 为 invalid 时,可能对应着两种情况,一种是其它进程正在进行读取,一种是之前的读取被终止了。所以当发现 buffer invalid 的时候,都需要主动去磁盘读取。但是这样会造成并发重复读取,所以还需要提供一个锁,专门负责这一块的同步,称为 io_lock。

// 获取分区 id,page_number 表示要获取的 page 标识
partition_id = get_hash_parition(page_number);
// 对此分区加读锁
lock_partition_read(hash_table, partition_id);
// 查找该 page 是否存在缓存
buffer_id, found = lookup(hash_table, page_number);if (found) {pin_buffer(buffer_id);unlock_partition(hash_table, partition_id);// 加锁lock_io(free_buffer_id);// 如果该缓存已经成功读取完了,那么直接返回。while (! is_valid(free_buffer_id)) {// 否则从磁盘读取read_from_disk(page_number, free_buffer_id);set_buffer_valid(free_buffer_id);}unlock_io(free_buffer_id)return buffer_id;
}// 释放锁,防止长时间占住分区锁
unlock_partition(hash_table, partition_id);// 如果没有对应的缓存,则需要从缓存池中,找到替换的位置
free_buffer_id = find_victim_buffer();
// 获取旧有位置的缓存对应哪个 page number
free_page_number = get_page_number(free_buffer_id);
// 及时 pin 操作
pin_buffer(free_buffer_id);// 对其所在的分区加写锁
free_partition_id = get_hash_parition(free_page_number);// 步骤1
lock_partition_write(hash_table, free_partition_id);
lock_partition_write(hash_table, partition_id);// 增加查看是否有别的进程已经在读取磁盘数据了
buffer_id, found = lookup(hash_table, page_number);
if (found) {return buffer_id;
}// 删除旧有的缓存
delete(hash_table, free_page_number);// 设置该缓存无效
set_buffer_invalid(free_buffer_id);// 并且添加到 hash 表
insert(hash_table, pageNumber, free_buffer_id);// 释放分区锁
unlock_partition(hash_table, free_buffer_id);
unlock_partition(hash_table, partition_id);// 加锁
lock_io(free_buffer_id);
// 如果该缓存已经成功读取完了,那么直接返回。
while (! is_valid(free_buffer_id)) {// 否则从磁盘读取read_from_disk(page_number, free_buffer_id);set_buffer_valid(free_buffer_id);
}
unlock_io(free_buffer_id)
return free_buffer_id;

锁实现

上面一步步推导出了最后的并发流程,接下来依次介绍这些锁的实现。

hash 表分区锁

postgresql 将 hash 表分成多个区,每个区都对应了一个读写锁。

BufferTag	newTag;  // 表示要查找的 page
newHash = BufTableHashCode(&newTag);  // 计算 hash 值
newPartitionLock = BufMappingPartitionLock(newHash);   // 根据hash值找到对应的分区锁
LWLockAcquire(newPartitionLock, LW_SHARED); // 加读锁

BufMappingPartitionLock函数分为两步,首先是找到对应的分区。然后是找到该分区对应的锁。

// 这里只是简单的取余操作,找到对应的分区。NUM_BUFFER_PARTITIONS 表示分区数目
#define BufTableHashPartition(hashcode)  ((hashcode) % NUM_BUFFER_PARTITIONS) // postgresql 分配了一个大的lwlock 数组,分区锁占用了其中一部分,从 BUFFER_MAPPING_LWLOCK_OFFSET 位置开始
#define BufMappingPartitionLock(hashcode) 	(&MainLWLockArray[BUFFER_MAPPING_LWLOCK_OFFSET + BufTableHashPartition(hashcode)].lock)

buffer header 锁

BufferDesc 包含了buffer的头部信息,也包含了相关的锁

typedef struct BufferDesc
{BufferTag	tag;			// 缓存哪个文件的blockint			buf_id;			// buffer idpg_atomic_uint32 state;		// 状态值int			wait_backend_pid;	/* backend PID of pin-count waiter */int			freeNext;		// 指向下个空闲位置LWLock		content_lock;	// 读写锁,用来控制内容的访问
} BufferDesc;

state字段是一个 atomic 类型的值,它的更新非常频繁。它有一个标记位BM_LOCKED,作为锁的实现。postgresql 使用了自旋 + CAS 操作来实现乐观锁。

uint32 LockBufHdr(BufferDesc *desc)
{// 用于自适应控制自旋间隙SpinDelayStatus delayStatus;uint32		old_buf_state;// 初始化init_local_spin_delay(&delayStatus);while (true){// CAS 操作,来设置BM_LOCKED标记位old_buf_state = pg_atomic_fetch_or_u32(&desc->state, BM_LOCKED);// 如果之前BM_LOCKED标记位没有设置,说明之前没有人加锁,现在我们已经成功加锁了。if (!(old_buf_state & BM_LOCKED))break;// 如果没有加锁成功,则判断是否需要延迟perform_spin_delay(&delayStatus);}finish_spin_delay(&delayStatus);return old_buf_state | BM_LOCKED;
}

释放锁就很简单,只是简单的取消标记位BM_LOCKED

#define UnlockBufHdr(desc, s)	\do {	\pg_write_barrier(); \pg_atomic_write_u32(&(desc)->state, (s) & (~BM_LOCKED)); \} while (0)

Pin Buffer

pin 操作也是基于 CAS 操作,state字段中保存了 pin count。不过它并没有使用BM_LOCKED标记位,因为 pin 操作非常频繁,postgresql 做了进一步的优化。它分为两种场景,一种是pin操作之前没有获取到锁,一种是已经获取到了锁。

// 调用之前没有获取到锁
static bool PinBuffer(BufferDesc *buf, BufferAccessStrategy strategy)
{Buffer		b = BufferDescriptorGetBuffer(buf);old_buf_state = pg_atomic_read_u32(&buf->state);for (;;){// 如果之前有锁,那么需要等待锁释放BM_LOCKED标记位if (old_buf_state & BM_LOCKED)// 注意到这里返回的值,肯定不会包含old_buf_state = WaitBufHdrUnlocked(buf);// 计算出新值buf_state = old_buf_state;buf_state += BUF_REFCOUNT_ONE;// 调用CAS操作,需要注意到 old_buf_state 不会包含BM_LOCKED标记位,所以永远不会更新带有BM_LOCKED标记位的值if (pg_atomic_compare_exchange_u32(&buf->state, &old_buf_state, buf_state)) {break;}}
}

第二种情况适用于获取到了锁,这种情况只是简单的修改state值就行。

// 调用之前已经获取到了锁
static void PinBuffer_Locked(BufferDesc *buf)
{uint32		buf_state;// 获取state值buf_state = pg_atomic_read_u32(&buf->state);Assert(buf_state & BM_LOCKED);// 修改state值buf_state += BUF_REFCOUNT_ONE;UnlockBufHdr(buf, buf_state);
}

pin 操作只是保证了对应的 buffer 不会被替换掉。如果需要保证 buffer 里面的数据并发修改,则需要加入 buffer 的读写锁。buffer 读写锁是BufferDesc的content_lock字段,操作比较简单,这里不再详述。

Buffer IO 锁

为了控制 IO 的同步,postgresql 使用了 buffer_io_lock 锁来控制。因为 IO 操作非常耗时,所以这里就不在使用了乐观锁。

  // IO 锁数组,对应了每个 Buffer
LWLockMinimallyPadded *BufferIOLWLockArray = NULL;
// 直接返回对应位置的IO锁
#define BufferDescriptorGetIOLock(bdesc)  (&(BufferIOLWLockArray[(bdesc)->buf_id]).lock)

总结

postgresql 对于缓存并发控制是非常精细的,它尽量将耗时的操作放在锁外面执行,很大的提高了并发性。还有进一步将不耗时的操作,通过乐观锁的方式实现。这些设计思想值得细细品味,对我们的程序设计会有很大帮助。
作者:zhmin
链接:https://zhmin.github.io/posts/postgresql-buffer-concurrent/
#PG证书#PG考试#postgresql培训#postgresql考试#postgresql认证

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/467139.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

计算机的错误计算(一百四十八)

摘要 本节探讨 MATLAB 中 附近数的正割函数与 附近数的余割函数的计算精度问题。 例1. 已知 计算 直接贴图吧: 另外,16位的正确值分别为 0.4105556037464873e9、0.3670813182326778e13、-0.2549029285657875e8 与 -0.1248777628817462e12&am…

《XGBoost算法的原理推导》12-14决策树复杂度的正则化项 公式解析

本文是将文章《XGBoost算法的原理推导》中的公式单独拿出来做一个详细的解析,便于初学者更好的理解。 我们定义一颗树的复杂度 Ω Ω Ω,它由两部分组成: 叶子结点的数量;叶子结点权重向量的 L 2 L2 L2范数; 公式(…

算法练习:1004. 最大连续1的个数 III

题目链接:1004. 最大连续1的个数 III。 题目要求,给定一个数组,这个数组里面只有0或1,然后计算有多少个连续的1的最大长度,同时给了一个条件就是,可以把k个0变成1,然后来计算长度。 暴力解法&a…

CCS下载安装(以12.3.0版本为例)

Code Composer Studio 是一个集成开发环境 (IDE),简称CCS软件。支持 TI 的微控制器和嵌入式处理器产品的开发。Code Composer Studio 包含一整套用于开发和调试嵌入式应用程序的工具。 CCS9.3.0及以上版本不需要License文件,但是CCS旧版本比如CCS5.5.0需…

串口接收,不定长数据接收

###1.CUBE-MX配置串口 2.我采用串口中断接收,打开中断接口 3.时钟同样8倍频,1分频,使用内部时钟 打开串口中断 main() { __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); // 启用空闲中断__HAL_UART_ENABLE_IT(&huart1, UART_IT_R…

密码学知识点整理二:常见的加密算法

常用的加密算法包括对称加密算法、非对称加密算法和散列算法。 对称加密算法 AES:高级加密标准,是目前使用最广泛的对称加密算法之一,支持多种密钥长度(128位、192位、256位),安全性高,加密效率…

MongoDB Shell 基本命令(三)聚合管道

管道含义 类似Linux中的管道,前一个命令的输出作为后一个命令的输入。 显示网络连接、路由表和网络接口统计信息 netstat -ano -netstat:network statistics 网络统计 -a:显示所有连接和监听端口,包括所有活动的TCP和UDP连接。 -n:以数字形式显示地址…

OpenCV相机标定与3D重建(1)概述

操作系统:ubuntu22.04 OpenCV版本:OpenCV4.9 IDE:Visual Studio Code 编程语言:C11 算法描述 本节中的函数使用所谓的针孔相机模型。通过使用透视变换将场景中的3D点 P w P_w Pw​ 投影到图像平面上,从而获得场景的视图&#x…

Docker部署Oracle 11g

1,拉取镜像: sudo docker pull registry.cn-hangzhou.aliyuncs.com/helowin/oracle_11gsudo docker images 2,启动一个临时容器,用于拷贝数据库文件,挂载到宿主主机,使数据持久化: sudo docke…

【Linux系统】—— 基本指令(二)

【Linux系统】—— 基本指令(二) 1 「alias」命令1.1 「ll」命令1.2 「alias」命令 2 「rmdir」指令与「rm」指令2.1 「rmdir」2.2 「rm」2.2.1 「rm」 删除普通文件2.2.2 「rm」 删除目录2.2.3 『 * 』 通配符 3 「man」 指令4 「cp」 指令4.1 拷贝普通…

从单层到 MVC,再到 DDD:架构演进的思考与实践

引言 在日常开发中,我们之前工作中经常接手的大多数都是传统 MVC 架构体系的项目。然而,随着现在分布式和微服务架构的普及,越来越多的项目开始重构、拆分,传统的 MVC 架构也逐渐向 DDD 架构演进。为什么需要将传统架构重构为 DD…

贝式计算的 AI4S 观察:使用机器学习对世界进行感知与推演,最大魅力在于横向扩展的有效性

「传统研究方法高度依赖于科研人员自身的特征和问题定义能力,通常采用小数据,在泛化能力和拓展能力上存疑。而 AI 研究方法则需要引入大规模、高质量数据,并采用机器学习进行特征抽取,这使得产生的科研结果在真实世界的问题中非常…

[产品管理-58]:安索夫矩阵矩阵帮助创业者确定研发出来的产品在市场中定位策略

目录 一、提出背景 二、核心思想与结构 三、应用背景与领域 四、实践案例 安索夫矩阵(Ansoff Matrix),也被称为产品/市场方格或成长矢量矩阵,其应用背景可以从以下几个方面进行详细阐述: 一、提出背景 安索夫矩阵…

安当ASP系统:适合中小企业的轻量级Radius认证服务器

安当ASP(Authentication Service Platform)身份认证系统是一款功能强大的身份认证服务平台,特别适用于中小企业。其中,简约型Radius认证服务器是安当ASP系统中的一个重要组成部分。以下是对该系统的详细介绍: 一、主要…

uniapp配置h5路由模式为history时404

为了不让URL中出现#,让uniapp项目配置h5路由模式为hisory 然而本地好好的,放到服务器上却404了。 解决方法是给nginx配置一个伪静态: location /xxx-html/ {alias /home/nginx_web/xxx_new_html/;try_files $uri $uri/ /xxx-html/index.ht…

Go-HTTP框架设计实现概述

1.再谈HTTP协议 第一个大规模使用:HTTP0.9 三十多年了 HTTP:超文本传输协议(Hypertext Transfer Protocal) 为什么是超文本:因为图片、音乐、视频是文本的扩充 为什么需要协议:约定俗称的规则(像说话&…

使用Matlab建立决策树

综述 除了神经网络模型以外,树模型及基于树的集成学习模型是较为常用的效果较好的预测模型。我们以下先构建一个决策树模型。 决策树算法的优点如下:1、 决策树易于理解和实现,用户在学习过程中不需要了解过多的背景知识,其能够…

【JavaSE】(3)数组

目录 一、数组的定义和初始化 1. 什么是数组 2. 数组的定义 3. 数组的初始化 4. 操作数组的工具包 二、数组的使用 三、引用类型 1. JVM内存分布 2. 引用变量 3. 默认值 null 四、二维数组 1. 二维数组的定义和初始化 2. 不规则的二维数组 一、数组的定义和初始化…

uniapp—android原生插件开发(3Android真机调试)

本篇文章从实战角度出发,将UniApp集成新大陆PDA设备RFID的全过程分为四部曲,涵盖环境搭建、插件开发、AAR打包、项目引入和功能调试。通过这份教程,轻松应对安卓原生插件开发与打包需求! 一、打包uniapp资源包: 打包…

【 AI写作鹅-注册安全分析报告-无验证方式导致安全隐患】

前言 由于网站注册入口容易被黑客攻击,存在如下安全问题: 1. 暴力破解密码,造成用户信息泄露 2. 短信盗刷的安全问题,影响业务及导致用户投诉 3. 带来经济损失,尤其是后付费客户,风险巨大,造…