spark 通信機制

轉載自:http://www.aboutyun.com/thread-21115-1-1.html

bute

對於Network通信,不管傳輸的是序列化後的對象還是文件,在網絡上表現的都是字節流。在傳統IO中,字節流表示爲Stream;在NIO中,字節流表示爲ByteBuffer;在Netty中字節流表示爲ByteBuff或FileRegion;在Spark中,針對Byte也做了一層包裝,支持對Byte和文件流進行處理,即ManagedBuffer;

ManagedBuffer是一個接口,包含了三個函數createInputStream(),nioByteBuffer(),convertToNetty()
起作用分別是獲取InputStream,ByteBuffer,ByteBuff或FileRegion;它有三種具體的現

  • NioManagedBuffer
  • NettyManagedBuffer
  • FileSegmentManagedBuffer

查看其接口註釋

只有FileSegmentManagedBuffer.convertToNetty() 返回的是 FileRegion, 其他的實現返回都是ByteBuff

public Object convertToNetty() throws IOException {
if (conf.lazyFileDescriptor()) {
return new LazyFileRegion(file, offset, length);
} else {
FileChannel fileChannel = new FileInputStream(file).getChannel();
return new DefaultFileRegion(fileChannel, offset, length);
}
}

更好的理解ManagedBuffer:比如Shuffle BlockManager模塊需要在內存中維護本地executor生成的shuffle-map輸出的文件引用,從而可以提供給shuffleFetch進行遠程讀取,此時文件表示爲FileSegmentManagedBuffer,shuffleFetch遠程調用FileSegmentManagedBuffer.nioByteBuffer/createInputStream函數從文件中讀取爲Bytes,並進行後面的網絡傳輸。如果已經在內存中bytes就更好理解了,比如將一個字符數組表示爲NettyManagedBuffer。

協議

協議是應用層通信的基礎,它提供了應用層通信的數據表示,以及編碼和解碼的能力。在Spark Network Common中,繼承AKKA中的定義,將協議命名爲Message,它繼承Encodable,提供了encode的能力。以下是message的層級結構。

Message根據請求響應可以劃分爲RequestMessage和ResponseMessage兩種;對於Response,根據處理結果,可以劃分爲Failure和Success兩種類型;根據功能的不同,z主要劃分爲Stream,ChunkFetch,Rpc。

request response
Stream StreamRequest StreamResponse, StreamFailure
ChunkFetch ChunkFetchRequest StreamFailure
Rpc RpcRequest RpcResponse, RpcFailure

Stream消息就是上面提到的ManagedBuffer中的Stream流,在Spark內部,比如SparkContext.addFile操作會在Driver中針對每一個add進來的file/jar會分配唯一的StreamID(file/[]filename],jars/[filename]);worker通過該StreamID向Driver發起一個StreamRequest的請求,Driver將文件轉換爲FileSegmentManagedBuffer返回給Worker,這就是StreamMessage的用途之一

ChunkFetch也有一個類似Stream的概念,ChunkFetch的對象是“一個內存中的Iterator[ManagedBuffer]”,即一組Buffer,每一個Buffer對應一個chunkIndex,整個Iterator[ManagedBuffer]由一個StreamID標識。Client每次的ChunkFetch請求是由(streamId,chunkIndex)組成的唯一的StreamChunkId,Server端根據StreamChunkId獲取爲一個Buffer並返回給Client; 不管是Stream還是ChunkFetch,在Server的內存中都需要管理一組由StreamID與資源之間映射,即StreamManager類

StreamManager提供了getChunk和openStream兩個接口來分別響應ChunkFetch與Stream兩種操作。它只有兩個實現類,OneForOneStreamManager和NettyStreamManager。

OneForOneStreamManager的主要目的是向StreamManager註冊ManagedBuffer,並且針對Server的ChunkFetch,可調用OneForOneStreamManager.registerStream(String appId, Iterator < ManagedBuffer> buffers)來註冊一組Buffer,比如可以將BlockManager中一組BlockID對應的Iterator[ManagedBuffer]註冊到StreamManager,從而支持遠程Block Fetch操作。NettyStreamManager主要負責提供文件服務(文件、JAR文件、目錄)。

RPC是第三種核心的Message,和Stream/ChunkFetch的Message不同,每次通信的Body是類型是確定的,在rpcHandler可以根據每種Body的類型進行相應的處理。 在Spark1.6.*版本中,也正式使用基於Netty的RPC框架來替代Akka。

Server的結構

Server構建在Netty之上,它提供兩種模型NIO和Epoll,可以通過參數(spark.[module].io.mode)進行配置,最基礎的module就是shuffle,不同的IOMode選型,對應了Netty底層不同的實現,Server的Init過程中,最重要的步驟就是根據不同的IOModel完成EventLoop和Pipeline的構造,如下所示:

//根據IO模型的不同,構造不同的EventLoop/ClientChannel/ServerChannel

EventLoopGroup createEventLoop(IOMode mode, int numThreads, String threadPrefix) {
    switch (mode) {
    case NIO:
        return new NioEventLoopGroup(numThreads, threadFactory);
    case EPOLL:
        return new EpollEventLoopGroup(numThreads, threadFactory);
    }
}

 Class<? extends Channel> getClientChannelClass(IOMode mode) {
    switch (mode) {
    case NIO:
        return NioSocketChannel.class;
    case EPOLL:
        return EpollSocketChannel.class;
    }
}

Class<? extends ServerChannel> getServerChannelClass(IOMode mode) {
    switch (mode) {
    case NIO:
        return NioServerSocketChannel.class;
    case EPOLL:
        return EpollServerSocketChannel.class;
    }
}

//構造pipelet
responseHandler = new TransportResponseHandler(channel);
TransportClient client = new TransportClient(channel, responseHandler);
requestHandler = new TransportRequestHandler(channel, client,rpcHandler);
channelHandler = new TransportChannelHandler(client, responseHandler, requestHandler,
                                 conf.connectionTimeoutMs(), closeIdleConnections);
channel.pipeline()
   .addLast("encoder", encoder)
   .addLast(TransportFrameDecoder.HANDLER_NAME, NettyUtils.createFrameDecoder())
   .addLast("decoder", decoder)
   .addLast("idleStateHandler", new IdleStateHandler())
   .addLast("handler", channelHandler);

其中,MessageEncoder/Decoder針對網絡包到Message的編碼和解碼,而最爲核心就TransportRequestHandler,它封裝了對所有請求/響應的處理;TransportChannelHandler內部實現也很簡單,它封裝了responseHandler和requestHandler,當從Netty中讀取一條Message以後,根據判斷路由給相應的responseHandler和requestHandler。

public void channelRead0(ChannelHandlerContext ctx, Message request) throws Exception {
    if (request instanceof RequestMessage) {
        requestHandler.handle((RequestMessage) request);
    } else {
        responseHandler.handle((ResponseMessage) request);
    }
}

Sever提供的RPC,ChunkFecth,Stream的功能都是依賴TransportRequestHandler來實現的;從原理上來說,RPC與ChunkFecth/Stream還是有很大不同的,其中RPC對於TransportRequestHandler來說是功能依賴,而ChunkFecth/Stream對於TransportRequestHandler來說只是數據依賴。怎麼理解?即TransportRequestHandler已經提供了ChunkFecth/Stream的實現,只需要在構造的時候,向TransportRequestHandler提供一個streamManager,告訴RequestHandler從哪裏可以讀取到Chunk或者Stream。而RPC需要向TransportRequestHandler註冊一個rpcHandler,針對每個RPC接口進行功能實現,同時RPC與ChunkFecth/Stream都會有同一個streamManager的依賴,因此注入到TransportRequestHandler中的streamManager也是依賴rpcHandler來實現,即rpcHandler中提供了RPC功能實現和streamManager的數據依賴。

//參考TransportRequestHandler的構造函數
public TransportRequestHandler(RpcHandler rpcHandler) {
    this.rpcHandler = rpcHandler;//****注入功能****
    this.streamManager = rpcHandler.getStreamManager();//****注入streamManager****
}
//實現ChunkFecth的功能
private void processFetchRequest(final ChunkFetchRequest req) {
    buf = streamManager.getChunk(req.streamId, req.chunkIndex);
    respond(new ChunkFetchSuccess(req.streamChunkId, buf));
}
//實現Stream的功能
private void processStreamRequest(final StreamRequest req) {
    buf = streamManager.openStream(req.streamId);
    respond(new StreamResponse(req.streamId, buf.size(), buf));
}
//實現RPC的功能
private void processRpcRequest(final RpcRequest req) {
    rpcHandler.receive(reverseClient, req.body().nioByteBuffer(), 
        new RpcResponseCallback() {
            public void onSuccess(ByteBuffer response) {
            respond(new RpcResponse(req.requestId, new NioManagedBuffer(response)));
            }
    });
}

Client的結構

Server是通過監聽一個端口,注入rpcHandler和streamManager從而對外提供RPC,ChunkFecth,Stream的服務,而Client即爲一個客戶端類,通過該類,可以將一個streamId/chunkIndex對應的ChunkFetch請求,streamId對應的Stream請求,以及一個RPC數據包對應的RPC請求發送到服務端,並監聽和處理來自服務端的響應;其中最重要的兩個類即爲TransportClient和TransportResponseHandler分別爲上述的“客戶端類”和“監聽和處理來自服務端的響應”。

那麼TransportClient和TransportResponseHandler是怎麼配合一起完成Client的工作呢?

如上所示,由TransportClient將用戶的RPC,ChunkFecth,Stream的請求進行打包併發送到Server端,同時將用戶提供的回調函數註冊到TransportResponseHandler,在上面一節中說過,TransportResponseHandler是TransportChannelHandler的一部分,在TransportChannelHandler接收到數據包,並判斷爲響應包以後,將包數據路由到TransportResponseHandler中,在TransportResponseHandler中通過註冊的回調函數,將響應包的數據返回給客戶端

//以TransportResponseHandler中處理ChunkFetchSuccess響應包的處理邏輯
public void handle(ResponseMessage message) throws Exception {
    String remoteAddress = NettyUtils.getRemoteAddress(channel);
    if (message instanceof ChunkFetchSuccess) {
        resp = (ChunkFetchSuccess) message;
        listener = outstandingFetches.get(resp.streamChunkId);
        if (listener == null) {
            //沒有監聽的回調函數
        } else {
            outstandingFetches.remove(resp.streamChunkId);
            //回調函數,並把resp.body()對應的chunk數據返回給listener
            listener.onSuccess(resp.streamChunkId.chunkIndex, resp.body());
            resp.body().release();
        }
    }
}
//ChunkFetchFailure/RpcResponse/RpcFailure/StreamResponse/StreamFailure處理的方法是一致的

Spark Network的功能應用–BlockTransfer&&Shuffle

無論是BlockTransfer還是ShuffleFetch都需要跨executor的數據傳輸,在每一個executor裏面都需要運行一個Server線程(後面也會分析到,對於Shuffle也可能是一個獨立的ShuffleServer進程存在)來提供對Block數據的遠程讀寫服務。

在每個Executor裏面,都有一個BlockManager模塊,它提供了對當前Executor所有的Block的“本地管理”,並對進程內其他模塊暴露getBlockData(blockId: BlockId): ManagedBuffer的Block讀取接口,但是這裏GetBlockData僅僅是提供本地的管理功能,對於跨遠程的Block傳輸,則由NettyBlockTransferService提供服務。

NettyBlockTransferService本身即是Server,爲其他其他遠程Executor提供Block的讀取功能,同時它即爲Client,爲本地其他模塊暴露fetchBlocks的接口,支持通過host/port拉取任何Executor上的一組的Blocks。

NettyBlockTransferService作爲一個Server

NettyBlockTransferService作爲一個Server,與Executor或Driver裏面其他的服務一樣,在進程啓動時,由SparkEnv初始化構造並啓動服務,在整個運行時的一部分。

val blockTransferService =
    new NettyBlockTransferService(conf, securityManager, hostname, numUsableCores)

val envInstance = new SparkEnv(executorId,rpcEnv,serializer, closureSerializer,
    blockTransferService,//爲SparkEnv的一個組成
    ....,conf)

在上文,我們談到,一個Server的構造依賴RpcHandler提供RPC的功能注入以及提供streamManager的數據注入。對於NettyBlockTransferService,該RpcHandler即爲NettyBlockRpcServer,在構造的過程中,需要與本地的BlockManager進行管理,從而支持對外提供本地BlockMananger中管理的數據

“RpcHandler提供RPC的功能注入”在這裏還是屬於比較“簡陋的”,畢竟他是屬於數據傳輸模塊,Server中提供的chunkFetch和stream已經足夠滿足他的功能需要,那現在問題就是怎麼從streamManager中讀取數據來提供給chunkFetch和stream進行使用呢?

就是NettyBlockRpcServer作爲RpcHandler提供的一個Rpc接口之一:OpenBlocks,它接受由Client提供一個Blockids列表,Server根據該BlockIds從BlockManager獲取到相應的數據並註冊到streamManager中,同時返回一個StreamID,後續Client即可以使用該StreamID發起ChunkFetch的操作。

//case openBlocks: OpenBlocks =>
val blocks: Seq[ManagedBuffer] =
    openBlocks.blockIds.map(BlockId.apply).map(blockManager.getBlockData)
val streamId = streamManager.registerStream(appId, blocks.iterator.asJava)
responseContext.onSuccess(new StreamHandle(streamId, blocks.size).toByteBuffer)

NettyBlockTransferService作爲一個Client

從NettyBlockTransferService作爲一個Server,我們基本可以推測NettyBlockTransferService作爲一個Client支持fetchBlocks的功能的基本方法:

  • Client將一組Blockid表示爲一個openMessage請求,發送到服務端,服務針對該組Blockid返回一個唯一的streamId
  • Client針對該streamId發起size(blockids)個fetchChunk操作。

核心代碼如下:

//發出openMessage請求
client.sendRpc(openMessage.toByteBuffer(), new RpcResponseCallback() {
    @Override
    public void onSuccess(ByteBuffer response) {
        streamHandle = (StreamHandle)response;//獲取streamId
        //針對streamid發出一組fetchChunk
        for (int i = 0; i < streamHandle.numChunks; i++) {
            client.fetchChunk(streamHandle.streamId, i, chunkCallback);
        }
    }
});

同時,爲了提高服務端穩定性,針對fetchBlocks操作NettyBlockTransferService提供了非重試版本和重試版本的BlockFetcher,分別爲OneForOneBlockFetcher和RetryingBlockFetcher,通過參數(spark.[module].io.maxRetries)進行配置,默認是重試3次,除非你蛋疼,你不重試!!!

在Spark,Block有各種類型,可以是ShuffleBlock,也可以是BroadcastBlock等等,對於ShuffleBlock的Fetch,除了由Executor內部的NettyBlockTransferService提供服務以外,也可以由外部的ShuffleService來充當Server的功能,並由專門的ExternalShuffleClient來與其進行交互,從而獲取到相應Block數據。功能的原理和實現,基本一致,但是問題來了?爲什麼需要一個專門的ShuffleService服務呢?主要原因還是爲了做到任務隔離,即減輕因爲fetch帶來對Executor的壓力,讓其專心的進行數據的計算。

其實外部的ShuffleService最終是來自Hadoop的AuxiliaryService概念,AuxiliaryService爲計算節點NodeManager常駐的服務線程,早期的MapReduce是進程級別的調度,ShuffleMap完成shuffle文件的輸出以後,即立即退出,在ShuffleReduce過程中由誰來提供文件的讀取服務呢?即AuxiliaryService,每一個ShuffleMap都會將自己在本地的輸出,註冊到AuxiliaryService,由AuxiliaryService提供本地數據的清理以及外部讀取的功能。

在目前Spark中,也提供了這樣的一個AuxiliaryService:YarnShuffleService,但是對於Spark不是必須的,如果你考慮到需要“通過減輕因爲fetch帶來對Executor的壓力”,那麼就可以嘗試嘗試。

同時,如果啓用了外部的ShuffleService,對於shuffleClient也不是使用上面的NettyBlockTransferService,而是專門的ExternalShuffleClient,功能邏輯基本一致!

Spark Network的功能應用–新的RPC框架

Akka的通信模型是基於Actor,一個Actor可以理解爲一個Service服務對象,它可以針對相應的RPC請求進行處理,如下所示,定義了一個最爲基本的Actor:

class HelloActor extends Actor {
    def receive = {
        case "hello" => println("world")
        case _       => println("huh?")
    }
}
//
Receive = PartialFunction[Any, Unit]

Actor內部只有唯一一個變量(當然也可以理解爲函數了),即Receive,它爲一個偏函數,通過case語句可以針對Any信息可以進行相應的處理,這裏Any消息在實際項目中就是消息包。

另外一個很重要的概念就是ActorSystem,它是一個Actor的容器,多個Actor可以通過name->Actor的註冊到Actor中,在ActorSystem中可以根據請求不同將請求路由給相應的Actor。ActorSystem和一組Actor構成一個完整的Server端,此時客戶端通過host:port與ActorSystem建立連接,通過指定name就可以相應的Actor進行通信,這裏客戶端就是ActorRef。所有Akka整個RPC通信系列是由Actor,ActorRef,ActorSystem組成。

Spark基於這個思想在上述的Network的基礎上實現一套自己的RPC Actor模型,從而取代Akka。其中RpcEndpoint對於Actor,RpcEndpointRef對應ActorRef,RpcEnv即對應了ActorSystem。

下面我們具體進行分析它的實現原理。

private[spark] trait RpcEndpoint {
    def receive: PartialFunction[Any, Unit] = {
        case _ => throw new SparkException()
    }
    def receiveAndReply(context: RpcCallContext): PartialFunction[Any, Unit] = {
        case _ => context.sendFailure(new SparkException())
    }
    //onStart(),onStop()
}

RpcEndpoint與Actor一樣,不同RPC Server可以根據業務需要指定相應receive/receiveAndReply的實現,在Spark內部現在有N多個這樣的Actor,比如Executor就是一個Actor,它處理來自Driver的LaunchTask/KillTask等消息。

RpcEnv相對於ActorSystem:

  • 首先它作爲一個Server,它通過NettyRpcHandler來提供了Server的服務能力
  • 其次它作爲RpcEndpoint的容器,它提供了setupEndpoint(name,endpoint)接口,從而實現將一個RpcEndpoint以一個Name對應關係註冊到容器中,從而通過Server對外提供Service
  • 最後它作爲Client的適配器,它提供了setupEndpointRef/setupEndpointRefByURI接口,通過指定Server端的Host和PORT,並指定RpcEndpointName,從而獲取一個與指定Endpoint通信的引用。

RpcEndpointRef即爲與相應Endpoint通信的引用,它對外暴露了send/ask等接口,實現將一個Message發送到Endpoint中

這就是新版本的RPC框架的基本功能,它的實現基本上與Akka無縫對接,業務的遷移的功能很小,目前基本上都全部遷移完了。

RpcEnv內部實現原理

RpcEnv不僅從外部接口與Akka基本一致,在內部的實現上,也基本差不多,都是按照MailBox的設計思路來實現的

與上圖所示,RpcEnv即充當着Server,同時也爲Client內部實現。 當As Server,RpcEnv會初始化一個Server,並註冊NettyRpcHandler,在前面描述過,RpcHandler的receive接口負責對每一個請求進行處理,一般情況下,簡單業務可以在RpcHandler直接完成請求的處理,但是考慮一個RpcEnv的Server上會掛載了很多個RpcEndpoint,每個RpcEndpoint的RPC請求頻率不可控,因此需要對一定的分發機制和隊列來維護這些請求,其中Dispatcher爲分發器,InBox即爲請求隊列;

在將RpcEndpoint註冊到RpcEnv過程中,也間接的將RpcEnv註冊到Dispatcher分發器中,Dispatcher針對每個RpcEndpoint維護一個InBox,在Dispatcher維持一個線程池(線程池大小默認爲系統可用的核數,當然也可以通過spark.rpc.netty.dispatcher.numThreads進行配置),線程針對每個InBox裏面的請求進行處理。當然實際的處理過程是由RpcEndpoint來完成。

這就是RpcEnv As Server的基本過程!

其次RpcEnv也完成Client的功能實現,RpcEndpointRef是以RpcEndpoint爲單位,即如果一個進程需要和遠程機器上N個RpcEndpoint服務進行通信,就對應N個RpcEndpointRef(後端的實際的網絡連接是公用,這個是TransportClient內部提供了連接池來實現的),當調用一個RpcEndpointRef的ask/send等接口時候,會將把“消息內容+RpcEndpointRef+本地地址”一起打包爲一個RequestMessage,交由RpcEnv進行發送。注意這裏打包的消息裏面包括RpcEndpointRef本身是很重要的,從而可以由Server端識別出這個消息對應的是哪一個RpcEndpoint。

和發送端一樣,在RpcEnv中,針對每個remote端的host:port維護一個隊列,即OutBox,RpcEnv的發送僅僅是把消息放入到相應的隊列中,但是和發送端不一樣的是:在OutBox中沒有維護一個所謂的線程池來定時清理OutBox,而是通過一堆synchronized來實現的,這點值得商討。

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