Spark分佈式計算執行模型

轉載:http://www.flickering.cn/%E5%88%86%E5%B8%83%E5%BC%8F%E8%AE%A1%E7%AE%97/2014/07/spark%E5%88%86%E5%B8%83%E5%BC%8F%E8%AE%A1%E7%AE%97%E6%89%A7%E8%A1%8C%E6%A8%A1%E5%9E%8B/

http://www.flickering.cn/%E8%B5%84%E6%96%99%E6%B1%87%E9%9B%86/

引言

相對Hadoop, Spark在處理需要迭代運算的機器學習訓練等任務上有着很大性能提升,同時提供了批處理、實時數據處理、機器學習以及圖算法等一站式的服務,因此最近大家一起來學習Spark,特別是MLLib

Spark中使用了RDD(Resilient Distributed Datasets, 彈性分佈式數據集)抽象分佈式計算,即使用RDD以及對應的transform/action等操作來執行分佈式計算;並且基於RDD之間的依賴關係組成lineage以及checkpoint等機制來保證整個分佈式計算的容錯性。因此,在學習MLLib的時候,很多時候看到的都是RDD的一些操作,而沒有涉及到分佈式計算上來,如下面的代碼:

// 創建SparkContext對象,作爲Spark的Driver應用程序,同Spark集羣進行交互
val conf = new SparkConf().setAppName(s"LinearRegression with $params")
val sc = new SparkContext(conf)
    
// 加載libsvm格式的訓練、測試instances數據
val examples = MLUtils.loadLibSVMFile(sc, params.input, multiclass = true).cache()

// 將數據分爲訓練集和測試集
val splits = examples.randomSplit(Array(0.8, 0.2))
val training = splits(0).cache()
val test = splits(1).cache()

examples.unpersist(blocking = false)

// 設置離線訓練算法
val updater = params.regType match {
  case NONE => new SimpleUpdater()
  case L1 => new L1Updater()
  case L2 => new SquaredL2Updater()
}

val algorithm = new LinearRegressionWithSGD()
algorithm.optimizer
  .setNumIterations(params.numIterations)
  .setStepSize(params.stepSize)
  .setUpdater(updater)
  .setRegParam(params.regParam)

// 根據設置的訓練算法和訓練數據集,得到模型
val model = algorithm.run(training)

// 使用模型來預測測試集合上對應的值
val prediction = model.predict(test.map(_.features))

由於Spark使用RDD抽象了分佈式計算的操作,因此,上面的代碼只是涉及到訓練集和測試集以及上面的操作,感覺不到該程序是單機模型訓練還是分佈式模型訓練。因此,有同學提出來問題,這些RDD操作如何進行分佈式計算的呢?這涉及Spark分佈式計算執行模型。下面從Spark集羣部署和應用程序提交、執行模型來展開介紹Spark如何進行分佈式計算。

Spark集羣部署和應用程序提交

Spark應用程序部署

Spark集羣部署

Spark集羣是由Cluster Manager和Worker Node組成。當前Spark支持如下三種不同的Cluster Manager:

  1. Standalone – 使用./sbin/start-master.sh和./sbin/start-slave.sh或者./sbin/start-all.sh即可非常容易地將Spark集羣搭建起來,Spark內置的集羣資源管理器。當前我們的實驗Spark集羣是基於Standalone的集羣管理模式搭建起來的
  2. Apache Mesos – 基於Mesos資源管理器來管理機器資源
  3. Hadoop Yarn – 基於Hadoop 2裏面的資源管理器來管理機器資源

提交Spark應用程序

不管Spark集羣是基於什麼樣資源管理器進行管理,通過spark-submit往Spark集羣上提交應用程序(包括有SparkContext對象的應用程序叫做Driver),提交Driver應用程序的時候,需指定Cluster Manager的地址,所需要的CPU核數、內存數目等。具體例子如下:

# Run on a Spark standalone cluster
./bin/spark-submit \
  --class org.apache.spark.examples.SparkPi \
  --master spark://207.184.161.138:7077 \
  --executor-memory 20G \
  --total-executor-cores 100 \
  /path/to/examples.jar \
  1000

# Run on a YARN cluster
export HADOOP_CONF_DIR=XXX
./bin/spark-submit \
  --class org.apache.spark.examples.SparkPi \
  --master yarn-cluster \  # can also be `yarn-client` for client mode
  --executor-memory 20G \
  --num-executors 50 \
  /path/to/examples.jar \
  1000

提交完應用程序之後,即Driver通過Cluster Manager去Worker Node上分配所需要的CPU和內存。從而有了分佈式計算所需要的物理資源,即擁有分佈在Spark集羣中Worker Node上的ExecutorBackend進程,該進程等待來自Driver的Task任務。

下面以Standalone集羣模式爲例說明整個過程,Master對應的時Cluster Manager, Worker對應爲Worker Node. 整個操作過程如下:

Spark Submit Application Process

  1. Driver通過AppClient向Master發送了RegisterApplication消息來註冊Application
  2. Master收到消息之後會發送RegisteredApplication通知Driver註冊成功,Driver的接收類還是AppClient
  3. Master接受到RegisterApplication之後會觸發調度過程,在資源足夠的情況下會向Woker和Driver分別發送LaunchExecutor、ExecutorAdded消息
  4. Worker接收到LaunchExecutor消息之後,會執行消息中攜帶的命令,執行CoarseGrainedExecutorBackend類(圖中僅以它繼承的接口ExecutorBackend代替),執行完畢之後會發送ExecutorStateChanged消息給Master
  5. Master接收ExecutorStateChanged之後,立即發送ExecutorUpdated消息通知Driver。Driver中的AppClient接收到Master發過來的ExecutorAdded和ExecutorUpdated後進行相應的處理
  6. 啓動之後的CoarseGrainedExecutorBackend會向Driver發送RegisterExecutor消息
  7. Driver中的SparkDeploySchedulerBackend(具體代碼在CoarseGrainedSchedulerBackend裏面)接收到RegisterExecutor消息,回覆註冊成功的消息RegisteredExecutor給ExecutorBackend,並且立馬準備給它發送任務

最後,CoarseGrainedExecutorBackend接收到RegisteredExecutor消息之後,實例化一個Executor等待任務的到來

Spark執行模型

Spark執行模型分如下三步:

  1. 創建應用程序計算RDD DAG (Directed acyclic graph,有向無環圖)
  2. 創建RDD DAG邏輯執行方案,即將整個計算過程對應到Stage上
  3. 根據上面介紹獲取到Executor來進行調度並執行各個Stage對應的ShuffleMapResult和ResultTask等任務。必須是執行一個Stage完成之後,才能往下執行接下來的Stage

下面以WordCount例子來說明Spark執行模型

WordCount Job例子

下面以map-reduce中經典例子WordCount爲例解釋Spark執行模型,整個WordCount Job是計算README.md這個文檔中各個單詞出現的頻次,並且將最終結果保存到wordcount_result目錄中去

val wordCountResult = sc.textFile("README.md", 4)
  .flatMap(line => line.split(" ")).map(word => (word, 1))
  .reduceByKey(_ + _, 2)
vordCountResult.saveAsTextFile("wordcount_result")

RDD DAG

RDD DAG描述的是各個RDD之間的依賴關係。上面例子從RDD DAG的角度來看如下:

RDD DAG

即該RDD DAG主要是包括有MappedRDD->FlatMappedRDD->MappedRDD->ShuffledRDD四個RDD的轉換(Transform), 根據Spark實現,RDD的轉換操作是不會提交給Spark集羣來執行的,因此,上面的操作必須要由Spark的行爲(Action)來觸發,因此,在最後調用saveAsTextFile這個行爲來將整個WordCount Job提交到Spark集羣中來執行。(備註:所有的Spark的轉換、行爲操作可以參考文檔Spark Programming Guide

RDD DAG邏輯執行方案

RDD DAG只是從整體的RDD角度來查看整個Job的執行過程。在RDD DAG邏輯執行方案,需要查看各個RDD中各個Partition的情況,以及各個RDD的Partition的依賴情況來決定如何劃分Stage。

根據RDD論文,將RDD的各個Partition的依賴情況劃分爲Narrow Dependencies和Wide Dependencies:

  • Narrow Dependencies – parent RDD中的一個Partition最多被child RDD中的一個Partition所依賴
  • Wide Dependencies – parent RDD中的一個Partition被child RDD中的多個Partition所依賴

Narrow and Wide Dependencies

如圖所示,map是Narrow Dependencies, groupByKey是Wide Dependencies。若在Job中存在有Wide Dependencies,就劃分爲不同的Stage。

具體到WordCount Job,具體的Stage劃分如下:

Spark Stage Partition劃分

由於flatMap、map等操作對RDD進行轉換得到的RDD的partition和parent RDD的partition是Narrow Dependencies關係,因此處於在同一個Stage中,即都在Stage 1中;而reduceByKey這個轉換,其對應的是Wide Dependencies關係,因此,需要新建一個Stage出來,即所在爲Stage 2,獨立於Stage 1。

當Job執行saveAsTextFile這個行爲的時候,其依賴於Stage 2中ShuffledRDD,而Stage 2又依賴於Stage 1,因此,需要先執行完Stage 1中所有的Task之後,才執行Stage 2中的所有的Task。當Stage 2中所有的任務執行完之後,整個Job即執行完成。

RDD Task執行

Spark通過分析各個RDD的依賴關係生成了RDD DAG,然後再通過分析各個RDD中的partition之間的依賴關係來將執行過程進行邏輯劃分成不同的Stage。有了這些Stage的依賴關係之後,從最parent stage開始執行,執行完了parent stage的所有的task再執行child stage中的所有的task,直到所有的Stage都執行完成。

針對WordCount這個例子來看,Stage 2依賴Stage 1,因此,先執行Stage 1中的Task。而Stage 1中各個RDD中是有4個partition(見textFile(“README.md”, 4)中的第二個參數來指定RDD中需要劃分多少partition,當然對於RDD也可以通過調用repartition和coalesce來改變partition數目),因此,在Stage 1中由Driver應用程序生成4個ShuffleMapTask並提交給之前分配得到的Executor中執行。當Stage 1中4個ShuffledMapTask執行完成之後,再開始執行Stage 2中的2個ResultTask(由於reduceByKey(_ + _, 2)中的第二個參數指定只需要2個reduce,因此,在ShuffledRDD中只有2個partition,因此,也只有對應的2個ResultTask).當Stage 2中的2個ResultTask執行完之後,saveAsTextFile會將ShuffledRDD中的內容落地到文件中中,即保存到wordcount_result目錄中去。從而完成了整個WordCount Job的執行任務。

從上面的描述可以看到,Stage 1中會生成4個ShuffleMapTask, 在提交WordCount Job應用程序給Spark集羣時候,獲取得到的Executor數目大於等於4個,那麼該4個ShuffleMapTask可以在這些Executor進行並行運行,從而實現了在不同的Executor進行分佈式計算。

最後說明下,RDD的Partition數目決定了執行過程中生成多少個Task,即決定於並行計算的數目,該參數是Spark應用程序中非常重要的參數,Partition設置的越大,並行度越高,在Executor資源有限的情況下,任務之間調度開銷會變大,同時若有Wide Dependencies的時候,Shuffle的代價也比較多,因此在實際應用中需要謹慎調整該參數。Spark作者推薦的“比較合理的partition數目”爲:

  1. 100-10000
  2. 最少要有2倍於申請的CPU核數
  3. 每個Partition對應的Task最少要運行100ms以上

結束語

RDD爲Spark抽象了分佈式計算的操作,即將任務進行分佈式計算轉成RDD的轉換和行爲上。爲了瞭解Spark究竟如何進行分佈式計算的,本文首先介紹提交Driver應用程序給Spark集羣,通過同Cluster Manager和Worker Node進行交互,得到該Driver所需要的Executor資源,然後再由Spark應用程序通過分析RDD DAG依賴關係,以及各個RDD之間partition的依賴關係來生成不同的Stage,再將Stage中的任務,按照RDD的partition個數生成相同數目的Task提交給Executor來執行,從而實現了Task在不同的Executor中進行分佈式計算,最終實現整個Driver應用程序的分佈式計算。

發佈了0 篇原創文章 · 獲贊 7 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章