[以浪爲碼]Spark源碼閱讀01-網絡傳輸 network

本文通過閱讀 Spark 網絡傳輸相關的代碼,位置在包 org.apache.spark.network 中,來了解 Spark 對網絡傳輸的實現。

Spark 底層的網絡傳輸當前通過 Netty 實現。

TransportContext 是傳輸服務的上下文,他可以創建傳輸服務端 TransportServer 與客戶端工廠 TransportClientFactory,並且使用 TransportChannelHandler 設置 Netty 服務端與客戶端的 pipelines。

網絡傳輸的客戶端 TransportClient 提供了兩種通信方向:控制相關的 RPC 與 數據相關的"塊獲取(chunk fetching)"。其中對 RPC 的處理是在 TransportContext 之外執行(比如在用戶提供的處理器中);另外他負責將流中的無組織數據通過零拷貝的方式設置進一個一個塊組成的流中。

TransportServerTransportClientFactory 都需要爲每個 channel 創建一個 TransportChannelHandler, 每個TransportChannelHandler 都包含一個 TransportClient ,這可以讓服務端使用現存的 channel 響應消息。

pipeline 設置

設置 Netty pipelines 的代碼如下:

code 1

  public TransportChannelHandler initializePipeline(
      SocketChannel channel,
      RpcHandler channelRpcHandler) {
    try {
      TransportChannelHandler channelHandler = createChannelHandler(channel, channelRpcHandler);
      ChunkFetchRequestHandler chunkFetchHandler =
        createChunkFetchHandler(channelHandler, channelRpcHandler);
      ChannelPipeline pipeline = channel.pipeline()
        .addLast("encoder", ENCODER)
        .addLast(TransportFrameDecoder.HANDLER_NAME, NettyUtils.createFrameDecoder())
        .addLast("decoder", DECODER)
        .addLast("idleStateHandler",
          new IdleStateHandler(0, 0, conf.connectionTimeoutMs() / 1000))
        // NOTE: Chunks are currently guaranteed to be returned in the order of request, but this
        // would require more logic to guarantee if this were not part of the same event loop.
        .addLast("handler", channelHandler);
      // Use a separate EventLoopGroup to handle ChunkFetchRequest messages for shuffle rpcs.
      if (conf.getModuleName() != null &&
          conf.getModuleName().equalsIgnoreCase("shuffle")
          && !isClientOnly) {
        pipeline.addLast(chunkFetchWorkers, "chunkFetchHandler", chunkFetchHandler);
      }
      return channelHandler;
    } catch (RuntimeException e) {
      logger.error("Error while initializing Netty pipeline", e);
      throw e;
    }
  }

由代碼可知,網絡傳輸使用一個 pipeline 來解決客戶端與服務端的消息處理,這裏分別描述消息的發送與消息的接收。

消息發送相關 Handler

消息發送的 Handler 即擁有出站功能的 Handler ,按照消息處理順序如下 :

  • encoder
  • idleStateHandler

encoder

encoder 即 編碼器,出站 Handler,他負責將發送方發送的Message實例編碼爲 Spark 自定義的消息段格式,以讓接收方可以方便進行消息處理。每個消息段的格式如下表所示:

Message 接口代表Spark的消息,有許多的子類代表不同的消息類型。

Message total Length Message Type Body(Option)
8 bytes 1 bytes

一共有三部分:

  1. Frame total Length:該消息段的總長度。一共佔 8 個字節,存儲一個 64 位 long 類型整型。
  2. Message Type: 消息類型(Message.type()),以區分消息以使用對應處理方式。佔一個字節,存儲一個 8 位整型,即最多可以表示 128 種類型。
  3. Body: 最後就爲數據部分,當然一個消息可以不需要數據部分。

idleStateHandler 貌似在消息發送時沒有作用,在下一節介紹。

消息接受相關 Handler

消息接受相關的 handler 即具有入站功能的 Handler,按照消息處理順序如下:

  • frameDecoder
  • decoder
  • idleStateHandler
  • transportChannelHandler(TransportChannelHandler 的實例)
  • chunkFetchHandler

frameDecoder

首先是 frameDecoder,他負責將接收到的 ByteBuf 按照消息格式解碼爲一個個消息段,使一個 ByteBuf 對應一個完整的消息,如此後面 handler 可以直接放心的使用協議規則去處理每一個傳來的ByteBuf.

此外,frameDecoder 可以配置攔截器,攔截器會攔截髮送到 frameDecoder 的數據,依次將每個 ByteBuf 灌入到攔截器中做一些其他的操作,且被攔截器攔截的ByteBuf 不會出現在之後的 handler 中,這個過程直到攔截器不再接受數據。Spark 使用攔截器完成數據上傳的處理。

decoder

即消息解碼器,他負責將 ByteBuf 解碼爲 Message實例。一個入站 Handler, 與 編碼器相對應,他會根據消息段中的 Message Type 來把消息轉換爲對應類型消息對象(Message)。

idleStateHandler

idleStateHandler 顧名思義是一個空閒狀態處理器,他是一個雙向處理器,即入站出站處理器。他負責檢查 channel 的活躍狀況——在一定時間內(默認 120s)是否有讀寫操作,如果持續沒有任何操作,就向其後的 handler 發送一個 IdleStateEvent,觸發其後的 handler 的 userEventTriggered 方法來進行一些處理。

transportChannelHandler

transportChannelHandler 是需要模塊用戶參與定義的 處理器,該處理器同時可以定義服務端對請求消息的處理與客戶端對響應消息的處理。用戶使用 org.apache.spark.network.server.RpcHandler 參與消息處理的定義。

每個TransportChannelHandler 都包含一個 TransportClient ,這可以讓服務端使用現存的 channel 響應消息。所以他提供了雙向傳輸的功能,服務端可以作爲客戶端向另一方發送請求,雖然帶入了一定的複雜性,但避免了 channel 冗餘。

transportChannelHandler 還負責處理超時(空閒)連接,transportChannelHandleridleStateHandler 之後,他實現的 userEventTriggered 方法,idleStateHandler 發送超時事件過來的時候,如果發現在配置的超時時間內沒有進行任何寫入或讀取操作,就將持有的 TransportClient 設爲超時並關閉 Netty 的 ChannelHandlerContext,避免資源的無效佔用。另外還會檢查是否還存在沒有被響應的請求,有表示是非正常超時,連接可能已經死亡,會打出 error 級別的日誌給與提醒。

chunkFetchHandler

chunkFetchHandler 只有在使用網絡傳輸的模塊爲 shuffle 時纔會被設置到 pipeline 上。這個入站 Handler 是爲了處理 ChunkFetchRequest 塊獲取請求而專門準備的。爲了避免同時幾百個塊請求作用在同一個 server 上時,因爲磁盤訪問的阻塞導致整個server被阻塞而不能接受其他的請求。


總的來說,利於 netty, 該 pipeline 定義了 Spark 消息與數據傳輸的基本過程。主要包括按照消息格式解編碼數據、處理超時、以及需要外部自定義的消息處理過程。簡化了傳輸的實現。

傳輸處理器 TransportChannelHandler

TransportChannelHandler 是處理網絡消息的主要抽象,他的構建過程如下:

code 2



public class TransportContext {
  ...
  private TransportChannelHandler createChannelHandler(Channel channel, RpcHandler rpcHandler) {
    TransportResponseHandler responseHandler = new TransportResponseHandler(channel);
    TransportClient client = new TransportClient(channel, responseHandler);
    TransportRequestHandler requestHandler = new TransportRequestHandler(channel, client,
      rpcHandler, conf.maxChunksBeingTransferred());
    return new TransportChannelHandler(client, responseHandler, requestHandler,
      conf.connectionTimeoutMs(), closeIdleConnections, this);
  }
  ...
}

他需要一個 TransportClient、響應與請求處理器、TransportContext 以及超時與是否處理空閒連接的配置。而 請求處理器TransportRequestHandler需要一個RpcHandler 來參與定義請求的處理。而對於 TransportResponseHandler 他只根據響應的類型與id執行每個請求預定義的回調函數完成響應的處理。

TransportChannelHandler 讀取到消息後,會根據消息的類型,調用請求處理器或響應處理器的處理方法,特別的,他不會處理ChunkFetchRequest 消息,如 code3 所示:

code 3


public class TransportChannelHandler extends SimpleChannelInboundHandler<Message>

  @Override
  public void channelRead0(ChannelHandlerContext ctx, Message request) throws Exception {
    // 調用具體的處理器處理
    if (request instanceof RequestMessage) {
      requestHandler.handle((RequestMessage) request);
    } else if (request instanceof ResponseMessage) {
      responseHandler.handle((ResponseMessage) request);
    } else {
      ctx.fireChannelRead(request);
    }
  }
  
  @Override
  public boolean acceptInboundMessage(Object msg) throws Exception {
    // 是塊獲取請求,則不理睬。
    if (msg instanceof ChunkFetchRequest) {
      return false;
    } else {
      return super.acceptInboundMessage(msg);
    }
  }
  
}

所以 TransportChannelHandler更像是一個代理處理器,只有他顯露給 Netty pipeline,而具體的處理過程是由具體的兩個處理器兄弟完成的。

另外上面提到的在 TransportChannelHandler 中的超時處理邏輯如下 code 4,可以發現傳送給他的 client,只是爲了設置超時。而這裏的超時的判斷標準是 channel 是否長時間沒有再有讀寫請求,而不會顧忌還有沒有未完成的請求。:

code 4

public class TransportChannelHandler extends SimpleChannelInboundHandler<Message>

  @Override
  public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
    if (evt instanceof IdleStateEvent) {
      IdleStateEvent e = (IdleStateEvent) evt;
      synchronized (this) {
        // 檢查是否超時
        boolean isActuallyOverdue =
          System.nanoTime() - responseHandler.getTimeOfLastRequestNs() > requestTimeoutNs;
        if (e.state() == IdleState.ALL_IDLE && isActuallyOverdue) {
          // 判斷是否有未收到響應的請求,有就只多一條打印 error 日誌。
          if (responseHandler.numOutstandingRequests() > 0) {
            String address = getRemoteAddress(ctx.channel());
            logger.error("Connection to {} has been quiet for {} ms while there are outstanding " +
              "requests. Assuming connection is dead; please adjust spark.network.timeout if " +
              "this is wrong.", address, requestTimeoutNs / 1000 / 1000);
            client.timeOut();
            ctx.close();
          } else if (closeIdleConnections) {
            // While CloseIdleConnections is enable, we also close idle connection
            client.timeOut();
            ctx.close();
          }
        }
      }
    }
    ctx.fireUserEventTriggered(evt);
  }
}

網絡傳輸客戶端 TransportClient

這裏具體介紹網絡傳輸的客戶端 org.apache.spark.network.client.TransportClient,他可以發送 rpc 請求與獲取在流中連續的塊。該客戶端打算允許進行大量數據的傳輸,他可以將數據分塊到幾百 kb 或幾MB 的塊中。

要注意的是,TransportClient 讀取的數據,必須是在運輸層外預先已經設置好的。該類提供了方便的sendRPC方法來提供客戶端與服務端之前的控制型交互。

需要使用 TransportClientFactory 來獲得 TransportClient。一個客戶端可能會作用在多個流上,但是爲了避免請求失序,一個流只能被一個客戶端操作。

並且 TransportClient 只是被用來發送請求,而具體響應的處理是由 TransportResponseHandler 來負責的。

TransportClient 是線程安全的。

我們看下注釋中該客戶端比較經典的使用例子,獲取文件流數據:

code 5

client.sendRPC(new OpenFile("/foo")) // returns StreamId = 100
client.fetchChunk(streamId = 100, chunkIndex = 0, callback)
client.fetchChunk(streamId = 100, chunkIndex = 1, callback)
// ...
client.sendRPC(new CloseStream(100))

先使用 sendRPC 請求文件的流數據,返回一個流id,之後使用流 id 通過 fetchChunk 依次獲取快數據,最後再sendRPC關閉流。

sendRPC 方法代碼如下:

code 6

public class TransportClient implements Closeable {
  /**
   * 發送 RPC 請求給 服務端,在請求失敗或成功的時候調用 callback 中對應的方法。
   *
   * @param message 要發送的消息.
   * @param callback 處理回覆的回調請求.
   * @return The RPC's id.
   */
  public long sendRpc(ByteBuffer message, RpcResponseCallback callback) {
    if (logger.isTraceEnabled()) {
      logger.trace("Sending RPC to {}", getRemoteAddress(channel));
    }
	// 生成請求 id。
    long requestId = requestId();
    // 向 ResponseHandler 中記錄已經發出的請求。
    handler.addRpcRequest(requestId, callback);
	// 該監聽器主要負責在請求成功的時候打印相關日誌,在失敗的時候調用 RpcResponseCallback#onFailure(Throwable e) 處理失敗請求。
    RpcChannelListener listener = new RpcChannelListener(requestId, callback);
    // 發送請求
    channel.writeAndFlush(new RpcRequest(requestId, new NioManagedBuffer(message)))
      .addListener(listener);

    return requestId;
  }
}

如代碼註釋,客戶端在開始請求時主要做了2件事情,一就是告訴 TransportResponseHandler 我發送了一個請求以及這個請求返回後如何處理(在 RpcResponseCallback 中定義,見 code 7);第二個就是將消息發出去。並且其他的請求方法也基本如此,有:

  • fetchChunk 獲取單個塊數據
  • stream 獲取流數據
  • sendRpc 發送 RPC 請求
  • uploadStream 使用流的方式上傳數據到server
  • sendRpcSync RPC 請求的同步實現。
  • send 發送一個不需要響應的消息。

總結來說就是 快請求,RPC請求與 stream 流請求。

看完了消息的發送,接下來我們看一下TransportResponseHandler 是如何處理響應的。

code 7 RpcResponseCallback.java

/**
 * 爲 PRC 請求的響應到達時準備的回調。要麼成功,要麼失敗。
 */
public interface RpcResponseCallback {
  /**
   * Successful serialized result from server.
   * 在 方法 返回後, 返回的數據將不再可用,如果想要使用返回的返回的數據,需要在調用該方法之前拷貝一份數據。
   */
  void onSuccess(ByteBuffer response);

  void onFailure(Throwable e);
}

TransportResponseHandler

由 code 3 可知,對請求的響應的處理是通過調用 TransportResponseHandler#handle(ResponseMessage message) 方法來完成的, 由於方法行數較多,下面只貼出 handle 方法處理 PRC 響應的部分,詳細可看完整代碼。

code 8

public class TransportResponseHandler extends MessageHandler<ResponseMessage> {
  
  // 用於記錄已經發出的 RPC 請求。
  private final Map<Long, RpcResponseCallback> outstandingRpcs;
  
  // 代碼 6 調用的用於記錄已發請求的方法。
  public void addRpcRequest(long requestId, RpcResponseCallback callback) {
    updateTimeOfLastRequest();
    outstandingRpcs.put(requestId, callback);
  }

  public void removeRpcRequest(long requestId) {
    outstandingRpcs.remove(requestId);
  }

  ...
  public void handle(ResponseMessage message) throws Exception {
    ...
    else if (message instanceof RpcResponse) {
      RpcResponse resp = (RpcResponse) message;
      RpcResponseCallback listener = outstandingRpcs.get(resp.requestId);
      if (listener == null) {
        logger.warn("Ignoring response for RPC {} from {} ({} bytes) since it is not outstanding",
          resp.requestId, getRemoteAddress(channel), resp.body().size());
      } else {
        outstandingRpcs.remove(resp.requestId);
        try {
          listener.onSuccess(resp.body().nioByteBuffer());
        } finally {
          resp.body().release();
        }
      }
    } else if (message instanceof RpcFailure) {
      RpcFailure resp = (RpcFailure) message;
      RpcResponseCallback listener = outstandingRpcs.get(resp.requestId);
      if (listener == null) {
        logger.warn("Ignoring response for RPC {} from {} ({}) since it is not outstanding",
          resp.requestId, getRemoteAddress(channel), resp.errorString);
      } else {
        outstandingRpcs.remove(resp.requestId);
        listener.onFailure(new RuntimeException(resp.errorString));
      }
    } 
    // 對其他響應類型處理。
    ...
  }
  ...
}

很簡單,TransportResponseHandler 記錄了請求的請求id以及處理回調。在進行響應處理時,會先找請求對應的回調,如果沒找到,則不僅進行處理(只打日誌),找到則調用回調的方法,代表成功的消息則調用回調的 onSuccess() 方法,失敗型消息則調用 onFailure() 方法,然後將請求從記錄 Map 中移出。

客戶端部分介紹完畢,下面介紹下服務端部分。

TransportClientFactory 還未介紹,這裏暫時不影響整理邏輯,他是一個客戶端對象池,他會使用現存的客戶端對象或者對應地址的客戶端不存在則新創建一個客戶端綁定的 channel 上並配置 pipeline,其中涉及到一些客戶端以及安全驗證方面的配置,待之後有機會再補充。

網絡傳輸服務端 TransportServer

TransportServer 表示一個網絡傳輸服務,該類很簡單,就是利用 Netty 得到一個傳輸服務。在他的構造函數中,他需要一個 RpcHandler 抽象類,最終由 TransportRequestHandler 使用,來讓使用傳輸模塊的用戶實現它,以來自定義響應的具體處理過程, RpcHandler 主要代碼如下:

code 9

public abstract class RpcHandler {
  private static final RpcResponseCallback ONE_WAY_CALLBACK = new OneWayRpcCallback();
  
  // 接收一個單獨的 RPC 請求,client 負責返回響應給發送請求的客戶端,callback 定義了接收任務成功成功與失敗時候的行爲。
  // 對於單個TransportClient,不會並行調用此方法或#receiveStream
  public abstract void receive(
      TransportClient client,
      ByteBuffer message,
      RpcResponseCallback callback);
  
  // 接收一個單獨的 RPC 請求,且請求中有流形式的數據。
  // 返回爲 StreamCallbackWithID 定義的流的處理行爲,注意這裏返回的只是定義了行爲的StreamCallbackWithID 子類對象,
  // 具體的處理過程並且不是該方法調用發起的。
  // 
  // 他需要一個 傳輸客戶端, 用於反向返送請求給客戶端,但當下Spark的實現只用到了客戶端的信息,沒有用他發過數據。
  // messageHeader 爲流的 header(元數據)
  // 對於單個TransportClient,不會並行調用此方法或#receive
  public StreamCallbackWithID receiveStream(
      TransportClient client,
      ByteBuffer messageHeader,
      RpcResponseCallback callback) {
    throw new UnsupportedOperationException();
  }
  // 獲得一個 StreamManager 。StreamManager 負責管理客戶端請求的流數據。
  public abstract StreamManager getStreamManager();
  
  // 接收不需響應的請求的處理方法。
  public void receive(TransportClient client, ByteBuffer message) {
    receive(client, message, ONE_WAY_CALLBACK);
  }
  
  // RpcResponseCallback 的一個實現,這裏定義行爲都是打印日誌。
  private static class OneWayRpcCallback implements RpcResponseCallback {

    private static final Logger logger = LoggerFactory.getLogger(OneWayRpcCallback.class);

    @Override
    public void onSuccess(ByteBuffer response) {
      logger.warn("Response provided for one-way RPC.");
    }

    @Override
    public void onFailure(Throwable e) {
      logger.error("Error response provided for one-way RPC.", e);
    }

  }

code 10 StreamCallbackWithID.java

public interface StreamCallback {
  /** Called upon receipt of stream data. */
  void onData(String streamId, ByteBuffer buf) throws IOException;

  /** Called when all data from the stream has been received. */
  void onComplete(String streamId) throws IOException;

  /** Called if there's an error reading data from the stream. */
  void onFailure(String streamId, Throwable cause) throws IOException;
}

public interface StreamCallbackWithID extends StreamCallback {
  String getID();
}

由代碼可知,RpcHandler 定義了接收 RPC 請求與流數據上傳的處理管理方法的接口,僅僅知道 RpcHandler 還不太足夠,我們需要知道 TransportRequestHandler 如何調用這些方法的, 接下來介紹 TransportRequestHandler

TransportRequestHandler

TransportRequestHandler 接受客戶端的請求並返回塊數據。每個處理器都附着在一個 channel 上,並且追蹤在該 channel 上的流,爲了在 channel 停止的時候清理他們。

請求的類型一共有四種, 見代碼 code 11,這裏依次介紹。

code 11

  public void handle(RequestMessage request) {
    
    if (request instanceof RpcRequest) {
      //  RPC 請求
      processRpcRequest((RpcRequest) request);
    } else if (request instanceof OneWayMessage) {
      // 不需響應消息
      processOneWayMessage((OneWayMessage) request);
    } else if (request instanceof StreamRequest) {
      // 流請求
      processStreamRequest((StreamRequest) request);
    } else if (request instanceof UploadStream) {
      // 流上傳
      processStreamUpload((UploadStream) request);
    } else {
      throw new IllegalArgumentException("Unknown request type: " + request);
    }
  }

RPC 請求處理

處理 RPC 請求的代碼如下,可以看到數據接收後直接由 rpcHandler 來進行處理,然後在 receive 方法中通過調用 callback 的方法來發送處理結果 :

code 12

  private void processRpcRequest(final RpcRequest req) {
    try {
      rpcHandler.receive(reverseClient, req.body().nioByteBuffer(), new RpcResponseCallback() {
        @Override
        public void onSuccess(ByteBuffer response) {
          respond(new RpcResponse(req.requestId, new NioManagedBuffer(response)));
        }

        @Override
        public void onFailure(Throwable e) {
          respond(new RpcFailure(req.requestId, Throwables.getStackTraceAsString(e)));
        }
      });
    } catch (Exception e) {
      logger.error("Error while invoking RpcHandler#receive() on RPC id " + req.requestId, e);
      respond(new RpcFailure(req.requestId, Throwables.getStackTraceAsString(e)));
    } finally {
      req.body().release();
    }
  }
  // 發送響應消息。
  private ChannelFuture respond(Encodable result) {
    SocketAddress remoteAddress = channel.remoteAddress();
    return channel.writeAndFlush(result).addListener(future -> {
      if (future.isSuccess()) {
        logger.trace("Sent result {} to client {}", result, remoteAddress);
      } else {
        logger.error(String.format("Error sending result %s to %s; closing connection",
          result, remoteAddress), future.cause());
        channel.close();
      }
    });
  }
  
  // 對於不需回覆的的消息,直接調用 rpcHandler 對應的 `receive` 方法。
  private void processOneWayMessage(OneWayMessage req) {
    try {
      rpcHandler.receive(reverseClient, req.body().nioByteBuffer());
    } catch (Exception e) {
      logger.error("Error while invoking RpcHandler#receive() for one-way message.", e);
    } finally {
      req.body().release();
    }
  }

流請求的處理

在之前先簡單介紹一下 StreamManager, 顧名思義就是管理流的類,這裏的流可以是文件或者內存數據,且流是在傳輸層之外的。將會爲每個流設置一個id,每個流可以分爲一個一個的塊(一個流可以只有一個塊)並以 ManagedBuffer 的形式展現給使用者,並且他保證了一個流只能被一個客戶端連接讀取,意味着流要以順序的方式被讀取,並且連接一旦關閉,與之關聯的流就不能再被使用。StreamManager部分代碼如下:

code 13

public abstract class StreamManager {
  // 打開流,返回對應的 ManagedBuffer。
  public ManagedBuffer openStream(String streamId) {
    throw new UnsupportedOperationException();
  }
  // 獲取流對應 index 的塊。
  public abstract ManagedBuffer getChunk(long streamId, int chunkIndex);
  // 返回已經傳輸的塊的數量
  public long chunksBeingTransferred() {
    return 0;
  }
  // 當塊開始傳送的時候可以調用一下這個方法,讓子類可以處理一下狀態問題。
  public void chunkBeingSent(long streamId) { }
  // 在流傳送時可以調用一下這個方法。
  public void streamBeingSent(String streamId) { }
  
  // 當塊傳送成功時可以調用這個方法。
  public void chunkSent(long streamId) { }

  // 當流傳送成功時可以調用這個方法。
  public void streamSent(String streamId) { }
}

ManagedBuffer 代表一個被管理的數據,他提供了 StreamManager與使用者之間對流數據的抽象,且提供了一些非常方便的在 流塊 上的方法,比如轉爲各種形式的 buffer: NIO、Netty、InputStream,方便使用者在不同的情景下使用。

接下來繼續看處理流請求的代碼:

code 14

  private void processStreamRequest(final StreamRequest req) {
    if (logger.isTraceEnabled()) {
      logger.trace("Received req from {} to fetch stream {}", getRemoteAddress(channel),
        req.streamId);
    }

    long chunksBeingTransferred = streamManager.chunksBeingTransferred();
    if (chunksBeingTransferred >= maxChunksBeingTransferred) {
      logger.warn("The number of chunks being transferred {} is above {}, close the connection.",
        chunksBeingTransferred, maxChunksBeingTransferred);
      channel.close();
      return;
    }
    ManagedBuffer buf;
    try {
      buf = streamManager.openStream(req.streamId);
    } catch (Exception e) {
      logger.error(String.format(
        "Error opening stream %s for request from %s", req.streamId, getRemoteAddress(channel)), e);
      respond(new StreamFailure(req.streamId, Throwables.getStackTraceAsString(e)));
      return;
    }

    if (buf != null) {
      streamManager.streamBeingSent(req.streamId);
      respond(new StreamResponse(req.streamId, buf.size(), buf)).addListener(future -> {
        streamManager.streamSent(req.streamId);
      });
    } else {
      respond(new StreamFailure(req.streamId, String.format(
        "Stream '%s' was not found.", req.streamId)));
    }
  }

由 code 14可知,處理過程並不複雜,先是檢查 StreamManager 傳輸的塊的數量是否超過規定的數目,沒有超過則打開流,獲取ManagedBuffer,如果獲取成功,則發送成功的流響應(StreamResponse)給客戶端,否則流失敗響應(StreamFailure)。

流上傳請求的處理

代碼如下,具體過程介紹在中文註釋裏,請耐心看:

code 15


 private void processStreamUpload(final UploadStream req) {
    assert (req.body() == null);
    try {
      // 請求處理完成後要做的事情的定義。這裏就直接響應RpcResponse或RpcFailure給客戶端。
      RpcResponseCallback callback = new RpcResponseCallback() {
        @Override
        public void onSuccess(ByteBuffer response) {
          respond(new RpcResponse(req.requestId, new NioManagedBuffer(response)));
        }

        @Override
        public void onFailure(Throwable e) {
          respond(new RpcFailure(req.requestId, Throwables.getStackTraceAsString(e)));
        }
      };
      
      // 獲取段解碼器
      TransportFrameDecoder frameDecoder = (TransportFrameDecoder)
          channel.pipeline().get(TransportFrameDecoder.HANDLER_NAME);
      // 獲得上傳請求的元數據
      ByteBuffer meta = req.meta.nioByteBuffer();
      // 由 rpcHandler#receiveStream 完成對數據上傳的處理。
      StreamCallbackWithID streamHandler = rpcHandler.receiveStream(reverseClient, meta, callback);
      
      if (streamHandler == null) {
        throw new NullPointerException("rpcHandler returned a null streamHandler");
      }
      // 一個 StreamCallbackWithID 的包裝類。在原來的 StreamCallbackWithID 增加了回調方法的調用,
      // 應該是爲了讓 `receiveStream` 專注於數據處理,不必在意之後的響應返回。
      StreamCallbackWithID wrappedCallback = new StreamCallbackWithID() {
        @Override
        public void onData(String streamId, ByteBuffer buf) throws IOException {
          streamHandler.onData(streamId, buf);
        }

        @Override
        public void onComplete(String streamId) throws IOException {
           try {
             streamHandler.onComplete(streamId);
             callback.onSuccess(ByteBuffer.allocate(0));
           } catch (Exception ex) {
             IOException ioExc = new IOException("Failure post-processing complete stream;" +
               " failing this rpc and leaving channel active", ex);
             callback.onFailure(ioExc);
             streamHandler.onFailure(streamId, ioExc);
           }
        }

        @Override
        public void onFailure(String streamId, Throwable cause) throws IOException {
          callback.onFailure(new IOException("Destination failed while reading stream", cause));
          streamHandler.onFailure(streamId, cause);
        }

        @Override
        public String getID() {
          return streamHandler.getID();
        }
      };
      if (req.bodyByteCount > 0) {
        // 攔截器之前提到過,pipeline會把之後的數據都交由攔截器處理,這個攔截器會不斷的使用 wrappedCallback 的 onData 方法處理 channel 中的數據,
        // 直到一共處理的數據的長度與 req.bodyByteCount 的長度相同,纔將攔截器從 pipeline 中移除。
        StreamInterceptor<RequestMessage> interceptor = new StreamInterceptor<>(
          this, wrappedCallback.getID(), req.bodyByteCount, wrappedCallback);
        frameDecoder.setInterceptor(interceptor);
      } else {
        wrappedCallback.onComplete(wrappedCallback.getID());
      }
    } catch (Exception e) {
      logger.error("Error while invoking RpcHandler#receive() on RPC id " + req.requestId, e);
      respond(new RpcFailure(req.requestId, Throwables.getStackTraceAsString(e)));
      // We choose to totally fail the channel, rather than trying to recover as we do in other
      // cases.  We don't know how many bytes of the stream the client has already sent for the
      // stream, it's not worth trying to recover.
      channel.pipeline().fireExceptionCaught(e);
    } finally {
      req.meta.release();
    }
  }

ChunkFetchRequestHandler

最後就是快獲取請求的處理,上面介紹過是由 ChunkFetchRequestHandler 單獨處理的。處理代碼如下, 詳細見中文註釋,與流獲取很相似,都是利用 StreamManager 使用的:

code15

public class ChunkFetchRequestHandler extends SimpleChannelInboundHandler<ChunkFetchRequest> {
  protected void channelRead0(
      ChannelHandlerContext ctx,
      final ChunkFetchRequest msg) throws Exception {
    Channel channel = ctx.channel();
    if (logger.isTraceEnabled()) {
      logger.trace("Received req from {} to fetch block {}", getRemoteAddress(channel),
        msg.streamChunkId);
    }
    
    // 檢查已傳輸的塊數量的限制
    long chunksBeingTransferred = streamManager.chunksBeingTransferred();
    
    if (chunksBeingTransferred >= maxChunksBeingTransferred) {
      logger.warn("The number of chunks being transferred {} is above {}, close the connection.",
        chunksBeingTransferred, maxChunksBeingTransferred);
      channel.close();
      return;
    }
    ManagedBuffer buf;
    try {
      // 檢查當前的認證,具體由 streamManager 實現來保證。
      streamManager.checkAuthorization(client, msg.streamChunkId.streamId);
      // 註冊流到渠道上 
      streamManager.registerChannel(channel, msg.streamChunkId.streamId);
      // 根據請求 chunkIndex 獲取快
      buf = streamManager.getChunk(msg.streamChunkId.streamId, msg.streamChunkId.chunkIndex);
    } catch (Exception e) {
      logger.error(String.format("Error opening block %s for request from %s",
        msg.streamChunkId, getRemoteAddress(channel)), e);
      respond(channel, new ChunkFetchFailure(msg.streamChunkId,
        Throwables.getStackTraceAsString(e)));
      return;
    }
	// 通知 streamManager 塊開始傳輸了
    streamManager.chunkBeingSent(msg.streamChunkId.streamId);
    // 具體的傳輸動作
    respond(channel, new ChunkFetchSuccess(msg.streamChunkId, buf)).addListener(
      (ChannelFutureListener) future -> streamManager.chunkSent(msg.streamChunkId.streamId));
  }
}

安全驗證

有空再說。

實戰

有空再寫。可以先看 Spark 源碼中的測試 suite。

總結

Spark 網絡傳輸的源碼到此介紹的差不多了。總結什麼的再說了。

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