Spark源碼解析(二):SparkContext內部執行流程

SparkContext內部執行的時序圖

對于這個時序圖的具體描述如下:

1.SparkSubmit在main()方法中執行,然后根據提交的類型調用相應的方法,這里是"Submit",調用submit()方法,submit()里面進行一些判斷后,

使用反射Class.forName(childMainClass, true, loader),然后調用invoke()方法來調用程序員自己寫的類,我們這里是WordCount。

2.在WordCount類中,main()方法里有調用SparkContext,SparkContext構造器使用createSparkEnv()方法,

這個方法使用SparkEnv.createDriverEnv(conf, isLocal, listenerBus)方法創建SparkEnv對象;

在SparkEnv類,調用create()方法來進行創建SparkEnv,在這個方法內部,有一個

AkkaUtils.createActorSystem(actorSystemName, hostname, port, conf, securityManager)的調用過程,

主要用來產生Akka中的ActorSystem以及得到綁定的端口號。

3.在創建SparkEnv對象后,SparkContext構造器使用代碼SparkContext.createTaskScheduler(this, master)創建TaskScheduler對象,

這里根據實際的提交模式來進行創建TaskScheduler對象,提交模式有:local、Mesos、Zookeeper、Simr、Spark,

這里模們主要分析Spark集群下的模式;然后還需要創建一個SparkDeploySchedulerBackend對象;

在創建TaskScheduler對象調用initialize()方法,這里選擇調度模式,主要有兩種模式,FIFO和FAIR,默認的調度模式;

最后調用taskScheduler的start()方法,里面主要調用SparkDeploySchedulerBackend對象的start()方法,

首先調用父類的start()方法產生一個用于和Executor通信的DriverActor對象,然后里面主要創建一個AppClient對象內部有ClientActor類對象,

用于Driver和Master進行RPC通信。

SparkContext源碼分析流程

1.SparkSubmit半生對象的源碼

1.1SparkSubmit的main()函數在SparkSubmit半生對象的104行左右,這個是程序的主要入口:

接下來主要進入submit()方法,下面是submit()方法

1.2SparkSubmit的submit()方法,代碼大約在142行左右, 這個方法的主要作用是根據不同的模式使用runMain()方法:

1.3SparkSubmit的runMain()方法,代碼大約在505行左右,這個方法主要的主要作用是通過反射獲取自定義類,這里我們主要的是WordCount,然后通過invoke方法調用main?這里是方法的重要代碼:

調用WordCount的main()方法后,接下來就要看SparkContext的內部了。

2.SparkContext內部源碼分析

很重要:SparkContext是Spark提交任務到集群的入口

我們看一下SparkContext的主構造器

1.調用createSparkEnv方法創建SparkEnv,里面有一個非常重要的對象ActorSystem

2.創建TaskScheduler -> 根據提交任務的URL進行匹配 -> TaskSchedulerImpl -> SparkDeploySchedulerBackend(里面有兩個Actor)

3.創建DAGScheduler

2.1創建SparkEnv獲取ActorSystem,代碼大約在275行左右,這一步的主要的作用是創建ActorSystem對象以后根據這個對象來創建相應的Actor

主要調用SparkEnv類的createDriverEnv()方法獲取SparkEnv對象,createDriverEnv()主要調用SparkEnv的create()方法,這里代碼大約

SparkEnv的154行,代碼具體如下:

createDriverEnv()內部主要調用create()方法,代碼大約在202行,重要的代碼如下:

這個方法的主要作用是調用AkkaUtils這個工具類創建ActorSystem

2.2創建TaskScheduler,代碼大約在374行,重要的代碼如下:

這里調用createTaskScheduler()方法,這個類主要的作用是根據提交的類型創建相應的TaskScheduler(),這里主要分析Spark集群下,主要的代碼如下:

這里進行模式匹配,以上代碼大約在SparkContext的2159行,主要的作用是創建TaskSchedulerImpl對象,然后初始化調度器這里,需要看的是initialize(),主要的實現是

TaskSchedulerImpl類,這里我們將會深入TaskSchedulerImpl類的initialize()方法,下面是該方法的實現:

主要用于調度的模式,調度模式主要分為FIFO和FAIR。在進行創建了TaskScheduler對象后,我們再來看一下主要的代碼:

上述代碼中,這里主要用于創建一個HeartbeatReceiver對象來進行心跳,用于Executors和DriverActor的心跳。

然后創建DAGScheduler對象,這個對象的主要作用是用來劃分Stage。

2.3TaskScheduler進行啟動,代碼大約在395行,重要的代碼如下:

由于這里是TaskScheduler的主要的實現類是TaskScheduler是TaskSchedulerImpl類,我們要進入的源碼是:

主要調用了SparkDeploySchedulerBackend的start()方法,接下來我們需要看SparkDeploySchedulerBackend內部實現。

以下是SparkDeploySchedulerBackend的構造器函數,這個代碼大約在SparkDeploySchedulerBackend的45行重要的代碼如下:

從上面的代碼可以看出首先調用父類(CoarseGrainedSchedulerBackend)的start()方法,然后對于一些重要的參數進行封裝,這里最重要的參數是

CoarseGrainedExecutorBackend類,還有一些driverUrl和WORKER_URL等參數的封裝,將CoarseGrainedExecutorBackend

封裝成Command,這是一個樣例類,不知道樣例類請點擊這里,將這個參數封裝成為一個

ApplicationDescription對象,創建一個AppClient對象,這個對象主要用于Driver和Master之間的通信,以下我們分析start()方法后再分析client.start()。

可以從上面的代碼看出,?這里主要創建一個DriverActor,這個Actor的主要的作用是Driver進程和Executor進程進行RPC通信

在分析完以上的CoarseGrainedSchedulerBackend的start()方法后,這里主要進行的源碼分析是client.start()方法這里創建一個ClientActor,準確來說是這個

ClientActor來和Master通信。

現在,這里就調用ClientActor的生命周期方法,對于Akka通信不了解的,請點擊這里進行了解Actor的生命周期方法。

Akka的Actor的生命周期方法主要從preStart()方法開始,這行代碼大約在67行左右,主要的內容如下:

在preStart()方法中主要做的事情是向Master注冊,registerWithMaster()的主要內容是:

這個方法主要是向活著的Master進行注冊,將以前所有的參數封裝的appDescription發送給Master,然后啟動定時調度器進行和Master的注冊,因為有可能進行多次通信。

Master收到通過樣例類的模式匹配,對于Driver向Master注冊Application,主要的作用是持久化Driver傳遞的參數和進行資源調度.

主要的代碼大約在Master類的315行,主要的代碼如下:

這段代碼的意義是:持久化信息,告知ClientActor發送注冊成功的信息,然后適使用schedule()進行資源的調度。

對于schedule()方法,代碼大約在533行,這里的主要作用是進行資源調度,主要的是兩種資源調度的方法,一種是盡量打散的分配資源,還有一種是盡量集中

對于盡量打散的方式:將每個app分配到盡可能多的worker中執行

App調度時會為app分配滿足條件的資源-----Worker(State是Alive,其上并沒有該Application的executor,可用內存滿足要求(spark.executor.memory指定,默認512),

核滿足要求(spark.cores.max, 最大可用core數,若未指定,則為全部資源)),然后通知Woker啟動Excutor. 及向AppClient發送ExecutorAdded消息。

進行調度時,調度程序會根據配制SpreadOutApps = spark.deploy.spreadOut情況決定資源分配方式。

1 從列表中取下一app,根據CPU情況找出合適的woker,按核從小到大排序

2 如果worker節點存在可以分配的core 則進行預分配處理(輪循一次分一個直至滿足app需求),并在分配列表(assigned = ArrayInt)中計數。

3根據assinged列表中的預分配信息,進行分配Executor(真實分配)

4 啟動Executor并設置app.state = ApplicationState.RUNNING

盡情集中的方式: 將每個app分配到盡可能少的worker中執行。?1 從可用的worker列表中取下一work. (worker <- workers if worker.coresFree > 0)

2 遍歷waitingApps 找到滿足app運行條件的app,進行分配

3啟動Executor(launchExecutor(w,e))并設置app.state = ApplicationState.RUNNING

其中:launchExcutor(worker, exec) 具體內容如下:

向executor分配給worker

通知worker啟動executor

由分配過程可知, 分配的Excutor個數與CPU核心數有關。當指定完Worker節點后,會在Worker節點創建ExecutorRunner,并啟動,執行App中的Command

去創建并啟動CoarseGrainedExecutorBackend。CoarseGrainedExecutorBackend啟動后,會首先通過傳入的driverUrl這個參數向

在CoarseGrainedSchedulerBackend::DriverActor(用于與Master通信,及調度任務)發送RegisterExecutor(executorId, hostPort, cores),

DriverActor會創建executorData(executor信息)加入executorDataMap供后續task使用,并回復RegisteredExecutor,

此時CoarseGrainedExecutorBackend會創建一個org.apache.spark.executor.Executor。至此,Executor創建完畢。

Executor是直接用于task執行, 是集群中的直接勞動者。至此,資源分配結束。

百度腦圖關于作業提交以及SparkContext的示意圖



注意:這里的SparkContext和Master是兩個獨立的類,由于Baidu腦圖不能獨立劃分,所以看起來像父子類關系。

總結

在SparkContext(這里基于Spark的版本是1.3.1)主要做的工作是:

1.創建SparkEnv,里面有一個很重要的對象ActorSystem

2.創建TaskScheduler,這里是根據提交的集群來創建相應的TaskScheduler

3.對于TaskScheduler,主要的任務調度模式有FIFO和FAIR

4.在SparkContext中創建了兩個Actor,一個是DriverActor,這里主要用于Driver和Executor之間的通信;還有一個是ClientActor,主要用于Driver和Master之間的通信。

5.創建DAGScheduler,其實這個是用于Stage的劃分

6.調用taskScheduler.start()方法啟動,進行資源調度,有兩種資源分配方法,一種是盡量打散;一種是盡量集中

7.Driver向Master注冊,發送了一些信息,其中一個重要的類是CoarseGrainedExecutorBackend,這個類以后用于創建Executor進程。


?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容