Kryo 和 FST 序列化
目錄
- 序列化探討
- 啟用 Kryo 和 FST
- 註冊要序列化的類別
- 無參數建構子和 Serializable 介面
- 序列化效能分析與測試
- 測試環境
- 測試腳本
- Dubbo RPC 中不同序列化產生位元組大小的比較
- Dubbo RPC 中不同序列化回應時間和吞吐量的比較
- 未來展望
序列化探討
dubbo RPC 是 dubbo 系統中核心的高性能、高吞吐量遠端呼叫方法。我喜歡稱之為多工 TCP 長連接呼叫。簡而言之
- 長連接:避免每次都需要建立新的 TCP 連接,提高呼叫的回應速度
- 多工:單個 TCP 連接可以交替傳輸多個請求和回應訊息,減少連接的等待閒置時間,從而在相同併發數下減少網路連接數量,提高系統吞吐量。
dubbo RPC 主要用於兩個 dubbo 系統之間的遠端呼叫,尤其適用於高併發、小資料量的網際網路場景。
序列化在遠端呼叫的回應速度、吞吐量和網路頻寬消耗方面也扮演著至關重要的角色,是我們提升分散式系統效能最關鍵的因素之一。
在 dubbo RPC 中,同時支援多種序列化方式,例如
- Dubbo 序列化:阿里巴巴尚未開發出成熟高效的 Java 序列化實現,阿里巴巴不建議在生產環境中使用
- Hessian2 序列化:Hessian 是一個跨語言的高效二進制序列化方法。但這裡實際上並不是原生的 hessian2 序列化,而是阿里巴巴修改過的 hessian lite,它是 dubbo RPC 預設啟用的序列化方法。
- JSON 序列化:目前有兩種實現,一種是使用阿里巴巴的 fastjson 庫,另一種是使用 dubbo 自己實現的簡單 json 庫,但其實現都不是特別成熟,而且 json 這種文本序列化的性能一般不如上面兩種二進制序列化。
- Java 序列化:主要是采用 JDK 自帶的 Java 序列化實現,性能很不理想。
總體上來說,這四種主要序列化方式的性能從上到下依次遞減。對於追求高性能的遠程調用框架 dubbo RPC 來說,實際上只有 1 和 2 兩種高效的序列化方式比較合適,而 dubbo 序列化由於還不成熟,所以實際上只有 Hessian2 一種選擇。所以 dubbo RPC 預設採用 hessian2 序列化。
但 hessian 是一個比較老的序列化實現,而且它是跨語言的,所以它並沒有針對 Java 做專門的優化。而實際上 dubbo RPC 是 Java 語言的 RPC,其實沒有必要採用跨語言的序列化(當然肯定也不排斥跨語言的序列化)。
近年來,各種新的高效序列化方式層出不窮,不斷刷新序列化性能的上限,比較典型的有
- 針對 Java 語言的:Kryo、FST 等
- 跨語言的:Protostuff、ProtoBuf、Thrift、Avro、MsgPack 等
這些序列化方式的性能大多都顯著優於 hessian2(甚至包括尚未成熟的 dubbo 序列化)。
有鑒於此,我們為 dubbo 引入了兩種高效的 Java 序列化實現:Kryo 和 FST,來逐步取代 hessian2。
其中,Kryo 是一個非常成熟的序列化實現,已經廣泛應用在 Twitter、Groupon、Yahoo 以及多個著名開源項目(如 Hive、Storm)中。而 FST 是一個比較新的序列化實現,目前還缺乏足夠多的成熟使用案例,但我認為其還是非常有潛力的。
在生產環境應用中,目前我推薦優先選擇 Kryo。
啟用 Kryo 和 FST
使用 Kryo 和 FST 非常簡單,只需添加相應的依賴即可:更多插件:Dubbo SPI 擴展
<dependency>
<groupId>org.apache.dubbo.extensions</groupId>
<artifactId>dubbo-serialization-kryo</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo.extensions</groupId>
<artifactId>dubbo-serialization-fst</artifactId>
<version>1.0.0</version>
</dependency>
然後在 dubbo RPC 的 XML 配置中添加一個屬性
<dubbo:protocol name="dubbo" serialization="kryo"/>
<dubbo:protocol name="dubbo" serialization="fst"/>
註冊要序列化的類別
為了使 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;
}
}
然後在 XML 配置中添加
<dubbo:protocol name="dubbo" serialization="kryo" optimizer="org.apache.dubbo.demo.SerializationOptimizerImpl"/>
註冊這些類之後,序列化性能可能會有顯著提升,特別是在對象嵌套層次比較淺的時候。
當然,在序列化一個類的時候,可能會級聯序列化很多類,比如 Java 集合類。針對這種情況,我們在對 JDK 中的常用類已經做了自動註冊,所以你不需要重複註冊這些類(當然重複註冊也沒有任何影響),包括
Gregorian Calendar
InvocationHandler
BigDecimal
BigInteger
pattern
BitSet
URIs
UUID
HashMap
ArrayList
LinkedList
HashSet
TreeSet
Hashtable
date
Calendar
ConcurrentHashMap
SimpleDateFormat
vector
BitSet
StringBuffer
String Builder
object
Object[]
String[]
byte[]
char[]
int[]
float[]
double[]
由於註冊需要序列化的類只是為了提升性能,所以即使你忘記註冊某些類也沒有關係。實際上,即使不註冊任何類,Kryo 和 FST 的性能也要普遍優於 hessian 和 dubbo 序列化。
當然,可能有人會問,為什麼不用配置文件來註冊這些類呢?這是因為,需要註冊的類往往數量很多,導致配置文件過於冗長;並且缺乏 IDE 的友好支持,編寫和重構配置文件都比 Java 類麻煩得多;最後,這些需要註冊的類一般在項目編譯打包之後就不會變更,所以沒必要放到配置文件中。
此外,有些人也會認為手動註冊序列化類是一個比較繁瑣的工作,是否可以用標註的方式,然後由系統自動發現並註冊。但標註方式的局限性在於,它只能用於標註你可以修改的類,而序列化中引用到的很多類很可能都是你無法修改的(比如第三方庫,或者 JDK 系統類或者其他專案的類)。另外,加入標註畢竟“污染”了代碼,使應用程式碼對框架產生了少許侵入性。
除了標註的方式,我們還可以考慮其他的自動註冊序列化類的方式,例如掃描類路徑,自動發現實現了 Serializable 接口(甚至包括 Externalizable)的類,將它們註冊。當然,我們知道,在類路徑下,可能會有很多的 Serializable 類,所以我們也可以考慮用包名前綴的方式,將掃描的範圍限定在一定的範圍內。
當然,在自動註冊機制中,特別需要注意的是如何保證服務提供方和消費方以相同的順序(或者 ID)註冊序列化類,以避免錯位。畢竟,在兩端,可以發現並註冊的類的數量可能都是不一樣的。
無參數建構子和 Serializable 介面
如果被序列化的類中不包含無參構造函數,則 Kryo 的序列化效能會大打折扣,因為此時我們會使用 Java 的序列化來透明地替換 Kryo 的序列化。因此,最佳實踐是為每一個被序列化的類都儘可能地添加無參構造函數(當然,如果一個 Java 類沒有自定義構造函數,那麼它就會有一個預設的無參構造函數)。
另外,Kryo 和 FST 並不要求被序列化的類實現 Serializable 接口,但我們仍然建議每個被序列化的類都去實現它,因為這樣可以保持和 Java 序列化的相容性,以及 dubbo 序列化的相容性。此外,它也使得我們未來可以考慮採用上述的某些自動註冊機制。
序列化效能分析與測試
在本文中,我們主要討論序列化,但在做效能分析和測試時,我們並非單獨針對每種序列化方式,而是將它們放到 dubbo RPC 中進行比較,因為這樣更加貼近實際情況。
測試環境
測試環境大致如下
- 兩台獨立的伺服器
- 四核 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
當然,由於測試環境的限制,所以目前的測試結果不一定非常權威和具有代表性。
測試腳本
測試用例
10 個併發客戶端持續發起請求
- 傳入一個嵌套的複雜對象(但單個數據量很小),不做任何處理,原樣返回
- 傳入 50K 的字串,不做任何處理,原樣返回(TODO:結果還未列出)
運行 5 分鐘的效能測試。(引用 dubbo 自身的測試說明:“主要是考量序列化和網路 IO 的效能,所以服務端不做任何業務邏輯。取 10 個併發是因為考慮到 rpc 協議在高併發下,CPU 使用率可能會成為瓶頸。”)
Dubbo RPC 中不同序列化產生位元組大小的比較
序列化生成的位元組數的大小是一個比較具有確定性的指標,它決定了遠程調用的網路傳輸時間和帶寬佔用。
複雜對象的測試結果如下(數值越小越好)
序列化實現 | 請求位元組數 | 回應位元組數 |
---|---|---|
Kryo | 272 | 90 |
FST | 288 | 96 |
Dubbo 序列化 | 430 | 186 |
Hessian | 546 | 329 |
FastJson | 461 | 218 |
JSON | 657 | 409 |
Java 序列化 | 963 | 630 |
Dubbo RPC 不同序列化方式的響應時間與吞吐量比較
遠端呼叫方法 | 平均響應時間 | 平均 TPS(每秒事務數) |
---|---|---|
REST: Jetty + JSON | 7.806 | 1280 |
REST: Jetty + JSON + GZIP | 待辦事項 | 待辦事項 |
REST: Jetty + XML | 待辦事項 | 待辦事項 |
REST: Jetty + XML + GZIP | 待辦事項 | 待辦事項 |
REST: Tomcat + JSON | 2.082 | 4796 |
REST: Netty + JSON | 2.182 | 4576 |
Dubbo: FST | 1.211 | 8244 |
Dubbo: Kryo | 1.182 | 8444 |
Dubbo: Dubbo 序列化 | 1.43 | 6982 |
Dubbo: Hessian2 | 1.49 | 6701 |
Dubbo: Fastjson | 1.572 | 6352 |
測試總結
從目前的結果來看,無論是產生的位元組大小、平均響應時間還是平均 TPS,Kryo 和 FST 在 Dubbo RPC 中相比原來的序列化方法都有顯著的提升。
未來展望
未來,當 Kryo 或 FST 在 Dubbo 中足夠成熟時,我們可能會將 Dubbo RPC 的預設序列化方式從 Hessian2 改為其中之一。