Spark Structured Stream的流关联(Stream-Stream Joins)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/bluishglc/article/details/81326624

自Spark 2.3开始,Spark Structured Streaming开始支持Stream-stream Joins。两个流之间的join与静态的数据集之间的join有一个很大的不同,那就是,对于流来说,在任意时刻,在join的两边(也就是两个流上),数据都是“不完全”的,当前流上的任何一行数据都可能会和被join的流上的未来某行数据匹配到,为此,Spark必须要缓存流上过去所有的输入,以Stream State信息的形式来维护。当然,和流上的聚合运算一样,我们也可以通过wartermark来处理data late问题。本文原文链接: http://blog.csdn.net/bluishglc/article/details/81326624 转载请注明出处。

内关联(Inner Joins)

Spark Structured Streaming支持任何列之间的join,但是这样会带来一个问题:随着stream的长期运行,stream的状态数据会无限制地增长,并且这些状态数据不能被释放,因为如前所诉,不管多么旧的数据,在未来某个时刻都有可能会被join到,因此我们必须在join上追加一些额外的条件来改善state无止境的维持旧有输入数据的问题。我们可以使用的方法有:

  1. 定义waterwark, 抛弃超过约定时限到达的输入数据。
  2. 在事件时间上添加约束,约定数据多旧之后就不再参与join了,这种约束可以通过以下两种方式之一来定义:
    1. 指定事件时间的区间:例如:JOIN ON leftTime BETWEN rightTime AND rightTime + INTERVAL 1 HOUR
    2. 指定事件时间的窗口:例如:JOIN ON leftTimeWindow = rightTimeWindow

在Spark的官方文档中有这样一个示例:在广告投放中,把用户打开包含广告的一个页面称为广告被“触达”一次,即advertisement impressions次数加1(impression指的就是广告触达),这一 实时数据对应一个流,叫impressions流,如果用户点击了广告,这一数据也会被实时采集到,对应的流叫clicks流,如果我们想实时地分析一个广告的触达率和点击率之间的关系,就需要将这两个流按广告的ID进行join,这就是一个典型的stream-stream join的案例。现在,为了防止无止尽的状态数据,我们做如下限制:

触达数据最多允许迟到2小时,点击数据最多允许迟到3小时。点击数据允许的延迟比触达数据长是有道理的,因为点击总是发生在用户看到广告之后,给定一个小时的时间差是一个比较宽泛和可靠的阈值。而在流上进行join时,我们需要相应地允许点击流join触达流上与之时间相同以及触达流上前推1小时内的数据,这与触达流(最大允许2小时延迟)允许比点击流(最大允许3小时延迟)多一小时的最大延迟时间是一致的。

以下代码是上述业务分析的实现:

import org.apache.spark.sql.functions.expr

val impressions = spark.readStream. ...
val clicks = spark.readStream. ...

// Apply watermarks on event-time columns
val impressionsWithWatermark = impressions.withWatermark("impressionTime", "2 hours")
val 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
    """)
)

需要重点解释的是:两个流都通过withWatermark对数据延迟进行了限定,之后在join操作中通过clickTime >= impressionTime AND clickTime <= impressionTime + interval 1 hour来限定时间尺度上的界限条件,这样达到的效果就是:允许并只允许以当前时间为基准向前推2小时内的触达数据和3小时内的点击数据进行join,超出这个时间界限的数据不会被Stream State维护,避免无止尽的State。

外关联(Outer Joins)

watermark + event-time对于内关联是非必须的,可选的(虽然大数情况下都应该制定),但是对外关联就是强制的了,因为在外关联中,如果关联的任何一方没有匹配的数据,都需要补齐空值,如果不对关联数据的范围进行限定,外关联的结果集会膨胀地非常厉害,也就是每一条没有匹配到的输入数据都要依据另一个流上的全体数据的总行数使用空值补齐。

前面内关联的例子使用外关联实现的代码如下:

import org.apache.spark.sql.functions.expr

val impressions = spark.readStream. ...
val clicks = spark.readStream. ...

// Apply watermarks on event-time columns
val impressionsWithWatermark = impressions.withWatermark("impressionTime", "2 hours")
val 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
    """),
  joinType = "leftOuter"      // can be "inner", "leftOuter", "rightOuter"
)

区另仅在于追加了一个参数来指定关联类型是leftOuter join.

此外,外关联结果集的生成也有这样一些重要的特点:

  1. 外关联的空的结果集必须要等到时间走过了指定的watermark和time range条件之后才会生成,原因和前面解释outer join必须依赖watermark + event-time的原因是一样的,就是外关联下空值必须要补齐到另一方的所有行上,因此引擎必须要等待另一方全部的数据(就是watermark和time range条件限定范围内的数据)就位之后才能进行补齐操作。

  2. 与维护任意状态流时没有一个确定的timeout触发时间类似(参考:http://spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.sql.streaming.GroupState 中关于“there is a no strict upper bound on when the timeout would occur”的解释),在没有数据输入的情况下,外关联结果集的输出也会延迟,而且可能会延迟非常长的时间。引起这些情况的原因都与watermark机制有关,因为watermark更新是依赖每一个新进的mirco-batch上的数据的event-time, 如果迟迟没有新的数据输入,就不会驱动watermark的更新,进行所有依赖watermark进行时间范围判定的动作也不会被触发,或者触发也不会有所变化,因为watermark没有更新,因此对产生各种延迟。

猜你喜欢

转载自blog.csdn.net/bluishglc/article/details/81326624