Spark 儲存模組原始碼學習

Hiway發表於2020-03-22

學習資料:
spark 原始碼分析之十八 -- Spark儲存體系剖析 **
Spark Core原始碼精讀計劃#29:BlockManager主從及RPC邏輯 **
Spark Block Manager管理
BlockManager初始化和註冊解密
Cloud Compution BlockManagerMaster
Spark自己的分散式儲存系統BlockManager全解析
[Spark核心] 第38課:BlockManager架構原理、執行流程圖和原始碼解密
Spark TaskMemoryManager如何為task分配執行記憶體
Spark儲存體系——Block傳輸服務 **
Spark儲存體系——塊管理器BlockManager **
Spark MapOutputTracker淺析 **
shuffle服務與客戶端 **

能力有限,目前還是個學習者的姿態,所以只是記錄一下spark儲存模組原始碼的學習過程。在學習的過程中發現上面幾個是不錯的學習資料,推薦給大家,帶*號表示值得優先檢視學習的資料。

因為每個資料都各有側重點,所以可能在看的時候對一些沒有細講的類和架構不瞭解。下面我對上面的資料進行了總結,可以在學習的過程中對照的來看。

重要概念

  • BroadcastManager:廣播管理器,在SparkEnv中是直接初始化
  • TorrentBroadcast:廣播變數類
  • BroadcastFactory:廣播變數工廠類,是Spark中broadcast中所有實現的介面。SparkContext使用BroadcastFactory實現來為整個Spark job例項化特定的broadcast。它有唯一子類 -- TorrentBroadcastFactory。
  • TorrentBroadcastFactory:其newBroadcast方法例項化TorrentBroadcast類
  • SerializerManager: 它是為各種Spark元件配置序列化,壓縮和加密的元件,包括自動選擇用於shuffle的Serializer。spark中的資料在network IO 或 local disk IO傳輸過程中。都需要序列化。其預設的 Serializer 是 org.apache.spark.serializer.JavaSerializer,在一定條件下,可以使用kryo,即org.apache.spark.serializer.KryoSerializer。
  • BlockInfo:記錄了block 的相關資訊。
  • BlockData:BlockData只是定義了資料轉化的規範,並沒有涉及具體的儲存格式和讀寫流程,實現起來比較自由,所以前面說它是個鬆散的特徵。BlockData目前有3個實現類:基於記憶體和ChunkedByteBuffer的ByteBufferBlockData、基於磁碟和File的DiskBlockData,以及加密的EncryptedBlockData
  • DiskBlockData:主要用於將磁碟中的block檔案轉換為指定的流或物件。
  • EncryptedBlockData:主要是用於加密的block磁碟檔案轉換為特定的流或物件。
  • ByteBufferBlockData:主要用於記憶體中的block資料轉換為指定的流或物件。
  • BlockInfoManager:block在讀寫時有鎖機制,並且委託給BlockInfoManager來管理。雖然BlockInfoManager的字面意思是“塊資訊管理器”,但管理塊資訊的意圖並不明顯,管理塊的鎖才是真正主要的任務。這個類公開的鎖定介面是readers-writer鎖。每次獲取鎖都會自動與正在執行的任務關聯,並在任務完成或失敗時自動釋放鎖。這個類是執行緒安全的。
    • lockForReading():為一個塊加讀鎖
    • lockForWriting():為一個塊加寫鎖
    • unlock():釋放單個鎖
    • releaseAllLocksForTask():釋放當前TaskAttemptId對應的所有鎖,並返回所有塊ID的序列
    • downgradeLock():鎖降級
    • removeBlock():從infos對映中刪掉對應的BlockInfo,同時釋放它對應的所有鎖。
  • MemoryManager:管理Spark在JVM中的總體記憶體使用情況。該元件實現了跨任務劃分可用記憶體以及在儲存(記憶體使用快取和資料傳輸)和執行(計算使用的記憶體,如shuffle,連線,排序和聚合)之間分配記憶體的策略。Spark環境中的每個JVM例項都會持有一個MemoryManager
  • TaskMemoryManager:TaskMemoryManager管理由各個任務分配的記憶體。任務與TaskMemoryManager互動,永遠不會直接與JVM範圍的MemoryManager互動。
  • MemoryPool:MemoryPool抽象類從邏輯上非常鬆散地定義了Spark記憶體池的一些基本約定。它負責管理可調大小的記憶體區域的簿記工作。可以這樣理解,記憶體就是一個金庫,它是一個負責記賬的管家,主要負責記錄記憶體的借出歸還。這個類專門為MemoryManager而設計。給記憶體記賬,其實從本質上來說,它不是Spark記憶體管理部分的核心功能,但是又很重要,它的核心方法都是被MemoryManager來呼叫的。理解了這個類,其子類就比較好理解了。記賬的管家有兩種實現,分別是StorageMemoryPoolExecutionMemoryPool
  • StorageMemoryPool:通過記賬的方式,管理用於儲存的可調整大小的內純池。
    • acquireMemory():獲取N個位元組的記憶體給指定的block,如果有必要,即記憶體不夠用了,可以將其他的從記憶體中驅除。
    • releaseMemory():釋放記憶體
    • freeSpaceToShrinkPool():縮小此儲存記憶體池可用空間spaceToFree位元組的大小。這個方法是在收縮儲存記憶體池之前呼叫的,因為這個方法返回值是要收縮的值。收縮儲存記憶體池是為了擴大執行記憶體池,即這個方法是在收縮儲存記憶體,擴大執行記憶體時用的,這個方法只是為了縮小儲存記憶體池作準備的,並沒有真正的縮小儲存記憶體池。
  • ExecutionMemoryPool:實現策略和記帳,以便在任務之間共享大小可調的記憶體池,用於管理執行記憶體池。
    • memoryUsed():獲取總的任務記憶體使用大小
    • getMemoryUsageForTask():獲取某一任務記憶體使用大小
    • acquireMemory():給一個任務分配記憶體
    • releaseMemory():釋放當前任務指定大小的記憶體空間
    • releaseAllMemoryForTask():釋放當前任務已經使用的所有記憶體空間
  • MemoryManager:一種抽象記憶體管理器,用於管理Execution和Storage之間共享記憶體的方式。在這個上下文下,Execution記憶體是指用於在shuffle,join,sort和aggregation中進行計算的記憶體,而Storage記憶體是指用於在群集中快取和傳播內部資料的記憶體。 每個JVM都有一個MemoryManager。
    • maxOnHeapStorageMemory()
    • maxOffHeapStorageMemory():獲取儲存池最大使用記憶體,抽象方法,待子類實現
    • executionMemoryUsed()
    • storageMemoryUsed()
    • getExecutionMemoryUsageForTask():獲取已使用記憶體
    • acquireStorageMemory()
    • acquireExecutionMemory()
    • acquireUnrollMemory():獲取記憶體,抽象方法,待子類實現
    • releaseExecutionMemory()
    • releaseAllExecutionMemoryForTask()
    • releaseStorageMemory()
    • releaseAllStorageMemory()
    • releaseUnrollMemory():釋放記憶體,這些請求都委託給對應的MemoryPool來做。什麼是Unroll記憶體呢?RDD在被快取之前,它所佔用的記憶體空間是不連續的,而被快取到儲存記憶體之後,就以塊的形式來儲存,佔用連續的記憶體空間了。Unroll就是這個將RDD固化在連續記憶體空間的過程,中文一般翻譯為“展開”。Unroll過程使用的記憶體空間就是展開記憶體,它本質上是儲存記憶體中比較特殊的一部分。
  • StaticMemoryManager:spark 1.6 之前 使用MemoryManager子類 StaticMemoryManager 來做記憶體管理。靜態記憶體管理中的執行池和儲存池之間有嚴格的界限,兩個池的大小永不改變。1.6後如果想使用這個記憶體管理方式,設定 spark.memory.useLegacyMode 為 true即可(預設是false)。
  • UnifiedMemoryManager:spark1。6之後使用的預設類。這個MemoryManager保證了儲存池和執行池之間的軟邊界,即可以互相借用記憶體來滿足彼此動態的記憶體需求變化。執行和儲存的佔比由 spark.memory.storageFraction 配置,預設是0.6,即偏向於儲存池。其中儲存池的預設佔比是由 spark.memory.storageFraction 引數決定,預設是 0.5 ,即 儲存池預設佔比 = 0.6 * 0.5 = 0.3 ,即儲存池預設佔比為0.3。儲存池可以儘可能多的向執行池借用空閒記憶體。但是當執行池需要它的記憶體的時候,會把一部分記憶體池的記憶體物件從記憶體中驅逐出,直到滿足執行池的記憶體需求。類似地,執行池也可以儘可能地借用儲存池中的空閒記憶體,不同的是,執行記憶體不會被儲存池驅逐出記憶體,也就是說,快取block時可能會因為執行池佔用了大量的記憶體池不能釋放導致快取block失敗,在這種情況下,新的block會根據StorageLevel做相應處理。
  • MemoryEntry:本質上就是記憶體中一個block,指向了儲存在記憶體中的真實資料。或者說是塊在記憶體中的抽象表示。
  • MemoryStore:執行將Block儲存在記憶體中的類,可以是反序列化Java物件的陣列,也可以是由ByteBuffers序列化的。
    • putBytes():直接寫入資料。先從MemoryManager中申請記憶體,如果申請成功,則呼叫回撥方法 _bytes 獲取ChunkedByteBuffer資料,然後封裝成 SerializedMemoryEntry物件 ,最後將封裝好的SerializedMemoryEntry物件快取到 entries中。
    • putIteratorAsValues():把迭代器中值儲存為記憶體中的Java物件
    • putIteratorAsBytes():把迭代器中值儲存為記憶體中的序列化位元組資料。所謂迭代器化的資料,就是指用Iterator[T]形式表示的塊資料。之所以會這樣表示,是因為有時單個塊對應的資料可能過大,不能一次性存入記憶體。為了避免造成OOM,就可以一邊遍歷迭代器,一邊週期性地寫記憶體,並檢查記憶體是否夠用,就像翻書一樣。“展開”(Unroll)這個詞形象地說明了該過程
    • reserveUnrollMemoryForThisTask():申請展開記憶體的方法。思路大致上是先從MemoryManager 申請攤開記憶體,若成功,則根據memoryMode在堆內或堆外記錄攤開記憶體的map上記錄新分配的記憶體。
    • releaseUnrollMemoryForThisTask():釋放展開記憶體的方法。先根據memoryMode獲取到對應記錄堆內或堆外記憶體的使用情況的map,然後在該task的攤開記憶體上減去這筆記憶體開銷,如果減完之後,task使用記憶體為0,則直接從map中移除對該task的記憶體記錄。
    • evictBlocksToFreeSpace():嘗試驅逐block來釋放指定大小的記憶體空間來儲存給定的block,用途為淘汰現有的一些塊,為新的塊騰出空間。
  • DiskBlockManager:負責維護塊資料與其在磁碟上儲存位置的關係,是用來建立並維護邏輯block和落地後的block檔案的對映關係的,它還負責建立用於shuffle或本地的臨時檔案。
    • createLocalDirs():建立本地儲存目錄
    • getFile():建立子目錄及建立File物件
    • getAllFiles():獲取所有檔案
    • getAllBlocks():獲取所有塊ID
    • createTempLocalBlock()
    • createTempShuffleBlock():用來建立Spark計算過程中的中間結果以及Shuffle Write階段輸出的儲存檔案。它們的塊ID分別用TempLocalBlockId和TempShuffleBlockId來表示。
    • DiskBlockManager.addShutdownHook()/doStop():繫結關閉鉤子與關閉。如果deleteFilesOnStop標記為真,則在DiskBlockManager關閉之前,會呼叫Utils.deleteRecursively()方法遞迴地刪掉本地儲存目錄。deleteFilesOnStop 通過構造方法傳入
  • DiskStore:真正負責磁碟儲存的元件,用來儲存block 到磁碟的。
    • putBytes():寫入位元組,將資料寫入到磁碟中。
    • getBytes():讀取位元組,getBytes獲取的是BlockData資料,注意現在只是返回檔案的引用,檔案的內容並沒有返回。
  • BlockManagerMaster:BlockManagerMaster 這個類是對 driver的 EndpointRef 的包裝,可以說是 driver EndpointRef的一個代理類,主要負責和driver的互動,來獲取跟底層儲存相關的資訊。BlockManager是典型的主從架構設計,不管Driver還是Executor上都要有BlockManager例項,那麼必然就得存在一個協調元件——Spark中就是BlockManagerMaster了。BlockManagerMaster服務取名為Master其實是一個挺迷糊的名稱;雖然它是Master,但是該物件並不是BlockManager的分散式服務的Master節點;而只是對Master節點一個連線符, 通過該連線符,從而已可以和真正的Master節點進行通訊;不管是在Driver還是在Executor上,都有一個BlockManagerMaster.真正的Master節點是BlockManagerMasterEndpoint這個物件。
    • removeExecutor()
    • removeExecutorAsync():移除executor,有同步和非同步兩種方案,這兩個方法只會在driver端使用。
    • registerBlockManager():向driver註冊blockmanager
  • BlockManagerMasterEndpoint:其在以前的版本中也叫BlockManageMasterActor。BlockManageMasterEndpoint只存在於Driver上,Executor在BlockManageMaster中獲取BlockManageMasterEndpoint的引用,並向其傳送訊息(使用ask、askSync、tell),實現和Driver的互動。
    • receiveAndReply():接受並回復RPC訊息,通過覆寫RpcEndpoint.receiveAndReply()方法來實現。
    • register():處理BlockManager註冊
    • heartbeatReceived():處理BlockManager心跳
  • BlockManagerSlaveEndpoint:SlaveEndpoint配合BlockManage執行一些來自於driver和executor的要求操作(通過BlockManageMaster),其主體函式同樣是receiveAndReply,不過內部執行操作的選項較少,主要包括去除Block、RDD、Broadcast,獲取資訊等操作,所有匹配後的具體操作都是通過相應的具體類(如BlockManage、shuffleManager等)完成。
    • receiveAndReply():接受並回復RPC訊息,通過覆寫RpcEndpoint.receiveAndReply()方法來實現。
  • BlockManager:塊管理器BlockManager會執行在Spark叢集中的所有節點上。每個節點上的BlockManager通過MemoryManager、MemoryStore、DiskBlockManager、DiskStore來管理其記憶體、磁碟中的塊,並與其他節點進行塊的互動,是一個規模龐大的元件。
    • initialize():1. 初始化BlockTransferService和ShuffleClient。2. 根據配置項spark.storage.replication.policy確定塊複製策略並通過反射建立。預設值為RandomBlockReplicationPolicy,說明是將塊的副本隨機放到不同的節點上。3. 根據Executor ID生成BlockManagerId,並呼叫BlockManagerMaster.registerBlockManager()方法註冊此ID與從RPC端點。註冊成功後,BlockManagerMaster會返回另一個正式的ID。4. 生成Shuffle服務的ID。如果當前節點是Executor並啟用了外部Shuffle服務的話,就呼叫registerWithExternalShuffleServer()方法註冊外部Shuffle服務
    • getOrElseUpdate():用於獲取Block。如果Block存在,則獲取此Block並返回BlockResult,否則呼叫makeIterator方法計算Block,並持久化後返回BlockResult或Iterator
    • reregister():重新向driver註冊blockManager方法
    • get():在getOrElseUpdate方法中被呼叫,該方法先呼叫getLocalValues()方法從本地(注意是本地Executor)讀取資料,如果讀取不到,就繼續呼叫getRemoteValues()方法從遠端獲取資料。
    • getLocalBytes():用於儲存體系獲取BlockId所對應Block的資料,並封裝為ChunkedByteBuffer後返回
    • getBlockData():用於獲取本地Block的資料。
    • putBlockData():用於將Block資料寫入本地
    • getLocalValues():用於從本地的BlockManager中獲取Block資料
    • 該類方法非常繁多,可檢視Spark儲存體系——塊管理器BlockManager文章,主要概括為讀取序列化資料,讀取物件資料,寫入序列化,寫入物件,從本地讀取,從遠端讀取等。
  • MapOutputTracker: MapOutputTracker 是一個定位跟蹤 stage 的map 輸出位置的類,driver 和 executor 有對應的實現,分別是 MapOutputTrackerMaster 和 MapOutputTrackerWorker。
  • ShuffleClient:不僅是將shuffle檔案上傳到其他Executor或者下載遠端Executor檔案到本地的客戶端,也是提供可以被其他Executor訪問的shuffle服務。
  • BlockTransferService:是繼承自ShuffleClient介面的抽象類,負責資料的傳輸。其中定義了blocks的批量獲取、單個獲取和單個同步或非同步上傳的介面。
    • init():初始化,在BlockManager的initialize()方法中被呼叫
    • uploadBlock():上傳單個block塊到遠端節點,僅在[[init]]之後才可使用。
    • fetchBlocks():從遠端節點非同步獲取一組blocks。注意:該API介面接收陣列介面,所以可以實現批量請求。另外,該方法沒有返回一個future物件,所以子類實現可以在一個block獲取成功後立即回撥onBlockFetchSuccess,而不是等待所有的blocks都獲取成功。
    • uploadBlockSync():上傳單個block塊到遠端節點,僅在[[init]]呼叫之後才可用。該方法類似於[[uploadBlock]]方法,除了該方法會阻塞執行緒直到block上傳完成,也就是同步上傳。
    • fetchBlockSync():一個特殊的[[fetchBlocks]]的例子,它阻塞式地讀取一個block塊,也就是同步讀取。只有在呼叫[[init]]後才可以使用它。
  • NettyBlockTransferService:BlockTransferService的實現類實現為NettyBlockTransferService,它使用Netty非同步時間驅動的網路應用框架,獲取和上傳遠端節點上的Block集合。

重要圖例

廣播資料的讀取流程圖

Spark 儲存模組原始碼學習

TaskMemoryManager被建立和使用流程圖

Spark 儲存模組原始碼學習

靜態記憶體管理StaticMemoryManager佈局圖解

Spark 儲存模組原始碼學習

統一記憶體管理UnifiedMemoryManager佈局圖解

Spark 儲存模組原始碼學習

BlockManagerMaster與RPC端點間的關係

Spark 儲存模組原始碼學習
BlockManagerMaster的名字有些許誤導性:實際上在每個節點都會有一個BlockManagerMaster,而不是Driver上有BlockManagerMaster,Executor上有BlockManagerSlave(當然它是不存在的)。BlockManager的主從則是靠RPC端點體系來體現的。之所以叫這個名字,可能是為了避免出現“塊管理器管理器”(BlockManagerManager)這樣更奇怪的名字吧。

BlockManager的讀寫流程(不含BlockTransferService)

Spark 儲存模組原始碼學習

相關文章