上一节我们通过简单的一个案列认识了SparkStreaming,接下来,我们将超越简单的示例,详细介绍 Spark Streaming 的基本知识。
1、链接
与 Spark 类似,Spark Streaming 可以通过 Maven Central 获得。要编写自己的 Spark Streaming 程序,您必须向 SBT 或 Maven 项目添加以下依赖项。
-- maven
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-streaming_2.12</artifactId>
<version>3.1.1</version>
<scope>provided</scope>
</dependency>
--SBT
libraryDependencies += "org.apache.spark" % "spark-streaming_2.12" % "3.1.1" % "provided"
为了从未出现在 Spark Streaming 核心 API 中的 Kafka 和 Kinesis 等源中获取数据,您必须将相应的artifact Spark-Streaming-xyz _ 2.12添加到依赖项中。例如,一些常见的添加如下。
Source | Artifact |
Kafka | spark-streaming-kafka-0-10_2.12 |
Kinesis | spark-streaming-kinesis-asl_2.12 [Amazon Software License] |
2、初始化StreamingContext
要初始化一个 Spark Streaming 程序,必须创建一个 StreamingContext 对象,它是所有 Spark Streaming 功能的主要入口点。
2.1、通过SparkConf
可以从 SparkConf 对象创建 StreamingContext 对象。
import org.apache.spark._
import org.apache.spark.streaming._
val conf = new SparkConf().setAppName(appName).setMaster(master)
val ssc = new StreamingContext(conf, Seconds(1))
appName 参数是应用程序要显示在集群 UI 上的名称。Master 是一个 Spark、 Mesos、 Kubernetes 或者 YARN cluster URL,或者一个用于在本地模式下运行的特殊的“ local [ * ]”字符串。实际上,在集群上运行时,您不希望在程序中硬编码 master,而是使用 spark-submit 启动应用程序并在那里接收它。但是,对于本地测试和单元测试,您可以通过“ local [ * ]”来运行 Spark Streaming in-process (检测本地系统中的核心数量)。注意,这在内部创建了一个 SparkContext (所有 Spark 功能的起点) ,可以作为 ssc.SparkContext 进行访问。
批处理间隔必须根据应用程序的延迟需求和可用的集群资源来设置。更多详细信息请参阅下一节的性能调优。
2.2、通过SparkContext
也可以从现有的 SparkContext 对象创建 StreamingContext 对象
import org.apache.spark.streaming._
val sc = ... // existing SparkContext
val ssc = new StreamingContext(sc, Seconds(1))
- 在定义了 context 之后,您必须执行以下操作。
- 通过创建输入 DStreams 定义输入源
- 通过对 DStreams 应用转换和输出操作来定义流计算
- 开始接收数据并使用
streamingContext.start()
- .
- 使用以下命令等待停止处理(手动或由于任何错误)
streamingContext.awaitTermination()
- .
- 可以使用以下命令手动停止处理
streamingContext.stop()
- .
2.3、注意事项
- 一旦Context已经启动,就不能设置或添加新的流计算
- 一旦停止了Context,就不能重新启动它
- JVM 中同一时间只能有一个 StreamingContext 是活动的
- StreamingContext.stop()也可以去停止SparkContext,如果仅仅是去通知StreamingContext,那么设置stop()的参数 stopSparkContext = false
- 只要在创建下一个 StreamingContext 之前停止前一个 StreamingContext (不停止 SparkContext) ,就可以重新使用 SparkContext 创建多个 StreamingContext
3、离散化的流(DStreams)
Discretized Stream or DStream是 Spark Streaming 提供的基本抽象。它表示一个连续的数据流,或者是从源接收的输入数据流,或者是通过转换输入流生成的处理过的数据流。在内部,DStream 由一系列的 RDDs 表示,RDDs 是 Spark 对不可变的分布式数据集的抽象(更多详细信息请参阅 Spark Programming Guide)。DStream 中的每个 RDD 包含来自某个时间间隔的数据,如下图所示。
对 DStream 应用的任何操作都转换为对基础 RDDs 的操作。例如,在前面的将行转换为单词的示例中,在lines DStream 中的每个 RDD 上应用 flatMap 操作,以生成words DStream 的 RDDs。如下图所示。
这些基础的 RDD 转换是由 Spark 引擎计算的。DStream 操作隐藏了大部分这些细节,并为开发人员提供了一个更高级别的 API,以方便其使用。这些操作将在后面的部分中详细讨论。
4、DStreams输入和Receivers
Input DStreams 表示的是 DStreams从流数据源接收的输入数据流。在上一节的简单示例中,lines 是一个 input DStream,因为它表示从 netcat 服务器接收的数据流。每个 Input DStream (file stream除外,本节后面将讨论)都与一个 Receiver (Scala doc,Java doc)对象相关联,该对象接收来自源的数据,并将其存储在 Spark 的内存中进行处理。
Spark Streaming 提供两类内置流源。
- Basic sources:在StreamingContext API中直接可用的源。例如文件系统和套接字连接
- Advanced sources:像Kafka, Kinesis等资源可以通过额外的实用程序类获得。这些需要根据 linking 章节中讨论的额外依赖项进行链接。
我们将在本节后面讨论每个类别中的一些来源。
请注意,如果您希望在流式应用程序中并行接收多个数据流,可以创建多个输入 DStreams (在性能调优部分进一步讨论)。这将创建多个 Receivers 将同时接收多个数据流。但是请注意,Spark worker/executor 是一个长时间运行的任务,因此它占用了分配给 Spark Streaming 应用程序的一个核心。因此,重要的是要记住,Spark Streaming 应用程序需要分配足够的核心(或线程,如果在本地运行)来处理接收到的数据,以及运行接收器。
4.1、需要记住的要点
- 在本地运行 Spark Streaming 程序时,不要使用“ local”或“ local [1]”作为 master URL。这意味着只有一个线程用于在本地运行任务。如果您使用基于接收器的 input DStream (例如 sockets、 Kafka 等) ,那么这个单线程将被用来去运行 Receiver,这样的话不会留下任何线程来处理接收到的数据。因此,在本地运行时,始终使用“ local [ n ]”作为 master URL,,其中 n > 要运行的接收器数量在集群上运行,分配给 Spark Streaming 应用程序的核心数必须超过接收器的数量。否则,系统将接收数据,但不能处理它。
4.2、基本数据来源
我们已经在上一节中研究了 ssc.socketTextStream (...) ,该示例根据通过 TCP 套接字连接接收的文本数据来创建 DStream。除了套接字,StreamingContext API 还提供了从files 创建 DStreams 作为输入源的方法。
4.2.1 File Streams 文件流
要从与 HDFS API 兼容的任何文件系统(即 HDFS、 S3、 NFS 等)上的文件中读取数据,可以通过 StreamingContext.fileStream[KeyClass, ValueClass, InputFormatClass]
创建 DStream。
文件流不需要运行receiver,因此不需要为接收文件数据分配任何cores。
对于简单的文本文件,最简单的方法是 StreamingContext.textFileStream(dataDirectory)
streamingContext.fileStream[KeyClass, ValueClass, InputFormatClass](dataDirectory)
对于文本文件
streamingContext.textFileStream(dataDirectory)
4.2.2 如何监视目录
Spark Streaming 将监视目录 dataDirectory 并处理在该目录中创建的任何文件。
- 可以监视一个简单目录,例如
"hdfs://namenode:8040/logs/"
所有直接位于该路径下的文件在被发现时将被处理。 - 可以监控一个模式匹配的目录,例如
"hdfs://namenode:8040/logs/2017/*"
.这里,DStream将包含模式匹配的到的所有文件。也就是说:它是目录的模式,而不是目录中的文件的模式。 - 所有文件必须采用相同的数据格式
- 文件被认为是时间周期的一部分,这取决于它的修改时间,而不是创建时间
- 一旦处理后,对当前窗口内文件的更改将不会导致重新读取该文件。也就是说:更新被忽略。
- 一个目录下的文件越多,扫描更改所需的时间就越长ーー即使没有文件被修改
"hdfs://namenode:8040/logs/2016-*"
- , 重命名整个目录以匹配路径将把该目录添加到监视目录列表中。只有修改时间在当前窗口内的目录中的文件才会被包含在流中。
- 调用 FileSystem.setTimes() 来修复时间戳是在以后的窗口中获取文件的一种方法,即使文件的内容没有更改。
4.2.3 使用对象存储作为数据源
诸如 HDFS 之类的“Full”文件系统倾向于在创建输出流后立即设置文件的修改时间。当一个文件被打开时,甚至在数据被完全写入之前,它可能被包含在 DStream 中——在这之后,对同一窗口中的文件的更新将被忽略。也就是说: 可能会遗漏更改,流中的数据也会被忽略。
为了保证在窗口中提取更改,将文件写入未监视的目录,然后在输出流关闭后立即将其重命名为目标目录。如果在创建窗口期间,重命名的文件出现在扫描的目标目录中,则将拾取新数据。
相比之下,如 Amazon s 3和 Azure Storage的对象存储,由于数据实际上是复制的,因此通常具有较慢的重命名操作。此外,重命名的对象可能将 rename ()操作的时间作为其修改时间,因此可能不被视为原始创建时间所暗示的窗口的一部分。
需要对目标对象存储进行仔细的测试,以验证存储的时间戳行为是否与 Spark Streaming 所期望的一致。直接写入目标目录可能是通过所选对象存储区传输数据的适当策略。
4.2.4 基于自定义接收器的数据流
通过自定义接收器接收的数据流可以创建 DStreams。详细信息请参阅自定义接收器指南。
4.2.5 作为流的 rdd 队列
对于使用测试数据测试 Spark Streaming 应用程序,还可以使用 streamingContext.queueStream (queueOfRDDs)创建基于 RDDs 队列的 DStream。推入队列的每个 RDD 将被视为 DStream 中的一批数据,并像流一样处理。
def queueStream[T: ClassTag](
queue: Queue[RDD[T]],
oneAtATime: Boolean = true
): InputDStream[T] = {
queueStream(queue, oneAtATime, sc.makeRDD(Seq[T](), 1))
}
关于来自 socket 和文件的流的更多细节,请参见 Scala StreamingContext、 Java JavaStreamingContext 和 Python StreamingContext 中相关函数的 API 文档。
4.3、高级资源
在 Spark 3.1.1中,Kafka 和 Kinesis 可以在 Python API 中使用。
这类源需要与外部非 spark 库进行链接,其中一些源具有复杂的依赖关系(例如 Kafka)。因此,为了尽量减少与依赖性版本冲突相关的问题,从这些源创建 DStreams 的功能已经移动到单独的库中,必要时可以显式地链接到这些库。
注意,这些高级源在 Spark shell 中不可用,因此基于这些高级源的应用程序不能在 shell 中测试。如果您真的想在 Spark shell 中使用它们,那么您必须下载相应的 Maven artifact’s JAR 及其依赖项,并将其添加到类路径中。
其中一些高级资源如下。
- Kafka:3.1.1与 Kafka broker 0.10或更高版本兼容。查看 Kafka 集成指南了解更多细节。
- Kinesis: Kinesis: Spark Streaming 3.1.1与 Kinesis 客户端库1.2.1兼容。
4.4、 自定义资源
这在 Python 中还不支持。、
还可以从自定义数据源创建输入 DStreams。您所需要做的就是实现一个用户定义的接收器(请参阅下一部分以了解它是什么) ,它可以接收来自自定义源的数据并将其推送到 Spark 中。详细信息请参阅自定义接收器指南。
4.5、 接收器的可靠性
根据数据源的可靠性,可以有两种数据源。数据源(如 Kafka)允许确认传输的数据。如果从这些可靠的来源去接收数据的系统确认接收到的数据,就可以确保不会有任何故障造成数据丢失。这就引出了两种接收器:
- Reliable Receiver - 当数据已经被接收并存储在Spark中并进行复制时,可靠的接收方会正确地向可靠的源发送确认。
- Unreliable Receiver - 不可靠的接收者不向源发送确认。这可用于不支持确认的源
关于如何编写一个可靠的接收器的详细信息在定制接收器指南中进行了讨论。
5、DStreams上的转换
与 rdd 类似,转换允许修改 input DStream 中的数据。DStreams 支持普通 Spark RDD 上可用的许多转换。一些常见的如下。
Transformation | Meaning |
map(func) | 通过将源DStream的每个元素传递给函数func来返回一个新的DStream。 |
flatMap(func) | 类似于map,但是每个输入项可以映射到0个或多个输出项。 |
filter(func) | 只返回经过func过滤后的值为true的元素 |
repartition(numPartitions) | 通过创建更多或更少的分区来更改DStream中的并行级别。 |
union(otherStream) | 返回一个新的DStream,它包含源DStream和otherDStream中的元素的并集。 |
count() | 通过计算源DStream中每个RDD中的元素个数,返回一个单元素RDD的新DStream。 |
reduce(func) | 通过使用函数func(接受两个参数并返回一个)聚合源DStream的每个RDD中的元素,返回一个单元素RDD的新DStream。这个函数应该是结合和交换的,这样它就可以并行计算 |
countByValue() | 当在一个K类型元素的DStream上调用时,返回一个新的DStream (K, Long)对,其中每个键的值是它在源DStream的每个RDD中的频率。 |
reduceByKey(func, [numTasks]) | 在(K, V)对的DStream上调用时,返回一个(K, V)对的新DStream,其中每个键的值使用给定的reduce函数进行聚合。注意:默认情况下,这使用Spark的默认并行任务数(本地模式为2,在集群模式下,并行任务数由配置属性Spark .default.parallelism决定)来进行分组。你可以传递一个可选的numTasks参数来设置不同数量的任务。 |
join(otherStream, [numTasks]) | 当调用两个DStream (K, V)和(K, W)对时,返回一个新的DStream (K, (V, W))对,包含每个键的所有元素对。 |
cogroup(otherStream, [numTasks]) | 当调用一个DStream of (K, V)和(K, W)对时,返回一个新的DStream of (K, Seq[V], Seq[W])元组。 |
transform(func) | 通过对源DStream中的每个RDD应用RDD-to-RDD函数,返回一个新的DStream。这可以用于在DStream上执行任意RDD操作。 |
updateStateByKey(func) | 返回一个新的“state”DStream,其中通过对键的前一个状态和键的新值应用给定的函数来更新每个键的状态。这可以用于维护每个键的任意状态数据。 |
其中一些转换值得更详细地讨论。
5.1 UpdateStateByKey Operation
updateStateByKey 操作允许您在使用新信息不断更新状态的同时维护任意状态。要使用它,你需要做两个步骤。
- Define the state - 状态可以是任意的数据类型
- Define the state update function -用函数指定如何使用以前的状态和输入流中的新值更新状态
在每个批处理中,Spark 将为所有现有键应用状态更新函数,而不管它们是否有批处理中的新数据。如果 update 函数返回 None,那么键值对将被消除。
让我们用一个例子来说明这一点。假设您希望保持文本数据流中每个单词的运行计数。在这里,运行计数是状态,它是一个整数。我们将 update 函数定义为:
def updateFunction(newValues: Seq[Int], runningCount: Option[Int]): Option[Int] = {
val newCount = ... // add the new values with the previous running count to get the new count
Some(newCount)
}
这将应用于包含单词的 DStream (比如前面示例中包含(word,1)对的pairs DStream)。
val runningCounts = pairs.updateStateByKey[Int](updateFunction _)
每个单词都会调用 update 函数,newValues 具有1的序列(来自(word,1)pairs) ,runningCount 具有前一个计数。
请注意,使用 updateStateByKey 需要配置检查点目录,这将在检查点小节中详细讨论。
5.2 、Transform Operation 转换操作
转换操作(及其变体,如 transformawith)允许在 DStream 上应用任意的 rdd 到 rdd 函数。它可以用于应用 DStream API 中未公开的任何 RDD 操作。例如,在 DStream API 中不直接公开将数据流中的每个批处理与另一个数据集联接的功能。但是,您可以轻松地使用转换来实现这一点。这就产生了非常强大的可能性。例如,可以通过将输入数据流与预先计算的垃圾信息(也可能是由 Spark 生成的)联接起来,然后基于它进行过滤,从而实现实时数据清理。
val spamInfoRDD = ssc.sparkContext.newAPIHadoopRDD(...) // RDD containing spam information
val cleanedDStream = wordCounts.transform { rdd =>
rdd.join(spamInfoRDD).filter(...) // join data stream with spam information to do data cleaning
...
}
注意,提供的函数在每个批处理间隔中调用。这允许您执行时变的 RDD 操作,也就是说,RDD 操作、分区数量、广播变量等可以在批之间进行更改。
5.3、 Window Operations 窗口操作
Spark Streaming 还提供窗口式计算,允许您对数据的滑动窗口应用转换。下图说明了这个滑动窗口。
如图所示,每次窗口在源 DStream 上滑动时,窗口中的源 RDDs 被组合和操作,以生成窗口 DStream 的 RDDs。在这种特定情况下,操作应用于数据的最后3个时间单位,并按2个时间单位滑动。这表明任何窗口操作都需要指定两个参数。
- window length 窗口长度 - 窗口的持续时间(图中3)
- sliding interval 滑动间隔 - 执行窗口操作的时间间隔
这两个参数必须是源 DStream batch interval 的倍数
让我们用一个例子来演示窗口操作。例如,您希望通过每10秒对数据的最后30秒生成单词计数来扩展前面的示例。为此,我们必须在数据的最后30秒内对(word,1) pairs 的 DStream 对应用 reduceByKey 操作。这是通过 reduceByKeyAndWindow 操作完成的。
// Reduce last 30 seconds of data, every 10 seconds
val windowedWordCounts = pairs.reduceByKeyAndWindow((a:Int,b:Int) => (a + b), Seconds(30), Seconds(10))
一些常见的窗口操作如下。所有这些操作都采用上述两个参数: windowLength 和 slideInterval。
Transformation | Meaning |
window(windowLength, slideInterval) | 返回一个新的DStream,它是基于源DStream的窗口批数计算的。 |
countByWindow(windowLength, slideInterval) | 返回流中元素的滑动窗口计数。 |
reduceByWindow(func, windowLength, slideInterval) | 返回一个新的单元素流,该流是通过使用func在滑动间隔上聚合流中的元素创建的。这个函数应该是结合的和交换的,这样它就可以被正确地并行计算。 |
reduceByKeyAndWindow(func, windowLength, slideInterval, [numTasks]) | 当调用(K, V)对的DStream时,返回一个(K, V)对的新DStream,其中每个键的值使用给定的reduce函数函数在滑动窗口中批量聚合。注意:默认情况下,这使用Spark的默认并行任务数(本地模式为2,在集群模式下,并行任务数由配置属性Spark .default.parallelism决定)来进行分组。你可以传递一个可选的numTasks参数来设置不同数量的任务。 |
reduceByKeyAndWindow(func, invFunc, windowLength, slideInterval, [numTasks]) | 上面的reduceByKeyAndWindow()的一个更有效的版本,其中每个窗口的reduce值是使用前一个窗口的reduce值递增计算的。这是通过减少进入滑动窗口的新数据和反向减少离开窗口的旧数据来实现的。一个例子是在窗口滑动时增加和减少键的计数。但只适用于可逆reduce函数,即具有相应“inverse reduce”的reduce函数(以invFunc为参数)。像reduceByKeyAndWindow一样,reduce任务的数量可以通过一个可选参数进行配置。注意,必须启用检查点才能使用此操作。 |
countByValueAndWindow(windowLength, slideInterval, [numTasks]) | 当调用一个(K, V)对的DStream时,返回一个新的(K, Long)对的DStream,其中每个键的值是它在滑动窗口中的频率。像reduceByKeyAndWindow一样,reduce任务的数量可以通过一个可选参数进行配置。 |
5.4、 Join Operations 连接操作
最后,值得强调的是,在 Spark Streaming 中执行不同类型的连接是多么容易。
5.4.1 Stream-stream joins
流可以很容易地与其他流连接起来。
val stream1: DStream[String, String] = ...
val stream2: DStream[String, String] = ...
val joinedStream = stream1.join(stream2)
在这里,在每个批处理间隔中,stream1生成的 RDD 将与 stream2生成的 RDD 联接。你也可以使用leftOuterJoin
, rightOuterJoin
, fullOuterJoin
. 此外,通过流的窗口进行连接通常是非常有用的。这也很简单。
val windowedStream1 = stream1.window(Seconds(20))
val windowedStream2 = stream2.window(Minutes(1))
val joinedStream = windowedStream1.join(windowedStream2)
5.4.2 Stream-dataset joins
在前面解释 DStream.transform 操作时已经显示了这一点。下面是另一个将窗口流与数据集联接起来的例子。
val dataset: RDD[String, String] = ...
val windowedStream = stream.window(Seconds(20))...
val joinedStream = windowedStream.transform { rdd => rdd.join(dataset) }
事实上,您还可以动态地更改要加入的数据集。为转换提供的函数在每个批处理间隔内进行评估,因此将使用数据集引用所指向的当前数据集。
DStream 转换的完整列表可以在 API 文档中找到。有关 Scala API,请参见 DStream 和 PairDStreamFunctions。有关 javaapi,请参见 JavaDStream 和 JavaPairDStream。有关 Python API,请参见 DStream。
6、DStreams上的输出操作
输出操作允许将 DStream 的数据推送到外部系统,如数据库或文件系统。由于输出操作实际上允许外部系统使用转换后的数据,因此它们触发了所有 DStream 转换的实际执行(类似于 rdd 的操作)。目前,定义了以下输出操作:
Output Operation | Meaning |
print() | 在运行流应用程序的驱动程序节点上打印DStream中每批数据的前10个元素。这对于开发和调试非常有用。 在python Api中调用 pprint() |
saveAsTextFiles(prefix, [suffix]) | 将DStream的内容保存为文本文件。每次批处理间隔的文件名根据前缀和后缀生成:"prefix- time IN MS[.suffix]"。 |
saveAsObjectFiles(prefix, [suffix]) | 将这个DStream的内容保存为序列化Java对象的SequenceFiles。每次批处理间隔的文件名根据前缀和后缀生成:"prefix- time IN MS[.suffix]"。 Python API中不支持 |
saveAsHadoopFiles(prefix, [suffix]) | 将DStream的内容保存为Hadoop文件。每次批处理间隔的文件名根据前缀和后缀生成:"prefix- time IN MS[.suffix]"。 Python API中不支持 |
foreachRDD(func) | 对从流生成的每个RDD应用函数func的最通用的输出操作符。这个函数应该将每个RDD中的数据推送到外部系统,比如将RDD保存到文件中,或者通过网络将其写入数据库。注意,函数func是在运行流应用程序的驱动程序进程中执行的,并且通常会有RDD操作,强制计算流RDD。 |
6.1 使用 foreachRDD 的设计模式
Foreachrdd 是非常强大的,它允许将数据发送到外部系统。然而,重要的是要了解如何正确有效地使用它。以下是一些常见的要避免的错误。
通常将数据写入外部系统需要创建一个连接对象(例如 TCP 连接到远程服务器) ,并使用它将数据发送到远程系统。为此,开发人员可能无意中尝试在 Spark 驱动程序中创建一个连接对象,然后尝试在 Spark 辅助程序中使用它来保存 rdd 中的记录。例如(在 Scala 中) ,
dstream.foreachRDD { rdd =>
val connection = createNewConnection() // executed at the driver
rdd.foreach { record =>
connection.send(record) // executed at the worker
}
}
这是不正确的,因为这需要序列化连接对象并将其从驱动程序发送给工作线程。这样的连接对象很少能跨机器转移。此错误可能表现为序列化错误(连接对象不可序列化)、初始化错误(连接对象需要在工作线程中进行初始化)等。正确的解决方案是在 worker 上创建连接对象。
但是,这可能会导致另一个常见错误——为每条记录创建新连接
dstream.foreachRDD { rdd =>
rdd.foreach { record =>
val connection = createNewConnection()
connection.send(record)
connection.close()
}
}
通常,创建连接对象需要时间和资源开销。因此,为每个记录创建和销毁连接对象可能会导致不必要的高开销,并且可能会显著降低系统的总吞吐量。一个更好的解决方案是使用 RDD.foreachPartition ——创建一个连接对象,并使用该连接发送 RDD 分区中的所有记录。
rdd.foreachPartition { partitionOfRecords =>
val connection = createNewConnection()
partitionOfRecords.foreach(record => connection.send(record))
connection.close()
}
}
这将在许多记录上分摊连接创建的开销。
最后,可以通过跨多个 RDDs/batches 重用连接对象进一步优化这一点。我们可以维护一个静态的连接对象池,当多个批的 rdd 被推送到外部系统时,可以重用这个池,从而进一步减少开销。
dstream.foreachRDD { rdd =>
rdd.foreachPartition { partitionOfRecords =>
// ConnectionPool is a static, lazily initialized pool of connections
val connection = ConnectionPool.getConnection()
partitionOfRecords.foreach(record => connection.send(record))
ConnectionPool.returnConnection(connection) // return to the pool for future reuse
}
}
请注意,池中的连接应该根据需要延迟创建,如果一段时间内没有使用,则应超时。这样可以最有效地将数据发送到外部系统。
6.2 其他需要记住的要点
- DStreams 通过输出操作延迟执行,就像 RDD 通过 RDD 操作延迟执行一样。具体来说,DStream 输出操作中的 RDD 操作强制处理接收到的数据。因此,如果您的应用程序没有任何输出操作,或者具有类似 dstream.foreachRDD ()这样的输出操作,而其中没有任何 RDD 操作,那么就不会执行任何操作。系统将简单地接收数据并丢弃它。
- 默认情况下,输出操作一次执行一个,并且按照在应用程序中定义的顺序执行。
7、DataFrame and SQL 操作
您可以轻松地对流数据使用 DataFrames 和 SQL 操作。您必须使用 StreamingContext 正在使用的 SparkContext 创建 SparkSession。此外,这样做是为了在驱动程序失败时可以重新启动。这是通过创建 SparkSession 的惰性实例化单例实例来实现的。下面的示例显示了这一点。它修改了前面的字数统计示例,以使用 DataFrames 和 SQL 生成字数统计。每个 RDD 都被转换为一个 DataFrame,注册为临时表,然后使用 SQL 查询。
/**
* Use DataFrames and SQL to count words in UTF8 encoded, '\n' delimited text received from the
* network every second.
*
* Usage: SqlNetworkWordCount <hostname> <port>
* <hostname> and <port> describe the TCP server that Spark Streaming would connect to receive data.
*
* To run this on your local machine, you need to first run a Netcat server
* `$ nc -lk 9999`
* and then run the example
* `$ bin/run-example org.apache.spark.examples.streaming.SqlNetworkWordCount localhost 9999`
*/
object SqlNetworkWordCount {
def main(args: Array[String]): Unit = {
if (args.length < 2) {
System.err.println("Usage: NetworkWordCount <hostname> <port>")
System.exit(1)
}
StreamingExamples.setStreamingLogLevels()
// Create the context with a 2 second batch size
val sparkConf = new SparkConf().setAppName("SqlNetworkWordCount")
val ssc = new StreamingContext(sparkConf, Seconds(2))
// Create a socket stream on target ip:port and count the
// words in input stream of \n delimited text (e.g. generated by 'nc')
// Note that no duplication in storage level only for running locally.
// Replication necessary in distributed scenario for fault tolerance.
val lines = ssc.socketTextStream(args(0), args(1).toInt, StorageLevel.MEMORY_AND_DISK_SER)
val words = lines.flatMap(_.split(" "))
// Convert RDDs of the words DStream to DataFrame and run SQL query
words.foreachRDD { (rdd: RDD[String], time: Time) =>
// Get the singleton instance of SparkSession
val spark = SparkSessionSingleton.getInstance(rdd.sparkContext.getConf)
import spark.implicits._
// Convert RDD[String] to RDD[case class] to DataFrame
val wordsDataFrame = rdd.map(w => Record(w)).toDF()
// Creates a temporary view using the DataFrame
wordsDataFrame.createOrReplaceTempView("words")
// Do word count on table using SQL and print it
val wordCountsDataFrame =
spark.sql("select word, count(*) as total from words group by word")
println(s"========= $time =========")
wordCountsDataFrame.show()
}
ssc.start()
ssc.awaitTermination()
}
}
/** Case class for converting RDD to DataFrame */
case class Record(word: String)
/** Lazily instantiated singleton instance of SparkSession */
object SparkSessionSingleton {
@transient private var instance: SparkSession = _
def getInstance(sparkConf: SparkConf): SparkSession = {
if (instance == null) {
instance = SparkSession
.builder
.config(sparkConf)
.getOrCreate()
}
instance
}
}
// scalastyle:on println
您还可以在来自不同线程(即异步到运行的 StreamingContext)的流数据上定义的表上运行 SQL 查询。只需确保您设置 StreamingContext 来记住足够数量的流数据,以便查询能够运行。否则 StreamingContext 将在查询完成之前删除旧的流数据,因为 StreamingContext 不知道有任何异步 SQL 查询。例如,如果您想查询最后一批,但是您的查询可能需要5分钟才能运行,那么调用 streamingContext.remember (Minutes (5))(在 Scala 中,或者在其他语言中相当于)。
请参阅 DataFrames 和 SQL 指南以了解更多关于 DataFrames 的信息。
8、MLlib Operations
您还可以轻松地使用 MLlib 提供的机器学习算法。首先,有流式机器学习算法(如流式线性回归、流式 KMeans 等) ,它们可以同时从流式数据中学习,并将模型应用于流式数据。除此之外,对于更大类别的机器学习算法,您可以离线学习一个学习模型(即使用历史数据) ,然后在流数据上应用该模型。详细信息请参阅 MLlib 指南。