大数据9_03_SparkStreaming原理及应用

1 SparkStreaming概述

image-20201202114106953

SparkStreaming是一个微批次,准实时的数据处理框架。

SparkStreaming用于流式数据处理

1.1 流处理、批处理、实时处理、离线处理

从数据处理方式的角度:

  • 流式处理:一条数据一条数据的处理,就是流处理
  • 批量处理:一批数据一起处理

从数据处理延迟的角度:

  • 实时处理:延迟的时间以毫秒为单位 => Flink
  • 准实时处理:延迟的时间以秒、分钟为单位 => SparkStreaming
  • 离线处理:延迟的时间以小时、天为单位 => MR

SparkStreaming是一个微批次、准实时的数据处理框架。

  • SparkStreaming基于SparkCore进行流式数据处理的模块;
  • SparkCore中的RDD没有办法进行真正的流式数据处理,其实是批量数据处理。不过是很小的批量
  • SparkStreaming的目的是为了进行实时数据分析。
  • SparkStreaming批量处理的数据不能太大,所以称为微批次处理。
  • SparkStreaming无法真正实现实时数据处理,因为延迟比较高,但是因为一批数据量比较小,又达不到离线的范畴,所以称为准实时。

1.2 DStream

DStream(discretized stream),离散化流

和SparkCore中的RDD相似,SparkStreaming使用DStream离散化流,作为抽象表示。

DStream是随时间推移而收到的数据的序列。这些序列其实就是RDD组成的。

其实!DStream就是RDD在实时数据处理场景的一种封装。

1.3 SparkStreaming架构

①Receiver模式(早期版本)

需要一个专门的Executor去接收数据,然后发送给Driver,Driver再将数据按照分区,转换成Task分发给其他的Executor节点做计算。

扫描二维码关注公众号,回复: 12687132 查看本文章

这样会存在一个一个问题:接收数据的Executor和计算的Executor速度有所不同

  • 特别是接收数据的Executor速度快于计算数据的Executor,那么采集器会有大量的数据堆积,会导致接收数据的Executor可能内存溢出

image-20201202183142901

②Direct模式

计算和采集放在一个节点上,这样就很好控制采集和计算的速率。

  • LocationStrategy.PreferBrokers

    image-20201202184909188

  • LocationStrategies.PreferConsistent(大多数情况使用这种情况)在所有执行程序之间,Kafka根据内存等资源,自动的选择节点。

    image-20201202185145211

③背压机制

背压机制(即Spark Streaming Backpressure): 根据JobScheduler反馈作业的执行信息来动态调整Receiver数据接收率。

Spark1.5版本以前版本,为了限制Receiver的数据接收速率。可以通过设置动态配置参数“spark.streaming.receiver.maxRate”的值来实现,来适配当前的处理能力,防止内存溢出,但也会引入其他问题。

为了更好的协调数据接收速率与资源处理能力,1.5版本开始SparkStreaming可以动态的控制数据接收速率来适配集群处理数据的能力。

通过属性“spark.streaming.backpressure.enabled”来控制是否启用backpressure机制,默认值false,即不启用。

2 Dstream入门

2.1 WordCount案例实操

需求:使用netcat工具向9999端口不断的发送数据,通过SparkStreaming读取端口数据并统计不同单词出现的次数。

①添加依赖

<dependency>
    <groupId>org.apache.spark</groupId>
    <artifactId>spark-streaming_2.12</artifactId>
    <version>3.0.0</version>
</dependency>

②编写代码

  • 创建StreamingContext(参数1,参数2)

    • 参数1:SparkConf对象,Spark的配置信息。

    • 参数2:批处理的时间,需要传Duration (private val millis: Long),还可以传下面的三个:

      object Milliseconds {
               
               
        def apply(milliseconds: Long): Duration = new Duration(milliseconds)
      }
      object Seconds {
               
               
        def apply(seconds: Long): Duration = new Duration(seconds * 1000)
      }
      object Minutes {
               
               
        def apply(minutes: Long): Duration = new Duration(minutes * 60000)
      }
      
  • 启动SparkStreamingContext

    • ssc.start()启动采集器
  • ssc.awaitTermination()

    • 这是一个阻塞,等待采集器的结束才结束。因为SparkStreaming一直读取数据,需要一个阻塞控制。不然就不行。
object Spark02_Streaming {
    
    
  def main(args: Array[String]): Unit = {
    
    
    //1. 创建SparkConf对象,初始化Spark配置信息
    val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("StreamWordCount")
    //2. 创建StreamingContext对象
    //需要两个参数,第一个是SparkConf配置,第二个参数是批处理的时间:还有Milliseconds、Seconds、Minutes
    val ssc: StreamingContext = new StreamingContext(conf, Duration(3000))
    //3. 通过监控端口创建DStream,读取进来的数据是一行一行的
    val lineStreams: ReceiverInputDStream[String] = ssc.socketTextStream("hadoop102", 9999)
    //每一行数据做切分,形成一个个单词
    val wordStreams: DStream[String] = lineStreams.flatMap(_.split(" "))
    //将单词映射元组(word,1)
    val wordToOneStreams: DStream[(String, Int)] = wordStreams.map((_, 1))
    //4. 打印
    wordToOneStreams.print()
    //5. 启动SparkStreamingContext
    ssc.start()
    //这是一个阻塞,不然就不行
    ssc.awaitTermination()
  }
}

3 DStream创建(采集数据)

从2.3采集数据的结果都会返回一个DStream

3.1 从端口号采集

作用:用来监听端口

socketTextStream(参数1,参数2)

  • 参数1:表示机器名
  • 参数2:端口号
val lineStreams: ReceiverInputDStream[String] = ssc.socketTextStream("hadoop102", 9999)

3.2 从文件获取

作用:用来监听文件夹内文件内容变化

textFileStream(“参数”):参数表示:要监听的文件夹

object Spark03_Streaming_Dir {
    
    
  def main(args: Array[String]): Unit = {
    
    
    //初始化Spark配置
    val conf = new SparkConf().setMaster("local[*]").setAppName("SparkStreamingRDD")
    //初始化SparkStreamingContext
    val ssc = new StreamingContext(conf, Duration(3000))

    //TODO 从文件获取数据:textFileStream(directory目录)
    val dirDS: DStream[String] = ssc.textFileStream("in")

    val wordDS: DStream[String] = dirDS.flatMap(_.split(" "))
    val wordToOneDS: DStream[(String, Int)] = wordDS.map((_, 1))
    val result = wordToOneDS.reduceByKey(_ + _)

    result.print()
    //开启SparkStreamingContext
    ssc.start()
    ssc.awaitTermination()
  }
}

3.3 从Queue队列中获取

object Spark03_Streaming_RDD {
    
    
  def main(args: Array[String]): Unit = {
    
    
    //1 初始化Spark配置
    val conf = new SparkConf().setMaster("local[*]").setAppName("SparkStreamingRDD")
    //2 初始化SparkStreamingContext
    val ssc = new StreamingContext(conf, Duration(3000))
    //TODO 3 创建RDD队列,Queue中存放 RDD[Int]
    val rddQueue: mutable.Queue[RDD[Int]] = new mutable.Queue[RDD[Int]]()
    //TODO 4 创建QueueInputDStream
    val inputStream: InputDStream[Int] = ssc.queueStream(rddQueue)
    //5 处理队列中的RDD数据
    val mappedStream: DStream[(Int, Int)] = inputStream.map((_, 1))
    val result: DStream[(Int, Int)] = mappedStream.reduceByKey(_ + _)
    //6 打印结果
    result.print()
    //7 启动任务
    ssc.start()
    //TODO 8 循环创建并向RDD队列中放入RDD
    for (i <- 1 to 5){
    
    
      rddQueue += ssc.sparkContext.makeRDD(1 to 300 , 10)
      Thread.sleep(2000)
    }
    ssc.awaitTermination()
  }
}

3.4 自定义数据源

自定义采集器步骤:

  • 继承org.apache.spark.streaming.receiver.Receiver类
  • 定义泛型:要采集的数据的泛型,一般采集的是String
  • 给Receiver父类传递参数:StorageLevel存储的级别
  • 重写方法:
    • onStart用while循环生成数据,用自带的store()存储数据。
    • onStop用来停止采集
object Spark03_Streaming_DIY {
    
    
  def main(args: Array[String]): Unit = {
    
    
    //1 初始化Spark配置
    val conf = new SparkConf().setMaster("local[*]").setAppName("SparkStreamingRDD")
    //2 初始化SparkStreamingContext
    val ssc = new StreamingContext(conf, Seconds(3))

    //TODO 创建自定义数据采集器
    val myReceiver = new MyReceiver

    //TODO 使用自定义采集器
    val inputStream: ReceiverInputDStream[String] = ssc.receiverStream(myReceiver)
    val dataToOne: DStream[(String, Int)] = inputStream.map((_, 1))
    val result: DStream[(String, Int)] = dataToOne.reduceByKey(_ + _)

    result.print()

    ssc.start()
    ssc.awaitTermination()
  }
  //1 继承Receiver
  //2 定义泛型:采集数据的类型
  //3 给父类传递初始参数,用于设定数据的存储级别
  //4 重写方法
  class MyReceiver extends Receiver[String](StorageLevel.MEMORY_ONLY){
    
    
    private var flowFlg = true
    override def onStart(): Unit = {
    
    
      while (flowFlg) {
    
    
        //生成数据
        val data = new Random().nextInt(10).toString
        //存储数据
        store(data)
      }
    }
    override def onStop(): Unit = {
    
    
      flowFlg = false
    }
  }
}

3.5 Kafka

  • ReceiverAPI:需要一个专门的Executor去接收数据,然后发送给其他的Executor做计算。存在的问题,接收数据的Executor和计算的Executor速度会有所不同,特别在接收数据的Executor速度大于计算的Executor速度,会导致计算数据的节点内存溢出。早期版本中提供此方式,当前版本不适用

  • DirectAPI:是由计算的Executor来主动消费Kafka的数据,速度由自身控制

  • 核心代码

        //3.定义Kafka参数
        val kafkaPara: Map[String, Object] = Map[String, Object](
          ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG -> "hadoop102:9092,hadoop103:9092,hadoop104: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("AA"), kafkaPara))
    

需求:通过SparkStreaming从Kafka读取数据,并将读取过来的数据做简单计算,最终打印到控制台。

步骤1:导入依赖

<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>

步骤2:首先需要使用kafka创建一个Topic

# 查看当前有哪些topic
[atguigu@hadoop102 kafka]$ kafka-topics.sh --bootstrap-server hadoop102:9092 --list
# 创建一个新的topic
[atguigu@hadoop102 kafka]$ kafka-topics.sh --bootstrap-server hadoop102:9092 --create --topic AA --partitions 2 --replication-factor 3
# 启动kafka的生产者生产消息
[atguigu@hadoop102 kafka]$ bin/kafka-console-producer.sh --broker-list hadoop102:9092 --topic AA

步骤3:编写代码,从kafka读取数据,计算wordCount

object Spark01_Streaming_kafka {
    
    
  def main(args: Array[String]): Unit = {
    
    
    //1.创建SparkConf
    val conf = new SparkConf().setMaster("local[*]").setAppName("Spark-Streaming-Kafka")
    //2.创建StreamingContext
    val ssc = new StreamingContext(conf, Duration(3000))
    //3.定义Kafka参数
    val kafkaPara: Map[String, Object] = Map[String, Object](
      ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG -> "hadoop102:9092,hadoop103:9092,hadoop104: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("AA"), 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()
  }
}

步骤4:查看Kafka消费进度

bin/kafka-consumer-groups.sh --describe --bootstrap-server linux1:9092 --group atguigu 

image-20201202210746758

4 DStream转换

  • 方法:scala中的方法
  • 算子:RDD中用来区别于scala方法的 函数的新称呼
  • 原语:用来区别于scala方法和RDD算子的 表示DStream的函数的新称呼

DStream的操作于RDD类似,分为Transformations(转换)和Output Operations(输出)两种,此外转换操作还有特殊的原语。如:updateStateByKey()和transform()等

4.1 无状态转换操作

什么是无状态转换操作?

更通俗的来说,就是在一个时间批次内计算的值,不会和下一个批次的进行累加!

就是简单的把RDD转化操作应用到每个批次上,也就是转换DStream中的每个RDD。部分无状态转化操作:

  • 注意:针对键值对的DStream转化操作(reduceByKey)要添加import StreamingContext._才能在Scala中使用。

image-20201203110147720

①transform

transform有一个好处就是可以周期性的执行一段代码。也就是说每一个采集周期都会执行一次。

object Spark04_Streaming_Transform {
    
    
  def main(args: Array[String]): Unit = {
    
    
    //1 创建SparkConf
    val conf = new SparkConf().setMaster("local[*]").setAppName("SparkStreaming")
    //2 创建SparkStreamingContext
    val ssc: StreamingContext = new StreamingContext(conf, Seconds(3))
    //3 从socket获取数据
    val inputStream: ReceiverInputDStream[String] = ssc.socketTextStream("hadoop102", 9999)
    //4 转为RDD操作
    val wordCountDS: DStream[(String, Int)] = inputStream.transform(rdd => {
    
    
      val words: RDD[String] = rdd.flatMap(_.split(" "))
      val wordToOne: RDD[(String, Int)] = words.map((_, 1))
      val result: RDD[(String, Int)] = wordToOne.reduceByKey(_ + _)
      result
    })
    //5 打印
    wordCountDS.print()
    //6 启动
    ssc.start()
    ssc.awaitTermination()
  }
}
object Spark04_Streaming_Transform_1 {
    
    
  def main(args: Array[String]): Unit = {
    
    
    //1 创建SparkConf
    val conf = new SparkConf().setMaster("local[*]").setAppName("SparkStreaming")
    //2 创建SparkStreamingContext
    val ssc: StreamingContext = new StreamingContext(conf, Seconds(3))
    //3 从socket获取数据
    val inputStream: ReceiverInputDStream[String] = ssc.socketTextStream("hadoop102", 9999)

    // TODO 原语,算子,方法
    // Coding => Driver端 (执行一次)
    inputStream.map(
      str => {
    
    
        // Coding => Executor端(任务数量)
        str * 2
    })
    // ===========================
    // Coding => Driver端 (执行一次)
    inputStream.transform(
      rdd => {
    
    
        // Coding => Driver端 (周期性执行)
        rdd.map(
          str => {
    
    
            // Coding => Executor端 (任务数量)
            str * 2
          }
        )
      }
    )
    //6 启动
    ssc.start()
    ssc.awaitTermination()
  }
}

②join

join可以将两个DStream根据相同key拼接起来。底层就是RDD的join

两个流之间的join需要两个流的批次大小一致,这样才能同时触发计算。

下面的例子是只有相同key的才会join在一起。


object Spark05_Streaming_Join {
    
    
  def main(args: Array[String]): Unit = {
    
    
    //创建SparkConf
    val conf = new SparkConf().setMaster("local[*]").setAppName("SparkStreaming")
    //创建StreamingContext
    val ssc: StreamingContext = new StreamingContext(conf, Duration(5000))

    //通过socket获取数据创建DS
    val ds1: ReceiverInputDStream[String] = ssc.socketTextStream("hadoop103", 9999)
    val ds2: ReceiverInputDStream[String] = ssc.socketTextStream("hadoop103", 8888)

    val kv1: DStream[(String, Int)] = ds1.map((_, 1))
    val kv2: DStream[(String, Int)] = ds2.map((_, 1))

    val joinDS: DStream[(String, (Int, Int))] = kv1.join(kv2)

    joinDS.print()
      
    ssc.start()
    ssc.awaitTermination()
  }
}

4.2 有状态转换操作

有状态转换操作:通俗来说就是,一个时间批次的计算结果会放到一个缓冲区暂存起来,会累加多个缓冲区的结果。

①UpdateStageByKey

UpdateStageByKey有状态操作,原语用来记录历史批次的值。

  • 如果使用reduceByKey,因为reduceByKey是无状态的操作,所以它仅仅是计算一个批次内的数据,计算时不考虑缓冲。

image-20201203120859297

  • updateStageByKey就是使用设置的checkpoint作为缓冲区,将不同批次的数据和缓冲区的暂存数据进行聚合操作。

image-20201203122028556

image-20201203145810919

  • updateStateByKey(参数1,参数2)
    • 参数1:相同key的value值的集合
    • 参数2:
object Spark06_Streaming_State {
    
    
  def main(args: Array[String]): Unit = {
    
    
    //创建SparkConf
    val conf = new SparkConf().setMaster("local[*]").setAppName("SparkStreaming")
    //创建StreamingContext
    val ssc: StreamingContext = new StreamingContext(conf, Duration(5000))

    //TODO 设置检查点:一般为HDFS路径
    ssc.checkpoint("cp")

    val inputDS: ReceiverInputDStream[String] = ssc.socketTextStream("hadoop102", 9999)
    val words: DStream[String] = inputDS.flatMap(_.split(" "))
    val wordToOne: DStream[(String, Int)] = words.map((_, 1))

    //wordToOneDS.reduceByKey(_+_) // 所谓的无状态操作,就是计算时不考虑缓冲
    //val result = wordToOne.reduceByKey(_ + _)

    //TODO updateStateByKey有状态数据操作
    //  updateStateByKey传递的参数时一个函数:
    //  函数有两个参数:
    //      第一个参数:表示相同key的value的集合(Seq)
    //      第二个参数:表示缓冲区数据对象

    //The checkpoint directory has not been set.
    //有状态计算其实就是使用缓冲区进行计算,这个缓冲区其实采用的是检查点操作
    val result: DStream[(String, Int)] = wordToOne.updateStateByKey(
      (seq: Seq[Int], buffer: Option[Int]) => {
    
    
        val currentVal = seq.sum
        val buffVal = buffer.getOrElse(0)
        val newVal = currentVal + buffVal
        Option(newVal)
      }
    )
    result.print()
    //启动StreamingContext
    ssc.start()
    ssc.awaitTermination()
  }
}

连接Kafka,消费消息

需要注意的一点!!!从kafka直接获得的数据是一个InputStream流,kafka传输是以k-v传输的。要获取record的value值,在进行计算。

kafkaDStream.map(record => record.value())

object Spark06_Streaming_State_1 {
    
    
  def main(args: Array[String]): Unit = {
    
    
    //创建SparkConf
    val conf = new SparkConf().setMaster("local[*]").setAppName("SparkStreaming")
    //创建StreamingContext
    val ssc: StreamingContext = new StreamingContext(conf, Duration(5000))

    //TODO 设置检查点:一般为HDFS路径
    ssc.checkpoint("cp")

    //TODO 定义Kafka参数
    val kafkaPara: Map[String, Object] = Map[String, Object](
      ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG -> "hadoop102:9092,hadoop103:9092,hadoop104:9092",
      ConsumerConfig.GROUP_ID_CONFIG -> "atguigu",
      "key.deserializer" -> "org.apache.kafka.common.serialization.StringDeserializer",
      "value.deserializer" -> "org.apache.kafka.common.serialization.StringDeserializer"
    )

    //TODO 从kafka获取数据
    val kafkaDStream: InputDStream[ConsumerRecord[String, String]] = KafkaUtils.createDirectStream(
      ssc,
      LocationStrategies.PreferConsistent,
      ConsumerStrategies.Subscribe[String, String](Set("CC"), kafkaPara)
    )
    //TODO !!!需要在kafka流中获取record的值。
    val lines: DStream[String] = kafkaDStream.map(record => record.value())
    val words: DStream[String] = lines.flatMap(_.split(" "))

    val wordToOne = words.map((_, 1))

    val result: DStream[(String, Int)] = wordToOne.updateStateByKey(
      (seq: Seq[Int], buffer: Option[Int]) => {
    
    
        val sum = seq.sum
        val bufferVal = buffer.getOrElse(0)
        val newVal = bufferVal + sum
        Option(newVal)
      })

    result.print()

    //启动StreamingContext
    ssc.start()
    ssc.awaitTermination()
  }
}

检查点:checkpoint

默认的检查点,所保存的路径是本地。但是一般我们都是将检查点保存到HDFS上。

那么如何设置SparkStreaming缓存的检查点为HDFS呢?

  • 将Hadoop的core-site.xml和hdfs.-site.xml放到工程的resource文件夹内,编译让target内的classes内出现才行。这样再设置的检查点路径,默认是采用HDFS上的路径。
  • 需要注意的是!需要设置代理访问用户。 System.setProperty(“HADOOP_USER_NAME”, “atguigu”)
object Spark06_Streaming_State_3 {
    
    
  def main(args: Array[String]): Unit = {
    
    
    System.setProperty("HADOOP_USER_NAME", "atguigu")

    val conf = new SparkConf().setMaster("local[*]").setAppName("SparkStreaming")
    val ssc = new StreamingContext(conf, Duration(3000))

    ssc.checkpoint("cp")
    //kafka
    val kafkaPara = Map[String, Object](
      ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG -> "hadoop102:9092,hadoop103:9092,hadoop104:9092",
      ConsumerConfig.GROUP_ID_CONFIG -> "atguigu",
      ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG -> "org.apache.kafka.common.serialization.StringDeserializer",
      ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG -> "org.apache.kafka.common.serialization.StringDeserializer"
    )
    val kafkaInputDStream: InputDStream[ConsumerRecord[String, String]] = KafkaUtils.createDirectStream(
      ssc,
      LocationStrategies.PreferConsistent,
      ConsumerStrategies.Subscribe[String, String](Set("BB"), kafkaPara)
    )

    //获取kafka的value值
    val kafkaDS: DStream[String] = kafkaInputDStream.map(_.value())
    val wordToOne: DStream[(String, Int)] = kafkaDS.map((_, 1))

    val result = wordToOne.updateStateByKey(
      (seq: Seq[Int], buffer: Option[Int]) => {
    
    
        val sum = seq.sum
        val bufferVal = buffer.getOrElse(0)
        val newVal = bufferVal + sum
        Option(newVal)
      }
    )
    result.print()
    ssc.start()
    ssc.awaitTermination()
  }
}

从检查点恢复数据

  • 检查点默认的是,不会保存数据,也就是说下次SparkStreaming读取数据的时候,会从0开始updateStateByKey。

  • 从检查点恢复数据,getActiveOrCreate(“检查点路径”, ()=> {匿名函数})

object Spark06_Streaming_State_2 {
    
    
  def main(args: Array[String]): Unit = {
    
    
    //设置访问HDFS的代理用户
    System.setProperty("HADOOP_USER_NAME", "atguigu")
    //TODO 从检查点恢复数据
    val streamingContext = StreamingContext.getActiveOrCreate("cp1", () => {
    
    
      val conf = new SparkConf().setMaster("local[*]").setAppName("SparkStreaming")
      val ssc = new StreamingContext(conf, Duration(3000))
      ssc.checkpoint("cp1")
      
      val inputDS: ReceiverInputDStream[String] = ssc.socketTextStream("hadoop102", 9999)
      
      val result = inputDS.flatMap(_.split(" ")).map((_, 1)).updateStateByKey(
        (seq: Seq[Int], buffer: Option[Int]) => {
    
    
          val sum = seq.sum
          val buffVal = buffer.getOrElse(0)
          val newVal = sum + buffVal
          Option(newVal)
        }
      )
      
      result.print()
      ssc
    })
    //启动SparkStreaming 
    streamingContext.start()
    streamingContext.awaitTermination()
  }
}

③窗口函数WindowOperations

  • 数据的操作范围(窗口):
  • 默认情况下窗口操作就是存在的,只不过默认窗口大小为一个采集周期,滑动幅度也是一个采集周期。
  • 默认的窗口是随着时间的推移进行滑动的,默认值是一个采集周期。
  • 默认的窗口操作不需要保存状态,只需要默认处理即可。

window(参数1,参数2)

  • 参数1:表示窗口的大小,以时间为单位,一般设定为采集周期的整数倍。
  • 参数2:表示窗口的滑动幅度,步长,一般设定为采集周期的整数倍,默认是采集周期的大小。

image-20201204234655535

  • 当窗口大小大于采集周期:会出现重复数据,会出现低 - 高 - 低的趋势

  • 当窗口大小小于或不等于采集周期的整数倍,会增加复杂度。

  • 当窗口大小等于滑动步长大小,不会出现重复数据

image-20201204235707045

object Spark08_Streaming_Window {
    
    
  def main(args: Array[String]): Unit = {
    
    
    val conf = new SparkConf().setMaster("local[*]").setAppName("Window")
    val ssc = new StreamingContext(conf, Duration(3000))

    val inputDS: ReceiverInputDStream[String] = ssc.socketTextStream("hadoop102", 9999)
    //无状态操作
    val wordCount = inputDS.flatMap(_.split(" ")).map((_, 1))

    //TODO 窗口函数
    //计算前,可以设定数据的操作范围(窗口)
    //窗口window方法用于进行数据的窗口计算
    //默认情况下,窗口随着时间的推移进行滑动的,默认值以一个采集周期
    //SparkStreaming中计算周期为窗口滑动步长
    //窗口操作默认就是存在的,只不过默认范围为一个采集周期,滑动幅度为一个采集周期
    //默认的窗口操作不需要状态保存,只需要默认处理即可。
    //Window(参数1, 参数2)
    //  参数1:表示窗口的范围,以时间为单位,一般设定为采集周期的整数倍
    //  参数2:表示窗口的滑动幅度,步长,一般设定为采集周期的整数倍,默认是采集周期的大小
    val windowDS = wordCount.window(Seconds(6), Seconds(6))

    val result = windowDS.reduceByKey(_ + _)
    result.print()

    ssc.start()
    ssc.awaitTermination()
  }
}

④其他窗口函数

(1)window(windowLength, slideInterval): 基于对源DStream窗化的批次进行计算返回一个新的Dstream;

(2)countByWindow(windowLength, slideInterval): 返回一个滑动窗口计数流中的元素个数;

需要设置检查点。

object Spark08_Streaming_Window_1 {
    
    
  def main(args: Array[String]): Unit = {
    
    
    System.setProperty("HADOOP_USER_NAME", "atguigu")
    val conf = new SparkConf().setMaster("local[*]").setAppName("Window")
    val ssc = new StreamingContext(conf, Duration(3000))
    ssc.checkpoint("cp")

    val inputDS: ReceiverInputDStream[String] = ssc.socketTextStream("hadoop102", 9999)
    //无状态操作
    val wordCount = inputDS.flatMap(_.split(" ")).map((_, 1))

    //TODO countByWindow(),返回一个滑动窗口计数流中的元素个数
    // The checkpoint directory has not been set
    val value: DStream[Long] = wordCount.countByWindow(Seconds(6), Seconds(6))
    value.print()
    ssc.start()
    ssc.awaitTermination()
  }
}

(3)reduceByWindow(func, windowLength, slideInterval): 通过使用自定义函数整合滑动区间流元素来创建一个新的单元素流;

object Spark08_Streaming_Window_2 {
    
    
  def main(args: Array[String]): Unit = {
    
    
    System.setProperty("HADOOP_USER_NAME", "atguigu")
    val conf = new SparkConf().setMaster("local[*]").setAppName("Window")
    val ssc = new StreamingContext(conf, Duration(3000))
    ssc.checkpoint("cp")

    val inputDS: ReceiverInputDStream[String] = ssc.socketTextStream("hadoop102", 9999)
    //无状态操作
    val wordCount = inputDS.flatMap(_.split(" ")).map((_, 1))

    //TODO reduceByWindow()
    val result = wordCount.reduceByWindow((t1, t2) => {
    
    
      (t1._1 + t2._1, t1._2 + t2._2)
    }, Seconds(6), Seconds(6)
    )

    result.print()
    ssc.start()
    ssc.awaitTermination()
  }
}

(4)reduceByKeyAndWindow(func, windowLength, slideInterval, [numTasks]): 当在一个(K,V)对的DStream上调用此函数,会返回一个新(K,V)对的DStream,此处通过对滑动窗口中批次数据使用reduce函数来整合每个key的value值。

(5)reduceByKeyAndWindow(func, invFunc, windowLength, slideInterval, [numTasks]): 这个函数是上述函数的变化版本,每个窗口的reduce值都是通过用前一个窗的reduce值来递增计算。通过reduce进入到滑动窗口数据并”反向reduce”离开窗口的旧数据来实现这个操作。一个例子是随着窗口滑动对keys的“加”“减”计数。通过前边介绍可以想到,这个函数只适用于”可逆的reduce函数”,也就是这些reduce函数有相应的”反reduce”函数(以参数invFunc形式传入)。如前述函数,reduce任务的数量通过可选参数来配置。

object Spark08_Streaming_Window_3 {
    
    
  def main(args: Array[String]): Unit = {
    
    
    System.setProperty("HADOOP_USER_NAME", "atguigu")
    val conf = new SparkConf().setMaster("local[*]").setAppName("Window")
    val ssc = new StreamingContext(conf, Duration(3000))
    ssc.checkpoint("cp")

    val inputDS: ReceiverInputDStream[String] = ssc.socketTextStream("hadoop102", 9999)
    //无状态操作
    val wordCount = inputDS.flatMap(_.split(" ")).map((_, 1))

    //TODO reduceByKeyAndWindow()
    val result = wordCount.reduceByKeyAndWindow(
      (x, y) => {
    
    
        println(x + " + " + y)
        x + y
      },
      (a, b) => {
    
    
        println(a + " - " + b)
        a - b
      },
      Seconds(9),
      Seconds(3)
    )

    result.print()
    ssc.start()
    ssc.awaitTermination()
  }
}

5 DStream输出

与RDD的惰性求值类似,如果一个DStream及其派生出的DStream都没有被执行输出操作,那么这些DStream就都不会被求值。如果StreamingContext中没有设定输出操作,整个context都不会启动。

①print()

打印DStream中每一批次数据的最开始10个元素

image-20201205001827572

②saveAsTextFiles(prefix, [suffix])

以text文件形式存储这个DStream的内容。每一批次的存储文件名基于参数中的prefix和suffix。”prefix-Time_IN_MS[.suffix]”

image-20201205002403522

object Spark09_Streaming_Output {
    
    
  def main(args: Array[String]): Unit = {
    
    
    System.setProperty("HADOOP_USER_NAME", "atguigu")
    val conf = new SparkConf().setMaster("local[*]").setAppName("Window")
    val ssc = new StreamingContext(conf, Duration(3000))
    ssc.checkpoint("cp")

    val inputDS: ReceiverInputDStream[String] = ssc.socketTextStream("hadoop102", 9999)
    //无状态操作
    val wordCount = inputDS.flatMap(_.split(" ")).map((_, 1))

    //TODO saveAsTextFiles
    wordCount.saveAsTextFiles("prefix", ".suffix")

    ssc.start()
    ssc.awaitTermination()
  }
}

③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存入文件或者通过网络将其写入数据库。

注意:1. 连接不能写在driver层面(需要考虑序列化问题)

​ 2. 如果写在foreache则每个RDD中的每一条数据都创建,浪费

		3. foreachPartition,在分区创建(获取)

6 优雅关闭

流式任务需要7*24小时执行,但是有时涉及到代码升级需要主动停止程序,但是分布式程序,没办法做到一个个进程杀死。需要使用外部文件系统来控制内部程序关闭。

object Spark10_Streaming_Close {
  def main(args: Array[String]): Unit = {
    //创建SparkStreamingContext
    val conf = new SparkConf().setMaster("local[*]").setAppName("GraceClose")
    val ssc = new StreamingContext(conf, Seconds(3))


    //获取数据
    val inputDS: ReceiverInputDStream[String] = ssc.socketTextStream("hadoop103", 8899)

    val result = inputDS.flatMap(_.split(" ")).map((_, 1)).reduceByKey(_ + _)
    result.print()


    ssc.start()
    //stop()方法不能在主线程中调用
    //1. 关闭需要在新的线程中执行
    //2. 方法的调用机制(分布式事务)
    //

//    var closeFlg = false

    new Thread(new Runnable {
      override def run(): Unit = {
//        while (true){
//          if (closeFlg){
//            ssc.stop()
//            return
//          }
//          Thread.sleep(10000)
//        }

        while (true) {
          try
            Thread.sleep(5000)
          catch {
            case e: InterruptedException =>
              e.printStackTrace()
          }

          //TODO 创建HDFS文件系统对象
          //  注意!端口是RPC端口,不是Web端口 => 8020
          //  注意!需要设置user=“atguigu”,不然就需要在首行设置代理用户
          val fs = FileSystem.get(new URI("hdfs://hadoop102:8020"), new Configuration(), "atguigu")

          val state = ssc.getState()

          val bool = fs.exists(new Path("hdfs://hadoop102:8020/stopSpark"))

          if (bool) {
            if (state == StreamingContextState.ACTIVE){
              //TODO 设置优雅关闭为true
              ssc.stop(stopSparkContext = true, stopGracefully = true)
              System.exit(0)
            }
          }
        }

      }
    }).start()

    //
    ssc.awaitTermination()
  }
}

猜你喜欢

转载自blog.csdn.net/qq_43523503/article/details/111189087
今日推荐