以下笔记基于对尚硅谷spark教程的学习,Spark版本3.0
目录
SparkStreaming 概述
Spark Streaming 是什么
准实时(秒,分钟级别),微批次(时间)的数据处理框架
Spark Streaming 用于流式数据的处理。
Spark Streaming 支持的数据输入源很多,例如:Kafka、Flume、Twitter、ZeroMQ 和简单的 TCP 套接字等等。
数据输入后可以用 Spark 的高度抽象原语,如:map、reduce、join、window 等进行运算。
而结果也能保存在很多地方,如 HDFS,数据库等。
和 Spark 基于 RDD 的概念很相似,Spark Streaming 使用离散化流(discretized stream)作为抽象表示,叫作 DStream。
DStream 是随时间推移而收到的数据的序列。在内部,每个时间区间收到的数据都作为 RDD 存在,而 DStream 是由这些 RDD 所组成的序列(因此得名“离散化”)。
所以简单来将,DStream 就是对 RDD 在实时数据处理场景的一种封装。
背压机制
Spark 1.5 以前版本,用户如果要限制 Receiver 的数据接收速率,可以通过设置静态配制参数“spark.streaming.receiver.maxRate”的值来实现,
此举虽然可以通过限制接收速率,来适配当前的处理能力,防止内存溢出,但也会引入其它问题。
比如:producer 数据生产高于 maxRate,当前集群处理能力也高于 maxRate,这就会造成资源利用率下降等问题。
为了更好的协调数据接收速率与资源处理能力,1.5 版本开始 Spark Streaming 可以动态控制数据接收速率来适配集群数据处理能力。
背压机制(即 Spark Streaming Backpressure): 根据JobScheduler 反馈作业的执行信息来动态调整 Receiver 数据接收率。
通过属性“spark.streaming.backpressure.enabled”来控制是否启用 backpressure 机制,默认值false,即不启用。
Dstream WordCount示例
1) 添加依赖
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-streaming_2.12</artifactId>
<version>3.0.0</version>
</dependency>
2) 编写代码
object StreamWordCount {
def main(args: Array[String]): Unit = {
//1.初始化 Spark 配置信息
val sparkConf = new SparkConf().setMaster("local[*]").setAppName("StreamWordCount")
//2.初始化 SparkStreamingContext,第二个参数表示批量处理的周期(采集周期)
val ssc = new StreamingContext(sparkConf, Seconds(3))
//3.通过监控端口创建 DStream,读进来的数据为一行行
val lineStreams = ssc.socketTextStream("localhost", 9999)
//将每一行数据做切分,形成一个个单词
val wordStreams = lineStreams.flatMap(_.split(" "))
//将单词映射成元组(word,1)
val wordAndOneStreams = wordStreams.map((_, 1))
//将相同的单词次数做统计
val wordAndCountStreams = wordAndOneStreams.reduceByKey(_+_)
//打印
wordAndCountStreams.print()
//启动采集器
ssc.start()
//等待采集器的关闭
ssc.awaitTermination()
}
}
3) 启动程序并通过 netcat 发送数据:
nc -lk 9999
hello spark
DStream 创建
StreamingContext的方法创建DStream
// 创建一个输入流,该输入流监视Hadoop兼容文件系统中的新文件,并将其作为平面二进制文件读取,假设每条记录的长度固定,每条记录生成一个字节数组。
// 必须将文件从同一文件系统中的另一位置“移动”到受监视的目录中。以.开头的文件名将被忽略。
def binaryRecordsStream(directory: String, recordLength: Int): DStream[Array[Byte]]
// 创建一个输入流,用于监视Hadoop兼容文件系统中的新文件,并使用给定的键值类型和输入格式读取这些文件。
// 必须将文件从同一文件系统中的另一位置“移动”到受监视的目录中。以.开头的文件名将被忽略。
// F为读取HDFS文件的输入格式类,包含行分隔符和读取数据返回K,V格式数据的方法
def fileStream[K, V, F <: InputFormat[K, V]](directory: String, filter: (Path) ⇒ Boolean, newFilesOnly: Boolean, conf: Configuration)(implicit arg0: ClassTag[K], arg1: ClassTag[V], arg2: ClassTag[F]): InputDStream[(K, V)]
def fileStream[K, V, F <: InputFormat[K, V]](directory: String, filter: (Path) ⇒ Boolean, newFilesOnly: Boolean)(implicit arg0: ClassTag[K], arg1: ClassTag[V], arg2: ClassTag[F]): InputDStream[(K, V)]
def fileStream[K, V, F <: InputFormat[K, V]](directory: String)(implicit arg0: ClassTag[K], arg1: ClassTag[V], arg2: ClassTag[F]): InputDStream[(K, V)]
// 创建一个输入流,用于监视Hadoop兼容文件系统中的新文件,并将其作为文本文件读取
// 必须将文件从同一文件系统中的另一位置“移动”到受监视的目录中。以.开头的文件名将被忽略
// 文本文件必须编码为UTF-8
// 内部调用fileStream方法,K:LongWritable,V:Text,InputFormat:TextInputFormat
def textFileStream(directory: String): DStream[String]
// 从RDD队列创建输入流。在每个批处理中,它将处理队列返回的一个或所有RDD
// defaultRDD:队列为空时返回默认RDD,未设置时队列为空不返回RDD
def queueStream[T](queue: Queue[RDD[T]], oneAtATime: Boolean, defaultRDD: RDD[T])(implicit arg0: ClassTag[T]): InputDStream[T]
def queueStream[T](queue: Queue[RDD[T]], oneAtATime: Boolean = true)(implicit arg0: ClassTag[T]): InputDStream[T]
// 从网络源hostname:port创建一个输入流,其中数据作为串行化块(使用Spark的串行化器进行串行化)接收,可以直接推送到块管理器中,而无需对其进行反序列化。
def rawSocketStream[T](hostname: String, port: Int, storageLevel: StorageLevel = StorageLevel.MEMORY_AND_DISK_SER_2)(implicit arg0: ClassTag[T]): ReceiverInputDStream[T]
// 从TCP源主机名:端口创建输入流。使用给定的转换器将接收字节解释为对象。
def socketStream[T](hostname: String, port: Int, converter: (InputStream) ⇒ Iterator[T], storageLevel: StorageLevel)(implicit arg0: ClassTag[T]): ReceiverInputDStream[T]
// 从TCP源主机名:端口创建输入流。使用行字符串转换器,编码UTF-8,行分隔符:\n
def socketTextStream(hostname: String, port: Int, storageLevel: StorageLevel = StorageLevel.MEMORY_AND_DISK_SER_2): ReceiverInputDStream[String]
// 使用任意用户实现的接收器创建输入流。
def receiverStream[T](receiver: Receiver[T])(implicit arg0: ClassTag[T]): ReceiverInputDStream[T]
// 使用给定的转换方法把多个流合并成一个流
def transform[T](dstreams: Seq[DStream[_]], transformFunc: (Seq[RDD[_]], Time) ⇒ RDD[T])(implicit arg0: ClassTag[T]): DStream[T]
// 从相同类型和相同采集周期的多个DStream创建统一的DStream。
def union[T](streams: Seq[DStream[T]])(implicit arg0: ClassTag[T]): DStream[T]
queueStream示例
object RDDStream {
def main(args: Array[String]) {
//1.初始化 Spark 配置信息
val conf = new SparkConf().setMaster("local[*]").setAppName("RDDStream")
//2.初始化 SparkStreamingContext
val ssc = new StreamingContext(conf, Seconds(4))
//3.创建 RDD 队列
val rddQueue = new mutable.Queue[RDD[Int]]()
//4.创建 QueueInputDStream
val inputStream = ssc.queueStream(rddQueue,oneAtATime = false)
//5.处理队列中的 RDD 数据
val mappedStream = inputStream.map((_,1))
val reducedStream = mappedStream.reduceByKey(_ + _)
//6.打印结果
reducedStream.print()
//7.启动任务
ssc.start()
//8.循环创建并向 RDD 队列中放入 RDD
for (i <- 1 to 5) {
rddQueue += ssc.sparkContext.makeRDD(1 to 300, 10)
Thread.sleep(2000)
}
ssc.awaitTermination()
}
}
自定义数据源
需要继承 Receiver,并实现 onStart、onStop 方法来自定义数据源采集。
/*
自定义数据采集器
1. 继承Receiver,定义泛型, 传递参数
2. 重写方法
*/
class MyReceiver extends Receiver[String](StorageLevel.MEMORY_ONLY) {
private var flg = true
override def onStart(): Unit = {
new Thread(new Runnable {
override def run(): Unit = {
while ( flg ) {
val message = "采集的数据为:" + new Random().nextInt(10).toString
store(message)
Thread.sleep(500)
}
}
}).start()
}
override def onStop(): Unit = {
flg = false;
}
}
// 使用自定义数据源
val messageDS: ReceiverInputDStream[String] = ssc.receiverStream(new MyReceiver())
Kafka 数据源
导入依赖
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-streaming-kafka-0-10_2.12</artifactId>
<version>3.0.0</version>
</dependency>
编写代码
object DirectAPI {
def main(args: Array[String]): Unit = {
//1.创建 SparkConf
val sparkConf: SparkConf = new SparkConf().setAppName("ReceiverWordCount").setMaster("local[*]")
//2.创建 StreamingContext
val ssc = new StreamingContext(sparkConf, Seconds(3))
//3.定义 Kafka 参数
val kafkaPara: Map[String, Object] = Map[String, Object](
ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG -> "linux1:9092,linux2:9092,linux3:9092",
ConsumerConfig.GROUP_ID_CONFIG -> "atguigu",
"key.deserializer" -> "org.apache.kafka.common.serialization.StringDeserializer",
"value.deserializer" -> "org.apache.kafka.common.serialization.StringDeserializer"
)
//4.读取 Kafka 数据创建 DStream
val kafkaDStream: InputDStream[ConsumerRecord[String, String]] = KafkaUtils.createDirectStream[String, String](
ssc,
LocationStrategies.PreferConsistent,
ConsumerStrategies.Subscribe[String, String](Set("atguigu"), kafkaPara)
)
//5.将每条消息的 KV 取出
val valueDStream: DStream[String] = kafkaDStream.map(record => record.value())
//6.计算 WordCount
valueDStream.flatMap(_.split(" "))
.map((_, 1))
.reduceByKey(_ + _)
.print()
//7.开启任务
ssc.start()
ssc.awaitTermination()
}
}
DStream 转换
无状态转化操作
无状态转化操作就是把简单的 RDD 转化操作应用到每个批次上,也就是转化 DStream 中的每一个 RDD。
部分无状态转化操作列在了下表中。注意,针对键值对的 DStream 转化操作(比如reduceByKey())要添加 import StreamingContext._才能在 Scala 中使用。
map()
对DStream中的每个元素应用给定函数,返回由输出元素组成的DStream
例如:ds.map(x => x + 1)
flatMap()
对DStream中的每个元素应用给定函数,返回由输出迭代器中元素组成的DStream
例如:ds.flatMap(x => x.split(" "))
filter()
返回由给定DStream中通过筛选的元素组成的DStream
例如:ds.filter(x => x!=1)
repartition()
改变DStream的分区数
例如:ds.repartition(10)
reduceByKey()
将每个批次中键相同的记录归约
例如:ds.reduceByKey((x, y) => x + y)
groupByKey()
将每个批次中的记录根据键分组
例如:ds.groupByKey()
需要记住的是,尽管这些函数看起来像作用在整个流上一样,但事实上每个 DStream 在内部是由许多 RDD(批次)组成,且无状态转化操作是分别应用到每个 RDD 上的。
例如:reduceByKey()会归约每个时间区间中的数据,但不会归约不同区间之间的数据。
Transform
Transform 允许 DStream 上执行任意的 RDD-to-RDD 函数。即使这些函数并没有在 DStream的 API 中暴露出来,通过该函数可以方便的扩展 Spark API。
该函数每一批次调度一次。其实也就是对 DStream 中的 RDD 应用转换。
val lines = ssc.socketTextStream("localhost", 9999)
// transform方法可以将底层RDD获取到后进行操作
// 1. DStream功能不完善
// 2. 需要代码周期性的执行
// Code : Driver端
val newDS: DStream[String] = lines.transform(
rdd => {
// Code : Driver端,(周期性执行)
rdd.map(
str => {
// Code : Executor端
str
}
)
}
)
// Code : Driver端
val newDS1: DStream[String] = lines.map(
data => {
// Code : Executor端
data
}
)
join
两个流之间的 join 需要两个流的批次大小一致,这样才能做到同时触发计算。
计算过程就是对当前批次的两个流中各自的 RDD 进行 join,与两个 RDD 的 join 效果相同。
val data9999 = ssc.socketTextStream("localhost", 9999)
val data8888 = ssc.socketTextStream("localhost", 8888)
val map9999: DStream[(String, Int)] = data9999.map((_,9))
val map8888: DStream[(String, Int)] = data8888.map((_,8))
val joinDS: DStream[(String, (Int, Int))] = map9999.join(map8888)
有状态转化操作
UpdateStateByKey
UpdateStateByKey原语用于记录历史记录,有时,我们需要在DStream中跨批次维护状态(例如流计算中累加wordcount)。
针对这种情况,updateStateByKey()为我们提供了对一个状态变量的访问,用于键值对形式的DStream。
给定一个由(键,事件)对构成的DStream,并传递一个指定如何根据新的事件更新每个键对应状态的函数,它可以构建出一个新的DStream,其内部数据为(键,状态) 对。
updateStateByKey() 的结果会是一个新的DStream,其内部的RDD 序列是由每个时间区间对应的(键,状态)对组成的。
updateStateByKey操作使得我们可以在用新信息进行更新时保持任意的状态。为使用这个功能,需要做下面两步:
1. 定义状态,状态可以是一个任意的数据类型。
2. 定义状态更新函数,用此函数阐明如何使用之前的状态和来自输入流的新值对状态进行更新。
// 使用updateStateByKey需要对检查点目录进行配置,会使用检查点来保存状态。
ssc.checkpoint("cp")
// updateStateByKey:根据key对数据的状态进行更新
// 传递的参数中含有两个值
// 第一个值表示相同的key的value数据
// 第二个值表示缓存区相同key的value数据
val state = wordToOne.updateStateByKey(
( seq:Seq[Int], buff:Option[Int] ) => {
val newCount = buff.getOrElse(0) + seq.sum
Option(newCount)
}
)
WindowOperations
Window Operations 可以设置窗口的大小和滑动窗口的间隔来动态的获取当前 Streaming 的状态。所有基于窗口的操作都需要两个参数,分别为窗口时长以及滑动步长。
窗口时长:计算内容的时间范围;
滑动步长:隔多久触发一次计算。
注意:这两者都必须为采集周期大小的整数倍。
window(windowLength, slideInterval)
基于对源 DStream 窗化的批次进行计算返回一个新的 Dstream;
countByWindow(windowLength, slideInterval)
返回一个滑动窗口计数流中的元素个数;
reduceByWindow(func, windowLength, slideInterval)
通过使用自定义函数整合滑动区间流元素来创建一个新的单元素流;
reduceByKeyAndWindow(func, windowLength, slideInterval, [numTasks])
当在一个(K,V)对的 DStream 上调用此函数,会返回一个新(K,V)对的 DStream,此处通过对滑动窗口中批次数据使用 reduce 函数来整合每个 key 的 value 值。
reduceByKeyAndWindow(func, invFunc, windowLength, slideInterval, [numTasks])
这个函数是上述函数的变化版本,每个窗口的 reduce 值都是通过用前一个窗的 reduce 值来递增计算。
通过 reduce 进入到滑动窗口数据并”反向 reduce”离开窗口的旧数据来实现这个操作。
一个例子是随着窗口滑动对 keys 的“加”“减”计数。
通过前边介绍可以想到,这个函数只适用于”可逆的 reduce 函数”,也就是这些 reduce 函数有相应的”反 reduce”函数(以参数 invFunc 形式传入)。
// 窗口的范围应该是采集周期的整数倍
// 窗口可以滑动的,但是默认情况下,一个采集周期进行滑动
// 这样的话,可能会出现重复数据的计算,为了避免这种情况,可以改变滑动的滑动(步长)
val windowDS: DStream[(String, Int)] = wordToOne.window(Seconds(6), Seconds(6))
// 需要设置检查点保存位置
ssc.checkpoint("cp")
// 当窗口范围比较大,但是滑动幅度比较小,那么可以采用增加数据和删除数据的方式无需重复计算,提升性能。
val windowDS: DStream[(String, Int)] =
wordToOne.reduceByKeyAndWindow(
(x:Int, y:Int) => { x + y},
(x:Int, y:Int) => {x - y},
Seconds(9), Seconds(3))
DStream 输出
输出操作指定了对流数据经转化操作得到的数据所要执行的操作(例如把结果推入外部数据库或输出到屏幕上)。
与RDD 中的惰性求值类似,如果一个 DStream 及其派生出的 DStream 都没有被执行输出操作,那么这些 DStream 就都不会被求值。
如果 StreamingContext 中没有设定输出操作,整个 context 就都不会启动。
print()
在运行流程序的驱动结点上打印 DStream 中每一批次数据的最开始 10 个元素。
这用于开发和调试。在 Python API 中,同样的操作叫 print()
saveAsTextFiles(prefix, [suffix])
以 text 文件形式存储这个 DStream 的内容
每一批次的存储文件名基于参数中的 prefix 和 suffix。”prefix-Time_IN_MS[.suffix]”
saveAsObjectFiles(prefix, [suffix])
以 Java 对象序列化的方式将 Stream 中的数据保存为SequenceFiles
每一批次的存储文件名基于参数中的为"prefix-TIME_IN_MS[.suffix]"
saveAsHadoopFiles(prefix, [suffix])
将 Stream 中的数据保存为 Hadoop files.
每一批次的存储文件名基于参数中的为"prefix-TIME_IN_MS[.suffix]"。
foreachRDD(func)
这是最通用的输出操作,即将函数 func 用于产生于 stream 的每一个RDD。
其中参数传入的函数 func 应该实现将每一个 RDD 中数据推送到外部系统,如将RDD 存入文件或者通过网络将其写入数据库。
注意:
连接不能写在 driver 层面(序列化)
如果写在 foreach 则每个 RDD 中的每一条数据都创建,得不偿失
增加 foreachPartition,在分区创建(获取)
优雅关闭
流式任务需要 7*24 小时执行,但是有时涉及到升级代码需要主动停止程序,但是分布式程序,没办法做到一个个进程去杀死,所有配置优雅的关闭就显得至关重要了。
示例:使用外部文件系统来控制内部程序关闭
class MonitorStop(ssc: StreamingContext) extends Runnable {
override def run(): Unit = {
val fs: FileSystem = FileSystem.get(new URI("hdfs://linux1:9000"), new Configuration(), "atguigu")
while (true) {
try
Thread.sleep(5000)
catch {
case e: InterruptedException => e.printStackTrace()
}
val state: StreamingContextState = ssc.getState
val bool: Boolean = fs.exists(new Path("hdfs://linux1:9000/stopSpark"))
if (bool) {
if (state == StreamingContextState.ACTIVE) {
ssc.stop(stopSparkContext = true, stopGracefully = true)
System.exit(0)
}
}
}
}
}
def main(args: Array[String]): Unit = {
// 在createSSC()方法中创建上下文和操作
val ssc: StreamingContext = StreamingContext.getActiveOrCreate("./ck", () => createSSC())
// 启动监视线程,获取信号后优雅的关闭
new Thread(new MonitorStop(ssc)).start()
// 启动Stream
ssc.start()
// 等待结束
ssc.awaitTermination()
}