Dubbo + Kryo 實現高速序列化

Dubbo 中的序列化

Dubbo RPC 是 Dubbo 體系中最核心的一種高性能、高吞吐量的遠程調用方式,可以稱之爲多路複用的 TCP 長連接調用:

  • 長連接:避免了每次調用新建 TCP 連接,提高了調用的響應速度
  • 多路複用:單個 TCP 連接可交替傳輸多個請求和響應的消息,降低了連接的等待閒置時間,從而減少了同樣併發數下的網絡連接數,提高了系統吞吐量

Dubbo RPC 主要用於兩個 Dubbo 系統之間的遠程調用,特別適合高併發、小數據的互聯網場景。而序列化對於遠程調用的響應速度、吞吐量、網絡帶寬消耗等同樣也起着至關重要的作用,是我們提升分佈式系統性能的最關鍵因素之一。

Dubbo 中支持的序列化方式:

  • dubbo 序列化:阿里尚未開發成熟的高效 java 序列化實現,阿里不建議在生產環境使用它
  • hessian2 序列化:hessian 是一種跨語言的高效二進制序列化方式。但這裏實際不是原生的 hessian2 序列化,而是阿里修改過的 hessian lite,它是 dubbo RPC 默認啓用的序列化方式
  • json 序列化:目前有兩種實現,一種是採用的阿里的 fastjson 庫,另一種是採用 dubbo 中自己實現的簡單 * json 庫,但其實現都不是特別成熟,而且 json 這種文本序列化性能一般不如上面兩種二進制序列化。
  • java 序列化:主要是採用 JDK 自帶的 Java 序列化實現,性能很不理想。

在通常情況下,這四種主要序列化方式的性能從上到下依次遞減。對於 dubbo RPC 這種追求高性能的遠程調用方式來說,實際上只有 1、2 兩種高效序列化方式比較般配,而第 1 個 dubbo 序列化由於還不成熟,所以實際只剩下 2 可用,所以 dubbo RPC 默認採用 hessian2 序列化。

但 hessian 是一個比較老的序列化實現了,而且它是跨語言的,所以不是單獨針對 Java 進行優化的。而 dubbo RPC 實際上完全是一種 Java to Java 的遠程調用,其實沒有必要採用跨語言的序列化方式(當然肯定也不排斥跨語言的序列化)。

最近幾年,各種新的高效序列化方式層出不窮,不斷刷新序列化性能的上限,最典型的包括:

  • 專門針對 Java 語言的:Kryo,FST 等等
  • 跨語言的:Protostuff,ProtoBuf,Thrift,Avro,MsgPack 等等

有鑑於此,我們爲 dubbo 引入 Kryo 和 FST 這兩種高效 Java 序列化實現,來逐步取代 hessian2。
在面向生產環境的應用中,目前更優先選擇 Kryo。

啓用 Kryo

ProviderConsumer項目啓用 Kryo 高速序列化功能,兩個項目的配置方式相同

增加 Kryo 依賴

<dependency>
    <groupId>de.javakaffee</groupId>
    <artifactId>kryo-serializers</artifactId>
    <version>0.42</version>
</dependency>

增加配置

在這裏插入圖片描述

註冊被序列化類

要讓 Kryo 和 FST 完全發揮出高性能,最好將那些需要被序列化的類註冊到 dubbo 系統中,例如,我們可以實現如下回調接口:

public class SerializationOptimizerImpl implements SerializationOptimizer {
    public Collection<Class> getSerializableClasses() {
        List<Class> classes = new LinkedList<Class>();
        classes.add(BidRequest.class);
        classes.add(BidResponse.class);
        classes.add(Device.class);
        classes.add(Geo.class);
        classes.add(Impression.class);
        classes.add(SeatBid.class);
        return classes;
    }
}

在註冊這些類後,序列化的性能可能被大大提升,特別針對小數量的嵌套對象的時候。

當然,在對一個類做序列化的時候,可能還級聯引用到很多類,比如 Java 集合類。針對這種情況,我們已經自動將 JDK 中的常用類進行了註冊,所以你不需要重複註冊它們(當然你重複註冊了也沒有任何影響),包括:

GregorianCalendar
InvocationHandler
BigDecimal
BigInteger
Pattern
BitSet
URI
UUID
HashMap
ArrayList
LinkedList
HashSet
TreeSet
Hashtable
Date
Calendar
ConcurrentHashMap
SimpleDateFormat
Vector
BitSet
StringBuffer
StringBuilder
Object
Object[]
String[]
byte[]
char[]
int[]
float[]
double[]

由於註冊被序列化的類僅僅是出於性能優化的目的,所以即使你忘記註冊某些類也沒有關係。事實上,即使不註冊任何類,Kryo 和 FST 的性能依然普遍優於 hessian 和 dubbo 序列化。

爲什麼需要手動註冊

當然,有人可能會問爲什麼不用配置文件來註冊這些類?這是因爲要註冊的類往往數量較多,導致配置文件冗長;而且在沒有好的 IDE 支持的情況下,配置文件的編寫和重構都比 Java 類麻煩得多;最後,這些註冊的類一般是不需要在項目編譯打包後還需要做動態修改的。

另外,有人也會覺得手工註冊被序列化的類是一種相對繁瑣的工作,是不是可以用 annotation 來標註,然後系統來自動發現並註冊。但這裏 annotation 的侷限是,它只能用來標註你可以修改的類,而很多序列化中引用的類很可能是你沒法做修改的(比如第三方庫或者 JDK 系統類或者其他項目的類)。另外,添加 annotation 畢竟稍微的“污染”了一下代碼,使應用代碼對框架增加了一點點的依賴性。

除了 annotation,我們還可以考慮用其它方式來自動註冊被序列化的類,例如掃描類路徑,自動發現實現 Serializable 接口(甚至包括 Externalizable)的類並將它們註冊。當然,我們知道類路徑上能找到 Serializable 類可能是非常多的,所以也可以考慮用 package 前綴之類來一定程度限定掃描範圍。

當然,在自動註冊機制中,特別需要考慮如何保證服務提供端和消費端都以同樣的順序(或者 ID)來註冊類,避免錯位,畢竟兩端可被發現然後註冊的類的數量可能都是不一樣的。

無參構造函數和 Serializable 接口

如果被序列化的類中 不包含無參的構造函數,則在 Kryo 的序列化中,性能將會大打折扣,因爲此時我們在底層將用 Java 的序列化來透明的取代 Kryo 序列化。所以,儘可能爲每一個被序列化的類添加無參構造函數是一種最佳實踐(當然一個 Java 類如果不自定義構造函數,默認就有無參構造函數)。

另外,Kryo 和 FST 都不需要被序列化類實現 Serializable 接口,但我們還是建議每個被序列化類都去實現 Serializable 接口,因爲這樣可以保持和 Java 序列化以及 dubbo 序列化的兼容性,另外也使我們未來採用上述某些自動註冊機制帶來可能。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章