list = Arrays.asList("str", new byte[1000], new int[100], new double[100]);
Collection bufferObjects = new ArrayList<>();
byte[] bytes = fury.serialize(list, e -> !bufferObjects.add(e));
// bytes can be data serialized by other languages.
List buffers =
bufferObjects.stream().map(BufferObject::toBuffer).collect(Collectors.toList());
System.out.println(fury.deserialize(bytes, buffers));
}
}Python序列化示例 import array
import pyfury
import numpy as np
if __name__ == "__main__":
fury_ = pyfury.Fury()
list_ = ["str", bytes(bytearray(1000)),
array.array("i", range(100)), np.full(100, 0.0, dtype=np.double)]
serialized_objects = []
data = fury_.serialize(list_, buffer_callback=serialized_objects.append)
buffers = [o.to_buffer() for o in serialized_objects]
# bytes can be data serialized by other languages.
print(fury_.deserialize(data, buffers=buffers))
Golang序列化示例 package main
import "code.alipay.com/ray-project/fury/go/fury"
import "fmt"
func main() {
fury := fury.NewFury(true)
// Golang版本暂不支持其他基本类型slice的zero-copy
list := []interface{}{"str", make([]byte, 1000)}
buf := fury.NewByteBuffer(nil)
var serializedObjects []fury.SerializedObject
fury.Serialize(buf, list, func(o fury.SerializedObject) bool {
serializedObjects = append(serializedObjects, o)
return false
})
var newList []interface{}
var buffers []*fury.ByteBuffer
for _, o := range serializedObjects {
buffers = append(buffers, o.ToBuffer())
}
err := fury.Deserialize(buf, &newList, buffers)
fmt.Println(newList)
}
Drop-in替换Kryo/Hessian 除了多语言原生序列化以外,Fury还是一个极致性能的通用Java序列化框架 ,可以序列化任意Java Object,支持堆内/堆外内存。可以Drop-in替换 jdk/kryo/hessian等序列化框架,同时性能最高是Kryo 20倍以上 ,Hessian100倍 以上,JDK自带序列化200倍 。
下面是一个序列化自定义类型的示例:
import io.fury.Fury;
import java.util.List;
import java.util.Arrays;
public class Example {
public static void main(String[] args) {
SomeClass object = new SomeClass();
// Fury实例应该在序列化多个对象之间复用,不要每次创建新的实例
{
Fury fury = Fury.builder()
.withLanguage(Language.JAVA)
// 设置为true可以避免反序列化未注册的非内置类型,
// 避免安全漏洞
.withClassRegistrationRequired(false)
.withReferenceTracking(true).build();
// 注册类型可以减少classname的序列化,不是强制要求
// fury.register(SomeClass.class);
byte[] bytes = fury.serialize(object);
System.out.println(fury.deserialize(bytes));
}
{
ThreadSafeFury fury = Fury.builder().withLanguage(Language.JAVA)
.withReferenceTracking(true)
.withClassRegistrationRequired(false)
.buildThreadSafeFury();
byte[] bytes = fury.serialize(object);
System.out.println(fury.deserialize(bytes));
}
{
ThreadSafeFury fury = new ThreadSafeFury(() -> {
Fury fury = Fury.builder()
.withLanguage(Language.JAVA)
.withClassRegistrationRequired(false)
.withReferenceTracking(true).build();
// 注册类型可以减少classname的序列化
fury.register(SomeClass.class);
return fury;
});
byte[] bytes = fury.serialize(object);
System.out.println(fury.deserialize(bytes));
}
}
}
通过Fury Format避免序列化 对于有极致性能要求的场景,如果用户只需要读取部分数据,或者根据对象树某个字段进行过滤,可以使用Fury Format来避免其它字段的序列化。Fury Row Format是参考SQL行存和Arrow列存实现的 一套可以随机访问的二进制行存结构。目前实现了Java/Python/C++版本,Python版本通过Cython绑定到C++实现。
由于该格式是自包含的,可以根据schema直接计算出任意字段的offset 。因此通过使用该格式,可以避免掉序列化,直接在二进制数据buffer上面进行所有读写操作。这样做有三个优势:
减少Java GC overhead 。由于避免了反序列化,因此不会创建对象,从而避免了GC问题
避免Python反序列化 。Python性能一直很慢,因此在跨语言序列化时,可以在Java侧序列化成Row-Format,然后Python侧直接使用该数据计算,这样就避免了Python反序列化的昂贵开销。同时由于Python的动态性,Fury的BinaryRow/BinaryArrays实现了
_getattr__/__getitem__/slice/
和其它
special methods,
保证了行为跟
python pojo/list/object
的一致性,用户没有任何感知
。
Cache-aware计算
。Java/Python对象都是稀疏分散存储的,而Fury的binary fow是密集存储且八字节对齐,缓存更加友好,读写更快。
Python示例 这里给出一个读取部分数据的样例以及性能测试结果。在下面这个序列化场景中,需要读取第二个数组字段的第10万个元素,Fury耗时几乎为0,而pickler需要8秒 。
@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()
print(f"start: {datetime.datetime.now()}")
foo_row = pyfury.RowData(encoder.schema, binary)
print(foo_row.f2[100000], foo_row.f4[100000].f1, foo_row.f4[200000].f2[5])
print(f"end: {datetime.datetime.now()}")
binary = pickle.dumps(foo)
print(f"pickle start: {datetime.datetime.now()}")
new_foo = pickle.loads(binary)
print(new_foo.f2[100000], new_foo.f4[100000].f1, new_foo.f4[200000].f2[5])
print(f"pickle end: {datetime.datetime.now()}")
Java示例 用户也可以在Java层把对象转换成该格式,然后在Python层直接读写该Binary数据,避免Python的性能瓶颈。同时Java层也可以部分读取该对象,或者部分反序列化为Java对象,避免反序列化开销和GC负荷。
public class Bar {
String f1;
List f2;
}
public class Foo {
int f1;
List f2;
Map f3;
List f4;
}
Encoder encoder = Encoders.rowEncoder(Foo.class);
BinaryRow binaryRow = encoder.toRow(foo); // 该数据可以被Python零拷贝解析
Foo newFoo = encoder.fromRow(binaryRow); // 可以是来自python序列化的数据
BinaryArray binaryArray2 = binaryRow.getArray(1); // 零拷贝读取List f2字段
BinaryArray binaryArray4 = binaryRow.getArray(4); // 零拷贝读取List f4字段
BinaryRow barStruct = binaryArray4.getStruct(10);// 零拷贝读取读取List f4第11个元素数据
// 零拷贝读取读取List f4第11个元素数据的f2字段的第6个元素
long aLong = barStruct.getArray(1).getLong(5);
Encoder barEncoder = Encoders.rowEncoder(Bar.class);
// 部分反序列化对象
Bar newBar = barEncoder.fromRow(barStruct);
Bar newBar2 = barEncoder.fromRow(binaryArray4.getStruct(20));
// 对象创建示例:
// Foo foo = new Foo();
// foo.f1 = 10;
// foo.f2 = IntStream.range(0, 1000000).boxed().collect(Collectors.toList());
// foo.f3 = IntStream.range(0, 1000000).boxed().collect(Collectors.toMap(i -> "k"+i, i->i));
// List bars = new ArrayList<>(1000000);
// for (int i = 0; i < 1000000; i++) {
// Bar bar = new Bar();
// bar.f1 = "s"+i;
// bar.f2 = LongStream.range(0, 10).boxed().collect(Collectors.toList());
// bars.add(bar);
// }
// foo.f4 = bars;
自动转换Arrow 该格式支持自动与Arrow列存互转。
Python示例:
import pyfury
encoder = pyfury.encoder(Foo)
encoder.to_arrow_record_batch([foo] * 10000)
encoder.to_arrow_table([foo] * 10000)
C++示例:
std::shared_ptr arrow_writer;
EXPECT_TRUE(
ArrowWriter::Make(schema, ::arrow::default_memory_pool(), &arrow_writer)
.ok());
for (auto &row : rows) {
EXPECT_TRUE(arrow_writer->Write(row).ok());
}
std::shared_ptr<::arrow::RecordBatch> record_batch;
EXPECT_TRUE(arrow_writer->Finish(&record_batch).ok());
EXPECT_TRUE(record_batch->Validate().ok());
EXPECT_EQ(record_batch->num_columns(), schema->num_fields());
EXPECT_EQ(record_batch->num_rows(), row_nums);
Java示例:
Schema schema = TypeInference.inferSchema(BeanA.class);
ArrowWriter arrowWriter = ArrowUtils.createArrowWriter(schema);
Encoder encoder = Encoders.rowEncoder(BeanA.class);
for (int i = 0; i < 10; i++) {
BeanA beanA = BeanA.createBeanA(2);
arrowWriter.write(encoder.toRow(beanA));
}
return arrowWriter.finishAsRecordBatch();
对比其它序列化框架 跟其它框架的对比将分为功能、易用性和性能三个维度,每个维度上Fury都有非常显著的优势。
功能比较 这里从10个维度将Fury跟别的框架进行对比,每个维度的含义分别为:
多语言/跨语言:是否支持多种语言以及是否支持跨语言序列化
自动序列化:是否需要写大量序列化代码,还是可以完全自动话
是否需要schema编译:是否需要编写schema IDL文件,并编译schema生成代码
自定义类型:是否支持自定义类型,即POJO/DataClass/Struct等
非自定义类型:是否支持非自定义类型,即是否支持直接序列化基本类型、数组、List、Map等,还是需要将这些类型包装到自定义类型内部才能进行序列化
引用/循环引用:对于指向同一个对象的两份引用,是否只会序列化数据一次;对于循环引用,是否能够进行序列化而不是出现递归报错
多态子类型:对于List/Map的多个子类型如ArrayList/LinkedList/ImmutableList,HashMap/LinkedHashMap等,反序列化是否能够得到相同的类型,还是会变成ArrayList和HashMap
反序列化是否需要传入类型:即是否需要在反序列化时需要提前知道数据对应的类型。如果需要的话则灵活性和易用性会受到限制,而且传入的类型不正确的话反序列化可能会crash
部分反序列化/随机读写:反序列化是否可以只读取部分字段或者嵌套的部分字段,对于大对象这可以节省大量序列化开销
堆外内存读写:即是否支持直接读写native内存
数值类型可空:是否支持基本类型为null,比如Java的Integer等装箱类型以及python的int/float可能为null
框架
多语言/跨语言
自动序列化
是否需要schema编译
自定义类型
非自定义类型
引用/
循环引用
多态子类型
反序列化是否需要传入类型
零拷贝
部分反序列化/随机读写
堆外内存读写
数值类型可空
Fury
支持
支持
不需要
支持
支持
支持
支持
不需要
支持
支持
支持
支持
Kryo
Java
支持
不需要
支持
支持
支持
支持
不需要
不支持
不支持
支持
支持
Fst
Java
支持
不需要
支持
支持
支持
支持
不需要
不支持
不支持
不支持
支持
JDK
Java
支持
不需要
支持
支持
支持
支持
不需要
不支持
不支持
不支持
支持
Pickle
Python
支持
不需要
支持
支持
支持
支持
不需要
支持
不支持
支持
支持
Protobuf
支持
不支持
需要
支持
不支持
不支持
不支持
需要
不支持
不支持
不支持
支持
Thrift
支持
不支持
需要
支持
不支持
不支持
不支持
需要
不支持
不支持
不支持
不支持
Flatbuffer
支持
不支持
需要
支持
不支持
不支持
不支持
需要
不支持
支持
支持
不支持
Avro
支持
支持
可选
支持
不支持
不支持
不支持
需要
不支持
不支持
不支持
不支持
Msgpack
支持
Java不支持
不需要
Java/Python不支持
支持
不支持
不支持
需要
不支持
不支持
不支持
不支持
易用性比较 这里以如下class结构为例对比易用性,最终需要序列化的对象是一个Bar的实例:
class Foo {
String f1;
Map f2;
}
class Bar {
Foo f1;
String f2;
List f3;
Map f4;
Integer f5;
Long f6;
Float f7;
Double f8;
short[] f9;
List f10;
}
Fury序列化 Fury序列化只需一行代码,且无任何学习成本。
Fury fury = Fury.builder().withLanguage(Language.XLANG).build();
byte[] data = fury.serialize(bar);
// 这里的data可以是被Fury python/Golang实现序列化的数据
Bar newBar = fury.deserialize(data);
对比Protobuf
syntax = "proto3";
package protobuf;
option java_package = "io.ray.fury.benchmark.state.generated";
option java_outer_classname = "ProtoMessage";
message Foo {
optional string f1 = 1;
map f2 = 2;
}
message Bar {
optional Foo f1 = 1;
optional string f2 = 2;
repeated Foo f3 = 3;
map f4 = 4;
optional int32 f5 = 5;
optional int64 f6 = 6;
optional float f7 = 7;
optional double f8 = 8;
repeated int32 f9 = 9; // proto不支持int16
repeated int64 f10 = 10;
}
为了避免把生成的代码提交到代码仓库,需要将proto跟构建工具进行集成,这块较为复杂,存在大量
构建工具集成成本。 且由于构建工具的不完善,这部分依然无法完全自动化,比如
protobuf-maven-plugin 依然需要用户在机器安装protoc,而不是自动下载protoc
。
public static byte[] serializeBar(Bar bar) {
return build(bar).build().toByteArray();
}
public static ProtoMessage.Bar.Builder build(Bar bar) {
ProtoMessage.Bar.Builder barBuilder = ProtoMessage.Bar.newBuilder();
if (bar.f1 == null) {
barBuilder.clearF1();
} else {
barBuilder.setF1(buildFoo(bar.f1));
}
if (bar.f2 == null) {
barBuilder.clearF2();
} else {
barBuilder.setF2(bar.f2);
}
if (bar.f3 == null) {
barBuilder.clearF3();
} else {
for (Foo foo : bar.f3) {
barBuilder.addF3(buildFoo(foo));
}
}
if (bar.f4 == null) {
barBuilder.clearF4();
} else {
bar.f4.forEach(
(k, v) -> {
ProtoMessage.Foo.Builder fooBuilder1 = ProtoMessage.Foo.newBuilder();
fooBuilder1.setF1(v.f1);
v.f2.forEach(fooBuilder1::putF2);
barBuilder.putF4(k, fooBuilder1.build());
});
}
if (bar.f5 == null) {
barBuilder.clearF5();
} else {
barBuilder.setF5(bar.f5);
}
if (bar.f6 == null) {
barBuilder.clearF6();
} else {
barBuilder.setF6(bar.f6);
}
if (bar.f7 == null) {
barBuilder.clearF7();
} else {
barBuilder.setF7(bar.f7);
}
if (bar.f8 == null) {
barBuilder.clearF8();
} else {
barBuilder.setF8(bar.f8);
}
if (bar.f9 == null) {
barBuilder.clearF9();
} else {
for (short i : bar.f9) {
barBuilder.addF9(i);
}
}
if (bar.f10 ==null) {
barBuilder.clearF10();
} else {
barBuilder.addAllF10(bar.f10);
}
return barBuilder;
}
public static ProtoMessage.Foo.Builder buildFoo(Foo foo) {
ProtoMessage.Foo.Builder builder = ProtoMessage.Foo.newBuilder();
if (foo.f1 == null) {
builder.clearF1();
} else {
builder.setF1(foo.f1);
}
if (foo.f2 == null) {
builder.clearF2();
} else {
foo.f2.forEach(builder::putF2);
}
return builder;
}
public static Foo fromFooBuilder(ProtoMessage.Foo.Builder builder) {
Foo foo = new Foo();
if (builder.hasF1()) {
foo.f1 = builder.getF1();
}
foo.f2 = builder.getF2Map();
return foo;
}
public static Bar deserializeBar(byte[] bytes) throws InvalidProtocolBufferException {
Bar bar = new Bar();
ProtoMessage.Bar.Builder barBuilder = ProtoMessage.Bar.newBuilder();
barBuilder.mergeFrom(bytes);
if (barBuilder.hasF1()) {
bar.f1 = fromFooBuilder(barBuilder.getF1Builder());
}
if (barBuilder.hasF2()) {
bar.f2 = barBuilder.getF2();
}
bar.f3 =
barBuilder.getF3BuilderList().stream()
.map(ProtoState::fromFooBuilder)
.collect(Collectors.toList());
bar.f4 = new HashMap<>();
barBuilder.getF4Map().forEach((k, v) -> bar.f4.put(k, fromFooBuilder(v.toBuilder())));
if (barBuilder.hasF5()) {
bar.f5 = barBuilder.getF5();
}
if (barBuilder.hasF6()) {
bar.f6 = barBuilder.getF6();
}
if (barBuilder.hasF7()) {
bar.f7 = barBuilder.getF7();
}
if (barBuilder.hasF8()) {
bar.f8 = barBuilder.getF8();
}
bar.f9 = new short[barBuilder.getF9Count()];
for (int i = 0; i < barBuilder.getF9Count(); i++) {
bar.f9[i] = (short) barBuilder.getF9(i);
}
bar.f10 = barBuilder.getF10List();
return bar;
}
Python序列化代码:大概130~150行
GoLang序列化代码:大概130~150行
即使之前没有针对该数据的自定义类型,也无法将protobuf生成的class直接用在业务代码里面。因为protobuf生成的class
并不符合面向对象设计 ,无法给生成的class添加行为。这时候就需要定义额外的wrapper,如果自动内部有其它自定义类型,还需要将这些类型转换成对应的wrapper,这进一步限制了使用的灵活性。
对比Flatbuffer Flatbuffer与protobuf一样,也需要大量的学习成本和开发成本:
安装
flatc编译器 ,对于Linux环境,可能还需要进行源码编译安装flatc。
定义Schema
namespace io.ray.fury.benchmark.state.generated;
table FBSFoo {
string:string;
f2_key:[string]; // flatbuffers不支持map
f2_value:[int];
}
table FBSBar {
f1:FBSFoo;
f2:string;
f3:[FBSFoo];
f4_key:[int]; // flatbuffers不支持map
f4_value:[FBSFoo];
f5:int;
f6:long;
f7:float;
f8:double;
f9:[short];
f10:[long];
// 由于fbs不支持基本类型nullable,因此还需要单独一组字段或者一个vector标识这些值是否为null
}
root_type FBSBar;
为了避免把生成的代码提交到代码仓库,需要将proto跟构建工具进行集成,目前似乎只有bazel构建工具有比较好的集成,别的构建工具如maven/gradle等似乎都没有比较好的集成方式。
因为生成的类不符合面向对象设计无法直接添加行为,同时已有系统里面已经有了需要被序列化的类型,因此也需要将已有类型的对象序列化成flatbuffer格式。Flatbuffer序列化代码不仅存在和Protobuf一样
代码冗长易出错难维护 问题,还存在以下问题:
代码不灵活、难写且极其容易出错。 由于flatbuffer在
序列化对象树时需要先深度优先和先序遍历整颗对象树,并手动保存每个变长字段的offset到临时状态,然后再序列化所有字段偏移或者内联标量值,这块代码写起来非常繁琐,一旦offset存储出现错误,序列化将会出现 assert/exception/panic等
报错,很难排查 。
list元素需要按照反向顺序进行序列化不符合直觉。由于buffer是从后往前构建,因此对于list,需要将元素逆向依次进行序列化。
不支持map类型们需要将map序列化为两个list或者序列化为一个table,这进一步带来了额外的开发成本。
下面是Java的序列化代码,大概需要100~150行;处理每个字段是否为null,大概还需要100行左右代码。因此Java序列化大概需要200~250行代码:
public static byte[] serialize(Bar bar) {
return buildBar(bar).sizedByteArray();
}
public static FlatBufferBuilder buildBar(Bar bar) {
// 这里忽略了空值处理的代码
FlatBufferBuilder builder = new FlatBufferBuilder();
int f2_offset = builder.createString(bar.f2);
int[] f3_offsets = new int[bar.f3.size()];
for (int i = 0; i < bar.f3.size(); i++) {
f3_offsets[i] = buildFoo(builder, bar.f3.get(i));
}
int f3_offset = FBSBar.createF3Vector(builder, f3_offsets);
int f4_key_offset;
int f4_value_offset;
{
int[] keys = new int[bar.f4.size()];
int[] valueOffsets = new int[bar.f4.size()];
int i = 0;
for (Map.Entry entry : bar.f4.entrySet()) {
keys[i] = entry.getKey();
valueOffsets[i] = buildFoo(builder, entry.getValue());
i++;
}
f4_key_offset = FBSBar.createF4KeyVector(builder, keys);
f4_value_offset = FBSBar.createF4ValueVector(builder, valueOffsets);
}
int f9_offset = FBSBar.createF9Vector(builder, bar.f9);
int f10_offset = FBSBar.createF10Vector(builder, bar.f10.stream().mapToLong(x -> x).toArray());
FBSBar.startFBSBar(builder);
FBSBar.addF1(builder, buildFoo(builder, bar.f1));
FBSBar.addF2(builder, f2_offset);
FBSBar.addF3(builder, f3_offset);
FBSBar.addF4Key(builder, f4_key_offset);
FBSBar.addF4Value(builder, f4_value_offset);
FBSBar.addF5(builder, bar.f5);
FBSBar.addF6(builder, bar.f6);
FBSBar.addF7(builder, bar.f7);
FBSBar.addF8(builder, bar.f8);
FBSBar.addF9(builder, f9_offset);
FBSBar.addF10(builder, f10_offset);
builder.finish(FBSBar.endFBSBar(builder));
return builder;
}
public static int buildFoo(FlatBufferBuilder builder, Foo foo) {
int stringOffset = builder.createString(foo.f1);
int[] keyOffsets = new int[foo.f2.size()];
int[] values = new int[foo.f2.size()];
int i = 0;
for (Map.Entry entry : foo.f2.entrySet()) {
keyOffsets[i] = builder.createString(entry.getKey());
values[i] = entry.getValue();
i++;
}
int keyOffset = FBSFoo.createF2KeyVector(builder, keyOffsets);
int f2ValueOffset = FBSFoo.createF2ValueVector(builder, values);
return FBSFoo.createFBSFoo(builder, stringOffset, keyOffset, f2ValueOffset);
}
public static Bar deserializeBar(ByteBuffer buffer) {
Bar bar = new Bar();
FBSBar fbsBar = FBSBar.getRootAsFBSBar(buffer);
bar.f1 = deserializeFoo(fbsBar.f1());
bar.f2 = fbsBar.f2();
{
ArrayList f3List = new ArrayList<>();
for (int i = 0; i < fbsBar.f3Length(); i++) {
f3List.add(deserializeFoo(fbsBar.f3(i)));
}
bar.f3 = f3List;
}
{
Map f4 = new HashMap<>();
for (int i = 0; i < fbsBar.f4KeyLength(); i++) {
f4.put(fbsBar.f4Key(i), deserializeFoo(fbsBar.f4Value(i)));
}
bar.f4 = f4;
}
bar.f5 = fbsBar.f5();
bar.f6 = fbsBar.f6();
bar.f7 = fbsBar.f7();
bar.f8 = fbsBar.f8();
{
short[] f9 = new short[fbsBar.f9Length()];
for (int i = 0; i < fbsBar.f9Length(); i++) {
f9[i] = fbsBar.f9(i);
}
bar.f9 = f9;
}
{
List f10 = new ArrayList<>();
for (int i = 0; i < fbsBar.f10Length(); i++) {
f10.add(fbsBar.f10(i));
}
bar.f10 = f10;
}
return bar;
}
public static Foo deserializeFoo(FBSFoo fbsFoo) {
Foo foo = new Foo();
foo.f1 = fbsFoo.string();
HashMap map = new HashMap<>();
foo.f2 = map;
for (int i = 0; i < fbsFoo.f2KeyLength(); i++) {
map.put(fbsFoo.f2Key(i), fbsFoo.f2Value(i));
}
return foo;
}
Python序列化代码:大概200~250行
GoLang序列化代码:大概200~250行
对比Msgpack Msgpack Java和Python并不支持自定义类型序列化,需要用户增加扩展类型手动进行序列化,因此这里省略。
性能比较(数值越小越好) 这里给出在纯Java序列化场景对比其它框架的性能测试结果。其它语言的性能测试将在后续文章当中发布。
测试环境:
测试原则:
自定义类型序列化测试数据使用的是kryo-benchmark 的数据,保证测试结果对Fury没有任何偏向性。尽管Kryo测试数据里面有大量基本类型数组,为了保证测试的公平性我们并没有开启Fury的Out-Of-Band零拷贝序列化能力。然后使用我们自己创建的对象单独准备了一组零拷贝测试用例。
测试工具:
为了避免JVM JIT给测试带来的影响,我们使用JMH 工具进行测试,每组测试在五个子进程依次进行,保证不受到进程CPU调度的影响,同时每个进程里面执行三组Warmup和5组正式测试,保证不会受到偶然的环境波动影响。
下面是我们使用JMH测试fury/kryo/fst/hessian/protostuff/jdk序列化框架在序列化到堆内存和堆外内存时的性能(数值越小越好)。
自定义类型性能对比 Struct Struct类型主要是有纯基本类型的字段组成,对于这类对象,Fury通过JIT等技术,可以达到Kryo 20倍 的性能。
public class Struct implements Serializable {
int f1;
long f2;
float f3;
double f4;
...
int f97;
long f98;
float f99;
double f100;
}
序列化:
反序列化:
Sample Sample类型主要由基本类型、装箱类型、字符串和数组等类型字段组成,对于这种类型的对象,Fury的性能可以达到Kryo的6~7倍。没有更快的原因是因为这里的多个基本类型数组需要进行拷贝,这块占用一定的耗时。如果使用Fury的Out-Of-Band序列化的话。这些额外的拷贝就可以完全避免掉,但这样比较不太公平,因此这里没有开启。
public final class Sample implements Serializable {
public int intValue;
public long longValue;
public float floatValue;
public double doubleValue;
public short shortValue;
public char charValue;
public boolean booleanValue;
public Integer IntValue;
public Long LongValue;
public Float FloatValue;
public Double DoubleValue;
public Short ShortValue;
public Character CharValue;
public Boolean BooleanValue;
public int[] intArray;
public long[] longArray;
public float[] floatArray;
public double[] doubleArray;
public short[] shortArray;
public char[] charArray;
public boolean[] booleanArray;
public String string; // Can be null.
public Sample sample; // Can be null.
public Sample() {}
public Sample populate(boolean circularReference) {
intValue = 123;
longValue = 1230000;
floatValue = 12.345f;
doubleValue = 1.234567;
shortValue = 12345;
charValue = '!';
booleanValue = true;
IntValue = 321;
LongValue = 3210000L;
FloatValue = 54.321f;
DoubleValue = 7.654321;
ShortValue = 32100;
CharValue = '$';
BooleanValue = Boolean.FALSE;
intArray = new int[] {-1234, -123, -12, -1, 0, 1, 12, 123, 1234};
longArray = new long[] {-123400, -12300, -1200, -100, 0, 100, 1200, 12300, 123400};
floatArray = new float[] {-12.34f, -12.3f, -12, -1, 0, 1, 12, 12.3f, 12.34f};
doubleArray = new double[] {-1.234, -1.23, -12, -1, 0, 1, 12, 1.23, 1.234};
shortArray = new short[] {-1234, -123, -12, -1, 0, 1, 12, 123, 1234};
charArray = "asdfASDF".toCharArray();
booleanArray = new boolean[] {true, false, false, true};
string = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
if (circularReference) {
sample = this;
}
return this;
}
}
序列化耗时:
反序列化耗时:
MediaContent 对于MediaContent这类包含大量String的数据结构,Fury性能大概是Kryo的4~5倍 。没有更快的原因是因为String序列化开销比较大 ,部分摊平了Fury JIT带来的性能提升。用户如果对String序列化有更好的性能要求的话,可以使用Fury的String零拷贝序列化协议 ,在序列化时直接把String内部的Buffer抽取出来,然后直接放到Out-Of-Band buffer里面,完全避免掉String序列化的开销 。
public final class Media implements java.io.Serializable {
public String uri;
public String title; // Can be null.
public int width;
public int height;
public String format;
public long duration;
public long size;
public int bitrate;
public boolean hasBitrate;
public List persons;
public Player player;
public String copyright; // Can be null.
public Media() {}
public enum Player {
JAVA,
FLASH;
}
}
public final class MediaContent implements java.io.Serializable {
public Media media;
public List images;
public MediaContent() {}
public MediaContent(Media media, List images) {
this.media = media;
this.images = images;
}
public MediaContent populate(boolean circularReference) {
media = new Media();
media.uri = "http://javaone.com/keynote.ogg";
media.width = 641;
media.height = 481;
media.format = "video/theora\u1234";
media.duration = 18000001;
media.size = 58982401;
media.persons = new ArrayList();
media.persons.add("Bill Gates, Jr.");
media.persons.add("Steven Jobs");
media.player = Media.Player.FLASH;
media.copyright = "Copyright (c) 2009, Scooby Dooby Doo";
images = new ArrayList();
Media media = circularReference ? this.media : null;
images.add(
new Image(
"http://javaone.com/keynote_huge.jpg",
"Javaone Keynote\u1234",
32000,
24000,
Image.Size.LARGE,
media));
images.add(
new Image(
"http://javaone.com/keynote_large.jpg", null, 1024, 768, Image.Size.LARGE, media));
images.add(
new Image("http://javaone.com/keynote_small.jpg", null, 320, 240, Image.Size.SMALL, media));
return this;
}
}
序列化耗时:
反序列化耗时:
Buffer零拷贝性能对比 基本类型数组 对于基本类型可以看到Fury序列化几乎耗时为0,而别的框架耗时随着数组大小线性增加。
反序列时Fury耗时也会线性增加是因为需要把Buffer拷贝到Java基本类型数组里面。
public class ArraysData implements Serializable {
public boolean[] booleans;
public byte[] bytes;
public int[] ints;
public long[] longs;
public double[] doubles;
public ArraysData() {}
public ArraysData(int arrLength) {
booleans = new boolean[arrLength];
bytes = new byte[arrLength];
ints = new int[arrLength];
longs = new long[arrLength];
doubles = new double[arrLength];
Random random = new Random();
random.nextBytes(bytes);
for (int i = 0; i < arrLength; i++) {
booleans[i] = random.nextBoolean();
ints[i] = random.nextInt();
longs[i] = random.nextLong();
doubles[i] = random.nextDouble();
}
}
}
序列化耗时:
反序列耗时:
堆外Buffer 除了基本类型数组,我们也测试了Java ByteBuffer的序列化性能。由于Kryo和Fst并不支持ByteBuffer序列化,同时并没有提供直接读写ByteBuffer的接口,因此我们使用了byte array来模拟内存拷贝。可以看到对于堆外Buffer,Fury的序列化和反序列化耗时都是一个常量,不随Buffer大小而增加。
序列化耗时:
反序列化耗时:
总结 Fury最早是我在2019年开发,当时是为了支持分布式计算框架Ray的跨语言序列化以及蚂蚁在线学习场景样本流的跨语言传输问题。目前已经在蚂蚁在线学习、运筹优化、Serving等多个场景稳定运行多年。
从我个人来看,Fury主要优势主要是:
跨语言原生序列化 ,大幅提高了跨语言序列化的易用性;
通过JIT技术来优化序列化性能 。这里也可以看到通过把数据库和大数据领域的代码生成思想用在序列化上面是一个很好的方向,可以取得非常显著的性能提升;
Zero-Copy序列化 ,避免所有不必要的内存拷贝;
多语言行存支持避免序列化和元数据开销 ;
希望接下来能够有更多场景使用到Fury的能力,同时希望可以听到更多来自用户的反馈。
合作共建 多语言的支持与生态建设是一项复杂的工作,我们希望能够拉通相关的中间件和分布式引擎团队在这块合作共建,共同打造业界新一代的序列化框架,为公司和开源社区带来更大的价值。接下来我们会在协议、框架和生态三个方面继续优化,希望感兴趣的同学可以一块参与进来 :
协议层面
JIT代码生成支持数据压缩模式
通过SIMD向量化指令进行大规模数据压缩
框架层面
更多Java序列化代码JIT化;
完善C++支持,通过使用Macro、模板和编译时反射在编译时注册捕获Fury需要的类型信息,实现自动C++序列化;
通过Golang-ASM支持基于JIT的Golang序列化实现;
通过将更多Python代码Cython化来进一步加速Python序列化;
支持JavaScript,打通NodeJS生态;
支持Rust;
与RPC框架SOFA、Dubbo、HSF等集成
与分布式计算框架Spark和Flink等集成
Fury代码仓库地址是https://code.alipay.com/ray-project/fury ,欢迎感兴趣的同学一起共建。
关于开源 Fury目前已经在蚂蚁内源:https://innersource.alipay.com/innersource/project/Fury 。 我们希望通过蚂蚁内源建立一个完善的可持续运营的开发者社区,非常欢迎来自大家的贡献 。接下来我们也会走开源流程,如果有开源社区使用场景非常欢迎私聊 @慕白 !!!
联系我们 如果想进一步了解Fury,或者对Fury有任何使用问题,欢迎钉钉私聊和通过下方二维码加入Fury用户群(群号:35683646):
致谢 整个Fury的开发,离不开小伙伴们的支持。
感谢Ray团队的同学@不涸 、@林濯 、@帛言 、@墨开 、@上霄 、@怜心 、@留宝 、@清乌 、@黑驰 、@楚风 的大力支持。
感谢各位老板@若夷 、@天苍 、@朔之 的大力支持。
感谢蚂蚁内源团队@边澄 、@花肉 、@蓝背 、@李莉 在整个项目孵化过程当中给出的大量帮助。
感谢设计同学@李娜 帮助设计了Fury的logo。
Fury系列相关文章