![d396aec939c90b080e8621420018249c.png](https://i-blog.csdnimg.cn/blog_migrate/07c5ebc00132beeced61f13ecec61cbd.jpeg)
写在前面
希望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](https://i-blog.csdnimg.cn/blog_migrate/78affa849b947d8026835c9140a1e58e.png)
从这个图中我们能从整体看出一个Streaming任务涉及到的类,并且他们的依赖关系。接下来会按照上图解释各类的关系。
- 首先Api类为我们自己定义的类,这个类的main方法里我们需要实例化StreamingContext类,这个类包含了所有的输入算子。这里我们用的是queueStream,所以只在图中展示了这一个方法。
- queueStream方法会实例化QueueInputDStream类,并将实例化的对象返回。
- QueueInputDStream类实现了InputDStream这个抽象类,在QueueInputDStream类中有compute方法,入参validTime为批次时间,此方法定义了每个批次要执行的逻辑,这个类中则定义了每个批次如何从队列中获取数据。QueueInputDStream类中的map方法是我们在应用程序中定义的接下来的算子,我们稍后再细说。我们先向右看,InputDStream类。
- InputDStream类是所有输入算子的父类,其核心差异为每个实现类的comput方法是根据具体的输入定义的。同时InputDStream是DStream的子类。所以类型DStream的父类都是DStream类。DStream类的comput是实际的执行方法,也是每个DStream的核心差异,之后我们会在用到DStream类的属性和方法时在解释其属性及方法。
- 我们回来继续看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。
- MapPartitionsRDD的prev对应的是上一个RDD对象,f为cmpute方法要执行的逻辑。MapPartitionsRDD继承RDD。
- 回到MappedDStream。MappedDSream中的filter方法是map之后要执行的方法,其实例化了FilterdDStream类,这个类除了compute的实现与MappedDStrea不一样其他都一样。不再具体介绍。
- 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
方法。首先调用parent
的getOrCompute
方法,获取由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 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
方法传给foreachRDD
,foreachRDD
会实例化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