Article / 文章中心

从托管到原生,MPP架构数据仓库的云原生实践

发布时间:2022-01-21 点击数:178

一  前言


Garner预测,到2022年,所有数据库中有75%将部署或迁移至云平台。另外一家权威机构IDC也预测,在2025年,超过50%的数据库将部署在公有云上,而中国则会达到惊人的70%以上。云数据库经过多年发展,经历从Cloud-Hosted (云托管)到 Cloud Native(云原生)模式的转变。
Cloud-Hosted:基于市场和业界的云需求,大部分厂商选择了云托管作为演进的第一步。这种模式将不再需要用户线下自建IDC,而是依托于云提供商的标准化资源将数据仓库进行移植并提供高度托管,从而解放了用户对底层硬件的管理成本和灵计划资源的约束。 
Cloud-Native:然而随着更多的业务向云上迁移,底层计算和存储一体的资源绑定,导致用户在使用的过程中依然需要考量不必要的资源浪费,如计算资源增加会要求存储关联增加,导致无效成本。用户开始期望云资源能够将数据仓库进行更为细粒度的资源拆解,即对计算,存储的能力进行解耦并拆分成可售卖单元,以满足业务的资源编排。到这里,云原生的最大化价值才被真正凸显,我们不在着重于打造存算平衡的数据仓库,而是面向用户业务,允许存在大规模的计算或存储倾斜,将业务所需要的资源进行独立部署,并按照最小单位进行售卖。这一刻我们真正的进入了数据仓库云原生时代。


阿里云在2021云栖大会上,预告了全新云原生架构的数据仓库【1】。本文介绍了云原生数据仓库产品AnalyticDB PostgreSQL(以下简称ADB PG)从Cloud-Hosted到Cloud-Native的演进探索,探讨为了实现真正的资源池化和灵活售卖的底层设计和思考,涵盖内容包括产品的架构设计,关键技术,性能结果,效果实现和后续计划几方面。(全文阅读时长约为10分钟)


二  ADB PG云原生架构


为了让用户可以快速的适配到云数据仓库,目前我们采用的是云上MPP架构的设计理念,将协调节点和计算节点进行独立部署,但承载于单个ECS上,实现了计算节点存储计算一体的部署设计,该设计由于设计架构和客户侧自建高度适配,可快速并无损的将数仓业务迁移至云上,对于早期的云适配非常友好且满足了资源可平行扩展的主要诉求。


分层存储的实现

如上图所示,我们把存储的资源分成3层,包括内存、本地盘和共享存储。
内存:主要负责行存访问加速,并负责文件统计信息的缓存;
本地盘:作为行存的持久化存储,并作为远端共享存储的本地加速器;
远端的共享存储:作为数据的持久化存储。


3  读写流程


写入流程如下:
  • 用户写入数据通过数据攒批直接写入OSS,同时会在本地磁盘上记录一条元数据。这条元数据记录了,文件和数据表的对应关系。元数据使用PG的行存表实现,我们通过file metadata表来保存这个信息。

  • 更新或者删除的时候,我们不需要直接修改OSS上面的数据,我们通过标记删除来实现,标记删除的信息也是保存在本地行存表中,我们通过visibility bitmap来存这个信息。标记删除会导致读的性能下降,我们通过后台merge来应用删除信息到文件,减少删除带来的读性能影响。


我们在写入的时候,是按照bucket对segment上的数据做了进一步划分,这里会带来小文件的问题。为了解决小文件问题,我们做了下面几点优化:
1. Group flush:一批写入的数据,可以通过group flush写到同一个OSS文件,我们的OSS文件采用了ORC格式,不同bucket写入到对应strip;
2. 流水线异步并行:编码攒批,排序是典型的cpu密集型任务,上传到oss是典型的网络IO密集型任务,我们会把这2种任务类型并行起来,在上传oss的任务作为异步任务执行,同时对下一批数据编码排序,加快写入性能。


因为远端持久化存储提供了12个9的持久性,所以只有保存元数据的行存才有WAL日志和双副本来保证可靠性,数据本身写穿到共享存储,无需WAL日志和多副本,由于减少了WAL日志和WAL日志的主备同步,又通过异步并行和攒批,在批量写入场景,我们写入性能做到了基本与ECS弹性存储版本性能持平。


读取流程如下:
  • 我们通过读取file metadata表,得到需要扫描的OSS文件。 

  • 根据OSS文件去读取对应文件。

  • 读到的文件通过元数据表visibility bitmap过滤掉已经被删除的数据。


为了解决读OSS带来的延迟,我们也引入了DADI帮忙我们实现缓存管理和封装了共享文件的访问,读文件的时候,首先会判断是否本地有缓存,如果有则直接从本地磁盘读,没有才会去 OSS读,读到后会缓存在本地。写的时候会直写OSS,并回写本地磁盘,回写是一个异步操作。对于本地缓存数据的淘汰我们也通过DADI来管理,他会根据LRU/LFU策略来自动淘汰冷数据。


由于事务是使用PG的行存实现,所以与ADB PG的事务完全兼容,带来的问题是,我们在扩缩容的时候需要重新分布这部分数据,我们重新设计了这块数据的重分布机制,通过预分区,并行拷贝,点对点拷贝等技术,极大缩短了扩缩容时间。


总结一下性能优化点:
•  通过本地行存表实现事务ACID,支持数据块级别的并发;
•  通过Batch和流水线并行化提高写入吞吐;
•  基于DADI实现内存、本地SSD多级缓存加速访问。


4  可见性表


我们在File Metadata中保存了共享存储文件相关的信息,它的结构如下:



字段 类型 说明
table_oid Int32 表的oid
hash_bucket_id Int16 hash_bucket的id
level Int16 逻辑文件所处的merge级别,0表示delta文件
physical_file_id Int64 逻辑文件对应的oss物理文件id
stripe_id Int64 逻辑文件对应的oss物理文件中的stripe id
Total_count int32 逻辑文件总共具有的行数,包括被删除行数

Hash bucket:是为了在扩缩容的时候搬迁数据的时候,能够按照bucket来扫描,查询的时候,也是一个bucket跟着一个bucket;
Level:是merge tree的层次,0层代表实时写入的数据,这部分数据在合并的时候有更高的权重;
Physical file id:是文件对应的id,64字节是因为它不再与segment关联,不再只需要保证segment内table的唯一性,需要全局唯一;
Stripe id:是因为一个oss文件可以包含多个bucket 的文件,以stripe为单位,方便在segment一次写入的多个bucket合并到一个oss文件中。避免oss小文件,导致性能下降,和oss小文件爆炸;
Total count:是文件行数,这也是后台合并的一个权重,越大合并的权重越低 。


Visibility bitmap记录了被删除的文件信息
字段 类型 说明
physical_file_id Int64 逻辑文件对应的oss物理文件id
stripe_id Int32 逻辑文件对应的oss物理文件中的stripe id
start_row Int32 delete_bitmap对应的起始行号,每32k行对应一个delete_bitmap
hash_bucket_id Int16 hash_bucket的id
delete_count Int32 该delete_bitmap总共记录删除了多少行
bitmap bytea delete_bitmap的具体数值,压缩存储

Start_row对应32k对应一个delete bitmap。这个32000 4k,行存使用的32k的page可以保存7条记录。
Delete count是被删除的数量。
我们无需访问oss,可以直接得到需要merge的文件,避免访问oss带来的延迟,另外oss对于访问的吞吐也有限额,避免频繁访问导致触发oss的限流。

5  行列混存


行列混存

Mergetree的结构如上图左侧所示,核心是通过后台merge的方式,把小文件merge成有序的大文件,并且在merge的时候,我们可以对数据重排,例如数据的有序特性做更多的优化,参考后续的有序感知优化。与leveldb的不同在于:
1.  0层实时写入的会做合并,不同bucket的文件会合并成大文件,不同bucket会落到对应的stripe;
2. Merge会跨层把符合merge的文件做多路归并,文件内严格有序,但是文件间大致有序,层数越高,文件越大,文件间的overlap越小。


每个文件我们使用了行列混存的格式,右侧为行列混存的具体的存储格式,我们是在ORC的基础上做了大量优化。
ORC文件:一个ORC文件中可以包含多个stripe,每一个stripe包含多个row group,每个row group包含固定条记录,这些记录按照列进行独立存储。
Postscript:包括文件的描述信息PostScript、文件meta信息(包括整个文件的统计信息,数据字典等)、所有stripe的信息和文件schema信息。
stripe:stripe是对行的切分,组行形成一个stripe,每次读取文件是以行组为单位的,保存了每一列的索引和数据。它由index data,row data和stripe footer组成。
File footer:保存stripe的位置、每一个列的在该stripe的统计信息以及所有的stream类型和位置。
Index data:保存了row group级别的统计信息。
Data stream:一个stream表示文件中一段有效的数据,包括索引和数据两类。
索引stream保存每一个row group的位置和统计信息,数据stream包括多种类型的数据,具体需要哪几种是由该列类型和编码方式决定,下面以integer和string 2种类型举例说明:


对于一个Integer字段