背景:双流 Join 的状态问题
在 Flink 中,双流 Join 是流式数据处理中常见的需求,用于将两条流的数据基于某些关联条件合并起来。例如,在电商场景中,可以将用户的曝光流与订单流 Join,用于分析广告效果。然而,双流 Join 通常需要在 State(状态) 中保存全量的历史数据,以便为未来的 Join 操作提供上下文。这种设计在大规模数据处理中存在以下问题:
状态过大:
- 假设淘宝的一个应用场景,有两个数据流:
- 曝光流:记录广告曝光事件,每天产生 10 亿条记录。
- 订单流:记录用户下单事件,每天产生 1 亿条记录。
- 双流 Join 需要在状态中存储曝光流的所有记录(50 亿条,约 10TB)以及订单流的所有记录(1 亿条,约 1TB)。因此,状态总量可能达到 50TB。
- 这导致 Checkpoint 非常耗时(可能需要数小时),并增加作业失败后的恢复时间。
高成本:
- 状态需要存储在内存或外部状态后端(如 RocksDB)中,这大大增加了集群的资源消耗。
- 淘宝某 Flink 作业消耗的资源达到 2300 CU,成本高昂。
作业不稳定:
- 在高并发和高吞吐量下,状态的膨胀可能导致 Checkpoint 超时或任务失败,进一步增加了系统不稳定性。
我们先来解释一下,为什么在Flink 双流 Join 的时候通常需要有大量数据需要保存在 State 里面,看下面一个数据场景:
流数据的无界性
- 流数据是无界的:流式数据是持续不断的,数据源不会终止。因此,Flink 必须在内存(或持久化到外部存储)中维护一部分历史数据,以便在未来的数据到达时能够完成 Join。
- 无法预先确定 Join 的匹配时间:例如,在广告曝光与订单的 Join 中,可能一个用户今天看到广告,明天才下单。如果不保存过去的广告记录,就无法将订单与正确的广告关联。
例子:电商场景
假设我们有两个数据流:
-
曝光流:记录用户的广告曝光,例如:
曝光流: {"ad_id": 1, "user_id": 101, "time": "2025-01-08T10:00:00"} {"ad_id": 2, "user_id": 102, "time": "2025-01-08T11:00:00"}
-
订单流:记录用户的下单,例如:
订单流: {"order_id": 1001, "ad_id": 1, "user_id": 101, "time": "2025-01-08T12:00:00"}
在这个场景中:
-
曝光事件和订单事件之间存在时间延迟(2小时)。
-
为了完成 Join,Flink 必须将曝光流的所有记录保存起来,直到订单流中匹配的数据到达。
Join 的对称性
- 双流 Join 是对称的:无论是哪一条流先到,Flink 都需要找到另一条流中是否有匹配的记录。
- 保存上下文:为了满足这种对称性,无论是左流还是右流的记录到达,Flink 都需要保存所有的历史记录以供匹配。
例子
- 曝光流到达时:Flink 必须检查当前是否有订单流中匹配的数据。
- 订单流到达时:Flink 必须检查之前的曝光流中是否有匹配的数据。
- 因此,必须保存两条流的历史数据。
窗口的限制
- 非窗口化的 Join:如果 Join 没有时间范围限制(如窗口),Flink 必须保存所有的历史数据。
- 窗口化的 Join:即使有窗口(如过去 1 小时的数据),Flink 仍需要保存窗口内的数据,直到窗口关闭。
例子:无窗口的 Join
假设没有时间窗口限制,用户的订单可能在一天甚至一周后才到达,那么所有的曝光数据都必须保存。
例子:有窗口的 Join
如果设置窗口为过去 1 小时:
- Flink 只需要保存最近 1 小时内的曝光数据。
- 但窗口过期之前,这些数据仍然需要保存在状态中。
上面这些例子可能就是我们在使用 Flink Join 的时候会使用State 保存大量数据,从而使得 Flink State 会膨胀很多倍。
Fluss 解决办法 -Delta Join
Fluss 的 CDC 流读+索引点查的能力研发了一套新的 Flink 的 Join 算子实现,叫 Delta Join。Delta Join 可以简单理解成“双边驱动的维表Join”,就是左边来了数据,就根据Join Key去点查右表;右边来了数据,就根据 Join Key 去点查左表。全程就像维表Join一样不需要state,但是实现了双流Join一样的语义,即任何一边有数据更新,都会触发对关联结果的更新
CDC 流读:Fluss 通过 CDC(Change Data Capture)实时捕获表的变更事件,并将这些事件作为流输入 Flink 作业。
- 示例:假设有一个订单表,包含以下数据:
当订单状态更新时,例如订单 1001 的状态从 “pending” 变为 “completed”,这条数据的变更会先存储到 Fluss 的LogStore 和KvStore 然后Fluss 通过CDC 会捕获这一变更,并将其作为事件发送到 Flink。
索引点查:Fluss 将数据存储在其内部的高效存储引擎中,支持基于主键的快速点查。当任意一边有新数据到达时,通过 Join Key(如 user_id
)实时点查另一边的数据。
- 示例:假设有一个用户表,包含以下数据:
当订单流中出现新的订单,例如:
Flink 会根据 user_id=101
到 Fluss 中点查用户信息,获取对应的用户数据:
双边驱动:无论是左流(如订单流)还是右流(如用户流)有变更,都会通过实时点查实现双流 Join 的更新,而无需将全量数据加载到内存中。
- 示例:如果用户表中新增一条记录:
当订单流中出现对应的订单时:
Flink 会根据 user_id=103
点查用户信息,获取 “Charlie” 的数据,实现实时 Join。
通过这种方式,Delta Join 避免了在 Flink 状态中维护全量数据的问题,利用 Fluss 的存储和点查能力,实现了高效、低延迟的双流 Join。
写在最后
下一篇文章会详细说一下为什么 Fluss 通过这样的点查会如此高效,会深入 Fluss 底层存储来说清楚查询和更新的流程。最后欢迎大家关注微信公众号 大圣数据星球 来讨论大数据技术。
共同学习,写下你的评论
评论加载中...
作者其他优质文章