父类一实现serializable_(一)Spark Streaming 算子梳理 — 简单介绍streaming运行逻辑

d396aec939c90b080e8621420018249c.png
写在前面
希望Spark Streaming算子梳理会是一个系列,能够覆盖到现在我们用的所用的Spark Streaming算子。以达到在开发时能够依据需求选择合适的算子,并且能够简单地理解每个算子的实现方式。
本系列开发环境:
1. spark v2.3.1
2. scala 2.11.8
3. jdk 1.8.0_191
目录
天小天:(一)Spark Streaming 算子梳理 — 简单介绍streaming运行逻辑
天小天:(二)Spark Streaming 算子梳理 — flatMap和mapPartitions
天小天:(三)Spark Streaming 算子梳理 — transform算子
天小天:(四)Spark Streaming 算子梳理 — Kafka createDirectStream
天小天:(五)Spark Streaming 算子梳理 — foreachRDD
天小天:(六)Spark Streaming 算子梳理 — glom算子
天小天:(七)Spark Streaming 算子梳理 — repartition算子
天小天:(八)Spark Streaming 算子梳理 — window算子

前言

本篇文章主要讲解:

1. Streaming的DStream与RDD之间的关系

2. queueStream、map、filter、print的实现

3. 会简单理解spark streaming的运行原理

由于本系列侧重于算子的逻辑,streaming的逻辑只会浅层的分析,不会涉及到Streaming的调度之类的逻辑。

上代码

话不多说,看代码

package streaming

import org.apache.spark.SparkConf
import org.apache.spark.rdd.RDD
import org.apache.spark.streaming.{Seconds, StreamingContext}

import scala.collection.mutable

/**
  * @date 2019/01/21
  */
object Api {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setAppName("api").setMaster("local[2]")
    val rddQueue = new mutable.Queue[RDD[Int]]()

    val ssc = new StreamingContext(sparkConf, Seconds(2))
    val lines = ssc.queueStream(rddQueue)
    val d = lines.map(_ + 1).filter(_ > 4)
    d.print()


    ssc.start()

    for (i <- 1 to 30) {
      rddQueue.synchronized {
        rddQueue += ssc.sparkContext.makeRDD(1 to 1000, 10)
      }
      Thread.sleep(1000)
    }
    ssc.stop()
  }
}

上面这段代码是一个streaming任务。

这个任务,接收来自一个RDD队列的数据,其中输入到RDD队列的是由1到1000的Int数字组成的,并且分成10个分区。

每个批次的RDD会调用map算子,将1 - 1000 的每个数加1,之后调用filter算子,过滤出大于4个数字。最后打印这组数字(这里其实只会打印前10个数字,原因会在讲解print时说到)。 以上就是这个spark streaming 任务的主要执行逻辑。

整体解读类之间的依赖

我们来看一个不算标准的uml图,来看下queueStream、map、filter、print是如何实现的,并且依赖了哪些类。

0555e252cd40bca7e6b296e0a2e1ac67.png

从这个图中我们能从整体看出一个Streaming任务涉及到的类,并且他们的依赖关系。接下来会按照上图解释各类的关系。

  1. 首先Api类为我们自己定义的类,这个类的main方法里我们需要实例化StreamingContext类,这个类包含了所有的输入算子。这里我们用的是queueStream,所以只在图中展示了这一个方法。
  2. queueStream方法会实例化QueueInputDStream类,并将实例化的对象返回。
  3. QueueInputDStream类实现了InputDStream这个抽象类,在QueueInputDStream类中有compute方法,入参validTime为批次时间,此方法定义了每个批次要执行的逻辑,这个类中则定义了每个批次如何从队列中获取数据。QueueInputDStream类中的map方法是我们在应用程序中定义的接下来的算子,我们稍后再细说。我们先向右看,InputDStream类。
  4. InputDStream类是所有输入算子的父类,其核心差异为每个实现类的comput方法是根据具体的输入定义的。同时InputDStream是DStream的子类。所以类型DStream的父类都是DStream类。DStream类的comput是实际的执行方法,也是每个DStream的核心差异,之后我们会在用到DStream类的属性和方法时在解释其属性及方法。
  5. 我们回来继续看MappedDStream类,这个类是由DStream调用map方法实例化的。这里有一个属性parent是指上一个DStream,这里指的是实例化的QueueInputDStream。compute方法用途与InputDStream中的DStream一样都是该类的在运行中的具体逻辑。MappedDStream的父类是DStream,其中DStream中的generatedRDDs数据结构是一个Map,Map的key是批次时间,Value是批次时间对应的RDD。MappedDStream的compute方法实现了MapPartitionsRDD,这里体现了DStream和RDD之间的关系,即DStream的comput方法实现了RDD。
  6. MapPartitionsRDD的prev对应的是上一个RDD对象,f为cmpute方法要执行的逻辑。MapPartitionsRDD继承RDD。
  7. 回到MappedDStream。MappedDSream中的filter方法是map之后要执行的方法,其实例化了FilterdDStream类,这个类除了compute的实现与MappedDStrea不一样其他都一样。不再具体介绍。
  8. FilteredDStream的print方法为接下来执行的逻辑,即打印filrer之后的RDD中的元素。其实print()调用的是print(10)方法。即print默认只打印RDD中前10个元素。print具体实现的是ForEachDStream,在每个批次运行时有generateJob把foreachFunc包装的具体实现逻辑提交。

简单总结一下Stream的逻辑:由DStream串联一个实时流的运行逻辑,DStream的comput串联起一个批次的RDD之间的逻辑。并且有执行算子计算出最终结果。

解析具体实现

接下来我们具体看下源码,理解每个算子的compute具体实现了什么逻辑。

QueueInputDStream

首先是QueueInputDStream。先贴代码。

private[streaming]
class QueueInputDStream[T: ClassTag](
    ssc: StreamingContext,
    val queue: Queue[RDD[T]],
    oneAtATime: Boolean,
    defaultRDD: RDD[T]
  ) extends InputDStream[T](ssc) {

  override def start() { }

  override def stop() { }

  private def readObject(in: ObjectInputStream): Unit = {
    throw new NotSerializableException("queueStream doesn't support checkpointing. " +
      "Please don't use queueStream when checkpointing is enabled.")
  }

  private def writeObject(oos: ObjectOutputStream): Unit = {
    logWarning("queueStream doesn't support checkpointing")
  }

  override def compute(validTime: Time): Option[RDD[T]] = {
    val buffer = new ArrayBuffer[RDD[T]]()
    // 从队列中取RDD
    queue.synchronized {
      if (oneAtATime && queue.nonEmpty) {
        // 如果oneAtATime为true,则每次取队列中的一个RDD
        buffer += queue.dequeue()
      } else {
        // 如果为false取所有的RDD,并且清空队列
        buffer ++= queue
        queue.clear()
      }
    }
    if (buffer.nonEmpty) {
      if (oneAtATime) {
        // 返回缓存中的第一个RDD
        Some(buffer.head)
      } else {
        // 返回全部RDD
        Some(new UnionRDD(context.sc, buffer.toSeq))
      }
    } else if (defaultRDD != null) {
      // 如果缓存为空,并且有默认RDD。则返回默认RDD
      Some(defaultRDD)
    } else {
      // 返回空的RDD
      Some(ssc.sparkContext.emptyRDD)
    }
  }

}

我们先看属性,ssc: StreamingContext,val queue: Queue[RDD[T]],oneAtATime: Boolean,defaultRDD: RDD[T]分别是应用程序定义的StreamingContext、消费的队列、一次只消费队列中一个RDD还是所有RDD(true 为消费一个RDD)、默认的RDD。

接下来我们只关注comput方法,其他的暂时不讲解。comput的我们可以看代码中的注释,理解每一步的作用。

MappedDStream

接下来我们看下MappedDStream如何实现的

private[streaming]
class MappedDStream[T: ClassTag, U: ClassTag] (
    parent: DStream[T], // 上一个DStream
    mapFunc: T => U // 队每一个元素执行的逻辑
  ) extends DStream[U](parent.ssc) {

  override def dependencies: List[DStream[_]] = List(parent)

  override def slideDuration: Duration = parent.slideDuration

  override def compute(validTime: Time): Option[RDD[U]] = {
    parent.getOrCompute(validTime).map(_.map[U](mapFunc))
  }
}

我们主要看compute方法。首先调用parentgetOrCompute方法,获取由Option包装的RDD。之后由Option的map方法执行RDD的map方法。

接下来看下getOrCompute的实现

private[streaming] final def getOrCompute(time: Time): Option[RDD[T]] = {
    // If RDD was already generated, then retrieve it from HashMap,
    // or else compute the RDD
    generatedRDDs.get(time).orElse {
      // Compute the RDD if time is valid (e.g. correct time in a sliding window)
      // of RDD generation, else generate nothing.
      if (isTimeValid(time)) {

        val rddOption = createRDDWithLocalProperties(time, displayInnerRDDOps = false) {
          // Disable checks for existing output directories in jobs launched by the streaming
          // scheduler, since we may need to write output to an existing directory during checkpoint
          // recovery; see SPARK-4835 for more details. We need to have this call here because
          // compute() might cause Spark jobs to be launched.
          SparkHadoopWriterUtils.disableOutputSpecValidation.withValue(true) {
            compute(time)
          }
        }

        rddOption.foreach { case newRDD =>
          // Register the generated RDD for caching and checkpointing
          if (storageLevel != StorageLevel.NONE) {
            newRDD.persist(storageLevel)
            logDebug(s"Persisting RDD ${newRDD.id} for time $time to $storageLevel")
          }
          if (checkpointDuration != null && (time - zeroTime).isMultipleOf(checkpointDuration)) {
            newRDD.checkpoint()
            logInfo(s"Marking RDD ${newRDD.id} for time $time for checkpointing")
          }
          generatedRDDs.put(time, newRDD)
        }
        rddOption
      } else {
        None
      }
    }
  }

这里简单说下逻辑,利用传入的批次时间去generatedRDDs取RDD,如果取得了则返回对应RDD,如果没有取到则从生成新的RDD,并把新的RDD和对应的批次时间回写到generatedRDDs

接下来看下RDD的map方法实现

/**
   * Return a new RDD by applying a function to all elements of this RDD.
   */
  def map[U: ClassTag](f: T => U): RDD[U] = withScope {
    val cleanF = sc.clean(f)
    new MapPartitionsRDD[U, T](this, (context, pid, iter) => iter.map(cleanF))
  }

核心是实例化MapPartitionsRDD,执行时调用的是Scala的map方法。这里不再对MapPartitionsRDD下钻具体实现。我们只看下scala的map方法。

/** Creates a new iterator that maps all produced values of this iterator
   *  to new values using a transformation function.
   *
   *  @param f  the transformation function
   *  @return a new iterator which transforms every value produced by this
   *          iterator by applying the function `f` to it.
   *  @note   Reuse: $consumesAndProducesIterator
   */
  def map[B](f: A => B): Iterator[B] = new AbstractIterator[B] {
    def hasNext = self.hasNext
    def next() = f(self.next())
  }

这里的map是Scala Iterator类下的map。在调用map是不会立即执行,而是返回AbstractIterator对象。当调用next方法时才会执行传入的方法。hasNext则是可以判断是否还有下一个元素。

FilteredDStream

private[streaming]
class FilteredDStream[T: ClassTag](
    parent: DStream[T],
    filterFunc: T => Boolean
  ) extends DStream[T](parent.ssc) {

  override def dependencies: List[DStream[_]] = List(parent)

  override def slideDuration: Duration = parent.slideDuration

  override def compute(validTime: Time): Option[RDD[T]] = {
    parent.getOrCompute(validTime).map(_.filter(filterFunc))
  }
}

这里的绝大部分逻辑与map一致。只是调用RDD的方法从map变为filter。

/**
   * Return a new RDD containing only the elements that satisfy a predicate.
   */
  def filter(f: T => Boolean): RDD[T] = withScope {
    val cleanF = sc.clean(f)
    new MapPartitionsRDD[T, T](
      this,
      (context, pid, iter) => iter.filter(cleanF),
      preservesPartitioning = true)
  }

RDD的filter方法实例化的也是MapPartitionsRDD,执行的方法是scala的filter方法。

/** Returns an iterator over all the elements of this iterator that satisfy the predicate `p`.
   *  The order of the elements is preserved.
   *
   *  @param p the predicate used to test values.
   *  @return  an iterator which produces those values of this iterator which satisfy the predicate `p`.
   *  @note    Reuse: $consumesAndProducesIterator
   */
  def filter(p: A => Boolean): Iterator[A] = new AbstractIterator[A] {
    // TODO 2.12 - Make a full-fledged FilterImpl that will reverse sense of p
    private var hd: A = _
    private var hdDefined: Boolean = false

    def hasNext: Boolean = hdDefined || {
      do {
        // 判断是否有下一个元素,如果没有返回false
        if (!self.hasNext) return false
        // 取下一个元素赋值各hd
        hd = self.next()
      // p为传入的方法,如果p方法返回为false则说明hd元素需要被过滤,继续循环
      // 如果p方法返回为true则说明 现在的hd为不需要过滤的元素。需要返回
      } while (!p(hd)) 
      // 置为true,下次调用hasNext返回之间返回true,不用再循环。
      hdDefined = true
      true
    }

    def next() = if (hasNext) { hdDefined = false; hd } else empty.next()
  }

上面代码为scala的filter方法实现,我把hasNext的一些逻辑加上注释会好理解些。

print

/**
   * Print the first num elements of each RDD generated in this DStream. This is an output
   * operator, so this DStream will be registered as an output stream and there materialized.
   */
  def print(num: Int): Unit = ssc.withScope { // num 代表要打印多少个元素
    def foreachFunc: (RDD[T], Time) => Unit = {
      (rdd: RDD[T], time: Time) => {
        // 比要打印的元素个数多一个
        val firstNum = rdd.take(num + 1)
        // scalastyle:off println
        println("-------------------------------------------")
        println(s"Time: $time")
        println("-------------------------------------------")
        // 打印num个元素
        firstNum.take(num).foreach(println)
        // 如果RDD元素个数大于num则大于"..."
        if (firstNum.length > num) println("...")
        println()
        // scalastyle:on println
      }
    }
    foreachRDD(context.sparkContext.clean(foreachFunc), displayInnerRDDOps = false)
  }

print主要的逻辑是foreachFunc方法。具体解释请看注释。之后会把foreachFunc方法传给foreachRDDforeachRDD会实例化ForEachDStream对象。

private[streaming]
class ForEachDStream[T: ClassTag] (
    parent: DStream[T], // 上一个DStream
    foreachFunc: (RDD[T], Time) => Unit, // 执行的方法
    displayInnerRDDOps: Boolean
  ) extends DStream[Unit](parent.ssc) {

  override def dependencies: List[DStream[_]] = List(parent)

  override def slideDuration: Duration = parent.slideDuration

  override def compute(validTime: Time): Option[RDD[Unit]] = None

  override def generateJob(time: Time): Option[Job] = {
    parent.getOrCompute(time) match {
      // 如果Option有值
      case Some(rdd) =>
        val jobFunc = () => createRDDWithLocalProperties(time, displayInnerRDDOps) {
          // 执行对应批次对应rdd的执行逻辑
          foreachFunc(rdd, time)
        }
        Some(new Job(time, jobFunc))
      case None => None
    }
  }
}

ForEachDStream主要作用是执行DStream,属性foreachFunc就是刚刚看的打印逻辑。接下来只要看generateJob方法。在注释里简单解释了如何执行打印方法。至于更深的将不在此介绍。

总结

至此,结合一个具体的Streaming逻辑,把Sreaming逻辑简单地讲解了。有的地方讲的比较浅,可能会在之后的文章中补充。

附上源代码链接:https://github.com/youtNa/all-practice/blob/master/spark-test/src/main/scala/streaming/Api.scala

引用

  • https://blog.csdn.net/snail_gesture/article/details/51448168
  • https://github.com/apache/spark/tree/v2.3.1
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值