Structured Stream(三)--Window Operations on Event Time

Handling Event-time and Late Data(处理事件时间和延迟数据)

事件时间是嵌入到数据本身中的时间。对于许多应用程序,您可能希望对这个事件时间进行操作。例如,如果您希望获得物联网设备每分钟生成的事件数,那么您可能希望使用数据生成时的时间(即数据中的事件时间),而不是Spark接收它们的时间。这个事件时间很自然地在这个模型中表示出来,来自设备的每个事件是表中的一行,而事件时间是行中的列值。这使得基于窗口的聚合(例如每分钟的事件数)成为事件时间列上的一种特殊类型的分组和聚合,每个时间窗口都是一个组,并且每一行都可以属于多个窗口/组。因此,可以在静态数据集(例如,从收集的设备事件日志)和数据流上一致地定义这种基于事件时间窗口的聚合查询,这使得用户的生活更加轻松。

此外,此模型自然会根据事件时间处理比预期晚到达的数据。因为Spark正在更新结果表,所以它可以完全控制在出现延迟数据时更新旧的聚合,以及清理旧的聚合以限制中间状态数据的大小。从Spark 2.1开始,我们支持水印,允许用户指定后期数据的阈值,允许引擎相应地清理旧状态。稍后将在窗口操作部分更详细地解释这些操作。

Window Operations on Event Time(事件时间上的窗口操作)

滑动事件时间窗口上的聚合与结构化流非常简单,非常类似于分组聚合。在分组聚合中,为用户指定的分组列中的每个惟一值维护聚合值(例如计数)。对于基于窗口的聚合,将为每个窗口维护聚合值,其中包含一行的事件时间。让我们用一个例子来理解它。

想象一下,我们的快速示例被修改了,流现在包含了行以及该行生成的时间。我们想要计算10分钟内窗口内的单词数,而不是运行单词计数,每5分钟更新一次。也就是说,在10分钟窗口内收到的单词数为12:00 - 12:10、12:05 - 12:15、12:10 - 12:20等。请注意,12:00 - 12:10表示在12:00之后但在12:10之前到达的数据。现在,考虑12:07收到的一个单词。此字应增加与两个窗口12:00 - 12:10和12:05 - 12:15对应的计数。因此计数将被分组键(即单词)和窗口(可以从事件时间计算)索引。

结果表将类似如下所示。
在这里插入图片描述

基本测试案例
object StructedStreamWordCountWindow {

  def main(args: Array[String]): Unit = {

    val spark = SparkSession
      .builder
      .appName("StructuredNetworkWordCount")
      .master("local[*]")
      .getOrCreate()

    spark.sparkContext.setLogLevel("ERROR")

    import spark.implicits._
    val lines:DataFrame = spark.readStream
      .format("socket")
      .option("host", "train")
      .option("port", 9999)
      .load()

    //word,templet
    val word = lines.as[String]
      .map(line => line.split(","))
      .map(t => (t(0), new Timestamp(t(1).toLong)))
      .toDF("word", "timestamp")


    import org.apache.spark.sql.functions._
    //将数据按窗口和单词分组,并计算每个组的计数
    val wordCounts = word.groupBy(window($"timestamp", "4 seconds", "2 seconds"), $"word")
      .count()
      .map(row => {
        var start = row.getStruct(0).getTimestamp(0)
        var end = row.getStruct(0).getTimestamp(1)

        var word = row.getString(1)
        var count = row.getLong(2)

        var sdf = new SimpleDateFormat("HH:mm:ss")
        (sdf.format(start.getTime), sdf.format(end.getTime), word, count)
      })
      .toDF("start", "end", "word", "count")


   //3.产生StreamQuery对象
    val query:StreamingQuery = wordCounts.writeStream
      .outputMode(OutputMode.Complete())
      .format("console")
      .start()
    query.awaitTermination()
  }

}
Handling Late Data and Watermarking(处理后期数据和水印)

现在,考虑如果其中一个事件延迟到应用程序会发生什么。例如,在12:04生成的单词(即事件时间)可以在12:11被应用程序接收。应用程序应该使用时间12:04而不是12:11来更新窗口12:00 - 12:10的旧计数。这在我们的基于窗口的分组中很自然地发生——结构化流可以在很长一段时间内保持部分聚合的中间状态,以便后期数据可以正确地更新旧窗口的聚合,如下所示。
在这里插入图片描述
在Spark 2.1中,我们引入了watermarking,用于告知计算节点,何时丢弃窗口聚合状态。因为流计算是一个长时间运行的任务,系统不可能无限制存储一些过旧的状态值。使用watermarking机制,系统可以删除那些过期的状态数据,用于释放内存。每个触发的窗口都有start timeend time属性,计算引擎会保留计算引擎所看到最大event time

watermark时间=max event time seen by the engine(数据的事件时间) - late threshold(允许迟到的时间)

如果watermarker时间T’ > 窗口的end time时间T则认为该窗口的计算状态可以丢弃

注意: 引入watermarker以后,用户只能使用updateappend模式,系统才会删除过期数据。

update-水位线没有没过窗口的end time之前,如果有数据落入到该窗口,该窗口会重复触发。

val spark = SparkSession
      .builder
      .appName("StructuredNetworkWordCount")
      .master("local[*]")
      .getOrCreate()

    spark.sparkContext.setLogLevel("ERROR")

    import spark.implicits._
    val lines:DataFrame = spark.readStream
      .format("socket")
      .option("host", "train")
      .option("port", 9999)
      .load()

    //word,templet
    val word = lines.as[String]
      .map(line => line.split(","))
      .map(t => (t(0), new Timestamp(t(1).toLong)))
      .toDF("word", "timestamp")


    import org.apache.spark.sql.functions._
    //将数据按窗口和单词分组,并计算每个组的计数
    val wordCounts = word.withWatermark("timestamp","1 second")
      .groupBy(window($"timestamp", "4 seconds", "2 seconds"), $"word")
      .count()
      .map(row => {
        var start = row.getStruct(0).getTimestamp(0)
        var end = row.getStruct(0).getTimestamp(1)

        var word = row.getString(1)
        var count = row.getLong(2)

        var sdf = new SimpleDateFormat("HH:mm:ss")
        (sdf.format(start.getTime), sdf.format(end.getTime), word, count)
      })
      .toDF("start", "end", "word", "count")


   //3.产生StreamQuery对象
    val query:StreamingQuery = wordCounts.writeStream
      .outputMode(OutputMode.Update())
      .format("console")
      .start()
    query.awaitTermination()

Append–水位线没有没过窗口的end time之前,如果有数据落入到该窗口,该窗口不会触发,只会默默的计算,只有当水位线没过窗口的end time的时候,才会做出最终输出。

val spark = SparkSession
      .builder
      .appName("StructuredNetworkWordCount")
      .master("local[*]")
      .getOrCreate()

    spark.sparkContext.setLogLevel("ERROR")

    import spark.implicits._
    val lines:DataFrame = spark.readStream
      .format("socket")
      .option("host", "train")
      .option("port", 9999)
      .load()

    //word,templet
    val word = lines.as[String]
      .map(line => line.split(","))
      .map(t => (t(0), new Timestamp(t(1).toLong)))
      .toDF("word", "timestamp")


    import org.apache.spark.sql.functions._
    //将数据按窗口和单词分组,并计算每个组的计数
    val wordCounts = word.withWatermark("timestamp","1 second")
      .groupBy(window($"timestamp", "4 seconds", "2 seconds"), $"word")
      .count()
      .map(row => {
        var start = row.getStruct(0).getTimestamp(0)
        var end = row.getStruct(0).getTimestamp(1)

        var word = row.getString(1)
        var count = row.getLong(2)

        var sdf = new SimpleDateFormat("HH:mm:ss")
        (sdf.format(start.getTime), sdf.format(end.getTime), word, count)
      })
      .toDF("start", "end", "word", "count")


   //3.产生StreamQuery对象
    val query:StreamingQuery = wordCounts.writeStream
      .outputMode(OutputMode.Append())
      .format("console")
      .start()
    query.awaitTermination()
Semantic Guarantees of Aggregation with Watermarking(语义保证聚合与水印)
  • 水印延迟(与水印一起设置)为“2小时”,保证引擎不会丢弃任何延迟小于2小时的数据。换句话说,任何在事件时间上少于2小时之前处理的最新数据都将被聚合。
  • 然而,这种保证只在一个方向上是严格的。延迟超过2小时的数据不保证被删除;它可能聚合,也可能不聚合。数据越是延迟,引擎处理它的可能性就越小。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值