本文整理自阿里云数据库开源社区 Maintainer 于巍(花名漠雪),在PostgreSQL数据库内核解读系列的分享。本篇内容主要分为四个部分: 1. 磁盘管理 2. 空闲空间映射表(FSM) 3. 可见性映射表(VM) 4. 内存管理。
一、磁盘管理
用户访问存储引擎的流程如下:用户发送SQL请求后,首先访问元组(每一行数据即为一个元组),将数据加载至Buffer Pool,再解析出需要的内容,同时还需进行内存管理。然后经过存储管理器、磁盘管理器和虚拟文件管理器的依次调用,最终访问到物理文件。
每个Backend 都利用SMgrRelationHash来管理SMgrRelationData RelFileNodeBackend包含了三个 OID 能够唯一定位到某表的文件,表的文件有三种类型,包含正常的数据文件、FSM 文件和 VM 文件。数据文件有容量限制,比如限制为2G,超限后会切换至另一个文件。而针对每一个打开的文件,会由MdfdVec来管理数据,即通过MdfdVec数组来管理RelFileNode的文件。MdfdVecn内存储了 FD 内cache数组的下标以及文件segmnent的blocknumber。
SMgrRelationHash的创建和查询过程如下:首先通过smgropen函数查看hash表是否为空。若为空,则新建哈希表;否则,将rnode的relation与Backend 结合为 key 用于查找。然后设置reln>md_num_open_segs[forknum]=0,并进行segment相关操作。
外部访问SMGR,主要通过比如BufferPool利用 smgr相关接口获取数据,然后通过smgrread获取相关数据页。其中ReadBuffer_ommon调用存储来获取数据页,需要通过smgrread函数传入页号等相关参数。
Smgrread调用了虚拟函数表内的smgr_read,smgr_read会从smgrsw[]中查找mdread,相当于smgr利用smgrsw封装了底层MD调用接口,且此处预留了扩展接口,以便后续实现更多访问方式。
MD是Berkeyley开源的唯一一个磁盘管理器,可以访问磁盘和SSD,Berkeyley在最初发布的时候就做了注释。Mdread 和 mdcreate 利用虚拟函数表找到对应函数,再往下层调用FileSeek。Mdfd_getseg将根据ForkNumber查找与 SMgrRolation 结构相关的信息,并根据信息找到Mdfd_Vec变量,将Mdfd_Vec变量作为参数传递到下一层的FileSeek和FileRead 等函数。
虚拟文件管理层最主要的作用是防止打开文件的数量超过操作系统的限制。多个进程会有多个 backend 同时运行为客户提供服务,每个 bankend 都有对应的VFD记录相关信息。而操作系统对打开文件的数量有要求,因此需要进行对应的设置。利用 LRU 淘汰机制,通过 LRU 的双向链表来管理。如果超过了规定数量,则将最不常用的文件删除。
其中 VFD 内的fd是操作系统里对应的文件句柄fd,nextFree负责串联freelist,lruMoreRecently和lruLessRecently负责管理双向链表LRU结构。需要新访问或打开文件的时候,如果文件不存在,则将其添加到 LRU 最前端;如果LRU列表已满,则将最后(最不常用的)一个元素删除。
之后为 VFDCache申请空间,新空间大小newCacheSize为sizeVdfCache*2。再申请内存,大小为vdf*newCacheSize,并进行freelist初始化,之后数据会逐步从freelist进入LRU。
set_max_safe_fds函数用于计算可以打开的最大文件数量,通过进程数减去已打开的文件数得出。插入新元素至LRU链表时,需从最常用一侧插入。
综上,存储引擎的访问流程可以总结为:通过元组访问,将数据加载到 buffer 里,再通过依次调用SMGR、MD、VFD,其中SMGR层的relationcache和VFD层的 vfdcache分别用于缓存和加速,最终通过read函数获取系统相关的接口来读取文件。
二、空闲空间映射表(FSM)
空闲空间映射表使用最大堆二叉树的结构做存储,每一层记录 4000 个页面,每个节点都用字节来存储。PG规定的表最大size为232,而40003>232,因此三层可覆盖所有数据页。
最底层的叶子用于存储页面对应的空闲空间值,从1开始编号,依次增大。叶子节点用1个字节记录块的大致剩余空间,分为256级,每个级别对应一个空间范围。可以通过pg_freespace查询每个blocknumber对应的空闲空间,空闲空间记录的是范围值,并不需要绝对精确,因为实际使用时往往只需选择比需要的空间略大的页面即可。
符合条件的空闲空间页面的检索逻辑如下:先通过 fsm_readbuf 函数读取页面。然后返回同时符合addr.level=FSM_BOTTOM_LEVEL和max_avail=fsm_get_max_avail(BuffergetPage(buf))以及slot!=-1的blocknumber。
三、可见性映射表
可见性映射表中,每一位数据负责管理某配置的状态是否可见。每个页面使用2个bits来存储heap page。
可以通过create extensiong pg_visibility;select pg_visibility_map(pg_static);语句查询状态可见性。pg_visibility_map()函数的实现原理如下:
首先打开 relation即VM文件,随后执行check_relation_relkind函数,此处只支持RELKIND_RELATION、RELKIND_INDEX、RELKIND_MATVIEW、RELKIND_SEQUENCE、RELKIND_TOASTVALUE几种类型。
再通过pg_visibility_tupdesc组装出tup的描述 。一般情况下,tupdesc能够直接在系统表里获取。而此处由于数据格式固定,因此需要自行生成 。
然后通过visibilitymap_get_status获取页面的可见性信息。函数逻辑为:传入blocknumber并计算其偏移量,然后读取对应的buffer和内容并返回状态信息。
将返回的状态信息与VISIBILITYMAP_ALL_VISIBLE、VISIBILITYMAP_ALL_FROZEN分别进行对比得到两个布尔值,将布尔值组装成DATUM值返回,并解析成(1,t,t,)、(2,f,f,)形式以显示可见状态。
四、内存管理
操作系统有自己的内存管理机制,Java语言也有内存管理和回收机制,而 C语言通常需要自行管理内存。频繁地向操作系统申请和释放内存会产生较大开销,导致效率低,因此,需要相应的内存管理策略——MemoryContex机制来解决问题。
MemoryContex机制与操作系统的管理机制较为相似。相同大小的内存分别用一串 freelist来管理。申请数据时,会一次性申请多个相同容量的空间存放至 freelist,需要时直接从freelist取,用完放回。这样能够大幅减少与操作系统之间的调用次数,能够使性能显著提升。而针对更大的内存比如超过256字节,会有另外的策略,如上图步骤一所示。
MemoryContex一般会定义11个大小不同的freelist,最小内存为23,而后按照24、25依次增大。
AllocSetContext结构用于管理MemoryContext,其中MemoryContextData为树结构,使用完MemoryContext后,可将其对应的内存信息全部清理干净。AllocChunk通过AllocChunkData串联而成。此外,AllocSetContext内还定义了initBlockSize、maxBlockSize、nextBlockSize、allocChunkLimit。
此前,MemoryContext出现问题难以定位,且一旦出现问题往往较为致命。而现在,我们可以利用pg_log_backend_memory_contexts()函数,传入Backend 信息,即可打印日志,查看MemoryContext的使用情况,如上图下方所示,包含了每一层的具体信息以及所有层累计后的汇总信息。
主要MemoryContext的创建通过需要MemoryContextInit函数。首先,创建TopMemoryContext,主要变量的定义如上图左下所示。创建内存都是在CurrentMemoryContext上进行,因此我们需要不停地维护CurrentMemoryContext以保证它总是指向想要创建的MemoryContext,即设置CurrentMemoryContext=TopMemoryContext。
MemoryContext的创建通过AttachPartitionEnsureIndexes函数,其中AllocSetContextCreate函数在CurrentMemoryContext下创建了名为“AttachPartitionEnsureIndexes”的MemoryContext,并得到一个句柄。之后,调用MemoryContextSwitchTo将CurrentMemoryContext指向新创建的MemoryContext。然后调用palloc函数申请内存。
使用完内存以后,再调用MemoryContextSwitchTo将CurrentMemoryContext指向老的MemoryContext,然后执行MemoryContextDelete(cxt),完成清理。
palloc申请内存时,首先通过new_stack=(BTStack)palloc(sizeof(BTStackData))传入对应参数,通过CurrentMemoryContext做内存的申请和管理。底层调用AllocSetAlloc函数来管理内存chunk和freelist等,然后调用虚拟函数表中的alloc方法完成内存申请。