在Dubbo中使用高效的Java序列化(Kryo和FST)
http://dangdangdotcom.github.io/dubbox/serialization.html
作者:沈理
完善中……
TODO 生成可點選的目錄
目錄
- 序列化漫談
- 啟用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兩種高效序列化方式比較般配,而第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和FST
使用Kryo和FST非常簡單,只需要在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="com.alibaba.dubbo.demo.SerializationOptimizerImpl"/>
在註冊這些類後,序列化的效能可能被大大提升,特別針對小數量的巢狀物件的時候。
當然,在對一個類做序列化的時候,可能還級聯引用到很多類,比如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介面,但我們還是建議每個被序列化類都去實現它,因為這樣可以保持和Java序列化以及dubbo序列化的相容性,另外也使我們未來採用上述某些自動註冊機制帶來可能。
序列化效能分析與測試
本文我們主要討論的是序列化,但在做效能分析和測試的時候我們並不單獨處理每種序列化方式,而是把它們放到dubbo RPC中加以對比,因為這樣更有現實意義。
測試環境
粗略如下:
- 兩臺獨立伺服器
- 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中不同序列化生成位元組大小比較
序列化生成位元組碼的大小是一個比較有確定性的指標,它決定了遠端呼叫的網路傳輸時間和頻寬佔用。
針對複雜物件的結果如下(數值越小越好):
序列化實現 | 請求位元組數 | 響應位元組數 |
---|---|---|
Kryo | 272 | 90 |
FST | 288 | 96 |
Dubbo Serialization | 430 | 186 |
Hessian | 546 | 329 |
FastJson | 461 | 218 |
Json | 657 | 409 |
Java Serialization | 963 | 630 |
Dubbo RPC中不同序列化響應時間和吞吐量對比
遠端呼叫方式 | 平均響應時間 | 平均TPS(每秒事務數) |
---|---|---|
REST: Jetty + JSON | 7.806 | 1280 |
REST: Jetty + JSON + GZIP | TODO | TODO |
REST: Jetty + XML | TODO | TODO |
REST: Jetty + XML + GZIP | TODO | TODO |
REST: Tomcat + JSON | 2.082 | 4796 |
REST: Netty + JSON | 2.182 | 4576 |
Dubbo: FST | 1.211 | 8244 |
Dubbo: kyro | 1.182 | 8444 |
Dubbo: dubbo serialization | 1.43 | 6982 |
Dubbo: hessian2 | 1.49 | 6701 |
Dubbo: fastjson | 1.572 | 6352 |
測試總結
就目前結果而言,我們可以看到不管從生成位元組的大小,還是平均響應時間和平均TPS,Kryo和FST相比Dubbo RPC中原有的序列化方式都有非常顯著的改進。
未來
未來,當Kryo或者FST在dubbo中當應用足夠成熟之後,我們很可能會將dubbo RPC的預設序列化從hessian2改為它們中間的某一個。