Spark的Shuffle過程介紹

Shuffle Writer

Spark豐富了任務類型,有些任務之間數據流轉不需要通過Shuffle,但是有些任務之間還是需要通過Shuffle來傳遞數據,比如wide dependency的group by key。

Spark中需要Shuffle輸出的Map任務會爲每個Reduce創建對應的bucket,Map產生的結果會根據設置的partitioner得到對應的bucketId,然後填充到相應的bucket中去。每個Map的輸出結果可能包含所有的Reduce所需要的數據,所以每個Map會創建R個bucket(R是reduce的個數),M個Map總共會創建M*R個bucket。

Map創建的bucket其實對應磁盤上的一個文件,Map的結果寫到每個bucket中其實就是寫到那個磁盤文件中,這個文件也被稱爲blockFile,是Disk Block Manager管理器通過文件名的Hash值對應到本地目錄的子目錄中創建的。每個Map要在節點上創建R個磁盤文件用於結果輸出,Map的結果是直接輸出到磁盤文件上的,100KB的內存緩衝是用來創建Fast Buffered OutputStream輸出流。這種方式一個問題就是Shuffle文件過多。

針對上述Shuffle過程產生的文件過多問題,Spark有另外一種改進的Shuffle過程:consolidation Shuffle,以期顯著減少Shuffle文件的數量。在consolidation Shuffle中每個bucket並非對應一個文件,而是對應文件中的一個segment部分。Job的map在某個節點上第一次執行,爲每個reduce創建bucket對應的輸出文件,把這些文件組織成ShuffleFileGroup,當這次map執行完之後,這個ShuffleFileGroup可以釋放爲下次循環利用;當又有map在這個節點上執行時,不需要創建新的bucket文件,而是在上次的ShuffleFileGroup中取得已經創建的文件繼續追加寫一個segment;當前次map還沒執行完,ShuffleFileGroup還沒有釋放,這時如果有新的map在這個節點上執行,無法循環利用這個ShuffleFileGroup,而是隻能創建新的bucket文件組成新的ShuffleFileGroup來寫輸出。

比如一個Job有3個Map和2個reduce:(1) 如果此時集羣有3個節點有空槽,每個節點空閒了一個core,則3個Map會調度到這3個節點上執行,每個Map都會創建2個Shuffle文件,總共創建6個Shuffle文件;(2) 如果此時集羣有2個節點有空槽,每個節點空閒了一個core,則2個Map先調度到這2個節點上執行,每個Map都會創建2個Shuffle文件,然後其中一個節點執行完Map之後又調度執行另一個Map,則這個Map不會創建新的Shuffle文件,而是把結果輸出追加到之前Map創建的Shuffle文件中;總共創建4個Shuffle文件;(3) 如果此時集羣有2個節點有空槽,一個節點有2個空core一個節點有1個空core,則一個節點調度2個Map一個節點調度1個Map,調度2個Map的節點上,一個Map創建了Shuffle文件,後面的Map還是會創建新的Shuffle文件,因爲上一個Map還正在寫,它創建的ShuffleFileGroup還沒有釋放;總共創建6個Shuffle文件。

Shuffle Fetcher

 

Reduce去拖Map的輸出數據,Spark提供了兩套不同的拉取數據框架:通過socket連接去取數據;使用netty框架去取數據。

每個節點的Executor會創建一個BlockManager,其中會創建一個BlockManagerWorker用於響應請求。當Reduce的GET_BLOCK的請求過來時,讀取本地文件將這個blockId的數據返回給Reduce。如果使用的是Netty框架,BlockManager會創建ShuffleSender用於發送Shuffle數據。

並不是所有的數據都是通過網絡讀取,對於在本節點的Map數據,Reduce直接去磁盤上讀取而不再通過網絡框架。

Reduce拖過來數據之後以什麼方式存儲呢?Spark Map輸出的數據沒有經過排序,Spark Shuffle過來的數據也不會進行排序,Spark認爲Shuffle過程中的排序不是必須的,並不是所有類型的Reduce需要的數據都需要排序,強制地進行排序只會增加Shuffle的負擔。Reduce拖過來的數據會放在一個HashMap中,HashMap中存儲的也是<key, value>對,key是Map輸出的key,Map輸出對應這個key的所有value組成HashMap的value。Spark將Shuffle取過來的每一個<key, value>對插入或者更新到HashMap中,來一個處理一個。HashMap全部放在內存中。

Shuffle取過來的數據全部存放在內存中,對於數據量比較小或者已經在Map端做過合併處理的Shuffle數據,佔用內存空間不會太大,但是對於比如group by key這樣的操作,Reduce需要得到key對應的所有value,並將這些value組一個數組放在內存中,這樣當數據量較大時,就需要較多內存。

當內存不夠時,要不就失敗,要不就用老辦法把內存中的數據移到磁盤上放着。Spark意識到在處理數據規模遠遠大於內存空間時所帶來的不足,引入了一個具有外部排序的方案。Shuffle過來的數據先放在內存中,當內存中存儲的<key, value>對超過1000並且內存使用超過70%時,判斷節點上可用內存如果還足夠,則把內存緩衝區大小翻倍,如果可用內存不再夠了,則把內存中的<key, value>對排序然後寫到磁盤文件中。最後把內存緩衝區中的數據排序之後和那些磁盤文件組成一個最小堆,每次從最小堆中讀取最小的數據,這個和MapReduce中的merge過程類似。

 

MapReduce和Spark的Shuffle過程對比

  MapReduce Spark
collect 在內存中構造了一塊數據結構用於map輸出的緩衝 沒有在內存中構造一塊數據結構用於map輸出的緩衝,而是直接把輸出寫到磁盤文件
sort map輸出的數據有排序 map輸出的數據沒有排序
merge 對磁盤上的多個spill文件最後進行合併成一個輸出文件 在map端沒有merge過程,在輸出時直接是對應一個reduce的數據寫到一個文件中,這些文件同時存在併發寫,最後不需要合併成一個
copy框架 jetty netty或者直接socket流
對於本節點上的文件 仍然是通過網絡框架拖取數據

不通過網絡框架,對於在本節點上的map輸出文件,採用本地讀取的方式

copy過來的數據存放位置 先放在內存,內存放不下時寫到磁盤

一種方式全部放在內存;

另一種方式先放在內存
merge sort 最後會對磁盤文件和內存中的數據進行合併排序 對於採用另一種方式時也會有合併排序的過程

Shuffle後續優化方向

通過上面的介紹,我們瞭解到,Shuffle過程的主要存儲介質是磁盤,儘量的減少IO是Shuffle的主要優化方向。我們腦海中都有那個經典的存儲金字塔體系,Shuffle過程爲什麼把結果都放在磁盤上,那是因爲現在內存再大也大不過磁盤,內存就那麼大,還這麼多張嘴吃,當然是分配給最需要的了。如果具有“土豪”內存節點,減少Shuffle IO的最有效方式無疑是儘量把數據放在內存中。下面列舉一些現在看可以優化的方面,期待經過我們不斷的努力,TDW計算引擎運行地更好。

MapReduce Shuffle後續優化方向

 

  • 壓縮:對數據進行壓縮,減少寫讀數據量;
  • 減少不必要的排序:並不是所有類型的Reduce需要的數據都是需要排序的,排序這個nb的過程如果不需要最好還是不要的好;
  • 內存化:Shuffle的數據不放在磁盤而是儘量放在內存中,除非逼不得已往磁盤上放;當然瞭如果有性能和內存相當的第三方存儲系統,那放在第三方存儲系統上也是很好的;這個是個大招;
  • 網絡框架:netty的性能據說要佔優了;
  • 本節點上的數據不走網絡框架:對於本節點上的Map輸出,Reduce直接去讀吧,不需要繞道網絡框架。

 

Spark Shuffle後續優化方向

Spark作爲MapReduce的進階架構,對於Shuffle過程已經是優化了的,特別是對於那些具有爭議的步驟已經做了優化,但是Spark的Shuffle對於我們來說在一些方面還是需要優化的。

 

  • 壓縮:對數據進行壓縮,減少寫讀數據量;
  • 內存化:Spark歷史版本中是有這樣設計的:Map寫數據先把數據全部寫到內存中,寫完之後再把數據刷到磁盤上;考慮內存是緊缺資源,後來修改成把數據直接寫到磁盤了;對於具有較大內存的集羣來講,還是儘量地往內存上寫吧,內存放不下了再放磁盤。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章