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 消息通信组件分别是: CoarseGrainedSchedulerBackend 和 CoarseGrainedExecutorBackend
先看 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 时,会创建 TransportChannelHandler。TransportChannelHandler 继承了 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)
}