在上篇 spark 源码分析之五 -- Spark内置RPC机制剖析之一创建NettyRPCEnv 中,涉及到了Diapatcher 内容,未做过多的剖析。本篇来剖析一下它的工作原理。

Dispatcher 是消息的分发器,负责将消息分发给适合的 endpoint

其实这个类还是比较简单的,先来看它的类图:

我们从成员变量入手分析整个类的内部构造和机理:

1. endpoints是一个 ConcurrentMap[String, EndpointData], 负责存储 endpoint name 和 EndpointData 的映射关系。其中,EndpointData又包含了 endpoint name, RpcEndpoint 以及 NettyRpcEndpointRef 的引用以及Inbox 对象(包含了RpcEndpoint 以及 NettyRpcEndpointRef 的引用)。

2. endpointRefs: ConcurrentMap[RpcEndpoint, RpcEndpointRef] 包含了 RpcEndpoint 和 RpcEndpointRef 的映射关系。

3. receivers 是一个 LinkedBlockingQueue[EndpointData] 消息阻塞队列,用于存放 EndpointData 对象。它主要用于追踪 那些可能会包含需要处理消息receiver(即EndpointData)。在post消息到Dispatcher 时,一般会先post 到 EndpointData 的 Inbox 中, 然后,再将 EndpointData对象放入 receivers 中,源码如下:

// Posts a message to a specific endpoint.
private def postMessage(
endpointName: String,
message: InboxMessage,
callbackIfStopped: (Exception) => Unit): Unit = {
val error = synchronized {
// 1. 先根据endpoint name从路由中找到data
val data = endpoints.get(endpointName)
if (stopped) {
Some(new RpcEnvStoppedException())
} else if (data == null) {
Some(new SparkException(s"Could not find $endpointName."))
} else {
// 2. 将待消费的消息发送到 inbox中
data.inbox.post(message)
// 3. 将 data 放到待消费的receiver 中
receivers.offer(data)
None
}
}
// We don't need to call `onStop` in the `synchronized` block
error.foreach(callbackIfStopped)
}

4. stopped 标志 Dispatcher 是否已经停止了
5. threadpool 是 ThreadPoolExecutor 对象, 其中的 线程的 core 数量的计算如下:
val availableCores = if (numUsableCores > 0) numUsableCores else Runtime.getRuntime.availableProcessors()val numThreads = nettyEnv.conf.getInt("spark.rpc.netty.dispatcher.numThreads", math.max(2, availableCores))
获取到线程数之后, 会初始化 一个固定的线程池,用来执行 MessageLoop 任务,MessageLoop 是一个Runnable 对象。它会不停地从 receiver 堵塞队列中, 把放入的 EndpointData对象取出来,并且去调用其inbox成员变量的 process 方法。
6. PoisonPill 是一个空的EndpointData对象,起了一个标志位的作用,如果想要停止 Diapatcher ,会把PoisonPill 喂给 receiver 吃,当threadpool 执行 MessageLoop 任务时, 吃到了毒药,马上退出,线程也就死掉了。PoisonPill命名很形象,关闭线程池的方式也是优雅的,是值得我们在工作中去学习和应用的。

从上面的成员变量分析部分可以知道,数据通过  postMessage  方法将 InboxMessage 数据 post 到 EndpointData的Inbox对象中,并将待处理的EndpointData 对象放入到 receivers 中,线程池会不断从这个队列中拿数据,分发数据。

引出Inbox

其实,data 就包含了 RpcEndpoint 和 RpcEndpointRef 对象,本可以在Dispatcher 中就可以调用 endpoint 的方法去处理。为什么还要设计出来一个 Inbox 层次的抽象呢?
下面我们就趁热剖析一下 Inbox 这个对象。

Inbox剖析

Inbox 的官方解释:
An inbox that stores messages for an RpcEndpoint and posts messages to it thread-safely.
其实就是它为RpcEndpoint 对象保存了消息,并且将消息 post给 RpcEndpoint,同时保证了线程的安全性。

类图如下:

跟 put 和 get 语义相似的有两个方法, 分别是post 和 process。其实这两个方法都是给 Dispatcher 对象调用的。post 将数据 存放到 堵塞消息队列队尾, pocess 则堵塞式 从消息队列中取出数据来,并处理之。

这两个关键方法源码如下:

 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
}
} /**
* Calls action closure, and calls the endpoint's onError function in the case of exceptions.
*/
private def safelyCall(endpoint: RpcEndpoint)(action: => Unit): Unit = {
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)
}
}
}
} /**
* Process stored messages.
*/
def process(dispatcher: Dispatcher): Unit = {
var message: InboxMessage = null
inbox.synchronized {
if (!enableConcurrent && numActiveThreads != 0) {
return
}
message = messages.poll()
if (message != null) {
numActiveThreads += 1
} else {
return
}
}
while (true) {
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)
}
} 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
}
}
}
}

其中,InboxMessage 继承关系如下:

这些InboxMessage子类型在process 方法源码中有体现。其中OneWayMessage和RpcMessage 都是自带消息content 的,其他的几种都是消息事件,本身不带任何除事件类型信息之外的信息。

在process 处理过程中,考虑到了 一次性批量处理消息问题、多线程安全问题、异常抛出问题,多消息分支处理问题等等。

此时可以回答上面我们的疑问了,抽象出来 Inbox 的原因在于,Diapatcher 的职责变得单一,只需要把数据分发就可以了。具体分发数据要如何处理的问题留给了 Inbox,Inbox 把关注点放在了 如何处理这些消息上。考虑并解决了 一次性批量处理消息问题、多线程安全问题、异常抛出问题,多消息分支处理问题等等问题。

Outbox

下面看一下Outbox, 它的内部构造和Inbox很类似,不再剖析。

OutboxMessage的继承关系如下:

其中,OneWayOutboxMessage 的行为是特定的。源码如下:

它没有回调方法。

RpcOutboxMessage 的回调则是通过构造方法传进来的。其源码如下:

RpcOutboxMessage 是有回调的,回调方法通过构造方法指定,内部onFailure和onSuccess是模板方法。

spark 源码分析之六--Spark RPC剖析之Dispatcher和Inbox、Outbox剖析的更多相关文章

  1. spark 源码分析之八--Spark RPC剖析之TransportContext和TransportClientFactory剖析

    spark 源码分析之八--Spark RPC剖析之TransportContext和TransportClientFactory剖析 TransportContext 首先官方文档对Transpor ...

  2. Spark源码分析之Spark Shell(下)

    继上次的Spark-shell脚本源码分析,还剩下后面半段.由于上次涉及了不少shell的基本内容,因此就把trap和stty放在这篇来讲述. 上篇回顾:Spark源码分析之Spark Shell(上 ...

  3. spark 源码分析之五 -- Spark内置RPC机制剖析之一创建NettyRpcEnv

    在前面源码剖析介绍中,spark 源码分析之二 -- SparkContext 的初始化过程 中的SparkEnv和 spark 源码分析之四 -- TaskScheduler的创建和启动过程 中的C ...

  4. Spark源码分析之六:Task调度(二)

    话说在<Spark源码分析之五:Task调度(一)>一文中,我们对Task调度分析到了DriverEndpoint的makeOffers()方法.这个方法针对接收到的ReviveOffer ...

  5. Spark源码分析之Spark Shell(上)

    终于开始看Spark源码了,先从最常用的spark-shell脚本开始吧.不要觉得一个启动脚本有什么东东,其实里面还是有很多知识点的.另外,从启动脚本入手,是寻找代码入口最简单的方法,很多开源框架,其 ...

  6. spark 源码分析之七--Spark RPC剖析之RpcEndPoint和RpcEndPointRef剖析

    RpcEndpoint 文档对RpcEndpoint的解释:An end point for the RPC that defines what functions to trigger given ...

  7. spark 源码分析之十二 -- Spark内置RPC机制剖析之八Spark RPC总结

    在spark 源码分析之五 -- Spark内置RPC机制剖析之一创建NettyRpcEnv中,剖析了NettyRpcEnv的创建过程. Dispatcher.NettyStreamManager.T ...

  8. spark 源码分析之十一--Spark RPC剖析之TransportClient、TransportServer剖析

    TransportClient类说明 先来看,官方文档给出的说明: Client for fetching consecutive chunks of a pre-negotiated stream. ...

  9. Spark 源码分析系列

    如下,是 spark 源码分析系列的一些文章汇总,持续更新中...... Spark RPC spark 源码分析之五--Spark RPC剖析之创建NettyRpcEnv spark 源码分析之六- ...

随机推荐

  1. git初学【常用命令、上传项目到码云或从码云拉取、克隆项目】

    1.下载git.https://git-scm.com/   注册码云:https://gitee.com/2.安装git:  默认安装即可:  安装完成之后打开git bash进行最后一步配置  输 ...

  2. 02、MySQL—数据库基本操作

    数据库是数据存储的最外层(最大单元) 1.创建数据库 基本语法:create database 数据库名字 [库选项]; 范例:使用create database 创建数据库 库选项:数据库的相关属性 ...

  3. Hadoop起步之图解SSH、免密登录原理和实现

    1. 前言 emmm….最近学习大数据,需要搭建Hadoop框架,当弄好linux系统之后,第一件事就是SSH免密登录的设置.对于SSH,我觉得使用过linux系统的程序员应该并不陌生.可是吧,用起来 ...

  4. PATB 1028. 人口普查(20)

    1028. 人口普查(20) 注意特判合理人数为0,否则格式错误.很暴力的sort排序找出最大最小. 时间限制 200 ms 内存限制 65536 kB 代码长度限制 8000 B 判题程序 Stan ...

  5. Java 8 新特性-Stream更优雅的处理集合入门

    Java 8 新特性之--Stream 一. 简单介绍 Stream是Java 8提出了的一种新的对集合对象功能的增强.它集合Lambda表达式,对集合提供了一些非常便利,高效的操作,使得代码具有非常 ...

  6. C# 画箭头

    绘制箭头   1,直接用平台库 Pen arrowPen = new Pen(Color.Blue);            arrowPen.Width = 4;            arrowP ...

  7. 【HDU - 1495】非常可乐

    -->非常可乐  Descriptions: 大家一定觉的运动以后喝可乐是一件很惬意的事情,但是seeyou却不这么认为.因为每次当seeyou买了可乐以后,阿牛就要求和seeyou一起分享这一 ...

  8. HTTP 学习笔记02

    请求行 格式:请求方式  资源路径  HTTP版本号 HTTP1.1 还需要 [host] 例:GET   /test.html   HTTP/1.0 GET   /test.html   HTTP/ ...

  9. HDU 6053:TrickGCD(莫比乌斯反演)

    题目链接 题意 给出n个数,问在这n个数里面,有多少组bi(1<=bi<=ai)可以使得任意两个bi不互质. 思路 想法就是枚举2~min(ai),然后去对于每个ai都去除以这些质数,然后 ...

  10. ElasticStack学习(四):ElasticSearch文档使用与操作

    一.文档的CRUD介绍 ElasticSearch中存在五种操作,分别如下: 1.Index 该操作表示:如果文档的ID不存在,则创建新的文档.若有相同的ID,先删除现有文档,然后再创建新的文档,同时 ...