文章目录


作用

在数据分析系统中,Struct Strreaming可以持续按照事件时间聚合数据,在此过程中并不能保证数据按照事件时间大小依次达到,在某一个时刻接受到数据远远落后之前批次已经处理过的事件时间,发生这种情况时,需要结合业务需要对延迟数据进行过滤

默认情况下,无论数据延迟多久,数据根据事件时间生成若干静态的时间窗口,即使延迟,也能按照所属的时间窗口正确聚合,该情况下,数据完成聚合已经输出也不能重内存中移除,需要保留当前状态,随着数据不断流入,状态持续增长,造成程序不稳定

水印主要解决以下问题

(1) 处理聚合过程中的延迟数据

(2)减少内存中维护的聚合状态

基于update模式,实现wordCount,结合waterMark处理延迟数据

package struct

import java.text.SimpleDateFormat

import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.execution.streaming.FileStreamSource.Timestamp
import org.apache.spark.sql.streaming.Trigger

object StructStream08 {
def main(args: Array[String]): Unit = {
val spark = SparkSession
.builder
.master("local[*]")
.appName("StructStream08")
.getOrCreate()
val lines = spark.readStream
.format("socket")
.option("host", "note01")
.option("port", 9999)
.load()
val sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
import org.apache.spark.sql.functions._
import spark.implicits._
val words = lines.as[String].map(s => {
val arr = s.split(",")
val date = sdf.parse(arr(0))
(new Timestamp(date.getTime), arr(1))
}).toDF("ts", "word")

val wordCounts = words.withWatermark("ts", "2 minutes")
.groupBy(
window($"ts", "10 minutes", "2 minutes")
, $"word"
).count()
val query = wordCounts.writeStream
.outputMode("update")
.trigger(Trigger.ProcessingTime(0))
.format("console")
.start()

query.awaitTermination()
}
}

通过withWatermark方法设置Delay Threshold(延迟阈值)


ts:事件时间所在列名
2 minutes : Delay Threshold(延迟阈值)数据允许延迟的时长为2 min,waterMark 为已处理的最大事件时间允许延迟时间,程序会根据每一批数据重新计算waterMark,但是waterMark只会递增不会减少


基于Append模式 ,实现wordCount,结合waterMark处理延迟数据

...
val query = wordCounts.writeStream
.outputMode("append")
.trigger(Trigger.ProcessingTime(0))
.format("console")
.start()
...

只输出新增数据,输出后数据无法变更

底层工作原理

将Threshold(延迟时长)转换为CalendarInterval实例,转入时间格式不正确抛出异常,如果解析完事负数,抛出异常

def withWatermark(eventTime: String, delayThreshold: String): Dataset[T] = withTypedPlan {
val parsedDelay =
Option(CalendarInterval.fromString("interval " + delayThreshold))
.getOrElse(throw new AnalysisException(s"Unable to parse time delay '$delayThreshold'"))
require(parsedDelay.milliseconds >= 0 && parsedDelay.months >= 0,
s"delay threshold ($delayThreshold) should not be negative.")
EliminateEventTimeWatermark(
EventTimeWatermark(UnresolvedAttribute(eventTime), parsedDelay, logicalPlan))
}

通过正则匹配时间,然后定义查找最大事件时间,通过累加器查找分区中最大事件时间,实例化注册累加器并更新Watermark,在每一个当前批次中,使用上一个批次计算得出的Watermark过滤过期数据,并清理过期状态

Watermark机制与输出模式

(1)Watermark在用于基于时间的有状态聚合操作时,该时间可以基于窗口也可以基于事件时间本身

(2)输出模式为(Complete)时,必须存在聚合操作,每次都要输出之前所有聚合结果,使用Watermark无意义

(3)输出模式为Append,设置Watermark使用聚合操作,从另一个层面说,Watermark定义在Append模式中何时输出聚合结果,并清理过期状态

(4)输出模式Update.Watermark主要用于过滤过期数据,并及时清理过期状态

(5)Watermark会在处理当前批次数据时进行更新,在处理下一批次生效,若节点故障,延迟若干批次生效