Spark Streaming

文章目录

  • Spark Streaming
    • Spark Streaming 概述
      • Spark Streaming 是什么
      • Spark Streaming 架构
        • 架构图
        • 背压机制
    • DStream 入门
      • WordCount案例实操
      • WordCount 解析
    • DStream 创建
      • RDD 队列
        • 用法及说明
        • 案例实操
      • 自定义数据源
        • 用法及说明
        • 案例实操
      • Kafka数据源(重点)
        • 版本选型
        • Kafka 0-8 Receiver 模式 (当前版本不适用)
        • Kafka 0-8 Direct 模式(当前版本不适用)
        • Kafka 0-10 Direct 模式
        • 查看 Kafka 消费进度
    • DStream 转换
      • 无状态转化操作
        • transform
        • join
      • 有状态转化操作
        • updateStateByKey
        • windowOperations
    • DStream 输出
    • 优雅关闭
      • MonitorStop
      • SparkTest

Spark Streaming

Spark Streaming 概述

Spark Streaming 是什么

Spark Streaming 使得构建可扩展的容错流应用程序变得更加容易
Spark Streaming 无法实现真正的流式数据处理。使用了微批次数据处理。
Spark Streaming 是一个准实时数据处理引擎。

  • 实时:数据处理的延迟在毫秒级进行响应

  • 离线:数据处理的延迟在小时,天,月,年进行响应

  • 批处理:数据处理的方式

  • 流式:数据处理的方式

Spark Streaming 支持的数据输入源很多,例如:Kafka,Flume,Twitter,ZeroMQ和简单的TCP Socket等等。
数据输入后,可以用Spark的高度抽象原语进行运算,如map,reduce,join,window。
结果也能保存在很多地方,如HDFS,数据库等。

Spark Streaming_第1张图片

和Spark基于RDD的概念很相似,Spark Streaming使用离散化流(discretized stream)作为抽象表示,叫做DStream。
DStream是随时间推移而收到的数据的序列。在内部,每个时间区间收到的数据都作为RDD存在,而DStream是由这些RDD所组成的序列(因此得名"散列化")。所以简单来讲,DStream就是对RDD在实时数据处理场景的一种封装。

Spark Streaming 架构

架构图

  • 整体架构图
    Spark Streaming_第2张图片

  • Spark Streaming 架构图
    Spark Streaming_第3张图片

背压机制

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案例实操

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

  • 添加依赖

  • 编写代码

  def main(args: Array[String]): Unit = {
    // 1.初始化Spark配置信息
    // SparkStreaming使用核数最少是两个:driver,receiver
    val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkStreaming WordCount")

    // 2.初始化SparkStreamingContext
    // conf: SparkConf, batchDuration: Duration - 采集周期
    val ssc = new StreamingContext(conf, Seconds(3))

    // 3.通过监控端口创建DStream,读进来的数据为一行行
    val lineStreams: ReceiverInputDStream[String] = ssc.socketTextStream("localhost", 9999)

    // 将每一行数据做切分,形成一个个单词
    val wordStreams: DStream[String] = lineStreams.flatMap(_.split(" "))

    // 将单词映射成元组(word, 1)
    val wordToOneStreams: DStream[(String, Int)] = wordStreams.map((_, 1))

    // 将相同的单词次数做统计
    val wordToCountStreams: DStream[(String, Int)] = wordToOneStreams.reduceByKey(_+_)

    // 打印
    wordToCountStreams.print()

    // 启动SparkStreamingContext
    ssc.start() // 启动采集器
    ssc.awaitTermination() // 等待采集器的结束
  }
  • 启动程序并通过netcat发送数据
E:\DeveloperTools\netcat-win32-1.12>nc -lp 9999
dsy sarah dongsaiyuan
hello word

-------------------------------------------
Time: 1592056995000 ms
-------------------------------------------
(dongsaiyuan,1)
(dsy,1)
(sarah,1)

-------------------------------------------
Time: 1592056998000 ms
-------------------------------------------
(word,1)
(hello,1)

WordCount 解析

Discretized Stream 是 Spark Streaming 的基础抽象,代表持续性的数据流和经过各种Spark原语操作后的结果数据流。在内部实现上,DStream是一系列连续的RDD来表示。每个RDD含有一段时间间隔内的数据。
在这里插入图片描述

对数据的操作也是按照RDD为单位来进行的
Spark Streaming_第4张图片

计算过程由 Spark Engine 来完成
在这里插入图片描述

DStream 创建

RDD 队列

用法及说明

测试过程中,可以通过使用ssc.queueStream(queueOfRDDs)来创建DStream。

案例实操

  • 需求:循环创建几个RDD,将RDD放入队列。通过Spark Streaming创建DStream,计算WordCount。
  def main(args: Array[String]): Unit = {
    // 1.初始化 Spark 配置信息
    val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD Stream")

    // 2.初始化 SparkStreamingContext
    val ssc = new StreamingContext(conf, Seconds(3))

    // 3.创建 RDD 队列
    val rddQueue = new mutable.Queue[RDD[Int]]()

    // 4.创建 QueueInputDStream
    val inputStream: InputDStream[Int] = ssc.queueStream(rddQueue, oneAtATime = false)

    // 5.处理队列中的 RDD 数据
    val mappedStream: DStream[(Int, Int)] = inputStream.map((_, 1))
    val reducedStream: DStream[(Int, Int)] = mappedStream.reduceByKey(_+_)

    // 6.打印结果
    reducedStream.print()

    // 7.启动任务
    ssc.start()

    // 8.循环创建并向 RDD 队列中放入 RDD
    for(i <- 1 to 5) {
      rddQueue += ssc.sparkContext.makeRDD(List(i))
      Thread.sleep(1000)
    }

    ssc.awaitTermination()
  }
-------------------------------------------
Time: 1592094957000 ms
-------------------------------------------
(1,1)
(2,1)
(3,1)

-------------------------------------------
Time: 1592094960000 ms
-------------------------------------------
(4,1)
(5,1)

自定义数据源

用法及说明

需要继承 Receiver,并实现 onStart,onStop 方法来自定义数据源采集

案例实操

需求:自定义数据源,实现监控某个端口号,获取该端口号内容

  def main(args: Array[String]): Unit = {
    // 1.初始化 Spark 配置信息
    val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("custom receiver")

    // 2.初始化 SparkStreamingContext
    val ssc = new StreamingContext(conf, Seconds(3))

    // 3.创建自定义 receiver 的 Streaming
    val lineStream: ReceiverInputDStream[String] = ssc.receiverStream(new CustomReceiver("localhost", 9999))

    // 4.将每一行数据做切分,形成一个个单词
    val wordStream: DStream[String] = lineStream.flatMap(_.split(" "))

    // 5.将单词映射成元组(word, 1)
    val wordToOneStream: DStream[(String, Int)] = wordStream.map((_, 1))

    // 6.将相同的单词次数做统计
    val wordToCountStream: DStream[(String, Int)] = wordToOneStream.reduceByKey(_+_)

    // 7.打印
    wordToCountStream.print()

    // 8.启动 SparkStreamingContext
    ssc.start()
    ssc.awaitTermination()
  }

  class CustomReceiver(host: String, port: Int) extends Receiver[String](StorageLevel.MEMORY_ONLY) {

    // 最初启动的时候,调用该方法,作用为:读数据并将数据发送给Spark
    override def onStart(): Unit = {
      new Thread("Socket Receiver") {
        override def run(): Unit = {
          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 = {

    }
  }
E:\DeveloperTools\netcat-win32-1.12>nc -lp 9999
hello spark
hello world
a
a
a
a
a

Time: 1592097228000 ms
-------------------------------------------
(hello,1)
(spark,1)

-------------------------------------------
Time: 1592097237000 ms
-------------------------------------------
(hello,1)
(world,1)

-------------------------------------------
Time: 1592097288000 ms
-------------------------------------------
(a,1)

-------------------------------------------
Time: 1592097291000 ms
-------------------------------------------
(a,4)

Kafka数据源(重点)

版本选型

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

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

Kafka 0-8 Receiver 模式 (当前版本不适用)

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

  • 导入依赖

    org.apache.spark
    spark-streaming-kafka-0-8_2.11
    2.4.5

  • 编写代码
  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数据创建DStream(基于Receive方式)
    val kafkaDStream: ReceiverInputDStream[(String, String)] = KafkaUtils.createStream(ssc,
      "hadoop131:2181,hadoop132:2181,hadoop133:2181",
      "dsy",
      Map[String, Int]("dsy" -> 1))

    //4.计算WordCount
    kafkaDStream.map { case (_, value) =>
      (value, 1)
    }.reduceByKey(_ + _)
      .print()

    //5.开启任务
    ssc.start()
    ssc.awaitTermination()
  }

Kafka 0-8 Direct 模式(当前版本不适用)

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

  • 导入依赖

    org.apache.spark
    spark-streaming-kafka-0-8_2.11
    2.4.5

  • 编写代码(自动维护offset)
val getSSC1: () => StreamingContext = () => {
    val sparkConf: SparkConf = new SparkConf().setAppName("ReceiverWordCount").setMaster("local[*]")
    val ssc = new StreamingContext(sparkConf, Seconds(3))
    ssc
  }

  def getSSC: StreamingContext = {

    //1.创建SparkConf
    val sparkConf: SparkConf = new SparkConf().setAppName("ReceiverWordCount").setMaster("local[*]")

    //2.创建StreamingContext
    val ssc = new StreamingContext(sparkConf, Seconds(3))

    //设置CK
    ssc.checkpoint("./ck2")

    //3.定义Kafka参数
    val kafkaPara: Map[String, String] = Map[String, String](
      ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG -> "hadoop131:9092,hadoop132:9092,hadoop133:9092",
      ConsumerConfig.GROUP_ID_CONFIG -> "dsy"
    )

    //4.读取Kafka数据
    val kafkaDStream: InputDStream[(String, String)] = KafkaUtils.createDirectStream[String, String, StringDecoder, StringDecoder](ssc,
      kafkaPara,
      Set("dsy"))

    //5.计算WordCount
    kafkaDStream.map(_._2)
      .flatMap(_.split(" "))
      .map((_, 1))
      .reduceByKey(_ + _)
      .print()

    //6.返回数据
    ssc
  }

  def main(args: Array[String]): Unit = {

    //获取SSC
    val ssc: StreamingContext = StreamingContext.getActiveOrCreate("./ck2", () => getSSC)

    //开启任务
    ssc.start()
    ssc.awaitTermination()
  }
  • 编写代码(手动维护offset)
  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, String] = Map[String, String](
      ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG -> "hadoop131:9092,hadoop132:9092,hadoop133:9092",
      ConsumerConfig.GROUP_ID_CONFIG -> "dsy"
    )

    //4.获取上一次启动最后保留的Offset=>getOffset(MySQL)
    val fromOffsets: Map[TopicAndPartition, Long] = Map[TopicAndPartition, Long](TopicAndPartition("dsy", 0) -> 20)

    //5.读取Kafka数据创建DStream
    val kafkaDStream: InputDStream[String] = KafkaUtils.createDirectStream[String, String, StringDecoder, StringDecoder, String](ssc,
      kafkaPara,
      fromOffsets,
      (m: MessageAndMetadata[String, String]) => m.message())

    //6.创建一个数组用于存放当前消费数据的offset信息
    var offsetRanges = Array.empty[OffsetRange]

    //7.获取当前消费数据的offset信息
    val wordToCountDStream: DStream[(String, Int)] = kafkaDStream.transform { rdd =>
      offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
      rdd
    }.flatMap(_.split(" "))
      .map((_, 1))
      .reduceByKey(_ + _)

    //8.打印Offset信息
    wordToCountDStream.foreachRDD(rdd => {
      for (o <- offsetRanges) {
        println(s"${o.topic}:${o.partition}:${o.fromOffset}:${o.untilOffset}")
      }
      rdd.foreach(println)
    })

    //9.开启任务
    ssc.start()
    ssc.awaitTermination()
  }

Kafka 0-10 Direct 模式

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

  • 导入依赖

     org.apache.spark
     spark-streaming-kafka-0-10_2.12
     2.4.5

  • 编写代码
  def main(args: Array[String]): Unit = {
    // 1.创建 SparkConf
    val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Kafka Direct")

    // 2.创建 StreamingContext
    val ssc = new StreamingContext(conf, Seconds(3))

    // 3.定义Kafka参数
    val kafkaParam = Map[String, Object](
      ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG -> "hadoop131:9092,hadoop132:9092,hadoop133:9092",
      ConsumerConfig.GROUP_ID_CONFIG -> "dsy",
      "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("dsy"), kafkaParam))

    // 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()
  }
[dsy@hadoop131 bin]$ kafka-console-producer.sh --broker-list hadoop131:9092 --topic dsy
>hello hello
>dsy dsy
>sarah sarah
>hello world

-------------------------------------------
Time: 1592101578000 ms
-------------------------------------------
(hello,2)

-------------------------------------------
Time: 1592101581000 ms
-------------------------------------------
(dsy,2)

-------------------------------------------
Time: 1592101584000 ms
-------------------------------------------
(sarah,2)

-------------------------------------------
Time: 1592101614000 ms
-------------------------------------------
(hello,1)
(world,1)

查看 Kafka 消费进度

[dsy@hadoop131 bin]$ kafka-consumer-groups.sh --bootstrap-server hadoop131:9092 --describe --group dsy

GROUP           TOPIC           PARTITION  CURRENT-OFFSET  LOG-END-OFFSET  LAG             CONSUMER-ID                                     HOST            CLIENT-ID
dsy             dsy             0          14              14              0               consumer-1-4994820d-8f2b-466e-809e-9de2a4e5c6fa /192.168.5.178  consumer-1

DStream 转换

DStream上的操作与RDD的类似,分为Transformations(转换)和Output Operations(输出)两种。
此外,转换操作中还有一些比较特殊的原语,如updateStateByKey(),transform()以及各种Window相关的原语。

无状态转化操作

无状态转化操作就是把简单RDD转换操作应用到每个批次上,也就是转化DStream中的每一个RDD。
部分无状态转化操作列在了下表中。
注意,针对键值对的DStream转化操作(比如reduceByKey())要添加import StreamingContext._才能在Scala中使用。

Spark Streaming_第5张图片

注意:尽管这些函数看起来像作用在整个流上一样,但事实上每个DStream在内部是由许多RDD(批次)组成,且无状态转化操作是分别应用到每个RDD上的。

transform

transform允许DStream上执行任意的RDD-to-RDD函数。
即使这些函数并没有在DStream的API中暴露处理,通过该函数可以方便的扩展Spark API。
该函数每一批次调度一次。其实就是对DStream中的RDD应用转化。

  def main(args: Array[String]): Unit = {
    // 创建 SparkConf
    val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Transform Demo")

    // 创建 StreamingContext
    val ssc = new StreamingContext(conf, Seconds(3))

    // 创建 DStream
    val lineDStream: ReceiverInputDStream[String] = ssc.socketTextStream("localhost", 9999)

    // 转换为 RDD 操作
    // code Driver(1)
    val wordToCountDS: DStream[(String, Int)] = lineDStream.transform(rdd => {
      // code Driver(N)
      rdd.flatMap(_.split(" "))
        .map(word => {
          // code Executor(N)
          (word, 1)
        })
        .reduceByKey(_ + _)
    })

    // 打印
    wordToCountDS.print()

    // 启动
    ssc.start()
    ssc.awaitTermination()
  }
E:\DeveloperTools\netcat-win32-1.12>nc -lp 9999
ddd
hello world

-------------------------------------------
Time: 1592106597000 ms
-------------------------------------------
(ddd,1)

-------------------------------------------
Time: 1592106612000 ms
-------------------------------------------
(hello,1)
(world,1)

join

两个流之间的join需要两个流的批次大小一致,这样才能做到同时触发计算。
计算过程就是对当前批次的两个流中各自的RDD进行join,与两个RDD的join效果相同。

  def main(args: Array[String]): Unit = {
    // 1.创建 SparkConf
    val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("join demo")

    // 2.创建 StreamingContext
    val ssc = new StreamingContext(conf, Seconds(3))

    // 3.从端口获取数据创建流
    val lineDStream1: ReceiverInputDStream[String] = ssc.socketTextStream("localhost", 9999)
    val lineDStream2: ReceiverInputDStream[String] = ssc.socketTextStream("localhost", 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()
  }

有状态转化操作

updateStateByKey

updateStateByKey 原语用于记录历史记录。可以在DStream中跨批次维护状态。updateStateByKey()为我们提供了对一个状态变量的访问,用于键值对形式的DStream。给定一个由(键, 事件)对构成的DStream,并传递一个指定如何根据新的事件更新每个键对应状态的函数,它可以构建出一个新的DStream,其内部数据为(键, 状态)对。

updateStateByKey() 的结果会是一个新的DStream,其内部的RDD序列是由每个时间区间对应的(键, 状态)对组成的。

updateStateByKey 操作使得我们可以在用新信息进行更新时保持任意的状态。

  • 需要做下面两步:
    • 定义状态,状态可以是一个任意的数据类型。
    • 定义状态更新函数,用此函数阐明如何使用之前的状态和来自输入流的新值对状态进行更新。

使用 updateStateByKey 需要对检查点目录进行配置,会使用检查点来保存状态。

  def main(args: Array[String]): Unit = {
    val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("updateStateByKey demo")
    val ssc = new StreamingContext(conf, Seconds(3))
    ssc.checkpoint("ck")

    val lineDStream: ReceiverInputDStream[String] = ssc.socketTextStream("localhost", 9999)
    val wordDStream: DStream[String] = lineDStream.flatMap(_.split(" "))
    val wordToOneDStream: DStream[(String, Int)] = wordDStream.map((_, 1))

    // 使用 updateStateByKey 来更新状态,统计从运行开始以来单词总的次数
    // 参数values为当前批次单词频度,state为以往批次单词频度
    val stateDStream: DStream[(String, Int)] = wordToOneDStream
      .updateStateByKey((values: Seq[Int], state: Option[Int]) => {
      val currentCount: Int = values.foldLeft(0)(_ + _)
      val previousCount: Int = state.getOrElse(0)
      Some(currentCount + previousCount)
    })
    stateDStream.print()

    ssc.start()
    ssc.awaitTermination()
  }
E:\DeveloperTools\netcat-win32-1.12>nc -lp 9999
hello world
hello spark
hello sarah
dsy
dongsaiyuan
hello world

-------------------------------------------
Time: 1592122218000 ms
-------------------------------------------
(dongsaiyuan,1)
(dsy,1)
(hello,4)
(world,2)
(sarah,1)
(spark,1)

windowOperations

windowOperations 可以设置窗口的大小和滑动窗口的间隔来动态的获取当前Streaming的运行状态。
所有基于窗口的操作都需要两个参数,窗口时长&滑动步长

  • 窗口时长:计算内容的时间范围
  • 滑动步长:隔多久触发一次计算

注意:这两者都必须为采集周期大小的整数倍。

  • 3s一个批次,窗口9s,滑步3s
  def main(args: Array[String]): Unit = {
    val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("reduceByKeyAndWindow demo")
    val ssc = new StreamingContext(conf, Seconds(3))
    ssc.checkpoint("ck")

    val lineDStream: ReceiverInputDStream[String] = ssc.socketTextStream("localhost", 9999)
    val wordDStream: DStream[String] = lineDStream.flatMap(_.split(" "))
    val wordToOneDStream: DStream[(String, Int)] = wordDStream.map((_, 1))

    val wordToCountDStream: DStream[(String, Int)] = wordToOneDStream
      .reduceByKeyAndWindow((a: Int, b: Int) => (a + b), Seconds(9), Seconds(3))

    wordToCountDStream.print()

    ssc.start()
    ssc.awaitTermination()
  }
E:\DeveloperTools\netcat-win32-1.12>nc -lp 9999
hello
world
hello scala
hello sarah
hello spark
dsy sarah

-------------------------------------------
Time: 1592125857000 ms
-------------------------------------------
(hello,1)
(world,1)

-------------------------------------------
Time: 1592125860000 ms
-------------------------------------------
(hello,2)
(world,1)
(scala,1)

-------------------------------------------
Time: 1592125863000 ms
-------------------------------------------
(hello,2)
(sarah,1)
(scala,1)

-------------------------------------------
Time: 1592125866000 ms
-------------------------------------------
(hello,3)
(sarah,1)
(spark,1)
(scala,1)

-------------------------------------------
Time: 1592125869000 ms
-------------------------------------------
(hello,2)
(sarah,1)
(spark,1)

-------------------------------------------
Time: 1592125872000 ms
-------------------------------------------
(dsy,1)
(hello,1)
(sarah,1)
(spark,1)

-------------------------------------------
Time: 1592125875000 ms
-------------------------------------------
(dsy,1)
(sarah,1)

-------------------------------------------
Time: 1592125878000 ms
-------------------------------------------
(dsy,1)
(sarah,1)

-------------------------------------------
Time: 1592125881000 ms
-------------------------------------------

关于Window的操作还有如下方法:

  • 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"离开窗口的旧数据来实现这个操作。这个函数只适用于"可逆的reduce函数",也就是这些reduce函数有相应的"反reduce函数"(以参数invFunc形式传入)。
  def main(args: Array[String]): Unit = {
    val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("reduceByKeyAndWindow demo")
    val ssc = new StreamingContext(conf, Seconds(3))
    ssc.checkpoint("ck")

    val lineDStream: ReceiverInputDStream[String] = ssc.socketTextStream("localhost", 9999)
    val wordDStream: DStream[String] = lineDStream.flatMap(_.split(" "))
    val wordToOneDStream: DStream[(String, Int)] = wordDStream.map((_, 1))

    val wordToCountDStream: DStream[(String, Int)] = wordToOneDStream
      .reduceByKeyAndWindow(
        (a: Int, b: Int) => (a + b), // 加上新进入窗口的批次中的元素
        (a: Int, b: Int) => (a - b), // 移除离开窗口的老批次中的元素
        Seconds(9), // 窗口时长
        Seconds(3)) // 滑动步长

    wordToCountDStream.print()

    ssc.start()
    ssc.awaitTermination()
  }
E:\DeveloperTools\netcat-win32-1.12>nc -lp 9999
hello world
hello world
hello world

-------------------------------------------
Time: 1592129055000 ms
-------------------------------------------
(hello,2)
(world,2)

-------------------------------------------
Time: 1592129058000 ms
-------------------------------------------
(hello,3)
(world,3)

-------------------------------------------
Time: 1592129061000 ms
-------------------------------------------
(hello,3)
(world,3)

-------------------------------------------
Time: 1592129064000 ms
-------------------------------------------
(hello,1)
(world,1)

-------------------------------------------
Time: 1592129067000 ms
-------------------------------------------
(hello,0)
(world,0)

countByWindow()和countByValueAndWindow()作为对数据进行计数操作的简写。
countByWindow()返回一个表示每个窗口中元素个数的DStream,而countByValueAndWindow()返回的DStream则包含窗口中每个值的个数。

    val countDStream1: DStream[((String, Int), Long)] = wordToOneDStream
      .countByValueAndWindow(
        Seconds(9),
        Seconds(3))
    countDStream1.print()

    val countDStream2: DStream[Long] = wordToOneDStream
      .countByWindow(
        Seconds(9),
        Seconds(3))
    countDStream2.print()

DStream 输出

输出操作指定了对流数据经转化操作得到的数据所要执行的操作(例如把结果推入外部数据库或输出到屏幕上)
与RDD中的惰性求值类似,如果一个DStream及其派生出的DStream都没有被执行输出操作,那么这些DStream都不会被求值。
如果StreamingContext中没有设定输出操作,整个context就都不会启动。

输出操作如下:

  • print():在运行流程序的驱动结点上打印DStream中每一批次数据的最开始10个元素。这用于开发和调试。在Python API中,同样的操作叫print()。
  • saveAsTextFile(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://hadoop131:9000"), new Configuration(), "dsy")

    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://hadoop131:9000/stopSpark"))

      if (bool) {
        if (state == StreamingContextState.ACTIVE) {
          ssc.stop(stopSparkContext = true, stopGracefully = true)
          System.exit(0)
        }
      }
    }
  }
}

SparkTest

import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
import org.apache.spark.streaming.{Seconds, StreamingContext}

object SparkTest {

  def createSSC(): _root_.org.apache.spark.streaming.StreamingContext = {

    val update: (Seq[Int], Option[Int]) => Some[Int] = (values: Seq[Int], status: Option[Int]) => {

      //当前批次内容的计算
      val sum: Int = values.sum

      //取出状态信息中上一次状态
      val lastStatu: Int = status.getOrElse(0)

      Some(sum + lastStatu)
    }

    val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkTest")

    //设置优雅的关闭
    sparkConf.set("spark.streaming.stopGracefullyOnShutdown", "true")

    val ssc = new StreamingContext(sparkConf, Seconds(5))

    ssc.checkpoint("./ck")

    val line: ReceiverInputDStream[String] = ssc.socketTextStream("hadoop131", 9999)

    val word: DStream[String] = line.flatMap(_.split(" "))

    val wordAndOne: DStream[(String, Int)] = word.map((_, 1))

    val wordAndCount: DStream[(String, Int)] = wordAndOne.updateStateByKey(update)

    wordAndCount.print()

    ssc
  }

  def main(args: Array[String]): Unit = {

    val ssc: StreamingContext = StreamingContext.getActiveOrCreate("./ck", () => createSSC())

    new Thread(new MonitorStop(ssc)).start()

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

你可能感兴趣的:(Spark)