第一章 SparkStreaming 概述
1.1 Spark Streaming 是什么
sparkStreaming 用于流式数据处理,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在实时数据处理场景的一种封装。
1.2 Spark Streaming 的特点
- 易用
- 容错
- 易整合到Spark体系
1.3 SparkStreaming 架构
1.3.1 架构图
Spark Streaming架构图
1.3.2 背压机制
Spark Streaming执行过程中 ,由于接收器和执行器不在同一节点,所以无法保证内部数据的一致且高效,所以spark1.5以后Spark Streaming可以动态控制数据接收速率来适配集群数据处理能力。背压机制(即Spark Streaming Backpressure): 根据JobScheduler反馈作业的执行信息来动态调整Receiver数据接收率。
通过属性“spark.streaming.backpressure.enabled”来控制是否启用backpressure机制,默认值false,即不启用。
但是如果是SparkStreaming外部传输数据过快,那就只能增加节点
第二章Dstream入门
2.1WordCount案例
需求:使用netcat工具向9999端口不断的发送数据,通过SparkStreaming读取端口数据并统计不同单词出现的次数
- 添加依赖
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-streaming_2.12</artifactId>
<version>3.0.0</version>
</dependency>
- 编写代码
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("StreamWordCount")
val ssc = new StreamingContext(conf,Seconds(3))
//通过监控端口创建Dstream ,读取数据为一行一行
val lineStreams: ReceiverInputDStream[String] = ssc.socketTextStream("192.168.23.100",9999)
//切分数据
val wordStreams: DStream[String] = lineStreams.flatMap(_.split(" "))
//将单词映射成元组(word,1)并将相同的单词次数做统计
val wordAndCountStreams: DStream[(String, Int)] = wordStreams.map(
word => {
(word, 1)
}
).reduceByKey(_ + _)
wordAndCountStreams.print()
//启动SparkStreamingContext
ssc.start()
ssc.awaitTermination()
}
- 启动程序并通过netcat发送数据:
nc -lk 9999
hello spark
第三章 Dstream 创建
3.1 RDD队列
3.1.1 用法及说明
测试过程中,可以通过使用ssc.queueStream(queueOfRDDs)来创建DStream,每一个推送到这个队列中的RDD,都会作为一个DStream处理。
3.1.2 案例实操
需求:循环创建几个RDD,将RDD放入队列。通过SparkStream创建Dstream,计算WordCount
- 编写代码
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("QueueWordCount")
val ssc = new StreamingContext(conf,Seconds(4))
// 创建RDD队列
val rddQueue = new mutable.Queue[RDD[Int]]()
//创建QueueInputDStream
val inputStream: InputDStream[Int] = ssc.queueStream(rddQueue,oneAtATime = false)
//处理队列中的RDD数据
val reduceStream: DStream[(Int, Int)] = inputStream.map((_,1)).reduceByKey(_+_)
reduceStream.print()
//启动SparkStreamingContext
ssc.start()
//循环创建RDD并向RDD队列放入
for (i <- 1 to 5 ){
val rdd: RDD[Int] = ssc.sparkContext.makeRDD(1 to 100 ,2)
rddQueue.enqueue(rdd)
Thread.sleep(2000)
}
ssc.awaitTermination()
}
3.2 自定义数据源
3.2.1 用法及说明
需要继承Receiver,并实现onStart、onStop方法来自定义数据源采集。
3.2.2 案例实操
需求:自定义数据源,实现监控某个端口号,获取该端口号内容。
//自定义数据源
class CustomerReceiver(host: String,port: Int)extends Receiver[String](StorageLevel.MEMORY_ONLY){
//最初启动的时候,调用该方法,作用为:读取数据并发给Spark
override def onStart(): Unit = {
new Thread("Socket Receiver") {
override def run() {
receive()
}
}.start()
//读取数据并发给Spark
def receive():Unit = {
//创建一个Socket
val socket = new Socket(host,port)
//定义一个变量,用来接收端口传过来的数据
var input : String = null
//创建一个BufferedReader用于读取端口传来的数据
val reader = new BufferedReader(new InputStreamReader(socket.getInputStream,StandardCharsets.UTF_8))
//读取数据
input = reader.readLine()
//当receiver没有关闭并且输入数据不为空,则循环发送数据给Spark
while (!isStopped() && input != null){
store(input)
input = reader.readLine()
}
//跳出循环则关闭资源
reader.close()
socket.close()
//重启任务
restart("restart")
}
}
override def onStop(): Unit = {}
}
//使用数据源
object SparkStreaming03_FlileStream {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("StreamWordCount")
val ssc = new StreamingContext(conf,Seconds(3))
//创建自定义receiver的Streaming
val lineStream: ReceiverInputDStream[String] = ssc.receiverStream(new CustomerReceiver("192.168.23.100",9999))
//处理读到的数据
val wordStream = lineStream.flatMap(_.split("\t"))
val reduceStream: DStream[(String, Int)] = wordStream.map((_,1)).reduceByKey(_+_)
reduceStream.print()
//启动SparkStreamingContext
ssc.start()
ssc.awaitTermination()
}
3.3Kafka数据源(重要)
3.3.1 版本选型
ReceiverAPI:需要一个专门的Executor去接收数据,然后发送给其他的Executor做计算。存在的问题,接收数据的Executor和计算的Executor速度会有所不同,特别在接收数据的Executor速度大于计算的Executor速度,会导致计算数据的节点内存溢出。早期版本中提供此方式,当前版本不适用
DirectAPI:是由计算的Executor来主动消费Kafka的数据,速度由自身控制。
3.3.2Kafka 0-10 Direct模式
- 需求:通过SparkStreaming从Kafka读取数据,并将读取过来的数据做计算,最终打印到控制台。
- 导入依赖
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-streaming-kafka-0-10_2.12</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.10.1</version>
</dependency>
- 编写代码
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("KafkaStreamWordCount")
val ssc = new StreamingContext(conf,Seconds(3))
//读取Kafka数据创建DStream
val kafkaDStream: InputDStream[ConsumerRecord[String, String]] = KafkaUtils.createDirectStream(
ssc,
LocationStrategies.PreferConsistent,
ConsumerStrategies.Subscribe(List("itguigu"), Map(
"bootstrap.servers" -> "192.168.23.100:9092,192.168.23.110:9092,192.168.23.120:9092",
"group.id" -> "itguigu",
"auto.offset.reset" -> "earliest",
"enable.auto.commit" -> (false:lang.Boolean),
"key.deserializer" -> classOf[StringDeserializer],
"value.deserializer" -> classOf[StringDeserializer]
)))
//处理读到的数据
val mapStream: DStream[String] = kafkaDStream.map(
result => {
result.value()
}
)
mapStream.flatMap(_.split(" ")).map((_,1)).reduceByKey(_+_).print()
//启动SparkStreamingContext
ssc.start()
ssc.awaitTermination()
}
第4章 DStream转换
DStream上的操作与RDD的类似,分为Transformations(转换)和Output Operations(输出)两种,此外转换操作中还有一些比较特殊的原语,如:updateStateByKey()、transform()以及各种Window相关的原语。
4.1 无状态转化操作
无状态转化操作就是把简单的RDD转化操作应用到每个批次上,也就是转化DStream中的每一个RDD。部分无状态转化操作列在了下表中。注意,针对键值对的DStream转化操作(比如 reduceByKey())要添加import StreamingContext._才能在Scala中使用。
需要注意的是,尽管这些函数看起来像作用在整个流上,但是事实上DStream在内部是由许多RDD(批次)组成,且无状态转化操作是分别应用到每个RDD上的。
例如:热度测ByKey()会归约每个时间区中的数据,但不会归约不同时间区之间的数据
4.1.1 Transform
Transform 允许DStream上执行任意的RDD-to-RDD函数,即使这些函数并没有在DStream的API中暴露出来,通过该函数可以方便的扩展Spark API 该函数每一批次调度一次。其实也就是对DStream中的RDD应用转换
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("StreamTransform")
val ssc = new StreamingContext(conf,Seconds(3))
//通过监控端口创建Dstream ,读取数据
val lineDStreams: ReceiverInputDStream[String] = ssc.socketTextStream("192.168.23.100",9999)
//转换RDD
val wordCountStreams: DStream[(String, Int)] = lineDStreams.transform(rdd => {
val words: RDD[String] = rdd.flatMap(_.split(" "))
words.map((_, 1)).reduceByKey(_ + _)
})
wordCountStreams.print()
//启动SparkStreamingContext
ssc.start()
ssc.awaitTermination()
}
说白了,无状态转换就是对每一个RDD进行直接操作
4.1.2join
两个流之间的join需要两个流的批次大小一致,这样才能做到同时触发计算。计算过程就是对当前批次的两个流中各自的RDD进行join,与两个RDD的join效果相同。
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("StreamJoin")
val ssc = new StreamingContext(conf,Seconds(5))
//通过监控端口创建Dstream ,读取数据
val lineDStream1: ReceiverInputDStream[String] = ssc.socketTextStream("192.168.23.100",9999)
val lineDStream2: ReceiverInputDStream[String] = ssc.socketTextStream("192.168.23.110",8888)
//将两个流处理为KV类型
val wordToOneDStream: DStream[(String, Int)] = lineDStream1.flatMap(_.split(" ")).map((_,1))
val wordToADStream: DStream[(String, String)] = lineDStream2.flatMap(_.split(" ")).map((_,"a"))
// 流之间进行join
val joinDStream: DStream[(String, (Int, String))] = wordToOneDStream.join(wordToADStream)
joinDStream.print()
//启动SparkStreamingContext
ssc.start()
ssc.awaitTermination()
}
4.2 有状态转化操作
所谓有状态转化,即在RDD操作时,要用到前面RDD计算的结果,将RDD视为整体,不独立
4.2.1UpdateStateByKey
UpdateStateByKey原语用于记录历史记录,有时,我们需要在DStream中跨批次维护状态(例如流计算中累加wordcount)。针对这种情况,updateStateByKey()为我们提供了对一个状态变量的访问,用于键值对形式的DStream。给定一个由(键,事件)对构成的 DStream,并传递一个指定如何根据新的事件更新每个键对应状态的函数,它可以构建出一个新的 DStream,其内部数据为(键,状态) 对。
updateStateByKey()的结果会是一个全新的DStream 。其内部的RDD序列是由每个时间区间对应的(键,状态)对组成
updateStateByKey操作使得我们可以在用新信息进行更新时保持任意的状态。为使用这个功能,需要做下面两步:
- 定义状态,状态可以是一个任意的数据类型。
- 定义状态更新函数,用此函数阐明如何使用之前的状态和来自输入流的新值对状态进行更新。
编写代码:
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("StreamJoin")
val ssc = new StreamingContext(conf,Seconds(5))
//如果使用有状态转换,就要设定检查节点,方便保存和计算
ssc.checkpoint("./ck")
val DStreams: ReceiverInputDStream[String] = ssc.socketTextStream("192.168.23.100",9999)
val pairs: DStream[(String, Int)] = DStreams.flatMap(_.split(" ")).map((_,1))
// 使用updateStateByKey来更新状态,统计从运行开始以来 的单词次数
val stateDStream: DStream[(String, Int)] = pairs.updateStateByKey[Int](
(value: Seq[Int], state: Option[Int]) => {
val valueCount: Int = value.foldLeft(0)(_ + _)
val oldConut: Int = state.getOrElse(0)
Some(valueCount + oldConut)
})
stateDStream.print()
//启动SparkStreamingContext
ssc.start()
ssc.awaitTermination()
}
4.2.2 WindowOperations
window Operations可以设置窗口大小和华东窗口间隔来动态获取当前Streaming的运行状态。所有基于窗口的操作都需要两参数,分别为窗口时长以及滑动步长。
- 窗口时长 : 计算内容的时间范围
- 滑动步长 : 隔多久触发一次计算
** 注意:这两者都必须为采集周期大小的整数倍
如:3秒一个批次,窗口12秒,滑步6秒。
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("StreamJoin")
val ssc = new StreamingContext(conf,Seconds(3))
//如果使用有状态转换,就要设定检查节点,方便保存和计算
ssc.checkpoint("./ck")
val DStreams: ReceiverInputDStream[String] = ssc.socketTextStream("192.168.23.100",9999)
val pairs: DStream[(String, Int)] = DStreams.flatMap(_.split(" ")).map((_,1))
val wordCountByWindow: DStream[(String, Int)] = pairs.reduceByKeyAndWindow((a:Int,b:Int)=>(a+b),Seconds(12),Seconds(6))
wordCountByWindow.print()
//启动SparkStreamingContext
ssc.start()
ssc.awaitTermination()
}
关于window的操作还有如下方法:
- window(windowLength,sideInterval):基于对源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形式传入)。如前述函数,reduce任务的数量通过可选参数来配置。
第五章 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]". Python中目前不可用。
- saveAsHadoopFiles(prefix, [suffix]):将Stream中的数据保存为 Hadoop files. 每一批次的存储文件名基于参数中的为"prefix-TIME_IN_MS[.suffix]"。Python API 中目前不可用。
- foreachRDD(func):这是最通用的输出操作,即将函数 func 用于产生于 stream的每一个RDD。其中参数传入的函数func应该实现将每一个RDD中数据推送到外部系统,如将RDD存入文件或者通过网络将其写入数据库。
通用的输出操作foreachRDD(),它用来对DStream中的RDD运行任意计算。这和transform() 有些类似,都可以让我们访问任意RDD。在foreachRDD()中,可以重用我们在Spark中实现的所有行动操作。比如,常见的用例之一是把数据写到诸如MySQL的外部数据库中。
注意:
- 连接不能写在driver层面(序列化)
- 如果写在foreach则每个RDD中的每一条数据都创建,得不偿失;
- 增加foreachPartition,在分区创建(获取)。
第六章 优雅关闭
流式任务需要7*24小时执行,但是有时涉及到升级代码需要主动停止程序,但是分布式程序,没办法做到一个个进程去杀死,所有配置优雅的关闭就显得至关重要了。
使用外部文件系统来控制内部程序关闭。
MonitorStop:
import java.net.URI
import org.apache.hadoop.conf.Configuration
import org.apache.hadoop.fs.{FileSystem, Path}
import org.apache.spark.streaming.{StreamingContext, StreamingContextState}
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)
}
}
}
}
}