github地址:
FURY是一个基于JIT动态编译和零拷贝的多语言序列化框架,提供极致的性能和易用性
它支持主流编程语言J**A、Python C++、Golang、J**Ascript,其他语言都可以轻松扩展。
统一的多语言序列化核心能力:
高度优化的序列化基元。
零拷贝序列化支持、带外序列化协议和堆外内存读/写支持。
基于JIT动态编译技术,异步多线程在运行时自动生成序列化,优化性能,增加方法内联,缓存和消除死,减少虚拟方法调用、条件分支、哈希查找、元数据写入、内存读写等。
多协议支持:将动态序列化的灵活性和易用性与静态序列化的跨语言功能相结合。
j**a 序列化:
无缝替代 JDK Kryo Hessian,无需修改任何内容**,但提供高达 170 倍的性能,可以大大提高高性能场景下 RPC 调用、数据传输和对象持久化的效率。
100%兼容JDK序列化,原生支持JDK自定义序列化方法:WriteObject ReadObject WriteReplace、ReadResolve、ReadObjectNoData
跨语言对象图序列化:
多语言 跨语言自动序列化任意对象,无需创建 IDL 文件、手动编译架构生成**以及将对象转换为中间格式。
多语言 自动序列化跨语言的共享引用和循环引用,而无需担心数据重复或递归错误。
支持对象类型多态性,可同时序列化多个子类型对象。
行内存序列化:
它提供了缓存友好的二进制随机存取行内存格式,支持跳过序列化和部分序列化,适用于高性能计算和大规模数据传输场景。
它支持使用箭头列存储进行自动转换。
** 作者 Nanqiu * Fury 序列化框架使用示例 * @slf4jpublic 类 furyexample ** 反序列化 * param bytes 字节数组 * 返回对象 * public static object deserialize(byte bytes) public static void main(string args)。", arrays.tostring(furyexample.serialize(furyobject)))furyobject furydeserialize = (furyobject)furyexample.deserialize(bytes); log.info("反序列化:[id:{}name:{}text:{}furyobject2:{}", furydeserialize.getid(),furydeserialize.getname(),furydeserialize.gettext(),furydeserialize.getfuryobject2())FURY定义并实现了一套基本的序列化能力,基于这些能力可以快速构建不同的多语言序列化协议,并通过编译加速进行优化,实现高性能。 同时,一个协议在基础能力方面的性能优化,也可以使所有序列化协议受益。
序列化中涉及的常见操作包括:
位图位操作。
整数编解码器。
整数压缩。 字符串创建和复制优化。
字符串编码:ASCII UTF8 UTF16。
内存复制优化。
数组复制压缩优化。
元数据编码、压缩和缓存。
对于这些操作,Fury 对每种语言都做了大量的优化,结合 SIMD 指令和高级语言功能将性能推向极致,方便使用不同的协议。
在大规模数据传输场景中,一个对象图内部往往存在多个二进制缓冲区,序列化框架在序列化过程中会将这些数据写入中间缓冲区,引入多个耗时的内存副本。 Fury 借鉴了 pickle5、ray 和 arrow 的零拷贝设计,实现了一套带外序列化协议,可以直接捕获对象图中的所有二进制缓冲区,避免缓冲区的中间副本,并将序列化时的内存副本开销降低到 0。
Fury 在零拷贝序列化过程时关闭引用支持。 如图所示
目前,FURY 内置了对以下类型的零拷贝支持:
j**a:所有基本类型的数组,ByteBuffer、ArrowRecordBatch 和 VectorSchemaRoot。
python:所有数组、numpy 数组、pyarrowstable、pyarrow.recordbatch。
golang:byte slice。
用户还可以基于 Fury 界面扩展新的零拷贝类型。
对于要序列化的自定义类型对象,通常包含大量的类型信息,FURY利用这些类型信息在运行时直接生成高效的序列化,并在动态编译阶段完成大量的运行时操作,从而增加方法内联和缓存,减少虚拟方法调用、条件分支、哈希查找、元数据写入、 In-Memory 读写等,最终大大提升了序列化性能。
对于J**A语言,FURY实现了一套运行时生成框架,为序列化逻辑定义了一组算子表达式ir,在运行时基于对象类型的泛型信息进行类型推断,然后构建描述序列化逻辑的表达式树,并根据表达式树生成高效的j**a,然后在运行时通过Janino编译成字节码, 然后将其加载到用户的类加载器或 Fury 创建的类加载器中,最后通过 j**a jit 编译成一个高效的汇编。
由于 JVM JIT 会跳过大方法的编译和内联,因此 Fury 还实现了一套优化器,将大方法递归拆分为小方法,从而保证 FURY 生成的所有**都可以被编译和内联,将 JVM 的性能压缩到极致。
如图所示
同时,FURY还支持异步多线程动态编译,将不同序列化器的**生成任务提交到线程池中执行,并在编译完成前以解释模式执行,从而保证不会出现序列化故障,也无需提前预热所有类型的序列化。
由于序列化需要对每种编程语言的对象进行紧密的操作,而编程语言不暴露内存模型的底层 API,因此通过原生方法调用存在较大的开销,因此无法通过 LLVM 构建统一的序列化器 JIT 框架,但需要结合每种语言中的语言特性实现特定的**生成框架和序列化器构造逻辑。
虽然 JIT 编译可以大大提高序列化效率,并基于运行时数据的统计分布重新生成更好的序列化,但 C++ Rust 等语言不支持反射,没有虚拟机,也没有提供内存模型的底层 API,因此无法通过 JIT 动态编译为此类语言生成序列化。
对于此类场景,Fury 正在实现一个 AOT 静态生成框架,该框架在编译时根据对象的架构提前生成序列化,然后使用生成的序列化进行自动序列化。 对于 Rust,Rust 的宏也将在未来编译时生成,提供更好的易用性。
在序列化自定义类型时,会对字段进行重新排序,以保证同一接口类型的字段按顺序序列化,增加缓存命中的概率,同时也促进了 CPU 指令缓存,以实现更高效的序列化。 对于基本类型字段,写入顺序按字节字段大小的降序排序,这样如果起始地址对齐,后续的读写就会在内存地址对齐位置发生,CPU执行效率更高。
基于Fury提供的多语言序列化核心能力,在此基础上构建了三种序列化协议,适用于不同的场景
J**A 序列化:适用于纯 J**A 序列化场景,性能提升 100 倍以上。
跨语言对象图序列化:非常适合面向应用程序的多语言编程,以及高性能的跨语言序列化。
行内存序列化:适用于分布式计算引擎,如Spark Flink Dories Velox Sample Stream Processing Framework Feature Storage等。
此外,用户还可以基于 Fury 的序列化能力构建自己的协议。
由于 J**A 在大数据、云原生、微服务和企业级应用中的广泛应用,J**A 序列化的性能优化可以大大降低系统延迟,提高吞吐量,降低服务器成本。
这也是为什么 FURY 对 j**a 序列化做了大量极致的性能优化,使其具备以下能力:
极致性能:利用 J**A 对象的类型和泛型信息,结合 JIT 编译和不安全的低阶操作,Fury 的性能比 JDK 高出 170 倍,比 Kryo Hessian 性能高出 50 100 倍。
100% JDK 序列化 API 兼容性:支持所有 JDK 自定义序列化方法 WriteObject、ReadObject WriteReplace、ReadResolve、ReadObjectNoData 的语义,保证在任何场景下替换 JDK 序列化的正确性。 现有的 J**A 序列化框架(如 Kryo Hessian)在这些场景中存在一定的正确性问题。
后向兼容:如果反序列化端和序列化端的类架构不一致,仍然可以正确反序列化,可以独立升级部署应用,可以独立添加或删除字段。 此外,元数据被极度压缩和共享,与类型一致模式相比,类型兼容模式几乎没有性能损失。
元数据共享:元数据(类名、字段名、最终字段类型信息等)在某个上下文(TCP连接)的多个序列化之间共享,该上下文中第一次序列化时会将这些信息发送给对等方,对等方可以根据类型信息重建同一个反序列化程序。
零拷贝支持:支持带外零拷贝和堆外内存读/写。
跨语言对象图序列化主要应用于对动态性和易用性要求较高的场景。 尽管 Protobuf Flatbuffer 等框架提供了多语言序列化功能,但仍存在一些缺点:
IDL 需要提前编写并静态编译生成**,不够动态和灵活。
生成的类不符合面向对象的设计,无法向类添加行为,并且不能直接用作多语言应用程序开发的域对象。
不支持子类序列化。 面向对象编程的主要特点是通过接口调用子类方法。 这种类型的模型也没有得到很好的支持。 虽然 flatbuffer 提供了联合,但 protobuf 提供了任意功能之一,这需要在序列化和反序列化时确定对象的类型,这与面向对象编程的设计不符。
不支持循环和共享引用,需要重新定义一组领域对象的 idl,并自行实现引用解析,然后用每种语言编写 **,实现领域对象和协议对象之间的转换,如果对象图的嵌套层数较深,则需要编写更多**。
结合以上几点,FURY实现了一套跨语言的对象图序列化协议:
多语言 跨语言自动序列化任意对象:在序列化和反序列化端定义两个类,自动将一种语言的对象序列化为另一种语言的对象,无需创建 IDL 文件、编译架构生成和转换手写内容。
多语言:跨语言自动序列化共享引用和循环引用。
支持对象类型多态性,符合面向对象的编程范式,可以同时自动对多个子类型对象进行反序列化,无需用户手动处理。
同时,我们也支持该协议的带外零拷贝
自动跨语言序列化示例:
对于高性能计算和大规模数据传输场景,数据序列化和传输往往是整个系统的性能瓶颈。 如果用户只需要读取部分数据,或根据对象的字段进行筛选,则反序列化整个数据将产生额外的开销。 因此,Fury 还提供了一组二进制数据结构,可以直接对二进制数据进行读写,避免序列化。
Apache Arrow 是一种成熟的列存储格式,支持二进制读取和写入。 但是,列存储并不能满足所有场景的需求,链路和流计算场景中的数据自然是行存储结构,当涉及到数据变化和哈希联接聚合操作时,列计算引擎也会使用行存储结构。
Spark Flink Doris Velox 等计算引擎定义了一组 rowstore 格式,不支持跨语言,只能由自己的引擎内部使用,不能在其他框架中使用。 FlatBuffer 虽然可以支持按需反序列化,但需要静态编译 schema IDL 和管理偏移量,无法满足复杂场景的动态易用需求。
因此,在早期,Fury 借用了 Spark Tungsten 和 Apache Arrow 格式,实现了一套可以随机访问的二进制行内存结构,现在实现了 J**A Python C++ 版本,实现了对二进制数据的直接读写,避免了所有序列化开销。
fury row 格式的二进制格式,如图所示:
该格式存储密集,数据对齐,缓存友好,读/写速度更快。 通过避免反序列化,可以降低 J**A GC 压力。 同时,它降低了 Python 的开销,并且由于 Python 的动态性质,Fury 的数据结构实现了 getattr getitem slice 等特殊方法,以确保行为与 Python dataclass list 对象一致,用户没有任何意识。
以下是一些j**a序列化的性能数据,其中标题为compatible的图表为受支持类型兼容性下的性能数据,不包含compatible的图表为不支持类型兼容性下的性能数据。 公平地说,所有测试狂怒都关闭了零拷贝功能。
注:本文主要参考《狂暴》作者杨朝坤的阿里云社区博文。
阅读推荐]更多精彩内容,如:
Redis 系列。
数据结构和算法。
NACOS系列。
MySQL系列。
JVM 系列。
卡夫卡系列。
请移至【南秋】个人主页参考。 内容不断更新。
关于作者]热爱科技、热爱生活的老宝贝,专注J**A领域,关注【南秋同学】带你一起学习,一起成长习