我们的业务场景是为了解决 CDC 数据的近实时同步,CDC 数据有个明显的特点,是存在大量的随机更新。这个场景下选择 COW,会导致写放大的问题比较严重,所以我们选择了 MOR 表。上图就是一个 MOR 表查询和写入的流程。第一个是列存储的基础镜像文件,我们称之为 Base 文件,第二个是行存储的增量日志,我们称之为 Log 文件。
每次查询时,需要将 Log 文件和 Base 文件合并,为了解决 MOR 表读放大的问题,通常我们会建一个 Compaction 的服务,通过周期性的调度,将 Log 文件和 Base 文件合并,生成一个新的 Base 文件。
Hudi 实时写入痛点
如图所示,这是原生的 Hudi 实时写入的流程图。
首先,我们接入 Hudi 数据,会进入 Flink State,它的作用是索引。Hudi 提供了很多索引机制,比如 BloomIndex。但是 BloomIndex 有个缺陷,它会出现假阳性,降级去遍历整个文件,在效率上有一定的影响。Flink State 的优势是支持增量更新,同时它读取的性能会比较高。经过 Flink State 之后,我们就可以确认这条记录是 Upsert,还是 Insert 记录,同时会分配一个 File Id。
紧接着,我们通过这个 File Id 会做一层 KeyBy,将相同 File 的数据分配到同一个Task。Task 会为每一个 File Id 在本地做一次缓存,当缓存达到上限后,会将这批数据 Flush 出去到 hoodie client 端。Hoodie client 主要是负责以块的方式来写增量的 Log 数据,以 Mini Batch 的方式将数据刷新到 HDFS。
所有的模块糅合在一个大的 jar 包中,包括引擎层、数据源层、基础框架层,模块耦合比较严重,数据处理流程也不清晰。针对这个问题,我们按照功能模块进行划分,将基础框架和数据源从引擎中独立出来,同时我们的技术组件采取可插拔的设计,以应对不同的用户环境,比如脏数据检测、Schema 同步、监控等等,在不同的环境中会有不同的实现方式。
4.1.2 接口抽象
框架对 Flink API 是深度绑定,用户需要深入到 Flink 引擎内部,这会导致整体 Connector 接入成本比较高。为了解决这个问题,我们抽象了新的读写接口,该接口与引擎无关,用户只要开发新的接口即可。同时在内部会做一层新的抽象接口与引擎接口的转换,这个转换对用户是屏蔽的,用户不需要了解底层引擎细节。