结构化流式传输支持将Dataset/DataFrame与静态Dataset/DataFrame以及另一个流式Dataset/DataFrame连接起来。流连接的结果以递增方式生成,类似于上一节中的流聚合的结果。本节,我们将探讨再上述情况下支持哪种类型的连接(即内部,外部等)。注意,在所有受支持的连接类型中,与流式Dataset/DataFrame的连接结果与使用包含流中相同数据的静态Dataset/DataFrame的结果完全相同。
静态-流 Join
自Spark2.0引入以来,Structured Streaming支持流和静态Dataset/DataFrame之间的连接(内连接和某种类型的外连接)。下面是一个简单的例子。
Dataset<Row> staticDf = spark.read(). ...;
Dataset<Row> streamingDf = spark.readStream(). ...;
// inner equi-join with a static DF
streamingDf.join(staticDf, "type");
// right outer join with a static DF
streamingDf.join(staticDf, "type", "right_join");
注意,静态流连接不是有状态的,因此不需要进行状态管理。但是,尚不支持集中类型的静态流式外连接。
流-流 Join
在Spark2.3中,添加了对Stream-Stream Join的支持,也就是说,我们可以加入两个流Dataset/DataFrame。在两个数据流之间生成连接结果的挑战是,在任何时间点,数据集的视图对于连接的两侧都是不完整的,这使得在输入之间找到匹配更加困难。从一个输入流接收的任何行都可以与来自另一个输入流的任何未来的,尚未接收的行匹配。因此,对于两个输入流,Spark将过去的输入缓冲为流状态,以便可以将每个未来输入与过去的输入相匹配,从而生成连接结果。此外,类似于流聚合,Spark自动处理迟到的无序数据,并可以使用水印限制状态。
带有水印的内连接
支持任何类型的列上的内部链接以及任何类型的连接条件。但是,当流运行时,流状态的大小将无线增长,因为必须保存所有过去的输入,因为任何新输入都可以与过去的任何输入匹配。为了避免无界状态,我们必须定义其他连接条件,以便于无限期使旧输入无法与将来的输入匹配,从而可以从状态清除。换句话说,我们必须在连接中执行以下附加步骤。
- 定义两个输入上的水印延迟,以便引擎直到输入的延迟时间(类似于流式聚合)
- 在两个输入上定义事件事件约束,以便引擎可以确定何时不需要一个输入的旧行(即不满足时间约束)与另一个输入匹配。可以用两种方式之一定义该约束。
- 时间范围连接条件(例如
...JOIN ON leftTime BETWEN rightTime AND rightTime + INTERVAL 1 HOUR
), - 加入事件时间窗口(例如
...JOIN ON leftTimeWindow = rightTimeWindow
)。
- 时间范围连接条件(例如
让我们通过一个例子来理解这一点。
假设我们希望加入一系列广告展示次数(展示广告时),并在广告上添加另一个用户点击流,以便在展示次数达到可获利的点击时进行关联。要在Stream-Stream连接中允许状态清理,我们必须指定水印延迟和时间约束,如下所示。
- 水印延迟:比如说,展示次数和相应的点击次数可以分别在时间时间内延迟/无序,最多2个小时和3个小时。
- 事件时间范围条件:假设,在相应的印象后0秒到1小时的时间范围内可能发生咔哒声。
示例代码如下:
Dataset<Row> impressions = spark.readStream(). ...
Dataset<Row> clicks = spark.readStream(). ...
// Apply watermarks on event-time columns
Dataset<Row> impressionsWithWatermark = impressions.withWatermark("impressionTime", "2 hours");
Dataset<Row> clicksWithWatermark = clicks.withWatermark("clickTime", "3 hours");
// Join with event-time constraints
impressionsWithWatermark.join(
clicksWithWatermark,
expr(
"clickAdId = impressionAdId AND " +
"clickTime >= impressionTime AND " +
"clickTime <= impressionTime + interval 1 hour ")
);
具有水印的流内部连接的语义保证
这类似于通过聚合水印提供的保证。水印延迟“2小时”可确保引擎永远不会丢失任何延迟小于2小时的数据。但延迟2小时以上的数据不能保证会得到处理。
带水印的外连接
虽然水印+事件时间约束对于内连接是可选的,但对于左外连接和右外连接,必须指定它们。这是因为为了在外连接中生成NULL结果,引擎必须直到输入行何时不会与将来的任何内容匹配。因此,必须指定水印+事件时间约束以生成正确的结果。因此,使用外部连接的查询看起来与之前的广告货币化示例非常相似,只是会有一个附加参数将其指定为外部连接。
impressionsWithWatermark.join(
clicksWithWatermark,
expr(
"clickAdId = impressionAdId AND " +
"clickTime >= impressionTime AND " +
"clickTime <= impressionTime + interval 1 hour "),
"leftOuter" // can be "inner", "leftOuter", "rightOuter"
);
具有水印的流 - 流外连接的语义保证
关于水印延迟以及数据是否会被丢弃,外连接与内部连接具有相同的保证。
注意事项
关于如何生成外部结果,有一些重要的特征需要注意。
- 将生成外部NULL结果,延迟取决于指定的水印延迟和时间范围条件。这是因为引擎必须等待那么长时间以确保没有匹配,并且将来不会再有匹配。
- 在微批量引擎的当前实现中,水印在微批次结束时前进,并且下一个微批次使用更新的水印来清理状态并输出外部的结果。由于我们仅在存在要处理的新数据时才触发微批处理,因此如果在流中没有接收到新的数据,则外部结果的生成可能会延迟。简而言之,如果连接的两个输入流中的任何一个在一段时间内没有接收到数据,则外部(两种情况,左侧或右侧)输出可能会延迟。
支持流式查询中的连接矩阵
左输入 | 右输入 | 连接类型 | |
Static | Static | 所有类型 | 支持,因为它不在流数据上,即使它可以存在于流式查询中 |
Stream | Static | 内连接 | 支持,无状态 |
左外连接 | 支持,无状态 | ||
右外连接 | 不支持 | ||
全外连接 | 不支持 | ||
Static | Stream | 内连接 | 支持,无状态 |
左外连接 | 不支持 | ||
右外连接 | 支持无状态 | ||
全外连接 | 不支持 | ||
Stream | Stream | 内连接 | 支持,可选择在两侧指定水印+状态清理的时间限制 |
左外连接 | 有条件支持,必须在右侧指定水印 + 时间约束上以获得正确的结果,可选择在左侧指定水印以进行所有状态清理 | ||
右外连接 | 有条件支持,必须在左侧指定水印 + 时间约束上以获得正确的结果,可选择在右侧指定水印以进行所有状态清理 | ||
全外连接 | 不支持 |
有关支持的连接大的其他详细信息:
- 连接可以级联,也就是说,我们可以做到df1.join(df2,...).join(df3,...).join(df4,...) 。
- 从Spark2.3开始,只有在查询出于追加输出模式时才能使用连接。其他输出模式尚不支持。
- 从Spark2.3开始,在连接之前不能使用其他非类似map的操作。以下时一些不能使用的例子。
- 在join之前无法使用流式聚合。
- 在join之前,无法在更新模式下使用mapGroupsWithState和flatMapGroupsWithState。