Spark RPC 源码剖析(以 Executor 启动后向 Driver 注册为例)

Spark-1.6之前,Spark 的 RPC 是基于 Akaa 来实现的。Akka 是一个基于 scala 语言的异步的消息框架。Spark-1.6 后,Spark 借鉴 Akka 的设计自己实现了一个基于 Netty 的 rpc 框架。

类比理解:

Akka:
  ActorSystem + Actor + ActorRef
Spark RPC:
  RpcEnv + RpcEndpoint + RpcEndpointRef

总结:

  • RpcEndpoint 是一个可以响应请求的服务,和 Akka 中的 Actor 类似
    •  注册一个 endpoint 方法:rpcEnv.setupEndpoint
  • RpcEndpointRef 类似于 Akka 中 ActorRef。它是 RpcEndpoint 的引用,提供的方法 send 等同于!, ask 方法等同于?
    •  获取一个 endpoint 引用的方法:rpcEnv.setupEndpointRef
  • NettyRpcEnvFactory:创建 Rpc 服务端的工厂类
    • RPC Server 的具体实现类:TransportServer
  • TransportClientFactory:创建 RPC 客户端的工厂类
    • RPC 客户端的具体实现类:TransportClient
  • Spark RPC 使用了生产消费者模式
    • 要发送出去的消息放在了 outbox,调用 drainOutbox() 方法消息盒子的消息进行发送
    • 接收到的消息放在了 inbox,创建 NettyRpcEnv 时会创建 Dispatcher 对象,Dispatcher 对象里面有个 MessageLoop 线程不停从 inbox 里面取出消息进行处理


下面以 Executor 启动向 Driver 注册的过程分析 Spark RPC 的源码验证前面总结的结论。
Spark版本:Spark2.4.5 版本
Driver Executor 消息通信组件分别是: CoarseGrainedSchedulerBackendCoarseGrainedExecutorBackend


先看 Executor 的启动。Executor 的启动其实就是启动一个 java 进程,入口类是 CoarseGrainedExecutorBackend

org.apache.spark.executor.CoarseGrainedExecutorBackend#main
    org.apache.spark.executor.CoarseGrainedExecutorBackend#run
        // 设置 Executor endpoint。用于和 Driver 进行通信
        env.rpcEnv.setupEndpoint("Executor", new CoarseGrainedExecutorBackend(
        env.rpcEnv, driverUrl, executorId, hostname, cores, userClassPath, env))
        // CoarseGrainedExecutorBackend 继承了 RpcEndpoint, 会调用 onStart 方法。
        // 原因是创建 endpoint 时会创建对应的 inbox,inbox 在初始化时会先发送一条 OnStart 消息给自己。
        // 由 MessageLoop 消费到这条消息后调用具体的 onStart 方法。具体 inbox 和 MessageLoop 如何创建的可以等会看下面 Driver 启动时的流程
              // OnStart should be the first message to process。发送一条 OnStart 消息给自己
              inbox.synchronized {
                messages.add(OnStart)
              }
              
              case OnStart =>
                // 调用 endpoint 具体的 onStart 方法。这里就是调用 CoarseGrainedExecutorBackend 的 onStart 方法
                endpoint.onStart()
                
            org.apache.spark.executor.CoarseGrainedExecutorBackend#onStart
                // 获取 driver 的 endpoint 引用,用于向 driver 发送消息
                rpcEnv.asyncSetupEndpointRefByURI(driverUrl)
                
                    org.apache.spark.rpc.netty.NettyRpcEnv#asyncSetupEndpointRefByURI
                        // 创建 driver 的 endpointRef
                        val endpointRef = new NettyRpcEndpointRef(conf, addr, this)
                        // 通过 RpcEndpointVerifier 查看是否有存在 driver endpoint。存在则返回
                        val verifier = new NettyRpcEndpointRef(conf, RpcEndpointAddress(addr.rpcAddress, RpcEndpointVerifier.NAME), this)
                        verifier.ask[Boolean](RpcEndpointVerifier.CheckExistence(endpointRef.name))
                
                    
                    // 发送注册 executor 的消息给 driver
                    ref.ask[Boolean](RegisterExecutor(executorId, self, hostname, cores, extractLogUrls))
                        
                        org.apache.spark.rpc.netty.NettyRpcEndpointRef#ask
                        
                            org.apache.spark.rpc.netty.NettyRpcEnv#ask
                                // 创建一个 RpcOutboxMessage 对象。主要是序列化消息、以及设置成功失败的回调函数
                                val rpcMessage = RpcOutboxMessage(message.serialize(this), onFailure, (client, response) => onSuccess(deserialize[Any](client, response)))
                                // 把消息发到 outbox 中
                                postToOutbox(message.receiver, rpcMessage)
                                        

Outbox 处理消息的逻辑

org.apache.spark.rpc.netty.NettyRpcEnv#postToOutbox
1、判断 outbox 是否存在,不存在创建一个
val targetOutbox = if (outbox == null) {
	val newOutbox = new Outbox(this, receiver.address)
}
2、往 targetOutbox 的队列塞消息
targetOutbox.send(message)

	org.apache.spark.rpc.netty.Outbox#send
		// 往阻塞队列放消息
		private val messages = new java.util.LinkedList[OutboxMessage]
		messages.add(message)
		// 从队列取出消息
		drainOutbox()
		
			org.apache.spark.rpc.netty.Outbox#drainOutbox
				1、当远程连接未建立时,会先建立连接,然后去消化OutboxMessage
				if (client == null) {
					// There is no connect task but client is null, so we need to launch the connect task.
					launchConnectTask()
						val _client = nettyEnv.createClient(address)
							clientFactory.createClient(address.host, address.port)
								org.apache.spark.network.client.TransportClientFactory#createClient(java.lang.String, int)
								// 创建一个 TransportClient 对象,创建成功放到池子里面
								clientPool.clients[clientIndex] = createClient(resolvedAddress);
									
								// 这里就是 netty 创建客户端的各种代码了,最后返回一个 TransportClient 对象,所以 Spark RPC 客户端的具体实现类是 TransportClient
								org.apache.spark.network.client.TransportClientFactory#createClient(java.net.InetSocketAddress)
									// netty 相关代码
									Bootstrap bootstrap = new Bootstrap();
									bootstrap.group(workerGroup)
									  .channel(socketChannelClass)
									  // Disable Nagle's Algorithm since we don't want packets to wait
									  .option(ChannelOption.TCP_NODELAY, true)
									  .option(ChannelOption.SO_KEEPALIVE, true)
									  .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, conf.connectionTimeoutMs())
									  .option(ChannelOption.ALLOCATOR, pooledAllocator);
				}
				2、获取到远程连接后,取出消息
				message = messages.poll()
				3、发送消息
				val _client = synchronized { client }
				if (_client != null) {
				  message.sendWith(_client)
					this.requestId = client.sendRpc(content, this)
						org.apache.spark.network.client.TransportClient#sendRpc
							// 调用 netty 的 API 发送消息出去
							channel.writeAndFlush(new RpcRequest(requestId, new NioManagedBuffer(message))).addListener(listener);
				}


至此。Executor 启动并向 Driver 发送 RegisterExecutor 消息的代码已经走完

下面看 Driver 启动并消费处理 RegisterExecutor 消息的代码
 

Spark Driver 启动程序入口:

// 创建 SparkSession
    org.apache.spark.sql.SparkSession.Builder#getOrCreate
        // 创建 SparkContext
        SparkContext.getOrCreate(sparkConf)
            setActiveContext(new SparkContext(config), allowMultipleContexts = false)
                // 创建 SparkContext 会创建 Spark Env
                _env = createSparkEnv(_conf, isLocal, listenerBus)
                    SparkEnv.createDriverEnv(conf, isLocal, listenerBus, SparkContext.numDriverCores(master, conf))
                        org.apache.spark.SparkEnv#create
                            val rpcEnv = RpcEnv.create(systemName, bindAddress, advertiseAddress, port.getOrElse(-1), conf,
                                securityManager, numUsableCores, !isDriver)
                                org.apache.spark.rpc.RpcEnv#create
                                    // 创建 NettyRpcEnv
                                    new NettyRpcEnvFactory().create(config)
                                    
                                        org.apache.spark.rpc.netty.NettyRpcEnvFactory#create
                                        
                                            val nettyEnv =
                                              new NettyRpcEnv(sparkConf, javaSerializerInstance, config.advertiseAddress,
                                                config.securityManager, config.numUsableCores)
                                                // 这里会创建一个比较重要的对象 Dispatcher。这个对象是用来分配消息处理的
                                                private val dispatcher: Dispatcher = new Dispatcher(this, numUsableCores)
                                                
                                            // 启动 RPC Server
                                            nettyEnv.startServer(config.bindAddress, actualPort)
                                                // 创建 TransportServer
                                                server = transportContext.createServer(bindAddress, port, bootstraps)
                                                
                                                    org.apache.spark.network.server.TransportServer#TransportServer
                                                        // 这里全都是 netty 相关的代码了,比如创建 boosgroup 和 workerGroup,这里主要看下 pipeline 处理消息部分
                                                        init(hostToBind, portToBind);
                                                            // 设置 pipeline
                                                            context.initializePipeline(ch, rpcHandler);
                                                            
                                                                // 这里设置了各种 channel handler,比如编码、解码
                                                                    /**
                                                                     * Initializes a client or server Netty Channel Pipeline which encodes/decodes messages and
                                                                     * has a {@link org.apache.spark.network.server.TransportChannelHandler} to handle request or
                                                                     * response messages.
                                                                     */
                                                                org.apache.spark.network.TransportContext#initializePipeline(io.netty.channel.socket.SocketChannel, org.apache.spark.network.server.RpcHandler)
                                                                TransportChannelHandler channelHandler = createChannelHandler(channel, channelRpcHandler);
                                                                
                                                                    // 消息接收处理。TransportChannelHandler 会把消息放到 inbox 里面,之后从 inbox 里面取出消息调用具体 endpoint 处理消息的方法进行消息处理
                                                                    TransportRequestHandler requestHandler = new TransportRequestHandler(channel, client, rpcHandler, conf.maxChunksBeingTransferred());
                                                                    return new TransportChannelHandler(client, responseHandler, requestHandler, conf.connectionTimeoutMs(), closeIdleConnections);    

上面是 Driver RPC Env 和 RPC Server 创建启动流程

创建 SparkContext 时同时还会启动 CoarseGrainedSchedulerBackend 和创建 Driver Endpoint

// SparkContext 构造时会创建很多组件,其中 CoarseGrainedSchedulerBackend 是用来与其他组件负责消息通信的
// Create and start the scheduler。这里讲的是生成常用的 yarn 集群模式,所以是 master 的值是 yarn
val (sched, ts) = SparkContext.createTaskScheduler(this, master, deployMode)
	// 这里的 cm 是 YarnClusterManager
	val cm = getClusterManager(masterUrl)
		// 创建 TaskScheduler:YarnClusterScheduler 的父类是 TaskSchedulerImpl
		val scheduler = cm.createTaskScheduler(sc, masterUrl)
		case "cluster" => new YarnClusterScheduler(sc)
		
		// 创建 SchedulerBackend:YarnClusterSchedulerBackend 的父类是 CoarseGrainedSchedulerBackend
		val backend = cm.createSchedulerBackend(sc, masterUrl, scheduler)
			org.apache.spark.scheduler.cluster.YarnClusterManager#createSchedulerBackend
				case "cluster" =>
					new YarnClusterSchedulerBackend(scheduler.asInstanceOf[TaskSchedulerImpl], sc)
					
				
_schedulerBackend = sched
_taskScheduler = ts
_taskScheduler.start()
	// 调用 YarnClusterScheduler 父类的 start
	org.apache.spark.scheduler.TaskSchedulerImpl#start
		backend.start()
			// 调用 YarnClusterSchedulerBackend 父类的 start
			org.apache.spark.scheduler.cluster.CoarseGrainedSchedulerBackend#start
				// driver endpoint 就是 driver 跟其他组件消息通信的
				driverEndpoint = createDriverEndpointRef(properties)
					rpcEnv.setupEndpoint(ENDPOINT_NAME, createDriverEndpoint(properties))
						// 创建 driver endpoint
						org.apache.spark.scheduler.cluster.CoarseGrainedSchedulerBackend#createDriverEndpoint
							new DriverEndpoint(rpcEnv, properties)
					
					
						// 注册 driver endpoint
						dispatcher.registerRpcEndpoint(name, endpoint)
							if (endpoints.putIfAbsent(name, new EndpointData(name, endpoint, endpointRef)) != null) {
								throw new IllegalArgumentException(s"There is already an RpcEndpoint called $name")
							}
				
								new EndpointData(name, endpoint, endpointRef)
									// 创建 inbox 对象,跟 endpoint 绑定
									val inbox = new Inbox(ref, endpoint)
										// 存储消息的列表,消息会先存放到 list 中,会有线程从这里取出消息进行消费
										protected val messages = new java.util.LinkedList[InboxMessage]()


以上就是 Driver 启动创建 Driver endpoint 的流程,下面看实际消费消息并进行逻辑处理的代码

可以看到创建 TransportServer 时,会创建 TransportChannelHandlerTransportChannelHandler 继承了 ChannelInboundHandlerAdapter 类,熟悉 netty 的都知道消息过来后会调用 channelRead 方法处理消息,channelRead 就是接收消息并将消息放到 inbox 中等待处理的地方

org.apache.spark.network.server.TransportChannelHandler#channelRead
    // 判断 message 类型,executor 发送注册消息,这里是请求类型
    if(request instanceof RequestMessage) {
        requestHandler.handle((RequestMessage) request);
    }
    
        org.apache.spark.network.server.TransportRequestHandler#handle
        // RegisterExecutor 是一个 RpcRequest,所以走这个分支
        if (request instanceof RpcRequest) {
          processRpcRequest((RpcRequest) request);
        }
        
            org.apache.spark.network.server.TransportRequestHandler#processRpcRequest
            rpcHandler.receive(reverseClient, req.body().nioByteBuffer(), new RpcResponseCallback() {}
            
                org.apache.spark.rpc.netty.NettyRpcHandler#receive
                
                    org.apache.spark.rpc.netty.Dispatcher#postRemoteMessage
                        // 组装成一个 RpcMessage
                        val rpcMessage = RpcMessage(message.senderAddress, message.content, rpcCallContext)
                        // 分发消息
                        postMessage(message.receiver.name, rpcMessage, (e) => callback.onFailure(e))
                        
                        // Posts a message to a specific endpoint.
                        org.apache.spark.rpc.netty.Dispatcher#postMessage
                        
                            // 创建 EndpointData 时会往 endpoints 里面 put。所以这里能拿到对应的 endpoint 和 inbox
                            val data = endpoints.get(endpointName)
                            // 往对应 endpoint 的 inbox 塞入数据,下一步再通知对应的 endpoint 进行处理
                            data.inbox.post(message)
                                messages.add(message)
                            // 通知对应的 endpoint,塞到 receivers 后就会被 MessageLoop 线程拿出来处理
                            receivers.offer(data)

放到 inbox 后,由 dispatcher 负责处理消息。创建 dispatcher 时会创建 MessageLoop.

MessageLoop 会不断从 receivers 这个阻塞队列拿出待处理的 inbox,调用它的 process 方法来处理消息
下面是初始化 dispatcher 对象时做的事情,以及 MessageLoop 最终如何处理消息的代码

1、创建成员变量:receivers
// Track the receivers whose inboxes may contain messages. 用来跟踪有哪些 endpoint 来消息了
private val receivers = new LinkedBlockingQueue[EndpointData]

2、消费消息的线程池,开启多个 MessageLoop 线程
/** Thread pool used for dispatching messages. */
private val threadpool: ThreadPoolExecutor = {
	val availableCores = if (numUsableCores > 0) numUsableCores else Runtime.getRuntime.availableProcessors()
	val numThreads = nettyEnv.conf.getInt("spark.rpc.netty.dispatcher.numThreads", math.max(2, availableCores))
	
	/**
	 *  注释: 启动 numThreads 个线程用来运行: MessageLoop
	 */
	val pool = ThreadUtils.newDaemonFixedThreadPool(numThreads, "dispatcher-event-loop")
	for (i <- 0 until numThreads) {
		pool.execute(new MessageLoop)
	}
	pool
}

3、MessageLoop 线程的 run 方法:
	while(true) {
		val data = receivers.take()
		data.inbox.process(Dispatcher.this)
			org.apache.spark.rpc.netty.Inbox#process
				// 从 messages list 对象取出消息
				message = messages.poll()
				// 判断消息属于哪种类型
				message match {
				  case RpcMessage(_sender, content, context) =>
					try {
					  // 调用 endpoint 具体实现的方法进行消息处理。receiveAndReply 方法不同 endpoint 有不同的实现。
					  // 这里会调用 DriverEndpoint 的 receiveAndReply 方法
					  endpoint.receiveAndReply(context).applyOrElse[Any, Unit](content, { msg =>
						throw new SparkException(s"Unsupported message $message from ${_sender}")
					  })
					}
				}
	}
	
4、DriverEndpoint 具体的 receiveAndReply 方法
org.apache.spark.scheduler.cluster.CoarseGrainedSchedulerBackend.DriverEndpoint#receiveAndReply
  case RegisterExecutor(executorId, executorRef, hostname, cores, logUrls) => {
	// 主要是往各种内存对象中存入过来注册的 executor 的信息
	// If the executor's rpc env is not listening for incoming connections, `hostPort`
	  // will be null, and the client connection should be used to contact the executor.
	  val executorAddress = if (executorRef.address != null) {
		  executorRef.address
		} else {
		  context.senderAddress
		}
	  logInfo(s"Registered executor $executorRef ($executorAddress) with ID $executorId")
	  addressToExecutorId(executorAddress) = executorId
	  totalCoreCount.addAndGet(cores)
	  totalRegisteredExecutors.addAndGet(1)
	  
	  // 给 executor 回注册成功的消息。executor 收到消息后的具体逻辑以此类推可以到 CoarseGrainedExecutorBackend 类中搜索 RegisteredExecutor
	  executorRef.send(RegisteredExecutor)
  }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值