简介: Fury是一个基于JIT动静编译的多语言原生序列化框架,反对Java/Python/Golang/C++等语言,提供全自动的对象多语言/跨语言序列化能力,以及相比于别的框架最高20~200倍的性能。
作者 | 杨朝坤(慕白)起源 | 阿里开发者公众号Fury是一个基于JIT动静编译的多语言原生序列化框架,反对Java/Python/Golang/C++等语言,提供全自动的对象多语言/跨语言序列化能力,以及相比于别的框架最高20~200倍的性能。引言过来十多年大数据和分布式系统蓬勃发展,序列化是其频繁应用的技术。当对象须要跨过程、跨语言、跨节点传输、长久化、状态读写时,都须要进行序列化,其性能和易用性影响着零碎的运行效率和开发效率。对于Java序列化,只管Kryo[1]等框架提供了相比JDK序列化数倍的性能,对于高吞吐、低提早、大规模数据传输场景,序列化依然是整个零碎的性能瓶颈。为了优化序列化的性能,分布式系统如Spark[2]、Flink[3]应用了专有行列存二进制格局如tungsten[4]和arrow[5]。这些格局缩小了序列化开销,但减少了零碎的复杂性,就义了编程的灵活性,同时也只笼罩了SQL等关系代数计算专有场景。对于通用分布式编程和跨过程通信,序列化性能始终是一个绕不过来的关键问题。同时随着计算和利用场景的日益复杂化,零碎曾经从繁多语言的编程范式倒退到多语言交融编程,对象在语言之间传输的易用性影响着零碎开发效率,进而影响业务的迭代效率。而已有的跨语言序列化框架protobuf/flatbuffer/msgpack等因为无奈反对援用、不反对Zero-Copy、大量手写代码以及生成的类不合乎面向对象设计[6]无奈给类增加行为,导致在易用性、灵活性、动态性和性能上的有余,并不能满足通用跨语言编程需要。基于此,咱们开发了Fury,通过一套反对援用、类型嵌入的语言无关协定,以及JIT动静编译减速、缓存优化和Zero-Copy等技术,实现了任意对象像动静语言主动序列化一样跨语言主动序列化,打消了语言之间的编程边界,并提供相比于业界别的框架最高20~200倍的性能。
Fury是什么Fury是一个基于JIT的高性能多语言原生序列化框架,专一于提供极致的序列化性能和易用性:反对支流编程语言如Java/Python/C++/Golang,其它语言可轻易扩大;多语言/跨语言主动序列化任意对象,无需创立IDL文件、手动编译schema生成代码以及将对象转换为两头格局;多语言/跨语言主动序列化共享援用和循环援用,用户只须要关怀对象,不须要关怀数据反复或者递归谬误;基于JIT动静编译技术在运行时主动生成序列化代码优化性能,减少办法内联、代码缓存和死代码打消,缩小虚办法调用/条件分支/Hash查找/元数据写入等,提供相比其它序列化框架20~200倍以上的性能;Zero-Copy序列化反对,反对Out of band序列化协定,反对堆外内存读写;提供缓存敌对的二进制随机拜访行存格局,反对跳过序列化和局部序列化,并能和列存主动互转;除了跨语言能力,Fury还具备以下能力:无缝代替JDK/Kryo/Hessian等Java序列化框架,无需批改任何代码,同时提供相比Kryo 20倍以上的性能,相比Hessian100倍以上的性能,相比JDK自带序列化200倍以上的性能,能够大幅晋升高性能场景RPC调用和对象长久化效率;反对共享援用和循环援用的Golang序列化框架;反对对象主动序列化的Golang序列化框架;
目前Fury曾经反对Java、Python、Golang以及C++。本文将首先简略介绍如何应用Fury,而后将Fury跟别的序列化框架进行性能、性能和易用性比拟,Fury的实现原理将在后续文章外面具体介绍。如何应用Fury这里给出跨语言序列化、纯Java序列化以及防止序列化的示例:跨语言序列化自定义类型跨语言序列化蕴含循环援用的自定义类型跨语言零拷贝序列化Drop-in代替Kryo/Hession/JDK序列化通过Fury Format防止序列化序列化自定义类型上面是序列化用户自定义类型的一个示例,该类型外面蕴含多个根本类型以及嵌套类型的字段,在业务利用外面相当常见。须要留神自定义类型跨语言序列化之前须要调用registerAPI注册自定义类型,建设类型在不同语言之间的映射关系,同时保障GoLang等动态语言编译器编译代码时不裁剪掉这部分类型的符号。Java序列化示例import com.google.common.collect.*;
import io.fury.*;
import java.util.*;
public class CustomObjectExample {
public static class SomeClass1 {
Object f1;Map<Byte, Integer> f2;
}
public static class SomeClass2 {
Object f1;String f2;List< Object> f3;Map< Byte, Integer> f4;Byte f5;Short f6;Integer f7;Long f8;Float f9;Double f10;short[] f11;List< Short> f12;
}
public static Object createObject() {
SomeClass1 obj1 = new SomeClass1();obj1.f1 = true;obj1.f2 = ImmutableMap.of((byte) -1, 2);SomeClass2 obj = new SomeClass2();obj.f1 = obj1;obj.f2 = "abc";obj.f3 = Arrays.asList("abc", "abc");obj.f4 = ImmutableMap.of((byte) 1, 2);obj.f5 = Byte.MAX_VALUE;obj.f6 = Short.MAX_VALUE;obj.f7 = Integer.MAX_VALUE;obj.f8 = Long.MAX_VALUE;obj.f9 = 1.0f / 2;obj.f10 = 1 / 3.0;obj.f11 = new short[] {(short) 1, (short) 2};obj.f12 = ImmutableList.of((short) -1, (short) 4);return obj;
}
}纯Java序列化:public class CustomObjectExample {
// mvn exec:java -Dexec.mainClass="io.fury.examples.CustomObjectExample"
public static void main(String[] args) {
// Fury应该在多个对象序列化之间复用,不要每次创立新的Fury实例Fury fury = Fury.builder().withLanguage(Language.JAVA) .withReferenceTracking(false) .withClassRegistrationRequired(false) .build();byte[] bytes = fury.serialize(createObject());System.out.println(fury.deserialize(bytes));;
}
}跨语言序列化:public class CustomObjectExample {
// mvn exec:java -Dexec.mainClass="io.fury.examples.CustomObjectExample"
public static void main(String[] args) {
// Fury应该在多个对象序列化之间复用,不要每次创立新的Fury实例Fury fury = Fury.builder().withLanguage(Language.XLANG) .withReferenceTracking(false).build();fury.register(SomeClass1.class, "example.SomeClass1");fury.register(SomeClass2.class, "example.SomeClass2");byte[] bytes = fury.serialize(createObject());// bytes can be data serialized by other languages.System.out.println(fury.deserialize(bytes));;
}
}Python序列化示例from dataclasses import dataclass
from typing import List, Dict
import pyfury
@dataclass
class SomeClass2:
f1: Any = Nonef2: str = Nonef3: List[str] = Nonef4: Dict[pyfury.Int8Type, pyfury.Int32Type] = Nonef5: pyfury.Int8Type = Nonef6: pyfury.Int16Type = Nonef7: pyfury.Int32Type = None# int类型默认会依照long类型进行序列化,如果对端是更加narrow的类型,# 须要应用pyfury.Int32Type等进行标注f8: int = None # 也能够应用pyfury.Int64Type进行标注f9: pyfury.Float32Type = Nonef10: float = None # 也能够应用pyfury.Float64Type进行标注f11: pyfury.Int16ArrayType = Nonef12: List[pyfury.Int16Type] = None
@dataclass
class SomeClass1:
f1: Anyf2: Dict[pyfury.Int8Type, pyfury.Int32Type]
if name == "__main__":
fury_ = pyfury.Fury(reference_tracking=False)fury_.register_class(SomeClass1, "example.SomeClass1")fury_.register_class(SomeClass2, "example.SomeClass2")obj2 = SomeClass2(f1=True, f2={-1: 2})obj1 = SomeClass1( f1=obj2, f2="abc", f3=["abc", "abc"], f4={1: 2}, f5=2 ** 7 - 1, f6=2 ** 15 - 1, f7=2 ** 31 - 1, f8=2 ** 63 - 1, f9=1.0 / 2, f10=1 / 3.0, f11=array.array("h", [1, 2]), f12=[-1, 4],)data = fury_.serialize(obj)# bytes can be data serialized by other languages.print(fury_.deserialize(data))GoLang序列化示例package main
import "code.alipay.com/ray-project/fury/go/fury"
import "fmt"
func main() {
type SomeClass1 struct {
F1 interface{}F2 stringF3 []interface{}F4 map[int8]int32F5 int8F6 int16F7 int32F8 int64F9 float32F10 float64F11 []int16F12 fury.Int16Slice
}
type SomeClas2 struct {
F1 interface{}F2 map[int8]int32
}
fury_ := fury.NewFury(false)if err := fury_.RegisterTagType("example.SomeClass1", SomeClass1{}); err != nil { panic(err)}if err := fury_.RegisterTagType("example.SomeClass2", SomeClass2{}); err != nil { panic(err)}
obj2 := &SomeClass2{}
obj2.F1 = true
obj2.F2 = map[int8]int32{-1: 2}
obj := &SomeClass1{}
obj.F1 = obj2
obj.F2 = "abc"
obj.F3 = []interface{}{"abc", "abc"}
f4 := map[int8]int32{1: 2}
obj.F4 = f4
obj.F5 = fury.MaxInt8
obj.F6 = fury.MaxInt16
obj.F7 = fury.MaxInt32
obj.F8 = fury.MaxInt64
obj.F9 = 1.0 / 2
obj.F10 = 1 / 3.0
obj.F11 = []int16{1, 2}
obj.F12 = []int16{-1, 4}
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)
}序列化共享&循环援用共享援用和循环援用是程序外面常见的结构,很多数据结构如图都蕴含大量的循环援用,而手动实现这些蕴含共享援用和循环援用的对象,须要大量简短简单易出错的代码。跨语言序列化框架反对循环援用能够极大简化这些简单场景的序列化,减速业务迭代效率。上面是一个蕴含循环援用的自定义类型跨语言序列化示例。Java序列化示例import com.google.common.collect.ImmutableMap;
import io.fury.*;
import java.util.Map;
public class ReferenceExample {
public static class SomeClass { SomeClass f1; Map< String, String> f2; Map< String, String> f3;}public static Object createObject() { SomeClass obj = new SomeClass(); obj.f1 = obj; obj.f2 = ImmutableMap.of("k1", "v1", "k2", "v2"); obj.f3 = obj.f2; return obj;}
}Java序列化:public class ReferenceExample {
// mvn exec:java -Dexec.mainClass="io.fury.examples.ReferenceExample"public static void main(String[] args) { // Fury应该在多个对象序列化之间复用,不要每次创立新的Fury实例 Fury fury = Fury.builder().withLanguage(Language.JAVA) .withReferenceTracking(true) .withClassRegistrationRequired(false) .build(); byte[] bytes = fury.serialize(createObject()); System.out.println(fury.deserialize(bytes));;}
}跨语言序列化:public class ReferenceExample {
// mvn exec:java -Dexec.mainClass="io.fury.examples.ReferenceExample"public static void main(String[] args) { // Fury应该在多个对象序列化之间复用,不要每次创立新的Fury实例 Fury fury = Fury.builder().withLanguage(Language.XLANG) .withReferenceTracking(true).build(); fury.register(SomeClass.class, "example.SomeClass"); byte[] bytes = fury.serialize(createObject()); // bytes can be data serialized by other languages. System.out.println(fury.deserialize(bytes));;}
}Python序列化示例from typing import Dict
import pyfury
class SomeClass:
f1: "SomeClass"f2: Dict[str, str]f3: Dict[str, str]
if name == "__main__":
fury_ = pyfury.Fury(reference_tracking=True)fury_.register_class(SomeClass, "example.SomeClass")obj = SomeClass()obj.f2 = {"k1": "v1", "k2": "v2"}obj.f1, obj.f3 = obj, obj.f2data = fury_.serialize(obj)# bytes can be data serialized by other languages.print(fury_.deserialize(data))Golang序列化示例package main
import "code.alipay.com/ray-project/fury/go/fury"
import "fmt"
func main() {
type SomeClass struct { F1 *SomeClass F2 map[string]string F3 map[string]string}fury_ := fury.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.F2value.F1 = valuebytes, 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)
}Zero-Copy序列化对于大规模数据传输场景,内存拷贝有时会成为整个零碎的瓶颈。为此各种语言和框架做了大量优化,比方Java提供了NIO能力,防止了内存在用户态和内核态之间的来回拷贝;Kafka应用Java的NIO来实现零拷贝;Python Pickle5提供了Out-Of-Band Buffer[7]序列化能力来防止额定拷贝。对于高性能跨语言数据传输,序列化框架也须要可能反对Zero-Copy,防止数据Buffer的额定拷贝。上面是一个Fury序列化多个根本类型数组组成的对象树的示例,别离对应到Java根本类型数组、Python Numpy数组、Golang 根本类型slice。对于ByteBuffer零拷贝,在本文的性能测试局部也给出了局部介绍。Java序列化示例Java序列化import io.fury.*;
import io.fury.serializers.BufferObject;
import io.fury.memory.MemoryBuffer;
import java.util.*;
import java.util.stream.Collectors;
public class ZeroCopyExample {
// mvn exec:java -Dexec.mainClass="io.fury.examples.ZeroCopyExample"
public static void main(String[] args) {
// Fury应该在多个对象序列化之间复用,不要每次创立新的Fury实例Fury fury = Fury.builder() .withLanguage(Language.JAVA) .withClassRegistrationRequired(false) .build();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));
}
}跨语言序列化:import io.fury.*;
import io.fury.serializers.BufferObject;
import io.fury.memory.MemoryBuffer;
import java.util.*;
import java.util.stream.Collectors;
public class ZeroCopyExample {
// mvn exec:java -Dexec.mainClass="io.fury.examples.ZeroCopyExample"
public static void main(String[] args) {
Fury fury = Fury.builder().withLanguage(Language.XLANG).build();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));// bytes can be data serialized by other languages.List< MemoryBuffer> 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-copylist := []interface{}{"str", make([]byte, 1000)}buf := fury.NewByteBuffer(nil)var serializedObjects []fury.SerializedObjectfury.Serialize(buf, list, func(o fury.SerializedObject) bool { serializedObjects = append(serializedObjects, o) return false})var newList []interface{}var buffers []*fury.ByteBufferfor _, o := range serializedObjects { buffers = append(buffers, o.ToBuffer())}err := fury.Deserialize(buf, &newList, buffers)fmt.Println(newList)Drop-in替换Kryo/Hession除了多语言原生序列化以外,Fury还是一个高性能的通用Java序列化框架,能够序列化任意Java Object,齐全兼容JDK序列化,包含反对序列化自定义writeObject/readObject/writeReplace/readResolve的对象,反对堆内/堆外内存。能够Drop-in替换jdk/kryo/hession等序列化框架,性能最高是Kryo 20倍以上,Hession100倍以上,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防止序列化对于有极致性能要求的场景,如果用户只须要读取局部数据,或者在Serving场景依据对象树某个字段进行过滤和转发,能够应用Fury Format来防止其它字段的序列化。Fury Row Format是参考SQL行存和Arrow列存实现的一套能够随机拜访的二进制行存构造。目前实现了Java/Python/C++版本,Python版本通过Cython绑定到C++实现。因为该格局是自蕴含的,能够依据schema间接计算出任意字段的offset。因而通过应用该格局,能够防止掉序列化,间接在二进制数据buffer下面进行所有读写操作,这样做有三个劣势:缩小Java GC overhead。因为防止了反序列化,因而不会创建对象,从而防止了GC问题。防止Python反序列化。Python性能始终很慢,因而在跨语言序列化时,能够在Java/C++侧把对象序列化成Row-Format,而后Python侧间接应用该数据计算,这样就防止了Python反序列化的低廉开销。同时因为Python的动态性,Fury的BinaryRow/BinaryArrays实现了_getattr__/__getitem__/slice/和其它special methods,保障了行为跟python pojo/list/object的一致性,用户没有任何感知。缓存敌对,数据密集存储。Python示例这里给出一个读取局部数据的样例以及性能测试后果。在上面这个序列化场景中,须要读取第二个数组字段的第10万个元素,Fury耗时简直为0,而pickler须要8秒。@dataclass
class Bar:
f1: strf2: List[pa.int64]
@dataclass
class Foo:
f1: pa.int32f2: 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示例public class Bar {
String f1;
List<Long> f2;
}
public class Foo {
int f1;
List< Integer> f2;
Map< String, Integer> f3;
List< Bar> f4;
}
Encoder< Foo> encoder = Encoders.rowEncoder(Foo.class);
BinaryRow binaryRow = encoder.toRow(foo); // 该数据能够被Python零拷贝解析
Foo newFoo = encoder.fromRow(binaryRow); // 能够是来自python序列化的数据
BinaryArray binaryArray2 = binaryRow.getArray(1); // 零拷贝读取List< Integer> f2字段
BinaryArray binaryArray4 = binaryRow.getArray(4); // 零拷贝读取List< Bar> f4字段
BinaryRow barStruct = binaryArray4.getStruct(10);// 零拷贝读取读取List< Bar> f4第11个元素数据
// 零拷贝读取读取List< Bar> f4第11个元素数据的f2字段的第6个元素
long aLong = barStruct.getArray(1).getLong(5);
Encoder< Bar> 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< Bar> 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;主动转换ArrowFury Format反对主动与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< ArrowWriter> 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< BeanA> 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。
性能比拟(数值越小越好)这里给出在纯Java序列化场景比照其它框架的性能测试后果。其它语言的性能测试将在后续文章当中公布。测试环境:操作系统:4.9.151-015.ali3000.alios7.x86_64CPU型号:Intel(R) Xeon(R) Platinum 8163 CPU @ 2.50GHzByte Order:Little EndianL1d cache:32KL1i cache:32KL2 cache:1024KL3 cache:33792K测试准则:自定义类型序列化测试数据应用的是kryo-benchmark[8]的数据,保障测试后果对Fury没有任何偏差性。只管Kryo测试数据外面有大量根本类型数组,为了保障测试的公平性咱们并没有开启Fury的Out-Of-Band零拷贝序列化能力。而后应用咱们本人创立的对象独自筹备了一组零拷贝测试用例。测试工具:为了防止JVM JIT给测试带来的影响,咱们应用JMH[9]工具进行测试,每组测试在五个子过程顺次进行,防止受到过程CPU调度的影响,同时每个过程外面执行三组Warmup和5组正式测试,防止受到偶尔的环境稳定影响。上面是咱们应用JMH测试fury/kryo/fst/hession/protostuff/jdk序列化框架在序列化到堆内存和堆外内存时的性能(数值越小越好)。自定义类型性能比照StructStruct类型次要是有纯根本类型的字段组成,对于这类对象,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;
}序列化:
反序列化:
SampleSample类型次要由根本类型、装箱类型、字符串和数组等类型字段组成,对于这种类型的对象,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< String> 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< Image> images;
public MediaContent() {}
public MediaContent(Media media, List< Image> 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大小而减少。序列化耗时:
反序列化耗时:
易用性比拟这里以一个自定义类型为例比照易用性,该类型蕴含常见根本类型字段以及汇合类型字段,最终须要序列化的对象是一个Bar的实例:class Foo {
String f1;
Map< String, Integer> f2;
}
class Bar {
Foo f1;
String f2;
List< Foo> f3;
Map< Integer, Foo> f4;
Integer f5;
Long f6;
Float f7;
Double f8;
short[] f9;
List< Long> 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首先须要装置protoc编译器[10],留神protoc的版本不能高于proto依赖库的版本而后定义针对须要序列化的对象的schema: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< string, int32> f2 = 2;
}
message Bar {
optional Foo f1 = 1;
optional string f2 = 2;
repeated Foo f3 = 3;
map< int32, Foo> 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;
}而后通过protoc编译schema生成Java/Python/GoLang代码文件。java: protoc --experimental_allow_proto3_optional -I=src/main/java/io/ray/fury/benchmark/state --java_out=src/main/java/ bench.protobench.proto生成Python/GoLang代码为了防止把生成的代码提交到代码仓库,须要将proto跟构建工具进行集成,这块较为简单,存在大量构建工具集成老本。且因为构建工具的不欠缺,这部分仍然无奈齐全自动化,比方protobuf-maven-plugin[11]仍然须要用户在机器装置protoc,而不是主动下载protoc。因为大部分场景都是用户曾经有了自定义类型和根本类型以及组合类型形成的对象(树)须要被序列化,因而须要将用户类型对象转换成protobuf格局。这外面就有较大的开发成本,且每种须要都须要写一遍,代码简短且易出错难保护,同时还存在大量数据转换和拷贝开销。另外转换过程没有思考理论类型,因而还存在类型失落的问题,比方LinkedList反序列化回来变成了ArrayList。上面是Java的序列化代码,大略须要130~150行。 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并不合乎面向对象设计[12],无奈给生成的class增加行为。这时候就须要定义额定的wrapper,如果主动外部有其它自定义类型,还须要将这些类型转换成对应的wrapper,这进一步限度了应用的灵活性。比照FlatbufferFlatbuffer与protobuf一样,也须要大量的学习老本和开发成本:装置flatc编译器[13],对于Linux环境,可能还须要进行源码编译装置flatc。定义Schemanamespace 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;而后通过flatc编译schema生成Java/Python/GoLang代码文件。java: flatc -I=src/main/java/io/ray/fury/benchmark/state -o=src/main/java/ bar.fbs生成Python/GoLang代码为了防止把生成的代码提交到代码仓库,须要将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< Integer, Foo> 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< String, Integer> 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< Foo> f3List = new ArrayList<>();for (int i = 0; i < fbsBar.f3Length(); i++) { f3List.add(deserializeFoo(fbsBar.f3(i)));}bar.f3 = f3List;
}
{
Map< Integer, Foo> 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< Long> 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< String, Integer> 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即将Flatbuffer生成类型包装到其它合乎面向对象设计的类外面:因为Flatbuffer序列化过程须要保留大量两头offset,且须要先把所有可变长度对象写入buffer,因而通过wrapper批改flatbuffer数据会比较复杂,使得包装Flatbuffer生成类型只适宜反序列化读数据过程,导致增加wrapper也变得很艰难。比照MsgpackMsgpack Java和Python并不反对自定义类型序列化,须要用户减少扩大类型手动进行序列化,因而这里省略。总结Fury最早是我在2019年开发,过后是为了反对分布式计算框架Ray[14]的跨语言序列化以及蚂蚁在线学习场景样本流的跨语言传输问题。通过蚂蚁丰盛业务场景的打磨,目前曾经在蚂蚁在线学习、运筹优化、Serving等多个计算场景稳固运行多年。总体来看Fury次要劣势次要是:跨语言原生序列化,大幅提高了跨语言序列化的易用性,升高研发老本;通过JIT技术来优化序列化性能。这里也能够看到通过把数据库和大数据畛域的代码生成思维用在序列化下面是一个很好的思路,能够获得十分显著的性能晋升;Zero-Copy序列化,防止所有不必要的内存拷贝;多语言行存反对防止序列化和元数据开销;将来咱们会在协定、框架和生态三个方面持续优化:协定层面JIT代码生成反对数据压缩模式进一步通过SIMD向量化指令进行大规模数据压缩框架层面更多Java序列化代码JIT化;欠缺C++反对,通过应用Macro、模板和编译时反射在编译时注册捕捉Fury须要的类型信息,实现主动C++序列化;通过Golang-ASM反对基于JIT的Golang序列化实现;通过将更多Python代码Cython化来进一步减速Python序列化;反对JavaScript,买通NodeJS生态;反对Rust;生态层面与RPC框架SOFA、Dubbo、Akka等集成与分布式计算框架Spark和Flink等集成多语言的反对与生态建设是一项简单的工作,接下来咱们会尽快开源Fury,吸引感兴趣的同学一起参加进来。如果有开源应用场景或者合作意向,欢送通过邮箱chaokun.yck@antgroup.com 交换。参考链接:[1]https://github.com/EsotericSo...[2]https://spark.apache.org/docs...[3]https://flink.apache.org/[4]https://databricks.com/blog/2...[5]https://arrow.apache.org/[6]https://developers.google.com...[7]https://peps.python.org/pep-0574[8]https://github.com/EsotericSo...[9]https://openjdk.org/projects/...[10]https://developers.google.com...[11]https://www.xolstice.org/prot...[12]https://developers.google.com...[13]https://github.com/google/fla...[14]https://github.com/ray-projec... 重磅来袭!2022上半年阿里云社区最热电子书榜单! 千万浏览量、百万下载量、上百本电子书,近200位阿里专家参加编写。多元化抉择、全畛域笼罩,汇聚阿里巴巴技术实际精髓,读、学、练一键三连。开发者藏经阁,开发者的工作伴侣~点击这里,查看详情。
原文链接:https://click.aliyun.com/m/10...本文为阿里云原创内容,未经容许不得转载。