叢集RPC通訊怎麼做

超人汪小建發表於2017-12-05

RPC

RPC即遠端過程呼叫,它的提出旨在消除通訊細節、遮蔽繁雜且易錯的底層網路通訊操作,像呼叫本地服務一般地呼叫遠端服務,讓業務開發者更多關注業務開發而不必考慮網路、硬體、系統的異構複雜環境。

RPC過程

先看看叢集中RPC的整個通訊過程,假設從節點node1開始一個RPC呼叫,

  1. 先將待傳遞的資料放到NIO叢集通訊框架中;
  2. 由於使用的是NIO模式,執行緒無需阻塞直接返回;
  3. 由於與叢集其他節點通訊需要花銷若干時間,為了提高CPU使用率當前執行緒應該放棄CPU的使用權進行等待操作;
  4. NIO叢集通訊框架接收到node2節點的響應訊息,並將訊息封裝成Response物件儲存至響應陣列;
  5. 通訊框架接收到node4節點的響應訊息,由於是使用了並行通訊,所以node4可能比node3先返回訊息,並將訊息封裝成Response物件儲存至響應陣列;
  6. 通訊框架最後接收到node3節點的響應訊息,並將訊息封裝成Response物件儲存至響應陣列;
  7. 現在所有節點的響應都已經收集完畢,是時候通知剛剛被阻塞的那條執行緒了,原來的執行緒被notify醒後拿到所有節點的響應Response[]進行處理,至此完成了整個叢集RPC過程。

叢集RPC通訊怎麼做

多執行緒

上面整個過程是在只有一條執行緒的情況下,一切看起來沒什麼問題,但如果有多條執行緒併發呼叫則會導致一個問題:執行緒與響應的對應關係將被打亂,無法確定哪個執行緒對應哪幾個響應。

因為NIO通訊框架不會每個執行緒都獨自使用一個socket通道,為提高效能一般都是使用長連線,所有執行緒共用一個socket通道,這時就算執行緒一比執行緒二先放入通訊框架也不能保證響應一比響應二先接收到,所以接收到響應一後不知道該通知執行緒一還是執行緒二。只有解決了這個問題才能保證RPC呼叫的正確性。

怎麼解決多執行緒

要解決執行緒與響應對應的問題就需要維護一個執行緒響應關係列表,響應從關係列表中就能查詢對應的執行緒,如圖,在傳送之前生成一個UUID標識,此標識要保證同socket中唯一,再把UUID與執行緒物件關係對應起來,可使用Map資料結構實現,UUID的值作為key,執行緒對應的鎖物件為value。

叢集RPC通訊怎麼做

接著制定一個協議報文,UUID作為報文的其中一部分,報文發往另一個節點node2後將響應資訊message放入報文中並返回,node1對接收到的報文進行解包根據UUID去查詢並喚起對應的執行緒,告訴它“你要的訊息已經收到,往下處理吧”。但在叢集環境下,我們更希望是叢集中所有節點的訊息都接收到了才往下處理,如圖下半部分,一個UUID1的請求報文會發往node2、node3和node4三個節點,這時假如只接收到一個響應則不喚起執行緒,直到node2、node3對應UUID1的響應報文都接收到後才喚起對應執行緒往下執行。同樣地,UUID2、UUID3的報文訊息都是如此處理,最後叢集中對應的響應都能正確回到各自執行緒上。

來個例子

用簡單程式碼實現一個RPC例子,自行選擇一個叢集通訊框架負責底層通訊,接著往下:

  1. 定義一個RPC介面,這些方法是預留提供給上層具體邏輯處理的入口,replyRequest方法用於處理響應邏輯,leftOver方法用於殘留請求的邏輯處理。
public interface RpcCallback {  
    public Serializable replyRequest(Serializable msg, Member sender);  
    public void leftOver(Serializable msg, Member sender);  
}  
複製程式碼
  1. 定義通訊訊息協議,實現Externalizable介面自定義序列化和反序列化,message用於存放響應訊息,uuid標識用於關聯執行緒,rpcId用於標識RPC例項,reply表示是否回覆。
public class RpcMessage implements Externalizable {
  protected Serializable message;
  protected byte[] uuid;
  protected byte[] rpcId;
  protected boolean reply = false;

  public RpcMessage() {}

  public RpcMessage(byte[] rpcId, byte[] uuid, Serializable message) {
    this.rpcId = rpcId;
    this.uuid = uuid;
    this.message = message;
  }

  @Override
  public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
    reply = in.readBoolean();
    int length = in.readInt();
    uuid = new byte[length];
    in.readFully(uuid);
    length = in.readInt();
    rpcId = new byte[length];
    in.readFully(rpcId);
    message = (Serializable) in.readObject();
  }

  @Override
  public void writeExternal(ObjectOutput out) throws IOException {
    out.writeBoolean(reply);
    out.writeInt(uuid.length);
    out.write(uuid, 0, uuid.length);
    out.writeInt(rpcId.length);
    out.write(rpcId, 0, rpcId.length);
    out.writeObject(message);
  }
}

複製程式碼
  1. 響應型別,提供多種喚起執行緒的條件,一共四種型別,分別表示接收到第一個響應就喚起執行緒、接收到叢集中大多數節點的響應就喚起執行緒、接收到叢集中所有節點的響應才喚起執行緒、無需等待響應的無響應模式。
public class RpcResponseType {  
  public static final int FIRST_REPLY = 1;  
  public static final int MAJORITY_REPLY = 2;  
  public static final int ALL_REPLY = 3;  
  public static final int NO_REPLY = 4;  
}  
複製程式碼
  1. 響應物件,用於封裝接收到的訊息,Member在通訊框架是節點的抽象,這裡用來表示來源節點。
public class RpcResponse {
  private Member source;
  private Serializable message;

  public RpcResponse() {}

  public RpcResponse(Member source, Serializable message) {
    this.source = source;
    this.message = message;
  }

  public void setSource(Member source) {
    this.source = source;
  }

  public void setMessage(Serializable message) {
    this.message = message;
  }

  public Member getSource() {
    return source;
  }

  public Serializable getMessage() {
    return message;
  }
}
 
複製程式碼
  1. RPC響應集,用於存放同個UUID的所有響應。
public class RpcCollector {  
    public ArrayList<RpcResponse> responses = new ArrayList<RpcResponse>();   
    public byte[] key;  
    public int options;  
    public int destcnt;  
    public RpcCollector(byte[] key, int options, int destcnt) {  
        this.key = key;  
        this.options = options;  
        this.destcnt = destcnt;  
    }  
    public void addResponse(Serializable message, Member sender){  
        RpcResponse resp = new RpcResponse(sender,message);  
        responses.add(resp);  
    }  
    public boolean isComplete() {  
        if ( destcnt <= 0 ) return true;  
        switch (options) {  
            case RpcResponseType.ALL_REPLY:  
                return destcnt == responses.size();  
            case RpcResponseType.MAJORITY_REPLY:  
            {  
                float perc = ((float)responses.size()) / ((float)destcnt);  
                return perc >= 0.50f;  
            }  
            case RpcResponseType.FIRST_REPLY:  
                return responses.size()>0;  
            default:  
                return false;  
        }  
    }  
    public RpcResponse[] getResponses() {  
        return responses.toArray(new RpcResponse[responses.size()]);  
    }  
}  
複製程式碼
  1. RPC核心類,是整個RPC的抽象,它實現通訊框架的ChannelListener介面,實現了該介面就能在messageReceived方法中處理接收到的訊息。因為所有的訊息都會通過此方法,所以它必須要根據key去處理對應的執行緒,同時它也要負責呼叫RpcCallback介面定義的相關的方法,例如響應請求的replyRequest方法和處理殘留的響應leftOver方法,殘留響應是指有時我們在接收到第一個響應後就喚起執行緒。
public class RpcChannel implements ChannelListener {
  private Channel channel;
  private RpcCallback callback;
  private byte[] rpcId;
  private int replyMessageOptions = 0;
  private HashMap<byte[], RpcCollector> responseMap = new HashMap<byte[], RpcCollector>();

  public RpcChannel(byte[] rpcId, Channel channel, RpcCallback callback) {
    this.rpcId = rpcId;
    this.channel = channel;
    this.callback = callback;
    channel.addChannelListener(this);
  }

  public RpcResponse[] send(Member[] destination, Serializable message, int rpcOptions,
      int channelOptions, long timeout) throws ChannelException {
    int sendOptions = channelOptions & ~Channel.SEND_OPTIONS_SYNCHRONIZED_ACK;
    byte[] key = UUIDGenerator.randomUUID(false);
    RpcCollector collector = new RpcCollector(key, rpcOptions, destination.length);
    try {
      synchronized (collector) {
        if (rpcOptions != RpcResponseType.NO_REPLY) responseMap.put(key, collector);
        RpcMessage rmsg = new RpcMessage(rpcId, key, message);
        channel.send(destination, rmsg, sendOptions);
        if (rpcOptions != RpcResponseType.NO_REPLY) collector.wait(timeout);
      }
    } catch (InterruptedException ix) {
      Thread.currentThread().interrupt();
    } finally {
      responseMap.remove(key);
    }
    return collector.getResponses();
  }

  @Override
  public void messageReceived(Serializable msg, Member sender) {
    RpcMessage rmsg = (RpcMessage) msg;
    byte[] key = rmsg.uuid;
    if (rmsg.reply) {
      RpcCollector collector = responseMap.get(key);
      if (collector == null) {
        callback.leftOver(rmsg.message, sender);
      } else {

        synchronized (collector) {
          if (responseMap.containsKey(key)) {
            collector.addResponse(rmsg.message, sender);
            if (collector.isComplete()) collector.notifyAll();
          } else {
            callback.leftOver(rmsg.message, sender);
          }
        }
      }
    } else {
      Serializable reply = callback.replyRequest(rmsg.message, sender);
      rmsg.reply = true;
      rmsg.message = reply;
      try {
        channel.send(new Member[] {sender}, rmsg,
            replyMessageOptions & ~Channel.SEND_OPTIONS_SYNCHRONIZED_ACK);
      } catch (Exception x) {}
    }
  }

  @Override
  public boolean accept(Serializable msg, Member sender) {
    if (msg instanceof RpcMessage) {
      RpcMessage rmsg = (RpcMessage) msg;
      return Arrays.equals(rmsg.rpcId, rpcId);
    } else
      return false;
  }
}

複製程式碼
  1. 自定義一個RPC,它要實現RpcCallback介面,分別對請求處理和殘留響應處理,這裡請求處理僅僅是簡單返回“hello,response for you!”作為響應訊息,殘留響應處理則是簡單輸出“receive a leftover message!”。假如整個叢集有五個節點,由於接收模式設定成了FIRST_REPLY,所以每個只會接受一個響應訊息,其他的響應都被當做殘留響應處理。
public class MyRPC implements RpcCallback {
  @Override
  public Serializable replyRequest(Serializable msg, Member sender) {
    RpcMessage mapmsg = (RpcMessage) msg;
    mapmsg.message = "hello,response for you!";
    return mapmsg;
  }

  @Override
  public void leftOver(Serializable msg, Member sender) {
    System.out.println("receive a leftover message!");
  }

  public static void main(String[] args) {
    MyRPC myRPC = new MyRPC();
    byte[] rpcId = new byte[] {1, 1, 1, 1};
    byte[] key = new byte[] {0, 0, 0, 0};
    String message = "hello";
    int sendOptions = Channel.SEND_OPTIONS_SYNCHRONIZED_ACK | Channel.SEND_OPTIONS_USE_ACK;
    RpcMessage msg = new RpcMessage(rpcId, key, (Serializable) message);
    RpcChannel rpcChannel = new RpcChannel(rpcId, channel, myRPC);
    RpcResponse[] resp =
        rpcChannel.send(channel.getMembers(), msg, RpcResponseType.FIRST_REPLY, sendOptions, 3000);
    while (true)
      Thread.currentThread().sleep(1000);
  }
}

複製程式碼

可以看到通過上面的RPC封裝後,上層可以把更多的精力關注到訊息邏輯處理上面了,而不必關注具體的網路IO如何實現,遮蔽了繁雜重複的網路傳輸操作,為上層提供了很大的方便。

=============廣告時間===============

公眾號的選單已分為“分散式”、“機器學習”、“深度學習”、“NLP”、“Java深度”、“Java併發核心”、“JDK原始碼”、“Tomcat核心”等,可能有一款適合你的胃口。

鄙人的新書《Tomcat核心設計剖析》已經在京東銷售了,有需要的朋友可以購買。感謝各位朋友。

為什麼寫《Tomcat核心設計剖析》

=========================

歡迎關注:

這裡寫圖片描述

相關文章