spark版本: 2.0.0
1.概念
master管理着spark的主要元數據,用於管理集羣,資源調度等。
2.master啓動過程
2.1 Master.main方法
在start-master.sh腳本中可以看出最終調用的是org.apache.spark.deploy.master.Master
的main方法。現在來分析一下這個方法:
def main(argStrings: Array[String]) {
// 日誌
Utils.initDaemon(log)
// spark 配置對象
val conf = new SparkConf
// master參數對象,用於解析傳遞參數,比如:--host ,--webui-port等
val args = new MasterArguments(argStrings, conf)
val (rpcEnv, _, _) =
// 啓動master通信端(核心方法)
startRpcEnvAndEndpoint(args.host, args.port, args.webUiPort, conf)
rpcEnv.awaitTermination()
}
2.2 Master.startRpcEnvAndEndpoint方法
def startRpcEnvAndEndpoint(
host: String,
port: Int,
webUiPort: Int,
conf: SparkConf): (RpcEnv, Int, Option[Int]) = {
// 安全管理器
val securityMgr = new SecurityManager(conf)
// 創建rpc環境對象,現在是基於netty
val rpcEnv = RpcEnv.create(SYSTEM_NAME, host, port, conf, securityMgr)
// 註冊master通信端,並返回其通信引用 【1】
val masterEndpoint = rpcEnv.setupEndpoint(ENDPOINT_NAME,
new Master(rpcEnv, rpcEnv.address, webUiPort, securityMgr, conf))
// 向Master的通信終端發送請求,獲取綁定的端口號 【2】
val portsResponse = masterEndpoint.askWithRetry[BoundPortsResponse](BoundPortsRequest)
(rpcEnv, portsResponse.webUIPort, portsResponse.restPort)
}
核心位置分析:
【1】
Dispatcher.scala
----------------------------
/**
* 註冊rpc通信端
* @param name
* @param endpoint
* @return
*/
def registerRpcEndpoint(name: String, endpoint: RpcEndpoint): NettyRpcEndpointRef = {
val addr = RpcEndpointAddress(nettyEnv.address, name)
// 獲取rpc通信端的引用,可以進行通信
val endpointRef = new NettyRpcEndpointRef(nettyEnv.conf, addr, nettyEnv)
synchronized {
if (stopped) {
throw new IllegalStateException("RpcEnv has been stopped")
}
// 添加endpoint名稱和對應的數據封裝映射
if (endpoints.putIfAbsent(name, new EndpointData(name, endpoint, endpointRef)) != null) {
throw new IllegalArgumentException(s"There is already an RpcEndpoint called $name")
}
val data = endpoints.get(name)
// 添加endpoint引用
endpointRefs.put(data.endpoint, data.ref)
// 添加到消息處理隊列中,等待定時任務處理
receivers.offer(data) // for the OnStart message
}
endpointRef
}
上面有一段最核心的代碼是:
receivers.offer(data)
看似只是將請求的數據放入receivers隊列中,但是它將觸發定時任務處理請求,詳情如下:
Dispatcher.scala
-------------------
/** 線程池一直在處理MessageLoop的run方法 */
private val threadpool: ThreadPoolExecutor = {
val numThreads = nettyEnv.conf.getInt("spark.rpc.netty.dispatcher.numThreads",
math.max(2, Runtime.getRuntime.availableProcessors()))
// 守護線程不停監聽消息
val pool = ThreadUtils.newDaemonFixedThreadPool(numThreads, "dispatcher-event-loop")
for (i <- 0 until numThreads) {
pool.execute(new MessageLoop)
}
pool
}
/** Message loop used for dispatching messages. */
private class MessageLoop extends Runnable {
override def run(): Unit = {
try {
// 不斷循環
while (true) {
try {
val data = receivers.take()
// 特殊請求
if (data == PoisonPill) {
// Put PoisonPill back so that other MessageLoops can see it.
receivers.offer(PoisonPill)
return
}
// 接收方處理收信箱
data.inbox.process(Dispatcher.this)
} catch {
case NonFatal(e) => logError(e.getMessage, e)
}
}
} catch {
case ie: InterruptedException => // exit
}
}
}
爲了解釋上面的data.inbox.process(Dispatcher.this)
,重點介紹一下data.inbox屬性
Dispatcher.scala
-------------------
private class EndpointData(
val name: String,
val endpoint: RpcEndpoint,
val ref: NettyRpcEndpointRef) {
// 每次創建一個新對象時,同時創建一個Inbox對象
val inbox = new Inbox(ref, endpoint)
}
private[netty] class Inbox(
val endpointRef: NettyRpcEndpointRef,
val endpoint: RpcEndpoint)
extends Logging {
inbox => // Give this an alias so we can use it more clearly in closures.
// 消息集合,放入這裏的消息並不會馬上處理,而是要加入到Dispatcher.receivers中,利用線程池併發處理
@GuardedBy("this")
protected val messages = new java.util.LinkedList[InboxMessage]()
/** True if the inbox (and its associated endpoint) is stopped. */
// 是否已經停止接收
@GuardedBy("this")
private var stopped = false
/** Allow multiple threads to process messages at the same time. */
// 是否允許併發
@GuardedBy("this")
private var enableConcurrent = false
/** The number of threads processing messages for this inbox. */
// inbox中活躍線程數
@GuardedBy("this")
private var numActiveThreads = 0
// OnStart should be the first message to process
// 每次創建Inbox對象時,都會先添加一個OnStart消息
inbox.synchronized {
messages.add(OnStart)
}
根據上面分析可知,每次創建EndpointData對象時,就會添加OnStart消息到inbox對象中。所以在註冊時receivers.offer(data)
就會添加一個OnStart消息等待處理,現在來看一下真正的處理消息方法(即解釋:data.inbox.process(Dispatcher.this)):
def process(dispatcher: Dispatcher): Unit = {
var message: InboxMessage = null
inbox.synchronized {
// 存在線程處理
if (!enableConcurrent && numActiveThreads != 0) {
return
}
// 讀取消息
message = messages.poll()
if (message != null) {
numActiveThreads += 1
} else {
return
}
}
while (true) {
safelyCall(endpoint) {
/**
* 處理各種類型的消息
*/
message match {
.......
// 只保留引用到的OnStart消息處理
case OnStart =>
// 這裏的endpoint指Master對象,所以就是調用Master.onStart方法
endpoint.onStart()
if (!endpoint.isInstanceOf[ThreadSafeRpcEndpoint]) {
inbox.synchronized {
if (!stopped) {
enableConcurrent = true
}
}
}
.......
}
}
.......
}
接着上面分析的節奏,來分析一下Master.onStart方法
Master.scala
----------------------
override def onStart(): Unit = {
logInfo("Starting Spark master at " + masterUrl)
logInfo(s"Running Spark version ${org.apache.spark.SPARK_VERSION}")
// 使用jetty創建web ui請求服務
webUi = new MasterWebUI(this, webUiPort)
webUi.bind()
masterWebUiUrl = "http://" + masterPublicAddress + ":" + webUi.boundPort
// 檢查超時
checkForWorkerTimeOutTask = forwardMessageThread.scheduleAtFixedRate(new Runnable {
override def run(): Unit = Utils.tryLogNonFatalError {
self.send(CheckForWorkerTimeOut)
}
}, 0, WORKER_TIMEOUT_MS, TimeUnit.MILLISECONDS)
// 如果啓用了rest server,那麼啓動rest服務,可以通過該服務向master提交各種請求
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())
// 指標監控(不是重點,建議直接跳過)
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.
// 監控的指標也放在web ui中
masterMetricsSystem.getServletHandlers.foreach(webUi.attachHandler)
applicationMetricsSystem.getServletHandlers.foreach(webUi.attachHandler)
// ------------這段屬於master HA部分,以後單獨介紹---------------
// 指定是java序列化方式,可以修改爲工廠模式
val serializer = new JavaSerializer(conf)
// 根據恢復模式選擇,持久化引擎和leader選舉
val (persistenceEngine_, leaderElectionAgent_) = RECOVERY_MODE match {
// 如果恢復模式是ZOOKEEPER,那麼通過zookeeper來持久化恢復狀態
case "ZOOKEEPER" =>
logInfo("Persisting recovery state to ZooKeeper")
val zkFactory =
new ZooKeeperRecoveryModeFactory(conf, serializer)
(zkFactory.createPersistenceEngine(), zkFactory.createLeaderElectionAgent(this))
// 如果恢復模式是文件系統,那麼通過文件系統來持久化恢復狀態
case "FILESYSTEM" =>
val fsFactory =
new FileSystemRecoveryModeFactory(conf, serializer)
(fsFactory.createPersistenceEngine(), fsFactory.createLeaderElectionAgent(this))
// 如果恢復模式是定製的,那麼指定你定製的全路徑類名,然後產生相關操作來持久化恢復狀態
case "CUSTOM" =>
val clazz = Utils.classForName(conf.get("spark.deploy.recoveryMode.factory"))
val factory = clazz.getConstructor(classOf[SparkConf], classOf[Serializer])
.newInstance(conf, serializer)
.asInstanceOf[StandaloneRecoveryModeFactory]
(factory.createPersistenceEngine(), factory.createLeaderElectionAgent(this))
// 其他處理方式
case _ =>
(new BlackHolePersistenceEngine(), new MonarchyLeaderAgent(this))
}
persistenceEngine = persistenceEngine_
leaderElectionAgent = leaderElectionAgent_
}
其中master.onStart非常簡單,就是創建監聽服務,訪問ui端口,確定master HA恢復模式
上面介紹了這麼多,其實只是介紹了startRpcEnvAndEndpoint
方法中的核心代碼之一的val masterEndpoint = rpcEnv.setupEndpoint(ENDPOINT_NAME, new Master(rpcEnv, rpcEnv.address, webUiPort, securityMgr, conf))
,現在來介紹一下:val portsResponse = masterEndpoint.askWithRetry[BoundPortsResponse](BoundPortsRequest)
【2】:
RpcEndpointRef.scala
-----------------------
/**
* 多次重試請求
*/
def askWithRetry[T: ClassTag](message: Any, timeout: RpcTimeout): T = {
// TODO: Consider removing multiple attempts
var attempts = 0
var lastException: Exception = null
// 如果沒有達到最大重試次數
while (attempts < maxRetries) {
attempts += 1
try {
// 處理請求(核心)
val future = ask[T](message, timeout)
// 等待處理結果
val result = timeout.awaitResult(future)
if (result == null) {
throw new SparkException("RpcEndpoint returned null")
}
return result
} catch {
case ie: InterruptedException => throw ie
case e: Exception =>
lastException = e
logWarning(s"Error sending message [message = $message] in $attempts attempts", e)
}
// 休眠等待下一次重試機會
if (attempts < maxRetries) {
Thread.sleep(retryWaitMs)
}
}
throw new SparkException(
s"Error sending message [message = $message]", lastException)
}
處理請求代碼ask[T](message, timeout)
(message=BoundPortsRequest)來分析一下,
NettyRpcEnv.scala
---------------------
private[netty] def ask[T: ClassTag](message: RequestMessage, timeout: RpcTimeout): Future[T] = {
val promise = Promise[Any]()
// 目標地址
val remoteAddr = message.receiver.address
def onFailure(e: Throwable): Unit = {
if (!promise.tryFailure(e)) {
logWarning(s"Ignored failure: $e")
}
}
def onSuccess(reply: Any): Unit = reply match {
case RpcFailure(e) => onFailure(e)
case rpcReply =>
if (!promise.trySuccess(rpcReply)) {
logWarning(s"Ignored message: $reply")
}
}
try {
// 如果請求的目標地址是本機
if (remoteAddr == address) {
val p = Promise[Any]()
// 異步處理消息
p.future.onComplete {
// 如果成功,會調用onSuccess方法,promise.future對象可以獲取到數據
case Success(response) => onSuccess(response)
case Failure(e) => onFailure(e)
}(ThreadUtils.sameThread)
// 發送本地消息
dispatcher.postLocalMessage(message, p)
} else {
// 封裝rpc請求對象
val rpcMessage = RpcOutboxMessage(serialize(message),
onFailure,
(client, response) => onSuccess(deserialize[Any](client, response)))
//
postToOutbox(message.receiver, rpcMessage)
promise.future.onFailure {
case _: TimeoutException => rpcMessage.onTimeout()
case _ =>
}(ThreadUtils.sameThread)
}
// 超時檢查
val timeoutCancelable = timeoutScheduler.schedule(new Runnable {
override def run(): Unit = {
onFailure(new TimeoutException(s"Cannot receive any reply in ${timeout.duration}"))
}
}, timeout.duration.toNanos, TimeUnit.NANOSECONDS)
promise.future.onComplete { v =>
timeoutCancelable.cancel(true)
}(ThreadUtils.sameThread)
} catch {
case NonFatal(e) =>
onFailure(e)
}
// 如果獲取到返回結果,直接轉換爲T類型對象;出現異常使用超時處理
promise.future.mapTo[T].recover(timeout.addMessageIfTimeout)(ThreadUtils.sameThread)
}
雖然上面的代碼很長,但是主要是區分兩種請求接收方:
(1) remoteAddr == address,請求和接收方是一臺服務器
核心代碼是:dispatcher.postLocalMessage(message, p)
(2) remoteAddr != address,不同服務器
核心代碼是:postToOutbox(message.receiver, rpcMessage)
不過由於master啓動,一般在本機執行,所以這裏先之分析remoteAddr == address的請況,在以後會介紹outbox處理。
接下來,我將依次分析這句代碼,想看一下:dispatcher.postLocalMessage(message, p)
,它表示通過消息分發器將message發送到本機:
Dispatcher.scala
-------------------
def postLocalMessage(message: RequestMessage, p: Promise[Any]): Unit = {
val rpcCallContext =
new LocalNettyRpcCallContext(message.senderAddress, p)
// 拼裝rpc消息對象
val rpcMessage = RpcMessage(message.senderAddress, message.content, rpcCallContext)
// 核心代碼**
postMessage(message.receiver.name, rpcMessage, (e) => p.tryFailure(e))
}
private def postMessage(
endpointName: String,
message: InboxMessage,
callbackIfStopped: (Exception) => Unit): Unit = {
val error = synchronized {
val data = endpoints.get(endpointName)
if (stopped) {
Some(new RpcEnvStoppedException())
} else if (data == null) {
Some(new SparkException(s"Could not find $endpointName."))
} else {
// 往需要發送的通信端inbox中添加一條消息,並添加到receivers從而觸發消息處理
data.inbox.post(message)
receivers.offer(data)
None
}
}
// We don't need to call `onStop` in the `synchronized` block
error.foreach(callbackIfStopped)
}
這段代碼是不是很熟悉,其實就是將message發送到endpoint的inbox,然後通過定時處理請求。
根據前面的分析,可以知道最終相當於調用inbox.process
方法,請求類型是RpcMessage
即:
def process(dispatcher: Dispatcher): Unit = {
..... 爲了突出重點,這裏是提出這段代碼
message match {
case RpcMessage(_sender, content, context) =>
try {
// 這裏endpoint = master 即調用master.receiveAndReply方法
endpoint.receiveAndReply(context).applyOrElse[Any, Unit](content, { msg =>
throw new SparkException(s"Unsupported message $message from ${_sender}")
})
} catch {
case NonFatal(e) =>
context.sendFailure(e)
// Throw the exception -- this exception will be caught by the safelyCall function.
// The endpoint's onError function will be called.
throw e
}
......
Master.scala ->
override def receiveAndReply(context: RpcCallContext): PartialFunction[Any, Unit] = {
.......
case BoundPortsRequest =>
context.reply(BoundPortsResponse(address.port, webUi.boundPort, restServerBoundPort))
......
Master endpoint對BoundPortsRequest請求處理邏輯非常簡單,不做多說明
至此,master啓動涉及的核心對象和方法就介紹完了。