Kryo 和 FST 序列化

在 Dubbo 中使用高效的 Java 序列化(Kryo 和 FST)

目录

  • 序列化谈话
  • 启用 Kryo 和 FST
  • 注册要序列化的类
  • 无参数构造函数和 Serializable 接口
  • 序列化性能分析和测试
    • 测试环境
    • 测试脚本
    • 比较 Dubbo RPC 中不同序列化生成的字节大小
    • 比较 Dubbo RPC 中不同序列化的响应时间和吞吐量
  • 未来

序列化谈话

dubbo RPC 是 dubbo 系统中核心高性能、高吞吐量的远程调用方式,我喜欢称之为多路复用 TCP 长连接调用。简单来说

  • 长连接:避免每次都需要创建新的 TCP 连接,提高调用响应速度
  • 多路复用:单个 TCP 连接可以交替传输多个请求和响应消息,减少连接的等待空闲时间,从而在相同并发数下减少网络连接数量,提高系统吞吐量。

dubbo RPC 主要用于两个 dubbo 系统之间的远程调用,特别适合高并发、小数据量的互联网场景。

序列化在远程调用的响应速度、吞吐量和网络带宽消耗方面也起着至关重要的作用,是我们提高分布式系统性能的最关键因素之一。

在 dubbo RPC 中,同时支持多种序列化方式,例如

  1. Dubbo 序列化:阿里还没有开发出成熟高效的 java 序列化实现,阿里不建议在生产环境中使用它
  2. Hessian2 序列化:Hessian 是一种跨语言高效的二进制序列化方式。但这里实际上不是原生的 hessian2 序列化,而是阿里修改过的 hessian lite,是 dubbo RPC 默认启用的序列化方式
  3. JSON 序列化:目前有两种实现,一种是使用阿里的 fastjson 库,另一种是使用 dubbo 实现的 simple json 库,但实现并不特别成熟,json 的文本序列序列化性能普遍不如上面两种二进制序列化。
  4. Java 序列化:主要通过使用 JDK 自带的 Java 序列化实现,性能并不理想。

总的来说,四种主要序列化方式的性能从上到下依次降低。对于追求高性能远程调用的 dubbo RPC 来说,实际上只有 1 和 2 两种高效的序列化方式更适合,而第一种 dubbo 序列化还不太成熟,所以实际上只有 2 可用。所以 dubbo RPC 默认使用 hessian2 序列化。

但 hessian 是一个比较老的序列化实现,而且是跨语言的,所以没有针对 java 单独优化。实际上,dubbo RPC 是从 Java 到 Java 的远程调用。实际上,没有必要采用跨语言序列化(当然,不排除跨语言序列化)。

近年来,各种高效的序列化方式层出不穷,不断刷新着序列化性能的上限,最典型的包括

  • 专门针对 Java 语言:Kryo、FST 等
  • 跨语言:Protostuff、ProtoBuf、Thrift、Avro、MsgPack 等

这些序列化方式的性能大多明显优于 hessian2(甚至包括不成熟的 dubbo 序列化)。

鉴于此,我们为 dubbo 引入了两种高效的 Java 序列化实现,Kryo 和 FST,逐步替换 hessian2。

其中,Kryo 是一个非常成熟的序列化实现,已经在 Twitter、Groupon、Yahoo 和许多著名的开源项目(如 Hive 和 Storm)中得到广泛应用。而 FST 是一个比较新的序列化实现,目前还没有足够成熟的应用案例,但我认为它仍然很有前景。

在生产环境应用中,我建议目前优先选择 Kryo。

启用 Kryo 和 FST

使用 Kryo 和 FST 非常简单,只需先添加相应的依赖项:更多插件:Dubbo SPI 扩展

<dependency>
   <groupId>org.apache.dubbo.extensions</groupId>
   <artifactId>dubbo-serialization-kryo</artifactId>
   <version>1.0.0</version>
</dependency>
<dependency>
   <groupId>org.apache.dubbo.extensions</groupId>
   <artifactId>dubbo-serialization-fst</artifactId>
   <version>1.0.0</version>
</dependency>

然后在 dubbo RPC 的 XML 配置中添加一个属性

<dubbo:protocol name="dubbo" serialization="kryo"/>
<dubbo:protocol name="dubbo" serialization="fst"/>

注册要序列化的类

为了使 Kryo 和 FST 在 Dubbo 系统中充分发挥高性能,最好注册需要序列化的类。例如,我们可以实现以下回调接口

public class SerializationOptimizerImpl implements SerializationOptimizer {

    public Collection<Class> getSerializableClasses() {
        List<Class> classes = new LinkedList<Class>();
        classes.add(BidRequest.class);
        classes. add(BidResponse. class);
        classes. add(Device. class);
        classes. add(Geo. class);
        classes. add(Impression. class);
        classes.add(SeatBid.class);
        return classes;
    }
}

然后在 XML 配置中添加

<dubbo:protocol name="dubbo" serialization="kryo" optimizer="org.apache.dubbo.demo.SerializationOptimizerImpl"/>

注册这些类后,序列化性能可能会大大提高,特别是对于少量嵌套对象。

当然,在序列化一个类时,可能会有很多类级联,例如 Java 集合类。针对这种情况,我们已经自动注册了 JDK 中的常用类,因此您无需重复注册(当然,重复注册也没有效果),包括

Gregorian Calendar
InvocationHandler
BigDecimal
BigInteger
pattern
BitSet
URIs
UUID
HashMap
ArrayList
LinkedList
HashSet
TreeSet
Hashtable
date
Calendar
ConcurrentHashMap
SimpleDateFormat
vector
BitSet
StringBuffer
String Builder
object
Object[]
String[]
byte[]
char[]
int[]
float[]
double[]

由于注册要序列化的类只是为了性能优化,所以即使忘记注册某些类也没有关系。事实上,即使不注册任何类,Kryo 和 FST 的性能通常也优于 hessian 和 dubbo 序列化。

当然,有人可能会问为什么不使用配置文件来注册这些类?这是因为通常需要注册大量的类,导致配置文件过长;而且没有良好的 IDE 支持,编写和重构配置文件比编写 Java 类麻烦得多;最后,这些注册的类通常在项目编译打包后不需要进行动态修改。

此外,有些人也会认为手动注册序列化类是一项比较繁琐的工作,能否用注解标记,然后系统自动发现并注册。但注解在这里的局限性在于,它只能用来标记可以修改的类,而序列化中引用的很多类很可能是你无法修改的(比如第三方库或 JDK 系统类或其他项目的类)。此外,添加注解毕竟会稍微“污染”代码,使应用程序代码对框架的依赖性稍微高一些。

除了注解之外,我们还可以考虑其他自动注册序列化类的方法,比如扫描类路径,自动发现实现 Serializable 接口的类(甚至包括 Externalizable)并注册它们。当然,我们知道类路径上可能有很多 Serializable 类,所以我们也可以考虑使用包前缀来一定程度上限制扫描范围。

当然,在自动注册机制中,尤其需要考虑如何确保服务提供者和消费者以相同的顺序(或 ID)注册类,以避免错位。毕竟,两端可以发现和注册的类数量可能不同。

无参数构造函数和 Serializable 接口

如果要序列化的类不包含无参构造函数,Kryo 序列化的性能会大大下降,因为此时我们会使用 Java 的序列化在底层透明地替换 Kryo 的序列化。因此,建议尽可能为每个序列化类添加一个无参构造函数(当然,如果一个 Java 类没有自定义构造函数,它默认会拥有一个无参构造函数)。

此外,Kryo 和 FST 不需要序列化类实现 Serializable 接口,但我们仍然建议每个序列化类都实现它,因为这可以保持与 Java 序列化和 dubbo 序列化的兼容性。此外,它也使我们将来有可能采用上述一些自动注册机制。

序列化性能分析和测试

本文主要讨论序列化,但在进行性能分析和测试时,我们并没有单独处理每种序列化方法,而是将它们放在 Dubbo RPC 中进行比较,因为这更贴近实际情况。

测试环境

大致如下

  • 两台独立服务器
  • 四核 Intel(R) Xeon(R) CPU E5-2603 0 @ 1.80GHz
  • 8G 内存
  • 虚拟机之间的网络通过 100M 交换机
  • CentOS 5
  • JDK 7
  • Tomcat 7
  • JVM 参数 -server -Xms1g -Xmx1g -XX:PermSize=64M -XX:+UseConcMarkSweepGC

当然,测试环境有限,所以目前的测试结果可能不太权威和具有代表性。

测试脚本

保持与 Dubbo 自身基准测试的接近性

10 个并发客户端持续发出请求

  • 传入一个嵌套的复杂对象(但单个数据量很小),不做任何处理,原样返回
  • 传入 50K 个字符串,不做任何处理,原样返回(TODO:结果尚未列出)

运行 5 分钟的性能测试。(引用 Dubbo 自身的测试考虑:“主要考察序列化和网络 IO 的性能,因此服务器没有业务逻辑。采用 10 个并发的原因是考虑到 rpc 协议在高并发下可能存在较高的 CPU 使用率,从而导致瓶颈。”)

比较 Dubbo RPC 中不同序列化生成的字节大小

序列化生成的字节数的大小是一个相对确定的指标,它决定了远程调用的网络传输时间和带宽占用。

复杂对象的测试结果如下(数字越小越好)

序列化实现请求字节响应字节
Kryo27290
FST28896
Dubbo 序列化430186
Hessian546329
FastJson461218
Json657409
Java 序列化963630

Dubbo RPC 中不同序列化响应时间和吞吐量的比较

远程调用方法平均响应时间平均 TPS(每秒事务数)
REST: Jetty + JSON7.8061280
REST: Jetty + JSON + GZIPTODOTODO
REST: Jetty + XMLTODOTODO
REST: Jetty + XML + GZIPTODOTODO
REST: Tomcat + JSON2.0824796
REST: Netty + JSON2.1824576
Dubbo: FST1.2118244
Dubbo: kyro1.1828444
Dubbo: dubbo 序列化1.436982
Dubbo: hessian21.496701
Dubbo: fastjson1.5726352

rt

tps

测试总结

就目前的结果来看,我们可以看到 Kryo 和 FST 与 Dubbo RPC 中的原始序列化方法相比有了显著的改进,无论是在生成的字节数大小、平均响应时间还是平均 TPS 方面。

未来

将来,当 Kryo 或 FST 在 Dubbo 中足够成熟时,我们可能会将 Dubbo RPC 的默认序列化从 hessian2 更改为其中之一。


最后修改时间:2023 年 2 月 22 日:合并重构网站 (#2293) (4517e8c1c9c)