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 中原有的序列化方式都有非常顯著的改進。