[Spark内核]通讯架构源码解析

个人博客文章地址

熟悉的套路,先大概的了解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组件之间是怎样通信的,只有看懂了这张图,有了大致的框架,有助于我们跟踪源码;
在这里插入图片描述

  1. RpcEndpoint:RPC端点,Spark针对每个节点(Client/Master/Worker)都称之为一个Rpc端点,且都实现RpcEndpoint接口,内部根据不同端点的需求,设计不同的消息和不同的业务处理,如果需要发送(询问)则调用Dispatcher;

  2. RpcEnv:RPC上下文环境,每个RPC端点运行时依赖的上下文环境称为RpcEnv;

  3. Dispatcher:消息分发器,针对于RPC端点需要发送消息或者从远程RPC接收到的消息,分发至对应的指令收件箱/发件箱。如果指令接收方是自己则存入收件箱,如果指令接收方不是自己,则放入发件箱;

  4. Inbox:指令消息收件箱,一个本地RpcEndpoint对应一个收件箱,Dispatcher在每次向Inbox存入消息时,都将对应EndpointData加入内部ReceiverQueue中,另外Dispatcher创建时会启动一个单独线程进行轮询ReceiverQueue,进行收件箱消息消费;

  5. RpcEndpointRef:RpcEndpointRef是对远程RpcEndpoint的一个引用。当我们需要向一个具体的RpcEndpoint发送消息时,一般我们需要获取到该RpcEndpoint的引用,然后通过该应用发送消息。

  6. OutBox:指令消息发件箱,对于当前RpcEndpoint来说,一个目标RpcEndpoint对应一个发件箱,如果向多个目标RpcEndpoint发送信息,则有多个OutBox。当消息放入Outbox后,紧接着通过TransportClient将消息发送出去。消息放入发件箱以及发送过程是在同一个线程中进行;

  7. RpcAddress:表示远程的RpcEndpointRef的地址,Host + Port。

  8. TransportClient:Netty通信客户端,一个OutBox对应一个TransportClient,TransportClient不断轮询OutBox,根据OutBox消息的receiver信息,请求对应的远程TransportServer;

  9. 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

码字不易,还请点波关注/赞;

发布了95 篇原创文章 · 获赞 64 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/qq_43115606/article/details/105010015