Spark Kernel
文章目录
一、Spark 内核概述
1.1 核心组件
-
Cluster Manager(Master)
主要负责对整个集群资源的分配与管理,在 Yarn 部署模式下为 ResourceManager,在 Mesos 部署模式下为 Mesos Master,在 Standalone 部署模式下为 Master。Cluster Manager 分配的资源属于一级分配,它将各个 Worker 上的内存,CPU 等资源分配给 Application,但并不负责对 Executor 的资源的分配。
-
Worker
Spark 的工作节点,Yarn 部署模式下实际由 NodeManager 替代。
负责将自己的内存,CPU 等资源通过注册机制告知 Cluster Manager;创建 Executor;同步资源信息,Executor 状态信息给 ClusterManager 等。
-
Driver
Spark 驱动器节点,用于执行 Spark 任务中的 main 方法,负责实际代码的执行工作。
主要负责:将用户程序转化为作业(Job);在 Executor 之间调度任务(Task);跟踪 Excutor 的运行情况;
通过 UI 展示查询 Excutor 运行情况。
-
Executor
Executor 节点是负责在 Spark 作业中运行具体任务,任务彼此之间相互独立。Spark 应用启动时,Executor 节点被同时启动,并且始终伴随着整个 Spark 应用的生命周期而存在。
如果有 Executor 节点发生了故障或崩溃,Spark 应用也可以继续执行,会将出错节点上的任务调度到其他 Executor 节点上继续运行。
主要功能:负责运行 Spark Application 的任务,并将结果返回给 Driver;为 RDD 提供内存式存储,使 RDD 直接缓存在 Executor 进程内的,因此任务可以在运行时充分利用缓存数据加速运算;将资源和任务进一步分配给 Executor。
-
Application
使用 Spark API 编写的应用程序就是 Application。
Application 通过 Spark API 将进行 RDD 的转换和 DAG 的构建,并通过 Driver 将 Application 注册到 Cluster Manager;Cluster Manager 将会根据 Application 的资源需求,通过一级分配将 Executor,内存,CPU 等资源分配给 Application;Driver 通过二级分配将 Executor 等资源分配给每一个任务,Application 最后通过 Driver 告诉 Executor 运行任务。
1.2 Spark 运行流程
- 任务提交后,都会先启动 Driver 程序;
- 随后 Driver 向集群管理器注册应用程序;
- 之后集群管理器根据此任务的配置文件分配 Executor 并启动该应用程序;
- 当 Driver 所需的资源全部满足后,Driver 开始执行 main 函数,Spark 转换为懒执行,当执行到 Action 算子时开始反向推算,根据宽依赖进行 Stage 的划分,随后每一个 Stage 对应一个 Taskset,Taskset 中有多个 Task;
- 根据本地化原则,Task 会被分发到指定的 Executor 去执行,在任务执行的过程中,Executor 也会不断与 Driver 进行通信,报告任务运行情况。
二、Spark 通讯架构
2.1 Spark 通讯架构概述
Spark 内置 RPC 框架
- Akka 的 Actor 通信模型
每个 Actor 都有一个 Mailbox,Maibox 的 Receive 方法接收数据,交给 Actor 处理,Actor 之间通过样例类(默认实现了序列化方法)通讯。
- Netty 通信模型
Endpoint 有1个 InBox 和 N 个 OutBox(与N个Endpoint通信就有N个OutBox),Endpoint 接收到的消息被写入 InBox,Endpoint 通过模式匹配获取 InBox 中的样例类(默认实现了序列化方法)。发送出去的消息写入 OutBox 并被发送到其他 Endpoint 的 InBox 中。
2.2 Netty 通信架构
-
每个节点(Client/Master/Worker)都称之为一个 RpcEndpoint,且都实现 RpcEndpoint 接口,内部根据不同的需求,设计不同的处理逻辑。当需要发送消息时,内部调用 Dispatcher 对应的方法:
- RpcEndpoint 是用来接收消息
- RpcEndpointRef 用来发送消息(其中包含 send/ask/askWithRetry 方法)
- RpcEndpointRef 的具体实现类是 NettyRpcEndpointRef
-
RpcEnv 是 Rpc的上下文(Rpc 环境)
每一个 RpcEndpoint 运行时以来的上下文环境都称之为 RpcEnv
-
Dispatcher 是消息分发器
RPC 端点需要发送消息或者从远程 RPC 端点接收到的消息,分发至对应的指令收件箱/发件箱。
指令接收方是自己则存入收件箱;指令接收方不是自己则放入发件箱。
-
Inbox 是指令收件箱:一个本地 RpcEndpoint 对应一个收件箱
-
RpcEndpiontRef 是对 RpcEndpoint 的一个引用。当我们需要向一个具体的 RpcEndpoint 发送消息时,一般我们需要获取到该RpcEndpoint 的引用,然后通过该引用发送消息。
-
OutBox 是指令发件箱
一个目标 RpcEndpoint 对应一个当前的发件箱,如果向多个目标 RpcEndpoint 发送信息,则有当前会有多个 OutBox。当消息放入 Outbox 后,紧接着通过 TransportClient 将消息发送出去。消息放入发件箱以及发送过程是在同一个线程中进行。
-
RpcAddress 表示远程的 RpcEndpointRef 的地址,Host + Port
-
TransportClient 是 Netty 通信客户端
一个 OutBox 对应一个 TransportClient,TransportClient 不断轮询OutBox,根据 OutBox 消息的 receiver 信息,请求对应的远程 TransportServer;
-
TransportServer 是 Netty 通信服务端
一个 RpcEndpoint 对应一个 TransportServer,接受远程消息后调用 Dispatcher 分发消息至自己的收件箱,或者对应的发件箱;
2.3 Spark 启动流程
- start-all.sh 脚本,实际是执行 java -cp Master 和 java -cp Worker;
- Master 启动时首先创建一个 RpcEnv 对象,负责管理所有通信逻辑;
- Master 通过 RpcEnv 对象创建一个 Endpoint,Master 就是一个 Endpoint,Worker 可以与其进行通信;
- Worker 启动时也是创建一个 RpcEnv 对象;
- Worker 通过 RpcEnv 对象创建一个 Endpoint;
- Worker 通过 RpcEnv 对象建立到 Master 的连接,获取到一个 RpcEndpointRef 对象,通过该对象可以与 Master 通信;
- Worker 向 Master 注册,注册内容包括主机名、端口、CPU Core 数量、内存数量;
- Master 接收到 Worker 的注册,将注册信息 WorkerInfo 维护在内存中的 HashSet 中,其中还包含了一个到Worker 的 RpcEndpointRef 对象引用;
- Master 回复 Worker 已经接收到注册,通过 Worker 的 RpcEndpointRef 发送消息,告知 Worker 已经注册成功;
- Worker 端收到成功注册响应后,开始周期性向 Master 发送心跳。
1. Master 源码分析
Master 启动脚本:start-master.sh
start-master.sh:
"${SPARK_HOME}/sbin"/spark-daemon.sh start $CLASS 1 \
--host $SPARK_MASTER_HOST --port $SPARK_MASTER_PORT --webui-port $SPARK_MASTER_WEBUI_PORT \
$ORIGINAL_ARGS
spark-daemon.sh:
case $option in
(start)
run_command class "$@"
;;
esac
run_command() {
mode="$1"
case "$mode" in
(class)
execute_command nice -n "$SPARK_NICENESS" "${SPARK_HOME}"/bin/spark-class "$command" "$@"
;;
esac
}
execute_command() {
if [ -z ${SPARK_NO_DAEMONIZE+set} ]; then
# 最终以后台守护进程的方式启动 Master
nohup -- "$@" >> $log 2>&1 < /dev/null &
fi
}
最终启动类: $CLASS
/opt/module/spark-standalone/bin/spark-class org.apache.spark.deploy.master.Master
--host hadoop201
--port 7077
--webui-port 8080
bin/spark-class
最终的启动命令:
/opt/module/jdk1.8.0_172/bin/java
-cp /opt/module/spark-standalone/conf/:/opt/module/spark-standalone/jars/*
-Xmx1g org.apache.spark.deploy.master.Master
--host hadoop201
--port 7077
--webui-port 8080
org.apache.spark.deploy.master.Master 类中,启动 Master 的入口为 Master 伴生对象的 main 方法:
-
Master 的伴生对象
private[deploy] object Master extends Logging { val SYSTEM_NAME = "sparkMaster" val ENDPOINT_NAME = "Master" // 启动 Master 的入口函数 def main(argStrings: Array[String]) { Utils.initDaemon(log) val conf = new SparkConf // 构建用于参数解析的实例 --host hadoop201 --port 7077 --webui-port 8080 val args = new MasterArguments(argStrings, conf) // 启动 RPC 通信环境和 MasterEndPoint(通信终端) val (rpcEnv, _, _) = startRpcEnvAndEndpoint(args.host, args.port, args.webUiPort, conf) rpcEnv.awaitTermination() } /** * Start the Master and return a three tuple of: * 启动 Master 并返回一个三元组 * (1) The Master RpcEnv * (2) The web UI bound port * (3) The REST server bound port, if any */ def startRpcEnvAndEndpoint( host: String, port: Int, webUiPort: Int, conf: SparkConf): (RpcEnv, Int, Option[Int]) = { val securityMgr = new SecurityManager(conf) // 创建 Master 端的 RpcEnv 环境 参数: sparkMaster hadoop201 7077 conf securityMgr // 实际类型是: NettyRpcEnv val rpcEnv: RpcEnv = RpcEnv.create(SYSTEM_NAME, host, port, conf, securityMgr) // 创建 Master对象, 该对象就是一个 RpcEndpoint, 在 RpcEnv中注册这个RpcEndpoint // 返回该 RpcEndpoint 的引用, 使用该引用来接收信息和发送信息 val masterEndpoint: RpcEndpointRef = rpcEnv.setupEndpoint(ENDPOINT_NAME, new Master(rpcEnv, rpcEnv.address, webUiPort, securityMgr, conf)) // 向 Master 的通信终端发送请求,获取 BoundPortsResponse 对象 // BoundPortsResponse 是一个样例类包含三个属性: rpcEndpointPort webUIPort restPort val portsResponse: BoundPortsResponse = masterEndpoint.askWithRetry[BoundPortsResponse](BoundPortsRequest) (rpcEnv, portsResponse.webUIPort, portsResponse.restPort) } }
-
RpcEnv 的创建
真正的创建是调用 NettyRpcEnvFactory 的 create 方法创建的
创建 NettyRpcEnv 的时候,会创建消息分发器,收件箱和存储远程地址与发件箱的 Map
RpcEnv.scala
def create( name: String, bindAddress: String, advertiseAddress: String, port: Int, conf: SparkConf, securityManager: SecurityManager, clientMode: Boolean): RpcEnv = { // 保存 RpcEnv 的配置信息 val config = RpcEnvConfig(conf, name, bindAddress, advertiseAddress, port, securityManager, clientMode) // 创建 NettyRpcEvn new NettyRpcEnvFactory().create(config) }
NettyRpcEnvFactory
private[rpc] class NettyRpcEnvFactory extends RpcEnvFactory with Logging { def create(config: RpcEnvConfig): RpcEnv = { val sparkConf = config.conf // Use JavaSerializerInstance in multiple threads is safe. However, if we plan to support // KryoSerializer in future, we have to use ThreadLocal to store SerializerInstance // 用于 Rpc传输对象时的序列化 val javaSerializerInstance: JavaSerializerInstance = new JavaSerializer(sparkConf) .newInstance() .asInstanceOf[JavaSerializerInstance] // 实例化 NettyRpcEnv val nettyEnv = new NettyRpcEnv( sparkConf, javaSerializerInstance, config.advertiseAddress, config.securityManager) if (!config.clientMode) { // 定义 NettyRpcEnv 的启动函数 val startNettyRpcEnv: Int => (NettyRpcEnv, Int) = { actualPort => nettyEnv.startServer(config.bindAddress, actualPort) (nettyEnv, nettyEnv.address.port) } try { // 启动 NettyRpcEnv Utils.startServiceOnPort(config.port, startNettyRpcEnv, sparkConf, config.name)._1 } catch { case NonFatal(e) => nettyEnv.shutdown() throw e } } nettyEnv } }
-
Master 伴生类(Master 端的 RpcEndpoint 启动)
Master 是一个 RpcEndpoint,它的声明周期是:constructor -> onStart -> receive* -> onStop
onStart 主要代码片段
// 创建 WebUI 服务器 webUi = new MasterWebUI(this, webUiPort) // 按照固定的频率去启动线程来检查 Worker 是否超时. 其实就是给自己发信息: CheckForWorkerTimeOut // 默认是每分钟检查一次. checkForWorkerTimeOutTask = forwardMessageThread.scheduleAtFixedRate(new Runnable { override def run(): Unit = Utils.tryLogNonFatalError { // 在 receive 方法中对 CheckForWorkerTimeOut 进行处理 self.send(CheckForWorkerTimeOut) } }, 0, WORKER_TIMEOUT_MS, TimeUnit.MILLISECONDS)
处理 Worker 是否超时的方法
/** Check for, and remove, any timed-out workers */ private def timeOutDeadWorkers() { // Copy the workers into an array so we don't modify the hashset while iterating through it val currentTime = System.currentTimeMillis() // 把超时的 Worker 从 Workers 中移除 val toRemove = workers.filter(_.lastHeartbeat < currentTime - WORKER_TIMEOUT_MS).toArray for (worker <- toRemove) { if (worker.state != WorkerState.DEAD) { logWarning("Removing %s because we got no heartbeat in %d seconds".format( worker.id, WORKER_TIMEOUT_MS / 1000)) removeWorker(worker) } else { if (worker.lastHeartbeat < currentTime - ((REAPER_ITERATIONS + 1) * WORKER_TIMEOUT_MS)) { workers -= worker // we've seen this DEAD worker in the UI, etc. for long enough; cull it } } } }
到此为止,Master 启动完成
2. Worker 源码分析
Worker 启动脚本:start-slaves.sh
start-slaves.sh
"${SPARK_HOME}/sbin/slaves.sh" cd "${SPARK_HOME}" \; "${SPARK_HOME}/sbin/start-slave.sh" "spark://$SPARK_MASTER_HOST:$SPARK_MASTER_PORT"
start-slave.sh
# worker类
CLASS="org.apache.spark.deploy.worker.Worker"
if [ "$SPARK_WORKER_WEBUI_PORT" = "" ]; then
# worker webui 端口号
SPARK_WORKER_WEBUI_PORT=8081
fi
if [ "$SPARK_WORKER_INSTANCES" = "" ]; then
start_instance 1 "$@"
fi
# 启动worker实例 spark-daemon.sh在启动Master的时候已经使用过一次了
function start_instance {
"${SPARK_HOME}/sbin"/spark-daemon.sh start $CLASS $WORKER_NUM \
--webui-port "$WEBUI_PORT" $PORT_FLAG $PORT_NUM $MASTER "$@"
}
最终启动类:
/opt/module/spark-standalone/bin/spark-class org.apache.spark.deploy.worker.Worker
--webui-port 8081
spark://hadoop201:7077
bin/spark-class
启动命令:
opt/module/jdk1.8.0_172/bin/java
-cp /opt/module/spark-standalone/conf/:/opt/module/spark-standalone/jars/*
-Xmx1g org.apache.spark.deploy.worker.Worker
--webui-port 8081
spark://hadoop201:7077
org.apache.spark.deploy.worker.Worker 类中,启动 Worker 的入口为 Worker 伴生对象的 main 方法:
-
Worker 伴生对象和 Master 一致
private[deploy] object Worker extends Logging { val SYSTEM_NAME = "sparkWorker" val ENDPOINT_NAME = "Worker" def main(argStrings: Array[String]) { Utils.initDaemon(log) val conf = new SparkConf // 构建解析参数的实例 val args = new WorkerArguments(argStrings, conf) // 启动 Rpc 环境和 Rpc 终端 val rpcEnv = startRpcEnvAndEndpoint(args.host, args.port, args.webUiPort, args.cores, args.memory, args.masters, args.workDir, conf = conf) rpcEnv.awaitTermination() } def startRpcEnvAndEndpoint( host: String, port: Int, webUiPort: Int, cores: Int, memory: Int, masterUrls: Array[String], workDir: String, workerNumber: Option[Int] = None, conf: SparkConf = new SparkConf): RpcEnv = { // The LocalSparkCluster runs multiple local sparkWorkerX RPC Environments val systemName = SYSTEM_NAME + workerNumber.map(_.toString).getOrElse("") val securityMgr = new SecurityManager(conf) // 创建 RpcEnv 实例 参数: "sparkWorker", "hadoop201", 8081, conf, securityMgr val rpcEnv = RpcEnv.create(systemName, host, port, conf, securityMgr) // 根据传入 masterUrls 得到 masterAddresses. 就是从命令行中传递过来的 Master 地址 val masterAddresses = masterUrls.map(RpcAddress.fromSparkURL(_)) // 最终实例化 Worker 得到 Worker 的 RpcEndpoint rpcEnv.setupEndpoint(ENDPOINT_NAME, new Worker(rpcEnv, webUiPort, cores, memory, masterAddresses, ENDPOINT_NAME, workDir, conf, securityMgr)) rpcEnv } }
-
Worker伴生类
onStart 方法
override def onStart() { // 第一次启动断言 Worker 未注册 assert(!registered) // 创建工作目录 createWorkDir() // 启动 shuffle 服务 shuffleService.startIfEnabled() // Worker的 WebUI webUi = new WorkerWebUI(this, workDir, webUiPort) webUi.bind() workerWebUiUrl = s"http://$publicAddress:${webUi.boundPort}" // 向 Master 注册 Worker registerWithMaster() }
registerWithMaster 方法
// 向所有的 Master 注册 registerMasterFutures = tryRegisterAllMasters()
tryRegisterAllMasters() 方法
private def tryRegisterAllMasters(): Array[JFuture[_]] = { masterRpcAddresses.map { masterAddress => // 从线程池中启动线程来执行 Worker 向 Master 注册 registerMasterThreadPool.submit(new Runnable { override def run(): Unit = { try { // 根据 Master 的地址得到一个 Master 的 RpcEndpointRef, 然后就可以和 Master 进行通讯了. val masterEndpoint = rpcEnv.setupEndpointRef(masterAddress, Master.ENDPOINT_NAME) // 向 Master 注册 registerWithMaster(masterEndpoint) } catch { } } }) } }
registerWithMaster 方法
private def registerWithMaster(masterEndpoint: RpcEndpointRef): Unit = { // 向 Master 对应的 receiveAndReply 方法发送信息 // 信息的类型是 RegisterWorker, 包括 Worker 的一些信息: id, 主机地址, 端口号, 内存, webUi masterEndpoint.ask[RegisterWorkerResponse](RegisterWorker( workerId, host, port, self, cores, memory, workerWebUiUrl)) .onComplete { // This is a very fast action so we can use "ThreadUtils.sameThread" // 注册成功 case Success(msg) => Utils.tryLogNonFatalError { handleRegisterResponse(msg) } // 注册失败 case Failure(e) => logError(s"Cannot register with master: ${masterEndpoint.address}", e) System.exit(1) }(ThreadUtils.sameThread) }
Master 的 receiveAndReply 方法
// 处理 Worker 的注册信息 case RegisterWorker( id, workerHost, workerPort, workerRef, cores, memory, workerWebUiUrl) => if (state == RecoveryState.STANDBY) { // 给发送者回应消息. 对方的 receive 方法会收到这个信息 context.reply(MasterInStandby) } else if (idToWorker.contains(id)) { // 如果要注册的 Worker 已经存在 context.reply(RegisterWorkerFailed("Duplicate worker ID")) } else { // 根据传来的信息封装 WorkerInfo val worker = new WorkerInfo(id, workerHost, workerPort, cores, memory, workerRef, workerWebUiUrl) if (registerWorker(worker)) { // 注册成功 persistenceEngine.addWorker(worker) // 响应信息 context.reply(RegisteredWorker(self, masterWebUiUrl)) schedule() } else { val workerAddress = worker.endpoint.address context.reply(RegisterWorkerFailed("Attempted to re-register worker at same address: " + workerAddress)) } } worker的handleRegisterResponse方法 case RegisteredWorker(masterRef, masterWebUiUrl) => logInfo("Successfully registered with master " + masterRef.address.toSparkURL) // 已经注册过了 registered = true // 更新 Master changeMaster(masterRef, masterWebUiUrl) // 通知自己给 Master 发送心跳信息 默认 1 分钟 4 次 forwordMessageScheduler.scheduleAtFixedRate(new Runnable { override def run(): Unit = Utils.tryLogNonFatalError { self.send(SendHeartbeat) } }, 0, HEARTBEAT_MILLIS, TimeUnit.MILLISECONDS)
Worker 的 receive 方法
case SendHeartbeat => if (connected) { // 给 Master 发送心跳 sendToMaster(Heartbeat(workerId, self)) }
Master 的 receive 方法
case Heartbeat(workerId, worker) => idToWorker.get(workerId) match { case Some(workerInfo) => // 记录该 Worker 的最新心跳 workerInfo.lastHeartbeat = System.currentTimeMillis() }
到此,Worker 启动完成
三、Spark 部署模式
① Standalone:独立模式;② YARN:统一的资源管理机制;③ Mesos:一个强大的分布式资源管理框架。
国内的企业,实际生产环境下使用的大多数的集群管理器是采用 Hadoop YARN。
3.1 Yarn cluster 模式源码分析(常用)
1. 流程分析
- 执行脚本提交任务,实际是启动一个 SparkSubmit 的 JVM 进程;
- SparkSubmit 类中的 main 方法反射调用 Client 的 main 方法;
- Client 创建 Yarn 客户端,然后向 Yarn 发送执行指令:bin/java ApplicationMaster;
- Yarn 框架收到指令后会选择一个 NM 启动 ApplicationMaster;
- ApplicationMaster 启动 Driver 线程,执行用户的作业;
- AM 向 RM 注册,申请资源;
- 获取资源后 AM 向 NM 发送指令:bin/java CoarseGrainedExecutorBacken;
- ExecutorBackend 进程会接收消息,启动计算对象 Executor 并跟 Driver 通信,注册已经启动的 Executor;
- Driver 分配任务并监控任务的执行。
注意:
- SparkSubmit、ApplicationMaster 和 CoarseGrainedExecutorBacken 是独立的进程;
- Client 和 Driver 是独立的线程;
- Executor 是一个对象。
2. 源码分析
执行 spark-submit 脚本:
bin/spark-submit \
--class org.apache.spark.examples.SparkPi \
--master yarn \
--deploy-mode cluster \
./examples/jars/spark-examples_2.11-2.1.1.jar \
100
Yarn 会按照下面的顺序一次启动3个进程
SparkSubmit
ApplicationMaster
CoarseGrainedExecutorB ackend
-
bin/spark-submit 启动脚本分析
-
启动类:org.apache.spark.deploy.SparkSubmit
exec "${SPARK_HOME}"/bin/spark-class org.apache.spark.deploy.SparkSubmit "$@"
-
bin/spark-class 脚本
exec "${CMD[@]}"
-
最终启动类
/opt/module/jdk1.8.0_172/bin/java -cp /opt/module/spark-yarn/conf/:/opt/module/spark-yarn/jars/*:/opt/module/hadoop-2.7.2/etc/hadoop/ org.apache.spark.deploy.SparkSubmit --master yarn --deploy-mode cluster --class org.apache.spark.examples.SparkPi ./examples/jars/spark-examples_2.11-2.1.1.jar 100
-
-
org.apache.spark.deploy.SparkSubmit 类的源码分析
SparkSubmit 伴生对象
main 方法
def main(args: Array[String]): Unit = { /* 参数 --master yarn --deploy-mode cluster --class org.apache.spark.examples.SparkPi ./examples/jars/spark-examples_2.11-2.1.1.jar 100 */ val appArgs = new SparkSubmitArguments(args) appArgs.action match { // 如果没有指定 action, 则 action 的默认值是: action = Option(action).getOrElse(SUBMIT) case SparkSubmitAction.SUBMIT => submit(appArgs) case SparkSubmitAction.KILL => kill(appArgs) case SparkSubmitAction.REQUEST_STATUS => requestStatus(appArgs) } }
submit 方法
/** * 使用提供的参数提交应用程序 * 有 2 步: * 1. 准备启动环境. * 根据集群管理器和部署模式为 child main class 设置正确的 classpath, 系统属性,应用参数 * 2. 使用启动环境调用 child main class 的 main 方法 */ @tailrec private def submit(args: SparkSubmitArguments): Unit = { // 准备提交环境 childMainClass = "org.apache.spark.deploy.yarn.Client" val (childArgs, childClasspath, sysProps, childMainClass) = prepareSubmitEnvironment(args) def doRunMain(): Unit = { if (args.proxyUser != null) { } else { runMain(childArgs, childClasspath, sysProps, childMainClass, args.verbose) } } if (args.isStandaloneCluster && args.useRest) { // 在其他任何模式, 仅仅运行准备好的主类 } else { doRunMain() } }
prepareSubmitEnvironment 方法
// In yarn-cluster mode, use yarn.Client as a wrapper around the user class if (isYarnCluster) { // 在 yarn 集群模式下, 使用 yarn.Client 来封装一下 user class childMainClass = "org.apache.spark.deploy.yarn.Client" }
doRunMain 方法
def doRunMain(): Unit = { if (args.proxyUser != null) { } else { runMain(childArgs, childClasspath, sysProps, childMainClass, args.verbose) } }
runMain 方法
/** * 使用给定启动环境运行 child class 的 main 方法 * 注意: 如果使用了cluster deploy mode, 主类并不是用户提供 */ private def runMain( childArgs: Seq[String], childClasspath: Seq[String], sysProps: Map[String, String], childMainClass: String, verbose: Boolean): Unit = { var mainClass: Class[_] = null try { // 使用反射的方式加载 childMainClass = "org.apache.spark.deploy.yarn.Client" mainClass = Utils.classForName(childMainClass) } catch { } // 反射出来 Client 的 main 方法 val mainMethod = mainClass.getMethod("main", new Array[String](0).getClass) if (!Modifier.isStatic(mainMethod.getModifiers)) { throw new IllegalStateException("The main method in the given main class must be static") } try { // 调用 main 方法. mainMethod.invoke(null, childArgs.toArray) } catch { } }
-
org.apache.spark.deploy.yarn.Client 源码分析
main 方法
def main(argStrings: Array[String]) { // 设置环境变量 SPARK_YARN_MODE 表示运行在 YARN mode // 注意: 任何带有 SPARK_ 前缀的环境变量都会分发到所有的进程, 也包括远程进程 System.setProperty("SPARK_YARN_MODE", "true") val sparkConf = new SparkConf // 对传递来的参数进一步封装 val args = new ClientArguments(argStrings) new Client(args, sparkConf).run() }
Client.run 方法
def run(): Unit = {
// 提交应用, 返回应用的 id
this.appId = submitApplication()
}
client.submitApplication 方法
/**
* 向 ResourceManager 提交运行 ApplicationMaster 的应用程序。
*/
def submitApplication(): ApplicationId = {
var appId: ApplicationId = null
try {
// 初始化 yarn 客户端
yarnClient.init(yarnConf)
// 启动 yarn 客户端
yarnClient.start()
// 从 RM 创建一个应用程序
val newApp = yarnClient.createApplication()
val newAppResponse = newApp.getNewApplicationResponse()
// 获取到 applicationID
appId = newAppResponse.getApplicationId()
reportLauncherState(SparkAppHandle.State.SUBMITTED)
launcherBackend.setAppId(appId.toString)
// Set up the appropriate contexts to launch our AM
// 设置正确的上下文对象来启动 ApplicationMaster
val containerContext = createContainerLaunchContext(newAppResponse)
// 创建应用程序提交任务上下文
val appContext = createApplicationSubmissionContext(newApp, containerContext)
// 提交应用给 ResourceManager 启动 ApplicationMaster
// "org.apache.spark.deploy.yarn.ApplicationMaster"
yarnClient.submitApplication(appContext)
appId
} catch {
}
}
方法: createContainerLaunchContext
private def createContainerLaunchContext(newAppResponse: GetNewApplicationResponse)
: ContainerLaunchContext = {
val amClass =
if (isClusterMode) { // 如果是 Cluster 模式
Utils.classForName("org.apache.spark.deploy.yarn.ApplicationMaster").getName
} else { // 如果是 Client 模式
Utils.classForName("org.apache.spark.deploy.yarn.ExecutorLauncher").getName
}
amContainer
}
至此,SparkSubmit 进程启动完毕
-
org.apache.spark.deploy.yarn.ApplicationMaster 源码分析
ApplicationMaster 伴生对象的 main 方法
def main(args: Array[String]): Unit = { SignalUtils.registerLogger(log) // 构建 ApplicationMasterArguments 对象, 对传来的参数做封装 val amArgs: ApplicationMasterArguments = new ApplicationMasterArguments(args) SparkHadoopUtil.get.runAsSparkUser { () => // 构建 ApplicationMaster 实例 ApplicationMaster 需要与 RM通讯 master = new ApplicationMaster(amArgs, new YarnRMClient) // 运行 ApplicationMaster 的 run 方法, run 方法结束之后, 结束 ApplicationMaster 进程 System.exit(master.run()) } }
ApplicationMaster 伴生类的 run 方法
final def run(): Int = { // 关键核心代码 try { val fs = FileSystem.get(yarnConf) if (isClusterMode) { runDriver(securityMgr) } else { runExecutorLauncher(securityMgr) } } catch { } exitCode }
runDriver 方法
private def runDriver(securityMgr: SecurityManager): Unit = { addAmIpFilter() // 开始执行用户类. 启动一个子线程来执行用户类的 main 方法. 返回值就是运行用户类的子线程. // 线程名就叫 "Driver" userClassThread = startUserApplication() val totalWaitTime = sparkConf.get(AM_MAX_WAIT_TIME) try { // 注册 ApplicationMaster , 其实就是请求资源 registerAM(sc.getConf, rpcEnv, driverRef, sc.ui.map(_.appUIAddress).getOrElse(""), securityMgr) // 线程 join: 把userClassThread线程执行完毕之后再继续执行当前线程. userClassThread.join() } catch { } }
startUserApplication 方法
private def startUserApplication(): Thread = {
// 得到用户类的 main 方法
val mainMethod = userClassLoader.loadClass(args.userClass)
.getMethod("main", classOf[Array[String]])
// 创建及线程
val userThread = new Thread {
override def run() {
try {
// 调用用户类的主函数
mainMethod.invoke(null, userArgs.toArray)
} catch {
} finally {
}
}
}
userThread.setContextClassLoader(userClassLoader)
userThread.setName("Driver")
userThread.start()
userThread
}
registerAM 方法
private def registerAM(
_sparkConf: SparkConf,
_rpcEnv: RpcEnv,
driverRef: RpcEndpointRef,
uiAddress: String,
securityMgr: SecurityManager) = {
// 向 RM 注册, 得到 YarnAllocator
allocator = client.register(driverUrl,
driverRef,
yarnConf,
_sparkConf,
uiAddress,
historyAddress,
securityMgr,
localResources)
// 请求分配资源
allocator.allocateResources()
}
allocator.allocateResources() 方法
/**
请求资源,如果 Yarn 满足了我们的所有要求,我们就会得到一些容器(数量: maxExecutors)。
通过在这些容器中启动 Executor 来处理 YARN 授予我们的任何容器。
必须同步,因为在此方法中读取的变量会被其他方法更改。
*/
def allocateResources(): Unit = synchronized {
if (allocatedContainers.size > 0) {
handleAllocatedContainers(allocatedContainers.asScala)
}
}
handleAllocatedContainers 方法
/**
处理 RM 授权给我们的容器
*/
def handleAllocatedContainers(allocatedContainers: Seq[Container]): Unit = {
val containersToUse = new ArrayBuffer[Container](allocatedContainers.size)
runAllocatedContainers(containersToUse)
}
runAllocatedContainers 方法
/**
* Launches executors in the allocated containers.
在已经分配的容器中启动 Executors
*/
private def runAllocatedContainers(containersToUse: ArrayBuffer[Container]): Unit = {
// 每个容器上启动一个 Executor
for (container <- containersToUse) {
if (numExecutorsRunning < targetNumExecutors) {
if (launchContainers) {
launcherPool.execute(new Runnable {
override def run(): Unit = {
try {
new ExecutorRunnable(
Some(container),
conf,
sparkConf,
driverUrl,
executorId,
executorHostname,
executorMemory,
executorCores,
appAttemptId.getApplicationId.toString,
securityMgr,
localResources
).run() // 启动 executor
updateInternalState()
} catch {
}
}
})
} else {
}
} else {
}
}
}
ExecutorRunnable.run 方法
def run(): Unit = {
logDebug("Starting Executor Container")
// 创建 NodeManager 客户端
nmClient = NMClient.createNMClient()
// 初始化 NodeManager 客户端
nmClient.init(conf)
// 启动 NodeManager 客户端
nmClient.start()
// 启动容器
startContainer()
}
ExecutorRunnable.startContainer()
def startContainer(): java.util.Map[String, ByteBuffer] = {
val ctx = Records.newRecord(classOf[ContainerLaunchContext])
.asInstanceOf[ContainerLaunchContext]
// 准备要执行的命令
val commands = prepareCommand()
ctx.setCommands(commands.asJava)
// Send the start request to the ContainerManager
try {
// 启动容器
nmClient.startContainer(container.get, ctx)
} catch {
}
}
ExecutorRunnable.prepareCommand 方法
private def prepareCommand(): List[String] = {
val commands = prefixEnv ++ Seq(
YarnSparkHadoopUtil.expandEnvironment(Environment.JAVA_HOME) + "/bin/java",
"-server") ++
javaOpts ++
// 要执行的类
Seq("org.apache.spark.executor.CoarseGrainedExecutorBackend",
"--driver-url", masterAddress,
"--executor-id", executorId,
"--hostname", hostname,
"--cores", executorCores.toString,
"--app-id", appId) ++
userClassPath ++
Seq(
s"1>${ApplicationConstants.LOG_DIR_EXPANSION_VAR}/stdout",
s"2>${ApplicationConstants.LOG_DIR_EXPANSION_VAR}/stderr")
commands.map(s => if (s == null) "null" else s).toList
}
至此,ApplicationMaster 进程启动完毕
-
org.apache.spark.executor.CoarseGrainedExecutorBackend 源码分析
CoarseGrainedExecutorBackend 伴生对象
main 方法
def main(args: Array[String]) { // 启动 CoarseGrainedExecutorBackend run(driverUrl, executorId, hostname, cores, appId, workerUrl, userClassPath) // 运行结束之后退出进程 System.exit(0) }
run 方法
/* 准备 RpcEnv */ private def run( driverUrl: String, executorId: String, hostname: String, cores: Int, appId: String, workerUrl: Option[String], userClassPath: Seq[URL]) { SparkHadoopUtil.get.runAsSparkUser { () => val env = SparkEnv.createExecutorEnv( driverConf, executorId, hostname, port, cores, cfg.ioEncryptionKey, isLocal = false) env.rpcEnv.setupEndpoint("Executor", new CoarseGrainedExecutorBackend( env.rpcEnv, driverUrl, executorId, hostname, cores, userClassPath, env)) } }
CoarseGrainedExecutorBackend 伴生类
继承自: ThreadSafeRpcEndpoint 是一个 RpcEndpoint
查看生命周期方法
onStart 方法
连接到 Driver,并向 Driver注册Executor
override def onStart() { rpcEnv.asyncSetupEndpointRefByURI(driverUrl).flatMap { ref => // This is a very fast action so we can use "ThreadUtils.sameThread" driver = Some(ref) // 向驱动注册 Executor 关键方法 ref.ask[Boolean](RegisterExecutor(executorId, self, hostname, cores, extractLogUrls)) }(ThreadUtils.sameThread).onComplete { case Success(msg) => case Failure(e) => // 注册失败, 退出 executor exitExecutor(1, s"Cannot register with driver: $driverUrl", e, notifyDriver = false) }(ThreadUtils.sameThread) }
Driver 端的 CoarseGrainedSchedulerBackend 的 receiveAndReply 方法
override def receiveAndReply(context: RpcCallContext): PartialFunction[Any, Unit] = { // 接收注册 Executor case RegisterExecutor(executorId, executorRef, hostname, cores, logUrls) => if (executorDataMap.contains(executorId)) { // 已经注册过了 } else { // 给 Executor 发送注册成功的信息 executorRef.send(RegisteredExecutor) } }
Eexcutor 端的 CoarseGrainedExecutorBackend 的 receive 方法
override def receive: PartialFunction[Any, Unit] = { // 向 Driver 注册成功 case RegisteredExecutor => logInfo("Successfully registered with driver") try { // 创建 Executor 对象 注意: Executor 其实是一个对象 executor = new Executor(executorId, hostname, env, userClassPath, isLocal = false) } catch { } }
至此,Executor 创建完毕
执行流程图
3.2 Yarn client 模式源码分析
1. 流程分析
- 执行脚本提交任务,实际就是启动一个 SparkSubmit 的 JVM 进程;
- SparkSubmit 类中的 main 方法反射调用用户代码的 main 方法;
- 启动 Driver 线程,执行用户的作业,并创建 ScheduleBackend;
- YarnClientSchedulerBackend 向 RM 发送指令:bin/java ExecutorLauncher;
- Yarn 框架收到指令会在指定的 NM 中启动 ExecutorLauncher (ExecutorLauncher 中还是调用了 AM 的 main)
- AM 向 RM 注册,申请资源;
- 获取资源后 AM 向 NM 发送指令:bin/java CoarseGrainedExecutorBacken;
- ExecutorBackend 进程会接收消息,启动计算对象 Executor 并跟 Driver 通信,注册已启动的 Executor;
- Driver 分配任务并监控任务的执行。
注意:
- SparkSubmit、ApplicationMaster 和 CoarseGrainedExecutorBacken 是独立的进程;
- Client 和 Driver 是独立的线程;
- Executor 是一个对象。
2. 源码分析
执行 spark-submit 脚本:
bin/spark-submit \
--class org.apache.spark.examples.SparkPi \
--master yarn \
--deploy-mode client \ # client 模式
./examples/jars/spark-examples_2.11-2.1.1.jar \
100
脚本最终启动的类:
/opt/module/jdk1.8.0_172/bin/java
-cp /opt/module/spark-yarn/conf/:/opt/module/spark-yarn/jars/*:/opt/module/hadoop-2.7.2/etc/hadoop/
-Xmx1g
org.apache.spark.deploy.SparkSubmit
--master yarn
--deploy-mode client # client 模式
--class org.apache.spark.examples.SparkPi
./examples/jars/spark-examples_2.11-2.1.1.jar 100
运行过程中 Yarn 会依次启动3个不同进程
SparkSubmit
ExecutorLauncher
CoarseGrainedExecutorBackend
同样 org.apache.spark.deploy.SparkSubmit 的伴生对象 main 为入口,进入 submit 方法,再进入 prepareSubmitEnvironment 方法
/*
client 模式下, 直接启动用户的主类
*/
if (deployMode == CLIENT || isYarnCluster) {
// 如果是客户端模式, childMainClass 就是用户的类
// 集群模式下, childMainClass 被重新赋值为 org.apache.spark.deploy.yarn.Client
childMainClass = args.mainClass
}
-
然后不会创建 ApplicationMaster,而是直接执行用户类的 main 方法,然后开始实例化 SparkContext
val (sched, ts):(SchedulerBackend, TaskScheduler) = SparkContext.createTaskScheduler(this, master, deployMode) _schedulerBackend = sched _taskScheduler = ts // 启动 YarnScheduler _taskScheduler.start()
SparkContext.createTaskScheduler 方法
private def createTaskScheduler( sc: SparkContext, master: String, deployMode: String): (SchedulerBackend, TaskScheduler) = { import SparkMasterRegex._ master match { case masterUrl => // 得到的是 YarnClusterManager val cm = getClusterManager(masterUrl) match { case Some(clusterMgr) => clusterMgr case None => throw new SparkException("Could not parse Master URL: '" + master + "'") } try { // 创建 YarnScheduler val scheduler: TaskScheduler = cm.createTaskScheduler(sc, masterUrl) // 创建 YarnClientSchedulerBackend val backend: SchedulerBackend = cm.createSchedulerBackend(sc, masterUrl, scheduler) cm.initialize(scheduler, backend) (backend, scheduler) } catch { } } }
YarnClusterManager 类
private[spark] class YarnClusterManager extends ExternalClusterManager { override def canCreate(masterURL: String): Boolean = { masterURL == "yarn" } override def createTaskScheduler(sc: SparkContext, masterURL: String): TaskScheduler = { sc.deployMode match { case "cluster" => new YarnClusterScheduler(sc) case "client" => new YarnScheduler(sc) case _ => throw new SparkException(s"Unknown deploy mode '${sc.deployMode}' for Yarn") } } override def createSchedulerBackend(sc: SparkContext, masterURL: String, scheduler: TaskScheduler): SchedulerBackend = { sc.deployMode match { case "cluster" => new YarnClusterSchedulerBackend(scheduler.asInstanceOf[TaskSchedulerImpl], sc) case "client" => new YarnClientSchedulerBackend(scheduler.asInstanceOf[TaskSchedulerImpl], sc) case _ => throw new SparkException(s"Unknown deploy mode '${sc.deployMode}' for Yarn") } } override def initialize(scheduler: TaskScheduler, backend: SchedulerBackend): Unit = { scheduler.asInstanceOf[TaskSchedulerImpl].initialize(backend) } }
_taskScheduler.start() YarnClientSchedulerBackend 的 start 方法
/** * Create a Yarn client to submit an application to the ResourceManager. * This waits until the application is running. * * 创建客户端, 提交应用给 ResourceManager * 会一直等到应用开始执行 */ override def start() val driverHost = conf.get("spark.driver.host") val driverPort = conf.get("spark.driver.port") val argsArrayBuf = new ArrayBuffer[String]() argsArrayBuf += ("--arg", hostport) val args = new ClientArguments(argsArrayBuf.toArray) client = new Client(args, conf) // 使用 Client 提交应用 bindToYarn(client.submitApplication(), None) waitForApplication() }
-
org.apache.spark.deploy.yarn.Client 源码再分析
submitApplication 方法
yarnClient.submitApplication(appContext)
ExecutorLauncher 类 yarnClient 提交应用的时候, 把要执行的主类(ExecutorLauncher)封装到配置中。所以不是启动 ApplicationMaster,而是启动 ExecutorLauncher
// createContainerLaunchContext() val amClass = if (isClusterMode) { Utils.classForName("org.apache.spark.deploy.yarn.ApplicationMaster").getName } else { Utils.classForName("org.apache.spark.deploy.yarn.ExecutorLauncher").getName } /** * This object does not provide any special functionality. It exists so that it's easy to tell * apart the client-mode AM from the cluster-mode AM when using tools such as ps or jps. * * 这个对象不提供任何特定的功能. * * 它的存在使得在使用诸如ps或jps之类的工具时,很容易区分客户机模式AM和集群模式AM。 * */ object ExecutorLauncher { def main(args: Array[String]): Unit = { ApplicationMaster.main(args) } }
-
ApplicationMaster 源码再分析
run 方法
final def run(): Int = { try { if (isClusterMode) { runDriver(securityMgr) } else { // 非集群模式, 直接执行 ExecutorLauncher, 而不在需要运行 Driver runExecutorLauncher(securityMgr) } } catch { } exitCode }
runExecutorLauncher
private def runExecutorLauncher(securityMgr: SecurityManager): Unit = { val driverRef = waitForSparkDriver() addAmIpFilter() registerAM(sparkConf, rpcEnv, driverRef, sparkConf.get("spark.driver.appUIAddress", ""), securityMgr) // In client mode the actor will stop the reporter thread. reporterThread.join() }
之后的执行流程和 yarn-cluster 模式一样
执行流程图
四、Spark 任务调度
4.1 任务调度概述
Spark Application 包括 Job,Stage,Task 三个概念:
- Job 是以 action 算子为界限的,一个 action 算子触发一个 Job;
- Stage 是 Job 的子集,以 RDD 宽依赖(即 Shuffe)为界限,遇到 Shuffle 划分一次 Stage;
- Task 是 Stage 的子集,以 并行度(分区数)划分,Stage 中有多少分区就有多少 Task。
Spark 的任务调度分为两路,一路是 Stage 级别的调度,一路是 Task 级别的调度。
Spark RDD 通过其 Transactions 算子,形成 RDD 血缘关系,即 DAG,最后通过 Action 的调用,触发 Job 并调度执。
DAGScheduler 负责 Stage 级的调度,主要是将 Job 切分成若干 Stages,并将每个 Stage 打包成 TaskSet 交给 TaskScheduler 调度。
TaskScheduler 负责 Task 级别的调度,将 DAGScheduler 传过来的 TaskSet 按照指定的调度策略分发到 Executor 上执行,调度过程中 SchedulerBackend 负责提供可用资源,其中 SchedulerBackend 有多种实现,分别对接不同的资源管理系统。
- Driver 初始化 SparkContext 过程中,会分别初始化 DAGScheduler、TaskScheduler、SchedulerBackend 以及 HeartReceiver,并启动 SchedulerBackend 以及 HeartbeatReceiver。
- SchedulerBackend 通过 ApplicationMaster 申请资源,并不断从 TaskScheduler 中拿到合适的 Task 分发到 Executor 执行。
- HeartbeatReceiver 负责接收 Executor 的心跳信息,监控 Executor 的存活状况,并通知到 TaskScheduler。
4.2 Stage 级别调度
Spark 的任务调度就是从 DAG 切割开始,主要是由 DAGScheduler 来完成。当遇到一个 Aciton 算子后就会触发一个 Job 的计算,并交给 DAGScheduler 来提交,下图涉及到 Job 提交的相关方法调用流程图。
- Job 由最终的 RDD 和 Action 方法封装而成;
- SparkContext 将 Job 交给 DAGScheduler 提交,它会根据 RDD 的 DAG 进行切分,将一个 Job 切分成若干个 Stage,具体划分策略是,由 最终的 RDD 不断通过依赖回溯判断父依赖是否是宽依赖,即以 Shuffle 为界。
- 划分的 Stages 分为两种,一种是 ResultStage,为 DAG 为最下的的 Stage,由 Action 方法决定;另一种是 ShuffleMapStage,为下游 Stage 准备数据。
- Stage 的划分策略本质上是一个深度优先搜索算法。
一个 Stage 是否被提交,需要判断它的父 Stage 是否执行,只有在父 Stage 执行完毕才能提交当前 Stage。如果一个Stage 没有父 Stage,那么从该 Stage 开始提交。
Stage 提交时会将 Task 信息(分区信息以及方法等)序列化并被打包成 TaskSet 交给 TaskScheduler,一个 Partition 对应一个 Task,另一方面 TaskScheduler 会监控 Stage 的运行状态,只有 Executor 丢失或者 Task 由于 Fetch 失败才需要重新提交失败的 Stage 以调度运行失败的任务,其他类型的 Task 失败会在 TaskScheduler 的调度过程中重试。相对来说,DAGScheduler 做的事情较为简单,仅仅是在 Stage 层面上划分 DAG,提交 Stage 并监控相关状态信息。
4.3 Task 级别调度
Spark Task 的调度是由 TaskScheduler 来完成的,DAGScheduler 将 Stage 打包到 TaskSet 交给 TaskScheduler,TaskScheduler 将 TaskSet 封装为 TaskSetManager 加入到调度队列中。TaskSetManager 封装了一个 Stage 的所有Task,并负责管理调度这些 Task。
TaskScheduler 初始化后会启动 SchedulerBackend,SchedulerBackend 负责跟外界打交道,接收 Executor 的注册信息,并维护 Executor 的状态。SchedulerBackend 会定期“询问”TaskScheduler 有没有要运行的 TaskSetManager,此时,TaskScheduler 会从调度队列中按照指定的调度策略选择 TaskSetManager 取调度运行,大致的流程如下:
将 TaskSetManager 加入 RootPool 调度池中之后,调用 SchedulerBackend 的 RiviveOffers 方法给 DriverEndpoint 发送 ReviveOffer 消息;DriverEndpoint 收到 ReviveOffer 消息后调用 makeOffers 方法,过滤出活跃状态的 Executor(这些 Executor 都是任务启动时反向注册到 Driver 的 Executor),然后将 Executor 封装成 WorkerOffer 对象;准备好计算资源(WorkerOffer)后,TaskScheduler 基于这些资源调用 ResourceOffer在Executor 上分配 Task。
1. FIFO 调度策略
FIFO 调度策略,对 TaskSetManager 按照先进先出的策略进行调度。
2. FAIR 调度策略
FAIR 模式中有一个 rootPool 和多个子 Pool,各个子 Pool 中存储着所有待分配的 TaskSetMagager。
Pool 和 TaskSetManager 都继承了 Schedulable 特质,都包含三个属性:runningTasks值(正在运行的Task数)、minShare值、weight值。FAIR 模式中,会先对子 Pool 进行排序,再对子 Pool 里面的 TaskSetManager 进行排序,minShare、weight 的值均在公平调度配置文件 fairscheduler.xml 中被指定,调度池在构建阶段会读取此文件的相关配置。
Pool 和 TaskSetManager 的排序规则:
- runningTasks < minShare 的 TaskSetManager 先执行,都小于进入2,都大于进入3;
- runningTasks / math(minShare, 1.0)的比值(minShare 使用率)越小月优先,相等进入4;
- runningTasks / weight 的比值(权重使用率)越小月优先,相等进入4;
- 比较两者的名字,名字越小越优先。
排序完成后,所有的 TaskSetManager 被放入一个 ArrayBuffer 里,之后依次被取出并发送给 Executor 执行。
设置 FAIR 调度策略
val conf = new SparkConf().setMaster(...).setAppName(...)
conf.set("spark.scheduler.mode", "FAIR")
3. 本地化调度
DAGScheduler 切割 Job,划分 Stage,通过调用 submitStage 来提交一个 Stage 对应的 Tasks,submitStage 会调用 submitMissingTasks,submitMissingTasks 确定每个需要计算的 task 的 preferredLocations,通过调用 getPreferrdeLocations() 得到 partition 的优先位置,由于一个 partition 对应一个 Task,此 partition 的优先位置就是 Task 的优先位置,根据每个 Task 的优先位置,确定 Task 的 Locality 级别,Locality 有五种,优先级由高到低顺序:
名称 | 解析 |
---|---|
PROCESS_LOCAL | 进程本地化,Task 和数据在同一个 Executor中,性能最好。 |
NODE_LOCAL | 节点本地化,Task 和数据在同一个节点中,但是 Task 和数据不在同一个Executor中,数据需要在进程间进行传输。 |
RACK_LOCAL | 机架本地化,Task和数据在同一个机架的两个节点上,数据需要通过网络在节点之间进行传输。 |
NO_PREF | 对于 Task来说,从哪里获取都一样,没有好坏之分。 |
ANY | Task 和数据可以在集群的任何地方,而且不在一个机架中,性能最差。 |
每个 Task 以最高的本地性级别来启动,当本地性级别对应的所有节点都没有空闲资源而启动失败,此时并不会马上降启动级别而是在某个时间长度内再次以本地性级别来启动该 Task,若超过限时时间则降级启动,去尝试下一个本地性级别,依次类推。
可以调大每个类别的最大容忍延迟时间,在等待阶段对应的 Executor 可能就会有相应的资源去执行此 Task,这就在一定程度上提升了运行性能。
4. 失败重试和黑名单
一个 Task 被提交到某个 Executor 启动执行后,Executor 会将执行状态反馈给 SchedulerBackend,SchedulerBackend 继续反馈给 TaskScheduler,TaskScheduler 通知该 Task 对应的 TaskSetManager,TaskSetManager 就能够获取自己的 Task 的运行状态,若检测到 Task 在这个 Executor 上启动失败的次数没有超过最大重试次数,这个 Task 就会被放回待调度的 Task Pool 中,否则整个 Application 失败。
Task 就会被放回待调度的 Task Pool 中时,TaskSetManager 会记录该 Task 上一次失败所在的 ExecutorId 和 Host,下次再调度这个 Task 时,用黑名单机制,避免它被调度到上一次失败的节点上,起到一定的容错作用。这个黑名单机制有一定的有效时间,也就是在某一段时间内不会再向这个 Executor 调度这个 Task 了。
4.4 Stage 级别调度源码
1. SparkContext 初始化
在 SparkContext 初始化的时候创建并启动三个组件:SchedulerBackend TaskScheduler DAGScheduler
// 用来与其他组件通讯用
private var _schedulerBackend: SchedulerBackend = _
// DAG 调度器, 是调度系统的中的中的重要组件之一, 负责创建 Job, 将 DAG 中的 RDD 划分到不同的 Stage, 提交 Stage 等.
// SparkUI 中有关 Job 和 Stage 的监控数据都来自 DAGScheduler
@volatile private var _dagScheduler: DAGScheduler = _
// TaskScheduler 按照调度算法对集群管理器已经分配给应用程序的资源进行二次调度后分配给任务
// TaskScheduler 调度的 Task 是由 DAGScheduler 创建, 所以 DAGScheduler 是 TaskScheduler 的前置调度器
private var _taskScheduler: TaskScheduler = _
// 创建 SchedulerBackend 和 TaskScheduler
val (sched, ts):(SchedulerBackend, TaskScheduler) = SparkContext.createTaskScheduler(this, master, deployMode)
_schedulerBackend = sched
_taskScheduler = ts
// 创建 DAGScheduler
_dagScheduler = new DAGScheduler(this)
// 启动 TaskScheduler, 内部会也会启动 SchedulerBackend
_taskScheduler.start()
2. RDD 类源码分析
从一个 action 行动算子开始,行动算子中会调用 sc.runJob 方法
def runJob[T, U: ClassTag](
rdd: RDD[T],
func: (TaskContext, Iterator[T]) => U,
partitions: Seq[Int],
resultHandler: (Int, U) => Unit): Unit = {
// 作业的切分
dagScheduler.runJob(rdd, cleanedFunc, partitions, callSite, resultHandler, localProperties.get)
}
3. DAGScheduler 类源码分析
sc.runJob 中会调用 DAGScheduler 的 runJob 方法
def runJob[T, U](
rdd: RDD[T],
func: (TaskContext, Iterator[T]) => U,
partitions: Seq[Int],
callSite: CallSite,
resultHandler: (Int, U) => Unit,
properties: Properties): Unit = {
// 提交任务 返回值 JobWaiter 用来确定 Job 的成功与失败
val waiter = submitJob(rdd, func, partitions, callSite, resultHandler, properties)
}
dagScheduler.submitJob 方法
def submitJob[T, U](
rdd: RDD[T],
func: (TaskContext, Iterator[T]) => U,
partitions: Seq[Int],
callSite: CallSite,
resultHandler: (Int, U) => Unit,
properties: Properties): JobWaiter[U] = {
// 创建 JobWaiter 对象
val waiter = new JobWaiter(this, jobId, partitions.size, resultHandler)
// 向内部事件循环器发送消息 JobSubmitted
eventProcessLoop.post(JobSubmitted(
jobId, rdd, func2, partitions.toArray, callSite, waiter,
SerializationUtils.clone(properties)))
waiter
}
DAGSchedulerEventProcessLoop 类
DAGSchedulerEventProcessLoop 是 DAGSheduler 内部的事件循环处理器,用于处理 DAGSchedulerEvent 类型的事件。前面发送的是 JobSubmitted 类型的事件。
private def doOnReceive(event: DAGSchedulerEvent): Unit = event match {
case JobSubmitted(jobId, rdd, func, partitions, callSite, listener, properties) =>
// 处理提交的 Job
dagScheduler.handleJobSubmitted(jobId, rdd, func, partitions, callSite, listener, properties)
}
DAGScheduler.handleJobSubmitted 处理提交的 Job
private[scheduler] def handleJobSubmitted(jobId: Int,
finalRDD: RDD[_],
func: (TaskContext, Iterator[_]) => _,
partitions: Array[Int],
callSite: CallSite,
listener: JobListener,
properties: Properties) {
var finalStage: ResultStage = null
try {
// New stage creation may throw an exception if, for example, jobs are run on a
// HadoopRDD whose underlying HDFS files have been deleted.
// Stage 的划分是从后向前推断的, 所以先创建最后的阶段
finalStage = createResultStage(finalRDD, func, partitions, jobId, callSite)
} catch {
}
submitStage(finalStage)
}
DAGScheduler.createResultStage() 方法
private def createResultStage(
rdd: RDD[_],
func: (TaskContext, Iterator[_]) => _,
partitions: Array[Int],
jobId: Int,
callSite: CallSite): ResultStage = {
// 1. 获取所有父 Stage 的列表
val parents: List[Stage] = getOrCreateParentStages(rdd, jobId)
// 2. 给 resultStage 生成一个 id
val id = nextStageId.getAndIncrement()
// 3. 创建 ResultStage
val stage: ResultStage = new ResultStage(id, rdd, func, partitions, parents, jobId, callSite)
// 4. stageId 和 ResultStage 做映射
stageIdToStage(id) = stage
updateJobIdStageIdMaps(jobId, stage)
stage
}
DAGScheduler.getOrCreateParentStages() 方法
private def getOrCreateParentStages(rdd: RDD[_], firstJobId: Int): List[Stage] = {
// 获取所有的 Shuffle 依赖(宽依赖)
getShuffleDependencies(rdd).map { shuffleDep =>
// 对每个 shuffle 依赖, 获取或者创建新的 Stage: ShuffleMapStage
getOrCreateShuffleMapStage(shuffleDep, firstJobId)
}.toList
}
一共有两种 Stage:ResultStage 和 ShuffleMapStage
// 采用递归调用的方法得到所有宽依赖
private[scheduler] def getShuffleDependencies(rdd: RDD[_]): HashSet[ShuffleDependency[_, _, _]] = {
val parents = new HashSet[ShuffleDependency[_, _, _]]
val visited = new HashSet[RDD[_]]
val waitingForVisit = new Stack[RDD[_]]
waitingForVisit.push(rdd)
while (waitingForVisit.nonEmpty) {
val toVisit = waitingForVisit.pop()
if (!visited(toVisit)) {
visited += toVisit
toVisit.dependencies.foreach {
case shuffleDep: ShuffleDependency[_, _, _] =>
parents += shuffleDep
case dependency =>
waitingForVisit.push(dependency.rdd)
}
}
}
parents
}
DAGScheduler.submitStage(finalStage) 方法,提交 finalStage
private def submitStage(stage: Stage) {
val jobId = activeJobForStage(stage)
if (jobId.isDefined) {
if (!waitingStages(stage) && !runningStages(stage) && !failedStages(stage)) {
// 获取没有提交的父 Stage
val missing = getMissingParentStages(stage).sortBy(_.id)
// 如果为空, 则提交这个 Stage
if (missing.isEmpty) {
submitMissingTasks(stage, jobId.get)
} else { // 如果还有父 Stage , 则递归向上查找
for (parent <- missing) {
submitStage(parent)
}
waitingStages += stage
}
}
} else {
}
}
从前面的分析可以看到,阶段划分是从后向前,最前面的 Stage 先提交
DAGScheduler.submitMissingTasks 方法
private def submitMissingTasks(stage: Stage, jobId: Int) {
// 根据 Stage 的不同,划分 task
val tasks: Seq[Task[_]] = try {
stage match {
case stage: ShuffleMapStage =>
partitionsToCompute.map { id =>
val locs = taskIdToLocations(id)
val part = stage.rdd.partitions(id)
new ShuffleMapTask(
stage.id,
stage.latestInfo.attemptId,
taskBinary, part, locs, stage.latestInfo.taskMetrics,
properties, Option(jobId),
Option(sc.applicationId),
sc.applicationAttemptId
)
}
case stage: ResultStage =>
partitionsToCompute.map { id =>
val p: Int = stage.partitions(id)
val part = stage.rdd.partitions(p)
val locs = taskIdToLocations(id)
new ResultTask(stage.id, stage.latestInfo.attemptId,
taskBinary, part, locs, id, properties, stage.latestInfo.taskMetrics,
Option(jobId), Option(sc.applicationId), sc.applicationAttemptId)
}
}
} catch {
}
// 提交任务
if (tasks.size > 0) {
// 把 tasks 封装到 TaskSet 中,交给 taskScheduler 来提交
taskScheduler.submitTasks(new TaskSet(
tasks.toArray, stage.id, stage.latestInfo.attemptId, jobId, properties))
} else {
}
}
4. TaskScheduler 类源码分析
TaskScheduler 是一个 Trait,分析它的实现类:TaskSchedulerImpl
override def submitTasks(taskSet: TaskSet) {
val tasks = taskSet.tasks
this.synchronized {
// 创建 TaskManger 对象. 用来追踪每个任务
val manager: TaskSetManager = createTaskSetManager(taskSet, maxTaskFailures)
val stage = taskSet.stageId
// manage 和 TaskSet 交给合适的任务调度器来调度
schedulableBuilder.addTaskSetManager(manager, manager.taskSet.properties)
}
// 跟 ExecutorBackend 通讯
backend.reviveOffers()
}
CoarseGrainedSchedulerBackend.reviveOffers
override def reviveOffers() {
// DriverEndpoint 给自己发信息: ReviveOffers
driverEndpoint.send(ReviveOffers)
}
DriverEndpoint.receive 方法
private def makeOffers() {
// 过滤出 Active 的 Executor
val activeExecutors = executorDataMap.filterKeys(executorIsAlive)
// 封装资源
val workOffers = activeExecutors.map { case (id, executorData) =>
new WorkerOffer(id, executorData.executorHost, executorData.freeCores)
}.toIndexedSeq
// 启动任务
launchTasks(scheduler.resourceOffers(workOffers))
}
DriverEndpoint.launchTasks
private def launchTasks(tasks: Seq[Seq[TaskDescription]]) {
for (task <- tasks.flatten) {
// 序列化任务
val serializedTask = ser.serialize(task)
if (serializedTask.limit >= maxRpcMessageSize) {
}
else {
val executorData = executorDataMap(task.executorId)
executorData.freeCores -= scheduler.CPUS_PER_TASK
// 发送任务到 Executor. CoarseGrainedExecutorBackend 会收到消息
executorData.executorEndpoint.send(LaunchTask(new SerializableBuffer(serializedTask)))
}
}
}
-
CoarseGrainedExecutorBackend 源码分析
override def receive: PartialFunction[Any, Unit] = { // case LaunchTask(data) => if (executor == null) { exitExecutor(1, "Received LaunchTask command but executor was null") } else { // 把要执行的任务反序列化 val taskDesc = ser.deserialize[TaskDescription](data.value) // 启动任务开始执行 executor.launchTask(this, taskId = taskDesc.taskId, attemptNumber = taskDesc.attemptNumber, taskDesc.name, taskDesc.serializedTask) } }
到此,Stage 级别的调度任务完成。
4.5 Task 级别调度源码
DAGSheduler 将 Task 提交给 TaskScheduler 时,需要将多个 Task 打包为 TaskSet。TaskSet 是整个调度池中对Task 进行调度管理的基本单位,由调度池中的 TaskManager 来管理。
taskScheduler.submitTasks(new TaskSet(
tasks.toArray, stage.id, stage.latestInfo.attemptId, jobId, properties))
taskScheduler.submitTasks 方法
// 把 TaskSet 交给任务调度池来调度
schedulableBuilder.addTaskSetManager(manager, manager.taskSet.properties)
schedulableBuilder 的类型是:SchedulableBuilder,它是一个 Trait,有两个已知的实现子类: FIFOSchedulableBuilder 和 FairSchedulableBuilder
1. SchedulableBuilder (调度池构建器)
-
FIFOSchedulableBuilder
FIFOSchedulableBuilder.addTaskSetManager
override def addTaskSetManager(manager: Schedulable, properties: Properties) { // 对 FIFO 调度, 则直接交给根调度器来调度 // 因为 FIFO 调度只有一个根调度度池 rootPool.addSchedulable(manager) }
rootPool 是根调度池,它的类型是 Pool,表示 Pool 或 TaskSet 的可调度实体。
FIFO 调度是默认调度算法
spark.scheduler.mode 类设置调度算法:FIFO,FAIR
根调度池是在初始化 TaskSchedulerImpl 的时候创建的
FIFOSchedulableBuilder 不需要构建子调度池,只需要有 rootPool
-
FairSchedulableBuilder
不仅仅需要根调度池,还需要创建更多的子调度池
FairSchedulableBuilder.buildPools 方法内会创建更多的子调度池
2.SchedulingAlgorithm (调度算法)
-
FIFOSchedulingAlgorithm
private[spark] class FIFOSchedulingAlgorithm extends SchedulingAlgorithm { // 是不是先调度 s1 override def comparator(s1: Schedulable, s2: Schedulable): Boolean = { val priority1 = s1.priority val priority2 = s2.priority var res = math.signum(priority1 - priority2) if (res == 0) { val stageId1 = s1.stageId val stageId2 = s2.stageId res = math.signum(stageId1 - stageId2) } res < 0 // 值小的先调度 } }
-
FairSchedulingAlgorithm
private[spark] class FairSchedulingAlgorithm extends SchedulingAlgorithm { override def comparator(s1: Schedulable, s2: Schedulable): Boolean = { val minShare1 = s1.minShare val minShare2 = s2.minShare val runningTasks1 = s1.runningTasks val runningTasks2 = s2.runningTasks val s1Needy = runningTasks1 < minShare1 val s2Needy = runningTasks2 < minShare2 val minShareRatio1 = runningTasks1.toDouble / math.max(minShare1, 1.0) val minShareRatio2 = runningTasks2.toDouble / math.max(minShare2, 1.0) val taskToWeightRatio1 = runningTasks1.toDouble / s1.weight.toDouble val taskToWeightRatio2 = runningTasks2.toDouble / s2.weight.toDouble var compare = 0 if (s1Needy && !s2Needy) { // 谁的 runningTasks1 < minShare1 谁先被调度 return true } else if (!s1Needy && s2Needy) { return false } else if (s1Needy && s2Needy) { // 如果都 runningTasks < minShare // 则比较 runningTasks / math.max(minShare1, 1.0) 的比值 小的优先级高 compare = minShareRatio1.compareTo(minShareRatio2) } else { // 如果都runningTasks > minShare, 则比较 runningTasks / weight 的比值 // 小的优先级高 compare = taskToWeightRatio1.compareTo(taskToWeightRatio2) } if (compare < 0) { true } else if (compare > 0) { false } else { // 如果前面都一样, 则比较 TaskSetManager 或 Pool 的名字 s1.name < s2.name } } }
五、Spark Shuffle 解析
5.1 Shuffle 核心
每一个 ShuffleMapStage 的结束伴随着 shuffle 文件的写磁盘。ResultStage 对应代码中的 Action 算子,将一个函数应用在 RDD 的各个 partition 的数据集上,意味着一个 Job 的运行结束。
Shuffle 流程源码
CoarseGrainedExecutorBackend
override def receive: PartialFunction[Any, Unit] = {
case LaunchTask(data) =>
if (executor == null) {
} else {
val taskDesc = ser.deserialize[TaskDescription](data.value)
// 启动任务
executor.launchTask(this, taskId = taskDesc.taskId, attemptNumber = taskDesc.attemptNumber,
taskDesc.name, taskDesc.serializedTask)
}
}
Executor.launchTask 方法
def launchTask(
context: ExecutorBackend,
taskId: Long,
attemptNumber: Int,
taskName: String,
serializedTask: ByteBuffer): Unit = {
// Runnable 接口的对象.
val tr = new TaskRunner(context, taskId = taskId, attemptNumber = attemptNumber, taskName,
serializedTask)
runningTasks.put(taskId, tr)
// 在线程池中执行 task
threadPool.execute(tr)
}
tr.run方法
override def run(): Unit = {
// 更新 task 的状态
execBackend.statusUpdate(taskId, TaskState.RUNNING, EMPTY_BYTE_BUFFER)
try {
// 把任务相关的数据反序列化出来
val (taskFiles, taskJars, taskProps, taskBytes) =
Task.deserializeWithDependencies(serializedTask)
val value = try {
// 开始运行 Task
val res = task.run(
taskAttemptId = taskId,
attemptNumber = attemptNumber,
metricsSystem = env.metricsSystem)
res
} finally {
}
} catch {
} finally {
}
}
Task.run 方法
final def run(
taskAttemptId: Long,
attemptNumber: Int,
metricsSystem: MetricsSystem): T = {
context = new TaskContextImpl(
stageId,
partitionId,
taskAttemptId,
attemptNumber,
taskMemoryManager,
localProperties,
metricsSystem,
metrics)
try {
// 运行任务
runTask(context)
} catch {
} finally {
}
}
Task.runTask 是一个抽象方法:有两个实现类(ShuffleMapTask、ResultTask),分别执行不同阶段的 Task
ShuffleMapTask.runTask 方法
override def runTask(context: TaskContext): MapStatus = {
var writer: ShuffleWriter[Any, Any] = null
try {
val manager = SparkEnv.get.shuffleManager
// 获取 ShuffleWriter
writer = manager.getWriter[Any, Any](dep.shuffleHandle, partitionId, context)
// 写出 RDD 中的数据. rdd.iterator 是读(计算)数据的操作.
writer.write(rdd.iterator(partition, context).asInstanceOf[Iterator[_ <: Product2[Any, Any]]])
} catch {
}
}
把数据写入到磁盘,是由 ShuffleWriter.write 方法来完成,ShuffleWriter 是一个抽象类,有 3 个实现
BypassMergeSortShuffleWriter、UnsafeShuffleWriter、SortShuffleWriter
根据 manager.getWriter(dep.shuffleHandle,partitionId, context) 中的 dep.shuffleHandle 由 manager 来决定选使用哪种 ShuffleWriter
ShuffleManager 是一个Trait,从 2.0.0 开始就只有一个实现类:SortShuffleManager
SortShuffleManager 的 registerShuffle 方法:匹配出来使用哪种 ShuffleHandle
override def registerShuffle[K, V, C](
shuffleId: Int,
numMaps: Int,
dependency: ShuffleDependency[K, V, C]): ShuffleHandle = {
if (SortShuffleWriter.shouldBypassMergeSort(SparkEnv.get.conf, dependency)) {
new BypassMergeSortShuffleHandle[K, V](
shuffleId, numMaps, dependency.asInstanceOf[ShuffleDependency[K, V, V]])
} else if (SortShuffleManager.canUseSerializedShuffle(dependency)) {
new SerializedShuffleHandle[K, V](
shuffleId, numMaps, dependency.asInstanceOf[ShuffleDependency[K, V, V]])
} else {
new BaseShuffleHandle(shuffleId, numMaps, dependency)
}
}
getWriter 方法
/** Get a writer for a given partition. Called on executors by map tasks. */
override def getWriter[K, V](
handle: ShuffleHandle,
mapId: Int,
context: TaskContext): ShuffleWriter[K, V] = {
// 根据不同的 Handle, 创建不同的 ShuffleWriter
handle match {
case unsafeShuffleHandle: SerializedShuffleHandle[K@unchecked, V@unchecked] =>
new UnsafeShuffleWriter(
env.blockManager,
shuffleBlockResolver.asInstanceOf[IndexShuffleBlockResolver],
context.taskMemoryManager(),
unsafeShuffleHandle,
mapId,
context,
env.conf)
case bypassMergeSortHandle: BypassMergeSortShuffleHandle[K@unchecked, V@unchecked] =>
new BypassMergeSortShuffleWriter(
env.blockManager,
shuffleBlockResolver.asInstanceOf[IndexShuffleBlockResolver],
bypassMergeSortHandle,
mapId,
context,
env.conf)
case other: BaseShuffleHandle[K@unchecked, V@unchecked, _] =>
new SortShuffleWriter(shuffleBlockResolver, other, mapId, context)
}
}
5.2 HashShuffle 解析
Spark-1.6 之前默认的 shuffle 方式是 hash。在 spark-1.6 版本之后使用 Sort-Base Shuffle,因为 HashShuffle 存在的不足所以就替换了 HashShuffle。Spark2.0 之后,从源码中完全移除了 HashShuffle。
1. 未优化的 HashShuffle
CPU Core = 1,Executor 只能一次运行一个 Task,且这些 Task 会有单独的 Buffer。
缺点:
- map 任务的中间结果首先存入内存(缓存),然后才写入磁盘。这对于内存的开销很大,当一个节点上 map 任务的输出结果集很大时,很容易导致OOM。
- 生成很多的小文件。假设有 M 个 MapTask,有 N 个 ReduceTask,则会创建 M * n 个小文件,磁盘 I/O 将成为性能瓶颈。
2. 优化的 HashShuffle
启用合并机制,复用 buffer,一个 Task 使用完 Buffer 后清空,下一个 Task 重复使用。spark.shuffle.consolidateFiles 的默认值为 false,设置为 true 即可开启合并机制。如果使用 HashShuffleManager,建议开启这个选项。
启动合并机制后,同一个 Executor 中生成的小文件数 = ReduceTask * Core 数,同样在 Core = 1 的情况下,生成的总文件数为 ReduceTask * Executor 的 Core 数。
5.3 SortShuffle 解析
1. 普通 SortShuffle
在溢写磁盘前,先根据 key 进行排序,排序过后的数据,会分批写入到磁盘文件中。默认批次为10000条,数据会以每批一万条写入到磁盘文件。写入磁盘文件通过缓冲区溢写的方式,每次溢写都会产生一个磁盘文件,也就是说一个 Task 过程会产生多个临时文件。
最后在每个 Task 中,将所有的临时文件合并,这就是 merge 过程,此过程将所有临时文件读取出来,一次写入到最终文件。意味着一个 Task 的所有数据都在这一个文件中。同时单独写一份索引文件,标识下游各个 Task 的数据在文件中的索引,start offset 和 end offset。
源码解析
write 方法
override def write(records: Iterator[Product2[K, V]]): Unit = {
// 排序器
sorter = if (dep.mapSideCombine) {
require(dep.aggregator.isDefined, "Map-side combine without Aggregator specified!")
new ExternalSorter[K, V, C](
context, dep.aggregator, Some(dep.partitioner), dep.keyOrdering, dep.serializer)
} else {
// In this case we pass neither an aggregator nor an ordering to the sorter, because we don't
// care whether the keys get sorted in each partition; that will be done on the reduce side
// if the operation being run is sortByKey.
new ExternalSorter[K, V, V](
context, aggregator = None, Some(dep.partitioner), ordering = None, dep.serializer)
}
// 将 Map 任务的输出记录插入到缓存中
sorter.insertAll(records)
// 数据 shuffle 数据文件
val output = shuffleBlockResolver.getDataFile(dep.shuffleId, mapId)
try { // 将 map 端缓存的数据写入到磁盘中, 并生成 Block 文件对应的索引文件.
val blockId = ShuffleBlockId(dep.shuffleId, mapId, IndexShuffleBlockResolver.NOOP_REDUCE_ID)
// 记录各个分区数据的长度
val partitionLengths = sorter.writePartitionedFile(blockId, tmp)
// 生成 Block 文件对应的索引文件. 此索引文件用于记录各个分区在 Block文件中的偏移量, 以便于
// Reduce 任务拉取时使用
shuffleBlockResolver.writeIndexFileAndCommit(dep.shuffleId, mapId, partitionLengths, tmp)
mapStatus = MapStatus(blockManager.shuffleServerId, partitionLengths)
} finally {
}
}
2. bypassSortShuffle
bypass 运行机制的触发条件如下:
1)shuffle map task 数量小于 spark.shuffle.sort.bypassMergeThreshold 参数的值,默认为200。
2)不是聚合类的 shuffle 算子(比如:reduceByKey)。
此时 Task 会为每个 reduce 端的 Task 都创建一个临时磁盘文件,并将数据按 key 进行 hash 然后根据 key 的 hash 值,将 key 写入对应的磁盘文件之中。当然,写入磁盘文件时也是先写入内存缓冲,缓冲写满之后再溢写到磁盘文件的。最后,同样会将所有临时磁盘文件都合并成一个磁盘文件,并创建一个单独的索引文件。
该过程的磁盘写机制其实跟未经优化的 HashShuffleManager 是一模一样的,因为都要创建数量惊人的磁盘文件,只是在最后会做一个磁盘文件的合并而已。因此 少量的最终磁盘文件,也让该机制相对未经优化的 HashShuffleManager 来说,shuffle read的性能会更好。而该机制与普通 SortShuffleManager 运行机制的不同在于:不会进行排序。也就是说,启用该机制的最大好处在于,shuffle write 过程中,不需要进行数据的排序操作,也就节省掉了这部分的性能开销。
源码解析
当 map 端不需要在持久化数据之前进行排序,那么 BypassMergeSortShuffleWriter 就派上用场了。
private[spark] object SortShuffleWriter {
def shouldBypassMergeSort(conf: SparkConf, dep: ShuffleDependency[_, _, _]): Boolean = {
// We cannot bypass sorting if we need to do map-side aggregation.
// 如果 map 端有聚合, 则不能绕过排序
if (dep.mapSideCombine) {
require(dep.aggregator.isDefined, "Map-side combine without Aggregator specified!")
false
} else {
val bypassMergeThreshold: Int = conf.getInt("spark.shuffle.sort.bypassMergeThreshold", 200)
// 分区数不能超过 200 默认值
dep.partitioner.numPartitions <= bypassMergeThreshold
}
}
}
六、Spark 内存管理
Spark 与 Hadoop 的重要区别之一就在于对于内存的使用,Spark 除将内存作为计算资源外, 还将内存的一部分纳入到存储体系中。
6.1 堆内堆外内存规划
Spark 将内存从逻辑上区分为堆内内存和堆外内存, 称为内存模型(MemoryMode)
Spark 的堆内存不能与 JVM 中的 Java 堆直接画等号, 它只是 JVM 堆内存的一部分。由 JVM 统一管理;堆外内存则是 Spark 使用 sun.misc.Unsafe 的 API 直接在工作节点的系统内存中开辟的空间。
抽象类 MemoryPool 定义了内存池的规范,对内存进行资源管理。
1. 堆内内存
堆内内存的大小由 Spark 应用程序启动时的 -executor-memory 或 spark.executor.memory 参数配置
- Executor 运行的并发任务共享 JVM 和堆内内存,这些内存在缓存 RDD 和广播变量时占用的内存被规划为存储内存
- Executor 运行的并发任务在执行 Shuffle 时占用的内存被规划为执行内存
- Spark 内部(包括用户定义)的对象实例,不做特殊规划,占用剩余的空间。
Spark 对堆内内存的管理是一种逻辑上的“规划式”的管理,因为对象实例占用内存的申请和释放都由 JVM 完成。 JVM 对于内存的清理是无法准确指定时间点的,因此无法实现精确的释放。
2. 堆外内存
为了进一步优化内存的使用以及提高 Shuffle 时排序的效率,Spark 引入了堆外(Off-heap)内存,使之可以直接在工作节点的系统内存中开辟空间,存储经过序列化的二进制数据。这样做的结果就是能保持一个较小的堆,以减少垃圾收集对应用的影响。
在默认情况下堆外内存并不启用,可通过配置 spark.memory.offHeap.enabled 参数启用,并由 spark.memory.offHeap.size 参数设定堆外空间的大小
堆外内存的申请和释放不再通过 JVM 机制,直接向操作系统申请,这使得堆外内存可以被精确地申请和释放。
6.2 内存空间分配
1. 静态内存管理
Spark 1.6 之前采用静态内存管理,存储内存、执行内存和其它内存的大小在 Spark 应用程序运行期间均为固定的,在应用启动前可以进行配置。
堆内内存管理
可用的存储内存 = systemMaxMemory * spark.storage.memoryFraction * spark.storage.safety Fraction
可用的执行内存 = systemMaxMemory * spark.shuffle.memoryFraction * spark.shuffle.safety Fraction
堆外内存管理
堆外的空间分配较为简单,只有存储内存和执行内存。可用的执行内存和存储内存占用的空间大小直接由参数 spark.memory.storageFraction 决定,由于堆外内存占用的空间可以被精确计算,所以无需再设定保险区域。
2. 动态内存管理
Spark 1.6 之后引入的统一内存管理机制,与静态内存管理的区别在于存储内存和执行内存共享同一块空间,可以动态占用对方的空闲区域。
堆内内存管理
堆外内存管理
统一内存管理最重要的优化在于 动态占用机制:(执行内存优先)
- 设定基本的存储内存和执行内存区域 spark.storage.storageFraction,该设定确定了双方各自拥有的空间的范围。
- 双方的空间都不足时,则存储到硬盘。若己方空间不足而对方空余时,可借用对方的空间。
- 执行内存的空间被对方占用后,可让对方将占用的部分转存到硬盘,然后“归还”借用的空间。
- 存储内存的空间被对方占用后,无法让对方“归还”,因为需要考虑 Shuffle 过程,执行空间无法立即归还。
6.3 存储内存管理
1. RDD 持久化
如果一个 RDD 上多次执行 action 算子,可以在第一次 action 中使用 persist 或 cache 方法,在内存或磁盘中持久化这个 RDD,从而在后面的 action 时提升计算速度。cache 方法是使用默认的 MEMORY_ONLY 的存储级别将 RDD 持久化到内存,所以缓存是一种特殊的持久化。
Driver 端的 BlockManager 为 Master,Executor 端的 BlockManager 为 Slave。Storage 模块在逻辑上以 Block 为基本存储单位,RDD 的每个 Partition 经过处理后唯一对应一个 Block。Master 负责整个 Spark 应用程序的 Block 元数据信息的管理和维护,而 Slave 需要将 Block 的更新状态上报到 Master,同时接收 Master 的命令,例如新增或删除一个 RDD。
Spark 规定了 MEMORY_ONLY、MEMORY_AND_DISK 等 7 种不同的存储级别 ,而存储级别是以下 5 个变量的组合:
class StorageLevel private(
private var _useDisk: Boolean,
private var _useMemory: Boolean,
private var _useOffHeap: Boolean,
private var _deserialized: Boolean,
private var _replication: Int = 1)
2. RDD 的缓存过程
RDD 在缓存到存储内存之前,Partition 中的数据一般以迭代器(Iterator)的数据结构来访问。通过 Iterator 可以获取分区中每一条序列化或者非序列化的数据项(Record),这些 Record 的对象实例在逻辑上占用了 JVM 堆内内存的 other 部分的空间。同一 Partition 的不同 Record 的空间并不连续。
RDD 在缓存到存储内存之后,Partition 被转换成 Block,Record 在堆内内存或堆外内存中占用一块连续的空间。
将 Partition 由不连续的存储空间转换为连续存储空间的过程,Spark 称之为展开(Unroll)
每个 Executor 的 Storage 模块用一个链式 Map 结构 (LinkedHashMap) 来管理堆内和堆外存储内存中所有的 Block 对象的实例,对这个 LinkedHashMap 新增和删除,间接记录了内存的申请和释放。
计算任务在 Unroll 时要向 MemoryManager 申请足够的 Unroll 空间来临时占位,空间不足则 Unroll 失败,空间足够时可以继续进行。 Unroll 成功,当前 Partition 所占用的 Unroll 空间被转换为正常的缓存 RDD 的存储空间。
3. 淘汰和落盘
同一个 Executor 的存储内存空间是有限的,当有新的 Block 需要缓存但是剩余空间不足无法动态占用时,就要对 LinkedHashMap中的旧 Block 进行淘汰(Eviction),被淘汰的 Block 如果其存储级别中同时包含存储到磁盘的要求, 则要对其进行落盘 (Drop)。否则就是直接删除该 Block。
- 被淘汰的旧 Block 要与新的 Block 的 MemoryNode 相同,即同属于堆内内存或者堆外内存。
- 新旧 Block 不能同属于一个 RDD,避免循环淘汰。
- 旧 Block 所属 RDD 不能处于被读状态,避免引发一致性问题。
- 遍历 LinkdHashMap 中的 Block,按照最近最少使用(LRU)的顺序淘汰,直到满足新的 Block 所需的空间。
落盘的流程则比较简单, 如果其存储级别符号 useDisk 为 true, 再根据其 deserialized 判断是否是非序列化的形式,若是则对其进行序列化,最后将数据存储到磁盘,然后在 Storage 模块中更新其信息。
6.4 执行内存管理
1. 多任务内存分配
Executor 内存运行的任务同样共享执行内存,Spark 用一个 HashMap 保存了 “任务 -> 内存耗费” 的映射。
每个任务可占用的执行内存大小的范围为 1/2N ~ 1/N,其中 N 为当前 Executor 内正在运行的任务的个数。
每个任务在启动时,会向 MemoryManager 申请最少 1/2N 的执行内存,如果不能被满足要求则该任务被阻塞,直到有其他任务释放了足够的执行内存,该任务才可以被唤醒。
2. Shuffle 的内存占用
执行内存主要用来存储任务在执行 Shuffle 时占用的内存,Shuffle 的两个阶段 Write 和 Read 堆内存的使用不同:
-
Shuffle Write
若在 map 端选择普通的排序方式,会采用 ExternalSorter 进行外排,在内存中存储数据时主要占用堆内执行空间。
若在 map 端选择 Tungsten 的排序方式,则采用 ShuffleExternalSorter 直接对以序列化形式存储的数据排序,在内存中存储数据时可以占用堆外或堆内执行空间,取决于用户是否开启了堆外内存以及堆外执行内存是否足够。
-
Shuffle Read
在对 reduce 端的数据进行聚合时,要将数据交给 Aggregator 处理,在内存中存储数据时占用堆内执行空间。
如果需要进行最终结果排序,则要将再次将数据交给 ExternalSorter 处理,占用堆内执行空间。