本文旨在弄清楚Spark Structured Streaming EventTime下Watermark生成与Window触发相关问题。
-
窗口起止时间。
-
水印的生成。
-
对迟到数据的处理。
-
窗口销毁的时机。
-
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)
}
}
调式验证
| 序号 | Record | EventTime | EventTime 对应的 Watermark | EventTime 对应的Window起止时间 | 触发Window聚合 | 输出 |
|---|---|---|---|---|---|---|
| 1 | {“eventTime”: “2016-01-01 10:02:00”, “eventType”: “browse”} | 2016-01-01 10:02:00 | 2016-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:05 | 2016-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:10 | 2016-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:15 | 2016-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:20 | 2016-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:25 | 2016-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:29 | 2016-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:30 | 2016-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:25 | 2016-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:35 | 2016-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:20 | 2016-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:40 | 2016-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:25 | 2016-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:29 | 2016-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:45 | 2016-01-01 10:02:35 | [2016-01-01 10:02:30, 2016-01-01 10:03:00) | 触发 | click: 4 |
重点说明
-
窗口长度30秒,最大迟到10秒。
-
以上验证是在
Update输出模式下。 -
Watermark实际上是Timestamp类型,格式如:
2016-01-01T02:02:00.000Z,这里为方便对齐比较,将Watermark加了8小时。 -
当输入记录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聚合。 -
当输入记录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聚合。 -
当输入记录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(或清除该窗口的状态)。 -
当输入记录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的聚合。该事件被丢弃。 -
当输入记录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的聚合。该事件被丢弃。
总结
-
窗口起止时间: 前闭后开的自然时间。
举例:
30s一个窗口,窗口间隔为[00:00:00, 00:00:30)、[00:00:30, 00:01:00)、[00:01:00, 00:01:30)… 以此类推。 -
Watermark计算逻辑:Structured Streaming Engine看到的最大EventTime - 迟到阈值(delayThreshold)。
-
窗口销毁的时机: 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的状态。 -
在窗口销毁前,只要有数据进来,就会触发窗口聚合并输出。
-
在窗口销毁后,依然有迟到的数据,默认会被丢弃(这部分如能不能单独收集起来或单独处理,没有找到好的方式)。
-
在
Complete输出模式下,不会删除旧的聚合状态,不论数据迟到多久,都会触发聚合并输出。

1627

被折叠的 条评论
为什么被折叠?



