四层解耦的Serverless | 元数据和事务系统的技术难题和关键设计

 引言

自Snowflake架构诞生之后,该公司最高市值接近1000亿美金,Snowflake提出的计算、存储和元数据三层分离架构也引领了国内众多厂商的架构设计,我们能够看到:

Hashdata的3层架构:

南大通用GBase:

doris 2.x版本的架构,并且已经开源:

该架构核心设计思想大体如下:

  1. 把有状态的模块,例如元数据,事务管理,锁管理等,Snowflake甚至把执行计划缓存,query cache缓存都放到元数据层,计算和存储都变成无状态的服务,方便对计算和存储做弹性;

  2. 作为有状态的服务不太好实现serverless,为了降低大规模场景下元数据层的成本,都把元数据层做成多租户共享的服务。

在众多厂商尚未明确阐述元数据层独立后所面临的挑战和技术细节的情况下,Relyt AI-ready Data Cloud作为一个采用四层解耦架构(元数据、计算、缓存和存储)的云原生数据仓库,通过多年的技术沉淀和广泛的客户实践,积累了丰富的经验。本期,我们邀请了质变科技AI-ready数据云团队布道师、云原生领域资深人士元飞为大家分享话题《计算、存储和元数据三层分离架构下,元数据和事务系统的挑战和关键技术》。

在上述背景下,应该如何应对元数据层独立后的技术挑战?

  1. 保持与PostgreSQL生态系统产品的一致体验:我们致力于让客户能够像使用单机版PostgreSQL一样便捷地使用云原生数据仓库。虽然开源产品的用户对事务能力的要求不高,但闭源产品通常提供标准的ACID事务能力,这已成为闭源产品提升用户体验的关键竞争力之一。

  2. 远端存储与本地存储性能差异的解决方案:我们着力解决远端存储与本地存储之间的性能差距,旨在让用户的DDL操作和读写过程能够达到与使用本地数据库相同的延迟。特别是在TPC-H等场景下,我们实现了高吞吐量和低延迟的性能优化。

  3. 元数据管理的挑战:传统大数据的元数据管理相对简单,因为它们主要面向不可变的大文件。我们探讨了元数据层如何应对实时数据仓库中的实时小批量写入,以及高频的update/delete操作,还有高QPS的点查Servering场景。

  4. 面向未来的演进:我们探讨了如何向湖仓一体架构演进,实现数据的开放共享,并面向AI实现AI-Ready的挑战。

元数据层作为数据仓库的核心组成部分,我们将从Relyt AI-ready Data Cloud的实践经验出发,深入分析其中的关键设计和技术难题。

 架构设计

  • 元数据服务层包括两层:无状态服务层 & 可扩展分布式存储层。

  • 无状态服务层:负责表、列、文件和统计信息的抽象,支持MVCC能力,能够提供一致的元数据视图,将KV结构封装为表API,方便多个DPS或应用服务的访问,DPS指代Relyt中一组计算资源,与Snowflake的virtual warehouse是一个概念;

  • 可扩展的分布式存储层:基于 FoundationDB (简称FDB)作为持久化存储,提供安全、可靠、可扩展的基础存储服务能力。

这样设计的好处在于:

  • 全局一致,多DPS统一元数据,update/delete跨DPS实时可见;

  • 极致弹性,服务节点无状态,动态弹缩,性能随节点线性扩展;

  • 安全可靠,fdb支持3AZ 部署,实时备份和恢复。

对无状态服务层做进一步展开,其中有下面几个关键服务:

  • Meta Service:负责表、列、文件和统计信息的抽象,支持MVCC能力,能够提供一致的元数据视图,将KV结构封装为表API,方便多个DPS或应用服务的访问。

  • Coordinator:基于PostgreSQL提供接入服务,执行计划的生成和执行计划的分发。

  • Master:负责全局事务管理,事务号分配,活动事务管理,2PC实现,以及元数据和加锁管理。

  • FDB:是一个ordered key-value,并且使用的是range分区,为了实现多租户下用户资源的隔离,我们把用户实例id作为key的前缀,不同的用户通过实例id做隔离,实例id下面是对应实例的数据。

  • Table Meta:表的metadata信息,包括表的描述,权限,可以参考PostgreSQL的pg_class。

  • Column Meta:列的信息,可以参考PostgreSQL的pg_attribute

  • File Meta:文件信息,此外我们还在key的编码中加入了文件的过滤信息和shard信息,实现了基于key前缀的高效过滤。

  • Delete Bitmap:文件中被删除的行的标记信息,被删除的行通过一个bit位来实现标记删除。在我们的列存实现中,采用的是与Greenplum类似的标记删除来实现update和delete。

  • Statistic:供优化器使用的统计信息。

  • Query cache:参考了Snowflake的设计,我们把query cache保存在FDB,实现跨实例和跨用户的高效查询。

这样设计的好处在于:

  • 事务兼容:支持MVCC,并发写;

  • PostgreSQL兼容:Coordinator基于PostgreSQL的插件机制来实现,兼容PostgreSQL;

  • Master:把事务管理的负载从Coordinator卸载下来,提升了Coordinator的事务处理能力,支撑高TPS的实时写入和Servering场景的高QPS查询任务。

 关键技术

关键读写流程

  1. client向Master发送begin命令,启动事务,在Master会开始事务状态机;

  2. Master把SQL和事务信息发送给Coordinator

  3. Coordinator从FDB读取元数据信息

  4. Coordinator对SQL做解析生成执行计划,

  5. Coordinator把执行计划下发给Worker

  6. Worker根据文件信息从对象存储读取文件,从FDB读取visimap,并执行

  7. Coordinator把执行结果返回给Master

  8. Master执行事务提交,在提交前首先做写写冲突检测,再把FDB的元数据做提交。这里我们把FDB的元数据分成temp space和work space,其中第第6步的写都写入temp space自己事务的空间,在提交的时候会"move"到work space,这个"move"通过FDB的事务来保证原子性。

  9. 把执行结果返回给客户端。

关键设计:

与大多数数仓的做法不同的是,我们在做写写冲突检测的时候,并没有遇到冲突就把整个事务回滚,原因是当前系统强依赖后台的vacuum对数据做合并和重排来提升查询的性能,如果后台的 vaccum和用户的update冲突就回归会降低用户体验,同时vaccum和update本身都是比较重的操作,会导致我们做大量的无用功。所以这里我们的做法是通过写补偿,把用户的delete操作在vaccum合并后的文件中重放,实现了“写写并发”。

Insert时序图

Lock table t:这里就是PostgreSQL本身的表级锁,insert/update/delete拿的是数据表的2级锁。

Unlock table t:与上面的lock table对应,释放的数据表的2级锁。

原子性保证,这个通过2PC来解决。

Delete时序图

这里的核心是如何解决写写冲突的问题。

写写冲突检测

Lock file_visibility table:对修改的表的辅助表加4级锁。为了避免在ww-conflict中间有其它事务对这个表也在做冲突检测,我们在开始ww-conflict之前,需要首先对该表的辅助表加4级锁,只允许对辅助表(非数据表)select,不允许对辅助表(非数据表)update/delete/insert,直到事务结束释放。由于是对辅助表加4级锁,而不是对数据表加4级锁,并不阻塞其它事务的update/delete/insert(在update/delete/insert过程中,与PG传统做法不同,不加2级锁,只加1级锁,因为此时并不会修改work space的数据),只会阻塞其它事务prepare/commit和冲突检测。

MVCC实现

我们基本借鉴了PostgreSQL本身的MVCC算法,在此基础上根据上述数据结构调整。实现了基于KV的可见性判断算法:

Haskell
/*  tmp space自己事务的数据:可见性判断 */
/* t_xmin status = ABORTED  针对子事务*/
Rule 1: IF t_xmin status is 'ABORTED' THEN
           RETURN 'Invisible'
       END IF
/* t_xmin status = IN_PROGRESS */
       IF t_xmin status is 'IN_PROGRESS' THEN
           IF t_xmin = current_txid THEN
Rule 2:         IF t_xmax = INVALID THEN
                   RETURN 'Visible'
Rule 3:         ELSE  /* this tuple has been deleted or updated by the current transaction itself. */
                   RETURN 'Invisible'
               END IF
Rule 4:     ELSE   /* t_xmin ≠ current_txid */
               RETURN 'Invisible'
           END IF
        END IF

/*  work space数据:可见性判断 */
/* t_xmin status = COMMITTED */
        IF t_xmin status is 'COMMITTED' THEN
Rule 5:     IF t_xmin is active in the obtained transaction snapshot THEN
               RETURN 'Invisible'
Rule 6:     ELSE IF t_xmax = INVALID OR status of t_xmax is 'ABORTED' THEN
               RETURN 'Visible'
           ELSE IF t_xmax status is 'IN_PROGRESS' THEN
Rule 7:         IF t_xmax =  current_txid THEN
                   RETURN 'Invisible'
Rule 8:         ELSE  /* t_xmax ≠ current_txid */
                   RETURN 'Visible'
               END IF
           ELSE IF t_xmax status is 'COMMITTED' THEN
Rule 9:         IF t_xmax is active in the obtained transaction snapshot THEN
                   RETURN 'Visible'
Rule 10:        ELSE
                   RETURN 'Invisible'
               END IF
           END IF
       END IF

虽然实现具有一定的复杂性,但是好处是行为保持与PG的行为保持一致,降低了Relyt的学习成本。

Timetravel实现

基于上述MVCC算法,我们可以很容易实现Timetravel。

Timetravel相当于我们去读某个历史时间点的元数据的快照。我们只需要变更读的事务快照,可见性判断读取的是数据的ts_xmin和ts_xmax,基于上述的MVCC算法,即可获取Timetravel所需版本的元数据。

极致性能优化

GMeta的cache实现

FDB作为元数据存储的一个优势在于它提供了比较大容量的kv存储能力,自动的扩缩容能力,但是在面向大量数据扫描,特别是visimap数据扫描的场景,它的压力还是比较大。8w个segfile查询,耗时在400~500ms之间,其中8w个segfile的数据总量在23MB左右,但是由于单次fdb交互只能拿到200KB左右的数据,导致需要100+次fdb的交互才能拿完,网络开销占比较高,如果一次性能拿完23MB的数据的话,按照网络带宽计算,大概耗时在20ms左右。为了解决这个问题,我们在FDB前面构建了元数据缓存服务,叫做gmeta service,我们把gmeta service管理的数据在本地的rocksdb保存一份。它的流程如下:

写入流程:

  1. 维持现有逻辑,将segfile 和visimap的数据写入到tmp space中;

  2. 在执行move阶段,先将tmp space的数据挪动到work space(此处不再清理tmp space下的数据);

  3. 在执行move阶段最后,将tmp space的txn infor设置为Committed状态,同时向事务提交队列写入一条关于该gxid的提交信息;事务提交队列即在FDB中单独分配了一段key 的range,保存事务的提交状态。

读取流程:

1.刚启动阶段(或out of sync阶段):

a.service获取当前事务提交队列中的最后一条记录,记录其分布式事务id,gxid;

b.(清理rocksdb)service扫描整个workspace的key,写入到本地的rocksdb中;

c.为提高可用性,可以在service未构建缓存前,提供fallback到直接访问FDB的模式;

2.client发起查询请求,请求包含两个信息,一个是事务信息,还有一个是zdbrelnode+seqno;

a.若client本地无缓存,则ZdbRelNode+seqno信息为空;

3.service接收请求,根据事务信息,得到该事务对应的当前的seqno,计算当前的 seqno和seqno之间的delta信息,将这部分delta内容返回给client;

4.client将最新的seqno和delta信息缓存在内存中,在下次查询时,使用最新的seqno信息;

a.注意,此处需要考虑到sortkey的查询,如果做本地缓存了,就没有必要将sortkey下推给gmeta service,而是直接拉全量数据到本地做裁剪;

GC流程:

  1. 在现有GC逻辑中,增加对每个relation的事务提交队列的清理逻辑,同时需要清理该事务对应的tmp space空间;

文件裁剪

数据的裁剪在OLAP类型的数据库中是一个重要的优化点,为了更进一步优化性能,我们把文件的sortkey编码到了文件名中,在检索文件的时候,实现了直接根据文件名做过滤,减少需要返回的文件数,同时也减少了打开文件再根据文件meta信息裁剪的开销。

减少读文件visimap请求

对于OLAP系统来说,数据写入后较少有修改,所以每个文件都去GMeta访问visimap是不合理的,特别是在高并发的AdHot查询中,高频访问gmeta会容易成为系统的瓶颈,所以我们在返回给worker的文件的时候,会带上文件是否有visimap的标志,读文件的时候如果文件没有visimap,就无需请求visimap。

经过上述的优化,在30TB TPC-H测试场景下:

  1. 查询获取fileinfo性能,依靠gmeta service cache方案,做到500ms以内。

  2. 查询获取visimap性能。初次总共耗时 600~700ms已经达到,依靠上述的gmeta client cache方案降低并发下网络带宽,一次查询带宽控制在1GB左右。

实时化

传统大数据的数据来源大部分是日志,对于TP数据库中的数据还是采用T+1全量同步的方式,无法满足实时数据大屏,实时运营分析场景下的需求,目前我们实现了Flink-CDC插件,支持分钟级的延迟的实时数据写入。对于有更高的ms级延迟的实时数据写入,并提供主键去重的能力,我们将在下一篇文章中详细介绍。

湖仓一体

对比湖方案,当前的元数据虽然在大规模数据集的性能上有一定的优化,但是开放性不足,后面我们会实现spark connector把当前的元数据和数据直接对spark计算引擎开发。

 总结

传统大数据从诞生的第一天起,就是存算分离,但是与数据库相比,大数据生态的存算分离在使用场景,无法很好的支持并发的增删改查;用户体验差,没有事务保证,需要用户自己来处理failover;需要多种产品和技术架构组合,带来使用的复杂性,提高运维成本。与其它数仓产品相比,Relyt支持“写写并发”,用户的update/delete与后台的vacuum能够并行,完整的事务ACID的能力,以及极致的性能,为客户在离在线一体数仓场景,提供一个更优的选择。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值