文章目录
Spark源码剖析——Master、Worker启动流程
当前环境与版本
| 环境 | 版本 |
|---|---|
| JDK | java version “1.8.0_231” (HotSpot) |
| Scala | Scala-2.11.12 |
| Spark | spark-2.4.4 |
1. 前言
- Master与Worker是Spark在Standalone模式下的主要节点,维护起了整个分布式集群的管理、资源分配、应用运行等重要工作。
- Master、Worker都是ThreadSafeRpcEndpoint的实现类,其启动流程部分较简单,查看此部分的代码,可以帮助我们快速上手,理解到集群中RpcEndpoint、RpcEnv的交互过程。这样在后续过程中查看其他代码将更加容易。
- 在看此部分之前,建议先看Spark源码剖析——RpcEndpoint、RpcEnv
2. Master启动流程
2.1 Master的伴生对象
- org.apache.spark.deploy.master.Master
- 我们先看Master的伴生对象,此处是Java进程的入口(被
start-master.sh启动)private[deploy] object Master extends Logging { val SYSTEM_NAME = "sparkMaster" val ENDPOINT_NAME = "Master" def main(argStrings: Array[String]) { Thread.setDefaultUncaughtExceptionHandler(new SparkUncaughtExceptionHandler( exitOnUncaughtException = false)) Utils.initDaemon(log) val conf = new SparkConf // 此处会解析外部传入的参数argStrings,由其内部的parse方法解析 // 示例:--port 7077 --webui-port 8081 val args = new MasterArguments(argStrings, conf) // 启动Master对应的RpcEndpoint、NettyRpcEnv val (rpcEnv, _, _) = startRpcEnvAndEndpoint(args.host, args.port, args.webUiPort, conf) // 此处调用的就是我们前面在NettyRpcEnv所讲的 // 'ctrl + alt + 鼠标左键' 点击'awaitTermination',选择其实现类NettyRpcEnv,可以看到调用了dispatcher // 再继续点击,可以看到实际是调用了threadpool.awaitTermination(...),在此处进行了阻塞 // 而该threadpool正是运行了MessageLoop(用于处理Inbox消息)的线程池 rpcEnv.awaitTermination() } def startRpcEnvAndEndpoint( host: String, port: Int, webUiPort: Int, conf: SparkConf): (RpcEnv, Int, Option[Int]) = { // 安全管理,例如ACL、Sasl val securityMgr = new SecurityManager(conf) // 创建NettyRpcEnv,由NettyRpcEnvFactory调用create(...)创建 val rpcEnv = RpcEnv.create(SYSTEM_NAME, host, port, conf, securityMgr) // 创建Master这个RpcEndpoint,并将其注册到RpcEnv中 val masterEndpoint = rpcEnv.setupEndpoint(ENDPOINT_NAME, new Master(rpcEnv, rpcEnv.address, webUiPort, securityMgr, conf)) // 向Master发送了一个BoundPortsRequest,并同步返回一个BoundPortsResponse(包含了Master的端口信息) val portsResponse = masterEndpoint.askSync[BoundPortsResponse](BoundPortsRequest) (rpcEnv, portsResponse.webUIPort, portsResponse.restPort) } } - 此处代码,相对来说还是比较简单的。Shell调用
start-master.sh后,会启动一个Java进程。传入的参数则被MasterArguments进行了解析,最重要的参数是host、port、webUiPort。 - 接着,就会调用startRpcEnvAndEndpoint(…),开始创建NettyRpcEnv与Master,并将Master注册进RpcEnv。
- 创建NettyRpcEnv是利用的NettyRpcEnvFactory调用create(…)
- Master则是直接被new实例化,此时该RpcEndpoint的构造器被调用
- 注册Master则是调用了setupEndpoint(…),进而调用了dispatcher的registerRpcEndpoint(…)方法:
- 为Master创建了一个EndpointData,包含一个Inbox。Inbox实例化时顺带将OnStart消息放入了队列。
- 将EndpointData放入了receivers队列中,后续会被MessageLoop取出
- 因此,我们可以看到,Master被实例化时,先调用了其构造器。接着,将其注册入RpcEnv时,其Inbox中放入了第一条消息OnStart。然后,该消息OnStart将被MessageLoop取出并处理,调用了Master这个Endpoint的onStart方法。也就是说Master的生命周期前面部分是:constructor -> onStart -> …
2.2 Master
- org.apache.spark.deploy.master.Master
- Master的class代码相对来说还是比较多的,我们主要看起启动流程部分代码
- 首先,我们来看其onStart()方法做了什么
override def onStart(): Unit = { logInfo("Starting Spark master at " + masterUrl) logInfo(s"Running Spark version ${org.apache.spark.SPARK_VERSION}") // 启动Master的WebUI界面 webUi = new MasterWebUI(this, webUiPort) webUi.bind() masterWebUiUrl = "http://" + masterPublicAddress + ":" + webUi.boundPort // 是否启用反向代理,默认为false if (reverseProxy) { masterWebUiUrl = conf.get("spark.ui.reverseProxyUrl", masterWebUiUrl) webUi.addProxy() logInfo(s"Spark Master is acting as a reverse proxy. Master, Workers and " + s"Applications UIs are available at $masterWebUiUrl") } // 启用定时任务,心跳机制,向自己发送CheckForWorkerTimeOut消息,用于检测Worker是否超时 // 跟踪代码可知,最终会调用timeOutDeadWorkers(),用于检测超时的Worker,并移除 checkForWorkerTimeOutTask = forwardMessageThread.scheduleAtFixedRate(new Runnable { override def run(): Unit = Utils.tryLogNonFatalError { self.send(CheckForWorkerTimeOut) } }, 0, WORKER_TIMEOUT_MS, TimeUnit.MILLISECONDS) // 是否启用了RESTServer,默认为false if (restServerEnabled) { val port = conf.getInt("spark.master.rest.port", 6066) restServer = Some(new StandaloneRestServer(address.host, port, conf, self, masterUrl)) } restServerBoundPort = restServer.map(_.start()) // MetricsSystem是Spark的监控度量系统 masterMetricsSystem.registerSource(masterSource) masterMetricsSystem.start() applicationMetricsSystem.start() // Attach the master and app metrics servlet handler to the web ui after the metrics systems are // started. masterMetricsSystem.getServletHandlers.foreach(webUi.attachHandler) applicationMetricsSystem.getServletHandlers.foreach(webUi.attachHandler) // Spark的恢复模式,暂时可以不管 // 省略部分代码 } - Master的onStart()方法主要做了以下几件事:
- 启动了Master的WebUI界面
- 开启了Worker的心跳检测定时任务
- 启动了监控度量系统MetricsSystem
- 至此,Master的启动就算结束了。后面会等待着接收消息,消息进入Inbox,再传给Master这个RpcEndpoint,调用Master的receive、receiveAndReply。
- 另外,在Master的伴生对象的startRpcEnvAndEndpoint(…)中,完成Endpoint的注册后,还会向Master发送一条同步消息BoundPortsRequest,并获得回应的消息BoundPortsResponse。
3. Worker启动流程
3.1 Worker的伴生对象
- org.apache.spark.deploy.worker.Worker
- Worker被
start-slave.sh启动 - 此伴生对象和Master的伴生对象代码逻辑几乎一样,就不再做展示,自行看代码即可。
- 需要注意的是
- main入口中一定要传入master的地址,传参示例
--webui-port 8081 spark://192.168.0.101:7077 --cores 2 --memory 2G - 实例化Worker时,同时也传入了masterAddresses,用于后续获取Master的RpcEndpointRef,向其发送消息
- main入口中一定要传入master的地址,传参示例
3.2 Worker
- org.apache.spark.deploy.worker.Worker
- Worker启动时,同Master一样,将调用其构造器,接着onStart方法被调用。我们来看Worker的onStart()方法做了什么。
override def onStart() { assert(!registered) logInfo("Starting Spark worker %s:%d with %d cores, %s RAM".format( host, port, cores, Utils.megabytesToString(memory))) logInfo(s"Running Spark version ${org.apache.spark.SPARK_VERSION}") logInfo("Spark home: " + sparkHome) // 创建工作目录 createWorkDir() // ExternalShuffleService是一个单独的进程服务,默认不开启 // 用于帮助Executor处理shuffle,降低Executor的压力 startExternalShuffleService() // 启动Worker的WebUI webUi = new WorkerWebUI(this, workDir, webUiPort) webUi.bind() workerWebUiUrl = s"http://$publicAddress:${webUi.boundPort}" // 注册到Master,下一部分来说 registerWithMaster() // 启动metricsSystem,用于度量各种指标 metricsSystem.registerSource(workerSource) metricsSystem.start() // Attach the worker metrics servlet handler to the web ui after the metrics system is started. metricsSystem.getServletHandlers.foreach(webUi.attachHandler) } - 可以看到,Worker的onStart()方法主要做了以下几件事:
- 创建工作目录
- 启动ExternalShuffleService(默认不启动)
- 启动Worker的WebUI
- 注册到Master(下一节详细来看)
- 启动metricsSystem,用于度量各种指标
- 至此,Worker启动结束。
- 后续Master与Worker只需等待新的应用提交上来,并运行。
4. Master与Worker的初步交互(注册)
- Worker在启动时,是需要注册到Master的,我们来详细看看此部分代码。
- Worker的onStart()中调用的registerWithMaster()方法如下
private def registerWithMaster() { registrationRetryTimer match { case None => // 第一次进来时,registrationRetryTimer为None registered = false // 此处,是向所有Master发起注册请求 // 因为高可用模式下会存在多个Master registerMasterFutures = tryRegisterAllMasters() connectionAttemptCount = 0 // 由于网络等问题,可能注册失败,因此需要一个能够重试的定时器,去注册 registrationRetryTimer = Some(forwordMessageScheduler.scheduleAtFixedRate( new Runnable { override def run(): Unit = Utils.tryLogNonFatalError { Option(self).foreach(_.send(ReregisterWithMaster)) } }, INITIAL_REGISTRATION_RETRY_INTERVAL_SECONDS, INITIAL_REGISTRATION_RETRY_INTERVAL_SECONDS, TimeUnit.SECONDS)) case Some(_) => // registrationRetryTimer已存在,不需要再创建了 logInfo("Not spawning another attempt to register with the master, since there is an" + " attempt scheduled already.") } } - 接着,再看tryRegisterAllMasters()的代码
private def tryRegisterAllMasters(): Array[JFuture[_]] = { masterRpcAddresses.map { masterAddress => // 线程池提交,返回一个JFuture registerMasterThreadPool.submit(new Runnable { override def run(): Unit = { try { logInfo("Connecting to master " + masterAddress + "...") // 获取到Master对应的RpcEndpointRef val masterEndpoint = rpcEnv.setupEndpointRef(masterAddress, Master.ENDPOINT_NAME) // 向Master发送注册消息 sendRegisterMessageToMaster(masterEndpoint) } catch { case ie: InterruptedException => // Cancelled case NonFatal(e) => logWarning(s"Failed to connect to master $masterAddress", e) } } }) } } - 再看sendRegisterMessageToMaster(…)方法
private def sendRegisterMessageToMaster(masterEndpoint: RpcEndpointRef): Unit = { masterEndpoint.send(RegisterWorker( workerId, host, port, self, cores, memory, workerWebUiUrl, masterEndpoint.address)) } - 此处,正式向Master发送了消息RegisterWorker,进行注册
- 快速查看技巧:利用’ctrl+鼠标左键’点击RegisterWorker,看到case class RegisterWorker。再次利用’ctrl+鼠标左键’点击RegisterWorker,IDEA会为我们展示出什么地方使用了它。可以看到IDEA展示的部分:
Worker.scala <- masterEndpoint.send(RegisterWorker(,此处是Worker发送该消息的代码处Master.scala <- case RegisterWorker(,此处既是Master接收到该消息的地方
- 利用上面的技巧,我们可以快速地在RpcEndpoint的代码之间跳转,方便了对其交互流程的查看。(消息通信的具体实现,请看RpcEndpoint、RpcEnv)
- 此时,我们来到了Master的receive方法,代码如下
override def receive: PartialFunction[Any, Unit] = { // 省略部分代码 case RegisterWorker( id, workerHost, workerPort, workerRef, cores, memory, workerWebUiUrl, masterAddress) => // Master收到了Worker发来的RegisterWorker消息,开始进行处理 logInfo("Registering worker %s:%d with %d cores, %s RAM".format( workerHost, workerPort, cores, Utils.megabytesToString(memory))) if (state == RecoveryState.STANDBY) { // 高可用模式下,该Master可能是STANDBY的,因此回复一个MasterInStandby workerRef.send(MasterInStandby) } else if (idToWorker.contains(id)) { // 如果该Worker已经注册了,回一个RegisterWorkerFailed workerRef.send(RegisterWorkerFailed("Duplicate worker ID")) } else { // 开始注册Worker val worker = new WorkerInfo(id, workerHost, workerPort, cores, memory, workerRef, workerWebUiUrl) // 调用registerWorker(...),将worker添加到本节点 if (registerWorker(worker)) { persistenceEngine.addWorker(worker) // 如果过成功,那么就向Worker回复消息RegisteredWorker workerRef.send(RegisteredWorker(self, masterWebUiUrl, masterAddress)) schedule() } else { // 注册失败,回复RegisterWorkerFailed val workerAddress = worker.endpoint.address logWarning("Worker registration failed. Attempted to re-register worker at same " + "address: " + workerAddress) workerRef.send(RegisterWorkerFailed("Attempted to re-register worker at same address: " + workerAddress)) } } // 省略部分代码 } - Master收到消息后,需要检测本节点的状态是否是STANDBY、是否已经注册该Worker,如果没问题,那么调用registerWorker(…),将worker添加到本节点,最后会回复Worker一个消息RegisteredWorker
- 跟随着RegisteredWorker消息,我们来到Worker接收消息处。Worker中先是receive被调用,再匹配到RegisterWorkerResponse,接着调用了handleRegisterResponse(…)方法,代码如下
private def handleRegisterResponse(msg: RegisterWorkerResponse): Unit = synchronized { msg match { case RegisteredWorker(masterRef, masterWebUiUrl, masterAddress) => // 如果在Master注册成功,则会收到RegisteredWorker if (preferConfiguredMasterAddress) { logInfo("Successfully registered with master " + masterAddress.toSparkURL) } else { logInfo("Successfully registered with master " + masterRef.address.toSparkURL) } registered = true // 修改本Worker节点对应的Master changeMaster(masterRef, masterWebUiUrl, masterAddress) // 启用定时器,发送心跳 // 追踪SendHeartbeat可知,定时器先发送给自己,Worker在receive处收到后,再调用sendToMaster(...)发送给Master forwordMessageScheduler.scheduleAtFixedRate(new Runnable { override def run(): Unit = Utils.tryLogNonFatalError { self.send(SendHeartbeat) } }, 0, HEARTBEAT_MILLIS, TimeUnit.MILLISECONDS) // 是否删除之前应用的工作目录,默认false if (CLEANUP_ENABLED) { logInfo( s"Worker cleanup enabled; old application directories will be deleted in: $workDir") forwordMessageScheduler.scheduleAtFixedRate(new Runnable { override def run(): Unit = Utils.tryLogNonFatalError { self.send(WorkDirCleanup) } }, CLEANUP_INTERVAL_MILLIS, CLEANUP_INTERVAL_MILLIS, TimeUnit.MILLISECONDS) } // 准备本Worker的Executor信息,并将最新状态消息WorkerLatestState发送给Master // 不过,显然第一次启动时,本节点是没有启动Excutor的 val execs = executors.values.map { e => new ExecutorDescription(e.appId, e.execId, e.cores, e.state) } masterRef.send(WorkerLatestState(workerId, execs.toList, drivers.keys.toSeq)) case RegisterWorkerFailed(message) => // 注册失败,回复此消息RegisterWorkerFailed if (!registered) { logError("Worker registration failed: " + message) System.exit(1) } case MasterInStandby => // Ignore. Master not yet ready. } } - 最后,Master将会收到WorkerLatestState消息,代码如下
override def receive: PartialFunction[Any, Unit] = { // 省略部分代码 case WorkerLatestState(workerId, executors, driverIds) => idToWorker.get(workerId) match { case Some(worker) => // 因为是第一次,因此该executors是空的,for中代码不执行 for (exec <- executors) { val executorMatches = worker.executors.exists { case (_, e) => e.application.id == exec.appId && e.id == exec.execId } if (!executorMatches) { // master doesn't recognize this executor. So just tell worker to kill it. worker.endpoint.send(KillExecutor(masterUrl, exec.appId, exec.execId)) } } // 因为是第一次,因此该driverIds是空的,for中代码不执行 for (driverId <- driverIds) { val driverMatches = worker.drivers.exists { case (id, _) => id == driverId } if (!driverMatches) { // master doesn't recognize this driver. So just tell worker to kill it. worker.endpoint.send(KillDriver(driverId)) } } case None => logWarning("Worker state from unknown worker: " + workerId) } // 省略部分代码 } - 至此,Worker注册到Master通信流程,完全结束。^_^
- 后面整个集群会持续以下模式:由Worker定时向Master发送心跳包,而Master也会在本节点定时检测Worker的心跳,移除超时的Worker。
- Worker注册到Master的通信流程示意图如下


本文深入分析Spark在Standalone模式下Master与Worker的启动流程,包括环境配置、启动步骤、注册交互及心跳检测机制。通过源码解读,揭示RpcEndpoint与RpcEnv在集群中的交互过程。

1124

被折叠的 条评论
为什么被折叠?



