趣談網路協議---二進位制類RPC協議:還是叫NBA吧,總說全稱多費勁
接入層,對於靜態資源或動態資源靜態化的部分可以做快取,但對於下單、支付等交易場景,還是需要呼叫 API。
對於微服務架構,API 需要一個 API 閘道器統一的處理。API 閘道器實現由多種方式,Nginx 或 OpenResty 結合 Lua 指令碼是常用的方式,也可以用 Spring Cloud 中的元件 Zuul。
資料中心內部是如何相互呼叫的?
API 閘道器用來管理 API,而 API 的實現一般在 Controller 層,基本上都是基於 RESTful 的,面向大規模的網際網路應用。 Controller 為網際網路應用的業務邏輯實現,業務邏輯的實現應該是無狀態的,從而可以橫向擴充套件。所以,資源狀態的維護在最底層的持久化層,一般會是分散式資料庫和 ElasticSearch。
因為硬碟讀寫效能差,持久化層往往吞吐量較小,因此前面需要由一層快取層,Redis 或 memcached。
快取和持久化層之上一般是基礎服務層,提供一些原子化的介面,如對使用者、商品、訂單、庫存的增刪改查。有了這層,上層業務邏輯看到的都是介面,而不會呼叫資料庫和快取,有利於對快取和資料庫的運維。
再往上是組合層,實現負責的業務邏輯,如下單、扣優惠券等。
Controller 層、組合服務層、基礎服務層會相互呼叫,這個呼叫在資料中心內部,使用 RPC 機制實現。
由於服務較多,需要一個單獨的註冊中心來做服務發現。服務提供方會自己提供服務註冊到註冊中心去,同時服務消費訂閱該服務,從而對該服務呼叫。
RPC 呼叫時,如果使用文字類,則相對於二進位制,同樣的資訊佔用更多空間,傳輸起來更加佔頻寬,時延也高。因而,多數公司還是更願意使用二進位制方案。
Dubbo 服務框架二進位制的 RPC 方式
- Dubbo 在客戶端的本地啟動一個 Proxy,即客戶端 Stub。
- Dubbo 從註冊中心獲取服務端的列表,根據路由規則和負責均衡規則,從多個服務端中選擇一個進行呼叫。
- 呼叫服務端時,進行編碼和序列化,形成 Dubbo 頭和序列化的方法和引數,交給網路客戶端傳送。
- 網路服務端收到訊息,解碼,將任務發給某個執行緒處理,執行緒呼叫服務端程式碼邏輯,然後返回結果。
如何解決協議約定問題?
Dubbo 中預設的 RPC 協議時 Hessian 2。Hessian2 將遠端呼叫序列化為二進位制進行傳輸,並且可以進行一定的壓縮。
Hessian2 與前面的二進位制的 RPC 的區別:
1、綜合了 XML 和二進位制共同的優勢。
原先要定義一個協議檔案,然後通過該檔案生成客戶端和服務端 Stub,才能相互呼叫。Hessian2 不需要定義協議檔案,而是自描述的。關於呼叫哪個函式,引數是什麼,另一方不需要拿到某個協議檔案,而是拿到二進位制後,靠 Hessian2 的規則解析。
Hessian2 的序列化額語法描述檔案: 從 Top 起,下一層是 value,直到形成一棵樹。為防止歧義,每個型別的起始數字都設定為獨一無二的。
“add(2, 3)” 被序列化後:
H x02 x00 # H 開頭,表示使用的協議是 Hessian
C # C 開頭,表示是一個 RPC 呼叫
x03 add # 0x03,表示方法名是 3 個字元
x92 # 0x92,表示有 2 個引數,加上 0x90 是為了防止歧義,表示這一定是個 int
x92 # 第一個引數是 2
x93 # 第二個引數是 3
2、Hessian2 是面向物件的,可傳輸一個物件。
class Car {
String color;
String model;
}
out.writeObject(new Car("red", "corvette"));
out.writeObject(new Car("green", "civic"));
---
C # 定義類,定義在位置 0
x0b example.Car # 類名為 example.Car,11 個字元
x92 # 2 個成員變數
x05 color # color 成員變數,5 個字元
x05 model # model 成員變數,5 個字元
O # 傳輸的物件引用這個類
x90 # 因為類定義在位置 0,所以物件會指向這個位置 0
x03 red # color 的值為 red,3 個字元
x08 corvette # model 的值為 corvette,8 個字元
x60 # 傳輸一個屬於相同類的物件,不儲存對類的引用,只儲存 0x60,表示同上
x05 green # color 的值為 green,5 個字元
x05 civic # model 的值為 civic,5 個字元
如何解決 RPC 傳輸問題?
Dubbo 中,使用了 Netty 的網路傳輸框架。
Netty 是一個非阻塞的基於事件的網路傳輸框架,在服務端啟動時,會監聽一個埠,並註冊以下事件。
- 連線事件:收到客戶端的連線事件時,呼叫 void connected(Channel channel) 方法。
- 可寫事件觸發時,呼叫 void sent(Channel channel, Object message),服務端向客戶端返回響應資料。
- 可讀事件觸發時,呼叫 void received(Channel channel, Object message),服務端接收客戶端的請求資料。
- 發生異常時,呼叫 void caught(Channel channel, Throwable exception)。
事件觸發後,服務端可直接在函式中操作,也可將請求分發到執行緒池處理。
上面的架構中,如果使用二進位制的方式進行序列化,雖然不用協議檔案生成 Stub,但對於介面的定義,及傳遞的物件 DTO,還是需要共享 JAR。客戶端 和服務端都有 JAR,才能成功地序列化和反序列化。
關係複雜時,JAR 的依賴也會複雜,在 DTO 中增加一個欄位,雙方 JAR 沒有匹配好,也會導致序列化失敗,還可能迴圈依賴,這時,一般有兩種選擇:
1、建立嚴格的專案管理流程
- 不允許迴圈呼叫,不訊息跨層呼叫。
- 介面保持相容性。
- 升級時,先升級服務提供端,再升級服務消費端。
2、改用 RESTful 的方式。
- 使用 Sprint Cloud,消費端和提供端不共享 JAR,各宣告各的,只要能變成 JSON 即可。
- 使用 RESTful 方式,效能會降低,所以需要通過橫向擴充套件來地下單機的效能損耗。