叢集RPC通訊

來源:互聯網
上載者:User

標籤:

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

先看看叢集中RPC的整個通訊過程,假設從節點node1開始一個RPC調用,①先將待傳遞的資料放到NIO叢集通訊架構(這裡使用的是tribes架構)中;②由於使用的是NIO模式,線程無需阻塞直接返回;③由於與叢集其他節點通訊需要花銷若干時間,為了提高CPU使用率當前線程應該放棄CPU的使用權進行等待操作;④NIO叢集通訊架構tribes接收到node2節點的響應訊息,並將訊息封裝成Response對象儲存至響應數組;⑤tribes接收到node4節點的響應訊息,由於是使用了並行通訊,所以node4可能比node3先返回訊息,並將訊息封裝成Response對象儲存至響應數組;⑥tribes最後接收到node3節點的響應訊息,並將訊息封裝成Response對象儲存至響應數組;⑦現在所有節點的響應都已經收集完畢,是時候通知剛剛被阻塞的那條線程了,原來的線程被notify醒後拿到所有節點的響應Response[]進行處理,至此完成了整個叢集RPC過程。

 

上面整個過程是在只有一條線程的情況下,一切看起來沒什麼問題,但如果有多條線程並發調用則會導致一個問題:線程與響應的對應關係將被打亂,無法確定哪個線程對應哪幾個響應。因為NIO通訊架構不會每個線程都獨自使用一個socket通道,為提高效能一般都是使用長串連,所有線程公用一個socket通道,這時就算線程一比線程二先放入tribes也不能保證響應一比響應二先接收到,所以接收到響應一後不知道該通知線程一還是線程二。只有解決了這個問題才能保證RPC調用的正確性。

要解決線程與響應對應的問題就需要維護一個線程響應關係列表,響應從關係列表中就能尋找對應的線程,如,在發送之前產生一個UUID標識,此標識要保證同socket中唯一,再把UUID與線程對象關係對應起來,可使用Map資料結構實現,UUID的值作為key,線程對應的鎖對象為value。接著制定一個協議報文,UUID作為報文的其中一部分,報文發往另一個節點node2後將響應資訊message放入報文中並返回,node1對接收到的報文進行解包根據UUID去尋找並喚起對應的線程,告訴它“你要的訊息已經收到,往下處理吧”。但在叢集環境下,我們更希望是叢集中所有節點的訊息都接收到了才往下處理,如下半部分,一個UUID1的請求報文會發往node2、node3和node4三個節點,這時假如只接收到一個響應則不喚起線程,直到node2、node3對應UUID1的響應報文都接收到後才喚起對應線程往下執行。同樣地,UUID2、UUID3的報文訊息都是如此處理,最後叢集中對應的響應都能正確回到各自線程上。

 

用簡單代碼實現一個RPC例子,選擇一個叢集通訊架構負責底層通訊,這裡使用tribes,接著往下:

①定義一個RPC介面,這些方法是預留提供給上層具體邏輯處理的入口,replyRequest方法用於處理響應邏輯,leftOver方法用於殘留請求的邏輯處理。

public interface RpcCallback {

    public Serializable replyRequest(Serializable msg, Member sender);

    public void leftOver(Serializable msg, Member sender);

}

②定義通訊訊息協議,實現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);

}

}

③響應類型,提供多種喚起線程的條件,一共四種類型,分別表示接收到第一個響應就喚起線程、接收到叢集中大多數節點的響應就喚起線程、接收到叢集中所有節點的響應才喚起線程、無需等待響應的無響應模式。

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;

}

④響應對象,用於封裝接收到的訊息,Member在通訊架構tribes是節點的抽象,這裡用來表示來源節點。

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;

}

}

⑤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()]);

    }

}

⑥RPC核心類,是整個RPC的抽象,它要實現tribes架構的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;

}

}

⑦自訂一個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如何?,屏蔽了繁雜重複的網路傳輸操作,為上層提供了很大的方便。

叢集RPC通訊

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.