概述
Structured Stream是基于Spark SQL引擎构建的可伸缩且容错的流处理引擎。使得用户可以像使用Spark SQL操作静态批处理计算一样使用Structured Stream的SQL操作流计算。当流数据继续到达时,Spark SQL引擎将负责递增地,连续地运行它并更新最终结果。使用 Dataset/DataFrame API 实现对实时数据的聚合、event-time 窗口计算以及流到批处理的join操作。最后,系统通过检查点
和预写日志
来确保端到端(end to end)的一次容错保证。简而言之,结构化流提供了快速,可伸缩,容错,端到端的精确一次流处理,而用户不必推理流。在内部,默认情况下,结构化流查询是使用微批量处理引擎
处理的,该引擎将数据流作为一系列小批量作业进行处理,从而实现了低至100毫秒的端到端延迟以及一次精确的容错保证。
快速入门
- 依赖
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-sql_2.11</artifactId>
<version>2.4.5</version>
</dependency>
- 编写Driver
val spark = SparkSession
.builder
.appName("StructuredNetworkWordCount")
.master("local[*]")
.getOrCreate()
spark.sparkContext.setLogLevel("ERROR")
import spark.implicits._
//1.创建输入流(表) 动态表
val lines:DataFrame = spark.readStream
.format("socket")
.option("host", "CentOS")
.option("port", 9999)
.load()
//2.对动态表执行 SQL 查询
val wordCounts:DataFrame = lines.as[String].flatMap(_.split(" "))
.groupBy("value").count()
//3.产生StreamQuery对象
val query:StreamingQuery = wordCounts.writeStream
.outputMode(OutputMode.Update())
.format("console")
.start()
query.awaitTermination()
Programming Model
Structured Stream中的关键思想是将实时数据流视为被连续追加的表。这导致了一个新的流处理模型,该模型与批处理模型非常相似。使用者将像在静态表上一样将流计算表示为类似于批处理的标准查询,Spark在无界输入表上将其作为增量查询
运行。
Basic Concepts
将输入数据流视为“Input Table”。流上到达的每个数据项都像是将新Row附加到输入表中。
输入查询将生成“Result Table”。在每个触发间隔(例如,每1秒钟),新行将附加到输入表中,并最终更新“Result Table”(输入查询是一种持续查询)。无论何时更新“Result Table”,我们都希望将更改后的结果行写入外部接收器。
“Output”定义为写到外部存储器的内容。可以在不同的模式下定义输出:
Complete Mode - 整个更新的Result Table将被写入外部存储器。由存储连接器决定如何处理整个表的写入。
Update Mode - 仅自上次触发以来在Result Table中已更新的行将被写入外部存储(自Spark 2.1.1起可用)。请注意,这与Complete Mode的不同之处在于此模式仅输出自上次触发以来已更改的行。
以上两种模式一般用于存在聚合(状态)算子的计算中。因为只有在Complete Mode或者Update Mode系统才会存储计算的中间结果。如果使用Update模式中不存在聚合操作,该模式等价于Append模式。
- Append Mode - 自上次触发以来,仅将追加在Result Table中的新行写入到外部存储中。这仅适用于预期结果表中现有行不会更改的查询(不会使用历史数据,做状态更新)。
为了说明此模型的用法,让我们在上面的快速入门”
的上下文中了解该模型。第一行DataFrame是输入表,最后的wordCounts DataFrame是结果表。请注意,在流lines’DataFrame上生成wordCounts的查询与静态DataFrame完全相同。但是,启动此查询时,Spark将连续检查Socket连接中是否有新数据。如果有新数据,Spark将运行一个“增量”查询,该查询将先前的运行计数与新数据结合起来以计算更新的计数,如下所示。
请注意,Structured Stream不会存储Input Table。它从流数据源读取最新的可用数据,对其进行增量处理以更新结果,然后丢弃该源数据。它仅保留更新结果所需的最小中间状态数据(例如,前面示例中的中间计数)。
此模型与许多其他流处理引擎明显不同。许多流系统要求用户自己维护运行中的聚合,因此必须考虑容错和数据一致性(至少一次,最多一次或精确一次)。在此模型中,Spark负责在有新数据时更新结果表,从而使用户免于推理。系统通过检查点
和预写日志
来确保端到端(end to end)的精准一次容错保证。
- 最多一次:如果计算过程中,存在失败,自动忽略,一般只考虑性能,不考虑安全-(丢数据)
- 至少一次:在故障时候,系统因为重试或者重新计算,导致记录重复参与计算-(不丢数据,重复更新,计算不准确)
- 精确一次:在故障时候,系统因为重试或者重新计算,不会导致数据丢失或者重复计算-(对数据精准性要求比较高的实时计算场景)
容错语义
提供端到端的精确一次语义是结构化流设计背后的主要目标之一。为此,我们设计了结构化流源
,接收器
和执行引擎
,以可靠地跟踪处理的确切进度,以便它可以通过重新启动和/或重新处理来处理任何类型的故障。假定每个流源
都有偏移量(类似于Kafka偏移量或Kinesis序列号)以跟踪流中的读取位置。引擎使用检查点
和预写日志
来记录每个触发器中正在处理的数据的偏移范围。流接收器被设计为是幂等
的,用于处理后处理。结合使用可重播的源
和幂等的接收器
,结构化流可以确保在发生任何故障时端到端的一次精确语义。
处理事件时间和延迟数据
事件时间是嵌入数据本身的时间。对于许多应用程序,您可能希望在此事件时间进行操作。例如,如果要获取每分钟由IoT设备生成的事件数,则可能要使用生成数据的时间(即数据中的事件时间),而不是Spark收到的时间。事件时间在此模型中非常自然地表示-设备中的每个事件是表中的一行,而事件时间是该行中的列值。这样一来,基于窗口的聚合(例如,每分钟的事件数)就可以成为事件时间列上一种特殊的分组和聚合类型-每个时间窗口都是一个组,每行可以属于多个窗口/组。因此,可以在静态数据集(例如从收集的设备事件日志中)以及在数据流上一致地定义这样的基于事件-时间-窗口的聚合查询。
此外,此模型自然会根据事件时间处理比预期晚到达的数据。由于Spark正在更新结果表,因此它具有完全控制权,可以在有较晚数据时更新旧聚合,并可以清除旧聚合以限制中间状态数据的大小。从Spark 2.1开始,我们支持水印功能,该功能允许用户指定最新数据的阈值,并允许引擎相应地清除旧状态。这些将在后面的“窗口操作”部分中详细介绍。
Dataset和DataFrame API
从Spark 2.0开始,DataFrame和Dataset可以表示静态有界数据以及流式计算的无界数据。同有界数据类似,用户可以使用SparkSession从流源创建DataFrame/Dataset.并且可以使用类似静态Dataframe/Dataset相同的操作。
Input Source
File source
读取来自文件目录下的文件,例如:文本格式、csv格式、json、orc、parquet等文本格式。
//0.创建spark对象
val spark = SparkSession
.builder
.appName("StructuredNetworkWordCount")
.master("local[*]")
.getOrCreate()
spark.sparkContext.setLogLevel("FATAL")
import spark