【PGCCC】Postgresql slru 缓存和存储

前言

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

缓存和文件的对应关系

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

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

结构体

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

文件配置

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

缓存配置

sql 复制代码
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表示

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

设置最新访问

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

sql 复制代码
// 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 存储事务状态信息。

sql 复制代码
[root@pt-java data]# ls pg_xact/
0000 0001

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

读取数据

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

sql 复制代码
/*
 * 参数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,用于一次性刷新所有脏页时,避免重复打开相同文件。

sql 复制代码
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认证

相关推荐
一 乐2 小时前
民宿|基于java的民宿推荐系统(源码+数据库+文档)
java·前端·数据库·vue.js·论文·源码
山猪打不过家猪4 小时前
(三)总结(缓存/ETag请求头)
缓存·微服务
美林数据Tempodata4 小时前
大模型驱动数据分析革新:美林数据智能问数解决方案破局传统 BI 痛点
数据库·人工智能·数据分析·大模型·智能问数
野槐4 小时前
node.js连接mysql写接口(一)
数据库·mysql
Zzzone6835 小时前
PostgreSQL日常维护
数据库·postgresql
chxii5 小时前
1.13使用 Node.js 操作 SQLite
数据库·sqlite·node.js
冰刀画的圈5 小时前
修改Oracle编码
数据库·oracle
这个胖子不太裤5 小时前
Django(自用)
数据库·django·sqlite
麻辣清汤5 小时前
MySQL 索引类型及其必要性与优点
数据库·mysql
2501_915374356 小时前
Neo4j 图数据库安装教程(2024最新版)—— Windows / Linux / macOS 全平台指南
数据库·windows·neo4j