Dubbo 支持的幾個(gè)主流序列化框架評(píng)測(cè)

前言
今天要聊的技術(shù)是序列化,這不是我第一次寫序列化相關(guān)的文章了,今天動(dòng)筆之前,我還特地去博客翻了下我博客早期的一篇序列化文章(如下圖),竟然都過去 4 年了。
為什么又想聊序列化了呢?因?yàn)樽罱墓ぷ饔玫搅诵蛄谢嚓P(guān)的內(nèi)容,其次,這幾年 Dubbo 也發(fā)生了翻天覆地的變化,其中 Dubbo 3.0 主推的 Tripple 協(xié)議,更是打著下一代 RPC 通信協(xié)議的旗號(hào),有取代 Dubbo 協(xié)議的勢(shì)頭。而 Tripple 協(xié)議使用的便是 Protobuf 序列化方案。
另外,Dubbo 社區(qū)也專門搞了一個(gè)序列化壓測(cè)的項(xiàng)目:https://github.com/apache/dubbo-benchmark.git,本文也將圍繞這個(gè)項(xiàng)目,從性能維度展開對(duì) Dubbo 支持的各個(gè)序列化框架的討論。
當(dāng)我們聊序列化的時(shí)候,我們關(guān)注什么?
最近幾年,各種新的高效序列化方式層出不窮,最典型的包括:
專門針對(duì) Java 語言的:JDK 序列化、Kryo、FST 跨語言的:Protostuff,ProtoBuf,Thrift,Avro,MsgPack 等等
為什么開源社區(qū)涌現(xiàn)了這么多的序列化框架,Dubbo 也擴(kuò)展了這么多的序列化實(shí)現(xiàn)呢?主要還是為了滿足不同的需求。
序列化框架的選擇主要有以下幾個(gè)方面:
跨語言。是否只能用于 java 間序列化 / 反序列化,是否跨語言,跨平臺(tái)。 性能。分為空間開銷和時(shí)間開銷。序列化后的數(shù)據(jù)一般用于存儲(chǔ)或網(wǎng)絡(luò)傳輸,其大小是很重要的一個(gè)參數(shù);解析的時(shí)間也影響了序列化協(xié)議的選擇,如今的系統(tǒng)都在追求極致的性能。 兼容性。系統(tǒng)升級(jí)不可避免,某一實(shí)體的屬性變更,會(huì)不會(huì)導(dǎo)致反序列化異常,也應(yīng)該納入序列化協(xié)議的考量范圍。
和 CAP 理論有點(diǎn)類似,目前市面上很少有一款序列化框架能夠同時(shí)在三個(gè)方面做到突出,例如 Hessian2 在兼容性方面的表現(xiàn)十分優(yōu)秀,性能也尚可,Dubbo 便使用了其作為默認(rèn)序列化實(shí)現(xiàn),而性能方面它其實(shí)是不如 Kryo 和 FST 的,在跨語言這一層面,它表現(xiàn)的也遠(yuǎn)不如 ProtoBuf,JSON。
其實(shí)反過來想想,要是有一個(gè)序列化方案既是跨語言的,又有超高的性能,又有很好的兼容性,那不早就成為分布式領(lǐng)域的標(biāo)準(zhǔn)了?其他框架早就被干趴了。
大多數(shù)時(shí)候,我們是挑選自己關(guān)注的點(diǎn),找到合適的框架,滿足我們的訴求,這才導(dǎo)致了序列化框架百花齊放的局面。
性能測(cè)試
很多序列化框架都宣稱自己是“高性能”的,光他們說不行呀,我還是比較篤信“benchmark everything”的箴言,這樣得出的結(jié)論,更能讓我對(duì)各個(gè)技術(shù)有自己的認(rèn)知,避免人云亦云,避免被不是很權(quán)威的博文誤導(dǎo)。
怎么做性能測(cè)試呢?例如像這樣?
long start = System.currentTimeMillis();
measure();
System.out.println(System.currentTimeMillis()-start);
貌似不太高大上,但又說不上有什么問題。如果你這么想,那我推薦你了解下 JMH 基準(zhǔn)測(cè)試框架,我之前寫過的一篇文章《JAVA 拾遺 — JMH 與 8 個(gè)測(cè)試陷阱》推薦你先閱讀以下。
事實(shí)上,Dubbo 社區(qū)的貢獻(xiàn)者們?cè)缇痛罱艘粋€(gè)比較完備的 Dubbo 序列化基礎(chǔ)測(cè)試工程:https://github.com/apache/dubbo-benchmark.git。

你只要具備基本的 JMH 和 Dubbo 的知識(shí),就可以測(cè)試出在 Dubbo 場景下各個(gè)序列化框架的表現(xiàn)。
我這里也準(zhǔn)備了一份我測(cè)試的報(bào)告,供讀者們參考。如果大家準(zhǔn)備自行測(cè)試,不建議在個(gè)人 windows/mac 上 benchmark,結(jié)論可能會(huì)不準(zhǔn)確。我使用了兩臺(tái)阿里云的 ECS 來進(jìn)行測(cè)試,測(cè)試環(huán)境:Aliyun Linux,4c8g,啟動(dòng)腳本:
java -server -Xmx2g -Xms2g -XX:MaxDirectMemorySize=1g -XX:+UseG1GC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:/home/admin/
為啥選擇這個(gè)配置?我手上正好有兩臺(tái)這樣的資源,沒有特殊的設(shè)置~,況且從啟動(dòng)腳本就可以看出來,壓測(cè)程序不會(huì)占用太多資源,我都沒用滿。
測(cè)試工程介紹:
public interface UserService {
public boolean existUser(String email);
public boolean createUser(User user);
public User getUser(long id);
public Page<User> listUser(int pageNo);
}
一個(gè) UserService 接口對(duì)業(yè)務(wù)應(yīng)用中的 CRUD 操作。server 端以不同的序列化方案提供該服務(wù),client 使用 JMH 進(jìn)行多輪壓測(cè)。
@Benchmark
@BenchmarkMode({Mode.Throughput })
@OutputTimeUnit(TimeUnit.SECONDS)
@Override
public boolean existUser() throws Exception {
// ...
}
@Benchmark
@BenchmarkMode({Mode.Throughput})
@OutputTimeUnit(TimeUnit.SECONDS)
@Override
public boolean createUser() throws Exception {
// ...
}
@Benchmark
@BenchmarkMode({Mode.Throughput})
@OutputTimeUnit(TimeUnit.SECONDS)
@Override
public User getUser() throws Exception {
// ...
}
@Benchmark
@BenchmarkMode({Mode.Throughput})
@OutputTimeUnit(TimeUnit.SECONDS)
@Override
public Page<User> listUser() throws Exception {
// ...
}
整體的 benchmark 框架結(jié)構(gòu)如上,詳細(xì)的實(shí)現(xiàn),可以參考源碼。我這里只選擇的一個(gè)評(píng)測(cè)指標(biāo) Throughput,即吞吐量。
省略一系列壓測(cè)過程,直接給出結(jié)果:
Kryo
Benchmark Mode Cnt Score Error Units
Client.createUser thrpt 3 20913.339 ± 3948.207 ops/s
Client.existUser thrpt 3 31669.871 ± 1582.723 ops/s
Client.getUser thrpt 3 29706.647 ± 3278.029 ops/s
Client.listUser thrpt 3 17234.979 ± 1818.964 ops/s
Fst
Benchmark Mode Cnt Score Error Units
Client.createUser thrpt 3 15438.865 ± 4396.911 ops/s
Client.existUser thrpt 3 25197.331 ± 12116.109 ops/s
Client.getUser thrpt 3 21723.626 ± 7441.582 ops/s
Client.listUser thrpt 3 15768.321 ± 11684.183 ops/s
Hessian2
Benchmark Mode Cnt Score Error Units
Client.createUser thrpt 3 22948.875 ± 2005.721 ops/s
Client.existUser thrpt 3 34735.122 ± 1477.339 ops/s
Client.getUser thrpt 3 20679.921 ± 999.129 ops/s
Client.listUser thrpt 3 3590.129 ± 673.889 ops/s
FastJson
Benchmark Mode Cnt Score Error Units
Client.createUser thrpt 3 26269.487 ± 1667.895 ops/s
Client.existUser thrpt 3 29468.687 ± 5152.944 ops/s
Client.getUser thrpt 3 25204.239 ± 4326.485 ops/s
Client.listUser thrpt 3 9823.574 ± 2087.110 ops/s
Tripple
Benchmark Mode Cnt Score Error Units
Client.createUser thrpt 3 19721.871 ± 5121.444 ops/s
Client.existUser thrpt 3 35350.031 ± 20801.169 ops/s
Client.getUser thrpt 3 20841.078 ± 8583.225 ops/s
Client.listUser thrpt 3 4655.687 ± 207.503 ops/s
怎么看到這個(gè)測(cè)試結(jié)果呢?createUser、existUser、getUser 這幾個(gè)方法測(cè)試下來,效果是參差不齊的,不能完全得出哪個(gè)框架性能最優(yōu),我的推測(cè)是因?yàn)樾蛄谢臄?shù)據(jù)量比較簡單,量也不大,就是一個(gè)簡單的 User 對(duì)象;而 listUser 的實(shí)現(xiàn)是返回了一個(gè)較大的 List<User> ,可以發(fā)現(xiàn),Kryo 和 Fst 序列化的確表現(xiàn)優(yōu)秀,處于第一梯隊(duì);令我意外的是 FastJson 竟然比 Hessian 還要優(yōu)秀,位列第二梯隊(duì);Tripple(背后是 ProtoBuf)和 Hessian2 位列第三梯隊(duì)。
當(dāng)然,這樣的結(jié)論一定受限于 benchmark 的模型,測(cè)試用例中模擬的 CRUD 也不一定完全貼近業(yè)務(wù)場景,畢竟業(yè)務(wù)是復(fù)雜的。
怎么樣,這樣的結(jié)果是不是也符合你的預(yù)期呢?
Dubbo 序列化二三事
最后,聊聊你可能知道也可能不知道的一些序列化知識(shí)。
hession-lite
Dubbo 使用的 Hessian2 其實(shí)并不是原生的 Hessian2 方案。注意看源碼中的依賴:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>hessian-lite</artifactId>
</dependency>
最早是阿里開源的 hessian-lite,后來隨著 Dubbo 貢獻(xiàn)給了 Apache,該項(xiàng)目也一并進(jìn)入了 Apache,github 地址:https://github.com/apache/dubbo-hessian-lite。相比原生 Hessian2,Dubbo 獨(dú)立了一個(gè)倉庫致力于在 RPC 場景下,發(fā)揮出更高的性能以及滿足一些定制化的需求。
在 IO 線程中進(jìn)行序列化
Dubbo 客戶端在高版本中默認(rèn)是在業(yè)務(wù)線程中進(jìn)行序列化的,而不是 IO 線程,你可以通過 decode.in.io 控制序列化與哪個(gè)線程綁定
<dubbo:reference id="userService" check="false"
interface="org.apache.dubbo.benchmark.service.UserService"
url="dubbo://${server.host}:${server.port}">
<dubbo:parameter key="decode.in.io" value="true" />
</dubbo:reference>
在 benchmark 時(shí),我發(fā)現(xiàn) IO 線程中進(jìn)行序列化,性能會(huì)更好,這可能和序列化本身是一個(gè)耗費(fèi) CPU 的操作,多線程無法加速反而會(huì)導(dǎo)致更多的競爭有關(guān)。
SerializationOptimizer
某些序列化實(shí)現(xiàn),例如 Kryo 和 Fst 可以通過顯示注冊(cè)序列化的類來進(jìn)行加速,如果想利用該特性來提升序列化性能,可以實(shí)現(xiàn) org.apache.dubbo.common.serialize.support.SerializationOptimizer 接口。一個(gè)示例:
public class SerializationOptimizerImpl implements SerializationOptimizer {
@Override
public Collection<Class<?>> getSerializableClasses() {
return Arrays.asList(User.class, Page.class, UserService.class);
}
}
按照大多數(shù)人的習(xí)慣,可能會(huì)覺得這很麻煩,估計(jì)很少有用戶這么用。注意客戶端和服務(wù)端需要同時(shí)開啟這一優(yōu)化。
別忘了在 protocol 上配置指定這一優(yōu)化器:
<dubbo:protocol name="dubbo" host="${server.host}" server="netty4" port="${server.port}" serialization="kryo" optimizer="org.apache.dubbo.benchmark.serialize.SerializationOptimizerImpl"/>
序列化方式由服務(wù)端指定
一般而言,Dubbo 框架使用的協(xié)議(默認(rèn)是 dubbo)和序列化方式(默認(rèn)是 hessian2)是由服務(wù)端指定的,不需要在消費(fèi)端指定。因?yàn)榉?wù)端是服務(wù)的提供者,擁有對(duì)服務(wù)的定義權(quán),消費(fèi)者在訂閱服務(wù)收到服務(wù)地址通知時(shí),服務(wù)地址會(huì)包含序列化的實(shí)現(xiàn)方式,Dubbo 以這樣的契約方式從而實(shí)現(xiàn) consumer 和 provider 的協(xié)同通信。
在大多數(shù)業(yè)務(wù)應(yīng)用,應(yīng)用可能既是服務(wù) A 的提供者,同時(shí)也是服務(wù) B 的消費(fèi)者,所以建議在架構(gòu)決策者層面協(xié)商固定出統(tǒng)一的協(xié)議,如果沒有特殊需求,保持默認(rèn)值即可。
但如果應(yīng)用僅僅作為消費(fèi)者,而又想指定序列化協(xié)議或者優(yōu)化器(某些特殊場景),注意這時(shí)候配置 protolcol 是不生效的,因?yàn)闆]有服務(wù)提供者是不會(huì)觸發(fā) protocol 的配置流程的。可以像下面這樣指定消費(fèi)者的配置:
<dubbo:reference id="userService" check="false"
interface="org.apache.dubbo.benchmark.service.UserService"
url="dubbo://${server.host}:${server.port}?optimizer=org.apache.dubbo.benchmark.serialize.SerializationOptimizerImpl&serialization=kryo">
<dubbo:parameter key="decode.in.io" value="true" />
</dubbo:reference>
&代表 &,避免 xml 中的轉(zhuǎn)義問題
總結(jié)
借 Dubbo 中各個(gè)序列化框架的實(shí)現(xiàn),本文探討了選擇序列化框架時(shí)我們的關(guān)注點(diǎn),并探討了各個(gè)序列化實(shí)現(xiàn)在 Dubbo 中具體的性能表現(xiàn), 給出了詳細(xì)的測(cè)試報(bào)告,同時(shí),也給出了一些序列化的小技巧,如果在 Dubbo 中修改默認(rèn)的序列化行為,你可能需要關(guān)注這些細(xì)節(jié)。
最后再借 Dubbo3 支持的 Tripple 協(xié)議來聊一下技術(shù)發(fā)展趨勢(shì)的問題。我們知道 json 能替代 xml 作為眾多前后端開發(fā)者耳熟能詳?shù)囊粋€(gè)技術(shù),并不是因?yàn)槠湫阅苋绾稳绾危窃谟谄淝∪缙浞值慕鉀Q了大家的問題。一個(gè)技術(shù)能否流行,也是如此,一定在于其幫助用戶解決了痛點(diǎn)。至于解決了什么問題,在各個(gè)歷史發(fā)展階段又是不同的,曾經(jīng),Dubbo2.x 憑借著其豐富的擴(kuò)展能力,強(qiáng)大的性能,活躍度高的社區(qū)等優(yōu)勢(shì)幫助用戶解決一系列的難題,也獲得了非常多用戶的親來;現(xiàn)在,Dubbo3.x 提出的應(yīng)用級(jí)服務(wù)發(fā)現(xiàn)、統(tǒng)一治理規(guī)則、Tripple 協(xié)議,也是在嘗試解決云原生時(shí)代下的難題,如多語言,適配云原生基礎(chǔ)設(shè)施等,追趕時(shí)代,幫助用戶。
