Dubbo 支持的幾個主流序列化框架評測


前言

今天要聊的技術是序列化,這不是我第一次寫序列化相關的文章了,今天動筆之前,我還特地去博客翻了下我博客早期的一篇序列化文章(如下圖),竟然都過去 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 也擴展了這麼多的序列化實現呢?主要還是爲了滿足不同的需求。

序列化框架的選擇主要有以下幾個方面:

  1. 跨語言。是否只能用於 java 間序列化 / 反序列化,是否跨語言,跨平臺。
  2. 性能。分爲空間開銷和時間開銷。序列化後的數據一般用於存儲或網絡傳輸,其大小是很重要的一個參數;解析的時間也影響了序列化協議的選擇,如今的系統都在追求極致的性能。
  3. 兼容性。系統升級不可避免,某一實體的屬性變更,會不會導致反序列化異常,也應該納入序列化協議的考量範圍。

和 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.classPage.classUserService.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&amp;serialization=kryo">

    <dubbo:parameter key="decode.in.io" value="true" />
</dubbo:reference>

&amp; 代表 &,避免 xml 中的轉義問題

總結

借 Dubbo 中各個序列化框架的實現,本文探討了選擇序列化框架時我們的關注點,並探討了各個序列化實現在 Dubbo 中具體的性能表現, 給出了詳細的測試報告,同時,也給出了一些序列化的小技巧,如果在 Dubbo 中修改默認的序列化行爲,你可能需要關注這些細節。

最後再借 Dubbo3 支持的 Tripple 協議來聊一下技術發展趨勢的問題。我們知道 json 能替代 xml 作爲衆多前後端開發者耳熟能詳的一個技術,並不是因爲其性能如何如何,而是在於其恰如其分的解決了大家的問題。一個技術能否流行,也是如此,一定在於其幫助用戶解決了痛點。至於解決了什麼問題,在各個歷史發展階段又是不同的,曾經,Dubbo2.x 憑藉着其豐富的擴展能力,強大的性能,活躍度高的社區等優勢幫助用戶解決一系列的難題,也獲得了非常多用戶的親來;現在,Dubbo3.x 提出的應用級服務發現、統一治理規則、Tripple 協議,也是在嘗試解決雲原生時代下的難題,如多語言,適配雲原生基礎設施等,追趕時代,幫助用戶。

END -

「技術分享」某種程度上,是讓作者和讀者,不那麼孤獨的東西。歡迎關注我的微信公衆號:「Kirito的技術分享」


本文分享自微信公衆號 - Kirito的技術分享(cnkirito)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章