一.Spark心跳概述
前面兩節中介紹了Spark RPC的基本知識,以及深入剖析了Spark RPC中一些原始碼的實現流程。
具體可以看這裡:
這一節我們來看看一個Spark RPC中的運用例項–Spark的心跳機制。當然這次主要還是從程式碼的角度來看。
我們首先要知道Spark的心跳有什麼用。心跳是分散式技術的基礎,我們知道在Spark中,是有一個Master和眾多的Worker,那麼Master怎麼知道每個Worker的情況呢,這就需要藉助心跳機制了。心跳除了傳輸資訊,另一個主要的作用就是Worker告訴Master它還活著,當心跳停止時,方便Master進行一些容錯操作,比如資料轉移備份等等。
與之前講Spark RPC一樣,我們同樣分成兩部分來分析Spark的心跳機制,分為服務端(Spark Context)和客戶端(Executor)。
二. Spark心跳服務端heartbeatReceiver解析
我們可以發現,SparkContext中有關於心跳的類以及RpcEndpoint註冊程式碼。
class SparkContext(config: SparkConf) extends Logging {
......
private var _heartbeatReceiver: RpcEndpointRef = _
......
//向 RpcEnv 註冊 Endpoint。
_heartbeatReceiver = env.rpcEnv.setupEndpoint(HeartbeatReceiver.ENDPOINT_NAME, new HeartbeatReceiver(this))
......
val (sched, ts) = SparkContext.createTaskScheduler(this, master, deployMode)
_schedulerBackend = sched
_taskScheduler = ts
_dagScheduler = new DAGScheduler(this)
_heartbeatReceiver.ask[Boolean](TaskSchedulerIsSet)
......
}
這裡rpcEnv已經在上下文中建立好,通過setupEndpoint向rpcEnv註冊一個心跳的Endpoint。還記得上一節中HelloworldServer的例子嗎,在setupEndpoint方法中,會去呼叫Dispatcher建立這個Endpoint(這裡就是HeartbeatReceiver)對應的Inbox和EndpointRef,然後在Inbox監聽是否有新訊息,有新訊息則處理它。註冊完會返回一個EndpointRef(注意這裡有Refer,即是客戶端,用來傳送訊息的)。
所以這一句
_heartbeatReceiver = env.rpcEnv.setupEndpoint(HeartbeatReceiver.ENDPOINT_NAME, new HeartbeatReceiver(this))
就已經完成了心跳服務端監聽的功能。
那麼這條程式碼的作用呢?
_heartbeatReceiver.ask[Boolean](TaskSchedulerIsSet)
這裡我們要看上面那句val (sched, ts) = SparkContext.createTaskScheduler(this, master, deployMode),它會根據master url建立SchedulerBackend和TaskScheduler。這兩個類都是和資源排程有關的,所以需要藉助心跳機制來傳送訊息。其中TaskScheduler負責任務排程資源分配,SchedulerBackend負責與Master、Worker通訊收集Worker上分配給該應用使用的資源情況。
這裡主要是告訴HeartbeatReceiver(心跳)的監聽端,告訴它TaskScheduler這個東西已經設定好啦。HeartbeatReceiver就會回應你說好的,我知道的,並持有這個TaskScheduler。
到這裡服務端heartbeatReceiver就差不多完了,我們可以發現,HeartbeatReceiver除了向RpcEnv註冊並監聽訊息之外,還會去持有一些資源排程相關的類,比如TaskSchedulerIsSet。
三. Spark心跳客戶端傳送心跳解析
傳送心跳傳送在Worker,每個Worker都會有一個Executor,所以我們可以發現在Executor中傳送心跳的程式碼。
private[spark] class Executor(
executorId: String,
executorHostname: String,
env: SparkEnv,
userClassPath: Seq[URL] = Nil,
isLocal: Boolean = false)
extends Logging {
......
// must be initialized before running startDriverHeartbeat()
//建立心跳的 EndpointRef
private val heartbeatReceiverRef = RpcUtils.makeDriverRef(HeartbeatReceiver.ENDPOINT_NAME, conf, env.rpcEnv)
......
startDriverHeartbeater()
......
/**
* Schedules a task to report heartbeat and partial metrics for active tasks to driver.
* 用一個 task 來報告活躍任務的資訊以及傳送心跳。
*/
private def startDriverHeartbeater(): Unit = {
val intervalMs = conf.getTimeAsMs("spark.executor.heartbeatInterval", "10s")
// Wait a random interval so the heartbeats don`t end up in sync
val initialDelay = intervalMs + (math.random * intervalMs).asInstanceOf[Int]
val heartbeatTask = new Runnable() {
override def run(): Unit = Utils.logUncaughtExceptions(reportHeartBeat())
}
//heartbeater是一個單執行緒執行緒池,scheduleAtFixedRate 是定時執行任務用的,和 schedule 類似,只是一些策略不同。
heartbeater.scheduleAtFixedRate(heartbeatTask, initialDelay, intervalMs, TimeUnit.MILLISECONDS)
}
......
}
可以看到,在Executor中會建立心跳的EndpointRef,變數名為heartbeatReceiverRef。
然後我們主要看startDriverHeartbeater()這個方法,它是關鍵。
我們可以看到最後部分程式碼
val heartbeatTask = new Runnable() {
override def run(): Unit = Utils.logUncaughtExceptions(reportHeartBeat())
}
heartbeater.scheduleAtFixedRate(heartbeatTask, initialDelay, intervalMs, TimeUnit.MILLISECONDS)
heartbeatTask是一個Runaable,即一個執行緒任務。scheduleAtFixedRate則是java concurrent包中用來執行定時任務的一個類,這裡的意思是每隔10s跑一次heartbeatTask中的執行緒任務,超時時間30s。
為什麼到這裡還是沒看到heartbeatReceiverRef呢,說好的傳送心跳呢?別急,其實在heartbeatTask執行緒任務中又呼叫了另一個方法,我們到裡面去一探究竟。
private[spark] class Executor(
executorId: String,
executorHostname: String,
env: SparkEnv,
userClassPath: Seq[URL] = Nil,
isLocal: Boolean = false)
extends Logging {
......
private def reportHeartBeat(): Unit = {
// list of (task id, accumUpdates) to send back to the driver
val accumUpdates = new ArrayBuffer[(Long, Seq[AccumulatorV2[_, _]])]()
val curGCTime = computeTotalGcTime()
for (taskRunner <- runningTasks.values().asScala) {
if (taskRunner.task != null) {
taskRunner.task.metrics.mergeShuffleReadMetrics()
taskRunner.task.metrics.setJvmGCTime(curGCTime - taskRunner.startGCTime)
accumUpdates += ((taskRunner.taskId, taskRunner.task.metrics.accumulators()))
}
}
val message = Heartbeat(executorId, accumUpdates.toArray, env.blockManager.blockManagerId)
try {
//終於看到 heartbeatReceiverRef 的身影了
val response = heartbeatReceiverRef.askWithRetry[HeartbeatResponse](
message, RpcTimeout(conf, "spark.executor.heartbeatInterval", "10s"))
if (response.reregisterBlockManager) {
logInfo("Told to re-register on heartbeat")
env.blockManager.reregister()
}
heartbeatFailures = 0
} catch {
case NonFatal(e) =>
logWarning("Issue communicating with driver in heartbeater", e)
heartbeatFailures += 1
if (heartbeatFailures >= HEARTBEAT_MAX_FAILURES) {
logError(s"Exit as unable to send heartbeats to driver " +
s"more than $HEARTBEAT_MAX_FAILURES times")
System.exit(ExecutorExitCode.HEARTBEAT_FAILURE)
}
}
}
......
}
可以看到,這裡heartbeatReceiverRef和我們上一節的例子,HelloworldClient類似,核心也是呼叫了askWithRetry()方法,這個方法是通過同步的方式傳送Rpc訊息。而這個方法裡其他程式碼其實就是獲取task的資訊啊,或者是一些容錯處理。核心就是呼叫askWithRetry()方法來傳送訊息。
看到這你就明白了吧。Executor初始化便會用一個定時任務不斷髮送心跳,同時當有task的時候,會獲取task的資訊一併傳送。這就是心跳的大概內容了。
總的來說Spark心跳的程式碼也是比較雜的,不過這些也都是為了讓設計更加高耦合,低內聚,讓這些程式碼更加方便得複用。不過通過層層剖析,我們還是發現其實它底層就是我們之前說到的Spark RPC框架的內容!!
OK,Spark RPC三部曲完畢。如果你能看到這裡那不容易呀,給自己點個贊吧!!
推薦閱讀 :
從分治演算法到 MapReduce
大資料儲存的進化史 –從 RAID 到 Hadoop Hdfs
一個故事告訴你什麼才是好的程式設計師