阿里開源!又一個序列化框架來了,性能炸裂!

Fury 是一個基於 JIT 動態編譯和零拷貝的多語言序列化框架,支持 Java/Python/Golang/JavaScript/C++ 等語言,提供全自動的對象多語言 / 跨語言序列化能力,和相比 JDK 最高 170 倍的性能。

代碼主倉庫的 GitHub 地址爲:

  • https://github.com/alipay/fury

官方網站:

  • https://furyio.org

背 景

序列化是系統通信的基礎組件,在大數據、AI 框架和雲原生等分佈式系統中廣泛使用。當對象需要跨進程、跨語言、跨節點傳輸、持久化、狀態讀寫、複製時,都需要進行序列化,其性能和易用性影響運行效率和開發效率

靜態序列化框架 protobuf/flatbuffer/thrift 由於不支持對象引用和多態、需要提前生成代碼等原因,無法作爲領域對象直接面嚮應用進行跨語言開發。而動態序列化框架 JDK 序列化 / Kryo/Fst/Hessian/Pickle 等,儘管提供了易用性和動態性,但不支持跨語言,且性能存在顯著不足,並不能滿足高吞吐、低延遲和大規模數據傳輸場景需求。

因此,我們開發了一個新的多語言序列化框架 Fury,並正式在 Github 開源。通過一套高度優化的序列化基礎原語,結合 JIT 動態編譯和 Zero-Copy 等技術,同時滿足了性能、功能和易用性的需求,實現了任意對象自動跨語言序列化,並提供極致的性能

Fury 簡介

Fury 是一個基於 JIT 動態編譯和零拷貝的多語言序列化框架,提供極致的性能和易用性:

序列化核心能力

儘管不同的場景對序列化有需求,但序列化的底層操作都是類似的。因此 Fury 定義和實現了一套序列化的基礎能力,基於這套能力能夠快速構建不同的多語言序列化協議,並通過編譯加速等優化具備高性能。同時針對一種協議在基礎能力上的性能優化,也能夠讓所有的序列化協議都受益

序列化原語

序列化涉及的常見操作主要包括:

Fury 針對這些操作在每種語言內部都做了大量的優化,結合 SIMD 指令和語言高級特性,將性能推到極致,從而方便不同協議使用。

零拷貝序列化

在大規模數據傳輸場景,一個對象圖內部往往有多個 binary buffer,而序列化框架在序列化過程當中會把這些數據寫入一箇中間 buffer,引入多次耗時內存拷貝。Fury 借鑑了 pickle5、ray 以及 arrow 的零拷貝設計,實現了一套 Out-Of-Band 序列化協議,能夠把一個對象圖當中的所有 binary buffer 直接抓取出來,避免掉這些 buffer 的中間拷貝,將序列化期間的內存拷貝開銷降低到 0。

下圖是 Fury 關閉引用支持時 Zero-Copy 的大致序列化過程。

目前 Fury 內置了以下類型的 Zero-Copy 支持:

用戶也可以基於 Fury 的接口擴展新的零拷貝類型。

JIT 動態編譯加速

對於要序列化的自定義類型對象,其中通常包含大量類型信息,Fury 利用這些類型信息在運行時直接生成高效的序列化代碼,將大量運行時的操作在動態編譯階段完成,從而增加方法內聯和代碼緩存,減少虛方法調用 / 條件分支 /Hash 查找 / 元數據寫入 / 內存讀寫等,最終大幅加速了序列化性能。

對於 Java 語言,Fury 實現了一套運行時代碼生成框架,定義了一套序列化邏輯的算子表達式 IR,在運行時基於對象類型的泛型信息進行類型推斷,然後構建一顆描述序列化代碼邏輯的表達式樹,根據表達式樹生成高效的 Java 代碼,再在運行時通過 Janino 編譯成字節碼,再加載到用戶的 ClassLoader 裏面或者 Fury 創建的 ClassLoader 裏面,最終通過 Java JIT 編譯成高效的彙編代碼。

由於 JVM JIT 會跳過大方法編譯和內聯,Fury 也實現了一套優化器,將大方法遞歸拆分成小方法,這樣就保證了 Fury 生成的所有代碼都可以被編譯和內聯,壓榨 JVM 的性能到極致。

同時 Fury 也支持異步多線程動態編譯,將不同序列化器的代碼生成任務提交到線程池執行,在編譯完成之前使用解釋模式執行,從而保證不會出現序列化毛刺,不需要提前預熱所有類型的序列化

Python 和 JavaScript 場景也是採用的類似代碼生成方式,這樣的生成方式開發門檻低,更容易排查問題。

由於序列化需要密切操作每種編程語言的對象,而編程語言並沒有暴露內存模型的低階 API,通過 Native 方法調用存在較大開銷,因此我們並不能通過 LLVM 構建一個統一的序列化器 JIT 框架,而是需要在每種語言內部結合語言特性實現特定的代碼生成框架以及序列化器構建邏輯。

靜態代碼生成

儘管 JIT 編譯能夠大幅提升序列化效率,並且在運行時能夠根據數據的統計分佈重新生成更優的序列化代碼,但 C++/Rust 等語言不支持反射,沒有虛擬機,也沒有提供內存模型的低階 API,因此我們無法針對這類語言通過 JIT 動態編譯生成序列化代碼。

對於此類場景,Fury 正在實現一套 AOT 靜態代碼生成框架,在編譯時根據對象的 schema 提前生成序列化代碼,然後使用生成的代碼進行自動序列化。對於 Rust,未來也會通過 Rust 的 macro 在編譯時生成代碼,提供更好的易用性。

緩存優化

在序列化自定義類型時,會把字段進行重排序,保證相同接口類型的字段依次序列化,增加緩存命中的概率,同時也促進了 CPU 指令緩存,實現了更加高效的序列化。對於基本類型字段將寫入順序按照字節字段大小降序排列,這樣如果開始地址是對齊的,隨後的讀寫都會發生在內存地址對齊的位置,CPU 執行起來更加高效。

多協議設計與實現

基於 Fury 提供的多語言序列化核心能力,我們在這之上構建了三種序列化協議,分別適用於不同的場景:

後續我們也會針對一些核心場景添加新的協議,用戶也可以基於 Fury 的序列化能力構建自己的協議。

Java 序列化

由於 Java 在大數據、雲原生、微服務和企業級應用的廣泛使用,對 Java 序列化的性能優化可以大幅降低系統延遲,提升吞吐率,降低服務器成本

因此 Fury 針對 Java 序列化進行了大量極致性能優化,我們的實現具備以下能力:

跨語言對象圖序列化

跨語言對象圖序列化主要用於對動態性和易用性有更高要求的場景。儘管 Protobuf/Flatbuffer 等框架提供了多語言序列化能力,但仍然存在一些不足:

結合以上幾點,Fury 實現了一套跨語言的對象圖序列化協議:

自動跨語言序列化示例:

行存序列化

對於高性能計算和大規模數據傳輸場景,數據序列化和傳輸往往是整個系統的性能瓶頸。如果用戶只需要讀取部分數據,或者根據對象某個字段進行過濾,反序列化整個數據將帶來額外開銷。因此 Fury 也提供了一套二進制數據結構,在二進制數據上直讀直寫,避開序列化

Apache arrow 是一個成熟的列存格式,支持二進制讀寫。但列存並不能滿足所有場景需求,在線鏈路和流式計算場景的數據天然就是行存結構,同時列式計算引擎內部在涉及到數據變更和 Hash/Join/Aggregation 操作時,也會使用到行存結構。

而行存並沒有一個統一標準實現,計算引擎如 Spark/Flink/Doris/Velox 等都定義了一套行存格式,這些格式不支持跨語言,且只能被自己引擎內部使用,無法用於其它框架。儘管 Flatbuffer 能夠支持按需反序列化,但需要靜態編譯 Schema IDL 和管理 offset,無法滿足複雜場景的動態性和易用性需求。

因此 Fury 在早期借鑑了 spark tungsten 和 apache arrow 格式,實現了一套可以隨機訪問的二進制行存結構,目前實現了 Java/Python/C++ 版本,實現了在二進制數據上面直讀直寫,避免掉了所有序列化開銷

下圖是 Fury Row Format 的二進制格式:

該格式密集存儲,數據對齊,緩存友好,讀寫更快。由於避免了反序列化,能夠減少 Java GC 壓力。同時降低 Python 開銷,同時由於 Python 的動態性,Fury 的數據結構實現了 _getattr__/getitem/slice/  和其它特殊方法,保證了行爲跟 python dataclass/list/object 的一致性,用戶沒有任何感知。

性能對比

這裏給出部分 Java 序列化性能數據,其中標題包含 compatible 的圖表是支持類型前後兼容下的性能數據,標題不包含 compatible 的圖表是不支持類型前後兼容下的性能數據。爲了公平起見,所有測試 Fury 關閉了零拷貝特性。

更多 benchmark 數據請參考 Fury Github 官方文檔:

  • https://github.com/alipay/fury/tree/main/docs/benchmarks

未來規劃

來源|公衆號 InfoQ、作者|楊朝坤(慕白)編輯|鄧豔琴

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/IWe4MX2SmwS44h2ayez_eg