Spark中的3中Join
Broadcast Join 大家知道,在資料庫的常見模型中(比如星型模型或者雪花模型),表一般分為兩種:事實表和維度表。維度表一般指固定的、變動較少的表,例如聯絡人、物品種類等,一般資料有限。而事實表一般記錄流水,比如銷售清單等,通常隨著時間的增長不斷膨脹。 因為Join操作是對兩個表中key值相同的記錄進行連線,在SparkSQL中,對兩個表做Join最直接的方式是先根據key分割槽,再在每個分割槽中把key值相同的記錄拿出來做連線操作。但這樣就不可避免地涉及到shuffle,而shuffle在Spark中是比較耗時的操作,我們應該儘可能的設計Spark應用使其避免大量的shuffle。 當維度表和事實表進行Join操作時,為了避免shuffle,我們可以將大小有限的維度表的全部資料分發到每個節點上,供事實表使用。executor儲存維度表的全部資料,一定程度上犧牲了空間,換取shuffle操作大量的耗時,這在SparkSQL中稱作Broadcast Join,如下圖所示:
Table B是較小的表,黑色表示將其廣播到每個executor節點上,Table A的每個partition會通過block manager取到Table A的資料。根據每條記錄的Join Key取到Table B中相對應的記錄,根據Join Type進行操作。這個過程比較簡單,不做贅述。 Broadcast Join的條件有以下幾個:
- 被廣播的表需要小於spark.sql.autoBroadcastJoinThreshold所配置的值,預設是10M (或者加了broadcast join的hint)
- 基表不能被廣播,比如left outer join時,只能廣播右表 看起來廣播是一個比較理想的方案,但它有沒有缺點呢?也很明顯。這個方案只能用於廣播較小的表,否則資料的冗餘傳輸就遠大於shuffle的開銷;另外,廣播時需要將被廣播的表現collect到driver端,當頻繁有廣播出現時,對driver的記憶體也是一個考驗。
Shuffle Hash Join 當一側的表比較小時,我們選擇將其廣播出去以避免shuffle,提高效能。但因為被廣播的表首先被collect到driver段,然後被冗餘分發到每個executor上,所以當表比較大時,採用broadcast join會對driver端和executor端造成較大的壓力。 但由於Spark是一個分散式的計算引擎,可以通過分割槽的形式將大批量的資料劃分成n份較小的資料集進行平行計算。這種思想應用到Join上便是Shuffle Hash Join了。利用key相同必然分割槽相同的這個原理,SparkSQL將較大表的join分而治之,先將表劃分成n個分割槽,再對兩個表中相對應分割槽的資料分別進行Hash Join,這樣即在一定程度上減少了driver廣播一側表的壓力,也減少了executor端取整張被廣播表的記憶體消耗。其原理如下圖:
Shuffle Hash Join分為兩步:
- 對兩張表分別按照join keys進行重分割槽,即shuffle,目的是為了讓有相同join keys值的記錄分到對應的分割槽中
- 對對應分割槽中的資料進行join,此處先將小表分割槽構造為一張hash表,然後根據大表分割槽中記錄的join keys值拿出來進行匹配 Shuffle Hash Join的條件有以下幾個:
- 分割槽的平均大小不超過spark.sql.autoBroadcastJoinThreshold所配置的值,預設是10M
- 基表不能被廣播,比如left outer join時,只能廣播右表
- 一側的表要明顯小於另外一側,小的一側將被廣播(明顯小於的定義為3倍小,此處為經驗值) 我們可以看到,在一定大小的表中,SparkSQL從時空結合的角度來看,將兩個表進行重新分割槽,並且對小表中的分割槽進行hash化,從而完成join。在保持一定複雜度的基礎上,儘量減少driver和executor的記憶體壓力,提升了計算時的穩定性。 Sort Merge Join 上面介紹的兩種實現對於一定大小的表比較適用,但當兩個表都非常大時,顯然無論適用哪種都會對計算記憶體造成很大壓力。這是因為join時兩者採取的都是hash join,是將一側的資料完全載入到記憶體中,使用hash code取join keys值相等的記錄進行連線。 當兩個表都非常大時,SparkSQL採用了一種全新的方案來對錶進行Join,即Sort Merge Join。這種實現方式不用將一側資料全部載入後再進星hash join,但需要在join前將資料排序,如下圖所示:
可以看到,首先將兩張表按照join keys進行了重新shuffle,保證join keys值相同的記錄會被分在相應的分割槽。分割槽後對每個分割槽內的資料進行排序,排序後再對相應的分割槽內的記錄進行連線,如下圖示:
看著很眼熟吧?也很簡單,因為兩個序列都是有序的,從頭遍歷,碰到key相同的就輸出;如果不同,左邊小就繼續取左邊,反之取右邊。 可以看出,無論分割槽有多大,Sort Merge Join都不用把某一側的資料全部載入到記憶體中,而是即用即取即丟,從而大大提升了大資料量下sql join的穩定性。