本文章,原創 若澤數據 ,禁止所有閱讀,轉載,分享及評論
spark on yarn 執行流程前置
構建Spark Application的運行環境(啟動SparkContext),SparkContext向資源管理器(可以是Standalone、Mesos或YARN)注冊并申請運行Executor資源;
資源管理器分配Executor資源并啟動StandaloneExecutorBackend,Executor運行情況將隨著心跳發送到資源管理器上;
SparkContext構建成DAG圖,將DAG圖分解成Stage,并把Taskset發送給Task Scheduler。Executor向SparkContext申請Task,Task Scheduler將Task發放給Executor運行同時SparkContext將應用程序代碼發放給Executor。
Task在Executor上運行,運行完畢釋放所有資源。
流程大概我們都清楚,但詳細的執行流程要從源碼里分析
當我們在開發完成一個appliction后,要用spark-submit提交到集群,通常都在提交到yarn上執行,執行的命令如下:
./bin/spark-submit \
--master yarn-cluster \
--num-executors 100 \
--executor-memory 6G \
--executor-cores 4 \
--driver-memory 1G \
--conf spark.default.parallelism=1000 \
--conf spark.storage.memoryFraction=0.5 \
--conf spark.shuffle.memoryFraction=0.3 \
spark提供了一個命令spark-submit,那么我們第一步就去看一下這個文件,發現里面只有這一行,說明實際上是去找另一個spark-class的文件,參數是SparkSubmit
exec "${SPARK_HOME}"/bin/spark-class org.apache.spark.deploy.SparkSubmit "$@"
在spark-class里,前面是一些Jdk,Spark assembly,還有環境變量的檢查與設置,
首先我們看這個文件:
首先檢查SPARK_HOME目錄若為空,剛設置SPARK_HOME目錄,接著加載load.spark-env.sh文件
if [ -z "${SPARK_HOME}" ]; then
export SPARK_HOME="$(cd "`dirname "$0"`"/..; pwd)"
fi
. "${SPARK_HOME}"/bin/load-spark-env.sh
那么我們進入load-spark-env.sh后,我們可以看出這個文件做了下面幾件事:
檢查SPARK_HOME是否設定
檢查SPARK_ENV_LOADED,得到SPARK_HOME/conf,加載sparn-env.sh
檢查并設置SPARK_SCALA_VERSION
if [ -z "$SPARK_ENV_LOADED" ]; then
export SPARK_ENV_LOADED=1
# Returns the parent of the directory this script lives in.
parent_dir="${SPARK_HOME}"
user_conf_dir="${SPARK_CONF_DIR:-"$parent_dir"/conf}"
if [ -f "${user_conf_dir}/spark-env.sh" ]; then
# Promote all variable declarations to environment (exported) variables
set -a
. "${user_conf_dir}/spark-env.sh"
set +a
fi
fi
if [ -z "$SPARK_SCALA_VERSION" ]; then
ASSEMBLY_DIR2="${SPARK_HOME}/assembly/target/scala-2.11"
ASSEMBLY_DIR1="${SPARK_HOME}/assembly/target/scala-2.10"
if [[ -d "$ASSEMBLY_DIR2" && -d "$ASSEMBLY_DIR1" ]]; then
echo -e "Presence of build for both scala versions(SCALA 2.10 and SCALA 2.11) detected." 1>&2
echo -e 'Either clean one of them or, export SPARK_SCALA_VERSION=2.11 in spark-env.sh.' 1>&2
exit 1
fi
if [ -d "$ASSEMBLY_DIR2" ]; then
export SPARK_SCALA_VERSION="2.11"
else
export SPARK_SCALA_VERSION="2.10"
fi
fi
返回到spark class文件,下面shell里可以看到,通過上面加載的spark-evn.sh,就可以檢查并找到JAVA_HOME,
接著,找到SPARK_ASSEMBLY_JAR及所在目錄,如下圖:
# Find the java binary
if [ -n "${JAVA_HOME}" ]; then
RUNNER="${JAVA_HOME}/bin/java"
else
if [ `command -v java` ]; then
RUNNER="java"
else
echo "JAVA_HOME is not set" >&2
exit 1
fi
fi
# Find assembly jar
SPARK_ASSEMBLY_JAR=
if [ -f "${SPARK_HOME}/RELEASE" ]; then
ASSEMBLY_DIR="${SPARK_HOME}/lib"
else
ASSEMBLY_DIR="${SPARK_HOME}/assembly/target/scala-$SPARK_SCALA_VERSION"
fi
其實核心就是下面這句了:
CMD=()
while IFS= read -d '' -r ARG; do
CMD+=("$ARG")
done < <("$RUNNER" -cp "$LAUNCH_CLASSPATH" org.apache.spark.launcher.Main "$@")
exec "${CMD[@]}"
循環讀取ARG參數,加入到CMD中。然后執行了"$RUNNER" -cp "$LAUNCH_CLASSPATH" org.apache.spark.launcher.Main "$@ 這個是真正執行的第一個spark的類
org.apache.spark.launcher.Main,下面是對這個類的理解,其實說白了,就是把我們上面submit時的參數拼裝,成為一個可命令的commend
* Usage: Main [class] [class args]
* <p>
* This CLI works in two different modes:
* <ul>
* <li>"spark-submit": if <i>class</i> is "org.apache.spark.deploy.SparkSubmit", the
* {@link SparkLauncher} class is used to launch a Spark application.</li>
* <li>"spark-class": if another class is provided, an internal Spark class is run.</li>
* </ul>
那么下面的exec "${CMD[@]}"
就是去執行這個命令
寫到這里,我們的寫的application jar才開始執行了,上面的全是一些在執行前的準備工作。
spark on yarn執行流程
當上面的準備工作全部完成后,那么執行命令,就開始我們真正的spark執行流程了
首先在通過命令的參數,找到我們打包里開發好的主類(java反射),
回想一下,之前我們寫的spark應用的第一行是不是先構造一個sparkConf,接著通過sparkConfs構造一個非常重要的對象:SparkContext
val conf = new SparkConf()
val sc = new SparkContext(conf)
現在我們進入SparkContext這個類中,發現在在初始化這個類的時候,實際上做了好多事,比如檢查--master是哪種運行模式等,如下面的源碼,是不是在new SparkContext時,去調用createTaskScheduler方法
// Create and start the scheduler
val (sched, ts) = SparkContext.createTaskScheduler(this, master)
_schedulerBackend = sched
_taskScheduler = ts
_dagScheduler = new DAGScheduler(this)
_heartbeatReceiver.ask[Boolean](TaskSchedulerIsSet)
其中在createTaskScheduler做的最重要的兩件事:
1:構造出DAGScheduler
2:構造出TaskScheduler
如下面的源碼里分析:
//當發現--master是Yarn-client模式后,那么是不是根據java的反射類加載,得到TaskSchedulerImpl對象
//也就是上面的構造出TaskScheduler
case "yarn-client" =>
val scheduler = try {
val clazz = Utils.classForName("org.apache.spark.scheduler.cluster.YarnScheduler")
val cons = clazz.getConstructor(classOf[SparkContext])
cons.newInstance(sc).asInstanceOf[TaskSchedulerImpl]
} catch {
case e: Exception => {
throw new SparkException("YARN mode not available ?", e)
}
}
//同樣,代碼真的很相似,YarnClientSchedulerBackend對象也創建出來了,也就是上面的構造出DAGScheduler
val backend = try {
val clazz =
Utils.classForName("org.apache.spark.scheduler.cluster.YarnClientSchedulerBackend")
val cons = clazz.getConstructor(classOf[TaskSchedulerImpl], classOf[SparkContext])
cons.newInstance(scheduler, sc).asInstanceOf[CoarseGrainedSchedulerBackend]
} catch {
case e: Exception => {
throw new SparkException("YARN mode not available ?", e)
}
}
scheduler.initialize(backend)
(backend, scheduler)
其實我們同相看下yarn-cluster的代碼,也非常的相似,只不過生成的
TaskScheduler的對象是YarnClusterScheduler
DAGScheduler的對象是YarnClusterSchedulerBackend
如圖:
case "yarn-standalone" | "yarn-cluster" =>
if (master == "yarn-standalone") {
logWarning(
"\"yarn-standalone\" is deprecated as of Spark 1.0. Use \"yarn-cluster\" instead.")
}
val scheduler = try {
val clazz = Utils.classForName("org.apache.spark.scheduler.cluster.YarnClusterScheduler")
val cons = clazz.getConstructor(classOf[SparkContext])
cons.newInstance(sc).asInstanceOf[TaskSchedulerImpl]
see tomorrow