FAIRYFAR-INTERNAL
 
  FAIRYFAR-INTERNAL  |  SITEMAP  |  ABOUT-ME  |  HOME  
您的足迹: PostgreSQL内存上下文
PostgreSQL内存上下文

转自:http://blog.csdn.net/u014539401/article/details/51893272

一、数据库内存上下文

PostgreSQL在7.1版本引入了内存上下文机制来解决日益严重的内存泄漏的问题,在引入了这种“内存池”机制后,数据库中的内存分配改为在“内存上下文中”进行,对用户来说,对内存的申请由原来的malloc、free变成了palloc、pfree。对内存上下文的常用操作包括:

  • 创建一个内存上下文:MemoryContextCreate
  • 在上下文中分配和释放内存:palloc、pfree
  • 删除内存上下文:MemoryContextDelete
  • 重置内存上下文:MemoryContextReset

这里引入两个概念:

  • Chunk(内存片):用户在内存上下文中申请(palloc)到的内存单位。
  • Block(内存块):内存上下文在内存中申请(malloc)到的内存单位。

二、数据结构

2.1. AllocSetContext

snippet.c
#define ALLOC_MINBITS		3	/* smallest chunk size is 8 bytes */
#define ALLOCSET_NUM_FREELISTS	11
#define ALLOC_CHUNK_LIMIT	(1 << (ALLOCSET_NUM_FREELISTS-1+ALLOC_MINBITS))
#define ALLOC_CHUNK_FRACTION	4  /* Size of largest chunk that we use a fixed size for */
 
typedef struct AllocSetContext
{
	MemoryContextData header;	/* Standard memory-context fields */
	/* Info about storage allocated in this context: */
	AllocBlock	blocks;			/* head of list of blocks in this set */
	AllocChunk	freelist[ALLOCSET_NUM_FREELISTS];	/* free chunk lists */
	/* Allocation parameters for this context: */
	Size		initBlockSize;	/* initial block size */
	Size		maxBlockSize;	/* maximum block size */
	Size		nextBlockSize;	/* next block size to allocate */
	Size		allocChunkLimit;	/* effective chunk size limit */
	AllocBlock	keeper;			/* keep this block over resets */
	/* freelist this context could be put in, or -1 if not a candidate: */
	int			freeListIndex;	/* index in context_freelists[], or -1 */
} AllocSetContext;
 
typedef AllocSetContext *AllocSet;

AllocSetContext是内存上下文的核心的控制结构,我们在代码中经常看到的内存上下文TopMemoryContext的定义为:

snippet.c
MemoryContext TopMemoryContext = NULL;

可以看到这个内存上下文的类型是MemoryContext,即:

snippet.c
typedef struct MemoryContextData
{
	NodeTag		type;			/* identifies exact kind of context */
	MemoryContextMethods methods;		/* virtual function table */
	MemoryContext parent;		/* NULL if no parent (toplevel context) */
	MemoryContext firstchild;	/* head of linked list of children */
	MemoryContext nextchild;	/* next child of same parent */
	char	   *name;			/* context name (just for debugging) */
	bool		isReset;		/* T = no space alloced since last reset */
 
  /* CDB: Lifetime cumulative stats for this context and all descendants */
  ……
} MemoryContextData;
 
typedef struct MemoryContextData *MemoryContext;

那么,MemoryContextData和AllocSetContext是什么样的关系呢?请看下图左半部分。

内存上下文数据结构

AllocSetContext结构的第一个指针用于指向MemoryContextData,也就是说TopMemoryContext实际上是一个AllocSetContext结构,但是使用时通常将类型转换为MemoryContextData,实际上这也是PG中最常用的技巧之一,在代码中可以看到这样的写法:

snippet.c
AllocSet    set = (AllocSet) context;

由于AllocSetContext结构中的首部存放着MemoryContextData结构成员,所以这种转换可以成功。这样的使用方法有些类似与类的继承:MemoryContextData代表父类,AllocSetContext在父类(头部的指针)的基础上增加了一些新的功能。实际上PG就是使用了这种机制实现了interface(MemoryContextData作为interface),而后面的实现可以有很多种(AllocSetContext是内存上下文的一种实现)。

言归正传,继续介绍MemoryContextData数据结构的功能:

  • methods:保存着内存上下文操作的函数指针(例如palloc、pfree)
  • parent、firstchild、nextchild:形成内存上下文的BTree结构
  • name:内存上下文名称(方便调试)
  • isReset:记录上次重置后是否有内存申请动作发生

MemoryContextData使内存上下文形成了一个二叉树的结构,这样的数据结构增加了内存上下文的易用性,即,在重置或删除内存上下文时,所有当前上下文的子节点也会被递归的删除或重置,避免错删或漏删上下文。methods中保存的全部为函数指针,在内存上下文创建时,这些指针会被赋予具体函数地址。

下面继续介绍AllocSetContext数据结构:

  • header:前面介绍过了
  • blocks:Block链表,内存上下文向OS申请连续大块内存后,空间由blocks链表维护
  • freelist:Chunk回收数组,后面具体分析
  • initBlockSize:上下文申请的第一个Block的大小
  • maxBlockSize: 上下文申请的最大的Block的大小
  • nextBlockSize: 上下文下一次申请的Block的大小(MemoryContextCreate函数中介绍这三个参数)
  • allocChunkLimit:申请Chunk/Block的阈值
  • keeper:这个指针指向内存上下文重置时不释放的Block

2.2. AllocChunkData

Chunk存在于Block以内,是Block分割后形成的一段空间,Chunk空间的头部为AllocChunkData结构体,后面跟着该Chunk的空间,实际上palloc返回的就是指向这段空间首地址的指针。Chunk有两种状态:

  • AllocSetContext中freelist数组中存放的是Chunk指针,是被回收的Chunk。
  • 另外一种Chunk是用户正在使用的Chunk。

注意:两种状态的Chunk都存在于Block中,被回收只是改变Chunk的aset指针,形成链表保存在freelist中;在使用中的Chunk的aset指针指向所属的AllocSetContext。

snippet.c
typedef struct AllocChunkData
{
    void       *aset;
    Size        size;
}   AllocChunkData;

在palloc时会发生两种情况:

  • AllocSet会在自己维护的Block链表(blocks)中寻找空间构造Chunk,然后分配给用户。
  • 申请新的Block追加到blocks链表中,在其中分配新的Chunk分配给用户。

Chunk的数据结构相对简单,空指针aset是一个复用的指针,当Chunk正在使用时,aset指向它属于的AllocSet结构,当Chunk被释放后,Chunk被freelist数组回收,aset作为实现链表的指针,用于形成Chunk的链式结构。

2.3. AllocBlockData

snippet.c
typedef struct AllocBlockData
{
    AllocSet    aset;           /* aset that owns this block */
    AllocBlock  next;           /* next block in aset's blocks list */
    char       *freeptr;        /* start of free space in this block */
    char       *endptr;         /* end of space in this block */
}   AllocBlockData;
 
typedef struct AllocBlockData *AllocBlock;

Block是内存上下文向操作系统申请的连续的一块内存空间,申请后将AllocBlockData结构置于空间的首部,其中freeptr和endptr用与指向当前Block中空闲空间的首地址和当前Block的尾地址,见第二章示意图的“连续内存段(内存块)”。aset指向控制结构AllocSetContext,next指针形成Block的链式结构。

2.4. freelist[ALLOCSET_NUM_FREELISTS]

AllocSetContext结构中的一个重要的数组freelist,这是一个定长数组:

snippet.c
#define ALLOCSET_NUM_FREELISTS  11
typedef struct AllocSetContext
{
	……
	AllocChunk	freelist[ALLOCSET_NUM_FREELISTS];	/* free chunk lists */
	……
} AllocSetContext;

这是一个存放Chunk指针的数组,数组中每一个元素都是一个Chunk指针,就像前面提到的,空闲Chunk会形成链表结构,而链表的头结点的指针就存放在这个数组中。从长度来看,这个数组可以保存11个Chunk的链表,每一个链表都保存着特定大小的Chunk:

freelist

上图描述的就是freelist数组的结构,数组下标0位置保存8字节的Chunk,下标1位置保存16字节的Chunk,以此类推,freelist中可以保存的最大的Chunk为8k字节。

相同大小的Chunk会串在同一个链表中,放在freelist中指定的位置,数组下标的计算按照公式: $$ \log(Size) - 3 $$ 例如,大小为512字节的Chunk被释放了,套用公式 log(512) - 3 = 5,那么这个Chunk就会维护到freelist[5]指向的链表中。(具体计算过程见AllocSetFreeIndex函数)。

2.5. AllocSetFreeIndex

log对数计算,GP使用了查表法:

snippet.c
#define LT16(n) n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n
static const unsigned char LogTable256[256] =
{
	0, 1, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4,
	LT16(5), LT16(6), LT16(6), LT16(7), LT16(7), LT16(7), LT16(7),
	LT16(8), LT16(8), LT16(8), LT16(8), LT16(8), LT16(8), LT16(8), LT16(8)
};
 
static inline int
AllocSetFreeIndex(Size size)
{
	int			idx;
	unsigned int t,
				tsize;
 
	if (size > (1 << ALLOC_MINBITS))
	{
		tsize = (size - 1) >> ALLOC_MINBITS;
 
		/*
		 * At this point we need to obtain log2(tsize)+1, ie, the number of
		 * not-all-zero bits at the right.  We used to do this with a
		 * shift-and-count loop, but this function is enough of a hotspot to
		 * justify micro-optimization effort.  The best approach seems to be
		 * to use a lookup table.  Note that this code assumes that
		 * ALLOCSET_NUM_FREELISTS <= 17, since we only cope with two bytes of
		 * the tsize value.
		 */
		t = tsize >> 8;
		idx = t ? LogTable256[t] + 8 : LogTable256[tsize];
 
		Assert(idx < ALLOCSET_NUM_FREELISTS);
	}
	else
		idx = 0;
 
	return idx;
}

三、算法

3.1. AllocSetContextCreate:创建内存上下文

snippet.c
MemoryContext
AllocSetContextCreate(MemoryContext parent,
                      const char *name,
                      Size minContextSize,
                      Size initBlockSize,
                      Size maxBlockSize)

内存上下文创建需要传入几个参数:

  • parent:当前创建内存上下文的父节点
  • name:当前创建内存上下文名称
  • minContextSize:创建上下文时申请Block大小
  • initBlockSize:该上下文第一次申请Block大小
  • maxBlockSize:该上下文可以申请的最大Block大小

让我们看几个数据库中最常见的上下文创建时的参数,结合具体值在说说创建时参数的作用:

img

  • minContextSize:如果这个值设定了并超过了一定大小(一个Block结构体加上一个Chunk结构体的大小),那么在创建上下文时立即申请一个Block,大小为minContextSize。上图中我们可以看到大部分上下文minContextSize都为0,那么ErrorContext的minContextSize为8k有什么作用呢?在系统出现OOM时,内存空间已经耗尽,但是ereport的错误处理流程仍然需要申请内存空间去打印错误信息,但系统已经没有内存可以申请了。这时ErrorContext中保留的8k空间可以保证最后的错误处理流程可以正确执行。
  • initBlockSize、maxBlockSize:内存上下文中的Block申请的大小是由这两个参数决定的,initBlockSize代表了第一次申请的Block大小,后面每一次申请都是前一次申请大小的二倍,直到申请内存大小为maxBlockSize为止,当达到maxBlockSize时,以后每一次申请的内存大小都等于maxBlockSize。(事实上如果多次在一个上下文申请内存,那么很快就会到达maxBlockSize,举个例子:TupleSort中申请Block的大小序列为:8k 16k 32k 64k 128k 256k 512k 1M 2M 4M 8M 8M 8M 8M …)
  • allocChunkLimit:这里引出一个重要的参数,Chunk申请阈值,这个值被开始被设为8k字节,但是后面会适当缩小到maxBlockSize的1/8。这个参数的调整是为了减少Chunk空间的浪费(Block中的最后一段内存不足以放下一个Chunk,所以这段空间被舍弃掉了,理论上浪费掉的空间最大为allocChunkLimit)。

申请内存的流程图:

img

需要重点关注的有几点:

  • 回收当前Block的剩余空间:将剩余空间切割成freelist能保存的最大值,例如,1000字节的Chunk回收时首先申请512字节的Chunk,然后挂在freelist[6]上,剩余488字节申请256字节的Chunk挂在freelist[5]上,剩余232字节继续上面处理流程,直到最后空间小于8字节为止。
  • 在多次申请Block后,Block的大小总会等于maxBlockSize,这样如果出现内存泄漏导致OOM时,如果某一个内存上下文非常大,可以利用这个特点分析内存问题的根因。例如每100次申请8M的Block时,打印一次Backtrace。

3.3. AllocSetFree

释放内存流程图:

img

3.4. AllocSetRealloc

relloc流程图:

img

3.5. AllocSetStats

这个函数会被MemoryContextStats递归调用,遍历内存上下文树的内个节点,并获取当前节点的信息。

GDB调试时这一个非常好用的函数,可以直接在log中打印内存上下文树,指令:

snippet.gdb
gdb > p MemoryContextStats(TopMemoryContext)

四、参考



打赏作者以资鼓励: