Spark Structured Streaming EventTime下Watermark生成与Window触发

Spark 同时被 2 个专栏收录
29 篇文章 1 订阅
28 篇文章 7 订阅

本文旨在弄清楚Spark Structured Streaming EventTime下Watermark生成与Window触发相关问题。

  1. 窗口起止时间。

  2. 水印的生成。

  3. 对迟到数据的处理。

  4. 窗口销毁的时机。

  5. Watermark与Update/Complete输出模式之间的关系。

测试数据

// 造的测试数据,如下:
// eventTime: 北京时间
 {"eventTime": "2016-01-01 10:02:00" ,"eventType": "browse/click"}

代码实现

package com.bigdata.structured.streaming.watermark

import java.sql.{Date, Timestamp}
import java.text.SimpleDateFormat
import java.time.{LocalDateTime, ZoneId}
import java.time.format.DateTimeFormatter
import java.util.TimeZone

import com.bigdata.structured.streaming.sink.FileSink
import org.apache.spark.sql.{SparkSession, functions}
import org.apache.spark.sql.functions.{col, count, from_json, lit, window}
import org.apache.spark.sql.streaming.Trigger
import org.apache.spark.sql.types.DataType
import org.slf4j.LoggerFactory

/**
  * Author: Wang Pei
  * Summary:
  *   EventTime下Watermark生成与Window触发
  */
object WaterMarkAndWindow {

  lazy val logger = LoggerFactory.getLogger(WaterMarkAndWindow.getClass)

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

    val spark = SparkSession.builder().master("local[3]").appName(this.getClass.getSimpleName.replace("$", "")).getOrCreate()
    import spark.implicits._

    // 注册UDF
    spark.udf.register("timezoneToTimestamp", timezoneToTimestamp _)
    spark.udf.register("timestampToTimezone", timestampToTimezone _)
    // 定义Kafka JSON Schema
    val jsonSchema ="""{"type":"struct","fields":[{"name":"eventTime","type":"string","nullable":true},{"name":"eventType","type":"string","nullable":true}]}"""

    // InputTable
    val inputTable = spark
      .readStream
      .format("kafka")
      .option("kafka.bootstrap.servers", "kafka01:9092,kafka02:9092,kafka03:9092")
      .option("subscribe", "test_1")
      .load()

    // ResultTable
    val resultTable = inputTable
      .select(from_json(col("value").cast("string"), DataType.fromJson(jsonSchema)).as("value"))
      .select($"value.*")
      .withColumn("timestamp", functions.callUDF("timezoneToTimestamp", functions.col("eventTime"),lit("yyyy-MM-dd HH:mm:ss"),lit("GMT+8")))
      .filter($"timestamp".isNotNull && $"eventType".isNotNull)
      // 最多迟到10秒
      .withWatermark("timestamp", "10 seconds")
      // 窗口30秒
      .groupBy(window($"timestamp", "30 seconds"), $"eventType")
      .agg(count(lit(1)).as("browsePV"))
      .withColumn("windowStart", functions.callUDF("timestampToTimezone", $"window.start", lit("yyyy-MM-dd HH:mm:ss"), lit("GMT+8")))
      .withColumn("windowEnd", functions.callUDF("timestampToTimezone", $"window.end", lit("yyyy-MM-dd HH:mm:ss"), lit("GMT+8")))
      .select($"windowStart", $"windowEnd", $"eventType", $"pv")

    // Query Start
    val query = resultTable
      .writeStream
      .format("console")
      .option("truncate", "false")
      .outputMode("complete")
      .trigger(Trigger.ProcessingTime("2 seconds"))
      .start()

    query.awaitTermination()

  }

  /**
    * 带时区的时间转换为Timestamp
    *
    * @param dateTime
    * @param dataTimeFormat
    * @param dataTimeZone
    * @return
    */
  def timezoneToTimestamp(dateTime: String, dataTimeFormat: String, dataTimeZone: String): Timestamp = {
    var output: Timestamp = null
    try {
      if (dateTime != null) {
        val format = DateTimeFormatter.ofPattern(dataTimeFormat)
        val eventTime = LocalDateTime.parse(dateTime, format).atZone(ZoneId.of(dataTimeZone));
        output = new Timestamp(eventTime.toInstant.toEpochMilli)
      }
    } catch {
      case ex: Exception => logger.error("时间转换异常..." + dateTime, ex)
    }
    output
  }

  /**
    * Timestamp转指定时区时间
    *
    * @param timestamp
    * @param targetFormat
    * @param targetZoneOffset
    * @return
    */
  def timestampToTimezone(timestamp: Timestamp, targetFormat: String, targetZoneOffset: String): String = {
    val date = new Date(timestamp.getTime)
    val simpleDateFormat = new SimpleDateFormat(targetFormat)
    simpleDateFormat.setTimeZone(TimeZone.getTimeZone(targetZoneOffset))
    simpleDateFormat.format(date)
  }

}

调式验证

序号RecordEventTimeEventTime 对应的 WatermarkEventTime 对应的Window起止时间触发Window聚合输出
1{“eventTime”: “2016-01-01 10:02:00”, “eventType”: “browse”}2016-01-01 10:02:002016-01-01 10:01:50[2016-01-01 10:02:00, 2016-01-01 10:02:30)触发browse: 1
2{“eventTime”: “2016-01-01 10:02:05”, “eventType”: “browse”}2016-01-01 10:02:052016-01-01 10:01:55[2016-01-01 10:02:00, 2016-01-01 10:02:30)触发browse: 2
3{“eventTime”: “2016-01-01 10:02:10”, “eventType”: “browse”}2016-01-01 10:02:102016-01-01 10:02:00[2016-01-01 10:02:00, 2016-01-01 10:02:30)触发browse: 3
4{“eventTime”: “2016-01-01 10:02:15”, “eventType”: “browse”}2016-01-01 10:02:152016-01-01 10:02:05[2016-01-01 10:02:00, 2016-01-01 10:02:30)触发browse: 4
5{“eventTime”: “2016-01-01 10:02:20”, “eventType”: “browse”}2016-01-01 10:02:202016-01-01 10:02:10[2016-01-01 10:02:00, 2016-01-01 10:02:30)触发browse: 5
6{“eventTime”: “2016-01-01 10:02:25”, “eventType”: “browse”}2016-01-01 10:02:252016-01-01 10:02:15[2016-01-01 10:02:00, 2016-01-01 10:02:30)触发browse: 6
7{“eventTime”: “2016-01-01 10:02:29”, “eventType”: “browse”}2016-01-01 10:02:292016-01-01 10:02:19[2016-01-01 10:02:00, 2016-01-01 10:02:30)触发browse: 7
8{“eventTime”: “2016-01-01 10:02:30”, “eventType”: “click”}2016-01-01 10:02:302016-01-01 10:02:20[2016-01-01 10:02:30, 2016-01-01 10:03:00)触发click: 1
9{“eventTime”: “2016-01-01 10:02:25”, “eventType”: “browse”}2016-01-01 10:02:252016-01-01 10:02:20[2016-01-01 10:02:00, 2016-01-01 10:02:30)触发browse: 8
10{“eventTime”: “2016-01-01 10:02:35”, “eventType”: “click”}2016-01-01 10:02:352016-01-01 10:02:25[2016-01-01 10:02:30, 2016-01-01 10:03:00)触发click: 2
11{“eventTime”: “2016-01-01 10:02:20”, “eventType”: “browse”}2016-01-01 10:02:202016-01-01 10:02:25[2016-01-01 10:02:00, 2016-01-01 10:02:30)触发browse: 9
12{“eventTime”: “2016-01-01 10:02:40”, “eventType”: “click”}2016-01-01 10:02:402016-01-01 10:02:30[2016-01-01 10:02:30, 2016-01-01 10:03:00)触发click: 3
13{“eventTime”: “2016-01-01 10:02:25”, “eventType”: “browse”}2016-01-01 10:02:252016-01-01 10:02:30[2016-01-01 10:02:00, 2016-01-01 10:02:30)不触发browse: 9
14{“eventTime”: “2016-01-01 10:02:29”, “eventType”: “browse”}2016-01-01 10:02:292016-01-01 10:02:30[2016-01-01 10:02:00, 2016-01-01 10:02:30)不触发browse: 9
15{“eventTime”: “2016-01-01 10:02:45”, “eventType”: “click”}2016-01-01 10:02:452016-01-01 10:02:35[2016-01-01 10:02:30, 2016-01-01 10:03:00)触发click: 4

重点说明

  1. 窗口长度30秒,最大迟到10秒。

  2. 以上验证是在Update输出模式下。

  3. Watermark实际上是Timestamp类型,格式如:2016-01-01T02:02:00.000Z,这里为方便对齐比较,将Watermark加了8小时。

  4. 当输入记录9时,EventTime: 2016-01-01 10:02:25, Watermark: 2016-01-01 10:02:20。Watermark<窗口W: [2016-01-01 10:02:00, 2016-01-01 10:02:30)的结束时间,触发该窗口W聚合。

  5. 当输入记录11时,EventTime: 2016-01-01 10:02:20,Watermark: 2016-01-01 10:02:25。Watermark<窗口W: [2016-01-01 10:02:00, 2016-01-01 10:02:30)的结束时间,触发该窗口W聚合。

  6. 当输入记录12时,EventTime: 2016-01-01 10:02:40, Watermark: 2016-01-01 10:02:30。Watermark>=窗口W: [2016-01-01 10:02:00, 2016-01-01 10:02:30)的结束时间,销毁该窗口W(或清除该窗口的状态)。

  7. 当输入记录13时,EventTime: 2016-01-01 10:02:25, Watermark: 2016-01-01 10:02:30。该事件对应的窗口W: [2016-01-01 10:02:00, 2016-01-01 10:02:30)已销毁,不再触发窗口W的聚合。该事件被丢弃。

  8. 当输入记录14时,EventTime: 2016-01-01 10:02:29, Watermark: 2016-01-01 10:02:30。该事件对应的窗口W: [2016-01-01 10:02:00, 2016-01-01 10:02:30)已销毁,不再触发窗口W的聚合。该事件被丢弃。

总结

  1. 窗口起止时间: 前闭后开的自然时间。

    举例: 30s一个窗口,窗口间隔为[00:00:00, 00:00:30)[00:00:30, 00:01:00)[00:01:00, 00:01:30) … 以此类推。

  2. Watermark计算逻辑:Structured Streaming Engine看到的最大EventTime - 迟到阈值(delayThreshold)。

  3. 窗口销毁的时机: Watermark >= Window End Time。如: 当Watermark: 2016-01-01 10:02:30大于等于窗口W: [2016-01-01 10:02:00, 2016-01-01 10:02:30)的结束时间时,销毁该窗口W。换句话,也可以这样讲,当Structured Streaming看到的最大EventTime-迟到阈值(delayThreshold)>=在时间T结束的特定窗口W,会清除该窗口W的状态。

  4. 在窗口销毁前,只要有数据进来,就会触发窗口聚合并输出。

  5. 在窗口销毁后,依然有迟到的数据,默认会被丢弃(这部分如能不能单独收集起来或单独处理,没有找到好的方式)。

  6. Complete输出模式下,不会删除旧的聚合状态,不论数据迟到多久,都会触发聚合并输出。

  • 1
    点赞
  • 2
    评论
  • 1
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

相关推荐
©️2020 CSDN 皮肤主题: 精致技术 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值