從0開始學微服務:06 如何實現RPC遠程服務調用?

從0開始學微服務:06 如何實現RPC遠程服務調用?

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

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

在介紹 RPC 調用的原理之前,先來想象一下一次電話通話的過程。首先,呼叫者 A 通過查詢號碼簿找到被呼叫者 B 的電話號碼,然後撥打 B 的電話。B 接到來電提示時,如果方便接聽的話就會接聽;如果不方便接聽的話,A 就得一直等待。當等待超過一段時間後,電話會因超時被掛斷,這個時候 A 需要再次撥打電話,一直等到 B 空閒的時候,才能接聽。

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

上面就是 RPC 調用的過程,由此可見,想要完成調用,你需要解決四個問題:

  • 客戶端和服務端如何建立網絡連接?
  • 服務端如何處理請求?
  • 數據傳輸採用什麼協議?
  • 數據該如何序列化和反序列化?

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

根據我的實踐經驗,客戶端和服務端之間基於 TCP 協議建立網絡連接最常用的途徑有兩種。

1. HTTP 通信

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

從0開始學微服務:06 如何實現RPC遠程服務調用?

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

從0開始學微服務:06 如何實現RPC遠程服務調用?

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() 函數,就可以得到得到返回結果。

直接理解可能有點抽象,你可以把這個過程套入前面我舉的“打電話”的例子,可以方便你理解 Socket 通信過程。

從0開始學微服務:06 如何實現RPC遠程服務調用?

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

  • 鏈路存活檢測:客戶端需要定時地發送心跳檢測消息(一般是通過 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 代表返回數據的類型;消息體中存放的是具體的返回結果,這裡就是一段 HTML 網頁代碼。

從0開始學微服務:06 如何實現RPC遠程服務調用?

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

再看第二個問題,數據該如何序列化和反序列化。

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

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

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

  • 支持數據結構類型的豐富度。數據結構種類支持的越多越好,這樣的話對於使用者來說在編程時更加友好,有些序列化框架如 Hessian 2.0 還支持複雜的數據結構比如 Map、List 等。
  • 跨語言支持。序列化方式是否支持跨語言也是一個很重要的因素,否則使用的場景就比較侷限,比如 Java 序列化只支持 Java 語言,就不能用於跨語言的服務調用了。
  • 性能。主要看兩點,一個是序列化後的壓縮比,一個是序列化的速度。以常用的 PB 序列化和 JSON 序列化協議為例來對比分析,PB 序列化的壓縮比和速度都要比 JSON 序列化高很多,所以對性能和存儲空間要求比較高的系統選用 PB 序列化更合適;而 JSON 序列化雖然性能要差一些,但可讀性更好,更適合對外部提供服務。

總結

今天我給你講解了服務調用需要解決的幾個問題,其中你需要掌握:

  • 通信框架。它主要解決客戶端和服務端如何建立連接、管理連接以及服務端如何處理請求的問題。
  • 通信協議。它主要解決客戶端和服務端採用哪種數據傳輸協議的問題。
  • 序列化和反序列化。它主要解決客戶端和服務端採用哪種數據編解碼的問題。

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


分享到:


相關文章: