限 时 特 惠: 本站每日持续稳定更新内部创业教程,一年会员只需98元,全站资源免费下载 点击查看详情
站 长 微 信: muyang-0410

作者 | 杨朝坤(慕白)

策划 | Tina

蚂蚁集团多语言序列化框架 Fury 于 2023 年 7 月份正式开源,2023 年 12 月 15 号我们将 Fury 捐赠给 软件基金会 ( )。Fury 以 14 个约束性投票 ( votes),6 个非约束性投票 (non- votes),无弃权和反对票leeco,全票通过投票决议的优秀表现,正式加入 孵化器,开启新的旅程。

从写下第一行代码、到开源的实践,再到成功捐赠给 基金会,Fury 始终坚持认真开源,也始终相信项目带来的实际生产价值。加入 孵化器后,Fury 将持续保持「开放、协作」的社区共建方式,相信在基金会的帮助下,Fury 能和更多的开发者一起释放更大的技术能量,为大家提供更好的序列化框架。

Fury 简介

Fury 是一个基于 JIT 动态编译和零拷贝技术的多语言序列化框架,支持 Java//C++/// Scala/Rust 等语言,提供最高 170 倍的性能和极致的易用性,可以大幅提升大数据计算、AI 应用和微服务等系统的性能,降低服务间调用延迟,节省成本并提高用户体验;同时提供全新的多语言编程范式,解决现有的多语言序列化动态性不足的问题,显著降低多语言系统的研发成本,帮助业务快速迭代。

Fury 发展历史

Fury 生态

在蚂蚁和阿里集团内部,Fury 被诸多系统用于提升性能:

自 23 年七月中旬 Fury 在 开源以来,Fury 在外部收获了诸多用户。开源五个月,Fury 累计收获 star 2.3K,发布版本 10 次,参与代码贡献人数 29 人。

Fury 被各行各业的企业 / 组织广泛用于解决系统间通信序列化问题,被大数据系统如 Flink、 等,微服务系统 Dubbo/Sofa 等,以及各类应用系统等广泛采用,帮助诸多系统提升了数倍吞吐,降低了大量延迟。

在流计算系统 Flink 场景中,Fury 比 Flink 自带的 快 1 倍以上。在湖仓系统 场景,Fury 帮助 的 CDC 同步任务端到端提升了一倍的吞吐量。在搜索推荐场景,唯品会从 切换成 Fury 端到端延迟 P99 减少了 30ms 以上。

未来我们计划跟 Spark/Flink//Hbase 等系统进行深度合作,为大数据生态提供更好的性能。

Fury 特性

极致性能:最高 170 倍加速

Fury 通过在运行时动态生成序列化代码,增加方法内联、代码缓存和消除死代码,减少虚方法调用 / 条件分支 /Hash 查找 / 元数据写入 / 内存读写等,可以提供相比别的序列化框架最高 170 倍的性能:

在处理大对象序列化时,Fury 比 Java 领域最流行的序列化框架 Kryo 快 80 倍以上:

更多 数据可以参考 Fury 官方文档:

JDK 17 类型支持

JDK17 引入了 数据类型,该类型自动支持了构造器 //// 方法,极大方便了应用开发,减少了 Java 样板代码。

但该类型有其复杂之处,给序列化引入了额外复杂性。JDK 屏蔽了 底层内存结构实现细节,无法像普通对象一样通过 / 反射来获取修改每个字段的值。每个字段值只能通过 类型的 方法来获取,同时每个字段值只能通过构造函数来进行 set。而构造函数对输入参数有特殊的顺序要求,需要每个参数跟定义 class 声明字段时保持相同的顺序。

因此该类型的序列化相当复杂,Fury 在序列化时,针对该类型做了大量优化。如果对象的 方法是 方法,则会在生成的代码里面直接调用该方法获取字段值,如果是非 方法,则会通过 动态生成字节码来零成本调用该方法获取字段值。对于反序列化,Fury 会先把所有字段序列化出来,放在当前上下文,然后对字段按照 类型构造器声明的顺序进行重排依次传入进行调用,从而完成整个反序列化的过程。通过动态代码生成,Fury 的 类型序列化相比 kryo 最高有十倍以上的性能提升:

Image AOT 支持

是 发布的支持 AOT 编译的 JDK,可以在 构建阶段将所有的 Java 字节码编译成 code,该方案移除了 JIT 编译器,在 编译阶段移除了无关代码,更加轻量级、更加安全,启动时即具备巅峰性能,是 Java 在云原生时代的利器。

Fury 在 0.4.0 版本也全面高效支持了 。通过与 的深度集成,Fury 会在 Image 的构建阶段调用 Fury JIT 框架完成完成所有序列化相关的字节码生成,从而在运行时移除了 JIT 的需要,保证了跟 兼容的同时,也具备极致的性能。

相比于 自带的 JDK 序列化,以及其它第三方序列化框架,Fury 不需要声明 meta json ,只需要在 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 f3,
    Map 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 初始化器对应的类型放到 -image. 里面,配置为构建时初始化即可:

Args = --initialize-at-build-time=org.apache.fury.graalvm.Example

复制代码

Fury 的实现,比 自带的 JDK 序列化快 50 倍以上,由于 Kryo 等框架无法直接运行在 上面,需要不少的改造工作量,但考虑到实现层面并没有变化,性能对比结果跟非 模式应该是类似的。

100% 的 JDK 序列化兼容性

为了支持更加广泛的场景,让更多应用平滑迁移,Fury 实现了对 JDK 序列化的 100% API 级别的兼容性,用户只需要将调用 JDK 进行序列化的地方换成 Fury,无需修改任何额外的代码,便可高效完成所有的序列化。JDK 提供了 //// / 等 API 来让用户自定义序列化,Fury 对这些 API 都进行了完整的兼容。

截止目前,Fury 是业界唯一一个完全兼容 JDK 序列化 API 的框架,Fury 甚至支持对 JDK8 序列化自身都序列化失败的对象进行序列化:。该 bug 在 JDK8 从未被修复,在 Spark 里面也出现过:。在蚂蚁内部有一些场景无法绕过,最终通过 Fury 才得以解决。

另外 Fury 也支持 JDK8~21 所有版本,同时支持跨 JDK 版本的兼容性。JDK8 序列化的对象,可以被 JDK21 正确反序列化,而不会有二进制兼容性问题。我们甚至支持将 JDK21 等高版本下 Fury 序列化的数据,在 JDK8 等低版本下正确反序列化。像 这种类型,在 J DK8/11 等版本如果有对应的 POJO 类型定义,则能够被正确反序列化成对应的 POJO 类型。通过该特性,用户就可以在服务端和客户端独立升级 JDK 版本,而不用同时升级,从而降低风险。而且在复杂的微服务场景下,每个服务被上下游几十个服务依赖,同时升级甚至是无法做到的。

高度兼容的 Scala 序列化

众所周知,Scala 编程语言提供了很多语法糖帮助提高编程的生产力leeco,但 Scala 引入了很多额外的抽象和字节码生成,无法直接使用已有的 Java 序列化框架进行高效序列化,导致业界并没有特别好的 Scala 序列化框架,目前用的比较多的是 开源的 Chill。Chill 能够处理大部分 Scala 类型的序列化,但存在性能和兼容性不足的问题。而 JDK 自带的序列化虽然有完整的兼容性,但是存在性能严重不足,序列化结果大幅膨胀的问题。

Fury 从 0.3.0 版本开始支持了 Scala 序列化,目前 Fury 支持序列化任意 Scala 对象,包括 case//tuple///enum//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 引擎深度优化的 实现

的灵活性使得它的性能很容易受编码方式以及协议的影响,在协议上 Fury 引入了 字符串,跨语言的类型兼容协议等,使得 v8 在运行过程中能充分内联,消除 。另外我们实验性的引入高性能的 hps 模块,通过 v8 fast-call 的特性让字符串的编码检测和写入性能得到的极大提升。

目前, 的实现相对于 JSON 性能提升 3~5 倍,相对于 在 2~3 倍。

高效的 序列化

由于其动态性且不支持 JIT,性能一直存在不足,序列化也不例外。Fury 通过在协议层面引入了 /UTF16 字符串,基于元数据共享的类型兼容协议,在协议层面大幅提升了序列化性能。同时我们将大部分耗时的序列化过程采用 /C++ 来实现,然后在上层再通过动态和加载生成 序列化代码来串联整个序列化过程,并引入 来进行序列化器分发和引用解析,在实现层面进一步提升了序列化性能。

另外 Fury 也实现了零拷贝按需读取的行存协议,将 Fury C++ 行存协议包装为 实现,可以在不反序列化解析数据的试试,读取嵌套数据结果的任意字段值,结合 自身的动态性,在不影响数据模型访问接口的同时,大幅降低了序列化的开销。

支持 Go 循环引用和多态

Fury 也提供了 序列化支持,Fury Go 支持基本类型、字符串、slice、map、、、指针,以及这些类型的任意组合的嵌套类型。

与其它序列化框架不同的是,Fury Go 支持共享引用和循环引用:

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//C++/// Scala/Rust 语言的自动跨语言序列化,下面是一个简单的示例:

对于 Java/Scala//,Fury 都是通过运行时代码生成来进行序列化。

对于 ,Fury 目前是通过反射实现的序列化,后续会支持静态代码生成来加速序列化。

对于 C++,Fury 实现了基于模板的自动序列化,在编译阶段 Fury 通过宏和模板实现了编译时反射,获取了结构体的所有字段类型及其访问器指针,同时通过模板生成了序列化代码。

对于 Rust,Fury 基于 Rust 宏在 Rust 编译时自动生成代码,无需 IDL 定义,即可使用对象序列化以及行存,由于 Rust 不支持引用,因此其它语言序列化时需要关闭引用。

零拷贝序列化协议

由于内存拷贝也存在开销,因此 Fury 也在多个维度支持了零拷贝,来进一步降低序列化的开销提升效率。

零拷贝

在大规模数据传输场景,一个对象图内部往往有多个 ,而序列化框架在序列化时会把这些数据写入一个中间 ,引入多次耗时内存拷贝。Fury 实现了一套 Out-Of-Band 序列化协议,把对象图中的所有 直接提取出来,避免掉这些 的中间拷贝,将序列化期间的大内存拷贝开销降低到 0。

堆外内存读写

对于 Java 等管理内存的语言,Fury 也支持直接读写堆外内存,而不需要先将内存读写到堆内存,这样可以减少一次内存拷贝开销,提升性能,同时也减少了 Java GC 的压力。

无需反序列化的行存协议

对于复杂对象,如果用户只需要读取部分数据,或者根据对象某个字段进行过滤,反序列化整个数据将带来额外开销。因此 Fury 也提供了一套二进制数据结构,在二进制数据上直读直写,避开序列化。

该格式密集存储,数据对齐,缓存友好,读写更快。由于避免了反序列化,能够减少 Java GC 压力。同时降低 开销,同时由于 的动态性,Fury 的数据结构实现了 //slice/ 和其它特殊方法,保证了行为跟 /list/ 的一致性,用户没有任何感知。

@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 名贡献者,感谢他们的贡献:,,,,,,,mof-dev-3,,hieu-ht,,,,,,,,,,,,,,leeco-cloud,,,

核心开发者简介:

致谢

Fury 是一个从个人项目逐渐成长起来的技术框架,能够发展到今天,顺利加入 孵化器,每一步都离不开大家的支持与鼓励,在此对大家表示衷心的感谢:

作者简介

杨朝坤,蚂蚁集团技术专家,花名慕白,Fury 框架作者。2018 年加入蚂蚁集团,先后从事流计算框架、在线学习框架、科学计算框架和 Ray 等分布式计算框架开发,对批计算、流计算、 计算、高性能计算、AI 框架、张量编译等有深入的理解。

原文链接:

限 时 特 惠: 本站每日持续稳定更新内部创业教程,一年会员只需98元,全站资源免费下载 点击查看详情
站 长 微 信: muyang-0410