轉載自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)
引言
我們前面在 [DStream, DStreamGraph 詳解](DStream, DStreamGraph 詳解) 講到,整個 DStreamGraph
是由 output stream
通過 dependency 引用關係,索引到上游 DStream
節點。而遞歸的追溯到最上游的 InputDStream
節點時,就沒有對其它 DStream
節點的依賴了,因爲 InputDStream
節點本身就代表了最原始的數據集。
我們對 模塊 3:數據產生與導入
細節的解釋,是僅針對 ReceiverInputDStream
及其子類的;其它 InputDStream
子類的講解,我們在另外的文章中進行。即,本模塊的討論範圍是:
- ReceiverInputDStream
- 子類 SocketInputDStream
- 子類 TwitterInputDStream
- 子類 RawInputDStream
- 子類 FlumePollingInputDStream
- 子類 MQTTInputDStream
- 子類 FlumeInputDStream
- 子類 PluggableInputDStream
- 子類 KafkaInputDStream
ReceiverTracker 分發 Receiver 過程
我們已經知道,ReceiverTracker
自身運行在 driver 端,是一個管理分佈在各個 executor 上的 Receiver
的總指揮者。
在 ssc.start()
時,將隱含地調用 ReceiverTracker.start()
;而 ReceiverTracker.start()
最重要的任務就是調用自己的 launchReceivers()
方法將 Receiver
分發到多個 executor 上去。然後在每個 executor 上,由 ReceiverSupervisor
來分別啓動一個 Receiver
接收數據。這個過程用下圖表示:
我們將以 1.4.0 和 1.5.0 這兩個版本爲代表,仔細分析一下 launchReceivers() 的實現。
1.4.0 代表了 1.5.0 以前的版本,如 1.2.x, 1.3.x, 1.4.x
1.5.0 代表了 1.5.0 以來的版本,如 1.5.x, 1.6.x
Spark 1.4.0 的 launchReceivers() 實現
Spark 1.4.0 的 launchReceivers()
的過程如下:
-
(1.a) 構造 Receiver RDD。具體的,是先遍歷所有的
ReceiverInputStream
,獲得將要啓動的所有x
個Receiver
的實例。然後,把這些實例當做x
份數據,在 driver 端構造一個RDD
實例,這個RDD
分爲x
個 partition,每個 partition 包含一個Receiver
數據(即Receiver
實例)。 -
(1.b) 定義計算 func。我們將在多個 executor 上共啓動
x
個Task
,每個Task
負責一個 partition 的數據,即一個Receiver
實例。我們要對這個Receiver
實例做的計算定義爲func
函數,具體的,func
是:- 以這個
Receiver
實例爲參數,構造新的ReceiverSupervisor
實例supervisor
:supervisor = new ReceiverSupervisorImpl(receiver, ...)
supervisor.start()
;這一步將啓動新線程啓動Receiver
實例,然後很快返回supervisor.awaitTermination()
;將一直 block 住當前Task
的線程
- 以這個
-
(1.c) 分發 RDD(Receiver) 和 func 到具體的 executor。上面 (a)(b) 兩步只是在 driver 端定義了
RDD[Receiver]
和 這個RDD
之上將執行的func
,但並沒有具體的去做。這一步是將兩者的定義分發到 executor 上去,馬上就可以實際執行了。 -
(2) 在各個 executor 端,執行(1.b) 中定義的
func
。即啓動Receiver
實例,並一直 block 住當前線程。
這樣,通過 1 個 RDD
實例包含 x
個 Receiver
,對應啓動 1 個 Job
包含 x
個 Task
,就可以完成 Receiver
的分發和部署了。上述 (1.a)(1.b)(1.c)(2) 的過程示意如下圖:
這裏 Spark Streaming 下層的 Spark Core 對 Receiver
分發是毫無感知的,它只是執行了“應用層面” -- 對 Spark Core 來講,Spark Streaming 就是“應用層面”-- 的一個普通 Job
;但 Spark Streaming 只通過這個普通 Job
即可完“特殊功能”的 Receiver
分發,可謂巧妙巧妙。
Spark 1.5.0 的 launchReceivers() 實現
其實上面這個實現,這個長時運行的分發 Job
還存在一些問題:
- 如果某個
Task
失敗超過spark.task.maxFailures(默認=4)
次的話,整個Job
就會失敗。這個在長時運行的 Spark Streaming 程序裏,Executor
多失效幾次就有可能導致Task
失敗達到上限次數了。 - 如果某個
Task
失效一下,Spark Core 的TaskScheduler
會將其重新部署到另一個 executor 上去重跑。但這裏的問題在於,負責重跑的 executor 可能是在下發重跑的那一刻是正在執行Task
數較少的,但不一定能夠將Receiver
分佈的最均衡的。 - 有個用戶 code 可能會想自定義一個
Receiver
的分佈策略,比如所有的Receiver
都部署到同一個節點上去。
從 1.5.0 開始,Spark Streaming 添加了增強的 Receiver
分發策略。對比之前的版本,主要的變更在於:
- 添加可插拔的
ReceiverSchedulingPolicy
- 把
1
個Job
(包含x
個Task
),改爲x
個Job
(每個Job
只包含1
個Task
) - 添加對
Receiver
的監控重啓機制
我們一個一個看一看。
(1) 可插拔的 ReceiverSchedulingPolicy
ReceiverSchedulingPolicy
的主要目的,是在 Spark Streaming 層面添加對 Receiver
的分發目的地的計算,相對於之前版本依賴 Spark Core 的 TaskScheduler
進行通用分發,新的 ReceiverSchedulingPolicy
會對 Streaming 應用的更好的語義理解,也能計算出更好的分發策略。
ReceiverSchedulingPolicy
有兩個方法,分別用於:
-
在 Streaming 程序首次啓動時:
- 收集所有
InputDStream
包含的所有Receiver
實例 ——receivers
- 收集所有的 executor ——
executors
—— 作爲候選目的地 - 然後就調用
ReceiverSchedulingPolicy.scheduleReceivers(receivers, executors)
來計算每個Receiver
的目的地 executor 列表
- 收集所有
-
在 Streaming 程序運行過程中,如果需要重啓某個
Receiver
:- 將首先看一看之前計算過的目的地 executor 有沒有還 alive 的
- 如果沒有,就需要
ReceiverSchedulingPolicy.rescheduleReceiver(receiver, ...)
來重新計算這個Receiver
的目的地 executor 列表
默認的 ReceiverSchedulingPolicy
是實現爲 round-robin
式的了。我們舉例說明下這兩個方法:
其中,在 Receiver y
失效時,以前的 Spark Streaming 有可能會在 executor 1 上重啓 Receiver y
,而 1.5.0 以來,將在 executor 3 上重啓 Receiver y
。
(2) 每個 Receiver 分發有單獨的 Job 負責
1.5.0 版本以來的 Spark Streaming,是爲每個 Receiver
都分配單獨的只有 1 個 Task
的 Job
來嘗試分發,這與以前版本將 x
個 Receiver
都放到一個有 x
個 Task
的 Job
裏分發是很不一樣的。
而且,對於這僅有的一個 Task
,只在第 1 次執行時,才嘗試啓動 Receiver
;如果該 Task
因爲失效而被調度到其它 executor 執行時,就不再嘗試啓動 Receiver
、只做一個空操作,從而導致本 Job
的狀態是成功執行已完成。ReceiverTracker
會另外調起一個 Job
—— 有可能會重新計算 Receiver
的目的地 —— 來繼續嘗試 Receiver
分發……如此直到成功爲止。
另外,由於 Spark Core 的 Task
下發時只會參考並大部分時候尊重 Spark Streaming 設置的 preferredLocation
目的地信息,還是有一定可能該分發 Receiver
的 Job
並沒有在我們想要調度的 executor 上運行。此時,在第 1 次執行 Task
時,會首先向 ReceiverTracker
發送 RegisterReceiver
消息,只有得到肯定的答覆時,才真正啓動 Receiver
,否則就繼續做一個空操作,導致本 Job
的狀態是成功執行已完成。當然,ReceiverTracker
也會另外調起一個 Job
,來繼續嘗試 Receiver
分發……如此直到成功爲止。
我們用圖示來表達這個改動:
所以通過上面可以看到,一個 Receiver
的分發 Job
是有可能沒有完成分發 Receiver
的目的的,所以 ReceiverTracker
會繼續再起一個 Job
來嘗試 Receiver
分發。這個機制保證了,如果一次 Receiver
如果沒有抵達預先計算好的 executor,就有機會再次進行分發,從而實現在 Spark Streaming 層面對 Receiver
所在位置更好的控制。
(3) 對 Receiver
的監控重啓機制
上面分析了每個 Receiver
都有專門的 Job
來保證分發後,我們發現這樣一來,Receiver
的失效重啓就不受 spark.task.maxFailures(默認=4)
次的限制了。
因爲現在的 Receiver
重試不是在 Task
級別,而是在 Job
級別;並且 Receiver
失效後並不會導致前一次 Job
失敗,而是前一次 Job
成功、並新起一個 Job
再次進行分發。這樣一來,不管 Spark Streaming 運行多長時間,Receiver
總是保持活性的,不會隨着 executor 的丟失而導致 Receiver
死去。
總結
我們再簡單對比一下 1.4.0 和 1.5.0 版本在 Receiver
分發上的區別:
通過以上分析,我們總結:
Spark Streaming 1.4.0 | Spark Streaming 1.5.0 | |
Receiver 活性 | 不保證永活 | 無限重試、保證永活 |
Receiver 均衡分發 | 無保證 | round-robin 策略 |
自定義 Receiver 分發 | 很 tricky | 方便 |