前言
今天要聊的技術是序列化,這不是我第一次寫序列化相關的文章了,今天動筆之前,我還特地去博客翻了下我博客早期的一篇序列化文章(如下圖),竟然都過去 4 年了。
爲什麼又想聊序列化了呢?因爲最近的工作用到了序列化相關的內容,其次,這幾年 Dubbo 也發生了翻天覆地的變化,其中 Dubbo 3.0 主推的 Tripple 協議,更是打着下一代 RPC 通信協議的旗號,有取代 Dubbo 協議的勢頭。而 Tripple 協議使用的便是 Protobuf 序列化方案。
另外,Dubbo 社區也專門搞了一個序列化壓測的項目:https://github.com/apache/dubbo-benchmark.git,本文也將圍繞這個項目,從性能維度展開對 Dubbo 支持的各個序列化框架的討論。
當我們聊序列化的時候,我們關注什麼?
最近幾年,各種新的高效序列化方式層出不窮,最典型的包括:
-
專門針對 Java 語言的:JDK 序列化、Kryo、FST -
跨語言的:Protostuff,ProtoBuf,Thrift,Avro,MsgPack 等等
爲什麼開源社區湧現了這麼多的序列化框架,Dubbo 也擴展了這麼多的序列化實現呢?主要還是爲了滿足不同的需求。
序列化框架的選擇主要有以下幾個方面:
-
跨語言。是否只能用於 java 間序列化 / 反序列化,是否跨語言,跨平臺。 -
性能。分爲空間開銷和時間開銷。序列化後的數據一般用於存儲或網絡傳輸,其大小是很重要的一個參數;解析的時間也影響了序列化協議的選擇,如今的系統都在追求極致的性能。 -
兼容性。系統升級不可避免,某一實體的屬性變更,會不會導致反序列化異常,也應該納入序列化協議的考量範圍。
和 CAP 理論有點類似,目前市面上很少有一款序列化框架能夠同時在三個方面做到突出,例如 Hessian2 在兼容性方面的表現十分優秀,性能也尚可,Dubbo 便使用了其作爲默認序列化實現,而性能方面它其實是不如 Kryo 和 FST 的,在跨語言這一層面,它表現的也遠不如 ProtoBuf,JSON。
其實反過來想想,要是有一個序列化方案既是跨語言的,又有超高的性能,又有很好的兼容性,那不早就成爲分佈式領域的標準了?其他框架早就被幹趴了。
大多數時候,我們是挑選自己關注的點,找到合適的框架,滿足我們的訴求,這才導致了序列化框架百花齊放的局面。
性能測試
很多序列化框架都宣稱自己是“高性能”的,光他們說不行呀,我還是比較篤信“benchmark everything”的箴言,這樣得出的結論,更能讓我對各個技術有自己的認知,避免人云亦云,避免被不是很權威的博文誤導。
怎麼做性能測試呢?例如像這樣?
long start = System.currentTimeMillis();
measure();
System.out.println(System.currentTimeMillis()-start);
貌似不太高大上,但又說不上有什麼問題。如果你這麼想,那我推薦你瞭解下 JMH 基準測試框架,我之前寫過的一篇文章《JAVA 拾遺 — JMH 與 8 個測試陷阱》推薦你先閱讀以下。
事實上,Dubbo 社區的貢獻者們早就搭建了一個比較完備的 Dubbo 序列化基礎測試工程:https://github.com/apache/dubbo-benchmark.git。
你只要具備基本的 JMH 和 Dubbo 的知識,就可以測試出在 Dubbo 場景下各個序列化框架的表現。
我這裏也準備了一份我測試的報告,供讀者們參考。如果大家準備自行測試,不建議在個人 windows/mac 上 benchmark,結論可能會不準確。我使用了兩臺阿里雲的 ECS 來進行測試,測試環境:Aliyun Linux,4c8g,啓動腳本:
java -server -Xmx2g -Xms2g -XX:MaxDirectMemorySize=1g -XX:+UseG1GC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:/home/admin/
爲啥選擇這個配置?我手上正好有兩臺這樣的資源,沒有特殊的設置~,況且從啓動腳本就可以看出來,壓測程序不會佔用太多資源,我都沒用滿。
測試工程介紹:
public interface UserService {
public boolean existUser(String email);
public boolean createUser(User user);
public User getUser(long id);
public Page<User> listUser(int pageNo);
}
一個 UserService
接口對業務應用中的 CRUD 操作。server 端以不同的序列化方案提供該服務,client 使用 JMH 進行多輪壓測。
@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 框架結構如上,詳細的實現,可以參考源碼。我這裏只選擇的一個評測指標 Throughput,即吞吐量。
省略一系列壓測過程,直接給出結果:
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
怎麼看到這個測試結果呢?createUser、existUser、getUser 這幾個方法測試下來,效果是參差不齊的,不能完全得出哪個框架性能最優,我的推測是因爲序列化的數據量比較簡單,量也不大,就是一個簡單的 User 對象;而 listUser 的實現是返回了一個較大的 List<User>
,可以發現,Kryo 和 Fst 序列化的確表現優秀,處於第一梯隊;令我意外的是 FastJson 竟然比 Hessian 還要優秀,位列第二梯隊;Tripple(背後是 ProtoBuf)和 Hessian2 位列第三梯隊。
當然,這樣的結論一定受限於 benchmark 的模型,測試用例中模擬的 CRUD 也不一定完全貼近業務場景,畢竟業務是複雜的。
怎麼樣,這樣的結果是不是也符合你的預期呢?
Dubbo 序列化二三事
最後,聊聊你可能知道也可能不知道的一些序列化知識。
hession-lite
Dubbo 使用的 Hessian2 其實並不是原生的 Hessian2 方案。注意看源碼中的依賴:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>hessian-lite</artifactId>
</dependency>
最早是阿里開源的 hessian-lite,後來隨着 Dubbo 貢獻給了 Apache,該項目也一併進入了 Apache,github 地址:https://github.com/apache/dubbo-hessian-lite。相比原生 Hessian2,Dubbo 獨立了一個倉庫致力於在 RPC 場景下,發揮出更高的性能以及滿足一些定製化的需求。
在 IO 線程中進行序列化
Dubbo 客戶端在高版本中默認是在業務線程中進行序列化的,而不是 IO 線程,你可以通過 decode.in.io 控制序列化與哪個線程綁定
<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 時,我發現 IO 線程中進行序列化,性能會更好,這可能和序列化本身是一個耗費 CPU 的操作,多線程無法加速反而會導致更多的競爭有關。
SerializationOptimizer
某些序列化實現,例如 Kryo 和 Fst 可以通過顯示註冊序列化的類來進行加速,如果想利用該特性來提升序列化性能,可以實現 org.apache.dubbo.common.serialize.support.SerializationOptimizer 接口。一個示例:
public class SerializationOptimizerImpl implements SerializationOptimizer {
@Override
public Collection<Class<?>> getSerializableClasses() {
return Arrays.asList(User.class, Page.class, UserService.class);
}
}
按照大多數人的習慣,可能會覺得這很麻煩,估計很少有用戶這麼用。注意客戶端和服務端需要同時開啓這一優化。
別忘了在 protocol 上配置指定這一優化器:
<dubbo:protocol name="dubbo" host="${server.host}" server="netty4" port="${server.port}" serialization="kryo" optimizer="org.apache.dubbo.benchmark.serialize.SerializationOptimizerImpl"/>
序列化方式由服務端指定
一般而言,Dubbo 框架使用的協議(默認是 dubbo)和序列化方式(默認是 hessian2)是由服務端指定的,不需要在消費端指定。因爲服務端是服務的提供者,擁有對服務的定義權,消費者在訂閱服務收到服務地址通知時,服務地址會包含序列化的實現方式,Dubbo 以這樣的契約方式從而實現 consumer 和 provider 的協同通信。
在大多數業務應用,應用可能既是服務 A 的提供者,同時也是服務 B 的消費者,所以建議在架構決策者層面協商固定出統一的協議,如果沒有特殊需求,保持默認值即可。
但如果應用僅僅作爲消費者,而又想指定序列化協議或者優化器(某些特殊場景),注意這時候配置 protolcol 是不生效的,因爲沒有服務提供者是不會觸發 protocol 的配置流程的。可以像下面這樣指定消費者的配置:
<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 中的轉義問題
總結
借 Dubbo 中各個序列化框架的實現,本文探討了選擇序列化框架時我們的關注點,並探討了各個序列化實現在 Dubbo 中具體的性能表現, 給出了詳細的測試報告,同時,也給出了一些序列化的小技巧,如果在 Dubbo 中修改默認的序列化行爲,你可能需要關注這些細節。
最後再借 Dubbo3 支持的 Tripple 協議來聊一下技術發展趨勢的問題。我們知道 json 能替代 xml 作爲衆多前後端開發者耳熟能詳的一個技術,並不是因爲其性能如何如何,而是在於其恰如其分的解決了大家的問題。一個技術能否流行,也是如此,一定在於其幫助用戶解決了痛點。至於解決了什麼問題,在各個歷史發展階段又是不同的,曾經,Dubbo2.x 憑藉着其豐富的擴展能力,強大的性能,活躍度高的社區等優勢幫助用戶解決一系列的難題,也獲得了非常多用戶的親來;現在,Dubbo3.x 提出的應用級服務發現、統一治理規則、Tripple 協議,也是在嘗試解決雲原生時代下的難題,如多語言,適配雲原生基礎設施等,追趕時代,幫助用戶。
- END -
「技術分享」某種程度上,是讓作者和讀者,不那麼孤獨的東西。歡迎關注我的微信公衆號:「Kirito的技術分享」
本文分享自微信公衆號 - Kirito的技術分享(cnkirito)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。