Hadoop -- RPC通信

Hadoop -- RPC通信

前言

本篇文章淺顯的介紹了Hadoop RPC的基礎以及三個大類RPC、Server、Client一些較爲重要的方法的源碼剖析,目的在於理解Hadoop RPC核心的原理以提升自身知識儲備。本篇博文參考了大量董西成老師的《Hadoop技術內幕-深入解析YARN架構設計與實現原理》,感謝老師的書籍在我學習Hadoop Yarn過程中給予的莫大幫助。

基礎庫

1. Protocol Buffers

Protocol Buffers是一種輕便高效的結構化數據存儲格式,可以用於結構化數據序列化/反序列化。目前作用於Yarn PRC中參數的序列化/反序列化,具有平臺無關、高性能(解析速度約是XML的20~100倍)、兼容性強、體積小(大小僅是XML文件的1/10~1/3)等優點

2. Apache Avro

Apache Avro是一個序列化框架,同時實現了PRC功能設計的最初動機是解決Yarn RPC兼容性和擴展性差等問題,但是在Yarn項目啓啓動時Apache Avro仍不成熟,考慮穩定性問題,Yarn暫時採用Protocol Buffers作爲序列化庫。目前用於MapReduce應用程序日誌(用於故障後應用程序恢復),今後可能會替代Protocol Buffers。

Hadoop RPC基礎介紹

一、通信模型

RPC通常採用客戶機/服務器模型。請求程序是一個客戶機,而服務提供程序則是一個服務器。一個典型的RPC框架包含以下幾個部分:

① 通信模塊:兩個相互協作的通信模塊實現請求-應答協議,它們在客戶和服務器之間傳遞和應答消息,一般不會對數據包進行任何處理。

② Stub程序: 客戶端和服務端均包含Stub程序,可看做一個代理程序。它使得遠程函數調用表現的像本地調用一樣

③調度程序:調用程序接收來自通信模塊的請求消息,並根據其中的標識選擇一個Stub程序進行處理。

④客戶程序/服務過程:請求的發出者與處理者。

通常,一個RPC請求從發送到獲取處理結果,有如下幾個步驟:

  1. 客戶程序以本地方式調用系統產生的Stub程序

  2. 該Stub程序將函數調用信息按照網絡通信模塊的要求封裝成消息包,並交給通信模塊發送到遠程服務器端

  3. 遠程服務器接收到此消息後,將此消息發送給相應的Stub程序

  4. Stub程序拆封消息,行程被調用過程要求的形式,並調用對應函數

  5. 被調用函數按照所獲參數執行,並將結果返回給Stub程序

  6. Stub程序將此結果封裝成消息,通過網絡通信模塊逐級地傳送給客戶程序

二、Hadoop RPC框架結構

①序列化層:主要作用是將結構化對象轉爲字節流以便於通過網絡進行傳輸或者寫入持久存儲,在RPC框架中,它主要用於將用戶請求中的參數或者應答轉化成字節流以便跨機器傳輸

②函數調用層:函數調用層主要功能是定位要調用的函數並執行該函數,Hadoop RPC採用了Java反射機制與動態代理實現了函數調用

③網絡傳輸層:描述了Client、Server之間的消息傳輸的方式,Hadoop RPC採用了基於TCP/IP的Socket機制

④服務器端處理框架。服務器端處理框架可被抽象爲網絡I/O模型,它描述了客戶端與服務器端間信息交互方式,它的設計直接決定着服務器端的併發處理能力。

三、RPC重要類詳解

Reactor架構圖

在看上圖時,我們需要先了解一些概念:

1)Listener線程:單線程,負責創建服務器監聽,即負責處理SelectionKey.OP_ACCEPT事件,一旦對應事件發生,就調用doAccept()方法對事件進行處理,處理方法其實只是將對應的channel封裝成Connection,Reader.getReader()負責選出一個Reader線程,然後把這個新的請求交付給這個Reader對象(添加到這個對象的pendingConnections隊列)。getReader()選擇Reader線程的方式爲簡單輪詢。

2)Reader線程:多線程,由Listener線程創建並管理,通過doRunLoop()方法,反覆從自己的pendingConnections 中取出連接通道,註冊到自己的readSelector,處理SelectionKey.OP_READ事件,一旦對應事件發生,則調用doRead()進行處理。doRead()的實際工作,是從請求頭中提取諸如callId、retry、rpcKind,生成對應的Call對象,放入callQueue中,callQueue 隊列將由Handler進行處理。

3)Call對象:封裝了RPC請求的信息,包括callId、retryCount、rpcKink(RPC.rpcKind)、clientId、connection信息。Reader線程創建了Call對象,封裝了請求信息,交付給下面的Handler線程。此後,信息在Reator的不同角色之間的傳遞都封裝在了Call對象中,包括請求、響應。

4)Handler線程:Handler的總體職責是取出Call對象中的用戶請求,對請求進行處理並拿到response,然後將response封裝在Call中,交付給Responder進行響應。

5)Responder線程:單線程,內部有一個Selector對象,負責監聽writeSelector上的SelectionKey.OP_WRITE,將response通過對應的連接返回給客戶端。後面我會詳細介紹到,並不是所有的寫都是Responder進行的,有一部分是Handler直接進行的:Handler在將響應交付給Responder之前,會檢查當前連接上的響應是否只有當前一個,如果是,就會嘗試在自己的當前線程中直接把響應發送出去,如果發現響應很多,或者這個響應無法完全發送給遠程客戶端,纔會將剩餘任務交付給Responder進行。

RPC

RPC類定義了一系列構建、銷燬RPC客戶端的方法,構造方法有getProxy和waitForProxy兩大類,銷燬則是stopProxy。而內部類Builder則是用於創建服務端時設置一些必要的參數。通過調動Builder.build()完成一個服務器對象的構建,最終調用start()實現Server的啓動。

Client

使用call()方法來執行某個遠程方法,涉及以下4個步驟:

  1. 創建一個Connection對象,並將遠程方法調用信息封裝成Call對象放到Connection對象的哈希表中

  2. 調用Connection類中的sendRpcRequest()方法將當前的Call對象發送給Server端;

  3. Server處理完RPC請求後,將結果通過網絡返回給Client端,Client端通過receiveRpcResponse()函數獲取結果

  4. Client檢查結果處理狀態(成功還是失敗),將對應的Call對象從哈希表中刪除

    public Writable call(RPC.RpcKind rpcKind, Writable rpcRequest,
        ConnectionId remoteId, int serviceClass,
        AtomicBoolean fallbackToSimpleAuth) throws IOException {
      final Call call = createCall(rpcKind, rpcRequest);
      //第一步創建Connection對象
      Connection connection = getConnection(remoteId, call, serviceClass,
        fallbackToSimpleAuth);
      try {
        connection.sendRpcRequest(call);                 // 第二步,將RPC請求發送到Server端
      } cathch(...)
    ​
      synchronized (call) {
        while (!call.done) {
          try {
            call.wait();                           // 第三步等待獲取結果
          } catch (...)
          ...
          return call.getRpcResponse(); //返回結果
       
      }
    }

Server

Server採用了許多提高併發處理能力的技術,主要包括線程池、事件驅動和Reactor設計模式等,Reactor是併發變成中的一種基於事件驅動的設計模式,具有通過派發I/O分離I/O操作事件提高系統的併發性能;提供了粗粒度的併發控制,使用單線程實現,避免複雜的同步處理。

Server類中有三個重要的內部類,都有各自的run()方法,分別是Listener、Responder、Hadnler。下面就要結合Server處理請求的過程來介紹這三個類。

Server接受來自Client客戶端的RPC請求。經過 調用相應的函數處理得到結果後返回給客戶端,所以Server可以分爲三個階段:

①接收請求

1)先來看一下Server類的構造函數,我只取了重要部分的代碼,可以看到內部去初始化幾個內部類,但是我們沒有看到Hadnler類,它是在Server類的start()方法中初始化並啓動的。

protected Server(String bindAddress, int port,
    Class<? extends Writable> rpcRequestClass, int handlerCount,
    int numReaders, int queueSizePerHandler, Configuration conf,
    String serverName, SecretManager<? extends TokenIdentifier> secretManager,
    String portRangeConfig)
  throws IOException {
  ...
  //起了一個監聽,用於監聽Client發過來的請求
  listener = new Listener();
 ...
  // Create the responder here
  responder = new Responder();
  
  if (secretManager != null || UserGroupInformation.isSecurityEnabled()) {
    SaslRpcServer.init(conf);
    saslPropsResolver = SaslPropertiesResolver.getInstance(conf);
  }
  
  this.exceptionsHandler.addTerseExceptions(StandbyException.class);
}

2)可以看到幾個重要內部類的初始化和啓動

public synchronized void start() {
  responder.start();
  listener.start();
  handlers = new Handler[handlerCount];
  
  for (int i = 0; i < handlerCount; i++) {
    handlers[i] = new Handler(i);
    handlers[i].start();
  }
}

3)那先來看一下Listener類的run方法,可以看到內部有一個doAcceept()

while (running) {
  SelectionKey key = null;
  try {
    getSelector().select();
    Iterator<SelectionKey> iter = getSelector().selectedKeys().iterator();
    while (iter.hasNext()) {
      key = iter.next();
      iter.remove();
      try {
        if (key.isValid()) {
          if (key.isAcceptable())
            doAccept(key);//執行ACCEPT對應的處理邏輯
        }
      } catch (IOException e) {
      }
      key = null;
    }

4) 跟蹤doAcceept()內部分析,發現初始化了一個Reader

void doAccept(SelectionKey key) throws InterruptedException, IOException,  OutOfMemoryError {
  ServerSocketChannel server = (ServerSocketChannel) key.channel();
  SocketChannel channel;
  while ((channel = server.accept()) != null) {
    channel.configureBlocking(false);
    channel.socket().setTcpNoDelay(tcpNoDelay);
    channel.socket().setKeepAlive(true);
    
    Reader reader = getReader(); //使用輪詢的方式選擇一個Reader
    Connection c = connectionManager.register(channel);
    // If the connectionManager can't take it, close the connection.
    if (c == null) {
      if (channel.isOpen()) {
        IOUtils.cleanup(null, channel);
      }
      connectionManager.droppedConnections.getAndIncrement();
      continue;
    }
    key.attach(c);  // so closeCurrentConnection can get the object
    reader.addConnection(c);
  }
}

5)那來看一下Reader都做了些什麼,再進入Reader內部的doRead()方法,Listener會將監聽到的新的請求交給Reader去處理,

      Connection c = (Connection)key.attachment();
      if (c == null) {
        return;  
      }
      c.setLastContact(Time.now());
      
      try {
        count = c.readAndProcess(); //這裏就是通過Listener監聽得到的請求後交給內部Reader去處理
      } catch (InterruptedException ieo) {
        LOG.info(Thread.currentThread().getName() + ": readAndProcess caught InterruptedException", ieo);
        throw ieo;
​

6)看一下關鍵代碼c.readAndProcess()

public int readAndProcess()
    throws WrappedRpcServerException, IOException, InterruptedException {
  while (true) {
    ...
    //讀取數據長度字段
    if (data == null) {
      dataLengthBuffer.flip();
      dataLength = dataLengthBuffer.getInt();
      checkDataLength(dataLength);
      data = ByteBuffer.allocate(dataLength);
    }
    //讀取數據
    count = channelRead(channel, data);
    
    if (data.remaining() == 0) {
      dataLengthBuffer.clear();
      data.flip();
      boolean isHeaderRead = connectionContextRead;
      processOneRpc(data.array()); //解析RPC請求,交付給具體的處理器類
      data = null;
      if (!isHeaderRead) {
        continue;
      }
    } 
    return count;
  }
}

7)那進入 processOneRpc(data.array())看一下,這個方法內部調用了processRpcRequest(header, dis),這個方法則是將請求封裝成了call對象放在了callQueue中等待Handler來處理這些信息。

try {
...
  
  if (callId < 0) { // callIds typically used during connection setup
    processRpcOutOfBandRequest(header, dis);
  } else if (!connectionContextRead) {
    throw new WrappedRpcServerException(
        RpcErrorCodeProto.FATAL_INVALID_RPC_HEADER,
        "Connection context not established");
  } else {
    processRpcRequest(header, dis);//真正處理RPC請求將請求封裝成call對象放在callQueue中
  }

②處理請求

1)接下來看一下Handler類似如何處理請求的,首先可以看到將之前放入的call從callQueue隊列中取出來

try {
  final Call call = callQueue.take(); // pop the queue; maybe blocked here
  if (LOG.isDebugEnabled()) {
    LOG.debug(Thread.currentThread().getName() + ": " + call + " for RpcKind " + call.rpcKind);
  }
  if (!call.connection.channel.isOpen()) {
    LOG.info(Thread.currentThread().getName() + ": skipped " + call);
    continue;
  }

2)然後交給對應的call函數去處理請求

if (call.connection.user == null) {
  value = call(call.rpcKind, call.connection.protocolName, call.rpcRequest, 

3)最終去設置Response,返回給對應的客戶端,內部就是通過Responsder去響應客戶端的,sendResponse()方法其實就是將Response放在了responseQueue隊列當中

  synchronized (call.connection.responseQueue) {
    setupResponse(buf, call, returnStatus, detailedErr,
        value, errorClass, error);
​
    // Discard the large buf and reset it back to smaller size
    // to free up heap.
    if (buf.size() > maxRespSize) {
      LOG.warn("Large response size " + buf.size() + " for call "
          + call.toString());
      buf = new ByteArrayOutputStream(INITIAL_RESP_BUF_SIZE);
    }
    //內部就是通過Responsder去響應客戶端的
    call.sendResponse();
  }
} catch (InterruptedException e) {

③返回結果

1)最終來看一下Responsder類中的run()方法,內部是如何處理responseQueue

while (iter.hasNext()) {
  SelectionKey key = iter.next();
  iter.remove();
  try {
    if (key.isWritable()) {
      doAsyncWrite(key);
    }
    }

2)進入doAsyncWritee(),可以看到同步去處理responseQueue,接下來去看一下processResponse(call.connection.responseQueue, false)

synchronized(call.connection.responseQueue) {
  if (processResponse(call.connection.responseQueue, false)) {
    try {
      key.interestOps(0);
    } catch (CancelledKeyException e) {

3)這裏則是最終在responseQueue裏添加返回的信息。以及內部有一個Selector對象,用於監聽SelectionKey.OP_WRITE。如果Handler沒能將結果一次性發送到客戶端時,迴向該對象註冊SelectionKey.OP_WRITE事件,進而由Responder線程採用異步方式繼續發送未發送完成的結果

call.connection.responseQueue.addFirst(call);//添加返回
if (inHandler) {
  // set the serve time when the response has to be sent later
  call.timestamp = Time.now();
  
  incPending();
  try {
    // Wakeup the thread blocked on select, only then can the call 
    // to channel.register() complete.
    writeSelector.wakeup();
    channel.register(writeSelector, SelectionKey.OP_WRITE, call);
  } catch (ClosedChannelException e) {
    //Its ok. channel might be closed else where.
    done = true;
  } finally {
    decPending();
  }
} 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章