PostgreSQL B+树索引---分裂持久性

B+树索引—分裂持久性

预备知识

PostgreSQL B+树索引—分裂

PostgreSQL 基础模块—表和元组组织方式

概述

在了解了B+树的分裂流程之后,我们来讨论B+树的持久性,也就是ACID的D。与B+树持久性相关有几个非常重要的问题:

  • 在B+树分裂期间,如果发生系统故障,如何确保系统重启后,B+树依然可用。
  • B+树完成分离,并且相关事务已经提交。如果系统在B+树落盘之前发生故障,在系统重启后如何对分裂进行恢复。

应对这两个问题的方案就是WAL。所以我们下面来看看在B+树发生分裂时,需要将哪些信息写入XLOG,以确保系统重启后能正确的恢复B+树。

回顾:B+树的分裂流程

首先,我们来回顾下《B+树索引—分裂》中讲过的B+树的分裂流程。B+树分裂可以分为三个步骤:

  • 定位分裂点
  • 分裂
  • 向父节点插入

这三个步骤中,定位步骤不涉及写数据的操作,所以无需XLOG。对于分裂步骤和父节点插入步骤都涉及写数据,那么当然就应该产生相关的XLOG。现在,我们来思考一个非常重要的问题:如果省略父节点插入这个步骤,分裂完成后,B+树会出现正确性问题么?这个问题,我个人认为是B+树索引中最核心的一个问题。B+树的并发控制以及备份恢复都是围绕这个问题展开的。这个问题的答案是:省略父节点插入这个步骤,B+树不会出现正确性问题。为什么?

在这里插入图片描述

图1

我们以图1为例,图1中,block4是由block1分裂产生的。整个分裂过程完整的执行了节点分裂的三个步骤。那么现在,我们假设block1发生分裂时,没有执步骤3,那么分裂完成后,我们得到的B+树就应该如图2所示:

在这里插入图片描述

图2

此时,如果需要查询2,那么首先会定位到block1,然后发现block1的high key < 2,于是执行move right操作,移动到block4,最终找到2。所以,不难看出,在这样的流程下图2所示的B+树索引没有任何正确性问题。只是存在性能问题,由于非叶子节点无法直接定位到block4,需要先访问blok1,从而多读取一个块。

这意味着什么?这意味着步骤2和步骤3是两个独立的步骤,只用分别保证步骤2和步骤3的原子性和持久性即可。在恢复时,如果只恢复了步骤2,而没有恢复步骤3,对B+树不会造成正确性问题。所以,我们需要重点关注分裂步骤(也就是前面的步骤2)产生的XLOG。

分裂之XLOG

我们用一个用例来阐述分裂的备份恢复流程:

在这里插入图片描述

图3

block1的状态如图3所示,现在我们想block1中插入1,block1将发生分裂,分裂点位于3。经过分裂步骤之后,block1的状态应该如图4所示。

在这里插入图片描述

图4

现在的场景是,insert操作成功执行并且提交,数据库在B+树落盘之前发生了崩溃,重启之后,block1的状态依然如图3所示,现在我们需要依据XLOG从图3的状态恢复到图4的状态。

分裂阶段在XLOG相关的代码在_bt_split的line:1268~1350。依据XLOG对分裂进行恢复的函数是btree_xlog_split。为了看起来更清晰我们把写XLOG和恢复的代码对应起来看。对于将图3恢复到图4,我们需要做三件事:

  • 恢复左节点
  • 恢复右节点
  • 恢复节点间的关系

现在,我们分别来看看如何完成这三件事。

恢复左节点

左节点block1,是发生分裂的节点。也就是原始节点。在重启之后block1的状态有两种可能。

  • 与图3的block1相同

    数据库在图4 block1落盘之前就崩溃了。

  • 与图4的block1相同

    数据库在图4 block1落盘之后才崩溃了。

显然如果重启后block1的状态为图4,那么我们就不需要恢复(page lsn一定等于log lsn)。我们需要考虑的是如何将block1由图3恢复到图4的状态?为此我们需要知道两个信息:

  • 分裂点。
  • insert的数据,即new item。

有了这两个信息之后,我可以采取如下流程恢复block1:

  • 分裂一个临时块tempblock。
  • 将block1中分裂点的数据作为tempblock的high key(分裂点的数据就是右节点的第一条数据)。
  • 将block1中分裂点之前的数据,以及new item的数据写入tempblock。
  • 将tempblock中的内容拷贝到block1中。

下面,我们来看看相关代码。

构建XLOG:

//位置:nbtinsert.c line 1274
//功能:将分裂点(firstright)写入XLOG
xlrec.level = ropaque->btpo.level;
xlrec.firstright = firstright;
xlrec.newitemoff = newitemoff;

XLogRegisterData((char *) &xlrec, SizeOfBtreeSplit);
//位置:nbtinsert.c line 1298
//功能:将new item写入XLOG
/*
 * Log the new item, if it was inserted on the left page. (If it was
 * put on the right page, we don't need to explicitly WAL log it
 * because it's included with all the other items on the right page.)
 * Show the new item as belonging to the left page buffer, so that it
 * is not stored if XLogInsert decides it needs a full-page image of
 * the left page.  We store the offset anyway, though, to support
 * archive compression of these records.
 */
if (newitemonleft)
    XLogRegisterBufData(0, (char *) newitem, MAXALIGN(newitemsz));

恢复:

//位置:nbtxlog.c line 299
//功能:恢复左节点

//向tempblock写入high key(newlpage就是tempblock)
leftoff = P_HIKEY;
if (PageAddItem(newlpage, left_hikey, left_hikeysz,
                P_HIKEY, false, false) == InvalidOffsetNumber)
    elog(PANIC, "failed to add high key to left page after split");
leftoff = OffsetNumberNext(leftoff);

//将block1中分裂点之前的item以及new item写入tempblock
for (off = P_FIRSTDATAKEY(lopaque); off < xlrec->firstright; off++)
{
    ItemId		itemid;
    Size		itemsz;
    Item		item;

    /* add the new item if it was inserted on left page 
     * 写new item
     */
    if (onleft && off == xlrec->newitemoff)
    {
        if (PageAddItem(newlpage, newitem, newitemsz, leftoff,
                        false, false) == InvalidOffsetNumber)
            elog(ERROR, "failed to add new item to left page after split");
        leftoff = OffsetNumberNext(leftoff);
    }

    itemid = PageGetItemId(lpage, off);
    itemsz = ItemIdGetLength(itemid);
    item = PageGetItem(lpage, itemid);
    if (PageAddItem(newlpage, item, itemsz, leftoff,
                    false, false) == InvalidOffsetNumber)
        elog(ERROR, "failed to add old item to left page after split");
    leftoff = OffsetNumberNext(leftoff);
}

/* cope with possibility that newitem goes at the end 
 * 说明new item为左节点的左后一条item
 */
if (onleft && off == xlrec->newitemoff)
{
    if (PageAddItem(newlpage, newitem, newitemsz, leftoff,
                    false, false) == InvalidOffsetNumber)
        elog(ERROR, "failed to add new item to left page after split");
    leftoff = OffsetNumberNext(leftoff);
}

//将tempblock中的内容拷贝到block1中
PageRestoreTempPage(newlpage, lpage);

不难看出,为了恢复左节点,我只需要将非常少量的信息写入XLOG。原因在于原始节点本身具备恢复锁需要的全部数据。

恢复右节点

右节点是分裂时新创建的一个节点。所以在分裂时我们需要将右节点的信息(比如:节点编号),写入XLOG。以便在恢复时创建右节点。在重启后,右节点的状态依然有两种可能:

  • 没有右节点
  • 与图4的block2相同

显然如果是第二种情况,我们也不用恢复右节点了。我们需要思考的是如何从无到有将block2创建出来。现在思考一个问题:我们是否可以按照恢复左节点的方式来恢复右节点。即直接将图3中block1中分裂点及分裂点之后的数据写入block2。这样XLOG中只要记录了分离点即可。然而这样做是行不通的,因为重启后的block1有两种状态,如果重启后block1为图3的状态,就可以采用这种方式。但如果重启后的block1为图4状态,那么压根就不存在分裂点右边的数据。所以要恢复右节点需要如下信息:

  • 原始右节点的节点信息
  • 原始右节点的数据

其实就相当于将右节点做备份区块。然而,为了减小XLOG的体量,PostgreSQL对于这部分XLOG做了一些精细处理。我们来看看如何精细。

索引结构

要理解右节点备份恢复的精细之处,首先需要了解索引节点的内部结构,假设我们按照如下顺序向block1中写入1~5:insert 1、insert 4、insert 3、insert 2、insert 5。那么block1的内部结构如下图5所示(假设block1为最左节点,没有high key):

在这里插入图片描述

图5

其中:

  • 块头

    即块的头部记录块的基本信息

  • item区

    item是一个定长的数据,最重要的作用就是用于指向indextuple,item区的item是按照indextuple的大小升序排列。

  • 空闲区

    空闲空间

  • indextuple区

    存放实际的indextuple,indextuple是按照插入顺序逆序存放。因为indextuple是从后向前写入,item是从前向后写入。

  • special区

    存放当前节点的左、右兄弟。

其实索引节点的结构与《基础模块—表和元组组织方式》中讲到到的数据块的结构几乎一致,只有两个区别:

  • 索引节点有special区,数据块没有(因为数据块没有左右兄弟一说)。
  • 索引节点的item区是有序的,数据块的Item区是无序的。

由于item区是有序的,所以B+树的二分法是在item区进行的,插入位置、分裂点也都是指item区的位置。

分裂流程

我们再来看看分裂流程。假设我们向图5中block1插入元素2。节点发生分裂时,首先确定分裂点,假设分裂点如图6所示:
在这里插入图片描述

图6

在分裂期间,PostgreSQL遵循如下步骤:

  • 分配两个节点,一个作为分裂后的右节点,一个作为临时节点用于存放分裂后的左节点
  • 向临时节点插入high key(临时节点的high key就是分裂点的那个元素,也是右节点的第一个元素)。
  • 遍历原始节点block1,将分裂点之前的元素依次迁移到临时节点,由于当前插入的new item为2,显然也应该写入临时节点。
  • 将分裂点即分裂点之后的元素依次迁移到右节点(右节点为最右节点不需要high key)。
  • 将临时节点和右节点串链。
  • 将临时节点的内容memcpy到左节点。

分裂完成之后,分裂后的两个节点情况应该如图7所示:
在这里插入图片描述

图7
右节点的备份与恢复

仔细观察图7,经过分裂之后,左右两个节点中的indextuple被重新整理过了。现在item区的item和indextuple区的indextuple成逆序关系(item:3、1、2、2,indextuple:2、2、1、3)。这个结论非重要,前面说了那么多都是为了得到这个结论。这意味着什么?这意味着只要有indextuple我们就可以得到item区的顺序,从而重构出item区。如此,我们在写XLOG的时候,不必备份整个右节点,只需要备份右节点的indextuple区。代码如下:

构建XLOG:

//位置:nbtinsert.c line 1328
//功能:有节点的indextuple区写入XLOG
/*
 * Log the contents of the right page in the format understood by
 * _bt_restore_page(). We set lastrdata->buffer to InvalidBuffer,
 * because we're going to recreate the whole page anyway, so it should
 * never be stored by XLogInsert.
 *
 * Direct access to page is not good but faster - we should implement
 * some new func in page API.  Note we only store the tuples
 * themselves, knowing that they were inserted in item-number order
 * and so the item pointers can be reconstructed.  See comments for
 * _bt_restore_page().
 */
XLogRegisterBufData(1,
				 (char *) rightpage + ((PageHeader) rightpage)->pd_upper,
				((PageHeader) rightpage)->pd_special - ((PageHeader) rightpage)->pd_upper);

恢复:

//位置:nbtxlog.c line 34
//功能:恢复右节点
static void
_bt_restore_page(Page page, char *from, int len)
{
	IndexTupleData itupdata;
	Size		itemsz;
	char	   *end = from + len;
	Item		items[MaxIndexTuplesPerPage];
	uint16		itemsizes[MaxIndexTuplesPerPage];
	int			i;
	int			nitems;

	/*
	 * To get the items back in the original order, we add them to the page in
	 * reverse.  To figure out where one tuple ends and another begins, we
	 * have to scan them in forward order first.
	 *
	 * indextuple区的的indextuple与item区的item区是逆序,所以这里拷贝到数组里面方便后面逆序遍历
	 * 这里使用数组会比使用栈更快
	 */
	i = 0;
	while (from < end)
	{
		/* Need to copy tuple header due to alignment considerations */
		memcpy(&itupdata, from, sizeof(IndexTupleData));
		itemsz = IndexTupleDSize(itupdata);
		itemsz = MAXALIGN(itemsz);

		items[i] = (Item) from;
		itemsizes[i] = itemsz;
		i++;

		from += itemsz;
	}
	nitems = i;

    /* 逆序遍历indextuple数组,做插入操作 */
	for (i = nitems - 1; i >= 0; i--)
	{
		if (PageAddItem(page, items[i], itemsizes[i], nitems - i,
						false, false) == InvalidOffsetNumber)
			elog(PANIC, "_bt_restore_page: cannot add item to page");
		from += itemsz;
	}
}

恢复节点间关系

恢复节点间关系就比较简单了,这里直接上代码:

构建XLOG:

//位置:nbtinsert.c line 1281
//功能:将原始节点、原始节点的右节点、分裂后的右节点,这三个节点的节点信息写入XLOG
XLogRegisterBuffer(0, buf, REGBUF_STANDARD);
XLogRegisterBuffer(1, rbuf, REGBUF_WILL_INIT);
/* Log the right sibling, because we've changed its prev-pointer. */
if (!P_RIGHTMOST(ropaque))
	XLogRegisterBuffer(2, sbuf, REGBUF_STANDARD);

恢复:

//位置:nbtxlog.c line 211
//功能:从XLOG中获取原始节点、原始节点的右节点、分裂后的右节点的节点信息。
XLogRecGetBlockTag(record, 0, NULL, NULL, &leftsib);
XLogRecGetBlockTag(record, 1, NULL, NULL, &rightsib);
if (!XLogRecGetBlockTag(record, 2, NULL, NULL, &rnext))
	rnext = P_NONE;

遗留问题

对于非叶子节点分裂的特殊处理流程目前还不太清楚是怎么回事。比如下面两段代码的意义:

//位置:nbtinsert.c line 1302
/* Log left page */
if (!isleaf)
{
    /*
	 * We must also log the left page's high key, because the right
	 * page's leftmost key is suppressed on non-leaf levels.  Show it
	 * as belonging to the left page buffer, so that it is not stored
	 * if XLogInsert decides it needs a full-page image of the left
	 * page.
	 */
    itemid = PageGetItemId(origpage, P_HIKEY);
    item = (IndexTuple) PageGetItem(origpage, itemid);
    XLogRegisterBufData(0, (char *) item, MAXALIGN(IndexTupleSize(item)));
}
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值