目录
1.概观
2.快速示例
3.编程模型
3.1.基本概念
3.2.处理事件时间和延迟数据
3.3.容错语义
4.使用数据集和数据框架的API
4.1.创建streaming DataFrames 和 streaming Datasets
4.1.1.输入源(Input Sources)
4.1.2.流式数据帧/数据集(streaming DataFrames/Datasets)的模式推断和划分
4.2.流式数据帧/数据集(streaming DataFrames/Datasets)上的操作
4.2.1.基本操作 - 选择,投影,聚合(Selection, Projection, Aggregation)
4.2.2.事件时间的窗口操作
4.2.3.加入运营(Join Operations)
4.2.3.1.流静态连接(Stream-static)
4.2.3.1.流连接(Stream-stream)
4.2.4.流式重复数据删除
4.2.5.处理多个水印的政策
4.2.6.任意有状态的行动
4.2.7.不支持的操作
4.3.启动流式查询
4.3.1.输出模式(Output Modes)
4.3.2.输出接收器(Output Sinks)
4.3.3.触发器(Triggers)
4.4.管理流式查询
4.5.监视流式查询
4.5.1.交互读取度量值
4.5.2.使用异步API以编程方式报告度量
4.5.3.使用Dropwizard报告指标
4.6.通过检查点从故障中恢复
4.7.流式查询中更改后的恢复语义
5.连续处理
5.1.[实验]
5.2.支持的查询
5.3.注意事项
6.附加信息
参考:官方文档
结构化流是一种基于Spark SQL引擎的可扩展且容错的流处理引擎。您可以像表达静态数据的批处理计算一样表达流式计算。Spark SQL引擎将负责逐步和连续地运行它,并在流数据继续到达时更新最终结果。您可以使用Scala,Java,Python或R中的数据集/数据框架API来表示流聚合,事件时间窗口,流到批处理连接等。计算在同一优化的Spark SQL引擎上执行。最后,系统通过检查点和预写日志确保端到端的一次性容错保证。简而言之,结构化流传输提供快速,可扩展,容错,端到端的精确一次流处理,而无需用户推理流式传输。
在内部,默认情况下,结构化流式查询使用微批处理引擎进行处理,该引擎将数据流作为一系列小批量作业处理,从而实现低至100毫秒的端到端延迟和完全一次的容错保证。但是,自Spark 2.3以来,我们引入了一种称为连续处理的新型低延迟处理模式,它可以实现低至1毫秒的端到端延迟,并且具有至少一次保证。无需更改查询中的数据集/数据框操作,您就可以根据应用程序要求选择模式。
在本指南中,我们将引导您完成编程模型和API。我们将解释大多使用默认的微批处理模式的概念,再后来讨论连续处理模式。首先,让我们从一个结构化流式查询的简单示例开始 - 一个流式字数。
设您希望维护从侦听TCP套接字的数据服务器接收的文本数据的运行字数。让我们看看如何使用Structured Streaming表达这一点。您可以在Scala/ Java / Python / R中看到完整的代码 。如果你下载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,它表示从侦听localhost:9999的服务器接收的文本数据,并转换DataFrame以计算字数。
// Create DataFrame representing the stream of input lines from connection to localhost:9999
Dataset lines = spark
.readStream()
.format("socket")
.option("host", "localhost")
.option("port", 9999)
.load();
// Split the lines into words
Dataset words = lines
.as(Encoders.STRING())
.flatMap((FlatMapFunction) x -> Arrays.asList(x.split(" ")).iterator(), Encoders.STRING());
// Generate running word count
Dataset wordCounts = words.groupBy("value").count();
此行DataFrame表示包含流式文本数据的无边界表。此表包含一列名为“value”的字符串,流式文本数据中的每一行将成为表中的一行。注意,由于我们只是在设置转换,所以目前还没有接收到任何数据,而且还没有启动转换。接下来,我们使用.as(encoders.string())将数据帧转换为字符串数据集,这样我们就可以应用平面图操作将每一行拆分为多个单词。结果单词数据集包含所有单词。最后,我们通过对数据集中的唯一值进行分组并对其进行计数来定义WordCounts数据帧。注意,这是一个流数据帧,它表示流的运行字数。
我们现在已经设置了关于流数据的查询。剩下的就是实际开始接收数据并计算计数。为此,我们将其设置为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
对象是该活动流式查询的句柄,我们决定等待查询终止,awaitTermination()
以防止在查询处于活动状态时退出该进程。
要实际执行此示例代码,您可以在自己的Spark应用程序中编译代码 ,或者只需 在下载Spark后运行该示例。我们正在展示后者。您首先需要使用Netcat(在大多数类Unix系统中找到的小实用程序)作为数据服务器运行
# TERMINAL 1:
# Running Netcat
$ nc -lk 9999
apache spark
apache hadoop
然后,在不同的终端中,您可以使用启动示例
# TERMINAL 2: RUNNING JavaStructuredNetworkWordCount
$ ./bin/run-example org.apache.spark.examples.sql.streaming.JavaStructuredNetworkWordCount localhost 9999
-------------------------------------------
Batch: 0
-------------------------------------------
+------+-----+
| value|count|
+------+-----+
|apache| 1|
| spark| 1|
+------+-----+
-------------------------------------------
Batch: 1
-------------------------------------------
+------+-----+
| value|count|
+------+-----+
|apache| 2|
| spark| 1|
|hadoop| 1|
+------+-----+
...
然后,在运行netcat服务器的终端中键入的任何行将被计数并每秒在屏幕上打印。它看起来像下面这样。
结构化流中的关键思想是将实时数据流视为连续追加的表。这导致新的流处理模型非常类似于批处理模型。您将流式计算表示为静态表上的标准批处理查询,Spark将其作为无界输入表上的增量查询运行。让我们更详细地了解这个模型。
将输入数据流视为“输入表”。到达流的每个数据项都像一个新行被附加到输入表。
对输入的查询将生成“结果表”。每个触发间隔(例如,每1秒),新行将附加到输入表,最终更新结果表。每当结果表更新时,我们都希望将更改的结果行写入外部接收器。
“输出”定义为写入外部存储器的内容。输出可以以不同的模式定义:
请注意,每种模式适用于某些类型的查询。稍后将对此进行详细讨论。
为了说明此模型的使用,让我们在上面的快速示例的上下文中理解模型。第一个lines
DataFrame是输入表,最终的wordCounts
DataFrame是结果表。需要注意的是在流媒体的查询lines
数据帧生成wordCounts
是完全一样的,因为它是一个静态的数据帧。但是,当启动此查询时,Spark将不断检查套接字连接中的新数据。如果有新数据,Spark将运行“增量”查询,该查询将先前运行的计数与新数据相结合,以计算更新的计数,如下所示。
请注意,Structured Streaming不会实现整个表。它从流数据源读取最新的可用数据,逐步处理以更新结果,然后丢弃源数据。它仅保留更新结果所需的最小中间状态数据(例如,前面示例中的中间计数)。
该模型与许多其他流处理引擎明显不同。许多流系统要求用户自己维护运行聚合,因此必须推断容错和数据一致性(至少一次,或至多一次,或完全一次)。在此模型中,Spark负责在有新数据时更新结果表,从而减轻用户对其的推理。作为一个例子,让我们看看这个模型如何处理基于事件时间的处理和迟到的数据。
事件时间是嵌入数据本身的时间。对于许多应用程序,您可能希望在此事件时间运行。例如,如果您想每分钟获取IoT设备生成的事件数,那么您可能希望使用生成数据的时间(即数据中的事件时间),而不是Spark接收的时间他们。此事件时间在此模型中非常自然地表达 - 来自设备的每个事件都是表中的一行,事件时间是行中的列值。因此,这种基于事件时间窗口的聚合查询既可以在静态数据集(例如,从收集的设备事件日志)上定义,也可以在数据流上定义,从而使用户的生活更加容易。
此外,该模型自然地处理基于其事件时间到达的时间晚于预期的数据。由于Spark正在更新结果表,因此它可以在存在延迟数据时完全控制更新旧聚合,以及清理旧聚合以限制中间状态数据的大小。从Spark 2.1开始,我们支持水印,允许用户指定后期数据的阈值,并允许引擎相应地清理旧状态。稍后将在“ 窗口操作”部分中详细介绍这些内容。
提供端到端的一次性语义是结构化流的设计背后的关键目标之一。为实现这一目标,我们设计了结构化流媒体源,接收器和执行引擎,以可靠地跟踪处理的确切进度,以便通过重新启动和/或重新处理来处理任何类型的故障。假设每个流源都具有偏移(类似于Kafka偏移或Kinesis序列号)以跟踪流中的读取位置。引擎使用检查点和预写日志来记录每个触发器中正在处理的数据的偏移范围。流式接收器设计为处理重新处理的幂等功能。结合使用可重放的源和幂等接收器,结构化流可以确保端到端的一次性语义 在任何失败。
从Spark 2.0开始,DataFrames和Datasets可以表示静态的,有界的数据,以及流式无界数据。与静态数据集/数据框类似,您可以使用公共入口点SparkSession
(Scala / Java / Python / R文档)从流源创建流式数据框/数据集,并对它们应用与静态数据框/数据集相同的操作。如果您不熟悉数据集/数据框架,强烈建议您使用“ 数据框架/数据集编程指南”熟悉它们 。
可以通过返回的DataStreamReader
接口(Scala / Java / Python文档)创建Streaming DataFrame SparkSession.readStream()
。在R中,用这个read.stream()
方法。与用于创建静态DataFrame的读取接口类似,您可以指定源的详细信息 - 数据格式,架构,选项等。
有一些内置源。
File source - 将目录中写入的文件作为数据流读取。支持的文件格式为text,csv,json,orc,parquet。有关更新的列表,请参阅DataStreamReader接口的文档,以及每种文件格式支持的选项。请注意,文件必须原子地放置在给定目录中,在大多数文件系统中,可以通过文件移动操作来实现。
Kafka source - 从Kafka读取数据。它与Kafka经纪人版本0.10.0或更高版本兼容。有关更多详细信息,请参阅Kafka集成指南。
Socket source (for testing) - 从套接字连接读取UTF8文本数据。侦听服务器套接字位于驱动程序中。请注意,这应仅用于测试,因为这不提供端到端的容错保证。
Rate source (for testing) - 以每秒指定的行数生成数据,每个输出行包含一个timestamp
和value
。其中timestamp
是一个Timestamp
含有信息分配的时间类型,并且value
是Long
包含消息的计数从0开始作为第一行类型。此源用于测试和基准测试。
某些源不具有容错能力,因为它们无法保证在发生故障后可以使用检查点偏移重放数据。请参阅前面 fault-tolerance semantics。以下是Spark中所有源代码的详细信息。
资源 | 选项 | 容错 | 笔记 |
---|---|---|---|
File source | path :输入目录的路径,并且对所有文件格式都是通用的。 maxFilesPerTrigger :每个触发器中要考虑的最大新文件数(默认值:无最大值) latestFirst :是否先处理最新的新文件,当存在大量积压的文件时有用(默认值:false) fileNameOnly :是否基于以下方法检查新文件只有文件名而不是完整路径(默认值:false)。将此设置为“true”时,以下文件将被视为同一文件,因为它们的文件名“dataset.txt”是相同的: “file:///dataset.txt” “s3:// a / dataset.txt“ ”s3n://a/b/dataset.txt“ ”s3a://a/b/c/dataset.txt“ 对于特定于文件格式的选项, DataStreamReader (Scala / Java / Python / R)。例如,“镶木地板”格式选项请参阅DataStreamReader.parquet() 。 此外,还有会话配置会影响某些文件格式。有关更多详细信息,请参见SQL编程指南。例如,对于“镶木地板”,请参阅镶木地板配置部分。 |
是 | 支持glob路径,但不支持多个以逗号分隔的路径/ globs。 |
Socket Source | host :要连接的主机,必须指定port :要连接的端口,必须指定 |
没有 | |
Rate Source | rowsPerSecond (例如100,默认值:1):每秒应生成多少行。rampUpTime (例如5s,默认值:0s):在生成速度变为之前加速多长时间rowsPerSecond 。使用比秒更精细的粒度将被截断为整数秒。numPartitions (例如10,默认值:Spark的默认并行性):生成的行的分区号。源代码将尽力达到目标 rowsPerSecond ,但查询可能会受到资源限制,并且numPartitions 可以进行调整以帮助达到所需的速度。 |
是 | |
Kafka Source | 请参阅Kafka集成指南。 | 是 |
这里有些例子。
SparkSession spark = ...
// Read text from socket
Dataset 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 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")
默认情况下,基于文件的源的结构化流需要您指定架构,而不是依靠Spark自动推断它。此限制可确保即使在出现故障的情况下,也将使用一致的架构进行流式查询。对于临时用例,您可以通过设置spark.sql.streaming.schemaInference=
true
重新启用架构推断。
当命名的子目录/key=value/
存在且列表将自动递归到这些目录中时,确实会发生分区发现。如果这些列出现在用户提供的模式中,则Spark将根据正在读取的文件的路径填充它们。构成分区方案的目录必须在查询开始时存在,并且必须保持静态。例如,当
/data/year=2015/
存在时可以添加/data/year=2016/
,但更改分区列(即通过创建目录/data/date=2016-04-17/
)无效。
您可以将各种操作上的流式数据帧/数据集(streaming DataFrames/Datasets)-从无类型,类似于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 deviceType;
private Double signal;
private java.sql.Date time;
...
// Getter and setter methods for each field
}
Dataset df = ...; // streaming DataFrame with IOT device data with schema { device: string, type: string, signal: double, time: DateType }
Dataset 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((FilterFunction) value -> value.getSignal() > 10)
.map((MapFunction) value -> value.getDevice(), Encoders.STRING());
// 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
ds.groupByKey((MapFunction) value -> value.getDeviceType(), Encoders.STRING())
.agg(typed.avg((MapFunction) value -> value.getSignal()));
您还可以将流式DataFrame / Dataset注册为临时视图,然后在其上应用SQL命令
df.createOrReplaceTempView("updates");
spark.sql("select count(*) from updates"); // returns another streaming DF
注意,您可以使用df.isStreaming来
确定DataFrame / Dataset是否具有流数据。
df.isStreaming()
使用结构化流式传输时,滑动事件时间窗口上的聚合非常简单,并且与分组聚合非常相似。在分组聚合中,为用户指定的分组列中的每个唯一值维护聚合值(例如计数)。在基于窗口的聚合的情况下,为每个窗口维护一行的事件时间的聚合值。让我们通过一个例子来理解这一点。
想象一下,我们的快速示例已被修改,流现在包含行以及生成行的时间。我们不想运行字数,而是计算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的计数。因此,计数将由分组键(即单词)和窗口(可以从事件时间计算)索引。
结果表看起来如下所示。
由于此窗口类似于分组,因此在代码中,您可以使用groupBy()
和window()
操作来表示窗口化聚合。您可以在Scala / Java / Python中看到以下示例的完整代码 。
Dataset words = ... // streaming DataFrame of schema { timestamp: Timestamp, word: String }
// Group the data by window and word and compute the count of each group
Dataset windowedCounts = words.groupBy(
functions.window(words.col("timestamp"), "10 minutes", "5 minutes"),
words.col("word")
).count();
处理延迟数据和水印(Watermarking)
现在考虑如果其中一个事件到达应用程序的后期会发生什么。例如,应用程序在12:11可以接收在12:04(即事件时间)生成的单词。应用程序应使用时间12:04而不是12:11来更新窗口的旧计数12:00 - 12:10
。这在我们基于窗口的分组中自然发生 - 结构化流可以长时间维持部分聚合的中间状态,以便后期数据可以正确更新旧窗口的聚合,如下所示。
但是,要运行此查询数天,系统必须限制它累积的中间内存中状态的数量。这意味着系统需要知道何时可以从内存状态中删除旧聚合,因为应用程序不再接收该聚合的后期数据。为了实现这一点,我们在Spark 2.1中引入了 水印,使引擎能够自动跟踪数据中的当前事件时间并尝试相应地清理旧状态。您可以通过指定事件时间列以及根据事件时间预计数据的延迟时间来定义查询的水印。对于特定时间结束的窗口T
,引擎将保持状态并允许延迟数据更新状态直到(max event time seen by the engine - late threshold > T)
。换句话说,阈值内的后期数据将被聚合,但是晚于阈值的数据将开始被丢弃(参见 本节后面的确切保证)。让我们通过一个例子来理解这一点。我们可以使用withWatermark()
如下所示的前一个示例轻松定义水印。
Dataset words = ... // streaming DataFrame of schema { timestamp: Timestamp, word: String }
// Group the data by window and word and compute the count of each group
Dataset 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
。该水印使发动机保持中间状态另外10分钟,以允许计算延迟数据。例如,数据(12:09, cat)
是乱序的,而且很晚,它落在窗口12:00 - 12:10
和12:05 - 12:15
。由于它仍然12:04
位于触发器中的水印之前,因此引擎仍将中间计数保持为状态并正确更新相关窗口的计数。但是,当水印更新为12:11
,窗口的中间状态(12:00 - 12:10)
被清除,所有后续数据(例如(12:04, donkey)
)被认为“太晚”,因此被忽略。请注意,在每次触发后,更新的计数(即紫色行)将作为触发输出写入sink,如更新模式所示。
某些接收器(例如文件)可能不支持更新模式所需的细粒度更新。为了使用它们,我们还支持附加模式,其中只有最终计数被写入接收器。这如下图所示。
请注意,withWatermark
在非流式数据集上使用是no-op。由于水印不应以任何方式影响任何批量查询,我们将直接忽略它。
与之前的更新模式类似,引擎维护每个窗口的中间计数。但是,部分计数不会更新到结果表,也不会写入接收器。引擎等待“10分钟”以计算延迟日期,然后丢弃窗口<水印的中间状态,并将最终计数附加到结果表/接收器。例如,12:00 - 12:10
只有在水印更新到之后,窗口的最终计数才会附加到结果表中12:11
。
用于清除聚合状态的水印的条件
重要的是要注意,在聚合查询中清除状态的水印必须满足以下条件(从Spark 2.1.1开始,将来可能会有变化)。
window
事件时间列上的。df.withWatermark("time", "1 min").groupBy("time2").count()
在追加输出模式下无效,因为水印是在与聚合列不同的列上定义的。df.groupBy("time").count().withWatermark("time", "1 min")
在追加输出模式下无效。带水印聚合的语义保证
withWatermark
)为“2小时”可确保引擎永远不会丢弃延迟小于2小时的任何数据。换句话说,任何不到2小时(在事件时间方面)的数据都保证汇总到那时处理的最新数据。
结构化流式传输支持将流式数据集/数据框架与静态数据集/数据框架以及另一个流式数据集/数据框架连接起来。
流连接的结果以递增方式生成,类似于上一节中的流聚合的结果。
请注意,在所有受支持的连接类型中,与流式数据集/数据框架的连接结果与使用包含流中相同数据的静态数据集/数据框架的结果完全相同。
自Spark 2.0引入以来,Structured Streaming支持流和静态DataFrame / Dataset之间的连接(内连接和某种类型的外连接)。这是一个简单的例子。
Dataset staticDf = spark.read(). ...;
Dataset 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
请注意,流静态连接不是有状态的,因此不需要进行状态管理。但是,尚不支持几种类型的流静态外连接。这些列在此加入部分的末尾。
在Spark 2.3中,我们添加了对流 - 流连接的支持,也就是说,您可以加入两个流数据集/数据框。在两个数据流之间生成连接结果的挑战在于,在任何时间点,数据集的视图对于连接的两侧都是不完整的,这使得在输入之间找到匹配更加困难。从一个输入流接收到的任何行都可以与将来的任何行匹配,但仍将从另一个输入流接收到该行。因此,对于两个输入流,我们将过去的输入缓冲为流状态,以便我们可以将每个未来输入与过去的输入相匹配,从而生成连接结果。此外,类似于流聚合,我们自动处理延迟的无序数据,并可以使用水印限制状态。让我们讨论不同类型的受支持的流 - 流连接以及如何使用它们。
带有可选水印的内部连接
支持任何类型在列上的内连接以及任何类型的连接条件。但是,当流运行时,流状态的大小将无限增长,因为 必须保存所有过去的输入,因为任何新输入都可以与过去的任何输入匹配。为了避免无界状态,您必须定义其他连接条件,以便无限期旧输入无法与将来的输入匹配,因此可以从状态清除。换句话说,您必须在连接中执行以下附加步骤。
定义两个输入上的水印延迟,以便引擎知道输入的延迟时间(类似于流聚合)
在两个输入上定义事件时间约束,以便引擎可以确定何时不需要一个输入的旧行(即不满足时间约束)与另一个输入匹配。可以用两种方式之一定义该约束。
时间范围连接条件(例如...JOIN ON leftTime BETWEEN rightTime AND rightTime + INTERVAL 1 HOUR
),
加入事件时间窗口(例如...JOIN ON leftTimeWindow = rightTimeWindow
)。
让我们通过一个例子来理解这一点。
假设我们希望加入一系列广告展示次数(展示广告时),并在广告上添加另一个用户点击流,以便在展示次数达到可获利的点击时进行关联。要在此流 - 流连接中允许状态清理,您必须指定水印延迟和时间约束,如下所示。
水印延迟:比如说,展示次数和相应的点击次数可以分别在事件时间内延迟/无序,最多2个小时和3个小时。
事件时间范围条件:假设,在相应的印象后0秒到1小时的时间范围内可能发生咔嗒声。
import static org.apache.spark.sql.functions.expr
Dataset impressions = spark.readStream(). ...
Dataset clicks = spark.readStream(). ...
// Apply watermarks on event-time columns
Dataset impressionsWithWatermark = impressions.withWatermark("impressionTime", "2 hours");
Dataset clicksWithWatermark = clicks.withWatermark("clickTime", "3 hours");
// Join with event-time constraints
impressionsWithWatermark.join(
clicksWithWatermark,
expr(
"clickAdId = impressionAdId AND " +
"clickTime >= impressionTime AND " +
"clickTime <= impressionTime + interval 1 hour ")
);
具有水印的流内部连接的语义保证
这类似于通过聚合水印提供的保证。水印延迟“2小时”可确保发动机永远不会丢失任何延迟小于2小时的数据。但延迟2小时以上的数据可能会或可能不会得到处理。
带水印的外部连接
虽然水印+事件时间约束对于内连接是可选的,但对于左外连接和右外连接,必须指定它们。这是因为为了在外连接中生成NULL结果,引擎必须知道输入行何时不会与将来的任何内容匹配。
因此,必须指定水印+事件时间约束以生成正确的结果。因此,使用外部联接的查询看起来与之前的广告货币化示例非常相似,只是会有一个附加参数将其指定为外部联接。
impressionsWithWatermark.join(
clicksWithWatermark,
expr(
"clickAdId = impressionAdId AND " +
"clickTime >= impressionTime AND " +
"clickTime <= impressionTime + interval 1 hour "),
"leftOuter" // can be "inner", "leftOuter", "rightOuter"
);
具有水印的流 - 流外连接的语义保证
外连接与内部连接具有相同的保证, 关于水印延迟以及数据是否会被丢弃。
注意事项
关于如何生成外部结果,有一些重要的特征需要注意。
将生成外部NULL结果,延迟取决于指定的水印延迟和时间范围条件。这是因为引擎必须等待那么长时间以确保没有匹配,并且将来不会再有匹配。
在微批量引擎的当前实现中,水印在微批次结束时前进,并且下一个微批次使用更新的水印来清理状态并输出外部结果。由于我们仅在存在要处理的新数据时才触发微批处理,因此如果在流中没有接收到新数据,则外部结果的生成可能会延迟。 简而言之,如果连接的两个输入流中的任何一个在一段时间内没有接收到数据,则外部(两种情况,左侧或右侧)输出可能会延迟。
支持流式查询中的连接矩阵
Left Input | Right Input | Join Type | |
---|---|---|---|
Static | Static | All types | 支持,因为它不在流数据上,即使它可以存在于流式查询中 |
Stream | Static | Inner | 支持,而不是有状态 |
Left Outer | 支持,而不是有状态 | ||
Right Outer | 不支持 | ||
Full Outer | 不支持 | ||
Static | Stream | Inner | 支持,而不是有状态 |
Left Outer | 不支持 | ||
Right Outer | 支持,而不是有状态 | ||
Full Outer | 不支持 | ||
Stream | Stream | Inner | 支持,可选择在两侧指定水印+状态清理的时间限制 |
Left Outer | 有条件支持,必须在正确+时间约束上指定水印以获得正确的结果,可选择在左侧指定水印以进行所有状态清理 | ||
Right Outer | 有条件支持,必须在左侧指定水印+时间约束以获得正确结果,可选择在右侧指定水印以进行所有状态清理 | ||
Full Outer | 不支持 |
有关支持的连接的其他详细信息
连接可以级联,也就是说,你可以做到df1.join(df2, ...).join(df3, ...).join(df4, ....)
。
从Spark 2.3开始,只有在查询处于追加输出模式时才能使用连接。其他输出模式尚不支持。
从Spark 2.3开始,在连接之前不能使用其他非类似地图的操作。以下是一些不能使用的例子。
在加入之前无法使用流聚合。
在连接之前,无法在更新模式下使用mapGroupsWithState和flatMapGroupsWithState。
您可以使用事件中的唯一标识符对数据流中的记录进行重复数据删除。这与使用唯一标识符列的静态重复数据删除完全相同。
该查询将存储来自先前记录的必要数据量,以便它可以过滤重复记录。与聚合类似,您可以使用带或不带水印的重复数据删除。
使用水印 - 如果重复记录的到达时间有上限,则可以在事件时间列上定义水印,并使用guid和事件时间列进行重复数据删除。该查询将使用水印从过去的记录中删除旧的状态数据,这些记录不会再被重复。这限制了查询必须维护的状态量。
没有水印 - 由于重复记录可能到达时没有界限,查询将来自所有过去记录的数据存储为状态。
Dataset streamingDf = spark.readStream(). ...; // columns: guid, eventTime, ...
// Without watermark using guid column
streamingDf.dropDuplicates("guid");
// With watermark using guid and eventTime columns
streamingDf
.withWatermark("eventTime", "10 seconds")
.dropDuplicates("guid", "eventTime");
流式查询可以具有多个联合或连接在一起的输入流。每个输入流可以具有不同的后期数据阈值,这些阈值需要被容忍用于有状态操作。您可以withWatermarks("eventTime", delay)
在每个输入流上指定这些阈值 。例如,考虑在inputStream1
和之间使用流 - 流连接的查询inputStream2
。
inputStream1.withWatermark(“eventTime1”, “1 hour”) .join( inputStream2.withWatermark(“eventTime2”, “2 hours”), joinCondition)
在执行查询时,Structured Streaming单独跟踪每个输入流中看到的最大事件时间,根据相应的延迟计算水印,并选择单个全局水印用于有状态操作。默认情况下,选择最小值作为全局水印,因为它确保如果其中一个流落后于其他流(例如,其中一个流因上游故障而停止接收数据),则不会意外丢弃数据。换句话说,全局水印将以最慢流的速度安全地移动,并且查询输出将相应地延迟。
但是,在某些情况下,您可能希望获得更快的结果,即使这意味着从最慢的流中删除数据。从Spark 2.4开始,您可以设置多个水印策略,通过将SQL配置设置spark.sql.streaming.multipleWatermarkPolicy
为max
(默认值为min
)来选择最大值作为全局水印 。这使得全局水印以最快的速度发展。但是,作为副作用,来自较慢流的数据将被积极地丢弃。因此,明智地使用此配置。
许多用例需要比聚合更高级的有状态操作。例如,在许多用例中,您必须从事件的数据流中跟踪会话。要进行此类会话,您必须将任意类型的数据保存为状态,并使用每个触发器中的数据流事件对状态执行任意操作。从Spark 2.2开始,这可以通过操作mapGroupsWithState
和更强大的操作来完成flatMapGroupsWithState
。这两个操作都允许您在分组数据集上应用用户定义的代码以更新用户定义的状态。有关更具体的详细信息,请查看API文档(Scala / Java)和示例(Scala / Java)。
流式DataFrames / Datasets不支持一些DataFrame / Dataset操作。其中一些如下。
流数据集尚不支持多个流聚合(即流式DF上的聚合链)。
流数据集不支持限制和前N行。
不支持对流数据集进行不同的操作。
仅在聚合和完全输出模式之后,流数据集才支持排序操作。
不支持流数据集上的几种外连接类型。有关 详细信息,请参阅“ 连接操作”部分中的 支持矩阵。
此外,有一些数据集方法不适用于流数据集。它们是立即运行查询并返回结果的操作,这对流式数据集没有意义。相反,这些功能可以通过显式启动流式查询来完成。
count()
- 无法从流式数据集返回单个计数。而是使用ds.groupBy().count()
它返回包含运行计数的流数据集。
foreach()
- 而是使用ds.writeStream.foreach(...)
。
show()
- 而是使用控制台接收器。
如果您尝试任何这些操作,您将看到AnalysisException
类似“流数据框/数据集不支持XYZ操作”。虽然其中一些可能在未来的Spark版本中得到支持,但还有一些基本上难以有效地实现流数据。例如,不支持对输入流进行排序,因为它需要跟踪流中接收的所有数据。因此,这基本上难以有效执行。
一旦定义了最终结果DataFrame / Dataset,剩下的就是开始流式计算。为此,您必须使用返回的DataStreamWriter
(Scala / Java / Python文档)Dataset.writeStream()
。您必须在此界面中指定以下一项或多项。
输出接收器的详细信息:数据格式,位置等。
输出模式:指定写入输出接收器的内容。
查询名称:可选地,指定查询的唯一名称以进行标识。
触发间隔:可选择指定触发间隔。如果未指定,则系统将在前一处理完成后立即检查新数据的可用性。如果由于先前的处理尚未完成而错过了触发时间,则系统将立即触发处理。
检查点位置:对于可以保证端到端容错的某些输出接收器,请指定系统写入所有检查点信息的位置。这应该是与HDFS兼容的容错文件系统中的目录。
追加模式(Append mode )(默认) - 这是默认模式,其中只有自上次触发后添加到结果表的新行才会输出到接收器。仅支持那些添加到结果表中的行永远不会更改的查询。因此,此模式保证每行仅输出一次(假设容错接收器)。例如,仅查询select
, where
,map
,flatMap
,filter
,join
,等会支持追加模式。
完整模式(Complete mode) - 每次触发后,整个结果表将输出到接收器。聚合查询支持此功能。
更新模式(Update mode) - (自Spark 2.1.1起可用)仅将结果表中自上次触发后更新的行输出到接收器。在将来的版本中添加更多信息。
不同类型的流式查询支持不同的输出模式。
查询类型 | 支持的输出模式 | 说明 | |
---|---|---|---|
Queries with aggregation | 事件时间与水印的聚合 | Append, Update, Complete | 追加模式使用水印来删除旧的聚合状态。但是窗口聚合的输出延迟了`withWatermark()`中指定的后期阈值,如模式语义,在结束后(即超过水印后)行只能添加到结果表中一次。有关详细信息,请参阅“ Late Data”部分 更新模式使用水印来删除旧的聚合状态。 完成模式不会丢弃旧的聚合状态,因为根据定义,此模式会保留结果表中的所有数据。 |
其他汇总 | Complete, Update | 由于未定义水印(仅在其他类别中定义),因此不会丢弃旧的聚合状态。 不支持追加模式,因为聚合可以更新,因此违反了此模式的语义。 |
|
Queries with mapGroupsWithState |
Update | ||
Queries with flatMapGroupsWithState |
附加操作模式 | Append | 之后允许聚合flatMapGroupsWithState 。 |
更新操作模式 | Update | 之后不允许聚合flatMapGroupsWithState 。 |
|
Queries with joins |
Append | 尚不支持更新和完成模式。有关支持 哪种类型的连接的详细信息,请参阅“ 连接操作”部分中的支持列表。 | |
Other queries | Append, Update | 不支持完整模式,因为在结果表中保留所有未聚合数据是不可行的。 |
有几种类型的内置输出接收器
writeStream
.format("parquet") // can be "orc", "json", "csv", etc.
.option("path", "path/to/destination/dir")
.start()
writeStream
.format("kafka")
.option("kafka.bootstrap.servers", "host1:port1,host2:port2")
.option("topic", "updates")
.start()
writeStream
.foreach(...)
.start()
writeStream
.format("console")
.start()
writeStream
.format("memory")
.queryName("tableName")
.start()
某些接收器不具有容错能力,因为它们不保证输出的持久性,仅用于调试目的。请参阅fault-tolerance semantics以下是Spark中所有接收器的详细信息。
接收器(Sink) | 支持的输出模式 | 选项 | 容错(Fault-tolerant) | 说明 |
---|---|---|---|---|
File Sink | Append | path :必须指定输出目录的路径。 有关特定于文件格式的选项,请参阅DataFrameWriter(Scala / Java/ Python / R)中的相关方法。例如,“parquet”格式选项请参阅 DataFrameWriter.parquet() |
是的(完全一次) | 支持写入分区表。按时间划分可能很有用。 |
Kafka Sink | Append, Update, Complete | 请参阅Kafka集成指南 | 是(至少一次) | “ Kafka集成指南”中的更多详细信息 |
Foreach Sink | Append, Update, Complete | 没有 | 取决于ForeachWriter的实现 | |
ForeachBatch Sink | Append, Update, Complete | 没有 | 取决于实施 | |
Console Sink | Append, Update, Complete | numRows :每次触发器打印的行数(默认值:20) truncate :是否过长时截断输出(默认值:true) |
没有 | |
Memory Sink | Append, Complete | 没有 | 不是。但在完整模式下,重新启动的查询将重新创建完整的表。 | 表名是查询名称。 |
请注意,您必须调用start()
实际开始执行查询。这将返回一个StreamingQuery对象,该对象是持续运行的执行的句柄。现在,让我们通过几个例子来理解这一切。
// ========== DF with no aggregations ==========
Dataset 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 ==========
Dataset 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和ForeachBatch
在foreach
与foreachBatch
操作让您应用在流媒体查询的输出任意操作和写作的逻辑。它们具有稍微不同的用例 - 同时foreach
允许每行上的自定义写入逻辑,foreachBatch
允许在每个微批量的输出上进行任意操作和自定义逻辑。
ForeachBatch
foreachBatch(...)
允许您指定在流式查询的每个微批次的输出数据上执行的函数。从Spark 2.4开始,Scala,Java和Python都支持它。它需要两个参数:DataFrame或Dataset,它具有微批次的输出数据和微批次的唯一ID。
streamingDatasetOfString.writeStream().foreachBatch(
new VoidFunction2, Long> {
public void call(Dataset dataset, Long batchId) {
// Transform and write batchDF
}
}
).start();
有了foreachBatch
,您可以执行以下操作。
foreachBatch
,您可以在每个微批次的输出上使用批处理数据编写器。写入多个位置 - 如果要将流式查询的输出写入多个位置,则可以简单地多次写入输出DataFrame / Dataset。但是,每次写入尝试都会导致重新计算输出数据(包括可能重新读取输入数据)。要避免重新计算,您应该缓存输出DataFrame / Dataset,将其写入多个位置,然后将其解除。这是一个大纲。
streamingDF.writeStream.foreachBatch {(batchDF:DataFrame,batchId:Long)=> batchDF.persist()batchDF.write.format(...).save(...)// location 1 batchDF.write.format(...).save( ...)// location 2 batchDF.unpersist()}
foreachBatch
,您可以在每个微批输出上应用其中一些操作。但是,您必须自己解释执行该操作的端到端语义。注意:
foreachBatch
仅提供至少一次写保证。但是,您可以使用提供给该函数的batchId作为重复数据删除输出并获得一次性保证的方法。foreachBatch
不适用于连续处理模式,因为它从根本上依赖于流式查询的微批量执行。如果以连续模式写入数据,请foreach
改用。Foreach
如果foreachBatch
不是一个选项(例如,相应的批处理数据写入器不存在,或连续处理模式),那么您可以使用foreach
编写自定义的逻辑。具体来说,可以通过将其划分为三种方法表达数据写入逻辑:open
,process
,和close
。从Spark 2.4开始,foreach
可以使用Scala,Java和Python。
在Java中,您必须扩展类ForeachWriter
(docs)。
streamingDatasetOfString.writeStream().foreach(
new ForeachWriter[String] {
@Override public boolean open(long partitionId, long version) {
// Open connection
}
@Override public void process(String record) {
// Write string to connection
}
@Override public void close(Throwable errorOrNull) {
// Close the connection
}
}
).start();
执行语义 启动流式查询时,Spark以下列方式调用函数或对象的方法:
此对象的单个副本负责查询中单个任务生成的所有数据。换句话说,一个实例负责处理以分布式方式生成的数据的一个分区。
此对象必须是可序列化的,因为每个任务都将获得所提供对象的新的序列化反序列化副本。因此,强烈建议在调用open()方法之后完成用于写入数据的任何初始化(例如,打开连接或启动事务),这表示任务已准备好生成数据。
方法的生命周期如下:
对于partition_id的每个分区:
对于epoch_id的流数据的每个批次/纪元:
方法open(partitionId,epochId)被调用。
如果open(...)返回true,则对于分区和批处理/纪元中的每一行,将调用方法进程(行)。
调用方法close(错误),在处理行时看到错误(如果有)。
如果open()方法存在并且成功返回(不管返回值),则调用close()方法(如果存在),除非JVM或Python进程在中间崩溃。
注意:当失败导致某些输入数据的重新处理时,open()方法中的partitionId和epochId可用于对生成的数据进行重复数据删除。这取决于查询的执行模式。如果以微批处理模式执行流式查询,则保证由唯一元组(partition_id,epoch_id)表示的每个分区具有相同的数据。因此,(partition_id,epoch_id)可用于对数据进行重复数据删除和/或事务提交,并实现一次性保证。但是,如果正在以连续模式执行流式查询,则此保证不成立,因此不应用于重复数据删除。
流式查询的触发器设置定义了流式数据处理的时间,查询是作为具有固定批处理间隔的微批量查询还是作为连续处理查询来执行。以下是支持的各种触发器。
触发类型 | 描述 |
---|---|
未指定(默认) | 如果未明确指定触发设置,则默认情况下,查询将以微批处理模式执行,一旦前一个微批处理完成处理,将立即生成微批处理。 |
固定间隔微批次 | 查询将以微批处理模式执行,其中微批处理将以用户指定的间隔启动。
|
一次性微批次 | 查询将执行*仅一个(only one)*微批处理所有可用数据,然后自行停止。这在您希望定期启动集群,处理自上一个时间段以来可用的所有内容,然后关闭集群的方案中非常有用。在某些情况下,这可能会显着节省成本。 |
连续固定检查点间隔 (实验) |
查询将以新的低延迟,连续处理模式执行。在下面的连续处理部分中阅读更多相关信息。 |
import org.apache.spark.sql.streaming.Trigger
// Default trigger (runs micro-batch as soon as it can)
df.writeStream
.format("console")
.start();
// ProcessingTime trigger with two-seconds micro-batch interval
df.writeStream
.format("console")
.trigger(Trigger.ProcessingTime("2 seconds"))
.start();
// One-time trigger
df.writeStream
.format("console")
.trigger(Trigger.Once())
.start();
// Continuous trigger with one-second checkpointing interval
df.writeStream
.format("console")
.trigger(Trigger.Continuous("1 second"))
.start();
StreamingQuery
启动查询时创建的对象可用于监视和管理查询。
StreamingQuery 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 文档),可用于管理当前活动查询。
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
有多种方法可以监控活动的流式查询。您可以使用Spark的Dropwizard Metrics支持将指标推送到外部系统,也可以通过编程方式访问它们。
您可以使用streamingQuery.lastProgress()
和streamingQuery.status()
直接获取活动查询的当前状态和指标 。 lastProgress()
返回Scala 和Java中的StreamingQueryProgress
对象以及 Python中具有相同字段的字典。它包含有关在流的最后一次触发中所取得进展的所有信息 - 处理了哪些数据,处理速率,延迟等等。还有streamingQuery.recentProgress
返回最后几个进展的数组。
此外,streamingQuery.status()
返回Scala 和Java中的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
}
*/
您还可以SparkSession
通过附加StreamingQueryListener
(Scala / Java文档)异步监视与a关联的所有查询 。附加自定义StreamingQueryListener
对象后 sparkSession.streams.attachListener()
,将在启动和停止查询以及活动查询中取得进展时获得回调。
parkSession spark = ...
spark.streams().addListener(new StreamingQueryListener() {
@Override
public void onQueryStarted(QueryStartedEvent queryStarted) {
System.out.println("Query started: " + queryStarted.id());
}
@Override
public void onQueryTerminated(QueryTerminatedEvent queryTerminated) {
System.out.println("Query terminated: " + queryTerminated.id());
}
@Override
public void onQueryProgress(QueryProgressEvent queryProgress) {
System.out.println("Query made progress: " + queryProgress.progress());
}
});
Spark支持使用Dropwizard库报告指标。要同时报告结构化流式查询的度量标准,您必须spark.sql.streaming.metricsEnabled
在SparkSession中显式启用配置。
spark.conf().set("spark.sql.streaming.metricsEnabled", "true");
// or
spark.sql("SET spark.sql.streaming.metricsEnabled=true");
启用此配置后,在SparkSession中启动的所有查询都会通过Dropwizard将指标报告给已配置的任何接收器(例如Ganglia,Graphite,JMX等)。
如果发生故障或故意关机,您可以恢复先前查询的先前进度和状态,并从中断处继续。这是使用检查点和预写日志完成的。您可以使用检查点位置配置查询,查询将保存所有进度信息(即每个触发器中处理的偏移范围)和运行聚合(例如快速示例中的字数)到检查点位置。此检查点位置必须是HDFS兼容文件系统中的路径,并且可以在启动查询时设置为DataStreamWriter中的选项。
aggDF
.writeStream()
.outputMode("complete")
.option("checkpointLocation", "path/to/HDFS/dir")
.format("memory")
.start();
在从同一检查点位置重新启动之间允许对流查询进行哪些更改存在限制。以下是一些不允许的更改,或者更改的效果未明确定义。对于他们所有人:
术语“ 允许”意味着您可以执行指定的更改,但其效果的语义是否明确定义取决于查询和更改。
术语“ 不允许”意味着您不应该执行指定的更改,因为重新启动的查询可能会因不可预测的错误而失败。sdf
表示使用sparkSession.readStream生成的流式DataFrame / Dataset。
变化的类型
输入源的数量或类型(即不同来源)的变化:这是不允许的。
输入源参数的更改:是否允许此更改以及更改的语义是否明确定义取决于源和查询。这里有一些例子。
允许添加/删除/修改费率限制:spark.readStream.format("kafka").option("subscribe", "topic")
spark.readStream.format("kafka").option("subscribe", "topic").option("maxOffsetsPerTrigger", ...)
通常不允许对订阅的主题/文件进行更改,因为结果是不可预测的:spark.readStream.format("kafka").option("subscribe", "topic")
spark.readStream.format("kafka").option("subscribe", "newTopic")
输出接收器类型的变化:允许几个特定接收器组合之间的变化。这需要根据具体情况进行验证。这里有一些例子。
允许File sink to Kafka sink。卡夫卡只会看到新数据。
不允许Kafka sink to file sink 。
Kafka sink changed to foreach, or vice versa is allowed。
输出接收器参数的变化:是否允许这种变化以及变化的语义是否明确定义取决于接收器和查询。这里有一些例子。
不允许更改文件接收器的输出目录:sdf.writeStream.format("parquet").option("path", "/somePath")
sdf.writeStream.format("parquet").option("path", "/anotherPath")
允许对输出主题进行更改:sdf.writeStream.format("kafka").option("topic", "someTopic")
sdf.writeStream.format("kafka").option("topic", "anotherTopic")
允许对用户定义的foreach接收器(即ForeachWriter
代码)进行更改,但更改的语义取决于代码。
*投影/过滤/类似地图操作的变化**:允许某些情况。例如:
允许添加/删除过滤器:sdf.selectExpr("a")
sdf.where(...).selectExpr("a").filter(...)
。
允许更改具有相同输出模式的投影:sdf.selectExpr("stringColumn AS json").writeStream
sdf.selectExpr("anotherStringColumn AS json").writeStream
在与不同的输出模式突起变化是有条件地允许:sdf.selectExpr("a").writeStream
以sdf.selectExpr("b").writeStream
允许仅当输出信宿允许从模式更改"a"
到"b"
。
有状态操作的变化:流式查询中的某些操作需要维护状态数据以便不断更新结果。结构化流自动检查状态数据到容错存储(例如,HDFS,AWS S3,Azure Blob存储)并在重新启动后恢复它。但是,这假设状态数据的模式在重新启动时保持相同。这意味着 在重新启动之间不允许对流式查询的有状态操作进行任何更改(即添加,删除或架构修改)。以下是有状态操作的列表,在重新启动之间不应更改其架构以确保状态恢复:
流聚合:例如,sdf.groupBy("a").agg(...)
。不允许对分组键或聚合的数量或类型进行任何更改。
流式重复数据删除:例如,sdf.dropDuplicates("a")
。不允许对分组键或聚合的数量或类型进行任何更改。
流 - 流连接:例如,sdf1.join(sdf2, ...)
(即两个输入都是使用sparkSession.readStream
)生成的。不允许更改架构或等连接列。不允许更改连接类型(外部或内部)。连接条件的其他更改是不明确的。
任意有状态操作:例如,sdf.groupByKey(...).mapGroupsWithState(...)
或sdf.groupByKey(...).flatMapGroupsWithState(...)
。不允许对用户定义状态的模式和超时类型进行任何更改。允许在用户定义的状态映射函数中进行任何更改,但更改的语义效果取决于用户定义的逻辑。如果您确实希望支持状态模式更改,则可以使用支持模式迁移的编码/解码方案将复杂状态数据结构显式编码/解码为字节。例如,如果将状态保存为Avro编码的字节,则可以在查询重新启动之间更改Avro状态模式,因为二进制状态将始终成功恢复。
连续处理是Spark 2.3中引入的一种新的实验性流执行模式,可实现低(~1 ms)端到端延迟,并且至少具有一次容错保证。将其与默认的微批处理引擎相比较,该引擎可以实现一次性保证,但最多可实现~100ms的延迟。对于某些类型的查询(如下所述),您可以选择执行它们的模式而无需修改应用程序逻辑(即不更改DataFrame / Dataset操作)。
要在连续处理模式下运行支持的查询,您只需指定一个连续触发器,并将所需的检查点间隔作为参数。例如,
import org.apache.spark.sql.streaming.Trigger;
spark
.readStream
.format("kafka")
.option("kafka.bootstrap.servers", "host1:port1,host2:port2")
.option("subscribe", "topic1")
.load()
.selectExpr("CAST(key AS STRING)", "CAST(value AS STRING)")
.writeStream
.format("kafka")
.option("kafka.bootstrap.servers", "host1:port1,host2:port2")
.option("topic", "topic1")
.trigger(Trigger.Continuous("1 second")) // only change in query
.start();
检查点间隔为1秒意味着连续处理引擎将每秒记录查询的进度。生成的检查点采用与微批处理引擎兼容的格式,因此可以使用任何触发器重新启动任何查询。例如,以微批处理模式启动的支持查询可以以连续模式重新启动,反之亦然。请注意,无论何时切换到连续模式,您都将获得至少一次的容错保证。
从Spark 2.3开始,连续处理模式仅支持以下类型的查询。
select
,map
,flatMap
,mapPartitions
,等)和选择(where
,filter
等)。
current_timestamp()
并且current_date()
(使用时间的确定性计算具有挑战性)。numPartitions
和rowsPerSecond
。有关它们的更多详细信息,请参阅输入源和输出接收器部分。虽然控制台接收器非常适合测试,但是使用Kafka作为源和接收器可以最好地观察到端到端的低延迟处理,因为这允许引擎处理数据并使结果在输出主题中可用输入主题中可用的毫秒输入数据。