熟悉的套路,先大概的了解spark的通讯架构怎么样工作,然后再去跟踪源码。
Spark2.x版本使用Netty通讯框架作为内部通讯组件。
Spark通讯框架中各个组件(Client/Master/Worker)可以认为是一个个独立的实体,各个实体之间通过消息来进行通信,如图:
Endpoint(Client/Master/Worker)有1个InBox和N个OutBox(N>=1,N取决于当前Endpoint与多少其他的Endpoint进行通信,一个与其通讯的其他Endpoint对应一个OutBox),Endpoint接收到的消息被写入InBox,发送出去的消息写入OutBox并被发送到其他Endpoint的InBox中。
上一个更详细的Spark通信架构图,更清楚的理解spark组件之间是怎样通信的,只有看懂了这张图,有了大致的框架,有助于我们跟踪源码;
RpcEndpoint:RPC端点,Spark针对每个节点(Client/Master/Worker)都称之为一个Rpc端点,且都实现RpcEndpoint接口,内部根据不同端点的需求,设计不同的消息和不同的业务处理,如果需要发送(询问)则调用Dispatcher;
RpcEnv:RPC上下文环境,每个RPC端点运行时依赖的上下文环境称为RpcEnv;
Dispatcher:消息分发器,针对于RPC端点需要发送消息或者从远程RPC接收到的消息,分发至对应的指令收件箱/发件箱。如果指令接收方是自己则存入收件箱,如果指令接收方不是自己,则放入发件箱;
Inbox:指令消息收件箱,一个本地RpcEndpoint对应一个收件箱,Dispatcher在每次向Inbox存入消息时,都将对应EndpointData加入内部ReceiverQueue中,另外Dispatcher创建时会启动一个单独线程进行轮询ReceiverQueue,进行收件箱消息消费;
RpcEndpointRef:RpcEndpointRef是对远程RpcEndpoint的一个引用。当我们需要向一个具体的RpcEndpoint发送消息时,一般我们需要获取到该RpcEndpoint的引用,然后通过该应用发送消息。
OutBox:指令消息发件箱,对于当前RpcEndpoint来说,一个目标RpcEndpoint对应一个发件箱,如果向多个目标RpcEndpoint发送信息,则有多个OutBox。当消息放入Outbox后,紧接着通过TransportClient将消息发送出去。消息放入发件箱以及发送过程是在同一个线程中进行;
RpcAddress:表示远程的RpcEndpointRef的地址,Host + Port。
TransportClient:Netty通信客户端,一个OutBox对应一个TransportClient,TransportClient不断轮询OutBox,根据OutBox消息的receiver信息,请求对应的远程TransportServer;
TransportServer:Netty通信服务端,一个RpcEndpoint对应一个TransportServer,接受远程消息后调用Dispatcher分发消息至对应收发件箱;
当我了解了spark的通信架构后,就可以开始阅读源码了。但是切入点在那呢?我们可以接着上次的源码分析。在RPC上下文环境环境中设置Excutor的通信端点开始。
CoarseGrainedExecutorBackend
main{
env.rpcEnv.setupEndpoint("Executor", new CoarseGrainedExecutorBackend(
env.rpcEnv, driverUrl, executorId, hostname, cores, userClassPath, env))
}
在上一篇文章我们关注的是CoarseGrainedExecutorBackend这个类的创建。但是没有关注setupEndpoint这个方法。所以我们点进去看会发现是RpcEnv抽象类的一个抽象方法,没有实现,所以必须去找这个类的子类。
private[spark] abstract class RpcEnv(conf: SparkConf) {
def setupEndpoint(name: String, endpoint: RpcEndpoint): RpcEndpointRef
}
idea按F4,会出现NettyRpcEnv;没错这个就要找的子类,紧接着找到setupEndpoint的实现。
private[netty] class NettyRpcEnv{
override def setupEndpoint(name: String, endpoint: RpcEndpoint): RpcEndpointRef = {
dispatcher.registerRpcEndpoint(name, endpoint)
}
}
突然会发现一个熟悉的字眼dispatcher,没错这就是我们的消息分发器,没话说看看它里面是什么东西。
private[netty] class Dispatcher(nettyEnv: NettyRpcEnv) extends Logging {
// 封装了数据、端点和引用
private class EndpointData(
val name: String,
val endpoint: RpcEndpoint,
val ref: NettyRpcEndpointRef) {
val inbox = new Inbox(ref, endpoint)
}
// 注册Executor的rpc端点
def registerRpcEndpoint(name: String, endpoint: RpcEndpoint): NettyRpcEndpointRef = {
// 封装RpcEndpointRef的地址,Host + Port
val addr = RpcEndpointAddress(nettyEnv.address, name)
// 创建一个RpcEndpoint的一个引用
val endpointRef = new NettyRpcEndpointRef(nettyEnv.conf, addr, nettyEnv)
synchronized {
if (stopped) {
throw new IllegalStateException("RpcEnv has been stopped")
}
// endpoints结构是 ConcurrentMap[String, EndpointData]
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)
// endpointRefs结构也是ConcurrentMap[RpcEndpoint, RpcEndpointRef],一个rpc端点对应一个rpc端点的引用
endpointRefs.put(data.endpoint, data.ref)
// private val receivers = new LinkedBlockingQueue[EndpointData] receiver是个阻塞队列,将data放入队列中就会有线程来取数据运行
receivers.offer(data) // for the OnStart message
}
endpointRef
}
}
通过源码看看到最后将OnStart message放入队列中,所以最后会处理OnStart message这条消息。究竟在那处理,处理了什么东西?其实就是上一篇文章中的CoarseGrainedExecutorBackend中的OnStart方法。
private[spark] class CoarseGrainedExecutorBackend() extends ThreadSafeRpcEndpoint{
// 由于该类继承了Rpc端点,所以该对象的生命周期是 constructor(创建) -> onStart(启动) -> receive*(接收消息) -> onStop(停止)
// 我们所说的Executor就是CoarseGrainedExecutorBackend中的一个属性对象
var executor: Executor = null
override def onStart() {
//向Driver反向注册
driver = Some(ref)
ref.ask[Boolean](RegisterExecutor(executorId, self, hostname, cores, extractLogUrls))
}
override def receive: PartialFunction[Any, Unit] = {
// 收到Driver注册成功的消息
case RegisteredExecutor =>
// 创建计算对象Executor
executor = new Executor(executorId, hostname, env, userClassPath, isLocal = false)
// 收到Driver端发送过来的task
case LaunchTask(data) =>
// 由executor对象调用方法运行
executor.launchTask(this, taskId = taskDesc.taskId, attemptNumber = taskDesc.attemptNumber,taskDesc.name, taskDesc.serializedTask)
}
}
看到这里会产生一个疑问,onStart方法里面调用ref.ask()向Driver反向注册的消息,究竟谁来接收这个消息,怎么处理的?
既然是向Drive注册,那么就应该去找Driver,而Driver就是用户创建SparkContent的那段程序,所以我们就可以去SparkContent里面找。
class SparkContext(config: SparkConf) extends Logging {
// 没错消息就是发给它了
private var _schedulerBackend: SchedulerBackend = _
}
接着我们就迫不及待的去看SchedulerBackend,不过它是个接口找到它的子类CoarseGrainedSchedulerBackend,看到这个类,是不是有种相识的感觉,CoarseGrainedExecutorBackend,说到底就是这两个对象在交互,明白了。
class CoarseGrainedSchedulerBackend(scheduler: TaskSchedulerImpl, val rpcEnv: RpcEnv){
override def receiveAndReply(context: RpcCallContext): PartialFunction[Any, Unit] = {
// 匹配反向注册消息
case RegisterExecutor(executorId, executorRef, hostname, cores, logUrls) =>
// 总的核心数要加上Executor注册的核心数
totalCoreCount.addAndGet(cores)
// Executor的数量加1
totalRegisteredExecutors.addAndGet(1)
// 注册成功的消息
executorRef.send(RegisteredExecutor)
}
}
到此spark的通信架构就了解怎么多,更多具体的就不去深入了。
Driver端用户交互的是SchedulerBackend,Executor端用户交互的是ExecutorBackend。
码字不易,还请点波关注/赞;