【前言:Spark目前提供了兩種有限定類型的共享變量:廣播變量和累加器,今天主要介紹一下基於Spark2.4版本的廣播變量。先前的版本比如Spark2.1之前的廣播變量有兩種實現:HttpBroadcast和TorrentBroadcast,但是鑑於HttpBroadcast有各種弊端,目前已經捨棄這種實現,本篇文章也主要闡述TorrentBroadcast】
廣播變量概述
廣播變量是一個只讀變量,通過它我們可以將一些共享數據集或者大變量緩存在Spark集羣中的各個機器上而不用每個task都需要copy一個副本,後續計算可以重複使用,減少了數據傳輸時網絡帶寬的使用,提高效率。相比於Hadoop的分佈式緩存,廣播的內容可以跨作業共享。
廣播變量要求廣播的數據不可變、不能太大但也不能太小(一般幾十M以上)、可被序列化和反序列化、並且必須在driver端聲明廣播變量,適用於廣播多個stage公用的數據,存儲級別目前是MEMORY_AND_DISK。
廣播變量存儲目前基於Spark實現的BlockManager分佈式存儲系統,Spark中的shuffle數據、加載HDFS數據時切分過來的block塊都存儲在BlockManager中,不是今天的討論點,這裏先不做詳述了。
廣播變量的創建方式和獲取
//創建廣播變量
val broadcastVar = sparkSession.sparkContext.broadcast(Array(1, 2, 3))
//獲取廣播變量
broadcastVar.value
廣播變量實例化過程
1.首先調用val broadcastVar = sparkSession.sparkContext.broadcast(Array(1, 2, 3))
2.調用BroadcastManager的newBroadcast方法
val bc = env.broadcastManager.newBroadcast[T](value, isLocal)
3.通過廣播工廠的newBroadcast方法進行創建
broadcastFactory.newBroadcast[T](value_, isLocal, nextBroadcastId.getAndIncrement())
在調用BroadcastManager的newBroadcast方法時已完成對廣播工廠的初始化(initialize方法),我們只需看BroadcastFactory的實現TorrentBroadcastFactory中對TorrentBroadcast的實例化過程:
new TorrentBroadcast[T](value_, id)
4.在構建TorrentBroadcast時,將廣播的數據寫入BlockManager
1)首先會將廣播變量序列化後的對象劃分爲多個block塊,存儲在driver端的BlockManager,這樣運行在driver端的task就不用創建廣播變量的副本了(具體可以查看TorrentBroadcast的writeBlocks方法)
2)每個executor在獲取廣播變量時首先從本地的BlockManager獲取。獲取不到就會從driver或者其他的executor上獲取,獲取之後,會將獲取到的數據保存在自己的BlockManager中
3)塊的大小默認4M
conf.getSizeAsKb("spark.broadcast.blockSize", "4m").toInt * 1024
廣播變量初始化過程
1.首先調用broadcastVar.value
2.TorrentBroadcast中lazy變量_value進行初始化,調用readBroadcastBlock()
3.先從緩存中讀取,對結果進行模式匹配,匹配成功的直接返回
4.讀取不到通過readBlocks()進行讀取
從driver端或者其他的executor中讀取,將讀取的對象存儲到本地,並存於緩存中
new ReferenceMap(AbstractReferenceMap.HARD, AbstractReferenceMap.WEAK)
Spark兩種廣播變量對比
正如【前言】中所說,HttpBroadcast在Spark後續的版本中已經被廢棄,但考慮到部分公司用的Spark版本較低,面試中仍有可能問到兩種實現的相關問題,這裏簡單介紹一下:
HttpBroadcast會在driver端的BlockManager裏面存儲廣播變量對象,並且將該廣播變量序列化寫入文件中去。所有獲取廣播數據請求都在driver端,所以存在單點故障和網絡IO性能問題。
TorrentBroadcast會在driver端的BlockManager裏面存儲廣播變量對象,並將廣播對象分割成若干序列化block塊(默認4M),存儲於BlockManager。小的block存儲位置信息,存儲於Driver端的BlockManagerMaster。數據請求並非集中於driver端,避免了單點故障和driver端網絡磁盤IO過高。
TorrentBroadcast在executor端存儲一個對象的同時會將獲取的block存儲於BlockManager,並向driver端的BlockManager彙報block的存儲信息。
請求數據的時候會先獲取block的所有存儲位置信息,並且是隨機的在所有存儲了該executor的BlockManager去獲取,避免了數據請求服務集中於一點。
總之就是HttpBroadcast導致獲取廣播變量的請求集中於driver端,容易引起driver端單點故障,網絡IO過高影響性能等問題,而TorrentBroadcast獲取廣播變量的請求服務即可以請求到driver端也可以在executor,避免了上述問題,當然這只是主要的優化點。
動態更新廣播變量
通過上面的介紹,大家都知道廣播變量是隻讀的,那麼在Spark流式處理中如何進行動態更新廣播變量?
既然無法更新,那麼只能動態生成,應用場景有實時風控中根據業務情況調整規則庫、實時日誌ETL服務中獲取最新的日誌格式以及字段變更等。
@volatile private var instance: Broadcast[Array[Int]] = null
//獲取廣播變量單例對象
def getInstance(sc: SparkContext, ctime: Long): Broadcast[Array[Int]] = {
if (instance == null) {
synchronized {
if (instance == null) {
instance = sc.broadcast(fetchLastestData())
}
}
}
instance
}
//加載要廣播的數據,並更新廣播變量
def updateBroadCastVar(sc: SparkContext, blocking: Boolean = false): Unit = {
if (instance != null) {
//刪除緩存在executors上的廣播副本,並可選擇是否在刪除完成後進行block等待
//底層可選擇是否將driver端的廣播副本也刪除
instance.unpersist(blocking)
instance = sc.broadcast(fetchLastestData())
}
}
def fetchLastestData() = {
//動態獲取需要更新的數據
//這裏是僞代碼
Array(1, 2, 3)
}
val dataFormat = FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss")
...
...
stream.foreachRDD { rdd =>
val current_time = dataFormat.format(new Date())
val new_time = current_time.substring(14, 16).toLong
//每10分鐘更新一次
if (new_time % 10 == 0) {
updateBroadCastVar(rdd.sparkContext, true)
}
rdd.foreachPartition { records =>
instance.value
...
}
}
注意:上述是給出了一個實現思路的僞代碼,實際生產中還需要進行一定的優化。
此外,這種方式有一定的弊端,就是廣播的數據因爲是週期性更新,所以存在一定的滯後性。廣播的週期不能太短,要考慮外部存儲要廣播數據的存儲系統的壓力。具體的還要看具體的業務場景,如果對實時性要求不是特別高的話,可以採取這種,當然也可以參考Flink是如何實現動態廣播的。
Spark流式程序中爲何使用單例模式
1.廣播變量是隻讀的,使用單例模式可以減少Spark流式程序中每次job生成執行,頻繁創建廣播變量帶來的開銷
2.廣播變量單例模式也需要做同步處理。在FIFO調度模式下,基本不會發生併發問題。但是如果你改變了調度模式,如採用公平調度模式,同時設置Spark流式程序並行執行的job數大於1,如設置參數spark.streaming.concurrentJobs=4,則必須加上同步代碼
3.在多個輸出流共享廣播變量的情況下,同時配置了公平調度模式,也會產生併發問題。建議在foreachRDD或者transform中使用局部變量進行廣播,避免在公平調度模式下不同job之間產生影響。
除了廣播變量,累加器也是一樣。在Spark流式組件如Spark Streaming底層,每個輸出流都會產生一個job,形成一個job集合提交到線程池裏併發執行,詳細的內容在後續介紹Spark Streaming、Structured Streaming時再做詳細闡述。