Spark学习之Spark Streaming(一)

Spark Streaming 实时数据流处理

一、Spark Streaming基础

1、Spark Streaming简介

官方文档

Spark Streaming是核心Spark API的扩展,可实现可扩展、高吞吐量、可容错的实时数据流处理。数据可以从诸如Kafka,Flume,Kinesis或TCP套接字等众多来源获取,并且可以使用由高级函数(如map,reduce,join和window)开发的复杂算法进行流数据处理。最后,处理后的数据可以被推送到文件系统,数据库和实时仪表板。而且,您还可以在数据流上应用Spark提供的机器学习和图处理算法。

2、Spark Streaming的特点

  • 便于使用

通过高级操作员构建应用程序。

Spark Streaming将Apache Spark的 语言集成API 引入流处理,使您可以像编写批处理作业一样编写流式作业。它支持Java,Scala和Python。

  • 容错

开箱即用的有状态的一次性语义。

Spark Streaming可以开箱即用,恢复丢失的工作和操作员状态(例如滑动窗口),而无需任何额外的代码。

  • Spark集成

将流式传输与批量和交互式查询相结合。

通过在Spark上运行,Spark Streaming允许您重复使用相同的代码进行批处理,将流加入历史数据,或者在流状态下运行即席查询。构建强大的交互式应用程序,而不只是分析。

3、Spark Streaming的内部结构

在内部,它的工作原理如下。Spark Streaming接收实时输入数据流,并将数据切分成批,然后由Spark引擎对其进行处理,最后生成“批”形式的结果流。



Spark Streaming将连续的数据流抽象为discretizedstream或DStream。在内部,DStream 由一个RDD序列表示。

4、第一个小案例:NetworkWordCount

(1)由于在本案例中需要使用netcat网络工具,所以需要先下载安装
(2)启动netcat数据流服务器,并监听端口:1234
命令:

nc -l -p 1234

服务器端:


(3)启动客户端

bin/run-example streaming.NetworkWordCount localhost 1234

客户端:


(一定注意):如果要执行本例,必须确保机器cpu核数大于2

5、开发自己的NetworkWordCount

MyNetworkWordCount.scala

object MyNetworkWordCount {
  def main(args: Array[String]): Unit = {
    //创建一个StreamingContext对象
    //注意:保证cpu核数大于等于2
    val conf = new SparkConf().setAppName("MyNetworkWordCount").setMaster("local[2]")
    //两个参数:1.conf 2.采样时间间隔:3s
    val ssc = new StreamingContext(conf, Seconds(3))
    //创建一个DStream
    val lines = ssc.socketTextStream("bigdata02", 5678, StorageLevel.MEMORY_ONLY)

    //进行单词计数,分词
    val words = lines.flatMap(_.split(" "))

    //计数,累加
    val wordCount = words.map((_, 1)).reduceByKey(_ + _)

   //val wordPair = words.transform(rdd => rdd.map(word=> (word, 1)))

    //打印结果
    wordCount.print()

    //启动StreamingContext,进行计算
    ssc.start()

    //等待任务的结束
    ssc.awaitTermination()
  }

}

(一定注意):

val sparkConf = new SparkConf().setAppName("NetworkWordCount").setMaster("local[2]")

官方的解释:
接收数据需要一个线程,处理数据需要一个线程

严格意义上:SparkStreaming不算真正的实时计算,他只是把连续的流式数据变成了不连续的离散数据.

二、Spark Streaming进阶

1、StreamingContext对象详解

初始化StreamingContext

第一种方式:从SparkConf对象中创建

    //创建一个StreamingContext对象
    val conf = new SparkConf().setAppName("MyNetworkWordCount").setMaster("local[2]")
    //两个参数:1.conf 2.采样时间间隔:3s
    val ssc = new StreamingContext(conf, Seconds(3))

第二种方式:SparkContext对象
sparkshell演示

bin/spark-shell --master spark://bigdata02:7077 --jars /home/bigdata/data/mysql-connector-java-5.1.40-bin.jar --driver-class-path /home/bigdata/data/mysql-connector-java-5.1.40-bin.jar

导入两个包

import org.apache.spark.streaming.StreamingContext
import org.apache.spark.streaming.Seconds
val ssc = new StreamingContext(sc,Seconds(3))

程序中的几点说明:

  • appName参数是应用程序在集群UI上显示的名称。

  • master是Spark,Mesos或YARN集群的URL,或者一个特殊的local [*]字符串来让程序以本地模式运行。

  • 当在集群上运行程序时,不需要在程序中硬编码master参数,而是使用spark-submit提交应用程序并将master的URL以脚本参数的形式传入。但是,对于本地测试和单元测试,您可以通过local[*]来运行Spark Streaming程序(请确保本地系统中的cpu核心数够用)。

  • StreamingContext会内在的创建一个SparkContext的实例(所有Spark功能的起始点),你可以通过ssc.sparkContext访问到这个实例。

  • 批处理的时间窗口长度必须根据应用程序的延迟要求和可用的集群资源进行设置。

请务必记住以下几点:

  • 一旦一个StreamingContextt开始运作,就不能设置或添加新的流计算。

  • 一旦一个上下文被停止,它将无法重新启动。

  • 同一时刻,一个JVM中只能有一个StreamingContext处于活动状态。

  • StreamingContext上的stop()方法也会停止SparkContext。 要仅停止StreamingContext(保持SparkContext活跃),请将stop() 方法的可选参数stopSparkContext设置为false。

  • 只要前一个StreamingContext在下一个StreamingContext被创建之前停止(不停止SparkContext),SparkContext就可以被重用来创建多个StreamingContext。

2、离散流(DStreams):Discretized Streams

DiscretizedStream或DStream 是Spark Streaming对流式数据的基本抽象。它表示连续的数据流,这些连续的数据流可以是从数据源接收的输入数据流,也可以是通过对输入数据流执行转换操作而生成的经处理的数据流。在内部,DStream由一系列连续的RDD表示,如下图:

  • 举例分析:在之前的NetworkWordCount的例子中,我们将一行行文本组成的流转换为单词流,具体做法为:将flatMap操作应用于名为lines的 DStream中的每个RDD上,以生成words DStream的RDD。如下图所示:


但是DStream和RDD也有区别,下面画图说明:


3、DStream中的转换操作(transformation)

最后两个transformation算子需要重点介绍一下:

  • 1.transform(func)

通过RDD-to-RDD函数作用于源DStream中的各个RDD,可以是任意的RDD操作,从而返回一个新的RDD

举例:在NetworkWordCount中,也可以使用transform来生成元组对

object MyNetworkWordCount {
  def main(args: Array[String]): Unit = {
    //创建一个StreamingContext对象
    //注意:保证cpu核数大于等于2
    val conf = new SparkConf().setAppName("MyNetworkWordCount").setMaster("local[2]")
    //两个参数:1.conf 2.采样时间间隔:3s
    val ssc = new StreamingContext(conf, Seconds(3))
    //创建一个DStream
    val lines = ssc.socketTextStream("bigdata02", 5678, StorageLevel.MEMORY_ONLY)

    //进行单词计数,分词
    val words = lines.flatMap(_.split(" "))

    //计数,累加
    //val wordCount = words.map((_, 1)).reduceByKey(_ + _)

    //在单词后面标注1
    val wordPair = words.transform(rdd => rdd.map(word=> (word, 1)))

    //打印结果
    //wordCount.print()
    wordPair.print()

    //启动StreamingContext,进行计算
    ssc.start()

    //等待任务的结束
    ssc.awaitTermination()
  }

}
  • 2.updateStateByKey(func)

操作允许不断用新信息更新它的同时保持任意状态。

    • 定义状态-状态可以是任何的数据类型
    • 定义状态更新函数-怎样利用更新前的状态和从输入流里面获取的新值更新状态

重写NetworkWordCount程序,累计每个单词出现的频率(注意:累计)

object MyNetworkTotalWordCount {
  def main(args: Array[String]): Unit = {
    System.setProperty("HADOOP_USER_NAME", "bigdata")
    //创建一个StreamingContext对象
    //注意:保证cpu核数大于等于2
    val conf = new SparkConf().setAppName("MyNetworkWordCount").setMaster("local[2]")
    //两个参数:1.conf 2.采样时间间隔:3s
    val ssc = new StreamingContext(conf, Seconds(3))
    //设置一个检查点目录,保存之前的状态信息

    ssc.checkpoint("hdfs://bigdata02:9000/SparkStreaming/checkpoint")

    //创建一个DStream
    val lines = ssc.socketTextStream("bigdata02", 5678, StorageLevel.MEMORY_ONLY)

    //进行单词计数,分词
    val words = lines.flatMap(_.split(" "))

    //每个单词计数
    val wordPair = words.map(x => (x, 1))

    //定义一个值函数
    /**
     * 第一个参数:当前值是多少
     * 第二个参数:之前的结果是多少
     */

    val updateFunc = (currVal: Seq[Int], preValueState: Option[Int]) => {
      //进行累加计算
      //1.把当前值的序列进行累加
      val currentTotal = currVal.sum
      //2.拿到之前的值
      val totalValue = preValueState.getOrElse(0)
      //返回
      Some(currentTotal + totalValue)
    }

    //进行累加运算
    val totalResult = wordPair.updateStateByKey(updateFunc)
    //输出
    totalResult.print()

    ssc.start()

    ssc.awaitTermination()

  }

}

输出结果:


4、窗口操作

Spark Streaming还提供了窗口计算功能,允许您在数据的滑动窗口上应用转换操作。下图说明了滑动窗口的工作方式:

如图所示,每当窗口滑过originalDStream时,落在窗口内的源RDD被组合并被执行操作以产生windowed DStream的RDD。在上面的例子中,操作应用于最近3个时间单位的数据,并以2个时间单位滑动。这表明任何窗口操作都需要指定两个参数。

  • 窗口长度(windowlength) - 窗口的时间长度(上图的示例中为:3)。

  • 滑动间隔(slidinginterval) - 两次相邻的窗口操作的间隔(即每次滑动的时间长度)(上图示例中为:2)。

这两个参数必须是源DStream的批间隔的倍数(上图示例中为:1)。

我们以一个例子来说明窗口操作。 假设您希望对之前的单词计数的示例进行扩展,每9秒钟对过去30秒的数据进行wordcount。为此,我们必须在最近30秒的pairs DStream数据中对(word, 1)键值对应用reduceByKey操作。这是通过使用reduceByKeyAndWindow操作完成的。

  • 窗口长度30s
  • 滑动距离:9s

窗口数据流的滑动距离(持续时间)必须是窗口长度的倍数

object MyNetworkWordCountByWindow {
  def main(args: Array[String]): Unit = {
    Logger.getLogger("org.apache.spark").setLevel(Level.ERROR)
    //创建一个StreamingContext对象
    //注意:保证cpu核数大于等于2
    val conf = new SparkConf().setAppName("MyNetworkWordCount").setMaster("local[2]")
    //两个参数:1.conf 2.采样时间间隔:3s
    val ssc = new StreamingContext(conf, Seconds(3))
    //创建一个DStream
    val lines = ssc.socketTextStream("bigdata02", 5678, StorageLevel.MEMORY_ONLY)

    //进行单词计数,分词
    val words = lines.flatMap(_.split(" "))

    //计数,累加
    //val wordCount = words.map((_, 1)).reduceByKey(_ + _)
    val wordPair = words.map(x => (x, 1))

    //每隔10s钟,把过去30s产生的字符串进行计数
    val result = wordPair.reduceByKeyAndWindow((a: Int, b: Int) => (a + b), Seconds(30), Seconds(10))

    //val wordPair = words.transform(rdd => rdd.map(word=> (word, 1)))

    //打印结果
    result.print()

    //启动StreamingContext,进行计算
    ssc.start()

    //等待任务的结束
    ssc.awaitTermination()

  }

}

一些常见的窗口操作如下表所示。所有这些操作都用到了上述两个参数 - windowLength和slideInterval。

  • window(windowLength, slideInterval)

基于源DStream产生的窗口化的批数据计算一个新的DStream

  • countByWindow(windowLength, slideInterval)

返回流中元素的一个滑动窗口数

  • reduceByWindow(func, windowLength, slideInterval)

返回一个单元素流。利用函数func聚集滑动时间间隔的流的元素创建这个单元素流。函数必须是相关联的以使计算能够正确的并行计算。

  • reduceByKeyAndWindow(func, windowLength, slideInterval, [numTasks])

应用到一个(K,V)对组成的DStream上,返回一个由(K,V)对组成的新的DStream。每一个key的值均由给定的reduce函数聚集起来。注意:在默认情况下,这个算子利用了Spark默认的并发任务数去分组。你可以用numTasks参数设置不同的任务数

  • reduceByKeyAndWindow(func, invFunc, windowLength, slideInterval, [numTasks])

上述reduceByKeyAndWindow() 的更高效的版本,其中使用前一窗口的reduce计算结果递增地计算每个窗口的reduce值。这是通过对进入滑动窗口的新数据进行reduce操作,以及“逆减(inverse reducing)”离开窗口的旧数据来完成的。一个例子是当窗口滑动时对键对应的值进行“一加一减”操作。但是,它仅适用于“可逆减函数(invertible reduce functions)”,即具有相应“反减”功能的减函数(作为参数invFunc)。 像reduceByKeyAndWindow一样,通过可选参数可以配置reduce任务的数量。 请注意,使用此操作必须启用检查点。

  • countByValueAndWindow(windowLength, slideInterval, [numTasks])

应用到一个(K,V)对组成的DStream上,返回一个由(K,V)对组成的新的DStream。每个key的值都是它们在滑动窗口中出现的频率。

5、输入DStreams和接收器

输入DStreams表示从数据源获取输入数据流的DStreams。在NetworkWordCount例子中,lines表示输入DStream,它代表从netcat服务器获取的数据流。每一个输入流DStream和一个Receiver对象相关联,这个Receiver从源中获取数据,并将数据存入内存中用于处理。

输入DStreams表示从数据源获取的原始数据流。Spark Streaming拥有两类数据源:

  • 基本源(Basic sources):这些源在StreamingContext API中直接可用。例如文件系统、套接字连接、Akka的actor等

  • 高级源(Advanced sources):这些源包括Kafka,Flume,Kinesis,Twitter等等。

下面通过具体的案例,详细说明:

  • 文件流 :通过监控文件系统的变化, 若有新文件添加 ,则将它读入并作为数据流

需要注意的是:

① 这些文件具有相同的格式

② 这些文件通过原子移动或重命名文件的方式在dataDirectory创建

③ 如果在文件中追加内容,这些追加的新数据也不会被读取。

object FileStreaming {
  def main(args: Array[String]): Unit = {
    Logger.getLogger("org.apache.spark").setLevel(Level.ERROR)

    //创建一个StreamingContext对象,以local模式为例
    //注意:保证cpu核数大于等于2
    val conf = new SparkConf().setAppName("FileStreaming").setMaster("local[2]")
    //两个参数:1.conf参数  2.采样时间间隔:每隔3s
    val ssc = new StreamingContext(conf, Seconds(3))

    //直接监控本地的某个目录,如果有新的文件产生,就读取进来-----> 直接打印
    val lines = ssc.textFileStream("E:\\RmDownloads\\spartdemo")
    //直接打印
    lines.print()

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

注意:要演示成功,需要在原文件中编辑,然后拷贝一份。


RDD队列流

使用streamingContext.queueStream(queueOfRDD)创建基于RDD队列的DStream,用于调试Spark Streaming应用程序。

object RDDQueueStream {
  def main(args: Array[String]): Unit = {
    Logger.getLogger("org.apache.spark").setLevel(Level.ERROR)

    val conf = new SparkConf().setAppName("RDDQueueStream").setMaster("local[2]")
    //两个参数:1、conf参数    2、采样时间间隔:每隔1秒
    val ssc = new StreamingContext(conf,Seconds(1))

    //需要有一个队列,往队列中放数据 ------>  本质:就是创建了一个数据源
    val rddQueue = new Queue[RDD[Int]]()

    for(i<- 1 to 3){
      rddQueue += ssc.sparkContext.makeRDD(1 to 10)
      //睡一秒钟
      Thread.sleep(1000)
    }

    //从队列中接收数据,创建DStream
    val inputDStream = ssc.queueStream(rddQueue)

    //处理数据
    val result = inputDStream.map(x => (x,x*10))
    result.print()

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

套接字流:通过监听Socket端口来接收数据

object ScoketStreaming {

  def main(args: Array[String]) {
    //创建一个本地的StreamingContext,含2个工作线程
    val conf = new SparkConf().setMaster("local[4]").setAppName("ScoketStreaming")
    val sc = new StreamingContext(conf,Seconds(5))   //每隔10秒统计一次字符总数

    //创建珍一个DStream,连接bigdata02 :7788
    val lines = sc.socketTextStream("bigdata02",7788)
    //打印数据
    lines.print()

    sc.start()         //开始计算
    sc.awaitTermination()   //通过手动终止计算,否则一直运行下去
  }
}

6、DStreams的输出操作

输出操作允许DStream的操作推到如数据库、文件系统等外部系统中。因为输出操作实际上是允许外部系统消费转换后的数据,它们触发的实际操作是DStream转换。目前,定义了下面几种输出操作:

foreachRDD的设计模式

DStream.foreachRDD是一个强大的原语,发送数据到外部系统中。

第一步:创建连接,将数据写入外部数据库(使用之前的NetworkWordCount,改写之前输出结果的部分,如下)

object MyNetworkWordCountDataFrame {
  def main(args: Array[String]): Unit = {
    //创建一个Context对象: StreamingContext  (SparkContext, SQLContext)
    //指定批处理的时间间隔
    val conf = new SparkConf().setAppName("MyNetworkWordCountDataFrame").setMaster("local[2]")
    val ssc = new StreamingContext(conf,Seconds(5))

    //创建一个DStream,处理数据
    val lines  = ssc.socketTextStream("bigdata02",5678,StorageLevel.MEMORY_AND_DISK_SER)

    //执行wordcount
    val words = lines.flatMap(_.split(" "))

    val wordPair = words.map(x => (x,1))

    val wordCountResult = wordPair.reduceByKey(_ + _)
    //val wordCountResult = wordPair.reduceByKeyAndWindow((a:Int,b:Int) => (a+b), Seconds(30),Seconds(10))

    //输出结果
    //wordCountResult.print()
    wordCountResult.foreachRDD(rdd =>{


        var conn:Connection = null
        var pst:PreparedStatement = null

        try {
          conn = DriverManager.getConnection("jdbc:mysql://bigdata02:3306/sqoopdb", "bigdata", "123456")

          rdd.foreach(record => {
            pst = conn.prepareStatement("insert into myresult values(?,?)")
            pst.setString(1, record._1)
            pst.setInt(2, record._2)
            //执行
            pst.executeUpdate()
          })
        }catch{
          case e1:Exception => e1.printStackTrace()
        }finally {
          if(pst != null) pst.close()
          if(conn != null) conn.close()
        }
      })


    //启动StreamingContext
    ssc.start()

    //等待计算完成
    ssc.awaitTermination()
  }
}

出现以下Exception:


原因是:Connection对象不是一个可被序列化的对象,不能RDD的每个Worker上运行;即:Connection不能在RDD分布式环境中的每个分区上运行,因为不同的分区可能运行在不同的Worker上。所以需要在每个RDD分区上单独创建Connection对象。

第二步:在每个RDD分区上单独创建Connection对象,如下:

create table myresult(name VARCHAR(25), number INT(11));
object MyNetworkWordCountDataFrame {
  def main(args: Array[String]): Unit = {
    //创建一个Context对象: StreamingContext  (SparkContext, SQLContext)
    //指定批处理的时间间隔
    val conf = new SparkConf().setAppName("MyNetworkWordCountDataFrame").setMaster("local[2]")
    val ssc = new StreamingContext(conf,Seconds(5))

    //创建一个DStream,处理数据
    val lines  = ssc.socketTextStream("bigdata02",5678,StorageLevel.MEMORY_AND_DISK_SER)

    //执行wordcount
    val words = lines.flatMap(_.split(" "))

    val wordPair = words.map(x => (x,1))

    val wordCountResult = wordPair.reduceByKey(_ + _)
    //val wordCountResult = wordPair.reduceByKeyAndWindow((a:Int,b:Int) => (a+b), Seconds(30),Seconds(10))

    //输出结果
    //wordCountResult.print()
    wordCountResult.foreachRDD(rdd =>{
      rdd.foreachPartition(partitionRecord =>{

        var conn:Connection = null
        var pst:PreparedStatement = null

        try {
          conn = DriverManager.getConnection("jdbc:mysql://bigdata02:3306/sqoopdb", "bigdata", "123456")

          partitionRecord.foreach(record => {
            pst = conn.prepareStatement("insert into myresult values(?,?)")
            pst.setString(1, record._1)
            pst.setInt(2, record._2)
            //执行
            pst.executeUpdate()
          })
        }catch{
          case e1:Exception => println("Some Error: " + e1.getMessage)
        }finally {
          if(pst != null) pst.close()
          if(conn != null) conn.close()
        }
      })
    })

    //启动StreamingContext
    ssc.start()

    //等待计算完成
    ssc.awaitTermination()
  }
}

7、DataFrame和SQL操作

我们可以很方便地使用DataFrames和SQL操作来处理流数据。您必须使用当前的StreamingContext对应的SparkContext创建一个SparkSession。此外,必须这样做的另一个原因是使得应用可以在driver程序故障时得以重新启动,这是通过创建一个可以延迟实例化的单例SparkSession来实现的。

在下面的示例中,我们使用DataFrames和SQL来修改之前的wordcount示例并对单词进行计数。我们将每个RDD转换为DataFrame,并注册为临时表,然后在这张表上执行SQL查询。

object MyNetworkWordCountWithSQL {
  def main(args: Array[String]): Unit = {
    Logger.getLogger("org.apache.spark").setLevel(Level.ERROR)
    //创建一个StreamingContext对象,以local模式为例
    //注意:保证cpu核数大于等于2
    val conf = new SparkConf().setAppName("WordCountWithSQL").setMaster("local[2]")
    //两个参数:1.conf参数  2.采样时间间隔:每隔3s
    val ssc = new StreamingContext(conf, Seconds(3))

    //创建一个DStream,从netcat服务器接收数据
    val lines = ssc.socketTextStream("bigdata02", 5678, StorageLevel.MEMORY_ONLY)

    //进行单词计数,分词
    val words = lines.flatMap(_.split(" "))


    //集成Spark SQL,使用SQL语句进行WordCount
    words.foreachRDD(rdd =>{
      //创建一个SparkSession对象
      val spark = SparkSession.builder().config(rdd.sparkContext.getConf).getOrCreate()

      //把rdd转成一个DataFrame
      import spark.implicits._
      val wordsDataFrame = rdd.toDF("word")  //------> 表df1:只有一个列“word”

      //创建视图
      wordsDataFrame.createOrReplaceTempView("words")

      //执行SQL,通过SQL执行WordCount
      spark.sql("select word ,count(*) as total from words group by word").show
    })

    ssc.start()

    ssc.awaitTermination()

  }
}

8、缓存/持久化

与RDD类似,DStreams还允许开发人员将流数据保留在内存中。也就是说,在DStream上调用persist() 方法会自动将该DStream的每个RDD保留在内存中。如果DStream中的数据将被多次计算(例如,相同数据上执行多个操作),这个操作就会很有用。对于基于窗口的操作,如reduceByWindow和reduceByKeyAndWindow以及基于状态的操作,如updateStateByKey,数据会默认进行持久化。 因此,基于窗口的操作生成的DStream会自动保存在内存中,而不需要开发人员调用persist()。

对于通过网络接收数据(例如Kafka,Flume,sockets等)的输入流,默认持久化级别被设置为将数据复制到两个节点进行容错。

请注意,与RDD不同,DStreams的默认持久化级别将数据序列化保存在内存中。

9、检查点支持

流数据处理程序通常都是全天候运行,因此必须对应用中逻辑无关的故障(例如,系统故障,JVM崩溃等)具有弹性。为了实现这一特性,Spark Streaming需要checkpoint足够的信息到容错存储系统,以便可以从故障中恢复。

① 一般会对两种类型的数据使用检查点:

  1. 元数据检查点(Metadatacheckpointing)

将定义流计算的信息保存到容错存储中(如HDFS)。这用于从运行streaming程序的driver程序的节点的故障中恢复。元数据包括以下几种:

  • 配置(Configuration)

      • 用于创建streaming应用程序的配置信息。
  • DStream操作(DStream operations)

      • 定义streaming应用程序的DStream操作集合。
  • 不完整的batch(Incomplete batches)

      • jobs还在队列中但尚未完成的batch。
  1. 数据检查点(Datacheckpointing)

将生成的RDD保存到可靠的存储层。对于一些需要将多个批次之间的数据进行组合的stateful变换操作,设置数据检查点是必需的。在这些转换操作中,当前生成的RDD依赖于先前批次的RDD,这导致依赖链的长度随时间而不断增加,由此也会导致基于血统机制的恢复时间无限增加。为了避免这种情况,stateful转换的中间RDD将定期设置检查点并保存到到可靠的存储层(例如HDFS)以切断依赖关系链。

总而言之,元数据检查点主要用于从driver程序故障中恢复,而数据或RDD检查点在任何使用stateful转换时是必须要有的。

② 何时启用检查点:

对于具有以下任一要求的应用程序,必须启用检查点:

  1. 使用状态转:如果在应用程序中使用updateStateByKey或reduceByKeyAndWindow(具有逆函数),则必须提供检查点目录以允许定期保存RDD检查点。

  2. 从运行应用程序的driver程序的故障中恢复:元数据检查点用于使用进度信息进行恢复。

③ 如何配置检查点:

可以通过在一些可容错、高可靠的文件系统(例如,HDFS,S3等)中设置保存检查点信息的目录来启用检查点。这是通过使用streamingContext.checkpoint(checkpointDirectory)完成的。设置检查点后,您就可以使用上述的有状态转换操作。此外,如果要使应用程序从驱动程序故障中恢复,您应该重写streaming应用程序以使程序具有以下行为:

  1. 当程序第一次启动时,它将创建一个新的StreamingContext,设置好所有流数据源,然后调用start()方法。

  2. 当程序在失败后重新启动时,它将从checkpoint目录中的检查点数据重新创建一个StreamingContext。

使用StreamingContext.getOrCreate可以简化此行为

④ 改写之前的WordCount程序,使得每次计算的结果和状态都保存到检查点目录下

object MyCheckpointNetworkWC {

  def main(args: Array[String]): Unit = {
    System.setProperty("HADOOP_USER_NAME", "bigdata")
    //在主程序中,创建一个Streaming Context对象
    //1、读取一个检查点的目录
    //2、如果该目录下已经存有之前的检查点信息,从已有的信息上创建这个Streaming Context对象
    //3、如果该目录下没有信息,创建一个新的Streaming Context
    val context = StreamingContext.getOrCreate("hdfs://bigdata02:9000/spark_checkpoint",createStreamingContext)

    //启动任务
    context.start()
    context.awaitTermination()
  }

  //创建一个StreamingContext对象,并且设置检查点目录,执行WordCount程序(记录之前的状态信息)
  def createStreamingContext():StreamingContext = {
    val conf = new SparkConf().setAppName("MyCheckpointNetworkWC").setMaster("local[2]")
    //创建这个StreamingContext对象
    val ssc = new StreamingContext(conf,Seconds(3))

    //设置检查点目录
    ssc.checkpoint("hdfs://bigdata02:9000/spark_checkpoint")

    //创建一个DStream,执行WordCount
    val lines = ssc.socketTextStream("bigdata02",5678,StorageLevel.MEMORY_AND_DISK_SER)

    //分词操作
    val words = lines.flatMap(_.split(" "))
    //每个单词记一次数
    val wordPair = words.map(x=> (x,1))

    //执行单词计数
    //定义一个新的函数:把当前的值跟之前的结果进行一个累加
    val addFunc = (currValues:Seq[Int],preValueState:Option[Int]) => {
      //当前当前批次的值
      val currentCount = currValues.sum

      //得到已经累加的值。如果是第一次求和,之前没有数值,从0开始计数
      val preValueCount = preValueState.getOrElse(0)

      //进行累加,然后累加后结果,是Option[Int]
      Some(currentCount + preValueCount)
    }

    //要把新的单词个数跟之前的结果进行叠加(累计)
    val totalCount = wordPair.updateStateByKey[Int](addFunc)

    //输出结果
    totalCount.print()

    //返回这个对象
    ssc
  }
}


不同写法

object MyCheckPointNetWorkWordCount {
  def main(args: Array[String]): Unit = {
    System.setProperty("HADOOP_USER_NAME", "bigdata")
    Logger.getLogger("org.apache.spark").setLevel(Level.ERROR)
    //通过StreamingContext直接创建一个StreamingContext对象 ----> 直接指定检查点
    val ssc = StreamingContext.getOrCreate("hdfs://bigdata02:9000/sparkckpt0", creatingFunc)

    //创建一个DStream,从netcat服务器接收数据
    val lines = ssc.socketTextStream("bigdata02", 5678, StorageLevel.MEMORY_ONLY)

    //进行单词计数,分词
    val words = lines.flatMap(_.split(" "))
    //计数
    val wordCount = words.map((_, 1)).reduceByKey(_ + _)
    //使用transform完成生成一个数组,完成跟map一样的作用
    // val wordPair = words.transform(x => x.map(x => (x, 1)))

    //打印结果
    wordCount.print()

    //启动StreamingContext,进行计算
    ssc.start()

    //等待任务的结束
    ssc.awaitTermination()
  }

  def creatingFunc():StreamingContext = {
    val sparkConf = new SparkConf().setAppName("MyCheckPointNetWorkWordCount").setMaster("local[2]")
    val ssc = new StreamingContext(sparkConf,Seconds(3))

    //设置检查点目录
    ssc.checkpoint("hdfs://bigdata02:9000/sparkckpt0")

    //返回对象
    ssc
  }
}

通过查看HDFS中的信息,可以看到相关的检查点信息,如下:

你可能感兴趣的:(Spark学习之Spark Streaming(一))