微服務:框架之如何實現RPC遠程調用


目錄

RPC 調用框架的三個部分

RPC 調用的過程需要解決四個問題:

  客戶端和服務端如何建立網絡連接?

  服務端如何處理請求?

  數據傳輸采用什么協議?

  數據該如何序列化和反序列化?

實戰:開源RPC框架如何選型

  常見開源PRC框架

  開源PRC框架對比選型


 

RPC 調用框架的三個部分

一個完整的 RPC 調用框架包含三個部分:

通信框架。它主要解決客戶端和服務端如何建立連接、管理連接以及服務端如何處理請求的問題。

通信協議。它主要解決客戶端和服務端采用哪種數據傳輸協議的問題。

序列化和反序列化。它主要解決客戶端和服務端采用哪種數據編解碼的問題。

通信框架提供了基礎的通信能力,通信協議描述了通信契約,而序列化和反序列化則用於數據的編 / 解碼。一個通信框架可以適配多種通信協議,也可以采用多種序列化和反序列化的格式,比如服務化框架 Dubbo 不僅支持 Dubbo 協議,還支持 RMI 協議、HTTP 協議等,而且還支持多種序列化和反序列化格式,比如 JSON、Hession 2.0 以及 Java 序列化等。

要完成一次服務調用,首先要解決的問題是服務消費者如何得到服務提供者的地址,其中注冊中心扮演了關鍵角色,服務提供者把自己的地址登記到注冊中心,服務消費者就可以查詢注冊中心得到服務提供者的地址,可以說注冊中心猶如海上的一座燈塔,為服務消費者指引了前行的方向。

有了服務提供者的地址后,服務消費者就可以向這個地址發起請求了,但這時候也產生了一個新的問題。你知道,在單體應用時,一次服務調用發生在同一台機器上的同一個進程內部,也就是說調用發生在本機內部,因此也被叫作本地方法調用。在進行服務化拆分之后,服務提供者和服務消費者運行在兩台不同物理機上的不同進程內,它們之間的調用相比於本地方法調用,可稱之為遠程方法調用,簡稱 RPC(Remote Procedure Call),那么RPC 調用是如何實現的呢?

RPC 調用的原理與此類似電話通話的過程,服務消費者叫作客戶端,服務提供者叫作服務端,兩者通常位於網絡上兩個不同的地址,要完成一次 RPC 調用,就必須先建立網絡連接。建立連接后,雙方還必須按照某種約定的協議進行網絡通信,這個協議就是通信協議。雙方能夠正常通信后,服務端接收到請求時,需要以某種方式進行處理,處理成功后,把請求結果返回給客戶端。為了減少傳輸的數據大小,還要對數據進行壓縮,也就是對數據進行序列化。

 

 RPC 調用的過程需要解決四個問題:

客戶端和服務端如何建立網絡連接?

服務端如何處理請求?

數據傳輸采用什么協議?

數據該如何序列化和反序列化?

 

客戶端和服務端如何建立網絡連接?

客戶端和服務端之間基於 TCP 協議建立網絡連接最常用的途徑有兩種。

1. HTTP 通信

HTTP 通信是基於應用層 HTTP 協議的,而 HTTP 協議又是基於傳輸層 TCP 協議的。一次 HTTP 通信過程就是發起一次 HTTP 調用,而一次 HTTP 調用就會建立一個 TCP 連接,經歷一次下圖所示的“三次握手”的過程來建立連接。

完成請求后,再經歷一次“四次揮手”的過程來斷開連接。

2. Socket 通信

Socket 通信是基於 TCP/IP 協議的封裝,建立一次 Socket 連接至少需要一對套接字,其中一個運行於客戶端,稱為 ClientSocket ;另一個運行於服務器端,稱為 ServerSocket 。就像下圖所描述的,Socket 通信的過程分為四個步驟:服務器監聽、客戶端請求、連接確認、數據傳輸。

服務器監聽:ServerSocket 通過調用 bind() 函數綁定某個具體端口,然后調用 listen() 函數實時監控網絡狀態,等待客戶端的連接請求。

客戶端請求:ClientSocket 調用 connect() 函數向 ServerSocket 綁定的地址和端口發起連接請求。

服務端連接確認:當 ServerSocket 監聽到或者接收到 ClientSocket 的連接請求時,調用 accept() 函數響應 ClientSocket 的請求,同客戶端建立連接。

數據傳輸:當 ClientSocket 和 ServerSocket 建立連接后,ClientSocket 調用 send() 函數,ServerSocket 調用 receive() 函數,ServerSocket 處理完請求后,調用 send() 函數,ClientSocket 調用 receive() 函數,就可以得到得到返回結果。

當客戶端和服務端建立網絡連接后,就可以發起請求了。但網絡不一定總是可靠的,經常會遇到網絡閃斷、連接超時、服務端宕機等各種異常,通常的處理手段有兩種。

鏈路存活檢測:客戶端需要定時地發送心跳檢測消息(一般是通過 ping 請求)給服務端,如果服務端連續 n 次心跳檢測或者超過規定的時間都沒有回復消息,則認為此時鏈路已經失效,這個時候客戶端就需要重新與服務端建立連接。

斷連重試:通常有多種情況會導致連接斷開,比如客戶端主動關閉、服務端宕機或者網絡故障等。這個時候客戶端就需要與服務端重新建立連接,但一般不能立刻完成重連,而是要等待固定的間隔后再發起重連,避免服務端的連接回收不及時,而客戶端瞬間重連的請求太多而把服務端的連接數占滿。

 

服務端如何處理請求?

假設這時候客戶端和服務端已經建立了網絡連接,服務端又該如何處理客戶端的請求呢?通常來講,有三種處理方式。

同步阻塞方式(BIO),客戶端每發一次請求,服務端就生成一個線程去處理。當客戶端同時發起的請求很多時,服務端需要創建很多的線程去處理每一個請求,如果達到了系統最大的線程數瓶頸,新來的請求就沒法處理了。

同步非阻塞方式 (NIO),客戶端每發一次請求,服務端並不是每次都創建一個新線程來處理,而是通過 I/O 多路復用技術進行處理。就是把多個 I/O 的阻塞復用到同一個 select 的阻塞上,從而使系統在單線程的情況下可以同時處理多個客戶端請求。這種方式的優勢是開銷小,不用為每個請求創建一個線程,可以節省系統開銷。

異步非阻塞方式(AIO),客戶端只需要發起一個 I/O 操作然后立即返回,等 I/O 操作真正完成以后,客戶端會得到 I/O 操作完成的通知,此時客戶端只需要對數據進行處理就好了,不需要進行實際的 I/O 讀寫操作,因為真正的 I/O 讀取或者寫入操作已經由內核完成了。這種方式的優勢是客戶端無需等待,不存在阻塞等待問題。

 

不同的處理方式適用於不同的業務場景:

BIO 適用於連接數比較小的業務場景,這樣的話不至於系統中沒有可用線程去處理請求。這種方式寫的程序也比較簡單直觀,易於理解。

NIO 適用於連接數比較多並且請求消耗比較輕的業務場景,比如聊天服務器。這種方式相比 BIO,相對來說編程比較復雜。

AIO 適用於連接數比較多而且請求消耗比較重的業務場景,比如涉及 I/O 操作的相冊服務器。這種方式相比另外兩種,編程難度最大,程序也不易於理解。

上面兩個問題就是“通信框架”要解決的問題,你可以基於現有的 Socket 通信,在服務消費者和服務提供者之間建立網絡連接,然后在服務提供者一側基於 BIO、NIO 和 AIO 三種方式中的任意一種實現服務端請求處理,最后再花費一些精力去解決服務消費者和服務提供者之間的網絡可靠性問題。這種方式對於 Socket 網絡編程、多線程編程知識都要求比較高,建議最為穩妥的方式是使用成熟的開源方案,比如 Netty、MINA 等,它們都是經過業界大規模應用后,被充分論證是很可靠的方案。

假設客戶端和服務端的連接已經建立了,服務端也能正確地處理請求了,接下來完成一次正常地 RPC 調用還需要解決兩個問題,即數據傳輸采用什么協議以及數據該如何序列化和反序列化。

 

數據傳輸采用什么協議?

首先來看第一個問題,數據傳輸采用什么協議?

最常用的有 HTTP 協議,它是一種開放的協議,各大網站的服務器和瀏覽器之間的數據傳輸大都采用了這種協議。還有一些定制的私有協議,比如阿里巴巴開源的 Dubbo 協議,也可以用於服務端和客戶端之間的數據傳輸。無論是開放的還是私有的協議,都必須定義一個“契約”,以便服務消費和服務提供者之間能夠達成共識。服務消費者按照契約,對傳輸的數據進行編碼,然后通過網絡傳輸過去;服務提供者從網絡上接收到數據后,按照契約,對傳輸的數據進行解碼,然后處理請求,再把處理后的結果進行編碼,通過網絡傳輸返回給服務消費者;服務消費者再對返回的結果進行解碼,最終得到服務提供者處理后的返回值。

通常協議契約包括兩個部分:消息頭和消息體。其中消息頭存放的是協議的公共字段以及用戶擴展字段,消息體存放的是傳輸數據的具體內容。

以 HTTP 協議為例,下圖展示了一段采用 HTTP 協議傳輸的數據響應報文,主要分為消息頭和消息體兩部分,其中消息頭中存放的是協議的公共字段,比如 Server 代表是服務端服務器類型、Content-Length 代表返回數據的長度、Content-Type 代表返回數據的類型;消息體中存放的是具體的返回結果。

數據該如何序列化和反序列化?

一般數據在網絡中進行傳輸前,都要先在發送方一端對數據進行編碼,經過網絡傳輸到達另一端后,再對數據進行解碼,這個過程就是序列化和反序列化。

為什么要對數據進行序列化和反序列化呢?要知道網絡傳輸的耗時一方面取決於網絡帶寬的大小,另一方面取決於數據傳輸量。要想加快網絡傳輸,要么提高帶寬,要么減小數據傳輸量,而對數據進行編碼的主要目的就是減小數據傳輸量。比如一部高清電影原始大小為 30GB,如果經過特殊編碼格式處理,可以減小到 3GB,同樣是 100MB/s 的網速,下載時間可以從 300s 減小到 30s。

常用的序列化方式分為兩類:文本類如 XML/JSON 等,二進制類如 PB/Thrift 等,而具體采用哪種序列化方式,主要取決於三個方面的因素。

支持數據結構類型的豐富度。數據結構種類支持的越多越好,這樣的話對於使用者來說在編程時更加友好,有些序列化框架如 Hessian 2.0 還支持復雜的數據結構比如 Map、List 等。

跨語言支持。序列化方式是否支持跨語言也是一個很重要的因素,否則使用的場景就比較局限,比如 Java 序列化只支持 Java 語言,就不能用於跨語言的服務調用了。

性能。主要看兩點,一個是序列化后的壓縮比,一個是序列化的速度。以常用的 PB 序列化和 JSON 序列化協議為例來對比分析,PB 序列化的壓縮比和速度都要比 JSON 序列化高很多,所以對性能和存儲空間要求比較高的系統選用 PB 序列化更合適;而 JSON 序列化雖然性能要差一些,但可讀性更好,更適合對外部提供服務。

 

實戰:開源RPC框架如何選型

常見開源PRC框架

業界應用比較廣泛的開源 RPC 框架有哪些呢?簡單划分的話,主要分為兩類:一類是跟某種特定語言平台綁定的,另一類是與語言無關即跨語言平台的。

跟語言平台綁定的開源 RPC 框架主要有下面幾種。

Dubbo:國內最早開源的 RPC 框架,由阿里巴巴公司開發並於 2011 年末對外開源,僅支持 Java 語言。

Motan:微博內部使用的 RPC 框架,於 2016 年對外開源,僅支持 Java 語言。

Tars:騰訊內部使用的 RPC 框架,於 2017 年對外開源,僅支持 C++ 語言。

Spring Cloud:國外 Pivotal 公司 2014 年對外開源的 RPC 框架,僅支持 Java 語言,最近幾年生態發展得比較好,是比較火的 RPC 框架。

而跨語言平台的開源 RPC 框架主要有以下幾種。

gRPC:Google 於 2015 年對外開源的跨語言 RPC 框架,支持常用的 C++、Java、Python、Go、Ruby、PHP、Android Java、Objective-C 等多種語言。

Thrift:最初是由 Facebook 開發的內部系統跨語言的 RPC 框架,2007 年貢獻給了 Apache 基金,成為 Apache 開源項目之一,支持常用的 C++、Java、PHP、Python、Ruby、Erlang 等多種語言。

所以很明顯,如果你的業務場景僅僅局限於一種語言的話,可以選擇跟語言綁定的 RPC 框架中的一種;如果涉及多個語言平台之間的相互調用,就應該選擇跨語言平台的 RPC 框架。

具體框架實現如下:

1. Dubbo

Dubbo 的架構主要包含四個角色,其中 Consumer 是服務消費者,Provider 是服務提供者,Registry 是注冊中心,Monitor 是監控系統。

具體的交互流程是 Consumer 一端通過注冊中心獲取到 Provider 節點后,通過 Dubbo 的客戶端 SDK 與 Provider 建立連接,並發起調用。Provider 一端通過 Dubbo 的服務端 SDK 接收到 Consumer 的請求,處理后再把結果返回給 Consumer。

服務消費者和服務提供者都需要引入 Dubbo 的 SDK 才來完成 RPC 調用,因為 Dubbo 本身是采用 Java 語言實現的,所以要求服務消費者和服務提供者也都必須采用 Java 語言實現才可以應用。

Dubbo 的調用框架實現:

通信框架方面,Dubbo 默認采用了 Netty 作為通信框架。

通信協議方面,Dubbo 除了支持私有的 Dubbo 協議外,還支持 RMI 協議、Hession 協議、HTTP 協議、Thrift 協議等。

序列化格式方面,Dubbo 支持多種序列化格式,比如 Dubbo、Hession、JSON、Kryo、FST 等。

2. Motan

Motan 是國內另外一個比較有名的開源的 RPC 框架,同樣也只支持 Java 語言實現。Motan 與 Dubbo 的架構類似,都需要在 Client 端(服務消費者)和 Server 端(服務提供者)引入 SDK,其中 Motan 框架主要包含下面幾個功能模塊。

register:用來和注冊中心交互,包括注冊服務、訂閱服務、服務變更通知、服務心跳發送等功能。Server 端會在系統初始化時通過 register 模塊注冊服務,Client 端會在系統初始化時通過 register 模塊訂閱到具體提供服務的 Server 列表,當 Server 列表發生變更時也由 register 模塊通知 Client。

protocol:用來進行 RPC 服務的描述和 RPC 服務的配置管理,這一層還可以添加不同功能的 filter 用來完成統計、並發限制等功能。

serialize:將 RPC 請求中的參數、結果等對象進行序列化與反序列化,即進行對象與字節流的互相轉換,默認使用對 Java 更友好的 Hessian 2 進行序列化。

transport:用來進行遠程通信,默認使用 Netty NIO 的 TCP 長鏈接方式。

cluster:Client 端使用的模塊,cluster 是一組可用的 Server 在邏輯上的封裝,包含若干可以提供 RPC 服務的 Server,實際請求時會根據不同的高可用與負載均衡策略選擇一個可用的 Server 發起遠程調用。

3. Tars

Tars 是騰訊根據內部多年使用微服務架構的實踐,總結而成的開源項目,僅支持 C++ 語言

Tars 的架構交互主要包括以下幾個流程:

服務發布流程:在 web 系統上傳 server 的發布包到 patch,上傳成功后,在 web 上提交發布 server 請求,由 registry 服務傳達到 node,然后 node 拉取 server 的發布包到本地,拉起 server 服務。

管理命令流程:web 系統上的可以提交管理 server 服務命令請求,由 registry 服務傳達到 node 服務,然后由 node 向 server 發送管理命令。

心跳上報流程:server 服務運行后,會定期上報心跳到 node,node 然后把服務心跳信息上報到 registry 服務,由 registry 進行統一管理。

信息上報流程:server 服務運行后,會定期上報統計信息到 stat,打印遠程日志到 log,定期上報屬性信息到 prop、上報異常信息到 notify、從 config 拉取服務配置信息。

client 訪問 server 流程:client 可以通過 server 的對象名 Obj 間接訪問 server,client 會從 registry 上拉取 server 的路由信息(如 IP、Port 信息),然后根據具體的業務特性(同步或者異步,TCP 或者 UDP 方式)訪問 server(當然 client 也可以通過 IP/Port 直接訪問 server)。

4. Spring Cloud

Spring Cloud 是為了解決微服務架構中服務治理而提供的一系列功能的開發框架,它是完全基於 Spring Boot 進行開發的,Spring Cloud 利用 Spring Boot 特性整合了開源行業中優秀的組件,整體對外提供了一套在微服務架構中服務治理的解決方案。因為 Spring Boot 是用 Java 語言編寫的,所以目前 Spring Cloud 也只支持 Java 語言平台。它由多個組件一起組成的,各個組件的交互流程如下。

請求統一通過 API 網關 Zuul 來訪問內部服務,先經過 Token 進行安全認證。

通過安全認證后,網關 Zuul 從注冊中心 Eureka 獲取可用服務節點列表。

從可用服務節點中選取一個可用節點,然后把請求分發到這個節點。

整個請求過程中,Hystrix 組件負責處理服務超時熔斷,Turbine 組件負責監控服務間的調用和熔斷相關指標,Sleuth 組件負責調用鏈監控,ELK 負責日志分析。

gRPC

gRPC,它的原理是通過 IDL(Interface Definition Language)文件定義服務接口的參數和返回值類型,然后通過代碼生成程序生成服務端和客戶端的具體實現代碼,這樣在 gRPC 里,客戶端應用可以像調用本地對象一樣調用另一台服務器上對應的方法。

它的主要特性包括三個方面。

通信協議采用了 HTTP/2,因為 HTTP/2 提供了連接復用、雙向流、服務器推送、請求優先級、首部壓縮等機制,所以在通信過程中可以節省帶寬、降低 TCP 連接次數、節省 CPU,尤其對於移動端應用來說,可以幫助延長電池壽命。

IDL 使用了ProtoBuf,ProtoBuf 是由 Google 開發的一種數據序列化協議,它的壓縮和傳輸效率極高,語法也簡單,所以被廣泛應用在數據存儲和通信協議上。

多語言支持,能夠基於多種語言自動生成對應語言的客戶端和服務端的代碼。

Thrift

Thrift 是一種輕量級的跨語言 RPC 通信方案,支持多達 25 種編程語言。為了支持多種語言,跟 gRPC 一樣,Thrift 也有一套自己的接口定義語言 IDL,可以通過代碼生成器,生成各種編程語言的 Client 端和 Server 端的 SDK 代碼,這樣就保證了不同語言之間可以相互通信。

Thrift RPC 框架的特性。

支持多種序列化格式:如 Binary、Compact、JSON、Multiplexed 等。

支持多種通信方式:如 Socket、Framed、File、Memory、zlib 等。

服務端支持多種處理方式:如 Simple 、Thread Pool、Non-Blocking 等。

 

開源PRC框架對比選型

如果你的語言平台是 C++,那么只能選擇 Tars;而如果是 Java 的話,可以選擇 Dubbo、Motan 或者 Spring Cloud。

而Dubbo、Motan 或者 Spring Cloud之間:

Spring Cloud 不僅提供了基本的 RPC 框架功能,還提供了服務注冊組件、配置中心組件、負載均衡組件、斷路器組件、分布式消息追蹤組件等一系列組件,也難怪被技術圈的人稱之為“Spring Cloud 全家桶”。如果你不想自己實現以上這些功能,那么 Spring Cloud 基本可以滿足你的全部需求。而 Dubbo、Motan 基本上只提供了最基礎的 RPC 框架的功能,其他微服務組件都需要自己去實現。

不過由於 Spring Cloud 的 RPC 通信采用了 HTTP 協議,相比 Dubbo 和 Motan 所采用的私有協議來說,在高並發的通信場景下,性能相對要差一些,所以對性能有苛刻要求的情況下,可以考慮 Dubbo 和 Motan。

那么涉及跨語言的服務調用場景,到底該選擇 gRPC 還是 Thrift 呢?

從成熟度上來講,Thrift 因為誕生的時間要早於 gRPC,所以使用的范圍要高於 gRPC,在 HBase、Hadoop、Scribe、Cassandra 等許多開源組件中都得到了廣泛地應用。而且 Thrift 支持多達 25 種語言,這要比 gRPC 支持的語言更多,所以如果遇到 gRPC 不支持的語言場景下,選擇 Thrift 更合適。

但 gRPC 作為后起之秀,因為采用了 HTTP/2 作為通信協議、ProtoBuf 作為數據序列化格式,在移動端設備的應用以及對傳輸帶寬比較敏感的場景下具有很大的優勢,而且開發文檔豐富,根據 ProtoBuf 文件生成的代碼要比 Thrift 更簡潔一些,從使用難易程度上更占優勢,所以如果使用的語言平台 gRPC 支持的話,建議還是采用 gRPC 比較好。

 

總結:從長遠來看,支持多語言是 RPC 框架未來的發展趨勢。正是基於此判斷,各個 RPC 框架都提供了 Sidecar 組件來支持多語言平台之間的 RPC 調用。

Dubbo 宣稱要引入 Sidecar 組件來構建Dubbo Mesh提供多語言支持。

Motan 對外開源了其內部的 Sidecar 組件:Motan-go,目前支持 PHP、Java 語言之間的相互調用。

Spring Cloud 也提供了 Sidecar 組件spring-cloud-netflix-sideca,可以讓其他語言也可以使用 Spring Cloud 的組件。

所以未來語言不會成為使用上面這幾種 RPC 框架的約束,而 gRPC 和 Thrift 雖然支持跨語言的 RPC 調用,但是因為它們只提供了最基本的 RPC 框架功能,缺乏一系列配套的服務化組件和服務治理功能的支撐,所以使用它們作為跨語言調用的 RPC 框架,就需要自己考慮注冊中心、熔斷、限流、監控、分布式追蹤等功能的實現,不過好在大多數功能都有開源實現,可以直接采用。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM