spark 结构化流的Window

Window Operations on Event Time

基于事件时间的窗口操作

​ 滑动event-time时间窗口的聚合在StructuredStreaming上很简单,并且和分组聚合非常相似。在分组聚合中,为用户指定的分组列中的每个唯一值维护聚合值(例如计数)。在基于窗口的聚合的情况下,为每一个event-time窗口维护聚合值

​ 想象一下,quickexample中的示例被修改,现在stream中的每行包含了生成的时间。我们不想运行word count,而是要在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收到的一个word。这个词应该增加对应于两个窗口的计数,分别为12:00 - 12:10和12:05 - 12:15。所以计数counts将会被group key(ie:the word)和window(根据event-time计算)索引。将会如下所示

在这里插入图片描述

由于此窗口类似于分组,因此在代码中,可以使用groupBy()和window()操作来表示窗口聚合。如:


Handling Late Data and Watermarking

处理延迟数据和水位线

默认: 延迟数据累加到窗口计算中,并且Spark在内存保存所有窗口计算的中间结果

现在考虑如果一个事件迟到应用程序会发生什么。例如,假设12:04(即event-time)生成的一个word可以在12:11被应用程序接收。应用程序应该使用时间12:04而不是12:11更新窗口的较旧计数,即12:00 - 12:10。这在我们基于窗口的分组中很自然有可能发生- Structured Streaming可以长时间维持部分聚合的中间状态,以便延迟的数据可以正确地更新旧窗口的聚合,如下所示:

但是,为了长久的运行这个查询,必须限制内存中间状态的数量。这就意味着,系统需要知道什么时候能够从内存中删除旧的聚合,此时默认应用接受延迟的数据之后不再进行聚合。Spark2.1中引入了watermarking(水位线),它能够让engine自动跟踪当前的数据中的event time并据此删除旧的状态表。你可以通过指定event-time列和时间阀值来指定一个查询的watermark,阀值以内的数据才会被处理。对于一个特定的开始于时间T的window窗口,引擎engine将会保持状态并且允许延迟的数据更新状态直到(max event time seen by the engine - late threshold > T)。换句话说,阀值内的数据将被聚合,阀值外的数据将会被丢弃。

wm(水位线) = 最大的事件时间-数据的延迟时间

作用: 界定过期数据和有效数据的一种规则

  • 水位线以内的延迟数据为有效数据,参与窗口的计算
  • 水位线以外的数据为无效数据,直接丢弃,水位线以外的窗口会自动drop
import spark.implicits._

val words = ... // streaming DataFrame of schema { timestamp: Timestamp, word: String }

// Group the data by window and word and compute the count of each group
val windowedCounts = words
    .withWatermark("timestamp", "10 minutes")
    .groupBy(
        window($"timestamp", "10 minutes", "5 minutes"),
        $"word")
    .count()

本例中,watermark的指定列为“timestamp”,并且指定了“10minute”作为阀值。如果这个查询运行在update的输出模式,引擎engine会持续更新window的counts到结果集中,直到窗口超过watermark的阀值,本例中,则是如果timestamp列的时间晚于当前时间10minute。

如上所示,蓝色虚线表示最大event-time,每次数据间隔触发开始时,watermark被设置为max eventtime - ‘10 mins’,如图红色实线所示。例如,当引擎engine观测到到数据(12:14,dog),对于下个触发器,watermark被设置为12:04。这个watermark允许引擎保持十分钟内的中间状态并且允许延迟数据更新聚合集。例如(12:09,cat)的数据未按照顺序延迟到达,它将落在12:05 – 12:15 和 12:10 – 12:20 。因为它依然大于12:04 ,所以引擎依然保持着中间结果集,能够正确的更新对应窗口的结果集。但是当watermark更新到12:11,中间结果集12:00-12:10的数据将会被清理掉,此时所有的数据(如(12:04,donkey))都会被认为“too late”从而被忽略。注意,每次触发器之后,更新的counts(如 purple rows)都会被写入sink作为输出的触发器,由更新模式控制。

某些接收器(例如文件)可能不支持更新模式所需的细粒度更新。要与他们一起工作,我们还支持append模式,只有最后的计数被写入sink。这如下所示。

请注意,在非流数据集上使用watermark是无效的。由于watermark不应以任何方式影响任何批次查询,我们将直接忽略它。

在这里插入图片描述

package com.baizhi.window

import java.sql.Timestamp
import java.text.SimpleDateFormat

import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.streaming.OutputMode

/**
  * 基于事件事件窗口操作
  *
  */
object SparkStructuredStreamingForLateDataHandling {
  def main(args: Array[String]): Unit = {
    val spark = SparkSession.builder().appName("window on event time").master("local[*]").getOrCreate()
    spark.sparkContext.setLogLevel("ERROR")
    import spark.implicits._

    val df = spark
      .readStream
      .format("socket")
      .option("host", "Spark")
      .option("port", "4444")
      .load()

    // 发送数据时:word,timestamp
    // Hello,1573525440000  --> 10:24:00

    // 导入隐式转换的函数
    import org.apache.spark.sql.functions._

    val sdf = new SimpleDateFormat("HH:mm:ss") // 时:分:秒

    df
      .as[String]
      .map(_.split(",")) // Hello,1573525440000 --> Array("Hello","1573525440000")
      .map(arr => (arr(0), new Timestamp(arr(1).toLong))) //("Hello",new Timestamp(1573525440000))
      .toDF("word", "timestamp")
      //-------------------------------------------------------------
      // 水位线操作
      .withWatermark("timestamp", "10 seconds") // vm = maxEventTime-2
      //-------------------------------------------------------------

      // 先根据事件时间划分窗口,再对窗口内的单词进一步分组
      .groupBy(window($"timestamp", "10 seconds", "5 seconds"), $"word") // 滑动窗口 长度5s  滑动3s
      // 统计窗口内单词出现的次数
      .count()

      /**
        * root
        * |-- window: struct (nullable = true)
        * |    |-- start: timestamp (nullable = true)
        * |    |-- end: timestamp (nullable = true)
        * |-- word: string (nullable = true)
        * |-- count: long (nullable = false)
        */
      //.printSchema()
      .map(row => (sdf.format(row.getStruct(0).getTimestamp(0)), sdf.format(row.getStruct(0).getTimestamp(1)), row.getString(1), row.getLong(2)))
      .toDF("startTime", "endTime", "word", "num")

      .writeStream
      .format("console")
      // .outputMode(OutputMode.Append()) // 注意:输出模式只能是 append和 update
      .outputMode(OutputMode.Update()) // 注意:输出模式只能是 append和 update
      .start()
      .awaitTermination()
  }
}

Join Operations

Streaming DataFrames可以与静态 DataFrames连接,以创建新的Streaming DataFrames。 例如下面的例子:

import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.types.{BooleanType, StructType}

object SparkStructuredStreamingForJoinOpt {
  def main(args: Array[String]): Unit = {
    val spark = SparkSession.builder().appName("join opt").master("local[*]").getOrCreate()

    val df1 = spark   // 基于静态文件创建DF
      .read
      .format("json")
      .load("file:///G:\\IDEA_WorkSpace\\scala-workspace\\spark-day11\\src\\main\\resources") // id name sex

    val df2 = spark  // 基于流数据创建DF
      .readStream
      .format("csv")
      .schema(
        new StructType()
          .add("id", "integer")
          .add("name", "string")
          .add("sex", BooleanType)
          .add("salary", "double")
      )
      .csv("file:///d://csv")

    // 批和流不允许Join【流数据不能join给批数据】
    // 正常:         【批数据join给流数据】
    df2.join(df1,Seq("id","id"),"leftOuter")  // 流DF join 批DF
      .writeStream
      .format("console")
      .outputMode("append")
      .start()
      .awaitTermination()
  }
}

//----------------------------------------------------------------------
Batch: 0
-------------------------------------------
+---+---+----+------+----+------+
| id| id|name|salary|name|salary|
+---+---+----+------+----+------+
|  1|  1|  zs|1000.0|  zs|3000.0|
|  2|  2|  ls|2000.0|  ls|3000.0|
|  3|  3|  ww|3000.0|  ww|3000.0|
|  4|  4|  zs|1000.0| zs2|3000.0|
|  5|  5|  ls|2000.0| ls2|3000.0|
|  6|  6|  ww|3000.0| ww2|3000.0|
|  4|  4|  zs|3000.0| zs2|3000.0|
|  7|  7|  ls|2000.0|null|  null|
|  8|  8|  ww|3000.0|null|  null|
+---+---+----+------+----+------+
  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值