Bootstrap

Spark-SparkEnv简析之一

SparkEnv 概述

私有方法 create 用于创建 SparkEnv,在这个方法中涉及很多 SparkEnv 内部组件的实例化过程

SparkEnv的组成:

  • SecurityManger
  • RpcEnv
  • serializerManager
  • BroadcastManager
  • mapOutputTracker
  • closureSerializer
  • MemoryManger
  • BlockTransferService
  • BlockTransferMaster
  • MetricsSystem
  • ShuffleManager
  • BlockManager
  • outputeCommitCoordinator

SecurityManger 简析

SecurityManager 主要对账号、权限及身份认证进行设置和管理,SecurityManager 还会给当前系统设置默认的口令认证实例

  • 部署模式为YARN时,需要生成 secretkey (密钥)并存入 HadoopUGI。
  • 其他模式时,需要设置环境变量 _SPARK_AUTHL_SBCRET(优先级更高) 或 spark.authenticate.secret 属性指定 secretkey(密钥)
// 创建 SecurityManager 的代码
val securityManager = new SecurityManager(conf, ioEncryptionKey)
属性
  • authOn : 是否开启认证。可以通过 spark.authenticate 属性配置,默认为 false
  • aclsOn : 是否对账号进行授权检查。可通过 spark.acls.enable(优先级较高)或 spark.ui.acls.enable(此属性是为了向前兼容)属性进行配置。 aclsOn的默认值为 false
  • adminAcls : 管理员账号集合。可以通过 spark.admin.acls 属性配置,默认为空
  • adminAclsGroups : 管理员账号所在组的集合。可以通过 spark.admin.acls.groups 属性配置,默认为空
  • viewAcis : 有查看权限的账号的集合。包括 adminAcls、defaultAclUsers 及 spark.ui.view.acls属性配置的用户
  • viewAclsGroups : 拥有查看权限的账号,所在组的集合。包括 adminAclsGroups 和 spark.ui.view.acls.groups 属性配置的用户
  • modifyAcls : 有修改权限的账号的集合。包括 adminAcls、defaultAclUsers及 spark.modify.acls属性配置的用户
  • modifyAclsGroups : 拥有修改权限的账号所在组的集合。包括 adminAclsGroups 和 spark.modify.acls.groups 属性配置的用户
  • defaultAcIUsers : 默认用户。包括系统属性 user.name 指定的用户或系统登录用户或者通过系统环境变量 SPARK_USER 进行设置的用户
  • secretKey : 密钥
    • 在YARN模式下,首先使用 sparkCookie 从 HadoopUGI 中获取密钥。如果HadoopUGI没有保存密钥,则生成新的密钥(密钥长度可以通过 spark.authenticate.secretBitLength 属性指定)并存入HadoopUGI
    • 其他模式下,则需要设置环境变量 _SPARK_AUTH_SECRET(优先级更高) 或 spark.authenticate.secret 属性指定

因Spark的节点间通信往往需要动态协商用户名、密码,SecurityManager 中设置了默认的口令认证实例 Authenticator,此实例采用匿名内部类实现,用于每次使用 HTTPclient 从 HTTP 服务器获取用户的用户名和密码

if (authOn) {
   // 在这里设置默认的口令认证实例 Authenticator,它的 getPasswordAuthentication 方法用于获取用户名、密码
    Authenticator.setDefault(
      new Authenticator() {
        override def getPasswordAuthentication(): PasswordAuthentication = {
          var passAuth: PasswordAuthentication = null
          val userInfo = getRequestingURL().getUserInfo()
          if (userInfo != null) {
            val  parts = userInfo.split(":", 2)
            passAuth = new PasswordAuthentication(parts(0), parts(1).toCharArray())
          }
          return passAuth
        }
      }
    )
  }

RpcEnv 简析

    // 生成系统名称 systemName,如果应用为 Driver(即 SparkEnv 位于 Driver 中),那么该变量就是 sparkDriver,否则为 SparkExecutor
    val systemName = if (isDriver) driverSystemName else executorSystemName
    // 调用 create 方法创建 RpcEnv
    val rpcEnv = RpcEnv.create(systemName, bindAddress, advertiseAddress, port.getOrElse(-1), conf,
      securityManager, numUsableCores, !isDriver)
  def create(
      name: String,
      bindAddress: String,
      advertiseAddress: String,
      port: Int,
      conf: SparkConf,
      securityManager: SecurityManager,
      numUsableCores: Int,
      clientMode: Boolean): RpcEnv = {
    // 构建 config,用于保存 RpcEnv 的配置信息
    val config = RpcEnvConfig(conf, name, bindAddress, advertiseAddress, port, securityManager,
      numUsableCores, clientMode)
    // 实际创建 RpcEnv 的动作发生在这里
    new NettyRpcEnvFactory().create(config)
  }
  def create(config: RpcEnvConfig): RpcEnv = {
    val sparkConf = 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
    // 创建 javaSerializerInstance ,此实例用于 RPC 传输对象的序列化
    val javaSerializerInstance =
      new JavaSerializer(sparkConf).newInstance().asInstanceOf[JavaSerializerInstance]
    // 创建 NettyRpcEnv,对 NettyRpcEnv 的创建其实是对内部各个子组件 TransportConf、Dispatcher、TransportContext、TransportClientFactory、TransportServer 的实例化过程
    val nettyEnv =
      new NettyRpcEnv(sparkConf, javaSerializerInstance, config.advertiseAddress,
        config.securityManager, config.numUsableCores)
    if (!config.clientMode) {
      // 这一步首先定义了一个偏函数 startNettyRpcEnv,其函数实际为执行 NettyRpcEnv 的 startServer 方法,最后在启动NettyRpcBnv 之后返回 NettyRpcBnv 及服务最终使用的端口
      val startNettyRpcEnv: Int => (NettyRpcEnv, Int) = { actualPort =>
        nettyEnv.startServer(config.bindAddress, actualPort)
        (nettyEnv, nettyEnv.address.port)
      }
      try {
        // 这里使用了 Utils 的 startServiceOnPort 方法,startServiceOnPort 实际上是调用了作为参数的偏函效startNettyRpcEnv
        Utils.startServiceOnPort(config.port, startNettyRpcEnv, sparkConf, config.name)._1
      } catch {
        case NonFatal(e) =>
          nettyEnv.shutdown()
          throw e
      }
    }
    nettyEnv
  }
RPC 端点 RpcEndpoint 简析

RPC端点是对 Spark 的 RPC 通信实体的统一抽象,所有运行于 RPC 框架之上的实体都应该继承 RpcEndpoint,RpcEndpoint 是对能够处理 RPC 请求,给某一特定服务提供本地调用及节点调用的 RPC 组件的抽象

private[spark] trait RpcEnvFactory {
  def create(config: RpcEnvConfig): RpcEnv
}

private[spark] trait RpcEndpoint {

  val rpcEnv: RpcEnv

  final def self: RpcEndpointRef = {
    require(rpcEnv != null, "rpcEnv has not been initialized")
    rpcEnv.endpointRef(this)
  }

  def receive: PartialFunction[Any, Unit] = {
    case _ => throw new SparkException(self + " does not implement 'receive'")
  }

  def receiveAndReply(context: RpcCallContext): PartialFunction[Any, Unit] = {
    case _ => context.sendFailure(new SparkException(self + " won't reply anything"))
  }

  def onError(cause: Throwable): Unit = {
    throw cause
  }

  def onConnected(remoteAddress: RpcAddress): Unit = {}

  def onDisconnected(remoteAddress: RpcAddress): Unit = {}

  def onNetworkError(cause: Throwable, remoteAddress: RpcAddress): Unit = {}

  def onStart(): Unit = {}

  def onStop(): Unit = {}

  final def stop(): Unit = {
    val _self = self
    if (_self != null) {
      rpcEnv.stop(_self)
    }
  }
}
private[spark] trait ThreadSafeRpcEndpoint extends RpcEndpoint

由上代码可以对 RpcEndpoint 的各个组成进行分析

  • rpcEnv : 当前 RpcEndpoint 所属的 RpcEnv
  • self : 获取 RpcEndpoint 相关联的 RpcEndpointRef 。从代码实现看到,其实现实际调用了 RpcEnv 的endpointRef 方法。由于RpcEnv并未实现此方法,所以需要RpcEnv的子类来实现
  • receive : 接收消息并处理,但不需要给客户端回复
  • receiveAndReply : 接收消息并处理,需要给客户端回复。回复是通过 RpcCallContext 来实现的
  • onError : 当处理消息发生异常时调用,可以对异常进行一些处理
  • onConnected : 当客户端与当前节点连接上之后调用,可以针对连接进行一些处理
  • onDisconnected : 当客户端与当前节点的连接断开之后调用,可以针对断开连接进行一些处理
  • onNetworkError : 当客户端与当前节点之间的连接发生网络错误时调用,可以针对连接发生的网络错误进行一些处理
  • onStart : 在 RpcEndpoint 开始处理消息之前调用,可以在 RpcEndpoint 正式工作之前做一些准备工作
  • onStop : 在停止 RpcEndpoint 时调用,可以在 RpcEndpoint 停止的时候做一些收尾工作
  • stop:用于停止当前 RpcEndpoint。从代码实现看到,其实现实际调用了 RpcEnv 的 stop 方法。由于RpcEnv并未实现此方法,所以需要其子类来实现
RPC 端点引用 RpcEndpointRef简析

在 Spark 2.x 中 RpcEndpoint 替代了 Akka 中 Actor,对应的 RpcEndpointRef 替代了 ActorRef,在 Akka 中只要持有了一个 Actor 的引用 ActorRef,那么就可以使用此 ActorRef 向远端 Actor 发起请求,RpcEndpointRef 也具有同等的效用,如果向一个远端的 RpcEndpoint 发起请求,就必须持有这个 RpcEndpoint 的 RpcEndpointRef,如下图可以看到 RpcEndpoint 和 RpcEndpointRef 之间的关系

在这里插入图片描述

消息投递规则简析

在理解 RpcEndpointRef 之前,需要先对消息投递规则进行理解

消息投递一般分为下面三种情况

  1. at-most-once : 意味着每条应用了这种机制的消息会被投递0次或1次。可以说这条消息可能会丢失
  2. at-least-once : 意味着每条应用了这种机制的消息潜在地存在多次投递尝试并保证至少会成功一次。就是说这条消息可能会重复但是不会丢失
  3. exactly-once : 意味着每条应用了这种机制的消息只会向接收者准确地发送一次。换言之,这种消息既不会丢失,也不会重复

上述三种情况的优缺点分别是:

  • at-most-once 的成本最低且性能最高,因为它在发送完消息后不会尝试去记录任何状态,然后这条消息将被抛之脑后
  • at-least-once 需要发送者必须认识它所发送过的消息,并对没有收到回复的消息进行发送重试。这就要求接收者对消息的处理必须是幂等的,否则可能会带来新的问题
  • exactly-once 的成本是三者中最高,性能却又是三者中最差的。它除了要求发送者有记忆和重试能力,还要求接收者能够认识接收过的消息并能过滤出那些重复的消息投递
RpcEndpointRef 的定义简析
private[spark] abstract class RpcEndpointRef(conf: SparkConf)
  extends Serializable with Logging {
	// RPC 最大重新连接次数,可以使用 spark.rpc.numRetries 属性进行配置,默认为3次
  private[this] val maxRetries = RpcUtils.numRetries(conf)
  // RPC 每次重新连接需要等待的毫秒数,可以使用 spark.rpc.retry.wait 属性进行配置,默认值为 3 秒
  private[this] val retryWaitMs = RpcUtils.retryWaitMs(conf)
  // RPC 的 ask 操作的默认超时时间。可以使用 spark.rpc.askTimeout 或者 spark.network.timeout 属性进行配置,默认值为120秒。前者的优先级更高
  private[this] val defaultAskTimeout = RpcUtils.askRpcTimeout(conf)
  // 返回当前 RpcEndpointRef 对应 RpcEndpoint 的RPC地址
  def address: RpcAddress
  // 返回当前 RpcEndpointRef 对应 RpcEndpoint 的名称
  def name: String
  // 发送单向异步的消息,所谓“单向”就是发送完后就会忘记此次发送,不会有任何状态要记录,也不会期望得到服务端的回复。send 采用了 at-most-once 的投递规则。RpcEndpointRef 的 send 方法非常类似于 Akka 中 Actor 的 tell 方法
  def send(message: Any): Unit
  
  def ask[T: ClassTag](message: Any, timeout: RpcTimeout): Future[T]
  // 以默认的超时时间作为 timeout 参数,调用 ask[T:ClassTagl(message:Any,timcout:RpcTimeout) 方法
  def ask[T: ClassTag](message: Any): Future[T] = ask(message, defaultAskTimeout)
  // 发送同步的请求,此类请求将会被 RpcEndpoint 接收,并在指定的超时时间内等待返回类型为 T 的处理结果。当此方法抛出SparkException 时,将会进行请求重试,直到超过了默认的重试次数为止。由于此类方法会重试,因此要求服务端对消息的处理是幂等的。此方法也来用了 at-least-once 的投递规则。此方法也非常类似于 Akka 中采用了 at-least-once 机制的 Actor 的 ask 方法
  def askSync[T: ClassTag](message: Any): T = askSync(message, defaultAskTimeout)

  def askSync[T: ClassTag](message: Any, timeout: RpcTimeout): T = {
    val future = ask[T](message, timeout)
    timeout.awaitResult(future)
  }

}
TransportConf 简析

TransportConf 是 RPC 框架中的配置类,由于 RPC 环境 RpcEnv 的底层也依赖于数据总线,因此需要创建传输上下文 TransportConf。创建 TransportConf 是构造 NettyRpcEnv 的过程中的第一步

  private[netty] val transportConf = SparkTransportConf.fromSparkConf(
    conf.clone.set("spark.rpc.io.numConnectionsPerPeer", "1"),
    "rpc",
    conf.getInt("spark.rpc.io.threads", numUsableCores))

这里首先对SparkConf进行了克隆,然后设置了 spark.rpc.io.numConnectionsPerPeer 限定了 module名为 rpc,通过 spark.rpc.io.threads 设置了 Netty 传输线程数,之后调用了 SparkTransportConf 的 fromSparkConf 方法

Dispatcher 消息调度器简析

创建消息调度器 Dispatcher 是有效提高 NettyRpcEnv 对消息异步处理并最大提升并行处理能力的前提

Dispatcher 负责将 RPC 消息路由到要该对此消息处理的 RpcEndpoint(RPC端点)

private val dispatcher: Dispatcher = new Dispatcher(this, numUsableCores)
Dispatcher 的基本概念
  • RpcEndpoint : RPC端点,即 RPC 分布式环境中一个具体的实例,其可以对指定的消息进行处理。由于RpcEndpoint是一个特质,所以需要提供RpcEndpoint的实现类
  • RpcEndpointRef : RPC端点引用,即RPC分布式环境中一个具体实体的引用,所谓引用实际是“spark://host;port/name”这种格式的地址。
    • host 为端点所在 RPC 服务所在的主机IP
    • port 是端点所在 RPC 服务的端口
    • name是端点实例的名称
  • InboxMessage : Inbox盒子内的消息。InboxMessage是一个特质,所有类型的RPC消息都继承自InboxMessage,IndboxMessage的一些实现类:
    • OneWayMessage : RpcEndpoint 处理此类型的消息后不需要向客户端回复信息
    • RpcMessage : RPC消息,RpcEndpoint 处理完此消息后需要向客户端回复信息
    • OnStart : 用于 Inbox 实例化后,再通知与此 Inbox 相关联的 RpcEndpoint 启动
    • OnStop : 用于Inbox停止后,通知与此 Inbox 相关联的 RpcEndpoint 停止
    • RemoteProcessConnected : 此消息用于告诉所有的 RpcEndpoint,有远端的进程已经与当前RPC服务建立了连接
    • RemoteProcessDisconnected : 此消息用于告诉所有的 RpcEndpoint,有远端的进程已经与当前RPC服务断开了连接
    • RemoteProcessConnectionError:此消息用于告诉所有的RpcEndpoint,与远端某个地址之间的连接发生了错误
  • Inbox : 端点内的盒子,每个 RpcEndpoint 都有一个对应的盒子,这个盒子里有个存储 InboxMessage 消息的列表 messages。所有的消息将缓存在 messages 列表里面,并由 RpcEndpoint 异步处理这些消息
  • EndpointData : RPC端点数据,它包括了 RpcEndpoint、NettyRpcEndpointRef 及 Inbox 等属于同一个端点的实例。Inbox与RpcEndpoint、NettyRpcEndpointRef 通过此 EndpointData 相关联

Dispatcher 的成员变量如下

  • endpoints : 端点实例名称与端点数据 EndpointData 之间映射关系的缓存。有了这个缓存,就可以使用端点名称从中快速获取或删除EndpointData 了
  • endpointRefs : 端点实例 RpcEndpoint 与端点实例引用 RpcEndpointRef 之间映射关系的缓存。有了这个缓存,就可以使用端点实例从中快速获取或删除端点实例引用了
  • receivers : 存储端点数据 EndpointData 的阻塞队列。只有 Inbox 中有消息的 EndpointData 才会被放人此阻塞队列
  • stopped:Dispatcher是否停止的状态。
  • threadpol : 用于对消息进行调度的线程池。此线程池运行的任务都是 MessageLoop

以上各种成员变量和概念的组织关系如下图

Dispatcher的内存模型

Dispatcher 的调度原理
  private val threadpool: ThreadPoolExecutor = {
    // 可用处理器数量为 spark.rpc.io.threads 和 系统可用的比较
    val availableCores =
      if (numUsableCores > 0) numUsableCores else Runtime.getRuntime.availableProcessors()
    // 获取此线程池的大小 numThreads,此线程池的大小默认为 2 与当前系统可用处理器数量之间的最大值,也可以使用 spark.rpc.netty.dispatcher.numThreads 直接进行配置
    val numThreads = nettyEnv.conf.getInt("spark.rpc.netty.dispatcher.numThreads",
      math.max(2, availableCores))
    // 创建线程池,此线程池是固定大小的线程池,并且启动的线程都为后台线程执行,且线程名以 dispatcher-event-loop 为前缀
    val pool = ThreadUtils.newDaemonFixedThreadPool(numThreads, "dispatcher-event-loop")
	  // 启动多个运行 MessageLoop 任务的线程
    for (i <- 0 until numThreads) {
      pool.execute(new MessageLoop)
    }
    pool
  }

在SparkUI中可以看到这个线程,在 Thread Dump 中查看

在这里插入图片描述

  private class MessageLoop extends Runnable {
    override def run(): Unit = {
      try {
        // 一直循环执行,除非出错
        while (true) {
          try {
            // 从 receivers 中获取 EndpointData。receivers 中的 EndpointData,其 Inbox 的 messages 列表中肯定有了新的消息。换言之,只有 Inbox 的 messages 列表中有了新的消息,此 EndpointData 才会被放入 receivers 中。由于receivers是个阻塞队列,所以当 receivers 中没有 EndpointData 时,MessageLoop 线程会被阻塞
            val data = receivers.take()
            if (data == PoisonPill) {
              // 如果取到的EndpointData是“毒药”(PoisonPill),那么此 MessageLoop 线程将退出(通过return语句)。这里有个动作就是将 PoisonPill 重新放入到 receivers 中,这是因为 threadpool 线程池极有可能不止一个 MessageLoop 线程,为了让大家都“毒发身亡”,还需要把“毒药”放回到 receivers 中,这样其他“活着”的线程就会再次误食“毒药”,达到所有 MessageLoop 线程都结束的效果
              receivers.offer(PoisonPill)
              return
            }
            // 如果取到的 EndpointData 不是“毒药”,那么调用 EndpointData 中 Inbox 的 process 方法对消息进行处理
            data.inbox.process(Dispatcher.this)
          } catch {
            case NonFatal(e) => logError(e.getMessage, e)
          }
        }
      } catch {
        case _: InterruptedException => // exit
        case t: Throwable =>
          try {
            threadpool.execute(new MessageLoop)
          } finally {
            throw t
          }
      }
    }
  }

由上面代码可以看出,MessageLoop 任务实际是将消息交给了 EndpointData 中 Inbox 的 process 方法处理

Inbox 中的成员属性简析如下

  • messages : 消息列表。用于缓存需要由对应 RpcEndpoint 处理的消息,即与 Inbox 在同一 EndpointData 中的 RpcEndpoint
  • stopped : Inbox的停止状态。
  • enableConcurrent : 是否允许多个线程同时处理 messages 中的消息。
  • numActiveThreads : 激活线程的数量,即正在处理 messages 中消息的线程数量
// 该方法中用到了 synchronized 锁,是因为 messages 是普通的 LinkedList,本身不是线程安全的,所以为了增加并发安全性,需要通过同步保护
// protected val messages = new java.util.LinkedList[InboxMessage]()
def process(dispatcher: Dispatcher): Unit = {
    var message: InboxMessage = null
    inbox.synchronized {
      // 进行线程并发检查。具体是,如果不允许多个线程同时处理 messages 中的消息( enableConcurrent 为false),并且当前激活线程数(numActiveThreads)不为0,这说明已经有线程在处理消息,所以当前线程不允许再去处理消息(使用 return 返回)
      if (!enableConcurrent && numActiveThreads != 0) {
        return
      }
      // 从messages中获取消息
      message = messages.poll()
      // 如果有消息未处理,则当前线程需要处理此消息,因而算是一个新的激活线程(需要将 numActiveThreads 加1)。如果 messages中没有消息了(一般发生在多线程情况下),则直接返回
      if (message != null) {
        numActiveThreads += 1
      } else {
        return
      }
    }
    while (true) {
      // 根据消息类型进行匹配,并执行对应的逻辑。这里有个小技巧值得借鉴,那就是匹配执行的过程中也许会发生错误,当发生错误的时候,我们希望当前 Inbox 所对应 RpcEndpoint 的错误处理方法 onError 可以接收到这些错误信息。Inbox 的 safelyCall 方法给我们提供了这方面的实现,可见下一个代码块
      safelyCall(endpoint) {
        message match {
          case RpcMessage(_sender, content, context) =>
            try {
              endpoint.receiveAndReply(context).applyOrElse[Any, Unit](content, { msg =>
                throw new SparkException(s"Unsupported message $message from ${_sender}")
              })
            } catch {
              case e: Throwable =>
                context.sendFailure(e)
                // Throw the exception -- this exception will be caught by the safelyCall function.
                // The endpoint's onError function will be called.
                throw e
            }

          case OneWayMessage(_sender, content) =>
            endpoint.receive.applyOrElse[Any, Unit](content, { msg =>
              throw new SparkException(s"Unsupported message $message from ${_sender}")
            })

          case OnStart =>
            endpoint.onStart()
            if (!endpoint.isInstanceOf[ThreadSafeRpcEndpoint]) {
              inbox.synchronized {
                if (!stopped) {
                  enableConcurrent = true
                }
              }
            }

          case OnStop =>
            val activeThreads = inbox.synchronized { inbox.numActiveThreads }
            assert(activeThreads == 1,
              s"There should be only a single active thread but found $activeThreads threads.")
            dispatcher.removeRpcEndpointRef(endpoint)
            endpoint.onStop()
            assert(isEmpty, "OnStop should be the last message")

          case RemoteProcessConnected(remoteAddress) =>
            endpoint.onConnected(remoteAddress)

          case RemoteProcessDisconnected(remoteAddress) =>
            endpoint.onDisconnected(remoteAddress)

          case RemoteProcessConnectionError(cause, remoteAddress) =>
            endpoint.onNetworkError(cause, remoteAddress)
        }
      }

      // 对激活线程数量进行控制。当对消息处理完毕后,当前线程作为之前已经激活的线程是否还有存在的必要呢 ?
      // 这里有两个判断 : 如果不允许多个线程同时处理 messages 中的消息并且当前激活的线程数多于1个,那么需要当前线程退出并numActiveThreads减1;如果 messages 已经没有消息要处理了,这说明当前线程无论如何也该返回并将 numActiveThreads 减1
      inbox.synchronized {
        // "enableConcurrent" will be set to false after `onStop` is called, so we should check it
        // every time.
        if (!enableConcurrent && numActiveThreads != 1) {
          // If we are not the only one worker, exit
          numActiveThreads -= 1
          return
        }
        message = messages.poll()
        if (message == null) {
          numActiveThreads -= 1
          return
        }
      }
    }
  }
  private def safelyCall(endpoint: RpcEndpoint)(action: => Unit): Unit = {
    def dealWithFatalError(fatal: Throwable): Unit = {
      inbox.synchronized {
        assert(numActiveThreads > 0, "The number of active threads should be positive.")
        // Should reduce the number of active threads before throw the error.
        numActiveThreads -= 1
      }
      logError(s"An error happened while processing message in the inbox for $endpointRef", fatal)
      throw fatal
    }

    try action catch {
      case NonFatal(e) =>
        try endpoint.onError(e) catch {
          case NonFatal(ee) =>
            if (stopped) {
              logDebug("Ignoring error", ee)
            } else {
              logError("Ignoring error", ee)
            }
          case fatal: Throwable =>
            dealWithFatalError(fatal)
        }
      case fatal: Throwable =>
        dealWithFatalError(fatal)
    }
  }
Index 的消息来源

MessageLoop 线程的执行逻辑是不断的消费各个 EndpointData 中 Index里面的消息,下面就对这一流程进行简析

  • 注册 RpcEndpoint

Dispatcher 的 registerRpcEndpoint 方法用于注册 RpcEndpoint,这个方法的副作用便可以将 EndpointData 放入 receivers

  def registerRpcEndpoint(name: String, endpoint: RpcEndpoint): NettyRpcEndpointRef = {
    // 使用当前 RpcEndpoint 所在 NettyRpcEnv 的地址和 RpcEndpoint 的名称创建 RpcEndpointAddress 对象
    val addr = RpcEndpointAddress(nettyEnv.address, name)
    // 创建 RpcEndpoint 的引用对象 NettyRpcEndpointRef
    val endpointRef = new NettyRpcEndpointRef(nettyEnv.conf, addr, nettyEnv)
    synchronized {
      if (stopped) {
        throw new IllegalStateException("RpcEnv has been stopped")
      }
      // 创建 EndpointData,并放入 endpoints 缓存
      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)
      // 将 RpcEndpoint 与 NettyRpcEndpointRef 的映射关系放入 endpointRefs 缓存
      endpointRefs.put(data.endpoint, data.ref)
      // 将 EndpointData 放入阻塞队列 receivers 的队尾。MessageLoop 线程异步获取到此 EndpointData,并处理其 Inbox中刚刚放人的 OnStart 消息,最终调用 RpcEndpoint 的 OnStart 方法在 RpcEndpoint 开始处理消息之前做一些准备工作
      receivers.offer(data)  // for the OnStart message
    }
    // 返回 NettyRpcEndpointRef
    endpointRef
  }
  • 对 RpcEndpoint 去注册
  def stop(rpcEndpointRef: RpcEndpointRef): Unit = {
    synchronized {
      // 判断 Dispatcher 是否已经停止,如果停止直接 return
      if (stopped) {
        // This endpoint will be stopped by Dispatcher.stop() method.
        return
      }
      // 没有停止,那么就对 RpcEndpoint 去注册
      unregisterRpcEndpoint(rpcEndpointRef.name)
    }
  }
  private def unregisterRpcEndpoint(name: String): Unit = {
    // 首先从 endpoints 中移除 EndpointData
    val data = endpoints.remove(name)
    if (data != null) {
      // 调用 EndpointData 中的 inbox 的 stop 方法停止 Inbox
      data.inbox.stop()
      // 将 EndpointData 重新放入 receivers
      receivers.offer(data)  // for the OnStop message
    }
  }

当要移除一个 EndpointData 时,其 Inbox 可能正在对消息进行处理,所以不能马上停止,这里采用了更平滑的停止方式,通过调用了 inbox 的 stop 方法来平滑过度

  def stop(): Unit = inbox.synchronized {
    if (!stopped) {
      // 根据之前的分析,MessageLoop有“允许并发运行”和“不允许并发运行”两种情况。对于允许并发的情况,为了确保安全,应该将enableConcurrent设置为false 
      enableConcurrent = false
      // 设置当前Inbox为停止状态
      stopped = true
      // 向 messages 中添加 OnStop 消息。根据之前对 MessageLoop 的分析,为了能够处理 OnStop 消息,只有 Inbox 所属的EndpointData 放入 receivers 中,其 messages 列表中的消息才会被处理。
      // 为了实现平滑停止,OnStop 消息最终将调用 Dispatcher 的 removeRpcEndpointRef 方法,将 RpcEndpoint 与RpcEndpointRef 的映射从缓存 endpointRefs 中移除。在匹配执行 OnStop 消息的最后,将调用 RpcEndpoint 的 onStop 方法对RpcEndpoint 停止
      messages.add(OnStop)
    }
  }
  • 将消息提交给指定的 RpcEndpoint
// Dispatcher 的 postMessage 用于将消息提交给指定的 RpcEndpoint  
private def postMessage(
      endpointName: String,
      message: InboxMessage,
      callbackIfStopped: (Exception) => Unit): Unit = {
    val error = synchronized {
      // 根据端点名称 endpointName 从缓存 endpoints 中获取 EndpointData
      val data = endpoints.get(endpointName)
      if (stopped) {
        Some(new RpcEnvStoppedException())
      } else if (data == null) {
        Some(new SparkException(s"Could not find $endpointName."))
      } else {
        // 如果当前 Dispatcher 没有停止并且缓存 endpoints 中确实存在名为 endpointName 的 EndpointData,那么将调用EndpointData 对应 Inbox 的 post 方法将消息加入 Inbox 的消息列表中,因此还需要将 EndpointData 推入 receivers,以便MessageLoop 处理此 Inbox中的消息
        data.inbox.post(message)
        receivers.offer(data)
        None
      }
    }
    // We don't need to call `onStop` in the `synchronized` block
    error.foreach(callbackIfStopped)
  }
// Inbox 未停止时向 messages 列表加入消息  
def post(message: InboxMessage): Unit = inbox.synchronized {
    if (stopped) {
      // We already put "OnStop" into "messages", so we should drop further messages
      onDrop(message)
    } else {
      messages.add(message)
      false
    }
  }

Dispatcher 中还有一些方法间接使用了 Dispatcher 的 postMessage 方法

  def postToAll(message: InboxMessage): Unit = {
    val iter = endpoints.keySet().iterator()
    while (iter.hasNext) {
      val name = iter.next
        postMessage(name, message, (e) => { e match {
          case e: RpcEnvStoppedException => logDebug (s"Message $message dropped. ${e.getMessage}")
          case e: Throwable => logWarning(s"Message $message dropped. ${e.getMessage}")
        }}
      )}
  }

  /** Posts a message sent by a remote endpoint. */
  def postRemoteMessage(message: RequestMessage, callback: RpcResponseCallback): Unit = {
    val rpcCallContext =
      new RemoteNettyRpcCallContext(nettyEnv, callback, message.senderAddress)
    val rpcMessage = RpcMessage(message.senderAddress, message.content, rpcCallContext)
    postMessage(message.receiver.name, rpcMessage, (e) => callback.onFailure(e))
  }

  /** Posts a message sent by a local endpoint. */
  def postLocalMessage(message: RequestMessage, p: Promise[Any]): Unit = {
    val rpcCallContext =
      new LocalNettyRpcCallContext(message.senderAddress, p)
    val rpcMessage = RpcMessage(message.senderAddress, message.content, rpcCallContext)
    postMessage(message.receiver.name, rpcMessage, (e) => p.tryFailure(e))
  }

  /** Posts a one-way message. */
  def postOneWayMessage(message: RequestMessage): Unit = {
    postMessage(message.receiver.name, OneWayMessage(message.senderAddress, message.content),
      (e) => throw e)
  }

上述的方法除了 postOneWayMessage 方法,其他需要恢复的消息都封装了 RpcCallContext,RpcCallContext 是用于回调客户端的上下文,一定定义了三个借口

  1. Reply : 用于向发送者回复消息
  2. sendFailure : 用于向发送者发送失败信息
  3. senderAddress : 用于获取发送者的地址
  • 停止 Dispatcher

Dispatcher 的 stop 方法用来停止 Dispatcher

  def stop(): Unit = {
    synchronized {
      if (stopped) {
        return
      }
      // 如果 Dispatcher 还未停止,则将自身状态修改为已停止
      stopped = true
    }
    // 对endpoints中的所有EndpointData去注册,这里通过调用 unregisterRpcEndpoint 方法,将向 endpoints 中的每个EndpointData 的 Inbox 里放置 OnStop 消息
    endpoints.keySet().asScala.foreach(unregisterRpcEndpoint)
    // 向receivers中投放“毒药”
    receivers.offer(PoisonPill)
    // 关闭threadpool线程池
    threadpool.shutdown()
  }

TransportContext 传输上下文创建简析

创建传输上下文 TransportContext 是 NettyRpcEnv 提供服务端与客户端能力的前提,前面博文(http://t.csdnimg.cn/jMcdi )中已经对 TransportContext 有部分介绍了,这里主要再介绍的是其构造器中传入的 RpcHandler 参数,这里用于构造 TransportContext 的 RpcHandler 实际是其实现类 NettyRpcHandler , NettyRpcHandler 的构造器里则以刚刚新建的 NettyStreamManager 实例作为参数

NettyStreamManager 简析

NettyStreamManager 实现了 StreamManager,专用于为 NettyRpcEnv 提供文件服务的能力

private val streamManager = new NettyStreamManager(this)

此处创建的 streamManager 的实际类型为 NettyStreamManager 。NettyStreamManager 用于提供 NettyRpcEnv 的文件流服务,这替代了在 Spark1.x.x 版本中使用 Jetty 实现的 HttpFileServer。RpcHandler 的 getStreamManager 方法返回的类型为 StreamManager。NettyStreamManager 正是继承了 StreamManager。NettyStreamManager 中定义了三个文件与目录缓存

  // 根据这三个缓存结构,NettyStreamManager 可以提供对普通文件、Jar 文件以及目录的下载支持
  private val files = new ConcurrentHashMap[String, File]()
  private val jars = new ConcurrentHashMap[String, File]()
  private val dirs = new ConcurrentHashMap[String, File]()
  
 // 下面方法用于支持文件流的读取,从缓存中获取文件后,将 TransportConf 及 File 等信息封装成 FileSegmentManagedBuffer 返回
  override def openStream(streamId: String): ManagedBuffer = {
    val Array(ftype, fname) = streamId.stripPrefix("/").split("/", 2)
    val file = ftype match {
      case "files" => files.get(fname)
      case "jars" => jars.get(fname)
      case other =>
        val dir = dirs.get(ftype)
        require(dir != null, s"Invalid stream URI: $ftype not found.")
        new File(dir, fname)
    }

    if (file != null && file.isFile()) {
      new FileSegmentManagedBuffer(rpcEnv.transportConf, file, 0, file.length())
    } else {
      null
    }
  }

 // 下面三个方法用于添加混存
  override def addFile(file: File): String = {
    val existingPath = files.putIfAbsent(file.getName, file)
    require(existingPath == null || existingPath == file,
      s"File ${file.getName} was already registered with a different path " +
        s"(old path = $existingPath, new path = $file")
    s"${rpcEnv.address.toSparkURL}/files/${Utils.encodeFileNameToURIRawPath(file.getName())}"
  }

  override def addJar(file: File): String = {
    val existingPath = jars.putIfAbsent(file.getName, file)
    require(existingPath == null || existingPath == file,
      s"File ${file.getName} was already registered with a different path " +
        s"(old path = $existingPath, new path = $file")
    s"${rpcEnv.address.toSparkURL}/jars/${Utils.encodeFileNameToURIRawPath(file.getName())}"
  }

  override def addDirectory(baseUri: String, path: File): String = {
    val fixedBaseUri = validateDirectoryUri(baseUri)
    require(dirs.putIfAbsent(fixedBaseUri.stripPrefix("/"), path) == null,
      s"URI '$fixedBaseUri' already registered.")
    s"${rpcEnv.address.toSparkURL}$fixedBaseUri"
  }

由于 NettyStreamManager 只实现了 StreamManage r的 openStream 方法,根据之前对 TransportRequestHandler 的 handle 方法和 procesStreamRequest 方法的介绍,我们知道 NettyStreamManager 将只提供对 StreamRequest 类型消息的处理。各个 Executor 节点就可以使用 Driver 节点的 RpcEnv 提供的 NettyStreamManager,从 Driver 将 Jar 包或文件下载到 Executor 节点上供任务执行

NettyRpcHandler 简析

NettyRpcEnv 中用于构造 TransportContext 的 RpcHandler 实际是其实现类 NettyRpcHandler,下面来对其重载的方法进行分析

  • 对客户端进行响应的 receive 方法之一
  override def receive(
      client: TransportClient,
      message: ByteBuffer,
      callback: RpcResponseCallback): Unit = {
    // 调用 internalReceive 方法将 ByteBuffer 类型的 message 转换为 RequestMessage
    val messageToDispatch = internalReceive(client, message)
    // 调用 Dispatcher 的 postRemoteMessage 方法消息转换为 RpcMessage 后放人 Inbox 的消息列表。MessageLoop 将调用RpcEndPoint 实现类的 receiveAndReply 方法,即 RpcEndPoint 处理完消息后会向客户端进行回复
    dispatcher.postRemoteMessage(messageToDispatch, callback)
  }
 // 用于将 ByteBuffer 类型的 message 转换为 RequestMessage
private def internalReceive(client: TransportClient, message: ByteBuffer): RequestMessage = {
    // 从 TransportClient 中获取远端地址 RpcAddress
    val addr = client.getChannel().remoteAddress().asInstanceOf[InetSocketAddress]
    assert(addr != null)
    val clientAddr = RpcAddress(addr.getHostString, addr.getPort)
   // 在 RequestMessage 的构造器中,调用NettyRpcEnv的deserialize方法对客户端发送的序列化后的消息(即ByteBuffer类型的消息)进行反序列化,根据deserialize的实现,反序列化实际使用了 javaSerializerInstance。javaSerializerlnstance 是通过NettyRpcEnv 的构造参数传入的对象,类型为 JavaSerializerlnstance
    val requestMessage = RequestMessage(nettyEnv, client, message)
    if (requestMessage.senderAddress == null) {
      // 如果反序列化得到的请求消息 requestMessage 中没有发送者的地址信息,则使用从 TransportClient 中获取的远端地址RpcAddress、requestMessage 的接收者(即RpcEndpoint)、requestMessage 的内容,以构造新的 RequestMessage
      new RequestMessage(clientAddr, requestMessage.receiver, requestMessage.content)
    } else {
      // 如果反序列化得到的请求消息 requestMessage 中含有发送者的地址信息,则将从 TransportClient 中获取的远端地址RpcAddress 与 requestMessage 中的发送者地址信息之间的映射关系放入缓存 remoteAddresses。还将调用 Dispatcher 的postToAll 方法向 endpoints 缓存的所有 EndpointData 的 Inbox 中放入 RemoteProcessConnected 消息
      val remoteEnvAddress = requestMessage.senderAddress
      if (remoteAddresses.putIfAbsent(clientAddr, remoteEnvAddress) == null) {
        dispatcher.postToAll(RemoteProcessConnected(remoteEnvAddress))
      }
      // 最后将返回requestMessage
      requestMessage
    }
  }
  • 对客户端进行响应的 receive 方法之二
  override def receive(
      client: TransportClient,
      message: ByteBuffer): Unit = {
    val messageToDispatch = internalReceive(client, message)
    // 此方法不会对客户端进行回复。此方法也调用了 internalReceive 方法,但是最后向 EndpointData 的 Inbox 投递消息使用了 postOneWayMessage 方法,将消息转换为 OneWayMessage 后放入 Inbox 的消息列表。将调用 RpcEndPoint 实现类的 receive 方法,也就是说 RpcEndPoint 处理完消息后不会向客户端进行回复
    dispatcher.postOneWayMessage(messageToDispatch)
  }

由于 NettyRpcHandler 实现了 RpcHandler 的两个 receive 方法,所以根据 TransportRequestHandler 的 handle 方法、processRpcRequest 方法及 processOneWayMessage 方法的介绍,我们知道 NettyRpcHandler 将提供对 OneWayMessage 和RpcRequest 两种类型消息的处理。

NettyRpcHandler 除实现了 RpcHandler 的两个 receive 方法,还实现了exceptionCaught、channelActive 与 channellnactive 等,这几个方法的处理都与receive方法类似

  • exceptionCaught 方法将会向 Inbox 中投递 RemoteProcessConnectionError 消息
  • channelActive 将会向 Inbox 中投递 RemoteProcessConnected
  • channelInactive 将会向 Inbox 中投递 RemoteProcessDisconnected 消息
TransportClientFactory 传输客户端工厂创建简析

创建传输客户端工厂 TransportClientFactory 是 NettyRpcEnv 向远端服务发起请求的基础,Spark 与远端 RpcEnv 进行通信都依赖于TransportClientFactory 生产的 TransportClient

NettyRpcBnv 中共创建了两个 TransportClientFactory

  private def createClientBootstraps(): java.util.List[TransportClientBootstrap] = {
    if (securityManager.isAuthenticationEnabled()) {
      java.util.Arrays.asList(new AuthClientBootstrap(transportConf,
        securityManager.getSaslUser(), securityManager))
    } else {
      java.util.Collections.emptyList[TransportClientBootstrap]
    }
  }
// 这里的 clientFactory 用于常规的发送请求和接收响应
  private val clientFactory = transportContext.createClientFactory(createClientBootstraps())
// fleDownloadFactory 用于文件下载。由于有些 RpcEnv 本身并不需要从远端下载文件,所以这里只声明了变量 fleDownloadFactory,并未进一步对其初始化。需要下载文件的 RpcEnv 会调用 downloadClient 方法创建TransportClientFactory,并用此 TransportClientFactory 创建下载所需的传输客户端 TransportClient
  @volatile private var fileDownloadFactory: TransportClientFactory = _
 private def downloadClient(host: String, port: Int): TransportClient = {
    if (fileDownloadFactory == null) synchronized {
      if (fileDownloadFactory == null) {
        val module = "files"
        val prefix = "spark.rpc.io."
        val clone = conf.clone()

        // Copy any RPC configuration that is not overridden in the spark.files namespace.
        conf.getAll.foreach { case (key, value) =>
          if (key.startsWith(prefix)) {
            val opt = key.substring(prefix.length())
            clone.setIfMissing(s"spark.$module.io.$opt", value)
          }
        }

        val ioThreads = clone.getInt("spark.files.io.threads", 1)
        val downloadConf = SparkTransportConf.fromSparkConf(clone, module, ioThreads)
        val downloadContext = new TransportContext(downloadConf, new NoOpRpcHandler(), true)
        fileDownloadFactory = downloadContext.createClientFactory(createClientBootstraps())
      }
    }
    fileDownloadFactory.createClient(host, port)
  }

fileDownloadFactory 与 clientFactory 使用的 SparkTransportConf 内部代理的 SparkConf 都是从 NettyRpcEnv 的 SparkConf 克隆来的,不同之处在于

  • clientFactory 所属的模块(module变量)为 rpc ,fileDownloadFactory 所属的模块为 files
  • clientFactory 中的读写线程数由 spark.rpc.io.numConnectionsPerPeer 属性控制,而 fleDownloadFactory 中的读写线程数由spark.files.io.threads 属性控制
TransportServer 创建简析
  @volatile private var server: TransportServer = _

  private val stopped = new AtomicBoolean(false)

可以看到,TransportServer 在此时并未实例化,真正实例化是在 startNettyRpcEnv 回调 NettyRpcEnv 的 startServer 方法时,startNettyRpcEnv 是 RpcEnv 的偏函数

 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。这里使用了 TransportContext 的 createServer 方法
    server = transportContext.createServer(bindAddress, port, bootstraps)
    // 向 Dispatcher 注册 RpcEndpointVerifier。RpcEndpointVerifier 用于校验指定名称的 RpcEndpoint 是否存在。RpcEndpointVerifier 在 Dispatcher 中的注册名內 endpoint-verifier
    dispatcher.registerRpcEndpoint(
      RpcEndpointVerifier.NAME, new RpcEndpointVerifier(this, dispatcher))
  }
// RpcEndpointVerifier 实现了 RpcEndpoint 的 receiveAndReply 方法。因此 MessageLoop 线程在处理RpcEndpointVerifier 所关联的 Inbox 中的消息时,会匹配 RpcMessage 调用 RpcEndpointVerifier 的 receiveAndReply 方法
private[netty] class RpcEndpointVerifier(override val rpcEnv: RpcEnv, dispatcher: Dispatcher)
  extends RpcEndpoint {
 
  // receiveAndReply 的处理步骤
  // 1.接收 CheckExistence 类型的消息,匹配出 name 参数,此参数代表要查询的 RpcEndpoint 的具体名称
  // 2.调用 Dispatcher 的 verify 方法。verify 用于校验 Dispatcher 的 endpoints 缓存中是否存在名为 name 的RpcEndpoint
  // 3.调用 RpcCallContext 的 reply 方法回复客户端
  override def receiveAndReply(context: RpcCallContext): PartialFunction[Any, Unit] = {
    case RpcEndpointVerifier.CheckExistence(name) => context.reply(dispatcher.verify(name))
  }
}

根据对 RpcEndpointVerifier 的实现分析,我们知道它对外提供了查询当前 RpcEndpointVerifier 所在 RpcEnv 的Dispatcher 中是否存在请求中指定名称所对应的 RpcEndpoint

TransportServe r初始化并且启动后,就可以利用 NettyRpcHandler 和 NettyStreamManager 对外提供服务了

客户端请求发送 简析

在客户端向远端 RpcEndpoint 发送消息情况下,当 TransportClient 发出请求之后,会等待获取服务端的回复,这就涉及超时问题。另外由于 TransportClientFactory.createClient 方法是阻塞式调用,所以需要一个异步的处理

// 用于处理请求超时的调度器。timeoutScheduler 的类型实际是 ScheduledExecutorService,比起使用 Timer 组件,ScheduledExecutorService 将比 Timer 更加稳定,比如线程挂掉后,ScheduledExecutorService 会重启一个新的线程定时检查请求是否超时  
val timeoutScheduler = ThreadUtils.newDaemonSingleThreadScheduledExecutor("netty-rpc-env-timeout")

// 一个用于异步处理 TransportClientFactory.createClient 方法调用的线程池。这个线程池的大小默认为64,可以使用spark.rpc.connect.threads 属性进行配置
private[netty] val clientConnectionExecutor = ThreadUtils.newDaemonCachedThreadPool(
    "netty-rpc-connection",
    conf.getInt("spark.rpc.connect.threads", 64))

// RpcAddress 与 Outbox 的映射关系的缓存。每次向远端发送请求时,此请求消息首先放入此远端地址对应的 Outbox,然后使用线程异步发送
private val outboxes = new ConcurrentHashMap[RpcAddress, Outbox]()
  • Outbox 和 OutboxMessage

outboxs 中缓存了远端 RPC 地址与 Outbox 的关系,以下是 Outbox 中的成员变量:

  • nettyEnv : 当前 Outbox 所在节点上的 NettyRpcEnv
  • address : Outbox 所对应的远端 NettyRpcEnv 的地址
  • messages : 向其他远端 NettyRpcEnv 上的所有 RpcEndpoint 发送的消息列表
  • client : 当前 Outbox 内的 TransportClient,消息的发送都依赖于此传输客户端
  • connectFuture : 指向当前 Outbox 内连接任务的 java.util.concurrent.Future 引用。如果当前没有连接任务,则 connectFuture 为null
  • stopped : 当前Outbox是否停止的状态
  • draining : 表示当前 Outbox 内正有线程在处理 messages 列表中消息的状态

消息列表 messages 中的消息类型为 OutboxMessage,所有将要向远端发送的消息都会被封装成 OutboxMessage 类型,OutboxMessage 作为一个特质,定义了所有向外发送消息的规范,根据 OutboxMessage 的名称,我们很容易与 Dispatcher 中 Inbox 里的 InboxMessage 类型的消息关联起来

  • OutboxMessage 在客户端使用,是对外发送消息的封装
  • InboxMessage 在服务端使用,是对所接收消息的封装
private[netty] case class RpcOutboxMessage(
    content: ByteBuffer,
    _onFailure: (Throwable) => Unit,
    _onSuccess: (TransportClient, ByteBuffer) => Unit)
  extends OutboxMessage with RpcResponseCallback with Logging {

  private var client: TransportClient = _
  private var requestId: Long = _
  // RpcOutboxMessage 重写的s endWith 方法正是利用了 TransportClient 的 sendRpc 方法
  override def sendWith(client: TransportClient): Unit = {
    this.client = client
    // 第二个参数是 RpcResponseCallback 类型,RpcOutboxMessage 本身也实现了 RpcResponseCallback,所以调用的时候传递了 RpcOutboxMessage 的 this 引用
    this.requestId = client.sendRpc(content, this)
  }

  def onTimeout(): Unit = {
    if (client != null) {
      client.removeRpcRequest(requestId)
    } else {
      logError("Ask timeout before connecting successfully")
    }
  }

  override def onFailure(e: Throwable): Unit = {
    _onFailure(e)
  }

  override def onSuccess(response: ByteBuffer): Unit = {
    _onSuccess(client, response)
  }

}

下面对 Outbox 的方法进行简析

  def send(message: OutboxMessage): Unit = {
    val dropped = synchronized {
      // 判断当前Outbox的状态是否已经停止
      if (stopped) {
        true
      } else {
        // 如果 Outbox 还未停止,则将 OutboxMessage 添加到 messages 列表中
        messages.add(message)
        false
      }
    }
    if (dropped) {
      // 如果Outbox已经停止,则向发送者发送SparkBxception异常
      message.onFailure(new SparkException("Message is dropped because Outbox is stopped"))
    } else {
      // 如果 Outbox 还未停止,调用 drainOutbox 方法处理 messages 中的消息
      drainOutbox()
    }
  }
private def drainOutbox(): Unit = {
    var message: OutboxMessage = null
    synchronized {
      // 如果当前 Outbox 已经停止或者正在连接远端服务,则返回
      if (stopped) {
        return
      }
      if (connectFuture != null) {
        return
      }
      if (client == null) {
        // 如果 Outbox 中的 TransportClient 为 null,这说明还未连接远端服务。此时需要调用 launchConnectTask 方法运行连接远端服务的任务,然后返回
        launchConnectTask()
        return
      }
      if (draining) {
        // 如果正有线程在处理(即发送) messages 列表中的消息
        return
      }
      message = messages.poll()
      // 如果 messages 列表中没有消息要处理,则返回。否则取出其中的一条消息,并将 draining 状态置为 true
      if (message == null) {
        return
      }
      draining = true
    }
  // 循环处理messages列表中的消息
    while (true) {
      try {
        val _client = synchronized { client }
        if (_client != null) {
          // 不断从 messages 列表中取出消息并调用 OutboxMessage 的 sendWith 方法发送消息
          message.sendWith(_client)
        } else {
          assert(stopped == true)
        }
      } catch {
        case NonFatal(e) =>
          handleNetworkFailure(e)
          return
      }
      synchronized {
        if (stopped) {
          return
        }
        message = messages.poll()
        if (message == null) {
          draining = false
          return
        }
      }
    }
  }
  private def launchConnectTask(): Unit = {
    // 构造 Callable 的匿名内部类,此匿名类将调用 NettyRpcEnv 的 createClient 方法创建 TransportClient,然后调用drainOutbox 方法处理 Outbox 中的消息,使用 NettyRpcEnv 中的 clientConnectionBxecutor 提交 Callable 的匿名内部类
    connectFuture = nettyEnv.clientConnectionExecutor.submit(new Callable[Unit] {

      override def call(): Unit = {
        try {
          val _client = nettyEnv.createClient(address)
          outbox.synchronized {
            client = _client
            if (stopped) {
              closeClient()
            }
          }
        } catch {
          case ie: InterruptedException =>
            // exit
            return
          case NonFatal(e) =>
            outbox.synchronized { connectFuture = null }
            handleNetworkFailure(e)
            return
        }
        outbox.synchronized { connectFuture = null }
        drainOutbox()
      }
    })
  }
  • NettyRpcEndpointRef 简析

在 NettyRpcEnv 中,要向远端 RpcEndpoint 发送请求,首先要持有 RpcEndpoint 的引用对象 NettyRpcEndpointRef (类似于Akka中Actor的ActorRef),NettyRpcEndpointRef 是 RpcEndpointRef 的唯一子类。NettyRpcEndpointRef 重写了 RpcEndpointRef 的部分方法

private[netty] class NettyRpcEndpointRef(
    @transient private val conf: SparkConf,
    private val endpointAddress: RpcEndpointAddress,
    @transient @volatile private var nettyEnv: NettyRpcEnv) extends RpcEndpointRef(conf) {
  // 类型为 TransportClient。NettyRpcEndpointRef将利用此 TransportClient 向远端的 RpcEndpoint 发送请求
  @transient @volatile var client: TransportClient = _
  // 远端 RpcEndpoint 的地址 RpcEndpointAddress
  override def address: RpcAddress =
    if (endpointAddress.rpcAddress != null) endpointAddress.rpcAddress else null

  private def readObject(in: ObjectInputStream): Unit = {
    in.defaultReadObject()
    nettyEnv = NettyRpcEnv.currentEnv.value
    client = NettyRpcEnv.currentClient.value
  }

  private def writeObject(out: ObjectOutputStream): Unit = {
    out.defaultWriteObject()
  }
 // 远端RpcEndpoint的名称
  override def name: String = endpointAddress.name
 // 首先将 message 封装为 RequestMessage,然后调用 NettyRpcEnv 的 ask 方法
  override def ask[T: ClassTag](message: Any, timeout: RpcTimeout): Future[T] = {
    nettyEnv.ask(new RequestMessage(nettyEnv.address, this, message), timeout)
  }
 // 首先将 message 封装 RequestMessage,然后调用 NettyRpcEnv 的 send 方法
  override def send(message: Any): Unit = {
    require(message != null, "Message is null")
    nettyEnv.send(new RequestMessage(nettyEnv.address, this, message))
  }

  override def toString: String = s"NettyRpcEndpointRef(${endpointAddress})"

  final override def equals(that: Any): Boolean = that match {
    case other: NettyRpcEndpointRef => endpointAddress == other.endpointAddress
    case _ => false
  }

  final override def hashCode(): Int =
    if (endpointAddress == null) 0 else endpointAddress.hashCode()
}

下面简析 NettyRpcEnv 的 ask 和 send 方法

  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 = {
      if (!promise.tryFailure(e)) {
        e match {
          case e : RpcEnvStoppedException => logDebug (s"Ignored failure: $e")
          case _ => logWarning(s"Ignored failure: $e")
        }
      }
    }

    def onSuccess(reply: Any): Unit = reply match {
      case RpcFailure(e) => onFailure(e)
      case rpcReply =>
        if (!promise.trySuccess(rpcReply)) {
          logWarning(s"Ignored message: $reply")
        }
    }

    try {
      // 如果请求消息的接收者的地址与当前 NettyRpcEnv 的地址相同(则说明处理请求的 RpcEndpoint 位于本地的 NettyRpcEnv中),那么新建 Promise对象,并且给 Promise 的 future(类型 Future)设置完成时的回调函数(成功时调用 onSuccess 方法,失败时调用 onFailure 方法)。发送消息最终通过调用本地 Dispatcher 的 postLocalMessage 方法 
      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 {
        // 如果请求消息的接收者的地址与当前 NettyRpcEnv 的地址不同(则说明处理请求的 RpcEndpoint 位于其他节点的NettyRpcEnv 中),那么将 message 序列化,并与 onFailure、onSuccess方 法一道封装为 RpcOutboxMessag e类型的消息。最后调用postToOutbox方法将消息投递出去
        val rpcMessage = RpcOutboxMessage(message.serialize(this),
          onFailure,
          (client, response) => onSuccess(deserialize[Any](client, response)))
        postToOutbox(message.receiver, rpcMessage)
        promise.future.failed.foreach {
          case _: TimeoutException => rpcMessage.onTimeout()
          case _ =>
        }(ThreadUtils.sameThread)
      }

      // 用 timeoutScheduler 设置一个定时器,用于超时处理。此定时器在等待指定的超时时间后将抛出 TimeoutException 异常。请求如果在超时时间内处理完毕,则会调用 timeoutScheduler 的 cancel 方法取消此超时定时器
      val timeoutCancelable = timeoutScheduler.schedule(new Runnable {
        override def run(): Unit = {
          onFailure(new TimeoutException(s"Cannot receive any reply from ${remoteAddr} " +
            s"in ${timeout.duration}"))
        }
      }, timeout.duration.toNanos, TimeUnit.NANOSECONDS)
      promise.future.onComplete { v =>
        timeoutCancelable.cancel(true)
      }(ThreadUtils.sameThread)
    } catch {
      case NonFatal(e) =>
        onFailure(e)
    }
    // 返回请求处理的结果
    promise.future.mapTo[T].recover(timeout.addMessageIfTimeout)(ThreadUtils.sameThread)
  }
  private[netty] def send(message: RequestMessage): Unit = {
    val remoteAddr = message.receiver.address
    // 如果请求消息的接收者的地址与当前 NettyRpcBnv 的地址相同。那么新建 Promise 对象,并且给 Promise 的 future(类型为Future)设置完成时的回调函数(成功时调用 onSuccess 方法,失败时调用 onFailure 方法)。发送消息最终通过调用本地 Dispatcher的 postOneWayMessage 方法(见代码清单5-15)实现。
    if (remoteAddr == address) {
      // Message to a local RPC endpoint.
      try {
        dispatcher.postOneWayMessage(message)
      } catch {
        case e: RpcEnvStoppedException => logDebug(e.getMessage)
      }
    // 如果请求消息的接收者的地址与当前 NettyRpcEnv 的地址不同®,那么将 message 序列化,并与 onFailure、onSuccess 方法一道封装为 RpcOutboxMessage 类型的消息。最后调用 postToOutbox 方法将消息投递出去。NettyRpcEnv 的 ask 和 send 方法都调用了私有方法 postToOutbox,postToOutbox 用于向远端节点上的 RpcEndpoint 发送消息
    } else {
      // Message to a remote RPC endpoint.
      postToOutbox(message.receiver, OneWayOutboxMessage(message.serialize(this)))
    }
  }
  private def postToOutbox(receiver: NettyRpcEndpointRef, message: OutboxMessage): Unit = {
    // 如果 NettyRpcEndpointRef 中的 TransportClient 不为空,则直接调用 OutboxMessage 的 sendWith 方法
    if (receiver.client != null) {
      message.sendWith(receiver.client)
    } else {
      require(receiver.address != null,
        "Cannot send message to client endpoint with no listen address.")
      val targetOutbox = {
        // 获取 NettyRpcEndpointRef 的远端 RpcEndpoint 地址所对应的 Outbox。首先从 outboxes 缓存中获取 Outbox。如果 outboxes 中没有相应的 Outbox,则需要新建 Outbox 并放入 outboxes 缓存中
        val outbox = outboxes.get(receiver.address)
        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
        }
      }
      // 如果当前 NettyRpcEnv 已经处于停止状态,则将上一步得到的 Outbox 从 outboxes 中移除,并且调用 Outbox 的 stop方法停止 Outbox
      if (stopped.get) {
        // It's possible that we put `targetOutbox` after stopping. So we need to clean it.
        outboxes.remove(receiver.address)
        // stop 方法用于停止 Outbox,步骤为:
        // 1.将 Outbox 的停止状态 stopped 置为true
        // 2.关闭 Outbox 中的 TransportClient
        // 3.清空 messages 列表中的消息
        targetOutbox.stop()
      } else {
        // 如果当前 NettyRpcBnv 还未停止,则调用上一步得到的 Outbox 的 send 方法发送消息
        targetOutbox.send(message)
      }
    }
  }

RPC客户端发送请求
未完待续。。。

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;