Fury系列(四):一个比Kryo/Hessian快30~40倍的类型前后兼容序列化器

简介: 问题背景类型前后兼容是复杂业务场景序列化的常见需求。在快速迭代的业务场景当中,读写端经常发生对象字段发生变更:在线应用场景:线上SOFA/HSF应用提供服务给多个调用方,服务的滚动升级以及各个调用方独立更新都可能导致对象类型不一致的情况;在线服务场景:在线服务框架常驻不更改对象类型,但调用方业务逻辑变动独立更新导致对象字段跟服务端不一致;对象持久化场景:对象数据序列化后持久化写入存储(如Spark

问题背景

类型前后兼容是复杂业务场景序列化的常见需求。在快速迭代的业务场景当中,读写端经常发生对象字段发生变更

  • 在线应用场景:线上SOFA/HSF应用提供服务给多个调用方,服务的滚动升级以及各个调用方独立更新都可能导致对象类型不一致的情况;
  • 在线服务场景:在线服务框架常驻不更改对象类型,但调用方业务逻辑变动独立更新导致对象字段跟服务端不一致;
  • 对象持久化场景:对象数据序列化后持久化写入存储(如Spark RDD提供的saveAsObjectFile API),对象类型发生变化后从存储反序列化旧的数据;

序列化框架需要处理这类序列化端和反序列化端对象字段不一致的情况,当序列化端发生字段变更时,无需更新反序列化端代码即可正确反序列化(向前兼容);当反序列化端发生字段变更时,无需更新序列化端的代码也可以正确反序列化(向后兼容)。

目前Hessian和Kryo都提供了类型前后兼容的序列化器,但都存在显著的性能瓶颈,并不能满足复杂业务场景的性能需求。Fury在0.9.0版本全新发布了一个类型前后兼容的序列化器,提供相比Kryo/Hessian100 30~40倍以上的性能,大幅提升此类场景的性能,将在线系统的性能带到新的巅峰。

如何使用

安装Maven依赖:


  io.fury
  fury-core
  0.9.0


创建Fury:

Fury fury = Fury.builder()
    .withLanguage(Language.JAVA)
    .withReferenceTracking(true)
    .disableSecureMode()
    .withCompatibleMode(CompatibleMode.COMPATIBLE)
    .build();

创建多线程安全的Fury:

ThreadSafeFury fury = Fury.builder()
    .withLanguage(Language.JAVA)
    .withReferenceTracking(true)
    .disableSecureMode()
    .withCompatibleMode(CompatibleMode.COMPATIBLE)
    .buildThreadSafeFury();

进行序列化和反序列化:

byte[] data = fury.serialzie(xxx);
Object obj = fury.deserialize(data);

性能对比结果(越小越好)

性能测试使用Java标准的基准测试工具JMH,每组测试在五个子进程依次进行,保证不受到进程CPU调度的影响,同时每个进程里面执行三组Warmup保证JVM JIT完全触发,然后进行5组正式测试,保证不会受到偶然的环境波动影响。测试数据分别是jvm-serializers的MediaContent,以及一百个基本类型字段的Struct:

Lib

Benchmark

Mode

Samples

Unit

bufferType

objectType

references

Tps

Fury_deserialize

compatible

thrpt

9

ops/s

array

STRUCT

False

3550792.14454

Hession_deserialize

compatible

thrpt

9

ops/s

array

STRUCT

False

107392.928574

Kryo_deserialize

compatible

thrpt

9

ops/s

array

STRUCT

False

106094.957831

Fury_deserialize

compatible

thrpt

9

ops/s

array

MEDIA_CONTENT

False

2186565.22658

Hession_deserialize

compatible

thrpt

9

ops/s

array

MEDIA_CONTENT

False

146023.282159

Kryo_deserialize

compatible

thrpt

9

ops/s

array

MEDIA_CONTENT

False

242556.254663

为什么Fury这么快

我们首先看一下决定序列化性能的关键点,然后分析Hessian和Kryo性能的主要瓶颈,再展开讲一下Fury的核心原理,对比说明Fury为什么可以做到数量级的性能提升。

序列化性能的关键

序列化的性能一般由两部分决定:

  • 在不涉及大的binary数据拷贝情况下, 将对象图的对象字段值读取出来以及写入buffer的开销
  • 在大的binary数据拷贝情况下, 内存拷贝的次数

对于第一点,目前的序列化框架都是通过循环+虚方法调用来进行实现的,即遍历对象树的每个字段,获取字段的序列化器,然后调用序列化器的write/read方法来完成序列化和反序列化。这样做存在的主要问题是大量虚方法调用无法内联,而对象树的叶子节点一般都是基本类型和字符串,虚方法调用的开销远远超过了将这些值写入Buffer的开销

对于这一点,Fury通过JIT代码生成,在运行时为每个类型创建一个序列化器类,在生成类里面直接调用对应的读写方法,并手动执行循环展开和常量折叠,保证整个序列化过程可以被完全内联,即序列化执行的实际代码从虚方法调用变成了可内联的具体方法调用,以及避免不必要的条件分支,从而避免了所有额外开销:

double f99 = struct0911_1538261.f99;
io.fury.memory.MemoryBuffer.unsafePutDouble(arr, value1, f99);
long f97 = struct0911_1538261.f97;
io.fury.memory.MemoryBuffer.unsafePutLong(arr, (value1 + 8L), f97);
double f95 = struct0911_1538261.f95;
io.fury.memory.MemoryBuffer.unsafePutDouble(arr, (value1 + 16L), f95);
java.lang.String title = media2.title;
if ((title == null)) {
  memoryBuffer.writeByte(((byte)-3));
} else {
  memoryBuffer.writeByte(((byte)0));
  strSerializer.writeJavaString(memoryBuffer, title);
}
io.fury.benchmark.data.Media.Player player = media2.player;
if ((player == null)) {
  memoryBuffer.writeByte(((byte)-3));
} else {
  memoryBuffer.writeByte(((byte)0));
  enumSerializer.write(memoryBuffer, player);
}

对于第二点,Fury通过支持多路输出的Out-of-band零拷贝协议以及堆外内存读写避免了中间过程当中的所有拷贝开销。

类型前后兼容的序列化里面,还存在两个额外的开销:

  • 字段名称编码读写的开销。在不考虑类型兼容的情况下,我们可以在序列化时 对所有字段进行排序,然后按照顺序对每个字段进行序列化和反序列化,这样就可以避免把字段名信息写到数据里面。但如果考虑类型前后兼容,就需要把字段名称编码到数据里面,然后在反序列化时根据这个信息进行比较是否跳过这个字段还是将字段值反序列化并设置到对象上面。
  • 字段类型信息读写的开销。在不考虑类型兼容的情况下,我们可以在序列化时 对final类型字段跳过写类型信息,但在类型兼容情况下,由于接收端可能缺乏某些字段,因此无法提前知道这些字段的类型信息,如果不把 字段类型信息编码到数据里面,就无法反序列化和跳过该字段。

对于复杂类型,字段名称和类型信息的序列化开销相对于字段数据本身的开销来说要小不少,对序列化影响较小;但由于对象图的数据本身展开下来最终还是基本类型和字符串组成的数据,而对于这些字段的序列化,如果序列化协议不够高效,字段名称和类型的开销将比序列化字段值本身还要大,导致这里也成为序列化的瓶颈。

接下来我们分析一下Hessian和Kryo的源码,来验证我们的假设。

Hessian性能瓶颈分析

序列化源码分析

总体流程:Hessian在序列化自定义对象时会先写入所有字段名称,然后再遍历对象字段调用对应的序列化器,序列化对应的数据。

序列化字段名称:

  private void writeDefinition20(AbstractHessianOutput out)
    throws IOException
  {
    out.writeClassFieldLength(_fields.length);

    for (int i = 0; i < _fields.length; i++) {
      Field field = _fields[i];

      out.writeString(field.getName());
    }
  }

序列化对象字段数据:

final public void writeInstance(Object obj, AbstractHessianOutput out)
    throws IOException
  {
    FieldSerializer []fieldSerializers = _fieldSerializers;
      int length = fieldSerializers.length;
      
      for (int i = 0; i < length; i++) {
        fieldSerializers[i].serialize(out, obj);
      }
  }

反序列化源码分析

反序列化过程与序列化类似,首先读取类名称,然后读取所有字段名称,再反序列化所有字段值并设置到目标对象上面。

反序列化字段名称:

private void readObjectDefinition(Class cl)
    throws IOException {
    String type = readString();
    int len = readInt();
    SerializerFactory factory = findSerializerFactory();
    Deserializer reader = factory.getObjectDeserializer(type, null);
    Object []fields = reader.createFields(len);
    String []fieldNames = new String[len];
    for (int i = 0; i < len; i++) {
      String name = readString();
      fields[i] = reader.createField(name);
      fieldNames[i] = name;
    }
    ObjectDefinition def = new ObjectDefinition(type, reader, fields, fieldNames);
    _classDefs.add(def);
  }

反序列化字段值,当遇到不存在的字段时,跳过该字段的数据:

 public Object readObject(AbstractHessianInput in,
                           Object obj,
                           String []fieldNames)  throws IOException
  {
    try {
      int ref = in.addRef(obj);
      for (String fieldName : fieldNames) {
        FieldDeserializer2 reader = _fieldMap.get(fieldName);  
        if (reader != null) reader.deserialize(in, obj);
        else in.readObject();
      }
      Object resolve = resolve(in, obj);
      if (obj != resolve) in.setRef(ref, resolve);
      return resolve;
    } catch (IOException e) {
      throw e;
    } catch (Exception e) {
      throw new IOExceptionWrapper(obj.getClass().getName() + ":" + e, e);
    }
  }

另外Hessian的字段名称编码没有考虑父子类同名字段的场景,在遇到此类case序列化会报错。

性能瓶颈总结

可以看到Hessian的瓶颈主要在于:

  • 序列化时需要对每个对象值查询序列化器,这部分存在hashmap查找开销
  • 序列化时调用序列化器的serialize方法,这部分存在虚方法调用开销。对于基本类型字段,还存在装箱开销
  • 反序列化时需要读取所有字段名称,这部分存在String对象的序列化以及创建开销
  • 根据字段名称从hashmap里面查询序列化器的开销
  • 调用序列化器的deserialize方法,这部分存在虚方法调用开销。对于基本类型字段,还存在装箱开销。

Kryo性能瓶颈分析

Kryo的类型兼容主要是通过com.esotericsoftware.kryo.serializers.CompatibleFieldSerializer来实现的,尽管kryo也提供了一些基于注解标注增量字段的方式,但对应用存在侵入性需要改造业务代码,因此这里不做展开,

CompatibleFieldSerializer的序列化总体思路跟hessian类似,线上序列化所有字段名称,然后再序列化所有字段值。

序列化源码分析

不同于Hessian的序列化,kryo会把每个字段单独写到一个chunk里面,这样在反序列化的时候如果发现某个字段不存在,就可以直接跳过这个chunk的反序列化。

public void write (Kryo kryo, Output output, T object) {
    CachedField[] fields = getFields();
    ObjectMap context = kryo.getGraphContext();
    if (!context.containsKey(this)) {
        context.put(this, null);
        if (TRACE) trace("kryo", "Write " + fields.length + " field names.");
        output.writeVarInt(fields.length, true);
        for (int i = 0, n = fields.length; i < n; i++)
            output.writeString(getCachedFieldName(fields[i]));
    }
    OutputChunked outputChunked = new OutputChunked(output, 1024);
    for (int i = 0, n = fields.length; i < n; i++) {
        fields[i].write(outputChunked, object);
        outputChunked.endChunks();
    }
}

反序列化源码分析

反序列化首先会读取所有字段名称:

String[] names = new String[length];
for (int i = 0; i < length; i++)
    names[i] = input.readString();

然后根据字段名称在当前类里面找到对应的序列化器,kryo把字段的序列化器是存储在一个CachedField的数据结构里面的:

public static abstract class CachedField {
    Field field;
    FieldAccess access;
    Class valueClass;
    Serializer serializer;
    boolean canBeNull;
    int accessIndex = -1;
    long offset = -1;
    boolean varIntsEnabled = true;
}

kryo在字段数量小于32的情况下会执行线性搜索把字段名称跟当前对象类型的字段序列化器(CachedField)进行匹配,在字段数量大于32的情况下会执行二分查找进行字段匹配:

if (length < 32) {
    outer:
        for (int i = 0; i < length; i++) {
            String schemaName = names[i];
            for (int ii = 0, nn = allFields.length; ii < nn; ii++) {
                if (getCachedFieldName(allFields[ii]).equals(schemaName)) {
                    fields[i] = allFields[ii];
                    continue outer;
                }
            }
            if (TRACE) trace("kryo", "Ignore obsolete field: " + schemaName);
        }
} else {
    // binary search for schemaName
    int low, mid, high;
    int compare;
    outerBinarySearch:
        for (int i = 0; i < length; i++) {
            String schemaName = names[i];
            low = 0;
            high = length - 1;
            while (low <= high) {
                mid = (low + high) >>> 1;
                String midVal = getCachedFieldName(allFields[mid]);
                compare = schemaName.compareTo(midVal);
                if (compare < 0) { high = mid - 1;}
                else if (compare > 0) {low = mid + 1;}
                else {
                    fields[i] = allFields[mid];
                    continue outerBinarySearch;
                }
            }
            if (TRACE) trace("kryo", "Ignore obsolete field: " + schemaName);
        }
}

个人感觉这块的代码可以优化,通过在发送端对字段做预排序,然后接收端可以执行更少的字段匹配操作。

在完成字段匹配后,kryo就会根据每个字段的序列化器反序列化数据并设置到对象上面:

InputChunked inputChunked = new InputChunked(input, 1024);
		boolean hasGenerics = getGenerics() != null;
for (int i = 0, n = fields.length; i < n; i++) {
    CachedField cachedField = fields[i];
    if (cachedField != null && hasGenerics) {
        // Generic type used to instantiate this field could have
        // been changed in the meantime. Therefore take the most
        // up-to-date definition of a field
        cachedField = getField(getCachedFieldName(cachedField));
    }
    if (cachedField == null) {
        if (TRACE) trace("kryo", "Skip obsolete field.");
        inputChunked.nextChunks();
        continue;
    }
    cachedField.read(inputChunked, object);
    inputChunked.nextChunks();
}

可以发现对于不存在的字段,kryo直接通过nextChunks方法跳过了反序列化。如果跳过的数据跟序列化流的其它数据存在相互引用关系,那么这里跳过反序列化就会导致后面的所有序列化全部出问题。同时Kryo也不支持父子类有同名字段的序列化。因此Kryo的类型兼容序列化实现是存在很大的限制的。

性能瓶颈总结

Kryo的瓶颈主要在于:

  • 序列化时需要对每个对象值查询序列化器,这部分存在hashmap查找开销
  • 序列化时调用序列化器的serialize方法,这部分存在虚方法调用开销。对于基本类型字段,还存在装箱开销。
  • 序列化时数据分chunk进行序列化,这部分存在chunk分配的开销
  • 反序列化时需要读取所有字段名称,这部分存在String对象的序列化以及创建开销
  • 根据字段名称线性查找或者二分查找匹配字段序列化器的开销,这个过程还存在string比较的开销。
  • 调用序列化器的deserialize方法,这部分存在虚方法调用开销。对于基本类型字段,还存在装箱开销。
  • 反序列化时数据分chunk进行反序列化,这部分存在chunk分配的开销

Fury类型兼容序列化协议

在类型兼容模式下,Fury会把字段分为四种类型:

  • 可以用四字节表示类型信息的字段:字段类型是final类型,且class id小于63,占用一个byte,字段名称占用三个byte;
  • 可以用8字节表示类型信息的字段:字段类型是final类型,且class id小于127,占用一个byte,字段名称占用7个byte。每个字符使用6个bit表示,七个byte可以表示9个字符;
  • 其它字段类型是final类型的字段:字段名称和字段类型一起编码;
  • 其它字段类型是非final类型的字段:字段名称和字段类型分开编码

如果父子类出现同名字段,则把classname作为字段名的一部分一起编码。然后将这些字段按照字段名称编码的整数值进行从小到大排序,在序列化时,首先写入编码的整数值,然后再写入具体的字段数据。反序列化时就可以直接进行整数大小判断字节流的当前字段是否存在于当前类型的字段当中,是在该类型第一个字段的前面,还是中间的不存在的字段,还是在当前类型最后一个该类型字段之后。然后根据对应的情况决定是序列化还是跳过序列化。具体细节可以看下面反序列化代码。

这样的话就避免了Hessian和Kryo反序列化字段名String的开销,以及Hash查找和二分搜索的开销。

Fury类型兼容序列化器实现

解释执行模式

序列化核心代码:

for (FieldResolver.FieldInfo fieldInfo : fieldResolver.getEmbedTypes4Fields()) {
      buffer.writeInt((int) fieldInfo.getEncodedFieldInfo());
      fieldInfo.getFieldWriter().write(buffer, value);
    }
for (FieldResolver.FieldInfo fieldInfo : fieldResolver.getEmbedTypes9Fields()) {
  buffer.writeLong(fieldInfo.getEncodedFieldInfo());
  fieldInfo.getFieldWriter().write(buffer, value);
}
... // 其它类型字段的读写类似
buffer.writeLong(fieldResolver.getEndTag());

反序列化核心代码:

readEmbedTypes4Fields(buffer, obj);
long tmp = buffer.readInt();
partFieldInfo = tmp << 32 | (partFieldInfo & 0x00000000ffffffffL);
readEmbedTypes9Fields(buffer, partFieldInfo, obj);
... // 其它类型字段的读写类似

反序列化可以用4字节表示字段信息的字段,核心逻辑就是先跳过小于在当前类型第一个该类字段之前的字段,然后依次找到每一个在当前类型的字段对应的数据进行反序列化,最后再跳过在当前类型最后一个该类字段的所有字段:

  private long readEmbedTypes4Fields(MemoryBuffer buffer, Object obj) {
    long partFieldInfo = buffer.readInt();
    FieldResolver.FieldInfo[] embedTypes4Fields = fieldResolver.getEmbedTypes4Fields();
    if (embedTypes4Fields.length > 0) {
      long minFieldInfo = embedTypes4Fields[0].getEncodedFieldInfo();
      while ((partFieldInfo & 0b11) == FieldResolver.EMBED_TYPES_4_FLAG
          && partFieldInfo < minFieldInfo) {
        long part = fieldResolver.skipDataBy4(buffer, (int) partFieldInfo);
        if (part != partFieldInfo) {
          return part;
        }
        partFieldInfo = buffer.readInt();
      }
      for (int i = 0; i < embedTypes4Fields.length; i++) {
        FieldResolver.FieldInfo fieldInfo = embedTypes4Fields[i];
        long encodedFieldInfo = fieldInfo.getEncodedFieldInfo();
        if (encodedFieldInfo == partFieldInfo) {
          fieldInfo.getFieldWriter().read(buffer, obj);
          partFieldInfo = buffer.readInt();
        } else {
          if ((partFieldInfo & 0b11) == FieldResolver.EMBED_TYPES_4_FLAG) {
            if (partFieldInfo < encodedFieldInfo) {
              long part = fieldResolver.skipDataBy4(buffer, (int) partFieldInfo);
              if (part != partFieldInfo) {
                return part;
              }
              partFieldInfo = buffer.readInt();
              i--;
            }
          } else {
            break;
          }
        }
      }
    }
    while ((partFieldInfo & 0b11) == FieldResolver.EMBED_TYPES_4_FLAG) {
      long part = fieldResolver.skipDataBy4(buffer, (int) partFieldInfo);
      if (part != partFieldInfo) {
        return part;
      }
      partFieldInfo = buffer.readInt();
    }
    return partFieldInfo;
  }

JIT执行模式

JIT执行模式就是把上面的解释执行模式用运行时代码生成重新实现了一遍,避免了所有虚方法调用开销,以及不必要的条件分支。感兴趣的同学可以查询Fury的源码CompatibleCodecBuilder

下面给出JIT部分源码作为参考:

for (FieldResolver.FieldInfo fieldInfo : fieldResolver.getEmbedTypes4Fields()) {
  expressions.add(
      new Invoke(
          buffer,
          "writeInt",
          new Literal((int) fieldInfo.getEncodedFieldInfo(), PRIMITIVE_INT_TYPE)));
  expressions.add(writeEmbedTypeFieldValue(bean, buffer, fieldInfo));
}
for (FieldResolver.FieldInfo fieldInfo : fieldResolver.getEmbedTypes9Fields()) {
  expressions.add(
      new Invoke(
          buffer,
          "writeLong",
          new Literal(fieldInfo.getEncodedFieldInfo(), PRIMITIVE_LONG_TYPE)));
  expressions.add(writeEmbedTypeFieldValue(bean, buffer, fieldInfo));
}
  private void readEmbedTypes4Fields(
      Expression.Reference buffer,
      ListExpression expressionBuilder,
      Expression bean,
      Expression partFieldInfo) {
    FieldInfo[] embedTypes4Fields = fieldResolver.getEmbedTypes4Fields();
    if (embedTypes4Fields.length > 0) {
      long minFieldInfo = embedTypes4Fields[0].getEncodedFieldInfo();
      expressionBuilder.add(skipDataBy4Until(bean, buffer, partFieldInfo, minFieldInfo, false));
      groupFields(embedTypes4Fields, 3)
          .forEach(
              group -> {
                Expression invokeGeneratedRead =
                    CodecOptimizer.invokeGenerated(
                        ctx,
                        () -> {
                          ListExpression groupExpressions = new ListExpression();
                          for (FieldInfo fieldInfo : group) {
                            long encodedFieldInfo = fieldInfo.getEncodedFieldInfo();
                            Descriptor descriptor = createDescriptor(fieldInfo);
                            Expression readField =
                                readEmbedTypes4(bean, buffer, descriptor, partFieldInfo);
                            Expression tryReadField =
                                new ListExpression(
                                    skipDataBy4Until(
                                        bean, buffer, partFieldInfo, encodedFieldInfo, true),
                                    new If(
                                        eq(
                                            partFieldInfo,
                                            new Literal(encodedFieldInfo, PRIMITIVE_LONG_TYPE)),
                                        readEmbedTypes4(bean, buffer, descriptor, partFieldInfo)));
                            groupExpressions.add(
                                new If(
                                    eq(
                                        partFieldInfo,
                                        new Literal(encodedFieldInfo, PRIMITIVE_LONG_TYPE)),
                                    readField,
                                    tryReadField,
                                    false,
                                    PRIMITIVE_VOID_TYPE));
                          }
                          groupExpressions.add(new Return(partFieldInfo));
                          return groupExpressions;
                        },
                        "readEmbedTypes4Fields",
                        true);
                expressionBuilder.add(
                    new Assign(partFieldInfo, invokeGeneratedRead),
                    new If(eq(partFieldInfo, endTagLiteral), new Return(bean)));
              });
    }
    expressionBuilder.add(skipField4End(bean, buffer, partFieldInfo));
  }

对应生成代码示例:

buffer.writeInt(-1542455263);
int f100 = struct0911_1538411.f100;
buffer.writeInt(f100);
buffer.writeInt(-1542454999);
long f101 = struct0911_1538411.f101;
buffer.writeLong(f101);
buffer.writeInt(-1542454747);
  long partFieldInfo = buffer.readInt();
  while ((((partFieldInfo & 3L) == ((byte)1)) && (partFieldInfo < -1542455263L))) {
    if (fieldResolver.skipDataBy4(buffer, ((int)partFieldInfo))) {
        return struct0911_1538412;
    }
    partFieldInfo = buffer.readInt();
  }
  if ((partFieldInfo == -1542455263L)) {
      int value1 = buffer.readInt();
      struct0911_1538412.f100 = value1;
      partFieldInfo = buffer.readInt();
  } else {
      while ((((partFieldInfo & 3L) == ((byte)1)) && (partFieldInfo < -1542455263L))) {
        if (fieldResolver.skipDataBy4(buffer, ((int)partFieldInfo))) {
            return struct0911_1538412;
        }
        partFieldInfo = buffer.readInt();
      }
      if ((partFieldInfo == -1542455263L)) {
          int value2 = buffer.readInt();
          struct0911_1538412.f100 = value2;
          partFieldInfo = buffer.readInt();
      }
  }

由于生成的代码比较复杂,如果对象字段数量较多,就可能出现方法体过大无法被Java JIT编译/内联的情况,下面是摘取的一些编译日志:

因此Fury在这里也实现了一套JIT动态优化框架,在运行时自动将大方法递归拆分成小方法,保证所有生成代码都可以被编译和内联,从而避免掉所有方法调用开销。详细优化流程可以参考文章

下面是开启JIT动态方法拆分优化前后的性能对比。

开启JIT动态方法拆分优化前的性能:

Benchmark                                             (bufferType)  (objectType)  (references)   Mode  Cnt       Score       Error  Units
UserTypeDeserializeSuite.fury_deserialize_compatible         array        STRUCT         false  thrpt    9  196757.168 ± 60328.206  ops/s
UserTypeDeserializeSuite.kryo_deserialize_compatible         array        STRUCT         false  thrpt    9  101101.512 ± 11280.283  ops/s

开启JIT动态方法拆分优化后的性能:

Benchmark                                             (bufferType)  (objectType)  (references)   Mode  Cnt        Score        Error  Units
UserTypeDeserializeSuite.fury_deserialize_compatible         array        STRUCT         false  thrpt    9  3585680.982 ± 201328.514  ops/s
UserTypeDeserializeSuite.kryo_deserialize_compatible         array        STRUCT         false  thrpt    9   104989.188 ±  20386.036  ops/s

可视化对比(左边是开启JIT动态方法拆分优化前,右边是开启JIT动态方法拆分优化后。纵轴越小越好):

后续规划

自从今年七月份Fury正式对外发布以来,Fury获得了大量用户的关注和贡献,感谢大家对Fury的支持。接下来我们会继续优化Fury,致力于为公司和业界提供最快最好的序列化框架。近期我们会把重心放在以下方向,欢迎感兴趣的同学一起参与进来:

  • Java对象图序列化
  • 类型前后兼容模式:
  • 支持异步编译:目前该模式已经在解释执行模式和JIT执行模式完成了协议兼容,需要支持该模式下的异步编译。即先用解释模式序列化器进行序列化,等JIT模式序列化器完成异步编译后,再自动切换到JIT模式。
    • 支持基于嵌套泛型信息的优化。目前对于List>这种类型的字段,fury在类型前后兼容模式下关闭了JIT嵌套泛型信息的优化,从而保证跟解释执行模式的一致性,因此会导致一些不必要的虚方法调用。对于这类case,首先需要支持在解释模式下读写嵌套泛型信息,然后利用嵌套泛型加速解释执行模式的性能,接下来再打开JIT模式的嵌套泛型信息优化。
  • 类型强一致模式
  • FallbackSerializer支持泛型信息:降低序列化大小开销,同时优化性能。参考io.fury.serializers.ComplexObjectSerializer
    • 协议兼容:解释执行模式JIT模式兼容,FallbackSerializer跟SeqCodecBuilder在协议层面保持一致,便于接下来实现类型强一致模式的异步编译。
    • 支持异步编译
  • 分层编译模式。目前目标类型是private时,由于编译代码时无法访问目标类型,因此这类对象会使用解释模式序列化器(FallbackSerializer/CompatibleSerialzier)进行序列化。而所有private自定义类型都会走到这里,导致JVM无法通过JIT对不同类型进行代码路径优化。如果在运行时针对每个使用FallbackSerializer/CompatibleSerialzier的类型,异步创建一个序列化器子类,那么JVM就可以根据该子类进行JIT代码路径优化,在解释执行模式下达到更高的性能。即对于无法JIT序列化的类型,先使用FallbackSerializer/CompatibleSerialzier进行序列化,同时异步创建FallbackSerializer/CompatibleSerialzier子类型,创建完成后切换成子类型进行序列化。
  • JIT编译性能优化。对Fury JIT框架进行性能Profiling,分析性能瓶颈并进行优化,缩短编译耗时。
  • 自定义JDK序列化性能优化:目前对于自定义了writeObject/readObject/writeResolve/readReplace的对象,Fury会通过ObjectOutputStream转发到JDK序列化,保证行为的一致性。但这样会影响序列化的性能和最终大小,因此Fury需要自己实现一套兼容JDK序列化行为的序列化器。
  • Binary Format
  • Python Row Format
  • 支持根节点是List
    • 支持根节点是Dict
  • Java Row Format
  • 支持根节点是Dict
  • C++ Row Format
  • 实现对象自动转换成row Format:通过定义一套宏注册对象字段信息,然后基于注册的字段信息实现自动将对象转换为row Format
    • 使用编译时反射实现上述自动转换能力
  • GoLang Row Format
  • 行存实现
    • 行存自动转换Apache Arrow列存
  • Rust Row Format
  • 行存实现
    • 行存自动转换Apache Arrow列存
  • TypeScript/NodeJS Row Format
  • 行存实现
    • 行存自动转换Apache Arrow列存
  • Python对象图序列化
  • 使用Cython优化对象引用解析性能
  • 使用Cython优化类型和序列化器查询性能
  • 优化String序列化性能
  • 直接使用Python C-API减少不必要的Cython生成的检查
  • 提供Mac M1的发布包
  • 提供纯Python的实现,便于debug测试以及避免未发布特定机器的wheel包导致fury在这些机器不可用
  • GoLang对象图序列化
  • 性能优化
  • 支持更多类型
  • Rust对象图序列化
  • TypeScript/NodeJS对象图序列化
  • 更多的测试UT
  • 用户手册完善
  • 编写开发指南
  • 代码注释/文档完善

联系我们

如果想进一步了解Fury,或者对Fury有任何使用问题,欢迎钉钉私聊和通过下方二维码加入Fury用户群(群号:35683646):

Fury系列相关文章

目录
相关文章
|
4月前
|
Java
JDK序列化原理问题之Hessian框架不支持writeObject/readObject方法如何解决
JDK序列化原理问题之Hessian框架不支持writeObject/readObject方法如何解决
|
4月前
|
缓存 Java
JDK序列化原理问题之Fury如何实现与JDK序列化100%兼容的如何解决
JDK序列化原理问题之Fury如何实现与JDK序列化100%兼容的如何解决
|
消息中间件 Dubbo Java
Simple RPC - 02 通用高性能序列化和反序列化设计与实现
Simple RPC - 02 通用高性能序列化和反序列化设计与实现
67 2
|
自然语言处理 Java 测试技术
序列化性能之巅:使用Fury替换Protobuf/Flatbuffers实现10倍加速
问题背景Protobuf/Flatbuffers是业界广泛使用的序列化库,服务于大量的业务场景。但随着业务场景的复杂化,Protobuf/Flatbuffers逐渐不能满足性能需求开始成为系统瓶颈,在这种情况下,用户不得不手写大量序列化逻辑来进行极致性能优化,但这带来了三个问题:大量字段手写序列化逻辑冗长易出错;手写重复序列化逻辑开发效率低下;难以处理发送端和接收端字段变更的前后兼容性问题;这里将
1909 0
|
存储 分布式计算 JavaScript
Fury系列(四):一个比Kryo/Hessian快30~40倍的类型前后兼容序列化器
问题背景类型前后兼容是复杂业务场景序列化的常见需求。在快速迭代的业务场景当中,读写端经常发生对象字段发生变更:在线应用场景:线上SOFA/HSF应用提供服务给多个调用方,服务的滚动升级以及各个调用方独立更新都可能导致对象类型不一致的情况;在线服务场景:在线服务框架常驻不更改对象类型,但调用方业务逻辑变动独立更新导致对象字段跟服务端不一致;对象持久化场景:对象数据序列化后持久化写入存储(如Spark
1614 2
Fury系列(四):一个比Kryo/Hessian快30~40倍的类型前后兼容序列化器
|
存储 安全 NoSQL
Java高性能序列化工具Kryo序列化
Java高性能序列化工具Kryo序列化
321 0
Java高性能序列化工具Kryo序列化
|
Dubbo 算法 安全
Java序列化案例demo(包含Kryo、JDK原生、Protobuf、ProtoStuff以及hessian)(二)
Java序列化案例demo(包含Kryo、JDK原生、Protobuf、ProtoStuff以及hessian)(二)
Java序列化案例demo(包含Kryo、JDK原生、Protobuf、ProtoStuff以及hessian)(二)
|
SQL 存储 Java
Java序列化案例demo(包含Kryo、JDK原生、Protobuf、ProtoStuff以及hessian)(一)
Java序列化案例demo(包含Kryo、JDK原生、Protobuf、ProtoStuff以及hessian)(一)
Java序列化案例demo(包含Kryo、JDK原生、Protobuf、ProtoStuff以及hessian)(一)
|
C#
C#高性能二进制序列化
二进制序列化可以方便快捷的将对象进行持久化或者网络传输,并且体积小、性能高,应用面甚至还要高于json的序列化;开始之前,先来看看dotcore/dotne自带的二进制序列化:C#中对象序列化和反序列化一般是通过BinaryFormatter类来实现的二进制序列化、反序列化的。
2012 0