「Spark-2.2.0」Structured Streaming - Watermarking操作详解

很高兴spark-2.2.0于昨天(2017.7.12)发布,结构化流式处理在该版本中可用于生产环境。

Spark Streaming 中 Exactly Once 指的是:

  • 每条数据从输入源传递到 Spark 应用程序 Exactly Once
  • 每条数据只会分到 Exactly Once batch 处理
  • 输出端文件系统保证幂等关系


streaming DataFrames/Datasets的操作


Structured Streaming 返回的是 DataFrame/DataSet,我们可以对其应用各种操作 - 从无类型,类似 SQL 的操作(例如 selectwheregroupBy)到类型化的 RDD 类操作(例如 mapfilterflatMap)。

基本操作:选择,投影,聚合

支持大多数的DataFrame/DataSet操作。
case class DeviceData(device: String, deviceType: String, signal: Double, time: DateTime)

val df: DataFrame = ... // streaming DataFrame with IOT device data with schema { device: string, deviceType: string, signal: double, time: string }
val ds: Dataset[DeviceData] = df.as[DeviceData]    // streaming Dataset with IOT device data

// Select the devices which have signal more than 10
df.select("device").where("signal > 10")      // using untyped APIs   
ds.filter(_.signal > 10).map(_.device)         // using typed APIs

// Running count of the number of updates for each device type
df.groupBy("deviceType").count()                          // using untyped API

// Running average signal for each device type
import org.apache.spark.sql.expressions.scalalang.typed
ds.groupByKey(_.deviceType).agg(typed.avg(_.signal))    // using typed API

不支持的操作:

但是,不是所有适用于静态 DataFrames/DataSet 的操作在流式 DataFrames/DataSet 中受支持。从 Spark 2.0 开始,一些不受支持的操作如下:

  • 在流 DataFrame/DataSet 上还不支持多个流聚集(即,流 DF 上的聚合链)。
  • 不支持 limit 和 take(N)
  • 不支持 Distinct
  • sort 操作仅在聚合后在完整输出模式下支持
  • 流和静态流的外连接支持是有条件的:
    • 不支持带有流 DataSet 的完全外连接
    • 不支持右侧的流的左外连接
    • 不支持左侧的流的右外部联接
  • 不支持两个流之间的任何 join
  • 此外,还有一些方法不能用于流DataSet,它们是将立即运行查询并返回结果的操作,这对流DataSet没有意义。相反,这些功能可以通过显式地启动流查询来完成。
  • count() - 无法从流 DataSet 返回单个计数。
    相反,使用 ds.groupBy.count() 返回包含运行计数的流DataSet
  • foreach() - 使用 ds.writeStream.foreach(...)(参见下一节)。
  • show() - 而是使用控制台接收器

如果您尝试任何这些操作,您将看到一个 AnalysisException 如“操作 XYZ 不支持与流 DataFrames/DataSet”。

事件时间上的窗口操作

事件时间是嵌入在数据本身的时间,对于许多应用程序,我们可能希望根据事件时间进行聚合操作,为此,Spark2.x 提供了基于滑动窗口的事件时间集合操作。通过结构化流式,滑动事件时间窗口的聚合很简单,与分组聚合非常相似。在分组聚合中,依据用户指定的分组列中的每个唯一值维护聚合值(例如计数),在基于窗口的聚合的情况下,对于行的事件时间落入的每个窗口维持聚合值。让我们以一个例证了解这一点。

想象一下,我们的快速示例被修改,并且流现在包含生成行的时间和行。我们不想运行字数,而是要在10分钟的窗口内计数单词,每5分钟更新一次。也就是说,在10分钟的窗口12:00 - 12:10,12:05 - 12:15,12:10 - 12:20等之间收到的单词计数。请注意,12:00 - 12:10表示数据12点后到12点10分抵达。现在,考虑在12:07收到的一个字。这个词应该增加对应于两个窗口的计数12:00 - 12:10和12:05 - 12:15。所以(计数)将由两者分组键(即单词)和窗口(可以从事件时间计算)进行索引。

结果表将如下所示。

「Spark-2.2.0」Structured Streaming - Watermarking操作详解_第1张图片

structured-streaming-window

 由于此窗口类似于分组,因此在代码中,您可以使用groupBy()window()操作来表达窗口聚合。您可以在Scala / Java / Python中看到以下示例的完整代码 。

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.groupBy(
  window($"timestamp", "10 minutes", "5 minutes"),
  $"word"
).count()

该段代码用于统计每10分钟内,接受到的不同词的个数,其中window($"timestamp", "10 minutes", "5 minutes")的含义为:假设初始时间 t=12:00,定义时间窗口为10分钟,每5分钟窗口滑动一次,也就是每5分钟对大小为10分钟的时间窗口进行一次聚合操作,并且聚合操作完成后,窗口向前滑动5分钟,产生新的窗口,如上图的一些列窗口 12:00-12:10,12:05-12:15,12:10-12:20。

在这里每个word包含两个时间,word产生的时间和流接收到word的时间,这里的timestamp就是word产生的时间,在很多情况下,word产生后,可能会延迟很久才被流接收,为了处理这种情况,Structured Streaming 引进了Watermarking(时间水印)功能,以保证能正确的对流的聚合结构进行更新. 

例如,说在12:04(即事件时间)生成的一个字可以在12:11被应用程序接收。应用程序应该使用12:04而不是12:11来更新窗口的较旧计数12:00 - 12:10这在我们基于窗口的分组中自然发生 - 结构化流可以长时间维持部分聚合的中间状态,以便后期数据可以正确地更新旧窗口的聚合,如下所示。

 
「Spark-2.2.0」Structured Streaming - Watermarking操作详解_第2张图片

 
structured-streaming-late-data
(就是把落下的数据加到它本来应该在的位置呗)


但是,为了每运行这个查询,系统必须限制其积累在内存的中间状态的数量。这意味着系统需要知道何时可以从内存状态中删除旧聚合,因为应用程序不会再为该集合接收到较晚的数据。为了实现这一点,在Spark 2.1中,我们引入了 Watermarking,这使得引擎可以自动跟踪数据中的当前事件时间,并尝试相应地清除旧状态。您可以通过指定事件时间列来定义查询的Watermarking,并根据事件时间预测数据的延迟时间。对于从时间开始的特定窗口T,引擎将保持状态,并允许后期数据更新状态,直到(max event time seen by the engine - late threshold > T)换一种说法,阈值内的滞后数据将被聚合,但是晚于阈值的数据将被丢弃让我们以一个例子来理解这一点。通过withWatermark()方法,我们可以很容易地定义上一个例子中的Watermarking,如下所示。

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()

这是一个例子。

在这个例子中,我们正在定义“timestamp”列查询的Watermarking,并将“10分钟”定义为允许数据延迟的阈值如果此查询在更新输出模式下运行(稍后在“输出模式”部分中讨论),则引擎将不断更新结果表中窗口的计数,直到 window Watermarking(在“timestamp“下为10分钟更旧。



Watermarking的计算方法

 「Spark-2.2.0」Structured Streaming - Watermarking操作详解_第3张图片

  • In every trigger, while aggregate the data, we also scan for the max value of event time in the trigger data
  • After trigger completes, compute watermark = MAX(event time before trigger, max event time in trigger)

Watermarking表示多长时间以前的数据将不再更新,也就是说每次窗口滑动之前会进行Watermarking的计算:

首先 统计 这次聚合操作返回的最大事件时间(也就是处理上一次事件的时间 12:14dog)

然后  减去 所然忍受的延迟时间 (阈值10m)                       就是Watermarking(12:04),

当一组数据或新接收的数据事件时间小于Watermarking时,则该数据不会更新,在内存中就不会维护该组数据的状态

如图所示,最大事件时间跟踪引擎 蓝色虚线 ,水印设置为(max event time - '10 mins') 每个触发开始时设置的水印是红线。例如,当引擎观察数据为 (12:14, dog)时,它将为下一个触发器为12:04。该水印允许发动机保持中间状态另外10分钟,以允许后期数据计数。例如,该数据(12:09, cat)是乱序和晚期,它落在窗户12:05 - 12:1512:10 - 12:20。由于12:04它仍然在触发器的水印之前,引擎仍然将中间计数保持为状态,并正确更新相关窗口的计数。然而,当水印更新到12:11,最近窗口(12:00 - 12:10)清除窗口的中间状态,并将所有后续数据(例如(12:04, donkey))视为“太晚”,因此被忽略。请注意,按照更新模式的规定,每次触发后,更新的计数(即紫色行)将被写入作为触发输出。

某些接收器(例如文件)可能不支持更新模式所需的细粒度更新。要与他们一起工作,我们还支持Append(附加模式),只有最后的计数被写入sink这如下所示。

注:withWatermark在非流数据集上使用是无操作的。由于水印不应以任何方式影响任何批次查询,我们将直接忽略它。

「Spark-2.2.0」Structured Streaming - Watermarking操作详解_第4张图片

与之前的更新模式类似,引擎维护每个窗口的中间计数。但是,部分计数不会更新到结果表,而不是写入sink。发动机等待迟到的“10分钟”进行计数,然后将窗口<水印的中间状态丢弃,并将最终计数附加到结果表/汇点。例如,窗口的最后计数12:00 - 12:10只有在水印更新到之后才会附加到结果表12:11


Structured Streaming 支持两种更新模式:

  1. Update 删除不再更新的时间窗口,每次触发聚合操作时,输出更新的窗口

「Spark-2.2.0」Structured Streaming - Watermarking操作详解_第5张图片
 
structured-streaming-watermark-update-mode


2. Append 当确定不会更新窗口时,将会输出该窗口的数据并删除,保证每个窗口的数据只会输出一次

 

「Spark-2.2.0」Structured Streaming - Watermarking操作详解_第6张图片
 
structured-streaming-watermark-append-mode

3.  Complete  不删除任何数据,在 Result Table 中保留所有数据,每次触发操作输出所有窗口数据

 


与之前的更新模式类似,引擎维护每个窗口的中间计数。但是,部分计数不会更新到结果表,也不写入sink。计算引擎等待迟到“10分钟”内进行计数,然后将(窗口 < 水印)的中间状态丢弃,并将最终计数附加到 结果表/sink例如,窗口的最后计数12:00 - 12:10只有在水印更新到之后才会附加到结果表12:11

水印清理聚合状态 的条件重要的是要注意,为了清除聚合查询中的状态(从Spark 2.1.1开始,将来会更改),必须满足以下条件。

  • 输出模式必须是追加或更新。完成模式要求保留所有聚合数据,因此不能使用水印来中断状态。有关 每种输出模式的语义的详细说明,请参见“ 输出模式”部分。

  • 聚合必须具有事件时间列或window事件时间列上的一个。

  • withWatermark必须在与聚合中使用的时间戳列相同的列上调用。例如, df.withWatermark("time", "1 min").groupBy("time2").count()在附加输出模式中无效,因为在聚合列的不同列上定义了水印。

  • withWatermark必须在聚合之前调用要使用的水印细节。例如,df.groupBy("time").count().withWatermark("time", "1 min")在(追加)输出模式中无效。

正确参考如下:

val windowedCounts = words
    .withWatermark("timestamp", "10 minutes")
    .groupBy(
        window($"timestamp", "10 minutes", "5 minutes"),
        $"word")
    .count()


你可能感兴趣的:(SparkStreaming)