基于spark2.1的官方文档翻译而来
structured streaming是一种基于Spark SQL引擎构建的可扩展且容错的流处理引擎。 您可以以静态数据表示批量计算的方式来表达流式计算。 Spark SQL引擎将随着流式传输数据持续到达而逐渐持续运行,并更新最终结果。 您可以使用Scala,Java或Python中的Dataset / DataFrame API来表达流聚合(Streaming aggregation),事件时间窗口(event-time windows),流到批处理连接(stream-to-batch)等。计算在相同的优化的Spark SQL引擎上执行。 最后,系统通过检查点和预写日志(wal)确保端到端的一次容错(end-to-end exactly-once fault-tolerance)保证。 简而言之,structured streaming提供快速,可扩展,容错,端到端的一次(end-to-end exactly-once)流处理,而无需用户理解streamming。
Structured Streaming 在spark2.1版本仍然是测试版,我们将引导您完成学习编程模型和API。首先以一个简单的例子开始-a streaming word count。
假设您想要保持从监听TCP socket的数据服务器接收的文本数据运行word count。 让我们看看如何使用structured streaming表达这一点。 您可以在Scala / Java / Python中看到完整的代码。 如果您下载了Spark,可以直接运行示例。 在任何情况下,让我们逐步了解示例并了解它的工作原理。 首先,我们必须导入必要的类并创建一个本地SparkSession,这是与Spark相关的所有功能的起点。
import org.apache.spark.sql.functions._
import org.apache.spark.sql.SparkSession
val spark = SparkSession
.builder
.appName("StructuredNetworkWordCount")
.getOrCreate()
import spark.implicits._
接下来,我们创建一个streaming DataFrame,它表示从localhost:9999的侦听服务接收的文本数据,并转换这个DataFrame用来计算字数。
// Create DataFrame representing the stream of input lines from connection to localhost:9999
val lines = spark.readStream
.format("socket")
.option("host", "localhost")
.option("port", 9999)
.load()
// Split the lines into words
val words = lines.as[String].flatMap(_.split(" "))
// Generate running word count
val wordCounts = words.groupBy("value").count()
这行DataFrame表示一个包含流文本数据的无界表。 此表包含一列名为“value”的字符串,并且流文本数据中的每一行都将成为表中的一行。 请注意,这并不是正在收到任何数据,因为我们只是设置转换,还没有开始。 接下来,我们使用.as [String]将DataFrame转换为String的Dataset,以便我们可以应用flatMap操作将每一行分割成多个单词。 所得词汇Dataset包含所有单词。 最后,我们已经通过将数据集中唯一的值进行分组并对它们进行计数来定义wordCounts DataFrame。 请注意,这是一个streaming DataFrame,它表示运行的stream的word counts。
我们现在已经设置了关于streaming data的查询。 剩下的就是实际开始接收数据并计算数量。 为此,我们将其设置为在每次更新时将完整的计数集合(由outputMode(“complete”)指定)打印到控制台。 然后使用start()启动流计算。
// Start running the query that prints the running counts to the console
val query = wordCounts.writeStream
.outputMode("complete")
.format("console")
.start()
query.awaitTermination()
执行此代码后,流式计算将在后台启动。 查询对象是该活动流查询的句柄,我们已经决定使用query.awaitTermination()等待查询的终止,以防止查询处于活动状态时退出进程。
要实际执行此示例代码,您可以在自己的Spark应用程序中编译代码,或者在下载Spark之后运行该示例。 我们将介绍后者。 您将首先需要运行Netcat(大多数类Unix系统中的一个小型实用程序)作为数据服务器
$ nc -lk 9999
然后在另一个terminal,可以通过下面的代码运行例子:
$ ./bin/run-example org.apache.spark.examples.sql.streaming.StructuredNetworkWordCount localhost 9999
然后,在运行netcat服务器的终端中输入的任何行将每秒计数并打印在屏幕上。 它将看起来像下面这样:
# TERMINAL 1:
# Running Netcat
$ nc -lk 9999
apache spark
apache hadoop
# TERMINAL 2: RUNNING StructuredNetworkWordCount
$ ./bin/run-example org.apache.spark.examples.sql.streaming.StructuredNetworkWordCount localhost 9999
-------------------------------------------
Batch: 0
-------------------------------------------
+------+-----+
| value|count|
+------+-----+
|apache| 1|
| spark| 1|
+------+-----+
-------------------------------------------
Batch: 1
-------------------------------------------
+------+-----+
| value|count|
+------+-----+
|apache| 2|
| spark| 1|
|hadoop| 1|
+------+-----+
...
Structured Streaming的关键思想是将实时数据流视为不断追加的表。 这导致与批处理模型非常相似的新流处理模型。 您将在静态表上将流式计算表示为标准的batch-like查询,而Spark会在无界输入表上作为增量查询来运行它。 让我们更详细地了解这个模型。
将输入数据流视为“输入表”。 到达流的每个数据项都像追加到输入表的新行一样。
对输入的查询将生成“结果表”。 每个触发间隔(例如,每1秒),新行将附加到输入表,最终更新结果表。 无论何时更新结果表,我们都希望将更改的结果行写入外部接收器。
“输出”被定义为写入外部存储器的内容。 可以以不同的模式定义输出:
- 完成模式(complete mode) - 整个更新的结果表将被写入外部存储。 由存储连接器决定如何处理整个表的写入。
- 追加模式(append mode) - 只有结果表中自上次触发后附加的新行将被写入外部存储。 这仅适用于不期望更改结果表中现有行的查询。
- 更新模式(update mode) - 只有自上次触发以来在结果表中更新的行将被写入外部存储(Spark 2.1.1开始可用)。 请注意,这与完全模式不同,因为此模式仅输出自上次触发以来更改的行。 如果查询不包含聚合,它将等同于附加模式。
请注意,每种模式适用于某些类型的查询。 这将在后面详细讨论。
为了说明这个模型的使用,让我们在上面的快速示例的上下文中理解模型。 第一行DataFrame是输入表,最后的wordCounts DataFrame是结果表。 请注意,Streaming lines DataFrame生成wordCounts的查询与静态DataFrame完全相同。 但是,当该查询启动时,Spark将连续检查socket连接中的新数据。 如果有新数据,Spark将运行一个“增量”查询,将以前的running counts与新数据相结合,去计算更新的counts,如下所示
这种模式与许多其他流处理引擎有显着差异。 许多流系统要求用户自己维护运行的聚合,因此必须对容错和数据一致性(at-least-once, or at-most-once, or exactly-once)进行说明。 在这个模型中,当有新数据时,Spark负责更新结果表,从而减轻用户的负担。 例如,我们来看看这个模型如何处理基于event-time的处理和迟到的数据。
event-time是数据本身嵌入的时间。 对于许多应用程序,您可能需要在此event-time进行操作。 例如,如果要每分钟获取IoT设备生成的事件数,则可能希望使用数据生成的时间(即数据中的event-time),而不是Spark接收到它们的时间 。 这个event-time在这个模型中非常自然地表现出来 - 来自设备的每个事件都是表中的一行,event-time是行中的列值。 这允许window-based的聚合(例如,每分钟的事件数)作为event-time列上的特殊类型的分组和聚合 - 每个时间窗口是一个组,每一行可以属于多个窗口/组。 因此,可以在静态数据集(例如来自收集的设备事件日志)以及数据流上一致地定义基于event-time-window-based的聚合查询,从而使用户的使用寿命更加容易。
此外,该模型自然地处理晚于event-time到达的数据。 由于Spark正在更新结果表,所以当有迟到的数据它可以完全控制更新旧的聚合,以及清理旧的聚合以限制中间状态数据的大小。 从Spark 2.1开始,我们支持watermarking ,允许用户指定迟到数据的阈值,并允许引擎相应地清理旧状态。 稍后将在“窗口操作”部分中详细说明这些。
提供end-to-end exactly-once语义是structured streaming设计背后的关键目标之一。 为了实现这一点,我们设计了structured streaming的sources,sinks和执行引擎,可靠地跟踪处理进程的准确进度,以便它可以通过重新启动和/或重新处理来解决任何类型的故障。 假设每个Streaming源具有跟踪流中读取位置的偏移(类似于Kafka偏移或Kinesis序列号)。 引擎使用检查点和WAL(write ahead logs)记录每个触发器中正在处理的数据的偏移范围。 Streaming sinks为了解决重复计算被设计为幂等。 一起使用可重放sources和幂等sinks,Structured Streaming可以在任何故障下确保end-to-end exactly-once的语义。
从Spark 2.0开始,DataFrames和Datasets可以表示静态,有界数据(bounded data),以及Streaming,无界数据(unbounded data)。 与静态Datasets/ DataFrames类似,您可以使用通用入口点SparkSession(Scala / Java / Python文档)从Streaming sources创建Streaming DataFrames/Datasets,并像静态DataFrames / Datasets一样应用其相同的操作。 如果您不熟悉Datasets / DataFrames,强烈建议您使用DataFrame / Dataset编程指南来熟悉它们。
Streaming DataFrames可以通过SparkSession.readStream()返回的DataStreamReader接口(Scala / Java / Python文档)创建。 与创建静态DataFrame的读取接口类似,您可以指定source - 数据格式,模式(schema),选项(options)等的详细信息。
在Spark 2.0中,有一些内置的sources。
- File source-读取写入目录中的文件作为数据流。 支持的文件格式为text,csv,json,parquet。 有关更多最新列表,请参阅DataStreamReader接口的文档,并支持各种文件格式的选项。 请注意,文件必须以原子方式放置在给定的目录中,这在大多数文件系统中可以通过文件移动操作实现。
- Kafka source-从kafka轮询数据。 它与Kafka broker0.10.0或更高版本兼容。 有关详细信息,请参阅“kafka集成指南”。
- Socket source (for testing)-从socket连接读取UTF8文本数据。 socket监听服务器位于驱动程序。 请注意,这只能用于测试,因为它不提供end-to-end的容错保证。
某些sources不是容错的,因为它们不能保证在故障后可以使用检查点偏移量重播数据。 请参阅上一节关于容错语义的部分。 以下是Spark中所有源的详细信息。
source | 选项 | 容错性 | 注意 |
---|---|---|---|
File source | path:输入目录的路径,并且与所有文件格式通用。有关特定于文件格式的选项,请参阅DataStreamReader(Scala / Java / Python)中的相关方法。 例如。 对于“parquet”格式选项,请参阅DataStreamReader.parquet() | yes | 支持glob路径,但是不支持多个逗号分割的 paths/globs |
Socket Source | host:连接的host,必须指定 port:连接的端口,必须指定 |
NO | |
kafka source | 参考kafka集成指南 | yes |
下面是简单的例子:
val spark: SparkSession = ...
// Read text from socket
val socketDF = spark
.readStream
.format("socket")
.option("host", "localhost")
.option("port", 9999)
.load()
socketDF.isStreaming // Returns True for DataFrames that have streaming sources
socketDF.printSchema
// Read all the csv files written atomically in a directory
val userSchema = new StructType().add("name", "string").add("age", "integer")
val csvDF = spark
.readStream
.option("sep", ";")
.schema(userSchema) // Specify schema of the csv files
.csv("/path/to/directory") // Equivalent to format("csv").load("/path/to/directory")
这些示例生成无类型的Streaming DataFrames,这意味着在编译时不会检查DataFrame的schema,仅在运行时当查询提交时进行检查。 某些操作,如map,flatMap等,需要在编译时知道该类型。 要做到这一点,您可以使用与静态DataFrame相同的方法将这些无类型的Streaming DataFrames转换为类型化的Streaming Datasets。 有关详细信息,请参阅SQL编程指南。 此外,有关支持的Streaming sources的更多详细信息将在文档后面讨论。
默认情况下,基于文件的source的Structured Streaming需要您指定schema,而不是依靠Spark自动推断。 这种限制确保了一致的schema将被用于Streaming式查询,即使在出现故障的情况下也是如此。 对于特殊用例,您可以通过将spark.sql.streaming.schemaInference设置为true来重新启用schema推断。
当存在名为/ key = value /的子目录时,会发生分区发现,并且列表将自动递归到这些目录中。 如果这些列显示在用户提供的模式中,则它们将根据正在读取的文件的路径由Spark填充。 构成分区方案的目录必须在查询开始时显示,并且必须保持静态。 例如,可以添加/ data / year = 2016 / when / data / year = 2015 /存在,但更改分区列无效(即创建目录/ data / date =17 /)。
您可以在Streaming DataFrames / Datasets上应用各种操作 - 从无类型的,类似SQL的操作(例如select,where,groupBy)到有类型的类似RDD的操作(例如,map,filter,flatMap)。 有关详细信息,请参阅SQL编程指南。 让我们来看看可以使用的几个示例操作。
Streaming支持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
通过Structured Streaming,滑动event-time窗口的聚合很简单。 了解基于窗口的聚合的关键概念与分组聚合非常相似。 在分组聚合中,为用户指定的分组列中的每个唯一值维护聚合值(例如计数)。 在基于窗口的聚合的情况下,针对每个窗口的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。 因此,counts将被分组key(即单词)和窗口(可以从event-time计算)二者来索引。
结果表将如下所示:
由于此窗口类似于分组,因此在代码中,可以使用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()
现在考虑如果其中一个事件延迟到达应用程序会发生什么。 例如,说在12:04(即事件时间)生成的一个单词可以在12:11被应用程序接收。 应用程序应该使用时间12:04而不是12:11更新12:00 - 12:10的窗口的较旧计数。 这在我们基于窗口的分组中自然发生 - Structured Streaming可以长时间维持部分聚合的中间状态,以便迟到的数据可以正确地更新旧窗口的聚合,如下所示。
但是,为了持续几天运行这个查询,系统必须限制其累积的内存中间状态的数量。这意味着系统需要知道什么时候可以从内存状态中删除旧的聚合,因为应用程序不会再为该集合接收到较晚的数据。为了实现这一点,在Spark 2.1中,我们引入了watermarking,让引擎自动跟踪数据中的当前event-time,并尝试相应地清理旧状态。您可以通过指定事件时间列来定义查询的watermarking,并根据事件时间指定数据的延迟时间的阈值。对于从时间T开始的特定窗口,引擎将保持状态,并允许延迟数据更新状态,直到引擎看到最大事件时间-迟到的最大阈值。换句话说,阈值内的迟到数据将被聚合,但是比阈值晚的数据将被丢弃。让我们以一个例子来理解这一点。我们可以使用Watermark()轻松定义上一个例子中的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”列的查询的watermark ,并将“10分钟”定义为允许数据延迟的阈值。 如果此查询在更新输出模式下运行(稍后在“输出模式”部分中讨论),则引擎将继续更新Resule表中窗口的计数,直到窗口比watermark 旧,滞后于当前事件时间列“ timestamp“10分钟。 下面一个例子。
如图所示,引擎跟踪的最大事件时间为蓝色虚线,红线是每个触发开始时设置为(最大事件时间 - “10分钟”)的watermark 。例如,当引擎观察数据(12:14,dog),它为下一个触发器设置watermark 为12:04。该watermark 允许发动机维持中间状态额外的10分钟,以允许后期数据被统计。例如,数据(12:09,cat)是乱序和迟到的,它属于Windows 12:05 - 12:15和12:10 - 12:20。由于在这个触发里它仍然在水印12:04之前,引擎仍然将中间计数保持为状态,并正确更新相关窗口的计数。然而,当watermark 更新为12:11时,窗口(12:00 - 12:10)的中间状态被清除,所有后续数据(例如(12:04,donkey))被认为是“too late”,因此被忽略。请注意,按照更新模式的规定,每次触发后,更新的计数(即紫色行)都将作为触发输出被输出到sink。
某些sinks(例如文件)可能不支持更新模式所需的细粒度更新。 要与他们一起工作,我们还支持附加模式,只有最后的计数被写入sink。 这如下所示。
与之前的更新模式类似,引擎维护每个窗口的中间计数。 但是,部分计数不会更新到结果表,也不写入sink。 引擎等待“10分钟”接收迟到数据,然后丢弃窗口(watermark)的中间状态,并将最终计数附加到结果表/sink。 例如,窗口12:00 - 12:10的最终计数仅在watermark更新到12:11之后才附加到结果表中。
watermarking 清理聚合状态的条件重要的是要注意,为了清理聚合查询中的状态,必须满足以下条件(从Spark 2.1.1开始,以后再进行更改)。
- 输出模式必须是追加或更新。 完整模式要求保留所有聚合数据,因此不能使用watermarking 去掉中间状态。 有关每种输出模式的语义的详细说明,请参见“输出模式”部分。
- 聚合必须具有事件时间列或事件时间列上的窗口。
- 必须在与聚合中使用的时间戳列相同的列上使用withWatermark 。 例如,df.withWatermark(“time”,“1 min”).groupBy(“time2”).count()在附加输出模式中无效,因为watermark 在不同的列上定义为聚合列。
- 必须在聚合之前调用withWatermark才能使用watermark 细节。 例如,在附加输出模式下,df.groupBy(“time”).count().withWatermark(“time”,“1 min”)无效。
Streaming DataFrames可以与静态 DataFrames连接,以创建新的Streaming DataFrames。 例如下面的例子。
val staticDf = spark.read. ...
val streamingDf = spark.readStream. ...
streamingDf.join(staticDf, "type") // inner equi-join with a static DF
streamingDf.join(staticDf, "type", "right_join") // right outer join with a static DF
有几个DataFrame / Dataset操作不支持streaming DataFrames / Datasets。 其中一些如下。
- streaming Datasets不支持多个streaming聚合(即streaming DF上的聚合链)。
- 流数据集不支持limit和取前N行。
- Streaming Datasets不支持Distinct 操作。
- 只有在在完全输出模式的聚合之后,streaming Datasets才支持排序操作。
- 有条件地支持Streaming和静态Datasets之间的外连接。
不支持与 streaming Dataset的Full outer join
不支持streaming Dataset 在右侧的Left outer join
不支持streaming Dataset在左侧的Right outer join
此外,还有一些Dataset方法将不适用于streaming Datasets。 它们是立即运行查询并返回结果的操作,这在streaming Datasets上没有意义。 相反,这些功能可以通过显式启动streaming查询来完成(参见下一节)。
- count() - 无法从流数据集返回单个计数。 而是使用ds.group By.count()返回一个包含running count的streaming Dataset 。
- foreach() - 而是使用ds.writeStream.foreach(…)(见下一节)。
- show() - Instead use the console sink (see next section).
如果您尝试任何这些操作,您将看到一个AnalysisException,如“操作XYZ不支持 streaming DataFrames/Datasets”。 虽然其中一些可能在未来版本的Spark中得到支持,但还有一些在基本上很难在流数据上实现高效数据。 例如,不支持对输入流进行排序,因为它需要跟踪流中接收到的所有数据。 因此,从根本上难以有效执行。
一旦定义了最终结果DataFrame / Dataset,剩下的就是开始streaming 计算。 为此,您必须使用通过Dataset.writeStream()返回的DataStreamWriter(Scala / Java / Python文档)。 您将必须在此接口中指定以下一个或多个。
- 输出sink的详细信息: Data format,位置(location), 等等.
- 输出模式: 指定输出到sink的内容
- 查询名称:可选地,指定用于标识查询的唯一名称
- 触发间隔:可选,指定触发间隔。 如果未指定,则系统将在上一次处理完成后立即检查新数据的可用性。 如果由于先前的处理尚未完成而导致错过触发时间,则系统将尝试在下一个触发点触发,而不是在处理完成后立即触发。
- 检查点位置:对于可以保证端对端容错能力的某些输出sinks,请指定系统将写入所有检查点信息的位置。 这应该是与HDFS兼容的容错文件系统中的目录。 检查点的语义将在下一节中进行更详细的讨论。
下面是几种输出模式:
- Append mode (default) - 这是默认模式,其中只有从上次触发后添加到结果表的新行将被输出到sink。 只有那些添加到“结果表”中并且从不会更改的行的查询才支持这一点。 因此,该模式保证每行只能输出一次(假定容错sink)。 例如,只有select,where,map,flatMap,filter,join等的查询将支持Append模式。
- Complete mode -每个触发后,整个结果表将被输出到sink。 聚合查询支持这一点。
- Update mode - (自Spark 2.1.1以来可用)只有结果表中自上次触发后更新的行才会被输出到sink。 更多信息将在以后的版本中添加。
不同类型的流式查询支持不同的输出模式。 以下是兼容性信息。
Query Type | Supported Output Modes | 备注 |
---|---|---|
没有聚合的查询 | Append, Update | 不支持完整模式,因为将所有数据保存在结果表中是不可行的。 |
有聚合的查询:使用watermark对event-time进行聚合 | Append, Update, Complete | 附加模式使用watermark 来降低旧聚合状态。 但是,窗口化聚合的输出会延迟“withWatermark()”中指定的晚期阈值,因为模式语义可以在结果表中定义后才能将结果表添加到结果表中(即在watermark 被交叉之后)。 有关详细信息,请参阅后期数据部分。更新模式使用水印去掉旧的聚合状态。完全模式不会丢弃旧的聚合状态,因为根据定义,此模式保留结果表中的所有数据。 |
有聚合的查询:其他聚合 | Complete, Update | 由于没有定义watermark (仅在其他类别中定义),旧的聚合状态不会被丢弃。不支持附加模式,因为聚合可以更新,从而违反了此模式的语义。 |
有几种类型的内置输出sinks。
- File sink-将输出存储到目录。
writeStream
.format("parquet") // can be "orc", "json", "csv", etc.
.option("path", "path/to/destination/dir")
.start()
writeStream
.foreach(...)
.start()
writeStream
.format("console")
.start()
writeStream
.format("memory")
.queryName("tableName")
.start()
某些sinks 不容错,因为它们不保证输出的持久性,仅用于调试目的。 请参阅上一节关于容错语义的部分。 以下是Spark中所有sinks 的详细信息。
sink | Supported Output Modes | Options | Fault-tolerant | Notes |
---|---|---|---|---|
File Sink | Append | path:输出目录的路径,必须指定。 maxFilesPerTrigger:每个触发器中要考虑的最大新文件数(默认值:无最大值) latestFirst:是否首先处理最新的新文件,当有大量的文件积压(default:false)时很有用 有关特定于文件格式的选项,请参阅DataFrameWriter(Scala / Java / Python)中的相关方法。 例如。 对于“parquet”格式选项请参阅DataFrameWriter.parquet() |
yes | 支持对分区表的写入。 按时间划分可能有用。 |
Foreach Sink | Append, Update, Compelete | None | 取决于ForeachWriter的实现 | 更多细节在下一节 |
Console Sink | Append, Update, Complete | numRows:每次触发打印的行数(默认值:20)truncate:输出太长是否截断(默认值:true) | no | |
Memory Sink | Append, Complete | None | 否。但在Complete模式下,重新启动的查询将重新创建整个表。 | 查询名就是表名 |
请注意,您必须调用start()来实际启动查询的执行。 这将返回一个StreamingQuery对象,它是连续运行执行的句柄。 您可以使用此对象来管理查询,我们将在下一小节中讨论。 现在,让我们通过几个例子了解所有这些。
// ========== DF with no aggregations ==========
val noAggDF = deviceDataDf.select("device").where("signal > 10")
// Print new data to console
noAggDF
.writeStream
.format("console")
.start()
// Write new data to Parquet files
noAggDF
.writeStream
.format("parquet")
.option("checkpointLocation", "path/to/checkpoint/dir")
.option("path", "path/to/destination/dir")
.start()
// ========== DF with aggregation ==========
val aggDF = df.groupBy("device").count()
// Print updated aggregations to console
aggDF
.writeStream
.outputMode("complete")
.format("console")
.start()
// Have all the aggregates in an in-memory table
aggDF
.writeStream
.queryName("aggregates") // this query name will be the table name
.outputMode("complete")
.format("memory")
.start()
spark.sql("select * from aggregates").show() // interactively query in-memory table
foreach操作允许在输出数据上计算任意操作。 从Spark 2.1开始,这只适用于Scala和Java。 要使用它,您将必须实现接口ForeachWriter(Scala / Java docs),它具有每当在触发之后生成作为输出的序列化行时就被调用的方法。 请注意以下要点。
- writer必须是可序列化的,因为它将被序列化并发送给executors执行。
- 所有三种方法,open,process 和close 将在executors上被调用。
- 写入程序只有在open方法被调用时才能执行所有的初始化(例如打开连接,启动事务等)。 请注意,如果在创建对象时立即在类中进行任何初始化,那么该初始化将在驱动程序中发生(因为这是正在创建实例的地方),这可能不是您打算的。
- 版本和分区是open的两个参数,唯一表示需要推出的一组行。 版本是每次触发增加的单调递增的id。 分区是表示输出分区的id,因为输出是分布式的,并且将在多个执行器上处理。
- open可以使用版本和分区来选择是否需要写入序列化的行。 因此,它可以返回true(继续写入)或false(不需要写入)。 如果返回false,则不会在任何行上调用进程。 例如,在部分失败之后,失败触发的一些输出分区可能已经被提交到数据库。 基于存储在数据库中的元数据,writer 可以识别已经提交的分区,因此返回false以跳过再次提交它们。
- 每当open被调用时,close也将被调用(除非JVM由于某些错误而退出)。 即使open返回false也是如此。 如果在处理和写入数据时出现任何错误,则将使用错误调用close。 您有责任清理在open中创建的状态(例如,连接,事务等),以免资源泄漏。
查询启动时创建的StreamingQuery对象可用于监视和管理查询。
val query = df.writeStream.format("console").start() // get the query object
query.id // get the unique identifier of the running query that persists across restarts from checkpoint data
query.runId // get the unique id of this run of the query, which will be generated at every start/restart
query.name // get the name of the auto-generated or user-specified name
query.explain() // print detailed explanations of the query
query.stop() // stop the query
query.awaitTermination() // block until query is terminated, with stop() or with error
query.exception // the exception if the query has been terminated with error
query.recentProgress // an array of the most recent progress updates for this query
query.lastProgress // the most recent progress update of this streaming query
您可以在单个SparkSession中启动任意数量的查询。 他们都将同时运行通过共享群集资源。 您可以使用sparkSession.streams()获取可用于管理当前活动查询的StreamingQueryManager(Scala / Java / Python文档)。
val spark: SparkSession = ...
spark.streams.active // get the list of currently active streaming queries
spark.streams.get(id) // get a query object by its unique id
spark.streams.awaitAnyTermination() // block until any one of them terminates
有两个API用于监视和调试活动查询- 以交互方式和异步方式。
您可以使用streamingQuery.lastProgress()和streamingQuery.status()直接获取活动查询的当前状态和指标。 lastProgress()在Scala和Java中返回一个StreamingQueryProgress对象,并在Python中返回与该字段相同的字典。 它具有关于流的最后一个触发器的进度的所有信息 - 处理哪些数据,处理速率,延迟等等。还有streamingQuery.recentProgress返回最后几个进度的数组。此外,streamingQuery.status()在Scala和Java中返回StreamingQueryStatus对象,在Python中返回具有相同字段的字典。 它提供有关查询当前执行的信息 - 触发器活动状态,正在处理的数据等。下面是几个例子:
val query: StreamingQuery = ...
println(query.lastProgress)
/* Will print something like the following.
{
"id" : "ce011fdc-8762-4dcb-84eb-a77333e28109",
"runId" : "88e2ff94-ede0-45a8-b687-6316fbef529a",
"name" : "MyQuery",
"timestamp" : "2016-12-14T18:45:24.873Z",
"numInputRows" : 10,
"inputRowsPerSecond" : 120.0,
"processedRowsPerSecond" : 200.0,
"durationMs" : {
"triggerExecution" : 3,
"getOffset" : 2
},
"eventTime" : {
"watermark" : "2016-12-14T18:45:24.873Z"
},
"stateOperators" : [ ],
"sources" : [ {
"description" : "KafkaSource[Subscribe[topic-0]]",
"startOffset" : {
"topic-0" : {
"2" : 0,
"4" : 1,
"1" : 1,
"3" : 1,
"0" : 1
}
},
"endOffset" : {
"topic-0" : {
"2" : 0,
"4" : 115,
"1" : 134,
"3" : 21,
"0" : 534
}
},
"numInputRows" : 10,
"inputRowsPerSecond" : 120.0,
"processedRowsPerSecond" : 200.0
} ],
"sink" : {
"description" : "MemorySink"
}
}
*/
println(query.status)
/* Will print something like the following.
{
"message" : "Waiting for data to arrive",
"isDataAvailable" : false,
"isTriggerActive" : false
}
*/
您还可以通过附加StreamingQueryListener(Scala / Java文档)来异步监视与SparkSession关联的所有查询。 一旦您使用sparkSession.streams.attachListener()附加您的自定义StreamingQueryListener对象,您将在查询启动和停止时以及在活动查询中创建进程时获得回调。 这是一个例子:
val spark: SparkSession = ...
spark.streams.addListener(new StreamingQueryListener() {
override def onQueryStarted(queryStarted: QueryStartedEvent): Unit = {
println("Query started: " + queryStarted.id)
}
override def onQueryTerminated(queryTerminated: QueryTerminatedEvent): Unit = {
println("Query terminated: " + queryTerminated.id)
}
override def onQueryProgress(queryProgress: QueryProgressEvent): Unit = {
println("Query made progress: " + queryProgress.progress)
}
})
如果发生故障或故意关机,您可以恢复之前的查询的进度和状态,并从停止处继续。 这是使用检查点和wal(write ahead logs)来完成的。 您可以使用检查点位置配置查询,并且查询将将所有进度信息(即,每个触发器中处理的偏移范围)和运行聚合(例如快速示例中的word counts)保存到检查点位置。 此检查点位置必须是HDFS兼容文件系统中的路径,并且可以在启动查询时将其设置为DataStreamWriter中的选项。
aggDF
.writeStream
.outputMode("complete")
.option("checkpointLocation", "path/to/HDFS/dir")
.format("memory")
.start()