Fury:一個基於JIT動態編譯的高性能多語言原生序列化框架

簡介: Fury是一個基於JIT動態編譯的多語言原生序列化框架,支持Java/Python/Golang/C++等語言,提供全自動的對象多語言/跨語言序列化能力,以及相比於別的框架最高20~200倍的性能。

image.png

作者 | 楊朝坤(慕白)
來源 | 阿里開發者公衆號

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倍的性能。

image.png

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序列化框架;

image.png

目前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 = None
    f2: str = None
    f3: List[str] = None
    f4: Dict[pyfury.Int8Type, pyfury.Int32Type] = None
    f5: pyfury.Int8Type = None
    f6: pyfury.Int16Type = None
    f7: pyfury.Int32Type = None
    # int類型默認會按照long類型進行序列化,如果對端是更加narrow的類型,
    # 需要使用pyfury.Int32Type等進行標註
    f8: int = None  # 也可以使用pyfury.Int64Type進行標註
    f9: pyfury.Float32Type = None
    f10: float = None  # 也可以使用pyfury.Float64Type進行標註
    f11: pyfury.Int16ArrayType = None
    f12: List[pyfury.Int16Type] = None

@dataclass
class SomeClass1:
    f1: Any
    f2: 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  string
    F3  []interface{}
    F4  map[int8]int32
    F5  int8
    F6  int16
    F7  int32
    F8  int64
    F9  float32
    F10 float64
    F11 []int16
    F12 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.f2
    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 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.F2
    value.F1 = value
    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)
}

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-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/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: 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示例

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;

自動轉換Arrow

Fury 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。

image.png
image.png

性能比較(數值越小越好)

這裏給出在純Java序列化場景對比其它框架的性能測試結果。其它語言的性能測試將在後續文章當中發佈。

測試環境:

  • 操作系統:4.9.151-015.ali3000.alios7.x86_64
  • CPU型號:Intel(R) Xeon(R) Platinum 8163 CPU @ 2.50GHz
  • Byte Order:Little Endian
  • L1d cache:32K
  • L1i cache:32K
  • L2 cache:1024K
  • L3 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序列化框架在序列化到堆內存和堆外內存時的性能(數值越小越好)。

自定義類型性能對比

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;
}

序列化:

image.png
image.png

反序列化:
image.png
image.png

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;
  }
}

序列化耗時:

image.png
image.png

反序列化耗時:

image.png
image.png

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;
  }
}

序列化耗時:

image.png
image.png

反序列化耗時:
image.png
image.png

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();
    }
  }
}

序列化耗時:

image.png
image.png

反序列耗時:

image.png
image.png

堆外Buffer

除了基本類型數組,我們也測試了Java ByteBuffer的序列化性能。由於Kryo和Fst並不支持ByteBuffer序列化,同時並沒有提供直接讀寫ByteBuffer的接口,因此我們使用了byte array來模擬內存拷貝。可以看到對於堆外Buffer,Fury的序列化和反序列化耗時都是一個常量,不隨Buffer大小而增加。

序列化耗時:

image.png

image.png

反序列化耗時:

image.png
image.png

易用性比較

這裏以一個自定義類型爲例對比易用性,該類型包含常見基本類型字段以及集合類型字段,最終需要序列化的對象是一個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.proto
    • bench.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,這進一步限制了使用的靈活性。

對比Flatbuffer

Flatbuffer與protobuf一樣,也需要大量的學習成本和開發成本:

  • 安裝flatc編譯器[13],對於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;
  • 然後通過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, 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章