为了账号安全,请及时绑定邮箱和手机立即绑定

Spark RPC 框架源码分析(二)运行时序

前情提要:

一. Spark RPC 概述概述

上一篇我们已经说明了 Spark RPC 框架的一个简单例子,以及一些基本概念的说明。这一篇我们主要讲述其运行时序,从而揭露 Spark RPC 框架的运行原理。我们将分为两部分,分别从服务端和客户端来看。

所用 spark 版本:spark 2.1.0

二. Spark RPC 服务端

我们以上一篇 HelloworldServer 为线索,深入到 Spark RPC 框架来看看启动一个服务时都做了些什么。

HelloworldServer{
  ......
  def main(args: Array[String]): Unit = {
	//val host = args(0)
    val host = "localhost"
    val config = RpcEnvServerConfig(new RpcConf(), "hello-server", host, 52345)
    val rpcEnv: RpcEnv = NettyRpcEnvFactory.create(config)
    val helloEndpoint: RpcEndpoint = new HelloEndpoint(rpcEnv)
    rpcEnv.setupEndpoint("hello-service", helloEndpoint)
    rpcEnv.awaitTermination()
  }
  ......
}
1011838-20190109170350641-488844565.png

这段代码中有两个主要流程,我们分别来说

2.1 Spark RPC 服务端 NettyRpcEnvFactory.create(config)

首先是下面这条代码的运行流程:

val rpcEnv: RpcEnv = NettyRpcEnvFactory.create(config)

其实就是通过 NettyRpcEnvFactory 创建出一个 RPC Environment ,其具体类是 NettyRpcEnv 。

我们再来看看创建过程中会发生什么。

object NettyRpcEnvFactory extends RpcEnvFactory {
	......
	def create(config: RpcEnvConfig): RpcEnv = {
		val conf = 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
		val javaSerializerInstance =
		new JavaSerializer(conf).newInstance().asInstanceOf[JavaSerializerInstance]
		//根据配置以及地址,new 一个 NettyRpcEnv ,
		val nettyEnv =
		new NettyRpcEnv(conf, javaSerializerInstance, config.bindAddress)
		//如果是服务端创建的,那么会启动服务。服务端和客户端都会通过这个方法创建一个 NettyRpcEnv ,但区别就在这里了。
		if (!config.clientMode) {
		val startNettyRpcEnv: Int => (NettyRpcEnv, Int) = { actualPort =>
			//启动服务的方法,下一步就是调用这个方法了
			nettyEnv.startServer(config.bindAddress, actualPort)
			(nettyEnv, nettyEnv.address.port)
		}
		try {
			Utils.startServiceOnPort(config.port, startNettyRpcEnv, conf, config.name)._1
		} catch {
			case NonFatal(e) =>
			nettyEnv.shutdown()
			throw e
		}
		}
		nettyEnv
	}
	......
}

还没完,如果是服务端调用这段代码,那么主要的功能是创建 RPCEnv ,即 NettyRpcEnv(客户端在后面说) 。以及通过下面这行代码,

nettyEnv.startServer(config.bindAddress, actualPort)

去调用相应的方法启动服务端的服务。下面进入到这个方法中去看看。

class NettyRpcEnv(
                   val conf: RpcConf,
                   javaSerializerInstance: JavaSerializerInstance,
                   host: String) extends RpcEnv(conf) {
  ......
  def startServer(bindAddress: String, port: Int): Unit = {
    // here disable security
    val bootstraps: java.util.List[TransportServerBootstrap] = java.util.Collections.emptyList()
    //TransportContext 属于 spark.network 中的部分,负责 RPC 消息在网络中的传输
    server = transportContext.createServer(bindAddress, port, bootstraps)
    //在每个 RpcEndpoint 注册的时候都会注册一个默认的 RpcEndpointVerifier,它的作用是客户端调用的时候先用它来询问 Endpoint 是否存在。
    dispatcher.registerRpcEndpoint(
      RpcEndpointVerifier.NAME, new RpcEndpointVerifier(this, dispatcher))
  }
  ......
}

执行完毕之后这个 create 方法就结束。这个流程主要就是开启一些服务,然后返回一个新的 NettyRpcEnv 。

2.2 Spark RPC 服务端 rpcEnv.setupEndpoint(“hello-service”, helloEndpoint)

这条代码会去调用 NettyRpcEnv 中相应的方法

class NettyRpcEnv(
                   val conf: RpcConf,
                   javaSerializerInstance: JavaSerializerInstance,
                   host: String) extends RpcEnv(conf) {
  ......
  override def setupEndpoint(name: String, endpoint: RpcEndpoint): RpcEndpointRef = {
    dispatcher.registerRpcEndpoint(name, endpoint)
  }
  ......
}

我们看到,这个方法主要是调用 dispatcher 进行注册的。dispatcher 的功能上一节已经说了,

Dispatcher 的主要作用是保存注册的RpcEndpoint、分发相应的Message到RpcEndPoint中进行处理。Dispatcher 即是上图中 ThreadPool的角色。它同时也维系一个 threadpool,用来处理每次接受到的 InboxMessage 。而这里处理 InboxMessage 是通过 inbox 实现的。

,这里我们就说一说 dispatcher 的流程。

dispatcher

dispatcher 在 NettyRpcEnv 被创建的时候创建出来。

class NettyRpcEnv(
                   val conf: RpcConf,
                   javaSerializerInstance: JavaSerializerInstance,
                   host: String) extends RpcEnv(conf) {
	......
	//初始化时创建 dispatcher
	private val dispatcher: Dispatcher = new Dispatcher(this)
	......
}

dispatcher 类被创建的时候也有几个属性需要注意:

private[netty] class Dispatcher(nettyEnv: NettyRpcEnv) {
	......
	//每个 RpcEndpoint 其实都会被整合成一个 EndpointData 。并且每个 RpcEndpoint 都会有一个 inbox。
	private class EndpointData(
								val name: String,
								val endpoint: RpcEndpoint,
								val ref: NettyRpcEndpointRef) {
		val inbox = new Inbox(ref, endpoint)
	}
	
	//一个阻塞队列,当有 RpcEndpoint 相关请求(InboxMessage)的时候,就会将请求塞到这个队列中,然后被线程池处理。
	private val receivers = new LinkedBlockingQueue[EndpointData]
	
	//初始化便创建出来的线程池,当上面的 receivers 队列中没内容时,会阻塞。当有 RpcEndpoint 相关请求(即 InboxMessage )的时候就会立刻执行。
	//这里处理 InboxMessage 本质上是调用相应 RpcEndpoint 的 inbox 去处理。
	private val threadpool: ThreadPoolExecutor = {
		val numThreads = nettyEnv.conf.getInt("spark.rpc.netty.dispatcher.numThreads",
		math.max(2, Runtime.getRuntime.availableProcessors()))
		val pool = ThreadUtils.newDaemonFixedThreadPool(numThreads, "dispatcher-event-loop")
		for (i <- 0 until numThreads) {
			pool.execute(new MessageLoop)
		}
		pool
	}
	......
}

了解一些 Dispatcher 的逻辑流程后,我们来正式看看 Dispatcher 的 registerRpcEndpoint 方法。

顾名思义,这个方法就是将 RpcEndpoint 注册到 Dispatcher 中去。当有 Message 到来的时候,便会分发 Message 到相应的 RpcEndPoint 中进行处理。

private[netty] class Dispatcher(nettyEnv: NettyRpcEnv) {
  ......
  def registerRpcEndpoint(name: String, endpoint: RpcEndpoint): NettyRpcEndpointRef = {
    val addr = RpcEndpointAddress(nettyEnv.address, name)
	//注册 RpcEndpoint 时需要的是 上面的 EndpointData ,其中就包含 endpointRef ,这个主要是供客户端使用的。
    val endpointRef = new NettyRpcEndpointRef(nettyEnv.conf, addr, nettyEnv)
	//多线程环境下,注册一个 RpcEndpoint 需要判断现在是否处于 stop 状态。
    synchronized {
      if (stopped) {
        throw new IllegalStateException("RpcEnv has been stopped")
      }
	  //新建 EndpointData 并存储到一个 ConcurrentMap 中。
      if (endpoints.putIfAbsent(name, new EndpointData(name, endpoint, endpointRef)) != null) {
        throw new IllegalArgumentException(s"There is already an RpcEndpoint called $name")
      }
      val data = endpoints.get(name)
      endpointRefs.put(data.endpoint, data.ref)
      //将 这个 EndpointData 加入到 receivers 队列中,此时 dispatcher 中的 threadpool 会去处理这个加进来的 EndpointData 
      //处理过程是调用它的 inbox 的 process()方法。然后 inbox 会等待消息到来。
      receivers.offer(data) // for the OnStart message
    }
    endpointRef
  }
  ......
}

Spark RPC 服务端逻辑小结:我们说明了 Spark RPC 服务端启动的逻辑流程,分为两个部分,第一个是 Spark RPC env ,即 NettyRpcEnv 的创建过程,第二个则是 RpcEndpoint 注册到 dispatcher 的流程。
1. NettyRpcEnvFactory 创建 NettyRpcEnv

  • 根据地址创建 NettyRpcEnv。
  • NettyRpcEnv 开始启动服务,包括 TransportContext 根据地址开启监听服务,向 Dispacther 注册一个 RpcEndpointVerifier 等待。

2. Dispatcher 注册 RpcEndpoint

  • Dispatcher 初始化时便创建一个线程池并阻塞等待 receivers 队列中加入新的 EndpointData
  • 一旦新加入 EndpointData 便会调用该 EndpointData 的 inbox 去处理消息。比如 OnStart 消息,或是 RPCMessage 等等。

三.Spark RPC 客户端

依旧是以上一节 Spark RPC 客户端 HelloWorld 的为线索,我们来逐层深入 Spark RPC 客户端 HelloworldClient 的 asyncCall() 方法。

object HelloworldClient {
  ......
  def asyncCall() = {
    val rpcConf = new RpcConf()
    val config = RpcEnvClientConfig(rpcConf, "hello-client")
    val rpcEnv: RpcEnv = NettyRpcEnvFactory.create(config)
    val endPointRef: RpcEndpointRef = rpcEnv.setupEndpointRef(RpcAddress("localhost", 52345), "hello-service")
    val future: Future[String] = endPointRef.ask[String](SayHi("neo"))
    future.onComplete {
      case scala.util.Success(value) => println(s"Got the result = $value")
      case scala.util.Failure(e) => println(s"Got error: $e")
    }
    Await.result(future, Duration.apply("30s"))
    rpcEnv.shutdown()
  }
  ......
}


1011838-20190109170355974-225523803.png

创建 Spark RPC 客户端 Env(即 NettyRpcEnvFactory ) 部分和 Spark RPC 服务端是一样的,只是不会开启监听服务,这里就不详细展开。

我们从这一句开始看,这也是 Spark RPC 客户端和服务端区别的地方所在。

val endPointRef: RpcEndpointRef = rpcEnv.setupEndpointRef(RpcAddress("localhost", 52345), "hello-service")

setupEndpointRef()

上面的的 setupEndpointRef 最终会去调用下面 setupEndpointRef() 这个方法,这个方法中又进行一次跳转,跳转去 setupEndpointRefByURI 这个方法中 。需要注意的是这两个方法都是 RpcEnv 里面的,而 RpcEnv 是抽象类,它里面只实现部分方法,而 NettyRpcEnv 继承了它,实现了全部方法。

abstract class RpcEnv(conf: RpcConf) {
  ......
  def setupEndpointRef(address: RpcAddress, endpointName: String): RpcEndpointRef = {
    //会跳转去调用下面的方法
    setupEndpointRefByURI(RpcEndpointAddress(address, endpointName).toString)
  }
  
  def setupEndpointRefByURI(uri: String): RpcEndpointRef = {
    //其中 asyncSetupEndpointRefByURI() 返回的是 Future[RpcEndpointRef]。 这里就是阻塞,等待返回一个 RpcEndpointRef。
    // defaultLookupTimeout.awaitResult 底层调用 Await.result 阻塞 直到结果返回或返回异常
    defaultLookupTimeout.awaitResult(asyncSetupEndpointRefByURI(uri))
  }
  ......
}  

这里最主要的代码其实就一句,

defaultLookupTimeout.awaitResult(asyncSetupEndpointRefByURI(uri))

这一段可以分为两部分, 第一部分的 defaultLookupTimeout.awaitResult 其实底层是调用 Await.result 阻塞等待一个异步操作,直到结果返回。

而asyncSetupEndpointRefByURI(uri) 则是根据给定的 uri 去返回一个 RpcEndpointRef ,它是在 NettyRpcEnv 中实现的:

class NettyRpcEnv(
                   val conf: RpcConf,
                   javaSerializerInstance: JavaSerializerInstance,
                   host: String) extends RpcEnv(conf) {
  ......
  def asyncSetupEndpointRefByURI(uri: String): Future[RpcEndpointRef] = {
    //获取地址
    val addr = RpcEndpointAddress(uri)
    //根据地址等信息新建一个 NettyRpcEndpointRef 。
    val RpcendpointRef = new NettyRpcEndpointRef(conf, addr, this) 
    //每个新建的 RpcendpointRef 都有先有一个对应的verifier 去检查服务端存不存在对应的 Rpcendpoint 。  
    val verifier = new NettyRpcEndpointRef(
      conf, RpcEndpointAddress(addr.rpcAddress, RpcEndpointVerifier.NAME), this)
    //向服务端发送请求判断是否存在对应的 Rpcendpoint。
    verifier.ask[Boolean](RpcEndpointVerifier.createCheckExistence(endpointRef.name)).flatMap { find =>
      if (find) {
        Future.successful(endpointRef)
      } else {
        Future.failed(new RpcEndpointNotFoundException(uri))
      }
    }(ThreadUtils.sameThread)
  }
  ......
}
  

asyncSetupEndpointRefByURI() 这个方法实现两个功能,第一个就是新建一个 RpcEndpointRef 。第二个是新建一个 verifier ,这个 verifier 的作用就是先给服务端发送一个请求判断是否存在 RpcEndpointRef 对应的 RpcEndpoint 。

这段代码中最重要的就是 verifiter.ask[Boolean](…) 了。如果有找到之后就会调用 Future.successful 这个方法,反之则会 通过 Future.failed 抛出一个异常。

ask 可以算是比较核心的一个方法,我们可以到 ask 方法中去看看。

class NettyRpcEnv{
	......
    private[netty] def ask[T: ClassTag](message: RequestMessage, timeout: RpcTimeout): Future[T] = {
      val promise = Promise[Any]()
      val remoteAddr = message.receiver.address
      //
      def onFailure(e: Throwable): Unit = {
  //      println("555");
        if (!promise.tryFailure(e)) {
          log.warn(s"Ignored failure: $e")
        }
      }
  
      def onSuccess(reply: Any): Unit = reply match {
        case RpcFailure(e) => onFailure(e)
        case rpcReply =>
          println("666");
          if (!promise.trySuccess(rpcReply)) {
            log.warn(s"Ignored message: $reply")
          }
      }
  
      try {
        if (remoteAddr == address) {
          val p = Promise[Any]()
          p.future.onComplete {
            case Success(response) => onSuccess(response)
            case Failure(e) => onFailure(e)
          }(ThreadUtils.sameThread)
          dispatcher.postLocalMessage(message, p)
        } else {
          //跳转到这里执行
          //封装一个 RpcOutboxMessage ,同时 onSuccess 方法也是在这里注册的。
          val rpcMessage = RpcOutboxMessage(serialize(message),
            onFailure,
            (client, response) => onSuccess(deserialize[Any](client, response)))
          postToOutbox(message.receiver, rpcMessage)
          promise.future.onFailure {
            case _: TimeoutException =>  println("111");rpcMessage.onTimeout()
  //          case _ => println("222");
          }(ThreadUtils.sameThread)
        }
        
        val timeoutCancelable = timeoutScheduler.schedule(new Runnable {
          override def run(): Unit = {
  //          println("333");
            onFailure(new TimeoutException(s"Cannot receive any reply in ${timeout.duration}"))
          }
        }, timeout.duration.toNanos, TimeUnit.NANOSECONDS)
        //promise 对应的 future onComplete时会去调用,但当 successful 的时候,上面的 run 并不会被调用。
        promise.future.onComplete { v =>
  //        println("4444");
          timeoutCancelable.cancel(true)
        }(ThreadUtils.sameThread)
  
      } catch {
        case NonFatal(e) =>
          onFailure(e)
      }
  
      promise.future.mapTo[T].recover(timeout.addMessageIfTimeout)(ThreadUtils.sameThread)
    }
	......
}

这里涉及到使用一些 scala 多线程的高级用法,包括 Promise 和 Future。如果想要对这些有更加深入的了解,可以参考这篇文章

这个函数的作用从名字中就可以看得出,其实就是将要 发送的消息封装成一个 RpcOutboxMessage ,然后交给 OutBox 去发送,OutBox 和前面所说的 InBox 对应,对应 Actor 模型中的 MailBox(信箱)。用于发送和接收消息。

其中使用到了 Future 和 Promise 进行异步并发以及错误处理,比如当发送时间超时的时候 Promise 就会返回一个 TimeoutException ,而我们就可以设置自己的 onFailure 函数去处理这些异常。

OK,注册完 RpcEndpointRef 后我们便可以用它来向服务端发送消息了,而其实 RpcEndpointRef 发送消息还是调用 ask 方法,就是上面的那个 ask 方法。上面也有介绍,本质上就是通过 OutBox 进行处理。

我们来梳理一下客户端的发送流程。

客户端逻辑小结:客户端和服务端比较类似,都是需要创建一个 NettyRpcEnv 。不同的是接下来客户端创建的是 RpcEndpointRef ,并用之向服务端对应的 RpcEndpoint 发送消息。

1. NettyRpcEnvFactory 创建 NettyRpcEnv

  • 根据地址创建 NettyRpcEnv。 根据地址开启监听服务,向 Dispacther 注册一个 RpcEndpointVerifier 等待。

2. 创建 RpcEndpointRef

  • 创建一个新的 RpcEndpointRef
  • 创建对应的 verifier ,使用 verifier 向服务端发送请求,判断对应的 RpcEndpoint 是否存在。若存在,返回该 RpcEndpointRef ,否则抛出异常。

3. RpcEndpointRef 使用同步或者异步的方式发送请求。

OK,以上就是 Spark RPC 时序的源码分析。下一篇会将一个实际的例子,Spark 的心跳机制和代码。喜欢的话就关注一波吧

点击查看更多内容
1人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
JAVA开发工程师
手记
粉丝
35
获赞与收藏
147

关注作者,订阅最新文章

阅读免费教程

感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消