《spark streaming源碼三》DStream 生成 RDD 實例詳解

 轉載自https://github.com/lw-lin/CoolplaySpark

本系列內容適用範圍:

* 2017.07.11 update, Spark 2.2 全系列 √ (已發佈:2.2.0)
* 2017.10.02 update, Spark 2.1 全系列 √ (已發佈:2.1.0, 2.1.1, 2.1.2)
* 2016.11.14 update, Spark 2.0 全系列 √ (已發佈:2.0.0, 2.0.1, 2.0.2)

目錄

引言

Quick Example

DStream 通過 generatedRDD 管理已生成的 RDD

(a) InputDStream 的 compute(time) 實現

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

MappedDStream 的 compute(time) 實現

FilteredDStream 的 compute(time) 實現

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

(c) ForEachDStream 的 compute(time) 實現

DStreamGraph 生成 RDD DAG 實例

Spark Streaming 的 Job

x.generateJob(time) 過程

y.generateJob(time) 過程

返回 Seq[Job]


引言

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

在 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 詳解](1.1 DStream, DStreamGraph 詳解.md) 中引用了 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 變量。

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 詳解](1.1 DStream, DStreamGraph 詳解.md) 中的 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 實現思路與模塊概述](0.1 Spark Streaming 實現思路與模塊概述.md) 中,我們曾經講過,在每個 batch 時,都由 JobGenerator 來要求 RDD DAG “模板” 來創建 RDD DAG 實例,即下圖中的第 (2) 步。

具體的,是 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) 方法。

舉個例子,如上圖,由於我們在代碼裏的兩次 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)

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

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

y.generateJob(time) 過程

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

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

返回 Seq[Job]

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

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

至此,在給定的 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,就分析完成了。

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