不懂RPC實現原理怎能實現架構夢

RPC(Remote Procedure Call Protocol)——遠程過程調用協議,它是一種經過網絡從遠程計算機程序上請求服務,而不須要了解底層網絡技術的協議。RPC協議假定某些傳輸協議的存在,如TCP或UDP,爲通訊程序之間攜帶信息數據。在OSI網絡通訊模型中,RPC跨越了傳輸層和應用層。RPC使得開發包括網絡分佈式多程序在內的應用程序更加容易。 RPC採用客戶機/服務器模式。請求程序就是一個客戶機,而服務提供程序就是一個服務器。首先,客戶機調用進程發送一個有進程參數的調用信息到服務進程,而後等待應答信息。在服務器端,進程保持睡眠狀態直到調用信息到達爲止。當一個調用信息到達,服務器得到進程參數,計算結果,發送答覆信息,而後等待下一個調用信息,最後,客戶端調用進程接收答覆信息,得到進程結果,而後調用執行繼續進行。java

有多種 RPC模式和執行。最初由 Sun 公司提出。IETF ONC 憲章從新修訂了 Sun 版本,使得 ONC RPC 協議成爲 IETF 標準協議。如今使用最廣泛的模式和執行是開放式軟件基礎的分佈式計算環境(DCE)。程序員

在支付系統的微服務架構中,基礎服務的構建是重中之重, 本文重點分析如何使用Apache Thrift + Google Protocol Buffer來構建基礎服務。數據庫

1、RPC vs Restful緩存

在微服務中,使用什麼協議來構建服務體系,一直是個熱門話題。 爭論的焦點集中在兩個候選技術: (binary) RPC or Restful。性能優化

以Apache Thrift爲表明的二進制RPC,支持多種語言(但不是全部語言),四層通信協議,性能高,節省帶寬。相對Restful協議,使用Thrifpt RPC,在同等硬件條件下,帶寬使用率僅爲前者的20%,性能卻提高一個數量級。可是這種協議最大的問題在於,沒法穿透防火牆。服務器

以Spring Cloud爲表明所支持的Restful 協議,優點在於可以穿透防火牆,使用方便,語言無關,基本上可使用各類開發語言實現的系統,均可以接受Restful 的請求。 但性能和帶寬佔用上有劣勢。網絡

因此,業內對微服務的實現,基本是肯定一個組織邊界,在該邊界內,使用RPC; 邊界外,使用Restful。這個邊界,能夠是業務、部門,甚至是全公司。數據結構

2、 RPC技術選型架構

RPC技術選型上,原則也是選擇本身熟悉的,或者公司內部內定的框架。 若是是新業務,則如今可選的框架其實也很少,卻也足夠讓人糾結。併發

Apache Thrift

國外用的多,源於facebook,後捐獻給Apache基金。是Apache的頂級項目Apache Thrift。使用者包括facebook, Evernote, Uber, Pinterest等大型互聯網公司。 而在開源界,Apache hadoop/hbase也在使用Thrift做爲內部通信協議。 這是目前最爲成熟的框架,優勢在於穩定、高性能。缺點在於它僅提供RPC服務,其餘的功能,包括限流、熔斷、服務治理等,都須要本身實現,或者使用第三方軟件。

Dubbo

國內用的多,源於阿里公司。 性能上略遜於Apache Thrift,但自身集成了大量的微服務治理功能,使用起來至關方便。 Dubbo的問題在於,該系統目前已經很長時間沒有維護更新了。官網顯示最近一次的更新也是8個月前。

Google Protobuf

和Apache Thrift相似,Google Protobuf也包括數據定義和服務定義兩部分。問題是,Google Protobuf一直只有數據模型的實現,沒有官方的RPC服務的實現。 直到2015年才推出gRPC,做爲RPC服務的官方實現。但缺少重量級的用戶。

以上僅作定性比較。定量的對比,網上有很多資料,可自行查閱。 此外,還有一些不錯的RPC框架,好比Zeroc ICE等,不在本文的比較範圍。

Thrift 提供多種高性能的傳輸協議,但在數據定義上,不如Protobuf強大。

同等格式數據, Protobuf壓縮率和序列化/反序列化性能都略高。

Protobuf支持對數據進行自定義標註,並能夠經過API來訪問這些標註,這使得Protobuf在數據操控上很是靈活。好比能夠經過option來定義protobuf定義的屬性和數據庫列的映射關係,實現數據存取。

數據結構升級是常見的需求,Protobuf在支持數據向下兼容上作的很是不錯。只要實現上處理得當,接口在升級時,老版本的用戶不會受到影響。

而Protobuf的劣勢在於其RPC服務的實現性能不佳(gRPC)。爲此,Apache Thrift + Protobuf的RPC實現,成爲很多公司的選擇。

3、Apache Thrift + Protobuf

如上所述,利用Protobuf在靈活數據定義、高性能的序列化/反序列化、兼容性上的優點,以及Thrift在傳輸上的成熟實現,將二者結合起來使用,是很多互聯網公司的選擇。

服務定義:

service HelloService{

binary hello(1: binary hello_request);

}

協議定義:

message HelloRequest{

optional string user_name = 1; //訪問這個接口的用戶

optional string password = 2; //訪問這個接口的密碼

optional string hello_word = 3; //其餘參數;

}

message HelloResponse{

optional string hello_word = 1; //訪問這個接口的用戶

}

想對於純的thrift實現,這種方式雖然看起來繁瑣,但其在可擴展性、可維護性和服務治理上,能夠帶來很多便利。

4、服務註冊與發現

Spring cloud提供了服務註冊和發現功能,若是須要本身實現,能夠考慮使用Apache Zookeeper做爲註冊表,使用Apache Curator來管理Zookeeper的連接,它實現以下功能:

偵聽註冊表項的變化,一旦有更新,能夠從新加載註冊表。

管理到zookeeper的連接,若是出現問題,則進行重試。

Curator的重試策略是可配置的,提供以下策略:

BoundedExponentialBackoffRetry

ExponentialBackoffRetry

RetryForever

RetryNTimes

RetryOneTime

RetryUntilElapsed

通常使用指數延遲策略,好比重試時間間隔爲1s,2s, 4s, 8s……指數增長,避免把服務器打死。

對服務註冊來講,註冊表結構須要詳細設計,通常註冊表結構會按照以下方式組織:

機房區域-部門-服務類型-服務名稱-服務器地址

因爲在zookeeper上的註冊和發現有必定的延遲,因此在實現上也得注意,當服務啓動成功後,才能註冊到zookeeper上;當服務要下線或者重啓前,須要先斷開同zookeeper的鏈接,再中止服務。

5、鏈接池

RPC服務訪問和數據庫相似,創建連接是一個耗時的過程,鏈接池是服務調用的標配。目前尚未成熟的開源Apache Thrift連接池,通常互聯網公司都會開發內部自用的連接池。本身實現能夠基於JDBC連接池作改進,好比參考Apache commons DBCP連接池,使用Apache Pools來管理連接。 在接口設計上,鏈接池須要管理的是RPC 的Transport:

public interface TransportPool {  /**  * 獲取一個transport  * @return* @throws TException  */  public TTransport getTransport() throws TException;}

鏈接池實現的主要難點在於如何從多個服務器中選舉出來爲當前調用提供服務的鏈接。好比目前有10臺機器在提供服務,上一次分配的是第4臺服務器,本次應該分配哪一臺?在實現上,須要收集每臺機器的QOS以及當前的負擔,分配一個最佳的鏈接。

6、API網關

隨着公司業務的增加,RPC服務愈來愈多,這也爲服務調用帶來挑戰。若是有一個應用須要調用多個服務,對這個應用來講,就須要維護和多個服務器之間的連接。服務的重啓,都會對鏈接池以及客戶端的訪問帶來影響。爲此,在微服務中,普遍會使用到API網關。API網關能夠認爲是一系列服務集合的訪問入口。從面向對象設計的角度看,它與外觀模式相似,實現對所提供服務的封裝。

網關做用

API網關自己不提供服務的具體實現,它根據請求,將服務分發到具體的實現上。 其主要做用:

API路由: 接受到請求時,將請求轉發到具體實現的worker機器上。避免使用方創建大量的鏈接。

協議轉換: 原API可能使用http或者其餘的協議來實現的,統一封裝爲rpc協議。注意,這裏的轉換,是批量轉換。也就是說,原來這一組的API是使用http實現的,如今要轉換爲RPC,因而引入網關來統一處理。對於單個服務的轉換,仍是單獨開發一個Adapter服務來執行。

封裝公共功能: 將微服務治理相關功能封裝到網關上,簡化微服務的開發,這包括熔斷、限流、身份驗證、監控、負載均衡、緩存等。

分流:經過控制API網關的分發策略,能夠很容易實現訪問的分流,這在灰度測試和AB測試時特別有用。

解耦合

RPC API網關在實現上,難點在於如何作到服務無關。咱們知道使用Nginx實現HTTP的路由網關,能夠實現和服務無關。而RPC網關因爲實現上的不規範,很難實現和服務無關。統一使用thrift + protobuf 來開發RPC服務能夠簡化API網關的開發,避免爲每一個服務上線而帶來的網關的調整,使得網關和具體的服務解耦合:

每一個服務實現的worker機器將服務註冊到zookeeper上;

API網關接收到zookeeper的變動,更新本地的路由表,記錄服務和worker(鏈接池)的映射關係。

當請求被提交到網關上時,網關能夠從rpc請求中提取出服務名稱,以後根據這個名稱,找到對應的worker機(鏈接池),調用該worker上的服務,接受到結果後,將結果返回給調用方。

權限和其餘

Protobuf的一個重要特性是,數據的序列化和名稱無關,只和屬性類型、編號有關。 這種方式,間接實現了類的繼承關係。以下所示,咱們能夠經過Person類來解析Girl和Boy的反序列化流:

message Person {

optional string user_name = 1;

optional string password = 2; }message Girl {

optional string user_name = 1;

optional string password = 2;

optional string favorite_toys = 3; }message Boy {

optional string user_name = 1;

optional string password = 2;

optional int32  favorite_club_count = 3;

optional string favorite_sports = 4; }

咱們只要對服務的輸入參數作合理的編排,將經常使用的屬性使用固定的編號來表示,既可使用通用的基礎類來解析輸入參數。好比咱們要求全部輸入的第一個和第二個元素必須是user_name和password,則咱們就可使用Person來解析這個輸入,從而能夠實現對服務的統一身份驗證,並基於驗證結果來實施QPS控制等工做。

7、熔斷與限流

Netflix Hystrix提供不錯的熔斷和限流的實現,參考其在GitHub上的項目介紹。這裏簡單說下熔斷和限流實現原理。

熔斷通常採用電路熔斷器模式(Circuit Breaker Patten)。當某個服務發生錯誤,每秒錯誤次數達到閾值時,再也不響應請求,直接返回服務器忙的錯誤給調用方。 延遲一段時間後,嘗試開放50%的訪問,若是錯誤仍是高,則繼續熔斷;不然恢復到正常狀況。

限流指按照訪問方、IP地址或者域名等方式對服務訪問進行限制,一旦超過給定額度,則禁止其訪問。 除了使用Hystrix,若是要本身實現,能夠考慮使用使用Guava RateLimiter

8、服務演化

隨着服務訪問量的增長,服務的實現也會不斷演化以提高性能。主要的方法有讀寫分離、緩存等。

讀寫分離

針對實體服務,讀寫分離是提高性能的第一步。 實現讀寫分離通常有如下幾種方式:

一、在同構數據庫上使用主從複製的方式: 通常數據庫,好比MySQL、HBase、Mongodb等,都提供主從複製功能。數據寫入主庫,讀取、檢索等操做都從從庫上執行,實現讀寫分離。這種方式實現簡單,無需額外開發數據同步程序。通常來講,對寫入有事務要求的數據庫,在讀取上的性能會比較差。雖然能夠經過增長從庫的方式來sharding請求,但這也會致使成本增長。

二、在異構數據庫上進行讀寫分離。發揮不一樣數據庫的優點,經過消息機制或者其餘方式,將數據從主庫同步到從庫。 好比使用MySQL做爲主庫來寫入,數據寫入時投遞消息到消息服務器,同步程序接收到消息後,將數據更新到讀庫中。可使用Redis,Mongodb等內存數據庫做爲讀庫,用來支持根據ID來讀取;使用Elastic做爲從庫,支持搜索。

三、微服務技術是程序員都離不開的話題,說到這裏,也給你們推薦一個交流學習平臺:架構交流羣650385180,裏面會分享一些資深架構師錄製的視頻錄像:有Spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化這些成爲架構師必備的知識體系。還能領取免費的學習資源,如下的課程體系圖也是在羣裏獲取。相信對於已經工做和遇到技術瓶頸的碼友,在這裏會有你須要的內容。

緩存使用

若是數據量大,使用從庫也會致使從庫成本很是高。對大部分數據來講,好比訂單庫,通常須要的只是一段時間,好比三個月內的數據。更長時間的數據訪問量就很是低了。 這種狀況下,沒有必要將全部數據加載到成本高昂的讀庫中,即這時候,讀庫是緩存模式。 在緩存模式下,數據更新策略是一個大問題。

對於實時性要求不高的數據,能夠考慮採用被動更新的策略。即數據加載到緩存的時候,設置過時時間。通常內存數據庫,包括Redis,couchbase等,都支持這個特性。到過時時間後,數據將失效,再次被訪問時,系統將觸發從主庫讀寫數據的流程。

對實時性要求高的數據,須要採用主動更新的策略,也就是接受Message後,當即更新緩存數據。

固然,在服務演化後,對原有服務的實現也會產生影響。 考慮到微服務的一個實現原則,即一個服務僅管一個存儲庫,原有的服務就被分裂成多個服務了。 爲了保持使用方的穩定,原有服務被從新實現爲服務網關,做爲各個子服務的代理來提供服務。

以上是RPC與微服務的所有內容,如下是thrift + protobuf的實現規範的介紹。

附1、基礎服務設計規範

基礎服務是微服務的服務棧中最底層的模塊, 基礎服務直接和數據存儲打交道,提供數據增刪改查的基本操做。

附1.1 設計規範

文件規範

rpc接口文件名以 xxx_rpc_service.thrift 來命名; protobuf參數文件名以 xxx_service.proto 來命名。

這兩種文件所有使用UTF-8編碼。

命名規範

服務名稱以 「XXXXService」 的格式命名, XXXX是實體,必須是名詞。如下是合理的接口名稱。

OrderService

AccountService

附1.2 方法設計

因爲基礎服務主要是解決數據讀寫問題,因此從使用的角度,對外提供的接口,能夠參考數據庫操做,標準化爲增、刪、改、查、統計等基本接口。接口採用 操做+實體來命名,如createOrder。 接口的輸入輸出參數採用 接口名+Request 和 接口名Response 的規範來命名。 這種方式使得接口易於使用和管理。

file: xxx_rpc_service.thrift

/**

  • 這裏是版權申明

**/

namespace java com.phoenix.service

/**

  • 提供關於XXX實體的增刪改查基本操做。

**/

service XXXRpcService {

/**

  • 建立實體

  • 輸入參數:

    1. createXXXRequest: 建立請求,支持建立多個實體;
  • 輸出參數

  • createXXXResponse: 建立成功,返回建立實體的ID列表;

  • 異常

    1. userException:輸入的參數有誤;
    1. systemExeption:服務器端出錯致使沒法建立;
    1. notFoundException: 必填的參數沒有提供。

**/

binary createXXX(1: binary create_xxx_request) throws (1: Errors.UserException userException, 2: Errors.systemException, 3: Errors.notFoundException)

/**

  • 更新實體

  • 輸入參數:

    1. updateXXXRequest: 更新請求,支持同時更新多個實體;
  • 輸出參數

  • updateXXXResponse: 更新成功,返回被更行的實體的ID列表;

  • 異常

    1. userException:輸入的參數有誤;
    1. systemExeption:服務器端出錯致使沒法建立;
    1. notFoundException: 該實體在服務器端沒有找到。

**/

binary updateXXX(1: binary update_xxx_request) throws (1: Errors.UserException userException, 2: Errors.systemException, 3: Errors.notFoundException)

/**

  • 刪除實體

  • 輸入參數:

    1. removeXXXRequest: 刪除請求,按照id來刪除,支持一次刪除多個實體;
  • 輸出參數

  • removeXXXResponse: 刪除成功,返回被刪除的實體的ID列表;

  • 異常

    1. userException:輸入的參數有誤;
    1. systemExeption:服務器端出錯致使沒法建立;
    1. notFoundException: 該實體在服務器端沒有找到。

**/

binary removeXXX(1: binary remove_xxx_request) throws (1: Errors.UserException userException, 2: Errors.systemException, 3: Errors.notFoundException)

/**

  • 根據ID獲取實體

  • 輸入參數:

    1. getXXXRequest: 獲取請求,按照id來獲取,支持一次獲取多個實體;
  • 輸出參數

  • getXXXResponse: 返回對應的實體列表;

  • 異常

    1. userException:輸入的參數有誤;
    1. systemExeption:服務器端出錯致使沒法建立;
    1. notFoundException: 該實體在服務器端沒有找到。

**/

binary getXXX(1: binary get_xxx_request) throws (1: Errors.UserException userException, 2: Errors.systemException, 3: Errors.notFoundException)

/**

  • 查詢實體

  • 輸入參數:

    1. queryXXXRequest: 查詢條件;
  • 輸出參數

  • queryXXXResponse: 返回對應的實體列表;

  • 異常

    1. userException:輸入的參數有誤;
    1. systemExeption:服務器端出錯致使沒法建立;
    1. notFoundException: 該實體在服務器端沒有找到。

**/

binary queryXXX(1: binary query_xxx_request) throws (1: Errors.UserException userException, 2: Errors.systemException, 3: Errors.notFoundException)

/**

  • 統計符合條件的實體的數量

  • 輸入參數:

    1. countXXXRequest: 查詢條件;
  • 輸出參數

  • countXXXResponse: 返回對應的實體數量;

  • 異常

    1. userException:輸入的參數有誤;
    1. systemExeption:服務器端出錯致使沒法建立;
    1. notFoundException: 該實體在服務器端沒有找到。

**/

binary countXXX(1: binary count_xxx_request) throws (1: Errors.UserException userException, 2: Errors.systemException, 3: Errors.notFoundException)

}

附1.3 參數設計

每一個方法的輸入輸出參數,採用protobuf來表示。

file: xxx_service.protobuf

/** * * 這裏是版權申明**/option java_package ="com.phoenix.service";import"entity.proto";import"taglib.proto";/** * 建立實體的請求 */message CreateXXXRequest {  optional string user_name = 1; //訪問這個接口的用戶  optional string password = 2; //訪問這個接口的密碼  repeated XXXX xxx = 21; // 實體內容;}/ * 建立實體的結果響應 * **/message CreateXXXResponse {repeated int64 id = 11;//成功建立的實體的ID列表}

附1.4 異常設計

RPC接口也不須要太複雜的異常,通常是定義三類異常。

file errors.thrift

/**

  • 因爲調用方的緣由引發的錯誤, 好比參數不符合規範、缺少必要的參數,沒有權限等。

  • 這種異常通常是能夠重試的。

**/

exception UserException {

1: required ErrorCode error_code;

2: optional string message;

}

/**

  • 因爲服務器端發生錯誤致使的,好比數據庫沒法鏈接。這也包括QPS超過限額的狀況,這時候rateLimit返回分配給的QPS上限;

**/

exception systemException {

1: required ErrorCode error_code;

2: optional string message;

3: i32 rateLimit;

}

/**

  • 根據給定的ID或者其餘條件沒法找到對象。

**/

exception systemException {

1: optional string identifier;

}

附2、服務SDK

固然,RPC服務不該該直接提供給業務方使用,須要提供封裝好的客戶端。 通常來講,客戶端除了提供訪問服務端的代理外,還須要對常有功能進行封裝,這包括服務發現、RPC鏈接池、重試機制、QPS控制。這裏首先介紹服務SDK的設計。 直接使用Protobuf做爲輸入參數和輸出參數,開發出來的代碼很繁瑣:

GetXXXRequest.Builder request = GetXXXRequest.newBuilder();request.setUsername("username");request.setPassword("password");request.addId("123");GetXXXResponse response = xxxService.getXXX(request.build());if(response.xxx.size()==1)XXX xxx = response.xxx.get(0);

如上,有大量的重複性代碼,使用起來不直觀也不方便。 於是須要使用客戶端SDK來作一層封裝,供業務方調用:

class XXXService {//根據ID獲取對象public XXX getXXX(String id){GetXXXRequest.Builder request = GetXXXRequest.newBuilder();request.setUsername("username");request.setPassword("password");request.addId("123");GetXXXResponse response = xxxService.getXXX(request.build());if(response.xxx.size()==1)returnresponse.xxx.get(0);returnnull;}}

對全部服務器端接口提供對應的客戶端SDK,也是微服務架構最佳實踐之一。以此封裝完成後,調用方便可以像使用普通接口同樣,無需瞭解實現細節。

若是想學習Java工程化、高性能及分佈式、深刻淺出。性能調優、Spring,MyBatis,Netty源碼分析的朋友能夠加個人Java高級架構進階羣:180705916,羣裏有阿里大牛直播講解技術,以及Java大型互聯網技術的視頻免費分享給你們

相關文章
相關標籤/搜索