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進程。