摘要:本文整理自 XTransfer 技术专家, Flink CDC Maintainer 孙家宝,在 Flink Forward Asia 2022 数据集成专场的分享。本篇内容主要分为四个部分:

  1. MongoDB 在实时数仓的探索
  2. MongoDB CDC Connector 的实现原理和使用实践
  3. FLIP-262 MongoDB Connector 的功能预览
  4. 总结和展望

点击查看原文视频 & 演讲PPT

一、MongoDB 在实时数仓的探索

MongoDB 是一款非关系型的文档数据库,支持大规模的数据存储和灵活的存储结构,在 XTransfer 内部有着比较大规模的应用。

另外,XTransfer 在实时数仓方面也有着积极的探索,除了目前比较流行的基于湖技术的构建实时数仓的方式外,Flink 和 MongoDB 也有着构建轻量级实时数仓的潜力。

1.1 MongoDB 简介

Flink CDC & MongoDB 联合实时数仓的探索实践_API

MongoDB 是一种面向文档的非关系型数据库,支持半结构化的数据存储。它还是一种分布式的数据库,提供副本集和分片级两种集群部署模式,具有高可用和水平扩展的能力,适合大规模的数据存储。另外,MongoDB 在 3.0 版本之后还引入了 Change Streams 特性,支持并简化了数据库的变更订阅。

1.2 常见的实时架构选型

Flink CDC & MongoDB 联合实时数仓的探索实践_SQL_02

  • Flink 和 Kafka 纯实时链路的实时数仓。

优势包括,数据新鲜度高;数据写入较快;Kafka 的周边组件生态较好。

缺陷包括,中间结果不可查。Kafka 是线性存储,记录了数据的每一次变更,因此如果要得到最新的镜像值,需要遍历所有在 Kafka 中的记录,因此也无法进行比较灵活快速的 OLAP 查询,对于排查问题方面也比较困难;Kafka 的冷热分离还有待实现,不能充分利用一些廉价存储;这套架构一般需要额外维护两套流批架构,对部署开发运维成本会较高。

Flink CDC & MongoDB 联合实时数仓的探索实践_大数据_03

  • 基于湖存储的实时数仓架构。

目前比较流行的数据湖 Iceberg、Hudi,同时支持了批式读取和流式读取的能力,可以通过 Flink 实现流批一体的计算能力,其次,湖存储在存储上会充分考虑如何利用廉价存储,相对于 Kafka 具有更低的存储成本。

但基于湖存储的实时数仓也有一些缺点,包括部署成本较高,例如需要额外部署一些 OLAP 查询引擎。其次,对于数据权限也需要额外的组件来支持。

Flink CDC & MongoDB 联合实时数仓的探索实践_SQL_04

  • 基于 MongoDB 的实时数仓。

MongoDB 本身支持大规模数据集存储,也支持灵活的数据格式;MongoDB 的部署成本低,组件依赖少,并且有完整的权限控制。相比于其他的实时数仓架构,Flink 和 MongoDB 也有着构建轻量级实时数仓的潜力。这种模式要求 Flink 对 MongoDB 拥有流式读取、批式读取、维表查询和行级写入的能力。

目前全增量一体化流式查询可以通过 Flink CDC MongoDB Connector 提供,批式读取维表查询写入的功能可以由 FLIP-262 MongoDB Connector 提供。

二、MongoDB CDC Connector 的实现原理和使用实践

2.1 MongoDB CDC Connector

Flink CDC & MongoDB 联合实时数仓的探索实践_API_05

MongoDB CDC Connector 由 XTransfer 基础架构团队开发,并已贡献给了 Flink CDC 社区。在 Flink CDC 2.1.0 版本中正式引入,支持了全增量一体化的 CDC 读取以及元数据提取的功能;2.1.1 版本中,支持连接未开启认证的 MongoDB;2.2.0 版本中,支持正则表达式筛选的功能;2.3.0 版本中,基于增量快照读框架,实现了并行增量快照读的功能。

2.2 Change Streams 特性

Flink CDC & MongoDB 联合实时数仓的探索实践_SQL_06

MongoDB CDC Connector 是基于 MongoDB Change Streams 特性来实现的。MongoDB 是一个分布式的数据库,在分布式的环境中,集群成员之间一般会进行相互复制,来确保数据的完整性和容错性。与 MySQL 的 Binlog 比较类似,MongoDB 也提供了 oplog 来记录数据的操作变更,次要节点之间通过访问主节点的变更日志来进行数据的同步。

我们也可以通过直接遍历 MongoDB oplog 的方式来获取数据库的变更。但分片集群一般由多个 shard 组成,每个 shard 一般也是一个独立的副本集。在分片上的 oplog 仅包含在它分片范围里的数据,因此我们需要遍历所有 shard 上的 oplog,并把它们根据时间进行排序合并,这显然会比较复杂。

值得一提的是,在 MongoDB 3.6 版本之后,引入了 Change Streams 特性,提供了更简单的 API 来简化数据订阅。

Flink CDC & MongoDB 联合实时数仓的探索实践_大数据_07

使用 Change Streams 的 API,我们可以屏蔽遍历 oplog 并整合的复杂度,并且支持实例、库、集合等多种级别的订阅方式,以及完整的故障恢复机制。

2.3 Change Streams 的故障恢复

Flink CDC & MongoDB 联合实时数仓的探索实践_大数据_08

MongoDB 通过 ResumeToken 来进行断点恢复,Change Streams 返回的每条记录都会携带一个 ResumeToken,每个 ResumeToken 都对应了 oplog 中的一条具体记录,表示已经读到的 oplog 的位置。另外,还记录了变更时间以及变更文档主键的信息。通过 ResumeAfter、startAfter 等方法,将 ResumeToken 作为起始参数可以对中断的 Change Streams 进行恢复。

Change Streams 的 ResumeToken 是由 MongoDB KeyStream 编码的一个字符串,它的结构如上图左侧所示。ts 代表数据发生变更的时间,ui 代表发生变更 collection 的 UUID,o2 代表发生变更的文档的主键。详细的 oplog 字段描述可以参考 oplog_entry

上图右侧是一个 oplog 的具体记录,它描述了在 107 结尾主键下的一条记录的一次更改,将 weight 字段修改成了 5.4。值得一提的是 MongoDB 在 6.0 版本中并没有提供变更前和变更后完整的镜像值。这也是我们没有直接采用 oplog 去实现 MongoDB CDC Connector 的一个原因。

2.4 Change Streams 的演进

Flink CDC & MongoDB 联合实时数仓的探索实践_大数据_09

MongoDB 在 3.6 版本中正式引入了变更流特性,但仅支持对于单个集合的订阅。在 4.0 版本支持了实例、库级别的订阅,也支持了指定时间戳开启变更流的功能。在 4.0.7 版本引入了 postBatchResumeToken:

在 4.0 版本之前打开一个变更流后,如果没有新的变更数据产生,那么将不会获取到最新的 ResumeToken。如果此时发生故障,并且尝试使用了比较老旧的 ResumeToken 来恢复,可能会降低服务器的性能,因为服务器可能会需要扫描更多的 oplog 的条目。如果 ResumeToken 对应的 oplog 被清除了,那么这个变更流将无法进行恢复。

为了解决这个问题,MongoDB 4.0 提供了 postBatchResumeToken,标记已经扫描的 oplog 的位置,并且会随时间持续推进。另外,利用这个特性,我们可以比较准确的定位当前 Change Streams 消费的位置,进而实现增量快照读的功能。

在 MongoDB 4.2 版本,可以使用 startAfter 去处理一些 invalid 的事件,在 MongoDB 5.1 版本对 Change Streams 进行了一系列的优化。在 MongoDB 6.0 版本,提供了 Change Streams 前置、后置镜像值完整信息,以及 Schema 变更的订阅机制。

2.5 MongoDB CDC Connector

Flink CDC & MongoDB 联合实时数仓的探索实践_SQL_10

MongoDB CDC Connector 的实现原理,是利用了 Change Streams 的特性,将增、删、改等变更事件转换成 Flink 的 upsert 类型的变更流。在 Flink SQL 场景下,Planner 会加上 Changelog Normalize 的算子,将 upsert 类型的变更流进行标准化。结合 Flink 强大的计算能力,容易实现实时 ETL 甚至异构异构数据源的计算场景。

Flink CDC & MongoDB 联合实时数仓的探索实践_API_11

在 Flink CDC 2.3 版本,依托于增量快照读框架实现了无锁快照读的功能,支持并发快照,大大缩短了快照时间。关于增量快照读的总体流程是如上图所示。为了让 snapshot 并行化,首先要将完整的数据集切分成多个区块。将这些区块分配给不同的 Source Reader 并行读取,以提升整个 snapshot 的速度。但 MongoDB 的主键它多为 ObjectId,不能按照简单的增加范围的方式去切分,因此对于 MongoDB 的切分策略需要单独去设计。

Flink CDC & MongoDB 联合实时数仓的探索实践_大数据_12

MongoDB 有以下三种切分策略,这些切分策略参考了 Mongo Spark 项目。

  • 第一种切分策略使用了 Sample 命令对集合进行随机采样,再通过文档的平均大小计算出分桶数量。然后将采样数据分配到各个桶中,构成 Chunk 的边界。优点是速度快,适用于数据量大且未分片的集合。缺点是采用抽样预估,Chunk 的大小不能做到绝对均匀。
  • 第二种切分策略使用了 SplitVector 命令。SplitVector 是 MongoDB 分片式计算分裂点的内部命令,通过访问指定索引来计算每个节点 Chunk 的边界。优点是速度快,并且 Chunk 的大小均匀,但它额外需要 SplitVector 命令的执行权限。
  • 第三种切分策略针对于分片集合,对于已经分片好的集合,我们不用重新计算它的分片结果,可以直接读取 MongoDB 已经分片好的结果作为 Check 的边界。优点是速度快,Chunk 的大小均匀,但是 Chunk 的大小无法调节,依赖于 MongoDB 自身对于每个分片的配置,默认大小为 64mb,另外它额外需要对 config 库的读取权限。

Flink CDC & MongoDB 联合实时数仓的探索实践_SQL_13

接下来介绍一下增量快照读的过程。对于一个已经切分好的区块,在快照执行前后分别记录当前 Change Streams 的位置。在快照结束之后,根据快照起始、结束的位点范围,对变更流进行回放,最后将快照记录和变更记录按 Key 进行合并,得到完整的结果,避免了重复数据的发送。

Flink CDC & MongoDB 联合实时数仓的探索实践_API_14

在单个 Chunk 的增量读阶段,我们读取了 Chunk 范围内的快照数据以及 Chunk 范围内的增量数据,并将其进行合并。但整体的 snapshot 的过程可能并没有结束,那么已经完成 snapshot 的区块,在后边的时间仍然可能会发生变更,因此我们需要对这些变更数据进行补偿。从全局最低的高水位点处开始启动变更流,对于变更时间高于 Chunk 高位点的变更数据进行补偿。当达到全局 snapshot 最高位点的时候,我们的补偿便可以结束。

Flink CDC & MongoDB 联合实时数仓的探索实践_Flink_15

接下来介绍一些关于 MongoDB CDC Connector 的生产建议。

  • 第一,使用增量快照特性,MongoDB 的最小可用版本在 4.0 版本。因为在 4.0 版本之前,没有发生变更时无法获取 ResumeToken,且也不能够从指定的时间点进行启动,因此难以实现增量快照特性。
    在 MongoDB 4.0.7 版本之后,引入了 postBatchResumeToken,可以比较容易的获取当前 Change Streams 的位置,因此比较推荐的版本在 4.0.7 以上。

Flink CDC & MongoDB 联合实时数仓的探索实践_API_16

  • 第二,控制文档的大小不要超过 8mb,因为 MongoDB 对单条文档有 16 mb 的限制,变更文档因为包含一些额外的信息,比如修改的字段是哪些等等,即使原文档没有超过 16mb,变更文档也会超过 16mb 的限制,从而导致 Change Streams 异常终止。这个应该属于 MongoDB Change Streams 的一个缺陷。
    关于 MongoDB 的变更文档可以超过 16mb 的限制,已在 MongoDB 的 issue 中进行推进。

Flink CDC & MongoDB 联合实时数仓的探索实践_数据_17

  • 第三,在 MongoDB 中分片件其实在开启事务之后允许被修改。但修改分片键可能会引起分片的频繁移动,引起额外的性能开销。另外,修改分片键还可能导致 Update Lookup 功能失效,在 CDC 的场景中可能会导致结果的不一致。

三、FLIP-262 MongoDB Connector 的功能预览

上面我们介绍了 MongoDB CDC Connector,可以对 MongoDB 进行增量的 CDC 读取,但如果要在 MongoDB 上构建实时数仓,我们还需要对 MongoDB 进行批量读取、写入以及 Lookup 的能力。这些功能在 FLIP-262 MongoDB Connector 中进行实现,目前已经发布第一个版本。

3.1 FLIP-262 Introduce MongoDB Connector

Flink CDC & MongoDB 联合实时数仓的探索实践_数据_18

在并行读取方面,MongoDB Connector 基于 FLIP-27 新的 Source API 实现;支持批量读取;支持 Lookup。在并行写入方面,基于 FLIP-177 Sink API 实现;支持 Upsert 写入。在 Table API 方面,实现了 FLIP-95 Table API 使用 Flink SQL 进行读取或写入。

3.2 读取 MongoDB

Flink CDC & MongoDB 联合实时数仓的探索实践_大数据_19

首先我们在 MongoDB 中插入一些测试数据,然后使用 Flink SQL 定义一张 users 表,通过 select 语句我们可以得到右边所示的结果。可以发现右边的结果和我们插入的测试数据是一致的。

3.3 写入 MongoDB

Flink CDC & MongoDB 联合实时数仓的探索实践_SQL_20

首先我们定义一张 users snapshot 的结果表,对应 MongoDB users snapshot 的集合。然后我们通过 Flink SQL 的 insert 语句,将上面定义的 users 表集合的数据,读取并写入到 MongoDB。

最后查询一下我们新定义的这张结果表,它的结果如右边所示。可以发现它的结果和之前源表的结果是一致的,这代表着我们写入一张新的集合是成功的。

3.4 用作维表关联

Flink CDC & MongoDB 联合实时数仓的探索实践_Flink_21

接下来来演示一下,将上面定义的 user 表作为维表进行 Lookup 的场景。

首先我们定义一张 pageviews 的事实表,user_id 作为 Lookup Key,对应于我们之前定义的 users 表的主键。然后我们查询 pageviews 表可以得到右边的结果。

Flink CDC & MongoDB 联合实时数仓的探索实践_大数据_22

接着定义一张结果表代表打款以后的结果,这个结果表对于 users 是作为维表关联去补充一些区域信息。然后我们通过 Flink SQL 将 pageviews 事实表和 users 维表进行关联,写入到结果表。然后查询结果表可以得到打宽后 user_region 的信息。如右图所示,打宽以后的 user_region 在最后一列,这说明我们的 Lookup 是成功的。

四、总结和展望

4.1 总结

Flink CDC & MongoDB 联合实时数仓的探索实践_大数据_23

至此,Flink 联合 MongoDB 的实时数仓架构便可以实现,在建设实时数仓时多了一份选择。如图所示,通过 CDC Connector 完成整套流式链路,辅助 Lookup 进行数据打宽。通过 Source Connector 完成一整套批式链路,最后将计算的中间结果通过 Sink Connector 进行存储,那么整套实时数仓的架构便得以实现。

4.2 存在的问题

目前还存在着以下问题:

  • Changelog Normalize 是一个有状态的算子,它会一些额外的状态开销。
  • Update Lookup 的完整状态提取,也需要一定的查询开销。
  • 在 MongoDB 中的文档大小有 16mb 限制。如果有一些很大的单条数据,那么可能并不适合采用这种架构。

4.3 未来规划

在 MongoDB CDC Connector 方面,我们需要:

  • 支持 MongoDB 6.0 版本。
  • 支持指定时间点启动。
  • 推进推进 Changelog Normalize 优化。

在 MongoDB Connector 方面,我们需要:

  • 支持谓词下推。
  • 支持 AsyncLookup。
  • 支持 AsyncSink。

点击查看原文视频 & 演讲PPT