likes
comments
collection
share

Fury序列化快速入门Apache Fury(孵化中)一个基于动态代码生成和零拷贝技术的多语言序列化框架,实现了无需ID

作者站长头像
站长
· 阅读数 26

开始

官网地址:fury.apache.org/

安装

官方的 Apache Fury 发行版以源代码形式提供。 要下载源代码,请参见 Fury 下载页面。

使用

通过maven添加Fury依赖:

<dependency>
  <groupId>org.apache.fury</groupId>
  <artifactId>fury-core</artifactId>
  <version>0.7.0</version>
</dependency>
<!-- row/arrow format support -->
<!-- <dependency>
  <groupId>org.apache.fury</groupId>
  <artifactId>fury-format</artifactId>
  <version>0.7.0</version>
</dependency> -->

介绍

简介

Fury 是一个极其快速的多语言序列化框架,通过即时编译(JIT)和零拷贝实现。

协议

不同的场景有不同的序列化需求。Fury 针对这些需求设计并实现了多种二进制协议: 跨语言对象图协议:

  • 自动跨语言序列化任何对象,无需IDL定义、模式编译和对象与协议之间的转换。
  • 支持共享引用和循环引用,无重复数据或递归错误。
  • 支持对象多态性。

本地 Java/Python 对象图协议:基于语言的类型系统进行高度优化。 行格式协议:一种缓存友好的二进制随机访问格式,支持跳过序列化和部分序列化,并且可以自动转换为列格式。 新的协议可以基于 Fury 现有的缓冲区、编码、元数据、代码生成等能力轻松添加。所有这些协议共享相同的代码库,对某一协议的优化可以被另一协议重用。

兼容性

模式兼容性

Fury 的 Java 对象图序列化支持类模式的前向/后向兼容性。序列化端和反序列化端可以独立添加/删除字段。 在完成元数据压缩后,我们计划添加对跨语言序列化的支持。

二进制兼容性

我们仍在改进我们的协议,目前 Fury 版本之间不确保二进制兼容性。如果将来需要升级 Fury,请对 Fury 进行着色处理。 在 Fury 1.0 之前将确保二进制兼容性。

安全性

静态序列化(如行格式)本质上是安全的。但动态对象图序列化支持反序列化未注册类型,可能引入安全风险。 例如,反序列化可能会调用初始化构造函数或equals/hashCode 方法,如果方法体包含恶意代码,系统将面临风险。 Fury 为此协议提供了类注册模式选项,并默认启用,仅允许反序列化可信的注册类型或内置类型以确保安全。 Fury 提供了类注册选项,并默认启用此类协议,仅允许反序列化可信的注册类型或内置类型。除非能够确保环境确实安全,否则不要禁用类注册或类注册检查。如果禁用了类注册选项,我们不对安全性负责。

路线图

  • 元数据压缩、自动元数据共享和跨语言模式兼容性。
  • C++/Golang 的 AOT 框架以静态生成代码。
  • 支持 C++/Rust 对象图序列化
  • 支持 Golang/Rust/NodeJS 行格式
  • 支持 ProtoBuffer 兼容性
  • 支持特征和知识图序列化的协议
  • 不断改进我们的序列化基础设施以支持任何新协议

基准测试

不同的序列化框架适用于不同的场景,以下基准测试结果仅供参考。 如果需要针对特定场景进行基准测试,请确保所有序列化框架都为该场景进行了适当配置。 动态序列化框架支持多态性和引用,与静态序列化框架相比,成本更高,除非像 Fury 那样使用 JIT 技术。由于 Fury 会在运行时生成代码,请在收集基准测试数据之前进行预热。

java序列化

Fury序列化快速入门Apache Fury(孵化中)一个基于动态代码生成和零拷贝技术的多语言序列化框架,实现了无需ID Fury序列化快速入门Apache Fury(孵化中)一个基于动态代码生成和零拷贝技术的多语言序列化框架,实现了无需ID Fury序列化快速入门Apache Fury(孵化中)一个基于动态代码生成和零拷贝技术的多语言序列化框架,实现了无需ID Fury序列化快速入门Apache Fury(孵化中)一个基于动态代码生成和零拷贝技术的多语言序列化框架,实现了无需ID

java反序列化

Fury序列化快速入门Apache Fury(孵化中)一个基于动态代码生成和零拷贝技术的多语言序列化框架,实现了无需ID Fury序列化快速入门Apache Fury(孵化中)一个基于动态代码生成和零拷贝技术的多语言序列化框架,实现了无需ID Fury序列化快速入门Apache Fury(孵化中)一个基于动态代码生成和零拷贝技术的多语言序列化框架,实现了无需ID Fury序列化快速入门Apache Fury(孵化中)一个基于动态代码生成和零拷贝技术的多语言序列化框架,实现了无需ID 有关类型前向/后向兼容性、堆外支持、零拷贝序列化的更多基准测试,请参见基准测试。

JavaScript

Fury序列化快速入门Apache Fury(孵化中)一个基于动态代码生成和零拷贝技术的多语言序列化框架,实现了无需ID 用于此条形图的数据包括一个具有多种字段类型的复杂对象,JSON 数据的大小为 3KB。

特性

多语言支持:Java/Python/C++/Golang/Javascript/Rust。 零拷贝:受 Pickle5 启发的跨语言带外序列化和堆外读写。 高性能:一个高度可扩展的 JIT 框架,在运行时以异步多线程方式生成序列化器代码,加速序列化,提供 20-170 倍的速度提升,通过:

  • 在生成的代码中内联变量,减少内存访问。
  • 在生成的代码中内联调用,减少虚方法调用。
  • 减少条件分支。
  • 减少哈希查找。
  • 二进制协议:对象图、行格式等。

除了跨语言序列化外,Fury 还具备以下特点:

  • 无需修改任何代码,即可替换 Java 序列化框架,如 JDK/Kryo/Hessian,但速度快 100 倍。可以大大提高高性能 RPC 调用、数据传输和对象持久化的效率。
  • 100% 兼容 JDK 序列化,原生支持 Java 自定义序列化方法 writeObject/readObject/writeReplace/readResolve/readObjectNoData。
  • 支持 Golang 的共享和循环引用对象序列化。
  • 支持 Golang 的自动对象序列化。

指导

java序列化指导

java对象图序列化

当只需要 Java 对象序列化时,与跨语言对象图序列化相比,此模式将具有更好的性能。

快速开始

请注意,创建 Fury 并不便宜,Fury 实例应在序列化之间重用,而不是每次都创建它。你应该将 Fury 保持为静态全局变量,或单例对象或有限对象的实例变量。

fury单线程的用法
import org.apache.fury.Fury;
import org.apache.fury.config.Language;

public static void main(String[] args) {
        SomeClass object = new SomeClass();
        // 请注意,Fury 实例应在多个不同对象的序列化之间重用。
        Fury fury = Fury.builder().withLanguage(Language.JAVA)
                // 允许反序列化未知类型的对象,
                // 更灵活但安全性较低。
                // .withSecureMode(false)
                .build();
        // 注册类型可以减少类名序列化开销,但不是必须的。
        // 如果启用了安全模式,则所有自定义类型都必须注册。
        fury.register(SomeClass.class);
        byte[] bytes = fury.serialize(object);
        System.out.println(fury.deserialize(bytes));
    }
}
fury多线程的用法
import com.study.cache.client.serialization.SomeClass;
import org.apache.fury.*;
import org.apache.fury.config.*;

public class Example {
    public static void main(String[] args) {
        SomeClass object = new SomeClass();
        //请注意,Fury 实例应在多个不同对象的序列化之间重用。
        ThreadSafeFury fury = new ThreadLocalFury(classLoader -> {
            Fury f = Fury.builder().withLanguage(Language.JAVA)
                    .withClassLoader(classLoader).build();
            f.register(SomeClass.class);
            return f;
        });
        byte[] bytes = fury.serialize(object);
        System.out.println(fury.deserialize(bytes));
    }
}

ThreadSafeFury 线程安全的序列化器接口。Fury 不是线程安全的,该接口的实现将是线程安全的,并且支持动态切换类加载器。

Fury实例重用实例
import com.study.cache.client.serialization.SomeClass;
import org.apache.fury.*;
import org.apache.fury.config.*;

public class Example {
    // 重用fury
    private static final ThreadSafeFury fury = new ThreadLocalFury(classLoader -> {
        Fury f = Fury.builder().withLanguage(Language.JAVA)
                .withClassLoader(classLoader).build();
        f.register(SomeClass.class);
        return f;
    });

    public static void main(String[] args) {
        SomeClass object = new SomeClass();
        byte[] bytes = fury.serialize(object);
        System.out.println(fury.deserialize(bytes));
    }
}

FuryBuilder 选项

选型名称描述默认值
timeRefIgnored当启用了引用跟踪时,是否忽略在 TimeSerializers和这些类型的子类中注册的所有时间类型的引用跟踪。如果忽略,可以通过调用 Fury#registerSerializer(Class, Serializer) 启用每种时间类型的引用跟踪。例如,fury.registerSerializer(Date.class, new DateSerializer(fury, true))。注意,在任何包含时间字段的类型的序列化器代码生成之前,应该启用引用跟踪,否则这些字段将仍然跳过引用跟踪。true
compressInt启用或禁用 int 压缩以减少大小。true
compressLong启用或禁用 long 压缩以减少大小。true
compressString启用或禁用字符串压缩以减少大小。true
classLoader类加载器不应更新;Fury缓存类的元数据。使用 LoaderBindingThreadSafeFury 来更新类加载器。Thread.currentThread().getContextClassLoader()
compatibleMode类型前向/后向兼容性配置。还与 checkClassVersion 配置相关。SCHEMA_CONSISTENT:类模式在序列化端和反序列化端之间必须一致。COMPATIBLE:类模式在序列化端和反序列化端之间可以不同。他们可以独立添加/删除字段。CompatibleMode.SCHEMA_CONSISTENT
checkClassVersion确定是否检查类模式的一致性。如果启用,Fury 将使用 classVersionHash 进行检查、写入和一致性检查。当启用 CompatibleMode#COMPATIBLE 时,它将自动禁用。除非能确保类不会演变,否则不建议禁用。false
checkJdkClassSerializable启用或禁用对 java.* 下类的 Serializable 接口的检查。如果java.*下的类不是 Serializable,Fury 将抛出 UnsupportedOperationExceptiontrue
registerGuavaTypes是否预注册 Guava 类型,如 RegularImmutableMap/RegularImmutableList。这些类型不是公共 API,但看起来相当稳定。true
requireClassRegistration禁用可能允许未知类被反序列化,可能导致安全风险。true
suppressClassRegistrationWarnings是否抑制类注册警告。警告可用于安全审计,但可能令人烦恼,此抑制默认启用。true
metaShareEnabled启用或禁用元数据共享模式。false
scopedMetaShareEnabled作用域元数据共享专注于单个序列化过程。在此过程中创建或识别的元数据是独有的,不与其他序列化共享。false
metaCompressor设置一个压缩器进行元数据压缩。注意,传递的 MetaCompressor 应该是线程安全的。默认情况下,将使用基于 Deflater 的压缩器 DeflaterMetaCompressor。用户可以传递其他压缩器,如 zstd,以获得更好的压缩率。DeflaterMetaCompressor
deserializeNonexistentClass启用或禁用对不存在类的数据反序列化/跳过。如果设置了 CompatibleMode.Compatible,则为 true,否则为 false``如果设置了CompatibleMode.Compatible 则为true,否则为false
codeGenEnabled禁用可能会导致初始序列化更快,但后续序列化更慢。true
asyncCompilationEnabled如果启用,序列化首先使用解释模式,并在类的异步序列化器 JIT 完成后切换到 JIT 序列化。false
scalaOptimizationEnabled启用或禁用特定于 Scala 的序列化优化。false
copyRef禁用时,复制性能会更好。但 Fury 深度复制将忽略循环和共享引用。同一对象图的引用将在一次 Fury#copy 中被复制成不同的对象。true

高级用法

创建Fury

单线程Fury
Fury fury = Fury.builder()
.withLanguage(Language.JAVA)
// 启用引用跟踪以支持共享/循环引用。
// 如果没有重复引用,禁用它可以提高性能。
.withRefTracking(false)
.withCompatibleMode(CompatibleMode.SCHEMA_CONSISTENT)
// 启用类型前向/后向兼容性
// 为了更小的尺寸和更好的性能禁用它。
// .withCompatibleMode(CompatibleMode.COMPATIBLE)
// 启用异步多线程编译。
.withAsyncCompilation(true)
.build();
byte[] bytes = fury.serialize(object);
System.out.println(fury.deserialize(bytes));
线程安全Fury
ThreadSafeFury fury = Fury.builder()
  .withLanguage(Language.JAVA)
  // 启用引用跟踪以支持共享/循环引用。
  // 如果没有重复引用,禁用它可以提高性能。
  .withRefTracking(false)
  // 压缩 int 类型以获得更小的尺寸
  // .withIntCompressed(true)
  // 压缩 long 类型以获得更小的尺寸
  // .withLongCompressed(true)
  .withCompatibleMode(CompatibleMode.SCHEMA_CONSISTENT)
  // 启用类型前向/后向兼容性
  // 为了更小的尺寸和更好的性能禁用它。
  // .withCompatibleMode(CompatibleMode.COMPATIBLE)
  // 启用异步多线程编译。
  .withAsyncCompilation(true)
  .buildThreadSafeFury();
byte[] bytes = fury.serialize(object);
System.out.println(fury.deserialize(bytes));

更小的大小

FuryBuilder#withIntCompressed/FuryBuilder#withLongCompressed 可以用于压缩 int/long 类型以减小尺寸。通常,压缩 int 类型已经足够。 这两种压缩默认都是启用的,如果序列化的大小不重要,比如你之前使用 flatbuffers 进行序列化,它不进行任何压缩,那么你应该禁用压缩。如果你的数据全是数字,压缩可能会带来 80% 的性能下降。 对于 int 压缩,Fury 使用 1~5 字节进行编码。每个字节的第一位表示是否有下一个字节。如果第一位被设置,则读取下一个字节,直到下一个字节的第一位未设置为止。 对于 long 压缩,Fury 支持两种编码:

  • Fury SLI(Small Long as Int)编码(默认使用):
    • 如果 long 值在 [-1073741824, 1073741823] 之间,编码为 4 字节 int:| little-endian:((int) value) << 1 |
    • 否则写为 9 字节:| 0b1 | little-endian 8 字节 long |
  • Fury PVL(Progressive Variable-length Long)编码:
    • 每个字节的第一位表示是否有下一个字节。如果第一位被设置,则读取下一个字节,直到下一个字节的第一位未设置为止。
    • 负数将通过 (v << 1) ^ (v >> 63) 转换为正数,以减少小负数的成本。 如果一个数字是 long 类型,通常不能用更小的字节表示,压缩效果不佳,相对于性能成本不值得。如果你发现压缩没有带来太多空间节省,可以尝试禁用 long 压缩。

对象深复制

深复制例子

Fury fury=Fury.builder()
  ...
  .withRefCopy(true).build();
  SomeClass a=xxx;
  SomeClass copied=fury.copy(a)

使 Fury 深拷贝忽略循环和共享引用,这种深拷贝模式将忽略循环和共享引用。在一次 Fury#copy 操作中,对象图中的相同引用将被复制成不同的对象。

Fury fury=Fury.builder()
  ...
  .withRefCopy(false).build();
  SomeClass a=xxx;
  SomeClass copied=fury.copy(a)

实现自定义的序列化

在某些情况下,您可能需要为您的类型实现一个序列化器,特别是一些类通过 JDK 的 writeObject/writeReplace/readObject/readResolve 自定义序列化,这些方法的效率很低。例如,您不希望以下的 Foo#writeObject 被调用,您可以参考下面的 FooSerializer 作为示例:

class Foo {
  public long f1;

  private void writeObject(ObjectOutputStream s) throws IOException {
    System.out.println(f1);
    s.defaultWriteObject();
  }
}

class FooSerializer extends Serializer<Foo> {
  public FooSerializer(Fury fury) {
    super(fury, Foo.class);
  }

  @Override
  public void write(MemoryBuffer buffer, Foo value) {
    buffer.writeInt64(value.f1);
  }

  @Override
  public Foo read(MemoryBuffer buffer) {
    Foo foo = new Foo();
    foo.f1 = buffer.readInt64();
    return foo;
  }
}

注册序列化器

Fury fury=getFury();
  fury.registerSerializer(Foo.class,new FooSerializer(fury));

安全和类注册

可以使用 FuryBuilder#requireClassRegistration来禁用类注册,这将允许反序列化未知类型的对象,更灵活但如果类包含恶意代码可能不安全。 除非能够确保环境安全,否则不要禁用类注册。当禁用此选项时,反序列化未知/不可信类型时,init/equals/hashCode 中的恶意代码可能会被执行。 类注册不仅可以降低安全风险,还可以避免类名序列化的开销。 您可以使用 API Fury#register 来注册类。 注意类注册的顺序很重要,序列化和反序列化的两端应该有相同的注册顺序。

Fury fury=xxx;
  fury.register(SomeClass.class);
  fury.register(SomeClass1.class,200);

如果您调用 FuryBuilder#requireClassRegistration(false) 来禁用类注册检查,可以通过 ClassResolver#setClassChecker 设置 org.apache.fury.resolver.ClassChecker 以控制允许序列化的类。例如,您可以允许以 org.example.*开头的类:

Fury fury=xxx;
  fury.getClassResolver().setClassChecker((classResolver,className)->className.startsWith("org.example."));
AllowListChecker checker=new AllowListChecker(AllowListChecker.CheckLevel.STRICT);
  ThreadSafeFury fury=new ThreadLocalFury(classLoader->{
  Fury f=Fury.builder().requireClassRegistration(true).withClassLoader(classLoader).build();
  f.getClassResolver().setClassChecker(checker);
  checker.addListener(f.getClassResolver());
  return f;
  });
  checker.allowClass("org.example.*");

Fury 还提供了一个基于允许/禁止列表的检查器 org.apache.fury.resolver.AllowListChecker,以简化类检查机制的自定义。您可以使用此检查器或自行实现更复杂的检查器。

序列化注册

您还可以通过 Fury#registerSerializer API 为类注册自定义序列化器。 或者实现 java.io.Externalizable 接口。

零拷贝

import org.apache.fury.*;
import org.apache.fury.config.*;
import org.apache.fury.serializers.BufferObject;
import org.apache.fury.memory.MemoryBuffer;

import java.util.*;
import java.util.stream.Collectors;

public class ZeroCopyExample {
  //注意,fury创建的实例应该被复用
  static Fury fury = Fury.builder()
    .withLanguage(Language.JAVA)
    .build();

  // mvn exec:java -Dexec.mainClass="io.ray.fury.examples.ZeroCopyExample"
  public static void main(String[] args) {
    List<Object> list = Arrays.asList("str", new byte[1000], new int[100], new double[100]);
    Collection<BufferObject> bufferObjects = new ArrayList<>();
    byte[] bytes = fury.serialize(list, e -> !bufferObjects.add(e));
    List<MemoryBuffer> buffers = bufferObjects.stream()
      .map(BufferObject::toBuffer).collect(Collectors.toList());
    System.out.println(fury.deserialize(bytes, buffers));
  }
}

共享元数据

Fury 支持在一个上下文(例如,TCP 连接)中共享多个序列化之间的类型元数据(类名、字段名、最终字段类型信息等),这些信息将在该上下文的首次序列化期间发送到对端。基于这些元数据,对端可以重建相同的反序列化器,从而避免后续序列化传输元数据,减少网络传输压力,并自动支持类型的前向/后向兼容性。

// Fury.builder()
//   .withLanguage(Language.JAVA)
//   .withRefTracking(false)
//   // share meta across serialization.
//   .withMetaContextShare(true)
// Not thread-safe fury.
MetaContext context=xxx;
  fury.getSerializationContext().setMetaContext(context);
  byte[]bytes=fury.serialize(o);
// 线程不安全的fury
  MetaContext context=xxx;
  fury.getSerializationContext().setMetaContext(context);
  fury.deserialize(bytes)

// 线程安全的fury
  fury.setClassLoader(beanA.getClass().getClassLoader());
  byte[]serialized=fury.execute(
  f->{
  f.getSerializationContext().setMetaContext(context);
  return f.serialize(beanA);
  }
  );
// 线程安全的fury
  fury.setClassLoader(beanA.getClass().getClassLoader());
  Object newObj=fury.execute(
  f->{
  f.getSerializationContext().setMetaContext(context);
  return f.deserialize(serialized);
  }
  );

反序列化不存在的类

Fury 支持反序列化不存在的类,可以通过 FuryBuilder#deserializeNonexistentClass(true) 启用此功能。启用后,并且元数据共享启用时,Fury 会将此类型的反序列化数据存储在 Map 的延迟子类中。通过使用 Fury 实现的延迟 Map,可以避免在反序列化期间填充 Map 的重新平衡成本,进一步提高性能。如果此数据被发送到另一个进程,并且该进程中存在此类,数据将被反序列化为该类型的对象而不丢失任何信息。 如果未启用元数据共享,新类数据将被跳过,并返回一个 NonexistentSkipClass 存根对象。

合并

JDK序列化合并

如果你之前使用的是 JDK 序列化,并且不能同时升级客户端和服务器(这对于在线应用程序来说是常见的情况),Fury 提供了一个工具方法 org.apache.fury.serializer.JavaSerializer.serializedByJDK 用于检查二进制数据是否由 JDK 序列化生成。你可以使用以下模式使现有的序列化协议感知,然后以异步滚动升级的方式将序列化升级到 Fury:

if(JavaSerializer.serializedByJDK(bytes)){
  ObjectInputStream objectInputStream=xxx;
  return objectInputStream.readObject();
  }else{
  return fury.deserialize(bytes);
  }

更新Fury

目前,二进制兼容性仅确保在小版本中。例如,如果你使用的是 Fury v0.2.0,升级到 Fury v0.2.1 将提供二进制兼容性。但如果升级到 Fury v0.4.1,则不保证二进制兼容性。大多数情况下,没有必要将 Fury 升级到更新的主版本,当前版本已经足够快速和紧凑,并且我们会为近期的旧版本提供一些小的修复。 但是,如果你确实想升级 Fury 以获得更好的性能和更小的尺寸,你需要使用如下代码将 Fury 版本作为头写入序列化数据中以保持二进制兼容性:

MemoryBuffer buffer=xxx;
  buffer.writeVarInt32(2);
  fury.serialize(buffer,obj);

然后对于反序列化,你需要:

MemoryBuffer buffer=xxx;
int furyVersion=buffer.readVarInt32()
Fury fury=getFury(furyVersion);
fury.deserialize(buffer);

getFury 是一个加载相应 Fury 的方法,你可以将不同版本的 Fury 着色并重新定位到不同的包,然后根据版本加载 Fury。 如果你通过小版本升级 Fury,或者你没有旧版 Fury 序列化的数据,你可以直接升级 Fury,无需对数据进行版本控制。

故障排除

类不一致和类版本检查

如果你在创建 Fury 时没有将 CompatibleMode 设置为 org.apache.fury.config.CompatibleMode.COMPATIBLE,并且遇到了奇怪的序列化错误,可能是由于序列化端和反序列化端之间的类不一致导致的。 在这种情况下,你可以调用 FuryBuilder#withClassVersionCheck 来创建 Fury 以验证它,如果反序列化抛出 org.apache.fury.exception.ClassNotCompatibleException,则表示类不一致,你应该使用 FuryBuilder#withCompatibleMode(CompatibleMode.COMPATIBLE) 创建 Fury。 CompatibleMode.COMPATIBLE 会带来更多的性能和空间成本,如果你的类在序列化和反序列化之间始终一致,不要默认设置它。

使用错误的反序列化 API

如果你通过调用 Fury#serialize 序列化一个对象,你应该调用 Fury#deserialize 进行反序列化,而不是 Fury#deserializeJavaObject。 如果你通过调用 Fury#serializeJavaObject 序列化一个对象,你应该调用 Fury#deserializeJavaObject 进行反序列化,而不是 Fury#deserializeJavaObjectAndClass/Fury#deserialize。 如果你通过调用 Fury#serializeJavaObjectAndClass 序列化一个对象,你应该调用 Fury#deserializeJavaObjectAndClass 进行反序列化,而不是 Fury#deserializeJavaObject/Fury#deserialize

转载自:https://juejin.cn/post/7407374655110250507
评论
请登录