赞
踩
Spark Streaming用于流式数据的处理。Spark Streaming支持的数据输入源很多,例如:Kafka、Flume、Twitter、ZeroMQ和简单的TCP套接字等等。数据输入后可以用Spark的高度抽象原语如:map、reduce、join、window等进行运算。而结果也能保存在很多地方,如HDFS,数据库等。
数据处理延迟方式
实时:数据处理在毫秒级别,秒
离线:数据处理延迟以小时,天为单位
数据处理的方式
流式:一个一个数据进行处理
批处理:一批一批数据进行处理
SparkCore: 离线数据分析框架|批处理
SparkStreaming: 基于SparkCore来完成实时数据处理分析(执行场景在离线批处理和实时流式之间,可以称为准实 时,微批次数据处理框架)
调整数据采集能力与数据消费能力
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,即不启用。
需求:使用netcat工具向9999端口不断的发送数据,通过SparkStreaming读取端口数据并统计不同单词出现的次数
添加依赖关系
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-streaming_2.12</artifactId>
<version>3.0.0</version>
</dependency>
idea代码
package com.pihao.streaming import org.apache.spark.SparkConf import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream} import org.apache.spark.streaming.{Seconds, StreamingContext} object WordCount02 { def main(args: Array[String]): Unit = { //TODO SparkStreaming流式数据处理 //TODO 建立环境 val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Streaming") //SparkStreaming环境对象的第二个参数表示数据采集周期 val ssc = new StreamingContext(sparkConf,Seconds(3)) //TODO 执行操作 //创建采集器(一行一行) val socketDS: ReceiverInputDStream[String] = ssc.socketTextStream("hadoop102",9999) //监听本机的9999端口 //拿到一个一个的单词 val wordDS: DStream[String] = socketDS.flatMap(_.split(" ")) val wordToCountDS: DStream[(String, Int)] = wordDS.map((_,1)).reduceByKey(_+_) wordToCountDS.print() //启动采集器 ssc.start() //等待结束 ssc.awaitTermination() } }
在hadoop102启动nc服务,然后启动idea
# nc -lk 9999
# nc -lp 9999
[atguigu@hadoop102 ~]$ nc -lk 9999
pihao
hello
world
测试过程中,可以通过使用ssc.queueStream(queueOfRDDs)来创建DStream,每一个推送到这个队列中的RDD,都会作为一个DStream处理。
案例实操
循环创建几个RDD,将RDD放入队列。通过SparkStream创建Dstream,计算WordCount
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() } }
自定义一个数据源,获取其中的数据
package com.pihao.streaming import java.util.concurrent.TimeUnit import org.apache.spark.SparkConf import org.apache.spark.storage.StorageLevel import org.apache.spark.streaming.{Seconds, StreamingContext} import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream} import org.apache.spark.streaming.receiver.Receiver import scala.util.Random object SparkStreaming_DIY { def main(args: Array[String]): Unit = { val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Streaming") val ssc = new StreamingContext(sparkConf,Seconds(3)) //执行操作 val ds: ReceiverInputDStream[String] = ssc.receiverStream(new MyReceiver()) //使用自己定义的接收器 ds.print() ssc.start() ssc.awaitTermination() } /** * 自定义数据采集器 * 1.继承Receiver,定义泛型,并指定存储级别 * 2.重写方法 * onStart * onStop */ class MyReceiver extends Receiver[String](StorageLevel.MEMORY_ONLY){ private var flag = true //开启接受 override def onStart(): Unit = { new Thread(new Runnable { override def run(): Unit = { while(flag){ val i: Int = new Random().nextInt(100) store(i+"") TimeUnit.NANOSECONDS.sleep(500) } } }).start() } //关闭接收 override def onStop(): Unit = { flag = false } } }
1.ReceiverAPI:需要一个专门的Executor去接收数据,然后发送给其他的Executor做计算。存在的问题,接收数据的Executor和计算的Executor速度会有所不同,特别在接收数据的Executor速度大于计算的Executor速度,会导致计算数据的节点内存溢出。早期版本中提供此方式,当前版本不适用
2.DirectAPI:是由计算的Executor来主动消费Kafka的数据,速度由自身控制。(采集和计算都在一个节点,容易控制)
需求:通过SparkStreaming从Kafka读取数据,并将读取过来的数据做计算,最终打印到控制台。
提前创建kafka主题
启动zookeeper集群
启动kafka集群
# 创建sparkstreaming的topic
[atguigu@hadoop102 ~]$ kafka-topics.sh --bootstrap-server hadoop102:9092 --create --topic sparkstreaming --partitions 3 --replication-factor 2
[atguigu@hadoop102 ~]$ kafka-topics.sh --bootstrap-server hadoop102:9092 --list
__consumer_offsets
sparkstreaming
[atguigu@hadoop102 ~]$
添加依赖
<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>
编写代码
package com.pihao.streaming import org.apache.kafka.clients.consumer.{ConsumerConfig, ConsumerRecord} import org.apache.spark.SparkConf import org.apache.spark.streaming.{Seconds, StreamingContext} import org.apache.spark.streaming.dstream.{DStream, InputDStream} import org.apache.spark.streaming.kafka010.{ConsumerStrategies, KafkaUtils, LocationStrategies} object KafkaStream { def main(args: Array[String]): Unit = { val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Streaming") val ssc = new StreamingContext(sparkConf,Seconds(3)) //TODO 执行操作 //定义kafka的连接配置 val kafkaPara: Map[String, Object] = Map[String, Object]( ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG -> "hadoop102:9092,hadoop103:9092,hadoop104:9092", ConsumerConfig.GROUP_ID_CONFIG -> "spark", "key.deserializer" -> "org.apache.kafka.common.serialization.StringDeserializer", "value.deserializer" -> "org.apache.kafka.common.serialization.StringDeserializer" ) //Kafka专门用于实时数据生成,提供了很多封装类 //ConsumerRecord是一个KV val kafkaDStream: InputDStream[ConsumerRecord[String, String]] = KafkaUtils.createDirectStream[String, String](ssc, LocationStrategies.PreferConsistent, //任务与采集器的位置关系,当前表示自动选择,也可以指定哪个broker ConsumerStrategies.Subscribe[String, String](Set("sparkstreaming"), kafkaPara)) //获取ConsumerRecord中的数据 val kafkaData: DStream[String] = kafkaDStream.map(_.value()) kafkaData.print() ssc.start() ssc.awaitTermination() } }
接收成功
DStream上的操作与RDD的类似,分为Transformations(转换)和Output Operations(输出)两种,此外转换操作中还有一些比较特殊的原语,如:updateStateByKey()、transform()以及各种Window相关的原语。
无状态转化操作就是把简单的RDD转化操作应用到每个批次上,也就是转化DStream中的每一个RDD。
Transform允许DStream上执行任意的RDD-to-RDD函数。即使这些函数并没有在DStream的API中暴露出来,通过该函数可以方便的扩展Spark API。该函数每一批次调度一次(周期性执行)。其实也就是对DStream中的RDD应用转换。
object Transform { def main(args: Array[String]): Unit = { //创建SparkConf val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("WordCount") //创建StreamingContext val ssc = new StreamingContext(sparkConf, Seconds(3)) //创建DStream val lineDStream: ReceiverInputDStream[String] = ssc.socketTextStream("host", 9999) //在某些情况,DS对象不能执行所有的操作 //如果想要进行特殊操作,那么可以直接通过底层的RDD进行 val wordAndCountDStream: DStream[(String, Int)] = lineDStream.transform(rdd => { val words: RDD[String] = rdd.flatMap(_.split(" ")) val wordAndOne: RDD[(String, Int)] = words.map((_, 1)) val value: RDD[(String, Int)] = wordAndOne.reduceByKey(_ + _) value }) //打印 wordAndCountDStream.print //启动 ssc.start() ssc.awaitTermination() } }
两个流之间的join需要两个流的批次大小一致,这样才能做到同时触发计算。计算过程就是对当前批次的两个流中各自的RDD进行join,与两个RDD的join效果相同。
import org.apache.spark.SparkConf import org.apache.spark.streaming.{Seconds, StreamingContext} import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream} object JoinTest { def main(args: Array[String]): Unit = { //1.创建SparkConf val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("JoinTest") //2.创建StreamingContext val ssc = new StreamingContext(sparkConf, Seconds(5)) //3.从端口获取数据创建流 val lineDStream1: ReceiverInputDStream[String] = ssc.socketTextStream("linux1", 9999) val lineDStream2: ReceiverInputDStream[String] = ssc.socketTextStream("linux2", 8888) //4.将两个流转换为KV类型 val wordToOneDStream: DStream[(String, Int)] = lineDStream1.flatMap(_.split(" ")).map((_, 1)) val wordToADStream: DStream[(String, String)] = lineDStream2.flatMap(_.split(" ")).map((_, "a")) //5.流的JOIN val joinDStream: DStream[(String, (Int, String))] = wordToOneDStream.join(wordToADStream) //6.打印 joinDStream.print() //7.启动任务 ssc.start() ssc.awaitTermination() } }
有时,我们需要在DStream中跨批次维护状态(例如流计算中累加wordcount)。需要保存之前每次数据的聚合操作
package com.pihao.streaming import org.apache.spark.SparkConf import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream} import org.apache.spark.streaming.{Seconds, StreamingContext} object SparkStreaming_State { def main(args: Array[String]): Unit = { val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Streaming") val ssc = new StreamingContext(sparkConf,Seconds(3)) //检查点应该设置在hdfs中比较稳妥 ssc.checkpoint("cp") //TODO 执行操作 val socketDS: ReceiverInputDStream[String] = ssc.socketTextStream("hadoop102",9999) val wordDS: DStream[String] = socketDS.flatMap(_.split(" ")) val wordToOneDS: DStream[(String, Int)] = wordDS.map((_,1)) //reduceByKey方法属于无状态操作的方法 //updateStateByKey方法属于有状态操作的方法 //方法参数中的第一个参数,表示相同key的value序列 //方法参数中的第二个参数,表示相同key缓冲区的数据值 val state: DStream[(String, Int)] = wordToOneDS.updateStateByKey( (seq: Seq[Int], buffer: Option[Int]) => { val newValue: Int = seq.sum + buffer.getOrElse(0) Option(newValue) } ) state.print() ssc.start() ssc.awaitTermination() } }
所谓的窗口操作,其实就是将多个采集周期的数据一次性进行处理,而不是一个采集周期处理一次。
SparkStreaming总的窗口范围不能切断采集周期,可以是采集周期的整数倍
SparkStreaming总的窗口滑动幅度也应该是采集周期的整数倍
package com.pihao.streaming import org.apache.spark.SparkConf import org.apache.spark.streaming.{Seconds, StreamingContext} import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream} object SparkStreaming_window{ def main(args: Array[String]): Unit = { val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Streaming") val ssc = new StreamingContext(sparkConf,Seconds(3)) //TODO 执行操作 val socketDS: ReceiverInputDStream[String] = ssc.socketTextStream("hadoop102",9999) val wordDS: DStream[String] = socketDS.flatMap(_.split(" ")) val wordToOneDS: DStream[(String, Int)] = wordDS.map((_,1)) //window方法可以传递一个参数,表示窗口的范围(时间周期,应该为采集周期的整数倍) //window方法可以传递两个参数, //第一个表示窗口的范围(时间周期,应该为采集周期的整数倍) //第二个表示窗口滑动的步长(时间周期,也是采集周期的整数倍,不传默认是一个一个采集周期) //窗口计算的时间以窗口步长做基础的 val windowDS: DStream[(String, Int)] = wordToOneDS.window(Seconds(6),Seconds(3)) val resultDS: DStream[(String, Int)] = windowDS.reduceByKey(_+_) resultDS.print() ssc.start() ssc.awaitTermination() } }
增量计算 = 当前窗口的值+ 新进入到窗口的数据-排除掉窗口的数据
使用场合:适合在窗口范围大,滑动浮动小的场合,这样就会有大量的重复数据
package com.pihao.streaming import org.apache.spark.SparkConf import org.apache.spark.streaming.{Seconds, StreamingContext} import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream} object Sparkstream_window2 { def main(args: Array[String]): Unit = { val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Streaming") val ssc = new StreamingContext(sparkConf,Seconds(3)) ssc.checkpoint("cp") //TODO 执行操作 val socketDS: ReceiverInputDStream[String] = ssc.socketTextStream("hadoop102",9999) val wordDS: DStream[String] = socketDS.flatMap(_.split(" ")) val wordToOneDS: DStream[(String, Int)] = wordDS.map((_,1)) //增量计算 //需要设定检查点路径 val windowDS: DStream[(String, Int)] = wordToOneDS.reduceByKeyAndWindow( (x: Int, y: Int) => { x + y }, (x: Int, y: Int) => { x - y }, Seconds(6), Seconds(3) ) windowDS.print() ssc.start() ssc.awaitTermination() } }
输出操作指定了对流数据经转化操作得到的数据所要执行的操作(例如把结果推入外部数据库或输出到屏幕上)。与RDD中的惰性求值类似,如果一个DStream及其派生出的DStream都没有被执行输出操作,那么这些DStream就都不会被求值。如果StreamingContext中没有设定输出操作,整个context就都不会启动。
#1 print():在运行流程序的驱动结点上打印DStream中每一批次数据的最开始10个元素。这用于开发和调试。在Python API中,同样的操作叫print()。
#2 saveAsTextFiles(prefix, [suffix]):以text文件形式存储这个DStream的内容。每一批次的存储文件名基于参数中的prefix和suffix。”prefix-Time_IN_MS[.suffix]”。
#3 saveAsObjectFiles(prefix, [suffix]):以Java对象序列化的方式将Stream中的数据保存为 SequenceFiles . 每一批次的存储文件名基于参数中的为"prefix-TIME_IN_MS[.suffix]". Python中目前不可用。
#4 saveAsHadoopFiles(prefix, [suffix]):将Stream中的数据保存为 Hadoop files. 每一批次的存储文件名基于参数中的为"prefix-TIME_IN_MS[.suffix]"。Python API 中目前不可用。
#5 foreachRDD(func):这是最通用的输出操作,即将函数 func 用于产生于 stream的每一个RDD。其中参数传入的函数func应该实现将每一个RDD中数据推送到外部系统,如将RDD存入文件或者通过网络将其写入数据库。
通用的输出操作foreachRDD(),它用来对DStream中的RDD运行任意计算。这和transform() 有些类似,都可以让我们访问任意RDD。在foreachRDD()中,可以重用我们在Spark中实现的所有行动操作。比如,常见的用例之一是把数据写到诸如MySQL的外部数据库中。
注意:
1)连接不能写在driver层面(序列化)
2)如果写在foreach则每个RDD中的每一条数据都创建,得不偿失;
增加foreachPartition,在分区创建(获取)。
流式任务需要7*24小时执行,但是有时涉及到升级代码需要主动停止程序,但是分布式程序,没办法做到一个个进程去杀死,所有配置优雅的关闭就显得至关重要了。
使用外部文件系统来控制内部程序关闭。
package com.pihao.streaming import org.apache.spark.SparkConf import org.apache.spark.streaming.{Seconds, StreamingContext, StreamingContextState} import org.apache.spark.streaming.dstream.{ ReceiverInputDStream} object SparkStreaming_Close { def main(args: Array[String]): Unit = { val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Streaming") val ssc = new StreamingContext(sparkConf,Seconds(3)) //TODO 执行操作 val socketDS: ReceiverInputDStream[String] = ssc.socketTextStream("hadoop102",9999) socketDS.print() //stop()方法主要用于停止数据采集和Driver的调度 //不可能启动采集后马上停止,所以一般在业务更新或者逻辑更新的场合停止 //在main线程中停止是不现实的。应该启动一个新的线程来执行停止操作 new Thread(new Runnable { override def run(): Unit = { while(true){ Thread.sleep(10000) //时间 //这个时间应该是判断数据处理是否可以继续的标记 //这个标记应该多线程都可以访问 //这个标记一般情况放在第三方的系统中(mysql,redis) val state: StreamingContextState = ssc.getState() //装填 if (state == StreamingContextState.ACTIVE){ ssc.stop(true,true) //优雅的关闭 System.exit(0) //关闭线程 } } } }).start() ssc.start() ssc.awaitTermination() } }
模拟实时生成数据,然后统计分析
package com.pihao.main import java.util.Properties import org.apache.kafka.clients.producer.{KafkaProducer, ProducerConfig, ProducerRecord} import scala.collection.mutable.ListBuffer import scala.util.Random object SparkStreaming_MockData { def main(args: Array[String]): Unit = { // 创建配置对象 val prop = new Properties() // 添加配置 prop.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop102:9092") prop.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer") prop.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer") val producer = new KafkaProducer[String,String](prop) val topic = "sparkstreaming" val areas = ListBuffer("华北","华东","华南") val citys = ListBuffer("北京","上海","深圳") while(true){ Thread.sleep(2000) //生成数据 for (i <- 1 to new Random().nextInt(20)){ val area: String = areas(new Random().nextInt(3)) val city: String = citys(new Random().nextInt(3)) val userId = new Random().nextInt(10) val adId = new Random().nextInt(10) val message = s"${System.currentTimeMillis()} ${area} ${city} ${userId} ${adId}" val record = new ProducerRecord[String,String](topic,message) producer.send(record) } } } }
package com.pihao.main import org.apache.kafka.clients.consumer.{ConsumerConfig, ConsumerRecord} import org.apache.spark.SparkConf import org.apache.spark.rdd.RDD import org.apache.spark.streaming.{Seconds, StreamingContext} import org.apache.spark.streaming.dstream.{DStream, InputDStream} import org.apache.spark.streaming.kafka010.{ConsumerStrategies, KafkaUtils, LocationStrategies} object KafkaStream { def main(args: Array[String]): Unit = { val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Streaming") val ssc = new StreamingContext(sparkConf,Seconds(3)) //TODO 执行操作 //定义kafka的连接配置 val kafkaPara: Map[String, Object] = Map[String, Object]( ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG -> "hadoop102:9092,hadoop103:9092,hadoop104:9092", ConsumerConfig.GROUP_ID_CONFIG -> "spark", "key.deserializer" -> "org.apache.kafka.common.serialization.StringDeserializer", "value.deserializer" -> "org.apache.kafka.common.serialization.StringDeserializer" ) //Kafka专门用于实时数据生成,提供了很多封装类 val kafkaDStream: InputDStream[ConsumerRecord[String, String]] = KafkaUtils.createDirectStream[String, String](ssc, LocationStrategies.PreferConsistent, //任务与采集器的位置关系,当前表示自动选择,也可以指定哪个broker ConsumerStrategies.Subscribe[String, String](Set("sparkstreaming"), kafkaPara)) //获取广告点击的数据 val kafkaData: DStream[AdvClickData] = kafkaDStream.map( data => { val kafkaVal: String = data.value() val datas: Array[String] = kafkaVal.split(" ") AdvClickData(datas(0),datas(1),datas(2),datas(3),datas(4)) } ) // todo 1.周期性获取数据库中最新的黑名单数据 val reduceDS: DStream[((String, String, String), Int)] = kafkaData.transform( rdd => { // todo 2.判断当前周期内的数据是否已经在黑名单中 // 查询数据库操作select val blackList = List[String]() val filterRDD: RDD[AdvClickData] = rdd.filter( data => !blackList.contains(data.userId) ) //todo 3.将一个周期内的数据进行统计 filterRDD.map( data => { //将数据组装返回 timestamp这里要换成日期格式 ((data.timestamp, data.userId, data.adId), 1) } ).reduceByKey(_ + _) } ) //todo 4.判断统计的结果是否超过阈值 reduceDS.foreachRDD( rdd => { rdd.foreach { case ((timestamp, userId, adId), sum) => { //如果当前采集周期内的sum都大于100,那么将userId就直接拉入黑名单 if (sum>=100){ //insert into }else{ val oldStateVal = 0 //select 根据userId查询mysql中旧的值 val newStateVal = 0 + sum if (newStateVal >= 100){ // insert into }else{ // update 根据数据库newStateVal } } } } } ) ssc.start() ssc.awaitTermination() } case class AdvClickData(timestamp: String, area: String, city: String, userId: String, adId: String) }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。