蚂蚁集团多语言序列化框架 Fury 于2023年7月份正式开源,2023年12月15号我们将 Fury 捐赠给 Apache 软件基金会(Apache Software Foundation)。Fury 以14个约束性投票(binding votes),6个非约束性投票(non-binding votes),无弃权和反对票,全票通过投票决议的优秀表现,正式加入 Apache 孵化器,开启新的旅程。
从写下第一行代码、到开源的实践,再到成功捐赠给 Apache 基金会,Fury 始终坚持认真开源,也始终相信项目带来的实际生产价值。加入 Apache 孵化器后,Fury 将持续保持「开放、协作」的社区共建方式,相信在基金会的帮助下,Fury 能和更多的开发者一起释放更大的技术能量,为大家提供更好的序列化框架。
Fury简介
Fury是一个基于JIT动态编译和零拷贝技术的多语言序列化框架,支持Java/Python/C++/JavaScript/Golang/ Scala/Rust等语言,提供最高170倍的性能和极致的易用性,可以大幅提升大数据计算、AI应用和微服务等系统的性能,降低服务间调用延迟,节省成本并提高用户体验;同时提供全新的多语言编程范式,解决现有的多语言序列化动态性不足的问题,显著降低多语言系统的研发成本,帮助业务快速迭代。
Fury发展历史
- 2019年,Fury 作者杨朝坤写下第一版代码,用于蚂蚁集团内部在线机器学习引擎 Mobius 和实时计算引擎 Realtime 的 Java/Python 进程间序列化。
- 2020年,Fury 在蚂蚁内部基于分布式计算引擎Ray的生态大规模落地。
- 2021年,我们申请将 Fury 开源,考虑到当时 Fury 只有作者杨朝坤一个人在开发,出于项目长期可维护性角度先在内部开源构建社区。
- 2022年7月,Fury 在蚂蚁内部开源,发布了 Java/Python/C++/Golang 实现,并在阿里和蚂蚁集团开始宣传,引起广泛关注,获得大量用户和开发者,被 Ray/Flink/Realtime 等分布式计算引擎、Mars 分布式科学计算框架、多媒体计算引擎、Lindorm 云原生多模数据库、Arec推荐系统、特征索引构建平台、SOFA/HSF 等中间件广泛使用。Fury核心开发者王为蓬也是在这期间加入了 Fury,负责 Fury NodeJS 实现。同期我们也对外发布多篇技术文章,收到了诸多外部用户关于开源和合作的邮件。
- 2023年7月,时值 Fury 内部开源一周年,Fury 正式对外开源。经过内部大量场景的打磨,项目的质量和价值得到了充分保证,开源十天 Github Star 突破1K,并连续两天登上 Github Trending Java排行榜第一,同时在后续也登上Hacknews 和 Reddit 技术头条。
- 2023年12月,为了更好的以社区化的方式发展,践行开放协作之道,Fury 正式加入 Apache 软件基金会。
Fury生态
在蚂蚁和阿里集团内部,Fury 被诸多系统用于提升性能:
- Ant Ray:Ray 是加州伯克利大学开源的高性能的分布式计算引擎,是大模型时代的底座,被 OpenAI 用于处理大规模深度学习负载。蚂蚁自2017年就开始跟 Ray 深度合作,目前在蚂蚁内部有百万核心CPU都在运行Ray。这些系统中的大部分包括 Serving、在线机器学习、离在线推理、多媒体计算、运筹计算等都使用 Fury 进行序列化。
- Ant Mars:Mars 是一个分布式Python科学计算框架,提供分布式的 Pandas 和 Numpy 实现,Ant Mars 基于 Fury 实现了2.5x的调度效率提升,4x的运行效率提升
- Aliyun Lindorm:Lindorm 是一个云原生多模态数据库,其使用 Fury 替代 Protobuf,用于客户端和服务端之间的序列化。
- 实时索引构建平台:索引构建平台使用 Flink 进行实时索引的构建,使用 Fury 取得了端到端一倍性能的提升
- Arec搜索推荐平台:Arec搜推平台使用 Fury 在进程间传输大量特征数据,在搜推场景中,经常需要对大量用户和 Item 进行召回,一次召回需要传输上千个高密度 Item,这些数据传输往往有大量开销,通过 Fury 可以数倍减少序列化开销提升吞吐。
- Sofa中间件:Sofa 使用Fury 为高性能计算场景的服务提升执行效率
自23年七月中旬 Fury 在 GitHub 开源以来,Fury在外部收获了诸多用户。开源五个月,Fury累计收获 star 2.3K,发布版本10次,参与代码贡献人数29人。
Fury被各行各业的企业/组织广泛用于解决系统间通信序列化问题,被大数据系统如Flink、LakeSoul等,微服务系统Dubbo/Sofa等,以及各类应用系统等广泛采用,帮助诸多系统提升了数倍吞吐,降低了大量延迟。
在流计算系统Flink场景中,Fury 比 Flink 自带的 POJOSerializer 快1倍以上。在湖仓系统LakeSoul场景,Fury 帮助 LakeSoul 的 CDC 同步任务端到端提升了一倍的吞吐量。在搜索推荐场景,唯品会从 Protobuf 切换成 Fury 端到端延迟P99减少了30ms以上。
未来我们计划跟 Spark/Flink/RockedMQ/Hbase 等系统进行深度合作,为大数据生态提供更好的性能。
Fury特性
极致性能:最高170倍加速
Fury 通过在运行时动态生成序列化代码,增加方法内联、代码缓存和消除死代码,减少虚方法调用/条件分支 /Hash 查找/元数据写入/内存读写等,可以提供相比别的序列化框架最高 170 倍的性能:
在处理大对象序列化时,Fury比Java领域最流行的序列化框架Kryo快80倍以上:
更多Benchmark数据可以参考 Fury 官方文档:https://github.com/apache/incubator-fury/tree/main/docs/benchmarks
JDK 17 Record类型支持
JDK17 引入了record 数据类型,该类型自动支持了构造器/getter/hashCode/equals/toString方法,极大方便了应用开发,减少了Java样板代码。
但该类型有其复杂之处,给序列化引入了额外复杂性。JDK屏蔽了record底层内存结构实现细节,无法像普通对象一样通过Unsafe/反射来获取修改每个字段的值。每个字段值只能通过record类型的getter方法来获取,同时每个字段值只能通过构造函数来进行set。而构造函数对输入参数有特殊的顺序要求,需要每个参数跟定义class声明字段时保持相同的顺序。
因此该类型的序列化相当复杂,Fury在序列化时,针对该类型做了大量优化。如果对象的getter方法是public方法,则会在生成的代码里面直接调用该方法获取字段值,如果是非public方法,则会通过MethodHandle动态生成字节码来零成本调用该方法获取字段值。对于反序列化,Fury会先把所有字段序列化出来,放在当前上下文,然后对字段按照record类型构造器声明的顺序进行重排依次传入进行调用,从而完成整个反序列化的过程。通过动态代码生成,Fury的record类型序列化相比kryo最高有十倍以上的性能提升:
GraalVM Native Image AOT支持
GraalVM 是 Oracle 发布的支持 Native AOT编译的JDK,可以在 binary 构建阶段将所有的 Java 字节码编译成native code,该方案移除了JIT编译器,在 binary 编译阶段移除了无关代码,更加轻量级、更加安全,启动时即具备巅峰性能,是 Java 在云原生时代的利器。
Fury 在0.4.0版本也全面高效支持了 GraalVM。通过与 GraalVM 的深度集成,Fury 会在GraalVM Native Image 的构建阶段调用 Fury JIT框架完成完成所有序列化相关的字节码生成,从而在运行时移除了JIT的需要,保证了跟GraalVM兼容的同时,也具备极致的性能。
相比于 GraalVM 自带的JDK序列化,以及其它第三方序列化框架,Fury 不需要声明graalvm reflect meta json config,只需要在 Fury 初始化阶段注册多个待序列化的类型即可,可以大幅简化使用,提升研发效率。
import org.apache.fury.Fury;
import org.apache.fury.util.Preconditions;
import java.util.List;
import java.util.Map;
public class Example {
public record Record (
int f1,
String f2,
List<String> f3,
Map<String, Long> f4) {
}
static Fury fury;
static {
fury = Fury.builder().build();
// register and generate serializer code.
fury.register(Record.class, true);
}
public static void main(String[] args) {
Record record = new Record(10, "abc", List.of("str1", "str2"), Map.of("k1", 10L, "k2", 20L));
System.out.println(record);
byte[] bytes = fury.serialize(record);
Object o = fury.deserialize(bytes);
System.out.println(o);
Preconditions.checkArgument(record.equals(o));
}
}
然后将Fury初始化器对应的类型放到 native-image.properties 里面,配置为构建时初始化即可:
Args = --initialize-at-build-time=org.apache.fury.graalvm.Example
Fury 的实现,比 GraalVM 自带的 JDK 序列化快50倍以上,由于 Kryo 等框架无法直接运行在 GraalVM 上面,需要不少的改造工作量,但考虑到实现层面并没有变化,性能对比结果跟非 GraalVM 模式应该是类似的。
100%的 JDK 序列化兼容性
为了支持更加广泛的场景,让更多应用平滑迁移,Fury实现了对 JDK 序列化的 100% API 级别的兼容性,用户只需要将调用 JDK 进行序列化的地方换成 Fury,无需修改任何额外的代码,便可高效完成所有的序列化。JDK提供了 writeObject/readObject/writeReplace/readResolve/ readObjectNoData/ Externalizable等 API 来让用户自定义序列化,Fury对这些 API 都进行了完整的兼容。
截止目前,Fury 是业界唯一一个完全兼容 JDK 序列化 API 的框架,Fury 甚至支持对 JDK8 序列化自身都序列化失败的对象进行序列化:Loading...。该bug 在 JDK8 从未被修复,在 Apache Spark 里面也出现过:[SPARK-19938] java.lang.ClassCastException: cannot assign instance of scala.collection.immutable.List$SerializationProxy to field - ASF JIRA在蚂蚁内部有一些场景无法绕过,最终通过 Fury 才得以解决。
另外 Fury 也支持 JDK8~21所有版本,同时支持跨 JDK 版本的兼容性。JDK8 序列化的对象,可以被 JDK21 正确反序列化,而不会有二进制兼容性问题。我们甚至支持将 JDK21 等高版本下 Fury 序列化的数据,在 JDK8 等低版本下正确反序列化。像 record 这种类型,在J DK8/11 等版本如果有对应的 POJO 类型定义,则能够被正确反序列化成对应的 POJO 类型。通过该特性,用户就可以在服务端和客户端独立升级JDK版本,而不用同时升级,从而降低风险。而且在复杂的微服务场景下,每个服务被上下游几十个服务依赖,同时升级甚至是无法做到的。
高度兼容的Scala序列化
众所周知,Scala编程语言提供了很多语法糖帮助提高编程的生产力,但 Scala 引入了很多额外的抽象和字节码生成,无法直接使用已有的Java序列化框架进行高效序列化,导致业界并没有特别好的 Scala 序列化框架,目前用的比较多的是 Twitter 开源的 Chill。Chill能够处理大部分Scala类型的序列化,但存在性能和兼容性不足的问题。而 JDK 自带的序列化虽然有完整的兼容性,但是存在性能严重不足,序列化结果大幅膨胀的问题。
Fury 从0.3.0版本开始支持了Scala序列化,目前 Fury 支持序列化任意 Scala 对象,包括case/object/tuple/string/collection/enum/option/basic等类型。
val fury = Fury.builder()
.withScalaOptimizationEnabled(true)
.requireClassRegistration(true)
.withRefTracking(true)
.build()
case class Person(github: String, age: Int, id: Long)
val p = Person("https://github.com/chaokunyang", 18, 1)
println(fury.deserialize(fury.serialize(p)))
class Foo(f1: Int, f2: String) {
override def toString: String = s"Foo($f1, $f2)"
}
println(fury.deserialize(fury.serialize(Foo(1, "chaokunyang"))))
object singleton {}
val o1 = fury.deserialize(fury.serialize(singleton))
println(o1 == singleton)
val seq = Seq(1,2)
val list = List("a", "b")
val map = Map("a" -> 1, "b" -> 2)
println(fury.deserialize(fury.serialize(seq)))
println(fury.deserialize(fury.serialize(list)))
println(fury.deserialize(fury.serialize(map)))
enum Color { case Red, Green, Blue }
println(fury.deserialize(fury.serialize(Color.Green)))
V8引擎深度优化的NodeJS实现
JavaScript的灵活性使得它的性能很容易受编码方式以及协议的影响,在协议上 Fury 引入了 Latin1 字符串,跨语言的类型兼容协议等,使得v8在运行过程中能充分内联,消除polymorphic。另外我们实验性的引入高性能的hps模块,通过v8 fast-call的特性让字符串的编码检测和写入性能得到的极大提升。
目前,JavaScript的实现相对于JSON性能提升3~5倍,相对于protobuf在2~3倍。
高效的Python序列化
Python由于其动态性且不支持JIT,性能一直存在不足,序列化也不例外。Fury 通过在协议层面引入了 Latin1/UTF16字符串,基于元数据共享的类型兼容协议,在协议层面大幅提升了序列化性能。同时我们将大部分耗时的序列化过程采用Cython/C++来实现,然后在上层再通过动态和加载生成 Python 序列化代码来串联整个序列化过程,并引入 Swisstable 来进行序列化器分发和引用解析,在实现层面进一步提升了序列化性能。
另外 Fury 也实现了零拷贝按需读取的行存协议,将 Fury C++ 行存协议包装为 Python 实现,可以在不反序列化解析数据的试试,读取嵌套数据结果的任意字段值,结合Python自身的动态性,在不影响数据模型访问接口的同时,大幅降低了序列化的开销。
支持Go循环引用和多态
Fury也提供了golang序列化支持,Fury Go支持基本类型、字符串、slice、map、struct、interface、指针,以及这些类型的任意组合的嵌套类型。
与其它序列化框架不同的是,Fury Go支持共享引用和循环引用:
- 对同一个slice/map的多个引用,在Fury里面只会被序列化一次,而使用msgpack等框架则会被重复序列化
- 对同一个struct的多个指针,在Fury里面只会被序列化一次
- struct之间可以通过指针相互引用,Fury也可以序列化这样的对象图
- 多态序列化支持:可以声明和序列化一个接口类型,反序列化的结果仍然是相同的类型
- No IDL:Fury Go不需要用户定义IDL,直接传入golang原生对象即可进行序列化,没有额外学习成本,更加易用
package main
import furygo "github.com/alipay/fury/go/fury"
import "fmt"
func main() {
type SomeClass struct {
F1 *SomeClass
F2 map[string]string
F3 map[string]string
}
fury := furygo.NewFury(true)
if err := fury.RegisterTagType("example.SomeClass", SomeClass{}); err != nil {
panic(err)
}
value := &SomeClass{F2: map[string]string{"k1": "v1", "k2": "v2"}}
value.F3 = value.F2
value.F1 = value
bytes, err := fury.Marshal(value)
if err != nil {
}
var newValue interface{}
// bytes can be data serialized by other languages.
if err := fury.Unmarshal(bytes, &newValue); err != nil {
panic(err)
}
fmt.Println(newValue)
}
自动化的跨语言序列化
Fury目前支持了Java/Python/C++/JavaScript/Golang/ Scala/Rust语言的自动跨语言序列化,下面是一个简单的示例:
对于Java/Scala/Python/JavaScript,Fury都是通过运行时代码生成来进行序列化。
对于Golang,Fury目前是通过反射实现的序列化,后续会支持静态代码生成来加速序列化。
对于C++,Fury实现了基于模板的自动序列化,在编译阶段Fury通过宏和模板实现了编译时反射,获取了结构体的所有字段类型及其访问器指针,同时通过模板生成了序列化代码。
对于Rust,Fury基于Rust 宏在Rust编译时自动生成代码,无需IDL定义,即可使用对象序列化以及行存,由于Rust不支持引用,因此其它语言序列化时需要关闭引用。
零拷贝序列化协议
由于内存拷贝也存在开销,因此 Fury 也在多个维度支持了零拷贝,来进一步降低序列化的开销提升效率。
Buffer零拷贝
在大规模数据传输场景,一个对象图内部往往有多个 binary buffer,而序列化框架在序列化时会把这些数据写入一个中间 buffer,引入多次耗时内存拷贝。Fury 实现了一套 Out-Of-Band 序列化协议,把对象图中的所有 buffer 直接提取出来,避免掉这些 buffer 的中间拷贝,将序列化期间的大内存拷贝开销降低到 0。
堆外内存读写
对于Java等管理内存的语言,Fury也支持直接读写堆外内存,而不需要先将内存读写到堆内存,这样可以减少一次内存拷贝开销,提升性能,同时也减少了Java GC的压力。
无需反序列化的行存协议
对于复杂对象,如果用户只需要读取部分数据,或者根据对象某个字段进行过滤,反序列化整个数据将带来额外开销。因此 Fury 也提供了一套二进制数据结构,在二进制数据上直读直写,避开序列化。
该格式密集存储,数据对齐,缓存友好,读写更快。由于避免了反序列化,能够减少 Java GC 压力。同时降低 Python 开销,同时由于 Python 的动态性,Fury 的数据结构实现了 _getattr__/getitem/slice/ 和其它特殊方法,保证了行为跟 python dataclass/list/object 的一致性,用户没有任何感知。
@dataclass
class Bar:
f1: str
f2: List[pa.int64]
@dataclass
class Foo:
f1: pa.int32
f2: List[pa.int32]
f3: Dict[str, pa.int32]
f4: List[Bar]
encoder = pyfury.encoder(Foo)
foo = Foo(f1=10, f2=list(range(1000_000)),
f3={f"k{i}": i for i in range(1000_000)},
f4=[Bar(f1=f"s{i}", f2=list(range(10))) for i in range(1000_000)])
binary: bytes = encoder.to_row(foo).to_bytes()
foo_row = pyfury.RowData(encoder.schema, binary)
# 直接读取这些字段值,不反序列化整个数据
print(foo_row.f2[100000], foo_row.f4[100000].f1, foo_row.f4[200000].f2[5])
Fury开发者
开源五个月,Fury 迎来了29名贡献者,感谢他们的贡献:chaokunyang,theweipeng,PragmaTwice,caicancai,tisonkun,pjfanning,bytemain,mof-dev-3,nandakumar131,hieu-ht,knutwannheden,voldyman,HimanshuMahto,pandalee99,Spyrosigma,s31k31,Shivam250702,Smoothieewastaken,iamahens,vidhijain27,vesense,ayushrakesh,farmerworking,leeco-cloud,xiguashu,rainsonGain,springrain
核心开发者简介:
- chaokunyang:Fury Java/Python/C++/Golang/Scala 核心开发者
- theweipeng:Fury NodeJS/Rust 核心开发者
- PragmaTwice:Apache KVRocks PMC member,Fury C++ 核心开发者
- pjfanning:Apache member,Pekko和 Jackson 核心开发者
- bytemain:opensumi核心开发者,Fury NodeJS 核心开发者
致谢
Fury 是一个从个人项目逐渐成长起来的技术框架,能够发展到今天,顺利加入 Apache 孵化器,每一步都离不开大家的支持与鼓励,在此对大家表示衷心的感谢:
- 社区用户和开发者对 Fury 的支持
- 王为蓬 和 @PragmaTwice 在社区构建,协作开发,以及捐赠过程的共同努力
- Champion(领路人) tison 的指导和帮助:感谢 tison 带领 Fury 加入 Apache,并在 Fury 开源早期给予开源社群建设的诸多指导。
- Mentors(导师) 的支持和协助:tison、PJ Fanning、李钰(绝顶)、王鑫(多佛)、Enrico Olivelli。
加入我们
欢迎对 Fury 感兴趣的各位用户和开发者加入 Fury 开源社区,欢迎任何形式的参与,包括但不限于提问、代码贡献、技术讨论等。非常期待收到大家的想法和反馈,一起参与到项目的建设中来,推动项目向前发展。可以通过以下方式关注和了解社区的最近动态,期待您的加入:
- Fury Github仓库地址:GitHub - apache/incubator-fury: A blazing fast multi-language serialization framework powered by JIT and zero-copy.
- 官方网站:https://fury.apache.org
- Fury 邮件列表地址:dev@fury.apache.org, 可通过向dev-subscribe@fury.apache.org发送邮件订阅
- Fury 微信公众号:apache-fury
作者简介
杨朝坤,蚂蚁集团技术专家,花名慕白,Fury 框架作者。2018 年加入蚂蚁集团,先后从事流计算框架、在线学习框架、科学计算框架和 Ray 等分布式计算框架开发,对批计算、流计算、Tensor 计算、高性能计算、AI 框架、张量编译等有深入的理解。