翻译自官方文档:
http://spark.apache.org/docs/2.4.0/structured-streaming-programming-guide.html
说明:
实现三种模式下的数据处理
目录
概述(Overview)
结构化流是基于Spark SQL引擎构建的可伸缩且容错的流处理引擎。 您可以像对静态数据进行批处理计算一样来表示流计算。当流数据持续到达时,Spark SQL引擎将负责逐步递增地运行它并更新最终结果。 您可以在Scala,Java,Python或R中使用Dataset / DataFrame API来表示流聚合、event-time窗口、stream-to-batch联接等。计算在同一个优化的Spark SQL引擎上执行。最后,系统通过检查点和写前日志来确保端到端精确的一次容错保证。简而言之,结构化流提供了快速、可伸缩、容错、端到端精确的一次流处理,而用户无需考虑流。
在内部,默认情况下,结构化流查询是使用微批处理引擎处理的,该引擎将数据流作为一系列小批处理作业进行处理,从而实现了低至100毫秒的端到端延迟以及一次精确的容错保证 。 但是,从Spark 2.3开始,我们引入了一种称为 “连续处理(Continuous Processing)” 的新低延迟处理模式,该模式可以在保证至少一次保证的情况下实现低至1毫秒的端到端延迟。 在不更改查询中的Dataset / DataFrame操作的情况下,您将能够根据应用程序需求选择模式。
在本指南中,我们将介绍编程模型和api。我们将主要使用默认的微批处理模型来解释这些概念,然后讨论连续处理模型。首先,让我们从一个结构化流查询的简单示例开始—一个流字数统计。
快速示例(Quick Example)
假设您要保持从数据服务器上监听TCP套接字的文本数据的运行字数统计。 让我们看看如何使用结构化流来表达这一点。 您可以在Scala / Java / Python / R中看到完整的代码。 如果下载了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._
接下来,创建一个流数据框架(DataFrame),该数据框架(DataFrame)表示从侦听localhost:9999的服务器接收的文本数据,并转换该数据框架以计算字数。
// 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()
这lines
DataFrame表示一个包含流文本数据的无界表。 该表包含一列名为“value”
的字符串,流文本数据中的每一行都成为表中的一行。请注意,由于我们只是在设置转换,还没有启动转换,所以目前还没有接收任何数据。 接下来,我们使用as[String]
将DataFrame转换为一个字符串数据集,这样我们就可以应用flatMap操作将每一行分割成多个单词。结果数据集words
包含所有单词。最后,我们定义了wordCounts
DataFrame,方法是根据数据集中的惟一值进行分组并计数。请注意,这是一个流DataFrame,它表示流的运行字数。
我们现在已经设置了对流数据的查询。剩下的就是实际开始接收数据并计算计数。为此,我们将它设置为在每次更新计数集(由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
然后,在另一个终端中,您可以使用以下命令启动示例
$ ./bin/run-example org.apache.spark.examples.sql.streaming.StructuredNetworkWordCount localhost 9999
这里我们通过启动spark shell来完成
complete mode
启动spark-shell并贴入代码:
> ./bin/spark-shell
然后,在运行netcat服务器的终端中键入的任何行都将被计数并在屏幕上每秒打印一次。它看起来像下面这样。
update mode
启动spark-shell并贴入代码:
> ./bin/spark-shell
然后,在运行netcat服务器的终端中键入的任何行都将被计数并在屏幕上每秒打印一次。它看起来像下面这样。
append mode
编程模型(Programming Model)
结构化流的关键思想是将实时数据流视为一个不断追加的表。 这导致了一个新的流处理模型,它非常类似于批处理模型。您可以将流计算表示为标准的类似批处理的查询,就像在静态表上一样,Spark将它作为无界输入表上的增量查询来运行。让我们更详细地了解这个模型。
基本概念(Basic Concepts)
将输入数据流视为输入表。到达流上的每个数据项就像一个新行被追加到输入表中。
对输入的查询将生成结果表。在每个触发间隔(例如,每1秒),新行被追加到输入表,最终更新结果表。无论何时更新结果表,我们都希望将更改后的结果行写入外部接收器。
“输出” 定义为写到外部存储器的内容。 可以在不同的模式下定义输出:
- Complete Mode. 整个更新后的结果表将被写入外部存储。由存储连接器决定如何处理整个表的写入。
- Append Mode. 自上次触发以来,仅追加到结果表中的新行将被写入外部存储。 这仅适用于结果表中现有行预计不会更改的查询。
- Update Mode. 仅自上次触发以来在结果表中已更新的行将被写入外部存储(自Spark 2.1.1起可用)。 请注意,这与完成模式的不同之处在于此模式仅输出自上次触发以来已更改的行。 如果查询不包含汇总,则相当于追加模式。
注意,每种模式都适用于特定类型的查询。稍后将对此进行详细讨论。
为了说明此模型的用法,让我们在上面的“快速示例”的上下文中了解该模型。第一个DataFrame lines
是输入表,最后一个DataFrame wordCounts
是结果表。请注意,用于生成wordCounts的流式DataFrame lines
上的查询与静态DataFrame上的查询完全相同。但是,当这个查询启动时,Spark将不断检查来自套接字连接的新数据。如果有新数据,Spark将运行一个增量查询(“incremental” query),该查询将以前的运行计数与新数据组合起来,以计算更新的计数,如下所示。
请注意,结构化流不会实现整个表。 它从流数据源读取最新的可用数据,以增量方式处理它以更新结果,然后丢弃源数据。它只保留更新结果所需的最小中间状态数据(例如,前面示例中的中间计数)。
此模型与许多其他流处理引擎明显不同。 许多流系统要求用户自己维护运行中的聚合,因此必须考虑容错和数据一致性(至少一次,最多一次或恰好一次)。 在此模型中,Spark负责在有新数据时更新结果表,从而使用户免于推理。 作为示例,让我们看看该模型如何处理基于事件时间的处理和延迟到达的数据。
处理事件时间和延迟数据(Handling Event-time and Late Data)
Event-time是嵌入到数据本身中的时间。对于许多应用程序,您可能希望对这个event-time进行操作。例如,如果您希望获得IoT设备每分钟生成的事件数,那么您可能希望使用数据生成时的时间(即数据中的event-time),而不是Spark接收它们的时间。
此event-time在此模型中非常自然地表示—设备中的每个事件都是表中的一行,而event-time是该行中的列值。 这样一来,基于窗口的聚合(例如每分钟的事件数)就可以成为event-time列上一种特殊的分组和聚合类型-每个时间窗口都是一个组,每行可以属于多个窗口/组。 因此,可以在静态数据集(例如从收集的设备事件日志中)以及在数据流上一致地定义这样的基于事件-时间-窗口的聚合查询,这使得用户的生活变得更加容易。
此外,此模型自然会根据事件时间处理比预期晚到达的数据。 由于Spark正在更新结果表,因此它具有完全控制权,可以在有较晚数据到达时更新旧聚合,并可以清除旧聚合以限制中间状态数据的大小。 从Spark 2.1开始,我们支持水印功能,该功能允许用户指定最新数据的阈值,并允许引擎相应地清除旧状态。 这些将在后面的“窗口操作”部分中详细介绍。
容错语义(Fault Tolerance Semantics)
提供端到端精确的一次性语义是结构化流设计背后的关键目标之一。为此,我们设计了结构化流源,接收器和执行引擎可靠地跟踪处理的确切进度,以便它可以通过重新启动和/或重新处理来处理任何类型的故障。 假定每个流源都有偏移量(类似于Kafka偏移量或Kinesis序列号),以跟踪流中的读取位置。 引擎使用检查点和预写日志来记录每个触发器中正在处理的数据的偏移范围。 流接收器被设计为是幂等的,用于处理后处理。 结合使用可重播的源和幂等的接收器,结构化流可以确保在发生任何故障时端到端的一次精确语义。
使用数据集和数据帧的API(API using Datasets and DataFrames)
从Spark 2.0开始,DataFrame和Dataset可以表示静态的有界数据以及流式无界数据。 与静态Datasets/DataFrames类似,您可以使用公共入口点SparkSession(Scala / Java / Python / R docs)从流数据源(streaming sources)创建流Datasets/DataFrames,并对它们应用与静态数据帧/数据集相同的操作。 如果您不熟悉Datasets/DataFrames,强烈建议您使用《数据框/数据集编程指南》来熟悉它们。
创建流式数据流和流式数据集(Creating streaming DataFrames and streaming Datasets)
可以通过SparkSession.readStream()
返回的DataStreamReader接口(Scala / Java / Python文档)创建流式DataFrame。 在R中,使用read.stream()
方法。 与用于创建静态DataFrame的读取接口类似,您可以指定源的详细信息——数据格式、架构、选项等。
输入源(Input Sources)
一些内置的源:
- File Source: 将写入目录中的文件作为数据流读取。支持的文件格式有文本、csv、json、orc、parquet。请参阅DataStreamReader接口的文档,以获得最新的列表和每种文件格式支持的选项。注意,文件必须自动地放在给定的目录中,在大多数文件系统中,可以通过文件移动操作来实现。
- Kafka Source: 从Kafka读取数据。它与Kafka代理版本0.10.0或更高兼容。有关更多细节,请参阅Kafka集成指南。
- Socket source (for testing): 从套接字连接读取UTF8文本数据。 监听服务器套接字位于驱动程序处。 请注意,这仅应用于测试,因为它不能提供端到端的容错保证。
- Rate source (for testing): 以每秒指定的行数生成数据,每个输出行包含一个时间戳和值。其中timestamp是包含消息分派(message dispatch)时间的时间戳类型,而value是包含消息计数的Long类型,从第一行的0开始。此源代码用于测试和基准测试。
有些源不是容错的,因为它们不保证在发生故障后可以使用检查点偏移量重拨(replay)数据。请参阅前面有关容错语义的部分。以下是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)中的相关方法。 例如,有关"parquet"格式选项的信息,请参见 DataStreamReader.parquet() 。此外,有些会话配置会影响某些文件格式。 有关更多详细信息,请参见《 SQL编程指南》。 例如,对于“parquet”,请参阅parquet配置部分。 | 是 | 支持全局路径,但不支持多个逗号分隔的路径/全局路径 |
Socket Source | host: 必须指定要连接的主机port: 必须指定要连接的端口 | 否 | |
Rate Source | rowsPerSecond (e.g. 100, default: 1): 每秒应该生成的行数rampUpTime (e.g. 5s, default: 0s): 在生成速度变为rowsPerSecond之前需要多长时间, 使用比秒更细的粒度将被截断为整数秒。numPartitions (e.g. 10, default: Spark's default parallelism): 生成行的分区号源将尽最大努力达到rowsPerSecond,但是查询可能受资源限制,并且可以对numPartitions进行调整以帮助达到所需的速度。 | 是 | |
Kafka Source | 请参阅《 Kafka集成指南》 | 是 |
[待续…]