Structured Streaming是构建在Spark SQL引擎上的流式数据处理引擎,使用Scala编写,具有容错功能。你可以像在使用静态RDD数据一样来编写你的流式计算过程。当流数据连续不断的产生时,Spark SQL将会增量的,持续不断的处理这些数据并将结果更新到结果集中。你可以使用DataSet/DataFrame API来展现数据流的aggregations, event-time windows,stream-to-batch joins等操作,支持的语言有Scala,Java和Python,计算过程运行在经过优化的Spark SQL引擎上。最终,Structured Streaming系统通过checkpoints和write ahead logs方式保证端到端数据的准确一次性以及容错性。简而言之,Structured Streaming提供了快速的,Scalable,容错的,端到端一次性的流数据处理,并且不需要用户关注数据流。
Spark2.0是Structured Streaming的最早发行版本,这个API 2 .0仍然处于实验性阶段,但是目前2.2版本已经不是测试阶段了。在本教程中,我们将了解它的编程模型以及API。首先,让我们从简单的streaming word count程序样例开始。
首先,假如你想监听一个数据服务器上的TCP Socket来获取源源不断的数据流,同时你想要实时的计算单词的数量。让我们看看你如何使用Structured Streaming完成这项工作。接下来将使用Scala语言来展现(官网包含Java和Python例子),如果你下载了Spark,你可以直接运行这个例子。无论如何,让我们一步步完成这个例子并了解它是如何工作的。首先,我们需要导入必要的classes,并且创建一个本地运行的SparkSession,它是链接到Spark的程序入口。
import org.apache.spark.sql.functions._
import org.apache.spark.sql.SparkSession
val spark =SparkSession
.builder
.appName("StructuredNetworkWordCount")
.config(new SparkConf().setMaster("local[2]"))
.getOrCreate()
importspark.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()
这个Lines DataFrame表示了一个包含了流式数据的无边界表。此表包含一列的字符串,名字为value,数据流中的每一行变成了表中的一个Row。注意,现在还没有接收任何的数据,仅仅是定义了转换操作的流程,并没有启动转换操作。接下来我们使用lines.as[String]将DataFrame转换成String类型的Dataset(注:Spark2.0中DataFrame和DataSet的API整合到了一起,DataFrame作为Dataset的一个特例存在,DataFrame=Dataset[Row]),因此我们可以对Dataset应用flatMap操作来分割一行数据,转化为单词的集合,结果集words Dataset包含了所有的单词。最终,我们通过groupingby(vlaue)将单词进行分组,然后使用count来计算它们。注意,这是一个流式的DataFrame,表示了计算一个源源不断的文本数据流中单词的数量。
我们已经在数据流上设定好了查询操作,接下来剩下的就是开始接收数据并且count这些数据。我们将会把全量的结果打印到控制台,(结果集的输出操作有三种模式,complete mode,append mode和update mode,稍后会详细介绍,指定mode使用outputMode(“complete”)),每次结果集更新,都会把所有的结果都打印一次。使用start()来启动流式数据计算流程。
val query=wordCounts.writeStream
.outputMode("complete")
.format("console")
.start()
query.awaitTermination()
当这段代码运行的时候,流式计算过程将会在后台启动。这个query对象是流查询的句柄,我们需要使用query.awaitTermination()来阻止程序在本查询依然运行的时候提前结束。
为了运行本例子,你可以编译你自己的Spark程序,或者如果你下载了Spark,可以简单的运行本例子。首先,你需要运行NetCat(大多数的Unix-like系统中都存在)作为数据服务器。使用如下命令:
$ nc -lk 9999
在打开这条命令的窗口,你可以源源不断地输入数据,作为数据流的源数据。
在另外一个terminal,你可以使用如下命令启动程序:
$ ./bin/run-example org.apache.spark.examples.sql.streaming.StructuredNetworkWordCount localhost 9999
运行的结果将会如下:
# 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|
+------+-----+
...
完整可运行程序如下:
package com.dyl.spark
import org.apache.spark
import org.apache.spark.SparkConf
import org.apache.spark.sql.SparkSession
/**
* Created by dongyunlong on 2017/7/13.
*/
object StructuredStreaming {
def main(args: Array[String]) {
val spark = SparkSession
.builder()
.config(new SparkConf().setMaster("local[2]"))
.appName(getClass.getName)
.getOrCreate()
import spark.implicits._
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()
// Start running the query that prints the running counts to the console
val query = wordCounts.writeStream
.outputMode("complete")
.format("console")
.start()
query.awaitTermination()
}
}
Structured Streaming的核心是将流式的数据看成一张不断增加的数据库表,这种流式的数据处理模型类似于数据块处理模型,你可以把静态数据库表的一些查询操作应用在流式计算中,Spark运行这些标准的SQL查询,从不断增加的无边界表中获取数据。让我们接下来详细的了解这个模型。
想象一下,我们把不断输入的流式数据加载为内存中一张没有边界的数据库表,每一条新来的数据都会作为一行数据新增到这张表中,如下所示:
每一条查询的操作都会产生一个结果集-Result Table。每一个触发间隔(比如说1秒),当新的数据新增到表中,都会最终更新Result Table。无论何时结果集发生了更新,我们都能将变化的结果写入一个外部的存储系统。
图中的OutPut表示将要存储到外部介质。OutPut可以定义不同的存储方式,有如下3种:
1:Complete Mode – 整个更新的结果集都会写入外部存储。整张表的写入操作将由外部存储系统的连接器Connector。
2:Append Mode – 当时间间隔触发时,只有在Result Table中新增加的数据行会被写入外部存储。这种方式只适用于结果集中已经存在的内容不希望发生改变的情况下,如果已经存在的数据会被更新,不适合适用此种方式。
3:Update Mode – 当时间间隔触发时,只有在Result Table中被更新的数据才会被写入外部存储系统(在Spark2.0中暂时尚未可用)。注意,和Complete Mode方式的不同之处是不更新的结果集不会写入外部存储。如果查询不包含aggregations, 将会和append模式相同
注意,每种模式适用于特定类型的查询,稍后将会详细描述。
为了说明本模型的使用方式,让我们理解一下上文中描述的Example。第一个lines DataFrame对象是一张数据输入的Input Table,最后的WordCounts DataFrame是一个结果集Result Table。在lines DataFrame数据流之上的查询产生了wordCounts的表示方式和在静态的Static DataFrame上的使用方式相同。然而,Spark会监控socket连接,获取新的持续不断产生的数据。当新的数据产生时,Spark将会在新数据上运行一个增量的counts查询,并且整合新的counts和之前已经计算出来的counts,获取更新后的counts,如下所示:
这个模型和其他的流式数据处理引擎是非常不一样的,许多系统要求用户自己维护运行的聚合等,从而需要熟悉容错机制和数据保障机制(at-least-once, or at-most-once, or exactly-once)。在本模型中,Spark负责更新结果集ResultTable,从而解放了用户。从上述例子中,我们仍然需要了解本模型如何提交基于event-time的处理过程以及稍后到达的数据。
Event-time是内嵌在数据本身里的时间。对于许多应用程序,您可能希望在此Event-time上进行操作。例如,如果要每分钟获取IoT设备生成的事件数,那么您可能希望使用数据生成的时间(即数据中的Event-time),而不是Spark接收到它们的时间。这个Event-time在这个模型中非常自然地表现出来 - 来自设备的每个事件都是表中的一行,Event-time是行中的列值。这允许window-based的聚合(例如,每分钟的事件数)仅仅是Event-time列上特殊类型的分组和聚合 - 每个时间窗口是一个group,每行可以属于多个windows/groups。因此,event-time-window-based查询可以在静态DataSet(例如来自收集设备的事件日志)以及Data Stream上进行一致地定义
此外,基于Event-time,该模型自然地处理晚于预期的数据。由于Spark更新结果表Result table,当延迟晚到的数据到来时,它能够完全控制更新旧的聚合集,并且能够清理旧的聚合集来限制中间状态数据的size大小。由于Spark 2.1,我们支持watermarking,允许用户指定延迟数据的阈值,并允许引擎相应地清理旧状态。稍后将在“Window Operations”部分中更详细地说明这些。
提供端到端的一次性语义是StructuedStreaming设计背后的主要目标之一。为了实现这一点,我们设计了Structured Streamingsources,the sinks 和 the execution engine,以便可靠地跟踪处理的确切进度,以便它可以通过restart和/或reprocessing来处理任何类型的故障。假设每个Streaming sources具有偏移量offsets(类似于Kafka偏移量或Kinesis序列号)以跟踪流中的读取位置。Engine使用checkpointing和write ahead logs记录每个触发器中正在处理的数据的偏移范围。streaming sinks设计为处理reprocessing。Replayable sources和idempotent sinks一起使用,StructuedStreaming可以在任何故障下确保端到端完全一次的语义。
注:idempotent 幂等 : 在编程中.一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。
从Spark2.0以来,DataFrames 和 Datasets可以表示静态的,有界的数据,也可以表示流式的,无界的数据。和静态的相似,您可以使用通用入口点SparkSession(Scala / Java / Python / R docs)从流式source创建流式DataFrames/Datasets,并且可以应用和静态数据相同的操作。如果不熟悉DataFrames/Datasets,可以参考官网指导。
Streaming DataFrames可以通过SparkSession.readStream()返回的DataStreamReader接口(Scala / Java / Python文档)创建。在R中,使用read.stream()方法。与创建静态DataFrame的接口类似,您可以指定source的细节,比如:data format,schema, options等。
在Spark 2.0中,有几个内置的source。
(1) File source :读取目录下的文件作为流式的数据。支持的格式有text, csv, json, parquet。 注意,文件在目录中需要保持原子性,一般大多数的文件系统中,可以通过move操作实现。
(2) Kafka source :从kafka poll数据。它与Kafka broker版本0.10.0或更高版本兼容。详细内容,查看官网kafka整合教程。
(3) Socket source (for testing) :从套接字连接读取UTF8文本数据。仅用于测试,不提供容错性保证。
并不是所有source都保证容错性语义,有些source自己不支持relay,spark一样无法通过checkpointedoffsets 重播数据。
Source |
Options |
Fault- tolerant |
Notes |
|
File source |
path:路径到输入目录,并且与所有文件格式通用。 maxFilesPerTrigger:每个时间间隔触发器中要考虑的最大新文件数(默认值:无最大值) latestFirst:是否首先处理最新的新文件,当有大量的文件积压(default:false)时很有用。 fileNameOnly:是否仅根据文件名而不是完整路径检查新文件(默认值:false)。将此设置为“true”,以下文件将被视为相同的文件,因为它们的文件名“dataset.txt”是相同的: · "file:///dataset.txt" · "s3://a/dataset.txt" · "s3n://a/b/dataset.txt" · "s3a://a/b/c/dataset.txt" |
YES |
支持glob路径,但不支持多个逗号分隔的poths/ globs。 |
|
Socket Source |
host:主机连接,必须指定 port:要连接的端口,必须指定 |
No |
|
|
Kafka Source |
详细查看kafka整合教程 |
|
|
如下是一些例子:
1. val spark: SparkSession = ...
2. // Read text from socket
3. val socketDF = spark
4. .readStream
5. .format("socket")
6. .option("host", "localhost")
7. .option("port", 9999)
8. .load()
9. socketDF.isStreaming // Returns True for DataFrames that have streaming sources
10. socketDF.printSchema
11. // Read all the csv files written atomically in a directory
12. val userSchema = new StructType().add("name", "string").add("age", "integer")
13. val csvDF = spark
14. .readStream
15. .option("sep", ";")
16. .schema(userSchema) // Specify schema of the csv files
17. .csv("/path/to/directory") // Equivalent to format("csv").load("/path/to/directory")
这些示例生成无类型的流DataFrames,这意味着DataFrame的架构在编译时未被检查,只在运行时在查询提交时进行检查。一些操作,如map,flatMap等,需要在编译时知道该类型。要做到这一点,您可以使用与静态DataFrame相同的方法将这些未类型化的流DataFrames转换为类型化的流式Datasets。有关详细信息,请参阅SQL编程指南。此外,有关支持的流媒体源的更多详细信息将在文档后面讨论。
默认情况下,基于文件的filesource的结构化流式传输(Structured Streaming)需要您指定模式,而不是依靠Spark自动推断。这种限制确保了即使在出现故障的情况下,一致的模式将被用于流式查询。对于特殊用例,您可以通过将spark.sql.streaming.schemaInference设置为true来重新启用模式推断。
当存在名为/ key = value /的子目录时,会发生分区发现,并且列表将自动递归到这些目录中。如果这些columns显示在用户提供的schema中,根据正在读取的文件的路径,spark将会填充它们。构成分区schema的目录必须在查询启动时存在,并且必须保持静态。例如,当/data/year=2015/存在时,可以添加/data/year=2016/。但是你不能改变分区column,例如无法创建目录/data/date=2016-04-17/。
您可以对流式DataFrames/ Datasets应用各种操作,从无类型,类似SQL的操作(例如select,where,groupBy)到类型类似RDD的操作(例如,map,filter,flatMap)。有关详细信息,请参阅SQL编程指南。我们来看一下可以使用的几个示例操作。
大多数DataFrame/Dataset的操作在streaming中都能够支持。稍后会指出哪些操作是不支持的。
1. case class DeviceData(device: String, deviceType: String, signal: Double, time: DateTime)
2.
3. val df: DataFrame = ... // streaming DataFrame with IOT device data with schema { device: string, deviceType: string, signal: double, time: string }
4. val ds: Dataset[DeviceData] = df.as[DeviceData] // streaming Dataset with IOT device data
5.
6. // Select the devices which have signal more than 10
7. df.select("device").where("signal > 10") // using untyped APIs
8. ds.filter(_.signal > 10).map(_.device) // using typed APIs
9.
10. // Running count of the number of updates for each device type
11. df.groupBy("deviceType").count() // using untyped API
12.
13. // Running average signal for each device type
14. import org.apache.spark.sql.expressions.scalalang.typed
15. ds.groupByKey(_.deviceType).agg(typed.avg(_.signal)) // using typed API
滑动event-time时间窗口的聚合在StructuredStreaming上很简单,并且和分组聚合非常相似。在分组聚合中,为用户指定的分组列中的每个唯一值维护聚合值(例如计数)。在基于窗口的聚合的情况下,为每一个event-time窗口维护聚合值。如下:
想象一下,quickexample中的示例被修改,现在stream中的每行包含了生成的时间。我们不想运行word count,而是要在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收到的一个word。这个词应该增加对应于两个窗口的计数,分别为12:00 - 12:10和12:05 - 12:15。所以计数counts将会被group key(ie:the word)和window(根据event-time计算)索引。将会如下所示:
由于此窗口类似于分组,因此在代码中,可以使用groupBy()和window()操作来表示窗口聚合。
1. import spark.implicits._
2.
3. val words = ... // streaming DataFrame of schema { timestamp: Timestamp, word: String }
4.
5. // Group the data by window and word and compute the count of each group
6. val windowedCounts = words.groupBy(
7. window($"timestamp", "10 minutes", "5 minutes"),
8. $"word"
9. ).count()
现在考虑如果一个事件迟到应用程序会发生什么。例如,假设12:04(即event-time)生成的一个word可以在12:11被应用程序接收。应用程序应该使用时间12:04而不是12:11更新窗口的较旧计数,即12:00 - 12:10。这在我们基于窗口的分组中很自然有可能发生- Structured Streaming可以长时间维持部分聚合的中间状态,以便延迟的数据可以正确地更新旧窗口的聚合,如下所示:
但是,为了长久的运行这个查询,必须限制内存中间状态的数量。这就意味着,系统需要知道什么时候能够从内存中删除旧的聚合,此时默认应用接受延迟的数据之后不再进行聚合。Spark2.1中引入了watermarking,它能够让engine自动跟踪当前的数据中的event time并据此删除旧的状态表。你可以通过指定event-time列和时间阀值来指定一个查询的watermark,阀值以内的数据才会被处理。对于一个特定的开始于时间T的window窗口,引擎engine将会保持状态并且允许延迟的数据更新状态直到(max event time seen by the engine - late threshold > T)。换句话说,阀值内的数据将被聚合,阀值外的数据将会被丢弃。
1. import spark.implicits._
2.
3. val words = ... // streaming DataFrame of schema { timestamp: Timestamp, word: String }
4.
5. // Group the data by window and word and compute the count of each group
6. val windowedCounts = words
7. .withWatermark("timestamp", "10 minutes")
8. .groupBy(
9. window($"timestamp", "10 minutes", "5 minutes"),
10. $"word")
11. .count()
本例中,watermark的指定列为“timestamp”,并且指定了“10minute”作为阀值。如果这个查询运行在update的输出模式,引擎engine会持续更新window的counts到结果集中,直到窗口超过watermark的阀值,本例中,则是如果timestamp列的时间晚于当前时间10minute。
如上所示,蓝色虚线表示最大event-time,每次数据间隔触发开始时,watermark被设置为max eventtime - '10 mins',如图红色实线所示。例如,当引擎engine观测到到数据(12:14,dog),对于下个触发器,watermark被设置为12:04。这个watermark允许引擎保持十分钟内的中间状态并且允许延迟数据更新聚合集。例如(12:09,cat)的数据未按照顺序延迟到达,它将落在12:05 – 12:15 和 12:10 – 12:20 。因为它依然大于12:04 ,所以引擎依然保持着中间结果集,能够正确的更新对应窗口的结果集。但是当watermark更新到12:11,中间结果集12:00-12:10的数据将会被清理掉,此时所有的数据(如(12:04,donkey))都会被认为“too late”从而被忽略。注意,每次触发器之后,更新的counts(如 purple rows)都会被写入sink作为输出的触发器,由更新模式控制。
某些接收器(例如文件)可能不支持更新模式所需的细粒度更新。要与他们一起工作,我们还支持附加模式,只有最后的计数被写入sink。这如下所示。
请注意,在非流数据集上使用watermark是无效的。由于watermark不应以任何方式影响任何批次查询,我们将直接忽略它。
与之前类似,但是知道12:20时,12:00-12:10的结果才输出,此时watermark为12:11,超过12:10,在此之前,其被维护成中间状态等待延迟的数据。当输出之后,其后的延迟的数据将被丢弃。
Watermark清理聚合状态的条件:重要的是要注意,为了清理聚合查询中的状态(从Spark2.1.1开始,将来会有变化),watermark需要满足以下条件。
(1) 输出模式必须是Append或者Update。Complete模式要求所有聚合的数据都被保留,因此不能使用watermark清理掉中间结果。
(2) 聚合必须包括event-time列或者基于event-time列的window窗口
(3) WithWatermark和聚合的时间列必须相同。例如,df.withWatermark("time", "1min").groupBy("time2").count()在append模式下是无效的。
(4) withWatermark必须在聚合之前被调用。例如:df.groupBy("time").count().withWatermark("time","1 min")是无效的。
流式DataFrames可以与静态DataFrames一起创建新的流数据帧。这里有几个例子。
1. val staticDf = spark.read. ...
2. val streamingDf = spark.readStream. ...
3.
4. streamingDf.join(staticDf, "type") // inner equi-join with a static DF
5. streamingDf.join(staticDf, "type", "right_join") // right outer join with a static DF
您可以使用事件中的唯一标识符对数据流中的记录进行重复数据删除。这与使用唯一标识符列的静态重复数据删除完全相同。该查询将存储先前记录所需的数据量,以便可以过滤重复的记录。与聚合类似,您可以使用带有或不带有watermark的重复数据删除功能。
(1) 带有watermark: 如果重复记录可能到达的时间有上限,则可以在event-time列上定义watermark,并使用guid和event-time列进行重复数据删除。该查询将使用watermark从以前的记录中删除旧的状态数据,这些记录不会再受到任何重复。这界定了查询必须维护的状态量。
(2) 不带watermark: 由于重复记录可能到达时没有界限,所以查询将来自所有过去记录的数据存储为状态。
(3)val streamingDf = spark.readStream. ... // columns: guid, eventTime, ...
(4)
(5)// Without watermark using guid column
(6)streamingDf.dropDuplicates("guid")
(7)
(8)// With watermark using guid and eventTime columns
(9)streamingDf
(10) .withWatermark("eventTime", "10 seconds")
(11) .dropDuplicates("guid", "eventTime")
许多用例需要比聚合更高级的状态操作。例如,在许多用例中,您必须跟踪事件数据流中的会话。对于进行此类会话,您将必须将任意类型的数据保存为状态,并在每个触发器中使用数据流事件对状态执行任意操作。由于Spark 2.2,这可以通过操作mapGroupsWithState和更强大的操作flatMapGroupsWithState来完成。这两个操作都允许您在分组的数据集上应用用户定义的代码来更新用户定义的状态。
有些DataFrame/Dataset的操作在Streaming中并不支持,如下所示:
(1) 多个流的聚合不支持(例如Streaming DF上的聚合链)
(2) 不支持limit并take前N行
(3) Distinct操作不支持
(4) 排序操作仅仅在聚合之后并且输出模式是complete时支持
(5) Outer join在streaming 和静态Datasets之间有条件的支持
1) Full outer join不支持
2) Left outer join 当stream在右侧时不支持
3) Right outer join 当stream在左侧时不支持
(6) 两个stream之间的任何join都不支持
除此之外,还有一些方法在Streaming中无法工作。它们是立即运行查询并返回结果的操作,这在流数据集上没有意义。相反,这些功能可以通过显式启动流式查询来完成(参见下一节)。
(1) count() : 无法从流数据集返回单个计数。可以使用ds.groupBy().count()返回一个包含运行计数的streamingDateset。
(2) foreach() – 使用 ds.writeStream.foreach(...)(see next section).
(3) show() – 使用 theconsole sink (see next section).
如果你使用了这些操作,你将会受到一个AnalysisException,例如“operationXYZ is not supported with streaming DataFrames/Datasets”。虽然其中一些可能在未来版本的Spark中得到支持,但还有其他一些基本上很难在流数据上高效地实现。例如,不支持对输入流进行排序,因为它需要跟踪流中接收到的所有数据。因此,从根本上难以有效执行。
使用通过Dataset.writeStream()返回的DataStreamWriter开始流式计算。可以指定如下一些配置:
(1) sink的详细信息:Dataformat,location等。
(2) output mode:指定写入sink的内容。
(3) query name:(可选)指定用于标识的查询的唯一名称。
(4) Trigger interval: 可选,指定触发间隔。如果未指定,系统将在上一次处理完成后立即检查新数据的可用性。如果由于先前的处理尚未完成而导致触发时间错误,则系统将尝试在下一个触发点触发,而不是在处理完成后立即触发。
(5) Checkpoint location:对于可以保证端到端容错的一些输出接收器,请指定系统将写入所有检查点信息的位置。这应该是与HDFS兼容的容错文件系统中的目录。检查点的语义将在下一节中进行更详细的讨论。
(1) Append mode (default) -这是默认模式,只有从上次触发后添加到结果表中的新行将被输出到接收器。只有那些添加到结果表中的行从不会改变的查询才支持这一点。因此,该模式保证每行只能输出一次(假定容错sink)。例如,只有select,where,map,flatMap,filter,join等的查询将支持Append模式。
(2) Complete mode - 每个触发后,整个结果表将被输出到sink。聚合查询支持这一点。
(3) Update mode - (自Spark 2.1.1起可用)只有结果表中自上次触发后更新的行才会被输出到接收器。
不同的查询支持不同的输出模式,详细见OutputModes
有几种类型的内置输出sink。如下所示:
(1) File sink - 将输出存储到目录。支持写入分区表。按时间划分可能是有用的。
1. writeStream
2. .format("parquet") // can be "orc", "json", "csv", etc.
3. .option("path", "path/to/destination/dir")
4. .start()
(2)Foreach sink - 对输出中的记录运行任意计算。有关详细信息,请参阅本节后面部分。
1. writeStream
2. .foreach(...)
3. .start()
(3)Console sink (for debugging)- 每次触发时,将输出打印到控制台/ stdout。支持“append”和“complete”输出模式。这应该用于低数据量的调试目的,因为在每次触发后,整个输出被收集并存储在驱动程序的存储器中。
1. writeStream
2. .format("console")
3. .start()
(4) Memory sink (for debugging) - 输出作为内存表存储在内存中。都支持“append”和“complete”输出模式。由于整个输出被收集并存储在驱动程序的存储器中,因此应用于低数据量的调试目的。因此,请谨慎使用。
1. writeStream
2. .format("memory")
3. .queryName("tableName")
4. .start()
请注意,您必须调用start()来实际启动查询的执行。这将返回一个StreamingQuery对象,它是持续运行的执行的句柄。您可以使用此对象来管理查询,我们将在下一小节中讨论。现在,让我们通过几个例子了解所有这些。
1. // ========== DF with no aggregations ==========
2. val noAggDF = deviceDataDf.select("device").where("signal > 10")
3.
4. // Print new data to console
5. noAggDF
6. .writeStream
7. .format("console")
8. .start()
9.
10. // Write new data to Parquet files
11. noAggDF
12. .writeStream
13. .format("parquet")
14. .option("checkpointLocation", "path/to/checkpoint/dir")
15. .option("path", "path/to/destination/dir")
16. .start()
17.
18. // ========== DF with aggregation ==========
19. val aggDF = df.groupBy("device").count()
20.
21. // Print updated aggregations to console
22. aggDF
23. .writeStream
24. .outputMode("complete")
25. .format("console")
26. .start()
27.
28. // Have all the aggregates in an in-memory table
29. aggDF
30. .writeStream
31. .queryName("aggregates") // this query name will be the table name
32. .outputMode("complete")
33. .format("memory")
34. .start()
35.
36. spark.sql("select * from aggregates").show() // interactively query in-memory table
foreach操作允许在输出数据上计算任意操作。从Spark 2.1起,这只适用于Scala和Java。要使用它,您将必须实现接口ForeachWriter,它有个方法,在每个触发器之后,无论任何时候有一行数据作为输出产生,都会被调用。注意一下重要点:
(1) writer必须被序列化,因为它将会被序列化,发送到executors端执行
(2) open,process和close在execu端都会被调用
(3) 只有当open方法被调用时,writer必须执行所有的初始化(例如打开连接,启动事务等)。注意,如果object创建的时候有任何的初始化发生,这些初始化都会发生在driver端(因为object在driver端创建),这可能不是你需要的。
(4) version和partition是open的两个参数,唯一表示一组需要推出的行。version是每个触发器增加的单调递增的id。partition是表示输出分区的id,因为输出是分布式的,并且将在多个执行器上处理。
(5) open可以使用version和partition来选择是否需要写入行的序列。因此,它可以返回true(继续写入)或false(不需要写入)。如果返回false,则不会在任何行上调用进程。例如,在部分故障之后,失败的触发器的一些输出分区可能已经被提交到数据库。基于存储在数据库中的元数据,writer可以识别已经被提交的分区,并因此返回false以跳过再次提交它们。
(6) 每当open被调用时,close也将被调用(除非JVM由于某些错误而退出)。即使打开返回false也是如此。如果在处理和写入数据时有任何错误,则关闭将依然被调用。您有责任清理在open中创建的状态(例如,连接,事务等),以免资源泄漏。