游戏公司吐槽:业内竟然没有应对这个场景的实时数仓
数据分析新范式:像使用单机数据库一样使用数仓
在一次国内 Top10 游戏厂商的拜访中,了解到客户数仓平台不到 5 个人,一半员工毕业不到 3 年,技能和资源非常有限,但是却在管理着整个游戏公司 PB 级别的数据,客户希望能把数仓当做一个“大号的数据库”一样使用,并吐槽业界没有能解决好以下 2 个典型分析场景的“实时数仓”。
场景 1:订单表实时分析。千亿订单表的实时入库和实时查询,最近 7 天数据有 30%的 update 和 delete。
场景 2:源端上百个 RDS 实例数据的实时入库和实时分析。需要实时同步到目标端的数仓中。
以上场景对传统数仓的挑战在于:
写入包含大量 update 操作。订单表不仅仅是 insert,订单记录的状态更新,每条订单都会有从创建->支付->发货->签收的状态变更,相当于每条写入的记录会被更新 5 次。
高并发写入。源端会有成百上千的 RDS 实例,对应成百上千的并发。
主键去重。订单表有订单 id 做为主键。
写入能够保证事务的 ACID,用户写入成功的数据立即可查,即使有节点故障数据也不应该丢失,否则会导致数据分析结果的错误。
高性能的列存和向量化执行引擎,具备业界第一阵营实时分析性能。
实时写入成本可控,不需要为业务流量高峰设置资源,能够按需的使用资源。
这些需求在大厂一般通过 lambda 架构,或者降低数据新鲜度都可以找到一些解决方案。但是对于中小厂商来说,如何提高数据分析的易用性,降低用户使用的成本,提升分析的时效,促使我们重新思考数仓,如何实现一款像单机数据库一样的数仓产品。在进入质变科技Relyt AI-ready Data Cloud的详细设计之前,我们来看看业界已有的一些方案。
业界类似方案
数据湖方案
Merge on Read
当获取 CDC 数据,排序后直接写入新的文件,不做重复键检查。读取时通过 Key 值比较进行 Merge,合并多个版本的数据,仅保留最新版本的数据返回。
Merge-on-Read 方式写入数据非常快,但是读数据比较慢,对查询的影响比较大。它参考的是存储引擎比较典型的、应用广泛的 LSM 树的数据结构。
Hudi 的 MOR 表以及 StarRocks 的 Unique key 表都是使用这种方式是实现的。
Copy on Write
当获取 CDC 数据后,新的数据和原来的记录进行 full join,检查新的数据中每条记录跟原数据中的记录是否有冲突(检查 Key 值相同的记录)。对有冲突的文件,重新写一份新的、包含了更新后数据的。
读取数据时直接读取最新数据文件即可,无需任何合并或者其他操作,查询性能最优,但是写入的代价很大,因此适合 T+1 的不会频繁更新的场景,不适合实时更新场景。
Delta Lake,Hudi 的 COW 表,Iceberg 以及商用的 Snowflake 都是使用这种方式达到数据更新目标。
这里还存在一个问题,就是频繁更新的场景,会造成大量小文件,小文件对对象存储极不友好,性能和成本剧增。此外对于主键去重的需求,读的时候再做去重会导致读的性能下降。
业界的普遍的解法是在数据源端和数据湖中间通过 kafka 构建一层聚合和缓冲层,通过在 kafka 做数据聚合和攒批,再落入数据湖,对于修改删除的数据,再通过上述的 MoR 或者 CoW 的方式来去重,上层通过假设 hive/spark/presto 来做分析。对于 update/delete 特别频繁的数据这种方式不太可行,通常会与业务方沟通后,通过降低数据的新鲜度,变成 T+1 的方式。
对于有资深和庞大团队的大厂来说,每个组件都有专门的团队来维护,虽然系统复杂,但是具有开放性的好处,这种方案得到大量的应用。
实时数仓 doris/starrocks 的方案
doris/starrocks 是业界开源的 OLAP 数仓方案,在实时分析上做了非常多的工作,成为众多厂商数据分析产品库中,补齐“实时分析”短板的一员。【1】
系统的 overview

打开 tablet 的实现

Rowset:一个 Tablet 的所有数据被切分成多个 Rowset,每个 Rowset 以列存文件的形式存储,可以理解成 parquet 文件的一个 rowgroup。多个 rowset 可以保存在一个文件中。
Meta(元数据):保存 Tablet 的版本历史以及每个版本的信息,比如包含哪些 Rowset 等。序列化后存储在 RocksDB 中,为了快速访问会缓存在内存中。
DelVector:记录每个 Rowset 中被标记为删除的行,同样保存在 RocksDB 中,也会缓存在内存中以便能够快速访问。可以理解成 visimap,在 doris 中每个 rowset 对应一个或者 0 个 DelVector。
Primary Index(主键索引):保存主键与该记录所在位置的映射。目前主要维护在内存中,正在实现把主键索引持久化到磁盘的功能,以节约内存。
doris/starrocks 的方案的问题在于:
delta 数据使用基于内存的 memtable 来存储,在典型的 RDS 多库同步的场景下,表的数量以千/万计的情况下,doris 的 BE 的内存很快耗尽,doris 的 BE 的内存很快耗尽,例如 1000 个表同时写入,每个 memtable 10MB,每个 BE 需要 10GB 内存,造成节点 OOM,系统不可用。
从目前的公开资料来看 PK 也是全内存,并没有落盘,节点故障会导致重建索引耗时,特别是千亿大表的场景显然不适合。
社区显然也考虑到了这些问题,但是数据落盘部分,数据的高可靠和高性能有着巨大的工作量,这个场景并不是他们核心的场景,所以部分能力(主键落盘)在规划中有体现,但是还未落实。【1】
Relyt AI-ready Data Cloud 技术方案
总体方案

Relyt 以对象存储作为主存,实现了数据,缓存,计算和元数据 4 层分离的架构,其中 Hybrid DPS 用来实现实时的写入和去重。Extream DPS 实现高性能查询和 ETL。NDP 实现数据的高速缓存和缓存上的计算,例如谓词下推和编解码,数据以对象存储作为主存。
进一步打开这个架构:

Hybrid DPS 的核心是行存的 Delta,加上以对象存储列存为核心的 Base 数据构建的实时引擎。实时写入的数据首先落在行存上,并基于 btree 索引做主键去重,后台任务会定期把数据刷入 Base 的列存,在刷入的过程中同时会做数据的合并,排序和删除。Extreme DPS 在查询的时候,会同时读取 Delta+Base 两部分数据。
整体的方案的原理非常简单,但是在实际的工程实践中,我们面临了巨大的挑战。
写写冲突带来的相互阻塞问题。通常的 delta 方案用户删除的时候,后台的 flush 行转列也需要对 delta 数据做删除,也就是同时有 2 个事务会需要写入,通常的解法要么是加锁等待,要么是 abort 其中一个事务,都会导致用户体验的下降。
Extreme DPS 读取 Delta 部分数据带来的带宽和时延问题。为了保证 delta 部分数据的攒批效果,extreme dps 需要保证 delta 部分的数据量,否则会导致大量的小文件,而 delta 部分的数据量增大又会导致 extreme dps 读数据的时候的大带宽,导致读并发起不来。
扩缩容时行存数据的重分布问题。还是为了保证 delta 部分 flush 到列存的攒批效果,行存本身是有部分数据量的,而用户实际的表的个数又是比较多,传统方案例如 Greenplum 是需要重新搬一次数据,导致扩容时间过长,并且扩容过长中 IO 压力剧增影响用户本身的业务。
模块设计
下面我们把上面的 delta+base 的架构做进一步打开,在进入这一节之前,读者可以先看看,我们如何把列存的数据与元数据事务解耦。【1】我们这里就不做复述,只介绍如何实现数据库的高并发实时更新。

如上所述实时写入部分分成 Delta+Base,其中 Delta 基于 PostgreSQL 的行存引擎来实现,Base 基于自研的列存引擎。Delta 部分进一步打开:
Delta data:实时写入的数据会以 append only 的方式写入 delta data,每条 delta 的数据,会分配一个唯一的 tuid 用来实现删除;
tomestone:删除的数据会以 tomestone 的方式写入,也是以 appendonly 的方式写入 tuid。
deletequeue:删除的记录如果已经 flush 到列存文件上,那么同时还会写入 deletequeue,用来生成列存的 visimap。
PK:delta+base 部分的数据的主键保存在 PK 中,用经典的 Btree+来存储,这部分我复用了 PostgreSQL 的能力。
列存部分:
ColumnStorageFile:PAX 的行列混存。
Delete bitmap:ColumnStorageFile 的标记删除数据。
写入流程
首先在 PK 查询是否有重复的数据,如果没有则写入 delta data 并在 PK 中新增记录,如果有重复记录,对于 copy on conflict do update 场景,会根据 PK 查询记录的 tuid,并把 tuid 写入 tomestone 做标记删除,再生成新的 tuid 并写入新的数据。
查询流程
Delta data+tomestone 去重后,以列存的形式返回
ColumnStorageFile 会读取 delete visimap,deletequeue 中如果还有未写入 delete visimap 的数据,会先进行 visimap 合并。
后台 flush 和 merge 流程
Delta data+tomestone 的数据当达到设定大小或者定时写成列存。
deletequeue 里面的数据会定时写入 delete bitmap。
同时 ColumnStorageFile 在有多个小文件或者 delete bitmap 删除的数据过多的情况下也会进行合并,合并的过程也会清理 tomestone 中的记录。
方案小结
这样做的好处在于:
delta 表是一个 appendonly 的结构,删除的时候是往 tomestone 表插入数据而不是对 delta data 中的数据做删除,tomestone 表中的数据只有 flush 和 merge 的时候才做删除,这样我避免用户的删除和后台 flush 的写写冲突的问题,而 flush 和 merge 我们可以通过后台表级别并发避免冲突。
为了解决 Extreme DPS 读取 Delta 部分数据带来的带宽和时延问题,我们给 Extreme DPS 配置了一个只读副本,这个只读副本通过消费 PostgreSQL 的 WAL 获取当前最新的 LSN,通过 LSN 去 Pageserver 上读 page 数据,并按 lsn 版本号缓存在本地,通过这种方式,我们解决了 Extreme DPS 到 Hybird DPS 读数据的带宽和延迟的问题。
为了解决 Delta 行存部分的扩缩容需要重分布导致的搬数据问题,我们把重分布改成 flush 操作,把 delta 部分数据写入列存,列存文件是带 shard 信息,只需要修改元数据重新映射计算节点和 shard 关系即可。对于 PK 信息,只需要读取 PK 列和 tuid 列在新的节点重建即可。通过这种方式,我们避免了重新搬数据导致的长时间不可用问题。
当然这个方案并不是完美,tomestone 实现删除,在有大量删除的场景下,有较大的写入放大问题,所以针对大量删除(全表删除)的场景,我们会自动退化到写列存,生成 visimap 的场景。写列存如果整个文件被删除,又会退化到文件删除,而不是写 visimap。此外,对于 visimap 的读取我们也做了大量的优化,具体可以参考【3】。此外上述的行存我们通过参考 Neon 的架构实现了存算分离。【4】
其它工作
除了上述的基础工作,为了实现 TP 库源端数据的实时写入,我们为了进一步优化用户体验和成本,还对 alter table 和 serverless 做了一些工作。
alter table
源端的 TP 库大部是 MySQL,而我们提供的 PostgreSQL 的语法,为了适配 MySQL,我们做了一些语法兼容的工作,例如 alter table add/modify column first/after 这样的语法,方便 MySQL 的用户集成。
spot 实例支持和自动启停
我能的计算和 Pageserver 的节点都是无状态的,所以我们把这些节点部署在 spot 实例上,spot 实例的成本大概为同等实例的 1/10~1/5,帮助我们大幅降低了成本,对于一些有典型波峰波谷的业务,我提供了自动启停的实例,用不不使用的情况下,保存用户数据,释放计算节点,并根据用户的请求按需拉起,进一步降低了成本。
性能对比
写入能力
测试的写入环境,约等于 32C。
将 TPC-H 的 150GB 的 lineitem 表分成 20 份,然后开 20 线程并发导入。
测试结果:
RPS 结果如图:

写入的吞吐:

写入能力基本与 Apache Doris 2.0 基于 3 副本本地盘的性能相当【2】。我们并没有对性能再做更极致的优化,在没有冲突的写入做到上面的性能 7.5MB/s/core 基本已经够用,Relyt 的差异化在于实现高并发小批量高频更新的 TP 库的稳定数据同步场景。
读能力
我们在 TPCH 场景下的测试结果如下:

查询能力可以排到 TPC-H 榜单的第一阵营。
此外我们的读写节点是典型的 MPP 架构,读写能力具备线性可扩展能力。
总结
综上所述,质变科技Relyt AI-ready Data Cloud为数据分析提供了一个更多的选择,我们的目标并不是与数据湖,或者已有的 OLAP 引擎擅长的方向去竞技,我们希望为数据分析提供一种新的范式,让用户能够像使用单机数据库一样简单使用数据分析。而不需要自己构建一个庞大的基础设施团队,不需要因为成本而牺牲数据的新鲜度,把 T+0 的分析变成 T+1。
上述方案经过接近 2 年时间的打磨,经过和种子客户的共建,经历了从 10 个源端库扩展到超过 100 个源端库的数据增长,以及多次洗表导致的大量删除,高频的 alter table 等极端场景的磨炼,目前趋于成熟。
引用:
【1】https://zhuanlan.zhihu.com/p/566219916
【2】https://www.selectdb.com/blog/106
评论