Spark RPC解读

Spark消息通信架构

在Sparkd定义了通信框架接口,这些接口实现中调用N etty的具体方法(Spark 2.0版本之前使用的是Akka)。RPC组件之间的关系如图所示:

image-20210610173139714

在框架中以RpcEndpoint和RpcEndpointRef实现了Actor和ActorRef相关动作(具体可以查阅Akka相关资料),其中RpcEndpointRef是RpcEndpoint的引用,在消息通信中消息发送方持有引用RpcEndpointRef.

通信框架中使用了工厂设计模式实现(文末献上鄙人对工厂模式的简单理解),这种设计方式实现了对Netty的解藕,能够根据需要引入其他的消息通信工具。

Spark启动消息通信

Spark启动过程中主要是进行Master与Worker之间的通信。首先是worker向Master发送注册,Master处理完成后,返回注册成功或注册失败消息,如果注册成功Worker定时发送心跳给Master。其关系图如下。

未命名文件

在各个模块中,如Master、Worker等,会先使用RpcEnv的静态方法创建RpcEnv实例,然后实例化Master,由于Master继承于ThreadSafeRpcEndpoint方法,创建的Master实例是一个线程安全的终端点,接着调用RpcEnv启动终端点方法,把master的终端点和其对应的引用注册到RpcEnv中。在消息通信中,其他对象只要获取了Master终端点的引用,就能够发送消息给Master进行通信。

Master实现

master的定义

源码如下:

private[deploy] class Master(
    override val rpcEnv: RpcEnv,
    address: RpcAddress,
    webUiPort: Int,
    val securityMgr: SecurityManager,
    val conf: SparkConf)
  extends ThreadSafeRpcEndpoint with Logging with LeaderElectable {
  ......
  }
 // 进一步定位
private[spark] trait ThreadSafeRpcEndpoint extends RpcEndpoint

可以看到master类继承自ThreadSafeRpcEndpoint,进一步定位可以发现ThreadSafeRpcEndpoint是继承自RpcEndpoint特质。

master启动

通过源码看看master是如何启动的


  /**
   * Start the Master and return a three tuple of:
   *   (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)
    //注册rpcEnv
    val rpcEnv = RpcEnv.create(SYSTEM_NAME, host, port, conf, securityMgr)
    // 将master自身加入到上面创建的rpcEnv中
    val masterEndpoint = rpcEnv.setupEndpoint(ENDPOINT_NAME,
      new Master(rpcEnv, rpcEnv.address, webUiPort, securityMgr, conf))
    val portsResponse = masterEndpoint.askSync[BoundPortsResponse](BoundPortsRequest)
    (rpcEnv, portsResponse.webUIPort, portsResponse.restPort)
  }

master是通过调用startRpcEnvAndEndpoint方法启动的,在这个方法里会创建一个rpcEnv,并将Master的终端点和其对应的引用注册到刚刚创建的rpcEnv中,最后将其返回。

RpcEnv抽象类

private[spark] abstract class RpcEnv(conf: SparkConf) {
  private[spark] val defaultLookupTimeout = RpcUtils.lookupRpcTimeout(conf)
  //返回endpointRef
  private[rpc] def endpointRef(endpoint: RpcEndpoint): RpcEndpointRef
  //返回RpcEnv监听的地址
  def address: RpcAddress
  //注册一个RpcEndpoint到RpcEnv并返回RpcEndpointRef
  def setupEndpoint(name: String, endpoint: RpcEndpoint): RpcEndpointRef
  //通过uri异步地查询RpcEndpointRef
  def asyncSetupEndpointRefByURI(uri: String): Future[RpcEndpointRef]
  //通过uri查询RpcEndpointRef,这种方式会产生阻塞
  def setupEndpointRefByURI(uri: String): RpcEndpointRef = {
    defaultLookupTimeout.awaitResult(asyncSetupEndpointRefByURI(uri))
  }
  //通过address和endpointName查询RpcEndpointRef,这种方式会产生阻塞
  def setupEndpointRef(address: RpcAddress, endpointName: String): RpcEndpointRef = {
    setupEndpointRefByURI(RpcEndpointAddress(address, endpointName).toString)
  }
  //关掉endpoint
  def stop(endpoint: RpcEndpointRef): Unit
  //关掉RpcEnv
  def shutdown(): Unit
  //等待结束
  def awaitTermination(): Unit
  //没有RpcEnv的话RpcEndpointRef是无法被反序列化的,这里是反序列化逻辑
  def deserialize[T](deserializationAction: () => T): T
  //返回文件server实例
  def fileServer: RpcEnvFileServer
  //开一个针对给定URI的channel用来下载文件
  def openChannel(uri: String): ReadableByteChannel
}

可以看下startRpcEnvAndEndpoint创建rpcEnv的代码:

private[spark] object RpcEnv {

  def create(
      name: String,
      host: String,
      port: Int,
      conf: SparkConf,
      securityManager: SecurityManager,
      clientMode: Boolean = false): RpcEnv = {
    val config = RpcEnvConfig(conf, name, host, port, securityManager, clientMode)
    new NettyRpcEnvFactory().create(config)
  }
}

这就是在master启动方法中的create具体实现,可以看到调用了Netty工厂方法NettyRpcEnvFactory,该方法是对Netty的具体封装。

RpcEndpoint

上面已经说了master的启动会创建一个RpcEnv并将自己注册到其中,继续看下Rpcendpoint

al rpcEnv: RpcEnv
  //直接用来发送消息的RpcEndpointRef,可以类比为Akka中的actorRef
  final def self: RpcEndpointRef = {
    require(rpcEnv != null, "rpcEnv has not been initialized")
    rpcEnv.endpointRef(this)
  }
  //处理来自RpcEndpointRef.send或者RpcCallContext.reply的消息
  def receive: PartialFunction[Any, Unit] = {
    case _ => throw new SparkException(self + " does not implement 'receive'")
  }
  //处理来自RpcEndpointRef.ask的消息,会有相应的回复
  def receiveAndReply(context: RpcCallContext): PartialFunction[Any, Unit] = {
    case _ => context.sendFailure(new SparkException(self + " won't reply anything"))
  }
  //篇幅限制,其余onError,onConnected,onDisconnected,onNetworkError,
  //onStart,onStop,stop方法此处省略
}

Worker的定义

worker和master的定义差不多在此不在做过多的说明。

Master和Worker之间的通信

当Master启动之后,随之启动各个Worker, Worker启动时会创建通信环境RpcEnv和终端点Endpoint,并向Master发送注册Worker的消息RegisterWorker.

由于worker可能需要注册到多个Master中(如HA环境),在Worker的tryRegisterAllMasters方法中创建注册线程池registerMasterThreadPool,把需要申请的请求放到该线程池中,然后通过线程池启动注册线程。在该注册中,获取Master终端点引用,接着调用sendRegisterMessageToMaster,向Master发送注册信息。

Worker.tryRegisterAllMasters方法代码如下:

private def tryRegisterAllMasters(): Array[JFuture[_]] = {
    masterRpcAddresses.map { masterAddress =>
      registerMasterThreadPool.submit(new Runnable {
        override def run(): Unit = {
          try {
            logInfo("Connecting to master " + masterAddress + "...")
            // 获取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方法如下:

private def sendRegisterMessageToMaster(masterEndpoint: RpcEndpointRef): Unit = {
    masterEndpoint.send(RegisterWorker(
      workerId,
      host,
      port,
      self,
      cores,
      memory,
      workerWebUiUrl,
      masterEndpoint.address,
      resources))
  }

Master收到消息之后,需要对Worker发送的信息进行验证、记录。如果注册成功,则发送RegisterWorker消息给对应worker,告诉Worker已经完成注册,随之进行步骤3,即Worker定期发送心跳信息给Master;如果注册失败则会发送RegisterWorkerFaild消息,Work打印错误日志并结束Worker启动。

当Master接收到Worker注册消息后,先判断Master当前状态是否处于STANDBY状态,如果是则忽略该消息,如果注册列表中发现了该Worker的编号,则发送注册失败的消息。Master.receive方法中注册Worker代码实现如下所示:

case RegisterWorker(
      id, workerHost, workerPort, workerRef, cores, memory, workerWebUiUrl,
      masterAddress, resources) =>
      logInfo("Registering worker %s:%d with %d cores, %s RAM".format(
        workerHost, workerPort, cores, Utils.megabytesToString(memory)))
        //Master处于STANDBY状态,返回Master处于STANDBY状态消息
      if (state == RecoveryState.STANDBY) {
        workerRef.send(MasterInStandby)
      } else if (idToWorker.contains(id)) {
        workerRef.send(RegisteredWorker(self, masterWebUiUrl, masterAddress, true))
      } else {
        val workerResources = resources.map(r => r._1 -> WorkerResourceInfo(r._1, r._2.addresses))
        //registerWorker方法中注册Worker,该方法中会把worker放到列表中
        val worker = new WorkerInfo(id, workerHost, workerPort, cores, memory,
          workerRef, workerWebUiUrl, workerResources)
        if (registerWorker(worker)) {
          persistenceEngine.addWorker(worker)
          workerRef.send(RegisteredWorker(self, masterWebUiUrl, masterAddress, false))
          schedule()
        } else {
          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))
        }
      }

当Worker接收到注册成功后,会定时发送心跳信息Heartbeat给Master,以便Master了解Worker的实时状态。间隔时间可以在spark.worker.timeout中设置,注意的是心跳间隔为该设置值的1/4。

private val HEARTBEAT_MILLIS = conf.get(WORKER_TIMEOUT) * 1000 / 4

当Worker获取到注册成功消息后,先记录日志并更新Master信息,然后启动定时调度进程发送心跳信息,该调度进程时间间隔为上面所定义的HEARTBEAT_MILLIS值。

case RegisteredWorker(masterRef, masterWebUiUrl, masterAddress, duplicate) =>
        val preferredMasterAddress = if (preferConfiguredMasterAddress) {
          masterAddress.toSparkURL
        } else {
          masterRef.address.toSparkURL
        }

        // there're corner cases which we could hardly avoid duplicate worker registration,
        // e.g. Master disconnect(maybe due to network drop) and recover immediately, see
        // SPARK-23191 for more details.
        if (duplicate) {
          logWarning(s"Duplicate registration at master $preferredMasterAddress")
        }

        logInfo(s"Successfully registered with master $preferredMasterAddress")
        registered = true
        changeMaster(masterRef, masterWebUiUrl, masterAddress)
        forwardMessageScheduler.scheduleAtFixedRate(
          () => Utils.tryLogNonFatalError { self.send(SendHeartbeat) },
          0, HEARTBEAT_MILLIS, TimeUnit.MILLISECONDS)
          // 如果设置清理以前应用使用的文件夹,则进行该动作
        if (CLEANUP_ENABLED) {
          logInfo(
            s"Worker cleanup enabled; old application directories will be deleted in: $workDir")
          forwardMessageScheduler.scheduleAtFixedRate(
            () => Utils.tryLogNonFatalError { self.send(WorkDirCleanup) },
            CLEANUP_INTERVAL_MILLIS, CLEANUP_INTERVAL_MILLIS, TimeUnit.MILLISECONDS)
        }
        // 向Master汇报Worker中Executor最新状态

        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))

小结

RPC的机制远远不止这些,得益于前人的工作我们可以不用关心RPC是如何实现的,设计出复杂的分布式系统。

注:工厂模式就是创建一个接口,然后让一些实体类去实现它,在创建一个工厂类对这些实体方法进行封装,然后用户可以创建一个工厂实例,通过该实例可以生成不同功能的实体类对象。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值