Handling Event-time and Late Data(处理事件时间和延迟数据)
事件时间是嵌入到数据本身中的时间。对于许多应用程序,您可能希望对这个事件时间进行操作。例如,如果您希望获得物联网设备每分钟生成的事件数,那么您可能希望使用数据生成时的时间(即数据中的事件时间),而不是Spark接收它们的时间。这个事件时间很自然地在这个模型中表示出来,来自设备的每个事件是表中的一行,而事件时间是行中的列值。这使得基于窗口的聚合(例如每分钟的事件数)成为事件时间列上的一种特殊类型的分组和聚合,每个时间窗口都是一个组,并且每一行都可以属于多个窗口/组。因此,可以在静态数据集(例如,从收集的设备事件日志)和数据流上一致地定义这种基于事件时间窗口的聚合查询,这使得用户的生活更加轻松。
此外,此模型自然会根据事件时间处理比预期晚到达的数据。因为Spark正在更新结果表,所以它可以完全控制在出现延迟数据时更新旧的聚合,以及清理旧的聚合以限制中间状态数据的大小。从Spark 2.1开始,我们支持水印,允许用户指定后期数据的阈值,允许引擎相应地清理旧状态。稍后将在窗口操作部分更详细地解释这些操作。
Window Operations on Event Time(事件时间上的窗口操作)
滑动事件时间窗口上的聚合与结构化流非常简单,非常类似于分组聚合。在分组聚合中,为用户指定的分组列中的每个惟一值维护聚合值(例如计数)。对于基于窗口的聚合,将为每个窗口维护聚合值,其中包含一行的事件时间。让我们用一个例子来理解它。
想象一下,我们的快速示例被修改了,流现在包含了行以及该行生成的时间。我们想要计算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收到的一个单词。此字应增加与两个窗口12:00 - 12:10和12:05 - 12:15对应的计数。因此计数将被分组键(即单词)和窗口(可以从事件时间计算)索引。
结果表将类似如下所示。
基本测试案例
object StructedStreamWordCountWindow {
def main(args: Array[String]): Unit = {
val spark = SparkSession
.builder
.appName("StructuredNetworkWordCount")
.master("local[*]")
.getOrCreate()
spark.sparkContext.setLogLevel("ERROR")
import spark.implicits._
val lines:DataFrame = spark.readStream
.format("socket")
.option("host", "train")
.option("port", 9999)
.load()
//word,templet
val word = lines.as[String]
.map(line => line.split(","))
.map(t => (t(0), new Timestamp(t(1).toLong)))
.toDF("word", "timestamp")
import org.apache.spark.sql.functions._
//将数据按窗口和单词分组,并计算每个组的计数
val wordCounts = word.groupBy(window($"timestamp", "4 seconds", "2 seconds"), $"word")
.count()
.map(row => {
var start = row.getStruct(0).getTimestamp(0)
var end = row.getStruct(0).getTimestamp(1)
var word = row.getString(1)
var count = row.getLong(2)
var sdf = new SimpleDateFormat("HH:mm:ss")
(sdf.format(start.getTime), sdf.format(end.getTime), word, count)
})
.toDF("start", "end", "word", "count")
//3.产生StreamQuery对象
val query:StreamingQuery = wordCounts.writeStream
.outputMode(OutputMode.Complete())
.format("console")
.start()
query.awaitTermination()
}
}
Handling Late Data and Watermarking(处理后期数据和水印)
现在,考虑如果其中一个事件延迟到应用程序会发生什么。例如,在12:04生成的单词(即事件时间)可以在12:11被应用程序接收。应用程序应该使用时间12:04而不是12:11来更新窗口12:00 - 12:10的旧计数。这在我们的基于窗口的分组中很自然地发生——结构化流可以在很长一段时间内保持部分聚合的中间状态,以便后期数据可以正确地更新旧窗口的聚合,如下所示。
在Spark 2.1中,我们引入了watermarking
,用于告知计算节点,何时丢弃窗口聚合状态。因为流计算是一个长时间运行的任务,系统不可能无限制存储一些过旧的状态值。使用watermarking
机制,系统可以删除那些过期的状态数据,用于释放内存。每个触发的窗口都有start time
和end time
属性,计算引擎会保留计算引擎所看到最大event time
watermark时间=max event time seen by the engine(数据的事件时间) - late threshold(允许迟到的时间)
如果watermarker时间T’ > 窗口的end time时间T则认为该窗口的计算状态可以丢弃
。
注意: 引入watermarker以后,用户只能使用update
、append
模式,系统才会删除过期数据。
update-水位线没有没过窗口的end time之前,如果有数据落入到该窗口,该窗口会重复触发。
val spark = SparkSession
.builder
.appName("StructuredNetworkWordCount")
.master("local[*]")
.getOrCreate()
spark.sparkContext.setLogLevel("ERROR")
import spark.implicits._
val lines:DataFrame = spark.readStream
.format("socket")
.option("host", "train")
.option("port", 9999)
.load()
//word,templet
val word = lines.as[String]
.map(line => line.split(","))
.map(t => (t(0), new Timestamp(t(1).toLong)))
.toDF("word", "timestamp")
import org.apache.spark.sql.functions._
//将数据按窗口和单词分组,并计算每个组的计数
val wordCounts = word.withWatermark("timestamp","1 second")
.groupBy(window($"timestamp", "4 seconds", "2 seconds"), $"word")
.count()
.map(row => {
var start = row.getStruct(0).getTimestamp(0)
var end = row.getStruct(0).getTimestamp(1)
var word = row.getString(1)
var count = row.getLong(2)
var sdf = new SimpleDateFormat("HH:mm:ss")
(sdf.format(start.getTime), sdf.format(end.getTime), word, count)
})
.toDF("start", "end", "word", "count")
//3.产生StreamQuery对象
val query:StreamingQuery = wordCounts.writeStream
.outputMode(OutputMode.Update())
.format("console")
.start()
query.awaitTermination()
Append–水位线没有没过窗口的end time之前,如果有数据落入到该窗口,该窗口不会触发,只会默默的计算,只有当水位线没过窗口的end time的时候,才会做出最终输出。
val spark = SparkSession
.builder
.appName("StructuredNetworkWordCount")
.master("local[*]")
.getOrCreate()
spark.sparkContext.setLogLevel("ERROR")
import spark.implicits._
val lines:DataFrame = spark.readStream
.format("socket")
.option("host", "train")
.option("port", 9999)
.load()
//word,templet
val word = lines.as[String]
.map(line => line.split(","))
.map(t => (t(0), new Timestamp(t(1).toLong)))
.toDF("word", "timestamp")
import org.apache.spark.sql.functions._
//将数据按窗口和单词分组,并计算每个组的计数
val wordCounts = word.withWatermark("timestamp","1 second")
.groupBy(window($"timestamp", "4 seconds", "2 seconds"), $"word")
.count()
.map(row => {
var start = row.getStruct(0).getTimestamp(0)
var end = row.getStruct(0).getTimestamp(1)
var word = row.getString(1)
var count = row.getLong(2)
var sdf = new SimpleDateFormat("HH:mm:ss")
(sdf.format(start.getTime), sdf.format(end.getTime), word, count)
})
.toDF("start", "end", "word", "count")
//3.产生StreamQuery对象
val query:StreamingQuery = wordCounts.writeStream
.outputMode(OutputMode.Append())
.format("console")
.start()
query.awaitTermination()
Semantic Guarantees of Aggregation with Watermarking(语义保证聚合与水印)
- 水印延迟(与水印一起设置)为“2小时”,保证引擎不会丢弃任何延迟小于2小时的数据。换句话说,任何在事件时间上少于2小时之前处理的最新数据都将被聚合。
- 然而,这种保证只在一个方向上是严格的。延迟超过2小时的数据不保证被删除;它可能聚合,也可能不聚合。数据越是延迟,引擎处理它的可能性就越小。