1、SparkStreaming的概述
1、什么是SparkStreaming?
SparkStreaming是一个微批次的流式处理的spark组件
2、什么是DStream
SparkStreaming是等待接收一定时间后,将这段时间的数据统一处理,这段时间内接收的数据称之为一个批次,sparkstreaming会将一个批次的数据封装成RDD。
DStream是离散化流,DStream中流动的是一个批次的RDD,RDD里面时一个个批次的数据。
3、背压机制
sparkstreaming每次是处理一个批次的数据[sparkstreaming处理批次数据:是先处理前一个批次,再处理后一个批次,批次之间执行是串行]
如果一个批次计算时间>批次时间,此时会出现批次积压的问题。
如果一个批次的计算时间<批次时间,会出现资源浪费的问题。
sparkstreaming提供了一个背压机制,能够根据数据的处理时间动态的调整数据的拉取速率。
可以在创建StreamingContext的时候再SparkConf中通过spark.streaming.backpressure.enabled开启。
4、WC案例
def main(args: Array[String]): Unit = {
//1、创建StreamingContext
val conf = new SparkConf().setMaster("local[4]").setAppName("test")
val ssc = new StreamingContext(conf,Seconds(5))
ssc.sparkContext.setLogLevel("error")
//2、读取数据
val ds = ssc.socketTextStream("hadoop102",9999)
//3、切割+压平
val ds2 = ds.flatMap(x=>{
//Thread.sleep(6000)
x.split(" ")
})
//4、统计单词个数
val ds3 = ds2.map( (_,1 ) )
val ds4 = ds3.reduceByKey(_+_)
//5、结果展示
ds4.print()
//6、启动streaming程序
ssc.start()
//7、阻塞
ssc.awaitTermination()
}
2、DStream的创建
1、RDD队列数据源
val queue:mutable.Queue[RDD[...]] = ...
ssc.queueStream(queue[,oneAtTime=true])
oneAtTime=true代表一个批次只从队列中取出一个RDD处理
oneAtTime=false代表一个批次将批次时间内所有RDD取出union之后统一处理
2、自定义数据源
1、自定义:
1、创建class继承Receiver[数据类型](存储级别)
2、重写抽象方法[onStart、onStop]
2、使用:
ssc.receiverStream(自定义数据对象)
代码
class MySocketSource(val host:String,val port:Int) extends Receiver[String](StorageLevel.MEMORY_AND_DISK) {
/**
* receiver启动的时候调用
*/
override def onStart(): Unit = {
new Thread(){
override def run(): Unit = {
receive()
}
}.start()
}
/**
* 读取数据
*/
def receive(): Unit ={
val socket = new Socket(host,port)
val br = new BufferedReader(new InputStreamReader(socket.getInputStream))
var line:String = br.readLine()
while( line !=null ){
//保存数据等到积累一个批次之后统一处理
store(line)
line = br.readLine()
}
br.close()
socket.close()
}
/**
* receiver停止时调用
*/
override def onStop(): Unit = {
}
}
3、kafka数据源
kafka数据源采用direct模式 (spark3.0.0以上版本只有Direct模式)
DirectAPI由Executor拉取Topic分区数据,速度由自身控制] ReceiverAPI[需要一个专门的Executor去接收数据,然后发送给其他的Executor计算]
//设置拉取数据的所有的topic名称
val topics = Array("spark_streaming")
//设置消费者组的配置
val kafkaparams = Map[String,Object](
//指定kafka集群地址
"bootstrap.servers"-> "hadoop102:9092,hadoop103:9092,hadoop104:9092",
//指定key的反序列化器
"key.deserializer"-> "org.apache.kafka.common.serialization.StringDeserializer",
//指定value的反序列化器
"value.deserializer" -> "org.apache.kafka.common.serialization.StringDeserializer",
//指定消费者组的id
"group.id" -> "gg1",
//指定消费者组第一次消费topic的时候从哪个位置开始消费
"auto.offset.reset"->"earliest",
//是否自动提交offset
"enable.auto.commit" -> "true"
)
val ds = KafkaUtils.createDirectStream[String,String]
(ssc,
//LocationStrategies: PreferBrokers[执行器和kafka在一个节点,本地] PreferConsistent[所有的Executor一致性分配分区(均匀分配)]
LocationStrategies.PreferConsistent,
ConsumerStrategies.Subscribe[String,String](topics,kafkaparams))
3、DStream转换
状态: 之前批次的数据/结果
1、无状态转换[只计算当前批次]
map/flatMap/filter/reduceByKey/sortBy...
transform(func: RDD[DStream中元素类型]=>RDD[B]): 一对一转换[DStream中一个RDD转换之后得到新RDD]
transform里面的函数是针对每个批次的RDD操作
2、有状态转换[计算当前与之前批次/结果]
updateStateByKey(func: ( Seq[DStream Value值类型],Option[S] )=>Option[S]): 根据key分组统计每个key的全局[从启动到当前批次]的结果
updateStateByKey函数第一个参数是代表分组之后每个key所有的value值的集合
updateStateByKey函数第二个参数是代表分组之后每个key之前批次的统计结果
updateStateByKey代码
val ds4 = ds3.updateStateByKey[Int]( (currentBachValues:Seq[Int], state:Option[Int]) =>{
//得到当前批次该单词总次数
val currentNum = currentBachValues.sum
//得到之前批次的该单词的总次数
val beforeNum = state.getOrElse(0)
Some( currentNum + beforeNum )
} )
window(窗口长度,滑动长度): 窗口[sparkstreaming会将多个批次的数据组成一个窗口统一计算]
窗口长度与滑动长度必须是批次时间的整数倍
window代码
val ds4 = ds3.window(Seconds(15),Seconds(5)).reduceByKey(_+_)
reduceByKeyAndWindow(func: (Value值类型,Value值类型)=>Value值类型,窗口长度,滑动长度):将窗口内的数据进行reduceByKey
reduceByKeyAndWindow( _+_ ,windowDuration = Seconds(15),Seconds(5)) //这里因为参数太多,不能每个都简写,需要指定为带名参数
reduceByKeyAndWindow代码
//窗口长度与滑动长度必须是批次时间整数倍
val ds4 = ds3.reduceByKeyAndWindow( _+_ ,windowDuration = Seconds(15),Seconds(5))
反向reduceByKeyAndWindow(划入func,划出func,窗口长度,滑动长度)
模拟场景: 窗口长度很长,滑动长度很小,此时上下两个窗口中包含大量重复批次,此时可以通过上一个创建结果-滑出批次+滑入批次计算出本批次的结果,从而提高计算效率
reduceByKeyAndWindow(
(a: Int, b: Int) => (a + b),
(x: Int, y: Int) => (x - y),
,windowDuration = Seconds(15),Seconds(5))
countByWindow(windowLength, slideInterval)[窗口长度,滑动长度]: 返回一个滑动窗口计数流中的元素个数;
reduceByWindow(func, windowLength, slideInterval)[聚合函数,窗口长度,滑动长度]: 通过使用自定义函数整合滑动区间流元素来创建一个新的单元素流;
3、foreachRDD(func)::是最通用的输出操作,将函数func用于产生DStream的每一个RDD。其中参数传入的函数func应该实现将每一个RDD中数据推送到外部系统,如将RDD存入文件或者写入数据库。
注:DStream算子中的函数参数如果是RDD,则函数不是在Task中执行的。只是构建好流程,触发了RDD的行动算子后,需将代码转成Job提交到Task,RDD的算子内的代码才是Task执行的逻辑。
4、优雅关闭
问题:如一个批次的数据处理到一半时把程序强制关闭,下次运行程序时已经处理了的数据这时已经拉不到了(相当于丢数据了),因为已经在Kafka内部Topic中保存了偏移量。
流式任务需要7*24小时执行,但是有时涉及到升级代码需要主动停止程序,但是分布式程序没办法做到一个个进程去杀死,所以需要配置优雅关闭。
关闭方式:
使用外部文件系统来控制内部程序关闭。
ssc.stop(stopSparkContext,stopGracefully):
stopSparkContext:[恒定为ture] 停止关联SparkContext
stopGracefully: 是否优雅的停止,等待所有接受到的数据处理完成
代码
//启动streaming程序
ssc.start()
//可以通过改变外部的动作来通知sparkstreaming停止程序
// 1、sparkstreaming可以监控mysql某个表某个字段的值,一旦外部改变该值则停止程序
// 2、sparkstreaming可以监控HDFS某个目录,一旦外部删除了该目录停止程序
//获取HDFS文件系统
val fs = FileSystem.get(new URI("hdfs://hadoop102:8020"),new Configuration())
val path = new Path("hdfs://hadoop102:8020/input")
while( fs.exists(path) ){
Thread.sleep(2000)
}
ssc.stop(true,true)