Fury序列化快速入门Apache Fury(孵化中)一个基于动态代码生成和零拷贝技术的多语言序列化框架,实现了无需ID
开始
官网地址: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序列化
java反序列化
有关类型前向/后向兼容性、堆外支持、零拷贝序列化的更多基准测试,请参见基准测试。
JavaScript
用于此条形图的数据包括一个具有多种字段类型的复杂对象,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 缓存类的元数据。使用 LoaderBinding 或 ThreadSafeFury 来更新类加载器。 | Thread.currentThread().getContextClassLoader() |
compatibleMode | 类型前向/后向兼容性配置。还与 checkClassVersion 配置相关。SCHEMA_CONSISTENT :类模式在序列化端和反序列化端之间必须一致。COMPATIBLE :类模式在序列化端和反序列化端之间可以不同。他们可以独立添加/删除字段。 | CompatibleMode.SCHEMA_CONSISTENT |
checkClassVersion | 确定是否检查类模式的一致性。如果启用,Fury 将使用 classVersionHash 进行检查、写入和一致性检查。当启用 CompatibleMode#COMPATIBLE 时,它将自动禁用。除非能确保类不会演变,否则不建议禁用。 | false |
checkJdkClassSerializable | 启用或禁用对 java.* 下类的 Serializable 接口的检查。如果java.* 下的类不是 Serializable ,Fury 将抛出 UnsupportedOperationException 。 | true |
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 |
- 如果 long 值在 [-1073741824, 1073741823] 之间,编码为 4 字节 int:
- 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