[Spark內核]通訊架構源碼解析

個人博客文章地址

熟悉的套路,先大概的瞭解spark的通訊架構怎麼樣工作,然後再去跟蹤源碼。

Spark2.x版本使用Netty通訊框架作爲內部通訊組件。

Spark通訊框架中各個組件(Client/Master/Worker)可以認爲是一個個獨立的實體,各個實體之間通過消息來進行通信,如圖:
在這裏插入圖片描述

Endpoint(Client/Master/Worker)有1個InBox和N個OutBox(N>=1,N取決於當前Endpoint與多少其他的Endpoint進行通信,一個與其通訊的其他Endpoint對應一個OutBox),Endpoint接收到的消息被寫入InBox,發送出去的消息寫入OutBox並被髮送到其他Endpoint的InBox中。

上一個更詳細的Spark通信架構圖,更清楚的理解spark組件之間是怎樣通信的,只有看懂了這張圖,有了大致的框架,有助於我們跟蹤源碼;
在這裏插入圖片描述

  1. RpcEndpoint:RPC端點,Spark針對每個節點(Client/Master/Worker)都稱之爲一個Rpc端點,且都實現RpcEndpoint接口,內部根據不同端點的需求,設計不同的消息和不同的業務處理,如果需要發送(詢問)則調用Dispatcher;

  2. RpcEnv:RPC上下文環境,每個RPC端點運行時依賴的上下文環境稱爲RpcEnv;

  3. Dispatcher:消息分發器,針對於RPC端點需要發送消息或者從遠程RPC接收到的消息,分發至對應的指令收件箱/發件箱。如果指令接收方是自己則存入收件箱,如果指令接收方不是自己,則放入發件箱;

  4. Inbox:指令消息收件箱,一個本地RpcEndpoint對應一個收件箱,Dispatcher在每次向Inbox存入消息時,都將對應EndpointData加入內部ReceiverQueue中,另外Dispatcher創建時會啓動一個單獨線程進行輪詢ReceiverQueue,進行收件箱消息消費;

  5. RpcEndpointRef:RpcEndpointRef是對遠程RpcEndpoint的一個引用。當我們需要向一個具體的RpcEndpoint發送消息時,一般我們需要獲取到該RpcEndpoint的引用,然後通過該應用發送消息。

  6. OutBox:指令消息發件箱,對於當前RpcEndpoint來說,一個目標RpcEndpoint對應一個發件箱,如果向多個目標RpcEndpoint發送信息,則有多個OutBox。當消息放入Outbox後,緊接着通過TransportClient將消息發送出去。消息放入發件箱以及發送過程是在同一個線程中進行;

  7. RpcAddress:表示遠程的RpcEndpointRef的地址,Host + Port。

  8. TransportClient:Netty通信客戶端,一個OutBox對應一個TransportClient,TransportClient不斷輪詢OutBox,根據OutBox消息的receiver信息,請求對應的遠程TransportServer;

  9. TransportServer:Netty通信服務端,一個RpcEndpoint對應一個TransportServer,接受遠程消息後調用Dispatcher分發消息至對應收發件箱;

當我瞭解了spark的通信架構後,就可以開始閱讀源碼了。但是切入點在那呢?我們可以接着上次的源碼分析。在RPC上下文環境環境中設置Excutor的通信端點開始。

CoarseGrainedExecutorBackend
main{
	env.rpcEnv.setupEndpoint("Executor", new CoarseGrainedExecutorBackend(
        env.rpcEnv, driverUrl, executorId, hostname, cores, userClassPath, env))
}

在上一篇文章我們關注的是CoarseGrainedExecutorBackend這個類的創建。但是沒有關注setupEndpoint這個方法。所以我們點進去看會發現是RpcEnv抽象類的一個抽象方法,沒有實現,所以必須去找這個類的子類。

private[spark] abstract class RpcEnv(conf: SparkConf) {
	def setupEndpoint(name: String, endpoint: RpcEndpoint): RpcEndpointRef
}

idea按F4,會出現NettyRpcEnv;沒錯這個就要找的子類,緊接着找到setupEndpoint的實現。

private[netty] class NettyRpcEnv{
	override def setupEndpoint(name: String, endpoint: RpcEndpoint): RpcEndpointRef = {
		dispatcher.registerRpcEndpoint(name, endpoint)
	}
}

突然會發現一個熟悉的字眼dispatcher,沒錯這就是我們的消息分發器,沒話說看看它裏面是什麼東西。

private[netty] class Dispatcher(nettyEnv: NettyRpcEnv) extends Logging { 
	// 封裝了數據、端點和引用
    private class EndpointData(      
        val name: String,      
        val endpoint: RpcEndpoint,      
        val ref: NettyRpcEndpointRef) {   
        val inbox = new Inbox(ref, endpoint) 
    }
    
	// 註冊Executor的rpc端點
	def registerRpcEndpoint(name: String, endpoint: RpcEndpoint): NettyRpcEndpointRef = {
		// 封裝RpcEndpointRef的地址,Host + Port
		val addr = RpcEndpointAddress(nettyEnv.address, name)
		
		// 創建一個RpcEndpoint的一個引用
		val endpointRef = new NettyRpcEndpointRef(nettyEnv.conf, addr, nettyEnv)
		synchronized {
		if (stopped) {
			throw new IllegalStateException("RpcEnv has been stopped")
		}
		
		// endpoints結構是 ConcurrentMap[String, EndpointData]
		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)
		
		// endpointRefs結構也是ConcurrentMap[RpcEndpoint, RpcEndpointRef],一個rpc端點對應一個rpc端點的引用
		endpointRefs.put(data.endpoint, data.ref)
		
		//  private val receivers = new LinkedBlockingQueue[EndpointData]  receiver是個阻塞隊列,將data放入隊列中就會有線程來取數據運行
		receivers.offer(data)  // for the OnStart message
		}
		endpointRef
	}
}

通過源碼看看到最後將OnStart message放入隊列中,所以最後會處理OnStart message這條消息。究竟在那處理,處理了什麼東西?其實就是上一篇文章中的CoarseGrainedExecutorBackend中的OnStart方法

private[spark] class CoarseGrainedExecutorBackend() extends ThreadSafeRpcEndpoint{
	// 由於該類繼承了Rpc端點,所以該對象的生命週期是 constructor(創建) -> onStart(啓動) -> receive*(接收消息) -> onStop(停止)

	// 我們所說的Executor就是CoarseGrainedExecutorBackend中的一個屬性對象
	var executor: Executor = null
	
	override def onStart() {
		//向Driver反向註冊
		driver = Some(ref)
		ref.ask[Boolean](RegisterExecutor(executorId, self, hostname, cores, extractLogUrls))
	}
	
	override def receive: PartialFunction[Any, Unit] = {
		// 收到Driver註冊成功的消息
		case RegisteredExecutor =>
			// 創建計算對象Executor
			executor = new Executor(executorId, hostname, env, userClassPath, isLocal = false)
		
		// 收到Driver端發送過來的task
		case LaunchTask(data) =>
			// 由executor對象調用方法運行
			executor.launchTask(this, taskId = taskDesc.taskId, attemptNumber = taskDesc.attemptNumber,taskDesc.name, taskDesc.serializedTask)
	}
}

看到這裏會產生一個疑問,onStart方法裏面調用ref.ask()向Driver反向註冊的消息,究竟誰來接收這個消息,怎麼處理的?
既然是向Drive註冊,那麼就應該去找Driver,而Driver就是用戶創建SparkContent的那段程序,所以我們就可以去SparkContent裏面找。

class SparkContext(config: SparkConf) extends Logging {
	// 沒錯消息就是發給它了
	private var _schedulerBackend: SchedulerBackend = _

}

接着我們就迫不及待的去看SchedulerBackend,不過它是個接口找到它的子類CoarseGrainedSchedulerBackend,看到這個類,是不是有種相識的感覺,CoarseGrainedExecutorBackend,說到底就是這兩個對象在交互,明白了。

class CoarseGrainedSchedulerBackend(scheduler: TaskSchedulerImpl, val rpcEnv: RpcEnv){

	override def receiveAndReply(context: RpcCallContext): PartialFunction[Any, Unit] = {
        // 匹配反向註冊消息
		case RegisterExecutor(executorId, executorRef, hostname, cores, logUrls) =>
            // 總的核心數要加上Executor註冊的核心數
			totalCoreCount.addAndGet(cores)
            // Executor的數量加1
			totalRegisteredExecutors.addAndGet(1)
            // 註冊成功的消息
			executorRef.send(RegisteredExecutor)
	}
}

到此spark的通信架構就瞭解怎麼多,更多具體的就不去深入了。

Driver端用戶交互的是SchedulerBackend,Executor端用戶交互的是ExecutorBackend

碼字不易,還請點波關注/贊;

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章