page的源码落在/src/backend/storage/page,page对于PostgreSQL是个什么概念?page,block,file,这些概念怎么理解?
文件是数据库的持久化存储,当然我们已经知道数据库的relation以文件的形式存在在磁盘上,无论是xxx文件还是xxx_fsm,还是xxx_vm,这是文件的概念。当relation的xxx的文件特别大,超过1G的时候,同一个relation还会分文件存储,出现xxx.1,xxx.2这种文件。Whatever,文件在,PostgreSQL数据库的信息就在。
所谓block,指的是每次加载进内存的基本单位,如果PostgreSQL需要某个relation的信息,不会是直接relation对应的磁盘文件全部读入内存,而是分block载入内存。PostgreSQL有一定的规则知道自己需要的信息或者记录或者说tuple 落在磁盘文件的那个8K block上,然后将8K block加载入local buffer,或者shared buffer,总之加载入内存。简单的说,block是磁盘上文件和内存之间加载/驱逐的基本单位。
page是个什么概念呢?page大小也是8K,就是上面提到的block,只不过,page仔细的端详了8KB的内容,分析了信息是如何组织,如何存放到8KB的block空间之内。注意每条记录内部的结构不是page关心的事情,他的视角没有这个细,我们关心的是这条记录作为一个整体如何存放到8K的page中去;当然8K的page可能存放多条记录,如何摆放到8K的page中去;当前page剩余空间还有多少;我有一条需要空间为size的记录,page是否有足够的空间容纳之;记录可能会插入,也可能会删除,page里面会不会因为删除动作,页面内部有很多的洞,或者页面碎片化,如何清理碎片,这些都是page要解决的问题。
简而言之,page,就是管理8K大小的一亩三分地,他要把多条记录(Tuple)有条不紊地组织在这8K的空间之内。
一条记录会插入到8KB的page之中,信息如何组织?自然大多数记录占用的空间不会超过8KB,以我们前边提到的friends为例:
这个friends的设计不太好,不过我们的重点不在于此,我们关心的是这长度为8192(1个Block或者说1个page)的文件,到底存放的是啥内容?
我们看到文件虽然有8K,但是实际上只有最前面的2行32字节,和最后面的64字节中包含信息,因为这个文件对应的就是我们的friends这张表,而这张表里面有Lee,Bean ,158XXXXX,Nan Jing等信息,当然了这是一条记录,或者一个Tuple,Tuple内部的组成或者layout我们不关心,但是这个16385文件作为一定记录了这些信息。我们用vbindiff查看之:
我们看到了,我们的信息Bean,Nan Jing之类的,不管是如何组织的,的确存储在表friend对应文件16385之中。这条记录如何放入8K的空间之内,头部的一些字符有是干啥的,记录的信息为何放到了现在的这个位置,这就是page要管的事情,我们下面详查之。
上图就是page的结构图,8K的空间包括一个头部Page Header,若干个Item,每个Item指向一条记录(Tuple),有些Page在初始化的时候,就page的末尾,预留出空间作为Special用,作什么用,我暂时不知,不过没关系,不影响我们理解Page。当然了,有些Page不需要Special空间,就没有预留。
好我们可以分析源码了。
INIT-page的初始化
首当其冲的是PageInit函数。我们申请了一个新的干净的8K的page,把记录插入page之前,需要将page初始化,基本就是初始化一下Page Header。:
- void
- PageInit(Page page, Size pageSize, Size specialSize)
- {
- PageHeader p = (PageHeader) page;
- specialSize = MAXALIGN(specialSize);
- Assert(pageSize == BLCKSZ);
- Assert(pageSize > specialSize + SizeOfPageHeaderData);
- /* Make sure all fields of page are zero, as well as unused space */
- MemSet(p, 0, pageSize);
- /* p->pd_flags = 0; done by above MemSet */
- p->pd_lower = SizeOfPageHeaderData;
- p->pd_upper = pageSize - specialSize;
- p->pd_special = pageSize - specialSize;
- PageSetPageSizeAndVersion(page, pageSize, PG_PAGE_LAYOUT_VERSION);
- /* p->pd_prune_xid = InvalidTransactionId; done by above MemSet */
- }
Init做的事情是
1 给special预留空间
- specialSize = MAXALIGN(specialSize); //4 字节对齐
- p->pd_special = pageSize - specialSize;
2 设置pd_lower和pg_upper
当初始化的时候,pd_lower设置为SizeOfPageHeaderData,pd_upper设置为和pd_special一样。但是注意,这个lower和upper不是固定的,随着Tuple的不断插入,lower变大,而upper不断变小。当我们每插入一条Tuple,需要在当前的lower位置再分配一个Item,记录Tuple的长度,Tuple的起始位置offset,还有flag信息。这个Page Header中的pd_lower就是记录分配下一个Item的起始位置。所以如果不断插入,lower不断增加,每增加一条Tuple,就要分配一个Item(4个字节)。同样道理,Tuple的存放位置,根据upper提供的信息,可以找到将Tuple分配到何处比较合。分配之后,pd_upper就会减少,减少Tuple的长度(对齐也考虑进去)。
3 设置 page的size 和version
- #define PageSetPageSizeAndVersion(page, size, version)
- (
- AssertMacro(((size) & 0xFF00) == (size)),
- AssertMacro(((version) & 0x00FF) == (version)),
- ((PageHeader) (page))->pd_pagesize_version = (size) | (version)
- )
下面我们比较刚初始化和插入一条记录之后的情形:
一个记录对应两个部分,就头部附近Item空间和真正记录信息的Tuple。Item记录的是Tuple在Page的offset,size等信息。
AddItem-page增加一个记录
Page是用来存放Tuple的,增加一个Tuple删除一个Tuple都是Page份内的事情,我们首先看下Page如何增加一个Tuple:
function PageAddItem是完成这件事情。因为这个接口是很通用的接口,要满足上层的各种需求,所以稍显复杂,不过整体还好。
- OffsetNumber
- PageAddItem(Page page,
- Item item,
- Size size,
- OffsetNumber offsetNumber,
- bool overwrite,
- bool is_heap)
因为Page Header的长度是固定,而紧跟其后的Item的长度也是固定的,而每增加一个Item,pd_lower就增加一个Item的长度,这样,根据pd_lower就可以算出当前的页面已经有几个Tuple了。
- #define PageGetMaxOffsetNumber(page)
- (((PageHeader) (page))->pd_lower <= SizeOfPageHeaderData ? 0 :
- ((((PageHeader) (page))->pd_lower - SizeOfPageHeaderData)
- / sizeof(ItemIdData)))
- limit = OffsetNumberNext(PageGetMaxOffsetNumber(page));
- if (OffsetNumberIsValid(offsetNumber)) //大爷型请求,值定了记录的存储位置
- {
- if (overwrite) //原有的记录删除,属于要求改写
- {
- if (offsetNumber < limit)
- {
- itemId = PageGetItemId(phdr, offsetNumber);
- if (ItemIdIsUsed(itemId) || ItemIdHasStorage(itemId))
- {
- elog(WARNING, "will not overwrite a used ItemId");
- return InvalidOffsetNumber;
- }
- }
- }
- else //新增加的客户要求这个位置,需要将原来位于这个位置的记录迁移到其他位置。
- {
- if (offsetNumber < limit)
- needshuffle = true; /* need to move existing linp's */
- }
- }
- else //普通客户
- {
-
- }
- if (OffsetNumberIsValid(offsetNumber))
- {
- ...
- }
- else
- {
- /* offsetNumber was not passed in, so find a free slot */
- /* if no free slot, we'll put it at limit (1st open slot) */
- if (PageHasFreeLinePointers(phdr))
- {
- /*
- * Look for "recyclable" (unused) ItemId. We check for no storage
- * as well, just to be paranoid --- unused items should never have
- * storage.
- */
- for (offsetNumber = 1; offsetNumber < limit; offsetNumber++)
- {
- itemId = PageGetItemId(phdr, offsetNumber);
- if (!ItemIdIsUsed(itemId) && !ItemIdHasStorage(itemId))
- break;
- }
- if (offsetNumber >= limit)
- {
- /* the hint is wrong, so reset it */
- PageClearHasFreeLinePointers(phdr);
- }
- }
- else
- {
- /* don't bother searching if hint says there's no free slot */
- offsetNumber = limit;
- }
- }
- #define PageHasFreeLinePointers(page)
- (((PageHeader) (page))->pd_flags & PD_HAS_FREE_LINES)
- if (offsetNumber == limit || needshuffle)
- lower = phdr->pd_lower + sizeof(ItemIdData); //新增一个Item
- else
- lower = phdr->pd_lower;
- alignedSize = MAXALIGN(size);
- upper = (int) phdr->pd_upper - (int) alignedSize;
- if (lower > upper)
- return InvalidOffsetNumber;
- /*
- * OK to insert the item. First, shuffle the existing pointers if needed.
- */
- itemId = PageGetItemId(phdr, offsetNumber);
- if (needshuffle)
- memmove(itemId + 1, itemId,
- (limit - offsetNumber) * sizeof(ItemIdData));
- /* set the item pointer */
- ItemIdSetNormal(itemId, upper, size);
- /* copy the item's data onto the page */
- memcpy((char *) page + upper, item, size);
- /* adjust page header */
- phdr->pd_lower = (LocationIndex) lower;
- phdr->pd_upper = (LocationIndex) upper;
- return offsetNumber;
ItemIdSetNormal把Tuple的size,offset信息记录在Item中:
- #define ItemIdSetNormal(itemId, off, len)
- (
- (itemId)->lp_flags = LP_NORMAL,
- (itemId)->lp_off = (off), //记录offset, page + off = Tuple的起始位置
- (itemId)->lp_len = (len) //记录Tuple的size 。 (page + off ,page + off + len)记录的是Tuple的信息
- )
我们下面讲述删除一条记录:
- void
- PageIndexTupleDelete(Page page, OffsetNumber offnum)
我们找到Item,就可以找到Tuple对应的offset和size:
- tup = PageGetItemId(page, offnum);
- Assert(ItemIdHasStorage(tup));
- size = ItemIdGetLength(tup);
- offset = ItemIdGetOffset(tup);
删除第二个记录之后,我们得到的Page布局如下:
我们可以看到,至少发生两次memmove
1 删除记录的Item后面的item都要往迁移,防止出现一个空洞
- nbytes = phdr->pd_lower -
- ((char *) &phdr->pd_linp[offidx + 1] - (char *) phdr);
- if (nbytes > 0)
- memmove((char *) &(phdr->pd_linp[offidx]),
- (char *) &(phdr->pd_linp[offidx + 1]),
- nbytes);
- addr = (char *) page + phdr->pd_upper;
- if (offset > phdr->pd_upper)
- memmove(addr + size, addr, (int) (offset - phdr->pd_upper));
- if (!PageIsEmpty(page))
- {
- int i;
- nline--; /* there's one less than when we started */
- for (i = 1; i <= nline; i++)
- {
- ItemId ii = PageGetItemId(phdr, i);
- Assert(ItemIdHasStorage(ii));
- if (ItemIdGetOffset(ii) <= offset) //在前面Tuple2 前面的Tuple,发生了移位,所以对应Item的lp_off要修改。
- ii->lp_off += size;
- }
- }
- Size
- PageGetFreeSpace(Page page)
- {
- int space;
- /*
- * Use signed arithmetic here so that we behave sensibly if pd_lower >
- * pd_upper.
- */
- space = (int) ((PageHeader) page)->pd_upper -
- (int) ((PageHeader) page)->pd_lower;
- if (space < (int) sizeof(ItemIdData))
- return 0;
- space -= sizeof(ItemIdData);
- return (Size) space;
- }
参考文献:
1 PostgreSQL 9.1.9 源码