序言
Greenplum(以下简称 GP)是一种基于开源PostgreSQL基础上采用MPP架构的关系型分布式数据库,具有强大的大规模数据分析处理能力。
GP有两种存储格式:Heap表和AO表。其中,AO表是Greenplum所特有的,主要面向OLAP场景,支持行存和列存,批量的数据写入,有利于高吞吐数据量的加载,同时支持对数据进行压缩,AOCO不仅支持表级别的压缩,同时也支持列级别的压缩。
GP-AO表的分析速度快,对于OLAP场景,每次分析涉及的字段较少,可以保证行级严格事务。这种设计对大批量数据的访问和统计需求而言,能够有效提升分析速度。
在近期的直播中,HashData内核研发工程师介绍了GP-AO表的设计和特点。以下内容根据直播文字整理。
存储引擎概要
Heap表是从PostgreSQL继承而来的,目前是 GP 默认的表存储格式,只支持行存储,不支持列存和压缩。
Heap页面存储的数据称为元组(Tuple),在物理文件上不按照某种顺序进行排序。当读取Heap文件页面时,也不会对元组的排序做任何假设。Heap的存储格式对于OLTP和OLAP两个访问模式都是比较有效的,但更适合OLTP的场景,比如插入新的元组不需要考虑元组间的相互顺序,而且删除和更新元组也非常简单。
在OLAP的场景下,查询更多的是全表扫描。通常情况下,查询语句并不会读取所有列的数据,而会筛选出感兴趣的列。由于元组是将所有列都存放在一块,这样会增加额外的IO开销。此外,Heap由于采用跨页存储,检索非常复杂,对数据的压缩也不如AO和AOCS,对存储的开销较大。
总体来讲,Heap表的设计理念注重平衡性,对于元组增删查改的应用场景支持比较均衡。
存储引擎的AO表设计
在OLAP的场景下,数据大多是一些历史数据或日志数据,一般不会修改或者仅少量修改,元组更多以追加而非修改的方式存放。
另外,从数据访问方式来看,OLAP需要读取大量记录,记录多以扫描的方式进行读取。同时,由于每个页面存在空洞或者已经被删除的无效数据,扫描访问方式对于Heap来说并不高效。
基于此,Greenplum引入了AO表,用来专门存储以追加方式插入的元组。最开始设计时,AO表被称为Append Only表,只支持追加新元组。在后来的演进中,也支持了删除和更新元组操作,因此现在AO表指的是Append Optimized。
与Heap表相比,AO表存储更紧凑,记录之间没有空余空间,在AO表上进行分析通常效率更高。此外,AO表可以支持列式存储,在处理大批量数据时具有显著优势,非常适合向量计算和JIT架构。
对于AO表的每个文件,元组总是添加到文件末尾,所以文件的结尾地址(EOF)就可以作为数据可见性判断的依据。只有当事务成功提交后,从原来EOF之后新追加的元组才对外可见,否则对外只能看到文件原来的EOF。
对此,Greenplum中提供了两个系统表记录相应的信息:pg_appendonly和pg_aoseg.pg_aoseg_。其中,pg_appendonly表中segrelid记录了AO表所对应pg_aoseg.pg_aoseg_表的OID。
pg_aoseg.pg_aoseg_为Heap表,记录了AO表每个数据文件的EOF。这样可以通过MVCC来管理pg_aoseg.pg_aoseg_表中的EOF信息。
pg_aoseg.pg_aoseg_中的OID指的是AO表的OID。
另外,Heap的元组存储了太多可见性相关的信息,由于EOF已经可以作为可见性判断的依据,所以AO表中存储的元组不需要存储这些额外的信息。
二者对比而言,AO表存储的元组结构是MemTuple,Heap假设的访问场景是随机和顺序访问,AO表假设的访问场景多是顺序扫描;Heap表的块必须要大小一致,便于随机寻址,AO表的块并不需要定长存储,可以进行变长压缩,以节省空间。
Heap表的块多个进程间需要共享,通过共享缓冲区进行管理。AO表由于是变长块,而且多是顺序扫描,所以不经过共享缓冲区。
为了支持删除操作,AO表引入了VisibilityMap。如果元组被删除,将会在VisibilityMap中标记。该VisibilityMap信息由系统表pg_aovisimap_保存。AO表的更新操作的实现也就转换为删除操作+插入操作。
由于AO表采用的是变长块,无法通过文件内的逻辑块号或行号直接定位到物理位置,但是为了支持在AO表上建立索引,需要通过行号快速定位到物理位置以便进行元组读取。当AO表上建有索引时,Greenplum中会创建系统表pg_aoblkdir_来存储行号到物理位置的映射信息,来减少额外的存储开销。
AO表设计框架结构总体概略
数据设计
AO表的数据设计实现面对的主要问题是数据的增删改查,这就需要解决四个问题:数据存放、数据可见性、数据表结构设计以及数据如何定位的实现。
其中,对于变长记录在数据库系统中的出现有几个原因。最常见的原因是变长域的出现,比如字符串。实现变长记录可以采用不同的技术,但都必须解决两个问题:
如何表示单条记录,使得此记录的单个属性能够被轻松地提取,即使这些属性是变长的;
如何在一个块中存储变长的记录,使得一个块中的记录能够被轻松地提取。
具有变长属性的记录表示通常包含两个部分:首先是带有定长信息的初始部分,其结构对于相同关系的所有记录都是一样的。紧接着是变长属性的内容,诸如数字值、日期或定长字符串等固定长度的属性,被分配存储它们的值所需的字节数。对于可变长字符串类型这类的变长属性,在记录的初始部分中被表示为一个(偏移量,长度)。
对于块中变长记录的存储问题,比如分槽的页结构,一般用于在块中组织记录,每个块开始有块头,包含如下信息:块头的记录项的数量、块中的自由空间的末尾处、一个包含每条记录的位置和大小的项组成的数组。
数据存放设计
AO表通过EOF来控制可见性,相比于HeapTuple,元组中不再需要存储可见性相关信息。MemTuple除了可用在AO表存储格式之外,还会用在执行器中,因为当元组从磁盘读取出到执行器后,不再需要保留可见性相关信息。
数据检索设计
对于AO表,每个事务会写不同的分片文件,所以AO表中元组的位置不再像Heap表中的线性结构,而是由分片文件编号(7位,范围0~127)和文件内行号(40位)确定。AO表通过AOTupleId数据结构来表示AO表中元组的地址,由于元组地址会用到多个地方,比如索引、索引扫描等,所以AOTupleId采用了和ItemPointerData同样的大小和对齐方式。AOTupleId一共48位6字节,以16位对齐。
AO表采用的是变长块,和ItemPointerData不同的是,并不能简单的从AOTupleId对应到元组的物理位置。在Greenplum中,引入了块目录(BlockDirectory)的表和数据结构,来方便查找从分片文件号以及块内行号来获得在文件中的物理位置。只有当AO表上创建有索引时,块目录才会创建。
块目录表维护了从分片文件号、列组编号(columngroup_no,AO表始终为0,AOCS会用到)、起始行号三者到minipage的映射。minipage是由MinipageEntry组成的数组,每个MinipageEntry记录了一个或多个块的起始位置,包含如下信息:表MinipageEntry重要成员、firstRowNum 起始行号、fileOffset文件内偏移位置、rowCount 行数。
索引扫描或者位图扫描时,会通过AOTupleId读取单个元组,其扫描过程如下:
1、通过AOTupleId计算得到目标分片文件号segmentFileNum和行号rowNum。
2、如果当前打开分片文件不是segmentFileNum,则关闭当前文件,重新打开第 segmentFileNum号文件。
3、判断AOTupleId在块目录表中是否存在,如果不存在,说明AOTupleId是旧的被回收的元组,或者之前异常终止事务插入的元组,这种情况下,返回读取失败。
4、通过VisibilityMap进行可见性检查,如果不可见,返回失败。
5、将文件的读取起止范围设置为块目录表项中的起止范围。由于块目录表项可能对应一个或者多个块,所以表项中记录的起始位置可能比实际的文件块大。
6、调用函数scanToFetchTuple在起止范围内逐个读取文件块,直到找到某个块满足:currentBlock.firstRowNum <= rowNum <= currentBlock.lastRowNum。如果没找到,则返回失败。
7、在块内查找查找行号为rowNum的元组并返回。
8、保存当前分片文件号,当前块起止行号,当前块目录信息,下次再次读取单个元组时,如果:
a、AOTupleId在当前起止行号之前,直接在当前块内查找元组,否则直接跳转到b。
b、在当前块目录范围内,跳转到第4步开始执行。
数据可见性设计
AO表引入了另外一个辅助的Heap表pg_aoseg.pg_aovisimap_,称为VisibilityMap。该表记录了删除元组的信息,并且通过MVCC控制可见效。如果删除成功,通过该表就能查询到删除元组,从而判断元组不可见。如果每个删除的元组占用一行,显然不经济并且低效。大部分情况下,AO表中元组的访问都是顺序扫描,VisibilityMap借鉴了块目录表的思路,将多个相邻元组可见性信息放在一起存储。
Greenplumn中用位图来表示每个分片文件中元组的可见性,如果元组被删除,对应位图的比特位置1。每32768(APPENDONLY_VISIMAP_MAX_RANGE)个元组的可见性比特位作为一个位图存储在表pg_aoseg.pg_aovisimap_的visimap属性中。
Greenplum中在位图表上建立了索引,方便快速查找。每个位图4096个字节,包含32768位,可以表示32768个元组的可见性。VisibilityMap通过函数AppendOnlyVisimapDelete_Init开始位图的修改,通过函数AppendOnlyVisimapDelete_Finish结束位图的修改。当某个行被删除时,需要判断该行对应的位图是否以及已经在VisibilityMap表中已记录,如果是则读入内存中,否则初始化一个全零的位图。
AOCS列存的设计
AO表整个行一起存储,当需要读取属性内容时,首先需要读取变长块,解压缩,然后再获取其中一个属性的值。这样做有时代价非常高,即使查询只涉及其中一个属性,也需要将所有内容读出来。在OLAP分析领域,列式存储是应对这种查询场景、提高性能的常见优化方式。Greenplum中也提供列式存储:AOCS
AOCS将不同的属性分成不同的文件存储。查询时,只需要读取需要的属性所对应的文件,其它属性不需要读入,节省了磁盘IO开销。
同AO表一样,AOCS的访问也不需要通过共享缓冲区管理器,直接从磁盘进行读写。另外,AOCS对数据的压缩做了特殊的处理,能够获得更好的表现。
AOCS表的存储类似AO表的存储,但是将每个列存储在单独的分片文件中,其中,第0到127号分片文件存储第一个属性,第128到255号分片文件存储第二个属性,依次类推。其中,第0,128,256等文件逻辑上属于第0个分片文件,分配给Utility模式使用,第1,129,257等文件逻辑上属于第1个分片文件,依次类推。
对于MPP模式,一个可以支持127个分配文件并发写。通过这种物理文件映射到逻辑分片文件的方法,AOCS表可以复用与AO表相同的逻辑,比如AOTupleId、索引、VisibilityMap。
结语
HashData内核基于开源的PostgreSQL和Greenplum Database构建,元数据采用开源的KV数据库FoundationDB提供持久化,通过ORC、Parquet等开放文件格式与其它大数据系统实现互通互联。
传统的Greenplum、Teradata等MPP 架构的数据库,存储、计算是紧耦合的,数据存储在本地系统,存储能力的扩展通过增加集群节点实现,这样会导致计算资源严重浪费,无法满足业务的发展。
作为一款企业级数据仓库产品,HashData在PostgreSQL和Greenplum Database等平台丰富的分析功能的基础上,针对云平台特性进行了大量改进和优化,实现了存算分离、湖仓一体化,满足企业对海量数据的分析与处理需求,加速企业数字化转型。