你應該知道的 RPC 原理

 

在校期間你們都寫過很多程序,好比寫個hello world服務類,而後本地調用下,以下所示。這些程序的特色是服務消費方和服務提供方是本地調用關係。java

而一旦踏入公司尤爲是大型互聯網公司就會發現,公司的系統都由成千上萬大大小小的服務組成,各服務部署在不一樣的機器上,由不一樣的團隊負責。這時就會遇到兩個問題:1)要搭建一個新服務,免不了須要依賴他人的服務,而如今他人的服務都在遠端,怎麼調用?2)其它團隊要使用咱們的服務,咱們的服務該怎麼發佈以便他人調用?下文咱們將對這兩個問題展開探討。git

 

1github

2面試

3apache

public interface HelloWorldService {網絡

    String sayHello(String msg);數據結構

}併發

 

1負載均衡

2框架

3

4

5

6

7

8

public class HelloWorldServiceImpl implements HelloWorldService {

    @Override

    public String sayHello(String msg) {

        String result = "hello world " + msg;

        System.out.println(result);

        return result;

    }

}

 

1

2

3

4

5

6

public class Test {

     public static void main(String[] args) {

         HelloWorldService helloWorldService = new HelloWorldServiceImpl();

         helloWorldService.sayHello("test");

     }

}

 

1 如何調用他人的遠程服務?

因爲各服務部署在不一樣機器,服務間的調用免不了網絡通訊過程,服務消費方每調用一個服務都要寫一坨網絡通訊相關的代碼,不只複雜並且極易出錯。

若是有一種方式能讓咱們像調用本地服務同樣調用遠程服務,而讓調用者對網絡通訊這些細節透明,那麼將大大提升生產力,好比服務消費方在執行helloWorldService.sayHello(「test」)時,實質上調用的是遠端的服務。這種方式其實就是RPC(Remote Procedure Call Protocol),在各大互聯網公司中被普遍使用,如阿里巴巴的hsf、dubbo(開源)、Facebook的thrift(開源)、Google grpc(開源)、Twitter的finagle等。

要讓網絡通訊細節對使用者透明,咱們天然須要對通訊細節進行封裝,咱們先看下一個RPC調用的流程:

  • 1)服務消費方(client)調用以本地調用方式調用服務;
  • 2)client stub接收到調用後負責將方法、參數等組裝成可以進行網絡傳輸的消息體;
  • 3)client stub找到服務地址,並將消息發送到服務端;
  • 4)server stub收到消息後進行解碼;
  • 5)server stub根據解碼結果調用本地的服務;
  • 6)本地服務執行並將結果返回給server stub;
  • 7)server stub將返回結果打包成消息併發送至消費方;
  • 8)client stub接收到消息,並進行解碼;
  • 9)服務消費方獲得最終結果。

RPC的目標就是要2~8這些步驟都封裝起來,讓用戶對這些細節透明。

1.1 怎麼作到透明化遠程服務調用?

怎麼封裝通訊細節才能讓用戶像以本地調用方式調用遠程服務呢?對java來講就是使用代理!java代理有兩種方式:1) jdk 動態代理;2)字節碼生成。儘管字節碼生成方式實現的代理更爲強大和高效,但代碼不易維護,大部分公司實現RPC框架時仍是選擇動態代理方式。

下面簡單介紹下動態代理怎麼實現咱們的需求。咱們須要實現RPCProxyClient代理類,代理類的invoke方法中封裝了與遠端服務通訊的細節,消費方首先從RPCProxyClient得到服務提供方的接口,當執行helloWorldService.sayHello(「test」)方法時就會調用invoke方法。

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

public class RPCProxyClient implements java.lang.reflect.InvocationHandler{

    private Object obj;

    public RPCProxyClient(Object obj){

        this.obj=obj;

    }

    /**

     * 獲得被代理對象;

     */

    public static Object getProxy(Object obj){

        return java.lang.reflect.Proxy.newProxyInstance(obj.getClass().getClassLoader(),

                obj.getClass().getInterfaces(), new RPCProxyClient(obj));

    }

 

    /**

     * 調用此方法執行

     */

    public Object invoke(Object proxy, Method method, Object[] args)

            throws Throwable {

        //結果參數;

        Object result = new Object();

        // ...執行通訊相關邏輯

        // ...

        return result;

    }

}

 

1

2

3

4

5

6

public class Test {

    public static void main(String[] args) {

        HelloWorldService helloWorldService = (HelloWorldService)RPCProxyClient.getProxy(HelloWorldService.class);

        helloWorldService.sayHello("test");

    }

}

 

1.2  怎麼對消息進行編碼和解碼?

1.2.1 肯定消息數據結構

上節講了invoke裏須要封裝通訊細節,而通訊的第一步就是要肯定客戶端和服務端相互通訊的消息結構。客戶端的請求消息結構通常須要包括如下內容:

1)接口名稱

在咱們的例子裏接口名是「HelloWorldService」,若是不傳,服務端就不知道調用哪一個接口了;

2)方法名

一個接口內可能有不少方法,若是不傳方法名服務端也就不知道調用哪一個方法;

3)參數類型&參數值

參數類型有不少,好比有bool、int、long、double、string、map、list,甚至如struct(class);

以及相應的參數值;

4)超時時間

5)requestID,標識惟一請求id,在下面一節會詳細描述requestID的用處。

同理服務端返回的消息結構通常包括如下內容。

1)返回值

2)狀態code

3)requestID

1.2.2 序列化

一旦肯定了消息的數據結構後,下一步就是要考慮序列化與反序列化了。

什麼是序列化?序列化就是將數據結構或對象轉換成二進制串的過程,也就是編碼的過程。

什麼是反序列化?將在序列化過程當中所生成的二進制串轉換成數據結構或者對象的過程。

爲何須要序列化?轉換爲二進制串後纔好進行網絡傳輸嘛!爲何須要反序列化?將二進制轉換爲對象纔好進行後續處理!

現現在序列化的方案愈來愈多,每種序列化方案都有優勢和缺點,它們在設計之初有本身獨特的應用場景,那到底選擇哪一種呢?從RPC的角度上看,主要看三點:1)通用性,好比是否能支持Map等複雜的數據結構;2)性能,包括時間複雜度和空間複雜度,因爲RPC框架將會被公司幾乎全部服務使用,若是序列化上能節約一點時間,對整個公司的收益都將很是可觀,同理若是序列化上能節約一點內存,網絡帶寬也能省下很多;3)可擴展性,對互聯網公司而言,業務變化快,若是序列化協議具備良好的可擴展性,支持自動增長新的業務字段,刪除老的字段,而不影響老的服務,這將大大提供系統的健壯性。

目前國內各大互聯網公司普遍使用hessian、protobuf、thrift、avro等成熟的序列化解決方案來搭建RPC框架,這些都是久經考驗的解決方案。

1.3  通訊

消息數據結構被序列化爲二進制串後,下一步就要進行網絡通訊了。目前有兩種IO通訊模型:1)BIO;2)NIO。通常RPC框架須要支持這兩種IO模型,原理可參考:《一個故事講清楚 NIO》

如何實現RPC的IO通訊框架?1)使用java nio方式自研,這種方式較爲複雜,並且頗有可能出現隱藏bug,見過一些互聯網公司使用這種方式;2)基於mina,mina在早幾年比較火熱,不過這些年版本更新緩慢;3)基於netty,如今不少RPC框架都直接基於netty這一IO通訊框架,好比阿里巴巴的HSF、dubbo,Twitter的finagle等。

1.4  消息裏爲何要帶有requestID?

若是使用netty的話,通常會用channel.writeAndFlush()方法來發送消息二進制串,這個方法調用後對於整個遠程調用(從發出請求到接收到結果)來講是一個異步的,即對於當前線程來講,將請求發送出來後,線程就能夠日後執行了,至於服務端的結果,是服務端處理完成後,再以消息的形式發送給客戶端的。因而這裏出現如下兩個問題:

1)怎麼讓當前線程「暫停」,等結果回來後,再向後執行?

2)若是有多個線程同時進行遠程方法調用,這時創建在client server之間的socket鏈接上會有不少雙方發送的消息傳遞,先後順序也多是隨機的,server處理完結果後,將結果消息發送給client,client收到不少消息,怎麼知道哪一個消息結果是原先哪一個線程調用的?

以下圖所示,線程A和線程B同時向client socket發送請求requestA和requestB,socket前後將requestB和requestA發送至server,而server可能將responseA先返回,儘管requestA請求到達時間更晚。咱們須要一種機制保證responseA丟給ThreadA,responseB丟給ThreadB。

怎麼解決呢?

1)client線程每次經過socket調用一次遠程接口前,生成一個惟一的ID,即requestID(requestID必需保證在一個Socket鏈接裏面是惟一的),通常經常使用AtomicLong從0開始累計數字生成惟一ID;

2)將處理結果的回調對象callback,存放到全局ConcurrentHashMap裏面put(requestID, callback);

3)當線程調用channel.writeAndFlush()發送消息後,緊接着執行callback的get()方法試圖獲取遠程返回的結果。在get()內部,則使用synchronized獲取回調對象callback的鎖,再先檢測是否已經獲取到結果,若是沒有,而後調用callback的wait()方法,釋放callback上的鎖,讓當前線程處於等待狀態。

4)服務端接收到請求並處理後,將response結果(此結果中包含了前面的requestID)發送給客戶端,客戶端socket鏈接上專門監聽消息的線程收到消息,分析結果,取到requestID,再從前面的ConcurrentHashMap裏面get(requestID),從而找到callback對象,再用synchronized獲取callback上的鎖,將方法調用結果設置到callback對象裏,再調用callback.notifyAll()喚醒前面處於等待狀態的線程。

 

1

2

3

4

5

6

7

public Object get() {

        synchronized (this) {  // 旋鎖

            while (!isDone) {  // 是否有結果了

                wait(); //沒結果是釋放鎖,讓當前線程處於等待狀態

            }

        }

    }

 

1

2

3

4

5

6

7

private void setDone(Response res) {

        this.res = res;

        isDone = true;

        synchronized (this) { //獲取鎖,由於前面wait()已經釋放了callback的鎖了

            notifyAll(); // 喚醒處於等待的線程

        }

    }

 

2 如何發佈本身的服務?

如何讓別人使用咱們的服務呢?有同窗說很簡單嘛,告訴使用者服務的IP以及端口就能夠了啊。確實是這樣,這裏問題的關鍵在因而自動告知仍是人肉告知。

人肉告知的方式:若是你發現你的服務一臺機器不夠,要再添加一臺,這個時候就要告訴調用者我如今有兩個ip了,大家要輪詢調用來實現負載均衡;調用者咬咬牙改了,結果某天一臺機器掛了,調用者發現服務有一半不可用,他又只能手動修改代碼來刪除掛掉那臺機器的ip。現實生產環境固然不會使用人肉方式。

有沒有一種方法能實現自動告知,即機器的增添、剔除對調用方透明,調用者再也不須要寫死服務提供方地址?固然能夠,現現在zookeeper被普遍用於實現服務自動註冊與發現功能!

簡單來說,zookeeper能夠充當一個服務註冊表(Service Registry),讓多個服務提供者造成一個集羣,讓服務消費者經過服務註冊表獲取具體的服務訪問地址(ip+端口)去訪問具體的服務提供者。以下圖所示:

具體來講,zookeeper就是個分佈式文件系統,每當一個服務提供者部署後都要將本身的服務註冊到zookeeper的某一路徑上: /{service}/{version}/{ip:port}, 好比咱們的HelloWorldService部署到兩臺機器,那麼zookeeper上就會建立兩條目錄:分別爲/HelloWorldService/1.0.0/100.19.20.01:16888  /HelloWorldService/1.0.0/100.19.20.02:16888。

zookeeper提供了「心跳檢測」功能,它會定時向各個服務提供者發送一個請求(實際上創建的是一個 socket 長鏈接),若是長期沒有響應,服務中心就認爲該服務提供者已經「掛了」,並將其剔除,好比100.19.20.02這臺機器若是宕機了,那麼zookeeper上的路徑就會只剩/HelloWorldService/1.0.0/100.19.20.01:16888。

服務消費者會去監聽相應路徑(/HelloWorldService/1.0.0),一旦路徑上的數據有任務變化(增長或減小),zookeeper都會通知服務消費方服務提供者地址列表已經發生改變,從而進行更新。

更爲重要的是zookeeper 與生俱來的容錯容災能力(好比leader選舉),能夠確保服務註冊表的高可用性。

3 小結

RPC幾乎是每個從學校進入互聯網公司的同窗都要首先學習的框架,以前面試過一個在大型互聯網公司工做過兩年的同窗,對RPC仍是停留在使用層面,這是不該該的。本文也僅是對RPC的一個比較粗糙的描述,但願對你們有所幫助,錯誤之處也請指出修正。

4 一些開源的RPC框架

https://github.com/alibaba/dubbo

http://thrift.apache.org/?cm_mc_uid=87762817217214314008006&cm_mc_sid_50200000=1444181090

相關文章
相關標籤/搜索