Spark集群
一組計算機的集合,每個計算機節點作為獨立的計算資源,又可以虛擬出多個具備計算能力的虛擬機,這些虛擬機是集群中的計算單元。Spark的核心模塊專注于調度和管理虛擬機之上分布式計算任務的執行,集群中的計算資源則交給Cluster Manager這個角色來管理,Cluster Manager可以為自帶的Standalone、或第三方的Yarn和Mesos。
Cluster Manager一般采用Master-Slave結構。以Yarn為例,部署ResourceManager服務的節點為Master,負責集群中所有計算資源的統一管理和分配;部署NodeManager服務的節點為Slave,負責在當前節點創建一個或多個具備獨立計算能力的JVM實例,在Spark中,這些節點也叫做Worker。
另外還有一個Client節點的概念,是指用戶提交Spark Application時所在的節點。
Application
用戶自己寫的Spark應用程序,批處理作業的集合。Application的main方法為應用程序的入口,用戶通過Spark的API,定義了RDD和對RDD的操作。這里可以參考一段定義:
可以認為應用是多次批量計算組合起來的過程,在物理上可以表現為你寫的程序包+部署配置。應用的概念類似于計算機中的程序,它只是一個藍本,尚沒有運行起來?!?a target="_blank" rel="nofollow">spark學習筆記三:spark原理介紹
SparkContext
Spark最重要的API,用戶邏輯與Spark集群主要的交互接口,它會和Cluster Master交互,包括向它申請計算資源等。
Driver和Executor
Spark在執行每個Application的過程中會啟動Driver和Executor兩種JVM進程:
- Driver進程為主控進程,負責執行用戶Application中的main方法,提交Job,并將Job轉化為Task,在各個Executor進程間協調Task的調度。
- 運行在Worker上的Executor進程負責執行Task,并將結果返回給Driver,同時為需要緩存的RDD提供存儲功能。
圖片來源 - Spark Cluster Mode Overview
Spark有Client和Cluster兩種部署Application的模式,Application以以Client模式部署時,Driver運行于Client節點,而以Cluster模式部署時,Driver運行于Worker節點,與Executor一樣由Cluster Manager啟動。
RDD
彈性分布式數據集,只讀分區記錄的集合,Spark對所處理數據的基本抽象。Spark中的計算可以簡單抽象為對RDD的創建、轉換和返回操作結果的過程:
- 創建
通過加載外部物理存儲(如HDFS)中的數據集,或Application中定義的對象集合(如List)來創建。RDD在創建后不可被改變,只可以對其執行下面兩種操作。 - 轉換(Transformation)
對已有的RDD中的數據執行計算進行轉換,而產生新的RDD,在這個過程中有時會產生中間RDD。Spark對于Transformation采用惰性計算機制,遇到Transformation時并不會立即計算結果,而是要等遇到Action時一起執行。 - 行動(Action)
對已有的RDD中的數據執行計算產生結果,將結果返回Driver程序或寫入到外部物理存儲。在Action過程中同樣有可能生成中間RDD。
Partition(分區)
一個RDD在物理上被切分為多個Partition,即數據分區,這些Partition可以分布在不同的節點上。Partition是Spark計算任務的基本處理單位,決定了并行計算的粒度,而Partition中的每一條Record為基本處理對象。例如對某個RDD進行map操作,在具體執行時是由多個并行的Task對各自分區的每一條記錄進行map映射。
Dependency(依賴)
對RDD的Transformation或Action操作,讓RDD產生了父子依賴關系(事實上,Transformation或Action操作生成的中間RDD也存在依賴關系),這種依賴分為寬依賴和窄依賴兩種:
- NarrowDependency (窄依賴)
parent RDD中的每個Partition最多被child RDD中的一個Partition使用。讓RDD產生窄依賴的操作可以稱為窄依賴操作,如map、union。 - WideDependency (或ShuffleDependency,寬依賴)
parent RDD中的每個Partition被child RDD中的多個Partition使用,這時會依據Record的key進行數據重組,這個過程即為Shuffle(洗牌)。讓RDD產生寬依賴的操作可以稱為寬依賴操作,如reduceByKey, groupByKey。
Spark根據用戶Application中的RDD的轉換和行動,生成RDD之間的依賴關系,RDD之間的計算鏈構成了RDD的血統(Lineage),同時也生成了邏輯上的DAG(有向無環圖)。每一個RDD都可以根據其依賴關系一級一級向前回溯重新計算,這便是Spark實現容錯的一種手段:
RDD的每次轉換都會生成一個新的RDD,所以RDD之間就會形成類似于流水線一樣的前后依賴關系。在部分分區數據丟失時,Spark可以通過這個依賴關系重新計算丟失的分區數據,而不是對RDD的所有分區進行重新計算。
——《Spark技術內幕》-第3章-RDD實現詳解
Job
在一個Application中,以Action為劃分邊界的Spark批處理作業。前面提到,Spark采用惰性機制,對RDD的創建和轉換并不會立即執行,只有在遇到第一個Action時才會生成一個Job,然后統一調度執行。一個Job包含N個Transformation和1個Action。
Shuffle
有一部分Transformation或Action會讓RDD產生寬依賴,這樣過程就像是將父RDD中所有分區的Record進行了“洗牌”(Shuffle),數據被打散重組,如屬于Transformation操作的join,以及屬于Action操作的reduce等,都會產生Shuffle。
Stage
一個Job中,以Shuffle為邊界劃分出的不同階段。每個階段包含一組可以被串行執行的窄依賴或寬依賴操作:
用戶提交的計算任務是一個由RDD構成的DAG,如果RDD在轉換的時候需要做Shuffle,那么這個Shuffle的過程就將這個DAG分為了不同的階段(即Stage)。由于Shuffle的存在,不同的Stage是不能并行計算的,因為后面Stage的計算需要前面Stage的Shuffle的結果。
——《Spark技術內幕》-第4章-Scheduler模塊詳解
在對Job中的所有操作劃分Stage時,一般會按照倒序進行,即從Action開始,遇到窄依賴操作,則劃分到同一個執行階段,遇到寬依賴操作,則劃分一個新的執行階段,且新的階段為之前階段的parent,然后依次類推遞歸執行。child Stage需要等待所有的parent Stage執行完之后才可以執行,這時Stage之間根據依賴關系構成了一個大粒度的DAG。
在一個Stage內,所有的操作以串行的Pipeline的方式,由一組Task完成計算。
Task
對一個Stage之內的RDD進行串行操作的計算任務。每個Stage由一組并發的Task組成(即TaskSet),這些Task的執行邏輯完全相同,只是作用于不同的Partition。一個Stage的總Task的個數由Stage中最后的一個RDD的Partition的個數決定。
Spark Driver會根據數據所在的位置分配計算任務,即把所有Task根據其Partition所在的位置分配給相應的Executor,以盡量減少數據的網絡傳輸(這也就是所謂的移動數據不如移動計算)。一個Executor內同一時刻可以并行執行的Task數由總CPU數/每個Task占用的CPU數
決定,即spark.executor.cores / spark.task.cpus
。
Task分為ShuffleMapTask和ResultTask兩種,位于最后一個Stage的Task為ResultTask,其他階段的屬于ShuffleMapTask。
Persist & Checkpoint
Persist
通過RDD的persist
方法,可以將RDD的分區數據持久化在內存或硬盤中,通過cache
方法則是緩存到內存。這里的persist和cache是一樣的機制,只不過cache是使用默認的MEMORY_ONLY
的存儲級別對RDD進行persist,故“緩存”也就是一種“持久化”。
前面提到,只有觸發了一個Action之后,Spark才會提交Job進行真正的計算。所以RDD只有經過一次Action之后,才能將RDD持久化,然后在Job間共享,即如果兩個Job用到了相同的RDD,那么可以在第一個Job中對這個RDD進行緩存,在第二個Job中就避免了RDD的重新計算。持久化機制使需要訪問重復數據的Application運行地更快,是能夠提升Spark運算速度的一個重要功能。
Checkpoint
調用RDD的checkpoint
方法,可以將RDD保存到外部存儲中,如硬盤或HDFS。Spark引入checkpoint機制,是因為持久化的RDD的數據有可能丟失或被替換,checkpoint可以在這時候發揮作用,避免重新計算。
創建checkpoint是在當前Job完成后,由另外一個專門的Job完成:
也就是說需要checkpoint的RDD會被計算兩次。因此,在使用rdd.checkpoint()的時候,建議加上rdd.cache(),這樣第二次運行的Job久不用再去計算該rdd了。
——Apache Spark的設計與實現- Cache和Checkpoint功能
一個Job在開始處理RDD的Partition時,或者更準確點說,在Executor中運行的任務在獲取Partition數據時,會先判斷是否被持久化,在沒有命中時再判斷是否保存了checkpoint,如果沒有讀取到則會重新計算該Partition。
案例分析
這里借用@JerryLead的ComplexJob案例做一下分析:
object complexJob {
def main(args: Array[String]) {
val sc = new SparkContext("local", "ComplexJob test")
val data1 = Array[(Int, Char)](
(1, 'a'), (2, 'b'),
(3, 'c'), (4, 'd'),
(5, 'e'), (3, 'f'),
(2, 'g'), (1, 'h'))
val rangePairs1 = sc.parallelize(data1, 3)
val hashPairs1 = rangePairs1.partitionBy(new HashPartitioner(3))
val data2 = Array[(Int, String)]((1, "A"), (2, "B"),
(3, "C"), (4, "D"))
val pairs2 = sc.parallelize(data2, 2)
val rangePairs2 = pairs2.map(x => (x._1, x._2.charAt(0)))
val data3 = Array[(Int, Char)]((1, 'X'), (2, 'Y'))
val rangePairs3 = sc.parallelize(data3, 2)
val rangePairs = rangePairs2.union(rangePairs3)
val result = hashPairs1.join(rangePairs)
result.foreachWith(i => i)((x, i) => println("[result " + i + "] " + x))
println(result.toDebugString)
}
}
作者在這個例子中主要定義了一個對RDD的union和join操作,主要的RDD之間的關系如下圖所示:
Job的物理執行圖:
圖片來源 - Job 物理執行圖
參考作者畫的物理執行圖,我們可以觀察到:
- 該Application僅有一個Job,由foreachWith這個Action觸發。
- 這個Job中有三個Stage,partitionBy操作對RDD重新分區產生了Shuffle,是劃分Stage0和Stage1的邊界。join操作則是Stage2和Stage0的邊界。
- 每個Stage的Task總數等于該階段的最后一個RDD的Partition個數。
- 每個Task都是串行執行一個Stage內的所有操作。
- Transformation操作的過程中會產生中間RDD。