我們在前面的文章講過,Spark Streaming 的 模塊
1 DAG 靜態定義
要解決的問題就是如何把計算邏輯描述爲一個 RDD DAG 的“模板”,在後面 Job 動態生成的時候,針對每個 batch,都將根據這個“模板”生成一個 RDD DAG 的實例。
在 Spark Streaming 裏,這個 RDD “模板”對應的具體的類是 DStream
,RDD
DAG “模板”對應的具體類是 DStreamGraph
。
本文我們就來詳解 DStream
最主要的功能:爲每個
batch 生成 RDD
實例。
Quick Example
我們在前文 DStream, DStreamGraph 詳解 中引用了 Spark Streaming 官方的 quick example 的這段對 DStream DAG 的定義,注意看代碼中的註釋講解內容:
這裏我們找到 ssc.socketTextStream("localhost",
9999)
的源碼實現:
也就是 ssc.socketTextStream()
將 new
出來一個 DStream
具體子類 SocketInputDStream
的實例。
然後我們繼續找到下一行 lines.flatMap(_.split("
"))
的源碼實現:
也就是 lines.flatMap(_.split("
"))
將 new
出來一個 DStream
具體子類 FlatMappedDStream
的實例。
後面幾行也是如此,所以我們如果用 DStream DAG 圖來表示之前那段 quick example 的話,就是這個樣子:
也即,我們給出的那段代碼,用具體的實現來替換的話,結果如下:
DStream
通過 generatedRDD
管理已生成的 RDD
DStream
內部用一個類型是 HashMap
的變量 generatedRDD
來記錄已經生成過的 RDD
:
generatedRDD
的
key 是一個 Time
;這個 Time
是與用戶指定的 batchDuration
對齊了的時間
—— 如每 15s 生成一個 batch 的話,那麼這裏的 key 的時間就是 08h:00m:00s
,08h:00m:15s
這種,所以其實也就代表是第幾個
batch。generatedRDD
的
value 就是 RDD
的實例。
需要注意,每一個不同的 DStream
實例,都有一個自己的 generatedRDD
。如在下圖中,DStream
a, b, c, d
各有自己的generatedRDD
變量;圖中也示意了 DStream
a
的 generatedRDD
變量。
DStream
對這個 HashMap
的存取主要是通過 getOrCompute(time:
Time)
方法,實現也很簡單,就是一個 —— 查表,如果有就直接返回,如果沒有就生成了放入表、再返回 —— 的邏輯:
最主要還是調用了一個 abstract 的 compute(time)
方法。這個方法用於生成 RDD
實例,生成後被放進 generatedRDD
裏供後續的查詢和使用。這個 compute(time)
方法在 DStream
類裏是
abstract 的,但在每個具體的子類裏都提供了實現。
(a) InputDStream
的 compute(time)
實現
InputDStream
是個有很多子類的抽象類,我們看一個具體的子類 FileInputDStream
。
而 filesToRDD()
實現如下:
所以,結合以上 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
—— MappedDStream
, FilteredDStream
——
的實現:
MappedDStream
的 compute(time)
實現
MappedDStream
很簡單,全類實現如下:
可以看到,首先在構造函數裏傳入了兩個重要內容:
- parent,是本
MappedDStream
上游依賴的DStream
- mapFunc,是本次 map() 轉換的具體函數
- 在前文 DStream, DStreamGraph 詳解 中的 quick example 裏的
val pairs = words.map(word => (word, 1))
的mapFunc
就是word => (word, 1)
- 在前文 DStream, DStreamGraph 詳解 中的 quick example 裏的
所以在 compute(time)
的具體實現裏,就很簡單了:
- (1) 獲取 parent
DStream
在本 batch 裏對應的RDD
實例 - (2) 在這個 parent
RDD
實例上,以mapFunc
爲參數調用.map(mapFunc)
方法,將得到的新RDD
實例返回- 完全相當於用 RDD API 寫了這樣的代碼:
return parentRDD.map(mapFunc)
- 完全相當於用 RDD API 寫了這樣的代碼:
FilteredDStream
的 compute(time)
實現
再看看 FilteredDStream
的全部實現:
同 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)
- 完全相當於用 RDD API 寫了這樣的代碼:
總結一般 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
全部實現如下:
同前面一樣,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)
的具體實現:
就可以知道,如果對着 rdd
調用上面這個 foreachFunc
的話,就會在每個
batch 裏,都會在 rdd
上執行 .take()
獲取一些元素到
driver 端,然後再 .foreach(println)
;也就形成了在
driver 端打印這個 DStream
的一些內容的效果了!
DStreamGraph 生成 RDD DAG 實例
在前文 Spark Streaming 實現思路與模塊概述 中,我們曾經講過,在每個 batch 時,都由 JobGenerator
來要求 RDD
DAG
“模板” 來創建 RDD
DAG
實例,即下圖中的第 (2) 步。
具體的,是 JobGenerator
來調用 DStreamGraph
的 generateJobs(time)
方法。
那麼翻出來 generateJobs()
的實現:
也就是說,是 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()
。
所以其實 Job
的本質是將實際的 func()
定義和 func()
被調用分離了
—— 就像 Runnable
是將 run()
的具體定義和run()
的被調用分離了一樣。
下面我們繼續來看 x.generateJob(time)
和 y.generateJob(time)
實現。
x.generateJob(time)
過程
x
是一個 ForEachDStream
,其 generateJob(time)
的實現如下:
就是這裏牽扯到了 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 實例化 RDD
,DStreamGraph
作爲 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 實例詳解