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
在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 序列化的兼容性,
另外也使我們未來採用上述某些自動註冊機制帶來可能。