分析Standalone模式下Master节点的启动流程及循环消息处理机制,基本把Master代码分析的比较透彻了,下面我们就剖析下Worker节点的启动流程和相关消息处理机制:


源码版本:

    Spark2.2.0


1.Worker启动脚本为start-worker.sh,最后日志里面打出来的启动日志

//启动脚本命令/home/cuadmin/ljs/spark2.2.1/sbin/start-worker.sh  //日志中打出来的,最后拼写后的命令 /usr/jdk64/jdk1.8.0_112/bin/java -cp /home/cuadmin/ljs/spark2.2.1/conf/:/home/cuadmin/ljs/spark2.2.1/jars/* -Xmx1g org.apache.spark.deploy.worker.Worker --webui-port 8081 spark://salver158.hadoop.unicom:7077

 

    Worker启动脚本里面的脚本除了加载了一些配置主要就是调用了org.apache.spark.deploy.worker.Worker这个类,我们直接从Worker.scala文件的的main()开始看:

def main(argStrings: Array[String]) {    //同样是初始化了日志对象    Utils.initDaemon(log)    val conf = new SparkConf    //根据用户指定的配置,覆盖默认的配置,如端口、cpu、内存等信息,实例化为args    val args = new WorkerArguments(argStrings, conf)    //跟Master节点一样调用startRpcEnvAndEndpoint,创建一个RpcEnv和Endpoint    val rpcEnv = startRpcEnvAndEndpoint(args.host, args.port, args.webUiPort, args.cores,      args.memory, args.masters, args.workDir, conf = conf)    rpcEnv.awaitTermination()  }

   

2.这个main函数就一行代码比较重要,就是startRpcEnvAndEndpoint()函数的调用,这一行其实跟Master节点基本一致,这里用到了Master的地址、端口,因为后期Worker是要跟Master进行通信的,比如Worker的注册;详细看下他的源代码:

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和endpoint,注意这里创建endpoint用到了Master的ip和端口信息    // 以及Worker自身分配的cpu、内存信息    val rpcEnv = RpcEnv.create(systemName, host, port, conf, securityMgr)    val masterAddresses = masterUrls.map(RpcAddress.fromSparkURL(_))    rpcEnv.setupEndpoint(ENDPOINT_NAME, new Worker(rpcEnv, webUiPort, cores, memory,      masterAddresses, ENDPOINT_NAME, workDir, conf, securityMgr))    rpcEnv  }


3.和Master一样,由于继承了ThreadSafeRpcEndpoint,重写里面的onStart方法,在启动的时候会执行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)       //创建Worker工作目录,是伸缩空间和日志输入的目录路径    //这里有三个相关的参数有兴趣可以了解下,控制目录文件大小:    //spark.executor.logs.rolling.strategy     //spark.executor.logs.rolling.maxSize 134217728 #default byte     //spark.executor.logs.rolling.maxRetainedFiles    createWorkDir()    //如果用户开启Shffle服务,则启动该服务,   External shuffle Service是长期存在于NodeManager进程中的一个辅助服务。    // 通过该服务来抓取shuffle数据,减少了Executor的压力,    // 在Executor GC的时候也不会影响其他Executor的任务运行。    //这个服务由参数spark.shuffle.service.enabled控制  默认为false 不开启    shuffleService.startIfEnabled()   //实例化Worker的UI界面,绑定端口    webUi = new WorkerWebUI(this, workDir, webUiPort)    webUi.bind()        workerWebUiUrl = s"http://$publicAddress:${webUi.boundPort}"    //这里比较重要,向Master进行注册    registerWithMaster()    //监控相关,这里不讲解    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)  }

    我们先看第18行,启动shuffle服务代码,跳转到最后调用的是ExternalShuffleService.scala中的start()函数:


4.ExternalShuffleService服务启动


     Spark系统在运行含shuffle过程的应用时,Executor进程除了运行task,还要负责写shuffle数据,给其他Executor提供shuffle数据。当Executor进程任务过重,导致GC而不能为其他Executor提供shuffle数据时,会影响任务运行。External shuffle Service是长期存在于NodeManager进程中的一个辅助服务。通过该服务来抓取shuffle数据,减少了Executor的压力,在Executor GC的时候也不会影响其他Executor的任务运行。


/** Start the external shuffle service */  def start() {    require(server == null, "Shuffle server already started")    val authEnabled = securityManager.isAuthenticationEnabled()    logInfo(s"Starting shuffle service on port $port (auth enabled = $authEnabled)")    val bootstraps: Seq[TransportServerBootstrap] =      if (authEnabled) {        Seq(new AuthServerBootstrap(transportConf, securityManager))      } else {        Nil      }    server = transportContext.createServer(port, bootstraps.asJava)
   masterMetricsSystem.registerSource(shuffleServiceSource)    masterMetricsSystem.start()  }


5.Worker向Master注册,调用registerWithMaster()函数,代码如下

private def registerWithMaster() {    // onDisconnected may be triggered multiple times, so don't attempt registration    // if there are outstanding registration attempts scheduled.    registrationRetryTimer match {      case None =>        registered = false        //主要是这行根据master地址和端口,向Master进行注册        registerMasterFutures = tryRegisterAllMasters()        connectionAttemptCount = 0        // //开启一个定时器、如果上面的tryRegisterAllMasters注册失败,那么registered字段就不为TRUE        //这里就判断registered字段,重试直到最大次数后,放弃重试.        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(_) =>        logInfo("Not spawning another attempt to register with the master, since there is an" +          " attempt scheduled already.")    }  }

    这里直接看tryRegisterAllMasters()函数:

private def tryRegisterAllMasters(): Array[JFuture[_]] = {    //循环该worker的构造参数属性masterRpcAddresses,这是一个存放着RpcAddress的集合    //然后使用一个线程池(个数=masterRpcAddresses个数)来多线程注册Master    //这里向所有Master进行注册,由Master的代码我们知道,只有Active的Master才会返回消息,其他不返回消息    //这个masterRpcAddresses是在startRpcEnvAndEndpoint函数中调用rpcEnv.setupEndpoint的时候    //会new Worker传进来的参数    masterRpcAddresses.map { masterAddress =>      registerMasterThreadPool.submit(new Runnable {        override def run(): Unit = {          try {            logInfo("Connecting to master " + masterAddress + "...")            //根据masterEndPoint地址 和名称 向Master进行注册            val masterEndpoint = rpcEnv.setupEndpointRef(masterAddress, Master.ENDPOINT_NAME)            sendRegisterMessageToMaster(masterEndpoint)          } catch {            case ie: InterruptedException => // Cancelled            case NonFatal(e) => logWarning(s"Failed to connect to master $masterAddress", e)          }        }      })    }  }

    

    最后就是调用sendRegisterMessageToMaster()向Master发送RegisterWorker消息,这个消息会在Maser.scala文件的receive()函数中进行处理,完成与Master的通信。

 private def sendRegisterMessageToMaster(masterEndpoint: RpcEndpointRef): Unit = {   // 发送消息 这里参数有唯一的id, 主机名 端口 cpu 内存等信息    masterEndpoint.send(RegisterWorker(      workerId,      host,      port,      self,      cores,      memory,      workerWebUiUrl,      masterEndpoint.address))  }

   

6.Worker里面还有几个比较重要的东西,这里说明:


    1).Worker向Master注册完成后,会启动一个定时任务,定时向Master发送心跳,说明自己还活着,主要就是下面这行代码:

 changeMaster(masterRef, masterWebUiUrl, masterAddress)        forwordMessageScheduler.scheduleAtFixedRate(new Runnable {          override def run(): Unit = Utils.tryLogNonFatalError {            self.send(SendHeartbeat)          }        }, 0, HEARTBEAT_MILLIS, TimeUnit.MILLISECONDS)

    2).还有一个就是Worker启动时会新建本地工作目录,它会启动一个定时任务去清理本地工作目录,代码如下:

 forwordMessageScheduler.scheduleAtFixedRate(new Runnable {            override def run(): Unit = Utils.tryLogNonFatalError {              self.send(WorkDirCleanup)            }          }, CLEANUP_INTERVAL_MILLIS, CLEANUP_INTERVAL_MILLIS, TimeUnit.MILLISECONDS)

   

    以上两个内容我会在下一篇文章中讲到,这里也提前提一下。


     

    这里Worker的启动基本结束了,总结一下,它主要是进行了Rpc环境的创建和初始化,初始化Worker实例,包含名称、地址、端口、cpu、内存信息,然后向Master进行注册,接下来我们将分析Worker端的消息处理。