spark入門筆記

fengye發表於2018-01-22

本文原始碼基於spark 2.2.0

基本概念

Application

使用者編寫的Spark程式,通過一個有main方法的類執行,完成一個計算任務的處理。它是由一個Driver程式和一組執行於Spark叢集上的Executor組成

RDD

彈性分散式資料集。RDD是Spark的核心資料結構,可以通過一系列運算元進行操作。當RDD遇到Action運算元時,將之前的所有運算元形成一個有向無環圖(DAG)。再在Spark中轉化為Job,提交到叢集執行

spark2.x後就使用DataFrame/DateSet了

SparkContext

SparkContext是Spark的入口,負責連線Spark叢集,建立RDD,累積量和廣播量等。從本質上來說,SparkContext是Spark的對外介面,負責向呼叫者提供Spark的各種功能。

SparkContext在Spark中的主要功能
driver program通過SparkContext連線到叢集管理器來實現對叢集中任務的控制。Spark配置引數的設定以及對SQLContext、HiveContext和StreamingContext的控制也要通過SparkContext進行

Only one SparkContext may be active per JVM. You must stop() the active SparkContext before creating a new one. This limitation may eventually be removed; see SPARK-2243 for more details.
每個JVM只有一個SparkContext,一臺伺服器可以啟動多個JVM

SparkSession

The entry point to programming Spark with the Dataset and DataFrame API.
包含了SQLContext和HiveContext

Driver

執行main方法的Java虛擬機器程式,負責監聽spark application的executor程式發來的通訊和連線,將工程jar傳送到所有的executor程式中
Driver與Master、Worker協作完成application程式的啟動、DAG劃分、計算任務封裝、分配task到executor上、計算資源的分配等排程執行作業等
driver排程task給executor執行,所以driver最好和spark叢集在一片網路內,便以通訊
driver程式通常在worker節點中,和Cluster Manager不在同一個節點上

Cluster Manager作用物件是整個saprk叢集(叢集資源分配),所有應用,而Driver是作用於某一個應用(協調已經分配給application的資源),管理層面不一樣

Worker

叢集中的工作節點,啟動並執行executor程式,執行作業程式碼的節點
standalone模式下:Worker程式所在節點
yarn模式下: yarn的nodemanager程式所在的節點

Executor

執行在worker節點上,負責執行作業的任務,並將資料儲存在記憶體或磁碟中
每個spark application,都有屬於自己的executor程式,spark application不會共享一個executor程式

在啟動引數中有executor-cores,executor-memory,每個executor都會佔用cpu core和記憶體,又spark application間不會複用executor,則很容易導致worker資源不足

executor在整個spark application執行的生命週期內,executor可以動態增加/釋放,見動態資源分配一節
executor使用多執行緒執行SparkContext分配過來的task,來一批task就執行一批

Job

一個spark application可能會被分為多個job,每次呼叫Action時,邏輯上會生成一個Job,一個Job包含了一個或多個Stage

Stage

每個job都會劃分為一個或多個stage(階段),每個stage都會有對應的一批task(即一個taskset),分配到executor上去執行

Stage包括兩類:ShuffleMapStage和ResultStage,如果使用者程式中呼叫了需要進行Shuffle計算的Operator,如groupByKey等,就會以Shuffle為邊界分成ShuffleMapStage和ResultStage。
如果一次shuffle都沒執行,那就只有一個stage

TaskSet

一組關聯的,但相互之間沒有Shuffle依賴關係的Task集合;Stage可以直接對映為TaskSet,一個TaskSet封裝了一次需要運算的、具有相同處理邏輯的Task,這些Task可以平行計算,粗粒度的排程是以TaskSet為單位的。

一個stage對應一個taskset

Task

driver傳送到executor上執行的計算單元,每個task負責在一個階段(stage),處理一小片資料,計算出對應的結果
Task是在物理節點上執行的基本單位,Task包含兩類:ShuffleMapTask和ResultTask,分別對應於Stage中ShuffleMapStage和ResultStage中的一個執行基本單元。
InputSplit-task-partition有一一對應關係,Spark會為每一個partition執行一個task來進行處理(見本文知識點-Spark叢集中的節點個數、RDD分割槽個數、cpu核心個數三者與並行度的關係一節)
手動設定task數量spark.default.parallelism

Cluster Manager

叢集管理器,為每個spark application在叢集中排程和分配資源的元件,如Spark Standalone、YARN、Mesos等

Deploy Mode

不論是standalone/yarn,都分為兩種模式,client和cluster,區別在於driver執行的位置
client模式下driver執行在提交spark作業的機器上,可以實時看到詳細的日誌資訊,方便追蹤和排查錯誤,用於測試
cluster模式下,spark application提交到cluster manager,cluster manager(比如master)負責在叢集中某個節點上,啟動driver程式,用於生產環境

通常情況下driver和worker在同一個網路中是最好的,而client很可能就是driver worker分開佈置,這樣網路通訊很耗時,cluster沒有這樣的問題

standalone模式

master做叢集管理
Master程式和Worker程式組成的叢集, 可以不需要yarn叢集,不需要HDFS

Master

standalone模式下,叢集管理器(Cluster Manager)的一種,為每個spark application在叢集中排程和分配資源的元件

注意和driver的區別,即Cluster Manager和driver的區別

yarn模式

yarn做叢集管理
ResourceManager程式和NodeManager程式組成的叢集

DAGScheduler

根據Job構建基於Stage的DAG,並提交Stage給TaskScheduler。

TaskScheduler

將Taskset提交給Worker node叢集執行並返回結果。

spark元件-百度腦圖

spark基本工作原理

Driver向Master申請資源;
Master讓Worker給程式分配具體的Executor
Driver把劃分好的Task傳送給Executor,Task就是我們的Spark程式的業務邏輯程式碼

job生成,stage劃分和task分配都是發生在driver端?是

Spark VS MapReduce

Spark和MapReduce最大不同:迭代式計算

  • MapReduce
    一個job分兩個階段,map和reduce,兩階段處理完就算結束了
  • Spark
    可分為n個階段,為記憶體迭代式

RDD

全稱為Resillient Distributed Dataset,即彈性分散式資料集。
提供了容錯性,可以自動從來源資料重新計算,從節點失敗中恢復過來
預設是在記憶體中,記憶體不足則寫入磁碟
一個RDD是分散式的,資料分佈在一批節點上,每個節點儲存了RDD部分partition

RDD記憶體不足會自動寫入磁碟,呼叫cache()和persist()會將RDD資料按storelevel儲存

RDD建立

  1. SparkContext.wholeTextFiles()可以針對一個目錄中的大量小檔案,返回<filename,fileContent>組成的個PairRDD
  2. SparkContext.sequenceFile[K,V]()可以針對SequenceFile建立RDD,K和V泛型型別就是SequenceFile的key和value的型別。K和V要求必須是Hadoop的序列化型別,比如IntWritable、Text等。
  3. SparkContext.hadoopRDD()可以針對Hadoop的自定義輸入型別建立RDD。該方法接收JobConf、InputFormatClass、Key和Value的Class。
  4. SparkContext.objectFile()方法,可以針對之前呼叫RDD.saveAsObjectFile()建立的物件序列化的檔案,反序列化檔案中的資料,並建立一個RDD。

並行化建立RDD
呼叫parallelize()方法,可以指定要將集合切分成多少個partition(實際上應該是指定了InputSplit數量,InputSplit-task-partition),Spark會為每一個partition執行一個task來進行處理(見本文知識點-Spark叢集中的節點個數、RDD分割槽個數、cpu核心個數三者與並行度的關係一節)
Spark官方建議為叢集中的每個CPU建立2~4個partition,避免CPU空載

如果叢集中執行了多個任務,包括spark hadoop任務,是否也是以一個cpu core負載2-4個計算任務來配置?

Transformation和Action

Transformation

針對已有的RDD建立一個新的RDD
transformation具有lazy特性,只是記錄了對RDD所做的操作,但是不會自發地執行。只有Action操作後,所有的transformation才會執行,可以避免產生過多中間結果

操作 介紹
map 將RDD中的每個元素傳入自定義函式,獲取一個新的元素,然後用新的元素組成新的RDD
filter 對RDD中每個元素進行判斷,如果返回true則保留,返回false則剔除。
flatMap 與map類似,是先對映後扁平化
gropuByKey 根據key進行分組,每個key對應一個Iterable
reduceByKey 對每個key對應的value進行reduce操作。
sortByKey 對每個key對應的value進行排序操作。
join 對兩個包含<key,value>對的RDD進行join操作,每個key join上的pair,都會傳入自定義函式進行處理。
cogroup 同join,但是每個key對應的Iterable都會傳入自定義函式進行處理。

map與flatMap的區別
map對rdd之中的元素逐一進行函式操作對映為另外一個rdd。
flatMap對集合中每個元素進行操作然後再扁平化。通常用來切分單詞

實驗:flatMap是否會將多層巢狀的元素再拍扁
實驗結論:只往下一層做flatten操作,不會遞迴進去做flatten操作

val arr = sc.parallelize(Array(("A", 1), ("B", 2), ("C", 3)))
arr.flatMap(x => (x._1 + x._2)).foreach(print)  //A1B2C3

val arr2 = sc.parallelize(Array(
                              Array(
                                ("A", 1), ("B", 2), ("C", 3)),
                              Array(
                                ("C", 1), ("D", 2), ("E", 3)),
                              Array(
                                ("F", 1), ("G", 2), ("H", 3))))
arr2.flatMap(x => x).foreach(print)  //(A,1)(B,2)(C,3)(C,1)(D,2)(E,3)(F,1)(G,2)(H,3)

val arr3 = sc.parallelize(Array(
                              Array(
                                Array(("A", 1), ("B", 2), ("C", 3))),
                              Array(
                                Array(("C", 1), ("D", 2), ("E", 3))),
                              Array(
                                Array(("F", 1), ("G", 2), ("H", 3)))))
arr3.flatMap(x => x).foreach(print)  //[Lscala.Tuple2;@11074bf8 [Lscala.Tuple2;@c10a22d [Lscala.Tuple2;@40ef42cd
複製程式碼

map和flatMap原始碼

  def map[B](f: A => B): Iterator[B] = new AbstractIterator[B] {
    def hasNext = self.hasNext
    //直接遍歷元素,對元素應用f方法
    def next() = f(self.next())
  }

  /** Creates a new iterator by applying a function to all values produced by this iterator
   *  and concatenating the results.
   *
   *  @return  the iterator resulting from applying the given iterator-valued function
   *           `f` to each value produced by this iterator and concatenating the results.
   */
  def flatMap[B](f: A => GenTraversableOnce[B]): Iterator[B] = new AbstractIterator[B] {
    private var cur: Iterator[B] = empty
    //這一步只是取當前元素的Iterator,沒有遞迴往下層取
    private def nextCur() { cur = f(self.next()).toIterator }
    def hasNext: Boolean = {
      while (!cur.hasNext) {
        if (!self.hasNext) return false
        nextCur()
      }
      true
    }
    //在呼叫next方法時,最終會呼叫到nextCur方法
    def next(): B = (if (hasNext) cur else empty).next()
  }
複製程式碼

join VS cogroup VS fullOuterJoin VS leftOuterJoin VS rightOuterJoin

val studentList = Array(
  Tuple2(1, "leo"),
  Tuple2(2, "jack"),
  Tuple2(3, "tom"));
val scoreList = Array(
  Tuple2(1, 100),
  Tuple2(2, 90),
  Tuple2(2, 90),
  Tuple2(4, 60));
val students = sc.parallelize(studentList);
val scores = sc.parallelize(scoreList);
/*
 * (4,(CompactBuffer(),CompactBuffer(60)))
 * (1,(CompactBuffer(leo),CompactBuffer(100)))
 * (3,(CompactBuffer(tom),CompactBuffer()))
 * (2,(CompactBuffer(jack),CompactBuffer(90, 90)))
 */
val studentCogroup = students.cogroup(scores)   //union key陣列延長
/*
 * (1,(leo,100))
 * (2,(jack,90))
 * (2,(jack,90))
 */
val studentJoin = students.join(scores) //交集
/*
 * (4,(None,Some(60)))
 * (1,(Some(leo),Some(100)))
 * (3,(Some(tom),None))
 * (2,(Some(jack),Some(90)))
 * (2,(Some(jack),Some(90)))
 */
val studentFullOuterJoin = students.fullOuterJoin(scores) //some可為空 union
/*
 * (1,(leo,Some(100)))
 * (3,(tom,None))
 * (2,(jack,Some(90)))
 * (2,(jack,Some(90)))
 */
val studentLeftOuterJoin = students.leftOuterJoin(scores) //左不為空
/*
 * (4,(None,60))
 * (1,(Some(leo),100))
 * (2,(Some(jack),90))
 * (2,(Some(jack),90))
 */
val studentRightOuterJoin = students.rightOuterJoin(scores) //右不為空
複製程式碼

Action

對RDD進行最後的操作,如遍歷,reduce,save等,啟動計算操作,並向使用者程式返回值或向外部儲存寫資料
觸發一個spark job的執行,從而觸發這個action之前所有的transformation的執行 對於操作key-value對的Tuple2 RDD,如groupByKey,scala是通過隱式轉換為PairRDDFunction,再提供對應groupByKey方法實現的,需要手動匯入Spark的相關隱式轉換,import org.apache.spark.SparkContext._

對groupByKey,saprk2.2顯式使用HashPartitioner,沒有看到隱式轉換為PairRDDFunction Action操作一定會將結果返回給driver?是的,見下文的runJob方法

Action操作特徵
Action操作在原始碼上必呼叫runJob()方法,可能是直接或間接呼叫

    //直接呼叫了runJob
  def collect(): Array[T] = withScope {
    val results = sc.runJob(this, (iter: Iterator[T]) => iter.toArray)
    Array.concat(results: _*)
  }
  
  /**
   * Run a function on a given set of partitions in an RDD and pass the results to the given
   * handler function. This is the main entry point for all actions in Spark.
   *
   * @param resultHandler callback to pass each result to
   */
   //會把結果傳遞給handler function,handle function就是對返回結果進行處理的方法
   //如上文的collect方法的handler function就是 (iter: Iterator[T]) => iter.toArray
  def runJob[T, U: ClassTag](
      rdd: RDD[T],
      func: (TaskContext, Iterator[T]) => U,
      partitions: Seq[Int],
      resultHandler: (Int, U) => Unit): Unit = {
    if (stopped.get()) {
      throw new IllegalStateException("SparkContext has been shutdown")
    }
    val callSite = getCallSite
    val cleanedFunc = clean(func)
    logInfo("Starting job: " + callSite.shortForm)
    if (conf.getBoolean("spark.logLineage", false)) {
      logInfo("RDD's recursive dependencies:\n" + rdd.toDebugString)
    }
    dagScheduler.runJob(rdd, cleanedFunc, partitions, callSite, resultHandler, localProperties.get)
    progressBar.foreach(_.finishAll())
    rdd.doCheckpoint()
  }
複製程式碼
操作 介紹
reduce 將RDD中的所有元素進行聚合操作。第一個和第二個元素聚合,值與第三個元素聚合,值與第四個元素聚合,以此類推。
collect 將RDD中所有元素獲取到本地客戶端。注意資料傳輸問題,spark.driver.maxResultSize可以限制action運算元返回driver的結果集最大數量
count 獲取RDD元素總數。
take(n) 獲取RDD中前n個元素。
saveAsTextFile 將RDD元素儲存到檔案中,對每個元素呼叫toString方法
countByKey 對每個key對應的值進行count計數。
foreach 遍歷RDD中的每個元素。
//從本地檔案建立
val lines = spark.sparkContext.textFile("hello.txt")
//Transformation,返回(key,value)的RDD
val linePairs = lines.map(line => (line, 1))
//Transformation,隱式裝換為PairRDDFunction,提供reduceByKey等方法
//原始碼中是用HashPartitioner
val lineCounts = linePairs.reduceByKey(_ + _)
//Action,傳送到driver端執行
lineCounts.foreach(lineCount => println(lineCount._1 + " appears " + lineCount._2 + " times."))
複製程式碼

mapPartitions

map:一次處理一個partition中的一條資料
mapPartitions:一次處理一個partition中所有的資料
使用場景:
RDD的資料量不是特別大,建議採用mapPartitions運算元替代map運算元,可以加快處理速度,如果RDD的資料量特別大,則不建議用mapPartitions,可能會記憶體溢位

val studentScoresRDD = studentNamesRDD.mapPartitions { it =>
    var studentScoreList = Array("a")
    while (it.hasNext) {
      ...
    }
    studentScoreList.iterator
}
複製程式碼

mapPartitionsWithIndex:加上了partition的index

studentNamesRDD.mapPartitionsWithIndex{(index:Int,it:Iterator[String])=>
      ...
 }
複製程式碼

其他運算元

  1. sample:按比例取樣本,transformation操作
  2. takeSample:按個數取樣本,action操作
  3. cartesian:笛卡爾積
  4. coalesce:將RDD的partition縮減,將資料壓縮到更少的partition中去.
    使用場景:若很多partition中的資料不均勻(如filter後),可以使用coalesce壓縮rdd的partition數量,從而讓各個partition中的資料都更加的緊湊
    rdd.coalesce(3):壓縮成3個partition

coalesce和repartition區別
repartition是coalesce的簡化版

/**
 * 返回一個經過簡化到numPartitions個分割槽的新RDD。這會導致一個窄依賴
 * 例如:你將1000個分割槽轉換成100個分割槽,這個過程不會發生shuffle,相反如果10個分割槽轉換成100個分割槽將會發生shuffle。
 * 然而如果你想大幅度合併分割槽,例如合併成一個分割槽,這會導致你的計算在少數幾個叢集節點上計算(言外之意:並行度不夠)
 * 為了避免這種情況,你可以將第二個shuffle引數傳遞一個true,這樣會在重新分割槽過程中多一步shuffle,這意味著上游的分割槽可以並行執行。
 */
def coalesce(numPartitions: Int, shuffle: Boolean = false,
           partitionCoalescer: Option[PartitionCoalescer] = Option.empty)
          (implicit ord: Ordering[T] = null)
  : RDD[T] = withScope {...}
/**
 * 返回一個恰好有numPartitions個分割槽的RDD,可以增加或者減少此RDD的並行度。
 * 在內部,這將使用shuffle重新分佈資料,如果你減少分割槽數,考慮使用coalesce,這樣可以避免執行shuffle
 */
def repartition(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T] = withScope {
    coalesce(numPartitions, shuffle = true)
}
複製程式碼

設RDD分割槽數從N變更為M

分割槽數關係 shuffle = true shuffle = false
N < M N個分割槽有資料分佈不均勻的狀況,利用HashPartitioner函式將資料重新分割槽為M個 coalesce為無效的,不進行shuffle過程,父RDD和子RDD之間是窄依賴關係
N > M 將N個分割槽中的若干個分割槽合併成一個新的分割槽,最終合併為M個分割槽
N >> M shuffle = true,在重新分割槽過程中多一步shuffle,上游的分割槽可以並行執行,使coalesce之前的操作有更好的並行度 父子RDD是窄依賴關係,在同一個Stage中,可能造成Spark程式的並行度不夠(計算在少數幾個叢集節點上進行),從而影響效能
  1. 返回一個減少到M個分割槽的新RDD,這會導致窄依賴,不會發生shuffle
  2. 返回一個增加到M個分割槽的新RDD,會發生shuffle
  3. 如果shuff為false時,N<M,RDD的分割槽數是不變的,也就是說不經過shuffle,是無法將RDD的partition數變多的

RDD持久化

cache()和persist()

  /**
   * Persist this RDD with the default storage level (`MEMORY_ONLY`).
   */
  def persist(): this.type = persist(StorageLevel.MEMORY_ONLY)
  /**
   * Persist this RDD with the default storage level (`MEMORY_ONLY`).
   */
  def cache(): this.type = persist()
複製程式碼

如果需要從記憶體中清除快取,那麼可以使用unpersist()方法。

class StorageLevel private(
    private var _useDisk: Boolean,  //磁碟
    private var _useMemory: Boolean,//記憶體
    private var _useOffHeap: Boolean,//記憶體滿就存磁碟
    private var _deserialized: Boolean,//序列化儲存
    private var _replication: Int = 1)//冗餘備份,預設1,只自己儲存一份
  extends Externalizable {
複製程式碼
持久化級別 含義
MEMORY_ONLY 以非序列化的Java物件的方式持久化在JVM記憶體中。如果記憶體無法完全儲存RDD所有的partition,那麼那些沒有持久化的partition就會在下一次需要使用它的時候,重新被計算
MEMORY_AND_DISK 同上,但是當某些partition無法儲存在記憶體中時,會持久化到磁碟中。下次需要使用這些partition時,需要從磁碟上讀取。
MEMORY_ONLY_SER 同MEMORY_ONLY,但是會使用Java序列化方式,將Java物件序列化後進行持久化。可以減少記憶體開銷,但是需要進行反序列化,因此會加大CPU開銷。
MEMORY_AND_DSK_SER 同MEMORY_AND_DSK。但是使用序列化方式持久化Java物件。
DISK_ONLY 使用非序列化Java物件的方式持久化,完全儲存到磁碟上。
MEMORY_ONLY_2 MEMORY_AND_DISK_2 等等 如果是尾部加了2的持久化級別,表示會將持久化資料複用一份,儲存到其他節點,從而在資料丟失時,不需要再次計算,只需要使用備份資料即可。

優先順序排序(記憶體優先)

  1. MEMORY_ONLY
  2. MEMORY_ONLY_SER,將資料進行序列化進行儲存
  3. DISK

共享變數

  1. 預設
    一個運算元的函式中使用到了某個外部的變數,則拷貝變數到每個task中,此時每個task只能操作自己的那份變數副本
  2. Broadcast Variable(廣播變數)
    將使用到的變數,為每個節點拷貝一份(不是每個task),減少網路傳輸以及記憶體消耗
    只讀變數
  3. Accumulator(累加變數)
    讓多個task共同操作一份變數,主要可以進行累加操作
val rdd = sc.parallelize(Array(1, 2, 3, 4, 5))
val factorBroadcast = sc.broadcast(3)
val sumAccumulator = new DoubleAccumulator()
//Accumulator must be registered before send to executor
sc.register(sumAccumulator)

val multipleRdd = rdd.map(num => num * factorBroadcast.value)
//不能獲取值,只能在driver端獲取
val accumulator = rdd.map(num2 => sumAccumulator.add(num2.toDouble))
//action:3,6,9,12,15
multipleRdd.foreach(num => println(num))
//要先執行action操作才能獲取值
accumulator.collect()  //15
println(sumAccumulator.value) 
accumulator.count()    //30,再次加15
println(sumAccumulator.value)
複製程式碼

spark 核心架構

standalone模式下

Spark架構原理-standalone模式下

TaskScheduler把taskSet裡每一個task提交到executor上執行

spark內部元件

寬依賴與窄依賴

窄依賴(narrow dependency):每個parent RDD 的 partition 最多被 child RDD的一個partition使用
寬依賴(wide dependency):每個parent RDD 的 partition 被多個 child RDD的partition使用

區別:

  1. 窄依賴允許在一個叢集節點上以流水線的方式(pipeline)計算所有父分割槽。例如,逐個元素地執行map、然後filter操作;
    寬依賴則需要首先計算好所有父分割槽資料,然後在節點之間進行Shuffle,這與MapReduce類似。
  2. 窄依賴能夠更有效地進行失效節點的恢復,即只需重新計算丟失RDD分割槽的父分割槽,而且不同節點之間可以平行計算;
    而對於一個寬依賴關係的Lineage圖,單個節點失效可能導致這個RDD的所有祖先丟失部分分割槽,因而需要整體重新計算。

寬依賴與窄依賴

spark提交模式

  1. spark核心模式/standalone 模式 : 基於spark的master-worker叢集
  2. 基於Yarn的yarn-cluster模式
  3. 基於Yarn的yarn-client模式

在spark提交指令碼中設定
--master引數值為yarn-cluster / yarn-client
預設是standalone 模式

spark提交指令碼

/usr/local/spark/bin/spark-submit \
--class com.feng.spark.spark1.StructuredNetworkWordCount \
--master spark://spark1:7077 \ #standalone模式
--num-executors 3 \     // #分配3個executor
--driver-memory 500m \  
--executor-memory 500m \    # //每個executor500m記憶體
--executor-cores 2 \    // # 每個executor2個core
/usr/local/test_data/spark1-0.0.1-SNAPSHOT-jar-with-dependencies.jar \
複製程式碼

整個應用需要3*500=1500m記憶體,3*2=6個core
--master local[8]:程式中用8個執行緒來模擬叢集的執行
--total-executor-cores:指定所有executor的總cpu core數量
--supervise:指定了spark監控driver節點,如果driver掛掉,自動重啟driver

配置方式

按優先順序從高到低排序

  1. SparkConf:通過程式設定,在編輯器中用local模式執行執行時,只能在SparkConf中設定屬性
  2. spark-submit指令碼命令:當前應用有效,推薦
  3. spark-defaults.conf檔案:全域性配置
SparkConf.set("spark.default.parallelism", "100")
spark-submit: --conf spark.default.parallelism=50
spark-defaults.conf: spark.default.parallelism 10
複製程式碼

在spark-submit指令碼中,可以使用--verbose,列印詳細的配置屬性的資訊

可以先在程式中建立一個空的SparkConf物件,如

val sc = new SparkContext(new SparkConf())
複製程式碼

然後在spark-submit指令碼中用--conf設定屬性值,如

--conf spark.eventLog.enabled=false
複製程式碼

依賴管理

--jars:額外依賴的jar包會自動被髮送到叢集上去
指定關聯的jar:

  1. file: 由driver的http檔案服務提供支援,所有的executor都會通過driver的HTTP服務來拉取檔案
  2. hdfs:/http:/https:/ftp: 直接根據URI,從指定的地方拉取
  3. local: 這種格式的檔案必須在每個worker節點上都要存在,所以不需要通過網路io去拉取檔案,適用於特別大的檔案或jar包,可以提升作業的執行效能

檔案和jar都會被拷貝到每個executor的工作目錄中,這就會佔用很大一片磁碟空間,因此需要在之後清理掉這些檔案
在yarn上執行spark作業時,依賴檔案的清理都是自動進行的
使用standalone模式,需要配置spark.worker.cleanup.appDataTtl屬性,來開啟自動清理依賴檔案和jar包

相關引數見conf/spark-evnsh引數一節

--packages:繫結maven的依賴包
--repositories:繫結額外的倉庫

yarn-cluster模式

用於生產模式,driver執行在nodeManager,沒有網路卡流量激增問題,但檢視log麻煩,除錯不方便

基於YARN的提交模式-yarn-cluster

yarn-client模式

yarn-client用於測試,driver執行在本地客戶端,負責排程application,會與yarn叢集產生超大量的網路通訊,從而導致網路卡流量激增
yarn-client可以在本地看到所有log,方便除錯

基於YARN的提交模式-yarn-client

  1. yarn-client下,driver執行在spark-submit提交的機器上,ApplicationMaster只是相當於一個ExecutorLauncher,僅僅負責申請啟動executor;driver負責具體排程
  2. yarn-cluster下,ApplicationMaster是driver,ApplicationMaster負責具體排程

standalone核心元件互動流程

參見Spark架構簡明分析

基本要點:

  1. 一個Application會啟動一個Driver
  2. 一個Driver負責跟蹤管理該Application執行過程中所有的資源狀態和任務狀態
  3. 一個Driver會管理一組Executor
  4. 一個Executor只執行屬於一個Driver的Task

standalone核心元件互動流程

  • 橙色:提交使用者Spark程式
    使用者提交一個Spark程式,主要的流程如下所示:

    1. 使用者spark-submit指令碼提交一個Spark程式,會建立一個ClientEndpoint物件,該物件負責與Master通訊互動
    2. ClientEndpoint向Master傳送一個RequestSubmitDriver訊息,表示提交使用者程式
    3. Master收到RequestSubmitDriver訊息,向ClientEndpoint回覆SubmitDriverResponse,表示使用者程式已經完成註冊

    結合4,5,應該是表示使用者程式已經在master註冊,但driver可能並未啟動

    1. ClientEndpoint向Master傳送RequestDriverStatus訊息,請求Driver狀態

    MasterEndPoint應該會向DriverClient返回一個類似DriverStatusResponse的應答?週期性應答,當獲知driver已啟動,則導致5

    1. 如果當前使用者程式對應的Driver已經啟動,則ClientEndpoint直接退出,完成提交使用者程式
  • 紫色:啟動Driver程式
    當使用者提交使用者Spark程式後,需要啟動Driver來處理使用者程式的計算邏輯,完成計算任務,這時Master需要啟動一個Driver:

    1. Maser記憶體中維護著使用者提交計算的任務Application,每次記憶體結構變更都會觸發排程,向Worker傳送LaunchDriver請求
    2. Worker收到LaunchDriver訊息,會啟動一個DriverRunner執行緒去執行LaunchDriver的任務
    3. DriverRunner執行緒在Worker上啟動一個新的JVM例項,該JVM例項內執行一個Driver程式,該Driver會建立SparkContext物件

    當前worker節點執行driver程式

  • 紅色:註冊Application
    Dirver啟動以後,它會建立SparkContext物件,初始化計算過程中必需的基本元件,並向Master註冊Application,流程描述如下:

    1. 建立SparkEnv物件,建立並管理一些基本元件

    SparkEnv Holds all the runtime environment objects for a running Spark instance (either master or worker), including the serializer, RpcEnv, block manager, map output tracker, etc. Currently Spark code finds the SparkEnv through a global variable, so all the threads can access the same SparkEnv

    1. 建立TaskScheduler,負責Task排程
    2. 建立StandaloneSchedulerBackend,負責與ClusterManager進行資源協商
    3. 建立DriverEndpoint,其它元件可以與Driver進行通訊

    只是建立,還未啟動

    1. 在StandaloneSchedulerBackend內部建立一個StandaloneAppClient,負責處理與Master的通訊互動
    2. StandaloneAppClient建立一個ClientEndpoint,實際負責與Master通訊
    3. ClientEndpoint向Master傳送RegisterApplication訊息,註冊Application
    4. Master收到RegisterApplication請求後,回覆ClientEndpoint一個RegisteredApplication訊息,表示已經註冊成功
  • 藍色:啟動Executor程式

    1. Master向Worker傳送LaunchExecutor訊息,請求啟動Executor;同時Master會向Driver傳送ExecutorAdded訊息,表示Master已經新增了一個Executor(此時還未啟動)

    executor還未真實啟動,master只是發出一個啟動executor的訊息給worker而已. 這一步表明master才是負責啟動和分配executor,driver只是提交task到executor

    1. Worker收到LaunchExecutor訊息,會啟動一個ExecutorRunner執行緒去執行LaunchExecutor的任務
    2. Worker向Master傳送ExecutorStageChanged訊息,通知Executor狀態已發生變化
    3. Master向Driver傳送ExecutorUpdated訊息,此時Executor已經啟動

    這裡master才真正告訴driver executor已經啟動

  • 粉色:啟動Task執行

    1. StandaloneSchedulerBackend啟動一個DriverEndpoint

    之前已經建立,但未啟動,之前和master的通訊都是StandaloneSchedulerBackend完成的

    1. DriverEndpoint啟動後,會週期性地檢查Driver維護的Executor的狀態,如果有空閒的Executor便會排程任務執行

    啟動一個driver-revive-thread後臺執行緒,週期性地傳送ReviveOffers給自己,讓自己檢查executor狀態

    1. DriverEndpoint向TaskScheduler傳送Resource Offer請求

    DriverEndpoint是CoarseGrainedSchedulerBackend內部的一個持有物件

    1. 如果有可用資源啟動Task,則DriverEndpoint向Executor傳送LaunchTask請求
    2. Executor程式內部的CoarseGrainedExecutorBackend呼叫內部的Executor執行緒的launchTask方法啟動Task
    3. Executor執行緒內部維護一個執行緒池,建立一個TaskRunner執行緒並提交到執行緒池執行
  • 綠色:Task執行完成

    1. Executor程式內部的Executor執行緒通知CoarseGrainedExecutorBackend,Task執行完成
    2. CoarseGrainedExecutorBackend向DriverEndpoint傳送StatusUpdated訊息,通知Driver執行的Task狀態發生變更
    3. StandaloneSchedulerBackend呼叫TaskScheduler的updateStatus方法更新Task狀態

    StandaloneSchedulerBackend父類CoarseGrainedSchedulerBackend內部持有DriverEndpoint(內部類),DriverEndpoint收到StatusUpdate資訊後,直接呼叫scheduler.statusUpdate(taskId, state, data.value)

    1. StandaloneSchedulerBackend繼續呼叫TaskScheduler的resourceOffers方法,排程其他任務執行

Spark Standalone叢集單獨啟動master和worker

start-all.sh指令碼可以啟動master程式和所有worker程式,快速啟動整個spark standalone叢集

分別啟動master和worker程式

為何要分別啟動

分別啟動可以通過命令列引數,為程式配置一些獨特的引數
如監聽埠號、web ui埠號、使用的cpu和記憶體
如同一臺機器上不僅執行了saprk程式,還執行了storm程式,就可以限制spark worker程式使用更少的資源(cpu core,memory),而非機器上所有資源

引數 含義 物件 使用頻率
-h HOST, --ip HOST 在哪臺機器上啟動,預設就是本機 master & worker 不常用
-p PORT, --port PORT 在機器上啟動後,使用哪個埠對外提供服務,master預設是7077,worker預設是隨機的 master & worker 不常用
--webui-port PORT web ui的埠,master預設是8080,worker預設是8081 master & worker 不常用
-c CORES, --cores CORES 總共能讓spark作業使用多少個cpu core,預設是當前機器上所有的cpu core worker 常用
-m MEM, --memory MEM 總共能讓spark作業使用多少記憶體,是100M或者1G這樣的格式,預設是1g worker 常用
-d DIR, --work-dir DIR 工作目錄,預設是SPARK_HOME/work目錄 worker 常用
--properties-file FILE master和worker載入預設配置檔案的地址,預設是conf/spark-defaults.conf master & worker 不常用

啟動順序

先啟動master,再啟動worker,因為worker啟動以後,需要向master註冊

關閉順序1.worker(./stop-slave.sh) ;2. master(./stop-master);3. 關閉叢集./stop-all.sh

啟動master

  1. 使用start-master.sh啟動
  2. 啟動日誌就會列印一行spark://HOST:PORT,這就是master的URL地址,worker程式就會通過這個URL地址來連線到master程式,並進行註冊

    可以使用SparkSession.master()設定master地址

  3. 可以通過http://MASTER_HOST:8080來訪問master叢集的監控web ui,web ui上, 會顯示master的URL地址

手動啟動worker程式

使用start-slave.sh <master-spark-URL>當前節點上啟動worker程式
http://MASTER_HOST:8080web ui上會顯示該節點的cpu和記憶體資源等資訊
eg:./start-slave.sh spark://192.168.0.001:8080 --memory 500m

spark所有啟動和關閉指令碼

引數 含義
sbin/start-all.sh 根據配置,在叢集中各個節點上,啟動一個master程式和多個worker程式
sbin/stop-all.sh 在叢集中停止所有master和worker程式
sbin/start-master.sh 在本地啟動一個master程式
sbin/stop-master.sh 關閉master程式
sbin/start-slaves.sh 根據conf/slaves檔案中配置的worker節點,啟動所有的worker程式
sbin/stop-slaves.sh 關閉所有worker程式
sbin/start-slave.sh 在本地啟動一個worker程式

配置檔案

worker節點配置

配置作為worker節點的機器,如hostname/ip地址,一個機器是一行
配置後,所有的節點上,都拷貝這份檔案
預設情況下,沒有conf/slaves檔案,只有一個空conf/slaves.template, 此時,就只是在當前主節點上啟動一個master程式和一個worker程式,此時就是master程式和worker程式在一個節點上,也就是偽分散式部署
conf/slaves檔案樣本

spark1  
spark2  
spark3  
複製程式碼

conf/spark-evnsh引數

是對整個spark的叢集部署,配置各個master和worker

和啟動指令碼--引數的效果一樣./start-slave.sh spark://192.168.0.001:8080 --memory 500m,臨時修改引數時這種指令碼命令更適合
命令列引數優先順序更高,會覆蓋spark-evnsh引數

引數 含義
SPARK_MASTER_IP 指定master程式所在的機器的ip地址
SPARK_MASTER_PORT 指定master監聽的埠號(預設是7077)
SPARK_MASTER_WEBUI_PORT 指定master web ui的埠號(預設是8080)
SPARK_MASTER_OPTS 設定master的額外引數,使用"-Dx=y"設定各個引數
SPARK_LOCAL_DIRS spark的工作目錄,包括了shuffle map輸出檔案,以及持久化到磁碟的RDD等
SPARK_WORKER_PORT worker節點的埠號,預設是隨機的
SPARK_WORKER_WEBUI_PORT worker節點的web ui埠號,預設是8081
SPARK_WORKER_CORES worker節點上,允許spark作業使用的最大cpu數量,預設是機器上所有的cpu core
SPARK_WORKER_MEMORY worker節點上,允許spark作業使用的最大記憶體量,格式為1000m,2g等,預設最小是1g記憶體
SPARK_WORKER_INSTANCES 當前機器上的worker程式數量,預設是1,可以設定成多個,但是這時一定要設定SPARK_WORKER_CORES,限制每個worker的cpu數量
SPARK_WORKER_DIR spark作業的工作目錄,包括了作業的日誌等,預設是spark_home/work
SPARK_WORKER_OPTS worker的額外引數,使用"-Dx=y"設定各個引數
SPARK_DAEMON_MEMORY 分配給master和worker程式自己本身的記憶體,預設是1g
SPARK_DAEMON_JAVA_OPTS 設定master和worker自己的jvm引數,使用"-Dx=y"設定各個引數
SPARK_PUBLISC_DNS master和worker的公共dns域名,預設是沒有的
  • SPARK_MASTER_OPTS
    設定master的額外引數,使用-Dx=y設定各個引數
    eg:export SPARK_MASTER_OPTS="-Dspark.deploy.defaultCores=1"

    引數名 預設值 含義
    spark.deploy.retainedApplications 200 在spark web ui上最多顯示多少個application的資訊
    spark.deploy.retainedDrivers 200 在spark web ui上最多顯示多少個driver的資訊
    spark.deploy.spreadOut true 資源排程策略,spreadOut會盡量將application的executor程式分佈在更多worker上,適合基於hdfs檔案計算的情況,提升資料本地化概率;非spreadOut會盡量將executor分配到一個worker上,適合計算密集型的作業
    spark.deploy.defaultCores 無限大 每個spark作業最多在standalone叢集中使用多少個cpu core,預設是無限大,有多少用多少
    spark.deploy.timeout 60 單位秒,一個worker多少時間沒有響應之後,master認為worker掛掉了
  • SPARK_WORKER_OPTS
    worker的額外引數

    引數名 預設值 含義
    spark.worker.cleanup.enabled false 是否啟動自動清理worker工作目錄,預設是false
    spark.worker.cleanup.interval 1800 單位秒,自動清理的時間間隔,預設是30分鐘
    spark.worker.cleanup.appDataTtl 7 * 24 * 3600 預設將一個spark作業的檔案在worker工作目錄保留多少時間,預設是7天

Spark Application執行

local 模式

主要用於本機測試

/usr/local/spark/bin/spark-submit \
--class cn.spark.study.core.xxx \
--num-executors 3 \
--driver-memory 100m \
--executor-memory 100m \
--executor-cores 2 \
/usr/local/test/xxx.jar \
複製程式碼

standalone模式

引數設定

standalone模式與local區別,就是要將master設定成spark://master_ip:port,如spark://192.168.0.103:7077

  1. 程式碼:val spark = SparkSession.builder().master("spark://IP:PORT")...
  2. spark-submit: --master spark://IP:PORT --deploy-mode client/cluster
    預設client模式
  3. spark-shell: --master spark://IP:PORT:用於實驗和測試
    /usr/local/spark/bin/spark-submit \
    --class cn.spark.study.core.xxx \
    --master spark://192.168.0.103:7077 \
    --deploy-mode client \
    --num-executors 1 \
    --driver-memory 100m \
    --executor-memory 100m \
    --executor-cores 1 \
    /usr/local/test/xxx.jar \
    複製程式碼

--master:

  1. 不設定:local模式
  2. spark://xxx:standalone模式,會提交到指定的URL的Master程式上去
  3. yarn-xxx:yarn模式,會讀取hadoop配置檔案,然後連線ResourceManager

standalone client模式作業程式

提交執行作業後,立即使用jps檢視程式,可以看到啟動了如下程式

  1. SparkSubmit: driver程式,在本機上啟動(spark-submit所在的機器)
  2. CoarseGrainedExecutorBackend(內部持有一個Executor物件,CoarseGrainedExecutorBackend即executor程式): 在執行spark作業的worker機器上,給作業分配和啟動一個executor程式
    SparkSubmit給CoarseGrainedExecutorBackend分配task

standalone cluster模式

standalone cluster模式支援監控driver程式,並且在driver掛掉的時候,自動重啟該程式,主要是用於spark streaming中的HA高可用性,spark-submit指令碼中,使用--supervise標識即可

要殺掉反覆掛掉的driver程式bin/spark-class org.apache.spark.deploy.Client kill <master url> <driver ID>,通過http://<maser url>:8080可檢視到driver id

yarn下殺掉applicationyarn application -kill applicationid

程式:

  1. SparkSubmit短暫執行,只是將driver註冊到master上,由master來啟動driver,馬上就停止;
  2. 在Worker上,會啟動DriverWrapper程式
  3. 如果能夠申請到足夠的cpu資源,會在其他worker上,啟動CoarseGrainedExecutorBackend程式
...
--deploy-mode cluster \
--num-executors 1 \
--executor-cores 1 \
...
複製程式碼

cluster模式下

  1. worker啟動driver,佔用一個cpu core
  2. driver去跟master申請資源,在有空閒cpu資源的worker上啟動一個executor程式

cpu core太少,可能導致executor無法啟動,一直waiting,比如只有一個worker,一個cpu core時

在 cluster 模式下,driver 是在叢集中的某個 Worker中的程式中啟動,並且 client程式將會在完成提交應用程式的任務之後退出,而不需要等待應用程式完成再退出

standalone多作業資源排程

預設提交的每一個spark作業都會嘗試使用叢集中所有可用的cpu資源,此時只能支援作業序列起來執行,所以standalone叢集對於同時提交上來的多個作業,僅僅支援FIFO排程策略

  • 設定多作業同時執行
    可以設定spark.cores.max引數,限制每個作業能夠使用的最大的cpu core數量,讓作業不會使用所有的cpu資源,後面提交上來的作業就可以獲取到資源執行,預設情況下,它將獲取叢集中的 all cores (核),這隻有在某一時刻只允許一個應用程式執行時才有意義
  1. spark.conf.set("spark.cores.max", "num")
  2. 提交指令碼命令spark-submit: --master spark://IP:PORT --conf spark.cores.max=num
  3. spark-env.sh全域性配置:export SPARK_MASTER_OPTS="-Dspark.deploy.defaultCores=num" 預設數量

standalone web ui

spark standalone模式預設在master機器上的8080埠提供web ui,可以通過配置spark-env.sh檔案等方式,來配置web ui的埠,地址如spark://192.168.0.103:8080

spark yarn模式下應該在YARN web ui上檢視,如http://192.168.0.103:8088/

  • application web ui

application detail ui在作業的driver所在的機器的4040埠

  • 作業層面
    可以用於具體定位問題,如
    1. task資料分佈不均勻:資料傾斜
    2. stage執行時間長:根據stage劃分演算法,定位stage對應的程式碼,去優化效能
    3. 每個作業在每個executor上的日誌
      stdout:System.out.println;
      stderr:System.err.println和系統級別log

      作業執行完,資訊消失,需要啟動history server

yarn模式

前提:spark-env.sh檔案中,配置HADOOP_CONF_DIR或者YARN_CONF_DIR屬性,值為hadoop的配置檔案目錄HADOOP_HOME/etc/hadoop,其中包含了hadoop和yarn所有的配置檔案,比如hdfs-site、yarn-site等
用途:spark讀寫hdfs,連線到yarn resourcemanager上

兩種執行模式

  • yarn-client模式
    driver程式會執行在提交作業的機器上,ApplicationMaster僅僅只是負責為作業向yarn申請資源(executor)而已,driver還是會負責作業排程
  • yarn-cluster模式
    driver程式會執行在yarn叢集的某個工作節點上,作為一個ApplicationMaster程式執行

檢視yarn日誌

日誌散落在叢集中各個機器上,引數配置yarn-site.xml

  1. 聚合日誌方式(推薦)
    屬性設定 含義
    yarn.log-aggregation-enable=true container的日誌會拷貝到hdfs上去,並從機器中刪除
    yarn.nodemanager.remote-app-log-dir 當應用程式執行結束後,日誌被轉移到的HDFS目錄(啟用日誌聚集功能時有效)
    yarn.nodemanager.remote-app-log-dir-suffix 遠端日誌目錄子目錄名稱(啟用日誌聚集功能時有效)
    yarn.log-aggregation.retain-seconds 聚合後的日誌在HDFS上儲存多長時間,單位為s
    yarn logs -applicationId <app ID> 檢視日誌,yarn web ui上可以檢視到applicationId(也可以直接在hdfs上檢視日誌檔案)
    yarn.nodemanager.log.retain-second 不啟用日誌聚合此引數生效,日誌檔案儲存在本地的時間,單位為s
    yarn.log-aggregation.retain-check-interval-seconds 隔多久刪除過期的日誌
  2. web ui檢視
    需要啟動History Server,執行spark history server和mapreduce history server
    不做配置就只能檢視到正在執行的日誌
    配置見Spark History Web UI一節
  3. 分散檢視
    預設日誌在YARN_APP_LOGS_DIR目錄下,如/tmp/logs或者$HADOOP_HOME/logs/userlogs
    如果yarn叢集中沒有開啟History Server,想要檢視system.out日誌,需要在yarn-site.xml檔案中設定yarn.log.aggregation-enable值為ture(將日誌拷貝到hdfs上),檢視時通過yarn logs -applicationId xxx在機器上檢視

提交指令碼

/usr/local/spark/bin/spark-submit \
--class xxx \
# 自動從hadoop配置目錄中的配置檔案中讀取cluster manager地址
--master yarn-cluster/yarn-client \ 
--num-executors 1 \
--driver-memory 100m \
--executor-memory 100m \
--executor-cores 1 \
--conf <key>=<value> \
# 指定不同的hadoop佇列,專案或部門之間佇列隔離
--queue hadoop佇列 \
/usr/local/test/xxx.jar \
${1}
複製程式碼

--conf: 配置所有spark支援的配置屬性,使用key=value的格式;如果value中包含了空格,那麼需要將key=value包裹的雙引號中--conf "<key>=<value>"
application-jar: 打包好的spark工程jar包,在當前機器上的全路徑名
application-arguments: 傳遞給主類的main方法的引數; 在shell中用${1}佔位符接收傳遞給shell的引數;在java中可以通過main方法的args[0]等引數獲取,提交spark應用程式時,用 ./指令碼.sh 引數值

yarn模式執行spark作業屬性

可以在提交指令碼上--conf設定屬性

屬性名稱 預設值 含義
spark.yarn.am.memory 512m client模式下,YARN Application Master使用的記憶體總量
spark.yarn.am.cores 1 client模式下,Application Master使用的cpu數量
spark.driver.cores 1 cluster模式下,driver使用的cpu core數量,driver與Application Master執行在一個程式中,所以也控制了Application Master的cpu數量
spark.yarn.am.waitTime 100s cluster模式下,Application Master要等待SparkContext初始化的時長; client模式下,application master等待driver來連線它的時長
spark.yarn.submit.file.replication hdfs副本數 作業寫到hdfs上的檔案的副本數量,比如工程jar,依賴jar,配置檔案等,最小一定是1
spark.yarn.preserve.staging.files false 如果設定為true,那麼在作業執行完之後,會避免工程jar等檔案被刪除掉
spark.yarn.scheduler.heartbeat.interval-ms 3000 application master向resourcemanager傳送心跳的間隔,單位ms
spark.yarn.scheduler.initial-allocation.interval 200ms application master在有pending住的container分配需求時,立即向resourcemanager傳送心跳的間隔
spark.yarn.max.executor.failures executor數量*2,最小3 整個作業判定為失敗之前,executor最大的失敗次數
spark.yarn.historyServer.address spark history server的地址
spark.yarn.dist.archives 每個executor都要獲取並放入工作目錄的archive
spark.yarn.dist.files 每個executor都要放入的工作目錄的檔案
spark.executor.instances 2 預設的executor數量
spark.yarn.executor.memoryOverhead executor記憶體10% 每個executor的堆外記憶體大小,用來存放諸如常量字串等東西
spark.yarn.driver.memoryOverhead driver記憶體7% 同上
spark.yarn.am.memoryOverhead AM記憶體7% 同上
spark.yarn.am.port 隨機 application master埠
spark.yarn.jar spark jar檔案的位置
spark.yarn.access.namenodes spark作業能訪問的hdfs namenode地址
spark.yarn.containerLauncherMaxThreads 25 application master能用來啟動executor container的最大執行緒數量
spark.yarn.am.extraJavaOptions application master的jvm引數
spark.yarn.am.extraLibraryPath application master的額外庫路徑
spark.yarn.maxAppAttempts 提交spark作業最大的嘗試次數
spark.yarn.submit.waitAppCompletion true cluster模式下,client是否等到作業執行完再退出

關於master的高可用方案

standalone模式下排程器依託於master程式來做出排程決策,這可能會造成單點故障:如果master掛掉了,就沒法提交新的應用程式了。
為了解決這個問題,spark提供了兩種高可用性方案,分別是基於zookeeper的HA方案(推薦)以及基於檔案系統的HA方案。

基於zookeeper的HA方案

概述

使用zookeeper來提供leader選舉以及一些狀態儲存,可以在叢集中啟動多個master程式,讓它們連線到zookeeper例項。其中一個master程式會被選舉為leader,其他的master會被指定為standby模式。
如果當前的leader master程式掛掉了,其他的standby master會被選舉,從而恢復舊master的狀態。

配置

在啟動一個zookeeper叢集之後,在多個節點上啟動多個master程式,並且給它們相同的zookeeper 配置(zookeeper url和目錄)。master就可以被動態加入master叢集,並可以在任何時間被移除掉

spark-env.sh檔案中,設定SPARK_DAEMON_JAVA_OPTS選項:

  1. spark.deploy.recoveryMode:設定為ZOOKEEPER來啟用standby master恢復模式(預設為NONE)
  2. spark.deploy.zookeeper.url:zookeeper叢集url
  3. spark.deploy.zookeeper.dir:zookeeper中用來儲存恢復狀態的目錄(預設是/spark
export SPARK_DAEMON_JAVA_OPTS="-Dspark.deploy.recoveryMode=ZOOKEEPER -Dspark.deploy.zookeeper.url=192.168.0.103:2181,192.168.0.104:2181 -Dspark.deploy.zookeeper.dir=/spark"
複製程式碼

如果在叢集中啟動了多個master節點,但是沒有正確配置master去使用zookeeper,master在掛掉進行恢復時是會失敗的,因為沒法發現其他master,並且都會認為自己是leader。這會導致叢集的狀態不是健康的,因為所有master都會自顧自地去排程。

細節

為了排程新的應用程式或者向叢集中新增worker節點,它們需要知道當前leader master的ip地址,這可以通過傳遞一個master列表來完成。可以將SparkSession 的master連線的地址指向spark://host1:port1,host2:port2。這就會導致SparkSession嘗試去註冊所有的master,如果host1掛掉了,那麼配置還是正確的,因為會找到新的leader master

當一個應用程式啟動的時候,或者worker需要被找到並且註冊到當前的leader master的時候。一旦它成功註冊了,就被儲存在zookeeper中了。如果故障發生了,new leader master會去聯絡所有的之前註冊過的應用程式和worker,並且通知它們master的改變。應用程式甚至在啟動的時候都不需要知道new master的存在。

故而,new master可以在任何時間被建立,只要新的應用程式和worker可以找到並且註冊到master即可

在其他節點啟動備用master:./start-master.sh

基於檔案系統的HA方案

概述

FILESYSTEM模式:當應用程式和worker都註冊到master之後,master就會將它們的資訊寫入指定的檔案系統目錄中,以便於重啟時恢復註冊的應用程式和worker狀態;
需要手動重啟

配置

spark-env.sh中設定SPARK_DAEMON_JAVA_OPTS

  1. spark.deploy.recoveryMode:設定為FILESYSTEM來啟用單點恢復(預設值為NONE)
  2. spark.deploy.recoveryDirectory:spark儲存狀態資訊的檔案系統目錄,必須是master可以訪問的目錄

eg:

export SPARK_DAEMON_JAVA_OPTS="-Dspark.deploy.recoveryMode=FILESYSTEM -Dspark.deploy.recoveryDirectory=/usr/local/spark_recovery"
複製程式碼

細節

  1. 該模式更加適合於開發和測試環境
  2. stop-master.sh指令碼殺掉一個master程式是不會清理它的恢復狀態,當重啟一個新的master程式時,它會進入恢復模式。需要等待之前所有已經註冊的worker等節點先timeout才能恢復。
  3. 可以使用一個NFS目錄(類似HDFS)作為恢復目錄。如果原先的master節點掛掉,可以在其他節點上啟動一個master程式,它會正確地恢復之前所有註冊的worker和應用程式。之後的應用程式可以找到新的master,然後註冊。

saprk作業監控

作業監控方式:Spark Web UI,Spark History Web UI,RESTFUL API以及Metrics

Spark Web UI

每提交Spark作業並啟動SparkSession後,會啟動一個對應的Spark Web UI服務。預設情況下Spark Web UI的訪問地址是driver程式所在節點的4040埠,如http://<driver-node>:4040

Spark Web UI包括了以下資訊:

  1. stage和task列表
  2. RDD大小以及記憶體使用的概覽
  3. 環境資訊
  4. 作業對應的executor的資訊

如果多個driver在一個機器上執行,它們會自動繫結到不同的埠上。預設從4040埠開始,如果發現已經被繫結,那麼會選擇4041、4042等埠,以此類推。

這些資訊預設情況下在作業執行期間有效,一旦作業完畢,driver程式以及對應的web ui服務也會停止。如果要在作業完成之後,也可以看到其Spark Web UI以及詳細資訊,需要啟用Spark的History Server。

Spark History Web UI

  1. 建立日誌儲存目錄
    建立的目錄hdfs://ip:port/dirName
    命令hdfs dfs -mkidr /dirName
  2. 修改spark-defaults.conf
    spark.eventLog.enabled  true    #啟用
    spark.eventLog.dir      hdfs://ip:port/dirName
    spark.eventLog.compress true    #壓縮
    複製程式碼
  3. 修改spark-env.sh
    export SPARK_HISTORY_OPTS="-Dspark.history.ui.port=18080 -Dspark.history.retainedApplications=50 -Dspark.history.fs.logDirectory=hdfs://ip:port/dirName"
    複製程式碼

    spark.eventLog.dir指定作業事件記錄地址
    spark.history.fs.logDirectory指定從哪個目錄中去讀取作業資料
    兩個目錄地址要相同

  4. 啟動HistoryServer
    ./sbin/start-history-server.sh 在啟動介面可以看到history-server的訪問地址,通過訪問地址開啟History Web UI

RESTFUL API

提供了RESTFUL API來返回關於日誌的json資料

API 含義
/applications 獲取作業列表
/applications/[app-id]/jobs 指定作業的job列表
/applications/[app-id]/jobs/[job-id] 指定job的資訊
/applications/[app-id]/stages 指定作業的stage列表
/applications/[app-id]/stages/[stage-id] 指定stage的所有attempt列表
/applications/[app-id]/stages/[stage-id]/[stage-attempt-id] 指定stage attempt的資訊
/applications/[app-id]/stages/[stage-id]/[stage-attempt-id]/taskSummary 指定stage attempt所有task的metrics統計資訊
/applications/[app-id]/stages/[stage-id]/[stage-attempt-id]/taskList 指定stage attempt的task列表
/applications/[app-id]/executors 指定作業的executor列表
/applications/[app-id]/storage/rdd 指定作業的持久化rdd列表
/applications/[app-id]/storage/rdd/[rdd-id] 指定持久化rdd的資訊
/applications/[app-id]/logs 下載指定作業的所有日誌的壓縮包
/applications/[app-id]/[attempt-id]/logs 下載指定作業的某次attempt的所有日誌的壓縮包

eg:http://192.168.0.103:18080/api/v1/applications

作業資源排程

靜態資源分配

  1. application並行:每個spark application都會執行自己獨立的一批executor程式,用於執行task和儲存資料,此時叢集管理器會提供同時排程多個application的功能
  2. job並行:在每個spark application內部,多個job也可以並行執行

同時提交多個spark application

預設的作業間資源分配策略為靜態資源分配,在這種方式下,每個作業都會被給予一個它能使用的 最大資源量的限額,並且可以在執行期間持有這些資源。這是spark standalone叢集和YARN叢集使用的預設方式。

  • Standalone叢集
    預設情況下,提交到standalone叢集上的多個作業,會通過FIFO的方式來執行,每個作業都會嘗試獲取所有的資源。
    spark.cores.max:限制每個作業能夠使用的cpu core最大數量
    spark.deploy.defaultCores:設定每個作業預設cpu core使用量
    spark.executor.memory:設定每個作業最大記憶體。

  • YARN
    --num-executors:配置作業可以在叢集中分配到多少個executor
    --executor-memory--executor-cores可以控制每個executor能夠使用的資源。

沒有一種cluster manager可以提供多個作業間的記憶體共享功能,需要共享記憶體,可以單獨使用一個服務(例如:alluxio),這樣就能實現多應用訪問同一個RDD的資料。

動態資源分配

當資源被分配給了一個作業,但資源有空閒,可以將資源還給cluster manager的資源池,被其他作業使用。在spark中,動態資源分配在executor粒度上被實現,啟用時設定spark.dynamicAllocation.enabled為true,在每個節點上啟動external shuffle service,並將spark.shuffle.service.enabled設為true。external shuffle service 的目的是在移除executor的時候,能夠保留executor輸出的shuffle檔案。

申請策略

spark application會在它有pending(等待執行)的task等待被排程時,申請額外的executor

task已提交但等待排程->executor數量不足

  1. driver輪詢式地申請executor
    當在一定時間內spark.dynamicAllocation.schedulerBacklogTimeout有pending的task時,就會觸發真正的executor申請
  2. 每隔一定時間後spark.dynamicAllocation.sustainedSchedulerBacklogTimeout,如果又有pending的task了,則再次觸發申請操作。
  3. 每一輪申請到的executor數量採用指數級增加(比如1,2,4,8,..):採用指數級增長策略的原因有兩個:
    第一,對於任何一個Spark應用如果只需要多申請少數幾個執行器的話,那麼必須非常謹慎的啟動資源申請,這和TCP慢啟動有些類似;
    第二,如果一旦Spark應用確實需要申請多個執行器的話,那麼可以確保其所需的計算資源及時增長。

移除策略

一個spark作業會在它的executor出現了空閒超過一定時間後(spark.dynamicAllocation.executorIdleTimeout),被移除掉。

這意味著沒有task被pending住,executor有空閒,和申請條件互斥。

儲存中間狀態

spark使用一個外部的shuffle服務來儲存每個executor的中間寫狀態,這個服務是一個長時間執行的程式,叢集的每個節點上都會執行一個,如果服務被啟用,那麼spark executor會在shuffle write和read時,將資料寫入該服務,並從該服務獲取資料。這意味著所有executor寫的shuffle資料都可以在executor宣告週期之外繼續使用。

多了箇中間資料儲存角色,也改變了executor的讀寫方式

除了寫shuffle檔案,executor也會在記憶體或磁碟中持久化資料。當一個executor被移除掉時,所有快取的資料都會消失。

shuffle服務寫入的資料和executor持久化資料不是一個概念?executor移除後/掛掉後,其持久化的資料將消失,而shuffle服務儲存的資料還將存在

standalone模式下動態資源分配

  1. 在worker啟動前設定spark.shuffle.service.enabled為true
  2. application
    --conf spark.dynamicAllocation.enabled=true \
    複製程式碼

Mesos模式下動態資源分配

  1. 在各個節點上執行$SPARK_HOME/sbin/start-mesos-shuffle-service.sh,並設定 spark.shuffle.service.enabled為true
  2. application
    --conf spark.dynamicAllocation.enabled=true \
    複製程式碼

yarn模式下動態資源分配

需要配置yarn的shuffle service(external shuffle service),用於儲存executor的shuffle write檔案,從而讓executor可以被安全地移除.

  1. 新增jar包
    $SPARK_HOME/lib下的spark-<version>-yarn-shuffle.jar加入到所有NodeManager的classpath中,即hadoop/yarn/lib目錄中
  2. 修改yarn-site.xml
    <propert>
        <name>yarn.nodemanager.aux-services</name>
        <value>spark_shuffle</value>
        <!-- <value>mapreduce_shuffle</value> -->
    </property>
    <propert>
        <name>yarn.nodemanager.aux-services.spark_shuffle.class</name>
        <value>org.apache.spark.network.yarn.YarnShuffleService</value>
    </property>
    複製程式碼
  3. 啟動spark application
    --conf spark.shuffle.service.enabled=true \
    --conf spark.shuffle.service.port=7337 \
    --conf spark.dynamicAllocation.enabled=true \
    複製程式碼

參見Configuring the External Shuffle Service

多個job排程

job是一個spark action操作觸發的計算單元,在一個spark作業內部,多個並行的job是可以同時執行的 。

FIFO排程

預設情況下,spark的排程會使用FIFO的方式來排程多個job。每個job都會被劃分為多個stage,而且第一個job會對所有可用的資源獲取優先使用權,並且讓它的stage的task去執行,然後第二個job再獲取資源的使用權,以此類推

Fair排程

在公平的資源共享策略下,spark會將多個job的task使用一種輪詢的方式來分配資源和執行,所以所有的job都有一個基本公平的機會去使用叢集的資源

conf.set("spark.scheduler.mode", "FAIR")
複製程式碼
--conf spark.scheduler.mode=FAIR
複製程式碼

公平排程資源池

fair scheduler也支援將job分成多個組並放入多個池中,以及為每個池設定不同的排程優先順序。這個feature對於將重要的和不重要的job隔離執行的情況非常有用,可以為重要的job分配一個池,並給予更高的優先順序; 為不重要的job分配另一個池,並給予較低的優先順序。

在程式碼中設定sparkContext.setLocalProperty("spark.scheduler.pool", "poolName"),所有在這個執行緒中提交的job都會進入這個池中,設定是以執行緒為單位儲存的,很容易實現用同一執行緒來提交同一使用者的所有作業到同一個資源池中。設定為null則清空池子。

預設情況下,每個池子都會對叢集資源有相同的優先使用權,但是在每個池內,job會使用FIFO的模式來執行。

可以通過配置檔案來修改池的屬性

  1. schedulingMode: FIFO/FAIR,來控制池中的jobs是否要排隊,或者是共享池中的資源
  2. weight: 控制資源池相對其他資源池,可以分配到資源的比例。預設情況下,所有池子的權重都是1.如果將某個資源池的 weight 設為 2,那麼該資源池中的資源將是其他池子的2倍,如果將 weight 設得很高,如 1000,可以實現資源池之間的排程優先順序 – weight=1000 的資源池總能立即啟動其對應的作業。
  3. minShare: 每個資源池最小資源分配值(CPU 個數),公平排程器總是會嘗試優先滿足所有活躍(active)資源池的最小資源分配值,然後再根據各個池子的 weight 來分配剩下的資源。因此,minShare 屬效能夠確保每個資源池都能至少獲得一定量的叢集資源。minShare 的預設值是 0。

配置檔案預設地址spark/conf/fairscheduler.xml,自定義檔案conf.set("spark.scheduler.allocation.file", "/path/to/file")

<allocations>
  <pool name="production">
    <schedulingMode>FAIR</schedulingMode>
    <weight>1</weight>
    <minShare>2</minShare>
  </pool>
  <pool name="test">
    <schedulingMode>FIFO</schedulingMode>
    <weight>2</weight>
    <minShare>3</minShare>
  </pool>
</allocations>
複製程式碼

沒有在配置檔案中配置的資源池都會使用預設配置(schedulingMode : FIFO,weight : 1,minShare : 0)。

Spark常用運算元

union

union運算元

  1. 新的rdd,會將舊的兩個rdd的partition,複製過去
  2. 新的rdd的partition的數量,就是舊的兩個rdd的partition的數量之和

groupByKey

groupByKey
在執行shuffle類的運算元時,運算元內部都會隱式地建立幾個RDD,主要是作為這個操作的一些中間資料的表達,以及作為stage劃分的邊界。

reduceByKey

reduceByKey
reduceByKey VS groupByKey

  • 不同之處
    reduceByKey,中間多了一個MapPartitionsRDD,是本地資料聚合後的rdd,可以減少網路資料傳輸。

  • 相同之處
    read和聚合的過程基本和groupByKey類似。都是ShuffledRDD做shuffle read再聚合,得到最終的rdd

distinct

distinct

  1. 將每個原始值轉換成tuple
  2. 會進行本地聚合(類似reduceByKey)
  3. 最後會將tuple轉換回單值

cogroup

cogroup運算元是其他運算元的基礎,如join,intersection操作

cogroup

先按RDD分割槽聚合結果,(hello,[(1,1),(1,1)]):第1個(1,1)是第一個RDD 的helo聚合結果,第二個(1,1)是第2個RDD聚合結果 若第一個RDD的第一個partition沒有hello,則(1),不是(,1)

intersection

intersection

filter:過濾掉兩個集合中任意一個集合為空的key

join

join

  1. cogroup,聚合兩個rdd的key
  2. flatMap,聚合後的每條資料,都可能返回多條資料 將每個key對應的兩個集合的所有元素,做了一個笛卡爾積

sortByKey

sortByKey

  1. ShuffledRDD,做shuffle read,將相同的key拉到一個http://ozijnir4t.bkt.clouddn.com/spark/learning/sortByKey.pngpartition中來
  2. mapPartitions,對每個partitions內的key進行全域性的排序

cartesian

笛卡爾乘積

cartesian

coalesce

一般用於減少partition數量

coalesce

repartition

repartition運算元=coalesce(true)

repartition

repartition操作在中間生成的隱式RDD中會給值計算出字首作為key,在最後做Shuffle操作時一個partition就放特定的一些key值對應的tuple,完成重分割槽操作

知識點

Spark叢集中的節點個數、RDD分割槽個數、cpu核心個數三者與並行度的關係

並行度數量關係

  1. 每個file包含多個block
  2. Spark讀取輸入檔案時,會根據具體資料格式對應的InputFormat進行解析,一般是將若干個Block合併成一個輸入分片,稱為InputSplit,注意InputSplit不能跨越檔案
  3. 一個InputSplit生成一個task
  4. 每個Executor由若干core組成,每個Executor的每個core一次只能執行一個Task
  5. 每個Task執行後生成了目標RDD的一個partiton

如果partition的數量多,能起例項的資源也多,那自然併發度就多
如果partition數量少,資源很多,則task數量不足,它也不會有很多併發
如果partition的數量很多,但是資源少(如core),那麼併發也不大,會算完一批再繼續起下一批

Task被執行的併發度 = Executor數目 * 每個Executor核數
複製程式碼

這裡的core是虛擬的core而不是機器的物理CPU核,可以理解為就是Executor的一個工作執行緒?
每個executor的core數目通過spark.executor.cores引數設定。這裡的cores其實是指的工作執行緒。cpu info裡看到的核數是物理核(或者一般機器開了超執行緒以後是的物理核數*2),和spark裡的core不是一個概念,但是一般來說spark作業配置的executor核數不應該超過機器的物理核數。

partition的數目

  1. 資料讀入階段,如sc.textFile,輸入檔案被劃分為多少InputSplit就會需要多少初始Task
  2. Map階段partition數目保持不變
  3. Reduce階段,RDD的聚合會觸發shuffle操作,聚合後的RDD的partition數目跟具體操作有關,例如repartition操作會聚合成指定分割槽數,還有一些運算元是可配置的

參考文獻

  1. 詳細探究Spark的shuffle實現
  2. Spark效能優化:資源調優篇
  3. 在Spark叢集中,叢集的節點個數、RDD分割槽個數、cpu核心個數三者與並行度的關係
  4. Spark筆記-repartition和coalesce
  5. Spark 2.0系列之SparkSession詳解
  6. YARN日誌聚合相關引數配置
  7. 深入理解Spark 2.1 Core (一):RDD的原理與原始碼分析
  8. Spark 2.0從入門到精通
  9. spark 2.2.0文件

相關文章