Spark RDD惰性計算的自主優化

原創/朱季謙

RDD(彈性分佈式數據集)中的數據就如final定義一般,只可讀而無法修改,若要對RDD進行轉換或操作,那就需要創建一個新的RDD來保存結果。故而就需要用到轉換和行動的算子。

Spark運行是惰性的,在RDD轉換階段,只會記錄該轉換邏輯而不會執行,只有在遇到行動算子時,纔會觸發真正的運算,若整個生命週期都沒有行動算子,那麼RDD的轉換代碼便不會運行。

這樣的惰性計算,其實是有好處的,它在遇到行動算子需要對整個DAG(有向無環圖)做優化,以下是一些優化說明——

本文的樣本部分內容如下,可以基於這些數據做驗證——

Amy Harris,39,男,18561,性價比,家居用品,天貓,微信支付,10,折扣優惠,品牌忠誠
Lori Willis,33,女,14071,功能性,家居用品,蘇寧易購,貨到付款,1,折扣優惠,日常使用
Jim Williams,61,男,14145,時尚潮流,汽車配件,淘寶,微信支付,3,免費贈品,禮物贈送
Anthony Perez,19,女,11587,時尚潮流,珠寶首飾,拼多多,支付寶,5,免費贈品,商品推薦
Allison Carroll,28,男,18292,環保可持續,美妝護膚,唯品會,信用卡,8,免費贈品,日常使用
Robert Rice,47,男,5347,時尚潮流,圖書音像,拼多多,微信支付,8,有優惠券,興趣愛好
Jason Bradley,25,男,9480,性價比,汽車配件,拼多多,信用卡,5,折扣優惠,促銷打折
Joel Small,18,女,15586,社交影響,食品飲料,亞馬遜,支付寶,5,無優惠券,日常使用
Stephanie Austin,33,男,7653,舒適度,汽車配件,亞馬遜,銀聯支付,3,無優惠券,跟風購買
Kathy Myers,33,男,18159,舒適度,美妝護膚,亞馬遜,貨到付款,4,無優惠券,商品推薦
Gabrielle Mccarty,57,男,19561,環保可持續,母嬰用品,網易考拉,支付寶,5,免費贈品,日常使用
Joan Smith,43,女,11896,品牌追求,圖書音像,亞馬遜,支付寶,4,免費贈品,商品推薦
Monica Garcia,19,男,16665,時尚潮流,電子產品,京東,貨到付款,7,免費贈品,商品推薦
Christopher Faulkner,55,男,3621,社交影響,美妝護膚,蘇寧易購,支付寶,7,無優惠券,日常使用

一、減少不必要的計算

RDD的惰性計算可以通過優化執行計劃去避免不必要的計算,同時可以將過濾操作下推到數據源或者其他轉換操作之前,減少需要處理的數據量,進而達到計算的優化。

例如,執行以下這段spark代碼時,

  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setMaster("local[*]").setAppName("count")
    val ss = SparkSession.builder().config(conf).getOrCreate()
    val filePath: String = "transaction_data.csv"
    val lineRDD = ss.sparkContext.textFile(filePath)
    val value = lineRDD.map { x => {
      println(s"打印 $x")
      x.split(",")
    } }
    value.take(10).foreach(println)
    ss.stop()
  }

若Spark不是惰性計算的情況下,代碼順序運行到這行 val lineRDD = ss.sparkContext.textFile(filePath)代碼時,就會將transaction_data.csv文件裏的幾萬條數據全部加載出來,然後再做計算。

而在惰性計算的情況下,直至運行這行代碼 value.take(10).foreach(println)而遇到foreach這個行動算子時,纔會去執行前面的轉換,這時它會基於RDD的轉化自行做一個優化——在這個例子裏,它會基於lineRDD.take(5)這行代碼只會從transaction_data.csv取出前5行,避免了將文件裏的幾萬條數據全部取出。

打印結果如下,發現lineRDD.map確實只處理了前5條數據——

打印 Amy Harris,39,男,18561,性價比,家居用品,天貓,微信支付,10,折扣優惠,品牌忠誠
打印 Lori Willis,33,女,14071,功能性,家居用品,蘇寧易購,貨到付款,1,折扣優惠,日常使用
打印 Jim Williams,61,男,14145,時尚潮流,汽車配件,淘寶,微信支付,3,免費贈品,禮物贈送
打印 Anthony Perez,19,女,11587,時尚潮流,珠寶首飾,拼多多,支付寶,5,免費贈品,商品推薦
打印 Allison Carroll,28,男,18292,環保可持續,美妝護膚,唯品會,信用卡,8,免費贈品,日常使用
[Ljava.lang.String;@3c87e6b7
[Ljava.lang.String;@77bbadc
[Ljava.lang.String;@3c3a0032
[Ljava.lang.String;@7ceb4478
[Ljava.lang.String;@7fdab70c

二、操作合併和優化

Spark在執行行動算子時,會自動將存在連續轉換的RDD操作合併到更爲高效的執行計劃,這樣可以減少中間不是必要的RDD數據的生成和傳輸,可以整體提高計算的效率。這很像是,擺在你面前是一條彎彎曲曲的道路,但是因爲你手裏有地圖,知道這條路是怎麼走的,因此,可以基於這樣的地圖,去嘗試發掘下是否有更好的直徑。

還是以一個代碼案例說明,假如需要統計薪資在10000以上的人數。

運行的代碼,是從transaction_data.csv讀取了幾萬條數據,然後將每行數據按","分割成數組,再基於每個數組去過濾出滿足薪資大於10000的數據,最後再做count統計出滿足條件的人數。

以下是最冗餘的代碼,每個步驟都轉換生成一個新的RDD,彼此之間是連續的,這些RDD是會佔內存空間,同時增加了很多不必要的計算。

def main(args: Array[String]): Unit = {
  val conf = new SparkConf().setMaster("local[*]").setAppName("count")
  val ss = SparkSession.builder().config(conf).getOrCreate()
  val filePath: String = "transaction_data.csv"
  val lineRDD = ss.sparkContext.textFile(filePath)
  val array = lineRDD.map(_.split(","))
  //過濾出薪資10000的數據
  val valueRdd = array.filter(x => x.apply(3).toInt > 10000)
  //統計薪資10000以上的人數
  val count = valueRdd.count()
  ss.stop()
}

Spark就可能會將這些存在連續的RDD進行優化,將其合併成一個單獨的轉換操作,直接就對原始RDD進行映射和過濾——

val value = ss.sparkContext.textFile(filePath).map(_.split(",")).filter(x =>{x.apply(3).toInt > 10000})
value.count()

這樣優化同時避免了多次循環遍歷,每個映射的數組只需要遍歷一次即可。

可以通過coalesce(1)只設置一個分區,使代碼串行運行,然後增加打印驗證一下效果——

val value = ss.sparkContext.textFile(filePath).coalesce(1).map(x =>{
  println(s"分割打印 $x")
  x.split(",")
}).filter(x =>
  {
    println(s"過濾打印 ${x.apply(0)}")
    x.apply(3).toInt > 10000
  }
 )
value.count()

打印部分結果,發現沒每遍歷一次,就把映射數組和過濾都完成了,沒有像先前多個RDD那樣需要每次都得遍歷,這樣就能達到一定優化效果——

分割打印 Amy Harris,39,男,18561,性價比,家居用品,天貓,微信支付,10,折扣優惠,品牌忠誠
過濾打印 Amy Harris
分割打印 Lori Willis,33,女,14071,功能性,家居用品,蘇寧易購,貨到付款,1,折扣優惠,日常使用
過濾打印 Lori Willis
分割打印 Jim Williams,61,男,14145,時尚潮流,汽車配件,淘寶,微信支付,3,免費贈品,禮物贈送
過濾打印 Jim Williams
分割打印 Anthony Perez,19,女,11587,時尚潮流,珠寶首飾,拼多多,支付寶,5,免費贈品,商品推薦
過濾打印 Anthony Perez
分割打印 Allison Carroll,28,男,18292,環保可持續,美妝護膚,唯品會,信用卡,8,免費贈品,日常使用
過濾打印 Allison Carroll
分割打印 Robert Rice,47,男,5347,時尚潮流,圖書音像,拼多多,微信支付,8,有優惠券,興趣愛好
過濾打印 Robert Rice

這樣也提醒了我們,在遇到連續轉換的RDD時,其實可以自行做代碼優化,避免產生中間可優化的RDD和遍歷操作。

三、窄依賴優化

RDD在執行惰性計算時,會盡可能進行窄依賴優化。

有窄依賴,便會有寬依賴,兩者有什麼區別呢?

窄依賴指的是父RDD的每個分區只需要通過簡單的轉換操作就可以計算出對應的子RDD分區,不涉及跨多個分區的數據交換,即父子之間每個分區都是一對一的。

前文提到的map、filter等轉換都屬於窄依賴的操作。

例如,array.filter(x => x.apply(3).toInt > 10000),父RDD有三個分區,那麼三個分區就會分別執行array.filter(x => x.apply(3).toInt > 10000)將過濾的數據傳給子RDD對應的分區——
image

寬依賴指父RDD的每個分區會通過跨區計算將原本同一個分區數據分發到不同子分區上,這中間涉及到shuffle重新洗牌操作,會存在較大的計算,父子之間分區是一對多的。可以看到,父RDD同一個分區的數據,在寬依賴情況下,會將相同的key傳輸到同一個分區裏,這就意味着,同一個父RDD,如果存在多個不同的key,可能會分發到多個不同的子分區上,進而出現shuffle重新洗牌操作。

image

因此,RDD會盡可能的進行窄依賴優化,在無需跨區計算的情況下,就避免進行shuffle重新洗牌操作,將父分區一對一地傳輸給子分區。同時,窄依賴還有一個好處是,在子分區出現丟失數據異常時,只需要重新計算對應的父分區數據即可,無需將父分區全部數據進行計算。

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