SOFA
Scalable Open Financial Architecture
是螞蟻金服自主研發的金融級分散式中介軟體,包含了構建金融級雲原生架構所需的各個元件,是在金融場景裡錘煉出來的最佳實踐。
本文為《剖析 | SOFARPC 框架》第十二篇,作者鷗波。
《剖析 | SOFARPC 框架》系列由 SOFA 團隊和原始碼愛好者們出品,
專案代號:
,官方目錄目前已經全部認領完畢,文末提供了已完成的文章目錄。
前言
隨著 TIOBE 10月份的程式語言排行的釋出,C++ 重回第三的位置,新興的 Swift 和 Go 表現出強勁的上升趨勢。雖然目前 Java 的領頭位置尚未出現有力挑戰,我們希望能夠在基礎設施的建設上預留跨語言的可擴充套件設計。同時,跨語言的挑戰也是工程實際面臨的現狀,螞蟻內部如 AI、IoT,演演算法等缺少 JVM 原生支援的領域,往往不可避免地需要涉及到跨語言呼叫的問題。
本文將為大家介紹 基於 SOFARPC 的微服務應用在面臨跨語言呼叫時的方案和實現。
總體設計
經過前面幾篇對 SOFARPC 的 BOLT 協議和序列化這些的介紹,相信大家已經對 RPC 有了一些理解,提到跨語言,我們會首先想到其他語言呼叫 Java,Java 呼叫其他語言,那麼這裡的跨,體現在程式碼上,到底跨在哪裡?
從跨語言的實現上來說,主要解決兩個方面的問題:
-
跨語言的通訊協議和序列化協議
-
跨語言服務發現
另外從跨語言的落地來說,還得解決一個平滑相容的問題。
業界常見的做法是一般是透過 DNS 和 HTTP 來解決跨語言的問題,但是在內部已經有完善技術棧體系的情況下,直接切換一個新的方案顯然是不合適的,所以螞蟻內部是在已有的技術體系基礎上進行改進。
螞蟻內部使用的通訊協議是 Bolt,序列化協議是 Hessian。我們知道,服務端和客戶端在請求和傳回之間攜帶的結構化的業務資料,需要在傳輸到達對端後,被本地的語言能夠易於解析消費。由於語言本身特性的差異,同一物件的在序列化和反序列化的轉換後,結構可能有差異,但是需要保證其轉換操作是可逆的。以上這點Hessian做的不是很好,其跨語言的相容性不能滿足跨語言的需求,所以另外一個可行的方案就是就是選擇其它基於 IDL 的序列化協議,例如 Protobuf。
現成的服務註冊中心一般都有一些多語言解決方案,像 Zookeeper、SOFARegistry、Consul、etcd 等都有多語言客戶端,所以服務發現這塊問題不算太大。
例如下麵就是一個基於註冊中心 + Bolt協議 + Protobuf 序列化的設計圖
通訊協議和序列化協議
通訊協議只要跨語言各方約定清楚,大家安裝約定實現即可,而序列化協議則需要較多的考量。
序列化的協議選擇列出一些考慮要點:
-
是否採用具備自我描述能力的序列化方案,如不需要藉助一些 schema 或者介面描述檔案。
-
是否為語言無關的,包括指令碼語言在內。
-
是否壓縮比例足夠小,滿足網路傳輸場景的要求。
-
是否序列化和反序列化的效能均足夠優秀。
-
是否向前/向後相容,能夠處理傳輸物件的新增屬性在服務端和客戶端版本不一致的情況。
-
是否支援加密、簽名、壓縮以及擴充套件的背景關係。
1、JSON Over HTTP
首先,說到跨語言,序列化支援,肯定有同學會問,為什麼不直接透過 Http的Json來搞定呢?
雖然得益於JSON和HTTP在各個語言的廣泛支援,在多語言場景下改造支援非常便捷,能夠低成本的解決網路通訊和序列化的問題。服務發現的過程則可以使用最簡單的固定URL(協議+域名+埠+路徑)的形式,負載均衡依賴於F5或者LVS等實現。
但是這個方案的有明顯的侷限性:
-
HTTP 作為無狀態的應用層協議,在效能上相比基於傳輸層協議(TCP)的方案處於劣勢。HTTP/1.1後可以透過設定keep-alive使用長連線,可以一定程度上規避建立連線的時間損耗;然而最大的問題是,客戶端執行緒採用了 request-response 的樣式,在發送了 request 之後被阻塞,直到拿到 response 之後才能繼續傳送。這一問題直到 HTTP/2.0 才被解決。
-
JSON 是基於明文的序列化,較二進位制的序列化方案,其序列化的結果可讀性強,但是壓縮率和效能仍有差距,這種對於網際網路高併發業務場景下,意味著硬體成本的提升。
-
對於網路變化的響應。訂閱端處理不夠強大。
2、Hessian Over BOLT
在否決了上一個方案後,我們繼續看,螞蟻內部,最開始的時候,SOFARPC 還沒有支援 Protobuf 作為序列化方式,當時為了跨語言,NodeJs的同學已經在此基礎上,用 js 重寫了一個 hessian 的版本,完成了序列化。也已經線上上平穩執行。但是當我們要擴充套件給其他語言的時候,重寫 hessian 的成本太高。而且 Java語言提供的介面和引數資訊,其他語言也需要自己理解一遍,對應地轉換成自己的語言物件。因此該方案在特定場景下是可行的。但不具備推廣至其他語言的優勢。
Node的實現版本可以參考:https://github.com/alipay/sofa-rpc-node
3、Protobuf Over BOLT
Protobuf 基於 IDL,本身具備平臺無關、跨語言的特性,是一個理想的序列化方案。但是需要先編寫proto檔案,結構化地描述傳輸的業務物件,並生成中間程式碼。
由於要重點介紹一下這種方案,因此再次回顧一下 SOFABolt 的協議規範部分,便於後面的解釋。
對於現有的通訊協議,我們改進時,將 content 部分儲存為入參物件和傳回值,他們都是 pb 序列化之後的值。這樣將直接對接到現在的協議上。又利用了 BOLT 的通訊協議。
以下描述了跨語言中對 Protobuf協議的使用:
首先我們看 essay-header 部分,是簡單的扁平化的 KV。預設會增加以下三個 Entry:
Key |
Value |
備註 |
sofa_head_method_name |
對方方法名 |
對應 SofaRequest#methodName |
sofa_head_target_app |
對方的應用名 |
對應 SofaRequest#targetAppName |
sofa_head_target_service |
對方的服務唯一命名 |
對應 SofaRequest#targetServiceUniqueName |
sofa_head_response_error |
true/false |
僅在響應中出現 |
我們再看 body 部分,根據 Protobuf 的實現,所有被序列化的物件均實現了 MessageLite 介面,然而由於多個 Classloader 存在的可能,程式碼上為了避免強轉 MessageList 介面的失敗,並未直接呼叫 toByteArray 方法,而是透過反射機制呼叫 toByteArray 獲得 byte 陣列。
針對 SofaRequest 這個 RPC 中的傳輸物件,由於 Protobuf 僅支援對於單個物件的序列化,因此 SofaRequest 型別的物件進行序列化,實際支援的是 SofaRequest#methodArgs 陣列中的首個元素物件進行的序列化,也就是說目前我們僅支援一個入參物件。
針對 SofaResponse 這個響應物件,當出現框架異常或者傳回物件是一個 Throwable 代表的業務異常時,直接將錯誤訊息字串序列化;併在響應頭中設定 sofa_head_response_error=true,其他情況才序列化業務傳回物件。這樣可以避免比如 Java 語言的錯誤棧,由於含有 一些執行緒類和異常類,其他語言是無法解析的。
反序列化的過程稍複雜一些,上游呼叫傳入 SofaRequest/SofaResponse 的實體,先要在空白的 SofaRequest 物件中填入前文中在 essay-header 反序列化中的解析的頭部資訊,接著根據 Header 中介面+方法名找到等待反序列化物件的 class,並藉助反射呼叫 parseFrom 介面生成物件,成為 SofaRequest#MethodArgs 的首個元素物件。
4、Others Over BOLT
在上一個方案的基礎上,我們也可以支援更多的語言,對 JSON、Kyro 的支援也分別處於開發和規劃中。 JSON 的支援已經開發完成待合併。這裡不再做過多說明。
服務發現
跨語言各方約定了通訊協議和序列化協議後,就可以完成各自的服務端和客戶端實現,跨語言已經能完成點對點的呼叫了。但在實際的線上場景下,我們還是需要透過註冊中心等服務發現的形式,來保證跨語言呼叫的可用性。目前,有兩種可選的方案。
1、各語言對接註冊中心
對於服務發現,前面說到的最早進行跨語言的 NodeJs 實現了對接 SOFARegistry 的能力。直接透過對 Java 原生序列化和一些 hessian 的重寫,來操作完成了。在螞蟻內部,這種方案在只有 Node 的情況下是可以的,但是更通用的場景下。如果我們有了新的註冊中心,要對接更多的註冊中心,其他語言在語言表達上的差異性,使得這種方案很難推廣到其他專案。NodeJs 版本的 hessian:https://github.com/alipay/sofa-hessian-node
2、各語言對接SOFAMosn
由於每個語言都去對接對接中心存在一定的難度,也不具備可推廣性,而在螞蟻內部,我們已經在一些跨語言的場景下,執行 SOFAMosn,透過 SOFAMosn,我們對接了站內的註冊中心,其他的語言,僅需要將自己需要訂閱和釋出的資訊,透過 Http 的介面形式,通知 SOFAMosn,SOFAMosn 將會將這些資訊和註冊中心進行註冊和訂閱,並維持地址資訊。
這樣對於其他語言來說,僅需要非常簡單的 json請求,就完成了跨語言的服務註冊和訂閱。後續新註冊中心的對接等等。其他語言都不再需要理解。相關的 SDK 我們已經開發並實現完成。對於 SOFAMosn 的更多介紹,可以參看 SOFAMosn 官網:
http://www.sofastack.tech/sofa-mosn/docs/README
語言 |
實現 |
python |
https://github.com/alipay/sofa-bolt-python |
node |
https://github.com/alipay/sofa-rpc-node |
c++ |
https://github.com/alipay/sofa-bolt-cpp |
當然如果你並不需要進行服務定址,或者能夠接受硬負載或者固定 IP的呼叫方式。也可以直接使用。
參考資料
-
JVM Serializers :
https://github.com/eishay/jvm-serializers/wiki
結語
至此,我們介紹了 SOFARPC 中對於 Protobuf 的跨語言實現,並介紹了一些 NodeJs 對跨語言的支援,最後介紹了我們用 SOFAMosn 實現通用的服務發現。
在大多數場景下,我們更推薦是使用 SOFAMosn 來做服務定址,這樣之後 Mosn 層面的一些限流熔斷,也可以在多語言上進行使用。
而對一些場景比較簡單,能夠容忍固定 IP 呼叫,或者使用硬體負載均衡裝置的。也可以直接使用各個跨語言客戶端,進行直接開發呼叫。
相關連結
SOFA 檔案: http://www.sofastack.tech/
SOFA: https://github.com/alipay
SOFARPC: https://github.com/alipay/sofa-rpc
SOFABolt: https://github.com/alipay/sofa-bolt
SOFAMosn: https://github.com/alipay/sofa-mosn
《剖析 | SOFARPC 框架》系列歷史文章
長按關註,獲取分散式架構乾貨
歡迎大家共同打造 SOFAStack https://github.com/alipay
點選閱讀原文,加入我們