当前位置: 首页 > article >正文

【PGCCC】Postgresql slru 缓存和存储

前言

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

缓存和文件的对应关系

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

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

结构体

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

文件配置

typedef struct SlruCtlData
{
	SlruShared	shared;           // 缓存
	bool		do_fsync;        // 写入数据时,是否需要fsync
	bool		(*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;
    // 计算属于文件内的第几个page
    int			rpageno = pageno % SLRU_PAGES_PER_SEGMENT;
    // BLCKSZ表示page的大小,默认8K
    int			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.kler.cn/a/383006.html

相关文章:

  • [原创](Modern C++)现代C++的字符编码迟迟得不到很好地解决(ASCII, ANSI, UNICODE通俗易懂的故事)
  • 【Python】怎么创建一个新的conda环境,并在其中安装所需的软件包
  • Linux云计算 |【第五阶段】CLOUD-DAY9
  • Java 异常处理的最佳实践
  • 1.1 关于游戏编程
  • MySQL_客户端工具建库.
  • 基于 Vue3、Vite 和 TypeScript 实现开发环境下解决跨域问题,实现前后端数据传递
  • 【人工智能】图神经网络(GNN)的原理与实现:Python与PyTorch Geometric在社交网络与化学分子建模中的应用
  • 前端八股文(二)CSS 持续更新中。。。
  • C++:继承及其相关问题
  • 国产MCU厂商第三季度取得亮眼成绩!
  • 新能源汽车与公共充电桩布局
  • 小华一级 代理商 HC32F005C6PA-TSSOP20 HC32F005系列
  • Neo4j 和 Python 初学者指南:如何使用可选关系匹配优化 Cypher 查询
  • 摄影 4.8.7.2| 惊艳的摄影和修图软件,含教程和作品欣赏。
  • Node.js简介以及安装部署 (基础介绍 一)
  • JAVA基础【第四篇】
  • 在docker中搭建redis哨兵环境
  • 编译器优化乌龙——记一次死循环不进入问题
  • Elasticsearch Interval 查询:为什么它们是真正的位置查询,以及如何从 Span 转换
  • aitrader产品规划:数据自动下载,策略,因子挖掘,实盘对接,gui界面(源码+数据)
  • 重构代码之移动方法
  • 数据挖掘(六)
  • CentOS 9 Stream 上安装 Nginx
  • 【c知道】Hadoop工作原理。
  • 前后端交互接口(三)