博主历时三年精心创作的《大数据平台架构与原型实现:数据中台建设实战》一书现已由知名IT图书品牌电子工业出版社博文视点出版发行,点击《重磅推荐:建大数据平台太难了!给我发个工程原型吧!》了解图书详情,京东购书链接:https://item.jd.com/12677623.html,扫描左侧二维码进入京东手机购书页面。 |
目前看,Flink 的 “维表 Join” 主要就三种实现方式,叫法可能会有细微差别,以下是我是用更直白的语言总结的称谓:
- 直连外部数据库进行关联
- 将维表加载到内存中关联
- 基于维表变更日志的关联
这些 Join 方案具体会使用到 Flink 的 Lookup Join、Temporal Join 等相关机制,所以在研究维表 Join 方案前,应先补齐这部分的知识,具体可参考本文末给出的本博客相关系列文章。
网上有这样一篇文章《Flink DataStream 关联维表实战》,总结得已经非常好了,我们不打算重新发明轮子,本文更像是一篇学习笔记。
1. 直连外部数据库进行关联
顾名思义,这种方式就不需要再解释了。Flink 中的 Lookup Join 就是应用在这种场景下的。我们来分析一下这种关联方式的优缺点,应该说它的优缺点是非常鲜明的:
-
优点
- 实现简单 => 是所有管理方式中最容易实现的
- 实时性高 => 维表发生变更后能立即反映到关联的结果集中
- 不占用内存 ( 能够关联较大的维表 ) => 维度数据是直接从数据库查询获得,不会占用 Flink 工作节点的内存,这也就意味着能关联较大的维表
-
缺点
- 数据库负载高 ( 容易成为性能瓶颈 ) => 流上的 Join 是持续查询,对维表数据库的访问会异常频繁,极易导致性能问题
-
适用场景:
-
数据流量不大的流
-
项目初期的原型快速实现
-
鉴于直连外部数据库进行关联对数据库的压力过大,有一些优化措施可以适当缓解一下数据库的压力,具体地说主要是“异步查询“和”引入缓存“两种优化措施,而上面说的默认实现方式是”同步查询“,这样,在该选型下,总共有三种细分方案。
1.1. 同步查询
没有任何特别之处,默认情况下,使用客户端查询数据库都是同步模式。同步模式的性能问题会尤为突出,没有做任何优化。
1.2. 异步查询
要想在 Flink 中异步访问数据库,首先需要目标数据库本身支持异步查询(提供异步查询的客户端、类库),然后,再基于 Flink 的 Async I/O API 进行一层封装才能实现异步查询。异步查询可以显著提升查询的吞吐量,但不能保准顺序性,所以在维度数据变更前后有可以会出现关联了错误版本的维度数据(无序性),通常,维表的变化速率都不会太快(缓慢变化维度),一般的应用是可以接受这种情况的,且流上的数据进入流计算引擎时本身就已经有了一定的时延,所以异步导致的问题基本都是可以忽略不计的。如果对关联的时间尺度要求极高,则应考虑”基于维表变更日志的关联“。
1.3. 引入缓存
和 Web 应用中引入缓存加速数据读取性能一样,Flink 程序也可以引入缓存实现同样的目标。这部分的实现其实和 Flink 本身的 API 没有太大关系,只是运行环境是 Flink 罢了,这个命题其实可以改为:在 Java 程序中有没有好的缓存框架以及如何使用。这一块可以研究一下 Guava Cache,看上去是目前 Java 平台上比较主流的缓存框架了。当然,引入专门的分布式缓存基础设施也是可考虑的方案之一。
引入缓存的收益是非常明显的,只是如何保持缓存数据和数据库的数据一致是比较麻烦的,同异步查询一样,如果对关联的时间尺度要求极高,在没有很好的缓存更新策略下,还是应考虑”基于维表变更日志的关联“。
2. 将维表加载到内存中关联
“将维表加载到内存中关联” 像是 “直连外部数据库实时关联” 的反向极端,它的优缺点也是非常鲜明的:
-
优点
- 极致的性能 => 不会有这种方式更快的关联方式了
- 高吞吐量 => 数据驻留内存,吞吐量瓶颈只取决于 Flink 集群自身
- 数据库负载低 => 对数据库的压力几乎可以忽略不计
-
缺点
- 高内存占用 => 占用过多内存,不能加载过大,过多的维表
- 实时性差 => 源表维度数据发生变更后,内存中的数据感知不到,只能手动触发更新或定时刷新
-
适用场景
- 体量小且几乎不会变更表,例如:字典表
在《Flink DataStream 关联维表实战》一文中还详细介绍了该方法下的几种优化方案,包括:只加载特定分区的数据缓解内存压力,如何进行定时的数据刷新,以及将加载数据到内存与直连数据库两种方法结合起来使用,详情可参考原文。
3. 基于维表变更日志的关联
基于维表变更日志的关联就是 Flink 的 Temporal Join,所以这种方法会细分为:
- 使用基于事件时间的 Temporal Join 关联维表
- 使用基于处理时间的 Temporal Join 关联维表
基于事件时间的维表关联,在时间尺度上是最严格,最准确的,它能确保关联到事实表数据所代表的事件在发生时维表上当时的对应数据,如果业务场景的维表变更速率很快,业务上又不容许任何时间上的错误关联(例如关联汇率表进行汇率换算),则这是唯一的关联方式。
基于处理时间的 Temporal Join 关联维表在时效性上其实也能满足绝大多数的场景需求,因为大多的维表变更速率都不快(缓慢变化维度)。基于基于处理时间的 Temporal Join 和 直连数据库的 Lookup 非常像,以至于有人错误地将它们认成是同一种关联方式,它们的主要区别是:Lookup Join 是直连数据查询的,而 “基于处理时间的 Temporal Join” 是构建在 Flink 上的动态表,变更是靠 CDC 实时同步的;Lookup Join 需要高频访问数据库,对于数据库会造成较高的负载,而“基于处理时间的 Temporal Join”维表数据是在流上,维表变化是通过 CDC 更新的,所以,后者不会对维表数据库造成压力。
-
优点
- 实时性高 => 尤其基于事件时间的 Temporal Join,实时性和准确性是最高的
- 数据库负载低 => 对数据库的压力几乎可以忽略不计
-
缺点
- 内存占用高 => 其内存占用没有“将维表加载到内存中关联”那样高,但是对于大的维表,为了维持状态,依然会占用相当可观的内存
-
适用场景
- 对关联的实时性和时态上的准确性要求严格的场景,如:关联汇率表计算汇率
关于这种方式的实现难度其实还好,主要是前期要积累足够的知识,包括 Flink CDC, Upsert-Kafka,Temporal Join 等,实现的代码并不多,只是知识密度比较高。
3.1 使用基于事件时间的 Temporal Join 关联维表
关于这种实现方式,请参考本博客另外两篇文章:
- 《Flink Temporal Join 系列 (1):用 Temporal Table DDL 实现基于事件时间的关联》
- 《Flink Temporal Join 系列 (3):用 Temporal Table Function 实现基于事件时间的关联》
3.2 使用基于处理时间的 Temporal Join 关联维表
关于这种实现方式,请参考本博客另外两篇文章:
-
《Flink Temporal Join 系列 (2):用 Temporal Table DDL 实现基于处理时间的关联》
-
《Flink Temporal Join 系列 (4):用 Temporal Table Function 实现基于处理时间的关联》
参考资料
- 《Flink DataStream 关联维表实战》