目錄
前言
思來想去,還是決定從頭開始寫起,從最基礎的東西入手,研讀Spark Core的源碼。相對于之前東一榔頭西一棒槌地抓重點,這樣應該更能夠由淺入深地建立對Spark核心的認知,寫起來也更加有條理。
除非特殊說明,本系列文章的源碼全部基于當前(2019年3月)最新的Spark 2.3.3版本。
星星之火,可以燎原。作為系列的第0篇,自然要請出大數據領域的Hello World程序——WordCount,以它為起點做一些準備,然后展開對Spark內部設計的探究。
Spark WordCount
Spark源碼自帶示例里只有Java版WordCount。考慮到Spark主要用Scala語言開發,下面給出一份Scala版本的,以它為準。
代碼#0.1 - Scala版WordCount
package me.lmagics.spark.example;
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.rdd.RDD
object ScalaWordCount {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("ScalaWordCount").setMaster("local[2]")
val sc: SparkContext = new SparkContext(conf)
val lines: RDD[String] = sc.textFile(args(0))
val words: RDD[String] = lines.flatMap(line => line.split(" "))
val ones: RDD[(String, Int)] = words.map(word => (word, 1))
val counts: RDD[(String, Int)] = ones.reduceByKey((i1, i2) => i1 + i2)
val result: Array[(String, Int)] = counts.collect()
for ((word, count) <- result) {
println(word + "\t" + count)
}
sc.stop()
}
}
為了方便理解,代碼中還顯式標出了每個變量的類型。實際編碼時可以不用這樣做,因為Scala支持類型推斷。
下面,通過WordCount程序中出現的要點,先來預熱(或者說復習?)一下Spark中最基礎的那些東西。
SparkConf
SparkConf類負責管理Spark的所有配置參數。用戶可以通過SparkConf類的實例來自行設置或獲取配置參數。
SparkContext
SparkContext類是一切Spark程序的總入口,它接受SparkConf中定義的配置參數,并完成大量的初始化工作。只有SparkContext初始化完成之后,Spark程序才能真正準備好運行。
RDD
RDD的正式名稱為彈性分布式數據集(Resilient distributed dataset),是Spark中最重要的概念之一。Spark官方文檔中對它的定義是:可以并行操作的、容錯的元素集合。
除了可并行操作、容錯兩點之外,RDD還具有一些其他特點,如:不可變性(只能生成或轉換,不能直接修改),分區性(內部數據會劃分為Partition),以及名稱中的彈性(可以靈活利用內存和外存)。
要生成RDD,有兩種方法。一是對內存中存在的數據執行并行化(Parallelize)操作,二是從外部存儲(例如HDFS、HBase、Cassandra等等)中的數據源讀取并生成。代碼#0.1中的RDD lines就是通過調用SparkContext.textFile()方法,從外部存儲中的文本文件生成的。
RDD操作(算子)
官方文檔中將類似flatMap()、map()、reduceByKey()、collect()這樣在RDD上定義的,對數據操作的方法統稱為Operation。但在中文環境中,可能是為了適應Scala函數式編程的特點,經常稱作“算子”。為了統一,之后再提及RDD上的這類方法,就全部叫做“算子”。
算子有兩類,即轉換(Transformation)算子與動作(Action)算子。
轉換算子用于對一個RDD施加一系列邏輯,使之變成另一個RDD。flatMap()、map()、reduceByKey()都是轉換算子;動作算子用于真正執行RDD轉換邏輯的計算,并返回其處理結果。collect()就是動作算子。
換句話說,轉換算子是懶執行(延遲執行)的,需要動作算子來觸發。如果沒有動作算子,那么RDD的轉換邏輯只會被“記憶”,而不會生效。
Spark Web UI
Spark Web UI是Spark中的兩個主要監控組件之一,另外一個是度量系統(Metrics System,下圖#0.2能看到)。通過Web UI可以直觀地觀察到Spark程序的運行狀況、資源狀態,以及度量系統收集來的各項監控指標,是調試Spark程序的基礎工具。
以代碼#0.1的WordCount程序為例,為方便講解,它采用最簡單的本地(Local)部署模式運行,而沒有使用集群。在它運行的同時,打開本機瀏覽器并訪問localhost:4040,就可以看到Web UI。4040是Web UI默認的端口。
下圖示出Web UI中的Jobs與Stages頁面。
這兩張圖中也有不少需要注意的點,來梳理一下。
Application
Application(應用)就是用戶使用Spark API編寫,并提交到Spark部署、執行的程序。一個應用即對應一個JVM,圖中的“ScalaWordCount”就是應用的名稱。
Job
Job(作業)是Spark調度體系中的高層。在Spark應用代碼中,一個動作算子就標志著一個作業的提交,其中包含該動作算子本身,以及它負責的前面所有轉換邏輯。示例的WordCount程序中只有一個動作算子,因此它只有一個Job 0。
Stage
Stage(階段)是Spark調度體系中的中間層,作業可以包含多個階段。階段的劃分是由Shuffle與Shuffle依賴決定的,下面也會簡要介紹Shuffle。圖#0.1顯示Job 0劃分為兩個Stage 0和1。
Spark中有兩種Stage,即ShuffleMapStage與ResultStage。
Task
Task(任務)是Spark調度體系中的低層,階段通常也都會包含多個任務,任務的數量則與RDD的分區數量有關。圖#0.1顯示每個Stage下又包含2個Task。圖#0.2顯示Stage 0下Task的詳情,以及對應的監控指標。
Spark中也有兩種Task,與Stage對應,分別為ShuffleMapTask與ResultTask。這種設計與Hadoop MapReduce中的Map任務和Reduce任務有點相似,但根本上還是不同的。
Shuffle
Shuffle的中文意譯是“洗牌”。它是所有采用類MapReduce思想的大數據框架計算過程的必經階段,處于Map與Reduce中間,非常重要。
它可以分為Shuffle write與Shuffle read兩個子階段。Shuffle write即Map階段的最后,輸出上游計算產生的中間數據。Shuffle read即Reduce階段的開頭,拉取中間數據用于下游計算。Shuffle一般會涉及大量的內存、磁盤讀寫及網絡傳輸,因此也是各個大數據框架的優化重點。
圖#0.1的右下角顯示出Stage 0的Shuffle write及Stage 1的Shuffle read數據量。
DAG與RDD依賴
DAG即有向無環圖(Directed acyclic graph),是個圖論概念。如果一個有向圖從任意頂點出發,均無法經過若干條邊回到該點,則這個圖是一個有向無環圖。關于它的細節可以參考其他資料,如英文維基。
在Spark中,DAG被用來存儲各個RDD之間的依賴關系,或者叫做“血緣”(Lineage),更能表達“一個RDD如何從其他RDD轉換過來”的含義。它是RDD容錯性的基礎,一旦數據丟失或Task出錯,可以根據DAG中的Lineage重算。圖#0.1顯示的DAG非常簡單,藍色方塊表示RDD,箭頭則表示Lineage。
RDD的依賴關系在Spark內部用Dependency來表示,分為窄依賴(NarrowDependency)和Shuffle依賴(又稱寬依賴,ShuffleDependency)兩種。窄依賴表示子RDD依賴于父RDD中固定的分區,Shuffle依賴則表示子RDD依賴于父RDD中不定數目的分區。
Executor與Driver
Executor(執行器)與Driver(驅動器)都是JVM進程。Executor負責執行Task的具體計算邏輯,并將計算結果返回給Driver。Driver負責執行Application中的main()方法,SparkContext及其他必需組件的初始化也是在Driver進行。另外,它也向Executor調度Task,以及跟蹤它們的運行狀況。
在圖#0.2的Tasks表格中可以看到Executor ID一列。由于這個WordCount程序是純本地執行的,因此Executor與Driver是同一個節點。
Spark集群
雖然上面的例子是用本地模式演示的,但在生產環境中,我們總會使用多臺服務器組成Spark集群,以最大化Spark的計算性能。官方文檔中提供的Spark集群架構如下圖所示。
Cluster Manager
Cluster Manager即集群管理器,它負責為集群中所有節點分配并管理資源。它根據部署模式的不同,也會有不同的角色。如果采用Standalone模式,即Spark自己管理,那么集群管理器就是Spark集群的Master。如果采用Spark on YARN模式,集群管理器就是YARN中的ResourceManager了。除了Standalone與YARN之外,還可以使用Mesos、Kubernetes作為集群管理器。
Worker
Worker是Spark集群中的工作節點。它負責使用分配的資源創建并持有Executor,以及與Cluster Manager保持通信,通知自己的資源狀況和Executor狀況。在Standalone模式下,Worker稱作Slave,即我們常說的Master-Slaves架構。在Spark on YARN模式下,Worker節點的角色實際會由YARN的NodeManager來替代。
總結
本文以最簡單的WordCount程序為切入點,介紹了代碼中涉及到的基礎知識。然后,通過引入程序執行時的Web UI界面和Spark集群的基礎架構,串聯并總結了其中展示的多個Spark基本概念。
通過對這些知識點的總結和整理,我們可以對Spark Core內部的組成有一個大致的先驗了解。除了SparkConf、SparkContext及它們背后的支撐體系之外,Spark Core還可以分為存儲、計算、調度、部署、監控五大子系統,這些理念滲透在字里行間,如果仔細閱讀,相信就能夠理解了。
下一篇計劃來寫SparkConf。加油。