Spark源码阅读篇-Rpc通信-Inbox收件箱

70 阅读8分钟

上一节介绍了MessageLoop消息循环,这一节介绍消息循环中每个节点Inbox收件箱,每个Inbox对应一个RpcEndpoint,Inbox如何将消息传递给RpcEndpoint呢?我们一起从源码中寻找答案。

Inbox源码

//只能在netty包内被继承的特质 trait--特质 类似于Java中接口
//sealed 的 trait 只能被同一个文件内的 class 继承
//消息收件箱
private[netty] sealed trait InboxMessage

//单向消息 通常不需要回复
private[netty] case class OneWayMessage(
    senderAddress: RpcAddress,//消息发送者的地址和内容
    content: Any) extends InboxMessage

//rpc消息 通常需要回复或者连接
private[netty] case class RpcMessage(
    senderAddress: RpcAddress,
    content: Any,
    context: NettyRpcCallContext) extends InboxMessage

//case object 单例对象 具备模式匹配的能力 编辑器自动生成唯一实例
//消息实体 表示启动
private[netty] case object OnStart extends InboxMessage

//消息实体 单例对象 表示停止
private[netty] case object OnStop extends InboxMessage

//case class 表示不可变且具有模式匹配能力的类
//用来表示远程进程连接过来了
private[netty] case class RemoteProcessConnected(remoteAddress: RpcAddress) extends InboxMessage

//用来表示远程进程已经断开连接了
private[netty] case class RemoteProcessDisconnected(remoteAddress: RpcAddress) extends InboxMessage

//用来表示远程进程连接异常
private[netty] case class RemoteProcessConnectionError(cause: Throwable, remoteAddress: RpcAddress)
  extends InboxMessage

//收件箱 一个收件箱对应一个节点endpoint
private[netty] class Inbox(val endpointName: String, val endpoint: RpcEndpoint)
  extends Logging {

  inbox =>  //给它一个别名,这样我们可以在闭包中更清楚地使用它。

  //GuardedBy表示在使用该对象时需要先获取到锁
  //链表 每个结点的值是InboxMessage
  @GuardedBy("this")
  protected val messages = new java.util.LinkedList[InboxMessage]()

  //状态信息 为真 表示该inbox和对应的节点endpoint已经停止运行了
  @GuardedBy("this")
  private var stopped = false
  
  //状态信息 允许同一时间多个线程去处理消息 默认不允许
  @GuardedBy("this")
  private var enableConcurrent = false

  //线程数 表示此时正在处理处理inbox的消息的线程数量
  @GuardedBy("this")
  private var numActiveThreads = 0

  //OnStart消息应该第一个被处理
  inbox.synchronized {
    //将OnStart消息放入messages链表中
    messages.add(OnStart)
  }

  //处理messages中存储的消息
  def process(dispatcher: Dispatcher): Unit = {
    //单个消息
    var message: InboxMessage = null
    inbox.synchronized {
      //如果当前消息不允许被处理或者正在处理消息的线程数不为0 则直接返回
      if (!enableConcurrent && numActiveThreads != 0) {
        return
      }
      //取到messages链表中第一个结点
      message = messages.poll()
      if (message != null) {//如果取到的消息不为空 则计数器加一 表示正在处理消息的线程数多了一个
        numActiveThreads += 1
      } else {//消息为空也直接返回
        return
      }
    }
    while (true) {
      //调用动作闭包 处理各种异常 包括消息处理过程中的异常和不匹配的消息异常
      safelyCall(endpoint) {
        //消息匹配
        message match {
          //如果是rpcmessage 则进行如下处理
          case RpcMessage(_sender, content, context) =>
            try {
              //调用endpoint引用的方法处理 有回复
              endpoint.receiveAndReply(context).applyOrElse[Any, Unit](content, { msg =>
                throw new SparkException(s"Unsupported message $message from ${_sender}")
              })
            } catch {
              case e: Throwable =>
                context.sendFailure(e)
                //抛出异常——这个异常将被safelyCall函数捕获。端点的onError函数将被调用。
                throw e
            }
          
          //单向消息处理
          case OneWayMessage(_sender, content) =>
            //调用endpoint引用的方法处理 无回复
            endpoint.receive.applyOrElse[Any, Unit](content, { msg =>
              throw new SparkException(s"Unsupported message $message from ${_sender}")
            })
          
          //OnStart消息 表示启动
          case OnStart =>
            //启动节点
            endpoint.onStart()
            //如果节点不是线程安全的rpcendpoint
            if (!endpoint.isInstanceOf[ThreadSafeRpcEndpoint]) {
              inbox.synchronized {
                //如果线程启动了
                if (!stopped) {
                  //允许消息被处理
                  enableConcurrent = true
                }
              }
            }
          
          //OnStop消息 表示停止
          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分发器移除该节点的引用
            dispatcher.removeRpcEndpointRef(endpoint)
            //停止该节点
            endpoint.onStop()
            //断言 链表不过不为空 则抛出异常
            assert(isEmpty, "OnStop should be the last message")
          
          //远程进程已连接 收到的回复
          case RemoteProcessConnected(remoteAddress) =>
            //向节点发送消息 远程节点已连接
            endpoint.onConnected(remoteAddress)
           
          //远程进程已断开连接 收到的回复
          case RemoteProcessDisconnected(remoteAddress) =>
            //向endpoint节点发送消息 远程节点已断开连接
            endpoint.onDisconnected(remoteAddress)
          
          //远程进程连接异常
          case RemoteProcessConnectionError(cause, remoteAddress) =>
            //向endpoint节点发送远程节点连接异常消息
            endpoint.onNetworkError(cause, remoteAddress)
        }
      }

      inbox.synchronized {
        //"enableConcurrent"将在' onStop '被调用后被设置为false,所以我们应该每次都检查它。
        //如果消息允许被处理 且还有其他线程在处理 
        if (!enableConcurrent && numActiveThreads != 1) {
          // 如果我们不是唯一的worker 则退出
          //正在处理消息的线程数减一
          numActiveThreads -= 1
          return
        }
        //取到链表的第一个消息
        message = messages.poll()
        if (message == null) {//如果消息为空
          numActiveThreads -= 1 //线程数减一
          return
        }
      }
    }
  }

  //发送消息
  def post(message: InboxMessage): Unit = inbox.synchronized {
    //如果inbox和对应的endpoint节点都停止运行了
    if (stopped) {
      //丢弃该消息 因为此时endpoint节点已经停止运行了 不再处理消息
      onDrop(message)
    } else {
      //如果inbox和对应的节点endpoint都在运行 将消息加入到messages链表末尾
      messages.add(message)
      false
    }
  }

  //停止inbox和对应的endpoint节点运行
  def stop(): Unit = inbox.synchronized {
    //下面的代码应该先获取锁,以便我们可以确保“OnStop”是最后一条消息
    //如果inbox和对应的节点endpoint都在运行中
    if (!stopped) {
      //我们应该在这里禁用concurrent。然后当RpcEndpoint.onStop被调用时,它是唯一正在处理消息的线程。
      //所以“RpcEndpoint.onStop可以安全地释放它的资源。
      //消息不允许被处理了
      enableConcurrent = false
      //inbox和对应的endpoint节点状态置停
      stopped = true
      //加入OnStop消息 停止endpoint节点
      messages.add(OnStop)
      // Note: The concurrent events in messages will be processed one by one.
    }
  }

  //判断messages链表是否为空
  def isEmpty: Boolean = inbox.synchronized { messages.isEmpty }

  //当我们发送消息时调用。测试用例覆盖它来测试消息丢弃。暴露用于测试。
  protected def onDrop(message: InboxMessage): Unit = {
    logWarning(s"Drop $message because endpoint $endpointName is stopped")
  }

   //调用动作闭包,并在异常情况下调用端点的onError函数。
   //闭包:一个函数在实现逻辑时,将外部的变量引入到函数的内部,改变了这个变量的生命周期,称之为闭包
  private def safelyCall(endpoint: RpcEndpoint)(action: => Unit): Unit = {
    //处理致命错误
    def dealWithFatalError(fatal: Throwable): Unit = {
      inbox.synchronized {
        //断言 如果正在处理消息的线程数小于或者等于0 则抛出异常
        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 $endpointName", fatal)
      throw fatal
    }

    try action catch {
      //如果是非致命异常,非致命异常是指可以被程序处理并继续执行的异常。
      //对于这些异常,我们希望能够捕获并优雅地处理,而不是让程序直接崩溃。
      case NonFatal(e) =>
        //endpoint返回异常原因 由catch后的代码块处理
        try endpoint.onError(e) catch {
          case NonFatal(ee) =>
            //如果inbox和对应的节点endpoint都停止了则忽视该异常 debug级别较低 不会输出
            if (stopped) {
              logDebug("Ignoring error", ee)
            } else {
            //如果inbox和对应的节点endpoint都在运行则打印该错误 并表示该错误可以忽略
              logError("Ignoring error", ee)
            }
          //如果是致命异常
          case fatal: Throwable =>
            dealWithFatalError(fatal)
        }
      //如果是致命异常 则交由函数处理
      case fatal: Throwable =>
        dealWithFatalError(fatal)
    }
  }

  //获取当前正在处理消息的线程
  def getNumActiveThreads: Int = {
    inbox.synchronized {
      inbox.numActiveThreads
    }
  }
}

Inbox中通过一个链表messages来保存收到的消息,该链表的每一个节点都是一个InboxMessage,InboxMessage是一个特质,类似于Java中的抽象类,具体实现包括单向消息OneWayMessage、需要回复的RpcMessage等等,

  @GuardedBy("this")
  protected val messages = new java.util.LinkedList[InboxMessage]()

messages被@GuardedBy注解修饰,意味着每次使用该变量时都需要获取该变量的锁,保证线程安全。

Inbox中的第一个消息是OnStart,该消息主要是启动对应的RpcEndpoint:

//OnStart消息 表示启动
          case OnStart =>
            //启动节点
            endpoint.onStart()
            //如果节点不是线程安全的rpcendpoint
            if (!endpoint.isInstanceOf[ThreadSafeRpcEndpoint]) {
              inbox.synchronized {
                //如果线程启动了
                if (!stopped) {
                  //允许消息被处理
                  enableConcurrent = true
                }
              }
            }

而MessageLoop中要给RpcEndpoint发消息实际上是调用Inbox的post方法:

MessageLoop源码

  //给指定的节点endpoint发消息
  override def post(endpointName: String, message: InboxMessage): Unit = {
    //获取该节点对应的inbox-收件箱
    val inbox = endpoints.get(endpointName)
    //inbox将消息放到队列末尾,同样是线程扫描处理
    inbox.post(message)
    //将inbox放到active队列末尾
    setActive(inbox)
  }

Inbox中的post方法实际上是把消息放进messages链表末尾,等待处理:

//发送消息
  def post(message: InboxMessage): Unit = inbox.synchronized {
    //如果inbox和对应的endpoint节点都停止运行了
    if (stopped) {
      // We already put "OnStop" into "messages", so we should drop further messages
      //丢弃该消息 因为此时endpoint节点已经停止运行了 不再处理消息
      onDrop(message)
    } else {
      //如果inbox和对应的节点endpoint都在运行 将消息加入到messages链表末尾
      messages.add(message)
      false
    }
  }

那么消息到底是怎么持续被处理的呢?得益于MessageLoop中有一个线程池,线程在运行时receiveLoop方法会扫描active对列(不清楚的同学可以看上一节MessageLoop源码),并对收件箱进行处理,处理方式就是调用Inbox的process方法:

MessageLoop源码

 //在线程中不间断执行,收到消息则放入到active队列末尾
  private def receiveLoop(): Unit = {
    try {
      while (true) {
        try {
          //取active队列第一个结点
          val inbox = active.take()
          if (inbox == MessageLoop.PoisonPill) {//如果取到毒丸 就放回队列末尾
            // Put PoisonPill back so that other threads can see it.
            setActive(MessageLoop.PoisonPill)
            return
          }
          //inbox针对不同类型的消息,调用endpoint对应的方法来处理,inbox与endpoint是一一对应的
          inbox.process(dispatcher)
        } catch {
          case NonFatal(e) => logError(e.getMessage, e)
        }
      }
    } catch {
      case _: InterruptedException => // exit
        case t: Throwable =>
          try {
            // Re-submit a receive task so that message delivery will still work if
            // UncaughtExceptionHandler decides to not kill JVM.
            //重新提交接收任务,以便消息传递仍然可以工作
            //UncaughtExceptionHandler决定不杀死JVM。
            threadpool.execute(receiveLoopRunnable)
          } finally {
            throw t
          }
    }
  }
}

Inbox中的process方法主要是处理messages链表中的具体消息InboxMessage,首先取出messages链表中的第一个节点的值,判断消息不为空则进行消息匹配,根绝不同消息类型做出不同的处理:

//消息匹配
        message match {
          //如果是rpcmessage 则进行如下处理
          case RpcMessage(_sender, content, context) =>
            try {
              //调用endpoint引用的方法处理 有回复
              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.
                //抛出异常——这个异常将被safelyCall函数捕获。端点的onError函数将被调用。
                throw e
            }
          
          //单向消息处理
          case OneWayMessage(_sender, content) =>
            //调用endpoint引用的方法处理 无回复
            endpoint.receive.applyOrElse[Any, Unit](content, { msg =>
              throw new SparkException(s"Unsupported message $message from ${_sender}")
            })
          
          //OnStart消息 表示启动
          case OnStart =>
            //启动节点
            endpoint.onStart()
            //如果节点不是线程安全的rpcendpoint
            if (!endpoint.isInstanceOf[ThreadSafeRpcEndpoint]) {
              inbox.synchronized {
                //如果线程启动了
                if (!stopped) {
                  //允许消息被处理
                  enableConcurrent = true
                }
              }
            }
          
          //OnStop消息 表示停止
          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分发器移除该节点的引用
            dispatcher.removeRpcEndpointRef(endpoint)
            //停止该节点
            endpoint.onStop()
            //断言 链表不过不为空 则抛出异常
            assert(isEmpty, "OnStop should be the last message")
          
          //远程进程已连接 收到的回复
          case RemoteProcessConnected(remoteAddress) =>
            //向节点发送消息 远程节点已连接
            endpoint.onConnected(remoteAddress)
           
          //远程进程已断开连接 收到的回复
          case RemoteProcessDisconnected(remoteAddress) =>
            //向endpoint节点发送消息 远程节点已断开连接
            endpoint.onDisconnected(remoteAddress)
          
          //远程进程连接异常
          case RemoteProcessConnectionError(cause, remoteAddress) =>
            //向endpoint节点发送远程节点连接异常消息
            endpoint.onNetworkError(cause, remoteAddress)
        }
      }

如果是单向消息则调用端点的receive.applyOrElse进行处理,如果是需要回复的rpc消息则调用端点的receiveAndReply进行处理,远程节点连接状态也会通知端点,消息在发送或者请求出现了异常都通过闭包safelyCall进行处理。

总结:在netty实现方式中主要是Inbox对消息进行判断并调用端点RpcEndpoint进行处理,而消息循环MeaasgeLoop主要作用是管理Inbox,并通过线程池中的线程检查收到的消息确保收到的消息都能及时且安全地被处理。