(1)RpcEndpoint:RPC端点,Spark将每个通信实体/集群节点(Client/Master/Worker等)都称之为一个Rpc端点,且都实现了RpcEndpoint接口,内部根据不同端点的需求,设计不同的消息和不同的业务处理,如果需要发送消息则调用Dispatcher中的相关方法;
(2)RpcEnv:RPC上下文环境,RpcEndpoint运行时依赖的上下文环境称之为RpcEnv,其管理RpcEndpoint需要的一些东西,如Dispatcher消息分发器,每个RpcEndpoint都对应着自己的一个RpcEnv;
(3)Dispatcher(存在于RpcEnv中):消息分发器,用于发送消息或者从远程RpcEndpoint和本地接收消息,并将消息分发至对应的指令收件箱/发件箱。如果消息接收方是自己的话(即发往本地的消息或从远程RpcEndpoint接收到的消息),那就将消息存入本地RpcEndpoint的收件箱,如果消息接收者为远程RpcEndpoint,则将消息放入指定远程RpcEndpoint的发件箱,每个RpcEndpoint内部维护了一批远程RpcEndpoint的Outbox,表示本地RpcEndpoint需要与这些RpcEndpoint进行通信。Dispatcher类中的MessageLoop线程负责读取LinkedBlockingQueue中的RpcMessage消息,然后处理Inbox中的消息,由于是阻塞队列,当没有消息的时候自然阻塞,一旦有消息,就开始工作。Dispatcher的ThreadPool负责消费这些Message。也就是说某个RpcEndpoint需要发送消息或接收到消息时均先将消息发送到自己的Dispatcher消息分发器中,然后再由消息分区器来分配消息的处理方式,RpcEndpoint对应的Dispatcher中维护了一些远程RpcEndpoint的地址(以Outbox的形式)。
(4)Inbox:本地RpcEndpoint的消息收件箱,一个本地RpcEndpoint对应一个Inbox,Dispatcher每次向Inbox存入消息时,都将对应的EndpointData(内部维护该消息发往的本地端点的endpoint和endpointRef,以及本地端点对应的Inbox)加入到内部的Receiver Queue(阻塞队列)中,且在Dispatcher创建时会启动一个单独的线程轮询Receiver Queue队列查看有无消息,若有消息则进行消息消费。
(5)Outbox(存在于RpcEnv中):消息发件箱,一个RpcEndpoint对应的RpcEnv对象中维护了一些远程RpcEndpoint地址及其Outbox的映射(即某个RpcEndpoint中维护了一些远程RpcEndpoint的地址和对应于该远程RpcEndpoint的发件箱,便于拿到远程RpcEndpoint的地址,发送消息给远程RpcEndpoint),当消息放入Outbox后,紧接着将消息通过远程RpcEndpoint(实际是调用该端点的ref引用)对应的TransportClient发送出去。消息放入发件箱以及发送过程是在同一个线程中进行的。即发件箱中的消息是通过远程RpcEndpoint对应的TransportClient发送给其它RpcEndpoint。
(6)TransportClient:Netty通信客户端,根据Outbox消息的receiver的地址信息,实例化相应远程端点的TransportClient,然后发送消息至远程TransportServer,每个RpcEndpoint也维护了一个其它远程RpcEndpoint的TransportClient集合。
(7)TransportServer:Netty通信服务端,一个RpcEndpoint对应一个TransportServer,接收到远程消息后调用自己的Dispatcher分发消息至本地endpoint的收件箱。
综上所述:一个RpcEndpoint内部主要维护了以下一些东西:
(1)依赖一个RpcEnv上下文执行环境;
(2)维护一个Inbox,接收本地发往本地的消息以及从远程RpcEndpoint接收到的消息;
(3)维护多个远程RpcEndpoint对应的Outbox集合,每个远程RpcEndpoint对应一个Outbox,本地往某个远程RpcEndpoint发消息时,先根据远程RpcEndpoint地址找到对应的Outbox,往那个Outbox添加消息;
(4)一个Dispatcher消息分发器;
(5)维护远程RpcEndpoint对应的TransportClient映射;
(6)维护本地的TransportServer用于接收远程RpcEndpoint发送过来的消息。
RpcEndpoint接收/发送消息的过程大致如下:
接收消息:每个RpcEndpoint对应的TransportServer负责接收其它RpcEndpoint发送过来的消息,然后将消息添加到本地RpcEndpoint中的Dispatcher中,Dispatcher再将消息添加到本地的Inbox收件箱,Inbox中会有一个阻塞队列用于存放接收到的消息,然后有一个线程会不断地轮询这个队列,拉取消息进行消息的消费;
发送消息:RpcEndpoint发送消息时,先将消息放到自己的Dispatcher中,然后Dispatcher根据消息发往的远程RpcEndpoint地址找到相应的Outbox发件箱(每个本地端点维护了一些远程端点的Outbox对象),然后将消息添加到指定的Outbox中,后续通过远程RpcEndpoint对应的TransportClient将对应Outbox发件箱中的消息发送出去。
在Spark中,RpcEndpoint是所有通信实体的抽象。RpcEndpoint是一个trait,其中定义了一些函数,这些函数都是在收到某个特定的消息后才会被触发,执行相应的逻辑(真正的执行逻辑是由具体实现类实现的,如Master),其中onStart、receive和onStop这三个方法的调用是有先后顺序的,RpcEndpoint中的方法如下:
rpcEnv:RpcEndpoint执行依赖的上下文环境;
receive:接收消息并处理;
receiveAndReply:接收消息处理后,并给消息发送者返回响应;
onError:发生异常时调用;
onConnected:当客户端(远程端点)与本地RpcEndpoint建立连接后调用;
onDisconnected:当客户端与本地RpcEndpoint失去连接后调用;
onNetworkError:当网络连接发生错误时调用;
onStart:RpcEndpoint启动时调用;
onStop:RpcEndpoint停止时调用。
RpcEndpoint中最重要的几个方法:onStart、receive、receiveAndReply以及onStop,这几个方法就是RpcEndpoint的生命周期。
RpcEndpoint的继承体系如下图:
由上图可知,Master和Worker等都是一个RpcEndpoint,ClientEndpoint是每个SparkApp的终端点——即Spark应用对应的RpcEndpoint,DriverEndpoint是Spark Driver的endpoint,HeartbeatReceiver是Executor发送心跳消息给Driver的Endpoint,CoarseGrainedExecutorBackend是Spark Executor的endpoint,用于执行Executor的相关操作。上述这些RpcEndpoint都继承自ThreadSafeRpcEndpoint,即均是线程安全的。
RpcEndpointRef是对RpcEndpoint的引用,本地RpcEndpoint要向远端的一个RpcEndpoint发送消息时,必须通过远程RpcEndpoint的引用 RpcEndpointRef才能往远程端点发送消息。RpcEndpointRef指定了ip和port,是一个类似spark://host:port这种的地址,RpcEndpointRef在Spark中只有一个子类,即NettyRpcEndpointRef,即无论是何种类型的RpcEndpoint,其ref引用都是NettyRpcEndpointRef对象,内部提供了一些方法用于发送消息,如下所示:
RpcEndpoint的地址在spark中表示为RpcAddress对象,该类只有两个字段:host和port
//每个RpcEndpoint均对应不同的port,所以一个RpcEndpoint的地址由host和port唯一确定 private[spark] case class RpcAddress(host: String, port: Int) { def hostPort: String = host + ":" + port def toSparkURL: String = "spark://" + hostPort override def toString: String = hostPort }
其中的address返回该引用对应的真实RpcEndpoint的地址,name则返回对应真实RpcEndpoint的名称。此外该类还提供了ask和askSync方法,其中ask方法是异步的,返回一个Future对象用于获取响应,而askSync则是同步的,调用时会阻塞等待结果返回,而send()方法只管发送消息,不关心响应,endpointRef中的ask和send方法都是用于向远程RpcEndpoint发送消息的。
RpcEnv是RpcEndpoint的运行环境,内部维护了RpcEndpoint运行所需的一系列东西,如Dispatcher消息分发器、Outbox发件箱等(每个远程端点都对应一个Outbox),内部结构如下图:
RpcEnv类有一个伴生对象RpcEnv,该伴生对象内提供了两个create方法用于创建RpcEnv对象,其实最终调用的都是第二个create方法,通过RpcEnvFactory工厂创建NettyRpcEnv对象。
RpcEnv类中提供了一些方法用于在Dispatcher中注册RpcEndpoint和RpcEndpointRef对象,其中setupEndpoint()用于注册RpcEndpoint,setupEndpointRef()则用于获取RpcEndpointRef,在获取RpcEndpointRef时会先调用RpcEndpointVerfier这个endpoint校验对应的RpcEndpoint是否已经注册,若RpcEndpoint没有注册,则对应的RpcEndpointRef获取会失败,若已经在Dispacher中注册,则实例化一个RpcEndpointRef对象。
此外还提供了一些额外的方法:如address获取当前RpcEnv对应的地址,endpointRef根据endpoint获取对应的endpointRef对象,stop则停止当前的RpcEndpoint,shutDown则销毁当前的这个RpcEnv。
RpcEnv的在Spark中的唯一实现类NettyRpcEnv:
NettyRpcEnv中的一些成员如下所示:
private[netty] class NettyRpcEnv( val conf: SparkConf,//SparkConf对象 javaSerializerInstance: JavaSerializerInstance, host: String,//RpcEndpoint所在节点ip securityManager: SecurityManager, //某个RpcEndpoint进程分配的可用核数 numUsableCores: Int) extends RpcEnv(conf) with Logging { //消息分发器,负责此RpcEndpoint的接收消息或发生消息 private val dispatcher: Dispatcher = new Dispatcher(this, numUsableCores) //创建TransportClientFactory和TransportServer时使用 private val transportContext = new TransportContext(transportConf, new NettyRpcHandler(dispatcher, this, streamManager)) //创建TransportClient的工厂对象TransportClientFactory private val clientFactory = transportContext.createClientFactory(createClientBootstraps()) //专门用于创建该RpcEndpoint对应的TransportClient的线程池,因为创建TransportClient是一个阻塞操作,所以将其放入线程池中执行实现主线程非阻塞地创建TransportClient对象,在建立Outbox与远程RpcEndpoint连接时使用该线程池创建远程RpcEndpoint对应的TransportClient对象 private[netty] val clientConnectionExecutor = ThreadUtils.newDaemonCachedThreadPool( "netty-rpc-connection",conf.getInt("spark.rpc.connect.threads", 64)) //该RpcEndpoint对应的TransportServer对象,用于接收远程RpcEndpoint发送过来的消息 @volatile private var server: TransportServer = _ //标识该RpcEndpoint是否停止运行 private val stopped = new AtomicBoolean(false) //维护一些远程RpcEndpoint及对应Outbox的映射,即本地RpcEndpoint会为远程RpcEndpoint创建一个Outbox,并维护起来,当需要往某个远程RpcEndpoint发送消息时,根据远程RpcEndpoint的RpcAddress地址拿到对应的Outbox,把消息放入该Outbox,后续通过远程RpcEndpoint的TransportClient对象发送消息即可 private val outboxes = new ConcurrentHashMap[RpcAddress, Outbox]()
NettyRpcEnv覆写了RpcEnv中的所有方法,如setupEndpoint()注册RpcEndpoint方法,此外NettyRpcEnv自己实现了一些其它的方法,如send()、postToOutbox()等方法——发送消息至指定Outbox的方法。endpointRef的ask和send会调用RpcEnv中的ask和send方法,最终调用NettyRpcEnv的postToOutbox方法将消息添加到指定远程RpcEndpoint的Outbox中。
RpcEnvFactory在spark中只有一个实现——NettyRpcEnvFactory;
RpcEnv在spark中也只有一个实现——NettyRpcEnv;
RpcEndpoint在spark中有多个实现,如Master/Worker/BlockManagerEndpoint等实现;
RpcEndpointRef在spark中只有一个实现——NettyRpcEndpointRef,即所有不同类型的RpcEndpoint的引用都是NettyRpcEndpointRef。
(1)对于服务端来说,RpcEnv是RpcEndpoint的运行环境,负责RpcEndpoint整个生命周期的管理,它可以注册RpcEndpoint,解析TCP层的数据包并反序列化,封装成RpcMessage,并且路由请求到指定的RpcEndpoint,调用业务逻辑代码,如果RpcEndpoint需要响应,则把返回的对象序列化后通过TCP层再传输到远程RpcEndpoint,如果RpcEndpoint发生异常,那么调用RpcCallContext.sendFailure把异常发送回去。
(2)对于客户端来说,通过本地RpcEndpoint的RpcEnv可以获取远程RpcEndpoint的RpcEndpointRef对象——封装在Outbox中,拿到RpcEndpointRef后,就可以调用相应的发送消息的方法将消息发往远程端点。
RpcEnv的创建由RpcEnvFactory负责,RpcEnvFactory在spark中目前只有一个子类——NettyRpcEnvFactory,NettyRpcEnvFactory.create()方法一旦调用(创建NettyRpcEnv)就同时会在相应的address和port上实例化并启动一个TransportServer用于接收其它远程RpcEndpoint发送过来的消息,即实例化某个RpcEndpoint的NettyRpcEnv对象时就同时实例化了该RpcEndpoint对应的TransportServer对象,并随之启动接收其它远程RpcEndpoint发送过来的消息。
NettyRpcEnv由NettyRpcEnvFactory.create()创建,这是整个Spark core和org.apache.spark.spark-network-common 的桥梁。其中核心方法setupEndpoint会在Dispatcher中注册Endpoint,而setupEndpointRef获取endpoint引用前会先去调用RpcEndpointVerifier这个终端点验证本地或者远程是否存在某个endpoint,若存在对应的endpoint才会获取相应RpcEndpoint的RpcEndpointRef引用对象。
Dispacher消息分发器对象在RpcEndpoint对应的NettyRpcEnv对象实例化时被初始化,其依附于NettyRpcEnv。Dispacher主要负责对应RpcEndpoint的发送和接收消息的流程。
2.2.5.1 Dispatcher中的重要成员
//nettyEnv:依赖的NettyRpcEnv对象,numUsableCores:对应RpcEndpoint进程分配的可用核数 private[netty] class Dispatcher(nettyEnv: NettyRpcEnv, numUsableCores: Int) extends Logging { //内部类EndpointData,包含name/RpcEndpoint/RpcEndpointRef/Inbox,每个RpcEndpoint只有一个Inbox,用于存放InboxMessage,表示本地发送到本地的消息以及远程端点发往本地的消息,RpcEndpoint接收到的消息都抽象为EndpointData对象,放入消息队列中 private class EndpointData( val name: String, val endpoint: RpcEndpoint, val ref: NettyRpcEndpointRef) { val inbox = new Inbox(ref, endpoint) } //维护本地RpcEndpoint的name -> EndpointData的映射,name是相应RpcEndpoint的名字,后续会先从此映射中获取本地RpcEndpoint对应的EndpointData对象,若没有则实例化一个新的EndPointData对象 private val endpoints: ConcurrentMap[String, EndpointData] = new ConcurrentHashMap[String, EndpointData] //维护本地的RpcEndpoint与RpcEndpointRef之间的映射,里面还有一个RpcEndpointVerfier这个类型endpoint,每个RpcEndpoint都对应着一个RpcEndpointVerfier类型的endpoint,用于验证RpcEndpoint是否已经成功注册,RpcEndpoint与对应的RpcEndpointVerfier这两个endpoint的地址是一样的 private val endpointRefs: ConcurrentMap[RpcEndpoint, RpcEndpointRef] = new ConcurrentHashMap[RpcEndpoint, RpcEndpointRef] //阻塞队列,维护EndpointData集合,一个EndpointData对应一个发往本地RpcEndpoint的消息,内部封装了本地的Inbox,每次发消息到本地时,都会获取一个EndpointData对象(要么从endpoints中获取,要么新实例化一个),并将其添加到receivers队列中等待消费 private val receivers = new LinkedBlockingQueue[EndpointData] //毒药消息,只有在Dispatcher调用stop()时,才会往receivers阻塞队列中添加这个毒药消息,此时对应的MessageLoop线程就停止了,该RpcEndpoint也就挂掉了,RpcEndpoint中的某个消息消费线程从receivers队列中接收到此毒药消息后,会立马停止线程,然后重新把该毒药消息放到receivers阻塞队列中,所以最终该RpcEndpoint所有的消息消费线程都会停止 private val PoisonPill = new EndpointData(null, null, null)
2.2.5.2 Dispatcher中的消息分发原理
Dispatcher中维护了一个线程池threadpool(执行MessageLoop线程),线程池中的线程会执行MessageLoop线程对象,然后这个线程对象内的逻辑就是一直在轮询receivers阻塞队列,处理其中的消息。
//维护的线程池对象,用于轮询receivers链表的消息,也就是当RpcEnv中实例化Dispatcher对象时,这个Dispatcher内部就起了numThreads个MessageLoop线程在轮询receivers阻塞队列,并行处理该RpcEndpoint收到的消息 private val threadpool: ThreadPoolExecutor = { // 获取该RpcEndpoint分配的核数 val availableCores = if (numUsableCores > 0) numUsableCores else Runtime.getRuntime.availableProcessors() // 线程池中的线程数目,即该RpcEndpoint中并行处理消息的线程数 val numThreads = nettyEnv.conf.getInt("spark.rpc.netty.dispatcher.numThreads", math.max(2, availableCores)) // 创建定长的守护线程池 val pool = ThreadUtils.newDaemonFixedThreadPool(numThreads, "dispatcher-event-loop") for (i <- 0 until numThreads) { // 线程池中起了numThreads个线程,分别执行MessageLoop的run(),从receivers阻塞队列中拉取消息进行消费 pool.execute(new MessageLoop) } pool }
MessageLoop线程对象:
//MessageLoop线程对象,其中的run()方法就是在不断地轮询receivers阻塞队列获取EndpointData对象,其中封装了本地RpcEndpoint的Inbox,真正的消息在Inbox中 private class MessageLoop extends Runnable { override def run(): Unit = { try { // 一直在循环,除非拿到毒药消息才会停止该线程 while (true) { try { //若receivers阻塞队列中没有消息,则所有的MessageLoop线程阻塞于此 val data = receivers.take() //若拿到的消息是个毒药消息,则重新将这个毒药消息放到receivers阻塞队列中,然后这个MessageLoop线程就停止消费消息了,最后线程池中所有的MessageLoop线程都会接收到此毒药消息进而停止消费消息 if (data == PoisonPill) { //重新将毒药消息放到receivers阻塞队列中 receivers.offer(PoisonPill) //拿到毒药消息,该线程直接退出 return } //调用获取到的EndpointData对象中的inbox的process方法处理Inbox中的消息 data.inbox.process(Dispatcher.this) } catch { case NonFatal(e) => logError(e.getMessage, e) } } } catch { ...... } } }
在实例化Dispatcher对象时会创建一个线程池,线程池数量为spark.rpc.netty.dispatcher.numThreads设置的值,若没有设置则使用默认值math.max(2, Runtime.getRuntime.availableProcessors())。该线程池会启动一些MessageLoop线程,这些MessageLoop线程一直在轮询Dispacher中的receivers阻塞队列,从中取出EndpointData对象进行处理,如果receivers中没有消息,则所有线程就会阻塞。有EndpointData就从该EndpointData的Inbox中取出消息进行消费,至于Inbox内部是如何消费消息的,后面再分析,若拿到的消息是PoisonPill毒药消息,则此MessageLoop线程停止,最后该Dispatcher所有的MessageLoop线程均会停止,导致整个Dispatcher停止。
2.2.5.3 Dispatcher中的其它方法
Dispatcher类中有一系列的postMessage()方法,如postLocalMessage、postOneWayMessage、postRemoteMessage等方法,这些方法均是往receivers阻塞队列中添加EndpointData对象,这些方法将本地发往本地的消息以及从远程RpcEndpoint接收到的消息都添加到receivers阻塞队列中,然后由上述启动的那些MessageLoop线程来消费这些消息。
Dispatcher类中还有一个registerRpcEndpoint()方法,即在Dispatcher消息分发器中注册并启动本地的RpcEndpoint对象,启动/注册endpoint时都会调用registerRpcEndpoint()方法,该方法会往本地RpcEndpoint的Inbox中添加一条OnStart消息,即发送一条OnStart消息给自己,启动相应的RpcEndpoint。
Inbox中的消息对象都是InboxMessage对象,具体使用时是其子类:OneWayMessage、RpcMessage等
2.2.6.1 Inbox类的重要成员
首先Inbox的构造会接收一个RpcEndpoint和RpcEndpointRef对象,标识该Inbox属于该RpcEndpoint:
private[netty] class Inbox( val endpointRef: NettyRpcEndpointRef, val endpoint: RpcEndpoint) extends Logging
//Inbox中存放消息的链表,消息的抽象是InboxMessage,其包含很多子类如OnStart、OnStop消息等 @GuardedBy("this") protected val messages = new java.util.LinkedList[InboxMessage]() //是否允许多个线程同时消费Inbox中的消息,默认为false @GuardedBy("this") private var enableConcurrent = false //同时消费消息的线程数 @GuardedBy("this") private var numActiveThreads = 0
// 在实例化Inbox对象时,会先将OnStart消息放入messages链表,即OnStart消息是所有RpcEndpoint消费的第一个消息,在注册/启动RpcEndpoint时就会实例化一个EndpointData,其内部会实例化Inbox对象,在实例化Inbox对象时,会添加一条OnStart消息至Inbox的messages消息链表中,然后MessageLoop线程处理该消息时会调用该RpcEndpoint的start()方法启动本地的RpcEndpoint inbox.synchronized { messages.add(OnStart) }
2.2.6.2 Inbox类的重要方法
主要是process()和post()方法,process()方法用于处理Inbox中的messages链表中的消息,根据不同的消息类型有不同的消息处理方法,后续再来看这个process方法。还有一个post()方法,该方法主要是将消息加入到messages链表中,然后等待process方法来处理消息。
2.2.6.3 Inbox的消息源
Dispatcher中的MessageLoop线程轮询receivers阻塞队列,消费各个EndpointData中的Inbox中的消息,这些消息的来源有以下几个:
(1)registerRpcEndpoint:向RpcEnv中的Dispacher注册RpcEndpoint
该方法会向RpcEnv(某个RpcEndpoint运行环境)中的Dispatcher注册该RpcEndpoint,即将该RpcEndpoint添加到Dispatcher的相关集合中,注册Endpoint时会实例化一个对应的EndpointData对象,而每次实例化EndpointData时都会创建一个与之对应的Inbox,在Inbox中会将OnStart消息加入其messages链表,最后将EndpointData放入receivers阻塞队列,此时MessageLoop线程就会消费该消息,每个RpcEndpoint对应一个EndpointData对象,并维护起来在endpoints集合中,key为endpoint的名字,value为对应的EndpointData对象,当调用registerRpcEndpoint方法时,会先实例化该endpoint对应的EndpointData对象(内部会实例化一个Inbox对象,同时往Inbox中添加一条OnStart消息),后续发送消息时,会根据endpoint名字从endpoints集合中找到对应的EndpointData对象,然后往其中的Inbox中添加其他消息,所以一个endpoint对应一个EndpointData对象,也对应一个Inbox对象。
//name:RpcEndpoint的名字;endpoint:待注册的RpcEndpoint def registerRpcEndpoint(name: String, endpoint: RpcEndpoint): NettyRpcEndpointRef = { //实例化RpcEndpoint的地址,包括ip、port和name val addr = RpcEndpointAddress(nettyEnv.address, name) //实例化RpcEndpoint对应的Ref引用对象 val endpointRef = new NettyRpcEndpointRef(nettyEnv.conf, addr, nettyEnv) synchronized { if (stopped) { throw new IllegalStateException("RpcEnv has been stopped") } //实例化该RpcEndpoint对应的EndpointData对象,添加至Dispacher维护的endpoints映射中 if (endpoints.putIfAbsent(name, new EndpointData(name, endpoint, endpointRef)) != null){ throw new IllegalArgumentException(s"There is already an RpcEndpoint called $name") } //根据RpcEndpoint名获取对应的EndpointData对象,所以RpcEndpoint的名字需要是全局唯一的 val data = endpoints.get(name) //将该RpcEndpoint与其对应的ref添加至endpointRefs映射 endpointRefs.put(data.endpoint, data.ref) //将此EndPointData对象添加到receivers阻塞队列,该EndpointData中的Inbox中的messages消息链表中已有OnStart消息 receivers.offer(data) } endpointRef }
经过上述的流程,RpcEndpoint就已经向其RpcEnv中的Dispacher注册成功了。
(2)unregisterRpcEndpoint:将某个RpcEndpoint从其依赖的RpcEnv中的Dispatcher中移除
private def unregisterRpcEndpoint(name: String): Unit = { val data = endpoints.remove(name)//先从endpoints集合移除相应RpcEndpoint的EndpointData对象 if (data != null) { //这里调用Inbox的stop()方法往Inbox的messages链表中添加OnStop消息停止该RpcEndpoint data.inbox.stop() //最后将添加了OnStop消息的EndpointData添加到receivers阻塞队列中,这是对应RpcEndpoint的最后一个消息,此时endpoints中已经没有该RpcEndpoint的EndpointData对象了 receivers.offer(data) } //在OnStop的消息处理过程中清空endpointRefs映射,若在此处清空,则可能其他地方还用到了该RpcEndpoint的ref引用,还要往该RpcEndpoint发送消息,这样就会报错,而OnStop消息一定是RpcEndpoint处理的最后一个消息,所以在处理OnStop消息时清空endpointRefs映射 }
(3)postMessage:将RpcEndpoint接收到的消息添加到Inbox中
Dispatcher类中提供了很多将接收到的消息添加至RpcEndpoint对应的Inbox的方法,如postRpcMessage、postRemoteMessage等,但底层都是调用postMessage方法,不管是本地发往本地的消息,还是从远程RpcEndpoint接收到的消息都需要先调用postMessage将接收到的消息添加到Inbox中,然后才能进行消费:
private def postMessage( endpointName: String,//本地RpcEndpoint名 message: InboxMessage,//接收到的消息 callbackIfStopped: (Exception) => Unit): Unit = { val error = synchronized { //先从endpoints集合中根据RpcEndpoint名获取对应的EndpointData对象,内部维护了该RpcEndpoint的Inbox对象。endpoints集合维护了endpoint的名字与对应EndpointData的映射。所以这里是先找到对应endpoint的EndpointData对象,然后往该EndpointData的Inbox中添加其他消息 //endpoints映射中还有另外一个RpcEndpoint,专门用于验证真正的RpcEndpoint是否注册成功,两个RpcEndpoint的地址相同 val data = endpoints.get(endpointName) if (stopped) { Some(new RpcEnvStoppedException()) } else if (data == null) { Some(new SparkException(s"Could not find $endpointName.")) } else { //调用Inbox的post方法往EndpointData的Inbox中添加消息 data.inbox.post(message) //将添加消息后的EndpointData对象添加到receivers阻塞队列中等待消费消息,还是同一个EndpointData对象,只是往其中的Inbox中添加了消息 receivers.offer(data) None } } error.foreach(callbackIfStopped) }
根据上面的代码,处理接收到的消息的逻辑如下:
Step1:根据消息发往的RpcEndpoint的名字(这里即是本地RpcEndpoint名字)从endpoints集合获取相应的EndpointData对象,每个EndpointData对应一个RpcEndpoint端点;
Step2:从获得的EndpointData对象中拿到指定RpcEndpoint的Inbox对象;
Step3:往拿到的Inbox对象内的messages消息链表中添加要发送的消息;
Step4:将添加消息后的EndpointData对象添加到receivers阻塞队列中。
后续再通过MessageLoop轮询receivers阻塞队列中的EndpointData对象,然后调用Inbox的process方法处理Inbox的message链表中的消息。
(4)stop:停止Dispatcher
Dispatcher类中有一个stop()方法,当调用了此方法后,表示这个Dispatcher对象就停止了,这个方法会调用unregisterRpcEndpoint方法,将RpcEndpoint从Dispacher中移除并停止RpcEndpoint,RpcEndpoint停止后会向Dispacher的receivers阻塞队列中投递PoisonPill毒药(其实也是一个EndpointData对象,只是内部成员全都是null),毒药消息会使Dispatcher维护的线程池中的MessageLoop线程全部停止运行(这段逻辑可回到MessageLoop查看,MessageLoop线程一旦拿到毒药消息,会将毒药消息放回receivers阻塞队列,最后此MessageLoop线程停止),直至最后所有的线程都停止了,关闭线程池。
//调用Dispatcher的stop方法停止Dispatcher def stop(): Unit = { synchronized { if (stopped) { return } stopped = true } //对每个RpcEndpoint对应的EndpointData对象调用unregisterRpcEndpoint方法停止所有的endpoint,实际是往所有的RpcEndpoint发送了一个OnStop消息,某个RpcEndpoint对应的Dispacher中的endpoints映射中只有两个RpcEndpoint,一个是真正的RpcEndpoint,还有一个是用于验证之前那个RpcEndpoint是否注册成功的RpcEndpoint endpoints.keySet().asScala.foreach(unregisterRpcEndpoint) // 往receivers阻塞队列中添加一个毒药消息,用于停止Dispatcher中的线程池中的所有MessageLoop线程 receivers.offer(PoisonPill) //最后关闭线程池 threadpool.shutdown() }
某个RpcEndpoint对应的NettyRpcEnv中有一个outboxes字段,其维护了远端RpcAddress -> Outbox的映射,即每个远程RpcEndpoint对应一个Outbox。当本地RpcEndpoint需要向另外一个远程RpcEndpoint发送消息时,会调用NettyRpcEnv的postToOutbox方法将消息添加到远程RpcEndpoint的Outbox中,由Outbox自行通过TransportClient发送消息至远程RpcEndpoint上,其中TransportClient也是对于某个远程RpcEndpoint的,远程RpcEndpoint的TransportServer接收到消息后,就会调用Dispacher的postMessage方法将接收到的消息添加到Inbox中进行消费。
2.2.8.1 Outbox类中的重要成员
//nettyEnv:本地RpcEndpoint运行的RpcEnv环境 //address:对应这个Outbox远程端点的地址,即消息接收者远程RpcEndpoint的地址 private[netty] class Outbox(nettyEnv: NettyRpcEnv, val address: RpcAddress) { //存放发送到此Outbox对应远程RpcEndpoint的消息的消息链表 @GuardedBy("this") private val messages = new java.util.LinkedList[OutboxMessage] //Outbox中发送消息到远程RpcEndpoint需要的TransportClient对象 @GuardedBy("this") private var client: TransportClient = null //connectFuture表示该Outbox与对应远程RpcEndpoint是否正在连接,即是否正在创建远程RpcEndpoint的TransportClient对象 @GuardedBy("this") private var connectFuture: java.util.concurrent.Future[Unit] = null //表示该Outbox是否停止 @GuardedBy("this") private var stopped = false //表示是否有线程在清空该Outbox的messages消息链表内的所有消息,即是否有线程正在发送outbox中的消息,因为Outbox中的消息都是一次性发送到远端的,不能同时有多个线程发送消息 @GuardedBy("this") private var draining = false }
2.2.8.2 Outbox类中的方法
(1)send:发送消息到远程端点
//发送消息至远程RpcEndpoint,每次调用Outbox的send发送消息时内部都会将Outbox中当前所有的消息都发送到远程RpcEndpoint上 def send(message: OutboxMessage): Unit = { val dropped = synchronized { if (stopped) { true } else { messages.add(message)//先将要发送的消息添加到messages消息链表中 false } } if (dropped) { message.onFailure(new SparkException("Message is dropped because Outbox is stopped")) } else { //将messages链表中的所有消息发送到远程RpcEndpoint,每次调用drainOutbox()时都会将messages链表中当前的所有消息发送给对应的远程RpcEndpoint,反过来即每次调用Outbox的send()方法时就会将消息链表中的所有消息发送给远程RpcEndpoint drainOutbox() } }
(2)drainOutbox():清空消息链表,即将消息链表中的所有消息都发送给远程RpcEndpoint
该方法主要是清空Oubox中的消息,若当前已经有其它线程在清空消息链表了,则该清空线程就退出。若当前Outbox没有建立与远程RpcEndpoint的连接,即Outbox的TransportClient成员为null,则该Outbox先与远程RpcEndpoint建立连接(即先根据消息接收者的地址,实例化一个TransportClient对象),然后再发送消息。
if (client == null) { // launchConnectTask方法中在创建了与远程RpcEndpoint的连接后,会立即调用drainOutbox()方法发送消息到远程RpcEndpoint,且创建连接是在子线程中做的,所以主线程在这里就return了,即在子线程中发送消息至远程RpcEndpoint launchConnectTask() return } // 如果本地端点已经拥有远程消息接收者的TransportClient对象(即之前已经发送过消息给这个远程RpcEndpoint,Outbox会维护对应远程RpcEndpoint的TransportClient对象),则直接从Outbox的messages消息链表中拉取一条消息发送到远程端点 message = messages.poll()//从消息链表中拉取一条消息 if (message == null) { return }
//这是一个死循环,只有当该Outbox停止或messages消息链表中的消息全部都发送给远程RpcEndpoint时才会退出 while (true) { try { val _client = synchronized { client } if (_client != null) { message.sendWith(_client)//调用OutboxMessage的sendWith()方法发送消息至远程RpcEndpoint,实际发送逻辑就是Netty里面发送消息的那套 } else { assert(stopped == true) } } catch { case NonFatal(e) => handleNetworkFailure(e) return } synchronized { if (stopped) { return } //发送完一条消息后,会再从messages消息链表中获取后面的消息,然后再次发送出去,直至messages链表中没有剩余消息才退出 message = messages.poll() if (message == null) { draining = false return } } }
(3)launchConnectTask():创建Outbox与对应远程RpcEndpoint之间的连接,只需建立一次连接即可,建立成功后该Outbox就会将该TransportClient维护起来,若在发送消息时,该Outbox还未与远程RpcEndpoint创建连接,则调用该方法建立连接,返回TransportClient对象进行保存,后续该Outbox发送消息时都用这个TransportClient对象,因为一个Outbox对应远端的一个RpcEndpoint,所以连接信息是不会变的。
private def launchConnectTask(): Unit = { //nettyEnv中的clientConnectionExecutor是线程池,所以是在线程池中非阻塞地创建与远程RpcEndpoint的连接,创建TransportClient成功后,也是在线程池中的子线程中发送消息的,所以主线程——Outbox.send()可以直接返回 connectFuture = nettyEnv.clientConnectionExecutor.submit(new Callable[Unit] { override def call(): Unit = { try { //通过TransportClientFactory创建与远程RpcEndpoint的TransportClient连接,address是消息接收者的地址 val _client = nettyEnv.createClient(address) outbox.synchronized { client = _client//保存创建成功的TransportClient对象,方便后续使用 if (stopped) { closeClient() } } } catch { ...... } outbox.synchronized { connectFuture = null } // 创建完连接后,由于当前messages消息链表中可能有未发送的消息,所以这里需要调用drainOutbox()方法将messages链表中的消息发送到远程端点,否则需要等到下一条消息过来才能将之前的消息一同发送过去 drainOutbox() } }) }
(4)stop():停止Outbox
def stop(): Unit = { synchronized { if (stopped) { return } stopped = true if (connectFuture != null) { connectFuture.cancel(true) } //关闭TransportClient closeClient() } //将messages消息链表中剩余未发送的消息标记failure返回 var message = messages.poll() while (message != null) { message.onFailure(new SparkException("Message is dropped because Outbox is stopped")) message = messages.poll() } }
2.2.8.3 本地RpcEndpoint向远程RpcEndpoint发送消息
调用的是NettyRpcEnv的postToOutbox()方法发送消息至远程RpcEndpoint。
//receiver:远程消息接收者的ref引用 private def postToOutbox(receiver: NettyRpcEndpointRef, message: OutboxMessage): Unit = { if (receiver.client != null) { //若NettyRpcEndpointRef对象中的TransportClient已经实例化了,则直接调用message的sendWith方法发送消息至远程端点即可 message.sendWith(receiver.client) } else { require(receiver.address != null, "Cannot send message to client endpoint with no listen address.") //根据要发送消息的远程RpcEndpoint地址,获取相应的Outbox val targetOutbox = { val outbox = outboxes.get(receiver.address) //若NettyRpcEnv的outboxes集合中还没有对应远程RpcEndpoint的Outbox,则实例化一个Outbox对象,放入NettyRpcEnv的outboxes集合中,若outboxes集合总已经有了对应的Outbox了,则直接返回这个Outbox if (outbox == null) { val newOutbox = new Outbox(this, receiver.address) val oldOutbox = outboxes.putIfAbsent(receiver.address, newOutbox) if (oldOutbox == null) { newOutbox } else { oldOutbox } } else { outbox } } if (stopped.get) { // It's possible that we put `targetOutbox` after stopping. So we need to clean it. outboxes.remove(receiver.address) targetOutbox.stop() } else { //拿到相应的Outbox后,就调用Outbox的send方法发送消息,send方法执行逻辑上面分析过,先将消息添加到messages链表中,然后发送到远程端点 targetOutbox.send(message) } } }
总体流程如下所述:
Step1:发送消息时,调用消息接收者引用NettyRpcEndpointRef的send和ask方法;
Step2:ref中的send和ask底层调用的是NettyRpcEnv中的send和ask方法(具体调用的是哪个RpcEndpoint依赖的RpcEnv则需要看当前执行的是哪个RpcEndpoint的逻辑,若消息接收者的地址与当前RpcEndpoint依赖的RpcEnv地址相同,则说明是发往本地的消息,否则就是发往远程RpcEndpoint的消息);
Step3:NettyRpcEnv中的send和ask方法中会调用postToOutbox方法实例化要发送的消息到远程RpcEndpoint对应的Outbox对象,并将该Outbox添加到本地RpcEndpoint依赖的NettyRpcEnv的outboxes集合中进行维护;
Step4:调用Outbox的send方法,将待发送的OutboxMessage添加到Outbox的messages消息链表中,然后调用Outbox的drainOutbox方法,将messages消息链表中当前的所有消息发送到指定的远程RpcEndpoint上进行处理。
启动流程分析:
Step1:不同类型的RpcEndpoint启动时,先会创建一个RpcEnvFactory(NettyRpcEnvFactory),然后通过这个RpcEnvFactory创建相应的RpcEnv(NettyRpcEnv),即RpcEndpoint运行需要的rpc环境;
Step2:在创建RpcEnv的过程中同时会实例化一个TransportServer对象用于接收其它远程RpcEndpoint发送到此RpcEndpoint上的消息;
Step3:在创建RpcEnv的过程中同时会实例化一个Dispatcher对象,表示该RpcEndpoint的消息分发器,调用NettyRpcEnv的setupEndpoint(注册本地端点)时会实例化一个对应的EndpointData对象,内部会实例化一个Inbox对象,表示本地端点的收件箱,并往Inbox的消息链表中添加OnStart消息;
Step4:将EndpointData添加到Dispatcher的receivers阻塞队列中,然后Dispatcher内部维护了一个线程池,该线程池中的线程(MessageLoop)会不断从receivers阻塞队列中拉取EndpointData对象,并获取其中的Inbox对象,最后调用Inbox的process方法处理之前添加的OnStart消息,接收到消息后具体的RpcEndpoint子类会调用其onStart方法进行启动。
流程分析:
Step1:RpcEndpoint调用send/ask方法发送消息,内部调用其引用NettyRpcEndpointRef的send/ask方法,最终调用的是NettyRpcEnv中的send/ask方法;
Step2:根据要发送消息对象中封装的RpcAddress对象判断该消息是发往本地的endpoint还是远程的endpoint;
Step3:若消息发往本地的endpoint,则调用postOneWayMessage方法,底层最终调用postMessage,该方法会从Dispatcher中维护的EndpointData集合中找到对应的EndpointData对象,然后将要发送的消息添加到这个EndpointData对应Inbox的messages消息链表中,并将这个添加消息后的EndpointData对象添加到receivers阻塞队列中,由于Dispatcher中的MessageLoop线程在不断轮询receivers阻塞队列,队列中一有消息,就会被消费;
Step4:若消息发往远程RpcEndpoint,则会调用NettyRpcEnv的postToOutbox方法,先根据消息发往的远程endpoint地址找到对应的Outbox,若没有对应的Outbox则实例化一个,然后将要发送的消息添加到这个Outbox的messages消息链表中,然后会调用drainOutbox方法将当前messages链表中的消息全部发往远程RpcEndpoint。