• Spark Streaming详解



    Spark Streaming编程指南

    Overview

    Spark Streaming属于Spark的核心api,它支持高吞吐量、支持容错的实时流数据处理。

    它可以接受来自Kafka, Flume, Twitter, ZeroMQ和TCP Socket的数据源,使用简单的api函数比如 mapreducejoinwindow等操作,还可以直接使用内置的机器学习算法、图算法包来处理数据。

     

    它的工作流程像下面的图所示一样,接受到实时数据后,给数据分批次,然后传给Spark Engine处理最后生成该批次的结果。

    它支持的数据流叫Dstream,直接支持Kafka、Flume的数据源。Dstream是一种连续的RDDs,下面是一个例子帮助大家理解Dstream。

    A Quick Example

     

    复制代码
    // 创建StreamingContext,1秒一个批次
    val ssc = new StreamingContext(sparkConf, Seconds(1));
    // 获得一个DStream负责连接 监听端口:地址 val lines = ssc.socketTextStream(serverIP, serverPort);
    // 对每一行数据执行Split操作 val words = lines.flatMap(_.split(" ")); // 统计word的数量 val pairs = words.map(word => (word, 1)); val wordCounts = pairs.reduceByKey(_ + _); // 输出结果 wordCounts.print(); ssc.start(); // 开始 ssc.awaitTermination(); // 计算完毕退出
    复制代码

    具体的代码可以访问这个页面:

    https://github.com/apache/incubator-spark/blob/master/examples/src/main/scala/org/apache/spark/streaming/examples/NetworkWordCount.scala

    如果已经装好Spark的朋友,我们可以通过下面的例子试试。

    首先,启动Netcat,这个工具在Unix-like的系统都存在,是个简易的数据服务器。

    使用下面这句命令来启动Netcat:

    $ nc -lk 9999

    接着启动example

    $ ./bin/run-example org.apache.spark.streaming.examples.NetworkWordCount local[2] localhost 9999

    在Netcat这端输入hello world,看Spark这边的

    复制代码
    # TERMINAL 1:
    # Running Netcat
    
    $ nc -lk 9999
    
    hello world
    
    ...
    # TERMINAL 2: RUNNING NetworkWordCount or JavaNetworkWordCount
    
    $ ./bin/run-example org.apache.spark.streaming.examples.NetworkWordCount local[2] localhost 9999
    ...
    -------------------------------------------
    Time: 1357008430000 ms
    -------------------------------------------
    (hello,1)
    (world,1)
    ...
    复制代码

     

    Basics

    下面这块是如何编写代码的啦,哇咔咔!

    首先我们要在SBT或者Maven工程添加以下信息:

    groupId = org.apache.spark
    artifactId = spark-streaming_2.10
    version = 0.9.0-incubating
    复制代码
    //需要使用一下数据源的,还要添加相应的依赖
    Source Artifact Kafka spark
    -streaming-kafka_2.10 Flume spark-streaming-flume_2.10 Twitter spark-streaming-twitter_2.10 ZeroMQ spark-streaming-zeromq_2.10 MQTT spark-streaming-mqtt_2.10
    复制代码

     

    接着就是实例化

    new StreamingContext(master, appName, batchDuration, [sparkHome], [jars])

    这是之前的例子对DStream的操作。

     

    Input Sources

    除了sockets之外,我们还可以这样创建Dstream

    streamingContext.fileStream(dataDirectory)

     

    这里有3个要点:

    (1)dataDirectory下的文件格式都是一样

    (2)在这个目录下创建文件都是通过移动或者重命名的方式创建的

    (3)一旦文件进去之后就不能再改变

    假设我们要创建一个Kafka的Dstream。

    import org.apache.spark.streaming.kafka._
    KafkaUtils.createStream(streamingContext, kafkaParams, ...)

     

    如果我们需要自定义流的receiver,可以查看https://spark.incubator.apache.org/docs/latest/streaming-custom-receivers.html

    Operations

    对于Dstream,我们可以进行两种操作,transformations 和 output 

    Transformations

    复制代码
    Transformation                          Meaning
    map(func)                        对每一个元素执行func方法
    flatMap(func)                    类似map函数,但是可以map到0+个输出
    filter(func)                     过滤
    repartition(numPartitions)       增加分区,提高并行度     
    union(otherStream)               合并两个流
    count()                    统计元素的个数
    reduce(func)                     对RDDs里面的元素进行聚合操作,2个输入参数,1个输出参数
    countByValue()                   针对类型统计,当一个Dstream的元素的类型是K的时候,调用它会返回一个新的Dstream,包含<K,Long>键值对,Long是每个K出现的频率。
    reduceByKey(func, [numTasks])    对于一个(K, V)类型的Dstream,为每个key,执行func函数,默认是local是2个线程,cluster是8个线程,也可以指定numTasks 
    join(otherStream, [numTasks])    把(K, V)和(K, W)的Dstream连接成一个(K, (V, W))的新Dstream 
    cogroup(otherStream, [numTasks]) 把(K, V)和(K, W)的Dstream连接成一个(K, Seq[V], Seq[W])的新Dstream 
    transform(func)                  转换操作,把原来的RDD通过func转换成一个新的RDD
    updateStateByKey(func) 针对key使用func来更新状态和值,可以将state该为任何值
    复制代码

    UpdateStateByKey Operation

    使用这个操作,我们是希望保存它状态的信息,然后持续的更新它,使用它有两个步骤:

    (1)定义状态,这个状态可以是任意的数据类型

    (2)定义状态更新函数,从前一个状态更改新的状态

    下面展示一个例子:

    def updateFunction(newValues: Seq[Int], runningCount: Option[Int]): Option[Int] = {
        val newCount = ...  // add the new values with the previous running count to get the new count
        Some(newCount)
    }

     

    它可以用在包含(word, 1) 的Dstream当中,比如前面展示的example

    val runningCounts = pairs.updateStateByKey[Int](updateFunction _)

     

    它会针对里面的每个word调用一下更新函数,newValues是最新的值,runningCount是之前的值。

    Transform Operation

    transformWith一样,可以对一个Dstream进行RDD->RDD操作,比如我们要对Dstream流里的RDD和另外一个数据集进行join操作,但是Dstream的API没有直接暴露出来,我们就可以使用transform方法来进行这个操作,下面是例子:

    val spamInfoRDD = sparkContext.hadoopFile(...) // RDD containing spam information
    
    val cleanedDStream = inputDStream.transform(rdd => {
      rdd.join(spamInfoRDD).filter(...) // join data stream with spam information to do data cleaning
      ...
    })

     

    另外,我们也可以在里面使用机器学习算法和图算法。

    Window Operations

    先举个例子吧,比如前面的word count的例子,我们想要每隔10秒计算一下最近30秒的单词总数。

    我们可以使用以下语句:

    // Reduce last 30 seconds of data, every 10 seconds
    val windowedWordCounts = pairs.reduceByKeyAndWindow(_ + _, Seconds(30), Seconds(10))

     

    这里面提到了windows的两个参数:

    (1)window length:window的长度是30秒,最近30秒的数据

    (2)slice interval:计算的时间间隔

    通过这个例子,我们大概能够窗口的意思了,定期计算滑动的数据。

    下面是window的一些操作函数,还是有点儿理解不了window的概念,Meaning就不翻译了,直接删掉

    复制代码
    Transformation                                                                              Meaning
    window(windowLength, slideInterval)     
    countByWindow(windowLength, slideInterval)     
    reduceByWindow(func, windowLength, slideInterval)     
    reduceByKeyAndWindow(func, windowLength, slideInterval, [numTasks])     
    reduceByKeyAndWindow(func, invFunc, windowLength, slideInterval, [numTasks])    
    countByValueAndWindow(windowLength, slideInterval, [numTasks])     
    复制代码

     

    Output Operations

    Output Operation                                      Meaning
    print()                                 打印到控制台
    foreachRDD(func)                        对Dstream里面的每个RDD执行func,保存到外部系统
    saveAsObjectFiles(prefix, [suffix])     保存流的内容为SequenceFile, 文件名 : "prefix-TIME_IN_MS[.suffix]".
    saveAsTextFiles(prefix, [suffix])       保存流的内容为文本文件, 文件名 : "prefix-TIME_IN_MS[.suffix]".
    saveAsHadoopFiles(prefix, [suffix])     保存流的内容为hadoop文件, 文件名 : "prefix-TIME_IN_MS[.suffix]".

     

    Persistence

     Dstream中的RDD也可以调用persist()方法保存在内存当中,但是基于window和state的操作,reduceByWindow,reduceByKeyAndWindow,updateStateByKey它们就是隐式的保存了,系统已经帮它自动保存了。

    从网络接收的数据(such as, Kafka, Flume, sockets, etc.),默认是保存在两个节点来实现容错性,以序列化的方式保存在内存当中。

    RDD Checkpointing

     状态的操作是基于多个批次的数据的。它包括基于window的操作和updateStateByKey。因为状态的操作要依赖于上一个批次的数据,所以它要根据时间,不断累积元数据。为了清空数据,它支持周期性的检查点,通过把中间结果保存到hdfs上。因为检查操作会导致保存到hdfs上的开销,所以设置这个时间间隔,要很慎重。对于小批次的数据,比如一秒的,检查操作会大大降低吞吐量。但是检查的间隔太长,会导致任务变大。通常来说,5-10秒的检查间隔时间是比较合适的。

    ssc.checkpoint(hdfsPath)  //设置检查点的保存位置
    dstream.checkpoint(checkpointInterval)  //设置检查点间隔

     

    对于必须设置检查点的Dstream,比如通过updateStateByKeyreduceByKeyAndWindow创建的Dstream,默认设置是至少10秒。

    Performance Tuning

    对于调优,可以从两个方面考虑:

    (1)利用集群资源,减少处理每个批次的数据的时间

    (2)给每个批次的数据量的设定一个合适的大小

    Level of Parallelism

    像一些分布式的操作,比如reduceByKey和reduceByKeyAndWindow,默认的8个并发线程,可以通过对应的函数提高它的值,或者通过修改参数spark.default.parallelism来提高这个默认值。

    Task Launching Overheads

    通过进行的任务太多也不好,比如每秒50个,发送任务的负载就会变得很重要,很难实现压秒级的时延了,当然可以通过压缩来降低批次的大小。

    Setting the Right Batch Size

    要使流程序能在集群上稳定的运行,要使处理数据的速度跟上数据流入的速度。最好的方式计算这个批量的大小,我们首先设置batch size为5-10秒和一个很低的数据输入速度。确实系统能跟上数据的速度的时候,我们可以根据经验设置它的大小,通过查看日志看看Total delay的多长时间。如果delay的小于batch的,那么系统可以稳定,如果delay一直增加,说明系统的处理速度跟不上数据的输入速度。

    24/7 Operation

    Spark默认不会忘记元数据,比如生成的RDD,处理的stages,但是Spark Streaming是一个24/7的程序,它需要周期性的清理元数据,通过spark.cleaner.ttl来设置。比如我设置它为600,当超过10分钟的时候,Spark就会清楚所有元数据,然后持久化RDDs。但是这个属性要在SparkContext 创建之前设置。

    但是这个值是和任何的window操作绑定。Spark会要求输入数据在过期之后必须持久化到内存当中,所以必须设置delay的值至少和最大的window操作一致,如果设置小了,就会报错。

    Monitoring

    除了Spark内置的监控能力,还可以StreamingListener这个接口来获取批处理的时间, 查询时延, 全部的端到端的试验。

    Memory Tuning

    Spark Stream默认的序列化方式是StorageLevel.MEMORY_ONLY_SER,而不是RDD的StorageLevel.MEMORY_ONLY

    默认的,所有持久化的RDD都会通过被Spark的LRU算法剔除出内存,如果设置了spark.cleaner.ttl,就会周期性的清理,但是这个参数设置要很谨慎。一个更好的方法是设置spark.streaming.unpersist为true,这就让Spark来计算哪些RDD需要持久化,这样有利于提高GC的表现。

    推荐使用concurrent mark-and-sweep GC,虽然这样会降低系统的吞吐量,但是这样有助于更稳定的进行批处理。

    Fault-tolerance Properties

    Failure of a Worker Node

    下面有两种失效的方式:

    1.使用hdfs上的文件,因为hdfs是可靠的文件系统,所以不会有任何的数据失效。

    2.如果数据来源是网络,比如Kafka和Flume,为了防止失效,默认是数据会保存到2个节点上,但是有一种可能性是接受数据的节点挂了,那么数据可能会丢失,因为它还没来得及把数据复制到另外一个节点。

    Failure of the Driver Node

    为了支持24/7不间断的处理,Spark支持驱动节点失效后,重新恢复计算。Spark Streaming会周期性的写数据到hdfs系统,就是前面的检查点的那个目录。驱动节点失效之后,StreamingContext可以被恢复的。

    为了让一个Spark Streaming程序能够被回复,它需要做以下操作:

    (1)第一次启动的时候,创建 StreamingContext,创建所有的streams,然后调用start()方法。

    (2)恢复后重启的,必须通过检查点的数据重新创建StreamingContext。

    下面是一个实际的例子:

    通过StreamingContext.getOrCreate来构造StreamingContext,可以实现上面所说的。

    复制代码
    // Function to create and setup a new StreamingContext
    def functionToCreateContext(): StreamingContext = {
        val ssc = new StreamingContext(...)   // new context
        val lines = ssc.socketTextStream(...) // create DStreams
        ...
        ssc.checkpoint(checkpointDirectory)   // set checkpoint directory
        ssc
    }
    
    // Get StreaminContext from checkpoint data or create a new one
    val context = StreamingContext.getOrCreate(checkpointDirectory, functionToCreateContext _)
    
    // Do additional setup on context that needs to be done,
    // irrespective of whether it is being started or restarted
    context. ...
    
    // Start the context
    context.start()
    context.awaitTermination()
    复制代码

     

    在stand-alone的部署模式下面,驱动节点失效了,也可以自动恢复,让别的驱动节点替代它。这个可以在本地进行测试,在提交的时候采用supervise模式,当提交了程序之后,使用jps查看进程,看到类似DriverWrapper就杀死它,如果是使用YARN模式的话就得使用其它方式来重新启动了。

    这里顺便提一下向客户端提交程序吧,之前总结的时候把这块给落下了。

    复制代码
    ./bin/spark-class org.apache.spark.deploy.Client launch
       [client-options] 
       <cluster-url> <application-jar-url> <main-class> 
       [application-options]
    
    cluster-url: master的地址.
    application-jar-url: jar包的地址,最好是hdfs上的,带上hdfs://...否则要所有的节点的目录下都有这个jar的 
    main-class: 要发布的程序的main函数所在类.
    Client Options:
    --memory <count> (驱动程序的内存,单位是MB)
    --cores <count> (为你的驱动程序分配多少个核心)
    --supervise (节点失效的时候,是否重新启动应用)
    --verbose (打印增量的日志输出)
    复制代码

     

    在未来的版本,会支持所有的数据源的可恢复性。

    为了更好的理解基于HDFS的驱动节点失效恢复,下面用一个简单的例子来说明:

    复制代码
    Time     Number of lines in input file     Output without driver failure     Output with driver failure
    1      10                     10                    10
    2      20                     20                    20
    3      30                     30                    30
    4      40                     40                    [DRIVER FAILS] no output
    5      50                     50                    no output
    6      60                     60                    no output
    7      70                     70                    [DRIVER RECOVERS] 40, 50, 60, 70
    8      80                     80                    80
    9      90                     90                    90
    10     100                     100                   100
    复制代码

     

     

    在4的时候出现了错误,40,50,60都没有输出,到70的时候恢复了,恢复之后把之前没输出的一下子全部输出。



    Example代码分析

    复制代码
    val ssc = new StreamingContext(sparkConf, Seconds(1));
    // 获得一个DStream负责连接 监听端口:地址
    val lines = ssc.socketTextStream(serverIP, serverPort);
    // 对每一行数据执行Split操作
    val words = lines.flatMap(_.split(" "));
    // 统计word的数量
    val pairs = words.map(word => (word, 1));
    val wordCounts = pairs.reduceByKey(_ + _);
    // 输出结果
    wordCounts.print();
    ssc.start();             // 开始
    ssc.awaitTermination();  // 计算完毕退出
    复制代码

    1、首先实例化一个StreamingContext

    2、调用StreamingContext的socketTextStream

    3、对获得的DStream进行处理

    4、调用StreamingContext是start方法,然后等待

    我们看StreamingContext的socketTextStream方法吧。

    复制代码
      def socketTextStream(
          hostname: String,
          port: Int,
          storageLevel: StorageLevel = StorageLevel.MEMORY_AND_DISK_SER_2
        ): ReceiverInputDStream[String] = {
        socketStream[String](hostname, port, SocketReceiver.bytesToLines, storageLevel)
      }
    复制代码

    1、StoageLevel是StorageLevel.MEMORY_AND_DISK_SER_2

    2、使用SocketReceiver的bytesToLines把输入流转换成可遍历的数据

    继续看socketStream方法,它直接new了一个

    new SocketInputDStream[T](this, hostname, port, converter, storageLevel)

    继续深入挖掘SocketInputDStream,追述一下它的继承关系,SocketInputDStream>>ReceiverInputDStream>>InputDStream>>DStream。

    具体实现ReceiverInputDStream的类有好几个,基本上都是从网络端来数据的。

    它实现了ReceiverInputDStream的getReceiver方法,实例化了一个SocketReceiver来接收数据。

    SocketReceiver的onStart方法里面调用了receive方法,处理代码如下:

          socket = new Socket(host, port)
          val iterator = bytesToObjects(socket.getInputStream())
          while(!isStopped && iterator.hasNext) {
            store(iterator.next)
          }

    1、new了一个Socket来接收数据,用bytesToLines方法把InputStream转换成一行一行的字符串。

    2、把每一行数据用store方法保存起来,store方法是从SocketReceiver的父类Receiver继承而来,内部实现是:

      def store(dataItem: T) {
        executor.pushSingle(dataItem)
      }

    executor是ReceiverSupervisor类型,Receiver的操作都是由它来处理。这里先不深纠,后面我们再说这个pushSingle的实现。

    到这里我们知道lines的类型是SocketInputDStream,然后对它是一顿的转换,flatMap、map、reduceByKey、print,这些方法都不是RDD的那种方法,而是DStream独有的。

    讲到上面这几个方法,我们开始转入DStream了,flatMap、map、reduceByKey、print方法都涉及到DStream的转换,这和RDD的转换是类似的。我们讲一下reduceByKey和print。

    reduceByKey方法和RDD一样,调用的combineByKey方法实现的,不一样的是它直接new了一个ShuffledDStream了,我们接着看一下它的实现吧。

    override def compute(validTime: Time): Option[RDD[(K,C)]] = {
        parent.getOrCompute(validTime) match {
          case Some(rdd) => Some(rdd.combineByKey[C](createCombiner, mergeValue, mergeCombiner, partitioner, mapSideCombine))
          case None => None
        }
      }

    在compute阶段,对通过Time获得的rdd进行reduceByKey操作。接下来的print方法也是一个转换:

    new ForEachDStream(this, context.sparkContext.clean(foreachFunc)).register()

    打印前十个,超过10个打印"..."。需要注意register方法。

    ssc.graph.addOutputStream(this)

    它会把代码插入到当前的DStream添加到outputStreams里面,后面输出的时候如果没有outputStream就不会有输出,这个需要记住哦!

    启动过程分析

    前戏结束之后,ssc.start() 高潮开始了。 start方法很小,最核心的一句是JobScheduler的start方法。我们得转到JobScheduler方法上面去。

    下面是start方法的代码:

    复制代码
      def start(): Unit = synchronized {
      // 接受到JobSchedulerEvent就处理事件 eventActor = ssc.env.actorSystem.actorOf(Props(new Actor { def receive = { case event: JobSchedulerEvent => processEvent(event) } }), "JobScheduler") listenerBus.start() receiverTracker = new ReceiverTracker(ssc) receiverTracker.start() jobGenerator.start() }
    复制代码

    1、启动了一个Actor来处理JobScheduler的JobStarted、JobCompleted、ErrorReported事件。

    2、启动StreamingListenerBus作为监听器。

    3、启动ReceiverTracker。

    4、启动JobGenerator。

    我们接下来看看ReceiverTracker的start方法。

      def start() = synchronized {if (!receiverInputStreams.isEmpty) {
          actor = ssc.env.actorSystem.actorOf(Props(new ReceiverTrackerActor), "ReceiverTracker")
          receiverExecutor.start()
        }
      }

    1、首先判断了一下receiverInputStreams不能为空,那receiverInputStreams是怎么时候写入值的呢?答案在SocketInputDStream的父类InputDStream当中,当实例化InputDStream的时候会在DStreamGraph里面添加InputStream。

    abstract class InputDStream[T: ClassTag] (@transient ssc_ : StreamingContext) extends DStream[T](ssc_) {
      ssc.graph.addInputStream(this)
      //....
    }

    2、实例化ReceiverTrackerActor,它负责RegisterReceiver(注册Receiver)、AddBlock、ReportError(报告错误)、DeregisterReceiver(注销Receiver)等事件的处理。

    3、启动receiverExecutor(实际类是ReceiverLauncher,这名字起得。。),它主要负责启动Receiver,start方法里面调用了startReceivers方法吧。

    复制代码
        private def startReceivers() {
         // 对应着上面的那个例子,getReceiver方法获得是SocketReceiver
          val receivers = receiverInputStreams.map(nis => {
            val rcvr = nis.getReceiver()
            rcvr.setReceiverId(nis.id)
            rcvr
          })
    
          // 查看是否所有的receivers都有优先选择机器,这个需要重写Receiver的preferredLocation方法,目前只有FlumeReceiver重写了
          val hasLocationPreferences = receivers.map(_.preferredLocation.isDefined).reduce(_ && _)
    
          // 创建一个并行receiver集合的RDD, 把它们分散到各个worker节点上
          val tempRDD =
            if (hasLocationPreferences) {
              val receiversWithPreferences = receivers.map(r => (r, Seq(r.preferredLocation.get)))
              ssc.sc.makeRDD[Receiver[_]](receiversWithPreferences)
            } else {
              ssc.sc.makeRDD(receivers, receivers.size)
            }
    
          // 在worker节点上启动Receiver的方法,遍历所有Receiver,然后启动
          val startReceiver = (iterator: Iterator[Receiver[_]]) => {
            if (!iterator.hasNext) {
              throw new SparkException("Could not start receiver as object not found.")
            }
            val receiver = iterator.next()
            val executor = new ReceiverSupervisorImpl(receiver, SparkEnv.get)
            executor.start()
            executor.awaitTermination()
          }
          // 运行这个重复的作业来确保所有的slave都已经注册了,避免所有的receivers都到一个节点上
          if (!ssc.sparkContext.isLocal) {
            ssc.sparkContext.makeRDD(1 to 50, 50).map(x => (x, 1)).reduceByKey(_ + _, 20).collect()
          }
    
          // 把receivers分发出去,启动
          ssc.sparkContext.runJob(tempRDD, startReceiver)
        }
    复制代码

    1、遍历receiverInputStreams获取所有的Receiver。

    2、查看这些Receiver是否全都有优先选择机器。

    3、把SparkContext的makeRDD方法把所有Receiver包装到ParallelCollectionRDD里面,并行度是Receiver的数量。

    4、发个小任务给确保所有的slave节点都已经注册了(这个小任务有点儿莫名其妙,感觉怪怪的)。

    5、提交作业,启动所有Receiver。

    Spark写得实在是太巧妙了,居然可以把Receiver包装在RDD里面,当做是数据来处理!

    启动Receiver的时候,new了一个ReceiverSupervisorImpl,然后调的start方法,主要干了这么三件事情,代码就不贴了。

    1、启动BlockGenerator。

    2、调用Receiver的OnStart方法,开始接受数据,并把数据写入到ReceiverSupervisor。

    3、调用onReceiverStart方法,发送RegisterReceiver消息给driver报告自己启动了。

    保存接收到的数据

    ok,到了这里,重点落到了BlockGenerator。前面说到SocketReceiver把接受到的数据调用ReceiverSupervisor的pushSingle方法保存。

    复制代码
      // 这是ReceiverSupervisorImpl的方法
    def pushSingle(data: Any) { blockGenerator += (data) } // 这是BlockGenerator的方法 def += (data: Any): Unit = synchronized { currentBuffer += data }
    复制代码

    我们看一下它的start方法吧。

      def start() {
        blockIntervalTimer.start()
        blockPushingThread.start()
      }

    它启动了一个定时器RecurringTimer和一个线程执行keepPushingBlocks方法。

    先看RecurringTimer的实现:

          while (!stopped) {
            clock.waitTillTime(nextTime)
            callback(nextTime)
            prevTime = nextTime
            nextTime += period
          }

    每隔一段时间就执行callback函数,callback函数是new的时候传进来的,是BlockGenerator的updateCurrentBuffer方法。

    复制代码
      private def updateCurrentBuffer(time: Long): Unit = synchronized {
        try {
          val newBlockBuffer = currentBuffer
          currentBuffer = new ArrayBuffer[Any]
          if (newBlockBuffer.size > 0) {
            val blockId = StreamBlockId(receiverId, time - blockInterval)
            val newBlock = new Block(blockId, newBlockBuffer)
            blocksForPushing.put(newBlock) 
    } } catch {case t: Throwable => reportError("Error in block updating thread", t) } }
    复制代码

    它new了一个Block出来,然后添加到blocksForPushing这个ArrayBlockingQueue队列当中。

    提到这里,有两个参数需要大家注意的:

    spark.streaming.blockInterval   默认值是200
    spark.streaming.blockQueueSize  默认值是10

    这是前面提到的间隔时间和队列的长度,间隔时间默认是200毫秒,队列是最多能容纳10个Block,多了就要阻塞了。

    我们接下来看一下BlockGenerator另外启动的那个线程执行的keepPushingBlocks方法到底在干什么?

    复制代码
      private def keepPushingBlocks() {
        while(!stopped) { Option(blocksForPushing.poll(100, TimeUnit.MILLISECONDS)) match { case Some(block) => pushBlock(block) case None => } }
       // ...退出之前把剩下的也输出去了 }
    复制代码

    它在把blocksForPushing中的block不停的拿出来,调用pushBlock方法,这个方法属于在实例化BlockGenerator的时候,从ReceiverSupervisorImpl传进来的BlockGeneratorListener的。

    复制代码
      private val blockGenerator = new BlockGenerator(new BlockGeneratorListener {
        def onError(message: String, throwable: Throwable) {
          reportError(message, throwable)
        }
    
        def onPushBlock(blockId: StreamBlockId, arrayBuffer: ArrayBuffer[_]) {
          pushArrayBuffer(arrayBuffer, None, Some(blockId))
        }
      }, streamId, env.conf)
    复制代码

    1、reportError,通过actor向driver发送错误报告消息ReportError。

    2、调用pushArrayBuffer保存数据。

    下面是pushArrayBuffer方法:

    复制代码
      def pushArrayBuffer(arrayBuffer: ArrayBuffer[_], optionalMetadata: Option[Any], optionalBlockId: Option[StreamBlockId]
        ) {
        val blockId = optionalBlockId.getOrElse(nextBlockId)
        val time = System.currentTimeMillis
        blockManager.put(blockId, arrayBuffer.asInstanceOf[ArrayBuffer[Any]], storageLevel, tellMaster = true)
        reportPushedBlock(blockId, arrayBuffer.size, optionalMetadata)
      }
    复制代码

    1、把Block保存到BlockManager当中,序列化方式为之前提到的StorageLevel.MEMORY_AND_DISK_SER_2(内存不够就写入到硬盘,并且在2个节点上保存的方式)。

    2、调用reportPushedBlock给driver发送AddBlock消息,报告新添加的Block,ReceiverTracker收到消息之后更新内部的receivedBlockInfo映射关系。

    处理接收到的数据

    前面只讲了数据的接收和保存,那数据是怎么处理的呢?

    之前一直讲ReceiverTracker,而忽略了之前的JobScheduler的start方法里面最后启动的JobGenerator

    复制代码
      def start(): Unit = synchronized {
        eventActor = ssc.env.actorSystem.actorOf(Props(new Actor {
          def receive = {
            case event: JobGeneratorEvent =>  processEvent(event)
          }
        }), "JobGenerator")
        if (ssc.isCheckpointPresent) {
          restart()
        } else {
          startFirstTime()
        }
      }
    复制代码

    1、启动一个actor处理JobGeneratorEvent事件。

    2、如果是已经有CheckPoint了,就接着上次的记录进行处理,否则就是第一次启动。

    我们先看startFirstTime吧,CheckPoint以后再说吧,有点儿小复杂。

      private def startFirstTime() {
        val startTime = new Time(timer.getStartTime())
        graph.start(startTime - graph.batchDuration)
        timer.start(startTime.milliseconds)
      }

    1、timer.getStartTime计算出来下一个周期的到期时间,计算公式:(math.floor(clock.currentTime.toDouble / period) + 1).toLong * period,以当前的时间/除以间隔时间,再用math.floor求出它的上一个整数(即上一个周期的到期时间点),加上1,再乘以周期就等于下一个周期的到期时间。

    2、启动DStreamGraph,启动时间=startTime - graph.batchDuration

    3、启动Timer,我们看看它的定义:

      private val timer = new RecurringTimer(clock, ssc.graph.batchDuration.milliseconds,
        longTime => eventActor ! GenerateJobs(new Time(longTime)), "JobGenerator")

    到这里就清楚了,DStreamGraph的间隔时间就是timer的间隔时间,启动时间要设置成比Timer早一个时间间隔,原因再慢慢探究。

    可以看出来每隔一段时间,Timer给eventActor发送GenerateJobs消息,我们直接去看它的处理方法generateJobs吧,中间忽略了一步,大家自己看。

    复制代码
      private def processEvent(event: JobGeneratorEvent) {
        event match {
          case GenerateJobs(time) => generateJobs(time)
          case ClearMetadata(time) => clearMetadata(time)
          case DoCheckpoint(time) => doCheckpoint(time)
          case ClearCheckpointData(time) => clearCheckpointData(time)
        }
      }
    复制代码

    下面是generateJobs方法。

    复制代码
      private def generateJobs(time: Time) {
        SparkEnv.set(ssc.env)
        Try(graph.generateJobs(time)) match {
          case Success(jobs) =>
            val receivedBlockInfo = graph.getReceiverInputStreams.map { stream =>
              val streamId = stream.id
              val receivedBlockInfo = stream.getReceivedBlockInfo(time)
              (streamId, receivedBlockInfo)
            }.toMap
            jobScheduler.submitJobSet(JobSet(time, jobs, receivedBlockInfo))
          case Failure(e) =>
            jobScheduler.reportError("Error generating jobs for time " + time, e)
        }
        eventActor ! DoCheckpoint(time)
      }
    复制代码

    1、DStreamGraph生成jobs。

    2、从stream那里获取接收到的Block信息。

    3、调用submitJobSet方法提交作业。

    4、提交完作业之后,做一个CheckPoint。

    先看DStreamGraph是怎么生成的jobs。

      def generateJobs(time: Time): Seq[Job] = {
        val jobs = this.synchronized {
          outputStreams.flatMap(outputStream => outputStream.generateJob(time))
        }
        jobs
      }

    outputStreams在这个例子里面是print这个方法里面添加的,这个在前面说了,我们继续看DStream的generateJob

    复制代码
      private[streaming] def generateJob(time: Time): Option[Job] = {
        getOrCompute(time) match {
          case Some(rdd) => {
            val jobFunc = () => {
              val emptyFunc = { (iterator: Iterator[T]) => {} }
              context.sparkContext.runJob(rdd, emptyFunc)
            }
            Some(new Job(time, jobFunc))
          }
          case None => None
        }
      }
    复制代码

    1、调用getOrCompute方法获得RDD

    2、new了一个方法去提交这个作业,缺什么都不做

    为什么呢?这是直接跳转的错误,呵呵,因为这个outputStream是print方法返回的,它应该是ForEachDStream,所以我们应该看的是它里面的generateJob方法。

    复制代码
      override def generateJob(time: Time): Option[Job] = {
        parent.getOrCompute(time) match {
          case Some(rdd) =>
            val jobFunc = () => {
              foreachFunc(rdd, time)
            }
            Some(new Job(time, jobFunc))
          case None => None
        }
      }
    复制代码

    这里请大家千万要注意,不要在这块被卡住了。

    我们看看它这个RDD是怎么出来的吧。

    复制代码
      private[streaming] def getOrCompute(time: Time): Option[RDD[T]] = {
        // If this DStream was not initialized (i.e., zeroTime not set), then do it
        // If RDD was already generated, then retrieve it from HashMap
        generatedRDDs.get(time) match {
    
          // 这个RDD已经被生成过了,直接用就是了
          case Some(oldRDD) => Some(oldRDD)
    
          // 还没生成过,就调用compte函数生成一个
          case None => {
            if (isTimeValid(time)) {
              compute(time) match {
                case Some(newRDD) =>
             // 设置保存的级别
                  if (storageLevel != StorageLevel.NONE) {
                    newRDD.persist(storageLevel)
                  }
             // 如果现在需要,就做CheckPoint
                  if (checkpointDuration != null && (time - zeroTime).isMultipleOf(checkpointDuration)) {
                    newRDD.checkpoint()
                  }
             // 添加到generatedRDDs里面去,可以再次利用
                  generatedRDDs.put(time, newRDD)
                  Some(newRDD)
                case None =>
                  None
              }
            } else {
              None
            }
          }
        }
      }
    复制代码

    从上面的方法可以看出来它是通过每个DStream自己实现的compute函数得出来的RDD。我们找到SocketInputDStream,没有compute函数,在父类ReceiverInputDStream里面找到了。

    复制代码
      override def compute(validTime: Time): Option[RDD[T]] = {
        // 如果出现了时间比startTime早的话,就返回一个空的RDD,因为这个很可能是master挂了之后的错误恢复
    if (validTime >= graph.startTime) { val blockInfo = ssc.scheduler.receiverTracker.getReceivedBlockInfo(id) receivedBlockInfo(validTime) = blockInfo val blockIds = blockInfo.map(_.blockId.asInstanceOf[BlockId]) Some(new BlockRDD[T](ssc.sc, blockIds)) } else { Some(new BlockRDD[T](ssc.sc, Array[BlockId]())) } }
    复制代码

    通过DStream的id把receiverTracker当中把接收到的block信息全部拿出来,记录到ReceiverInputDStream自身的receivedBlockInfo这个HashMap里面,就把RDD返回了,RDD里面实际包含的是Block的id的集合。

    现在我们就可以回到之前JobGenerator的generateJobs方法,我们就清楚它这句是提交的什么了。

    jobScheduler.submitJobSet(JobSet(time, jobs, receivedBlockInfo))

    JobSet是记录Job的完成情况的,直接看submitJobSet方法吧。

    复制代码
      def submitJobSet(jobSet: JobSet) {
        if (jobSet.jobs.isEmpty) {
        } else {
          jobSets.put(jobSet.time, jobSet)
          jobSet.jobs.foreach(job => jobExecutor.execute(new JobHandler(job)))
        }
      }
    复制代码

    遍历jobSet里面的所有jobs,通过jobExecutor这个线程池提交。我们看一下JobHandler就知道了。

    复制代码
      private class JobHandler(job: Job) extends Runnable {
        def run() {
          eventActor ! JobStarted(job)
          job.run()
          eventActor ! JobCompleted(job)
        }
      }
    复制代码

    1、通知eventActor处理JobStarted事件。

    2、运行job。

    3、通知eventActor处理JobCompleted事件。

    这里的重点是job.run,事件处理只是更新相关的job信息。

      def run() {
        result = Try(func())
      }

    在遍历BlockRDD的时候,在compute函数获取该Block(详细请看BlockRDD),然后对这个RDD的结果进行打印。

     

    到这里就算结束了,最后来个总结吧,图例在下一章补上,这一章只是过程分析:

    1、可以有多个输入,我们可以通过StreamingContext定义多个输入,比如我们监听多个(host,ip),可以给它们定义各自的处理逻辑和输出,输出方式不仅限于print方法,还可以有别的方法,saveAsTextFiles和saveAsObjectFiles。这块的设计是支持共享StreamingContext的。

    2、StreamingContext启动了JobScheduler,JobScheduler启动ReceiverTracker和JobGenerator。

    3、ReceiverTracker是通过把Receiver包装成RDD的方式,发送到Executor端运行起来的,Receiver起来之后向ReceiverTracker发送RegisterReceiver消息。

    3、Receiver把接收到的数据,通过ReceiverSupervisor保存。

    4、ReceiverSupervisorImpl把数据写入到BlockGenerator的一个ArrayBuffer当中。

    5、BlockGenerator内部每个一段时间(默认是200毫秒)就把这个ArrayBuffer构造成Block添加到blocksForPushing当中。

    6、BlockGenerator的另外一条线程则不断的把加入到blocksForPushing当中的Block写入到BlockManager当中,并向ReceiverTracker发送AddBlock消息。

    7、JobGenerator内部有个定时器,定期生成Job,通过DStream的id,把ReceiverTracker接收到的Block信息从BlockManager上抓取下来进行处理,这个间隔时间是我们在实例化StreamingContext的时候传进去的那个时间,在这个例子里面是Seconds(1)。


    转自:http://www.cnblogs.com/cenyuhai/p/3577204.html

    正因为当初对未来做了太多的憧憬,所以对现在的自己尤其失望。生命中曾经有过的所有灿烂,终究都需要用寂寞来偿还。
  • 相关阅读:
    XAML语言
    Sqlite 数据库插入标示字段 获取新Id 及利用索引优化查询
    提高C#编程水平的50个要点 ——学生的迷茫
    734条高频词组笔记
    C#读取ini配置文件
    MD5加密
    SQL Server 2000 及 2005 端口修改
    Java控制台程序20例
    Tomcat 6.0+ SQL Server 2005连接池的配
    阿里巴巴离职DBA 35岁总结的职业生涯
  • 原文地址:https://www.cnblogs.com/candlia/p/11920141.html
Copyright © 2020-2023  润新知