从spark启动任务源头 $SPARK_HOME/bin/spark-submit 开始阅读spark源码。

一、脚本阶段

提交任务命令,先使用local模式

spark-submit --master local --class com.lof.main.SparkPi /Users/user/Desktop/SparkPi.jar

sparkPi代码:

public class SparkPi {
    public static void main(String[] args) {
        SparkSession spark = SparkSession.builder().appName("JavaSparkPi").getOrCreate();
        JavaSparkContext jsc = new JavaSparkContext(spark.sparkContext());
        int slices = (args.length == 1) ? Integer.parseInt(args[0]) : 2;
        List<Integer> l = new ArrayList<Integer>(100000 * slices);
        for (int i = 0; i < n; i++) {
            l.add(i);
        }
        JavaRDD<Integer> dataSet = jsc.parallelize(l, slices);
        int count = dataSet.map(integer -> {
            double x = Math.random() * 2 - 1;
            double y = Math.random() * 2 - 1;
            return (x * x + y * y <= 1) ? 1 : 0;
        }).reduce((integer, integer2) -> integer + integer2);
        System.out.println("Pi is roughly " + 4.0 * count / n);
        spark.stop();
    }
}

spark的任务提交脚本 $SPARK_HOME/bin/spark-submit

#!/usr/bin/env bash

# 如环境变量中没有配置 SPARK_HOME , 自动配置
if [ -z "${SPARK_HOME}" ]; then
  source "$(dirname "$0")"/find-spark-home
fi

# disable randomized hash for string in Python 3.3+
export PYTHONHASHSEED=0
exec "${SPARK_HOME}"/bin/spark-class org.apache.spark.deploy.SparkSubmit "$@"

调用bin目录下的 spark-class ,并将参数传过去,接下来看一看 spark-class

#!/usr/bin/env bash
## 只保留核心代码

# 执行load-spark-env.sh,最终调用spark-env.sh
. "${SPARK_HOME}"/bin/load-spark-env.sh

# 查找jars目录
# 源码编译的目录下没有 ${SPARK_HOME}/jars 没有该目录
if [ -d "${SPARK_HOME}/jars" ]; then
  SPARK_JARS_DIR="${SPARK_HOME}/jars"
else
  SPARK_JARS_DIR="${SPARK_HOME}/assembly/target/scala-$SPARK_SCALA_VERSION/jars"
fi

# 执行 lauancher.Main 方法,生成 commond 命令
build_command() {
  "$RUNNER" -Xmx128m -cp "$LAUNCH_CLASSPATH" org.apache.spark.launcher.Main "$@"
  printf "%d\0" $?
}

# Turn off posix mode since it does not allow process substitution
set +o posix
CMD=()
while IFS= read -d '' -r ARG; do
  CMD+=("$ARG")
done < <(build_command "$@")

CMD=("${CMD[@]:0:$LAST}")
echo "${CMD[@]}"
exec "${CMD[@]}"

将"{CMD[@]}"输出,最终 spark-class 执行的命令为

/Library/Java/JavaVirtualMachines/jdk1.8.0_221.jdk/Contents/Home/bin/java 
-cp /spark-2.3.2/conf/:/spark-2.3.2/assembly/target/scala-2.11/jars/*:/hadoop-2.7.3/etc/hadoop/ 
-Xmx1g org.apache.spark.deploy.SparkSubmit 
--master local --class org.apache.spark.examples.SparkPi /Users/xxx/Desktop/SparkPi

可以看出,调用的是org.apache.spark.deploy.SparkSubmit类执行提交操作

源码中找到 org.apache.spark.deploy.SparkSubmit

在idea中,将–master local --class org.apache.spark.examples.SparkPi /Users/xxx/Desktop/SparkPi设置为启动参数,模拟执行

二、进入代码

org.apache.spark.deploy.SparkSubmit中main方法 核心是:1、加载参数 2、提交任务

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Kkg8iUfJ-1585278184804)(/Users/lofty/Library/Application Support/typora-user-images/image-20200326172915847.png)]

1、加载环境参数

// 将提交参数解析及加载环境变量
val appArgs = new SparkSubmitArguments(args)

SparkSubmitArguments中核心方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bEK842eW-1585278184806)(/Users/lofty/Library/Application Support/typora-user-images/image-20200326175820154.png)]




根据这3个方法可以看出,在提交过程中的参数的优先级:
命令行参数 > spark-defaults.conf > 环境变量

2、提交任务

提交任务首先执行SparkSubmit.scala 中的 submit()方法。

Java提交任务 Spark spark提交任务源码_jar


然后执行doMain()方法

Java提交任务 Spark spark提交任务源码_SPARK_02


在doMain()方法中调用runMain() 方法,runMain方法较长,只保留核心代码

private def runMain(
    childArgs: Seq[String],
    childClasspath: Seq[String],
    sparkConf: SparkConf,
    childMainClass: String,
    verbose: Boolean): Unit = {

  // 获取ClassLoader
  val loader =
    if (sparkConf.get(DRIVER_USER_CLASS_PATH_FIRST)) {
      new ChildFirstURLClassLoader(new Array[URL](0),
        Thread.currentThread.getContextClassLoader)
    } else {
      new MutableURLClassLoader(new Array[URL](0),
        Thread.currentThread.getContextClassLoader)
    }
  Thread.currentThread.setContextClassLoader(loader)

  // TODO 将jar包路径添加进ClassLoader中,以便后续通过反射直接调用直接
  for (jar <- childClasspath) {
    addJarToClasspath(jar, loader)
  }
  
  var mainClass: Class[_] = null
  try {
    // TODO 创建主类 class
    mainClass = Utils.classForName(childMainClass)
  } catch {
    // 此处省略
  }

  // TODO 判断 SparkApplication是否其 父类 (主类是否实现SparkApplication接口)
  val app: SparkApplication = if (classOf[SparkApplication].isAssignableFrom(mainClass)) {
    mainClass.newInstance().asInstanceOf[SparkApplication]
  } else {
   		// 此处省略
    }
    // TODO 直接通过反射执行 传入jar包的main方法
    new JavaMainApplication(mainClass)
  }

  try {
    // 调用start方法启动
    app.start(childArgs.toArray, sparkConf)
  } catch {
        // 此处省略
  }
}

第一步:获取当前线程的 ClassLoader

第二步:将application的jar包路径添加进ClassLoader中,以便后续直接通过反射调用

第三步:创建application主类的class对象

第四步:如果主类实现SparkApplication方法,则调用start()方法启动。否则通过JavaMainApplication的start()反射调用主类的main方法。

local模式就这么简单,加载参数、启动任务,全在当前JVM中执行

下一篇聊聊生产环境常用的 Yarn Cluster模式。