轉載自: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來實現的,這點值得商討。