Dubbo + Kryo 實現高速序列化

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 等等

這些序列化方式的性能多數都顯著優于 hessian2(甚至包括尚未成熟的 dubbo 序列化)

有鑒于此,我們為 dubbo 引入 Kryo 和 FST 這兩種高效 Java 序列化實現,來逐步取代 hessian2。

其中,Kryo 是一種非常成熟的序列化實現,已經在 Twitter、Groupon、Yahoo 以及多個著名開源項目(如 Hive、Storm)中廣泛的使用。而 FST 是一種較新的序列化實現,目前還缺乏足夠多的成熟使用案例。

在面向生產環境的應用中,目前更優先選擇 Kryo。

啟用 Kryo

在 Provider 和 Consumer 項目啟用 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 序列化的兼容性,另外也使我們未來采用上述某些自動注冊機制帶來可能。

附:序列化性能分析與測試

測試環境

  • 兩臺獨立服務器
  • 4 核 Intel(R) Xeon(R) CPU E5-2603 0 @ 1.80GHz
  • 8G 內存
  • 虛擬機之間網絡通過百兆交換機
  • CentOS 5
  • JDK 7
  • Tomcat 7
  • JVM 參數 -server -Xms1g -Xmx1g -XX:PermSize=64M -XX:+UseConcMarkSweepGC

注意: 當然這個測試環境較有局限,故當前測試結果未必有非常權威的代表性

測試腳本

和 dubbo 自身的基準測試保持接近,10 個并發客戶端持續不斷發出請求:

  • 傳入嵌套復雜對象(但單個數據量很小),不做任何處理,原樣返回
  • 傳入 50K 字符串,不做任何處理,原樣返回(TODO:結果尚未列出)

進行 5 分鐘性能測試。(引用 dubbo 自身測試的考慮:“主要考察序列化和網絡 IO 的性能,因此服務端無任何業務邏輯。取 10 并發是考慮到 http 協議在高并發下對 CPU 的使用率較高可能會先達到瓶頸。”)

Dubbo RPC 中不同序列化生成字節大小比較

序列化生成字節碼的大小是一個比較有確定性的指標,它決定了遠程調用的網絡傳輸時間和帶寬占用。

針對復雜對象的結果如下(數值越小越好):

Dubbo RPC 中不同序列化響應時間和吞吐量對比

結論

就目前結果而言,我們可以看到不管從生成字節的大小,還是平均響應時間和平均 TPS,Kryo 和 FST 相比 Dubbo RPC 中原有的序列化方式都有非常顯著的改進。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。