结构化流编程指南
概观
结构化流是一种基于Spark SQL引擎构建的可扩展且容错的流处理引擎。您可以使用与表达静态数据的批量计算相同的方式来表达流式计算。Spark SQL引擎会逐步持续运行,并在流数据持续到达时更新最终结果。您可以使用Scala,Java或Python中的Dataset / DataFrame API来表达流聚合,事件时间窗口,流到批处理连接等。计算在相同的优化的Spark SQL引擎上执行。最后,系统通过检查点和预写日志确保端到端的一次容错保证。简而言之,结构化流提供快速,可扩展,容错,端到端的一次性流处理,而无需用户理解流。
结构化Streaming仍然是Spark 2.1中的ALPHA, API仍然是实验性的。在本指南中,我们将介绍编程模型和API。首先,我们从一个简单的例子开始 - 一个流字数。
快速示例
假设您希望保持从监听TCP套接字的数据服务器接收的文本数据的运行字数。我们来看看如何使用结构化流式表达这一点。您可以在Scala / Java /Python中看到完整的代码 。如果您下载Spark,可以直接运行该示例。在任何情况下,让我们逐步了解示例并了解它的工作原理。首先,我们必须导入必要的类并创建一个本地SparkSession,这是与Spark相关的所有功能的起点。
import org.apache.spark.api.java.function.FlatMapFunction;
import org.apache.spark.sql.*;
import org.apache.spark.sql.streaming.StreamingQuery;
import java.util.Arrays;
import java.util.Iterator;
SparkSession spark = SparkSession
.builder()
.appName("JavaStructuredNetworkWordCount")
.getOrCreate();
接下来,我们创建一个流DataFrame,它表示从本地主机侦听的服务器接收的文本数据:9999,并转换DataFrame以计算字数。
// Create DataFrame representing the stream of input lines from connection to localhost:9999
Dataset<Row> lines = spark
.readStream()
.format("socket")
.option("host", "localhost")
.option("port", 9999)
.load();
// Split the lines into words
Dataset<String> words = lines
.as(Encoders.STRING())
.flatMap(
new FlatMapFunction<String, String>() {
@Override
public Iterator<String> call(String x) {
return Arrays.asList(x.split(" ")).iterator();
}
}, Encoders.STRING());
// Generate running word count
Dataset<Row> wordCounts = words.groupBy("value").count();
该lines
DataFrame表示包含流文本数据的无界表。此表包含一列名为“value”的字符串,并且流文本数据中的每一行都将成为表中的一行。请注意,这并不是正在收到任何数据,因为我们只是设置转换,还没有开始。接下来,我们已经将DataFrame转换为String的数据集.as(Encoders.STRING())
,因此我们可以应用该flatMap
操作将每行划分成多个单词。所得words
数据集包含所有单词。最后,我们已经wordCounts
通过数据集中唯一的值进行分组并对它们进行计数来定义了DataFrame。请注意,这是一个流式DataFrame,它表示流的运行字数。
我们现在已经设置了关于流数据的查询。剩下的就是实际开始接收数据并计算计数。为此,我们将其设置为outputMode("complete")
每次更新时将完整的计数(由指定)计算到控制台。然后开始使用流计算start()
。
// Start running the query that prints the running counts to the console
StreamingQuery query = wordCounts.writeStream()
.outputMode("complete")
.format("console")
.start();
query.awaitTermination();
执行此代码后,流式计算将在后台启动。该query
对象是该活动流查询的句柄,我们已经决定等待查询的终止query.awaitTermination()
以阻止进程在查询处于活动状态时退出。
要实际执行此示例代码,您可以在自己的Spark应用程序中编译代码 ,或者一旦下载了Spark 即可 运行该示例。我们正在展示后者。您将首先需要运行Netcat(大多数类Unix系统中的一个小型实用程序)作为数据服务器
$ nc -lk 9999
然后,在不同的终端中,您可以使用启动示例
$ ./bin/run-example org.apache.spark.examples.sql.streaming.JavaStructuredNetworkWordCount localhost 9999
然后,在运行netcat服务器的终端中输入的任何行将每秒计数并打印在屏幕上。它会看起来像下面这样。
|
|
编程模型
结构化流式传输的关键思想是将实时数据流视为不断附加的表格。这导致与批处理模型非常相似的新流处理模型。您将在静态表格上将流式计算表示为标准批量查询,而Spark会在无界输入表上将其作为增量查询来运行。让我们更详细地了解这个模型。
基本概念
将输入数据流视为“输入表”。到达流的每个数据项都像追加到输入表的新行一样。
对输入的查询将生成“结果表”。每个触发间隔(例如,每1秒),新行将附加到输入表,最终更新结果表。无论何时更新结果表,我们都希望将更改的结果行写入外部接收器。
“输出”定义为写入外部存储器的内容。可以以不同的模式定义输出
-
完整模式 - 整个更新的结果表将写入外部存储。由存储连接器决定如何处理整个表的写入。
-
附加模式 - 只有结果表中自上次触发后附加的新行将被写入外部存储。这仅适用于不期望更改结果表中现有行的查询。
-
更新模式 - 只有在上次触发后,结果表中更新的行将被写入外部存储(Spark 2.0中尚不可用)。请注意,这与完全模式不同,因为该模式不会输出未更改的行。
请注意,每个模式适用于某些类型的查询。这将在后面详细讨论。
为了说明这个模型的使用,让我们在上面的快速示例的上下文中了解模型。第一个lines
DataFrame是输入表,最后的wordCounts
DataFrame是结果表。需要注意的是在流媒体的查询lines
数据帧生成wordCounts
是完全一样的,因为它是一个静态的数据帧。但是,当该查询启动时,Spark将不断地从套接字连接检查新数据。如果有新数据,Spark将运行一个“增量”查询,将先前的运行计数与新数据相结合,以计算更新的计数,如下所示。
这种模式与许多其他流处理引擎显着不同。许多流媒体系统需要用户自己维护运行聚合,因此必须对容错和数据一致性(至少一次,或最多一次,或一次)进行说明。在这个模型中,当有新数据时,Spark负责更新结果表,从而减轻用户对它的推理。例如,我们来看看这个模型如何处理基于事件时间的处理和迟到的数据。
处理事件时间和延迟数据
事件时间是嵌入数据本身的时间。对于许多应用程序,您可能希望在此事件时间进行操作。例如,如果要每分钟获取IoT设备生成的事件数,那么您可能希望使用数据生成的时间(即数据中的事件时间),而不是Spark接收到的时间他们。这个事件时间在这个模型中非常自然地表现出来 - 来自设备的每个事件都是表中的一行,事件时间是行中的列值。这允许基于窗口的聚合(例如,每分钟的事件数)只是偶数列上的特殊类型的分组和聚合 - 每个时间窗口是一个组,每行可以属于多个窗口/组。因此,可以在静态数据集(例如来自收集的设备事件日志)以及数据流上一致地定义基于事件时间窗口的聚合查询,从而使用户的使用寿命更加容易。
此外,该模型自然地处理基于事件时间晚于预期的数据。由于Spark正在更新结果表,它可以完全控制更新旧的聚合,当有迟到的数据,以及清理旧的聚合以限制中间状态数据的大小。由于Spark 2.1,我们支持水印,允许用户指定后期数据的阈值,并允许引擎相应地清理旧状态。稍后将在“窗口操作”部分中详细介绍这些。
容错语义
提供端到端的一次性语义是结构化流媒体设计背后的关键目标之一。为了实现这一点,我们设计了结构化流源,汇和执行引擎,以便可靠地跟踪处理的确切进度,以便它可以通过重新启动和/或重新处理来处理任何类型的故障。假设每个流源具有偏移量(类似于Kafka偏移量或Kinesis序列号)以跟踪流中的读取位置。引擎使用检查点和写入日志记录每个触发器中正在处理的数据的偏移范围。流宿设计为处理后处理的幂等。一起使用可重放源和幂等接收器,结构化流可以在任何故障下确保端到端完全一次的语义。
API使用Datasets和DataFrames
由于Spark 2.0,DataFrames和Datasets可以表示静态,有界数据,以及流式传输,无界数据。与静态数据集/ DataFrames类似,您可以使用通用入口点SparkSession
(Scala / Java / Python文档)从流源创建流式DataFrames / Datasets,并将其作为静态DataFrames / Datasets应用相同的操作。如果您不熟悉Datasets / DataFrames,强烈建议您使用DataFrame / Dataset编程指南来熟悉它们 。
创建流数据帧和流数据集
Streaming DataFrames可以通过返回的DataStreamReader
接口(Scala / Java / Python文档)创建SparkSession.readStream()
。与创建静态DataFrame的读取界面类似,您可以指定源 - 数据格式,模式,选项等的详细信息。
数据源
在Spark 2.0中,有几个内置源代码。
-
文件源 - 将目录中写入的文件作为数据流读取。支持的文件格式为text,csv,json,parquet。有关更多最新列表,请参阅DataStreamReader界面的文档,以及每个文件格式的支持选项。请注意,文件必须以原子方式放置在给定的目录中,这在大多数文件系统中可以通过文件移动操作实现。
-
卡夫卡来源 - 来自卡夫卡的投票数据。它与Kafka代理商版本0.10.0或更高版本兼容。有关详细信息,请参阅“ 卡夫卡集成指南”。
-
套接字源(用于测试) - 从套接字连接读取UTF8文本数据。侦听服务器插座位于驱动程序中。请注意,这仅应用于测试,因为这不提供端到端容错保证。
这里有些例子。
SparkSession spark = ...
// Read text from socket
Dataset[Row] 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
StructType userSchema = new StructType().add("name", "string").add("age", "integer");
Dataset[Row] 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")
这些示例生成无类型的流DataFrames,这意味着DataFrame的架构在编译时未被检查,只在运行时在查询提交时进行检查。像某些操作map
,flatMap
等需要在编译时已知的类型。要做到这一点,您可以使用与静态DataFrame相同的方法将这些未类型化的流DataFrames转换为类型化的流数据集。有关详细信息,请参阅SQL编程指南。此外,有关支持的流媒体源的更多详细信息将在文档后面讨论。
流式DataFrames / Datasets的模式推理和分区
默认情况下,基于文件的源的结构化流式传输需要您指定模式,而不是依靠Spark自动推断。这种限制确保了即使在出现故障的情况下,一致的模式将被用于流式查询。对于特殊用例,您可以通过设置spark.sql.streaming.schemaInference
来重新启用模式推断true
。
当存在命名的子目录/key=value/
并且列表将自动递归到这些目录中时,会发生分区发现。如果这些列显示在用户提供的模式中,则它们将根据正在读取的文件的路径由Spark填充。构成分区方案的目录必须在查询启动时存在,并且必须保持静态。例如,可以添加/data/year=2016/
何时/data/year=2015/
存在,但是更改分区列(即通过创建目录/data/date=2016-04-17/
)无效。
流数据帧/数据集的操作
您可以将各种操作上的流DataFrames /数据集-从无类型,类似于SQL的操作(例如select
,where
,groupBy
),为键入RDD般的操作(例如map
,filter
,flatMap
)。有关详细信息,请参阅SQL编程指南。我们来看一下可以使用的几个示例操作。
基本操作 - 选择,投影,聚合
DataFrame / Dataset的大多数常见操作都支持流式传输。本节稍后将讨论不支持的少数操作。
import org.apache.spark.api.java.function.*;
import org.apache.spark.sql.*;
import org.apache.spark.sql.expressions.javalang.typed;
import org.apache.spark.sql.catalyst.encoders.ExpressionEncoder;
public class DeviceData {
private String device;
private String type;
private Double signal;
private java.sql.Date time;
...
// Getter and setter methods for each field
}
Dataset<Row> df = ...; // streaming DataFrame with IOT device data with schema { device: string, type: string, signal: double, time: DateType }
Dataset<DeviceData> ds = df.as(ExpressionEncoder.javaBean(DeviceData.class)); // 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(new FilterFunction<DeviceData>() { // using typed APIs
@Override
public boolean call(DeviceData value) throws Exception {
return value.getSignal() > 10;
}
}).map(new MapFunction<DeviceData, String>() {
@Override
public String call(DeviceData value) throws Exception {
return value.getDevice();
}
}, Encoders.STRING());
// Running count of the number of updates for each device type
df.groupBy("type").count(); // using untyped API
// Running average signal for each device type
ds.groupByKey(new MapFunction<DeviceData, String>() { // using typed API
@Override
public String call(DeviceData value) throws Exception {
return value.getType();
}
}, Encoders.STRING()).agg(typed.avg(new MapFunction<DeviceData, Double>() {
@Override
public Double call(DeviceData value) throws Exception {
return value.getSignal();
}
}));
事件时间窗口操作
通过结构化流式,滑动事件时间窗口的聚合很简单。了解基于窗口的聚合的关键思想与分组聚合非常相似。在分组聚合中,为用户指定的分组列中的每个唯一值维护聚合值(例如计数)。在基于窗口的聚合的情况下,对于行的事件时间的每个窗口,维护聚合值。让我们以一个例证了解这一点。
想象一下,我们的快速示例被修改,并且流现在包含生成行的时间和行。我们不想运行字数,而是要在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。所以计数将由两者分组键(即单词)和窗口(可以从事件时间计算)进行索引。
结果表将如下所示。
由于此窗口类似于分组,因此在代码中,您可以使用groupBy()
和window()
操作来表达窗口聚合。您可以在Scala / Java / Python中看到以下示例的完整代码 。
Dataset<Row> words = ... // streaming DataFrame of schema { timestamp: Timestamp, word: String }
// Group the data by window and word and compute the count of each group
Dataset<Row> windowedCounts = words.groupBy(
functions.window(words.col("timestamp"), "10 minutes", "5 minutes"),
words.col("word")
).count();
处理晚数据和水印
现在考虑如果一个事件迟到应用程序会发生什么。例如,说在12:04(即事件时间)生成的一个字可以在12:11被应用程序收到。应用程序应该使用12:04而不是12:11来更新窗口的较旧计数12:00 - 12:10
。这在我们基于窗口的分组中自然发生 - 结构化流可以长时间维持部分聚合的中间状态,以便后期数据可以正确地更新旧窗口的聚合,如下所示。
但是,为了运行这个查询几天,系统必须绑定其累积的内存中间状态的数量。这意味着系统需要知道何时可以从内存状态中删除旧聚合,因为应用程序不会再为该集合接收到较晚的数据。为了实现这一点,在Spark 2.1中,我们引入了 水印,让引擎自动跟踪数据中的当前事件时间,并尝试相应地清除旧状态。您可以通过指定事件时间列来定义查询的水印,并根据事件时间来确定数据预计的延迟。对于从时间开始的特定窗口T
,引擎将保持状态,并允许延迟数据更新状态,直到(max event time seen by the engine - late threshold > T)
。换句话说,阈值内的晚期数据将被聚合,但是晚于阈值的数据将被丢弃。让我们以一个例子来理解这一点。我们可以很容易地定义上一个例子中的水印withWatermark()
,如下所示。
Dataset<Row> words = ... // streaming DataFrame of schema { timestamp: Timestamp, word: String }
// Group the data by window and word and compute the count of each group
Dataset<Row> windowedCounts = words
.withWatermark("timestamp", "10 minutes")
.groupBy(
functions.window(words.col("timestamp"), "10 minutes", "5 minutes"),
words.col("word"))
.count();
在这个例子中,我们正在定义“timestamp”列的查询的水印,并将“10分钟”定义为允许数据延迟的阈值。如果这个查询运行在追加输出模式(稍后在输出模式部分中讨论),引擎将从列“时间戳”跟踪当前事件时间,并在事件时间之前等待额外的“10分钟”,然后才能完成加窗计数并添加他们到结果表。这是一个例子。
如图所示,发动机跟踪的最大事件时间是 蓝色虚线,并且(max event time - '10 mins')
每个触发开始时设置的水印是红线。例如,当引擎观察数据时 (12:14, dog)
,它将为下一个触发器为12:04
。对于窗口12:00 - 12:10
,当系统等待后期数据时,部分计数被保持为内部状态。在系统发现(12:21, owl)
水印超过12:10的数据(即)之后,部分计数被确定并附加到表中。由于所有超过12:10的“太晚”数据将被忽略,因此此计数将不会改变。
请注意,在附加输出模式下,系统必须等待“晚期门槛”时间才能输出窗口的聚合。如果数据可能非常迟,(如1天),并且您喜欢在没有等待一天的情况下进行部分计数,这可能不是理想的。将来,我们将添加更新输出模式,这将允许每个更新聚合被写入以吸收每个触发。
水印清除聚合状态 的条件重要的是要注意,为了清理聚合查询中的状态,必须满足以下条件(从Spark 2.1开始,将来会有变化)。
-
输出模式必须为“追加”。完整模式要求保留所有聚合数据,因此不能使用水印来中断状态。有关每种输出模式的语义的详细说明,请参见“ 输出模式”部分。
-
聚合必须具有事件时间列或
window
事件时间列上的一个。 -
withWatermark
必须在与聚合中使用的时间戳列相同的列上调用。例如,df.withWatermark("time", "1 min").groupBy("time2").count()
在附加输出模式中无效,因为水印在不同的列上定义为聚合列。 -
withWatermark
必须在聚合之前调用要使用的水印细节。例如,df.groupBy("time").count().withWatermark("time", "1 min")
在追加输出模式中无效。
加盟业务
流数据帧可以与静态DataFrames一起创建新的流数据帧。这里有几个例子。
Dataset<Row> staticDf = spark.read. ...;
Dataset<Row> 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
不支持的操作
但是请注意,静态DataFrames / Datasets中的所有操作都不支持流式传输DataFrames / Datasets。虽然其中一些不受支持的操作将在未来版本的Spark中得到支持,但还有一些在基本上很难在流数据上高效地实现。例如,输入流数据集不支持排序,因为它需要跟踪流中接收到的所有数据。因此,从根本上难以有效执行。从Spark 2.0开始,一些不受支持的操作如下
-
流数据集不支持多个流聚合(即流DF上的聚合链)。
-
流数据集不支持限制并采取前N行。
-
不支持流数据集上的不同操作。
-
只有在聚合和完全输出模式下,流数据集才支持排序操作。
-
有条件地支持流和静态数据集之间的外连接。
-
不支持与流数据集完全外连接
-
不支持左侧外连接与右侧的流数据集
-
右侧外连接与左侧的流数据集不支持
-
-
两个流数据集之间的任何形式的连接都不支持。
此外,还有一些Dataset方法将不适用于流数据集。它们是立即运行查询并返回结果的操作,这在流数据集上没有意义。相反,这些功能可以通过显式启动流式查询来完成(参见下一节)。
-
count()
- 不能从流数据集返回单个计数。而是使用ds.groupBy.count()
它返回包含运行计数的流数据集。 -
foreach()
- 而是使用ds.writeStream.foreach(...)
(见下一节)。 -
show()
- 而是使用控制台接收器(见下一节)。
如果您尝试任何这些操作,您将看到一个AnalysisException,如“操作XYZ不支持流数据帧/数据集”。
开始流式查询
一旦定义了最终结果DataFrame / Dataset,剩下的就是开始流计算。为此,您必须使用返回的DataStreamWriter
(Scala / Java / Python文档)Dataset.writeStream()
。您将必须在此界面中指定以下一个或多个。
-
输出接收器的详细信息:数据格式,位置等
-
输出模式:指定写入输出接收器的内容。
-
查询名称:可选地,指定用于标识的查询的唯一名称。
-
触发间隔:可选,指定触发间隔。如果未指定,系统将在上一次处理完成后立即检查新数据的可用性。如果由于先前的处理尚未完成而导致触发时间错误,则系统将尝试在下一个触发点触发,而不是在处理完成后立即触发。
-
检查点位置:对于可以保证端到端容错的某些输出接收器,请指定系统将写入所有检查点信息的位置。这应该是与HDFS兼容的容错文件系统中的目录。检查点的语义将在下一节中进行更详细的讨论。
输出模式
有几种类型的输出模式。
-
附加模式(默认) - 这是默认模式,只有从上次触发后添加到结果表的新行将被输出到接收器。只有那些添加到结果表中的行从不会改变的查询才支持这一点。因此,该模式保证每行只能输出一次(假定容错接收器)。例如,仅查询
select
,where
,map
,flatMap
,filter
,join
,等会支持追加模式。 -
完成模式 - 每次触发后,整个结果表将被输出到接收器。聚合查询支持这一点。
-
更新模式 - (Spark 2.1中不可用)只有结果表中自上次触发后更新的行才会被输出到接收器。更多信息将在以后的版本中添加。
不同类型的流式查询支持不同的输出模式。以下是兼容性矩阵。
查询类型 | 支持的输出模式 | 笔记 | |
---|---|---|---|
没有聚合的查询 | 附加 | 支持完整模式备注,因为将所有数据保存在结果表中是不可行的。 | |
聚合查询 | 使用水印对事件时间进行聚合 | 追加,完成 | 附加模式使用水印来删除旧的聚合状态。但是,窗口聚合的输出会延迟“withWatermark()”中指定的晚期阈值,因为模式语义可以在结果表中定义后将其添加到结果表中一次(即在水印被交叉之后)。有关详细信息,请参阅延迟数据部分。 完全模式不会降低旧的聚合状态,因为根据定义,此模式保留结果表中的所有数据。 |
其他汇总 | 完成 | 不支持附加模式,因为聚合可以更新,从而违反了此模式的语义。 完全模式不会降低旧的聚合状态,因为根据定义,此模式保留结果表中的所有数据。 | |
输出接收端
有几种类型的内置输出接收器。
-
文件宿 - 将输出存储到目录中。
-
Foreach sink - 对输出中的记录运行任意计算。有关详细信息,请参阅本节后面部分。
-
控制台接收器(用于调试) - 每次触发时,将输出打印到控制台/ stdout。都支持“追加”和“完成”输出模式。这应该用于低数据量的调试目的,因为在每次触发后,整个输出被收集并存储在驱动程序的存储器中。
-
存储器接收器(用于调试) - 输出作为内存表存储在存储器中。都支持“追加”和“完成”输出模式。这应该用于低数据量的调试目的,因为在每次触发后,整个输出被收集并存储在驱动程序的存储器中。
这是一个表的所有的接收器和相应的设置。
水槽 | 支持的输出模式 | 用法 | 容错 | 笔记 |
---|---|---|---|---|
文件槽 | 附加 | writeStream .format(“parquet”).start () | 是 | 支持写入分区表。按时间划分可能是有用的。 |
Foreach水槽 | 所有模式 | writeStream .foreach(...).start () | 取决于ForeachWriter的实现 | 更多细节在下一节 |
控制台接收器 | 追加,完成 | writeStream .format(“console”).start () | 没有 | |
内存槽 | 追加,完成 | writeStream .format(“memory”). queryName(“table”). start() | 没有 | 将输出数据保存为表,进行交互式查询。表名是查询名。 |
最后,您必须调用start()
实际启动查询的执行。这将返回一个StreamingQuery对象,它是持续运行的执行的句柄。您可以使用此对象来管理查询,我们将在下一小节中讨论。现在,让我们通过几个例子了解所有这些。
// ========== DF with no aggregations ==========
Dataset<Row> 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()
.parquet("path/to/destination/directory")
.start();
// ========== DF with aggregation ==========
Dataset<Row> 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
该foreach
操作允许在输出数据上计算任意操作。从Spark 2.1起,这只适用于Scala和Java。要使用它,您将必须实现接口ForeachWriter
(Scala /Java文档),该接口具有在触发器之后生成作为输出的行序列时调用的方法。请注意以下要点。
-
作者必须是可序列化的,因为它将被序列化并发送给执行者执行。
-
所有这三种方法,
open
,process
并且close
将在执行者调用。 -
只有在
open
调用该方法时,写入程序才能执行所有的初始化(例如打开连接,启动事务等)。请注意,如果在创建对象之后,类中有任何初始化,那么该初始化将在驱动程序中发生(因为这是正在创建的实例),这可能不是您打算的。 -
version
并且partition
是两个参数,open
其中唯一地表示需要被推出的一组行。version
是每个触发器增加的单调递增的id。partition
是表示输出分区的id,因为输出是分布式的,并且将在多个执行器上进行处理。 -
open
可以使用version
和partition
选择是否需要写入行的顺序。因此,它可以返回true
(继续写入),或false
(不需要写入)。如果false
返回,则process
不会在任何行上调用。例如,在部分故障之后,失败的触发器的一些输出分区可能已经被提交到数据库。基于存储在数据库中的元数据,作者可以识别已经被提交的分区,并因此返回false以跳过再次提交它们。 -
无论何时
open
被调用,close
也将被调用(除非JVM由于某些错误而退出)。即使open
返回false 也是如此。如果在处理和写入数据时有任何错误,close
将被调用带有错误。清理已经创建的状态(例如连接,事务等)是您的责任open
,因为没有资源泄漏。
管理流式查询
StreamingQuery
查询启动时创建的对象可用于监视和管理查询。
StreamingQuery query = df.writeStream().format("console").start(); // get the query object
query.id(); // get the unique identifier of the running query
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.sourceStatus(); // progress information about data has been read from the input sources
query.sinkStatus(); // progress information about data written to the output sink
您可以在单个SparkSession中启动任意数量的查询。他们都将同时运行共享群集资源。您可以使用sparkSession.streams()
来获取StreamingQueryManager
(斯卡拉 / Java的 / Python的文档),可用于管理当前活动查询。
SparkSession spark = ...
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用于监视和调试活动查询 - 以交互方式和异步方式。
互动API
您可以使用streamingQuery.lastProgress()
和直接获取活动查询的当前状态和指标 streamingQuery.status()
。 在Scala 和Java中lastProgress()
返回一个StreamingQueryProgress
对象,并 在Python中返回与该字段相同的字典。它具有关于流的最后一个触发器的进展的所有信息 - 处理哪些数据,处理速率,延迟等等。还有 哪些返回最后几个进度的数组。 streamingQuery.recentProgress
此外,在Scala 和Java中streamingQuery.status()
返回StreamingQueryStatus
对象以及 Python中具有相同字段的字典。它提供有关查询立即执行的信息 - 触发器是活动的,正在处理的数据等。
这里有几个例子。
StreamingQuery query = ...
System.out.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"
}
}
*/
System.out.println(query.status());
/* Will print something like the following.
{
"message" : "Waiting for data to arrive",
"isDataAvailable" : false,
"isTriggerActive" : false
}
*/
异步API
您还可以SparkSession
通过附加StreamingQueryListener
(Scala / Java文档)异步监视与a相关联的所有查询 。一旦你附加了你的自定义StreamingQueryListener
对象 sparkSession.streams.attachListener()
,当查询被启动和停止时以及在一个活动查询中进行进度时,你将得到回调。这是一个例子,
SparkSession spark = ...
spark.streams.addListener(new StreamingQueryListener() {
@Overrides void onQueryStarted(QueryStartedEvent queryStarted) {
System.out.println("Query started: " + queryStarted.id());
}
@Overrides void onQueryTerminated(QueryTerminatedEvent queryTerminated) {
System.out.println("Query terminated: " + queryTerminated.id());
}
@Overrides void onQueryProgress(QueryProgressEvent queryProgress) {
System.out.println("Query made progress: " + queryProgress.progress());
}
});
从故障恢复与检查点
如果发生故障或故意关机,您可以恢复之前的查询的进度和状态,并继续停止。这是使用检查点和写入日志完成的。您可以使用检查点位置配置查询,并且查询将将所有进度信息(即每个触发器中处理的偏移范围)和运行聚合(例如快速示例中的字数)保存到检查点位置。此检查点位置必须是HDFS兼容文件系统中的路径,并且可以在启动查询时将其设置为DataStreamWriter中的选项。
aggDF
.writeStream()
.outputMode("complete")
.option("checkpointLocation", "path/to/HDFS/dir")
.format("memory")
.start();