[TOC]
一、wordcount程序的執行過程
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
object WordCount {
def main(args: Array[String]): Unit = {
//創建spark配置文件對象.設置app名稱,master地址,local表示爲本地模式。
//如果是提交到集羣中,通常不指定。因爲可能在多個集羣匯上跑,寫死不方便
val conf = new SparkConf().setAppName("wordCount")
//創建spark context對象
val sc = new SparkContext(conf)
sc.textFile(args(0)).flatMap(_.split(" "))
.map((_,1))
.reduceByKey(_+_)
.saveAsTextFile(args(1))
sc.stop()
}
}
核心代碼很簡單,首先看 textFile這個函數
SparkContext.scala
def textFile(
path: String,
minPartitions: Int = defaultMinPartitions): RDD[String] = withScope {
assertNotStopped()
//指定文件路徑、輸入的格式類爲textinputformat,輸出的key類型爲longwritable,輸出的value類型爲text
//map(pair => pair._2.toString)取出前面的value,然後將value轉爲string類型
//最後將處理後的value返回成一個新的list,也就是RDD[String]
//setName(path) 設置該file名字爲路徑
hadoopFile(path, classOf[TextInputFormat], classOf[LongWritable], classOf[Text],
minPartitions).map(pair => pair._2.toString).setName(path)
}
關鍵性的操作就是:
返回了一個hadoopFile,它有幾個參數:
path:文件路徑
classOf[TextInputFormat]:這個其實就是輸入文件的處理類,也就是我們mr中分析過的TextInputFormat,其實就是直接拿過來的用的,不要懷疑,就是醬紫的
classOf[LongWritable], classOf[Text]:這兩個其實可以猜到了,就是輸入的key和value的類型。
接着執行了一個map(pair => pair._2.toString),將KV中的value轉爲string類型
我們接着看看hadoopFile 這個方法
def hadoopFile[K, V](
path: String,
inputFormatClass: Class[_ <: InputFormat[K, V]],
keyClass: Class[K],
valueClass: Class[V],
minPartitions: Int = defaultMinPartitions): RDD[(K, V)] = withScope {
assertNotStopped()
// This is a hack to enforce loading hdfs-site.xml.
// See SPARK-11227 for details.
FileSystem.getLocal(hadoopConfiguration)
// A Hadoop configuration can be about 10 KB, which is pretty big, so broadcast it.
val confBroadcast = broadcast(new SerializableConfiguration(hadoopConfiguration))
val setInputPathsFunc = (jobConf: JobConf) => FileInputFormat.setInputPaths(jobConf, path)
//看到這裏,最後返回的是一個 HadoopRDD 對象
//指定sc對象,配置文件、輸入方法類、KV類型、分區個數
new HadoopRDD(
this,
confBroadcast,
Some(setInputPathsFunc),
inputFormatClass,
keyClass,
valueClass,
minPartitions).setName(path)
}
最後返回HadoopRDD對象。
接着就是flatMap(.split(" ")) .map((,1)),比較簡單
flatMap(_.split(" "))
就是將輸入每一行,按照空格切割,然後切割後的元素稱爲一個新的數組。
然後將每一行生成的數組合併成一個大數組。
map((_,1))
將每個元素進行1的計數,組成KV對,K是元素,V是1
接着看.reduceByKey(_+_)
這個其實就是將同一key的KV進行聚合分組,然後將同一key的value進行相加,最後就得出某個key對應的value,也就是某個單詞的個數
看看這個函數
def reduceByKey(func: (V, V) => V): RDD[(K, V)] = self.withScope {
reduceByKey(defaultPartitioner(self), func)
}
這個過程中會分區,默認分區數是2,使用的是HashPartitioner進行分區,可以指定分區的最小個數
二、spark的資源調度
2.1 資源調度流程
圖2.1 spark資源調度
1、執行提交命令,會在client客戶端啓動一個spark-submit進程(用來爲Driver申請資源)。
2、爲Driver向Master申請資源,在Master的waitingDrivers 集合中添加這個Driver要申請的信息。Master查看works集合,挑選出合適的Work節點。
3、在選中的Work節點中啓動Driver進程(Driver進程已經啓動了,spark-submit的使命已經完成了,關閉該進程)。所以其實driver也需要資源,也只是跑在executor上的一個線程而已
4、Driver進程爲要運行的Application申請資源(這個資源指的是Executor進程)。此時Master的waitingApps 中要添加這個Application申請的資源信息。這時要根據申請資源的要求去計算查看需要用到哪些Worker節點(每一個節點要用多少資源)。在這些節點啓動Executor進程。
(注:輪詢啓動Executor。Executor佔用這個節點1G內存和這個Worker所能管理的所有的core)
5、此時Driver就可以分發任務到各個Worker節點的Executor進程中運行了。
Master中的三個集合
val works = new HashSet[WorkInfo]()
works 集合採用HashSet數組存儲work的節點信息,可以避免存放重複的work節點。爲什麼要避免重複?首先我們要知道work節點有可能因爲某些原因掛掉,掛掉之後下一次與master通信時會報告給master,這個節點掛掉了,然後master會在works對象裏把這個節點去掉,等下次再用到這個節點是時候,再加進來。這樣來說,理論上是不會有重複的work節點的。可是有一種特殊情況:work掛掉了,在下一次通信前又自己啓動了,這時works裏面就會有重複的work信息。
val waitingDrivers = new ArrayBuffer[DriverInfo]()
當客戶端向master爲Driver申請資源時,會將要申請的Driver的相關信息封裝到master節點的DriverInfo這個泛型裏,然後添加到waitingDrivers 裏。master會監控這個waitingDrivers 對象,當waitingDrivers集合中的元素不爲空時,說明有客戶端向master申請資源了。此時應該先查看一下works集合,找到符合要求的worker節點,啓動Driver。當Driver啓動成功後,會把這個申請信息從waitingDrivers 對象中移除。
val waitingApps = new ArrayBuffer[ApplicationInfo]()
Driver啓動成功後,會爲application向master申請資源,這個申請信息封存到master節點的waitingApps 對象中。同樣的,當waitingApps 集合不爲空,說明有Driver向Master爲當前的Application申請資源。此時查看workers集合,查找到合適的Worker節點啓動Executor進程,默認的情況下每一個Worker只是爲每一個Application啓動一個Executor,這個Executor會使用1G內存和所有的core。啓動Executor後把申請信息從waitingApps 對象中移除。
注意點:上面說到master會監控這三個集合,那麼到底是怎麼監控的呢???
master並不是分出來線程專門的對這三個集合進行監控,相對而言這樣是比較浪費資源的。master實際上是‘監控’這三個集合的改變,當這三個集合中的某一個集合發生變化時(新增或者刪除),那麼就會調用schedule()方法。schedule方法中封裝了上面提到的處理邏輯。
2.2 application和executor的關係
1、默認情況下,每一個Worker只會爲每一個Application啓動一個Executor。每個Executor默認使用1G內存和這個Worker所能管理的所有的core。
2、如果想要在一個Worker上啓動多個Executor,在提交Application的時候要指定Executor使用的core數量(避免使用該worker所有的core)。提交命令:spark-submit --executor-cores
3、默認情況下,Executor的啓動方式是輪詢啓動,一定程度上有利於數據的本地化。
什麼是輪詢啓動???爲什麼要輪訓啓動呢???
輪詢啓動:輪詢啓動就是一個個的啓動。例如這裏有5個人,每個人要發一個蘋果+一個香蕉。輪詢啓動的分發思路就是:五個人先一人分一個蘋果,分發完蘋果再分發香蕉。
爲什麼要使用輪詢啓動的方式呢???我們做大數據計算首先肯定想的是計算找數據。在數據存放的地方直接計算,而不是把數據搬過來再計算。我們有n臺Worker節點,如果只是在數據存放的節點計算。只用了幾臺Worker去計算,大部分的worker都是閒置的。這種方案肯定不可行。所以我們就使用輪詢方式啓動Executor,先在每一臺節點都允許一個任務。
存放數據的節點由於不需要網絡傳輸數據,所以肯定速度快,執行的task數量就會比較多。這樣不會浪費集羣資源,也可以在存放數據的節點進行計算,在一定程度上也有利於數據的本地化。
2.3 spark的粗細粒度調度
粗粒度(富二代):
在任務執行之前,會先將資源申請完畢,當所有的task執行完畢,纔會釋放這部分資源。
優點:每一個task執行前。不需要自己去申請資源了,節省啓動時間。
缺點:等到所有的task執行完纔會釋放資源(也就是整個job執行完成),集羣的資源就無法充分利用。
這是spark使用的調度粒度,主要是爲了讓stage,job,task的執行效率高一點
細粒度(窮二代):
Application提交的時候,每一個task自己去申請資源,task申請到資源纔會執行,執行完這個task會立刻釋放資源。
優點:每一個task執行完畢之後會立刻釋放資源,有利於充分利用資源。
缺點:由於需要每一個task自己去申請資源,導致task啓動時間過長,進而導致stage、job、application啓動時間延長。
2.4 spark-submit提交任務對資源的限制
我們提交任務時,可以指定一些資源限制的參數:
--executor-cores : 單個executor使用的core數量,不指定的話默認使用該worker所有能調用的core
--executor-memory : 單個executor使用的內存大小,如1G。默認是1G
--total-executor-cores : 整個application最多使用的core數量,防止獨佔整個集羣資源
三、整個spark資源調度+任務調度的流程
3.1 總的調度流程
https://blog.csdn.net/qq_33247435/article/details/83653584#3Spark_51
一個application的調度到完成,需要經過以下階段:
application-->資源調度-->任務調度(task)-->並行計算-->完成
圖3.1 spark調度流程
可以看到,driver啓動後,會有下面兩個對象:
DAGScheduler:
據RDD的寬窄依賴關係將DAG有向無環圖切割成一個個的stage,將stage封裝給另一個對象taskSet,taskSet=stage,然後將一個個的taskSet給taskScheduler。
taskScheduler:
taskSeheduler拿倒taskSet之後,會遍歷這個taskSet,拿到每一個task,然後去調用HDFS上的方法,獲取數據的位置,根據獲得的數據位置分發task到響應的Worker節點的Executor進程中的線程池中執行。並且會根據每個task的執行情況監控,等到所有task執行完成後,就告訴master將所喲executor殺死
任務調度中主要涉涉及到以下流程:
1)、DAGScheduler:根據RDD的寬窄依賴關係將DAG有向無環圖切割成一個個的stage,將stage封裝給另一個對象taskSet,taskSet=stage,然後將一個個的taskSet給taskScheduler。
2)、taskScheduler:taskSeheduler拿倒taskSet之後,會遍歷這個taskSet,拿到每一個task,然後去調用HDFS上的方法,獲取數據的位置,根據獲得的數據位置分發task到響應的Worker節點的Executor進程中的線程池中執行。
3)、taskScheduler:taskScheduler節點會跟蹤每一個task的執行情況,若執行失敗,TaskScher會嘗試重新提交,默認會重試提交三次,如果重試三次依然失敗,那麼這個task所在的stage失敗,此時TaskScheduler向DAGScheduler做彙報。
4)DAGScheduler:接收到stage失敗的請求後,,此時DAGSheduler會重新提交這個失敗的stage,已經成功的stage不會重複提交,只會重試這個失敗的stage。
(注:如果DAGScheduler重試了四次依然失敗,那麼這個job就失敗了,job不會重試
掉隊任務的概念:
當所有的task中,75%以上的task都運行成功了,就會每隔一百秒計算一次,計算出目前所有未成功任務執行時間的中位數*1.5,凡是比這個時間長的task都是掙扎的task。
總的調度流程:
=======================================資源調度=========================================
1、啓動Master和備用Master(如果是高可用集羣需要啓動備用Master,否則沒有備用Master)。
2、啓動Worker節點。Worker節點啓動成功後會向Master註冊。在works集合中添加自身信息。
3、在客戶端提交Application,啓動spark-submit進程。僞代碼:spark-submit --master --deploy-mode cluster --class jarPath
4、Client向Master爲Driver申請資源。申請信息到達Master後在Master的waitingDrivers集合中添加該Driver的申請信息。
5、當waitingDrivers集合不爲空,調用schedule()方法,Master查找works集合,在符合條件的Work節點啓動Driver。啓動Driver成功後,waitingDrivers集合中的該條申請信息移除。Client客戶端的spark-submit進程關閉。
(Driver啓動成功後,會創建DAGScheduler對象和TaskSchedule對象)
6、當TaskScheduler創建成功後,會向Master會Application申請資源。申請請求發送到Master端後會在waitingApps集合中添加該申請信息。
7、當waitingApps集合中的元素髮生改變,會調用schedule()方法。查找works集合,在符合要求的worker節點啓動Executor進程。
8、當Executor進程啓動成功後會將waitingApps集合中的該申請信息移除。並且向TaskSchedule反向註冊。此時TaskSchedule就有一批Executor的列表信息。
=======================================任務調度=========================================
9、根據RDD的寬窄依賴,切割job,劃分stage。每一個stage是由一組task組成的。每一個task是一個pipleline計算模式。
10、TaskScheduler會根據數據位置分發task。(taskScheduler是如何拿到數據位置的???TaskSchedule調用HDFS的api,拿到數據的block塊以及block塊的位置信息)
11、TaskSchedule分發task並且監控task的執行情況。
12、若task執行失敗或者掙扎。會重試這個task。默認會重試三次。
13、若重試三次依舊失敗。會把這個task返回給DAGScheduler,DAGScheduler會重試這個失敗的stage(只重試失敗的這個stage)。默認重試四次。
14、告訴master,將集羣中的executor殺死,釋放資源。