Spark消息通信架构
在Sparkd定义了通信框架接口,这些接口实现中调用N etty的具体方法(Spark 2.0版本之前使用的是Akka)。RPC组件之间的关系如图所示:
在框架中以RpcEndpoint和RpcEndpointRef实现了Actor和ActorRef相关动作(具体可以查阅Akka相关资料),其中RpcEndpointRef是RpcEndpoint的引用,在消息通信中消息发送方持有引用RpcEndpointRef.
通信框架中使用了工厂设计模式实现(文末献上鄙人对工厂模式的简单理解),这种设计方式实现了对Netty的解藕,能够根据需要引入其他的消息通信工具。
Spark启动消息通信
Spark启动过程中主要是进行Master与Worker之间的通信。首先是worker向Master发送注册,Master处理完成后,返回注册成功或注册失败消息,如果注册成功Worker定时发送心跳给Master。其关系图如下。
在各个模块中,如Master、Worker等,会先使用RpcEnv的静态方法创建RpcEnv实例,然后实例化Master,由于Master继承于ThreadSafeRpcEndpoint方法,创建的Master实例是一个线程安全的终端点,接着调用RpcEnv启动终端点方法,把master的终端点和其对应的引用注册到RpcEnv中。在消息通信中,其他对象只要获取了Master终端点的引用,就能够发送消息给Master进行通信。
Master实现
master的定义
源码如下:
private[deploy] class Master(
override val rpcEnv: RpcEnv,
address: RpcAddress,
webUiPort: Int,
val securityMgr: SecurityManager,
val conf: SparkConf)
extends ThreadSafeRpcEndpoint with Logging with LeaderElectable {
......
}
// 进一步定位
private[spark] trait ThreadSafeRpcEndpoint extends RpcEndpoint
可以看到master类继承自ThreadSafeRpcEndpoint,进一步定位可以发现ThreadSafeRpcEndpoint是继承自RpcEndpoint特质。
master启动
通过源码看看master是如何启动的
/**
* Start the Master and return a three tuple of:
* (1) The Master RpcEnv
* (2) The web UI bound port
* (3) The REST server bound port, if any
*/
def startRpcEnvAndEndpoint(
host: String,
port: Int,
webUiPort: Int,
conf: SparkConf): (RpcEnv, Int, Option[Int]) = {
val securityMgr = new SecurityManager(conf)
//注册rpcEnv
val rpcEnv = RpcEnv.create(SYSTEM_NAME, host, port, conf, securityMgr)
// 将master自身加入到上面创建的rpcEnv中
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是通过调用startRpcEnvAndEndpoint方法启动的,在这个方法里会创建一个rpcEnv,并将Master的终端点和其对应的引用注册到刚刚创建的rpcEnv中,最后将其返回。
RpcEnv抽象类
private[spark] abstract class RpcEnv(conf: SparkConf) {
private[spark] val defaultLookupTimeout = RpcUtils.lookupRpcTimeout(conf)
//返回endpointRef
private[rpc] def endpointRef(endpoint: RpcEndpoint): RpcEndpointRef
//返回RpcEnv监听的地址
def address: RpcAddress
//注册一个RpcEndpoint到RpcEnv并返回RpcEndpointRef
def setupEndpoint(name: String, endpoint: RpcEndpoint): RpcEndpointRef
//通过uri异步地查询RpcEndpointRef
def asyncSetupEndpointRefByURI(uri: String): Future[RpcEndpointRef]
//通过uri查询RpcEndpointRef,这种方式会产生阻塞
def setupEndpointRefByURI(uri: String): RpcEndpointRef = {
defaultLookupTimeout.awaitResult(asyncSetupEndpointRefByURI(uri))
}
//通过address和endpointName查询RpcEndpointRef,这种方式会产生阻塞
def setupEndpointRef(address: RpcAddress, endpointName: String): RpcEndpointRef = {
setupEndpointRefByURI(RpcEndpointAddress(address, endpointName).toString)
}
//关掉endpoint
def stop(endpoint: RpcEndpointRef): Unit
//关掉RpcEnv
def shutdown(): Unit
//等待结束
def awaitTermination(): Unit
//没有RpcEnv的话RpcEndpointRef是无法被反序列化的,这里是反序列化逻辑
def deserialize[T](deserializationAction: () => T): T
//返回文件server实例
def fileServer: RpcEnvFileServer
//开一个针对给定URI的channel用来下载文件
def openChannel(uri: String): ReadableByteChannel
}
可以看下startRpcEnvAndEndpoint创建rpcEnv的代码:
private[spark] object RpcEnv {
def create(
name: String,
host: String,
port: Int,
conf: SparkConf,
securityManager: SecurityManager,
clientMode: Boolean = false): RpcEnv = {
val config = RpcEnvConfig(conf, name, host, port, securityManager, clientMode)
new NettyRpcEnvFactory().create(config)
}
}
这就是在master启动方法中的create具体实现,可以看到调用了Netty工厂方法NettyRpcEnvFactory,该方法是对Netty的具体封装。
RpcEndpoint
上面已经说了master的启动会创建一个RpcEnv并将自己注册到其中,继续看下Rpcendpoint
al rpcEnv: RpcEnv
//直接用来发送消息的RpcEndpointRef,可以类比为Akka中的actorRef
final def self: RpcEndpointRef = {
require(rpcEnv != null, "rpcEnv has not been initialized")
rpcEnv.endpointRef(this)
}
//处理来自RpcEndpointRef.send或者RpcCallContext.reply的消息
def receive: PartialFunction[Any, Unit] = {
case _ => throw new SparkException(self + " does not implement 'receive'")
}
//处理来自RpcEndpointRef.ask的消息,会有相应的回复
def receiveAndReply(context: RpcCallContext): PartialFunction[Any, Unit] = {
case _ => context.sendFailure(new SparkException(self + " won't reply anything"))
}
//篇幅限制,其余onError,onConnected,onDisconnected,onNetworkError,
//onStart,onStop,stop方法此处省略
}
Worker的定义
worker和master的定义差不多在此不在做过多的说明。
Master和Worker之间的通信
当Master启动之后,随之启动各个Worker, Worker启动时会创建通信环境RpcEnv和终端点Endpoint,并向Master发送注册Worker的消息RegisterWorker.
由于worker可能需要注册到多个Master中(如HA环境),在Worker的tryRegisterAllMasters方法中创建注册线程池registerMasterThreadPool,把需要申请的请求放到该线程池中,然后通过线程池启动注册线程。在该注册中,获取Master终端点引用,接着调用sendRegisterMessageToMaster,向Master发送注册信息。
Worker.tryRegisterAllMasters方法代码如下:
private def tryRegisterAllMasters(): Array[JFuture[_]] = {
masterRpcAddresses.map { masterAddress =>
registerMasterThreadPool.submit(new Runnable {
override def run(): Unit = {
try {
logInfo("Connecting to master " + masterAddress + "...")
// 获取master终端点引用
val masterEndpoint = rpcEnv.setupEndpointRef(masterAddress, Master.ENDPOINT_NAME)
// 发送注册信息
sendRegisterMessageToMaster(masterEndpoint)
} catch {
case ie: InterruptedException => // Cancelled
case NonFatal(e) => logWarning(s"Failed to connect to master $masterAddress", e)
}
}
})
}
}
其中sendRegisterMessageToMaster方法如下:
private def sendRegisterMessageToMaster(masterEndpoint: RpcEndpointRef): Unit = {
masterEndpoint.send(RegisterWorker(
workerId,
host,
port,
self,
cores,
memory,
workerWebUiUrl,
masterEndpoint.address,
resources))
}
Master收到消息之后,需要对Worker发送的信息进行验证、记录。如果注册成功,则发送RegisterWorker消息给对应worker,告诉Worker已经完成注册,随之进行步骤3,即Worker定期发送心跳信息给Master;如果注册失败则会发送RegisterWorkerFaild消息,Work打印错误日志并结束Worker启动。
当Master接收到Worker注册消息后,先判断Master当前状态是否处于STANDBY状态,如果是则忽略该消息,如果注册列表中发现了该Worker的编号,则发送注册失败的消息。Master.receive方法中注册Worker代码实现如下所示:
case RegisterWorker(
id, workerHost, workerPort, workerRef, cores, memory, workerWebUiUrl,
masterAddress, resources) =>
logInfo("Registering worker %s:%d with %d cores, %s RAM".format(
workerHost, workerPort, cores, Utils.megabytesToString(memory)))
//Master处于STANDBY状态,返回Master处于STANDBY状态消息
if (state == RecoveryState.STANDBY) {
workerRef.send(MasterInStandby)
} else if (idToWorker.contains(id)) {
workerRef.send(RegisteredWorker(self, masterWebUiUrl, masterAddress, true))
} else {
val workerResources = resources.map(r => r._1 -> WorkerResourceInfo(r._1, r._2.addresses))
//registerWorker方法中注册Worker,该方法中会把worker放到列表中
val worker = new WorkerInfo(id, workerHost, workerPort, cores, memory,
workerRef, workerWebUiUrl, workerResources)
if (registerWorker(worker)) {
persistenceEngine.addWorker(worker)
workerRef.send(RegisteredWorker(self, masterWebUiUrl, masterAddress, false))
schedule()
} else {
val workerAddress = worker.endpoint.address
logWarning("Worker registration failed. Attempted to re-register worker at same " +
"address: " + workerAddress)
workerRef.send(RegisterWorkerFailed("Attempted to re-register worker at same address: "
+ workerAddress))
}
}
当Worker接收到注册成功后,会定时发送心跳信息Heartbeat给Master,以便Master了解Worker的实时状态。间隔时间可以在spark.worker.timeout中设置,注意的是心跳间隔为该设置值的1/4。
private val HEARTBEAT_MILLIS = conf.get(WORKER_TIMEOUT) * 1000 / 4
当Worker获取到注册成功消息后,先记录日志并更新Master信息,然后启动定时调度进程发送心跳信息,该调度进程时间间隔为上面所定义的HEARTBEAT_MILLIS值。
case RegisteredWorker(masterRef, masterWebUiUrl, masterAddress, duplicate) =>
val preferredMasterAddress = if (preferConfiguredMasterAddress) {
masterAddress.toSparkURL
} else {
masterRef.address.toSparkURL
}
// there're corner cases which we could hardly avoid duplicate worker registration,
// e.g. Master disconnect(maybe due to network drop) and recover immediately, see
// SPARK-23191 for more details.
if (duplicate) {
logWarning(s"Duplicate registration at master $preferredMasterAddress")
}
logInfo(s"Successfully registered with master $preferredMasterAddress")
registered = true
changeMaster(masterRef, masterWebUiUrl, masterAddress)
forwardMessageScheduler.scheduleAtFixedRate(
() => Utils.tryLogNonFatalError { self.send(SendHeartbeat) },
0, HEARTBEAT_MILLIS, TimeUnit.MILLISECONDS)
// 如果设置清理以前应用使用的文件夹,则进行该动作
if (CLEANUP_ENABLED) {
logInfo(
s"Worker cleanup enabled; old application directories will be deleted in: $workDir")
forwardMessageScheduler.scheduleAtFixedRate(
() => Utils.tryLogNonFatalError { self.send(WorkDirCleanup) },
CLEANUP_INTERVAL_MILLIS, CLEANUP_INTERVAL_MILLIS, TimeUnit.MILLISECONDS)
}
// 向Master汇报Worker中Executor最新状态
val execs = executors.values.map { e =>
new ExecutorDescription(e.appId, e.execId, e.cores, e.state)
}
masterRef.send(WorkerLatestState(workerId, execs.toList, drivers.keys.toSeq))
小结
RPC的机制远远不止这些,得益于前人的工作我们可以不用关心RPC是如何实现的,设计出复杂的分布式系统。
注:工厂模式就是创建一个接口,然后让一些实体类去实现它,在创建一个工厂类对这些实体方法进行封装,然后用户可以创建一个工厂实例,通过该实例可以生成不同功能的实体类对象。