DStream 生成 RDD 實例詳解

我們在前面的文章講過,Spark Streaming 的 模塊 1 DAG 靜態定義 要解決的問題就是如何把計算邏輯描述爲一個 RDD DAG 的“模板”,在後面 Job 動態生成的時候,針對每個 batch,都將根據這個“模板”生成一個 RDD DAG 的實例。

image

在 Spark Streaming 裏,這個 RDD “模板”對應的具體的類是 DStream,RDD DAG “模板”對應的具體類是 DStreamGraph

DStream      的全限定名是:org.apache.spark.streaming.dstream.DStream
DStreamGraph 的全限定名是:org.apache.spark.streaming.DStreamGraph

本文我們就來詳解 DStream 最主要的功能:爲每個 batch 生成 RDD 實例。

Quick Example

我們在前文 DStream, DStreamGraph 詳解 中引用了 Spark Streaming 官方的 quick example 的這段對 DStream DAG 的定義,注意看代碼中的註釋講解內容:

// ssc.socketTextStream() 將創建一個 SocketInputDStream;這個 InputDStream 的 SocketReceiver 將監聽本機 9999 端口
val lines = ssc.socketTextStream("localhost", 9999)

val words = lines.flatMap(_.split(" "))      // DStream transformation
val pairs = words.map(word => (word, 1))     // DStream transformation
val wordCounts = pairs.reduceByKey(_ + _)    // DStream transformation
wordCounts.print()                           // DStream output

這裏我們找到 ssc.socketTextStream("localhost", 9999) 的源碼實現:

def socketStream[T: ClassTag](hostname: String, port: Int, converter: (InputStream) => Iterator[T], storageLevel: StorageLevel): ReceiverInputDStream[T] = {
  new SocketInputDStream[T](this, hostname, port, converter, storageLevel)
}

也就是 ssc.socketTextStream() 將 new 出來一個 DStream 具體子類 SocketInputDStream 的實例。

然後我們繼續找到下一行 lines.flatMap(_.split(" ")) 的源碼實現:

def flatMap[U: ClassTag](flatMapFunc: T => Traversable[U]): DStream[U] = ssc.withScope {
  new FlatMappedDStream(this, context.sparkContext.clean(flatMapFunc))
}

也就是 lines.flatMap(_.split(" ")) 將 new 出來一個 DStream 具體子類 FlatMappedDStream 的實例。

後面幾行也是如此,所以我們如果用 DStream DAG 圖來表示之前那段 quick example 的話,就是這個樣子:

也即,我們給出的那段代碼,用具體的實現來替換的話,結果如下:

val lines = new SocketInputDStream("localhost", 9999)   // 類型是 SocketInputDStream

val words = new FlatMappedDStream(lines, _.split(" "))  // 類型是 FlatMappedDStream
val pairs = new MappedDStream(words, word => (word, 1)) // 類型是 MappedDStream
val wordCounts = new ShuffledDStream(pairs, _ + _)      // 類型是 ShuffledDStream
new ForeachDStream(wordCounts, cnt => cnt.print())      // 類型是 ForeachDStream

DStream 通過 generatedRDD 管理已生成的 RDD

DStream 內部用一個類型是 HashMap 的變量 generatedRDD 來記錄已經生成過的 RDD

private[streaming] var generatedRDDs = new HashMap[Time, RDD[T]] ()

generatedRDD 的 key 是一個 Time;這個 Time 是與用戶指定的 batchDuration 對齊了的時間 —— 如每 15s 生成一個 batch 的話,那麼這裏的 key 的時間就是 08h:00m:00s08h:00m:15s 這種,所以其實也就代表是第幾個 batch。generatedRDD 的 value 就是 RDD 的實例。

需要注意,每一個不同的 DStream 實例,都有一個自己的 generatedRDD。如在下圖中,DStream a, b, c, d各有自己的generatedRDD 變量;圖中也示意了 DStream a 的 generatedRDD 變量。

image

DStream 對這個 HashMap 的存取主要是通過 getOrCompute(time: Time) 方法,實現也很簡單,就是一個 —— 查表,如果有就直接返回,如果沒有就生成了放入表、再返回 —— 的邏輯:

private[streaming] final def getOrCompute(time: Time): Option[RDD[T]] = {
    // 從 generatedRDDs 裏 get 一下:如果有 rdd 就返回,沒有 rdd 就進行 orElse 下面的 rdd 生成步驟
    generatedRDDs.get(time).orElse {
      // 驗證 time 需要是 valid
      if (isTimeValid(time)) {
        // 然後調用 compute(time) 方法獲得 rdd 實例,並存入 rddOption 變量
        val rddOption = createRDDWithLocalProperties(time) {
          PairRDDFunctions.disableOutputSpecValidation.withValue(true) {
            compute(time)
          }
        }

        rddOption.foreach { case newRDD =>
          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")
          }
          // 將剛剛實例化出來的 rddOption 放入 generatedRDDs 對應的 time 位置
          generatedRDDs.put(time, newRDD)
        }
        // 返回剛剛實例化出來的 rddOption
        rddOption
      } else {
        None
      }
    }
  }

最主要還是調用了一個 abstract 的 compute(time) 方法。這個方法用於生成 RDD 實例,生成後被放進 generatedRDD 裏供後續的查詢和使用。這個 compute(time) 方法在 DStream 類裏是 abstract 的,但在每個具體的子類裏都提供了實現。

(a) InputDStream 的 compute(time) 實現

InputDStream 是個有很多子類的抽象類,我們看一個具體的子類 FileInputDStream

// 來自 FileInputDStream
override def compute(validTime: Time): Option[RDD[(K, V)]] = {
    // 通過一個 findNewFiles() 方法,找到 validTime 以後產生的新 file 的數據
    val newFiles = findNewFiles(validTime.milliseconds)
    logInfo("New files at time " + validTime + ":\n" + newFiles.mkString("\n"))
    batchTimeToSelectedFiles += ((validTime, newFiles))
    recentlySelectedFiles ++= newFiles

    // 找到了一些新 file;以新 file 的數組爲參數,通過 filesToRDD() 生成單個 RDD 實例 rdds
    val rdds = Some(filesToRDD(newFiles))

    val metadata = Map(
      "files" -> newFiles.toList,
      StreamInputInfo.METADATA_KEY_DESCRIPTION -> newFiles.mkString("\n"))
    val inputInfo = StreamInputInfo(id, 0, metadata)
    ssc.scheduler.inputInfoTracker.reportInfo(validTime, inputInfo)

    // 返回生成的單個 RDD 實例 rdds
    rdds
  }

而 filesToRDD() 實現如下:

// 來自 FileInputDStream
private def filesToRDD(files: Seq[String]): RDD[(K, V)] = {
  // 對每個 file,都 sc.newAPIHadoopFile(file) 來生成一個 RDD
  val fileRDDs = files.map { file =>
    val rdd = serializableConfOpt.map(_.value) match {
      case Some(config) => context.sparkContext.newAPIHadoopFile(
        file,
        fm.runtimeClass.asInstanceOf[Class[F]],
        km.runtimeClass.asInstanceOf[Class[K]],
        vm.runtimeClass.asInstanceOf[Class[V]],
        config)
      case None => context.sparkContext.newAPIHadoopFile[K, V, F](file)
    }
    if (rdd.partitions.size == 0) {
      logError("File " + file + " has no data in it. Spark Streaming can only ingest " +
        "files that have been \"moved\" to the directory assigned to the file stream. " +
        "Refer to the streaming programming guide for more details.")
    }
    rdd
  }
  // 將每個 file 對應的 RDD 進行 union,返回一個 union 後的 UnionRDD
  new UnionRDD(context.sparkContext, fileRDDs)
}

所以,結合以上 compute(validTime: Time) 和 filesToRDD(files: Seq[String]) 方法,我們得出 FileInputDStream 爲每個 batch 生成 RDD 的實例過程如下:

  • (1) 先通過一個 findNewFiles() 方法,找到 validTime 以後產生的多個新 file
  • (2) 對每個新 file,都將其作爲參數調用 sc.newAPIHadoopFile(file),生成一個 RDD 實例
  • (3) 將 (2) 中的多個新 file 對應的多個 RDD 實例進行 union,返回一個 union 後的 UnionRDD

其它 InputDStream 的爲每個 batch 生成 RDD 實例的過程也比較類似了。

(b) 一般 DStream 的 compute(time) 實現

前一小節的 InputDStream 沒有上游依賴的 DStream,可以直接爲每個 batch 產生 RDD 實例。一般 DStream都是由transofrmation 生成的,都有上游依賴的 DStream,所以爲了爲 batch 產生 RDD 實例,就需要在 compute(time) 方法裏先獲取上游依賴的 DStream 產生的 RDD 實例。

具體的,我們看兩個具體 DStream —— MappedDStreamFilteredDStream —— 的實現:

MappedDStream 的 compute(time) 實現

MappedDStream 很簡單,全類實現如下:

package org.apache.spark.streaming.dstream

import org.apache.spark.streaming.{Duration, Time}
import org.apache.spark.rdd.RDD
import scala.reflect.ClassTag

private[streaming]
class MappedDStream[T: ClassTag, U: ClassTag] (
    parent: DStream[T],
    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))
  }
}

可以看到,首先在構造函數裏傳入了兩個重要內容:

  • parent,是本 MappedDStream 上游依賴的 DStream
  • mapFunc,是本次 map() 轉換的具體函數
    • 在前文 DStream, DStreamGraph 詳解 中的 quick example 裏的 val pairs = words.map(word => (word, 1)) 的mapFunc 就是 word => (word, 1)

所以在 compute(time) 的具體實現裏,就很簡單了:

  • (1) 獲取 parent DStream 在本 batch 裏對應的 RDD 實例
  • (2) 在這個 parent RDD 實例上,以 mapFunc 爲參數調用 .map(mapFunc) 方法,將得到的新 RDD 實例返回
    • 完全相當於用 RDD API 寫了這樣的代碼:return parentRDD.map(mapFunc)

FilteredDStream 的 compute(time) 實現

再看看 FilteredDStream 的全部實現:

package org.apache.spark.streaming.dstream

import org.apache.spark.streaming.{Duration, Time}
import org.apache.spark.rdd.RDD
import scala.reflect.ClassTag

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))
  }
}

同 MappedDStream 一樣,FilteredDStream 也在構造函數裏傳入了兩個重要內容:

  • parent,是本 FilteredDStream 上游依賴的 DStream
  • filterFunc,是本次 filter() 轉換的具體函數

所以在 compute(time) 的具體實現裏,就很簡單了:

  • (1) 獲取 parent DStream 在本 batch 裏對應的 RDD 實例
  • (2) 在這個 parent RDD 實例上,以 filterFunc 爲參數調用 .filter(filterFunc) 方法,將得到的新 RDD 實例返回
    • 完全相當於用 RDD API 寫了這樣的代碼:return parentRDD.filter(filterFunc)

總結一般 DStream 的 compute(time) 實現

總結上面 MappedDStream 和 FilteredDStream 的實現,可以看到:

  • DStream 的 .map() 操作生成了 MappedDStream,而 MappedDStream 在每個 batch 裏生成 RDD 實例時,將對 parentRDD調用 RDD 的 .map() 操作 —— DStream.map() 操作完美複製爲每個 batch 的 RDD.map()操作
  • DStream 的 .filter() 操作生成了 FilteredDStream,而 FilteredDStream 在每個 batch 裏生成 RDD實例時,將對parentRDD 調用 RDD 的 .filter() 操作 —— DStream.filter() 操作完美複製爲每個 batch 的 RDD.filter() 操作

在最開始, DStream 的 transformation 的 API 設計與 RDD 的 transformation 設計保持了一致,就使得,每一個dStreamA.transformation() 得到的新 dStreamB 能將 dStreamA.transformation() 操作完美複製爲每個 batch 的rddA.transformation() 操作。

這也就是 DStream 能夠作爲 RDD 模板,在每個 batch 裏實例化 RDD 的根本原因。

(c) ForEachDStream 的 compute(time) 實現

上面分析了 DStream 的 transformation 如何在 compute(time) 裏複製爲 RDD 的 transformation,下面我們分析 DStream 的output 如何在 compute(time) 裏複製爲 RDD 的 action

我們前面講過,對一個 DStream 進行 output 操作,將生成一個新的 ForEachDStream,這個 ForEachDStream用一個foreachFunc 成員來記錄 output 的具體內容。

ForEachDStream 全部實現如下:

package org.apache.spark.streaming.dstream

import org.apache.spark.rdd.RDD
import org.apache.spark.streaming.{Duration, Time}
import org.apache.spark.streaming.scheduler.Job
import scala.reflect.ClassTag

private[streaming]
class ForEachDStream[T: ClassTag] (
    parent: DStream[T],
    foreachFunc: (RDD[T], Time) => Unit
  ) 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 {
      case Some(rdd) =>
        val jobFunc = () => createRDDWithLocalProperties(time) {
          ssc.sparkContext.setCallSite(creationSite)
          foreachFunc(rdd, time)
        }
        Some(new Job(time, jobFunc))
      case None => None
    }
  }
}

同前面一樣,ForEachDStream 也在構造函數裏傳入了兩個重要內容:

  • parent,是本 ForEachDStream 上游依賴的 DStream
  • foreachFunc,是本次 output 的具體函數

所以在 compute(time) 的具體實現裏,就很簡單了:

  • (1) 獲取 parent DStream 在本 batch 裏對應的 RDD 實例
  • (2) 以這個 parent RDD 和本次 batch 的 time 爲參數,調用 foreachFunc(parentRDD, time) 方法

例如,我們看看 DStream.print() 裏 foreachFunc(rdd, time) 的具體實現:

def foreachFunc: (RDD[T], Time) => Unit = {
  val firstNum = rdd.take(num + 1)
  println("-------------------------------------------")
  println("Time: " + time)
  println("-------------------------------------------")
  firstNum.take(num).foreach(println)
  if (firstNum.length > num) println("...")
  println()
}

就可以知道,如果對着 rdd 調用上面這個 foreachFunc 的話,就會在每個 batch 裏,都會在 rdd 上執行 .take() 獲取一些元素到 driver 端,然後再 .foreach(println);也就形成了在 driver 端打印這個 DStream 的一些內容的效果了!

DStreamGraph 生成 RDD DAG 實例

在前文 Spark Streaming 實現思路與模塊概述 中,我們曾經講過,在每個 batch 時,都由 JobGenerator 來要求 RDD DAG “模板” 來創建 RDD DAG 實例,即下圖中的第 (2) 步。

image

具體的,是 JobGenerator 來調用 DStreamGraph 的 generateJobs(time) 方法。

那麼翻出來 generateJobs() 的實現:

// 來自 DStreamGraph
def generateJobs(time: Time): Seq[Job] = {
  logDebug("Generating jobs for time " + time)
  val jobs = this.synchronized {
    outputStreams.flatMap(outputStream => outputStream.generateJob(time))
  }
  logDebug("Generated " + jobs.length + " jobs for time " + time)
  jobs
}

也就是說,是 DStreamGraph 繼續調用了每個 outputStream 的 generateJob(time) 方法 —— 而我們知道,只有 ForEachDStream 是 outputStream,所以將調用 ForEachDStream 的 generateJob(time) 方法。

image

舉個例子,如上圖,由於我們在代碼裏的兩次 print() 操作產生了兩個 ForEachDStream 節點 x 和 y,那麼DStreamGraph.generateJobs(time) 就將先後調用 x.generateJob(time) 和 y.generateJob(time) 方法,並將各獲得一個 Job。

但是…… x.generateJob(time) 和 y.generateJob(time) 的返回值 Job 到底是啥?那我們先插播一下 Job

Spark Streaming 的 Job

Spark Streaming 裏重新定義了一個 Job 類,功能與 Java 的 Runnable 差不多:一個 Job 能夠自定義一個 func() 函數,而 Job 的 .run() 方法實現就是執行這個 func()

// 節選自 org.apache.spark.streaming.scheduler.Job
private[streaming]
class Job(val time: Time, func: () => _) {
  ...

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

  ...
}

所以其實 Job 的本質是將實際的 func() 定義和 func() 被調用分離了 —— 就像 Runnable 是將 run() 的具體定義和run() 的被調用分離了一樣。

下面我們繼續來看 x.generateJob(time) 和 y.generateJob(time) 實現。

x.generateJob(time) 過程

x 是一個 ForEachDStream,其 generateJob(time) 的實現如下:

// 來自 ForEachDStream
override def generateJob(time: Time): Option[Job] = {
  // 【首先調用 parentDStream 的 getOrCompute() 來獲取 parentRDD】
  parent.getOrCompute(time) match {
    case Some(rdd) =>
      // 【然後定義 jobFunc 爲在 parentRDD 上執行 foreachFun() 】
      val jobFunc = () => createRDDWithLocalProperties(time) {
        ssc.sparkContext.setCallSite(creationSite)
        foreachFunc(rdd, time)
      }
      // 【最後將 jobFunc 包裝爲 Job 返回】
      Some(new Job(time, jobFunc))
    case None => None
  }
}

就是這裏牽扯到了 x 的 parentDStream.getOrCompute(time),即 d.getOrCompute(time);而 d.getOrCompute(time) 會牽扯c.getOrCompute(time),乃至 a.getOrCompute(time)b.getOrCompute(time)

用一個時序圖來表達這裏的調用關係會清晰很多:

image

所以最後的時候,由於對 x.generateJob(time) 形成的遞歸調用, 將形成一個 Job,其內容 func 如下圖:

image

y.generateJob(time) 過程

同樣的,y 節點生成 Job 的過程,與 x 節點的過程非常類似,只是在 b.getOrCompute(time) 時,會命中 get(time) 而不需要觸發 compute(time) 了,這是因爲該 RDD 實例已經在 x 節點的生成過程中被實例化過一次,所以在這裏只需要取出來用就可以了。

同樣,最後的時候,由於對 y.generateJob(time) 形成的遞歸調用, 將形成一個 Job,其內容 func 如下圖:

image

返回 Seq[Job]

所以當 DStreamGraph.generateJobs(time) 結束時,會返回多個 Job,是因爲作爲 output stream 的每個 ForEachDStream 都通過 generateJob(time) 方法貢獻了一個 Job

image

比如在上圖裏,DStreamGraph.generateJobs(time) 會返回一個 Job 的序列,其大小爲 2,其內容分別爲:

image

至此,在給定的 batch 裏,DStreamGraph.generateJobs(time) 的工作已經全部完成,Seq[Job] 作爲結果返回給 JobGenerator後,JobGenerator 也會盡快提交到 JobSheduler 那裏儘快調用 Job.run() 使得這 2 個 RDD DAG 儘快運行起來。

而且,每個新 batch 生成時,都會調用 DStreamGraph.generateJobs(time),也進而出發我們之前討論這個 Job 生成過程,週而復始。

到此,整個 DStream 作爲 RDD 的 “模板” 爲每個 batch 實例化 RDDDStreamGraph 作爲 RDD DAG 的 “模板” 爲每個 batch 實例化 RDD DAG,就分析完成了。

轉自:GitHub

https://github.com/proflin/CoolplaySpark/blob/master/Spark%20Streaming%20%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90%E7%B3%BB%E5%88%97/1.2%20DStream%20%E7%94%9F%E6%88%90%20RDD%20%E5%AE%9E%E4%BE%8B%E8%AF%A6%E8%A7%A3.md

轉載請註明:人人都是數據咖 » DStream 生成 RDD 實例詳解

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章