pg内核之日志管理器(四)SUBTRANS日志

概念

pg中有嵌套事务的概念,它的基本思想是嵌套事务中存在一个事务树。从根开始,每个事务都可以建立更低层次的子事务,子事务被嵌套在父节点的控制区域之内。为此,pg引入了SUBTRANS日志,记录每个事务的父事务ID。
SUBTRANS日志通过SUBTRANS日志管理器来管理。SUBTRANS日志管理器管理着一个SLRU缓冲池,是类似于提交日志管理器的管理器,存储着每一个事务的父事务ID。它是嵌套事务实现的一个基础部分。一个主事务的父事务是INVALID的,每一个子事务都会有一个直接的父事务。遍历子事务可以很容易得由一个子事务到父事务,但是反过来是不能实现的。

SUBTRANS日志的健壮性要求和CLOG日志是完全不同的,因为需要记录的只是当前打开事务的子事务信息。所以在系统崩溃或重启时并不需要保存数据。由于在系统崩溃时不需要保存数据,因此也不需要写XLOG日志,也没有响应的REDO函数。在数据库启动时,只要使当前活跃的子事务页面全为0即可。
但是,它也会写入到磁盘日志文件中,因为SLRU缓冲池只有8个缓冲块,当全部都用满时,就需要通过SLRU算法选择一个最少用的页,先将页内数据刷入磁盘,缓冲页再用来存新的数据,而存入磁盘的数据,在数据库运行期间还是有可能用到的。数据库故障重启时由于事务都会回滚,所以启动后就不需要原先记录的pg_subtrans数据了。

SUBTRANS日志管理器相关数据结构

SUBTRANS日志中记录的是一个事务的父事务ID。由于事务ID目前是32位的,所以一个SUBTRANS日志记录大小也是32byte(4个字节)。所以一个SUBTRANS日志文件能够存储的子事务日志记录数量为: 32 * 8K /4 = 64K =2^16个。
在这里插入图片描述

既然SUBTRANS日志管理器也是基于SLRU缓冲池实现的,其日志文件也就以段文件为单位,并以段号命名,每个段有32个页。SUBTRANS日志的数据位于PGDATA/pg_subtrans目录下。 这样我们就能通过一个三元组来定位一个子事务日志记录: <segmentno, Pageno, Pageindex>

  • Segmentno: 段号
  • Pageno: 页号
  • Pageindex: 页内偏移
    给定一个事务ID,通过其三元组,就可以计算出其SUBTRANS日志存储的位置。计算公式为:
segmentno = xid /64k
pageno = xid / 2k
pageindex = xid % 2k

例如事务ID为2002,其三元组为<0,1,2>即其SUBTRANS值存放在第1个段文件的第1页的2个位置(2 * 4=8字节偏移处)。
子事务日志缓冲池是一个SLRU缓冲池,在整个数据库系统重,子事务日志缓冲池只有一个,它在共享内存中是经过注册的,其名称是SubTransCtl,它也是SLRU缓冲池控制,记录了子事务的SLRU缓冲池的数据库缓冲区在共享内存中的地址,以及对子事务日志进行写操作的同步信息,默认要求子事务日志的写操作时非同步写操作。

#define SUBTRANS_XACTS_PER_PAGE (BLCKSZ / sizeof(TransactionId)) //每页可以保存的事务ID数量,= 8K/4=2K 
#define TransactionIdToPage(xid) ((xid) / (TransactionId) SUBTRANS_XACTS_PER_PAGE) //根据事务ID获取页码
#define TransactionIdToEntry(xid) ((xid) % (TransactionId) SUBTRANS_XACTS_PER_PAGE) //页内偏移

SUBTRANS日志管理器的主要操作

SUBTRANS日志的初始化操作

SUBTRANSShmemInit

该函数初始化SUBTRANS的SLRU缓冲池,并注册缓冲页对比函数SubTransPagePrecedes。日志目录是PGDATA/subtrans。缓冲池大小大致为256K 到几M之间。

void
SUBTRANSShmemInit(void)
{
	SubTransCtl->PagePrecedes = SubTransPagePrecedes;
	SimpleLruInit(SubTransCtl, "Subtrans", NUM_SUBTRANS_BUFFERS, 0,
				  SubtransSLRULock, "pg_subtrans",
				  LWTRANCHE_SUBTRANS_BUFFER, SYNC_HANDLER_NONE); //NUM_SUBTRANS_BUFFERS=32
	SlruPagePrecedesUnitTests(SubTransCtl, SUBTRANS_XACTS_PER_PAGE);
}

SUBTRANS日志的写操作

SubTransSetParent

设置对应事务的父事务

  • 根据事务ID获取其所在的页及业内偏移
  • 申请缓冲区的锁SubtransSLRULock,以排他模式
  • 从SLRU缓冲池中找到对应的页及页内偏移的位置
  • 如果缓冲区内保存的父事务ID与要存入的一样,则不再重复写入,否则就将父事务ID写入缓冲页的对应位置,并将该页标记为脏
  • 释放锁
void
SubTransSetParent(TransactionId xid, TransactionId parent)
{
	int			pageno = TransactionIdToPage(xid); //获取页号
	int			entryno = TransactionIdToEntry(xid); //获取页内偏移
	int			slotno;
	TransactionId *ptr;
	LWLockAcquire(SubtransSLRULock, LW_EXCLUSIVE);
	slotno = SimpleLruReadPage(SubTransCtl, pageno, true, xid); //获取缓冲页的页号
	ptr = (TransactionId *) SubTransCtl->shared->page_buffer[slotno];//获取页地址
	ptr += entryno; //获取xid对应的页内地址
	if (*ptr != parent) //如果已保存的与要写入的父事务不等,需要写入新ID,否则无需再写入
	{
		Assert(*ptr == InvalidTransactionId);
		*ptr = parent; //写入事务ID
		SubTransCtl->shared->page_dirty[slotno] = true; //将缓冲页标记为脏
	}

	LWLockRelease(SubtransSLRULock);
}

SUBTRANS日志的读操作

SubTransGetParent

跟据事务ID获取其父事务ID,注意查询的事务ID不能小于当前环境中的最小事务ID,因为可能已经被冻结清理掉了。

  • 获取页号和页内偏移
  • 如果事务ID<3, 直接返回
  • 读取缓冲池中页号和页地址
  • 读取对应位置的值并返回
/*
 * Interrogate the parent of a transaction in the subtrans log.
 从子事务日志中查询指定事务的父事务ID。
 */
TransactionId
SubTransGetParent(TransactionId xid)
{
	int			pageno = TransactionIdToPage(xid); //获取页号
	int			entryno = TransactionIdToEntry(xid); //获取页内偏移
	int			slotno;
	TransactionId *ptr;
	TransactionId parent;
	Assert(TransactionIdFollowsOrEquals(xid, TransactionXmin));//查询的事务ID不能小于当前的最小事务ID
	if (!TransactionIdIsNormal(xid)) //小于3的是启动事务ID和冻结事务ID,肯定没有父事务ID
		return InvalidTransactionId;
	slotno = SimpleLruReadPage_ReadOnly(SubTransCtl, pageno, xid); //获取缓冲池中页号
	ptr = (TransactionId *) SubTransCtl->shared->page_buffer[slotno]; //获取缓冲页的地址
	ptr += entryno; 
	parent = *ptr; //获取保存的父事务ID
	LWLockRelease(SubtransSLRULock); //释放锁
	return parent;
}

SubTransGetTopmostTransaction

根据给定的事务ID,找到其最顶层的事务ID并返回。启动循环,循环遍历当前事务ID及其父事务ID,如果父事务ID为INVALIDTRANSACTIONID或者事务ID小于当前数据库最小的事务ID为,退出循环,并以此时的事务ID作为最终的结果返回。该函数可能返回一个中间子事务ID而不是真正的顶层父事务ID。这没关系,因为在实践中,我们只关心顶层父事务是否仍在运行或是否是当前快照中仍在运行的事务列表的一部分。因此,任何早于TransactionXmin的XID与其他XID一样好。

TransactionId
SubTransGetTopmostTransaction(TransactionId xid)
{
    TransactionId parentXid = xid,
                    previousXid = xid;

    /* 不能询问可能已经不存在的事务 */
    Assert(TransactionIdFollowsOrEquals(xid, TransactionXmin));

    while (TransactionIdIsValid(parentXid))
    {
        previousXid = parentXid;
        if (TransactionIdPrecedes(parentXid, TransactionXmin))
            break;
        parentXid = SubTransGetParent(parentXid);

        /*
         * 根据约定,父XID首先被分配,因此应该始终早于子XID。其他任何情况都表明数据结构被破坏,可能导致无限循环,因此退出。
         */
        if (!TransactionIdPrecedes(parentXid, previousXid))
            elog(ERROR, "pg_subtrans包含无效条目:xid %u 指向父xid %u",
                 previousXid, parentXid);
    }

    Assert(TransactionIdIsValid(previousXid));

    return previousXid;
}

SUBTRANS的创建操作

在数据库安装时会调用一次,主要是创建SUBTRANS的段文件及目录。

BootStrapSUBTRANS

  • 其他排他模式申请SubtransSLRULock锁
  • 初始化第一个缓冲页,初始化为0
  • 将缓冲页写入磁盘
  • 释放锁
void
BootStrapSUBTRANS(void)
{
	int			slotno;

	LWLockAcquire(SubtransSLRULock, LW_EXCLUSIVE); //获取缓冲区的锁
	slotno = ZeroSUBTRANSPage(0); //创建第一个页并初始化为0
	SimpleLruWritePage(SubTransCtl, slotno); //将页内容写入磁盘
	Assert(!SubTransCtl->shared->page_dirty[slotno]);
	LWLockRelease(SubtransSLRULock); //释放锁
}

SUBTRANS的启动操作

StartupSUBTRANS

数据库启动或者故障恢复时执行的操作。传入的参数是两阶段提交的最小的事务ID,如果没有的话就是当前最新的nextid, 将这个ID到当前最新ID之间的所有页清零。

  • 以排他模式申请SubtransSLRULock锁
  • 获取传入的oldestXid对应的页,获取当前最新的事务ID对应的页。
  • 循环遍历oldestXid对应的页到当前最新事务ID对应的页之间的所有的页,每个页都初始化为0.
  • 释放锁
void
StartupSUBTRANS(TransactionId oldestActiveXID)
{
	FullTransactionId nextXid;
	int			startPage;
	int			endPage;
	LWLockAcquire(SubtransSLRULock, LW_EXCLUSIVE);

	startPage = TransactionIdToPage(oldestActiveXID); //获取对应的页码
	nextXid = ShmemVariableCache->nextXid; //获取当前最新的可分配事务ID
	endPage = TransactionIdToPage(XidFromFullTransactionId(nextXid)); //获取nextid对应的页码

	while (startPage != endPage) //循环遍历他们之间所有的页
	{
		(void) ZeroSUBTRANSPage(startPage); //该页清零
		startPage++;
		/* must account for wraparound */
		if (startPage > TransactionIdToPage(MaxTransactionId)) //防止越界
			startPage = 0;
	}
	(void) ZeroSUBTRANSPage(startPage); //最终的页也清零
 
	LWLockRelease(SubtransSLRULock); //释放锁
}

SUBTRANS的checkpoint操作

就是调用SimpleLruWriteAll(SubTransCtl, true)函数将所有SUBTRANS的缓冲页数据刷入磁盘。

SUBTRANS的扩展

ExtendSUBTRANS

保证SUBTRANS有足够的空间存储新的事务ID信息。
实际上就是调用slruSelectLRUPage函数根据LRU找出一个可用的页面,清空后使用。

SUBTRANS的删除

TruncateSUBTRANS

清除所有小于传入的事务ID的段。

void
TruncateSUBTRANS(TransactionId oldestXact)
{
	int			cutoffPage;

	TransactionIdRetreat(oldestXact); //往前回退一个事务
	cutoffPage = TransactionIdToPage(oldestXact); //获取其对应的页码

	SimpleLruTruncate(SubTransCtl, cutoffPage); //删除小于当前页码的所有段文件
}

【参考】

  1. 《PostgreSQL数据库内核分析》
  2. 《Postgresql技术内幕-事务处理深度探索》
  3. pg14源码
上一篇:Apache Kafka的伸缩性探究:实现高性能、弹性扩展的关键


下一篇:Ansible