SOFA
gitScalable Open Financial Architecture程序員
是螞蟻金服自主研發的金融級分佈式中間件,包含了構建金融級雲原生架構所需的各個組件,是在金融場景裏錘鍊出來的最佳實踐。github
SOFABolt 是一款基於 Netty 最佳實踐,通用、高效、穩定的通訊框架。目前已經運用在了螞蟻中間件的微服務,消息中心,分佈式事務,分佈式開關,配置中心等衆多產品上。編程
本文將重點分析 SOFABolt 的序列化機制。json
咱們知道,但凡在網絡中傳輸數據,都涉及到序列化以及反序列化。即將數據編碼成字節,再把字節解碼成數據的過程。設計模式
例如在 RPC 框架中,一個重要的性能優化點是序列化機制的設計。即如何爲服務消費者和和服務提供者提供靈活的,高性能的序列化器。數組
這裏說的序列化器,不只僅是指「對象」的序列化器,例如 Hessian,Protostuff,JDK 原生這種「對象」級別的序列化器,而是指「協議」級別的序列化器,「對象」的序列化只是其中一部分。一般「協議」級別的序列化器包含更多的信息。性能優化
下面咱們將先從 SOFABolt 的設計及實現入手,進而分析 SOFABolt 詳細的序列化與分序列化流程,最後介紹 SOFABolt 序列化擴展。網絡
一個優秀的網絡通訊框架,必然要有一個靈活的,高性能的序列化機制。那麼,SOFABolt 序列化機制的設計目標是什麼呢?具體又是如何設計的呢?架構
首先說靈活,靈活指的是,框架的使用方(這裏指的是網絡通訊框架的使用方,例如 RPC,消息中心等中間件)可以自定義本身的實現,即用戶決定使用什麼類型的序列化以及怎麼序列化。
再說高效,序列化和反序列化事實上是一個重量級的操做,阿里 HSF 做者畢玄在著名的 NFS-RPC框架優化過程(從37k到168k) 文章中提到,其優化 RPC 傳輸性能的第一步就是調整反序列化操做,從而將 TPS 從 37k 提高到 56k。以後又經過更換對象序列化器,又將 TPS 提高了將近 10k。因而可知,合理地設計序列化機制對性能的影響十分巨大。
而 SOFABolt 和 HSF 有着親密的血緣關係,不但有着 HSF 的高性能,甚至在某些地方,優化的更爲完全。
咱們如今能夠看看 SOFABolt 序列化設計。
SOFABolt 設計了兩個接口:
Serializer
該接口定義 serialize 方法和 deserialize 方法,用於對象的序列化和反序列化。
CustomSerializer
該接口定義了不少方法,主要針對自定義協議中的 header 和 content 進行序列化和反序列化。同時提供上下文,以精細的控制時機。
同時,從框架設計的角度說,他們能夠稱之爲 「核心域」, 他們也被對應的 「服務域」 進行管理。
這裏解釋一下服務域和核心域,在框架設計裏,一般會有「核心域」,「服務域」, 「會話域」 這三部分組成。
例如在 Spring 中,Bean 就是核心域,是核心領域模型,全部其餘模型都向其靠攏;而 BeanFactory 是服務域,即服務「核心域」的模型,一般長期存在於系統中,且是單例;「會話域」 指的是一次會話產生的對象,會話結束則對象銷燬,例如 Request,Response。
在 SOFABolt 序列化機制中,Serializer 和 CustomSerializer 能夠認爲是核心域,同時,也有服務於他們的 「服務域」,即 SerializerManager 和 CustomSerializerManager。「會話域」 RpcCommand 依賴 「服務域」 獲取 「核心域」 實例。
UML 設計圖以下:
其中紅色部分就是 SOFABolt 序列化機制的核心接口,同時也是用戶的擴展接口,他們被各自的 Manager 服務域進行管理,最後,會話域 RpcCommand 依賴着 Manager 以獲取序列化組件。
這兩個接口的使用場景一般在數據被 協議編解碼器
編碼以前或解碼以後,進行處理。
例如在發送數據以前,協議編碼器
根據通訊協議(如 bolt 協議)進行編碼,編碼以前,用戶須要將數據的具體內容進行序列化,協議編解碼器
再進行更詳細的編碼。
一樣,協議解碼器
在接收到 Socket 發送來的字節後,根據協議將字節解碼成對象,可是,對象的內容仍是字節,須要用戶進行反序列化。
一個比較簡單的流程圖就是這樣的:
上圖中,假設場景是 Client 發送數據給 Server,那麼,編解碼器負責將字節流解碼成 Command 對象,序列化器負責將 Command 對象裏的內容反序列化成業務對象,從設計模式的角度看,這裏是 GOF 中 「命令模式」和「職責鏈模式」的組合設計。
看完了設計,再看看實現。
咱們能夠看看這兩個接口的實現。
Serializer
Serializer 接口在 SOFABolt 中已有默認實現,即 HessianSerializer,目前使用的是 hessian-3.3.0 版本。經過一個 SerializerManager 管理器進行管理。注意,這個管理器內部使用的是數組,而不是 Map,這在上文畢玄的文章也曾提到:經過使用數組替換成 Map,NFS-RPC 框架的 TPS 從 153k 提高到 160k。事實上,任何對性能很是敏感的框架,能用數組就毫不用 Map,例如 Netty 的 FastThreadLocal,也是如此。
固然,Serializer 接口用戶也是能夠擴展的,例如使用 protostuff,FastJson,kryo 等,擴展後,經過 SerializerManager 能夠將本身的序列化器添加到 SOFABolt 中。注意:這裏的序列化 type 實際就是上面提到的數組的下標,因此不能和其餘序列化器的下標有衝突。
CustomSerializer
再說 CustomSerializer,這個接口也是有默認實現的,用戶也能夠選擇本身實現,咱們這裏以 SOFARPC 爲例。
SOFARPC 在其擴展模塊 sofa-rpc-remoting-bolt
中,經過實現 CustomSerializer 接口,本身實現了序列化 header,content。
這裏稍微擴展講一下 header 和 content。實際上,header 和 content 相似 http 協議的消息頭和消息體,header 和 content 中到底存放什麼內容,取決於協議設計者。
例如在 SOFARPC 的協議中,header 裏存放的是一些擴展屬性和元信息上下文。而 content 中存放的則是主要的一些信息,好比 request 對象,request 對象裏就存放了 RPC 調用中經常使用信息了,例如參數,類型,方法名稱。
同時,CustomSerializer 接口定義的方法中,提供了 InvokeContext 上下文,例如是否泛化調用
等信息,當進行序列化時,將是否泛型的信息放入上下文,反序列化時,再從上下文中取出該屬性,便可正確處理泛化調用。
注意,若是用戶已經本身實現了 CustomSerializer 接口,那麼 SOFABolt 的 SerializerManager 中設置的序列化器將不起做用!由於 SOFABolt 優先使用用戶的序列化器。
具體代碼以下:
行文至此,討論的都是「靈活」這個設計,即用戶既可使用 SOFABolt 默認的序列化器,也可使用自定義序列化器作更多的定製,值得注意的是: SOFABolt 優先使用用戶的序列化器。
讓咱們再談談序列化的高性能部分 。
上文提到,序列化和反序列化是重量級操做。一般,對性能敏感的框架都會對這一塊進行性能優化。
通常對序列化操做進行性能優化有如下三個實踐:
減小字段,即便用更加複雜的映射從而減小網絡中字段的傳輸和編解碼。
使用零拷貝的序列化器,例如利用 Protostuff 實現序列化零拷貝。一般的反序列化都是 ByteBuf-->byte[]-->Biz 轉換過程,咱們能夠將中間的 byte[] 轉換過程砍掉,實現序列化的零拷貝。
將字段拆分在不一樣的線程裏進行反序列化。
限於篇幅,本文將重點介紹第三點。
咱們以 SOFARPC 協議爲例,序列化內容包括 4 個部分:
基本字段(固定24字節)
ClassName(變長字節)
Header(變長字節)
Content(變長字節)
能夠看到,基本字段數據不多,序列化的主要壓力在後 3 個部分。
注意: 在請求發送階段,即調用 Netty 的 writeAndFlush 接口以前,會在業務線程作好序列化,這部分沒什麼壓力。
可是,反序列化就不一樣了。
咱們知道,高性能的網絡框架基本都是使用的 Reactor 模型,即一個線程掛載多個 Channel(Socket),這個線程通常稱之爲 IO 線程,若是這個線程執行任務耗時過長,將影響該線程下全部 Channel 的響應時間。不管是 Netty 的主要 Commiter —— Norman 仍是 HSF 做者畢玄,都曾提出:永遠不要在 IO 線程作過多的耗時任務或者阻塞 IO 線程。
所以,爲了性能考慮,這 3 個字段一般不會都在 IO 線程中進行反序列化。
在 SOFABolt 默認的 RPC 協議實現中,默認 IO 線程只反序列化 ClassName,剩下的內容由業務線程反序列化。同時,爲了最大程度配合業務特性,保證總體吞吐量, SOFABolt 設計了精細的開關來控制反序列化時機:
使用場景
|
IO線程池策略
|
業務線程池策略
|
|
場景1
|
業務邏輯執行耗時(默認)
|
只反序列化className
|
反序列化header和content,並執行業務邏輯
|
場景2
|
隔離業務線程池
|
反序列化className和header,並根據header選擇業務線程池
|
反序列化content並執行業務邏輯
|
場景3
|
不切換線程,應用於TPS較低的場景
|
IO線程完成全部的操做,反序列化className、header、content、執行業務邏輯
|
無業務線程池
|
其中,SOFABolt 提供了一個接口,用於定義是否在 IO 線程執行全部任務:
UserProcessor#processInIOThread
若是用戶返回 true,表示,全部的序列化及業務邏輯都在 IO 線程中執行。
反之,若是返回 fasle 且用戶使用了線程池隔離策略,那麼就由 IO 線程反序列化 header + className。
最後,若是返回 false,但用戶沒有使用線程池隔離策略,那麼全部的反序列化和業務邏輯則都在默認(Server默認或者業務默認)線程池執行。
僞代碼以下:
爲了直觀的描述 SOFABolt 序列化與反序列化流程, 咱們將會給出對象處理的時序圖。實際上,應該有 4 種序列圖:
Request 對象的序列化
Request 對象的反序列化
Response 對象的序列化
Response 對象的反序列化
但限於篇幅,本文只給出 2 和 3 的序列圖,只當拋磚引玉,有興趣的同窗能夠本身查看源碼:)
首先是客戶端序列化 Response 對象。
而後是服務端反序列化 Request 對象,實際上,性能優化一般就是在這個調用序列中 :)
注意,上圖 「處理器根據用戶設置進行精細
反序列化」 步驟,就是 SOFABolt 對序列化優化的核心步驟。
爲了方便用戶自定義序列化需求,SOFABolt 提供了兩種擴展方式設計:
如上文所述,若是沒有自定義 header 和 content 的需求,那麼直接使用 SOFABolt 的默認序列化便可,你能夠經過如下方式來更換不一樣的序列化器(默認 hessian):
若是你須要自定義序列化,那麼你能夠參考 SOFARPC 的方式,本身實現 CustomSerializer 接口,而後將其註冊到 SOFABolt 中,示例代碼:
同時,SOFABolt 源碼中有更詳細的示例代碼,地址:使用示例
上文闡述了 SOFABolt 序列化的設計與實現,以及 SOFABolt 的序列化詳細機制,這裏再作一下總結:
靈活的控制反序列化時機的重要性
因爲服務提供者須要提供高性能的服務,一般使用 Reactor 模型的架構,那麼,就須要注意:一般不能在 IO 線程作耗時操做。所以,SOFABolt 默認只在 IO 線程反序列化少許數據(ClassName),其他的數據都由業務線程進行反序列化,以最大化的利用 IO 線程處理鏈接的能力。
同時,SOFABolt 也提供了更多場景的下的反序列化時機,例如 IO 密集型的業務,爲了防止大量上下文切換,就能夠直接在 IO 線程處理全部任務,包括業務邏輯。同時也停供業務線程池隔離的場景,此時 IO 線程在反序列化 ClassName 的基礎上,再反序列化 header,剩下的交有業務線程池。不可謂不靈活。
可擴展機制的重要性
一個好的設計的框架,一般遵照 "微核插件式,平等對待第三方規則,若是作不到微核,至少要平等對待第三方, 原做者要把本身看成擴展者,這樣才能保證框架的可持續性及由內向外的穩定性"。
SOFABolt 的序列化器,用戶能夠自定義擴展,不管是簡單的修改對象序列化器,仍是自定義整個 header 和 content 的序列化,都是很是簡單的。讓用戶能夠方便的擴展。所以,不管你是 RPC 中間件,仍是消息隊列中間件,使用 SOFABolt 來進行序列化都是很是的方便。
好了,本文到這裏,關於 SOFABolt 的序列化機制部分就介紹完畢了,讀者若是對序列化機制有什麼疑問,可在下方評論與做者溝通 ,期待共同交流
相關連接
SOFA 文檔: http://www.sofastack.tech/
SOFA: https://github.com/alipay
SOFARPC: https://github.com/alipay/sofa-rpc
SOFABolt: https://github.com/alipay/sofa-bolt
參與文章共建,獲取 SOFA 限量周邊,作最酷的程序員
(這張是做者曬單!)
長按關注,獲取分佈式架構乾貨
歡迎你們共同打造 SOFAStack https://github.com/alipay