转载:http://my.oschina.net/u/233784/blog/146389
前面几篇博客分析了shared buffer,从shared buffer到磁盘文件的映射,到shared buffer的分配和替换,再到如何测量shared buffer的性能情况,配置是否合理,基本把shared buffer大概介绍了下,这篇博客主要分析page。
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)
- {
- PageHeaderp=(PageHeader)page;
-
- specialSize=MAXALIGN(specialSize);
-
- Assert(pageSize=BLCKSZ;
- Assert>specialSize+SizeOfPageHeaderData;
-
- /*Make sure all fields of page are zerospace*/
- MemSet(ppageSize*p->pd_flags=0;done by above MemSet/
- p>pd_lower=SizeOfPageHeaderData;
- p>pd_upper=pageSize-specialSize>pd_special;
- PageSetPageSizeAndVersion(page;
- >pd_prune_xid=InvalidTransactionId/
- }
Init做的事情是
1 给special预留空间
- specialSize; //4 字节对齐
- p;
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 PageSetPageSizeAndVersionversion)
- (
- AssertMacro((size&0xFF00
- AssertMacro(version&0x00FF
- >pd_pagesize_version|)
下面我们比较刚初始化和插入一条记录之后的情形:
一个记录对应两个部分,就头部附近Item空间和真正记录信息的Tuple。Item记录的是Tuple在Page的offset,size等信息。
AddItem-page增加一个记录
Page是用来存放Tuple的,增加一个Tuple删除一个Tuple都是Page份内的事情,我们首先看下Page如何增加一个Tuple:
functionPageAddItem是完成这件事情。因为这个接口是很通用的接口,要满足上层的各种需求,所以稍显复杂,不过整体还好。
- OffsetNumber
- PageAddItem
- Item item
- Size size
- OffsetNumber offsetNumber
- bool overwrite
- bool is_heap)
因为Page Header的长度是固定,而紧跟其后的Item的长度也是固定的,而每增加一个Item,pd_lower就增加一个Item的长度,这样,根据pd_lower就可以算出当前的页面已经有几个Tuple了。
- #define PageGetMaxOffsetNumber<?0:
- -SizeOfPageHeaderData)
- /sizeof(ItemIdData)
- limit=OffsetNumberNext(PageGetMaxOffsetNumber;
- if(OffsetNumberIsValid(offsetNumber) //大爷型请求,值定了记录的存储位置
- {
- (overwrite) //原有的记录删除,属于要求改写
- {
- <limit{
- itemId=PageGetItemId(phdr(ItemIdIsUsed(itemId|ItemIdHasStorage{
- elog(WARNING"will not overwrite a used ItemId";
- return InvalidOffsetNumber}
- else //新增加的客户要求这个位置,需要将原来位于这个位置的记录迁移到其他位置。
- )
- needshuffletrue;*needtomove existing linp'selse //普通客户
- {
-
- }
- if)
- {
- ...
- else
- *offsetNumber wasnotpassedinifno free slot'll put it at limit(1st open slot(PageHasFreeLinePointers*
- *Lookfor"recyclable"(unused)ItemId.We checkforno storage
- *as welltobe paranoid-unused items should never have
- *storage.
- for=1;offsetNumber+!ItemIdIsUsed&!ItemIdHasStorage)
- break>=limit*the hintiswrong/
- PageClearHasFreeLinePointers*don't bother searchingifhint says there's no free slot/
- offsetNumber}
- #define PageHasFreeLinePointers&PD_HAS_FREE_LINES)
- |needshuffle)
- lower=phdr+sizeof; //新增一个Item
- else
- lower;
-
- alignedSize;
-
- upper(int)phdr)alignedSize(lower>upper)
- return InvalidOffsetNumber*OKtoinsert the item.Firstifneeded/
- itemId(needshuffle)
- memmove+1(limit-offsetNumber*sizeofsetthe item pointer/
- ItemIdSetNormal*copy the item's data onto the page/
- memcpy(char+upper*adjust page header/
- phdr(LocationIndex)lower;
- phdr)upper;
-
- return offsetNumber;
-
ItemIdSetNormal把Tuple的size,offset信息记录在Item中:
- #define ItemIdSetNormallen(
- >lp_flags=LP_NORMAL>lp_off(off
- >lp_len) //记录Tuple的size 。 (page + off,page + off + len)记录的是Tuple的信息
- )
我们下面讲述删除一条记录:
- void
- PageIndexTupleDelete)
我们找到Item,就可以找到Tuple对应的offset和size:
- tup;
- Assert(ItemIdHasStorage(tup;
- size=ItemIdGetLength;
- offset=ItemIdGetOffset;
删除第二个记录之后,我们得到的Page布局如下:
我们可以看到,至少发生两次memmove
1 删除记录的Item后面的item都要往迁移,防止出现一个空洞
- nbytes-
- &phdr>pd_linp[offidx](nbytes>0
- nbytes);
- addr+phdr;
-
- (offset>phdr(addr+size-phdr;
- !PageIsEmpty{
- inti;
-
- nline*there's one less than when we started(i;i=nline{
- ItemIdii(ii(ItemIdGetOffset=offset) //在前面Tuple2 前面的Tuple,发生了移位,所以对应Item的lp_off要修改。
- ii=size}
- Size
- PageGetFreeSpace)
- {
- intspace;
-
- /*Use signed arithmetic here so that we behave sensiblyifpd_lower>
- *pd_upper/
- spaceint-
- (space)
- return 0;
- space;
-
- returnSize)space;
- }
参考文献: 1 Postgresql 9.1.9 源码 原文链接:https://www.f2er.com/postgresql/195913.html