前段时间研究了一下spark rpc这部分源码,现在来总结下,以免以后忘记,spark rpc代码比较复杂抽象,这里我就想到什么就写什么,可能逻辑顺序上不是很严谨,大家多见谅;说明下,这里源码的为2.x版本;
底层通信框架
spark在1.6版本之后底层通信框架用netty替代了actor;具体原因,查看了网上的文档,大多说开发中经常使用到actor框架,容易和spark中的actor版本冲突,所以使用netty取代;关于spark使用netty,个人感觉还是结合了scala语言偏函数的特性;在进程之间通信不需要在额外定义通信协议;说白了,就是说我随便定义一个case class就可以当做请求消息;只需要在服务端的接收入口函数中对请求进行模糊匹配,匹配到相应的请求处理分支就行
请求分类
- spark rpc中的请求分为两种,一种是只请求不需要返回值;另一种就是需要返回值(这里使用回调的方式获取返回值);
- spark使用全双工的通信方式;也就是说,服务端和客户端既可以接收消息,也可以发送请求
- spark进程之间通信,按距离划分,可以分为本地调用走的inbox处理分支;远程调用使用outbox处理分支(inbox和outbox后续会介绍)
通信环境以及重要组件
RpcEndpoint
RpcEndpoint可以理解为服务端接口,接收处理client发送过来的请求;例如standalone部署模式中的Master和Worker,都实现了RpcEndpoint接口;只不过他们是在不同节点启动的进程;
RpcEndpoint的生命周期:onStart -> receive(receiveAndReply)* -> onStop
这里我们以Master启动来具体说下Endpoint的生命周期
- Master启动
Master启动直接调用startRpcEnvAndEndpoint()这个方法
def startRpcEnvAndEndpoint(
host: String,
port: Int,
webUiPort: Int,
conf: SparkConf): (RpcEnv, Int, Option[Int]) = {
val securityMgr = new SecurityManager(conf)
val rpcEnv = RpcEnv.create(SYSTEM_NAME, host, port, conf, securityMgr)
val masterEndpoint = rpcEnv.setupEndpoint(ENDPOINT_NAME,
new Master(rpcEnv, rpcEnv.address, webUiPort, securityMgr, conf))
val portsResponse = masterEndpoint.askSync[BoundPortsResponse](BoundPortsRequest)
(rpcEnv, portsResponse.webUIPort, portsResponse.restPort)
}
Master启动后会调用onstart()方法,下面代码是onstart部分代码
checkForWorkerTimeOutTask = forwardMessageThread.scheduleAtFixedRate(new Runnable {
override def run(): Unit = Utils.tryLogNonFatalError {
self.send(CheckForWorkerTimeOut)
}
}, 0, WORKER_TIMEOUT_MS, TimeUnit.MILLISECONDS)
主要是检测所有注册到Master上的worker是否发生超时;之前说过sparkrpc分为本地调用和远程调用;self.send(CheckForWorkerTimeOut)这个就属于本地调用;master给自己发送CheckForWorkerTimeOut消息,Master接收消息使用receive这个方法,也就是生命周期中的第二部分
override def receive: PartialFunction[Any, Unit] = {
case ElectedLeader =>省略处理逻辑
case CompleteRecovery => completeRecovery()
case RevokedLeadership =>省略处理逻辑
case RegisterWorker(
//workerRef worker的引用,master与worker通行需要workerRef
id, workerHost, workerPort, workerRef, cores, memory, workerWebUiUrl, masterAddress) =>
//处理worker发送过来的application注册请求
case RegisterApplication(description, driver) =>
case ExecutorStateChanged(appId, execId, state, message, exitStatus) =>
case DriverStateChanged(driverId, state, exception) =>
case Heartbeat(workerId, worker) =>
case MasterChangeAcknowledged(appId) =>
case WorkerSchedulerStateResponse(workerId, executors, driverIds) =>
case WorkerLatestState(workerId, executors, driverIds) =>
// master收到worker发送的最近状态消息后,对worker上的Executor和driver的合法性进行判断,如果
// 存在executor和dirver不合法,也就是master的缓存中不存在,则通知worker进行kill操作
case UnregisterApplication(applicationId) =>
case CheckForWorkerTimeOut =>
timeOutDeadWorkers()
}
receive()这个方法使用case class这种匹配方式处理来自本地和远程发送的请求,这也是借助于scala的特性;可以看到Master接收到CheckForWorkerTimeOut 这个类别的消息,使用timeOutDeadWorkers()这个方法处理;master根据之前心跳包发送情况,把发生超时的worker从内存中移除(Master用HashSet存储所有注册到自身的worker)
private def timeOutDeadWorkers() {
// Copy the workers into an array so we don't modify the hashset while iterating through it
val currentTime = System.currentTimeMillis()
val toRemove = workers.filter(_.lastHeartbeat < currentTime - WORKER_TIMEOUT_MS).toArray
for (worker <- toRemove) {
if (worker.state != WorkerState.DEAD) {
logWarning("Removing %s because we got no heartbeat in %d seconds".format(
worker.id, WORKER_TIMEOUT_MS / 1000))
removeWorker(worker, s"Not receiving heartbeat for ${WORKER_TIMEOUT_MS / 1000} seconds")
} else {
if (worker.lastHeartbeat < currentTime - ((REAPER_ITERATIONS + 1) * WORKER_TIMEOUT_MS)) {
workers -= worker // we've seen this DEAD worker in the UI, etc. for long enough; cull it
}
}
}
}
Endpoint生命周期第三部分onStop,这个方法没啥好讲的,就是各种stop,以及资源释放
override def onStop() {
masterMetricsSystem.report()
applicationMetricsSystem.report()
// prevent the CompleteRecovery message sending to restarted master
if (recoveryCompletionTask != null) {
recoveryCompletionTask.cancel(true)
}
if (checkForWorkerTimeOutTask != null) {
checkForWorkerTimeOutTask.cancel(true)
}
forwardMessageThread.shutdownNow()
webUi.stop()
restServer.foreach(_.stop())
masterMetricsSystem.stop()
applicationMetricsSystem.stop()
persistenceEngine.close()
leaderElectionAgent.stop()
}
RpcEndpointRef
RpcEndpointRef可以理解为是Endpoint的引用,试想一下,我们如果想要和远程的Endpoint进行通信,那第一步就是获得远程调用的引用;之前讲解的Master检查worker超时使用的是本地调用,接下来我们用worker注册来讲解下EndpointRef。
worker只是集群中某一个节点上启动的一个jvm进程;worker启动后,第一件事就是向master汇报注册,通知master自己的存在,并接受master的调用管理;我们来看下worker启动的代码片段
rpcEnv.setupEndpoint(ENDPOINT_NAME, new Worker(rpcEnv, webUiPort, cores, memory,
masterAddresses, ENDPOINT_NAME, workDir, conf, securityMgr))
按照之前介绍的Endpoint的生命周期,worker启动后首先将会调用onStart()方法
override def onStart() {
//省略
//向Master注册
registerWithMaster()
//省略
}
onstart()方法中使用registerWithMaster() 向Master发送注册请求,具体方法调用链
onStart()->registerWithMaster()->tryRegisterAllMasters()->sendRegisterMessageToMaster()
获得masterEndpoint引用代码,和发送注册请求代码片段如下,大家自己沿着上面的方法调用链追踪下
val masterEndpoint = rpcEnv.setupEndpointRef(masterAddress, Master.ENDPOINT_NAME)
masterEndpoint.send(RegisterWorker(
workerId,
host,
port,
self,
cores,
memory,
workerWebUiUrl,
masterEndpoint.address)
调用NettyRpcEnv的setupEndpointRef获得MasterEndpoint的引用masterEndpoint ,这里传入masterAddress, Master.ENDPOINT_NAME两个参数;这个很好理解,你需要获得远程引用是不是得获得ip,以及引用的名字;
本地调用的话ip就是localhost;获得masterEndpointRef后,随后就给master发送RegisterWorker消息,接下来我们看下master是怎么处理的[Endpoint使用receive()方法处理接收到的请求(本地和远程)]
override def receive: PartialFunction[Any, Unit] = {
case ElectedLeader =>省略处理逻辑
case RegisterWorker(
id, workerHost, workerPort, workerRef, cores, memory, workerWebUiUrl, masterAddress) =>
if (state == RecoveryState.STANDBY) {
workerRef.send(MasterInStandby)
} else if (idToWorker.contains(id)) {
workerRef.send(RegisterWorkerFailed("Duplicate worker ID"))
} else {
val worker = new WorkerInfo(id, workerHost, workerPort, cores, memory,
workerRef, workerWebUiUrl)
if (registerWorker(worker)) {
//对worker进行持久化
persistenceEngine.addWorker(worker)
// 向worker发送注册完成的消息
workerRef.send(RegisteredWorker(self, masterWebUiUrl, masterAddress))
schedule()
} else {
val workerAddress = worker.endpoint.address
workerRef.send(RegisterWorkerFailed("Attempted to re-register worker at same address: "
+ workerAddress))
}
}
}
Master收到worker发送过来的注册请求后,首先会对worker信息进行持久化
persistenceEngine.addWorker(worker)
持久化的方式用很多种:内存,zookeeper和Filesystem等等,可以简单理解为master会将所有注册上来的worker信息(包括ip,workerid等)在自己能访问的地方保留一份,方便之后通信使用;持久化之后master需要告诉worker注册成功,所以master会给worker发送RegisteredWorker消息,到这里worker注册就结束了;
NettyRpcEnv
NettyRpcEnv是spark rpc通信集成环境,对netty框架进行封装,用户不需要过多的去考虑netty通信相关细节,只需要实现Endpoint即可,接下来我们来介绍下spark rpc是如何将netty集成到运行环境的,NettyRpcEnv的构建如下
// NettyRpcEnv
val rpcEnv = RpcEnv.create(SYSTEM_NAME, host, port, conf, securityMgr)
val masterEndpoint = rpcEnv.setupEndpoint(ENDPOINT_NAME,
new Master(rpcEnv, rpcEnv.address, webUiPort, securityMgr, conf))
rpcEnv.awaitTermination()
在每一个Endpoint启动的时候,首先都会去构建NettyRpcEnv,然后使用setupEndpoint将Endpoint注册到NettyRpcEnv中去(其实最终Endpoint会被注册到Dispatcher中去);有点扯远了,回到构建NettyRpcEnv;在NettyRpcEnv构造完后,如果之前conf中传入的不是config.clientMode,NettyRpcEnv就会启动netty服务,所有
EndPoint之间的远程调用都是通过netty,关于netty这里就不展开讲,内容太多了
def startServer(bindAddress: String, port: Int): Unit = {
val bootstraps: java.util.List[TransportServerBootstrap] =
if (securityManager.isAuthenticationEnabled()) {
java.util.Arrays.asList(new AuthServerBootstrap(transportConf, securityManager))
} else {
java.util.Collections.emptyList()
}
//创建TransportServer,启动netty服务
server = transportContext.createServer(bindAddress, port, bootstraps)
dispatcher.registerRpcEndpoint(
RpcEndpointVerifier.NAME, new RpcEndpointVerifier(this, dispatcher))
}
关于NettyRpcEnv你需要记住的是它集成了netty通信框架;除此之外,NettyRpcEnv还集成了Dispatcher,这个可以算是最核心的部分了,接下来我们着重讲下Dispatcher
Dispatcher
首先来看下Dispatcher都有些什么东西
private class EndpointData(
val name: String,
val endpoint: RpcEndpoint,
val ref: NettyRpcEndpointRef) {
val inbox = new Inbox(ref, endpoint)
}
private val endpoints: ConcurrentMap[String, EndpointData] =
new ConcurrentHashMap[String, EndpointData]
private val endpointRefs: ConcurrentMap[RpcEndpoint, RpcEndpointRef] =
new ConcurrentHashMap[RpcEndpoint, RpcEndpointRef]
private val receivers = new LinkedBlockingQueue[EndpointData]
private val threadpool: ThreadPoolExecutor
- 内部使用集合endpoints和endpointRefs维护Endpoint、EndpointRef,对外通过registerRpcEndpoint、removeRpcEndpointRef、getRpcEndpointRef等方法提供Endpoint注册删除和获取EndpointRef等服务。
- 利用EndpointData和Inbox结构完成消息的存储,Inbox可以理解为Endpoint的收件箱,每一个Endpoint都有一个Inbox,所有给Endpoint发送的请求都会才在Inbox的维护的队列中;inbox中接收的是本地调用发送的消息,也就是说远程调用发送的请求不会进入到inbox;关于远程发送的消息其实走的是outbox分支,随后会讲解
- 创建线程池threadpool,执行MessageLoop线程,消费消息。线程池里的线程用于处理一类Endpoint的Inbox中的消息
在构建NettyRpcEnv的时候,都会调用rpcEnv.awaitTermination(),其实这个方法调用的是Distpatcher的awaitTermination()
def awaitTermination(): Unit = {
threadpool.awaitTermination(Long.MaxValue, TimeUnit.MILLISECONDS)
}
Distpatcher调用此方法是会堵塞主线程,直至处理完所有Endpoint Inbox中的消息(消息怎么处理的完?其实就是相当于启动了一个服务,处理所有接收到的消息),讲到这里我们先总结下Distpatcher的作用:
Distpatcher维护了一批Endpoint,每个Endpoint都有一个Inbox,Inbox保存了这个Endpoint接收到的消息(本地调用发送的消息);Distpatcher在启动的时候,初始化了一个线程池threadpool,此线程池中的线程用于处理Endpoint Inbox中的消息
现在我们来看下消息是如何推送到Inbox,还是以之前master检测worker超时为例,画个图
Master启动后调用onstart()给自己发送一个CheckForWorkerTimeOut消息,发送消息使用的是NettyRpcEnv的send()方法,NettyRpcEnv首先会检查消息的地址remoteAddr,也就是这个消息是发送给谁的,发现该消息的remoteAddr和本地address是相同的,则把消息交由Dispatcher处理;Dispatcher接收到这个消息后,找到MasterEndpoint的Inbox,并把消息扔到Inbox中去;接下来threadpool中的线程发现Inbox中收到了消息,就调用Inbox.process()方法处理消息,到这里本地消息发送处理就结束了
接下来我们讲解下远程消息发送的流程,这里就以worker注册为例
worker启动后,会自动执行onstart()方法里的registerWithMaster();worker会根据master的IP地址和名字获得MasterEndpointRef,并向master发送RegisterWorker消息,这里也是使用NettyRpcEnv中的send方法,不同是,remoteAddress和本地address不相同,所以消息会放到Outbox中,并使用TransportClient向远程的MasterEndpoint发送请求注册请求;MasterEndpoint接收到消息后会直接调用Distpatcher的postRemoteMessage()方法,将收到的消息推送到Distpatcher维护的MasterEndpoint的Inbox中去,接下来Distpatcher线程池中的线程就会自动处理发送过来的消息[这里隐藏了netty通信的细节]
到这里spark rpc的基本流程也就基本写完里,写的比较笼统,sparkrpc的源码是非常值得去深入研究学习的,希望对大家学习有所帮助