一、Spark Streaming是什么?
Spark Streaming是Spark的上一代流式计算引擎。Spark Streaming不再有更新,它是一个遗留项目。Spark中有一个更新且更易于使用的流式计算引擎:Structured Streaming。因此我们只要做到初步了解并简单使用Spark Streaming就可以了。
下面就让我们跟着官网来学习吧
Spark Streaming - Spark 3.5.3 Documentation
二、概览
Spark Streaming是核心Spark API的扩展,它支持实时数据流的可扩展、高吞吐量、容错流处理。
- 数据来源:Kafka、Kinesis或TCP socket
- 数据处理:如
map
、reduce
、join
和window
等高级函数,也可以使用Spark的机器学习和图形计算算法 - 输出:推送到文件系统、数据库和实时仪表板
Spark Streaming接收实时输入数据流并将数据分成批次进行处理来生成结果流。【微批】
Spark Streaming提供了一种称为离散流 或DStream的高级抽象,它表示连续的数据流。DStreams可以从Kafka和Kinesis等源的输入数据流创建,也可以通过对其他DStreams应用高级操作来创建。在内部,DStream表示为一系列RDD。
三、入门例子
下面我们用一个例子理解Spark Streaming的数据处理过程。
假设我们要计算从侦听TCP socket 的数据服务器接收到的文本数据中的字数
1、添加maven依赖
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-streaming_2.12</artifactId>
<version>3.5.3</version>
<scope>provided</scope>
</dependency>
Spark Streaming核心API中不存在从Kafka和Kinesis等源获取数据,需要单独添加依赖
<!-- https://mvnrepository.com/artifact/org.apache.spark/spark-streaming-kafka-0-10 -->
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-streaming-kafka-0-10_2.12</artifactId>
<version>2.4.4</version>
</dependency>
2、官方例子
Scala
object NetworkWordCount {
def main(args: Array[String]): Unit = {
if (args.length < 2) {
System.err.println("Usage: NetworkWordCount <hostname> <port>")
System.exit(1)
}
StreamingExamples.setStreamingLogLevels()
// 创建一个具有2个线程和1秒批处理间隔的本地StreamingContext。
val sparkConf = new SparkConf().setAppName("NetworkWordCount")
val ssc = new StreamingContext(sparkConf, Seconds(1))
// 创建一个 DStream 去连接 hostname:port
//linesDStream表示将从数据服务器接收的数据流,此DStream中的每条记录都是一行文本
val lines = ssc.socketTextStream(args(0), args(1).toInt, StorageLevel.MEMORY_AND_DISK_SER)
// 按空格字符将行拆分为单词
// flatMap是一个一对多的DStream操作,它通过从源DStream中的每个记录生成多个新记录来创建一个新的DStream
// 在这种情况下,每行将被分成多个单词,单词流被表示为wordsDStream
val words = lines.flatMap(_.split(" "))
// 统计每批中的每个单词
val wordCounts = words.map(x => (x, 1)).reduceByKey(_ + _)
// 每秒打印下单词的统计
wordCounts.print()
ssc.start() // 开始计算
ssc.awaitTermination() // 等待计算终止
}
}
3、运行
运行Netcat
nc -lk 9999
新建一个窗口运行官方例子
cd /opt/cloudera/parcels/CDH-6.3.1-1.cdh6.3.1.p0.1470567/lib/spark/
bin/run-example org.apache.spark.examples.streaming.NetworkWordCount cdh1 9999
向 9999 端口写入数据
查看Spark Streaming 窗口
Spark Streaming 会1s处理一次从9999端口来的文本数据,并进行统计
四、基本概念
1、StreamingContext
SparkContext 是 Spark 程序的入口
StreamingContext 是 Spark Streaming 程序的入口
可以从SparkConf对象创建StreamingContext对象,也可以从现有的SparkContext
对象创建StreamingContext对象。定义完StreamingContext对象后必须执行如下操作:
- 通过创建输入DStreams来定义输入源
- 通过将转换和输出操作应用于DStreams来定义流计算
- 开始接收数据并使用
streamingContext.start()
进行处理。 - 等待处理停止(手动或由于任何错误)使用
streamingContext.awaitTermination()
同样要记住以下要点:
- 一旦上下文启动,就不能设置或添加新的流计算
- 上下文一旦停止,就无法重新启动
- JVM中只能同时激活一个StreamingContext
- StreamingContext上的stop()也会停止SparkContext。要仅停止StreamingContext,请将
stop()
的可选参数stopSparkContext
设置为false - 只要在创建下一个StreamingContext之前停止前一个StreamingContext(不停止SparkContext),就可以重新使用SparkContext来创建多个StreamingContext
2、DStreams
它是Discretized Streams的简称,因此成为离散流
离散流 或DStream是Spark Streaming提供的基本抽象,它表示一个连续的数据流,要么是从源接收的输入数据流,要么是通过转换输入流产生的处理后的数据流。在内部,一个DStream由一系列连续的RDD表示,这是Spark对一个不可变的、分布式数据集的抽象。DStream中的每个RDD都包含来自某个区间的数据,如下图所示
在DStream上的任何操作最终都是在RDD上的操作。例如在入门例子中对单词的操作,DStream中的lines
中的每个RDD应用flatMap
操作以生成words
DStream的RDD。如下图所示
这些底层RDD转换由Spark引擎计算。DStream操作隐藏了大部分细节,并为开发人员提供了更高级别的API。
3、DStreams上的输入处理
在入门例子中,lines
是输入DStream,因为它表示从netcat服务器接收的数据流。每个输入DStream(除了本节后面讨论的文件流)都与一个接收器对象相关联,该对象从源接收数据并将其存储在Spark的内存中进行处理。
Spark Streaming提供了两类内置流源:
- 基本源:StreamingContext API中直接可用的源。示例:文件系统和套接字连接。
- 高级源代码:Kafka、Kinesis等源代码可通过额外的实用程序类获得。这些需要针对额外的依赖项进行链接
此外还可以自定义数据流源,此时需要自己定义一个**接收器,**它可以从自定义源接收数据并将其推送到Spark。
4、DStreams上的转换操作
与RDD类似,转换允许修改来自输入DStream的数据。DStreams支持普通Spark RDD上可用的许多转换。
一些常见的算子操作如下:
map (func )、flatMap (func )、filter (func )、repartition (numPartitions )、union (otherStream )、count ()、reduce (func )、countByValue ()、reduceByKey (func , [numTasks ])、join (otherStream , [numTasks ])、cogroup (otherStream , [numTasks ])、transform (func )、updateStateByKey (func)
其中updateStateByKey (func)我们重点说下:通过它可以操作可以保持任意状态,同时不断地用新信息更新它。
- 定义状态-状态可以是任意数据类型
- 定义状态更新函数-使用函数指定如何使用先前的状态和输入流中的新值来更新状态
在每个批处理中,Spark将对所有现有键应用状态更新函数,无论它们在批处理中是否有新数据。如果更新函数返回None
,则键值对将被消除
假设我们要维护文本数据流中看到的每个单词的运行计数,在这里,运行计数是状态,它是一个整数。我们将更新函数定义为:
Scala
def updateFunction(newValues: Seq[Int], runningCount: Option[Int]): Option[Int] = {
val newCount = ... // 将新值与之前的运行计数相加,得到新计数
Some(newCount)
}
并将这个更新函数应用于入门例子中的DStream
Scala
val runningCounts = pairs.updateStateByKey[Int](updateFunction _)
将为每个单词调用update函数,其中newValues
具有1的序列,并且runningCount
具有前一个count
5、DStreams上的窗口操作
Spark Streaming还提供窗口计算,允许您在数据滑动窗口上应用转换。下图说明了这个滑动窗口
如图所示,每次窗口在源DStream上滑动时,落入窗口内的源RDD被组合并操作以生成窗口化DStream的RDD。在这种情况下,操作应用于数据的最后3个时间单位,并滑动2个时间单位。这表明任何窗口操作都需要指定两个参数
- 窗口长度-窗口的持续时间
- 滑动间隔-执行窗口操作的间隔
这两个参数必须是源DStream的批处理间隔的倍数,间隔是处理数据的最小单位。
6、DStreams上的输出操作
输出操作允许将DStream的数据推送到外部系统,如数据库或文件系统。由于输出操作实际上允许转换后的数据被外部系统使用,它们触发所有DStream转换的实际执行(类似于RDD的Action算子)。目前,定义了以下输出操作:
- print():在运行流应用程序的驱动程序节点上打印DStream中每批数据的前十个元素。这对于开发和调试很有用。python API 中是pprint()
- saveAsTextFiles() :将此DStream的内容保存为文本文件。每个批处理间隔的文件名是根据前缀和后缀生成的:"前缀-TIME_IN_MS[.后缀]"
- saveAsObjectFiles() :将此DStream的内容保存为序列化Java对象的
SequenceFiles
。每个批处理间隔的文件名是根据 "prefix-TIME_IN_MS[.suffix]" *。*Python API 不适用 - saveAsHadoopFiles() : 将此DStream的内容保存为Hadoop文件。每个批处理间隔的文件名是根据前缀和后缀生成的:"prefix-TIME_IN_MS[.suffix]"。Python API 不适用
- foreachRDD() :最通用的输出运算符,将函数func应用于从流生成的每个RDD。此函数应将每个RDD中的数据推送到外部系统,例如将RDD保存到文件中,或将其通过网络写入数据库。请注意,函数func在运行流应用程序的驱动程序进程中执行,并且通常会在其中包含RDD操作,这将强制计算流RDD。
7、DataFrame 和 SQL 操作
DataFrame 和 SQL在处理DStreams时同样适用。但必须使用StreamingContext正在使用的SparkContext创建SparkSession。需要将RDD转换为DataFrame,注册为临时表,然后使用SQL进行查询。如下示例:
Scala
val words: DStream[String] = ...
words.foreachRDD { rdd =>
// 获取SparkSession的单例实例
val spark = SparkSession.builder.config(rdd.sparkContext.getConf).getOrCreate()
import spark.implicits._
// 将 RDD[String] 转换为 DataFrame
val wordsDataFrame = rdd.toDF("word")
// 创建临时表
wordsDataFrame.createOrReplaceTempView("words")
// 使用SQL对DataFrame进行字数统计并打印出来
val wordCountsDataFrame =
spark.sql("select word, count(*) as total from words group by word")
wordCountsDataFrame.show()
}
8、MLlib 操作
MLlib提供的机器学习算法同样可以用到DStreams中。首先,有流式机器学习算法(例如流式线性回归、流式KMeans等),它们可以同时从流式数据中学习,并将模型应用于流式数据。除此之外,对于更大的机器学习算法类别,也可以离线学习学习模型(即使用历史数据),然后将模型在线应用于流式数据。
9、缓存和持久化
与RDD类似,DStreams也允许开发人员将流的数据持久化在内存中。也就是说,在DStream上使用persist()
方法将自动将该DStream的每个RDD持久化在内存中。如果DStream中的数据将被多次计算(例如,对同一数据进行多次操作),这将非常有用。对于基于窗口的操作,如reduceByWindow
和reduceByKeyAndWindow
和基于状态的操作,如updateStateByKey
,这是隐含的。因此,基于窗口的操作生成的DStreams会自动持久化在内存中,而无需开发人员调用persist()
。
对于通过网络接收数据的输入流(例如Kafka、sockets等),默认持久性级别设置为将数据复制到两个节点以实现容错。
请注意,与RDD不同,DStreams的默认持久性级别将数据序列化在内存中
10、检查点
流应用程序必须全天候运行,因此必须能够抵御与应用程序逻辑无关的故障(例如,系统故障、JVM崩溃等)。为了实现这一点,Spark Streaming需要将足够的信息检查点到容错存储系统,以便它可以从故障中恢复。有两种类型的数据被检查点。
- 元数据检查点 *:*将定义流计算的信息保存到HDFS等容错存储中。这用于从运行流应用程序驱动程序的节点故障中恢复。元数据包括:配置、DStream操作、作业已排队但尚未完成的批次。
- 数据检查点:将生成的RDD保存到可靠存储中。这在一些跨多个批次组合数据的有状态转换中是必要的。在这种转换中,生成的RDD依赖于以前批次的RDD,这导致依赖链的长度随着时间不断增加。为了避免恢复时间的无限增加(与依赖链成正比),有状态转换的中间RDD被定期检查点到可靠存储(例如HDFS)以切断依赖链。
11、累加器和广播变量
累加器和广播变量无法从Spark Streaming中的检查点恢复。如果启用检查点并同时使用累加器或广播变量,则必须为累加器和广播变量创建延迟实例化的单例实例,以便在驱动程序失败时重新启动后可以重新实例化它们。例如:
Scala
object WordExcludeList {
@volatile private var instance: Broadcast[Seq[String]] = null
def getInstance(sc: SparkContext): Broadcast[Seq[String]] = {
if (instance == null) {
synchronized {
if (instance == null) {
val wordExcludeList = Seq("a", "b", "c")
instance = sc.broadcast(wordExcludeList)
}
}
}
instance
}
}
object DroppedWordsCounter {
@volatile private var instance: LongAccumulator = null
def getInstance(sc: SparkContext): LongAccumulator = {
if (instance == null) {
synchronized {
if (instance == null) {
instance = sc.longAccumulator("DroppedWordsCounter")
}
}
}
instance
}
}
wordCounts.foreachRDD { (rdd: RDD[(String, Int)], time: Time) =>
// 获取或者注册 the excludeList Broadcast
val excludeList = WordExcludeList.getInstance(rdd.sparkContext)
// 获取或者注册 the droppedWordsCounter Accumulator
val droppedWordsCounter = DroppedWordsCounter.getInstance(rdd.sparkContext)
// 用 excludeList 去删除单词,用 droppedWordsCounter 统计
val counts = rdd.filter { case (word, count) =>
if (excludeList.value.contains(word)) {
droppedWordsCounter.add(count)
false
} else {
true
}
}.collect().mkString("[", ", ", "]")
val output = "Counts at time " + time + " " + counts
})