SOFA
Scalable Open Financial Architecture
是螞蟻金服自主研發的金融級分布式中間件,包含了建構金融級雲原生架構所需的各個元件,是在金融場景裡錘煉出來的最佳實踐。
本文為《剖析 | SOFARPC 架構》第七篇,作者莫那·魯道 ,來自 E簽寶。
《剖析 | SOFARPC 架構》系列由 SOFA 團隊和源碼愛好者們出品,
項目代号:<SOFA:RPCLab/>,官方目錄目前已經全部認領完畢。
前言
我們知道,在 RPC 調用中,用戶端需要加載服務端提供的接口定義類,但是,很多情況下,這個并不總是可行的,于是,衍生了泛化調用的需求,一個成熟的,功能完善的 RPC 架構一般都會支援泛化調用,那麼什麼是泛化調用呢?SOFA RPC 又是如何支援泛化調用的?同時又是如何實作的? 和其他的 RPC 泛化調用又有何不同?有何優勢?我們将在本文一一解答這些問題。
泛化調用介紹
當用戶端因為某種原因無法得到服務提供方的接口 jar 包時,或者是用戶端是一個比較通用的系統,并不想依賴每個服務提供方提供的 facade接口,但是又需要進行調用,那麼此時就需要進行泛化調用。
例如:
- 當分布式系統由多個語言開發,假設是 Node Js ,同時 Node Js 需要調用 Java 語言的 RPC 服務,那麼,我們就需要在兩者之間架設适配層,讓适配層處理 Node Js 的請求後再轉發給 Java 的 RPC 服務。
- 一些中間系統的功能,比如某些内部網關,需要以一個統一的方式實作對其他下遊系統的調用(非 SPI的情況),逐個依賴下遊的包顯然是不可能的。
- 一些流量回放類的線上系統,可以将資料采集攔截,之後,通過泛化調用回放,而不需要依賴全站的應用。
那麼這種情況下,肯定不能包含所有接口的 jar 檔案,否則就太臃腫了。實際上也是不現實的,總不能每增加一個服務端,就增加一個 jar 包依賴,然後應用進行釋出重新開機。
這個時候就可以使用泛化調用,将相應的請求包裝成泛化調用,就能夠實作不依賴接口 jar 包,多語言調用 RPC 服務,避免重複開發。
SOFA RPC 的泛化調用使用
SOFA RPC 的官方文檔十分詳細,在官方 wiki
泛化調用中,已有詳細介紹。同時,在源碼中的 example 子產品中,也有現成的 demo 可以跑起來,讀者可以自己 clone 源碼閱讀,這裡我們簡要說明一下使用方式,以便大家有一個直覺的了解。
接口定義
總的來說,泛化調用有 2 個 API,包含 5 個方法,其中, 2 個方法已經廢棄,也就是說,有 3 個主要方法。分别是:
/**
* 泛化調用
* @return 正常類型(不能是GenericObject類型)
*/
Object $invoke(String methodName, String[] argTypes, Object[] args);
/**
* 支援參數類型無法在類加載器加載情況的泛化調用
* @return 除了JDK等内置類型,其它對象是GenericObject類型
*/
Object $genericInvoke(String methodName, String[] argTypes, Object[] args);
/**
* 支援參數類型無法在類加載器加載情況的泛化調用
* @return 傳回指定的T類型傳回對象
*/
<T> T $genericInvoke(String methodName, String[] argTypes, Object[] args, Class<T> clazz);
- $invoke 該方法使用場景:使用者知道參數類型和傳回值類型,那麼就可以使用該方法。
- $genericInvoke 該方法是個重載方法,重載一的使用場景是:如果你的應用不知道接口的參數類型和傳回值類型,這個時候,你就需要使用 GenericObject 類,來包裝傳回值和參數。
- $genericInvoke 重載二的使用場景是:如果應用不知道接口參數類型,但是知道接口傳回值的類型,那麼就不需要使用 GenericObject 作為傳回值了。
基本上,已經覆寫了常用的集中場景,可以說功能相當全面。
泛化使用
由于篇幅有限,這裡就不貼使用 demo 了,感興趣的可以通過連結檢視官方的 demo 或者源碼,包含 SOFARPC 的 API 使用方式和 SOFABoot 的使用方式:
- demo wiki 位址: 使用者手冊->基本特性->泛化調用
- 源碼位址: 示例源碼
SOFARPC 泛化調用的設計與實作
接下來我們重點來介紹 SOFARPC 是如何實作泛化調用的。
架構調用設計
簡單來說,泛化調用的關鍵就是對象表示和序列化,SOFARPC 提供了 GenericObject 等對象來表示參數對象或者傳回值對象,而将 GenericObject 對象序列化成目标對象,或者将傳回值反序列化成 GenericObject 對象,是 SOFARPC 實作泛化的關鍵。
這裡我們先來看一下 SOFARPC 泛化調用的流程圖,有助于後面了解泛化實作。
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiIn5GcugjZlJWO1UzYzEjM10SOxIGOtIDOiRTL0ATM10SO0kzNmFmYx0CO1cDM4YzNzgzNzUTMvwVNwczN1EzLcdmbw9CX4EDMy8CXw8CXlVXc1l3Lc12bj5yayFGbu5ibkN2Lc9CX6MHc0RHaiojIsJye.png)
我們來說一下這個圖:
- 泛化 API 調用時,會加載泛化過濾器,作用是做一些參數轉換,同時設定序列化工廠類型。
- SOFARPC 在使用 SOFABolt 進行網絡調用前,會建立 context 上下文并傳遞給 SOFABolt,上下文中包含着序列化工廠類型資訊,這個資訊将決定使用何種序列化器,同時這個上下文将流轉于整個調用期間。
- 在 SOFABolt 正式發送資料之前,會将 GenericObject 對象序列化成普通對象的位元組流,這樣,服務提供方就不必關心是否為泛化調用,從圖中可見,提供方不用對泛化調用做任何改變 —— __這是 SOFARPC 泛化差別于其他 RPC 泛化的關鍵__。
- 當提供方成功接收請求後,使用普通序列化器即可反序列化資料,隻需要正常調用并傳回即可。
- 當消費者的 SOFABolt 接收到響應資料後,便根據 context 的序列化類型,對傳回值做反序列化,即将普通的位元組流反序列化成 GenericObject 對象 —— 因為用戶端有可能不知道傳回值的 Class 類型。
- 最終,泛化 API 即可得到 GenericObject 類型的傳回值。
從上面的流程可以看出,序列化器在泛化調用中,占了極大的篇幅和作用。而 SOFARPC 針對泛化調用,對 hessian3 進行了改造,使其支援泛化調用所需要的序列化功能。
SOFA-Hessian的改動可以參考這裡。
Hessian泛化實作
SOFA-Hessian 在 hessian 的包中加入了 com.alipay.hessian.generic 包,此包的作用就是處理泛化調用,重寫的關鍵是實作或繼承 SerializerFactory 類和 Serializer、Deserializer 等接口。在這裡,設計了一下幾個類,來描述
中對應的類型資訊,同時實作這幾個類的序列化和反序列化。對應關系如下
我們以 GenericObjectSerializer 為例,該序列化器重寫了 writeObject 方法,該方法的作用就是将 GenericObject 對象序列化成目标對象位元組流。即,拿出 GenericObject 的 type 字段和 fields 字段,組裝成目标對象的位元組流。
有一個類型是的 RPC 對象
public class TestObj {
private String str;
private int num;
}
在泛化調用用戶端,可以直接構造一個 GenericObject對象
GenericObject genericObject = new GenericObject(
"com.alipay.sofa.rpc.invoke.generic.TestObj");
genericObject.putField("str", "xxxx");
genericObject.putField("num", 222);
此時,GenericObjectSerializer 就可以通過這些資訊,将 GenericObject 對象轉成 TestObj 對象的位元組流。服務提供方就可以通過普通的 hessian2 反序列化得到對象。
相比較其他 RPC 架構兩端都需要對泛化進行支援,SOFARPC 顯得要友好的多。也就是說,如果應用想要支援泛化,隻需要更新用戶端(消費者)即可,服務端(提供者)是無感覺的。因為在服務端看來,收到的對象是完全一緻的。你可能覺得對于複雜類型,寫出這樣一個構造是很困難的。SOFA-Hessian中已經提供了一個工具類
com.alipay.hessian.generic.util.GenericUtils
來輔助使用者來生成,可以直接使用。
SOFARPC 與 Dubbo 的泛化調用比較
下面我們來介紹下泛化調用和業界一些其他産品的比較,首先介紹一下序列化本身的一些性能和優勢比較。
序列化本身的比較
在 github 上,有一個專門針對
Java 序列化進行的 benchmark,可以稍微做一下參考。雖然在實際的場景中, 每個序列化的場景不同,帶來的結果可能和這裡的 benchmark 結果不同,但還是有參考意義,從該項目的基準測試可以看出:Json 無論是壓縮比還是序列化時間,相比 hessian 等都有相當大的__劣勢__。
同時,雖然 hessian 相對于 protostuff、kryo 等在性能上有一點差距,但是 hessian 反序列化無需指定類型,這個優勢是非常有價值的。
Dubbo的泛化調用
在衆多的 RPC 架構中,Dubbo 也提供了泛化調用的功能,接下來我們再來說說 Dubbo 的泛化。Dubbo 泛化和 SOFA RPC 泛化最大的不同就是:Dubbo 需要服務端也支援泛化,是以,如果想提供泛化功能,服務端也必須進行更新,這看起來可能沒有 SOFA RPC 友好。
Dubbo 的泛化調用流程如下圖:
可以看到,Dubbo 的服務端也需要泛化過濾器将 Map 解析成 POJO 來解析資料。
總結
本文主要講解了 SOFARPC 泛化調用的設計與實作,介紹了泛化調用的場景,同時,提及了 SOFA RPC 泛化調用的 API 使用,也詳細講解了 SOFARPC 的泛化設計和實作。最後,對社群中的一些 RPC 架構的泛化調用做了簡單的比較。
這裡對SOFARPC 的泛化設計與實作做個小結:
- 設計目标是:服務端無需感覺是否泛化,一切都是由用戶端進行處理。帶來的好處是:應用如果想要支援泛化,不需要改動服務端,隻需要修改用戶端即可。這是和其他 RPC 架構泛化調用最大的差別。
- 實作方式:通過SOFA-Hessian 序列化支援泛化序列化,在進行泛化調用時,bolt 會根據上下文的序列化标記來使用對應的序列化器,SOFA-Hessian 特有的泛化序列化器可将 GenericObject 對象序列化成目标對象的位元組流,服務端按正常反序列化即可。SOFA-Hessian 特有的泛化反序列化器也可将目标傳回值反序列化成 GenericObject 等對象。
參考
https://github.com/eishay/jvm-serializers https://github.com/alipay/sofa-hessian http://www.sofastack.tech/sofa-rpc/docs/Generic-Invoke長按關注,擷取分布式架構幹貨
歡迎大家共同打造 SOFAStack
https://github.com/alipay