spark rpc

前段时间研究了一下spark rpc这部分源码,现在来总结下,以免以后忘记,spark rpc代码比较复杂抽象,这里我就想到什么就写什么,可能逻辑顺序上不是很严谨,大家多见谅;说明下,这里源码的为2.x版本;

底层通信框架

spark在1.6版本之后底层通信框架用netty替代了actor;具体原因,查看了网上的文档,大多说开发中经常使用到actor框架,容易和spark中的actor版本冲突,所以使用netty取代;关于spark使用netty,个人感觉还是结合了scala语言偏函数的特性;在进程之间通信不需要在额外定义通信协议;说白了,就是说我随便定义一个case class就可以当做请求消息;只需要在服务端的接收入口函数中对请求进行模糊匹配,匹配到相应的请求处理分支就行

请求分类
  1. spark rpc中的请求分为两种,一种是只请求不需要返回值;另一种就是需要返回值(这里使用回调的方式获取返回值);
  2. spark使用全双工的通信方式;也就是说,服务端和客户端既可以接收消息,也可以发送请求
  3. spark进程之间通信,按距离划分,可以分为本地调用走的inbox处理分支;远程调用使用outbox处理分支(inbox和outbox后续会介绍)
通信环境以及重要组件
RpcEndpoint

RpcEndpoint可以理解为服务端接口,接收处理client发送过来的请求;例如standalone部署模式中的Master和Worker,都实现了RpcEndpoint接口;只不过他们是在不同节点启动的进程;
RpcEndpoint的生命周期:onStart -> receive(receiveAndReply)* -> onStop
这里我们以Master启动来具体说下Endpoint的生命周期

  • Master启动
    Master启动直接调用startRpcEnvAndEndpoint()这个方法
  def startRpcEnvAndEndpoint(
      host: String,
      port: Int,
      webUiPort: Int,
      conf: SparkConf): (RpcEnv, Int, Option[Int]) = {
    val securityMgr = new SecurityManager(conf)
    val rpcEnv = RpcEnv.create(SYSTEM_NAME, host, port, conf, securityMgr)
    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启动后会调用onstart()方法,下面代码是onstart部分代码

    checkForWorkerTimeOutTask = forwardMessageThread.scheduleAtFixedRate(new Runnable {
      override def run(): Unit = Utils.tryLogNonFatalError {
        self.send(CheckForWorkerTimeOut)
      }
    }, 0, WORKER_TIMEOUT_MS, TimeUnit.MILLISECONDS)

主要是检测所有注册到Master上的worker是否发生超时;之前说过sparkrpc分为本地调用和远程调用;self.send(CheckForWorkerTimeOut)这个就属于本地调用;master给自己发送CheckForWorkerTimeOut消息,Master接收消息使用receive这个方法,也就是生命周期中的第二部分

override def receive: PartialFunction[Any, Unit] = {
    case ElectedLeader =>省略处理逻辑
    case CompleteRecovery => completeRecovery()
    case RevokedLeadership =>省略处理逻辑
    case RegisterWorker(
    //workerRef worker的引用,master与worker通行需要workerRef
      id, workerHost, workerPort, workerRef, cores, memory, workerWebUiUrl, masterAddress) =>
      //处理worker发送过来的application注册请求
    case RegisterApplication(description, driver) =>
    case ExecutorStateChanged(appId, execId, state, message, exitStatus) =>
    case DriverStateChanged(driverId, state, exception) =>
    case Heartbeat(workerId, worker) =>
    case MasterChangeAcknowledged(appId) =>
    case WorkerSchedulerStateResponse(workerId, executors, driverIds) =>
    case WorkerLatestState(workerId, executors, driverIds) =>
//      master收到worker发送的最近状态消息后,对worker上的Executor和driver的合法性进行判断,如果
//      存在executor和dirver不合法,也就是master的缓存中不存在,则通知worker进行kill操作
    case UnregisterApplication(applicationId) =>
    case CheckForWorkerTimeOut =>
      timeOutDeadWorkers()
  }

receive()这个方法使用case class这种匹配方式处理来自本地和远程发送的请求,这也是借助于scala的特性;可以看到Master接收到CheckForWorkerTimeOut 这个类别的消息,使用timeOutDeadWorkers()这个方法处理;master根据之前心跳包发送情况,把发生超时的worker从内存中移除(Master用HashSet存储所有注册到自身的worker)

  private def timeOutDeadWorkers() {
    // Copy the workers into an array so we don't modify the hashset while iterating through it
    val currentTime = System.currentTimeMillis()
    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, s"Not receiving heartbeat for ${WORKER_TIMEOUT_MS / 1000} seconds")
      } 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
        }
      }
    }
  }

Endpoint生命周期第三部分onStop,这个方法没啥好讲的,就是各种stop,以及资源释放

  override def onStop() {
    masterMetricsSystem.report()
    applicationMetricsSystem.report()
    // prevent the CompleteRecovery message sending to restarted master
    if (recoveryCompletionTask != null) {
      recoveryCompletionTask.cancel(true)
    }
    if (checkForWorkerTimeOutTask != null) {
      checkForWorkerTimeOutTask.cancel(true)
    }
    forwardMessageThread.shutdownNow()
    webUi.stop()
    restServer.foreach(_.stop())
    masterMetricsSystem.stop()
    applicationMetricsSystem.stop()
    persistenceEngine.close()
    leaderElectionAgent.stop()
  }
RpcEndpointRef

RpcEndpointRef可以理解为是Endpoint的引用,试想一下,我们如果想要和远程的Endpoint进行通信,那第一步就是获得远程调用的引用;之前讲解的Master检查worker超时使用的是本地调用,接下来我们用worker注册来讲解下EndpointRef。
worker只是集群中某一个节点上启动的一个jvm进程;worker启动后,第一件事就是向master汇报注册,通知master自己的存在,并接受master的调用管理;我们来看下worker启动的代码片段

    rpcEnv.setupEndpoint(ENDPOINT_NAME, new Worker(rpcEnv, webUiPort, cores, memory,
      masterAddresses, ENDPOINT_NAME, workDir, conf, securityMgr))

按照之前介绍的Endpoint的生命周期,worker启动后首先将会调用onStart()方法

  override def onStart() {
    //省略
    //向Master注册
    registerWithMaster()
    //省略
  }

onstart()方法中使用registerWithMaster() 向Master发送注册请求,具体方法调用链

onStart()->registerWithMaster()->tryRegisterAllMasters()->sendRegisterMessageToMaster()

获得masterEndpoint引用代码,和发送注册请求代码片段如下,大家自己沿着上面的方法调用链追踪下

val masterEndpoint = rpcEnv.setupEndpointRef(masterAddress, Master.ENDPOINT_NAME)
masterEndpoint.send(RegisterWorker(
      workerId,
      host,
      port,
      self,
      cores,
      memory,
      workerWebUiUrl,
      masterEndpoint.address)

调用NettyRpcEnv的setupEndpointRef获得MasterEndpoint的引用masterEndpoint ,这里传入masterAddress, Master.ENDPOINT_NAME两个参数;这个很好理解,你需要获得远程引用是不是得获得ip,以及引用的名字;
本地调用的话ip就是localhost;获得masterEndpointRef后,随后就给master发送RegisterWorker消息,接下来我们看下master是怎么处理的[Endpoint使用receive()方法处理接收到的请求(本地和远程)]

override def receive: PartialFunction[Any, Unit] = {
    case ElectedLeader =>省略处理逻辑
    case RegisterWorker(
      id, workerHost, workerPort, workerRef, cores, memory, workerWebUiUrl, masterAddress) =>
      if (state == RecoveryState.STANDBY) {
        workerRef.send(MasterInStandby)
      } else if (idToWorker.contains(id)) {
        workerRef.send(RegisterWorkerFailed("Duplicate worker ID"))
      } else {
        val worker = new WorkerInfo(id, workerHost, workerPort, cores, memory,
          workerRef, workerWebUiUrl)
        if (registerWorker(worker)) {
          //对worker进行持久化
          persistenceEngine.addWorker(worker)
//          向worker发送注册完成的消息
          workerRef.send(RegisteredWorker(self, masterWebUiUrl, masterAddress))
          schedule()
        } else {
          val workerAddress = worker.endpoint.address
          workerRef.send(RegisterWorkerFailed("Attempted to re-register worker at same address: "
            + workerAddress))
        }
      }

  }

Master收到worker发送过来的注册请求后,首先会对worker信息进行持久化

persistenceEngine.addWorker(worker)

持久化的方式用很多种:内存,zookeeper和Filesystem等等,可以简单理解为master会将所有注册上来的worker信息(包括ip,workerid等)在自己能访问的地方保留一份,方便之后通信使用;持久化之后master需要告诉worker注册成功,所以master会给worker发送RegisteredWorker消息,到这里worker注册就结束了;

NettyRpcEnv

NettyRpcEnv是spark rpc通信集成环境,对netty框架进行封装,用户不需要过多的去考虑netty通信相关细节,只需要实现Endpoint即可,接下来我们来介绍下spark rpc是如何将netty集成到运行环境的,NettyRpcEnv的构建如下

//    NettyRpcEnv
    val rpcEnv = RpcEnv.create(SYSTEM_NAME, host, port, conf, securityMgr)
    val masterEndpoint = rpcEnv.setupEndpoint(ENDPOINT_NAME,
      new Master(rpcEnv, rpcEnv.address, webUiPort, securityMgr, conf))
    rpcEnv.awaitTermination()

在每一个Endpoint启动的时候,首先都会去构建NettyRpcEnv,然后使用setupEndpoint将Endpoint注册到NettyRpcEnv中去(其实最终Endpoint会被注册到Dispatcher中去);有点扯远了,回到构建NettyRpcEnv;在NettyRpcEnv构造完后,如果之前conf中传入的不是config.clientMode,NettyRpcEnv就会启动netty服务,所有
EndPoint之间的远程调用都是通过netty,关于netty这里就不展开讲,内容太多了

  def startServer(bindAddress: String, port: Int): Unit = {
    val bootstraps: java.util.List[TransportServerBootstrap] =
      if (securityManager.isAuthenticationEnabled()) {
        java.util.Arrays.asList(new AuthServerBootstrap(transportConf, securityManager))
      } else {
        java.util.Collections.emptyList()
      }
    //创建TransportServer,启动netty服务
    server = transportContext.createServer(bindAddress, port, bootstraps)
    dispatcher.registerRpcEndpoint(
      RpcEndpointVerifier.NAME, new RpcEndpointVerifier(this, dispatcher))
  }

关于NettyRpcEnv你需要记住的是它集成了netty通信框架;除此之外,NettyRpcEnv还集成了Dispatcher,这个可以算是最核心的部分了,接下来我们着重讲下Dispatcher

Dispatcher

首先来看下Dispatcher都有些什么东西

  private class EndpointData(
      val name: String,
      val endpoint: RpcEndpoint,
      val ref: NettyRpcEndpointRef) {
    val inbox = new Inbox(ref, endpoint)
  }

  private val endpoints: ConcurrentMap[String, EndpointData] =
    new ConcurrentHashMap[String, EndpointData]
  private val endpointRefs: ConcurrentMap[RpcEndpoint, RpcEndpointRef] =
    new ConcurrentHashMap[RpcEndpoint, RpcEndpointRef]
  private val receivers = new LinkedBlockingQueue[EndpointData]
  private val threadpool: ThreadPoolExecutor
  1. 内部使用集合endpoints和endpointRefs维护Endpoint、EndpointRef,对外通过registerRpcEndpoint、removeRpcEndpointRef、getRpcEndpointRef等方法提供Endpoint注册删除和获取EndpointRef等服务。
  2. 利用EndpointData和Inbox结构完成消息的存储,Inbox可以理解为Endpoint的收件箱,每一个Endpoint都有一个Inbox,所有给Endpoint发送的请求都会才在Inbox的维护的队列中;inbox中接收的是本地调用发送的消息,也就是说远程调用发送的请求不会进入到inbox;关于远程发送的消息其实走的是outbox分支,随后会讲解
  3. 创建线程池threadpool,执行MessageLoop线程,消费消息。线程池里的线程用于处理一类Endpoint的Inbox中的消息

在构建NettyRpcEnv的时候,都会调用rpcEnv.awaitTermination(),其实这个方法调用的是Distpatcher的awaitTermination()

  def awaitTermination(): Unit = {
    threadpool.awaitTermination(Long.MaxValue, TimeUnit.MILLISECONDS)
  }

Distpatcher调用此方法是会堵塞主线程,直至处理完所有Endpoint Inbox中的消息(消息怎么处理的完?其实就是相当于启动了一个服务,处理所有接收到的消息),讲到这里我们先总结下Distpatcher的作用:
Distpatcher维护了一批Endpoint,每个Endpoint都有一个Inbox,Inbox保存了这个Endpoint接收到的消息(本地调用发送的消息);Distpatcher在启动的时候,初始化了一个线程池threadpool,此线程池中的线程用于处理Endpoint Inbox中的消息

现在我们来看下消息是如何推送到Inbox,还是以之前master检测worker超时为例,画个图
这里写图片描述
Master启动后调用onstart()给自己发送一个CheckForWorkerTimeOut消息,发送消息使用的是NettyRpcEnv的send()方法,NettyRpcEnv首先会检查消息的地址remoteAddr,也就是这个消息是发送给谁的,发现该消息的remoteAddr和本地address是相同的,则把消息交由Dispatcher处理;Dispatcher接收到这个消息后,找到MasterEndpoint的Inbox,并把消息扔到Inbox中去;接下来threadpool中的线程发现Inbox中收到了消息,就调用Inbox.process()方法处理消息,到这里本地消息发送处理就结束了

接下来我们讲解下远程消息发送的流程,这里就以worker注册为例
这里写图片描述
worker启动后,会自动执行onstart()方法里的registerWithMaster();worker会根据master的IP地址和名字获得MasterEndpointRef,并向master发送RegisterWorker消息,这里也是使用NettyRpcEnv中的send方法,不同是,remoteAddress和本地address不相同,所以消息会放到Outbox中,并使用TransportClient向远程的MasterEndpoint发送请求注册请求;MasterEndpoint接收到消息后会直接调用Distpatcher的postRemoteMessage()方法,将收到的消息推送到Distpatcher维护的MasterEndpoint的Inbox中去,接下来Distpatcher线程池中的线程就会自动处理发送过来的消息[这里隐藏了netty通信的细节]

到这里spark rpc的基本流程也就基本写完里,写的比较笼统,sparkrpc的源码是非常值得去深入研究学习的,希望对大家学习有所帮助

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值