文章目录
- Spark源码剖析——Master、Worker启动流程
- 当前环境与版本
- 1. 前言
- 2. Master启动流程
- 2.1 Master的伴生对象
- 2.2 Master
- 3. Worker启动流程
- 3.1 Worker的伴生对象
- 3.2 Worker
- 4. Master与Worker的初步交互(注册)
Spark源码剖析——Master、Worker启动流程
当前环境与版本
环境 | 版本 |
JDK | java version “1.8.0_231” (HotSpot) |
Scala | Scala-2.11.12 |
Spark | spark-2.4.4 |
1. 前言
- Master与Worker是Spark在Standalone模式下的主要节点,维护起了整个分布式集群的管理、资源分配、应用运行等重要工作。
- Master、Worker都是ThreadSafeRpcEndpoint的实现类,其启动流程部分较简单,查看此部分的代码,可以帮助我们快速上手,理解到集群中RpcEndpoint、RpcEnv的交互过程。这样在后续过程中查看其他代码将更加容易。
- 在看此部分之前,建议先看Spark源码剖析——RpcEndpoint、RpcEnv
2. Master启动流程
2.1 Master的伴生对象
- org.apache.spark.deploy.master.Master
- 我们先看Master的伴生对象,此处是Java进程的入口(被
start-master.sh
启动)
private[deploy] object Master extends Logging {
val SYSTEM_NAME = "sparkMaster"
val ENDPOINT_NAME = "Master"
def main(argStrings: Array[String]) {
Thread.setDefaultUncaughtExceptionHandler(new SparkUncaughtExceptionHandler(
exitOnUncaughtException = false))
Utils.initDaemon(log)
val conf = new SparkConf
// 此处会解析外部传入的参数argStrings,由其内部的parse方法解析
// 示例:--port 7077 --webui-port 8081
val args = new MasterArguments(argStrings, conf)
// 启动Master对应的RpcEndpoint、NettyRpcEnv
val (rpcEnv, _, _) = startRpcEnvAndEndpoint(args.host, args.port, args.webUiPort, conf)
// 此处调用的就是我们前面在NettyRpcEnv所讲的
// 'ctrl + alt + 鼠标左键' 点击'awaitTermination',选择其实现类NettyRpcEnv,可以看到调用了dispatcher
// 再继续点击,可以看到实际是调用了threadpool.awaitTermination(...),在此处进行了阻塞
// 而该threadpool正是运行了MessageLoop(用于处理Inbox消息)的线程池
rpcEnv.awaitTermination()
}
def startRpcEnvAndEndpoint(
host: String,
port: Int,
webUiPort: Int,
conf: SparkConf): (RpcEnv, Int, Option[Int]) = {
// 安全管理,例如ACL、Sasl
val securityMgr = new SecurityManager(conf)
// 创建NettyRpcEnv,由NettyRpcEnvFactory调用create(...)创建
val rpcEnv = RpcEnv.create(SYSTEM_NAME, host, port, conf, securityMgr)
// 创建Master这个RpcEndpoint,并将其注册到RpcEnv中
val masterEndpoint = rpcEnv.setupEndpoint(ENDPOINT_NAME,
new Master(rpcEnv, rpcEnv.address, webUiPort, securityMgr, conf))
// 向Master发送了一个BoundPortsRequest,并同步返回一个BoundPortsResponse(包含了Master的端口信息)
val portsResponse = masterEndpoint.askSync[BoundPortsResponse](BoundPortsRequest)
(rpcEnv, portsResponse.webUIPort, portsResponse.restPort)
}
}
- 此处代码,相对来说还是比较简单的。Shell调用
start-master.sh
后,会启动一个Java进程。传入的参数则被MasterArguments进行了解析,最重要的参数是host、port、webUiPort。 - 接着,就会调用startRpcEnvAndEndpoint(…),开始创建NettyRpcEnv与Master,并将Master注册进RpcEnv。
- 创建NettyRpcEnv是利用的NettyRpcEnvFactory调用create(…)
- Master则是直接被new实例化,此时该RpcEndpoint的构造器被调用
- 注册Master则是调用了setupEndpoint(…),进而调用了dispatcher的registerRpcEndpoint(…)方法:
- 为Master创建了一个EndpointData,包含一个Inbox。Inbox实例化时顺带将OnStart消息放入了队列。
- 将EndpointData放入了receivers队列中,后续会被MessageLoop取出
- 因此,我们可以看到,Master被实例化时,先调用了其构造器。接着,将其注册入RpcEnv时,其Inbox中放入了第一条消息OnStart。然后,该消息OnStart将被MessageLoop取出并处理,调用了Master这个Endpoint的onStart方法。也就是说Master的生命周期前面部分是:constructor -> onStart -> …
2.2 Master
- org.apache.spark.deploy.master.Master
- Master的class代码相对来说还是比较多的,我们主要看起启动流程部分代码
- 首先,我们来看其onStart()方法做了什么
override def onStart(): Unit = {
logInfo("Starting Spark master at " + masterUrl)
logInfo(s"Running Spark version ${org.apache.spark.SPARK_VERSION}")
// 启动Master的WebUI界面
webUi = new MasterWebUI(this, webUiPort)
webUi.bind()
masterWebUiUrl = "http://" + masterPublicAddress + ":" + webUi.boundPort
// 是否启用反向代理,默认为false
if (reverseProxy) {
masterWebUiUrl = conf.get("spark.ui.reverseProxyUrl", masterWebUiUrl)
webUi.addProxy()
logInfo(s"Spark Master is acting as a reverse proxy. Master, Workers and " +
s"Applications UIs are available at $masterWebUiUrl")
}
// 启用定时任务,心跳机制,向自己发送CheckForWorkerTimeOut消息,用于检测Worker是否超时
// 跟踪代码可知,最终会调用timeOutDeadWorkers(),用于检测超时的Worker,并移除
checkForWorkerTimeOutTask = forwardMessageThread.scheduleAtFixedRate(new Runnable {
override def run(): Unit = Utils.tryLogNonFatalError {
self.send(CheckForWorkerTimeOut)
}
}, 0, WORKER_TIMEOUT_MS, TimeUnit.MILLISECONDS)
// 是否启用了RESTServer,默认为false
if (restServerEnabled) {
val port = conf.getInt("spark.master.rest.port", 6066)
restServer = Some(new StandaloneRestServer(address.host, port, conf, self, masterUrl))
}
restServerBoundPort = restServer.map(_.start())
// MetricsSystem是Spark的监控度量系统
masterMetricsSystem.registerSource(masterSource)
masterMetricsSystem.start()
applicationMetricsSystem.start()
// Attach the master and app metrics servlet handler to the web ui after the metrics systems are
// started.
masterMetricsSystem.getServletHandlers.foreach(webUi.attachHandler)
applicationMetricsSystem.getServletHandlers.foreach(webUi.attachHandler)
// Spark的恢复模式,暂时可以不管
// 省略部分代码
}
- Master的onStart()方法主要做了以下几件事:
- 启动了Master的WebUI界面
- 开启了Worker的心跳检测定时任务
- 启动了监控度量系统MetricsSystem
- 至此,Master的启动就算结束了。后面会等待着接收消息,消息进入Inbox,再传给Master这个RpcEndpoint,调用Master的receive、receiveAndReply。
- 另外,在Master的伴生对象的startRpcEnvAndEndpoint(…)中,完成Endpoint的注册后,还会向Master发送一条同步消息BoundPortsRequest,并获得回应的消息BoundPortsResponse。
3. Worker启动流程
3.1 Worker的伴生对象
- org.apache.spark.deploy.worker.Worker
- Worker被
start-slave.sh
启动 - 此伴生对象和Master的伴生对象代码逻辑几乎一样,就不再做展示,自行看代码即可。
- 需要注意的是
- main入口中一定要传入master的地址,传参示例
--webui-port 8081 spark://192.168.0.101:7077 --cores 2 --memory 2G
- 实例化Worker时,同时也传入了masterAddresses,用于后续获取Master的RpcEndpointRef,向其发送消息
3.2 Worker
- org.apache.spark.deploy.worker.Worker
- Worker启动时,同Master一样,将调用其构造器,接着onStart方法被调用。我们来看Worker的onStart()方法做了什么。
override def onStart() {
assert(!registered)
logInfo("Starting Spark worker %s:%d with %d cores, %s RAM".format(
host, port, cores, Utils.megabytesToString(memory)))
logInfo(s"Running Spark version ${org.apache.spark.SPARK_VERSION}")
logInfo("Spark home: " + sparkHome)
// 创建工作目录
createWorkDir()
// ExternalShuffleService是一个单独的进程服务,默认不开启
// 用于帮助Executor处理shuffle,降低Executor的压力
startExternalShuffleService()
// 启动Worker的WebUI
webUi = new WorkerWebUI(this, workDir, webUiPort)
webUi.bind()
workerWebUiUrl = s"http://$publicAddress:${webUi.boundPort}"
// 注册到Master,下一部分来说
registerWithMaster()
// 启动metricsSystem,用于度量各种指标
metricsSystem.registerSource(workerSource)
metricsSystem.start()
// Attach the worker metrics servlet handler to the web ui after the metrics system is started.
metricsSystem.getServletHandlers.foreach(webUi.attachHandler)
}
- 可以看到,Worker的onStart()方法主要做了以下几件事:
- 创建工作目录
- 启动ExternalShuffleService(默认不启动)
- 启动Worker的WebUI
- 注册到Master(下一节详细来看)
- 启动metricsSystem,用于度量各种指标
- 至此,Worker启动结束。
- 后续Master与Worker只需等待新的应用提交上来,并运行。
4. Master与Worker的初步交互(注册)
- Worker在启动时,是需要注册到Master的,我们来详细看看此部分代码。
- Worker的onStart()中调用的registerWithMaster()方法如下
private def registerWithMaster() {
registrationRetryTimer match {
case None => // 第一次进来时,registrationRetryTimer为None
registered = false
// 此处,是向所有Master发起注册请求
// 因为高可用模式下会存在多个Master
registerMasterFutures = tryRegisterAllMasters()
connectionAttemptCount = 0
// 由于网络等问题,可能注册失败,因此需要一个能够重试的定时器,去注册
registrationRetryTimer = Some(forwordMessageScheduler.scheduleAtFixedRate(
new Runnable {
override def run(): Unit = Utils.tryLogNonFatalError {
Option(self).foreach(_.send(ReregisterWithMaster))
}
},
INITIAL_REGISTRATION_RETRY_INTERVAL_SECONDS,
INITIAL_REGISTRATION_RETRY_INTERVAL_SECONDS,
TimeUnit.SECONDS))
case Some(_) =>
// registrationRetryTimer已存在,不需要再创建了
logInfo("Not spawning another attempt to register with the master, since there is an" +
" attempt scheduled already.")
}
}
- 接着,再看tryRegisterAllMasters()的代码
private def tryRegisterAllMasters(): Array[JFuture[_]] = {
masterRpcAddresses.map { masterAddress =>
// 线程池提交,返回一个JFuture
registerMasterThreadPool.submit(new Runnable {
override def run(): Unit = {
try {
logInfo("Connecting to master " + masterAddress + "...")
// 获取到Master对应的RpcEndpointRef
val masterEndpoint = rpcEnv.setupEndpointRef(masterAddress, Master.ENDPOINT_NAME)
// 向Master发送注册消息
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))
}
- 此处,正式向Master发送了消息RegisterWorker,进行注册
- 快速查看技巧:利用’ctrl+鼠标左键’点击RegisterWorker,看到case class RegisterWorker。再次利用’ctrl+鼠标左键’点击RegisterWorker,IDEA会为我们展示出什么地方使用了它。可以看到IDEA展示的部分:
-
Worker.scala <- masterEndpoint.send(RegisterWorker(
,此处是Worker发送该消息的代码处 -
Master.scala <- case RegisterWorker(
,此处既是Master接收到该消息的地方
- 利用上面的技巧,我们可以快速地在RpcEndpoint的代码之间跳转,方便了对其交互流程的查看。此时,我们来到了Master的receive方法,代码如下
override def receive: PartialFunction[Any, Unit] = {
// 省略部分代码
case RegisterWorker(
id, workerHost, workerPort, workerRef, cores, memory, workerWebUiUrl, masterAddress) =>
// Master收到了Worker发来的RegisterWorker消息,开始进行处理
logInfo("Registering worker %s:%d with %d cores, %s RAM".format(
workerHost, workerPort, cores, Utils.megabytesToString(memory)))
if (state == RecoveryState.STANDBY) {
// 高可用模式下,该Master可能是STANDBY的,因此回复一个MasterInStandby
workerRef.send(MasterInStandby)
} else if (idToWorker.contains(id)) {
// 如果该Worker已经注册了,回一个RegisterWorkerFailed
workerRef.send(RegisterWorkerFailed("Duplicate worker ID"))
} else {
// 开始注册Worker
val worker = new WorkerInfo(id, workerHost, workerPort, cores, memory,
workerRef, workerWebUiUrl)
// 调用registerWorker(...),将worker添加到本节点
if (registerWorker(worker)) {
persistenceEngine.addWorker(worker)
// 如果过成功,那么就向Worker回复消息RegisteredWorker
workerRef.send(RegisteredWorker(self, masterWebUiUrl, masterAddress))
schedule()
} else {
// 注册失败,回复RegisterWorkerFailed
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))
}
}
// 省略部分代码
}
- Master收到消息后,需要检测本节点的状态是否是STANDBY、是否已经注册该Worker,如果没问题,那么调用registerWorker(…),将worker添加到本节点,最后会回复Worker一个消息RegisteredWorker
- 跟随着RegisteredWorker消息,我们来到Worker接收消息处。Worker中先是receive被调用,再匹配到RegisterWorkerResponse,接着调用了handleRegisterResponse(…)方法,代码如下
private def handleRegisterResponse(msg: RegisterWorkerResponse): Unit = synchronized {
msg match {
case RegisteredWorker(masterRef, masterWebUiUrl, masterAddress) =>
// 如果在Master注册成功,则会收到RegisteredWorker
if (preferConfiguredMasterAddress) {
logInfo("Successfully registered with master " + masterAddress.toSparkURL)
} else {
logInfo("Successfully registered with master " + masterRef.address.toSparkURL)
}
registered = true
// 修改本Worker节点对应的Master
changeMaster(masterRef, masterWebUiUrl, masterAddress)
// 启用定时器,发送心跳
// 追踪SendHeartbeat可知,定时器先发送给自己,Worker在receive处收到后,再调用sendToMaster(...)发送给Master
forwordMessageScheduler.scheduleAtFixedRate(new Runnable {
override def run(): Unit = Utils.tryLogNonFatalError {
self.send(SendHeartbeat)
}
}, 0, HEARTBEAT_MILLIS, TimeUnit.MILLISECONDS)
// 是否删除之前应用的工作目录,默认false
if (CLEANUP_ENABLED) {
logInfo(
s"Worker cleanup enabled; old application directories will be deleted in: $workDir")
forwordMessageScheduler.scheduleAtFixedRate(new Runnable {
override def run(): Unit = Utils.tryLogNonFatalError {
self.send(WorkDirCleanup)
}
}, CLEANUP_INTERVAL_MILLIS, CLEANUP_INTERVAL_MILLIS, TimeUnit.MILLISECONDS)
}
// 准备本Worker的Executor信息,并将最新状态消息WorkerLatestState发送给Master
// 不过,显然第一次启动时,本节点是没有启动Excutor的
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))
case RegisterWorkerFailed(message) =>
// 注册失败,回复此消息RegisterWorkerFailed
if (!registered) {
logError("Worker registration failed: " + message)
System.exit(1)
}
case MasterInStandby =>
// Ignore. Master not yet ready.
}
}
- 最后,Master将会收到WorkerLatestState消息,代码如下
override def receive: PartialFunction[Any, Unit] = {
// 省略部分代码
case WorkerLatestState(workerId, executors, driverIds) =>
idToWorker.get(workerId) match {
case Some(worker) =>
// 因为是第一次,因此该executors是空的,for中代码不执行
for (exec <- executors) {
val executorMatches = worker.executors.exists {
case (_, e) => e.application.id == exec.appId && e.id == exec.execId
}
if (!executorMatches) {
// master doesn't recognize this executor. So just tell worker to kill it.
worker.endpoint.send(KillExecutor(masterUrl, exec.appId, exec.execId))
}
}
// 因为是第一次,因此该driverIds是空的,for中代码不执行
for (driverId <- driverIds) {
val driverMatches = worker.drivers.exists { case (id, _) => id == driverId }
if (!driverMatches) {
// master doesn't recognize this driver. So just tell worker to kill it.
worker.endpoint.send(KillDriver(driverId))
}
}
case None =>
logWarning("Worker state from unknown worker: " + workerId)
}
// 省略部分代码
}
- 至此,Worker注册到Master通信流程,完全结束。^_^
- 后面整个集群会持续以下模式:由Worker定时向Master发送心跳包,而Master也会在本节点定时检测Worker的心跳,移除超时的Worker。
- Worker注册到Master的通信流程示意图如下