【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.xdnf.cn/news/8925.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

Python数据分析-Netflix数据分析和可视化

一、研究背景 在当今时代,流媒体技术迅猛发展,如风暴般席卷全球娱乐产业,重塑了大众的娱乐消费模式。Netflix 在这一潮流中一马当先,成为全球首屈一指的在线流媒体平台。自 2007 年开启流媒体服务后,Netflix 就马不停…

数据集市是什么?有什么优势?

一、数据集市是什么? 1、数据集市的产生背景: 因为数据仓库的工作范围和成本比较巨大,技术部门必须对所有的以全企业的眼光对待任何一次决策分析,这样就变成了成本高、耗时高的大项目,而且这种集中式的数据处理方式往往…

Cross Modal Transformer: Towards Fast and Robust 3D Object Detection

代码地址 https://github.com/junjie18/CMT 1. 引言 在本文中,我们提出了Cross-Modal Transformer(CMT),这是一种简单而有效的端到端管道,用于鲁棒的3D对象检测(见图1(c)&#xf…

Oracle数据库 查看SQL执行计划的几种方法

前言 在日常的运维工作中,SQL优化是DBA的进阶技能,SQL优化的前提是要看SQL的执行计划是否正确,下面分享几种查看执行计划的方法,每一种方法都各有各的好处,可以根据特定场景选择某种方法。 一.使用AUTOTRACE查看执行…

简单介绍Nginx服务器的反向代理、负载均衡

欢迎来到“雪碧聊技术”CSDN博客! 在这里,您将踏入一个专注于Java开发技术的知识殿堂。无论您是Java编程的初学者,还是具有一定经验的开发者,相信我的博客都能为您提供宝贵的学习资源和实用技巧。作为您的技术向导,我将…

域名+服务器+Nginx+宝塔使用SSL证书配置HTTPS

前言 在我的前面文章里,有写过一篇文章 linux服务器宝塔从头部署别人可访问的网站 在这篇文章,有教学怎么使用宝塔和买的服务器的公网IP,以及教怎么打包vue和springboot去部署不用域名的网站让别人访问 那么,这篇文章将在这个…

Chromium 中chrome.webRequest扩展接口定义c++

一、chrome.webRequest 注意 :从 Manifest V3 开始,"webRequestBlocking" 权限不再适用于大多数扩展程序。以 "declarativeNetRequest" 为例,它允许使用 declarativeNetRequest API。除了 "webRequestBlocking&quo…

.NET中通过C#实现Excel与DataTable的数据互转

在.NET框架中,使用C#进行Excel数据与DataTable之间的转换是数据分析、报表生成、数据迁移等操作中的常见需求。这一过程涉及到将Excel文件中的数据读取并加载至DataTable中,以便于利用.NET提供的丰富数据处理功能进行操作,同时也包括将DataTa…

多个NVR同时管理EasyNVR多品牌NVR管理工具/设备:IP常见问题解决方案

随着视频监控技术的不断发展,NVR(网络视频录像机)已经成为现代安防系统的重要组成部分。而为了更高效地管理多个品牌的NVR设备,EasyNVR这一多品牌NVR管理工具应运而生。然而,在实际使用过程中,尤其是在多个…

虚幻引擎 CEO 谈元宇宙:发展、策略与布局

在当今科技领域,元宇宙无疑是最热门的话题之一。Epic Games 首席执行官 Tim Sweeney 对元宇宙的未来发展充满信心,他认为开放元宇宙将融合娱乐、游戏和科技产业,带来一个光明的未来。本文将深入探讨采访中的关键内容,分析元宇宙的…

支付宝与华为终端联手,移动支付即将进入“碰时代”

大家好,我是小悟。 支付宝与华为终端强强联手,达成了战略合作!这可不仅仅是个简单的合作哦,它预示着我们的移动支付方式即将迎来一场革命性的变革,正式进入“碰时代”! 支付宝,作为全球领先的…

常用机器人算法原理介绍

一、引言 随着科技的不断发展,机器人技术在各个领域得到了广泛应用。机器人算法是机器人实现各种功能的核心,它决定了机器人的行为和性能。本文将介绍几种常用的机器人算法原理,包括路径规划算法、定位算法和运动控制算法。 二、路径规划算法…

【go从零单排】迭代器(Iterators)

🌈Don’t worry , just coding! 内耗与overthinking只会削弱你的精力,虚度你的光阴,每天迈出一小步,回头时发现已经走了很远。 📗概念 在 Go 语言中,迭代器的实现通常不是通过语言内置的迭代器类型&#x…

Java 连接操作 MySQL 数据库(增删查改操作)

环境 MySQL 5.5 版本eclipseMySQL 连接驱动 mysql-connector-java-5.1.18-bin.jar mysql8.0之前的版本与之后的版本使用的jar包是不同的,在使用时也有一定的区别。这里,我的 MySQL 版本为 5.5。 准备工作 将 jar 包添加到项目中,右键项目&a…

STL---迭代器

本文来源:《C语言程序设计》第10章 理解迭代器对于理解STL框架并掌握STL的使用至关重要。 迭代器是泛化的指针,STL算法利用迭代器对存储在容器中的元素序列进行遍历,迭代器提供了访问容器中每个元素的方法。 虽然指针也是一种迭代器&#…

TSMI252012PMX-3R3MT功率电感详细解析

TSMI252012PMX-3R3MT功率电感详细解析 一、引言 在现代电子设备的不断小型化和高性能化的趋势下,功率电感作为电路中的关键元件,其性能的好坏直接影响到整个电路的稳定性和效率。TSMI252012PMX-3R3MT作为深圳市时源芯微科技有限公司(TimeSo…

Ubuntu22.04安装DataEase

看到DataEase的驾驶舱,感觉比PowerBI要好用一点,于是搭建起来玩玩。Dataease推荐的操作系统是Ubuntu22.04/Centos 7。 下载了Ubuntu22.04和DataEase 最新版本的离线安装包 一.安装ubuntu22.04 在安装的时候,没有顺手设置IP地址信息&#xff…

OpenEuler 下 Docker 安装、配置与测试实例

文章目录 前言1. 环境准备2. 下载 Docker3.配置服务文件4.配置加速器加速下载docker镜像5. 验证 Docker 安装 前言 Docker 安装大致分为包管理器安装、脚本安装、离线手动安装、容器编排工具安装、桌面版安装等,每种安装各有特点,但涉及知识面不少&…

wordpress实用功能A5资源网同款 隐藏下载框 支付框 需要登录才能查看隐藏的内容

实用功能 隐藏下载框 支付框 需要登录才能查看隐藏的内容, 个人网站防天朝申查实测有效 。 登录前,未登录: 登录后,已登录: 功能说明 该代码段的主要功能是隐藏支付框并为未 登录用户显示一条提示信息,告知他们需要…

C 语言学习-05【数组】

1、一维数组元素的操作 输入一个数&#xff0c;按原来排序的规律将它插入到一个一排列好的数组中&#xff1a; #include <stdio.h>int main() {int i, data, a[10] {2, 3, 6, 9, 11, 12, 14, 17, 19};printf("Primitive series: \n");for (i 0; i < 9; i)…