概述
本文介紹spark中Broadcast Variables的實(shí)現(xiàn)原理。
基本概念
在spark中廣播變量屬于共享變量的一種,spark對(duì)共享變量的介紹如下:
通常,當(dāng)在遠(yuǎn)程集群節(jié)點(diǎn)上執(zhí)行傳遞給Spark操作(例如map或reduce)的函數(shù)時(shí),它將在函數(shù)中使用的所有變量的單獨(dú)副本上工作。這些變量將復(fù)制到每臺(tái)計(jì)算機(jī),而且遠(yuǎn)程機(jī)器上的變量的更新不會(huì)同步給驅(qū)動(dòng)程序(driver)端。這種情況下,跨任務(wù)讀寫(xiě)共享變量效率低下。但是,Spark確實(shí)為兩種常見(jiàn)的使用模式提供了兩種有限類(lèi)型的共享變量:廣播變量和累加器。
spark的共享變量有兩種:
廣播變量(broadcast variables)
累加器(accumulators)
注意: 每個(gè)廣播變量和累加器只能在一個(gè)上下文中(context)寫(xiě)入(分別是驅(qū)動(dòng)程序(driver)或工作程序(worker)),而在另一個(gè)上下文中(context)讀取。
廣播變量可以在driver程序中寫(xiě)入,在executor端讀取。
累加器在executors中寫(xiě)入,而在驅(qū)動(dòng)程序(driver端)讀取。
廣播變量(Broadcast Variables)介紹
Spark將值傳遞給Spark executor一次,并且當(dāng)多次使用廣播變量時(shí),任務(wù)可以共享它而不會(huì)導(dǎo)致重復(fù)的網(wǎng)絡(luò)傳輸。
廣播變量為我們提供了一種方法,可以在驅(qū)動(dòng)程序(driver端)上獲取本地值,并將只讀副本分發(fā)給每臺(tái)機(jī)器(worker),而不是為每個(gè)任務(wù)(task)發(fā)送新副本。廣播變量似乎不是特別有用,因?yàn)槲覀兛梢栽陂]包中捕獲局部變量,以便將數(shù)據(jù)從驅(qū)動(dòng)程序傳輸?shù)絯orker; 但是,每臺(tái)機(jī)器只發(fā)送一個(gè)副本而不是每個(gè)任務(wù)發(fā)送一個(gè)副本可以節(jié)省大量成本,特別是在相同的廣播變量用于其他轉(zhuǎn)換時(shí)。 使用廣播變量的兩個(gè)常見(jiàn)示例是:
廣播需要join的的小表。
廣播機(jī)器學(xué)習(xí)模型以便能夠?qū)ξ覀兊臄?shù)據(jù)進(jìn)行預(yù)測(cè)。
通過(guò)在SparkContext上調(diào)用broadcast來(lái)創(chuàng)建廣播變量。 這會(huì)將值分配給worker并為我們提供一個(gè)包裝器(wrapper),允許我們通過(guò)調(diào)用value來(lái)訪問(wèn)worker上的值。如果使用變量輸入創(chuàng)建廣播變量,則在創(chuàng)建變量后不應(yīng)修改輸入,因?yàn)楝F(xiàn)有worker將看不到更新,新的worker才可能會(huì)看到新的值。
另外要注意:廣播變量的值必須是本地的可序列化的值:而不是RDD或其他分布式數(shù)據(jù)結(jié)構(gòu)。
Broadcast的實(shí)現(xiàn)
大致的實(shí)現(xiàn)如下圖所示:
broadcast()函數(shù)
該函數(shù)在SparkContext中進(jìn)行定義,函數(shù)原型如下:
def broadcast[T: ClassTag](value: T): Broadcast[T]
1
在SparkContext中需要調(diào)用broadcast函數(shù)來(lái)創(chuàng)建一個(gè)廣播變量,并返回一個(gè)org.apache.spark.broadcast.Broadcast對(duì)象這樣可以在分布式函數(shù)中來(lái)讀取廣播變量的值。該變量會(huì)被發(fā)送到spark集群的每個(gè)執(zhí)行的節(jié)點(diǎn)上。
注意:該廣播變量一旦創(chuàng)建,將不可修改,因?yàn)榧词剐薷牧嗽撟兞康闹担矡o(wú)法讓spark集群的執(zhí)行節(jié)點(diǎn)看到改變后的新值。
broadcast()函數(shù)的實(shí)現(xiàn)流程如下:
判斷需要廣播的變量是否是分布式變量,若是則終止函數(shù),報(bào)告“不能廣播分布式變量”的錯(cuò)誤。
通過(guò)通過(guò)BroadcastManager的newBroadcast函數(shù)來(lái)創(chuàng)建廣播變量,并返回一個(gè)Broadcast對(duì)象,這里其實(shí)是TorrentBroadcast類(lèi)的對(duì)象
注冊(cè)broadcast的cleanup函數(shù),可以用來(lái)清除不再使用的broadcast變量。
最后,返回新創(chuàng)建的對(duì)象
注意:不能對(duì)分布式變量,比如:rdd,進(jìn)行廣播。
在類(lèi)SparkContext中,broadcast函數(shù)的實(shí)現(xiàn)代碼如下:
def broadcast[T: ClassTag](value: T): Broadcast[T] = {
assertNotStopped()
// 不能直接廣播rdd等分布式變量
require(!classOf[RDD[_]].isAssignableFrom(classTag[T].runtimeClass),
"Can not directly broadcast RDDs; instead, call collect() and broadcast the result.")
// 通過(guò)BroadcastManager工具類(lèi)來(lái)創(chuàng)建一個(gè)BroadcastFactory對(duì)象
val bc = env.broadcastManager.newBroadcast[T](value, isLocal)
val callSite = getCallSite
logInfo("Created broadcast " + bc.id + " from " + callSite.shortForm)
cleaner.foreach(_.registerBroadcastForCleanup(bc))
// 返回Broadcast對(duì)象,這里其實(shí)是TorrentBroadcast類(lèi)的對(duì)象
bc
}
BroadcastManager
該類(lèi)是一個(gè)輔助類(lèi),用來(lái)統(tǒng)一創(chuàng)建broadcast對(duì)外的接口。該類(lèi)的構(gòu)造函數(shù)流程如下:
定義了兩個(gè)私有化變量,并且會(huì)為每個(gè)廣播變量生成一個(gè)唯一的id,在創(chuàng)建broadcast變量時(shí)會(huì)通過(guò)nextBroadcastId.getAndIncrement()進(jìn)行自增,并調(diào)用initialize()函數(shù)進(jìn)行初始化:
// 是否已經(jīng)初始
private var initialized = false
private var broadcastFactory: BroadcastFactory = null
initialize()
// 生成廣播變量的id,該id是唯一的,這里先初始化,會(huì)在創(chuàng)建broadcast變量時(shí)進(jìn)行自增操作
private val nextBroadcastId = new AtomicLong(0)
initialize()函數(shù)的實(shí)現(xiàn)邏輯如下:
(1)初始化broadcastFactory變量,這里創(chuàng)建了TorrentBroadcastFactory對(duì)象
(2)調(diào)用TorrentBroadcastFactory的initialize函數(shù)來(lái)初始化。在實(shí)際的代碼中,該類(lèi)的initialize函數(shù)什么都不做。
(3)把initialized設(shè)置為true,同一個(gè)對(duì)象只初始化一次
// Called by SparkContext or Executor before using Broadcast
private def initialize() {
synchronized { // 加鎖
if (!initialized) {
// 初始化broadcastFactory變量,這里創(chuàng)建了TorrentBroadcastFactory對(duì)象
broadcastFactory = new TorrentBroadcastFactory
// 調(diào)用TorrentBroadcastFactory的initialize函數(shù)來(lái)初始化
broadcastFactory.initialize(isDriver, conf, securityManager)
// 把initialized設(shè)置為true,同一個(gè)對(duì)象只初始化一次
initialized = true
}
}
}
從以上分析可以看到,當(dāng)創(chuàng)建廣播變量時(shí),實(shí)際上是調(diào)用的TorrentBroadcastFactory類(lèi)的newBroadcast函數(shù)來(lái)進(jìn)行創(chuàng)建。
TorrentBroadcastFactory工廠類(lèi)
該類(lèi)實(shí)現(xiàn)了一個(gè)類(lèi)似于BitTorrent的協(xié)議,通過(guò)該協(xié)議把廣播數(shù)據(jù)分發(fā)到各個(gè)executor中。這些操作其實(shí)是在類(lèi)TorrentBroadcast中實(shí)現(xiàn)。
該類(lèi)的代碼相對(duì)簡(jiǎn)單,如下:
private[spark] class TorrentBroadcastFactory extends BroadcastFactory {
override def initialize(isDriver: Boolean, conf: SparkConf, securityMgr: SecurityManager) { }
// 調(diào)用創(chuàng)建一個(gè)TorrentBroadcast對(duì)象
override def newBroadcast[T: ClassTag](value_ : T, isLocal: Boolean, id: Long): Broadcast[T] = {
new TorrentBroadcast[T](value_, id)
}
override def stop() { }
/**
* Remove all persisted state associated with the torrent broadcast with the given ID.
* @param removeFromDriver Whether to remove state from the driver.
* @param blocking Whether to block until unbroadcasted
*/
// 刪除廣播變量
override def unbroadcast(id: Long, removeFromDriver: Boolean, blocking: Boolean) {
TorrentBroadcast.unpersist(id, removeFromDriver, blocking)
}
}
TorrentBroadcast類(lèi)
介紹
真正實(shí)現(xiàn)廣播變量的操作是在TorrentBroadcast類(lèi)中實(shí)現(xiàn)的。該類(lèi)實(shí)現(xiàn)了以下的機(jī)制:
驅(qū)動(dòng)程序(driver)將序列化對(duì)象分成小塊,并將這些塊存儲(chǔ)在驅(qū)動(dòng)程序(driver)的BlockManager中。
在每個(gè)executor上,executor首先嘗試從其BlockManager中獲取對(duì)象。 若它不存在,則遠(yuǎn)程從driver或其他executor(如果可用)中獲取對(duì)象塊。 一旦獲得塊,它就會(huì)將塊放在自己的BlockManager中,為其他executor來(lái)獲取數(shù)據(jù)做好準(zhǔn)備。
通過(guò)這種方式,可以防止driver成為發(fā)送多個(gè)廣播數(shù)據(jù)副本的瓶頸(每個(gè)executor一個(gè))。
代碼實(shí)現(xiàn)分析
該類(lèi)的構(gòu)造過(guò)程如下:
通過(guò)readBroadcastBlock函數(shù)來(lái)從新構(gòu)造廣播對(duì)象,該函數(shù)會(huì)先從driver或其他executors中讀取數(shù)據(jù)塊。在driver端,若需要value值,它會(huì)直接從本地的block manager中讀取數(shù)據(jù)。readBroadcastBlock函數(shù)的實(shí)現(xiàn)邏輯如下:
從SparkEnv.get.broadcastManager.cachedValues從來(lái)獲取對(duì)應(yīng)broadcastId的數(shù)據(jù)塊值:broadcastCache.get(broadcastId)
從blockManager中獲取對(duì)應(yīng)id的廣播變量的值:blockManager.getLocalValues(broadcastId)
若從blockManager中獲取到了該變量的值,則:
若不能從blockManager中獲取值,則調(diào)用readBlocks函數(shù)來(lái)讀取數(shù)據(jù)塊。該函數(shù)會(huì)從driver或其他的executors中讀取該變量的數(shù)據(jù)。該函數(shù)會(huì)調(diào)用blockManager中的getLocalBytes函數(shù)來(lái)獲取遠(yuǎn)端executor中的數(shù)據(jù)塊。
設(shè)置配置信息:setConf(SparkEnv.get.conf)
初始化廣播變量的唯一id值:private val broadcastId = BroadcastBlockId(id)
調(diào)用writeBlocks把廣播變量劃分成多個(gè)塊,并保存到blockManager中。
廣播變量的生命周期
Broadcast類(lèi)是一個(gè)抽象類(lèi),它是TorrentBroadcast的父類(lèi)。在該抽象類(lèi)中,定義了一些常規(guī)的操作主要,包括以下一些操作:
destroy函數(shù)
該函數(shù)最終會(huì)調(diào)用實(shí)體類(lèi):TorrentBroadcast類(lèi)中的unpersist方法。該方法會(huì)從master的blockManager中刪除該廣播變量。
最后,會(huì)調(diào)用doDestroy方法(廣播實(shí)現(xiàn)應(yīng)該提供)。
unpersist()函數(shù)
該函數(shù)的實(shí)現(xiàn)如下:
def unpersist(id: Long, removeFromDriver: Boolean, blocking: Boolean): Unit = {
logDebug(s"Unpersisting TorrentBroadcast $id")
SparkEnv.get.blockManager.master.removeBroadcast(id, removeFromDriver, blocking)
}
該函數(shù)會(huì)調(diào)用blockManagerMaster的removeBroadcast函數(shù)來(lái)刪除在executor上屬于該broadcast變量的所有數(shù)據(jù)塊。
實(shí)現(xiàn)過(guò)程是:從driver端發(fā)送一個(gè)RemoveBroadcast消息。
destory()函數(shù)
該函數(shù)和unpersist()函數(shù)的實(shí)現(xiàn)類(lèi)似,不過(guò)該函數(shù)還會(huì)把廣播變量從driver端刪除。
總結(jié)
本文分析了spark中廣播變量的實(shí)現(xiàn)原理。