Spark中廣播變量詳解以及如何動態更新廣播變量

前言: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時再做詳細闡述。

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