5 sparkStreaming
5.1 streaming 原理
5.1.1 原理
“准实时,微批次”的处理方式
- Spark Streaming将连续不断的数据按
固定的时间间隔进行切分,使其变为一个个连续的批次,进而按顺序对每一个批次进行处理
,一般情况下,每一个批次都会很小,这就是微批次
,可见SparkStreaming处理数据并非完全实时,而是按时间间隔进行处理,一般时间间隔为秒或毫秒级别,这就是准实时
。 - 时间间隔的长度,在初始化Spark Streaming 任务时进程是给定
接收器,执行器
- 可以看出spark streaming的实时计算由两个过程组成,采取数据和数据运算。因此提交任务时,Driver 需至少启动两个executor(工作节点),一个负责采集数据,称为接收器。一个负责数据的储存和计算,可以称为执行器。
- 接收器需不停的采集数据,因此
接收器需长期运行,所以需要使线程不能关闭
。 - 接收器每次传出的数据可以看成一个RDD
运行流程
- 当有Application任务提交到yarn时,driver会同时启动至少两个executor,一个用于接收数据,另一个用于处理数据。
- 当开始有流式数据进入到接收器时,接收器会将数据转化为Dstream,然后
每隔一个时间段进行将数据传输到其他节点进行保存,并将数据情况传输给driver
。 - driver接收到接收器的信息后,会根据逻辑产生一个个的task,传输到储存有流式数据的节点上进行运行。
5.1.2 DStream
Spark Streaming的抽象数据结构:DStream – 离散化的RDD
- DStream为接收器的返回对象,其是随时间推移的数据序列,是一个离散化的流数据。而序列中的数据类型为RDD,也就是说
DStream为不同时间段的RDD组成的序列
。
所有的RDD不可能同时共存!但可能出现多个共存的情况,比如设置窗口
- DStream中的RDD来一个就会被计算一个,因此不会出现共存的情况,但是
当一个RDD计算时间大于采集数据间隔时,就会出现RDD共存的情况,因为新到来的RDD需要等待前一个RDD计算完毕
。因此Spark Streaming应该有较大的算力。
5.1.3 workcount实现
- 一个简单的例子来演示Spark Streaming的使用
package wordcount
import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
import org.apache.spark.streaming.{Seconds, StreamingContext}
object wordcount{
def main(args: Array[String]): Unit = {
// TODO 创建spark集群的连接
"""至少要执行两个线程"""
val conf: SparkConf = new SparkConf().setAppName("scSpark").setMaster("local[*]")
""" 创建sparkStreaming的连接"""
val ssc: StreamingContext = new StreamingContext(conf, Seconds(3)) // 每3秒接受一次
// TODO 连接到数据原
"用netcat模拟数据源,在Linux的node2上"
val dStream: ReceiverInputDStream[String] = ssc.socketTextStream("node2", 6666) // 指定数据源的ip与端口
"进行wordcount"
val dStream1: DStream[(String, Int)] = dStream.flatMap(_.split(" ")).map((_, 1)).reduceByKey(_ + _) // 输出
dStream1.print()
// TODO 开启任务
ssc.start()
// TODO 任务持续执行,处理下一个批次
"线程需要不断的运行,因为接收器需要不断运行"
ssc.awaitTermination()
}
}
5.2 DStream输入
DStream的数据可以有三种方式,分别为文件源输入,RDD队列输入,自定义数据源
5.2.1 文件源输入
- ⽂件数据流:能够读取所有HDFS API兼容的⽂件系统⽂件,通过fileStream⽅法进⾏读取,Spark Streaming 将会监控 dataDirectory ⽬录,并不断处理移动进来的⽂件,记住⽬前不⽀持嵌套⽬录。
只有新移动进来的数据才会被监视,原有的数据不会被监视
streamingContext.textFileStream(dataDirectory)
5.2.2 RDD队列
- 测试过程中,可以通过使⽤ssc.queueStream(queueOfRDDs)来创建DStream,每⼀个推送到这个队列 中的RDD,都会作为⼀个DStream处理。
ssc.queueStream(queueOfRDDs)
5.2.3 自定义数据源
- 自定义数据源主要是
继承Receiver,并重写onStart,onStop
,来采集数据源。 - 调用方法:val lineStream = ssc.receiverStream(new CustomerReceiver(“hadoop01”, 6666))
import java.io.{BufferedReader,InputStreamReader}
import java.net.Socket
import java.nio.charset.StandardCharsets
import org.apache.spark.storage.StorageLevel
import org.apache.spark.streaming.receiver.Receiver
class CustomerReceiver(host: String,port: Int) extends Receiver[String]
(StorageLevel.MEMORY_ONLY) {
//最初启动的时候,调⽤该⽅法,作⽤为:读数据并将数据发送给Spark
override def onStart(): Unit = {
new Thread("Socket Receiver") {
override def run() {
receive()
}
}.start()
}
//读数据并将数据发送给Spark
def receive(): Unit = {
//创建⼀个Socket
var socket: Socket = new Socket(host,port)
//定义⼀个变量,⽤来接收端⼝传过来的数据
var input: String = null
//创建⼀个BufferedReader⽤于读取端⼝传来的数据
val reader = new BufferedReader(new InputStreamReader(socket.getInputStream,StandardCharsets.UTF_8))
//读取数据
input = reader.readLine()
//当receiver没有关闭并且输⼊数据不为空,
while (!isStopped() && input != null) {
store(input)//如果接收到了数据就保存
input = reader.readLine()
}
//跳出循环则关闭资源
reader.close()
socket.close()
//重新连接重新执⾏Onstart⽅法,参数只要是字符串就可以
restart("restart")
}
//程序停⽌的时候执⾏
override def onStop(): Unit = {}
}
"调用"
val lineStream = ssc.receiverStream(**new CustomerReceiver("hadoop01", 6666)**)
5.3 DStream算子
DStream算子可分为两种,转化算子和输出算子,转化算子又可分为无转化转化和有状态转化
5.3.1 无状态转化
- ⽆状态转换就是当前算⼦应⽤完就完事不会对之前的算⼦产⽣影响,因为SparkStreaming是批次获取数据,所以批次和批次之间所产⽣的数据是没有影响的,只会对当前的有影响,如果在wordcount中,每一次输出只会记录当前RDD的单词个数,并没有和之前RDD一起进行记录,因此为无状态转化。
- 需要记住的是,
尽管这些函数看起来像作⽤在整个流上⼀样,但事实上每个DStream在内部是由许多 RDD(批次)组成,且⽆状态转化操作是分别应⽤到每个RDD上的
。例如,reduceByKey()会归约每个时 间区间中的数据,但不会归约不同区间之间的数据
。 - ⽆状态转化操作也能在多个DStream间整合数据,不过也是在各个时间区间内。例如,键 值对 DStream拥有和RDD⼀样的与连接相关的转化操作,也就是cogroup()、join()、leftOuterJoin() 等。我 们可以在DStream上使⽤这些操作,这样就对每个批次分别执⾏了对应的RDD操作。
- RDD相关的算子都能应用到DStream上。
5.3.2 有状态转化
- 有状态转化调用该算子会使当前的RDD在前方RDD的计算结果上进行计算,也就是每个批次的RDD计算结果都会进行叠加,同时对下一次运算产生影响。例如wordcount想统计整个流的数据,则需要不断将RDD计算结果进行累加。
- 可见,实现有转化转化的主要思路为
将每次RDD的计算结果进行保存
,主要的算子有:updataStateByKey,transform,mapWithState
(1) updateStateByKey
-
该算子用于
键值对类型的DStream
,可将其根据键值对
进行逻辑运算,然后将运算结果进行保存,供后续RDD进行使用。实现过程:当DStream调用updateStateByKey后,算子会对DStream本轮的RDD根据key值进行分组,获得各个分组的序列Seq[V],对于每一个序列Seq[V],算子会从检查点中提前对应key值的当前状态Option[S],传入状态更新函数updateFunc获得新的状态Option[S],保存于检查点,以此类推。
因此在实现时有两个步骤:定义状态,定义状态更新函数
-
函数的参数有多种,主要有以下方式:
"""传输为状态更新函数,返回类型为DStream[(K, S)]""" def updateStateByKey[S: ClassTag]( updateFunc: (Seq[V], Option[S]) => Option[S] ): DStream[(K, S)]
-
状态更新函数
:updateFunc:(Seq[V], Option[S]) => Option[S] """ 可见,状态更新函数传入两个参数,返回一个参数 参数1:Seq[V],表示当前批次,相同key值的value组成的序列。 参数2:Option[S],当前key值,对应的状态,即以往批次的计算结果。option是可选的意思,即该参数可能为None或S,(因为并不是所有eky值都会出现再一个批次中) 返回值:Option[S],经过本来计算后的新的状态结果。 """ // 可以看出,第二个参数和返回值类型一样,这是因为都是状态
-
注意:
由上述描述可以看出,每次计算结果都会进行保存,因此需要为计算过程记录一个保存点,可以是本地或者HDFS路径,可通过以下函数指定。
ssc.checkpoint(path)
-
例子:
有状态的实现wordcount,即每次统计结果进行累加
def main(args: Array[String]): Unit = { val conf: SparkConf = new SparkConf().setMaster("local[3]").setAppName("update") val ssc: StreamingContext = new StreamingContext(conf, Seconds(2)) "1. TODO 连接到端口" val Dstream: ReceiverInputDStream[String] = ssc.socketTextStream("localhost", 6666) "2. 设置状态存放记录的位置" ssc.checkpoint("out") "3. UpdateStateByKey需要接收对(k,v)类型进行操作,所以先转化为(k,v)类型" val DStream1: DStream[(String, Int)] = Dstream.flatMap(_.split(' ')).map((_, 1)) "4. 使用UpdateStateByKey,传入参数为状态更新函数" DStream1.updateStateByKey(updatefun1) sumd.print() ssc.start() ssc.awaitTermination() } "5. 实现状态更新函数 " def updatefun1: (Seq[Int], Option[Int]) => Some[Int] = (values:Seq[Int], state:Option[Int])=>{ "传入参数values为序列,state为对应的状态值,状态值可能不存在,所以未option类型" val now_sum: Int = values.sum val last_sum: Int = state.getOrElse(0) Some(now_sum + last_sum) // 可选,要么是None,要么是Int,其是Option的一个子类 }
(2) mapWithState
-
该算子与updateStateByKey的作用相同,同是有状态的转化,且同样根据key值对数据进行处理,只不要每次处理,mapWithState只放回产生变化的key,效率更高。
-
相同点
:- 为有状态转化
- 同样是针对KV类型数据
-
不同点
:-
checkpoint储存情况不同,每次迭代updateStateByKey会对所有数据进行输出,输出是增量的,当数据量较大时,占用内存过高,且小文件较多,对HDFS不友好;mapWithState只关系本批次中的更新情况,所需要的内存少。
-
可实现的功能不同:后者可以实现更多的功能。
mapWithState是可以储存任何类型的状态,而updateStateByKey只能储存Option[(k,v)]类型的数据
后者可以
初始化状态
后者可以
设置超时时间
,当某个key值超过一定时间未更新时,将会取消该key值。
-
-
源码实现:
/** * :: Experimental :: * Return a [[MapWithStateDStream]] by applying a function to every key-value element of * `this` stream, while maintaining some state data for each unique key. The mapping function * and other specification (e.g. partitioners, timeouts, initial state data, etc.) of this * transformation can be specified using `StateSpec` class. The state data is accessible in * as a parameter of type `State` in the mapping function. * * Example of using `mapWithState`: * {{{ * // A mapping function that maintains an integer state and return a String * def mappingFunction(key: String, value: Option[Int], state: State[Int]): Option[String] = { * // Use state.exists(), state.get(), state.update() and state.remove() * // to manage state, and return the necessary string * } * * val spec = StateSpec.function(mappingFunction).numPartitions(10) * * val mapWithStateDStream = keyValueDStream.mapWithState[StateType, MappedType](spec) * }}} * * @param spec Specification of this transformation * @tparam StateType Class type of the state data * @tparam MappedType Class type of the mapped data */ @Experimental def mapWithState[StateType: ClassTag, MappedType: ClassTag]( spec: StateSpec[K, V, StateType, MappedType] ): MapWithStateDStream[K, V, StateType, MappedType] = { new MapWithStateDStreamImpl[K, V, StateType, MappedType]( self, spec.asInstanceOf[StateSpecImpl[K, V, StateType, MappedType]] ) }
由源码可以看出,mapWithState的实现有以下几个步骤
-
确定状态数据类型StateMap,确定返回数据类型MapType,定义状态更新函数
def mappingFunction(key: String, value: Option[Int], state: State[StateMap]): Option[MapType] = { """ 第一二个参数为键值对 第三个参数为状态值,State类可以理解为Option,通过state.getOption()可转化为Option类型。 返回类型为Option[String],可以根据需要确定返回类型。 """ command... state.update(值) // 更新状态❤️ MapType // 返回值❤️ } ps:返回类型和状态并不一样,状态储存于checkpoint,返回类型是我们想要的数据。在updateStateByKey中两者需一致。
-
确定初始值和超时时间
import org.apache.spark.streaming.{Seconds, State, StateSpec, StreamingContext} val initialRDD = ssc.sparkContext.makeRDD(List[MapType](初始化状态)) // 初始化状态 val spec = StateSpec.function(mappingFunction).initialState(initialRDD).timeout(10)
-
使用算子
DStream1.mapWithState[StateType, MappedType](spec)
实现wordcount
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setMaster("local[3]").setAppName("mapWithState_1")
val ssc = new StreamingContext(conf, Seconds(3))
val DStream: ReceiverInputDStream[String] = ssc.socketTextStream("localhost", 6666)
"checkPoint位置"
ssc.checkpoint("out")
val DStream1: DStream[(String, Int)] = DStream.flatMap(_.split(' ')).map((_, 1)) // 将数据转化为 KV类型
// 初始化状态
val initialRDD: RDD[(String, Int)] = ssc.sparkContext.makeRDD(List(("hello", 10), ("spark", 1)))
// 确定初始值和超时时间
val spec = StateSpec.function(mappingFunction).initialState(initialRDD).timeout(10)
// 使用算子
val MapDStream: MapWithStateDStream[String, Int, Int, (String, Int)] = DStream1.mapWithState(spec)
MapDStream.print()
ssc.start()
ssc.awaitTermination()
}
" 状态转化函数 "
def mappingFunction = (key:String, value: Option[Int], state:State[Int])=>{
val sum: Int = value.getOrElse(0) + state.getOption().getOrElse(0)
val output: (String, Int) = (key, sum) // 返回值
state.update(sum) // 状态更新
output // 返回值
}
(3) transform / foreachRDD
- 这两个算子都是对DStream中的每一个RDD进行操作的算子,该函数每一个批次调用一次,因此可以通过该算子可以使DStream变相使用RDD。
- 不同: 前者主要使用RDD-to-RDD的函数。后者都能使用。
5.4 窗口操作
5.4.1 作用
window机制使Spark Streaming可以同时处理连续多个批次的数据
- Spark Streaming将数据分为一个一个批次传入spark中,一般情况下,每一个批次处理一次。当我们需要对多个批次进行处理时,这个时候就需要设置窗口了。
- 设置窗口可以将多个批次中的数据放入窗口去,并通过
滑动步长
来决定每次对窗口进行运算的时间. - 不属于窗口的数据将被丢弃
批次,窗口大小,滑动长度的关系
窗口大小,滑动长度需是批次的倍数
,这是因为批次不可分,需要使多个批次处于一个窗口- 窗口运行的流程为:
- 生成窗口
- 对窗口内数据进行计算
- 窗口滑动
- 获得批次
- 返回第一步
- 窗口可以保存多个批次的数据,如图配置情况,每一个批次为5s;窗口长度为15s,则每一个窗口将保存过去15s的数据,滑动步长为10s,则每10秒将统计一次窗口中的数据。总体而言该图实现的情况功能为:
每隔10s统计前15s的数据情况
.
5.4.2 算子
函数 | 作用 |
---|---|
window (windowLength,slideInterval) | 返回基于源DStream的窗⼝批次计算的新 DStream。 |
countByWindow (windowLength, slideInterval) | 返回指定⻓度窗⼝中的元素个数 |
reduceByWindow (func,windowLength, slideInterval) | 返回⼀个新的单元素流,通过使⽤func在滑动 间隔中通过在流中聚合元素创建。 |
reduceByKeyAndWindow (func, windowLength,slideInterval,[ numTasks ]) | 当对(K,V)对的DStream进⾏调⽤时,返 回(K,V)对的新DStream,其中每个键的 值 在滑动窗⼝中使⽤给定的减少函数func进 ⾏聚合。 |
countByValueAndWindow (windowLength, slideInterval,[numTasks ]) | 当调⽤(K,V)对的DStream时,返回(K, Long)对的新DStream,其中每个键的值是 其滑动窗⼝内的频率。 |
6 structured streaming
官方文档<Structured Streaming Programming Guide - Spark 3.4.0 Documentation>
6.1 概述
- Structured Streaming (结构化数据流)的关键思想是
将实时数据流视为一个不断追加的表格
。这导致了一种新的流处理模型,非常类似于批处理模型。您可以将流计算表达为标准批处理查询,就像在静态表格上一样,Spark 将其作为增量查询
在无界输入表格上运行。
6.1.1 核心设计
unbounded table
- 将已输入的数据视为一个表格,而新进入的数据视为新的行append到表格中去,由于数据使连绵不断的,所以该表格可以抽象为
unbounded(无界)表格
input、result、output
-
其中unbound表格可以抽象为三种表格:input table,result table,output table。
input table
由每一条输入的数据组成
,即每一次触发间隔(窗口滑动时长),新的行数据将被添加到input中去。每一个query(查询)即对input table进行查询,将会生成result table
,而每次新的数据进入input table,result table也会随着进行更新
。当我们需要将result table输出到工作台上时,就形成了ouput table。
table并非随时间而不断变大
structured streaming将每一个条数据append到input,但是也会对旧的数据进行删除。它从数据源中获得可用的流数据,对其进行增量处理以更新result table,随后将其进行舍弃,只保留更新result相关的最小中间状态数据(state)
所以说,其实table是不会无限增加的,result table也是如此,因为旧的数据一旦不需要就会被舍弃。而Output可能会无限增加,因为毕竟是外部储存了,无所谓。
6.1.2 output Mode
Complete、Append、Update
"Output"表示将result输出到外部,比如工作台、file文件、hdfs、Kafka等等。其可以被分为三种模式
Complete Mode
-每次触发query后,将整个result table都进行输出。如何处理表的写入由对应连接器决定。Append Mode
-每次触发query后,将得到的结果加入到result table中去,当引擎判断出某一行不再发生改变时,将该行进行输出
。一般的聚合会随时间影响,所以append不适用于一般的聚合。Update Mode
-每次触发query后,将result table中发生改变
的行进行输出。当query不是聚合查询时,Update Mode 等同于 Append Mode,因为不是聚合则不受时间影响,因此已经产生的result不再发生变化。
6.1.3 事件时间和延迟数据
structured streaming 处理对象对事件时间、且可以处理延迟数据
- 事件时间(event-time)是指时间产生的时间。对于spark streaming,其处理的时间形式为事件接收时间,即spark接收到事件的时间,而很多情况下,event-time时间才是我们需要统计的时间。structured streaming就是处理的event-time
在structured streaming中,所有的event最后都是input table中的一行,而对应的event-time是该行的timestamp列上
。在这样的模式下,流数据就是一张静态表,只需要简单的对event-time这一列进行groupby即可将生成多个窗口,然后就可以快速实现流数据的聚合。- 延迟数据是指
event的接收事件晚于event的产生时间
。由于event-time列的存在,structured streaming可以判断其是否属于延迟数据,来决定是否进行丢弃,还是更新result。而且structured streaming会不断检查result table中窗口的event-time,并对将旧的窗口进行删除,确保result中的内存不会过大。关于延迟数据的判断,以及result中窗口删除,可以通过定义watermark来实现,后续讲解。
6.1.3 一次性语义
一次性语义指:消费一次性、处理一致性、输出一致性
- 提供端到端的精确一次语义是 Structured Streaming 设计的关键目标之一。为了实现这一目标,我们设计了 Structured Streaming 的数据源、数据汇以及执行引擎,以可靠地跟踪处理的确切进度,从而能够通过重新启动和/或重新处理来处理任何类型的故障。假定每个流式数据源都有偏移量(类似于 Kafka 偏移量或 Kinesis 序列号)来跟踪流中的读取位置。引擎使用检查点和预写式日志来记录每个触发器中正在处理的数据的偏移量范围。流式数据汇被设计为幂等,以处理重新处理。通过使用可重放的数据源和幂等的数据汇,Structured Streaming 可以确保在任何故障情况下实现端到端的精确一次语义。
6.1.4 接口
spark sql的接口
- Structured Streaming 是基于 Spark SQL 引擎构建的可扩展和容错的流处理引擎。您可以像处理静态数据的批处理计算一样表达您的流处理计算。Spark SQL 引擎会持续地增量运行它,并在流式数据不断到达时更新最终结果。您可以使用 Scala、Java、Python 或 R 中的 Dataset/DataFrame API 表达流聚合、事件时间窗口、流到批处理连接等操作。这些计算都在同一个经过优化的 Spark SQL 引擎上执行。最后,系统通过检查点和预写式日志确保端到端的精确一次容错保证。简而言之,Structured Streaming 提供了快速、可扩展、容错、端到端精确一次的流处理,用户无需考虑流式数据的特性。
6.1.5 快速案例
import org.apache.spark.sql.functions._ // 导入需要的依赖包
import org.apache.spark.sql.SparkSession
// 连接到structured streaming
val spark = SparkSession
.builder
.appName("StructuredNetworkWordCount")
.getOrCreate()
import spark.implicits._
// Create DataFrame representing the stream of input lines from connection to localhost:9999
val lines = spark.readStream // 输入源,即input
.format("socket")
.option("host", "localhost")
.option("port", 9999)
.load()
// Split the lines into words
val words = lines.as[String].flatMap(_.split(" ")) // 返回result
// Generate running word count
val wordCounts = words.groupBy("value").count() // 聚合操作,返回result
// Start running the query that prints the running counts to the console
val query = wordCounts.writeStream
.outputMode("complete") // 输出模式
.format("console") // 输出对象
.start()
query.awaitTermination()
6.2 Spark Streaming vs Structured Streaming
6.2.1 spark streaming不足
-
前者处理是processing-time而不是event-time
,spark streaming处理数据是基于processing-time,这样不利于延迟数据的统计 -
end to end的原因:
spark streaming只能保存处理一致性,而不难保证输入,输出的一致性。
-
批流不统一
6.2.2 Structured Streaming的优势
-
模型简单:将流式数据作为一张表,便于理解
-
与SparkSql公用API,代码简洁。
-
性能卓越,Structured Streaming 在与 Spark SQL 共⽤ API 的同时,也直接使⽤了 Spark SQL 的 Catalyst 优化器和 Tungsten,数据处理性能⼗分出⾊。此外,Structured Streaming 还可以直接从未来 Spark SQL 的各种性能优化中受益。
-
end to end的一次性语义,确保了从input到output的一次处理
-
真正的流处理
延迟时间低,可扩展性弱
Structured Streaming是真正的流处理
- sparkstreaming不是流处理,而是批处理。而structured streaming是真正的流处理。
- 由图可以看出,structured streaming是真正的流处理,遇到数据就会延迟一点点然后进行计算,延迟时长可以设置。
6.3 Windows 和 watermark
6.3.1 窗口
不需要指定批次
- structured streaming不需要指定批次,它是流式数据,接到数据后就会进行操作,而不是每隔一个时间进行批次划分。集体流操作后续
讲解
窗口长度,滑动长度
- 窗口长度即input可以储存过往数据量的大小,滑动长度即每次统计的间隔,与spark streaming大致相同
简单实现方式
import spark.implicits._
// input
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.groupBy(
window($"timestamp", "10 minutes", "5 minutes"), // 根据timestamp建立窗口,大小为10分钟,滑动时长为5分钟
$"word"
).count()
6.3.2 watermark
watermark,用于判断延迟数据;watermark = max-event-time - late-threshold(延迟阈值)
watermark用于处理延迟数据,是引擎自动判断延迟到达的事件数据,是否删除还是用于更新result,以及判定result中的哪些窗口的state应该视为旧数据并进行删除
。- watermark = max-event-time - late-threshold,其中late-threshold为我们延迟数据的阈值,可以简单理解为允许迟到的时间。其中max-event-time表示上一次处理的事件中(滑动步长确定每一次处理的间隔),最晚的event-time。
- watermark的处理机制为:
- 每一次query后,根据max-event-time和设置的late-threshold更新watermark。
- 根据watermark对result table进行更新:将result中window都小于watermark的窗口进行删除,注意,
只有该窗口完全小于watermark后,该窗口才会被删除,若该窗口包含由watermark,该窗口不会被删除
。 - 对接受数据进行判断:当新接受到的数据在result table中没有对应的窗口时,该数据将被丢弃,反之则会对result table进行更新。
- watermark机制在不同的ouput模式中会有不同的体现形式。
watermark on update mode
-
在update中,引擎会不断更新result中所有窗口的state,直到该window小于watermark。
-
见图,
late-threshold = 10min,窗口长度为10m,滑动长度为5m
在12:15时触发了一次,该触发中最大的事件为12:14,经过计算得出watermark为12:04,
此时result中没有任何窗口完全小于watermark
。在12:20时触发了一次,此时所触发的最大事件为12:21,减掉late-threshold得到watermark为12:11,此时result中12:00~12:10的windows完全小于12:11,因此该窗口中result中进行删除。
在12:25触发时,收到了event-time为12:04的数据,该数据所属窗口12:00~12:10已经不存在,所以被舍弃。
- 由图可以看出,update Mode下,进行输出的result table是会发生变化的,也就是说,已经输出的结果会随时间发生变化,而有一些输出引擎并不支持这样的细粒度改变,如file(file并不支持一直改变,只支持追加),为了适应更多的输出引擎,还有append mode
watermark in append mode
- 如图为watermark在append mode上的情况,注意红字,
当result中窗口未小于watermark时,其不会作为output进行输出,只有当窗口小于watermark时,result才会作为output进行输出
。这就符合append的语义了,在append只会将result中不会发生改变后的行进行输出,而小于watermark的窗口不会再发生改变,所以其符合append的语义。
简单实现
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()