蚂蚁集团多语言序列化框架 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 初始化阶段注册多个待序列化的类型即可,可以大幅简化使用,提升研发效率。
然后将 Fury 初始化器对应的类型放到 native-image.properties 里面,配置为构建时初始化即可:
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 序列化自身都序列化失败的对象进行序列化:https://bugs.openjdk.org/browse/JDK-8024931。该 bug 在 JDK8 从未被修复,在 Apache Spark 里面也出现过:https://issues.apache.org/jira/browse/SPARK-19938。在蚂蚁内部有一些场景无法绕过,最终通过 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 等类型。
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 原生对象即可进行序列化,没有额外学习成本,更加易用
自动化的跨语言序列化
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 的一致性,用户没有任何感知。
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 框架作者。2018 年加入蚂蚁集团,先后从事流计算框架、在线学习框架、科学计算框架和 Ray 等分布式计算框架开发,对批计算、流计算、Tensor 计算、高性能计算、AI 框架、张量编译等有深入的理解。
评论