Apache Spark Day3

caijq_newid發表於2020-10-30

Apache Spark Day3

RDD進階(面試)

分析WordCount

sc.textFile("hdfs:///words/t_word") //RDD0
   .flatMap(_.split(" "))                //RDD1
   .map((_,1))                           //RDD2
   .reduceByKey(_+_)                     //RDD3  finalRDD
   .collect                              //Array 任務提交

在這裡插入圖片描述

RDD都有哪些特性?

* Internally, each RDD is characterized by five main properties:
*
*  - A list of partitions
*  - A function for computing each split
*  - A list of dependencies on other RDDs
*  - Optionally, a Partitioner for key-value RDDs (e.g. to say that the RDD is hash-partitioned)
*  - Optionally, a list of preferred locations to compute each split on (e.g. block locations for
*    an HDFS file)
*
  • RDD只讀的具有分割槽分散式資料集-分割槽數等於該RDD並行度
  • 每個分割槽獨立運算,儘可能實現分割槽本地性計算
  • 只讀的資料集且RDD與RDD之間存在著相互依賴關係
  • 針對於 key-value RDD,可以指定分割槽策略【可選】
  • 基於資料所屬的位置,選擇最優位置實現本地性計算【可選】

RDD容錯

在理解DAGSchedule如何做狀態劃分的前提是需要大家瞭解一個專業術語lineage通常被人們稱為RDD的血統。在瞭解什麼是RDD的血統之前,先來看看程式猿進化過程。

程式猿進化過程

上圖中描述了一個程式猿起源變化的過程,我們可以近似的理解類似於RDD的轉換也是一樣的,Spark的計算本質就是對RDD做各種轉換,因為RDD是一個不可變只讀的集合,因此每次的轉換都需要上一次的RDD作為本次轉換的輸入,因此RDD的lineage描述的是RDD間的相互依賴關係。為了保證RDD中資料的健壯性,RDD資料集通過所謂的血統關係(Lineage)記住了它是如何從其它RDD中轉換過來的。Spark將RDD之間的關係歸類為寬依賴窄依賴。Spark會根據Lineage儲存的RDD的依賴關係對RDD計算做故障容錯。目前Saprk的容錯策略根據RDD依賴關係重新計算-無需干預RDD做Cache-臨時快取RDD做Checkpoint-持久化手段完成RDD計算的故障容錯。

RDD快取

快取是一種RDD計算容錯的一種手段,程式在RDD資料丟失的時候,可以通過快取快速計算當前RDD的值,而不需要反推出所有的RDD重新計算,因此Spark在需要對某個RDD多次使用的時候,為了提高程式的執行效率使用者可以考慮使用RDD的cache。

scala> var finalRDD=sc.textFile("hdfs:///words/src").flatMap(_.split(" ")).map((_,1)).reduceByKey(_+_)
finalRDD: org.apache.spark.rdd.RDD[(String, Int)] = ShuffledRDD[25] at reduceByKey at <console>:24

scala> finalRDD.cache
res7: org.apache.spark.rdd.RDD[(String, Int)] = ShuffledRDD[25] at reduceByKey at <console>:24

scala> finalRDD.collect
res8: Array[(String, Int)] = Array((this,1), (is,1), (day,2), (come,1), (hello,1), (baby,1), (up,1), (spark,1), (a,1), (on,1), (demo,1), (good,2), (study,1))

scala> finalRDD.collect
res9: Array[(String, Int)] = Array((this,1), (is,1), (day,2), (come,1), (hello,1), (baby,1), (up,1), (spark,1), (a,1), (on,1), (demo,1), (good,2), (study,1))

使用者可以呼叫upersist方法清空快取

scala> finalRDD.unpersist()
res11: org.apache.spark.rdd.RDD[(String, Int)] @scala.reflect.internal.annotations.uncheckedBounds = ShuffledRDD[25] at reduceByKey at <console>:24

除了呼叫cache之外,Spark提供了更細粒度的RDD快取方案,使用者可以根據叢集的記憶體狀態選擇合適的快取策略。使用者可以使用persist方法指定快取級別。

RDD#persist(StorageLevel.MEMORY_ONLY)

目前Spark支援的快取方案如下:

object StorageLevel {
  val NONE = new StorageLevel(false, false, false, false)
  val DISK_ONLY = new StorageLevel(true, false, false, false)# 僅僅儲存磁碟
  val DISK_ONLY_2 = new StorageLevel(true, false, false, false, 2) # 僅僅儲存磁碟 儲存兩份
  val MEMORY_ONLY = new StorageLevel(false, true, false, true)
  val MEMORY_ONLY_2 = new StorageLevel(false, true, false, true, 2)
  val MEMORY_ONLY_SER = new StorageLevel(false, true, false, false) # 先序列化再 儲存記憶體,費CPU節省記憶體
  val MEMORY_ONLY_SER_2 = new StorageLevel(false, true, false, false, 2)
  val MEMORY_AND_DISK = new StorageLevel(true, true, false, true) # 選擇這個!
  val MEMORY_AND_DISK_2 = new StorageLevel(true, true, false, true, 2)
  val MEMORY_AND_DISK_SER = new StorageLevel(true, true, false, false)
  val MEMORY_AND_DISK_SER_2 = new StorageLevel(true, true, false, false, 2)
  val OFF_HEAP = new StorageLevel(true, true, true, false, 1)
...

那如何選擇呢?

預設情況下,效能最高的當然是MEMORY_ONLY,但前提是你的記憶體必須足夠足夠大,可以綽綽有餘地存放下整個RDD的所有資料。因為不進行序列化與反序列化操作,就避免了這部分的效能開銷;對這個RDD的後續運算元操作,都是基於純記憶體中的資料的操作,不需要從磁碟檔案中讀取資料,效能也很高;而且不需要複製一份資料副本,並遠端傳送到其他節點上。但是這裡必須要注意的是,在實際的生產環境中,恐怕能夠直接用這種策略的場景還是有限的,如果RDD中資料比較多時(比如幾十億),直接用這種持久化級別,會導致JVM的OOM記憶體溢位異常。

如果使用MEMORY_ONLY級別時發生了記憶體溢位,那麼建議嘗試使用MEMORY_ONLY_SER級別。該級別會將RDD資料序列化後再儲存在記憶體中,此時每個partition僅僅是一個位元組陣列而已,大大減少了物件數量,並降低了記憶體佔用。這種級別比MEMORY_ONLY多出來的效能開銷,主要就是序列化與反序列化的開銷。但是後續運算元可以基於純記憶體進行操作,因此效能總體還是比較高的。此外,可能發生的問題同上,如果RDD中的資料量過多的話,還是可能會導致OOM記憶體溢位的異常。

不要洩漏到磁碟,除非你在記憶體中計算需要很大的花費,或者可以過濾大量資料,儲存部分相對重要的在記憶體中。否則儲存在磁碟中計算速度會很慢,效能急劇降低。

字尾為_2的級別,必須將所有資料都複製一份副本,併傳送到其他節點上,資料複製以及網路傳輸會導致較大的效能開銷,除非是要求作業的高可用性,否則不建議使用。

CheckPoint 機制

除了使用快取機制可以有效的保證RDD的故障恢復,但是如果快取失效還是會在導致系統重新計算RDD的結果,所以對於一些RDD的lineage較長的場景,計算比較耗時,使用者可以嘗試使用checkpoint機制儲存RDD的計算結果,該種機制和快取最大的不同在於,使用checkpoint之後被checkpoint的RDD資料直接持久化在檔案系統中,一般推薦將結果寫在hdfs中,這種checpoint並不會自動清空。注意checkpoint在計算的過程中先是對RDD做mark,在任務執行結束後,再對mark的RDD實行checkpoint,也就是要重新計算被Mark之後的rdd的依賴和結果。

sc.setCheckpointDir("hdfs://CentOS:9000/checkpoints")

val rdd1 = sc.textFile("hdfs://CentOS:9000/demo/words/")
.map(line => {
  println(line)
})

//對當前RDD做標記
rdd1.checkpoint()

rdd1.collect()

因此在checkpoint一般需要和cache連用,這樣就可以保證計算一次。

sc.setCheckpointDir("hdfs://CentOS:9000/checkpoints")

val rdd1 = sc.textFile("hdfs://CentOS:9000/demo/words/")
.map(line => {
  println(line)
})

rdd1.persist(StorageLevel.MEMORY_AND_DISK)//先cache
//對當前RDD做標記
rdd1.checkpoint()
rdd1.collect()
rdd1.unpersist()//刪除快取

任務計算原始碼剖析

理論指導

sc.textFile("hdfs:///demo/words/t_word") //RDD0
   .flatMap(_.split(" "))                //RDD1
   .map((_,1))                           //RDD2
   .reduceByKey(_+_)                     //RDD3  finalRDD
   .collect                              //Array 任務提交

在這裡插入圖片描述

通過分析以上的程式碼,我們不難發現Spark在執行任務前期,會根據RDD的轉換關係形成一個任務執行DAG。將任務劃分成若干個stage。Spark底層在劃分stage的依據是根據RDD間的依賴關係劃分。Spark將RDD與RDD間的轉換分類:ShuffleDependency-寬依賴 | NarrowDependency-窄依賴,Spark如果發現RDD與RDD之間存在窄依賴關係,系統會自動將存在窄依賴關係的RDD的計算運算元歸納為一個stage,如果遇到寬依賴系統開啟一個新的stage.

Spark 寬窄依賴判斷

在這裡插入圖片描述

寬依賴:父RDD的一個分割槽對應了子RDD的多個分割槽,出現分叉就認定為寬依賴。ShuffleDependency

窄依賴:父RDD的1個分割槽(多個父RDD)僅僅只對應子RDD的一個分割槽認定為窄依賴。OneToOneDependency|RangeDependency|PruneDependency

Spark在任務提交前期,首先根據finalRDD逆推出所有依賴RDD,以及RDD間依賴關係,如果遇到窄依賴合併在當前的stage中,如果是寬依賴開啟新的stage。

在這裡插入圖片描述

getMissingParentStages

private def getMissingParentStages(stage: Stage): List[Stage] = {
    val missing = new HashSet[Stage]
    val visited = new HashSet[RDD[_]]
    // We are manually maintaining a stack here to prevent StackOverflowError
    // caused by recursively visiting
    val waitingForVisit = new ArrayStack[RDD[_]]
    def visit(rdd: RDD[_]) {
      if (!visited(rdd)) {
        visited += rdd
        val rddHasUncachedPartitions = getCacheLocs(rdd).contains(Nil)
        if (rddHasUncachedPartitions) {
          for (dep <- rdd.dependencies) {
            dep match {
              case shufDep: ShuffleDependency[_, _, _] =>
                val mapStage = getOrCreateShuffleMapStage(shufDep, stage.firstJobId)
                if (!mapStage.isAvailable) {
                  missing += mapStage
                }
              case narrowDep: NarrowDependency[_] =>
                waitingForVisit.push(narrowDep.rdd)
            }
          }
        }
      }
    }
    waitingForVisit.push(stage.rdd)
    while (waitingForVisit.nonEmpty) {
      visit(waitingForVisit.pop())
    }
    missing.toList
  }

遇到寬依賴,系統會自動的建立一個ShuffleMapStage

submitMissingTasks

  private def submitMissingTasks(stage: Stage, jobId: Int) {
    
        //計算分割槽
        val partitionsToCompute: Seq[Int] = stage.findMissingPartitions()
        ...
        //計算最佳位置
      val taskIdToLocations: Map[Int, Seq[TaskLocation]] = try {
        stage match {
          case s: ShuffleMapStage =>
            partitionsToCompute.map { id => (id, getPreferredLocs(stage.rdd, id))}.toMap
          case s: ResultStage =>
            partitionsToCompute.map { id =>
              val p = s.partitions(id)
              (id, getPreferredLocs(stage.rdd, p))
            }.toMap
        }
      } catch {
        case NonFatal(e) =>
          stage.makeNewStageAttempt(partitionsToCompute.size)
          listenerBus.post(SparkListenerStageSubmitted(stage.latestInfo, properties))
          abortStage(stage, s"Task creation failed: $e\n${Utils.exceptionString(e)}", Some(e))
          runningStages -= stage
          return
      }
    //將分割槽對映TaskSet
    val tasks: Seq[Task[_]] = try {
      val serializedTaskMetrics = closureSerializer.serialize(stage.latestInfo.taskMetrics).array()
      stage match {
        case stage: ShuffleMapStage =>
          stage.pendingPartitions.clear()
          partitionsToCompute.map { id =>
            val locs = taskIdToLocations(id)
            val part = partitions(id)
            stage.pendingPartitions += id
            new ShuffleMapTask(stage.id, stage.latestInfo.attemptNumber,
              taskBinary, part, locs, properties, serializedTaskMetrics, Option(jobId),
              Option(sc.applicationId), sc.applicationAttemptId, stage.rdd.isBarrier())
          }

        case stage: ResultStage =>
          partitionsToCompute.map { id =>
            val p: Int = stage.partitions(id)
            val part = partitions(p)
            val locs = taskIdToLocations(id)
            new ResultTask(stage.id, stage.latestInfo.attemptNumber,
              taskBinary, part, locs, id, properties, serializedTaskMetrics,
              Option(jobId), Option(sc.applicationId), sc.applicationAttemptId,
              stage.rdd.isBarrier())
          }
      }
    } catch {
      case NonFatal(e) =>
        abortStage(stage, s"Task creation failed: $e\n${Utils.exceptionString(e)}", Some(e))
        runningStages -= stage
        return
    }
    //呼叫taskScheduler#submitTasks TaskSet
    if (tasks.size > 0) {
      logInfo(s"Submitting ${tasks.size} missing tasks from $stage (${stage.rdd}) (first 15 " +
        s"tasks are for partitions ${tasks.take(15).map(_.partitionId)})")
      taskScheduler.submitTasks(new TaskSet(
        tasks.toArray, stage.id, stage.latestInfo.attemptNumber, jobId, properties))
    } 
    ...
  } 

總結關鍵字:逆推、finalRDD、ResultStage 、ShuffleMapStage、ShuffleMapTask、ResultTask、ShuffleDependency、NarrowDependency、DAGSchedulerTaskSchedulerSchedulerBackendDAGSchedulerEventProcessLoop

Jars依賴問題

1、可以使用–packages或者–jars解決依賴問題

[root@CentOS ~]# spark-submit  --master spark://CentOS:7077 --deploy-mode client --class com.baizhi.outputs.SparkWordCountApplication --name RedisSinkDemo --total-executor-cores 6 --packages redis.clients:jedis:2.9.2  /root/original-spark-rdd-1.0-SNAPSHOT.jar

2、可以使用fat jar外掛將需要的依賴打包

[root@CentOS ~]# spark-submit  --master spark://CentOS:7077 --deploy-mode client --class com.baizhi.outputs.SparkWordCountApplication --name RedisSinkDemo --total-executor-cores 6 /root/spark-rdd-1.0-SNAPSHOT.jar

3、注意當整合MySQL的時候,需要額外注意

  • 將MySQL新增到HADOOP_CLASSPATH類路徑下
  • 使用spark.executor.extraClassPath和spark.driver.extraClassPath能夠解決MySQL依賴問題
[root@CentOS ~]#  spark-submit  --master spark://CentOS:7077 --deploy-mode client --class com.baizhi.inputs.SparkMySQLUserQueryApplication  --name MysqLReadDemo --total-executor-cores 6 --conf spark.driver.extraClassPath=/root/mysql-connector-java-5.1.49.jar --conf  spark.executor.extraClassPath=/root/mysql-connector-java-5.1.49.jar  /root/original-spark-rdd-1.0-SNAPSHOT.jar

如果大家覺得麻煩,還可以在 spark-defaut.conf 配置改引數:

spark.executor.extraClassPath=/root/.ivy2/jars/* 
spark.driver.extraClassPath=/root/.ivy2/jars/*
[root@CentOS ~]#  spark-submit  --master spark://CentOS:7077 --deploy-mode client --class com.baizhi.inputs.SparkMySQLUserQueryApplication  --name MysqLReadDemo --total-executor-cores 6 --packages mysql:mysql-connector-java:5.1.38 /root/original-spark-rdd-1.0-SNAPSHOT.jar

/.ivy2/jars/*


```shell
[root@CentOS ~]#  spark-submit  --master spark://CentOS:7077 --deploy-mode client --class com.baizhi.inputs.SparkMySQLUserQueryApplication  --name MysqLReadDemo --total-executor-cores 6 --packages mysql:mysql-connector-java:5.1.38 /root/original-spark-rdd-1.0-SNAPSHOT.jar

相關文章