【PGCCC】Postgresql slru 缓存和存储

ops/2024/11/13 20:25:50/

前言

简单的 lru 缓存管理(简称 slru),用于持久化数据并且提供 lru 算法来缓存。slru 在 postgresql 存在着多处使用,比如存储事务状态的 clog 日志,就是使用 slru 来管理的。

缓存和文件的对应关系

文件的数据都是存储在 page 里,每个 page 的大小都是相同的。这些连续的 page 就构成了文件。

一个缓存对应着一个 page,所以缓存的大小和 page 的大小是相同的。

结构体

slru 需要负责文件和缓存两个方面,所以会有两个配置。

文件配置

typedef struct SlruCtlData
{SlruShared	shared;           // 缓存bool		do_fsync;        // 写入数据时,是否需要fsyncbool		(*PagePrecedes) (int, int);char		Dir[64];         // 数据存储的目录
} SlruCtlData;

缓存配置

typedef struct SlruSharedData
{LWLock	   *ControlLock;        /* 用于保存成员的锁 */int			num_slots;         /* buffer数目 */char	  **page_buffer;       /* buffer数组地址 */SlruPageStatus *page_status;   /* buffer状态数组*/bool	   *page_dirty;       /* 哪些buffer为脏页 */int		   *page_number;      /* buffer对应的page num */int		   *page_lru_count;   /* 表示buffer的新旧程度,越小表示数据越旧,越有可能被替换 */XLogRecPtr *group_lsn;   /* 缓存里的数据对应的xlog日志的位置 */int			lsn_groups_per_page;  /* 每个缓存包含的xlog日志位置的数量 */int			cur_lru_count;        /* 用于设置buffer的新旧程度 */int			latest_page_number;   /* 文件中最新的page num */int			lwlock_tranche_id;char		lwlock_tranche_name[SLRU_MAX_NAME_LENGTH];LWLockPadded *buffer_locks;   /* buffer读写锁数组 */
} SlruSharedData;

缓存的状态有下面四种,由SlruPageStatus表示

typedef enum
{SLRU_PAGE_EMPTY,			/* 空闲状态 */SLRU_PAGE_READ_IN_PROGRESS, /* 正在读取数据到缓存 */SLRU_PAGE_VALID,			/* 正常状态,里面包含了数据,没有进行读写操作 */SLRU_PAGE_WRITE_IN_PROGRESS /* 缓存正在写入文件 */
} SlruPageStatus;

设置最新访问

既然 slru 使用 lru 算法来管理缓存,那么我们需要了解下它是如何实现的。postgresql 提供了 SlruRecentlyUsed宏,来标记缓存为最近被访问了,通过它的定义就可以知道实现原理了。

// share参数是SlruSharedData类型,slotno参数指明哪个buffer
#define SlruRecentlyUsed(shared, slotno)	
do { // 获取cur_lru_count数值int		new_lru_count = (shared)->cur_lru_count; if (new_lru_count != (shared)->page_lru_count[slotno]) {// 自增cur_lru_count数值(shared)->cur_lru_count = ++new_lru_count;// 更新指定buffer的page_lru_count(shared)->page_lru_count[slotno] = new_lru_count; } 
} while (0)

SlruRecentlyUsed宏只是将全局的cur_lru_count自增,然后提高指定 buffer 的page_lru_count。这里需要注意page_lru_count属性,通过它的大小,就可以判断出缓存是否最近被访问了。page_lru_count越大,就代表着数据最近被使用过。当要替换掉长时间不在访问的 buffer 时,就选择page_lru_count值小的。

当每次读取到缓存时,就会调用SlruRecentlyUsed设置为最近访问。

挑选空闲缓存

当我们需要读取指定 page 的数据时,需要经过下图的步骤。整体思想分为三部分:

  1. 如果 page 数据已经存储在缓存中,则直接返回
  2. 如果有空闲状态的缓存,则直接返回
  3. 如果有不处于读写的缓存,则从中挑选出一个
  4. 等待缓存读写完成
    在这里插入图片描述

文件读写

文件格式

我们以pg_xact目录为例,它使用 slru 存储事务状态信息。

[root@pt-java data]# ls pg_xact/
0000 0001

这个目录存在了多个文件,这些文件称作 segment,文件名称表示 segment 的编号,由4 个十六进制数字组成。数据都是存储在page单元里,page的大小是固定的,默认 8KB。多个page组织成了一个 segment 文件,每个 segment 文件的大小也是固定的,它包含了相同数目的page。

读取数据

SlruPhysicalReadPage负责读取指定 page 的数据。它会确定数据位于哪个 segment 文件,还有所在文件的偏移量。然后打开文件读取。

/** 参数pageno指定page的编号* 参数slotno指定读取数据到哪个buffer*/
static bool SlruPhysicalReadPage(SlruCtl ctl, int pageno, int slotno)
{// SLRU_PAGES_PER_SEGMENT表示segment文件包含的page数目,默认为32// 计算属于哪个segment文件int			segno = pageno / SLRU_PAGES_PER_SEGMENT;// 计算属于文件内的第几个pageint			rpageno = pageno % SLRU_PAGES_PER_SEGMENT;// BLCKSZ表示page的大小,默认8Kint			offset = rpageno * BLCKSZ;// 生成segment文件路径,文件目录等于SlruCtl的Dir成员,文件名格式为segno的16进制数SlruFileName(ctl, path, segno);// 打开文件,并且移动读取位置,然后读取到缓存fd = OpenTransientFile(path, O_RDONLY | PG_BINARY);lseek(fd, (off_t) offset, SEEK_SET);read(fd, shared->page_buffer[slotno], BLCKSZ);
}

写入数据

SlruPhysicalWritePage负责将刷新指定缓存到磁盘。它的原理同读取数据相同,也是先定位文件的位置,然后打开文件写入。这里多了一个参数SlruFlush,用于一次性刷新所有脏页时,避免重复打开相同文件。

static bool SlruPhysicalWritePage(SlruCtl ctl, int pageno, int slotno, SlruFlush fdata); 

在写入磁盘之前,会将这个缓存里所有数据,对应的 xlog 刷新文件中。最简单的一种实现方式,就是找到 xlog 位置最大的值,然后调用XLogFlush函数,将指定位置之前的 xlog 都刷新。

读写锁

slru 在读取数据或者写入数据的时候,为了防止并发引起的错误,都采用了锁机制。它有两种锁,一种是SlruSharedData的ControlLock全局锁,另一种是每个缓存对应的读写锁。

ControlLock是读写锁LwLock,在读取数据时或者刷新缓存到文件的时候,都会获取它的写锁。它是所有缓存共享的,所以叫做全局锁。

刷新缓存

刷新缓存的流程:

  1. 获取ControlLock的全局锁
  2. 设置缓存的状态为正在写入中,并且清除脏页标记
  3. 获取缓存的写锁
  4. 释放ControlLock的全局锁,因为刷新磁盘的时间会很长,这里释放全局锁提高并发性能
  5. 刷新缓存到文件
  6. 重新获取ControlLock全局锁,因为接下来要修改缓存的状态
  7. 设置缓存的状态为有效状态
  8. 释放缓存的写锁
  9. 释放ControlLock全局锁

读取数据

读取数据到缓存的流程:

  1. 获取ControlLock的全局锁
  2. 挑选出替换的缓存,更新缓存的状态为正在读
  3. 获取缓存的写锁
  4. 释放ControlLock的全局锁,因为刷新磁盘的时间会很长,这里释放全局锁提高并发性能
  5. 从文件中读取数据到缓存
  6. 重新获取ControlLock全局锁,因为接下来要修改缓存的状态
  7. 设置缓存的状态为有效状态
  8. 释放ControlLock全局锁
  9. 释放缓存的写锁
  10. 并且设置缓存为最近访问

作者:zhmin
链接:https://zhmin.github.io/posts/postgresql-slru/
#PG证书#PG中级#postgresql培训#postgresql考试#postgresql认证


http://www.ppmy.cn/ops/132902.html

相关文章

面向对象技术简述(含设计模式)

6.9.2 面向对象技术 面向对象 对象 分类 继承 通过消息的通信 面向对象 对象 分类 继承 通过消息的通信 面向对象对象分类继承通过消息的通信其中包括: 对象 运行的实体;既包含属性/数据,又包含方法/行为/操作数据的函数;…

【Django】Clickjacking点击劫持攻击实现和防御措施

Clickjacking点击劫持 1、clickjacking攻击2、clickjacking攻击场景 1、clickjacking攻击 clickjacking攻击又称为点击劫持攻击,是一种在网页中将恶意代码等隐藏在看似无害的内容(如按钮)之下,并诱使用户点击的手段。 2、clickj…

Android CCodec Codec2 (二十)C2Buffer与Codec2Buffer

在阅读Codec2框架代码时,我们可能会发现好几个名称中都带有“buffer”的类,如MediaCodecBuffer、ABuffer、CCodecBuffers、Codec2Buffer以及C2Buffer。它们分别是什么?各自承担着什么功能?它们之间有何联系?本文将围绕…

昇思大模型平台打卡体验活动:项目1基于MindSpore实现BERT对话情绪识别

基于MindSpore实现BERT对话情绪识别 1. 模型简介 BERT(Bidirectional Encoder Representations from Transformers)是由Google于2018年末开发并发布的一种新型语言模型,基于Transformer架构中的Encoder,并且具有双向编码的特性。…

Java中的时间类型:从java.util.Date到java.time

引言 在Java编程中,时间处理是一个常见且重要的任务。无论是记录日志、处理日期、计算时间差,还是进行定时任务,我们都需要与时间类型打交道。随着Java版本的迭代,时间处理的API也在不断演进,从早期的java.util.Date到…

Android Framework 框架层主要功能类的基本介绍

Android 框架层简介 Android框架层(Android Framework)是Android操作系统中负责提供应用程序编程接口(API)的一部分,它构成了Android的中间层,位于操作系统核心与应用层之间。框架层的主要功能是为应用开发者提供各种服务和功能,以便他们能够更容易地构建Android应用。…

【360】基于springboot的志愿服务管理系统

摘 要 传统办法管理信息首先需要花费的时间比较多,其次数据出错率比较高,而且对错误的数据进行更改也比较困难,最后,检索数据费事费力。因此,在计算机上安装志愿服务管理系统软件来发挥其高效地信息处理的作用&#x…

贪心算法day3(最长递增序列问题)

目录 1.最长递增三元子序列 2.最长连续递增序列 1.最长递增三元子序列 题目链接:. - 力扣(LeetCode) 思路:我们只需要设置两个数进行比较就好。设a为nums[0],b 为一个无穷大的数,只要有比a小的数字就赋值…