Spark消息通信

二、Spark通信机制

2.1 Spark通信机制的重要概念

Spark消息通信_第1张图片

(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发件箱中的消息发送出去。

2.2 Spark通信相关类解析

2.2.1 RpcEndpoint

在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的继承体系如下图:

img

由上图可知,Master和Worker等都是一个RpcEndpoint,ClientEndpoint是每个SparkApp的终端点——即Spark应用对应的RpcEndpoint,DriverEndpoint是Spark Driver的endpoint,HeartbeatReceiver是Executor发送心跳消息给Driver的Endpoint,CoarseGrainedExecutorBackend是Spark Executor的endpoint,用于执行Executor的相关操作。上述这些RpcEndpoint都继承自ThreadSafeRpcEndpoint,即均是线程安全的。

2.2.2 RpcEndpointRef

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发送消息的

2.2.3 RpcEnv

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中

2.2.4 RpcEnv/RpcEndpoint以及RpcEndpointRef之间的关系

Spark消息通信_第2张图片

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引用对象

2.2.5 Dispatcher消息分发器

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。

2.2.6 Inbox收件箱

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

2.2.7 Dispatcher和Inbox的请求流程

Spark消息通信_第3张图片

2.2.8 Outbox

某个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)
    }
  }
}

2.2.9 Outbox和TransportClient消息发送请求流程

Spark消息通信_第4张图片

总体流程如下所述:

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上进行处理。

2.2.10 RpcEndpoint启动时序图

Spark消息通信_第5张图片

启动流程分析:

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方法进行启动。

2.2.11 RpcEndpoint的send和ask发送消息时序图

Spark消息通信_第6张图片

流程分析:

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。

2.2.12 RpcEndpoint receive接收消息时序图

Spark消息通信_第7张图片

你可能感兴趣的:(大数据,spark)