Spark 从 0 到 1 学习(8) —— Spark Streaming

1. SparkStreaming 简介

SparkStreaming 是流式处理框架,是 Spark API 的扩展。支持可扩展、高吞吐量、容错的实时数据流处理。实时数据的来源可以是:Kafka、Flume、Twitter、ZeroMq 或者是 TCP sockets,并且可以使用高级功能的复杂算子来处理流数据。例如:map、reduce、join、window。最终,处理后的数据可以存放在文件系统、数据库等,方便实时展示。

2. SparkStreaming 与 Storm 的区别

  1. Storm 是纯实时的流式处理框架,SparkStreaming是准实时的处理框架 (微批处理)。因为微批处理,SparkStreaming 的吞吐量比 Storm 要高。
  2. Storm 的事物机制要比 SparkStreaming 的完善。
  3. Storm 支持动态资源调度。(Spark1.2 开始也支持)。
  4. SparkStreaming 擅长复杂的业务处理。Storm 不擅长复杂的业务处理。擅长简单的汇总行的计算。

3. SparkStreaming 初始

3.1 SparkStreaming 初始理解

在这里插入图片描述

注意:

  • receiver task 是 7*24 小时一直在执行,一直接受数据。将一段时间内接收来的数据保存到 batch 中。假设 batchInterval 为 5s,那么会将接收过来的数据每隔 5s 封装到一个 batch 中,batch 没有分布式计算特性,这一个 batch 的数据又被封装到一个 RDD 中,RDD 最终封装到一个 DStream 中。

    例如:假设 batchInterval 为 5s,每隔 5s 通过 SparkStreaming 将得到一个 Dstream,然后计算这 5s 的数据。假设执行任务的时间是 3s,那么第 6~9 秒一边接收数据,一边在计算任务,9~10 秒只是在接收数据。然后再第 11s 的时候重复上面的操作。

  • 如果 job 执行的时间大于 batchInterval 会有什么样的问题?

    如果接收过来的数据设置的级别是仅内存,接收过来的数据会越积越多。最后可能会导致 OOM。如果设置 StorageLevel 包含 disk,则内存存放不下的数据会溢写到磁盘,这样会加大延迟。

3.2 SparkStreaming 代码

3.2.1 socket 生产数据

import org.apache.commons.lang3.RandomUtils;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class NcServer implements Runnable{
    
    
    String[] words = {
    
    "java","python","spark","flink","hive","hadoop","hbase"};

    private Socket socket;


    public NcServer(Socket socket) {
    
    
        this.socket = socket;
    }


    public static void main(String[] args) throws Exception{
    
    
        ServerSocket serverSocket = new ServerSocket(8080);
        ExecutorService es = Executors.newSingleThreadExecutor();
        while (true){
    
    
            Socket socket = serverSocket.accept();
            System.out.println("接受请求");
            es.execute(new NcServer(socket));
        }
    }


    @Override
    public void run() {
    
    
        try {
    
    
            PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
            while (true) {
    
    
                String word = words[RandomUtils.nextInt(0,words.length)];
                System.out.println(word);
                out.write(word+"\n");
                out.flush();
                TimeUnit.MILLISECONDS.sleep(1000);
            }
        }catch (Exception e){
    
    
            e.printStackTrace();
        }finally {
    
    
            try {
    
    
                socket.close();
            } catch (IOException e) {
    
    
                e.printStackTrace();
            }
        }
    }
}

3.2.2 SparkStreaming 代码注意事项

  • receiver 模式下接收数据,local 的模拟线程必须大于等于 2,一个线程用来 receiver 接收数据。另一个线程用来执行 job。
  • Durations时间设置就是我们能接收数据的延迟度。这个需要根据集群的资源情况以及任务的执行情况来调节。
  • 创建 StreamingContext方式有两种:StreamingContext(conf,Durations.seconds(5))StreamingContext(sc,Durations.seconds(5))
  • 所有的代码逻辑完成后要有一个 output operation 类算子。
  • StreamingContext.start()后不能添加业务逻辑
  • StreamingContext.stop()无参的 stop 方法将 SparkContext一同关闭。stop(false),不会关闭 SparkContext。并且之后不能调用 start()
import org.apache.spark.rdd.RDD
import org.apache.spark.streaming.dstream.{
    
    DStream, ReceiverInputDStream}
import org.apache.spark.streaming.{
    
    Durations, StreamingContext}
import org.apache.spark.{
    
    SparkConf, SparkContext}
object WordCountFromSocket {
    
    
  def main(args: Array[String]): Unit = {
    
    
    val conf = new SparkConf()
    conf.setAppName("WordCountOnLine").setMaster("local[2]")
    // 通过 SparkConf 创建 StreamingContext 这种方式默认会创建SparkContext
    val ssc = new StreamingContext(conf,Durations.seconds(5))
    ssc.sparkContext.setLogLevel("ERROR")
    // 通过 SparkContext 创建 StreamingContext
    //val sc = new SparkContext(conf)
    //val ssc = new StreamingContext(sc,Durations.seconds(5))
    //从ssc中获取SparkContext()
//    val context: SparkContext = ssc.sparkContext

    val lines: ReceiverInputDStream[String] = ssc.socketTextStream("127.0.0.1",8080)
    val words: DStream[String] = lines.flatMap(line=>{
    
    line.split(" ")})
    val pairWords: DStream[(String, Int)] = words.map(word=>{
    
    (word,1)})
    val result: DStream[(String, Int)] = pairWords.reduceByKey((v1, v2)=>{
    
    v1+v2})
	// 输出
    result.print()
    // 启动
    ssc.start()
    ssc.awaitTermination()
    ssc.stop()

  }
}

4. SparkStreaming 算子操作

4.1 foreachRDD

output operation 算子,必须对抽取出来的 RDD 执行 action 类算子,代码才能执行。

result.foreachRDD(wordCountRDD=>{
    
    
    
    println("******* 在 Driver 端执行 *******")
    
    val sortRDD: RDD[(String, Int)] = wordCountRDD.sortByKey(false)
    val result: RDD[(String, Int)] = sortRDD.filter(tp => {
    
    
        println("******* Executor 端执行 *******")
        true
    })
    result.foreach(println)
})

注意事项:

  • foreachRDD中可以拿到 DStream中的 RDD,对 RDD 进行操作,但是一点要使用 RDD 的 action 算子触发执行,不然 DStream 的逻辑也不会执行。
  • froeachRDD算子内,除了拿到的 RDD 算子操作外,这段代码是在 Driver 端执行的,可以利用这点做到动态的改变广播变量。

4.2 transform

  • transformation 类算子

  • 可以通过 transform 算子,对 DStream 做 RDD 的任意操作。

    //广播变量
    val filterWord = ListBuffer[String]("java","flink")
    val broadcastWord: Broadcast[ListBuffer[String]] = ssc.sparkContext.broadcast(filterWord)
    
    words.transform(rdd =>{
          
          
         println("在 Driver 端执行")
         // 修改广播变量
         val bword: ListBuffer[String] = broadcastWord.value
         bword += "hadoop"
         println(broadcastWord.value)
         val filterRdd = rdd.filter((word: String) => {
          
          
             val wordList = broadcastWord.value
             !wordList.contains(word)
         })
         filterRdd
     }).map(word =>(word,1)).reduceByKey(_+_).print()
    

注意事项:

  • transform算子可以拿到 DStream 中的 RDD,对 RDD 使用 RDD 的算子操作,但是最后要返回 RDD,返回的 RDD 又被封装到一个 DStream。
  • transform中拿到的 RDD 的算子外,代码是在 Driver 端执行的。可以做到动态的改变广播变量。

4.3 updateStateByKey

  • transformation 算子

  • updateStateByKey 作用:

    1. 为 SparkStreaming 中每个 key 维护一份 state 状态,state 类型可以是任意类型的,也可以是一个自定义的对象。更新函数也可以自定义。
    2. 通过更新函数对该 key 的状态不断更新。对于每个新的 batch 而言,SparkStreaming 会在使用 updateStateByKey的时候为已经存在的 key 进行 state 的状态更新。
  • 使用 updateStateByKey要开启 checkpoint 机制和功能。

  • 什么时候会把内存中的 state 写入到磁盘?

    如果 batchInterval 设置的时间小于 10s,那么每 10s 写入磁盘一次。

    如果 batchInterval 设置的时间大于 10s,那么就会按照 batchInterval 的时间间隔写入磁盘一次。这样做的目的是减少写入磁盘的次数。

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

object UpdateStateByKey {
    
    

  def main(args: Array[String]): Unit = {
    
    
    val conf = new SparkConf();
    conf.setAppName("UpdateStateByKey")
    conf.setMaster("local[2]")
    val ssc = new StreamingContext(conf,Durations.seconds(5))
    // 设置日志级别
    ssc.sparkContext.setLogLevel("ERROR")
    val host = "127.0.0.1"
    val port = 8080
    val lines: ReceiverInputDStream[String] = ssc.socketTextStream(host,port)
    val words = lines.map(word => (word,1))
    val path = "/app/checkpoint/updatStateByKey";
    ssc.sparkContext.setCheckpointDir(path)
    /**
      * currentValues:当前批次某个 key 对应所有的 value 组成的一个集合。
      * preValue:以往批次当前 key 对应的总状态值。
      */
    val result: DStream[(String, Int)] = words.updateStateByKey((currentValues: Seq[Int], preValue: Option[Int]) => {
    
    
      var totalValue = preValue.getOrElse(0)
      for (value <- currentValues) {
    
    
        totalValue += value
      }
      Option(totalValue)
    })
    result.print()
    ssc.start()
    ssc.awaitTermination()
    ssc.stop()
  }
}

4.4 window

4.4.1 window 操作图解

在这里插入图片描述

假设每隔 5 秒 1 个batch,上图中窗口长度为 15s,窗口滑动间隔 10s。

窗口长度和滑动间隔必须是 batchInterval 的整数倍。如果不是整数倍会检测报错。

4.4.2 window 操作优化

在这里插入图片描述

  • checkpoint保存上一次执行的结果。
  • 执行当前 window 操作的时候,把上一次的执行结果加上这次的结果,然后减去这次滑出去的结果。
object WindowWordCount {
    
    
  
  def main(args:Array[String]) {
    
    
    Logger.getLogger("org.apache.spark").setLevel(Level.ERROR)
    Logger.getLogger("org.eclipse.jetty.server").setLevel(Level.OFF)
    val conf = new SparkConf().setAppName("WindowWordCount").setMaster("local[2]")
    val sc = new SparkContext(conf)
    //创建StreamingContext
    val ssc = new StreamingContext(sc, Seconds(5))
    

    //获取从Socket发送过来的数据
     val hostname = "127.0.0.1"
     val port = 8080
     val lines = ssc.socketTextStream(hostname, port, StorageLevel.MEMORY_ONLY_SER)
     val words = lines.flatMap(_.split(","))
     
     // windows操作
     words.map(x =>(x, 1)).reduceByKeyAndWindow((a:Int, b:Int) => (a+b),Seconds(5), Seconds(5)).print();
     // 优化 window 操作
     // 定义checkpoint目录
     val path = "/app/checkpoint/window"
    ssc.checkpoint(path)
    words.map(w =>(w,1)).reduceByKeyAndWindow(
      (v1:Int,v2:Int) => {
    
    v1+v2},
      (v1:Int,v2:Int) => {
    
    v1-v2},
      Seconds(15),
      Seconds(5)
    ).print()
     
     ssc.start()
     ssc.awaitTermination()
  }
}

5. Driver HA (Standalone 或 Mesos)

因为 SparkStreaming 是7*24 小时运行,Driver 是一个简单的进程,有可能会挂掉,所以实现 Driver 的 HA 就有必要。如果使用的 Client 模式无法实现 Driver HA。这里针对的是 cluster 模式。Yarn 平台 cluster 模式提交任务,AM (ApplicationMaster) 相当于 Driver,如果挂掉会自动启动 AM 。这里所说的 Driver HA 针对的是 Spark standalone 和 Mesos 资源调度的情况下。实现 Driver 的高可用有两个步骤:

  1. 第一 :提交任务的时候要加上 --supervise,当 Driver 挂掉的时候回自动重启 Driver。

  2. 第二:使用 StreamingContext.getOrCreate(checkpointPath,creatingFunc)

    Driver中元数据包括:

    • 创建应用程序的配置信息。
    • DStream 的操作逻辑。
    • job 中没有完成的批次数据,也就是 job 的执行进度。
    package com.abcft.spark.streaming
    
    import org.apache.log4j.{
          
          Level, Logger}
    import org.apache.spark.{
          
          SparkConf, SparkContext}
    import org.apache.spark.storage.StorageLevel
    import org.apache.spark.streaming.{
          
          Seconds, StreamingContext}
    
    /**
      * Driver HA :
      * 1.在提交application的时候  添加 --supervise 选项  如果Driver挂掉 会自动启动一个Driver
      * 2.代码层面恢复Driver(StreamingContext)
      *
      */
    object WindowWordCountDriverHA {
          
          
      val checkpointPath = "/app/checkpoint/driverHA"
    
      def main(args:Array[String]) {
          
          
    
        /**
          * StreamingContext.getOrCreate(checkpointPath,createStreamingContext)
          *   这个方法首先会从 checkpointPath 目录中获取StreamingContext【 因为StreamingContext是序列化存储在Checkpoint目录中,恢复时会尝试反序列化这些objects。
          *   如果用修改过的 class 可能会导致错误,此时需要更换 checkpoint 目录或者删除 checkpoint 目录中的数据,程序才能起来。】
          *
          *   若能获取回来StreamingContext,就不会执行 createStreamingContext 这个方法创建,否则就会创建
          *
          */
        val ssc: StreamingContext = StreamingContext.getOrCreate(checkpointPath,createStreamingContext)
        ssc.start()
        ssc.awaitTermination()
      }
    
      def createStreamingContext():StreamingContext ={
          
          
        Logger.getLogger("org.apache.spark").setLevel(Level.ERROR)
        Logger.getLogger("org.eclipse.jetty.server").setLevel(Level.OFF)
        val conf = new SparkConf().setAppName("WindowWordCount").setMaster("local[2]")
        val sc = new SparkContext(conf)
        //创建StreamingContext
        val ssc = new StreamingContext(sc, Seconds(5))
    
        //义checkpoint目录
        ssc.checkpoint(checkpointPath)
        //获取从Socket发送过来的数据
        val hostname = "127.0.0.1"
        val port = 8080
        val lines = ssc.socketTextStream(hostname, port, StorageLevel.MEMORY_ONLY_SER)
        val words = lines.flatMap(_.split(","))
    
    
        words.map(w =>(w,1)).reduceByKeyAndWindow(
          (v1:Int,v2:Int) => {
          
          v1+v2},
          (v1:Int,v2:Int) => {
          
          v1-v2},
          Seconds(15),
          Seconds(5)
        ).print()
        ssc
      }
      
    }
    

猜你喜欢

转载自blog.csdn.net/dwjf321/article/details/109050011
今日推荐