螞蟻金服通訊框架SOFABolt解析|序列化機制(Serializer)

花肉醬發表於2018-12-11

SOFA

Scalable Open Financial Architecture

是螞蟻金服自主研發的金融級分散式中介軟體,包含了構建金融級雲原生架構所需的各個元件,是在金融場景裡錘鍊出來的最佳實踐。

本文為《螞蟻金服通訊框架SOFABolt解析》系列第二篇,作者魯道,就職於 E 籤寶。

《螞蟻金服通訊框架SOFABolt解析》系列由 SOFA 團隊和原始碼愛好者們出品。

前言

SOFABolt 是一款基於 Netty 最佳實踐,通用、高效、穩定的通訊框架。目前已經運用在了螞蟻中介軟體的微服務,訊息中心,分散式事務,分散式開關,配置中心等眾多產品上。

本文將重點分析 SOFABolt 的序列化機制。

我們知道,但凡在網路中傳輸資料,都涉及到序列化以及反序列化。即將資料編碼成位元組,再把位元組解碼成資料的過程。

例如在 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 設計了兩個介面:

  1. Serializer

    該介面定義 serialize 方法和 deserialize 方法,用於物件的序列化和反序列化。
  2. CustomSerializer
         該介面定義了很多方法,主要針對自定義協議中的 header 和 content 進行序列化和反序列化。同時提供上下文,以精細的控制時機。

同時,從框架設計的角度說,他們可以稱之為 “核心域”, 他們也被對應的 “服務域” 進行管理。

這裡解釋一下服務域和核心域,在框架設計裡,通常會有“核心域”,“服務域”, “會話域” 這三部分組成。

例如在 Spring 中,Bean 就是核心域,是核心領域模型,所有其他模型都向其靠攏;而 BeanFactory 是服務域,即服務“核心域”的模型,通常長期存在於系統中,且是單例;“會話域” 指的是一次會話產生的物件,會話結束則物件銷燬,例如 Request,Response。

在 SOFABolt 序列化機制中,Serializer 和 CustomSerializer 可以認為是核心域,同時,也有服務於他們的 “服務域”,即 SerializerManager 和 CustomSerializerManager。“會話域” RpcCommand 依賴 “服務域” 獲取 “核心域” 例項。

UML 設計圖如下:

image.png | left | 827x451

其中紅色部分就是 SOFABolt 序列化機制的核心介面,同時也是使用者的擴充套件介面,他們被各自的 Manager 服務域進行管理,最後,會話域 RpcCommand 依賴著 Manager 以獲取序列化元件。

這兩個介面的使用場景通常在資料被 協議編解碼器 編碼之前或解碼之後,進行處理。

例如在傳送資料之前,協議編碼器 根據通訊協議(如 bolt 協議)進行編碼,編碼之前,使用者需要將資料的具體內容進行序列化,協議編解碼器 再進行更詳細的編碼。

同樣,協議解碼器 在接收到 Socket 傳送來的位元組後,根據協議將位元組解碼成物件,但是,物件的內容還是位元組,需要使用者進行反序列化。

一個比較簡單的流程圖就是這樣的:

image.png | left | 827x238

上圖中,假設場景是 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 優先使用使用者的序列化器。

具體程式碼如下:

image.png | left | 827x363

行文至此,討論的都是“靈活”這個設計,即使用者既可以使用 SOFABolt 預設的序列化器,也可以使用自定義序列化器做更多的定製,值得注意的是: SOFABolt 優先使用使用者的序列化器。

讓我們再談談序列化的高效能部分 。

效能優化

上文提到,序列化和反序列化是重量級操作。通常,對效能敏感的框架都會對這一塊進行效能優化。

一般對序列化操作進行效能優化有以下三個實踐:
  1. 減少欄位,即使用更加複雜的對映從而減少網路中欄位的傳輸和編解碼。
 2. 使用零拷貝的序列化器,例如利用 Protostuff 實現序列化零拷貝。通常的反序列化都是 ByteBuf–>byte[]–>Biz 轉換過程,我們可以將中間的 byte[] 轉換過程砍掉,實現序列化的零拷貝。

  1. 將欄位拆分在不同的執行緒裡進行反序列化。

限於篇幅,本文將重點介紹第三點。

我們以 SOFARPC 協議為例,序列化內容包括 4 個部分:

  1. 基本欄位(固定24位元組)
  2. ClassName(變長位元組)
  3. Header(變長位元組)
  4. Content(變長位元組)

可以看到,基本欄位資料很少,序列化的主要壓力在後 3 個部分。

注意: 在請求傳送階段,即呼叫 Netty 的 writeAndFlush 介面之前,會在業務執行緒做好序列化,這部分沒什麼壓力。

但是,反序列化就不同了。

我們知道,高效能的網路框架基本都是使用的 Reactor 模型,即一個執行緒掛載多個 Channel(Socket),這個執行緒一般稱之為 IO 執行緒,如果這個執行緒執行任務耗時過長,將影響該執行緒下所有 Channel 的響應時間。無論是 Netty 的主要 Commiter —— Norman 還是 HSF 作者畢玄,都曾提出:永遠不要在 IO 執行緒做過多的耗時任務或者阻塞 IO 執行緒。

因此,為了效能考慮,這 3 個欄位通常不會都在 IO 執行緒中進行反序列化。

在 SOFABolt 預設的 RPC 協議實現中,__預設 IO 執行緒只反序列化 ClassName__,剩下的內容由業務執行緒反序列化。同時,為了最大程度配合業務特性,保證整體吞吐量, SOFABolt 設計了精細的開關來控制反序列化時機:

image.png | left | 735x418

使用場景 IO執行緒池策略 業務執行緒池策略
場景1 業務邏輯執行耗時(預設) 只反序列化className 反序列化header和content,並執行業務邏輯
場景2 隔離業務執行緒池 反序列化className和header,並根據header選擇業務執行緒池 反序列化content並執行業務邏輯
場景3 不切換執行緒,應用於TPS較低的場景 IO執行緒完成所有的操作,反序列化className、header、content、執行業務邏輯 無業務執行緒池

反序列化時機的選擇關係到系統的效能,同時在選擇這個策略時也要結合具體的業務場景。比如使用場景1的方式,可以在業務執行緒池中再加一個根據Header的分發邏輯,使IO執行緒做盡量少的工作,同時不同的業務操作之間也能通過執行緒池隔離,達到場景2的目的,但是相對場景2的方式多了一次執行緒切換的開銷。比如業務場景非常簡單且預期的TPS也很低,那麼選擇場景3的方式來減少程式設計的複雜度可能是更好的方式。反序列化時機的選擇需要貼合自己的實際業務場景去考量。

其中,SOFABolt 提供了一個介面,用於定義是否在 IO 執行緒執行所有任務:

其中,SOFABolt 提供了一個介面,用於定義是否在 IO 執行緒執行所有任務:

  • UserProcessor#processInIOThread
  1. 如果使用者返回 true,表示,所有的序列化及業務邏輯都在 IO 執行緒中執行。
  2. 反之,如果返回 fasle 且使用者使用了執行緒池隔離策略,那麼就由 IO 執行緒反序列化 header + className。
  3. 最後,如果返回 false,但使用者沒有使用執行緒池隔離策略,那麼所有的反序列化和業務邏輯則都在預設(Server預設或者業務預設)執行緒池執行。

虛擬碼如下:

image.png | left | 773x445

流程分析

為了直觀的描述 SOFABolt 序列化與反序列化流程, 我們將會給出物件處理的時序圖。實際上,應該有 4 種序列圖:

  1. Request 物件的序列化
  2. Request 物件的反序列化
  3. Response 物件的序列化
  4. Response 物件的反序列化

但限於篇幅,本文只給出 2 和 3 的序列圖,只當拋磚引玉,有興趣的同學可以自己檢視原始碼:)

首先是客戶端序列化 Response 物件。

image.png | left | 827x510

然後是服務端反序列化 Request 物件,實際上,效能優化通常就是在這個呼叫序列中 :)

image.png | left | 827x536

注意,上圖 “處理器根據使用者設定進行精細
反序列化” 步驟,就是 SOFABolt 對序列化優化的核心步驟。

擴充套件設計

為了方便使用者自定義序列化需求,SOFABolt 提供了兩種擴充套件方式設計:

1. 簡單的物件序列化擴充套件,例如 hessian,json,protostuff

如上文所述,如果沒有自定義 header 和 content 的需求,那麼直接使用 SOFABolt 的預設序列化即可,你可以通過以下方式來更換不同的序列化器(預設 hessian):

image.png | left | 827x443

2. 擴充套件 CustomSerializer 介面,自定義序列化 header,content

如果你需要自定義序列化,那麼你可以參考 SOFARPC 的方式,自己實現 CustomSerializer 介面,然後將其註冊到 SOFABolt 中,示例程式碼:

image.png | left | 827x411

同時,SOFABolt 原始碼中有更詳細的示例程式碼,地址:使用示例

總結

上文闡述了 SOFABolt 序列化的設計與實現,以及 SOFABolt 的序列化詳細機制,這裡再做一下總結:

  1. 靈活的控制反序列化時機的重要性

          由於服務提供者需要提供__高效能__的服務,通常使用 Reactor 模型的架構,那麼,就需要注意:通常不能在 IO 執行緒做耗時操作。因此,SOFABolt 預設只在 IO 執行緒反序列化少量資料(ClassName),其餘的資料都由業務執行緒進行反序列化,以最大化的利用 IO 執行緒處理連線的能力。
          同時,SOFABolt 也提供了更多場景的下的反序列化時機,例如 IO 密集型的業務,為了防止大量上下文切換,就可以直接在 IO 執行緒處理所有任務,包括業務邏輯。同時也停供業務執行緒池隔離的場景,此時 IO 執行緒在反序列化 ClassName 的基礎上,再反序列化 header,剩下的交有業務執行緒池。不可謂不靈活。
  2. 可擴充套件機制的重要性

          一個好的設計的框架,通常遵守 "微核外掛式,平等對待第三方規則,如果做不到微核,至少要平等對待第三方, 原作者要把自己當作擴充套件者,這樣才能保證框架的可持續性及由內向外的穩定性"。
    SOFABolt 的序列化器,使用者可以自定義擴充套件,無論是簡單的修改物件序列化器,還是自定義整個 header 和 content 的序列化,都是非常簡單的。讓使用者可以方便的擴充套件。因此,無論你是 RPC 中介軟體,還是訊息佇列中介軟體,使用 SOFABolt 來進行序列化都是非常的方便。
    

好了,本文到這裡,關於 SOFABolt 的序列化機制部分就介紹完畢了,讀者如果對序列化機制有什麼疑問,可在下方評論與作者溝通 ,期待共同交流 ?

image | left | 216x216

長按關注,獲取分散式架構乾貨

歡迎大家共同打造 SOFAStack https://github.com/alipay


相關文章