spark 廣播變量的設計和實現

spark 官網上對 廣播變量的描述

Broadcast variables allow the programmer to keep a read-only variable cached on each machinerather than shipping a copy of it with tasks. They can be used, for example, to give every node a copy of a large input dataset in an efficient manner. Spark also attempts to distribute broadcast variables using efficient broadcast algorithms to reduce communication cost.

大意是, 使用廣播變量,每個Executor的內存中,只駐留一份變量副本, 而不是對每個 task 都傳輸一次大變量,省了很多的網絡傳輸, 對性能提升具有很大幫助, 而且會通過高效的廣播算法來減少傳輸代價。

使用廣播變量的場景很多, 我們都知道spark 一種常見的優化方式就是小表廣播, 使用 map join 來代替 reduce join, 我們通過把小的數據集廣播到各個節點上,節省了一次特別 expensive 的 shuffle 操作。

比如driver 上有一張數據量很小的表, 其他節點上的task 都需要 lookup 這張表, 那麼 driver 可以先把這張表 copy 到這些節點,這樣 task 就可以在本地查表了。

今天我們來看下 spark 對 廣播變量的設計和實現

spark 廣播的方式

spark 歷史上採用了兩種廣播的方式,一種是通過 Http 協議傳輸數據, 一種是通過 Torrent 協議來傳輸數據, 但是最新的 spark 版本中, http 的方式已經廢棄了(pr 在此 https://github.com/apache/spark/pull/10531), spark 是在 spark 1.1 版本中引入了 TorrentBroadcast, 此後就沒有更新 HttpBroadcast 和相關文檔了, spark2.0 的時候完全可以刪除 HttpBroadcast 了, 之後統一把  TorrentBroadcast 作爲廣播變量的唯一實現方式。 但是代碼沒有寫死, 還是保留了擴展性(BroadcastFactory 作爲一個 trait, TorrentBroadcastFactory 只是一種實現方式, 符合依賴倒置原則, 依賴抽象,不依賴具體實現), 萬一之後想到了更牛x 的實現方式,  可以方便的加上,但是我估計一時半會應該沒有了。

本着過時不講的原則, 我們這裏只說 TorrentBroadcast

大家可以到這裏看下動圖

 

你能看到不同的數據塊是來自不同的節點, 多個節點一起組成一個網絡,在你下載的同時,你也在上傳,所以說在享受別人提供的下載的同時,你也在貢獻,最終所有人一起受益。

我們看下 BitTorrent 協議, wiki 定義

BitTorrent協議(簡稱BT,俗稱比特洪流、BT下載)是用在對等網絡中文件分享的網絡協議程序。和點對點(point-to-point)的協議程序不同,它是用戶羣對用戶羣(peer-to-peer),而且用戶越多,下載同一文件的人越多,下載該檔案的速度越快。且下載後,繼續維持上傳的狀態,就可以“分享”,成爲其用戶端節點下載的種子文件(.torrent),同時上傳及下載。

具體感興趣的可以看下這個論文

http://www.webpaas.com/usr/uploads/2015/01/52279564.pdf

關鍵的幾個點

  • 下載者要下載文件內容,需要先得到相應的種子文件,然後使用BT客戶端軟件進行下載。

  • 提供下載的文件虛擬分成大小相等的塊, 並把每個塊的索引信息和Hash驗證碼寫入種子文件中

  • 有一個 Tracker 負責維護元信息, 所有的客戶端都可以通過 Tracker 找到每個快離自己最近的其他下載者

  • 下載時,BT客戶端首先解析種子文件得到Tracker地址,然後連接Tracker服務器。Tracker服務器迴應下載者的請求,提供下載者其他下載者(包括髮布者)的IP。下載者再連接其他下載者,根據種子文件,兩者分別告知對方自己已經有的塊,然後交換對方所沒有的數據。此時不需要其他服務器參與,分散了單個線路上的數據流量,因此減輕了服務器負擔。

  • 下載者每得到一個塊,需要算出下載塊的Hash驗證碼與種子文件中的對比,如果一樣則說明塊正確,不一樣則需要重新下載這個塊。這種規定是爲了解決下載內容準確性的問題。

針對以上的幾個點, spark 是怎麼做的, 我們看下:

  • TorrentBroadcast 底層使用的是 BlockManager, 下載每個數據塊先要去 master 去獲取 Block 所在的位置 (location)。

  • 在把大變量寫到廣播變量的時候,  通過 ChunkedByteBufferOutputStream把輸入的數據分成多個小塊, zipWithIndex 中, 爲每個小塊加一個唯一標識,   形如 broadcast_broadcastId_pieceId。  作爲BlockId, 存儲在 BlockManager 中。 而且對每個小的數據塊加上一個校驗碼。

  • BlockManagerMaster 作爲 tracker 維護所有 Block塊的元信息, 知道每個數據塊所在的 executor和存儲級別。 Broadcast 變量中維護屬於自己的所有小塊的 BlockId

  • 通過 value 方法讀取 Boradcast 變量的時候, 取出所有小塊的 BlockId, 對於每個 BlockId, 通過BlockManagerMaster 獲取了該BlockId的位置的集合, 隨機化,位置集合被打亂, 優先找同主機的地址(這樣可以走回環),然後從隨機的地址集合按順序取地址一個一個嘗試去獲取數據,因爲隨機化了地址,那麼executor不只會從Driver去獲取數據。分散了driver 上的壓力。

  •  

    取到 Block piece 後, 使用校驗碼進行校驗,看看數據塊有沒有損壞, 如果沒有損壞, 然後按照順序拼在一起。

大家比較一下, 流程是不是差不多, 基本貫穿了 BitTorrent 的思想原理。

大家看下上面的圖, 開始的時候, 大家都是通過 driver 拿數據, 但是一旦其他 executor 上有了數據塊之後, 所有的 executor 都是有機會通過別的 executor 來獲取數據塊, 這樣就分散了 driver 的壓力。 套用一句話, 下載的 executor 越多, 下載的越快。

spark 廣播變量的使用姿勢

val array: Array[Int] = ???

val broadcasted = sc.broadcast(array)

val rdd: RDD[Int] = ???

rdd.map(i => array.contains(i))  // 這種沒有使用 broadcast, 每次 task 都要傳一下 數組, 浪費內網帶寬

rdd.map(i => broadcasted.value.contains(i))

上面的一個小的 demo 就是把一個 數組通過 broadcast 的方式廣播出去, 然後就可以在 task 裏面使用數組變量了, 這個數組變量是駐留在  executor上的, 不用每次調度 task運行的時候都得傳輸一次 數組。

我們可以看到對於 broadcast 的使用, 無非就是 sc.broadcast 定義了一個 廣播變量 和 broadcasted.value 使用廣播變量的 value 方法,找到真正的數組。

spark context 初始化的時候, sparkEnv 中初始化了一個 broadcastManager,初始化方法裏面, 現在默認使用的 TorrentBroadcastFactory, 調用 sc.broadcast 方法, 就會使用工廠模式創建一個  TorrentBroadcast,這時候就會調用寫操作, 把數據分成小塊寫到 BlockManager 中,   broadcasted 只是一個 TorrentBroadcast 類型的實例, 並沒有數組數據, 這個實例只維護了數據的 元信息, 也就是一組BlockId 信息, 這個實例被序列化被傳到 executor上,  在 executor 上調用這個實例的 value 方法,纔會觸發去 BlockManager 上讀真正的數據。

廣播變量的回收

在調用 sc.Broadcast 方法中, 會去 ContextCleaner 中註冊一下, 之前講的 緩存RDD 的時候也要去 ContextCleaner 中註冊一下, 兩個差不多,都是爲了回收。

cleaner.foreach(_.registerBroadcastForCleanup(bc))

當廣播變量引用爲null的時候, 在context cleaner 裏面會回調 broadcastManager.unbroadcast 方法, 會把 Broadcast 變量從 BlockManager 存儲中幹掉。

爲什麼只能 broadcast 只讀的變量

這就涉及一致性的問題,如果變量可以被更新,那麼一旦變量被某個節點更新,其他節點要不要一塊更新?如果多個節點同時在更新,更新順序是什麼?怎麼做同步? 仔細想一下, 每個都很頭疼, spark 目前就索性搞成了只讀的。  因爲分佈式強一致性真的很蛋疼

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